mirror of
https://github.com/zoriya/flood.git
synced 2025-12-06 07:16:18 +00:00
server: migrate torrent details functions to TypeScript
This commit is contained in:
@@ -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 = [
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -45,9 +45,6 @@ const torrentProperties = {
|
||||
basePath: {
|
||||
id: 'torrents.properties.base.path',
|
||||
},
|
||||
comment: {
|
||||
id: 'torrents.properties.comment',
|
||||
},
|
||||
hash: {
|
||||
id: 'torrents.properties.hash',
|
||||
},
|
||||
|
||||
@@ -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},
|
||||
|
||||
@@ -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
6
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
};
|
||||
5
server/constants/rTorrentMethodCallConfigs/index.ts
Normal file
5
server/constants/rTorrentMethodCallConfigs/index.ts
Normal 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';
|
||||
30
server/constants/rTorrentMethodCallConfigs/torrentContent.ts
Normal file
30
server/constants/rTorrentMethodCallConfigs/torrentContent.ts
Normal 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;
|
||||
@@ -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;
|
||||
54
server/constants/rTorrentMethodCallConfigs/torrentPeer.ts
Normal file
54
server/constants/rTorrentMethodCallConfigs/torrentPeer.ts
Normal 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;
|
||||
30
server/constants/rTorrentMethodCallConfigs/torrentTracker.ts
Normal file
30
server/constants/rTorrentMethodCallConfigs/torrentTracker.ts
Normal 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;
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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({});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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>;
|
||||
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
45
server/util/fileTreeUtil.js
Normal file
45
server/util/fileTreeUtil.js
Normal 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;
|
||||
};
|
||||
50
server/util/rTorrentMethodCallUtil.ts
Normal file
50
server/util/rTorrentMethodCallUtil.ts
Normal 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]),
|
||||
};
|
||||
}),
|
||||
)),
|
||||
);
|
||||
};
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
15
shared/types/TorrentPeer.ts
Normal file
15
shared/types/TorrentPeer.ts
Normal 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;
|
||||
}
|
||||
9
shared/types/TorrentTracker.ts
Normal file
9
shared/types/TorrentTracker.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export interface TorrentTracker {
|
||||
index: number;
|
||||
id: string;
|
||||
url: string;
|
||||
type: number;
|
||||
group: number;
|
||||
minInterval: number;
|
||||
normalInterval: number;
|
||||
}
|
||||
Reference in New Issue
Block a user