mirror of
https://github.com/Prowlarr/Prowlarr.git
synced 2026-04-17 21:44:48 -04:00
Compare commits
17 Commits
v1.23.0.46
...
v1.24.0.47
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
66d722e097 | ||
|
|
39befe5aa4 | ||
|
|
ab043e87dc | ||
|
|
58ae9c0a13 | ||
|
|
44c446943c | ||
|
|
8301b669fe | ||
|
|
6fa0b79c67 | ||
|
|
32d23d6636 | ||
|
|
b31b695887 | ||
|
|
33de32b138 | ||
|
|
753b53a529 | ||
|
|
123535b9a5 | ||
|
|
7a5fa452f0 | ||
|
|
281e712542 | ||
|
|
c2c34ecf53 | ||
|
|
615193617c | ||
|
|
1b58d50b6d |
@@ -9,7 +9,7 @@ variables:
|
||||
testsFolder: './_tests'
|
||||
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
|
||||
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
|
||||
majorVersion: '1.23.0'
|
||||
majorVersion: '1.24.0'
|
||||
minorVersion: $[counter('minorVersion', 1)]
|
||||
prowlarrVersion: '$(majorVersion).$(minorVersion)'
|
||||
buildName: '$(Build.SourceBranchName).$(prowlarrVersion)'
|
||||
|
||||
@@ -7,7 +7,7 @@ import { icons } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import IndexerSearchInputConnector from './IndexerSearchInputConnector';
|
||||
import KeyboardShortcutsModal from './KeyboardShortcutsModal';
|
||||
import PageHeaderActionsMenuConnector from './PageHeaderActionsMenuConnector';
|
||||
import PageHeaderActionsMenu from './PageHeaderActionsMenu';
|
||||
import styles from './PageHeader.css';
|
||||
|
||||
class PageHeader extends Component {
|
||||
@@ -87,7 +87,8 @@ class PageHeader extends Component {
|
||||
to="https://translate.servarr.com/projects/servarr/prowlarr/"
|
||||
size={24}
|
||||
/>
|
||||
<PageHeaderActionsMenuConnector
|
||||
|
||||
<PageHeaderActionsMenu
|
||||
onKeyboardShortcutsPress={this.onOpenKeyboardShortcutsModal}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,90 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Icon from 'Components/Icon';
|
||||
import Menu from 'Components/Menu/Menu';
|
||||
import MenuButton from 'Components/Menu/MenuButton';
|
||||
import MenuContent from 'Components/Menu/MenuContent';
|
||||
import MenuItem from 'Components/Menu/MenuItem';
|
||||
import MenuItemSeparator from 'Components/Menu/MenuItemSeparator';
|
||||
import { align, icons, kinds } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './PageHeaderActionsMenu.css';
|
||||
|
||||
function PageHeaderActionsMenu(props) {
|
||||
const {
|
||||
formsAuth,
|
||||
onKeyboardShortcutsPress,
|
||||
onRestartPress,
|
||||
onShutdownPress
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Menu alignMenu={align.RIGHT}>
|
||||
<MenuButton className={styles.menuButton} aria-label="Menu Button">
|
||||
<Icon
|
||||
name={icons.INTERACTIVE}
|
||||
title={translate('Menu')}
|
||||
/>
|
||||
</MenuButton>
|
||||
|
||||
<MenuContent>
|
||||
<MenuItem onPress={onKeyboardShortcutsPress}>
|
||||
<Icon
|
||||
className={styles.itemIcon}
|
||||
name={icons.KEYBOARD}
|
||||
/>
|
||||
{translate('KeyboardShortcuts')}
|
||||
</MenuItem>
|
||||
|
||||
<MenuItemSeparator />
|
||||
|
||||
<MenuItem onPress={onRestartPress}>
|
||||
<Icon
|
||||
className={styles.itemIcon}
|
||||
name={icons.RESTART}
|
||||
/>
|
||||
{translate('Restart')}
|
||||
</MenuItem>
|
||||
|
||||
<MenuItem onPress={onShutdownPress}>
|
||||
<Icon
|
||||
className={styles.itemIcon}
|
||||
name={icons.SHUTDOWN}
|
||||
kind={kinds.DANGER}
|
||||
/>
|
||||
{translate('Shutdown')}
|
||||
</MenuItem>
|
||||
|
||||
{
|
||||
formsAuth &&
|
||||
<div className={styles.separator} />
|
||||
}
|
||||
|
||||
{
|
||||
formsAuth &&
|
||||
<MenuItem
|
||||
to={`${window.Prowlarr.urlBase}/logout`}
|
||||
noRouter={true}
|
||||
>
|
||||
<Icon
|
||||
className={styles.itemIcon}
|
||||
name={icons.LOGOUT}
|
||||
/>
|
||||
Logout
|
||||
</MenuItem>
|
||||
}
|
||||
</MenuContent>
|
||||
</Menu>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
PageHeaderActionsMenu.propTypes = {
|
||||
formsAuth: PropTypes.bool.isRequired,
|
||||
onKeyboardShortcutsPress: PropTypes.func.isRequired,
|
||||
onRestartPress: PropTypes.func.isRequired,
|
||||
onShutdownPress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default PageHeaderActionsMenu;
|
||||
@@ -0,0 +1,90 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import AppState from 'App/State/AppState';
|
||||
import Icon from 'Components/Icon';
|
||||
import Menu from 'Components/Menu/Menu';
|
||||
import MenuButton from 'Components/Menu/MenuButton';
|
||||
import MenuContent from 'Components/Menu/MenuContent';
|
||||
import MenuItem from 'Components/Menu/MenuItem';
|
||||
import MenuItemSeparator from 'Components/Menu/MenuItemSeparator';
|
||||
import { align, icons, kinds } from 'Helpers/Props';
|
||||
import { restart, shutdown } from 'Store/Actions/systemActions';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './PageHeaderActionsMenu.css';
|
||||
|
||||
interface PageHeaderActionsMenuProps {
|
||||
onKeyboardShortcutsPress(): void;
|
||||
}
|
||||
|
||||
function PageHeaderActionsMenu(props: PageHeaderActionsMenuProps) {
|
||||
const { onKeyboardShortcutsPress } = props;
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const { authentication, isDocker } = useSelector(
|
||||
(state: AppState) => state.system.status.item
|
||||
);
|
||||
|
||||
const formsAuth = authentication === 'forms';
|
||||
|
||||
const handleRestartPress = useCallback(() => {
|
||||
dispatch(restart());
|
||||
}, [dispatch]);
|
||||
|
||||
const handleShutdownPress = useCallback(() => {
|
||||
dispatch(shutdown());
|
||||
}, [dispatch]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Menu alignMenu={align.RIGHT}>
|
||||
<MenuButton className={styles.menuButton} aria-label="Menu Button">
|
||||
<Icon name={icons.INTERACTIVE} title={translate('Menu')} />
|
||||
</MenuButton>
|
||||
|
||||
<MenuContent>
|
||||
<MenuItem onPress={onKeyboardShortcutsPress}>
|
||||
<Icon className={styles.itemIcon} name={icons.KEYBOARD} />
|
||||
{translate('KeyboardShortcuts')}
|
||||
</MenuItem>
|
||||
|
||||
{isDocker ? null : (
|
||||
<>
|
||||
<MenuItemSeparator />
|
||||
|
||||
<MenuItem onPress={handleRestartPress}>
|
||||
<Icon className={styles.itemIcon} name={icons.RESTART} />
|
||||
{translate('Restart')}
|
||||
</MenuItem>
|
||||
|
||||
<MenuItem onPress={handleShutdownPress}>
|
||||
<Icon
|
||||
className={styles.itemIcon}
|
||||
name={icons.SHUTDOWN}
|
||||
kind={kinds.DANGER}
|
||||
/>
|
||||
{translate('Shutdown')}
|
||||
</MenuItem>
|
||||
</>
|
||||
)}
|
||||
|
||||
{formsAuth ? (
|
||||
<>
|
||||
<MenuItemSeparator />
|
||||
|
||||
<MenuItem
|
||||
to={`${window.Prowlarr.urlBase}/logout`}
|
||||
noRouter={true}
|
||||
>
|
||||
<Icon className={styles.itemIcon} name={icons.LOGOUT} />
|
||||
{translate('Logout')}
|
||||
</MenuItem>
|
||||
</>
|
||||
) : null}
|
||||
</MenuContent>
|
||||
</Menu>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default PageHeaderActionsMenu;
|
||||
@@ -1,56 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { restart, shutdown } from 'Store/Actions/systemActions';
|
||||
import PageHeaderActionsMenu from './PageHeaderActionsMenu';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.system.status,
|
||||
(status) => {
|
||||
return {
|
||||
formsAuth: status.item.authentication === 'forms'
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
restart,
|
||||
shutdown
|
||||
};
|
||||
|
||||
class PageHeaderActionsMenuConnector extends Component {
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onRestartPress = () => {
|
||||
this.props.restart();
|
||||
};
|
||||
|
||||
onShutdownPress = () => {
|
||||
this.props.shutdown();
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<PageHeaderActionsMenu
|
||||
{...this.props}
|
||||
onRestartPress={this.onRestartPress}
|
||||
onShutdownPress={this.onShutdownPress}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
PageHeaderActionsMenuConnector.propTypes = {
|
||||
restart: PropTypes.func.isRequired,
|
||||
shutdown: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(PageHeaderActionsMenuConnector);
|
||||
@@ -54,18 +54,20 @@ function getAverageResponseTimeData(indexerStats: IndexerStatsIndexer[]) {
|
||||
}
|
||||
|
||||
function getFailureRateData(indexerStats: IndexerStatsIndexer[]) {
|
||||
const data = indexerStats.map((indexer) => ({
|
||||
label: indexer.indexerName,
|
||||
value:
|
||||
(indexer.numberOfFailedQueries +
|
||||
indexer.numberOfFailedRssQueries +
|
||||
indexer.numberOfFailedAuthQueries +
|
||||
indexer.numberOfFailedGrabs) /
|
||||
(indexer.numberOfQueries +
|
||||
indexer.numberOfRssQueries +
|
||||
indexer.numberOfAuthQueries +
|
||||
indexer.numberOfGrabs),
|
||||
}));
|
||||
const data = [...indexerStats]
|
||||
.map((indexer) => ({
|
||||
label: indexer.indexerName,
|
||||
value:
|
||||
(indexer.numberOfFailedQueries +
|
||||
indexer.numberOfFailedRssQueries +
|
||||
indexer.numberOfFailedAuthQueries +
|
||||
indexer.numberOfFailedGrabs) /
|
||||
(indexer.numberOfQueries +
|
||||
indexer.numberOfRssQueries +
|
||||
indexer.numberOfAuthQueries +
|
||||
indexer.numberOfGrabs),
|
||||
}))
|
||||
.filter((s) => s.value > 0);
|
||||
|
||||
data.sort((a, b) => b.value - a.value);
|
||||
|
||||
@@ -73,13 +75,20 @@ function getFailureRateData(indexerStats: IndexerStatsIndexer[]) {
|
||||
}
|
||||
|
||||
function getTotalRequestsData(indexerStats: IndexerStatsIndexer[]) {
|
||||
const statistics = [...indexerStats].sort(
|
||||
(a, b) =>
|
||||
b.numberOfQueries +
|
||||
b.numberOfRssQueries +
|
||||
b.numberOfAuthQueries -
|
||||
(a.numberOfQueries + a.numberOfRssQueries + a.numberOfAuthQueries)
|
||||
);
|
||||
const statistics = [...indexerStats]
|
||||
.filter(
|
||||
(s) =>
|
||||
s.numberOfQueries > 0 ||
|
||||
s.numberOfRssQueries > 0 ||
|
||||
s.numberOfAuthQueries > 0
|
||||
)
|
||||
.sort(
|
||||
(a, b) =>
|
||||
b.numberOfQueries +
|
||||
b.numberOfRssQueries +
|
||||
b.numberOfAuthQueries -
|
||||
(a.numberOfQueries + a.numberOfRssQueries + a.numberOfAuthQueries)
|
||||
);
|
||||
|
||||
return {
|
||||
labels: statistics.map((indexer) => indexer.indexerName),
|
||||
@@ -101,10 +110,12 @@ function getTotalRequestsData(indexerStats: IndexerStatsIndexer[]) {
|
||||
}
|
||||
|
||||
function getNumberGrabsData(indexerStats: IndexerStatsIndexer[]) {
|
||||
const data = indexerStats.map((indexer) => ({
|
||||
label: indexer.indexerName,
|
||||
value: indexer.numberOfGrabs - indexer.numberOfFailedGrabs,
|
||||
}));
|
||||
const data = [...indexerStats]
|
||||
.map((indexer) => ({
|
||||
label: indexer.indexerName,
|
||||
value: indexer.numberOfGrabs - indexer.numberOfFailedGrabs,
|
||||
}))
|
||||
.filter((s) => s.value > 0);
|
||||
|
||||
data.sort((a, b) => b.value - a.value);
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Web;
|
||||
|
||||
namespace NzbDrone.Common.Extensions
|
||||
{
|
||||
@@ -18,5 +19,24 @@ namespace NzbDrone.Common.Extensions
|
||||
|
||||
return Uri.TryCreate(path, UriKind.Absolute, out var uri) && uri.IsWellFormedOriginalString();
|
||||
}
|
||||
|
||||
public static Uri RemoveQueryParam(this Uri url, string name)
|
||||
{
|
||||
var uriBuilder = new UriBuilder(url);
|
||||
var query = HttpUtility.ParseQueryString(uriBuilder.Query);
|
||||
|
||||
query.Remove(name);
|
||||
uriBuilder.Query = query.ToString() ?? string.Empty;
|
||||
|
||||
return uriBuilder.Uri;
|
||||
}
|
||||
|
||||
public static string GetQueryParam(this Uri url, string name)
|
||||
{
|
||||
var uriBuilder = new UriBuilder(url);
|
||||
var query = HttpUtility.ParseQueryString(uriBuilder.Query);
|
||||
|
||||
return query[name];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,8 @@ namespace NzbDrone.Common.Http.Proxy
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(BypassFilter))
|
||||
{
|
||||
var hostlist = BypassFilter.Split(',');
|
||||
var hostlist = BypassFilter.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||
|
||||
for (var i = 0; i < hostlist.Length; i++)
|
||||
{
|
||||
if (hostlist[i].StartsWith("*"))
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="DryIoc.dll" Version="5.4.3" />
|
||||
<PackageReference Include="IPAddressRange" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="6.0.2" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
|
||||
@@ -12,7 +12,7 @@ namespace NzbDrone.Core.Test.Http
|
||||
{
|
||||
private HttpProxySettings GetProxySettings()
|
||||
{
|
||||
return new HttpProxySettings(ProxyType.Socks5, "localhost", 8080, "*.httpbin.org,google.com", true, null, null);
|
||||
return new HttpProxySettings(ProxyType.Socks5, "localhost", 8080, "*.httpbin.org,google.com,172.16.0.0/12", true, null, null);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -23,6 +23,7 @@ namespace NzbDrone.Core.Test.Http
|
||||
Subject.ShouldProxyBeBypassed(settings, new HttpUri("http://eu.httpbin.org/get")).Should().BeTrue();
|
||||
Subject.ShouldProxyBeBypassed(settings, new HttpUri("http://google.com/get")).Should().BeTrue();
|
||||
Subject.ShouldProxyBeBypassed(settings, new HttpUri("http://localhost:8654/get")).Should().BeTrue();
|
||||
Subject.ShouldProxyBeBypassed(settings, new HttpUri("http://172.21.0.1:8989/api/v3/indexer/schema")).Should().BeTrue();
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -31,6 +32,7 @@ namespace NzbDrone.Core.Test.Http
|
||||
var settings = GetProxySettings();
|
||||
|
||||
Subject.ShouldProxyBeBypassed(settings, new HttpUri("http://bing.com/get")).Should().BeFalse();
|
||||
Subject.ShouldProxyBeBypassed(settings, new HttpUri("http://172.3.0.1:8989/api/v3/indexer/schema")).Should().BeFalse();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -384,7 +384,7 @@ namespace NzbDrone.Core.Configuration
|
||||
}
|
||||
|
||||
// If SSL is enabled and a cert hash is still in the config file or cert path is empty disable SSL
|
||||
if (EnableSsl && (GetValue("SslCertHash", null).IsNotNullOrWhiteSpace() || SslCertPath.IsNullOrWhiteSpace()))
|
||||
if (EnableSsl && (GetValue("SslCertHash", string.Empty, false).IsNotNullOrWhiteSpace() || SslCertPath.IsNullOrWhiteSpace()))
|
||||
{
|
||||
SetValue("EnableSsl", false);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using NetTools;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Common.Http.Proxy;
|
||||
using NzbDrone.Core.Configuration;
|
||||
@@ -52,7 +54,15 @@ namespace NzbDrone.Core.Http
|
||||
//We are utilizing the WebProxy implementation here to save us having to re-implement it. This way we use Microsofts implementation
|
||||
var proxy = new WebProxy(proxySettings.Host + ":" + proxySettings.Port, proxySettings.BypassLocalAddress, proxySettings.BypassListAsArray);
|
||||
|
||||
return proxy.IsBypassed((Uri)url);
|
||||
return proxy.IsBypassed((Uri)url) || IsBypassedByIpAddressRange(proxySettings.BypassListAsArray, url.Host);
|
||||
}
|
||||
|
||||
private static bool IsBypassedByIpAddressRange(string[] bypassList, string host)
|
||||
{
|
||||
return bypassList.Any(bypass =>
|
||||
IPAddressRange.TryParse(bypass, out var ipAddressRange) &&
|
||||
IPAddress.TryParse(host, out var ipAddress) &&
|
||||
ipAddressRange.Contains(ipAddress));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,7 +93,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
|
||||
if (searchCriteria.IsRssSearch)
|
||||
{
|
||||
cleanReleases = cleanReleases.Where((r, index) => r.PublishDate > DateTime.Now.AddDays(-1) || index < 20).ToList();
|
||||
cleanReleases = cleanReleases.Where((r, index) => r.PublishDate > DateTime.UtcNow.AddDays(-1) || index < 20).ToList();
|
||||
}
|
||||
|
||||
return cleanReleases.Select(r => (ReleaseInfo)r.Clone()).ToList();
|
||||
|
||||
@@ -59,8 +59,10 @@ public class GazelleParser : IParseIndexerResponse
|
||||
{
|
||||
foreach (var torrent in result.Torrents)
|
||||
{
|
||||
var isFreeLeech = torrent.IsFreeLeech || torrent.IsNeutralLeech || torrent.IsPersonalFreeLeech;
|
||||
|
||||
// skip releases that cannot be used with freeleech tokens when the option is enabled
|
||||
if (Settings.UseFreeleechToken && !torrent.CanUseToken)
|
||||
if (Settings.UseFreeleechToken && !torrent.CanUseToken && !isFreeLeech)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
@@ -79,7 +81,7 @@ public class GazelleParser : IParseIndexerResponse
|
||||
{
|
||||
Guid = infoUrl,
|
||||
InfoUrl = infoUrl,
|
||||
DownloadUrl = GetDownloadUrl(id, !torrent.IsFreeLeech && !torrent.IsNeutralLeech && !torrent.IsPersonalFreeLeech),
|
||||
DownloadUrl = GetDownloadUrl(id, torrent.CanUseToken && !isFreeLeech),
|
||||
Title = WebUtility.HtmlDecode(title),
|
||||
Container = torrent.Encoding,
|
||||
Files = torrent.FileCount,
|
||||
@@ -91,7 +93,7 @@ public class GazelleParser : IParseIndexerResponse
|
||||
PublishDate = torrent.Time.ToUniversalTime(),
|
||||
Scene = torrent.Scene,
|
||||
PosterUrl = posterUrl,
|
||||
DownloadVolumeFactor = torrent.IsFreeLeech || torrent.IsNeutralLeech || torrent.IsPersonalFreeLeech ? 0 : 1,
|
||||
DownloadVolumeFactor = isFreeLeech ? 0 : 1,
|
||||
UploadVolumeFactor = torrent.IsNeutralLeech ? 0 : 1
|
||||
};
|
||||
|
||||
@@ -110,8 +112,10 @@ public class GazelleParser : IParseIndexerResponse
|
||||
}
|
||||
else
|
||||
{
|
||||
var isFreeLeech = result.IsFreeLeech || result.IsNeutralLeech || result.IsPersonalFreeLeech;
|
||||
|
||||
// skip releases that cannot be used with freeleech tokens when the option is enabled
|
||||
if (Settings.UseFreeleechToken && !result.CanUseToken)
|
||||
if (Settings.UseFreeleechToken && !result.CanUseToken && !isFreeLeech)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
@@ -124,7 +128,7 @@ public class GazelleParser : IParseIndexerResponse
|
||||
{
|
||||
Guid = infoUrl,
|
||||
InfoUrl = infoUrl,
|
||||
DownloadUrl = GetDownloadUrl(id, !result.IsFreeLeech && !result.IsNeutralLeech && !result.IsPersonalFreeLeech),
|
||||
DownloadUrl = GetDownloadUrl(id, result.CanUseToken && !isFreeLeech),
|
||||
Title = groupName,
|
||||
Size = long.Parse(result.Size),
|
||||
Seeders = int.Parse(result.Seeders),
|
||||
@@ -133,7 +137,7 @@ public class GazelleParser : IParseIndexerResponse
|
||||
Grabs = result.Snatches,
|
||||
PublishDate = long.TryParse(result.GroupTime, out var num) ? DateTimeOffset.FromUnixTimeSeconds(num).UtcDateTime : DateTimeUtil.FromFuzzyTime((string)result.GroupTime),
|
||||
PosterUrl = posterUrl,
|
||||
DownloadVolumeFactor = result.IsFreeLeech || result.IsNeutralLeech || result.IsPersonalFreeLeech ? 0 : 1,
|
||||
DownloadVolumeFactor = isFreeLeech ? 0 : 1,
|
||||
UploadVolumeFactor = result.IsNeutralLeech ? 0 : 1
|
||||
};
|
||||
|
||||
|
||||
@@ -168,8 +168,10 @@ public class GreatPosterWallParser : GazelleParser
|
||||
{
|
||||
foreach (var torrent in result.Torrents)
|
||||
{
|
||||
var isFreeLeech = torrent.IsFreeleech || torrent.IsNeutralLeech || torrent.IsPersonalFreeleech;
|
||||
|
||||
// skip releases that cannot be used with freeleech tokens when the option is enabled
|
||||
if (_settings.UseFreeleechToken && !torrent.CanUseToken)
|
||||
if (_settings.UseFreeleechToken && !torrent.CanUseToken && !isFreeLeech)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
@@ -181,7 +183,7 @@ public class GreatPosterWallParser : GazelleParser
|
||||
{
|
||||
Guid = infoUrl,
|
||||
InfoUrl = infoUrl,
|
||||
DownloadUrl = GetDownloadUrl(torrent.TorrentId, !torrent.IsFreeleech && !torrent.IsNeutralLeech && !torrent.IsPersonalFreeleech),
|
||||
DownloadUrl = GetDownloadUrl(torrent.TorrentId, torrent.CanUseToken && !isFreeLeech),
|
||||
Title = WebUtility.HtmlDecode(torrent.FileName).Trim(),
|
||||
PosterUrl = GetPosterUrl(result.Cover),
|
||||
PublishDate = new DateTimeOffset(time, TimeSpan.FromHours(8)).UtcDateTime, // Time is Chinese Time, add 8 hours difference from UTC
|
||||
@@ -192,7 +194,7 @@ public class GreatPosterWallParser : GazelleParser
|
||||
Grabs = torrent.Snatches,
|
||||
Files = torrent.FileCount,
|
||||
Scene = torrent.Scene,
|
||||
DownloadVolumeFactor = torrent.IsFreeleech || torrent.IsNeutralLeech || torrent.IsPersonalFreeleech ? 0 : 1,
|
||||
DownloadVolumeFactor = isFreeLeech ? 0 : 1,
|
||||
UploadVolumeFactor = torrent.IsNeutralLeech ? 0 : 1,
|
||||
MinimumRatio = 1,
|
||||
MinimumSeedTime = 172800 // 48 hours
|
||||
|
||||
@@ -7,6 +7,7 @@ using System.Net;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using FluentValidation;
|
||||
using FluentValidation.Results;
|
||||
using Newtonsoft.Json;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Cache;
|
||||
@@ -55,11 +56,13 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
|
||||
public override async Task<IndexerDownloadResponse> Download(Uri link)
|
||||
{
|
||||
if (Settings.Freeleech)
|
||||
{
|
||||
_logger.Debug($"Attempting to use freeleech token for {link.AbsoluteUri}");
|
||||
var downloadLink = link.RemoveQueryParam("canUseToken");
|
||||
|
||||
var idMatch = TorrentIdRegex.Match(link.AbsoluteUri);
|
||||
if (Settings.Freeleech && bool.TryParse(link.GetQueryParam("canUseToken"), out var canUseToken) && canUseToken)
|
||||
{
|
||||
_logger.Debug("Attempting to use freeleech token for {0}", downloadLink.AbsoluteUri);
|
||||
|
||||
var idMatch = TorrentIdRegex.Match(downloadLink.AbsoluteUri);
|
||||
if (idMatch.Success)
|
||||
{
|
||||
var id = int.Parse(idMatch.Groups["id"].Value);
|
||||
@@ -78,25 +81,41 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
|
||||
if (resource.Success)
|
||||
{
|
||||
_logger.Debug($"Successfully to used freeleech token for torrentid ${id}");
|
||||
_logger.Debug("Successfully to used freeleech token for torrentid {0}", id);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.Debug($"Failed to use freeleech token: ${resource.Error}");
|
||||
_logger.Debug("Failed to use freeleech token: {0}", resource.Error);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.Debug($"Could not get torrent id from link ${link.AbsoluteUri}, skipping freeleech");
|
||||
_logger.Debug("Could not get torrent id from link {0}, skipping freeleech", downloadLink.AbsoluteUri);
|
||||
}
|
||||
}
|
||||
|
||||
return await base.Download(link).ConfigureAwait(false);
|
||||
return await base.Download(downloadLink).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
protected override IDictionary<string, string> GetCookies()
|
||||
{
|
||||
return CookieUtil.CookieHeaderToDictionary("mam_id=" + Settings.MamId);
|
||||
var cookies = base.GetCookies();
|
||||
|
||||
if (cookies is { Count: > 0 } && cookies.TryGetValue("mam_id", out var mamId) && mamId.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
return cookies;
|
||||
}
|
||||
|
||||
return CookieUtil.CookieHeaderToDictionary($"mam_id={Settings.MamId}");
|
||||
}
|
||||
|
||||
protected override async Task Test(List<ValidationFailure> failures)
|
||||
{
|
||||
UpdateCookies(null, null);
|
||||
|
||||
_logger.Debug("Cookies cleared.");
|
||||
|
||||
await base.Test(failures).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private IndexerCapabilities SetCapabilities()
|
||||
@@ -359,11 +378,12 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
private readonly IndexerCapabilitiesCategories _categories;
|
||||
private readonly IIndexerHttpClient _httpClient;
|
||||
private readonly Logger _logger;
|
||||
|
||||
private readonly ICached<string> _userClassCache;
|
||||
private readonly HashSet<string> _vipFreeleechUserClasses = new (StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
"VIP",
|
||||
"Elite VIP",
|
||||
"Elite VIP"
|
||||
};
|
||||
|
||||
public MyAnonamouseParser(MyAnonamouseSettings settings,
|
||||
@@ -376,32 +396,35 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
_categories = categories;
|
||||
_httpClient = httpClient;
|
||||
_logger = logger;
|
||||
|
||||
_userClassCache = cacheManager.GetCache<string>(GetType());
|
||||
}
|
||||
|
||||
public IList<ReleaseInfo> ParseResponse(IndexerResponse indexerResponse)
|
||||
{
|
||||
var httpResponse = indexerResponse.HttpResponse;
|
||||
|
||||
// Throw auth errors here before we try to parse
|
||||
if (indexerResponse.HttpResponse.StatusCode == HttpStatusCode.Forbidden)
|
||||
if (httpResponse.StatusCode == HttpStatusCode.Forbidden)
|
||||
{
|
||||
throw new IndexerAuthException("[403 Forbidden] - mam_id expired or invalid");
|
||||
}
|
||||
|
||||
// Throw common http errors here before we try to parse
|
||||
if (indexerResponse.HttpResponse.StatusCode != HttpStatusCode.OK)
|
||||
if (httpResponse.StatusCode != HttpStatusCode.OK)
|
||||
{
|
||||
// Remove cookie cache
|
||||
CookiesUpdater(null, null);
|
||||
|
||||
throw new IndexerException(indexerResponse, $"Unexpected response status {indexerResponse.HttpResponse.StatusCode} code from indexer request");
|
||||
throw new IndexerException(indexerResponse, $"Unexpected response status {httpResponse.StatusCode} code from indexer request");
|
||||
}
|
||||
|
||||
if (!indexerResponse.HttpResponse.Headers.ContentType.Contains(HttpAccept.Json.Value))
|
||||
if (!httpResponse.Headers.ContentType.Contains(HttpAccept.Json.Value))
|
||||
{
|
||||
// Remove cookie cache
|
||||
CookiesUpdater(null, null);
|
||||
|
||||
throw new IndexerException(indexerResponse, $"Unexpected response header {indexerResponse.HttpResponse.Headers.ContentType} from indexer request, expected {HttpAccept.Json.Value}");
|
||||
throw new IndexerException(indexerResponse, $"Unexpected response header {httpResponse.Headers.ContentType} from indexer request, expected {HttpAccept.Json.Value}");
|
||||
}
|
||||
|
||||
var releaseInfos = new List<ReleaseInfo>();
|
||||
@@ -414,7 +437,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
return releaseInfos.ToArray();
|
||||
}
|
||||
|
||||
var hasUserVip = HasUserVip();
|
||||
var hasUserVip = HasUserVip(httpResponse.GetCookies());
|
||||
|
||||
foreach (var item in jsonResponse.Data)
|
||||
{
|
||||
@@ -464,8 +487,10 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
release.Title += " [VIP]";
|
||||
}
|
||||
|
||||
release.DownloadUrl = _settings.BaseUrl + "tor/download.php?tid=" + id;
|
||||
release.InfoUrl = _settings.BaseUrl + "t/" + id;
|
||||
var isFreeLeech = item.Free || item.PersonalFreeLeech || (hasUserVip && item.FreeVip);
|
||||
|
||||
release.DownloadUrl = GetDownloadUrl(id, !isFreeLeech);
|
||||
release.InfoUrl = $"{_settings.BaseUrl}t/{id}";
|
||||
release.Guid = release.InfoUrl;
|
||||
release.Categories = _categories.MapTrackerCatToNewznab(item.Category);
|
||||
release.PublishDate = DateTime.ParseExact(item.Added, "yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal).ToLocalTime();
|
||||
@@ -474,7 +499,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
release.Seeders = item.Seeders;
|
||||
release.Peers = item.Leechers + release.Seeders;
|
||||
release.Size = ParseUtil.GetBytes(item.Size);
|
||||
release.DownloadVolumeFactor = item.Free ? 0 : hasUserVip && item.FreeVip ? 0 : 1;
|
||||
release.DownloadVolumeFactor = isFreeLeech ? 0 : 1;
|
||||
release.UploadVolumeFactor = 1;
|
||||
release.MinimumRatio = 1;
|
||||
release.MinimumSeedTime = 259200; // 72 hours
|
||||
@@ -482,10 +507,27 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
releaseInfos.Add(release);
|
||||
}
|
||||
|
||||
// Update cookies with the updated mam_id value received in the response
|
||||
CookiesUpdater(httpResponse.GetCookies(), DateTime.Now.AddDays(30));
|
||||
|
||||
return releaseInfos.ToArray();
|
||||
}
|
||||
|
||||
private bool HasUserVip()
|
||||
private string GetDownloadUrl(int torrentId, bool canUseToken)
|
||||
{
|
||||
var url = new HttpUri(_settings.BaseUrl)
|
||||
.CombinePath("/tor/download.php")
|
||||
.AddQueryParam("tid", torrentId);
|
||||
|
||||
if (_settings.Freeleech && canUseToken)
|
||||
{
|
||||
url = url.AddQueryParam("canUseToken", "true");
|
||||
}
|
||||
|
||||
return url.FullUri;
|
||||
}
|
||||
|
||||
private bool HasUserVip(Dictionary<string, string> cookies)
|
||||
{
|
||||
var cacheKey = "myanonamouse_user_class_" + _settings.ToJson().SHA256Hash();
|
||||
|
||||
@@ -496,11 +538,10 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
var request = new HttpRequestBuilder(_settings.BaseUrl.Trim('/'))
|
||||
.Resource("/jsonLoad.php")
|
||||
.Accept(HttpAccept.Json)
|
||||
.SetCookies(cookies)
|
||||
.Build();
|
||||
|
||||
_logger.Debug("Fetching user data: " + request.Url.FullUri);
|
||||
|
||||
request.Cookies.Add("mam_id", _settings.MamId);
|
||||
_logger.Debug("Fetching user data: {0}", request.Url.FullUri);
|
||||
|
||||
var response = _httpClient.Get(request);
|
||||
var jsonResponse = JsonConvert.DeserializeObject<MyAnonamouseUserDataResponse>(response.Content);
|
||||
@@ -564,14 +605,19 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
{
|
||||
[FieldOption(Label="All torrents", Hint = "Search everything")]
|
||||
All = 0,
|
||||
|
||||
[FieldOption(Label="Only active", Hint = "Last update had 1+ seeders")]
|
||||
Active = 1,
|
||||
|
||||
[FieldOption(Label="Freeleech", Hint = "Freeleech torrents")]
|
||||
Freeleech = 2,
|
||||
|
||||
[FieldOption(Label="Freeleech or VIP", Hint = "Freeleech or VIP torrents")]
|
||||
FreeleechOrVip = 3,
|
||||
|
||||
[FieldOption(Label="VIP", Hint = "VIP torrents")]
|
||||
Vip = 4,
|
||||
|
||||
[FieldOption(Label="Not VIP", Hint = "Torrents not VIP")]
|
||||
NotVip = 5,
|
||||
}
|
||||
@@ -588,6 +634,8 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
public string Filetype { get; set; }
|
||||
public bool Vip { get; set; }
|
||||
public bool Free { get; set; }
|
||||
[JsonProperty(PropertyName = "personal_freeleech")]
|
||||
public bool PersonalFreeLeech { get; set; }
|
||||
[JsonProperty(PropertyName = "fl_vip")]
|
||||
public bool FreeVip { get; set; }
|
||||
public string Category { get; set; }
|
||||
|
||||
@@ -244,7 +244,9 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
|
||||
if (indexerResponse.HttpResponse.StatusCode != HttpStatusCode.OK)
|
||||
{
|
||||
throw new IndexerException(indexerResponse, "Unexpected response status '{0}' code from indexer request", indexerResponse.HttpResponse.StatusCode);
|
||||
STJson.TryDeserialize<JsonRpcResponse<NebulanceErrorResponse>>(indexerResponse.HttpResponse.Content, out var errorResponse);
|
||||
|
||||
throw new IndexerException(indexerResponse, "Unexpected response status '{0}' code from indexer request: {1}", indexerResponse.HttpResponse.StatusCode, errorResponse?.Result?.Error?.Message ?? "Check the logs for more information.");
|
||||
}
|
||||
|
||||
JsonRpcResponse<NebulanceResponse> jsonResponse;
|
||||
@@ -410,4 +412,14 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
|
||||
public IEnumerable<string> Tags { get; set; } = Array.Empty<string>();
|
||||
}
|
||||
|
||||
public class NebulanceErrorResponse
|
||||
{
|
||||
public NebulanceErrorMessage Error { get; set; }
|
||||
}
|
||||
|
||||
public class NebulanceErrorMessage
|
||||
{
|
||||
public string Message { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -289,12 +289,13 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
|
||||
var title = GetTitle(result, torrent);
|
||||
var infoUrl = GetInfoUrl(result.GroupId, id);
|
||||
var isFreeLeech = torrent.IsFreeLeech || torrent.IsNeutralLeech || torrent.IsPersonalFreeLeech;
|
||||
|
||||
var release = new TorrentInfo
|
||||
{
|
||||
Guid = infoUrl,
|
||||
InfoUrl = infoUrl,
|
||||
DownloadUrl = GetDownloadUrl(id, !torrent.IsFreeLeech && !torrent.IsNeutralLeech && !torrent.IsPersonalFreeLeech),
|
||||
DownloadUrl = GetDownloadUrl(id, torrent.CanUseToken && !isFreeLeech),
|
||||
Title = WebUtility.HtmlDecode(title),
|
||||
Artist = WebUtility.HtmlDecode(result.Artist),
|
||||
Album = WebUtility.HtmlDecode(result.GroupName),
|
||||
@@ -308,7 +309,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
Scene = torrent.Scene,
|
||||
Files = torrent.FileCount,
|
||||
Grabs = torrent.Snatches,
|
||||
DownloadVolumeFactor = torrent.IsFreeLeech || torrent.IsNeutralLeech || torrent.IsPersonalFreeLeech ? 0 : 1,
|
||||
DownloadVolumeFactor = isFreeLeech ? 0 : 1,
|
||||
UploadVolumeFactor = torrent.IsNeutralLeech ? 0 : 1
|
||||
};
|
||||
|
||||
@@ -337,12 +338,13 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
|
||||
var id = result.TorrentId;
|
||||
var infoUrl = GetInfoUrl(result.GroupId, id);
|
||||
var isFreeLeech = result.IsFreeLeech || result.IsNeutralLeech || result.IsPersonalFreeLeech;
|
||||
|
||||
var release = new TorrentInfo
|
||||
{
|
||||
Guid = infoUrl,
|
||||
InfoUrl = infoUrl,
|
||||
DownloadUrl = GetDownloadUrl(id, !result.IsFreeLeech && !result.IsNeutralLeech && !result.IsPersonalFreeLeech),
|
||||
DownloadUrl = GetDownloadUrl(id, result.CanUseToken && !isFreeLeech),
|
||||
Title = WebUtility.HtmlDecode(result.GroupName),
|
||||
Size = long.Parse(result.Size),
|
||||
Seeders = int.Parse(result.Seeders),
|
||||
@@ -350,7 +352,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
PublishDate = long.TryParse(result.GroupTime, out var num) ? DateTimeOffset.FromUnixTimeSeconds(num).UtcDateTime : DateTimeUtil.FromFuzzyTime(result.GroupTime),
|
||||
Files = result.FileCount,
|
||||
Grabs = result.Snatches,
|
||||
DownloadVolumeFactor = result.IsFreeLeech || result.IsNeutralLeech || result.IsPersonalFreeLeech ? 0 : 1,
|
||||
DownloadVolumeFactor = isFreeLeech ? 0 : 1,
|
||||
UploadVolumeFactor = result.IsNeutralLeech ? 0 : 1
|
||||
};
|
||||
|
||||
|
||||
@@ -263,12 +263,13 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
|
||||
var title = GetTitle(result, torrent);
|
||||
var infoUrl = GetInfoUrl(result.GroupId, id);
|
||||
var isFreeLeech = torrent.IsFreeLeech || torrent.IsNeutralLeech || torrent.IsFreeload || torrent.IsPersonalFreeLeech;
|
||||
|
||||
var release = new TorrentInfo
|
||||
{
|
||||
Guid = infoUrl,
|
||||
InfoUrl = infoUrl,
|
||||
DownloadUrl = GetDownloadUrl(id, !torrent.IsFreeLeech && !torrent.IsNeutralLeech && !torrent.IsFreeload && !torrent.IsPersonalFreeLeech),
|
||||
DownloadUrl = GetDownloadUrl(id, torrent.CanUseToken && !isFreeLeech),
|
||||
Title = WebUtility.HtmlDecode(title),
|
||||
Artist = WebUtility.HtmlDecode(result.Artist),
|
||||
Album = WebUtility.HtmlDecode(result.GroupName),
|
||||
@@ -282,7 +283,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
Scene = torrent.Scene,
|
||||
Files = torrent.FileCount,
|
||||
Grabs = torrent.Snatches,
|
||||
DownloadVolumeFactor = torrent.IsFreeLeech || torrent.IsNeutralLeech || torrent.IsFreeload || torrent.IsPersonalFreeLeech ? 0 : 1,
|
||||
DownloadVolumeFactor = isFreeLeech ? 0 : 1,
|
||||
UploadVolumeFactor = torrent.IsNeutralLeech || torrent.IsFreeload ? 0 : 1
|
||||
};
|
||||
|
||||
@@ -317,12 +318,13 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
|
||||
var id = result.TorrentId;
|
||||
var infoUrl = GetInfoUrl(result.GroupId, id);
|
||||
var isFreeLeech = result.IsFreeLeech || result.IsNeutralLeech || result.IsFreeload || result.IsPersonalFreeLeech;
|
||||
|
||||
var release = new TorrentInfo
|
||||
{
|
||||
Guid = infoUrl,
|
||||
InfoUrl = infoUrl,
|
||||
DownloadUrl = GetDownloadUrl(id, !result.IsFreeLeech && !result.IsNeutralLeech && !result.IsFreeload && !result.IsPersonalFreeLeech),
|
||||
DownloadUrl = GetDownloadUrl(id, result.CanUseToken && !isFreeLeech),
|
||||
Title = WebUtility.HtmlDecode(result.GroupName),
|
||||
Size = long.Parse(result.Size),
|
||||
Seeders = int.Parse(result.Seeders),
|
||||
@@ -330,7 +332,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
PublishDate = DateTimeOffset.FromUnixTimeSeconds(ParseUtil.CoerceLong(result.GroupTime)).UtcDateTime,
|
||||
Files = result.FileCount,
|
||||
Grabs = result.Snatches,
|
||||
DownloadVolumeFactor = result.IsFreeLeech || result.IsNeutralLeech || result.IsFreeload || result.IsPersonalFreeLeech ? 0 : 1,
|
||||
DownloadVolumeFactor = isFreeLeech ? 0 : 1,
|
||||
UploadVolumeFactor = result.IsNeutralLeech || result.IsFreeload ? 0 : 1
|
||||
};
|
||||
|
||||
|
||||
@@ -607,12 +607,7 @@ namespace NzbDrone.Core.Indexers
|
||||
|
||||
protected virtual bool CheckIfLoginNeeded(HttpResponse httpResponse)
|
||||
{
|
||||
if (httpResponse.StatusCode == HttpStatusCode.Unauthorized)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
return httpResponse.StatusCode == HttpStatusCode.Unauthorized;
|
||||
}
|
||||
|
||||
protected virtual Task DoLogin()
|
||||
|
||||
@@ -318,5 +318,24 @@ namespace NzbDrone.Core.Indexers
|
||||
|
||||
base.Update(definition);
|
||||
}
|
||||
|
||||
public override IEnumerable<IndexerDefinition> Update(IEnumerable<IndexerDefinition> definitions)
|
||||
{
|
||||
var indexerDefinitions = definitions.ToList();
|
||||
|
||||
foreach (var definition in indexerDefinitions)
|
||||
{
|
||||
var provider = _providers.First(v => v.GetType().Name == definition.Implementation);
|
||||
|
||||
SetProviderCharacteristics(provider, definition);
|
||||
|
||||
if (definition.Implementation == nameof(Cardigann))
|
||||
{
|
||||
MapCardigannDefinition(definition);
|
||||
}
|
||||
}
|
||||
|
||||
return base.Update(indexerDefinitions);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -771,5 +771,7 @@
|
||||
"AverageGrabs": "Promedio de capturas",
|
||||
"AllSearchResultsHiddenByFilter": "Todos los resultados están ocultos por el filtro aplicado.",
|
||||
"PackageVersionInfo": "{packageVersion} por {packageAuthor}",
|
||||
"HealthMessagesInfoBox": "Puede encontrar más información sobre la causa de estos mensajes de comprobación de salud haciendo clic en el enlace wiki (icono de libro) al final de la fila, o comprobando sus [logs]({link}). Si tienes dificultades para interpretar estos mensajes, puedes ponerte en contacto con nuestro servicio de asistencia en los enlaces que aparecen a continuación."
|
||||
"HealthMessagesInfoBox": "Puede encontrar más información sobre la causa de estos mensajes de comprobación de salud haciendo clic en el enlace wiki (icono de libro) al final de la fila, o comprobando sus [logs]({link}). Si tienes dificultades para interpretar estos mensajes, puedes ponerte en contacto con nuestro servicio de asistencia en los enlaces que aparecen a continuación.",
|
||||
"LogSizeLimit": "Límite de tamaño de registro",
|
||||
"LogSizeLimitHelpText": "Máximo tamaño de archivo de registro en MB antes de archivarlo. Predeterminado es 1MB."
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
"Proxy": "Proxy",
|
||||
"Protocol": "Protocole",
|
||||
"Options": "Options",
|
||||
"NoChanges": "Aucuns changements",
|
||||
"NoChanges": "Aucun changement",
|
||||
"NoChange": "Pas de changement",
|
||||
"MoreInfo": "Plus d'informations",
|
||||
"Grabbed": "Saisie",
|
||||
@@ -509,7 +509,7 @@
|
||||
"DeleteSelectedDownloadClients": "Supprimer le(s) client(s) de téléchargement",
|
||||
"DeleteSelectedDownloadClientsMessageText": "Voulez-vous vraiment supprimer {count} client(s) de téléchargement sélectionné(s) ?",
|
||||
"StopSelecting": "Effacer la sélection",
|
||||
"UpdateAvailableHealthCheckMessage": "Une nouvelle mise à jour est disponible",
|
||||
"UpdateAvailableHealthCheckMessage": "Une nouvelle mise à jour est disponible : {version}",
|
||||
"AdvancedSettingsHiddenClickToShow": "Paramètres avancés masqués, cliquez pour afficher",
|
||||
"AdvancedSettingsShownClickToHide": "Paramètres avancés affichés, cliquez pour masquer",
|
||||
"AppsMinimumSeeders": "Apps avec le nombre minimum de seeders disponibles",
|
||||
@@ -644,7 +644,7 @@
|
||||
"IndexerSettingsQueryLimitHelpText": "Le nombre de requêtes maximales tel que spécifié par l'unité respective que {appName} autorisera au site",
|
||||
"IndexerSettingsRssKey": "Clé RSS",
|
||||
"IndexerSettingsSeedRatioHelpText": "Le ratio qu'un torrent doit atteindre avant de s'arrêter, vide utilise la valeur par défaut du client de téléchargement. Le ratio doit être d'au moins 1.0 et suivre les règles des indexeurs",
|
||||
"IndexerSettingsSeedTime": "Temps d'envoie",
|
||||
"IndexerSettingsSeedTime": "Temps d'envoi",
|
||||
"IndexerTorrentSyndikatSettingsApiKeyHelpText": "Clé API du site",
|
||||
"BlackholeFolderHelpText": "Dossier dans lequel {appName} stockera le fichier {extension}",
|
||||
"DefaultCategory": "Catégorie par défaut",
|
||||
@@ -692,7 +692,7 @@
|
||||
"IndexerSettingsCookie": "Cookie",
|
||||
"IndexerSettingsCookieHelpText": "Cookie du site",
|
||||
"IndexerSettingsSeedTimeHelpText": "Durée pendant laquelle un torrent doit être envoyé avant de s'arrêter, vide utilise la valeur par défaut du client de téléchargement",
|
||||
"IndexerSettingsSeedRatio": "Ratio d'envoie",
|
||||
"IndexerSettingsSeedRatio": "Ratio d'envoi",
|
||||
"IndexerSettingsVipExpiration": "Expiration de la carte VIP",
|
||||
"SecretToken": "Jeton secret",
|
||||
"TorrentBlackholeSaveMagnetFiles": "Enregistrer les fichiers magnétiques",
|
||||
@@ -771,5 +771,7 @@
|
||||
"AverageGrabs": "Prises moyennes",
|
||||
"AverageQueries": "Requêtes moyennes",
|
||||
"PackageVersionInfo": "{packageVersion} par {packageAuthor}",
|
||||
"HealthMessagesInfoBox": "Vous pouvez trouver plus d'informations sur la cause de ces messages de contrôle de santé en cliquant sur le lien wiki (icône de livre) à la fin de la ligne, ou en vérifiant vos [journaux]({link}). Si vous rencontrez des difficultés pour interpréter ces messages, vous pouvez contacter notre support, via les liens ci-dessous."
|
||||
"HealthMessagesInfoBox": "Vous pouvez trouver plus d'informations sur la cause de ces messages de contrôle de santé en cliquant sur le lien wiki (icône de livre) à la fin de la ligne, ou en vérifiant vos [journaux]({link}). Si vous rencontrez des difficultés pour interpréter ces messages, vous pouvez contacter notre support, via les liens ci-dessous.",
|
||||
"LogSizeLimit": "Limite de taille du journal",
|
||||
"LogSizeLimitHelpText": "Taille maximale du fichier journal en Mo avant archivage. La valeur par défaut est de 1 Mo."
|
||||
}
|
||||
|
||||
@@ -144,7 +144,7 @@
|
||||
"ConnectionLost": "Connessione Persa",
|
||||
"Component": "Componente",
|
||||
"Columns": "Colonne",
|
||||
"DeleteBackupMessageText": "Sei sicuro di voler cancellare il backup '{name}'?",
|
||||
"DeleteBackupMessageText": "Sei sicuro di voler eliminare il backup '{name}'?",
|
||||
"CancelPendingTask": "Sei sicuro di voler cancellare questa operazione in sospeso?",
|
||||
"BranchUpdateMechanism": "Ramo utilizzato dal sistema di aggiornamento esterno",
|
||||
"BranchUpdate": "Branca da usare per aggiornare {appName}",
|
||||
|
||||
@@ -498,5 +498,6 @@
|
||||
"Script": "Script",
|
||||
"PublishedDate": "Publicatie Datum",
|
||||
"Redirected": "Omleiden",
|
||||
"AllSearchResultsHiddenByFilter": "Alle resultaten zijn verborgen door het toegepaste filter"
|
||||
"AllSearchResultsHiddenByFilter": "Alle resultaten zijn verborgen door het toegepaste filter",
|
||||
"Clone": "Kloon"
|
||||
}
|
||||
|
||||
@@ -109,7 +109,7 @@
|
||||
"AuthenticationMethodHelpText": "Wymagaj nazwy użytkownika i hasła, aby uzyskać dostęp do {appName}",
|
||||
"BackupFolderHelpText": "Względne ścieżki będą znajdować się w katalogu AppData {appName}",
|
||||
"BackupRetentionHelpText": "Automatyczne kopie zapasowe starsze niż okres przechowywania zostaną automatycznie wyczyszczone",
|
||||
"BindAddressHelpText": "Prawidłowy adres IP4 lub „*” dla wszystkich interfejsów",
|
||||
"BindAddressHelpText": "Prawidłowy adres IP, localhost lub '*' dla wszystkich interfejsów",
|
||||
"BranchUpdateMechanism": "Gałąź używana przez zewnętrzny mechanizm aktualizacji",
|
||||
"BypassProxyForLocalAddresses": "Pomijaj serwer proxy dla adresów lokalnych",
|
||||
"CancelPendingTask": "Czy na pewno chcesz anulować to oczekujące zadanie?",
|
||||
@@ -124,9 +124,9 @@
|
||||
"DatabaseMigration": "Migracja bazy danych",
|
||||
"DeleteApplicationMessageText": "Czy na pewno chcesz usunąć powiadomienie „{0}”?",
|
||||
"DeleteBackup": "Usuń kopię zapasową",
|
||||
"DeleteBackupMessageText": "Czy na pewno chcesz usunąć kopię zapasową „{0}”?",
|
||||
"DeleteBackupMessageText": "Czy na pewno chcesz usunąć kopię zapasową „{name}”?",
|
||||
"DeleteDownloadClient": "Usuń klienta pobierania",
|
||||
"DeleteDownloadClientMessageText": "Czy na pewno chcesz usunąć klienta pobierania „{0}”?",
|
||||
"DeleteDownloadClientMessageText": "Czy na pewno chcesz usunąć klienta pobierania „{name}”?",
|
||||
"DeleteNotification": "Usuń powiadomienie",
|
||||
"Disabled": "Wyłączone",
|
||||
"Docker": "Doker",
|
||||
@@ -237,7 +237,7 @@
|
||||
"SSLCertPathHelpText": "Ścieżka do pliku pfx",
|
||||
"SSLPort": "Port SSL",
|
||||
"StartTypingOrSelectAPathBelow": "Zacznij pisać lub wybierz ścieżkę poniżej",
|
||||
"StartupDirectory": "Katalog startowy",
|
||||
"StartupDirectory": "Katalog Startowy",
|
||||
"Status": "Status",
|
||||
"SuggestTranslationChange": "Zaproponuj zmianę tłumaczenia",
|
||||
"SystemTimeCheckMessage": "Czas systemowy jest wyłączony o więcej niż 1 dzień. Zaplanowane zadania mogą nie działać poprawnie, dopóki czas nie zostanie skorygowany",
|
||||
@@ -382,7 +382,7 @@
|
||||
"EditDownloadClientImplementation": "Dodaj klienta pobierania - {implementationName}",
|
||||
"Id": "Identyfikator",
|
||||
"AddApplicationImplementation": "Dodaj Connection - {implementationName}",
|
||||
"AddIndexerImplementation": "Dodaj condition - {implementationName}",
|
||||
"AddIndexerImplementation": "Dodaj indeks - {implementationName}",
|
||||
"AddIndexerProxyImplementation": "Dodaj condition - {implementationName}",
|
||||
"EditConnectionImplementation": "Dodaj Connection - {implementationName}",
|
||||
"EditApplicationImplementation": "Dodaj Connection - {implementationName}",
|
||||
@@ -397,5 +397,23 @@
|
||||
"Script": "Scenariusz",
|
||||
"BuiltIn": "Wbudowany",
|
||||
"PublishedDate": "Data publikacji",
|
||||
"AllSearchResultsHiddenByFilter": "Wszystkie wyniki są ukrywane przez zastosowany filtr"
|
||||
"AllSearchResultsHiddenByFilter": "Wszystkie wyniki są ukrywane przez zastosowany filtr",
|
||||
"AppUpdated": "{appName} Zaktualizowany",
|
||||
"AppUpdatedVersion": "{appName} został zaktualizowany do wersji `{version}`, by uzyskać nowe zmiany należy przeładować {appName}",
|
||||
"AddCustomFilter": "Dodaj niestandardowy filtr",
|
||||
"AuthenticationMethodHelpTextWarning": "Wybierz prawidłową metodę autoryzacji",
|
||||
"Any": "Dowolny",
|
||||
"AuthenticationMethod": "Metoda Autoryzacji",
|
||||
"AuthenticationRequired": "Wymagana Autoryzacja",
|
||||
"Categories": "Kategorie",
|
||||
"Label": "Etykieta",
|
||||
"Notification": "Powiadomienia",
|
||||
"Season": "Sezon",
|
||||
"Theme": "Motyw",
|
||||
"Artist": "artysta",
|
||||
"Album": "album",
|
||||
"Connect": "Powiadomienia",
|
||||
"Episode": "odcinek",
|
||||
"Notifications": "Powiadomienia",
|
||||
"Publisher": "Wydawca"
|
||||
}
|
||||
|
||||
@@ -771,5 +771,7 @@
|
||||
"AverageGrabs": "Média de Capturas",
|
||||
"AllSearchResultsHiddenByFilter": "Todos os resultados da pesquisa são ocultados pelo filtro aplicado.",
|
||||
"PackageVersionInfo": "{packageVersion} por {packageAuthor}",
|
||||
"HealthMessagesInfoBox": "Para saber mais sobre a causa dessas mensagens de verificação de integridade, clique no link da wiki (ícone de livro) no final da linha ou verifique os [logs]({link}). Se tiver dificuldade em interpretar essas mensagens, entre em contato com nosso suporte nos links abaixo."
|
||||
"HealthMessagesInfoBox": "Para saber mais sobre a causa dessas mensagens de verificação de integridade, clique no link da wiki (ícone de livro) no final da linha ou verifique os [logs]({link}). Se tiver dificuldade em interpretar essas mensagens, entre em contato com nosso suporte nos links abaixo.",
|
||||
"LogSizeLimit": "Limite de Tamanho do Registro",
|
||||
"LogSizeLimitHelpText": "Tamanho máximo do arquivo de registro em MB antes do arquivamento. O padrão é 1 MB."
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@
|
||||
"Mechanism": "Механизм",
|
||||
"NoBackupsAreAvailable": "Нет резервных копий",
|
||||
"DeleteDownloadClientMessageText": "Вы уверены, что хотите удалить клиент загрузки '{name}'?",
|
||||
"Edit": "Редактирование",
|
||||
"Edit": "Изменить",
|
||||
"Enable": "Включить",
|
||||
"Indexer": "Индексатор",
|
||||
"SettingsShortDateFormat": "Короткий формат даты",
|
||||
@@ -372,7 +372,7 @@
|
||||
"ApplyTagsHelpTextHowToApplyIndexers": "Как применить теги к выбранным индексаторам",
|
||||
"ApplyTagsHelpTextReplace": "Заменить: заменить теги введенными тегами (оставьте поле пустым, чтобы удалить все теги)",
|
||||
"Track": "След",
|
||||
"UpdateAvailableHealthCheckMessage": "Доступно новое обновление",
|
||||
"UpdateAvailableHealthCheckMessage": "Доступно новое обновление: {version}",
|
||||
"More": "Более",
|
||||
"Publisher": "Издатель",
|
||||
"ConnectionLostReconnect": "{appName} попытается соединиться автоматически или нажмите кнопку внизу.",
|
||||
@@ -531,5 +531,7 @@
|
||||
"Script": "Скрипт",
|
||||
"InfoUrl": "URL-адрес информации",
|
||||
"PublishedDate": "Дата публикации",
|
||||
"AllSearchResultsHiddenByFilter": "Все результаты скрыты фильтром"
|
||||
"AllSearchResultsHiddenByFilter": "Все результаты скрыты фильтром",
|
||||
"HealthMessagesInfoBox": "Дополнительную информацию о причине появления этих сообщений о проверке работоспособности можно найти, щелкнув вики-ссылку (значок книги) в конце строки или проверив свои [журналы]({link}). Если у вас возникли трудности с пониманием этих сообщений, вы можете обратиться в нашу службу поддержки по ссылкам ниже.",
|
||||
"PackageVersionInfo": "{packageVersion} от {packageAuthor}"
|
||||
}
|
||||
|
||||
@@ -518,7 +518,7 @@
|
||||
"OnApplicationUpdateHelpText": "Uygulama Güncellemesinde",
|
||||
"DeleteSelectedApplicationsMessageText": "Seçilen {count} içe aktarma listesini silmek istediğinizden emin misiniz?",
|
||||
"ProxyValidationBadRequest": "Proxy ile test edilemedi. DurumKodu: {statusCode}",
|
||||
"UpdateAvailableHealthCheckMessage": "Yeni güncelleme mevcut",
|
||||
"UpdateAvailableHealthCheckMessage": "Yeni güncelleme mevcut: {version}",
|
||||
"days": "gün",
|
||||
"Default": "Varsayılan",
|
||||
"GrabRelease": "Yayın Yakalama",
|
||||
@@ -534,5 +534,7 @@
|
||||
"Any": "Herhangi",
|
||||
"AllSearchResultsHiddenByFilter": "Tüm sonuçlar, uygulanan filtre tarafından gizlenir",
|
||||
"HealthMessagesInfoBox": "Satırın sonundaki wiki bağlantısını (kitap simgesi) tıklayarak veya [günlüklerinizi]({link}) kontrol ederek bu durum kontrolü mesajlarının nedeni hakkında daha fazla bilgi bulabilirsiniz. Bu mesajları yorumlamakta zorluk yaşıyorsanız aşağıdaki bağlantılardan destek ekibimize ulaşabilirsiniz.",
|
||||
"PackageVersionInfo": "{packageAuthor} tarafından {packageVersion}"
|
||||
"PackageVersionInfo": "{packageAuthor} tarafından {packageVersion}",
|
||||
"LogSizeLimit": "Log Boyutu Sınırı",
|
||||
"LogSizeLimitHelpText": "Arşivlemeden önce MB cinsinden maksimum log dosya boyutu. Varsayılan 1 MB'tır."
|
||||
}
|
||||
|
||||
@@ -21,10 +21,10 @@
|
||||
"AllIndexersHiddenDueToFilter": "由于应用了筛选器,所有索引器都被隐藏。",
|
||||
"Analytics": "分析",
|
||||
"AnalyticsEnabledHelpText": "将匿名使用情况和错误信息发送到{appName}的服务器。这包括有关您的浏览器的信息、您使用的{appName} WebUI页面、错误报告以及操作系统和运行时版本。我们将使用此信息来确定功能和错误修复的优先级。",
|
||||
"ApiKey": "接口密钥 (API Key)",
|
||||
"ApiKeyValidationHealthCheckMessage": "请将API密钥更新为至少{length}个字符长。您可以通过设置或配置文件执行此操作",
|
||||
"AppDataDirectory": "AppData目录",
|
||||
"AppDataLocationHealthCheckMessage": "正在更新期间的 AppData 不会被更新删除",
|
||||
"ApiKey": "API 密钥",
|
||||
"ApiKeyValidationHealthCheckMessage": "请将API密钥更新为至少 {length} 个字符长。您可以通过设置或配置文件完成此操作",
|
||||
"AppDataDirectory": "AppData 目录",
|
||||
"AppDataLocationHealthCheckMessage": "更新时无法阻止删除 AppData",
|
||||
"AppProfileInUse": "正在使用的应用程序配置文件",
|
||||
"AppProfileSelectHelpText": "应用程序配置用于控制应用程序同步设置 RSS、自动搜索和交互式搜索设置",
|
||||
"AppSettingsSummary": "配置{appName}与PVR程序交互方式的应用和设置",
|
||||
@@ -34,7 +34,7 @@
|
||||
"ApplicationStatusCheckAllClientMessage": "由于故障所用应用程序都不可用",
|
||||
"ApplicationStatusCheckSingleClientMessage": "由于故障应用程序不可用",
|
||||
"ApplicationURL": "应用程序 URL",
|
||||
"ApplicationUrlHelpText": "此应用的外部URL,包含 http(s)://、端口和基本URL",
|
||||
"ApplicationUrlHelpText": "此应用的外部 URL,包含 http(s)://、端口和基本 URL",
|
||||
"Applications": "程序",
|
||||
"Apply": "应用",
|
||||
"ApplyTags": "应用标签",
|
||||
@@ -45,9 +45,9 @@
|
||||
"Auth": "认证",
|
||||
"Authentication": "认证",
|
||||
"AuthenticationMethodHelpText": "需要用户名和密码以访问 {appName}",
|
||||
"AuthenticationRequired": "需要身份验证",
|
||||
"AuthenticationRequiredHelpText": "修改哪些请求需要认证。除非你了解其中的风险,否则不要更改。",
|
||||
"AuthenticationRequiredWarning": "为了防止未经身份验证的远程访问,{appName} 现在需要启用身份验证。您可以禁用本地地址的身份验证。",
|
||||
"AuthenticationRequired": "需要认证",
|
||||
"AuthenticationRequiredHelpText": "修改这些请求需要认证。除非你了解其中的风险,否则不要进行更改。",
|
||||
"AuthenticationRequiredWarning": "为防止未经认证的远程访问,{appName} 现需要启用身份认证。您可以选择禁用本地地址的身份认证。",
|
||||
"Author": "作者",
|
||||
"Automatic": "自动化",
|
||||
"AutomaticSearch": "自动搜索",
|
||||
@@ -194,7 +194,7 @@
|
||||
"IndexerDetails": "索引器详细信息",
|
||||
"IndexerDisabled": "索引器已被禁用",
|
||||
"IndexerFailureRate": "Indexer失败率",
|
||||
"IndexerFlags": "搜刮器标记",
|
||||
"IndexerFlags": "索引器标志",
|
||||
"IndexerHealthCheckNoIndexers": "未启用任何搜刮器,{appName}将不会返回搜索结果",
|
||||
"IndexerInfo": "索引器信息",
|
||||
"IndexerLongTermStatusAllUnavailableHealthCheckMessage": "由于故障超过6小时,所有搜刮器均不可用",
|
||||
@@ -322,7 +322,7 @@
|
||||
"Queue": "队列",
|
||||
"Queued": "队列中",
|
||||
"Rss": "RSS",
|
||||
"RssIsNotSupportedWithThisIndexer": "该搜刮器不支持RSS",
|
||||
"RssIsNotSupportedWithThisIndexer": "该索引器不支持 RSS",
|
||||
"RawSearchSupported": "支持原始搜索",
|
||||
"ReadTheWikiForMoreInformation": "查阅Wiki获得更多信息",
|
||||
"Reddit": "Reddit",
|
||||
@@ -425,7 +425,7 @@
|
||||
"TestAll": "测试全部",
|
||||
"TestAllApps": "测试全部应用",
|
||||
"TestAllClients": "测试全部客户端",
|
||||
"TestAllIndexers": "测试全部搜刮器",
|
||||
"TestAllIndexers": "测试全部索引器",
|
||||
"TheLatestVersionIsAlreadyInstalled": "已安装最新版本的{appName}",
|
||||
"Theme": "主题",
|
||||
"ThemeHelpText": "更改应用程序UI主题,“自动”主题将使用您的操作系统主题设置亮或暗模式。灵感来源于{inspirredby}。",
|
||||
@@ -484,7 +484,7 @@
|
||||
"Warn": "警告",
|
||||
"Website": "网站",
|
||||
"Wiki": "Wiki",
|
||||
"Year": "年",
|
||||
"Year": "年份",
|
||||
"Yes": "确定",
|
||||
"YesCancel": "确定,取消",
|
||||
"Yesterday": "昨天",
|
||||
@@ -493,9 +493,9 @@
|
||||
"ApplyChanges": "应用更改",
|
||||
"ApplyTagsHelpTextAdd": "添加: 添加标签至已有的标签列表中",
|
||||
"CountDownloadClientsSelected": "已选择 {count} 个下载客户端",
|
||||
"CountIndexersSelected": "已选择 {count} 个索引器",
|
||||
"CountIndexersSelected": "选定 {count} 个索引器",
|
||||
"DeleteSelectedDownloadClientsMessageText": "您确定要删除 {count} 个选定的下载客户端吗?",
|
||||
"DeleteSelectedIndexersMessageText": "您确定要删除{count}选定的索引器吗?",
|
||||
"DeleteSelectedIndexersMessageText": "您确定要删除选定的 {count} 个索引器吗?",
|
||||
"EditSelectedDownloadClients": "编辑选定的下载客户端",
|
||||
"Implementation": "执行",
|
||||
"ManageDownloadClients": "管理下载客户端",
|
||||
@@ -503,12 +503,12 @@
|
||||
"NoIndexersFound": "未找到索引器",
|
||||
"SelectIndexers": "搜刮器搜索",
|
||||
"ApplyTagsHelpTextHowToApplyApplications": "如何给选中的电影添加标签",
|
||||
"ApplyTagsHelpTextHowToApplyIndexers": "如何将标签应用到已选择的索引器",
|
||||
"ApplyTagsHelpTextHowToApplyIndexers": "如何将标签应用到已选中的索引器",
|
||||
"ApplyTagsHelpTextReplace": "替换: 用输入的标签替换当前标签 (不输入将会清除所有标签)",
|
||||
"DeleteSelectedApplicationsMessageText": "您确定要删除{count}选定的应用程序吗?",
|
||||
"DeleteSelectedDownloadClients": "删除下载客户端",
|
||||
"DownloadClientPriorityHelpText": "优先考虑多个下载客户端,循环查询用于具有相同优先级的客户端。",
|
||||
"EditSelectedIndexers": "编辑选定的索引器",
|
||||
"EditSelectedIndexers": "编辑选定索引器",
|
||||
"OnHealthRestored": "健康度恢复",
|
||||
"OnHealthRestoredHelpText": "健康度恢复",
|
||||
"ApplyTagsHelpTextRemove": "移除: 移除已输入的标签",
|
||||
@@ -559,11 +559,11 @@
|
||||
"AddConnectionImplementation": "添加连接- {implementationName}",
|
||||
"AddDownloadClientImplementation": "添加下载客户端- {implementationName}",
|
||||
"AddIndexerProxyImplementation": "添加搜刮器代理-{实体名称}",
|
||||
"AppUpdated": "{appName} 升级",
|
||||
"AppUpdated": "{appName} 已升级",
|
||||
"EditDownloadClientImplementation": "编辑下载客户端- {implementationName}",
|
||||
"EditIndexerImplementation": "编辑索引器- {implementationName}",
|
||||
"EditIndexerProxyImplementation": "添加搜刮器代理-{实体名称}",
|
||||
"AppUpdatedVersion": "{appName} 已经更新到 {version} 版本,重新加载 {appName} 使更新生效",
|
||||
"AppUpdatedVersion": "{appName} 已经更新到版本 {version} ,重新加载 {appName} 使更新生效",
|
||||
"EditApplicationImplementation": "添加应用-{实体名称}",
|
||||
"EditConnectionImplementation": "编辑连接- {implementationName}",
|
||||
"NotificationStatusAllClientHealthCheckMessage": "由于故障所用应用程序都不可用",
|
||||
@@ -571,7 +571,7 @@
|
||||
"AuthBasic": "基础(浏览器弹出对话框)",
|
||||
"AuthForm": "表单(登陆页面)",
|
||||
"AuthenticationMethod": "认证方式",
|
||||
"AuthenticationMethodHelpTextWarning": "请选择一个有效的身份验证方式",
|
||||
"AuthenticationMethodHelpTextWarning": "请选择一个有效的认证方式",
|
||||
"AuthenticationRequiredPasswordHelpTextWarning": "请输入新密码",
|
||||
"AuthenticationRequiredUsernameHelpTextWarning": "请输入新用户名",
|
||||
"Clone": "复制",
|
||||
@@ -580,7 +580,7 @@
|
||||
"External": "外部的",
|
||||
"None": "无",
|
||||
"ResetAPIKeyMessageText": "您确定要重置您的 API 密钥吗?",
|
||||
"IndexerDownloadClientHealthCheckMessage": "有无效下载客户端的索引器:{indexerNames}。",
|
||||
"IndexerDownloadClientHealthCheckMessage": "使用无效下载客户端的索引器:{indexerNames}。",
|
||||
"ApplicationTagsHelpTextWarning": "标签应该谨慎使用,它们可能会产生意想不到的效果。带有标签的应用程序只会与具有相同标签的索引器同步。",
|
||||
"EditCategory": "编辑分类",
|
||||
"IndexerHistoryLoadError": "加载索引器历史记录出错",
|
||||
@@ -603,7 +603,7 @@
|
||||
"NoIndexerCategories": "没有找到此索引器的分类",
|
||||
"DownloadClientQbittorrentSettingsContentLayout": "内容布局",
|
||||
"DownloadClientQbittorrentSettingsContentLayoutHelpText": "是否使用 qBittorrent 配置的内容布局,使用种子的原始布局或始终创建子文件夹(qBittorrent 4.3.2+)",
|
||||
"DownloadClientAriaSettingsDirectoryHelpText": "可选的下载位置,留空使用 Aria2 默认位置",
|
||||
"DownloadClientAriaSettingsDirectoryHelpText": "下载位置可选择,留空使用 Aria2 默认位置",
|
||||
"ManageClients": "管理客户端",
|
||||
"CustomFilter": "自定义过滤器",
|
||||
"BlackholeFolderHelpText": "{appName} 将在其中存储 {extension} 文件的文件夹",
|
||||
@@ -616,7 +616,7 @@
|
||||
"DownloadClientFloodSettingsAdditionalTags": "附加标签",
|
||||
"DownloadClientFreeboxSettingsApiUrl": "API 地址",
|
||||
"DownloadClientFreeboxSettingsAppId": "App ID",
|
||||
"DownloadClientDelugeSettingsUrlBaseHelpText": "向 deluge json url 添加前缀,请参阅 {url}",
|
||||
"DownloadClientDelugeSettingsUrlBaseHelpText": "向 Deluge JSON URL 添加前缀,请参阅 {url}",
|
||||
"DownloadClientFloodSettingsUrlBaseHelpText": "为 Flood API 添加前缀,例如 {url}",
|
||||
"DownloadClientFreeboxSettingsAppToken": "App Token",
|
||||
"DownloadClientFreeboxSettingsAppTokenHelpText": "创建访问 Freebox API 所需的 App token(即“ app_token”)",
|
||||
@@ -639,7 +639,7 @@
|
||||
"SecretToken": "密钥令牌",
|
||||
"XmlRpcPath": "XML RPC 路径",
|
||||
"ApplicationSettingsSyncRejectBlocklistedTorrentHashesHelpText": "如果 torrent 的哈希被屏蔽了,某些索引器在使用RSS或者搜索期间可能无法正确拒绝它,启用此功能将允许在抓取 torrent 之后但在将其发送到客户端之前拒绝它。",
|
||||
"DownloadClientDownloadStationSettingsDirectoryHelpText": "用于存放下载内容的可选共享文件夹,留空以使用默认的 Download Station 位置",
|
||||
"DownloadClientDownloadStationSettingsDirectoryHelpText": "用于存放下载内容的共享文件夹可选择,留空使用默认的 Download Station 位置",
|
||||
"DownloadClientFloodSettingsAdditionalTagsHelpText": "添加媒体属性作为标签。 提示是示例。",
|
||||
"DownloadClientFreeboxSettingsApiUrlHelpText": "使用 API 版本定义 Freebox API 基本 URL,例如“{url}”,默认为“{defaultApiUrl}”",
|
||||
"DownloadClientPneumaticSettingsStrmFolder": "Strm 文件夹",
|
||||
@@ -680,10 +680,10 @@
|
||||
"IndexerSettingsCookie": "Cookie",
|
||||
"ProxyValidationBadRequest": "测试代理失败。状态码:{statusCode}",
|
||||
"ProxyValidationUnableToConnect": "无法连接到索引器:{exceptionMessage}。 检查有关此错误的日志以了解详细信息",
|
||||
"DownloadClientFloodSettingsTagsHelpText": "下载的初始标签。 要被识别,下载必须具有所有初始标签。 这可以避免与不相关的下载发生冲突。",
|
||||
"DownloadClientFloodSettingsTagsHelpText": "下载的初始标签。下载必须具有所有初始标签才可被识别。 这可以避免与不相关的下载发生冲突。",
|
||||
"DownloadClientPneumaticSettingsStrmFolderHelpText": "该文件夹中的 .strm 文件将由 drone 导入",
|
||||
"IndexerHDBitsSettingsCodecsHelpText": "如果未指定,则使用所有选项。",
|
||||
"IndexerSettingsSeedRatioHelpText": "种子在停止之前应达到的比率,留空使用下载客户端的默认值。 比率应至少为 1.0 并遵循索引器规则",
|
||||
"IndexerSettingsSeedRatioHelpText": "停止之前应达到的做种比率,留空使用下载客户端的默认值。 比率应至少为 1.0 并遵循索引器规则",
|
||||
"IndexerSettingsSeedTimeHelpText": "停止前应做种的时间,留空使用下载客户端的默认值",
|
||||
"TorrentBlackholeSaveMagnetFiles": "保存磁力链接文件",
|
||||
"TorrentBlackholeTorrentFolder": "种子文件夹",
|
||||
@@ -697,5 +697,8 @@
|
||||
"Redirected": "重定向",
|
||||
"AllSearchResultsHiddenByFilter": "根据过滤条件所有结果已隐藏",
|
||||
"HealthMessagesInfoBox": "您可以通过单击行尾的wiki链接(图书图标)或检查[日志]({link})来查找有关这些运行状况检查消息原因的更多信息。如果你在理解这些信息方面有困难,你可以通过下面的链接联系我们的支持。",
|
||||
"PackageVersionInfo": "{packageVersion} 由 {packageAuthor} 制作"
|
||||
"PackageVersionInfo": "{packageVersion} 由 {packageAuthor} 制作",
|
||||
"LabelIsRequired": "需要标签",
|
||||
"LogSizeLimit": "日志大小限制",
|
||||
"LogSizeLimitHelpText": "存档前的最大日志文件大小(MB)。默认值为 1 MB。"
|
||||
}
|
||||
|
||||
@@ -127,10 +127,12 @@ namespace NzbDrone.Core.ThingiProvider
|
||||
|
||||
public virtual IEnumerable<TProviderDefinition> Update(IEnumerable<TProviderDefinition> definitions)
|
||||
{
|
||||
_providerRepository.UpdateMany(definitions.ToList());
|
||||
_eventAggregator.PublishEvent(new ProviderBulkUpdatedEvent<TProvider>(definitions));
|
||||
var providerDefinitions = definitions.ToList();
|
||||
|
||||
return definitions;
|
||||
_providerRepository.UpdateMany(providerDefinitions);
|
||||
_eventAggregator.PublishEvent(new ProviderBulkUpdatedEvent<TProvider>(providerDefinitions));
|
||||
|
||||
return providerDefinitions;
|
||||
}
|
||||
|
||||
public void Delete(int id)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using FluentValidation;
|
||||
using NzbDrone.Core.Indexers;
|
||||
using NzbDrone.Core.Validation;
|
||||
using Prowlarr.Http;
|
||||
@@ -14,9 +15,10 @@ namespace Prowlarr.Api.V1.Indexers
|
||||
DownloadClientExistsValidator downloadClientExistsValidator)
|
||||
: base(indexerFactory, "indexer", resourceMapper, bulkResourceMapper)
|
||||
{
|
||||
Http.Validation.RuleBuilderExtensions.ValidId(SharedValidator.RuleFor(s => s.AppProfileId));
|
||||
SharedValidator.RuleFor(c => c.AppProfileId).Cascade(CascadeMode.Stop)
|
||||
.ValidId()
|
||||
.SetValidator(appProfileExistsValidator);
|
||||
|
||||
SharedValidator.RuleFor(c => c.AppProfileId).SetValidator(appProfileExistsValidator);
|
||||
SharedValidator.RuleFor(c => c.DownloadClientId).SetValidator(downloadClientExistsValidator);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user