mirror of
https://github.com/Radarr/Radarr
synced 2026-05-08 12:51:02 +02:00
New: Add support for additional Torznab indexer flags
This commit is contained in:
parent
a2bde5e016
commit
ff3d38a515
12 changed files with 123 additions and 93 deletions
|
|
@ -8,6 +8,7 @@ import Language from 'Language/Language';
|
||||||
import DownloadClient from 'typings/DownloadClient';
|
import DownloadClient from 'typings/DownloadClient';
|
||||||
import ImportList from 'typings/ImportList';
|
import ImportList from 'typings/ImportList';
|
||||||
import Indexer from 'typings/Indexer';
|
import Indexer from 'typings/Indexer';
|
||||||
|
import IndexerFlag from 'typings/IndexerFlag';
|
||||||
import Notification from 'typings/Notification';
|
import Notification from 'typings/Notification';
|
||||||
import QualityProfile from 'typings/QualityProfile';
|
import QualityProfile from 'typings/QualityProfile';
|
||||||
import { UiSettings } from 'typings/UiSettings';
|
import { UiSettings } from 'typings/UiSettings';
|
||||||
|
|
@ -35,12 +36,14 @@ export interface QualityProfilesAppState
|
||||||
extends AppSectionState<QualityProfile>,
|
extends AppSectionState<QualityProfile>,
|
||||||
AppSectionSchemaState<QualityProfile> {}
|
AppSectionSchemaState<QualityProfile> {}
|
||||||
|
|
||||||
|
export type IndexerFlagSettingsAppState = AppSectionState<IndexerFlag>;
|
||||||
export type LanguageSettingsAppState = AppSectionState<Language>;
|
export type LanguageSettingsAppState = AppSectionState<Language>;
|
||||||
export type UiSettingsAppState = AppSectionItemState<UiSettings>;
|
export type UiSettingsAppState = AppSectionItemState<UiSettings>;
|
||||||
|
|
||||||
interface SettingsAppState {
|
interface SettingsAppState {
|
||||||
downloadClients: DownloadClientAppState;
|
downloadClients: DownloadClientAppState;
|
||||||
importLists: ImportListAppState;
|
importLists: ImportListAppState;
|
||||||
|
indexerFlags: IndexerFlagSettingsAppState;
|
||||||
indexers: IndexerAppState;
|
indexers: IndexerAppState;
|
||||||
languages: LanguageSettingsAppState;
|
languages: LanguageSettingsAppState;
|
||||||
notifications: NotificationAppState;
|
notifications: NotificationAppState;
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ import DownloadClientSelectInputConnector from './DownloadClientSelectInputConne
|
||||||
import EnhancedSelectInput from './EnhancedSelectInput';
|
import EnhancedSelectInput from './EnhancedSelectInput';
|
||||||
import EnhancedSelectInputConnector from './EnhancedSelectInputConnector';
|
import EnhancedSelectInputConnector from './EnhancedSelectInputConnector';
|
||||||
import FormInputHelpText from './FormInputHelpText';
|
import FormInputHelpText from './FormInputHelpText';
|
||||||
import IndexerFlagsSelectInputConnector from './IndexerFlagsSelectInputConnector';
|
import IndexerFlagsSelectInput from './IndexerFlagsSelectInput';
|
||||||
import IndexerSelectInputConnector from './IndexerSelectInputConnector';
|
import IndexerSelectInputConnector from './IndexerSelectInputConnector';
|
||||||
import KeyValueListInput from './KeyValueListInput';
|
import KeyValueListInput from './KeyValueListInput';
|
||||||
import LanguageSelectInputConnector from './LanguageSelectInputConnector';
|
import LanguageSelectInputConnector from './LanguageSelectInputConnector';
|
||||||
|
|
@ -76,7 +76,7 @@ function getComponent(type) {
|
||||||
return RootFolderSelectInputConnector;
|
return RootFolderSelectInputConnector;
|
||||||
|
|
||||||
case inputTypes.INDEXER_FLAGS_SELECT:
|
case inputTypes.INDEXER_FLAGS_SELECT:
|
||||||
return IndexerFlagsSelectInputConnector;
|
return IndexerFlagsSelectInput;
|
||||||
|
|
||||||
case inputTypes.DOWNLOAD_CLIENT_SELECT:
|
case inputTypes.DOWNLOAD_CLIENT_SELECT:
|
||||||
return DownloadClientSelectInputConnector;
|
return DownloadClientSelectInputConnector;
|
||||||
|
|
|
||||||
60
frontend/src/Components/Form/IndexerFlagsSelectInput.tsx
Normal file
60
frontend/src/Components/Form/IndexerFlagsSelectInput.tsx
Normal file
|
|
@ -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 (
|
||||||
|
<EnhancedSelectInput
|
||||||
|
{...props}
|
||||||
|
value={value}
|
||||||
|
values={values}
|
||||||
|
onChange={onChangeWrapper}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default IndexerFlagsSelectInput;
|
||||||
|
|
@ -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 (
|
|
||||||
<EnhancedSelectInput
|
|
||||||
{...this.props}
|
|
||||||
onChange={this.onChange}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
@ -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;
|
||||||
6
frontend/src/typings/IndexerFlag.ts
Normal file
6
frontend/src/typings/IndexerFlag.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
interface IndexerFlag {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default IndexerFlag;
|
||||||
|
|
@ -85,17 +85,12 @@ private int CompareCustomFormatScore(DownloadDecision x, DownloadDecision y)
|
||||||
|
|
||||||
private int CompareIndexerFlags(DownloadDecision x, DownloadDecision y)
|
private int CompareIndexerFlags(DownloadDecision x, DownloadDecision y)
|
||||||
{
|
{
|
||||||
var releaseX = x.RemoteMovie.Release;
|
if (!_configService.PreferIndexerFlags)
|
||||||
var releaseY = y.RemoteMovie.Release;
|
|
||||||
|
|
||||||
if (_configService.PreferIndexerFlags)
|
|
||||||
{
|
|
||||||
return CompareBy(x.RemoteMovie.Release, y.RemoteMovie.Release, release => ScoreFlags(release.IndexerFlags));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
{
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return CompareBy(x.RemoteMovie.Release, y.RemoteMovie.Release, release => ScoreFlags(release.IndexerFlags));
|
||||||
}
|
}
|
||||||
|
|
||||||
private int CompareProtocol(DownloadDecision x, DownloadDecision y)
|
private int CompareProtocol(DownloadDecision x, DownloadDecision y)
|
||||||
|
|
@ -206,12 +201,10 @@ private int ScoreFlags(IndexerFlags flags)
|
||||||
case IndexerFlags.G_Freeleech:
|
case IndexerFlags.G_Freeleech:
|
||||||
case IndexerFlags.PTP_Approved:
|
case IndexerFlags.PTP_Approved:
|
||||||
case IndexerFlags.PTP_Golden:
|
case IndexerFlags.PTP_Golden:
|
||||||
case IndexerFlags.HDB_Internal:
|
case IndexerFlags.G_Internal:
|
||||||
case IndexerFlags.AHD_Internal:
|
|
||||||
score += 2;
|
score += 2;
|
||||||
break;
|
break;
|
||||||
case IndexerFlags.G_Halfleech:
|
case IndexerFlags.G_Halfleech:
|
||||||
case IndexerFlags.AHD_UserRelease:
|
|
||||||
score += 1;
|
score += 1;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ public class FileListTorrent
|
||||||
public uint Files { get; set; }
|
public uint Files { get; set; }
|
||||||
[JsonProperty(PropertyName = "imdb")]
|
[JsonProperty(PropertyName = "imdb")]
|
||||||
public string ImdbId { get; set; }
|
public string ImdbId { get; set; }
|
||||||
|
public bool Internal { get; set; }
|
||||||
[JsonProperty(PropertyName = "freeleech")]
|
[JsonProperty(PropertyName = "freeleech")]
|
||||||
public bool FreeLeech { get; set; }
|
public bool FreeLeech { get; set; }
|
||||||
[JsonProperty(PropertyName = "upload_date")]
|
[JsonProperty(PropertyName = "upload_date")]
|
||||||
|
|
|
||||||
|
|
@ -41,15 +41,20 @@ public IList<ReleaseInfo> ParseResponse(IndexerResponse indexerResponse)
|
||||||
flags |= IndexerFlags.G_Freeleech;
|
flags |= IndexerFlags.G_Freeleech;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (result.Internal)
|
||||||
|
{
|
||||||
|
flags |= IndexerFlags.G_Internal;
|
||||||
|
}
|
||||||
|
|
||||||
var imdbId = 0;
|
var imdbId = 0;
|
||||||
if (result.ImdbId != null && result.ImdbId.Length > 2)
|
if (result.ImdbId != null && result.ImdbId.Length > 2)
|
||||||
{
|
{
|
||||||
imdbId = int.Parse(result.ImdbId.Substring(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,
|
Title = result.Name,
|
||||||
Size = result.Size,
|
Size = result.Size,
|
||||||
DownloadUrl = GetDownloadUrl(id),
|
DownloadUrl = GetDownloadUrl(id),
|
||||||
|
|
|
||||||
|
|
@ -62,7 +62,7 @@ public IList<ReleaseInfo> ParseResponse(IndexerResponse indexerResponse)
|
||||||
|
|
||||||
if (internalRelease)
|
if (internalRelease)
|
||||||
{
|
{
|
||||||
flags |= IndexerFlags.HDB_Internal;
|
flags |= IndexerFlags.G_Internal;
|
||||||
}
|
}
|
||||||
|
|
||||||
torrentInfos.Add(new HDBitsInfo()
|
torrentInfos.Add(new HDBitsInfo()
|
||||||
|
|
|
||||||
|
|
@ -208,24 +208,45 @@ protected IndexerFlags GetFlags(XElement item)
|
||||||
IndexerFlags flags = 0;
|
IndexerFlags flags = 0;
|
||||||
|
|
||||||
var downloadFactor = TryGetFloatTorznabAttribute(item, "downloadvolumefactor", 1);
|
var downloadFactor = TryGetFloatTorznabAttribute(item, "downloadvolumefactor", 1);
|
||||||
|
|
||||||
var uploadFactor = TryGetFloatTorznabAttribute(item, "uploadvolumefactor", 1);
|
var uploadFactor = TryGetFloatTorznabAttribute(item, "uploadvolumefactor", 1);
|
||||||
|
|
||||||
if (uploadFactor == 2)
|
|
||||||
{
|
|
||||||
flags |= IndexerFlags.G_DoubleUpload;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (downloadFactor == 0.5)
|
if (downloadFactor == 0.5)
|
||||||
{
|
{
|
||||||
flags |= IndexerFlags.G_Halfleech;
|
flags |= IndexerFlags.G_Halfleech;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (downloadFactor == 0.75)
|
||||||
|
{
|
||||||
|
flags |= IndexerFlags.G_Freeleech25;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (downloadFactor == 0.25)
|
||||||
|
{
|
||||||
|
flags |= IndexerFlags.G_Freeleech75;
|
||||||
|
}
|
||||||
|
|
||||||
if (downloadFactor == 0.0)
|
if (downloadFactor == 0.0)
|
||||||
{
|
{
|
||||||
flags |= IndexerFlags.G_Freeleech;
|
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;
|
return flags;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -106,11 +106,13 @@ public enum IndexerFlags
|
||||||
G_DoubleUpload = 4, // General
|
G_DoubleUpload = 4, // General
|
||||||
PTP_Golden = 8, // PTP
|
PTP_Golden = 8, // PTP
|
||||||
PTP_Approved = 16, // PTP
|
PTP_Approved = 16, // PTP
|
||||||
HDB_Internal = 32, // HDBits, internal
|
G_Internal = 32, // General, internal
|
||||||
|
[Obsolete]
|
||||||
AHD_Internal = 64, // AHD, internal
|
AHD_Internal = 64, // AHD, internal
|
||||||
G_Scene = 128, // General, the torrent comes from the "scene"
|
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_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.
|
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
|
AHD_UserRelease = 1024 // AHD, internal
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue