Переглянути джерело

Merge remote-tracking branch 'upstream/master' into authenticationdb-efcore

Cody Robibero 3 роки тому
батько
коміт
ff9d14c811
100 змінених файлів з 1740 додано та 816 видалено
  1. 3 0
      .gitignore
  2. 1 0
      CONTRIBUTORS.md
  3. 1 1
      Dockerfile
  4. 1 1
      Dockerfile.arm
  5. 1 1
      Dockerfile.arm64
  6. 1 1
      Emby.Dlna/ContentDirectory/ServerItem.cs
  7. 1 1
      Emby.Dlna/Didl/DidlBuilder.cs
  8. 39 29
      Emby.Dlna/DlnaManager.cs
  9. 2 2
      Emby.Server.Implementations/ApplicationHost.cs
  10. 4 4
      Emby.Server.Implementations/Channels/ChannelManager.cs
  11. 14 20
      Emby.Server.Implementations/Collections/CollectionManager.cs
  12. 1 1
      Emby.Server.Implementations/Data/BaseSqliteRepository.cs
  13. 18 2
      Emby.Server.Implementations/Data/SqliteItemRepository.cs
  14. 4 4
      Emby.Server.Implementations/Data/SqliteUserDataRepository.cs
  15. 19 22
      Emby.Server.Implementations/Dto/DtoService.cs
  16. 3 2
      Emby.Server.Implementations/Emby.Server.Implementations.csproj
  17. 2 2
      Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs
  18. 3 0
      Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs
  19. 2 2
      Emby.Server.Implementations/HttpServer/WebSocketConnection.cs
  20. 1 1
      Emby.Server.Implementations/IStartupOptions.cs
  21. 6 6
      Emby.Server.Implementations/Images/CollectionFolderImageProvider.cs
  22. 5 8
      Emby.Server.Implementations/Library/LibraryManager.cs
  23. 6 6
      Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs
  24. 24 19
      Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs
  25. 0 0
      Emby.Server.Implementations/Library/Resolvers/GenericVideoResolver.cs
  26. 1 1
      Emby.Server.Implementations/Library/Resolvers/ItemResolver.cs
  27. 2 2
      Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs
  28. 2 1
      Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs
  29. 8 5
      Emby.Server.Implementations/Library/Validators/StudiosValidator.cs
  30. 3 5
      Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs
  31. 20 27
      Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
  32. 0 5
      Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs
  33. 0 1
      Emby.Server.Implementations/LiveTv/EmbyTV/EpgChannelData.cs
  34. 1 1
      Emby.Server.Implementations/LiveTv/EmbyTV/IRecorder.cs
  35. 5 7
      Emby.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs
  36. 113 509
      Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs
  37. 36 0
      Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/BroadcasterDto.cs
  38. 24 0
      Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/CaptionDto.cs
  39. 48 0
      Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/CastDto.cs
  40. 31 0
      Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ChannelDto.cs
  41. 24 0
      Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ContentRatingDto.cs
  42. 42 0
      Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/CrewDto.cs
  43. 39 0
      Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/DayDto.cs
  44. 24 0
      Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/Description1000Dto.cs
  45. 24 0
      Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/Description100Dto.cs
  46. 25 0
      Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/DescriptionsProgramDto.cs
  47. 18 0
      Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/EventDetailsDto.cs
  48. 24 0
      Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/GracenoteDto.cs
  49. 37 0
      Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/HeadendsDto.cs
  50. 72 0
      Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ImageDataDto.cs
  51. 42 0
      Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/LineupDto.cs
  52. 37 0
      Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/LineupsDto.cs
  53. 36 0
      Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/LogoDto.cs
  54. 48 0
      Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MapDto.cs
  55. 30 0
      Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MetadataDto.cs
  56. 18 0
      Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MetadataProgramsDto.cs
  57. 42 0
      Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MetadataScheduleDto.cs
  58. 31 0
      Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MovieDto.cs
  59. 24 0
      Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MultipartDto.cs
  60. 157 0
      Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ProgramDetailsDto.cs
  61. 91 0
      Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ProgramDto.cs
  62. 42 0
      Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/QualityRatingDto.cs
  63. 24 0
      Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/RatingDto.cs
  64. 24 0
      Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/RecommendationDto.cs
  65. 25 0
      Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/RequestScheduleForChannelDto.cs
  66. 25 0
      Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ShowImagesDto.cs
  67. 67 0
      Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/StationDto.cs
  68. 18 0
      Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/TitleDto.cs
  69. 36 0
      Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/TokenDto.cs
  70. 3 3
      Emby.Server.Implementations/LiveTv/LiveTvManager.cs
  71. 1 0
      Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs
  72. 22 16
      Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs
  73. 12 10
      Emby.Server.Implementations/Localization/Core/af.json
  74. 2 2
      Emby.Server.Implementations/Localization/Core/bg-BG.json
  75. 1 1
      Emby.Server.Implementations/Localization/Core/ca.json
  76. 1 1
      Emby.Server.Implementations/Localization/Core/cs.json
  77. 7 5
      Emby.Server.Implementations/Localization/Core/el.json
  78. 3 3
      Emby.Server.Implementations/Localization/Core/en-US.json
  79. 10 8
      Emby.Server.Implementations/Localization/Core/es-MX.json
  80. 1 1
      Emby.Server.Implementations/Localization/Core/es.json
  81. 1 1
      Emby.Server.Implementations/Localization/Core/hu.json
  82. 3 3
      Emby.Server.Implementations/Localization/Core/it.json
  83. 1 1
      Emby.Server.Implementations/Localization/Core/ja.json
  84. 1 1
      Emby.Server.Implementations/Localization/Core/kk.json
  85. 1 1
      Emby.Server.Implementations/Localization/Core/ko.json
  86. 1 1
      Emby.Server.Implementations/Localization/Core/ml.json
  87. 4 3
      Emby.Server.Implementations/Localization/Core/pl.json
  88. 1 0
      Emby.Server.Implementations/Localization/Core/pr.json
  89. 3 1
      Emby.Server.Implementations/Localization/Core/pt-BR.json
  90. 2 1
      Emby.Server.Implementations/Localization/Core/pt.json
  91. 1 1
      Emby.Server.Implementations/Localization/Core/ru.json
  92. 1 1
      Emby.Server.Implementations/Localization/Core/sk.json
  93. 5 3
      Emby.Server.Implementations/Localization/Core/sv.json
  94. 1 1
      Emby.Server.Implementations/Localization/Core/tr.json
  95. 2 2
      Emby.Server.Implementations/Localization/Core/vi.json
  96. 5 5
      Emby.Server.Implementations/Localization/Core/zh-CN.json
  97. 6 3
      Emby.Server.Implementations/Localization/Core/zh-HK.json
  98. 4 2
      Emby.Server.Implementations/Localization/Core/zh-TW.json
  99. 27 36
      Emby.Server.Implementations/Localization/LocalizationManager.cs
  100. 5 0
      Emby.Server.Implementations/Playlists/ManualPlaylistsFolder.cs

+ 3 - 0
.gitignore

@@ -278,3 +278,6 @@ web/
 web-src.*
 MediaBrowser.WebDashboard/jellyfin-web
 apiclient/generated
+
+# Omnisharp crash logs
+mono_crash.*.json

+ 1 - 0
CONTRIBUTORS.md

@@ -212,4 +212,5 @@
  - [Tim Hobbs](https://github.com/timhobbs)
  - [SvenVandenbrande](https://github.com/SvenVandenbrande)
  - [olsh](https://github.com/olsh)
+ - [lbenini](https://github.com/lbenini)
  - [gnuyent](https://github.com/gnuyent)

+ 1 - 1
Dockerfile

@@ -8,7 +8,7 @@ RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-
  && npm ci --no-audit --unsafe-perm \
  && mv dist /dist
 
-FROM debian:buster-slim as app
+FROM debian:bullseye-slim as app
 
 # https://askubuntu.com/questions/972516/debian-frontend-environment-variable
 ARG DEBIAN_FRONTEND="noninteractive"

+ 1 - 1
Dockerfile.arm

@@ -14,7 +14,7 @@ RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-
  && mv dist /dist
 
 FROM multiarch/qemu-user-static:x86_64-arm as qemu
-FROM arm32v7/debian:buster-slim as app
+FROM arm32v7/debian:bullseye-slim as app
 
 # https://askubuntu.com/questions/972516/debian-frontend-environment-variable
 ARG DEBIAN_FRONTEND="noninteractive"

+ 1 - 1
Dockerfile.arm64

@@ -14,7 +14,7 @@ RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-
  && mv dist /dist
 
 FROM multiarch/qemu-user-static:x86_64-aarch64 as qemu
-FROM arm64v8/debian:buster-slim as app
+FROM arm64v8/debian:bullseye-slim as app
 
 # https://askubuntu.com/questions/972516/debian-frontend-environment-variable
 ARG DEBIAN_FRONTEND="noninteractive"

+ 1 - 1
Emby.Dlna/ContentDirectory/ServerItem.cs

@@ -17,7 +17,7 @@ namespace Emby.Dlna.ContentDirectory
         {
             Item = item;
 
-            if (item is IItemByName && !(item is Folder))
+            if (item is IItemByName && item is not Folder)
             {
                 StubType = Dlna.ContentDirectory.StubType.Folder;
             }

+ 1 - 1
Emby.Dlna/Didl/DidlBuilder.cs

@@ -748,7 +748,7 @@ namespace Emby.Dlna.Didl
                 AddValue(writer, "upnp", "publisher", studio, NsUpnp);
             }
 
-            if (!(item is Folder))
+            if (item is not Folder)
             {
                 if (filter.Contains("dc:description"))
                 {

+ 39 - 29
Emby.Dlna/DlnaManager.cs

@@ -1,7 +1,4 @@
-#nullable disable
-
 #pragma warning disable CS1591
-
 using System;
 using System.Collections.Generic;
 using System.Globalization;
@@ -96,12 +93,14 @@ namespace Emby.Dlna
             }
         }
 
+        /// <inheritdoc />
         public DeviceProfile GetDefaultProfile()
         {
             return new DefaultProfile();
         }
 
-        public DeviceProfile GetProfile(DeviceIdentification deviceInfo)
+        /// <inheritdoc />
+        public DeviceProfile? GetProfile(DeviceIdentification deviceInfo)
         {
             if (deviceInfo == null)
             {
@@ -111,13 +110,13 @@ namespace Emby.Dlna
             var profile = GetProfiles()
                 .FirstOrDefault(i => i.Identification != null && IsMatch(deviceInfo, i.Identification));
 
-            if (profile != null)
+            if (profile == null)
             {
-                _logger.LogDebug("Found matching device profile: {ProfileName}", profile.Name);
+                LogUnmatchedProfile(deviceInfo);
             }
             else
             {
-                LogUnmatchedProfile(deviceInfo);
+                _logger.LogDebug("Found matching device profile: {ProfileName}", profile.Name);
             }
 
             return profile;
@@ -187,7 +186,8 @@ namespace Emby.Dlna
             }
         }
 
-        public DeviceProfile GetProfile(IHeaderDictionary headers)
+        /// <inheritdoc />
+        public DeviceProfile? GetProfile(IHeaderDictionary headers)
         {
             if (headers == null)
             {
@@ -195,15 +195,13 @@ namespace Emby.Dlna
             }
 
             var profile = GetProfiles().FirstOrDefault(i => i.Identification != null && IsMatch(headers, i.Identification));
-
-            if (profile != null)
+            if (profile == null)
             {
-                _logger.LogDebug("Found matching device profile: {0}", profile.Name);
+                _logger.LogDebug("No matching device profile found. {@Headers}", headers);
             }
             else
             {
-                var headerString = string.Join(", ", headers.Select(i => string.Format(CultureInfo.InvariantCulture, "{0}={1}", i.Key, i.Value)));
-                _logger.LogDebug("No matching device profile found. {0}", headerString);
+                _logger.LogDebug("Found matching device profile: {0}", profile.Name);
             }
 
             return profile;
@@ -253,19 +251,19 @@ namespace Emby.Dlna
                 return xmlFies
                     .Select(i => ParseProfileFile(i, type))
                     .Where(i => i != null)
-                    .ToList();
+                    .ToList()!; // We just filtered out all the nulls
             }
             catch (IOException)
             {
-                return new List<DeviceProfile>();
+                return Array.Empty<DeviceProfile>();
             }
         }
 
-        private DeviceProfile ParseProfileFile(string path, DeviceProfileType type)
+        private DeviceProfile? ParseProfileFile(string path, DeviceProfileType type)
         {
             lock (_profiles)
             {
-                if (_profiles.TryGetValue(path, out Tuple<InternalProfileInfo, DeviceProfile> profileTuple))
+                if (_profiles.TryGetValue(path, out Tuple<InternalProfileInfo, DeviceProfile>? profileTuple))
                 {
                     return profileTuple.Item2;
                 }
@@ -293,7 +291,8 @@ namespace Emby.Dlna
             }
         }
 
-        public DeviceProfile GetProfile(string id)
+        /// <inheritdoc />
+        public DeviceProfile? GetProfile(string id)
         {
             if (string.IsNullOrEmpty(id))
             {
@@ -322,6 +321,7 @@ namespace Emby.Dlna
             }
         }
 
+        /// <inheritdoc />
         public IEnumerable<DeviceProfileInfo> GetProfileInfos()
         {
             return GetProfileInfosInternal().Select(i => i.Info);
@@ -329,17 +329,14 @@ namespace Emby.Dlna
 
         private InternalProfileInfo GetInternalProfileInfo(FileSystemMetadata file, DeviceProfileType type)
         {
-            return new InternalProfileInfo
-            {
-                Path = file.FullName,
-
-                Info = new DeviceProfileInfo
+            return new InternalProfileInfo(
+                new DeviceProfileInfo
                 {
                     Id = file.FullName.ToLowerInvariant().GetMD5().ToString("N", CultureInfo.InvariantCulture),
                     Name = _fileSystem.GetFileNameWithoutExtension(file),
                     Type = type
-                }
-            };
+                },
+                file.FullName);
         }
 
         private async Task ExtractSystemProfilesAsync()
@@ -359,7 +356,8 @@ namespace Emby.Dlna
                     systemProfilesPath,
                     Path.GetFileName(name.AsSpan()).Slice(namespaceName.Length));
 
-                using (var stream = _assembly.GetManifestResourceStream(name))
+                // The stream should exist as we just got its name from GetManifestResourceNames
+                using (var stream = _assembly.GetManifestResourceStream(name)!)
                 {
                     var fileInfo = _fileSystem.GetFileInfo(path);
 
@@ -380,6 +378,7 @@ namespace Emby.Dlna
             Directory.CreateDirectory(UserProfilesPath);
         }
 
+        /// <inheritdoc />
         public void DeleteProfile(string id)
         {
             var info = GetProfileInfosInternal().First(i => string.Equals(id, i.Info.Id, StringComparison.OrdinalIgnoreCase));
@@ -397,6 +396,7 @@ namespace Emby.Dlna
             }
         }
 
+        /// <inheritdoc />
         public void CreateProfile(DeviceProfile profile)
         {
             profile = ReserializeProfile(profile);
@@ -412,6 +412,7 @@ namespace Emby.Dlna
             SaveProfile(profile, path, DeviceProfileType.User);
         }
 
+        /// <inheritdoc />
         public void UpdateProfile(DeviceProfile profile)
         {
             profile = ReserializeProfile(profile);
@@ -470,9 +471,11 @@ namespace Emby.Dlna
 
             var json = JsonSerializer.Serialize(profile, _jsonOptions);
 
-            return JsonSerializer.Deserialize<DeviceProfile>(json, _jsonOptions);
+            // Output can't be null if the input isn't null
+            return JsonSerializer.Deserialize<DeviceProfile>(json, _jsonOptions)!;
         }
 
+        /// <inheritdoc />
         public string GetServerDescriptionXml(IHeaderDictionary headers, string serverUuId, string serverAddress)
         {
             var profile = GetDefaultProfile();
@@ -482,6 +485,7 @@ namespace Emby.Dlna
             return new DescriptionXmlBuilder(profile, serverUuId, serverAddress, _appHost.FriendlyName, serverId).GetXml();
         }
 
+        /// <inheritdoc />
         public ImageStream GetIcon(string filename)
         {
             var format = filename.EndsWith(".png", StringComparison.OrdinalIgnoreCase)
@@ -499,9 +503,15 @@ namespace Emby.Dlna
 
         private class InternalProfileInfo
         {
-            internal DeviceProfileInfo Info { get; set; }
+            internal InternalProfileInfo(DeviceProfileInfo info, string path)
+            {
+                Info = info;
+                Path = path;
+            }
+
+            internal DeviceProfileInfo Info { get; }
 
-            internal string Path { get; set; }
+            internal string Path { get; }
         }
     }
 

+ 2 - 2
Emby.Server.Implementations/ApplicationHost.cs

@@ -453,6 +453,7 @@ namespace Emby.Server.Implementations
         /// <summary>
         /// Runs the startup tasks.
         /// </summary>
+        /// <param name="cancellationToken">The cancellation token.</param>
         /// <returns><see cref="Task" />.</returns>
         public async Task RunStartupTasksAsync(CancellationToken cancellationToken)
         {
@@ -466,7 +467,7 @@ namespace Emby.Server.Implementations
 
             _mediaEncoder.SetFFmpegPath();
 
-            Logger.LogInformation("ServerId: {0}", SystemId);
+            Logger.LogInformation("ServerId: {ServerId}", SystemId);
 
             var entryPoints = GetExports<IServerEntryPoint>();
 
@@ -1089,7 +1090,6 @@ namespace Emby.Server.Implementations
                 ServerName = FriendlyName,
                 LocalAddress = GetSmartApiUrl(source),
                 SupportsLibraryMonitor = true,
-                EncoderLocation = _mediaEncoder.EncoderLocation,
                 SystemArchitecture = RuntimeInformation.OSArchitecture,
                 PackageName = _startupOptions.PackageName
             };

+ 4 - 4
Emby.Server.Implementations/Channels/ChannelManager.cs

@@ -102,7 +102,7 @@ namespace Emby.Server.Implementations.Channels
             var internalChannel = _libraryManager.GetItemById(item.ChannelId);
             var channel = Channels.FirstOrDefault(i => GetInternalChannelId(i.Name).Equals(internalChannel.Id));
 
-            return !(channel is IDisableMediaSourceDisplay);
+            return channel is not IDisableMediaSourceDisplay;
         }
 
         /// <inheritdoc />
@@ -880,7 +880,7 @@ namespace Emby.Server.Implementations.Channels
             }
         }
 
-        private async Task CacheResponse(object result, string path)
+        private async Task CacheResponse(ChannelItemResult result, string path)
         {
             try
             {
@@ -1079,11 +1079,11 @@ namespace Emby.Server.Implementations.Channels
 
             // was used for status
             // if (!string.Equals(item.ExternalEtag ?? string.Empty, info.Etag ?? string.Empty, StringComparison.Ordinal))
-            //{
+            // {
             //    item.ExternalEtag = info.Etag;
             //    forceUpdate = true;
             //    _logger.LogDebug("Forcing update due to ExternalEtag {0}", item.Name);
-            //}
+            // }
 
             if (!internalChannelId.Equals(item.ChannelId))
             {

+ 14 - 20
Emby.Server.Implementations/Collections/CollectionManager.cs

@@ -1,5 +1,3 @@
-#nullable disable
-
 using System;
 using System.Collections.Generic;
 using System.IO;
@@ -63,13 +61,13 @@ namespace Emby.Server.Implementations.Collections
         }
 
         /// <inheritdoc />
-        public event EventHandler<CollectionCreatedEventArgs> CollectionCreated;
+        public event EventHandler<CollectionCreatedEventArgs>? CollectionCreated;
 
         /// <inheritdoc />
-        public event EventHandler<CollectionModifiedEventArgs> ItemsAddedToCollection;
+        public event EventHandler<CollectionModifiedEventArgs>? ItemsAddedToCollection;
 
         /// <inheritdoc />
-        public event EventHandler<CollectionModifiedEventArgs> ItemsRemovedFromCollection;
+        public event EventHandler<CollectionModifiedEventArgs>? ItemsRemovedFromCollection;
 
         private IEnumerable<Folder> FindFolders(string path)
         {
@@ -80,7 +78,7 @@ namespace Emby.Server.Implementations.Collections
                 .Where(i => _fileSystem.AreEqual(path, i.Path) || _fileSystem.ContainsSubPath(i.Path, path));
         }
 
-        internal async Task<Folder> EnsureLibraryFolder(string path, bool createIfNeeded)
+        internal async Task<Folder?> EnsureLibraryFolder(string path, bool createIfNeeded)
         {
             var existingFolder = FindFolders(path).FirstOrDefault();
             if (existingFolder != null)
@@ -97,7 +95,7 @@ namespace Emby.Server.Implementations.Collections
 
             var libraryOptions = new LibraryOptions
             {
-                PathInfos = new[] { new MediaPathInfo { Path = path } },
+                PathInfos = new[] { new MediaPathInfo(path) },
                 EnableRealtimeMonitor = false,
                 SaveLocalMetadata = true
             };
@@ -114,7 +112,7 @@ namespace Emby.Server.Implementations.Collections
             return Path.Combine(_appPaths.DataPath, "collections");
         }
 
-        private Task<Folder> GetCollectionsFolder(bool createIfNeeded)
+        private Task<Folder?> GetCollectionsFolder(bool createIfNeeded)
         {
             return EnsureLibraryFolder(GetCollectionsFolderPath(), createIfNeeded);
         }
@@ -203,8 +201,7 @@ namespace Emby.Server.Implementations.Collections
 
         private async Task AddToCollectionAsync(Guid collectionId, IEnumerable<Guid> ids, bool fireEvent, MetadataRefreshOptions refreshOptions)
         {
-            var collection = _libraryManager.GetItemById(collectionId) as BoxSet;
-            if (collection == null)
+            if (_libraryManager.GetItemById(collectionId) is not BoxSet collection)
             {
                 throw new ArgumentException("No collection exists with the supplied Id");
             }
@@ -256,9 +253,7 @@ namespace Emby.Server.Implementations.Collections
         /// <inheritdoc />
         public async Task RemoveFromCollectionAsync(Guid collectionId, IEnumerable<Guid> itemIds)
         {
-            var collection = _libraryManager.GetItemById(collectionId) as BoxSet;
-
-            if (collection == null)
+            if (_libraryManager.GetItemById(collectionId) is not BoxSet collection)
             {
                 throw new ArgumentException("No collection exists with the supplied Id");
             }
@@ -312,11 +307,7 @@ namespace Emby.Server.Implementations.Collections
 
             foreach (var item in items)
             {
-                if (item is not ISupportsBoxSetGrouping)
-                {
-                    results[item.Id] = item;
-                }
-                else
+                if (item is ISupportsBoxSetGrouping)
                 {
                     var itemId = item.Id;
 
@@ -340,6 +331,7 @@ namespace Emby.Server.Implementations.Collections
                     }
 
                     var alreadyInResults = false;
+
                     // this is kind of a performance hack because only Video has alternate versions that should be in a box set?
                     if (item is Video video)
                     {
@@ -355,11 +347,13 @@ namespace Emby.Server.Implementations.Collections
                         }
                     }
 
-                    if (!alreadyInResults)
+                    if (alreadyInResults)
                     {
-                        results[itemId] = item;
+                        continue;
                     }
                 }
+
+                results[item.Id] = item;
             }
 
             return results.Values;

+ 1 - 1
Emby.Server.Implementations/Data/BaseSqliteRepository.cs

@@ -61,7 +61,7 @@ namespace Emby.Server.Implementations.Data
         protected virtual int? CacheSize => null;
 
         /// <summary>
-        /// Gets the journal mode. <see href="https://www.sqlite.org/pragma.html#pragma_journal_mode" />
+        /// Gets the journal mode. <see href="https://www.sqlite.org/pragma.html#pragma_journal_mode" />.
         /// </summary>
         /// <value>The journal mode.</value>
         protected virtual string JournalMode => "TRUNCATE";

+ 18 - 2
Emby.Server.Implementations/Data/SqliteItemRepository.cs

@@ -75,6 +75,12 @@ namespace Emby.Server.Implementations.Data
         /// <summary>
         /// Initializes a new instance of the <see cref="SqliteItemRepository"/> class.
         /// </summary>
+        /// <param name="config">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
+        /// <param name="appHost">Instance of the <see cref="IServerApplicationHost"/> interface.</param>
+        /// <param name="logger">Instance of the <see cref="ILogger{SqliteItemRepository}"/> interface.</param>
+        /// <param name="localization">Instance of the <see cref="ILocalizationManager"/> interface.</param>
+        /// <param name="imageProcessor">Instance of the <see cref="IImageProcessor"/> interface.</param>
+        /// <exception cref="ArgumentNullException">config is null.</exception>
         public SqliteItemRepository(
             IServerConfigurationManager config,
             IServerApplicationHost appHost,
@@ -1135,15 +1141,25 @@ namespace Emby.Server.Implementations.Data
                 Path = RestorePath(path.ToString())
             };
 
-            if (long.TryParse(dateModified, NumberStyles.Any, CultureInfo.InvariantCulture, out var ticks))
+            if (long.TryParse(dateModified, NumberStyles.Any, CultureInfo.InvariantCulture, out var ticks)
+                && ticks >= DateTime.MinValue.Ticks
+                && ticks <= DateTime.MaxValue.Ticks)
             {
                 image.DateModified = new DateTime(ticks, DateTimeKind.Utc);
             }
+            else
+            {
+                return null;
+            }
 
             if (Enum.TryParse(imageType.ToString(), true, out ImageType type))
             {
                 image.Type = type;
             }
+            else
+            {
+                return null;
+            }
 
             // Optional parameters: width*height*blurhash
             if (nextSegment + 1 < value.Length - 1)
@@ -4879,7 +4895,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
 
             foreach (var t in _knownTypes)
             {
-                dict[t.Name] = t.FullName ;
+                dict[t.Name] = t.FullName;
             }
 
             dict["Program"] = typeof(LiveTvProgram).FullName;

+ 4 - 4
Emby.Server.Implementations/Data/SqliteUserDataRepository.cs

@@ -174,7 +174,6 @@ namespace Emby.Server.Implementations.Data
         /// <param name="key">The key.</param>
         /// <param name="userData">The user data.</param>
         /// <param name="cancellationToken">The cancellation token.</param>
-        /// <returns>Task.</returns>
         public void PersistUserData(long internalUserId, string key, UserItemData userData, CancellationToken cancellationToken)
         {
             cancellationToken.ThrowIfCancellationRequested();
@@ -319,8 +318,8 @@ namespace Emby.Server.Implementations.Data
         /// <summary>
         /// Return all user-data associated with the given user.
         /// </summary>
-        /// <param name="internalUserId"></param>
-        /// <returns></returns>
+        /// <param name="internalUserId">The internal user id.</param>
+        /// <returns>The list of user item data.</returns>
         public List<UserItemData> GetAllUserData(long internalUserId)
         {
             if (internalUserId <= 0)
@@ -349,7 +348,8 @@ namespace Emby.Server.Implementations.Data
         /// <summary>
         /// Read a row from the specified reader into the provided userData object.
         /// </summary>
-        /// <param name="reader"></param>
+        /// <param name="reader">The list of result set values.</param>
+        /// <returns>The user item data.</returns>
         private UserItemData ReadRow(IReadOnlyList<ResultSetValue> reader)
         {
             var userData = new UserItemData();

+ 19 - 22
Emby.Server.Implementations/Dto/DtoService.cs

@@ -807,7 +807,7 @@ namespace Emby.Server.Implementations.Dto
 
             dto.MediaType = item.MediaType;
 
-            if (!(item is LiveTvProgram))
+            if (item is not LiveTvProgram)
             {
                 dto.LocationType = item.LocationType;
             }
@@ -928,9 +928,9 @@ namespace Emby.Server.Implementations.Dto
                 }
 
                 // if (options.ContainsField(ItemFields.MediaSourceCount))
-                //{
+                // {
                 // Songs always have one
-                //}
+                // }
             }
 
             if (item is IHasArtist hasArtist)
@@ -938,10 +938,10 @@ namespace Emby.Server.Implementations.Dto
                 dto.Artists = hasArtist.Artists;
 
                 // var artistItems = _libraryManager.GetArtists(new InternalItemsQuery
-                //{
+                // {
                 //    EnableTotalRecordCount = false,
                 //    ItemIds = new[] { item.Id.ToString("N", CultureInfo.InvariantCulture) }
-                //});
+                // });
 
                 // dto.ArtistItems = artistItems.Items
                 //    .Select(i =>
@@ -958,7 +958,7 @@ namespace Emby.Server.Implementations.Dto
                 // Include artists that are not in the database yet, e.g., just added via metadata editor
                 // var foundArtists = artistItems.Items.Select(i => i.Item1.Name).ToList();
                 dto.ArtistItems = hasArtist.Artists
-                    //.Except(foundArtists, new DistinctNameComparer())
+                    // .Except(foundArtists, new DistinctNameComparer())
                     .Select(i =>
                     {
                         // This should not be necessary but we're seeing some cases of it
@@ -990,10 +990,10 @@ namespace Emby.Server.Implementations.Dto
                 dto.AlbumArtist = hasAlbumArtist.AlbumArtists.FirstOrDefault();
 
                 // var artistItems = _libraryManager.GetAlbumArtists(new InternalItemsQuery
-                //{
+                // {
                 //    EnableTotalRecordCount = false,
                 //    ItemIds = new[] { item.Id.ToString("N", CultureInfo.InvariantCulture) }
-                //});
+                // });
 
                 // dto.AlbumArtists = artistItems.Items
                 //    .Select(i =>
@@ -1008,7 +1008,7 @@ namespace Emby.Server.Implementations.Dto
                 //    .ToList();
 
                 dto.AlbumArtists = hasAlbumArtist.AlbumArtists
-                    //.Except(foundArtists, new DistinctNameComparer())
+                    // .Except(foundArtists, new DistinctNameComparer())
                     .Select(i =>
                     {
                         // This should not be necessary but we're seeing some cases of it
@@ -1035,8 +1035,7 @@ namespace Emby.Server.Implementations.Dto
             }
 
             // Add video info
-            var video = item as Video;
-            if (video != null)
+            if (item is Video video)
             {
                 dto.VideoType = video.VideoType;
                 dto.Video3DFormat = video.Video3DFormat;
@@ -1075,9 +1074,7 @@ namespace Emby.Server.Implementations.Dto
             if (options.ContainsField(ItemFields.MediaStreams))
             {
                 // Add VideoInfo
-                var iHasMediaSources = item as IHasMediaSources;
-
-                if (iHasMediaSources != null)
+                if (item is IHasMediaSources)
                 {
                     MediaStream[] mediaStreams;
 
@@ -1146,7 +1143,7 @@ namespace Emby.Server.Implementations.Dto
                 // TODO maybe remove the if statement entirely
                 // if (options.ContainsField(ItemFields.SeriesPrimaryImage))
                 {
-                    episodeSeries = episodeSeries ?? episode.Series;
+                    episodeSeries ??= episode.Series;
                     if (episodeSeries != null)
                     {
                         dto.SeriesPrimaryImageTag = GetTagAndFillBlurhash(dto, episodeSeries, ImageType.Primary);
@@ -1159,7 +1156,7 @@ namespace Emby.Server.Implementations.Dto
 
                 if (options.ContainsField(ItemFields.SeriesStudio))
                 {
-                    episodeSeries = episodeSeries ?? episode.Series;
+                    episodeSeries ??= episode.Series;
                     if (episodeSeries != null)
                     {
                         dto.SeriesStudio = episodeSeries.Studios.FirstOrDefault();
@@ -1172,7 +1169,7 @@ namespace Emby.Server.Implementations.Dto
             {
                 dto.AirDays = series.AirDays;
                 dto.AirTime = series.AirTime;
-                dto.Status = series.Status.HasValue ? series.Status.Value.ToString() : null;
+                dto.Status = series.Status?.ToString();
             }
 
             // Add SeasonInfo
@@ -1185,7 +1182,7 @@ namespace Emby.Server.Implementations.Dto
 
                 if (options.ContainsField(ItemFields.SeriesStudio))
                 {
-                    series = series ?? season.Series;
+                    series ??= season.Series;
                     if (series != null)
                     {
                         dto.SeriesStudio = series.Studios.FirstOrDefault();
@@ -1196,7 +1193,7 @@ namespace Emby.Server.Implementations.Dto
                 // TODO maybe remove the if statement entirely
                 // if (options.ContainsField(ItemFields.SeriesPrimaryImage))
                 {
-                    series = series ?? season.Series;
+                    series ??= season.Series;
                     if (series != null)
                     {
                         dto.SeriesPrimaryImageTag = GetTagAndFillBlurhash(dto, series, ImageType.Primary);
@@ -1283,7 +1280,7 @@ namespace Emby.Server.Implementations.Dto
 
             var parent = currentItem.DisplayParent ?? currentItem.GetOwner() ?? currentItem.GetParent();
 
-            if (parent == null && !(originalItem is UserRootFolder) && !(originalItem is UserView) && !(originalItem is AggregateFolder) && !(originalItem is ICollectionFolder) && !(originalItem is Channel))
+            if (parent == null && originalItem is not UserRootFolder && originalItem is not UserView && originalItem is not AggregateFolder && originalItem is not ICollectionFolder && originalItem is not Channel)
             {
                 parent = _libraryManager.GetCollectionFolders(originalItem).FirstOrDefault();
             }
@@ -1317,7 +1314,7 @@ namespace Emby.Server.Implementations.Dto
             var imageTags = dto.ImageTags;
 
             while (((!(imageTags != null && imageTags.ContainsKey(ImageType.Logo)) && logoLimit > 0) || (!(imageTags != null && imageTags.ContainsKey(ImageType.Art)) && artLimit > 0) || (!(imageTags != null && imageTags.ContainsKey(ImageType.Thumb)) && thumbLimit > 0) || parent is Series) &&
-                (parent = parent ?? (isFirst ? GetImageDisplayParent(item, item) ?? owner : parent)) != null)
+                (parent ??= (isFirst ? GetImageDisplayParent(item, item) ?? owner : parent)) != null)
             {
                 if (parent == null)
                 {
@@ -1348,7 +1345,7 @@ namespace Emby.Server.Implementations.Dto
                     }
                 }
 
-                if (thumbLimit > 0 && !(imageTags != null && imageTags.ContainsKey(ImageType.Thumb)) && (dto.ParentThumbItemId == null || parent is Series) && !(parent is ICollectionFolder) && !(parent is UserView))
+                if (thumbLimit > 0 && !(imageTags != null && imageTags.ContainsKey(ImageType.Thumb)) && (dto.ParentThumbItemId == null || parent is Series) && parent is not ICollectionFolder && parent is not UserView)
                 {
                     var image = allImages.FirstOrDefault(i => i.Type == ImageType.Thumb);
 

+ 3 - 2
Emby.Server.Implementations/Emby.Server.Implementations.csproj

@@ -23,14 +23,15 @@
   </ItemGroup>
 
   <ItemGroup>
+    <PackageReference Include="DiscUtils.Udf" Version="0.16.4" />
     <PackageReference Include="Jellyfin.XmlTv" Version="10.6.2" />
     <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="5.0.2" />
     <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="5.0.0" />
     <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="5.0.0" />
     <PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="5.0.0" />
-    <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="5.0.8" />
+    <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="5.0.9" />
     <PackageReference Include="Mono.Nat" Version="3.0.1" />
-    <PackageReference Include="prometheus-net.DotNetRuntime" Version="4.1.0" />
+    <PackageReference Include="prometheus-net.DotNetRuntime" Version="4.2.0" />
     <PackageReference Include="sharpcompress" Version="0.28.3" />
     <PackageReference Include="SQLitePCL.pretty.netstandard" Version="3.1.0" />
     <PackageReference Include="DotNet.Glob" Version="3.1.2" />

+ 2 - 2
Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs

@@ -149,7 +149,7 @@ namespace Emby.Server.Implementations.EntryPoints
 
         private static bool EnableRefreshMessage(BaseItem item)
         {
-            if (!(item is Folder folder))
+            if (item is not Folder folder)
             {
                 return false;
             }
@@ -403,7 +403,7 @@ namespace Emby.Server.Implementations.EntryPoints
                 return false;
             }
 
-            if (item is IItemByName && !(item is MusicArtist))
+            if (item is IItemByName && item is not MusicArtist)
             {
                 return false;
             }

+ 3 - 0
Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs

@@ -37,6 +37,9 @@ namespace Emby.Server.Implementations.EntryPoints
         /// <summary>
         /// Initializes a new instance of the <see cref="UdpServerEntryPoint" /> class.
         /// </summary>
+        /// <param name="logger">Instance of the <see cref="ILogger{UdpServerEntryPoint}"/> interface.</param>
+        /// <param name="appHost">Instance of the <see cref="IServerApplicationHost"/> interface.</param>
+        /// <param name="configuration">Instance of the <see cref="IConfiguration"/> interface.</param>
         public UdpServerEntryPoint(
             ILogger<UdpServerEntryPoint> logger,
             IServerApplicationHost appHost,

+ 2 - 2
Emby.Server.Implementations/HttpServer/WebSocketConnection.cs

@@ -62,7 +62,7 @@ namespace Emby.Server.Implementations.HttpServer
         public event EventHandler<EventArgs>? Closed;
 
         /// <summary>
-        /// Gets or sets the remote end point.
+        /// Gets the remote end point.
         /// </summary>
         public IPAddress? RemoteEndPoint { get; }
 
@@ -82,7 +82,7 @@ namespace Emby.Server.Implementations.HttpServer
         public DateTime LastKeepAliveDate { get; set; }
 
         /// <summary>
-        /// Gets or sets the query string.
+        /// Gets the query string.
         /// </summary>
         /// <value>The query string.</value>
         public IQueryCollection QueryString { get; }

+ 1 - 1
Emby.Server.Implementations/IStartupOptions.cs

@@ -10,7 +10,7 @@ namespace Emby.Server.Implementations
         string? FFmpegPath { get; }
 
         /// <summary>
-        /// Gets the value of the --service command line option.
+        /// Gets a value value indicating whether to run as service by the --service command line option.
         /// </summary>
         bool IsService { get; }
 

+ 6 - 6
Emby.Server.Implementations/Images/CollectionFolderImageProvider.cs

@@ -30,27 +30,27 @@ namespace Emby.Server.Implementations.Images
 
             string[] includeItemTypes;
 
-            if (string.Equals(viewType, CollectionType.Movies))
+            if (string.Equals(viewType, CollectionType.Movies, StringComparison.Ordinal))
             {
                 includeItemTypes = new string[] { "Movie" };
             }
-            else if (string.Equals(viewType, CollectionType.TvShows))
+            else if (string.Equals(viewType, CollectionType.TvShows, StringComparison.Ordinal))
             {
                 includeItemTypes = new string[] { "Series" };
             }
-            else if (string.Equals(viewType, CollectionType.Music))
+            else if (string.Equals(viewType, CollectionType.Music, StringComparison.Ordinal))
             {
                 includeItemTypes = new string[] { "MusicAlbum" };
             }
-            else if (string.Equals(viewType, CollectionType.Books))
+            else if (string.Equals(viewType, CollectionType.Books, StringComparison.Ordinal))
             {
                 includeItemTypes = new string[] { "Book", "AudioBook" };
             }
-            else if (string.Equals(viewType, CollectionType.BoxSets))
+            else if (string.Equals(viewType, CollectionType.BoxSets, StringComparison.Ordinal))
             {
                 includeItemTypes = new string[] { "BoxSet" };
             }
-            else if (string.Equals(viewType, CollectionType.HomeVideos) || string.Equals(viewType, CollectionType.Photos))
+            else if (string.Equals(viewType, CollectionType.HomeVideos, StringComparison.Ordinal) || string.Equals(viewType, CollectionType.Photos, StringComparison.Ordinal))
             {
                 includeItemTypes = new string[] { "Video", "Photo" };
             }

+ 5 - 8
Emby.Server.Implementations/Library/LibraryManager.cs

@@ -287,14 +287,14 @@ namespace Emby.Server.Implementations.Library
 
             if (item is IItemByName)
             {
-                if (!(item is MusicArtist))
+                if (item is not MusicArtist)
                 {
                     return;
                 }
             }
             else if (!item.IsFolder)
             {
-                if (!(item is Video) && !(item is LiveTvChannel))
+                if (item is not Video && item is not LiveTvChannel)
                 {
                     return;
                 }
@@ -866,7 +866,7 @@ namespace Emby.Server.Implementations.Library
         {
             var path = Person.GetPath(name);
             var id = GetItemByNameId<Person>(path);
-            if (!(GetItemById(id) is Person item))
+            if (GetItemById(id) is not Person item)
             {
                 item = new Person
                 {
@@ -2118,7 +2118,7 @@ namespace Emby.Server.Implementations.Library
 
         public LibraryOptions GetLibraryOptions(BaseItem item)
         {
-            if (!(item is CollectionFolder collectionFolder))
+            if (item is not CollectionFolder collectionFolder)
             {
                 // List.Find is more performant than FirstOrDefault due to enumerator allocation
                 collectionFolder = GetCollectionFolders(item)
@@ -3173,10 +3173,7 @@ namespace Emby.Server.Implementations.Library
                 {
                     if (!list.Any(i => string.Equals(i.Path, location, StringComparison.Ordinal)))
                     {
-                        list.Add(new MediaPathInfo
-                        {
-                            Path = location
-                        });
+                        list.Add(new MediaPathInfo(location));
                     }
                 }
 

+ 6 - 6
Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs

@@ -21,11 +21,11 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
     /// </summary>
     public class AudioResolver : ItemResolver<MediaBrowser.Controller.Entities.Audio.Audio>, IMultiItemResolver
     {
-        private readonly ILibraryManager LibraryManager;
+        private readonly ILibraryManager _libraryManager;
 
         public AudioResolver(ILibraryManager libraryManager)
         {
-            LibraryManager = libraryManager;
+            _libraryManager = libraryManager;
         }
 
         /// <summary>
@@ -88,13 +88,13 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
                 }
 
                 var files = args.FileSystemChildren
-                    .Where(i => !LibraryManager.IgnoreFile(i, args.Parent))
+                    .Where(i => !_libraryManager.IgnoreFile(i, args.Parent))
                     .ToList();
 
                 return FindAudio<AudioBook>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, false);
             }
 
-            if (LibraryManager.IsAudioFile(args.Path))
+            if (_libraryManager.IsAudioFile(args.Path))
             {
                 var extension = Path.GetExtension(args.Path);
 
@@ -107,7 +107,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
                 var isMixedCollectionType = string.IsNullOrEmpty(collectionType);
 
                 // For conflicting extensions, give priority to videos
-                if (isMixedCollectionType && LibraryManager.IsVideoFile(args.Path))
+                if (isMixedCollectionType && _libraryManager.IsVideoFile(args.Path))
                 {
                     return null;
                 }
@@ -182,7 +182,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
                 }
             }
 
-            var namingOptions = ((LibraryManager)LibraryManager).GetNamingOptions();
+            var namingOptions = ((LibraryManager)_libraryManager).GetNamingOptions();
 
             var resolver = new AudioBookListResolver(namingOptions);
             var resolverResult = resolver.Resolve(files).ToList();

+ 24 - 19
Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs

@@ -5,6 +5,7 @@
 using System;
 using System.IO;
 using System.Linq;
+using DiscUtils.Udf;
 using Emby.Naming.Video;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
@@ -16,7 +17,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
     /// <summary>
     /// Resolves a Path into a Video or Video subclass.
     /// </summary>
-    /// <typeparam name="T"></typeparam>
+    /// <typeparam name="T">The type of item to resolve.</typeparam>
     public abstract class BaseVideoResolver<T> : MediaBrowser.Controller.Resolvers.ItemResolver<T>
         where T : Video, new()
     {
@@ -80,7 +81,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
                             break;
                         }
 
-                        if (IsBluRayDirectory(child.FullName, filename, args.DirectoryService))
+                        if (IsBluRayDirectory(filename))
                         {
                             videoInfo = VideoResolver.ResolveDirectory(args.Path, namingOptions);
 
@@ -201,6 +202,22 @@ namespace Emby.Server.Implementations.Library.Resolvers
                 {
                     video.IsoType = IsoType.BluRay;
                 }
+                else
+                {
+                    // use disc-utils, both DVDs and BDs use UDF filesystem
+                    using (var videoFileStream = File.Open(video.Path, FileMode.Open, FileAccess.Read))
+                    {
+                        UdfReader udfReader = new UdfReader(videoFileStream);
+                        if (udfReader.DirectoryExists("VIDEO_TS"))
+                        {
+                            video.IsoType = IsoType.Dvd;
+                        }
+                        else if (udfReader.DirectoryExists("BDMV"))
+                        {
+                            video.IsoType = IsoType.BluRay;
+                        }
+                    }
+                }
             }
         }
 
@@ -279,25 +296,13 @@ namespace Emby.Server.Implementations.Library.Resolvers
         }
 
         /// <summary>
-        /// Determines whether [is blu ray directory] [the specified directory name].
+        /// Determines whether [is bluray directory] [the specified directory name].
         /// </summary>
-        protected bool IsBluRayDirectory(string fullPath, string directoryName, IDirectoryService directoryService)
+        /// <param name="directoryName">The directory name.</param>
+        /// <returns>Whether the directory is a bluray directory.</returns>
+        protected bool IsBluRayDirectory(string directoryName)
         {
-            if (!string.Equals(directoryName, "bdmv", StringComparison.OrdinalIgnoreCase))
-            {
-                return false;
-            }
-
-            return true;
-            // var blurayExtensions = new[]
-            //{
-            //    ".mts",
-            //    ".m2ts",
-            //    ".bdmv",
-            //    ".mpls"
-            //};
-
-            // return directoryService.GetFiles(fullPath).Any(i => blurayExtensions.Contains(i.Extension ?? string.Empty, StringComparer.OrdinalIgnoreCase));
+            return string.Equals(directoryName, "bdmv", StringComparison.OrdinalIgnoreCase);
         }
     }
 }

+ 0 - 0
Emby.Server.Implementations/Library/Resolvers/VideoResolver.cs → Emby.Server.Implementations/Library/Resolvers/GenericVideoResolver.cs


+ 1 - 1
Emby.Server.Implementations/Library/Resolvers/ItemResolver.cs

@@ -9,7 +9,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
     /// <summary>
     /// Class ItemResolver.
     /// </summary>
-    /// <typeparam name="T"></typeparam>
+    /// <typeparam name="T">The type of BaseItem.</typeparam>
     public abstract class ItemResolver<T> : IItemResolver
         where T : BaseItem, new()
     {

+ 2 - 2
Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs

@@ -400,7 +400,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
                         return movie;
                     }
 
-                    if (IsBluRayDirectory(child.FullName, filename, directoryService))
+                    if (IsBluRayDirectory(filename))
                     {
                         var movie = new T
                         {
@@ -481,7 +481,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
                     return true;
                 }
 
-                if (subfolders.Any(s => IsBluRayDirectory(s.FullName, s.Name, directoryService)))
+                if (subfolders.Any(s => IsBluRayDirectory(s.Name)))
                 {
                     videoTypes.Add(VideoType.BluRay);
                     return true;

+ 2 - 1
Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs

@@ -18,7 +18,8 @@ namespace Emby.Server.Implementations.Library.Resolvers
     /// </summary>
     public class PlaylistResolver : FolderResolver<Playlist>
     {
-        private string[] _musicPlaylistCollectionTypes = new string[] {
+        private string[] _musicPlaylistCollectionTypes =
+        {
             string.Empty,
             CollectionType.Music
         };

+ 8 - 5
Emby.Server.Implementations/Library/Validators/StudiosValidator.cs

@@ -87,12 +87,15 @@ namespace Emby.Server.Implementations.Library.Validators
 
             foreach (var item in deadEntities)
             {
-                _logger.LogInformation("Deleting dead {2} {0} {1}.", item.Id.ToString("N", CultureInfo.InvariantCulture), item.Name, item.GetType().Name);
+                _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);
+                _libraryManager.DeleteItem(
+                    item,
+                    new DeleteOptions
+                    {
+                        DeleteFileLocation = false
+                    },
+                    false);
             }
 
             progress.Report(100);

+ 3 - 5
Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs

@@ -1,5 +1,3 @@
-#nullable disable
-
 #pragma warning disable CS1591
 
 using System;
@@ -33,7 +31,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
             return targetFile;
         }
 
-        public Task Record(IDirectStreamProvider directStreamProvider, MediaSourceInfo mediaSource, string targetFile, TimeSpan duration, Action onStarted, CancellationToken cancellationToken)
+        public Task Record(IDirectStreamProvider? directStreamProvider, MediaSourceInfo mediaSource, string targetFile, TimeSpan duration, Action onStarted, CancellationToken cancellationToken)
         {
             if (directStreamProvider != null)
             {
@@ -45,7 +43,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
 
         private async Task RecordFromDirectStreamProvider(IDirectStreamProvider directStreamProvider, string targetFile, TimeSpan duration, Action onStarted, CancellationToken cancellationToken)
         {
-            Directory.CreateDirectory(Path.GetDirectoryName(targetFile));
+            Directory.CreateDirectory(Path.GetDirectoryName(targetFile) ?? throw new ArgumentException("Path can't be a root directory.", nameof(targetFile)));
 
             // use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
             using (var output = new FileStream(targetFile, FileMode.Create, FileAccess.Write, FileShare.None))
@@ -71,7 +69,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
 
             _logger.LogInformation("Opened recording stream from tuner provider");
 
-            Directory.CreateDirectory(Path.GetDirectoryName(targetFile));
+            Directory.CreateDirectory(Path.GetDirectoryName(targetFile) ?? throw new ArgumentException("Path can't be a root directory.", nameof(targetFile)));
 
             // use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
             await using var output = new FileStream(targetFile, FileMode.Create, FileAccess.Write, FileShare.None);

+ 20 - 27
Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs

@@ -159,8 +159,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
             try
             {
                 var recordingFolders = GetRecordingFolders().ToArray();
-                var virtualFolders = _libraryManager.GetVirtualFolders()
-                    .ToList();
+                var virtualFolders = _libraryManager.GetVirtualFolders();
 
                 var allExistingPaths = virtualFolders.SelectMany(i => i.Locations).ToList();
 
@@ -177,7 +176,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
                         continue;
                     }
 
-                    var mediaPathInfos = pathsToCreate.Select(i => new MediaPathInfo { Path = i }).ToArray();
+                    var mediaPathInfos = pathsToCreate.Select(i => new MediaPathInfo(i)).ToArray();
 
                     var libraryOptions = new LibraryOptions
                     {
@@ -210,7 +209,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
 
                 foreach (var path in pathsToRemove)
                 {
-                    await RemovePathFromLibrary(path).ConfigureAwait(false);
+                    await RemovePathFromLibraryAsync(path).ConfigureAwait(false);
                 }
             }
             catch (Exception ex)
@@ -219,13 +218,12 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
             }
         }
 
-        private async Task RemovePathFromLibrary(string path)
+        private async Task RemovePathFromLibraryAsync(string path)
         {
             _logger.LogDebug("Removing path from library: {0}", path);
 
             var requiresRefresh = false;
-            var virtualFolders = _libraryManager.GetVirtualFolders()
-               .ToList();
+            var virtualFolders = _libraryManager.GetVirtualFolders();
 
             foreach (var virtualFolder in virtualFolders)
             {
@@ -460,7 +458,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
             if (!string.IsNullOrWhiteSpace(tunerChannel.TunerChannelId))
             {
                 var tunerChannelId = tunerChannel.TunerChannelId;
-                if (tunerChannelId.IndexOf(".json.schedulesdirect.org", StringComparison.OrdinalIgnoreCase) != -1)
+                if (tunerChannelId.Contains(".json.schedulesdirect.org", StringComparison.OrdinalIgnoreCase))
                 {
                     tunerChannelId = tunerChannelId.Replace(".json.schedulesdirect.org", string.Empty, StringComparison.OrdinalIgnoreCase).TrimStart('I');
                 }
@@ -620,8 +618,8 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
 
             if (existingTimer != null)
             {
-                if (existingTimer.Status == RecordingStatus.Cancelled ||
-                    existingTimer.Status == RecordingStatus.Completed)
+                if (existingTimer.Status == RecordingStatus.Cancelled
+                    || existingTimer.Status == RecordingStatus.Completed)
                 {
                     existingTimer.Status = RecordingStatus.New;
                     existingTimer.IsManual = true;
@@ -913,18 +911,14 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
 
                 var epgChannel = await GetEpgChannelFromTunerChannel(provider.Item1, provider.Item2, channel, cancellationToken).ConfigureAwait(false);
 
-                List<ProgramInfo> programs;
-
                 if (epgChannel == null)
                 {
                     _logger.LogDebug("EPG channel not found for tuner channel {0}-{1} from {2}-{3}", channel.Number, channel.Name, provider.Item1.Name, provider.Item2.ListingsId ?? string.Empty);
-                    programs = new List<ProgramInfo>();
+                    continue;
                 }
-                else
-                {
-                    programs = (await provider.Item1.GetProgramsAsync(provider.Item2, epgChannel.Id, startDateUtc, endDateUtc, cancellationToken)
+
+                List<ProgramInfo> programs = (await provider.Item1.GetProgramsAsync(provider.Item2, epgChannel.Id, startDateUtc, endDateUtc, cancellationToken)
                            .ConfigureAwait(false)).ToList();
-                }
 
                 // Replace the value that came from the provider with a normalized value
                 foreach (var program in programs)
@@ -940,7 +934,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
                 }
             }
 
-            return new List<ProgramInfo>();
+            return Enumerable.Empty<ProgramInfo>();
         }
 
         private List<Tuple<IListingsProvider, ListingsProviderInfo>> GetListingProviders()
@@ -1292,7 +1286,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
 
                 _logger.LogInformation("Beginning recording. Will record for {0} minutes.", duration.TotalMinutes.ToString(CultureInfo.InvariantCulture));
 
-                _logger.LogInformation("Writing file to path: " + recordPath);
+                _logger.LogInformation("Writing file to: {Path}", recordPath);
 
                 Action onStarted = async () =>
                 {
@@ -1417,13 +1411,13 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
 
         private void TriggerRefresh(string path)
         {
-            _logger.LogInformation("Triggering refresh on {path}", path);
+            _logger.LogInformation("Triggering refresh on {Path}", path);
 
             var item = GetAffectedBaseItem(Path.GetDirectoryName(path));
 
             if (item != null)
             {
-                _logger.LogInformation("Refreshing recording parent {path}", item.Path);
+                _logger.LogInformation("Refreshing recording parent {Path}", item.Path);
 
                 _providerManager.QueueRefresh(
                     item.Id,
@@ -1458,7 +1452,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
                 if (item.GetType() == typeof(Folder) && string.Equals(item.Path, parentPath, StringComparison.OrdinalIgnoreCase))
                 {
                     var parentItem = item.GetParent();
-                    if (parentItem != null && !(parentItem is AggregateFolder))
+                    if (parentItem != null && parentItem is not AggregateFolder)
                     {
                         item = parentItem;
                     }
@@ -1512,8 +1506,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
 
                 DeleteLibraryItemsForTimers(timersToDelete);
 
-                var librarySeries = _libraryManager.FindByPath(seriesPath, true) as Folder;
-                if (librarySeries == null)
+                if (_libraryManager.FindByPath(seriesPath, true) is not Folder librarySeries)
                 {
                     return;
                 }
@@ -1667,7 +1660,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
 
                 _logger.LogInformation("Running recording post processor {0} {1}", process.StartInfo.FileName, process.StartInfo.Arguments);
 
-                process.Exited += Process_Exited;
+                process.Exited += OnProcessExited;
                 process.Start();
             }
             catch (Exception ex)
@@ -1681,7 +1674,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
             return arguments.Replace("{path}", path, StringComparison.OrdinalIgnoreCase);
         }
 
-        private void Process_Exited(object sender, EventArgs e)
+        private void OnProcessExited(object sender, EventArgs e)
         {
             using (var process = (Process)sender)
             {
@@ -2239,7 +2232,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
             var enabledTimersForSeries = new List<TimerInfo>();
             foreach (var timer in allTimers)
             {
-                var existingTimer = _timerProvider.GetTimer(timer.Id) 
+                var existingTimer = _timerProvider.GetTimer(timer.Id)
                     ?? (string.IsNullOrWhiteSpace(timer.ProgramId)
                         ? null
                         : _timerProvider.GetTimerByProgramId(timer.ProgramId));

+ 0 - 5
Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs

@@ -319,11 +319,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
                     }
                 }
             }
-            catch (ObjectDisposedException)
-            {
-                // TODO Investigate and properly fix.
-                // Don't spam the log. This doesn't seem to throw in windows, but sometimes under linux
-            }
             catch (Exception ex)
             {
                 _logger.LogError(ex, "Error reading ffmpeg recording log");

+ 0 - 1
Emby.Server.Implementations/LiveTv/EmbyTV/EpgChannelData.cs

@@ -8,7 +8,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
 {
     internal class EpgChannelData
     {
-
         private readonly Dictionary<string, ChannelInfo> _channelsById;
 
         private readonly Dictionary<string, ChannelInfo> _channelsByNumber;

+ 1 - 1
Emby.Server.Implementations/LiveTv/EmbyTV/IRecorder.cs

@@ -13,7 +13,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
         /// <summary>
         /// Records the specified media source.
         /// </summary>
-        Task Record(IDirectStreamProvider directStreamProvider, MediaSourceInfo mediaSource, string targetFile, TimeSpan duration, Action onStarted, CancellationToken cancellationToken);
+        Task Record(IDirectStreamProvider? directStreamProvider, MediaSourceInfo mediaSource, string targetFile, TimeSpan duration, Action onStarted, CancellationToken cancellationToken);
 
         string GetOutputPath(MediaSourceInfo mediaSource, string targetFile);
     }

+ 5 - 7
Emby.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs

@@ -1,5 +1,3 @@
-#nullable disable
-
 #pragma warning disable CS1591
 
 using System;
@@ -23,7 +21,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
         {
         }
 
-        public event EventHandler<GenericEventArgs<TimerInfo>> TimerFired;
+        public event EventHandler<GenericEventArgs<TimerInfo>>? TimerFired;
 
         public void RestartTimers()
         {
@@ -145,9 +143,9 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
             }
         }
 
-        private void TimerCallback(object state)
+        private void TimerCallback(object? state)
         {
-            var timerId = (string)state;
+            var timerId = (string?)state ?? throw new ArgumentNullException(nameof(state));
 
             var timer = GetAll().FirstOrDefault(i => string.Equals(i.Id, timerId, StringComparison.OrdinalIgnoreCase));
             if (timer != null)
@@ -156,12 +154,12 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
             }
         }
 
-        public TimerInfo GetTimer(string id)
+        public TimerInfo? GetTimer(string id)
         {
             return GetAll().FirstOrDefault(r => string.Equals(r.Id, id, StringComparison.OrdinalIgnoreCase));
         }
 
-        public TimerInfo GetTimerByProgramId(string programId)
+        public TimerInfo? GetTimerByProgramId(string programId)
         {
             return GetAll().FirstOrDefault(r => string.Equals(r.ProgramId, programId, StringComparison.OrdinalIgnoreCase));
         }

+ 113 - 509
Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs

@@ -14,8 +14,9 @@ using System.Text;
 using System.Text.Json;
 using System.Threading;
 using System.Threading.Tasks;
-using MediaBrowser.Common;
+using Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos;
 using Jellyfin.Extensions.Json;
+using MediaBrowser.Common;
 using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.LiveTv;
 using MediaBrowser.Model.Cryptography;
@@ -96,12 +97,12 @@ namespace Emby.Server.Implementations.LiveTv.Listings
             var dates = GetScheduleRequestDates(startDateUtc, endDateUtc);
 
             _logger.LogInformation("Channel Station ID is: {ChannelID}", channelId);
-            var requestList = new List<ScheduleDirect.RequestScheduleForChannel>()
+            var requestList = new List<RequestScheduleForChannelDto>()
                 {
-                    new ScheduleDirect.RequestScheduleForChannel()
+                    new RequestScheduleForChannelDto()
                     {
-                        stationID = channelId,
-                        date = dates
+                        StationId = channelId,
+                        Date = dates
                     }
                 };
 
@@ -113,61 +114,61 @@ namespace Emby.Server.Implementations.LiveTv.Listings
             options.Headers.TryAddWithoutValidation("token", token);
             using var response = await Send(options, true, info, cancellationToken).ConfigureAwait(false);
             await using var responseStream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
-            var dailySchedules = await JsonSerializer.DeserializeAsync<List<ScheduleDirect.Day>>(responseStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
+            var dailySchedules = await JsonSerializer.DeserializeAsync<List<DayDto>>(responseStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
             _logger.LogDebug("Found {ScheduleCount} programs on {ChannelID} ScheduleDirect", dailySchedules.Count, channelId);
 
             using var programRequestOptions = new HttpRequestMessage(HttpMethod.Post, ApiUrl + "/programs");
             programRequestOptions.Headers.TryAddWithoutValidation("token", token);
 
-            var programsID = dailySchedules.SelectMany(d => d.programs.Select(s => s.programID)).Distinct();
+            var programsID = dailySchedules.SelectMany(d => d.Programs.Select(s => s.ProgramId)).Distinct();
             programRequestOptions.Content = new StringContent("[\"" + string.Join("\", \"", programsID) + "\"]", Encoding.UTF8, MediaTypeNames.Application.Json);
 
             using var innerResponse = await Send(programRequestOptions, true, info, cancellationToken).ConfigureAwait(false);
             await using var innerResponseStream = await innerResponse.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
-            var programDetails = await JsonSerializer.DeserializeAsync<List<ScheduleDirect.ProgramDetails>>(innerResponseStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
-            var programDict = programDetails.ToDictionary(p => p.programID, y => y);
+            var programDetails = await JsonSerializer.DeserializeAsync<List<ProgramDetailsDto>>(innerResponseStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
+            var programDict = programDetails.ToDictionary(p => p.ProgramId, y => y);
 
             var programIdsWithImages = programDetails
-                .Where(p => p.hasImageArtwork).Select(p => p.programID)
+                .Where(p => p.HasImageArtwork).Select(p => p.ProgramId)
                 .ToList();
 
             var images = await GetImageForPrograms(info, programIdsWithImages, cancellationToken).ConfigureAwait(false);
 
             var programsInfo = new List<ProgramInfo>();
-            foreach (ScheduleDirect.Program schedule in dailySchedules.SelectMany(d => d.programs))
+            foreach (ProgramDto schedule in dailySchedules.SelectMany(d => d.Programs))
             {
                 // _logger.LogDebug("Proccesing Schedule for statio ID " + stationID +
                 //              " which corresponds to channel " + channelNumber + " and program id " +
-                //              schedule.programID + " which says it has images? " +
-                //              programDict[schedule.programID].hasImageArtwork);
+                //              schedule.ProgramId + " which says it has images? " +
+                //              programDict[schedule.ProgramId].hasImageArtwork);
 
                 if (images != null)
                 {
-                    var imageIndex = images.FindIndex(i => i.programID == schedule.programID.Substring(0, 10));
+                    var imageIndex = images.FindIndex(i => i.ProgramId == schedule.ProgramId[..10]);
                     if (imageIndex > -1)
                     {
-                        var programEntry = programDict[schedule.programID];
+                        var programEntry = programDict[schedule.ProgramId];
 
-                        var allImages = images[imageIndex].data ?? new List<ScheduleDirect.ImageData>();
-                        var imagesWithText = allImages.Where(i => string.Equals(i.text, "yes", StringComparison.OrdinalIgnoreCase));
-                        var imagesWithoutText = allImages.Where(i => string.Equals(i.text, "no", StringComparison.OrdinalIgnoreCase));
+                        var allImages = images[imageIndex].Data ?? new List<ImageDataDto>();
+                        var imagesWithText = allImages.Where(i => string.Equals(i.Text, "yes", StringComparison.OrdinalIgnoreCase));
+                        var imagesWithoutText = allImages.Where(i => string.Equals(i.Text, "no", StringComparison.OrdinalIgnoreCase));
 
                         const double DesiredAspect = 2.0 / 3;
 
-                        programEntry.primaryImage = GetProgramImage(ApiUrl, imagesWithText, true, DesiredAspect) ??
+                        programEntry.PrimaryImage = GetProgramImage(ApiUrl, imagesWithText, true, DesiredAspect) ??
                                                     GetProgramImage(ApiUrl, allImages, true, DesiredAspect);
 
                         const double WideAspect = 16.0 / 9;
 
-                        programEntry.thumbImage = GetProgramImage(ApiUrl, imagesWithText, true, WideAspect);
+                        programEntry.ThumbImage = GetProgramImage(ApiUrl, imagesWithText, true, WideAspect);
 
                         // Don't supply the same image twice
-                        if (string.Equals(programEntry.primaryImage, programEntry.thumbImage, StringComparison.Ordinal))
+                        if (string.Equals(programEntry.PrimaryImage, programEntry.ThumbImage, StringComparison.Ordinal))
                         {
-                            programEntry.thumbImage = null;
+                            programEntry.ThumbImage = null;
                         }
 
-                        programEntry.backdropImage = GetProgramImage(ApiUrl, imagesWithoutText, true, WideAspect);
+                        programEntry.BackdropImage = GetProgramImage(ApiUrl, imagesWithoutText, true, WideAspect);
 
                         // programEntry.bannerImage = GetProgramImage(ApiUrl, data, "Banner", false) ??
                         //    GetProgramImage(ApiUrl, data, "Banner-L1", false) ??
@@ -176,15 +177,15 @@ namespace Emby.Server.Implementations.LiveTv.Listings
                     }
                 }
 
-                programsInfo.Add(GetProgram(channelId, schedule, programDict[schedule.programID]));
+                programsInfo.Add(GetProgram(channelId, schedule, programDict[schedule.ProgramId]));
             }
 
             return programsInfo;
         }
 
-        private static int GetSizeOrder(ScheduleDirect.ImageData image)
+        private static int GetSizeOrder(ImageDataDto image)
         {
-            if (int.TryParse(image.height, out int value))
+            if (int.TryParse(image.Height, out int value))
             {
                 return value;
             }
@@ -192,53 +193,53 @@ namespace Emby.Server.Implementations.LiveTv.Listings
             return 0;
         }
 
-        private static string GetChannelNumber(ScheduleDirect.Map map)
+        private static string GetChannelNumber(MapDto map)
         {
-            var channelNumber = map.logicalChannelNumber;
+            var channelNumber = map.LogicalChannelNumber;
 
             if (string.IsNullOrWhiteSpace(channelNumber))
             {
-                channelNumber = map.channel;
+                channelNumber = map.Channel;
             }
 
             if (string.IsNullOrWhiteSpace(channelNumber))
             {
-                channelNumber = map.atscMajor + "." + map.atscMinor;
+                channelNumber = map.AtscMajor + "." + map.AtscMinor;
             }
 
             return channelNumber.TrimStart('0');
         }
 
-        private static bool IsMovie(ScheduleDirect.ProgramDetails programInfo)
+        private static bool IsMovie(ProgramDetailsDto programInfo)
         {
-            return string.Equals(programInfo.entityType, "movie", StringComparison.OrdinalIgnoreCase);
+            return string.Equals(programInfo.EntityType, "movie", StringComparison.OrdinalIgnoreCase);
         }
 
-        private ProgramInfo GetProgram(string channelId, ScheduleDirect.Program programInfo, ScheduleDirect.ProgramDetails details)
+        private ProgramInfo GetProgram(string channelId, ProgramDto programInfo, ProgramDetailsDto details)
         {
-            var startAt = GetDate(programInfo.airDateTime);
-            var endAt = startAt.AddSeconds(programInfo.duration);
+            var startAt = GetDate(programInfo.AirDateTime);
+            var endAt = startAt.AddSeconds(programInfo.Duration);
             var audioType = ProgramAudio.Stereo;
 
-            var programId = programInfo.programID ?? string.Empty;
+            var programId = programInfo.ProgramId ?? string.Empty;
 
             string newID = programId + "T" + startAt.Ticks + "C" + channelId;
 
-            if (programInfo.audioProperties != null)
+            if (programInfo.AudioProperties != null)
             {
-                if (programInfo.audioProperties.Exists(item => string.Equals(item, "atmos", StringComparison.OrdinalIgnoreCase)))
+                if (programInfo.AudioProperties.Exists(item => string.Equals(item, "atmos", StringComparison.OrdinalIgnoreCase)))
                 {
                     audioType = ProgramAudio.Atmos;
                 }
-                else if (programInfo.audioProperties.Exists(item => string.Equals(item, "dd 5.1", StringComparison.OrdinalIgnoreCase)))
+                else if (programInfo.AudioProperties.Exists(item => string.Equals(item, "dd 5.1", StringComparison.OrdinalIgnoreCase)))
                 {
                     audioType = ProgramAudio.DolbyDigital;
                 }
-                else if (programInfo.audioProperties.Exists(item => string.Equals(item, "dd", StringComparison.OrdinalIgnoreCase)))
+                else if (programInfo.AudioProperties.Exists(item => string.Equals(item, "dd", StringComparison.OrdinalIgnoreCase)))
                 {
                     audioType = ProgramAudio.DolbyDigital;
                 }
-                else if (programInfo.audioProperties.Exists(item => string.Equals(item, "stereo", StringComparison.OrdinalIgnoreCase)))
+                else if (programInfo.AudioProperties.Exists(item => string.Equals(item, "stereo", StringComparison.OrdinalIgnoreCase)))
                 {
                     audioType = ProgramAudio.Stereo;
                 }
@@ -249,9 +250,9 @@ namespace Emby.Server.Implementations.LiveTv.Listings
             }
 
             string episodeTitle = null;
-            if (details.episodeTitle150 != null)
+            if (details.EpisodeTitle150 != null)
             {
-                episodeTitle = details.episodeTitle150;
+                episodeTitle = details.EpisodeTitle150;
             }
 
             var info = new ProgramInfo
@@ -260,22 +261,22 @@ namespace Emby.Server.Implementations.LiveTv.Listings
                 Id = newID,
                 StartDate = startAt,
                 EndDate = endAt,
-                Name = details.titles[0].title120 ?? "Unknown",
+                Name = details.Titles[0].Title120 ?? "Unknown",
                 OfficialRating = null,
                 CommunityRating = null,
                 EpisodeTitle = episodeTitle,
                 Audio = audioType,
                 // IsNew = programInfo.@new ?? false,
-                IsRepeat = programInfo.@new == null,
-                IsSeries = string.Equals(details.entityType, "episode", StringComparison.OrdinalIgnoreCase),
-                ImageUrl = details.primaryImage,
-                ThumbImageUrl = details.thumbImage,
-                IsKids = string.Equals(details.audience, "children", StringComparison.OrdinalIgnoreCase),
-                IsSports = string.Equals(details.entityType, "sports", StringComparison.OrdinalIgnoreCase),
+                IsRepeat = programInfo.New == null,
+                IsSeries = string.Equals(details.EntityType, "episode", StringComparison.OrdinalIgnoreCase),
+                ImageUrl = details.PrimaryImage,
+                ThumbImageUrl = details.ThumbImage,
+                IsKids = string.Equals(details.Audience, "children", StringComparison.OrdinalIgnoreCase),
+                IsSports = string.Equals(details.EntityType, "sports", StringComparison.OrdinalIgnoreCase),
                 IsMovie = IsMovie(details),
-                Etag = programInfo.md5,
-                IsLive = string.Equals(programInfo.liveTapeDelay, "live", StringComparison.OrdinalIgnoreCase),
-                IsPremiere = programInfo.premiere || (programInfo.isPremiereOrFinale ?? string.Empty).IndexOf("premiere", StringComparison.OrdinalIgnoreCase) != -1
+                Etag = programInfo.Md5,
+                IsLive = string.Equals(programInfo.LiveTapeDelay, "live", StringComparison.OrdinalIgnoreCase),
+                IsPremiere = programInfo.Premiere || (programInfo.IsPremiereOrFinale ?? string.Empty).IndexOf("premiere", StringComparison.OrdinalIgnoreCase) != -1
             };
 
             var showId = programId;
@@ -298,15 +299,15 @@ namespace Emby.Server.Implementations.LiveTv.Listings
 
             info.ShowId = showId;
 
-            if (programInfo.videoProperties != null)
+            if (programInfo.VideoProperties != null)
             {
-                info.IsHD = programInfo.videoProperties.Contains("hdtv", StringComparer.OrdinalIgnoreCase);
-                info.Is3D = programInfo.videoProperties.Contains("3d", StringComparer.OrdinalIgnoreCase);
+                info.IsHD = programInfo.VideoProperties.Contains("hdtv", StringComparer.OrdinalIgnoreCase);
+                info.Is3D = programInfo.VideoProperties.Contains("3d", StringComparer.OrdinalIgnoreCase);
             }
 
-            if (details.contentRating != null && details.contentRating.Count > 0)
+            if (details.ContentRating != null && details.ContentRating.Count > 0)
             {
-                info.OfficialRating = details.contentRating[0].code.Replace("TV", "TV-", StringComparison.Ordinal)
+                info.OfficialRating = details.ContentRating[0].Code.Replace("TV", "TV-", StringComparison.Ordinal)
                     .Replace("--", "-", StringComparison.Ordinal);
 
                 var invalid = new[] { "N/A", "Approved", "Not Rated", "Passed" };
@@ -316,15 +317,15 @@ namespace Emby.Server.Implementations.LiveTv.Listings
                 }
             }
 
-            if (details.descriptions != null)
+            if (details.Descriptions != null)
             {
-                if (details.descriptions.description1000 != null && details.descriptions.description1000.Count > 0)
+                if (details.Descriptions.Description1000 != null && details.Descriptions.Description1000.Count > 0)
                 {
-                    info.Overview = details.descriptions.description1000[0].description;
+                    info.Overview = details.Descriptions.Description1000[0].Description;
                 }
-                else if (details.descriptions.description100 != null && details.descriptions.description100.Count > 0)
+                else if (details.Descriptions.Description100 != null && details.Descriptions.Description100.Count > 0)
                 {
-                    info.Overview = details.descriptions.description100[0].description;
+                    info.Overview = details.Descriptions.Description100[0].Description;
                 }
             }
 
@@ -334,18 +335,18 @@ namespace Emby.Server.Implementations.LiveTv.Listings
 
                 info.SeriesProviderIds[MetadataProvider.Zap2It.ToString()] = info.SeriesId;
 
-                if (details.metadata != null)
+                if (details.Metadata != null)
                 {
-                    foreach (var metadataProgram in details.metadata)
+                    foreach (var metadataProgram in details.Metadata)
                     {
                         var gracenote = metadataProgram.Gracenote;
                         if (gracenote != null)
                         {
-                            info.SeasonNumber = gracenote.season;
+                            info.SeasonNumber = gracenote.Season;
 
-                            if (gracenote.episode > 0)
+                            if (gracenote.Episode > 0)
                             {
-                                info.EpisodeNumber = gracenote.episode;
+                                info.EpisodeNumber = gracenote.Episode;
                             }
 
                             break;
@@ -354,25 +355,25 @@ namespace Emby.Server.Implementations.LiveTv.Listings
                 }
             }
 
-            if (!string.IsNullOrWhiteSpace(details.originalAirDate))
+            if (!string.IsNullOrWhiteSpace(details.OriginalAirDate))
             {
-                info.OriginalAirDate = DateTime.Parse(details.originalAirDate, CultureInfo.InvariantCulture);
+                info.OriginalAirDate = DateTime.Parse(details.OriginalAirDate, CultureInfo.InvariantCulture);
                 info.ProductionYear = info.OriginalAirDate.Value.Year;
             }
 
-            if (details.movie != null)
+            if (details.Movie != null)
             {
-                if (!string.IsNullOrEmpty(details.movie.year)
-                    && int.TryParse(details.movie.year, out int year))
+                if (!string.IsNullOrEmpty(details.Movie.Year)
+                    && int.TryParse(details.Movie.Year, out int year))
                 {
                     info.ProductionYear = year;
                 }
             }
 
-            if (details.genres != null)
+            if (details.Genres != null)
             {
-                info.Genres = details.genres.Where(g => !string.IsNullOrWhiteSpace(g)).ToList();
-                info.IsNews = details.genres.Contains("news", StringComparer.OrdinalIgnoreCase);
+                info.Genres = details.Genres.Where(g => !string.IsNullOrWhiteSpace(g)).ToList();
+                info.IsNews = details.Genres.Contains("news", StringComparer.OrdinalIgnoreCase);
 
                 if (info.Genres.Contains("children", StringComparer.OrdinalIgnoreCase))
                 {
@@ -395,11 +396,11 @@ namespace Emby.Server.Implementations.LiveTv.Listings
             return date;
         }
 
-        private string GetProgramImage(string apiUrl, IEnumerable<ScheduleDirect.ImageData> images, bool returnDefaultImage, double desiredAspect)
+        private string GetProgramImage(string apiUrl, IEnumerable<ImageDataDto> images, bool returnDefaultImage, double desiredAspect)
         {
             var match = images
                 .OrderBy(i => Math.Abs(desiredAspect - GetAspectRatio(i)))
-                .ThenByDescending(GetSizeOrder)
+                .ThenByDescending(i => GetSizeOrder(i))
                 .FirstOrDefault();
 
             if (match == null)
@@ -407,7 +408,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
                 return null;
             }
 
-            var uri = match.uri;
+            var uri = match.Uri;
 
             if (string.IsNullOrWhiteSpace(uri))
             {
@@ -423,19 +424,19 @@ namespace Emby.Server.Implementations.LiveTv.Listings
             }
         }
 
-        private static double GetAspectRatio(ScheduleDirect.ImageData i)
+        private static double GetAspectRatio(ImageDataDto i)
         {
             int width = 0;
             int height = 0;
 
-            if (!string.IsNullOrWhiteSpace(i.width))
+            if (!string.IsNullOrWhiteSpace(i.Width))
             {
-                int.TryParse(i.width, out width);
+                _ = int.TryParse(i.Width, out width);
             }
 
-            if (!string.IsNullOrWhiteSpace(i.height))
+            if (!string.IsNullOrWhiteSpace(i.Height))
             {
-                int.TryParse(i.height, out height);
+                _ = int.TryParse(i.Height, out height);
             }
 
             if (height == 0 || width == 0)
@@ -448,14 +449,14 @@ namespace Emby.Server.Implementations.LiveTv.Listings
             return result;
         }
 
-        private async Task<List<ScheduleDirect.ShowImages>> GetImageForPrograms(
+        private async Task<List<ShowImagesDto>> GetImageForPrograms(
             ListingsProviderInfo info,
             IReadOnlyList<string> programIds,
             CancellationToken cancellationToken)
         {
             if (programIds.Count == 0)
             {
-                return new List<ScheduleDirect.ShowImages>();
+                return new List<ShowImagesDto>();
             }
 
             StringBuilder str = new StringBuilder("[", 1 + (programIds.Count * 13));
@@ -479,13 +480,13 @@ namespace Emby.Server.Implementations.LiveTv.Listings
             {
                 using var innerResponse2 = await Send(message, true, info, cancellationToken).ConfigureAwait(false);
                 await using var response = await innerResponse2.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
-                return await JsonSerializer.DeserializeAsync<List<ScheduleDirect.ShowImages>>(response, _jsonOptions, cancellationToken).ConfigureAwait(false);
+                return await JsonSerializer.DeserializeAsync<List<ShowImagesDto>>(response, _jsonOptions, cancellationToken).ConfigureAwait(false);
             }
             catch (Exception ex)
             {
                 _logger.LogError(ex, "Error getting image info from schedules direct");
 
-                return new List<ScheduleDirect.ShowImages>();
+                return new List<ShowImagesDto>();
             }
         }
 
@@ -508,18 +509,18 @@ namespace Emby.Server.Implementations.LiveTv.Listings
                 using var httpResponse = await Send(options, false, info, cancellationToken).ConfigureAwait(false);
                 await using var response = await httpResponse.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
 
-                var root = await JsonSerializer.DeserializeAsync<List<ScheduleDirect.Headends>>(response, _jsonOptions, cancellationToken).ConfigureAwait(false);
+                var root = await JsonSerializer.DeserializeAsync<List<HeadendsDto>>(response, _jsonOptions, cancellationToken).ConfigureAwait(false);
 
                 if (root != null)
                 {
-                    foreach (ScheduleDirect.Headends headend in root)
+                    foreach (HeadendsDto headend in root)
                     {
-                        foreach (ScheduleDirect.Lineup lineup in headend.lineups)
+                        foreach (LineupDto lineup in headend.Lineups)
                         {
                             lineups.Add(new NameIdPair
                             {
-                                Name = string.IsNullOrWhiteSpace(lineup.name) ? lineup.lineup : lineup.name,
-                                Id = lineup.uri.Substring(18)
+                                Name = string.IsNullOrWhiteSpace(lineup.Name) ? lineup.Lineup : lineup.Name,
+                                Id = lineup.Uri[18..]
                             });
                         }
                     }
@@ -649,14 +650,14 @@ namespace Emby.Server.Implementations.LiveTv.Listings
             using var response = await Send(options, false, null, cancellationToken).ConfigureAwait(false);
             response.EnsureSuccessStatusCode();
             await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
-            var root = await JsonSerializer.DeserializeAsync<ScheduleDirect.Token>(stream, _jsonOptions, cancellationToken).ConfigureAwait(false);
-            if (string.Equals(root.message, "OK", StringComparison.Ordinal))
+            var root = await JsonSerializer.DeserializeAsync<TokenDto>(stream, _jsonOptions, cancellationToken).ConfigureAwait(false);
+            if (string.Equals(root.Message, "OK", StringComparison.Ordinal))
             {
-                _logger.LogInformation("Authenticated with Schedules Direct token: " + root.token);
-                return root.token;
+                _logger.LogInformation("Authenticated with Schedules Direct token: {Token}", root.Token);
+                return root.Token;
             }
 
-            throw new Exception("Could not authenticate with Schedules Direct Error: " + root.message);
+            throw new Exception("Could not authenticate with Schedules Direct Error: " + root.Message);
         }
 
         private async Task AddLineupToAccount(ListingsProviderInfo info, CancellationToken cancellationToken)
@@ -705,9 +706,9 @@ namespace Emby.Server.Implementations.LiveTv.Listings
                 httpResponse.EnsureSuccessStatusCode();
                 await using var stream = await httpResponse.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
                 using var response = httpResponse.Content;
-                var root = await JsonSerializer.DeserializeAsync<ScheduleDirect.Lineups>(stream, _jsonOptions, cancellationToken).ConfigureAwait(false);
+                var root = await JsonSerializer.DeserializeAsync<LineupsDto>(stream, _jsonOptions, cancellationToken).ConfigureAwait(false);
 
-                return root.lineups.Any(i => string.Equals(info.ListingsId, i.lineup, StringComparison.OrdinalIgnoreCase));
+                return root.Lineups.Any(i => string.Equals(info.ListingsId, i.Lineup, StringComparison.OrdinalIgnoreCase));
             }
             catch (HttpRequestException ex)
             {
@@ -777,35 +778,35 @@ namespace Emby.Server.Implementations.LiveTv.Listings
 
             using var httpResponse = await Send(options, true, info, cancellationToken).ConfigureAwait(false);
             await using var stream = await httpResponse.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
-            var root = await JsonSerializer.DeserializeAsync<ScheduleDirect.Channel>(stream, _jsonOptions, cancellationToken).ConfigureAwait(false);
-            _logger.LogInformation("Found {ChannelCount} channels on the lineup on ScheduleDirect", root.map.Count);
+            var root = await JsonSerializer.DeserializeAsync<ChannelDto>(stream, _jsonOptions, cancellationToken).ConfigureAwait(false);
+            _logger.LogInformation("Found {ChannelCount} channels on the lineup on ScheduleDirect", root.Map.Count);
             _logger.LogInformation("Mapping Stations to Channel");
 
-            var allStations = root.stations ?? new List<ScheduleDirect.Station>();
+            var allStations = root.Stations ?? new List<StationDto>();
 
-            var map = root.map;
+            var map = root.Map;
             var list = new List<ChannelInfo>(map.Count);
             foreach (var channel in map)
             {
                 var channelNumber = GetChannelNumber(channel);
 
-                var station = allStations.Find(item => string.Equals(item.stationID, channel.stationID, StringComparison.OrdinalIgnoreCase))
-                    ?? new ScheduleDirect.Station
+                var station = allStations.Find(item => string.Equals(item.StationId, channel.StationId, StringComparison.OrdinalIgnoreCase))
+                    ?? new StationDto
                     {
-                        stationID = channel.stationID
+                        StationId = channel.StationId
                     };
 
                 var channelInfo = new ChannelInfo
                 {
-                    Id = station.stationID,
-                    CallSign = station.callsign,
+                    Id = station.StationId,
+                    CallSign = station.Callsign,
                     Number = channelNumber,
-                    Name = string.IsNullOrWhiteSpace(station.name) ? channelNumber : station.name
+                    Name = string.IsNullOrWhiteSpace(station.Name) ? channelNumber : station.Name
                 };
 
-                if (station.logo != null)
+                if (station.Logo != null)
                 {
-                    channelInfo.ImageUrl = station.logo.URL;
+                    channelInfo.ImageUrl = station.Logo.Url;
                 }
 
                 list.Add(channelInfo);
@@ -818,402 +819,5 @@ namespace Emby.Server.Implementations.LiveTv.Listings
         {
             return value.Replace(" ", string.Empty, StringComparison.Ordinal).Replace("-", string.Empty, StringComparison.Ordinal);
         }
-
-        public class ScheduleDirect
-        {
-            public class Token
-            {
-                public int code { get; set; }
-
-                public string message { get; set; }
-
-                public string serverID { get; set; }
-
-                public string token { get; set; }
-            }
-
-            public class Lineup
-            {
-                public string lineup { get; set; }
-
-                public string name { get; set; }
-
-                public string transport { get; set; }
-
-                public string location { get; set; }
-
-                public string uri { get; set; }
-            }
-
-            public class Lineups
-            {
-                public int code { get; set; }
-
-                public string serverID { get; set; }
-
-                public string datetime { get; set; }
-
-                public List<Lineup> lineups { get; set; }
-            }
-
-            public class Headends
-            {
-                public string headend { get; set; }
-
-                public string transport { get; set; }
-
-                public string location { get; set; }
-
-                public List<Lineup> lineups { get; set; }
-            }
-
-            public class Map
-            {
-                public string stationID { get; set; }
-
-                public string channel { get; set; }
-
-                public string logicalChannelNumber { get; set; }
-
-                public int uhfVhf { get; set; }
-
-                public int atscMajor { get; set; }
-
-                public int atscMinor { get; set; }
-            }
-
-            public class Broadcaster
-            {
-                public string city { get; set; }
-
-                public string state { get; set; }
-
-                public string postalcode { get; set; }
-
-                public string country { get; set; }
-            }
-
-            public class Logo
-            {
-                public string URL { get; set; }
-
-                public int height { get; set; }
-
-                public int width { get; set; }
-
-                public string md5 { get; set; }
-            }
-
-            public class Station
-            {
-                public string stationID { get; set; }
-
-                public string name { get; set; }
-
-                public string callsign { get; set; }
-
-                public List<string> broadcastLanguage { get; set; }
-
-                public List<string> descriptionLanguage { get; set; }
-
-                public Broadcaster broadcaster { get; set; }
-
-                public string affiliate { get; set; }
-
-                public Logo logo { get; set; }
-
-                public bool? isCommercialFree { get; set; }
-            }
-
-            public class Metadata
-            {
-                public string lineup { get; set; }
-
-                public string modified { get; set; }
-
-                public string transport { get; set; }
-            }
-
-            public class Channel
-            {
-                public List<Map> map { get; set; }
-
-                public List<Station> stations { get; set; }
-
-                public Metadata metadata { get; set; }
-            }
-
-            public class RequestScheduleForChannel
-            {
-                public string stationID { get; set; }
-
-                public List<string> date { get; set; }
-            }
-
-            public class Rating
-            {
-                public string body { get; set; }
-
-                public string code { get; set; }
-            }
-
-            public class Multipart
-            {
-                public int partNumber { get; set; }
-
-                public int totalParts { get; set; }
-            }
-
-            public class Program
-            {
-                public string programID { get; set; }
-
-                public string airDateTime { get; set; }
-
-                public int duration { get; set; }
-
-                public string md5 { get; set; }
-
-                public List<string> audioProperties { get; set; }
-
-                public List<string> videoProperties { get; set; }
-
-                public List<Rating> ratings { get; set; }
-
-                public bool? @new { get; set; }
-
-                public Multipart multipart { get; set; }
-
-                public string liveTapeDelay { get; set; }
-
-                public bool premiere { get; set; }
-
-                public bool repeat { get; set; }
-
-                public string isPremiereOrFinale { get; set; }
-            }
-
-            public class MetadataSchedule
-            {
-                public string modified { get; set; }
-
-                public string md5 { get; set; }
-
-                public string startDate { get; set; }
-
-                public string endDate { get; set; }
-
-                public int days { get; set; }
-            }
-
-            public class Day
-            {
-                public string stationID { get; set; }
-
-                public List<Program> programs { get; set; }
-
-                public MetadataSchedule metadata { get; set; }
-
-                public Day()
-                {
-                    programs = new List<Program>();
-                }
-            }
-
-            public class Title
-            {
-                public string title120 { get; set; }
-            }
-
-            public class EventDetails
-            {
-                public string subType { get; set; }
-            }
-
-            public class Description100
-            {
-                public string descriptionLanguage { get; set; }
-
-                public string description { get; set; }
-            }
-
-            public class Description1000
-            {
-                public string descriptionLanguage { get; set; }
-
-                public string description { get; set; }
-            }
-
-            public class DescriptionsProgram
-            {
-                public List<Description100> description100 { get; set; }
-
-                public List<Description1000> description1000 { get; set; }
-            }
-
-            public class Gracenote
-            {
-                public int season { get; set; }
-
-                public int episode { get; set; }
-            }
-
-            public class MetadataPrograms
-            {
-                public Gracenote Gracenote { get; set; }
-            }
-
-            public class ContentRating
-            {
-                public string body { get; set; }
-
-                public string code { get; set; }
-            }
-
-            public class Cast
-            {
-                public string billingOrder { get; set; }
-
-                public string role { get; set; }
-
-                public string nameId { get; set; }
-
-                public string personId { get; set; }
-
-                public string name { get; set; }
-
-                public string characterName { get; set; }
-            }
-
-            public class Crew
-            {
-                public string billingOrder { get; set; }
-
-                public string role { get; set; }
-
-                public string nameId { get; set; }
-
-                public string personId { get; set; }
-
-                public string name { get; set; }
-            }
-
-            public class QualityRating
-            {
-                public string ratingsBody { get; set; }
-
-                public string rating { get; set; }
-
-                public string minRating { get; set; }
-
-                public string maxRating { get; set; }
-
-                public string increment { get; set; }
-            }
-
-            public class Movie
-            {
-                public string year { get; set; }
-
-                public int duration { get; set; }
-
-                public List<QualityRating> qualityRating { get; set; }
-            }
-
-            public class Recommendation
-            {
-                public string programID { get; set; }
-
-                public string title120 { get; set; }
-            }
-
-            public class ProgramDetails
-            {
-                public string audience { get; set; }
-
-                public string programID { get; set; }
-
-                public List<Title> titles { get; set; }
-
-                public EventDetails eventDetails { get; set; }
-
-                public DescriptionsProgram descriptions { get; set; }
-
-                public string originalAirDate { get; set; }
-
-                public List<string> genres { get; set; }
-
-                public string episodeTitle150 { get; set; }
-
-                public List<MetadataPrograms> metadata { get; set; }
-
-                public List<ContentRating> contentRating { get; set; }
-
-                public List<Cast> cast { get; set; }
-
-                public List<Crew> crew { get; set; }
-
-                public string entityType { get; set; }
-
-                public string showType { get; set; }
-
-                public bool hasImageArtwork { get; set; }
-
-                public string primaryImage { get; set; }
-
-                public string thumbImage { get; set; }
-
-                public string backdropImage { get; set; }
-
-                public string bannerImage { get; set; }
-
-                public string imageID { get; set; }
-
-                public string md5 { get; set; }
-
-                public List<string> contentAdvisory { get; set; }
-
-                public Movie movie { get; set; }
-
-                public List<Recommendation> recommendations { get; set; }
-            }
-
-            public class Caption
-            {
-                public string content { get; set; }
-
-                public string lang { get; set; }
-            }
-
-            public class ImageData
-            {
-                public string width { get; set; }
-
-                public string height { get; set; }
-
-                public string uri { get; set; }
-
-                public string size { get; set; }
-
-                public string aspect { get; set; }
-
-                public string category { get; set; }
-
-                public string text { get; set; }
-
-                public string primary { get; set; }
-
-                public string tier { get; set; }
-
-                public Caption caption { get; set; }
-            }
-
-            public class ShowImages
-            {
-                public string programID { get; set; }
-
-                public List<ImageData> data { get; set; }
-            }
-        }
     }
 }

+ 36 - 0
Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/BroadcasterDto.cs

@@ -0,0 +1,36 @@
+#nullable disable
+
+using System.Text.Json.Serialization;
+
+namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
+{
+    /// <summary>
+    /// Broadcaster dto.
+    /// </summary>
+    public class BroadcasterDto
+    {
+        /// <summary>
+        /// Gets or sets the city.
+        /// </summary>
+        [JsonPropertyName("city")]
+        public string City { get; set; }
+
+        /// <summary>
+        /// Gets or sets the state.
+        /// </summary>
+        [JsonPropertyName("state")]
+        public string State { get; set; }
+
+        /// <summary>
+        /// Gets or sets the postal code.
+        /// </summary>
+        [JsonPropertyName("postalCode")]
+        public string Postalcode { get; set; }
+
+        /// <summary>
+        /// Gets or sets the country.
+        /// </summary>
+        [JsonPropertyName("country")]
+        public string Country { get; set; }
+    }
+}

+ 24 - 0
Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/CaptionDto.cs

@@ -0,0 +1,24 @@
+#nullable disable
+
+using System.Text.Json.Serialization;
+
+namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
+{
+    /// <summary>
+    /// Caption dto.
+    /// </summary>
+    public class CaptionDto
+    {
+        /// <summary>
+        /// Gets or sets the content.
+        /// </summary>
+        [JsonPropertyName("content")]
+        public string Content { get; set; }
+
+        /// <summary>
+        /// Gets or sets the lang.
+        /// </summary>
+        [JsonPropertyName("lang")]
+        public string Lang { get; set; }
+    }
+}

+ 48 - 0
Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/CastDto.cs

@@ -0,0 +1,48 @@
+#nullable disable
+
+using System.Text.Json.Serialization;
+
+namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
+{
+    /// <summary>
+    /// Cast dto.
+    /// </summary>
+    public class CastDto
+    {
+        /// <summary>
+        /// Gets or sets the billing order.
+        /// </summary>
+        [JsonPropertyName("billingOrder")]
+        public string BillingOrder { get; set; }
+
+        /// <summary>
+        /// Gets or sets the role.
+        /// </summary>
+        [JsonPropertyName("role")]
+        public string Role { get; set; }
+
+        /// <summary>
+        /// Gets or sets the name id.
+        /// </summary>
+        [JsonPropertyName("nameId")]
+        public string NameId { get; set; }
+
+        /// <summary>
+        /// Gets or sets the person id.
+        /// </summary>
+        [JsonPropertyName("personId")]
+        public string PersonId { get; set; }
+
+        /// <summary>
+        /// Gets or sets the name.
+        /// </summary>
+        [JsonPropertyName("name")]
+        public string Name { get; set; }
+
+        /// <summary>
+        /// Gets or sets the character name.
+        /// </summary>
+        [JsonPropertyName("characterName")]
+        public string CharacterName { get; set; }
+    }
+}

+ 31 - 0
Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ChannelDto.cs

@@ -0,0 +1,31 @@
+#nullable disable
+
+using System.Collections.Generic;
+using System.Text.Json.Serialization;
+
+namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
+{
+    /// <summary>
+    /// Channel dto.
+    /// </summary>
+    public class ChannelDto
+    {
+        /// <summary>
+        /// Gets or sets the list of maps.
+        /// </summary>
+        [JsonPropertyName("map")]
+        public List<MapDto> Map { get; set; }
+
+        /// <summary>
+        /// Gets or sets the list of stations.
+        /// </summary>
+        [JsonPropertyName("stations")]
+        public List<StationDto> Stations { get; set; }
+
+        /// <summary>
+        /// Gets or sets the metadata.
+        /// </summary>
+        [JsonPropertyName("metadata")]
+        public MetadataDto Metadata { get; set; }
+    }
+}

+ 24 - 0
Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ContentRatingDto.cs

@@ -0,0 +1,24 @@
+#nullable disable
+
+using System.Text.Json.Serialization;
+
+namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
+{
+    /// <summary>
+    /// Content rating dto.
+    /// </summary>
+    public class ContentRatingDto
+    {
+        /// <summary>
+        /// Gets or sets the body.
+        /// </summary>
+        [JsonPropertyName("body")]
+        public string Body { get; set; }
+
+        /// <summary>
+        /// Gets or sets the code.
+        /// </summary>
+        [JsonPropertyName("code")]
+        public string Code { get; set; }
+    }
+}

+ 42 - 0
Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/CrewDto.cs

@@ -0,0 +1,42 @@
+#nullable disable
+
+using System.Text.Json.Serialization;
+
+namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
+{
+    /// <summary>
+    /// Crew dto.
+    /// </summary>
+    public class CrewDto
+    {
+        /// <summary>
+        /// Gets or sets the billing order.
+        /// </summary>
+        [JsonPropertyName("billingOrder")]
+        public string BillingOrder { get; set; }
+
+        /// <summary>
+        /// Gets or sets the role.
+        /// </summary>
+        [JsonPropertyName("role")]
+        public string Role { get; set; }
+
+        /// <summary>
+        /// Gets or sets the name id.
+        /// </summary>
+        [JsonPropertyName("nameId")]
+        public string NameId { get; set; }
+
+        /// <summary>
+        /// Gets or sets the person id.
+        /// </summary>
+        [JsonPropertyName("personId")]
+        public string PersonId { get; set; }
+
+        /// <summary>
+        /// Gets or sets the name.
+        /// </summary>
+        [JsonPropertyName("name")]
+        public string Name { get; set; }
+    }
+}

+ 39 - 0
Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/DayDto.cs

@@ -0,0 +1,39 @@
+#nullable disable
+
+using System.Collections.Generic;
+using System.Text.Json.Serialization;
+
+namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
+{
+    /// <summary>
+    /// Day dto.
+    /// </summary>
+    public class DayDto
+    {
+        /// <summary>
+        /// Initializes a new instance of the <see cref="DayDto"/> class.
+        /// </summary>
+        public DayDto()
+        {
+            Programs = new List<ProgramDto>();
+        }
+
+        /// <summary>
+        /// Gets or sets the station id.
+        /// </summary>
+        [JsonPropertyName("stationID")]
+        public string StationId { get; set; }
+
+        /// <summary>
+        /// Gets or sets the list of programs.
+        /// </summary>
+        [JsonPropertyName("programs")]
+        public List<ProgramDto> Programs { get; set; }
+
+        /// <summary>
+        /// Gets or sets the metadata schedule.
+        /// </summary>
+        [JsonPropertyName("metadata")]
+        public MetadataScheduleDto Metadata { get; set; }
+    }
+}

+ 24 - 0
Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/Description1000Dto.cs

@@ -0,0 +1,24 @@
+#nullable disable
+
+using System.Text.Json.Serialization;
+
+namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
+{
+    /// <summary>
+    /// Description 1_000 dto.
+    /// </summary>
+    public class Description1000Dto
+    {
+        /// <summary>
+        /// Gets or sets the description language.
+        /// </summary>
+        [JsonPropertyName("descriptionLanguage")]
+        public string DescriptionLanguage { get; set; }
+
+        /// <summary>
+        /// Gets or sets the description.
+        /// </summary>
+        [JsonPropertyName("description")]
+        public string Description { get; set; }
+    }
+}

+ 24 - 0
Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/Description100Dto.cs

@@ -0,0 +1,24 @@
+#nullable disable
+
+using System.Text.Json.Serialization;
+
+namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
+{
+    /// <summary>
+    /// Description 100 dto.
+    /// </summary>
+    public class Description100Dto
+    {
+        /// <summary>
+        /// Gets or sets the description language.
+        /// </summary>
+        [JsonPropertyName("descriptionLanguage")]
+        public string DescriptionLanguage { get; set; }
+
+        /// <summary>
+        /// Gets or sets the description.
+        /// </summary>
+        [JsonPropertyName("description")]
+        public string Description { get; set; }
+    }
+}

+ 25 - 0
Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/DescriptionsProgramDto.cs

@@ -0,0 +1,25 @@
+#nullable disable
+
+using System.Collections.Generic;
+using System.Text.Json.Serialization;
+
+namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
+{
+    /// <summary>
+    /// Descriptions program dto.
+    /// </summary>
+    public class DescriptionsProgramDto
+    {
+        /// <summary>
+        /// Gets or sets the list of description 100.
+        /// </summary>
+        [JsonPropertyName("description100")]
+        public List<Description100Dto> Description100 { get; set; }
+
+        /// <summary>
+        /// Gets or sets the list of description1000.
+        /// </summary>
+        [JsonPropertyName("description1000")]
+        public List<Description1000Dto> Description1000 { get; set; }
+    }
+}

+ 18 - 0
Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/EventDetailsDto.cs

@@ -0,0 +1,18 @@
+#nullable disable
+
+using System.Text.Json.Serialization;
+
+namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
+{
+    /// <summary>
+    /// Event details dto.
+    /// </summary>
+    public class EventDetailsDto
+    {
+        /// <summary>
+        /// Gets or sets the sub type.
+        /// </summary>
+        [JsonPropertyName("subType")]
+        public string SubType { get; set; }
+    }
+}

+ 24 - 0
Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/GracenoteDto.cs

@@ -0,0 +1,24 @@
+#nullable disable
+
+using System.Text.Json.Serialization;
+
+namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
+{
+    /// <summary>
+    /// Gracenote dto.
+    /// </summary>
+    public class GracenoteDto
+    {
+        /// <summary>
+        /// Gets or sets the season.
+        /// </summary>
+        [JsonPropertyName("season")]
+        public int Season { get; set; }
+
+        /// <summary>
+        /// Gets or sets the episode.
+        /// </summary>
+        [JsonPropertyName("episode")]
+        public int Episode { get; set; }
+    }
+}

+ 37 - 0
Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/HeadendsDto.cs

@@ -0,0 +1,37 @@
+#nullable disable
+
+using System.Collections.Generic;
+using System.Text.Json.Serialization;
+
+namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
+{
+    /// <summary>
+    /// Headends dto.
+    /// </summary>
+    public class HeadendsDto
+    {
+        /// <summary>
+        /// Gets or sets the headend.
+        /// </summary>
+        [JsonPropertyName("headend")]
+        public string Headend { get; set; }
+
+        /// <summary>
+        /// Gets or sets the transport.
+        /// </summary>
+        [JsonPropertyName("transport")]
+        public string Transport { get; set; }
+
+        /// <summary>
+        /// Gets or sets the location.
+        /// </summary>
+        [JsonPropertyName("location")]
+        public string Location { get; set; }
+
+        /// <summary>
+        /// Gets or sets the list of lineups.
+        /// </summary>
+        [JsonPropertyName("lineups")]
+        public List<LineupDto> Lineups { get; set; }
+    }
+}

+ 72 - 0
Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ImageDataDto.cs

@@ -0,0 +1,72 @@
+#nullable disable
+
+using System.Text.Json.Serialization;
+
+namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
+{
+    /// <summary>
+    /// Image data dto.
+    /// </summary>
+    public class ImageDataDto
+    {
+        /// <summary>
+        /// Gets or sets the width.
+        /// </summary>
+        [JsonPropertyName("width")]
+        public string Width { get; set; }
+
+        /// <summary>
+        /// Gets or sets the height.
+        /// </summary>
+        [JsonPropertyName("height")]
+        public string Height { get; set; }
+
+        /// <summary>
+        /// Gets or sets the uri.
+        /// </summary>
+        [JsonPropertyName("uri")]
+        public string Uri { get; set; }
+
+        /// <summary>
+        /// Gets or sets the size.
+        /// </summary>
+        [JsonPropertyName("size")]
+        public string Size { get; set; }
+
+        /// <summary>
+        /// Gets or sets the aspect.
+        /// </summary>
+        [JsonPropertyName("aspect")]
+        public string aspect { get; set; }
+
+        /// <summary>
+        /// Gets or sets the category.
+        /// </summary>
+        [JsonPropertyName("category")]
+        public string Category { get; set; }
+
+        /// <summary>
+        /// Gets or sets the text.
+        /// </summary>
+        [JsonPropertyName("text")]
+        public string Text { get; set; }
+
+        /// <summary>
+        /// Gets or sets the primary.
+        /// </summary>
+        [JsonPropertyName("primary")]
+        public string Primary { get; set; }
+
+        /// <summary>
+        /// Gets or sets the tier.
+        /// </summary>
+        [JsonPropertyName("tier")]
+        public string Tier { get; set; }
+
+        /// <summary>
+        /// Gets or sets the caption.
+        /// </summary>
+        [JsonPropertyName("caption")]
+        public CaptionDto Caption { get; set; }
+    }
+}

+ 42 - 0
Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/LineupDto.cs

@@ -0,0 +1,42 @@
+#nullable disable
+
+using System.Text.Json.Serialization;
+
+namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
+{
+    /// <summary>
+    /// The lineup dto.
+    /// </summary>
+    public class LineupDto
+    {
+        /// <summary>
+        /// Gets or sets the linup.
+        /// </summary>
+        [JsonPropertyName("lineup")]
+        public string Lineup { get; set; }
+
+        /// <summary>
+        /// Gets or sets the lineup name.
+        /// </summary>
+        [JsonPropertyName("name")]
+        public string Name { get; set; }
+
+        /// <summary>
+        /// Gets or sets the transport.
+        /// </summary>
+        [JsonPropertyName("transport")]
+        public string Transport { get; set; }
+
+        /// <summary>
+        /// Gets or sets the location.
+        /// </summary>
+        [JsonPropertyName("location")]
+        public string Location { get; set; }
+
+        /// <summary>
+        /// Gets or sets the uri.
+        /// </summary>
+        [JsonPropertyName("uri")]
+        public string Uri { get; set; }
+    }
+}

+ 37 - 0
Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/LineupsDto.cs

@@ -0,0 +1,37 @@
+#nullable disable
+
+using System.Collections.Generic;
+using System.Text.Json.Serialization;
+
+namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
+{
+    /// <summary>
+    /// Lineups dto.
+    /// </summary>
+    public class LineupsDto
+    {
+        /// <summary>
+        /// Gets or sets the response code.
+        /// </summary>
+        [JsonPropertyName("code")]
+        public int Code { get; set; }
+
+        /// <summary>
+        /// Gets or sets the server id.
+        /// </summary>
+        [JsonPropertyName("serverID")]
+        public string ServerId { get; set; }
+
+        /// <summary>
+        /// Gets or sets the datetime.
+        /// </summary>
+        [JsonPropertyName("datetime")]
+        public string Datetime { get; set; }
+
+        /// <summary>
+        /// Gets or sets the list of lineups.
+        /// </summary>
+        [JsonPropertyName("lineups")]
+        public List<LineupDto> Lineups { get; set; }
+    }
+}

+ 36 - 0
Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/LogoDto.cs

@@ -0,0 +1,36 @@
+#nullable disable
+
+using System.Text.Json.Serialization;
+
+namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
+{
+    /// <summary>
+    /// Logo dto.
+    /// </summary>
+    public class LogoDto
+    {
+        /// <summary>
+        /// Gets or sets the url.
+        /// </summary>
+        [JsonPropertyName("URL")]
+        public string Url { get; set; }
+
+        /// <summary>
+        /// Gets or sets the height.
+        /// </summary>
+        [JsonPropertyName("height")]
+        public int Height { get; set; }
+
+        /// <summary>
+        /// Gets or sets the width.
+        /// </summary>
+        [JsonPropertyName("width")]
+        public int Width { get; set; }
+
+        /// <summary>
+        /// Gets or sets the md5.
+        /// </summary>
+        [JsonPropertyName("md5")]
+        public string Md5 { get; set; }
+    }
+}

+ 48 - 0
Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MapDto.cs

@@ -0,0 +1,48 @@
+#nullable disable
+
+using System.Text.Json.Serialization;
+
+namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
+{
+    /// <summary>
+    /// Map dto.
+    /// </summary>
+    public class MapDto
+    {
+        /// <summary>
+        /// Gets or sets the station id.
+        /// </summary>
+        [JsonPropertyName("stationID")]
+        public string StationId { get; set; }
+
+        /// <summary>
+        /// Gets or sets the channel.
+        /// </summary>
+        [JsonPropertyName("channel")]
+        public string Channel { get; set; }
+
+        /// <summary>
+        /// Gets or sets the logical channel number.
+        /// </summary>
+        [JsonPropertyName("logicalChannelNumber")]
+        public string LogicalChannelNumber { get; set; }
+
+        /// <summary>
+        /// Gets or sets the uhfvhf.
+        /// </summary>
+        [JsonPropertyName("uhfVhf")]
+        public int UhfVhf { get; set; }
+
+        /// <summary>
+        /// Gets or sets the atsc major.
+        /// </summary>
+        [JsonPropertyName("atscMajor")]
+        public int AtscMajor { get; set; }
+
+        /// <summary>
+        /// Gets or sets the atsc minor.
+        /// </summary>
+        [JsonPropertyName("atscMinor")]
+        public int AtscMinor { get; set; }
+    }
+}

+ 30 - 0
Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MetadataDto.cs

@@ -0,0 +1,30 @@
+#nullable disable
+
+using System.Text.Json.Serialization;
+
+namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
+{
+    /// <summary>
+    /// Metadata dto.
+    /// </summary>
+    public class MetadataDto
+    {
+        /// <summary>
+        /// Gets or sets the linup.
+        /// </summary>
+        [JsonPropertyName("lineup")]
+        public string Lineup { get; set; }
+
+        /// <summary>
+        /// Gets or sets the modified timestamp.
+        /// </summary>
+        [JsonPropertyName("modified")]
+        public string Modified { get; set; }
+
+        /// <summary>
+        /// Gets or sets the transport.
+        /// </summary>
+        [JsonPropertyName("transport")]
+        public string Transport { get; set; }
+    }
+}

+ 18 - 0
Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MetadataProgramsDto.cs

@@ -0,0 +1,18 @@
+#nullable disable
+
+using System.Text.Json.Serialization;
+
+namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
+{
+    /// <summary>
+    /// Metadata programs dto.
+    /// </summary>
+    public class MetadataProgramsDto
+    {
+        /// <summary>
+        /// Gets or sets the gracenote object.
+        /// </summary>
+        [JsonPropertyName("gracenote")]
+        public GracenoteDto Gracenote { get; set; }
+    }
+}

+ 42 - 0
Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MetadataScheduleDto.cs

@@ -0,0 +1,42 @@
+#nullable disable
+
+using System.Text.Json.Serialization;
+
+namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
+{
+    /// <summary>
+    /// Metadata schedule dto.
+    /// </summary>
+    public class MetadataScheduleDto
+    {
+        /// <summary>
+        /// Gets or sets the modified timestamp.
+        /// </summary>
+        [JsonPropertyName("modified")]
+        public string Modified { get; set; }
+
+        /// <summary>
+        /// Gets or sets the md5.
+        /// </summary>
+        [JsonPropertyName("md5")]
+        public string Md5 { get; set; }
+
+        /// <summary>
+        /// Gets or sets the start date.
+        /// </summary>
+        [JsonPropertyName("startDate")]
+        public string StartDate { get; set; }
+
+        /// <summary>
+        /// Gets or sets the end date.
+        /// </summary>
+        [JsonPropertyName("endDate")]
+        public string EndDate { get; set; }
+
+        /// <summary>
+        /// Gets or sets the days count.
+        /// </summary>
+        [JsonPropertyName("days")]
+        public int Days { get; set; }
+    }
+}

+ 31 - 0
Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MovieDto.cs

@@ -0,0 +1,31 @@
+#nullable disable
+
+using System.Collections.Generic;
+using System.Text.Json.Serialization;
+
+namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
+{
+    /// <summary>
+    /// Movie dto.
+    /// </summary>
+    public class MovieDto
+    {
+        /// <summary>
+        /// Gets or sets the year.
+        /// </summary>
+        [JsonPropertyName("year")]
+        public string Year { get; set; }
+
+        /// <summary>
+        /// Gets or sets the duration.
+        /// </summary>
+        [JsonPropertyName("duration")]
+        public int Duration { get; set; }
+
+        /// <summary>
+        /// Gets or sets the list of quality rating.
+        /// </summary>
+        [JsonPropertyName("qualityRating")]
+        public List<QualityRatingDto> QualityRating { get; set; }
+    }
+}

+ 24 - 0
Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MultipartDto.cs

@@ -0,0 +1,24 @@
+#nullable disable
+
+using System.Text.Json.Serialization;
+
+namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
+{
+    /// <summary>
+    /// Multipart dto.
+    /// </summary>
+    public class MultipartDto
+    {
+        /// <summary>
+        /// Gets or sets the part number.
+        /// </summary>
+        [JsonPropertyName("partNumber")]
+        public int PartNumber { get; set; }
+
+        /// <summary>
+        /// Gets or sets the total parts.
+        /// </summary>
+        [JsonPropertyName("totalParts")]
+        public int TotalParts { get; set; }
+    }
+}

+ 157 - 0
Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ProgramDetailsDto.cs

@@ -0,0 +1,157 @@
+#nullable disable
+
+using System.Collections.Generic;
+using System.Text.Json.Serialization;
+
+namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
+{
+    /// <summary>
+    /// Program details dto.
+    /// </summary>
+    public class ProgramDetailsDto
+    {
+        /// <summary>
+        /// Gets or sets the audience.
+        /// </summary>
+        [JsonPropertyName("audience")]
+        public string Audience { get; set; }
+
+        /// <summary>
+        /// Gets or sets the program id.
+        /// </summary>
+        [JsonPropertyName("programID")]
+        public string ProgramId { get; set; }
+
+        /// <summary>
+        /// Gets or sets the list of titles.
+        /// </summary>
+        [JsonPropertyName("titles")]
+        public List<TitleDto> Titles { get; set; }
+
+        /// <summary>
+        /// Gets or sets the event details object.
+        /// </summary>
+        [JsonPropertyName("eventDetails")]
+        public EventDetailsDto EventDetails { get; set; }
+
+        /// <summary>
+        /// Gets or sets the descriptions.
+        /// </summary>
+        [JsonPropertyName("descriptions")]
+        public DescriptionsProgramDto Descriptions { get; set; }
+
+        /// <summary>
+        /// Gets or sets the original air date.
+        /// </summary>
+        [JsonPropertyName("originalAirDate")]
+        public string OriginalAirDate { get; set; }
+
+        /// <summary>
+        /// Gets or sets the list of genres.
+        /// </summary>
+        [JsonPropertyName("genres")]
+        public List<string> Genres { get; set; }
+
+        /// <summary>
+        /// Gets or sets the episode title.
+        /// </summary>
+        [JsonPropertyName("episodeTitle150")]
+        public string EpisodeTitle150 { get; set; }
+
+        /// <summary>
+        /// Gets or sets the list of metadata.
+        /// </summary>
+        [JsonPropertyName("metadata")]
+        public List<MetadataProgramsDto> Metadata { get; set; }
+
+        /// <summary>
+        /// Gets or sets the list of content raitings.
+        /// </summary>
+        [JsonPropertyName("contentRating")]
+        public List<ContentRatingDto> ContentRating { get; set; }
+
+        /// <summary>
+        /// Gets or sets the list of cast.
+        /// </summary>
+        [JsonPropertyName("cast")]
+        public List<CastDto> Cast { get; set; }
+
+        /// <summary>
+        /// Gets or sets the list of crew.
+        /// </summary>
+        [JsonPropertyName("crew")]
+        public List<CrewDto> Crew { get; set; }
+
+        /// <summary>
+        /// Gets or sets the entity type.
+        /// </summary>
+        [JsonPropertyName("entityType")]
+        public string EntityType { get; set; }
+
+        /// <summary>
+        /// Gets or sets the show type.
+        /// </summary>
+        [JsonPropertyName("showType")]
+        public string ShowType { get; set; }
+
+        /// <summary>
+        /// Gets or sets a value indicating whether there is image artwork.
+        /// </summary>
+        [JsonPropertyName("hasImageArtwork")]
+        public bool HasImageArtwork { get; set; }
+
+        /// <summary>
+        /// Gets or sets the primary image.
+        /// </summary>
+        [JsonPropertyName("primaryImage")]
+        public string PrimaryImage { get; set; }
+
+        /// <summary>
+        /// Gets or sets the thumb image.
+        /// </summary>
+        [JsonPropertyName("thumbImage")]
+        public string ThumbImage { get; set; }
+
+        /// <summary>
+        /// Gets or sets the backdrop image.
+        /// </summary>
+        [JsonPropertyName("backdropImage")]
+        public string BackdropImage { get; set; }
+
+        /// <summary>
+        /// Gets or sets the banner image.
+        /// </summary>
+        [JsonPropertyName("bannerImage")]
+        public string BannerImage { get; set; }
+
+        /// <summary>
+        /// Gets or sets the image id.
+        /// </summary>
+        [JsonPropertyName("imageID")]
+        public string ImageId { get; set; }
+
+        /// <summary>
+        /// Gets or sets the md5.
+        /// </summary>
+        [JsonPropertyName("md5")]
+        public string Md5 { get; set; }
+
+        /// <summary>
+        /// Gets or sets the list of content advisory.
+        /// </summary>
+        [JsonPropertyName("contentAdvisory")]
+        public List<string> ContentAdvisory { get; set; }
+
+        /// <summary>
+        /// Gets or sets the movie object.
+        /// </summary>
+        [JsonPropertyName("movie")]
+        public MovieDto Movie { get; set; }
+
+        /// <summary>
+        /// Gets or sets the list of recommendations.
+        /// </summary>
+        [JsonPropertyName("recommendations")]
+        public List<RecommendationDto> Recommendations { get; set; }
+    }
+}

+ 91 - 0
Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ProgramDto.cs

@@ -0,0 +1,91 @@
+#nullable disable
+
+using System.Collections.Generic;
+using System.Text.Json.Serialization;
+
+namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
+{
+    /// <summary>
+    /// Program dto.
+    /// </summary>
+    public class ProgramDto
+    {
+        /// <summary>
+        /// Gets or sets the program id.
+        /// </summary>
+        [JsonPropertyName("programID")]
+        public string ProgramId { get; set; }
+
+        /// <summary>
+        /// Gets or sets the air date time.
+        /// </summary>
+        [JsonPropertyName("airDateTime")]
+        public string AirDateTime { get; set; }
+
+        /// <summary>
+        /// Gets or sets the duration.
+        /// </summary>
+        [JsonPropertyName("duration")]
+        public int Duration { get; set; }
+
+        /// <summary>
+        /// Gets or sets the md5.
+        /// </summary>
+        [JsonPropertyName("md5")]
+        public string Md5 { get; set; }
+
+        /// <summary>
+        /// Gets or sets the list of audio properties.
+        /// </summary>
+        [JsonPropertyName("audioProperties")]
+        public List<string> AudioProperties { get; set; }
+
+        /// <summary>
+        /// Gets or sets the list of video properties.
+        /// </summary>
+        [JsonPropertyName("videoProperties")]
+        public List<string> VideoProperties { get; set; }
+
+        /// <summary>
+        /// Gets or sets the list of ratings.
+        /// </summary>
+        [JsonPropertyName("ratings")]
+        public List<RatingDto> Ratings { get; set; }
+
+        /// <summary>
+        /// Gets or sets a value indicating whether this program is new.
+        /// </summary>
+        [JsonPropertyName("new")]
+        public bool? New { get; set; }
+
+        /// <summary>
+        /// Gets or sets the multipart object.
+        /// </summary>
+        [JsonPropertyName("multipart")]
+        public MultipartDto Multipart { get; set; }
+
+        /// <summary>
+        /// Gets or sets the live tape delay.
+        /// </summary>
+        [JsonPropertyName("liveTapeDelay")]
+        public string LiveTapeDelay { get; set; }
+
+        /// <summary>
+        /// Gets or sets a value indicating whether this is the premiere.
+        /// </summary>
+        [JsonPropertyName("premiere")]
+        public bool Premiere { get; set; }
+
+        /// <summary>
+        /// Gets or sets a value indicating whether this is a repeat.
+        /// </summary>
+        [JsonPropertyName("repeat")]
+        public bool Repeat { get; set; }
+
+        /// <summary>
+        /// Gets or sets the premiere or finale.
+        /// </summary>
+        [JsonPropertyName("isPremiereOrFinale")]
+        public string IsPremiereOrFinale { get; set; }
+    }
+}

+ 42 - 0
Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/QualityRatingDto.cs

@@ -0,0 +1,42 @@
+#nullable disable
+
+using System.Text.Json.Serialization;
+
+namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
+{
+    /// <summary>
+    /// Quality rating dto.
+    /// </summary>
+    public class QualityRatingDto
+    {
+        /// <summary>
+        /// Gets or sets the ratings body.
+        /// </summary>
+        [JsonPropertyName("ratingsBody")]
+        public string RatingsBody { get; set; }
+
+        /// <summary>
+        /// Gets or sets the rating.
+        /// </summary>
+        [JsonPropertyName("rating")]
+        public string Rating { get; set; }
+
+        /// <summary>
+        /// Gets or sets the min rating.
+        /// </summary>
+        [JsonPropertyName("minRating")]
+        public string MinRating { get; set; }
+
+        /// <summary>
+        /// Gets or sets the max rating.
+        /// </summary>
+        [JsonPropertyName("maxRating")]
+        public string MaxRating { get; set; }
+
+        /// <summary>
+        /// Gets or sets the increment.
+        /// </summary>
+        [JsonPropertyName("increment")]
+        public string Increment { get; set; }
+    }
+}

+ 24 - 0
Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/RatingDto.cs

@@ -0,0 +1,24 @@
+#nullable disable
+
+using System.Text.Json.Serialization;
+
+namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
+{
+    /// <summary>
+    /// Rating dto.
+    /// </summary>
+    public class RatingDto
+    {
+        /// <summary>
+        /// Gets or sets the body.
+        /// </summary>
+        [JsonPropertyName("body")]
+        public string Body { get; set; }
+
+        /// <summary>
+        /// Gets or sets the code.
+        /// </summary>
+        [JsonPropertyName("code")]
+        public string Code { get; set; }
+    }
+}

+ 24 - 0
Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/RecommendationDto.cs

@@ -0,0 +1,24 @@
+#nullable disable
+
+using System.Text.Json.Serialization;
+
+namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
+{
+    /// <summary>
+    /// Recommendation dto.
+    /// </summary>
+    public class RecommendationDto
+    {
+        /// <summary>
+        /// Gets or sets the program id.
+        /// </summary>
+        [JsonPropertyName("programID")]
+        public string ProgramId { get; set; }
+
+        /// <summary>
+        /// Gets or sets the title.
+        /// </summary>
+        [JsonPropertyName("title120")]
+        public string Title120 { get; set; }
+    }
+}

+ 25 - 0
Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/RequestScheduleForChannelDto.cs

@@ -0,0 +1,25 @@
+#nullable disable
+
+using System.Collections.Generic;
+using System.Text.Json.Serialization;
+
+namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
+{
+    /// <summary>
+    /// Request schedule for channel dto.
+    /// </summary>
+    public class RequestScheduleForChannelDto
+    {
+        /// <summary>
+        /// Gets or sets the station id.
+        /// </summary>
+        [JsonPropertyName("stationID")]
+        public string StationId { get; set; }
+
+        /// <summary>
+        /// Gets or sets the list of dates.
+        /// </summary>
+        [JsonPropertyName("date")]
+        public List<string> Date { get; set; }
+    }
+}

+ 25 - 0
Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ShowImagesDto.cs

@@ -0,0 +1,25 @@
+#nullable disable
+
+using System.Collections.Generic;
+using System.Text.Json.Serialization;
+
+namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
+{
+    /// <summary>
+    /// Show image dto.
+    /// </summary>
+    public class ShowImagesDto
+    {
+        /// <summary>
+        /// Gets or sets the program id.
+        /// </summary>
+        [JsonPropertyName("programID")]
+        public string ProgramId { get; set; }
+
+        /// <summary>
+        /// Gets or sets the list of data.
+        /// </summary>
+        [JsonPropertyName("data")]
+        public List<ImageDataDto> Data { get; set; }
+    }
+}

+ 67 - 0
Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/StationDto.cs

@@ -0,0 +1,67 @@
+#nullable disable
+
+using System.Collections.Generic;
+using System.Text.Json.Serialization;
+
+namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
+{
+    /// <summary>
+            /// Station dto.
+            /// </summary>
+            public class StationDto
+            {
+                /// <summary>
+                /// Gets or sets the station id.
+                /// </summary>
+                [JsonPropertyName("stationID")]
+                public string StationId { get; set; }
+
+                /// <summary>
+                /// Gets or sets the name.
+                /// </summary>
+                [JsonPropertyName("name")]
+                public string Name { get; set; }
+
+                /// <summary>
+                /// Gets or sets the callsign.
+                /// </summary>
+                [JsonPropertyName("callsign")]
+                public string Callsign { get; set; }
+
+                /// <summary>
+                /// Gets or sets the broadcast language.
+                /// </summary>
+                [JsonPropertyName("broadcastLanguage")]
+                public List<string> BroadcastLanguage { get; set; }
+
+                /// <summary>
+                /// Gets or sets the description language.
+                /// </summary>
+                [JsonPropertyName("descriptionLanguage")]
+                public List<string> DescriptionLanguage { get; set; }
+
+                /// <summary>
+                /// Gets or sets the broadcaster.
+                /// </summary>
+                [JsonPropertyName("broadcaster")]
+                public BroadcasterDto Broadcaster { get; set; }
+
+                /// <summary>
+                /// Gets or sets the affiliate.
+                /// </summary>
+                [JsonPropertyName("affiliate")]
+                public string Affiliate { get; set; }
+
+                /// <summary>
+                /// Gets or sets the logo.
+                /// </summary>
+                [JsonPropertyName("logo")]
+                public LogoDto Logo { get; set; }
+
+                /// <summary>
+                /// Gets or set a value indicating whether it is commercial free.
+                /// </summary>
+                [JsonPropertyName("isCommercialFree")]
+                public bool? IsCommercialFree { get; set; }
+            }
+}

+ 18 - 0
Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/TitleDto.cs

@@ -0,0 +1,18 @@
+#nullable disable
+
+using System.Text.Json.Serialization;
+
+namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
+{
+    /// <summary>
+    /// Title dto.
+    /// </summary>
+    public class TitleDto
+    {
+        /// <summary>
+        /// Gets or sets the title.
+        /// </summary>
+        [JsonPropertyName("title120")]
+        public string Title120 { get; set; }
+    }
+}

+ 36 - 0
Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/TokenDto.cs

@@ -0,0 +1,36 @@
+#nullable disable
+
+using System.Text.Json.Serialization;
+
+namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
+{
+    /// <summary>
+    /// The token dto.
+    /// </summary>
+    public class TokenDto
+    {
+        /// <summary>
+        /// Gets or sets the response code.
+        /// </summary>
+        [JsonPropertyName("code")]
+        public int Code { get; set; }
+
+        /// <summary>
+        /// Gets or sets the response message.
+        /// </summary>
+        [JsonPropertyName("message")]
+        public string Message { get; set; }
+
+        /// <summary>
+        /// Gets or sets the server id.
+        /// </summary>
+        [JsonPropertyName("serverID")]
+        public string ServerId { get; set; }
+
+        /// <summary>
+        /// Gets or sets the token.
+        /// </summary>
+        [JsonPropertyName("token")]
+        public string Token { get; set; }
+    }
+}

+ 3 - 3
Emby.Server.Implementations/LiveTv/LiveTvManager.cs

@@ -403,7 +403,7 @@ namespace Emby.Server.Implementations.LiveTv
             // Set the total bitrate if not already supplied
             mediaSource.InferTotalBitrate();
 
-            if (!(service is EmbyTV.EmbyTV))
+            if (service is not EmbyTV.EmbyTV)
             {
                 // We can't trust that we'll be able to direct stream it through emby server, no matter what the provider says
                 // mediaSource.SupportsDirectPlay = false;
@@ -1724,7 +1724,7 @@ namespace Emby.Server.Implementations.LiveTv
 
             await service.CancelTimerAsync(timer.ExternalId, CancellationToken.None).ConfigureAwait(false);
 
-            if (!(service is EmbyTV.EmbyTV))
+            if (service is not EmbyTV.EmbyTV)
             {
                 TimerCancelled?.Invoke(this, new GenericEventArgs<TimerEventInfo>(new TimerEventInfo(id)));
             }
@@ -2050,7 +2050,7 @@ namespace Emby.Server.Implementations.LiveTv
 
             _logger.LogInformation("New recording scheduled");
 
-            if (!(service is EmbyTV.EmbyTV))
+            if (service is not EmbyTV.EmbyTV)
             {
                 TimerCreated?.Invoke(this, new GenericEventArgs<TimerEventInfo>(
                     new TimerEventInfo(newTimerId)

+ 1 - 0
Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs

@@ -27,6 +27,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
     {
         private string _channel;
         private string _program;
+
         public LegacyHdHomerunChannelCommands(string url)
         {
             // parse url for channel and program

+ 22 - 16
Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs

@@ -13,7 +13,6 @@ using System.Threading.Tasks;
 using Jellyfin.Extensions;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.Net;
-using MediaBrowser.Controller;
 using MediaBrowser.Controller.LiveTv;
 using MediaBrowser.Model.LiveTv;
 using Microsoft.Extensions.Logging;
@@ -44,22 +43,29 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
 
         public async Task<Stream> GetListingsStream(TunerHostInfo info, CancellationToken cancellationToken)
         {
-            if (info.Url.StartsWith("http", StringComparison.OrdinalIgnoreCase))
+            if (info == null)
             {
-                using var requestMessage = new HttpRequestMessage(HttpMethod.Get, info.Url);
-                if (!string.IsNullOrEmpty(info.UserAgent))
-                {
-                    requestMessage.Headers.UserAgent.TryParseAdd(info.UserAgent);
-                }
+                throw new ArgumentNullException(nameof(info));
+            }
+
+            if (!info.Url.StartsWith("http", StringComparison.OrdinalIgnoreCase))
+            {
+                return File.OpenRead(info.Url);
+            }
 
-                var response = await _httpClientFactory.CreateClient(NamedClient.Default)
-                    .SendAsync(requestMessage, cancellationToken)
-                    .ConfigureAwait(false);
-                response.EnsureSuccessStatusCode();
-                return await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
+            using var requestMessage = new HttpRequestMessage(HttpMethod.Get, info.Url);
+            if (!string.IsNullOrEmpty(info.UserAgent))
+            {
+                requestMessage.Headers.UserAgent.TryParseAdd(info.UserAgent);
             }
 
-            return File.OpenRead(info.Url);
+            // Set HttpCompletionOption.ResponseHeadersRead to prevent timeouts on larger files
+            var response = await _httpClientFactory.CreateClient(NamedClient.Default)
+                .SendAsync(requestMessage, HttpCompletionOption.ResponseHeadersRead, cancellationToken)
+                .ConfigureAwait(false);
+            response.EnsureSuccessStatusCode();
+
+            return await response.Content.ReadAsStreamAsync(cancellationToken);
         }
 
         private async Task<List<ChannelInfo>> GetChannelsAsync(TextReader reader, string channelIdPrefix, string tunerHostId)
@@ -83,7 +89,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
                 if (trimmedLine.StartsWith(ExtInfPrefix, StringComparison.OrdinalIgnoreCase))
                 {
                     extInf = trimmedLine.Substring(ExtInfPrefix.Length).Trim();
-                    _logger.LogInformation("Found m3u channel: {0}", extInf);
                 }
                 else if (!string.IsNullOrWhiteSpace(extInf) && !trimmedLine.StartsWith('#'))
                 {
@@ -99,6 +104,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
 
                     channel.Path = trimmedLine;
                     channels.Add(channel);
+                    _logger.LogInformation("Parsed channel: {ChannelName}", channel.Name);
                     extInf = string.Empty;
                 }
             }
@@ -289,11 +295,11 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
                 }
             }
 
-            attributes.TryGetValue("tvg-name", out string name);
+            string name = nameInExtInf;
 
             if (string.IsNullOrWhiteSpace(name))
             {
-                name = nameInExtInf;
+                attributes.TryGetValue("tvg-name", out name);
             }
 
             if (string.IsNullOrWhiteSpace(name))

+ 12 - 10
Emby.Server.Implementations/Localization/Core/af.json

@@ -2,24 +2,24 @@
     "Artists": "Kunstenare",
     "Channels": "Kanale",
     "Folders": "Lêergidse",
-    "Favorites": "Gunstellinge",
+    "Favorites": "Gunstelinge",
     "HeaderFavoriteShows": "Gunsteling Vertonings",
     "ValueSpecialEpisodeName": "Spesiale - {0}",
-    "HeaderAlbumArtists": "Album Kunstenaars",
+    "HeaderAlbumArtists": "Kunstenaars se Album",
     "Books": "Boeke",
     "HeaderNextUp": "Volgende",
     "Movies": "Flieks",
     "Shows": "Televisie Reekse",
     "HeaderContinueWatching": "Kyk Verder",
     "HeaderFavoriteEpisodes": "Gunsteling Episodes",
-    "Photos": "Fotos",
+    "Photos": "Foto's",
     "Playlists": "Snitlyste",
     "HeaderFavoriteArtists": "Gunsteling Kunstenaars",
     "HeaderFavoriteAlbums": "Gunsteling Albums",
     "Sync": "Sinkroniseer",
     "HeaderFavoriteSongs": "Gunsteling Liedjies",
     "Songs": "Liedjies",
-    "DeviceOnlineWithName": "{0} gekoppel is",
+    "DeviceOnlineWithName": "{0} is gekoppel",
     "DeviceOfflineWithName": "{0} is ontkoppel",
     "Collections": "Versamelings",
     "Inherit": "Ontvang",
@@ -71,7 +71,7 @@
     "NameSeasonUnknown": "Seisoen Onbekend",
     "NameSeasonNumber": "Seisoen {0}",
     "NameInstallFailed": "{0} installering het misluk",
-    "MusicVideos": "Musiek videos",
+    "MusicVideos": "Musiek Videos",
     "Music": "Musiek",
     "MixedContent": "Gemengde inhoud",
     "MessageServerConfigurationUpdated": "Bediener konfigurasie is opgedateer",
@@ -79,15 +79,15 @@
     "MessageApplicationUpdatedTo": "Jellyfin Bediener is opgedateer na {0}",
     "MessageApplicationUpdated": "Jellyfin Bediener is opgedateer",
     "Latest": "Nuutste",
-    "LabelRunningTimeValue": "Lopende tyd: {0}",
+    "LabelRunningTimeValue": "Werktyd: {0}",
     "LabelIpAddressValue": "IP adres: {0}",
     "ItemRemovedWithName": "{0} is uit versameling verwyder",
-    "ItemAddedWithName": "{0} is in die versameling",
-    "HomeVideos": "Tuis opnames",
+    "ItemAddedWithName": "{0} is by die versameling gevoeg",
+    "HomeVideos": "Tuis Videos",
     "HeaderRecordingGroups": "Groep Opnames",
     "Genres": "Genres",
     "FailedLoginAttemptWithUserName": "Mislukte aansluiting van {0}",
-    "ChapterNameValue": "Hoofstuk",
+    "ChapterNameValue": "Hoofstuk {0}",
     "CameraImageUploadedFrom": "'n Nuwe kamera photo opgelaai van {0}",
     "AuthenticationSucceededWithUserName": "{0} suksesvol geverifieer",
     "Albums": "Albums",
@@ -117,5 +117,7 @@
     "Forced": "Geforseer",
     "Default": "Oorspronklik",
     "TaskCleanActivityLogDescription": "Verwyder aktiwiteitsaantekeninge ouer as die opgestelde ouderdom.",
-    "TaskCleanActivityLog": "Maak Aktiwiteitsaantekeninge Skoon"
+    "TaskCleanActivityLog": "Maak Aktiwiteitsaantekeninge Skoon",
+    "TaskOptimizeDatabaseDescription": "Komprimeer databasis en verkort vrye ruimte. As hierdie taak uitgevoer word nadat die media versameling geskandeer is of ander veranderings aangebring is wat databasisaanpassings impliseer, kan dit die prestasie verbeter.",
+    "TaskOptimizeDatabase": "Optimaliseer databasis"
 }

+ 2 - 2
Emby.Server.Implementations/Localization/Core/bg-BG.json

@@ -25,7 +25,7 @@
     "HeaderLiveTV": "Телевизия на живо",
     "HeaderNextUp": "Следва",
     "HeaderRecordingGroups": "Запис групи",
-    "HomeVideos": "Домашни клипове",
+    "HomeVideos": "Домашни Клипове",
     "Inherit": "Наследяване",
     "ItemAddedWithName": "{0} е добавено към библиотеката",
     "ItemRemovedWithName": "{0} е премахнато от библиотеката",
@@ -39,7 +39,7 @@
     "MixedContent": "Смесено съдържание",
     "Movies": "Филми",
     "Music": "Музика",
-    "MusicVideos": "Музикални видеа",
+    "MusicVideos": "Музикални Видеа",
     "NameInstallFailed": "{0} не можа да се инсталира",
     "NameSeasonNumber": "Сезон {0}",
     "NameSeasonUnknown": "Неразпознат сезон",

+ 1 - 1
Emby.Server.Implementations/Localization/Core/ca.json

@@ -5,7 +5,7 @@
     "Artists": "Artistes",
     "AuthenticationSucceededWithUserName": "{0} s'ha autenticat correctament",
     "Books": "Llibres",
-    "CameraImageUploadedFrom": "Una nova imatge de la càmera ha estat pujada des de {0}",
+    "CameraImageUploadedFrom": "S'ha pujat una nova imatge des de la camera desde {0}",
     "Channels": "Canals",
     "ChapterNameValue": "Capítol {0}",
     "Collections": "Col·leccions",

+ 1 - 1
Emby.Server.Implementations/Localization/Core/cs.json

@@ -15,7 +15,7 @@
     "Favorites": "Oblíbené",
     "Folders": "Složky",
     "Genres": "Žánry",
-    "HeaderAlbumArtists": "Umělci alba",
+    "HeaderAlbumArtists": "Album umělce",
     "HeaderContinueWatching": "Pokračovat ve sledování",
     "HeaderFavoriteAlbums": "Oblíbená alba",
     "HeaderFavoriteArtists": "Oblíbení interpreti",

+ 7 - 5
Emby.Server.Implementations/Localization/Core/el.json

@@ -1,5 +1,5 @@
 {
-    "Albums": "Άλμπουμς",
+    "Albums": "Άλμπουμ",
     "AppDeviceValues": "Εφαρμογή: {0}, Συσκευή: {1}",
     "Application": "Εφαρμογή",
     "Artists": "Καλλιτέχνες",
@@ -15,7 +15,7 @@
     "Favorites": "Αγαπημένα",
     "Folders": "Φάκελοι",
     "Genres": "Είδη",
-    "HeaderAlbumArtists": "Καλλιτέχνες του Άλμπουμ",
+    "HeaderAlbumArtists": "Άλμπουμ Καλλιτέχνη",
     "HeaderContinueWatching": "Συνεχίστε την παρακολούθηση",
     "HeaderFavoriteAlbums": "Αγαπημένα Άλμπουμ",
     "HeaderFavoriteArtists": "Αγαπημένοι Καλλιτέχνες",
@@ -39,7 +39,7 @@
     "MixedContent": "Ανάμεικτο Περιεχόμενο",
     "Movies": "Ταινίες",
     "Music": "Μουσική",
-    "MusicVideos": "Μουσικά βίντεο",
+    "MusicVideos": "Μουσικά Βίντεο",
     "NameInstallFailed": "{0} η εγκατάσταση απέτυχε",
     "NameSeasonNumber": "Κύκλος {0}",
     "NameSeasonUnknown": "Άγνωστος Κύκλος",
@@ -62,7 +62,7 @@
     "NotificationOptionVideoPlaybackStopped": "Η αναπαραγωγή βίντεο σταμάτησε",
     "Photos": "Φωτογραφίες",
     "Playlists": "Λίστες αναπαραγωγής",
-    "Plugin": "Plugin",
+    "Plugin": "Πρόσθετο",
     "PluginInstalledWithName": "{0} εγκαταστήθηκε",
     "PluginUninstalledWithName": "{0} έχει απεγκατασταθεί",
     "PluginUpdatedWithName": "{0} έχει αναβαθμιστεί",
@@ -118,5 +118,7 @@
     "TaskCleanActivityLog": "Καθαρό Αρχείο Καταγραφής Δραστηριοτήτων",
     "Undefined": "Απροσδιόριστο",
     "Forced": "Εξαναγκασμένο",
-    "Default": "Προεπιλογή"
+    "Default": "Προεπιλογή",
+    "TaskOptimizeDatabaseDescription": "Συμπιέζει τη βάση δεδομένων και δημιουργεί ελεύθερο χώρο. Η εκτέλεση αυτής της εργασίας μετά τη σάρωση της βιβλιοθήκης ή την πραγματοποίηση άλλων αλλαγών που συνεπάγονται τροποποιήσεις της βάσης δεδομένων μπορεί να βελτιώσει την απόδοση.",
+    "TaskOptimizeDatabase": "Βελτιστοποίηση βάσης δεδομένων"
 }

+ 3 - 3
Emby.Server.Implementations/Localization/Core/en-US.json

@@ -17,7 +17,7 @@
     "Folders": "Folders",
     "Forced": "Forced",
     "Genres": "Genres",
-    "HeaderAlbumArtists": "Album Artists",
+    "HeaderAlbumArtists": "Artist's Album",
     "HeaderContinueWatching": "Continue Watching",
     "HeaderFavoriteAlbums": "Favorite Albums",
     "HeaderFavoriteArtists": "Favorite Artists",
@@ -27,7 +27,7 @@
     "HeaderLiveTV": "Live TV",
     "HeaderNextUp": "Next Up",
     "HeaderRecordingGroups": "Recording Groups",
-    "HomeVideos": "Home videos",
+    "HomeVideos": "Home Videos",
     "Inherit": "Inherit",
     "ItemAddedWithName": "{0} was added to the library",
     "ItemRemovedWithName": "{0} was removed from the library",
@@ -41,7 +41,7 @@
     "MixedContent": "Mixed content",
     "Movies": "Movies",
     "Music": "Music",
-    "MusicVideos": "Music videos",
+    "MusicVideos": "Music Videos",
     "NameInstallFailed": "{0} installation failed",
     "NameSeasonNumber": "Season {0}",
     "NameSeasonUnknown": "Season Unknown",

+ 10 - 8
Emby.Server.Implementations/Localization/Core/es-MX.json

@@ -15,7 +15,7 @@
     "Favorites": "Favoritos",
     "Folders": "Carpetas",
     "Genres": "Géneros",
-    "HeaderAlbumArtists": "Artistas del álbum",
+    "HeaderAlbumArtists": "Artistas del Álbum",
     "HeaderContinueWatching": "Continuar viendo",
     "HeaderFavoriteAlbums": "Álbumes favoritos",
     "HeaderFavoriteArtists": "Artistas favoritos",
@@ -25,7 +25,7 @@
     "HeaderLiveTV": "TV en vivo",
     "HeaderNextUp": "A continuación",
     "HeaderRecordingGroups": "Grupos de grabación",
-    "HomeVideos": "Videos caseros",
+    "HomeVideos": "Videos Caseros",
     "Inherit": "Heredar",
     "ItemAddedWithName": "{0} fue agregado a la biblioteca",
     "ItemRemovedWithName": "{0} fue removido de la biblioteca",
@@ -39,7 +39,7 @@
     "MixedContent": "Contenido mezclado",
     "Movies": "Películas",
     "Music": "Música",
-    "MusicVideos": "Videos musicales",
+    "MusicVideos": "Videos Musicales",
     "NameInstallFailed": "Falló la instalación de {0}",
     "NameSeasonNumber": "Temporada {0}",
     "NameSeasonUnknown": "Temporada desconocida",
@@ -49,7 +49,7 @@
     "NotificationOptionAudioPlayback": "Reproducción de audio iniciada",
     "NotificationOptionAudioPlaybackStopped": "Reproducción de audio detenida",
     "NotificationOptionCameraImageUploaded": "Imagen de la cámara subida",
-    "NotificationOptionInstallationFailed": "Falla de instalación",
+    "NotificationOptionInstallationFailed": "Fallo en la instalación",
     "NotificationOptionNewLibraryContent": "Nuevo contenido agregado",
     "NotificationOptionPluginError": "Falla de complemento",
     "NotificationOptionPluginInstalled": "Complemento instalado",
@@ -69,7 +69,7 @@
     "ProviderValue": "Proveedor: {0}",
     "ScheduledTaskFailedWithName": "{0} falló",
     "ScheduledTaskStartedWithName": "{0} iniciado",
-    "ServerNameNeedsToBeRestarted": "{0} debe ser reiniciado",
+    "ServerNameNeedsToBeRestarted": "{0} necesita ser reiniciado",
     "Shows": "Programas",
     "Songs": "Canciones",
     "StartupEmbyServerIsLoading": "El servidor Jellyfin está cargando. Por favor, intente de nuevo pronto.",
@@ -94,9 +94,9 @@
     "VersionNumber": "Versión {0}",
     "TaskDownloadMissingSubtitlesDescription": "Busca subtítulos faltantes en Internet basándose en la configuración de metadatos.",
     "TaskDownloadMissingSubtitles": "Descargar subtítulos faltantes",
-    "TaskRefreshChannelsDescription": "Actualiza la información de canales de Internet.",
+    "TaskRefreshChannelsDescription": "Actualiza la información de los canales de Internet.",
     "TaskRefreshChannels": "Actualizar canales",
-    "TaskCleanTranscodeDescription": "Elimina archivos transcodificados que tengan más de un día.",
+    "TaskCleanTranscodeDescription": "Elimina archivos transcodificados que tengan más de un día de antigüedad.",
     "TaskCleanTranscode": "Limpiar directorio de transcodificado",
     "TaskUpdatePluginsDescription": "Descarga e instala actualizaciones para complementos que están configurados para actualizarse automáticamente.",
     "TaskUpdatePlugins": "Actualizar complementos",
@@ -118,5 +118,7 @@
     "TaskCleanActivityLog": "Limpiar registro de actividades",
     "Undefined": "Sin definir",
     "Forced": "Forzado",
-    "Default": "Predeterminado"
+    "Default": "Predeterminado",
+    "TaskOptimizeDatabase": "Optimizar base de datos",
+    "TaskOptimizeDatabaseDescription": "Compacta la base de datos y trunca el espacio libre. Puede mejorar el rendimiento si se realiza esta tarea después de escanear la biblioteca o después de realizar otros cambios que impliquen modificar la base de datos."
 }

+ 1 - 1
Emby.Server.Implementations/Localization/Core/es.json

@@ -15,7 +15,7 @@
     "Favorites": "Favoritos",
     "Folders": "Carpetas",
     "Genres": "Géneros",
-    "HeaderAlbumArtists": "Artistas del álbum",
+    "HeaderAlbumArtists": "Artista del álbum",
     "HeaderContinueWatching": "Continuar viendo",
     "HeaderFavoriteAlbums": "Álbumes favoritos",
     "HeaderFavoriteArtists": "Artistas favoritos",

+ 1 - 1
Emby.Server.Implementations/Localization/Core/hu.json

@@ -15,7 +15,7 @@
     "Favorites": "Kedvencek",
     "Folders": "Könyvtárak",
     "Genres": "Műfajok",
-    "HeaderAlbumArtists": "Album előadók",
+    "HeaderAlbumArtists": "Előadó albumai",
     "HeaderContinueWatching": "Megtekintés folytatása",
     "HeaderFavoriteAlbums": "Kedvenc albumok",
     "HeaderFavoriteArtists": "Kedvenc előadók",

+ 3 - 3
Emby.Server.Implementations/Localization/Core/it.json

@@ -15,7 +15,7 @@
     "Favorites": "Preferiti",
     "Folders": "Cartelle",
     "Genres": "Generi",
-    "HeaderAlbumArtists": "Artisti degli Album",
+    "HeaderAlbumArtists": "Artisti dell'Album",
     "HeaderContinueWatching": "Continua a guardare",
     "HeaderFavoriteAlbums": "Album Preferiti",
     "HeaderFavoriteArtists": "Artisti Preferiti",
@@ -25,7 +25,7 @@
     "HeaderLiveTV": "Diretta TV",
     "HeaderNextUp": "Prossimo",
     "HeaderRecordingGroups": "Gruppi di Registrazione",
-    "HomeVideos": "Video personali",
+    "HomeVideos": "Video Personali",
     "Inherit": "Eredita",
     "ItemAddedWithName": "{0} è stato aggiunto alla libreria",
     "ItemRemovedWithName": "{0} è stato rimosso dalla libreria",
@@ -39,7 +39,7 @@
     "MixedContent": "Contenuto misto",
     "Movies": "Film",
     "Music": "Musica",
-    "MusicVideos": "Video musicali",
+    "MusicVideos": "Video Musicali",
     "NameInstallFailed": "{0} installazione fallita",
     "NameSeasonNumber": "Stagione {0}",
     "NameSeasonUnknown": "Stagione sconosciuta",

+ 1 - 1
Emby.Server.Implementations/Localization/Core/ja.json

@@ -15,7 +15,7 @@
     "Favorites": "お気に入り",
     "Folders": "フォルダー",
     "Genres": "ジャンル",
-    "HeaderAlbumArtists": "アルバムアーティスト",
+    "HeaderAlbumArtists": "アーティストのアルバム",
     "HeaderContinueWatching": "視聴を続ける",
     "HeaderFavoriteAlbums": "お気に入りのアルバム",
     "HeaderFavoriteArtists": "お気に入りのアーティスト",

+ 1 - 1
Emby.Server.Implementations/Localization/Core/kk.json

@@ -15,7 +15,7 @@
     "Favorites": "Tañdaulylar",
     "Folders": "Qaltalar",
     "Genres": "Janrlar",
-    "HeaderAlbumArtists": "Älbom oryndauşylary",
+    "HeaderAlbumArtists": "Oryndauşynyñ älbomy",
     "HeaderContinueWatching": "Qaraudy jalğastyru",
     "HeaderFavoriteAlbums": "Tañdauly älbomdar",
     "HeaderFavoriteArtists": "Tañdauly oryndauşylar",

+ 1 - 1
Emby.Server.Implementations/Localization/Core/ko.json

@@ -15,7 +15,7 @@
     "Favorites": "즐겨찾기",
     "Folders": "폴더",
     "Genres": "장르",
-    "HeaderAlbumArtists": "앨범 아티스트",
+    "HeaderAlbumArtists": "아티스트의 앨범",
     "HeaderContinueWatching": "계속 시청하기",
     "HeaderFavoriteAlbums": "즐겨찾는 앨범",
     "HeaderFavoriteArtists": "즐겨찾는 아티스트",

+ 1 - 1
Emby.Server.Implementations/Localization/Core/ml.json

@@ -103,7 +103,7 @@
     "ValueSpecialEpisodeName": "പ്രത്യേക - {0}",
     "Collections": "ശേഖരങ്ങൾ",
     "Folders": "ഫോൾഡറുകൾ",
-    "HeaderAlbumArtists": "ആൽബം ആർട്ടിസ്റ്റുകൾ",
+    "HeaderAlbumArtists": "കലാകാരന്റെ ആൽബം",
     "Sync": "സമന്വയിപ്പിക്കുക",
     "Movies": "സിനിമകൾ",
     "Photos": "ഫോട്ടോകൾ",

+ 4 - 3
Emby.Server.Implementations/Localization/Core/pl.json

@@ -15,7 +15,7 @@
     "Favorites": "Ulubione",
     "Folders": "Foldery",
     "Genres": "Gatunki",
-    "HeaderAlbumArtists": "Wykonawcy albumów",
+    "HeaderAlbumArtists": "Album artysty",
     "HeaderContinueWatching": "Kontynuuj odtwarzanie",
     "HeaderFavoriteAlbums": "Ulubione albumy",
     "HeaderFavoriteArtists": "Ulubieni wykonawcy",
@@ -25,7 +25,7 @@
     "HeaderLiveTV": "Telewizja",
     "HeaderNextUp": "Do obejrzenia",
     "HeaderRecordingGroups": "Grupy nagrań",
-    "HomeVideos": "Nagrania prywatne",
+    "HomeVideos": "Nagrania domowe",
     "Inherit": "Dziedzicz",
     "ItemAddedWithName": "{0} zostało dodane do biblioteki",
     "ItemRemovedWithName": "{0} zostało usunięte z biblioteki",
@@ -119,5 +119,6 @@
     "Undefined": "Nieustalony",
     "Forced": "Wymuszony",
     "Default": "Domyślne",
-    "TaskOptimizeDatabase": "Optymalizuj bazę danych"
+    "TaskOptimizeDatabase": "Optymalizuj bazę danych",
+    "TaskOptimizeDatabaseDescription": "Kompaktuje bazę danych i obcina wolne miejsce. Uruchomienie tego zadania po przeskanowaniu biblioteki lub dokonaniu innych zmian, które pociągają za sobą modyfikacje bazy danych, może poprawić wydajność."
 }

+ 1 - 0
Emby.Server.Implementations/Localization/Core/pr.json

@@ -0,0 +1 @@
+{}

+ 3 - 1
Emby.Server.Implementations/Localization/Core/pt-BR.json

@@ -118,5 +118,7 @@
     "TaskCleanActivityLog": "Limpar Registro de Atividades",
     "Undefined": "Indefinido",
     "Forced": "Forçado",
-    "Default": "Padrão"
+    "Default": "Padrão",
+    "TaskOptimizeDatabaseDescription": "Compactar base de dados e liberar espaço livre. Executar esta tarefa após realizar mudanças que impliquem em modificações da base de dados pode trazer melhorias de desempenho.",
+    "TaskOptimizeDatabase": "Otimizar base de dados"
 }

+ 2 - 1
Emby.Server.Implementations/Localization/Core/pt.json

@@ -117,5 +117,6 @@
     "Undefined": "Indefinido",
     "Forced": "Forçado",
     "Default": "Predefinição",
-    "TaskCleanActivityLogDescription": "Apaga itens no registro com idade acima do que é configurado."
+    "TaskCleanActivityLogDescription": "Apaga itens no registro com idade acima do que é configurado.",
+    "TaskOptimizeDatabase": "Otimizar base de dados"
 }

+ 1 - 1
Emby.Server.Implementations/Localization/Core/ru.json

@@ -25,7 +25,7 @@
     "HeaderLiveTV": "Эфир",
     "HeaderNextUp": "Очередное",
     "HeaderRecordingGroups": "Группы записей",
-    "HomeVideos": "Домашнее видео",
+    "HomeVideos": "Домашние видео",
     "Inherit": "Наследуемое",
     "ItemAddedWithName": "{0} - добавлено в медиатеку",
     "ItemRemovedWithName": "{0} - изъято из медиатеки",

+ 1 - 1
Emby.Server.Implementations/Localization/Core/sk.json

@@ -39,7 +39,7 @@
     "MixedContent": "Zmiešaný obsah",
     "Movies": "Filmy",
     "Music": "Hudba",
-    "MusicVideos": "Hudobné videoklipy",
+    "MusicVideos": "Hudobné videá",
     "NameInstallFailed": "Inštalácia {0} zlyhala",
     "NameSeasonNumber": "Séria {0}",
     "NameSeasonUnknown": "Neznáma séria",

+ 5 - 3
Emby.Server.Implementations/Localization/Core/sv.json

@@ -15,7 +15,7 @@
     "Favorites": "Favoriter",
     "Folders": "Mappar",
     "Genres": "Genrer",
-    "HeaderAlbumArtists": "Albumartister",
+    "HeaderAlbumArtists": "Artistens album",
     "HeaderContinueWatching": "Fortsätt kolla",
     "HeaderFavoriteAlbums": "Favoritalbum",
     "HeaderFavoriteArtists": "Favoritartister",
@@ -25,7 +25,7 @@
     "HeaderLiveTV": "Live-TV",
     "HeaderNextUp": "Nästa",
     "HeaderRecordingGroups": "Inspelningsgrupper",
-    "HomeVideos": "Hemvideor",
+    "HomeVideos": "Hemmavideor",
     "Inherit": "Ärv",
     "ItemAddedWithName": "{0} lades till i biblioteket",
     "ItemRemovedWithName": "{0} togs bort från biblioteket",
@@ -118,5 +118,7 @@
     "TaskCleanActivityLog": "Rensa Aktivitets Logg",
     "Undefined": "odefinierad",
     "Forced": "Tvingad",
-    "Default": "Standard"
+    "Default": "Standard",
+    "TaskOptimizeDatabase": "Optimera databasen",
+    "TaskOptimizeDatabaseDescription": "Komprimerar databasen och trunkerar ledigt utrymme. Prestandan kan förbättras genom att köra denna task efter att du har skannat biblioteket eller gjort andra förändringar som indikerar att databasen har modifierats."
 }

+ 1 - 1
Emby.Server.Implementations/Localization/Core/tr.json

@@ -25,7 +25,7 @@
     "HeaderLiveTV": "Canlı TV",
     "HeaderNextUp": "Gelecek Hafta",
     "HeaderRecordingGroups": "Kayıt Grupları",
-    "HomeVideos": "Ev videoları",
+    "HomeVideos": "Ana sayfa videoları",
     "Inherit": "Devral",
     "ItemAddedWithName": "{0} kütüphaneye eklendi",
     "ItemRemovedWithName": "{0} kütüphaneden silindi",

+ 2 - 2
Emby.Server.Implementations/Localization/Core/vi.json

@@ -3,7 +3,7 @@
     "Favorites": "Yêu Thích",
     "Folders": "Thư Mục",
     "Genres": "Thể Loại",
-    "HeaderAlbumArtists": "Tuyển Tập Nghệ sĩ",
+    "HeaderAlbumArtists": "Album Nghệ sĩ",
     "HeaderContinueWatching": "Xem Tiếp",
     "HeaderLiveTV": "TV Trực Tiếp",
     "Movies": "Phim",
@@ -82,7 +82,7 @@
     "NameSeasonUnknown": "Không Rõ Mùa",
     "NameSeasonNumber": "Phần {0}",
     "NameInstallFailed": "{0} cài đặt thất bại",
-    "MusicVideos": "Video Nhạc",
+    "MusicVideos": "Videos Nhạc",
     "Music": "Nhạc",
     "MixedContent": "Nội dung hỗn hợp",
     "MessageServerConfigurationUpdated": "Cấu hình máy chủ đã được cập nhật",

+ 5 - 5
Emby.Server.Implementations/Localization/Core/zh-CN.json

@@ -7,7 +7,7 @@
     "Books": "书籍",
     "CameraImageUploadedFrom": "新的相机图像已从 {0} 上传",
     "Channels": "频道",
-    "ChapterNameValue": "第 {0} 集",
+    "ChapterNameValue": "章节 {0}",
     "Collections": "合集",
     "DeviceOfflineWithName": "{0} 已断开",
     "DeviceOnlineWithName": "{0} 已连接",
@@ -15,8 +15,8 @@
     "Favorites": "我的最爱",
     "Folders": "文件夹",
     "Genres": "风格",
-    "HeaderAlbumArtists": "专辑家",
-    "HeaderContinueWatching": "继续观",
+    "HeaderAlbumArtists": "专辑艺术家",
+    "HeaderContinueWatching": "继续观",
     "HeaderFavoriteAlbums": "收藏的专辑",
     "HeaderFavoriteArtists": "最爱的艺术家",
     "HeaderFavoriteEpisodes": "最爱的剧集",
@@ -108,8 +108,8 @@
     "TaskCleanLogs": "清理日志目录",
     "TaskRefreshLibraryDescription": "扫描你的媒体库以获取新文件并刷新元数据。",
     "TaskRefreshLibrary": "扫描媒体库",
-    "TaskRefreshChapterImagesDescription": "为包含剧集的视频提取缩略图。",
-    "TaskRefreshChapterImages": "提取剧集图片",
+    "TaskRefreshChapterImagesDescription": "为包含章节的视频提取缩略图。",
+    "TaskRefreshChapterImages": "提取章节图片",
     "TaskCleanCacheDescription": "删除系统不再需要的缓存文件。",
     "TaskCleanCache": "清理缓存目录",
     "TasksApplicationCategory": "应用程序",

+ 6 - 3
Emby.Server.Implementations/Localization/Core/zh-HK.json

@@ -13,7 +13,7 @@
     "DeviceOnlineWithName": "{0} 已經連接",
     "FailedLoginAttemptWithUserName": "來自 {0} 的登入失敗",
     "Favorites": "我的最愛",
-    "Folders": "檔案夾",
+    "Folders": "資料夾",
     "Genres": "風格",
     "HeaderAlbumArtists": "專輯藝人",
     "HeaderContinueWatching": "繼續觀看",
@@ -39,7 +39,7 @@
     "MixedContent": "混合內容",
     "Movies": "電影",
     "Music": "音樂",
-    "MusicVideos": "音樂視頻",
+    "MusicVideos": "音樂影片",
     "NameInstallFailed": "{0} 安裝失敗",
     "NameSeasonNumber": "第 {0} 季",
     "NameSeasonUnknown": "未知季數",
@@ -117,5 +117,8 @@
     "TaskCleanActivityLog": "清理活動記錄",
     "Undefined": "未定義",
     "Forced": "強制",
-    "Default": "預設"
+    "Default": "預設",
+    "TaskOptimizeDatabaseDescription": "壓縮數據庫並截斷可用空間。在掃描媒體庫或執行其他數據庫的修改後運行此任務可能會提高性能。",
+    "TaskOptimizeDatabase": "最佳化數據庫",
+    "TaskCleanActivityLogDescription": "刪除早於設定時間的日誌記錄。"
 }

+ 4 - 2
Emby.Server.Implementations/Localization/Core/zh-TW.json

@@ -24,7 +24,7 @@
     "HeaderFavoriteSongs": "最愛歌曲",
     "HeaderLiveTV": "電視直播",
     "HeaderNextUp": "接下來",
-    "HomeVideos": "自製影片",
+    "HomeVideos": "家庭影片",
     "ItemAddedWithName": "{0} 已新增至媒體庫",
     "ItemRemovedWithName": "{0} 已從媒體庫移除",
     "LabelIpAddressValue": "IP 位址:{0}",
@@ -117,5 +117,7 @@
     "TaskCleanActivityLog": "清除活動紀錄",
     "Undefined": "未定義的",
     "Forced": "強制",
-    "Default": "原本"
+    "Default": "原本",
+    "TaskOptimizeDatabaseDescription": "縮小資料庫並釋放可用空間。在掃描資料庫或進行資料庫相關的更動後使用此功能會增加效能。",
+    "TaskOptimizeDatabase": "最佳化資料庫"
 }

+ 27 - 36
Emby.Server.Implementations/Localization/LocalizationManager.cs

@@ -1,5 +1,3 @@
-#nullable disable
-
 using System;
 using System.Collections.Concurrent;
 using System.Collections.Generic;
@@ -38,10 +36,10 @@ namespace Emby.Server.Implementations.Localization
         private readonly ConcurrentDictionary<string, Dictionary<string, string>> _dictionaries =
             new ConcurrentDictionary<string, Dictionary<string, string>>(StringComparer.OrdinalIgnoreCase);
 
-        private List<CultureDto> _cultures;
-
         private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
 
+        private List<CultureDto> _cultures = new List<CultureDto>();
+
         /// <summary>
         /// Initializes a new instance of the <see cref="LocalizationManager" /> class.
         /// </summary>
@@ -72,8 +70,8 @@ namespace Emby.Server.Implementations.Localization
                 string countryCode = resource.Substring(RatingsPath.Length, 2);
                 var dict = new Dictionary<string, ParentalRating>(StringComparer.OrdinalIgnoreCase);
 
-                await using var str = _assembly.GetManifestResourceStream(resource);
-                using var reader = new StreamReader(str);
+                await using var stream = _assembly.GetManifestResourceStream(resource);
+                using var reader = new StreamReader(stream!); // shouldn't be null here, we just got the resource path from Assembly.GetManifestResourceNames()
                 await foreach (var line in reader.ReadAllLinesAsync().ConfigureAwait(false))
                 {
                     if (string.IsNullOrWhiteSpace(line))
@@ -113,7 +111,8 @@ namespace Emby.Server.Implementations.Localization
         {
             List<CultureDto> list = new List<CultureDto>();
 
-            await using var stream = _assembly.GetManifestResourceStream(CulturesPath);
+            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))
             {
@@ -162,7 +161,7 @@ namespace Emby.Server.Implementations.Localization
         }
 
         /// <inheritdoc />
-        public CultureDto FindLanguageInfo(string language)
+        public CultureDto? FindLanguageInfo(string language)
         {
             // TODO language should ideally be a ReadOnlySpan but moq cannot mock ref structs
             for (var i = 0; i < _cultures.Count; i++)
@@ -183,9 +182,10 @@ namespace Emby.Server.Implementations.Localization
         /// <inheritdoc />
         public IEnumerable<CountryInfo> GetCountries()
         {
-            using StreamReader reader = new StreamReader(_assembly.GetManifestResourceStream(CountriesPath));
-
-            return JsonSerializer.Deserialize<IEnumerable<CountryInfo>>(reader.ReadToEnd(), _jsonOptions);
+            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}'");
         }
 
         /// <inheritdoc />
@@ -205,7 +205,9 @@ namespace Emby.Server.Implementations.Localization
                 countryCode = "us";
             }
 
-            return GetRatings(countryCode) ?? GetRatings("us");
+            return GetRatings(countryCode)
+                ?? GetRatings("us")
+                ?? throw new InvalidOperationException($"Invalid resource path: '{CountriesPath}'");
         }
 
         /// <summary>
@@ -213,7 +215,7 @@ namespace Emby.Server.Implementations.Localization
         /// </summary>
         /// <param name="countryCode">The country code.</param>
         /// <returns>The ratings.</returns>
-        private Dictionary<string, ParentalRating> GetRatings(string countryCode)
+        private Dictionary<string, ParentalRating>? GetRatings(string countryCode)
         {
             _allParentalRatings.TryGetValue(countryCode, out var value);
 
@@ -238,7 +240,7 @@ namespace Emby.Server.Implementations.Localization
 
             var ratingsDictionary = GetParentalRatingsDictionary();
 
-            if (ratingsDictionary.TryGetValue(rating, out ParentalRating value))
+            if (ratingsDictionary.TryGetValue(rating, out ParentalRating? value))
             {
                 return value.Value;
             }
@@ -268,20 +270,6 @@ namespace Emby.Server.Implementations.Localization
             return null;
         }
 
-        /// <inheritdoc />
-        public bool HasUnicodeCategory(string value, UnicodeCategory category)
-        {
-            foreach (var chr in value)
-            {
-                if (char.GetUnicodeCategory(chr) == category)
-                {
-                    return true;
-                }
-            }
-
-            return false;
-        }
-
         /// <inheritdoc />
         public string GetLocalizedString(string phrase)
         {
@@ -347,18 +335,21 @@ namespace Emby.Server.Implementations.Localization
         {
             await 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 != null)
+            if (stream == null)
             {
-                var dict = await JsonSerializer.DeserializeAsync<Dictionary<string, string>>(stream, _jsonOptions).ConfigureAwait(false);
+                _logger.LogError("Missing translation/culture resource: {ResourcePath}", resourcePath);
+                return;
+            }
 
-                foreach (var key in dict.Keys)
-                {
-                    dictionary[key] = dict[key];
-                }
+            var dict = await JsonSerializer.DeserializeAsync<Dictionary<string, string>>(stream, _jsonOptions).ConfigureAwait(false);
+            if (dict == null)
+            {
+                throw new InvalidOperationException($"Resource contains invalid data: '{stream}'");
             }
-            else
+
+            foreach (var key in dict.Keys)
             {
-                _logger.LogError("Missing translation/culture resource: {ResourcePath}", resourcePath);
+                dictionary[key] = dict[key];
             }
         }
 

+ 5 - 0
Emby.Server.Implementations/Playlists/ManualPlaylistsFolder.cs

@@ -49,5 +49,10 @@ namespace Emby.Server.Implementations.Playlists
             query.Parent = null;
             return LibraryManager.GetItemsResult(query);
         }
+
+        public override string GetClientTypeName()
+        {
+            return "ManualPlaylistsFolder";
+        }
     }
 }

Деякі файли не було показано, через те що забагато файлів було змінено