PyBrid Auto Converter

Automatically convert magnet links through PyBrid

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name      PyBrid Auto Converter
// @namespace PybridMagnetListener
// @version   4.0
// @description  Automatically convert magnet links through PyBrid
// @author       harryeffinpotter
// @match        *://*/*
// @grant        GM_xmlhttpRequest
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_deleteValue
// @license      GPL-3.0-only
// @connect      *
// @connect      https://pydrive.harryeffingpotter.com
// @run-at       document-start
// ==/UserScript==

/* jshint esversion: 11 */

(function() {
    'use strict';

    // Configuration
    const PYBRID_URL = 'https://pydrive.harryeffingpotter.com'; // UPDATE THIS WITH YOUR ACTUAL URL

    // Account management
    async function getCredentials() {
        const username = GM_getValue('pybrid_username', null);
        const password = GM_getValue('pybrid_password', null);
        return username && password ? { username, password } : null;
    }

    async function saveCredentials(username, password) {
        await GM_setValue('pybrid_username', username);
        await GM_setValue('pybrid_password', password);
    }

    async function promptForCredentials() {
        return new Promise((resolve) => {
            // Create modal overlay
            const overlay = document.createElement('div');
            overlay.style.cssText = `
                position: fixed;
                top: 0;
                left: 0;
                width: 100%;
                height: 100%;
                background: rgba(0, 0, 0, 0.5);
                z-index: 999999;
                display: flex;
                align-items: center;
                justify-content: center;
            `;

            // Create modal
            const modal = document.createElement('div');
            modal.style.cssText = `
                background: white;
                padding: 30px;
                border-radius: 8px;
                box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
                max-width: 400px;
                width: 90%;
            `;

            modal.innerHTML = `
                <h2 style="margin: 0 0 20px 0; color: #333;">PyBrid Login Required</h2>
                <div style="margin-bottom: 15px;">
                    <label style="display: block; margin-bottom: 5px; color: #555;">Username:</label>
                    <input type="text" id="pybrid-username" style="
                        width: 100%;
                        padding: 10px;
                        border: 1px solid #ddd;
                        border-radius: 4px;
                        font-size: 16px;
                        box-sizing: border-box;
                    ">
                </div>
                <div style="margin-bottom: 20px;">
                    <label style="display: block; margin-bottom: 5px; color: #555;">Password:</label>
                    <div style="position: relative;">
                        <input type="password" id="pybrid-password" style="
                            width: 100%;
                            padding: 10px;
                            padding-right: 40px;
                            border: 1px solid #ddd;
                            border-radius: 4px;
                            font-size: 16px;
                            box-sizing: border-box;
                        ">
                        <button type="button" id="toggle-password" style="
                            position: absolute;
                            right: 10px;
                            top: 50%;
                            transform: translateY(-50%);
                            background: none;
                            border: none;
                            cursor: pointer;
                            padding: 5px;
                            color: #666;
                        ">
                            <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
                                <path id="eye-open" d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path>
                                <circle id="eye-pupil" cx="12" cy="12" r="3"></circle>
                                <path id="eye-closed" d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24M1 1l22 22" style="display: none;"></path>
                            </svg>
                        </button>
                    </div>
                </div>
                <div style="display: flex; gap: 10px; justify-content: flex-end;">
                    <button id="cancel-btn" style="
                        padding: 10px 20px;
                        border: 1px solid #ddd;
                        background: white;
                        color: #666;
                        border-radius: 4px;
                        cursor: pointer;
                        font-size: 16px;
                    ">Cancel</button>
                    <button id="login-btn" style="
                        padding: 10px 20px;
                        border: none;
                        background: #5DADE2;
                        color: white;
                        border-radius: 4px;
                        cursor: pointer;
                        font-size: 16px;
                    ">Login</button>
                </div>
            `;

            overlay.appendChild(modal);
            document.body.appendChild(overlay);

            // Focus username field
            const usernameInput = modal.querySelector('#pybrid-username');
            const passwordInput = modal.querySelector('#pybrid-password');
            usernameInput.focus();

            // Toggle password visibility
            const toggleBtn = modal.querySelector('#toggle-password');
            const eyeOpen = modal.querySelector('#eye-open');
            const eyePupil = modal.querySelector('#eye-pupil');
            const eyeClosed = modal.querySelector('#eye-closed');

            toggleBtn.addEventListener('click', () => {
                if (passwordInput.type === 'password') {
                    passwordInput.type = 'text';
                    eyeOpen.style.display = 'none';
                    eyePupil.style.display = 'none';
                    eyeClosed.style.display = 'block';
                } else {
                    passwordInput.type = 'password';
                    eyeOpen.style.display = 'block';
                    eyePupil.style.display = 'block';
                    eyeClosed.style.display = 'none';
                }
            });

            // Handle login
            const handleLogin = async () => {
                const username = usernameInput.value.trim();
                const password = passwordInput.value;

                if (username && password) {
                    await saveCredentials(username, password);
                    document.body.removeChild(overlay);
                    resolve({ username, password });
                }
            };

            // Handle cancel
            const handleCancel = () => {
                document.body.removeChild(overlay);
                resolve(null);
            };

            // Button clicks
            modal.querySelector('#login-btn').addEventListener('click', handleLogin);
            modal.querySelector('#cancel-btn').addEventListener('click', handleCancel);

            // Enter key submits
            usernameInput.addEventListener('keypress', (e) => {
                if (e.key === 'Enter') passwordInput.focus();
            });
            passwordInput.addEventListener('keypress', (e) => {
                if (e.key === 'Enter') handleLogin();
            });

            // Escape key cancels
            overlay.addEventListener('keydown', (e) => {
                if (e.key === 'Escape') handleCancel();
            });
        });
    }

    // Convert a magnet link
    async function convertMagnet(magnetUrl, retryCount = 0) {
        // Get saved credentials or prompt for them
        let credentials = await getCredentials();
        if (!credentials) {
            credentials = await promptForCredentials();
            if (!credentials) {
                alert('Cannot convert without login credentials.');
                return null;
            }
        }

        const formData = new FormData();
        formData.append('link_0', magnetUrl);
        formData.append('source', 'tampermonkey');
        formData.append('username', credentials.username);
        formData.append('password', credentials.password);

        return new Promise((resolve) => {
            GM_xmlhttpRequest({
                method: 'POST',
                url: `${PYBRID_URL}/convert`,
                data: formData,
                onload: async function(response) {
                    if (response.status === 200 || response.status === 303) {
                        // Success - get the redirect URL
                        const redirectUrl = response.finalUrl || response.responseHeaders.match(/location: (.+)/i)?.[1];
                        if (redirectUrl) {
                            window.open(redirectUrl, '_blank');
                            resolve(true);
                        } else {
                            alert('Conversion succeeded but no redirect URL found');
                            resolve(false);
                        }
                    } else if (response.status === 401 && retryCount === 0) {
                        // Invalid credentials - clear and retry once
                        await GM_deleteValue('pybrid_username');
                        await GM_deleteValue('pybrid_password');
                        alert('Invalid username or password. Please login again.');
                        resolve(await convertMagnet(magnetUrl, 1));
                    } else if (response.status === 429) {
                        alert('Rate limit exceeded. Please wait a moment and try again.');
                        resolve(false);
                    } else {
                        alert(`Conversion failed: ${response.status}`);
                        resolve(false);
                    }
                },
                onerror: function(error) {
                    console.error('PyBrid conversion error:', error);
                    alert('Network error. This usually means:\n\n' +
                          '1. Tampermonkey blocked the connection\n' +
                          '2. Wrong PyBrid URL in script\n' +
                          '3. PyBrid server is down\n\n' +
                          'Check browser console (F12) for details.');
                    resolve(false);
                }
            });
        });
    }

    // Intercept all clicks
    function interceptClicks(e) {
        let target = e.target;

        // Check if clicked element or any parent is a link
        while (target && target !== document.body) {
            if (target.tagName === 'A' && target.href) {
                const href = target.href;

                // Check if it's a magnet link
                if (href.startsWith('magnet:')) {
                    e.preventDefault();
                    e.stopPropagation();
                    e.stopImmediatePropagation();

                    // Convert the magnet link
                    convertMagnet(href);
                    return false;
                }
            }
            target = target.parentElement;
        }
    }

    // Set up interceptors as early as possible
    document.addEventListener('click', interceptClicks, true);

    // Also intercept dynamically added links
    const observer = new MutationObserver((mutations) => {
        mutations.forEach((mutation) => {
            mutation.addedNodes.forEach((node) => {
                if (node.nodeType === 1) { // Element node
                    // Check if it's a link or contains links
                    if (node.tagName === 'A' && node.href && node.href.startsWith('magnet:')) {
                        node.addEventListener('click', interceptClicks, true);
                    } else if (node.querySelectorAll) {
                        node.querySelectorAll('a[href^="magnet:"]').forEach(link => {
                            link.addEventListener('click', interceptClicks, true);
                        });
                    }
                }
            });
        });
    });

    // Start observing when DOM is ready
    if (document.body) {
        observer.observe(document.body, { childList: true, subtree: true });
    } else {
        document.addEventListener('DOMContentLoaded', () => {
            observer.observe(document.body, { childList: true, subtree: true });
        });
    }

})();