From 745d05089bd861e39e6261b40c32d5f974939325 Mon Sep 17 00:00:00 2001 From: FinalDoom <677609+FinalDoom@users.noreply.github.com> Date: Tue, 19 Apr 2022 16:04:11 -0600 Subject: [PATCH] feature: display comment inside .torrent in torrent details (#541) * Added comment to torrent details Mostly simple as it's supported by the various clients, except in the case of rtorrent. For rtorrent, tags are stored in custom1, consistent with other clients. For that reason, comment is being stored in custom2, which is also consistent with other clients. In particular, rutorrent uses a prefix on the comment for some reason, which is duplicated in this change to preserve cross-compatibility. * Fix lint 'let' and noreferrer Co-authored-by: FinalDoom <7464170-FinalDoom@users.noreply.gitlab.com> --- .../components/general/LinkedText.tsx | 38 +++++++++++++++++++ .../TorrentGeneralInfo.tsx | 9 +++++ .../services/Deluge/clientGatewayService.ts | 2 + .../Deluge/types/DelugeCoreMethods.ts | 2 +- .../Transmission/clientGatewayService.ts | 2 + .../qBittorrent/clientGatewayService.ts | 14 ++++++- .../services/rTorrent/clientGatewayService.ts | 32 +++++++++++++--- .../methodCallConfigs/torrentList.ts | 11 ++++++ server/util/torrentFileUtil.ts | 10 +++++ shared/types/Torrent.ts | 1 + 10 files changed, 112 insertions(+), 9 deletions(-) create mode 100644 client/src/javascript/components/general/LinkedText.tsx diff --git a/client/src/javascript/components/general/LinkedText.tsx b/client/src/javascript/components/general/LinkedText.tsx new file mode 100644 index 00000000..81cf6092 --- /dev/null +++ b/client/src/javascript/components/general/LinkedText.tsx @@ -0,0 +1,38 @@ +import {FC} from 'react'; + +interface LinkedTextProps { + text: string; + className?: string; +} + +function isValidHttpUrl(s: string) { + let url; + + try { + url = new URL(s); + } catch (_) { + return false; + } + + return url.protocol === 'http:' || url.protocol === 'https:'; +} + +const LinkedText: FC = ({text, className}: LinkedTextProps) => { + const nodes = text.split(/(?<=\s)(?!\s)(?:\b|\B)/).map((s) => + isValidHttpUrl(s.trimEnd()) ? ( + + {s} + + ) : ( + s + ), + ); + + return {nodes}; +}; + +LinkedText.defaultProps = { + className: undefined, +}; + +export default LinkedText; diff --git a/client/src/javascript/components/modals/torrent-details-modal/TorrentGeneralInfo.tsx b/client/src/javascript/components/modals/torrent-details-modal/TorrentGeneralInfo.tsx index be935ad8..450a3e3c 100644 --- a/client/src/javascript/components/modals/torrent-details-modal/TorrentGeneralInfo.tsx +++ b/client/src/javascript/components/modals/torrent-details-modal/TorrentGeneralInfo.tsx @@ -4,6 +4,7 @@ import {Trans, useLingui} from '@lingui/react'; import type {TorrentProperties} from '@shared/types/Torrent'; +import LinkedText from '../../general/LinkedText'; import Size from '../../general/Size'; import TorrentStore from '../../../stores/TorrentStore'; import UIStore from '../../../stores/UIStore'; @@ -201,6 +202,14 @@ const TorrentGeneralInfo: FC = observer(() => { : i18n._('torrents.details.general.type.public')} + + + + + + + + diff --git a/server/services/Deluge/clientGatewayService.ts b/server/services/Deluge/clientGatewayService.ts index b66829fb..cfb64e7b 100644 --- a/server/services/Deluge/clientGatewayService.ts +++ b/server/services/Deluge/clientGatewayService.ts @@ -284,6 +284,7 @@ class DelugeClientGatewayService extends ClientGatewayService { return this.clientRequestManager .coreGetTorrentsStatus([ 'active_time', + 'comment', 'download_location', 'download_payload_rate', 'eta', @@ -322,6 +323,7 @@ class DelugeClientGatewayService extends ClientGatewayService { const torrentProperties: TorrentProperties = { bytesDone: status.total_done, + comment: status.comment, dateActive: status.download_payload_rate > 0 || status.upload_payload_rate > 0 ? -1 : status.active_time, dateAdded: status.time_added, diff --git a/server/services/Deluge/types/DelugeCoreMethods.ts b/server/services/Deluge/types/DelugeCoreMethods.ts index c6c4c060..2bc4c179 100644 --- a/server/services/Deluge/types/DelugeCoreMethods.ts +++ b/server/services/Deluge/types/DelugeCoreMethods.ts @@ -151,7 +151,7 @@ export interface DelugeCoreTorrentStatuses { trackers: Array; tracker_status: unknown; upload_payload_rate: number; - comment: unknown; + comment: string; creator: unknown; num_files: unknown; num_pieces: unknown; diff --git a/server/services/Transmission/clientGatewayService.ts b/server/services/Transmission/clientGatewayService.ts index c07b01ec..d1f24e5e 100644 --- a/server/services/Transmission/clientGatewayService.ts +++ b/server/services/Transmission/clientGatewayService.ts @@ -353,6 +353,7 @@ class TransmissionClientGatewayService extends ClientGatewayService { 'hashString', 'downloadDir', 'name', + 'comment', 'haveValid', 'addedDate', 'dateCreated', @@ -389,6 +390,7 @@ class TransmissionClientGatewayService extends ClientGatewayService { const torrentProperties: TorrentProperties = { hash: torrent.hashString.toUpperCase(), name: torrent.name, + comment: torrent.comment, bytesDone: torrent.haveValid, dateActive: torrent.rateDownload > 0 || torrent.rateUpload > 0 ? -1 : torrent.activityDate, dateAdded: torrent.addedDate, diff --git a/server/services/qBittorrent/clientGatewayService.ts b/server/services/qBittorrent/clientGatewayService.ts index 305a3a88..1c388b5b 100644 --- a/server/services/qBittorrent/clientGatewayService.ts +++ b/server/services/qBittorrent/clientGatewayService.ts @@ -46,7 +46,10 @@ import {TorrentTrackerType} from '../../../shared/types/TorrentTracker'; class QBittorrentClientGatewayService extends ClientGatewayService { private clientRequestManager = new ClientRequestManager(this.user.client as QBittorrentConnectionSettings); - private cachedProperties: Record> = {}; + private cachedProperties: Record< + string, + Pick + > = {}; async addTorrentsByFile({ files, @@ -358,6 +361,7 @@ class QBittorrentClientGatewayService extends ClientGatewayService { if (properties != null && trackers != null && Array.isArray(trackers)) { this.cachedProperties[hash] = { + comment: properties?.comment, dateCreated: properties?.creation_date, isPrivate: trackers[0]?.msg.includes('is private'), trackerURIs: getDomainsFromURLs( @@ -374,10 +378,16 @@ class QBittorrentClientGatewayService extends ClientGatewayService { {}, ...(await Promise.all( infos.map(async (info) => { - const {dateCreated = 0, isPrivate = false, trackerURIs = []} = this.cachedProperties[info.hash] || {}; + const { + comment = '', + dateCreated = 0, + isPrivate = false, + trackerURIs = [], + } = this.cachedProperties[info.hash] || {}; const torrentProperties: TorrentProperties = { bytesDone: info.completed, + comment: comment, dateActive: info.dlspeed > 0 || info.upspeed > 0 ? -1 : info.last_activity, dateAdded: info.added_on, dateCreated, diff --git a/server/services/rTorrent/clientGatewayService.ts b/server/services/rTorrent/clientGatewayService.ts index 6855a449..192dc688 100644 --- a/server/services/rTorrent/clientGatewayService.ts +++ b/server/services/rTorrent/clientGatewayService.ts @@ -36,7 +36,7 @@ import ClientGatewayService from '../clientGatewayService'; import ClientRequestManager from './clientRequestManager'; import {fetchUrls} from '../../util/fetchUtil'; import {getMethodCalls, processMethodCallResponse} from './util/rTorrentMethodCallUtil'; -import {setCompleted, setTrackers} from '../../util/torrentFileUtil'; +import {getComment, setCompleted, setTrackers} from '../../util/torrentFileUtil'; import { encodeTags, getAddTorrentPropertiesCalls, @@ -60,6 +60,15 @@ class RTorrentClientGatewayService extends ClientGatewayService { clientRequestManager = new ClientRequestManager(this.user.client as RTorrentConnectionSettings); availableMethodCalls = this.fetchAvailableMethodCalls(true); + async appendTorrentCommentCall(file: string, additionalCalls: string[]) { + const comment = await getComment(Buffer.from(file, 'base64')); + if (comment && comment.length > 0) { + // VRS24mrker is used for compatability with ruTorrent + return [...additionalCalls, `d.custom2.set="VRS24mrker${encodeURIComponent(comment)}"`]; + } + return additionalCalls; + } + async addTorrentsByFile({ files, destination, @@ -102,10 +111,16 @@ class RTorrentClientGatewayService extends ClientGatewayService { if (hasLoadThrow && this.clientRequestManager.isJSONCapable) { await this.clientRequestManager .methodCall('system.multicall', [ - processedFiles.map((file) => ({ - methodName: start ? 'load.start_throw' : 'load.throw', - params: ['', `data:applications/x-bittorrent;base64,${file}`, ...additionalCalls], - })), + await Promise.all( + processedFiles.map(async (file) => ({ + methodName: start ? 'load.start_throw' : 'load.throw', + params: [ + '', + `data:applications/x-bittorrent;base64,${file}`, + ...(await this.appendTorrentCommentCall(file, additionalCalls)), + ], + })), + ), ]) .then(this.processClientRequestSuccess, this.processRTorrentRequestError) .then((response: Array>) => { @@ -116,7 +131,11 @@ class RTorrentClientGatewayService extends ClientGatewayService { await Promise.all( processedFiles.map(async (file) => { await this.clientRequestManager - .methodCall(start ? 'load.raw_start' : 'load.raw', ['', Buffer.from(file, 'base64'), ...additionalCalls]) + .methodCall(start ? 'load.raw_start' : 'load.raw', [ + '', + Buffer.from(file, 'base64'), + ...(await this.appendTorrentCommentCall(file, additionalCalls)), + ]) .then(this.processClientRequestSuccess, this.processRTorrentRequestError); }), ); @@ -643,6 +662,7 @@ class RTorrentClientGatewayService extends ClientGatewayService { processedResponses.map(async (response) => { const torrentProperties: TorrentProperties = { bytesDone: response.bytesDone, + comment: response.comment, dateActive: response.downRate > 0 || response.upRate > 0 ? -1 : response.dateActive, dateAdded: response.dateAdded, dateCreated: response.dateCreated, diff --git a/server/services/rTorrent/constants/methodCallConfigs/torrentList.ts b/server/services/rTorrent/constants/methodCallConfigs/torrentList.ts index 385151ef..da7938f9 100644 --- a/server/services/rTorrent/constants/methodCallConfigs/torrentList.ts +++ b/server/services/rTorrent/constants/methodCallConfigs/torrentList.ts @@ -18,6 +18,17 @@ const torrentListMethodCallConfigs = { methodCall: 'd.state=', transformValue: booleanTransformer, }, + comment: { + methodCall: 'd.custom2=', + transformValue: (value: unknown): string => { + // ruTorrent sets VRS24mrkr as a comment prefix, so we use it as well for compatability + if (value === '' || typeof value !== 'string' || value.indexOf('VRS24mrker') !== 0) { + return ''; + } + + return decodeURIComponent(value.substring(10)); + }, + }, isActive: { methodCall: 'd.is_active=', transformValue: booleanTransformer, diff --git a/server/util/torrentFileUtil.ts b/server/util/torrentFileUtil.ts index ed073e75..75545fb8 100644 --- a/server/util/torrentFileUtil.ts +++ b/server/util/torrentFileUtil.ts @@ -22,6 +22,16 @@ const openAndDecodeTorrent = async (torrentPath: string): Promise => { + const torrentData: TorrentFile | null = await bencode.decode(torrent); + + if (torrentData == null) { + return; + } + + return torrentData.comment?.toString(); +}; + export const getContentSize = async (info: TorrentFile['info']): Promise => { if (info.length != null) { // Single file torrent diff --git a/shared/types/Torrent.ts b/shared/types/Torrent.ts index 79312191..d10f8073 100644 --- a/shared/types/Torrent.ts +++ b/shared/types/Torrent.ts @@ -18,6 +18,7 @@ export enum TorrentPriority { export interface TorrentProperties { bytesDone: number; + comment: string; // Last time the torrent is active, -1 means currently active, 0 means data unavailable dateActive: number; dateAdded: number;