"use strict"; class trserver extends EventTarget { #logcb; #log(loglevel, msg) { if (this.#logcb !== null) { this.#logcb(`server:${this.name}`, loglevel, msg); } } #name; get name() { return this.#name; } #rpcurl; #authHeader; #sessionHeader; #requests = []; #elements = { serverlist: { row: document.createElement('div'), name: document.createElement('div'), buttons: {} } }; get element() { return this.#elements.serverlist.row; } addServerListButton(name, button) { if (this.#elements.serverlist.buttons[name] != null) { this.removeServerListButton(name); } this.#elements.serverlist.buttons[name] = button; this.#elements.serverlist.row.appendChild(button); } removeServerListButton(name) { if (this.#elements.serverlist.buttons[name] != null) { this.#elements.serverlist.row.removeChild(this.#elements.serverlist.buttons[name]); this.#elements.serverlist.buttons[name] = null; } } #torrentControls = {}; addControl(hash, control) { this.#torrentControls[hash] = control; } getControl(hash) { return this.#torrentControls[hash]; } #isOnline = false; #setOnline(online) { if (this.isOnline == online) { return; } this.#isOnline = online; let detail = { }; let etype; if (online) { etype = 'torrentserver-online'; } else { etype = 'torrentserver-offline'; } this.dispatchEvent(new CustomEvent(etype, detail)); } constructor (name, initdata, logcb = null) { super(); this.#logcb = logcb; this.#name = name; this.#rpcurl = initdata.rpcurl; this.#authHeader = initdata.auth; this.#sessionHeader = null; this.#elements.serverlist.row.classList.add('d-flex', 'flex-row'); this.#elements.serverlist.row.appendChild(this.#elements.serverlist.name); this.#elements.serverlist.name.classList.add('flex-grow-1'); this.#elements.serverlist.name.appendChild(document.createTextNode(this.name)); } #rpccall_prepare(method, args) { let request = { 'payload': { 'method': method, 'arguments': args } }; let tag = this.#requests.push(request) - 1; return tag; } #rpccall(tag) { let request = this.#requests[tag]; //this.#logger(`[${tag}] >> ${request.payload.method}`); let rpc = new window.XMLHttpRequest(); rpc.tag = tag; rpc.open('POST', this.#rpcurl); if (this.#authHeader != null) { rpc.setRequestHeader('authorization', this.#authHeader); } if (this.#sessionHeader != null) { rpc.setRequestHeader('X-Transmission-Session-Id', this.#sessionHeader); } rpc.setRequestHeader('content-type', 'application/json'); rpc.addEventListener('readystatechange', this); rpc.addEventListener('timeout', this); rpc.addEventListener('error', this); rpc.timeout = 10000; try { rpc.send(JSON.stringify(request.payload)); } catch (error) { this.#log(2, error); } } rpccall_debug(method, args) { var tag = this.#rpccall_prepare(method, args); let request = this.#requests[tag]; request.success = console.info; this.#rpccall(tag); } handleEvent(e) { switch (e.type) { case 'readystatechange': if (e.target.readyState == 4) { let tag = e.target.tag; let request = this.#requests[tag]; switch (e.target.status) { case 200: if (!this.#isOnline) { this.#log(4, 'Server API connection estabilished'); this.#isOnline = true; this.#forceUpdateTorrents(); } //request.response = JSON.parse(e.target.responseText); if (request.success != null) { request.success(JSON.parse(e.target.responseText).arguments); } else { //this.#logger(e.target.responseText); } break; case 409: this.#sessionHeader = e.target.getResponseHeader('X-Transmission-Session-Id'); this.#rpccall(tag); break; default: request.response = {'code': e.target.status, 'content': e.target.responseText}; //this.#logger(JSON.stringify(request.response)); break; } } break; case 'timeout': if (this.#isOnline) { this.#log(3, 'Server API connection timed out'); this.#isOnline = false; this.#forceUpdateTorrents(); } break; case 'error': if (this.#isOnline) { this.#log(3, 'Server API refuses connection'); this.#isOnline = false; this.#forceUpdateTorrents(); } this.#log(5, JSON.stringify(e)); break; } } #forceUpdateTorrents() { for (const control of Object.values(this.#torrentControls)) { control.updateDOM(); } } #parseTorrents(response) { let updatedHashes = []; // Update torrent data for (const fromserver of response.torrents) { let hash = fromserver.hashString; let initdata = {}; for (const [key, value] of Object.entries(fromserver)) { let parsedValue = undefined; switch (key) { case 'hashString': break; case 'percentDone': parsedValue = Math.floor(value * 100); break; default: parsedValue = value; break; } if (parsedValue != undefined) { initdata[key] = parsedValue; } } let control; if (!(hash in this.#torrentControls)) { control = new trdom_torrentcontrol(hash, this, initdata, this.#logcb); let createControls = new CustomEvent('torrent-initialize', { detail: { torrentHash: hash, torrentControl: control, serverObject: this } }); this.dispatchEvent(createControls); } else { control = this.getControl(hash); } let e = new CustomEvent('torrent-updated', { detail: { torrentHash: hash, torrentInfo: initdata } }); control.dispatchEvent(e); updatedHashes.push(hash); } for (const [hash, control] of Object.entries(this.#torrentControls)) { if (!updatedHashes.includes(hash) && (control.exists)) { let e = new CustomEvent('torrent-deleted', { detail: { torrentHash: hash } }); control.dispatchEvent(e); } } } refreshTorrentList() { let tag = this.#rpccall_prepare('torrent-get', {'fields': ['id', 'hashString', 'name', 'status', 'percentDone', 'comment', 'magnetLink']}); this.#requests[tag].success = this.#parseTorrents.bind(this); this.#rpccall(tag); } torrent_add_url(url, dontstart, path) { let args = { 'filename': url, 'paused': dontstart }; if (path != null) { args['download-dir'] = path; } let tag = this.#rpccall_prepare('torrent-add', args); this.#rpccall(tag); } torrent_start(hash) { let tag = this.#rpccall_prepare('torrent-start', {'ids':[hash]}); this.#rpccall(tag); } torrent_pause(hash) { let tag = this.#rpccall_prepare('torrent-stop', {'ids':[hash]}); this.#rpccall(tag); } getInstanceData() { return { 'rpcurl': this.#rpcurl, 'auth': this.#authHeader }; } isOnline() { return this.#isOnline; } };