iOS App Download Link Extractor

Extracts the IPA download link from itms-services URLs or displays an error message, shown next to the original button.

当前为 2025-03-03 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name:zh             IPA提取助手
// @name                iOS App Download Link Extractor
// @name:zh-TW          IPA提取助手
// @namespace
// @copyright           2025, WangONC
// @version             0.7.4
// @description:zh      从 itms-services 链接中提取 IPA 下载直链或显示错误提示,并显示在原始按钮旁边
// @description         Extracts the IPA download link from itms-services URLs or displays an error message, shown next to the original button.
// @description:zh-TW   從 itms-services 連結中提取 IPA 下載直鍊或顯示錯誤提示,並顯示在原始按鈕旁邊
// @author              WangONC
// @source              https://github.com/WangONC/ios-app-download-link-extractor
// @match               *://*/*
// @grant               GM.xmlHttpRequest
// @license MIT
// @namespace https://greasyfork.org/users/1441770
// ==/UserScript==
 
(function() {
    'use strict';
 
    // 处理页面中的链接
    function processLinks() {
        let links = document.querySelectorAll('a[href^="itms-services://?action=download-manifest&url="]');
        if (links.length === 0) {
            // console.log('No matching links found');
            return;
        }
 
        for (let link of links) {
            // 检查是否已经处理过该链接
            if (link.nextElementSibling && (link.nextElementSibling.classList.contains('download-link') || link.nextElementSibling.classList.contains('error-link'))) {
                continue;
            }
 
            try {
                let href = link.href;
                let query = href.split('?')[1];
                if (!query) throw new Error('Invalid link format');
                let params = new URLSearchParams(query);
                let plistUrl = params.get('url');
                if (!plistUrl) throw new Error('No "url" parameter found');
 
                // 解码 plistUrl 以处理 URL 编码
                plistUrl = decodeURIComponent(plistUrl);
 
                GM.xmlHttpRequest({
                    method: 'GET',
                    url: plistUrl,
                    onload: function(response) {
                        if (response.status === 200) {
                            let xmlText = response.responseText;
                            let parser = new DOMParser();
                            let xmlDoc = parser.parseFromString(xmlText, 'text/xml');
                            let downloadUrl = extractDownloadUrl(xmlDoc);
                            if (downloadUrl) {
                                let downloadLink = document.createElement('a');
                                downloadLink.href = downloadUrl;
                                downloadLink.target = '_blank'; // 新窗口打开,方便下载
                                downloadLink.textContent = 'Download IPA';
                                downloadLink.style.marginLeft = '13px';
                                downloadLink.classList.add('download-link'); // 添加类名以便识别
                                link.insertAdjacentElement('afterend', downloadLink);
                            } else {
                                showError(link, 'Unable to parse plist file');
                            }
                        } else {
                            showError(link, `Request failed: ${response.status}`);
                        }
                    },
                    onerror: function() {
                        showError(link, 'Network Error');
                    }
                });
            } catch (e) {
                showError(link, e.message);
            }
        }
    }
 
    // 提取下载链接的核心函数
    function extractDownloadUrl(xmlDoc) {
        let dict = xmlDoc.querySelector('plist > dict');
        if (!dict) return null;
 
        let keys = Array.from(dict.children).filter(el => el.tagName === 'key');
        let itemsKey = keys.find(key => key.textContent === 'items');
        if (!itemsKey) return null;
 
        let itemsArray = itemsKey.nextElementSibling;
        if (!itemsArray || itemsArray.tagName !== 'array') return null;
 
        let firstItem = itemsArray.querySelector('dict');
        if (!firstItem) return null;
 
        let assetsKey = Array.from(firstItem.children).find(el => el.tagName === 'key' && el.textContent === 'assets');
        if (!assetsKey) return null;
 
        let assetsArray = assetsKey.nextElementSibling;
        if (!assetsArray || assetsArray.tagName !== 'array') return null;
 
        let softwarePackageDict = Array.from(assetsArray.children).find(dict => {
            let kindKey = Array.from(dict.children).find(key => key.tagName === 'key' && key.textContent === 'kind');
            if (kindKey && kindKey.nextElementSibling.textContent === 'software-package') {
                return true;
            }
            return false;
        });
        if (!softwarePackageDict) return null;
 
        let urlKey = Array.from(softwarePackageDict.children).find(el => el.tagName === 'key' && el.textContent === 'url');
        if (!urlKey) return null;
 
        return urlKey.nextElementSibling.textContent;
    }
 
    // 显示错误提示的函数
    function showError(link, message) {
        let errorLink = document.createElement('a');
        errorLink.textContent = message;
        errorLink.style.color = 'red';
        errorLink.style.marginLeft = '13px';
        errorLink.style.pointerEvents = 'none'; // 防止点击
        errorLink.classList.add('error-link'); // 添加类名以便识别
        link.insertAdjacentElement('afterend', errorLink);
    }
 
    // 在 DOM 加载完成后执行
    document.addEventListener('DOMContentLoaded', function() {
        processLinks();
    });
 
    // 监听动态内容加载
    const observer = new MutationObserver(() => {
        processLinks();
    });
    observer.observe(document.body, { childList: true, subtree: true });
})();