mirror of
https://github.com/Prowlarr/Prowlarr.git
synced 2026-04-17 21:44:48 -04:00
Compare commits
21 Commits
v1.19.0.45
...
v1.20.1.46
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
45d8a8a4e6 | ||
|
|
a4546c77ce | ||
|
|
d69bf6360a | ||
|
|
da9ce5b5c3 | ||
|
|
e092098101 | ||
|
|
1a89a79b74 | ||
|
|
cb6bf49922 | ||
|
|
4bcaba0be0 | ||
|
|
220ef723c7 | ||
|
|
9c599a6be4 | ||
|
|
715ce1fc6c | ||
|
|
8c3a192dd0 | ||
|
|
d22bf93dfd | ||
|
|
886054fdf8 | ||
|
|
4188510586 | ||
|
|
fedebca5e1 | ||
|
|
e2ce6437e9 | ||
|
|
bdae60bac9 | ||
|
|
2d6c818aec | ||
|
|
a1d19852dc | ||
|
|
104c95f28f |
@@ -9,7 +9,7 @@ variables:
|
||||
testsFolder: './_tests'
|
||||
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
|
||||
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
|
||||
majorVersion: '1.19.0'
|
||||
majorVersion: '1.20.1'
|
||||
minorVersion: $[counter('minorVersion', 1)]
|
||||
prowlarrVersion: '$(majorVersion).$(minorVersion)'
|
||||
buildName: '$(Build.SourceBranchName).$(prowlarrVersion)'
|
||||
@@ -20,7 +20,7 @@ variables:
|
||||
innoVersion: '6.2.2'
|
||||
windowsImage: 'windows-2022'
|
||||
linuxImage: 'ubuntu-20.04'
|
||||
macImage: 'macOS-11'
|
||||
macImage: 'macOS-12'
|
||||
|
||||
trigger:
|
||||
branches:
|
||||
|
||||
@@ -271,26 +271,29 @@ class EnhancedSelectInput extends Component {
|
||||
this.setState({ isOpen: !this.state.isOpen });
|
||||
};
|
||||
|
||||
onSelect = (value) => {
|
||||
if (Array.isArray(this.props.value)) {
|
||||
let newValue = null;
|
||||
const index = this.props.value.indexOf(value);
|
||||
onSelect = (newValue) => {
|
||||
const { name, value, values, onChange } = this.props;
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
let arrayValue = null;
|
||||
const index = value.indexOf(newValue);
|
||||
|
||||
if (index === -1) {
|
||||
newValue = this.props.values.map((v) => v.key).filter((v) => (v === value) || this.props.value.includes(v));
|
||||
arrayValue = values.map((v) => v.key).filter((v) => (v === newValue) || value.includes(v));
|
||||
} else {
|
||||
newValue = [...this.props.value];
|
||||
newValue.splice(index, 1);
|
||||
arrayValue = [...value];
|
||||
arrayValue.splice(index, 1);
|
||||
}
|
||||
this.props.onChange({
|
||||
name: this.props.name,
|
||||
value: newValue
|
||||
onChange({
|
||||
name,
|
||||
value: arrayValue
|
||||
});
|
||||
} else {
|
||||
this.setState({ isOpen: false });
|
||||
|
||||
this.props.onChange({
|
||||
name: this.props.name,
|
||||
value
|
||||
onChange({
|
||||
name,
|
||||
value: newValue
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -485,7 +488,7 @@ class EnhancedSelectInput extends Component {
|
||||
values.map((v, index) => {
|
||||
const hasParent = v.parentKey !== undefined;
|
||||
const depth = hasParent ? 1 : 0;
|
||||
const parentSelected = hasParent && value.includes(v.parentKey);
|
||||
const parentSelected = hasParent && Array.isArray(value) && value.includes(v.parentKey);
|
||||
return (
|
||||
<OptionComponent
|
||||
key={v.key}
|
||||
|
||||
@@ -34,7 +34,8 @@ function getSelectOptions(items) {
|
||||
key: option.value,
|
||||
value: option.name,
|
||||
hint: option.hint,
|
||||
parentKey: option.parentValue
|
||||
parentKey: option.parentValue,
|
||||
isDisabled: option.isDisabled
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
@@ -4,16 +4,16 @@ import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import TableRowButton from 'Components/Table/TableRowButton';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import CapabilitiesLabel from 'Indexer/Index/Table/CapabilitiesLabel';
|
||||
import PrivacyLabel from 'Indexer/Index/Table/PrivacyLabel';
|
||||
import ProtocolLabel from 'Indexer/Index/Table/ProtocolLabel';
|
||||
import { IndexerCapabilities } from 'Indexer/Indexer';
|
||||
import firstCharToUpper from 'Utilities/String/firstCharToUpper';
|
||||
import { IndexerCapabilities, IndexerPrivacy } from 'Indexer/Indexer';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './SelectIndexerRow.css';
|
||||
|
||||
interface SelectIndexerRowProps {
|
||||
name: string;
|
||||
protocol: string;
|
||||
privacy: string;
|
||||
privacy: IndexerPrivacy;
|
||||
language: string;
|
||||
description: string;
|
||||
capabilities: IndexerCapabilities;
|
||||
@@ -63,7 +63,9 @@ function SelectIndexerRow(props: SelectIndexerRowProps) {
|
||||
|
||||
<TableRowCell>{description}</TableRowCell>
|
||||
|
||||
<TableRowCell>{translate(firstCharToUpper(privacy))}</TableRowCell>
|
||||
<TableRowCell>
|
||||
<PrivacyLabel privacy={privacy} />
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell>
|
||||
<CapabilitiesLabel capabilities={capabilities} />
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
import React, { useCallback, useMemo, useRef, useState } from 'react';
|
||||
import React, {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { SelectProvider } from 'App/SelectContext';
|
||||
import ClientSideCollectionAppState from 'App/State/ClientSideCollectionAppState';
|
||||
@@ -22,12 +28,17 @@ import AddIndexerModal from 'Indexer/Add/AddIndexerModal';
|
||||
import EditIndexerModalConnector from 'Indexer/Edit/EditIndexerModalConnector';
|
||||
import NoIndexer from 'Indexer/NoIndexer';
|
||||
import { executeCommand } from 'Store/Actions/commandActions';
|
||||
import { cloneIndexer, testAllIndexers } from 'Store/Actions/indexerActions';
|
||||
import {
|
||||
cloneIndexer,
|
||||
fetchIndexers,
|
||||
testAllIndexers,
|
||||
} from 'Store/Actions/indexerActions';
|
||||
import {
|
||||
setIndexerFilter,
|
||||
setIndexerSort,
|
||||
setIndexerTableOption,
|
||||
} from 'Store/Actions/indexerIndexActions';
|
||||
import { fetchIndexerStatus } from 'Store/Actions/indexerStatusActions';
|
||||
import scrollPositions from 'Store/scrollPositions';
|
||||
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
||||
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
||||
@@ -82,6 +93,11 @@ const IndexerIndex = withScrollPosition((props: IndexerIndexProps) => {
|
||||
);
|
||||
const [isSelectMode, setIsSelectMode] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchIndexers());
|
||||
dispatch(fetchIndexerStatus());
|
||||
}, [dispatch]);
|
||||
|
||||
const onAddIndexerPress = useCallback(() => {
|
||||
setIsAddIndexerModalOpen(true);
|
||||
}, [setIsAddIndexerModalOpen]);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useSelect } from 'App/SelectContext';
|
||||
import Label from 'Components/Label';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import RelativeDateCell from 'Components/Table/Cells/RelativeDateCell';
|
||||
import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell';
|
||||
@@ -15,10 +14,10 @@ import createIndexerIndexItemSelector from 'Indexer/Index/createIndexerIndexItem
|
||||
import Indexer from 'Indexer/Indexer';
|
||||
import IndexerTitleLink from 'Indexer/IndexerTitleLink';
|
||||
import { SelectStateInputProps } from 'typings/props';
|
||||
import firstCharToUpper from 'Utilities/String/firstCharToUpper';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import CapabilitiesLabel from './CapabilitiesLabel';
|
||||
import IndexerStatusCell from './IndexerStatusCell';
|
||||
import PrivacyLabel from './PrivacyLabel';
|
||||
import ProtocolLabel from './ProtocolLabel';
|
||||
import styles from './IndexerIndexRow.css';
|
||||
|
||||
@@ -175,7 +174,7 @@ function IndexerIndexRow(props: IndexerIndexRowProps) {
|
||||
if (name === 'privacy') {
|
||||
return (
|
||||
<VirtualTableRowCell key={name} className={styles[name]}>
|
||||
<Label>{translate(firstCharToUpper(privacy))}</Label>
|
||||
<PrivacyLabel privacy={privacy} />
|
||||
</VirtualTableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
20
frontend/src/Indexer/Index/Table/PrivacyLabel.css
Normal file
20
frontend/src/Indexer/Index/Table/PrivacyLabel.css
Normal file
@@ -0,0 +1,20 @@
|
||||
.publicLabel {
|
||||
composes: label from '~Components/Label.css';
|
||||
|
||||
border-color: var(--dangerColor);
|
||||
background-color: var(--dangerColor);
|
||||
}
|
||||
|
||||
.semiPrivateLabel {
|
||||
composes: label from '~Components/Label.css';
|
||||
|
||||
border-color: var(--warningColor);
|
||||
background-color: var(--warningColor);
|
||||
}
|
||||
|
||||
.privateLabel {
|
||||
composes: label from '~Components/Label.css';
|
||||
|
||||
border-color: var(--infoColor);
|
||||
background-color: var(--infoColor);
|
||||
}
|
||||
9
frontend/src/Indexer/Index/Table/PrivacyLabel.css.d.ts
vendored
Normal file
9
frontend/src/Indexer/Index/Table/PrivacyLabel.css.d.ts
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
// This file is automatically generated.
|
||||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
'privateLabel': string;
|
||||
'publicLabel': string;
|
||||
'semiPrivateLabel': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
export default cssExports;
|
||||
20
frontend/src/Indexer/Index/Table/PrivacyLabel.tsx
Normal file
20
frontend/src/Indexer/Index/Table/PrivacyLabel.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import React from 'react';
|
||||
import Label from 'Components/Label';
|
||||
import { IndexerPrivacy } from 'Indexer/Indexer';
|
||||
import firstCharToUpper from 'Utilities/String/firstCharToUpper';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './PrivacyLabel.css';
|
||||
|
||||
interface PrivacyLabelProps {
|
||||
privacy: IndexerPrivacy;
|
||||
}
|
||||
|
||||
function PrivacyLabel({ privacy }: PrivacyLabelProps) {
|
||||
return (
|
||||
<Label className={styles[`${privacy}Label`]}>
|
||||
{translate(firstCharToUpper(privacy))}
|
||||
</Label>
|
||||
);
|
||||
}
|
||||
|
||||
export default PrivacyLabel;
|
||||
@@ -24,6 +24,8 @@ export interface IndexerCapabilities extends ModelBase {
|
||||
categories: IndexerCategory[];
|
||||
}
|
||||
|
||||
export type IndexerPrivacy = 'public' | 'semiPrivate' | 'private';
|
||||
|
||||
export interface IndexerField extends ModelBase {
|
||||
order: number;
|
||||
name: string;
|
||||
@@ -47,7 +49,7 @@ interface Indexer extends ModelBase {
|
||||
supportsRedirect: boolean;
|
||||
supportsPagination: boolean;
|
||||
protocol: string;
|
||||
privacy: string;
|
||||
privacy: IndexerPrivacy;
|
||||
priority: number;
|
||||
fields: IndexerField[];
|
||||
tags: number[];
|
||||
|
||||
@@ -24,6 +24,7 @@ import TagListConnector from 'Components/TagListConnector';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
import DeleteIndexerModal from 'Indexer/Delete/DeleteIndexerModal';
|
||||
import EditIndexerModalConnector from 'Indexer/Edit/EditIndexerModalConnector';
|
||||
import PrivacyLabel from 'Indexer/Index/Table/PrivacyLabel';
|
||||
import Indexer, { IndexerCapabilities } from 'Indexer/Indexer';
|
||||
import { createIndexerSelectorForHook } from 'Store/Selectors/createIndexerSelector';
|
||||
import translate from 'Utilities/String/translate';
|
||||
@@ -64,6 +65,7 @@ function IndexerInfoModalContent(props: IndexerInfoModalContentProps) {
|
||||
fields,
|
||||
tags,
|
||||
protocol,
|
||||
privacy,
|
||||
capabilities = {} as IndexerCapabilities,
|
||||
} = indexer as Indexer;
|
||||
|
||||
@@ -160,6 +162,11 @@ function IndexerInfoModalContent(props: IndexerInfoModalContentProps) {
|
||||
title={translate('Language')}
|
||||
data={language ?? '-'}
|
||||
/>
|
||||
<DescriptionListItem
|
||||
descriptionClassName={styles.description}
|
||||
title={translate('Privacy')}
|
||||
data={privacy ? <PrivacyLabel privacy={privacy} /> : '-'}
|
||||
/>
|
||||
{vipExpiration ? (
|
||||
<DescriptionListItem
|
||||
descriptionClassName={styles.description}
|
||||
|
||||
@@ -44,7 +44,7 @@ namespace NzbDrone.Core.Applications.Sonarr
|
||||
public IEnumerable<int> AnimeSyncCategories { get; set; }
|
||||
|
||||
[FieldDefinition(5, Label = "Sync Anime Standard Format Search", Type = FieldType.Checkbox, HelpText = "Sync also searching for anime using the standard numbering", Advanced = true)]
|
||||
public bool SyncAnimeStandardFormatSearch { get; set; }
|
||||
public bool SyncAnimeStandardFormatSearch { get; set; } = true;
|
||||
|
||||
[FieldDefinition(6, Type = FieldType.Checkbox, Label = "ApplicationSettingsSyncRejectBlocklistedTorrentHashes", HelpText = "ApplicationSettingsSyncRejectBlocklistedTorrentHashesHelpText", Advanced = true)]
|
||||
public bool SyncRejectBlocklistedTorrentHashesWhileGrabbing { get; set; }
|
||||
|
||||
@@ -1,18 +1,26 @@
|
||||
using NzbDrone.Core.Instrumentation;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.Instrumentation;
|
||||
|
||||
namespace NzbDrone.Core.Housekeeping.Housekeepers
|
||||
{
|
||||
public class TrimLogDatabase : IHousekeepingTask
|
||||
{
|
||||
private readonly ILogRepository _logRepo;
|
||||
private readonly IConfigFileProvider _configFileProvider;
|
||||
|
||||
public TrimLogDatabase(ILogRepository logRepo)
|
||||
public TrimLogDatabase(ILogRepository logRepo, IConfigFileProvider configFileProvider)
|
||||
{
|
||||
_logRepo = logRepo;
|
||||
_configFileProvider = configFileProvider;
|
||||
}
|
||||
|
||||
public void Clean()
|
||||
{
|
||||
if (!_configFileProvider.LogDbEnabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_logRepo.Trim();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,9 +31,7 @@ namespace NzbDrone.Core.IndexerSearch.Definitions
|
||||
!IsIdSearch;
|
||||
|
||||
public override bool IsIdSearch =>
|
||||
Episode.IsNotNullOrWhiteSpace() ||
|
||||
ImdbId.IsNotNullOrWhiteSpace() ||
|
||||
Season.HasValue ||
|
||||
TvdbId.HasValue ||
|
||||
RId.HasValue ||
|
||||
TraktId.HasValue ||
|
||||
@@ -116,7 +114,7 @@ namespace NzbDrone.Core.IndexerSearch.Definitions
|
||||
string episodeString;
|
||||
if (DateTime.TryParseExact($"{Season} {Episode}", "yyyy MM/dd", CultureInfo.InvariantCulture, DateTimeStyles.None, out var showDate))
|
||||
{
|
||||
episodeString = showDate.ToString("yyyy.MM.dd");
|
||||
episodeString = showDate.ToString("yyyy.MM.dd", CultureInfo.InvariantCulture);
|
||||
}
|
||||
else if (Episode.IsNullOrWhiteSpace())
|
||||
{
|
||||
|
||||
@@ -26,11 +26,15 @@ namespace NzbDrone.Core.IndexerStats
|
||||
{
|
||||
var history = _historyService.Between(start, end);
|
||||
|
||||
var filteredHistory = history.Where(h => indexerIds.Contains(h.IndexerId));
|
||||
var filteredHistory = history.Where(h => indexerIds.Contains(h.IndexerId)).ToArray();
|
||||
|
||||
var groupedByIndexer = filteredHistory.GroupBy(h => h.IndexerId);
|
||||
var groupedByUserAgent = filteredHistory.GroupBy(h => h.Data.GetValueOrDefault("source") ?? "");
|
||||
var groupedByHost = filteredHistory.GroupBy(h => h.Data.GetValueOrDefault("host") ?? "");
|
||||
var groupedByIndexer = filteredHistory.GroupBy(h => h.IndexerId).ToArray();
|
||||
var groupedByUserAgent = filteredHistory
|
||||
.Where(h => h.EventType != HistoryEventType.IndexerAuth)
|
||||
.GroupBy(h => h.Data.GetValueOrDefault("source") ?? "").ToArray();
|
||||
var groupedByHost = filteredHistory
|
||||
.Where(h => h.EventType != HistoryEventType.IndexerAuth)
|
||||
.GroupBy(h => h.Data.GetValueOrDefault("host") ?? "").ToArray();
|
||||
|
||||
var indexerStatsList = new List<IndexerStatistics>();
|
||||
var userAgentStatsList = new List<UserAgentStatistics>();
|
||||
@@ -60,7 +64,7 @@ namespace NzbDrone.Core.IndexerStats
|
||||
var temp = 0;
|
||||
var elapsedTimeEvents = sortedEvents
|
||||
.Where(h => int.TryParse(h.Data.GetValueOrDefault("elapsedTime"), out temp) && h.Data.GetValueOrDefault("cached") != "1")
|
||||
.Select(h => temp)
|
||||
.Select(_ => temp)
|
||||
.ToArray();
|
||||
|
||||
indexerStats.AverageResponseTime = elapsedTimeEvents.Any() ? (int)elapsedTimeEvents.Average() : 0;
|
||||
@@ -68,6 +72,7 @@ namespace NzbDrone.Core.IndexerStats
|
||||
foreach (var historyEvent in sortedEvents)
|
||||
{
|
||||
var failed = !historyEvent.Successful;
|
||||
|
||||
switch (historyEvent.EventType)
|
||||
{
|
||||
case HistoryEventType.IndexerQuery:
|
||||
@@ -101,8 +106,6 @@ namespace NzbDrone.Core.IndexerStats
|
||||
indexerStats.NumberOfFailedRssQueries++;
|
||||
}
|
||||
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -118,8 +121,8 @@ namespace NzbDrone.Core.IndexerStats
|
||||
};
|
||||
|
||||
var sortedEvents = indexer.OrderBy(v => v.Date)
|
||||
.ThenBy(v => v.Id)
|
||||
.ToArray();
|
||||
.ThenBy(v => v.Id)
|
||||
.ToArray();
|
||||
|
||||
foreach (var historyEvent in sortedEvents)
|
||||
{
|
||||
@@ -128,13 +131,10 @@ namespace NzbDrone.Core.IndexerStats
|
||||
case HistoryEventType.IndexerRss:
|
||||
case HistoryEventType.IndexerQuery:
|
||||
indexerStats.NumberOfQueries++;
|
||||
|
||||
break;
|
||||
case HistoryEventType.ReleaseGrabbed:
|
||||
indexerStats.NumberOfGrabs++;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -149,8 +149,8 @@ namespace NzbDrone.Core.IndexerStats
|
||||
};
|
||||
|
||||
var sortedEvents = indexer.OrderBy(v => v.Date)
|
||||
.ThenBy(v => v.Id)
|
||||
.ToArray();
|
||||
.ThenBy(v => v.Id)
|
||||
.ToArray();
|
||||
|
||||
foreach (var historyEvent in sortedEvents)
|
||||
{
|
||||
@@ -163,8 +163,6 @@ namespace NzbDrone.Core.IndexerStats
|
||||
case HistoryEventType.ReleaseGrabbed:
|
||||
indexerStats.NumberOfGrabs++;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ namespace NzbDrone.Core.IndexerVersions
|
||||
/* Update Service will fall back if version # does not exist for an indexer per Ta */
|
||||
|
||||
private const string DEFINITION_BRANCH = "master";
|
||||
private const int DEFINITION_VERSION = 10;
|
||||
private const int DEFINITION_VERSION = 11;
|
||||
|
||||
// Used when moving yml to C#
|
||||
private readonly List<string> _definitionBlocklist = new ()
|
||||
|
||||
@@ -644,16 +644,16 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
|
||||
private static int? ParseSeasonFromTitles(IReadOnlyCollection<string> titles)
|
||||
{
|
||||
var advancedSeasonRegex = new Regex(@"(\d+)(st|nd|rd|th) Season", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
var advancedSeasonRegex = new Regex(@"\b(?:(?<season>\d+)(?:st|nd|rd|th) Season|Season (?<season>\d+))\b", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
var seasonCharactersRegex = new Regex(@"(I{2,})$", RegexOptions.Compiled);
|
||||
var seasonNumberRegex = new Regex(@"\b(?:S)?([2-9])$", RegexOptions.Compiled);
|
||||
var seasonNumberRegex = new Regex(@"\b(?<!Part[- ._])(?:S)?(?<season>[2-9])$", RegexOptions.Compiled);
|
||||
|
||||
foreach (var title in titles)
|
||||
{
|
||||
var advancedSeasonRegexMatch = advancedSeasonRegex.Match(title);
|
||||
if (advancedSeasonRegexMatch.Success)
|
||||
{
|
||||
return ParseUtil.CoerceInt(advancedSeasonRegexMatch.Groups[1].Value);
|
||||
return ParseUtil.CoerceInt(advancedSeasonRegexMatch.Groups["season"].Value);
|
||||
}
|
||||
|
||||
var seasonCharactersRegexMatch = seasonCharactersRegex.Match(title);
|
||||
@@ -665,7 +665,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
var seasonNumberRegexMatch = seasonNumberRegex.Match(title);
|
||||
if (seasonNumberRegexMatch.Success)
|
||||
{
|
||||
return ParseUtil.CoerceInt(seasonNumberRegexMatch.Groups[1].Value);
|
||||
return ParseUtil.CoerceInt(seasonNumberRegexMatch.Groups["season"].Value);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -74,7 +74,7 @@ namespace NzbDrone.Core.Indexers.BroadcastheNet
|
||||
else if (DateTime.TryParseExact($"{searchCriteria.Season} {searchCriteria.Episode}", "yyyy MM/dd", CultureInfo.InvariantCulture, DateTimeStyles.None, out var showDate))
|
||||
{
|
||||
// Daily Episode
|
||||
parameters.Name = showDate.ToString("yyyy.MM.dd");
|
||||
parameters.Name = showDate.ToString("yyyy.MM.dd", CultureInfo.InvariantCulture);
|
||||
parameters.Category = "Episode";
|
||||
pageableRequests.Add(GetPagedRequests(parameters, btnResults, btnOffset));
|
||||
}
|
||||
|
||||
@@ -139,20 +139,13 @@ namespace NzbDrone.Core.Indexers.Definitions.Cardigann
|
||||
{
|
||||
var selectorSelector = ApplyGoTemplateText(selector.Selector, variables);
|
||||
|
||||
if (dom.Matches(selectorSelector))
|
||||
{
|
||||
selection = dom;
|
||||
}
|
||||
else
|
||||
{
|
||||
selection = QuerySelector(dom, selectorSelector);
|
||||
}
|
||||
selection = dom.Matches(selectorSelector) ? dom : QuerySelector(dom, selectorSelector);
|
||||
|
||||
if (selection == null)
|
||||
{
|
||||
if (required)
|
||||
{
|
||||
throw new Exception(string.Format("Selector \"{0}\" didn't match {1}", selectorSelector, dom.ToHtmlPretty()));
|
||||
throw new Exception($"Selector \"{selectorSelector}\" didn't match {dom.ToHtmlPretty()}");
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -195,7 +188,7 @@ namespace NzbDrone.Core.Indexers.Definitions.Cardigann
|
||||
{
|
||||
if (required)
|
||||
{
|
||||
throw new Exception(string.Format("Attribute \"{0}\" is not set for element {1}", selector.Attribute, selection.ToHtmlPretty()));
|
||||
throw new Exception($"Attribute \"{selector.Attribute}\" is not set for element {selection.ToHtmlPretty()}");
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -340,6 +333,7 @@ namespace NzbDrone.Core.Indexers.Definitions.Cardigann
|
||||
case "info_cookie":
|
||||
case "info_flaresolverr":
|
||||
case "info_useragent":
|
||||
case "info_category_8000":
|
||||
case "cardigannCaptcha":
|
||||
// no-op
|
||||
break;
|
||||
|
||||
@@ -332,37 +332,47 @@ namespace NzbDrone.Core.Indexers.Definitions.Cardigann
|
||||
}
|
||||
|
||||
// selector inputs
|
||||
if (login.Selectorinputs != null)
|
||||
if (login.Selectorinputs != null && login.Selectorinputs.Any())
|
||||
{
|
||||
foreach (var selectorinput in login.Selectorinputs)
|
||||
foreach (var selectorInput in login.Selectorinputs)
|
||||
{
|
||||
string value = null;
|
||||
try
|
||||
{
|
||||
value = HandleSelector(selectorinput.Value, landingResultDocument.FirstElementChild);
|
||||
pairs[selectorinput.Key] = value;
|
||||
var value = HandleSelector(selectorInput.Value, landingResultDocument.FirstElementChild, required: !selectorInput.Value.Optional);
|
||||
|
||||
if (selectorInput.Value.Optional && value == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
pairs[selectorInput.Key] = value;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new CardigannException(string.Format("Error while parsing selector input={0}, selector={1}, value={2}: {3}", selectorinput.Key, selectorinput.Value.Selector, value, ex.Message));
|
||||
throw new CardigannException($"Error while parsing selector input={selectorInput.Key}, selector={selectorInput.Value.Selector}: {ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// getselector inputs
|
||||
if (login.Getselectorinputs != null)
|
||||
if (login.Getselectorinputs != null && login.Getselectorinputs.Any())
|
||||
{
|
||||
foreach (var selectorinput in login.Getselectorinputs)
|
||||
foreach (var selectorInput in login.Getselectorinputs)
|
||||
{
|
||||
string value = null;
|
||||
try
|
||||
{
|
||||
value = HandleSelector(selectorinput.Value, landingResultDocument.FirstElementChild);
|
||||
queryCollection[selectorinput.Key] = value;
|
||||
var value = HandleSelector(selectorInput.Value, landingResultDocument.FirstElementChild, required: !selectorInput.Value.Optional);
|
||||
|
||||
if (selectorInput.Value.Optional && value == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
queryCollection[selectorInput.Key] = value;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new CardigannException(string.Format("Error while parsing get selector input={0}, selector={1}, value={2}: {3}", selectorinput.Key, selectorinput.Value.Selector, value, ex.Message));
|
||||
throw new CardigannException($"Error while parsing get selector input={selectorInput.Key}, selector={selectorInput.Value.Selector}: {ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,7 +66,7 @@ namespace NzbDrone.Core.Indexers.Definitions.HDBits
|
||||
|
||||
if (DateTime.TryParseExact($"{searchCriteria.Season} {searchCriteria.Episode}", "yyyy MM/dd", CultureInfo.InvariantCulture, DateTimeStyles.None, out var showDate))
|
||||
{
|
||||
query.Search = showDate.ToString("yyyy-MM-dd");
|
||||
query.Search = showDate.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -70,16 +70,17 @@ namespace NzbDrone.Core.Indexers.Headphones
|
||||
protected override bool PostProcess(IndexerResponse indexerResponse, List<XElement> items, List<ReleaseInfo> releases)
|
||||
{
|
||||
var enclosureTypes = items.SelectMany(GetEnclosures).Select(v => v.Type).Distinct().ToArray();
|
||||
|
||||
if (enclosureTypes.Any() && enclosureTypes.Intersect(PreferredEnclosureMimeTypes).Empty())
|
||||
{
|
||||
if (enclosureTypes.Intersect(TorrentEnclosureMimeTypes).Any())
|
||||
{
|
||||
_logger.Warn("Feed does not contain {0}, found {1}, did you intend to add a Torznab indexer?", NzbEnclosureMimeType, enclosureTypes[0]);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.Warn("Feed does not contain {0}, found {1}.", NzbEnclosureMimeType, enclosureTypes[0]);
|
||||
_logger.Warn("{0} does not contain {1}, found {2}, did you intend to add a Torznab indexer?", indexerResponse.Request.Url, NzbEnclosureMimeType, enclosureTypes[0]);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
_logger.Warn("{0} does not contain {1}, found {2}.", indexerResponse.Request.Url, NzbEnclosureMimeType, enclosureTypes[0]);
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
@@ -5,7 +5,6 @@ using System.Linq;
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using Newtonsoft.Json;
|
||||
using NLog;
|
||||
@@ -42,7 +41,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
|
||||
public override IIndexerRequestGenerator GetRequestGenerator()
|
||||
{
|
||||
return new NebulanceRequestGenerator(Settings);
|
||||
return new NebulanceRequestGenerator(Settings, _logger);
|
||||
}
|
||||
|
||||
public override IParseIndexerResponse GetParser()
|
||||
@@ -68,26 +67,6 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
return Task.FromResult(request);
|
||||
}
|
||||
|
||||
protected override IList<ReleaseInfo> CleanupReleases(IEnumerable<ReleaseInfo> releases, SearchCriteriaBase searchCriteria)
|
||||
{
|
||||
var cleanReleases = base.CleanupReleases(releases, searchCriteria);
|
||||
|
||||
return FilterReleasesByQuery(cleanReleases, searchCriteria).ToList();
|
||||
}
|
||||
|
||||
protected override IEnumerable<ReleaseInfo> FilterReleasesByQuery(IEnumerable<ReleaseInfo> releases, SearchCriteriaBase searchCriteria)
|
||||
{
|
||||
if (!searchCriteria.IsRssSearch &&
|
||||
searchCriteria.IsIdSearch &&
|
||||
searchCriteria is TvSearchCriteria tvSearchCriteria &&
|
||||
tvSearchCriteria.EpisodeSearchString.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
releases = releases.Where(r => r.Title.IsNotNullOrWhiteSpace() && r.Title.ContainsIgnoreCase(tvSearchCriteria.EpisodeSearchString)).ToList();
|
||||
}
|
||||
|
||||
return releases;
|
||||
}
|
||||
|
||||
private IndexerCapabilities SetCapabilities()
|
||||
{
|
||||
var caps = new IndexerCapabilities
|
||||
@@ -111,10 +90,12 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
public class NebulanceRequestGenerator : IIndexerRequestGenerator
|
||||
{
|
||||
private readonly NebulanceSettings _settings;
|
||||
private readonly Logger _logger;
|
||||
|
||||
public NebulanceRequestGenerator(NebulanceSettings settings)
|
||||
public NebulanceRequestGenerator(NebulanceSettings settings, Logger logger)
|
||||
{
|
||||
_settings = settings;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria)
|
||||
@@ -136,40 +117,53 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
Age = ">0"
|
||||
};
|
||||
|
||||
if (searchCriteria.SanitizedTvSearchString.IsNotNullOrWhiteSpace())
|
||||
if (searchCriteria.TvMazeId is > 0)
|
||||
{
|
||||
queryParams.Name = "%" + Regex.Replace(searchCriteria.SanitizedTvSearchString, "[\\W]+", "%").Trim() + "%";
|
||||
queryParams.TvMaze = searchCriteria.TvMazeId.Value;
|
||||
}
|
||||
else if (searchCriteria.ImdbId.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
queryParams.Imdb = searchCriteria.FullImdbId;
|
||||
}
|
||||
|
||||
if (searchCriteria.TvMazeId.HasValue)
|
||||
{
|
||||
queryParams.Tvmaze = searchCriteria.TvMazeId.Value;
|
||||
var searchQuery = searchCriteria.SanitizedSearchTerm.Trim();
|
||||
|
||||
if (searchCriteria.EpisodeSearchString.IsNotNullOrWhiteSpace())
|
||||
if (searchQuery.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
queryParams.Release = searchQuery;
|
||||
}
|
||||
|
||||
if (DateTime.TryParseExact($"{searchCriteria.Season} {searchCriteria.Episode}", "yyyy MM/dd", CultureInfo.InvariantCulture, DateTimeStyles.None, out var showDate))
|
||||
{
|
||||
queryParams.Name = searchQuery;
|
||||
queryParams.Release = showDate.ToString("yyyy.MM.dd", CultureInfo.InvariantCulture);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (searchCriteria.Season.HasValue)
|
||||
{
|
||||
queryParams.Name = "%" + Regex.Replace(searchCriteria.EpisodeSearchString, "[\\W]+", "%").Trim() + "%";
|
||||
queryParams.Season = searchCriteria.Season.Value;
|
||||
}
|
||||
|
||||
if (searchCriteria.Episode.IsNotNullOrWhiteSpace() && int.TryParse(searchCriteria.Episode, out var episodeNumber))
|
||||
{
|
||||
queryParams.Episode = episodeNumber;
|
||||
}
|
||||
}
|
||||
else if (searchCriteria.ImdbId.IsNotNullOrWhiteSpace() && int.TryParse(searchCriteria.ImdbId, out var intImdb))
|
||||
{
|
||||
queryParams.Imdb = intImdb;
|
||||
|
||||
if (searchCriteria.EpisodeSearchString.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
queryParams.Name = "%" + Regex.Replace(searchCriteria.EpisodeSearchString, "[\\W]+", "%").Trim() + "%";
|
||||
}
|
||||
if ((queryParams.Season.HasValue || queryParams.Episode.HasValue) &&
|
||||
queryParams.Name.IsNullOrWhiteSpace() &&
|
||||
queryParams.Release.IsNullOrWhiteSpace() &&
|
||||
!queryParams.TvMaze.HasValue &&
|
||||
queryParams.Imdb.IsNullOrWhiteSpace())
|
||||
{
|
||||
_logger.Debug("NBL API does not support season calls without name, series, id, imdb, tvmaze, or time keys.");
|
||||
|
||||
return new IndexerPageableRequestChain();
|
||||
}
|
||||
|
||||
pageableRequests.Add(GetPagedRequests(queryParams, searchCriteria.Limit, searchCriteria.Offset));
|
||||
|
||||
if (queryParams.Name.IsNotNullOrWhiteSpace() && (queryParams.Tvmaze is > 0 || queryParams.Imdb is > 0))
|
||||
{
|
||||
queryParams = queryParams.Clone();
|
||||
queryParams.Name = null;
|
||||
|
||||
pageableRequests.Add(GetPagedRequests(queryParams, searchCriteria.Limit, searchCriteria.Offset));
|
||||
}
|
||||
|
||||
return pageableRequests;
|
||||
}
|
||||
|
||||
@@ -187,9 +181,11 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
Age = ">0"
|
||||
};
|
||||
|
||||
if (searchCriteria.SanitizedSearchTerm.IsNotNullOrWhiteSpace())
|
||||
var searchQuery = searchCriteria.SanitizedSearchTerm.Trim();
|
||||
|
||||
if (searchQuery.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
queryParams.Name = "%" + Regex.Replace(searchCriteria.SanitizedSearchTerm, "[\\W]+", "%").Trim() + "%";
|
||||
queryParams.Release = searchQuery;
|
||||
}
|
||||
|
||||
pageableRequests.Add(GetPagedRequests(queryParams, searchCriteria.Limit, searchCriteria.Offset));
|
||||
@@ -231,11 +227,11 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
throw new IndexerException(indexerResponse, "Unexpected response status '{0}' code from indexer request", indexerResponse.HttpResponse.StatusCode);
|
||||
}
|
||||
|
||||
JsonRpcResponse<NebulanceTorrents> jsonResponse;
|
||||
JsonRpcResponse<NebulanceResponse> jsonResponse;
|
||||
|
||||
try
|
||||
{
|
||||
jsonResponse = STJson.Deserialize<JsonRpcResponse<NebulanceTorrents>>(indexerResponse.HttpResponse.Content);
|
||||
jsonResponse = STJson.Deserialize<JsonRpcResponse<NebulanceResponse>>(indexerResponse.HttpResponse.Content);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -249,7 +245,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
throw new IndexerException(indexerResponse, "Indexer API call returned an error [{0}]", jsonResponse.Error);
|
||||
}
|
||||
|
||||
if (jsonResponse.Result.Items.Count == 0)
|
||||
if (jsonResponse.Result?.Items == null || jsonResponse.Result.Items.Count == 0)
|
||||
{
|
||||
return torrentInfos;
|
||||
}
|
||||
@@ -264,14 +260,13 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
|
||||
var release = new TorrentInfo
|
||||
{
|
||||
Title = title,
|
||||
Guid = details,
|
||||
InfoUrl = details,
|
||||
PosterUrl = row.Banner,
|
||||
DownloadUrl = row.Download,
|
||||
Title = title.Trim(),
|
||||
Categories = new List<IndexerCategory> { TvCategoryFromQualityParser.ParseTvShowQuality(row.ReleaseTitle) },
|
||||
Size = ParseUtil.CoerceLong(row.Size),
|
||||
Files = row.FileList.Length,
|
||||
Files = row.FileList.Count(),
|
||||
PublishDate = DateTime.Parse(row.PublishDateUtc, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal),
|
||||
Grabs = ParseUtil.CoerceInt(row.Snatch),
|
||||
Seeders = ParseUtil.CoerceInt(row.Seed),
|
||||
@@ -280,7 +275,8 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
MinimumRatio = 0, // ratioless
|
||||
MinimumSeedTime = row.Category.ToLower() == "season" ? 432000 : 86400, // 120 hours for seasons and 24 hours for episodes
|
||||
DownloadVolumeFactor = 0, // ratioless tracker
|
||||
UploadVolumeFactor = 1
|
||||
UploadVolumeFactor = 1,
|
||||
PosterUrl = row.Banner
|
||||
};
|
||||
|
||||
if (row.TvMazeId.IsNotNullOrWhiteSpace())
|
||||
@@ -312,60 +308,86 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
{
|
||||
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
|
||||
public string Id { get; set; }
|
||||
|
||||
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
|
||||
public string Time { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName="age", DefaultValueHandling = DefaultValueHandling.Ignore)]
|
||||
public string Age { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName="tvmaze", DefaultValueHandling = DefaultValueHandling.Ignore)]
|
||||
public int? Tvmaze { get; set; }
|
||||
public int? TvMaze { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName="imdb", DefaultValueHandling = DefaultValueHandling.Ignore)]
|
||||
public int? Imdb { get; set; }
|
||||
public string Imdb { get; set; }
|
||||
|
||||
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
|
||||
public string Hash { get; set; }
|
||||
|
||||
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
|
||||
public string[] Tags { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName="name", DefaultValueHandling = DefaultValueHandling.Ignore)]
|
||||
public string Name { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName="release", DefaultValueHandling = DefaultValueHandling.Ignore)]
|
||||
public string Release { get; set; }
|
||||
|
||||
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
|
||||
public string Category { get; set; }
|
||||
|
||||
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
|
||||
public string Series { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName="season", DefaultValueHandling = DefaultValueHandling.Ignore)]
|
||||
public int? Season { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName="episode", DefaultValueHandling = DefaultValueHandling.Ignore)]
|
||||
public int? Episode { get; set; }
|
||||
|
||||
public NebulanceQuery Clone()
|
||||
{
|
||||
return MemberwiseClone() as NebulanceQuery;
|
||||
}
|
||||
}
|
||||
|
||||
public class NebulanceResponse
|
||||
{
|
||||
public List<NebulanceTorrent> Items { get; set; }
|
||||
}
|
||||
|
||||
public class NebulanceTorrent
|
||||
{
|
||||
[JsonPropertyName("rls_name")]
|
||||
public string ReleaseTitle { get; set; }
|
||||
|
||||
[JsonPropertyName("cat")]
|
||||
public string Category { get; set; }
|
||||
|
||||
public string Size { get; set; }
|
||||
public string Seed { get; set; }
|
||||
public string Leech { get; set; }
|
||||
public string Snatch { get; set; }
|
||||
public string Download { get; set; }
|
||||
|
||||
[JsonPropertyName("file_list")]
|
||||
public string[] FileList { get; set; }
|
||||
public IEnumerable<string> FileList { get; set; } = Array.Empty<string>();
|
||||
|
||||
[JsonPropertyName("group_name")]
|
||||
public string GroupName { get; set; }
|
||||
|
||||
[JsonPropertyName("series_banner")]
|
||||
public string Banner { get; set; }
|
||||
|
||||
[JsonPropertyName("group_id")]
|
||||
public string TorrentId { get; set; }
|
||||
|
||||
[JsonPropertyName("series_id")]
|
||||
public string TvMazeId { get; set; }
|
||||
|
||||
[JsonPropertyName("rls_utc")]
|
||||
public string PublishDateUtc { get; set; }
|
||||
public IEnumerable<string> Tags { get; set; }
|
||||
}
|
||||
|
||||
public class NebulanceTorrents
|
||||
{
|
||||
public List<NebulanceTorrent> Items { get; set; }
|
||||
public int Results { get; set; }
|
||||
public IEnumerable<string> Tags { get; set; } = Array.Empty<string>();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,16 +74,17 @@ namespace NzbDrone.Core.Indexers.Newznab
|
||||
protected override bool PostProcess(IndexerResponse indexerResponse, List<XElement> items, List<ReleaseInfo> releases)
|
||||
{
|
||||
var enclosureTypes = items.SelectMany(GetEnclosures).Select(v => v.Type).Distinct().ToArray();
|
||||
|
||||
if (enclosureTypes.Any() && enclosureTypes.Intersect(PreferredEnclosureMimeTypes).Empty())
|
||||
{
|
||||
if (enclosureTypes.Intersect(TorrentEnclosureMimeTypes).Any())
|
||||
{
|
||||
_logger.Warn("Feed does not contain {0}, found {1}, did you intend to add a Torznab indexer?", NzbEnclosureMimeType, enclosureTypes[0]);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.Warn("Feed does not contain {0}, found {1}.", NzbEnclosureMimeType, enclosureTypes[0]);
|
||||
_logger.Warn("{0} does not contain {1}, found {2}, did you intend to add a Torznab indexer?", indexerResponse.Request.Url, NzbEnclosureMimeType, enclosureTypes[0]);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
_logger.Warn("{0} does not contain {1}, found {2}.", indexerResponse.Request.Url, NzbEnclosureMimeType, enclosureTypes[0]);
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
@@ -75,6 +75,8 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
|
||||
public class SubsPleaseRequestGenerator : IIndexerRequestGenerator
|
||||
{
|
||||
private static readonly Regex ResolutionRegex = new (@"\d{3,4}p", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
|
||||
private readonly NoAuthTorrentBaseSettings _settings;
|
||||
|
||||
public SubsPleaseRequestGenerator(NoAuthTorrentBaseSettings settings)
|
||||
@@ -134,15 +136,6 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
|
||||
private IEnumerable<IndexerRequest> GetSearchRequests(string term, SearchCriteriaBase searchCriteria)
|
||||
{
|
||||
var searchTerm = Regex.Replace(term, "\\[?SubsPlease\\]?\\s*", string.Empty, RegexOptions.IgnoreCase).Trim();
|
||||
|
||||
// If the search terms contain a resolution, remove it from the query sent to the API
|
||||
var resMatch = Regex.Match(searchTerm, "\\d{3,4}[p|P]");
|
||||
if (resMatch.Success)
|
||||
{
|
||||
searchTerm = searchTerm.Replace(resMatch.Value, string.Empty).Trim();
|
||||
}
|
||||
|
||||
var queryParameters = new NameValueCollection
|
||||
{
|
||||
{ "tz", "UTC" }
|
||||
@@ -154,6 +147,16 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
}
|
||||
else
|
||||
{
|
||||
var searchTerm = Regex.Replace(term, "\\[?SubsPlease\\]?\\s*", string.Empty, RegexOptions.IgnoreCase).Trim();
|
||||
|
||||
// If the search terms contain a resolution, remove it from the query sent to the API
|
||||
var resolutionMatch = ResolutionRegex.Match(searchTerm);
|
||||
|
||||
if (resolutionMatch.Success)
|
||||
{
|
||||
searchTerm = searchTerm.Replace(resolutionMatch.Value, string.Empty).Trim();
|
||||
}
|
||||
|
||||
queryParameters.Set("f", "search");
|
||||
queryParameters.Set("s", searchTerm);
|
||||
}
|
||||
@@ -201,7 +204,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
{
|
||||
var release = new TorrentInfo
|
||||
{
|
||||
InfoUrl = _settings.BaseUrl + $"shows/{value.Page}/",
|
||||
InfoUrl = $"{_settings.BaseUrl}shows/{value.Page}/",
|
||||
PublishDate = value.ReleaseDate.LocalDateTime,
|
||||
Files = 1,
|
||||
Categories = new List<IndexerCategory> { NewznabStandardCategory.TVAnime },
|
||||
@@ -213,13 +216,18 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
UploadVolumeFactor = 1
|
||||
};
|
||||
|
||||
if (value.ImageUrl.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
release.PosterUrl = _settings.BaseUrl + value.ImageUrl.TrimStart('/');
|
||||
}
|
||||
|
||||
if (value.Episode.ToLowerInvariant() == "movie")
|
||||
{
|
||||
release.Categories.Add(NewznabStandardCategory.MoviesOther);
|
||||
}
|
||||
|
||||
// Ex: [SubsPlease] Shingeki no Kyojin (The Final Season) - 64 (1080p)
|
||||
release.Title += $"[SubsPlease] {value.Show} - {value.Episode} ({d.Resolution}p)";
|
||||
release.Title = $"[SubsPlease] {value.Show} - {value.Episode} ({d.Resolution}p)";
|
||||
release.MagnetUrl = d.Magnet;
|
||||
release.DownloadUrl = null;
|
||||
release.Guid = d.Magnet;
|
||||
@@ -269,6 +277,8 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
public string Episode { get; set; }
|
||||
public SubPleaseDownloadInfo[] Downloads { get; set; }
|
||||
public string Xdcc { get; set; }
|
||||
|
||||
[JsonProperty("image_url")]
|
||||
public string ImageUrl { get; set; }
|
||||
public string Page { get; set; }
|
||||
}
|
||||
|
||||
@@ -3,10 +3,11 @@ using System.Collections.Generic;
|
||||
using System.Collections.Specialized;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System.Text.Json.Serialization;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Common.Serializer;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.Indexers.Settings;
|
||||
using NzbDrone.Core.IndexerSearch.Definitions;
|
||||
@@ -145,32 +146,31 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
{
|
||||
var releaseInfos = new List<ReleaseInfo>();
|
||||
|
||||
var jsonContent = JArray.Parse(indexerResponse.Content);
|
||||
var jsonResponse = STJson.Deserialize<TorrentsCSVResponse>(indexerResponse.Content);
|
||||
|
||||
foreach (var torrent in jsonContent)
|
||||
foreach (var torrent in jsonResponse.Torrents)
|
||||
{
|
||||
if (torrent == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var infoHash = torrent.Value<string>("infohash");
|
||||
var title = torrent.Value<string>("name");
|
||||
var size = torrent.Value<long>("size_bytes");
|
||||
var seeders = torrent.Value<int?>("seeders") ?? 0;
|
||||
var leechers = torrent.Value<int?>("leechers") ?? 0;
|
||||
var grabs = torrent.Value<int?>("completed") ?? 0;
|
||||
var publishDate = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc).AddSeconds(torrent.Value<long>("created_unix"));
|
||||
var infoHash = torrent.InfoHash;
|
||||
var title = torrent.Name;
|
||||
var seeders = torrent.Seeders ?? 0;
|
||||
var leechers = torrent.Leechers ?? 0;
|
||||
var grabs = torrent.Completed ?? 0;
|
||||
var publishDate = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc).AddSeconds(torrent.Created);
|
||||
|
||||
var release = new TorrentInfo
|
||||
{
|
||||
Title = title,
|
||||
InfoUrl = $"{_settings.BaseUrl.TrimEnd('/')}/search/{title}", // there is no details link
|
||||
Guid = $"magnet:?xt=urn:btih:{infoHash}",
|
||||
InfoUrl = $"{_settings.BaseUrl.TrimEnd('/')}/search?q={title}", // there is no details link
|
||||
Title = title,
|
||||
InfoHash = infoHash, // magnet link is auto generated from infohash
|
||||
Categories = new List<IndexerCategory> { NewznabStandardCategory.Other },
|
||||
PublishDate = publishDate,
|
||||
Size = size,
|
||||
Size = torrent.Size,
|
||||
Grabs = grabs,
|
||||
Seeders = seeders,
|
||||
Peers = leechers + seeders,
|
||||
@@ -188,4 +188,29 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
|
||||
public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }
|
||||
}
|
||||
|
||||
public class TorrentsCSVResponse
|
||||
{
|
||||
public IReadOnlyCollection<TorrentsCSVTorrent> Torrents { get; set; }
|
||||
}
|
||||
|
||||
public class TorrentsCSVTorrent
|
||||
{
|
||||
[JsonPropertyName("infohash")]
|
||||
public string InfoHash { get; set; }
|
||||
|
||||
public string Name { get; set; }
|
||||
|
||||
[JsonPropertyName("size_bytes")]
|
||||
public long Size { get; set; }
|
||||
|
||||
[JsonPropertyName("created_unix")]
|
||||
public long Created { get; set; }
|
||||
|
||||
public int? Leechers { get; set; }
|
||||
|
||||
public int? Seeders { get; set; }
|
||||
|
||||
public int? Completed { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,16 +100,17 @@ namespace NzbDrone.Core.Indexers.Torznab
|
||||
protected override bool PostProcess(IndexerResponse indexerResponse, List<XElement> items, List<ReleaseInfo> releases)
|
||||
{
|
||||
var enclosureTypes = items.SelectMany(GetEnclosures).Select(v => v.Type).Distinct().ToArray();
|
||||
|
||||
if (enclosureTypes.Any() && enclosureTypes.Intersect(PreferredEnclosureMimeTypes).Empty())
|
||||
{
|
||||
if (enclosureTypes.Intersect(UsenetEnclosureMimeTypes).Any())
|
||||
{
|
||||
_logger.Warn("Feed does not contain {0}, found {1}, did you intend to add a Newznab indexer?", TorrentEnclosureMimeType, enclosureTypes[0]);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.Warn("Feed does not contain {0}, found {1}.", TorrentEnclosureMimeType, enclosureTypes[0]);
|
||||
_logger.Warn("{0} does not contain {1}, found {2}, did you intend to add a Newznab indexer?", indexerResponse.Request.Url, TorrentEnclosureMimeType, enclosureTypes[0]);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
_logger.Warn("{0} does not contain {1}, found {2}.", indexerResponse.Request.Url, TorrentEnclosureMimeType, enclosureTypes[0]);
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
@@ -290,9 +290,9 @@ namespace NzbDrone.Core.Indexers
|
||||
Length = v.Attribute("length")?.Value?.ParseInt64() ?? 0
|
||||
};
|
||||
}
|
||||
catch (Exception e)
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Warn(e, "Failed to get enclosure for: {0}", item.Title());
|
||||
_logger.Warn(ex, "Failed to get enclosure for: {0}", item.Title());
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
@@ -60,7 +60,7 @@ namespace Prowlarr.Api.V1.Indexers
|
||||
|
||||
if (definition.Implementation == nameof(Cardigann))
|
||||
{
|
||||
var extraFields = definition.ExtraFields?.Select(MapCardigannField).ToList() ?? new List<Field>();
|
||||
var extraFields = definition.ExtraFields?.Select((field, i) => MapCardigannField(definition, field, i)).ToList() ?? new List<Field>();
|
||||
|
||||
resource.Fields.AddRange(extraFields);
|
||||
|
||||
@@ -160,7 +160,7 @@ namespace Prowlarr.Api.V1.Indexers
|
||||
};
|
||||
}
|
||||
|
||||
private Field MapCardigannField(SettingsField setting, int order)
|
||||
private Field MapCardigannField(IndexerDefinition definition, SettingsField setting, int order)
|
||||
{
|
||||
var field = new Field
|
||||
{
|
||||
@@ -185,7 +185,7 @@ namespace Prowlarr.Api.V1.Indexers
|
||||
{
|
||||
field.Value = bool.TryParse(setting.Default, out var value) && value;
|
||||
}
|
||||
else if (setting.Type is "info_cookie" or "info_flaresolverr" or "info_useragent")
|
||||
else if (setting.Type is "info_cookie" or "info_flaresolverr" or "info_useragent" or "info_category_8000")
|
||||
{
|
||||
field.Type = "info";
|
||||
|
||||
@@ -203,6 +203,10 @@ namespace Prowlarr.Api.V1.Indexers
|
||||
field.Label = "How to get the User-Agent";
|
||||
field.Value = "<ol><li>From the same place you fetched the cookie,</li><li>Find <b>'user-agent:'</b> in the <b>Request Headers</b> section</li><li><b>Select</b> and <b>Copy</b> the whole user-agent string <i>(everything after 'user-agent: ')</i> and <b>Paste</b> here.</li></ol>";
|
||||
break;
|
||||
case "info_category_8000":
|
||||
field.Label = $"About {definition.Name} Categories";
|
||||
field.Value = $"{definition.Name} does not return categories in its search results. To sync to your apps, include 8000(Other) in your Apps' Sync Categories.";
|
||||
break;
|
||||
}
|
||||
}
|
||||
else
|
||||
|
||||
Reference in New Issue
Block a user