mirror of
https://github.com/Prowlarr/Prowlarr.git
synced 2025-09-17 17:14:18 +02:00
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:
@@ -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}>
|
||||||
|
@@ -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;
|
||||||
|
@@ -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;
|
||||||
|
@@ -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
|
||||||
|
@@ -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;
|
||||||
|
@@ -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;
|
||||||
|
@@ -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;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -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'),
|
||||||
|
@@ -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),
|
||||||
|
@@ -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; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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}",
|
||||||
|
@@ -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;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -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);
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user