aboutsummaryrefslogtreecommitdiff
path: root/Emby.Server.Implementations
diff options
context:
space:
mode:
Diffstat (limited to 'Emby.Server.Implementations')
-rw-r--r--Emby.Server.Implementations/ApplicationHost.cs1
-rw-r--r--Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs41
-rw-r--r--Emby.Server.Implementations/Dto/DtoService.cs21
-rw-r--r--Emby.Server.Implementations/Emby.Server.Implementations.csproj3
-rw-r--r--Emby.Server.Implementations/Library/LibraryManager.cs40
-rw-r--r--Emby.Server.Implementations/Library/MediaSourceManager.cs10
-rw-r--r--Emby.Server.Implementations/Library/PathManager.cs45
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/ExtraResolver.cs4
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs4
-rw-r--r--Emby.Server.Implementations/Library/SplashscreenPostScanTask.cs13
-rw-r--r--Emby.Server.Implementations/Library/UserDataManager.cs14
-rw-r--r--Emby.Server.Implementations/Library/Validators/GenresValidator.cs23
-rw-r--r--Emby.Server.Implementations/Localization/Core/af.json8
-rw-r--r--Emby.Server.Implementations/Localization/Core/de.json40
-rw-r--r--Emby.Server.Implementations/Localization/Core/ms.json34
-rw-r--r--Emby.Server.Implementations/Localization/Core/te.json10
-rw-r--r--Emby.Server.Implementations/Localization/LocalizationManager.cs199
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/0-prefer.csv11
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/0-prefer.json34
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/au.csv17
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/au.json69
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/be.csv11
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/be.json55
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/br.csv14
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/br.json55
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/ca.csv18
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/ca.json90
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/cl.json41
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/co.csv7
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/co.json55
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/de.csv17
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/de.json41
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/dk.csv7
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/dk.json48
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/es.csv25
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/es.json90
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/fi.csv10
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/fi.json41
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/fr.csv13
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/fr.json69
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/gb.csv23
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/gb.json97
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/ie.csv10
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/ie.json55
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/jp.csv11
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/jp.json62
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/kz.csv6
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/kz.json41
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/mx.csv6
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/mx.json41
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/nl.csv8
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/nl.json55
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/no.csv10
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/no.json69
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/nz.csv16
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/nz.json69
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/pl.json41
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/ro.csv6
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/ro.json48
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/ru.csv6
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/ru.json48
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/se.csv10
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/se.json55
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/sk.csv6
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/sk.json41
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/uk.csv22
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/uk.json97
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/us.csv52
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/us.json83
-rw-r--r--Emby.Server.Implementations/Playlists/PlaylistManager.cs12
-rw-r--r--Emby.Server.Implementations/Session/SessionManager.cs88
-rw-r--r--Emby.Server.Implementations/Sorting/OfficialRatingComparer.cs71
-rw-r--r--Emby.Server.Implementations/SyncPlay/Group.cs16
-rw-r--r--Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs36
-rw-r--r--Emby.Server.Implementations/SystemManager.cs36
75 files changed, 2121 insertions, 580 deletions
diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs
index 4d959905d..5bb75e2b9 100644
--- a/Emby.Server.Implementations/ApplicationHost.cs
+++ b/Emby.Server.Implementations/ApplicationHost.cs
@@ -505,6 +505,7 @@ namespace Emby.Server.Implementations
serviceCollection.AddSingleton<IChapterRepository, ChapterRepository>();
serviceCollection.AddSingleton<IMediaAttachmentRepository, MediaAttachmentRepository>();
serviceCollection.AddSingleton<IMediaStreamRepository, MediaStreamRepository>();
+ serviceCollection.AddSingleton<IKeyframeRepository, KeyframeRepository>();
serviceCollection.AddSingleton<IItemTypeLookup, ItemTypeLookup>();
serviceCollection.AddSingleton<IMediaEncoder, MediaBrowser.MediaEncoding.Encoder.MediaEncoder>();
diff --git a/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs b/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs
index 63481b1f8..9a80eafe5 100644
--- a/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs
+++ b/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs
@@ -1,14 +1,14 @@
#pragma warning disable CS1591
using System;
+using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Database.Implementations;
-using MediaBrowser.Controller;
using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.IO;
using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Trickplay;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
@@ -19,15 +19,18 @@ public class CleanDatabaseScheduledTask : ILibraryPostScanTask
private readonly ILibraryManager _libraryManager;
private readonly ILogger<CleanDatabaseScheduledTask> _logger;
private readonly IDbContextFactory<JellyfinDbContext> _dbProvider;
+ private readonly IPathManager _pathManager;
public CleanDatabaseScheduledTask(
ILibraryManager libraryManager,
ILogger<CleanDatabaseScheduledTask> logger,
- IDbContextFactory<JellyfinDbContext> dbProvider)
+ IDbContextFactory<JellyfinDbContext> dbProvider,
+ IPathManager pathManager)
{
_libraryManager = libraryManager;
_logger = logger;
_dbProvider = dbProvider;
+ _pathManager = pathManager;
}
public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
@@ -56,6 +59,38 @@ public class CleanDatabaseScheduledTask : ILibraryPostScanTask
{
_logger.LogInformation("Cleaning item {Item} type: {Type} path: {Path}", item.Name, item.GetType().Name, item.Path ?? string.Empty);
+ foreach (var mediaSource in item.GetMediaSources(false))
+ {
+ // Delete extracted subtitles
+ try
+ {
+ var subtitleFolder = _pathManager.GetSubtitleFolderPath(mediaSource.Id);
+ if (Directory.Exists(subtitleFolder))
+ {
+ Directory.Delete(subtitleFolder, true);
+ }
+ }
+ catch (Exception e)
+ {
+ _logger.LogWarning("Failed to remove subtitle cache folder for {Item}: {Exception}", item.Id, e.Message);
+ }
+
+ // Delete extracted attachments
+ try
+ {
+ var attachmentFolder = _pathManager.GetAttachmentFolderPath(mediaSource.Id);
+ if (Directory.Exists(attachmentFolder))
+ {
+ Directory.Delete(attachmentFolder, true);
+ }
+ }
+ catch (Exception e)
+ {
+ _logger.LogWarning("Failed to remove attachment cache folder for {Item}: {Exception}", item.Id, e.Message);
+ }
+ }
+
+ // Delete item
_libraryManager.DeleteItem(item, new DeleteOptions
{
DeleteFileLocation = false
diff --git a/Emby.Server.Implementations/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs
index 0ce967e6a..5b0fc9ef3 100644
--- a/Emby.Server.Implementations/Dto/DtoService.cs
+++ b/Emby.Server.Implementations/Dto/DtoService.cs
@@ -41,7 +41,6 @@ namespace Emby.Server.Implementations.Dto
private readonly ILogger<DtoService> _logger;
private readonly ILibraryManager _libraryManager;
private readonly IUserDataManager _userDataRepository;
- private readonly IItemRepository _itemRepo;
private readonly IImageProcessor _imageProcessor;
private readonly IProviderManager _providerManager;
@@ -58,7 +57,6 @@ namespace Emby.Server.Implementations.Dto
ILogger<DtoService> logger,
ILibraryManager libraryManager,
IUserDataManager userDataRepository,
- IItemRepository itemRepo,
IImageProcessor imageProcessor,
IProviderManager providerManager,
IRecordingsManager recordingsManager,
@@ -71,7 +69,6 @@ namespace Emby.Server.Implementations.Dto
_logger = logger;
_libraryManager = libraryManager;
_userDataRepository = userDataRepository;
- _itemRepo = itemRepo;
_imageProcessor = imageProcessor;
_providerManager = providerManager;
_recordingsManager = recordingsManager;
@@ -99,11 +96,11 @@ namespace Emby.Server.Implementations.Dto
if (item is LiveTvChannel tvChannel)
{
- (channelTuples ??= new()).Add((dto, tvChannel));
+ (channelTuples ??= []).Add((dto, tvChannel));
}
else if (item is LiveTvProgram)
{
- (programTuples ??= new()).Add((item, dto));
+ (programTuples ??= []).Add((item, dto));
}
if (item is IItemByName byName)
@@ -590,12 +587,12 @@ namespace Emby.Server.Implementations.Dto
if (dto.ImageBlurHashes is not null)
{
// Only add BlurHash for the person's image.
- baseItemPerson.ImageBlurHashes = new Dictionary<ImageType, Dictionary<string, string>>();
+ baseItemPerson.ImageBlurHashes = [];
foreach (var (imageType, blurHash) in dto.ImageBlurHashes)
{
if (blurHash is not null)
{
- baseItemPerson.ImageBlurHashes[imageType] = new Dictionary<string, string>();
+ baseItemPerson.ImageBlurHashes[imageType] = [];
foreach (var (imageId, blurHashValue) in blurHash)
{
if (string.Equals(baseItemPerson.PrimaryImageTag, imageId, StringComparison.OrdinalIgnoreCase))
@@ -674,11 +671,11 @@ namespace Emby.Server.Implementations.Dto
if (!string.IsNullOrEmpty(image.BlurHash))
{
- dto.ImageBlurHashes ??= new Dictionary<ImageType, Dictionary<string, string>>();
+ dto.ImageBlurHashes ??= [];
if (!dto.ImageBlurHashes.TryGetValue(image.Type, out var value))
{
- value = new Dictionary<string, string>();
+ value = [];
dto.ImageBlurHashes[image.Type] = value;
}
@@ -709,7 +706,7 @@ namespace Emby.Server.Implementations.Dto
if (hashes.Count > 0)
{
- dto.ImageBlurHashes ??= new Dictionary<ImageType, Dictionary<string, string>>();
+ dto.ImageBlurHashes ??= [];
dto.ImageBlurHashes[imageType] = hashes;
}
@@ -756,7 +753,7 @@ namespace Emby.Server.Implementations.Dto
dto.AspectRatio = hasAspectRatio.AspectRatio;
}
- dto.ImageBlurHashes = new Dictionary<ImageType, Dictionary<string, string>>();
+ dto.ImageBlurHashes = [];
var backdropLimit = options.GetImageLimit(ImageType.Backdrop);
if (backdropLimit > 0)
@@ -772,7 +769,7 @@ namespace Emby.Server.Implementations.Dto
if (options.EnableImages)
{
- dto.ImageTags = new Dictionary<ImageType, string>();
+ dto.ImageTags = [];
// Prevent implicitly captured closure
var currentItem = item;
diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj
index 8f89f35ac..d99923b4f 100644
--- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj
+++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj
@@ -22,6 +22,7 @@
</ItemGroup>
<ItemGroup>
+ <PackageReference Include="BitFaster.Caching" />
<PackageReference Include="DiscUtils.Udf" />
<PackageReference Include="Microsoft.Data.Sqlite" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
@@ -67,6 +68,6 @@
<EmbeddedResource Include="Localization\iso6392.txt" />
<EmbeddedResource Include="Localization\countries.json" />
<EmbeddedResource Include="Localization\Core\*.json" />
- <EmbeddedResource Include="Localization\Ratings\*.csv" />
+ <EmbeddedResource Include="Localization\Ratings\*.json" />
</ItemGroup>
</Project>
diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs
index c6b8501cb..40045782b 100644
--- a/Emby.Server.Implementations/Library/LibraryManager.cs
+++ b/Emby.Server.Implementations/Library/LibraryManager.cs
@@ -2,7 +2,6 @@
#pragma warning disable CA5394
using System;
-using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
@@ -11,6 +10,7 @@ using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
+using BitFaster.Caching.Lru;
using Emby.Naming.Common;
using Emby.Naming.TV;
using Emby.Server.Implementations.Library.Resolvers;
@@ -64,7 +64,6 @@ namespace Emby.Server.Implementations.Library
private const string ShortcutFileExtension = ".mblink";
private readonly ILogger<LibraryManager> _logger;
- private readonly ConcurrentDictionary<Guid, BaseItem> _cache;
private readonly ITaskManager _taskManager;
private readonly IUserManager _userManager;
private readonly IUserDataManager _userDataRepository;
@@ -81,6 +80,7 @@ namespace Emby.Server.Implementations.Library
private readonly IPeopleRepository _peopleRepository;
private readonly ExtraResolver _extraResolver;
private readonly IPathManager _pathManager;
+ private readonly FastConcurrentLru<Guid, BaseItem> _cache;
/// <summary>
/// The _root folder sync lock.
@@ -150,7 +150,9 @@ namespace Emby.Server.Implementations.Library
_mediaEncoder = mediaEncoder;
_itemRepository = itemRepository;
_imageProcessor = imageProcessor;
- _cache = new ConcurrentDictionary<Guid, BaseItem>();
+
+ _cache = new FastConcurrentLru<Guid, BaseItem>(_configurationManager.Configuration.CacheSize);
+
_namingOptions = namingOptions;
_peopleRepository = peopleRepository;
_pathManager = pathManager;
@@ -158,7 +160,7 @@ namespace Emby.Server.Implementations.Library
_configurationManager.ConfigurationUpdated += ConfigurationUpdated;
- RecordConfigurationValues(configurationManager.Configuration);
+ RecordConfigurationValues(_configurationManager.Configuration);
}
/// <summary>
@@ -306,7 +308,7 @@ namespace Emby.Server.Implementations.Library
}
}
- _cache[item.Id] = item;
+ _cache.AddOrUpdate(item.Id, item);
}
public void DeleteItem(BaseItem item, DeleteOptions options)
@@ -460,14 +462,13 @@ namespace Emby.Server.Implementations.Library
item.SetParent(null);
_itemRepository.DeleteItem(item.Id);
+ _cache.TryRemove(item.Id, out _);
foreach (var child in children)
{
_itemRepository.DeleteItem(child.Id);
_cache.TryRemove(child.Id, out _);
}
- _cache.TryRemove(item.Id, out _);
-
ReportItemRemoved(item, parent);
}
@@ -491,7 +492,24 @@ namespace Emby.Server.Implementations.Library
if (item is Video video)
{
+ // Trickplay
list.Add(_pathManager.GetTrickplayDirectory(video));
+
+ // Subtitles and attachments
+ foreach (var mediaSource in item.GetMediaSources(false))
+ {
+ var subtitleFolder = _pathManager.GetSubtitleFolderPath(mediaSource.Id);
+ if (subtitleFolder is not null)
+ {
+ list.Add(subtitleFolder);
+ }
+
+ var attachmentFolder = _pathManager.GetAttachmentFolderPath(mediaSource.Id);
+ if (attachmentFolder is not null)
+ {
+ list.Add(attachmentFolder);
+ }
+ }
}
return list;
@@ -1255,7 +1273,7 @@ namespace Emby.Server.Implementations.Library
throw new ArgumentException("Guid can't be empty", nameof(id));
}
- if (_cache.TryGetValue(id, out BaseItem? item))
+ if (_cache.TryGet(id, out var item))
{
return item;
}
@@ -1272,7 +1290,7 @@ namespace Emby.Server.Implementations.Library
/// <inheritdoc />
public T? GetItemById<T>(Guid id)
- where T : BaseItem
+ where T : BaseItem
{
var item = GetItemById(id);
if (item is T typedItem)
@@ -2708,7 +2726,7 @@ namespace Emby.Server.Implementations.Library
public IEnumerable<BaseItem> FindExtras(BaseItem owner, IReadOnlyList<FileSystemMetadata> fileSystemChildren, IDirectoryService directoryService)
{
- var ownerVideoInfo = VideoResolver.Resolve(owner.Path, owner.IsFolder, _namingOptions);
+ var ownerVideoInfo = VideoResolver.Resolve(owner.Path, owner.IsFolder, _namingOptions, libraryRoot: owner.ContainingFolderPath);
if (ownerVideoInfo is null)
{
yield break;
@@ -2903,7 +2921,7 @@ namespace Emby.Server.Implementations.Library
throw new ArgumentNullException(nameof(name));
}
- name = _fileSystem.GetValidFilename(name);
+ name = _fileSystem.GetValidFilename(name.Trim());
var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath;
diff --git a/Emby.Server.Implementations/Library/MediaSourceManager.cs b/Emby.Server.Implementations/Library/MediaSourceManager.cs
index afe5b14e9..c6cfd5391 100644
--- a/Emby.Server.Implementations/Library/MediaSourceManager.cs
+++ b/Emby.Server.Implementations/Library/MediaSourceManager.cs
@@ -427,6 +427,7 @@ namespace Emby.Server.Implementations.Library
if (source.MediaStreams.Any(i => i.Type == MediaStreamType.Audio && i.Index == index))
{
source.DefaultAudioStreamIndex = index;
+ source.DefaultAudioIndexSource = AudioIndexSource.User;
return;
}
}
@@ -434,6 +435,15 @@ namespace Emby.Server.Implementations.Library
var preferredAudio = NormalizeLanguage(user.AudioLanguagePreference);
source.DefaultAudioStreamIndex = MediaStreamSelector.GetDefaultAudioStreamIndex(source.MediaStreams, preferredAudio, user.PlayDefaultAudioTrack);
+ if (user.PlayDefaultAudioTrack)
+ {
+ source.DefaultAudioIndexSource |= AudioIndexSource.Default;
+ }
+
+ if (preferredAudio.Count > 0)
+ {
+ source.DefaultAudioIndexSource |= AudioIndexSource.Language;
+ }
}
public void SetDefaultAudioAndSubtitleStreamIndices(BaseItem item, MediaSourceInfo source, User user)
diff --git a/Emby.Server.Implementations/Library/PathManager.cs b/Emby.Server.Implementations/Library/PathManager.cs
index c910abadb..83a6df964 100644
--- a/Emby.Server.Implementations/Library/PathManager.cs
+++ b/Emby.Server.Implementations/Library/PathManager.cs
@@ -1,5 +1,7 @@
+using System;
using System.Globalization;
using System.IO;
+using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.IO;
@@ -12,25 +14,60 @@ namespace Emby.Server.Implementations.Library;
public class PathManager : IPathManager
{
private readonly IServerConfigurationManager _config;
+ private readonly IApplicationPaths _appPaths;
/// <summary>
/// Initializes a new instance of the <see cref="PathManager"/> class.
/// </summary>
/// <param name="config">The server configuration manager.</param>
+ /// <param name="appPaths">The application paths.</param>
public PathManager(
- IServerConfigurationManager config)
+ IServerConfigurationManager config,
+ IApplicationPaths appPaths)
{
_config = config;
+ _appPaths = appPaths;
+ }
+
+ private string SubtitleCachePath => Path.Combine(_appPaths.DataPath, "subtitles");
+
+ private string AttachmentCachePath => Path.Combine(_appPaths.DataPath, "attachments");
+
+ /// <inheritdoc />
+ public string GetAttachmentPath(string mediaSourceId, string fileName)
+ {
+ return Path.Join(GetAttachmentFolderPath(mediaSourceId), fileName);
+ }
+
+ /// <inheritdoc />
+ public string GetAttachmentFolderPath(string mediaSourceId)
+ {
+ var id = Guid.Parse(mediaSourceId).ToString("D", CultureInfo.InvariantCulture).AsSpan();
+
+ return Path.Join(AttachmentCachePath, id[..2], id);
+ }
+
+ /// <inheritdoc />
+ public string GetSubtitleFolderPath(string mediaSourceId)
+ {
+ var id = Guid.Parse(mediaSourceId).ToString("D", CultureInfo.InvariantCulture).AsSpan();
+
+ return Path.Join(SubtitleCachePath, id[..2], id);
+ }
+
+ /// <inheritdoc />
+ public string GetSubtitlePath(string mediaSourceId, int streamIndex, string extension)
+ {
+ return Path.Join(GetSubtitleFolderPath(mediaSourceId), streamIndex.ToString(CultureInfo.InvariantCulture) + extension);
}
/// <inheritdoc />
public string GetTrickplayDirectory(BaseItem item, bool saveWithMedia = false)
{
- var basePath = _config.ApplicationPaths.TrickplayPath;
- var idString = item.Id.ToString("N", CultureInfo.InvariantCulture);
+ var id = item.Id.ToString("D", CultureInfo.InvariantCulture).AsSpan();
return saveWithMedia
? Path.Combine(item.ContainingFolderPath, Path.ChangeExtension(item.Path, ".trickplay"))
- : Path.Combine(basePath, idString);
+ : Path.Join(_config.ApplicationPaths.TrickplayPath, id[..2], id);
}
}
diff --git a/Emby.Server.Implementations/Library/Resolvers/ExtraResolver.cs b/Emby.Server.Implementations/Library/Resolvers/ExtraResolver.cs
index b4791b945..b9f9f2972 100644
--- a/Emby.Server.Implementations/Library/Resolvers/ExtraResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/ExtraResolver.cs
@@ -54,9 +54,9 @@ namespace Emby.Server.Implementations.Library.Resolvers
_ => _videoResolvers
};
- public bool TryGetExtraTypeForOwner(string path, VideoFileInfo ownerVideoFileInfo, [NotNullWhen(true)] out ExtraType? extraType)
+ public bool TryGetExtraTypeForOwner(string path, VideoFileInfo ownerVideoFileInfo, [NotNullWhen(true)] out ExtraType? extraType, string? libraryRoot = "")
{
- var extraResult = GetExtraInfo(path, _namingOptions);
+ var extraResult = GetExtraInfo(path, _namingOptions, libraryRoot);
if (extraResult.ExtraType is null)
{
extraType = null;
diff --git a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs
index 4debe722b..f1aeb1340 100644
--- a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs
@@ -270,11 +270,11 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
}
var videoInfos = files
- .Select(i => VideoResolver.Resolve(i.FullName, i.IsDirectory, NamingOptions, parseName))
+ .Select(i => VideoResolver.Resolve(i.FullName, i.IsDirectory, NamingOptions, parseName, parent.ContainingFolderPath))
.Where(f => f is not null)
.ToList();
- var resolverResult = VideoListResolver.Resolve(videoInfos, NamingOptions, supportMultiEditions, parseName);
+ var resolverResult = VideoListResolver.Resolve(videoInfos, NamingOptions, supportMultiEditions, parseName, parent.ContainingFolderPath);
var result = new MultiItemResolverResult
{
diff --git a/Emby.Server.Implementations/Library/SplashscreenPostScanTask.cs b/Emby.Server.Implementations/Library/SplashscreenPostScanTask.cs
index 0c9edd839..71ce3b601 100644
--- a/Emby.Server.Implementations/Library/SplashscreenPostScanTask.cs
+++ b/Emby.Server.Implementations/Library/SplashscreenPostScanTask.cs
@@ -11,7 +11,6 @@ using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Persistence;
using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Querying;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.Library;
@@ -78,15 +77,15 @@ public class SplashscreenPostScanTask : ILibraryPostScanTask
CollapseBoxSetItems = false,
Recursive = true,
DtoOptions = new DtoOptions(false),
- ImageTypes = new[] { imageType },
+ ImageTypes = [imageType],
Limit = 30,
// TODO max parental rating configurable
- MaxParentalRating = 10,
- OrderBy = new[]
- {
+ MaxParentalRating = new(10, null),
+ OrderBy =
+ [
(ItemSortBy.Random, SortOrder.Ascending)
- },
- IncludeItemTypes = new[] { BaseItemKind.Movie, BaseItemKind.Series }
+ ],
+ IncludeItemTypes = [BaseItemKind.Movie, BaseItemKind.Series]
});
}
}
diff --git a/Emby.Server.Implementations/Library/UserDataManager.cs b/Emby.Server.Implementations/Library/UserDataManager.cs
index 8b88b904b..be1d96bf0 100644
--- a/Emby.Server.Implementations/Library/UserDataManager.cs
+++ b/Emby.Server.Implementations/Library/UserDataManager.cs
@@ -1,14 +1,13 @@
#pragma warning disable RS0030 // Do not use banned APIs
using System;
-using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading;
+using BitFaster.Caching.Lru;
using Jellyfin.Database.Implementations;
using Jellyfin.Database.Implementations.Entities;
-using Jellyfin.Extensions;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
@@ -26,11 +25,9 @@ namespace Emby.Server.Implementations.Library
/// </summary>
public class UserDataManager : IUserDataManager
{
- private readonly ConcurrentDictionary<string, UserItemData> _userData =
- new ConcurrentDictionary<string, UserItemData>(StringComparer.OrdinalIgnoreCase);
-
private readonly IServerConfigurationManager _config;
private readonly IDbContextFactory<JellyfinDbContext> _repository;
+ private readonly FastConcurrentLru<string, UserItemData> _cache;
/// <summary>
/// Initializes a new instance of the <see cref="UserDataManager"/> class.
@@ -43,6 +40,7 @@ namespace Emby.Server.Implementations.Library
{
_config = config;
_repository = repository;
+ _cache = new FastConcurrentLru<string, UserItemData>(Environment.ProcessorCount, _config.Configuration.CacheSize, StringComparer.OrdinalIgnoreCase);
}
/// <inheritdoc />
@@ -81,7 +79,7 @@ namespace Emby.Server.Implementations.Library
var userId = user.InternalId;
var cacheKey = GetCacheKey(userId, item.Id);
- _userData.AddOrUpdate(cacheKey, userData, (_, _) => userData);
+ _cache.AddOrUpdate(cacheKey, userData);
UserDataSaved?.Invoke(this, new UserDataSaveEventArgs
{
@@ -182,7 +180,7 @@ namespace Emby.Server.Implementations.Library
{
var cacheKey = GetCacheKey(user.InternalId, itemId);
- if (_userData.TryGetValue(cacheKey, out var data))
+ if (_cache.TryGet(cacheKey, out var data))
{
return data;
}
@@ -197,7 +195,7 @@ namespace Emby.Server.Implementations.Library
};
}
- return _userData.GetOrAdd(cacheKey, data);
+ return _cache.GetOrAdd(cacheKey, _ => data);
}
private UserItemData? GetUserDataInternal(Guid userId, Guid itemId, List<string> keys)
diff --git a/Emby.Server.Implementations/Library/Validators/GenresValidator.cs b/Emby.Server.Implementations/Library/Validators/GenresValidator.cs
index e59c62e23..364770fcd 100644
--- a/Emby.Server.Implementations/Library/Validators/GenresValidator.cs
+++ b/Emby.Server.Implementations/Library/Validators/GenresValidator.cs
@@ -1,6 +1,9 @@
using System;
+using System.Globalization;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Data.Enums;
+using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Persistence;
using Microsoft.Extensions.Logging;
@@ -75,6 +78,26 @@ namespace Emby.Server.Implementations.Library.Validators
progress.Report(percent);
}
+ var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery
+ {
+ IncludeItemTypes = [BaseItemKind.Genre, BaseItemKind.MusicGenre],
+ IsDeadGenre = true,
+ IsLocked = false
+ });
+
+ foreach (var item in deadEntities)
+ {
+ _logger.LogInformation("Deleting dead {ItemType} {ItemId} {ItemName}", item.GetType().Name, item.Id.ToString("N", CultureInfo.InvariantCulture), item.Name);
+
+ _libraryManager.DeleteItem(
+ item,
+ new DeleteOptions
+ {
+ DeleteFileLocation = false
+ },
+ false);
+ }
+
progress.Report(100);
}
}
diff --git a/Emby.Server.Implementations/Localization/Core/af.json b/Emby.Server.Implementations/Localization/Core/af.json
index e89ede10b..1dce58923 100644
--- a/Emby.Server.Implementations/Localization/Core/af.json
+++ b/Emby.Server.Implementations/Localization/Core/af.json
@@ -129,5 +129,11 @@
"TaskAudioNormalizationDescription": "Skandeer lêers vir oudio-normaliseringsdata.",
"TaskAudioNormalization": "Odio Normalisering",
"TaskCleanCollectionsAndPlaylists": "Maak versamelings en snitlyste skoon",
- "TaskCleanCollectionsAndPlaylistsDescription": "Verwyder items uit versamelings en snitlyste wat nie meer bestaan nie."
+ "TaskCleanCollectionsAndPlaylistsDescription": "Verwyder items uit versamelings en snitlyste wat nie meer bestaan nie.",
+ "TaskDownloadMissingLyrics": "Laai tekorte lirieke af",
+ "TaskDownloadMissingLyricsDescription": "Laai lirieke af vir liedjies",
+ "TaskExtractMediaSegments": "Media Segment Skandeer",
+ "TaskExtractMediaSegmentsDescription": "Onttrek of verkry mediasegmente van MediaSegment-geaktiveerde inproppe.",
+ "TaskMoveTrickplayImages": "Migreer Trickplay Beeldligging",
+ "TaskMoveTrickplayImagesDescription": "Skuif ontstaande trickplay lêers volgens die biblioteekinstellings."
}
diff --git a/Emby.Server.Implementations/Localization/Core/de.json b/Emby.Server.Implementations/Localization/Core/de.json
index c38af5bf4..f5ae43bb5 100644
--- a/Emby.Server.Implementations/Localization/Core/de.json
+++ b/Emby.Server.Implementations/Localization/Core/de.json
@@ -43,7 +43,7 @@
"NameInstallFailed": "Installation von {0} fehlgeschlagen",
"NameSeasonNumber": "Staffel {0}",
"NameSeasonUnknown": "Staffel unbekannt",
- "NewVersionIsAvailable": "Eine neue Version von Jellyfin-Server steht zum Download bereit.",
+ "NewVersionIsAvailable": "Eine neue Jellyfin-Serverversion steht zum Download bereit.",
"NotificationOptionApplicationUpdateAvailable": "Anwendungsaktualisierung verfügbar",
"NotificationOptionApplicationUpdateInstalled": "Anwendungsaktualisierung installiert",
"NotificationOptionAudioPlayback": "Audiowiedergabe gestartet",
@@ -72,12 +72,12 @@
"ServerNameNeedsToBeRestarted": "{0} muss neu gestartet werden",
"Shows": "Serien",
"Songs": "Lieder",
- "StartupEmbyServerIsLoading": "Jellyfin-Server startet, bitte versuche es gleich noch einmal.",
+ "StartupEmbyServerIsLoading": "Jellyfin-Server lädt. Bitte versuche es gleich noch einmal.",
"SubtitleDownloadFailureForItem": "Download der Untertitel fehlgeschlagen für {0}",
"SubtitleDownloadFailureFromForItem": "Untertitel von {0} für {1} konnten nicht heruntergeladen werden",
"Sync": "Synchronisation",
"System": "System",
- "TvShows": "TV-Sendungen",
+ "TvShows": "TV-Serien",
"User": "Benutzer",
"UserCreatedWithName": "Benutzer {0} wurde erstellt",
"UserDeletedWithName": "Benutzer {0} wurde gelöscht",
@@ -92,30 +92,30 @@
"ValueHasBeenAddedToLibrary": "{0} wurde deiner Bibliothek hinzugefügt",
"ValueSpecialEpisodeName": "Extra - {0}",
"VersionNumber": "Version {0}",
- "TaskDownloadMissingSubtitlesDescription": "Suche im Internet basierend auf den Metadaten-Einstellungen nach fehlenden Untertiteln.",
- "TaskDownloadMissingSubtitles": "Lade fehlende Untertitel herunter",
- "TaskRefreshChannelsDescription": "Aktualisiere Internet-Kanal-Informationen.",
- "TaskRefreshChannels": "Aktualisiere Kanäle",
- "TaskCleanTranscodeDescription": "Löscht Transkodierdateien, die älter als einen Tag sind.",
- "TaskCleanTranscode": "Räume Transkodierungs-Verzeichnis auf",
+ "TaskDownloadMissingSubtitlesDescription": "Sucht im Internet basierend auf den Metadaten-Einstellungen nach fehlenden Untertiteln.",
+ "TaskDownloadMissingSubtitles": "Fehlende Untertitel herunterladen",
+ "TaskRefreshChannelsDescription": "Aktualisiert Internet-Kanal-Informationen.",
+ "TaskRefreshChannels": "Kanäle aktualisieren",
+ "TaskCleanTranscodeDescription": "Löscht Transkodierungsdateien, die älter als einen Tag sind.",
+ "TaskCleanTranscode": "Transkodierungs-Verzeichnis aufräumen",
"TaskUpdatePluginsDescription": "Lädt Updates für Plugins herunter, welche für automatische Updates konfiguriert sind und installiert diese.",
- "TaskUpdatePlugins": "Aktualisiere Plugins",
+ "TaskUpdatePlugins": "Plugins aktualisieren",
"TaskRefreshPeopleDescription": "Aktualisiert Metadaten für Schauspieler und Regisseure in deinen Bibliotheken.",
- "TaskRefreshPeople": "Aktualisiere Personen",
+ "TaskRefreshPeople": "Personen aktualisieren",
"TaskCleanLogsDescription": "Lösche Log-Dateien, die älter als {0} Tage sind.",
- "TaskCleanLogs": "Räumt Log-Verzeichnis auf",
- "TaskRefreshLibraryDescription": "Scannt alle Bibliotheken nach neu hinzugefügten Dateien und aktualisiere Metadaten.",
- "TaskRefreshLibrary": "Scanne Medien-Bibliothek",
+ "TaskCleanLogs": "Log-Verzeichnis aufräumen",
+ "TaskRefreshLibraryDescription": "Durchsucht alle Bibliotheken nach neu hinzugefügten Dateien und aktualisiert Metadaten.",
+ "TaskRefreshLibrary": "Medien-Bibliothek scannen",
"TaskRefreshChapterImagesDescription": "Erstellt Vorschaubilder für Videos, die Kapitel besitzen.",
- "TaskRefreshChapterImages": "Extrahiere Kapitel-Bilder",
- "TaskCleanCacheDescription": "Löscht nicht mehr benötigte Zwischenspeicherdateien.",
- "TaskCleanCache": "Leere Zwischenspeicher",
+ "TaskRefreshChapterImages": "Kapitel-Bilder extrahieren",
+ "TaskCleanCacheDescription": "Löscht vom System nicht mehr benötigte Zwischenspeicherdateien.",
+ "TaskCleanCache": "Zwischenspeicher-Verzeichnis aufräumen",
"TasksChannelsCategory": "Internet-Kanäle",
"TasksApplicationCategory": "Anwendung",
"TasksLibraryCategory": "Bibliothek",
"TasksMaintenanceCategory": "Wartung",
"TaskCleanActivityLogDescription": "Löscht Aktivitätsprotokolleinträge, die älter als das konfigurierte Alter sind.",
- "TaskCleanActivityLog": "Aktivitätsprotokoll aufräumen",
+ "TaskCleanActivityLog": "Aktivitätsprotokolle aufräumen",
"Undefined": "Undefiniert",
"Forced": "Erzwungen",
"Default": "Standard",
@@ -128,12 +128,12 @@
"TaskRefreshTrickplayImages": "Trickplay-Bilder generieren",
"TaskRefreshTrickplayImagesDescription": "Erstellt ein Trickplay-Vorschauen für Videos in aktivierten Bibliotheken.",
"TaskCleanCollectionsAndPlaylists": "Sammlungen und Playlisten aufräumen",
- "TaskCleanCollectionsAndPlaylistsDescription": "Lösche nicht mehr vorhandene Einträge aus den Sammlungen und Playlisten.",
+ "TaskCleanCollectionsAndPlaylistsDescription": "Löscht nicht mehr vorhandene Einträge aus den Sammlungen und Playlisten.",
"TaskAudioNormalization": "Audio Normalisierung",
"TaskAudioNormalizationDescription": "Durchsucht Dateien nach Audionormalisierungsdaten.",
"TaskDownloadMissingLyricsDescription": "Lädt Songtexte herunter",
"TaskDownloadMissingLyrics": "Fehlende Songtexte herunterladen",
- "TaskExtractMediaSegments": "Scanne Mediensegmente",
+ "TaskExtractMediaSegments": "Mediensegmente scannen",
"TaskExtractMediaSegmentsDescription": "Extrahiert oder empfängt Mediensegmente von Plugins die Mediensegmente nutzen.",
"TaskMoveTrickplayImages": "Verzeichnis für Trickplay-Bilder migrieren",
"TaskMoveTrickplayImagesDescription": "Trickplay-Bilder werden entsprechend der Bibliothekseinstellungen verschoben."
diff --git a/Emby.Server.Implementations/Localization/Core/ms.json b/Emby.Server.Implementations/Localization/Core/ms.json
index c64bcda04..a3fc7881e 100644
--- a/Emby.Server.Implementations/Localization/Core/ms.json
+++ b/Emby.Server.Implementations/Localization/Core/ms.json
@@ -9,14 +9,14 @@
"Channels": "Saluran",
"ChapterNameValue": "Bab {0}",
"Collections": "Koleksi",
- "DeviceOfflineWithName": "{0} telah diputuskan sambungan",
+ "DeviceOfflineWithName": "{0} telah dinyahsambung",
"DeviceOnlineWithName": "{0} telah disambung",
- "FailedLoginAttemptWithUserName": "Percubaan log masuk daripada {0} gagal",
+ "FailedLoginAttemptWithUserName": "Percubaan gagal log masuk daripada {0}",
"Favorites": "Kegemaran",
- "Folders": "Fail-fail",
+ "Folders": "Folder-folder",
"Genres": "Genre-genre",
- "HeaderAlbumArtists": "Album Artis-artis",
- "HeaderContinueWatching": "Terus Menonton",
+ "HeaderAlbumArtists": "Album artis-artis",
+ "HeaderContinueWatching": "Teruskan Menonton",
"HeaderFavoriteAlbums": "Album-album Kegemaran",
"HeaderFavoriteArtists": "Artis-artis Kegemaran",
"HeaderFavoriteEpisodes": "Episod-episod Kegemaran",
@@ -25,26 +25,26 @@
"HeaderLiveTV": "TV Siaran Langsung",
"HeaderNextUp": "Seterusnya",
"HeaderRecordingGroups": "Kumpulan-kumpulan Rakaman",
- "HomeVideos": "Video Personal",
- "Inherit": "Mewarisi",
- "ItemAddedWithName": "{0} telah ditambahkan ke dalam pustaka",
+ "HomeVideos": "Video Peribadi",
+ "Inherit": "Warisi",
+ "ItemAddedWithName": "{0} telah ditambah ke dalam pustaka",
"ItemRemovedWithName": "{0} telah dibuang daripada pustaka",
"LabelIpAddressValue": "Alamat IP: {0}",
"LabelRunningTimeValue": "Masa berjalan: {0}",
- "Latest": "Terbaru",
- "MessageApplicationUpdated": "Jellyfin Server telah dikemas kini",
- "MessageApplicationUpdatedTo": "Jellyfin Server telah dikemas kini ke {0}",
- "MessageNamedServerConfigurationUpdatedWithValue": "Konfigurasi pelayan di bahagian {0} telah dikemas kini",
+ "Latest": "Terbaharu",
+ "MessageApplicationUpdated": "Pelayan Jellyfin telah dikemas kini",
+ "MessageApplicationUpdatedTo": "Pelayan Jellyfin telah dikemas kini ke {0}",
+ "MessageNamedServerConfigurationUpdatedWithValue": "Konfigurasi pelayan bahagian {0} telah dikemas kini",
"MessageServerConfigurationUpdated": "Konfigurasi pelayan telah dikemas kini",
"MixedContent": "Kandungan campuran",
"Movies": "Filem-filem",
"Music": "Muzik",
"MusicVideos": "Video Muzik",
"NameInstallFailed": "{0} pemasangan gagal",
- "NameSeasonNumber": "Musim {0}",
+ "NameSeasonNumber": "Musim ke-{0}",
"NameSeasonUnknown": "Musim Tidak Diketahui",
- "NewVersionIsAvailable": "Versi terbaru Jellyfin Server bersedia untuk dimuat turunkan.",
- "NotificationOptionApplicationUpdateAvailable": "Kemas kini aplikasi telah sedia",
+ "NewVersionIsAvailable": "Versi terbaharu Pelayan Jellyfin telah tersedia untuk dimuat turun.",
+ "NotificationOptionApplicationUpdateAvailable": "Kemas kini aplikasi telah tersedia",
"NotificationOptionApplicationUpdateInstalled": "Kemas kini aplikasi telah dipasang",
"NotificationOptionAudioPlayback": "Ulangmain audio bermula",
"NotificationOptionAudioPlaybackStopped": "Ulangmain audio dihentikan",
@@ -98,8 +98,8 @@
"TasksLibraryCategory": "Perpustakaan",
"TasksMaintenanceCategory": "Penyelenggaraan",
"Undefined": "Tidak ditentukan",
- "Forced": "Paksa",
- "Default": "Asal",
+ "Forced": "Dipaksa",
+ "Default": "Lalai",
"TaskCleanCache": "Bersihkan Direktori Cache",
"TaskCleanActivityLogDescription": "Padamkan entri log aktiviti yang lebih tua daripada usia yang dikonfigurasi.",
"TaskRefreshPeople": "Segarkan Orang",
diff --git a/Emby.Server.Implementations/Localization/Core/te.json b/Emby.Server.Implementations/Localization/Core/te.json
index 7d4422d62..1fa2a3cc5 100644
--- a/Emby.Server.Implementations/Localization/Core/te.json
+++ b/Emby.Server.Implementations/Localization/Core/te.json
@@ -51,5 +51,13 @@
"Latest": "తాజా",
"NameInstallFailed": "{0} ఇన్‌స్టాలేషన్ విఫలమైంది",
"NameSeasonUnknown": "భాగం తెలియదు",
- "NotificationOptionApplicationUpdateAvailable": "అప్లికేషన్ అప్‌డేట్ అందుబాటులో ఉంది"
+ "NotificationOptionApplicationUpdateAvailable": "అప్లికేషన్ అప్‌డేట్ అందుబాటులో ఉంది",
+ "NameSeasonNumber": "సీజన్ {0}",
+ "NotificationOptionAudioPlaybackStopped": "ఆడియో ఆడటం ఆగిపోయింది",
+ "NotificationOptionNewLibraryContent": "కొత్త కంటెంట్ జోడించబడింది",
+ "MixedContent": "వివిధ రకాల కంటెంట్",
+ "NotificationOptionAudioPlayback": "ఆడియో ప్లే కావడం మొదలైంది",
+ "NotificationOptionCameraImageUploaded": "కెమెరా చిత్రాన్ని అప్లోడ్ చేశారు",
+ "NotificationOptionInstallationFailed": "ఇన్స్టాలేషన్ విఫలమైంది",
+ "NotificationOptionServerRestartRequired": "సర్వర్ రీస్టార్ట్ అవసరం"
}
diff --git a/Emby.Server.Implementations/Localization/LocalizationManager.cs b/Emby.Server.Implementations/Localization/LocalizationManager.cs
index 754a01329..17db7ad4c 100644
--- a/Emby.Server.Implementations/Localization/LocalizationManager.cs
+++ b/Emby.Server.Implementations/Localization/LocalizationManager.cs
@@ -1,7 +1,8 @@
using System;
using System.Collections.Concurrent;
+using System.Collections.Frozen;
using System.Collections.Generic;
-using System.Globalization;
+using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Reflection;
@@ -26,20 +27,20 @@ namespace Emby.Server.Implementations.Localization
private const string CulturesPath = "Emby.Server.Implementations.Localization.iso6392.txt";
private const string CountriesPath = "Emby.Server.Implementations.Localization.countries.json";
private static readonly Assembly _assembly = typeof(LocalizationManager).Assembly;
- private static readonly string[] _unratedValues = { "n/a", "unrated", "not rated", "nr" };
+ private static readonly string[] _unratedValues = ["n/a", "unrated", "not rated", "nr"];
private readonly IServerConfigurationManager _configurationManager;
private readonly ILogger<LocalizationManager> _logger;
- private readonly Dictionary<string, Dictionary<string, ParentalRating>> _allParentalRatings =
- new Dictionary<string, Dictionary<string, ParentalRating>>(StringComparer.OrdinalIgnoreCase);
+ private readonly Dictionary<string, Dictionary<string, ParentalRatingScore?>> _allParentalRatings = new(StringComparer.OrdinalIgnoreCase);
- private readonly ConcurrentDictionary<string, Dictionary<string, string>> _dictionaries =
- new ConcurrentDictionary<string, Dictionary<string, string>>(StringComparer.OrdinalIgnoreCase);
+ private readonly ConcurrentDictionary<string, Dictionary<string, string>> _dictionaries = new(StringComparer.OrdinalIgnoreCase);
private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
- private List<CultureDto> _cultures = new List<CultureDto>();
+ private List<CultureDto> _cultures = [];
+
+ private FrozenDictionary<string, string> _iso6392BtoT = null!;
/// <summary>
/// Initializes a new instance of the <see cref="LocalizationManager" /> class.
@@ -68,35 +69,26 @@ namespace Emby.Server.Implementations.Localization
continue;
}
- string countryCode = resource.Substring(RatingsPath.Length, 2);
- var dict = new Dictionary<string, ParentalRating>(StringComparer.OrdinalIgnoreCase);
-
- var stream = _assembly.GetManifestResourceStream(resource);
- await using (stream!.ConfigureAwait(false)) // shouldn't be null here, we just got the resource path from Assembly.GetManifestResourceNames()
+ using var stream = _assembly.GetManifestResourceStream(resource);
+ if (stream is not null)
{
- using var reader = new StreamReader(stream!);
- await foreach (var line in reader.ReadAllLinesAsync().ConfigureAwait(false))
+ var ratingSystem = await JsonSerializer.DeserializeAsync<ParentalRatingSystem>(stream, _jsonOptions).ConfigureAwait(false)
+ ?? throw new InvalidOperationException($"Invalid resource path: '{CountriesPath}'");
+
+ var dict = new Dictionary<string, ParentalRatingScore?>();
+ if (ratingSystem.Ratings is not null)
{
- if (string.IsNullOrWhiteSpace(line))
+ foreach (var ratingEntry in ratingSystem.Ratings)
{
- continue;
+ foreach (var ratingString in ratingEntry.RatingStrings)
+ {
+ dict[ratingString] = ratingEntry.RatingScore;
+ }
}
- string[] parts = line.Split(',');
- if (parts.Length == 2
- && int.TryParse(parts[1], NumberStyles.Integer, CultureInfo.InvariantCulture, out var value))
- {
- var name = parts[0];
- dict.Add(name, new ParentalRating(name, value));
- }
- else
- {
- _logger.LogWarning("Malformed line in ratings file for country {CountryCode}", countryCode);
- }
+ _allParentalRatings[ratingSystem.CountryCode] = dict;
}
}
-
- _allParentalRatings[countryCode] = dict;
}
await LoadCultures().ConfigureAwait(false);
@@ -111,22 +103,30 @@ namespace Emby.Server.Implementations.Localization
private async Task LoadCultures()
{
- List<CultureDto> list = new List<CultureDto>();
+ List<CultureDto> list = [];
+ Dictionary<string, string> iso6392BtoTdict = new Dictionary<string, string>();
- await using var stream = _assembly.GetManifestResourceStream(CulturesPath)
- ?? throw new InvalidOperationException($"Invalid resource path: '{CulturesPath}'");
- using var reader = new StreamReader(stream);
- await foreach (var line in reader.ReadAllLinesAsync().ConfigureAwait(false))
+ using var stream = _assembly.GetManifestResourceStream(CulturesPath);
+ if (stream is null)
{
- if (string.IsNullOrWhiteSpace(line))
+ throw new InvalidOperationException($"Invalid resource path: '{CulturesPath}'");
+ }
+ else
+ {
+ using var reader = new StreamReader(stream);
+ await foreach (var line in reader.ReadAllLinesAsync().ConfigureAwait(false))
{
- continue;
- }
+ if (string.IsNullOrWhiteSpace(line))
+ {
+ continue;
+ }
- var parts = line.Split('|');
+ var parts = line.Split('|');
+ if (parts.Length != 5)
+ {
+ throw new InvalidDataException($"Invalid culture data found at: '{line}'");
+ }
- if (parts.Length == 5)
- {
string name = parts[3];
if (string.IsNullOrWhiteSpace(name))
{
@@ -139,21 +139,26 @@ namespace Emby.Server.Implementations.Localization
continue;
}
- string[] threeletterNames;
+ string[] threeLetterNames;
if (string.IsNullOrWhiteSpace(parts[1]))
{
- threeletterNames = new[] { parts[0] };
+ threeLetterNames = [parts[0]];
}
else
{
- threeletterNames = new[] { parts[0], parts[1] };
+ threeLetterNames = [parts[0], parts[1]];
+
+ // In cases where there are two TLN the first one is ISO 639-2/T and the second one is ISO 639-2/B
+ // We need ISO 639-2/T for the .NET cultures so we cultivate a dictionary for the translation B->T
+ iso6392BtoTdict.TryAdd(parts[1], parts[0]);
}
- list.Add(new CultureDto(name, name, twoCharName, threeletterNames));
+ list.Add(new CultureDto(name, name, twoCharName, threeLetterNames));
}
- }
- _cultures = list;
+ _cultures = list;
+ _iso6392BtoT = iso6392BtoTdict.ToFrozenDictionary(StringComparer.OrdinalIgnoreCase);
+ }
}
/// <inheritdoc />
@@ -176,82 +181,80 @@ namespace Emby.Server.Implementations.Localization
}
/// <inheritdoc />
- public IEnumerable<CountryInfo> GetCountries()
+ public IReadOnlyList<CountryInfo> GetCountries()
{
- using StreamReader reader = new StreamReader(
- _assembly.GetManifestResourceStream(CountriesPath) ?? throw new InvalidOperationException($"Invalid resource path: '{CountriesPath}'"));
- return JsonSerializer.Deserialize<IEnumerable<CountryInfo>>(reader.ReadToEnd(), _jsonOptions)
- ?? throw new InvalidOperationException($"Resource contains invalid data: '{CountriesPath}'");
+ using var stream = _assembly.GetManifestResourceStream(CountriesPath) ?? throw new InvalidOperationException($"Invalid resource path: '{CountriesPath}'");
+
+ return JsonSerializer.Deserialize<IReadOnlyList<CountryInfo>>(stream, _jsonOptions) ?? [];
}
/// <inheritdoc />
- public IEnumerable<ParentalRating> GetParentalRatings()
+ public IReadOnlyList<ParentalRating> GetParentalRatings()
{
// Use server default language for ratings
// Fall back to empty list if there are no parental ratings for that language
- var ratings = GetParentalRatingsDictionary()?.Values.ToList()
- ?? new List<ParentalRating>();
+ var ratings = GetParentalRatingsDictionary()?.Select(x => new ParentalRating(x.Key, x.Value)).ToList() ?? [];
// Add common ratings to ensure them being available for selection
// Based on the US rating system due to it being the main source of rating in the metadata providers
// Unrated
- if (!ratings.Any(x => x.Value is null))
+ if (!ratings.Any(x => x is null))
{
- ratings.Add(new ParentalRating("Unrated", null));
+ ratings.Add(new("Unrated", null));
}
// Minimum rating possible
- if (ratings.All(x => x.Value != 0))
+ if (ratings.All(x => x.RatingScore?.Score != 0))
{
- ratings.Add(new ParentalRating("Approved", 0));
+ ratings.Add(new("Approved", new(0, null)));
}
// Matches PG (this has different age restrictions depending on country)
- if (ratings.All(x => x.Value != 10))
+ if (ratings.All(x => x.RatingScore?.Score != 10))
{
- ratings.Add(new ParentalRating("10", 10));
+ ratings.Add(new("10", new(10, null)));
}
// Matches PG-13
- if (ratings.All(x => x.Value != 13))
+ if (ratings.All(x => x.RatingScore?.Score != 13))
{
- ratings.Add(new ParentalRating("13", 13));
+ ratings.Add(new("13", new(13, null)));
}
// Matches TV-14
- if (ratings.All(x => x.Value != 14))
+ if (ratings.All(x => x.RatingScore?.Score != 14))
{
- ratings.Add(new ParentalRating("14", 14));
+ ratings.Add(new("14", new(14, null)));
}
// Catchall if max rating of country is less than 21
// Using 21 instead of 18 to be sure to allow access to all rated content except adult and banned
- if (!ratings.Any(x => x.Value >= 21))
+ if (!ratings.Any(x => x.RatingScore?.Score >= 21))
{
- ratings.Add(new ParentalRating("21", 21));
+ ratings.Add(new ParentalRating("21", new(21, null)));
}
// A lot of countries don't explicitly have a separate rating for adult content
- if (ratings.All(x => x.Value != 1000))
+ if (ratings.All(x => x.RatingScore?.Score != 1000))
{
- ratings.Add(new ParentalRating("XXX", 1000));
+ ratings.Add(new ParentalRating("XXX", new(1000, null)));
}
// A lot of countries don't explicitly have a separate rating for banned content
- if (ratings.All(x => x.Value != 1001))
+ if (ratings.All(x => x.RatingScore?.Score != 1001))
{
- ratings.Add(new ParentalRating("Banned", 1001));
+ ratings.Add(new ParentalRating("Banned", new(1001, null)));
}
- return ratings.OrderBy(r => r.Value);
+ return [.. ratings.OrderBy(r => r.RatingScore?.Score).ThenBy(r => r.RatingScore?.SubScore)];
}
/// <summary>
/// Gets the parental ratings dictionary.
/// </summary>
/// <param name="countryCode">The optional two letter ISO language string.</param>
- /// <returns><see cref="Dictionary{String, ParentalRating}" />.</returns>
- private Dictionary<string, ParentalRating>? GetParentalRatingsDictionary(string? countryCode = null)
+ /// <returns><see cref="Dictionary{String, ParentalRatingScore}" />.</returns>
+ private Dictionary<string, ParentalRatingScore?>? GetParentalRatingsDictionary(string? countryCode = null)
{
// Fallback to server default if no country code is specified.
if (string.IsNullOrEmpty(countryCode))
@@ -268,7 +271,7 @@ namespace Emby.Server.Implementations.Localization
}
/// <inheritdoc />
- public int? GetRatingLevel(string rating, string? countryCode = null)
+ public ParentalRatingScore? GetRatingScore(string rating, string? countryCode = null)
{
ArgumentException.ThrowIfNullOrEmpty(rating);
@@ -278,11 +281,11 @@ namespace Emby.Server.Implementations.Localization
return null;
}
- // Convert integers directly
+ // Convert ints directly
// This may override some of the locale specific age ratings (but those always map to the same age)
if (int.TryParse(rating, out var ratingAge))
{
- return ratingAge;
+ return new(ratingAge, null);
}
// Fairly common for some users to have "Rated R" in their rating field
@@ -295,9 +298,9 @@ namespace Emby.Server.Implementations.Localization
if (!string.IsNullOrEmpty(countryCode))
{
var ratingsDictionary = GetParentalRatingsDictionary(countryCode);
- if (ratingsDictionary is not null && ratingsDictionary.TryGetValue(rating, out ParentalRating? value))
+ if (ratingsDictionary is not null && ratingsDictionary.TryGetValue(rating, out ParentalRatingScore? value))
{
- return value.Value;
+ return value;
}
}
else
@@ -305,9 +308,9 @@ namespace Emby.Server.Implementations.Localization
// Fall back to server default language for ratings check
// If it has no ratings, use the US ratings
var ratingsDictionary = GetParentalRatingsDictionary() ?? GetParentalRatingsDictionary("us");
- if (ratingsDictionary is not null && ratingsDictionary.TryGetValue(rating, out ParentalRating? value))
+ if (ratingsDictionary is not null && ratingsDictionary.TryGetValue(rating, out ParentalRatingScore? value))
{
- return value.Value;
+ return value;
}
}
@@ -316,7 +319,7 @@ namespace Emby.Server.Implementations.Localization
{
if (dictionary.TryGetValue(rating, out var value))
{
- return value.Value;
+ return value;
}
}
@@ -326,7 +329,7 @@ namespace Emby.Server.Implementations.Localization
var ratingLevelRightPart = rating.AsSpan().RightPart(':');
if (ratingLevelRightPart.Length != 0)
{
- return GetRatingLevel(ratingLevelRightPart.ToString());
+ return GetRatingScore(ratingLevelRightPart.ToString());
}
}
@@ -342,7 +345,7 @@ namespace Emby.Server.Implementations.Localization
if (ratingLevelRightPart.Length != 0)
{
// Check rating system of culture
- return GetRatingLevel(ratingLevelRightPart.ToString(), culture?.TwoLetterISOLanguageName);
+ return GetRatingScore(ratingLevelRightPart.ToString(), culture?.TwoLetterISOLanguageName);
}
}
@@ -406,7 +409,7 @@ namespace Emby.Server.Implementations.Localization
private async Task CopyInto(IDictionary<string, string> dictionary, string resourcePath)
{
- await using var stream = _assembly.GetManifestResourceStream(resourcePath);
+ using var stream = _assembly.GetManifestResourceStream(resourcePath);
// If a Culture doesn't have a translation the stream will be null and it defaults to en-us further up the chain
if (stream is null)
{
@@ -414,12 +417,7 @@ namespace Emby.Server.Implementations.Localization
return;
}
- var dict = await JsonSerializer.DeserializeAsync<Dictionary<string, string>>(stream, _jsonOptions).ConfigureAwait(false);
- if (dict is null)
- {
- throw new InvalidOperationException($"Resource contains invalid data: '{stream}'");
- }
-
+ var dict = await JsonSerializer.DeserializeAsync<Dictionary<string, string>>(stream, _jsonOptions).ConfigureAwait(false) ?? throw new InvalidOperationException($"Resource contains invalid data: '{stream}'");
foreach (var key in dict.Keys)
{
dictionary[key] = dict[key];
@@ -517,5 +515,26 @@ namespace Emby.Server.Implementations.Localization
yield return new LocalizationOption("漢語 (繁體字)", "zh-TW");
yield return new LocalizationOption("廣東話 (香港)", "zh-HK");
}
+
+ /// <inheritdoc />
+ public bool TryGetISO6392TFromB(string isoB, [NotNullWhen(true)] out string? isoT)
+ {
+ // Unlikely case the dictionary is not (yet) initialized properly
+ if (_iso6392BtoT == null)
+ {
+ isoT = null;
+ return false;
+ }
+
+ var result = _iso6392BtoT.TryGetValue(isoB, out isoT) && !string.IsNullOrEmpty(isoT);
+
+ // Ensure the ISO code being null if the result is false
+ if (!result)
+ {
+ isoT = null;
+ }
+
+ return result;
+ }
}
}
diff --git a/Emby.Server.Implementations/Localization/Ratings/0-prefer.csv b/Emby.Server.Implementations/Localization/Ratings/0-prefer.csv
deleted file mode 100644
index 36886ba76..000000000
--- a/Emby.Server.Implementations/Localization/Ratings/0-prefer.csv
+++ /dev/null
@@ -1,11 +0,0 @@
-E,0
-EC,0
-T,7
-M,18
-AO,18
-UR,18
-RP,18
-X,1000
-XX,1000
-XXX,1000
-XXXX,1000
diff --git a/Emby.Server.Implementations/Localization/Ratings/0-prefer.json b/Emby.Server.Implementations/Localization/Ratings/0-prefer.json
new file mode 100644
index 000000000..b39015161
--- /dev/null
+++ b/Emby.Server.Implementations/Localization/Ratings/0-prefer.json
@@ -0,0 +1,34 @@
+{
+ "countryCode": "0-prefer",
+ "supportsSubScores": false,
+ "ratings": [
+ {
+ "ratingStrings": ["E", "EC"],
+ "ratingScore": {
+ "score": 0,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["T"],
+ "ratingScore": {
+ "score": 7,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["M", "AO", "UR", "RP"],
+ "ratingScore": {
+ "score": 18,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["X", "XX", "XXX", "XXXX"],
+ "ratingScore": {
+ "score": 1000,
+ "subScore": null
+ }
+ }
+ ]
+}
diff --git a/Emby.Server.Implementations/Localization/Ratings/au.csv b/Emby.Server.Implementations/Localization/Ratings/au.csv
deleted file mode 100644
index 6e12759a4..000000000
--- a/Emby.Server.Implementations/Localization/Ratings/au.csv
+++ /dev/null
@@ -1,17 +0,0 @@
-Exempt,0
-G,0
-7+,7
-PG,15
-M,15
-MA,15
-MA15+,15
-MA 15+,15
-16+,16
-R,18
-R18+,18
-R 18+,18
-18+,18
-X18+,1000
-X 18+,1000
-X,1000
-RC,1001
diff --git a/Emby.Server.Implementations/Localization/Ratings/au.json b/Emby.Server.Implementations/Localization/Ratings/au.json
new file mode 100644
index 000000000..a563df899
--- /dev/null
+++ b/Emby.Server.Implementations/Localization/Ratings/au.json
@@ -0,0 +1,69 @@
+{
+ "countryCode": "au",
+ "supportsSubScores": true,
+ "ratings": [
+ {
+ "ratingStrings": ["Exempt", "G"],
+ "ratingScore": {
+ "score": 0,
+ "subScore": 0
+ }
+ },
+ {
+ "ratingStrings": ["7+"],
+ "ratingScore": {
+ "score": 7,
+ "subScore": 0
+ }
+ },
+ {
+ "ratingStrings": ["PG"],
+ "ratingScore": {
+ "score": 15,
+ "subScore": 1
+ }
+ },
+ {
+ "ratingStrings": ["M"],
+ "ratingScore": {
+ "score": 15,
+ "subScore": 2
+ }
+ },
+ {
+ "ratingStrings": ["MA", "MA 15+", "MA15+"],
+ "ratingScore": {
+ "score": 15,
+ "subScore": 3
+ }
+ },
+ {
+ "ratingStrings": ["16+"],
+ "ratingScore": {
+ "score": 16,
+ "subScore": 0
+ }
+ },
+ {
+ "ratingStrings": ["18+", "R", "R18+", "R 18+"],
+ "ratingScore": {
+ "score": 18,
+ "subScore": 1
+ }
+ },
+ {
+ "ratingStrings": ["X", "X18", "X 18"],
+ "ratingScore": {
+ "score": 1000,
+ "subScore": 0
+ }
+ },
+ {
+ "ratingStrings": ["RC"],
+ "ratingScore": {
+ "score": 1001,
+ "subScore": 0
+ }
+ }
+ ]
+}
diff --git a/Emby.Server.Implementations/Localization/Ratings/be.csv b/Emby.Server.Implementations/Localization/Ratings/be.csv
deleted file mode 100644
index d171a7132..000000000
--- a/Emby.Server.Implementations/Localization/Ratings/be.csv
+++ /dev/null
@@ -1,11 +0,0 @@
-AL,0
-KT,0
-TOUS,0
-MG6,6
-6,6
-9,9
-KNT,12
-12,12
-14,14
-16,16
-18,18
diff --git a/Emby.Server.Implementations/Localization/Ratings/be.json b/Emby.Server.Implementations/Localization/Ratings/be.json
new file mode 100644
index 000000000..18ea2c260
--- /dev/null
+++ b/Emby.Server.Implementations/Localization/Ratings/be.json
@@ -0,0 +1,55 @@
+{
+ "countryCode": "be",
+ "supportsSubScores": false,
+ "ratings": [
+ {
+ "ratingStrings": ["AL", "KT", "TOUS"],
+ "ratingScore": {
+ "score": 0,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["6", "MG6"],
+ "ratingScore": {
+ "score": 6,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["9"],
+ "ratingScore": {
+ "score": 9,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["12", "KNT"],
+ "ratingScore": {
+ "score": 12,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["14"],
+ "ratingScore": {
+ "score": 14,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["16"],
+ "ratingScore": {
+ "score": 16,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["18"],
+ "ratingScore": {
+ "score": 18,
+ "subScore": null
+ }
+ }
+ ]
+}
diff --git a/Emby.Server.Implementations/Localization/Ratings/br.csv b/Emby.Server.Implementations/Localization/Ratings/br.csv
deleted file mode 100644
index f6053c88c..000000000
--- a/Emby.Server.Implementations/Localization/Ratings/br.csv
+++ /dev/null
@@ -1,14 +0,0 @@
-Livre,0
-L,0
-AL,0
-ER,10
-10,10
-A10,10
-12,12
-A12,12
-14,14
-A14,14
-16,16
-A16,16
-18,18
-A18,18
diff --git a/Emby.Server.Implementations/Localization/Ratings/br.json b/Emby.Server.Implementations/Localization/Ratings/br.json
new file mode 100644
index 000000000..f455b6643
--- /dev/null
+++ b/Emby.Server.Implementations/Localization/Ratings/br.json
@@ -0,0 +1,55 @@
+{
+ "countryCode": "br",
+ "supportsSubScores": false,
+ "ratings": [
+ {
+ "ratingStrings": ["L", "AL", "Livre"],
+ "ratingScore": {
+ "score": 0,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["9"],
+ "ratingScore": {
+ "score": 9,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["10", "A10", "ER"],
+ "ratingScore": {
+ "score": 10,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["12", "A12"],
+ "ratingScore": {
+ "score": 12,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["14", "A14"],
+ "ratingScore": {
+ "score": 14,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["16", "A16"],
+ "ratingScore": {
+ "score": 16,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["18", "A18"],
+ "ratingScore": {
+ "score": 18,
+ "subScore": null
+ }
+ }
+ ]
+}
diff --git a/Emby.Server.Implementations/Localization/Ratings/ca.csv b/Emby.Server.Implementations/Localization/Ratings/ca.csv
deleted file mode 100644
index 41dbda134..000000000
--- a/Emby.Server.Implementations/Localization/Ratings/ca.csv
+++ /dev/null
@@ -1,18 +0,0 @@
-E,0
-G,0
-TV-Y,0
-TV-G,0
-TV-Y7,7
-TV-Y7-FV,7
-PG,9
-TV-PG,9
-TV-14,14
-14A,14
-16+,16
-NC-17,17
-R,18
-TV-MA,18
-18A,18
-18+,18
-A,1000
-Prohibited,1001
diff --git a/Emby.Server.Implementations/Localization/Ratings/ca.json b/Emby.Server.Implementations/Localization/Ratings/ca.json
new file mode 100644
index 000000000..fa43a8f2b
--- /dev/null
+++ b/Emby.Server.Implementations/Localization/Ratings/ca.json
@@ -0,0 +1,90 @@
+{
+ "countryCode": "ca",
+ "supportsSubScores": true,
+ "ratings": [
+ {
+ "ratingStrings": ["E", "G", "TV-Y", "TV-G"],
+ "ratingScore": {
+ "score": 0,
+ "subScore": 0
+ }
+ },
+ {
+ "ratingStrings": ["TV-Y7"],
+ "ratingScore": {
+ "score": 7,
+ "subScore": 0
+ }
+ },
+ {
+ "ratingStrings": ["TV-Y7-FV"],
+ "ratingScore": {
+ "score": 7,
+ "subScore": 1
+ }
+ },
+ {
+ "ratingStrings": ["PG", "TV-PG"],
+ "ratingScore": {
+ "score": 9,
+ "subScore": 0
+ }
+ },
+ {
+ "ratingStrings": ["14A"],
+ "ratingScore": {
+ "score": 14,
+ "subScore": 0
+ }
+ },
+ {
+ "ratingStrings": ["TV-14"],
+ "ratingScore": {
+ "score": 14,
+ "subScore": 1
+ }
+ },
+ {
+ "ratingStrings": ["16+"],
+ "ratingScore": {
+ "score": 16,
+ "subScore": 0
+ }
+ },
+ {
+ "ratingStrings": ["NC-17"],
+ "ratingScore": {
+ "score": 17,
+ "subScore": 0
+ }
+ },
+ {
+ "ratingStrings": ["18A"],
+ "ratingScore": {
+ "score": 18,
+ "subScore": 0
+ }
+ },
+ {
+ "ratingStrings": ["18+", "TV-MA", "R"],
+ "ratingScore": {
+ "score": 18,
+ "subScore": 1
+ }
+ },
+ {
+ "ratingStrings": ["A"],
+ "ratingScore": {
+ "score": 1000,
+ "subScore": 0
+ }
+ },
+ {
+ "ratingStrings": ["Prohibited"],
+ "ratingScore": {
+ "score": 1001,
+ "subScore": 0
+ }
+ }
+ ]
+}
diff --git a/Emby.Server.Implementations/Localization/Ratings/cl.json b/Emby.Server.Implementations/Localization/Ratings/cl.json
new file mode 100644
index 000000000..086619471
--- /dev/null
+++ b/Emby.Server.Implementations/Localization/Ratings/cl.json
@@ -0,0 +1,41 @@
+{
+ "countryCode": "cl",
+ "supportsSubScores": false,
+ "ratings": [
+ {
+ "ratingStrings": ["TE"],
+ "ratingScore": {
+ "score": 0,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["6"],
+ "ratingScore": {
+ "score": 6,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["TE+7"],
+ "ratingScore": {
+ "score": 7,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["14"],
+ "ratingScore": {
+ "score": 14,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["18", "18V", "18S"],
+ "ratingScore": {
+ "score": 18,
+ "subScore": null
+ }
+ }
+ ]
+}
diff --git a/Emby.Server.Implementations/Localization/Ratings/co.csv b/Emby.Server.Implementations/Localization/Ratings/co.csv
deleted file mode 100644
index e1e96c590..000000000
--- a/Emby.Server.Implementations/Localization/Ratings/co.csv
+++ /dev/null
@@ -1,7 +0,0 @@
-T,0
-7,7
-12,12
-15,15
-18,18
-X,1000
-Prohibited,1001
diff --git a/Emby.Server.Implementations/Localization/Ratings/co.json b/Emby.Server.Implementations/Localization/Ratings/co.json
new file mode 100644
index 000000000..4eff6dcc5
--- /dev/null
+++ b/Emby.Server.Implementations/Localization/Ratings/co.json
@@ -0,0 +1,55 @@
+{
+ "countryCode": "co",
+ "supportsSubScores": false,
+ "ratings": [
+ {
+ "ratingStrings": ["T"],
+ "ratingScore": {
+ "score": 0,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["7"],
+ "ratingScore": {
+ "score": 7,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["12"],
+ "ratingScore": {
+ "score": 12,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["15"],
+ "ratingScore": {
+ "score": 15,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["18"],
+ "ratingScore": {
+ "score": 18,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["X"],
+ "ratingScore": {
+ "score": 1000,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["Prohibited"],
+ "ratingScore": {
+ "score": 1001,
+ "subScore": null
+ }
+ }
+ ]
+}
diff --git a/Emby.Server.Implementations/Localization/Ratings/de.csv b/Emby.Server.Implementations/Localization/Ratings/de.csv
deleted file mode 100644
index f6181575e..000000000
--- a/Emby.Server.Implementations/Localization/Ratings/de.csv
+++ /dev/null
@@ -1,17 +0,0 @@
-Educational,0
-Infoprogramm,0
-FSK-0,0
-FSK 0,0
-0,0
-FSK-6,6
-FSK 6,6
-6,6
-FSK-12,12
-FSK 12,12
-12,12
-FSK-16,16
-FSK 16,16
-16,16
-FSK-18,18
-FSK 18,18
-18,18
diff --git a/Emby.Server.Implementations/Localization/Ratings/de.json b/Emby.Server.Implementations/Localization/Ratings/de.json
new file mode 100644
index 000000000..30c34b230
--- /dev/null
+++ b/Emby.Server.Implementations/Localization/Ratings/de.json
@@ -0,0 +1,41 @@
+{
+ "countryCode": "de",
+ "supportsSubScores": false,
+ "ratings": [
+ {
+ "ratingStrings": ["0", "FSK 0", "FSK-0", "Educational", "Infoprogramm"],
+ "ratingScore": {
+ "score": 0,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["6", "FSK 6", "FSK-6"],
+ "ratingScore": {
+ "score": 6,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["12", "FSK 12", "FSK-12"],
+ "ratingScore": {
+ "score": 12,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["16", "FSK 16", "FSK-16"],
+ "ratingScore": {
+ "score": 16,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["18", "FSK 18", "FSK-18"],
+ "ratingScore": {
+ "score": 18,
+ "subScore": null
+ }
+ }
+ ]
+}
diff --git a/Emby.Server.Implementations/Localization/Ratings/dk.csv b/Emby.Server.Implementations/Localization/Ratings/dk.csv
deleted file mode 100644
index 4ef63b2ea..000000000
--- a/Emby.Server.Implementations/Localization/Ratings/dk.csv
+++ /dev/null
@@ -1,7 +0,0 @@
-F,0
-A,0
-7,7
-11,11
-12,12
-15,15
-16,16
diff --git a/Emby.Server.Implementations/Localization/Ratings/dk.json b/Emby.Server.Implementations/Localization/Ratings/dk.json
new file mode 100644
index 000000000..9fcd6d44f
--- /dev/null
+++ b/Emby.Server.Implementations/Localization/Ratings/dk.json
@@ -0,0 +1,48 @@
+{
+ "countryCode": "dk",
+ "supportsSubScores": false,
+ "ratings": [
+ {
+ "ratingStrings": ["F", "A"],
+ "ratingScore": {
+ "score": 0,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["7"],
+ "ratingScore": {
+ "score": 7,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["11"],
+ "ratingScore": {
+ "score": 11,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["12"],
+ "ratingScore": {
+ "score": 12,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["15"],
+ "ratingScore": {
+ "score": 15,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["16"],
+ "ratingScore": {
+ "score": 16,
+ "subScore": null
+ }
+ }
+ ]
+}
diff --git a/Emby.Server.Implementations/Localization/Ratings/es.csv b/Emby.Server.Implementations/Localization/Ratings/es.csv
deleted file mode 100644
index ee5866090..000000000
--- a/Emby.Server.Implementations/Localization/Ratings/es.csv
+++ /dev/null
@@ -1,25 +0,0 @@
-A,0
-A/fig,0
-A/i,0
-A/i/fig,0
-APTA,0
-ERI,0
-TP,0
-0+,0
-6+,6
-7/fig,7
-7/i,7
-7/i/fig,7
-7,7
-9+,9
-10,10
-12,12
-12/fig,12
-13,13
-14,14
-16,16
-16/fig,16
-18,18
-18/fig,18
-X,1000
-Banned,1001
diff --git a/Emby.Server.Implementations/Localization/Ratings/es.json b/Emby.Server.Implementations/Localization/Ratings/es.json
new file mode 100644
index 000000000..c19629939
--- /dev/null
+++ b/Emby.Server.Implementations/Localization/Ratings/es.json
@@ -0,0 +1,90 @@
+{
+ "countryCode": "es",
+ "supportsSubScores": false,
+ "ratings": [
+ {
+ "ratingStrings": ["0+", "A", "A/i", "A/fig", "A/i/fig", "APTA", "ERI", "TP"],
+ "ratingScore": {
+ "score": 0,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["6+"],
+ "ratingScore": {
+ "score": 6,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["7", "7/i", "7/fig", "7/i/fig"],
+ "ratingScore": {
+ "score": 11,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["9+"],
+ "ratingScore": {
+ "score": 9,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["10"],
+ "ratingScore": {
+ "score": 10,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["12", "12/fig"],
+ "ratingScore": {
+ "score": 12,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["13"],
+ "ratingScore": {
+ "score": 13,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["14"],
+ "ratingScore": {
+ "score": 14,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["16", "16/fig"],
+ "ratingScore": {
+ "score": 16,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["18", "18/fig"],
+ "ratingScore": {
+ "score": 18,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["X"],
+ "ratingScore": {
+ "score": 1000,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["Banned"],
+ "ratingScore": {
+ "score": 1001,
+ "subScore": null
+ }
+ }
+ ]
+}
diff --git a/Emby.Server.Implementations/Localization/Ratings/fi.csv b/Emby.Server.Implementations/Localization/Ratings/fi.csv
deleted file mode 100644
index 7ff92f259..000000000
--- a/Emby.Server.Implementations/Localization/Ratings/fi.csv
+++ /dev/null
@@ -1,10 +0,0 @@
-S,0
-T,0
-K7,7
-7,7
-K12,12
-12,12
-K16,16
-16,16
-K18,18
-18,18
diff --git a/Emby.Server.Implementations/Localization/Ratings/fi.json b/Emby.Server.Implementations/Localization/Ratings/fi.json
new file mode 100644
index 000000000..3152317b5
--- /dev/null
+++ b/Emby.Server.Implementations/Localization/Ratings/fi.json
@@ -0,0 +1,41 @@
+{
+ "countryCode": "fi",
+ "supportsSubScores": false,
+ "ratings": [
+ {
+ "ratingStrings": ["S", "T"],
+ "ratingScore": {
+ "score": 0,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["7", "K7"],
+ "ratingScore": {
+ "score": 7,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["12", "K12"],
+ "ratingScore": {
+ "score": 12,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["16", "K16"],
+ "ratingScore": {
+ "score": 16,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["18", "K18"],
+ "ratingScore": {
+ "score": 18,
+ "subScore": null
+ }
+ }
+ ]
+}
diff --git a/Emby.Server.Implementations/Localization/Ratings/fr.csv b/Emby.Server.Implementations/Localization/Ratings/fr.csv
deleted file mode 100644
index 139ea376b..000000000
--- a/Emby.Server.Implementations/Localization/Ratings/fr.csv
+++ /dev/null
@@ -1,13 +0,0 @@
-Public Averti,0
-Tous Publics,0
-TP,0
-U,0
-0+,0
-6+,6
-9+,9
-10,10
-12,12
-14+,14
-16,16
-18,18
-X,1000
diff --git a/Emby.Server.Implementations/Localization/Ratings/fr.json b/Emby.Server.Implementations/Localization/Ratings/fr.json
new file mode 100644
index 000000000..e8bafd6b8
--- /dev/null
+++ b/Emby.Server.Implementations/Localization/Ratings/fr.json
@@ -0,0 +1,69 @@
+{
+ "countryCode": "fr",
+ "supportsSubScores": false,
+ "ratings": [
+ {
+ "ratingStrings": ["0+", "Public Averti", "Tous Publics", "TP", "U"],
+ "ratingScore": {
+ "score": 0,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["6+"],
+ "ratingScore": {
+ "score": 6,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["9+"],
+ "ratingScore": {
+ "score": 9,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["10"],
+ "ratingScore": {
+ "score": 10,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["12"],
+ "ratingScore": {
+ "score": 12,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["14+"],
+ "ratingScore": {
+ "score": 14,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["16"],
+ "ratingScore": {
+ "score": 16,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["18"],
+ "ratingScore": {
+ "score": 18,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["X"],
+ "ratingScore": {
+ "score": 1000,
+ "subScore": null
+ }
+ }
+ ]
+}
diff --git a/Emby.Server.Implementations/Localization/Ratings/gb.csv b/Emby.Server.Implementations/Localization/Ratings/gb.csv
deleted file mode 100644
index 858b9a32d..000000000
--- a/Emby.Server.Implementations/Localization/Ratings/gb.csv
+++ /dev/null
@@ -1,23 +0,0 @@
-All,0
-E,0
-G,0
-U,0
-0+,0
-6+,6
-7+,7
-PG,8
-9,9
-12,12
-12+,12
-12A,12
-12PG,12
-Teen,13
-13+,13
-14+,14
-15,15
-16,16
-Caution,18
-18,18
-Mature,1000
-Adult,1000
-R18,1000
diff --git a/Emby.Server.Implementations/Localization/Ratings/gb.json b/Emby.Server.Implementations/Localization/Ratings/gb.json
new file mode 100644
index 000000000..7fc88272c
--- /dev/null
+++ b/Emby.Server.Implementations/Localization/Ratings/gb.json
@@ -0,0 +1,97 @@
+{
+ "countryCode": "gb",
+ "supportsSubScores": true,
+ "ratings": [
+ {
+ "ratingStrings": ["0+", "All", "E", "G", "U"],
+ "ratingScore": {
+ "score": 0,
+ "subScore": 0
+ }
+ },
+ {
+ "ratingStrings": ["6+"],
+ "ratingScore": {
+ "score": 6,
+ "subScore": 0
+ }
+ },
+ {
+ "ratingStrings": ["7+"],
+ "ratingScore": {
+ "score": 7,
+ "subScore": 0
+ }
+ },
+ {
+ "ratingStrings": ["PG"],
+ "ratingScore": {
+ "score": 8,
+ "subScore": 0
+ }
+ },
+ {
+ "ratingStrings": ["9"],
+ "ratingScore": {
+ "score": 9,
+ "subScore": 0
+ }
+ },
+ {
+ "ratingStrings": ["12A", "12PG"],
+ "ratingScore": {
+ "score": 12,
+ "subScore": 0
+ }
+ },
+ {
+ "ratingStrings": ["12", "12+"],
+ "ratingScore": {
+ "score": 12,
+ "subScore": 1
+ }
+ },
+ {
+ "ratingStrings": ["13+", "Teen"],
+ "ratingScore": {
+ "score": 13,
+ "subScore": 0
+ }
+ },
+ {
+ "ratingStrings": ["14+"],
+ "ratingScore": {
+ "score": 14,
+ "subScore": 0
+ }
+ },
+ {
+ "ratingStrings": ["15"],
+ "ratingScore": {
+ "score": 15,
+ "subScore": 3
+ }
+ },
+ {
+ "ratingStrings": ["16"],
+ "ratingScore": {
+ "score": 16,
+ "subScore": 0
+ }
+ },
+ {
+ "ratingStrings": ["18", "Caution"],
+ "ratingScore": {
+ "score": 18,
+ "subScore": 1
+ }
+ },
+ {
+ "ratingStrings": ["Mature", "Adult", "R18"],
+ "ratingScore": {
+ "score": 1000,
+ "subScore": 0
+ }
+ }
+ ]
+}
diff --git a/Emby.Server.Implementations/Localization/Ratings/ie.csv b/Emby.Server.Implementations/Localization/Ratings/ie.csv
deleted file mode 100644
index d3c634fc9..000000000
--- a/Emby.Server.Implementations/Localization/Ratings/ie.csv
+++ /dev/null
@@ -1,10 +0,0 @@
-G,4
-PG,12
-12,12
-12A,12
-12PG,12
-15,15
-15PG,15
-15A,15
-16,16
-18,18
diff --git a/Emby.Server.Implementations/Localization/Ratings/ie.json b/Emby.Server.Implementations/Localization/Ratings/ie.json
new file mode 100644
index 000000000..f6cc56ed6
--- /dev/null
+++ b/Emby.Server.Implementations/Localization/Ratings/ie.json
@@ -0,0 +1,55 @@
+{
+ "countryCode": "ie",
+ "supportsSubScores": true,
+ "ratings": [
+ {
+ "ratingStrings": ["G"],
+ "ratingScore": {
+ "score": 4,
+ "subScore": 0
+ }
+ },
+ {
+ "ratingStrings": ["12A", "12PG", "PG"],
+ "ratingScore": {
+ "score": 12,
+ "subScore": 0
+ }
+ },
+ {
+ "ratingStrings": ["12"],
+ "ratingScore": {
+ "score": 12,
+ "subScore": 1
+ }
+ },
+ {
+ "ratingStrings": ["15A", "15PG"],
+ "ratingScore": {
+ "score": 15,
+ "subScore": 0
+ }
+ },
+ {
+ "ratingStrings": ["15"],
+ "ratingScore": {
+ "score": 15,
+ "subScore": 3
+ }
+ },
+ {
+ "ratingStrings": ["16"],
+ "ratingScore": {
+ "score": 16,
+ "subScore": 0
+ }
+ },
+ {
+ "ratingStrings": ["18"],
+ "ratingScore": {
+ "score": 18,
+ "subScore": 1
+ }
+ }
+ ]
+}
diff --git a/Emby.Server.Implementations/Localization/Ratings/jp.csv b/Emby.Server.Implementations/Localization/Ratings/jp.csv
deleted file mode 100644
index bfb5fdaae..000000000
--- a/Emby.Server.Implementations/Localization/Ratings/jp.csv
+++ /dev/null
@@ -1,11 +0,0 @@
-A,0
-G,0
-B,12
-PG12,12
-C,15
-15+,15
-R15+,15
-16+,16
-D,17
-Z,18
-18+,18
diff --git a/Emby.Server.Implementations/Localization/Ratings/jp.json b/Emby.Server.Implementations/Localization/Ratings/jp.json
new file mode 100644
index 000000000..efff9e92c
--- /dev/null
+++ b/Emby.Server.Implementations/Localization/Ratings/jp.json
@@ -0,0 +1,62 @@
+{
+ "countryCode": "jp",
+ "supportsSubScores": true,
+ "ratings": [
+ {
+ "ratingStrings": ["A", "G"],
+ "ratingScore": {
+ "score": 0,
+ "subScore": 0
+ }
+ },
+ {
+ "ratingStrings": ["PG12"],
+ "ratingScore": {
+ "score": 12,
+ "subScore": 0
+ }
+ },
+ {
+ "ratingStrings": ["B"],
+ "ratingScore": {
+ "score": 12,
+ "subScore": 1
+ }
+ },
+ {
+ "ratingStrings": ["15A", "15PG"],
+ "ratingScore": {
+ "score": 15,
+ "subScore": 0
+ }
+ },
+ {
+ "ratingStrings": ["C", "15+", "R15+"],
+ "ratingScore": {
+ "score": 15,
+ "subScore": 1
+ }
+ },
+ {
+ "ratingStrings": ["16+"],
+ "ratingScore": {
+ "score": 16,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["D"],
+ "ratingScore": {
+ "score": 17,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["18+", "Z"],
+ "ratingScore": {
+ "score": 18,
+ "subScore": null
+ }
+ }
+ ]
+}
diff --git a/Emby.Server.Implementations/Localization/Ratings/kz.csv b/Emby.Server.Implementations/Localization/Ratings/kz.csv
deleted file mode 100644
index e26b32b67..000000000
--- a/Emby.Server.Implementations/Localization/Ratings/kz.csv
+++ /dev/null
@@ -1,6 +0,0 @@
-K,0
-БА,12
-Б14,14
-E16,16
-E18,18
-HA,18
diff --git a/Emby.Server.Implementations/Localization/Ratings/kz.json b/Emby.Server.Implementations/Localization/Ratings/kz.json
new file mode 100644
index 000000000..0f8f0c68e
--- /dev/null
+++ b/Emby.Server.Implementations/Localization/Ratings/kz.json
@@ -0,0 +1,41 @@
+{
+ "countryCode": "kz",
+ "supportsSubScores": false,
+ "ratings": [
+ {
+ "ratingStrings": ["K"],
+ "ratingScore": {
+ "score": 0,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["БА"],
+ "ratingScore": {
+ "score": 12,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["Б14"],
+ "ratingScore": {
+ "score": 14,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["E16"],
+ "ratingScore": {
+ "score": 16,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["E18", "HA"],
+ "ratingScore": {
+ "score": 18,
+ "subScore": null
+ }
+ }
+ ]
+}
diff --git a/Emby.Server.Implementations/Localization/Ratings/mx.csv b/Emby.Server.Implementations/Localization/Ratings/mx.csv
deleted file mode 100644
index 305912f23..000000000
--- a/Emby.Server.Implementations/Localization/Ratings/mx.csv
+++ /dev/null
@@ -1,6 +0,0 @@
-A,0
-AA,0
-B,12
-B-15,15
-C,18
-D,1000
diff --git a/Emby.Server.Implementations/Localization/Ratings/mx.json b/Emby.Server.Implementations/Localization/Ratings/mx.json
new file mode 100644
index 000000000..9dc3b89bd
--- /dev/null
+++ b/Emby.Server.Implementations/Localization/Ratings/mx.json
@@ -0,0 +1,41 @@
+{
+ "countryCode": "mx",
+ "supportsSubScores": false,
+ "ratings": [
+ {
+ "ratingStrings": ["A", "AA"],
+ "ratingScore": {
+ "score": 0,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["B"],
+ "ratingScore": {
+ "score": 12,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["B-15"],
+ "ratingScore": {
+ "score": 15,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["C"],
+ "ratingScore": {
+ "score": 18,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["D"],
+ "ratingScore": {
+ "score": 1000,
+ "subScore": null
+ }
+ }
+ ]
+}
diff --git a/Emby.Server.Implementations/Localization/Ratings/nl.csv b/Emby.Server.Implementations/Localization/Ratings/nl.csv
deleted file mode 100644
index 44f372b2d..000000000
--- a/Emby.Server.Implementations/Localization/Ratings/nl.csv
+++ /dev/null
@@ -1,8 +0,0 @@
-AL,0
-MG6,6
-6,6
-9,9
-12,12
-14,14
-16,16
-18,18
diff --git a/Emby.Server.Implementations/Localization/Ratings/nl.json b/Emby.Server.Implementations/Localization/Ratings/nl.json
new file mode 100644
index 000000000..2e43eb83a
--- /dev/null
+++ b/Emby.Server.Implementations/Localization/Ratings/nl.json
@@ -0,0 +1,55 @@
+{
+ "countryCode": "nl",
+ "supportsSubScores": false,
+ "ratings": [
+ {
+ "ratingStrings": ["AL"],
+ "ratingScore": {
+ "score": 0,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["6", "MG6"],
+ "ratingScore": {
+ "score": 6,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["9"],
+ "ratingScore": {
+ "score": 9,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["12"],
+ "ratingScore": {
+ "score": 12,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["14"],
+ "ratingScore": {
+ "score": 14,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["16"],
+ "ratingScore": {
+ "score": 16,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["18"],
+ "ratingScore": {
+ "score": 18,
+ "subScore": null
+ }
+ }
+ ]
+}
diff --git a/Emby.Server.Implementations/Localization/Ratings/no.csv b/Emby.Server.Implementations/Localization/Ratings/no.csv
deleted file mode 100644
index 6856a2dbb..000000000
--- a/Emby.Server.Implementations/Localization/Ratings/no.csv
+++ /dev/null
@@ -1,10 +0,0 @@
-A,0
-6,6
-7,7
-9,9
-11,11
-12,12
-15,15
-18,18
-C,18
-Not approved,1001
diff --git a/Emby.Server.Implementations/Localization/Ratings/no.json b/Emby.Server.Implementations/Localization/Ratings/no.json
new file mode 100644
index 000000000..a5e952316
--- /dev/null
+++ b/Emby.Server.Implementations/Localization/Ratings/no.json
@@ -0,0 +1,69 @@
+{
+ "countryCode": "no",
+ "supportsSubScores": false,
+ "ratings": [
+ {
+ "ratingStrings": ["A"],
+ "ratingScore": {
+ "score": 0,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["6"],
+ "ratingScore": {
+ "score": 6,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["7"],
+ "ratingScore": {
+ "score": 7,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["9"],
+ "ratingScore": {
+ "score": 9,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["11"],
+ "ratingScore": {
+ "score": 11,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["12"],
+ "ratingScore": {
+ "score": 12,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["15"],
+ "ratingScore": {
+ "score": 15,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["18"],
+ "ratingScore": {
+ "score": 18,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["Not approved"],
+ "ratingScore": {
+ "score": 1001,
+ "subScore": null
+ }
+ }
+ ]
+}
diff --git a/Emby.Server.Implementations/Localization/Ratings/nz.csv b/Emby.Server.Implementations/Localization/Ratings/nz.csv
deleted file mode 100644
index 633da78fe..000000000
--- a/Emby.Server.Implementations/Localization/Ratings/nz.csv
+++ /dev/null
@@ -1,16 +0,0 @@
-Exempt,0
-G,0
-GY,13
-PG,13
-R13,13
-RP13,13
-R15,15
-M,16
-R16,16
-RP16,16
-GA,18
-R18,18
-RP18,18
-MA,1000
-R,1001
-Objectionable,1001
diff --git a/Emby.Server.Implementations/Localization/Ratings/nz.json b/Emby.Server.Implementations/Localization/Ratings/nz.json
new file mode 100644
index 000000000..3c1332271
--- /dev/null
+++ b/Emby.Server.Implementations/Localization/Ratings/nz.json
@@ -0,0 +1,69 @@
+{
+ "countryCode": "nz",
+ "supportsSubScores": true,
+ "ratings": [
+ {
+ "ratingStrings": ["Exempt", "G"],
+ "ratingScore": {
+ "score": 0,
+ "subScore": 0
+ }
+ },
+ {
+ "ratingStrings": ["RP13", "PG"],
+ "ratingScore": {
+ "score": 13,
+ "subScore": 0
+ }
+ },
+ {
+ "ratingStrings": ["GY", "R13"],
+ "ratingScore": {
+ "score": 13,
+ "subScore": 1
+ }
+ },
+ {
+ "ratingStrings": ["RP16", "M"],
+ "ratingScore": {
+ "score": 16,
+ "subScore": 0
+ }
+ },
+ {
+ "ratingStrings": ["R16"],
+ "ratingScore": {
+ "score": 16,
+ "subScore": 1
+ }
+ },
+ {
+ "ratingStrings": ["RP18"],
+ "ratingScore": {
+ "score": 18,
+ "subScore": 0
+ }
+ },
+ {
+ "ratingStrings": ["R18", "GA"],
+ "ratingScore": {
+ "score": 18,
+ "subScore": 1
+ }
+ },
+ {
+ "ratingStrings": ["MA"],
+ "ratingScore": {
+ "score": 1000,
+ "subScore": 0
+ }
+ },
+ {
+ "ratingStrings": ["Objectionable", "R"],
+ "ratingScore": {
+ "score": 1001,
+ "subScore": 0
+ }
+ }
+ ]
+}
diff --git a/Emby.Server.Implementations/Localization/Ratings/pl.json b/Emby.Server.Implementations/Localization/Ratings/pl.json
new file mode 100644
index 000000000..c3001ffb3
--- /dev/null
+++ b/Emby.Server.Implementations/Localization/Ratings/pl.json
@@ -0,0 +1,41 @@
+{
+ "countryCode": "pl",
+ "supportsSubScores": false,
+ "ratings": [
+ {
+ "ratingStrings": ["b.o.", "AL"],
+ "ratingScore": {
+ "score": 0,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["7", "od 7 lat"],
+ "ratingScore": {
+ "score": 7,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["12", "od 12 lat"],
+ "ratingScore": {
+ "score": 12,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["16", "od 16 lat"],
+ "ratingScore": {
+ "score": 16,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["18", "od 18 lat", "R"],
+ "ratingScore": {
+ "score": 18,
+ "subScore": null
+ }
+ }
+ ]
+}
diff --git a/Emby.Server.Implementations/Localization/Ratings/ro.csv b/Emby.Server.Implementations/Localization/Ratings/ro.csv
deleted file mode 100644
index 44c23e248..000000000
--- a/Emby.Server.Implementations/Localization/Ratings/ro.csv
+++ /dev/null
@@ -1,6 +0,0 @@
-AG,0
-AP-12,12
-N-15,15
-IM-18,18
-IM-18-XXX,1000
-IC,1001
diff --git a/Emby.Server.Implementations/Localization/Ratings/ro.json b/Emby.Server.Implementations/Localization/Ratings/ro.json
new file mode 100644
index 000000000..9cf735a54
--- /dev/null
+++ b/Emby.Server.Implementations/Localization/Ratings/ro.json
@@ -0,0 +1,48 @@
+{
+ "countryCode": "ro",
+ "supportsSubScores": false,
+ "ratings": [
+ {
+ "ratingStrings": ["AG"],
+ "ratingScore": {
+ "score": 0,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["AP-12"],
+ "ratingScore": {
+ "score": 12,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["N-15"],
+ "ratingScore": {
+ "score": 15,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["IM-18"],
+ "ratingScore": {
+ "score": 18,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["IM-18-XXX"],
+ "ratingScore": {
+ "score": 1000,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["IC"],
+ "ratingScore": {
+ "score": 1001,
+ "subScore": null
+ }
+ }
+ ]
+}
diff --git a/Emby.Server.Implementations/Localization/Ratings/ru.csv b/Emby.Server.Implementations/Localization/Ratings/ru.csv
deleted file mode 100644
index 8b264070b..000000000
--- a/Emby.Server.Implementations/Localization/Ratings/ru.csv
+++ /dev/null
@@ -1,6 +0,0 @@
-0+,0
-6+,6
-12+,12
-16+,16
-18+,18
-Refused classification,1001
diff --git a/Emby.Server.Implementations/Localization/Ratings/ru.json b/Emby.Server.Implementations/Localization/Ratings/ru.json
new file mode 100644
index 000000000..d1b8b13aa
--- /dev/null
+++ b/Emby.Server.Implementations/Localization/Ratings/ru.json
@@ -0,0 +1,48 @@
+{
+ "countryCode": "ru",
+ "supportsSubScores": false,
+ "ratings": [
+ {
+ "ratingStrings": ["0+"],
+ "ratingScore": {
+ "score": 0,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["6+"],
+ "ratingScore": {
+ "score": 6,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["12+"],
+ "ratingScore": {
+ "score": 12,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["16+"],
+ "ratingScore": {
+ "score": 16,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["18+"],
+ "ratingScore": {
+ "score": 18,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["Refused classification"],
+ "ratingScore": {
+ "score": 1001,
+ "subScore": null
+ }
+ }
+ ]
+}
diff --git a/Emby.Server.Implementations/Localization/Ratings/se.csv b/Emby.Server.Implementations/Localization/Ratings/se.csv
deleted file mode 100644
index e129c3561..000000000
--- a/Emby.Server.Implementations/Localization/Ratings/se.csv
+++ /dev/null
@@ -1,10 +0,0 @@
-Alla,0
-Barntillåten,0
-Btl,0
-0+,0
-7,7
-9+,9
-10+,10
-11,11
-14,14
-15,15
diff --git a/Emby.Server.Implementations/Localization/Ratings/se.json b/Emby.Server.Implementations/Localization/Ratings/se.json
new file mode 100644
index 000000000..70084995d
--- /dev/null
+++ b/Emby.Server.Implementations/Localization/Ratings/se.json
@@ -0,0 +1,55 @@
+{
+ "countryCode": "se",
+ "supportsSubScores": false,
+ "ratings": [
+ {
+ "ratingStrings": ["0+", "Alla", "Barntillåten", "Btl"],
+ "ratingScore": {
+ "score": 0,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["7"],
+ "ratingScore": {
+ "score": 7,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["9+"],
+ "ratingScore": {
+ "score": 9,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["10+"],
+ "ratingScore": {
+ "score": 10,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["11"],
+ "ratingScore": {
+ "score": 11,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["14"],
+ "ratingScore": {
+ "score": 14,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["15"],
+ "ratingScore": {
+ "score": 15,
+ "subScore": null
+ }
+ }
+ ]
+}
diff --git a/Emby.Server.Implementations/Localization/Ratings/sk.csv b/Emby.Server.Implementations/Localization/Ratings/sk.csv
deleted file mode 100644
index dbafd8efa..000000000
--- a/Emby.Server.Implementations/Localization/Ratings/sk.csv
+++ /dev/null
@@ -1,6 +0,0 @@
-NR,0
-U,0
-7,7
-12,12
-15,15
-18,18
diff --git a/Emby.Server.Implementations/Localization/Ratings/sk.json b/Emby.Server.Implementations/Localization/Ratings/sk.json
new file mode 100644
index 000000000..5ec6111ec
--- /dev/null
+++ b/Emby.Server.Implementations/Localization/Ratings/sk.json
@@ -0,0 +1,41 @@
+{
+ "countryCode": "sk",
+ "supportsSubScores": false,
+ "ratings": [
+ {
+ "ratingStrings": ["U", "NR"],
+ "ratingScore": {
+ "score": 0,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["7"],
+ "ratingScore": {
+ "score": 7,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["12"],
+ "ratingScore": {
+ "score": 12,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["15"],
+ "ratingScore": {
+ "score": 15,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["18"],
+ "ratingScore": {
+ "score": 18,
+ "subScore": null
+ }
+ }
+ ]
+}
diff --git a/Emby.Server.Implementations/Localization/Ratings/uk.csv b/Emby.Server.Implementations/Localization/Ratings/uk.csv
deleted file mode 100644
index 75b1c2058..000000000
--- a/Emby.Server.Implementations/Localization/Ratings/uk.csv
+++ /dev/null
@@ -1,22 +0,0 @@
-All,0
-E,0
-G,0
-U,0
-0+,0
-6+,6
-7+,7
-PG,8
-9+,9
-12,12
-12+,12
-12A,12
-Teen,13
-13+,13
-14+,14
-15,15
-16,16
-Caution,18
-18,18
-Mature,1000
-Adult,1000
-R18,1000
diff --git a/Emby.Server.Implementations/Localization/Ratings/uk.json b/Emby.Server.Implementations/Localization/Ratings/uk.json
new file mode 100644
index 000000000..7fc88272c
--- /dev/null
+++ b/Emby.Server.Implementations/Localization/Ratings/uk.json
@@ -0,0 +1,97 @@
+{
+ "countryCode": "gb",
+ "supportsSubScores": true,
+ "ratings": [
+ {
+ "ratingStrings": ["0+", "All", "E", "G", "U"],
+ "ratingScore": {
+ "score": 0,
+ "subScore": 0
+ }
+ },
+ {
+ "ratingStrings": ["6+"],
+ "ratingScore": {
+ "score": 6,
+ "subScore": 0
+ }
+ },
+ {
+ "ratingStrings": ["7+"],
+ "ratingScore": {
+ "score": 7,
+ "subScore": 0
+ }
+ },
+ {
+ "ratingStrings": ["PG"],
+ "ratingScore": {
+ "score": 8,
+ "subScore": 0
+ }
+ },
+ {
+ "ratingStrings": ["9"],
+ "ratingScore": {
+ "score": 9,
+ "subScore": 0
+ }
+ },
+ {
+ "ratingStrings": ["12A", "12PG"],
+ "ratingScore": {
+ "score": 12,
+ "subScore": 0
+ }
+ },
+ {
+ "ratingStrings": ["12", "12+"],
+ "ratingScore": {
+ "score": 12,
+ "subScore": 1
+ }
+ },
+ {
+ "ratingStrings": ["13+", "Teen"],
+ "ratingScore": {
+ "score": 13,
+ "subScore": 0
+ }
+ },
+ {
+ "ratingStrings": ["14+"],
+ "ratingScore": {
+ "score": 14,
+ "subScore": 0
+ }
+ },
+ {
+ "ratingStrings": ["15"],
+ "ratingScore": {
+ "score": 15,
+ "subScore": 3
+ }
+ },
+ {
+ "ratingStrings": ["16"],
+ "ratingScore": {
+ "score": 16,
+ "subScore": 0
+ }
+ },
+ {
+ "ratingStrings": ["18", "Caution"],
+ "ratingScore": {
+ "score": 18,
+ "subScore": 1
+ }
+ },
+ {
+ "ratingStrings": ["Mature", "Adult", "R18"],
+ "ratingScore": {
+ "score": 1000,
+ "subScore": 0
+ }
+ }
+ ]
+}
diff --git a/Emby.Server.Implementations/Localization/Ratings/us.csv b/Emby.Server.Implementations/Localization/Ratings/us.csv
deleted file mode 100644
index 9aa5c00eb..000000000
--- a/Emby.Server.Implementations/Localization/Ratings/us.csv
+++ /dev/null
@@ -1,52 +0,0 @@
-Approved,0
-G,0
-TV-G,0
-TV-Y,0
-TV-Y7,7
-TV-Y7-FV,7
-PG,10
-TV-PG,10
-TV-PG-D,10
-TV-PG-L,10
-TV-PG-S,10
-TV-PG-V,10
-TV-PG-DL,10
-TV-PG-DS,10
-TV-PG-DV,10
-TV-PG-LS,10
-TV-PG-LV,10
-TV-PG-SV,10
-TV-PG-DLS,10
-TV-PG-DLV,10
-TV-PG-DSV,10
-TV-PG-LSV,10
-TV-PG-DLSV,10
-PG-13,13
-TV-14,14
-TV-14-D,14
-TV-14-L,14
-TV-14-S,14
-TV-14-V,14
-TV-14-DL,14
-TV-14-DS,14
-TV-14-DV,14
-TV-14-LS,14
-TV-14-LV,14
-TV-14-SV,14
-TV-14-DLS,14
-TV-14-DLV,14
-TV-14-DSV,14
-TV-14-LSV,14
-TV-14-DLSV,14
-NC-17,17
-R,17
-TV-MA,17
-TV-MA-L,17
-TV-MA-S,17
-TV-MA-V,17
-TV-MA-LS,17
-TV-MA-LV,17
-TV-MA-SV,17
-TV-MA-LSV,17
-TV-X,18
-TV-AO,18
diff --git a/Emby.Server.Implementations/Localization/Ratings/us.json b/Emby.Server.Implementations/Localization/Ratings/us.json
new file mode 100644
index 000000000..08a637312
--- /dev/null
+++ b/Emby.Server.Implementations/Localization/Ratings/us.json
@@ -0,0 +1,83 @@
+{
+ "countryCode": "us",
+ "supportsSubScores": true,
+ "ratings": [
+ {
+ "ratingStrings": ["Approved", "G", "TV-G", "TV-Y"],
+ "ratingScore": {
+ "score": 0,
+ "subScore": 0
+ }
+ },
+ {
+ "ratingStrings": ["TV-Y7"],
+ "ratingScore": {
+ "score": 7,
+ "subScore": 0
+ }
+ },
+ {
+ "ratingStrings": ["TV-Y7-FV"],
+ "ratingScore": {
+ "score": 7,
+ "subScore": 1
+ }
+ },
+ {
+ "ratingStrings": ["PG", "TV-PG"],
+ "ratingScore": {
+ "score": 10,
+ "subScore": 0
+ }
+ },
+ {
+ "ratingStrings": ["TV-PG-D", "TV-PG-L", "TV-PG-S", "TV-PG-V", "TV-PG-DL", "TV-PG-DS", "TV-PG-DV", "TV-PG-LS", "TV-PG-LV", "TV-PG-SV", "TV-PG-DLS", "TV-PG-DLV", "TV-PG-DSV", "TV-PG-LSV", "TV-PG-DLSV"],
+ "ratingScore": {
+ "score": 10,
+ "subScore": 1
+ }
+ },
+ {
+ "ratingStrings": ["PG-13"],
+ "ratingScore": {
+ "score": 13,
+ "subScore": 0
+ }
+ },
+ {
+ "ratingStrings": ["TV-14"],
+ "ratingScore": {
+ "score": 14,
+ "subScore": 0
+ }
+ },
+ {
+ "ratingStrings": ["TV-14-D", "TV-14-L", "TV-14-S", "TV-14-V", "TV-14-DL", "TV-14-DS", "TV-14-DV", "TV-14-LS", "TV-14-LV", "TV-14-SV", "TV-14-DLS", "TV-14-DLV", "TV-14-DSV", "TV-14-LSV", "TV-14-DLSV"],
+ "ratingScore": {
+ "score": 14,
+ "subScore": 1
+ }
+ },
+ {
+ "ratingStrings": ["R"],
+ "ratingScore": {
+ "score": 17,
+ "subScore": 0
+ }
+ },
+ {
+ "ratingStrings": ["NC-17", "TV-MA", "TV-MA-L", "TV-MA-S", "TV-MA-V", "TV-MA-LS", "TV-MA-LV", "TV-MA-SV", "TV-MA-LSV"],
+ "ratingScore": {
+ "score": 17,
+ "subScore": 1
+ }
+ },
+ {
+ "ratingStrings": ["TV-X", "TV-AO"],
+ "ratingScore": {
+ "score": 18,
+ "subScore": 0
+ }
+ }
+ ]
+}
diff --git a/Emby.Server.Implementations/Playlists/PlaylistManager.cs b/Emby.Server.Implementations/Playlists/PlaylistManager.cs
index 7b0a16441..98a43b6c9 100644
--- a/Emby.Server.Implementations/Playlists/PlaylistManager.cs
+++ b/Emby.Server.Implementations/Playlists/PlaylistManager.cs
@@ -283,6 +283,16 @@ namespace Emby.Server.Implementations.Playlists
RefreshPriority.High);
}
+ internal static int DetermineAdjustedIndex(int newPriorIndexAllChildren, int newIndex)
+ {
+ if (newIndex == 0)
+ {
+ return newPriorIndexAllChildren > 0 ? newPriorIndexAllChildren - 1 : 0;
+ }
+
+ return newPriorIndexAllChildren + 1;
+ }
+
public async Task MoveItemAsync(string playlistId, string entryId, int newIndex, Guid callingUserId)
{
if (_libraryManager.GetItemById(playlistId) is not Playlist playlist)
@@ -305,7 +315,7 @@ namespace Emby.Server.Implementations.Playlists
var newPriorItemIndex = newIndex > oldIndexAccessible ? newIndex : newIndex - 1 < 0 ? 0 : newIndex - 1;
var newPriorItemId = accessibleChildren[newPriorItemIndex].Item1.ItemId;
var newPriorItemIndexOnAllChildren = children.FindIndex(c => c.Item1.ItemId.Equals(newPriorItemId));
- var adjustedNewIndex = newPriorItemIndexOnAllChildren + 1;
+ var adjustedNewIndex = DetermineAdjustedIndex(newPriorItemIndexOnAllChildren, newIndex);
var item = playlist.LinkedChildren.FirstOrDefault(i => string.Equals(entryId, i.ItemId?.ToString("N", CultureInfo.InvariantCulture), StringComparison.OrdinalIgnoreCase));
if (item is null)
diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs
index 42f7deca1..924f50286 100644
--- a/Emby.Server.Implementations/Session/SessionManager.cs
+++ b/Emby.Server.Implementations/Session/SessionManager.cs
@@ -64,6 +64,9 @@ namespace Emby.Server.Implementations.Session
private readonly ConcurrentDictionary<string, SessionInfo> _activeConnections
= new(StringComparer.OrdinalIgnoreCase);
+ private readonly ConcurrentDictionary<string, ConcurrentDictionary<string, string>> _activeLiveStreamSessions
+ = new(StringComparer.OrdinalIgnoreCase);
+
private Timer _idleTimer;
private Timer _inactiveTimer;
@@ -311,7 +314,7 @@ namespace Emby.Server.Implementations.Session
_activeConnections.TryRemove(key, out _);
if (!string.IsNullOrEmpty(session.PlayState?.LiveStreamId))
{
- await _mediaSourceManager.CloseLiveStream(session.PlayState.LiveStreamId).ConfigureAwait(false);
+ await CloseLiveStreamIfNeededAsync(session.PlayState.LiveStreamId, session.Id).ConfigureAwait(false);
}
await OnSessionEnded(session).ConfigureAwait(false);
@@ -319,6 +322,42 @@ namespace Emby.Server.Implementations.Session
}
/// <inheritdoc />
+ public async Task CloseLiveStreamIfNeededAsync(string liveStreamId, string sessionIdOrPlaySessionId)
+ {
+ bool liveStreamNeedsToBeClosed = false;
+
+ if (_activeLiveStreamSessions.TryGetValue(liveStreamId, out var activeSessionMappings))
+ {
+ if (activeSessionMappings.TryRemove(sessionIdOrPlaySessionId, out var correspondingId))
+ {
+ if (!string.IsNullOrEmpty(correspondingId))
+ {
+ activeSessionMappings.TryRemove(correspondingId, out _);
+ }
+
+ liveStreamNeedsToBeClosed = true;
+ }
+
+ if (activeSessionMappings.IsEmpty)
+ {
+ _activeLiveStreamSessions.TryRemove(liveStreamId, out _);
+ }
+ }
+
+ if (liveStreamNeedsToBeClosed)
+ {
+ try
+ {
+ await _mediaSourceManager.CloseLiveStream(liveStreamId).ConfigureAwait(false);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error closing live stream");
+ }
+ }
+ }
+
+ /// <inheritdoc />
public async ValueTask ReportSessionEnded(string sessionId)
{
CheckDisposed();
@@ -737,6 +776,11 @@ namespace Emby.Server.Implementations.Session
}
}
+ if (!string.IsNullOrEmpty(info.LiveStreamId))
+ {
+ UpdateLiveStreamActiveSessionMappings(info.LiveStreamId, info.SessionId, info.PlaySessionId);
+ }
+
var eventArgs = new PlaybackStartEventArgs
{
Item = libraryItem,
@@ -794,6 +838,32 @@ namespace Emby.Server.Implementations.Session
return OnPlaybackProgress(info, false);
}
+ private void UpdateLiveStreamActiveSessionMappings(string liveStreamId, string sessionId, string playSessionId)
+ {
+ var activeSessionMappings = _activeLiveStreamSessions.GetOrAdd(liveStreamId, _ => new ConcurrentDictionary<string, string>());
+
+ if (!string.IsNullOrEmpty(playSessionId))
+ {
+ if (!activeSessionMappings.TryGetValue(sessionId, out var currentPlaySessionId) || currentPlaySessionId != playSessionId)
+ {
+ if (!string.IsNullOrEmpty(currentPlaySessionId))
+ {
+ activeSessionMappings.TryRemove(currentPlaySessionId, out _);
+ }
+
+ activeSessionMappings[sessionId] = playSessionId;
+ activeSessionMappings[playSessionId] = sessionId;
+ }
+ }
+ else
+ {
+ if (!activeSessionMappings.TryGetValue(sessionId, out _))
+ {
+ activeSessionMappings[sessionId] = string.Empty;
+ }
+ }
+ }
+
/// <summary>
/// Used to report playback progress for an item.
/// </summary>
@@ -834,6 +904,11 @@ namespace Emby.Server.Implementations.Session
}
}
+ if (!string.IsNullOrEmpty(info.LiveStreamId))
+ {
+ UpdateLiveStreamActiveSessionMappings(info.LiveStreamId, info.SessionId, info.PlaySessionId);
+ }
+
var eventArgs = new PlaybackProgressEventArgs
{
Item = libraryItem,
@@ -1016,14 +1091,7 @@ namespace Emby.Server.Implementations.Session
if (!string.IsNullOrEmpty(info.LiveStreamId))
{
- try
- {
- await _mediaSourceManager.CloseLiveStream(info.LiveStreamId).ConfigureAwait(false);
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Error closing live stream");
- }
+ await CloseLiveStreamIfNeededAsync(info.LiveStreamId, session.Id).ConfigureAwait(false);
}
var eventArgs = new PlaybackStopEventArgs
@@ -1740,7 +1808,6 @@ namespace Emby.Server.Implementations.Session
fields.Remove(ItemFields.DateLastSaved);
fields.Remove(ItemFields.DisplayPreferencesId);
fields.Remove(ItemFields.Etag);
- fields.Remove(ItemFields.InheritedParentalRatingValue);
fields.Remove(ItemFields.ItemCounts);
fields.Remove(ItemFields.MediaSourceCount);
fields.Remove(ItemFields.MediaStreams);
@@ -2071,6 +2138,7 @@ namespace Emby.Server.Implementations.Session
}
_activeConnections.Clear();
+ _activeLiveStreamSessions.Clear();
}
}
}
diff --git a/Emby.Server.Implementations/Sorting/OfficialRatingComparer.cs b/Emby.Server.Implementations/Sorting/OfficialRatingComparer.cs
index b4ee2c723..789af01cc 100644
--- a/Emby.Server.Implementations/Sorting/OfficialRatingComparer.cs
+++ b/Emby.Server.Implementations/Sorting/OfficialRatingComparer.cs
@@ -1,45 +1,54 @@
-#pragma warning disable CS1591
-
using System;
using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Sorting;
+using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Globalization;
-using MediaBrowser.Model.Querying;
-namespace Emby.Server.Implementations.Sorting
+namespace Emby.Server.Implementations.Sorting;
+
+/// <summary>
+/// Class providing comparison for official ratings.
+/// </summary>
+public class OfficialRatingComparer : IBaseItemComparer
{
- public class OfficialRatingComparer : IBaseItemComparer
+ private readonly ILocalizationManager _localizationManager;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="OfficialRatingComparer"/> class.
+ /// </summary>
+ /// <param name="localizationManager">Instance of the <see cref="ILocalizationManager"/> interface.</param>
+ public OfficialRatingComparer(ILocalizationManager localizationManager)
{
- private readonly ILocalizationManager _localization;
+ _localizationManager = localizationManager;
+ }
- public OfficialRatingComparer(ILocalizationManager localization)
+ /// <summary>
+ /// Gets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ public ItemSortBy Type => ItemSortBy.OfficialRating;
+
+ /// <summary>
+ /// Compares the specified x.
+ /// </summary>
+ /// <param name="x">The x.</param>
+ /// <param name="y">The y.</param>
+ /// <returns>System.Int32.</returns>
+ public int Compare(BaseItem? x, BaseItem? y)
+ {
+ ArgumentNullException.ThrowIfNull(x);
+ ArgumentNullException.ThrowIfNull(y);
+ var zeroRating = new ParentalRatingScore(0, 0);
+
+ var ratingX = string.IsNullOrEmpty(x.OfficialRating) ? zeroRating : _localizationManager.GetRatingScore(x.OfficialRating) ?? zeroRating;
+ var ratingY = string.IsNullOrEmpty(y.OfficialRating) ? zeroRating : _localizationManager.GetRatingScore(y.OfficialRating) ?? zeroRating;
+ var scoreCompare = ratingX.Score.CompareTo(ratingY.Score);
+ if (scoreCompare is 0)
{
- _localization = localization;
+ return (ratingX.SubScore ?? 0).CompareTo(ratingY.SubScore ?? 0);
}
- /// <summary>
- /// Gets the name.
- /// </summary>
- /// <value>The name.</value>
- public ItemSortBy Type => ItemSortBy.OfficialRating;
-
- /// <summary>
- /// Compares the specified x.
- /// </summary>
- /// <param name="x">The x.</param>
- /// <param name="y">The y.</param>
- /// <returns>System.Int32.</returns>
- public int Compare(BaseItem? x, BaseItem? y)
- {
- ArgumentNullException.ThrowIfNull(x);
-
- ArgumentNullException.ThrowIfNull(y);
-
- var levelX = string.IsNullOrEmpty(x.OfficialRating) ? 0 : _localization.GetRatingLevel(x.OfficialRating) ?? 0;
- var levelY = string.IsNullOrEmpty(y.OfficialRating) ? 0 : _localization.GetRatingLevel(y.OfficialRating) ?? 0;
-
- return levelX.CompareTo(levelY);
- }
+ return scoreCompare;
}
}
diff --git a/Emby.Server.Implementations/SyncPlay/Group.cs b/Emby.Server.Implementations/SyncPlay/Group.cs
index d47e47793..c2e834ad5 100644
--- a/Emby.Server.Implementations/SyncPlay/Group.cs
+++ b/Emby.Server.Implementations/SyncPlay/Group.cs
@@ -273,7 +273,7 @@ namespace Emby.Server.Implementations.SyncPlay
SetState(waitingState);
}
- var updateSession = NewSyncPlayGroupUpdate(GroupUpdateType.GroupJoined, GetInfo());
+ var updateSession = new SyncPlayGroupJoinedUpdate(GroupId, GetInfo());
SendGroupUpdate(session, SyncPlayBroadcastType.CurrentSession, updateSession, cancellationToken);
_state.SessionJoined(this, _state.Type, session, cancellationToken);
@@ -291,10 +291,10 @@ namespace Emby.Server.Implementations.SyncPlay
{
AddSession(session);
- var updateSession = NewSyncPlayGroupUpdate(GroupUpdateType.GroupJoined, GetInfo());
+ var updateSession = new SyncPlayGroupJoinedUpdate(GroupId, GetInfo());
SendGroupUpdate(session, SyncPlayBroadcastType.CurrentSession, updateSession, cancellationToken);
- var updateOthers = NewSyncPlayGroupUpdate(GroupUpdateType.UserJoined, session.UserName);
+ var updateOthers = new SyncPlayUserJoinedUpdate(GroupId, session.UserName);
SendGroupUpdate(session, SyncPlayBroadcastType.AllExceptCurrentSession, updateOthers, cancellationToken);
_state.SessionJoined(this, _state.Type, session, cancellationToken);
@@ -314,10 +314,10 @@ namespace Emby.Server.Implementations.SyncPlay
RemoveSession(session);
- var updateSession = NewSyncPlayGroupUpdate(GroupUpdateType.GroupLeft, GroupId.ToString());
+ var updateSession = new SyncPlayGroupLeftUpdate(GroupId, GroupId.ToString());
SendGroupUpdate(session, SyncPlayBroadcastType.CurrentSession, updateSession, cancellationToken);
- var updateOthers = NewSyncPlayGroupUpdate(GroupUpdateType.UserLeft, session.UserName);
+ var updateOthers = new SyncPlayUserLeftUpdate(GroupId, session.UserName);
SendGroupUpdate(session, SyncPlayBroadcastType.AllExceptCurrentSession, updateOthers, cancellationToken);
_logger.LogInformation("Session {SessionId} left group {GroupId}.", session.Id, GroupId.ToString());
@@ -426,12 +426,6 @@ namespace Emby.Server.Implementations.SyncPlay
}
/// <inheritdoc />
- public GroupUpdate<T> NewSyncPlayGroupUpdate<T>(GroupUpdateType type, T data)
- {
- return new GroupUpdate<T>(GroupId, type, data);
- }
-
- /// <inheritdoc />
public long SanitizePositionTicks(long? positionTicks)
{
var ticks = positionTicks ?? 0;
diff --git a/Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs b/Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs
index fdfff8f3b..b45d75455 100644
--- a/Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs
+++ b/Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs
@@ -100,7 +100,7 @@ namespace Emby.Server.Implementations.SyncPlay
}
/// <inheritdoc />
- public void NewGroup(SessionInfo session, NewGroupRequest request, CancellationToken cancellationToken)
+ public GroupInfoDto NewGroup(SessionInfo session, NewGroupRequest request, CancellationToken cancellationToken)
{
if (session is null)
{
@@ -132,6 +132,7 @@ namespace Emby.Server.Implementations.SyncPlay
UpdateSessionsCounter(session.UserId, 1);
group.CreateGroup(session, request, cancellationToken);
+ return group.GetInfo();
}
}
@@ -159,7 +160,7 @@ namespace Emby.Server.Implementations.SyncPlay
{
_logger.LogWarning("Session {SessionId} tried to join group {GroupId} that does not exist.", session.Id, request.GroupId);
- var error = new GroupUpdate<string>(Guid.Empty, GroupUpdateType.GroupDoesNotExist, string.Empty);
+ var error = new SyncPlayGroupDoesNotExistUpdate(Guid.Empty, string.Empty);
_sessionManager.SendSyncPlayGroupUpdate(session.Id, error, CancellationToken.None);
return;
}
@@ -171,7 +172,7 @@ namespace Emby.Server.Implementations.SyncPlay
{
_logger.LogWarning("Session {SessionId} tried to join group {GroupId} but does not have access to some content of the playing queue.", session.Id, group.GroupId.ToString());
- var error = new GroupUpdate<string>(group.GroupId, GroupUpdateType.LibraryAccessDenied, string.Empty);
+ var error = new SyncPlayLibraryAccessDeniedUpdate(group.GroupId, string.Empty);
_sessionManager.SendSyncPlayGroupUpdate(session.Id, error, CancellationToken.None);
return;
}
@@ -248,7 +249,7 @@ namespace Emby.Server.Implementations.SyncPlay
{
_logger.LogWarning("Session {SessionId} does not belong to any group.", session.Id);
- var error = new GroupUpdate<string>(Guid.Empty, GroupUpdateType.NotInGroup, string.Empty);
+ var error = new SyncPlayNotInGroupUpdate(Guid.Empty, string.Empty);
_sessionManager.SendSyncPlayGroupUpdate(session.Id, error, CancellationToken.None);
}
}
@@ -289,6 +290,31 @@ namespace Emby.Server.Implementations.SyncPlay
}
/// <inheritdoc />
+ public GroupInfoDto GetGroup(SessionInfo session, Guid groupId)
+ {
+ ArgumentNullException.ThrowIfNull(session);
+
+ var user = _userManager.GetUserById(session.UserId);
+
+ lock (_groupsLock)
+ {
+ foreach (var (_, group) in _groups)
+ {
+ // Locking required as group is not thread-safe.
+ lock (group)
+ {
+ if (group.GroupId.Equals(groupId) && group.HasAccessToPlayQueue(user))
+ {
+ return group.GetInfo();
+ }
+ }
+ }
+ }
+
+ return null;
+ }
+
+ /// <inheritdoc />
public void HandleRequest(SessionInfo session, IGroupPlaybackRequest request, CancellationToken cancellationToken)
{
if (session is null)
@@ -327,7 +353,7 @@ namespace Emby.Server.Implementations.SyncPlay
{
_logger.LogWarning("Session {SessionId} does not belong to any group.", session.Id);
- var error = new GroupUpdate<string>(Guid.Empty, GroupUpdateType.NotInGroup, string.Empty);
+ var error = new SyncPlayNotInGroupUpdate(Guid.Empty, string.Empty);
_sessionManager.SendSyncPlayGroupUpdate(session.Id, error, CancellationToken.None);
}
}
diff --git a/Emby.Server.Implementations/SystemManager.cs b/Emby.Server.Implementations/SystemManager.cs
index c4552474c..92b59b23c 100644
--- a/Emby.Server.Implementations/SystemManager.cs
+++ b/Emby.Server.Implementations/SystemManager.cs
@@ -1,9 +1,12 @@
+using System;
using System.Linq;
using System.Threading.Tasks;
+using Jellyfin.Server.Implementations.StorageHelpers;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Updates;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Library;
using MediaBrowser.Model.System;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Hosting;
@@ -19,6 +22,7 @@ public class SystemManager : ISystemManager
private readonly IServerConfigurationManager _configurationManager;
private readonly IStartupOptions _startupOptions;
private readonly IInstallationManager _installationManager;
+ private readonly ILibraryManager _libraryManager;
/// <summary>
/// Initializes a new instance of the <see cref="SystemManager"/> class.
@@ -29,13 +33,15 @@ public class SystemManager : ISystemManager
/// <param name="configurationManager">Instance of <see cref="IServerConfigurationManager"/>.</param>
/// <param name="startupOptions">Instance of <see cref="IStartupOptions"/>.</param>
/// <param name="installationManager">Instance of <see cref="IInstallationManager"/>.</param>
+ /// <param name="libraryManager">Instance of <see cref="ILibraryManager"/>.</param>
public SystemManager(
IHostApplicationLifetime applicationLifetime,
IServerApplicationHost applicationHost,
IServerApplicationPaths applicationPaths,
IServerConfigurationManager configurationManager,
IStartupOptions startupOptions,
- IInstallationManager installationManager)
+ IInstallationManager installationManager,
+ ILibraryManager libraryManager)
{
_applicationLifetime = applicationLifetime;
_applicationHost = applicationHost;
@@ -43,6 +49,7 @@ public class SystemManager : ISystemManager
_configurationManager = configurationManager;
_startupOptions = startupOptions;
_installationManager = installationManager;
+ _libraryManager = libraryManager;
}
/// <inheritdoc />
@@ -53,9 +60,11 @@ public class SystemManager : ISystemManager
HasPendingRestart = _applicationHost.HasPendingRestart,
IsShuttingDown = _applicationLifetime.ApplicationStopping.IsCancellationRequested,
Version = _applicationHost.ApplicationVersionString,
+ ProductName = _applicationHost.Name,
WebSocketPortNumber = _applicationHost.HttpPort,
CompletedInstallations = _installationManager.CompletedInstallations.ToArray(),
Id = _applicationHost.SystemId,
+#pragma warning disable CS0618 // Type or member is obsolete
ProgramDataPath = _applicationPaths.ProgramDataPath,
WebPath = _applicationPaths.WebPath,
LogPath = _applicationPaths.LogDirectoryPath,
@@ -63,14 +72,39 @@ public class SystemManager : ISystemManager
InternalMetadataPath = _applicationPaths.InternalMetadataPath,
CachePath = _applicationPaths.CachePath,
TranscodingTempPath = _configurationManager.GetTranscodePath(),
+#pragma warning restore CS0618 // Type or member is obsolete
ServerName = _applicationHost.FriendlyName,
LocalAddress = _applicationHost.GetSmartApiUrl(request),
+ StartupWizardCompleted = _configurationManager.CommonConfiguration.IsStartupWizardCompleted,
SupportsLibraryMonitor = true,
PackageName = _startupOptions.PackageName,
CastReceiverApplications = _configurationManager.Configuration.CastReceiverApplications
};
}
+ /// <inheritdoc/>
+ public SystemStorageInfo GetSystemStorageInfo()
+ {
+ var virtualFolderInfos = _libraryManager.GetVirtualFolders().Select(e => new LibraryStorageInfo()
+ {
+ Id = Guid.Parse(e.ItemId),
+ Name = e.Name,
+ Folders = e.Locations.Select(f => StorageHelper.GetFreeSpaceOf(f)).ToArray()
+ });
+
+ return new SystemStorageInfo()
+ {
+ ProgramDataFolder = StorageHelper.GetFreeSpaceOf(_applicationPaths.ProgramDataPath),
+ WebFolder = StorageHelper.GetFreeSpaceOf(_applicationPaths.WebPath),
+ LogFolder = StorageHelper.GetFreeSpaceOf(_applicationPaths.LogDirectoryPath),
+ ImageCacheFolder = StorageHelper.GetFreeSpaceOf(_applicationPaths.ImageCachePath),
+ InternalMetadataFolder = StorageHelper.GetFreeSpaceOf(_applicationPaths.InternalMetadataPath),
+ CacheFolder = StorageHelper.GetFreeSpaceOf(_applicationPaths.CachePath),
+ TranscodingTempFolder = StorageHelper.GetFreeSpaceOf(_configurationManager.GetTranscodePath()),
+ Libraries = virtualFolderInfos.ToArray()
+ };
+ }
+
/// <inheritdoc />
public PublicSystemInfo GetPublicSystemInfo(HttpRequest request)
{