export * from './responses';
import {
TorrentIds, TorrentId, TransmissionTorrentAddSource
} from './shared';
import {
TransmissionSession as SessionResponse,
TransmissionSession_Mutable as SessionMutableFields,
TransmissionTorrent as TorrentResponse,
TransmissionTorrent_Mutable as TorrentMutableFields,
TransmissionTorrent_WriteOnly as TorrentWriteOnlyFields,
TransmissionRecentlyActiveTorrents as TransmissionRecentlyActiveResponse,
TransmissionSessionStats as SessionResponseStats,
TransmissionNewTorrent as NewTorrentResponse
} from './responses';
/* eslint-disable @typescript-eslint/no-explicit-any */
/**
* Converts an {@code TorrentIds} argument to an object with
* the associated fields for a request. This really only exists
* to make stripping out the ids field when {@code ids} is undefined
* more straightforward.
*
* @param ids that are going to be passed in the request.
* @returns an empty params object containing just the intended
* recipients for the current request.
*/
function torrentIdsToParam(ids: TorrentIds, extraProps?: any) {
// because an empty [] is also false :/
if (ids === undefined || ids === null) {
return {...extraProps}
}
return {ids: ids, ...extraProps}
}
/**
* The Puddle Transmission Interface.
*
* This class contains methods to automate the interaction between
* the puddle frontend and the transmission backend in a type safe
* way. Including automatic assigning of associated CSRF tokens or
* request tags between requests. Automatically casting returns
* values to appropriate interfaces, etc.
*
*/
export default abstract class Transmission {
url: string
sessionId: string = ''
requestTag?: number
constructor(url: string) {
this.url = url
}
/**
* Construct the body of a fetch request from a transmission
* request (an object containing a method and some method args).
*
* @returns an object that can be passed to {@code fetch}.
*/
private requestInit = (params: any): RequestInit => {
if (this.requestTag) {
// WARN mutates params in place
params.tag = this.requestTag
}
return {
method: 'POST',
headers: {
"Content-Type": "application/json",
'X-Transmission-Session-Id': this.sessionId,
},
body: JSON.stringify(params)
}
}
abstract fetch(url: string, body: any);
/**
* Perform a request to the associated transmission daemon.
*
* NOTE this method is public for debug purposes, you shouldn't
* ever need to call it directly. Instead you should use one of
* the extension methods to better interface with the transmission
* daemon.
*
* @param method the transmission request to perform (see the RPC
* specification).
* @param args the associated arguments for {@code method}.
* @returns a promise containing the JSON response of the request.
*/
request = (method: string, args: any) => {
const params = { method: method, 'arguments': args }
return this.fetch(this.url, this.requestInit(params))
.then(resp => {
// check and refresh the session id in case it has expired.
if (resp.status === 409) {
const sessionId = resp.headers.get('x-transmission-session-id')
if (sessionId) {
this.sessionId = sessionId
return this.fetch(this.url, this.requestInit(params))
}
}
if (!resp.ok) throw resp; else return resp;
})
.then(resp => resp.json())
.then(json => {
if (json.tag) {
this.requestTag = json.tag;
}
if (json.result !== "success") {
throw json
} else {
return json
}});
}
session = async () => {
return this.request('session-get', {})
.then(json => json["arguments"] as SessionResponse);
}
sessionStats = async () => {
return this.request('session-stats', {})
.then(json => json['arguments'] as SessionResponseStats)
}
/**
* Set values for the mutable fields of the current session
* instance. Compile time type safety is insured due to the
* reciever type, but there won't be any runtime safety.
*
* For those using vanilla JS bindings, make sure not to pass
* any fields in {@code props} that isn't in the underlying
* type.
*
* @param props the new assignments for the session fields.
*/
setSession = async (props: Partial<SessionMutableFields>) => {
return this.request('session-set', props);
}
moveTorrentsUp = async (torrents: TorrentIds) => {
return this.request('queue-move-up', torrentIdsToParam(torrents))
}
moveTorrentsDown = async (torrents: TorrentIds) => {
return this.request('queue-move-down', torrentIdsToParam(torrents))
}
moveTorrentsToTop = async (torrents: TorrentIds) => {
return this.request('queue-move-top', torrentIdsToParam(torrents))
}
moveTorrentsToBottom = async (torrents: TorrentIds) => {
return this.request('queue-move-bottom', torrentIdsToParam(torrents))
}
startTorrent = async (torrents: TorrentIds) => {
return this.request('torrent-start', torrentIdsToParam(torrents))
}
startTorrentNow = async (torrents: TorrentIds) => {
return this.request('torrent-start-now', torrentIdsToParam(torrents))
}
stopTorrent = async (torrents: TorrentIds) => {
return this.request('torrent-stop', torrentIdsToParam(torrents))
}
verifyTorrent = async (torrents: TorrentIds) => {
return this.request('torrent-verify', torrentIdsToParam(torrents))
}
reannounceTorrent = async (torrents: TorrentIds) => {
return this.request('torrent-reannounce', torrentIdsToParam(torrents))
}
setTorrentLocation = async (torrents: TorrentIds, location: string, move: boolean=false) => {
return this.request('torrent-set-location', torrentIdsToParam(torrents, {
location: location,
move: move
}))
}
removeTorrent = async (ids: TorrentIds, eraseLocalData: boolean) => {
return this.request('torrent-remove', torrentIdsToParam(ids, {
'delete-local-data': eraseLocalData
}))
}
addTorrent = async (
source: TransmissionTorrentAddSource,
downloadDir?: string,
cookies?: string,
paused: boolean = true,
// other possible parameters which I doubt we'll be using.
/* filesWanted: string[],
filesUnwanted: string[],
priorityHigh: string[],
priorityLow: string[],
priorityNormal: string[],
bandwidthPriority?: number,
peerLimit?: number, */
) => {
const props = source
if (undefined !== cookies) props['cookies'] = cookies
if (undefined !== downloadDir) props['download-dir'] = downloadDir
if (undefined !== paused) props['paused'] = paused
return this.request('torrent-add', props)
.then(resp => {
const value = resp['arguments']['torrent-duplicate'] ||
resp['arguments']['torrent-added'];
value.isDuplicate = !!resp['arguments']['torrent-duplicate']
return value as NewTorrentResponse
});
}
setTorrent = async (ids: TorrentIds, props: Partial<TorrentMutableFields & TorrentWriteOnlyFields>) => {
return this.request('torrent-set', torrentIdsToParam(ids, props))
}
/**
* An alias for {@code this.torrents} which operates specifically on a single
* torrent.
*/
torrent = async (id: TorrentId, ...fields: (keyof TorrentResponse)[]) => {
return this.torrents(id, ...fields)
.then(torrents => torrents[0]);
}
torrents = async (ids: TorrentIds, ...fields: (keyof TorrentResponse)[]) => {
// NOTE there may be a way to get even more helpful type information here.
// if typescripts Partial lets you specify at compile time the fields that
// the response definitely has. In practice that doesn't seem likely :cry:.
return this.request('torrent-get', torrentIdsToParam(ids, {fields: fields}))
.then(resp => resp['arguments'].torrents.map(torrent => torrent as Partial<TorrentResponse>))
}
/**
* Transmission supports a special field for the ids parameter in requests,
* that returns information about how the state of transmission has changed
* over the last [[https://github.com/transmission/transmission/issues/809][RECENTLY_ACTIVE_SECONDS]].
*/
recentlyActiveTorrents = async (...fields: (keyof TorrentResponse)[]) => {
return this.request('torrent-get', {ids: 'recently-active', fields: fields})
.then(resp => resp['arguments'] as TransmissionRecentlyActiveResponse)
}
}