server: migrate torrent details functions to TypeScript

This commit is contained in:
Jesse Chan
2020-10-07 22:57:39 +08:00
parent e0e68a19df
commit 95cf01f598
41 changed files with 612 additions and 687 deletions

View File

@@ -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<DurationProps> {
@@ -23,7 +23,7 @@ export default class Duration extends React.Component<DurationProps> {
suffix = <span className="duration--segment">{suffix}</span>;
}
if (duration === 'Infinity') {
if (duration === -1) {
content = <FormattedMessage id="unit.time.infinity" />;
} else if (duration.years != null && duration.years > 0) {
content = [

View File

@@ -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';

View File

@@ -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';

View File

@@ -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';

View File

@@ -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;

View File

@@ -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<TorrentDetailsModalProps> {

View File

@@ -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';

View File

@@ -123,12 +123,6 @@ class TorrentGeneralInfo extends React.Component<TorrentGeneralInfoProps> {
<FormattedMessage id="torrents.details.general.heading.torrent" />
</td>
</tr>
<tr className="torrent-details__detail torrent-details__detail--comment">
<td className="torrent-details__detail__label">
<FormattedMessage id="torrents.details.general.comment" />
</td>
<td className="torrent-details__detail__value">{torrent.comment || VALUE_NOT_AVAILABLE}</td>
</tr>
<tr className="torrent-details__detail torrent-details__detail--created">
<td className="torrent-details__detail__label">
<FormattedMessage id="torrents.details.general.date.created" />

View File

@@ -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';

View File

@@ -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';

View File

@@ -45,9 +45,6 @@ const torrentProperties = {
basePath: {
id: 'torrents.properties.base.path',
},
comment: {
id: 'torrents.properties.comment',
},
hash: {
id: 'torrents.properties.hash',
},

View File

@@ -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},

View File

@@ -25,7 +25,7 @@ function sortTorrents(torrents: Array<TorrentProperties>, sortBy: Readonly<Flood
case 'eta':
sortRules.push({
[sortBy.direction]: (p: TorrentProperties) => {
if (p.eta === 'Infinity') {
if (p.eta === -1) {
return -1;
}
return p.eta.cumSeconds;
@@ -40,7 +40,6 @@ function sortTorrents(torrents: Array<TorrentProperties>, sortBy: Readonly<Flood
} as SortRule);
break;
case 'basePath':
case 'comment':
case 'hash':
case 'message':
case 'name':

6
package-lock.json generated
View File

@@ -2373,6 +2373,12 @@
"@types/node": "*"
}
},
"@types/geoip-country": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/@types/geoip-country/-/geoip-country-4.0.0.tgz",
"integrity": "sha512-RngLyEh1cMcH/fphQa4+AiMJX+t0/kD/CijkRCgZzQWwFE5ZnSP/WxVhcMAHfTY7fNgZvWtkgeKBc96dsEss9Q==",
"dev": true
},
"@types/geojson": {
"version": "7946.0.7",
"resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.7.tgz",

View File

@@ -61,6 +61,7 @@
"@types/express-rate-limit": "^5.1.0",
"@types/flux": "^3.1.9",
"@types/fs-extra": "^9.0.1",
"@types/geoip-country": "^4.0.0",
"@types/http-errors": "^1.8.0",
"@types/jest": "^26.0.14",
"@types/jsonwebtoken": "^8.5.0",

View File

@@ -1,36 +0,0 @@
import {defaultTransformer, numberTransformer} from './rTorrentMethodCall';
const fileListMethodCallConfigs = [
{
propLabel: 'path',
methodCall: 'f.path=',
transformValue: defaultTransformer,
},
{
propLabel: 'pathComponents',
methodCall: 'f.path_components=',
transformValue: defaultTransformer,
},
{
propLabel: 'priority',
methodCall: 'f.priority=',
transformValue: defaultTransformer,
},
{
propLabel: 'sizeBytes',
methodCall: 'f.size_bytes=',
transformValue: numberTransformer,
},
{
propLabel: 'sizeChunks',
methodCall: 'f.size_chunks=',
transformValue: numberTransformer,
},
{
propLabel: 'completedChunks',
methodCall: 'f.completed_chunks=',
transformValue: numberTransformer,
},
] as const;
export default fileListMethodCallConfigs;

View File

@@ -1,21 +0,0 @@
export interface MethodCallConfig {
readonly propLabel: string;
readonly methodCall: string;
readonly transformValue: (value: string) => string | string[] | boolean | number;
}
export type MethodCallConfigs = Readonly<Array<MethodCallConfig>>;
export type MultiMethodCalls = Array<{methodName: string; params: Array<string | Buffer>}>;
export const defaultTransformer = (value: string): string => {
return value;
};
export const booleanTransformer = (value: string): boolean => {
return value === '1';
};
export const numberTransformer = (value: string): number => {
return Number(value);
};

View File

@@ -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';

View File

@@ -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;

View File

@@ -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<string> = [];
@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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);

View File

@@ -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({});

View File

@@ -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;

View File

@@ -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<T extends keyof TorrentProperties = keyof TorrentProperties> {
key: T;
reduce: (properties: Record<string, unknown>) => TorrentProperties[T];
}
class ClientGatewayService extends BaseService<ClientGatewayServiceEvents> {
hasError: boolean | null = null;
torrentListReducers: Array<TorrentListReducer> = [];
constructor(...args: ConstructorParameters<typeof BaseService>) {
super(...args);
@@ -54,28 +63,6 @@ class ClientGatewayService extends BaseService<ClientGatewayServiceEvents> {
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<T extends TorrentListReducer>(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<ClientGatewayServiceEvents> {
);
}
/**
* Gets the list of contents of a torrent.
*
* @param {string} hash - Hash of torrent
* @return {Promise<TorrentContentTree>} - Resolves with TorrentContentTree or rejects with error.
*/
async getTorrentContents(hash: TorrentProperties['hash']): Promise<TorrentContentTree> {
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<Array<TorrentPeer>>} - Resolves with an array of TorrentPeer or rejects with error.
*/
async getTorrentPeers(hash: TorrentProperties['hash']): Promise<Array<TorrentPeer>> {
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<Array<TorrentTracker>>} - Resolves with an array of TorrentTracker or rejects with error.
*/
async getTorrentTrackers(hash: TorrentProperties['hash']): Promise<Array<TorrentTracker>> {
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<TorrentTracker>),
);
}) || 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<ClientGatewayServiceEvents> {
}
/**
* 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<TorrentListSummary>} - Resolves with TorrentListSummary or rejects with error.
*/
async fetchTorrentList(configs: MethodCallConfigs) {
async fetchTorrentList(): Promise<TorrentListSummary> {
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<Array<string>>, 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<TransferSummary>} - Resolves with TransferSummary or rejects with error.
*/
async fetchTransferSummary(): Promise<TransferSummary> {
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<ClientGatewayServiceEvents> {
this.services?.clientRequestManager
.methodCall('system.multicall', [methodCalls])
.then(this.processClientRequestSuccess)
.then(
(transferRate) => this.processTransferRateResponse(transferRate as Array<string>, 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<ClientGatewayServiceEvents> {
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<Array<string>>,
configs: MethodCallConfigs,
): Promise<TorrentListSummary> {
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<string, unknown>, 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<string>, 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) {

View File

@@ -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<string | Buffer | MultiMethodCalls>;

View File

@@ -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<HistoryServiceEvents> {
}
this.services?.clientGatewayService
.fetchTransferSummary(transferSummaryMethodCallConfigs)
.fetchTransferSummary()
.then(this.handleFetchTransferSummarySuccess.bind(this))
.catch(this.handleFetchTransferSummaryError.bind(this));
}

View File

@@ -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<TorrentServiceEvents> {
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<TorrentServiceEvents> {
}
return this.services?.clientGatewayService
.fetchTorrentList(torrentListMethodCallConfigs)
.fetchTorrentList()
.then(this.handleFetchTorrentListSuccess)
.catch(this.handleFetchTorrentListError);
}

View File

@@ -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;

View File

@@ -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;
};

View File

@@ -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<string | Buffer>}>;
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 <T extends MethodCallConfigs, P extends keyof T>(
response: Array<Parameters<T[P]['transformValue']>[0]>,
configs: T,
): Promise<
{
[propLabel in P]: ReturnType<T[propLabel]['transformValue']>;
}
> => {
return Object.assign(
{},
...(await Promise.all(
Object.keys(configs).map(async (propLabel, index) => {
return {
[propLabel]: configs[propLabel].transformValue(response[index]),
};
}),
)),
);
};

View File

@@ -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;

View File

@@ -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<string, unknown>) => {
export const getTorrentETAFromProperties = (
processingTorrentProperties: Record<string, unknown>,
): 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<string, unknown>) => {
export const getTorrentPercentCompleteFromProperties = (
processingTorrentProperties: Record<string, unknown>,
): 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<string, unknown>) => {
export const getTorrentStatusFromProperties = (
processingTorrentProperties: Record<string, unknown>,
): TorrentProperties['status'] => {
const {isHashing, isComplete, isOpen, upRate, downRate, state, message} = processingTorrentProperties;
const torrentStatus: Array<TorrentStatus> = [];
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<TorrentProperties> = {},
nextData: Partial<TorrentProperties> = {},
) => {
): boolean => {
if (prevData.status != null && prevData.status.includes('checking')) {
return false;
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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<TorrentStatus>;
tags: Array<string>;
throttleName: string;
trackerURIs: Array<string>;
upRate: number;
upTotal: number;

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -0,0 +1,9 @@
export interface TorrentTracker {
index: number;
id: string;
url: string;
type: number;
group: number;
minInterval: number;
normalInterval: number;
}