aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CONTRIBUTORS.md2
-rw-r--r--Emby.Dlna/DlnaManager.cs16
-rw-r--r--Emby.Dlna/Images/logo240.jpgbin11520 -> 11483 bytes
-rw-r--r--Emby.Dlna/Images/people48.pngbin286 -> 278 bytes
-rw-r--r--Emby.Dlna/PlayTo/PlayToController.cs87
-rw-r--r--Emby.Drawing/ImageProcessor.cs2
-rw-r--r--Emby.Naming/AudioBook/AudioBookFilePathParser.cs26
-rw-r--r--Emby.Naming/AudioBook/AudioBookFilePathParserResult.cs3
-rw-r--r--Emby.Naming/AudioBook/AudioBookResolver.cs2
-rw-r--r--Emby.Server.Implementations/ApplicationHost.cs120
-rw-r--r--Emby.Server.Implementations/Data/SqliteExtensions.cs4
-rw-r--r--Emby.Server.Implementations/Dto/DtoService.cs4
-rw-r--r--Emby.Server.Implementations/Emby.Server.Implementations.csproj8
-rw-r--r--Emby.Server.Implementations/Localization/Core/af.json34
-rw-r--r--Emby.Server.Implementations/Localization/Core/fr.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/gl.json10
-rw-r--r--Emby.Server.Implementations/Localization/Core/ko.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/nb.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/sq.json117
-rw-r--r--Emby.Server.Implementations/Localization/Core/ta.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/th.json10
-rw-r--r--Emby.Server.Implementations/Localization/Core/vi.json117
-rw-r--r--Emby.Server.Implementations/Localization/Core/zh-TW.json2
-rw-r--r--Emby.Server.Implementations/Localization/LocalizationManager.cs1
-rw-r--r--Emby.Server.Implementations/Plugins/PluginManifest.cs60
-rw-r--r--Emby.Server.Implementations/Session/SessionManager.cs4
-rw-r--r--Emby.Server.Implementations/Updates/InstallationManager.cs34
-rw-r--r--Jellyfin.Api/Controllers/DynamicHlsController.cs56
-rw-r--r--Jellyfin.Api/Controllers/ItemLookupController.cs2
-rw-r--r--Jellyfin.Api/Controllers/LiveTvController.cs2
-rw-r--r--Jellyfin.Api/Controllers/SessionController.cs47
-rw-r--r--Jellyfin.Api/Controllers/SubtitleController.cs8
-rw-r--r--Jellyfin.Api/Controllers/UserController.cs6
-rw-r--r--Jellyfin.Api/Controllers/VideosController.cs4
-rw-r--r--Jellyfin.Api/Helpers/StreamingHelpers.cs2
-rw-r--r--Jellyfin.Api/Helpers/TranscodingJobHelper.cs5
-rw-r--r--Jellyfin.Api/Jellyfin.Api.csproj4
-rw-r--r--Jellyfin.Api/Models/UserDtos/ForgotPasswordDto.cs16
-rw-r--r--Jellyfin.Data/Jellyfin.Data.csproj4
-rw-r--r--Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj4
-rw-r--r--Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj4
-rw-r--r--Jellyfin.Server/Jellyfin.Server.csproj8
-rw-r--r--MediaBrowser.Common/MediaBrowser.Common.csproj4
-rw-r--r--MediaBrowser.Controller/Extensions/StringExtensions.cs6
-rw-r--r--MediaBrowser.Controller/Library/NameExtensions.cs3
-rw-r--r--MediaBrowser.Controller/MediaBrowser.Controller.csproj4
-rw-r--r--MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs5
-rw-r--r--MediaBrowser.Model/Dlna/ResolutionNormalizer.cs1
-rw-r--r--MediaBrowser.Model/MediaBrowser.Model.csproj2
-rw-r--r--MediaBrowser.Model/Session/GeneralCommand.cs2
-rw-r--r--MediaBrowser.Providers/Manager/MetadataService.cs19
-rw-r--r--MediaBrowser.Providers/MediaBrowser.Providers.csproj6
-rw-r--r--MediaBrowser.Providers/Plugins/TheTvdb/TvdbEpisodeProvider.cs1
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/General/Videos.cs2
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/Trailers.cs2
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/People/PersonImages.cs2
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbImageProvider.cs5
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbImageSettings.cs (renamed from MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbSettings.cs)11
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs28
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbSearch.cs7
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbSettingsResult.cs9
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Music/TmdbMusicVideoProvider.cs4
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs11
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeImageProvider.cs23
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs15
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProviderBase.cs13
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonImageProvider.cs5
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs23
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesImageProvider.cs28
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs37
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Trailers/TmdbTrailerProvider.cs8
-rw-r--r--MediaBrowser.Providers/Studios/StudiosImageProvider.cs16
-rw-r--r--MediaBrowser.Providers/Subtitles/SubtitleManager.cs2
-rw-r--r--MediaBrowser.Providers/TV/MissingEpisodeProvider.cs15
-rw-r--r--MediaBrowser.Providers/TV/SeasonMetadataService.cs6
-rw-r--r--README.md9
-rw-r--r--RSSDP/DisposableManagedObjectBase.cs4
-rw-r--r--deployment/Dockerfile.debian.amd642
-rw-r--r--deployment/Dockerfile.debian.arm642
-rw-r--r--deployment/Dockerfile.debian.armhf2
-rw-r--r--deployment/Dockerfile.linux.amd642
-rw-r--r--deployment/Dockerfile.macos2
-rw-r--r--deployment/Dockerfile.portable2
-rw-r--r--deployment/Dockerfile.ubuntu.amd642
-rw-r--r--deployment/Dockerfile.ubuntu.arm642
-rw-r--r--deployment/Dockerfile.ubuntu.armhf2
-rw-r--r--deployment/Dockerfile.windows.amd642
-rw-r--r--fedora/jellyfin.spec4
-rw-r--r--tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj4
-rw-r--r--tests/Jellyfin.Naming.Tests/AudioBook/AudioBookFileInfoTests.cs30
-rw-r--r--tests/Jellyfin.Naming.Tests/AudioBook/AudioBookListResolverTests.cs90
-rw-r--r--tests/Jellyfin.Naming.Tests/AudioBook/AudioBookResolverTests.cs57
-rw-r--r--tests/Jellyfin.Naming.Tests/Video/ExtraTests.cs16
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Library/PathExtensionsTests.cs1
94 files changed, 1029 insertions, 376 deletions
diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md
index f0724b412..efd83012e 100644
--- a/CONTRIBUTORS.md
+++ b/CONTRIBUTORS.md
@@ -135,6 +135,7 @@
- [YouKnowBlom](https://github.com/YouKnowBlom)
- [KristupasSavickas](https://github.com/KristupasSavickas)
- [Pusta](https://github.com/pusta)
+ - [nielsvanvelzen](https://github.com/nielsvanvelzen)
# Emby Contributors
@@ -198,3 +199,4 @@
- [tikuf](https://github.com/tikuf/)
- [Tim Hobbs](https://github.com/timhobbs)
- [SvenVandenbrande](https://github.com/SvenVandenbrande)
+ - [olsh](https://github.com/olsh)
diff --git a/Emby.Dlna/DlnaManager.cs b/Emby.Dlna/DlnaManager.cs
index 5612376a5..1807ac6a1 100644
--- a/Emby.Dlna/DlnaManager.cs
+++ b/Emby.Dlna/DlnaManager.cs
@@ -126,14 +126,14 @@ namespace Emby.Dlna
var builder = new StringBuilder();
builder.AppendLine("No matching device profile found. The default will need to be used.");
- builder.AppendFormat(CultureInfo.InvariantCulture, "FriendlyName:{0}", profile.FriendlyName ?? string.Empty).AppendLine();
- builder.AppendFormat(CultureInfo.InvariantCulture, "Manufacturer:{0}", profile.Manufacturer ?? string.Empty).AppendLine();
- builder.AppendFormat(CultureInfo.InvariantCulture, "ManufacturerUrl:{0}", profile.ManufacturerUrl ?? string.Empty).AppendLine();
- builder.AppendFormat(CultureInfo.InvariantCulture, "ModelDescription:{0}", profile.ModelDescription ?? string.Empty).AppendLine();
- builder.AppendFormat(CultureInfo.InvariantCulture, "ModelName:{0}", profile.ModelName ?? string.Empty).AppendLine();
- builder.AppendFormat(CultureInfo.InvariantCulture, "ModelNumber:{0}", profile.ModelNumber ?? string.Empty).AppendLine();
- builder.AppendFormat(CultureInfo.InvariantCulture, "ModelUrl:{0}", profile.ModelUrl ?? string.Empty).AppendLine();
- builder.AppendFormat(CultureInfo.InvariantCulture, "SerialNumber:{0}", profile.SerialNumber ?? string.Empty).AppendLine();
+ builder.Append("FriendlyName:").AppendLine(profile.FriendlyName);
+ builder.Append("Manufacturer:").AppendLine(profile.Manufacturer);
+ builder.Append("ManufacturerUrl:").AppendLine(profile.ManufacturerUrl);
+ builder.Append("ModelDescription:").AppendLine(profile.ModelDescription);
+ builder.Append("ModelName:").AppendLine(profile.ModelName);
+ builder.Append("ModelNumber:").AppendLine(profile.ModelNumber);
+ builder.Append("ModelUrl:").AppendLine(profile.ModelUrl);
+ builder.Append("SerialNumber:").AppendLine(profile.SerialNumber);
_logger.LogInformation(builder.ToString());
}
diff --git a/Emby.Dlna/Images/logo240.jpg b/Emby.Dlna/Images/logo240.jpg
index da1cb5e07..78a27f1b5 100644
--- a/Emby.Dlna/Images/logo240.jpg
+++ b/Emby.Dlna/Images/logo240.jpg
Binary files differ
diff --git a/Emby.Dlna/Images/people48.png b/Emby.Dlna/Images/people48.png
index 7fb25e6b3..dae5f6057 100644
--- a/Emby.Dlna/Images/people48.png
+++ b/Emby.Dlna/Images/people48.png
Binary files differ
diff --git a/Emby.Dlna/PlayTo/PlayToController.cs b/Emby.Dlna/PlayTo/PlayToController.cs
index 328759c5b..460ac2d8d 100644
--- a/Emby.Dlna/PlayTo/PlayToController.cs
+++ b/Emby.Dlna/PlayTo/PlayToController.cs
@@ -669,62 +669,57 @@ namespace Emby.Dlna.PlayTo
private Task SendGeneralCommand(GeneralCommand command, CancellationToken cancellationToken)
{
- if (Enum.TryParse(command.Name, true, out GeneralCommandType commandType))
- {
- switch (commandType)
- {
- case GeneralCommandType.VolumeDown:
- return _device.VolumeDown(cancellationToken);
- case GeneralCommandType.VolumeUp:
- return _device.VolumeUp(cancellationToken);
- case GeneralCommandType.Mute:
- return _device.Mute(cancellationToken);
- case GeneralCommandType.Unmute:
- return _device.Unmute(cancellationToken);
- case GeneralCommandType.ToggleMute:
- return _device.ToggleMute(cancellationToken);
- case GeneralCommandType.SetAudioStreamIndex:
- if (command.Arguments.TryGetValue("Index", out string index))
+ switch (command.Name)
+ {
+ case GeneralCommandType.VolumeDown:
+ return _device.VolumeDown(cancellationToken);
+ case GeneralCommandType.VolumeUp:
+ return _device.VolumeUp(cancellationToken);
+ case GeneralCommandType.Mute:
+ return _device.Mute(cancellationToken);
+ case GeneralCommandType.Unmute:
+ return _device.Unmute(cancellationToken);
+ case GeneralCommandType.ToggleMute:
+ return _device.ToggleMute(cancellationToken);
+ case GeneralCommandType.SetAudioStreamIndex:
+ if (command.Arguments.TryGetValue("Index", out string index))
+ {
+ if (int.TryParse(index, NumberStyles.Integer, _usCulture, out var val))
{
- if (int.TryParse(index, NumberStyles.Integer, _usCulture, out var val))
- {
- return SetAudioStreamIndex(val);
- }
-
- throw new ArgumentException("Unsupported SetAudioStreamIndex value supplied.");
+ return SetAudioStreamIndex(val);
}
- throw new ArgumentException("SetAudioStreamIndex argument cannot be null");
- case GeneralCommandType.SetSubtitleStreamIndex:
- if (command.Arguments.TryGetValue("Index", out index))
- {
- if (int.TryParse(index, NumberStyles.Integer, _usCulture, out var val))
- {
- return SetSubtitleStreamIndex(val);
- }
+ throw new ArgumentException("Unsupported SetAudioStreamIndex value supplied.");
+ }
- throw new ArgumentException("Unsupported SetSubtitleStreamIndex value supplied.");
+ throw new ArgumentException("SetAudioStreamIndex argument cannot be null");
+ case GeneralCommandType.SetSubtitleStreamIndex:
+ if (command.Arguments.TryGetValue("Index", out index))
+ {
+ if (int.TryParse(index, NumberStyles.Integer, _usCulture, out var val))
+ {
+ return SetSubtitleStreamIndex(val);
}
- throw new ArgumentException("SetSubtitleStreamIndex argument cannot be null");
- case GeneralCommandType.SetVolume:
- if (command.Arguments.TryGetValue("Volume", out string vol))
- {
- if (int.TryParse(vol, NumberStyles.Integer, _usCulture, out var volume))
- {
- return _device.SetVolume(volume, cancellationToken);
- }
+ throw new ArgumentException("Unsupported SetSubtitleStreamIndex value supplied.");
+ }
- throw new ArgumentException("Unsupported volume value supplied.");
+ throw new ArgumentException("SetSubtitleStreamIndex argument cannot be null");
+ case GeneralCommandType.SetVolume:
+ if (command.Arguments.TryGetValue("Volume", out string vol))
+ {
+ if (int.TryParse(vol, NumberStyles.Integer, _usCulture, out var volume))
+ {
+ return _device.SetVolume(volume, cancellationToken);
}
- throw new ArgumentException("Volume argument cannot be null");
- default:
- return Task.CompletedTask;
- }
- }
+ throw new ArgumentException("Unsupported volume value supplied.");
+ }
- return Task.CompletedTask;
+ throw new ArgumentException("Volume argument cannot be null");
+ default:
+ return Task.CompletedTask;
+ }
}
private async Task SetAudioStreamIndex(int? newIndex)
diff --git a/Emby.Drawing/ImageProcessor.cs b/Emby.Drawing/ImageProcessor.cs
index f585b90ca..ed20292f6 100644
--- a/Emby.Drawing/ImageProcessor.cs
+++ b/Emby.Drawing/ImageProcessor.cs
@@ -455,7 +455,7 @@ namespace Emby.Drawing
throw new ArgumentException("Path can't be empty.", nameof(path));
}
- if (path.IsEmpty)
+ if (filename.IsEmpty)
{
throw new ArgumentException("Filename can't be empty.", nameof(filename));
}
diff --git a/Emby.Naming/AudioBook/AudioBookFilePathParser.cs b/Emby.Naming/AudioBook/AudioBookFilePathParser.cs
index 3c874c62c..14edd6492 100644
--- a/Emby.Naming/AudioBook/AudioBookFilePathParser.cs
+++ b/Emby.Naming/AudioBook/AudioBookFilePathParser.cs
@@ -1,6 +1,6 @@
+#nullable enable
#pragma warning disable CS1591
-using System;
using System.Globalization;
using System.IO;
using System.Text.RegularExpressions;
@@ -19,12 +19,7 @@ namespace Emby.Naming.AudioBook
public AudioBookFilePathParserResult Parse(string path)
{
- if (path == null)
- {
- throw new ArgumentNullException(nameof(path));
- }
-
- var result = new AudioBookFilePathParserResult();
+ AudioBookFilePathParserResult result = default;
var fileName = Path.GetFileNameWithoutExtension(path);
foreach (var expression in _options.AudioBookPartsExpressions)
{
@@ -50,27 +45,14 @@ namespace Emby.Naming.AudioBook
{
if (int.TryParse(value.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intValue))
{
- result.ChapterNumber = intValue;
+ result.PartNumber = intValue;
}
}
}
}
}
- /*var matches = _iRegexProvider.GetRegex("\\d+", RegexOptions.IgnoreCase).Matches(fileName);
- if (matches.Count > 0)
- {
- if (!result.ChapterNumber.HasValue)
- {
- result.ChapterNumber = int.Parse(matches[0].Groups[0].Value);
- }
-
- if (matches.Count > 1)
- {
- result.PartNumber = int.Parse(matches[matches.Count - 1].Groups[0].Value);
- }
- }*/
- result.Success = result.PartNumber.HasValue || result.ChapterNumber.HasValue;
+ result.Success = result.ChapterNumber.HasValue || result.PartNumber.HasValue;
return result;
}
diff --git a/Emby.Naming/AudioBook/AudioBookFilePathParserResult.cs b/Emby.Naming/AudioBook/AudioBookFilePathParserResult.cs
index e28a58db7..7bfc4479d 100644
--- a/Emby.Naming/AudioBook/AudioBookFilePathParserResult.cs
+++ b/Emby.Naming/AudioBook/AudioBookFilePathParserResult.cs
@@ -1,8 +1,9 @@
+#nullable enable
#pragma warning disable CS1591
namespace Emby.Naming.AudioBook
{
- public class AudioBookFilePathParserResult
+ public struct AudioBookFilePathParserResult
{
public int? PartNumber { get; set; }
diff --git a/Emby.Naming/AudioBook/AudioBookResolver.cs b/Emby.Naming/AudioBook/AudioBookResolver.cs
index 5466b4637..ed53bd04f 100644
--- a/Emby.Naming/AudioBook/AudioBookResolver.cs
+++ b/Emby.Naming/AudioBook/AudioBookResolver.cs
@@ -55,8 +55,8 @@ namespace Emby.Naming.AudioBook
{
Path = path,
Container = container,
- PartNumber = parsingResult.PartNumber,
ChapterNumber = parsingResult.ChapterNumber,
+ PartNumber = parsingResult.PartNumber,
IsDirectory = isDirectory
};
}
diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs
index 642e2fdbe..7a46fdf2e 100644
--- a/Emby.Server.Implementations/ApplicationHost.cs
+++ b/Emby.Server.Implementations/ApplicationHost.cs
@@ -4,6 +4,7 @@ using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
+using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
@@ -37,6 +38,7 @@ using Emby.Server.Implementations.LiveTv;
using Emby.Server.Implementations.Localization;
using Emby.Server.Implementations.Net;
using Emby.Server.Implementations.Playlists;
+using Emby.Server.Implementations.Plugins;
using Emby.Server.Implementations.QuickConnect;
using Emby.Server.Implementations.ScheduledTasks;
using Emby.Server.Implementations.Security;
@@ -119,6 +121,7 @@ namespace Emby.Server.Implementations
private readonly IFileSystem _fileSystemManager;
private readonly INetworkManager _networkManager;
private readonly IXmlSerializer _xmlSerializer;
+ private readonly IJsonSerializer _jsonSerializer;
private readonly IStartupOptions _startupOptions;
private IMediaEncoder _mediaEncoder;
@@ -255,6 +258,8 @@ namespace Emby.Server.Implementations
IServiceCollection serviceCollection)
{
_xmlSerializer = new MyXmlSerializer();
+ _jsonSerializer = new JsonSerializer();
+
ServiceCollection = serviceCollection;
_networkManager = networkManager;
@@ -1022,6 +1027,119 @@ namespace Emby.Server.Implementations
protected abstract void RestartInternal();
/// <summary>
+ /// Comparison function used in <see cref="GetPlugins" />.
+ /// </summary>
+ /// <param name="a">Item to compare.</param>
+ /// <param name="b">Item to compare with.</param>
+ /// <returns>Boolean result of the operation.</returns>
+ private static int VersionCompare(
+ (Version PluginVersion, string Name, string Path) a,
+ (Version PluginVersion, string Name, string Path) b)
+ {
+ int compare = string.Compare(a.Name, b.Name, true, CultureInfo.InvariantCulture);
+
+ if (compare == 0)
+ {
+ return a.PluginVersion.CompareTo(b.PluginVersion);
+ }
+
+ return compare;
+ }
+
+ /// <summary>
+ /// Returns a list of plugins to install.
+ /// </summary>
+ /// <param name="path">Path to check.</param>
+ /// <param name="cleanup">True if an attempt should be made to delete old plugs.</param>
+ /// <returns>Enumerable list of dlls to load.</returns>
+ private IEnumerable<string> GetPlugins(string path, bool cleanup = true)
+ {
+ var dllList = new List<string>();
+ var versions = new List<(Version PluginVersion, string Name, string Path)>();
+ var directories = Directory.EnumerateDirectories(path, "*.*", SearchOption.TopDirectoryOnly);
+ string metafile;
+
+ foreach (var dir in directories)
+ {
+ try
+ {
+ metafile = Path.Combine(dir, "meta.json");
+ if (File.Exists(metafile))
+ {
+ var manifest = _jsonSerializer.DeserializeFromFile<PluginManifest>(metafile);
+
+ if (!Version.TryParse(manifest.TargetAbi, out var targetAbi))
+ {
+ targetAbi = new Version(0, 0, 0, 1);
+ }
+
+ if (!Version.TryParse(manifest.Version, out var version))
+ {
+ version = new Version(0, 0, 0, 1);
+ }
+
+ if (ApplicationVersion >= targetAbi)
+ {
+ // Only load Plugins if the plugin is built for this version or below.
+ versions.Add((version, manifest.Name, dir));
+ }
+ }
+ else
+ {
+ // No metafile, so lets see if the folder is versioned.
+ metafile = dir.Split(new[] { Path.DirectorySeparatorChar }, StringSplitOptions.RemoveEmptyEntries)[^1];
+
+ int versionIndex = dir.LastIndexOf('_');
+ if (versionIndex != -1 && Version.TryParse(dir.Substring(versionIndex + 1), out Version ver))
+ {
+ // Versioned folder.
+ versions.Add((ver, metafile, dir));
+ }
+ else
+ {
+ // Un-versioned folder - Add it under the path name and version 0.0.0.1.
+ versions.Add((new Version(0, 0, 0, 1), metafile, dir));
+ }
+ }
+ }
+ catch
+ {
+ continue;
+ }
+ }
+
+ string lastName = string.Empty;
+ versions.Sort(VersionCompare);
+ // Traverse backwards through the list.
+ // The first item will be the latest version.
+ for (int x = versions.Count - 1; x >= 0; x--)
+ {
+ if (!string.Equals(lastName, versions[x].Name, StringComparison.OrdinalIgnoreCase))
+ {
+ dllList.AddRange(Directory.EnumerateFiles(versions[x].Path, "*.dll", SearchOption.AllDirectories));
+ lastName = versions[x].Name;
+ continue;
+ }
+
+ if (!string.IsNullOrEmpty(lastName) && cleanup)
+ {
+ // Attempt a cleanup of old folders.
+ try
+ {
+ Logger.LogDebug("Deleting {Path}", versions[x].Path);
+ Directory.Delete(versions[x].Path, true);
+ }
+ catch (Exception e)
+ {
+ Logger.LogWarning(e, "Unable to delete {Path}", versions[x].Path);
+ }
+ }
+ }
+
+ return dllList;
+ }
+
+ /// <summary>
/// Gets the composable part assemblies.
/// </summary>
/// <returns>IEnumerable{Assembly}.</returns>
@@ -1029,7 +1147,7 @@ namespace Emby.Server.Implementations
{
if (Directory.Exists(ApplicationPaths.PluginsPath))
{
- foreach (var file in Directory.EnumerateFiles(ApplicationPaths.PluginsPath, "*.dll", SearchOption.AllDirectories))
+ foreach (var file in GetPlugins(ApplicationPaths.PluginsPath))
{
Assembly plugAss;
try
diff --git a/Emby.Server.Implementations/Data/SqliteExtensions.cs b/Emby.Server.Implementations/Data/SqliteExtensions.cs
index 716e5071d..70a6df977 100644
--- a/Emby.Server.Implementations/Data/SqliteExtensions.cs
+++ b/Emby.Server.Implementations/Data/SqliteExtensions.cs
@@ -234,7 +234,9 @@ namespace Emby.Server.Implementations.Data
{
if (statement.BindParameters.TryGetValue(name, out IBindParameter bindParam))
{
- bindParam.Bind(value.ToByteArray());
+ Span<byte> byteValue = stackalloc byte[16];
+ value.TryWriteBytes(byteValue);
+ bindParam.Bind(byteValue);
}
else
{
diff --git a/Emby.Server.Implementations/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs
index 57c1398e9..38f38ddbf 100644
--- a/Emby.Server.Implementations/Dto/DtoService.cs
+++ b/Emby.Server.Implementations/Dto/DtoService.cs
@@ -1139,6 +1139,7 @@ namespace Emby.Server.Implementations.Dto
if (episodeSeries != null)
{
dto.SeriesPrimaryImageTag = GetTagAndFillBlurhash(dto, episodeSeries, ImageType.Primary);
+ AttachPrimaryImageAspectRatio(dto, episodeSeries);
}
}
@@ -1185,6 +1186,7 @@ namespace Emby.Server.Implementations.Dto
if (series != null)
{
dto.SeriesPrimaryImageTag = GetTagAndFillBlurhash(dto, series, ImageType.Primary);
+ AttachPrimaryImageAspectRatio(dto, series);
}
}
}
@@ -1431,7 +1433,7 @@ namespace Emby.Server.Implementations.Dto
return null;
}
- return width / height;
+ return (double)width / height;
}
}
}
diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj
index 0a348f0d0..c84c7b53d 100644
--- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj
+++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj
@@ -32,10 +32,10 @@
<PackageReference Include="Microsoft.AspNetCore.ResponseCompression" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Server.Kestrel" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.WebSockets" Version="2.2.1" />
- <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.1.7" />
- <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="3.1.7" />
- <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.7" />
- <PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="3.1.7" />
+ <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.1.8" />
+ <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="3.1.8" />
+ <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.8" />
+ <PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="3.1.8" />
<PackageReference Include="Mono.Nat" Version="2.0.2" />
<PackageReference Include="prometheus-net.DotNetRuntime" Version="3.4.0" />
<PackageReference Include="ServiceStack.Text.Core" Version="5.9.2" />
diff --git a/Emby.Server.Implementations/Localization/Core/af.json b/Emby.Server.Implementations/Localization/Core/af.json
index e587c37d5..d33e11893 100644
--- a/Emby.Server.Implementations/Localization/Core/af.json
+++ b/Emby.Server.Implementations/Localization/Core/af.json
@@ -1,19 +1,19 @@
{
"Artists": "Kunstenare",
"Channels": "Kanale",
- "Folders": "Fouers",
- "Favorites": "Gunstelinge",
+ "Folders": "Lêergidse",
+ "Favorites": "Gunstellinge",
"HeaderFavoriteShows": "Gunsteling Vertonings",
"ValueSpecialEpisodeName": "Spesiale - {0}",
"HeaderAlbumArtists": "Album Kunstenaars",
"Books": "Boeke",
"HeaderNextUp": "Volgende",
- "Movies": "Rolprente",
- "Shows": "Program",
- "HeaderContinueWatching": "Hou Aan Kyk",
+ "Movies": "Flieks",
+ "Shows": "Televisie Reekse",
+ "HeaderContinueWatching": "Kyk Verder",
"HeaderFavoriteEpisodes": "Gunsteling Episodes",
"Photos": "Fotos",
- "Playlists": "Speellysse",
+ "Playlists": "Snitlyste",
"HeaderFavoriteArtists": "Gunsteling Kunstenaars",
"HeaderFavoriteAlbums": "Gunsteling Albums",
"Sync": "Sinkroniseer",
@@ -23,7 +23,7 @@
"DeviceOfflineWithName": "{0} is ontkoppel",
"Collections": "Versamelings",
"Inherit": "Ontvang",
- "HeaderLiveTV": "Live TV",
+ "HeaderLiveTV": "Lewendige TV",
"Application": "Program",
"AppDeviceValues": "App: {0}, Toestel: {1}",
"VersionNumber": "Weergawe {0}",
@@ -95,5 +95,23 @@
"TasksChannelsCategory": "Internet kanale",
"TasksApplicationCategory": "aansoek",
"TasksLibraryCategory": "biblioteek",
- "TasksMaintenanceCategory": "onderhoud"
+ "TasksMaintenanceCategory": "onderhoud",
+ "TaskCleanCacheDescription": "Vee kasregister lêers uit wat nie meer deur die stelsel benodig word nie.",
+ "TaskCleanCache": "Reinig Kasgeheue Lêergids",
+ "TaskDownloadMissingSubtitlesDescription": "Soek aanlyn vir vermiste onderskrifte gebasseer op metadata verstellings.",
+ "TaskDownloadMissingSubtitles": "Laai vermiste onderskrifte af",
+ "TaskRefreshChannelsDescription": "Vervris internet kanaal inligting.",
+ "TaskRefreshChannels": "Vervris Kanale",
+ "TaskCleanTranscodeDescription": "Vee transkodering lêers uit wat ouer is as een dag.",
+ "TaskCleanTranscode": "Reinig Transkoderings Leêrbinder",
+ "TaskUpdatePluginsDescription": "Laai opgedateerde inprop-sagteware af en installeer inprop-sagteware wat verstel is om outomaties op te dateer.",
+ "TaskUpdatePlugins": "Dateer Inprop-Sagteware Op",
+ "TaskRefreshPeopleDescription": "Vervris metadata oor akteurs en regisseurs in u media versameling.",
+ "TaskRefreshPeople": "Vervris Mense",
+ "TaskCleanLogsDescription": "Vee loglêers wat ouer as {0} dae is uit.",
+ "TaskCleanLogs": "Reinig Loglêer Lêervouer",
+ "TaskRefreshLibraryDescription": "Skandeer u media versameling vir nuwe lêers en verfris metadata.",
+ "TaskRefreshLibrary": "Skandeer Media Versameling",
+ "TaskRefreshChapterImagesDescription": "Maak kleinkiekeis (fotos) vir films wat hoofstukke het.",
+ "TaskRefreshChapterImages": "Verkry Hoofstuk Beelde"
}
diff --git a/Emby.Server.Implementations/Localization/Core/fr.json b/Emby.Server.Implementations/Localization/Core/fr.json
index 47ebe1254..7fc996821 100644
--- a/Emby.Server.Implementations/Localization/Core/fr.json
+++ b/Emby.Server.Implementations/Localization/Core/fr.json
@@ -107,7 +107,7 @@
"TaskCleanLogsDescription": "Supprime les journaux de plus de {0} jours.",
"TaskCleanLogs": "Nettoyer le répertoire des journaux",
"TaskRefreshLibraryDescription": "Scanne toute les bibliothèques pour trouver les nouveaux fichiers et rafraîchit les métadonnées.",
- "TaskRefreshLibrary": "Scanner toute les Bibliothèques",
+ "TaskRefreshLibrary": "Scanner toutes les Bibliothèques",
"TaskRefreshChapterImagesDescription": "Crée des images de miniature pour les vidéos ayant des chapitres.",
"TaskRefreshChapterImages": "Extraire les images de chapitre",
"TaskCleanCacheDescription": "Supprime les fichiers de cache dont le système n'a plus besoin.",
diff --git a/Emby.Server.Implementations/Localization/Core/gl.json b/Emby.Server.Implementations/Localization/Core/gl.json
index 94034962d..faee2519a 100644
--- a/Emby.Server.Implementations/Localization/Core/gl.json
+++ b/Emby.Server.Implementations/Localization/Core/gl.json
@@ -1,3 +1,11 @@
{
- "Albums": "Álbumes"
+ "Albums": "Álbumes",
+ "Collections": "Colecións",
+ "ChapterNameValue": "Capítulos {0}",
+ "Channels": "Canles",
+ "CameraImageUploadedFrom": "Cargouse unha nova imaxe da cámara desde {0}",
+ "Books": "Libros",
+ "AuthenticationSucceededWithUserName": "{0} autenticouse correctamente",
+ "Artists": "Artistas",
+ "Application": "Aplicativo"
}
diff --git a/Emby.Server.Implementations/Localization/Core/ko.json b/Emby.Server.Implementations/Localization/Core/ko.json
index 9e3ecd5a8..a33953c27 100644
--- a/Emby.Server.Implementations/Localization/Core/ko.json
+++ b/Emby.Server.Implementations/Localization/Core/ko.json
@@ -84,8 +84,8 @@
"UserDeletedWithName": "사용자 {0} 삭제됨",
"UserDownloadingItemWithValues": "{0}이(가) {1}을 다운로드 중입니다",
"UserLockedOutWithName": "유저 {0} 은(는) 잠금처리 되었습니다",
- "UserOfflineFromDevice": "{1}로부터 {0}의 연결이 끊겼습니다",
- "UserOnlineFromDevice": "{0}은 {1}에서 온라인 상태입니다",
+ "UserOfflineFromDevice": "{1}에서 {0}의 연결이 끊킴",
+ "UserOnlineFromDevice": "{0}이 {1}으로 접속",
"UserPasswordChangedWithName": "사용자 {0}의 비밀번호가 변경되었습니다",
"UserPolicyUpdatedWithName": "{0}의 사용자 정책이 업데이트되었습니다",
"UserStartedPlayingItemWithValues": "{2}에서 {0}이 {1} 재생 중",
diff --git a/Emby.Server.Implementations/Localization/Core/nb.json b/Emby.Server.Implementations/Localization/Core/nb.json
index a97c2e17a..07a599121 100644
--- a/Emby.Server.Implementations/Localization/Core/nb.json
+++ b/Emby.Server.Implementations/Localization/Core/nb.json
@@ -50,7 +50,7 @@
"NotificationOptionAudioPlayback": "Lydavspilling startet",
"NotificationOptionAudioPlaybackStopped": "Lydavspilling stoppet",
"NotificationOptionCameraImageUploaded": "Kamerabilde lastet opp",
- "NotificationOptionInstallationFailed": "Installasjonsfeil",
+ "NotificationOptionInstallationFailed": "Installasjonen feilet",
"NotificationOptionNewLibraryContent": "Nytt innhold lagt til",
"NotificationOptionPluginError": "Pluginfeil",
"NotificationOptionPluginInstalled": "Plugin installert",
diff --git a/Emby.Server.Implementations/Localization/Core/sq.json b/Emby.Server.Implementations/Localization/Core/sq.json
new file mode 100644
index 000000000..347ba5f97
--- /dev/null
+++ b/Emby.Server.Implementations/Localization/Core/sq.json
@@ -0,0 +1,117 @@
+{
+ "MessageApplicationUpdatedTo": "Serveri Jellyfin u përditesua në versionin {0}",
+ "Inherit": "Trashgimi",
+ "TaskDownloadMissingSubtitlesDescription": "Kërkon në internet për titra që mungojnë bazuar tek konfigurimi i metadata-ve.",
+ "TaskDownloadMissingSubtitles": "Shkarko titra që mungojnë",
+ "TaskRefreshChannelsDescription": "Rifreskon informacionin e kanaleve të internetit.",
+ "TaskRefreshChannels": "Rifresko Kanalet",
+ "TaskCleanTranscodeDescription": "Fshin skedarët e transkodimit që janë më të vjetër se një ditë.",
+ "TaskCleanTranscode": "Fshi dosjen e transkodimit",
+ "TaskUpdatePluginsDescription": "Shkarkon dhe instalon përditësimi për plugin që janë konfiguruar të përditësohen automatikisht.",
+ "TaskUpdatePlugins": "Përditëso Plugin",
+ "TaskRefreshPeopleDescription": "Përditëson metadata të aktorëve dhe regjizorëve në librarinë tuaj.",
+ "TaskRefreshPeople": "Rifresko aktorët",
+ "TaskCleanLogsDescription": "Fshin skëdarët log që janë më të vjetër se {0} ditë.",
+ "TaskCleanLogs": "Fshi dosjen Log",
+ "TaskRefreshLibraryDescription": "Skanon librarinë media për skedarë të rinj dhe rifreskon metadata.",
+ "TaskRefreshLibrary": "Skano librarinë media",
+ "TaskRefreshChapterImagesDescription": "Krijon imazh për videot që kanë kapituj.",
+ "TaskRefreshChapterImages": "Ekstrakto Imazhet e Kapitullit",
+ "TaskCleanCacheDescription": "Fshi skedarët e cache-s që nuk i duhen më sistemit.",
+ "TaskCleanCache": "Pastro memorjen cache",
+ "TasksChannelsCategory": "Kanalet nga interneti",
+ "TasksApplicationCategory": "Aplikacioni",
+ "TasksLibraryCategory": "Libraria",
+ "TasksMaintenanceCategory": "Mirëmbajtje",
+ "VersionNumber": "Versioni {0}",
+ "ValueSpecialEpisodeName": "Speciale - {0}",
+ "ValueHasBeenAddedToLibrary": "{0} u shtua tek libraria juaj",
+ "UserStoppedPlayingItemWithValues": "{0} mbaroi së shikuari {1} tek {2}",
+ "UserStartedPlayingItemWithValues": "{0} po shikon {1} tek {2}",
+ "UserPolicyUpdatedWithName": "Politika e përdoruesit u përditësua për {0}",
+ "UserPasswordChangedWithName": "Fjalëkalimi u ndryshua për përdoruesin {0}",
+ "UserOnlineFromDevice": "{0} është në linjë nga {1}",
+ "UserOfflineFromDevice": "{0} u shkëput nga {1}",
+ "UserLockedOutWithName": "Përdoruesi {0} u përjashtua",
+ "UserDownloadingItemWithValues": "{0} po shkarkon {1}",
+ "UserDeletedWithName": "Përdoruesi {0} u fshi",
+ "UserCreatedWithName": "Përdoruesi {0} u krijua",
+ "User": "Përdoruesi",
+ "TvShows": "Seriale TV",
+ "System": "Sistemi",
+ "Sync": "Sinkronizo",
+ "SubtitleDownloadFailureFromForItem": "Titrat deshtuan të shkarkohen nga {0} për {1}",
+ "StartupEmbyServerIsLoading": "Serveri Jellyfin po ngarkohet. Ju lutemi provoni përseri pas pak.",
+ "Songs": "Këngë",
+ "Shows": "Seriale",
+ "ServerNameNeedsToBeRestarted": "{0} duhet të ristartoj",
+ "ScheduledTaskStartedWithName": "{0} filloi",
+ "ScheduledTaskFailedWithName": "{0} dështoi",
+ "ProviderValue": "Ofruesi: {0}",
+ "PluginUpdatedWithName": "{0} u përditësua",
+ "PluginUninstalledWithName": "{0} u çinstalua",
+ "PluginInstalledWithName": "{0} u instalua",
+ "Plugin": "Plugin",
+ "Playlists": "Listat për luajtje",
+ "Photos": "Fotografitë",
+ "NotificationOptionVideoPlaybackStopped": "Luajtja e videos ndaloi",
+ "NotificationOptionVideoPlayback": "Luajtja e videos filloi",
+ "NotificationOptionUserLockedOut": "Përdoruesi u përjashtua",
+ "NotificationOptionTaskFailed": "Ushtrimi i planifikuar dështoi",
+ "NotificationOptionServerRestartRequired": "Kërkohet ristartim i serverit",
+ "NotificationOptionPluginUpdateInstalled": "Përditësimi i plugin u instalua",
+ "NotificationOptionPluginUninstalled": "Plugin u çinstalua",
+ "NotificationOptionPluginInstalled": "Plugin u instalua",
+ "NotificationOptionPluginError": "Plugin dështoi",
+ "NotificationOptionNewLibraryContent": "Një përmbajtje e re u shtua",
+ "NotificationOptionInstallationFailed": "Instalimi dështoi",
+ "NotificationOptionCameraImageUploaded": "Fotoja nga kamera u ngarkua",
+ "NotificationOptionAudioPlaybackStopped": "Luajtja e audios ndaloi",
+ "NotificationOptionAudioPlayback": "Luajtja e audios filloi",
+ "NotificationOptionApplicationUpdateInstalled": "Përditësimi i aplikacionit u instalua",
+ "NotificationOptionApplicationUpdateAvailable": "Një perditësim i aplikacionit është gati",
+ "NewVersionIsAvailable": "Një version i ri i Jellyfin është gati për tu shkarkuar.",
+ "NameSeasonUnknown": "Sezon i panjohur",
+ "NameSeasonNumber": "Sezoni {0}",
+ "NameInstallFailed": "Instalimi i {0} dështoi",
+ "MusicVideos": "Video muzikore",
+ "Music": "Muzikë",
+ "Movies": "Filma",
+ "MixedContent": "Përmbajtje e përzier",
+ "MessageServerConfigurationUpdated": "Konfigurimet e serverit u përditësuan",
+ "MessageNamedServerConfigurationUpdatedWithValue": "Seksioni i konfigurimit të serverit {0} u përditësua",
+ "MessageApplicationUpdated": "Serveri Jellyfin u përditësua",
+ "Latest": "Të fundit",
+ "LabelRunningTimeValue": "Kohëzgjatja: {0}",
+ "LabelIpAddressValue": "Adresa IP: {0}",
+ "ItemRemovedWithName": "{0} u fshi nga libraria",
+ "ItemAddedWithName": "{0} u shtua tek libraria",
+ "HomeVideos": "Video personale",
+ "HeaderRecordingGroups": "Grupet e regjistrimit",
+ "HeaderNextUp": "Në vazhdim",
+ "HeaderLiveTV": "TV Live",
+ "HeaderFavoriteSongs": "Kënget e preferuara",
+ "HeaderFavoriteShows": "Serialet e preferuar",
+ "HeaderFavoriteEpisodes": "Episodet e preferuar",
+ "HeaderFavoriteArtists": "Artistët e preferuar",
+ "HeaderFavoriteAlbums": "Albumet e preferuar",
+ "HeaderContinueWatching": "Vazhdo të shikosh",
+ "HeaderCameraUploads": "Ngarkimet nga Kamera",
+ "HeaderAlbumArtists": "Artistët e albumeve",
+ "Genres": "Zhanre",
+ "Folders": "Dosje",
+ "Favorites": "Të preferuara",
+ "FailedLoginAttemptWithUserName": "Përpjekja për hyrje dështoi nga {0}",
+ "DeviceOnlineWithName": "{0} u lidh",
+ "DeviceOfflineWithName": "{0} u shkëput",
+ "Collections": "Koleksione",
+ "ChapterNameValue": "Kapituj",
+ "Channels": "Kanale",
+ "CameraImageUploadedFrom": "Një foto e re nga kamera u ngarkua nga {0}",
+ "Books": "Libra",
+ "AuthenticationSucceededWithUserName": "{0} u identifikua me sukses",
+ "Artists": "Artistë",
+ "Application": "Aplikacioni",
+ "AppDeviceValues": "Aplikacioni: {0}, Pajisja: {1}",
+ "Albums": "Albumet"
+}
diff --git a/Emby.Server.Implementations/Localization/Core/ta.json b/Emby.Server.Implementations/Localization/Core/ta.json
index ed6877f7d..810b1b9ab 100644
--- a/Emby.Server.Implementations/Localization/Core/ta.json
+++ b/Emby.Server.Implementations/Localization/Core/ta.json
@@ -26,7 +26,7 @@
"DeviceOnlineWithName": "{0} இணைக்கப்பட்டது",
"DeviceOfflineWithName": "{0} துண்டிக்கப்பட்டது",
"Collections": "தொகுப்புகள்",
- "CameraImageUploadedFrom": "{0} இலிருந்து புதிய புகைப்படம் பதிவேற்றப்பட்டது",
+ "CameraImageUploadedFrom": "{0} இல் இருந்து புதிய புகைப்படம் பதிவேற்றப்பட்டது",
"AppDeviceValues": "செயலி: {0}, சாதனம்: {1}",
"TaskDownloadMissingSubtitles": "விடுபட்டுபோன வசன வரிகளைப் பதிவிறக்கு",
"TaskRefreshChannels": "சேனல்களை புதுப்பி",
diff --git a/Emby.Server.Implementations/Localization/Core/th.json b/Emby.Server.Implementations/Localization/Core/th.json
index 3f6f3b23c..3b77215a3 100644
--- a/Emby.Server.Implementations/Localization/Core/th.json
+++ b/Emby.Server.Implementations/Localization/Core/th.json
@@ -20,8 +20,8 @@
"NotificationOptionCameraImageUploaded": "อัปโหลดภาพถ่ายแล้ว",
"NotificationOptionAudioPlaybackStopped": "หยุดเล่นเสียง",
"NotificationOptionAudioPlayback": "เริ่มเล่นเสียง",
- "NotificationOptionApplicationUpdateInstalled": "ติดตั้งการอัปเดตแอพพลิเคชันแล้ว",
- "NotificationOptionApplicationUpdateAvailable": "มีการอัปเดตแอพพลิเคชัน",
+ "NotificationOptionApplicationUpdateInstalled": "ติดตั้งการอัปเดตแอปพลิเคชันแล้ว",
+ "NotificationOptionApplicationUpdateAvailable": "มีการอัปเดตแอปพลิเคชัน",
"NewVersionIsAvailable": "เวอร์ชันใหม่ของเซิร์ฟเวอร์ Jellyfin พร้อมให้ดาวน์โหลดแล้ว",
"NameSeasonUnknown": "ไม่ทราบซีซัน",
"NameSeasonNumber": "ซีซัน {0}",
@@ -65,8 +65,8 @@
"Books": "หนังสือ",
"AuthenticationSucceededWithUserName": "{0} ยืนยันตัวสำเร็จแล้ว",
"Artists": "ศิลปิน",
- "Application": "แอพพลิเคชัน",
- "AppDeviceValues": "แอพ: {0}, อุปกรณ์: {1}",
+ "Application": "แอปพลิเคชัน",
+ "AppDeviceValues": "แอป: {0}, อุปกรณ์: {1}",
"Albums": "อัลบั้ม",
"ScheduledTaskStartedWithName": "{0} เริ่มต้น",
"ScheduledTaskFailedWithName": "{0} ล้มเหลว",
@@ -92,7 +92,7 @@
"TaskCleanCacheDescription": "ลบไฟล์แคชที่ระบบไม่ต้องการ",
"TaskCleanCache": "ล้างไดเรกทอรีแคช",
"TasksChannelsCategory": "ช่องอินเทอร์เน็ต",
- "TasksApplicationCategory": "แอพพลิเคชัน",
+ "TasksApplicationCategory": "แอปพลิเคชัน",
"TasksLibraryCategory": "ไลบรารี",
"TasksMaintenanceCategory": "ปิดซ่อมบำรุง",
"VersionNumber": "เวอร์ชัน {0}",
diff --git a/Emby.Server.Implementations/Localization/Core/vi.json b/Emby.Server.Implementations/Localization/Core/vi.json
new file mode 100644
index 000000000..2392c8347
--- /dev/null
+++ b/Emby.Server.Implementations/Localization/Core/vi.json
@@ -0,0 +1,117 @@
+{
+ "Collections": "Bộ Sưu Tập",
+ "Favorites": "Yêu Thích",
+ "Folders": "Thư Mục",
+ "Genres": "Thể Loại",
+ "HeaderAlbumArtists": "Bộ Sưu Tập Nghệ sĩ",
+ "HeaderContinueWatching": "Xem Tiếp",
+ "HeaderLiveTV": "TV Trực Tiếp",
+ "Movies": "Phim",
+ "Photos": "Ảnh",
+ "Playlists": "Danh sách phát",
+ "Shows": "Chương Trình TV",
+ "Songs": "Các Bài Hát",
+ "Sync": "Đồng Bộ",
+ "ValueSpecialEpisodeName": "Đặc Biệt - {0}",
+ "Albums": "Albums",
+ "Artists": "Các Nghệ 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",
+ "TaskRefreshChannelsDescription": "Làm mới thông tin kênh internet.",
+ "TaskRefreshChannels": "Làm Mới Kênh",
+ "TaskCleanTranscodeDescription": "Xóa các tệp chuyển mã cũ hơn một ngày.",
+ "TaskCleanTranscode": "Làm Sạch Thư Mục Chuyển Mã",
+ "TaskUpdatePluginsDescription": "Tải xuống và cài đặt các bản cập nhật cho các plugin được định cấu hình để cập nhật tự động.",
+ "TaskUpdatePlugins": "Cập Nhật Plugins",
+ "TaskRefreshPeopleDescription": "Cập nhật thông tin chi tiết cho diễn viên và đạo diễn trong thư viện phương tiện của bạn.",
+ "TaskRefreshPeople": "Làm mới Người dùng",
+ "TaskCleanLogsDescription": "Xóa tập tin nhật ký cũ hơn {0} ngày.",
+ "TaskCleanLogs": "Làm sạch nhật ký",
+ "TaskRefreshLibraryDescription": "Quét thư viện phương tiện của bạn để tìm các tệp mới và làm mới thông tin chi tiết.",
+ "TaskRefreshLibrary": "Quét Thư viện Phương tiện",
+ "TaskRefreshChapterImagesDescription": "Tạo hình thu nhỏ cho video có các phân cảnh.",
+ "TaskRefreshChapterImages": "Trích Xuất Ảnh Phân Cảnh",
+ "TaskCleanCacheDescription": "Xóa các tệp cache không còn cần thiết của hệ thống.",
+ "TaskCleanCache": "Làm Sạch Thư Mục Cache",
+ "TasksChannelsCategory": "Kênh Internet",
+ "TasksApplicationCategory": "Ứng Dụng",
+ "TasksLibraryCategory": "Thư Viện",
+ "TasksMaintenanceCategory": "Bảo Trì",
+ "VersionNumber": "Phiên Bản {0}",
+ "ValueHasBeenAddedToLibrary": "{0} đã được thêm vào thư viện của bạn",
+ "UserStoppedPlayingItemWithValues": "{0} đã phát xong {1} trên {2}",
+ "UserStartedPlayingItemWithValues": "{0} đang phát {1} trên {2}",
+ "UserPolicyUpdatedWithName": "Chính sách người dùng đã được cập nhật cho {0}",
+ "UserPasswordChangedWithName": "Mật khẩu đã được thay đổi cho người dùng {0}",
+ "UserOnlineFromDevice": "{0} trực tuyến từ {1}",
+ "UserOfflineFromDevice": "{0} đã ngắt kết nối từ {1}",
+ "UserLockedOutWithName": "User {0} đã bị khóa",
+ "UserDownloadingItemWithValues": "{0} đang tải xuống {1}",
+ "UserDeletedWithName": "Người Dùng {0} đã được xóa",
+ "UserCreatedWithName": "Người Dùng {0} đã được tạo",
+ "User": "Người Dùng",
+ "TvShows": "Chương Trình TV",
+ "System": "Hệ Thống",
+ "SubtitleDownloadFailureFromForItem": "Không thể tải xuống phụ đề từ {0} cho {1}",
+ "StartupEmbyServerIsLoading": "Jellyfin Server đang tải. Vui lòng thử lại trong thời gian ngắn.",
+ "ServerNameNeedsToBeRestarted": "{0} cần được khởi động lại",
+ "ScheduledTaskStartedWithName": "{0} đã bắt đầu",
+ "ScheduledTaskFailedWithName": "{0} đã thất bại",
+ "ProviderValue": "Provider: {0}",
+ "PluginUpdatedWithName": "{0} đã cập nhật",
+ "PluginUninstalledWithName": "{0} đã được gỡ bỏ",
+ "PluginInstalledWithName": "{0} đã được cài đặt",
+ "Plugin": "Plugin",
+ "NotificationOptionVideoPlaybackStopped": "Phát lại video đã dừng",
+ "NotificationOptionVideoPlayback": "Đã bắt đầu phát lại video",
+ "NotificationOptionUserLockedOut": "Người dùng bị khóa",
+ "NotificationOptionTaskFailed": "Lỗi tác vụ đã lên lịch",
+ "NotificationOptionServerRestartRequired": "Yêu cầu khởi động lại Server",
+ "NotificationOptionPluginUpdateInstalled": "Cập nhật Plugin đã được cài đặt",
+ "NotificationOptionPluginUninstalled": "Đã gỡ bỏ Plugin",
+ "NotificationOptionPluginInstalled": "Đã cài đặt Plugin",
+ "NotificationOptionPluginError": "Thất bại Plugin",
+ "NotificationOptionNewLibraryContent": "Nội dung mới được thêm vào",
+ "NotificationOptionInstallationFailed": "Cài đặt thất bại",
+ "NotificationOptionCameraImageUploaded": "Đã tải lên hình ảnh máy ảnh",
+ "NotificationOptionAudioPlaybackStopped": "Phát lại âm thanh đã dừng",
+ "NotificationOptionAudioPlayback": "Phát lại âm thanh đã bắt đầu",
+ "NotificationOptionApplicationUpdateInstalled": "Bản cập nhật ứng dụng đã được cài đặt",
+ "NotificationOptionApplicationUpdateAvailable": "Bản cập nhật ứng dụng hiện sẵn có",
+ "NewVersionIsAvailable": "Một phiên bản mới của Jellyfin Server sẵn có để tải.",
+ "NameSeasonUnknown": "Không Rõ Mùa",
+ "NameSeasonNumber": "Mùa {0}",
+ "NameInstallFailed": "{0} cài đặt thất bại",
+ "MusicVideos": "Video Nhạc",
+ "Music": "Nhạc",
+ "MixedContent": "Nội dung hỗn hợp",
+ "MessageServerConfigurationUpdated": "Cấu hình máy chủ đã được cập nhật",
+ "MessageNamedServerConfigurationUpdatedWithValue": "Phần cấu hình máy chủ {0} đã được cập nhật",
+ "MessageApplicationUpdatedTo": "Jellyfin Server đã được cập nhật lên {0}",
+ "MessageApplicationUpdated": "Jellyfin Server đã được cập nhật",
+ "Latest": "Gần Nhất",
+ "LabelRunningTimeValue": "Thời Gian Chạy: {0}",
+ "LabelIpAddressValue": "Địa Chỉ IP: {0}",
+ "ItemRemovedWithName": "{0} đã xóa khỏi thư viện",
+ "ItemAddedWithName": "{0} được thêm vào thư viện",
+ "Inherit": "Thừa hưởng",
+ "HomeVideos": "Video nhà",
+ "HeaderRecordingGroups": "Nhóm Ghi Video",
+ "HeaderNextUp": "Tiếp Theo",
+ "HeaderFavoriteSongs": "Bài Hát Yêu Thích",
+ "HeaderFavoriteShows": "Chương Trình Yêu Thích",
+ "HeaderFavoriteEpisodes": "Tập Phim Yêu Thích",
+ "HeaderFavoriteArtists": "Nghệ Sĩ Yêu Thích",
+ "HeaderFavoriteAlbums": "Album Ưa Thích",
+ "HeaderCameraUploads": "Máy Ảnh Tải Lên",
+ "FailedLoginAttemptWithUserName": "Nỗ lực đăng nhập thất bại từ {0}",
+ "DeviceOnlineWithName": "{0} đã kết nối",
+ "DeviceOfflineWithName": "{0} đã ngắt kết nối",
+ "ChapterNameValue": "Phân Cảnh {0}",
+ "Channels": "Các Kênh",
+ "CameraImageUploadedFrom": "Một hình ảnh máy ảnh mới đã được tải lên từ {0}",
+ "Books": "Sách",
+ "AuthenticationSucceededWithUserName": "{0} xác thực thành công",
+ "Application": "Ứng Dụng",
+ "AppDeviceValues": "Ứng Dụng: {0}, Thiết Bị: {1}"
+}
diff --git a/Emby.Server.Implementations/Localization/Core/zh-TW.json b/Emby.Server.Implementations/Localization/Core/zh-TW.json
index 01108fe84..7b6540c3e 100644
--- a/Emby.Server.Implementations/Localization/Core/zh-TW.json
+++ b/Emby.Server.Implementations/Localization/Core/zh-TW.json
@@ -96,7 +96,7 @@
"TaskDownloadMissingSubtitles": "下載遺失的字幕",
"TaskRefreshChannels": "重新整理頻道",
"TaskUpdatePlugins": "更新外掛",
- "TaskRefreshPeople": "重新整理人員",
+ "TaskRefreshPeople": "刷新用戶",
"TaskCleanLogsDescription": "刪除超過 {0} 天的舊紀錄檔。",
"TaskCleanLogs": "清空紀錄資料夾",
"TaskRefreshLibraryDescription": "重新掃描媒體庫的新檔案並更新描述資料。",
diff --git a/Emby.Server.Implementations/Localization/LocalizationManager.cs b/Emby.Server.Implementations/Localization/LocalizationManager.cs
index 90e2766b8..30aaf3a05 100644
--- a/Emby.Server.Implementations/Localization/LocalizationManager.cs
+++ b/Emby.Server.Implementations/Localization/LocalizationManager.cs
@@ -413,6 +413,7 @@ namespace Emby.Server.Implementations.Localization
yield return new LocalizationOption("Swedish", "sv");
yield return new LocalizationOption("Swiss German", "gsw");
yield return new LocalizationOption("Turkish", "tr");
+ yield return new LocalizationOption("Tiếng Việt", "vi");
}
}
}
diff --git a/Emby.Server.Implementations/Plugins/PluginManifest.cs b/Emby.Server.Implementations/Plugins/PluginManifest.cs
new file mode 100644
index 000000000..33762791b
--- /dev/null
+++ b/Emby.Server.Implementations/Plugins/PluginManifest.cs
@@ -0,0 +1,60 @@
+using System;
+
+namespace Emby.Server.Implementations.Plugins
+{
+ /// <summary>
+ /// Defines a Plugin manifest file.
+ /// </summary>
+ public class PluginManifest
+ {
+ /// <summary>
+ /// Gets or sets the category of the plugin.
+ /// </summary>
+ public string Category { get; set; }
+
+ /// <summary>
+ /// Gets or sets the changelog information.
+ /// </summary>
+ public string Changelog { get; set; }
+
+ /// <summary>
+ /// Gets or sets the description of the plugin.
+ /// </summary>
+ public string Description { get; set; }
+
+ /// <summary>
+ /// Gets or sets the Global Unique Identifier for the plugin.
+ /// </summary>
+ public Guid Guid { get; set; }
+
+ /// <summary>
+ /// Gets or sets the Name of the plugin.
+ /// </summary>
+ public string Name { get; set; }
+
+ /// <summary>
+ /// Gets or sets an overview of the plugin.
+ /// </summary>
+ public string Overview { get; set; }
+
+ /// <summary>
+ /// Gets or sets the owner of the plugin.
+ /// </summary>
+ public string Owner { get; set; }
+
+ /// <summary>
+ /// Gets or sets the compatibility version for the plugin.
+ /// </summary>
+ public string TargetAbi { get; set; }
+
+ /// <summary>
+ /// Gets or sets the timestamp of the plugin.
+ /// </summary>
+ public DateTime Timestamp { get; set; }
+
+ /// <summary>
+ /// Gets or sets the Version number of the plugin.
+ /// </summary>
+ public string Version { get; set; }
+ }
+}
diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs
index ca8e0e29b..e42d47853 100644
--- a/Emby.Server.Implementations/Session/SessionManager.cs
+++ b/Emby.Server.Implementations/Session/SessionManager.cs
@@ -1037,7 +1037,7 @@ namespace Emby.Server.Implementations.Session
var generalCommand = new GeneralCommand
{
- Name = GeneralCommandType.DisplayMessage.ToString()
+ Name = GeneralCommandType.DisplayMessage
};
generalCommand.Arguments["Header"] = command.Header;
@@ -1268,7 +1268,7 @@ namespace Emby.Server.Implementations.Session
{
var generalCommand = new GeneralCommand
{
- Name = GeneralCommandType.DisplayContent.ToString(),
+ Name = GeneralCommandType.DisplayContent,
Arguments =
{
["ItemId"] = command.ItemId,
diff --git a/Emby.Server.Implementations/Updates/InstallationManager.cs b/Emby.Server.Implementations/Updates/InstallationManager.cs
index e19158f8e..8e24bf55c 100644
--- a/Emby.Server.Implementations/Updates/InstallationManager.cs
+++ b/Emby.Server.Implementations/Updates/InstallationManager.cs
@@ -15,12 +15,14 @@ using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Net;
using MediaBrowser.Common.Plugins;
using MediaBrowser.Common.Updates;
+using MediaBrowser.Common.System;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Net;
using MediaBrowser.Model.Serialization;
using MediaBrowser.Model.Updates;
using Microsoft.Extensions.Logging;
+using MediaBrowser.Model.System;
namespace Emby.Server.Implementations.Updates
{
@@ -377,11 +379,20 @@ namespace Emby.Server.Implementations.Updates
throw new InvalidDataException("The checksum of the received data doesn't match.");
}
+ // Version folder as they cannot be overwritten in Windows.
+ targetDir += "_" + package.Version;
+
if (Directory.Exists(targetDir))
{
- Directory.Delete(targetDir, true);
+ try
+ {
+ Directory.Delete(targetDir, true);
+ }
+ catch
+ {
+ // Ignore any exceptions.
+ }
}
-
stream.Position = 0;
_zipClient.ExtractAllFromZip(stream, targetDir, true);
@@ -423,15 +434,22 @@ namespace Emby.Server.Implementations.Updates
path = file;
}
- if (isDirectory)
+ try
{
- _logger.LogInformation("Deleting plugin directory {0}", path);
- Directory.Delete(path, true);
+ if (isDirectory)
+ {
+ _logger.LogInformation("Deleting plugin directory {0}", path);
+ Directory.Delete(path, true);
+ }
+ else
+ {
+ _logger.LogInformation("Deleting plugin file {0}", path);
+ _fileSystem.DeleteFile(path);
+ }
}
- else
+ catch
{
- _logger.LogInformation("Deleting plugin file {0}", path);
- _fileSystem.DeleteFile(path);
+ // Ignore file errors.
}
var list = _config.Configuration.UninstalledPlugins.ToList();
diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs
index 670b41611..1153a601e 100644
--- a/Jellyfin.Api/Controllers/DynamicHlsController.cs
+++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Diagnostics.CodeAnalysis;
@@ -113,7 +113,6 @@ namespace Jellyfin.Api.Controllers
/// Gets a video hls playlist stream.
/// </summary>
/// <param name="itemId">The item id.</param>
- /// <param name="container">The video container. Possible values are: ts, webm, asf, wmv, ogv, mp4, m4v, mkv, mpeg, mpg, avi, 3gp, wmv, wtv, m2ts, mov, iso, flv. </param>
/// <param name="static">Optional. If true, the original file will be streamed statically without any encoding. Use either no url extension or the original file extension. true/false.</param>
/// <param name="params">The streaming parameters.</param>
/// <param name="tag">The tag.</param>
@@ -170,7 +169,6 @@ namespace Jellyfin.Api.Controllers
[ProducesPlaylistFile]
public async Task<ActionResult> GetMasterHlsVideoPlaylist(
[FromRoute, Required] Guid itemId,
- [FromRoute, Required] string container,
[FromQuery] bool? @static,
[FromQuery] string? @params,
[FromQuery] string? tag,
@@ -223,7 +221,6 @@ namespace Jellyfin.Api.Controllers
var streamingRequest = new HlsVideoRequestDto
{
Id = itemId,
- Container = container,
Static = @static ?? true,
Params = @params,
Tag = tag,
@@ -281,7 +278,6 @@ namespace Jellyfin.Api.Controllers
/// Gets an audio hls playlist stream.
/// </summary>
/// <param name="itemId">The item id.</param>
- /// <param name="container">The video container. Possible values are: ts, webm, asf, wmv, ogv, mp4, m4v, mkv, mpeg, mpg, avi, 3gp, wmv, wtv, m2ts, mov, iso, flv. </param>
/// <param name="static">Optional. If true, the original file will be streamed statically without any encoding. Use either no url extension or the original file extension. true/false.</param>
/// <param name="params">The streaming parameters.</param>
/// <param name="tag">The tag.</param>
@@ -338,7 +334,6 @@ namespace Jellyfin.Api.Controllers
[ProducesPlaylistFile]
public async Task<ActionResult> GetMasterHlsAudioPlaylist(
[FromRoute, Required] Guid itemId,
- [FromQuery, Required] string container,
[FromQuery] bool? @static,
[FromQuery] string? @params,
[FromQuery] string? tag,
@@ -391,7 +386,6 @@ namespace Jellyfin.Api.Controllers
var streamingRequest = new HlsAudioRequestDto
{
Id = itemId,
- Container = container,
Static = @static ?? true,
Params = @params,
Tag = tag,
@@ -449,7 +443,6 @@ namespace Jellyfin.Api.Controllers
/// Gets a video stream using HTTP live streaming.
/// </summary>
/// <param name="itemId">The item id.</param>
- /// <param name="container">The video container. Possible values are: ts, webm, asf, wmv, ogv, mp4, m4v, mkv, mpeg, mpg, avi, 3gp, wmv, wtv, m2ts, mov, iso, flv. </param>
/// <param name="static">Optional. If true, the original file will be streamed statically without any encoding. Use either no url extension or the original file extension. true/false.</param>
/// <param name="params">The streaming parameters.</param>
/// <param name="tag">The tag.</param>
@@ -504,7 +497,6 @@ namespace Jellyfin.Api.Controllers
[ProducesPlaylistFile]
public async Task<ActionResult> GetVariantHlsVideoPlaylist(
[FromRoute, Required] Guid itemId,
- [FromQuery, Required] string container,
[FromQuery] bool? @static,
[FromQuery] string? @params,
[FromQuery] string? tag,
@@ -557,7 +549,6 @@ namespace Jellyfin.Api.Controllers
var streamingRequest = new VideoRequestDto
{
Id = itemId,
- Container = container,
Static = @static ?? true,
Params = @params,
Tag = tag,
@@ -615,7 +606,6 @@ namespace Jellyfin.Api.Controllers
/// Gets an audio stream using HTTP live streaming.
/// </summary>
/// <param name="itemId">The item id.</param>
- /// <param name="container">The video container. Possible values are: ts, webm, asf, wmv, ogv, mp4, m4v, mkv, mpeg, mpg, avi, 3gp, wmv, wtv, m2ts, mov, iso, flv. </param>
/// <param name="static">Optional. If true, the original file will be streamed statically without any encoding. Use either no url extension or the original file extension. true/false.</param>
/// <param name="params">The streaming parameters.</param>
/// <param name="tag">The tag.</param>
@@ -670,7 +660,6 @@ namespace Jellyfin.Api.Controllers
[ProducesPlaylistFile]
public async Task<ActionResult> GetVariantHlsAudioPlaylist(
[FromRoute, Required] Guid itemId,
- [FromQuery, Required] string container,
[FromQuery] bool? @static,
[FromQuery] string? @params,
[FromQuery] string? tag,
@@ -723,7 +712,6 @@ namespace Jellyfin.Api.Controllers
var streamingRequest = new StreamingRequestDto
{
Id = itemId,
- Container = container,
Static = @static ?? true,
Params = @params,
Tag = tag,
@@ -841,7 +829,7 @@ namespace Jellyfin.Api.Controllers
[FromRoute, Required] Guid itemId,
[FromRoute, Required] string playlistId,
[FromRoute, Required] int segmentId,
- [FromRoute, Required] string container,
+ [FromRoute] string container,
[FromQuery] bool? @static,
[FromQuery] string? @params,
[FromQuery] string? tag,
@@ -1011,7 +999,7 @@ namespace Jellyfin.Api.Controllers
[FromRoute, Required] Guid itemId,
[FromRoute, Required] string playlistId,
[FromRoute, Required] int segmentId,
- [FromRoute, Required] string container,
+ [FromRoute] string container,
[FromQuery] bool? @static,
[FromQuery] string? @params,
[FromQuery] string? tag,
@@ -1144,30 +1132,30 @@ namespace Jellyfin.Api.Controllers
var builder = new StringBuilder();
- builder.AppendLine("#EXTM3U");
- builder.AppendLine("#EXT-X-PLAYLIST-TYPE:VOD");
- builder.AppendLine("#EXT-X-VERSION:3");
- builder.AppendLine("#EXT-X-TARGETDURATION:" + Math.Ceiling(segmentLengths.Length > 0 ? segmentLengths.Max() : state.SegmentLength).ToString(CultureInfo.InvariantCulture));
- builder.AppendLine("#EXT-X-MEDIA-SEQUENCE:0");
+ builder.AppendLine("#EXTM3U")
+ .AppendLine("#EXT-X-PLAYLIST-TYPE:VOD")
+ .AppendLine("#EXT-X-VERSION:3")
+ .Append("#EXT-X-TARGETDURATION:")
+ .Append(Math.Ceiling(segmentLengths.Length > 0 ? segmentLengths.Max() : state.SegmentLength))
+ .AppendLine()
+ .AppendLine("#EXT-X-MEDIA-SEQUENCE:0");
- var queryString = Request.QueryString;
var index = 0;
-
var segmentExtension = GetSegmentFileExtension(streamingRequest.SegmentContainer);
+ var queryString = Request.QueryString;
foreach (var length in segmentLengths)
{
- builder.AppendLine("#EXTINF:" + length.ToString("0.0000", CultureInfo.InvariantCulture) + ", nodesc");
- builder.AppendLine(
- string.Format(
- CultureInfo.InvariantCulture,
- "hls1/{0}/{1}{2}{3}",
- name,
- index.ToString(CultureInfo.InvariantCulture),
- segmentExtension,
- queryString));
-
- index++;
+ builder.Append("#EXTINF:")
+ .Append(length.ToString("0.0000", CultureInfo.InvariantCulture))
+ .AppendLine(", nodesc")
+ .Append("hls1/")
+ .Append(name)
+ .Append('/')
+ .Append(index++)
+ .Append(segmentExtension)
+ .Append(queryString)
+ .AppendLine();
}
builder.AppendLine("#EXT-X-ENDLIST");
@@ -1465,7 +1453,7 @@ namespace Jellyfin.Api.Controllers
var args = "-codec:v:0 " + codec;
- // if (state.EnableMpegtsM2TsMode)
+ // if (state.EnableMpegtsM2TsMode)
// {
// args += " -mpegts_m2ts_mode 1";
// }
diff --git a/Jellyfin.Api/Controllers/ItemLookupController.cs b/Jellyfin.Api/Controllers/ItemLookupController.cs
index cf7038650..ab73aa428 100644
--- a/Jellyfin.Api/Controllers/ItemLookupController.cs
+++ b/Jellyfin.Api/Controllers/ItemLookupController.cs
@@ -292,7 +292,7 @@ namespace Jellyfin.Api.Controllers
/// A <see cref="Task" /> that represents the asynchronous operation to get the remote search results.
/// The task result contains an <see cref="NoContentResult"/>.
/// </returns>
- [HttpPost("Items/RemoteSearch/Apply/{id}")]
+ [HttpPost("Items/RemoteSearch/Apply/{itemId}")]
[Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task<ActionResult> ApplySearchCriteria(
diff --git a/Jellyfin.Api/Controllers/LiveTvController.cs b/Jellyfin.Api/Controllers/LiveTvController.cs
index 32ebfbd98..3557e6304 100644
--- a/Jellyfin.Api/Controllers/LiveTvController.cs
+++ b/Jellyfin.Api/Controllers/LiveTvController.cs
@@ -1017,9 +1017,9 @@ namespace Jellyfin.Api.Controllers
[FromQuery] bool validateListings = false,
[FromQuery] bool validateLogin = false)
{
- using var sha = SHA1.Create();
if (!string.IsNullOrEmpty(pw))
{
+ using var sha = SHA1.Create();
listingsProviderInfo.Password = Hex.Encode(sha.ComputeHash(Encoding.UTF8.GetBytes(pw)));
}
diff --git a/Jellyfin.Api/Controllers/SessionController.cs b/Jellyfin.Api/Controllers/SessionController.cs
index b00675d67..39bf6e6dc 100644
--- a/Jellyfin.Api/Controllers/SessionController.cs
+++ b/Jellyfin.Api/Controllers/SessionController.cs
@@ -1,5 +1,3 @@
-#pragma warning disable CA1801
-
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
@@ -150,25 +148,25 @@ namespace Jellyfin.Api.Controllers
/// Instructs a session to play an item.
/// </summary>
/// <param name="sessionId">The session id.</param>
- /// <param name="command">The type of play command to issue (PlayNow, PlayNext, PlayLast). Clients who have not yet implemented play next and play last may play now.</param>
+ /// <param name="playCommand">The type of play command to issue (PlayNow, PlayNext, PlayLast). Clients who have not yet implemented play next and play last may play now.</param>
/// <param name="itemIds">The ids of the items to play, comma delimited.</param>
/// <param name="startPositionTicks">The starting position of the first item.</param>
/// <response code="204">Instruction sent to session.</response>
/// <returns>A <see cref="NoContentResult"/>.</returns>
- [HttpPost("Sessions/{sessionId}/Playing/{command}")]
+ [HttpPost("Sessions/{sessionId}/Playing")]
[Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult Play(
[FromRoute, Required] string sessionId,
- [FromRoute, Required] PlayCommand command,
- [FromQuery] Guid[] itemIds,
+ [FromQuery, Required] PlayCommand playCommand,
+ [FromQuery, Required] string itemIds,
[FromQuery] long? startPositionTicks)
{
var playRequest = new PlayRequest
{
- ItemIds = itemIds,
+ ItemIds = RequestHelpers.GetGuids(itemIds),
StartPositionTicks = startPositionTicks,
- PlayCommand = command
+ PlayCommand = playCommand
};
_sessionManager.SendPlayCommand(
@@ -184,20 +182,29 @@ namespace Jellyfin.Api.Controllers
/// Issues a playstate command to a client.
/// </summary>
/// <param name="sessionId">The session id.</param>
- /// <param name="playstateRequest">The <see cref="PlaystateRequest"/>.</param>
+ /// <param name="command">The <see cref="PlaystateCommand"/>.</param>
+ /// <param name="seekPositionTicks">The optional position ticks.</param>
+ /// <param name="controllingUserId">The optional controlling user id.</param>
/// <response code="204">Playstate command sent to session.</response>
/// <returns>A <see cref="NoContentResult"/>.</returns>
- [HttpPost("Sessions/{sessionId}/Playing")]
+ [HttpPost("Sessions/{sessionId}/Playing/{command}")]
[Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult SendPlaystateCommand(
[FromRoute, Required] string sessionId,
- [FromBody] PlaystateRequest playstateRequest)
+ [FromRoute, Required] PlaystateCommand command,
+ [FromQuery] long? seekPositionTicks,
+ [FromQuery] string? controllingUserId)
{
_sessionManager.SendPlaystateCommand(
RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id,
sessionId,
- playstateRequest,
+ new PlaystateRequest()
+ {
+ Command = command,
+ ControllingUserId = controllingUserId,
+ SeekPositionTicks = seekPositionTicks,
+ },
CancellationToken.None);
return NoContent();
@@ -215,18 +222,12 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult SendSystemCommand(
[FromRoute, Required] string sessionId,
- [FromRoute, Required] string command)
+ [FromRoute, Required] GeneralCommandType command)
{
- var name = command;
- if (Enum.TryParse(name, true, out GeneralCommandType commandType))
- {
- name = commandType.ToString();
- }
-
var currentSession = RequestHelpers.GetSession(_sessionManager, _authContext, Request);
var generalCommand = new GeneralCommand
{
- Name = name,
+ Name = command,
ControllingUserId = currentSession.UserId
};
@@ -247,7 +248,7 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult SendGeneralCommand(
[FromRoute, Required] string sessionId,
- [FromRoute, Required] string command)
+ [FromRoute, Required] GeneralCommandType command)
{
var currentSession = RequestHelpers.GetSession(_sessionManager, _authContext, Request);
@@ -434,9 +435,9 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult ReportViewing(
[FromQuery] string? sessionId,
- [FromQuery] string? itemId)
+ [FromQuery, Required] string? itemId)
{
- string session = RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id;
+ string session = sessionId ?? RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id;
_sessionManager.ReportNowViewingItem(session, itemId);
return NoContent();
diff --git a/Jellyfin.Api/Controllers/SubtitleController.cs b/Jellyfin.Api/Controllers/SubtitleController.cs
index 78c9d4398..cc682ed54 100644
--- a/Jellyfin.Api/Controllers/SubtitleController.cs
+++ b/Jellyfin.Api/Controllers/SubtitleController.cs
@@ -281,7 +281,8 @@ namespace Jellyfin.Api.Controllers
var builder = new StringBuilder();
builder.AppendLine("#EXTM3U")
.Append("#EXT-X-TARGETDURATION:")
- .AppendLine(segmentLength.ToString(CultureInfo.InvariantCulture))
+ .Append(segmentLength)
+ .AppendLine()
.AppendLine("#EXT-X-VERSION:3")
.AppendLine("#EXT-X-MEDIA-SEQUENCE:0")
.AppendLine("#EXT-X-PLAYLIST-TYPE:VOD");
@@ -296,8 +297,9 @@ namespace Jellyfin.Api.Controllers
var lengthTicks = Math.Min(remaining, segmentLengthTicks);
builder.Append("#EXTINF:")
- .Append(TimeSpan.FromTicks(lengthTicks).TotalSeconds.ToString(CultureInfo.InvariantCulture))
- .AppendLine(",");
+ .Append(TimeSpan.FromTicks(lengthTicks).TotalSeconds)
+ .Append(',')
+ .AppendLine();
var endPositionTicks = Math.Min(runtime, positionTicks + segmentLengthTicks);
diff --git a/Jellyfin.Api/Controllers/UserController.cs b/Jellyfin.Api/Controllers/UserController.cs
index 630e9df6a..50bb8bb2a 100644
--- a/Jellyfin.Api/Controllers/UserController.cs
+++ b/Jellyfin.Api/Controllers/UserController.cs
@@ -505,17 +505,17 @@ namespace Jellyfin.Api.Controllers
/// <summary>
/// Initiates the forgot password process for a local user.
/// </summary>
- /// <param name="enteredUsername">The entered username.</param>
+ /// <param name="forgotPasswordRequest">The forgot password request containing the entered username.</param>
/// <response code="200">Password reset process started.</response>
/// <returns>A <see cref="Task"/> containing a <see cref="ForgotPasswordResult"/>.</returns>
[HttpPost("ForgotPassword")]
[ProducesResponseType(StatusCodes.Status200OK)]
- public async Task<ActionResult<ForgotPasswordResult>> ForgotPassword([FromBody] string? enteredUsername)
+ public async Task<ActionResult<ForgotPasswordResult>> ForgotPassword([FromBody, Required] ForgotPasswordDto forgotPasswordRequest)
{
var isLocal = HttpContext.IsLocal()
|| _networkManager.IsInLocalNetwork(HttpContext.GetNormalizedRemoteIp());
- var result = await _userManager.StartForgotPasswordProcess(enteredUsername, isLocal).ConfigureAwait(false);
+ var result = await _userManager.StartForgotPasswordProcess(forgotPasswordRequest.EnteredUsername, isLocal).ConfigureAwait(false);
return result;
}
diff --git a/Jellyfin.Api/Controllers/VideosController.cs b/Jellyfin.Api/Controllers/VideosController.cs
index cce4cfbe3..4de7aac71 100644
--- a/Jellyfin.Api/Controllers/VideosController.cs
+++ b/Jellyfin.Api/Controllers/VideosController.cs
@@ -326,9 +326,9 @@ namespace Jellyfin.Api.Controllers
/// <param name="streamOptions">Optional. The streaming options.</param>
/// <response code="200">Video stream returned.</response>
/// <returns>A <see cref="FileResult"/> containing the audio file.</returns>
- [HttpGet("{itemId}/{stream=stream}.{container?}", Name = "GetVideoStream_2")]
+ [HttpGet("{itemId}/{stream=stream}.{container?}", Name = "GetVideoStreamWithExt")]
[HttpGet("{itemId}/stream")]
- [HttpHead("{itemId}/{stream=stream}.{container?}", Name = "HeadVideoStream_2")]
+ [HttpHead("{itemId}/{stream=stream}.{container?}", Name = "HeadVideoStreamWithExt")]
[HttpHead("{itemId}/stream", Name = "HeadVideoStream")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesVideoFile]
diff --git a/Jellyfin.Api/Helpers/StreamingHelpers.cs b/Jellyfin.Api/Helpers/StreamingHelpers.cs
index 89ab2da62..f4ec29bde 100644
--- a/Jellyfin.Api/Helpers/StreamingHelpers.cs
+++ b/Jellyfin.Api/Helpers/StreamingHelpers.cs
@@ -169,7 +169,7 @@ namespace Jellyfin.Api.Helpers
string? containerInternal = Path.GetExtension(state.RequestedUrl);
- if (string.IsNullOrEmpty(streamingRequest.Container))
+ if (!string.IsNullOrEmpty(streamingRequest.Container))
{
containerInternal = streamingRequest.Container;
}
diff --git a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs
index 67e450372..64d1227f7 100644
--- a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs
+++ b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs
@@ -504,6 +504,11 @@ namespace Jellyfin.Api.Helpers
}
}
+ if (string.IsNullOrEmpty(_mediaEncoder.EncoderPath))
+ {
+ throw new ArgumentException("FFMPEG path not set.");
+ }
+
var process = new Process
{
StartInfo = new ProcessStartInfo
diff --git a/Jellyfin.Api/Jellyfin.Api.csproj b/Jellyfin.Api/Jellyfin.Api.csproj
index ca0542b03..c27dce8dd 100644
--- a/Jellyfin.Api/Jellyfin.Api.csproj
+++ b/Jellyfin.Api/Jellyfin.Api.csproj
@@ -14,9 +14,9 @@
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Authentication" Version="2.2.0" />
- <PackageReference Include="Microsoft.AspNetCore.Authorization" Version="3.1.7" />
+ <PackageReference Include="Microsoft.AspNetCore.Authorization" Version="3.1.8" />
<PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.2.0" />
- <PackageReference Include="Microsoft.Extensions.Http" Version="3.1.7" />
+ <PackageReference Include="Microsoft.Extensions.Http" Version="3.1.8" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="5.5.1" />
<PackageReference Include="Swashbuckle.AspNetCore.ReDoc" Version="5.5.1" />
</ItemGroup>
diff --git a/Jellyfin.Api/Models/UserDtos/ForgotPasswordDto.cs b/Jellyfin.Api/Models/UserDtos/ForgotPasswordDto.cs
new file mode 100644
index 000000000..b31c6539c
--- /dev/null
+++ b/Jellyfin.Api/Models/UserDtos/ForgotPasswordDto.cs
@@ -0,0 +1,16 @@
+using System.ComponentModel.DataAnnotations;
+
+namespace Jellyfin.Api.Models.UserDtos
+{
+ /// <summary>
+ /// Forgot Password request body DTO.
+ /// </summary>
+ public class ForgotPasswordDto
+ {
+ /// <summary>
+ /// Gets or sets the entered username to have its password reset.
+ /// </summary>
+ [Required]
+ public string? EnteredUsername { get; set; }
+ }
+}
diff --git a/Jellyfin.Data/Jellyfin.Data.csproj b/Jellyfin.Data/Jellyfin.Data.csproj
index 95343f91b..6bb0d8ce2 100644
--- a/Jellyfin.Data/Jellyfin.Data.csproj
+++ b/Jellyfin.Data/Jellyfin.Data.csproj
@@ -41,8 +41,8 @@
</ItemGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="3.1.7" />
- <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="3.1.7" />
+ <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="3.1.8" />
+ <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="3.1.8" />
</ItemGroup>
<ItemGroup>
diff --git a/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj b/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj
index c71c76f08..f86b14244 100644
--- a/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj
+++ b/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj
@@ -20,8 +20,8 @@
<ItemGroup>
<PackageReference Include="BlurHashSharp" Version="1.1.0" />
<PackageReference Include="BlurHashSharp.SkiaSharp" Version="1.1.0" />
- <PackageReference Include="SkiaSharp" Version="2.80.1" />
- <PackageReference Include="SkiaSharp.NativeAssets.Linux" Version="2.80.1" />
+ <PackageReference Include="SkiaSharp" Version="2.80.2" />
+ <PackageReference Include="SkiaSharp.NativeAssets.Linux" Version="2.80.2" />
</ItemGroup>
<ItemGroup>
diff --git a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj
index 30ed3e6af..4e79dd8d6 100644
--- a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj
+++ b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj
@@ -24,11 +24,11 @@
</ItemGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="3.1.7">
+ <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="3.1.8">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
- <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="3.1.7">
+ <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="3.1.8">
<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 0ac309a0b..648172fbf 100644
--- a/Jellyfin.Server/Jellyfin.Server.csproj
+++ b/Jellyfin.Server/Jellyfin.Server.csproj
@@ -41,10 +41,10 @@
<ItemGroup>
<PackageReference Include="CommandLineParser" Version="2.8.0" />
- <PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="3.1.7" />
- <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="3.1.7" />
- <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="3.1.7" />
- <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="3.1.7" />
+ <PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="3.1.8" />
+ <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="3.1.8" />
+ <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="3.1.8" />
+ <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="3.1.8" />
<PackageReference Include="prometheus-net" Version="3.6.0" />
<PackageReference Include="prometheus-net.AspNetCore" Version="3.6.0" />
<PackageReference Include="Serilog.AspNetCore" Version="3.4.0" />
diff --git a/MediaBrowser.Common/MediaBrowser.Common.csproj b/MediaBrowser.Common/MediaBrowser.Common.csproj
index 70dcc2397..322740cca 100644
--- a/MediaBrowser.Common/MediaBrowser.Common.csproj
+++ b/MediaBrowser.Common/MediaBrowser.Common.csproj
@@ -18,8 +18,8 @@
</ItemGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.7" />
- <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="3.1.7" />
+ <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.8" />
+ <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="3.1.8" />
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All"/>
<PackageReference Include="Microsoft.Net.Http.Headers" Version="2.2.8" />
</ItemGroup>
diff --git a/MediaBrowser.Controller/Extensions/StringExtensions.cs b/MediaBrowser.Controller/Extensions/StringExtensions.cs
index 3cc1f328a..182c8ef65 100644
--- a/MediaBrowser.Controller/Extensions/StringExtensions.cs
+++ b/MediaBrowser.Controller/Extensions/StringExtensions.cs
@@ -1,3 +1,4 @@
+#nullable enable
#pragma warning disable CS1591
using System;
@@ -15,11 +16,6 @@ namespace MediaBrowser.Controller.Extensions
{
public static string RemoveDiacritics(this string text)
{
- if (text == null)
- {
- throw new ArgumentNullException(nameof(text));
- }
-
var chars = Normalize(text, NormalizationForm.FormD)
.Where(ch => CharUnicodeInfo.GetUnicodeCategory(ch) != UnicodeCategory.NonSpacingMark);
diff --git a/MediaBrowser.Controller/Library/NameExtensions.cs b/MediaBrowser.Controller/Library/NameExtensions.cs
index 21f33ad19..1c90bb4e0 100644
--- a/MediaBrowser.Controller/Library/NameExtensions.cs
+++ b/MediaBrowser.Controller/Library/NameExtensions.cs
@@ -1,3 +1,4 @@
+#nullable enable
#pragma warning disable CS1591
using System;
@@ -9,7 +10,7 @@ namespace MediaBrowser.Controller.Library
{
public static class NameExtensions
{
- private static string RemoveDiacritics(string name)
+ private static string RemoveDiacritics(string? name)
{
if (name == null)
{
diff --git a/MediaBrowser.Controller/MediaBrowser.Controller.csproj b/MediaBrowser.Controller/MediaBrowser.Controller.csproj
index 9854ec520..654470406 100644
--- a/MediaBrowser.Controller/MediaBrowser.Controller.csproj
+++ b/MediaBrowser.Controller/MediaBrowser.Controller.csproj
@@ -14,8 +14,8 @@
</PropertyGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.7" />
- <PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="3.1.7" />
+ <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.8" />
+ <PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="3.1.8" />
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All"/>
</ItemGroup>
diff --git a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs
index c8bf5557b..3287f9814 100644
--- a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs
+++ b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs
@@ -212,7 +212,10 @@ namespace MediaBrowser.MediaEncoding.Encoder
if (match.Success)
{
- return new Version(match.Groups[1].Value);
+ if (Version.TryParse(match.Groups[1].Value, out var result))
+ {
+ return result;
+ }
}
var versionMap = GetFFmpegLibraryVersions(output);
diff --git a/MediaBrowser.Model/Dlna/ResolutionNormalizer.cs b/MediaBrowser.Model/Dlna/ResolutionNormalizer.cs
index 102db3b44..a4305c810 100644
--- a/MediaBrowser.Model/Dlna/ResolutionNormalizer.cs
+++ b/MediaBrowser.Model/Dlna/ResolutionNormalizer.cs
@@ -15,6 +15,7 @@ namespace MediaBrowser.Model.Dlna
new ResolutionConfiguration(720, 950000),
new ResolutionConfiguration(1280, 2500000),
new ResolutionConfiguration(1920, 4000000),
+ new ResolutionConfiguration(2560, 8000000),
new ResolutionConfiguration(3840, 35000000)
};
diff --git a/MediaBrowser.Model/MediaBrowser.Model.csproj b/MediaBrowser.Model/MediaBrowser.Model.csproj
index c0a75009a..264681090 100644
--- a/MediaBrowser.Model/MediaBrowser.Model.csproj
+++ b/MediaBrowser.Model/MediaBrowser.Model.csproj
@@ -34,7 +34,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All"/>
<PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" Version="2.2.0" />
- <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="3.1.7" />
+ <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="3.1.8" />
<PackageReference Include="System.Globalization" Version="4.3.0" />
<PackageReference Include="System.Text.Json" Version="5.0.0-preview.8.20407.11" />
</ItemGroup>
diff --git a/MediaBrowser.Model/Session/GeneralCommand.cs b/MediaBrowser.Model/Session/GeneralCommand.cs
index 9794bd292..77bb6bcf7 100644
--- a/MediaBrowser.Model/Session/GeneralCommand.cs
+++ b/MediaBrowser.Model/Session/GeneralCommand.cs
@@ -8,7 +8,7 @@ namespace MediaBrowser.Model.Session
{
public class GeneralCommand
{
- public string Name { get; set; }
+ public GeneralCommandType Name { get; set; }
public Guid ControllingUserId { get; set; }
diff --git a/MediaBrowser.Providers/Manager/MetadataService.cs b/MediaBrowser.Providers/Manager/MetadataService.cs
index 42785b057..f110eafa5 100644
--- a/MediaBrowser.Providers/Manager/MetadataService.cs
+++ b/MediaBrowser.Providers/Manager/MetadataService.cs
@@ -297,7 +297,7 @@ namespace MediaBrowser.Providers.Manager
}
/// <summary>
- /// Befores the save.
+ /// Before the save.
/// </summary>
/// <param name="item">The item.</param>
/// <param name="isFullRefresh">if set to <c>true</c> [is full refresh].</param>
@@ -355,13 +355,12 @@ namespace MediaBrowser.Providers.Manager
protected virtual IList<BaseItem> GetChildrenForMetadataUpdates(TItemType item)
{
- var folder = item as Folder;
- if (folder != null)
+ if (item is Folder folder)
{
return folder.GetRecursiveChildren();
}
- return new List<BaseItem>();
+ return Array.Empty<BaseItem>();
}
protected virtual ItemUpdateType UpdateMetadataFromChildren(TItemType item, IList<BaseItem> children, bool isFullRefresh, ItemUpdateType currentUpdateType)
@@ -814,7 +813,7 @@ namespace MediaBrowser.Providers.Manager
try
{
- refreshResult.UpdateType = refreshResult.UpdateType | await provider.FetchAsync(item, options, cancellationToken).ConfigureAwait(false);
+ refreshResult.UpdateType |= await provider.FetchAsync(item, options, cancellationToken).ConfigureAwait(false);
}
catch (OperationCanceledException)
{
@@ -882,16 +881,6 @@ namespace MediaBrowser.Providers.Manager
return refreshResult;
}
- private string NormalizeLanguage(string language)
- {
- if (string.IsNullOrWhiteSpace(language))
- {
- return "en";
- }
-
- return language;
- }
-
private void MergeNewData(TItemType source, TIdType lookupInfo)
{
// Copy new provider id's that may have been obtained
diff --git a/MediaBrowser.Providers/MediaBrowser.Providers.csproj b/MediaBrowser.Providers/MediaBrowser.Providers.csproj
index 39f93c479..51ca26361 100644
--- a/MediaBrowser.Providers/MediaBrowser.Providers.csproj
+++ b/MediaBrowser.Providers/MediaBrowser.Providers.csproj
@@ -16,9 +16,9 @@
</ItemGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.7" />
- <PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="3.1.7" />
- <PackageReference Include="Microsoft.Extensions.Http" Version="3.1.7" />
+ <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.8" />
+ <PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="3.1.8" />
+ <PackageReference Include="Microsoft.Extensions.Http" Version="3.1.8" />
<PackageReference Include="OptimizedPriorityQueue" Version="4.2.0" />
<PackageReference Include="PlaylistsNET" Version="1.1.2" />
<PackageReference Include="TvDbSharper" Version="3.2.1" />
diff --git a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbEpisodeProvider.cs b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbEpisodeProvider.cs
index c088d8cec..5fa8a3e1c 100644
--- a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbEpisodeProvider.cs
+++ b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbEpisodeProvider.cs
@@ -141,6 +141,7 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb
Name = episode.EpisodeName,
Overview = episode.Overview,
CommunityRating = (float?)episode.SiteRating,
+ OfficialRating = episode.ContentRating,
}
};
result.ResetPeople();
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Videos.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Videos.cs
index 241dcab4d..1c673fdbd 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Videos.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Videos.cs
@@ -6,6 +6,6 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Models.General
{
public class Videos
{
- public List<Video> Results { get; set; }
+ public IReadOnlyList<Video> Results { get; set; }
}
}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/Trailers.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/Trailers.cs
index 166860f51..057177294 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/Trailers.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/Trailers.cs
@@ -6,6 +6,6 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Models.Movies
{
public class Trailers
{
- public List<Youtube> Youtube { get; set; }
+ public IReadOnlyList<Youtube> Youtube { get; set; }
}
}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/People/PersonImages.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/People/PersonImages.cs
index 3ea12334e..d82e0fc6d 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/Models/People/PersonImages.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Models/People/PersonImages.cs
@@ -7,6 +7,6 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Models.People
{
public class PersonImages
{
- public List<Profile> Profiles { get; set; }
+ public IReadOnlyList<Profile> Profiles { get; set; }
}
}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbImageProvider.cs
index a975fb8f6..9db7e0997 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbImageProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbImageProvider.cs
@@ -38,6 +38,9 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
public static string ProviderName => TmdbUtils.ProviderName;
+ /// <inheritdoc />
+ public int Order => 0;
+
public bool Supports(BaseItem item)
{
return item is Movie || item is MusicVideo || item is Trailer;
@@ -201,8 +204,6 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
return null;
}
- public int Order => 0;
-
public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
{
return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken);
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbSettings.cs b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbImageSettings.cs
index 128258ab3..1ba8f9072 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbSettings.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbImageSettings.cs
@@ -6,22 +6,17 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
{
internal class TmdbImageSettings
{
- public List<string> backdrop_sizes { get; set; }
+ public IReadOnlyList<string> backdrop_sizes { get; set; }
public string secure_base_url { get; set; }
- public List<string> poster_sizes { get; set; }
+ public IReadOnlyList<string> poster_sizes { get; set; }
- public List<string> profile_sizes { get; set; }
+ public IReadOnlyList<string> profile_sizes { get; set; }
public string GetImageUrl(string image)
{
return secure_base_url + image;
}
}
-
- internal class TmdbSettingsResult
- {
- public TmdbImageSettings images { get; set; }
- }
}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs
index 5d383722a..31cfd8649 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs
@@ -34,7 +34,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
private const string TmdbConfigUrl = TmdbUtils.BaseTmdbApiUrl + "3/configuration?api_key={0}";
private const string GetMovieInfo3 = TmdbUtils.BaseTmdbApiUrl + @"3/movie/{0}?api_key={1}&append_to_response=casts,releases,images,keywords,trailers";
- internal static TmdbMovieProvider Current { get; private set; }
+ private readonly CultureInfo _usCulture = new CultureInfo("en-US");
private readonly IJsonSerializer _jsonSerializer;
private readonly IHttpClientFactory _httpClientFactory;
@@ -44,7 +44,10 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
private readonly ILibraryManager _libraryManager;
private readonly IApplicationHost _appHost;
- private readonly CultureInfo _usCulture = new CultureInfo("en-US");
+ /// <summary>
+ /// The _TMDB settings task.
+ /// </summary>
+ private TmdbSettingsResult _tmdbSettings;
public TmdbMovieProvider(
IJsonSerializer jsonSerializer,
@@ -65,6 +68,14 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
Current = this;
}
+ internal static TmdbMovieProvider Current { get; private set; }
+
+ /// <inheritdoc />
+ public string Name => TmdbUtils.ProviderName;
+
+ /// <inheritdoc />
+ public int Order => 1;
+
public Task<IEnumerable<RemoteSearchResult>> GetSearchResults(MovieInfo searchInfo, CancellationToken cancellationToken)
{
return GetMovieSearchResults(searchInfo, cancellationToken);
@@ -131,13 +142,6 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
return movieDb.GetMetadata(id, cancellationToken);
}
- public string Name => TmdbUtils.ProviderName;
-
- /// <summary>
- /// The _TMDB settings task.
- /// </summary>
- private TmdbSettingsResult _tmdbSettings;
-
/// <summary>
/// Gets the TMDB settings.
/// </summary>
@@ -272,7 +276,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
languages.Add("en");
}
- return string.Join(",", languages);
+ return string.Join(',', languages);
}
public static string NormalizeLanguage(string language)
@@ -381,6 +385,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
/// <summary>
/// Gets the movie db response.
/// </summary>
+ /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
internal Task<HttpResponseMessage> GetMovieDbResponse(HttpRequestMessage message, CancellationToken cancellationToken = default)
{
message.Headers.UserAgent.ParseAdd(_appHost.ApplicationUserAgent);
@@ -388,9 +393,6 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
}
/// <inheritdoc />
- public int Order => 1;
-
- /// <inheritdoc />
public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
{
return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken);
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbSearch.cs b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbSearch.cs
index d885cd90b..36a4eef8a 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbSearch.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbSearch.cs
@@ -207,7 +207,12 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
return results
.Select(i =>
{
- var remoteResult = new RemoteSearchResult {SearchProviderName = TmdbMovieProvider.Current.Name, Name = i.Title ?? i.Name ?? i.Original_Title, ImageUrl = string.IsNullOrWhiteSpace(i.Poster_Path) ? null : baseImageUrl + i.Poster_Path};
+ var remoteResult = new RemoteSearchResult
+ {
+ SearchProviderName = TmdbMovieProvider.Current.Name,
+ Name = i.Title ?? i.Name ?? i.Original_Title,
+ ImageUrl = string.IsNullOrWhiteSpace(i.Poster_Path) ? null : baseImageUrl + i.Poster_Path
+ };
if (!string.IsNullOrWhiteSpace(i.Release_Date))
{
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbSettingsResult.cs b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbSettingsResult.cs
new file mode 100644
index 000000000..c7ba97438
--- /dev/null
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbSettingsResult.cs
@@ -0,0 +1,9 @@
+#pragma warning disable CS1591
+
+namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
+{
+ internal class TmdbSettingsResult
+ {
+ public TmdbImageSettings images { get; set; }
+ }
+}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Music/TmdbMusicVideoProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/Music/TmdbMusicVideoProvider.cs
index 73e49ba5b..b88ecce87 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/Music/TmdbMusicVideoProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Music/TmdbMusicVideoProvider.cs
@@ -14,6 +14,8 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Music
{
public class TmdbMusicVideoProvider : IRemoteMetadataProvider<MusicVideo, MusicVideoInfo>
{
+ public string Name => TmdbMovieProvider.Current.Name;
+
public Task<MetadataResult<MusicVideo>> GetMetadata(MusicVideoInfo info, CancellationToken cancellationToken)
{
return TmdbMovieProvider.Current.GetItemMetadata<MusicVideo>(info, cancellationToken);
@@ -24,8 +26,6 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Music
return Task.FromResult((IEnumerable<RemoteSearchResult>)new List<RemoteSearchResult>());
}
- public string Name => TmdbMovieProvider.Current.Name;
-
public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
{
throw new NotImplementedException();
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs
index 8d1a854d6..777ebce49 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs
@@ -31,7 +31,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People
{
public class TmdbPersonProvider : IRemoteMetadataProvider<Person, PersonLookupInfo>
{
- const string DataFileName = "info.json";
+ private const string DataFileName = "info.json";
private readonly CultureInfo _usCulture = new CultureInfo("en-US");
@@ -39,20 +39,17 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People
private readonly IFileSystem _fileSystem;
private readonly IServerConfigurationManager _configurationManager;
private readonly IHttpClientFactory _httpClientFactory;
- private readonly ILogger<TmdbPersonProvider> _logger;
public TmdbPersonProvider(
IFileSystem fileSystem,
IServerConfigurationManager configurationManager,
IJsonSerializer jsonSerializer,
- IHttpClientFactory httpClientFactory,
- ILogger<TmdbPersonProvider> logger)
+ IHttpClientFactory httpClientFactory)
{
_fileSystem = fileSystem;
_configurationManager = configurationManager;
_jsonSerializer = jsonSerializer;
_httpClientFactory = httpClientFactory;
- _logger = logger;
Current = this;
}
@@ -75,7 +72,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People
var dataFilePath = GetPersonDataFilePath(_configurationManager.ApplicationPaths, tmdbId);
var info = _jsonSerializer.DeserializeFromFile<PersonResult>(dataFilePath);
- var images = (info.Images ?? new PersonImages()).Profiles ?? new List<Profile>();
+ IReadOnlyList<Profile> images = info.Images?.Profiles ?? Array.Empty<Profile>();
var result = new RemoteSearchResult
{
@@ -95,7 +92,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People
if (searchInfo.IsAutomated)
{
// Don't hammer moviedb searching by name
- return new List<RemoteSearchResult>();
+ return Array.Empty<RemoteSearchResult>();
}
var url = string.Format(
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeImageProvider.cs
index eebecdac6..c56774f8e 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeImageProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeImageProvider.cs
@@ -28,7 +28,13 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
{
public TmdbEpisodeImageProvider(IHttpClientFactory httpClientFactory, IServerConfigurationManager configurationManager, IJsonSerializer jsonSerializer, IFileSystem fileSystem, ILocalizationManager localization, ILoggerFactory loggerFactory)
: base(httpClientFactory, configurationManager, jsonSerializer, fileSystem, localization, loggerFactory)
- { }
+ {
+ }
+
+ public string Name => TmdbUtils.ProviderName;
+
+ // After TheTvDb
+ public int Order => 1;
public IEnumerable<ImageType> GetSupportedImages(BaseItem item)
{
@@ -43,7 +49,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
var episode = (Controller.Entities.TV.Episode)item;
var series = episode.Series;
- var seriesId = series != null ? series.GetProviderId(MetadataProvider.Tmdb) : null;
+ var seriesId = series?.GetProviderId(MetadataProvider.Tmdb);
var list = new List<RemoteImageInfo>();
@@ -62,8 +68,12 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
var language = item.GetPreferredMetadataLanguage();
- var response = await GetEpisodeInfo(seriesId, seasonNumber.Value, episodeNumber.Value,
- language, cancellationToken).ConfigureAwait(false);
+ var response = await GetEpisodeInfo(
+ seriesId,
+ seasonNumber.Value,
+ episodeNumber.Value,
+ language,
+ cancellationToken).ConfigureAwait(false);
var tmdbSettings = await TmdbMovieProvider.Current.GetTmdbSettings(cancellationToken).ConfigureAwait(false);
@@ -120,14 +130,9 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
return GetResponse(url, cancellationToken);
}
- public string Name => TmdbUtils.ProviderName;
-
public bool Supports(BaseItem item)
{
return item is Controller.Entities.TV.Episode;
}
-
- // After TheTvDb
- public int Order => 1;
}
}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs
index ed4739acb..a7e3a03fe 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs
@@ -29,7 +29,13 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
{
public TmdbEpisodeProvider(IHttpClientFactory httpClientFactory, IServerConfigurationManager configurationManager, IJsonSerializer jsonSerializer, IFileSystem fileSystem, ILocalizationManager localization, ILoggerFactory loggerFactory)
: base(httpClientFactory, configurationManager, jsonSerializer, fileSystem, localization, loggerFactory)
- { }
+ {
+ }
+
+ // After TheTvDb
+ public int Order => 1;
+
+ public string Name => TmdbUtils.ProviderName;
public async Task<IEnumerable<RemoteSearchResult>> GetSearchResults(EpisodeInfo searchInfo, CancellationToken cancellationToken)
{
@@ -41,7 +47,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
return list;
}
- var metadataResult = await GetMetadata(searchInfo, cancellationToken);
+ var metadataResult = await GetMetadata(searchInfo, cancellationToken).ConfigureAwait(false);
if (metadataResult.HasMetadata)
{
@@ -205,10 +211,5 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
{
return GetResponse(url, cancellationToken);
}
-
- // After TheTvDb
- public int Order => 1;
-
- public string Name => TmdbUtils.ProviderName;
}
}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProviderBase.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProviderBase.cs
index 55b0f0409..34d2424a3 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProviderBase.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProviderBase.cs
@@ -21,11 +21,11 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
public abstract class TmdbEpisodeProviderBase
{
private const string EpisodeUrlPattern = TmdbUtils.BaseTmdbApiUrl + @"3/tv/{0}/season/{1}/episode/{2}?api_key={3}&append_to_response=images,external_ids,credits,videos";
+
private readonly IHttpClientFactory _httpClientFactory;
private readonly IServerConfigurationManager _configurationManager;
private readonly IJsonSerializer _jsonSerializer;
private readonly IFileSystem _fileSystem;
- private readonly ILocalizationManager _localization;
private readonly ILogger<TmdbEpisodeProviderBase> _logger;
protected TmdbEpisodeProviderBase(IHttpClientFactory httpClientFactory, IServerConfigurationManager configurationManager, IJsonSerializer jsonSerializer, IFileSystem fileSystem, ILocalizationManager localization, ILoggerFactory loggerFactory)
@@ -34,13 +34,16 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
_configurationManager = configurationManager;
_jsonSerializer = jsonSerializer;
_fileSystem = fileSystem;
- _localization = localization;
_logger = loggerFactory.CreateLogger<TmdbEpisodeProviderBase>();
}
protected ILogger Logger => _logger;
- protected async Task<EpisodeResult> GetEpisodeInfo(string seriesTmdbId, int season, int episodeNumber, string preferredMetadataLanguage,
+ protected async Task<EpisodeResult> GetEpisodeInfo(
+ string seriesTmdbId,
+ int season,
+ int episodeNumber,
+ string preferredMetadataLanguage,
CancellationToken cancellationToken)
{
await EnsureEpisodeInfo(seriesTmdbId, season, episodeNumber, preferredMetadataLanguage, cancellationToken)
@@ -93,7 +96,9 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
var path = TmdbSeriesProvider.GetSeriesDataPath(_configurationManager.ApplicationPaths, tmdbId);
- var filename = string.Format(CultureInfo.InvariantCulture, "season-{0}-episode-{1}-{2}.json",
+ var filename = string.Format(
+ CultureInfo.InvariantCulture,
+ "season-{0}-episode-{1}-{2}.json",
seasonNumber.ToString(CultureInfo.InvariantCulture),
episodeNumber.ToString(CultureInfo.InvariantCulture),
preferredLanguage);
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonImageProvider.cs
index e7e2fd05b..dcc7f8700 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonImageProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonImageProvider.cs
@@ -112,9 +112,10 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
private async Task<List<Poster>> FetchImages(Season item, string tmdbId, string language, CancellationToken cancellationToken)
{
- await TmdbSeasonProvider.Current.EnsureSeasonInfo(tmdbId, item.IndexNumber.GetValueOrDefault(), language, cancellationToken).ConfigureAwait(false);
+ var seasonNumber = item.IndexNumber.GetValueOrDefault();
+ await TmdbSeasonProvider.Current.EnsureSeasonInfo(tmdbId, seasonNumber, language, cancellationToken).ConfigureAwait(false);
- var path = TmdbSeriesProvider.Current.GetDataFilePath(tmdbId, language);
+ var path = TmdbSeasonProvider.Current.GetDataFilePath(tmdbId, seasonNumber, language);
if (!string.IsNullOrEmpty(path))
{
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs
index 40f1c4e69..c9b257fcc 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs
@@ -28,26 +28,32 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
public class TmdbSeasonProvider : IRemoteMetadataProvider<Season, SeasonInfo>
{
private const string GetTvInfo3 = TmdbUtils.BaseTmdbApiUrl + @"3/tv/{0}/season/{1}?api_key={2}&append_to_response=images,keywords,external_ids,credits,videos";
+
private readonly IHttpClientFactory _httpClientFactory;
private readonly IServerConfigurationManager _configurationManager;
private readonly IJsonSerializer _jsonSerializer;
private readonly IFileSystem _fileSystem;
- private readonly ILocalizationManager _localization;
private readonly ILogger<TmdbSeasonProvider> _logger;
internal static TmdbSeasonProvider Current { get; private set; }
- public TmdbSeasonProvider(IHttpClientFactory httpClientFactory, IServerConfigurationManager configurationManager, IFileSystem fileSystem, ILocalizationManager localization, IJsonSerializer jsonSerializer, ILogger<TmdbSeasonProvider> logger)
+ public TmdbSeasonProvider(
+ IHttpClientFactory httpClientFactory,
+ IServerConfigurationManager configurationManager,
+ IFileSystem fileSystem,
+ IJsonSerializer jsonSerializer,
+ ILogger<TmdbSeasonProvider> logger)
{
_httpClientFactory = httpClientFactory;
_configurationManager = configurationManager;
_fileSystem = fileSystem;
- _localization = localization;
_jsonSerializer = jsonSerializer;
_logger = logger;
Current = this;
}
+ public string Name => TmdbUtils.ProviderName;
+
public async Task<MetadataResult<Season>> GetMetadata(SeasonInfo info, CancellationToken cancellationToken)
{
var result = new MetadataResult<Season>();
@@ -116,8 +122,6 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
return result;
}
- public string Name => TmdbUtils.ProviderName;
-
public Task<IEnumerable<RemoteSearchResult>> GetSearchResults(SeasonInfo searchInfo, CancellationToken cancellationToken)
{
return Task.FromResult<IEnumerable<RemoteSearchResult>>(new List<RemoteSearchResult>());
@@ -128,7 +132,10 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken);
}
- private async Task<SeasonResult> GetSeasonInfo(string seriesTmdbId, int season, string preferredMetadataLanguage,
+ private async Task<SeasonResult> GetSeasonInfo(
+ string seriesTmdbId,
+ int season,
+ string preferredMetadataLanguage,
CancellationToken cancellationToken)
{
await EnsureSeasonInfo(seriesTmdbId, season, preferredMetadataLanguage, cancellationToken)
@@ -181,7 +188,9 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
var path = TmdbSeriesProvider.GetSeriesDataPath(_configurationManager.ApplicationPaths, tmdbId);
- var filename = string.Format(CultureInfo.InvariantCulture, "season-{0}-{1}.json",
+ var filename = string.Format(
+ CultureInfo.InvariantCulture,
+ "season-{0}-{1}.json",
seasonNumber.ToString(CultureInfo.InvariantCulture),
preferredLanguage);
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesImageProvider.cs
index 125560175..179ceb825 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesImageProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesImageProvider.cs
@@ -2,6 +2,7 @@
using System;
using System.Collections.Generic;
+using System.IO;
using System.Linq;
using System.Net.Http;
using System.Threading;
@@ -12,7 +13,6 @@ using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.IO;
using MediaBrowser.Model.Providers;
using MediaBrowser.Model.Serialization;
using MediaBrowser.Providers.Plugins.Tmdb.Models.General;
@@ -25,19 +25,20 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
{
private readonly IJsonSerializer _jsonSerializer;
private readonly IHttpClientFactory _httpClientFactory;
- private readonly IFileSystem _fileSystem;
- public TmdbSeriesImageProvider(IJsonSerializer jsonSerializer, IHttpClientFactory httpClientFactory, IFileSystem fileSystem)
+ public TmdbSeriesImageProvider(IJsonSerializer jsonSerializer, IHttpClientFactory httpClientFactory)
{
_jsonSerializer = jsonSerializer;
_httpClientFactory = httpClientFactory;
- _fileSystem = fileSystem;
}
public string Name => ProviderName;
public static string ProviderName => TmdbUtils.ProviderName;
+ // After tvdb and fanart
+ public int Order => 2;
+
public bool Supports(BaseItem item)
{
return item is Series;
@@ -56,7 +57,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
{
var list = new List<RemoteImageInfo>();
- var results = await FetchImages(item, null, _jsonSerializer, cancellationToken).ConfigureAwait(false);
+ var results = await FetchImages(item, null, cancellationToken).ConfigureAwait(false);
if (results == null)
{
@@ -148,10 +149,11 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
/// </summary>
/// <param name="item">The item.</param>
/// <param name="language">The language.</param>
- /// <param name="jsonSerializer">The json serializer.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task{MovieImages}.</returns>
- private async Task<Images> FetchImages(BaseItem item, string language, IJsonSerializer jsonSerializer,
+ private async Task<Images> FetchImages(
+ BaseItem item,
+ string language,
CancellationToken cancellationToken)
{
var tmdbId = item.GetProviderId(MetadataProvider.Tmdb);
@@ -165,22 +167,14 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
var path = TmdbSeriesProvider.Current.GetDataFilePath(tmdbId, language);
- if (!string.IsNullOrEmpty(path))
+ if (!string.IsNullOrEmpty(path) && File.Exists(path))
{
- var fileInfo = _fileSystem.GetFileInfo(path);
-
- if (fileInfo.Exists)
- {
- return jsonSerializer.DeserializeFromFile<SeriesResult>(path).Images;
- }
+ return _jsonSerializer.DeserializeFromFile<SeriesResult>(path).Images;
}
return null;
}
- // After tvdb and fanart
- public int Order => 2;
-
public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
{
return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken);
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs
index fa0873b9d..287ebca8c 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs
@@ -17,8 +17,6 @@ using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Globalization;
-using MediaBrowser.Model.IO;
using MediaBrowser.Model.Providers;
using MediaBrowser.Model.Serialization;
using MediaBrowser.Providers.Plugins.Tmdb.Models.Search;
@@ -33,38 +31,35 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
private const string GetTvInfo3 = TmdbUtils.BaseTmdbApiUrl + @"3/tv/{0}?api_key={1}&append_to_response=credits,images,keywords,external_ids,videos,content_ratings";
private readonly IJsonSerializer _jsonSerializer;
- private readonly IFileSystem _fileSystem;
private readonly IServerConfigurationManager _configurationManager;
private readonly ILogger<TmdbSeriesProvider> _logger;
- private readonly ILocalizationManager _localization;
private readonly IHttpClientFactory _httpClientFactory;
private readonly ILibraryManager _libraryManager;
private readonly CultureInfo _usCulture = new CultureInfo("en-US");
- internal static TmdbSeriesProvider Current { get; private set; }
-
public TmdbSeriesProvider(
IJsonSerializer jsonSerializer,
- IFileSystem fileSystem,
IServerConfigurationManager configurationManager,
ILogger<TmdbSeriesProvider> logger,
- ILocalizationManager localization,
IHttpClientFactory httpClientFactory,
ILibraryManager libraryManager)
{
_jsonSerializer = jsonSerializer;
- _fileSystem = fileSystem;
_configurationManager = configurationManager;
_logger = logger;
- _localization = localization;
_httpClientFactory = httpClientFactory;
_libraryManager = libraryManager;
Current = this;
}
+ internal static TmdbSeriesProvider Current { get; private set; }
+
public string Name => TmdbUtils.ProviderName;
+ // After TheTVDB
+ public int Order => 1;
+
public async Task<IEnumerable<RemoteSearchResult>> GetSearchResults(SeriesInfo searchInfo, CancellationToken cancellationToken)
{
var tmdbId = searchInfo.GetProviderId(MetadataProvider.Tmdb);
@@ -129,8 +124,10 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
public async Task<MetadataResult<Series>> GetMetadata(SeriesInfo info, CancellationToken cancellationToken)
{
- var result = new MetadataResult<Series>();
- result.QueriedById = true;
+ var result = new MetadataResult<Series>
+ {
+ QueriedById = true
+ };
var tmdbId = info.GetProviderId(MetadataProvider.Tmdb);
@@ -206,9 +203,11 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
await EnsureSeriesInfo(tmdbId, language, cancellationToken).ConfigureAwait(false);
- var result = new MetadataResult<Series>();
- result.Item = new Series();
- result.ResultLanguage = seriesInfo.ResultLanguage;
+ var result = new MetadataResult<Series>
+ {
+ Item = new Series(),
+ ResultLanguage = seriesInfo.ResultLanguage
+ };
var settings = await TmdbMovieProvider.Current.GetTmdbSettings(cancellationToken).ConfigureAwait(false);
@@ -474,12 +473,11 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
var path = GetDataFilePath(tmdbId, language);
- var fileInfo = _fileSystem.GetFileSystemInfo(path);
-
+ var fileInfo = new FileInfo(path);
if (fileInfo.Exists)
{
// If it's recent or automatic updates are enabled, don't re-download
- if ((DateTime.UtcNow - _fileSystem.GetLastWriteTimeUtc(fileInfo)).TotalDays <= 2)
+ if ((DateTime.UtcNow - fileInfo.LastWriteTimeUtc).TotalDays <= 2)
{
return Task.CompletedTask;
}
@@ -549,9 +547,6 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
return null;
}
- // After TheTVDB
- public int Order => 1;
-
public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
{
return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken);
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Trailers/TmdbTrailerProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/Trailers/TmdbTrailerProvider.cs
index 25296387b..613dc17e3 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/Trailers/TmdbTrailerProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Trailers/TmdbTrailerProvider.cs
@@ -21,6 +21,10 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Trailers
_httpClientFactory = httpClientFactory;
}
+ public string Name => TmdbMovieProvider.Current.Name;
+
+ public int Order => 0;
+
public Task<IEnumerable<RemoteSearchResult>> GetSearchResults(TrailerInfo searchInfo, CancellationToken cancellationToken)
{
return TmdbMovieProvider.Current.GetMovieSearchResults(searchInfo, cancellationToken);
@@ -31,10 +35,6 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Trailers
return TmdbMovieProvider.Current.GetItemMetadata<Trailer>(info, cancellationToken);
}
- public string Name => TmdbMovieProvider.Current.Name;
-
- public int Order => 0;
-
public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
{
return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken);
diff --git a/MediaBrowser.Providers/Studios/StudiosImageProvider.cs b/MediaBrowser.Providers/Studios/StudiosImageProvider.cs
index 76dc7df7f..90e13f12f 100644
--- a/MediaBrowser.Providers/Studios/StudiosImageProvider.cs
+++ b/MediaBrowser.Providers/Studios/StudiosImageProvider.cs
@@ -33,6 +33,8 @@ namespace MediaBrowser.Providers.Studios
public string Name => "Emby Designs";
+ public int Order => 0;
+
public bool Supports(BaseItem item)
{
return item is Studio;
@@ -119,8 +121,6 @@ namespace MediaBrowser.Providers.Studios
return EnsureList(url, file, _fileSystem, cancellationToken);
}
- public int Order => 0;
-
public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
{
var httpClient = _httpClientFactory.CreateClient(NamedClient.Default);
@@ -161,12 +161,12 @@ namespace MediaBrowser.Providers.Studios
private string GetComparableName(string name)
{
- return name.Replace(" ", string.Empty)
- .Replace(".", string.Empty)
- .Replace("&", string.Empty)
- .Replace("!", string.Empty)
- .Replace(",", string.Empty)
- .Replace("/", string.Empty);
+ return name.Replace(" ", string.Empty, StringComparison.Ordinal)
+ .Replace(".", string.Empty, StringComparison.Ordinal)
+ .Replace("&", string.Empty, StringComparison.Ordinal)
+ .Replace("!", string.Empty, StringComparison.Ordinal)
+ .Replace(",", string.Empty, StringComparison.Ordinal)
+ .Replace("/", string.Empty, StringComparison.Ordinal);
}
public IEnumerable<string> GetAvailableImages(string file)
diff --git a/MediaBrowser.Providers/Subtitles/SubtitleManager.cs b/MediaBrowser.Providers/Subtitles/SubtitleManager.cs
index 0f7cb3f8f..f25d3d5ee 100644
--- a/MediaBrowser.Providers/Subtitles/SubtitleManager.cs
+++ b/MediaBrowser.Providers/Subtitles/SubtitleManager.cs
@@ -303,7 +303,7 @@ namespace MediaBrowser.Providers.Subtitles
private ISubtitleProvider GetProvider(string id)
{
- return _subtitleProviders.First(i => string.Equals(id, GetProviderId(i.Name)));
+ return _subtitleProviders.First(i => string.Equals(id, GetProviderId(i.Name), StringComparison.Ordinal));
}
/// <inheritdoc />
diff --git a/MediaBrowser.Providers/TV/MissingEpisodeProvider.cs b/MediaBrowser.Providers/TV/MissingEpisodeProvider.cs
index 616c61ec0..c833b1227 100644
--- a/MediaBrowser.Providers/TV/MissingEpisodeProvider.cs
+++ b/MediaBrowser.Providers/TV/MissingEpisodeProvider.cs
@@ -48,18 +48,25 @@ namespace MediaBrowser.Providers.TV
public async Task<bool> Run(Series series, bool addNewItems, CancellationToken cancellationToken)
{
- var tvdbId = series.GetProviderId(MetadataProvider.Tvdb);
- if (string.IsNullOrEmpty(tvdbId))
+ var tvdbIdString = series.GetProviderId(MetadataProvider.Tvdb);
+ if (string.IsNullOrEmpty(tvdbIdString))
{
return false;
}
- var episodes = await _tvdbClientManager.GetAllEpisodesAsync(Convert.ToInt32(tvdbId), series.GetPreferredMetadataLanguage(), cancellationToken);
+ var episodes = await _tvdbClientManager.GetAllEpisodesAsync(
+ int.Parse(tvdbIdString, CultureInfo.InvariantCulture),
+ series.GetPreferredMetadataLanguage(),
+ cancellationToken).ConfigureAwait(false);
var episodeLookup = episodes
.Select(i =>
{
- DateTime.TryParse(i.FirstAired, out var firstAired);
+ if (!DateTime.TryParse(i.FirstAired, out var firstAired))
+ {
+ firstAired = default;
+ }
+
var seasonNumber = i.AiredSeason.GetValueOrDefault(-1);
var episodeNumber = i.AiredEpisodeNumber.GetValueOrDefault(-1);
return (seasonNumber, episodeNumber, firstAired);
diff --git a/MediaBrowser.Providers/TV/SeasonMetadataService.cs b/MediaBrowser.Providers/TV/SeasonMetadataService.cs
index 5431de623..4e59f78bc 100644
--- a/MediaBrowser.Providers/TV/SeasonMetadataService.cs
+++ b/MediaBrowser.Providers/TV/SeasonMetadataService.cs
@@ -28,6 +28,9 @@ namespace MediaBrowser.Providers.TV
}
/// <inheritdoc />
+ protected override bool EnableUpdatingPremiereDateFromChildren => true;
+
+ /// <inheritdoc />
protected override ItemUpdateType BeforeSaveInternal(Season item, bool isFullRefresh, ItemUpdateType currentUpdateType)
{
var updateType = base.BeforeSaveInternal(item, isFullRefresh, currentUpdateType);
@@ -68,9 +71,6 @@ namespace MediaBrowser.Providers.TV
}
/// <inheritdoc />
- protected override bool EnableUpdatingPremiereDateFromChildren => true;
-
- /// <inheritdoc />
protected override IList<BaseItem> GetChildrenForMetadataUpdates(Season item)
=> item.GetEpisodes();
diff --git a/README.md b/README.md
index 55d6917ae..435e709b3 100644
--- a/README.md
+++ b/README.md
@@ -53,18 +53,19 @@ Jellyfin is a Free Software Media System that puts you in control of managing an
For further details, please see [our documentation page](https://docs.jellyfin.org/). To receive the latest updates, get help with Jellyfin, and join the community, please visit [one of our communication channels](https://docs.jellyfin.org/general/getting-help.html). For more information about the project, please see our [about page](https://docs.jellyfin.org/general/about.html).
<strong>Want to get started?</strong><br/>
-Choose from <a href="https://docs.jellyfin.org/general/administration/installing.html">Prebuilt Packages</a> or <a href="https://docs.jellyfin.org/general/administration/building.html">Build from Source</a>, then see our <a href="https://docs.jellyfin.org/general/quick-start.html">quick start guide</a>.<br/>
+Check out our <a href="https://jellyfin.org/downloads">downloads page</a> or our <a href="https://docs.jellyfin.org/general/administration/installing.html">installation guide</a>, then see our <a href="https://docs.jellyfin.org/general/quick-start.html">quick start guide</a>. You can also <a href="https://docs.jellyfin.org/general/administration/building.html">build from source</a>.<br/>
<strong>Something not working right?</strong><br/>
Open an <a href="https://docs.jellyfin.org/general/contributing/issues.html">Issue</a> on GitHub.<br/>
<strong>Want to contribute?</strong><br/>
-Check out <a href="https://docs.jellyfin.org/general/contributing/index.html">our documentation for guidelines</a>.<br/>
+Check out our <a href="https://jellyfin.org/contribute">contributing choose-your-own-adventure</a> to see where you can help, then see our <a href="https://docs.jellyfin.org/general/contributing/index.html">contributing guide</a> and our <a href="https://jellyfin.org/docs/general/community-standards">community standards</a>.<br/>
<strong>New idea or improvement?</strong><br/>
Check out our <a href="https://features.jellyfin.org/?view=most-wanted">feature request hub</a>.<br/>
-Most of the translations can be found in the web client but we have several other clients that have missing strings. Translations can be improved very easily from our <a href="https://translate.jellyfin.org/projects/jellyfin/jellyfin-core">Weblate</a> instance. Look through the following graphic to see if your native language could use some work!
+<strong>Don't see Jellyfin in your language?</strong><br/>
+Check out our <a href="https://translate.jellyfin.org">Weblate instance</a> to help translate Jellyfin and its subprojects.<br/>
<a href="https://translate.jellyfin.org/engage/jellyfin/?utm_source=widget">
<img src="https://translate.jellyfin.org/widgets/jellyfin/-/jellyfin-web/multi-auto.svg" alt="Detailed Translation Status"/>
@@ -124,7 +125,7 @@ To run the project with Visual Studio Code you will first need to open the repos
Second, you need to [install the recommended extensions for the workspace](https://code.visualstudio.com/docs/editor/extension-gallery#_recommended-extensions). Note that extension recommendations are classified as either "Workspace Recommendations" or "Other Recommendations", but only the "Workspace Recommendations" are required.
-After the required extensions are installed, you can can run the server by pressing `F5`.
+After the required extensions are installed, you can run the server by pressing `F5`.
#### Running From The Command Line
diff --git a/RSSDP/DisposableManagedObjectBase.cs b/RSSDP/DisposableManagedObjectBase.cs
index 66a0c5ec4..745ec359c 100644
--- a/RSSDP/DisposableManagedObjectBase.cs
+++ b/RSSDP/DisposableManagedObjectBase.cs
@@ -43,13 +43,13 @@ namespace Rssdp.Infrastructure
{
var builder = new StringBuilder();
- const string argFormat = "{0}: {1}\r\n";
+ const string ArgFormat = "{0}: {1}\r\n";
builder.AppendFormat("{0}\r\n", header);
foreach (var pair in values)
{
- builder.AppendFormat(argFormat, pair.Key, pair.Value);
+ builder.AppendFormat(ArgFormat, pair.Key, pair.Value);
}
builder.Append("\r\n");
diff --git a/deployment/Dockerfile.debian.amd64 b/deployment/Dockerfile.debian.amd64
index 1ac5f76d6..7202c5883 100644
--- a/deployment/Dockerfile.debian.amd64
+++ b/deployment/Dockerfile.debian.amd64
@@ -16,7 +16,7 @@ RUN apt-get update \
# Install dotnet repository
# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
-RUN wget https://download.visualstudio.microsoft.com/download/pr/4f9b8a64-5e09-456c-a087-527cfc8b4cd2/15e14ec06eab947432de139f172f7a98/dotnet-sdk-3.1.401-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget https://download.visualstudio.microsoft.com/download/pr/f01e3d97-c1c3-4635-bc77-0c893be36820/6ec6acabc22468c6cc68b61625b14a7d/dotnet-sdk-3.1.402-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.debian.arm64 b/deployment/Dockerfile.debian.arm64
index 68381e7bf..e9f30213f 100644
--- a/deployment/Dockerfile.debian.arm64
+++ b/deployment/Dockerfile.debian.arm64
@@ -16,7 +16,7 @@ RUN apt-get update \
# Install dotnet repository
# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
-RUN wget https://download.visualstudio.microsoft.com/download/pr/4f9b8a64-5e09-456c-a087-527cfc8b4cd2/15e14ec06eab947432de139f172f7a98/dotnet-sdk-3.1.401-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget https://download.visualstudio.microsoft.com/download/pr/f01e3d97-c1c3-4635-bc77-0c893be36820/6ec6acabc22468c6cc68b61625b14a7d/dotnet-sdk-3.1.402-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.debian.armhf b/deployment/Dockerfile.debian.armhf
index ce1b100c1..91a8a6e7a 100644
--- a/deployment/Dockerfile.debian.armhf
+++ b/deployment/Dockerfile.debian.armhf
@@ -16,7 +16,7 @@ RUN apt-get update \
# Install dotnet repository
# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
-RUN wget https://download.visualstudio.microsoft.com/download/pr/4f9b8a64-5e09-456c-a087-527cfc8b4cd2/15e14ec06eab947432de139f172f7a98/dotnet-sdk-3.1.401-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget https://download.visualstudio.microsoft.com/download/pr/f01e3d97-c1c3-4635-bc77-0c893be36820/6ec6acabc22468c6cc68b61625b14a7d/dotnet-sdk-3.1.402-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.linux.amd64 b/deployment/Dockerfile.linux.amd64
index b4a3c1b76..828d5c2cf 100644
--- a/deployment/Dockerfile.linux.amd64
+++ b/deployment/Dockerfile.linux.amd64
@@ -16,7 +16,7 @@ RUN apt-get update \
# Install dotnet repository
# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
-RUN wget https://download.visualstudio.microsoft.com/download/pr/4f9b8a64-5e09-456c-a087-527cfc8b4cd2/15e14ec06eab947432de139f172f7a98/dotnet-sdk-3.1.401-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget https://download.visualstudio.microsoft.com/download/pr/f01e3d97-c1c3-4635-bc77-0c893be36820/6ec6acabc22468c6cc68b61625b14a7d/dotnet-sdk-3.1.402-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.macos b/deployment/Dockerfile.macos
index 7912e018e..0b2a0fe5f 100644
--- a/deployment/Dockerfile.macos
+++ b/deployment/Dockerfile.macos
@@ -16,7 +16,7 @@ RUN apt-get update \
# Install dotnet repository
# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
-RUN wget https://download.visualstudio.microsoft.com/download/pr/4f9b8a64-5e09-456c-a087-527cfc8b4cd2/15e14ec06eab947432de139f172f7a98/dotnet-sdk-3.1.401-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget https://download.visualstudio.microsoft.com/download/pr/f01e3d97-c1c3-4635-bc77-0c893be36820/6ec6acabc22468c6cc68b61625b14a7d/dotnet-sdk-3.1.402-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.portable b/deployment/Dockerfile.portable
index 949f1ef8f..7d5de230f 100644
--- a/deployment/Dockerfile.portable
+++ b/deployment/Dockerfile.portable
@@ -15,7 +15,7 @@ RUN apt-get update \
# Install dotnet repository
# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
-RUN wget https://download.visualstudio.microsoft.com/download/pr/4f9b8a64-5e09-456c-a087-527cfc8b4cd2/15e14ec06eab947432de139f172f7a98/dotnet-sdk-3.1.401-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget https://download.visualstudio.microsoft.com/download/pr/f01e3d97-c1c3-4635-bc77-0c893be36820/6ec6acabc22468c6cc68b61625b14a7d/dotnet-sdk-3.1.402-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 9518d8493..9c63f43df 100644
--- a/deployment/Dockerfile.ubuntu.amd64
+++ b/deployment/Dockerfile.ubuntu.amd64
@@ -16,7 +16,7 @@ RUN apt-get update \
# Install dotnet repository
# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
-RUN wget https://download.visualstudio.microsoft.com/download/pr/4f9b8a64-5e09-456c-a087-527cfc8b4cd2/15e14ec06eab947432de139f172f7a98/dotnet-sdk-3.1.401-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget https://download.visualstudio.microsoft.com/download/pr/f01e3d97-c1c3-4635-bc77-0c893be36820/6ec6acabc22468c6cc68b61625b14a7d/dotnet-sdk-3.1.402-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 0174f2f2a..51612dd44 100644
--- a/deployment/Dockerfile.ubuntu.arm64
+++ b/deployment/Dockerfile.ubuntu.arm64
@@ -16,7 +16,7 @@ RUN apt-get update \
# Install dotnet repository
# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
-RUN wget https://download.visualstudio.microsoft.com/download/pr/4f9b8a64-5e09-456c-a087-527cfc8b4cd2/15e14ec06eab947432de139f172f7a98/dotnet-sdk-3.1.401-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget https://download.visualstudio.microsoft.com/download/pr/f01e3d97-c1c3-4635-bc77-0c893be36820/6ec6acabc22468c6cc68b61625b14a7d/dotnet-sdk-3.1.402-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 0e02240c8..4ed7f8687 100644
--- a/deployment/Dockerfile.ubuntu.armhf
+++ b/deployment/Dockerfile.ubuntu.armhf
@@ -16,7 +16,7 @@ RUN apt-get update \
# Install dotnet repository
# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
-RUN wget https://download.visualstudio.microsoft.com/download/pr/4f9b8a64-5e09-456c-a087-527cfc8b4cd2/15e14ec06eab947432de139f172f7a98/dotnet-sdk-3.1.401-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget https://download.visualstudio.microsoft.com/download/pr/f01e3d97-c1c3-4635-bc77-0c893be36820/6ec6acabc22468c6cc68b61625b14a7d/dotnet-sdk-3.1.402-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.windows.amd64 b/deployment/Dockerfile.windows.amd64
index d1f2f9e48..5671cc598 100644
--- a/deployment/Dockerfile.windows.amd64
+++ b/deployment/Dockerfile.windows.amd64
@@ -15,7 +15,7 @@ RUN apt-get update \
# Install dotnet repository
# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
-RUN wget https://download.visualstudio.microsoft.com/download/pr/4f9b8a64-5e09-456c-a087-527cfc8b4cd2/15e14ec06eab947432de139f172f7a98/dotnet-sdk-3.1.401-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget https://download.visualstudio.microsoft.com/download/pr/f01e3d97-c1c3-4635-bc77-0c893be36820/6ec6acabc22468c6cc68b61625b14a7d/dotnet-sdk-3.1.402-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/fedora/jellyfin.spec b/fedora/jellyfin.spec
index 37b573e50..bfb2b3be2 100644
--- a/fedora/jellyfin.spec
+++ b/fedora/jellyfin.spec
@@ -84,6 +84,10 @@ EOF
%{_libdir}/jellyfin/*.so
%{_libdir}/jellyfin/*.a
%{_libdir}/jellyfin/createdump
+%{_libdir}/jellyfin/*.xml
+%{_libdir}/jellyfin/wwwroot/api-docs/*
+%{_libdir}/jellyfin/wwwroot/api-docs/redoc/*
+%{_libdir}/jellyfin/wwwroot/api-docs/swagger/*
# Needs 755 else only root can run it since binary build by dotnet is 722
%attr(755,root,root) %{_libdir}/jellyfin/jellyfin
%{_libdir}/jellyfin/SOS_README.md
diff --git a/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj b/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj
index bcba3a203..e3a7a5428 100644
--- a/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj
+++ b/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj
@@ -16,8 +16,8 @@
<PackageReference Include="AutoFixture" Version="4.13.0" />
<PackageReference Include="AutoFixture.AutoMoq" Version="4.13.0" />
<PackageReference Include="AutoFixture.Xunit2" Version="4.13.0" />
- <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="3.1.7" />
- <PackageReference Include="Microsoft.Extensions.Options" Version="3.1.7" />
+ <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="3.1.8" />
+ <PackageReference Include="Microsoft.Extensions.Options" Version="3.1.8" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.1" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
diff --git a/tests/Jellyfin.Naming.Tests/AudioBook/AudioBookFileInfoTests.cs b/tests/Jellyfin.Naming.Tests/AudioBook/AudioBookFileInfoTests.cs
new file mode 100644
index 000000000..a214bc57c
--- /dev/null
+++ b/tests/Jellyfin.Naming.Tests/AudioBook/AudioBookFileInfoTests.cs
@@ -0,0 +1,30 @@
+using Emby.Naming.AudioBook;
+using Xunit;
+
+namespace Jellyfin.Naming.Tests.AudioBook
+{
+ public class AudioBookFileInfoTests
+ {
+ [Fact]
+ public void CompareTo_Same_Success()
+ {
+ var info = new AudioBookFileInfo();
+ Assert.Equal(0, info.CompareTo(info));
+ }
+
+ [Fact]
+ public void CompareTo_Null_Success()
+ {
+ var info = new AudioBookFileInfo();
+ Assert.Equal(1, info.CompareTo(null));
+ }
+
+ [Fact]
+ public void CompareTo_Empty_Success()
+ {
+ var info1 = new AudioBookFileInfo();
+ var info2 = new AudioBookFileInfo();
+ Assert.Equal(0, info1.CompareTo(info2));
+ }
+ }
+}
diff --git a/tests/Jellyfin.Naming.Tests/AudioBook/AudioBookListResolverTests.cs b/tests/Jellyfin.Naming.Tests/AudioBook/AudioBookListResolverTests.cs
new file mode 100644
index 000000000..1084e20bd
--- /dev/null
+++ b/tests/Jellyfin.Naming.Tests/AudioBook/AudioBookListResolverTests.cs
@@ -0,0 +1,90 @@
+using System.Linq;
+using Emby.Naming.AudioBook;
+using Emby.Naming.Common;
+using MediaBrowser.Model.IO;
+using Xunit;
+
+namespace Jellyfin.Naming.Tests.AudioBook
+{
+ public class AudioBookListResolverTests
+ {
+ private readonly NamingOptions _namingOptions = new NamingOptions();
+
+ [Fact]
+ public void TestStackAndExtras()
+ {
+ // No stacking here because there is no part/disc/etc
+ var files = new[]
+ {
+ "Harry Potter and the Deathly Hallows/Part 1.mp3",
+ "Harry Potter and the Deathly Hallows/Part 2.mp3",
+ "Harry Potter and the Deathly Hallows/book.nfo",
+
+ "Batman/Chapter 1.mp3",
+ "Batman/Chapter 2.mp3",
+ "Batman/Chapter 3.mp3",
+ };
+
+ var resolver = GetResolver();
+
+ var result = resolver.Resolve(files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = false,
+ FullName = i
+ })).ToList();
+
+ Assert.Equal(2, result[0].Files.Count);
+ // Assert.Empty(result[0].Extras); FIXME: AudioBookListResolver should resolve extra files properly
+ Assert.Equal("Harry Potter and the Deathly Hallows", result[0].Name);
+
+ Assert.Equal(3, result[1].Files.Count);
+ Assert.Empty(result[1].Extras);
+ Assert.Equal("Batman", result[1].Name);
+ }
+
+ [Fact]
+ public void TestWithMetadata()
+ {
+ var files = new[]
+ {
+ "Harry Potter and the Deathly Hallows/Chapter 1.ogg",
+ "Harry Potter and the Deathly Hallows/Harry Potter and the Deathly Hallows.nfo"
+ };
+
+ var resolver = GetResolver();
+
+ var result = resolver.Resolve(files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = false,
+ FullName = i
+ }));
+
+ Assert.Single(result);
+ }
+
+ [Fact]
+ public void TestWithExtra()
+ {
+ var files = new[]
+ {
+ "Harry Potter and the Deathly Hallows/Chapter 1.mp3",
+ "Harry Potter and the Deathly Hallows/Harry Potter and the Deathly Hallows trailer.mp3"
+ };
+
+ var resolver = GetResolver();
+
+ var result = resolver.Resolve(files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = false,
+ FullName = i
+ })).ToList();
+
+ Assert.Single(result);
+ }
+
+ private AudioBookListResolver GetResolver()
+ {
+ return new AudioBookListResolver(_namingOptions);
+ }
+ }
+}
diff --git a/tests/Jellyfin.Naming.Tests/AudioBook/AudioBookResolverTests.cs b/tests/Jellyfin.Naming.Tests/AudioBook/AudioBookResolverTests.cs
new file mode 100644
index 000000000..83d44721c
--- /dev/null
+++ b/tests/Jellyfin.Naming.Tests/AudioBook/AudioBookResolverTests.cs
@@ -0,0 +1,57 @@
+using System.Collections.Generic;
+using Emby.Naming.AudioBook;
+using Emby.Naming.Common;
+using Xunit;
+
+namespace Jellyfin.Naming.Tests.AudioBook
+{
+ public class AudioBookResolverTests
+ {
+ private readonly NamingOptions _namingOptions = new NamingOptions();
+
+ public static IEnumerable<object[]> GetResolveFileTestData()
+ {
+ yield return new object[]
+ {
+ new AudioBookFileInfo()
+ {
+ Path = @"/server/AudioBooks/Larry Potter/Larry Potter.mp3",
+ Container = "mp3",
+ }
+ };
+ yield return new object[]
+ {
+ new AudioBookFileInfo()
+ {
+ Path = @"/server/AudioBooks/Berry Potter/Chapter 1 .ogg",
+ Container = "ogg",
+ ChapterNumber = 1
+ }
+ };
+ yield return new object[]
+ {
+ new AudioBookFileInfo()
+ {
+ Path = @"/server/AudioBooks/Nerry Potter/Part 3 - Chapter 2.mp3",
+ Container = "mp3",
+ ChapterNumber = 2,
+ PartNumber = 3
+ }
+ };
+ }
+
+ [Theory]
+ [MemberData(nameof(GetResolveFileTestData))]
+ public void ResolveFile_ValidFileName_Success(AudioBookFileInfo expectedResult)
+ {
+ var result = new AudioBookResolver(_namingOptions).Resolve(expectedResult.Path);
+
+ Assert.NotNull(result);
+ Assert.Equal(result.Path, expectedResult.Path);
+ Assert.Equal(result.Container, expectedResult.Container);
+ Assert.Equal(result.ChapterNumber, expectedResult.ChapterNumber);
+ Assert.Equal(result.PartNumber, expectedResult.PartNumber);
+ Assert.Equal(result.IsDirectory, expectedResult.IsDirectory);
+ }
+ }
+}
diff --git a/tests/Jellyfin.Naming.Tests/Video/ExtraTests.cs b/tests/Jellyfin.Naming.Tests/Video/ExtraTests.cs
index a2722a175..8dfb8f859 100644
--- a/tests/Jellyfin.Naming.Tests/Video/ExtraTests.cs
+++ b/tests/Jellyfin.Naming.Tests/Video/ExtraTests.cs
@@ -44,14 +44,14 @@ namespace Jellyfin.Naming.Tests.Video
}
[Theory]
- [InlineData(ExtraType.BehindTheScenes, "behind the scenes" )]
- [InlineData(ExtraType.DeletedScene, "deleted scenes" )]
- [InlineData(ExtraType.Interview, "interviews" )]
- [InlineData(ExtraType.Scene, "scenes" )]
- [InlineData(ExtraType.Sample, "samples" )]
- [InlineData(ExtraType.Clip, "shorts" )]
- [InlineData(ExtraType.Clip, "featurettes" )]
- [InlineData(ExtraType.Unknown, "extras" )]
+ [InlineData(ExtraType.BehindTheScenes, "behind the scenes")]
+ [InlineData(ExtraType.DeletedScene, "deleted scenes")]
+ [InlineData(ExtraType.Interview, "interviews")]
+ [InlineData(ExtraType.Scene, "scenes")]
+ [InlineData(ExtraType.Sample, "samples")]
+ [InlineData(ExtraType.Clip, "shorts")]
+ [InlineData(ExtraType.Clip, "featurettes")]
+ [InlineData(ExtraType.Unknown, "extras")]
public void TestDirectories(ExtraType type, string dirName)
{
Test(dirName + "/300.mp4", type, _videoOptions);
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Library/PathExtensionsTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Library/PathExtensionsTests.cs
index c771f5f4a..6d768af89 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/Library/PathExtensionsTests.cs
+++ b/tests/Jellyfin.Server.Implementations.Tests/Library/PathExtensionsTests.cs
@@ -10,6 +10,7 @@ namespace Jellyfin.Server.Implementations.Tests.Library
[InlineData("Superman: Red Son [imdbid=tt10985510]", "imdbid", "tt10985510")]
[InlineData("Superman: Red Son - tt10985510", "imdbid", "tt10985510")]
[InlineData("Superman: Red Son", "imdbid", null)]
+ [InlineData("Superman: Red Son", "something", null)]
public void GetAttributeValue_ValidArgs_Correct(string input, string attribute, string? expectedResult)
{
Assert.Equal(expectedResult, PathExtensions.GetAttributeValue(input, attribute));