MaM SendToClient

Send torrent to local client from MyAnonamouse with UI config (qBittorrent, Deluge, Transmission supported)

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         MaM SendToClient
// @namespace    http://tampermonkey.net/
// @version      0.0.3
// @license      GPL3
// @description  Send torrent to local client from MyAnonamouse with UI config (qBittorrent, Deluge, Transmission supported)
// @author       BareMetal
// @match        https://www.myanonamouse.net/t/*
// @grant        GM.xmlHttpRequest
// @connect      localhost
// ==/UserScript==

// ========== CONFIGURATION STORAGE ==========
const defaultConfig = {
    clientType: "qbittorrent",
    clientAddress: "http://localhost:8080",
    username: "admin",
    password: "adminadmin",
    category: "books",
    savePath: "",
    startPaused: true
};

function getConfig() {
    return { ...defaultConfig, ...JSON.parse(localStorage.getItem("torClientConfig") || "{}") };
}

function saveConfig(newConfig) {
    localStorage.setItem("torClientConfig", JSON.stringify(newConfig));
}

// ========== CLIENT HANDLERS ==========

const Clients = {
    qbittorrent: async function (torrentBlob, config) {
        return new Promise((resolve, reject) => {
            GM.xmlHttpRequest({
                method: "POST",
                url: `${config.clientAddress}/api/v2/auth/login`,
                headers: { "Content-Type": "application/x-www-form-urlencoded" },
                data: `username=${config.username}&password=${config.password}`,
                onload: function (authRes) {
                    if (authRes.responseText.trim() === "Ok.") {
                        let formData = new FormData();
                        formData.append("torrents", torrentBlob, "sendtoclient.torrent");
                        if (config.category) formData.append("category", config.category);
                        if (config.savePath) formData.append("savepath", config.savePath);
                        if (config.startPaused) formData.append("paused", "true");

                        GM.xmlHttpRequest({
                            method: "POST",
                            url: `${config.clientAddress}/api/v2/torrents/add`,
                            data: formData,
                            onload: () => resolve(),
                            onerror: err => reject(err)
                        });
                    } else {
                        reject("Authentication failed.");
                    }
                },
                onerror: err => reject(err)
            });
        });
    },

    transmission: async function (torrentBlob, config) {
        return new Promise((resolve, reject) => {
            GM.xmlHttpRequest({
                method: "POST",
                url: `${config.clientAddress}/transmission/rpc`,
                headers: {
                    "Authorization": "Basic " + btoa(`${config.username}:${config.password}`)
                },
                onload: function (sessionRes) {
                    const sessionId = sessionRes.responseHeaders.match(/X-Transmission-Session-Id: (.+)/i)?.[1]?.trim();
                    if (!sessionId) return reject("Failed to get Transmission session ID.");

                    let reader = new FileReader();
                    reader.onload = function () {
                        const base64Data = btoa(reader.result);
                        const payload = {
                            method: "torrent-add",
                            arguments: {
                                metainfo: base64Data,
                                "download-dir": config.savePath || undefined,
                                paused: config.startPaused || false
                            }
                        };

                        GM.xmlHttpRequest({
                            method: "POST",
                            url: `${config.clientAddress}/transmission/rpc`,
                            headers: {
                                "X-Transmission-Session-Id": sessionId,
                                "Authorization": "Basic " + btoa(`${config.username}:${config.password}`),
                                "Content-Type": "application/json"
                            },
                            data: JSON.stringify(payload),
                            onload: () => resolve(),
                            onerror: err => reject(err)
                        });
                    };
                    reader.readAsBinaryString(torrentBlob);
                },
                onerror: err => reject(err)
            });
        });
    },

    deluge: async function (torrentBlob, config) {
        return new Promise((resolve, reject) => {
            GM.xmlHttpRequest({
                method: "POST",
                url: `${config.clientAddress}/json`,
                data: JSON.stringify({ method: "auth.login", params: [config.password], id: 1 }),
                headers: { "Content-Type": "application/json" },
                onload: function (authRes) {
                    const res = JSON.parse(authRes.responseText);
                    if (!res.result) return reject("Deluge authentication failed.");

                    let reader = new FileReader();
                    reader.onload = function () {
                        const base64Data = btoa(reader.result);
                        const addPayload = {
                            method: "web.add_torrents",
                            params: [[{
                                path: base64Data,
                                name: "sendtoclient.torrent",
                                options: {
                                    download_location: config.savePath || "",
                                    add_paused: config.startPaused || false
                                }
                            }]],
                            id: 2
                        };

                        GM.xmlHttpRequest({
                            method: "POST",
                            url: `${config.clientAddress}/json`,
                            data: JSON.stringify(addPayload),
                            headers: { "Content-Type": "application/json" },
                            onload: () => resolve(),
                            onerror: err => reject(err)
                        });
                    };
                    reader.readAsBinaryString(torrentBlob);
                },
                onerror: err => reject(err)
            });
        });
    }
};

// ========== Send Torrent ==========

async function sendTorrent(tID) {
    const config = getConfig();
    const downloadURL = `https://www.myanonamouse.net/tor/download.php?tid=${tID}`;
    const response = await fetch(downloadURL);
    const blob = await response.blob();

    if (!Clients[config.clientType]) {
        alert(`Unsupported client type: ${config.clientType}`);
        return;
    }

    try {
        await Clients[config.clientType](blob, config);
        alert("Torrent sent to client!");
    } catch (err) {
        console.error(err);
        alert("Failed to send torrent: " + err.toString());
    }
}

// ========== UI Injection ==========

function injectSendButton(torrentID) {
    let button = document.createElement('button');
    button.textContent = "Send to Client";
    button.style = "margin-top: 1em; padding: 0.5em; background-color: #4CAF50; color: white; border: none; cursor: pointer; font-weight: bold;";
    button.onclick = () => sendTorrent(torrentID);

    let target = document.querySelector('#tddl');
    if (target) {
        target.parentElement.appendChild(document.createElement('br'));
        target.parentElement.appendChild(button);
    }
}

function injectSettingsUI() {
    let settingsBtn = document.createElement("button");
    settingsBtn.textContent = "⚙️ Client Settings";
    settingsBtn.style = "position: fixed; bottom: 10px; right: 10px; z-index: 9999; padding: 0.5em;";

    let modal = document.createElement("div");
    modal.style = "position: fixed; bottom: 50px; right: 10px; background: white; border: 1px solid #ccc; padding: 1em; z-index: 9999; display: none;";
    modal.innerHTML = `
        <label>Client Type:
            <select id="clientType">
                <option value="qbittorrent">qBittorrent</option>
                <option value="deluge">Deluge</option>
                <option value="transmission">Transmission</option>
            </select>
        </label><br>
        <label>Address: <input type="text" id="clientAddress" size="30" /></label><br>
        <label>Username: <input type="text" id="user" /></label><br>
        <label>Password: <input id="pass" /></label><br>
        <label>Category: <input type="text" id="category" /></label><br>
        <label>Download Path: <input type="text" id="savePath" /></label><br>
        <label><input type="checkbox" id="startPaused" /> Add torrents in Paused state</label><br>
        <button id="saveConfigBtn">Save</button>
    `;

    settingsBtn.onclick = () => {
        modal.style.display = modal.style.display === "none" ? "block" : "none";
        let cfg = getConfig();
        document.getElementById("clientType").value = cfg.clientType;
        document.getElementById("clientAddress").value = cfg.clientAddress;
        document.getElementById("user").value = cfg.username;
        document.getElementById("pass").value = cfg.password;
        document.getElementById("category").value = cfg.category;
        document.getElementById("savePath").value = cfg.savePath;
        document.getElementById("startPaused").checked = cfg.startPaused;
    };

    modal.querySelector("#saveConfigBtn").onclick = () => {
        const newCfg = {
            clientType: document.getElementById("clientType").value,
            clientAddress: document.getElementById("clientAddress").value,
            username: document.getElementById("user").value,
            password: document.getElementById("pass").value,
            category: document.getElementById("category").value,
            savePath: document.getElementById("savePath").value,
            startPaused: document.getElementById("startPaused").checked
        };
        saveConfig(newCfg);
        modal.style.display = "none";
        alert("Settings saved.");
    };

    document.body.appendChild(settingsBtn);
    document.body.appendChild(modal);
}

// ========== Entry Point ==========
(function () {
    const match = document.URL.match(/\/t\/(\d+)/);
    injectSendButton(match[1]);
    injectSettingsUI();
})();