1
0
mirror of https://github.com/Sonarr/Sonarr.git synced 2026-04-18 21:35:27 -04:00

Compare commits

...

32 Commits

Author SHA1 Message Date
Mark McDowall 97e85a908d Bump version to 4.0.17 2026-03-16 13:49:10 -07:00
Mark McDowall 6d91c3b62e Bump ImageSharp to 3.1.12 2026-03-16 13:49:09 -07:00
Mark McDowall f30207c3d1 Improve HTTP file mappers 2026-03-16 13:48:38 -07:00
Stevie Robinson 028d2414e7 Fixed: Plexmatch special episode numbers
Closes #8270
2025-12-22 12:28:23 -08:00
Stevie Robinson cbd7df2c91 Fixed: Multiple XML declarations in kodi/xmbc episodes metadata
Closes #8242
2025-12-22 12:14:12 -08:00
Mark McDowall 52972e7efc Add private IPv6 networks 2025-11-03 07:35:36 -08:00
Mark McDowall 8c50919499 Bump version to 4.0.16 2025-10-28 15:53:41 -07:00
Polgonite fdc07a47b1 Fixed: qBittorrent /login API success check 2025-10-26 08:31:50 -07:00
Collin Heist 36225c3709 Fixed: Prevent modals from overflowing screen width
Closes #8085
2025-10-26 08:31:50 -07:00
康小广 bc037ae356 Follow redirects when fetching Custom Lists 2025-10-26 08:31:50 -07:00
Mark McDowall 77a335de30 Fixed: Default runtime to 45 minutes if unavailable when importing episode files
Closes #7780
2025-10-26 08:31:50 -07:00
Mark McDowall 88d56361c4 Add XML declaration and clean up Kodi metadata generation
Closes #7753
2025-10-26 08:31:50 -07:00
Mark McDowall d10107739b Set known networks to RFC 1918 ranges during startup 2025-10-25 19:18:43 -07:00
Mark McDowall 7db7567c8e Bump version to 4.0.15 2025-06-09 17:19:54 -07:00
Michael Peleshenko 2b2b973b30 Fixed: Prevent series without IMDB ID from being removed erroneously 2025-06-09 17:19:10 -07:00
Mark McDowall bb954a7424 Fixed: Trakt Import List authentication after 24 hours
Closes #7874
2025-06-09 17:18:54 -07:00
Mark McDowall 640e3e5d44 Bump version to 4.0.14 2025-03-15 09:43:34 -07:00
Mark McDowall 1260d3c800 Upgrade ImageSharp 2025-03-15 09:29:03 -07:00
v3DJG6GL feeed9a7cf New: .arj and .lzh extensions are potentially dangerous 2025-03-15 09:25:40 -07:00
Mark McDowall c8cb74a976 Fixed: Downloads failed for file contents will be removed from client 2025-03-08 19:59:13 -08:00
Stevie Robinson 7193acb5ee Fixed: Improve rejected download handling 2025-03-08 19:59:07 -08:00
Stevie Robinson 6f1fc1686f Fixed: Don't return warning in title field for rejected downloads
Closes #7663
2025-02-22 12:42:35 -08:00
Stevie Robinson b7407837b7 Fixed: Rejected Imports with no associated release or indexer 2025-02-22 12:40:49 -08:00
Mark McDowall 4e65669c48 Bump version to 4.0.13 2025-02-11 19:25:11 -08:00
Weblate fa38498db0 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Magnus5405 <magnus5405@outlook.com>
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/da/
Translation: Servarr/Sonarr
2025-02-11 19:24:56 -08:00
Mark McDowall 3b024443c5 Fixed: Drop downs flickering in some cases
Closes #7608
2025-01-30 20:58:11 -08:00
Weblate 4ba9b21bb7 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Oskari Lavinto <olavinto@protonmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/fi/
Translation: Servarr/Sonarr
2025-01-30 20:57:59 -08:00
Stevie Robinson e37684e045 Fixed: Failing dangerous and executable single file downloads 2025-01-25 18:29:07 -08:00
Stevie Robinson 103ccd74f3 New: Treat .scr as dangerous file
Closes #7588
2025-01-25 18:27:32 -08:00
Stevie Robinson ba22992265 Fixed: Don't search for unmonitored specials when searching season
Closes #7589
2025-01-25 18:26:48 -08:00
Bogdan 963395b969 Prevent page crash on console.error being used with non-string values 2025-01-25 18:26:10 -08:00
Weblate 970df1a1d8 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Dani Talens <databio@gmail.com>
Co-authored-by: Gallyam Biktashev <gallyamb@gmail.com>
Co-authored-by: Georgi Panov <darkfella91@gmail.com>
Co-authored-by: Lizandra Candido da Silva <lizandra.c.s@gmail.com>
Co-authored-by: Oskari Lavinto <olavinto@protonmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/bg/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/ca/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/fi/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/ru/
Translation: Servarr/Sonarr
2025-01-25 18:26:02 -08:00
43 changed files with 977 additions and 750 deletions
+1 -1
View File
@@ -22,7 +22,7 @@ env:
FRAMEWORK: net6.0 FRAMEWORK: net6.0
RAW_BRANCH_NAME: ${{ github.head_ref || github.ref_name }} RAW_BRANCH_NAME: ${{ github.head_ref || github.ref_name }}
SONARR_MAJOR_VERSION: 4 SONARR_MAJOR_VERSION: 4
VERSION: 4.0.12 VERSION: 4.0.17
jobs: jobs:
backend: backend:
@@ -29,6 +29,8 @@ import HintedSelectInputOption from './HintedSelectInputOption';
import HintedSelectInputSelectedValue from './HintedSelectInputSelectedValue'; import HintedSelectInputSelectedValue from './HintedSelectInputSelectedValue';
import styles from './EnhancedSelectInput.css'; import styles from './EnhancedSelectInput.css';
const MINIMUM_DISTANCE_FROM_EDGE = 10;
function isArrowKey(keyCode: number) { function isArrowKey(keyCode: number) {
return keyCode === keyCodes.UP_ARROW || keyCode === keyCodes.DOWN_ARROW; return keyCode === keyCodes.UP_ARROW || keyCode === keyCodes.DOWN_ARROW;
} }
@@ -189,14 +191,9 @@ function EnhancedSelectInput<T extends EnhancedSelectInputValue<V>, V>(
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
const handleComputeMaxHeight = useCallback((data: any) => { const handleComputeMaxHeight = useCallback((data: any) => {
const { top, bottom } = data.offsets.reference;
const windowHeight = window.innerHeight; const windowHeight = window.innerHeight;
if (/^bottom/.test(data.placement)) { data.styles.maxHeight = windowHeight - MINIMUM_DISTANCE_FROM_EDGE;
data.styles.maxHeight = windowHeight - bottom;
} else {
data.styles.maxHeight = top;
}
return data; return data;
}, []); }, []);
@@ -508,6 +505,10 @@ function EnhancedSelectInput<T extends EnhancedSelectInputValue<V>, V>(
enabled: true, enabled: true,
fn: handleComputeMaxHeight, fn: handleComputeMaxHeight,
}, },
preventOverflow: {
enabled: true,
boundariesElement: 'viewport',
},
}} }}
> >
{({ ref, style, scheduleUpdate }) => { {({ ref, style, scheduleUpdate }) => {
+1
View File
@@ -19,6 +19,7 @@
.modal { .modal {
position: relative; position: relative;
display: flex; display: flex;
max-width: 90%;
max-height: 90%; max-height: 90%;
border-radius: 6px; border-radius: 6px;
opacity: 1; opacity: 1;
+5 -4
View File
@@ -23,12 +23,13 @@ const error = console.error;
function logError(...parameters: any[]) { function logError(...parameters: any[]) {
const filter = parameters.find((parameter) => { const filter = parameters.find((parameter) => {
return ( return (
parameter.includes( typeof parameter === 'string' &&
(parameter.includes(
'Support for defaultProps will be removed from function components in a future major release' 'Support for defaultProps will be removed from function components in a future major release'
) || ) ||
parameter.includes( parameter.includes(
'findDOMNode is deprecated and will be removed in the next major release' 'findDOMNode is deprecated and will be removed in the next major release'
) ))
); );
}); });
@@ -390,5 +390,12 @@ namespace NzbDrone.Common.Http
return this; return this;
} }
public virtual HttpRequestBuilder AllowRedirect(bool allowAutoRedirect = true)
{
AllowAutoRedirect = allowAutoRedirect;
return this;
}
} }
} }
+9
View File
@@ -0,0 +1,9 @@
using System.IO;
using System.Text;
namespace NzbDrone.Common;
public class Utf8StringWriter : StringWriter
{
public override Encoding Encoding => Encoding.UTF8;
}
@@ -213,5 +213,16 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport
Subject.IsSample(_localEpisode).Should().Be(DetectSampleResult.Sample); Subject.IsSample(_localEpisode).Should().Be(DetectSampleResult.Sample);
} }
[Test]
public void should_default_to_45_minutes_if_runtime_is_zero()
{
GivenRuntime(120);
_localEpisode.Series.Runtime = 0;
_localEpisode.Episodes.First().Runtime = 0;
Subject.IsSample(_localEpisode).Should().Be(DetectSampleResult.Sample);
}
} }
} }
@@ -425,8 +425,8 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
} }
catch (HttpException ex) catch (HttpException ex)
{ {
_logger.Debug("qbitTorrent authentication failed."); _logger.Debug(ex, "qbitTorrent authentication failed.");
if (ex.Response.StatusCode == HttpStatusCode.Forbidden) if (ex.Response.StatusCode is HttpStatusCode.Unauthorized or HttpStatusCode.Forbidden)
{ {
throw new DownloadClientAuthenticationException("Failed to authenticate with qBittorrent.", ex); throw new DownloadClientAuthenticationException("Failed to authenticate with qBittorrent.", ex);
} }
@@ -438,7 +438,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
throw new DownloadClientUnavailableException("Failed to connect to qBittorrent, please check your settings.", ex); throw new DownloadClientUnavailableException("Failed to connect to qBittorrent, please check your settings.", ex);
} }
if (response.Content != "Ok.") if (response.Content.IsNotNullOrWhiteSpace() && response.Content != "Ok.")
{ {
// returns "Fails." on bad login // returns "Fails." on bad login
_logger.Debug("qbitTorrent authentication failed."); _logger.Debug("qbitTorrent authentication failed.");
@@ -1,5 +1,5 @@
using System.Collections.Generic;
using System.Linq; using System.Linq;
using NLog;
using NzbDrone.Core.Download.TrackedDownloads; using NzbDrone.Core.Download.TrackedDownloads;
using NzbDrone.Core.Indexers; using NzbDrone.Core.Indexers;
using NzbDrone.Core.MediaFiles.EpisodeImport; using NzbDrone.Core.MediaFiles.EpisodeImport;
@@ -14,15 +14,17 @@ public interface IRejectedImportService
public class RejectedImportService : IRejectedImportService public class RejectedImportService : IRejectedImportService
{ {
private readonly ICachedIndexerSettingsProvider _cachedIndexerSettingsProvider; private readonly ICachedIndexerSettingsProvider _cachedIndexerSettingsProvider;
private readonly Logger _logger;
public RejectedImportService(ICachedIndexerSettingsProvider cachedIndexerSettingsProvider) public RejectedImportService(ICachedIndexerSettingsProvider cachedIndexerSettingsProvider, Logger logger)
{ {
_cachedIndexerSettingsProvider = cachedIndexerSettingsProvider; _cachedIndexerSettingsProvider = cachedIndexerSettingsProvider;
_logger = logger;
} }
public bool Process(TrackedDownload trackedDownload, ImportResult importResult) public bool Process(TrackedDownload trackedDownload, ImportResult importResult)
{ {
if (importResult.Result != ImportResultType.Rejected || importResult.ImportDecision.LocalEpisode != null) if (importResult.Result != ImportResultType.Rejected || trackedDownload.RemoteEpisode?.Release == null)
{ {
return false; return false;
} }
@@ -30,19 +32,27 @@ public class RejectedImportService : IRejectedImportService
var indexerSettings = _cachedIndexerSettingsProvider.GetSettings(trackedDownload.RemoteEpisode.Release.IndexerId); var indexerSettings = _cachedIndexerSettingsProvider.GetSettings(trackedDownload.RemoteEpisode.Release.IndexerId);
var rejectionReason = importResult.ImportDecision.Rejections.FirstOrDefault()?.Reason; var rejectionReason = importResult.ImportDecision.Rejections.FirstOrDefault()?.Reason;
if (indexerSettings == null)
{
trackedDownload.Warn(new TrackedDownloadStatusMessage(trackedDownload.DownloadItem.Title, importResult.Errors));
return true;
}
if (rejectionReason == ImportRejectionReason.DangerousFile && if (rejectionReason == ImportRejectionReason.DangerousFile &&
indexerSettings.FailDownloads.Contains(FailDownloads.PotentiallyDangerous)) indexerSettings.FailDownloads.Contains(FailDownloads.PotentiallyDangerous))
{ {
_logger.Trace("Download '{0}' contains potentially dangerous file, marking as failed", trackedDownload.DownloadItem.Title);
trackedDownload.Fail(); trackedDownload.Fail();
} }
else if (rejectionReason == ImportRejectionReason.ExecutableFile && else if (rejectionReason == ImportRejectionReason.ExecutableFile &&
indexerSettings.FailDownloads.Contains(FailDownloads.Executables)) indexerSettings.FailDownloads.Contains(FailDownloads.Executables))
{ {
_logger.Trace("Download '{0}' contains executable file, marking as failed", trackedDownload.DownloadItem.Title);
trackedDownload.Fail(); trackedDownload.Fail();
} }
else else
{ {
trackedDownload.Warn(new TrackedDownloadStatusMessage(importResult.Errors.First(), new List<string>())); trackedDownload.Warn(new TrackedDownloadStatusMessage(trackedDownload.DownloadItem.Title, importResult.Errors));
} }
return true; return true;
@@ -40,6 +40,9 @@ namespace NzbDrone.Core.Download.TrackedDownloads
{ {
Status = TrackedDownloadStatus.Error; Status = TrackedDownloadStatus.Error;
State = TrackedDownloadState.FailedPending; State = TrackedDownloadState.FailedPending;
// Set CanBeRemoved to allow the failed item to be removed from the client
DownloadItem.CanBeRemoved = true;
} }
} }
@@ -73,7 +73,7 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Plex
if (episodeFile.SeasonNumber == 0) if (episodeFile.SeasonNumber == 0)
{ {
episodeFormat = $"SP{episodesInFile.First():00}"; episodeFormat = $"SP{episodesInFile.First().EpisodeNumber:00}";
} }
content.AppendLine($"Episode: {episodeFormat}: {episodeFile.RelativePath}"); content.AppendLine($"Episode: {episodeFormat}: {episodeFile.RelativePath}");
@@ -8,6 +8,7 @@ using System.Text.RegularExpressions;
using System.Xml; using System.Xml;
using System.Xml.Linq; using System.Xml.Linq;
using NLog; using NLog;
using NzbDrone.Common;
using NzbDrone.Common.Disk; using NzbDrone.Common.Disk;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
using NzbDrone.Common.Serializer; using NzbDrone.Common.Serializer;
@@ -149,110 +150,116 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Xbmc
if (Settings.SeriesMetadata) if (Settings.SeriesMetadata)
{ {
_logger.Debug("Generating Series Metadata for: {0}", series.Title); _logger.Debug("Generating Series Metadata for: {0}", series.Title);
var sb = new StringBuilder();
var xws = new XmlWriterSettings();
xws.OmitXmlDeclaration = true;
xws.Indent = false;
using (var xw = XmlWriter.Create(sb, xws)) var tvShow = new XElement("tvshow");
tvShow.Add(new XElement("title", series.Title));
if (series.Ratings != null && series.Ratings.Votes > 0)
{ {
var tvShow = new XElement("tvshow"); tvShow.Add(new XElement("rating", series.Ratings.Value));
tvShow.Add(new XElement("title", series.Title));
if (series.Ratings != null && series.Ratings.Votes > 0)
{
tvShow.Add(new XElement("rating", series.Ratings.Value));
}
tvShow.Add(new XElement("plot", series.Overview));
tvShow.Add(new XElement("mpaa", series.Certification));
tvShow.Add(new XElement("id", series.TvdbId));
var uniqueId = new XElement("uniqueid", series.TvdbId);
uniqueId.SetAttributeValue("type", "tvdb");
uniqueId.SetAttributeValue("default", true);
tvShow.Add(uniqueId);
if (series.ImdbId.IsNotNullOrWhiteSpace())
{
var imdbId = new XElement("uniqueid", series.ImdbId);
imdbId.SetAttributeValue("type", "imdb");
tvShow.Add(imdbId);
}
if (series.TmdbId > 0)
{
var tmdbId = new XElement("uniqueid", series.TmdbId);
tmdbId.SetAttributeValue("type", "tmdb");
tvShow.Add(tmdbId);
}
if (series.TvMazeId > 0)
{
var tvMazeId = new XElement("uniqueid", series.TvMazeId);
tvMazeId.SetAttributeValue("type", "tvmaze");
tvShow.Add(tvMazeId);
}
foreach (var genre in series.Genres)
{
tvShow.Add(new XElement("genre", genre));
}
if (series.Tags.Any())
{
var tags = _tagRepo.GetTags(series.Tags);
foreach (var tag in tags)
{
tvShow.Add(new XElement("tag", tag.Label));
}
}
tvShow.Add(new XElement("status", series.Status));
if (series.FirstAired.HasValue)
{
tvShow.Add(new XElement("premiered", series.FirstAired.Value.ToString("yyyy-MM-dd")));
}
// Add support for Jellyfin's "enddate" tag
if (series.Status == SeriesStatusType.Ended && series.LastAired.HasValue)
{
tvShow.Add(new XElement("enddate", series.LastAired.Value.ToString("yyyy-MM-dd")));
}
tvShow.Add(new XElement("studio", series.Network));
foreach (var actor in series.Actors)
{
var xmlActor = new XElement("actor",
new XElement("name", actor.Name),
new XElement("role", actor.Character));
if (actor.Images.Any())
{
xmlActor.Add(new XElement("thumb", actor.Images.First().RemoteUrl));
}
tvShow.Add(xmlActor);
}
if (Settings.SeriesMetadataEpisodeGuide)
{
var episodeGuide = new KodiEpisodeGuide(series);
var serializerSettings = STJson.GetSerializerSettings();
serializerSettings.WriteIndented = false;
tvShow.Add(new XElement("episodeguide", JsonSerializer.Serialize(episodeGuide, serializerSettings)));
}
var doc = new XDocument(tvShow);
doc.Save(xw);
xmlResult += doc.ToString();
} }
tvShow.Add(new XElement("plot", series.Overview));
tvShow.Add(new XElement("mpaa", series.Certification));
tvShow.Add(new XElement("id", series.TvdbId));
var uniqueId = new XElement("uniqueid", series.TvdbId);
uniqueId.SetAttributeValue("type", "tvdb");
uniqueId.SetAttributeValue("default", true);
tvShow.Add(uniqueId);
if (series.ImdbId.IsNotNullOrWhiteSpace())
{
var imdbId = new XElement("uniqueid", series.ImdbId);
imdbId.SetAttributeValue("type", "imdb");
tvShow.Add(imdbId);
}
if (series.TmdbId > 0)
{
var tmdbId = new XElement("uniqueid", series.TmdbId);
tmdbId.SetAttributeValue("type", "tmdb");
tvShow.Add(tmdbId);
}
if (series.TvMazeId > 0)
{
var tvMazeId = new XElement("uniqueid", series.TvMazeId);
tvMazeId.SetAttributeValue("type", "tvmaze");
tvShow.Add(tvMazeId);
}
foreach (var genre in series.Genres)
{
tvShow.Add(new XElement("genre", genre));
}
if (series.Tags.Any())
{
var tags = _tagRepo.GetTags(series.Tags);
foreach (var tag in tags)
{
tvShow.Add(new XElement("tag", tag.Label));
}
}
tvShow.Add(new XElement("status", series.Status));
if (series.FirstAired.HasValue)
{
tvShow.Add(new XElement("premiered", series.FirstAired.Value.ToString("yyyy-MM-dd")));
}
// Add support for Jellyfin's "enddate" tag
if (series.Status == SeriesStatusType.Ended && series.LastAired.HasValue)
{
tvShow.Add(new XElement("enddate", series.LastAired.Value.ToString("yyyy-MM-dd")));
}
tvShow.Add(new XElement("studio", series.Network));
foreach (var actor in series.Actors)
{
var xmlActor = new XElement("actor",
new XElement("name", actor.Name),
new XElement("role", actor.Character));
if (actor.Images.Any())
{
xmlActor.Add(new XElement("thumb", actor.Images.First().RemoteUrl));
}
tvShow.Add(xmlActor);
}
if (Settings.SeriesMetadataEpisodeGuide)
{
var episodeGuide = new KodiEpisodeGuide(series);
var serializerSettings = STJson.GetSerializerSettings();
serializerSettings.WriteIndented = false;
tvShow.Add(new XElement("episodeguide", JsonSerializer.Serialize(episodeGuide, serializerSettings)));
}
var doc = new XDocument(tvShow)
{
Declaration = new XDeclaration("1.0", "UTF-8", "yes"),
};
var sb = new StringBuilder();
using var sw = new Utf8StringWriter();
using var xw = XmlWriter.Create(sw, new XmlWriterSettings
{
Encoding = Encoding.UTF8,
Indent = true
});
doc.Save(xw);
xw.Flush();
xmlResult += sw.ToString();
} }
if (Settings.SeriesMetadataUrl) if (Settings.SeriesMetadataUrl)
@@ -279,116 +286,117 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Xbmc
var watched = GetExistingWatchedStatus(series, episodeFile.RelativePath); var watched = GetExistingWatchedStatus(series, episodeFile.RelativePath);
var xmlResult = string.Empty; var xws = new XmlWriterSettings
{
Encoding = Encoding.UTF8,
Indent = true,
ConformanceLevel = ConformanceLevel.Fragment
};
using var sw = new Utf8StringWriter();
using var xw = XmlWriter.Create(sw, xws);
xw.WriteProcessingInstruction("xml", "version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"");
foreach (var episode in episodeFile.Episodes.Value) foreach (var episode in episodeFile.Episodes.Value)
{ {
var sb = new StringBuilder(); var image = episode.Images.SingleOrDefault(i => i.CoverType == MediaCoverTypes.Screenshot);
var xws = new XmlWriterSettings();
xws.OmitXmlDeclaration = true;
xws.Indent = false;
using (var xw = XmlWriter.Create(sb, xws)) var details = new XElement("episodedetails");
details.Add(new XElement("title", episode.Title));
details.Add(new XElement("season", episode.SeasonNumber));
details.Add(new XElement("episode", episode.EpisodeNumber));
details.Add(new XElement("aired", episode.AirDate));
details.Add(new XElement("plot", episode.Overview));
if (episode.SeasonNumber == 0 && episode.AiredAfterSeasonNumber.HasValue)
{ {
var doc = new XDocument(); details.Add(new XElement("displayafterseason", episode.AiredAfterSeasonNumber));
var image = episode.Images.SingleOrDefault(i => i.CoverType == MediaCoverTypes.Screenshot);
var details = new XElement("episodedetails");
details.Add(new XElement("title", episode.Title));
details.Add(new XElement("season", episode.SeasonNumber));
details.Add(new XElement("episode", episode.EpisodeNumber));
details.Add(new XElement("aired", episode.AirDate));
details.Add(new XElement("plot", episode.Overview));
if (episode.SeasonNumber == 0 && episode.AiredAfterSeasonNumber.HasValue)
{
details.Add(new XElement("displayafterseason", episode.AiredAfterSeasonNumber));
}
else if (episode.SeasonNumber == 0 && episode.AiredBeforeSeasonNumber.HasValue)
{
details.Add(new XElement("displayseason", episode.AiredBeforeSeasonNumber));
details.Add(new XElement("displayepisode", episode.AiredBeforeEpisodeNumber ?? -1));
}
var tvdbId = new XElement("uniqueid", episode.TvdbId);
tvdbId.SetAttributeValue("type", "tvdb");
tvdbId.SetAttributeValue("default", true);
details.Add(tvdbId);
var sonarrId = new XElement("uniqueid", episode.Id);
sonarrId.SetAttributeValue("type", "sonarr");
details.Add(sonarrId);
if (image == null)
{
details.Add(new XElement("thumb"));
}
else if (Settings.EpisodeImageThumb)
{
details.Add(new XElement("thumb", image.RemoteUrl));
}
details.Add(new XElement("watched", watched));
if (episode.Ratings != null && episode.Ratings.Votes > 0)
{
details.Add(new XElement("rating", episode.Ratings.Value));
}
if (episodeFile.MediaInfo != null)
{
var sceneName = episodeFile.GetSceneOrFileName();
var fileInfo = new XElement("fileinfo");
var streamDetails = new XElement("streamdetails");
var video = new XElement("video");
video.Add(new XElement("aspect", (float)episodeFile.MediaInfo.Width / (float)episodeFile.MediaInfo.Height));
video.Add(new XElement("bitrate", episodeFile.MediaInfo.VideoBitrate));
video.Add(new XElement("codec", MediaInfoFormatter.FormatVideoCodec(episodeFile.MediaInfo, sceneName)));
video.Add(new XElement("framerate", episodeFile.MediaInfo.VideoFps));
video.Add(new XElement("height", episodeFile.MediaInfo.Height));
video.Add(new XElement("scantype", episodeFile.MediaInfo.ScanType));
video.Add(new XElement("width", episodeFile.MediaInfo.Width));
video.Add(new XElement("duration", episodeFile.MediaInfo.RunTime.TotalMinutes));
video.Add(new XElement("durationinseconds", Math.Round(episodeFile.MediaInfo.RunTime.TotalSeconds)));
streamDetails.Add(video);
var audio = new XElement("audio");
var audioChannelCount = episodeFile.MediaInfo.AudioChannels;
audio.Add(new XElement("bitrate", episodeFile.MediaInfo.AudioBitrate));
audio.Add(new XElement("channels", audioChannelCount));
audio.Add(new XElement("codec", MediaInfoFormatter.FormatAudioCodec(episodeFile.MediaInfo, sceneName)));
audio.Add(new XElement("language", episodeFile.MediaInfo.AudioLanguages));
streamDetails.Add(audio);
if (episodeFile.MediaInfo.Subtitles != null && episodeFile.MediaInfo.Subtitles.Count > 0)
{
foreach (var s in episodeFile.MediaInfo.Subtitles)
{
var subtitle = new XElement("subtitle");
subtitle.Add(new XElement("language", s));
streamDetails.Add(subtitle);
}
}
fileInfo.Add(streamDetails);
details.Add(fileInfo);
}
// Todo: get guest stars, writer and director
// details.Add(new XElement("credits", tvdbEpisode.Writer.FirstOrDefault()));
// details.Add(new XElement("director", tvdbEpisode.Directors.FirstOrDefault()));
doc.Add(details);
doc.Save(xw);
xmlResult += doc.ToString();
xmlResult += Environment.NewLine;
} }
else if (episode.SeasonNumber == 0 && episode.AiredBeforeSeasonNumber.HasValue)
{
details.Add(new XElement("displayseason", episode.AiredBeforeSeasonNumber));
details.Add(new XElement("displayepisode", episode.AiredBeforeEpisodeNumber ?? -1));
}
var tvdbId = new XElement("uniqueid", episode.TvdbId);
tvdbId.SetAttributeValue("type", "tvdb");
tvdbId.SetAttributeValue("default", true);
details.Add(tvdbId);
var sonarrId = new XElement("uniqueid", episode.Id);
sonarrId.SetAttributeValue("type", "sonarr");
details.Add(sonarrId);
if (image == null)
{
details.Add(new XElement("thumb"));
}
else if (Settings.EpisodeImageThumb)
{
details.Add(new XElement("thumb", image.RemoteUrl));
}
details.Add(new XElement("watched", watched));
if (episode.Ratings != null && episode.Ratings.Votes > 0)
{
details.Add(new XElement("rating", episode.Ratings.Value));
}
if (episodeFile.MediaInfo != null)
{
var sceneName = episodeFile.GetSceneOrFileName();
var fileInfo = new XElement("fileinfo");
var streamDetails = new XElement("streamdetails");
var video = new XElement("video");
video.Add(new XElement("aspect", (float)episodeFile.MediaInfo.Width / (float)episodeFile.MediaInfo.Height));
video.Add(new XElement("bitrate", episodeFile.MediaInfo.VideoBitrate));
video.Add(new XElement("codec", MediaInfoFormatter.FormatVideoCodec(episodeFile.MediaInfo, sceneName)));
video.Add(new XElement("framerate", episodeFile.MediaInfo.VideoFps));
video.Add(new XElement("height", episodeFile.MediaInfo.Height));
video.Add(new XElement("scantype", episodeFile.MediaInfo.ScanType));
video.Add(new XElement("width", episodeFile.MediaInfo.Width));
video.Add(new XElement("duration", episodeFile.MediaInfo.RunTime.TotalMinutes));
video.Add(new XElement("durationinseconds", Math.Round(episodeFile.MediaInfo.RunTime.TotalSeconds)));
streamDetails.Add(video);
var audio = new XElement("audio");
var audioChannelCount = episodeFile.MediaInfo.AudioChannels;
audio.Add(new XElement("bitrate", episodeFile.MediaInfo.AudioBitrate));
audio.Add(new XElement("channels", audioChannelCount));
audio.Add(new XElement("codec", MediaInfoFormatter.FormatAudioCodec(episodeFile.MediaInfo, sceneName)));
audio.Add(new XElement("language", episodeFile.MediaInfo.AudioLanguages));
streamDetails.Add(audio);
if (episodeFile.MediaInfo.Subtitles != null && episodeFile.MediaInfo.Subtitles.Count > 0)
{
foreach (var s in episodeFile.MediaInfo.Subtitles)
{
var subtitle = new XElement("subtitle");
subtitle.Add(new XElement("language", s));
streamDetails.Add(subtitle);
}
}
fileInfo.Add(streamDetails);
details.Add(fileInfo);
}
// Todo: get guest stars, writer and director
// details.Add(new XElement("credits", tvdbEpisode.Writer.FirstOrDefault()));
// details.Add(new XElement("director", tvdbEpisode.Directors.FirstOrDefault()));
details.WriteTo(xw);
} }
xw.Flush();
var xmlResult = sw.ToString();
return new MetadataFileResult(GetEpisodeMetadataFilename(episodeFile.RelativePath), xmlResult.Trim(Environment.NewLine.ToCharArray())); return new MetadataFileResult(GetEpisodeMetadataFilename(episodeFile.RelativePath), xmlResult.Trim(Environment.NewLine.ToCharArray()));
} }
@@ -72,7 +72,7 @@ namespace NzbDrone.Core.ImportLists.Custom
} }
var baseUrl = settings.BaseUrl.TrimEnd('/'); var baseUrl = settings.BaseUrl.TrimEnd('/');
var request = new HttpRequestBuilder(baseUrl).Accept(HttpAccept.Json).Build(); var request = new HttpRequestBuilder(baseUrl).Accept(HttpAccept.Json).AllowRedirect().Build();
var response = _httpClient.Get(request); var response = _httpClient.Get(request);
var results = JsonConvert.DeserializeObject<List<TResource>>(response.Content); var results = JsonConvert.DeserializeObject<List<TResource>>(response.Content);
@@ -305,7 +305,7 @@ namespace NzbDrone.Core.ImportLists
{ {
var seriesExists = allListItems.Where(l => var seriesExists = allListItems.Where(l =>
l.TvdbId == series.TvdbId || l.TvdbId == series.TvdbId ||
l.ImdbId == series.ImdbId || (l.ImdbId.IsNotNullOrWhiteSpace() && series.ImdbId.IsNotNullOrWhiteSpace() && l.ImdbId == series.ImdbId) ||
l.TmdbId == series.TmdbId || l.TmdbId == series.TmdbId ||
series.MalIds.Contains(l.MalId) || series.MalIds.Contains(l.MalId) ||
series.AniListIds.Contains(l.AniListId)).ToList(); series.AniListIds.Contains(l.AniListId)).ToList();
@@ -1,5 +1,5 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Text.Json.Serialization; using Newtonsoft.Json;
namespace NzbDrone.Core.ImportLists.Trakt namespace NzbDrone.Core.ImportLists.Trakt
{ {
@@ -17,7 +17,7 @@ namespace NzbDrone.Core.ImportLists.Trakt
public string Title { get; set; } public string Title { get; set; }
public int? Year { get; set; } public int? Year { get; set; }
public TraktSeriesIdsResource Ids { get; set; } public TraktSeriesIdsResource Ids { get; set; }
[JsonPropertyName("aired_episodes")] [JsonProperty("aired_episodes")]
public int AiredEpisodes { get; set; } public int AiredEpisodes { get; set; }
} }
@@ -44,11 +44,11 @@ namespace NzbDrone.Core.ImportLists.Trakt
public class RefreshRequestResponse public class RefreshRequestResponse
{ {
[JsonPropertyName("access_token")] [JsonProperty("access_token")]
public string AccessToken { get; set; } public string AccessToken { get; set; }
[JsonPropertyName("expires_in")] [JsonProperty("expires_in")]
public int ExpiresIn { get; set; } public int ExpiresIn { get; set; }
[JsonPropertyName("refresh_token")] [JsonProperty("refresh_token")]
public string RefreshToken { get; set; } public string RefreshToken { get; set; }
} }
@@ -367,7 +367,9 @@ namespace NzbDrone.Core.IndexerSearch
// build list of queries for each episode in the form: "<series> <episode-title>" // build list of queries for each episode in the form: "<series> <episode-title>"
searchSpec.EpisodeQueryTitles = episodes.Where(e => !string.IsNullOrWhiteSpace(e.Title)) searchSpec.EpisodeQueryTitles = episodes.Where(e => !string.IsNullOrWhiteSpace(e.Title))
.Where(e => interactiveSearch || !monitoredOnly || e.Monitored)
.SelectMany(e => searchSpec.CleanSceneTitles.Select(title => title + " " + SearchCriteriaBase.GetCleanSceneTitle(e.Title))) .SelectMany(e => searchSpec.CleanSceneTitles.Select(title => title + " " + SearchCriteriaBase.GetCleanSceneTitle(e.Title)))
.Distinct(StringComparer.InvariantCultureIgnoreCase)
.ToArray(); .ToArray();
downloadDecisions.AddRange(await Dispatch(indexer => indexer.Fetch(searchSpec), searchSpec)); downloadDecisions.AddRange(await Dispatch(indexer => indexer.Fetch(searchSpec), searchSpec));
+80 -27
View File
@@ -5,7 +5,7 @@
"AddDownloadClientImplementation": "Добави клиент за изтегляне - {implementationName}", "AddDownloadClientImplementation": "Добави клиент за изтегляне - {implementationName}",
"AddExclusion": "Добави изключение", "AddExclusion": "Добави изключение",
"AddImportList": "Добави списък за импортиране", "AddImportList": "Добави списък за импортиране",
"AddIndexer": "Добави индексатор", "AddIndexer": "Добавете индексатор",
"AirsTomorrowOn": "Утре от {time} по {networkLabel}", "AirsTomorrowOn": "Утре от {time} по {networkLabel}",
"AddedToDownloadQueue": "Добавен към опашката за изтегляне", "AddedToDownloadQueue": "Добавен към опашката за изтегляне",
"AfterManualRefresh": "След ръчно опресняване", "AfterManualRefresh": "След ръчно опресняване",
@@ -17,10 +17,10 @@
"Any": "Всякакви", "Any": "Всякакви",
"AddConditionImplementation": "Добави условие - {implementationName}", "AddConditionImplementation": "Добави условие - {implementationName}",
"AddConnectionImplementation": "Добави връзка - {implementationName}", "AddConnectionImplementation": "Добави връзка - {implementationName}",
"AddListExclusion": "Добави изключение от списъка", "AddListExclusion": "Добавете изключение от списъка",
"AddImportListExclusion": "Добави изключение от списъка за импортиране", "AddImportListExclusion": "Добави изключение от списъка за импортиране",
"AddImportListExclusionError": "Не може да се добави ново изключение от списъка за импортиране, моля, опитайте отново.", "AddImportListExclusionError": "Не може да се добави ново изключение от списъка за импортиране, моля, опитайте отново.",
"AddListExclusionSeriesHelpText": "Предотврати добавянето на сериили в {appName} посредством списъци", "AddListExclusionSeriesHelpText": "Предотвратете добавянето на сериали в {appName} чрез списъци",
"AnimeEpisodeTypeFormat": "Абсолютен номер на епизода ({format})", "AnimeEpisodeTypeFormat": "Абсолютен номер на епизода ({format})",
"AddRootFolderError": "Не може да се добави основна папка, моля, опитайте отново", "AddRootFolderError": "Не може да се добави основна папка, моля, опитайте отново",
"AnimeEpisodeTypeDescription": "Епизоди, издадени с абсолютен номер на епизод", "AnimeEpisodeTypeDescription": "Епизоди, издадени с абсолютен номер на епизод",
@@ -28,63 +28,116 @@
"AnalyticsEnabledHelpText": "Изпращайте анонимна информация за използването и грешките към сървърите на {appName}. Това включва информация за вашия браузър, кои страници на уеб интерфейса на {appName} използвате, докладваните грешки, както и версията на операционната система и изпълнителната среда. Ще използваме тази информация, за да приоритизираме нови функции и поправки на бъгове.", "AnalyticsEnabledHelpText": "Изпращайте анонимна информация за използването и грешките към сървърите на {appName}. Това включва информация за вашия браузър, кои страници на уеб интерфейса на {appName} използвате, докладваните грешки, както и версията на операционната система и изпълнителната среда. Ще използваме тази информация, за да приоритизираме нови функции и поправки на бъгове.",
"Anime": "Аниме", "Anime": "Аниме",
"AddIndexerError": "Не може да се добави нов индексатор, моля, опитайте отново.", "AddIndexerError": "Не може да се добави нов индексатор, моля, опитайте отново.",
"AddIndexerImplementation": "Добави индексатор - {implementationName}", "AddIndexerImplementation": "Добавете индексатор - {implementationName}",
"AddDelayProfileError": "Не може да се добави нов профил за забавяне, моля, опитайте отново.", "AddDelayProfileError": "Не може да се добави нов профил за забавяне, моля, опитайте отново.",
"AddNotificationError": "Не може да се добави ново известие, моля, опитайте отново.", "AddNotificationError": "Не може да се добави ново известие, моля, опитайте отново.",
"AddImportListImplementation": "Добави списък за импортиране - {implementationName}", "AddImportListImplementation": "Добави списък за импортиране - {implementationName}",
"AddList": "Добави списък", "AddList": "Добавете списък",
"AddNewSeriesSearchForMissingEpisodes": "Започни търсене на липсващи епизоди", "AddNewSeriesSearchForMissingEpisodes": "Започнете търсене на липсващи епизоди",
"AddRemotePathMapping": "Добави мапиране към отдалечен път", "AddRemotePathMapping": "Добавете мапиране към отдалечен път",
"AddRemotePathMappingError": "Не може да се добави ново мапиране към отдалечен път, моля, опитайте отново.", "AddRemotePathMappingError": "Не може да се добави ново мапиране към отдалечен път, моля, опитайте отново.",
"AddToDownloadQueue": "Добави към опашката за изтегляне", "AddToDownloadQueue": "Добавете към опашката за изтегляне",
"AlreadyInYourLibrary": "Вече е във вашата библиотека", "AlreadyInYourLibrary": "Вече е във вашата библиотека",
"AnEpisodeIsDownloading": "Изтегля се епизод", "AnEpisodeIsDownloading": "Изтегля се епизод",
"AnimeEpisodeFormat": "Аниме Епизод Формат", "AnimeEpisodeFormat": "Формат на Аниме епизодите",
"ApiKey": "API ключ", "ApiKey": "API ключ",
"Added": "Добавен", "Added": "Добавен",
"ApiKeyValidationHealthCheckMessage": "Моля, актуализирайте ключа си за API, за да бъде с дължина най-малко {length} знака. Може да направите това чрез настройките или конфигурационния файл", "ApiKeyValidationHealthCheckMessage": "Моля, актуализирайте вашия API ключ, за да бъде с дължина най-малко {length} знака. Може да направите това чрез настройките или конфигурационния файл",
"AddConditionError": "Не може да се добави новo условие, моля, опитайте отново.", "AddConditionError": "Не може да се добави новo условие, моля, опитайте отново.",
"AddAutoTagError": "Не може да се добави нов автоматичен таг, моля, опитайте отново.", "AddAutoTagError": "Не може да се добави нов автоматичен таг, моля, опитайте отново.",
"AddConnection": "Добави връзка", "AddConnection": "Добави връзка",
"AddCustomFormat": "Добави персонализиран формат", "AddCustomFormat": "Добавете персонализиран формат",
"AddCustomFormatError": "Не може да се добави нов персонализиран формат, моля, опитайте отново.", "AddCustomFormatError": "Не може да се добави нов персонализиран формат, моля, опитайте отново.",
"AddDelayProfile": "Добави профил за забавяне", "AddDelayProfile": "Добавете профил за забавяне",
"AddDownloadClient": "Добави клиент за изтегляне", "AddDownloadClient": "Добави клиент за изтегляне",
"AddDownloadClientError": "Не може да се добави нов клиент за изтегляне, моля, опитайте отново.", "AddDownloadClientError": "Не може да се добави нов клиент за изтегляне, моля, опитайте отново.",
"AddListExclusionError": "Не може да се добави ново изключение от списъка, моля, опитайте отново.", "AddListExclusionError": "Не може да се добави ново изключение от списъка, моля, опитайте отново.",
"AddNewRestriction": "Добави новo ограничение", "AddNewRestriction": "Добавете новo ограничение",
"AddListError": "Не може да се добави нов списък, моля, опитайте отново.", "AddListError": "Не може да се добави нов списък, моля, опитайте отново.",
"AddQualityProfile": "Добави профил за качество", "AddQualityProfile": "Добавете профил за качество",
"AddQualityProfileError": "Не може да се добави нов профил за качество, моля, опитайте отново.", "AddQualityProfileError": "Не може да се добави нов профил за качество, моля, опитайте отново.",
"AddReleaseProfile": "Добави профил за издания", "AddReleaseProfile": "Добавете профил за издания",
"Always": "Винаги", "Always": "Винаги",
"AnalyseVideoFiles": "Анализирай видео файлове", "AnalyseVideoFiles": "Анализирайте видео файловете",
"Analytics": "Анализ", "Analytics": "Анализ",
"AgeWhenGrabbed": "Възраст (при грабване)", "AgeWhenGrabbed": "Възраст (при грабване)",
"AddAutoTag": "Добави автоматичен етикет", "AddAutoTag": "Добави автоматичен таг",
"AddCondition": "Добави условие", "AddCondition": "Добави условие",
"AirDate": "Ефирна дата", "AirDate": "Ефирна дата",
"AllTitles": "Всички заглавия", "AllTitles": "Всички заглавия",
"AddRootFolder": "Добави основна папка", "AddRootFolder": "Добавете основна папка",
"Add": "Добави", "Add": "Добавяне",
"AddingTag": "Добавяне на етикет", "AddingTag": "Добавяне на таг",
"Age": "Възраст", "Age": "Възраст",
"All": "Всички", "All": "Всички",
"Activity": "Активност", "Activity": "Дейност",
"AddNew": "Добави нов", "AddNew": "Добавете нов",
"Actions": "Действия", "Actions": "Действия",
"About": "Относно", "About": "Относно",
"Agenda": "Агенда", "Agenda": "Агенда",
"AddNewSeries": "Добави нов сериал", "AddNewSeries": "Добавете нов сериал",
"AddNewSeriesError": "Неуспешно зареждане на резултатите от търсенето, моля, опитайте отново.", "AddNewSeriesError": "Неуспешно зареждане на резултатите от търсенето, моля, опитайте отново.",
"AddNewSeriesHelpText": "Добавянето на нови сериали е лесно, започнете, като напишете името на сериала, който искате да добавите.", "AddNewSeriesHelpText": "Лесно е да добавите нов сериал, просто започнете да въвеждате името на сериала, който искате да добавите.",
"AddNewSeriesRootFolderHelpText": "'{folder}' подпапка ще бъде създадена автоматично", "AddNewSeriesRootFolderHelpText": "Подпапката '{folder}' ще бъде създадена автоматично",
"AddNewSeriesSearchForCutoffUnmetEpisodes": "Започни търсене на епизоди, които не са достигнали максималното качество за надграждане", "AddNewSeriesSearchForCutoffUnmetEpisodes": "Започни търсене на епизоди, които не са достигнали максималното качество за надграждане",
"AddSeriesWithTitle": "Добави {title}", "AddSeriesWithTitle": "Добавете {title}",
"Absolute": "Абсолютен", "Absolute": "Абсолютен",
"AllSeriesAreHiddenByTheAppliedFilter": "Всички резултати са скрити от приложения филтър", "AllSeriesAreHiddenByTheAppliedFilter": "Всички резултати са скрити от приложения филтър",
"AllSeriesInRootFolderHaveBeenImported": "Всички сериали в {path} са импортирани", "AllSeriesInRootFolderHaveBeenImported": "Всички сериали в {path} са импортирани",
"AbsoluteEpisodeNumber": "Абсолютен епизоден номер", "AbsoluteEpisodeNumber": "Абсолютен епизоден номер",
"AllResultsAreHiddenByTheAppliedFilter": "Всички резултати са скрити от приложения филтър", "AllResultsAreHiddenByTheAppliedFilter": "Всички резултати са скрити от приложения филтър",
"AppDataDirectory": "Директория на AppData" "AppDataDirectory": "Директория на приложението",
"SeasonFolder": "Папка ( Сезони )",
"SeasonDetails": "Детайли за сезона",
"SeasonCount": "Брой сезони",
"SslPort": "SSL порт",
"AppDataLocationHealthCheckMessage": "Актуализирането няма да бъде възможно, за да се предотврати изтриването на папката на приложението по време на актуализацията",
"AppUpdated": "{appName} Актуализиран",
"ApplyTagsHelpTextReplace": "Замяна: Заменете таговете с въведените тагове (не въвеждайте тагове, за да изчистите всички тагове)",
"AudioLanguages": "Аудио езици",
"AuthBasic": "Основно (изскачащ прозорец на браузъра)",
"AuthForm": "Формуляри (Страница за вход)",
"AuthenticationMethodHelpText": "Изисквайте потребителско име и парола за достъп до {appName}",
"AuthenticationRequiredPasswordHelpTextWarning": "Въведете нова парола",
"ApplicationURL": "URL адрес на приложението",
"AuthenticationMethodHelpTextWarning": "Моля, изберете валиден метод за удостоверяване",
"AuthenticationRequiredUsernameHelpTextWarning": "Въведете ново потребителско име",
"AuthenticationRequiredWarning": "За да предотврати отдалечен достъп без удостоверяване, {appName} вече изисква удостоверяването да бъде активирано. По желание можете да деактивирате удостоверяването от локални адреси.",
"SeriesFolderFormat": "Формат на папката ( Сериали )",
"ApplyChanges": "Прилагане на промените",
"AutoTaggingLoadError": "Не може да се зареди автоматичното маркиране",
"SeasonFinale": "Финал на сезона",
"AppUpdatedVersion": "{appName} е актуализиран до версия `{version}`, за да получите най-новите промени, ще трябва да презаредите {appName} ",
"ApplicationUrlHelpText": "Външният URL на това приложение, включително http(s)://, порт и базов URL",
"AutoTagging": "Автоматично маркиране",
"Apply": "Приложете",
"ApplyTags": "Прилагане на тагове",
"AutoAdd": "Автоматично добавяне",
"ApplyTagsHelpTextHowToApplyImportLists": "Как да добавите тагове към избраните списъци за импортиране",
"ApplyTagsHelpTextHowToApplySeries": "Как да добавите тагове към избраните сериали",
"SeasonFolderFormat": "Формат на папката ( Сезони )",
"AudioInfo": "Аудио информация",
"Season": "Сезон",
"ApplyTagsHelpTextAdd": "Добавяне: Добавете маркерите към съществуващия списък с маркери",
"ApplyTagsHelpTextRemove": "Премахване: Премахнете въведените тагове",
"RenameEpisodesHelpText": "{appName} ще използва съществуващото име на файла, ако преименуването е деактивирано",
"ApplyTagsHelpTextHowToApplyDownloadClients": "Как да приложите тагове към избраните приложения за сваляне",
"Authentication": "Удостоверяване",
"AuthenticationRequiredHelpText": "Променете за кои заявки се изисква удостоверяване. Не променяйте, освен ако не разбирате рисковете.",
"AutoRedownloadFailed": "Неуспешно повторно изтегляне",
"AutoRedownloadFailedFromInteractiveSearchHelpText": "Автоматично търсене и опит за изтегляне на различна версия, когато неуспешната версия е била взета от интерактивно търсене",
"AuthenticationRequiredPasswordConfirmationHelpTextWarning": "Потвърдете новата парола",
"AutoTaggingNegateHelpText": "Ако е отметнато, правилото за автоматично маркиране няма да се приложи, ако това условие {implementationName} съвпада.",
"AutoRedownloadFailedFromInteractiveSearch": "Неуспешно повторно изтегляне от интерактивното търсене",
"AutoRedownloadFailedHelpText": "Автоматично търсене и опит за сваляне на различна версия",
"AptUpdater": "Използвайте apt, за да инсталирате актуализацията",
"ApplyTagsHelpTextHowToApplyIndexers": "Как да добавите тагове към избраните индексатори",
"AuthenticationMethod": "Метод за удостоверяване",
"AuthenticationRequired": "Изисква се удостоверяване",
"RenameEpisodes": "Преименуване на епизоди",
"Standard": "Стандартен",
"StandardEpisodeFormat": "Формат на епизода ( Стандартен )",
"SslCertPathHelpText": "Път до \"pfx\" файл",
"EpisodeNaming": "Именуване на епизоди",
"Close": "Затвори"
} }
+9 -1
View File
@@ -750,5 +750,13 @@
"Organize": "Organitza", "Organize": "Organitza",
"Search": "Cerca", "Search": "Cerca",
"SelectDropdown": "Seleccioneu...", "SelectDropdown": "Seleccioneu...",
"Shutdown": "Apaga" "Shutdown": "Apaga",
"ClickToChangeReleaseType": "Feu clic per canviar el tipus de llançament",
"BlocklistFilterHasNoItems": "El filtre de la llista de bloqueig seleccionat no conté elements",
"CustomColonReplacement": "Reemplaçament personalitzat de dos punts",
"CountVotes": "{votes} vots",
"Completed": "Completat",
"ContinuingOnly": "Només en emissió",
"CleanLibraryLevel": "Neteja el nivell de la llibreria",
"CountCustomFormatsSelected": "{count} format(s) personalitzat(s) seleccionat(s)"
} }
+16 -1
View File
@@ -103,5 +103,20 @@
"DeleteReleaseProfileMessageText": "Er du sikker på, at du vil slette udgivelsesprofilen »{name}«?", "DeleteReleaseProfileMessageText": "Er du sikker på, at du vil slette udgivelsesprofilen »{name}«?",
"MinutesSixty": "60 minutter: {sixty}", "MinutesSixty": "60 minutter: {sixty}",
"NegateHelpText": "Hvis dette er markeret, gælder det tilpassede format ikke, hvis denne {implementationName}-betingelse stemmer overens.", "NegateHelpText": "Hvis dette er markeret, gælder det tilpassede format ikke, hvis denne {implementationName}-betingelse stemmer overens.",
"RemoveSelectedItemsQueueMessageText": "Er du sikker på, at du vil fjerne {selectedCount} elementer fra køen?" "RemoveSelectedItemsQueueMessageText": "Er du sikker på, at du vil fjerne {selectedCount} elementer fra køen?",
"AddNewSeriesSearchForCutoffUnmetEpisodes": "Start søgning efter uopfyldte cutoff epsioder",
"AddNewSeriesRootFolderHelpText": "'{folder}' undermappen vil blive oprettet automatisk",
"AddNewSeriesSearchForMissingEpisodes": "Start søgning efter manglende episoder",
"AddListExclusionSeriesHelpText": "Forhindre serie fra at blive tilføjet til {appName} af lister",
"AddQualityProfile": "Tilføj Kvalitetsprofil",
"AddReleaseProfile": "Tilføj udgivelsesprofil",
"AddNewSeriesError": "Kunne ikke indlæse søgeresultater, prøv igen.",
"AddNewSeriesHelpText": "Det er nemt at tilføje en ny serie, bare start med at skrive navnet på serien du gerne vil tilføje.",
"AddQualityProfileError": "Kunne ikke tilføje ny kvalitetsprofil, prøv igen.",
"AddListExclusionError": "Kunne ikke tilføje den nye liste eksklusion, prøv igen.",
"AddNewRestriction": "Tilføj ny restriktion",
"AddNotificationError": "Kunne ikke tilføje ny notifikation, prøv igen.",
"AddRemotePathMapping": "Tilføj Sammenkædning med fjernsti",
"AddNew": "Tilføj ny",
"AddNewSeries": "Tilføj ny serie"
} }
+45 -45
View File
@@ -77,7 +77,7 @@
"AddImportListExclusion": "Lisää tuontilistapoikkeus", "AddImportListExclusion": "Lisää tuontilistapoikkeus",
"AddDownloadClientError": "Latauspalvelun lisääminen epäonnistui. Yritä uudelleen.", "AddDownloadClientError": "Latauspalvelun lisääminen epäonnistui. Yritä uudelleen.",
"AddExclusion": "Lisää poikkeussääntö", "AddExclusion": "Lisää poikkeussääntö",
"AddIndexerError": "Virhe lisättäessä tietolähdettä. Yritä uudelleen.", "AddIndexerError": "Virhe lisättäessä hakupalvelua. Yritä uudelleen.",
"AddList": "Lisää lista", "AddList": "Lisää lista",
"AddIndexer": "Lisää tietolähde", "AddIndexer": "Lisää tietolähde",
"AddCondition": "Lisää ehto", "AddCondition": "Lisää ehto",
@@ -93,7 +93,7 @@
"AddANewPath": "Lisää uusi polku", "AddANewPath": "Lisää uusi polku",
"RemotePathMappingBadDockerPathHealthCheckMessage": "Käytät Dockeria ja latauspalvelu {downloadClientName} tallentaa lataukset kohteeseen \"{path}\", mutta se ei ole kelvollinen {osName}-sijainti. Tarkista etäsijaintien kohdistukset ja latauspalvelun asetukset.", "RemotePathMappingBadDockerPathHealthCheckMessage": "Käytät Dockeria ja latauspalvelu {downloadClientName} tallentaa lataukset kohteeseen \"{path}\", mutta se ei ole kelvollinen {osName}-sijainti. Tarkista etäsijaintien kohdistukset ja latauspalvelun asetukset.",
"AddDownloadClientImplementation": "Lisätään latauspalvelua {implementationName}", "AddDownloadClientImplementation": "Lisätään latauspalvelua {implementationName}",
"AddImportListExclusionError": "Virhe lisättäessä tuontilistapokkeusta. Yritä uudelleen.", "AddImportListExclusionError": "Virhe lisättäessä listapoikkeusta. Yritä uudelleen.",
"AddIndexerImplementation": "Lisätään tietolähdettä {implementationName}", "AddIndexerImplementation": "Lisätään tietolähdettä {implementationName}",
"CalendarOptions": "Kalenterin asetukset", "CalendarOptions": "Kalenterin asetukset",
"BlocklistReleases": "Lisää julkaisut estolistalle", "BlocklistReleases": "Lisää julkaisut estolistalle",
@@ -102,7 +102,7 @@
"ImportMechanismHandlingDisabledHealthCheckMessage": "Käytä valmistuneiden latausten käsittelyä", "ImportMechanismHandlingDisabledHealthCheckMessage": "Käytä valmistuneiden latausten käsittelyä",
"Remove": "Poista", "Remove": "Poista",
"RemoveFromDownloadClient": "Poista latauspalvelusta", "RemoveFromDownloadClient": "Poista latauspalvelusta",
"DownloadClientCheckUnableToCommunicateWithHealthCheckMessage": "Viestintä latauspalvelun \"{downloadClientName}\" kanssa epäonnistui. {errorMessage}", "DownloadClientCheckUnableToCommunicateWithHealthCheckMessage": "Virhe viestittäessä latauspalvelun \"{downloadClientName}\" kanssa. {errorMessage}.",
"AnimeEpisodeFormat": "Animejaksojen kaava", "AnimeEpisodeFormat": "Animejaksojen kaava",
"CheckDownloadClientForDetails": "katso lisätietoja latauspalvelusta", "CheckDownloadClientForDetails": "katso lisätietoja latauspalvelusta",
"Donations": "Lahjoitukset", "Donations": "Lahjoitukset",
@@ -142,7 +142,7 @@
"BypassDelayIfHighestQuality": "Ohita, jos korkein laatu", "BypassDelayIfHighestQuality": "Ohita, jos korkein laatu",
"CancelPendingTask": "Haluatko varmasti perua odottavan tehtävän?", "CancelPendingTask": "Haluatko varmasti perua odottavan tehtävän?",
"Clear": "Tyhjennä", "Clear": "Tyhjennä",
"CollectionsLoadError": "Kokoelmien lataus epäonnistui", "CollectionsLoadError": "Virhe ladattaessa kokoelmia.",
"CreateEmptySeriesFolders": "Luo sarjoille tyhjät kansiot", "CreateEmptySeriesFolders": "Luo sarjoille tyhjät kansiot",
"CreateEmptySeriesFoldersHelpText": "Luo puuttuvat sarjakansiot kirjastotarkistusten yhteydessä.", "CreateEmptySeriesFoldersHelpText": "Luo puuttuvat sarjakansiot kirjastotarkistusten yhteydessä.",
"CustomFormatsLoadError": "Mukautettujen muotojen lataus epäonnistui", "CustomFormatsLoadError": "Mukautettujen muotojen lataus epäonnistui",
@@ -199,7 +199,7 @@
"IndexerSettingsSeedRatio": "Jakosuhde", "IndexerSettingsSeedRatio": "Jakosuhde",
"IndexerSettingsWebsiteUrl": "Verkkosivuston URL", "IndexerSettingsWebsiteUrl": "Verkkosivuston URL",
"IndexerValidationInvalidApiKey": "Rajapinnan avain ei kelpaa", "IndexerValidationInvalidApiKey": "Rajapinnan avain ei kelpaa",
"IndexersLoadError": "Tietolähteiden lataus epäonnistui", "IndexersLoadError": "Virhe ladattaessa hakupalveluita.",
"IndexersSettingsSummary": "Hakupalvelut ja julkaisurajoitukset.", "IndexersSettingsSummary": "Hakupalvelut ja julkaisurajoitukset.",
"Indexers": "Tietolähteet", "Indexers": "Tietolähteet",
"KeyboardShortcutsFocusSearchBox": "Kohdista hakukenttä", "KeyboardShortcutsFocusSearchBox": "Kohdista hakukenttä",
@@ -219,7 +219,7 @@
"Missing": "Puuttuu", "Missing": "Puuttuu",
"MonitorMissingEpisodes": "Puuttuvat jaksot", "MonitorMissingEpisodes": "Puuttuvat jaksot",
"MissingEpisodes": "Puuttuvia jaksoja", "MissingEpisodes": "Puuttuvia jaksoja",
"MonitorNewSeasons": "Valvo uusia kausia", "MonitorNewSeasons": "Uusien kausien valvonta",
"MonitorLastSeasonDescription": "Valvo kaikkia viimeisen kauden jaksoja.", "MonitorLastSeasonDescription": "Valvo kaikkia viimeisen kauden jaksoja.",
"MonitorNewSeasonsHelpText": "Uusien kausien automaattivalvonnan käytäntö.", "MonitorNewSeasonsHelpText": "Uusien kausien automaattivalvonnan käytäntö.",
"MoveSeriesFoldersToRootFolder": "Haluatko siirtää sarjakansiot kohteeseen \"{destinationRootFolder}\"?", "MoveSeriesFoldersToRootFolder": "Haluatko siirtää sarjakansiot kohteeseen \"{destinationRootFolder}\"?",
@@ -239,7 +239,7 @@
"ReleaseSceneIndicatorUnknownSeries": "Tuntematon jakso tai sarja.", "ReleaseSceneIndicatorUnknownSeries": "Tuntematon jakso tai sarja.",
"RemotePathMappingFilesBadDockerPathHealthCheckMessage": "Käytät Dockeria ja latauspalvelu {downloadClientName} ilmoitti tiedostosijainniksi \"{path}\", mutta se ei ole kelvollinen {osName}-sijainti. Tarkista etäsijaintien kohdistukset ja latauspalvelun asetukset.", "RemotePathMappingFilesBadDockerPathHealthCheckMessage": "Käytät Dockeria ja latauspalvelu {downloadClientName} ilmoitti tiedostosijainniksi \"{path}\", mutta se ei ole kelvollinen {osName}-sijainti. Tarkista etäsijaintien kohdistukset ja latauspalvelun asetukset.",
"RemoveCompletedDownloads": "Poista valmistuneet lataukset", "RemoveCompletedDownloads": "Poista valmistuneet lataukset",
"RemotePathMappingsLoadError": "Etäsijaintien kohdistusten lataus epäonnistui", "RemotePathMappingsLoadError": "Virhe ladattaessa etäsijaintien kohdistuksia.",
"RemoveFailedDownloads": "Poista epäonnistuneet lataukset", "RemoveFailedDownloads": "Poista epäonnistuneet lataukset",
"RemoveFailed": "Poisto epäonnistui", "RemoveFailed": "Poisto epäonnistui",
"RemoveFromBlocklist": "Poista estolistalta", "RemoveFromBlocklist": "Poista estolistalta",
@@ -247,7 +247,7 @@
"RemoveFromQueue": "Poista jonosta", "RemoveFromQueue": "Poista jonosta",
"RenameEpisodesHelpText": "Jos uudelleennimeäminen ei ole käytössä, {appName} käyttää nykyistä tiedostonimeä.", "RenameEpisodesHelpText": "Jos uudelleennimeäminen ei ole käytössä, {appName} käyttää nykyistä tiedostonimeä.",
"RestoreBackup": "Palauta varmuuskopio", "RestoreBackup": "Palauta varmuuskopio",
"RestrictionsLoadError": "Rajoitusten lataus epäonnistui", "RestrictionsLoadError": "Virhe ladattaessa rajoituksia.",
"SceneInfo": "Kohtaustiedot", "SceneInfo": "Kohtaustiedot",
"SceneInformation": "Kohtaustiedot", "SceneInformation": "Kohtaustiedot",
"SelectFolderModalTitle": "{modalTitle} Valitse kansio", "SelectFolderModalTitle": "{modalTitle} Valitse kansio",
@@ -271,7 +271,7 @@
"OnLatestVersion": "Uusin {appName}-versio on jo asennettu", "OnLatestVersion": "Uusin {appName}-versio on jo asennettu",
"OnSeriesDelete": "Kun sarja poistetaan", "OnSeriesDelete": "Kun sarja poistetaan",
"PrioritySettings": "Painotus: {priority}", "PrioritySettings": "Painotus: {priority}",
"QualitiesLoadError": "Laatujen lataus epäonnistui", "QualitiesLoadError": "Virhe ladattaessa laatuja.",
"QualityProfiles": "Laatuprofiilit", "QualityProfiles": "Laatuprofiilit",
"QualityProfileInUseSeriesListCollection": "Sarjaan, listaan tai kokoelmaan liitettyä laatuprofiilia ei ole mahdollista poistaa.", "QualityProfileInUseSeriesListCollection": "Sarjaan, listaan tai kokoelmaan liitettyä laatuprofiilia ei ole mahdollista poistaa.",
"ReadTheWikiForMoreInformation": "Wikistä löydät lisää tietoja", "ReadTheWikiForMoreInformation": "Wikistä löydät lisää tietoja",
@@ -320,11 +320,11 @@
"StandardEpisodeTypeFormat": "Kausien ja jaksojen numerointi ({format})", "StandardEpisodeTypeFormat": "Kausien ja jaksojen numerointi ({format})",
"StartupDirectory": "Käynnistyskansio", "StartupDirectory": "Käynnistyskansio",
"Started": "Alkoi", "Started": "Alkoi",
"SupportedIndexersMoreInfo": "Saat tietoja yksittäisistä palveluista painamalla niiden ohessa olevia lisätietopainikkeita.", "SupportedIndexersMoreInfo": "Saat lisätietoja yksittäisistä palveluista niiden ohessa olevilla painikkeilla.",
"Status": "Tila", "Status": "Tila",
"SupportedListsSeries": "{appName} tukee useita listoja, joiden avulla sarjoja voidaan tuoda tietokantaan.", "SupportedListsSeries": "{appName} tukee useita listoja, joiden avulla sarjoja voidaan tuoda tietokantaan.",
"SystemTimeHealthCheckMessage": "Järjestelmän aika on ainakin vuorokauden pielessä, eivätkä ajoitetut tehtävät toimi oikein ennen kuin se on korjattu.", "SystemTimeHealthCheckMessage": "Järjestelmän aika on ainakin vuorokauden pielessä, eivätkä ajoitetut tehtävät toimi oikein ennen kuin se on korjattu.",
"TagsLoadError": "Tunnisteiden lataus epäonnistui", "TagsLoadError": "Virhe ladattaessa tunnisteita.",
"TagsSettingsSummary": "Täältä näet kaikki tunnisteet käyttökohteineen ja voit poistaa käyttämättömät tunnisteet.", "TagsSettingsSummary": "Täältä näet kaikki tunnisteet käyttökohteineen ja voit poistaa käyttämättömät tunnisteet.",
"Tomorrow": "Huomenna", "Tomorrow": "Huomenna",
"TestParsing": "Testaa jäsennystä", "TestParsing": "Testaa jäsennystä",
@@ -429,7 +429,7 @@
"EditDownloadClientImplementation": "Muokataan latauspalvelua {implementationName}", "EditDownloadClientImplementation": "Muokataan latauspalvelua {implementationName}",
"EditImportListImplementation": "Muokataan tuontilistaa {implementationName}", "EditImportListImplementation": "Muokataan tuontilistaa {implementationName}",
"EndedOnly": "Vain päättyneet", "EndedOnly": "Vain päättyneet",
"EnableInteractiveSearchHelpTextWarning": "Tämä hakupalvelu ei tue hakua.", "EnableInteractiveSearchHelpTextWarning": "Tämä hakupalvelu ei tue hakutoimintoa.",
"Episode": "Jakso", "Episode": "Jakso",
"EpisodeCount": "Jaksomäärä", "EpisodeCount": "Jaksomäärä",
"EpisodeAirDate": "Jakson esitysaika", "EpisodeAirDate": "Jakson esitysaika",
@@ -458,10 +458,10 @@
"IndexerSettingsSeedRatioHelpText": "Suhde, joka torrentin tulee saavuttaa ennen sen pysäytystä. Käytä latauspalvelun oletusta jättämällä tyhjäksi. Suhteen tulisi olla ainakin 1.0 ja noudattaa tietolähteen sääntöjä.", "IndexerSettingsSeedRatioHelpText": "Suhde, joka torrentin tulee saavuttaa ennen sen pysäytystä. Käytä latauspalvelun oletusta jättämällä tyhjäksi. Suhteen tulisi olla ainakin 1.0 ja noudattaa tietolähteen sääntöjä.",
"IndexerStatusAllUnavailableHealthCheckMessage": "Tietolähteet eivät ole käytettävissä virheiden vuoksi", "IndexerStatusAllUnavailableHealthCheckMessage": "Tietolähteet eivät ole käytettävissä virheiden vuoksi",
"LibraryImportTipsDontUseDownloadsFolder": "Älä käytä tätä latausten tuontiin latauspalvelulta. Tämä on tarkoitettu vain olemassa olevien ja järjestettyjen kirjastojen, eikä lajittelemattomien tiedostojen tuontiin.", "LibraryImportTipsDontUseDownloadsFolder": "Älä käytä tätä latausten tuontiin latauspalvelulta. Tämä on tarkoitettu vain olemassa olevien ja järjestettyjen kirjastojen, eikä lajittelemattomien tiedostojen tuontiin.",
"LibraryImportTips": "Muutama vinkki, joilla homma sujuu:", "LibraryImportTips": "Muutama vinkki, joiden avulla homma sujuu:",
"ListWillRefreshEveryInterval": "Lista päivittyy {refreshInterval} välein", "ListWillRefreshEveryInterval": "Lista päivittyy {refreshInterval} välein",
"ListExclusionsLoadError": "Listapoikkeusten lataus epäonnistui", "ListExclusionsLoadError": "Virhe ladattaessa listapoikkeuksia.",
"ManualImportItemsLoadError": "Manuaalituonnin kohteiden lataus epäonnistui", "ManualImportItemsLoadError": "Virhe ladattaessa manuaalisesti tuotavia kohteita.",
"MediaManagementSettingsSummary": "Tiedostojen nimeämis- ja hallinta-asetukset, sekä kirjaston juurikansiot.", "MediaManagementSettingsSummary": "Tiedostojen nimeämis- ja hallinta-asetukset, sekä kirjaston juurikansiot.",
"Message": "Viesti", "Message": "Viesti",
"MetadataSettings": "Metatietoasetukset", "MetadataSettings": "Metatietoasetukset",
@@ -483,7 +483,7 @@
"MonitorPilotEpisodeDescription": "Valvo vain ensimmäisen kauden ensimmäistä jaksoa.", "MonitorPilotEpisodeDescription": "Valvo vain ensimmäisen kauden ensimmäistä jaksoa.",
"Name": "Nimi", "Name": "Nimi",
"NamingSettings": "Nimeämisasetukset", "NamingSettings": "Nimeämisasetukset",
"NoEpisodeHistory": "Jaksohistoriaa ei ole", "NoEpisodeHistory": "Jaksolle ei ole historiaa.",
"DeleteSeriesFolderEpisodeCount": "{episodeFileCount} jaksotiedostoa, kooltaan yhteensä {size}.", "DeleteSeriesFolderEpisodeCount": "{episodeFileCount} jaksotiedostoa, kooltaan yhteensä {size}.",
"DeleteSeriesFolderCountWithFilesConfirmation": "Haluatko varmasti poistaa {count} valittua sarjaa ja niiden kaiken sisällön?", "DeleteSeriesFolderCountWithFilesConfirmation": "Haluatko varmasti poistaa {count} valittua sarjaa ja niiden kaiken sisällön?",
"DeleteSeriesFoldersHelpText": "Poista sarjakansiot ja niiden kaikki sisältö.", "DeleteSeriesFoldersHelpText": "Poista sarjakansiot ja niiden kaikki sisältö.",
@@ -559,7 +559,7 @@
"Tags": "Tunnisteet", "Tags": "Tunnisteet",
"ToggleUnmonitoredToMonitored": "Ei valvota (aloita painamalla)", "ToggleUnmonitoredToMonitored": "Ei valvota (aloita painamalla)",
"TheLogLevelDefault": "Lokikirjauksen oletusarvoinen laajuus on \"Informatiivinen\". Laajuutta voidaan muuttaa [Yleisistä asetuksista](/settings/general).", "TheLogLevelDefault": "Lokikirjauksen oletusarvoinen laajuus on \"Informatiivinen\". Laajuutta voidaan muuttaa [Yleisistä asetuksista](/settings/general).",
"UiSettingsLoadError": "Käyttöliittymäasetusten lataus epäonnistui", "UiSettingsLoadError": "Virhe ladattaessa käyttöliittymäasetuksia.",
"UnableToUpdateSonarrDirectly": "{appName}ia ei voida päivittää suoraan,", "UnableToUpdateSonarrDirectly": "{appName}ia ei voida päivittää suoraan,",
"UnmonitoredOnly": "Vain valvomattomat", "UnmonitoredOnly": "Vain valvomattomat",
"UnmonitorDeletedEpisodes": "Lopeta poistettujen jaksojen valvonta", "UnmonitorDeletedEpisodes": "Lopeta poistettujen jaksojen valvonta",
@@ -568,7 +568,7 @@
"UpdateAll": "Päivitä kaikki", "UpdateAll": "Päivitä kaikki",
"UpcomingSeriesDescription": "Sarja on julkistettu, mutta tarkka esitysaika ei ole vielä tiedossa.", "UpcomingSeriesDescription": "Sarja on julkistettu, mutta tarkka esitysaika ei ole vielä tiedossa.",
"UnselectAll": "Tyhjennä valinnat", "UnselectAll": "Tyhjennä valinnat",
"UpdateMonitoring": "Vaihda valvontatilaa", "UpdateMonitoring": "Muuta valvontaa",
"UpdateAppDirectlyLoadError": "{appName}ia ei voida päivittää suoraan,", "UpdateAppDirectlyLoadError": "{appName}ia ei voida päivittää suoraan,",
"UpgradeUntilThisQualityIsMetOrExceeded": "Päivitä kunnes tämä laatu on savutettu tai ylitetty", "UpgradeUntilThisQualityIsMetOrExceeded": "Päivitä kunnes tämä laatu on savutettu tai ylitetty",
"Updates": "Päivitykset", "Updates": "Päivitykset",
@@ -583,7 +583,7 @@
"Wanted": "Halutut", "Wanted": "Halutut",
"Warn": "Varoita", "Warn": "Varoita",
"AirsDateAtTimeOn": "{date} klo {time} kanavalla {networkLabel}", "AirsDateAtTimeOn": "{date} klo {time} kanavalla {networkLabel}",
"AirsTbaOn": "TBA kanavalla {networkLabel}", "AirsTbaOn": "Tulossa kanavalle {networkLabel}",
"AirsTimeOn": "{time} kanavalla {networkLabel}", "AirsTimeOn": "{time} kanavalla {networkLabel}",
"DownloadClientDownloadStationValidationFolderMissing": "Kansiota ei ole olemassa", "DownloadClientDownloadStationValidationFolderMissing": "Kansiota ei ole olemassa",
"DownloadClientDownloadStationValidationNoDefaultDestination": "Oletussijaintia ei ole", "DownloadClientDownloadStationValidationNoDefaultDestination": "Oletussijaintia ei ole",
@@ -614,18 +614,18 @@
"EnableSslHelpText": "Käyttöönotto vaatii in uudelleenkäynnistyksen järjestelmänvalvojan oikeuksilla.", "EnableSslHelpText": "Käyttöönotto vaatii in uudelleenkäynnistyksen järjestelmänvalvojan oikeuksilla.",
"EpisodeFileRenamedTooltip": "Jaksotiedosto nimettiin uudelleen", "EpisodeFileRenamedTooltip": "Jaksotiedosto nimettiin uudelleen",
"EpisodeInfo": "Jakson tiedot", "EpisodeInfo": "Jakson tiedot",
"EpisodeFilesLoadError": "Jaksotiedostojen lataus epäonnistui", "EpisodeFilesLoadError": "Virhe ladattaessa jaksotiedostoja.",
"EpisodeIsNotMonitored": "Jaksoa ei valvota", "EpisodeIsNotMonitored": "Jaksoa ei valvota",
"EpisodeIsDownloading": "Jaksoa ladataan", "EpisodeIsDownloading": "Jaksoa ladataan",
"EpisodeMissingFromDisk": "Jaksoa ei ole levyllä", "EpisodeMissingFromDisk": "Jaksoa ei ole levyllä",
"EpisodeSearchResultsLoadError": "Tämän jaksohaun tulosten lataus epäonnistui. Yritä myöhemmin uudelleen.", "EpisodeSearchResultsLoadError": "Virhe ladattaessa tämän jaksohaun tuloksia. Yritä myöhemmin uudelleen.",
"EpisodeTitle": "Jakson nimi", "EpisodeTitle": "Jakson nimi",
"EpisodeTitleRequired": "Jakson nimi on pakollinen.", "EpisodeTitleRequired": "Jakson nimi on pakollinen.",
"Episodes": "Jaksot", "Episodes": "Jaksot",
"ErrorLoadingContents": "Virhe ladattaessa sisältöjä", "ErrorLoadingContents": "Virhe ladattaessa sisältöjä",
"EpisodesLoadError": "Jaksojen lataus epäonnistui", "EpisodesLoadError": "Virhe ladattaessa jaksoja.",
"ErrorLoadingContent": "Virhe ladattaessa tätä sisältöä", "ErrorLoadingContent": "Virhe ladattaessa tätä sisältöä",
"FailedToLoadCustomFiltersFromApi": "Suodatinmukautusten lataus rajapinnasta epäonnistui", "FailedToLoadCustomFiltersFromApi": "Omien suodattimien lataus rajapinnalta epäonnistui.",
"FailedToLoadQualityProfilesFromApi": "Laatuprofiilien lataus rajapinnasta epäonnistui", "FailedToLoadQualityProfilesFromApi": "Laatuprofiilien lataus rajapinnasta epäonnistui",
"CalendarFeed": "{appName}in kalenterisyöte", "CalendarFeed": "{appName}in kalenterisyöte",
"Agenda": "Agenda", "Agenda": "Agenda",
@@ -681,8 +681,8 @@
"Quality": "Laatu", "Quality": "Laatu",
"PortNumber": "Portin numero", "PortNumber": "Portin numero",
"QualitySettings": "Laatuasetukset", "QualitySettings": "Laatuasetukset",
"QuickSearch": "Pikahaku", "QuickSearch": "Etsi automaattisesti",
"QualityProfilesLoadError": "Laatuprofiilien lataus epäonnistui", "QualityProfilesLoadError": "Virhe ladattaessa laatuprofiileja.",
"SeriesDetailsCountEpisodeFiles": "{episodeFileCount} jaksotiedostoa", "SeriesDetailsCountEpisodeFiles": "{episodeFileCount} jaksotiedostoa",
"SeriesEditor": "Sarjojen muokkaus", "SeriesEditor": "Sarjojen muokkaus",
"SeriesIndexFooterMissingUnmonitored": "Jaksoja puuttuu (sarjaa ei valvota)", "SeriesIndexFooterMissingUnmonitored": "Jaksoja puuttuu (sarjaa ei valvota)",
@@ -744,9 +744,9 @@
"SeriesDetailsGoTo": "Avaa {title}", "SeriesDetailsGoTo": "Avaa {title}",
"SeriesEditRootFolderHelpText": "Siirtämällä sarjat samaan juurikansioon voidaan niiden kansioiden nimet päivittää vastaamaan päivittynyttä nimikettä tai nimeämiskaavaa.", "SeriesEditRootFolderHelpText": "Siirtämällä sarjat samaan juurikansioon voidaan niiden kansioiden nimet päivittää vastaamaan päivittynyttä nimikettä tai nimeämiskaavaa.",
"WouldYouLikeToRestoreBackup": "Haluatko palauttaa varmuuskopion \"{name}\"?", "WouldYouLikeToRestoreBackup": "Haluatko palauttaa varmuuskopion \"{name}\"?",
"SeriesLoadError": "Sarjojen lataus epäonnistui", "SeriesLoadError": "Virhe ladattaessa sarjoja.",
"IconForCutoffUnmetHelpText": "Näytä kuvake tiedostoille, joiden määritettyä katkaisutasoa ei ole vielä saavutettu.", "IconForCutoffUnmetHelpText": "Näytä kuvake tiedostoille, joiden määritettyä katkaisutasoa ei ole vielä saavutettu.",
"DownloadClientOptionsLoadError": "Latauspalveluasetusten lataus epäonnistui", "DownloadClientOptionsLoadError": "Virhe ladattaessa latauspalveluasetuksia.",
"UseHardlinksInsteadOfCopy": "Käytä hardlink-kytköksiä", "UseHardlinksInsteadOfCopy": "Käytä hardlink-kytköksiä",
"TorrentBlackholeSaveMagnetFilesReadOnlyHelpText": "Tiedostojen siirron sijaan tämä ohjaa {appName}in kopioimaan tiedostot tai käyttämään hardlink-kytköksiä (asetuksista/järjestelmästä riippuen).", "TorrentBlackholeSaveMagnetFilesReadOnlyHelpText": "Tiedostojen siirron sijaan tämä ohjaa {appName}in kopioimaan tiedostot tai käyttämään hardlink-kytköksiä (asetuksista/järjestelmästä riippuen).",
"IndexerValidationUnableToConnectHttpError": "Hakupalveluun ei voitu muodostaa yhteyttä. Tarkista DNS-asetukset ja varmista, että IPv6 toimii tai on poistettu käytöstä. {exceptionMessage}.", "IndexerValidationUnableToConnectHttpError": "Hakupalveluun ei voitu muodostaa yhteyttä. Tarkista DNS-asetukset ja varmista, että IPv6 toimii tai on poistettu käytöstä. {exceptionMessage}.",
@@ -762,7 +762,7 @@
"MonitoringOptions": "Valvonta-asetukset", "MonitoringOptions": "Valvonta-asetukset",
"MonitoredOnly": "Vain valvotut", "MonitoredOnly": "Vain valvotut",
"MonitorSelected": "Valvo valittuja", "MonitorSelected": "Valvo valittuja",
"MonitorSeries": "Valvo sarjaa", "MonitorSeries": "Sarjojen valvonta",
"New": "Uutta", "New": "Uutta",
"NoHistoryFound": "Historiaa ei löytynyt", "NoHistoryFound": "Historiaa ei löytynyt",
"NoEpisodesInThisSeason": "Kaudelle ei ole jaksoja", "NoEpisodesInThisSeason": "Kaudelle ei ole jaksoja",
@@ -888,7 +888,7 @@
"EnableAutomaticSearch": "Käytä automaattihakua", "EnableAutomaticSearch": "Käytä automaattihakua",
"EndedSeriesDescription": "Uusia jaksoja tai kausia ei tiettävästi ole tulossa", "EndedSeriesDescription": "Uusia jaksoja tai kausia ei tiettävästi ole tulossa",
"EditSelectedSeries": "Muokkaa valittuja sarjoja", "EditSelectedSeries": "Muokkaa valittuja sarjoja",
"EpisodeHistoryLoadError": "Jaksohistorian lataus epäonnistui", "EpisodeHistoryLoadError": "Virhe ladattaessa jakson historiatietoja.",
"Ended": "Päättynyt", "Ended": "Päättynyt",
"ExistingSeries": "Olemassa olevat sarjat", "ExistingSeries": "Olemassa olevat sarjat",
"FreeSpace": "Vapaa tila", "FreeSpace": "Vapaa tila",
@@ -918,7 +918,7 @@
"Logout": "Kirjaudu ulos", "Logout": "Kirjaudu ulos",
"IndexerSettings": "Tietolähdeasetukset", "IndexerSettings": "Tietolähdeasetukset",
"IncludeHealthWarnings": "Sisällytä kuntovaroitukset", "IncludeHealthWarnings": "Sisällytä kuntovaroitukset",
"ListsLoadError": "Listojen lataus epäonnistui", "ListsLoadError": "Virhe ladattaessa listoja.",
"IndexerValidationUnableToConnect": "Hakupalveluun ei voitu muodostaa yhteyttä: {exceptionMessage}. Etsi tietoja tämän virheen lähellä olevista lokimerkinnöistä.", "IndexerValidationUnableToConnect": "Hakupalveluun ei voitu muodostaa yhteyttä: {exceptionMessage}. Etsi tietoja tämän virheen lähellä olevista lokimerkinnöistä.",
"MetadataSettingsSeriesSummary": "Luo metatietotiedostot kun jaksoja tuodaan tai sarjojen tietoja päivitetään.", "MetadataSettingsSeriesSummary": "Luo metatietotiedostot kun jaksoja tuodaan tai sarjojen tietoja päivitetään.",
"MassSearchCancelWarning": "Tämä on mahdollista keskeyttää vain käynnistämällä {appName} uudelleen tai poistamalla kaikki tietolähteet käytöstä.", "MassSearchCancelWarning": "Tämä on mahdollista keskeyttää vain käynnistämällä {appName} uudelleen tai poistamalla kaikki tietolähteet käytöstä.",
@@ -941,7 +941,7 @@
"OrganizeLoadError": "Virhe ladattaessa esikatseluita", "OrganizeLoadError": "Virhe ladattaessa esikatseluita",
"QualityCutoffNotMet": "Laadun katkaisutasoa ei ole saavutettu", "QualityCutoffNotMet": "Laadun katkaisutasoa ei ole saavutettu",
"ProtocolHelpText": "Valitse käytettävä(t) protokolla(t) ja mitä käytetään ensisijaisesti valittaessa muutoin tasaveroisista julkaisuista.", "ProtocolHelpText": "Valitse käytettävä(t) protokolla(t) ja mitä käytetään ensisijaisesti valittaessa muutoin tasaveroisista julkaisuista.",
"QualityDefinitionsLoadError": "Laatumääritysten lataus epäonnistui", "QualityDefinitionsLoadError": "Virhe ladattaessa laatumäärityksiä.",
"RemotePathMappingLocalWrongOSPathHealthCheckMessage": "Paikallinen latauspalvelu {downloadClientName} tallentaa lataukset kohteeseen \"{path}\", mutta se ei ole kelvollinen {osName}-sijainti. Tarkista latauspalvelun asetukset.", "RemotePathMappingLocalWrongOSPathHealthCheckMessage": "Paikallinen latauspalvelu {downloadClientName} tallentaa lataukset kohteeseen \"{path}\", mutta se ei ole kelvollinen {osName}-sijainti. Tarkista latauspalvelun asetukset.",
"RemotePathMappingFilesLocalWrongOSPathHealthCheckMessage": "Paikallinen latauspalvelu {downloadClientName} ilmoitti tiedostosijainniksi \"{path}\", mutta se ei ole kelvollinen {osName}-sijainti. Tarkista latauspalvelun asetukset.", "RemotePathMappingFilesLocalWrongOSPathHealthCheckMessage": "Paikallinen latauspalvelu {downloadClientName} ilmoitti tiedostosijainniksi \"{path}\", mutta se ei ole kelvollinen {osName}-sijainti. Tarkista latauspalvelun asetukset.",
"RemoveDownloadsAlert": "Poistoasetukset on siirretty yllä olevan taulukon latauspalvelukohtaisiin asetuksiin.", "RemoveDownloadsAlert": "Poistoasetukset on siirretty yllä olevan taulukon latauspalvelukohtaisiin asetuksiin.",
@@ -965,7 +965,7 @@
"LogLevelTraceHelpTextWarning": "Jäljityskirjausta tulee käyttää vain tilapäisesti.", "LogLevelTraceHelpTextWarning": "Jäljityskirjausta tulee käyttää vain tilapäisesti.",
"ListTagsHelpText": "Tunnisteet, joilla tältä tuontilistalta lisätyt kohteet merkitään.", "ListTagsHelpText": "Tunnisteet, joilla tältä tuontilistalta lisätyt kohteet merkitään.",
"ManageEpisodes": "Jaksojen hallinta", "ManageEpisodes": "Jaksojen hallinta",
"ManageEpisodesSeason": "Hallitse tuotantokauden jaksotiedostoja", "ManageEpisodesSeason": "Tuotantokauden jaksotiedostojen hallinta",
"ManageIndexers": "Hallitse tietolähteitä", "ManageIndexers": "Hallitse tietolähteitä",
"LocalPath": "Paikallinen sijainti", "LocalPath": "Paikallinen sijainti",
"NoChanges": "Muutoksia ei ole", "NoChanges": "Muutoksia ei ole",
@@ -1011,7 +1011,7 @@
"Seasons": "Kaudet", "Seasons": "Kaudet",
"SearchAll": "Etsi kaikkia", "SearchAll": "Etsi kaikkia",
"SearchByTvdbId": "Voit etsiä myös sarjojen TheTVDB-tunnisteilla (esim. \"tvdb:71663\").", "SearchByTvdbId": "Voit etsiä myös sarjojen TheTVDB-tunnisteilla (esim. \"tvdb:71663\").",
"RootFoldersLoadError": "Juurikansioiden lataus epäonnistui", "RootFoldersLoadError": "Virhe ladattaessa juurikansioita.",
"SearchFailedError": "Haku epäonnistui. Yritä myöhemmin uudelleen.", "SearchFailedError": "Haku epäonnistui. Yritä myöhemmin uudelleen.",
"Year": "Vuosi", "Year": "Vuosi",
"WeekColumnHeader": "Viikkosarakkeen otsikko", "WeekColumnHeader": "Viikkosarakkeen otsikko",
@@ -1029,7 +1029,7 @@
"MonitoredEpisodesHelpText": "Lataa tämän sarjan valvotut jaksot.", "MonitoredEpisodesHelpText": "Lataa tämän sarjan valvotut jaksot.",
"MoveSeriesFoldersDontMoveFiles": "En, siirrän tiedostot itse", "MoveSeriesFoldersDontMoveFiles": "En, siirrän tiedostot itse",
"MoveSeriesFoldersMoveFiles": "Kyllä, siirrä tiedostot", "MoveSeriesFoldersMoveFiles": "Kyllä, siirrä tiedostot",
"MonitorNewItems": "Valvo uusia kohteita", "MonitorNewItems": "Uusien kausien valvonta",
"MonitorSpecialEpisodesDescription": "Valvo kaikkia erikoisjaksoja muuttamatta muiden jaksojen tilaa.", "MonitorSpecialEpisodesDescription": "Valvo kaikkia erikoisjaksoja muuttamatta muiden jaksojen tilaa.",
"MonitorNoNewSeasons": "Ei uusia kausia", "MonitorNoNewSeasons": "Ei uusia kausia",
"OpenSeries": "Avaa sarja", "OpenSeries": "Avaa sarja",
@@ -1040,7 +1040,7 @@
"DeleteCondition": "Poista ehto", "DeleteCondition": "Poista ehto",
"Delete": "Poista", "Delete": "Poista",
"ApiKey": "Rajapinnan avain", "ApiKey": "Rajapinnan avain",
"CertificateValidationHelpText": "Määritä HTTPS-varmennevahvistuksen tiukkuus. Älä muta, jos et ymmärrä riskejä.", "CertificateValidationHelpText": "Määritä HTTPS-varmennevahvistuksen tiukkuus. Älä muuta, jos et ymmärrä riskejä.",
"Certification": "Varmennus", "Certification": "Varmennus",
"ChangeFileDate": "Muuta tiedoston päiväys", "ChangeFileDate": "Muuta tiedoston päiväys",
"DelayingDownloadUntil": "Lataus on lykätty alkamaan {date} klo {time}", "DelayingDownloadUntil": "Lataus on lykätty alkamaan {date} klo {time}",
@@ -1310,8 +1310,8 @@
"Ok": "Ok", "Ok": "Ok",
"General": "Yleiset", "General": "Yleiset",
"Folders": "Kansiot", "Folders": "Kansiot",
"IndexerRssNoIndexersAvailableHealthCheckMessage": "RSS-syötteitä tukevat hakupalvelut eivät ole hiljattaisten hakupalveluvirheiden vuoksi tilapäisesti käytettävissä.", "IndexerRssNoIndexersAvailableHealthCheckMessage": "RSS-syötteitä tukevat hakupalvelut eivät ole tilapäisesti käytettävissä hiljattaisten palveluvirheiden vuoksi.",
"IndexerSearchNoAvailableIndexersHealthCheckMessage": "Hakua tukevat hakupalvelut eivät ole hiljattaisten hakupalveluvirheiden vuoksi tilapäisesti käytettävissä.", "IndexerSearchNoAvailableIndexersHealthCheckMessage": "Mitkään hakua tukevat hakupalvelut eivät ole tilapäisesti käytettävissä hiljattaisten palveluvirheiden vuoksi.",
"IndexerSettingsCategoriesHelpText": "Pudotusvalikko. Poista vakio-/päivittäissarjat käytöstä jättämällä tyhjäksi.", "IndexerSettingsCategoriesHelpText": "Pudotusvalikko. Poista vakio-/päivittäissarjat käytöstä jättämällä tyhjäksi.",
"IndexerSettingsSeasonPackSeedTime": "Kausikoosteiden jakoaika", "IndexerSettingsSeasonPackSeedTime": "Kausikoosteiden jakoaika",
"KeyboardShortcutsSaveSettings": "Tallenna asetukset", "KeyboardShortcutsSaveSettings": "Tallenna asetukset",
@@ -1455,7 +1455,7 @@
"HealthMessagesInfoBox": "Saat lisätietoja näiden vakausviestien syistä painamalla rivin lopussa olevaa wikilinkkiä (kirjakuvake) tai tarkastelemalla [lokitietoja]({link}). Mikäli et osaa tulkita näitä viestejä, tavoitat tukemme alla olevilla linkeillä.", "HealthMessagesInfoBox": "Saat lisätietoja näiden vakausviestien syistä painamalla rivin lopussa olevaa wikilinkkiä (kirjakuvake) tai tarkastelemalla [lokitietoja]({link}). Mikäli et osaa tulkita näitä viestejä, tavoitat tukemme alla olevilla linkeillä.",
"MegabytesPerMinute": "Megatavua minuutissa", "MegabytesPerMinute": "Megatavua minuutissa",
"MustContain": "Täytyy sisältää", "MustContain": "Täytyy sisältää",
"NoLinks": "Linkkejä ei ole", "NoLinks": "Kytköksiä ei ole",
"Proxy": "Välityspalvelin", "Proxy": "Välityspalvelin",
"ProxyUsernameHelpText": "Käyttäjätunnus ja salasana tulee täyttää vain tarvittaessa. Mikäli näitä ei ole, tulee kentät jättää tyhjiksi.", "ProxyUsernameHelpText": "Käyttäjätunnus ja salasana tulee täyttää vain tarvittaessa. Mikäli näitä ei ole, tulee kentät jättää tyhjiksi.",
"ImportListsSettingsSummary": "Sisällön tuonti muista {appName}-instansseista tai palveluista, ja poikkeuslistojen hallinta.", "ImportListsSettingsSummary": "Sisällön tuonti muista {appName}-instansseista tai palveluista, ja poikkeuslistojen hallinta.",
@@ -1619,8 +1619,8 @@
"NotificationsTelegramSettingsChatIdHelpText": "Vastaanottaaksesi viestejä, sinun on aloitettava keskustelu botin kanssa tai lisättävä se ryhmääsi.", "NotificationsTelegramSettingsChatIdHelpText": "Vastaanottaaksesi viestejä, sinun on aloitettava keskustelu botin kanssa tai lisättävä se ryhmääsi.",
"NotificationsTraktSettingsAccessToken": "Käyttötunniste", "NotificationsTraktSettingsAccessToken": "Käyttötunniste",
"NotificationsTraktSettingsAuthUser": "Todennettu käyttäjä", "NotificationsTraktSettingsAuthUser": "Todennettu käyttäjä",
"NotificationsValidationUnableToSendTestMessage": "Testiviestin lähetys ei onnistu: {exceptionMessage}", "NotificationsValidationUnableToSendTestMessage": "Virhe lähetettäessä testiviestiä: {exceptionMessage}.",
"NotificationsValidationUnableToSendTestMessageApiResponse": "Testiviestin lähetys ei onnistu. API vastasi: {error}", "NotificationsValidationUnableToSendTestMessageApiResponse": "Virhe lähetettäessä testiviestiä. API vastasi: {error}.",
"NotificationsEmailSettingsUseEncryption": "Käytä salausta", "NotificationsEmailSettingsUseEncryption": "Käytä salausta",
"ParseModalHelpTextDetails": "{appName} pyrkii jäsentämään nimen ja näyttämään sen tiedot.", "ParseModalHelpTextDetails": "{appName} pyrkii jäsentämään nimen ja näyttämään sen tiedot.",
"ImportScriptPathHelpText": "Tuontiin käytettävän komentosarjan sijainti.", "ImportScriptPathHelpText": "Tuontiin käytettävän komentosarjan sijainti.",
@@ -1628,7 +1628,7 @@
"NotificationsEmailSettingsUseEncryptionHelpText": "Määrittää suositaanko salausta, jos se on määritetty palvelimelle, käytetäänkö aina SSL- (vain portti 465) tai StartTLS-salausta (kaikki muut portit), voi käytetäänkö salausta lainkaan.", "NotificationsEmailSettingsUseEncryptionHelpText": "Määrittää suositaanko salausta, jos se on määritetty palvelimelle, käytetäänkö aina SSL- (vain portti 465) tai StartTLS-salausta (kaikki muut portit), voi käytetäänkö salausta lainkaan.",
"RemoveMultipleFromDownloadClientHint": "Poistaa lataukset ja tiedostot latauspalvelusta.", "RemoveMultipleFromDownloadClientHint": "Poistaa lataukset ja tiedostot latauspalvelusta.",
"RemoveQueueItemRemovalMethodHelpTextWarning": "\"Poista latauspalvelusta\" poistaa latauksen ja sen tiedostot.", "RemoveQueueItemRemovalMethodHelpTextWarning": "\"Poista latauspalvelusta\" poistaa latauksen ja sen tiedostot.",
"UnableToLoadAutoTagging": "Automaattimerkinnän lataus epäonnistui", "UnableToLoadAutoTagging": "Virhe ladattaessa automaattimerkintää.",
"IndexerSettingsRejectBlocklistedTorrentHashes": "Hylkää estetyt torrent-hajautusarvot kaapattaessa", "IndexerSettingsRejectBlocklistedTorrentHashes": "Hylkää estetyt torrent-hajautusarvot kaapattaessa",
"IndexerSettingsRejectBlocklistedTorrentHashesHelpText": "Jos torrent on estetty hajautusarvon perusteella sitä ei välttämättä hylätä oikein joidenkin hakupalveluiden RSS-syötteestä tai hausta. Tämän käyttöönotto mahdollistaa tällaisten torrentien hylkäämisen kaappauksen jälkeen, kuitenkin ennen kuin niitä välitetään latauspalvelulle.", "IndexerSettingsRejectBlocklistedTorrentHashesHelpText": "Jos torrent on estetty hajautusarvon perusteella sitä ei välttämättä hylätä oikein joidenkin hakupalveluiden RSS-syötteestä tai hausta. Tämän käyttöönotto mahdollistaa tällaisten torrentien hylkäämisen kaappauksen jälkeen, kuitenkin ennen kuin niitä välitetään latauspalvelulle.",
"NotificationsSynologyValidationTestFailed": "Ei ole Synology tai synoindex ei ole käytettävissä", "NotificationsSynologyValidationTestFailed": "Ei ole Synology tai synoindex ei ole käytettävissä",
@@ -1782,10 +1782,10 @@
"DownloadClientSabnzbdValidationEnableDisableTvSorting": "Älä järjestele sarjoja", "DownloadClientSabnzbdValidationEnableDisableTvSorting": "Älä järjestele sarjoja",
"EnableAutomaticAddSeriesHelpText": "Lisää tämän listan sarjat {appName}iin kun synkronointi suoritetaan manuaalisesti käyttöliittymästä tai {appName}in toimesta.", "EnableAutomaticAddSeriesHelpText": "Lisää tämän listan sarjat {appName}iin kun synkronointi suoritetaan manuaalisesti käyttöliittymästä tai {appName}in toimesta.",
"DownloadClientValidationTestNzbs": "NZB-listauksen nouto epäonnistui: {exceptionMessage}.", "DownloadClientValidationTestNzbs": "NZB-listauksen nouto epäonnistui: {exceptionMessage}.",
"DownloadClientValidationUnableToConnect": "Latauspalveluun {clientName} ei voida muodostaa yhteyttä", "DownloadClientValidationUnableToConnect": "Latauspalveluun {clientName} ei voida muodostaa yhteyttä.",
"DownloadClientNzbgetValidationKeepHistoryZero": "NzbGetin \"KeepHistory\"-asetuksen tulee olla suurempi kuin 0.", "DownloadClientNzbgetValidationKeepHistoryZero": "NzbGetin \"KeepHistory\"-asetuksen tulee olla suurempi kuin 0.",
"DownloadClientTransmissionSettingsDirectoryHelpText": "Vaihtoehtoinen latausten tallennussijainti. Käytä Transmissionin oletusta jättämällä tyhjäksi.", "DownloadClientTransmissionSettingsDirectoryHelpText": "Vaihtoehtoinen latausten tallennussijainti. Käytä Transmissionin oletusta jättämällä tyhjäksi.",
"AddDelayProfileError": "Virhe lisättäessä viiveporofiilia. Yritä uudelleen.", "AddDelayProfileError": "Virhe lisättäessä viiveprofiilia. Yritä uudelleen.",
"DownloadClientPneumaticSettingsStrmFolderHelpText": "Tämän kansion .strm-tiedostot tuodaan droonilla.", "DownloadClientPneumaticSettingsStrmFolderHelpText": "Tämän kansion .strm-tiedostot tuodaan droonilla.",
"DownloadClientSabnzbdValidationEnableDisableDateSorting": "Älä järjestele päiväyksellä", "DownloadClientSabnzbdValidationEnableDisableDateSorting": "Älä järjestele päiväyksellä",
"DownloadClientTransmissionSettingsUrlBaseHelpText": "Lisää latauspalvelun {clientName} RPC-URL-osoitteeseen etuliitteen, esim. \"{url}\". Oletus on \"{defaultUrl}\".", "DownloadClientTransmissionSettingsUrlBaseHelpText": "Lisää latauspalvelun {clientName} RPC-URL-osoitteeseen etuliitteen, esim. \"{url}\". Oletus on \"{defaultUrl}\".",
@@ -1827,7 +1827,7 @@
"PublishedDate": "Julkaisupäivä", "PublishedDate": "Julkaisupäivä",
"DeleteSelectedCustomFormatsMessageText": "Haluatko varmasti poistaa valitut {count} mukautettua muotoa?", "DeleteSelectedCustomFormatsMessageText": "Haluatko varmasti poistaa valitut {count} mukautettua muotoa?",
"DownloadClientValidationGroupMissingDetail": "Syötettyä ryhmää ei ole lautaustyökalussa {clientName}. Luo se sinne ensin.", "DownloadClientValidationGroupMissingDetail": "Syötettyä ryhmää ei ole lautaustyökalussa {clientName}. Luo se sinne ensin.",
"ImportListsAniListSettingsImportCancelled": "Tuonti peruttiin", "ImportListsAniListSettingsImportCancelled": "Tuo lopetetut",
"ImportListsAniListSettingsImportCancelledHelpText": "Media: sarja on lopetettu", "ImportListsAniListSettingsImportCancelledHelpText": "Media: sarja on lopetettu",
"FolderNameTokens": "Kansionimimuuttujat", "FolderNameTokens": "Kansionimimuuttujat",
"Delay": "Viive", "Delay": "Viive",
@@ -2068,7 +2068,7 @@
"ImportListsTraktSettingsPopularListTypeRecommendedYearShows": "Vuosikohtaiset sarjasuositukset", "ImportListsTraktSettingsPopularListTypeRecommendedYearShows": "Vuosikohtaiset sarjasuositukset",
"ImportListsTraktSettingsPopularListTypeTopYearShows": "Vuosikohtaisesti katselluimmat sarjat", "ImportListsTraktSettingsPopularListTypeTopYearShows": "Vuosikohtaisesti katselluimmat sarjat",
"ImportListsTraktSettingsPopularListTypeTopAllTimeShows": "Kaikkien aikojen katselluimmat sarjat", "ImportListsTraktSettingsPopularListTypeTopAllTimeShows": "Kaikkien aikojen katselluimmat sarjat",
"ImportListsTraktSettingsPopularListTypeTrendingShows": "Nousevat sarjat", "ImportListsTraktSettingsPopularListTypeTrendingShows": "Trendaavat sarjat",
"ImportListsTraktSettingsUserListName": "Trakt-käyttäjän listat", "ImportListsTraktSettingsUserListName": "Trakt-käyttäjän listat",
"ImportListsTraktSettingsUsernameHelpText": "Tuotavan listan käyttäjätunnus", "ImportListsTraktSettingsUsernameHelpText": "Tuotavan listan käyttäjätunnus",
"ImportListsTraktSettingsWatchedListTypeAll": "Kaikki", "ImportListsTraktSettingsWatchedListTypeAll": "Kaikki",
File diff suppressed because it is too large Load Diff
+5 -1
View File
@@ -2089,5 +2089,9 @@
"LogSizeLimit": "Ограничение размера журнала", "LogSizeLimit": "Ограничение размера журнала",
"LogSizeLimitHelpText": "Максимальный размер файла журнала в МБ перед архивированием. По умолчанию - 1 МБ.", "LogSizeLimitHelpText": "Максимальный размер файла журнала в МБ перед архивированием. По умолчанию - 1 МБ.",
"IndexerHDBitsSettingsMediums": "Mediums", "IndexerHDBitsSettingsMediums": "Mediums",
"CountCustomFormatsSelected": "{count} пользовательских форматов выбрано" "CountCustomFormatsSelected": "{count} пользовательских форматов выбрано",
"Completed": "Завершено",
"CutoffNotMet": "Порог не достигнут",
"CustomFormatsSpecificationExceptLanguage": "Кроме языка",
"CustomFormatsSpecificationExceptLanguageHelpText": "Подходит, если есть любой язык кроме указанного"
} }
@@ -260,6 +260,26 @@ namespace NzbDrone.Core.MediaFiles
var extension = Path.GetExtension(fileInfo.Name); var extension = Path.GetExtension(fileInfo.Name);
if (FileExtensions.DangerousExtensions.Contains(extension))
{
return new List<ImportResult>
{
new ImportResult(new ImportDecision(new LocalEpisode { Path = fileInfo.FullName },
new ImportRejection(ImportRejectionReason.DangerousFile, $"Caution: Found potentially dangerous file with extension: {extension}")),
$"Caution: Found potentially dangerous file with extension: {extension}")
};
}
if (FileExtensions.ExecutableExtensions.Contains(extension))
{
return new List<ImportResult>
{
new ImportResult(new ImportDecision(new LocalEpisode { Path = fileInfo.FullName },
new ImportRejection(ImportRejectionReason.ExecutableFile, $"Caution: Found executable file with extension: '{extension}'")),
$"Caution: Found executable file with extension: '{extension}'")
};
}
if (extension.IsNullOrWhiteSpace() || !MediaFileExtensions.Extensions.Contains(extension)) if (extension.IsNullOrWhiteSpace() || !MediaFileExtensions.Extensions.Contains(extension))
{ {
_logger.Debug("[{0}] has an unsupported extension: '{1}'", fileInfo.FullName, extension); _logger.Debug("[{0}] has an unsupported extension: '{1}'", fileInfo.FullName, extension);
@@ -66,6 +66,12 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport
return DetectSampleResult.Indeterminate; return DetectSampleResult.Indeterminate;
} }
if (runtime == 0)
{
_logger.Debug("Series runtime is 0, defaulting runtime to 45 minutes");
runtime = 45;
}
return IsSample(localEpisode.Path, localEpisode.MediaInfo.RunTime, runtime); return IsSample(localEpisode.Path, localEpisode.MediaInfo.RunTime, runtime);
} }
@@ -23,8 +23,11 @@ namespace NzbDrone.Core.MediaFiles
private static List<string> _dangerousExtensions = new List<string> private static List<string> _dangerousExtensions = new List<string>
{ {
".arj",
".lnk", ".lnk",
".lzh",
".ps1", ".ps1",
".scr",
".vbs", ".vbs",
".zipx" ".zipx"
}; };
+1 -1
View File
@@ -20,7 +20,7 @@
<PackageReference Include="Servarr.FluentMigrator.Runner.SQLite" Version="3.3.2.9" /> <PackageReference Include="Servarr.FluentMigrator.Runner.SQLite" Version="3.3.2.9" />
<PackageReference Include="Servarr.FluentMigrator.Runner.Postgres" Version="3.3.2.9" /> <PackageReference Include="Servarr.FluentMigrator.Runner.Postgres" Version="3.3.2.9" />
<PackageReference Include="FluentValidation" Version="9.5.4" /> <PackageReference Include="FluentValidation" Version="9.5.4" />
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.6" /> <PackageReference Include="SixLabors.ImageSharp" Version="3.1.12" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="NLog" Version="5.3.4" /> <PackageReference Include="NLog" Version="5.3.4" />
<PackageReference Include="MonoTorrent" Version="2.0.7" /> <PackageReference Include="MonoTorrent" Version="2.0.7" />
+6 -2
View File
@@ -1,6 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Net;
using DryIoc; using DryIoc;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
@@ -59,8 +60,11 @@ namespace NzbDrone.Host
services.Configure<ForwardedHeadersOptions>(options => services.Configure<ForwardedHeadersOptions>(options =>
{ {
options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto | ForwardedHeaders.XForwardedHost; options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto | ForwardedHeaders.XForwardedHost;
options.KnownNetworks.Clear(); options.KnownNetworks.Add(new IPNetwork(IPAddress.Parse("10.0.0.0"), 8));
options.KnownProxies.Clear(); options.KnownNetworks.Add(new IPNetwork(IPAddress.Parse("172.16.0.0"), 12));
options.KnownNetworks.Add(new IPNetwork(IPAddress.Parse("192.168.0.0"), 16));
options.KnownNetworks.Add(new IPNetwork(IPAddress.Parse("fc00::"), 7));
options.KnownNetworks.Add(new IPNetwork(IPAddress.Parse("fe80::"), 10));
}); });
services.AddRouting(options => options.LowercaseUrls = true); services.AddRouting(options => options.LowercaseUrls = true);
@@ -15,11 +15,13 @@ namespace Sonarr.Http.Frontend.Mappers
_backupService = backupService; _backupService = backupService;
} }
public override string Map(string resourceUrl) protected override string FolderPath => _backupService.GetBackupFolder();
protected override string MapPath(string resourceUrl)
{ {
var path = resourceUrl.Replace("/backup/", "").Replace('/', Path.DirectorySeparatorChar); var path = resourceUrl.Replace("/backup/", "").Replace('/', Path.DirectorySeparatorChar);
return Path.Combine(_backupService.GetBackupFolder(), path); return Path.Combine(FolderPath, path);
} }
public override bool CanHandle(string resourceUrl) public override bool CanHandle(string resourceUrl)
@@ -8,13 +8,20 @@ namespace Sonarr.Http.Frontend.Mappers
{ {
public class BrowserConfig : UrlBaseReplacementResourceMapperBase public class BrowserConfig : UrlBaseReplacementResourceMapperBase
{ {
private readonly IAppFolderInfo _appFolderInfo;
private readonly IConfigFileProvider _configFileProvider;
public BrowserConfig(IAppFolderInfo appFolderInfo, IDiskProvider diskProvider, IConfigFileProvider configFileProvider, Logger logger) public BrowserConfig(IAppFolderInfo appFolderInfo, IDiskProvider diskProvider, IConfigFileProvider configFileProvider, Logger logger)
: base(diskProvider, configFileProvider, logger) : base(diskProvider, configFileProvider, logger)
{ {
FilePath = Path.Combine(appFolderInfo.StartUpFolder, configFileProvider.UiFolder, "Content", "browserconfig.xml"); _appFolderInfo = appFolderInfo;
_configFileProvider = configFileProvider;
} }
public override string Map(string resourceUrl) protected override string FolderPath => Path.Combine(_appFolderInfo.StartUpFolder, _configFileProvider.UiFolder);
protected override string FilePath => Path.Combine(FolderPath, "Content", "browserconfig.xml");
protected override string MapPath(string resourceUrl)
{ {
return FilePath; return FilePath;
} }
@@ -37,6 +37,12 @@ namespace Sonarr.Http.Frontend.Mappers
var mapper = _diskMappers.Single(m => m.CanHandle(resourceUrl)); var mapper = _diskMappers.Single(m => m.CanHandle(resourceUrl));
var pathToFile = mapper.Map(resourceUrl); var pathToFile = mapper.Map(resourceUrl);
if (pathToFile == null)
{
return resourceUrl;
}
var hash = _hashProvider.ComputeMd5(pathToFile).ToBase64(); var hash = _hashProvider.ComputeMd5(pathToFile).ToBase64();
return resourceUrl + "?h=" + hash.Trim('='); return resourceUrl + "?h=" + hash.Trim('=');
@@ -18,7 +18,9 @@ namespace Sonarr.Http.Frontend.Mappers
_configFileProvider = configFileProvider; _configFileProvider = configFileProvider;
} }
public override string Map(string resourceUrl) protected override string FolderPath => Path.Combine(_appFolderInfo.StartUpFolder, _configFileProvider.UiFolder);
protected override string MapPath(string resourceUrl)
{ {
var fileName = "favicon.ico"; var fileName = "favicon.ico";
@@ -29,7 +31,7 @@ namespace Sonarr.Http.Frontend.Mappers
var path = Path.Combine("Content", "Images", "Icons", fileName); var path = Path.Combine("Content", "Images", "Icons", fileName);
return Path.Combine(_appFolderInfo.StartUpFolder, _configFileProvider.UiFolder, path); return Path.Combine(FolderPath, path);
} }
public override bool CanHandle(string resourceUrl) public override bool CanHandle(string resourceUrl)
@@ -4,6 +4,7 @@ using System.Text.RegularExpressions;
using NLog; using NLog;
using NzbDrone.Common.Disk; using NzbDrone.Common.Disk;
using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Core.Configuration;
namespace Sonarr.Http.Frontend.Mappers namespace Sonarr.Http.Frontend.Mappers
{ {
@@ -13,19 +14,22 @@ namespace Sonarr.Http.Frontend.Mappers
private readonly Lazy<ICacheBreakerProvider> _cacheBreakProviderFactory; private readonly Lazy<ICacheBreakerProvider> _cacheBreakProviderFactory;
private static readonly Regex ReplaceRegex = new Regex(@"(?:(?<attribute>href|src)=\"")(?<path>.*?(?<extension>css|js|png|ico|ics|svg|json))(?:\"")(?:\s(?<nohash>data-no-hash))?", RegexOptions.Compiled | RegexOptions.IgnoreCase); private static readonly Regex ReplaceRegex = new Regex(@"(?:(?<attribute>href|src)=\"")(?<path>.*?(?<extension>css|js|png|ico|ics|svg|json))(?:\"")(?:\s(?<nohash>data-no-hash))?", RegexOptions.Compiled | RegexOptions.IgnoreCase);
private string _urlBase;
private string _generatedContent; private string _generatedContent;
protected HtmlMapperBase(IDiskProvider diskProvider, protected HtmlMapperBase(IDiskProvider diskProvider,
IConfigFileProvider configFileProvider,
Lazy<ICacheBreakerProvider> cacheBreakProviderFactory, Lazy<ICacheBreakerProvider> cacheBreakProviderFactory,
Logger logger) Logger logger)
: base(diskProvider, logger) : base(diskProvider, logger)
{ {
_diskProvider = diskProvider; _diskProvider = diskProvider;
_cacheBreakProviderFactory = cacheBreakProviderFactory; _cacheBreakProviderFactory = cacheBreakProviderFactory;
_urlBase = configFileProvider.UrlBase;
} }
protected string HtmlPath; protected abstract string HtmlPath { get; }
protected string UrlBase;
protected override Stream GetContentStream(string filePath) protected override Stream GetContentStream(string filePath)
{ {
@@ -62,10 +66,10 @@ namespace Sonarr.Http.Frontend.Mappers
url = cacheBreakProvider.AddCacheBreakerToPath(match.Groups["path"].Value); url = cacheBreakProvider.AddCacheBreakerToPath(match.Groups["path"].Value);
} }
return $"{match.Groups["attribute"].Value}=\"{UrlBase}{url}\""; return $"{match.Groups["attribute"].Value}=\"{_urlBase}{url}\"";
}); });
text = text.Replace("__URL_BASE__", UrlBase); text = text.Replace("__URL_BASE__", _urlBase);
_generatedContent = text; _generatedContent = text;
@@ -9,6 +9,7 @@ namespace Sonarr.Http.Frontend.Mappers
{ {
public class IndexHtmlMapper : HtmlMapperBase public class IndexHtmlMapper : HtmlMapperBase
{ {
private readonly IAppFolderInfo _appFolderInfo;
private readonly IConfigFileProvider _configFileProvider; private readonly IConfigFileProvider _configFileProvider;
public IndexHtmlMapper(IAppFolderInfo appFolderInfo, public IndexHtmlMapper(IAppFolderInfo appFolderInfo,
@@ -16,15 +17,16 @@ namespace Sonarr.Http.Frontend.Mappers
IConfigFileProvider configFileProvider, IConfigFileProvider configFileProvider,
Lazy<ICacheBreakerProvider> cacheBreakProviderFactory, Lazy<ICacheBreakerProvider> cacheBreakProviderFactory,
Logger logger) Logger logger)
: base(diskProvider, cacheBreakProviderFactory, logger) : base(diskProvider, configFileProvider, cacheBreakProviderFactory, logger)
{ {
_appFolderInfo = appFolderInfo;
_configFileProvider = configFileProvider; _configFileProvider = configFileProvider;
HtmlPath = Path.Combine(appFolderInfo.StartUpFolder, _configFileProvider.UiFolder, "index.html");
UrlBase = configFileProvider.UrlBase;
} }
public override string Map(string resourceUrl) protected override string FolderPath => Path.Combine(_appFolderInfo.StartUpFolder, _configFileProvider.UiFolder);
protected override string HtmlPath => Path.Combine(FolderPath, "index.html");
protected override string MapPath(string resourceUrl)
{ {
return HtmlPath; return HtmlPath;
} }
@@ -16,12 +16,14 @@ namespace Sonarr.Http.Frontend.Mappers
_appFolderInfo = appFolderInfo; _appFolderInfo = appFolderInfo;
} }
public override string Map(string resourceUrl) protected override string FolderPath => _appFolderInfo.GetLogFolder();
protected override string MapPath(string resourceUrl)
{ {
var path = resourceUrl.Replace('/', Path.DirectorySeparatorChar); var path = resourceUrl.Replace('/', Path.DirectorySeparatorChar);
path = Path.GetFileName(path); path = Path.GetFileName(path);
return Path.Combine(_appFolderInfo.GetLogFolder(), path); return Path.Combine(FolderPath, path);
} }
public override bool CanHandle(string resourceUrl) public override bool CanHandle(string resourceUrl)
@@ -9,6 +9,7 @@ namespace Sonarr.Http.Frontend.Mappers
{ {
public class LoginHtmlMapper : HtmlMapperBase public class LoginHtmlMapper : HtmlMapperBase
{ {
private readonly IAppFolderInfo _appFolderInfo;
private readonly IConfigFileProvider _configFileProvider; private readonly IConfigFileProvider _configFileProvider;
public LoginHtmlMapper(IAppFolderInfo appFolderInfo, public LoginHtmlMapper(IAppFolderInfo appFolderInfo,
@@ -16,14 +17,16 @@ namespace Sonarr.Http.Frontend.Mappers
Lazy<ICacheBreakerProvider> cacheBreakProviderFactory, Lazy<ICacheBreakerProvider> cacheBreakProviderFactory,
IConfigFileProvider configFileProvider, IConfigFileProvider configFileProvider,
Logger logger) Logger logger)
: base(diskProvider, cacheBreakProviderFactory, logger) : base(diskProvider, configFileProvider, cacheBreakProviderFactory, logger)
{ {
_appFolderInfo = appFolderInfo;
_configFileProvider = configFileProvider; _configFileProvider = configFileProvider;
HtmlPath = Path.Combine(appFolderInfo.StartUpFolder, configFileProvider.UiFolder, "login.html");
UrlBase = configFileProvider.UrlBase;
} }
public override string Map(string resourceUrl) protected override string FolderPath => Path.Combine(_appFolderInfo.StartUpFolder, _configFileProvider.UiFolder);
protected override string HtmlPath => Path.Combine(FolderPath, "login.html");
protected override string MapPath(string resourceUrl)
{ {
return HtmlPath; return HtmlPath;
} }
@@ -8,6 +8,7 @@ namespace Sonarr.Http.Frontend.Mappers
{ {
public class ManifestMapper : UrlBaseReplacementResourceMapperBase public class ManifestMapper : UrlBaseReplacementResourceMapperBase
{ {
private readonly IAppFolderInfo _appFolderInfo;
private readonly IConfigFileProvider _configFileProvider; private readonly IConfigFileProvider _configFileProvider;
private string _generatedContent; private string _generatedContent;
@@ -15,11 +16,14 @@ namespace Sonarr.Http.Frontend.Mappers
public ManifestMapper(IAppFolderInfo appFolderInfo, IDiskProvider diskProvider, IConfigFileProvider configFileProvider, Logger logger) public ManifestMapper(IAppFolderInfo appFolderInfo, IDiskProvider diskProvider, IConfigFileProvider configFileProvider, Logger logger)
: base(diskProvider, configFileProvider, logger) : base(diskProvider, configFileProvider, logger)
{ {
_appFolderInfo = appFolderInfo;
_configFileProvider = configFileProvider; _configFileProvider = configFileProvider;
FilePath = Path.Combine(appFolderInfo.StartUpFolder, configFileProvider.UiFolder, "Content", "manifest.json");
} }
public override string Map(string resourceUrl) protected override string FolderPath => Path.Combine(_appFolderInfo.StartUpFolder, _configFileProvider.UiFolder);
protected override string FilePath => Path.Combine(FolderPath, "Content", "manifest.json");
protected override string MapPath(string resourceUrl)
{ {
return FilePath; return FilePath;
} }
@@ -22,7 +22,9 @@ namespace Sonarr.Http.Frontend.Mappers
_diskProvider = diskProvider; _diskProvider = diskProvider;
} }
public override string Map(string resourceUrl) protected override string FolderPath => Path.Combine(_appFolderInfo.GetAppDataPath(), "MediaCover");
protected override string MapPath(string resourceUrl)
{ {
var path = resourceUrl.Replace('/', Path.DirectorySeparatorChar); var path = resourceUrl.Replace('/', Path.DirectorySeparatorChar);
path = path.Trim(Path.DirectorySeparatorChar); path = path.Trim(Path.DirectorySeparatorChar);
@@ -18,11 +18,13 @@ namespace Sonarr.Http.Frontend.Mappers
_configFileProvider = configFileProvider; _configFileProvider = configFileProvider;
} }
public override string Map(string resourceUrl) protected override string FolderPath => Path.Combine(_appFolderInfo.StartUpFolder, _configFileProvider.UiFolder);
protected override string MapPath(string resourceUrl)
{ {
var path = Path.Combine("Content", "robots.txt"); var path = Path.Combine("Content", "robots.txt");
return Path.Combine(_appFolderInfo.StartUpFolder, _configFileProvider.UiFolder, path); return Path.Combine(FolderPath, path);
} }
public override bool CanHandle(string resourceUrl) public override bool CanHandle(string resourceUrl)
@@ -18,12 +18,14 @@ namespace Sonarr.Http.Frontend.Mappers
_configFileProvider = configFileProvider; _configFileProvider = configFileProvider;
} }
public override string Map(string resourceUrl) protected override string FolderPath => Path.Combine(_appFolderInfo.StartUpFolder, _configFileProvider.UiFolder);
protected override string MapPath(string resourceUrl)
{ {
var path = resourceUrl.Replace('/', Path.DirectorySeparatorChar); var path = resourceUrl.Replace('/', Path.DirectorySeparatorChar);
path = path.Trim(Path.DirectorySeparatorChar); path = path.Trim(Path.DirectorySeparatorChar);
return Path.Combine(_appFolderInfo.StartUpFolder, _configFileProvider.UiFolder, path); return Path.Combine(FolderPath, path);
} }
public override bool CanHandle(string resourceUrl) public override bool CanHandle(string resourceUrl)
@@ -27,14 +27,28 @@ namespace Sonarr.Http.Frontend.Mappers
_caseSensitive = RuntimeInfo.IsProduction ? DiskProviderBase.PathStringComparison : StringComparison.OrdinalIgnoreCase; _caseSensitive = RuntimeInfo.IsProduction ? DiskProviderBase.PathStringComparison : StringComparison.OrdinalIgnoreCase;
} }
public abstract string Map(string resourceUrl); protected abstract string FolderPath { get; }
protected abstract string MapPath(string resourceUrl);
public abstract bool CanHandle(string resourceUrl); public abstract bool CanHandle(string resourceUrl);
public string Map(string resourceUrl)
{
var filePath = Path.GetFullPath(MapPath(resourceUrl));
var parentPath = Path.GetFullPath(FolderPath) + Path.DirectorySeparatorChar;
return filePath.StartsWith(parentPath) ? filePath : null;
}
public Task<IActionResult> GetResponse(string resourceUrl) public Task<IActionResult> GetResponse(string resourceUrl)
{ {
var filePath = Map(resourceUrl); var filePath = Map(resourceUrl);
if (filePath == null)
{
return Task.FromResult<IActionResult>(null);
}
if (_diskProvider.FileExists(filePath, _caseSensitive)) if (_diskProvider.FileExists(filePath, _caseSensitive))
{ {
if (!_mimeTypeProvider.TryGetContentType(filePath, out var contentType)) if (!_mimeTypeProvider.TryGetContentType(filePath, out var contentType))
@@ -16,12 +16,14 @@ namespace Sonarr.Http.Frontend.Mappers
_appFolderInfo = appFolderInfo; _appFolderInfo = appFolderInfo;
} }
public override string Map(string resourceUrl) protected override string FolderPath => _appFolderInfo.GetUpdateLogFolder();
protected override string MapPath(string resourceUrl)
{ {
var path = resourceUrl.Replace('/', Path.DirectorySeparatorChar); var path = resourceUrl.Replace('/', Path.DirectorySeparatorChar);
path = Path.GetFileName(path); path = Path.GetFileName(path);
return Path.Combine(_appFolderInfo.GetUpdateLogFolder(), path); return Path.Combine(FolderPath, path);
} }
public override bool CanHandle(string resourceUrl) public override bool CanHandle(string resourceUrl)
@@ -20,9 +20,9 @@ namespace Sonarr.Http.Frontend.Mappers
_urlBase = configFileProvider.UrlBase; _urlBase = configFileProvider.UrlBase;
} }
protected string FilePath; protected abstract string FilePath { get; }
public override string Map(string resourceUrl) protected override string MapPath(string resourceUrl)
{ {
return FilePath; return FilePath;
} }
@@ -1,5 +1,6 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Cors; using Microsoft.AspNetCore.Cors;
@@ -16,6 +17,7 @@ namespace Sonarr.Http.Frontend
{ {
private readonly IEnumerable<IMapHttpRequestsToDisk> _requestMappers; private readonly IEnumerable<IMapHttpRequestsToDisk> _requestMappers;
private readonly Logger _logger; private readonly Logger _logger;
private static readonly Regex InvalidPathRegex = new (@"([\/\\]|%2f|%5c)\.\.|\.\.([\/\\]|%2f|%5c)", RegexOptions.IgnoreCase | RegexOptions.Compiled);
public StaticResourceController(IEnumerable<IMapHttpRequestsToDisk> requestMappers, public StaticResourceController(IEnumerable<IMapHttpRequestsToDisk> requestMappers,
Logger logger) Logger logger)
@@ -50,6 +52,11 @@ namespace Sonarr.Http.Frontend
{ {
path = "/" + (path ?? ""); path = "/" + (path ?? "");
if (InvalidPathRegex.IsMatch(path))
{
return NotFound();
}
var mapper = _requestMappers.SingleOrDefault(m => m.CanHandle(path)); var mapper = _requestMappers.SingleOrDefault(m => m.CanHandle(path));
if (mapper != null) if (mapper != null)