Neu: Transmission Remote Script
This commit is contained in:
780
torrent-to-transmission.user.js
Normal file
780
torrent-to-transmission.user.js
Normal file
@ -0,0 +1,780 @@
|
||||
// ==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);
|
||||
}
|
||||
})();
|
Reference in New Issue
Block a user