Browse Source

Merge remote-tracking branch 'upstream/master' into api-stream-return

crobibero 4 năm trước cách đây
mục cha
commit
612e135c8c
74 tập tin đã thay đổi với 1279 bổ sung663 xóa
  1. 11 21
      Emby.Dlna/DlnaManager.cs
  2. 2 2
      Emby.Dlna/PlayTo/SsdpHttpClient.cs
  3. 9 6
      Emby.Server.Implementations/ApplicationHost.cs
  4. 5 3
      Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs
  5. 26 26
      Emby.Server.Implementations/Localization/Core/zh-TW.json
  6. 0 6
      Emby.Server.Implementations/Udp/UdpServer.cs
  7. 9 4
      Emby.Server.Implementations/Updates/InstallationManager.cs
  8. 1 1
      Jellyfin.Api/Controllers/LiveTvController.cs
  9. 6 4
      Jellyfin.Api/Controllers/PackageController.cs
  10. 4 5
      Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs
  11. 0 1
      Jellyfin.Api/Helpers/StreamingHelpers.cs
  12. 7 35
      Jellyfin.Server.Implementations/JellyfinDb.cs
  13. 461 0
      Jellyfin.Server.Implementations/Migrations/20200905220533_FixDisplayPreferencesIndex.Designer.cs
  14. 51 0
      Jellyfin.Server.Implementations/Migrations/20200905220533_FixDisplayPreferencesIndex.cs
  15. 4 2
      Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs
  16. 49 0
      Jellyfin.Server/Configuration/CorsPolicyProvider.cs
  17. 4 5
      Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
  18. 1 1
      Jellyfin.Server/Jellyfin.Server.csproj
  19. 5 1
      Jellyfin.Server/Middleware/BaseUrlRedirectionMiddleware.cs
  20. 1 0
      Jellyfin.Server/Middleware/ExceptionMiddleware.cs
  21. 0 30
      Jellyfin.Server/Models/ServerCorsPolicy.cs
  22. 2 7
      Jellyfin.Server/Startup.cs
  23. 3 1
      MediaBrowser.Common/Updates/IInstallationManager.cs
  24. 0 2
      MediaBrowser.Controller/IServerApplicationHost.cs
  25. 6 0
      MediaBrowser.Model/Configuration/ServerConfiguration.cs
  26. 0 6
      MediaBrowser.Model/Dlna/DeviceIdentification.cs
  27. 7 1
      MediaBrowser.Model/System/PublicSystemInfo.cs
  28. 0 1
      MediaBrowser.Providers/Folders/CollectionFolderMetadataService.cs
  29. 0 0
      MediaBrowser.Providers/LiveTv/LiveTvMetadataService.cs
  30. 16 16
      MediaBrowser.Providers/Manager/ImageSaver.cs
  31. 27 29
      MediaBrowser.Providers/Manager/ItemImageProvider.cs
  32. 29 32
      MediaBrowser.Providers/Manager/MetadataService.cs
  33. 1 3
      MediaBrowser.Providers/Manager/ProviderManager.cs
  34. 2 2
      MediaBrowser.Providers/Manager/ProviderUtils.cs
  35. 15 0
      MediaBrowser.Providers/Manager/RefreshResult.cs
  36. 8 8
      MediaBrowser.Providers/MediaInfo/AudioImageProvider.cs
  37. 16 13
      MediaBrowser.Providers/MediaInfo/FFProbeAudioInfo.cs
  38. 34 38
      MediaBrowser.Providers/MediaInfo/FFProbeProvider.cs
  39. 12 11
      MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs
  40. 25 6
      MediaBrowser.Providers/MediaInfo/SubtitleDownloader.cs
  41. 11 7
      MediaBrowser.Providers/MediaInfo/SubtitleResolver.cs
  42. 49 55
      MediaBrowser.Providers/MediaInfo/SubtitleScheduledTask.cs
  43. 5 4
      MediaBrowser.Providers/MediaInfo/VideoImageProvider.cs
  44. 0 18
      MediaBrowser.Providers/Movies/ImdbExternalId.cs
  45. 27 0
      MediaBrowser.Providers/Movies/ImdbPersonExternalId.cs
  46. 0 0
      MediaBrowser.Providers/Music/AlbumInfoExtensions.cs
  47. 0 0
      MediaBrowser.Providers/Music/ImvdbId.cs
  48. 4 6
      MediaBrowser.Providers/Playlists/PlaylistItemsProvider.cs
  49. 5 5
      MediaBrowser.Providers/Plugins/AudioDb/ArtistProvider.cs
  50. 27 0
      MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumExternalId.cs
  51. 27 0
      MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistExternalId.cs
  52. 27 0
      MediaBrowser.Providers/Plugins/AudioDb/AudioDbOtherAlbumExternalId.cs
  53. 27 0
      MediaBrowser.Providers/Plugins/AudioDb/AudioDbOtherArtistExternalId.cs
  54. 0 81
      MediaBrowser.Providers/Plugins/AudioDb/ExternalIds.cs
  55. 6 6
      MediaBrowser.Providers/Plugins/AudioDb/Plugin.cs
  56. 15 14
      MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs
  57. 6 6
      MediaBrowser.Providers/Plugins/Omdb/OmdbImageProvider.cs
  58. 5 5
      MediaBrowser.Providers/Plugins/Omdb/OmdbItemProvider.cs
  59. 3 6
      MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetProvider.cs
  60. 5 5
      MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs
  61. 2 2
      MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbSearch.cs
  62. 2 2
      MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs
  63. 1 1
      MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs
  64. 8 2
      MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProviderBase.cs
  65. 8 3
      MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs
  66. 9 7
      MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs
  67. 9 5
      MediaBrowser.Providers/TV/DummySeasonProvider.cs
  68. 20 11
      MediaBrowser.Providers/TV/MissingEpisodeProvider.cs
  69. 0 82
      MediaBrowser.Providers/TV/TvExternalIds.cs
  70. 28 0
      MediaBrowser.Providers/TV/TvdbEpisodeExternalId.cs
  71. 28 0
      MediaBrowser.Providers/TV/TvdbExternalId.cs
  72. 28 0
      MediaBrowser.Providers/TV/TvdbSeasonExternalId.cs
  73. 28 0
      MediaBrowser.Providers/TV/Zap2ItExternalId.cs
  74. 0 1
      MediaBrowser.sln

+ 11 - 21
Emby.Dlna/DlnaManager.cs

@@ -126,7 +126,6 @@ namespace Emby.Dlna
             var builder = new StringBuilder();
 
             builder.AppendLine("No matching device profile found. The default will need to be used.");
-            builder.AppendFormat(CultureInfo.InvariantCulture, "DeviceDescription:{0}", profile.DeviceDescription ?? string.Empty).AppendLine();
             builder.AppendFormat(CultureInfo.InvariantCulture, "FriendlyName:{0}", profile.FriendlyName ?? string.Empty).AppendLine();
             builder.AppendFormat(CultureInfo.InvariantCulture, "Manufacturer:{0}", profile.Manufacturer ?? string.Empty).AppendLine();
             builder.AppendFormat(CultureInfo.InvariantCulture, "ManufacturerUrl:{0}", profile.ManufacturerUrl ?? string.Empty).AppendLine();
@@ -141,17 +140,9 @@ namespace Emby.Dlna
 
         private bool IsMatch(DeviceIdentification deviceInfo, DeviceIdentification profileInfo)
         {
-            if (!string.IsNullOrEmpty(profileInfo.DeviceDescription))
-            {
-                if (deviceInfo.DeviceDescription == null || !IsRegexMatch(deviceInfo.DeviceDescription, profileInfo.DeviceDescription))
-                {
-                    return false;
-                }
-            }
-
             if (!string.IsNullOrEmpty(profileInfo.FriendlyName))
             {
-                if (deviceInfo.FriendlyName == null || !IsRegexMatch(deviceInfo.FriendlyName, profileInfo.FriendlyName))
+                if (deviceInfo.FriendlyName == null || !IsRegexOrSubstringMatch(deviceInfo.FriendlyName, profileInfo.FriendlyName))
                 {
                     return false;
                 }
@@ -159,7 +150,7 @@ namespace Emby.Dlna
 
             if (!string.IsNullOrEmpty(profileInfo.Manufacturer))
             {
-                if (deviceInfo.Manufacturer == null || !IsRegexMatch(deviceInfo.Manufacturer, profileInfo.Manufacturer))
+                if (deviceInfo.Manufacturer == null || !IsRegexOrSubstringMatch(deviceInfo.Manufacturer, profileInfo.Manufacturer))
                 {
                     return false;
                 }
@@ -167,7 +158,7 @@ namespace Emby.Dlna
 
             if (!string.IsNullOrEmpty(profileInfo.ManufacturerUrl))
             {
-                if (deviceInfo.ManufacturerUrl == null || !IsRegexMatch(deviceInfo.ManufacturerUrl, profileInfo.ManufacturerUrl))
+                if (deviceInfo.ManufacturerUrl == null || !IsRegexOrSubstringMatch(deviceInfo.ManufacturerUrl, profileInfo.ManufacturerUrl))
                 {
                     return false;
                 }
@@ -175,7 +166,7 @@ namespace Emby.Dlna
 
             if (!string.IsNullOrEmpty(profileInfo.ModelDescription))
             {
-                if (deviceInfo.ModelDescription == null || !IsRegexMatch(deviceInfo.ModelDescription, profileInfo.ModelDescription))
+                if (deviceInfo.ModelDescription == null || !IsRegexOrSubstringMatch(deviceInfo.ModelDescription, profileInfo.ModelDescription))
                 {
                     return false;
                 }
@@ -183,7 +174,7 @@ namespace Emby.Dlna
 
             if (!string.IsNullOrEmpty(profileInfo.ModelName))
             {
-                if (deviceInfo.ModelName == null || !IsRegexMatch(deviceInfo.ModelName, profileInfo.ModelName))
+                if (deviceInfo.ModelName == null || !IsRegexOrSubstringMatch(deviceInfo.ModelName, profileInfo.ModelName))
                 {
                     return false;
                 }
@@ -191,7 +182,7 @@ namespace Emby.Dlna
 
             if (!string.IsNullOrEmpty(profileInfo.ModelNumber))
             {
-                if (deviceInfo.ModelNumber == null || !IsRegexMatch(deviceInfo.ModelNumber, profileInfo.ModelNumber))
+                if (deviceInfo.ModelNumber == null || !IsRegexOrSubstringMatch(deviceInfo.ModelNumber, profileInfo.ModelNumber))
                 {
                     return false;
                 }
@@ -199,7 +190,7 @@ namespace Emby.Dlna
 
             if (!string.IsNullOrEmpty(profileInfo.ModelUrl))
             {
-                if (deviceInfo.ModelUrl == null || !IsRegexMatch(deviceInfo.ModelUrl, profileInfo.ModelUrl))
+                if (deviceInfo.ModelUrl == null || !IsRegexOrSubstringMatch(deviceInfo.ModelUrl, profileInfo.ModelUrl))
                 {
                     return false;
                 }
@@ -207,7 +198,7 @@ namespace Emby.Dlna
 
             if (!string.IsNullOrEmpty(profileInfo.SerialNumber))
             {
-                if (deviceInfo.SerialNumber == null || !IsRegexMatch(deviceInfo.SerialNumber, profileInfo.SerialNumber))
+                if (deviceInfo.SerialNumber == null || !IsRegexOrSubstringMatch(deviceInfo.SerialNumber, profileInfo.SerialNumber))
                 {
                     return false;
                 }
@@ -216,11 +207,11 @@ namespace Emby.Dlna
             return true;
         }
 
-        private bool IsRegexMatch(string input, string pattern)
+        private bool IsRegexOrSubstringMatch(string input, string pattern)
         {
             try
             {
-                return Regex.IsMatch(input, pattern);
+                return input.Contains(pattern, StringComparison.OrdinalIgnoreCase) || Regex.IsMatch(input, pattern, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
             }
             catch (ArgumentException ex)
             {
@@ -511,8 +502,7 @@ namespace Emby.Dlna
 
         public string GetServerDescriptionXml(IHeaderDictionary headers, string serverUuId, string serverAddress)
         {
-            var profile = GetProfile(headers) ??
-                          GetDefaultProfile();
+            var profile = GetDefaultProfile();
 
             var serverId = _appHost.SystemId;
 

+ 2 - 2
Emby.Dlna/PlayTo/SsdpHttpClient.cs

@@ -101,7 +101,7 @@ namespace Emby.Dlna.PlayTo
                 LoadOptions.PreserveWhitespace);
         }
 
-        private Task<HttpResponseMessage> PostSoapDataAsync(
+        private async Task<HttpResponseMessage> PostSoapDataAsync(
             string url,
             string soapAction,
             string postData,
@@ -126,7 +126,7 @@ namespace Emby.Dlna.PlayTo
 
             options.Content = new StringContent(postData, Encoding.UTF8, MediaTypeNames.Text.Xml);
 
-            return _httpClientFactory.CreateClient(NamedClient.Default).SendAsync(options, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
+            return await _httpClientFactory.CreateClient(NamedClient.Default).SendAsync(options, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
         }
     }
 }

+ 9 - 6
Emby.Server.Implementations/ApplicationHost.cs

@@ -238,8 +238,14 @@ namespace Emby.Server.Implementations
         public IServerConfigurationManager ServerConfigurationManager => (IServerConfigurationManager)ConfigurationManager;
 
         /// <summary>
-        /// Initializes a new instance of the <see cref="ApplicationHost" /> class.
+        /// Initializes a new instance of the <see cref="ApplicationHost"/> class.
         /// </summary>
+        /// <param name="applicationPaths">Instance of the <see cref="IServerApplicationPaths"/> interface.</param>
+        /// <param name="loggerFactory">Instance of the <see cref="ILoggerFactory"/> interface.</param>
+        /// <param name="options">Instance of the <see cref="IStartupOptions"/> interface.</param>
+        /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
+        /// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
+        /// <param name="serviceCollection">Instance of the <see cref="IServiceCollection"/> interface.</param>
         public ApplicationHost(
             IServerApplicationPaths applicationPaths,
             ILoggerFactory loggerFactory,
@@ -1137,7 +1143,8 @@ namespace Emby.Server.Implementations
                 Id = SystemId,
                 OperatingSystem = OperatingSystem.Id.ToString(),
                 ServerName = FriendlyName,
-                LocalAddress = localAddress
+                LocalAddress = localAddress,
+                StartupWizardCompleted = ConfigurationManager.CommonConfiguration.IsStartupWizardCompleted
             };
         }
 
@@ -1431,10 +1438,6 @@ namespace Emby.Server.Implementations
             }
         }
 
-        public virtual void EnableLoopback(string appName)
-        {
-        }
-
         private bool _disposed = false;
 
         /// <summary>

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

@@ -468,13 +468,15 @@ namespace Emby.Server.Implementations.LiveTv.Listings
 
             imageIdString = imageIdString.TrimEnd(',') + "]";
 
-            using var message = new HttpRequestMessage(HttpMethod.Post, ApiUrl + "/metadata/programs");
-            message.Content = new StringContent(imageIdString, Encoding.UTF8, MediaTypeNames.Application.Json);
+            using var message = new HttpRequestMessage(HttpMethod.Post, ApiUrl + "/metadata/programs")
+            {
+                Content = new StringContent(imageIdString, Encoding.UTF8, MediaTypeNames.Application.Json)
+            };
 
             try
             {
                 using var innerResponse2 = await Send(message, true, info, cancellationToken).ConfigureAwait(false);
-                await using var response = await innerResponse2.Content.ReadAsStreamAsync();
+                await using var response = await innerResponse2.Content.ReadAsStreamAsync().ConfigureAwait(false);
                 return await _jsonSerializer.DeserializeFromStreamAsync<List<ScheduleDirect.ShowImages>>(
                     response).ConfigureAwait(false);
             }

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

@@ -1,6 +1,6 @@
 {
     "Albums": "專輯",
-    "AppDeviceValues": "軟體: {0}, 裝置: {1}",
+    "AppDeviceValues": "軟體:{0},裝置:{1}",
     "Application": "應用程式",
     "Artists": "演出者",
     "AuthenticationSucceededWithUserName": "{0} 成功授權",
@@ -11,7 +11,7 @@
     "Collections": "合輯",
     "DeviceOfflineWithName": "{0} 已經斷線",
     "DeviceOnlineWithName": "{0} 已經連線",
-    "FailedLoginAttemptWithUserName": "來自 {0} 的失敗登入嘗試",
+    "FailedLoginAttemptWithUserName": "來自使用者 {0} 的失敗登入",
     "Favorites": "我的最愛",
     "Folders": "資料夾",
     "Genres": "風格",
@@ -28,8 +28,8 @@
     "HomeVideos": "自製影片",
     "ItemAddedWithName": "{0} 已新增至媒體庫",
     "ItemRemovedWithName": "{0} 已從媒體庫移除",
-    "LabelIpAddressValue": "IP 位置: {0}",
-    "LabelRunningTimeValue": "運行時間: {0}",
+    "LabelIpAddressValue": "IP 位址:{0}",
+    "LabelRunningTimeValue": "運行時間{0}",
     "Latest": "最新",
     "MessageApplicationUpdated": "Jellyfin Server 已經更新",
     "MessageApplicationUpdatedTo": "Jellyfin Server 已經更新至 {0}",
@@ -42,18 +42,18 @@
     "NameInstallFailed": "{0} 安裝失敗",
     "NameSeasonNumber": "第 {0} 季",
     "NameSeasonUnknown": "未知季數",
-    "NewVersionIsAvailable": "新版本的Jellyfin Server 軟體已經推出可供下載。",
+    "NewVersionIsAvailable": "新版本的 Jellyfin Server 軟體已經可供下載。",
     "NotificationOptionApplicationUpdateAvailable": "有可用的應用程式更新",
-    "NotificationOptionApplicationUpdateInstalled": "應用程式已更新",
+    "NotificationOptionApplicationUpdateInstalled": "軟體更新已安裝",
     "NotificationOptionAudioPlayback": "音樂開始播放",
     "NotificationOptionAudioPlaybackStopped": "音樂停止播放",
     "NotificationOptionCameraImageUploaded": "相機相片已上傳",
     "NotificationOptionInstallationFailed": "安裝失敗",
     "NotificationOptionNewLibraryContent": "已新增新內容",
-    "NotificationOptionPluginError": "插件安裝錯誤",
-    "NotificationOptionPluginInstalled": "插件已安裝",
-    "NotificationOptionPluginUninstalled": "插件已移除",
-    "NotificationOptionPluginUpdateInstalled": "插件已更新",
+    "NotificationOptionPluginError": "外掛安裝失敗",
+    "NotificationOptionPluginInstalled": "外掛已安裝",
+    "NotificationOptionPluginUninstalled": "外掛已移除",
+    "NotificationOptionPluginUpdateInstalled": "外掛已更新",
     "NotificationOptionServerRestartRequired": "伺服器需要重新啟動",
     "NotificationOptionTaskFailed": "排程任務失敗",
     "NotificationOptionUserLockedOut": "使用者已鎖定",
@@ -61,14 +61,14 @@
     "NotificationOptionVideoPlaybackStopped": "影片停止播放",
     "Photos": "相片",
     "Playlists": "播放清單",
-    "Plugin": "插件",
+    "Plugin": "外掛",
     "PluginInstalledWithName": "{0} 已安裝",
     "PluginUninstalledWithName": "{0} 已移除",
     "PluginUpdatedWithName": "{0} 已更新",
     "ProviderValue": "提供商: {0}",
-    "ScheduledTaskFailedWithName": "{0} 已失敗",
-    "ScheduledTaskStartedWithName": "{0} 已開始",
-    "ServerNameNeedsToBeRestarted": "{0} 需要重新啟動",
+    "ScheduledTaskFailedWithName": "排程任務 {0} 已失敗",
+    "ScheduledTaskStartedWithName": "排程任務 {0} 已開始",
+    "ServerNameNeedsToBeRestarted": "伺服器 {0} 需要重新啟動",
     "Shows": "節目",
     "Songs": "歌曲",
     "StartupEmbyServerIsLoading": "Jellyfin Server正在啟動,請稍後再試一次。",
@@ -78,10 +78,10 @@
     "User": "使用者",
     "UserCreatedWithName": "使用者 {0} 已建立",
     "UserDeletedWithName": "使用者 {0} 已移除",
-    "UserDownloadingItemWithValues": "{0} 正在下載 {1}",
+    "UserDownloadingItemWithValues": "使用者 {0} 正在下載 {1}",
     "UserLockedOutWithName": "使用者 {0} 已鎖定",
-    "UserOfflineFromDevice": "{0} 已從 {1} 斷線",
-    "UserOnlineFromDevice": "{0} 已連線,來自 {1}",
+    "UserOfflineFromDevice": "使用者 {0} 已從 {1} 斷線",
+    "UserOnlineFromDevice": "使用者 {0} 已從 {1} 連線",
     "UserPasswordChangedWithName": "使用者 {0} 的密碼已變更",
     "UserPolicyUpdatedWithName": "使用者條約已更新為 {0}",
     "UserStartedPlayingItemWithValues": "{0}正在使用 {2} 播放 {1}",
@@ -95,23 +95,23 @@
     "TaskDownloadMissingSubtitlesDescription": "在網路上透過中繼資料搜尋遺失的字幕。",
     "TaskDownloadMissingSubtitles": "下載遺失的字幕",
     "TaskRefreshChannels": "重新整理頻道",
-    "TaskUpdatePlugins": "更新插件",
+    "TaskUpdatePlugins": "更新外掛",
     "TaskRefreshPeople": "重新整理人員",
-    "TaskCleanLogsDescription": "刪除超過{0}天的紀錄檔。",
+    "TaskCleanLogsDescription": "刪除超過 {0} 天的紀錄檔。",
     "TaskCleanLogs": "清空紀錄資料夾",
-    "TaskRefreshLibraryDescription": "掃描媒體庫內新的檔案並重新整理描述資料。",
-    "TaskRefreshLibrary": "掃描媒體庫",
+    "TaskRefreshLibraryDescription": "重新掃描媒體庫的新檔案並更新描述資料。",
+    "TaskRefreshLibrary": "重新掃描媒體庫",
     "TaskRefreshChapterImages": "擷取章節圖片",
-    "TaskCleanCacheDescription": "刪除系統長時間不需要的快取。",
+    "TaskCleanCacheDescription": "刪除系統不需要的快取。",
     "TaskCleanCache": "清除快取資料夾",
     "TasksLibraryCategory": "媒體庫",
-    "TaskRefreshChannelsDescription": "重新整理網頻道資料。",
+    "TaskRefreshChannelsDescription": "重新整理網頻道資料。",
     "TaskCleanTranscodeDescription": "刪除超過一天的轉碼檔案。",
     "TaskCleanTranscode": "清除轉碼資料夾",
-    "TaskUpdatePluginsDescription": "下載並安裝配置為自動更新的插件的更新。",
+    "TaskUpdatePluginsDescription": "為設置自動更新的外掛下載並安裝更新。",
     "TaskRefreshPeopleDescription": "更新媒體庫中演員和導演的中繼資料。",
-    "TaskRefreshChapterImagesDescription": "為有章節的視頻創建縮圖。",
-    "TasksChannelsCategory": "網頻道",
+    "TaskRefreshChapterImagesDescription": "為有章節的影片建立縮圖。",
+    "TasksChannelsCategory": "網頻道",
     "TasksApplicationCategory": "應用程式",
     "TasksMaintenanceCategory": "維修"
 }

+ 0 - 6
Emby.Server.Implementations/Udp/UdpServer.cs

@@ -68,12 +68,6 @@ namespace Emby.Server.Implementations.Udp
                 {
                     _logger.LogError(ex, "Error sending response message");
                 }
-
-                var parts = messageText.Split('|');
-                if (parts.Length > 1)
-                {
-                    _appHost.EnableLoopback(parts[1]);
-                }
             }
             else
             {

+ 9 - 4
Emby.Server.Implementations/Updates/InstallationManager.cs

@@ -183,7 +183,8 @@ namespace Emby.Server.Implementations.Updates
             IEnumerable<PackageInfo> availablePackages,
             string name = null,
             Guid guid = default,
-            Version minVersion = null)
+            Version minVersion = null,
+            Version specificVersion = null)
         {
             var package = FilterPackages(availablePackages, name, guid).FirstOrDefault();
 
@@ -197,7 +198,11 @@ namespace Emby.Server.Implementations.Updates
             var availableVersions = package.versions
                 .Where(x => Version.Parse(x.targetAbi) <= appVer);
 
-            if (minVersion != null)
+            if (specificVersion != null)
+            {
+                availableVersions = availableVersions.Where(x => new Version(x.version) == specificVersion);
+            }
+            else if (minVersion != null)
             {
                 availableVersions = availableVersions.Where(x => new Version(x.version) >= minVersion);
             }
@@ -227,8 +232,8 @@ namespace Emby.Server.Implementations.Updates
         {
             foreach (var plugin in _applicationHost.Plugins)
             {
-                var compatibleversions = GetCompatibleVersions(pluginCatalog, plugin.Name, plugin.Id, plugin.Version);
-                var version = compatibleversions.FirstOrDefault(y => y.Version > plugin.Version);
+                var compatibleVersions = GetCompatibleVersions(pluginCatalog, plugin.Name, plugin.Id, minVersion: plugin.Version);
+                var version = compatibleVersions.FirstOrDefault(y => y.Version > plugin.Version);
                 if (version != null && CompletedInstallations.All(x => x.Guid != version.Guid))
                 {
                     yield return version;

+ 1 - 1
Jellyfin.Api/Controllers/LiveTvController.cs

@@ -448,7 +448,7 @@ namespace Jellyfin.Api.Controllers
         [HttpGet("Timers/{timerId}")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [Authorize(Policy = Policies.DefaultAuthorization)]
-        public async Task<ActionResult<TimerInfoDto>> GetTimer(string timerId)
+        public async Task<ActionResult<TimerInfoDto>> GetTimer([FromRoute, Required] string timerId)
         {
             return await _liveTvManager.GetTimer(timerId, CancellationToken.None).ConfigureAwait(false);
         }

+ 6 - 4
Jellyfin.Api/Controllers/PackageController.cs

@@ -49,9 +49,10 @@ namespace Jellyfin.Api.Controllers
         {
             var packages = await _installationManager.GetAvailablePackages().ConfigureAwait(false);
             var result = _installationManager.FilterPackages(
-                packages,
-                name,
-                string.IsNullOrEmpty(assemblyGuid) ? default : Guid.Parse(assemblyGuid)).FirstOrDefault();
+                    packages,
+                    name,
+                    string.IsNullOrEmpty(assemblyGuid) ? default : Guid.Parse(assemblyGuid))
+                .FirstOrDefault();
 
             return result;
         }
@@ -93,7 +94,8 @@ namespace Jellyfin.Api.Controllers
                     packages,
                     name,
                     string.IsNullOrEmpty(assemblyGuid) ? Guid.Empty : Guid.Parse(assemblyGuid),
-                    string.IsNullOrEmpty(version) ? null : Version.Parse(version)).FirstOrDefault();
+                    specificVersion: string.IsNullOrEmpty(version) ? null : Version.Parse(version))
+                .FirstOrDefault();
 
             if (package == null)
             {

+ 4 - 5
Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs

@@ -1,4 +1,4 @@
-using System;
+using System;
 using System.IO;
 using System.Net.Http;
 using System.Threading;
@@ -123,10 +123,9 @@ namespace Jellyfin.Api.Helpers
                     state.Dispose();
                 }
 
-                var memoryStream = new MemoryStream();
-                await new ProgressiveFileCopier(outputPath, job, transcodingJobHelper, CancellationToken.None).WriteToAsync(memoryStream, CancellationToken.None).ConfigureAwait(false);
-                memoryStream.Position = 0;
-                return new FileStreamResult(memoryStream, contentType);
+                await new ProgressiveFileCopier(outputPath, job, transcodingJobHelper, CancellationToken.None)
+                    .WriteToAsync(httpContext.Response.Body, CancellationToken.None).ConfigureAwait(false);
+                return new FileStreamResult(httpContext.Response.Body, contentType);
             }
             finally
             {

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

@@ -92,7 +92,6 @@ 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)

+ 7 - 35
Jellyfin.Server.Implementations/JellyfinDb.cs

@@ -1,6 +1,5 @@
 #pragma warning disable CS1591
 
-using System;
 using System.Linq;
 using Jellyfin.Data.Entities;
 using Jellyfin.Data.Interfaces;
@@ -9,7 +8,7 @@ using Microsoft.EntityFrameworkCore;
 namespace Jellyfin.Server.Implementations
 {
     /// <inheritdoc/>
-    public partial class JellyfinDb : DbContext
+    public class JellyfinDb : DbContext
     {
         /// <summary>
         /// Initializes a new instance of the <see cref="JellyfinDb"/> class.
@@ -138,47 +137,20 @@ namespace Jellyfin.Server.Implementations
             return base.SaveChanges();
         }
 
-        /// <inheritdoc/>
-        public override void Dispose()
-        {
-            foreach (var entry in ChangeTracker.Entries())
-            {
-                entry.State = EntityState.Detached;
-            }
-
-            GC.SuppressFinalize(this);
-            base.Dispose();
-        }
-
-        /// <inheritdoc />
-        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
-        {
-            CustomInit(optionsBuilder);
-        }
-
         /// <inheritdoc />
         protected override void OnModelCreating(ModelBuilder modelBuilder)
         {
             base.OnModelCreating(modelBuilder);
-            OnModelCreatingImpl(modelBuilder);
 
             modelBuilder.HasDefaultSchema("jellyfin");
 
-            /*modelBuilder.Entity<Artwork>().HasIndex(t => t.Kind);
-
-            modelBuilder.Entity<Genre>().HasIndex(t => t.Name)
-                        .IsUnique();
+            modelBuilder.Entity<DisplayPreferences>()
+                .HasIndex(entity => entity.UserId)
+                .IsUnique(false);
 
-            modelBuilder.Entity<LibraryItem>().HasIndex(t => t.UrlId)
-                        .IsUnique();*/
-
-            OnModelCreatedImpl(modelBuilder);
+            modelBuilder.Entity<DisplayPreferences>()
+                .HasIndex(entity => new { entity.UserId, entity.Client })
+                .IsUnique();
         }
-
-        partial void CustomInit(DbContextOptionsBuilder optionsBuilder);
-
-        partial void OnModelCreatingImpl(ModelBuilder modelBuilder);
-
-        partial void OnModelCreatedImpl(ModelBuilder modelBuilder);
     }
 }

+ 461 - 0
Jellyfin.Server.Implementations/Migrations/20200905220533_FixDisplayPreferencesIndex.Designer.cs

@@ -0,0 +1,461 @@
+#pragma warning disable CS1591
+
+// <auto-generated />
+using System;
+using Jellyfin.Server.Implementations;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+namespace Jellyfin.Server.Implementations.Migrations
+{
+    [DbContext(typeof(JellyfinDb))]
+    [Migration("20200905220533_FixDisplayPreferencesIndex")]
+    partial class FixDisplayPreferencesIndex
+    {
+        protected override void BuildTargetModel(ModelBuilder modelBuilder)
+        {
+#pragma warning disable 612, 618
+            modelBuilder
+                .HasDefaultSchema("jellyfin")
+                .HasAnnotation("ProductVersion", "3.1.7");
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("DayOfWeek")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<double>("EndHour")
+                        .HasColumnType("REAL");
+
+                    b.Property<double>("StartHour")
+                        .HasColumnType("REAL");
+
+                    b.Property<Guid>("UserId")
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("UserId");
+
+                    b.ToTable("AccessSchedules");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.ActivityLog", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<DateTime>("DateCreated")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("ItemId")
+                        .HasColumnType("TEXT")
+                        .HasMaxLength(256);
+
+                    b.Property<int>("LogSeverity")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Name")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasMaxLength(512);
+
+                    b.Property<string>("Overview")
+                        .HasColumnType("TEXT")
+                        .HasMaxLength(512);
+
+                    b.Property<uint>("RowVersion")
+                        .IsConcurrencyToken()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("ShortOverview")
+                        .HasColumnType("TEXT")
+                        .HasMaxLength(512);
+
+                    b.Property<string>("Type")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasMaxLength(256);
+
+                    b.Property<Guid>("UserId")
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("Id");
+
+                    b.ToTable("ActivityLogs");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("ChromecastVersion")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Client")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasMaxLength(32);
+
+                    b.Property<string>("DashboardTheme")
+                        .HasColumnType("TEXT")
+                        .HasMaxLength(32);
+
+                    b.Property<bool>("EnableNextVideoInfoOverlay")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int?>("IndexBy")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("ScrollDirection")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("ShowBackdrop")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("ShowSidebar")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("SkipBackwardLength")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("SkipForwardLength")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("TvHome")
+                        .HasColumnType("TEXT")
+                        .HasMaxLength(32);
+
+                    b.Property<Guid>("UserId")
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("UserId");
+
+                    b.HasIndex("UserId", "Client")
+                        .IsUnique();
+
+                    b.ToTable("DisplayPreferences");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("DisplayPreferencesId")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("Order")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("Type")
+                        .HasColumnType("INTEGER");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("DisplayPreferencesId");
+
+                    b.ToTable("HomeSection");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<DateTime>("LastModified")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Path")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasMaxLength(512);
+
+                    b.Property<Guid?>("UserId")
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("UserId")
+                        .IsUnique();
+
+                    b.ToTable("ImageInfos");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Client")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasMaxLength(32);
+
+                    b.Property<int?>("IndexBy")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<bool>("RememberIndexing")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("RememberSorting")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("SortBy")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasMaxLength(64);
+
+                    b.Property<int>("SortOrder")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid>("UserId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int>("ViewType")
+                        .HasColumnType("INTEGER");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("UserId");
+
+                    b.ToTable("ItemDisplayPreferences");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("Kind")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid?>("Permission_Permissions_Guid")
+                        .HasColumnType("TEXT");
+
+                    b.Property<uint>("RowVersion")
+                        .IsConcurrencyToken()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("Value")
+                        .HasColumnType("INTEGER");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("Permission_Permissions_Guid");
+
+                    b.ToTable("Permissions");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("Kind")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid?>("Preference_Preferences_Guid")
+                        .HasColumnType("TEXT");
+
+                    b.Property<uint>("RowVersion")
+                        .IsConcurrencyToken()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Value")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasMaxLength(65535);
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("Preference_Preferences_Guid");
+
+                    b.ToTable("Preferences");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.User", b =>
+                {
+                    b.Property<Guid>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("AudioLanguagePreference")
+                        .HasColumnType("TEXT")
+                        .HasMaxLength(255);
+
+                    b.Property<string>("AuthenticationProviderId")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasMaxLength(255);
+
+                    b.Property<bool>("DisplayCollectionsView")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("DisplayMissingEpisodes")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("EasyPassword")
+                        .HasColumnType("TEXT")
+                        .HasMaxLength(65535);
+
+                    b.Property<bool>("EnableAutoLogin")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("EnableLocalPassword")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("EnableNextEpisodeAutoPlay")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("EnableUserPreferenceAccess")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("HidePlayedInLatest")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<long>("InternalId")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("InvalidLoginAttemptCount")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<DateTime?>("LastActivityDate")
+                        .HasColumnType("TEXT");
+
+                    b.Property<DateTime?>("LastLoginDate")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int?>("LoginAttemptsBeforeLockout")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int?>("MaxParentalAgeRating")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("MustUpdatePassword")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Password")
+                        .HasColumnType("TEXT")
+                        .HasMaxLength(65535);
+
+                    b.Property<string>("PasswordResetProviderId")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasMaxLength(255);
+
+                    b.Property<bool>("PlayDefaultAudioTrack")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("RememberAudioSelections")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("RememberSubtitleSelections")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int?>("RemoteClientBitrateLimit")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<uint>("RowVersion")
+                        .IsConcurrencyToken()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("SubtitleLanguagePreference")
+                        .HasColumnType("TEXT")
+                        .HasMaxLength(255);
+
+                    b.Property<int>("SubtitleMode")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("SyncPlayAccess")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Username")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasMaxLength(255);
+
+                    b.HasKey("Id");
+
+                    b.ToTable("Users");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
+                {
+                    b.HasOne("Jellyfin.Data.Entities.User", null)
+                        .WithMany("AccessSchedules")
+                        .HasForeignKey("UserId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
+                {
+                    b.HasOne("Jellyfin.Data.Entities.User", null)
+                        .WithOne("DisplayPreferences")
+                        .HasForeignKey("Jellyfin.Data.Entities.DisplayPreferences", "UserId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b =>
+                {
+                    b.HasOne("Jellyfin.Data.Entities.DisplayPreferences", null)
+                        .WithMany("HomeSections")
+                        .HasForeignKey("DisplayPreferencesId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b =>
+                {
+                    b.HasOne("Jellyfin.Data.Entities.User", null)
+                        .WithOne("ProfileImage")
+                        .HasForeignKey("Jellyfin.Data.Entities.ImageInfo", "UserId");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b =>
+                {
+                    b.HasOne("Jellyfin.Data.Entities.User", null)
+                        .WithMany("ItemDisplayPreferences")
+                        .HasForeignKey("UserId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b =>
+                {
+                    b.HasOne("Jellyfin.Data.Entities.User", null)
+                        .WithMany("Permissions")
+                        .HasForeignKey("Permission_Permissions_Guid");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b =>
+                {
+                    b.HasOne("Jellyfin.Data.Entities.User", null)
+                        .WithMany("Preferences")
+                        .HasForeignKey("Preference_Preferences_Guid");
+                });
+#pragma warning restore 612, 618
+        }
+    }
+}

+ 51 - 0
Jellyfin.Server.Implementations/Migrations/20200905220533_FixDisplayPreferencesIndex.cs

@@ -0,0 +1,51 @@
+#pragma warning disable CS1591
+#pragma warning disable SA1601
+
+using Microsoft.EntityFrameworkCore.Migrations;
+
+namespace Jellyfin.Server.Implementations.Migrations
+{
+    public partial class FixDisplayPreferencesIndex : Migration
+    {
+        protected override void Up(MigrationBuilder migrationBuilder)
+        {
+            migrationBuilder.DropIndex(
+                name: "IX_DisplayPreferences_UserId",
+                schema: "jellyfin",
+                table: "DisplayPreferences");
+
+            migrationBuilder.CreateIndex(
+                name: "IX_DisplayPreferences_UserId",
+                schema: "jellyfin",
+                table: "DisplayPreferences",
+                column: "UserId");
+
+            migrationBuilder.CreateIndex(
+                name: "IX_DisplayPreferences_UserId_Client",
+                schema: "jellyfin",
+                table: "DisplayPreferences",
+                columns: new[] { "UserId", "Client" },
+                unique: true);
+        }
+
+        protected override void Down(MigrationBuilder migrationBuilder)
+        {
+            migrationBuilder.DropIndex(
+                name: "IX_DisplayPreferences_UserId",
+                schema: "jellyfin",
+                table: "DisplayPreferences");
+
+            migrationBuilder.DropIndex(
+                name: "IX_DisplayPreferences_UserId_Client",
+                schema: "jellyfin",
+                table: "DisplayPreferences");
+
+            migrationBuilder.CreateIndex(
+                name: "IX_DisplayPreferences_UserId",
+                schema: "jellyfin",
+                table: "DisplayPreferences",
+                column: "UserId",
+                unique: true);
+        }
+    }
+}

+ 4 - 2
Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs

@@ -15,7 +15,7 @@ namespace Jellyfin.Server.Implementations.Migrations
 #pragma warning disable 612, 618
             modelBuilder
                 .HasDefaultSchema("jellyfin")
-                .HasAnnotation("ProductVersion", "3.1.6");
+                .HasAnnotation("ProductVersion", "3.1.7");
 
             modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
                 {
@@ -136,7 +136,9 @@ namespace Jellyfin.Server.Implementations.Migrations
 
                     b.HasKey("Id");
 
-                    b.HasIndex("UserId")
+                    b.HasIndex("UserId");
+
+                    b.HasIndex("UserId", "Client")
                         .IsUnique();
 
                     b.ToTable("DisplayPreferences");

+ 49 - 0
Jellyfin.Server/Configuration/CorsPolicyProvider.cs

@@ -0,0 +1,49 @@
+using System;
+using System.Threading.Tasks;
+using MediaBrowser.Controller.Configuration;
+using Microsoft.AspNetCore.Cors.Infrastructure;
+using Microsoft.AspNetCore.Http;
+
+namespace Jellyfin.Server.Configuration
+{
+    /// <summary>
+    /// Cors policy provider.
+    /// </summary>
+    public class CorsPolicyProvider : ICorsPolicyProvider
+    {
+        private readonly IServerConfigurationManager _serverConfigurationManager;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="CorsPolicyProvider"/> class.
+        /// </summary>
+        /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
+        public CorsPolicyProvider(IServerConfigurationManager serverConfigurationManager)
+        {
+            _serverConfigurationManager = serverConfigurationManager;
+        }
+
+        /// <inheritdoc />
+        public Task<CorsPolicy> GetPolicyAsync(HttpContext context, string policyName)
+        {
+            var corsHosts = _serverConfigurationManager.Configuration.CorsHosts;
+            var builder = new CorsPolicyBuilder()
+                .AllowAnyMethod()
+                .AllowAnyHeader();
+
+            // No hosts configured or only default configured.
+            if (corsHosts.Length == 0
+                || (corsHosts.Length == 1
+                    && string.Equals(corsHosts[0], CorsConstants.AnyOrigin, StringComparison.Ordinal)))
+            {
+                builder.AllowAnyOrigin();
+            }
+            else
+            {
+                builder.WithOrigins(corsHosts)
+                    .AllowCredentials();
+            }
+
+            return Task.FromResult(builder.Build());
+        }
+    }
+}

+ 4 - 5
Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs

@@ -15,14 +15,15 @@ using Jellyfin.Api.Auth.LocalAccessPolicy;
 using Jellyfin.Api.Auth.RequiresElevationPolicy;
 using Jellyfin.Api.Constants;
 using Jellyfin.Api.Controllers;
+using Jellyfin.Server.Configuration;
 using Jellyfin.Server.Filters;
 using Jellyfin.Server.Formatters;
-using Jellyfin.Server.Models;
 using MediaBrowser.Common.Json;
 using MediaBrowser.Model.Entities;
 using Microsoft.AspNetCore.Authentication;
 using Microsoft.AspNetCore.Authorization;
 using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Cors.Infrastructure;
 using Microsoft.AspNetCore.HttpOverrides;
 using Microsoft.Extensions.DependencyInjection;
 using Microsoft.OpenApi.Models;
@@ -139,10 +140,8 @@ namespace Jellyfin.Server.Extensions
         public static IMvcBuilder AddJellyfinApi(this IServiceCollection serviceCollection, IEnumerable<Assembly> pluginAssemblies)
         {
             IMvcBuilder mvcBuilder = serviceCollection
-                .AddCors(options =>
-                {
-                    options.AddPolicy(ServerCorsPolicy.DefaultPolicyName, ServerCorsPolicy.DefaultPolicy);
-                })
+                .AddCors()
+                .AddTransient<ICorsPolicyProvider, CorsPolicyProvider>()
                 .Configure<ForwardedHeadersOptions>(options =>
                 {
                     options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;

+ 1 - 1
Jellyfin.Server/Jellyfin.Server.csproj

@@ -54,7 +54,7 @@
     <PackageReference Include="Serilog.Sinks.Console" Version="3.1.1" />
     <PackageReference Include="Serilog.Sinks.File" Version="4.1.0" />
     <PackageReference Include="Serilog.Sinks.Graylog" Version="2.1.3" />
-    <PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3" Version="2.0.3" />
+    <PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3" Version="2.0.4" />
     <PackageReference Include="SQLitePCLRaw.provider.sqlite3.netstandard11" Version="1.1.14" />
   </ItemGroup>
 

+ 5 - 1
Jellyfin.Server/Middleware/BaseUrlRedirectionMiddleware.cs

@@ -44,7 +44,11 @@ namespace Jellyfin.Server.Middleware
             var localPath = httpContext.Request.Path.ToString();
             var baseUrlPrefix = serverConfigurationManager.Configuration.BaseUrl;
 
-            if (!localPath.StartsWith(baseUrlPrefix, StringComparison.OrdinalIgnoreCase))
+            if (string.Equals(localPath, baseUrlPrefix + "/", StringComparison.OrdinalIgnoreCase)
+                || string.Equals(localPath, baseUrlPrefix, StringComparison.OrdinalIgnoreCase)
+                || string.Equals(localPath, "/", StringComparison.OrdinalIgnoreCase)
+                || string.IsNullOrEmpty(localPath)
+                || !localPath.StartsWith(baseUrlPrefix, StringComparison.OrdinalIgnoreCase))
             {
                 // Always redirect back to the default path if the base prefix is invalid or missing
                 _logger.LogDebug("Normalizing an URL at {LocalPath}", localPath);

+ 1 - 0
Jellyfin.Server/Middleware/ExceptionMiddleware.cs

@@ -125,6 +125,7 @@ namespace Jellyfin.Server.Middleware
             switch (ex)
             {
                 case ArgumentException _: return StatusCodes.Status400BadRequest;
+                case AuthenticationException _:
                 case SecurityException _: return StatusCodes.Status401Unauthorized;
                 case DirectoryNotFoundException _:
                 case FileNotFoundException _:

+ 0 - 30
Jellyfin.Server/Models/ServerCorsPolicy.cs

@@ -1,30 +0,0 @@
-using Microsoft.AspNetCore.Cors.Infrastructure;
-
-namespace Jellyfin.Server.Models
-{
-    /// <summary>
-    /// Server Cors Policy.
-    /// </summary>
-    public static class ServerCorsPolicy
-    {
-        /// <summary>
-        /// Default policy name.
-        /// </summary>
-        public const string DefaultPolicyName = "DefaultCorsPolicy";
-
-        /// <summary>
-        /// Default Policy. Allow Everything.
-        /// </summary>
-        public static readonly CorsPolicy DefaultPolicy = new CorsPolicy
-        {
-            // Allow any origin
-            Origins = { "*" },
-
-            // Allow any method
-            Methods = { "*" },
-
-            // Allow any header
-            Headers = { "*" }
-        };
-    }
-}

+ 2 - 7
Jellyfin.Server/Startup.cs

@@ -5,7 +5,6 @@ using Jellyfin.Api.TypeConverters;
 using Jellyfin.Server.Extensions;
 using Jellyfin.Server.Implementations;
 using Jellyfin.Server.Middleware;
-using Jellyfin.Server.Models;
 using MediaBrowser.Common.Net;
 using MediaBrowser.Controller;
 using MediaBrowser.Controller.Configuration;
@@ -94,11 +93,7 @@ namespace Jellyfin.Server
             IWebHostEnvironment env,
             IConfiguration appConfig)
         {
-            // Only add base url redirection if a base url is set.
-            if (!string.IsNullOrEmpty(_serverConfigurationManager.Configuration.BaseUrl))
-            {
-                app.UseBaseUrlRedirection();
-            }
+            app.UseBaseUrlRedirection();
 
             // Wrap rest of configuration so everything only listens on BaseUrl.
             app.Map(_serverConfigurationManager.Configuration.BaseUrl, mainApp =>
@@ -116,7 +111,7 @@ namespace Jellyfin.Server
 
                 mainApp.UseResponseCompression();
 
-                mainApp.UseCors(ServerCorsPolicy.DefaultPolicyName);
+                mainApp.UseCors();
 
                 if (_serverConfigurationManager.Configuration.RequireHttps
                     && _serverApplicationHost.ListenWithHttps)

+ 3 - 1
MediaBrowser.Common/Updates/IInstallationManager.cs

@@ -73,12 +73,14 @@ namespace MediaBrowser.Common.Updates
         /// <param name="name">The name.</param>
         /// <param name="guid">The guid of the plugin.</param>
         /// <param name="minVersion">The minimum required version of the plugin.</param>
+        /// <param name="specificVersion">The specific version of the plugin to install.</param>
         /// <returns>All compatible versions ordered from newest to oldest.</returns>
         IEnumerable<InstallationInfo> GetCompatibleVersions(
             IEnumerable<PackageInfo> availablePackages,
             string name = null,
             Guid guid = default,
-            Version minVersion = null);
+            Version minVersion = null,
+            Version specificVersion = null);
 
         /// <summary>
         /// Returns the available plugin updates.

+ 0 - 2
MediaBrowser.Controller/IServerApplicationHost.cs

@@ -114,8 +114,6 @@ namespace MediaBrowser.Controller
         /// <exception cref="NotSupportedException"><see cref="CanLaunchWebBrowser"/> is false.</exception>
         void LaunchUrl(string url);
 
-        void EnableLoopback(string appName);
-
         IEnumerable<WakeOnLanInfo> GetWakeOnLanInfo();
 
         string ExpandVirtualPath(string path);

+ 6 - 0
MediaBrowser.Model/Configuration/ServerConfiguration.cs

@@ -263,6 +263,11 @@ namespace MediaBrowser.Model.Configuration
         /// </summary>
         public long SlowResponseThresholdMs { get; set; }
 
+        /// <summary>
+        /// Gets or sets the cors hosts.
+        /// </summary>
+        public string[] CorsHosts { get; set; }
+
         /// <summary>
         /// Initializes a new instance of the <see cref="ServerConfiguration" /> class.
         /// </summary>
@@ -372,6 +377,7 @@ namespace MediaBrowser.Model.Configuration
 
             EnableSlowResponseWarning = true;
             SlowResponseThresholdMs = 500;
+            CorsHosts = new[] { "*" };
         }
     }
 

+ 0 - 6
MediaBrowser.Model/Dlna/DeviceIdentification.cs

@@ -37,12 +37,6 @@ namespace MediaBrowser.Model.Dlna
         /// <value>The model description.</value>
         public string ModelDescription { get; set; }
 
-        /// <summary>
-        /// Gets or sets the device description.
-        /// </summary>
-        /// <value>The device description.</value>
-        public string DeviceDescription { get; set; }
-
         /// <summary>
         /// Gets or sets the model URL.
         /// </summary>

+ 7 - 1
MediaBrowser.Model/System/PublicSystemInfo.cs

@@ -24,7 +24,7 @@ namespace MediaBrowser.Model.System
         public string Version { get; set; }
 
         /// <summary>
-        /// The product name. This is the AssemblyProduct name.
+        /// Gets or sets the product name. This is the AssemblyProduct name.
         /// </summary>
         public string ProductName { get; set; }
 
@@ -39,5 +39,11 @@ namespace MediaBrowser.Model.System
         /// </summary>
         /// <value>The id.</value>
         public string Id { get; set; }
+
+        /// <summary>
+        /// Gets or sets a value indicating whether the startup wizard is completed.
+        /// </summary>
+        /// <value>The startup completion status.</value>
+        public bool StartupWizardCompleted { get; set; }
     }
 }

+ 0 - 1
MediaBrowser.Providers/Folders/CollectionFolderMetadataService.cs

@@ -1,6 +1,5 @@
 #pragma warning disable CS1591
 
-
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;

+ 0 - 0
MediaBrowser.Providers/LiveTv/ProgramMetadataService.cs → MediaBrowser.Providers/LiveTv/LiveTvMetadataService.cs


+ 16 - 16
MediaBrowser.Providers/Manager/ImageSaver.cs

@@ -7,7 +7,6 @@ using System.IO;
 using System.Linq;
 using System.Threading;
 using System.Threading.Tasks;
-using Jellyfin.Data.Entities;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Entities;
@@ -59,6 +58,16 @@ namespace MediaBrowser.Providers.Manager
             _logger = logger;
         }
 
+        private bool EnableExtraThumbsDuplication
+        {
+            get
+            {
+                var config = _config.GetConfiguration<XbmcMetadataOptions>("xbmcmetadata");
+
+                return config.EnableExtraThumbsDuplication;
+            }
+        }
+
         /// <summary>
         /// Saves the image.
         /// </summary>
@@ -69,7 +78,7 @@ namespace MediaBrowser.Providers.Manager
         /// <param name="imageIndex">Index of the image.</param>
         /// <param name="cancellationToken">The cancellation token.</param>
         /// <returns>Task.</returns>
-        /// <exception cref="ArgumentNullException">mimeType</exception>
+        /// <exception cref="ArgumentNullException">mimeType.</exception>
         public Task SaveImage(BaseItem item, Stream source, string mimeType, ImageType type, int? imageIndex, CancellationToken cancellationToken)
         {
             return SaveImage(item, source, mimeType, type, imageIndex, null, cancellationToken);
@@ -312,7 +321,7 @@ namespace MediaBrowser.Providers.Manager
         /// <exception cref="ArgumentNullException">
         /// imageIndex
         /// or
-        /// imageIndex
+        /// imageIndex.
         /// </exception>
         private ItemImageInfo GetCurrentImage(BaseItem item, ImageType type, int imageIndex)
         {
@@ -328,7 +337,8 @@ namespace MediaBrowser.Providers.Manager
         /// <param name="path">The path.</param>
         /// <exception cref="ArgumentNullException">imageIndex
         /// or
-        /// imageIndex</exception>
+        /// imageIndex.
+        /// </exception>
         private void SetImagePath(BaseItem item, ImageType type, int? imageIndex, string path)
         {
             item.SetImagePath(type, imageIndex ?? 0, _fileSystem.GetFileInfo(path));
@@ -346,7 +356,7 @@ namespace MediaBrowser.Providers.Manager
         /// <exception cref="ArgumentNullException">
         /// imageIndex
         /// or
-        /// imageIndex
+        /// imageIndex.
         /// </exception>
         private string GetStandardSavePath(BaseItem item, ImageType type, int? imageIndex, string mimeType, bool saveLocally)
         {
@@ -500,7 +510,7 @@ namespace MediaBrowser.Providers.Manager
         /// <param name="imageIndex">Index of the image.</param>
         /// <param name="mimeType">Type of the MIME.</param>
         /// <returns>IEnumerable{System.String}.</returns>
-        /// <exception cref="ArgumentNullException">imageIndex</exception>
+        /// <exception cref="ArgumentNullException">imageIndex.</exception>
         private string[] GetCompatibleSavePaths(BaseItem item, ImageType type, int? imageIndex, string mimeType)
         {
             var season = item as Season;
@@ -604,16 +614,6 @@ namespace MediaBrowser.Providers.Manager
             return new[] { GetStandardSavePath(item, type, imageIndex, mimeType, true) };
         }
 
-        private bool EnableExtraThumbsDuplication
-        {
-            get
-            {
-                var config = _config.GetConfiguration<XbmcMetadataOptions>("xbmcmetadata");
-
-                return config.EnableExtraThumbsDuplication;
-            }
-        }
-
         /// <summary>
         /// Gets the save path for item in mixed folder.
         /// </summary>

+ 27 - 29
MediaBrowser.Providers/Manager/ItemImageProvider.cs

@@ -28,6 +28,22 @@ namespace MediaBrowser.Providers.Manager
         private readonly IProviderManager _providerManager;
         private readonly IFileSystem _fileSystem;
 
+        /// <summary>
+        /// Image types that are only one per item.
+        /// </summary>
+        private readonly ImageType[] _singularImages =
+        {
+            ImageType.Primary,
+            ImageType.Art,
+            ImageType.Banner,
+            ImageType.Box,
+            ImageType.BoxRear,
+            ImageType.Disc,
+            ImageType.Logo,
+            ImageType.Menu,
+            ImageType.Thumb
+        };
+
         public ItemImageProvider(ILogger logger, IProviderManager providerManager, IFileSystem fileSystem)
         {
             _logger = logger;
@@ -175,22 +191,6 @@ namespace MediaBrowser.Providers.Manager
             }
         }
 
-        /// <summary>
-        /// Image types that are only one per item.
-        /// </summary>
-        private readonly ImageType[] _singularImages =
-        {
-            ImageType.Primary,
-            ImageType.Art,
-            ImageType.Banner,
-            ImageType.Box,
-            ImageType.BoxRear,
-            ImageType.Disc,
-            ImageType.Logo,
-            ImageType.Menu,
-            ImageType.Thumb
-        };
-
         private bool HasImage(BaseItem item, ImageType type)
         {
             return item.HasImage(type);
@@ -378,7 +378,6 @@ namespace MediaBrowser.Providers.Manager
                     }
                     else
                     {
-
                         var newDateModified = _fileSystem.GetLastWriteTimeUtc(image.FileInfo);
 
                         // If date changed then we need to reset saved image dimensions
@@ -441,7 +440,9 @@ namespace MediaBrowser.Providers.Manager
             return changed;
         }
 
-        private async Task<bool> DownloadImage(BaseItem item, LibraryOptions libraryOptions,
+        private async Task<bool> DownloadImage(
+            BaseItem item,
+            LibraryOptions libraryOptions,
             IRemoteImageProvider provider,
             RefreshResult result,
             IEnumerable<RemoteImageInfo> images,
@@ -522,11 +523,6 @@ namespace MediaBrowser.Providers.Manager
                 return false;
             }
 
-            // if (!item.IsSaveLocalMetadataEnabled())
-            //{
-            //    return true;
-            //}
-
             return true;
         }
 
@@ -539,13 +535,15 @@ namespace MediaBrowser.Providers.Manager
 
         private void SaveImageStub(BaseItem item, ImageType imageType, IEnumerable<string> urls, int newIndex)
         {
-            var path = string.Join("|", urls.Take(1).ToArray());
+            var path = string.Join('|', urls.Take(1));
 
-            item.SetImage(new ItemImageInfo
-            {
-                Path = path,
-                Type = imageType
-            }, newIndex);
+            item.SetImage(
+                new ItemImageInfo
+                {
+                    Path = path,
+                    Type = imageType
+                },
+                newIndex);
         }
 
         private async Task DownloadBackdrops(BaseItem item, LibraryOptions libraryOptions, ImageType imageType, int limit, IRemoteImageProvider provider, RefreshResult result, IEnumerable<RemoteImageInfo> images, int minWidth, CancellationToken cancellationToken)

+ 29 - 32
MediaBrowser.Providers/Manager/MetadataService.cs

@@ -21,12 +21,6 @@ namespace MediaBrowser.Providers.Manager
         where TItemType : BaseItem, IHasLookupInfo<TIdType>, new()
         where TIdType : ItemLookupInfo, new()
     {
-        protected readonly IServerConfigurationManager ServerConfigurationManager;
-        protected readonly ILogger<MetadataService<TItemType, TIdType>> Logger;
-        protected readonly IProviderManager ProviderManager;
-        protected readonly IFileSystem FileSystem;
-        protected readonly ILibraryManager LibraryManager;
-
         protected MetadataService(IServerConfigurationManager serverConfigurationManager, ILogger<MetadataService<TItemType, TIdType>> logger, IProviderManager providerManager, IFileSystem fileSystem, ILibraryManager libraryManager)
         {
             ServerConfigurationManager = serverConfigurationManager;
@@ -36,6 +30,26 @@ namespace MediaBrowser.Providers.Manager
             LibraryManager = libraryManager;
         }
 
+        protected IServerConfigurationManager ServerConfigurationManager { get; }
+
+        protected ILogger<MetadataService<TItemType, TIdType>> Logger { get; }
+
+        protected IProviderManager ProviderManager { get; }
+
+        protected IFileSystem FileSystem { get; }
+
+        protected ILibraryManager LibraryManager { get; }
+
+        protected virtual bool EnableUpdatingPremiereDateFromChildren => false;
+
+        protected virtual bool EnableUpdatingGenresFromChildren => false;
+
+        protected virtual bool EnableUpdatingStudiosFromChildren => false;
+
+        protected virtual bool EnableUpdatingOfficialRatingFromChildren => false;
+
+        public virtual int Order => 0;
+
         private FileSystemMetadata TryGetFile(string path, IDirectoryService directoryService)
         {
             try
@@ -442,14 +456,6 @@ namespace MediaBrowser.Providers.Manager
             return updateType;
         }
 
-        protected virtual bool EnableUpdatingPremiereDateFromChildren => false;
-
-        protected virtual bool EnableUpdatingGenresFromChildren => false;
-
-        protected virtual bool EnableUpdatingStudiosFromChildren => false;
-
-        protected virtual bool EnableUpdatingOfficialRatingFromChildren => false;
-
         private ItemUpdateType UpdatePremiereDate(TItemType item, IList<BaseItem> children)
         {
             var updateType = ItemUpdateType.None;
@@ -658,7 +664,8 @@ namespace MediaBrowser.Providers.Manager
             return type == typeof(TItemType);
         }
 
-        protected virtual async Task<RefreshResult> RefreshWithProviders(MetadataResult<TItemType> metadata,
+        protected virtual async Task<RefreshResult> RefreshWithProviders(
+            MetadataResult<TItemType> metadata,
             TIdType id,
             MetadataRefreshOptions options,
             List<IMetadataProvider> providers,
@@ -773,7 +780,7 @@ namespace MediaBrowser.Providers.Manager
                     else
                     {
                         // TODO: If the new metadata from above has some blank data, this can cause old data to get filled into those empty fields
-                        MergeData(metadata, temp, new MetadataField[] { }, false, false);
+                        MergeData(metadata, temp, Array.Empty<MetadataField>(), false, false);
                         MergeData(temp, metadata, item.LockedFields, true, false);
                     }
                 }
@@ -900,24 +907,23 @@ namespace MediaBrowser.Providers.Manager
             }
         }
 
-        protected abstract void MergeData(MetadataResult<TItemType> source,
+        protected abstract void MergeData(
+            MetadataResult<TItemType> source,
             MetadataResult<TItemType> target,
             MetadataField[] lockedFields,
             bool replaceData,
             bool mergeMetadataSettings);
 
-        public virtual int Order => 0;
-
         private bool HasChanged(BaseItem item, IHasItemChangeMonitor changeMonitor, IDirectoryService directoryService)
         {
             try
             {
                 var hasChanged = changeMonitor.HasChanged(item, directoryService);
 
-                // if (hasChanged)
-                //{
-                //    logger.LogDebug("{0} reports change to {1}", changeMonitor.GetType().Name, item.Path ?? item.Name);
-                //}
+                if (hasChanged)
+                {
+                    Logger.LogDebug("{0} reports change to {1}", changeMonitor.GetType().Name, item.Path ?? item.Name);
+                }
 
                 return hasChanged;
             }
@@ -928,13 +934,4 @@ namespace MediaBrowser.Providers.Manager
             }
         }
     }
-
-    public class RefreshResult
-    {
-        public ItemUpdateType UpdateType { get; set; }
-
-        public string ErrorMessage { get; set; }
-
-        public int Failures { get; set; }
-    }
 }

+ 1 - 3
MediaBrowser.Providers/Manager/ProviderManager.cs

@@ -9,7 +9,6 @@ using System.Net.Http;
 using System.Net.Mime;
 using System.Threading;
 using System.Threading.Tasks;
-using Jellyfin.Data.Entities;
 using Jellyfin.Data.Events;
 using MediaBrowser.Common.Net;
 using MediaBrowser.Common.Progress;
@@ -905,8 +904,7 @@ namespace MediaBrowser.Providers.Manager
             return provider.GetImageResponse(url, cancellationToken);
         }
 
-        /// <inheritdoc/>
-        public IEnumerable<IExternalId> GetExternalIds(IHasProviderIds item)
+        private IEnumerable<IExternalId> GetExternalIds(IHasProviderIds item)
         {
             return _externalIds.Where(i =>
             {

+ 2 - 2
MediaBrowser.Providers/Manager/ProviderUtils.cs

@@ -26,12 +26,12 @@ namespace MediaBrowser.Providers.Manager
 
             if (source == null)
             {
-                throw new ArgumentNullException(nameof(source));
+                throw new ArgumentException("Item cannot be null.", nameof(sourceResult));
             }
 
             if (target == null)
             {
-                throw new ArgumentNullException(nameof(target));
+                throw new ArgumentException("Item cannot be null.", nameof(targetResult));
             }
 
             if (!lockedFields.Contains(MetadataField.Name))

+ 15 - 0
MediaBrowser.Providers/Manager/RefreshResult.cs

@@ -0,0 +1,15 @@
+#pragma warning disable CS1591
+
+using MediaBrowser.Controller.Library;
+
+namespace MediaBrowser.Providers.Manager
+{
+    public class RefreshResult
+    {
+        public ItemUpdateType UpdateType { get; set; }
+
+        public string ErrorMessage { get; set; }
+
+        public int Failures { get; set; }
+    }
+}

+ 8 - 8
MediaBrowser.Providers/MediaInfo/AudioImageProvider.cs

@@ -34,6 +34,10 @@ namespace MediaBrowser.Providers.MediaInfo
             _fileSystem = fileSystem;
         }
 
+        public string AudioImagesPath => Path.Combine(_config.ApplicationPaths.CachePath, "extracted-audio-images");
+
+        public string Name => "Image Extractor";
+
         public IEnumerable<ImageType> GetSupportedImages(BaseItem item)
         {
             return new List<ImageType> { ImageType.Primary };
@@ -97,11 +101,11 @@ namespace MediaBrowser.Providers.MediaInfo
 
             if (item.GetType() == typeof(Audio))
             {
-                var albumArtist = item.AlbumArtists.FirstOrDefault();
-
-                if (!string.IsNullOrWhiteSpace(item.Album) && !string.IsNullOrWhiteSpace(albumArtist))
+                if (item.AlbumArtists.Count > 0
+                    && !string.IsNullOrWhiteSpace(item.Album)
+                    && !string.IsNullOrWhiteSpace(item.AlbumArtists[0]))
                 {
-                    filename = (item.Album + "-" + albumArtist).GetMD5().ToString("N", CultureInfo.InvariantCulture);
+                    filename = (item.Album + "-" + item.AlbumArtists[0]).GetMD5().ToString("N", CultureInfo.InvariantCulture);
                 }
                 else
                 {
@@ -121,10 +125,6 @@ namespace MediaBrowser.Providers.MediaInfo
             return Path.Join(AudioImagesPath, prefix, filename);
         }
 
-        public string AudioImagesPath => Path.Combine(_config.ApplicationPaths.CachePath, "extracted-audio-images");
-
-        public string Name => "Image Extractor";
-
         public bool Supports(BaseItem item)
         {
             if (item.IsShortcut)

+ 16 - 13
MediaBrowser.Providers/MediaInfo/FFProbeAudioInfo.cs

@@ -37,7 +37,9 @@ namespace MediaBrowser.Providers.MediaInfo
             _mediaSourceManager = mediaSourceManager;
         }
 
-        public async Task<ItemUpdateType> Probe<T>(T item, MetadataRefreshOptions options,
+        public async Task<ItemUpdateType> Probe<T>(
+            T item,
+            MetadataRefreshOptions options,
             CancellationToken cancellationToken)
             where T : Audio
         {
@@ -52,19 +54,21 @@ namespace MediaBrowser.Providers.MediaInfo
                     protocol = _mediaSourceManager.GetPathProtocol(path);
                 }
 
-                var result = await _mediaEncoder.GetMediaInfo(new MediaInfoRequest
-                {
-                    MediaType = DlnaProfileType.Audio,
-                    MediaSource = new MediaSourceInfo
+                var result = await _mediaEncoder.GetMediaInfo(
+                    new MediaInfoRequest
                     {
-                        Path = path,
-                        Protocol = protocol
-                    }
-                }, cancellationToken).ConfigureAwait(false);
+                        MediaType = DlnaProfileType.Audio,
+                        MediaSource = new MediaSourceInfo
+                        {
+                            Path = path,
+                            Protocol = protocol
+                        }
+                    },
+                    cancellationToken).ConfigureAwait(false);
 
                 cancellationToken.ThrowIfCancellationRequested();
 
-                Fetch(item, cancellationToken, result);
+                Fetch(item, result, cancellationToken);
             }
 
             return ItemUpdateType.MetadataImport;
@@ -74,10 +78,9 @@ namespace MediaBrowser.Providers.MediaInfo
         /// Fetches the specified audio.
         /// </summary>
         /// <param name="audio">The audio.</param>
-        /// <param name="cancellationToken">The cancellation token.</param>
         /// <param name="mediaInfo">The media information.</param>
-        /// <returns>Task.</returns>
-        protected void Fetch(Audio audio, CancellationToken cancellationToken, Model.MediaInfo.MediaInfo mediaInfo)
+        /// <param name="cancellationToken">The cancellation token.</param>
+        protected void Fetch(Audio audio, Model.MediaInfo.MediaInfo mediaInfo, CancellationToken cancellationToken)
         {
             var mediaStreams = mediaInfo.MediaStreams;
 

+ 34 - 38
MediaBrowser.Providers/MediaInfo/FFProbeProvider.cs

@@ -5,8 +5,6 @@ using System.IO;
 using System.Linq;
 using System.Threading;
 using System.Threading.Tasks;
-using MediaBrowser.Common.Configuration;
-using MediaBrowser.Controller.Channels;
 using MediaBrowser.Controller.Chapters;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Entities;
@@ -20,9 +18,7 @@ using MediaBrowser.Controller.Providers;
 using MediaBrowser.Controller.Subtitles;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Globalization;
-using MediaBrowser.Model.IO;
 using MediaBrowser.Model.MediaInfo;
-using MediaBrowser.Model.Serialization;
 using Microsoft.Extensions.Logging;
 
 namespace MediaBrowser.Providers.MediaInfo
@@ -50,9 +46,43 @@ namespace MediaBrowser.Providers.MediaInfo
         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);
+
+        public FFProbeProvider(
+            ILogger<FFProbeProvider> logger,
+            IMediaSourceManager mediaSourceManager,
+            IMediaEncoder mediaEncoder,
+            IItemRepository itemRepo,
+            IBlurayExaminer blurayExaminer,
+            ILocalizationManager localization,
+            IEncodingManager encodingManager,
+            IServerConfigurationManager config,
+            ISubtitleManager subtitleManager,
+            IChapterManager chapterManager,
+            ILibraryManager libraryManager)
+        {
+            _logger = logger;
+            _mediaEncoder = mediaEncoder;
+            _itemRepo = itemRepo;
+            _blurayExaminer = blurayExaminer;
+            _localization = localization;
+            _encodingManager = encodingManager;
+            _config = config;
+            _subtitleManager = subtitleManager;
+            _chapterManager = chapterManager;
+            _libraryManager = libraryManager;
+            _mediaSourceManager = mediaSourceManager;
+
+            _subtitleResolver = new SubtitleResolver(BaseItem.LocalizationManager);
+        }
 
         public string Name => "ffprobe";
 
+        // Run last
+        public int Order => 100;
+
         public bool HasChanged(BaseItem item, IDirectoryService directoryService)
         {
             var video = item as Video;
@@ -117,37 +147,6 @@ namespace MediaBrowser.Providers.MediaInfo
             return FetchAudioInfo(item, options, cancellationToken);
         }
 
-        private SubtitleResolver _subtitleResolver;
-
-        public FFProbeProvider(
-            ILogger<FFProbeProvider> logger,
-            IMediaSourceManager mediaSourceManager,
-            IMediaEncoder mediaEncoder,
-            IItemRepository itemRepo,
-            IBlurayExaminer blurayExaminer,
-            ILocalizationManager localization,
-            IEncodingManager encodingManager,
-            IServerConfigurationManager config,
-            ISubtitleManager subtitleManager,
-            IChapterManager chapterManager,
-            ILibraryManager libraryManager)
-        {
-            _logger = logger;
-            _mediaEncoder = mediaEncoder;
-            _itemRepo = itemRepo;
-            _blurayExaminer = blurayExaminer;
-            _localization = localization;
-            _encodingManager = encodingManager;
-            _config = config;
-            _subtitleManager = subtitleManager;
-            _chapterManager = chapterManager;
-            _libraryManager = libraryManager;
-            _mediaSourceManager = mediaSourceManager;
-
-            _subtitleResolver = new SubtitleResolver(BaseItem.LocalizationManager);
-        }
-
-        private readonly Task<ItemUpdateType> _cachedTask = Task.FromResult(ItemUpdateType.None);
         public Task<ItemUpdateType> FetchVideoInfo<T>(T item, MetadataRefreshOptions options, CancellationToken cancellationToken)
             where T : Video
         {
@@ -234,8 +233,5 @@ namespace MediaBrowser.Providers.MediaInfo
 
             return prober.Probe(item, options, cancellationToken);
         }
-
-        // Run last
-        public int Order => 100;
     }
 }

+ 12 - 11
MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs

@@ -539,17 +539,18 @@ namespace MediaBrowser.Providers.MediaInfo
 
             if (enableSubtitleDownloading && enabled)
             {
-                var downloadedLanguages = await new SubtitleDownloader(_logger,
-                    _subtitleManager)
-                    .DownloadSubtitles(video,
-                    currentStreams.Concat(externalSubtitleStreams).ToList(),
-                    skipIfEmbeddedSubtitlesPresent,
-                    skipIfAudioTrackMatches,
-                    requirePerfectMatch,
-                    subtitleDownloadLanguages,
-                    libraryOptions.DisabledSubtitleFetchers,
-                    libraryOptions.SubtitleFetcherOrder,
-                    cancellationToken).ConfigureAwait(false);
+                var downloadedLanguages = await new SubtitleDownloader(
+                    _logger,
+                    _subtitleManager).DownloadSubtitles(
+                        video,
+                        currentStreams.Concat(externalSubtitleStreams).ToList(),
+                        skipIfEmbeddedSubtitlesPresent,
+                        skipIfAudioTrackMatches,
+                        requirePerfectMatch,
+                        subtitleDownloadLanguages,
+                        libraryOptions.DisabledSubtitleFetchers,
+                        libraryOptions.SubtitleFetcherOrder,
+                        cancellationToken).ConfigureAwait(false);
 
                 // Rescan
                 if (downloadedLanguages.Count > 0)

+ 25 - 6
MediaBrowser.Providers/MediaInfo/SubtitleDownloader.cs

@@ -42,8 +42,16 @@ namespace MediaBrowser.Providers.MediaInfo
 
             foreach (var lang in languages)
             {
-                var downloaded = await DownloadSubtitles(video, mediaStreams, skipIfEmbeddedSubtitlesPresent,
-                    skipIfAudioTrackMatches, requirePerfectMatch, lang, disabledSubtitleFetchers, subtitleFetcherOrder, cancellationToken).ConfigureAwait(false);
+                var downloaded = await DownloadSubtitles(
+                    video,
+                    mediaStreams,
+                    skipIfEmbeddedSubtitlesPresent,
+                    skipIfAudioTrackMatches,
+                    requirePerfectMatch,
+                    lang,
+                    disabledSubtitleFetchers,
+                    subtitleFetcherOrder,
+                    cancellationToken).ConfigureAwait(false);
 
                 if (downloaded)
                 {
@@ -54,7 +62,8 @@ namespace MediaBrowser.Providers.MediaInfo
             return downloadedLanguages;
         }
 
-        public Task<bool> DownloadSubtitles(Video video,
+        public Task<bool> DownloadSubtitles(
+            Video video,
             List<MediaStream> mediaStreams,
             bool skipIfEmbeddedSubtitlesPresent,
             bool skipIfAudioTrackMatches,
@@ -90,11 +99,21 @@ namespace MediaBrowser.Providers.MediaInfo
                 return Task.FromResult(false);
             }
 
-            return DownloadSubtitles(video, mediaStreams, skipIfEmbeddedSubtitlesPresent, skipIfAudioTrackMatches,
-                requirePerfectMatch, lang, disabledSubtitleFetchers, subtitleFetcherOrder, mediaType, cancellationToken);
+            return DownloadSubtitles(
+                video,
+                mediaStreams,
+                skipIfEmbeddedSubtitlesPresent,
+                skipIfAudioTrackMatches,
+                requirePerfectMatch,
+                lang,
+                disabledSubtitleFetchers,
+                subtitleFetcherOrder,
+                mediaType,
+                cancellationToken);
         }
 
-        private async Task<bool> DownloadSubtitles(Video video,
+        private async Task<bool> DownloadSubtitles(
+            Video video,
             List<MediaStream> mediaStreams,
             bool skipIfEmbeddedSubtitlesPresent,
             bool skipIfAudioTrackMatches,

+ 11 - 7
MediaBrowser.Providers/MediaInfo/SubtitleResolver.cs

@@ -66,9 +66,10 @@ namespace MediaBrowser.Providers.MediaInfo
             return streams;
         }
 
-        public List<string> GetExternalSubtitleFiles(Video video,
-          IDirectoryService directoryService,
-          bool clearCache)
+        public List<string> GetExternalSubtitleFiles(
+            Video video,
+            IDirectoryService directoryService,
+            bool clearCache)
         {
             var list = new List<string>();
 
@@ -87,7 +88,9 @@ namespace MediaBrowser.Providers.MediaInfo
             return list;
         }
 
-        private void AddExternalSubtitleStreams(List<MediaStream> streams, string folder,
+        private void AddExternalSubtitleStreams(
+            List<MediaStream> streams,
+            string folder,
             string videoPath,
             int startIndex,
             IDirectoryService directoryService,
@@ -98,7 +101,8 @@ namespace MediaBrowser.Providers.MediaInfo
             AddExternalSubtitleStreams(streams, videoPath, startIndex, files);
         }
 
-        public void AddExternalSubtitleStreams(List<MediaStream> streams,
+        public void AddExternalSubtitleStreams(
+            List<MediaStream> streams,
             string videoPath,
             int startIndex,
             string[] files)
@@ -185,8 +189,8 @@ namespace MediaBrowser.Providers.MediaInfo
         private string NormalizeFilenameForSubtitleComparison(string filename)
         {
             // Try to account for sloppy file naming
-            filename = filename.Replace("_", string.Empty);
-            filename = filename.Replace(" ", string.Empty);
+            filename = filename.Replace("_", string.Empty, StringComparison.Ordinal);
+            filename = filename.Replace(" ", string.Empty, StringComparison.Ordinal);
 
             // can't normalize this due to languages such as pt-br
             // filename = filename.Replace("-", string.Empty);

+ 49 - 55
MediaBrowser.Providers/MediaInfo/SubtitleScheduledTask.cs

@@ -12,11 +12,10 @@ using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Subtitles;
 using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Globalization;
 using MediaBrowser.Model.Providers;
-using MediaBrowser.Model.Serialization;
 using MediaBrowser.Model.Tasks;
 using Microsoft.Extensions.Logging;
-using MediaBrowser.Model.Globalization;
 
 namespace MediaBrowser.Providers.MediaInfo
 {
@@ -25,29 +24,37 @@ namespace MediaBrowser.Providers.MediaInfo
         private readonly ILibraryManager _libraryManager;
         private readonly IServerConfigurationManager _config;
         private readonly ISubtitleManager _subtitleManager;
-        private readonly IMediaSourceManager _mediaSourceManager;
         private readonly ILogger<SubtitleScheduledTask> _logger;
-        private readonly IJsonSerializer _json;
         private readonly ILocalizationManager _localization;
 
         public SubtitleScheduledTask(
             ILibraryManager libraryManager,
-            IJsonSerializer json,
             IServerConfigurationManager config,
             ISubtitleManager subtitleManager,
             ILogger<SubtitleScheduledTask> logger,
-            IMediaSourceManager mediaSourceManager,
             ILocalizationManager localization)
         {
             _libraryManager = libraryManager;
             _config = config;
             _subtitleManager = subtitleManager;
             _logger = logger;
-            _mediaSourceManager = mediaSourceManager;
-            _json = json;
             _localization = localization;
         }
 
+        public string Name => _localization.GetLocalizedString("TaskDownloadMissingSubtitles");
+
+        public string Description => _localization.GetLocalizedString("TaskDownloadMissingSubtitlesDescription");
+
+        public string Category => _localization.GetLocalizedString("TasksLibraryCategory");
+
+        public string Key => "DownloadSubtitles";
+
+        public bool IsHidden => false;
+
+        public bool IsEnabled => true;
+
+        public bool IsLogged => true;
+
         private SubtitleOptions GetOptions()
         {
             return _config.GetConfiguration<SubtitleOptions>("subtitles");
@@ -66,23 +73,23 @@ namespace MediaBrowser.Providers.MediaInfo
                 var libraryOptions = _libraryManager.GetLibraryOptions(library);
 
                 string[] subtitleDownloadLanguages;
-                bool SkipIfEmbeddedSubtitlesPresent;
-                bool SkipIfAudioTrackMatches;
-                bool RequirePerfectMatch;
+                bool skipIfEmbeddedSubtitlesPresent;
+                bool skipIfAudioTrackMatches;
+                bool requirePerfectMatch;
 
                 if (libraryOptions.SubtitleDownloadLanguages == null)
                 {
                     subtitleDownloadLanguages = options.DownloadLanguages;
-                    SkipIfEmbeddedSubtitlesPresent = options.SkipIfEmbeddedSubtitlesPresent;
-                    SkipIfAudioTrackMatches = options.SkipIfAudioTrackMatches;
-                    RequirePerfectMatch = options.RequirePerfectMatch;
+                    skipIfEmbeddedSubtitlesPresent = options.SkipIfEmbeddedSubtitlesPresent;
+                    skipIfAudioTrackMatches = options.SkipIfAudioTrackMatches;
+                    requirePerfectMatch = options.RequirePerfectMatch;
                 }
                 else
                 {
                     subtitleDownloadLanguages = libraryOptions.SubtitleDownloadLanguages;
-                    SkipIfEmbeddedSubtitlesPresent = libraryOptions.SkipSubtitlesIfEmbeddedSubtitlesPresent;
-                    SkipIfAudioTrackMatches = libraryOptions.SkipSubtitlesIfAudioTrackMatches;
-                    RequirePerfectMatch = libraryOptions.RequirePerfectSubtitleMatch;
+                    skipIfEmbeddedSubtitlesPresent = libraryOptions.SkipSubtitlesIfEmbeddedSubtitlesPresent;
+                    skipIfAudioTrackMatches = libraryOptions.SkipSubtitlesIfAudioTrackMatches;
+                    requirePerfectMatch = libraryOptions.RequirePerfectSubtitleMatch;
                 }
 
                 foreach (var lang in subtitleDownloadLanguages)
@@ -98,12 +105,12 @@ namespace MediaBrowser.Providers.MediaInfo
                         Recursive = true
                     };
 
-                    if (SkipIfAudioTrackMatches)
+                    if (skipIfAudioTrackMatches)
                     {
                         query.HasNoAudioTrackWithLanguage = lang;
                     }
 
-                    if (SkipIfEmbeddedSubtitlesPresent)
+                    if (skipIfEmbeddedSubtitlesPresent)
                     {
                         // Exclude if it already has any subtitles of the same language
                         query.HasNoSubtitleTrackWithLanguage = lang;
@@ -160,36 +167,37 @@ namespace MediaBrowser.Providers.MediaInfo
             var libraryOptions = _libraryManager.GetLibraryOptions(video);
 
             string[] subtitleDownloadLanguages;
-            bool SkipIfEmbeddedSubtitlesPresent;
-            bool SkipIfAudioTrackMatches;
-            bool RequirePerfectMatch;
+            bool skipIfEmbeddedSubtitlesPresent;
+            bool skipIfAudioTrackMatches;
+            bool requirePerfectMatch;
 
             if (libraryOptions.SubtitleDownloadLanguages == null)
             {
                 subtitleDownloadLanguages = options.DownloadLanguages;
-                SkipIfEmbeddedSubtitlesPresent = options.SkipIfEmbeddedSubtitlesPresent;
-                SkipIfAudioTrackMatches = options.SkipIfAudioTrackMatches;
-                RequirePerfectMatch = options.RequirePerfectMatch;
+                skipIfEmbeddedSubtitlesPresent = options.SkipIfEmbeddedSubtitlesPresent;
+                skipIfAudioTrackMatches = options.SkipIfAudioTrackMatches;
+                requirePerfectMatch = options.RequirePerfectMatch;
             }
             else
             {
                 subtitleDownloadLanguages = libraryOptions.SubtitleDownloadLanguages;
-                SkipIfEmbeddedSubtitlesPresent = libraryOptions.SkipSubtitlesIfEmbeddedSubtitlesPresent;
-                SkipIfAudioTrackMatches = libraryOptions.SkipSubtitlesIfAudioTrackMatches;
-                RequirePerfectMatch = libraryOptions.RequirePerfectSubtitleMatch;
+                skipIfEmbeddedSubtitlesPresent = libraryOptions.SkipSubtitlesIfEmbeddedSubtitlesPresent;
+                skipIfAudioTrackMatches = libraryOptions.SkipSubtitlesIfAudioTrackMatches;
+                requirePerfectMatch = libraryOptions.RequirePerfectSubtitleMatch;
             }
 
-            var downloadedLanguages = await new SubtitleDownloader(_logger,
-                _subtitleManager)
-                .DownloadSubtitles(video,
-                mediaStreams,
-                SkipIfEmbeddedSubtitlesPresent,
-                SkipIfAudioTrackMatches,
-                RequirePerfectMatch,
-                subtitleDownloadLanguages,
-                libraryOptions.DisabledSubtitleFetchers,
-                libraryOptions.SubtitleFetcherOrder,
-                cancellationToken).ConfigureAwait(false);
+            var downloadedLanguages = await new SubtitleDownloader(
+                _logger,
+                _subtitleManager).DownloadSubtitles(
+                    video,
+                    mediaStreams,
+                    skipIfEmbeddedSubtitlesPresent,
+                    skipIfAudioTrackMatches,
+                    requirePerfectMatch,
+                    subtitleDownloadLanguages,
+                    libraryOptions.DisabledSubtitleFetchers,
+                    libraryOptions.SubtitleFetcherOrder,
+                    cancellationToken).ConfigureAwait(false);
 
             // Rescan
             if (downloadedLanguages.Count > 0)
@@ -203,25 +211,11 @@ namespace MediaBrowser.Providers.MediaInfo
 
         public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
         {
-            return new[] {
-
+            return new[]
+            {
                 // Every so often
                 new TaskTriggerInfo { Type = TaskTriggerInfo.TriggerInterval, IntervalTicks = TimeSpan.FromHours(24).Ticks}
             };
         }
-
-        public string Name => _localization.GetLocalizedString("TaskDownloadMissingSubtitles");
-
-        public string Description => _localization.GetLocalizedString("TaskDownloadMissingSubtitlesDescription");
-
-        public string Category => _localization.GetLocalizedString("TasksLibraryCategory");
-
-        public string Key => "DownloadSubtitles";
-
-        public bool IsHidden => false;
-
-        public bool IsEnabled => true;
-
-        public bool IsLogged => true;
     }
 }

+ 5 - 4
MediaBrowser.Providers/MediaInfo/VideoImageProvider.cs

@@ -29,6 +29,11 @@ namespace MediaBrowser.Providers.MediaInfo
             _fileSystem = fileSystem;
         }
 
+        public string Name => "Screen Grabber";
+
+        // Make sure this comes after internet image providers
+        public int Order => 100;
+
         public IEnumerable<ImageType> GetSupportedImages(BaseItem item)
         {
             return new List<ImageType> { ImageType.Primary };
@@ -127,8 +132,6 @@ namespace MediaBrowser.Providers.MediaInfo
             };
         }
 
-        public string Name => "Screen Grabber";
-
         public bool Supports(BaseItem item)
         {
             if (item.IsShortcut)
@@ -150,7 +153,5 @@ namespace MediaBrowser.Providers.MediaInfo
 
             return false;
         }
-        // Make sure this comes after internet image providers
-        public int Order => 100;
     }
 }

+ 0 - 18
MediaBrowser.Providers/Movies/MovieExternalIds.cs → MediaBrowser.Providers/Movies/ImdbExternalId.cs

@@ -36,22 +36,4 @@ namespace MediaBrowser.Providers.Movies
             return item is Movie || item is MusicVideo || item is Series || item is Episode || item is Trailer;
         }
     }
-
-    public class ImdbPersonExternalId : IExternalId
-    {
-        /// <inheritdoc />
-        public string ProviderName => "IMDb";
-
-        /// <inheritdoc />
-        public string Key => MetadataProvider.Imdb.ToString();
-
-        /// <inheritdoc />
-        public ExternalIdMediaType? Type => ExternalIdMediaType.Person;
-
-        /// <inheritdoc />
-        public string UrlFormatString => "https://www.imdb.com/name/{0}";
-
-        /// <inheritdoc />
-        public bool Supports(IHasProviderIds item) => item is Person;
-    }
 }

+ 27 - 0
MediaBrowser.Providers/Movies/ImdbPersonExternalId.cs

@@ -0,0 +1,27 @@
+#pragma warning disable CS1591
+
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Providers;
+
+namespace MediaBrowser.Providers.Movies
+{
+    public class ImdbPersonExternalId : IExternalId
+    {
+        /// <inheritdoc />
+        public string ProviderName => "IMDb";
+
+        /// <inheritdoc />
+        public string Key => MetadataProvider.Imdb.ToString();
+
+        /// <inheritdoc />
+        public ExternalIdMediaType? Type => ExternalIdMediaType.Person;
+
+        /// <inheritdoc />
+        public string UrlFormatString => "https://www.imdb.com/name/{0}";
+
+        /// <inheritdoc />
+        public bool Supports(IHasProviderIds item) => item is Person;
+    }
+}

+ 0 - 0
MediaBrowser.Providers/Music/Extensions.cs → MediaBrowser.Providers/Music/AlbumInfoExtensions.cs


+ 0 - 0
MediaBrowser.Providers/Music/MusicExternalIds.cs → MediaBrowser.Providers/Music/ImvdbId.cs


+ 4 - 6
MediaBrowser.Providers/Playlists/PlaylistItemsProvider.cs

@@ -10,7 +10,6 @@ using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Playlists;
 using MediaBrowser.Controller.Providers;
-using MediaBrowser.Model.IO;
 using Microsoft.Extensions.Logging;
 using PlaylistsNET.Content;
 
@@ -23,16 +22,17 @@ namespace MediaBrowser.Providers.Playlists
         IHasItemChangeMonitor
     {
         private readonly ILogger<PlaylistItemsProvider> _logger;
-        private IFileSystem _fileSystem;
 
-        public PlaylistItemsProvider(IFileSystem fileSystem, ILogger<PlaylistItemsProvider> logger)
+        public PlaylistItemsProvider(ILogger<PlaylistItemsProvider> logger)
         {
-            _fileSystem = fileSystem;
             _logger = logger;
         }
 
         public string Name => "Playlist Reader";
 
+        // Run last
+        public int Order => 100;
+
         public Task<ItemUpdateType> FetchAsync(Playlist item, MetadataRefreshOptions options, CancellationToken cancellationToken)
         {
             var path = item.Path;
@@ -163,7 +163,5 @@ namespace MediaBrowser.Providers.Playlists
 
             return false;
         }
-        // Run last
-        public int Order => 100;
     }
 }

+ 5 - 5
MediaBrowser.Providers/Plugins/AudioDb/ArtistProvider.cs

@@ -23,16 +23,14 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
 {
     public class AudioDbArtistProvider : IRemoteMetadataProvider<MusicArtist, ArtistInfo>, IHasOrder
     {
+        private const string ApiKey = "195003";
+        public const string BaseUrl = "https://www.theaudiodb.com/api/v1/json/" + ApiKey;
+
         private readonly IServerConfigurationManager _config;
         private readonly IFileSystem _fileSystem;
         private readonly IHttpClientFactory _httpClientFactory;
         private readonly IJsonSerializer _json;
 
-        public static AudioDbArtistProvider Current;
-
-        private const string ApiKey = "195003";
-        public const string BaseUrl = "https://www.theaudiodb.com/api/v1/json/" + ApiKey;
-
         public AudioDbArtistProvider(IServerConfigurationManager config, IFileSystem fileSystem, IHttpClientFactory httpClientFactory, IJsonSerializer json)
         {
             _config = config;
@@ -42,6 +40,8 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
             Current = this;
         }
 
+        public static AudioDbArtistProvider Current { get; private set; }
+
         /// <inheritdoc />
         public string Name => "TheAudioDB";
 

+ 27 - 0
MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumExternalId.cs

@@ -0,0 +1,27 @@
+#pragma warning disable CS1591
+
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Providers;
+
+namespace MediaBrowser.Providers.Plugins.AudioDb
+{
+    public class AudioDbAlbumExternalId : IExternalId
+    {
+        /// <inheritdoc />
+        public string ProviderName => "TheAudioDb";
+
+        /// <inheritdoc />
+        public string Key => MetadataProvider.AudioDbAlbum.ToString();
+
+        /// <inheritdoc />
+        public ExternalIdMediaType? Type => null;
+
+        /// <inheritdoc />
+        public string UrlFormatString => "https://www.theaudiodb.com/album/{0}";
+
+        /// <inheritdoc />
+        public bool Supports(IHasProviderIds item) => item is MusicAlbum;
+    }
+}

+ 27 - 0
MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistExternalId.cs

@@ -0,0 +1,27 @@
+#pragma warning disable CS1591
+
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Providers;
+
+namespace MediaBrowser.Providers.Plugins.AudioDb
+{
+    public class AudioDbArtistExternalId : IExternalId
+    {
+        /// <inheritdoc />
+        public string ProviderName => "TheAudioDb";
+
+        /// <inheritdoc />
+        public string Key => MetadataProvider.AudioDbArtist.ToString();
+
+        /// <inheritdoc />
+        public ExternalIdMediaType? Type => ExternalIdMediaType.Artist;
+
+        /// <inheritdoc />
+        public string UrlFormatString => "https://www.theaudiodb.com/artist/{0}";
+
+        /// <inheritdoc />
+        public bool Supports(IHasProviderIds item) => item is MusicArtist;
+    }
+}

+ 27 - 0
MediaBrowser.Providers/Plugins/AudioDb/AudioDbOtherAlbumExternalId.cs

@@ -0,0 +1,27 @@
+#pragma warning disable CS1591
+
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Providers;
+
+namespace MediaBrowser.Providers.Plugins.AudioDb
+{
+    public class AudioDbOtherAlbumExternalId : IExternalId
+    {
+        /// <inheritdoc />
+        public string ProviderName => "TheAudioDb";
+
+        /// <inheritdoc />
+        public string Key => MetadataProvider.AudioDbAlbum.ToString();
+
+        /// <inheritdoc />
+        public ExternalIdMediaType? Type => ExternalIdMediaType.Album;
+
+        /// <inheritdoc />
+        public string UrlFormatString => "https://www.theaudiodb.com/album/{0}";
+
+        /// <inheritdoc />
+        public bool Supports(IHasProviderIds item) => item is Audio;
+    }
+}

+ 27 - 0
MediaBrowser.Providers/Plugins/AudioDb/AudioDbOtherArtistExternalId.cs

@@ -0,0 +1,27 @@
+#pragma warning disable CS1591
+
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Providers;
+
+namespace MediaBrowser.Providers.Plugins.AudioDb
+{
+    public class AudioDbOtherArtistExternalId : IExternalId
+    {
+        /// <inheritdoc />
+        public string ProviderName => "TheAudioDb";
+
+        /// <inheritdoc />
+        public string Key => MetadataProvider.AudioDbArtist.ToString();
+
+        /// <inheritdoc />
+        public ExternalIdMediaType? Type => ExternalIdMediaType.OtherArtist;
+
+        /// <inheritdoc />
+        public string UrlFormatString => "https://www.theaudiodb.com/artist/{0}";
+
+        /// <inheritdoc />
+        public bool Supports(IHasProviderIds item) => item is Audio || item is MusicAlbum;
+    }
+}

+ 0 - 81
MediaBrowser.Providers/Plugins/AudioDb/ExternalIds.cs

@@ -1,81 +0,0 @@
-#pragma warning disable CS1591
-
-using MediaBrowser.Controller.Entities.Audio;
-using MediaBrowser.Controller.Providers;
-using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Providers;
-
-namespace MediaBrowser.Providers.Plugins.AudioDb
-{
-    public class AudioDbAlbumExternalId : IExternalId
-    {
-        /// <inheritdoc />
-        public string ProviderName => "TheAudioDb";
-
-        /// <inheritdoc />
-        public string Key => MetadataProvider.AudioDbAlbum.ToString();
-
-        /// <inheritdoc />
-        public ExternalIdMediaType? Type => null;
-
-        /// <inheritdoc />
-        public string UrlFormatString => "https://www.theaudiodb.com/album/{0}";
-
-        /// <inheritdoc />
-        public bool Supports(IHasProviderIds item) => item is MusicAlbum;
-    }
-
-    public class AudioDbOtherAlbumExternalId : IExternalId
-    {
-        /// <inheritdoc />
-        public string ProviderName => "TheAudioDb";
-
-        /// <inheritdoc />
-        public string Key => MetadataProvider.AudioDbAlbum.ToString();
-
-        /// <inheritdoc />
-        public ExternalIdMediaType? Type => ExternalIdMediaType.Album;
-
-        /// <inheritdoc />
-        public string UrlFormatString => "https://www.theaudiodb.com/album/{0}";
-
-        /// <inheritdoc />
-        public bool Supports(IHasProviderIds item) => item is Audio;
-    }
-
-    public class AudioDbArtistExternalId : IExternalId
-    {
-        /// <inheritdoc />
-        public string ProviderName => "TheAudioDb";
-
-        /// <inheritdoc />
-        public string Key => MetadataProvider.AudioDbArtist.ToString();
-
-        /// <inheritdoc />
-        public ExternalIdMediaType? Type => ExternalIdMediaType.Artist;
-
-        /// <inheritdoc />
-        public string UrlFormatString => "https://www.theaudiodb.com/artist/{0}";
-
-        /// <inheritdoc />
-        public bool Supports(IHasProviderIds item) => item is MusicArtist;
-    }
-
-    public class AudioDbOtherArtistExternalId : IExternalId
-    {
-        /// <inheritdoc />
-        public string ProviderName => "TheAudioDb";
-
-        /// <inheritdoc />
-        public string Key => MetadataProvider.AudioDbArtist.ToString();
-
-        /// <inheritdoc />
-        public ExternalIdMediaType? Type => ExternalIdMediaType.OtherArtist;
-
-        /// <inheritdoc />
-        public string UrlFormatString => "https://www.theaudiodb.com/artist/{0}";
-
-        /// <inheritdoc />
-        public bool Supports(IHasProviderIds item) => item is Audio || item is MusicAlbum;
-    }
-}

+ 6 - 6
MediaBrowser.Providers/Plugins/AudioDb/Plugin.cs

@@ -11,6 +11,12 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
 {
     public class Plugin : BasePlugin<PluginConfiguration>, IHasWebPages
     {
+        public Plugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer)
+            : base(applicationPaths, xmlSerializer)
+        {
+            Instance = this;
+        }
+
         public static Plugin Instance { get; private set; }
 
         public override Guid Id => new Guid("a629c0da-fac5-4c7e-931a-7174223f14c8");
@@ -22,12 +28,6 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
         // TODO remove when plugin removed from server.
         public override string ConfigurationFileName => "Jellyfin.Plugin.AudioDb.xml";
 
-        public Plugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer)
-            : base(applicationPaths, xmlSerializer)
-        {
-            Instance = this;
-        }
-
         public IEnumerable<PluginPageInfo> GetPages()
         {
             yield return new PluginPageInfo

+ 15 - 14
MediaBrowser.Providers/Plugins/MusicBrainz/AlbumProvider.cs → MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs

@@ -8,7 +8,6 @@ using System.IO;
 using System.Linq;
 using System.Net;
 using System.Net.Http;
-using System.Net.Http.Headers;
 using System.Text;
 using System.Threading;
 using System.Threading.Tasks;
@@ -26,14 +25,6 @@ namespace MediaBrowser.Providers.Music
 {
     public class MusicBrainzAlbumProvider : IRemoteMetadataProvider<MusicAlbum, AlbumInfo>, IHasOrder
     {
-        /// <summary>
-        /// The Jellyfin user-agent is unrestricted but source IP must not exceed
-        /// one request per second, therefore we rate limit to avoid throttling.
-        /// Be prudent, use a value slightly above the minimun required.
-        /// https://musicbrainz.org/doc/XML_Web_Service/Rate_Limiting
-        /// </summary>
-        private readonly long _musicBrainzQueryIntervalMs;
-
         /// <summary>
         /// For each single MB lookup/search, this is the maximum number of
         /// attempts that shall be made whilst receiving a 503 Server
@@ -41,7 +32,13 @@ namespace MediaBrowser.Providers.Music
         /// </summary>
         private const uint MusicBrainzQueryAttempts = 5u;
 
-        internal static MusicBrainzAlbumProvider Current;
+        /// <summary>
+        /// The Jellyfin user-agent is unrestricted but source IP must not exceed
+        /// one request per second, therefore we rate limit to avoid throttling.
+        /// Be prudent, use a value slightly above the minimun required.
+        /// https://musicbrainz.org/doc/XML_Web_Service/Rate_Limiting
+        /// </summary>
+        private readonly long _musicBrainzQueryIntervalMs;
 
         private readonly IHttpClientFactory _httpClientFactory;
         private readonly IApplicationHost _appHost;
@@ -69,6 +66,8 @@ namespace MediaBrowser.Providers.Music
             Current = this;
         }
 
+        internal static MusicBrainzAlbumProvider Current { get; private set; }
+
         /// <inheritdoc />
         public string Name => "MusicBrainz";
 
@@ -112,7 +111,7 @@ namespace MediaBrowser.Providers.Music
                 else
                 {
                     // I'm sure there is a better way but for now it resolves search for 12" Mixes
-                    var queryName = searchInfo.Name.Replace("\"", string.Empty);
+                    var queryName = searchInfo.Name.Replace("\"", string.Empty, StringComparison.Ordinal);
 
                     url = string.Format(
                         CultureInfo.InvariantCulture,
@@ -277,7 +276,9 @@ namespace MediaBrowser.Providers.Music
 
         private async Task<ReleaseResult> GetReleaseResult(string albumName, string artistId, CancellationToken cancellationToken)
         {
-            var url = string.Format(CultureInfo.InvariantCulture, "/ws/2/release/?query=\"{0}\" AND arid:{1}",
+            var url = string.Format(
+                CultureInfo.InvariantCulture,
+                "/ws/2/release/?query=\"{0}\" AND arid:{1}",
                 WebUtility.UrlEncode(albumName),
                 artistId);
 
@@ -496,7 +497,7 @@ namespace MediaBrowser.Providers.Music
             }
         }
 
-        private static ValueTuple<string, string> ParseArtistCredit(XmlReader reader)
+        private static (string, string) ParseArtistCredit(XmlReader reader)
         {
             reader.MoveToContent();
             reader.Read();
@@ -531,7 +532,7 @@ namespace MediaBrowser.Providers.Music
                 }
             }
 
-            return new ValueTuple<string, string>();
+            return default;
         }
 
         private static (string, string) ParseArtistNameCredit(XmlReader reader)

+ 6 - 6
MediaBrowser.Providers/Plugins/Omdb/OmdbImageProvider.cs

@@ -36,6 +36,12 @@ namespace MediaBrowser.Providers.Plugins.Omdb
             _appHost = appHost;
         }
 
+        public string Name => "The Open Movie Database";
+
+        // After other internet providers, because they're better
+        // But before fallback providers like screengrab
+        public int Order => 90;
+
         public IEnumerable<ImageType> GetSupportedImages(BaseItem item)
         {
             return new List<ImageType>
@@ -86,15 +92,9 @@ namespace MediaBrowser.Providers.Plugins.Omdb
             return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken);
         }
 
-        public string Name => "The Open Movie Database";
-
         public bool Supports(BaseItem item)
         {
             return item is Movie || item is Trailer || item is Episode;
         }
-
-        // After other internet providers, because they're better
-        // But before fallback providers like screengrab
-        public int Order => 90;
     }
 }

+ 5 - 5
MediaBrowser.Providers/Plugins/Omdb/OmdbItemProvider.cs

@@ -49,6 +49,8 @@ namespace MediaBrowser.Providers.Plugins.Omdb
             _appHost = appHost;
         }
 
+        public string Name => "The Open Movie Database";
+
         // After primary option
         public int Order => 2;
 
@@ -199,8 +201,6 @@ namespace MediaBrowser.Providers.Plugins.Omdb
             return GetSearchResults(searchInfo, "movie", cancellationToken);
         }
 
-        public string Name => "The Open Movie Database";
-
         public async Task<MetadataResult<Series>> GetMetadata(SeriesInfo info, CancellationToken cancellationToken)
         {
             var result = new MetadataResult<Series>
@@ -263,14 +263,14 @@ namespace MediaBrowser.Providers.Plugins.Omdb
         {
             var results = await GetSearchResultsInternal(info, "movie", false, cancellationToken).ConfigureAwait(false);
             var first = results.FirstOrDefault();
-            return first == null ? null : first.GetProviderId(MetadataProvider.Imdb);
+            return first?.GetProviderId(MetadataProvider.Imdb);
         }
 
         private async Task<string> GetSeriesImdbId(SeriesInfo info, CancellationToken cancellationToken)
         {
             var results = await GetSearchResultsInternal(info, "series", false, cancellationToken).ConfigureAwait(false);
             var first = results.FirstOrDefault();
-            return first == null ? null : first.GetProviderId(MetadataProvider.Imdb);
+            return first?.GetProviderId(MetadataProvider.Imdb);
         }
 
         public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
@@ -278,7 +278,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb
             return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken);
         }
 
-        class SearchResult
+        private class SearchResult
         {
             public string Title { get; set; }
 

+ 3 - 6
MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetProvider.cs

@@ -37,7 +37,6 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.BoxSets
         private readonly IJsonSerializer _json;
         private readonly IServerConfigurationManager _config;
         private readonly IFileSystem _fileSystem;
-        private readonly ILocalizationManager _localization;
         private readonly IHttpClientFactory _httpClientFactory;
         private readonly ILibraryManager _libraryManager;
 
@@ -46,7 +45,6 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.BoxSets
             IJsonSerializer json,
             IServerConfigurationManager config,
             IFileSystem fileSystem,
-            ILocalizationManager localization,
             IHttpClientFactory httpClientFactory,
             ILibraryManager libraryManager)
         {
@@ -54,7 +52,6 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.BoxSets
             _json = json;
             _config = config;
             _fileSystem = fileSystem;
-            _localization = localization;
             _httpClientFactory = httpClientFactory;
             _libraryManager = libraryManager;
             Current = this;
@@ -177,7 +174,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.BoxSets
 
         private async Task<CollectionResult> FetchMainResult(string id, string language, CancellationToken cancellationToken)
         {
-            var url = string.Format(GetCollectionInfo3, id, TmdbUtils.ApiKey);
+            var url = string.Format(CultureInfo.InvariantCulture, GetCollectionInfo3, id, TmdbUtils.ApiKey);
 
             if (!string.IsNullOrEmpty(language))
             {
@@ -195,7 +192,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.BoxSets
                 requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(header));
             }
 
-            using var mainResponse = await TmdbMovieProvider.Current.GetMovieDbResponse(requestMessage);
+            using var mainResponse = await TmdbMovieProvider.Current.GetMovieDbResponse(requestMessage, cancellationToken).ConfigureAwait(false);
             await using var stream = await mainResponse.Content.ReadAsStreamAsync().ConfigureAwait(false);
             var mainResult = await _json.DeserializeFromStreamAsync<CollectionResult>(stream).ConfigureAwait(false);
 
@@ -205,7 +202,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.BoxSets
             {
                 if (!string.IsNullOrEmpty(language) && !string.Equals(language, "en", StringComparison.OrdinalIgnoreCase))
                 {
-                    url = string.Format(GetCollectionInfo3, id, TmdbUtils.ApiKey) + "&language=en";
+                    url = string.Format(CultureInfo.InvariantCulture, GetCollectionInfo3, id, TmdbUtils.ApiKey) + "&language=en";
 
                     if (!string.IsNullOrEmpty(language))
                     {

+ 5 - 5
MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs

@@ -155,7 +155,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
                 requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(header));
             }
 
-            using var response = await GetMovieDbResponse(requestMessage).ConfigureAwait(false);
+            using var response = await GetMovieDbResponse(requestMessage, cancellationToken).ConfigureAwait(false);
             await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
             _tmdbSettings = await _jsonSerializer.DeserializeFromStreamAsync<TmdbSettingsResult>(stream).ConfigureAwait(false);
             return _tmdbSettings;
@@ -335,7 +335,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
                 requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(header));
             }
 
-            using var mainResponse = await GetMovieDbResponse(requestMessage).ConfigureAwait(false);
+            using var mainResponse = await GetMovieDbResponse(requestMessage, cancellationToken).ConfigureAwait(false);
             if (mainResponse.StatusCode == HttpStatusCode.NotFound)
             {
                 return null;
@@ -368,7 +368,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
                     langRequestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(header));
                 }
 
-                using var langResponse = await GetMovieDbResponse(langRequestMessage).ConfigureAwait(false);
+                using var langResponse = await GetMovieDbResponse(langRequestMessage, cancellationToken).ConfigureAwait(false);
 
                 await using var langStream = await langResponse.Content.ReadAsStreamAsync().ConfigureAwait(false);
                 var langResult = await _jsonSerializer.DeserializeFromStreamAsync<MovieResult>(stream).ConfigureAwait(false);
@@ -381,10 +381,10 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
         /// <summary>
         /// Gets the movie db response.
         /// </summary>
-        internal Task<HttpResponseMessage> GetMovieDbResponse(HttpRequestMessage message)
+        internal Task<HttpResponseMessage> GetMovieDbResponse(HttpRequestMessage message, CancellationToken cancellationToken = default)
         {
             message.Headers.UserAgent.ParseAdd(_appHost.ApplicationUserAgent);
-            return _httpClientFactory.CreateClient(NamedClient.Default).SendAsync(message);
+            return _httpClientFactory.CreateClient(NamedClient.Default).SendAsync(message, cancellationToken);
         }
 
         /// <inheritdoc />

+ 2 - 2
MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbSearch.cs

@@ -198,7 +198,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
                 requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(header));
             }
 
-            using var response = await TmdbMovieProvider.Current.GetMovieDbResponse(requestMessage).ConfigureAwait(false);
+            using var response = await TmdbMovieProvider.Current.GetMovieDbResponse(requestMessage, cancellationToken).ConfigureAwait(false);
             await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
             var searchResults = await _json.DeserializeFromStreamAsync<TmdbSearchResult<MovieResult>>(stream).ConfigureAwait(false);
 
@@ -261,7 +261,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
                 requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(header));
             }
 
-            using var response = await TmdbMovieProvider.Current.GetMovieDbResponse(requestMessage).ConfigureAwait(false);
+            using var response = await TmdbMovieProvider.Current.GetMovieDbResponse(requestMessage, cancellationToken).ConfigureAwait(false);
             await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
             var searchResults = await _json.DeserializeFromStreamAsync<TmdbSearchResult<TvResult>>(stream).ConfigureAwait(false);
 

+ 2 - 2
MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs

@@ -110,7 +110,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People
                 requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(header));
             }
 
-            var response = await TmdbMovieProvider.Current.GetMovieDbResponse(requestMessage).ConfigureAwait(false);
+            var response = await TmdbMovieProvider.Current.GetMovieDbResponse(requestMessage, cancellationToken).ConfigureAwait(false);
             await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
 
             var result2 = await _jsonSerializer.DeserializeFromStreamAsync<TmdbSearchResult<PersonSearchResult>>(stream).ConfigureAwait(false)
@@ -243,7 +243,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People
                 requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(header));
             }
 
-            using var response = await TmdbMovieProvider.Current.GetMovieDbResponse(requestMessage).ConfigureAwait(false);
+            using var response = await TmdbMovieProvider.Current.GetMovieDbResponse(requestMessage, cancellationToken).ConfigureAwait(false);
             Directory.CreateDirectory(Path.GetDirectoryName(dataFilePath));
             await using var fs = new FileStream(dataFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, true);
             await response.Content.CopyToAsync(fs).ConfigureAwait(false);

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

@@ -109,7 +109,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
                 item.ParentIndexNumber = info.ParentIndexNumber;
                 item.IndexNumberEnd = info.IndexNumberEnd;
 
-                if (response.External_Ids.Tvdb_Id > 0)
+                if (response.External_Ids != null && response.External_Ids.Tvdb_Id > 0)
                 {
                     item.SetProviderId(MetadataProvider.Tvdb, response.External_Ids.Tvdb_Id.Value.ToString(CultureInfo.InvariantCulture));
                 }

+ 8 - 2
MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProviderBase.cs

@@ -113,7 +113,13 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
 
         internal async Task<EpisodeResult> FetchMainResult(string urlPattern, string id, int seasonNumber, int episodeNumber, string language, CancellationToken cancellationToken)
         {
-            var url = string.Format(urlPattern, id, seasonNumber.ToString(CultureInfo.InvariantCulture), episodeNumber, TmdbUtils.ApiKey);
+            var url = string.Format(
+                CultureInfo.InvariantCulture,
+                urlPattern,
+                id,
+                seasonNumber.ToString(CultureInfo.InvariantCulture),
+                episodeNumber,
+                TmdbUtils.ApiKey);
 
             if (!string.IsNullOrEmpty(language))
             {
@@ -132,7 +138,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
                 requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(header));
             }
 
-            using var response = await TmdbMovieProvider.Current.GetMovieDbResponse(requestMessage);
+            using var response = await TmdbMovieProvider.Current.GetMovieDbResponse(requestMessage, cancellationToken).ConfigureAwait(false);
             await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
             return await _jsonSerializer.DeserializeFromStreamAsync<EpisodeResult>(stream).ConfigureAwait(false);
         }

+ 8 - 3
MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs

@@ -75,7 +75,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
 
                     result.Item.Overview = seasonInfo.Overview;
 
-                    if (seasonInfo.External_Ids.Tvdb_Id > 0)
+                    if (seasonInfo.External_Ids != null && seasonInfo.External_Ids.Tvdb_Id > 0)
                     {
                         result.Item.SetProviderId(MetadataProvider.Tvdb, seasonInfo.External_Ids.Tvdb_Id.Value.ToString(CultureInfo.InvariantCulture));
                     }
@@ -200,7 +200,12 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
 
         internal async Task<SeasonResult> FetchMainResult(string id, int seasonNumber, string language, CancellationToken cancellationToken)
         {
-            var url = string.Format(GetTvInfo3, id, seasonNumber.ToString(CultureInfo.InvariantCulture), TmdbUtils.ApiKey);
+            var url = string.Format(
+                CultureInfo.InvariantCulture,
+                GetTvInfo3,
+                id,
+                seasonNumber.ToString(CultureInfo.InvariantCulture),
+                TmdbUtils.ApiKey);
 
             if (!string.IsNullOrEmpty(language))
             {
@@ -219,7 +224,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
                 requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(header));
             }
 
-            using var response = await TmdbMovieProvider.Current.GetMovieDbResponse(requestMessage).ConfigureAwait(false);
+            using var response = await TmdbMovieProvider.Current.GetMovieDbResponse(requestMessage, cancellationToken).ConfigureAwait(false);
             await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
             return await _jsonSerializer.DeserializeFromStreamAsync<SeasonResult>(stream).ConfigureAwait(false);
         }

+ 9 - 7
MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs

@@ -92,7 +92,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
                 remoteResult.SetProviderId(MetadataProvider.Tmdb, obj.Id.ToString(_usCulture));
                 remoteResult.SetProviderId(MetadataProvider.Imdb, obj.External_Ids.Imdb_Id);
 
-                if (obj.External_Ids.Tvdb_Id > 0)
+                if (obj.External_Ids != null && obj.External_Ids.Tvdb_Id > 0)
                 {
                     remoteResult.SetProviderId(MetadataProvider.Tvdb, obj.External_Ids.Tvdb_Id.Value.ToString(_usCulture));
                 }
@@ -405,7 +405,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
 
         internal async Task<SeriesResult> FetchMainResult(string id, string language, CancellationToken cancellationToken)
         {
-            var url = string.Format(GetTvInfo3, id, TmdbUtils.ApiKey);
+            var url = string.Format(CultureInfo.InvariantCulture, GetTvInfo3, id, TmdbUtils.ApiKey);
 
             if (!string.IsNullOrEmpty(language))
             {
@@ -421,7 +421,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
                 mainRequestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(header));
             }
 
-            using var mainResponse = await TmdbMovieProvider.Current.GetMovieDbResponse(mainRequestMessage);
+            using var mainResponse = await TmdbMovieProvider.Current.GetMovieDbResponse(mainRequestMessage, cancellationToken).ConfigureAwait(false);
             await using var mainStream = await mainResponse.Content.ReadAsStreamAsync().ConfigureAwait(false);
             var mainResult = await _jsonSerializer.DeserializeFromStreamAsync<SeriesResult>(mainStream).ConfigureAwait(false);
 
@@ -440,7 +440,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
             {
                 _logger.LogInformation("MovieDbSeriesProvider couldn't find meta for language {Language}. Trying English...", language);
 
-                url = string.Format(GetTvInfo3, id, TmdbUtils.ApiKey) + "&language=en";
+                url = string.Format(CultureInfo.InvariantCulture, GetTvInfo3, id, TmdbUtils.ApiKey) + "&language=en";
 
                 if (!string.IsNullOrEmpty(language))
                 {
@@ -454,7 +454,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
                     mainRequestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(header));
                 }
 
-                using var response = await TmdbMovieProvider.Current.GetMovieDbResponse(requestMessage);
+                using var response = await TmdbMovieProvider.Current.GetMovieDbResponse(requestMessage, cancellationToken).ConfigureAwait(false);
                 await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
                 var englishResult = await _jsonSerializer.DeserializeFromStreamAsync<SeriesResult>(stream).ConfigureAwait(false);
 
@@ -504,7 +504,9 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
 
         private async Task<RemoteSearchResult> FindByExternalId(string id, string externalSource, CancellationToken cancellationToken)
         {
-            var url = string.Format(TmdbUtils.BaseTmdbApiUrl + @"3/find/{0}?api_key={1}&external_source={2}",
+            var url = string.Format(
+                CultureInfo.InvariantCulture,
+                TmdbUtils.BaseTmdbApiUrl + @"3/find/{0}?api_key={1}&external_source={2}",
                 id,
                 TmdbUtils.ApiKey,
                 externalSource);
@@ -515,7 +517,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
                 requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(header));
             }
 
-            using var response = await TmdbMovieProvider.Current.GetMovieDbResponse(requestMessage);
+            using var response = await TmdbMovieProvider.Current.GetMovieDbResponse(requestMessage, cancellationToken).ConfigureAwait(false);
             await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
 
             var result = await _jsonSerializer.DeserializeFromStreamAsync<ExternalIdLookupResult>(stream).ConfigureAwait(false);

+ 9 - 5
MediaBrowser.Providers/TV/DummySeasonProvider.cs

@@ -124,7 +124,8 @@ namespace MediaBrowser.Providers.TV
         /// <summary>
         /// Adds the season.
         /// </summary>
-        public async Task<Season> AddSeason(Series series,
+        public async Task<Season> AddSeason(
+            Series series,
             int? seasonNumber,
             bool isVirtualItem,
             CancellationToken cancellationToken)
@@ -211,11 +212,14 @@ namespace MediaBrowser.Providers.TV
             {
                 _logger.LogInformation("Removing virtual season {0} {1}", series.Name, seasonToRemove.IndexNumber);
 
-                _libraryManager.DeleteItem(seasonToRemove, new DeleteOptions
-                {
-                    DeleteFileLocation = true
+                _libraryManager.DeleteItem(
+                    seasonToRemove,
+                    new DeleteOptions
+                    {
+                        DeleteFileLocation = true
 
-                }, false);
+                    },
+                    false);
 
                 hasChanges = true;
             }

+ 20 - 11
MediaBrowser.Providers/TV/MissingEpisodeProvider.cs

@@ -159,7 +159,7 @@ namespace MediaBrowser.Providers.TV
 
                 var now = DateTime.UtcNow.AddDays(-UnairedEpisodeThresholdDays);
 
-                if (airDate < now && addMissingEpisodes || airDate > now)
+                if ((airDate < now && addMissingEpisodes) || airDate > now)
                 {
                     // tvdb has a lot of nearly blank episodes
                     _logger.LogInformation("Creating virtual missing/unaired episode {0} {1}x{2}", series.Name, tuple.seasonNumber, tuple.episodenumber);
@@ -232,10 +232,13 @@ namespace MediaBrowser.Providers.TV
 
             foreach (var episodeToRemove in episodesToRemove)
             {
-                _libraryManager.DeleteItem(episodeToRemove, new DeleteOptions
-                {
-                    DeleteFileLocation = true
-                }, false);
+                _libraryManager.DeleteItem(
+                    episodeToRemove,
+                    new DeleteOptions
+                    {
+                        DeleteFileLocation = true
+                    },
+                    false);
 
                 hasChanges = true;
             }
@@ -246,7 +249,7 @@ namespace MediaBrowser.Providers.TV
         /// <summary>
         /// Removes the obsolete or missing seasons.
         /// </summary>
-        /// <param name="allRecursiveChildren"></param>
+        /// <param name="allRecursiveChildren">All recursive children.</param>
         /// <param name="episodeLookup">The episode lookup.</param>
         /// <returns><see cref="bool" />.</returns>
         private bool RemoveObsoleteOrMissingSeasons(
@@ -297,10 +300,13 @@ namespace MediaBrowser.Providers.TV
 
             foreach (var seasonToRemove in seasonsToRemove)
             {
-                _libraryManager.DeleteItem(seasonToRemove, new DeleteOptions
-                {
-                    DeleteFileLocation = true
-                }, false);
+                _libraryManager.DeleteItem(
+                    seasonToRemove,
+                    new DeleteOptions
+                    {
+                        DeleteFileLocation = true
+                    },
+                    false);
 
                 hasChanges = true;
             }
@@ -354,7 +360,10 @@ namespace MediaBrowser.Providers.TV
         /// <param name="seasonCounts"></param>
         /// <param name="episodeTuple"></param>
         /// <returns>Episode.</returns>
-        private Episode GetExistingEpisode(IEnumerable<Episode> existingEpisodes, IReadOnlyDictionary<int, int> seasonCounts, (int seasonNumber, int episodeNumber, DateTime firstAired) episodeTuple)
+        private Episode GetExistingEpisode(
+            IEnumerable<Episode> existingEpisodes,
+            IReadOnlyDictionary<int, int> seasonCounts,
+            (int seasonNumber, int episodeNumber, DateTime firstAired) episodeTuple)
         {
             var seasonNumber = episodeTuple.seasonNumber;
             var episodeNumber = episodeTuple.episodeNumber;

+ 0 - 82
MediaBrowser.Providers/TV/TvExternalIds.cs

@@ -1,82 +0,0 @@
-#pragma warning disable CS1591
-
-using MediaBrowser.Controller.Entities.TV;
-using MediaBrowser.Controller.Providers;
-using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Providers;
-using MediaBrowser.Providers.Plugins.TheTvdb;
-
-namespace MediaBrowser.Providers.TV
-{
-    public class Zap2ItExternalId : IExternalId
-    {
-        /// <inheritdoc />
-        public string ProviderName => "Zap2It";
-
-        /// <inheritdoc />
-        public string Key => MetadataProvider.Zap2It.ToString();
-
-        /// <inheritdoc />
-        public ExternalIdMediaType? Type => null;
-
-        /// <inheritdoc />
-        public string UrlFormatString => "http://tvlistings.zap2it.com/overview.html?programSeriesId={0}";
-
-        /// <inheritdoc />
-        public bool Supports(IHasProviderIds item) => item is Series;
-    }
-
-    public class TvdbExternalId : IExternalId
-    {
-        /// <inheritdoc />
-        public string ProviderName => "TheTVDB";
-
-        /// <inheritdoc />
-        public string Key => MetadataProvider.Tvdb.ToString();
-
-        /// <inheritdoc />
-        public ExternalIdMediaType? Type => null;
-
-        /// <inheritdoc />
-        public string UrlFormatString => TvdbUtils.TvdbBaseUrl + "?tab=series&id={0}";
-
-        /// <inheritdoc />
-        public bool Supports(IHasProviderIds item) => item is Series;
-    }
-
-    public class TvdbSeasonExternalId : IExternalId
-    {
-        /// <inheritdoc />
-        public string ProviderName => "TheTVDB";
-
-        /// <inheritdoc />
-        public string Key => MetadataProvider.Tvdb.ToString();
-
-        /// <inheritdoc />
-        public ExternalIdMediaType? Type => ExternalIdMediaType.Season;
-
-        /// <inheritdoc />
-        public string UrlFormatString => null;
-
-        /// <inheritdoc />
-        public bool Supports(IHasProviderIds item) => item is Season;
-    }
-
-    public class TvdbEpisodeExternalId : IExternalId
-    {
-        /// <inheritdoc />
-        public string ProviderName => "TheTVDB";
-
-        /// <inheritdoc />
-        public string Key => MetadataProvider.Tvdb.ToString();
-
-        /// <inheritdoc />
-        public ExternalIdMediaType? Type => ExternalIdMediaType.Episode;
-
-        /// <inheritdoc />
-        public string UrlFormatString => TvdbUtils.TvdbBaseUrl + "?tab=episode&id={0}";
-
-        /// <inheritdoc />
-        public bool Supports(IHasProviderIds item) => item is Episode;
-    }
-}

+ 28 - 0
MediaBrowser.Providers/TV/TvdbEpisodeExternalId.cs

@@ -0,0 +1,28 @@
+#pragma warning disable CS1591
+
+using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Providers;
+using MediaBrowser.Providers.Plugins.TheTvdb;
+
+namespace MediaBrowser.Providers.TV
+{
+    public class TvdbEpisodeExternalId : IExternalId
+    {
+        /// <inheritdoc />
+        public string ProviderName => "TheTVDB";
+
+        /// <inheritdoc />
+        public string Key => MetadataProvider.Tvdb.ToString();
+
+        /// <inheritdoc />
+        public ExternalIdMediaType? Type => ExternalIdMediaType.Episode;
+
+        /// <inheritdoc />
+        public string UrlFormatString => TvdbUtils.TvdbBaseUrl + "?tab=episode&id={0}";
+
+        /// <inheritdoc />
+        public bool Supports(IHasProviderIds item) => item is Episode;
+    }
+}

+ 28 - 0
MediaBrowser.Providers/TV/TvdbExternalId.cs

@@ -0,0 +1,28 @@
+#pragma warning disable CS1591
+
+using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Providers;
+using MediaBrowser.Providers.Plugins.TheTvdb;
+
+namespace MediaBrowser.Providers.TV
+{
+    public class TvdbExternalId : IExternalId
+    {
+        /// <inheritdoc />
+        public string ProviderName => "TheTVDB";
+
+        /// <inheritdoc />
+        public string Key => MetadataProvider.Tvdb.ToString();
+
+        /// <inheritdoc />
+        public ExternalIdMediaType? Type => null;
+
+        /// <inheritdoc />
+        public string UrlFormatString => TvdbUtils.TvdbBaseUrl + "?tab=series&id={0}";
+
+        /// <inheritdoc />
+        public bool Supports(IHasProviderIds item) => item is Series;
+    }
+}

+ 28 - 0
MediaBrowser.Providers/TV/TvdbSeasonExternalId.cs

@@ -0,0 +1,28 @@
+#pragma warning disable CS1591
+
+using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Providers;
+using MediaBrowser.Providers.Plugins.TheTvdb;
+
+namespace MediaBrowser.Providers.TV
+{
+    public class TvdbSeasonExternalId : IExternalId
+    {
+        /// <inheritdoc />
+        public string ProviderName => "TheTVDB";
+
+        /// <inheritdoc />
+        public string Key => MetadataProvider.Tvdb.ToString();
+
+        /// <inheritdoc />
+        public ExternalIdMediaType? Type => ExternalIdMediaType.Season;
+
+        /// <inheritdoc />
+        public string UrlFormatString => null;
+
+        /// <inheritdoc />
+        public bool Supports(IHasProviderIds item) => item is Season;
+    }
+}

+ 28 - 0
MediaBrowser.Providers/TV/Zap2ItExternalId.cs

@@ -0,0 +1,28 @@
+#pragma warning disable CS1591
+
+using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Providers;
+using MediaBrowser.Providers.Plugins.TheTvdb;
+
+namespace MediaBrowser.Providers.TV
+{
+    public class Zap2ItExternalId : IExternalId
+    {
+        /// <inheritdoc />
+        public string ProviderName => "Zap2It";
+
+        /// <inheritdoc />
+        public string Key => MetadataProvider.Zap2It.ToString();
+
+        /// <inheritdoc />
+        public ExternalIdMediaType? Type => null;
+
+        /// <inheritdoc />
+        public string UrlFormatString => "http://tvlistings.zap2it.com/overview.html?programSeriesId={0}";
+
+        /// <inheritdoc />
+        public bool Supports(IHasProviderIds item) => item is Series;
+    }
+}

+ 0 - 1
MediaBrowser.sln

@@ -208,6 +208,5 @@ Global
 		{A2FD0A10-8F62-4F9D-B171-FFDF9F0AFA9D} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6}
 		{2E3A1B4B-4225-4AAA-8B29-0181A84E7AEE} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6}
 		{462584F7-5023-4019-9EAC-B98CA458C0A0} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6}
-		{7C93C84F-105C-48E5-A878-406FA0A5B296} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6}
 	EndGlobalSection
 EndGlobal