780 lines
34 KiB
JavaScript
780 lines
34 KiB
JavaScript
// ==UserScript==
|
|
// @name Transmission Remote Script
|
|
// @namespace https://git.ponywave.de/Akamaru/Userscripts
|
|
// @version 1.0
|
|
// @description Sendet Magnet-Links und .torrent-Dateien an Transmission im Netzwerk mit Pfad-Auswahl und Verwaltung
|
|
// @author Akamaru
|
|
// @match *://*/*
|
|
// @grant GM_registerMenuCommand
|
|
// @grant GM_xmlhttpRequest
|
|
// @grant GM_setValue
|
|
// @grant GM_getValue
|
|
// @updateURL https://git.ponywave.de/Akamaru/Userscripts/raw/branch/master/torrent-to-transmission.user.js
|
|
// ==/UserScript==
|
|
|
|
(function() {
|
|
'use strict';
|
|
|
|
// --- Hilfsfunktionen für Storage ---
|
|
async function getConfig() {
|
|
let config = await GM_getValue('transmissionConfig');
|
|
if (config) return JSON.parse(config);
|
|
return await askForConfig();
|
|
}
|
|
async function setConfig(config) {
|
|
await GM_setValue('transmissionConfig', JSON.stringify(config));
|
|
}
|
|
async function askForConfig() {
|
|
let ipport = prompt('Transmission IP und Port (z.B. 192.168.1.100:9091):');
|
|
if (!ipport) return null;
|
|
let user = prompt('Benutzername (leer lassen, falls nicht benötigt):');
|
|
let pass = prompt('Passwort (leer lassen, falls nicht benötigt):');
|
|
let url = 'http://' + ipport + '/transmission/rpc';
|
|
let config = { url, user, pass };
|
|
await setConfig(config);
|
|
return config;
|
|
}
|
|
|
|
async function getPaths() {
|
|
let paths = await GM_getValue('transmissionPaths');
|
|
if (paths) return JSON.parse(paths);
|
|
return [];
|
|
}
|
|
async function setPaths(paths) {
|
|
await GM_setValue('transmissionPaths', JSON.stringify(paths));
|
|
}
|
|
|
|
// --- Hilfsfunktionen für modale Dialoge ---
|
|
function showModalDialog({title, content, onOk, onCancel, okText = 'OK', cancelText = 'Abbrechen', showCancel = true}) {
|
|
let overlay = document.createElement('div');
|
|
overlay.style.position = 'fixed';
|
|
overlay.style.top = 0;
|
|
overlay.style.left = 0;
|
|
overlay.style.width = '100vw';
|
|
overlay.style.height = '100vh';
|
|
overlay.style.background = 'rgba(0,0,0,0.5)';
|
|
overlay.style.zIndex = 99999;
|
|
overlay.style.display = 'flex';
|
|
overlay.style.alignItems = 'center';
|
|
overlay.style.justifyContent = 'center';
|
|
|
|
let dialog = document.createElement('div');
|
|
dialog.style.background = '#222';
|
|
dialog.style.color = '#fff';
|
|
dialog.style.padding = '24px 32px';
|
|
dialog.style.borderRadius = '10px';
|
|
dialog.style.minWidth = '480px';
|
|
dialog.style.boxShadow = '0 4px 24px rgba(0,0,0,0.4)';
|
|
dialog.style.display = 'flex';
|
|
dialog.style.flexDirection = 'column';
|
|
dialog.style.gap = '16px';
|
|
|
|
if (title) {
|
|
let h2 = document.createElement('h2');
|
|
h2.textContent = title;
|
|
h2.style.margin = '0 0 8px 0';
|
|
dialog.appendChild(h2);
|
|
}
|
|
if (typeof content === 'string') {
|
|
let p = document.createElement('div');
|
|
p.innerHTML = content;
|
|
dialog.appendChild(p);
|
|
} else if (content instanceof Node) {
|
|
dialog.appendChild(content);
|
|
}
|
|
|
|
let btnRow = document.createElement('div');
|
|
btnRow.style.display = 'flex';
|
|
btnRow.style.justifyContent = 'flex-end';
|
|
btnRow.style.gap = '12px';
|
|
|
|
if (showCancel) {
|
|
let cancelBtn = document.createElement('button');
|
|
cancelBtn.textContent = cancelText;
|
|
cancelBtn.style.background = '#666';
|
|
cancelBtn.style.color = '#fff';
|
|
cancelBtn.style.border = 'none';
|
|
cancelBtn.style.padding = '8px 18px';
|
|
cancelBtn.style.borderRadius = '4px';
|
|
cancelBtn.style.cursor = 'pointer';
|
|
cancelBtn.onclick = function() {
|
|
document.body.removeChild(overlay);
|
|
if (onCancel) onCancel();
|
|
};
|
|
btnRow.appendChild(cancelBtn);
|
|
}
|
|
|
|
let okBtn = document.createElement('button');
|
|
okBtn.textContent = okText;
|
|
okBtn.style.background = '#6c3';
|
|
okBtn.style.color = '#222';
|
|
okBtn.style.border = 'none';
|
|
okBtn.style.padding = '8px 18px';
|
|
okBtn.style.borderRadius = '4px';
|
|
okBtn.style.cursor = 'pointer';
|
|
okBtn.onclick = function() {
|
|
document.body.removeChild(overlay);
|
|
if (onOk) onOk();
|
|
};
|
|
btnRow.appendChild(okBtn);
|
|
|
|
dialog.appendChild(btnRow);
|
|
overlay.appendChild(dialog);
|
|
document.body.appendChild(overlay);
|
|
return {overlay, dialog};
|
|
}
|
|
|
|
// --- Eigener Dialog für Pfadverwaltung ---
|
|
async function editPathsMenu() {
|
|
let paths = await getPaths();
|
|
let container = document.createElement('div');
|
|
container.style.display = 'flex';
|
|
container.style.flexDirection = 'column';
|
|
container.style.gap = '10px';
|
|
|
|
let list = document.createElement('ul');
|
|
list.style.listStyle = 'none';
|
|
list.style.padding = '0';
|
|
list.style.margin = '0';
|
|
function renderList() {
|
|
list.innerHTML = '';
|
|
paths.forEach((p, i) => {
|
|
let li = document.createElement('li');
|
|
li.style.display = 'flex';
|
|
li.style.alignItems = 'center';
|
|
li.style.gap = '8px';
|
|
let span = document.createElement('span');
|
|
span.textContent = p;
|
|
li.appendChild(span);
|
|
let delBtn = document.createElement('button');
|
|
delBtn.textContent = 'Löschen';
|
|
delBtn.style.background = '#a33';
|
|
delBtn.style.color = '#fff';
|
|
delBtn.style.border = 'none';
|
|
delBtn.style.padding = '2px 8px';
|
|
delBtn.style.borderRadius = '4px';
|
|
delBtn.style.cursor = 'pointer';
|
|
delBtn.onclick = async function() {
|
|
paths.splice(i, 1);
|
|
await setPaths(paths);
|
|
renderList();
|
|
};
|
|
li.appendChild(delBtn);
|
|
list.appendChild(li);
|
|
});
|
|
}
|
|
renderList();
|
|
container.appendChild(list);
|
|
|
|
let addRow = document.createElement('div');
|
|
addRow.style.display = 'flex';
|
|
addRow.style.gap = '8px';
|
|
let input = document.createElement('input');
|
|
input.type = 'text';
|
|
input.placeholder = 'Neuen Pfad eingeben...';
|
|
input.style.flex = '1';
|
|
input.style.padding = '4px';
|
|
input.style.borderRadius = '4px';
|
|
input.style.border = '1px solid #888';
|
|
input.style.background = '#333';
|
|
input.style.color = '#fff';
|
|
addRow.appendChild(input);
|
|
let addBtn = document.createElement('button');
|
|
addBtn.textContent = 'Hinzufügen';
|
|
addBtn.style.background = '#444';
|
|
addBtn.style.color = '#fff';
|
|
addBtn.style.border = 'none';
|
|
addBtn.style.padding = '4px 12px';
|
|
addBtn.style.borderRadius = '4px';
|
|
addBtn.style.cursor = 'pointer';
|
|
addBtn.onclick = async function() {
|
|
let val = input.value.trim();
|
|
if (val) {
|
|
paths.push(val);
|
|
await setPaths(paths);
|
|
input.value = '';
|
|
renderList();
|
|
}
|
|
};
|
|
addRow.appendChild(addBtn);
|
|
container.appendChild(addRow);
|
|
|
|
showModalDialog({
|
|
title: 'Download-Pfade verwalten',
|
|
content: container,
|
|
okText: 'Schließen',
|
|
showCancel: false
|
|
});
|
|
}
|
|
|
|
// --- Eigener Dialog für Transmission-Konfiguration ---
|
|
async function askForConfig() {
|
|
return new Promise(resolve => {
|
|
let container = document.createElement('div');
|
|
container.style.display = 'flex';
|
|
container.style.flexDirection = 'column';
|
|
container.style.gap = '10px';
|
|
|
|
let ipInput = document.createElement('input');
|
|
ipInput.type = 'text';
|
|
ipInput.placeholder = 'IP und Port (z.B. 192.168.1.100:9091)';
|
|
ipInput.style.padding = '4px';
|
|
ipInput.style.borderRadius = '4px';
|
|
ipInput.style.border = '1px solid #888';
|
|
ipInput.style.background = '#333';
|
|
ipInput.style.color = '#fff';
|
|
container.appendChild(ipInput);
|
|
|
|
let userInput = document.createElement('input');
|
|
userInput.type = 'text';
|
|
userInput.placeholder = 'Benutzername (optional)';
|
|
userInput.style.padding = '4px';
|
|
userInput.style.borderRadius = '4px';
|
|
userInput.style.border = '1px solid #888';
|
|
userInput.style.background = '#333';
|
|
userInput.style.color = '#fff';
|
|
container.appendChild(userInput);
|
|
|
|
let passInput = document.createElement('input');
|
|
passInput.type = 'password';
|
|
passInput.placeholder = 'Passwort (optional)';
|
|
passInput.style.padding = '4px';
|
|
passInput.style.borderRadius = '4px';
|
|
passInput.style.border = '1px solid #888';
|
|
passInput.style.background = '#333';
|
|
passInput.style.color = '#fff';
|
|
container.appendChild(passInput);
|
|
|
|
showModalDialog({
|
|
title: 'Transmission konfigurieren',
|
|
content: container,
|
|
okText: 'Speichern',
|
|
cancelText: 'Abbrechen',
|
|
showCancel: true,
|
|
onOk: async () => {
|
|
let ipport = ipInput.value.trim();
|
|
let user = userInput.value.trim();
|
|
let pass = passInput.value;
|
|
if (!ipport) return;
|
|
let url = 'http://' + ipport + '/transmission/rpc';
|
|
let config = { url, user, pass };
|
|
await setConfig(config);
|
|
resolve(config);
|
|
},
|
|
onCancel: () => resolve(null)
|
|
});
|
|
});
|
|
}
|
|
|
|
// --- Transmission Session-ID holen ---
|
|
function getSessionId(config, callback) {
|
|
let headers = {};
|
|
if (config.user) {
|
|
headers['Authorization'] = 'Basic ' + btoa(config.user + ':' + config.pass);
|
|
}
|
|
GM_xmlhttpRequest({
|
|
method: 'POST',
|
|
url: config.url,
|
|
headers: headers,
|
|
onload: function(response) {
|
|
const match = response.responseHeaders.match(/x-transmission-session-id:(.+)/i);
|
|
const sessionId = match ? match[1].trim() : null;
|
|
callback(sessionId);
|
|
},
|
|
onerror: function() {
|
|
showToast('Fehler beim Abrufen der Session-ID!');
|
|
}
|
|
});
|
|
}
|
|
|
|
// --- Magnet/Torrent an Transmission senden ---
|
|
function sendToTransmission(data, isMagnet, downloadDir, retry) {
|
|
getConfig().then(config => {
|
|
getSessionId(config, function(sessionId) {
|
|
let headers = {
|
|
'Content-Type': 'application/json',
|
|
'X-Transmission-Session-Id': sessionId
|
|
};
|
|
if (config.user) {
|
|
headers['Authorization'] = 'Basic ' + btoa(config.user + ':' + config.pass);
|
|
}
|
|
let args = isMagnet ? { filename: data } : { metainfo: data };
|
|
if (downloadDir) args['download-dir'] = downloadDir;
|
|
let body = {
|
|
method: 'torrent-add',
|
|
arguments: args
|
|
};
|
|
GM_xmlhttpRequest({
|
|
method: 'POST',
|
|
url: config.url,
|
|
headers: headers,
|
|
data: JSON.stringify(body),
|
|
onload: function(response) {
|
|
if (response.status === 409 && !retry) {
|
|
const match = response.responseHeaders.match(/x-transmission-session-id:(.+)/i);
|
|
const newSessionId = match ? match[1].trim() : null;
|
|
if (newSessionId) {
|
|
headers['X-Transmission-Session-Id'] = newSessionId;
|
|
GM_xmlhttpRequest({
|
|
method: 'POST',
|
|
url: config.url,
|
|
headers: headers,
|
|
data: JSON.stringify(body),
|
|
onload: function(resp2) {
|
|
if (resp2.status === 409) {
|
|
showToast('Session-ID konnte nicht erneuert werden!\nResponse Headers:\n' + resp2.responseHeaders);
|
|
} else {
|
|
showToast('An Transmission gesendet!');
|
|
}
|
|
},
|
|
onerror: function() {
|
|
showToast('Fehler beim Senden an Transmission!');
|
|
}
|
|
});
|
|
} else {
|
|
showToast('Session-ID konnte nicht erneuert werden!\nResponse Headers:\n' + response.responseHeaders);
|
|
}
|
|
} else if (response.status === 409 && retry) {
|
|
showToast('Session-ID konnte nicht erneuert werden!\nResponse Headers:\n' + response.responseHeaders);
|
|
} else {
|
|
showToast('An Transmission gesendet!');
|
|
}
|
|
},
|
|
onerror: function() {
|
|
showToast('Fehler beim Senden an Transmission!');
|
|
}
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
// --- .torrent-Datei herunterladen und in Base64 umwandeln ---
|
|
function fetchAndEncodeTorrent(url, callback) {
|
|
GM_xmlhttpRequest({
|
|
method: 'GET',
|
|
url: url,
|
|
responseType: 'arraybuffer',
|
|
onload: function(response) {
|
|
const bytes = new Uint8Array(response.response);
|
|
let binary = '';
|
|
for (let i = 0; i < bytes.byteLength; i++) {
|
|
binary += String.fromCharCode(bytes[i]);
|
|
}
|
|
const base64 = btoa(binary);
|
|
callback(base64);
|
|
},
|
|
onerror: function() {
|
|
showToast('Fehler beim Herunterladen der .torrent-Datei!');
|
|
}
|
|
});
|
|
}
|
|
|
|
// --- Dialog für Pfad-Auswahl und Senden ---
|
|
async function showPathDialog(onSend, onCancel, fileList) {
|
|
let paths = await getPaths();
|
|
// Dialog-Elemente erzeugen
|
|
let overlay = document.createElement('div');
|
|
overlay.style.position = 'fixed';
|
|
overlay.style.top = 0;
|
|
overlay.style.left = 0;
|
|
overlay.style.width = '100vw';
|
|
overlay.style.height = '100vh';
|
|
overlay.style.background = 'rgba(0,0,0,0.5)';
|
|
overlay.style.zIndex = 99999;
|
|
overlay.style.display = 'flex';
|
|
overlay.style.alignItems = 'center';
|
|
overlay.style.justifyContent = 'center';
|
|
|
|
let dialog = document.createElement('div');
|
|
dialog.style.background = '#222';
|
|
dialog.style.color = '#fff';
|
|
dialog.style.padding = '24px 32px';
|
|
dialog.style.borderRadius = '10px';
|
|
dialog.style.minWidth = '480px';
|
|
dialog.style.boxShadow = '0 4px 24px rgba(0,0,0,0.4)';
|
|
dialog.style.display = 'flex';
|
|
dialog.style.flexDirection = 'column';
|
|
dialog.style.gap = '16px';
|
|
|
|
let title = document.createElement('h2');
|
|
title.textContent = 'Download-Verzeichnis wählen';
|
|
title.style.margin = '0 0 8px 0';
|
|
dialog.appendChild(title);
|
|
|
|
// Dateiliste für .torrent-Dateien mit Checkboxen
|
|
let selectedFiles = [];
|
|
let fileCheckboxes = [];
|
|
if (fileList && fileList.length > 0) {
|
|
selectedFiles = fileList.map(() => true); // standardmäßig alle ausgewählt
|
|
let fileBox = document.createElement('div');
|
|
fileBox.style.maxHeight = '180px';
|
|
fileBox.style.overflowY = 'auto';
|
|
fileBox.style.background = '#181818';
|
|
fileBox.style.border = '1px solid #444';
|
|
fileBox.style.borderRadius = '6px';
|
|
fileBox.style.padding = '8px 12px';
|
|
fileBox.style.marginBottom = '8px';
|
|
fileBox.style.width = '100%';
|
|
let fileTitle = document.createElement('div');
|
|
fileTitle.textContent = 'Dateien im Torrent:';
|
|
fileTitle.style.fontWeight = 'bold';
|
|
fileTitle.style.marginBottom = '6px';
|
|
fileBox.appendChild(fileTitle);
|
|
|
|
// Buttons für alle an/abwählen
|
|
let btnRow = document.createElement('div');
|
|
btnRow.style.display = 'flex';
|
|
btnRow.style.gap = '8px';
|
|
btnRow.style.marginBottom = '8px';
|
|
let allOnBtn = document.createElement('button');
|
|
allOnBtn.textContent = 'Alle auswählen';
|
|
allOnBtn.style.background = '#444';
|
|
allOnBtn.style.color = '#fff';
|
|
allOnBtn.style.border = 'none';
|
|
allOnBtn.style.padding = '2px 10px';
|
|
allOnBtn.style.borderRadius = '4px';
|
|
allOnBtn.style.cursor = 'pointer';
|
|
allOnBtn.onclick = function() {
|
|
fileCheckboxes.forEach((cb, i) => { cb.checked = true; selectedFiles[i] = true; });
|
|
};
|
|
btnRow.appendChild(allOnBtn);
|
|
let allOffBtn = document.createElement('button');
|
|
allOffBtn.textContent = 'Alle abwählen';
|
|
allOffBtn.style.background = '#444';
|
|
allOffBtn.style.color = '#fff';
|
|
allOffBtn.style.border = 'none';
|
|
allOffBtn.style.padding = '2px 10px';
|
|
allOffBtn.style.borderRadius = '4px';
|
|
allOffBtn.style.cursor = 'pointer';
|
|
allOffBtn.onclick = function() {
|
|
fileCheckboxes.forEach((cb, i) => { cb.checked = false; selectedFiles[i] = false; });
|
|
};
|
|
btnRow.appendChild(allOffBtn);
|
|
fileBox.appendChild(btnRow);
|
|
|
|
fileList.forEach((f, idx) => {
|
|
let fileRow = document.createElement('div');
|
|
fileRow.style.display = 'flex';
|
|
fileRow.style.alignItems = 'center';
|
|
fileRow.style.gap = '8px';
|
|
fileRow.style.fontSize = '0.98em';
|
|
fileRow.style.wordBreak = 'break-all';
|
|
fileRow.style.whiteSpace = 'normal';
|
|
let cb = document.createElement('input');
|
|
cb.type = 'checkbox';
|
|
cb.checked = true;
|
|
cb.onchange = function() { selectedFiles[idx] = cb.checked; };
|
|
fileCheckboxes.push(cb);
|
|
fileRow.appendChild(cb);
|
|
let label = document.createElement('span');
|
|
label.textContent = f;
|
|
fileRow.appendChild(label);
|
|
fileBox.appendChild(fileRow);
|
|
});
|
|
dialog.appendChild(fileBox);
|
|
}
|
|
|
|
let select = document.createElement('select');
|
|
select.style.fontSize = '1.1em';
|
|
select.style.padding = '6px';
|
|
select.style.marginBottom = '8px';
|
|
select.style.borderRadius = '4px';
|
|
select.style.border = '1px solid #888';
|
|
select.style.background = '#333';
|
|
select.style.color = '#fff';
|
|
select.style.width = '100%';
|
|
paths.forEach(p => {
|
|
let opt = document.createElement('option');
|
|
opt.value = p;
|
|
opt.textContent = p;
|
|
select.appendChild(opt);
|
|
});
|
|
dialog.appendChild(select);
|
|
|
|
let btnRow = document.createElement('div');
|
|
btnRow.style.display = 'flex';
|
|
btnRow.style.justifyContent = 'flex-end';
|
|
btnRow.style.gap = '12px';
|
|
|
|
let cancelBtn = document.createElement('button');
|
|
cancelBtn.textContent = 'Abbrechen';
|
|
cancelBtn.style.background = '#666';
|
|
cancelBtn.style.color = '#fff';
|
|
cancelBtn.style.border = 'none';
|
|
cancelBtn.style.padding = '8px 18px';
|
|
cancelBtn.style.borderRadius = '4px';
|
|
cancelBtn.style.cursor = 'pointer';
|
|
cancelBtn.onclick = function() {
|
|
document.body.removeChild(overlay);
|
|
if (onCancel) onCancel();
|
|
};
|
|
btnRow.appendChild(cancelBtn);
|
|
|
|
let sendBtn = document.createElement('button');
|
|
sendBtn.textContent = 'Senden';
|
|
sendBtn.style.background = '#6c3';
|
|
sendBtn.style.color = '#222';
|
|
sendBtn.style.border = 'none';
|
|
sendBtn.style.padding = '8px 18px';
|
|
sendBtn.style.borderRadius = '4px';
|
|
sendBtn.style.cursor = 'pointer';
|
|
sendBtn.onclick = function() {
|
|
let dir = select.value;
|
|
document.body.removeChild(overlay);
|
|
if (onSend) {
|
|
if (fileList && fileList.length > 0) {
|
|
// Übergebe die Auswahl als Array von bools
|
|
onSend(dir, selectedFiles);
|
|
} else {
|
|
onSend(dir);
|
|
}
|
|
}
|
|
};
|
|
btnRow.appendChild(sendBtn);
|
|
|
|
dialog.appendChild(btnRow);
|
|
overlay.appendChild(dialog);
|
|
document.body.appendChild(overlay);
|
|
}
|
|
|
|
// --- Hilfsfunktion: Nach dem Hinzufügen eines Torrents Dateien abwählen ---
|
|
function setUnwantedFiles(torrentId, unwantedIndices, config) {
|
|
if (!unwantedIndices || unwantedIndices.length === 0) return;
|
|
let headers = {
|
|
'Content-Type': 'application/json',
|
|
};
|
|
if (config.user) {
|
|
headers['Authorization'] = 'Basic ' + btoa(config.user + ':' + config.pass);
|
|
}
|
|
getSessionId(config, function(sessionId) {
|
|
headers['X-Transmission-Session-Id'] = sessionId;
|
|
let body = {
|
|
method: 'torrent-set',
|
|
arguments: {
|
|
ids: [torrentId],
|
|
'files-unwanted': unwantedIndices
|
|
}
|
|
};
|
|
GM_xmlhttpRequest({
|
|
method: 'POST',
|
|
url: config.url,
|
|
headers: headers,
|
|
data: JSON.stringify(body),
|
|
onload: function(response) {
|
|
// Optional: Rückmeldung anzeigen
|
|
if (response.status === 200) {
|
|
showToast('Dateiauswahl an Transmission übermittelt!');
|
|
} else {
|
|
showToast('Fehler beim Setzen der Dateiauswahl!');
|
|
}
|
|
},
|
|
onerror: function() {
|
|
showToast('Fehler beim Setzen der Dateiauswahl!');
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
// --- Hilfsfunktion: Torrent-Dateiliste aus bencode extrahieren ---
|
|
function parseTorrentFileList(arrayBuffer) {
|
|
// Minimaler Bencode-Parser für 'info.files' oder 'info.name' (single file)
|
|
// Korrigiert: Dateinamen werden anhand der Längenangabe korrekt extrahiert
|
|
try {
|
|
let bytes = new Uint8Array(arrayBuffer);
|
|
let str = '';
|
|
for (let i = 0; i < bytes.length; i++) str += String.fromCharCode(bytes[i]);
|
|
// Suche nach '4:info' und dann nach '5:files' oder '4:name'
|
|
let infoIdx = str.indexOf('4:info');
|
|
if (infoIdx === -1) return [];
|
|
let filesIdx = str.indexOf('5:files', infoIdx);
|
|
if (filesIdx !== -1) {
|
|
// Multi-File Torrent: Dateinamen extrahieren
|
|
let fileList = [];
|
|
// Suche alle "4:pathl...e"-Blöcke ab filesIdx
|
|
let pathIdx = filesIdx;
|
|
while ((pathIdx = str.indexOf('4:pathl', pathIdx)) !== -1) {
|
|
pathIdx += 7; // nach '4:pathl'
|
|
// Es können mehrere Strings (Verzeichnisse + Dateiname) folgen, jeweils als "<len>:<name>"
|
|
let nameParts = [];
|
|
while (str[pathIdx] >= '0' && str[pathIdx] <= '9') {
|
|
// Lese Längenangabe
|
|
let lenStr = '';
|
|
while (str[pathIdx] >= '0' && str[pathIdx] <= '9') {
|
|
lenStr += str[pathIdx++];
|
|
}
|
|
if (str[pathIdx] !== ':') break;
|
|
pathIdx++;
|
|
let len = parseInt(lenStr, 10);
|
|
let name = str.substr(pathIdx, len);
|
|
nameParts.push(name);
|
|
pathIdx += len;
|
|
}
|
|
if (nameParts.length > 0) fileList.push(nameParts.join('/'));
|
|
// Suche nächstes '4:pathl' ab aktueller Position
|
|
}
|
|
return fileList;
|
|
} else {
|
|
// Single-File Torrent: nur Name
|
|
let nameIdx = str.indexOf('4:name', infoIdx);
|
|
if (nameIdx !== -1) {
|
|
let lenStr = '';
|
|
let idx = nameIdx + 6;
|
|
while (str[idx] >= '0' && str[idx] <= '9') {
|
|
lenStr += str[idx++];
|
|
}
|
|
if (str[idx] === ':') {
|
|
idx++;
|
|
let len = parseInt(lenStr, 10);
|
|
let name = str.substr(idx, len);
|
|
return [name];
|
|
}
|
|
}
|
|
}
|
|
} catch (e) {}
|
|
return [];
|
|
}
|
|
|
|
// --- Klicks abfangen ---
|
|
document.addEventListener('click', function(e) {
|
|
let el = e.target.closest('a');
|
|
if (!el) return;
|
|
|
|
// Magnet-Link
|
|
if (el.href.startsWith('magnet:?')) {
|
|
e.preventDefault();
|
|
showPathDialog(function(dir) {
|
|
sendToTransmission(el.href, true, dir, false);
|
|
});
|
|
}
|
|
|
|
// .torrent-Datei
|
|
if (el.href.match(/\.torrent($|\?)/i)) {
|
|
e.preventDefault();
|
|
GM_xmlhttpRequest({
|
|
method: 'GET',
|
|
url: el.href,
|
|
responseType: 'arraybuffer',
|
|
onload: function(response) {
|
|
const arrayBuffer = response.response;
|
|
const base64 = btoa(String.fromCharCode.apply(null, new Uint8Array(arrayBuffer)));
|
|
const fileList = parseTorrentFileList(arrayBuffer);
|
|
showPathDialog(function(dir, selectedFiles) {
|
|
// selectedFiles: Array von bools, true = gewünscht
|
|
// Sende an Transmission, und setze unwanted falls nötig
|
|
getConfig().then(config => {
|
|
getSessionId(config, function(sessionId) {
|
|
let headers = {
|
|
'Content-Type': 'application/json',
|
|
'X-Transmission-Session-Id': sessionId
|
|
};
|
|
if (config.user) {
|
|
headers['Authorization'] = 'Basic ' + btoa(config.user + ':' + config.pass);
|
|
}
|
|
let args = { metainfo: base64 };
|
|
if (dir) args['download-dir'] = dir;
|
|
let body = {
|
|
method: 'torrent-add',
|
|
arguments: args
|
|
};
|
|
GM_xmlhttpRequest({
|
|
method: 'POST',
|
|
url: config.url,
|
|
headers: headers,
|
|
data: JSON.stringify(body),
|
|
onload: function(response) {
|
|
let respObj = {};
|
|
try { respObj = JSON.parse(response.responseText); } catch (e) {}
|
|
if (response.status === 409) {
|
|
const match = response.responseHeaders.match(/x-transmission-session-id:(.+)/i);
|
|
const newSessionId = match ? match[1].trim() : null;
|
|
if (newSessionId) {
|
|
headers['X-Transmission-Session-Id'] = newSessionId;
|
|
GM_xmlhttpRequest({
|
|
method: 'POST',
|
|
url: config.url,
|
|
headers: headers,
|
|
data: JSON.stringify(body),
|
|
onload: function(resp2) {
|
|
let respObj2 = {};
|
|
try { respObj2 = JSON.parse(resp2.responseText); } catch (e) {}
|
|
if (resp2.status === 409) {
|
|
showToast('Session-ID konnte nicht erneuert werden!\nResponse Headers:\n' + resp2.responseHeaders);
|
|
} else {
|
|
showToast('An Transmission gesendet!');
|
|
// Nach dem Hinzufügen: unwanted setzen
|
|
if (Array.isArray(selectedFiles) && fileList.length === selectedFiles.length) {
|
|
let unwanted = [];
|
|
for (let i = 0; i < selectedFiles.length; i++) {
|
|
if (!selectedFiles[i]) unwanted.push(i);
|
|
}
|
|
if (respObj2.result === 'success' && respObj2.arguments && respObj2.arguments['torrent-added']) {
|
|
let tid = respObj2.arguments['torrent-added'].id;
|
|
setUnwantedFiles(tid, unwanted, config);
|
|
}
|
|
}
|
|
}
|
|
},
|
|
onerror: function() {
|
|
showToast('Fehler beim Senden an Transmission!');
|
|
}
|
|
});
|
|
} else {
|
|
showToast('Session-ID konnte nicht erneuert werden!\nResponse Headers:\n' + response.responseHeaders);
|
|
}
|
|
} else {
|
|
showToast('An Transmission gesendet!');
|
|
// Nach dem Hinzufügen: unwanted setzen
|
|
if (Array.isArray(selectedFiles) && fileList.length === selectedFiles.length) {
|
|
let unwanted = [];
|
|
for (let i = 0; i < selectedFiles.length; i++) {
|
|
if (!selectedFiles[i]) unwanted.push(i);
|
|
}
|
|
if (respObj.result === 'success' && respObj.arguments && respObj.arguments['torrent-added']) {
|
|
let tid = respObj.arguments['torrent-added'].id;
|
|
setUnwantedFiles(tid, unwanted, config);
|
|
}
|
|
}
|
|
}
|
|
},
|
|
onerror: function() {
|
|
showToast('Fehler beim Senden an Transmission!');
|
|
}
|
|
});
|
|
});
|
|
});
|
|
}, undefined, fileList);
|
|
},
|
|
onerror: function() {
|
|
showToast('Fehler beim Herunterladen der .torrent-Datei!');
|
|
}
|
|
});
|
|
}
|
|
}, true);
|
|
|
|
// --- Menüeinträge ---
|
|
if (typeof GM_registerMenuCommand !== 'undefined') {
|
|
GM_registerMenuCommand('Transmission konfigurieren', askForConfig);
|
|
GM_registerMenuCommand('Download-Pfade verwalten', editPathsMenu);
|
|
}
|
|
|
|
// --- Rückmeldungen als Toast ---
|
|
function showToast(msg) {
|
|
let toast = document.createElement('div');
|
|
toast.textContent = msg;
|
|
toast.style.position = 'fixed';
|
|
toast.style.bottom = '32px';
|
|
toast.style.right = '32px';
|
|
toast.style.background = '#222';
|
|
toast.style.color = '#fff';
|
|
toast.style.padding = '12px 24px';
|
|
toast.style.borderRadius = '8px';
|
|
toast.style.boxShadow = '0 2px 8px rgba(0,0,0,0.2)';
|
|
toast.style.zIndex = 999999;
|
|
document.body.appendChild(toast);
|
|
setTimeout(() => {
|
|
toast.style.transition = 'opacity 0.5s';
|
|
toast.style.opacity = 0;
|
|
setTimeout(() => toast.remove(), 500);
|
|
}, 2200);
|
|
}
|
|
})();
|