From 95cf01f5988c9ef035f1b7a5ea02a4d60dc60d98 Mon Sep 17 00:00:00 2001 From: Jesse Chan Date: Wed, 7 Oct 2020 22:57:39 +0800 Subject: [PATCH] server: migrate torrent details functions to TypeScript --- .../components/general/Duration.tsx | 4 +- .../general/filesystem/DirectoryFileList.tsx | 6 +- .../general/filesystem/DirectoryTree.tsx | 2 +- .../general/filesystem/DirectoryTreeNode.tsx | 2 +- .../lists/TorrentDetailItemsList.js | 4 +- .../TorrentDetailsModal.tsx | 2 +- .../torrent-details-modal/TorrentFiles.tsx | 2 +- .../TorrentGeneralInfo.tsx | 6 - .../torrent-details-modal/TorrentPeers.tsx | 2 +- .../torrent-details-modal/TorrentTrackers.tsx | 2 +- .../javascript/constants/TorrentProperties.ts | 3 - client/src/javascript/stores/SettingsStore.ts | 1 - client/src/javascript/util/sortTorrents.ts | 3 +- package-lock.json | 6 + package.json | 1 + server/constants/fileListMethodCallConfigs.ts | 36 --- server/constants/rTorrentMethodCall.ts | 21 -- .../rTorrentMethodCallConfigs/index.ts | 5 + .../torrentContent.ts | 30 ++ .../torrentList.ts} | 254 +++++++--------- .../rTorrentMethodCallConfigs/torrentPeer.ts | 54 ++++ .../torrentTracker.ts | 30 ++ .../transferSummary.ts} | 24 +- server/models/ClientRequest.js | 11 - server/models/client.js | 32 +- server/routes/api/torrents.ts | 65 +++- server/services/clientGatewayService.ts | 277 ++++++++++-------- server/services/clientRequestManager.ts | 2 +- server/services/historyService.ts | 3 +- server/services/torrentService.ts | 25 +- server/util/clientResponseUtil.js | 145 --------- server/util/fileTreeUtil.js | 45 +++ server/util/rTorrentMethodCallUtil.ts | 50 ++++ server/util/rTorrentPropMap.ts | 12 - server/util/torrentPropertiesUtil.ts | 20 +- shared/constants/torrentPeerPropsMap.ts | 49 ---- shared/constants/torrentTrackerPropsMap.ts | 16 - shared/types/Torrent.ts | 16 +- .../TorrentContent.ts} | 7 - shared/types/TorrentPeer.ts | 15 + shared/types/TorrentTracker.ts | 9 + 41 files changed, 612 insertions(+), 687 deletions(-) delete mode 100644 server/constants/fileListMethodCallConfigs.ts delete mode 100644 server/constants/rTorrentMethodCall.ts create mode 100644 server/constants/rTorrentMethodCallConfigs/index.ts create mode 100644 server/constants/rTorrentMethodCallConfigs/torrentContent.ts rename server/constants/{torrentListMethodCallConfigs.ts => rTorrentMethodCallConfigs/torrentList.ts} (56%) create mode 100644 server/constants/rTorrentMethodCallConfigs/torrentPeer.ts create mode 100644 server/constants/rTorrentMethodCallConfigs/torrentTracker.ts rename server/constants/{transferSummaryMethodCallConfigs.ts => rTorrentMethodCallConfigs/transferSummary.ts} (66%) delete mode 100644 server/util/clientResponseUtil.js create mode 100644 server/util/fileTreeUtil.js create mode 100644 server/util/rTorrentMethodCallUtil.ts delete mode 100644 server/util/rTorrentPropMap.ts delete mode 100644 shared/constants/torrentPeerPropsMap.ts delete mode 100644 shared/constants/torrentTrackerPropsMap.ts rename shared/{constants/torrentFilePropsMap.ts => types/TorrentContent.ts} (69%) create mode 100644 shared/types/TorrentPeer.ts create mode 100644 shared/types/TorrentTracker.ts diff --git a/client/src/javascript/components/general/Duration.tsx b/client/src/javascript/components/general/Duration.tsx index 7a44c9c7..e3420d66 100644 --- a/client/src/javascript/components/general/Duration.tsx +++ b/client/src/javascript/components/general/Duration.tsx @@ -5,7 +5,7 @@ import type {Duration as DurationType} from '@shared/types/Torrent'; interface DurationProps { suffix?: React.ReactNode; - value: 'Infinity' | DurationType; + value: -1 | DurationType; } export default class Duration extends React.Component { @@ -23,7 +23,7 @@ export default class Duration extends React.Component { suffix = {suffix}; } - if (duration === 'Infinity') { + if (duration === -1) { content = ; } else if (duration.years != null && duration.years > 0) { content = [ diff --git a/client/src/javascript/components/general/filesystem/DirectoryFileList.tsx b/client/src/javascript/components/general/filesystem/DirectoryFileList.tsx index 65ead123..04833951 100644 --- a/client/src/javascript/components/general/filesystem/DirectoryFileList.tsx +++ b/client/src/javascript/components/general/filesystem/DirectoryFileList.tsx @@ -2,11 +2,7 @@ import classnames from 'classnames'; import PropTypes from 'prop-types'; import React from 'react'; -import type { - TorrentContent, - TorrentContentSelection, - TorrentContentSelectionTree, -} from '@shared/constants/torrentFilePropsMap'; +import type {TorrentContent, TorrentContentSelection, TorrentContentSelectionTree} from '@shared/types/TorrentContent'; import type {TorrentProperties} from '@shared/types/Torrent'; import {Checkbox} from '../../../ui'; diff --git a/client/src/javascript/components/general/filesystem/DirectoryTree.tsx b/client/src/javascript/components/general/filesystem/DirectoryTree.tsx index 0a535b07..17494f60 100644 --- a/client/src/javascript/components/general/filesystem/DirectoryTree.tsx +++ b/client/src/javascript/components/general/filesystem/DirectoryTree.tsx @@ -1,7 +1,7 @@ import PropTypes from 'prop-types'; import React from 'react'; -import type {TorrentContentSelection, TorrentContentSelectionTree} from '@shared/constants/torrentFilePropsMap'; +import type {TorrentContentSelection, TorrentContentSelectionTree} from '@shared/types/TorrentContent'; import type {TorrentDetails, TorrentProperties} from '@shared/types/Torrent'; import DirectoryFileList from './DirectoryFileList'; diff --git a/client/src/javascript/components/general/filesystem/DirectoryTreeNode.tsx b/client/src/javascript/components/general/filesystem/DirectoryTreeNode.tsx index ab38c52b..98d86f58 100644 --- a/client/src/javascript/components/general/filesystem/DirectoryTreeNode.tsx +++ b/client/src/javascript/components/general/filesystem/DirectoryTreeNode.tsx @@ -6,7 +6,7 @@ import type { TorrentContentSelection, TorrentContentSelectionTree, TorrentContentTree, -} from '@shared/constants/torrentFilePropsMap'; +} from '@shared/types/TorrentContent'; import type {TorrentProperties} from '@shared/types/Torrent'; import {Checkbox} from '../../../ui'; diff --git a/client/src/javascript/components/modals/settings-modal/lists/TorrentDetailItemsList.js b/client/src/javascript/components/modals/settings-modal/lists/TorrentDetailItemsList.js index d2aaa78d..4bdd8ffc 100644 --- a/client/src/javascript/components/modals/settings-modal/lists/TorrentDetailItemsList.js +++ b/client/src/javascript/components/modals/settings-modal/lists/TorrentDetailItemsList.js @@ -114,7 +114,9 @@ class TorrentDetailItemsList extends React.Component { render() { const lockedIDs = this.getLockedIDs(); - let torrentDetailItems = this.state.torrentDetails.slice(); + let torrentDetailItems = this.state.torrentDetails + .slice() + .filter((property) => Object.prototype.hasOwnProperty.call(TorrentProperties, property.id)); if (this.props.torrentListViewSize === 'expanded') { let nextUnlockedIndex = lockedIDs.length; diff --git a/client/src/javascript/components/modals/torrent-details-modal/TorrentDetailsModal.tsx b/client/src/javascript/components/modals/torrent-details-modal/TorrentDetailsModal.tsx index 8a82a30d..9ce3e7e0 100644 --- a/client/src/javascript/components/modals/torrent-details-modal/TorrentDetailsModal.tsx +++ b/client/src/javascript/components/modals/torrent-details-modal/TorrentDetailsModal.tsx @@ -17,7 +17,7 @@ import TorrentTrackers from './TorrentTrackers'; export interface TorrentDetailsModalProps extends WrappedComponentProps { options: {hash: TorrentProperties['hash']}; torrent?: TorrentProperties; - torrentDetails?: TorrentDetails; + torrentDetails?: TorrentDetails | null; } class TorrentDetailsModal extends React.Component { diff --git a/client/src/javascript/components/modals/torrent-details-modal/TorrentFiles.tsx b/client/src/javascript/components/modals/torrent-details-modal/TorrentFiles.tsx index f4436f88..84dcf98b 100644 --- a/client/src/javascript/components/modals/torrent-details-modal/TorrentFiles.tsx +++ b/client/src/javascript/components/modals/torrent-details-modal/TorrentFiles.tsx @@ -7,7 +7,7 @@ import type { TorrentContentSelection, TorrentContentSelectionTree, TorrentContentTree, -} from '@shared/constants/torrentFilePropsMap'; +} from '@shared/types/TorrentContent'; import type {TorrentProperties} from '@shared/types/Torrent'; import {Button, Checkbox, Form, FormRow, FormRowItem, Select, SelectItem} from '../../../ui'; diff --git a/client/src/javascript/components/modals/torrent-details-modal/TorrentGeneralInfo.tsx b/client/src/javascript/components/modals/torrent-details-modal/TorrentGeneralInfo.tsx index 8c14c0de..62d5a660 100644 --- a/client/src/javascript/components/modals/torrent-details-modal/TorrentGeneralInfo.tsx +++ b/client/src/javascript/components/modals/torrent-details-modal/TorrentGeneralInfo.tsx @@ -123,12 +123,6 @@ class TorrentGeneralInfo extends React.Component { - - - - - {torrent.comment || VALUE_NOT_AVAILABLE} - diff --git a/client/src/javascript/components/modals/torrent-details-modal/TorrentPeers.tsx b/client/src/javascript/components/modals/torrent-details-modal/TorrentPeers.tsx index 67b70314..a10cbd8f 100644 --- a/client/src/javascript/components/modals/torrent-details-modal/TorrentPeers.tsx +++ b/client/src/javascript/components/modals/torrent-details-modal/TorrentPeers.tsx @@ -1,7 +1,7 @@ import {FormattedMessage} from 'react-intl'; import React from 'react'; -import type {TorrentPeer} from '@shared/constants/torrentPeerPropsMap'; +import type {TorrentPeer} from '@shared/types/TorrentPeer'; import Badge from '../../general/Badge'; import Size from '../../general/Size'; diff --git a/client/src/javascript/components/modals/torrent-details-modal/TorrentTrackers.tsx b/client/src/javascript/components/modals/torrent-details-modal/TorrentTrackers.tsx index 4efcfeb6..30db6c8c 100644 --- a/client/src/javascript/components/modals/torrent-details-modal/TorrentTrackers.tsx +++ b/client/src/javascript/components/modals/torrent-details-modal/TorrentTrackers.tsx @@ -1,7 +1,7 @@ import {FormattedMessage} from 'react-intl'; import React from 'react'; -import type {TorrentTracker} from '@shared/constants/torrentTrackerPropsMap'; +import type {TorrentTracker} from '@shared/types/TorrentTracker'; import Badge from '../../general/Badge'; diff --git a/client/src/javascript/constants/TorrentProperties.ts b/client/src/javascript/constants/TorrentProperties.ts index 8c7c0b0d..17f27a57 100644 --- a/client/src/javascript/constants/TorrentProperties.ts +++ b/client/src/javascript/constants/TorrentProperties.ts @@ -45,9 +45,6 @@ const torrentProperties = { basePath: { id: 'torrents.properties.base.path', }, - comment: { - id: 'torrents.properties.comment', - }, hash: { id: 'torrents.properties.hash', }, diff --git a/client/src/javascript/stores/SettingsStore.ts b/client/src/javascript/stores/SettingsStore.ts index 4a6b7956..c146a7c0 100644 --- a/client/src/javascript/stores/SettingsStore.ts +++ b/client/src/javascript/stores/SettingsStore.ts @@ -85,7 +85,6 @@ class SettingsStoreClass extends BaseStore { {id: 'dateAdded', visible: true}, {id: 'dateCreated', visible: false}, {id: 'basePath', visible: false}, - {id: 'comment', visible: false}, {id: 'hash', visible: false}, {id: 'isPrivate', visible: false}, {id: 'message', visible: false}, diff --git a/client/src/javascript/util/sortTorrents.ts b/client/src/javascript/util/sortTorrents.ts index 45d5db15..798a0ed3 100644 --- a/client/src/javascript/util/sortTorrents.ts +++ b/client/src/javascript/util/sortTorrents.ts @@ -25,7 +25,7 @@ function sortTorrents(torrents: Array, sortBy: Readonly { - if (p.eta === 'Infinity') { + if (p.eta === -1) { return -1; } return p.eta.cumSeconds; @@ -40,7 +40,6 @@ function sortTorrents(torrents: Array, sortBy: Readonly { - return Number(value); -}; diff --git a/server/constants/rTorrentMethodCallConfigs/index.ts b/server/constants/rTorrentMethodCallConfigs/index.ts new file mode 100644 index 00000000..79e9ea32 --- /dev/null +++ b/server/constants/rTorrentMethodCallConfigs/index.ts @@ -0,0 +1,5 @@ +export {default as torrentContentMethodCallConfigs} from './torrentContent'; +export {default as torrentListMethodCallConfigs} from './torrentList'; +export {default as torrentPeerMethodCallConfigs} from './torrentPeer'; +export {default as torrentTrackerMethodCallConfigs} from './torrentTracker'; +export {default as transferSummaryMethodCallConfigs} from './transferSummary'; diff --git a/server/constants/rTorrentMethodCallConfigs/torrentContent.ts b/server/constants/rTorrentMethodCallConfigs/torrentContent.ts new file mode 100644 index 00000000..20240e57 --- /dev/null +++ b/server/constants/rTorrentMethodCallConfigs/torrentContent.ts @@ -0,0 +1,30 @@ +import {stringTransformer, stringArrayTransformer, numberTransformer} from '../../util/rTorrentMethodCallUtil'; + +const torrentContentMethodCallConfigs = { + path: { + methodCall: 'f.path=', + transformValue: stringTransformer, + }, + pathComponents: { + methodCall: 'f.path_components=', + transformValue: stringArrayTransformer, + }, + priority: { + methodCall: 'f.priority=', + transformValue: stringTransformer, + }, + sizeBytes: { + methodCall: 'f.size_bytes=', + transformValue: numberTransformer, + }, + sizeChunks: { + methodCall: 'f.size_chunks=', + transformValue: numberTransformer, + }, + completedChunks: { + methodCall: 'f.completed_chunks=', + transformValue: numberTransformer, + }, +} as const; + +export default torrentContentMethodCallConfigs; diff --git a/server/constants/torrentListMethodCallConfigs.ts b/server/constants/rTorrentMethodCallConfigs/torrentList.ts similarity index 56% rename from server/constants/torrentListMethodCallConfigs.ts rename to server/constants/rTorrentMethodCallConfigs/torrentList.ts index 6fa6d63c..679aa016 100644 --- a/server/constants/torrentListMethodCallConfigs.ts +++ b/server/constants/rTorrentMethodCallConfigs/torrentList.ts @@ -1,147 +1,109 @@ -import {defaultTransformer, booleanTransformer, numberTransformer} from './rTorrentMethodCall'; -import regEx from '../../shared/util/regEx'; +import regEx from '../../../shared/util/regEx'; +import {stringTransformer, booleanTransformer, numberTransformer} from '../../util/rTorrentMethodCallUtil'; -const torrentListMethodCallConfigs = [ - { - propLabel: 'hash', +const torrentListMethodCallConfigs = { + hash: { methodCall: 'd.hash=', - transformValue: defaultTransformer, + transformValue: stringTransformer, }, - { - propLabel: 'name', + name: { methodCall: 'd.name=', - transformValue: defaultTransformer, + transformValue: stringTransformer, }, - { - propLabel: 'message', + message: { methodCall: 'd.message=', - transformValue: defaultTransformer, + transformValue: stringTransformer, }, - { - propLabel: 'state', + state: { methodCall: 'd.state=', - transformValue: defaultTransformer, + transformValue: stringTransformer, }, - { - propLabel: 'isStateChanged', - methodCall: 'd.state_changed=', - transformValue: booleanTransformer, - }, - { - propLabel: 'isActive', + isActive: { methodCall: 'd.is_active=', transformValue: booleanTransformer, }, - { - propLabel: 'isComplete', + isComplete: { methodCall: 'd.complete=', transformValue: booleanTransformer, }, - { - propLabel: 'isHashing', - methodCall: 'd.hashing=', - transformValue: defaultTransformer, - }, - { - propLabel: 'isOpen', - methodCall: 'd.is_open=', - transformValue: booleanTransformer, - }, - { - propLabel: 'priority', - methodCall: 'd.priority=', - transformValue: numberTransformer, - }, - { - propLabel: 'upRate', - methodCall: 'd.up.rate=', - transformValue: numberTransformer, - }, - { - propLabel: 'upTotal', - methodCall: 'd.up.total=', - transformValue: numberTransformer, - }, - { - propLabel: 'downRate', - methodCall: 'd.down.rate=', - transformValue: numberTransformer, - }, - { - propLabel: 'downTotal', - methodCall: 'd.down.total=', - transformValue: numberTransformer, - }, - { - propLabel: 'ratio', - methodCall: 'd.ratio=', - transformValue: numberTransformer, - }, - { - propLabel: 'bytesDone', - methodCall: 'd.bytes_done=', - transformValue: numberTransformer, - }, - { - propLabel: 'sizeBytes', - methodCall: 'd.size_bytes=', - transformValue: numberTransformer, - }, - { - propLabel: 'directory', - methodCall: 'd.directory=', - transformValue: defaultTransformer, - }, - { - propLabel: 'basePath', - methodCall: 'd.base_path=', - transformValue: defaultTransformer, - }, - { - propLabel: 'baseFilename', - methodCall: 'd.base_filename=', - transformValue: defaultTransformer, - }, - { - propLabel: 'baseDirectory', - methodCall: 'd.directory_base=', - transformValue: defaultTransformer, - }, - { - propLabel: 'seedingTime', - methodCall: 'd.custom=seedingtime', - transformValue: defaultTransformer, - }, - { - propLabel: 'dateAdded', - methodCall: 'd.custom=addtime', - transformValue: numberTransformer, - }, - { - propLabel: 'dateCreated', - methodCall: 'd.creation_date=', - transformValue: numberTransformer, - }, - { - propLabel: 'throttleName', - methodCall: 'd.throttle_name=', - transformValue: defaultTransformer, - }, - { - propLabel: 'isMultiFile', + isMultiFile: { methodCall: 'd.is_multi_file=', transformValue: booleanTransformer, }, - { - propLabel: 'isPrivate', + isPrivate: { methodCall: 'd.is_private=', transformValue: booleanTransformer, }, - { - propLabel: 'tags', + isOpen: { + methodCall: 'd.is_open=', + transformValue: booleanTransformer, + }, + isHashing: { + methodCall: 'd.hashing=', + transformValue: (value: unknown): boolean => { + return value !== '0'; + }, + }, + priority: { + methodCall: 'd.priority=', + transformValue: numberTransformer, + }, + upRate: { + methodCall: 'd.up.rate=', + transformValue: numberTransformer, + }, + upTotal: { + methodCall: 'd.up.total=', + transformValue: numberTransformer, + }, + downRate: { + methodCall: 'd.down.rate=', + transformValue: numberTransformer, + }, + downTotal: { + methodCall: 'd.down.total=', + transformValue: numberTransformer, + }, + ratio: { + methodCall: 'd.ratio=', + transformValue: numberTransformer, + }, + bytesDone: { + methodCall: 'd.bytes_done=', + transformValue: numberTransformer, + }, + sizeBytes: { + methodCall: 'd.size_bytes=', + transformValue: numberTransformer, + }, + directory: { + methodCall: 'd.directory=', + transformValue: stringTransformer, + }, + basePath: { + methodCall: 'd.base_path=', + transformValue: stringTransformer, + }, + baseFilename: { + methodCall: 'd.base_filename=', + transformValue: stringTransformer, + }, + baseDirectory: { + methodCall: 'd.directory_base=', + transformValue: stringTransformer, + }, + dateAdded: { + methodCall: 'd.custom=addtime', + transformValue: numberTransformer, + }, + dateCreated: { + methodCall: 'd.creation_date=', + transformValue: numberTransformer, + }, + tags: { methodCall: 'd.custom1=', - transformValue: (value: string) => { - if (value === '') { + transformValue: (value: unknown): string[] => { + if (value === '' || typeof value !== 'string') { return []; } @@ -151,23 +113,13 @@ const torrentListMethodCallConfigs = [ .map((tag) => decodeURIComponent(tag)); }, }, - { - propLabel: 'comment', - methodCall: 'd.custom2=', - transformValue: (value: string) => { - let comment = decodeURIComponent(value); - - if (comment.match(/^VRS24mrker/)) { - comment = comment.substr(10); + trackerURIs: { + methodCall: 'cat="$t.multicall=d.hash=,t.is_enabled=,t.url=,cat={|||}"', + transformValue: (value: unknown): string[] => { + if (typeof value !== 'string') { + return []; } - return comment; - }, - }, - { - propLabel: 'trackerURIs', - methodCall: 'cat="$t.multicall=d.hash=,t.is_enabled=,t.url=,cat={|||}"', - transformValue: (value: string) => { const trackers = value.split('|||'); const trackerDomains: Array = []; @@ -203,26 +155,32 @@ const torrentListMethodCallConfigs = [ return [...new Set(trackerDomains)]; }, }, - { - propLabel: 'seedsConnected', + seedsConnected: { methodCall: 'd.peers_complete=', transformValue: numberTransformer, }, - { - propLabel: 'seedsTotal', + seedsTotal: { methodCall: 'cat="$t.multicall=d.hash=,t.scrape_complete=,cat={|||}"', - transformValue: (value: string) => Number(value.substr(0, value.indexOf('|||'))), + transformValue: (value: unknown): number => { + if (typeof value !== 'string') { + return 0; + } + return Number(value.substr(0, value.indexOf('|||'))); + }, }, - { - propLabel: 'peersConnected', + peersConnected: { methodCall: 'd.peers_accounted=', transformValue: numberTransformer, }, - { - propLabel: 'peersTotal', + peersTotal: { methodCall: 'cat="$t.multicall=d.hash=,t.scrape_incomplete=,cat={|||}"', - transformValue: (value: string) => Number(value.substr(0, value.indexOf('|||'))), + transformValue: (value: unknown): number => { + if (typeof value !== 'string') { + return 0; + } + return Number(value.substr(0, value.indexOf('|||'))); + }, }, -] as const; +} as const; export default torrentListMethodCallConfigs; diff --git a/server/constants/rTorrentMethodCallConfigs/torrentPeer.ts b/server/constants/rTorrentMethodCallConfigs/torrentPeer.ts new file mode 100644 index 00000000..a9d868dd --- /dev/null +++ b/server/constants/rTorrentMethodCallConfigs/torrentPeer.ts @@ -0,0 +1,54 @@ +import {stringTransformer, booleanTransformer, numberTransformer} from '../../util/rTorrentMethodCallUtil'; + +const torrentPeerMethodCallConfigs = { + id: { + methodCall: 'p.id=', + transformValue: stringTransformer, + }, + address: { + methodCall: 'p.address=', + transformValue: stringTransformer, + }, + clientVersion: { + methodCall: 'p.client_version=', + transformValue: stringTransformer, + }, + completedPercent: { + methodCall: 'p.completed_percent=', + transformValue: numberTransformer, + }, + downloadRate: { + methodCall: 'p.down_rate=', + transformValue: numberTransformer, + }, + downloadTotal: { + methodCall: 'p.down_total=', + transformValue: numberTransformer, + }, + uploadRate: { + methodCall: 'p.up_rate=', + transformValue: numberTransformer, + }, + uploadTotal: { + methodCall: 'p.up_total=', + transformValue: numberTransformer, + }, + peerRate: { + methodCall: 'p.peer_rate=', + transformValue: numberTransformer, + }, + peerTotal: { + methodCall: 'p.peer_total=', + transformValue: numberTransformer, + }, + isEncrypted: { + methodCall: 'p.is_encrypted=', + transformValue: booleanTransformer, + }, + isIncoming: { + methodCall: 'p.is_incoming=', + transformValue: booleanTransformer, + }, +} as const; + +export default torrentPeerMethodCallConfigs; diff --git a/server/constants/rTorrentMethodCallConfigs/torrentTracker.ts b/server/constants/rTorrentMethodCallConfigs/torrentTracker.ts new file mode 100644 index 00000000..e966f1df --- /dev/null +++ b/server/constants/rTorrentMethodCallConfigs/torrentTracker.ts @@ -0,0 +1,30 @@ +import {stringTransformer, numberTransformer} from '../../util/rTorrentMethodCallUtil'; + +const torrentTrackerMethodCallConfigs = { + id: { + methodCall: 't.id=', + transformValue: stringTransformer, + }, + url: { + methodCall: 't.url=', + transformValue: stringTransformer, + }, + type: { + methodCall: 't.type=', + transformValue: numberTransformer, + }, + group: { + methodCall: 't.group=', + transformValue: numberTransformer, + }, + minInterval: { + methodCall: 't.min_interval=', + transformValue: numberTransformer, + }, + normalInterval: { + methodCall: 't.normal_interval=', + transformValue: numberTransformer, + }, +} as const; + +export default torrentTrackerMethodCallConfigs; diff --git a/server/constants/transferSummaryMethodCallConfigs.ts b/server/constants/rTorrentMethodCallConfigs/transferSummary.ts similarity index 66% rename from server/constants/transferSummaryMethodCallConfigs.ts rename to server/constants/rTorrentMethodCallConfigs/transferSummary.ts index 312592e6..8223e914 100644 --- a/server/constants/transferSummaryMethodCallConfigs.ts +++ b/server/constants/rTorrentMethodCallConfigs/transferSummary.ts @@ -1,36 +1,30 @@ -import {numberTransformer} from './rTorrentMethodCall'; +import {numberTransformer} from '../../util/rTorrentMethodCallUtil'; -const transferSummaryMethodCallConfigs = [ - { - propLabel: 'upRate', +const transferSummaryMethodCallConfigs = { + upRate: { methodCall: 'throttle.global_up.rate', transformValue: numberTransformer, }, - { - propLabel: 'upTotal', + upTotal: { methodCall: 'throttle.global_up.total', transformValue: numberTransformer, }, - { - propLabel: 'upThrottle', + upThrottle: { methodCall: 'throttle.global_up.max_rate', transformValue: numberTransformer, }, - { - propLabel: 'downRate', + downRate: { methodCall: 'throttle.global_down.rate', transformValue: numberTransformer, }, - { - propLabel: 'downTotal', + downTotal: { methodCall: 'throttle.global_down.total', transformValue: numberTransformer, }, - { - propLabel: 'downThrottle', + downThrottle: { methodCall: 'throttle.global_down.max_rate', transformValue: numberTransformer, }, -] as const; +} as const; export default transferSummaryMethodCallConfigs; diff --git a/server/models/ClientRequest.js b/server/models/ClientRequest.js index b0df7f4c..c8a81c85 100644 --- a/server/models/ClientRequest.js +++ b/server/models/ClientRequest.js @@ -4,7 +4,6 @@ import util from 'util'; import {clientSettingsMap} from '../../shared/constants/clientSettingsMap'; -import rTorrentPropMap from '../util/rTorrentPropMap'; const getEnsuredArray = (item) => { if (!util.isArray(item)) { @@ -108,16 +107,6 @@ class ClientRequest { }); } - getTorrentDetails(options) { - const peerParams = [options.hash, ''].concat(options.peerProps); - const fileParams = [options.hash, ''].concat(options.fileProps); - const trackerParams = [options.hash, ''].concat(options.trackerProps); - - this.requests.push(getMethodCall('p.multicall', peerParams)); - this.requests.push(getMethodCall('f.multicall', fileParams)); - this.requests.push(getMethodCall('t.multicall', trackerParams)); - } - setSettings(options) { const settings = getEnsuredArray(options.settings); diff --git a/server/models/client.js b/server/models/client.js index f15000d0..d87f0b17 100644 --- a/server/models/client.js +++ b/server/models/client.js @@ -5,30 +5,26 @@ import {series} from 'async'; import tar from 'tar-stream'; import ClientRequest from './ClientRequest'; -import clientResponseUtil from '../util/clientResponseUtil'; import {clientSettingsBiMap} from '../../shared/constants/clientSettingsMap'; -import torrentFilePropsMap from '../../shared/constants/torrentFilePropsMap'; -import torrentPeerPropsMap from '../../shared/constants/torrentPeerPropsMap'; import torrentFileUtil from '../util/torrentFileUtil'; -import torrentTrackerPropsMap from '../../shared/constants/torrentTrackerPropsMap'; const client = { - downloadFiles(user, services, hash, fileString, res) { + downloadFiles(services, hash, fileString, res) { try { const selectedTorrent = services.torrentService.getTorrent(hash); if (!selectedTorrent) return res.status(404).json({error: 'Torrent not found.'}); - this.getTorrentDetails(user, services, hash, (torrentDetails) => { - if (!torrentDetails) return res.status(404).json({error: 'Torrent details not found'}); + services.clientGatewayService.getTorrentContents(hash).then((contents) => { + if (!contents) return res.status(404).json({error: 'Torrent contents not found'}); let files; if (!fileString || fileString === 'all') { - files = torrentDetails.fileTree.files.map((x, i) => `${i}`); + files = contents.files.map((x, i) => `${i}`); } else { files = fileString.split(','); } - const filePathsToDownload = this.findFilesByIndicies(files, torrentDetails.fileTree).map((file) => + const filePathsToDownload = this.findFilesByIndices(files, contents).map((file) => path.join(selectedTorrent.directory, file.path), ); @@ -76,7 +72,7 @@ const client = { } }, - findFilesByIndicies(indices, fileTree = {}) { + findFilesByIndices(indices, fileTree = {}) { const {directories, files = []} = fileTree; let selectedFiles = files.filter((file) => indices.includes(`${file.index}`)); @@ -84,7 +80,7 @@ const client = { if (directories != null) { selectedFiles = selectedFiles.concat( Object.keys(directories).reduce( - (accumulator, directory) => accumulator.concat(this.findFilesByIndicies(indices, directories[directory])), + (accumulator, directory) => accumulator.concat(this.findFilesByIndices(indices, directories[directory])), [], ), ); @@ -133,20 +129,6 @@ const client = { request.send(); }, - getTorrentDetails(user, services, hash, callback) { - const request = new ClientRequest(user, services); - - request.getTorrentDetails({ - hash, - fileProps: torrentFilePropsMap.methods, - peerProps: torrentPeerPropsMap.methods, - trackerProps: torrentTrackerPropsMap.methods, - }); - request.postProcess(clientResponseUtil.processTorrentDetails); - request.onComplete(callback); - request.send(); - }, - setSettings(user, services, payloads, callback) { const request = new ClientRequest(user, services); if (payloads.length === 0) return callback({}); diff --git a/server/routes/api/torrents.ts b/server/routes/api/torrents.ts index c11ebff8..b7c60c2f 100644 --- a/server/routes/api/torrents.ts +++ b/server/routes/api/torrents.ts @@ -337,13 +337,22 @@ router.patch('/tracker', (req, res) => { */ /** - * TODO: API not yet implemented * GET /api/torrents/{hash}/contents * @summary Gets the list of contents of a torrent and their properties. * @tags Torrent * @security AuthenticatedUser * @param {string} hash.path */ +router.get('/:hash/contents', (req, res) => { + const callback = ajaxUtil.getResponseFn(res); + + req.services?.clientGatewayService + .getTorrentContents(req.params.hash) + .then(callback) + .catch((err) => { + callback(null, err); + }); +}); /** * PATCH /api/torrents/{hash}/contents @@ -387,8 +396,22 @@ router.get('/:hash/contents/:indices/data', (req, res) => { * @security AuthenticatedUser * @param {string} hash.path */ -router.get('/:hash/details', (req, res) => { - client.getTorrentDetails(req.user, req.services, req.params.hash, ajaxUtil.getResponseFn(res)); +router.get('/:hash/details', async (req, res) => { + const callback = ajaxUtil.getResponseFn(res); + + try { + const contents = req.services?.clientGatewayService.getTorrentContents(req.params.hash); + const peers = req.services?.clientGatewayService.getTorrentPeers(req.params.hash); + const trackers = req.services?.clientGatewayService.getTorrentTrackers(req.params.hash); + + callback({ + fileTree: await contents, + peers: await peers, + trackers: await trackers, + }); + } catch (e) { + callback(null, e); + } }); /** @@ -402,4 +425,40 @@ router.get('/:hash/mediainfo', (req, res) => { mediainfo.getMediainfo(req.services, req.params.hash, ajaxUtil.getResponseFn(res)); }); +/** + * GET /api/torrents/{hash}/peers + * @summary Gets the list of peers of a torrent. + * @tags Torrent + * @security AuthenticatedUser + * @param {string} hash.path + */ +router.get('/:hash/peers', (req, res) => { + const callback = ajaxUtil.getResponseFn(res); + + req.services?.clientGatewayService + .getTorrentPeers(req.params.hash) + .then(callback) + .catch((err) => { + callback(null, err); + }); +}); + +/** + * GET /api/torrents/{hash}/trackers + * @summary Gets the list of trackers of a torrent. + * @tags Torrent + * @security AuthenticatedUser + * @param {string} hash.path + */ +router.get('/:hash/trackers', (req, res) => { + const callback = ajaxUtil.getResponseFn(res); + + req.services?.clientGatewayService + .getTorrentTrackers(req.params.hash) + .then(callback) + .catch((err) => { + callback(null, err); + }); +}); + export default router; diff --git a/server/services/clientGatewayService.ts b/server/services/clientGatewayService.ts index 501acaf0..aef3dd02 100644 --- a/server/services/clientGatewayService.ts +++ b/server/services/clientGatewayService.ts @@ -1,9 +1,13 @@ import path from 'path'; import fs from 'fs'; +import geoip from 'geoip-country'; import {moveSync} from 'fs-extra'; import type {RTorrentConnectionSettings} from '@shared/schema/ClientConnectionSettings'; +import type {TorrentContentTree} from '@shared/types/TorrentContent'; import type {TorrentList, TorrentListSummary, TorrentProperties} from '@shared/types/Torrent'; +import type {TorrentPeer} from '@shared/types/TorrentPeer'; +import type {TorrentTracker} from '@shared/types/TorrentTracker'; import type {TransferSummary} from '@shared/types/TransferData'; import type { AddTorrentByFileOptions, @@ -20,32 +24,37 @@ import type { import {accessDeniedError, createDirectory, isAllowedPath, sanitizePath} from '../util/fileUtil'; import BaseService from './BaseService'; -import {encodeTags} from '../util/torrentPropertiesUtil'; -import fileListMethodCallConfigs from '../constants/fileListMethodCallConfigs'; +import {getFileTreeFromPathsArr} from '../util/fileTreeUtil'; import scgiUtil from '../util/scgiUtil'; +import {getMethodCalls, processMethodCallResponse} from '../util/rTorrentMethodCallUtil'; +import { + encodeTags, + getTorrentETAFromProperties, + getTorrentPercentCompleteFromProperties, + getTorrentStatusFromProperties, +} from '../util/torrentPropertiesUtil'; +import { + torrentContentMethodCallConfigs, + torrentListMethodCallConfigs, + torrentPeerMethodCallConfigs, + torrentTrackerMethodCallConfigs, + transferSummaryMethodCallConfigs, +} from '../constants/rTorrentMethodCallConfigs'; -import type {MethodCallConfigs, MultiMethodCalls} from '../constants/rTorrentMethodCall'; +import type {MultiMethodCalls} from '../util/rTorrentMethodCallUtil'; -const filePathMethodCalls = fileListMethodCallConfigs - .filter((config) => config.propLabel === 'pathComponents') - .map((config) => config.methodCall); +const filePathMethodCalls = getMethodCalls({pathComponents: torrentContentMethodCallConfigs.pathComponents}); interface ClientGatewayServiceEvents { CLIENT_CONNECTION_STATE_CHANGE: () => void; PROCESS_TORRENT_LIST_START: () => void; - PROCESS_TORRENT_LIST_END: (processedTorrentList: {torrents: TorrentList}) => void; - PROCESS_TORRENT: (processedTorrentDetailValues: TorrentProperties) => void; + PROCESS_TORRENT_LIST_END: (torrentListSummary: TorrentListSummary) => void; + PROCESS_TORRENT: (torrentProperties: TorrentProperties) => void; PROCESS_TRANSFER_RATE_START: () => void; } -interface TorrentListReducer { - key: T; - reduce: (properties: Record) => TorrentProperties[T]; -} - class ClientGatewayService extends BaseService { hasError: boolean | null = null; - torrentListReducers: Array = []; constructor(...args: ConstructorParameters) { super(...args); @@ -54,28 +63,6 @@ class ClientGatewayService extends BaseService { this.processClientRequestSuccess = this.processClientRequestSuccess.bind(this); } - /** - * Adds a reducer to be applied when processing the torrent list. - * - * @param {Object} reducer - The reducer object - * @param {string} reducer.key - The key of the reducer, to be applied to the - * torrent list object. - * @param {function} reducer.reduce - The actual reducer. This will receive - * the entire processed torrent list response and it should return it own - * processed value, to be assigned to the provided key. - */ - addTorrentListReducer(reducer: T) { - if (typeof reducer.key !== 'string') { - throw new Error('reducer.key must be a string.'); - } - - if (typeof reducer.reduce !== 'function') { - throw new Error('reducer.reduce must be a function.'); - } - - this.torrentListReducers.push(reducer); - } - /** * Adds torrents by file * @@ -179,6 +166,78 @@ class ClientGatewayService extends BaseService { ); } + /** + * Gets the list of contents of a torrent. + * + * @param {string} hash - Hash of torrent + * @return {Promise} - Resolves with TorrentContentTree or rejects with error. + */ + async getTorrentContents(hash: TorrentProperties['hash']): Promise { + const configs = torrentContentMethodCallConfigs; + return ( + this.services?.clientRequestManager + .methodCall('f.multicall', [hash, ''].concat(getMethodCalls(configs))) + .then(this.processClientRequestSuccess, this.processClientRequestError) + .then((responses: string[][]) => { + return Promise.all(responses.map((response) => processMethodCallResponse(response, configs))); + }) + .then((processedResponses) => { + return processedResponses.reduce( + (memo, content, index) => getFileTreeFromPathsArr(memo, content.pathComponents[0], {index, ...content}), + {}, + ); + }) || Promise.reject() + ); + } + + /** + * Gets the list of peers of a torrent. + * + * @param {string} hash - Hash of torrent + * @return {Promise>} - Resolves with an array of TorrentPeer or rejects with error. + */ + async getTorrentPeers(hash: TorrentProperties['hash']): Promise> { + const configs = torrentPeerMethodCallConfigs; + return ( + this.services?.clientRequestManager + .methodCall('p.multicall', [hash, ''].concat(getMethodCalls(configs))) + .then(this.processClientRequestSuccess, this.processClientRequestError) + .then((responses: string[][]) => { + return Promise.all(responses.map((response) => processMethodCallResponse(response, configs))); + }) + .then((processedResponses) => { + return Promise.all( + processedResponses.map(async (processedResponse) => { + return { + ...processedResponse, + country: geoip.lookup(processedResponse.address)?.country || '', + }; + }), + ); + }) || Promise.reject() + ); + } + + /** + * Gets the list of trackers of a torrent. + * + * @param {string} hash - Hash of torrent + * @return {Promise>} - Resolves with an array of TorrentTracker or rejects with error. + */ + async getTorrentTrackers(hash: TorrentProperties['hash']): Promise> { + const configs = torrentTrackerMethodCallConfigs; + return ( + this.services?.clientRequestManager + .methodCall('t.multicall', [hash, ''].concat(getMethodCalls(configs))) + .then(this.processClientRequestSuccess, this.processClientRequestError) + .then((responses: string[][]) => { + return Promise.all( + responses.map((response) => processMethodCallResponse(response, configs) as Promise), + ); + }) || Promise.reject() + ); + } + /** * Moves torrents to specified destination path. * This function requires that the destination path is allowed by config. @@ -467,28 +526,62 @@ class ClientGatewayService extends BaseService { } /** - * Sends a multicall request to rTorrent with the requested method calls. + * Fetches the list of torrents * - * @param {MethodCallConfigs} configs - An array of method call config... - * @return {Promise} - Resolves with the processed client response or rejects - * with the processed client error. + * @return {Promise} - Resolves with TorrentListSummary or rejects with error. */ - async fetchTorrentList(configs: MethodCallConfigs) { + async fetchTorrentList(): Promise { + const configs = torrentListMethodCallConfigs; return ( this.services?.clientRequestManager - .methodCall('d.multicall2', ['', 'main'].concat(configs.map((config) => config.methodCall))) - .then(this.processClientRequestSuccess) - .then( - (torrents) => this.processTorrentListResponse(torrents as Array>, configs), - this.processClientRequestError, - ) || Promise.reject() + .methodCall('d.multicall2', ['', 'main'].concat(getMethodCalls(configs))) + .then(this.processClientRequestSuccess, this.processClientRequestError) + .then((responses: string[][]) => { + this.emit('PROCESS_TORRENT_LIST_START'); + return Promise.all(responses.map((response) => processMethodCallResponse(response, configs))); + }) + .then(async (processedResponses) => { + const torrentList: TorrentList = Object.assign( + {}, + ...(await Promise.all( + processedResponses.map(async (response) => { + const torrentProperties: TorrentProperties = { + ...response, + status: getTorrentStatusFromProperties(response), + percentComplete: getTorrentPercentCompleteFromProperties(response), + eta: getTorrentETAFromProperties(response), + }; + + this.emit('PROCESS_TORRENT', torrentProperties); + + return { + [response.hash]: torrentProperties, + }; + }), + )), + ); + + const torrentListSummary = { + id: Date.now(), + torrents: torrentList, + }; + + this.emit('PROCESS_TORRENT_LIST_END', torrentListSummary); + return torrentListSummary; + }) || Promise.reject() ); } - async fetchTransferSummary(configs: MethodCallConfigs) { - const methodCalls: MultiMethodCalls = configs.map((config) => { + /** + * Fetches the transfer summary + * + * @return {Promise} - Resolves with TransferSummary or rejects with error. + */ + async fetchTransferSummary(): Promise { + const configs = transferSummaryMethodCallConfigs; + const methodCalls: MultiMethodCalls = getMethodCalls(configs).map((methodCall) => { return { - methodName: config.methodCall, + methodName: methodCall, params: [], }; }); @@ -497,10 +590,10 @@ class ClientGatewayService extends BaseService { this.services?.clientRequestManager .methodCall('system.multicall', [methodCalls]) .then(this.processClientRequestSuccess) - .then( - (transferRate) => this.processTransferRateResponse(transferRate as Array, configs), - this.processClientRequestError, - ) || Promise.reject() + .then((response) => { + this.emit('PROCESS_TRANSFER_RATE_START'); + return processMethodCallResponse(response, configs); + }, this.processClientRequestError) || Promise.reject() ); } @@ -521,82 +614,6 @@ class ClientGatewayService extends BaseService { return Promise.reject(error); } - /** - * After rTorrent responds with the requested torrent details, we construct - * an object with hashes as keys and processed details as values. - * - * @param {Array} response - The array of all torrents and their details. - * @param {MethodCallConfigs} configs - An array of method call config... - * @return {Object} - An object that represents all torrents with hashes as - * keys, each value being an object of detail labels and values. - */ - async processTorrentListResponse( - torrentList: Array>, - configs: MethodCallConfigs, - ): Promise { - this.emit('PROCESS_TORRENT_LIST_START'); - - // We map the array of details to objects with sensibly named keys. We want - // to return an object with torrent hashes as keys and an object of torrent - // details as values. - const processedTorrentList = Object.assign( - {}, - ...(await Promise.all( - torrentList.map(async (torrentDetailValues) => { - // Transform the array of torrent detail values to an object with - // sensibly named keys. - const processingTorrentDetailValues = torrentDetailValues.reduce( - (accumulator: Record, value: string, index: number) => { - const {propLabel, transformValue} = configs[index]; - - accumulator[propLabel] = transformValue(value); - - return accumulator; - }, - {}, - ); - - // Assign values from external reducers to the torrent list object. - this.torrentListReducers.forEach((reducer) => { - const {key, reduce} = reducer; - processingTorrentDetailValues[key] = reduce(processingTorrentDetailValues); - }); - - const processedTorrentDetailValues = (processingTorrentDetailValues as unknown) as TorrentProperties; - - this.emit('PROCESS_TORRENT', processedTorrentDetailValues); - - return { - [processedTorrentDetailValues.hash]: processedTorrentDetailValues, - }; - }), - )), - ) as TorrentList; - - const torrentListSummary = { - id: Date.now(), - torrents: processedTorrentList, - }; - - this.emit('PROCESS_TORRENT_LIST_END', torrentListSummary); - - return torrentListSummary; - } - - async processTransferRateResponse(transferRate: Array, configs: MethodCallConfigs) { - this.emit('PROCESS_TRANSFER_RATE_START'); - - return Object.assign( - {}, - ...transferRate.map((value, index) => { - const {propLabel, transformValue} = configs[index]; - return { - [propLabel]: transformValue(value), - }; - }), - ) as TransferSummary; - } - testGateway(clientSettings?: RTorrentConnectionSettings) { if (clientSettings == null) { if (this.services != null && this.services.clientRequestManager != null) { diff --git a/server/services/clientRequestManager.ts b/server/services/clientRequestManager.ts index 009d6608..0d6bea50 100644 --- a/server/services/clientRequestManager.ts +++ b/server/services/clientRequestManager.ts @@ -1,7 +1,7 @@ import BaseService from './BaseService'; import scgiUtil from '../util/scgiUtil'; -import type {MultiMethodCalls} from '../constants/rTorrentMethodCall'; +import type {MultiMethodCalls} from '../util/rTorrentMethodCallUtil'; type MethodCallParameters = Array; diff --git a/server/services/historyService.ts b/server/services/historyService.ts index 548f2e1b..ef8f881d 100644 --- a/server/services/historyService.ts +++ b/server/services/historyService.ts @@ -7,7 +7,6 @@ import config from '../../config'; import HistoryEra from '../models/HistoryEra'; import historySnapshotTypes from '../../shared/constants/historySnapshotTypes'; import objectUtil from '../../shared/util/objectUtil'; -import transferSummaryMethodCallConfigs from '../constants/transferSummaryMethodCallConfigs'; type HistorySnapshotEvents = { // TODO: Switch to string literal template type when TypeScript 4.1 is released. @@ -148,7 +147,7 @@ class HistoryService extends BaseService { } this.services?.clientGatewayService - .fetchTransferSummary(transferSummaryMethodCallConfigs) + .fetchTransferSummary() .then(this.handleFetchTransferSummarySuccess.bind(this)) .catch(this.handleFetchTransferSummaryError.bind(this)); } diff --git a/server/services/torrentService.ts b/server/services/torrentService.ts index 7f880b93..f01e0274 100644 --- a/server/services/torrentService.ts +++ b/server/services/torrentService.ts @@ -4,14 +4,8 @@ import type {TorrentProperties, TorrentListDiff, TorrentListSummary} from '@shar import BaseService from './BaseService'; import config from '../../config'; -import torrentListMethodCallConfigs from '../constants/torrentListMethodCallConfigs'; -import { - getTorrentETAFromProperties, - getTorrentPercentCompleteFromProperties, - getTorrentStatusFromProperties, - hasTorrentFinished, -} from '../util/torrentPropertiesUtil'; +import {hasTorrentFinished} from '../util/torrentPropertiesUtil'; interface TorrentServiceEvents { FETCH_TORRENT_LIST_SUCCESS: () => void; @@ -42,21 +36,6 @@ class TorrentService extends BaseService { const {clientGatewayService} = this.services; - clientGatewayService.addTorrentListReducer({ - key: 'status', - reduce: getTorrentStatusFromProperties, - }); - - clientGatewayService.addTorrentListReducer({ - key: 'percentComplete', - reduce: getTorrentPercentCompleteFromProperties, - }); - - clientGatewayService.addTorrentListReducer({ - key: 'eta', - reduce: getTorrentETAFromProperties, - }); - clientGatewayService.on('PROCESS_TORRENT', this.handleTorrentProcessed); this.fetchTorrentList(); @@ -141,7 +120,7 @@ class TorrentService extends BaseService { } return this.services?.clientGatewayService - .fetchTorrentList(torrentListMethodCallConfigs) + .fetchTorrentList() .then(this.handleFetchTorrentListSuccess) .catch(this.handleFetchTorrentListError); } diff --git a/server/util/clientResponseUtil.js b/server/util/clientResponseUtil.js deleted file mode 100644 index e25d9695..00000000 --- a/server/util/clientResponseUtil.js +++ /dev/null @@ -1,145 +0,0 @@ -import geoip from 'geoip-country'; -import truncateTo from './numberUtils'; -import torrentFilePropsMap from '../../shared/constants/torrentFilePropsMap'; -import torrentPeerPropsMap from '../../shared/constants/torrentPeerPropsMap'; -import torrentTrackerPropsMap from '../../shared/constants/torrentTrackerPropsMap'; - -const processFile = (file) => { - file.filename = file.pathComponents[file.pathComponents.length - 1]; - file.percentComplete = truncateTo((file.completedChunks / file.sizeChunks) * 100); - file.priority = Number(file.priority); - file.sizeBytes = Number(file.sizeBytes); - - delete file.completedChunks; - delete file.pathComponents; - delete file.sizeChunks; - - return file; -}; - -const getFileTreeFromPathsArr = (tree, directory, file, depth) => { - if (depth == null) { - depth = 0; - } - - if (tree == null) { - tree = {}; - } - - if (depth++ < file.pathComponents.length - 1) { - if (!tree.directories) { - tree.directories = {}; - } - - tree.directories[directory] = getFileTreeFromPathsArr( - tree.directories[directory], - file.pathComponents[depth], - file, - depth, - ); - } else { - if (!tree.files) { - tree.files = []; - } - - tree.files.push(processFile(file)); - } - - return tree; -}; - -const mapPropsToResponse = (requestedKeys, clientResponse) => { - if (clientResponse.length === 0) { - return []; - } - - // clientResponse is always an array of arrays. - if (clientResponse[0].length === 1) { - // When the length of the nested arrays is 1, the nested arrays represent a - // singular requested value (e.g. total data transferred or current upload - // speed). Therefore we construct an object where the requested keys map to - // their values. - return clientResponse.reduce((memo, value, index) => { - const singleValue = value[0]; - memo[requestedKeys[index]] = singleValue; - - return memo; - }, {}); - } - // When the length of the nested arrays is more than 1, the nested arrays - // represent one of many items of the same type (e.g. a list of torrents, - // peers, files, etc). Therefore we construct an array of objects, where each - // object contains all of the requested keys and its value. We add an index - // for each item, a requirement for file lists. - return clientResponse.map( - (listItem, index) => - listItem.reduce( - (nestedMemo, value, nestedIndex) => { - nestedMemo[requestedKeys[nestedIndex]] = value; - - return nestedMemo; - }, - {index}, - ), - [], - ); -}; - -const processTorrentDetails = (data) => { - // TODO: This is ugly. - const peersData = data[0][0] || null; - const filesData = data[1][0] || null; - const trackerData = data[2][0] || null; - let peers = null; - let files = null; - let trackers = null; - let fileTree = {}; - - if (peersData && peersData.length) { - peers = mapPropsToResponse(torrentPeerPropsMap.props, peersData).map((peer) => { - const geoData = geoip.lookup(peer.address) || {}; - peer.country = geoData.country; - - // Strings to boolean - peer.isEncrypted = peer.isEncrypted === '1'; - peer.isIncoming = peer.isIncoming === '1'; - - // Strings to number - peer.completedPercent = Number(peer.completedPercent); - peer.downloadRate = Number(peer.downloadRate); - peer.downloadTotal = Number(peer.downloadTotal); - peer.uploadRate = Number(peer.uploadRate); - peer.uploadTotal = Number(peer.uploadTotal); - peer.peerRate = Number(peer.peerRate); - peer.peerTotal = Number(peer.peerTotal); - - return peer; - }); - } - - if (filesData && filesData.length) { - files = mapPropsToResponse(torrentFilePropsMap.props, filesData); - fileTree = files.reduce((memo, file) => getFileTreeFromPathsArr(memo, file.pathComponents[0], file), {}); - } - - if (trackerData && trackerData.length) { - trackers = mapPropsToResponse(torrentTrackerPropsMap.props, trackerData).map((tracker) => { - tracker.group = Number(tracker.group); - tracker.minInterval = Number(tracker.minInterval); - tracker.normalInterval = Number(tracker.normalInterval); - tracker.type = Number(tracker.type); - - return tracker; - }); - } - - return {peers, trackers, fileTree}; -}; - -const clientResponseUtil = { - mapPropsToResponse, - processFile, - processTorrentDetails, -}; - -export default clientResponseUtil; diff --git a/server/util/fileTreeUtil.js b/server/util/fileTreeUtil.js new file mode 100644 index 00000000..ef153729 --- /dev/null +++ b/server/util/fileTreeUtil.js @@ -0,0 +1,45 @@ +import truncateTo from './numberUtils'; + +const processFile = (file) => { + file.filename = file.pathComponents[file.pathComponents.length - 1]; + file.percentComplete = truncateTo((file.completedChunks / file.sizeChunks) * 100); + file.priority = Number(file.priority); + file.sizeBytes = Number(file.sizeBytes); + + delete file.completedChunks; + delete file.pathComponents; + delete file.sizeChunks; + + return file; +}; + +export const getFileTreeFromPathsArr = (tree, directory, file, depth) => { + if (depth == null) { + depth = 0; + } + + if (tree == null) { + tree = {}; + } + + if (depth++ < file.pathComponents.length - 1) { + if (!tree.directories) { + tree.directories = {}; + } + + tree.directories[directory] = getFileTreeFromPathsArr( + tree.directories[directory], + file.pathComponents[depth], + file, + depth, + ); + } else { + if (!tree.files) { + tree.files = []; + } + + tree.files.push(processFile(file)); + } + + return tree; +}; diff --git a/server/util/rTorrentMethodCallUtil.ts b/server/util/rTorrentMethodCallUtil.ts new file mode 100644 index 00000000..fb5c3902 --- /dev/null +++ b/server/util/rTorrentMethodCallUtil.ts @@ -0,0 +1,50 @@ +export interface MethodCallConfig { + readonly methodCall: string; + readonly transformValue: (value: unknown) => string | boolean | number | string[]; +} + +export type MethodCallConfigs = Readonly<{ + [propLabel: string]: MethodCallConfig; +}>; + +export type MultiMethodCalls = Array<{methodName: string; params: Array}>; + +export const stringTransformer = (value: unknown): string => { + return value as string; +}; + +export const stringArrayTransformer = (value: unknown): string[] => { + return value as string[]; +}; + +export const booleanTransformer = (value: unknown): boolean => { + return value === '1'; +}; + +export const numberTransformer = (value: unknown): number => { + return Number(value); +}; + +export const getMethodCalls = (configs: MethodCallConfigs) => { + return Object.values(configs).map((config) => config.methodCall); +}; + +export const processMethodCallResponse = async ( + response: Array[0]>, + configs: T, +): Promise< + { + [propLabel in P]: ReturnType; + } +> => { + return Object.assign( + {}, + ...(await Promise.all( + Object.keys(configs).map(async (propLabel, index) => { + return { + [propLabel]: configs[propLabel].transformValue(response[index]), + }; + }), + )), + ); +}; diff --git a/server/util/rTorrentPropMap.ts b/server/util/rTorrentPropMap.ts deleted file mode 100644 index e352f534..00000000 --- a/server/util/rTorrentPropMap.ts +++ /dev/null @@ -1,12 +0,0 @@ -const RTORRENT_PROPS_MAP = { - transferData: { - uploadRate: 'throttle.global_up.rate', - uploadTotal: 'throttle.global_up.total', - uploadThrottle: 'throttle.global_up.max_rate', - downloadRate: 'throttle.global_down.rate', - downloadTotal: 'throttle.global_down.total', - downloadThrottle: 'throttle.global_down.max_rate', - }, -} as const; - -export default RTORRENT_PROPS_MAP; diff --git a/server/util/torrentPropertiesUtil.ts b/server/util/torrentPropertiesUtil.ts index 0848d179..69573362 100644 --- a/server/util/torrentPropertiesUtil.ts +++ b/server/util/torrentPropertiesUtil.ts @@ -4,21 +4,25 @@ import truncateTo from './numberUtils'; import type {TorrentProperties} from '../../shared/types/Torrent'; import type {TorrentStatus} from '../../shared/constants/torrentStatusMap'; -export const getTorrentETAFromProperties = (processingTorrentProperties: Record) => { +export const getTorrentETAFromProperties = ( + processingTorrentProperties: Record, +): TorrentProperties['eta'] => { const {downRate, bytesDone, sizeBytes} = processingTorrentProperties; if (typeof downRate !== 'number' || typeof bytesDone !== 'number' || typeof sizeBytes !== 'number') { - return Infinity; + return -1; } if (downRate > 0) { return formatUtil.secondsToDuration((sizeBytes - bytesDone) / downRate); } - return Infinity; + return -1; }; -export const getTorrentPercentCompleteFromProperties = (processingTorrentProperties: Record) => { +export const getTorrentPercentCompleteFromProperties = ( + processingTorrentProperties: Record, +): TorrentProperties['percentComplete'] => { const {bytesDone, sizeBytes} = processingTorrentProperties; if (typeof bytesDone !== 'number' || typeof sizeBytes !== 'number') { @@ -37,12 +41,14 @@ export const getTorrentPercentCompleteFromProperties = (processingTorrentPropert return percentComplete; }; -export const getTorrentStatusFromProperties = (processingTorrentProperties: Record) => { +export const getTorrentStatusFromProperties = ( + processingTorrentProperties: Record, +): TorrentProperties['status'] => { const {isHashing, isComplete, isOpen, upRate, downRate, state, message} = processingTorrentProperties; const torrentStatus: Array = []; - if (isHashing !== '0') { + if (isHashing) { torrentStatus.push('checking'); } else if (isComplete && isOpen && state === '1') { torrentStatus.push('complete'); @@ -84,7 +90,7 @@ export const getTorrentStatusFromProperties = (processingTorrentProperties: Reco export const hasTorrentFinished = ( prevData: Partial = {}, nextData: Partial = {}, -) => { +): boolean => { if (prevData.status != null && prevData.status.includes('checking')) { return false; } diff --git a/shared/constants/torrentPeerPropsMap.ts b/shared/constants/torrentPeerPropsMap.ts deleted file mode 100644 index 2503e84c..00000000 --- a/shared/constants/torrentPeerPropsMap.ts +++ /dev/null @@ -1,49 +0,0 @@ -const torrentPeerPropsMap = { - props: [ - 'address', - 'completedPercent', - 'clientVersion', - 'downloadRate', - 'downloadTotal', - 'uploadRate', - 'uploadTotal', - 'id', - 'peerRate', - 'peerTotal', - 'isEncrypted', - 'isIncoming', - ], - methods: [ - 'p.address=', - 'p.completed_percent=', - 'p.client_version=', - 'p.down_rate=', - 'p.down_total=', - 'p.up_rate=', - 'p.up_total=', - 'p.id=', - 'p.peer_rate=', - 'p.peer_total=', - 'p.is_encrypted=', - 'p.is_incoming=', - ], -} as const; - -export interface TorrentPeer { - index: number; - country: string; - address: string; - completedPercent: number; - clientVersion: string; - downloadRate: number; - downloadTotal: number; - uploadRate: number; - uploadTotal: number; - id: string; - peerRate: number; - peerTotal: number; - isEncrypted: boolean; - isIncoming: boolean; -} - -export default torrentPeerPropsMap; diff --git a/shared/constants/torrentTrackerPropsMap.ts b/shared/constants/torrentTrackerPropsMap.ts deleted file mode 100644 index d74e6760..00000000 --- a/shared/constants/torrentTrackerPropsMap.ts +++ /dev/null @@ -1,16 +0,0 @@ -const torrentTrackerPropsMap = { - props: ['group', 'url', 'id', 'minInterval', 'normalInterval', 'type'], - methods: ['t.group=', 't.url=', 't.id=', 't.min_interval=', 't.normal_interval=', 't.type='], -} as const; - -export interface TorrentTracker { - index: number; - id: string; - url: string; - type: number; - group: number; - minInterval: number; - normalInterval: number; -} - -export default torrentTrackerPropsMap; diff --git a/shared/types/Torrent.ts b/shared/types/Torrent.ts index 6974d81d..474a0117 100644 --- a/shared/types/Torrent.ts +++ b/shared/types/Torrent.ts @@ -1,7 +1,7 @@ -import type {TorrentContentTree} from '../constants/torrentFilePropsMap'; -import type {TorrentPeer} from '../constants/torrentPeerPropsMap'; +import type {TorrentContentTree} from './TorrentContent'; +import type {TorrentPeer} from './TorrentPeer'; import type {TorrentStatus} from '../constants/torrentStatusMap'; -import type {TorrentTracker} from '../constants/torrentTrackerPropsMap'; +import type {TorrentTracker} from './TorrentTracker'; export interface Duration { years?: number; @@ -26,22 +26,20 @@ export interface TorrentProperties { baseFilename: string; basePath: string; bytesDone: number; - comment: string; dateAdded: number; dateCreated: number; - details: TorrentDetails; + details?: TorrentDetails; directory: string; downRate: number; downTotal: number; - eta: 'Infinity' | Duration; + eta: -1 | Duration; hash: string; isActive: boolean; isComplete: boolean; - isHashing: string; + isHashing: boolean; isMultiFile: boolean; isOpen: boolean; isPrivate: boolean; - isStateChanged: boolean; message: string; name: string; peersConnected: number; @@ -49,14 +47,12 @@ export interface TorrentProperties { percentComplete: number; priority: number; ratio: number; - seedingTime: string; seedsConnected: number; seedsTotal: number; sizeBytes: number; state: string; status: Array; tags: Array; - throttleName: string; trackerURIs: Array; upRate: number; upTotal: number; diff --git a/shared/constants/torrentFilePropsMap.ts b/shared/types/TorrentContent.ts similarity index 69% rename from shared/constants/torrentFilePropsMap.ts rename to shared/types/TorrentContent.ts index e43e612c..f036bc56 100644 --- a/shared/constants/torrentFilePropsMap.ts +++ b/shared/types/TorrentContent.ts @@ -1,8 +1,3 @@ -const torrentFilePropsMap = { - props: ['path', 'pathComponents', 'priority', 'sizeBytes', 'sizeChunks', 'completedChunks'], - methods: ['f.path=', 'f.path_components=', 'f.priority=', 'f.size_bytes=', 'f.size_chunks=', 'f.completed_chunks='], -} as const; - export interface TorrentContent { index: number; path: string; @@ -35,5 +30,3 @@ export interface TorrentContentSelectionTree { [directoryName: string]: TorrentContentSelectionTree; }; } - -export default torrentFilePropsMap; diff --git a/shared/types/TorrentPeer.ts b/shared/types/TorrentPeer.ts new file mode 100644 index 00000000..804121bd --- /dev/null +++ b/shared/types/TorrentPeer.ts @@ -0,0 +1,15 @@ +export interface TorrentPeer { + country: string; + address: string; + completedPercent: number; + clientVersion: string; + downloadRate: number; + downloadTotal: number; + uploadRate: number; + uploadTotal: number; + id: string; + peerRate: number; + peerTotal: number; + isEncrypted: boolean; + isIncoming: boolean; +} diff --git a/shared/types/TorrentTracker.ts b/shared/types/TorrentTracker.ts new file mode 100644 index 00000000..454b9dfd --- /dev/null +++ b/shared/types/TorrentTracker.ts @@ -0,0 +1,9 @@ +export interface TorrentTracker { + index: number; + id: string; + url: string; + type: number; + group: number; + minInterval: number; + normalInterval: number; +}