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;