Compare commits

...

21 Commits

Author SHA1 Message Date
Bogdan
45d8a8a4e6 Minor fixes and cover link for SubsPlease 2024-07-06 22:12:36 +03:00
Bogdan
a4546c77ce Avoid invalid requests for Nebulance 2024-07-06 11:33:21 +03:00
Bogdan
d69bf6360a Fixed: (Nebulance) Improve searching by release names 2024-07-06 09:21:55 +03:00
Bogdan
da9ce5b5c3 New: Enable "Sync Anime Standard Format Search" by default for new Sonarr apps 2024-07-05 22:26:34 +03:00
Bogdan
e092098101 Minor improvements to season parsing from titles for AnimeBytes 2024-07-05 16:39:04 +03:00
Bogdan
1a89a79b74 Avoid NullRef for missing filelist and tags fields 2024-07-05 16:13:32 +03:00
Bogdan
cb6bf49922 New: (Nebulance) Improvements for season and episode searching 2024-07-05 12:42:51 +03:00
Bogdan
4bcaba0be0 Fixed: Trimming disabled logs database
(cherry picked from commit d5dff8e8d6301b661a713702e1c476705423fc4f)
2024-07-01 05:41:37 +03:00
Bogdan
220ef723c7 Bump version to 1.20.1 2024-06-30 07:22:49 +03:00
Bogdan
9c599a6be4 New: (UI) Indexer privacy label
Fixes #2132
2024-06-28 05:43:09 +03:00
Bogdan
715ce1fc6c Refresh indexers list and status on page change 2024-06-28 04:57:24 +03:00
Bogdan
8c3a192dd0 Fixed: Ignore auth events from queries and grab stats 2024-06-28 04:56:18 +03:00
Bogdan
d22bf93dfd Fixed: Searches with season/episodes should not be treated as ID searches 2024-06-27 08:06:26 +03:00
Bogdan
886054fdf8 Bump indexers definition version to 11 2024-06-27 04:35:27 +03:00
Bogdan
4188510586 New: (Cardigann) Add info_category_8000 2024-06-27 04:35:27 +03:00
Bogdan
fedebca5e1 New: (Cardigann) Optional login selectorinputs and getselectorinputs 2024-06-27 04:35:27 +03:00
Bogdan
e2ce6437e9 Bump mac image to 12 2024-06-26 23:52:41 +03:00
Mark McDowall
bdae60bac9 Improvements to EnhancedSelectInput
(cherry picked from commit 4c622fd41289cd293a68a6a9f6b8da2a086edecb)
2024-06-26 04:47:25 +03:00
Bogdan
2d6c818aec Fixed: Exclude invalid releases from Newznab and Torznab parsers 2024-06-26 03:47:08 +03:00
Bogdan
a1d19852dc Switch TorrentsCSV to STJson 2024-06-21 02:25:21 +03:00
Bogdan
104c95f28f Bump version to 1.20.0 2024-06-20 19:03:57 +03:00
29 changed files with 336 additions and 185 deletions

View File

@@ -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:

View File

@@ -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}

View File

@@ -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
};
});
}

View File

@@ -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} />

View File

@@ -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]);

View File

@@ -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>
);
}

View 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);
}

View 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;

View 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;

View File

@@ -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[];

View File

@@ -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}

View File

@@ -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; }

View File

@@ -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();
}
}

View File

@@ -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())
{

View File

@@ -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;
}
}

View File

@@ -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 ()

View File

@@ -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);
}
}

View File

@@ -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));
}

View File

@@ -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;

View File

@@ -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);
}
}
}

View File

@@ -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
{

View File

@@ -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;

View File

@@ -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>();
}
}

View File

@@ -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;

View File

@@ -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; }
}

View File

@@ -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; }
}
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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