aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.github/workflows/openapi.yml2
-rw-r--r--CONTRIBUTORS.md2
-rw-r--r--Emby.Dlna/PlayTo/PlayToController.cs16
-rw-r--r--Emby.Naming/Common/NamingOptions.cs285
-rw-r--r--Emby.Naming/ExternalFiles/ExternalPathParser.cs116
-rw-r--r--Emby.Naming/ExternalFiles/ExternalPathParserResult.cs (renamed from Emby.Naming/Subtitles/SubtitleInfo.cs)20
-rw-r--r--Emby.Naming/Subtitles/SubtitleParser.cs71
-rw-r--r--Emby.Server.Implementations/ApplicationHost.cs14
-rw-r--r--Emby.Server.Implementations/Channels/ChannelManager.cs29
-rw-r--r--Emby.Server.Implementations/Channels/RefreshChannelsScheduledTask.cs2
-rw-r--r--Emby.Server.Implementations/Data/SqliteItemRepository.cs335
-rw-r--r--Emby.Server.Implementations/Data/SqliteUserDataRepository.cs10
-rw-r--r--Emby.Server.Implementations/Dto/DtoService.cs13
-rw-r--r--Emby.Server.Implementations/Emby.Server.Implementations.csproj6
-rw-r--r--Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs13
-rw-r--r--Emby.Server.Implementations/IO/ManagedFileSystem.cs4
-rw-r--r--Emby.Server.Implementations/Images/BaseFolderImageProvider.cs2
-rw-r--r--Emby.Server.Implementations/Library/LibraryManager.cs75
-rw-r--r--Emby.Server.Implementations/Library/MediaSourceManager.cs2
-rw-r--r--Emby.Server.Implementations/Library/MediaStreamSelector.cs90
-rw-r--r--Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs33
-rw-r--r--Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs33
-rw-r--r--Emby.Server.Implementations/LiveTv/LiveTvManager.cs42
-rw-r--r--Emby.Server.Implementations/LiveTv/RefreshGuideScheduledTask.cs2
-rw-r--r--Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs2
-rw-r--r--Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs2
-rw-r--r--Emby.Server.Implementations/Localization/Core/cs.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/fa.json3
-rw-r--r--Emby.Server.Implementations/Localization/Core/vi.json2
-rw-r--r--Emby.Server.Implementations/Localization/LocalizationManager.cs8
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs6
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs9
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/Tasks/CleanActivityLogTask.cs4
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs17
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteLogFileTask.cs9
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteTranscodeFileTask.cs15
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/Tasks/OptimizeDatabaseTask.cs9
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/Tasks/PeopleValidationTask.cs11
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/Tasks/PluginUpdateTask.cs9
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/Tasks/RefreshMediaLibraryTask.cs9
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/Triggers/DailyTrigger.cs17
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/Triggers/IntervalTrigger.cs17
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/Triggers/WeeklyTrigger.cs17
-rw-r--r--Emby.Server.Implementations/TV/TVSeriesManager.cs36
-rw-r--r--Emby.Server.Implementations/Udp/UdpServer.cs10
-rw-r--r--Jellyfin.Api/Controllers/SearchController.cs6
-rw-r--r--Jellyfin.Api/Controllers/TvShowsController.cs7
-rw-r--r--Jellyfin.Api/Helpers/ProgressiveFileStream.cs16
-rw-r--r--Jellyfin.Api/Helpers/TranscodingJobHelper.cs2
-rw-r--r--Jellyfin.Api/Jellyfin.Api.csproj2
-rw-r--r--Jellyfin.Networking/Manager/NetworkManager.cs3
-rw-r--r--Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj10
-rw-r--r--Jellyfin.Server/Jellyfin.Server.csproj6
-rw-r--r--Jellyfin.Server/Middleware/BaseUrlRedirectionMiddleware.cs6
-rw-r--r--Jellyfin.Server/Program.cs7
-rw-r--r--Jellyfin.Server/StartupOptions.cs9
-rw-r--r--MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs5
-rw-r--r--MediaBrowser.Controller/Library/IIntroProvider.cs6
-rw-r--r--MediaBrowser.Controller/Library/ILibraryManager.cs31
-rw-r--r--MediaBrowser.Controller/LiveTv/LiveTvChannel.cs2
-rw-r--r--MediaBrowser.Controller/Persistence/IItemRepository.cs25
-rw-r--r--MediaBrowser.Controller/Persistence/IRepository.cs16
-rw-r--r--MediaBrowser.Controller/Persistence/IUserDataRepository.cs3
-rw-r--r--MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs2
-rw-r--r--MediaBrowser.Model/Configuration/EmbeddedSubtitleOptions.cs30
-rw-r--r--MediaBrowser.Model/Configuration/LibraryOptions.cs3
-rw-r--r--MediaBrowser.Model/Dlna/ContentFeatureBuilder.cs2
-rw-r--r--MediaBrowser.Model/Dlna/DlnaProfileType.cs3
-rw-r--r--MediaBrowser.Model/Dlna/StreamInfo.cs28
-rw-r--r--MediaBrowser.Model/Dto/MediaSourceInfo.cs4
-rw-r--r--MediaBrowser.Model/Dto/MetadataEditorInfo.cs14
-rw-r--r--MediaBrowser.Model/Globalization/CultureDto.cs28
-rw-r--r--MediaBrowser.Model/IO/IFileSystem.cs2
-rw-r--r--MediaBrowser.Model/MediaBrowser.Model.csproj4
-rw-r--r--MediaBrowser.Model/Querying/NextUpQuery.cs6
-rw-r--r--MediaBrowser.Model/Search/SearchHintResult.cs22
-rw-r--r--MediaBrowser.Model/Tasks/IScheduledTask.cs4
-rw-r--r--MediaBrowser.Providers/MediaInfo/AudioResolver.cs158
-rw-r--r--MediaBrowser.Providers/MediaInfo/FFProbeAudioInfo.cs4
-rw-r--r--MediaBrowser.Providers/MediaInfo/FFProbeProvider.cs17
-rw-r--r--MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs42
-rw-r--r--MediaBrowser.Providers/MediaInfo/MediaInfoResolver.cs231
-rw-r--r--MediaBrowser.Providers/MediaInfo/SubtitleResolver.cs235
-rw-r--r--MediaBrowser.Providers/MediaInfo/SubtitleScheduledTask.cs4
-rw-r--r--MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs13
-rw-r--r--deployment/Dockerfile.centos.amd642
-rw-r--r--deployment/Dockerfile.fedora.amd642
-rw-r--r--deployment/Dockerfile.ubuntu.amd642
-rw-r--r--deployment/Dockerfile.ubuntu.arm642
-rw-r--r--deployment/Dockerfile.ubuntu.armhf2
-rw-r--r--jellyfin.ruleset6
-rw-r--r--src/Jellyfin.MediaEncoding.Hls/ScheduledTasks/KeyframeExtractionScheduledTask.cs2
-rw-r--r--src/Jellyfin.MediaEncoding.Keyframes/Jellyfin.MediaEncoding.Keyframes.csproj2
-rw-r--r--tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj6
-rw-r--r--tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj4
-rw-r--r--tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj4
-rw-r--r--tests/Jellyfin.Dlna.Tests/Jellyfin.Dlna.Tests.csproj4
-rw-r--r--tests/Jellyfin.Extensions.Tests/Jellyfin.Extensions.Tests.csproj4
-rw-r--r--tests/Jellyfin.MediaEncoding.Hls.Tests/Jellyfin.MediaEncoding.Hls.Tests.csproj4
-rw-r--r--tests/Jellyfin.MediaEncoding.Keyframes.Tests/Jellyfin.MediaEncoding.Keyframes.Tests.csproj4
-rw-r--r--tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj4
-rw-r--r--tests/Jellyfin.Model.Tests/Jellyfin.Model.Tests.csproj4
-rw-r--r--tests/Jellyfin.Naming.Tests/ExternalFiles/ExternalPathParserTests.cs111
-rw-r--r--tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj5
-rw-r--r--tests/Jellyfin.Naming.Tests/Subtitles/SubtitleParserTests.cs41
-rw-r--r--tests/Jellyfin.Networking.Tests/Jellyfin.Networking.Tests.csproj4
-rw-r--r--tests/Jellyfin.Providers.Tests/Jellyfin.Providers.Tests.csproj4
-rw-r--r--tests/Jellyfin.Providers.Tests/MediaInfo/AudioResolverTests.cs79
-rw-r--r--tests/Jellyfin.Providers.Tests/MediaInfo/MediaInfoResolverTests.cs375
-rw-r--r--tests/Jellyfin.Providers.Tests/MediaInfo/SubtitleResolverTests.cs158
-rw-r--r--tests/Jellyfin.Providers.Tests/Test Data/Video/My.Video.mkv0
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj4
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Library/MediaStreamSelectorTests.cs30
-rw-r--r--tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj6
-rw-r--r--tests/Jellyfin.Server.Integration.Tests/JellyfinApplicationFactory.cs4
-rw-r--r--tests/Jellyfin.Server.Tests/Jellyfin.Server.Tests.csproj6
-rw-r--r--tests/Jellyfin.XbmcMetadata.Tests/Jellyfin.XbmcMetadata.Tests.csproj4
-rw-r--r--tests/Jellyfin.XbmcMetadata.Tests/Parsers/EpisodeNfoProviderTests.cs14
-rw-r--r--tests/Jellyfin.XbmcMetadata.Tests/Test Data/Sonarr-Thumb.nfo34
119 files changed, 1803 insertions, 1583 deletions
diff --git a/.github/workflows/openapi.yml b/.github/workflows/openapi.yml
index 3e9346840..eb2e98070 100644
--- a/.github/workflows/openapi.yml
+++ b/.github/workflows/openapi.yml
@@ -14,7 +14,7 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v2
with:
- ref: ${{ github.event.pull_request.head.ref }}
+ ref: ${{ github.event.pull_request.head.sha }}
repository: ${{ github.event.pull_request.head.repo.full_name }}
- name: Setup .NET Core
uses: actions/setup-dotnet@v1
diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md
index d52e13324..86a8ecc82 100644
--- a/CONTRIBUTORS.md
+++ b/CONTRIBUTORS.md
@@ -151,6 +151,7 @@
- [peterspenler](https://github.com/peterspenler)
- [MBR-0001](https://github.com/MBR-0001)
- [jonas-resch](https://github.com/jonas-resch)
+ - [vgambier](https://github.com/vgambier)
# Emby Contributors
@@ -217,3 +218,4 @@
- [olsh](https://github.com/olsh)
- [lbenini](https://github.com/lbenini)
- [gnuyent](https://github.com/gnuyent)
+ - [Matthew Jones](https://github.com/matthew-jones-uk)
diff --git a/Emby.Dlna/PlayTo/PlayToController.cs b/Emby.Dlna/PlayTo/PlayToController.cs
index 23f2456ac..e147cb977 100644
--- a/Emby.Dlna/PlayTo/PlayToController.cs
+++ b/Emby.Dlna/PlayTo/PlayToController.cs
@@ -569,7 +569,7 @@ namespace Emby.Dlna.PlayTo
streamInfo.TargetVideoCodecTag,
streamInfo.IsTargetAVC);
- return list.Count == 0 ? null : list[0];
+ return list.FirstOrDefault();
}
return null;
@@ -883,7 +883,7 @@ namespace Emby.Dlna.PlayTo
private class StreamParams
{
- private MediaSourceInfo mediaSource;
+ private MediaSourceInfo _mediaSource;
private IMediaSourceManager _mediaSourceManager;
public Guid ItemId { get; set; }
@@ -908,24 +908,22 @@ namespace Emby.Dlna.PlayTo
public async Task<MediaSourceInfo> GetMediaSource(CancellationToken cancellationToken)
{
- if (mediaSource != null)
+ if (_mediaSource != null)
{
- return mediaSource;
+ return _mediaSource;
}
- var hasMediaSources = Item as IHasMediaSources;
-
- if (hasMediaSources == null)
+ if (Item is not IHasMediaSources)
{
return null;
}
if (_mediaSourceManager != null)
{
- mediaSource = await _mediaSourceManager.GetMediaSource(Item, MediaSourceId, LiveStreamId, false, cancellationToken).ConfigureAwait(false);
+ _mediaSource = await _mediaSourceManager.GetMediaSource(Item, MediaSourceId, LiveStreamId, false, cancellationToken).ConfigureAwait(false);
}
- return mediaSource;
+ return _mediaSource;
}
private static Guid GetItemId(string url)
diff --git a/Emby.Naming/Common/NamingOptions.cs b/Emby.Naming/Common/NamingOptions.cs
index eb211050f..a79153e86 100644
--- a/Emby.Naming/Common/NamingOptions.cs
+++ b/Emby.Naming/Common/NamingOptions.cs
@@ -23,47 +23,60 @@ namespace Emby.Naming.Common
{
VideoFileExtensions = new[]
{
- ".m4v",
+ ".001",
+ ".3g2",
".3gp",
- ".nsv",
- ".ts",
- ".ty",
- ".strm",
- ".rm",
- ".rmvb",
- ".ifo",
- ".mov",
- ".qt",
- ".divx",
- ".xvid",
- ".bivx",
- ".vob",
- ".nrg",
- ".img",
- ".iso",
- ".pva",
- ".wmv",
+ ".amv",
".asf",
".asx",
- ".ogm",
- ".m2v",
".avi",
".bin",
+ ".bivx",
+ ".divx",
+ ".dv",
".dvr-ms",
- ".mpg",
- ".mpeg",
- ".mp4",
+ ".f4v",
+ ".fli",
+ ".flv",
+ ".ifo",
+ ".img",
+ ".iso",
+ ".m2t",
+ ".m2ts",
+ ".m2v",
+ ".m4v",
".mkv",
- ".avc",
- ".vp3",
- ".svq3",
+ ".mk3d",
+ ".mov",
+ ".mp2",
+ ".mp4",
+ ".mpe",
+ ".mpeg",
+ ".mpg",
+ ".mts",
+ ".mxf",
+ ".nrg",
+ ".nsv",
".nuv",
+ ".ogg",
+ ".ogm",
+ ".ogv",
+ ".pva",
+ ".qt",
+ ".rec",
+ ".rm",
+ ".rmvb",
+ ".svq3",
+ ".tp",
+ ".ts",
+ ".ty",
".viv",
- ".dv",
- ".fli",
- ".flv",
- ".001",
- ".tp"
+ ".vob",
+ ".vp3",
+ ".webm",
+ ".wmv",
+ ".wtv",
+ ".xvid"
};
VideoFlagDelimiters = new[]
@@ -149,32 +162,20 @@ namespace Emby.Naming.Common
SubtitleFileExtensions = new[]
{
+ ".ass",
+ ".mks",
+ ".sami",
+ ".smi",
".srt",
".ssa",
- ".ass",
- ".sub"
- };
-
- SubtitleFlagDelimiters = new[]
- {
- '.'
- };
-
- SubtitleForcedFlags = new[]
- {
- "foreign",
- "forced"
- };
-
- SubtitleDefaultFlags = new[]
- {
- "default"
+ ".sub",
+ ".vtt",
};
AlbumStackingPrefixes = new[]
{
- "disc",
"cd",
+ "disc",
"disk",
"vol",
"volume"
@@ -182,68 +183,101 @@ namespace Emby.Naming.Common
AudioFileExtensions = new[]
{
- ".nsv",
- ".m4a",
- ".flac",
+ ".669",
+ ".3gp",
+ ".aa",
".aac",
- ".strm",
- ".pls",
- ".rm",
- ".mpa",
- ".wav",
- ".wma",
- ".ogg",
- ".opus",
- ".mp3",
- ".mp2",
- ".mod",
+ ".aax",
+ ".ac3",
+ ".act",
+ ".adp",
+ ".adplug",
+ ".adx",
+ ".afc",
".amf",
- ".669",
+ ".aif",
+ ".aiff",
+ ".alac",
+ ".amr",
+ ".ape",
+ ".ast",
+ ".au",
+ ".awb",
+ ".cda",
+ ".cue",
".dmf",
+ ".dsf",
".dsm",
+ ".dsp",
+ ".dts",
+ ".dvf",
".far",
+ ".flac",
".gdm",
+ ".gsm",
+ ".gym",
+ ".hps",
".imf",
".it",
".m15",
+ ".m4a",
+ ".m4b",
+ ".mac",
".med",
+ ".mka",
+ ".mmf",
+ ".mod",
+ ".mogg",
+ ".mp2",
+ ".mp3",
+ ".mpa",
+ ".mpc",
+ ".mpp",
+ ".mp+",
+ ".msv",
+ ".nmf",
+ ".nsf",
+ ".nsv",
+ ".oga",
+ ".ogg",
".okt",
+ ".opus",
+ ".pls",
+ ".ra",
+ ".rf64",
+ ".rm",
".s3m",
- ".stm",
".sfx",
+ ".shn",
+ ".sid",
+ ".spc",
+ ".stm",
+ ".strm",
".ult",
".uni",
- ".xm",
- ".sid",
- ".ac3",
- ".dts",
- ".cue",
- ".aif",
- ".aiff",
- ".ape",
- ".mac",
- ".mpc",
- ".mp+",
- ".mpp",
- ".shn",
+ ".vox",
+ ".wav",
+ ".wma",
".wv",
- ".nsf",
- ".spc",
- ".gym",
- ".adplug",
- ".adx",
- ".dsp",
- ".adp",
- ".ymf",
- ".ast",
- ".afc",
- ".hps",
+ ".xm",
".xsp",
- ".acc",
- ".m4b",
- ".oga",
- ".dsf",
- ".mka"
+ ".ymf"
+ };
+
+ MediaFlagDelimiters = new[]
+ {
+ "."
+ };
+
+ MediaForcedFlags = new[]
+ {
+ "foreign",
+ "forced"
+ };
+
+ MediaDefaultFlags = new[]
+ {
+ "default"
};
EpisodeExpressions = new[]
@@ -648,45 +682,6 @@ namespace Emby.Naming.Common
@"^\s*(?<name>[^ ].*?)\s*$"
};
- var extensions = VideoFileExtensions.ToList();
-
- extensions.AddRange(new[]
- {
- ".mkv",
- ".m2t",
- ".m2ts",
- ".img",
- ".iso",
- ".mk3d",
- ".ts",
- ".rmvb",
- ".mov",
- ".avi",
- ".mpg",
- ".mpeg",
- ".wmv",
- ".mp4",
- ".divx",
- ".dvr-ms",
- ".wtv",
- ".ogm",
- ".ogv",
- ".asf",
- ".m4v",
- ".flv",
- ".f4v",
- ".3gp",
- ".webm",
- ".mts",
- ".m2v",
- ".rec",
- ".mxf"
- });
-
- VideoFileExtensions = extensions
- .Distinct(StringComparer.OrdinalIgnoreCase)
- .ToArray();
-
MultipleEpisodeExpressions = new[]
{
@".*(\\|\/)[sS]?(?<seasonnumber>[0-9]{1,4})[xX](?<epnumber>[0-9]{1,3})((-| - )[0-9]{1,4}[eExX](?<endingepnumber>[0-9]{1,3}))+[^\\\/]*$",
@@ -718,29 +713,29 @@ namespace Emby.Naming.Common
public string[] AudioFileExtensions { get; set; }
/// <summary>
- /// Gets or sets list of album stacking prefixes.
+ /// Gets or sets list of external media flag delimiters.
/// </summary>
- public string[] AlbumStackingPrefixes { get; set; }
+ public string[] MediaFlagDelimiters { get; set; }
/// <summary>
- /// Gets or sets list of subtitle file extensions.
+ /// Gets or sets list of external media forced flags.
/// </summary>
- public string[] SubtitleFileExtensions { get; set; }
+ public string[] MediaForcedFlags { get; set; }
/// <summary>
- /// Gets or sets list of subtitles flag delimiters.
+ /// Gets or sets list of external media default flags.
/// </summary>
- public char[] SubtitleFlagDelimiters { get; set; }
+ public string[] MediaDefaultFlags { get; set; }
/// <summary>
- /// Gets or sets list of subtitle forced flags.
+ /// Gets or sets list of album stacking prefixes.
/// </summary>
- public string[] SubtitleForcedFlags { get; set; }
+ public string[] AlbumStackingPrefixes { get; set; }
/// <summary>
- /// Gets or sets list of subtitle default flags.
+ /// Gets or sets list of subtitle file extensions.
/// </summary>
- public string[] SubtitleDefaultFlags { get; set; }
+ public string[] SubtitleFileExtensions { get; set; }
/// <summary>
/// Gets or sets list of episode regular expressions.
diff --git a/Emby.Naming/ExternalFiles/ExternalPathParser.cs b/Emby.Naming/ExternalFiles/ExternalPathParser.cs
new file mode 100644
index 000000000..9d07dc2f9
--- /dev/null
+++ b/Emby.Naming/ExternalFiles/ExternalPathParser.cs
@@ -0,0 +1,116 @@
+using System;
+using System.IO;
+using System.Linq;
+using Emby.Naming.Common;
+using Jellyfin.Extensions;
+using MediaBrowser.Model.Dlna;
+using MediaBrowser.Model.Globalization;
+
+namespace Emby.Naming.ExternalFiles
+{
+ /// <summary>
+ /// External media file parser class.
+ /// </summary>
+ public class ExternalPathParser
+ {
+ private readonly NamingOptions _namingOptions;
+ private readonly DlnaProfileType _type;
+ private readonly ILocalizationManager _localizationManager;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ExternalPathParser"/> class.
+ /// </summary>
+ /// <param name="localizationManager">The localization manager.</param>
+ /// <param name="namingOptions">The <see cref="NamingOptions"/> object containing FileExtensions, MediaDefaultFlags, MediaForcedFlags and MediaFlagDelimiters.</param>
+ /// <param name="type">The <see cref="DlnaProfileType"/> of the parsed file.</param>
+ public ExternalPathParser(NamingOptions namingOptions, ILocalizationManager localizationManager, DlnaProfileType type)
+ {
+ _localizationManager = localizationManager;
+ _namingOptions = namingOptions;
+ _type = type;
+ }
+
+ /// <summary>
+ /// Parse filename and extract information.
+ /// </summary>
+ /// <param name="path">Path to file.</param>
+ /// <param name="extraString">Part of the filename only containing the extra information.</param>
+ /// <returns>Returns null or an <see cref="ExternalPathParserResult"/> object if parsing is successful.</returns>
+ public ExternalPathParserResult? ParseFile(string path, string? extraString)
+ {
+ if (path.Length == 0)
+ {
+ return null;
+ }
+
+ var extension = Path.GetExtension(path);
+ if (!(_type == DlnaProfileType.Subtitle && _namingOptions.SubtitleFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
+ && !(_type == DlnaProfileType.Audio && _namingOptions.AudioFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase)))
+ {
+ return null;
+ }
+
+ var pathInfo = new ExternalPathParserResult(path);
+
+ if (string.IsNullOrEmpty(extraString))
+ {
+ return pathInfo;
+ }
+
+ foreach (var separator in _namingOptions.MediaFlagDelimiters)
+ {
+ var languageString = extraString;
+ var titleString = string.Empty;
+ int separatorLength = separator.Length;
+
+ while (languageString.Length > 0)
+ {
+ int lastSeparator = languageString.LastIndexOf(separator, StringComparison.OrdinalIgnoreCase);
+
+ if (lastSeparator == -1)
+ {
+ break;
+ }
+
+ string currentSlice = languageString[lastSeparator..];
+ string currentSliceWithoutSeparator = currentSlice[separatorLength..];
+
+ if (_namingOptions.MediaDefaultFlags.Any(s => currentSliceWithoutSeparator.Contains(s, StringComparison.OrdinalIgnoreCase)))
+ {
+ pathInfo.IsDefault = true;
+ extraString = extraString.Replace(currentSlice, string.Empty, StringComparison.OrdinalIgnoreCase);
+ languageString = languageString[..lastSeparator];
+ continue;
+ }
+
+ if (_namingOptions.MediaForcedFlags.Any(s => currentSliceWithoutSeparator.Contains(s, StringComparison.OrdinalIgnoreCase)))
+ {
+ pathInfo.IsForced = true;
+ extraString = extraString.Replace(currentSlice, string.Empty, StringComparison.OrdinalIgnoreCase);
+ languageString = languageString[..lastSeparator];
+ continue;
+ }
+
+ // Try to translate to three character code
+ var culture = _localizationManager.FindLanguageInfo(currentSliceWithoutSeparator);
+
+ if (culture != null && pathInfo.Language == null)
+ {
+ pathInfo.Language = culture.ThreeLetterISOLanguageName;
+ extraString = extraString.Replace(currentSlice, string.Empty, StringComparison.OrdinalIgnoreCase);
+ }
+ else
+ {
+ titleString = currentSlice + titleString;
+ }
+
+ languageString = languageString[..lastSeparator];
+ }
+
+ pathInfo.Title = separatorLength <= titleString.Length ? titleString[separatorLength..] : null;
+ }
+
+ return pathInfo;
+ }
+ }
+}
diff --git a/Emby.Naming/Subtitles/SubtitleInfo.cs b/Emby.Naming/ExternalFiles/ExternalPathParserResult.cs
index 1fb2e0dc8..1cc773a2e 100644
--- a/Emby.Naming/Subtitles/SubtitleInfo.cs
+++ b/Emby.Naming/ExternalFiles/ExternalPathParserResult.cs
@@ -1,17 +1,17 @@
-namespace Emby.Naming.Subtitles
+namespace Emby.Naming.ExternalFiles
{
/// <summary>
- /// Class holding information about subtitle.
+ /// Class holding information about external files.
/// </summary>
- public class SubtitleInfo
+ public class ExternalPathParserResult
{
/// <summary>
- /// Initializes a new instance of the <see cref="SubtitleInfo"/> class.
+ /// Initializes a new instance of the <see cref="ExternalPathParserResult"/> class.
/// </summary>
/// <param name="path">Path to file.</param>
- /// <param name="isDefault">Is subtitle default.</param>
- /// <param name="isForced">Is subtitle forced.</param>
- public SubtitleInfo(string path, bool isDefault, bool isForced)
+ /// <param name="isDefault">Is default.</param>
+ /// <param name="isForced">Is forced.</param>
+ public ExternalPathParserResult(string path, bool isDefault = false, bool isForced = false)
{
Path = path;
IsDefault = isDefault;
@@ -31,6 +31,12 @@ namespace Emby.Naming.Subtitles
public string? Language { get; set; }
/// <summary>
+ /// Gets or sets the title.
+ /// </summary>
+ /// <value>The title.</value>
+ public string? Title { get; set; }
+
+ /// <summary>
/// Gets or sets a value indicating whether this instance is default.
/// </summary>
/// <value><c>true</c> if this instance is default; otherwise, <c>false</c>.</value>
diff --git a/Emby.Naming/Subtitles/SubtitleParser.cs b/Emby.Naming/Subtitles/SubtitleParser.cs
deleted file mode 100644
index 5809c512a..000000000
--- a/Emby.Naming/Subtitles/SubtitleParser.cs
+++ /dev/null
@@ -1,71 +0,0 @@
-using System;
-using System.IO;
-using System.Linq;
-using Emby.Naming.Common;
-using Jellyfin.Extensions;
-
-namespace Emby.Naming.Subtitles
-{
- /// <summary>
- /// Subtitle Parser class.
- /// </summary>
- public class SubtitleParser
- {
- private readonly NamingOptions _options;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="SubtitleParser"/> class.
- /// </summary>
- /// <param name="options"><see cref="NamingOptions"/> object containing SubtitleFileExtensions, SubtitleDefaultFlags, SubtitleForcedFlags and SubtitleFlagDelimiters.</param>
- public SubtitleParser(NamingOptions options)
- {
- _options = options;
- }
-
- /// <summary>
- /// Parse file to determine if is subtitle and <see cref="SubtitleInfo"/>.
- /// </summary>
- /// <param name="path">Path to file.</param>
- /// <returns>Returns null or <see cref="SubtitleInfo"/> object if parsing is successful.</returns>
- public SubtitleInfo? ParseFile(string path)
- {
- if (path.Length == 0)
- {
- return null;
- }
-
- var extension = Path.GetExtension(path);
- if (!_options.SubtitleFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
- {
- return null;
- }
-
- var flags = GetFlags(path);
- var info = new SubtitleInfo(
- path,
- _options.SubtitleDefaultFlags.Any(i => flags.Contains(i, StringComparison.OrdinalIgnoreCase)),
- _options.SubtitleForcedFlags.Any(i => flags.Contains(i, StringComparison.OrdinalIgnoreCase)));
-
- var parts = flags.Where(i => !_options.SubtitleDefaultFlags.Contains(i, StringComparison.OrdinalIgnoreCase)
- && !_options.SubtitleForcedFlags.Contains(i, StringComparison.OrdinalIgnoreCase))
- .ToList();
-
- // Should have a name, language and file extension
- if (parts.Count >= 3)
- {
- info.Language = parts[^2];
- }
-
- return info;
- }
-
- private string[] GetFlags(string path)
- {
- // Note: the tags need be surrounded be either a space ( ), hyphen -, dot . or underscore _.
-
- var file = Path.GetFileName(path);
-
- return file.Split(_options.SubtitleFlagDelimiters, StringSplitOptions.RemoveEmptyEntries);
- }
- }
-}
diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs
index e7efc81d7..82294644b 100644
--- a/Emby.Server.Implementations/ApplicationHost.cs
+++ b/Emby.Server.Implementations/ApplicationHost.cs
@@ -44,7 +44,6 @@ using Emby.Server.Implementations.Serialization;
using Emby.Server.Implementations.Session;
using Emby.Server.Implementations.SyncPlay;
using Emby.Server.Implementations.TV;
-using Emby.Server.Implementations.Udp;
using Emby.Server.Implementations.Updates;
using Jellyfin.Api.Helpers;
using Jellyfin.MediaEncoding.Hls.Playlist;
@@ -104,6 +103,7 @@ using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Prometheus.DotNetRuntime;
+using static MediaBrowser.Controller.Extensions.ConfigurationExtensions;
using WebSocketManager = Emby.Server.Implementations.HttpServer.WebSocketManager;
namespace Emby.Server.Implementations
@@ -150,7 +150,7 @@ namespace Emby.Server.Implementations
/// <param name="loggerFactory">Instance of the <see cref="ILoggerFactory"/> interface.</param>
/// <param name="options">Instance of the <see cref="IStartupOptions"/> interface.</param>
/// <param name="startupConfig">The <see cref="IConfiguration" /> interface.</param>
- public ApplicationHost(
+ protected ApplicationHost(
IServerApplicationPaths applicationPaths,
ILoggerFactory loggerFactory,
IStartupOptions options,
@@ -185,6 +185,11 @@ namespace Emby.Server.Implementations
public event EventHandler HasPendingRestartChanged;
/// <summary>
+ /// Gets the value of the PublishedServerUrl setting.
+ /// </summary>
+ private string PublishedServerUrl => _startupConfig[AddressOverrideKey];
+
+ /// <summary>
/// Gets a value indicating whether this instance can self restart.
/// </summary>
public bool CanSelfRestart => _startupOptions.RestartPath != null;
@@ -260,11 +265,6 @@ namespace Emby.Server.Implementations
/// </summary>
public int HttpsPort { get; private set; }
- /// <summary>
- /// Gets the value of the PublishedServerUrl setting.
- /// </summary>
- public string PublishedServerUrl => _startupOptions.PublishedServerUrl ?? _startupConfig[UdpServer.AddressOverrideConfigKey];
-
/// <inheritdoc />
public Version ApplicationVersion { get; }
diff --git a/Emby.Server.Implementations/Channels/ChannelManager.cs b/Emby.Server.Implementations/Channels/ChannelManager.cs
index a107e7a52..09429c73f 100644
--- a/Emby.Server.Implementations/Channels/ChannelManager.cs
+++ b/Emby.Server.Implementations/Channels/ChannelManager.cs
@@ -39,7 +39,7 @@ namespace Emby.Server.Implementations.Channels
/// <summary>
/// The LiveTV channel manager.
/// </summary>
- public class ChannelManager : IChannelManager
+ public class ChannelManager : IChannelManager, IDisposable
{
private readonly IUserManager _userManager;
private readonly IUserDataManager _userDataManager;
@@ -52,6 +52,7 @@ namespace Emby.Server.Implementations.Channels
private readonly IMemoryCache _memoryCache;
private readonly SemaphoreSlim _resourcePool = new SemaphoreSlim(1, 1);
private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
+ private bool _disposed = false;
/// <summary>
/// Initializes a new instance of the <see cref="ChannelManager"/> class.
@@ -1213,5 +1214,31 @@ namespace Emby.Server.Implementations.Channels
return result;
}
+
+ /// <inheritdoc />
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ /// <summary>
+ /// Releases unmanaged and optionally managed resources.
+ /// </summary>
+ /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
+ protected virtual void Dispose(bool disposing)
+ {
+ if (_disposed)
+ {
+ return;
+ }
+
+ if (disposing)
+ {
+ _resourcePool?.Dispose();
+ }
+
+ _disposed = true;
+ }
}
}
diff --git a/Emby.Server.Implementations/Channels/RefreshChannelsScheduledTask.cs b/Emby.Server.Implementations/Channels/RefreshChannelsScheduledTask.cs
index e5dde48d8..cfd08e653 100644
--- a/Emby.Server.Implementations/Channels/RefreshChannelsScheduledTask.cs
+++ b/Emby.Server.Implementations/Channels/RefreshChannelsScheduledTask.cs
@@ -62,7 +62,7 @@ namespace Emby.Server.Implementations.Channels
public string Key => "RefreshInternetChannels";
/// <inheritdoc />
- public async Task Execute(CancellationToken cancellationToken, IProgress<double> progress)
+ public async Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken)
{
var manager = (ChannelManager)_channelManager;
diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs
index 1da9b4650..b3b383bfd 100644
--- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs
+++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs
@@ -317,11 +317,6 @@ namespace Emby.Server.Implementations.Data
IImageProcessor imageProcessor)
: base(logger)
{
- if (config == null)
- {
- throw new ArgumentNullException(nameof(config));
- }
-
_config = config;
_appHost = appHost;
_localization = localization;
@@ -334,9 +329,6 @@ namespace Emby.Server.Implementations.Data
}
/// <inheritdoc />
- public string Name => "SQLite";
-
- /// <inheritdoc />
protected override int? CacheSize => 20000;
/// <inheritdoc />
@@ -573,22 +565,6 @@ namespace Emby.Server.Implementations.Data
userDataRepo.Initialize(userManager, WriteLock, WriteConnection);
}
- /// <summary>
- /// Save a standard item in the repo.
- /// </summary>
- /// <param name="item">The item.</param>
- /// <param name="cancellationToken">The cancellation token.</param>
- /// <exception cref="ArgumentNullException"><paramref name="item"/> is <c>null</c>.</exception>
- public void SaveItem(BaseItem item, CancellationToken cancellationToken)
- {
- if (item == null)
- {
- throw new ArgumentNullException(nameof(item));
- }
-
- SaveItems(new[] { item }, cancellationToken);
- }
-
public void SaveImages(BaseItem item)
{
if (item == null)
@@ -605,7 +581,7 @@ namespace Emby.Server.Implementations.Data
{
using (var saveImagesStatement = PrepareStatement(db, "Update TypedBaseItems set Images=@Images where guid=@Id"))
{
- saveImagesStatement.TryBind("@Id", item.Id.ToByteArray());
+ saveImagesStatement.TryBind("@Id", item.Id);
saveImagesStatement.TryBind("@Images", SerializeImages(item.ImageInfos));
saveImagesStatement.MoveNext();
@@ -750,7 +726,7 @@ namespace Emby.Server.Implementations.Data
saveItemStatement.TryBindNull("@EndDate");
}
- saveItemStatement.TryBind("@ChannelId", item.ChannelId.Equals(Guid.Empty) ? null : item.ChannelId.ToString("N", CultureInfo.InvariantCulture));
+ saveItemStatement.TryBind("@ChannelId", item.ChannelId.Equals(default) ? null : item.ChannelId.ToString("N", CultureInfo.InvariantCulture));
if (item is IHasProgramAttributes hasProgramAttributes)
{
@@ -780,7 +756,7 @@ namespace Emby.Server.Implementations.Data
saveItemStatement.TryBind("@ProductionYear", item.ProductionYear);
var parentId = item.ParentId;
- if (parentId.Equals(Guid.Empty))
+ if (parentId.Equals(default))
{
saveItemStatement.TryBindNull("@ParentId");
}
@@ -975,7 +951,7 @@ namespace Emby.Server.Implementations.Data
{
saveItemStatement.TryBind("@SeasonName", episode.SeasonName);
- var nullableSeasonId = episode.SeasonId == Guid.Empty ? (Guid?)null : episode.SeasonId;
+ var nullableSeasonId = episode.SeasonId.Equals(default) ? (Guid?)null : episode.SeasonId;
saveItemStatement.TryBind("@SeasonId", nullableSeasonId);
}
@@ -987,7 +963,7 @@ namespace Emby.Server.Implementations.Data
if (item is IHasSeries hasSeries)
{
- var nullableSeriesId = hasSeries.SeriesId.Equals(Guid.Empty) ? (Guid?)null : hasSeries.SeriesId;
+ var nullableSeriesId = hasSeries.SeriesId.Equals(default) ? (Guid?)null : hasSeries.SeriesId;
saveItemStatement.TryBind("@SeriesId", nullableSeriesId);
saveItemStatement.TryBind("@SeriesPresentationUniqueKey", hasSeries.SeriesPresentationUniqueKey);
@@ -1060,7 +1036,7 @@ namespace Emby.Server.Implementations.Data
}
Guid ownerId = item.OwnerId;
- if (ownerId == Guid.Empty)
+ if (ownerId.Equals(default))
{
saveItemStatement.TryBindNull("@OwnerId");
}
@@ -1198,13 +1174,15 @@ namespace Emby.Server.Implementations.Data
bldr.Append(Delimiter)
// Replace delimiters with other characters.
// This can be removed when we migrate to a proper DB.
- .Append(hash.Replace('*', '/').Replace('|', '\\'));
+ .Append(hash.Replace(Delimiter, '/').Replace('|', '\\'));
}
}
internal ItemImageInfo ItemImageInfoFromValueString(ReadOnlySpan<char> value)
{
- var nextSegment = value.IndexOf('*');
+ const char Delimiter = '*';
+
+ var nextSegment = value.IndexOf(Delimiter);
if (nextSegment == -1)
{
return null;
@@ -1212,7 +1190,7 @@ namespace Emby.Server.Implementations.Data
ReadOnlySpan<char> path = value[..nextSegment];
value = value[(nextSegment + 1)..];
- nextSegment = value.IndexOf('*');
+ nextSegment = value.IndexOf(Delimiter);
if (nextSegment == -1)
{
return null;
@@ -1220,7 +1198,7 @@ namespace Emby.Server.Implementations.Data
ReadOnlySpan<char> dateModified = value[..nextSegment];
value = value[(nextSegment + 1)..];
- nextSegment = value.IndexOf('*');
+ nextSegment = value.IndexOf(Delimiter);
if (nextSegment == -1)
{
nextSegment = value.Length;
@@ -1257,7 +1235,7 @@ namespace Emby.Server.Implementations.Data
if (nextSegment + 1 < value.Length - 1)
{
value = value[(nextSegment + 1)..];
- nextSegment = value.IndexOf('*');
+ nextSegment = value.IndexOf(Delimiter);
if (nextSegment == -1 || nextSegment == value.Length)
{
return image;
@@ -1266,7 +1244,7 @@ namespace Emby.Server.Implementations.Data
ReadOnlySpan<char> widthSpan = value[..nextSegment];
value = value[(nextSegment + 1)..];
- nextSegment = value.IndexOf('*');
+ nextSegment = value.IndexOf(Delimiter);
if (nextSegment == -1)
{
nextSegment = value.Length;
@@ -1292,7 +1270,7 @@ namespace Emby.Server.Implementations.Data
var c = value[i];
blurHashSpan[i] = c switch
{
- '/' => '*',
+ '/' => Delimiter,
'\\' => '|',
_ => c
};
@@ -1314,7 +1292,7 @@ namespace Emby.Server.Implementations.Data
/// <exception cref="ArgumentException"><paramr name="id"/> is <seealso cref="Guid.Empty"/>.</exception>
public BaseItem RetrieveItem(Guid id)
{
- if (id == Guid.Empty)
+ if (id.Equals(default))
{
throw new ArgumentException("Guid can't be empty", nameof(id));
}
@@ -2086,7 +2064,7 @@ namespace Emby.Server.Implementations.Data
{
CheckDisposed();
- if (id.Equals(Guid.Empty))
+ if (id.Equals(default))
{
throw new ArgumentNullException(nameof(id));
}
@@ -2492,12 +2470,12 @@ namespace Emby.Server.Implementations.Data
searchTerm = GetCleanValue(searchTerm);
var commandText = statement.SQL;
- if (commandText.IndexOf("@SearchTermStartsWith", StringComparison.OrdinalIgnoreCase) != -1)
+ if (commandText.Contains("@SearchTermStartsWith", StringComparison.OrdinalIgnoreCase))
{
statement.TryBind("@SearchTermStartsWith", searchTerm + "%");
}
- if (commandText.IndexOf("@SearchTermContains", StringComparison.OrdinalIgnoreCase) != -1)
+ if (commandText.Contains("@SearchTermContains", StringComparison.OrdinalIgnoreCase))
{
statement.TryBind("@SearchTermContains", "%" + searchTerm + "%");
}
@@ -2514,17 +2492,17 @@ namespace Emby.Server.Implementations.Data
var commandText = statement.SQL;
- if (commandText.IndexOf("@ItemOfficialRating", StringComparison.OrdinalIgnoreCase) != -1)
+ if (commandText.Contains("@ItemOfficialRating", StringComparison.OrdinalIgnoreCase))
{
statement.TryBind("@ItemOfficialRating", item.OfficialRating);
}
- if (commandText.IndexOf("@ItemProductionYear", StringComparison.OrdinalIgnoreCase) != -1)
+ if (commandText.Contains("@ItemProductionYear", StringComparison.OrdinalIgnoreCase))
{
statement.TryBind("@ItemProductionYear", item.ProductionYear ?? 0);
}
- if (commandText.IndexOf("@SimilarItemId", StringComparison.OrdinalIgnoreCase) != -1)
+ if (commandText.Contains("@SimilarItemId", StringComparison.OrdinalIgnoreCase))
{
statement.TryBind("@SimilarItemId", item.Id);
}
@@ -2758,12 +2736,12 @@ namespace Emby.Server.Implementations.Data
foreach (var providerId in newItem.ProviderIds)
{
- if (providerId.Key == MetadataProvider.TmdbCollection.ToString())
+ if (string.Equals(providerId.Key, nameof(MetadataProvider.TmdbCollection), StringComparison.Ordinal))
{
continue;
}
- if (item.GetProviderId(providerId.Key) == providerId.Value)
+ if (string.Equals(item.GetProviderId(providerId.Key), providerId.Value, StringComparison.Ordinal))
{
if (newItem.SourceType == SourceType.Library)
{
@@ -3185,220 +3163,6 @@ namespace Emby.Server.Implementations.Data
return list;
}
- public List<Tuple<Guid, string>> GetItemIdsWithPath(InternalItemsQuery query)
- {
- if (query == null)
- {
- throw new ArgumentNullException(nameof(query));
- }
-
- CheckDisposed();
-
- var now = DateTime.UtcNow;
-
- var columns = new List<string> { "guid", "path" };
- SetFinalColumnsToSelect(query, columns);
- var commandText = "select " + string.Join(',', columns) + FromText;
-
- var whereClauses = GetWhereClauses(query, null);
- if (whereClauses.Count != 0)
- {
- commandText += " where " + string.Join(" AND ", whereClauses);
- }
-
- commandText += GetGroupBy(query)
- + GetOrderByText(query);
-
- if (query.Limit.HasValue || query.StartIndex.HasValue)
- {
- var offset = query.StartIndex ?? 0;
-
- if (query.Limit.HasValue || offset > 0)
- {
- commandText += " LIMIT " + (query.Limit ?? int.MaxValue).ToString(CultureInfo.InvariantCulture);
- }
-
- if (offset > 0)
- {
- commandText += " OFFSET " + offset.ToString(CultureInfo.InvariantCulture);
- }
- }
-
- var list = new List<Tuple<Guid, string>>();
- using (var connection = GetConnection(true))
- {
- using (var statement = PrepareStatement(connection, commandText))
- {
- if (EnableJoinUserData(query))
- {
- statement.TryBind("@UserId", query.User.InternalId);
- }
-
- // Running this again will bind the params
- GetWhereClauses(query, statement);
-
- foreach (var row in statement.ExecuteQuery())
- {
- var id = row.GetGuid(0);
-
- row.TryGetString(1, out var path);
-
- list.Add(new Tuple<Guid, string>(id, path));
- }
- }
- }
-
- LogQueryTime("GetItemIdsWithPath", commandText, now);
-
- return list;
- }
-
- public QueryResult<Guid> GetItemIds(InternalItemsQuery query)
- {
- if (query == null)
- {
- throw new ArgumentNullException(nameof(query));
- }
-
- CheckDisposed();
-
- if (!query.EnableTotalRecordCount || (!query.Limit.HasValue && (query.StartIndex ?? 0) == 0))
- {
- var returnList = GetItemIdsList(query);
- return new QueryResult<Guid>(
- query.StartIndex,
- returnList.Count,
- returnList);
- }
-
- var now = DateTime.UtcNow;
-
- var columns = new List<string> { "guid" };
- SetFinalColumnsToSelect(query, columns);
- var commandText = "select "
- + string.Join(',', columns)
- + FromText
- + GetJoinUserDataText(query);
-
- var whereClauses = GetWhereClauses(query, null);
-
- var whereText = whereClauses.Count == 0 ?
- string.Empty :
- " where " + string.Join(" AND ", whereClauses);
-
- commandText += whereText
- + GetGroupBy(query)
- + GetOrderByText(query);
-
- if (query.Limit.HasValue || query.StartIndex.HasValue)
- {
- var offset = query.StartIndex ?? 0;
-
- if (query.Limit.HasValue || offset > 0)
- {
- commandText += " LIMIT " + (query.Limit ?? int.MaxValue).ToString(CultureInfo.InvariantCulture);
- }
-
- if (offset > 0)
- {
- commandText += " OFFSET " + offset.ToString(CultureInfo.InvariantCulture);
- }
- }
-
- var isReturningZeroItems = query.Limit.HasValue && query.Limit <= 0;
-
- var statementTexts = new List<string>();
- if (!isReturningZeroItems)
- {
- statementTexts.Add(commandText);
- }
-
- if (query.EnableTotalRecordCount)
- {
- commandText = string.Empty;
-
- List<string> columnsToSelect;
- if (EnableGroupByPresentationUniqueKey(query))
- {
- columnsToSelect = new List<string> { "count (distinct PresentationUniqueKey)" };
- }
- else if (query.GroupBySeriesPresentationUniqueKey)
- {
- columnsToSelect = new List<string> { "count (distinct SeriesPresentationUniqueKey)" };
- }
- else
- {
- columnsToSelect = new List<string> { "count (guid)" };
- }
-
- SetFinalColumnsToSelect(query, columnsToSelect);
- commandText += " select " + string.Join(',', columnsToSelect) + FromText;
-
- commandText += GetJoinUserDataText(query)
- + whereText;
- statementTexts.Add(commandText);
- }
-
- var list = new List<Guid>();
- var result = new QueryResult<Guid>();
- using (var connection = GetConnection(true))
- {
- connection.RunInTransaction(
- db =>
- {
- var statements = PrepareAll(db, statementTexts);
-
- if (!isReturningZeroItems)
- {
- using (var statement = statements[0])
- {
- if (EnableJoinUserData(query))
- {
- statement.TryBind("@UserId", query.User.InternalId);
- }
-
- BindSimilarParams(query, statement);
- BindSearchParams(query, statement);
-
- // Running this again will bind the params
- GetWhereClauses(query, statement);
-
- foreach (var row in statement.ExecuteQuery())
- {
- list.Add(row[0].ReadGuidFromBlob());
- }
- }
- }
-
- if (query.EnableTotalRecordCount)
- {
- using (var statement = statements[statements.Length - 1])
- {
- if (EnableJoinUserData(query))
- {
- statement.TryBind("@UserId", query.User.InternalId);
- }
-
- BindSimilarParams(query, statement);
- BindSearchParams(query, statement);
-
- // Running this again will bind the params
- GetWhereClauses(query, statement);
-
- result.TotalRecordCount = statement.ExecuteQuery().SelectScalarInt().First();
- }
- }
- },
- ReadTransactionMode);
- }
-
- LogQueryTime("GetItemIds", commandText, now);
-
- result.StartIndex = query.StartIndex ?? 0;
- result.Items = list;
- return result;
- }
-
private bool IsAlphaNumeric(string str)
{
if (string.IsNullOrWhiteSpace(str))
@@ -3665,7 +3429,7 @@ namespace Emby.Server.Implementations.Data
whereClauses.Add($"ChannelId in ({inClause})");
}
- if (!query.ParentId.Equals(Guid.Empty))
+ if (!query.ParentId.Equals(default))
{
whereClauses.Add("ParentId=@ParentId");
statement?.TryBind("@ParentId", query.ParentId);
@@ -4025,7 +3789,7 @@ namespace Emby.Server.Implementations.Data
clauses.Add("(guid in (select itemid from itemvalues where CleanValue = (select CleanName from TypedBaseItems where guid=" + paramName + ") and Type<=1))");
if (statement != null)
{
- statement.TryBind(paramName, artistId.ToByteArray());
+ statement.TryBind(paramName, artistId);
}
index++;
@@ -4046,7 +3810,7 @@ namespace Emby.Server.Implementations.Data
clauses.Add("(guid in (select itemid from itemvalues where CleanValue = (select CleanName from TypedBaseItems where guid=" + paramName + ") and Type=1))");
if (statement != null)
{
- statement.TryBind(paramName, artistId.ToByteArray());
+ statement.TryBind(paramName, artistId);
}
index++;
@@ -4067,7 +3831,7 @@ namespace Emby.Server.Implementations.Data
clauses.Add("((select CleanName from TypedBaseItems where guid=" + paramName + ") in (select CleanValue from itemvalues where ItemId=Guid and Type=0) AND (select CleanName from TypedBaseItems where guid=" + paramName + ") not in (select CleanValue from itemvalues where ItemId=Guid and Type=1))");
if (statement != null)
{
- statement.TryBind(paramName, artistId.ToByteArray());
+ statement.TryBind(paramName, artistId);
}
index++;
@@ -4088,7 +3852,7 @@ namespace Emby.Server.Implementations.Data
clauses.Add("Album in (select Name from typedbaseitems where guid=" + paramName + ")");
if (statement != null)
{
- statement.TryBind(paramName, albumId.ToByteArray());
+ statement.TryBind(paramName, albumId);
}
index++;
@@ -4109,7 +3873,7 @@ namespace Emby.Server.Implementations.Data
clauses.Add("(guid not in (select itemid from itemvalues where CleanValue = (select CleanName from TypedBaseItems where guid=" + paramName + ") and Type<=1))");
if (statement != null)
{
- statement.TryBind(paramName, artistId.ToByteArray());
+ statement.TryBind(paramName, artistId);
}
index++;
@@ -4130,7 +3894,7 @@ namespace Emby.Server.Implementations.Data
clauses.Add("(guid in (select itemid from itemvalues where CleanValue = (select CleanName from TypedBaseItems where guid=" + paramName + ") and Type=2))");
if (statement != null)
{
- statement.TryBind(paramName, genreId.ToByteArray());
+ statement.TryBind(paramName, genreId);
}
index++;
@@ -4209,7 +3973,7 @@ namespace Emby.Server.Implementations.Data
if (statement != null)
{
- statement.TryBind(paramName, studioId.ToByteArray());
+ statement.TryBind(paramName, studioId);
}
index++;
@@ -4494,7 +4258,7 @@ namespace Emby.Server.Implementations.Data
var index = 0;
foreach (var pair in query.ExcludeProviderIds)
{
- if (string.Equals(pair.Key, MetadataProvider.TmdbCollection.ToString(), StringComparison.OrdinalIgnoreCase))
+ if (string.Equals(pair.Key, nameof(MetadataProvider.TmdbCollection), StringComparison.OrdinalIgnoreCase))
{
continue;
}
@@ -4524,7 +4288,7 @@ namespace Emby.Server.Implementations.Data
var index = 0;
foreach (var pair in query.HasAnyProviderId)
{
- if (string.Equals(pair.Key, MetadataProvider.TmdbCollection.ToString(), StringComparison.OrdinalIgnoreCase))
+ if (string.Equals(pair.Key, nameof(MetadataProvider.TmdbCollection), StringComparison.OrdinalIgnoreCase))
{
continue;
}
@@ -4942,7 +4706,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
public void DeleteItem(Guid id)
{
- if (id == Guid.Empty)
+ if (id.Equals(default))
{
throw new ArgumentNullException(nameof(id));
}
@@ -4954,7 +4718,8 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
connection.RunInTransaction(
db =>
{
- var idBlob = id.ToByteArray();
+ Span<byte> idBlob = stackalloc byte[16];
+ id.TryWriteBytes(idBlob);
// Delete people
ExecuteWithSingleParam(db, "delete from People where ItemId=@Id", idBlob);
@@ -5003,7 +4768,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
if (whereClauses.Count != 0)
{
- commandText.Append(" where ").Append(string.Join(" AND ", whereClauses));
+ commandText.Append(" where ").AppendJoin(" AND ", whereClauses);
}
commandText.Append(" order by ListOrder");
@@ -5089,16 +4854,16 @@ AND Type = @InternalPersonType)");
statement?.TryBind("@InternalPersonType", typeof(Person).FullName);
}
- if (!query.ItemId.Equals(Guid.Empty))
+ if (!query.ItemId.Equals(default))
{
whereClauses.Add("ItemId=@ItemId");
- statement?.TryBind("@ItemId", query.ItemId.ToByteArray());
+ statement?.TryBind("@ItemId", query.ItemId);
}
- if (!query.AppearsInItemId.Equals(Guid.Empty))
+ if (!query.AppearsInItemId.Equals(default))
{
whereClauses.Add("p.Name in (Select Name from People where ItemId=@AppearsInItemId)");
- statement?.TryBind("@AppearsInItemId", query.AppearsInItemId.ToByteArray());
+ statement?.TryBind("@AppearsInItemId", query.AppearsInItemId);
}
var queryPersonTypes = query.PersonTypes.Where(IsValidPersonType).ToList();
@@ -5151,7 +4916,7 @@ AND Type = @InternalPersonType)");
private void UpdateAncestors(Guid itemId, List<Guid> ancestorIds, IDatabaseConnection db, IStatement deleteAncestorsStatement)
{
- if (itemId.Equals(Guid.Empty))
+ if (itemId.Equals(default))
{
throw new ArgumentNullException(nameof(itemId));
}
@@ -5683,7 +5448,7 @@ AND Type = @InternalPersonType)");
private void UpdateItemValues(Guid itemId, List<(int MagicNumber, string Value)> values, IDatabaseConnection db)
{
- if (itemId.Equals(Guid.Empty))
+ if (itemId.Equals(default))
{
throw new ArgumentNullException(nameof(itemId));
}
@@ -5759,7 +5524,7 @@ AND Type = @InternalPersonType)");
public void UpdatePeople(Guid itemId, List<PersonInfo> people)
{
- if (itemId.Equals(Guid.Empty))
+ if (itemId.Equals(default))
{
throw new ArgumentNullException(nameof(itemId));
}
@@ -5892,7 +5657,7 @@ AND Type = @InternalPersonType)");
using (var statement = PrepareStatement(connection, cmdText))
{
- statement.TryBind("@ItemId", query.ItemId.ToByteArray());
+ statement.TryBind("@ItemId", query.ItemId);
if (query.Type.HasValue)
{
@@ -5914,11 +5679,11 @@ AND Type = @InternalPersonType)");
}
}
- public void SaveMediaStreams(Guid id, List<MediaStream> streams, CancellationToken cancellationToken)
+ public void SaveMediaStreams(Guid id, IReadOnlyList<MediaStream> streams, CancellationToken cancellationToken)
{
CheckDisposed();
- if (id == Guid.Empty)
+ if (id.Equals(default))
{
throw new ArgumentNullException(nameof(id));
}
@@ -5946,7 +5711,7 @@ AND Type = @InternalPersonType)");
}
}
- private void InsertMediaStreams(byte[] idBlob, List<MediaStream> streams, IDatabaseConnection db)
+ private void InsertMediaStreams(byte[] idBlob, IReadOnlyList<MediaStream> streams, IDatabaseConnection db)
{
const int Limit = 10;
var startIndex = 0;
@@ -6254,7 +6019,7 @@ AND Type = @InternalPersonType)");
CancellationToken cancellationToken)
{
CheckDisposed();
- if (id == Guid.Empty)
+ if (id.Equals(default))
{
throw new ArgumentException("Guid can't be empty.", nameof(id));
}
diff --git a/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs b/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs
index 80b8f9ebf..ba86dc156 100644
--- a/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs
+++ b/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs
@@ -26,9 +26,6 @@ namespace Emby.Server.Implementations.Data
DbFilePath = Path.Combine(appPaths.DataPath, "library.db");
}
- /// <inheritdoc />
- public string Name => "SQLite";
-
/// <summary>
/// Opens the connection to the database.
/// </summary>
@@ -102,7 +99,7 @@ namespace Emby.Server.Implementations.Data
continue;
}
- statement.TryBind("@UserId", user.Id.ToByteArray());
+ statement.TryBind("@UserId", user.Id);
statement.TryBind("@InternalUserId", user.InternalId);
statement.MoveNext();
@@ -390,6 +387,7 @@ namespace Emby.Server.Implementations.Data
return userData;
}
+#pragma warning disable CA2215
/// <inheritdoc/>
/// <remarks>
/// There is nothing to dispose here since <see cref="BaseSqliteRepository.WriteLock"/> and
@@ -398,6 +396,10 @@ namespace Emby.Server.Implementations.Data
/// </remarks>
protected override void Dispose(bool dispose)
{
+ // The write lock and connection for the item repository are shared with the user data repository
+ // since they point to the same database. The item repo has responsibility for disposing these two objects,
+ // so the user data repo should not attempt to dispose them as well
}
+#pragma warning restore CA2215
}
}
diff --git a/Emby.Server.Implementations/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs
index f5ca006dd..2b2190b16 100644
--- a/Emby.Server.Implementations/Dto/DtoService.cs
+++ b/Emby.Server.Implementations/Dto/DtoService.cs
@@ -21,7 +21,6 @@ using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Controller.Persistence;
using MediaBrowser.Controller.Playlists;
using MediaBrowser.Controller.Providers;
-using MediaBrowser.Model.Drawing;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Querying;
@@ -738,8 +737,7 @@ namespace Emby.Server.Implementations.Dto
dto.Tags = item.Tags;
}
- var hasAspectRatio = item as IHasAspectRatio;
- if (hasAspectRatio != null)
+ if (item is IHasAspectRatio hasAspectRatio)
{
dto.AspectRatio = hasAspectRatio.AspectRatio;
}
@@ -889,15 +887,13 @@ namespace Emby.Server.Implementations.Dto
dto.CommunityRating = item.CommunityRating;
}
- var supportsPlaceHolders = item as ISupportsPlaceHolders;
- if (supportsPlaceHolders != null && supportsPlaceHolders.IsPlaceHolder)
+ if (item is ISupportsPlaceHolders supportsPlaceHolders && supportsPlaceHolders.IsPlaceHolder)
{
dto.IsPlaceHolder = supportsPlaceHolders.IsPlaceHolder;
}
// Add audio info
- var audio = item as Audio;
- if (audio != null)
+ if (item is Audio audio)
{
dto.Album = audio.Album;
if (audio.ExtraType.HasValue)
@@ -970,8 +966,7 @@ namespace Emby.Server.Implementations.Dto
}).Where(i => i != null).ToArray();
}
- var hasAlbumArtist = item as IHasAlbumArtist;
- if (hasAlbumArtist != null)
+ if (item is IHasAlbumArtist hasAlbumArtist)
{
dto.AlbumArtist = hasAlbumArtist.AlbumArtists.FirstOrDefault();
diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj
index d59db6aa7..a5cc125ec 100644
--- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj
+++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj
@@ -26,12 +26,12 @@
<PackageReference Include="DiscUtils.Udf" Version="0.16.13" />
<PackageReference Include="Jellyfin.XmlTv" Version="10.6.2" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" />
- <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="6.0.0" />
+ <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="6.0.1" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="6.0.0" />
- <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="6.0.1" />
+ <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="6.0.2" />
<PackageReference Include="Mono.Nat" Version="3.0.2" />
- <PackageReference Include="prometheus-net.DotNetRuntime" Version="4.2.2" />
+ <PackageReference Include="prometheus-net.DotNetRuntime" Version="4.2.3" />
<PackageReference Include="sharpcompress" Version="0.30.1" />
<PackageReference Include="SQLitePCL.pretty.netstandard" Version="3.1.0" />
<PackageReference Include="DotNet.Glob" Version="3.1.3" />
diff --git a/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs b/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs
index feaccf9fa..34fdfbe8d 100644
--- a/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs
+++ b/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs
@@ -3,6 +3,8 @@ using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
using Emby.Server.Implementations.Udp;
+using Jellyfin.Networking.Configuration;
+using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Plugins;
using Microsoft.Extensions.Configuration;
@@ -26,6 +28,7 @@ namespace Emby.Server.Implementations.EntryPoints
private readonly ILogger<UdpServerEntryPoint> _logger;
private readonly IServerApplicationHost _appHost;
private readonly IConfiguration _config;
+ private readonly IConfigurationManager _configurationManager;
/// <summary>
/// The UDP server.
@@ -40,14 +43,17 @@ namespace Emby.Server.Implementations.EntryPoints
/// <param name="logger">Instance of the <see cref="ILogger{UdpServerEntryPoint}"/> interface.</param>
/// <param name="appHost">Instance of the <see cref="IServerApplicationHost"/> interface.</param>
/// <param name="configuration">Instance of the <see cref="IConfiguration"/> interface.</param>
+ /// <param name="configurationManager">Instance of the <see cref="IConfigurationManager"/> interface.</param>
public UdpServerEntryPoint(
ILogger<UdpServerEntryPoint> logger,
IServerApplicationHost appHost,
- IConfiguration configuration)
+ IConfiguration configuration,
+ IConfigurationManager configurationManager)
{
_logger = logger;
_appHost = appHost;
_config = configuration;
+ _configurationManager = configurationManager;
}
/// <inheritdoc />
@@ -55,6 +61,11 @@ namespace Emby.Server.Implementations.EntryPoints
{
CheckDisposed();
+ if (_configurationManager.GetNetworkConfiguration().AutoDiscovery)
+ {
+ return Task.CompletedTask;
+ }
+
try
{
_udpServer = new UdpServer(_logger, _appHost, _config, PortNumber);
diff --git a/Emby.Server.Implementations/IO/ManagedFileSystem.cs b/Emby.Server.Implementations/IO/ManagedFileSystem.cs
index 5c86dbbb7..4f8a52f41 100644
--- a/Emby.Server.Implementations/IO/ManagedFileSystem.cs
+++ b/Emby.Server.Implementations/IO/ManagedFileSystem.cs
@@ -581,7 +581,7 @@ namespace Emby.Server.Implementations.IO
}
/// <inheritdoc />
- public virtual List<FileSystemMetadata> GetDrives()
+ public virtual IEnumerable<FileSystemMetadata> GetDrives()
{
// check for ready state to avoid waiting for drives to timeout
// some drives on linux have no actual size or are used for other purposes
@@ -595,7 +595,7 @@ namespace Emby.Server.Implementations.IO
Name = d.Name,
FullName = d.RootDirectory.FullName,
IsDirectory = true
- }).ToList();
+ });
}
/// <inheritdoc />
diff --git a/Emby.Server.Implementations/Images/BaseFolderImageProvider.cs b/Emby.Server.Implementations/Images/BaseFolderImageProvider.cs
index 1c69056d2..6fc7f1ac3 100644
--- a/Emby.Server.Implementations/Images/BaseFolderImageProvider.cs
+++ b/Emby.Server.Implementations/Images/BaseFolderImageProvider.cs
@@ -22,7 +22,7 @@ namespace Emby.Server.Implementations.Images
{
private readonly ILibraryManager _libraryManager;
- public BaseFolderImageProvider(IFileSystem fileSystem, IProviderManager providerManager, IApplicationPaths applicationPaths, IImageProcessor imageProcessor, ILibraryManager libraryManager)
+ protected BaseFolderImageProvider(IFileSystem fileSystem, IProviderManager providerManager, IApplicationPaths applicationPaths, IImageProcessor imageProcessor, ILibraryManager libraryManager)
: base(fileSystem, providerManager, applicationPaths, imageProcessor)
{
_libraryManager = libraryManager;
diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs
index 064fd2372..e3be5627f 100644
--- a/Emby.Server.Implementations/Library/LibraryManager.cs
+++ b/Emby.Server.Implementations/Library/LibraryManager.cs
@@ -45,7 +45,6 @@ using MediaBrowser.Model.IO;
using MediaBrowser.Model.Library;
using MediaBrowser.Model.Querying;
using MediaBrowser.Model.Tasks;
-using MediaBrowser.Providers.MediaInfo;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
using Episode = MediaBrowser.Controller.Entities.TV.Episode;
@@ -680,9 +679,7 @@ namespace Emby.Server.Implementations.Library
if (result?.Items.Count > 0)
{
- var items = new List<BaseItem>();
- items.AddRange(result.Items);
-
+ var items = result.Items;
foreach (var item in items)
{
ResolverHelper.SetInitialItemValues(item, parent, this, directoryService);
@@ -1007,14 +1004,8 @@ namespace Emby.Server.Implementations.Library
return GetNewItemIdInternal(path, typeof(T), forceCaseInsensitiveId);
}
- /// <summary>
- /// Validate and refresh the People sub-set of the IBN.
- /// The items are stored in the db but not loaded into memory until actually requested by an operation.
- /// </summary>
- /// <param name="cancellationToken">The cancellation token.</param>
- /// <param name="progress">The progress.</param>
- /// <returns>Task.</returns>
- public Task ValidatePeople(CancellationToken cancellationToken, IProgress<double> progress)
+ /// <inheritdoc />
+ public Task ValidatePeopleAsync(IProgress<double> progress, CancellationToken cancellationToken)
{
// Ensure the location is available.
Directory.CreateDirectory(_configurationManager.ApplicationPaths.PeoplePath);
@@ -1037,15 +1028,6 @@ namespace Emby.Server.Implementations.Library
}
/// <summary>
- /// Queues the library scan.
- /// </summary>
- public void QueueLibraryScan()
- {
- // Just run the scheduled task so that the user can see it
- _taskManager.QueueScheduledTask<RefreshMediaLibraryTask>();
- }
-
- /// <summary>
/// Validates the media library internal.
/// </summary>
/// <param name="progress">The progress.</param>
@@ -1651,27 +1633,6 @@ namespace Emby.Server.Implementations.Library
}
/// <summary>
- /// Gets all intro files.
- /// </summary>
- /// <returns>IEnumerable{System.String}.</returns>
- public IEnumerable<string> GetAllIntroFiles()
- {
- return IntroProviders.SelectMany(i =>
- {
- try
- {
- return i.GetAllIntroFiles().ToList();
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Error getting intro files");
-
- return new List<string>();
- }
- });
- }
-
- /// <summary>
/// Resolves the intro.
/// </summary>
/// <param name="info">The info.</param>
@@ -2469,24 +2430,6 @@ namespace Emby.Server.Implementations.Library
return item;
}
- public void AddExternalSubtitleStreams(
- List<MediaStream> streams,
- string videoPath,
- string[] files)
- {
- new SubtitleResolver(BaseItem.LocalizationManager).AddExternalSubtitleStreams(streams, videoPath, streams.Count, files);
- }
-
- public BaseItem GetParentItem(string parentId, Guid? userId)
- {
- if (string.IsNullOrEmpty(parentId))
- {
- return GetParentItem((Guid?)null, userId);
- }
-
- return GetParentItem(new Guid(parentId), userId);
- }
-
public BaseItem GetParentItem(Guid? parentId, Guid? userId)
{
if (parentId.HasValue)
@@ -2494,7 +2437,7 @@ namespace Emby.Server.Implementations.Library
return GetItemById(parentId.Value);
}
- if (userId.HasValue && userId != Guid.Empty)
+ if (userId.HasValue && !userId.Equals(default))
{
return GetUserRootFolder();
}
@@ -2784,16 +2727,6 @@ namespace Emby.Server.Implementations.Library
return path;
}
- public string SubstitutePath(string path, string from, string to)
- {
- if (path.TryReplaceSubPath(from, to, out var newPath))
- {
- return newPath;
- }
-
- return path;
- }
-
public List<PersonInfo> GetPeople(InternalPeopleQuery query)
{
return _itemRepository.GetPeople(query);
diff --git a/Emby.Server.Implementations/Library/MediaSourceManager.cs b/Emby.Server.Implementations/Library/MediaSourceManager.cs
index a414e7e16..eb95977ef 100644
--- a/Emby.Server.Implementations/Library/MediaSourceManager.cs
+++ b/Emby.Server.Implementations/Library/MediaSourceManager.cs
@@ -344,7 +344,7 @@ namespace Emby.Server.Implementations.Library
return sources;
}
- private string[] NormalizeLanguage(string language)
+ private IReadOnlyList<string> NormalizeLanguage(string language)
{
if (string.IsNullOrEmpty(language))
{
diff --git a/Emby.Server.Implementations/Library/MediaStreamSelector.cs b/Emby.Server.Implementations/Library/MediaStreamSelector.cs
index da0c89c13..c5abb9a0a 100644
--- a/Emby.Server.Implementations/Library/MediaStreamSelector.cs
+++ b/Emby.Server.Implementations/Library/MediaStreamSelector.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
#pragma warning disable CS1591
using System;
@@ -13,10 +11,9 @@ namespace Emby.Server.Implementations.Library
{
public static class MediaStreamSelector
{
- public static int? GetDefaultAudioStreamIndex(List<MediaStream> streams, string[] preferredLanguages, bool preferDefaultTrack)
+ public static int? GetDefaultAudioStreamIndex(IReadOnlyList<MediaStream> streams, IReadOnlyList<string> preferredLanguages, bool preferDefaultTrack)
{
- streams = GetSortedStreams(streams, MediaStreamType.Audio, preferredLanguages)
- .ToList();
+ var sortedStreams = GetSortedStreams(streams, MediaStreamType.Audio, preferredLanguages);
if (preferDefaultTrack)
{
@@ -28,24 +25,15 @@ namespace Emby.Server.Implementations.Library
}
}
- var stream = streams.FirstOrDefault();
-
- if (stream != null)
- {
- return stream.Index;
- }
-
- return null;
+ return sortedStreams.FirstOrDefault()?.Index;
}
public static int? GetDefaultSubtitleStreamIndex(
IEnumerable<MediaStream> streams,
- string[] preferredLanguages,
+ IReadOnlyList<string> preferredLanguages,
SubtitlePlaybackMode mode,
string audioTrackLanguage)
{
- MediaStream stream = null;
-
if (mode == SubtitlePlaybackMode.None)
{
return null;
@@ -59,6 +47,7 @@ namespace Emby.Server.Implementations.Library
.ThenByDescending(x => x.IsDefault)
.ToList();
+ MediaStream? stream = null;
if (mode == SubtitlePlaybackMode.Default)
{
// Prefer embedded metadata over smart logic
@@ -95,26 +84,27 @@ namespace Emby.Server.Implementations.Library
return stream?.Index;
}
- private static IEnumerable<MediaStream> GetSortedStreams(IEnumerable<MediaStream> streams, MediaStreamType type, string[] languagePreferences)
+ private static IEnumerable<MediaStream> GetSortedStreams(IEnumerable<MediaStream> streams, MediaStreamType type, IReadOnlyList<string> languagePreferences)
{
// Give some preference to external text subs for better performance
- return streams.Where(i => i.Type == type)
+ return streams
+ .Where(i => i.Type == type)
.OrderBy(i =>
- {
- var index = FindIndex(languagePreferences, i.Language);
-
- return index == -1 ? 100 : index;
- })
- .ThenBy(i => GetBooleanOrderBy(i.IsDefault))
- .ThenBy(i => GetBooleanOrderBy(i.SupportsExternalStream))
- .ThenBy(i => GetBooleanOrderBy(i.IsTextSubtitleStream))
- .ThenBy(i => GetBooleanOrderBy(i.IsExternal))
- .ThenBy(i => i.Index);
+ {
+ var index = languagePreferences.FindIndex(x => string.Equals(x, i.Language, StringComparison.OrdinalIgnoreCase));
+
+ return index == -1 ? 100 : index;
+ })
+ .ThenBy(i => GetBooleanOrderBy(i.IsDefault))
+ .ThenBy(i => GetBooleanOrderBy(i.SupportsExternalStream))
+ .ThenBy(i => GetBooleanOrderBy(i.IsTextSubtitleStream))
+ .ThenBy(i => GetBooleanOrderBy(i.IsExternal))
+ .ThenBy(i => i.Index);
}
public static void SetSubtitleStreamScores(
- List<MediaStream> streams,
- string[] preferredLanguages,
+ IReadOnlyList<MediaStream> streams,
+ IReadOnlyList<string> preferredLanguages,
SubtitlePlaybackMode mode,
string audioTrackLanguage)
{
@@ -123,15 +113,14 @@ namespace Emby.Server.Implementations.Library
return;
}
- streams = GetSortedStreams(streams, MediaStreamType.Subtitle, preferredLanguages)
- .ToList();
+ var sortedStreams = GetSortedStreams(streams, MediaStreamType.Subtitle, preferredLanguages);
var filteredStreams = new List<MediaStream>();
if (mode == SubtitlePlaybackMode.Default)
{
// Prefer embedded metadata over smart logic
- filteredStreams = streams.Where(s => s.IsForced || s.IsDefault)
+ filteredStreams = sortedStreams.Where(s => s.IsForced || s.IsDefault)
.ToList();
}
else if (mode == SubtitlePlaybackMode.Smart)
@@ -139,54 +128,37 @@ namespace Emby.Server.Implementations.Library
// Prefer smart logic over embedded metadata
if (!preferredLanguages.Contains(audioTrackLanguage, StringComparison.OrdinalIgnoreCase))
{
- filteredStreams = streams.Where(s => !s.IsForced && preferredLanguages.Contains(s.Language, StringComparison.OrdinalIgnoreCase))
+ filteredStreams = sortedStreams.Where(s => !s.IsForced && preferredLanguages.Contains(s.Language, StringComparison.OrdinalIgnoreCase))
.ToList();
}
}
else if (mode == SubtitlePlaybackMode.Always)
{
// always load the most suitable full subtitles
- filteredStreams = streams.Where(s => !s.IsForced)
- .ToList();
+ filteredStreams = sortedStreams.Where(s => !s.IsForced).ToList();
}
else if (mode == SubtitlePlaybackMode.OnlyForced)
{
// always load the most suitable full subtitles
- filteredStreams = streams.Where(s => s.IsForced).ToList();
+ filteredStreams = sortedStreams.Where(s => s.IsForced).ToList();
}
// load forced subs if we have found no suitable full subtitles
- if (filteredStreams.Count == 0)
- {
- filteredStreams = streams
- .Where(s => s.IsForced && string.Equals(s.Language, audioTrackLanguage, StringComparison.OrdinalIgnoreCase))
- .ToList();
- }
+ var iterStreams = filteredStreams.Count == 0
+ ? sortedStreams.Where(s => s.IsForced && string.Equals(s.Language, audioTrackLanguage, StringComparison.OrdinalIgnoreCase))
+ : filteredStreams;
- foreach (var stream in filteredStreams)
+ foreach (var stream in iterStreams)
{
stream.Score = GetSubtitleScore(stream, preferredLanguages);
}
}
- private static int FindIndex(string[] list, string value)
- {
- for (var i = 0; i < list.Length; i++)
- {
- if (string.Equals(list[i], value, StringComparison.OrdinalIgnoreCase))
- {
- return i;
- }
- }
-
- return -1;
- }
-
- private static int GetSubtitleScore(MediaStream stream, string[] languagePreferences)
+ private static int GetSubtitleScore(MediaStream stream, IReadOnlyList<string> languagePreferences)
{
var values = new List<int>();
- var index = FindIndex(languagePreferences, stream.Language);
+ var index = languagePreferences.FindIndex(x => string.Equals(x, stream.Language, StringComparison.OrdinalIgnoreCase));
values.Add(index == -1 ? 0 : 100 - index);
diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs
index f1d4b6097..582e61d79 100644
--- a/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs
+++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs
@@ -24,7 +24,7 @@ using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.LiveTv.EmbyTV
{
- public class EncodedRecorder : IRecorder
+ public class EncodedRecorder : IRecorder, IDisposable
{
private readonly ILogger _logger;
private readonly IMediaEncoder _mediaEncoder;
@@ -36,6 +36,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
private Stream _logFileStream;
private string _targetPath;
private Process _process;
+ private bool _disposed = false;
public EncodedRecorder(
ILogger logger,
@@ -323,5 +324,35 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
_logger.LogError(ex, "Error reading ffmpeg recording log");
}
}
+
+ /// <inheritdoc />
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ /// <summary>
+ /// Releases unmanaged and optionally managed resources.
+ /// </summary>
+ /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
+ protected virtual void Dispose(bool disposing)
+ {
+ if (_disposed)
+ {
+ return;
+ }
+
+ if (disposing)
+ {
+ _logFileStream?.Dispose();
+ _process?.Dispose();
+ }
+
+ _logFileStream = null;
+ _process = null;
+
+ _disposed = true;
+ }
}
}
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs
index a8440102d..ffa0d9b6a 100644
--- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs
+++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs
@@ -28,7 +28,7 @@ using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.LiveTv.Listings
{
- public class SchedulesDirect : IListingsProvider
+ public class SchedulesDirect : IListingsProvider, IDisposable
{
private const string ApiUrl = "https://json.schedulesdirect.org/20141201";
@@ -39,6 +39,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
private readonly ConcurrentDictionary<string, NameValuePair> _tokens = new ConcurrentDictionary<string, NameValuePair>();
private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
private DateTime _lastErrorResponse;
+ private bool _disposed = false;
public SchedulesDirect(
ILogger<SchedulesDirect> logger,
@@ -58,8 +59,8 @@ namespace Emby.Server.Implementations.LiveTv.Listings
{
var dates = new List<string>();
- var start = new List<DateTime> { startDateUtc, startDateUtc.ToLocalTime() }.Min().Date;
- var end = new List<DateTime> { endDateUtc, endDateUtc.ToLocalTime() }.Max().Date;
+ var start = new[] { startDateUtc, startDateUtc.ToLocalTime() }.Min().Date;
+ var end = new[] { endDateUtc, endDateUtc.ToLocalTime() }.Max().Date;
while (start <= end)
{
@@ -822,5 +823,31 @@ namespace Emby.Server.Implementations.LiveTv.Listings
return list;
}
+
+ /// <inheritdoc />
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ /// <summary>
+ /// Releases unmanaged and optionally managed resources.
+ /// </summary>
+ /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
+ protected virtual void Dispose(bool disposing)
+ {
+ if (_disposed)
+ {
+ return;
+ }
+
+ if (disposing)
+ {
+ _tokenSemaphore?.Dispose();
+ }
+
+ _disposed = true;
+ }
}
}
diff --git a/Emby.Server.Implementations/LiveTv/LiveTvManager.cs b/Emby.Server.Implementations/LiveTv/LiveTvManager.cs
index 6a9a3077c..71a29e3cb 100644
--- a/Emby.Server.Implementations/LiveTv/LiveTvManager.cs
+++ b/Emby.Server.Implementations/LiveTv/LiveTvManager.cs
@@ -39,7 +39,7 @@ namespace Emby.Server.Implementations.LiveTv
/// <summary>
/// Class LiveTvManager.
/// </summary>
- public class LiveTvManager : ILiveTvManager, IDisposable
+ public class LiveTvManager : ILiveTvManager
{
private const int MaxGuideDays = 14;
private const string ExternalServiceTag = "ExternalServiceId";
@@ -63,8 +63,6 @@ namespace Emby.Server.Implementations.LiveTv
private ITunerHost[] _tunerHosts = Array.Empty<ITunerHost>();
private IListingsProvider[] _listingProviders = Array.Empty<IListingsProvider>();
- private bool _disposed = false;
-
public LiveTvManager(
IServerConfigurationManager config,
ILogger<LiveTvManager> logger,
@@ -312,7 +310,7 @@ namespace Emby.Server.Implementations.LiveTv
{
if (isVideo)
{
- mediaSource.MediaStreams.AddRange(new List<MediaStream>
+ mediaSource.MediaStreams = new MediaStream[]
{
new MediaStream
{
@@ -329,11 +327,11 @@ namespace Emby.Server.Implementations.LiveTv
// Set the index to -1 because we don't know the exact index of the audio stream within the container
Index = -1
}
- });
+ };
}
else
{
- mediaSource.MediaStreams.AddRange(new List<MediaStream>
+ mediaSource.MediaStreams = new MediaStream[]
{
new MediaStream
{
@@ -341,7 +339,7 @@ namespace Emby.Server.Implementations.LiveTv
// Set the index to -1 because we don't know the exact index of the audio stream within the container
Index = -1
}
- });
+ };
}
}
@@ -2092,36 +2090,6 @@ namespace Emby.Server.Implementations.LiveTv
};
}
- /// <inheritdoc />
- public void Dispose()
- {
- Dispose(true);
- GC.SuppressFinalize(this);
- }
-
- /// <summary>
- /// Releases unmanaged and - optionally - managed resources.
- /// </summary>
- /// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
- protected virtual void Dispose(bool dispose)
- {
- if (_disposed)
- {
- return;
- }
-
- if (dispose)
- {
- // TODO: Dispose stuff
- }
-
- _services = null;
- _listingProviders = null;
- _tunerHosts = null;
-
- _disposed = true;
- }
-
private LiveTvServiceInfo[] GetServiceInfos()
{
return Services.Select(GetServiceInfo).ToArray();
diff --git a/Emby.Server.Implementations/LiveTv/RefreshGuideScheduledTask.cs b/Emby.Server.Implementations/LiveTv/RefreshGuideScheduledTask.cs
index 15df0dcf1..72bbdd14a 100644
--- a/Emby.Server.Implementations/LiveTv/RefreshGuideScheduledTask.cs
+++ b/Emby.Server.Implementations/LiveTv/RefreshGuideScheduledTask.cs
@@ -50,7 +50,7 @@ namespace Emby.Server.Implementations.LiveTv
public string Key => "RefreshGuide";
/// <inheritdoc />
- public Task Execute(CancellationToken cancellationToken, IProgress<double> progress)
+ public Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken)
{
var manager = (LiveTvManager)_liveTvManager;
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs
index 532790019..e0eaa8e58 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs
@@ -446,7 +446,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
{
Path = url,
Protocol = MediaProtocol.Udp,
- MediaStreams = new List<MediaStream>
+ MediaStreams = new MediaStream[]
{
new MediaStream
{
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs
index dd83f9a53..2a468e14d 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs
@@ -170,7 +170,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
{
Path = path,
Protocol = protocol,
- MediaStreams = new List<MediaStream>
+ MediaStreams = new MediaStream[]
{
new MediaStream
{
diff --git a/Emby.Server.Implementations/Localization/Core/cs.json b/Emby.Server.Implementations/Localization/Core/cs.json
index 65a31e676..7ee8d1040 100644
--- a/Emby.Server.Implementations/Localization/Core/cs.json
+++ b/Emby.Server.Implementations/Localization/Core/cs.json
@@ -77,7 +77,7 @@
"SubtitleDownloadFailureFromForItem": "Stažení titulků pro {1} z {0} selhalo",
"Sync": "Synchronizace",
"System": "Systém",
- "TvShows": "TV seriály",
+ "TvShows": "Seriály",
"User": "Uživatel",
"UserCreatedWithName": "Uživatel {0} byl vytvořen",
"UserDeletedWithName": "Uživatel {0} byl smazán",
diff --git a/Emby.Server.Implementations/Localization/Core/fa.json b/Emby.Server.Implementations/Localization/Core/fa.json
index 6960ff007..8e8eef867 100644
--- a/Emby.Server.Implementations/Localization/Core/fa.json
+++ b/Emby.Server.Implementations/Localization/Core/fa.json
@@ -119,5 +119,6 @@
"TaskCleanActivityLogDescription": "ورودی‌های قدیمی‌تر از سن تنظیم شده در سیاهه فعالیت را حذف می‌کند.",
"TaskCleanActivityLog": "پاکسازی سیاهه فعالیت",
"Undefined": "تعریف نشده",
- "TaskOptimizeDatabase": "بهینه سازی پایگاه داده"
+ "TaskOptimizeDatabase": "بهینه سازی پایگاه داده",
+ "TaskOptimizeDatabaseDescription": "فشرده سازی پایگاه داده و باز کردن فضای آزاد.اجرای این گزینه بعد از اسکن کردن کتابخانه یا تغییرات دیگر که روی پایگاه داده تأثیر میگذارند میتواند کارایی را بهبود ببخشد."
}
diff --git a/Emby.Server.Implementations/Localization/Core/vi.json b/Emby.Server.Implementations/Localization/Core/vi.json
index d80f1760d..d0e08d8ee 100644
--- a/Emby.Server.Implementations/Localization/Core/vi.json
+++ b/Emby.Server.Implementations/Localization/Core/vi.json
@@ -13,7 +13,7 @@
"Songs": "Bài Hát",
"Sync": "Đồng Bộ",
"ValueSpecialEpisodeName": "Đặc Biệt - {0}",
- "Albums": "",
+ "Albums": "Album",
"Artists": "Ca Sĩ",
"TaskDownloadMissingSubtitlesDescription": "Tìm kiếm phụ đề bị thiếu trên Internet dựa trên cấu hình dữ liệu mô tả.",
"TaskDownloadMissingSubtitles": "Tải Xuống Phụ Đề Bị Thiếu",
diff --git a/Emby.Server.Implementations/Localization/LocalizationManager.cs b/Emby.Server.Implementations/Localization/LocalizationManager.cs
index dbd70342a..281dbb00b 100644
--- a/Emby.Server.Implementations/Localization/LocalizationManager.cs
+++ b/Emby.Server.Implementations/Localization/LocalizationManager.cs
@@ -147,13 +147,7 @@ namespace Emby.Server.Implementations.Localization
threeletterNames = new[] { parts[0], parts[1] };
}
- list.Add(new CultureDto
- {
- DisplayName = name,
- Name = name,
- ThreeLetterISOLanguageNames = threeletterNames,
- TwoLetterISOLanguageName = twoCharName
- });
+ list.Add(new CultureDto(name, name, twoCharName, threeletterNames));
}
}
diff --git a/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs b/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs
index 299f10544..2c4d6884d 100644
--- a/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs
@@ -414,7 +414,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
CurrentCancellationTokenSource.CancelAfter(TimeSpan.FromTicks(options.MaxRuntimeTicks.Value));
}
- await ScheduledTask.Execute(CurrentCancellationTokenSource.Token, progress).ConfigureAwait(false);
+ await ScheduledTask.ExecuteAsync(progress, CurrentCancellationTokenSource.Token).ConfigureAwait(false);
status = TaskCompletionStatus.Completed;
}
@@ -757,6 +757,10 @@ namespace Emby.Server.Implementations.ScheduledTasks
var trigger = triggerInfo.Item2;
trigger.Triggered -= OnTriggerTriggered;
trigger.Stop();
+ if (trigger is IDisposable disposable)
+ {
+ disposable.Dispose();
+ }
}
}
}
diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs
index a5786a3d7..0bf0838fa 100644
--- a/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs
@@ -88,13 +88,8 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
};
}
- /// <summary>
- /// Returns the task to be executed.
- /// </summary>
- /// <param name="cancellationToken">The cancellation token.</param>
- /// <param name="progress">The progress.</param>
- /// <returns>Task.</returns>
- public async Task Execute(CancellationToken cancellationToken, IProgress<double> progress)
+ /// <inheritdoc />
+ public async Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken)
{
var videos = _libraryManager.GetItemList(new InternalItemsQuery
{
diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanActivityLogTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanActivityLogTask.cs
index 79886cb52..776079044 100644
--- a/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanActivityLogTask.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanActivityLogTask.cs
@@ -57,12 +57,12 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
public bool IsLogged => true;
/// <inheritdoc />
- public Task Execute(CancellationToken cancellationToken, IProgress<double> progress)
+ public Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken)
{
var retentionDays = _serverConfigurationManager.Configuration.ActivityLogRetentionDays;
if (!retentionDays.HasValue || retentionDays < 0)
{
- throw new Exception($"Activity Log Retention days must be at least 0. Currently: {retentionDays}");
+ throw new InvalidOperationException($"Activity Log Retention days must be at least 0. Currently: {retentionDays}");
}
var startDate = DateTime.UtcNow.AddDays(-retentionDays.Value);
diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs
index 0941902fc..03935b384 100644
--- a/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs
@@ -79,19 +79,14 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
};
}
- /// <summary>
- /// Returns the task to be executed.
- /// </summary>
- /// <param name="cancellationToken">The cancellation token.</param>
- /// <param name="progress">The progress.</param>
- /// <returns>Task.</returns>
- public Task Execute(CancellationToken cancellationToken, IProgress<double> progress)
+ /// <inheritdoc />
+ public Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken)
{
var minDateModified = DateTime.UtcNow.AddDays(-30);
try
{
- DeleteCacheFilesFromDirectory(cancellationToken, _applicationPaths.CachePath, minDateModified, progress);
+ DeleteCacheFilesFromDirectory(_applicationPaths.CachePath, minDateModified, progress, cancellationToken);
}
catch (DirectoryNotFoundException)
{
@@ -104,7 +99,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
try
{
- DeleteCacheFilesFromDirectory(cancellationToken, _applicationPaths.TempDirectory, minDateModified, progress);
+ DeleteCacheFilesFromDirectory(_applicationPaths.TempDirectory, minDateModified, progress, cancellationToken);
}
catch (DirectoryNotFoundException)
{
@@ -117,11 +112,11 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
/// <summary>
/// Deletes the cache files from directory with a last write time less than a given date.
/// </summary>
- /// <param name="cancellationToken">The task cancellation token.</param>
/// <param name="directory">The directory.</param>
/// <param name="minDateModified">The min date modified.</param>
/// <param name="progress">The progress.</param>
- private void DeleteCacheFilesFromDirectory(CancellationToken cancellationToken, string directory, DateTime minDateModified, IProgress<double> progress)
+ /// <param name="cancellationToken">The task cancellation token.</param>
+ private void DeleteCacheFilesFromDirectory(string directory, DateTime minDateModified, IProgress<double> progress, CancellationToken cancellationToken)
{
var filesToDelete = _fileSystem.GetFiles(directory, true)
.Where(f => _fileSystem.GetLastWriteTimeUtc(f) < minDateModified)
diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteLogFileTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteLogFileTask.cs
index fedb5deb0..9739d7327 100644
--- a/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteLogFileTask.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteLogFileTask.cs
@@ -69,13 +69,8 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
};
}
- /// <summary>
- /// Returns the task to be executed.
- /// </summary>
- /// <param name="cancellationToken">The cancellation token.</param>
- /// <param name="progress">The progress.</param>
- /// <returns>Task.</returns>
- public Task Execute(CancellationToken cancellationToken, IProgress<double> progress)
+ /// <inheritdoc />
+ public Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken)
{
// Delete log files more than n days old
var minDateModified = DateTime.UtcNow.AddDays(-_configurationManager.CommonConfiguration.LogFileRetentionDays);
diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteTranscodeFileTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteTranscodeFileTask.cs
index 099d781cd..e4e565c64 100644
--- a/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteTranscodeFileTask.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteTranscodeFileTask.cs
@@ -78,18 +78,13 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
};
}
- /// <summary>
- /// Returns the task to be executed.
- /// </summary>
- /// <param name="cancellationToken">The cancellation token.</param>
- /// <param name="progress">The progress.</param>
- /// <returns>Task.</returns>
- public Task Execute(CancellationToken cancellationToken, IProgress<double> progress)
+ /// <inheritdoc />
+ public Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken)
{
var minDateModified = DateTime.UtcNow.AddDays(-1);
progress.Report(50);
- DeleteTempFilesFromDirectory(cancellationToken, _configurationManager.GetTranscodePath(), minDateModified, progress);
+ DeleteTempFilesFromDirectory(_configurationManager.GetTranscodePath(), minDateModified, progress, cancellationToken);
return Task.CompletedTask;
}
@@ -97,11 +92,11 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
/// <summary>
/// Deletes the transcoded temp files from directory with a last write time less than a given date.
/// </summary>
- /// <param name="cancellationToken">The task cancellation token.</param>
/// <param name="directory">The directory.</param>
/// <param name="minDateModified">The min date modified.</param>
/// <param name="progress">The progress.</param>
- private void DeleteTempFilesFromDirectory(CancellationToken cancellationToken, string directory, DateTime minDateModified, IProgress<double> progress)
+ /// <param name="cancellationToken">The task cancellation token.</param>
+ private void DeleteTempFilesFromDirectory(string directory, DateTime minDateModified, IProgress<double> progress, CancellationToken cancellationToken)
{
var filesToDelete = _fileSystem.GetFiles(directory, true)
.Where(f => _fileSystem.GetLastWriteTimeUtc(f) < minDateModified)
diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/OptimizeDatabaseTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/OptimizeDatabaseTask.cs
index 35a4aeef6..98e45fa46 100644
--- a/Emby.Server.Implementations/ScheduledTasks/Tasks/OptimizeDatabaseTask.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/OptimizeDatabaseTask.cs
@@ -69,13 +69,8 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
};
}
- /// <summary>
- /// Returns the task to be executed.
- /// </summary>
- /// <param name="cancellationToken">The cancellation token.</param>
- /// <param name="progress">The progress.</param>
- /// <returns>Task.</returns>
- public Task Execute(CancellationToken cancellationToken, IProgress<double> progress)
+ /// <inheritdoc />
+ public Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken)
{
_logger.LogInformation("Optimizing and vacuuming jellyfin.db...");
diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/PeopleValidationTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/PeopleValidationTask.cs
index 34780111b..7d60ea731 100644
--- a/Emby.Server.Implementations/ScheduledTasks/Tasks/PeopleValidationTask.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/PeopleValidationTask.cs
@@ -62,15 +62,10 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
};
}
- /// <summary>
- /// Returns the task to be executed.
- /// </summary>
- /// <param name="cancellationToken">The cancellation token.</param>
- /// <param name="progress">The progress.</param>
- /// <returns>Task.</returns>
- public Task Execute(CancellationToken cancellationToken, IProgress<double> progress)
+ /// <inheritdoc />
+ public Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken)
{
- return _libraryManager.ValidatePeople(cancellationToken, progress);
+ return _libraryManager.ValidatePeopleAsync(progress, cancellationToken);
}
}
}
diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/PluginUpdateTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/PluginUpdateTask.cs
index b3973cecb..443649e6e 100644
--- a/Emby.Server.Implementations/ScheduledTasks/Tasks/PluginUpdateTask.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/PluginUpdateTask.cs
@@ -68,13 +68,8 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
yield return new TaskTriggerInfo { Type = TaskTriggerInfo.TriggerInterval, IntervalTicks = TimeSpan.FromHours(24).Ticks };
}
- /// <summary>
- /// Update installed plugins.
- /// </summary>
- /// <param name="cancellationToken">The cancellation token.</param>
- /// <param name="progress">The progress.</param>
- /// <returns><see cref="Task" />.</returns>
- public async Task Execute(CancellationToken cancellationToken, IProgress<double> progress)
+ /// <inheritdoc />
+ public async Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken)
{
progress.Report(0);
diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/RefreshMediaLibraryTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/RefreshMediaLibraryTask.cs
index 7c27ae384..065008157 100644
--- a/Emby.Server.Implementations/ScheduledTasks/Tasks/RefreshMediaLibraryTask.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/RefreshMediaLibraryTask.cs
@@ -58,13 +58,8 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
};
}
- /// <summary>
- /// Executes the internal.
- /// </summary>
- /// <param name="cancellationToken">The cancellation token.</param>
- /// <param name="progress">The progress.</param>
- /// <returns>Task.</returns>
- public Task Execute(CancellationToken cancellationToken, IProgress<double> progress)
+ /// <inheritdoc />
+ public Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
diff --git a/Emby.Server.Implementations/ScheduledTasks/Triggers/DailyTrigger.cs b/Emby.Server.Implementations/ScheduledTasks/Triggers/DailyTrigger.cs
index dc5eb7391..63f11a22c 100644
--- a/Emby.Server.Implementations/ScheduledTasks/Triggers/DailyTrigger.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/Triggers/DailyTrigger.cs
@@ -8,10 +8,11 @@ namespace Emby.Server.Implementations.ScheduledTasks.Triggers
/// <summary>
/// Represents a task trigger that fires everyday.
/// </summary>
- public sealed class DailyTrigger : ITaskTrigger
+ public sealed class DailyTrigger : ITaskTrigger, IDisposable
{
private readonly TimeSpan _timeOfDay;
private Timer? _timer;
+ private bool _disposed = false;
/// <summary>
/// Initializes a new instance of the <see cref="DailyTrigger"/> class.
@@ -71,6 +72,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Triggers
private void DisposeTimer()
{
_timer?.Dispose();
+ _timer = null;
}
/// <summary>
@@ -80,5 +82,18 @@ namespace Emby.Server.Implementations.ScheduledTasks.Triggers
{
Triggered?.Invoke(this, EventArgs.Empty);
}
+
+ /// <inheritdoc />
+ public void Dispose()
+ {
+ if (_disposed)
+ {
+ return;
+ }
+
+ DisposeTimer();
+
+ _disposed = true;
+ }
}
}
diff --git a/Emby.Server.Implementations/ScheduledTasks/Triggers/IntervalTrigger.cs b/Emby.Server.Implementations/ScheduledTasks/Triggers/IntervalTrigger.cs
index 927f57e95..3eb800199 100644
--- a/Emby.Server.Implementations/ScheduledTasks/Triggers/IntervalTrigger.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/Triggers/IntervalTrigger.cs
@@ -9,11 +9,12 @@ namespace Emby.Server.Implementations.ScheduledTasks.Triggers
/// <summary>
/// Represents a task trigger that runs repeatedly on an interval.
/// </summary>
- public sealed class IntervalTrigger : ITaskTrigger
+ public sealed class IntervalTrigger : ITaskTrigger, IDisposable
{
private readonly TimeSpan _interval;
private DateTime _lastStartDate;
private Timer? _timer;
+ private bool _disposed = false;
/// <summary>
/// Initializes a new instance of the <see cref="IntervalTrigger"/> class.
@@ -89,6 +90,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Triggers
private void DisposeTimer()
{
_timer?.Dispose();
+ _timer = null;
}
/// <summary>
@@ -104,5 +106,18 @@ namespace Emby.Server.Implementations.ScheduledTasks.Triggers
Triggered(this, EventArgs.Empty);
}
}
+
+ /// <inheritdoc />
+ public void Dispose()
+ {
+ if (_disposed)
+ {
+ return;
+ }
+
+ DisposeTimer();
+
+ _disposed = true;
+ }
}
}
diff --git a/Emby.Server.Implementations/ScheduledTasks/Triggers/WeeklyTrigger.cs b/Emby.Server.Implementations/ScheduledTasks/Triggers/WeeklyTrigger.cs
index 2392b20fd..fab49f2fb 100644
--- a/Emby.Server.Implementations/ScheduledTasks/Triggers/WeeklyTrigger.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/Triggers/WeeklyTrigger.cs
@@ -8,11 +8,12 @@ namespace Emby.Server.Implementations.ScheduledTasks.Triggers
/// <summary>
/// Represents a task trigger that fires on a weekly basis.
/// </summary>
- public sealed class WeeklyTrigger : ITaskTrigger
+ public sealed class WeeklyTrigger : ITaskTrigger, IDisposable
{
private readonly TimeSpan _timeOfDay;
private readonly DayOfWeek _dayOfWeek;
private Timer? _timer;
+ private bool _disposed = false;
/// <summary>
/// Initializes a new instance of the <see cref="WeeklyTrigger"/> class.
@@ -94,6 +95,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Triggers
private void DisposeTimer()
{
_timer?.Dispose();
+ _timer = null;
}
/// <summary>
@@ -103,5 +105,18 @@ namespace Emby.Server.Implementations.ScheduledTasks.Triggers
{
Triggered?.Invoke(this, EventArgs.Empty);
}
+
+ /// <inheritdoc />
+ public void Dispose()
+ {
+ if (_disposed)
+ {
+ return;
+ }
+
+ DisposeTimer();
+
+ _disposed = true;
+ }
}
}
diff --git a/Emby.Server.Implementations/TV/TVSeriesManager.cs b/Emby.Server.Implementations/TV/TVSeriesManager.cs
index a18af27f3..a47650a32 100644
--- a/Emby.Server.Implementations/TV/TVSeriesManager.cs
+++ b/Emby.Server.Implementations/TV/TVSeriesManager.cs
@@ -140,7 +140,7 @@ namespace Emby.Server.Implementations.TV
var currentUser = user;
var allNextUp = seriesKeys
- .Select(i => GetNextUp(i, currentUser, dtoOptions));
+ .Select(i => GetNextUp(i, currentUser, dtoOptions, request.Rewatching));
// If viewing all next up for all series, remove first episodes
// But if that returns empty, keep those first episodes (avoid completely empty view)
@@ -186,9 +186,9 @@ namespace Emby.Server.Implementations.TV
/// Gets the next up.
/// </summary>
/// <returns>Task{Episode}.</returns>
- private Tuple<DateTime, Func<Episode>> GetNextUp(string seriesKey, User user, DtoOptions dtoOptions)
+ private Tuple<DateTime, Func<Episode>> GetNextUp(string seriesKey, User user, DtoOptions dtoOptions, bool rewatching)
{
- var lastWatchedEpisode = _libraryManager.GetItemList(new InternalItemsQuery(user)
+ var lastQuery = new InternalItemsQuery(user)
{
AncestorWithPresentationUniqueKey = null,
SeriesPresentationUniqueKey = seriesKey,
@@ -202,23 +202,43 @@ namespace Emby.Server.Implementations.TV
Fields = new[] { ItemFields.SortName },
EnableImages = false
}
- }).Cast<Episode>().FirstOrDefault();
+ };
+
+ if (rewatching)
+ {
+ // find last watched by date played, not by newest episode watched
+ lastQuery.OrderBy = new[] { (ItemSortBy.DatePlayed, SortOrder.Descending) };
+ }
+
+ var lastWatchedEpisode = _libraryManager.GetItemList(lastQuery).Cast<Episode>().FirstOrDefault();
Func<Episode> getEpisode = () =>
{
- var nextEpisode = _libraryManager.GetItemList(new InternalItemsQuery(user)
+ var nextQuery = new InternalItemsQuery(user)
{
AncestorWithPresentationUniqueKey = null,
SeriesPresentationUniqueKey = seriesKey,
IncludeItemTypes = new[] { BaseItemKind.Episode },
OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) },
Limit = 1,
- IsPlayed = false,
+ IsPlayed = rewatching,
IsVirtualItem = false,
ParentIndexNumberNotEquals = 0,
MinSortName = lastWatchedEpisode?.SortName,
DtoOptions = dtoOptions
- }).Cast<Episode>().FirstOrDefault();
+ };
+
+ Episode nextEpisode;
+ if (rewatching)
+ {
+ nextQuery.Limit = 2;
+ // get watched episode after most recently watched
+ nextEpisode = _libraryManager.GetItemList(nextQuery).Cast<Episode>().ElementAtOrDefault(1);
+ }
+ else
+ {
+ nextEpisode = _libraryManager.GetItemList(nextQuery).Cast<Episode>().FirstOrDefault();
+ }
if (_configurationManager.Configuration.DisplaySpecialsWithinSeasons)
{
@@ -228,7 +248,7 @@ namespace Emby.Server.Implementations.TV
SeriesPresentationUniqueKey = seriesKey,
ParentIndexNumber = 0,
IncludeItemTypes = new[] { BaseItemKind.Episode },
- IsPlayed = false,
+ IsPlayed = rewatching,
IsVirtualItem = false,
DtoOptions = dtoOptions
})
diff --git a/Emby.Server.Implementations/Udp/UdpServer.cs b/Emby.Server.Implementations/Udp/UdpServer.cs
index c8ab99de4..937e792f5 100644
--- a/Emby.Server.Implementations/Udp/UdpServer.cs
+++ b/Emby.Server.Implementations/Udp/UdpServer.cs
@@ -9,6 +9,7 @@ using MediaBrowser.Controller;
using MediaBrowser.Model.ApiClient;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
+using static MediaBrowser.Controller.Extensions.ConfigurationExtensions;
namespace Emby.Server.Implementations.Udp
{
@@ -18,11 +19,6 @@ namespace Emby.Server.Implementations.Udp
public sealed class UdpServer : IDisposable
{
/// <summary>
- /// Address Override Configuration Key.
- /// </summary>
- public const string AddressOverrideConfigKey = "PublishedServerUrl";
-
- /// <summary>
/// The _logger.
/// </summary>
private readonly ILogger _logger;
@@ -60,7 +56,7 @@ namespace Emby.Server.Implementations.Udp
private async Task RespondToV2Message(EndPoint endpoint, CancellationToken cancellationToken)
{
- string? localUrl = _config[AddressOverrideConfigKey];
+ string? localUrl = _config[AddressOverrideKey];
if (string.IsNullOrEmpty(localUrl))
{
localUrl = _appHost.GetSmartApiUrl(((IPEndPoint)endpoint).Address);
@@ -68,7 +64,7 @@ namespace Emby.Server.Implementations.Udp
if (string.IsNullOrEmpty(localUrl))
{
- _logger.LogWarning("Unable to respond to udp request because the local ip address could not be determined.");
+ _logger.LogWarning("Unable to respond to server discovery request because the local ip address could not be determined.");
return;
}
diff --git a/Jellyfin.Api/Controllers/SearchController.cs b/Jellyfin.Api/Controllers/SearchController.cs
index 26acb4cdc..6fcd2ae40 100644
--- a/Jellyfin.Api/Controllers/SearchController.cs
+++ b/Jellyfin.Api/Controllers/SearchController.cs
@@ -121,11 +121,7 @@ namespace Jellyfin.Api.Controllers
IsSports = isSports
});
- return new SearchHintResult
- {
- TotalRecordCount = result.TotalRecordCount,
- SearchHints = result.Items.Select(GetSearchHintResult).ToArray()
- };
+ return new SearchHintResult(result.Items.Select(GetSearchHintResult).ToArray(), result.TotalRecordCount);
}
/// <summary>
diff --git a/Jellyfin.Api/Controllers/TvShowsController.cs b/Jellyfin.Api/Controllers/TvShowsController.cs
index 9425fe519..5d39d906f 100644
--- a/Jellyfin.Api/Controllers/TvShowsController.cs
+++ b/Jellyfin.Api/Controllers/TvShowsController.cs
@@ -68,6 +68,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="nextUpDateCutoff">Optional. Starting date of shows to show in Next Up section.</param>
/// <param name="enableTotalRecordCount">Whether to enable the total records count. Defaults to true.</param>
/// <param name="disableFirstEpisode">Whether to disable sending the first episode in a series as next up.</param>
+ /// <param name="rewatching">Whether to get a rewatching next up instead of standard next up.</param>
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the next up episodes.</returns>
[HttpGet("NextUp")]
[ProducesResponseType(StatusCodes.Status200OK)]
@@ -84,7 +85,8 @@ namespace Jellyfin.Api.Controllers
[FromQuery] bool? enableUserData,
[FromQuery] DateTime? nextUpDateCutoff,
[FromQuery] bool enableTotalRecordCount = true,
- [FromQuery] bool disableFirstEpisode = false)
+ [FromQuery] bool disableFirstEpisode = false,
+ [FromQuery] bool rewatching = false)
{
var options = new DtoOptions { Fields = fields }
.AddClientFields(Request)
@@ -100,7 +102,8 @@ namespace Jellyfin.Api.Controllers
UserId = userId ?? Guid.Empty,
EnableTotalRecordCount = enableTotalRecordCount,
DisableFirstEpisode = disableFirstEpisode,
- NextUpDateCutoff = nextUpDateCutoff ?? DateTime.MinValue
+ NextUpDateCutoff = nextUpDateCutoff ?? DateTime.MinValue,
+ Rewatching = rewatching
},
options);
diff --git a/Jellyfin.Api/Helpers/ProgressiveFileStream.cs b/Jellyfin.Api/Helpers/ProgressiveFileStream.cs
index 3fa07720a..6f5b64ea8 100644
--- a/Jellyfin.Api/Helpers/ProgressiveFileStream.cs
+++ b/Jellyfin.Api/Helpers/ProgressiveFileStream.cs
@@ -83,10 +83,10 @@ namespace Jellyfin.Api.Helpers
int totalBytesRead = 0;
var stopwatch = Stopwatch.StartNew();
- while (KeepReading(stopwatch.ElapsedMilliseconds))
+ while (true)
{
totalBytesRead += _stream.Read(buffer);
- if (totalBytesRead > 0)
+ if (StopReading(totalBytesRead, stopwatch.ElapsedMilliseconds))
{
break;
}
@@ -109,10 +109,10 @@ namespace Jellyfin.Api.Helpers
int totalBytesRead = 0;
var stopwatch = Stopwatch.StartNew();
- while (KeepReading(stopwatch.ElapsedMilliseconds))
+ while (true)
{
totalBytesRead += await _stream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false);
- if (totalBytesRead > 0)
+ if (StopReading(totalBytesRead, stopwatch.ElapsedMilliseconds))
{
break;
}
@@ -172,10 +172,12 @@ namespace Jellyfin.Api.Helpers
}
}
- private bool KeepReading(long elapsed)
+ private bool StopReading(int bytesRead, long elapsed)
{
- // If the job is null it's a live stream and will require user action to close, but don't keep it open indefinitely
- return !_job?.HasExited ?? elapsed < _timeoutMs;
+ // It should stop reading when anything has been successfully read or if the job has exited
+ // If the job is null, however, it's a live stream and will require user action to close,
+ // but don't keep it open indefinitely if it isn't reading anything
+ return bytesRead > 0 || (_job?.HasExited ?? elapsed >= _timeoutMs);
}
}
}
diff --git a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs
index 3526d56c6..56a54fb1d 100644
--- a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs
+++ b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs
@@ -753,6 +753,8 @@ namespace Jellyfin.Api.Helpers
job.HasExited = true;
job.ExitCode = process.ExitCode;
+ ReportTranscodingProgress(job, state, null, null, null, null, null);
+
_logger.LogDebug("Disposing stream resources");
state.Dispose();
diff --git a/Jellyfin.Api/Jellyfin.Api.csproj b/Jellyfin.Api/Jellyfin.Api.csproj
index 3a7d39365..c5b240e92 100644
--- a/Jellyfin.Api/Jellyfin.Api.csproj
+++ b/Jellyfin.Api/Jellyfin.Api.csproj
@@ -17,7 +17,7 @@
</PropertyGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.AspNetCore.Authorization" Version="6.0.1" />
+ <PackageReference Include="Microsoft.AspNetCore.Authorization" Version="6.0.2" />
<PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.2.3" />
<PackageReference Include="Swashbuckle.AspNetCore.ReDoc" Version="6.2.3" />
diff --git a/Jellyfin.Networking/Manager/NetworkManager.cs b/Jellyfin.Networking/Manager/NetworkManager.cs
index 58b30ad2d..b16dc5390 100644
--- a/Jellyfin.Networking/Manager/NetworkManager.cs
+++ b/Jellyfin.Networking/Manager/NetworkManager.cs
@@ -628,7 +628,6 @@ namespace Jellyfin.Networking.Manager
}
TrustAllIP6Interfaces = config.TrustAllIP6Interfaces;
- // UdpHelper.EnableMultiSocketBinding = config.EnableMultiSocketBinding;
if (string.IsNullOrEmpty(MockNetworkSettings))
{
@@ -750,7 +749,7 @@ namespace Jellyfin.Networking.Manager
bool partial = token[^1] == '*';
if (partial)
{
- token = token[0..^1];
+ token = token[..^1];
}
foreach ((string interfc, int interfcIndex) in _interfaceNames)
diff --git a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj
index a83c2e2e4..b7dab82af 100644
--- a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj
+++ b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj
@@ -26,14 +26,14 @@
</ItemGroup>
<ItemGroup>
- <PackageReference Include="System.Linq.Async" Version="5.1.0" />
- <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.1" />
- <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="6.0.1" />
- <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.1">
+ <PackageReference Include="System.Linq.Async" Version="6.0.1" />
+ <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.2" />
+ <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="6.0.2" />
+ <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
- <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="6.0.1">
+ <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="6.0.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
diff --git a/Jellyfin.Server/Jellyfin.Server.csproj b/Jellyfin.Server/Jellyfin.Server.csproj
index f4e9f2197..6743a24aa 100644
--- a/Jellyfin.Server/Jellyfin.Server.csproj
+++ b/Jellyfin.Server/Jellyfin.Server.csproj
@@ -35,10 +35,10 @@
<ItemGroup>
<PackageReference Include="CommandLineParser" Version="2.8.0" />
- <PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="6.0.0" />
+ <PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="6.0.1" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="6.0.0" />
- <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="6.0.1" />
- <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="6.0.1" />
+ <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="6.0.2" />
+ <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="6.0.2" />
<PackageReference Include="prometheus-net" Version="5.0.2" />
<PackageReference Include="prometheus-net.AspNetCore" Version="5.0.2" />
<PackageReference Include="Serilog.AspNetCore" Version="4.1.0" />
diff --git a/Jellyfin.Server/Middleware/BaseUrlRedirectionMiddleware.cs b/Jellyfin.Server/Middleware/BaseUrlRedirectionMiddleware.cs
index 3e5982eed..e0c112d60 100644
--- a/Jellyfin.Server/Middleware/BaseUrlRedirectionMiddleware.cs
+++ b/Jellyfin.Server/Middleware/BaseUrlRedirectionMiddleware.cs
@@ -5,7 +5,7 @@ using MediaBrowser.Controller.Configuration;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
-using ConfigurationExtensions = MediaBrowser.Controller.Extensions.ConfigurationExtensions;
+using static MediaBrowser.Controller.Extensions.ConfigurationExtensions;
namespace Jellyfin.Server.Middleware
{
@@ -65,7 +65,7 @@ namespace Jellyfin.Server.Middleware
{
// Always redirect back to the default path if the base prefix is invalid, missing, or is the full path.
_logger.LogDebug("Normalizing an URL at {LocalPath}", localPath);
- httpContext.Response.Redirect(baseUrlPrefix + "/" + _configuration[ConfigurationExtensions.DefaultRedirectKey]);
+ httpContext.Response.Redirect(baseUrlPrefix + "/" + _configuration[DefaultRedirectKey]);
return;
}
}
@@ -74,7 +74,7 @@ namespace Jellyfin.Server.Middleware
{
// Always redirect back to the default path if root is requested.
_logger.LogDebug("Normalizing an URL at {LocalPath}", localPath);
- httpContext.Response.Redirect("/" + _configuration[ConfigurationExtensions.DefaultRedirectKey]);
+ httpContext.Response.Redirect("/" + _configuration[DefaultRedirectKey]);
return;
}
diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs
index ce11c63f9..fc871f064 100644
--- a/Jellyfin.Server/Program.cs
+++ b/Jellyfin.Server/Program.cs
@@ -13,7 +13,6 @@ using Emby.Server.Implementations;
using Jellyfin.Server.Implementations;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Net;
-using MediaBrowser.Controller.Extensions;
using MediaBrowser.Model.IO;
using Microsoft.AspNetCore.Hosting;
using Microsoft.EntityFrameworkCore;
@@ -26,7 +25,7 @@ using Microsoft.Extensions.Logging.Abstractions;
using Serilog;
using Serilog.Extensions.Logging;
using SQLitePCL;
-using ConfigurationExtensions = MediaBrowser.Controller.Extensions.ConfigurationExtensions;
+using static MediaBrowser.Controller.Extensions.ConfigurationExtensions;
using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace Jellyfin.Server
@@ -168,7 +167,7 @@ namespace Jellyfin.Server
"server, you may set the '--nowebclient' command line flag, or set" +
"'{ConfigKey}=false' in your config settings.",
webContentPath,
- ConfigurationExtensions.HostWebClientKey);
+ HostWebClientKey);
Environment.ExitCode = 1;
return;
}
@@ -583,7 +582,7 @@ namespace Jellyfin.Server
var inMemoryDefaultConfig = ConfigurationOptions.DefaultConfiguration;
if (startupConfig != null && !startupConfig.HostWebClient())
{
- inMemoryDefaultConfig[ConfigurationExtensions.DefaultRedirectKey] = "api-docs/swagger";
+ inMemoryDefaultConfig[DefaultRedirectKey] = "api-docs/swagger";
}
return config
diff --git a/Jellyfin.Server/StartupOptions.cs b/Jellyfin.Server/StartupOptions.cs
index a1cecc8c6..84ebde68c 100644
--- a/Jellyfin.Server/StartupOptions.cs
+++ b/Jellyfin.Server/StartupOptions.cs
@@ -1,8 +1,7 @@
using System.Collections.Generic;
using CommandLine;
using Emby.Server.Implementations;
-using Emby.Server.Implementations.Udp;
-using MediaBrowser.Controller.Extensions;
+using static MediaBrowser.Controller.Extensions.ConfigurationExtensions;
namespace Jellyfin.Server
{
@@ -86,17 +85,17 @@ namespace Jellyfin.Server
if (NoWebClient)
{
- config.Add(ConfigurationExtensions.HostWebClientKey, bool.FalseString);
+ config.Add(HostWebClientKey, bool.FalseString);
}
if (PublishedServerUrl != null)
{
- config.Add(UdpServer.AddressOverrideConfigKey, PublishedServerUrl);
+ config.Add(AddressOverrideKey, PublishedServerUrl);
}
if (FFmpegPath != null)
{
- config.Add(ConfigurationExtensions.FfmpegPathKey, FFmpegPath);
+ config.Add(FfmpegPathKey, FFmpegPath);
}
return config;
diff --git a/MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs b/MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs
index f9285c768..957ce6744 100644
--- a/MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs
+++ b/MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs
@@ -15,6 +15,11 @@ namespace MediaBrowser.Controller.Extensions
public const string DefaultRedirectKey = "DefaultRedirectPath";
/// <summary>
+ /// The key for the address override option.
+ /// </summary>
+ public const string AddressOverrideKey = "PublishedServerUrl";
+
+ /// <summary>
/// The key for a setting that indicates whether the application should host web client content.
/// </summary>
public const string HostWebClientKey = "hostwebclient";
diff --git a/MediaBrowser.Controller/Library/IIntroProvider.cs b/MediaBrowser.Controller/Library/IIntroProvider.cs
index a74d1b9f0..4a9721acb 100644
--- a/MediaBrowser.Controller/Library/IIntroProvider.cs
+++ b/MediaBrowser.Controller/Library/IIntroProvider.cs
@@ -24,11 +24,5 @@ namespace MediaBrowser.Controller.Library
/// <param name="user">The user.</param>
/// <returns>IEnumerable{System.String}.</returns>
Task<IEnumerable<IntroInfo>> GetIntros(BaseItem item, Jellyfin.Data.Entities.User user);
-
- /// <summary>
- /// Gets all intro files.
- /// </summary>
- /// <returns>IEnumerable{System.String}.</returns>
- IEnumerable<string> GetAllIntroFiles();
}
}
diff --git a/MediaBrowser.Controller/Library/ILibraryManager.cs b/MediaBrowser.Controller/Library/ILibraryManager.cs
index 2b0193771..313d27ce6 100644
--- a/MediaBrowser.Controller/Library/ILibraryManager.cs
+++ b/MediaBrowser.Controller/Library/ILibraryManager.cs
@@ -138,10 +138,10 @@ namespace MediaBrowser.Controller.Library
/// Validate and refresh the People sub-set of the IBN.
/// The items are stored in the db but not loaded into memory until actually requested by an operation.
/// </summary>
- /// <param name="cancellationToken">The cancellation token.</param>
/// <param name="progress">The progress.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task.</returns>
- Task ValidatePeople(CancellationToken cancellationToken, IProgress<double> progress);
+ Task ValidatePeopleAsync(IProgress<double> progress, CancellationToken cancellationToken);
/// <summary>
/// Reloads the root media folder.
@@ -151,11 +151,6 @@ namespace MediaBrowser.Controller.Library
/// <returns>Task.</returns>
Task ValidateMediaLibrary(IProgress<double> progress, CancellationToken cancellationToken);
- /// <summary>
- /// Queues the library scan.
- /// </summary>
- void QueueLibraryScan();
-
Task UpdateImagesAsync(BaseItem item, bool forceUpdate = false);
/// <summary>
@@ -182,12 +177,6 @@ namespace MediaBrowser.Controller.Library
Task<IEnumerable<Video>> GetIntros(BaseItem item, User user);
/// <summary>
- /// Gets all intro files.
- /// </summary>
- /// <returns>IEnumerable{System.String}.</returns>
- IEnumerable<string> GetAllIntroFiles();
-
- /// <summary>
/// Adds the parts.
/// </summary>
/// <param name="rules">The rules.</param>
@@ -508,15 +497,6 @@ namespace MediaBrowser.Controller.Library
string GetPathAfterNetworkSubstitution(string path, BaseItem ownerItem = null);
/// <summary>
- /// Substitutes the path.
- /// </summary>
- /// <param name="path">The path.</param>
- /// <param name="from">From.</param>
- /// <param name="to">To.</param>
- /// <returns>System.String.</returns>
- string SubstitutePath(string path, string from, string to);
-
- /// <summary>
/// Converts the image to local.
/// </summary>
/// <param name="item">The item.</param>
@@ -587,15 +567,8 @@ namespace MediaBrowser.Controller.Library
int GetCount(InternalItemsQuery query);
- void AddExternalSubtitleStreams(
- List<MediaStream> streams,
- string videoPath,
- string[] files);
-
Task RunMetadataSavers(BaseItem item, ItemUpdateType updateReason);
- BaseItem GetParentItem(string parentId, Guid? userId);
-
BaseItem GetParentItem(Guid? parentId, Guid? userId);
}
}
diff --git a/MediaBrowser.Controller/LiveTv/LiveTvChannel.cs b/MediaBrowser.Controller/LiveTv/LiveTvChannel.cs
index e63874f21..335222da9 100644
--- a/MediaBrowser.Controller/LiveTv/LiveTvChannel.cs
+++ b/MediaBrowser.Controller/LiveTv/LiveTvChannel.cs
@@ -134,7 +134,7 @@ namespace MediaBrowser.Controller.LiveTv
{
Id = Id.ToString("N", CultureInfo.InvariantCulture),
Protocol = PathProtocol ?? MediaProtocol.File,
- MediaStreams = new List<MediaStream>(),
+ MediaStreams = Array.Empty<MediaStream>(),
Name = Name,
Path = Path,
RunTimeTicks = RunTimeTicks,
diff --git a/MediaBrowser.Controller/Persistence/IItemRepository.cs b/MediaBrowser.Controller/Persistence/IItemRepository.cs
index 837bf0bb2..24f7b5cd3 100644
--- a/MediaBrowser.Controller/Persistence/IItemRepository.cs
+++ b/MediaBrowser.Controller/Persistence/IItemRepository.cs
@@ -15,16 +15,9 @@ namespace MediaBrowser.Controller.Persistence
/// <summary>
/// Provides an interface to implement an Item repository.
/// </summary>
- public interface IItemRepository : IRepository
+ public interface IItemRepository : IDisposable
{
/// <summary>
- /// Saves an item.
- /// </summary>
- /// <param name="item">The item.</param>
- /// <param name="cancellationToken">The cancellation token.</param>
- void SaveItem(BaseItem item, CancellationToken cancellationToken);
-
- /// <summary>
/// Deletes the item.
/// </summary>
/// <param name="id">The identifier.</param>
@@ -81,7 +74,7 @@ namespace MediaBrowser.Controller.Persistence
/// <param name="id">The identifier.</param>
/// <param name="streams">The streams.</param>
/// <param name="cancellationToken">The cancellation token.</param>
- void SaveMediaStreams(Guid id, List<MediaStream> streams, CancellationToken cancellationToken);
+ void SaveMediaStreams(Guid id, IReadOnlyList<MediaStream> streams, CancellationToken cancellationToken);
/// <summary>
/// Gets the media attachments.
@@ -99,13 +92,6 @@ namespace MediaBrowser.Controller.Persistence
void SaveMediaAttachments(Guid id, IReadOnlyList<MediaAttachment> attachments, CancellationToken cancellationToken);
/// <summary>
- /// Gets the item ids.
- /// </summary>
- /// <param name="query">The query.</param>
- /// <returns>IEnumerable&lt;Guid&gt;.</returns>
- QueryResult<Guid> GetItemIds(InternalItemsQuery query);
-
- /// <summary>
/// Gets the items.
/// </summary>
/// <param name="query">The query.</param>
@@ -141,13 +127,6 @@ namespace MediaBrowser.Controller.Persistence
List<string> GetPeopleNames(InternalPeopleQuery query);
/// <summary>
- /// Gets the item ids with path.
- /// </summary>
- /// <param name="query">The query.</param>
- /// <returns>QueryResult&lt;Tuple&lt;Guid, System.String&gt;&gt;.</returns>
- List<Tuple<Guid, string>> GetItemIdsWithPath(InternalItemsQuery query);
-
- /// <summary>
/// Gets the item list.
/// </summary>
/// <param name="query">The query.</param>
diff --git a/MediaBrowser.Controller/Persistence/IRepository.cs b/MediaBrowser.Controller/Persistence/IRepository.cs
deleted file mode 100644
index 42f285076..000000000
--- a/MediaBrowser.Controller/Persistence/IRepository.cs
+++ /dev/null
@@ -1,16 +0,0 @@
-using System;
-
-namespace MediaBrowser.Controller.Persistence
-{
- /// <summary>
- /// Provides a base interface for all the repository interfaces.
- /// </summary>
- public interface IRepository : IDisposable
- {
- /// <summary>
- /// Gets the name of the repository.
- /// </summary>
- /// <value>The name.</value>
- string Name { get; }
- }
-}
diff --git a/MediaBrowser.Controller/Persistence/IUserDataRepository.cs b/MediaBrowser.Controller/Persistence/IUserDataRepository.cs
index c43acfb6d..f2fb2826a 100644
--- a/MediaBrowser.Controller/Persistence/IUserDataRepository.cs
+++ b/MediaBrowser.Controller/Persistence/IUserDataRepository.cs
@@ -1,5 +1,6 @@
#nullable disable
+using System;
using System.Collections.Generic;
using System.Threading;
using MediaBrowser.Controller.Entities;
@@ -9,7 +10,7 @@ namespace MediaBrowser.Controller.Persistence
/// <summary>
/// Provides an interface to implement a UserData repository.
/// </summary>
- public interface IUserDataRepository : IRepository
+ public interface IUserDataRepository : IDisposable
{
/// <summary>
/// Saves the user data.
diff --git a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs
index 00f51d0eb..f4842d368 100644
--- a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs
+++ b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs
@@ -434,7 +434,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
throw;
}
- var ranToCompletion = await process.WaitForExitAsync(TimeSpan.FromMinutes(5)).ConfigureAwait(false);
+ var ranToCompletion = await process.WaitForExitAsync(TimeSpan.FromMinutes(30)).ConfigureAwait(false);
if (!ranToCompletion)
{
diff --git a/MediaBrowser.Model/Configuration/EmbeddedSubtitleOptions.cs b/MediaBrowser.Model/Configuration/EmbeddedSubtitleOptions.cs
new file mode 100644
index 000000000..42f07dbff
--- /dev/null
+++ b/MediaBrowser.Model/Configuration/EmbeddedSubtitleOptions.cs
@@ -0,0 +1,30 @@
+namespace MediaBrowser.Model.Configuration
+{
+ /// <summary>
+ /// An enum representing the options to disable embedded subs.
+ /// </summary>
+ public enum EmbeddedSubtitleOptions
+ {
+
+ /// <summary>
+ /// Allow all embedded subs.
+ /// </summary>
+ AllowAll = 0,
+
+ /// <summary>
+ /// Allow only embedded subs that are text based.
+ /// </summary>
+ AllowText = 1,
+
+ /// <summary>
+ /// Allow only embedded subs that are image based.
+ /// </summary>
+ AllowImage = 2,
+
+ /// <summary>
+ /// Disable all embedded subs.
+ /// </summary>
+ AllowNone = 3,
+ }
+
+}
diff --git a/MediaBrowser.Model/Configuration/LibraryOptions.cs b/MediaBrowser.Model/Configuration/LibraryOptions.cs
index d3ce6aa7f..ad3bce86e 100644
--- a/MediaBrowser.Model/Configuration/LibraryOptions.cs
+++ b/MediaBrowser.Model/Configuration/LibraryOptions.cs
@@ -15,6 +15,7 @@ namespace MediaBrowser.Model.Configuration
SkipSubtitlesIfAudioTrackMatches = true;
RequirePerfectSubtitleMatch = true;
+ AllowEmbeddedSubtitles = EmbeddedSubtitleOptions.AllowAll;
AutomaticallyAddToCollection = true;
EnablePhotos = true;
@@ -84,6 +85,8 @@ namespace MediaBrowser.Model.Configuration
public bool AutomaticallyAddToCollection { get; set; }
+ public EmbeddedSubtitleOptions AllowEmbeddedSubtitles { get; set; }
+
public TypeOptions[] TypeOptions { get; set; }
public TypeOptions? GetTypeOptions(string type)
diff --git a/MediaBrowser.Model/Dlna/ContentFeatureBuilder.cs b/MediaBrowser.Model/Dlna/ContentFeatureBuilder.cs
index 58b06ca1d..6e129246b 100644
--- a/MediaBrowser.Model/Dlna/ContentFeatureBuilder.cs
+++ b/MediaBrowser.Model/Dlna/ContentFeatureBuilder.cs
@@ -115,7 +115,7 @@ namespace MediaBrowser.Model.Dlna
return "DLNA.ORG_PN=" + orgPn + orgOp + orgCi + dlnaflags;
}
- public static List<string> BuildVideoHeader(
+ public static IEnumerable<string> BuildVideoHeader(
DeviceProfile profile,
string container,
string videoCodec,
diff --git a/MediaBrowser.Model/Dlna/DlnaProfileType.cs b/MediaBrowser.Model/Dlna/DlnaProfileType.cs
index e30ed0f3c..c1a663bf1 100644
--- a/MediaBrowser.Model/Dlna/DlnaProfileType.cs
+++ b/MediaBrowser.Model/Dlna/DlnaProfileType.cs
@@ -6,6 +6,7 @@ namespace MediaBrowser.Model.Dlna
{
Audio = 0,
Video = 1,
- Photo = 2
+ Photo = 2,
+ Subtitle = 3
}
}
diff --git a/MediaBrowser.Model/Dlna/StreamInfo.cs b/MediaBrowser.Model/Dlna/StreamInfo.cs
index cf8465067..a678c54e7 100644
--- a/MediaBrowser.Model/Dlna/StreamInfo.cs
+++ b/MediaBrowser.Model/Dlna/StreamInfo.cs
@@ -675,7 +675,7 @@ namespace MediaBrowser.Model.Dlna
return string.Format(CultureInfo.InvariantCulture, "{0}/videos/{1}/stream{2}?{3}", baseUrl, ItemId, extension, queryString);
}
- private static List<NameValuePair> BuildParams(StreamInfo item, string accessToken)
+ private static IEnumerable<NameValuePair> BuildParams(StreamInfo item, string accessToken)
{
var list = new List<NameValuePair>();
@@ -805,34 +805,12 @@ namespace MediaBrowser.Model.Dlna
return list;
}
- public List<SubtitleStreamInfo> GetExternalSubtitles(ITranscoderSupport transcoderSupport, bool includeSelectedTrackOnly, string baseUrl, string accessToken)
- {
- return GetExternalSubtitles(transcoderSupport, includeSelectedTrackOnly, false, baseUrl, accessToken);
- }
-
- public List<SubtitleStreamInfo> GetExternalSubtitles(ITranscoderSupport transcoderSupport, bool includeSelectedTrackOnly, bool enableAllProfiles, string baseUrl, string accessToken)
- {
- var list = GetSubtitleProfiles(transcoderSupport, includeSelectedTrackOnly, enableAllProfiles, baseUrl, accessToken);
- var newList = new List<SubtitleStreamInfo>();
-
- // First add the selected track
- foreach (SubtitleStreamInfo stream in list)
- {
- if (stream.DeliveryMethod == SubtitleDeliveryMethod.External)
- {
- newList.Add(stream);
- }
- }
-
- return newList;
- }
-
- public List<SubtitleStreamInfo> GetSubtitleProfiles(ITranscoderSupport transcoderSupport, bool includeSelectedTrackOnly, string baseUrl, string accessToken)
+ public IEnumerable<SubtitleStreamInfo> GetSubtitleProfiles(ITranscoderSupport transcoderSupport, bool includeSelectedTrackOnly, string baseUrl, string accessToken)
{
return GetSubtitleProfiles(transcoderSupport, includeSelectedTrackOnly, false, baseUrl, accessToken);
}
- public List<SubtitleStreamInfo> GetSubtitleProfiles(ITranscoderSupport transcoderSupport, bool includeSelectedTrackOnly, bool enableAllProfiles, string baseUrl, string accessToken)
+ public IEnumerable<SubtitleStreamInfo> GetSubtitleProfiles(ITranscoderSupport transcoderSupport, bool includeSelectedTrackOnly, bool enableAllProfiles, string baseUrl, string accessToken)
{
var list = new List<SubtitleStreamInfo>();
diff --git a/MediaBrowser.Model/Dto/MediaSourceInfo.cs b/MediaBrowser.Model/Dto/MediaSourceInfo.cs
index ec3b37efa..049e14333 100644
--- a/MediaBrowser.Model/Dto/MediaSourceInfo.cs
+++ b/MediaBrowser.Model/Dto/MediaSourceInfo.cs
@@ -15,7 +15,7 @@ namespace MediaBrowser.Model.Dto
public MediaSourceInfo()
{
Formats = Array.Empty<string>();
- MediaStreams = new List<MediaStream>();
+ MediaStreams = Array.Empty<MediaStream>();
MediaAttachments = Array.Empty<MediaAttachment>();
RequiredHttpHeaders = new Dictionary<string, string>();
SupportsTranscoding = true;
@@ -88,7 +88,7 @@ namespace MediaBrowser.Model.Dto
public Video3DFormat? Video3DFormat { get; set; }
- public List<MediaStream> MediaStreams { get; set; }
+ public IReadOnlyList<MediaStream> MediaStreams { get; set; }
public IReadOnlyList<MediaAttachment> MediaAttachments { get; set; }
diff --git a/MediaBrowser.Model/Dto/MetadataEditorInfo.cs b/MediaBrowser.Model/Dto/MetadataEditorInfo.cs
index e0e889f7d..d098669ba 100644
--- a/MediaBrowser.Model/Dto/MetadataEditorInfo.cs
+++ b/MediaBrowser.Model/Dto/MetadataEditorInfo.cs
@@ -1,7 +1,7 @@
-#nullable disable
#pragma warning disable CS1591
using System;
+using System.Collections.Generic;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.Providers;
@@ -19,16 +19,16 @@ namespace MediaBrowser.Model.Dto
ContentTypeOptions = Array.Empty<NameValuePair>();
}
- public ParentalRating[] ParentalRatingOptions { get; set; }
+ public IReadOnlyList<ParentalRating> ParentalRatingOptions { get; set; }
- public CountryInfo[] Countries { get; set; }
+ public IReadOnlyList<CountryInfo> Countries { get; set; }
- public CultureDto[] Cultures { get; set; }
+ public IReadOnlyList<CultureDto> Cultures { get; set; }
- public ExternalIdInfo[] ExternalIdInfos { get; set; }
+ public IReadOnlyList<ExternalIdInfo> ExternalIdInfos { get; set; }
- public string ContentType { get; set; }
+ public string? ContentType { get; set; }
- public NameValuePair[] ContentTypeOptions { get; set; }
+ public IReadOnlyList<NameValuePair> ContentTypeOptions { get; set; }
}
}
diff --git a/MediaBrowser.Model/Globalization/CultureDto.cs b/MediaBrowser.Model/Globalization/CultureDto.cs
index 5246f87d9..d0cf2aad0 100644
--- a/MediaBrowser.Model/Globalization/CultureDto.cs
+++ b/MediaBrowser.Model/Globalization/CultureDto.cs
@@ -1,7 +1,6 @@
-#nullable disable
#pragma warning disable CS1591
-using System;
+using System.Collections.Generic;
namespace MediaBrowser.Model.Globalization
{
@@ -10,39 +9,42 @@ namespace MediaBrowser.Model.Globalization
/// </summary>
public class CultureDto
{
- public CultureDto()
+ public CultureDto(string name, string displayName, string twoLetterISOLanguageName, IReadOnlyList<string> threeLetterISOLanguageNames)
{
- ThreeLetterISOLanguageNames = Array.Empty<string>();
+ Name = name;
+ DisplayName = displayName;
+ TwoLetterISOLanguageName = twoLetterISOLanguageName;
+ ThreeLetterISOLanguageNames = threeLetterISOLanguageNames;
}
/// <summary>
- /// Gets or sets the name.
+ /// Gets the name.
/// </summary>
/// <value>The name.</value>
- public string Name { get; set; }
+ public string Name { get; }
/// <summary>
- /// Gets or sets the display name.
+ /// Gets the display name.
/// </summary>
/// <value>The display name.</value>
- public string DisplayName { get; set; }
+ public string DisplayName { get; }
/// <summary>
- /// Gets or sets the name of the two letter ISO language.
+ /// Gets the name of the two letter ISO language.
/// </summary>
/// <value>The name of the two letter ISO language.</value>
- public string TwoLetterISOLanguageName { get; set; }
+ public string TwoLetterISOLanguageName { get; }
/// <summary>
/// Gets the name of the three letter ISO language.
/// </summary>
/// <value>The name of the three letter ISO language.</value>
- public string ThreeLetterISOLanguageName
+ public string? ThreeLetterISOLanguageName
{
get
{
var vals = ThreeLetterISOLanguageNames;
- if (vals.Length > 0)
+ if (vals.Count > 0)
{
return vals[0];
}
@@ -51,6 +53,6 @@ namespace MediaBrowser.Model.Globalization
}
}
- public string[] ThreeLetterISOLanguageNames { get; set; }
+ public IReadOnlyList<string> ThreeLetterISOLanguageNames { get; }
}
}
diff --git a/MediaBrowser.Model/IO/IFileSystem.cs b/MediaBrowser.Model/IO/IFileSystem.cs
index 0f77d6b5b..7207795b0 100644
--- a/MediaBrowser.Model/IO/IFileSystem.cs
+++ b/MediaBrowser.Model/IO/IFileSystem.cs
@@ -199,6 +199,6 @@ namespace MediaBrowser.Model.IO
void SetAttributes(string path, bool isHidden, bool readOnly);
- List<FileSystemMetadata> GetDrives();
+ IEnumerable<FileSystemMetadata> GetDrives();
}
}
diff --git a/MediaBrowser.Model/MediaBrowser.Model.csproj b/MediaBrowser.Model/MediaBrowser.Model.csproj
index b54a40b42..4386f75af 100644
--- a/MediaBrowser.Model/MediaBrowser.Model.csproj
+++ b/MediaBrowser.Model/MediaBrowser.Model.csproj
@@ -35,12 +35,12 @@
<ItemGroup>
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="6.0.0" />
- <PackageReference Include="MimeTypes" Version="2.2.1">
+ <PackageReference Include="MimeTypes" Version="2.3.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="System.Globalization" Version="4.3.0" />
- <PackageReference Include="System.Text.Json" Version="6.0.1" />
+ <PackageReference Include="System.Text.Json" Version="6.0.2" />
</ItemGroup>
<ItemGroup>
diff --git a/MediaBrowser.Model/Querying/NextUpQuery.cs b/MediaBrowser.Model/Querying/NextUpQuery.cs
index fa8aa829d..7c65fda1a 100644
--- a/MediaBrowser.Model/Querying/NextUpQuery.cs
+++ b/MediaBrowser.Model/Querying/NextUpQuery.cs
@@ -14,6 +14,7 @@ namespace MediaBrowser.Model.Querying
EnableTotalRecordCount = true;
DisableFirstEpisode = false;
NextUpDateCutoff = DateTime.MinValue;
+ Rewatching = false;
}
/// <summary>
@@ -81,5 +82,10 @@ namespace MediaBrowser.Model.Querying
/// Gets or sets a value indicating the oldest date for a show to appear in Next Up.
/// </summary>
public DateTime NextUpDateCutoff { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether getting rewatching next up list.
+ /// </summary>
+ public bool Rewatching { get; set; }
}
}
diff --git a/MediaBrowser.Model/Search/SearchHintResult.cs b/MediaBrowser.Model/Search/SearchHintResult.cs
index 92ba4139e..762a9a078 100644
--- a/MediaBrowser.Model/Search/SearchHintResult.cs
+++ b/MediaBrowser.Model/Search/SearchHintResult.cs
@@ -1,4 +1,5 @@
-#nullable disable
+using System.Collections.Generic;
+
namespace MediaBrowser.Model.Search
{
/// <summary>
@@ -7,15 +8,26 @@ namespace MediaBrowser.Model.Search
public class SearchHintResult
{
/// <summary>
- /// Gets or sets the search hints.
+ /// Initializes a new instance of the <see cref="SearchHintResult" /> class.
+ /// </summary>
+ /// <param name="searchHints">The search hints.</param>
+ /// <param name="totalRecordCount">The total record count.</param>
+ public SearchHintResult(IReadOnlyList<SearchHint> searchHints, int totalRecordCount)
+ {
+ SearchHints = searchHints;
+ TotalRecordCount = totalRecordCount;
+ }
+
+ /// <summary>
+ /// Gets the search hints.
/// </summary>
/// <value>The search hints.</value>
- public SearchHint[] SearchHints { get; set; }
+ public IReadOnlyList<SearchHint> SearchHints { get; }
/// <summary>
- /// Gets or sets the total record count.
+ /// Gets the total record count.
/// </summary>
/// <value>The total record count.</value>
- public int TotalRecordCount { get; set; }
+ public int TotalRecordCount { get; }
}
}
diff --git a/MediaBrowser.Model/Tasks/IScheduledTask.cs b/MediaBrowser.Model/Tasks/IScheduledTask.cs
index bf87088e4..123902d90 100644
--- a/MediaBrowser.Model/Tasks/IScheduledTask.cs
+++ b/MediaBrowser.Model/Tasks/IScheduledTask.cs
@@ -36,10 +36,10 @@ namespace MediaBrowser.Model.Tasks
/// <summary>
/// Executes the task.
/// </summary>
- /// <param name="cancellationToken">The cancellation token.</param>
/// <param name="progress">The progress.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task.</returns>
- Task Execute(CancellationToken cancellationToken, IProgress<double> progress);
+ Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken);
/// <summary>
/// Gets the default triggers that define when the task will run.
diff --git a/MediaBrowser.Providers/MediaInfo/AudioResolver.cs b/MediaBrowser.Providers/MediaInfo/AudioResolver.cs
index 425913501..ff90eeffb 100644
--- a/MediaBrowser.Providers/MediaInfo/AudioResolver.cs
+++ b/MediaBrowser.Providers/MediaInfo/AudioResolver.cs
@@ -1,176 +1,28 @@
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Runtime.CompilerServices;
-using System.Threading;
-using System.Threading.Tasks;
-using Emby.Naming.Audio;
using Emby.Naming.Common;
-using Jellyfin.Extensions;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.MediaEncoding;
-using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Dlna;
-using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Globalization;
-using MediaBrowser.Model.MediaInfo;
namespace MediaBrowser.Providers.MediaInfo
{
/// <summary>
- /// Resolves external audios for videos.
+ /// Resolves external audio files for <see cref="Video"/>.
/// </summary>
- public class AudioResolver
+ public class AudioResolver : MediaInfoResolver
{
- private readonly ILocalizationManager _localizationManager;
- private readonly IMediaEncoder _mediaEncoder;
- private readonly NamingOptions _namingOptions;
-
/// <summary>
- /// Initializes a new instance of the <see cref="AudioResolver"/> class.
+ /// Initializes a new instance of the <see cref="AudioResolver"/> class for external audio file processing.
/// </summary>
/// <param name="localizationManager">The localization manager.</param>
/// <param name="mediaEncoder">The media encoder.</param>
- /// <param name="namingOptions">The naming options.</param>
+ /// <param name="namingOptions">The <see cref="NamingOptions"/> object containing FileExtensions, MediaDefaultFlags, MediaForcedFlags and MediaFlagDelimiters.</param>
public AudioResolver(
ILocalizationManager localizationManager,
IMediaEncoder mediaEncoder,
NamingOptions namingOptions)
- {
- _localizationManager = localizationManager;
- _mediaEncoder = mediaEncoder;
- _namingOptions = namingOptions;
- }
-
- /// <summary>
- /// Returns the audio streams found in the external audio files for the given video.
- /// </summary>
- /// <param name="video">The video to get the external audio streams from.</param>
- /// <param name="startIndex">The stream index to start adding audio streams at.</param>
- /// <param name="directoryService">The directory service to search for files.</param>
- /// <param name="clearCache">True if the directory service cache should be cleared before searching.</param>
- /// <param name="cancellationToken">The cancellation token to cancel operation.</param>
- /// <returns>A list of external audio streams.</returns>
- public async IAsyncEnumerable<MediaStream> GetExternalAudioStreams(
- Video video,
- int startIndex,
- IDirectoryService directoryService,
- bool clearCache,
- [EnumeratorCancellation] CancellationToken cancellationToken)
- {
- cancellationToken.ThrowIfCancellationRequested();
-
- if (!video.IsFileProtocol)
- {
- yield break;
- }
-
- IEnumerable<string> paths = GetExternalAudioFiles(video, directoryService, clearCache);
- foreach (string path in paths)
+ : base(localizationManager, mediaEncoder, namingOptions, DlnaProfileType.Audio)
{
- string fileNameWithoutExtension = Path.GetFileNameWithoutExtension(path);
- Model.MediaInfo.MediaInfo mediaInfo = await GetMediaInfo(path, cancellationToken).ConfigureAwait(false);
-
- foreach (MediaStream mediaStream in mediaInfo.MediaStreams)
- {
- mediaStream.Index = startIndex++;
- mediaStream.Type = MediaStreamType.Audio;
- mediaStream.IsExternal = true;
- mediaStream.Path = path;
- mediaStream.IsDefault = false;
- mediaStream.Title = null;
-
- if (string.IsNullOrEmpty(mediaStream.Language))
- {
- // Try to translate to three character code
- // Be flexible and check against both the full and three character versions
- var language = StringExtensions.RightPart(fileNameWithoutExtension, '.').ToString();
-
- if (language != fileNameWithoutExtension)
- {
- var culture = _localizationManager.FindLanguageInfo(language);
-
- language = culture == null ? language : culture.ThreeLetterISOLanguageName;
- mediaStream.Language = language;
- }
- }
-
- yield return mediaStream;
- }
- }
- }
-
- /// <summary>
- /// Returns the external audio file paths for the given video.
- /// </summary>
- /// <param name="video">The video to get the external audio file paths from.</param>
- /// <param name="directoryService">The directory service to search for files.</param>
- /// <param name="clearCache">True if the directory service cache should be cleared before searching.</param>
- /// <returns>A list of external audio file paths.</returns>
- public IEnumerable<string> GetExternalAudioFiles(
- Video video,
- IDirectoryService directoryService,
- bool clearCache)
- {
- if (!video.IsFileProtocol)
- {
- yield break;
- }
-
- // Check if video folder exists
- string folder = video.ContainingFolderPath;
- if (!Directory.Exists(folder))
- {
- yield break;
- }
-
- string videoFileNameWithoutExtension = Path.GetFileNameWithoutExtension(video.Path);
-
- var files = directoryService.GetFilePaths(folder, clearCache, true);
- for (int i = 0; i < files.Count; i++)
- {
- string file = files[i];
- if (string.Equals(video.Path, file, StringComparison.OrdinalIgnoreCase)
- || !AudioFileParser.IsAudioFile(file, _namingOptions)
- || Path.GetExtension(file.AsSpan()).Equals(".strm", StringComparison.OrdinalIgnoreCase))
- {
- continue;
- }
-
- string fileNameWithoutExtension = Path.GetFileNameWithoutExtension(file);
- // The audio filename must either be equal to the video filename or start with the video filename followed by a dot
- if (videoFileNameWithoutExtension.Equals(fileNameWithoutExtension, StringComparison.OrdinalIgnoreCase)
- || (fileNameWithoutExtension.Length > videoFileNameWithoutExtension.Length
- && fileNameWithoutExtension[videoFileNameWithoutExtension.Length] == '.'
- && fileNameWithoutExtension.StartsWith(videoFileNameWithoutExtension, StringComparison.OrdinalIgnoreCase)))
- {
- yield return file;
- }
- }
- }
-
- /// <summary>
- /// Returns the media info of the given audio file.
- /// </summary>
- /// <param name="path">The path to the audio file.</param>
- /// <param name="cancellationToken">The cancellation token to cancel operation.</param>
- /// <returns>The media info for the given audio file.</returns>
- private Task<Model.MediaInfo.MediaInfo> GetMediaInfo(string path, CancellationToken cancellationToken)
- {
- cancellationToken.ThrowIfCancellationRequested();
-
- return _mediaEncoder.GetMediaInfo(
- new MediaInfoRequest
- {
- MediaType = DlnaProfileType.Audio,
- MediaSource = new MediaSourceInfo
- {
- Path = path,
- Protocol = MediaProtocol.File
- }
- },
- cancellationToken);
}
}
}
diff --git a/MediaBrowser.Providers/MediaInfo/FFProbeAudioInfo.cs b/MediaBrowser.Providers/MediaInfo/FFProbeAudioInfo.cs
index 9eb79c39d..f22965436 100644
--- a/MediaBrowser.Providers/MediaInfo/FFProbeAudioInfo.cs
+++ b/MediaBrowser.Providers/MediaInfo/FFProbeAudioInfo.cs
@@ -84,8 +84,6 @@ namespace MediaBrowser.Providers.MediaInfo
/// <param name="cancellationToken">The cancellation token.</param>
protected void Fetch(Audio audio, Model.MediaInfo.MediaInfo mediaInfo, CancellationToken cancellationToken)
{
- var mediaStreams = mediaInfo.MediaStreams;
-
audio.Container = mediaInfo.Container;
audio.TotalBitrate = mediaInfo.Bitrate;
@@ -97,7 +95,7 @@ namespace MediaBrowser.Providers.MediaInfo
FetchDataFromTags(audio, mediaInfo);
- _itemRepo.SaveMediaStreams(audio.Id, mediaStreams, cancellationToken);
+ _itemRepo.SaveMediaStreams(audio.Id, mediaInfo.MediaStreams, cancellationToken);
}
/// <summary>
diff --git a/MediaBrowser.Providers/MediaInfo/FFProbeProvider.cs b/MediaBrowser.Providers/MediaInfo/FFProbeProvider.cs
index 19a435196..560e20dae 100644
--- a/MediaBrowser.Providers/MediaInfo/FFProbeProvider.cs
+++ b/MediaBrowser.Providers/MediaInfo/FFProbeProvider.cs
@@ -19,6 +19,7 @@ using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Controller.Persistence;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Controller.Subtitles;
+using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.MediaInfo;
@@ -39,11 +40,10 @@ namespace MediaBrowser.Providers.MediaInfo
IHasItemChangeMonitor
{
private readonly ILogger<FFProbeProvider> _logger;
- private readonly SubtitleResolver _subtitleResolver;
private readonly AudioResolver _audioResolver;
+ private readonly SubtitleResolver _subtitleResolver;
private readonly FFProbeVideoInfo _videoProber;
private readonly FFProbeAudioInfo _audioProber;
-
private readonly Task<ItemUpdateType> _cachedTask = Task.FromResult(ItemUpdateType.None);
public FFProbeProvider(
@@ -62,7 +62,7 @@ namespace MediaBrowser.Providers.MediaInfo
{
_logger = logger;
_audioResolver = new AudioResolver(localization, mediaEncoder, namingOptions);
- _subtitleResolver = new SubtitleResolver(BaseItem.LocalizationManager);
+ _subtitleResolver = new SubtitleResolver(localization, mediaEncoder, namingOptions);
_videoProber = new FFProbeVideoInfo(
_logger,
mediaSourceManager,
@@ -75,7 +75,8 @@ namespace MediaBrowser.Providers.MediaInfo
subtitleManager,
chapterManager,
libraryManager,
- _audioResolver);
+ _audioResolver,
+ _subtitleResolver);
_audioProber = new FFProbeAudioInfo(mediaSourceManager, mediaEncoder, itemRepo, libraryManager);
}
@@ -104,7 +105,9 @@ namespace MediaBrowser.Providers.MediaInfo
if (item.SupportsLocalMetadata && video != null && !video.IsPlaceHolder
&& !video.SubtitleFiles.SequenceEqual(
- _subtitleResolver.GetExternalSubtitleFiles(video, directoryService, false), StringComparer.Ordinal))
+ _subtitleResolver.GetExternalFiles(video, directoryService, false)
+ .Select(info => info.Path).ToList(),
+ StringComparer.Ordinal))
{
_logger.LogDebug("Refreshing {ItemPath} due to external subtitles change.", item.Path);
return true;
@@ -112,7 +115,9 @@ namespace MediaBrowser.Providers.MediaInfo
if (item.SupportsLocalMetadata && video != null && !video.IsPlaceHolder
&& !video.AudioFiles.SequenceEqual(
- _audioResolver.GetExternalAudioFiles(video, directoryService, false), StringComparer.Ordinal))
+ _audioResolver.GetExternalFiles(video, directoryService, false)
+ .Select(info => info.Path).ToList(),
+ StringComparer.Ordinal))
{
_logger.LogDebug("Refreshing {ItemPath} due to external audio change.", item.Path);
return true;
diff --git a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs
index a2fb2a3c9..26ff0412b 100644
--- a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs
+++ b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs
@@ -45,6 +45,7 @@ namespace MediaBrowser.Providers.MediaInfo
private readonly IChapterManager _chapterManager;
private readonly ILibraryManager _libraryManager;
private readonly AudioResolver _audioResolver;
+ private readonly SubtitleResolver _subtitleResolver;
private readonly IMediaSourceManager _mediaSourceManager;
private readonly long _dummyChapterDuration = TimeSpan.FromMinutes(5).Ticks;
@@ -61,9 +62,11 @@ namespace MediaBrowser.Providers.MediaInfo
ISubtitleManager subtitleManager,
IChapterManager chapterManager,
ILibraryManager libraryManager,
- AudioResolver audioResolver)
+ AudioResolver audioResolver,
+ SubtitleResolver subtitleResolver)
{
_logger = logger;
+ _mediaSourceManager = mediaSourceManager;
_mediaEncoder = mediaEncoder;
_itemRepo = itemRepo;
_blurayExaminer = blurayExaminer;
@@ -74,7 +77,7 @@ namespace MediaBrowser.Providers.MediaInfo
_chapterManager = chapterManager;
_libraryManager = libraryManager;
_audioResolver = audioResolver;
- _mediaSourceManager = mediaSourceManager;
+ _subtitleResolver = subtitleResolver;
}
public async Task<ItemUpdateType> ProbeVideo<T>(
@@ -172,7 +175,7 @@ namespace MediaBrowser.Providers.MediaInfo
if (mediaInfo != null)
{
- mediaStreams = mediaInfo.MediaStreams;
+ mediaStreams = mediaInfo.MediaStreams.ToList();
mediaAttachments = mediaInfo.MediaAttachments;
video.TotalBitrate = mediaInfo.Bitrate;
@@ -202,7 +205,7 @@ namespace MediaBrowser.Providers.MediaInfo
video.Container = mediaInfo.Container;
- chapters = mediaInfo.Chapters == null ? Array.Empty<ChapterInfo>() : mediaInfo.Chapters;
+ chapters = mediaInfo.Chapters ?? Array.Empty<ChapterInfo>();
if (blurayInfo != null)
{
FetchBdInfo(video, ref chapters, mediaStreams, blurayInfo);
@@ -215,7 +218,7 @@ namespace MediaBrowser.Providers.MediaInfo
chapters = Array.Empty<ChapterInfo>();
}
- await AddExternalSubtitles(video, mediaStreams, options, cancellationToken).ConfigureAwait(false);
+ await AddExternalSubtitlesAsync(video, mediaStreams, options, cancellationToken).ConfigureAwait(false);
await AddExternalAudioAsync(video, mediaStreams, options, cancellationToken).ConfigureAwait(false);
@@ -229,12 +232,24 @@ namespace MediaBrowser.Providers.MediaInfo
video.Video3DFormat ??= mediaInfo.Video3DFormat;
}
+ if (libraryOptions.AllowEmbeddedSubtitles == EmbeddedSubtitleOptions.AllowText || libraryOptions.AllowEmbeddedSubtitles == EmbeddedSubtitleOptions.AllowNone)
+ {
+ _logger.LogDebug("Disabling embedded image subtitles for {Path} due to DisableEmbeddedImageSubtitles setting", video.Path);
+ mediaStreams.RemoveAll(i => i.Type == MediaStreamType.Subtitle && !i.IsExternal && !i.IsTextSubtitleStream);
+ }
+
+ if (libraryOptions.AllowEmbeddedSubtitles == EmbeddedSubtitleOptions.AllowImage || libraryOptions.AllowEmbeddedSubtitles == EmbeddedSubtitleOptions.AllowNone)
+ {
+ _logger.LogDebug("Disabling embedded text subtitles for {Path} due to DisableEmbeddedTextSubtitles setting", video.Path);
+ mediaStreams.RemoveAll(i => i.Type == MediaStreamType.Subtitle && !i.IsExternal && i.IsTextSubtitleStream);
+ }
+
var videoStream = mediaStreams.FirstOrDefault(i => i.Type == MediaStreamType.Video);
video.Height = videoStream?.Height ?? 0;
video.Width = videoStream?.Width ?? 0;
- video.DefaultVideoStreamIndex = videoStream == null ? (int?)null : videoStream.Index;
+ video.DefaultVideoStreamIndex = videoStream?.Index;
video.HasSubtitles = mediaStreams.Any(i => i.Type == MediaStreamType.Subtitle);
@@ -514,16 +529,14 @@ namespace MediaBrowser.Providers.MediaInfo
/// <param name="options">The refreshOptions.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task.</returns>
- private async Task AddExternalSubtitles(
+ private async Task AddExternalSubtitlesAsync(
Video video,
List<MediaStream> currentStreams,
MetadataRefreshOptions options,
CancellationToken cancellationToken)
{
- var subtitleResolver = new SubtitleResolver(_localization);
-
var startIndex = currentStreams.Count == 0 ? 0 : (currentStreams.Select(i => i.Index).Max() + 1);
- var externalSubtitleStreams = subtitleResolver.GetExternalSubtitleStreams(video, startIndex, options.DirectoryService, false);
+ var externalSubtitleStreams = await _subtitleResolver.GetExternalStreamsAsync(video, startIndex, options.DirectoryService, false, cancellationToken);
var enableSubtitleDownloading = options.MetadataRefreshMode == MetadataRefreshMode.Default ||
options.MetadataRefreshMode == MetadataRefreshMode.FullRefresh;
@@ -577,7 +590,7 @@ namespace MediaBrowser.Providers.MediaInfo
// Rescan
if (downloadedLanguages.Count > 0)
{
- externalSubtitleStreams = subtitleResolver.GetExternalSubtitleStreams(video, startIndex, options.DirectoryService, true);
+ externalSubtitleStreams = await _subtitleResolver.GetExternalStreamsAsync(video, startIndex, options.DirectoryService, true, cancellationToken);
}
}
@@ -600,12 +613,9 @@ namespace MediaBrowser.Providers.MediaInfo
CancellationToken cancellationToken)
{
var startIndex = currentStreams.Count == 0 ? 0 : currentStreams.Max(i => i.Index) + 1;
- var externalAudioStreams = _audioResolver.GetExternalAudioStreams(video, startIndex, options.DirectoryService, false, cancellationToken);
+ var externalAudioStreams = await _audioResolver.GetExternalStreamsAsync(video, startIndex, options.DirectoryService, false, cancellationToken).ConfigureAwait(false);
- await foreach (MediaStream externalAudioStream in externalAudioStreams)
- {
- currentStreams.Add(externalAudioStream);
- }
+ currentStreams = currentStreams.Concat(externalAudioStreams).ToList();
// Select all external audio file paths
video.AudioFiles = currentStreams.Where(i => i.Type == MediaStreamType.Audio && i.IsExternal).Select(i => i.Path).Distinct().ToArray();
diff --git a/MediaBrowser.Providers/MediaInfo/MediaInfoResolver.cs b/MediaBrowser.Providers/MediaInfo/MediaInfoResolver.cs
new file mode 100644
index 000000000..40b45faf5
--- /dev/null
+++ b/MediaBrowser.Providers/MediaInfo/MediaInfoResolver.cs
@@ -0,0 +1,231 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Emby.Naming.Common;
+using Emby.Naming.ExternalFiles;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.MediaEncoding;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Dlna;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Globalization;
+using MediaBrowser.Model.MediaInfo;
+
+namespace MediaBrowser.Providers.MediaInfo
+{
+ /// <summary>
+ /// Resolves external files for <see cref="Video"/>.
+ /// </summary>
+ public abstract class MediaInfoResolver
+ {
+ /// <summary>
+ /// The <see cref="CompareOptions"/> instance.
+ /// </summary>
+ private const CompareOptions CompareOptions = System.Globalization.CompareOptions.IgnoreCase | System.Globalization.CompareOptions.IgnoreNonSpace | System.Globalization.CompareOptions.IgnoreSymbols;
+
+ /// <summary>
+ /// The <see cref="CompareInfo"/> instance.
+ /// </summary>
+ private readonly CompareInfo _compareInfo = CultureInfo.InvariantCulture.CompareInfo;
+
+ /// <summary>
+ /// The <see cref="ExternalPathParser"/> instance.
+ /// </summary>
+ private readonly ExternalPathParser _externalPathParser;
+
+ /// <summary>
+ /// The <see cref="IMediaEncoder"/> instance.
+ /// </summary>
+ private readonly IMediaEncoder _mediaEncoder;
+
+ /// <summary>
+ /// The <see cref="NamingOptions"/> instance.
+ /// </summary>
+ private readonly NamingOptions _namingOptions;
+
+ /// <summary>
+ /// The <see cref="DlnaProfileType"/> of the files this resolver should resolve.
+ /// </summary>
+ private readonly DlnaProfileType _type;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="MediaInfoResolver"/> class.
+ /// </summary>
+ /// <param name="localizationManager">The localization manager.</param>
+ /// <param name="mediaEncoder">The media encoder.</param>
+ /// <param name="namingOptions">The <see cref="NamingOptions"/> object containing FileExtensions, MediaDefaultFlags, MediaForcedFlags and MediaFlagDelimiters.</param>
+ /// <param name="type">The <see cref="DlnaProfileType"/> of the parsed file.</param>
+ protected MediaInfoResolver(
+ ILocalizationManager localizationManager,
+ IMediaEncoder mediaEncoder,
+ NamingOptions namingOptions,
+ DlnaProfileType type)
+ {
+ _mediaEncoder = mediaEncoder;
+ _namingOptions = namingOptions;
+ _type = type;
+ _externalPathParser = new ExternalPathParser(namingOptions, localizationManager, _type);
+ }
+
+ /// <summary>
+ /// Retrieves the external streams for the provided video.
+ /// </summary>
+ /// <param name="video">The <see cref="Video"/> object to search external streams for.</param>
+ /// <param name="startIndex">The stream index to start adding external streams at.</param>
+ /// <param name="directoryService">The directory service to search for files.</param>
+ /// <param name="clearCache">True if the directory service cache should be cleared before searching.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>The external streams located.</returns>
+ public async Task<IReadOnlyList<MediaStream>> GetExternalStreamsAsync(
+ Video video,
+ int startIndex,
+ IDirectoryService directoryService,
+ bool clearCache,
+ CancellationToken cancellationToken)
+ {
+ if (!video.IsFileProtocol)
+ {
+ return Array.Empty<MediaStream>();
+ }
+
+ var pathInfos = GetExternalFiles(video, directoryService, clearCache);
+
+ if (!pathInfos.Any())
+ {
+ return Array.Empty<MediaStream>();
+ }
+
+ var mediaStreams = new List<MediaStream>();
+
+ foreach (var pathInfo in pathInfos)
+ {
+ var mediaInfo = await GetMediaInfo(pathInfo.Path, _type, cancellationToken).ConfigureAwait(false);
+
+ if (mediaInfo.MediaStreams.Count == 1)
+ {
+ MediaStream mediaStream = mediaInfo.MediaStreams[0];
+ mediaStream.Index = startIndex++;
+ mediaStream.IsDefault = pathInfo.IsDefault || mediaStream.IsDefault;
+ mediaStream.IsForced = pathInfo.IsForced || mediaStream.IsForced;
+
+ mediaStreams.Add(MergeMetadata(mediaStream, pathInfo));
+ }
+ else
+ {
+ foreach (MediaStream mediaStream in mediaInfo.MediaStreams)
+ {
+ mediaStream.Index = startIndex++;
+
+ mediaStreams.Add(MergeMetadata(mediaStream, pathInfo));
+ }
+ }
+ }
+
+ return mediaStreams.AsReadOnly();
+ }
+
+ /// <summary>
+ /// Returns the external file infos for the given video.
+ /// </summary>
+ /// <param name="video">The <see cref="Video"/> object to search external files for.</param>
+ /// <param name="directoryService">The directory service to search for files.</param>
+ /// <param name="clearCache">True if the directory service cache should be cleared before searching.</param>
+ /// <returns>The external file paths located.</returns>
+ public IReadOnlyList<ExternalPathParserResult> GetExternalFiles(
+ Video video,
+ IDirectoryService directoryService,
+ bool clearCache)
+ {
+ if (!video.IsFileProtocol)
+ {
+ return Array.Empty<ExternalPathParserResult>();
+ }
+
+ // Check if video folder exists
+ string folder = video.ContainingFolderPath;
+ if (!Directory.Exists(folder))
+ {
+ return Array.Empty<ExternalPathParserResult>();
+ }
+
+ var externalPathInfos = new List<ExternalPathParserResult>();
+
+ var files = directoryService.GetFilePaths(folder, clearCache).ToList();
+ files.AddRange(directoryService.GetFilePaths(video.GetInternalMetadataPath(), clearCache));
+
+ if (!files.Any())
+ {
+ return Array.Empty<ExternalPathParserResult>();
+ }
+
+ foreach (var file in files)
+ {
+ var fileNameWithoutExtension = Path.GetFileNameWithoutExtension(file);
+ if (_compareInfo.IsPrefix(fileNameWithoutExtension, video.FileNameWithoutExtension, CompareOptions, out int matchLength)
+ && (fileNameWithoutExtension.Length == matchLength || _namingOptions.MediaFlagDelimiters.Contains(fileNameWithoutExtension[matchLength].ToString())))
+ {
+ var externalPathInfo = _externalPathParser.ParseFile(file, fileNameWithoutExtension[matchLength..]);
+
+ if (externalPathInfo != null)
+ {
+ externalPathInfos.Add(externalPathInfo);
+ }
+ }
+ }
+
+ return externalPathInfos;
+ }
+
+ /// <summary>
+ /// Returns the media info of the given file.
+ /// </summary>
+ /// <param name="path">The path to the file.</param>
+ /// <param name="type">The <see cref="DlnaProfileType"/>.</param>
+ /// <param name="cancellationToken">The cancellation token to cancel operation.</param>
+ /// <returns>The media info for the given file.</returns>
+ private Task<Model.MediaInfo.MediaInfo> GetMediaInfo(string path, DlnaProfileType type, CancellationToken cancellationToken)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ return _mediaEncoder.GetMediaInfo(
+ new MediaInfoRequest
+ {
+ MediaType = type,
+ MediaSource = new MediaSourceInfo
+ {
+ Path = path,
+ Protocol = MediaProtocol.File
+ }
+ },
+ cancellationToken);
+ }
+
+ /// <summary>
+ /// Merges path metadata into stream metadata.
+ /// </summary>
+ /// <param name="mediaStream">The <see cref="MediaStream"/> object.</param>
+ /// <param name="pathInfo">The <see cref="ExternalPathParserResult"/> object.</param>
+ /// <returns>The modified mediaStream.</returns>
+ private MediaStream MergeMetadata(MediaStream mediaStream, ExternalPathParserResult pathInfo)
+ {
+ mediaStream.Path = pathInfo.Path;
+ mediaStream.IsExternal = true;
+ mediaStream.Title = string.IsNullOrEmpty(mediaStream.Title) ? (string.IsNullOrEmpty(pathInfo.Title) ? null : pathInfo.Title) : mediaStream.Title;
+ mediaStream.Language = string.IsNullOrEmpty(mediaStream.Language) ? (string.IsNullOrEmpty(pathInfo.Language) ? null : pathInfo.Language) : mediaStream.Language;
+
+ mediaStream.Type = _type switch
+ {
+ DlnaProfileType.Audio => MediaStreamType.Audio,
+ DlnaProfileType.Subtitle => MediaStreamType.Subtitle,
+ _ => mediaStream.Type
+ };
+
+ return mediaStream;
+ }
+ }
+}
diff --git a/MediaBrowser.Providers/MediaInfo/SubtitleResolver.cs b/MediaBrowser.Providers/MediaInfo/SubtitleResolver.cs
index ba284187e..289036fda 100644
--- a/MediaBrowser.Providers/MediaInfo/SubtitleResolver.cs
+++ b/MediaBrowser.Providers/MediaInfo/SubtitleResolver.cs
@@ -1,235 +1,28 @@
-using System;
-using System.Collections.Generic;
-using System.IO;
+using Emby.Naming.Common;
using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Providers;
-using MediaBrowser.Model.Entities;
+using MediaBrowser.Controller.MediaEncoding;
+using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Globalization;
namespace MediaBrowser.Providers.MediaInfo
{
/// <summary>
- /// Resolves external subtitles for videos.
+ /// Resolves external subtitle files for <see cref="Video"/>.
/// </summary>
- public class SubtitleResolver
+ public class SubtitleResolver : MediaInfoResolver
{
- private readonly ILocalizationManager _localization;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="SubtitleResolver"/> class.
- /// </summary>
- /// <param name="localization">The localization manager.</param>
- public SubtitleResolver(ILocalizationManager localization)
- {
- _localization = localization;
- }
-
- /// <summary>
- /// Retrieves the external subtitle streams for the provided video.
- /// </summary>
- /// <param name="video">The video to search from.</param>
- /// <param name="startIndex">The stream index to start adding subtitle streams at.</param>
- /// <param name="directoryService">The directory service to search for files.</param>
- /// <param name="clearCache">True if the directory service cache should be cleared before searching.</param>
- /// <returns>The external subtitle streams located.</returns>
- public List<MediaStream> GetExternalSubtitleStreams(
- Video video,
- int startIndex,
- IDirectoryService directoryService,
- bool clearCache)
- {
- var streams = new List<MediaStream>();
-
- if (!video.IsFileProtocol)
- {
- return streams;
- }
-
- AddExternalSubtitleStreams(streams, video.ContainingFolderPath, video.Path, startIndex, directoryService, clearCache);
-
- startIndex += streams.Count;
-
- string folder = video.GetInternalMetadataPath();
-
- if (!Directory.Exists(folder))
- {
- return streams;
- }
-
- try
- {
- AddExternalSubtitleStreams(streams, folder, video.Path, startIndex, directoryService, clearCache);
- }
- catch (IOException)
- {
- }
-
- return streams;
- }
-
- /// <summary>
- /// Locates the external subtitle files for the provided video.
- /// </summary>
- /// <param name="video">The video to search from.</param>
- /// <param name="directoryService">The directory service to search for files.</param>
- /// <param name="clearCache">True if the directory service cache should be cleared before searching.</param>
- /// <returns>The external subtitle file paths located.</returns>
- public IEnumerable<string> GetExternalSubtitleFiles(
- Video video,
- IDirectoryService directoryService,
- bool clearCache)
- {
- if (!video.IsFileProtocol)
- {
- yield break;
- }
-
- var streams = GetExternalSubtitleStreams(video, 0, directoryService, clearCache);
-
- foreach (var stream in streams)
- {
- yield return stream.Path;
- }
- }
-
/// <summary>
- /// Extracts the subtitle files from the provided list and adds them to the list of streams.
+ /// Initializes a new instance of the <see cref="SubtitleResolver"/> class for external subtitle file processing.
/// </summary>
- /// <param name="streams">The list of streams to add external subtitles to.</param>
- /// <param name="videoPath">The path to the video file.</param>
- /// <param name="startIndex">The stream index to start adding subtitle streams at.</param>
- /// <param name="files">The files to add if they are subtitles.</param>
- public void AddExternalSubtitleStreams(
- List<MediaStream> streams,
- string videoPath,
- int startIndex,
- IReadOnlyList<string> files)
- {
- var videoFileNameWithoutExtension = NormalizeFilenameForSubtitleComparison(videoPath);
-
- for (var i = 0; i < files.Count; i++)
+ /// <param name="localizationManager">The localization manager.</param>
+ /// <param name="mediaEncoder">The media encoder.</param>
+ /// <param name="namingOptions">The <see cref="NamingOptions"/> object containing FileExtensions, MediaDefaultFlags, MediaForcedFlags and MediaFlagDelimiters.</param>
+ public SubtitleResolver(
+ ILocalizationManager localizationManager,
+ IMediaEncoder mediaEncoder,
+ NamingOptions namingOptions)
+ : base(localizationManager, mediaEncoder, namingOptions, DlnaProfileType.Subtitle)
{
- var fullName = files[i];
- var extension = Path.GetExtension(fullName.AsSpan());
- if (!IsSubtitleExtension(extension))
- {
- continue;
- }
-
- var fileNameWithoutExtension = NormalizeFilenameForSubtitleComparison(fullName);
-
- MediaStream mediaStream;
-
- // The subtitle filename must either be equal to the video filename or start with the video filename followed by a dot
- if (videoFileNameWithoutExtension.Equals(fileNameWithoutExtension, StringComparison.OrdinalIgnoreCase))
- {
- mediaStream = new MediaStream
- {
- Index = startIndex++,
- Type = MediaStreamType.Subtitle,
- IsExternal = true,
- Path = fullName
- };
- }
- else if (fileNameWithoutExtension.Length > videoFileNameWithoutExtension.Length
- && fileNameWithoutExtension[videoFileNameWithoutExtension.Length] == '.'
- && fileNameWithoutExtension.StartsWith(videoFileNameWithoutExtension, StringComparison.OrdinalIgnoreCase))
- {
- var isForced = fullName.Contains(".forced.", StringComparison.OrdinalIgnoreCase)
- || fullName.Contains(".foreign.", StringComparison.OrdinalIgnoreCase);
-
- var isDefault = fullName.Contains(".default.", StringComparison.OrdinalIgnoreCase);
-
- // Support xbmc naming conventions - 300.spanish.srt
- var languageSpan = fileNameWithoutExtension;
- while (languageSpan.Length > 0)
- {
- var lastDot = languageSpan.LastIndexOf('.');
- if (lastDot < videoFileNameWithoutExtension.Length)
- {
- languageSpan = ReadOnlySpan<char>.Empty;
- break;
- }
-
- var currentSlice = languageSpan[lastDot..];
- if (currentSlice.Equals(".default", StringComparison.OrdinalIgnoreCase)
- || currentSlice.Equals(".forced", StringComparison.OrdinalIgnoreCase)
- || currentSlice.Equals(".foreign", StringComparison.OrdinalIgnoreCase))
- {
- languageSpan = languageSpan[..lastDot];
- continue;
- }
-
- languageSpan = languageSpan[(lastDot + 1)..];
- break;
- }
-
- var language = languageSpan.ToString();
- if (string.IsNullOrWhiteSpace(language))
- {
- language = null;
- }
- else
- {
- // Try to translate to three character code
- // Be flexible and check against both the full and three character versions
- var culture = _localization.FindLanguageInfo(language);
-
- language = culture == null ? language : culture.ThreeLetterISOLanguageName;
- }
-
- mediaStream = new MediaStream
- {
- Index = startIndex++,
- Type = MediaStreamType.Subtitle,
- IsExternal = true,
- Path = fullName,
- Language = language,
- IsForced = isForced,
- IsDefault = isDefault
- };
- }
- else
- {
- continue;
- }
-
- mediaStream.Codec = extension.TrimStart('.').ToString().ToLowerInvariant();
-
- streams.Add(mediaStream);
- }
- }
-
- private static bool IsSubtitleExtension(ReadOnlySpan<char> extension)
- {
- return extension.Equals(".srt", StringComparison.OrdinalIgnoreCase)
- || extension.Equals(".ssa", StringComparison.OrdinalIgnoreCase)
- || extension.Equals(".ass", StringComparison.OrdinalIgnoreCase)
- || extension.Equals(".sub", StringComparison.OrdinalIgnoreCase)
- || extension.Equals(".vtt", StringComparison.OrdinalIgnoreCase)
- || extension.Equals(".smi", StringComparison.OrdinalIgnoreCase)
- || extension.Equals(".sami", StringComparison.OrdinalIgnoreCase);
- }
-
- private static ReadOnlySpan<char> NormalizeFilenameForSubtitleComparison(string filename)
- {
- // Try to account for sloppy file naming
- filename = filename.Replace("_", string.Empty, StringComparison.Ordinal);
- filename = filename.Replace(" ", string.Empty, StringComparison.Ordinal);
- return Path.GetFileNameWithoutExtension(filename.AsSpan());
- }
-
- private void AddExternalSubtitleStreams(
- List<MediaStream> streams,
- string folder,
- string videoPath,
- int startIndex,
- IDirectoryService directoryService,
- bool clearCache)
- {
- var files = directoryService.GetFilePaths(folder, clearCache, true);
-
- AddExternalSubtitleStreams(streams, videoPath, startIndex, files);
}
}
}
diff --git a/MediaBrowser.Providers/MediaInfo/SubtitleScheduledTask.cs b/MediaBrowser.Providers/MediaInfo/SubtitleScheduledTask.cs
index 58651d42a..eb9071a52 100644
--- a/MediaBrowser.Providers/MediaInfo/SubtitleScheduledTask.cs
+++ b/MediaBrowser.Providers/MediaInfo/SubtitleScheduledTask.cs
@@ -63,7 +63,8 @@ namespace MediaBrowser.Providers.MediaInfo
return _config.GetConfiguration<SubtitleOptions>("subtitles");
}
- public async Task Execute(CancellationToken cancellationToken, IProgress<double> progress)
+ /// <inheritdoc />
+ public async Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken)
{
var options = GetOptions();
@@ -210,6 +211,7 @@ namespace MediaBrowser.Providers.MediaInfo
return true;
}
+ /// <inheritdoc />
public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
{
return new[]
diff --git a/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs b/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs
index 007101868..09ff84044 100644
--- a/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs
+++ b/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs
@@ -778,7 +778,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers
case "thumb":
{
- FetchThumbNode(reader, itemResult);
+ FetchThumbNode(reader, itemResult, "thumb");
break;
}
@@ -796,7 +796,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers
break;
}
- FetchThumbNode(subtree, itemResult);
+ FetchThumbNode(subtree, itemResult, "fanart");
break;
}
@@ -819,17 +819,22 @@ namespace MediaBrowser.XbmcMetadata.Parsers
}
}
- private void FetchThumbNode(XmlReader reader, MetadataResult<T> itemResult)
+ private void FetchThumbNode(XmlReader reader, MetadataResult<T> itemResult, string parentNode)
{
var artType = reader.GetAttribute("aspect");
var val = reader.ReadElementContentAsString();
// artType is null if the thumb node is a child of the fanart tag
// -> set image type to fanart
- if (string.IsNullOrWhiteSpace(artType))
+ if (string.IsNullOrWhiteSpace(artType) && parentNode.Equals("fanart", StringComparison.Ordinal))
{
artType = "fanart";
}
+ else if (string.IsNullOrWhiteSpace(artType))
+ {
+ // Sonarr writes thumb tags for posters without aspect property
+ artType = "poster";
+ }
// skip:
// - empty uri
diff --git a/deployment/Dockerfile.centos.amd64 b/deployment/Dockerfile.centos.amd64
index 708c706b5..350f0076a 100644
--- a/deployment/Dockerfile.centos.amd64
+++ b/deployment/Dockerfile.centos.amd64
@@ -13,7 +13,7 @@ RUN yum update -yq \
&& yum install -yq @buildsys-build rpmdevtools yum-plugins-core libcurl-devel fontconfig-devel freetype-devel openssl-devel glibc-devel libicu-devel git wget
# Install DotNET SDK
-RUN wget -q https://download.visualstudio.microsoft.com/download/pr/ede8a287-3d61-4988-a356-32ff9129079e/bdb47b6b510ed0c4f0b132f7f4ad9d5a/dotnet-sdk-6.0.101-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget -q https://download.visualstudio.microsoft.com/download/pr/e7acb87d-ab08-4620-9050-b3e80f688d36/e93bbadc19b12f81e3a6761719f28b47/dotnet-sdk-6.0.102-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
&& mkdir -p dotnet-sdk \
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
diff --git a/deployment/Dockerfile.fedora.amd64 b/deployment/Dockerfile.fedora.amd64
index 30615cd42..eeff9a96f 100644
--- a/deployment/Dockerfile.fedora.amd64
+++ b/deployment/Dockerfile.fedora.amd64
@@ -12,7 +12,7 @@ RUN dnf update -yq \
&& dnf install -yq @buildsys-build rpmdevtools git dnf-plugins-core libcurl-devel fontconfig-devel freetype-devel openssl-devel glibc-devel libicu-devel systemd wget
# Install DotNET SDK
-RUN wget -q https://download.visualstudio.microsoft.com/download/pr/ede8a287-3d61-4988-a356-32ff9129079e/bdb47b6b510ed0c4f0b132f7f4ad9d5a/dotnet-sdk-6.0.101-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget -q https://download.visualstudio.microsoft.com/download/pr/e7acb87d-ab08-4620-9050-b3e80f688d36/e93bbadc19b12f81e3a6761719f28b47/dotnet-sdk-6.0.102-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
&& mkdir -p dotnet-sdk \
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
diff --git a/deployment/Dockerfile.ubuntu.amd64 b/deployment/Dockerfile.ubuntu.amd64
index ccfaaa5f0..9d2deb1c6 100644
--- a/deployment/Dockerfile.ubuntu.amd64
+++ b/deployment/Dockerfile.ubuntu.amd64
@@ -17,7 +17,7 @@ RUN apt-get update -yqq \
libfreetype6-dev libssl-dev libssl1.1 liblttng-ust0
# Install dotnet repository
-RUN wget -q https://download.visualstudio.microsoft.com/download/pr/ede8a287-3d61-4988-a356-32ff9129079e/bdb47b6b510ed0c4f0b132f7f4ad9d5a/dotnet-sdk-6.0.101-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget -q https://download.visualstudio.microsoft.com/download/pr/e7acb87d-ab08-4620-9050-b3e80f688d36/e93bbadc19b12f81e3a6761719f28b47/dotnet-sdk-6.0.102-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
&& mkdir -p dotnet-sdk \
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
diff --git a/deployment/Dockerfile.ubuntu.arm64 b/deployment/Dockerfile.ubuntu.arm64
index 988c8f16d..ec90dba83 100644
--- a/deployment/Dockerfile.ubuntu.arm64
+++ b/deployment/Dockerfile.ubuntu.arm64
@@ -16,7 +16,7 @@ RUN apt-get update -yqq \
mmv build-essential lsb-release
# Install dotnet repository
-RUN wget -q https://download.visualstudio.microsoft.com/download/pr/ede8a287-3d61-4988-a356-32ff9129079e/bdb47b6b510ed0c4f0b132f7f4ad9d5a/dotnet-sdk-6.0.101-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget -q https://download.visualstudio.microsoft.com/download/pr/e7acb87d-ab08-4620-9050-b3e80f688d36/e93bbadc19b12f81e3a6761719f28b47/dotnet-sdk-6.0.102-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
&& mkdir -p dotnet-sdk \
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
diff --git a/deployment/Dockerfile.ubuntu.armhf b/deployment/Dockerfile.ubuntu.armhf
index 61a008d6a..3685e16c4 100644
--- a/deployment/Dockerfile.ubuntu.armhf
+++ b/deployment/Dockerfile.ubuntu.armhf
@@ -16,7 +16,7 @@ RUN apt-get update -yqq \
mmv build-essential lsb-release
# Install dotnet repository
-RUN wget -q https://download.visualstudio.microsoft.com/download/pr/ede8a287-3d61-4988-a356-32ff9129079e/bdb47b6b510ed0c4f0b132f7f4ad9d5a/dotnet-sdk-6.0.101-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget -q https://download.visualstudio.microsoft.com/download/pr/e7acb87d-ab08-4620-9050-b3e80f688d36/e93bbadc19b12f81e3a6761719f28b47/dotnet-sdk-6.0.102-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
&& mkdir -p dotnet-sdk \
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
diff --git a/jellyfin.ruleset b/jellyfin.ruleset
index cc7c54b97..1c834de82 100644
--- a/jellyfin.ruleset
+++ b/jellyfin.ruleset
@@ -57,6 +57,10 @@
</Rules>
<Rules AnalyzerId="Microsoft.CodeAnalysis.NetAnalyzers" RuleNamespace="Microsoft.Design">
+ <!-- error on CA1001: Types that own disposable fields should be disposable -->
+ <Rule Id="CA1001" Action="Error" />
+ <!-- error on CA1012: Abstract types should not have public constructors -->
+ <Rule Id="CA1012" Action="Error" />
<!-- error on CA1063: Implement IDisposable correctly -->
<Rule Id="CA1063" Action="Error" />
<!-- error on CA1305: Specify IFormatProvider -->
@@ -80,6 +84,8 @@
<!-- error on CA2016: Forward the CancellationToken parameter to methods that take one
or pass in 'CancellationToken.None' explicitly to indicate intentionally not propagating the token -->
<Rule Id="CA2016" Action="Error" />
+ <!-- error on CA2215: Dispose methods should call base class dispose -->
+ <Rule Id="CA2215" Action="Error" />
<!-- error on CA2254: Template should be a static expression -->
<Rule Id="CA2254" Action="Error" />
diff --git a/src/Jellyfin.MediaEncoding.Hls/ScheduledTasks/KeyframeExtractionScheduledTask.cs b/src/Jellyfin.MediaEncoding.Hls/ScheduledTasks/KeyframeExtractionScheduledTask.cs
index 03acc6911..d80925fc9 100644
--- a/src/Jellyfin.MediaEncoding.Hls/ScheduledTasks/KeyframeExtractionScheduledTask.cs
+++ b/src/Jellyfin.MediaEncoding.Hls/ScheduledTasks/KeyframeExtractionScheduledTask.cs
@@ -51,7 +51,7 @@ public class KeyframeExtractionScheduledTask : IScheduledTask
public string Category => _localizationManager.GetLocalizedString("TasksLibraryCategory");
/// <inheritdoc />
- public Task Execute(CancellationToken cancellationToken, IProgress<double> progress)
+ public Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken)
{
var query = new InternalItemsQuery
{
diff --git a/src/Jellyfin.MediaEncoding.Keyframes/Jellyfin.MediaEncoding.Keyframes.csproj b/src/Jellyfin.MediaEncoding.Keyframes/Jellyfin.MediaEncoding.Keyframes.csproj
index 5ec09c768..31da11e24 100644
--- a/src/Jellyfin.MediaEncoding.Keyframes/Jellyfin.MediaEncoding.Keyframes.csproj
+++ b/src/Jellyfin.MediaEncoding.Keyframes/Jellyfin.MediaEncoding.Keyframes.csproj
@@ -21,7 +21,7 @@
</ItemGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="5.0.0" />
+ <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="6.0.0" />
</ItemGroup>
<ItemGroup>
diff --git a/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj b/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj
index 64e420c71..e04bfffbb 100644
--- a/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj
+++ b/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj
@@ -15,12 +15,12 @@
<PackageReference Include="AutoFixture" Version="4.17.0" />
<PackageReference Include="AutoFixture.AutoMoq" Version="4.17.0" />
<PackageReference Include="AutoFixture.Xunit2" Version="4.17.0" />
- <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="6.0.1" />
+ <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="6.0.2" />
<PackageReference Include="Microsoft.Extensions.Options" Version="6.0.0" />
- <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
- <PackageReference Include="coverlet.collector" Version="3.1.1" />
+ <PackageReference Include="coverlet.collector" Version="3.1.2" />
<PackageReference Include="Moq" Version="4.16.1" />
</ItemGroup>
diff --git a/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj b/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj
index 8564a0dd3..1ad0f4e00 100644
--- a/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj
+++ b/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj
@@ -12,10 +12,10 @@
</PropertyGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
- <PackageReference Include="coverlet.collector" Version="3.1.1" />
+ <PackageReference Include="coverlet.collector" Version="3.1.2" />
<PackageReference Include="FsCheck.Xunit" Version="2.16.4" />
</ItemGroup>
diff --git a/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj b/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj
index 8971f72a4..f7f9c0361 100644
--- a/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj
+++ b/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj
@@ -12,11 +12,11 @@
</PropertyGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" />
<PackageReference Include="Moq" Version="4.16.1" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
- <PackageReference Include="coverlet.collector" Version="3.1.1" />
+ <PackageReference Include="coverlet.collector" Version="3.1.2" />
</ItemGroup>
<!-- Code Analyzers -->
diff --git a/tests/Jellyfin.Dlna.Tests/Jellyfin.Dlna.Tests.csproj b/tests/Jellyfin.Dlna.Tests/Jellyfin.Dlna.Tests.csproj
index 39fe0d1c5..a9935bbdb 100644
--- a/tests/Jellyfin.Dlna.Tests/Jellyfin.Dlna.Tests.csproj
+++ b/tests/Jellyfin.Dlna.Tests/Jellyfin.Dlna.Tests.csproj
@@ -7,11 +7,11 @@
</PropertyGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" />
<PackageReference Include="Moq" Version="4.16.1" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
- <PackageReference Include="coverlet.collector" Version="3.1.1" />
+ <PackageReference Include="coverlet.collector" Version="3.1.2" />
</ItemGroup>
<!-- Code Analyzers -->
diff --git a/tests/Jellyfin.Extensions.Tests/Jellyfin.Extensions.Tests.csproj b/tests/Jellyfin.Extensions.Tests/Jellyfin.Extensions.Tests.csproj
index 5e3ec8694..55125eb11 100644
--- a/tests/Jellyfin.Extensions.Tests/Jellyfin.Extensions.Tests.csproj
+++ b/tests/Jellyfin.Extensions.Tests/Jellyfin.Extensions.Tests.csproj
@@ -7,13 +7,13 @@
</PropertyGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
- <PackageReference Include="coverlet.collector" Version="3.1.1">
+ <PackageReference Include="coverlet.collector" Version="3.1.2">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
diff --git a/tests/Jellyfin.MediaEncoding.Hls.Tests/Jellyfin.MediaEncoding.Hls.Tests.csproj b/tests/Jellyfin.MediaEncoding.Hls.Tests/Jellyfin.MediaEncoding.Hls.Tests.csproj
index 64bb15af2..c53dab6d8 100644
--- a/tests/Jellyfin.MediaEncoding.Hls.Tests/Jellyfin.MediaEncoding.Hls.Tests.csproj
+++ b/tests/Jellyfin.MediaEncoding.Hls.Tests/Jellyfin.MediaEncoding.Hls.Tests.csproj
@@ -7,13 +7,13 @@
</PropertyGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.4" />
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
- <PackageReference Include="coverlet.collector" Version="3.1.1">
+ <PackageReference Include="coverlet.collector" Version="3.1.2">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
diff --git a/tests/Jellyfin.MediaEncoding.Keyframes.Tests/Jellyfin.MediaEncoding.Keyframes.Tests.csproj b/tests/Jellyfin.MediaEncoding.Keyframes.Tests/Jellyfin.MediaEncoding.Keyframes.Tests.csproj
index e3f0a7595..268631e58 100644
--- a/tests/Jellyfin.MediaEncoding.Keyframes.Tests/Jellyfin.MediaEncoding.Keyframes.Tests.csproj
+++ b/tests/Jellyfin.MediaEncoding.Keyframes.Tests/Jellyfin.MediaEncoding.Keyframes.Tests.csproj
@@ -8,13 +8,13 @@
</PropertyGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.4" />
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
- <PackageReference Include="coverlet.collector" Version="3.1.1">
+ <PackageReference Include="coverlet.collector" Version="3.1.2">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
diff --git a/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj b/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj
index 5959e0f7b..7a1d88ca6 100644
--- a/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj
+++ b/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj
@@ -21,8 +21,8 @@
<PackageReference Include="AutoFixture" Version="4.17.0" />
<PackageReference Include="AutoFixture.AutoMoq" Version="4.17.0" />
<PackageReference Include="AutoFixture.Xunit2" Version="4.17.0" />
- <PackageReference Include="coverlet.collector" Version="3.1.1" />
- <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
+ <PackageReference Include="coverlet.collector" Version="3.1.2" />
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" />
<PackageReference Include="Moq" Version="4.16.1" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
diff --git a/tests/Jellyfin.Model.Tests/Jellyfin.Model.Tests.csproj b/tests/Jellyfin.Model.Tests/Jellyfin.Model.Tests.csproj
index 4eb598765..9da80c312 100644
--- a/tests/Jellyfin.Model.Tests/Jellyfin.Model.Tests.csproj
+++ b/tests/Jellyfin.Model.Tests/Jellyfin.Model.Tests.csproj
@@ -7,10 +7,10 @@
</PropertyGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" />
- <PackageReference Include="coverlet.collector" Version="3.1.1" />
+ <PackageReference Include="coverlet.collector" Version="3.1.2" />
<PackageReference Include="FsCheck.Xunit" Version="2.16.4" />
</ItemGroup>
diff --git a/tests/Jellyfin.Naming.Tests/ExternalFiles/ExternalPathParserTests.cs b/tests/Jellyfin.Naming.Tests/ExternalFiles/ExternalPathParserTests.cs
new file mode 100644
index 000000000..b396b5440
--- /dev/null
+++ b/tests/Jellyfin.Naming.Tests/ExternalFiles/ExternalPathParserTests.cs
@@ -0,0 +1,111 @@
+using System.Text.RegularExpressions;
+using Emby.Naming.Common;
+using Emby.Naming.ExternalFiles;
+using MediaBrowser.Model.Dlna;
+using MediaBrowser.Model.Globalization;
+using Moq;
+using Xunit;
+
+namespace Jellyfin.Naming.Tests.ExternalFiles;
+
+public class ExternalPathParserTests
+{
+ private readonly ExternalPathParser _audioPathParser;
+ private readonly ExternalPathParser _subtitlePathParser;
+
+ public ExternalPathParserTests()
+ {
+ var englishCultureDto = new CultureDto("English", "English", "en", new[] { "eng" });
+ var frenchCultureDto = new CultureDto("French", "French", "fr", new[] { "fre", "fra" });
+
+ var localizationManager = new Mock<ILocalizationManager>(MockBehavior.Loose);
+ localizationManager.Setup(lm => lm.FindLanguageInfo(It.IsRegex(@"en.*", RegexOptions.IgnoreCase)))
+ .Returns(englishCultureDto);
+ localizationManager.Setup(lm => lm.FindLanguageInfo(It.IsRegex(@"fr.*", RegexOptions.IgnoreCase)))
+ .Returns(frenchCultureDto);
+
+ _audioPathParser = new ExternalPathParser(new NamingOptions(), localizationManager.Object, DlnaProfileType.Audio);
+ _subtitlePathParser = new ExternalPathParser(new NamingOptions(), localizationManager.Object, DlnaProfileType.Subtitle);
+ }
+
+ [Theory]
+ [InlineData("")]
+ [InlineData("MyVideo.ass")]
+ [InlineData("MyVideo.mks")]
+ [InlineData("MyVideo.sami")]
+ [InlineData("MyVideo.srt")]
+ [InlineData("MyVideo.m4v")]
+ public void ParseFile_AudioExtensionsNotMatched_ReturnsNull(string path)
+ {
+ Assert.Null(_audioPathParser.ParseFile(path, string.Empty));
+ }
+
+ [Theory]
+ [InlineData("MyVideo.aa")]
+ [InlineData("MyVideo.aac")]
+ [InlineData("MyVideo.flac")]
+ [InlineData("MyVideo.m4a")]
+ [InlineData("MyVideo.mka")]
+ [InlineData("MyVideo.mp3")]
+ public void ParseFile_AudioExtensionsMatched_ReturnsPath(string path)
+ {
+ var actual = _audioPathParser.ParseFile(path, string.Empty);
+ Assert.NotNull(actual);
+ Assert.Equal(path, actual!.Path);
+ }
+
+ [Theory]
+ [InlineData("")]
+ [InlineData("MyVideo.aa")]
+ [InlineData("MyVideo.aac")]
+ [InlineData("MyVideo.flac")]
+ [InlineData("MyVideo.mka")]
+ [InlineData("MyVideo.m4v")]
+ public void ParseFile_SubtitleExtensionsNotMatched_ReturnsNull(string path)
+ {
+ Assert.Null(_subtitlePathParser.ParseFile(path, string.Empty));
+ }
+
+ [Theory]
+ [InlineData("MyVideo.ass")]
+ [InlineData("MyVideo.mks")]
+ [InlineData("MyVideo.sami")]
+ [InlineData("MyVideo.srt")]
+ [InlineData("MyVideo.vtt")]
+ public void ParseFile_SubtitleExtensionsMatched_ReturnsPath(string path)
+ {
+ var actual = _subtitlePathParser.ParseFile(path, string.Empty);
+ Assert.NotNull(actual);
+ Assert.Equal(path, actual!.Path);
+ }
+
+ [Theory]
+ [InlineData("", null, null)]
+ [InlineData(".default", null, null, true, false)]
+ [InlineData(".forced", null, null, false, true)]
+ [InlineData(".foreign", null, null, false, true)]
+ [InlineData(".default.forced", null, null, true, true)]
+ [InlineData(".forced.default", null, null, true, true)]
+ [InlineData(".DEFAULT.FORCED", null, null, true, true)]
+ [InlineData(".en", null, "eng")]
+ [InlineData(".EN", null, "eng")]
+ [InlineData(".fr.en", "fr", "eng")]
+ [InlineData(".en.fr", "en", "fre")]
+ [InlineData(".title.en.fr", "title.en", "fre")]
+ [InlineData(".Title Goes Here", "Title Goes Here", null)]
+ [InlineData(".Title.with.Separator", "Title.with.Separator", null)]
+ [InlineData(".title.en.default.forced", "title", "eng", true, true)]
+ [InlineData(".forced.default.en.title", "title", "eng", true, true)]
+ public void ParseFile_ExtraTokens_ParseToValues(string tokens, string? title, string? language, bool isDefault = false, bool isForced = false)
+ {
+ var path = "My.Video" + tokens + ".srt";
+
+ var actual = _subtitlePathParser.ParseFile(path, tokens);
+
+ Assert.NotNull(actual);
+ Assert.Equal(title, actual!.Title);
+ Assert.Equal(language, actual.Language);
+ Assert.Equal(isDefault, actual.IsDefault);
+ Assert.Equal(isForced, actual.IsForced);
+ }
+}
diff --git a/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj b/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj
index 53587205c..cc3d4faa0 100644
--- a/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj
+++ b/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj
@@ -12,10 +12,11 @@
</PropertyGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" />
+ <PackageReference Include="Moq" Version="4.16.1" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
- <PackageReference Include="coverlet.collector" Version="3.1.1" />
+ <PackageReference Include="coverlet.collector" Version="3.1.2" />
</ItemGroup>
<ItemGroup>
diff --git a/tests/Jellyfin.Naming.Tests/Subtitles/SubtitleParserTests.cs b/tests/Jellyfin.Naming.Tests/Subtitles/SubtitleParserTests.cs
deleted file mode 100644
index 2446660f3..000000000
--- a/tests/Jellyfin.Naming.Tests/Subtitles/SubtitleParserTests.cs
+++ /dev/null
@@ -1,41 +0,0 @@
-using Emby.Naming.Common;
-using Emby.Naming.Subtitles;
-using Xunit;
-
-namespace Jellyfin.Naming.Tests.Subtitles
-{
- public class SubtitleParserTests
- {
- private readonly NamingOptions _namingOptions = new NamingOptions();
-
- [Theory]
- [InlineData("The Skin I Live In (2011).srt", null, false, false)]
- [InlineData("The Skin I Live In (2011).eng.srt", "eng", false, false)]
- [InlineData("The Skin I Live In (2011).eng.default.srt", "eng", true, false)]
- [InlineData("The Skin I Live In (2011).eng.forced.srt", "eng", false, true)]
- [InlineData("The Skin I Live In (2011).eng.foreign.srt", "eng", false, true)]
- [InlineData("The Skin I Live In (2011).eng.default.foreign.srt", "eng", true, true)]
- [InlineData("The Skin I Live In (2011).default.foreign.eng.srt", "eng", true, true)]
- public void SubtitleParser_ValidFileName_Parses(string input, string language, bool isDefault, bool isForced)
- {
- var parser = new SubtitleParser(_namingOptions);
-
- var result = parser.ParseFile(input);
-
- Assert.Equal(language, result?.Language, true);
- Assert.Equal(isDefault, result?.IsDefault);
- Assert.Equal(isForced, result?.IsForced);
- Assert.Equal(input, result?.Path);
- }
-
- [Theory]
- [InlineData("The Skin I Live In (2011).mp4")]
- [InlineData("")]
- public void SubtitleParser_InvalidFileName_ReturnsNull(string input)
- {
- var parser = new SubtitleParser(_namingOptions);
-
- Assert.Null(parser.ParseFile(input));
- }
- }
-}
diff --git a/tests/Jellyfin.Networking.Tests/Jellyfin.Networking.Tests.csproj b/tests/Jellyfin.Networking.Tests/Jellyfin.Networking.Tests.csproj
index 1b6635ead..00aac8965 100644
--- a/tests/Jellyfin.Networking.Tests/Jellyfin.Networking.Tests.csproj
+++ b/tests/Jellyfin.Networking.Tests/Jellyfin.Networking.Tests.csproj
@@ -12,10 +12,10 @@
</PropertyGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" />
- <PackageReference Include="coverlet.collector" Version="3.1.1" />
+ <PackageReference Include="coverlet.collector" Version="3.1.2" />
<PackageReference Include="FsCheck.Xunit" Version="2.16.4" />
<PackageReference Include="Moq" Version="4.16.1" />
</ItemGroup>
diff --git a/tests/Jellyfin.Providers.Tests/Jellyfin.Providers.Tests.csproj b/tests/Jellyfin.Providers.Tests/Jellyfin.Providers.Tests.csproj
index cf130ff0d..5531049f5 100644
--- a/tests/Jellyfin.Providers.Tests/Jellyfin.Providers.Tests.csproj
+++ b/tests/Jellyfin.Providers.Tests/Jellyfin.Providers.Tests.csproj
@@ -13,14 +13,14 @@
</ItemGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" />
<PackageReference Include="Moq" Version="4.16.1" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
- <PackageReference Include="coverlet.collector" Version="3.1.1">
+ <PackageReference Include="coverlet.collector" Version="3.1.2">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
diff --git a/tests/Jellyfin.Providers.Tests/MediaInfo/AudioResolverTests.cs b/tests/Jellyfin.Providers.Tests/MediaInfo/AudioResolverTests.cs
new file mode 100644
index 000000000..381d6c72d
--- /dev/null
+++ b/tests/Jellyfin.Providers.Tests/MediaInfo/AudioResolverTests.cs
@@ -0,0 +1,79 @@
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using Emby.Naming.Common;
+using MediaBrowser.Controller;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Movies;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.LiveTv;
+using MediaBrowser.Controller.MediaEncoding;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Globalization;
+using MediaBrowser.Providers.MediaInfo;
+using Moq;
+using Xunit;
+
+namespace Jellyfin.Providers.Tests.MediaInfo;
+
+public class AudioResolverTests
+{
+ private readonly AudioResolver _audioResolver;
+
+ public AudioResolverTests()
+ {
+ // prep BaseItem and Video for calls made that expect managers
+ Video.LiveTvManager = Mock.Of<ILiveTvManager>();
+
+ var applicationPaths = new Mock<IServerApplicationPaths>().Object;
+ var serverConfig = new Mock<IServerConfigurationManager>();
+ serverConfig.Setup(c => c.ApplicationPaths)
+ .Returns(applicationPaths);
+ BaseItem.ConfigurationManager = serverConfig.Object;
+
+ // build resolver to test with
+ var localizationManager = Mock.Of<ILocalizationManager>();
+
+ var mediaEncoder = new Mock<IMediaEncoder>(MockBehavior.Strict);
+ mediaEncoder.Setup(me => me.GetMediaInfo(It.IsAny<MediaInfoRequest>(), It.IsAny<CancellationToken>()))
+ .Returns<MediaInfoRequest, CancellationToken>((_, _) => Task.FromResult(new MediaBrowser.Model.MediaInfo.MediaInfo
+ {
+ MediaStreams = new List<MediaStream>
+ {
+ new()
+ }
+ }));
+
+ _audioResolver = new AudioResolver(localizationManager, mediaEncoder.Object, new NamingOptions());
+ }
+
+ [Theory]
+ [InlineData("My.Video.srt", false, false)]
+ [InlineData("My.Video.mp3", false, true)]
+ [InlineData("My.Video.srt", true, false)]
+ [InlineData("My.Video.mp3", true, true)]
+ public async void GetExternalStreams_MixedFilenames_PicksAudio(string file, bool metadataDirectory, bool matches)
+ {
+ BaseItem.MediaSourceManager = Mock.Of<IMediaSourceManager>();
+
+ var video = new Movie
+ {
+ Path = MediaInfoResolverTests.VideoDirectoryPath + "/My.Video.mkv"
+ };
+
+ var directoryService = MediaInfoResolverTests.GetDirectoryServiceForExternalFile(file, metadataDirectory);
+ var streams = await _audioResolver.GetExternalStreamsAsync(video, 0, directoryService, false, CancellationToken.None);
+
+ if (matches)
+ {
+ Assert.Single(streams);
+ var actual = streams[0];
+ Assert.Equal(MediaStreamType.Audio, actual.Type);
+ }
+ else
+ {
+ Assert.Empty(streams);
+ }
+ }
+}
diff --git a/tests/Jellyfin.Providers.Tests/MediaInfo/MediaInfoResolverTests.cs b/tests/Jellyfin.Providers.Tests/MediaInfo/MediaInfoResolverTests.cs
new file mode 100644
index 000000000..926ec5c91
--- /dev/null
+++ b/tests/Jellyfin.Providers.Tests/MediaInfo/MediaInfoResolverTests.cs
@@ -0,0 +1,375 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text.RegularExpressions;
+using System.Threading;
+using System.Threading.Tasks;
+using Emby.Naming.Common;
+using MediaBrowser.Controller;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Movies;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.LiveTv;
+using MediaBrowser.Controller.MediaEncoding;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Globalization;
+using MediaBrowser.Model.MediaInfo;
+using MediaBrowser.Providers.MediaInfo;
+using Moq;
+using Xunit;
+
+namespace Jellyfin.Providers.Tests.MediaInfo;
+
+public class MediaInfoResolverTests
+{
+ public const string VideoDirectoryPath = "Test Data/Video";
+ private const string VideoDirectoryRegex = @"Test Data[/\\]Video";
+ private const string MetadataDirectoryPath = "library/00/00000000000000000000000000000000";
+ private const string MetadataDirectoryRegex = @"library.*";
+
+ private readonly ILocalizationManager _localizationManager;
+ private readonly MediaInfoResolver _subtitleResolver;
+
+ public MediaInfoResolverTests()
+ {
+ // prep BaseItem and Video for calls made that expect managers
+ Video.LiveTvManager = Mock.Of<ILiveTvManager>();
+
+ var applicationPaths = new Mock<IServerApplicationPaths>().Object;
+ var serverConfig = new Mock<IServerConfigurationManager>();
+ serverConfig.Setup(c => c.ApplicationPaths)
+ .Returns(applicationPaths);
+ BaseItem.ConfigurationManager = serverConfig.Object;
+
+ // build resolver to test with
+ var englishCultureDto = new CultureDto("English", "English", "en", new[] { "eng" });
+
+ var localizationManager = new Mock<ILocalizationManager>(MockBehavior.Loose);
+ localizationManager.Setup(lm => lm.FindLanguageInfo(It.IsRegex(@"en.*", RegexOptions.IgnoreCase)))
+ .Returns(englishCultureDto);
+ _localizationManager = localizationManager.Object;
+
+ var mediaEncoder = new Mock<IMediaEncoder>(MockBehavior.Strict);
+ mediaEncoder.Setup(me => me.GetMediaInfo(It.IsAny<MediaInfoRequest>(), It.IsAny<CancellationToken>()))
+ .Returns<MediaInfoRequest, CancellationToken>((_, _) => Task.FromResult(new MediaBrowser.Model.MediaInfo.MediaInfo
+ {
+ MediaStreams = new List<MediaStream>
+ {
+ new()
+ }
+ }));
+
+ _subtitleResolver = new SubtitleResolver(_localizationManager, mediaEncoder.Object, new NamingOptions());
+ }
+
+ [Theory]
+ [InlineData("https://url.com/My.Video.mkv")]
+ [InlineData("non-existent/path")]
+ public void GetExternalFiles_BadPaths_ReturnsNoSubtitles(string path)
+ {
+ // need a media source manager capable of returning something other than file protocol
+ var mediaSourceManager = new Mock<IMediaSourceManager>();
+ mediaSourceManager.Setup(m => m.GetPathProtocol(It.IsRegex(@"http.*")))
+ .Returns(MediaProtocol.Http);
+ BaseItem.MediaSourceManager = mediaSourceManager.Object;
+
+ var video = new Movie
+ {
+ Path = path
+ };
+
+ var files = _subtitleResolver.GetExternalFiles(video, Mock.Of<IDirectoryService>(), false);
+
+ Assert.Empty(files);
+ }
+
+ [Theory]
+ [InlineData("My.Video.srt", null)] // exact
+ [InlineData("My.Video.en.srt", "eng")]
+ [InlineData("MyVideo.en.srt", "eng")] // shorter title
+ [InlineData("My _ Video.en.srt", "eng")] // longer title
+ [InlineData("My.Video.en.srt", "eng", true)]
+ public void GetExternalFiles_FuzzyMatching_MatchesAndParsesToken(string file, string? language, bool metadataDirectory = false)
+ {
+ BaseItem.MediaSourceManager = Mock.Of<IMediaSourceManager>();
+
+ var video = new Movie
+ {
+ Path = VideoDirectoryPath + "/My.Video.mkv"
+ };
+
+ var directoryService = GetDirectoryServiceForExternalFile(file, metadataDirectory);
+ var streams = _subtitleResolver.GetExternalFiles(video, directoryService, false).ToList();
+
+ Assert.Single(streams);
+ var actual = streams[0];
+ Assert.Equal(language, actual.Language);
+ Assert.Null(actual.Title);
+ }
+
+ [Theory]
+ [InlineData("My.Video.mp3")]
+ [InlineData("My.Video.png")]
+ [InlineData("My.Video.txt")]
+ [InlineData("My.Video Sequel.srt")]
+ [InlineData("Some.Other.Video.srt")]
+ public void GetExternalFiles_FuzzyMatching_RejectsNonMatches(string file)
+ {
+ BaseItem.MediaSourceManager = Mock.Of<IMediaSourceManager>();
+
+ var video = new Movie
+ {
+ Path = VideoDirectoryPath + "/My.Video.mkv"
+ };
+
+ var directoryService = GetDirectoryServiceForExternalFile(file);
+ var streams = _subtitleResolver.GetExternalFiles(video, directoryService, false).ToList();
+
+ Assert.Empty(streams);
+ }
+
+ [Theory]
+ [InlineData("https://url.com/My.Video.mkv")]
+ [InlineData("non-existent/path")]
+ [InlineData(VideoDirectoryPath)] // valid but no files found for this test
+ public async void GetExternalStreams_BadPaths_ReturnsNoSubtitles(string path)
+ {
+ // need a media source manager capable of returning something other than file protocol
+ var mediaSourceManager = new Mock<IMediaSourceManager>();
+ mediaSourceManager.Setup(m => m.GetPathProtocol(It.IsRegex(@"http.*")))
+ .Returns(MediaProtocol.Http);
+ BaseItem.MediaSourceManager = mediaSourceManager.Object;
+
+ var video = new Movie
+ {
+ Path = path
+ };
+
+ var directoryService = new Mock<IDirectoryService>(MockBehavior.Strict);
+ directoryService.Setup(ds => ds.GetFilePaths(It.IsAny<string>(), It.IsAny<bool>(), It.IsAny<bool>()))
+ .Returns(Array.Empty<string>());
+
+ var mediaEncoder = Mock.Of<IMediaEncoder>(MockBehavior.Strict);
+
+ var subtitleResolver = new SubtitleResolver(_localizationManager, mediaEncoder, new NamingOptions());
+
+ var streams = await subtitleResolver.GetExternalStreamsAsync(video, 0, directoryService.Object, false, CancellationToken.None);
+
+ Assert.Empty(streams);
+ }
+
+ private static TheoryData<string, MediaStream[], MediaStream[]> GetExternalStreams_MergeMetadata_HandlesOverridesCorrectly_Data()
+ {
+ var data = new TheoryData<string, MediaStream[], MediaStream[]>();
+
+ // filename and stream have no metadata set
+ string file = "My.Video.srt";
+ data.Add(
+ file,
+ new[]
+ {
+ CreateMediaStream(VideoDirectoryPath + "/" + file, null, null, 0)
+ },
+ new[]
+ {
+ CreateMediaStream(VideoDirectoryPath + "/" + file, null, null, 0)
+ });
+
+ // filename has metadata
+ file = "My.Video.Title1.default.forced.en.srt";
+ data.Add(
+ file,
+ new[]
+ {
+ CreateMediaStream(VideoDirectoryPath + "/" + file, null, null, 0)
+ },
+ new[]
+ {
+ CreateMediaStream(VideoDirectoryPath + "/" + file, "eng", "Title1", 0, true, true)
+ });
+
+ // single stream with metadata
+ file = "My.Video.mks";
+ data.Add(
+ file,
+ new[]
+ {
+ CreateMediaStream(VideoDirectoryPath + "/" + file, "eng", "Title", 0, true, true)
+ },
+ new[]
+ {
+ CreateMediaStream(VideoDirectoryPath + "/" + file, "eng", "Title", 0, true, true)
+ });
+
+ // stream wins for title/language, filename wins for flags when conflicting
+ file = "My.Video.Title2.default.forced.en.srt";
+ data.Add(
+ file,
+ new[]
+ {
+ CreateMediaStream(VideoDirectoryPath + "/" + file, "fra", "Metadata", 0)
+ },
+ new[]
+ {
+ CreateMediaStream(VideoDirectoryPath + "/" + file, "fra", "Metadata", 0, true, true)
+ });
+
+ // multiple stream with metadata - filename flags ignored but other data filled in when missing from stream
+ file = "My.Video.Title3.default.forced.en.srt";
+ data.Add(
+ file,
+ new[]
+ {
+ CreateMediaStream(VideoDirectoryPath + "/" + file, null, null, 0, true, true),
+ CreateMediaStream(VideoDirectoryPath + "/" + file, "fra", "Metadata", 1)
+ },
+ new[]
+ {
+ CreateMediaStream(VideoDirectoryPath + "/" + file, "eng", "Title3", 0, true, true),
+ CreateMediaStream(VideoDirectoryPath + "/" + file, "fra", "Metadata", 1)
+ });
+
+ return data;
+ }
+
+ [Theory]
+ [MemberData(nameof(GetExternalStreams_MergeMetadata_HandlesOverridesCorrectly_Data))]
+ public async void GetExternalStreams_MergeMetadata_HandlesOverridesCorrectly(string file, MediaStream[] inputStreams, MediaStream[] expectedStreams)
+ {
+ BaseItem.MediaSourceManager = Mock.Of<IMediaSourceManager>();
+
+ var video = new Movie
+ {
+ Path = VideoDirectoryPath + "/My.Video.mkv"
+ };
+
+ var mediaEncoder = new Mock<IMediaEncoder>(MockBehavior.Strict);
+ mediaEncoder.Setup(me => me.GetMediaInfo(It.IsAny<MediaInfoRequest>(), It.IsAny<CancellationToken>()))
+ .Returns<MediaInfoRequest, CancellationToken>((_, _) => Task.FromResult(new MediaBrowser.Model.MediaInfo.MediaInfo
+ {
+ MediaStreams = inputStreams.ToList()
+ }));
+
+ var subtitleResolver = new SubtitleResolver(_localizationManager, mediaEncoder.Object, new NamingOptions());
+
+ var directoryService = GetDirectoryServiceForExternalFile(file);
+ var streams = await subtitleResolver.GetExternalStreamsAsync(video, 0, directoryService, false, CancellationToken.None);
+
+ Assert.Equal(expectedStreams.Length, streams.Count);
+ for (var i = 0; i < expectedStreams.Length; i++)
+ {
+ var expected = expectedStreams[i];
+ var actual = streams[i];
+
+ Assert.True(actual.IsExternal);
+ Assert.Equal(expected.Index, actual.Index);
+ Assert.Equal(expected.Type, actual.Type);
+ Assert.Equal(expected.Path, actual.Path);
+ Assert.Equal(expected.IsDefault, actual.IsDefault);
+ Assert.Equal(expected.IsForced, actual.IsForced);
+ Assert.Equal(expected.Language, actual.Language);
+ Assert.Equal(expected.Title, actual.Title);
+ }
+ }
+
+ [Theory]
+ [InlineData(1, 1)]
+ [InlineData(1, 2)]
+ [InlineData(2, 1)]
+ [InlineData(2, 2)]
+ public async void GetExternalStreams_StreamIndex_HandlesFilesAndContainers(int fileCount, int streamCount)
+ {
+ BaseItem.MediaSourceManager = Mock.Of<IMediaSourceManager>();
+
+ var video = new Movie
+ {
+ Path = VideoDirectoryPath + "/My.Video.mkv"
+ };
+
+ var files = new string[fileCount];
+ for (int i = 0; i < fileCount; i++)
+ {
+ files[i] = $"{VideoDirectoryPath}/MyVideo.{i}.srt";
+ }
+
+ var directoryService = new Mock<IDirectoryService>(MockBehavior.Strict);
+ directoryService.Setup(ds => ds.GetFilePaths(It.IsRegex(VideoDirectoryRegex), It.IsAny<bool>(), It.IsAny<bool>()))
+ .Returns(files);
+ directoryService.Setup(ds => ds.GetFilePaths(It.IsRegex(MetadataDirectoryRegex), It.IsAny<bool>(), It.IsAny<bool>()))
+ .Returns(Array.Empty<string>());
+
+ List<MediaStream> GenerateMediaStreams()
+ {
+ var mediaStreams = new List<MediaStream>();
+ for (int i = 0; i < streamCount; i++)
+ {
+ mediaStreams.Add(new());
+ }
+
+ return mediaStreams;
+ }
+
+ var mediaEncoder = new Mock<IMediaEncoder>(MockBehavior.Strict);
+ mediaEncoder.Setup(me => me.GetMediaInfo(It.IsAny<MediaInfoRequest>(), It.IsAny<CancellationToken>()))
+ .Returns<MediaInfoRequest, CancellationToken>((_, _) => Task.FromResult(new MediaBrowser.Model.MediaInfo.MediaInfo
+ {
+ MediaStreams = GenerateMediaStreams()
+ }));
+
+ var subtitleResolver = new SubtitleResolver(_localizationManager, mediaEncoder.Object, new NamingOptions());
+
+ int startIndex = 1;
+ var streams = await subtitleResolver.GetExternalStreamsAsync(video, startIndex, directoryService.Object, false, CancellationToken.None);
+
+ Assert.Equal(fileCount * streamCount, streams.Count);
+ for (var i = 0; i < streams.Count; i++)
+ {
+ Assert.Equal(startIndex + i, streams[i].Index);
+ // intentional integer division to ensure correct number of streams come back from each file
+ Assert.Matches(@$".*\.{i / streamCount}\.srt", streams[i].Path);
+ }
+ }
+
+ private static MediaStream CreateMediaStream(string path, string? language, string? title, int index, bool isForced = false, bool isDefault = false)
+ {
+ return new MediaStream
+ {
+ Index = index,
+ Type = MediaStreamType.Subtitle,
+ Path = path,
+ IsDefault = isDefault,
+ IsForced = isForced,
+ Language = language,
+ Title = title
+ };
+ }
+
+ /// <summary>
+ /// Provides an <see cref="IDirectoryService"/> that when queried for the test video/metadata directory will return a path including the provided file name.
+ /// </summary>
+ /// <param name="file">The name of the file to locate.</param>
+ /// <param name="useMetadataDirectory"><c>true</c> if the file belongs in the metadata directory.</param>
+ /// <returns>A mocked <see cref="IDirectoryService"/>.</returns>
+ public static IDirectoryService GetDirectoryServiceForExternalFile(string file, bool useMetadataDirectory = false)
+ {
+ var directoryService = new Mock<IDirectoryService>(MockBehavior.Strict);
+ if (useMetadataDirectory)
+ {
+ directoryService.Setup(ds => ds.GetFilePaths(It.IsRegex(VideoDirectoryRegex), It.IsAny<bool>(), It.IsAny<bool>()))
+ .Returns(Array.Empty<string>());
+ directoryService.Setup(ds => ds.GetFilePaths(It.IsRegex(MetadataDirectoryRegex), It.IsAny<bool>(), It.IsAny<bool>()))
+ .Returns(new[] { MetadataDirectoryPath + "/" + file });
+ }
+ else
+ {
+ directoryService.Setup(ds => ds.GetFilePaths(It.IsRegex(VideoDirectoryRegex), It.IsAny<bool>(), It.IsAny<bool>()))
+ .Returns(new[] { VideoDirectoryPath + "/" + file });
+ directoryService.Setup(ds => ds.GetFilePaths(It.IsRegex(MetadataDirectoryRegex), It.IsAny<bool>(), It.IsAny<bool>()))
+ .Returns(Array.Empty<string>());
+ }
+
+ return directoryService.Object;
+ }
+}
diff --git a/tests/Jellyfin.Providers.Tests/MediaInfo/SubtitleResolverTests.cs b/tests/Jellyfin.Providers.Tests/MediaInfo/SubtitleResolverTests.cs
index 040ea5d1d..0f1086f59 100644
--- a/tests/Jellyfin.Providers.Tests/MediaInfo/SubtitleResolverTests.cs
+++ b/tests/Jellyfin.Providers.Tests/MediaInfo/SubtitleResolverTests.cs
@@ -1,129 +1,79 @@
-#pragma warning disable CA1002 // Do not expose generic lists
-
using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using Emby.Naming.Common;
+using MediaBrowser.Controller;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Movies;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.LiveTv;
+using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Globalization;
using MediaBrowser.Providers.MediaInfo;
using Moq;
using Xunit;
-namespace Jellyfin.Providers.Tests.MediaInfo
+namespace Jellyfin.Providers.Tests.MediaInfo;
+
+public class SubtitleResolverTests
{
- public class SubtitleResolverTests
- {
- public static TheoryData<List<MediaStream>, string, int, string[], MediaStream[]> AddExternalSubtitleStreams_GivenMixedFilenames_ReturnsValidSubtitles_TestData()
- {
- var data = new TheoryData<List<MediaStream>, string, int, string[], MediaStream[]>();
+ private readonly SubtitleResolver _subtitleResolver;
- var index = 0;
- data.Add(
- new List<MediaStream>(),
- "/video/My.Video.mkv",
- index,
- new[]
- {
- "/video/My.Video.mp3",
- "/video/My.Video.png",
- "/video/My.Video.srt",
- "/video/My.Video.txt",
- "/video/My.Video.vtt",
- "/video/My.Video.ass",
- "/video/My.Video.sub",
- "/video/My.Video.ssa",
- "/video/My.Video.smi",
- "/video/My.Video.sami",
- "/video/My.Video.en.srt",
- "/video/My.Video.default.en.srt",
- "/video/My.Video.default.forced.en.srt",
- "/video/My.Video.en.default.forced.srt",
- "/video/My.Video.With.Additional.Garbage.en.srt",
- "/video/My.Video With Additional Garbage.srt"
- },
- new[]
- {
- CreateMediaStream("/video/My.Video.srt", "srt", null, index++),
- CreateMediaStream("/video/My.Video.vtt", "vtt", null, index++),
- CreateMediaStream("/video/My.Video.ass", "ass", null, index++),
- CreateMediaStream("/video/My.Video.sub", "sub", null, index++),
- CreateMediaStream("/video/My.Video.ssa", "ssa", null, index++),
- CreateMediaStream("/video/My.Video.smi", "smi", null, index++),
- CreateMediaStream("/video/My.Video.sami", "sami", null, index++),
- CreateMediaStream("/video/My.Video.en.srt", "srt", "en", index++),
- CreateMediaStream("/video/My.Video.default.en.srt", "srt", "en", index++, isDefault: true),
- CreateMediaStream("/video/My.Video.default.forced.en.srt", "srt", "en", index++, isForced: true, isDefault: true),
- CreateMediaStream("/video/My.Video.en.default.forced.srt", "srt", "en", index++, isForced: true, isDefault: true),
- CreateMediaStream("/video/My.Video.With.Additional.Garbage.en.srt", "srt", "en", index),
- });
+ public SubtitleResolverTests()
+ {
+ // prep BaseItem and Video for calls made that expect managers
+ Video.LiveTvManager = Mock.Of<ILiveTvManager>();
- return data;
- }
+ var applicationPaths = new Mock<IServerApplicationPaths>().Object;
+ var serverConfig = new Mock<IServerConfigurationManager>();
+ serverConfig.Setup(c => c.ApplicationPaths)
+ .Returns(applicationPaths);
+ BaseItem.ConfigurationManager = serverConfig.Object;
- [Theory]
- [MemberData(nameof(AddExternalSubtitleStreams_GivenMixedFilenames_ReturnsValidSubtitles_TestData))]
- public void AddExternalSubtitleStreams_GivenMixedFilenames_ReturnsValidSubtitles(List<MediaStream> streams, string videoPath, int startIndex, string[] files, MediaStream[] expectedResult)
- {
- new SubtitleResolver(Mock.Of<ILocalizationManager>()).AddExternalSubtitleStreams(streams, videoPath, startIndex, files);
+ // build resolver to test with
+ var localizationManager = Mock.Of<ILocalizationManager>();
- Assert.Equal(expectedResult.Length, streams.Count);
- for (var i = 0; i < expectedResult.Length; i++)
+ var mediaEncoder = new Mock<IMediaEncoder>(MockBehavior.Strict);
+ mediaEncoder.Setup(me => me.GetMediaInfo(It.IsAny<MediaInfoRequest>(), It.IsAny<CancellationToken>()))
+ .Returns<MediaInfoRequest, CancellationToken>((_, _) => Task.FromResult(new MediaBrowser.Model.MediaInfo.MediaInfo
{
- var expected = expectedResult[i];
- var actual = streams[i];
+ MediaStreams = new List<MediaStream>
+ {
+ new()
+ }
+ }));
- Assert.Equal(expected.Index, actual.Index);
- Assert.Equal(expected.Type, actual.Type);
- Assert.Equal(expected.IsExternal, actual.IsExternal);
- Assert.Equal(expected.Path, actual.Path);
- Assert.Equal(expected.IsDefault, actual.IsDefault);
- Assert.Equal(expected.IsForced, actual.IsForced);
- Assert.Equal(expected.Language, actual.Language);
- }
- }
+ _subtitleResolver = new SubtitleResolver(localizationManager, mediaEncoder.Object, new NamingOptions());
+ }
- [Theory]
- [InlineData("/video/My Video.mkv", "/video/My Video.srt", "srt", null, false, false)]
- [InlineData("/video/My.Video.mkv", "/video/My.Video.srt", "srt", null, false, false)]
- [InlineData("/video/My.Video.mkv", "/video/My.Video.foreign.srt", "srt", null, true, false)]
- [InlineData("/video/My Video.mkv", "/video/My Video.forced.srt", "srt", null, true, false)]
- [InlineData("/video/My.Video.mkv", "/video/My.Video.default.srt", "srt", null, false, true)]
- [InlineData("/video/My.Video.mkv", "/video/My.Video.forced.default.srt", "srt", null, true, true)]
- [InlineData("/video/My.Video.mkv", "/video/My.Video.en.srt", "srt", "en", false, false)]
- [InlineData("/video/My.Video.mkv", "/video/My.Video.default.en.srt", "srt", "en", false, true)]
- [InlineData("/video/My.Video.mkv", "/video/My.Video.default.forced.en.srt", "srt", "en", true, true)]
- [InlineData("/video/My.Video.mkv", "/video/My.Video.en.default.forced.srt", "srt", "en", true, true)]
- public void AddExternalSubtitleStreams_GivenSingleFile_ReturnsExpectedSubtitle(string videoPath, string file, string codec, string? language, bool isForced, bool isDefault)
+ [Theory]
+ [InlineData("My.Video.srt", false, true)]
+ [InlineData("My.Video.mp3", false, false)]
+ [InlineData("My.Video.srt", true, true)]
+ [InlineData("My.Video.mp3", true, false)]
+ public async void GetExternalStreams_MixedFilenames_PicksSubtitles(string file, bool metadataDirectory, bool matches)
+ {
+ BaseItem.MediaSourceManager = Mock.Of<IMediaSourceManager>();
+
+ var video = new Movie
{
- var streams = new List<MediaStream>();
- var expected = CreateMediaStream(file, codec, language, 0, isForced, isDefault);
+ Path = MediaInfoResolverTests.VideoDirectoryPath + "/My.Video.mkv"
+ };
- new SubtitleResolver(Mock.Of<ILocalizationManager>()).AddExternalSubtitleStreams(streams, videoPath, 0, new[] { file });
+ var directoryService = MediaInfoResolverTests.GetDirectoryServiceForExternalFile(file, metadataDirectory);
+ var streams = await _subtitleResolver.GetExternalStreamsAsync(video, 0, directoryService, false, CancellationToken.None);
+ if (matches)
+ {
Assert.Single(streams);
-
var actual = streams[0];
-
- Assert.Equal(expected.Index, actual.Index);
- Assert.Equal(expected.Type, actual.Type);
- Assert.Equal(expected.IsExternal, actual.IsExternal);
- Assert.Equal(expected.Path, actual.Path);
- Assert.Equal(expected.IsDefault, actual.IsDefault);
- Assert.Equal(expected.IsForced, actual.IsForced);
- Assert.Equal(expected.Language, actual.Language);
+ Assert.Equal(MediaStreamType.Subtitle, actual.Type);
}
-
- private static MediaStream CreateMediaStream(string path, string codec, string? language, int index, bool isForced = false, bool isDefault = false)
+ else
{
- return new()
- {
- Index = index,
- Codec = codec,
- Type = MediaStreamType.Subtitle,
- IsExternal = true,
- Path = path,
- IsDefault = isDefault,
- IsForced = isForced,
- Language = language
- };
+ Assert.Empty(streams);
}
}
}
diff --git a/tests/Jellyfin.Providers.Tests/Test Data/Video/My.Video.mkv b/tests/Jellyfin.Providers.Tests/Test Data/Video/My.Video.mkv
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/tests/Jellyfin.Providers.Tests/Test Data/Video/My.Video.mkv
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj b/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj
index dcba0fefe..066112dcb 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj
+++ b/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj
@@ -21,12 +21,12 @@
<ItemGroup>
<PackageReference Include="AutoFixture" Version="4.17.0" />
<PackageReference Include="AutoFixture.AutoMoq" Version="4.17.0" />
- <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" />
<PackageReference Include="Moq" Version="4.16.1" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
<PackageReference Include="Xunit.SkippableFact" Version="1.4.13" />
- <PackageReference Include="coverlet.collector" Version="3.1.1" />
+ <PackageReference Include="coverlet.collector" Version="3.1.2" />
</ItemGroup>
<!-- Code Analyzers -->
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Library/MediaStreamSelectorTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Library/MediaStreamSelectorTests.cs
new file mode 100644
index 000000000..d59f2f4e5
--- /dev/null
+++ b/tests/Jellyfin.Server.Implementations.Tests/Library/MediaStreamSelectorTests.cs
@@ -0,0 +1,30 @@
+using System;
+using Emby.Server.Implementations.Library;
+using MediaBrowser.Model.Entities;
+using Xunit;
+
+namespace Jellyfin.Server.Implementations.Tests.Library;
+
+public class MediaStreamSelectorTests
+{
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ public void GetDefaultAudioStreamIndex_EmptyStreams_Null(bool preferDefaultTrack)
+ {
+ Assert.Null(MediaStreamSelector.GetDefaultAudioStreamIndex(Array.Empty<MediaStream>(), Array.Empty<string>(), preferDefaultTrack));
+ }
+
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ public void GetDefaultAudioStreamIndex_WithoutDefault_NotNull(bool preferDefaultTrack)
+ {
+ var streams = new[]
+ {
+ new MediaStream()
+ };
+
+ Assert.NotNull(MediaStreamSelector.GetDefaultAudioStreamIndex(streams, Array.Empty<string>(), preferDefaultTrack));
+ }
+}
diff --git a/tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj b/tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj
index 8425cdf33..43e38ea6e 100644
--- a/tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj
+++ b/tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj
@@ -9,13 +9,13 @@
<PackageReference Include="AutoFixture" Version="4.17.0" />
<PackageReference Include="AutoFixture.AutoMoq" Version="4.17.0" />
<PackageReference Include="AutoFixture.Xunit2" Version="4.17.0" />
- <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="6.0.1" />
+ <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="6.0.2" />
<PackageReference Include="Microsoft.Extensions.Options" Version="6.0.0" />
- <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
<PackageReference Include="Xunit.Priority" Version="1.1.6" />
- <PackageReference Include="coverlet.collector" Version="3.1.1" />
+ <PackageReference Include="coverlet.collector" Version="3.1.2" />
<PackageReference Include="Moq" Version="4.16.1" />
</ItemGroup>
diff --git a/tests/Jellyfin.Server.Integration.Tests/JellyfinApplicationFactory.cs b/tests/Jellyfin.Server.Integration.Tests/JellyfinApplicationFactory.cs
index 3d34a18e7..adaf624a9 100644
--- a/tests/Jellyfin.Server.Integration.Tests/JellyfinApplicationFactory.cs
+++ b/tests/Jellyfin.Server.Integration.Tests/JellyfinApplicationFactory.cs
@@ -7,11 +7,11 @@ using MediaBrowser.Common;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.AspNetCore.TestHost;
-using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Serilog;
using Serilog.Extensions.Logging;
+using static MediaBrowser.Controller.Extensions.ConfigurationExtensions;
namespace Jellyfin.Server.Integration.Tests
{
@@ -74,7 +74,7 @@ namespace Jellyfin.Server.Integration.Tests
appPaths,
loggerFactory,
commandLineOpts,
- new ConfigurationBuilder().Build());
+ startupConfig);
_disposableComponents.Add(appHost);
var serviceCollection = new ServiceCollection();
appHost.Init(serviceCollection);
diff --git a/tests/Jellyfin.Server.Tests/Jellyfin.Server.Tests.csproj b/tests/Jellyfin.Server.Tests/Jellyfin.Server.Tests.csproj
index f65faa27e..9576f6a11 100644
--- a/tests/Jellyfin.Server.Tests/Jellyfin.Server.Tests.csproj
+++ b/tests/Jellyfin.Server.Tests/Jellyfin.Server.Tests.csproj
@@ -10,12 +10,12 @@
<PackageReference Include="AutoFixture" Version="4.17.0" />
<PackageReference Include="AutoFixture.AutoMoq" Version="4.17.0" />
<PackageReference Include="AutoFixture.Xunit2" Version="4.17.0" />
- <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="6.0.1" />
+ <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="6.0.2" />
<PackageReference Include="Microsoft.Extensions.Options" Version="6.0.0" />
- <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
- <PackageReference Include="coverlet.collector" Version="3.1.1" />
+ <PackageReference Include="coverlet.collector" Version="3.1.2" />
<PackageReference Include="Moq" Version="4.16.1" />
</ItemGroup>
diff --git a/tests/Jellyfin.XbmcMetadata.Tests/Jellyfin.XbmcMetadata.Tests.csproj b/tests/Jellyfin.XbmcMetadata.Tests/Jellyfin.XbmcMetadata.Tests.csproj
index a0aa51dc9..f34dbc922 100644
--- a/tests/Jellyfin.XbmcMetadata.Tests/Jellyfin.XbmcMetadata.Tests.csproj
+++ b/tests/Jellyfin.XbmcMetadata.Tests/Jellyfin.XbmcMetadata.Tests.csproj
@@ -13,11 +13,11 @@
</ItemGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" />
<PackageReference Include="Moq" Version="4.16.1" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
- <PackageReference Include="coverlet.collector" Version="3.1.1" />
+ <PackageReference Include="coverlet.collector" Version="3.1.2" />
</ItemGroup>
<!-- Code Analyzers -->
diff --git a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/EpisodeNfoProviderTests.cs b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/EpisodeNfoProviderTests.cs
index 3e726f23d..4f4ae5afb 100644
--- a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/EpisodeNfoProviderTests.cs
+++ b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/EpisodeNfoProviderTests.cs
@@ -123,6 +123,20 @@ namespace Jellyfin.XbmcMetadata.Tests.Parsers
}
[Fact]
+ public void Parse_GivenFileWithThumbWithoutAspect_Success()
+ {
+ var result = new MetadataResult<Episode>
+ {
+ Item = new Episode()
+ };
+
+ _parser.Fetch(result, "Test Data/Sonarr-Thumb.nfo", CancellationToken.None);
+
+ Assert.Single(result.RemoteImages.Where(x => x.Type == ImageType.Primary));
+ Assert.Equal("https://artworks.thetvdb.com/banners/episodes/359095/7081317.jpg", result.RemoteImages.First(x => x.Type == ImageType.Primary).Url);
+ }
+
+ [Fact]
public void Fetch_WithNullItem_ThrowsArgumentException()
{
var result = new MetadataResult<Episode>();
diff --git a/tests/Jellyfin.XbmcMetadata.Tests/Test Data/Sonarr-Thumb.nfo b/tests/Jellyfin.XbmcMetadata.Tests/Test Data/Sonarr-Thumb.nfo
new file mode 100644
index 000000000..fb86768ef
--- /dev/null
+++ b/tests/Jellyfin.XbmcMetadata.Tests/Test Data/Sonarr-Thumb.nfo
@@ -0,0 +1,34 @@
+<episodedetails>
+ <title>Sometimes a Genius's Every Action Is at the Mercy of X</title>
+ <season>1</season>
+ <episode>8</episode>
+ <aired>2019-05-26</aired>
+ <plot>After Nariyuki wins a smartphone in a lottery, he can't wait to use it for apps like the dictionary, schedule managing, and the like. He also learns that studying in the bathtub is effective and quickly puts the method into practice.</plot>
+ <uniqueid type="sonarr" default="true">4289</uniqueid>
+ <thumb>https://artworks.thetvdb.com/banners/episodes/359095/7081317.jpg</thumb>
+ <watched>false</watched>
+ <fileinfo>
+ <streamdetails>
+ <video>
+ <aspect>1.77777779</aspect>
+ <bitrate>2208901</bitrate>
+ <codec>x265</codec>
+ <framerate>23.976</framerate>
+ <height>1080</height>
+ <scantype></scantype>
+ <width>1920</width>
+ <duration>23.683416666666666</duration>
+ <durationinseconds>1421</durationinseconds>
+ </video>
+ <audio>
+ <bitrate>1468567</bitrate>
+ <channels>2</channels>
+ <codec>FLAC</codec>
+ <language>Japanese / Japanese</language>
+ </audio>
+ <subtitle>
+ <language>English</language>
+ </subtitle>
+ </streamdetails>
+ </fileinfo>
+</episodedetails>