RuStore Downloader

Добавление кнопки скачивания APK на страницы приложений RuStore.

スクリプトをインストールするには、Tampermonkey, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

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

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

作者のサイトでサポートを受ける。または、このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==UserScript==
// @name         RuStore Downloader
// @namespace    http://tampermonkey.net/
// @version      1.2
// @description  Добавление кнопки скачивания APK на страницы приложений RuStore.
// @homepageURL  https://gist.github.com/smi-falcon/f75edfb5c0143f81ff6ea25e315f0964
// @supportURL   https://github.com/smi-falcon
// @author       Falcon
// @match        https://www.rustore.ru/catalog/app/*
// @match        https://apps.rustore.ru/app/*
// @icon         https://www.rustore.ru/favicon.ico
// @license      MIT
// @grant        GM_xmlhttpRequest
// @connect      backapi.rustore.ru
// @connect      static.rustore.ru
// @noframes
// ==/UserScript==

(function() {
    'use strict';

    const style = document.createElement('style');
    style.textContent = `
        .rustore-download-btn {
            display: inline-flex;
            align-items: center;
            gap: 8px;
            padding: 10px 20px;
            background: linear-gradient(135deg, #6B4BFF, #8B6BFF);
            color: white;
            border: none;
            border-radius: 12px;
            font-size: 16px;
            font-weight: 600;
            cursor: pointer;
            transition: all 0.3s ease;
            box-shadow: 0 4px 15px rgba(107, 75, 255, 0.3);
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
        }

        .rustore-download-btn:hover {
            transform: translateY(-2px);
            box-shadow: 0 6px 20px rgba(107, 75, 255, 0.5);
            background: linear-gradient(135deg, #8B6BFF, #A78BFF);
        }

        .rustore-download-btn:active {
            transform: translateY(0);
        }

        .rustore-download-btn.loading {
            pointer-events: none;
            opacity: 0.7;
        }

        .rustore-download-btn .spinner {
            display: none;
            width: 16px;
            height: 16px;
            border: 2px solid rgba(255,255,255,0.3);
            border-top: 2px solid white;
            border-radius: 50%;
            animation: rustore-spin 0.8s linear infinite;
        }

        .rustore-download-btn.loading .spinner {
            display: block;
        }

        .rustore-download-btn.loading .btn-text {
            display: none;
        }

        @keyframes rustore-spin {
            0% { transform: rotate(0deg); }
            100% { transform: rotate(360deg); }
        }

        .rustore-download-error {
            color: #EF4444;
            font-size: 14px;
            margin-top: 8px;
            padding: 8px 12px;
            background: rgba(239, 68, 68, 0.1);
            border-radius: 8px;
            border: 1px solid rgba(239, 68, 68, 0.2);
        }
    `;
    document.head.appendChild(style);

    function getPackageNameFromUrl() {
        const pathParts = window.location.pathname.split('/').filter(Boolean);
        if (pathParts.length >= 2) {
            return pathParts[pathParts.length - 1];
        }
        return null;
    }

    async function getAppInfo(packageName) {
        return new Promise((resolve, reject) => {
            GM_xmlhttpRequest({
                method: 'GET',
                url: `https://backapi.rustore.ru/applicationData/overallInfo/${packageName}`,
                headers: {
                    'ruStoreVerCode': '247'
                },
                responseType: 'json',
                onload: function(response) {
                    if (response.status === 200 && response.response.code === 'OK') {
                        resolve(response.response.body);
                    } else {
                        reject(new Error('Не удалось получить информацию о приложении'));
                    }
                },
                onerror: function() {
                    reject(new Error('Ошибка сети'));
                }
            });
        });
    }

    async function getDownloadLink(appId, sdkVersion) {
        return new Promise((resolve, reject) => {
            GM_xmlhttpRequest({
                method: 'POST',
                url: 'https://backapi.rustore.ru/applicationData/v2/download-link',
                headers: {
                    'Content-Type': 'application/json',
                    'ruStoreVerCode': '247'
                },
                data: JSON.stringify({
                    appId: appId,
                    firstInstall: true,
                    mobileServices: [],
                    supportedAbis: ["x86_64", "arm64-v8a", "x86", "armeabi-v7a", "armeabi"],
                    screenDensity: 0,
                    supportedLocales: ["ru_RU"],
                    sdkVersion: sdkVersion,
                    withoutSplits: true,
                    signatureFingerprint: null
                }),
                responseType: 'json',
                onload: function(response) {
                    if (response.status === 200 && response.response.code === 'OK') {
                        resolve(response.response.body);
                    } else {
                        reject(new Error('Не удалось получить ссылку на скачивание'));
                    }
                },
                onerror: function() {
                    reject(new Error('Ошибка сети'));
                }
            });
        });
    }

    function downloadFile(url, filename) {
        const link = document.createElement('a');
        link.href = url;
        link.download = filename || 'app.apk';
        link.style.display = 'none';
        document.body.appendChild(link);
        link.click();
        document.body.removeChild(link);
    }

    function resetButton(button) {
        button.classList.remove('loading');
        button.style.background = 'linear-gradient(135deg, #6B4BFF, #8B6BFF)';
        button.innerHTML = `
            <span class="spinner"></span>
            <span class="btn-text">
                <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
                    <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
                    <polyline points="7 10 12 15 17 10"></polyline>
                    <line x1="12" y1="15" x2="12" y2="3"></line>
                </svg>
                Скачать APK
            </span>
        `;
    }

    function showSuccess(button) {
        button.classList.remove('loading');
        button.style.background = 'linear-gradient(135deg, #10B981, #059669)';
        button.innerHTML = `
            <span class="btn-text">
                <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
                    <polyline points="20 6 9 17 4 12"></polyline>
                </svg>
                Скачивание началось!
            </span>
        `;
        setTimeout(() => resetButton(button), 3000);
    }

    function showError(button, message) {
        button.classList.remove('loading');
        button.style.background = 'linear-gradient(135deg, #EF4444, #DC2626)';
        button.innerHTML = `
            <span class="btn-text">
                <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
                    <circle cx="12" cy="12" r="10"></circle>
                    <line x1="15" y1="9" x2="9" y2="15"></line>
                    <line x1="9" y1="9" x2="15" y2="15"></line>
                </svg>
                Ошибка загрузки
            </span>
        `;

        const existingError = button.parentNode.querySelector('.rustore-download-error');
        if (existingError) {
            existingError.remove();
        }

        const errorDiv = document.createElement('div');
        errorDiv.className = 'rustore-download-error';
        errorDiv.textContent = message;
        button.parentNode.appendChild(errorDiv);

        setTimeout(() => {
            if (errorDiv.parentNode) {
                errorDiv.remove();
            }
        }, 5000);

        setTimeout(() => resetButton(button), 3000);
    }

    function createDownloadButton() {
        const button = document.createElement('button');
        button.className = 'rustore-download-btn';
        button.innerHTML = `
            <span class="spinner"></span>
            <span class="btn-text">
                <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
                    <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
                    <polyline points="7 10 12 15 17 10"></polyline>
                    <line x1="12" y1="15" x2="12" y2="3"></line>
                </svg>
                Скачать APK
            </span>
        `;

        button.addEventListener('click', async function(e) {
            e.preventDefault();
            e.stopPropagation();

            button.classList.add('loading');

            try {
                const packageName = getPackageNameFromUrl();
                if (!packageName) {
                    throw new Error('Не удалось определить package name');
                }

                const appInfo = await getAppInfo(packageName);

                if (!appInfo || !appInfo.appId) {
                    throw new Error('Не удалось получить информацию о приложении');
                }

                const downloadInfo = await getDownloadLink(appInfo.appId, appInfo.minSdkVersion);

                if (!downloadInfo || !downloadInfo.downloadUrls || !downloadInfo.downloadUrls[0]) {
                    throw new Error('Не удалось получить ссылку на скачивание');
                }

                const downloadUrl = downloadInfo.downloadUrls[0].url;
                const filename = `${appInfo.packageName}_v${appInfo.versionName}.apk`;

                downloadFile(downloadUrl, filename);
                showSuccess(button);

            } catch (error) {
                showError(button, error.message);
            }
        });

        return button;
    }

    function insertDownloadButton() {
        const existingButton = document.querySelector('.rustore-download-btn');
        if (existingButton) {
            const container = existingButton.closest('.rustore-download-container');
            if (container) {
                container.remove();
            }
        }

        const targetBlock = document.querySelector('._0oh6ea_1');

        if (!targetBlock) {
            return false;
        }

        let buttonContainer = targetBlock.parentNode.querySelector('.rustore-download-container');
        if (buttonContainer) {
            buttonContainer.remove();
        }

        buttonContainer = document.createElement('div');
        buttonContainer.className = 'rustore-download-container';
        buttonContainer.style.cssText = `
            margin-top: 16px;
            width: 100%;
        `;

        buttonContainer.appendChild(createDownloadButton());
        targetBlock.parentNode.insertBefore(buttonContainer, targetBlock.nextSibling);

        return true;
    }

    function observeUrlChanges() {
        let lastUrl = location.href;

        const domObserver = new MutationObserver(() => {
            if (lastUrl !== location.href) {
                lastUrl = location.href;
                setTimeout(() => {
                    insertDownloadButton();
                }, 500);
            } else {

                if (!document.querySelector('.rustore-download-container')) {
                    insertDownloadButton();
                }
            }
        });

        domObserver.observe(document.body, {
            childList: true,
            subtree: true,
            attributes: true,
            attributeFilter: ['class', 'style']
        });

        const originalPushState = history.pushState;
        const originalReplaceState = history.replaceState;

        history.pushState = function() {
            originalPushState.apply(this, arguments);
            setTimeout(() => insertDownloadButton(), 300);
        };

        history.replaceState = function() {
            originalReplaceState.apply(this, arguments);
            setTimeout(() => insertDownloadButton(), 300);
        };

        window.addEventListener('popstate', () => {
            setTimeout(() => insertDownloadButton(), 300);
        });

        insertDownloadButton();
    }

    function startPeriodicCheck() {
        let checkInterval = setInterval(() => {
            const targetBlock = document.querySelector('._0oh6ea_1');
            const hasButton = document.querySelector('.rustore-download-container');

            if (targetBlock && !hasButton) {
                insertDownloadButton();
            }

            if (window.location.pathname.match(/\/catalog\/app\/|\/app\//) && !hasButton) {
                insertDownloadButton();
            }
        }, 2000);

        setTimeout(() => clearInterval(checkInterval), 30000);
    }

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', () => {
            observeUrlChanges();
            startPeriodicCheck();
        });
    } else {
        observeUrlChanges();
        startPeriodicCheck();
    }
})();