From ff3d38a515a3a5fcfdaaaa6feba49408038decbf Mon Sep 17 00:00:00 2001 From: Bogdan Date: Fri, 8 Sep 2023 08:52:55 +0300 Subject: [PATCH] New: Add support for additional Torznab indexer flags --- frontend/src/App/State/SettingsAppState.ts | 3 + .../src/Components/Form/FormInputGroup.js | 4 +- .../Form/IndexerFlagsSelectInput.tsx | 60 ++++++++++++++++ .../Form/IndexerFlagsSelectInputConnector.js | 70 ------------------- .../Selectors/createIndexerFlagsSelector.ts | 9 +++ frontend/src/typings/IndexerFlag.ts | 6 ++ .../DownloadDecisionComparer.cs | 15 ++-- .../Indexers/FileList/FileListApi.cs | 1 + .../Indexers/FileList/FileListParser.cs | 9 ++- .../Indexers/HDBits/HDBitsParser.cs | 2 +- .../Indexers/Torznab/TorznabRssParser.cs | 33 +++++++-- src/NzbDrone.Core/Parser/Model/ReleaseInfo.cs | 4 +- 12 files changed, 123 insertions(+), 93 deletions(-) create mode 100644 frontend/src/Components/Form/IndexerFlagsSelectInput.tsx delete mode 100644 frontend/src/Components/Form/IndexerFlagsSelectInputConnector.js create mode 100644 frontend/src/Store/Selectors/createIndexerFlagsSelector.ts create mode 100644 frontend/src/typings/IndexerFlag.ts diff --git a/frontend/src/App/State/SettingsAppState.ts b/frontend/src/App/State/SettingsAppState.ts index e249f2d20..49bba4334 100644 --- a/frontend/src/App/State/SettingsAppState.ts +++ b/frontend/src/App/State/SettingsAppState.ts @@ -8,6 +8,7 @@ import Language from 'Language/Language'; import DownloadClient from 'typings/DownloadClient'; import ImportList from 'typings/ImportList'; import Indexer from 'typings/Indexer'; +import IndexerFlag from 'typings/IndexerFlag'; import Notification from 'typings/Notification'; import QualityProfile from 'typings/QualityProfile'; import { UiSettings } from 'typings/UiSettings'; @@ -35,12 +36,14 @@ export interface QualityProfilesAppState extends AppSectionState, AppSectionSchemaState {} +export type IndexerFlagSettingsAppState = AppSectionState; export type LanguageSettingsAppState = AppSectionState; export type UiSettingsAppState = AppSectionItemState; interface SettingsAppState { downloadClients: DownloadClientAppState; importLists: ImportListAppState; + indexerFlags: IndexerFlagSettingsAppState; indexers: IndexerAppState; languages: LanguageSettingsAppState; notifications: NotificationAppState; diff --git a/frontend/src/Components/Form/FormInputGroup.js b/frontend/src/Components/Form/FormInputGroup.js index a668e60f2..813dcf176 100644 --- a/frontend/src/Components/Form/FormInputGroup.js +++ b/frontend/src/Components/Form/FormInputGroup.js @@ -12,7 +12,7 @@ import DownloadClientSelectInputConnector from './DownloadClientSelectInputConne import EnhancedSelectInput from './EnhancedSelectInput'; import EnhancedSelectInputConnector from './EnhancedSelectInputConnector'; import FormInputHelpText from './FormInputHelpText'; -import IndexerFlagsSelectInputConnector from './IndexerFlagsSelectInputConnector'; +import IndexerFlagsSelectInput from './IndexerFlagsSelectInput'; import IndexerSelectInputConnector from './IndexerSelectInputConnector'; import KeyValueListInput from './KeyValueListInput'; import LanguageSelectInputConnector from './LanguageSelectInputConnector'; @@ -76,7 +76,7 @@ function getComponent(type) { return RootFolderSelectInputConnector; case inputTypes.INDEXER_FLAGS_SELECT: - return IndexerFlagsSelectInputConnector; + return IndexerFlagsSelectInput; case inputTypes.DOWNLOAD_CLIENT_SELECT: return DownloadClientSelectInputConnector; diff --git a/frontend/src/Components/Form/IndexerFlagsSelectInput.tsx b/frontend/src/Components/Form/IndexerFlagsSelectInput.tsx new file mode 100644 index 000000000..fd3520ceb --- /dev/null +++ b/frontend/src/Components/Form/IndexerFlagsSelectInput.tsx @@ -0,0 +1,60 @@ +import React, { useCallback } from 'react'; +import { useSelector } from 'react-redux'; +import { createSelector } from 'reselect'; +import AppState from 'App/State/AppState'; +import EnhancedSelectInput from './EnhancedSelectInput'; + +interface IndexerFlagsSelectInputProps { + name: string; + indexerFlags: number; + onChange(payload: object): void; +} + +const selectIndexerFlagsValues = (selectedFlags: number) => + createSelector( + (state: AppState) => state.settings.indexerFlags, + (indexerFlags) => { + const value = indexerFlags.items + .filter( + // eslint-disable-next-line no-bitwise + (item) => (selectedFlags & item.id) === item.id + ) + .map(({ id }) => id); + + const values = indexerFlags.items.map(({ id, name }) => ({ + key: id, + value: name, + })); + + return { + value, + values, + }; + } + ); + +function IndexerFlagsSelectInput(props: IndexerFlagsSelectInputProps) { + const { indexerFlags, onChange } = props; + + const { value, values } = useSelector(selectIndexerFlagsValues(indexerFlags)); + + const onChangeWrapper = useCallback( + ({ name, value }: { name: string; value: number[] }) => { + const indexerFlags = value.reduce((acc, flagId) => acc + flagId, 0); + + onChange({ name, value: indexerFlags }); + }, + [onChange] + ); + + return ( + + ); +} + +export default IndexerFlagsSelectInput; diff --git a/frontend/src/Components/Form/IndexerFlagsSelectInputConnector.js b/frontend/src/Components/Form/IndexerFlagsSelectInputConnector.js deleted file mode 100644 index 8d5c44193..000000000 --- a/frontend/src/Components/Form/IndexerFlagsSelectInputConnector.js +++ /dev/null @@ -1,70 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { connect } from 'react-redux'; -import { createSelector } from 'reselect'; -import EnhancedSelectInput from './EnhancedSelectInput'; - -function createMapStateToProps() { - return createSelector( - (state, { indexerFlags }) => indexerFlags, - (state) => state.settings.indexerFlags, - (selectedFlags, indexerFlags) => { - const value = []; - - indexerFlags.items.forEach((item) => { - // eslint-disable-next-line no-bitwise - if ((selectedFlags & item.id) === item.id) { - value.push(item.id); - } - }); - - const values = indexerFlags.items.map(({ id, name }) => { - return { - key: id, - value: name - }; - }); - - return { - value, - values - }; - } - ); -} - -class IndexerFlagsSelectInputConnector extends Component { - - onChange = ({ name, value }) => { - let indexerFlags = 0; - - value.forEach((flagId) => { - indexerFlags += flagId; - }); - - this.props.onChange({ name, value: indexerFlags }); - }; - - // - // Render - - render() { - - return ( - - ); - } -} - -IndexerFlagsSelectInputConnector.propTypes = { - name: PropTypes.string.isRequired, - indexerFlags: PropTypes.number.isRequired, - value: PropTypes.arrayOf(PropTypes.number).isRequired, - values: PropTypes.arrayOf(PropTypes.object).isRequired, - onChange: PropTypes.func.isRequired -}; - -export default connect(createMapStateToProps)(IndexerFlagsSelectInputConnector); diff --git a/frontend/src/Store/Selectors/createIndexerFlagsSelector.ts b/frontend/src/Store/Selectors/createIndexerFlagsSelector.ts new file mode 100644 index 000000000..90587639c --- /dev/null +++ b/frontend/src/Store/Selectors/createIndexerFlagsSelector.ts @@ -0,0 +1,9 @@ +import { createSelector } from 'reselect'; +import AppState from 'App/State/AppState'; + +const createIndexerFlagsSelector = createSelector( + (state: AppState) => state.settings.indexerFlags, + (indexerFlags) => indexerFlags +); + +export default createIndexerFlagsSelector; diff --git a/frontend/src/typings/IndexerFlag.ts b/frontend/src/typings/IndexerFlag.ts new file mode 100644 index 000000000..2c7d97a73 --- /dev/null +++ b/frontend/src/typings/IndexerFlag.ts @@ -0,0 +1,6 @@ +interface IndexerFlag { + id: number; + name: string; +} + +export default IndexerFlag; diff --git a/src/NzbDrone.Core/DecisionEngine/DownloadDecisionComparer.cs b/src/NzbDrone.Core/DecisionEngine/DownloadDecisionComparer.cs index b17c2c4ea..115e6702a 100644 --- a/src/NzbDrone.Core/DecisionEngine/DownloadDecisionComparer.cs +++ b/src/NzbDrone.Core/DecisionEngine/DownloadDecisionComparer.cs @@ -85,17 +85,12 @@ private int CompareCustomFormatScore(DownloadDecision x, DownloadDecision y) private int CompareIndexerFlags(DownloadDecision x, DownloadDecision y) { - var releaseX = x.RemoteMovie.Release; - var releaseY = y.RemoteMovie.Release; - - if (_configService.PreferIndexerFlags) - { - return CompareBy(x.RemoteMovie.Release, y.RemoteMovie.Release, release => ScoreFlags(release.IndexerFlags)); - } - else + if (!_configService.PreferIndexerFlags) { return 0; } + + return CompareBy(x.RemoteMovie.Release, y.RemoteMovie.Release, release => ScoreFlags(release.IndexerFlags)); } private int CompareProtocol(DownloadDecision x, DownloadDecision y) @@ -206,12 +201,10 @@ private int ScoreFlags(IndexerFlags flags) case IndexerFlags.G_Freeleech: case IndexerFlags.PTP_Approved: case IndexerFlags.PTP_Golden: - case IndexerFlags.HDB_Internal: - case IndexerFlags.AHD_Internal: + case IndexerFlags.G_Internal: score += 2; break; case IndexerFlags.G_Halfleech: - case IndexerFlags.AHD_UserRelease: score += 1; break; } diff --git a/src/NzbDrone.Core/Indexers/FileList/FileListApi.cs b/src/NzbDrone.Core/Indexers/FileList/FileListApi.cs index eea73fae6..280c668e4 100644 --- a/src/NzbDrone.Core/Indexers/FileList/FileListApi.cs +++ b/src/NzbDrone.Core/Indexers/FileList/FileListApi.cs @@ -16,6 +16,7 @@ public class FileListTorrent public uint Files { get; set; } [JsonProperty(PropertyName = "imdb")] public string ImdbId { get; set; } + public bool Internal { get; set; } [JsonProperty(PropertyName = "freeleech")] public bool FreeLeech { get; set; } [JsonProperty(PropertyName = "upload_date")] diff --git a/src/NzbDrone.Core/Indexers/FileList/FileListParser.cs b/src/NzbDrone.Core/Indexers/FileList/FileListParser.cs index 0936eba62..4e807d83f 100644 --- a/src/NzbDrone.Core/Indexers/FileList/FileListParser.cs +++ b/src/NzbDrone.Core/Indexers/FileList/FileListParser.cs @@ -41,15 +41,20 @@ public IList ParseResponse(IndexerResponse indexerResponse) flags |= IndexerFlags.G_Freeleech; } + if (result.Internal) + { + flags |= IndexerFlags.G_Internal; + } + var imdbId = 0; if (result.ImdbId != null && result.ImdbId.Length > 2) { imdbId = int.Parse(result.ImdbId.Substring(2)); } - torrentInfos.Add(new TorrentInfo() + torrentInfos.Add(new TorrentInfo { - Guid = string.Format("FileList-{0}", id), + Guid = $"FileList-{id}", Title = result.Name, Size = result.Size, DownloadUrl = GetDownloadUrl(id), diff --git a/src/NzbDrone.Core/Indexers/HDBits/HDBitsParser.cs b/src/NzbDrone.Core/Indexers/HDBits/HDBitsParser.cs index a772e9f74..a076cd4ba 100644 --- a/src/NzbDrone.Core/Indexers/HDBits/HDBitsParser.cs +++ b/src/NzbDrone.Core/Indexers/HDBits/HDBitsParser.cs @@ -62,7 +62,7 @@ public IList ParseResponse(IndexerResponse indexerResponse) if (internalRelease) { - flags |= IndexerFlags.HDB_Internal; + flags |= IndexerFlags.G_Internal; } torrentInfos.Add(new HDBitsInfo() diff --git a/src/NzbDrone.Core/Indexers/Torznab/TorznabRssParser.cs b/src/NzbDrone.Core/Indexers/Torznab/TorznabRssParser.cs index 0ed9cb066..edaa44506 100644 --- a/src/NzbDrone.Core/Indexers/Torznab/TorznabRssParser.cs +++ b/src/NzbDrone.Core/Indexers/Torznab/TorznabRssParser.cs @@ -208,24 +208,45 @@ protected IndexerFlags GetFlags(XElement item) IndexerFlags flags = 0; var downloadFactor = TryGetFloatTorznabAttribute(item, "downloadvolumefactor", 1); - var uploadFactor = TryGetFloatTorznabAttribute(item, "uploadvolumefactor", 1); - if (uploadFactor == 2) - { - flags |= IndexerFlags.G_DoubleUpload; - } - if (downloadFactor == 0.5) { flags |= IndexerFlags.G_Halfleech; } + if (downloadFactor == 0.75) + { + flags |= IndexerFlags.G_Freeleech25; + } + + if (downloadFactor == 0.25) + { + flags |= IndexerFlags.G_Freeleech75; + } + if (downloadFactor == 0.0) { flags |= IndexerFlags.G_Freeleech; } + if (uploadFactor == 2.0) + { + flags |= IndexerFlags.G_DoubleUpload; + } + + var tags = TryGetMultipleTorznabAttributes(item, "tag"); + + if (tags.Any(t => t.EqualsIgnoreCase("internal"))) + { + flags |= IndexerFlags.G_Internal; + } + + if (tags.Any(t => t.EqualsIgnoreCase("scene"))) + { + flags |= IndexerFlags.G_Scene; + } + return flags; } diff --git a/src/NzbDrone.Core/Parser/Model/ReleaseInfo.cs b/src/NzbDrone.Core/Parser/Model/ReleaseInfo.cs index de03449fa..7f2b536a9 100644 --- a/src/NzbDrone.Core/Parser/Model/ReleaseInfo.cs +++ b/src/NzbDrone.Core/Parser/Model/ReleaseInfo.cs @@ -106,11 +106,13 @@ public enum IndexerFlags G_DoubleUpload = 4, // General PTP_Golden = 8, // PTP PTP_Approved = 16, // PTP - HDB_Internal = 32, // HDBits, internal + G_Internal = 32, // General, internal + [Obsolete] AHD_Internal = 64, // AHD, internal G_Scene = 128, // General, the torrent comes from the "scene" G_Freeleech75 = 256, // Currently only used for AHD, signifies a torrent counts towards 75 percent of your download quota. G_Freeleech25 = 512, // Currently only used for AHD, signifies a torrent counts towards 25 percent of your download quota. + [Obsolete] AHD_UserRelease = 1024 // AHD, internal } }