New: Option to prefer magnet URLs over torrent file links

Co-authored-by: Deathspike <meister.deathspike@outlook.com>

New: Bulk edit Prefer Magnet Url for indexers
This commit is contained in:
Bogdan
2024-09-11 14:55:28 +03:00
parent a32ab3acfd
commit 4e8b9e81cf
13 changed files with 97 additions and 6 deletions

View File

@@ -19,6 +19,7 @@ interface SavePayload {
seedRatio?: number; seedRatio?: number;
seedTime?: number; seedTime?: number;
packSeedTime?: number; packSeedTime?: number;
preferMagnetUrl?: boolean;
} }
interface EditIndexerModalContentProps { interface EditIndexerModalContentProps {
@@ -65,6 +66,9 @@ function EditIndexerModalContent(props: EditIndexerModalContentProps) {
const [packSeedTime, setPackSeedTime] = useState<null | string | number>( const [packSeedTime, setPackSeedTime] = useState<null | string | number>(
null null
); );
const [preferMagnetUrl, setPreferMagnetUrl] = useState<
null | string | boolean
>(null);
const save = useCallback(() => { const save = useCallback(() => {
let hasChanges = false; let hasChanges = false;
@@ -105,6 +109,11 @@ function EditIndexerModalContent(props: EditIndexerModalContentProps) {
payload.packSeedTime = packSeedTime as number; payload.packSeedTime = packSeedTime as number;
} }
if (preferMagnetUrl !== null) {
hasChanges = true;
payload.preferMagnetUrl = preferMagnetUrl === 'true';
}
if (hasChanges) { if (hasChanges) {
onSavePress(payload); onSavePress(payload);
} }
@@ -118,6 +127,7 @@ function EditIndexerModalContent(props: EditIndexerModalContentProps) {
seedRatio, seedRatio,
seedTime, seedTime,
packSeedTime, packSeedTime,
preferMagnetUrl,
onSavePress, onSavePress,
onModalClose, onModalClose,
]); ]);
@@ -146,6 +156,9 @@ function EditIndexerModalContent(props: EditIndexerModalContentProps) {
case 'packSeedTime': case 'packSeedTime':
setPackSeedTime(value); setPackSeedTime(value);
break; break;
case 'preferMagnetUrl':
setPreferMagnetUrl(value);
break;
default: default:
console.warn(`EditIndexersModalContent Unknown Input: '${name}'`); console.warn(`EditIndexersModalContent Unknown Input: '${name}'`);
} }
@@ -254,6 +267,18 @@ function EditIndexerModalContent(props: EditIndexerModalContentProps) {
onChange={onInputChange} onChange={onInputChange}
/> />
</FormGroup> </FormGroup>
<FormGroup size={sizes.MEDIUM}>
<FormLabel>{translate('PreferMagnetUrl')}</FormLabel>
<FormInputGroup
type={inputTypes.SELECT}
name="preferMagnetUrl"
value={preferMagnetUrl}
values={enableOptions}
onChange={onInputChange}
/>
</FormGroup>
</ModalBody> </ModalBody>
<ModalFooter className={styles.modalFooter}> <ModalFooter className={styles.modalFooter}>

View File

@@ -29,7 +29,8 @@
.minimumSeeders, .minimumSeeders,
.seedRatio, .seedRatio,
.seedTime, .seedTime,
.packSeedTime { .packSeedTime,
.preferMagnetUrl {
composes: cell; composes: cell;
flex: 0 0 90px; flex: 0 0 90px;

View File

@@ -11,6 +11,7 @@ interface CssExports {
'id': string; 'id': string;
'minimumSeeders': string; 'minimumSeeders': string;
'packSeedTime': string; 'packSeedTime': string;
'preferMagnetUrl': string;
'priority': string; 'priority': string;
'privacy': string; 'privacy': string;
'protocol': string; 'protocol': string;

View File

@@ -1,6 +1,7 @@
import React, { useCallback, useState } from 'react'; import React, { useCallback, useState } from 'react';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { useSelect } from 'App/SelectContext'; import { useSelect } from 'App/SelectContext';
import CheckInput from 'Components/Form/CheckInput';
import IconButton from 'Components/Link/IconButton'; import IconButton from 'Components/Link/IconButton';
import RelativeDateCell from 'Components/Table/Cells/RelativeDateCell'; import RelativeDateCell from 'Components/Table/Cells/RelativeDateCell';
import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell'; import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell';
@@ -74,6 +75,10 @@ function IndexerIndexRow(props: IndexerIndexRowProps) {
fields.find((field) => field.name === 'torrentBaseSettings.packSeedTime') fields.find((field) => field.name === 'torrentBaseSettings.packSeedTime')
?.value ?? undefined; ?.value ?? undefined;
const preferMagnetUrl =
fields.find((field) => field.name === 'torrentBaseSettings.preferMagnetUrl')
?.value ?? undefined;
const rssUrl = `${window.location.origin}${ const rssUrl = `${window.location.origin}${
window.Prowlarr.urlBase window.Prowlarr.urlBase
}/${id}/api?apikey=${encodeURIComponent( }/${id}/api?apikey=${encodeURIComponent(
@@ -102,6 +107,10 @@ function IndexerIndexRow(props: IndexerIndexRowProps) {
setIsDeleteIndexerModalOpen(false); setIsDeleteIndexerModalOpen(false);
}, [setIsDeleteIndexerModalOpen]); }, [setIsDeleteIndexerModalOpen]);
const checkInputCallback = useCallback(() => {
// Mock handler to satisfy `onChange` being required for `CheckInput`.
}, []);
const onSelectedChange = useCallback( const onSelectedChange = useCallback(
({ id, value, shiftKey }: SelectStateInputProps) => { ({ id, value, shiftKey }: SelectStateInputProps) => {
selectDispatch({ selectDispatch({
@@ -277,6 +286,21 @@ function IndexerIndexRow(props: IndexerIndexRowProps) {
); );
} }
if (name === 'preferMagnetUrl') {
return (
<VirtualTableRowCell key={name} className={styles[name]}>
{preferMagnetUrl === undefined ? null : (
<CheckInput
name="preferMagnetUrl"
value={preferMagnetUrl}
isDisabled={true}
onChange={checkInputCallback}
/>
)}
</VirtualTableRowCell>
);
}
if (name === 'actions') { if (name === 'actions') {
return ( return (
<VirtualTableRowCell <VirtualTableRowCell

View File

@@ -22,7 +22,8 @@
.minimumSeeders, .minimumSeeders,
.seedRatio, .seedRatio,
.seedTime, .seedTime,
.packSeedTime { .packSeedTime,
.preferMagnetUrl {
composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css'; composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css';
flex: 0 0 90px; flex: 0 0 90px;

View File

@@ -8,6 +8,7 @@ interface CssExports {
'id': string; 'id': string;
'minimumSeeders': string; 'minimumSeeders': string;
'packSeedTime': string; 'packSeedTime': string;
'preferMagnetUrl': string;
'priority': string; 'priority': string;
'privacy': string; 'privacy': string;
'protocol': string; 'protocol': string;

View File

@@ -116,6 +116,26 @@ export const sortPredicates = {
vipExpiration: function({ fields = [] }) { vipExpiration: function({ fields = [] }) {
return fields.find((field) => field.name === 'vipExpiration')?.value ?? ''; return fields.find((field) => field.name === 'vipExpiration')?.value ?? '';
},
minimumSeeders: function({ fields = [] }) {
return fields.find((field) => field.name === 'torrentBaseSettings.appMinimumSeeders')?.value ?? undefined;
},
seedRatio: function({ fields = [] }) {
return fields.find((field) => field.name === 'torrentBaseSettings.seedRatio')?.value ?? undefined;
},
seedTime: function({ fields = [] }) {
return fields.find((field) => field.name === 'torrentBaseSettings.seedTime')?.value ?? undefined;
},
packSeedTime: function({ fields = [] }) {
return fields.find((field) => field.name === 'torrentBaseSettings.packSeedTime')?.value ?? undefined;
},
preferMagnetUrl: function({ fields = [] }) {
return fields.find((field) => field.name === 'torrentBaseSettings.preferMagnetUrl')?.value ?? undefined;
} }
}; };

View File

@@ -116,6 +116,12 @@ export const defaultState = {
isSortable: true, isSortable: true,
isVisible: false isVisible: false
}, },
{
name: 'preferMagnetUrl',
label: () => translate('PreferMagnetUrl'),
isSortable: true,
isVisible: false
},
{ {
name: 'tags', name: 'tags',
label: () => translate('Tags'), label: () => translate('Tags'),

View File

@@ -54,7 +54,7 @@ namespace NzbDrone.Core.IndexerSearch
return new XElement(feedNamespace + "attr", new XAttribute("name", name), new XAttribute("value", value)); return new XElement(feedNamespace + "attr", new XAttribute("name", name), new XAttribute("value", value));
} }
public string ToXml(DownloadProtocol protocol) public string ToXml(DownloadProtocol protocol, bool preferMagnetUrl = false)
{ {
// IMPORTANT: We can't use Uri.ToString(), because it generates URLs without URL encode (links with unicode // IMPORTANT: We can't use Uri.ToString(), because it generates URLs without URL encode (links with unicode
// characters are broken). We must use Uri.AbsoluteUri instead that handles encoding correctly // characters are broken). We must use Uri.AbsoluteUri instead that handles encoding correctly
@@ -73,6 +73,7 @@ namespace NzbDrone.Core.IndexerSearch
new XElement("title", "Prowlarr"), new XElement("title", "Prowlarr"),
from r in Releases from r in Releases
let t = (r as TorrentInfo) ?? new TorrentInfo() let t = (r as TorrentInfo) ?? new TorrentInfo()
let downloadUrl = preferMagnetUrl ? t.MagnetUrl ?? r.DownloadUrl : r.DownloadUrl ?? t.MagnetUrl
select new XElement("item", select new XElement("item",
new XElement("title", RemoveInvalidXMLChars(r.Title)), new XElement("title", RemoveInvalidXMLChars(r.Title)),
new XElement("description", RemoveInvalidXMLChars(r.Description)), new XElement("description", RemoveInvalidXMLChars(r.Description)),
@@ -85,11 +86,11 @@ namespace NzbDrone.Core.IndexerSearch
r.InfoUrl == null ? null : new XElement("comments", r.InfoUrl), r.InfoUrl == null ? null : new XElement("comments", r.InfoUrl),
r.PublishDate == DateTime.MinValue ? new XElement("pubDate", XmlDateFormat(DateTime.Now)) : new XElement("pubDate", XmlDateFormat(r.PublishDate)), r.PublishDate == DateTime.MinValue ? new XElement("pubDate", XmlDateFormat(DateTime.Now)) : new XElement("pubDate", XmlDateFormat(r.PublishDate)),
new XElement("size", r.Size), new XElement("size", r.Size),
new XElement("link", r.DownloadUrl ?? t.MagnetUrl ?? string.Empty), new XElement("link", downloadUrl ?? string.Empty),
r.Categories == null ? null : from c in r.Categories select new XElement("category", c.Id), r.Categories == null ? null : from c in r.Categories select new XElement("category", c.Id),
new XElement( new XElement(
"enclosure", "enclosure",
new XAttribute("url", r.DownloadUrl ?? t.MagnetUrl ?? string.Empty), new XAttribute("url", downloadUrl ?? string.Empty),
r.Size == null ? null : new XAttribute("length", r.Size), r.Size == null ? null : new XAttribute("length", r.Size),
new XAttribute("type", protocol == DownloadProtocol.Torrent ? "application/x-bittorrent" : "application/x-nzb")), new XAttribute("type", protocol == DownloadProtocol.Torrent ? "application/x-bittorrent" : "application/x-nzb")),
r.Categories == null ? null : from c in r.Categories select GetNabElement("category", c.Id, protocol), r.Categories == null ? null : from c in r.Categories select GetNabElement("category", c.Id, protocol),

View File

@@ -63,5 +63,8 @@ namespace NzbDrone.Core.Indexers
[FieldDefinition(4, Type = FieldType.Number, Label = "IndexerSettingsPackSeedTime", HelpText = "IndexerSettingsPackSeedTimeIndexerHelpText", Unit = "minutes", Advanced = true)] [FieldDefinition(4, Type = FieldType.Number, Label = "IndexerSettingsPackSeedTime", HelpText = "IndexerSettingsPackSeedTimeIndexerHelpText", Unit = "minutes", Advanced = true)]
public int? PackSeedTime { get; set; } public int? PackSeedTime { get; set; }
[FieldDefinition(5, Type = FieldType.Checkbox, Label = "IndexerSettingsPreferMagnetUrl", HelpText = "IndexerSettingsPreferMagnetUrlHelpText", Advanced = true)]
public bool PreferMagnetUrl { get; set; }
} }
} }

View File

@@ -407,6 +407,8 @@
"IndexerSettingsPackSeedTime": "Pack Seed Time", "IndexerSettingsPackSeedTime": "Pack Seed Time",
"IndexerSettingsPackSeedTimeIndexerHelpText": "The time a pack (season or discography) torrent should be seeded before stopping, empty is app's default", "IndexerSettingsPackSeedTimeIndexerHelpText": "The time a pack (season or discography) torrent should be seeded before stopping, empty is app's default",
"IndexerSettingsPasskey": "Pass Key", "IndexerSettingsPasskey": "Pass Key",
"IndexerSettingsPreferMagnetUrl": "Prefer Magnet URL",
"IndexerSettingsPreferMagnetUrlHelpText": "When enabled, this indexer will prefer the use of magnet URLs for grabs with fallback to torrent links",
"IndexerSettingsQueryLimit": "Query Limit", "IndexerSettingsQueryLimit": "Query Limit",
"IndexerSettingsQueryLimitHelpText": "The number of max queries as specified by the respective unit that {appName} will allow to the site", "IndexerSettingsQueryLimitHelpText": "The number of max queries as specified by the respective unit that {appName} will allow to the site",
"IndexerSettingsRssKey": "RSS Key", "IndexerSettingsRssKey": "RSS Key",
@@ -541,6 +543,8 @@
"PendingChangesStayReview": "Stay and review changes", "PendingChangesStayReview": "Stay and review changes",
"Port": "Port", "Port": "Port",
"PortNumber": "Port Number", "PortNumber": "Port Number",
"PreferMagnetUrl": "Prefer Magnet URL",
"PreferMagnetUrlHelpText": "When enabled, this indexer will prefer the use of magnet URLs for grabs with fallback to torrent links",
"Presets": "Presets", "Presets": "Presets",
"Priority": "Priority", "Priority": "Priority",
"PrioritySettings": "Priority: {priority}", "PrioritySettings": "Priority: {priority}",

View File

@@ -12,6 +12,7 @@ namespace Prowlarr.Api.V1.Indexers
public double? SeedRatio { get; set; } public double? SeedRatio { get; set; }
public int? SeedTime { get; set; } public int? SeedTime { get; set; }
public int? PackSeedTime { get; set; } public int? PackSeedTime { get; set; }
public bool? PreferMagnetUrl { get; set; }
} }
public class IndexerBulkResourceMapper : ProviderBulkResourceMapper<IndexerBulkResource, IndexerDefinition> public class IndexerBulkResourceMapper : ProviderBulkResourceMapper<IndexerBulkResource, IndexerDefinition>
@@ -35,6 +36,7 @@ namespace Prowlarr.Api.V1.Indexers
((ITorrentIndexerSettings)existing.Settings).TorrentBaseSettings.SeedRatio = resource.SeedRatio ?? ((ITorrentIndexerSettings)existing.Settings).TorrentBaseSettings.SeedRatio; ((ITorrentIndexerSettings)existing.Settings).TorrentBaseSettings.SeedRatio = resource.SeedRatio ?? ((ITorrentIndexerSettings)existing.Settings).TorrentBaseSettings.SeedRatio;
((ITorrentIndexerSettings)existing.Settings).TorrentBaseSettings.SeedTime = resource.SeedTime ?? ((ITorrentIndexerSettings)existing.Settings).TorrentBaseSettings.SeedTime; ((ITorrentIndexerSettings)existing.Settings).TorrentBaseSettings.SeedTime = resource.SeedTime ?? ((ITorrentIndexerSettings)existing.Settings).TorrentBaseSettings.SeedTime;
((ITorrentIndexerSettings)existing.Settings).TorrentBaseSettings.PackSeedTime = resource.PackSeedTime ?? ((ITorrentIndexerSettings)existing.Settings).TorrentBaseSettings.PackSeedTime; ((ITorrentIndexerSettings)existing.Settings).TorrentBaseSettings.PackSeedTime = resource.PackSeedTime ?? ((ITorrentIndexerSettings)existing.Settings).TorrentBaseSettings.PackSeedTime;
((ITorrentIndexerSettings)existing.Settings).TorrentBaseSettings.PreferMagnetUrl = resource.PreferMagnetUrl ?? ((ITorrentIndexerSettings)existing.Settings).TorrentBaseSettings.PreferMagnetUrl;
} }
}); });

View File

@@ -198,7 +198,9 @@ namespace NzbDrone.Api.V1.Indexers
} }
} }
return CreateResponse(results.ToXml(indexer.Protocol)); var preferMagnetUrl = indexer.Protocol == DownloadProtocol.Torrent && indexerDef.Settings is ITorrentIndexerSettings torrentIndexerSettings && (torrentIndexerSettings.TorrentBaseSettings?.PreferMagnetUrl ?? false);
return CreateResponse(results.ToXml(indexer.Protocol, preferMagnetUrl));
default: default:
return CreateResponse(CreateErrorXML(202, $"No such function ({requestType})"), statusCode: StatusCodes.Status400BadRequest); return CreateResponse(CreateErrorXML(202, $"No such function ({requestType})"), statusCode: StatusCodes.Status400BadRequest);
} }