mirror of
https://github.com/zoriya/flood.git
synced 2025-12-06 07:16:18 +00:00
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:
38
client/src/javascript/components/general/LinkedText.tsx
Normal file
38
client/src/javascript/components/general/LinkedText.tsx
Normal 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;
|
||||
@@ -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" />
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user