Ver código fonte

Merge branch 'support-external-audio-files' of github.com:jonas-resch/jellyfin into support-external-audio-files

Jonas Resch 3 anos atrás
pai
commit
87a6fdf847
30 arquivos alterados com 497 adições e 237 exclusões
  1. 4 5
      Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs
  2. 1 1
      Emby.Server.Implementations/Localization/Core/ar.json
  3. 2 2
      Emby.Server.Implementations/Localization/Core/es.json
  4. 2 2
      Emby.Server.Implementations/Localization/Core/fa.json
  5. 2 2
      Emby.Server.Implementations/Localization/Core/ja.json
  6. 1 1
      Emby.Server.Implementations/Localization/Core/mk.json
  7. 17 17
      Emby.Server.Implementations/Localization/Core/ms.json
  8. 1 1
      Emby.Server.Implementations/Localization/Core/ne.json
  9. 1 1
      Emby.Server.Implementations/Localization/Core/nl.json
  10. 7 7
      Emby.Server.Implementations/Localization/Core/pa.json
  11. 3 3
      Emby.Server.Implementations/Localization/Core/pl.json
  12. 3 2
      Emby.Server.Implementations/Localization/Core/sr.json
  13. 1 1
      Emby.Server.Implementations/Localization/Core/vi.json
  14. 1 0
      Jellyfin.Api/Helpers/StreamingHelpers.cs
  15. 2 2
      MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj
  16. 1 0
      MediaBrowser.Model/Configuration/LibraryOptions.cs
  17. 4 0
      MediaBrowser.Model/MediaBrowser.Model.csproj
  18. 50 99
      MediaBrowser.Model/Net/MimeTypes.cs
  19. 20 43
      MediaBrowser.Providers/MediaInfo/FFProbeProvider.cs
  20. 41 0
      MediaBrowser.Providers/Plugins/Tmdb/Api/TmdbController.cs
  21. 20 0
      MediaBrowser.Providers/Plugins/Tmdb/Configuration/PluginConfiguration.cs
  22. 62 2
      MediaBrowser.Providers/Plugins/Tmdb/Configuration/config.html
  23. 57 11
      MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs
  24. 14 1
      debian/jellyfin.service
  25. 164 0
      tests/Jellyfin.Model.Tests/Net/MimeTypesTests.cs
  26. 3 3
      tests/Jellyfin.Server.Integration.Tests/AuthHelper.cs
  27. 4 9
      tests/Jellyfin.Server.Integration.Tests/Controllers/DlnaControllerTests.cs
  28. 3 6
      tests/Jellyfin.Server.Integration.Tests/Controllers/MediaStructureControllerTests.cs
  29. 3 6
      tests/Jellyfin.Server.Integration.Tests/Controllers/StartupControllerTests.cs
  30. 3 10
      tests/Jellyfin.Server.Integration.Tests/Controllers/UserControllerTests.cs

+ 4 - 5
Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs

@@ -9,6 +9,7 @@ using System.Globalization;
 using System.Linq;
 using System.Net;
 using System.Net.Http;
+using System.Net.Http.Json;
 using System.Net.Http.Headers;
 using System.Net.Mime;
 using System.Security.Cryptography;
@@ -101,11 +102,10 @@ namespace Emby.Server.Implementations.LiveTv.Listings
                     }
                 };
 
-            var requestString = JsonSerializer.Serialize(requestList, _jsonOptions);
-            _logger.LogDebug("Request string for schedules is: {RequestString}", requestString);
+            _logger.LogDebug("Request string for schedules is: {@RequestString}", requestList);
 
             using var options = new HttpRequestMessage(HttpMethod.Post, ApiUrl + "/schedules");
-            options.Content = new StringContent(requestString, Encoding.UTF8, MediaTypeNames.Application.Json);
+            options.Content = JsonContent.Create(requestList, options: _jsonOptions);
             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);
@@ -121,8 +121,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
             programRequestOptions.Headers.TryAddWithoutValidation("token", token);
 
             var programIds = dailySchedules.SelectMany(d => d.Programs.Select(s => s.ProgramId)).Distinct();
-            programRequestOptions.Content = new ByteArrayContent(JsonSerializer.SerializeToUtf8Bytes(programIds, _jsonOptions));
-            programRequestOptions.Content.Headers.ContentType = MediaTypeHeaderValue.Parse(MediaTypeNames.Application.Json);
+            programRequestOptions.Content = JsonContent.Create(programIds, options: _jsonOptions);
 
             using var innerResponse = await Send(programRequestOptions, true, info, cancellationToken).ConfigureAwait(false);
             await using var innerResponseStream = await innerResponse.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);

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

@@ -11,7 +11,7 @@
     "Collections": "التجميعات",
     "DeviceOfflineWithName": "قُطِع الاتصال ب{0}",
     "DeviceOnlineWithName": "{0} متصل",
-    "FailedLoginAttemptWithUserName": "عملية تسجيل الدخول فشلت من {0}",
+    "FailedLoginAttemptWithUserName": "محاولة تسجيل الدخول فشلت من {0}",
     "Favorites": "مفضلات",
     "Folders": "المجلدات",
     "Genres": "التضنيفات",

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

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

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

@@ -6,7 +6,7 @@
     "AuthenticationSucceededWithUserName": "{0} با موفقیت تایید اعتبار شد",
     "Books": "کتاب‌ها",
     "CameraImageUploadedFrom": "یک عکس جدید از دوربین ارسال شده است {0}",
-    "Channels": "کانالها",
+    "Channels": "کانالها",
     "ChapterNameValue": "قسمت {0}",
     "Collections": "مجموعه‌ها",
     "DeviceOfflineWithName": "ارتباط {0} قطع شد",
@@ -37,7 +37,7 @@
     "MessageNamedServerConfigurationUpdatedWithValue": "پکربندی بخش {0} سرور بروزرسانی شد",
     "MessageServerConfigurationUpdated": "پیکربندی سرور بروزرسانی شد",
     "MixedContent": "محتوای مخلوط",
-    "Movies": "فیلمها",
+    "Movies": "فیلم ها",
     "Music": "موسیقی",
     "MusicVideos": "موزیک ویدیوها",
     "NameInstallFailed": "{0} نصب با مشکل مواجه شد",

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

@@ -11,11 +11,11 @@
     "Collections": "コレクション",
     "DeviceOfflineWithName": "{0} が切断されました",
     "DeviceOnlineWithName": "{0} が接続されました",
-    "FailedLoginAttemptWithUserName": "ログインを試行しましたが {0}によって失敗しました",
+    "FailedLoginAttemptWithUserName": "ログインを試行しましたが {0} によって失敗しました",
     "Favorites": "お気に入り",
     "Folders": "フォルダー",
     "Genres": "ジャンル",
-    "HeaderAlbumArtists": "アーティストのアルバム",
+    "HeaderAlbumArtists": "アルバムアーティスト",
     "HeaderContinueWatching": "視聴を続ける",
     "HeaderFavoriteAlbums": "お気に入りのアルバム",
     "HeaderFavoriteArtists": "お気に入りのアーティスト",

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

@@ -5,7 +5,7 @@
     "PluginUninstalledWithName": "{0} беше успешно деинсталирано",
     "PluginInstalledWithName": "{0} беше успешно инсталирано",
     "Plugin": "Додатоци",
-    "Playlists": "Листи",
+    "Playlists": "Плејлисти",
     "Photos": "Слики",
     "NotificationOptionVideoPlaybackStopped": "Видео стопирано",
     "NotificationOptionVideoPlayback": "Видео пуштено",

+ 17 - 17
Emby.Server.Implementations/Localization/Core/ms.json

@@ -37,7 +37,7 @@
     "MessageNamedServerConfigurationUpdatedWithValue": "Konfigurasi pelayan di bahagian {0} telah dikemas kini",
     "MessageServerConfigurationUpdated": "Konfigurasi pelayan telah dikemas kini",
     "MixedContent": "Kandungan campuran",
-    "Movies": "Filem",
+    "Movies": "Filem-filem",
     "Music": "Muzik",
     "MusicVideos": "",
     "NameInstallFailed": "{0} pemasangan gagal",
@@ -53,23 +53,23 @@
     "NotificationOptionNewLibraryContent": "Kandungan baru telah ditambah",
     "NotificationOptionPluginError": "Kegagalan plugin",
     "NotificationOptionPluginInstalled": "Plugin telah dipasang",
-    "NotificationOptionPluginUninstalled": "Plugin uninstalled",
-    "NotificationOptionPluginUpdateInstalled": "Plugin update installed",
+    "NotificationOptionPluginUninstalled": "Plugin telah dinyahpasang",
+    "NotificationOptionPluginUpdateInstalled": "Kemaskini plugin telah dipasang",
     "NotificationOptionServerRestartRequired": "Server restart required",
-    "NotificationOptionTaskFailed": "Scheduled task failure",
-    "NotificationOptionUserLockedOut": "User locked out",
-    "NotificationOptionVideoPlayback": "Video playback started",
+    "NotificationOptionTaskFailed": "Kegagalan tugas berjadual",
+    "NotificationOptionUserLockedOut": "Pengguna telah dikunci",
+    "NotificationOptionVideoPlayback": "Ulangmain video bermula",
     "NotificationOptionVideoPlaybackStopped": "Ulangmain video dihentikan",
     "Photos": "Gambar-gambar",
     "Playlists": "Senarai main",
     "Plugin": "Plugin",
-    "PluginInstalledWithName": "{0} was installed",
-    "PluginUninstalledWithName": "{0} was uninstalled",
-    "PluginUpdatedWithName": "{0} was updated",
+    "PluginInstalledWithName": "{0} telah dipasang",
+    "PluginUninstalledWithName": "{0} telah dinyahpasang",
+    "PluginUpdatedWithName": "{0} telah dikemaskini",
     "ProviderValue": "Provider: {0}",
     "ScheduledTaskFailedWithName": "{0} gagal",
     "ScheduledTaskStartedWithName": "{0} bermula",
-    "ServerNameNeedsToBeRestarted": "{0} needs to be restarted",
+    "ServerNameNeedsToBeRestarted": "{0} perlu di ulangmula",
     "Shows": "Series",
     "Songs": "Lagu-lagu",
     "StartupEmbyServerIsLoading": "Pelayan Jellyfin sedang dimuatkan. Sila cuba sebentar lagi.",
@@ -77,19 +77,19 @@
     "SubtitleDownloadFailureFromForItem": "Muat turun sarikata gagal dari {0} untuk {1}",
     "Sync": "Sync",
     "System": "Sistem",
-    "TvShows": "TV Shows",
+    "TvShows": "Tayangan TV",
     "User": "User",
-    "UserCreatedWithName": "User {0} has been created",
-    "UserDeletedWithName": "User {0} has been deleted",
-    "UserDownloadingItemWithValues": "{0} is downloading {1}",
+    "UserCreatedWithName": "Pengguna {0} telah diwujudkan",
+    "UserDeletedWithName": "Pengguna {0} telah dipadamkan",
+    "UserDownloadingItemWithValues": "{0} sedang memuat turun {1}",
     "UserLockedOutWithName": "Pengguna {0} telah dikunci",
     "UserOfflineFromDevice": "{0} telah terputus dari {1}",
     "UserOnlineFromDevice": "{0} berada dalam talian dari {1}",
     "UserPasswordChangedWithName": "Kata laluan telah ditukar bagi pengguna {0}",
     "UserPolicyUpdatedWithName": "Dasar pengguna telah dikemas kini untuk {0}",
-    "UserStartedPlayingItemWithValues": "{0} is playing {1} on {2}",
-    "UserStoppedPlayingItemWithValues": "{0} has finished playing {1} on {2}",
-    "ValueHasBeenAddedToLibrary": "{0} has been added to your media library",
+    "UserStartedPlayingItemWithValues": "{0} sedang dimainkan {1} pada {2}",
+    "UserStoppedPlayingItemWithValues": "{0} telah tamat dimainkan {1} pada {2}",
+    "ValueHasBeenAddedToLibrary": "{0} telah ditambah ke media library anda",
     "ValueSpecialEpisodeName": "Khas - {0}",
     "VersionNumber": "Versi {0}",
     "TaskCleanActivityLog": "Log Aktiviti Bersih",

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

@@ -69,7 +69,7 @@
     "UserDeletedWithName": "प्रयोगकर्ता {0} हटाइएको छ",
     "UserCreatedWithName": "प्रयोगकर्ता {0} सिर्जना गरिएको छ",
     "User": "प्रयोगकर्ता",
-    "PluginInstalledWithName": "",
+    "PluginInstalledWithName": "{0} सभएको थियो",
     "StartupEmbyServerIsLoading": "Jellyfin सर्भर लोड हुँदैछ। कृपया छिट्टै फेरि प्रयास गर्नुहोस्।",
     "Songs": "गीतहरू",
     "Shows": "शोहरू",

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

@@ -15,7 +15,7 @@
     "Favorites": "Favorieten",
     "Folders": "Mappen",
     "Genres": "Genres",
-    "HeaderAlbumArtists": "Artiests Album",
+    "HeaderAlbumArtists": "Album Artiesten",
     "HeaderContinueWatching": "Kijken hervatten",
     "HeaderFavoriteAlbums": "Favoriete albums",
     "HeaderFavoriteArtists": "Favoriete artiesten",

+ 7 - 7
Emby.Server.Implementations/Localization/Core/pa.json

@@ -24,7 +24,7 @@
     "TasksLibraryCategory": "ਲਾਇਬ੍ਰੇਰੀ",
     "TasksMaintenanceCategory": "ਰੱਖ-ਰਖਾਅ",
     "VersionNumber": "ਵਰਜਨ {0}",
-    "ValueSpecialEpisodeName": "ਵਿਸ਼ੇਸ਼ - {0}",
+    "ValueSpecialEpisodeName": "ਖਾਸ - {0}",
     "ValueHasBeenAddedToLibrary": "{0} ਤੁਹਾਡੀ ਮੀਡੀਆ ਲਾਇਬ੍ਰੇਰੀ ਵਿੱਚ ਸ਼ਾਮਲ ਕੀਤਾ ਗਿਆ ਹੈ",
     "UserStoppedPlayingItemWithValues": "{0} ਨੇ {2} 'ਤੇ {1} ਖੇਡਣਾ ਪੂਰਾ ਕਰ ਲਿਆ ਹੈ",
     "UserStartedPlayingItemWithValues": "{0} {2} 'ਤੇ {1} ਖੇਡ ਰਿਹਾ ਹੈ",
@@ -43,8 +43,8 @@
     "Sync": "ਸਿੰਕ",
     "SubtitleDownloadFailureFromForItem": "ਉਪਸਿਰਲੇਖ {1} ਲਈ {0} ਤੋਂ ਡਾ toਨਲੋਡ ਕਰਨ ਵਿੱਚ ਅਸਫਲ ਰਹੇ",
     "StartupEmbyServerIsLoading": "ਜੈਲੀਫਿਨ ਸਰਵਰ ਲੋਡ ਹੋ ਰਿਹਾ ਹੈ. ਕਿਰਪਾ ਕਰਕੇ ਜਲਦੀ ਹੀ ਦੁਬਾਰਾ ਕੋਸ਼ਿਸ਼ ਕਰੋ.",
-    "Songs": "ਗਾਣੇ",
-    "Shows": "ਸ਼ੋਅਜ਼",
+    "Songs": "ਗਾਣੇ",
+    "Shows": "ਸ਼ੋਅ",
     "ServerNameNeedsToBeRestarted": "{0} ਮੁੜ ਚਾਲੂ ਕਰਨ ਦੀ ਲੋੜ ਹੈ",
     "ScheduledTaskStartedWithName": "{0} ਸ਼ੁਰੂ ਹੋਇਆ",
     "ScheduledTaskFailedWithName": "{0} ਅਸਫਲ",
@@ -53,7 +53,7 @@
     "PluginUninstalledWithName": "{0} ਅਣਇੰਸਟੌਲ ਕੀਤਾ ਗਿਆ ਸੀ",
     "PluginInstalledWithName": "{0} ਲਗਾਇਆ ਗਿਆ ਸੀ",
     "Plugin": "ਪਲੱਗਇਨ",
-    "Playlists": "ਪਲੇਲਿਸਟਸ",
+    "Playlists": "ਪਲੇਸੂਚੀਆਂ",
     "Photos": "ਫੋਟੋਆਂ",
     "NotificationOptionVideoPlaybackStopped": "ਵੀਡੀਓ ਪਲੇਬੈਕ ਰੋਕਿਆ ਗਿਆ",
     "NotificationOptionVideoPlayback": "ਵੀਡੀਓ ਪਲੇਬੈਕ ਸ਼ੁਰੂ ਹੋਇਆ",
@@ -102,13 +102,13 @@
     "HeaderAlbumArtists": "ਐਲਬਮ ਕਲਾਕਾਰ",
     "Genres": "ਸ਼ੈਲੀਆਂ",
     "Forced": "ਮਜਬੂਰ",
-    "Folders": "ਫੋਲਡਰ",
+    "Folders": "ਫੋਲਡਰ",
     "Favorites": "ਮਨਪਸੰਦ",
     "FailedLoginAttemptWithUserName": "ਤੋਂ ਲਾਗਇਨ ਕੋਸ਼ਿਸ਼ ਫੇਲ ਹੋਈ {0}",
     "DeviceOnlineWithName": "{0} ਜੁੜਿਆ ਹੋਇਆ ਹੈ",
     "DeviceOfflineWithName": "{0} ਡਿਸਕਨੈਕਟ ਹੋ ਗਿਆ ਹੈ",
-    "Default": "ਮੂਲ",
-    "Collections": "ਸੰਗ੍ਰਹਿ",
+    "Default": "ਡਿਫੌਲਟ",
+    "Collections": "ਸੰਗ੍ਰਹਿ",
     "ChapterNameValue": "ਅਧਿਆਇ {0}",
     "Channels": "ਚੈਨਲ",
     "CameraImageUploadedFrom": "ਤੋਂ ਇੱਕ ਨਵਾਂ ਕੈਮਰਾ ਚਿੱਤਰ ਅਪਲੋਡ ਕੀਤਾ ਗਿਆ ਹੈ {0}",

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

@@ -15,7 +15,7 @@
     "Favorites": "Ulubione",
     "Folders": "Foldery",
     "Genres": "Gatunki",
-    "HeaderAlbumArtists": "Album artysty",
+    "HeaderAlbumArtists": "Wykonawcy albumów",
     "HeaderContinueWatching": "Kontynuuj odtwarzanie",
     "HeaderFavoriteAlbums": "Ulubione albumy",
     "HeaderFavoriteArtists": "Ulubieni wykonawcy",
@@ -47,7 +47,7 @@
     "NotificationOptionApplicationUpdateAvailable": "Dostępna aktualizacja aplikacji",
     "NotificationOptionApplicationUpdateInstalled": "Zaktualizowano aplikację",
     "NotificationOptionAudioPlayback": "Rozpoczęto odtwarzanie muzyki",
-    "NotificationOptionAudioPlaybackStopped": "Odtwarzane dźwięku zatrzymane",
+    "NotificationOptionAudioPlaybackStopped": "Odtwarzanie dźwięku zatrzymane",
     "NotificationOptionCameraImageUploaded": "Przekazano obraz z urządzenia przenośnego",
     "NotificationOptionInstallationFailed": "Nieudana instalacja",
     "NotificationOptionNewLibraryContent": "Dodano nową zawartość",
@@ -98,7 +98,7 @@
     "TaskRefreshChannels": "Odśwież kanały",
     "TaskCleanTranscodeDescription": "Usuwa transkodowane pliki starsze niż 1 dzień.",
     "TaskCleanTranscode": "Wyczyść folder transkodowania",
-    "TaskUpdatePluginsDescription": "Pobiera i instaluje aktualizacje dla pluginów które są skonfigurowane do automatycznej aktualizacji.",
+    "TaskUpdatePluginsDescription": "Pobiera i instaluje aktualizacje dla pluginów, które są skonfigurowane do automatycznej aktualizacji.",
     "TaskUpdatePlugins": "Aktualizuj pluginy",
     "TaskRefreshPeopleDescription": "Odświeża metadane o aktorów i reżyserów w Twojej bibliotece mediów.",
     "TaskRefreshPeople": "Odśwież obsadę",

+ 3 - 2
Emby.Server.Implementations/Localization/Core/sr.json

@@ -64,7 +64,7 @@
     "ItemRemovedWithName": "{0} уклоњено из библиотеке",
     "ItemAddedWithName": "{0} додато у библиотеку",
     "Inherit": "Наследи",
-    "HomeVideos": "Кућни видео",
+    "HomeVideos": "Кућни Видео",
     "HeaderRecordingGroups": "Групе снимања",
     "HeaderNextUp": "Следи",
     "HeaderLiveTV": "ТВ уживо",
@@ -117,5 +117,6 @@
     "TaskCleanActivityLog": "Очисти историју активности",
     "Undefined": "Недефинисано",
     "Forced": "Принудно",
-    "Default": "Подразумевано"
+    "Default": "Подразумевано",
+    "TaskOptimizeDatabase": "Оптимизуј датабазу"
 }

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

@@ -103,7 +103,7 @@
     "HeaderFavoriteEpisodes": "Tập Phim Yêu Thích",
     "HeaderFavoriteArtists": "Nghệ Sĩ Yêu Thích",
     "HeaderFavoriteAlbums": "Album Ưa Thích",
-    "FailedLoginAttemptWithUserName": "Nỗ lực đăng nhập thất bại từ {0}",
+    "FailedLoginAttemptWithUserName": "Đăng nhập không thành công thử từ {0}",
     "DeviceOnlineWithName": "{0} đã kết nối",
     "DeviceOfflineWithName": "{0} đã ngắt kết nối",
     "ChapterNameValue": "Phân Cảnh {0}",

+ 1 - 0
Jellyfin.Api/Helpers/StreamingHelpers.cs

@@ -90,6 +90,7 @@ namespace Jellyfin.Api.Helpers
             }
 
             var enableDlnaHeaders = !string.IsNullOrWhiteSpace(streamingRequest.Params) ||
+                                    streamingRequest.StreamOptions.ContainsKey("dlnaheaders") ||
                                     string.Equals(httpRequest.Headers["GetContentFeatures.DLNA.ORG"], "1", StringComparison.OrdinalIgnoreCase);
 
             var state = new StreamState(mediaSourceManager, transcodingJobType, transcodingJobHelper)

+ 2 - 2
MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj

@@ -22,8 +22,8 @@
   </ItemGroup>
 
   <ItemGroup>
-    <PackageReference Include="BDInfo" Version="0.7.6.1" />
-    <PackageReference Include="libse" Version="3.6.2" />
+    <PackageReference Include="BDInfo" Version="0.7.6.2" />
+    <PackageReference Include="libse" Version="3.6.4" />
     <PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0" />
     <PackageReference Include="System.Text.Encoding.CodePages" Version="6.0.0" />
     <PackageReference Include="UTF.Unknown" Version="2.5.0" />

+ 1 - 0
MediaBrowser.Model/Configuration/LibraryOptions.cs

@@ -81,6 +81,7 @@ namespace MediaBrowser.Model.Configuration
         public bool RequirePerfectSubtitleMatch { get; set; }
 
         public bool SaveSubtitlesWithMedia { get; set; }
+
         public bool AutomaticallyAddToCollection { get; set; }
 
         public TypeOptions[] TypeOptions { get; set; }

+ 4 - 0
MediaBrowser.Model/MediaBrowser.Model.csproj

@@ -31,6 +31,10 @@
   <ItemGroup>
     <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All" />
     <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="6.0.0" />
+    <PackageReference Include="MimeTypes" Version="2.2.1">
+      <PrivateAssets>all</PrivateAssets>
+      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
+    </PackageReference>
     <PackageReference Include="System.Globalization" Version="4.3.0" />
     <PackageReference Include="System.Text.Json" Version="6.0.0" />
   </ItemGroup>

+ 50 - 99
MediaBrowser.Model/Net/MimeTypes.cs

@@ -12,6 +12,15 @@ namespace MediaBrowser.Model.Net
     /// <summary>
     /// Class MimeTypes.
     /// </summary>
+    ///
+    /// <remarks>
+    /// For more information on MIME types:
+    /// <list type="bullet">
+    ///     <item>http://en.wikipedia.org/wiki/Internet_media_type</item>
+    ///     <item>https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types</item>
+    ///     <item>http://www.iana.org/assignments/media-types/media-types.xhtml</item>
+    /// </list>
+    /// </remarks>
     public static class MimeTypes
     {
         /// <summary>
@@ -50,81 +59,26 @@ namespace MediaBrowser.Model.Net
             ".wtv",
         };
 
-        // http://en.wikipedia.org/wiki/Internet_media_type
-        // https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types
-        // http://www.iana.org/assignments/media-types/media-types.xhtml
-        // Add more as needed
+        /// <summary>
+        /// Used for extensions not in <see cref="Model.MimeTypes"/> or to override them.
+        /// </summary>
         private static readonly Dictionary<string, string> _mimeTypeLookup = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
         {
             // Type application
-            { ".7z", "application/x-7z-compressed" },
-            { ".azw", "application/vnd.amazon.ebook" },
             { ".azw3", "application/vnd.amazon.ebook" },
-            { ".cbz", "application/x-cbz" },
-            { ".cbr", "application/epub+zip" },
-            { ".eot", "application/vnd.ms-fontobject" },
-            { ".epub", "application/epub+zip" },
-            { ".js", "application/x-javascript" },
-            { ".json", "application/json" },
-            { ".m3u8", "application/x-mpegURL" },
-            { ".map", "application/x-javascript" },
-            { ".mobi", "application/x-mobipocket-ebook" },
-            { ".opf", "application/oebps-package+xml" },
-            { ".pdf", "application/pdf" },
-            { ".rar", "application/vnd.rar" },
-            { ".srt", "application/x-subrip" },
-            { ".ttml", "application/ttml+xml" },
-            { ".wasm", "application/wasm" },
-            { ".xml", "application/xml" },
-            { ".zip", "application/zip" },
 
             // Type image
-            { ".bmp", "image/bmp" },
-            { ".gif", "image/gif" },
-            { ".ico", "image/vnd.microsoft.icon" },
-            { ".jpg", "image/jpeg" },
-            { ".jpeg", "image/jpeg" },
-            { ".png", "image/png" },
-            { ".svg", "image/svg+xml" },
-            { ".svgz", "image/svg+xml" },
             { ".tbn", "image/jpeg" },
-            { ".tif", "image/tiff" },
-            { ".tiff", "image/tiff" },
-            { ".webp", "image/webp" },
-
-            // Type font
-            { ".ttf", "font/ttf" },
-            { ".woff", "font/woff" },
-            { ".woff2", "font/woff2" },
 
             // Type text
             { ".ass", "text/x-ssa" },
             { ".ssa", "text/x-ssa" },
-            { ".css", "text/css" },
-            { ".csv", "text/csv" },
             { ".edl", "text/plain" },
-            { ".rtf", "text/rtf" },
-            { ".txt", "text/plain" },
-            { ".vtt", "text/vtt" },
+            { ".html", "text/html; charset=UTF-8" },
+            { ".htm", "text/html; charset=UTF-8" },
 
             // Type video
-            { ".3gp", "video/3gpp" },
-            { ".3g2", "video/3gpp2" },
-            { ".asf", "video/x-ms-asf" },
-            { ".avi", "video/x-msvideo" },
-            { ".flv", "video/x-flv" },
-            { ".mp4", "video/mp4" },
-            { ".m4s", "video/mp4" },
-            { ".m4v", "video/x-m4v" },
             { ".mpegts", "video/mp2t" },
-            { ".mpg", "video/mpeg" },
-            { ".mkv", "video/x-matroska" },
-            { ".mov", "video/quicktime" },
-            { ".mpd", "video/vnd.mpeg.dash.mpd" },
-            { ".ogv", "video/ogg" },
-            { ".ts", "video/mp2t" },
-            { ".webm", "video/webm" },
-            { ".wmv", "video/x-ms-wmv" },
 
             // Type audio
             { ".aac", "audio/aac" },
@@ -133,37 +87,47 @@ namespace MediaBrowser.Model.Net
             { ".dsf", "audio/dsf" },
             { ".dsp", "audio/dsp" },
             { ".flac", "audio/flac" },
-            { ".m4a", "audio/mp4" },
             { ".m4b", "audio/m4b" },
-            { ".mid", "audio/midi" },
-            { ".midi", "audio/midi" },
             { ".mp3", "audio/mpeg" },
-            { ".oga", "audio/ogg" },
-            { ".ogg", "audio/ogg" },
-            { ".opus", "audio/ogg" },
             { ".vorbis", "audio/vorbis" },
-            { ".wav", "audio/wav" },
             { ".webma", "audio/webm" },
-            { ".wma", "audio/x-ms-wma" },
             { ".wv", "audio/x-wavpack" },
             { ".xsp", "audio/xsp" },
         };
 
-        private static readonly Dictionary<string, string> _extensionLookup = CreateExtensionLookup();
-
-        private static Dictionary<string, string> CreateExtensionLookup()
+        private static readonly Dictionary<string, string> _extensionLookup = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
         {
-            var dict = _mimeTypeLookup
-                .GroupBy(i => i.Value)
-                .ToDictionary(x => x.Key, x => x.First().Key, StringComparer.OrdinalIgnoreCase);
+            // Type application
+            { "application/x-cbz", ".cbz" },
+            { "application/x-javascript", ".js" },
+            { "application/xml", ".xml" },
+            { "application/x-mpegURL", ".m3u8" },
 
-            dict["image/jpg"] = ".jpg";
-            dict["image/x-png"] = ".png";
+            // Type audio
+            { "audio/aac", ".aac" },
+            { "audio/ac3", ".ac3" },
+            { "audio/dsf", ".dsf" },
+            { "audio/dsp", ".dsp" },
+            { "audio/flac", ".flac" },
+            { "audio/m4b", ".m4b" },
+            { "audio/vorbis", ".vorbis" },
+            { "audio/x-ape", ".ape" },
+            { "audio/xsp", ".xsp" },
+            { "audio/x-wavpack", ".wv" },
 
-            dict["audio/x-aac"] = ".aac";
+            // Type image
+            { "image/jpg", ".jpg" },
+            { "image/x-png", ".png" },
 
-            return dict;
-        }
+            // Type text
+            { "text/plain", ".txt" },
+            { "text/rtf", ".rtf" },
+            { "text/x-ssa", ".ssa" },
+
+            // Type video
+            { "video/vnd.mpeg.dash.mpd", ".mpd" },
+            { "video/x-matroska", ".mkv" },
+        };
 
         public static string GetMimeType(string path) => GetMimeType(path, "application/octet-stream");
 
@@ -188,29 +152,15 @@ namespace MediaBrowser.Model.Net
                 return result;
             }
 
-            // Catch-all for all video types that don't require specific mime types
-            if (_videoFileExtensions.Contains(ext))
-            {
-                return string.Concat("video/", ext.AsSpan(1));
-            }
-
-            // Type text
-            if (string.Equals(ext, ".html", StringComparison.OrdinalIgnoreCase)
-                || string.Equals(ext, ".htm", StringComparison.OrdinalIgnoreCase))
+            if (Model.MimeTypes.TryGetMimeType(filename, out var mimeType))
             {
-                return "text/html; charset=UTF-8";
+                return mimeType;
             }
 
-            if (string.Equals(ext, ".log", StringComparison.OrdinalIgnoreCase)
-                || string.Equals(ext, ".srt", StringComparison.OrdinalIgnoreCase))
-            {
-                return "text/plain";
-            }
-
-            // Misc
-            if (string.Equals(ext, ".dll", StringComparison.OrdinalIgnoreCase))
+            // Catch-all for all video types that don't require specific mime types
+            if (_videoFileExtensions.Contains(ext))
             {
-                return "application/octet-stream";
+                return string.Concat("video/", ext.AsSpan(1));
             }
 
             return defaultValue;
@@ -231,7 +181,8 @@ namespace MediaBrowser.Model.Net
                 return result;
             }
 
-            return null;
+            var extension = Model.MimeTypes.GetMimeTypeExtensions(mimeType).FirstOrDefault();
+            return string.IsNullOrEmpty(extension) ? null : "." + extension;
         }
     }
 }

+ 20 - 43
MediaBrowser.Providers/MediaInfo/FFProbeProvider.cs

@@ -39,20 +39,12 @@ namespace MediaBrowser.Providers.MediaInfo
         IHasItemChangeMonitor
     {
         private readonly ILogger<FFProbeProvider> _logger;
-        private readonly IMediaEncoder _mediaEncoder;
-        private readonly IItemRepository _itemRepo;
-        private readonly IBlurayExaminer _blurayExaminer;
-        private readonly ILocalizationManager _localization;
-        private readonly IEncodingManager _encodingManager;
-        private readonly IServerConfigurationManager _config;
-        private readonly ISubtitleManager _subtitleManager;
-        private readonly IChapterManager _chapterManager;
-        private readonly ILibraryManager _libraryManager;
-        private readonly IMediaSourceManager _mediaSourceManager;
         private readonly SubtitleResolver _subtitleResolver;
-        private readonly Task<ItemUpdateType> _cachedTask = Task.FromResult(ItemUpdateType.None);
-        private readonly NamingOptions _namingOptions;
         private readonly AudioResolver _audioResolver;
+        private readonly FFProbeVideoInfo _videoProber;
+        private readonly FFProbeAudioInfo _audioProber;
+
+        private readonly Task<ItemUpdateType> _cachedTask = Task.FromResult(ItemUpdateType.None);
 
         public FFProbeProvider(
             ILogger<FFProbeProvider> logger,
@@ -69,20 +61,21 @@ namespace MediaBrowser.Providers.MediaInfo
             NamingOptions namingOptions)
         {
             _logger = logger;
-            _mediaEncoder = mediaEncoder;
-            _itemRepo = itemRepo;
-            _blurayExaminer = blurayExaminer;
-            _localization = localization;
-            _encodingManager = encodingManager;
-            _config = config;
-            _subtitleManager = subtitleManager;
-            _chapterManager = chapterManager;
-            _libraryManager = libraryManager;
-            _mediaSourceManager = mediaSourceManager;
-            _namingOptions = namingOptions;
-
+            _audioResolver = new AudioResolver(localization, mediaEncoder, namingOptions);
             _subtitleResolver = new SubtitleResolver(BaseItem.LocalizationManager);
-            _audioResolver = new AudioResolver(_localization, _mediaEncoder, namingOptions);
+            _videoProber = new FFProbeVideoInfo(
+                _logger,
+                mediaSourceManager,
+                mediaEncoder,
+                itemRepo,
+                blurayExaminer,
+                localization,
+                encodingManager,
+                config,
+                subtitleManager,
+                chapterManager,
+                libraryManager);
+            _audioProber = new FFProbeAudioInfo(mediaSourceManager, mediaEncoder, itemRepo, libraryManager);
         }
 
         public string Name => "ffprobe";
@@ -190,21 +183,7 @@ namespace MediaBrowser.Providers.MediaInfo
                 FetchShortcutInfo(item);
             }
 
-            var prober = new FFProbeVideoInfo(
-                _logger,
-                _mediaSourceManager,
-                _mediaEncoder,
-                _itemRepo,
-                _blurayExaminer,
-                _localization,
-                _encodingManager,
-                _config,
-                _subtitleManager,
-                _chapterManager,
-                _libraryManager,
-                _audioResolver);
-
-            return prober.ProbeVideo(item, options, cancellationToken);
+            return _videoProber.ProbeVideo(item, options, cancellationToken);
         }
 
         private string NormalizeStrmLine(string line)
@@ -240,9 +219,7 @@ namespace MediaBrowser.Providers.MediaInfo
                 FetchShortcutInfo(item);
             }
 
-            var prober = new FFProbeAudioInfo(_mediaSourceManager, _mediaEncoder, _itemRepo, _libraryManager);
-
-            return prober.Probe(item, options, cancellationToken);
+            return _audioProber.Probe(item, options, cancellationToken);
         }
     }
 }

+ 41 - 0
MediaBrowser.Providers/Plugins/Tmdb/Api/TmdbController.cs

@@ -0,0 +1,41 @@
+using System.Net.Mime;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using TMDbLib.Objects.General;
+
+namespace MediaBrowser.Providers.Plugins.Tmdb.Api
+{
+    /// <summary>
+    /// The TMDb api controller.
+    /// </summary>
+    [ApiController]
+    [Authorize(Policy = "DefaultAuthorization")]
+    [Route("[controller]")]
+    [Produces(MediaTypeNames.Application.Json)]
+    public class TmdbController : ControllerBase
+    {
+        private readonly TmdbClientManager _tmdbClientManager;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="TmdbController"/> class.
+        /// </summary>
+        /// <param name="tmdbClientManager">The TMDb client manager.</param>
+        public TmdbController(TmdbClientManager tmdbClientManager)
+        {
+            _tmdbClientManager = tmdbClientManager;
+        }
+
+        /// <summary>
+        /// Gets the TMDb image configuration options.
+        /// </summary>
+        /// <returns>The image portion of the TMDb client configuration.</returns>
+        [HttpGet("ClientConfiguration")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        public async Task<ConfigImageTypes> TmdbClientConfiguration()
+        {
+            return (await _tmdbClientManager.GetClientConfiguration().ConfigureAwait(false)).Images;
+        }
+    }
+}

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

@@ -26,5 +26,25 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
         /// Gets or sets a value indicating the maximum number of cast members to fetch for an item.
         /// </summary>
         public int MaxCastMembers { get; set; } = 15;
+
+        /// <summary>
+        /// Gets or sets a value indicating the poster image size to fetch.
+        /// </summary>
+        public string? PosterSize { get; set; }
+
+        /// <summary>
+        /// Gets or sets a value indicating the backdrop image size to fetch.
+        /// </summary>
+        public string? BackdropSize { get; set; }
+
+        /// <summary>
+        /// Gets or sets a value indicating the profile image size to fetch.
+        /// </summary>
+        public string? ProfileSize { get; set; }
+
+        /// <summary>
+        /// Gets or sets a value indicating the still image size to fetch.
+        /// </summary>
+        public string? StillSize { get; set; }
     }
 }

+ 62 - 2
MediaBrowser.Providers/Plugins/Tmdb/Configuration/config.html

@@ -24,7 +24,21 @@
                         <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 />
+                    <div class="verticalSection verticalSection-extrabottompadding">
+                        <h2>Image Scaling</h2>
+                        <div class="selectContainer">
+                            <select is="emby-select" id="selectPosterSize" label="Poster"></select>
+                        </div>
+                        <div class="selectContainer">
+                            <select is="emby-select" id="selectBackdropSize" label="Backdrop"></select>
+                        </div>
+                        <div class="selectContainer">
+                            <select is="emby-select" id="selectProfileSize" label="Profile"></select>
+                        </div>
+                        <div class="selectContainer">
+                            <select is="emby-select" id="selectStillSize" label="Still"></select>
+                        </div>
+                    </div>
                     <div>
                         <button is="emby-button" type="submit" class="raised button-submit block"><span>Save</span></button>
                     </div>
@@ -39,6 +53,47 @@
             document.querySelector('.configPage')
                 .addEventListener('pageshow', function () {
                     Dashboard.showLoadingMsg();
+
+                    var clientConfig, pluginConfig;
+                    var configureImageScaling = function() {
+                        if (clientConfig === null || pluginConfig === null) {
+                            return;
+                        }
+
+                        var sizeOptionsGenerator = function (size) {
+                            return '<option value="' + size + '">' + size + '</option>';
+                        }
+
+                        var selPosterSize = document.querySelector('#selectPosterSize');
+                        selPosterSize.innerHTML = clientConfig.PosterSizes.map(sizeOptionsGenerator);
+                        selPosterSize.value = pluginConfig.PosterSize;
+
+                        var selBackdropSize = document.querySelector('#selectBackdropSize');
+                        selBackdropSize.innerHTML = clientConfig.BackdropSizes.map(sizeOptionsGenerator);
+                        selBackdropSize.value = pluginConfig.BackdropSize;
+
+                        var selProfileSize = document.querySelector('#selectProfileSize');
+                        selProfileSize.innerHTML = clientConfig.ProfileSizes.map(sizeOptionsGenerator);
+                        selProfileSize.value = pluginConfig.ProfileSize;
+
+                        var selStillSize = document.querySelector('#selectStillSize');
+                        selStillSize.innerHTML = clientConfig.StillSizes.map(sizeOptionsGenerator);
+                        selStillSize.value = pluginConfig.StillSize;
+
+                        Dashboard.hideLoadingMsg();
+                    }
+
+                    const request = {
+                        url: ApiClient.getUrl('tmdb/ClientConfiguration'),
+                        dataType: 'json',
+                        type: 'GET',
+                        headers: { accept: 'application/json' }
+                    }
+                    ApiClient.fetch(request).then(function (config) {
+                        clientConfig = config;
+                        configureImageScaling();
+                    });
+
                     ApiClient.getPluginConfiguration(PluginConfig.pluginId).then(function (config) {
                         document.querySelector('#includeAdult').checked = config.IncludeAdult;
                         document.querySelector('#excludeTagsSeries').checked = config.ExcludeTagsSeries;
@@ -51,7 +106,8 @@
                             cancelable: false
                         }));
 
-                        Dashboard.hideLoadingMsg();
+                        pluginConfig = config;
+                        configureImageScaling();
                     });
                 });
 
@@ -65,6 +121,10 @@
                         config.ExcludeTagsSeries = document.querySelector('#excludeTagsSeries').checked;
                         config.ExcludeTagsMovies = document.querySelector('#excludeTagsMovies').checked;
                         config.MaxCastMembers = document.querySelector('#maxCastMembers').value;
+                        config.PosterSize = document.querySelector('#selectPosterSize').value;
+                        config.BackdropSize = document.querySelector('#selectBackdropSize').value;
+                        config.ProfileSize = document.querySelector('#selectProfileSize').value;
+                        config.StillSize = document.querySelector('#selectStillSize').value;
                         ApiClient.updatePluginConfiguration(PluginConfig.pluginId, config).then(Dashboard.processPluginConfigurationUpdateResult);
                     });
 

+ 57 - 11
MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs

@@ -498,7 +498,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
                 return null;
             }
 
-            return _tmDbClient.GetImageUrl(size, path).ToString();
+            return _tmDbClient.GetImageUrl(size, path, true).ToString();
         }
 
         /// <summary>
@@ -508,7 +508,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
         /// <returns>The absolute URL.</returns>
         public string GetPosterUrl(string posterPath)
         {
-            return GetUrl(_tmDbClient.Config.Images.PosterSizes[^1], posterPath);
+            return GetUrl(Plugin.Instance.Configuration.PosterSize, posterPath);
         }
 
         /// <summary>
@@ -518,7 +518,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
         /// <returns>The absolute URL.</returns>
         public string GetProfileUrl(string actorProfilePath)
         {
-            return GetUrl(_tmDbClient.Config.Images.ProfileSizes[^1], actorProfilePath);
+            return GetUrl(Plugin.Instance.Configuration.ProfileSize, actorProfilePath);
         }
 
         /// <summary>
@@ -529,7 +529,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
         /// <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);
+            ConvertToRemoteImageInfo(images, Plugin.Instance.Configuration.PosterSize, ImageType.Primary, requestLanguage, results);
         }
 
         /// <summary>
@@ -540,7 +540,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
         /// <param name="results">The collection to add the remote images into.</param>
         public void ConvertBackdropsToRemoteImageInfo(List<ImageData> images, string requestLanguage, List<RemoteImageInfo> results)
         {
-            ConvertToRemoteImageInfo(images, _tmDbClient.Config.Images.BackdropSizes[^1], ImageType.Backdrop, requestLanguage, results);
+            ConvertToRemoteImageInfo(images, Plugin.Instance.Configuration.BackdropSize, ImageType.Backdrop, requestLanguage, results);
         }
 
         /// <summary>
@@ -551,7 +551,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
         /// <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);
+            ConvertToRemoteImageInfo(images, Plugin.Instance.Configuration.ProfileSize, ImageType.Primary, requestLanguage, results);
         }
 
         /// <summary>
@@ -562,7 +562,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
         /// <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);
+            ConvertToRemoteImageInfo(images, Plugin.Instance.Configuration.StillSize, ImageType.Primary, requestLanguage, results);
         }
 
         /// <summary>
@@ -575,16 +575,20 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
         /// <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)
         {
+            // sizes provided are for original resolution, don't store them when downloading scaled images
+            var scaleImage = !string.Equals(size, "original", StringComparison.OrdinalIgnoreCase);
+
             for (var i = 0; i < images.Count; i++)
             {
                 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,
+                    Width = scaleImage ? null : image.Width,
+                    Height = scaleImage ? null : image.Height,
                     Language = TmdbUtils.AdjustImageLanguage(image.Iso_639_1, requestLanguage),
                     ProviderName = TmdbUtils.ProviderName,
                     Type = type,
@@ -593,9 +597,51 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
             }
         }
 
-        private Task EnsureClientConfigAsync()
+        private async Task EnsureClientConfigAsync()
+        {
+            if (!_tmDbClient.HasConfig)
+            {
+                var config = await _tmDbClient.GetConfigAsync().ConfigureAwait(false);
+                ValidatePreferences(config);
+            }
+        }
+
+        private static void ValidatePreferences(TMDbConfig config)
         {
-            return !_tmDbClient.HasConfig ? _tmDbClient.GetConfigAsync() : Task.CompletedTask;
+            var imageConfig = config.Images;
+
+            var pluginConfig = Plugin.Instance.Configuration;
+
+            if (!imageConfig.PosterSizes.Contains(pluginConfig.PosterSize))
+            {
+                pluginConfig.PosterSize = imageConfig.PosterSizes[^1];
+            }
+
+            if (!imageConfig.BackdropSizes.Contains(pluginConfig.BackdropSize))
+            {
+                pluginConfig.BackdropSize = imageConfig.BackdropSizes[^1];
+            }
+
+            if (!imageConfig.ProfileSizes.Contains(pluginConfig.ProfileSize))
+            {
+                pluginConfig.ProfileSize = imageConfig.ProfileSizes[^1];
+            }
+
+            if (!imageConfig.StillSizes.Contains(pluginConfig.StillSize))
+            {
+                pluginConfig.StillSize = imageConfig.StillSizes[^1];
+            }
+        }
+
+        /// <summary>
+        /// Gets the <see cref="TMDbClient"/> configuration.
+        /// </summary>
+        /// <returns>The configuration.</returns>
+        public async Task<TMDbConfig> GetClientConfiguration()
+        {
+            await EnsureClientConfigAsync().ConfigureAwait(false);
+
+            return _tmDbClient.Config;
         }
 
         /// <inheritdoc />

+ 14 - 1
debian/jellyfin.service

@@ -13,7 +13,20 @@ TimeoutSec = 15
 NoNewPrivileges=true
 SystemCallArchitectures=native
 RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 AF_NETLINK
-ProtectKernelModules=True
+RestrictNamespaces=true
+RestrictRealtime=true
+RestrictSUIDSGID=true
+ProtectClock=true
+ProtectControlGroups=true
+ProtectHostname=true
+ProtectKernelLogs=true
+ProtectKernelModules=true
+ProtectKernelTunables=true
+LockPersonality=true
+PrivateTmp=true
+PrivateDevices=false
+PrivateUsers=true
+RemoveIPC=true
 SystemCallFilter=~@clock
 SystemCallFilter=~@aio
 SystemCallFilter=~@chown

+ 164 - 0
tests/Jellyfin.Model.Tests/Net/MimeTypesTests.cs

@@ -0,0 +1,164 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using MediaBrowser.Model.Net;
+using Xunit;
+
+namespace Jellyfin.Model.Tests.Net
+{
+    public class MimeTypesTests
+    {
+        [Theory]
+        [InlineData(".dll", "application/octet-stream")]
+        [InlineData(".log", "text/plain")]
+        [InlineData(".srt", "application/x-subrip")]
+        [InlineData(".html", "text/html; charset=UTF-8")]
+        [InlineData(".htm", "text/html; charset=UTF-8")]
+        [InlineData(".7z", "application/x-7z-compressed")]
+        [InlineData(".azw", "application/vnd.amazon.ebook")]
+        [InlineData(".azw3", "application/vnd.amazon.ebook")]
+        [InlineData(".eot", "application/vnd.ms-fontobject")]
+        [InlineData(".epub", "application/epub+zip")]
+        [InlineData(".json", "application/json")]
+        [InlineData(".mobi", "application/x-mobipocket-ebook")]
+        [InlineData(".opf", "application/oebps-package+xml")]
+        [InlineData(".pdf", "application/pdf")]
+        [InlineData(".rar", "application/vnd.rar")]
+        [InlineData(".ttml", "application/ttml+xml")]
+        [InlineData(".wasm", "application/wasm")]
+        [InlineData(".xml", "application/xml")]
+        [InlineData(".zip", "application/zip")]
+        [InlineData(".bmp", "image/bmp")]
+        [InlineData(".gif", "image/gif")]
+        [InlineData(".ico", "image/vnd.microsoft.icon")]
+        [InlineData(".jpg", "image/jpeg")]
+        [InlineData(".jpeg", "image/jpeg")]
+        [InlineData(".png", "image/png")]
+        [InlineData(".svg", "image/svg+xml")]
+        [InlineData(".svgz", "image/svg+xml")]
+        [InlineData(".tbn", "image/jpeg")]
+        [InlineData(".tif", "image/tiff")]
+        [InlineData(".tiff", "image/tiff")]
+        [InlineData(".webp", "image/webp")]
+        [InlineData(".ttf", "font/ttf")]
+        [InlineData(".woff", "font/woff")]
+        [InlineData(".woff2", "font/woff2")]
+        [InlineData(".ass", "text/x-ssa")]
+        [InlineData(".ssa", "text/x-ssa")]
+        [InlineData(".css", "text/css")]
+        [InlineData(".csv", "text/csv")]
+        [InlineData(".edl", "text/plain")]
+        [InlineData(".txt", "text/plain")]
+        [InlineData(".vtt", "text/vtt")]
+        [InlineData(".3gp", "video/3gpp")]
+        [InlineData(".3g2", "video/3gpp2")]
+        [InlineData(".asf", "video/x-ms-asf")]
+        [InlineData(".avi", "video/x-msvideo")]
+        [InlineData(".flv", "video/x-flv")]
+        [InlineData(".mp4", "video/mp4")]
+        [InlineData(".m4v", "video/x-m4v")]
+        [InlineData(".mpegts", "video/mp2t")]
+        [InlineData(".mpg", "video/mpeg")]
+        [InlineData(".mkv", "video/x-matroska")]
+        [InlineData(".mov", "video/quicktime")]
+        [InlineData(".ogv", "video/ogg")]
+        [InlineData(".ts", "video/mp2t")]
+        [InlineData(".webm", "video/webm")]
+        [InlineData(".wmv", "video/x-ms-wmv")]
+        [InlineData(".aac", "audio/aac")]
+        [InlineData(".ac3", "audio/ac3")]
+        [InlineData(".ape", "audio/x-ape")]
+        [InlineData(".dsf", "audio/dsf")]
+        [InlineData(".dsp", "audio/dsp")]
+        [InlineData(".flac", "audio/flac")]
+        [InlineData(".m4a", "audio/mp4")]
+        [InlineData(".m4b", "audio/m4b")]
+        [InlineData(".mid", "audio/midi")]
+        [InlineData(".midi", "audio/midi")]
+        [InlineData(".mp3", "audio/mpeg")]
+        [InlineData(".oga", "audio/ogg")]
+        [InlineData(".ogg", "audio/ogg")]
+        [InlineData(".opus", "audio/ogg")]
+        [InlineData(".vorbis", "audio/vorbis")]
+        [InlineData(".wav", "audio/wav")]
+        [InlineData(".webma", "audio/webm")]
+        [InlineData(".wma", "audio/x-ms-wma")]
+        [InlineData(".wv", "audio/x-wavpack")]
+        [InlineData(".xsp", "audio/xsp")]
+        public void GetMimeType_Valid_ReturnsCorrectResult(string input, string expectedResult)
+        {
+            Assert.Equal(expectedResult, MimeTypes.GetMimeType(input, null));
+        }
+
+        [Theory]
+        [InlineData("application/epub+zip", ".epub")]
+        [InlineData("application/json", ".json")]
+        [InlineData("application/oebps-package+xml", ".opf")]
+        [InlineData("application/pdf", ".pdf")]
+        [InlineData("application/ttml+xml", ".ttml")]
+        [InlineData("application/vnd.amazon.ebook", ".azw")]
+        [InlineData("application/vnd.ms-fontobject", ".eot")]
+        [InlineData("application/vnd.rar", ".rar")]
+        [InlineData("application/wasm", ".wasm")]
+        [InlineData("application/x-7z-compressed", ".7z")]
+        [InlineData("application/x-cbz", ".cbz")]
+        [InlineData("application/x-javascript", ".js")]
+        [InlineData("application/x-mobipocket-ebook", ".mobi")]
+        [InlineData("application/x-mpegURL", ".m3u8")]
+        [InlineData("application/x-subrip", ".srt")]
+        [InlineData("application/xml", ".xml")]
+        [InlineData("application/zip", ".zip")]
+        [InlineData("audio/aac", ".aac")]
+        [InlineData("audio/ac3", ".ac3")]
+        [InlineData("audio/dsf", ".dsf")]
+        [InlineData("audio/dsp", ".dsp")]
+        [InlineData("audio/flac", ".flac")]
+        [InlineData("audio/m4b", ".m4b")]
+        [InlineData("audio/mp4", ".m4a")]
+        [InlineData("audio/vorbis", ".vorbis")]
+        [InlineData("audio/wav", ".wav")]
+        [InlineData("audio/x-aac", ".aac")]
+        [InlineData("audio/x-ape", ".ape")]
+        [InlineData("audio/x-ms-wma", ".wma")]
+        [InlineData("audio/x-wavpack", ".wv")]
+        [InlineData("audio/xsp", ".xsp")]
+        [InlineData("font/ttf", ".ttf")]
+        [InlineData("font/woff", ".woff")]
+        [InlineData("font/woff2", ".woff2")]
+        [InlineData("image/bmp", ".bmp")]
+        [InlineData("image/gif", ".gif")]
+        [InlineData("image/jpg", ".jpg")]
+        [InlineData("image/png", ".png")]
+        [InlineData("image/svg+xml", ".svg")]
+        [InlineData("image/tiff", ".tif")]
+        [InlineData("image/vnd.microsoft.icon", ".ico")]
+        [InlineData("image/webp", ".webp")]
+        [InlineData("image/x-png", ".png")]
+        [InlineData("text/css", ".css")]
+        [InlineData("text/csv", ".csv")]
+        [InlineData("text/plain", ".txt")]
+        [InlineData("text/rtf", ".rtf")]
+        [InlineData("text/vtt", ".vtt")]
+        [InlineData("text/x-ssa", ".ssa")]
+        [InlineData("video/3gpp", ".3gp")]
+        [InlineData("video/3gpp2", ".3g2")]
+        [InlineData("video/mp2t", ".ts")]
+        [InlineData("video/mp4", ".mp4")]
+        [InlineData("video/ogg", ".ogv")]
+        [InlineData("video/quicktime", ".mov")]
+        [InlineData("video/vnd.mpeg.dash.mpd", ".mpd")]
+        [InlineData("video/webm", ".webm")]
+        [InlineData("video/x-flv", ".flv")]
+        [InlineData("video/x-m4v", ".m4v")]
+        [InlineData("video/x-matroska", ".mkv")]
+        [InlineData("video/x-ms-asf", ".asf")]
+        [InlineData("video/x-ms-wmv", ".wmv")]
+        [InlineData("video/x-msvideo", ".avi")]
+        public void ToExtension_Valid_ReturnsCorrectResult(string input, string expectedResult)
+        {
+            Assert.Equal(expectedResult, MimeTypes.ToExtension(input));
+        }
+    }
+}

+ 3 - 3
tests/Jellyfin.Server.Integration.Tests/AuthHelper.cs

@@ -1,6 +1,7 @@
 using System;
 using System.Net;
 using System.Net.Http;
+using System.Net.Http.Json;
 using System.Net.Http.Headers;
 using System.Net.Mime;
 using System.Text.Json;
@@ -26,14 +27,13 @@ namespace Jellyfin.Server.Integration.Tests
             using var completeResponse = await client.PostAsync("/Startup/Complete", new ByteArrayContent(Array.Empty<byte>())).ConfigureAwait(false);
             Assert.Equal(HttpStatusCode.NoContent, completeResponse.StatusCode);
 
-            using var content = new ByteArrayContent(JsonSerializer.SerializeToUtf8Bytes(
+            using var content = JsonContent.Create(
                 new AuthenticateUserByName()
                 {
                     Username = user!.Name,
                     Pw = user.Password,
                 },
-                jsonOptions));
-            content.Headers.ContentType = MediaTypeHeaderValue.Parse(MediaTypeNames.Application.Json);
+                options: jsonOptions);
             content.Headers.Add("X-Emby-Authorization", DummyAuthHeader);
 
             using var authResponse = await client.PostAsync("/Users/AuthenticateByName", content).ConfigureAwait(false);

+ 4 - 9
tests/Jellyfin.Server.Integration.Tests/Controllers/DlnaControllerTests.cs

@@ -2,6 +2,7 @@ using System;
 using System.Linq;
 using System.Net;
 using System.Net.Http;
+using System.Net.Http.Json;
 using System.Net.Http.Headers;
 using System.Net.Mime;
 using System.Text;
@@ -62,9 +63,7 @@ namespace Jellyfin.Server.Integration.Tests.Controllers
                 Name = "ThisProfileDoesNotExist"
             };
 
-            using var content = new ByteArrayContent(JsonSerializer.SerializeToUtf8Bytes(deviceProfile, _jsonOptions));
-            content.Headers.ContentType = MediaTypeHeaderValue.Parse(MediaTypeNames.Application.Json);
-            using var getResponse = await client.PostAsync("/Dlna/Profiles/" + NonExistentProfile, content).ConfigureAwait(false);
+            using var getResponse = await client.PostAsJsonAsync("/Dlna/Profiles/" + NonExistentProfile, deviceProfile, _jsonOptions).ConfigureAwait(false);
             Assert.Equal(HttpStatusCode.NotFound, getResponse.StatusCode);
         }
 
@@ -80,9 +79,7 @@ namespace Jellyfin.Server.Integration.Tests.Controllers
                 Name = "ThisProfileIsNew"
             };
 
-            using var content = new ByteArrayContent(JsonSerializer.SerializeToUtf8Bytes(deviceProfile, _jsonOptions));
-            content.Headers.ContentType = MediaTypeHeaderValue.Parse(MediaTypeNames.Application.Json);
-            using var getResponse = await client.PostAsync("/Dlna/Profiles", content).ConfigureAwait(false);
+            using var getResponse = await client.PostAsJsonAsync("/Dlna/Profiles", deviceProfile, _jsonOptions).ConfigureAwait(false);
             Assert.Equal(HttpStatusCode.NoContent, getResponse.StatusCode);
         }
 
@@ -120,9 +117,7 @@ namespace Jellyfin.Server.Integration.Tests.Controllers
                 Id = _newDeviceProfileId
             };
 
-            using var content = new ByteArrayContent(JsonSerializer.SerializeToUtf8Bytes(updatedProfile, _jsonOptions));
-            content.Headers.ContentType = MediaTypeHeaderValue.Parse(MediaTypeNames.Application.Json);
-            using var getResponse = await client.PostAsync("/Dlna/Profiles", content).ConfigureAwait(false);
+            using var getResponse = await client.PostAsJsonAsync("/Dlna/Profiles", updatedProfile, _jsonOptions).ConfigureAwait(false);
             Assert.Equal(HttpStatusCode.NoContent, getResponse.StatusCode);
         }
 

+ 3 - 6
tests/Jellyfin.Server.Integration.Tests/Controllers/MediaStructureControllerTests.cs

@@ -1,6 +1,7 @@
 using System;
 using System.Net;
 using System.Net.Http;
+using System.Net.Http.Json;
 using System.Net.Http.Headers;
 using System.Net.Mime;
 using System.Text.Json;
@@ -71,9 +72,7 @@ namespace Jellyfin.Server.Integration.Tests.Controllers
                 Path = "/this/path/doesnt/exist"
             };
 
-            using var postContent = new ByteArrayContent(JsonSerializer.SerializeToUtf8Bytes(data, _jsonOptions));
-            postContent.Headers.ContentType = MediaTypeHeaderValue.Parse(MediaTypeNames.Application.Json);
-            var response = await client.PostAsync("Library/VirtualFolders/Paths", postContent).ConfigureAwait(false);
+            var response = await client.PostAsJsonAsync("Library/VirtualFolders/Paths", data, _jsonOptions).ConfigureAwait(false);
 
             Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
         }
@@ -90,9 +89,7 @@ namespace Jellyfin.Server.Integration.Tests.Controllers
                 PathInfo = new MediaPathInfo("test")
             };
 
-            using var postContent = new ByteArrayContent(JsonSerializer.SerializeToUtf8Bytes(data, _jsonOptions));
-            postContent.Headers.ContentType = MediaTypeHeaderValue.Parse(MediaTypeNames.Application.Json);
-            var response = await client.PostAsync("Library/VirtualFolders/Paths/Update", postContent).ConfigureAwait(false);
+            var response = await client.PostAsJsonAsync("Library/VirtualFolders/Paths/Update", data, _jsonOptions).ConfigureAwait(false);
 
             Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
         }

+ 3 - 6
tests/Jellyfin.Server.Integration.Tests/Controllers/StartupControllerTests.cs

@@ -1,6 +1,7 @@
 using System;
 using System.Net;
 using System.Net.Http;
+using System.Net.Http.Json;
 using System.Net.Http.Headers;
 using System.Net.Mime;
 using System.Text.Json;
@@ -36,9 +37,7 @@ namespace Jellyfin.Server.Integration.Tests.Controllers
                 PreferredMetadataLanguage = "nl"
             };
 
-            using var postContent = new ByteArrayContent(JsonSerializer.SerializeToUtf8Bytes(config, _jsonOptions));
-            postContent.Headers.ContentType = MediaTypeHeaderValue.Parse(MediaTypeNames.Application.Json);
-            using var postResponse = await client.PostAsync("/Startup/Configuration", postContent).ConfigureAwait(false);
+            using var postResponse = await client.PostAsJsonAsync("/Startup/Configuration", config, _jsonOptions).ConfigureAwait(false);
             Assert.Equal(HttpStatusCode.NoContent, postResponse.StatusCode);
 
             using var getResponse = await client.GetAsync("/Startup/Configuration").ConfigureAwait(false);
@@ -80,9 +79,7 @@ namespace Jellyfin.Server.Integration.Tests.Controllers
                 Password = "NewPassword"
             };
 
-            using var postContent = new ByteArrayContent(JsonSerializer.SerializeToUtf8Bytes(user, _jsonOptions));
-            postContent.Headers.ContentType = MediaTypeHeaderValue.Parse(MediaTypeNames.Application.Json);
-            var postResponse = await client.PostAsync("/Startup/User", postContent).ConfigureAwait(false);
+            var postResponse = await client.PostAsJsonAsync("/Startup/User", user, _jsonOptions).ConfigureAwait(false);
             Assert.Equal(HttpStatusCode.NoContent, postResponse.StatusCode);
 
             var getResponse = await client.GetAsync("/Startup/User").ConfigureAwait(false);

+ 3 - 10
tests/Jellyfin.Server.Integration.Tests/Controllers/UserControllerTests.cs

@@ -3,6 +3,7 @@ using System.Globalization;
 using System.Linq;
 using System.Net;
 using System.Net.Http;
+using System.Net.Http.Json;
 using System.Net.Http.Headers;
 using System.Net.Mime;
 using System.Text.Json;
@@ -31,18 +32,10 @@ namespace Jellyfin.Server.Integration.Tests.Controllers
         }
 
         private Task<HttpResponseMessage> CreateUserByName(HttpClient httpClient, CreateUserByName request)
-        {
-            using var postContent = new ByteArrayContent(JsonSerializer.SerializeToUtf8Bytes(request, _jsonOpions));
-            postContent.Headers.ContentType = MediaTypeHeaderValue.Parse(MediaTypeNames.Application.Json);
-            return httpClient.PostAsync("Users/New", postContent);
-        }
+            => httpClient.PostAsJsonAsync("Users/New", request, _jsonOpions);
 
         private Task<HttpResponseMessage> UpdateUserPassword(HttpClient httpClient, Guid userId, UpdateUserPassword request)
-        {
-            using var postContent = new ByteArrayContent(JsonSerializer.SerializeToUtf8Bytes(request, _jsonOpions));
-            postContent.Headers.ContentType = MediaTypeHeaderValue.Parse(MediaTypeNames.Application.Json);
-            return httpClient.PostAsync("Users/" + userId.ToString("N", CultureInfo.InvariantCulture) + "/Password", postContent);
-        }
+            => httpClient.PostAsJsonAsync("Users/" + userId.ToString("N", CultureInfo.InvariantCulture) + "/Password", request, _jsonOpions);
 
         [Fact]
         [Priority(-1)]