Browse Source

Merge remote-tracking branch 'upstream/master' into client-logger

Cody Robibero 3 years ago
parent
commit
d3d9311f48
27 changed files with 237 additions and 356 deletions
  1. 4 2
      .github/workflows/openapi.yml
  2. 8 8
      Emby.Server.Implementations/Localization/Core/eo.json
  3. 1 1
      Emby.Server.Implementations/Localization/Core/ru.json
  4. 1 1
      Emby.Server.Implementations/Localization/Core/sk.json
  5. 1 1
      Emby.Server.Implementations/Localization/Core/tr.json
  6. 35 71
      MediaBrowser.Controller/Entities/BaseItem.cs
  7. 0 9
      MediaBrowser.Controller/Entities/IHasScreenshots.cs
  8. 0 10
      MediaBrowser.LocalMetadata/Images/LocalImageProvider.cs
  9. 2 7
      MediaBrowser.MediaEncoding/Subtitles/VttWriter.cs
  10. 0 11
      MediaBrowser.Providers/Manager/ItemImageProvider.cs
  11. 5 34
      MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetImageProvider.cs
  12. 5 0
      MediaBrowser.Providers/Plugins/Tmdb/Configuration/PluginConfiguration.cs
  13. 21 0
      MediaBrowser.Providers/Plugins/Tmdb/Configuration/config.html
  14. 5 34
      MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieImageProvider.cs
  15. 1 2
      MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs
  16. 3 14
      MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonImageProvider.cs
  17. 3 18
      MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeImageProvider.cs
  18. 2 2
      MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs
  19. 3 18
      MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonImageProvider.cs
  20. 1 1
      MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs
  21. 3 35
      MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesImageProvider.cs
  22. 1 1
      MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs
  23. 80 29
      MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs
  24. 0 5
      MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs
  25. 5 1
      MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs
  26. 34 41
      tests/Jellyfin.Providers.Tests/Manager/ItemImageProviderTests.cs
  27. 13 0
      tests/Jellyfin.Providers.Tests/Tmdb/TmdbUtilsTests.cs

+ 4 - 2
.github/workflows/openapi.yml

@@ -3,7 +3,7 @@ on:
   push:
   push:
     branches:
     branches:
       - master
       - master
-  pull_request:
+  pull_request_target:
 
 
 jobs:
 jobs:
   openapi-head:
   openapi-head:
@@ -12,6 +12,8 @@ jobs:
     steps:
     steps:
       - name: Checkout repository
       - name: Checkout repository
         uses: actions/checkout@v2
         uses: actions/checkout@v2
+        with:
+          ref: ${{ github.head_ref }}
       - name: Setup .NET Core
       - name: Setup .NET Core
         uses: actions/setup-dotnet@v1
         uses: actions/setup-dotnet@v1
         with:
         with:
@@ -53,7 +55,7 @@ jobs:
 
 
   openapi-diff:
   openapi-diff:
     name: OpenAPI - Difference
     name: OpenAPI - Difference
-    if: ${{ github.event_name == 'pull_request' }}
+    if: ${{ github.event_name == 'pull_request_target' }}
     runs-on: ubuntu-latest
     runs-on: ubuntu-latest
     needs:
     needs:
       - openapi-head
       - openapi-head

+ 8 - 8
Emby.Server.Implementations/Localization/Core/eo.json

@@ -1,5 +1,5 @@
 {
 {
-    "NotificationOptionInstallationFailed": "Instalada fiasko",
+    "NotificationOptionInstallationFailed": "Instalada malsukceso",
     "NotificationOptionAudioPlaybackStopped": "Ludado de sono haltis",
     "NotificationOptionAudioPlaybackStopped": "Ludado de sono haltis",
     "NotificationOptionAudioPlayback": "Ludado de sono lanĉis",
     "NotificationOptionAudioPlayback": "Ludado de sono lanĉis",
     "NameSeasonUnknown": "Sezono Nekonata",
     "NameSeasonUnknown": "Sezono Nekonata",
@@ -48,17 +48,17 @@
     "Shows": "Serioj",
     "Shows": "Serioj",
     "HeaderFavoriteShows": "Favorataj Serioj",
     "HeaderFavoriteShows": "Favorataj Serioj",
     "TvShows": "TV-serioj",
     "TvShows": "TV-serioj",
-    "Favorites": "Favoratoj",
+    "Favorites": "Favorataj",
     "TaskCleanLogs": "Purigi Ĵurnalan Katalogon",
     "TaskCleanLogs": "Purigi Ĵurnalan Katalogon",
-    "TaskRefreshLibrary": "Skanu Plurmeditekon",
+    "TaskRefreshLibrary": "Skani Plurmeditekon",
     "ValueSpecialEpisodeName": "Speciala - {0}",
     "ValueSpecialEpisodeName": "Speciala - {0}",
-    "TaskOptimizeDatabase": "Optimigi datumbazon",
+    "TaskOptimizeDatabase": "Optimumigi datenbazon",
     "TaskRefreshChannels": "Refreŝigi Kanalojn",
     "TaskRefreshChannels": "Refreŝigi Kanalojn",
     "TaskUpdatePlugins": "Ĝisdatigi Kromprogramojn",
     "TaskUpdatePlugins": "Ĝisdatigi Kromprogramojn",
     "TaskRefreshPeople": "Refreŝigi Homojn",
     "TaskRefreshPeople": "Refreŝigi Homojn",
     "TasksChannelsCategory": "Interretaj Kanaloj",
     "TasksChannelsCategory": "Interretaj Kanaloj",
     "ProviderValue": "Provizanto: {0}",
     "ProviderValue": "Provizanto: {0}",
-    "NotificationOptionPluginError": "Kromprograma malsukceso",
+    "NotificationOptionPluginError": "Kromprogramo malsukcesis",
     "MixedContent": "Miksita enhavo",
     "MixedContent": "Miksita enhavo",
     "TasksApplicationCategory": "Aplikaĵo",
     "TasksApplicationCategory": "Aplikaĵo",
     "TasksMaintenanceCategory": "Prizorgado",
     "TasksMaintenanceCategory": "Prizorgado",
@@ -75,7 +75,7 @@
     "ServerNameNeedsToBeRestarted": "{0} devas esti relanĉita",
     "ServerNameNeedsToBeRestarted": "{0} devas esti relanĉita",
     "NotificationOptionVideoPlayback": "La videoludado lanĉis",
     "NotificationOptionVideoPlayback": "La videoludado lanĉis",
     "NotificationOptionServerRestartRequired": "Servila relanĉigo bezonata",
     "NotificationOptionServerRestartRequired": "Servila relanĉigo bezonata",
-    "TaskOptimizeDatabaseDescription": "Kompaktigas datenbazon kaj trunkas liberan lokon. Lanĉi ĉi tiun taskon post la teka skanado aŭ fari aliajn ŝanĝojn, kiuj implicas datenbazajn modifojn, povus plibonigi rendimenton.",
+    "TaskOptimizeDatabaseDescription": "Kompaktigas datenbazon kaj trunkas liberan lokon. Lanĉi ĉi tiun taskon post la plurmediteka skanado aŭ fari aliajn ŝanĝojn, kiuj implicas datenbazajn modifojn, povus plibonigi rendimenton.",
     "TaskUpdatePluginsDescription": "Elŝutas kaj instalas ĝisdatigojn por kromprogramojn, kiuj estas agorditaj por ĝisdatigi aŭtomate.",
     "TaskUpdatePluginsDescription": "Elŝutas kaj instalas ĝisdatigojn por kromprogramojn, kiuj estas agorditaj por ĝisdatigi aŭtomate.",
     "TaskDownloadMissingSubtitlesDescription": "Serĉas en interreto mankantajn subtekstojn surbaze de metadatena agordaro.",
     "TaskDownloadMissingSubtitlesDescription": "Serĉas en interreto mankantajn subtekstojn surbaze de metadatena agordaro.",
     "TaskRefreshPeopleDescription": "Ĝisdatigas metadatenojn por aktoroj kaj reĵisoroj en via plurmediteko.",
     "TaskRefreshPeopleDescription": "Ĝisdatigas metadatenojn por aktoroj kaj reĵisoroj en via plurmediteko.",
@@ -102,9 +102,9 @@
     "MessageApplicationUpdatedTo": "Jellyfin Server estis ĝisdatigita al {0}",
     "MessageApplicationUpdatedTo": "Jellyfin Server estis ĝisdatigita al {0}",
     "MessageApplicationUpdated": "Jellyfin Server estis ĝisdatigita",
     "MessageApplicationUpdated": "Jellyfin Server estis ĝisdatigita",
     "TaskRefreshChannelsDescription": "Refreŝigas informon pri interretaj kanaloj.",
     "TaskRefreshChannelsDescription": "Refreŝigas informon pri interretaj kanaloj.",
-    "TaskDownloadMissingSubtitles": "Elŝutu mankantajn subtekstojn",
+    "TaskDownloadMissingSubtitles": "Elŝuti mankantajn subtekstojn",
     "TaskCleanTranscode": "Malplenigi Transkodadan Katalogon",
     "TaskCleanTranscode": "Malplenigi Transkodadan Katalogon",
-    "TaskRefreshChapterImages": "Eltiru Ĉapitro-Bildojn",
+    "TaskRefreshChapterImages": "Eltiri Ĉapitraj Bildojn",
     "TaskCleanCache": "Malplenigi Staplan Katalogon",
     "TaskCleanCache": "Malplenigi Staplan Katalogon",
     "TaskCleanActivityLog": "Malplenigi Aktivecan Ĵurnalon",
     "TaskCleanActivityLog": "Malplenigi Aktivecan Ĵurnalon",
     "PluginUpdatedWithName": "{0} estis ĝisdatigita",
     "PluginUpdatedWithName": "{0} estis ĝisdatigita",

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

@@ -119,6 +119,6 @@
     "Undefined": "Не определено",
     "Undefined": "Не определено",
     "Forced": "Форсир-ые",
     "Forced": "Форсир-ые",
     "Default": "По умолчанию",
     "Default": "По умолчанию",
-    "TaskOptimizeDatabaseDescription": "Сжимает базу данных и обрезает свободное место. Выполнение этой задачи после сканирования библиотеки или внесения других изменений, предполагающих модификации базы данных, может повысить производительность.",
+    "TaskOptimizeDatabaseDescription": "Сжимает базу данных и вырезает свободные места. Выполнение этой задачи после сканирования библиотеки или внесения других изменений, предполагающих модификации базы данных, может повысить производительность.",
     "TaskOptimizeDatabase": "Оптимизировать базу данных"
     "TaskOptimizeDatabase": "Оптимизировать базу данных"
 }
 }

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

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

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

@@ -8,7 +8,7 @@
     "CameraImageUploadedFrom": "{0} 'den yeni bir kamera resmi yüklendi",
     "CameraImageUploadedFrom": "{0} 'den yeni bir kamera resmi yüklendi",
     "Channels": "Kanallar",
     "Channels": "Kanallar",
     "ChapterNameValue": "Bölüm {0}",
     "ChapterNameValue": "Bölüm {0}",
-    "Collections": "Koleksiyon",
+    "Collections": "Koleksiyonlar",
     "DeviceOfflineWithName": "{0} bağlantısı kesildi",
     "DeviceOfflineWithName": "{0} bağlantısı kesildi",
     "DeviceOnlineWithName": "{0} bağlı",
     "DeviceOnlineWithName": "{0} bağlı",
     "FailedLoginAttemptWithUserName": "{0} adresinden giriş başarısız oldu",
     "FailedLoginAttemptWithUserName": "{0} adresinden giriş başarısız oldu",

+ 35 - 71
MediaBrowser.Controller/Entities/BaseItem.cs

@@ -84,8 +84,6 @@ namespace MediaBrowser.Controller.Entities
             Model.Entities.ExtraType.Scene
             Model.Entities.ExtraType.Scene
         };
         };
 
 
-        public static readonly char[] SlugReplaceChars = { '?', '/', '&' };
-
         /// <summary>
         /// <summary>
         /// The supported extra folder names and types. See <see cref="Emby.Naming.Common.NamingOptions" />.
         /// The supported extra folder names and types. See <see cref="Emby.Naming.Common.NamingOptions" />.
         /// </summary>
         /// </summary>
@@ -354,11 +352,6 @@ namespace MediaBrowser.Controller.Entities
         {
         {
             get
             get
             {
             {
-                // if (IsOffline)
-                // {
-                //    return LocationType.Offline;
-                // }
-
                 var path = Path;
                 var path = Path;
                 if (string.IsNullOrEmpty(path))
                 if (string.IsNullOrEmpty(path))
                 {
                 {
@@ -391,7 +384,7 @@ namespace MediaBrowser.Controller.Entities
         }
         }
 
 
         [JsonIgnore]
         [JsonIgnore]
-        public bool IsFileProtocol => IsPathProtocol(MediaProtocol.File);
+        public bool IsFileProtocol => PathProtocol == MediaProtocol.File;
 
 
         [JsonIgnore]
         [JsonIgnore]
         public bool HasPathProtocol => PathProtocol.HasValue;
         public bool HasPathProtocol => PathProtocol.HasValue;
@@ -583,14 +576,7 @@ namespace MediaBrowser.Controller.Entities
         }
         }
 
 
         [JsonIgnore]
         [JsonIgnore]
-        public virtual Guid DisplayParentId
-        {
-            get
-            {
-                var parentId = ParentId;
-                return parentId;
-            }
-        }
+        public virtual Guid DisplayParentId => ParentId;
 
 
         [JsonIgnore]
         [JsonIgnore]
         public BaseItem DisplayParent
         public BaseItem DisplayParent
@@ -853,13 +839,6 @@ namespace MediaBrowser.Controller.Entities
             return Id.ToString("N", CultureInfo.InvariantCulture);
             return Id.ToString("N", CultureInfo.InvariantCulture);
         }
         }
 
 
-        public bool IsPathProtocol(MediaProtocol protocol)
-        {
-            var current = PathProtocol;
-
-            return current.HasValue && current.Value == protocol;
-        }
-
         private List<Tuple<StringBuilder, bool>> GetSortChunks(string s1)
         private List<Tuple<StringBuilder, bool>> GetSortChunks(string s1)
         {
         {
             var list = new List<Tuple<StringBuilder, bool>>();
             var list = new List<Tuple<StringBuilder, bool>>();
@@ -987,7 +966,7 @@ namespace MediaBrowser.Controller.Entities
 
 
             ReadOnlySpan<char> idString = Id.ToString("N", CultureInfo.InvariantCulture);
             ReadOnlySpan<char> idString = Id.ToString("N", CultureInfo.InvariantCulture);
 
 
-            return System.IO.Path.Join(basePath, "library", idString.Slice(0, 2), idString);
+            return System.IO.Path.Join(basePath, "library", idString[..2], idString);
         }
         }
 
 
         /// <summary>
         /// <summary>
@@ -1302,8 +1281,7 @@ namespace MediaBrowser.Controller.Entities
                 terms.Add(item.Name);
                 terms.Add(item.Name);
             }
             }
 
 
-            var video = item as Video;
-            if (video != null)
+            if (item is Video video)
             {
             {
                 if (video.Video3DFormat.HasValue)
                 if (video.Video3DFormat.HasValue)
                 {
                 {
@@ -1338,7 +1316,7 @@ namespace MediaBrowser.Controller.Entities
                 }
                 }
             }
             }
 
 
-            return string.Join('/', terms.ToArray());
+            return string.Join('/', terms);
         }
         }
 
 
         /// <summary>
         /// <summary>
@@ -1361,9 +1339,7 @@ namespace MediaBrowser.Controller.Entities
                 .Select(audio =>
                 .Select(audio =>
                 {
                 {
                     // Try to retrieve it from the db. If we don't find it, use the resolved version
                     // Try to retrieve it from the db. If we don't find it, use the resolved version
-                    var dbItem = LibraryManager.GetItemById(audio.Id) as Audio.Audio;
-
-                    if (dbItem != null)
+                    if (LibraryManager.GetItemById(audio.Id) is Audio.Audio dbItem)
                     {
                     {
                         audio = dbItem;
                         audio = dbItem;
                     }
                     }
@@ -1570,8 +1546,7 @@ namespace MediaBrowser.Controller.Entities
                     }
                     }
                 }
                 }
 
 
-                var hasTrailers = this as IHasTrailers;
-                if (hasTrailers != null)
+                if (this is IHasTrailers hasTrailers)
                 {
                 {
                     localTrailersChanged = await RefreshLocalTrailers(hasTrailers, options, fileSystemChildren, cancellationToken).ConfigureAwait(false);
                     localTrailersChanged = await RefreshLocalTrailers(hasTrailers, options, fileSystemChildren, cancellationToken).ConfigureAwait(false);
                 }
                 }
@@ -2268,7 +2243,11 @@ namespace MediaBrowser.Controller.Entities
 
 
             var existingImage = GetImageInfo(image.Type, index);
             var existingImage = GetImageInfo(image.Type, index);
 
 
-            if (existingImage != null)
+            if (existingImage == null)
+            {
+                AddImage(image);
+            }
+            else
             {
             {
                 existingImage.Path = image.Path;
                 existingImage.Path = image.Path;
                 existingImage.DateModified = image.DateModified;
                 existingImage.DateModified = image.DateModified;
@@ -2276,15 +2255,6 @@ namespace MediaBrowser.Controller.Entities
                 existingImage.Height = image.Height;
                 existingImage.Height = image.Height;
                 existingImage.BlurHash = image.BlurHash;
                 existingImage.BlurHash = image.BlurHash;
             }
             }
-            else
-            {
-                var current = ImageInfos;
-                var currentCount = current.Length;
-                var newArr = new ItemImageInfo[currentCount + 1];
-                current.CopyTo(newArr, 0);
-                newArr[currentCount] = image;
-                ImageInfos = newArr;
-            }
         }
         }
 
 
         public void SetImagePath(ImageType type, int index, FileSystemMetadata file)
         public void SetImagePath(ImageType type, int index, FileSystemMetadata file)
@@ -2298,7 +2268,7 @@ namespace MediaBrowser.Controller.Entities
 
 
             if (image == null)
             if (image == null)
             {
             {
-                ImageInfos = ImageInfos.Concat(new[] { GetImageInfo(file, type) }).ToArray();
+                AddImage(GetImageInfo(file, type));
             }
             }
             else
             else
             {
             {
@@ -2342,7 +2312,7 @@ namespace MediaBrowser.Controller.Entities
 
 
         public void RemoveImage(ItemImageInfo image)
         public void RemoveImage(ItemImageInfo image)
         {
         {
-            RemoveImages(new List<ItemImageInfo> { image });
+            RemoveImages(new[] { image });
         }
         }
 
 
         public void RemoveImages(IEnumerable<ItemImageInfo> deletedImages)
         public void RemoveImages(IEnumerable<ItemImageInfo> deletedImages)
@@ -2350,6 +2320,16 @@ namespace MediaBrowser.Controller.Entities
             ImageInfos = ImageInfos.Except(deletedImages).ToArray();
             ImageInfos = ImageInfos.Except(deletedImages).ToArray();
         }
         }
 
 
+        public void AddImage(ItemImageInfo image)
+        {
+            var current = ImageInfos;
+            var currentCount = current.Length;
+            var newArr = new ItemImageInfo[currentCount + 1];
+            current.CopyTo(newArr, 0);
+            newArr[currentCount] = image;
+            ImageInfos = newArr;
+        }
+
         public virtual Task UpdateToRepositoryAsync(ItemUpdateType updateReason, CancellationToken cancellationToken)
         public virtual Task UpdateToRepositoryAsync(ItemUpdateType updateReason, CancellationToken cancellationToken)
          => LibraryManager.UpdateItemAsync(this, GetParent(), updateReason, cancellationToken);
          => LibraryManager.UpdateItemAsync(this, GetParent(), updateReason, cancellationToken);
 
 
@@ -2373,7 +2353,7 @@ namespace MediaBrowser.Controller.Entities
 
 
             if (deletedImages.Count > 0)
             if (deletedImages.Count > 0)
             {
             {
-                ImageInfos = ImageInfos.Except(deletedImages).ToArray();
+                RemoveImages(deletedImages);
             }
             }
 
 
             return deletedImages.Count > 0;
             return deletedImages.Count > 0;
@@ -2715,7 +2695,7 @@ namespace MediaBrowser.Controller.Entities
 
 
         protected static string GetMappedPath(BaseItem item, string path, MediaProtocol? protocol)
         protected static string GetMappedPath(BaseItem item, string path, MediaProtocol? protocol)
         {
         {
-            if (protocol.HasValue && protocol.Value == MediaProtocol.File)
+            if (protocol == MediaProtocol.File)
             {
             {
                 return LibraryManager.GetPathAfterNetworkSubstitution(path, item);
                 return LibraryManager.GetPathAfterNetworkSubstitution(path, item);
             }
             }
@@ -2743,8 +2723,10 @@ namespace MediaBrowser.Controller.Entities
 
 
         protected Task RefreshMetadataForOwnedItem(BaseItem ownedItem, bool copyTitleMetadata, MetadataRefreshOptions options, CancellationToken cancellationToken)
         protected Task RefreshMetadataForOwnedItem(BaseItem ownedItem, bool copyTitleMetadata, MetadataRefreshOptions options, CancellationToken cancellationToken)
         {
         {
-            var newOptions = new MetadataRefreshOptions(options);
-            newOptions.SearchResult = null;
+            var newOptions = new MetadataRefreshOptions(options)
+            {
+                SearchResult = null
+            };
 
 
             var item = this;
             var item = this;
 
 
@@ -2805,8 +2787,10 @@ namespace MediaBrowser.Controller.Entities
 
 
         protected Task RefreshMetadataForOwnedVideo(MetadataRefreshOptions options, bool copyTitleMetadata, string path, CancellationToken cancellationToken)
         protected Task RefreshMetadataForOwnedVideo(MetadataRefreshOptions options, bool copyTitleMetadata, string path, CancellationToken cancellationToken)
         {
         {
-            var newOptions = new MetadataRefreshOptions(options);
-            newOptions.SearchResult = null;
+            var newOptions = new MetadataRefreshOptions(options)
+            {
+                SearchResult = null
+            };
 
 
             var id = LibraryManager.GetNewItemId(path, typeof(Video));
             var id = LibraryManager.GetNewItemId(path, typeof(Video));
 
 
@@ -2820,14 +2804,6 @@ namespace MediaBrowser.Controller.Entities
                 newOptions.ForceSave = true;
                 newOptions.ForceSave = true;
             }
             }
 
 
-            // var parentId = Id;
-            // if (!video.IsOwnedItem || video.ParentId != parentId)
-            // {
-            //    video.IsOwnedItem = true;
-            //    video.ParentId = parentId;
-            //    newOptions.ForceSave = true;
-            // }
-
             if (video == null)
             if (video == null)
             {
             {
                 return Task.FromResult(true);
                 return Task.FromResult(true);
@@ -2911,7 +2887,7 @@ namespace MediaBrowser.Controller.Entities
                 .Select(i => i.OfficialRating)
                 .Select(i => i.OfficialRating)
                 .Where(i => !string.IsNullOrEmpty(i))
                 .Where(i => !string.IsNullOrEmpty(i))
                 .Distinct(StringComparer.OrdinalIgnoreCase)
                 .Distinct(StringComparer.OrdinalIgnoreCase)
-                .Select(i => new Tuple<string, int?>(i, LocalizationManager.GetRatingLevel(i)))
+                .Select(i => (i, LocalizationManager.GetRatingLevel(i)))
                 .OrderBy(i => i.Item2 ?? 1000)
                 .OrderBy(i => i.Item2 ?? 1000)
                 .Select(i => i.Item1);
                 .Select(i => i.Item1);
 
 
@@ -2958,18 +2934,6 @@ namespace MediaBrowser.Controller.Entities
                 .Where(i => i.ExtraType.HasValue && extraTypes.Contains(i.ExtraType.Value));
                 .Where(i => i.ExtraType.HasValue && extraTypes.Contains(i.ExtraType.Value));
         }
         }
 
 
-        public IEnumerable<BaseItem> GetTrailers()
-        {
-            if (this is IHasTrailers)
-            {
-                return ((IHasTrailers)this).LocalTrailerIds.Select(LibraryManager.GetItemById).Where(i => i != null).OrderBy(i => i.SortName);
-            }
-            else
-            {
-                return Array.Empty<BaseItem>();
-            }
-        }
-
         public virtual long GetRunTimeTicksForPlayState()
         public virtual long GetRunTimeTicksForPlayState()
         {
         {
             return RunTimeTicks ?? 0;
             return RunTimeTicks ?? 0;

+ 0 - 9
MediaBrowser.Controller/Entities/IHasScreenshots.cs

@@ -1,9 +0,0 @@
-namespace MediaBrowser.Controller.Entities
-{
-    /// <summary>
-    /// The item has screenshots.
-    /// </summary>
-    public interface IHasScreenshots
-    {
-    }
-}

+ 0 - 10
MediaBrowser.LocalMetadata/Images/LocalImageProvider.cs

@@ -256,11 +256,6 @@ namespace MediaBrowser.LocalMetadata.Images
             {
             {
                 PopulateBackdrops(item, images, files, imagePrefix, isInMixedFolder);
                 PopulateBackdrops(item, images, files, imagePrefix, isInMixedFolder);
             }
             }
-
-            if (item is IHasScreenshots)
-            {
-                PopulateScreenshots(images, files, imagePrefix, isInMixedFolder);
-            }
         }
         }
 
 
         private void PopulatePrimaryImages(BaseItem item, List<LocalImageInfo> images, List<FileSystemMetadata> files, string imagePrefix, bool isInMixedFolder)
         private void PopulatePrimaryImages(BaseItem item, List<LocalImageInfo> images, List<FileSystemMetadata> files, string imagePrefix, bool isInMixedFolder)
@@ -363,11 +358,6 @@ namespace MediaBrowser.LocalMetadata.Images
             }));
             }));
         }
         }
 
 
-        private void PopulateScreenshots(List<LocalImageInfo> images, List<FileSystemMetadata> files, string imagePrefix, bool isInMixedFolder)
-        {
-            PopulateBackdrops(images, files, imagePrefix, "screenshot", "screenshot", isInMixedFolder, ImageType.Screenshot);
-        }
-
         private void PopulateBackdrops(List<LocalImageInfo> images, List<FileSystemMetadata> files, string imagePrefix, string firstFileName, string subsequentFileNamePrefix, bool isInMixedFolder, ImageType type)
         private void PopulateBackdrops(List<LocalImageInfo> images, List<FileSystemMetadata> files, string imagePrefix, string firstFileName, string subsequentFileNamePrefix, bool isInMixedFolder, ImageType type)
         {
         {
             AddImage(files, images, imagePrefix + firstFileName, type);
             AddImage(files, images, imagePrefix + firstFileName, type);

+ 2 - 7
MediaBrowser.MediaEncoding/Subtitles/VttWriter.cs

@@ -19,12 +19,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
             {
             {
                 writer.WriteLine("WEBVTT");
                 writer.WriteLine("WEBVTT");
                 writer.WriteLine();
                 writer.WriteLine();
-                writer.WriteLine("REGION");
-                writer.WriteLine("id:subtitle");
-                writer.WriteLine("width:80%");
-                writer.WriteLine("lines:3");
-                writer.WriteLine("regionanchor:50%,100%");
-                writer.WriteLine("viewportanchor:50%,90%");
+                writer.WriteLine("Region: id:subtitle width:80% lines:3 regionanchor:50%,100% viewportanchor:50%,90%");
                 writer.WriteLine();
                 writer.WriteLine();
                 foreach (var trackEvent in info.TrackEvents)
                 foreach (var trackEvent in info.TrackEvents)
                 {
                 {
@@ -39,7 +34,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
                         endTime = startTime.Add(TimeSpan.FromMilliseconds(1));
                         endTime = startTime.Add(TimeSpan.FromMilliseconds(1));
                     }
                     }
 
 
-                    writer.WriteLine(@"{0:hh\:mm\:ss\.fff} --> {1:hh\:mm\:ss\.fff} region:subtitle", startTime, endTime);
+                    writer.WriteLine(@"{0:hh\:mm\:ss\.fff} --> {1:hh\:mm\:ss\.fff} region:subtitle line:90%", startTime, endTime);
 
 
                     var text = trackEvent.Text;
                     var text = trackEvent.Text;
 
 

+ 0 - 11
MediaBrowser.Providers/Manager/ItemImageProvider.cs

@@ -343,12 +343,6 @@ namespace MediaBrowser.Providers.Manager
 
 
                 minWidth = savedOptions.GetMinWidth(ImageType.Backdrop);
                 minWidth = savedOptions.GetMinWidth(ImageType.Backdrop);
                 await DownloadMultiImages(item, ImageType.Backdrop, refreshOptions, backdropLimit, provider, result, list, minWidth, cancellationToken).ConfigureAwait(false);
                 await DownloadMultiImages(item, ImageType.Backdrop, refreshOptions, backdropLimit, provider, result, list, minWidth, cancellationToken).ConfigureAwait(false);
-
-                if (item is IHasScreenshots)
-                {
-                    minWidth = savedOptions.GetMinWidth(ImageType.Screenshot);
-                    await DownloadMultiImages(item, ImageType.Screenshot, refreshOptions, screenshotLimit, provider, result, list, minWidth, cancellationToken).ConfigureAwait(false);
-                }
             }
             }
             catch (OperationCanceledException)
             catch (OperationCanceledException)
             {
             {
@@ -438,11 +432,6 @@ namespace MediaBrowser.Providers.Manager
                 changed = true;
                 changed = true;
             }
             }
 
 
-            if (item is IHasScreenshots && UpdateMultiImages(item, images, ImageType.Screenshot))
-            {
-                changed = true;
-            }
-
             return changed;
             return changed;
         }
         }
 
 

+ 5 - 34
MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetImageProvider.cs

@@ -13,7 +13,6 @@ using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Movies;
 using MediaBrowser.Controller.Entities.Movies;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Controller.Providers;
-using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Providers;
 using MediaBrowser.Model.Providers;
 
 
@@ -67,40 +66,12 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.BoxSets
                 return Enumerable.Empty<RemoteImageInfo>();
                 return Enumerable.Empty<RemoteImageInfo>();
             }
             }
 
 
-            var remoteImages = new List<RemoteImageInfo>();
+            var posters = collection.Images.Posters;
+            var backdrops = collection.Images.Backdrops;
+            var remoteImages = new List<RemoteImageInfo>(posters.Count + backdrops.Count);
 
 
-            for (var i = 0; i < collection.Images.Posters.Count; i++)
-            {
-                var poster = collection.Images.Posters[i];
-                remoteImages.Add(new RemoteImageInfo
-                {
-                    Url = _tmdbClientManager.GetPosterUrl(poster.FilePath),
-                    CommunityRating = poster.VoteAverage,
-                    VoteCount = poster.VoteCount,
-                    Width = poster.Width,
-                    Height = poster.Height,
-                    Language = TmdbUtils.AdjustImageLanguage(poster.Iso_639_1, language),
-                    ProviderName = Name,
-                    Type = ImageType.Primary,
-                    RatingType = RatingType.Score
-                });
-            }
-
-            for (var i = 0; i < collection.Images.Backdrops.Count; i++)
-            {
-                var backdrop = collection.Images.Backdrops[i];
-                remoteImages.Add(new RemoteImageInfo
-                {
-                    Url = _tmdbClientManager.GetBackdropUrl(backdrop.FilePath),
-                    CommunityRating = backdrop.VoteAverage,
-                    VoteCount = backdrop.VoteCount,
-                    Width = backdrop.Width,
-                    Height = backdrop.Height,
-                    ProviderName = Name,
-                    Type = ImageType.Backdrop,
-                    RatingType = RatingType.Score
-                });
-            }
+            _tmdbClientManager.ConvertPostersToRemoteImageInfo(posters, language, remoteImages);
+            _tmdbClientManager.ConvertBackdropsToRemoteImageInfo(backdrops, language, remoteImages);
 
 
             return remoteImages;
             return remoteImages;
         }
         }

+ 5 - 0
MediaBrowser.Providers/Plugins/Tmdb/Configuration/PluginConfiguration.cs

@@ -21,5 +21,10 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
         /// Gets or sets a value indicating whether tags should be imported for movies from TMDb.
         /// Gets or sets a value indicating whether tags should be imported for movies from TMDb.
         /// </summary>
         /// </summary>
         public bool ExcludeTagsMovies { get; set; }
         public bool ExcludeTagsMovies { get; set; }
+
+        /// <summary>
+        /// Gets or sets a value indicating the maximum number of cast members to fetch for an item.
+        /// </summary>
+        public int MaxCastMembers { get; set; } = 15;
     }
     }
 }
 }

+ 21 - 0
MediaBrowser.Providers/Plugins/Tmdb/Configuration/config.html

@@ -12,6 +12,18 @@
                         <input is="emby-checkbox" type="checkbox" id="includeAdult" />
                         <input is="emby-checkbox" type="checkbox" id="includeAdult" />
                         <span>Include adult content in search results.</span>
                         <span>Include adult content in search results.</span>
                     </label>
                     </label>
+                    <label class="checkboxContainer">
+                        <input is="emby-checkbox" type="checkbox" id="excludeTagsSeries" />
+                        <span>Exclude tags/keywords from metadata fetched for series.</span>
+                    </label>
+                    <label class="checkboxContainer">
+                        <input is="emby-checkbox" type="checkbox" id="excludeTagsMovies" />
+                        <span>Exclude tags/keywords from metadata fetched for movies.</span>
+                    </label>
+                    <div class="inputContainer">
+                        <input is="emby-input" type="number" id="maxCastMembers" pattern="[0-9]*" required min="0" max="1000" label="Max Cast Members" />
+                        <div class="fieldDescription">The maximum number of cast members to fetch for an item.</div>
+                    </div>
                     <br />
                     <br />
                     <div>
                     <div>
                         <button is="emby-button" type="submit" class="raised button-submit block"><span>Save</span></button>
                         <button is="emby-button" type="submit" class="raised button-submit block"><span>Save</span></button>
@@ -31,6 +43,14 @@
                         document.querySelector('#includeAdult').checked = config.IncludeAdult;
                         document.querySelector('#includeAdult').checked = config.IncludeAdult;
                         document.querySelector('#excludeTagsSeries').checked = config.ExcludeTagsSeries;
                         document.querySelector('#excludeTagsSeries').checked = config.ExcludeTagsSeries;
                         document.querySelector('#excludeTagsMovies').checked = config.ExcludeTagsMovies;
                         document.querySelector('#excludeTagsMovies').checked = config.ExcludeTagsMovies;
+
+                        var maxCastMembers = document.querySelector('#maxCastMembers');
+                        maxCastMembers.value = config.MaxCastMembers;
+                        maxCastMembers.dispatchEvent(new Event('change', {
+                            bubbles: true,
+                            cancelable: false
+                        }));
+
                         Dashboard.hideLoadingMsg();
                         Dashboard.hideLoadingMsg();
                     });
                     });
                 });
                 });
@@ -44,6 +64,7 @@
                         config.IncludeAdult = document.querySelector('#includeAdult').checked;
                         config.IncludeAdult = document.querySelector('#includeAdult').checked;
                         config.ExcludeTagsSeries = document.querySelector('#excludeTagsSeries').checked;
                         config.ExcludeTagsSeries = document.querySelector('#excludeTagsSeries').checked;
                         config.ExcludeTagsMovies = document.querySelector('#excludeTagsMovies').checked;
                         config.ExcludeTagsMovies = document.querySelector('#excludeTagsMovies').checked;
+                        config.MaxCastMembers = document.querySelector('#maxCastMembers').value;
                         ApiClient.updatePluginConfiguration(PluginConfig.pluginId, config).then(Dashboard.processPluginConfigurationUpdateResult);
                         ApiClient.updatePluginConfiguration(PluginConfig.pluginId, config).then(Dashboard.processPluginConfigurationUpdateResult);
                     });
                     });
 
 

+ 5 - 34
MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieImageProvider.cs

@@ -13,7 +13,6 @@ using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Movies;
 using MediaBrowser.Controller.Entities.Movies;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Controller.Providers;
-using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Providers;
 using MediaBrowser.Model.Providers;
 using TMDbLib.Objects.Find;
 using TMDbLib.Objects.Find;
@@ -84,40 +83,12 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
                 return Enumerable.Empty<RemoteImageInfo>();
                 return Enumerable.Empty<RemoteImageInfo>();
             }
             }
 
 
-            var remoteImages = new List<RemoteImageInfo>();
+            var posters = movie.Images.Posters;
+            var backdrops = movie.Images.Backdrops;
+            var remoteImages = new List<RemoteImageInfo>(posters.Count + backdrops.Count);
 
 
-            for (var i = 0; i < movie.Images.Posters.Count; i++)
-            {
-                var poster = movie.Images.Posters[i];
-                remoteImages.Add(new RemoteImageInfo
-                {
-                    Url = _tmdbClientManager.GetPosterUrl(poster.FilePath),
-                    CommunityRating = poster.VoteAverage,
-                    VoteCount = poster.VoteCount,
-                    Width = poster.Width,
-                    Height = poster.Height,
-                    Language = TmdbUtils.AdjustImageLanguage(poster.Iso_639_1, language),
-                    ProviderName = Name,
-                    Type = ImageType.Primary,
-                    RatingType = RatingType.Score
-                });
-            }
-
-            for (var i = 0; i < movie.Images.Backdrops.Count; i++)
-            {
-                var backdrop = movie.Images.Backdrops[i];
-                remoteImages.Add(new RemoteImageInfo
-                {
-                    Url = _tmdbClientManager.GetPosterUrl(backdrop.FilePath),
-                    CommunityRating = backdrop.VoteAverage,
-                    VoteCount = backdrop.VoteCount,
-                    Width = backdrop.Width,
-                    Height = backdrop.Height,
-                    ProviderName = Name,
-                    Type = ImageType.Backdrop,
-                    RatingType = RatingType.Score
-                });
-            }
+            _tmdbClientManager.ConvertPostersToRemoteImageInfo(posters, language, remoteImages);
+            _tmdbClientManager.ConvertBackdropsToRemoteImageInfo(backdrops, language, remoteImages);
 
 
             return remoteImages;
             return remoteImages;
         }
         }

+ 1 - 2
MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs

@@ -241,8 +241,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
 
 
             if (movieResult.Credits?.Cast != null)
             if (movieResult.Credits?.Cast != null)
             {
             {
-                // TODO configurable
-                foreach (var actor in movieResult.Credits.Cast.OrderBy(a => a.Order).Take(TmdbUtils.MaxCastMembers))
+                foreach (var actor in movieResult.Credits.Cast.OrderBy(a => a.Order).Take(Plugin.Instance.Configuration.MaxCastMembers))
                 {
                 {
                     var personInfo = new PersonInfo
                     var personInfo = new PersonInfo
                     {
                     {

+ 3 - 14
MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonImageProvider.cs

@@ -60,21 +60,10 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People
                 return Enumerable.Empty<RemoteImageInfo>();
                 return Enumerable.Empty<RemoteImageInfo>();
             }
             }
 
 
-            var remoteImages = new RemoteImageInfo[personResult.Images.Profiles.Count];
+            var profiles = personResult.Images.Profiles;
+            var remoteImages = new List<RemoteImageInfo>(profiles.Count);
 
 
-            for (var i = 0; i < personResult.Images.Profiles.Count; i++)
-            {
-                var image = personResult.Images.Profiles[i];
-                remoteImages[i] = new RemoteImageInfo
-                {
-                    ProviderName = Name,
-                    Type = ImageType.Primary,
-                    Width = image.Width,
-                    Height = image.Height,
-                    Language = TmdbUtils.AdjustImageLanguage(image.Iso_639_1, language),
-                    Url = _tmdbClientManager.GetProfileUrl(image.FilePath)
-                };
-            }
+            _tmdbClientManager.ConvertProfilesToRemoteImageInfo(profiles, language, remoteImages);
 
 
             return remoteImages;
             return remoteImages;
         }
         }

+ 3 - 18
MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeImageProvider.cs

@@ -12,7 +12,6 @@ using System.Threading.Tasks;
 using MediaBrowser.Common.Net;
 using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Controller.Providers;
-using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Providers;
 using MediaBrowser.Model.Providers;
 
 
@@ -75,23 +74,9 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
                 return Enumerable.Empty<RemoteImageInfo>();
                 return Enumerable.Empty<RemoteImageInfo>();
             }
             }
 
 
-            var remoteImages = new RemoteImageInfo[stills.Count];
-            for (var i = 0; i < stills.Count; i++)
-            {
-                var image = stills[i];
-                remoteImages[i] = new RemoteImageInfo
-                {
-                    Url = _tmdbClientManager.GetStillUrl(image.FilePath),
-                    CommunityRating = image.VoteAverage,
-                    VoteCount = image.VoteCount,
-                    Width = image.Width,
-                    Height = image.Height,
-                    Language = TmdbUtils.AdjustImageLanguage(image.Iso_639_1, language),
-                    ProviderName = Name,
-                    Type = ImageType.Primary,
-                    RatingType = RatingType.Score
-                };
-            }
+            var remoteImages = new List<RemoteImageInfo>(stills.Count);
+
+            _tmdbClientManager.ConvertStillsToRemoteImageInfo(stills, language, remoteImages);
 
 
             return remoteImages;
             return remoteImages;
         }
         }

+ 2 - 2
MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs

@@ -154,7 +154,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
 
 
             if (credits?.Cast != null)
             if (credits?.Cast != null)
             {
             {
-                foreach (var actor in credits.Cast.OrderBy(a => a.Order).Take(TmdbUtils.MaxCastMembers))
+                foreach (var actor in credits.Cast.OrderBy(a => a.Order).Take(Plugin.Instance.Configuration.MaxCastMembers))
                 {
                 {
                     metadataResult.AddPerson(new PersonInfo
                     metadataResult.AddPerson(new PersonInfo
                     {
                     {
@@ -168,7 +168,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
 
 
             if (credits?.GuestStars != null)
             if (credits?.GuestStars != null)
             {
             {
-                foreach (var guest in credits.GuestStars.OrderBy(a => a.Order).Take(TmdbUtils.MaxCastMembers))
+                foreach (var guest in credits.GuestStars.OrderBy(a => a.Order).Take(Plugin.Instance.Configuration.MaxCastMembers))
                 {
                 {
                     metadataResult.AddPerson(new PersonInfo
                     metadataResult.AddPerson(new PersonInfo
                     {
                     {

+ 3 - 18
MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonImageProvider.cs

@@ -11,7 +11,6 @@ using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Controller.Providers;
-using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Providers;
 using MediaBrowser.Model.Providers;
 
 
@@ -62,23 +61,9 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
                 return Enumerable.Empty<RemoteImageInfo>();
                 return Enumerable.Empty<RemoteImageInfo>();
             }
             }
 
 
-            var remoteImages = new RemoteImageInfo[posters.Count];
-            for (var i = 0; i < posters.Count; i++)
-            {
-                var image = posters[i];
-                remoteImages[i] = new RemoteImageInfo
-                {
-                    Url = _tmdbClientManager.GetPosterUrl(image.FilePath),
-                    CommunityRating = image.VoteAverage,
-                    VoteCount = image.VoteCount,
-                    Width = image.Width,
-                    Height = image.Height,
-                    Language = TmdbUtils.AdjustImageLanguage(image.Iso_639_1, language),
-                    ProviderName = Name,
-                    Type = ImageType.Primary,
-                    RatingType = RatingType.Score
-                };
-            }
+            var remoteImages = new List<RemoteImageInfo>(posters.Count);
+
+            _tmdbClientManager.ConvertPostersToRemoteImageInfo(posters, language, remoteImages);
 
 
             return remoteImages;
             return remoteImages;
         }
         }

+ 1 - 1
MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs

@@ -67,7 +67,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
             var credits = seasonResult.Credits;
             var credits = seasonResult.Credits;
             if (credits?.Cast != null)
             if (credits?.Cast != null)
             {
             {
-                var cast = credits.Cast.OrderBy(c => c.Order).Take(TmdbUtils.MaxCastMembers).ToList();
+                var cast = credits.Cast.OrderBy(c => c.Order).Take(Plugin.Instance.Configuration.MaxCastMembers).ToList();
                 for (var i = 0; i < cast.Count; i++)
                 for (var i = 0; i < cast.Count; i++)
                 {
                 {
                     result.AddPerson(new PersonInfo
                     result.AddPerson(new PersonInfo

+ 3 - 35
MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesImageProvider.cs

@@ -11,7 +11,6 @@ using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Controller.Providers;
-using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Providers;
 using MediaBrowser.Model.Providers;
 
 
@@ -70,41 +69,10 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
 
 
             var posters = series.Images.Posters;
             var posters = series.Images.Posters;
             var backdrops = series.Images.Backdrops;
             var backdrops = series.Images.Backdrops;
+            var remoteImages = new List<RemoteImageInfo>(posters.Count + backdrops.Count);
 
 
-            var remoteImages = new RemoteImageInfo[posters.Count + backdrops.Count];
-
-            for (var i = 0; i < posters.Count; i++)
-            {
-                var poster = posters[i];
-                remoteImages[i] = new RemoteImageInfo
-                {
-                    Url = _tmdbClientManager.GetPosterUrl(poster.FilePath),
-                    CommunityRating = poster.VoteAverage,
-                    VoteCount = poster.VoteCount,
-                    Width = poster.Width,
-                    Height = poster.Height,
-                    Language = TmdbUtils.AdjustImageLanguage(poster.Iso_639_1, language),
-                    ProviderName = Name,
-                    Type = ImageType.Primary,
-                    RatingType = RatingType.Score
-                };
-            }
-
-            for (var i = 0; i < backdrops.Count; i++)
-            {
-                var backdrop = series.Images.Backdrops[i];
-                remoteImages[posters.Count + i] = new RemoteImageInfo
-                {
-                    Url = _tmdbClientManager.GetBackdropUrl(backdrop.FilePath),
-                    CommunityRating = backdrop.VoteAverage,
-                    VoteCount = backdrop.VoteCount,
-                    Width = backdrop.Width,
-                    Height = backdrop.Height,
-                    ProviderName = Name,
-                    Type = ImageType.Backdrop,
-                    RatingType = RatingType.Score
-                };
-            }
+            _tmdbClientManager.ConvertPostersToRemoteImageInfo(posters, language, remoteImages);
+            _tmdbClientManager.ConvertBackdropsToRemoteImageInfo(backdrops, language, remoteImages);
 
 
             return remoteImages;
             return remoteImages;
         }
         }

+ 1 - 1
MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs

@@ -331,7 +331,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
         {
         {
             if (seriesResult.Credits?.Cast != null)
             if (seriesResult.Credits?.Cast != null)
             {
             {
-                foreach (var actor in seriesResult.Credits.Cast.OrderBy(a => a.Order).Take(TmdbUtils.MaxCastMembers))
+                foreach (var actor in seriesResult.Credits.Cast.OrderBy(a => a.Order).Take(Plugin.Instance.Configuration.MaxCastMembers))
                 {
                 {
                     var personInfo = new PersonInfo
                     var personInfo = new PersonInfo
                     {
                     {

+ 80 - 29
MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs

@@ -1,10 +1,13 @@
-#nullable disable
+#nullable disable
 
 
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.Globalization;
 using System.Globalization;
 using System.Threading;
 using System.Threading;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Providers;
 using Microsoft.Extensions.Caching.Memory;
 using Microsoft.Extensions.Caching.Memory;
 using TMDbLib.Client;
 using TMDbLib.Client;
 using TMDbLib.Objects.Collections;
 using TMDbLib.Objects.Collections;
@@ -483,33 +486,29 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
         }
         }
 
 
         /// <summary>
         /// <summary>
-        /// Gets the absolute URL of the poster.
+        /// Handles bad path checking and builds the absolute url.
         /// </summary>
         /// </summary>
-        /// <param name="posterPath">The relative URL of the poster.</param>
+        /// <param name="size">The image size to fetch.</param>
+        /// <param name="path">The relative URL of the image.</param>
         /// <returns>The absolute URL.</returns>
         /// <returns>The absolute URL.</returns>
-        public string GetPosterUrl(string posterPath)
+        private string GetUrl(string size, string path)
         {
         {
-            if (string.IsNullOrEmpty(posterPath))
+            if (string.IsNullOrEmpty(path))
             {
             {
                 return null;
                 return null;
             }
             }
 
 
-            return _tmDbClient.GetImageUrl(_tmDbClient.Config.Images.PosterSizes[^1], posterPath).ToString();
+            return _tmDbClient.GetImageUrl(size, path).ToString();
         }
         }
 
 
         /// <summary>
         /// <summary>
-        /// Gets the absolute URL of the backdrop image.
+        /// Gets the absolute URL of the poster.
         /// </summary>
         /// </summary>
-        /// <param name="posterPath">The relative URL of the backdrop image.</param>
+        /// <param name="posterPath">The relative URL of the poster.</param>
         /// <returns>The absolute URL.</returns>
         /// <returns>The absolute URL.</returns>
-        public string GetBackdropUrl(string posterPath)
+        public string GetPosterUrl(string posterPath)
         {
         {
-            if (string.IsNullOrEmpty(posterPath))
-            {
-                return null;
-            }
-
-            return _tmDbClient.GetImageUrl(_tmDbClient.Config.Images.BackdropSizes[^1], posterPath).ToString();
+            return GetUrl(_tmDbClient.Config.Images.PosterSizes[^1], posterPath);
         }
         }
 
 
         /// <summary>
         /// <summary>
@@ -519,27 +518,79 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
         /// <returns>The absolute URL.</returns>
         /// <returns>The absolute URL.</returns>
         public string GetProfileUrl(string actorProfilePath)
         public string GetProfileUrl(string actorProfilePath)
         {
         {
-            if (string.IsNullOrEmpty(actorProfilePath))
-            {
-                return null;
-            }
+            return GetUrl(_tmDbClient.Config.Images.ProfileSizes[^1], actorProfilePath);
+        }
 
 
-            return _tmDbClient.GetImageUrl(_tmDbClient.Config.Images.ProfileSizes[^1], actorProfilePath).ToString();
+        /// <summary>
+        /// Converts poster <see cref="ImageData"/>s into <see cref="RemoteImageInfo"/>s.
+        /// </summary>
+        /// <param name="images">The input images.</param>
+        /// <param name="requestLanguage">The requested language.</param>
+        /// <param name="results">The collection to add the remote images into.</param>
+        public void ConvertPostersToRemoteImageInfo(List<ImageData> images, string requestLanguage, List<RemoteImageInfo> results)
+        {
+            ConvertToRemoteImageInfo(images, _tmDbClient.Config.Images.PosterSizes[^1], ImageType.Primary, requestLanguage, results);
         }
         }
 
 
         /// <summary>
         /// <summary>
-        /// Gets the absolute URL of the still image.
+        /// Converts backdrop <see cref="ImageData"/>s into <see cref="RemoteImageInfo"/>s.
         /// </summary>
         /// </summary>
-        /// <param name="filePath">The relative URL of the still image.</param>
-        /// <returns>The absolute URL.</returns>
-        public string GetStillUrl(string filePath)
+        /// <param name="images">The input images.</param>
+        /// <param name="requestLanguage">The requested language.</param>
+        /// <param name="results">The collection to add the remote images into.</param>
+        public void ConvertBackdropsToRemoteImageInfo(List<ImageData> images, string requestLanguage, List<RemoteImageInfo> results)
         {
         {
-            if (string.IsNullOrEmpty(filePath))
+            ConvertToRemoteImageInfo(images, _tmDbClient.Config.Images.BackdropSizes[^1], ImageType.Backdrop, requestLanguage, results);
+        }
+
+        /// <summary>
+        /// Converts profile <see cref="ImageData"/>s into <see cref="RemoteImageInfo"/>s.
+        /// </summary>
+        /// <param name="images">The input images.</param>
+        /// <param name="requestLanguage">The requested language.</param>
+        /// <param name="results">The collection to add the remote images into.</param>
+        public void ConvertProfilesToRemoteImageInfo(List<ImageData> images, string requestLanguage, List<RemoteImageInfo> results)
+        {
+            ConvertToRemoteImageInfo(images, _tmDbClient.Config.Images.ProfileSizes[^1], ImageType.Primary, requestLanguage, results);
+        }
+
+        /// <summary>
+        /// Converts still <see cref="ImageData"/>s into <see cref="RemoteImageInfo"/>s.
+        /// </summary>
+        /// <param name="images">The input images.</param>
+        /// <param name="requestLanguage">The requested language.</param>
+        /// <param name="results">The collection to add the remote images into.</param>
+        public void ConvertStillsToRemoteImageInfo(List<ImageData> images, string requestLanguage, List<RemoteImageInfo> results)
+        {
+            ConvertToRemoteImageInfo(images, _tmDbClient.Config.Images.StillSizes[^1], ImageType.Primary, requestLanguage, results);
+        }
+
+        /// <summary>
+        /// Converts <see cref="ImageData"/>s into <see cref="RemoteImageInfo"/>s.
+        /// </summary>
+        /// <param name="images">The input images.</param>
+        /// <param name="size">The size of the image to fetch.</param>
+        /// <param name="type">The type of the image.</param>
+        /// <param name="requestLanguage">The requested language.</param>
+        /// <param name="results">The collection to add the remote images into.</param>
+        private void ConvertToRemoteImageInfo(List<ImageData> images, string size, ImageType type, string requestLanguage, List<RemoteImageInfo> results)
+        {
+            for (var i = 0; i < images.Count; i++)
             {
             {
-                return null;
+                var image = images[i];
+                results.Add(new RemoteImageInfo
+                {
+                    Url = GetUrl(size, image.FilePath),
+                    CommunityRating = image.VoteAverage,
+                    VoteCount = image.VoteCount,
+                    Width = image.Width,
+                    Height = image.Height,
+                    Language = TmdbUtils.AdjustImageLanguage(image.Iso_639_1, requestLanguage),
+                    ProviderName = TmdbUtils.ProviderName,
+                    Type = type,
+                    RatingType = RatingType.Score
+                });
             }
             }
-
-            return _tmDbClient.GetImageUrl(_tmDbClient.Config.Images.StillSizes[^1], filePath).ToString();
         }
         }
 
 
         private Task EnsureClientConfigAsync()
         private Task EnsureClientConfigAsync()
@@ -554,7 +605,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
             GC.SuppressFinalize(this);
             GC.SuppressFinalize(this);
         }
         }
 
 
-/// <summary>
+        /// <summary>
         /// Releases unmanaged and - optionally - managed resources.
         /// Releases unmanaged and - optionally - managed resources.
         /// </summary>
         /// </summary>
         /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
         /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>

+ 0 - 5
MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs

@@ -28,11 +28,6 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
         /// </summary>
         /// </summary>
         public const string ApiKey = "4219e299c89411838049ab0dab19ebd5";
         public const string ApiKey = "4219e299c89411838049ab0dab19ebd5";
 
 
-        /// <summary>
-        /// Maximum number of cast members to pull.
-        /// </summary>
-        public const int MaxCastMembers = 15;
-
         /// <summary>
         /// <summary>
         /// The crew types to keep.
         /// The crew types to keep.
         /// </summary>
         /// </summary>

+ 5 - 1
MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs

@@ -785,7 +785,11 @@ namespace MediaBrowser.XbmcMetadata.Parsers
                 case "fanart":
                 case "fanart":
                     {
                     {
                         var subtree = reader.ReadSubtree();
                         var subtree = reader.ReadSubtree();
-                        subtree.ReadToDescendant("thumb");
+                        if (!subtree.ReadToDescendant("thumb"))
+                        {
+                            break;
+                        }
+
                         FetchThumbNode(subtree, itemResult);
                         FetchThumbNode(subtree, itemResult);
                         break;
                         break;
                     }
                     }

+ 34 - 41
tests/Jellyfin.Providers.Tests/Manager/ItemImageProviderTests.cs

@@ -10,7 +10,6 @@ using System.Text.RegularExpressions;
 using System.Threading;
 using System.Threading;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Entities.Movies;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Configuration;
 using MediaBrowser.Model.Configuration;
@@ -28,13 +27,13 @@ namespace Jellyfin.Providers.Tests.Manager
 {
 {
     public class ItemImageProviderTests
     public class ItemImageProviderTests
     {
     {
-        private static readonly string TestDataImagePath = "Test Data/Images/blank{0}.jpg";
+        private const string TestDataImagePath = "Test Data/Images/blank{0}.jpg";
 
 
         [Fact]
         [Fact]
         public void ValidateImages_PhotoEmptyProviders_NoChange()
         public void ValidateImages_PhotoEmptyProviders_NoChange()
         {
         {
             var itemImageProvider = GetItemImageProvider(null, null);
             var itemImageProvider = GetItemImageProvider(null, null);
-            var changed = itemImageProvider.ValidateImages(new Photo(), new List<ILocalImageProvider>(), null);
+            var changed = itemImageProvider.ValidateImages(new Photo(), Enumerable.Empty<ILocalImageProvider>(), null);
 
 
             Assert.False(changed);
             Assert.False(changed);
         }
         }
@@ -43,7 +42,7 @@ namespace Jellyfin.Providers.Tests.Manager
         public void ValidateImages_EmptyItemEmptyProviders_NoChange()
         public void ValidateImages_EmptyItemEmptyProviders_NoChange()
         {
         {
             var itemImageProvider = GetItemImageProvider(null, null);
             var itemImageProvider = GetItemImageProvider(null, null);
-            var changed = itemImageProvider.ValidateImages(new MovieWithScreenshots(), new List<ILocalImageProvider>(), null);
+            var changed = itemImageProvider.ValidateImages(new Video(), Enumerable.Empty<ILocalImageProvider>(), null);
 
 
             Assert.False(changed);
             Assert.False(changed);
         }
         }
@@ -55,8 +54,7 @@ namespace Jellyfin.Providers.Tests.Manager
                 // minimal test cases that hit different handling
                 // minimal test cases that hit different handling
                 { ImageType.Primary, 1 },
                 { ImageType.Primary, 1 },
                 { ImageType.Backdrop, 1 },
                 { ImageType.Backdrop, 1 },
-                { ImageType.Backdrop, 2 },
-                { ImageType.Screenshot, 1 }
+                { ImageType.Backdrop, 2 }
             };
             };
 
 
             return theoryTypes;
             return theoryTypes;
@@ -69,11 +67,11 @@ namespace Jellyfin.Providers.Tests.Manager
             // Has to exist for querying DateModified time on file, results stored but not checked so not populating
             // Has to exist for querying DateModified time on file, results stored but not checked so not populating
             BaseItem.FileSystem = Mock.Of<IFileSystem>();
             BaseItem.FileSystem = Mock.Of<IFileSystem>();
 
 
-            var item = new MovieWithScreenshots();
+            var item = new Video();
             var imageProvider = GetImageProvider(imageType, imageCount, true);
             var imageProvider = GetImageProvider(imageType, imageCount, true);
 
 
             var itemImageProvider = GetItemImageProvider(null, null);
             var itemImageProvider = GetItemImageProvider(null, null);
-            var changed = itemImageProvider.ValidateImages(item, new List<ILocalImageProvider> { imageProvider }, null);
+            var changed = itemImageProvider.ValidateImages(item, new[] { imageProvider }, null);
 
 
             Assert.True(changed);
             Assert.True(changed);
             Assert.Equal(imageCount, item.GetImages(imageType).Count());
             Assert.Equal(imageCount, item.GetImages(imageType).Count());
@@ -86,7 +84,7 @@ namespace Jellyfin.Providers.Tests.Manager
             var item = GetItemWithImages(imageType, imageCount, true);
             var item = GetItemWithImages(imageType, imageCount, true);
 
 
             var itemImageProvider = GetItemImageProvider(null, null);
             var itemImageProvider = GetItemImageProvider(null, null);
-            var changed = itemImageProvider.ValidateImages(item, new List<ILocalImageProvider>(), null);
+            var changed = itemImageProvider.ValidateImages(item, Enumerable.Empty<ILocalImageProvider>(), null);
 
 
             Assert.False(changed);
             Assert.False(changed);
             Assert.Equal(imageCount, item.GetImages(imageType).Count());
             Assert.Equal(imageCount, item.GetImages(imageType).Count());
@@ -99,7 +97,7 @@ namespace Jellyfin.Providers.Tests.Manager
             var item = GetItemWithImages(imageType, imageCount, false);
             var item = GetItemWithImages(imageType, imageCount, false);
 
 
             var itemImageProvider = GetItemImageProvider(null, null);
             var itemImageProvider = GetItemImageProvider(null, null);
-            var changed = itemImageProvider.ValidateImages(item, new List<ILocalImageProvider>(), null);
+            var changed = itemImageProvider.ValidateImages(item, Enumerable.Empty<ILocalImageProvider>(), null);
 
 
             Assert.True(changed);
             Assert.True(changed);
             Assert.Empty(item.GetImages(imageType));
             Assert.Empty(item.GetImages(imageType));
@@ -109,7 +107,7 @@ namespace Jellyfin.Providers.Tests.Manager
         public void MergeImages_EmptyItemNewImagesEmpty_NoChange()
         public void MergeImages_EmptyItemNewImagesEmpty_NoChange()
         {
         {
             var itemImageProvider = GetItemImageProvider(null, null);
             var itemImageProvider = GetItemImageProvider(null, null);
-            var changed = itemImageProvider.MergeImages(new MovieWithScreenshots(), new List<LocalImageInfo>());
+            var changed = itemImageProvider.MergeImages(new Video(), Array.Empty<LocalImageInfo>());
 
 
             Assert.False(changed);
             Assert.False(changed);
         }
         }
@@ -237,7 +235,8 @@ namespace Jellyfin.Providers.Tests.Manager
             var refreshOptions = forceRefresh
             var refreshOptions = forceRefresh
                 ? new ImageRefreshOptions(null)
                 ? new ImageRefreshOptions(null)
                 {
                 {
-                    ImageRefreshMode = MetadataRefreshMode.FullRefresh, ReplaceAllImages = true
+                    ImageRefreshMode = MetadataRefreshMode.FullRefresh,
+                    ReplaceAllImages = true
                 }
                 }
                 : new ImageRefreshOptions(null);
                 : new ImageRefreshOptions(null);
 
 
@@ -269,7 +268,7 @@ namespace Jellyfin.Providers.Tests.Manager
             // Has to exist for querying DateModified time on file, results stored but not checked so not populating
             // Has to exist for querying DateModified time on file, results stored but not checked so not populating
             BaseItem.FileSystem = Mock.Of<IFileSystem>();
             BaseItem.FileSystem = Mock.Of<IFileSystem>();
 
 
-            var item = new MovieWithScreenshots();
+            var item = new Video();
 
 
             var libraryOptions = GetLibraryOptions(item, imageType, imageCount);
             var libraryOptions = GetLibraryOptions(item, imageType, imageCount);
 
 
@@ -311,11 +310,9 @@ namespace Jellyfin.Providers.Tests.Manager
         [InlineData(ImageType.Primary, 1, false)]
         [InlineData(ImageType.Primary, 1, false)]
         [InlineData(ImageType.Backdrop, 1, false)]
         [InlineData(ImageType.Backdrop, 1, false)]
         [InlineData(ImageType.Backdrop, 2, false)]
         [InlineData(ImageType.Backdrop, 2, false)]
-        [InlineData(ImageType.Screenshot, 2, false)]
         [InlineData(ImageType.Primary, 1, true)]
         [InlineData(ImageType.Primary, 1, true)]
         [InlineData(ImageType.Backdrop, 1, true)]
         [InlineData(ImageType.Backdrop, 1, true)]
         [InlineData(ImageType.Backdrop, 2, true)]
         [InlineData(ImageType.Backdrop, 2, true)]
-        [InlineData(ImageType.Screenshot, 2, true)]
         public async void RefreshImages_PopulatedItemPopulatedProviderRemote_UpdatesImagesIfForced(ImageType imageType, int imageCount, bool forceRefresh)
         public async void RefreshImages_PopulatedItemPopulatedProviderRemote_UpdatesImagesIfForced(ImageType imageType, int imageCount, bool forceRefresh)
         {
         {
             var item = GetItemWithImages(imageType, imageCount, false);
             var item = GetItemWithImages(imageType, imageCount, false);
@@ -330,19 +327,20 @@ namespace Jellyfin.Providers.Tests.Manager
             var refreshOptions = forceRefresh
             var refreshOptions = forceRefresh
                 ? new ImageRefreshOptions(null)
                 ? new ImageRefreshOptions(null)
                 {
                 {
-                    ImageRefreshMode = MetadataRefreshMode.FullRefresh, ReplaceAllImages = true
+                    ImageRefreshMode = MetadataRefreshMode.FullRefresh,
+                    ReplaceAllImages = true
                 }
                 }
                 : new ImageRefreshOptions(null);
                 : new ImageRefreshOptions(null);
 
 
-            var remoteInfo = new List<RemoteImageInfo>();
+            var remoteInfo = new RemoteImageInfo[imageCount];
             for (int i = 0; i < imageCount; i++)
             for (int i = 0; i < imageCount; i++)
             {
             {
-                remoteInfo.Add(new RemoteImageInfo
+                remoteInfo[i] = new RemoteImageInfo
                 {
                 {
                     Type = imageType,
                     Type = imageType,
                     Url = "image url " + i,
                     Url = "image url " + i,
                     Width = 1 // min width is set to 0, this will always pass
                     Width = 1 // min width is set to 0, this will always pass
-                });
+                };
             }
             }
 
 
             var providerManager = new Mock<IProviderManager>(MockBehavior.Strict);
             var providerManager = new Mock<IProviderManager>(MockBehavior.Strict);
@@ -383,7 +381,7 @@ namespace Jellyfin.Providers.Tests.Manager
             // seek 2 so it won't short-circuit out of downloading when populated
             // seek 2 so it won't short-circuit out of downloading when populated
             var libraryOptions = GetLibraryOptions(item, imageType, 2);
             var libraryOptions = GetLibraryOptions(item, imageType, 2);
 
 
-            var content = "Content";
+            const string Content = "Content";
             var remoteProvider = new Mock<IRemoteImageProvider>(MockBehavior.Strict);
             var remoteProvider = new Mock<IRemoteImageProvider>(MockBehavior.Strict);
             remoteProvider.Setup(rp => rp.Name).Returns("MockRemoteProvider");
             remoteProvider.Setup(rp => rp.Name).Returns("MockRemoteProvider");
             remoteProvider.Setup(rp => rp.GetSupportedImages(item))
             remoteProvider.Setup(rp => rp.GetSupportedImages(item))
@@ -393,7 +391,7 @@ namespace Jellyfin.Providers.Tests.Manager
                 {
                 {
                     ReasonPhrase = url,
                     ReasonPhrase = url,
                     StatusCode = HttpStatusCode.OK,
                     StatusCode = HttpStatusCode.OK,
-                    Content = new StringContent(content, Encoding.UTF8, "image/jpeg")
+                    Content = new StringContent(Content, Encoding.UTF8, "image/jpeg")
                 });
                 });
 
 
             var refreshOptions = fullRefresh
             var refreshOptions = fullRefresh
@@ -404,15 +402,15 @@ namespace Jellyfin.Providers.Tests.Manager
                 }
                 }
                 : new ImageRefreshOptions(null);
                 : new ImageRefreshOptions(null);
 
 
-            var remoteInfo = new List<RemoteImageInfo>();
+            var remoteInfo = new RemoteImageInfo[targetImageCount];
             for (int i = 0; i < targetImageCount; i++)
             for (int i = 0; i < targetImageCount; i++)
             {
             {
-                remoteInfo.Add(new RemoteImageInfo
+                remoteInfo[i] = new RemoteImageInfo()
                 {
                 {
                     Type = imageType,
                     Type = imageType,
                     Url = "image url " + i,
                     Url = "image url " + i,
                     Width = 1 // min width is set to 0, this will always pass
                     Width = 1 // min width is set to 0, this will always pass
-                });
+                };
             }
             }
 
 
             var providerManager = new Mock<IProviderManager>(MockBehavior.Strict);
             var providerManager = new Mock<IProviderManager>(MockBehavior.Strict);
@@ -425,7 +423,7 @@ namespace Jellyfin.Providers.Tests.Manager
             var fileSystem = new Mock<IFileSystem>();
             var fileSystem = new Mock<IFileSystem>();
             // match reported file size to image content length - condition for skipping already downloaded multi-images
             // match reported file size to image content length - condition for skipping already downloaded multi-images
             fileSystem.Setup(fs => fs.GetFileInfo(It.IsAny<string>()))
             fileSystem.Setup(fs => fs.GetFileInfo(It.IsAny<string>()))
-                .Returns(new FileSystemMetadata { Length = content.Length });
+                .Returns(new FileSystemMetadata { Length = Content.Length });
             var itemImageProvider = GetItemImageProvider(providerManager.Object, fileSystem);
             var itemImageProvider = GetItemImageProvider(providerManager.Object, fileSystem);
             var result = await itemImageProvider.RefreshImages(item, libraryOptions, new List<IImageProvider> { remoteProvider.Object }, refreshOptions, CancellationToken.None);
             var result = await itemImageProvider.RefreshImages(item, libraryOptions, new List<IImageProvider> { remoteProvider.Object }, refreshOptions, CancellationToken.None);
 
 
@@ -437,7 +435,7 @@ namespace Jellyfin.Providers.Tests.Manager
         [MemberData(nameof(GetImageTypesWithCount))]
         [MemberData(nameof(GetImageTypesWithCount))]
         public async void RefreshImages_EmptyItemPopulatedProviderRemoteExtras_LimitsImages(ImageType imageType, int imageCount)
         public async void RefreshImages_EmptyItemPopulatedProviderRemoteExtras_LimitsImages(ImageType imageType, int imageCount)
         {
         {
-            var item = new MovieWithScreenshots();
+            var item = new Video();
 
 
             var libraryOptions = GetLibraryOptions(item, imageType, imageCount);
             var libraryOptions = GetLibraryOptions(item, imageType, imageCount);
 
 
@@ -449,15 +447,16 @@ namespace Jellyfin.Providers.Tests.Manager
             var refreshOptions = new ImageRefreshOptions(null);
             var refreshOptions = new ImageRefreshOptions(null);
 
 
             // populate remote with double the required images to verify count is trimmed to the library option count
             // populate remote with double the required images to verify count is trimmed to the library option count
-            var remoteInfo = new List<RemoteImageInfo>();
-            for (int i = 0; i < imageCount * 2; i++)
+            var remoteInfoCount = imageCount * 2;
+            var remoteInfo = new RemoteImageInfo[remoteInfoCount];
+            for (int i = 0; i < remoteInfoCount; i++)
             {
             {
-                remoteInfo.Add(new RemoteImageInfo
+                remoteInfo[i] = new RemoteImageInfo()
                 {
                 {
                     Type = imageType,
                     Type = imageType,
                     Url = "image url " + i,
                     Url = "image url " + i,
                     Width = 1 // min width is set to 0, this will always pass
                     Width = 1 // min width is set to 0, this will always pass
-                });
+                };
             }
             }
 
 
             var providerManager = new Mock<IProviderManager>(MockBehavior.Strict);
             var providerManager = new Mock<IProviderManager>(MockBehavior.Strict);
@@ -525,7 +524,7 @@ namespace Jellyfin.Providers.Tests.Manager
             // Has to exist for querying DateModified time on file, results stored but not checked so not populating
             // Has to exist for querying DateModified time on file, results stored but not checked so not populating
             BaseItem.FileSystem ??= Mock.Of<IFileSystem>();
             BaseItem.FileSystem ??= Mock.Of<IFileSystem>();
 
 
-            var item = new MovieWithScreenshots();
+            var item = new Video();
 
 
             var path = validPaths ? TestDataImagePath : "invalid path {0}";
             var path = validPaths ? TestDataImagePath : "invalid path {0}";
             for (int i = 0; i < count; i++)
             for (int i = 0; i < count; i++)
@@ -552,20 +551,20 @@ namespace Jellyfin.Providers.Tests.Manager
         /// <summary>
         /// <summary>
         /// Creates a list of <see cref="LocalImageInfo"/> references of the specified type and size, optionally pointing to files that exist.
         /// Creates a list of <see cref="LocalImageInfo"/> references of the specified type and size, optionally pointing to files that exist.
         /// </summary>
         /// </summary>
-        private static List<LocalImageInfo> GetImages(ImageType type, int count, bool validPaths)
+        private static LocalImageInfo[] GetImages(ImageType type, int count, bool validPaths)
         {
         {
             var path = validPaths ? TestDataImagePath : "invalid path {0}";
             var path = validPaths ? TestDataImagePath : "invalid path {0}";
-            var images = new List<LocalImageInfo>(count);
+            var images = new LocalImageInfo[count];
             for (int i = 0; i < count; i++)
             for (int i = 0; i < count; i++)
             {
             {
-                images.Add(new LocalImageInfo
+                images[i] = new LocalImageInfo
                 {
                 {
                     Type = type,
                     Type = type,
                     FileInfo = new FileSystemMetadata
                     FileInfo = new FileSystemMetadata
                     {
                     {
                         FullName = string.Format(CultureInfo.InvariantCulture, path, i)
                         FullName = string.Format(CultureInfo.InvariantCulture, path, i)
                     }
                     }
-                });
+                };
             }
             }
 
 
             return images;
             return images;
@@ -596,11 +595,5 @@ namespace Jellyfin.Providers.Tests.Manager
                 }
                 }
             };
             };
         }
         }
-
-        // Create a class that implements IHasScreenshots for testing since no BaseItem class is also IHasScreenshots
-        private class MovieWithScreenshots : Movie, IHasScreenshots
-        {
-            // No contents
-        }
     }
     }
 }
 }

+ 13 - 0
tests/Jellyfin.Providers.Tests/Tmdb/TmdbUtilsTests.cs

@@ -23,5 +23,18 @@ namespace Jellyfin.Providers.Tests.Tmdb
         {
         {
             Assert.Equal(expected, TmdbUtils.NormalizeLanguage(input!));
             Assert.Equal(expected, TmdbUtils.NormalizeLanguage(input!));
         }
         }
+
+        [Theory]
+        [InlineData(null, null, null)]
+        [InlineData(null, "en-US", null)]
+        [InlineData("en", null, "en")]
+        [InlineData("en", "en-US", "en-US")]
+        [InlineData("fr-CA", "fr-BE", "fr-CA")]
+        [InlineData("fr-CA", "fr", "fr-CA")]
+        [InlineData("de", "en-US", "de")]
+        public static void AdjustImageLanguage_Valid_Success(string imageLanguage, string requestLanguage, string expected)
+        {
+            Assert.Equal(expected, TmdbUtils.AdjustImageLanguage(imageLanguage, requestLanguage));
+        }
     }
     }
 }
 }