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>
This commit is contained in:
FinalDoom
2022-04-19 16:04:11 -06:00
committed by GitHub
parent 5a9f7013ba
commit 745d05089b
10 changed files with 112 additions and 9 deletions

View File

@@ -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<LinkedTextProps> = ({text, className}: LinkedTextProps) => {
const nodes = text.split(/(?<=\s)(?!\s)(?:\b|\B)/).map((s) =>
isValidHttpUrl(s.trimEnd()) ? (
<a href={s.trimEnd()} target="_blank" rel="noopener noreferrer">
{s}
</a>
) : (
s
),
);
return <span className={className}>{nodes}</span>;
};
LinkedText.defaultProps = {
className: undefined,
};
export default LinkedText;

View File

@@ -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')}
</td>
</tr>
<tr className="torrent-details__detail torrent-details__detail--comment">
<td className="torrent-details__detail__label">
<Trans id="torrents.details.general.comment" />
</td>
<td className="torrent-details_detail__value">
<LinkedText text={torrent.comment} />
</td>
</tr>
<tr className="torrent-details__table__heading">
<td className="torrent-details__table__heading--tertiary" colSpan={2}>
<Trans id="torrents.details.general.heading.tracker" />

View File

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

View File

@@ -151,7 +151,7 @@ export interface DelugeCoreTorrentStatuses {
trackers: Array<DelugeCoreTorrentTrackerStatuses>;
tracker_status: unknown;
upload_payload_rate: number;
comment: unknown;
comment: string;
creator: unknown;
num_files: unknown;
num_pieces: unknown;

View File

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

View File

@@ -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<string, Pick<TorrentProperties, 'dateCreated' | 'isPrivate' | 'trackerURIs'>> = {};
private cachedProperties: Record<
string,
Pick<TorrentProperties, 'comment' | 'dateCreated' | 'isPrivate' | 'trackerURIs'>
> = {};
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,

View File

@@ -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<Array<string | number>>) => {
@@ -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,

View File

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

View File

@@ -22,6 +22,16 @@ const openAndDecodeTorrent = async (torrentPath: string): Promise<TorrentFile |
return torrentData;
};
export const getComment = async (torrent: Buffer): Promise<string | undefined> => {
const torrentData: TorrentFile | null = await bencode.decode(torrent);
if (torrentData == null) {
return;
}
return torrentData.comment?.toString();
};
export const getContentSize = async (info: TorrentFile['info']): Promise<number> => {
if (info.length != null) {
// Single file torrent

View File

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