diff --git a/source/Generic/ExtraMetadataLoader/ExtraMetadataLoader.cs b/source/Generic/ExtraMetadataLoader/ExtraMetadataLoader.cs index 353093861..91d64958d 100644 --- a/source/Generic/ExtraMetadataLoader/ExtraMetadataLoader.cs +++ b/source/Generic/ExtraMetadataLoader/ExtraMetadataLoader.cs @@ -28,6 +28,7 @@ using YouTubeCommon; using Microsoft.Extensions.DependencyInjection; using EventsCommon; +using ExtraMetadataLoader.MetadataProviders.LaunchBox; using ExtraMetadataLoader.VideosProcessor; namespace ExtraMetadataLoader @@ -158,6 +159,8 @@ public ExtraMetadataLoader(IPlayniteAPI api) : base(api) services.AddTransient(); services.AddTransient(); + services.AddSingleton(new LaunchBoxMetadataCache(Path.Combine(GetPluginUserDataPath(), "LaunchBox"), _logger)); + services.AddTransient(); services.AddTransient(); _serviceProvider = services.BuildServiceProvider(); @@ -422,6 +425,40 @@ public override IEnumerable GetGameMenuItems(GetGameMenuItemsArgs } }, new GameMenuItem + { + Description = ResourceProvider.GetString("LOCExtra_Metadata_Loader_MenuItemDescriptionDownloadLaunchBoxLogosSelectedGames"), + MenuSection = $"Extra Metadata|{logosSection}", + Icon = "emtDownloadIcon", + Action = _ => { + var overwrite = GetBoolFromYesNoDialog(ResourceProvider.GetString("LOCExtra_Metadata_Loader_DialogMessageOverwriteLogosChoice")); + var isBackgroundDownload = GetBoolFromYesNoDialog(ResourceProvider.GetString("LOCExtra_Metadata_Loader_DialogAskSelectLogosAutomatically")); + var progressTitle = ResourceProvider.GetString("LOCExtra_Metadata_Loader_DialogMessageDownloadingLogosLaunchBox"); + + var progressOptions = new GlobalProgressOptions(progressTitle, true); + progressOptions.IsIndeterminate = false; + var metadataDownloadService = _serviceProvider.GetRequiredService(); + var logoProvider = metadataDownloadService.GetLogoProviderById("launchBoxProvider"); + PlayniteApi.Dialogs.ActivateGlobalProgress((a) => + { + var games = args.Games.Distinct(); + a.ProgressMaxValue = games.Count() + 1; + foreach (var game in games) + { + if (a.CancelToken.IsCancellationRequested) + { + break; + } + + a.CurrentProgressValue++; + a.Text = $"{progressTitle}\n\n{a.CurrentProgressValue}/{games.Count()}\n{game.Name}"; + + GetGameLogo(logoProvider, game, isBackgroundDownload, overwrite, a.CancelToken); + }; + }, progressOptions); + PlayniteApi.Dialogs.ShowMessage(ResourceProvider.GetString("LOCExtra_Metadata_Loader_DialogMessageDone"), "Extra Metadata Loader"); + } + }, + new GameMenuItem { Description = ResourceProvider.GetString("LOCExtra_Metadata_Loader_MenuItemDescriptionDownloadGoogleLogoSelectedGame"), MenuSection = $"Extra Metadata|{logosSection}", @@ -749,6 +786,17 @@ public override IEnumerable GetGameMenuItems(GetGameMenuItemsArgs return gameMenuItems; } + private void GetGameLogo(ILogoProvider logoProvider, Game game, bool isBackgroundDownload, bool overwrite, CancellationToken cancelToken = default) + { + var metadataDownloadService = _serviceProvider.GetRequiredService(); + metadataDownloadService.DownloadLogoAsync(logoProvider, game, isBackgroundDownload, overwrite, cancelToken).GetAwaiter().GetResult(); + } + + private bool ProcessLogoImage(string logoPath) + { + return _serviceProvider.GetRequiredService().ProcessLogoImage(logoPath); + } + private string GetPlatformName(Game game, bool appendSpace = false) { if (!game.Platforms.HasItems()) @@ -901,7 +949,6 @@ public override void OnLibraryUpdated(OnLibraryUpdatedEventArgs args) var progressTitle = ResourceProvider.GetString("LOCExtra_Metadata_Loader_DialogMessageLibUpdateAutomaticDownloadVideos"); var progressOptions = new GlobalProgressOptions(progressTitle, true); progressOptions.IsIndeterminate = false; - var downloadOptions = new VideoDownloadOptions(VideoType.Trailer,); PlayniteApi.Dialogs.ActivateGlobalProgress((a) => { var games = PlayniteApi.Database.Games.Where(x => x.Added.HasValue && x.Added > settings.Settings.LastAutoLibUpdateAssetsDownload); @@ -1036,4 +1083,4 @@ public override UserControl GetSettingsView(bool firstRunSettings) return new ExtraMetadataLoaderSettingsView(); } } -} \ No newline at end of file +} diff --git a/source/Generic/ExtraMetadataLoader/ExtraMetadataLoader.csproj b/source/Generic/ExtraMetadataLoader/ExtraMetadataLoader.csproj index 5384392f5..37d3e9fd2 100644 --- a/source/Generic/ExtraMetadataLoader/ExtraMetadataLoader.csproj +++ b/source/Generic/ExtraMetadataLoader/ExtraMetadataLoader.csproj @@ -81,6 +81,8 @@ + + ..\..\packages\System.Diagnostics.DiagnosticSource.6.0.0\lib\net461\System.Diagnostics.DiagnosticSource.dll @@ -189,6 +191,9 @@ + + + @@ -275,4 +280,4 @@ xcopy "$(ProjectDir)Localization\*.xaml" "$(TargetDir)\Localization" /Y /I /E - \ No newline at end of file + diff --git a/source/Generic/ExtraMetadataLoader/ExtraMetadataLoaderSettings.cs b/source/Generic/ExtraMetadataLoader/ExtraMetadataLoaderSettings.cs index 5823dd169..ca8cf6047 100644 --- a/source/Generic/ExtraMetadataLoader/ExtraMetadataLoaderSettings.cs +++ b/source/Generic/ExtraMetadataLoader/ExtraMetadataLoaderSettings.cs @@ -434,6 +434,18 @@ public bool LibUpdateSelectLogosAutomatically } } + [DontSerialize] + private bool useLaunchBoxForAutomaticLogoDownloads = false; + public bool UseLaunchBoxForAutomaticLogoDownloads + { + get => useLaunchBoxForAutomaticLogoDownloads; + set + { + useLaunchBoxForAutomaticLogoDownloads = value; + OnPropertyChanged(); + } + } + public DateTime LastAutoLibUpdateAssetsDownload = DateTime.MinValue; [DontSerialize] @@ -887,4 +899,4 @@ public void LoginToYoutube() webView.Dispose(); } } -} \ No newline at end of file +} diff --git a/source/Generic/ExtraMetadataLoader/ExtraMetadataLoaderSettingsView.xaml b/source/Generic/ExtraMetadataLoader/ExtraMetadataLoaderSettingsView.xaml index 9d7d96526..7d6e8ade9 100644 --- a/source/Generic/ExtraMetadataLoader/ExtraMetadataLoaderSettingsView.xaml +++ b/source/Generic/ExtraMetadataLoader/ExtraMetadataLoaderSettingsView.xaml @@ -184,6 +184,8 @@ + - \ No newline at end of file + diff --git a/source/Generic/ExtraMetadataLoader/Localization/af_ZA.xaml b/source/Generic/ExtraMetadataLoader/Localization/af_ZA.xaml index 007d6f540..4c4188d8a 100644 --- a/source/Generic/ExtraMetadataLoader/Localization/af_ZA.xaml +++ b/source/Generic/ExtraMetadataLoader/Localization/af_ZA.xaml @@ -47,6 +47,7 @@ Download videos of newly added games on library update Download Micro videos of newly added games on library update Download logos of newly added games on library update + Use LaunchBox for automatic logo downloads Select logos to download automatically Video @@ -65,6 +66,7 @@ Downloading videos from Steam... Downloading Micro videos from Steam... Downloading logos from Steam... + Downloading logos from LaunchBox... Downloading logos from SteamGridDB... Downloading video from YouTube... Failed to download video from YouTube. @@ -86,6 +88,7 @@ Please verify that your yt-dlp installation is up to date. Download videos from Steam for selected games Download Micro videos from Steam for selected games Download logos from Steam for selected games + Download logos from LaunchBox for selected games Download logos from SteamGridDB for selected games Open Extra Metadata directory Delete logos of selected games diff --git a/source/Generic/ExtraMetadataLoader/Localization/ar_SA.xaml b/source/Generic/ExtraMetadataLoader/Localization/ar_SA.xaml index f552a5cff..b8468dae2 100644 --- a/source/Generic/ExtraMetadataLoader/Localization/ar_SA.xaml +++ b/source/Generic/ExtraMetadataLoader/Localization/ar_SA.xaml @@ -47,6 +47,7 @@ تحميل مقاطع الفيديو للألعاب المضافة حديثا عند تحديث المكتبة تحميل مقاطع الفيديو الصغيرة للألعاب المضافة حديثا على تحديث المكتبة تحميل شعارات الألعاب المضافة حديثا على تحديث المكتبة + Use LaunchBox for automatic logo downloads حدد الشعارات لتحميلها تلقائيًا فيديو @@ -65,6 +66,7 @@ جاري تحميل مقاطع الفيديو من Steam... جاري تحميل مقاطع الفيديو الصغيرة من Steam... جاري تحميل الشعارات من Steam... + Downloading logos from LaunchBox... جاري تحميل الشعارات من SteamGridDB... جاري تحميل الفيديو من يوتيوب... فشل تحميل الفيديو من يوتيوب. @@ -86,6 +88,7 @@ تحميل مقاطع الفيديو من Steam للألعاب المحددة تحميل مقاطع الفيديو الصغيرة من Steam للألعاب المحددة تحميل الشعارات من Steam للألعاب المحددة + Download logos from LaunchBox for selected games تحميل الشعارات من SteamGridDB للألعاب المحددة افتح مجلد Extra Metadata حذف شعارات الألعاب المحددة diff --git a/source/Generic/ExtraMetadataLoader/Localization/ca_ES.xaml b/source/Generic/ExtraMetadataLoader/Localization/ca_ES.xaml index 007d6f540..4c4188d8a 100644 --- a/source/Generic/ExtraMetadataLoader/Localization/ca_ES.xaml +++ b/source/Generic/ExtraMetadataLoader/Localization/ca_ES.xaml @@ -47,6 +47,7 @@ Download videos of newly added games on library update Download Micro videos of newly added games on library update Download logos of newly added games on library update + Use LaunchBox for automatic logo downloads Select logos to download automatically Video @@ -65,6 +66,7 @@ Downloading videos from Steam... Downloading Micro videos from Steam... Downloading logos from Steam... + Downloading logos from LaunchBox... Downloading logos from SteamGridDB... Downloading video from YouTube... Failed to download video from YouTube. @@ -86,6 +88,7 @@ Please verify that your yt-dlp installation is up to date. Download videos from Steam for selected games Download Micro videos from Steam for selected games Download logos from Steam for selected games + Download logos from LaunchBox for selected games Download logos from SteamGridDB for selected games Open Extra Metadata directory Delete logos of selected games diff --git a/source/Generic/ExtraMetadataLoader/Localization/cs_CZ.xaml b/source/Generic/ExtraMetadataLoader/Localization/cs_CZ.xaml index 007d6f540..4c4188d8a 100644 --- a/source/Generic/ExtraMetadataLoader/Localization/cs_CZ.xaml +++ b/source/Generic/ExtraMetadataLoader/Localization/cs_CZ.xaml @@ -47,6 +47,7 @@ Download videos of newly added games on library update Download Micro videos of newly added games on library update Download logos of newly added games on library update + Use LaunchBox for automatic logo downloads Select logos to download automatically Video @@ -65,6 +66,7 @@ Downloading videos from Steam... Downloading Micro videos from Steam... Downloading logos from Steam... + Downloading logos from LaunchBox... Downloading logos from SteamGridDB... Downloading video from YouTube... Failed to download video from YouTube. @@ -86,6 +88,7 @@ Please verify that your yt-dlp installation is up to date. Download videos from Steam for selected games Download Micro videos from Steam for selected games Download logos from Steam for selected games + Download logos from LaunchBox for selected games Download logos from SteamGridDB for selected games Open Extra Metadata directory Delete logos of selected games diff --git a/source/Generic/ExtraMetadataLoader/Localization/da_DK.xaml b/source/Generic/ExtraMetadataLoader/Localization/da_DK.xaml index 007d6f540..4c4188d8a 100644 --- a/source/Generic/ExtraMetadataLoader/Localization/da_DK.xaml +++ b/source/Generic/ExtraMetadataLoader/Localization/da_DK.xaml @@ -47,6 +47,7 @@ Download videos of newly added games on library update Download Micro videos of newly added games on library update Download logos of newly added games on library update + Use LaunchBox for automatic logo downloads Select logos to download automatically Video @@ -65,6 +66,7 @@ Downloading videos from Steam... Downloading Micro videos from Steam... Downloading logos from Steam... + Downloading logos from LaunchBox... Downloading logos from SteamGridDB... Downloading video from YouTube... Failed to download video from YouTube. @@ -86,6 +88,7 @@ Please verify that your yt-dlp installation is up to date. Download videos from Steam for selected games Download Micro videos from Steam for selected games Download logos from Steam for selected games + Download logos from LaunchBox for selected games Download logos from SteamGridDB for selected games Open Extra Metadata directory Delete logos of selected games diff --git a/source/Generic/ExtraMetadataLoader/Localization/de_DE.xaml b/source/Generic/ExtraMetadataLoader/Localization/de_DE.xaml index 8f66882a2..bd5352ab7 100644 --- a/source/Generic/ExtraMetadataLoader/Localization/de_DE.xaml +++ b/source/Generic/ExtraMetadataLoader/Localization/de_DE.xaml @@ -47,6 +47,7 @@ Herunterladen von Videos von neu hinzugefügten Spielen bei Bibliotheksupdate Herunterladen von Mikro Videos von neu hinzugefügten Spielen bei Bibliotheksupdate Logos von neu hinzugefügten Spielen bei Bibliotheksupdate herunterladen + Use LaunchBox for automatic logo downloads Wählen Sie Logos zum automatischen Download aus Video @@ -65,6 +66,7 @@ Videos von Steam herunterladen... Mikro Videos von Steam herunterladen... Logos von Steam herunterladen... + Downloading logos from LaunchBox... Logos von SteamGridDB herunterladen... Video von YouTube herunterladen... Download des Videos von YouTube fehlgeschlagen. @@ -86,6 +88,7 @@ Bitte überprüfe, ob die Installatierte yt-dlp auf dem neuesten Stand ist.Videos von Steam für ausgewählte Spiele herunterladen Mikro Videos von Steam für ausgewählte Spiele herunterladen Logo von Steam für ausgewählte Spiele herunterladen + Download logos from LaunchBox for selected games Logo von SteamGridDB für ausgewählte Spiele herunterladen Metadatenverzeichnis öffnen Logos von ausgewählten Spielen löschen diff --git a/source/Generic/ExtraMetadataLoader/Localization/en_US.xaml b/source/Generic/ExtraMetadataLoader/Localization/en_US.xaml index 6f2b6b72d..809c74b19 100644 --- a/source/Generic/ExtraMetadataLoader/Localization/en_US.xaml +++ b/source/Generic/ExtraMetadataLoader/Localization/en_US.xaml @@ -48,6 +48,7 @@ Download videos of newly added games on library update Download Micro videos of newly added games on library update Download logos of newly added games on library update + Use LaunchBox for automatic logo downloads Select logos to download automatically Video @@ -66,6 +67,7 @@ Downloading videos from Steam... Downloading Micro videos from Steam... Downloading logos from Steam... + Downloading logos from LaunchBox... Downloading logos from SteamGridDB... Downloading video from YouTube... Failed to download video from YouTube. @@ -87,6 +89,7 @@ Please verify that your yt-dlp installation is up to date. Download videos from Steam for selected games Download Micro videos from Steam for selected games Download logos from Steam for selected games + Download logos from LaunchBox for selected games Download logos from SteamGridDB for selected games Open Extra Metadata directory Delete logos of selected games diff --git a/source/Generic/ExtraMetadataLoader/Localization/eo_UY.xaml b/source/Generic/ExtraMetadataLoader/Localization/eo_UY.xaml index 007d6f540..4c4188d8a 100644 --- a/source/Generic/ExtraMetadataLoader/Localization/eo_UY.xaml +++ b/source/Generic/ExtraMetadataLoader/Localization/eo_UY.xaml @@ -47,6 +47,7 @@ Download videos of newly added games on library update Download Micro videos of newly added games on library update Download logos of newly added games on library update + Use LaunchBox for automatic logo downloads Select logos to download automatically Video @@ -65,6 +66,7 @@ Downloading videos from Steam... Downloading Micro videos from Steam... Downloading logos from Steam... + Downloading logos from LaunchBox... Downloading logos from SteamGridDB... Downloading video from YouTube... Failed to download video from YouTube. @@ -86,6 +88,7 @@ Please verify that your yt-dlp installation is up to date. Download videos from Steam for selected games Download Micro videos from Steam for selected games Download logos from Steam for selected games + Download logos from LaunchBox for selected games Download logos from SteamGridDB for selected games Open Extra Metadata directory Delete logos of selected games diff --git a/source/Generic/ExtraMetadataLoader/Localization/es_ES.xaml b/source/Generic/ExtraMetadataLoader/Localization/es_ES.xaml index 0bb59c17f..37c07fc4d 100644 --- a/source/Generic/ExtraMetadataLoader/Localization/es_ES.xaml +++ b/source/Generic/ExtraMetadataLoader/Localization/es_ES.xaml @@ -47,6 +47,7 @@ Descargar videos de juegos recién añadidos al actualizar la biblioteca Descargar videos micro de juegos recién añadidos al actualizar la biblioteca Descargar logos de juegos recién añadidos al actualizar la biblioteca + Use LaunchBox for automatic logo downloads Seleccionar logos para descargar automáticamente Video @@ -65,6 +66,7 @@ Descargando videos de Steam... Descargando videos micro de Steam... Descargando logos de Steam... + Downloading logos from LaunchBox... Descargando logos de SteamGridDB... Descargando vídeo de YouTube... No se pudo descargar el vídeo de YouTube. @@ -86,6 +88,7 @@ Por favor, comprueba que tu instalación yt-dlp está actualizada. Descargar videos de Steam para los juegos seleccionados Descargar videos micro de Steam para los juegos seleccionados Descargar logos de Steam para los juegos seleccionados + Download logos from LaunchBox for selected games Descargar logos de SteamGridDB para los juegos seleccionados Abrir el directorio de Extra Metadata Eliminar logos de los juegos seleccionados diff --git a/source/Generic/ExtraMetadataLoader/Localization/fa_IR.xaml b/source/Generic/ExtraMetadataLoader/Localization/fa_IR.xaml index 007d6f540..4c4188d8a 100644 --- a/source/Generic/ExtraMetadataLoader/Localization/fa_IR.xaml +++ b/source/Generic/ExtraMetadataLoader/Localization/fa_IR.xaml @@ -47,6 +47,7 @@ Download videos of newly added games on library update Download Micro videos of newly added games on library update Download logos of newly added games on library update + Use LaunchBox for automatic logo downloads Select logos to download automatically Video @@ -65,6 +66,7 @@ Downloading videos from Steam... Downloading Micro videos from Steam... Downloading logos from Steam... + Downloading logos from LaunchBox... Downloading logos from SteamGridDB... Downloading video from YouTube... Failed to download video from YouTube. @@ -86,6 +88,7 @@ Please verify that your yt-dlp installation is up to date. Download videos from Steam for selected games Download Micro videos from Steam for selected games Download logos from Steam for selected games + Download logos from LaunchBox for selected games Download logos from SteamGridDB for selected games Open Extra Metadata directory Delete logos of selected games diff --git a/source/Generic/ExtraMetadataLoader/Localization/fi_FI.xaml b/source/Generic/ExtraMetadataLoader/Localization/fi_FI.xaml index 007d6f540..4c4188d8a 100644 --- a/source/Generic/ExtraMetadataLoader/Localization/fi_FI.xaml +++ b/source/Generic/ExtraMetadataLoader/Localization/fi_FI.xaml @@ -47,6 +47,7 @@ Download videos of newly added games on library update Download Micro videos of newly added games on library update Download logos of newly added games on library update + Use LaunchBox for automatic logo downloads Select logos to download automatically Video @@ -65,6 +66,7 @@ Downloading videos from Steam... Downloading Micro videos from Steam... Downloading logos from Steam... + Downloading logos from LaunchBox... Downloading logos from SteamGridDB... Downloading video from YouTube... Failed to download video from YouTube. @@ -86,6 +88,7 @@ Please verify that your yt-dlp installation is up to date. Download videos from Steam for selected games Download Micro videos from Steam for selected games Download logos from Steam for selected games + Download logos from LaunchBox for selected games Download logos from SteamGridDB for selected games Open Extra Metadata directory Delete logos of selected games diff --git a/source/Generic/ExtraMetadataLoader/Localization/fr_FR.xaml b/source/Generic/ExtraMetadataLoader/Localization/fr_FR.xaml index 23557cd5f..9c9e567be 100644 --- a/source/Generic/ExtraMetadataLoader/Localization/fr_FR.xaml +++ b/source/Generic/ExtraMetadataLoader/Localization/fr_FR.xaml @@ -47,6 +47,7 @@ Télécharger les vidéos des jeux ajoutés lors de la mise à jour de la bibliothèque Télécharger les micro vidéos des jeux ajoutés lors de la mise à jour de la bibliothèque Télécharger les logos des jeux ajoutés lors de la mise à jour de la bibliothèque + Use LaunchBox for automatic logo downloads Choisir les logos à télécharger Vidéo @@ -65,6 +66,7 @@ Téléchargement de vidéos depuis Steam... Téléchargement de micro vidéos depuis Steam... Téléchargement de logos depuis Steam... + Downloading logos from LaunchBox... Téléchargement de logos depuis SteamGridD... Téléchargement de vidéo depuis YouTube... Impossible de télécharger la vidéo depuis YouTube. @@ -86,6 +88,7 @@ Veuillez vérifier que votre installation yt-dlp est à jour. Télécharger les vidéos depuis Steam pour les jeux sélectionnés Télécharger les micro vidéos depuis Steam pour les jeux sélectionnés Télécharger les logos depuis Steam pour les jeux sélectionnés + Download logos from LaunchBox for selected games Télécharger les logos depuis SteamGridDB pour les jeux sélectionnés Ouvrir le dossier Extra Metadata Supprimer les logos des jeux sélectionnés diff --git a/source/Generic/ExtraMetadataLoader/Localization/gl_ES.xaml b/source/Generic/ExtraMetadataLoader/Localization/gl_ES.xaml index 007d6f540..4c4188d8a 100644 --- a/source/Generic/ExtraMetadataLoader/Localization/gl_ES.xaml +++ b/source/Generic/ExtraMetadataLoader/Localization/gl_ES.xaml @@ -47,6 +47,7 @@ Download videos of newly added games on library update Download Micro videos of newly added games on library update Download logos of newly added games on library update + Use LaunchBox for automatic logo downloads Select logos to download automatically Video @@ -65,6 +66,7 @@ Downloading videos from Steam... Downloading Micro videos from Steam... Downloading logos from Steam... + Downloading logos from LaunchBox... Downloading logos from SteamGridDB... Downloading video from YouTube... Failed to download video from YouTube. @@ -86,6 +88,7 @@ Please verify that your yt-dlp installation is up to date. Download videos from Steam for selected games Download Micro videos from Steam for selected games Download logos from Steam for selected games + Download logos from LaunchBox for selected games Download logos from SteamGridDB for selected games Open Extra Metadata directory Delete logos of selected games diff --git a/source/Generic/ExtraMetadataLoader/Localization/hr_HR.xaml b/source/Generic/ExtraMetadataLoader/Localization/hr_HR.xaml index 007d6f540..4c4188d8a 100644 --- a/source/Generic/ExtraMetadataLoader/Localization/hr_HR.xaml +++ b/source/Generic/ExtraMetadataLoader/Localization/hr_HR.xaml @@ -47,6 +47,7 @@ Download videos of newly added games on library update Download Micro videos of newly added games on library update Download logos of newly added games on library update + Use LaunchBox for automatic logo downloads Select logos to download automatically Video @@ -65,6 +66,7 @@ Downloading videos from Steam... Downloading Micro videos from Steam... Downloading logos from Steam... + Downloading logos from LaunchBox... Downloading logos from SteamGridDB... Downloading video from YouTube... Failed to download video from YouTube. @@ -86,6 +88,7 @@ Please verify that your yt-dlp installation is up to date. Download videos from Steam for selected games Download Micro videos from Steam for selected games Download logos from Steam for selected games + Download logos from LaunchBox for selected games Download logos from SteamGridDB for selected games Open Extra Metadata directory Delete logos of selected games diff --git a/source/Generic/ExtraMetadataLoader/Localization/hu_HU.xaml b/source/Generic/ExtraMetadataLoader/Localization/hu_HU.xaml index c7235fec1..e4f06c2cb 100644 --- a/source/Generic/ExtraMetadataLoader/Localization/hu_HU.xaml +++ b/source/Generic/ExtraMetadataLoader/Localization/hu_HU.xaml @@ -47,6 +47,7 @@ Download videos of newly added games on library update Download Micro videos of newly added games on library update Download logos of newly added games on library update + Use LaunchBox for automatic logo downloads Select logos to download automatically Videó @@ -65,6 +66,7 @@ Downloading videos from Steam... Downloading Micro videos from Steam... Downloading logos from Steam... + Downloading logos from LaunchBox... Downloading logos from SteamGridDB... Downloading video from YouTube... Failed to download video from YouTube. @@ -86,6 +88,7 @@ Please verify that your yt-dlp installation is up to date. Download videos from Steam for selected games Download Micro videos from Steam for selected games Download logos from Steam for selected games + Download logos from LaunchBox for selected games Download logos from SteamGridDB for selected games Open Extra Metadata directory Delete logos of selected games diff --git a/source/Generic/ExtraMetadataLoader/Localization/it_IT.xaml b/source/Generic/ExtraMetadataLoader/Localization/it_IT.xaml index 2006e9257..b309de95e 100644 --- a/source/Generic/ExtraMetadataLoader/Localization/it_IT.xaml +++ b/source/Generic/ExtraMetadataLoader/Localization/it_IT.xaml @@ -47,6 +47,7 @@ Scarica i video dei nuovi giochi aggiunti all'aggiornamento della libreria Scarica i micro video dei nuovi giochi aggiunti all'aggiornamento della libreria Scarica i loghi dei nuovi giochi aggiunti all'aggiornamento della libreria + Use LaunchBox for automatic logo downloads Seleziona i loghi da scaricare automaticamente Video @@ -65,6 +66,7 @@ Scaricamento video da Steam... Scaricamento micro video da Steam... Scaricamento loghi da Steam... + Downloading logos from LaunchBox... Scaricamento loghi da SteamGridDB... Scaricamento video da YouTube... Impossibile scaricare il video da YouTube. @@ -86,6 +88,7 @@ Verifica che l'installazione di yt-dlp sia aggiornata. Scarica video da Steam per i giochi selezionati Scarica micro video da Steam per i giochi selezionati Scarica loghi da Steam per i giochi selezionati + Download logos from LaunchBox for selected games Scarica loghi da SteamGridDB per i giochi selezionati Apri cartella metadata extra Elimina i loghi dei giochi selezionati diff --git a/source/Generic/ExtraMetadataLoader/Localization/ja_JP.xaml b/source/Generic/ExtraMetadataLoader/Localization/ja_JP.xaml index 5d5986371..e119661ca 100644 --- a/source/Generic/ExtraMetadataLoader/Localization/ja_JP.xaml +++ b/source/Generic/ExtraMetadataLoader/Localization/ja_JP.xaml @@ -47,6 +47,7 @@ ライブラリの更新時に新しく追加されたゲームの動画をダウンロード ライブラリの更新時に新しく追加されたゲームのマイクロ動画をダウンロード ライブラリの更新時に新しく追加されたゲームのロゴをダウンロード + Use LaunchBox for automatic logo downloads ダウンロードするロゴを自動的に選択 動画 @@ -65,6 +66,7 @@ Steam から動画をダウンロード中... Steam からマイクロ動画をダウンロード中... Steam からロゴをダウンロード中... + Downloading logos from LaunchBox... SteamGridDB からロゴをダウンロード中... YouTube から動画をダウンロード中... Failed to download video from YouTube. @@ -86,6 +88,7 @@ Please verify that your yt-dlp installation is up to date. 選択したゲームの動画を Steam からダウンロード 選択したゲームのマイクロ動画を Steam からダウンロード 選択したゲームのロゴを Steam からダウンロード + Download logos from LaunchBox for selected games 選択したゲームのロゴを SteamGridDB からダウンロード Extra Metadata ディレクトリを開く 選択したゲームのロゴを削除 diff --git a/source/Generic/ExtraMetadataLoader/Localization/ko_KR.xaml b/source/Generic/ExtraMetadataLoader/Localization/ko_KR.xaml index 007d6f540..4c4188d8a 100644 --- a/source/Generic/ExtraMetadataLoader/Localization/ko_KR.xaml +++ b/source/Generic/ExtraMetadataLoader/Localization/ko_KR.xaml @@ -47,6 +47,7 @@ Download videos of newly added games on library update Download Micro videos of newly added games on library update Download logos of newly added games on library update + Use LaunchBox for automatic logo downloads Select logos to download automatically Video @@ -65,6 +66,7 @@ Downloading videos from Steam... Downloading Micro videos from Steam... Downloading logos from Steam... + Downloading logos from LaunchBox... Downloading logos from SteamGridDB... Downloading video from YouTube... Failed to download video from YouTube. @@ -86,6 +88,7 @@ Please verify that your yt-dlp installation is up to date. Download videos from Steam for selected games Download Micro videos from Steam for selected games Download logos from Steam for selected games + Download logos from LaunchBox for selected games Download logos from SteamGridDB for selected games Open Extra Metadata directory Delete logos of selected games diff --git a/source/Generic/ExtraMetadataLoader/Localization/nl_NL.xaml b/source/Generic/ExtraMetadataLoader/Localization/nl_NL.xaml index 007d6f540..4c4188d8a 100644 --- a/source/Generic/ExtraMetadataLoader/Localization/nl_NL.xaml +++ b/source/Generic/ExtraMetadataLoader/Localization/nl_NL.xaml @@ -47,6 +47,7 @@ Download videos of newly added games on library update Download Micro videos of newly added games on library update Download logos of newly added games on library update + Use LaunchBox for automatic logo downloads Select logos to download automatically Video @@ -65,6 +66,7 @@ Downloading videos from Steam... Downloading Micro videos from Steam... Downloading logos from Steam... + Downloading logos from LaunchBox... Downloading logos from SteamGridDB... Downloading video from YouTube... Failed to download video from YouTube. @@ -86,6 +88,7 @@ Please verify that your yt-dlp installation is up to date. Download videos from Steam for selected games Download Micro videos from Steam for selected games Download logos from Steam for selected games + Download logos from LaunchBox for selected games Download logos from SteamGridDB for selected games Open Extra Metadata directory Delete logos of selected games diff --git a/source/Generic/ExtraMetadataLoader/Localization/no_NO.xaml b/source/Generic/ExtraMetadataLoader/Localization/no_NO.xaml index dce5e3878..65971ab53 100644 --- a/source/Generic/ExtraMetadataLoader/Localization/no_NO.xaml +++ b/source/Generic/ExtraMetadataLoader/Localization/no_NO.xaml @@ -47,6 +47,7 @@ Last ned videoer for nye spill under bibliotekoppdatering Last ned mikrovideoer for nye spill under bibliotekoppdatering Last ned logoer for nye spill under bibliotekoppdatering + Use LaunchBox for automatic logo downloads Velg automatisk logoene som skal lastes ned Video @@ -65,6 +66,7 @@ Laster ned videoer fra Steam... Laster ned mikrovideoer fra Steam... Laster ned logoer fra Steam... + Downloading logos from LaunchBox... Laster ned logoer fra SteamGridDB... Laster ned video fra YouTube... Kan ikke laste ned video fra YouTube. @@ -86,6 +88,7 @@ Vennligst kontroller at yt-dlp-installasjonen er oppdatert. Last ned videoer fra Steam for valgte spill Last ned mikrovideoer fra Steam for valgte spill Last ned logoer fra Steam for valgte spill + Download logos from LaunchBox for selected games Last ned logoer fra SteamGridDB for valgte spill Åpne Extra metadata-mappe Slett logoer for valgte spill diff --git a/source/Generic/ExtraMetadataLoader/Localization/pl_PL.xaml b/source/Generic/ExtraMetadataLoader/Localization/pl_PL.xaml index 007d6f540..4c4188d8a 100644 --- a/source/Generic/ExtraMetadataLoader/Localization/pl_PL.xaml +++ b/source/Generic/ExtraMetadataLoader/Localization/pl_PL.xaml @@ -47,6 +47,7 @@ Download videos of newly added games on library update Download Micro videos of newly added games on library update Download logos of newly added games on library update + Use LaunchBox for automatic logo downloads Select logos to download automatically Video @@ -65,6 +66,7 @@ Downloading videos from Steam... Downloading Micro videos from Steam... Downloading logos from Steam... + Downloading logos from LaunchBox... Downloading logos from SteamGridDB... Downloading video from YouTube... Failed to download video from YouTube. @@ -86,6 +88,7 @@ Please verify that your yt-dlp installation is up to date. Download videos from Steam for selected games Download Micro videos from Steam for selected games Download logos from Steam for selected games + Download logos from LaunchBox for selected games Download logos from SteamGridDB for selected games Open Extra Metadata directory Delete logos of selected games diff --git a/source/Generic/ExtraMetadataLoader/Localization/pt_BR.xaml b/source/Generic/ExtraMetadataLoader/Localization/pt_BR.xaml index 63b96ed56..5f25efdf0 100644 --- a/source/Generic/ExtraMetadataLoader/Localization/pt_BR.xaml +++ b/source/Generic/ExtraMetadataLoader/Localization/pt_BR.xaml @@ -47,6 +47,7 @@ Baixar vídeos de jogos adicionados recentemente na atualização da biblioteca Baixar Micro vídeos dos jogos adicionados recentemente na atualização da biblioteca Baixar logotipos de jogos adicionados recentemente na biblioteca + Use LaunchBox for automatic logo downloads Selecione os logotipos para download automaticamente Vídeo @@ -65,6 +66,7 @@ Baixando vídeos do Steam... Baixando Micro vídeos do Steam... Baixando logotipos do Steam... + Downloading logos from LaunchBox... Baixando logotipos do SteamGridDB... Baixando vídeo do YouTube... Não foi possível baixar o vídeo do YouTube. @@ -86,6 +88,7 @@ Verifique se a instalação do Yt-dlp está atualizada. Baixar vídeos do Steam para os jogos selecionados Baixar Micro vídeos do Steam para os jogos selecionados Baixar logotipos do Steam para os jogos selecionados + Download logos from LaunchBox for selected games Baixar logotipos do SteamGridDB para jogos selecionados Abrir o diretório do Extra Metadata Excluir logotipos dos jogos selecionados diff --git a/source/Generic/ExtraMetadataLoader/Localization/pt_PT.xaml b/source/Generic/ExtraMetadataLoader/Localization/pt_PT.xaml index a7a48e4d6..6b09bec81 100644 --- a/source/Generic/ExtraMetadataLoader/Localization/pt_PT.xaml +++ b/source/Generic/ExtraMetadataLoader/Localization/pt_PT.xaml @@ -47,6 +47,7 @@ Download videos of newly added games on library update Download Micro videos of newly added games on library update Download logos of newly added games on library update + Use LaunchBox for automatic logo downloads Select logos to download automatically Video @@ -65,6 +66,7 @@ Downloading videos from Steam... Downloading Micro videos from Steam... Downloading logos from Steam... + Downloading logos from LaunchBox... Downloading logos from SteamGridDB... Downloading video from YouTube... Failed to download video from YouTube. @@ -86,6 +88,7 @@ Please verify that your yt-dlp installation is up to date. Download videos from Steam for selected games Download Micro videos from Steam for selected games Download logos from Steam for selected games + Download logos from LaunchBox for selected games Download logos from SteamGridDB for selected games Open Extra Metadata directory Delete logos of selected games diff --git a/source/Generic/ExtraMetadataLoader/Localization/ru_RU.xaml b/source/Generic/ExtraMetadataLoader/Localization/ru_RU.xaml index 3f1e12c4c..4267ff073 100644 --- a/source/Generic/ExtraMetadataLoader/Localization/ru_RU.xaml +++ b/source/Generic/ExtraMetadataLoader/Localization/ru_RU.xaml @@ -47,6 +47,7 @@ Загружать видео для недавно добавленных игр при обновлении библиотеки Загружать микровидео для недавно добавленных игр при обновлении библиотеки Загружать лого для недавно добавленных игр при обновлении библиотеки + Use LaunchBox for automatic logo downloads Выбрать логотип для загрузки автоматически Видео @@ -65,6 +66,7 @@ Загрузка видео из Steam... Загрузка микровидео из Steam... Загрузка лого из Steam... + Downloading logos from LaunchBox... Загрузка лого из SteamGridDB... Загрузка видео из YouTube... Не удалось загрузить видео с YouTube. @@ -86,6 +88,7 @@ Загрузить видео из Steam Загрузить микро-видео из Steam Загрузить логотип из Steam + Download logos from LaunchBox for selected games Загрузить логотип из SteamGridDB Открыть каталог Extra Metadata Удалить логотип у этой игры diff --git a/source/Generic/ExtraMetadataLoader/Localization/sr_SP.xaml b/source/Generic/ExtraMetadataLoader/Localization/sr_SP.xaml index 007d6f540..4c4188d8a 100644 --- a/source/Generic/ExtraMetadataLoader/Localization/sr_SP.xaml +++ b/source/Generic/ExtraMetadataLoader/Localization/sr_SP.xaml @@ -47,6 +47,7 @@ Download videos of newly added games on library update Download Micro videos of newly added games on library update Download logos of newly added games on library update + Use LaunchBox for automatic logo downloads Select logos to download automatically Video @@ -65,6 +66,7 @@ Downloading videos from Steam... Downloading Micro videos from Steam... Downloading logos from Steam... + Downloading logos from LaunchBox... Downloading logos from SteamGridDB... Downloading video from YouTube... Failed to download video from YouTube. @@ -86,6 +88,7 @@ Please verify that your yt-dlp installation is up to date. Download videos from Steam for selected games Download Micro videos from Steam for selected games Download logos from Steam for selected games + Download logos from LaunchBox for selected games Download logos from SteamGridDB for selected games Open Extra Metadata directory Delete logos of selected games diff --git a/source/Generic/ExtraMetadataLoader/Localization/tr_TR.xaml b/source/Generic/ExtraMetadataLoader/Localization/tr_TR.xaml index 3cdff21cc..edf8a4e60 100644 --- a/source/Generic/ExtraMetadataLoader/Localization/tr_TR.xaml +++ b/source/Generic/ExtraMetadataLoader/Localization/tr_TR.xaml @@ -47,6 +47,7 @@ Kütüphane güncellemesinde yeni eklenen oyunların da videolarını indir Kütüphane güncellemesinde yeni eklenen oyunların da mikro videolarını indir Kütüphane güncellemesinde yeni eklenen oyunların da logolarını indir + Use LaunchBox for automatic logo downloads Kendiliğinden indirilecek logoları seç Video @@ -65,6 +66,7 @@ Steam üzerinden videolar indiriliyor... Steam üzerinden mikro videolar indiriliyor... Steam üzerinden logolar indiriliyor... + Downloading logos from LaunchBox... SteamGridDB üzerinden logolar indiriliyor... Youtube üzerinden video indiriliyor... YouTube üzerinden video indirilemedi. @@ -86,6 +88,7 @@ Lütfen yt-dlp kurulumunuzun güncel olup olmadığını denetleyin.Seçilen oyunlar için Steam'den videolar indirin Seçilen oyunlar için Steam'den mikro videolar indirin Seçilen oyunlar için Steam'den logo indirin + Download logos from LaunchBox for selected games Seçilen oyunlar için logoları SteamGridDB'den indirin Extra Metadata dizinini aç Seçilen oyunların logolarını sil diff --git a/source/Generic/ExtraMetadataLoader/Localization/uk_UA.xaml b/source/Generic/ExtraMetadataLoader/Localization/uk_UA.xaml index aaee77900..6f9347757 100644 --- a/source/Generic/ExtraMetadataLoader/Localization/uk_UA.xaml +++ b/source/Generic/ExtraMetadataLoader/Localization/uk_UA.xaml @@ -47,6 +47,7 @@ Завантажити відео нещодавно доданих ігор при оновленні бібліотеки Завантажити мікро-відео нещодавно доданих ігор при оновленні бібліотеки Завантажити логотипи нещодавно доданих ігор при оновленні бібліотеки + Use LaunchBox for automatic logo downloads Виберіть логотипи, які завантажувати автоматично Відео @@ -65,6 +66,7 @@ Завантаження відео зі Steam... Завантаження мікро-відео зі Steam... Завантаження логотипів із Steam... + Downloading logos from LaunchBox... Завантаження логотипів із SteamGridDB... Завантаження відео з YouTube... Не вдалося завантажити відео з YouTube. @@ -86,6 +88,7 @@ Завантажити відео зі Steam для вибраних ігор Завантажити мікро-відео зі Steam для вибраних ігор Завантажити логотипи із Steam для вибраних ігор + Download logos from LaunchBox for selected games Завантажити логотипи із SteamGridDB для вибраних ігор Відкрити каталог додаткових метаданих Видалити логотипи вибраних ігор diff --git a/source/Generic/ExtraMetadataLoader/Localization/vi_VN.xaml b/source/Generic/ExtraMetadataLoader/Localization/vi_VN.xaml index 007d6f540..4c4188d8a 100644 --- a/source/Generic/ExtraMetadataLoader/Localization/vi_VN.xaml +++ b/source/Generic/ExtraMetadataLoader/Localization/vi_VN.xaml @@ -47,6 +47,7 @@ Download videos of newly added games on library update Download Micro videos of newly added games on library update Download logos of newly added games on library update + Use LaunchBox for automatic logo downloads Select logos to download automatically Video @@ -65,6 +66,7 @@ Downloading videos from Steam... Downloading Micro videos from Steam... Downloading logos from Steam... + Downloading logos from LaunchBox... Downloading logos from SteamGridDB... Downloading video from YouTube... Failed to download video from YouTube. @@ -86,6 +88,7 @@ Please verify that your yt-dlp installation is up to date. Download videos from Steam for selected games Download Micro videos from Steam for selected games Download logos from Steam for selected games + Download logos from LaunchBox for selected games Download logos from SteamGridDB for selected games Open Extra Metadata directory Delete logos of selected games diff --git a/source/Generic/ExtraMetadataLoader/Localization/zh_CN.xaml b/source/Generic/ExtraMetadataLoader/Localization/zh_CN.xaml index 116a77b5b..cc2ab11d8 100644 --- a/source/Generic/ExtraMetadataLoader/Localization/zh_CN.xaml +++ b/source/Generic/ExtraMetadataLoader/Localization/zh_CN.xaml @@ -47,6 +47,7 @@ 在库更新时下载新添加游戏的视频 在库更新时下载新添加游戏的精简视频 在库更新时下载新添加游戏的徽标 + Use LaunchBox for automatic logo downloads 自动选择要下载的徽标 视频 @@ -65,6 +66,7 @@ 正在从 Steam 下载视频... 正在从 Steam 下载精简视频... 正在从 Steam 下载徽标... + Downloading logos from LaunchBox... 正在从 SteamGridDB 下载徽标... 正在从 YouTube 下载视频... Failed to download video from YouTube. @@ -86,6 +88,7 @@ Please verify that your yt-dlp installation is up to date. 从 Steam 下载所选游戏的视频 从 Steam 下载所选游戏的精简视频 从 Steam 下载所选游戏的徽标 + Download logos from LaunchBox for selected games 从 SteamGridDB 下载所选游戏的徽标 打开 Extra Metadata 目录 删除所选游戏的徽标 diff --git a/source/Generic/ExtraMetadataLoader/Localization/zh_TW.xaml b/source/Generic/ExtraMetadataLoader/Localization/zh_TW.xaml index 007d6f540..4c4188d8a 100644 --- a/source/Generic/ExtraMetadataLoader/Localization/zh_TW.xaml +++ b/source/Generic/ExtraMetadataLoader/Localization/zh_TW.xaml @@ -47,6 +47,7 @@ Download videos of newly added games on library update Download Micro videos of newly added games on library update Download logos of newly added games on library update + Use LaunchBox for automatic logo downloads Select logos to download automatically Video @@ -65,6 +66,7 @@ Downloading videos from Steam... Downloading Micro videos from Steam... Downloading logos from Steam... + Downloading logos from LaunchBox... Downloading logos from SteamGridDB... Downloading video from YouTube... Failed to download video from YouTube. @@ -86,6 +88,7 @@ Please verify that your yt-dlp installation is up to date. Download videos from Steam for selected games Download Micro videos from Steam for selected games Download logos from Steam for selected games + Download logos from LaunchBox for selected games Download logos from SteamGridDB for selected games Open Extra Metadata directory Delete logos of selected games diff --git a/source/Generic/ExtraMetadataLoader/MetadataProviders/ILogoProvider.cs b/source/Generic/ExtraMetadataLoader/MetadataProviders/ILogoProvider.cs index 9772082b3..ba994d9a7 100644 --- a/source/Generic/ExtraMetadataLoader/MetadataProviders/ILogoProvider.cs +++ b/source/Generic/ExtraMetadataLoader/MetadataProviders/ILogoProvider.cs @@ -11,10 +11,12 @@ namespace ExtraMetadataLoader.MetadataProviders public class LogoDownloadOptions { public bool IsBackgroundDownload { get; private set; } + public bool IsLibraryUpdateDownload { get; private set; } - public LogoDownloadOptions(bool isBackgroundDownload) + public LogoDownloadOptions(bool isBackgroundDownload, bool isLibraryUpdateDownload = false) { IsBackgroundDownload = isBackgroundDownload; + IsLibraryUpdateDownload = isLibraryUpdateDownload; } } diff --git a/source/Generic/ExtraMetadataLoader/MetadataProviders/IVideoProvider.cs b/source/Generic/ExtraMetadataLoader/MetadataProviders/IVideoProvider.cs index b391e781b..b3a4512b2 100644 --- a/source/Generic/ExtraMetadataLoader/MetadataProviders/IVideoProvider.cs +++ b/source/Generic/ExtraMetadataLoader/MetadataProviders/IVideoProvider.cs @@ -8,6 +8,12 @@ namespace ExtraMetadataLoader.MetadataProviders { + public enum VideoType + { + Trailer, + Microtrailer + } + public class VideoResult { public string Url { get; private set; } @@ -31,11 +37,13 @@ public class VideoDownloadOptions { public string DownloadPath { get; private set; } public bool IsBackgroundDownload { get; private set; } + public VideoType VideoType { get; private set; } - public VideoDownloadOptions (string downloadPath, bool isBackgroundDownload) + public VideoDownloadOptions (string downloadPath, bool isBackgroundDownload, VideoType videoType = VideoType.Trailer) { DownloadPath = downloadPath; IsBackgroundDownload = isBackgroundDownload; + VideoType = videoType; } } diff --git a/source/Generic/ExtraMetadataLoader/MetadataProviders/LaunchBox/LaunchBoxClearLogoProvider.cs b/source/Generic/ExtraMetadataLoader/MetadataProviders/LaunchBox/LaunchBoxClearLogoProvider.cs new file mode 100644 index 000000000..a5a6ccbdf --- /dev/null +++ b/source/Generic/ExtraMetadataLoader/MetadataProviders/LaunchBox/LaunchBoxClearLogoProvider.cs @@ -0,0 +1,648 @@ +using Playnite.SDK; +using Playnite.SDK.Models; +using PluginsCommon; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Threading; + +namespace ExtraMetadataLoader.MetadataProviders.LaunchBox +{ + internal class LaunchBoxClearLogoProvider : ILogoProvider + { + // Metadata.zip stores image filenames; LaunchBox currently serves those files from this CDN host. + private const string ImageBaseUrl = "https://images.launchbox-app.com/"; + private const int MaxCandidateResults = 25; + private const int AutomaticMinimumScore = 110; + private const int AutomaticMinimumScoreGap = 10; + private static readonly TimeSpan ImageProbeTimeout = TimeSpan.FromSeconds(15); + private readonly IPlayniteAPI _playniteApi; + private readonly ExtraMetadataLoaderSettings _settings; + private readonly ILogger _logger; + private readonly LaunchBoxMetadataCache _metadataCache; + + public string Id => "launchBoxProvider"; + + public LaunchBoxClearLogoProvider( + IPlayniteAPI playniteApi, + ExtraMetadataLoaderSettings settings, + ILogger logger, + LaunchBoxMetadataCache metadataCache) + { + _playniteApi = playniteApi; + _settings = settings; + _logger = logger; + _metadataCache = metadataCache; + } + + public string GetLogoUrl(Game game, LogoDownloadOptions downloadOptions, CancellationToken cancelToken = default) + { + if (downloadOptions.IsLibraryUpdateDownload && !_settings.UseLaunchBoxForAutomaticLogoDownloads) + { + _logger.Debug("LaunchBox automatic logo download skipped because the setting is disabled."); + return null; + } + + if (cancelToken.IsCancellationRequested) + { + return null; + } + + var index = _metadataCache.GetIndex(cancelToken); + if (index?.Games == null || index.Games.Count == 0) + { + _logger.Debug("LaunchBox clear-logo lookup skipped because no metadata index is available."); + return null; + } + + var matches = GetMatches(game, index.Games, game.Name).Take(MaxCandidateResults).ToList(); + if (matches.Count == 0) + { + _logger.Debug($"LaunchBox clear-logo lookup returned no candidates for '{game.Name}'."); + return null; + } + + LaunchBoxMatch selectedMatch; + if (downloadOptions.IsBackgroundDownload) + { + selectedMatch = GetAutomaticMatch(game, matches); + } + else + { + selectedMatch = GetManualMatch(game, index, matches); + } + + if (selectedMatch == null) + { + return null; + } + + var logoUrl = downloadOptions.IsBackgroundDownload + ? GetBestLogoUrl(selectedMatch.Game, cancelToken) + : GetLogoUrlFromSelection(game, selectedMatch.Game, cancelToken); + + if (logoUrl.IsNullOrWhiteSpace()) + { + _logger.Debug($"LaunchBox clear-logo lookup found no usable image URL for '{game.Name}' from '{FormatMatchForLog(selectedMatch)}'."); + return null; + } + + _logger.Debug($"LaunchBox clear-logo URL selected for '{game.Name}' from '{FormatMatchForLog(selectedMatch)}'."); + return logoUrl; + } + + private LaunchBoxMatch GetAutomaticMatch(Game game, List matches) + { + var bestMatch = matches.FirstOrDefault(); + if (bestMatch == null) + { + return null; + } + + var runnerUp = matches.Skip(1).FirstOrDefault(); + var scoreGap = runnerUp == null ? int.MaxValue : bestMatch.Score - runnerUp.Score; + if ((bestMatch.Score >= AutomaticMinimumScore || bestMatch.NameScore >= 95) && + scoreGap >= AutomaticMinimumScoreGap) + { + return bestMatch; + } + + _logger.Debug($"LaunchBox automatic logo lookup skipped ambiguous match for '{game.Name}'. Top score: {bestMatch.Score}. Runner-up score: {runnerUp?.Score.ToString(CultureInfo.InvariantCulture) ?? "none"}."); + return null; + } + + private LaunchBoxMatch GetManualMatch(Game game, LaunchBoxMetadataIndex index, List matches) + { + var bestMatch = matches.First(); + var runnerUp = matches.Skip(1).FirstOrDefault(); + if (runnerUp == null || bestMatch.Score - runnerUp.Score >= AutomaticMinimumScoreGap && bestMatch.NameScore >= 95) + { + return bestMatch; + } + + var selectedResult = _playniteApi.Dialogs.ChooseItemWithSearch( + ToGenericItemOptions(matches), + searchTerm => ToGenericItemOptions(GetMatches(game, index.Games, searchTerm).Take(MaxCandidateResults)), + game.Name.NormalizeGameName(), + ResourceProvider.GetString("LOCExtra_Metadata_Loader_DialogCaptionSelectGame")); + + if (selectedResult == null || selectedResult.Description.IsNullOrWhiteSpace()) + { + return null; + } + + var selectedMatch = GetMatches(game, index.Games, selectedResult.Name).FirstOrDefault(x => x.Game.DatabaseId == selectedResult.Description); + if (selectedMatch != null) + { + return selectedMatch; + } + + var selectedGame = index.Games.FirstOrDefault(x => x.DatabaseId == selectedResult.Description); + if (selectedGame == null) + { + return null; + } + + return new LaunchBoxMatch + { + Game = selectedGame, + Score = 0, + NameScore = 0, + MatchReason = "manual selection" + }; + } + + private string GetBestLogoUrl(LaunchBoxGameEntry game, CancellationToken cancelToken) + { + foreach (var logo in GetOrderedLogos(game)) + { + var url = GetImageUrl(logo.FileName); + if (IsImageUrlAvailable(url, logo.FileName, cancelToken)) + { + return url; + } + } + + return null; + } + + private string GetLogoUrlFromSelection(Game playniteGame, LaunchBoxGameEntry launchBoxGame, CancellationToken cancelToken) + { + var logos = GetOrderedLogos(launchBoxGame).ToList(); + if (logos.Count == 0) + { + return null; + } + + if (logos.Count == 1) + { + var singleUrl = GetImageUrl(logos[0].FileName); + return IsImageUrlAvailable(singleUrl, logos[0].FileName, cancelToken) ? singleUrl : null; + } + + var imageFileOptions = new List(); + foreach (var logo in logos) + { + var url = GetImageUrl(logo.FileName); + if (IsImageUrlAvailable(url, logo.FileName, cancelToken)) + { + imageFileOptions.Add(new ImageFileOption + { + Path = url + }); + } + } + + if (imageFileOptions.Count == 0) + { + return null; + } + + var selectedOption = _playniteApi.Dialogs.ChooseImageFile( + imageFileOptions, + string.Format(ResourceProvider.GetString("LOCExtra_Metadata_Loader_DialogCaptionSelectLogo"), playniteGame.Name)); + + return selectedOption?.Path; + } + + private IEnumerable GetMatches(Game game, IEnumerable games, string searchTerm) + { + if (searchTerm.IsNullOrWhiteSpace()) + { + searchTerm = game.Name; + } + + var gamePlatforms = GetGamePlatforms(game); + var gameReleaseYear = GetGameReleaseYear(game); + var developers = GetCompanyNames(game.Developers); + var publishers = GetCompanyNames(game.Publishers); + + return games + .Select(x => ScoreMatch(searchTerm, gamePlatforms, gameReleaseYear, developers, publishers, x)) + .Where(x => x != null) + .GroupBy(x => x.Game.DatabaseId) + .Select(x => x.OrderByDescending(y => y.Score).First()) + .OrderByDescending(x => x.Score) + .ThenByDescending(x => x.NameScore) + .ThenBy(x => x.Game.Name) + .ThenBy(x => x.Game.Platform); + } + + private LaunchBoxMatch ScoreMatch( + string searchTerm, + List gamePlatforms, + int? gameReleaseYear, + List developers, + List publishers, + LaunchBoxGameEntry launchBoxGame) + { + string matchReason; + var nameScore = GetNameScore(searchTerm, launchBoxGame, out matchReason); + if (nameScore < 70) + { + return null; + } + + var score = nameScore; + score += GetPlatformScore(gamePlatforms, launchBoxGame.Platform); + score += GetReleaseYearScore(gameReleaseYear, launchBoxGame.ReleaseYear); + score += GetCompanyScore(developers, launchBoxGame.Developer); + score += GetCompanyScore(publishers, launchBoxGame.Publisher); + + return new LaunchBoxMatch + { + Game = launchBoxGame, + Score = score, + NameScore = nameScore, + MatchReason = matchReason + }; + } + + private int GetNameScore(string searchTerm, LaunchBoxGameEntry launchBoxGame, out string matchReason) + { + matchReason = null; + var searchName = searchTerm.NormalizeGameName(); + var searchKey = NormalizeForMatch(searchTerm); + if (searchKey.IsNullOrWhiteSpace()) + { + return 0; + } + + var launchBoxName = launchBoxGame.Name.NormalizeGameName(); + var launchBoxKey = NormalizeForMatch(launchBoxGame.Name); + if (searchKey == launchBoxKey) + { + matchReason = "exact title"; + return 100; + } + + foreach (var alternateName in launchBoxGame.AlternateNames ?? new List()) + { + if (searchKey == NormalizeForMatch(alternateName)) + { + matchReason = "exact alternate title"; + return 98; + } + } + + if (searchKey.Length > 3 && launchBoxKey.Contains(searchKey)) + { + matchReason = "contained title"; + return 84; + } + + if (launchBoxKey.Length > 3 && searchKey.Contains(launchBoxKey)) + { + matchReason = "contained title"; + return 82; + } + + if (searchName.MatchesAllWords(launchBoxName) || launchBoxName.MatchesAllWords(searchName)) + { + matchReason = "word match"; + return 78; + } + + var similarity = searchName.GetJaroWinklerSimilarityIgnoreCase(launchBoxName); + if (similarity >= 0.94) + { + matchReason = "very similar title"; + return 88; + } + + if (similarity >= 0.88) + { + matchReason = "similar title"; + return 72; + } + + return 0; + } + + private static List GetGamePlatforms(Game game) + { + if (game.Platforms == null || game.Platforms.Count == 0) + { + return new List(); + } + + return game.Platforms + .Where(x => !x.Name.IsNullOrWhiteSpace()) + .Select(x => NormalizePlatformName(x.Name)) + .Distinct() + .ToList(); + } + + private static int? GetGameReleaseYear(Game game) + { + if (!game.ReleaseDate.HasValue) + { + return null; + } + + var year = game.ReleaseDate.Value.Year; + return year > 0 ? year : (int?)null; + } + + private static List GetCompanyNames(IEnumerable companies) + { + if (companies == null) + { + return new List(); + } + + return companies + .Where(x => !x.Name.IsNullOrWhiteSpace()) + .Select(x => NormalizeForMatch(x.Name)) + .Distinct() + .ToList(); + } + + private static int GetPlatformScore(List gamePlatforms, string launchBoxPlatform) + { + if (gamePlatforms.Count == 0 || launchBoxPlatform.IsNullOrWhiteSpace()) + { + return 0; + } + + var normalizedLaunchBoxPlatform = NormalizePlatformName(launchBoxPlatform); + if (gamePlatforms.Contains(normalizedLaunchBoxPlatform)) + { + return 18; + } + + if (gamePlatforms.Any(x => x.Contains(normalizedLaunchBoxPlatform) || normalizedLaunchBoxPlatform.Contains(x))) + { + return 8; + } + + return -8; + } + + private static int GetReleaseYearScore(int? playniteYear, int? launchBoxYear) + { + if (!playniteYear.HasValue || !launchBoxYear.HasValue) + { + return 0; + } + + var difference = Math.Abs(playniteYear.Value - launchBoxYear.Value); + if (difference == 0) + { + return 8; + } + + if (difference == 1) + { + return 4; + } + + return -3; + } + + private static int GetCompanyScore(List playniteCompanies, string launchBoxCompany) + { + if (playniteCompanies.Count == 0 || launchBoxCompany.IsNullOrWhiteSpace()) + { + return 0; + } + + var normalizedLaunchBoxCompany = NormalizeForMatch(launchBoxCompany); + foreach (var company in playniteCompanies) + { + if (company == normalizedLaunchBoxCompany) + { + return 4; + } + + if (normalizedLaunchBoxCompany.Contains(company) || company.Contains(normalizedLaunchBoxCompany)) + { + return 2; + } + } + + return 0; + } + + private static string NormalizeForMatch(string value) + { + return value?.NormalizeGameName().Satinize() ?? string.Empty; + } + + private static string NormalizePlatformName(string platformName) + { + var normalized = NormalizeForMatch(platformName); + switch (normalized) + { + case "pc": + case "pcwindows": + case "windows": + case "microsoftwindows": + case "ibmpccompatible": + return "windows"; + case "mac": + case "macintosh": + case "macos": + case "applemacos": + return "macos"; + case "sonyplaystation": + case "playstation": + case "ps1": + case "psx": + return "playstation"; + case "sonyplaystation2": + case "playstation2": + case "ps2": + return "playstation2"; + case "sonyplaystation3": + case "playstation3": + case "ps3": + return "playstation3"; + case "sonyplaystation4": + case "playstation4": + case "ps4": + return "playstation4"; + case "sonyplaystation5": + case "playstation5": + case "ps5": + return "playstation5"; + case "microsoftxbox": + case "xbox": + return "xbox"; + case "microsoftxbox360": + case "xbox360": + return "xbox360"; + case "microsoftxboxone": + case "xboxone": + return "xboxone"; + case "microsoftxboxseriesxs": + case "xboxseriesxs": + case "xboxseriesx": + return "xboxseriesxs"; + case "nintendones": + case "nintendoentertainmentsystem": + return "nintendoentertainmentsystem"; + case "supernintendo": + case "supernintendoentertainmentsystem": + case "snes": + return "supernintendoentertainmentsystem"; + case "nintendo64": + case "n64": + return "nintendo64"; + case "nintendogamecube": + case "gamecube": + return "nintendogamecube"; + case "nintendowii": + case "wii": + return "nintendowii"; + case "nintendowiiu": + case "wiiu": + return "nintendowiiu"; + case "nintendoswitch": + case "switch": + return "nintendoswitch"; + case "segagenesis": + case "segamegadrive": + case "megadrive": + case "genesis": + return "segagenesis"; + default: + return normalized; + } + } + + private static IEnumerable GetOrderedLogos(LaunchBoxGameEntry game) + { + return (game.Logos ?? new List()) + .Where(x => LaunchBoxMetadataCache.IsValidImageFileName(x.FileName)) + .GroupBy(x => x.FileName, StringComparer.OrdinalIgnoreCase) + .Select(x => x.First()) + .OrderByDescending(GetRegionScore) + .ThenByDescending(GetLogoArea) + .ThenBy(x => x.FileName); + } + + private static int GetRegionScore(LaunchBoxLogoEntry logo) + { + if (logo.Region.IsNullOrWhiteSpace() || logo.Region.Equals("World", StringComparison.OrdinalIgnoreCase)) + { + return 2; + } + + return 1; + } + + private static long GetLogoArea(LaunchBoxLogoEntry logo) + { + if (logo.Width.HasValue && logo.Height.HasValue) + { + return (long)logo.Width.Value * logo.Height.Value; + } + + return 0; + } + + private static string GetImageUrl(string fileName) + { + return ImageBaseUrl + Uri.EscapeDataString(fileName); + } + + private bool IsImageUrlAvailable(string url, string fileName, CancellationToken cancelToken) + { + try + { + using (var handler = new HttpClientHandler { AllowAutoRedirect = true, AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate }) + using (var client = new HttpClient(handler)) + { + client.Timeout = ImageProbeTimeout; + var response = SendImageProbe(client, HttpMethod.Head, url, cancelToken); + if (response.StatusCode == HttpStatusCode.MethodNotAllowed || response.StatusCode == HttpStatusCode.NotImplemented) + { + response.Dispose(); + response = SendImageProbe(client, HttpMethod.Get, url, cancelToken); + } + + using (response) + { + if (response.StatusCode == (HttpStatusCode)429) + { + _logger.Debug($"LaunchBox image request was rate limited for '{fileName}'."); + return false; + } + + if (!response.IsSuccessStatusCode) + { + _logger.Debug($"LaunchBox image URL failed for '{fileName}' with HTTP {(int)response.StatusCode} {response.ReasonPhrase}."); + return false; + } + + var contentType = response.Content.Headers.ContentType?.MediaType; + if (contentType.IsNullOrWhiteSpace()) + { + return LaunchBoxMetadataCache.IsValidImageFileName(fileName); + } + + if (contentType.StartsWith("image/", StringComparison.OrdinalIgnoreCase)) + { + return true; + } + + _logger.Debug($"LaunchBox image URL skipped for '{fileName}' because content type '{contentType}' is not an image."); + return false; + } + } + } + catch (OperationCanceledException) + { + throw; + } + catch (Exception ex) + { + _logger.Debug($"LaunchBox image URL validation failed for '{fileName}'. Error: {ex.Message}"); + return false; + } + } + + private static HttpResponseMessage SendImageProbe(HttpClient client, HttpMethod method, string url, CancellationToken cancelToken) + { + using (var request = new HttpRequestMessage(method, url)) + { + request.Headers.TryAddWithoutValidation("User-Agent", "ExtraMetadataLoader LaunchBoxProvider"); + request.Headers.TryAddWithoutValidation("Accept", "image/*,*/*;q=0.8"); + return client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancelToken).ConfigureAwait(false).GetAwaiter().GetResult(); + } + } + + private static List ToGenericItemOptions(IEnumerable matches) + { + return matches + .Select(x => new GenericItemOption(FormatMatchForSelection(x), x.Game.DatabaseId)) + .ToList(); + } + + private static string FormatMatchForSelection(LaunchBoxMatch match) + { + var parts = new List(); + if (!match.Game.Platform.IsNullOrWhiteSpace()) + { + parts.Add(match.Game.Platform); + } + + if (match.Game.ReleaseYear.HasValue) + { + parts.Add(match.Game.ReleaseYear.Value.ToString(CultureInfo.InvariantCulture)); + } + + var suffix = parts.Count > 0 ? $" ({string.Join(", ", parts)})" : string.Empty; + return $"{match.Game.Name}{suffix}"; + } + + private static string FormatMatchForLog(LaunchBoxMatch match) + { + return $"{FormatMatchForSelection(match)} [{match.MatchReason}, score {match.Score}, id {match.Game.DatabaseId}]"; + } + } +} diff --git a/source/Generic/ExtraMetadataLoader/MetadataProviders/LaunchBox/LaunchBoxMetadataCache.cs b/source/Generic/ExtraMetadataLoader/MetadataProviders/LaunchBox/LaunchBoxMetadataCache.cs new file mode 100644 index 000000000..58589e697 --- /dev/null +++ b/source/Generic/ExtraMetadataLoader/MetadataProviders/LaunchBox/LaunchBoxMetadataCache.cs @@ -0,0 +1,597 @@ +using Playnite.SDK; +using Playnite.SDK.Data; +using PluginsCommon; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading; +using System.Xml; + +namespace ExtraMetadataLoader.MetadataProviders.LaunchBox +{ + internal class LaunchBoxMetadataCache + { + internal const string MetadataUrl = "https://gamesdb.launchbox-app.com/Metadata.zip"; + + private const int CacheSchemaVersion = 2; + private const string MetadataZipFileName = "Metadata.zip"; + private const string IndexFileName = "clearLogoIndex.json"; + private const string StateFileName = "clearLogoCacheState.json"; + private static readonly TimeSpan CacheCheckInterval = TimeSpan.FromDays(1); + private static readonly TimeSpan DownloadTimeout = TimeSpan.FromMinutes(10); + private readonly string _cacheDirectory; + private readonly string _metadataZipPath; + private readonly string _indexPath; + private readonly string _statePath; + private readonly ILogger _logger; + private readonly object _lock = new object(); + private LaunchBoxMetadataIndex _loadedIndex; + + public LaunchBoxMetadataCache(string cacheDirectory, ILogger logger) + { + _cacheDirectory = cacheDirectory; + _metadataZipPath = Path.Combine(cacheDirectory, MetadataZipFileName); + _indexPath = Path.Combine(cacheDirectory, IndexFileName); + _statePath = Path.Combine(cacheDirectory, StateFileName); + _logger = logger; + } + + public LaunchBoxMetadataIndex GetIndex(CancellationToken cancelToken) + { + lock (_lock) + { + cancelToken.ThrowIfCancellationRequested(); + if (_loadedIndex != null) + { + return _loadedIndex; + } + + FileSystem.CreateDirectory(_cacheDirectory); + var state = LoadState(); + if (ShouldRefresh(state)) + { + var refreshed = RefreshMetadata(state, cancelToken); + if (refreshed) + { + _loadedIndex = ParseAndSaveIndex(cancelToken); + } + } + + if (_loadedIndex == null) + { + _loadedIndex = LoadIndex(); + } + + if (_loadedIndex == null && FileSystem.FileExists(_metadataZipPath)) + { + _loadedIndex = ParseAndSaveIndex(cancelToken); + } + + if (_loadedIndex == null) + { + _logger.Debug("LaunchBox metadata cache is unavailable. No cached index could be loaded."); + _loadedIndex = CreateEmptyIndex(); + } + + return _loadedIndex; + } + } + + private bool ShouldRefresh(LaunchBoxMetadataCacheState state) + { + if (!FileSystem.FileExists(_indexPath) && !FileSystem.FileExists(_metadataZipPath)) + { + return true; + } + + if (state.SchemaVersion != CacheSchemaVersion) + { + return true; + } + + if (state.LastCheckedUtc == DateTime.MinValue) + { + return true; + } + + return DateTime.UtcNow - state.LastCheckedUtc > CacheCheckInterval; + } + + private bool RefreshMetadata(LaunchBoxMetadataCacheState state, CancellationToken cancelToken) + { + try + { + _logger.Debug("Checking LaunchBox metadata cache."); + using (var handler = new HttpClientHandler { AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate }) + using (var client = new HttpClient(handler)) + using (var request = new HttpRequestMessage(HttpMethod.Get, MetadataUrl)) + { + client.Timeout = DownloadTimeout; + request.Headers.TryAddWithoutValidation("User-Agent", "ExtraMetadataLoader LaunchBoxProvider"); + request.Headers.TryAddWithoutValidation("Accept", "application/zip, application/octet-stream"); + if (!state.MetadataETag.IsNullOrWhiteSpace()) + { + request.Headers.TryAddWithoutValidation("If-None-Match", state.MetadataETag); + } + + if (!state.MetadataLastModified.IsNullOrWhiteSpace()) + { + request.Headers.TryAddWithoutValidation("If-Modified-Since", state.MetadataLastModified); + } + + using (var response = client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancelToken).ConfigureAwait(false).GetAwaiter().GetResult()) + { + state.SchemaVersion = CacheSchemaVersion; + state.LastCheckedUtc = DateTime.UtcNow; + + if (response.StatusCode == HttpStatusCode.NotModified) + { + SaveState(state); + _logger.Debug("LaunchBox metadata cache is current."); + return false; + } + + if (response.StatusCode == (HttpStatusCode)429) + { + SaveState(state); + _logger.Debug("LaunchBox metadata request was rate limited. Existing cache will be used if available."); + return false; + } + + if (!response.IsSuccessStatusCode) + { + SaveState(state); + _logger.Debug($"LaunchBox metadata download failed with HTTP {(int)response.StatusCode} {response.ReasonPhrase}."); + return false; + } + + var tempPath = _metadataZipPath + ".tmp"; + FileSystem.DeleteFile(tempPath, true); + using (var responseStream = response.Content.ReadAsStreamAsync().ConfigureAwait(false).GetAwaiter().GetResult()) + using (var fileStream = new FileStream(FileSystem.FixPathLength(tempPath), FileMode.Create, FileAccess.Write, FileShare.None)) + { + CopyStream(responseStream, fileStream, cancelToken); + } + + FileSystem.DeleteFile(_metadataZipPath, true); + File.Move(FileSystem.FixPathLength(tempPath), FileSystem.FixPathLength(_metadataZipPath)); + + state.MetadataETag = response.Headers.ETag?.ToString(); + state.MetadataLastModified = response.Content.Headers.LastModified?.ToString("R", CultureInfo.InvariantCulture); + state.MetadataContentLength = response.Content.Headers.ContentLength; + state.MetadataDownloadedUtc = DateTime.UtcNow; + state.SourceTimestampUtc = response.Content.Headers.LastModified?.UtcDateTime ?? state.MetadataDownloadedUtc; + SaveState(state); + _logger.Debug("LaunchBox metadata cache downloaded successfully."); + return true; + } + } + } + catch (OperationCanceledException) + { + throw; + } + catch (Exception ex) + { + _logger.Debug($"LaunchBox metadata refresh failed. Existing cache will be used if available. Error: {ex.Message}"); + return false; + } + } + + private LaunchBoxMetadataIndex LoadIndex() + { + if (!FileSystem.FileExists(_indexPath)) + { + return null; + } + + try + { + var json = File.ReadAllText(FileSystem.FixPathLength(_indexPath), Encoding.UTF8); + var index = Serialization.FromJson(json); + if (index?.SchemaVersion != CacheSchemaVersion || index.Games == null) + { + _logger.Debug("LaunchBox metadata index has an unsupported schema version and will be rebuilt."); + return null; + } + + return index; + } + catch (Exception ex) + { + _logger.Debug($"LaunchBox metadata index could not be loaded and will be rebuilt if possible. Error: {ex.Message}"); + return null; + } + } + + private LaunchBoxMetadataCacheState LoadState() + { + if (!FileSystem.FileExists(_statePath)) + { + return new LaunchBoxMetadataCacheState(); + } + + try + { + var json = File.ReadAllText(FileSystem.FixPathLength(_statePath), Encoding.UTF8); + return Serialization.FromJson(json) ?? new LaunchBoxMetadataCacheState(); + } + catch (Exception ex) + { + _logger.Debug($"LaunchBox metadata cache state could not be loaded. Error: {ex.Message}"); + return new LaunchBoxMetadataCacheState(); + } + } + + private void SaveState(LaunchBoxMetadataCacheState state) + { + FileSystem.WriteStringToFile(_statePath, Serialization.ToJson(state), true); + } + + private LaunchBoxMetadataIndex ParseAndSaveIndex(CancellationToken cancelToken) + { + try + { + var index = ParseMetadataZip(cancelToken); + FileSystem.WriteStringToFile(_indexPath, Serialization.ToJson(index), true); + _logger.Debug($"LaunchBox clear-logo metadata index saved with {index.Games.Count} games."); + return index; + } + catch (OperationCanceledException) + { + throw; + } + catch (Exception ex) + { + _logger.Debug($"LaunchBox metadata cache could not be parsed. Error: {ex.Message}"); + return null; + } + } + + private LaunchBoxMetadataIndex ParseMetadataZip(CancellationToken cancelToken) + { + var games = new Dictionary(StringComparer.OrdinalIgnoreCase); + var alternateNames = new Dictionary>(StringComparer.OrdinalIgnoreCase); + var logos = new Dictionary>(StringComparer.OrdinalIgnoreCase); + + using (var archive = ZipFile.OpenRead(FileSystem.FixPathLength(_metadataZipPath))) + { + var metadataEntry = archive.Entries.FirstOrDefault(x => x.Name.Equals("Metadata.xml", StringComparison.OrdinalIgnoreCase)) ?? + archive.Entries.FirstOrDefault(x => x.Name.EndsWith(".xml", StringComparison.OrdinalIgnoreCase)); + if (metadataEntry == null) + { + throw new InvalidDataException("Metadata.zip did not contain an XML metadata file."); + } + + var xmlSettings = new XmlReaderSettings + { + DtdProcessing = DtdProcessing.Ignore, + IgnoreComments = true, + IgnoreWhitespace = true + }; + + using (var stream = metadataEntry.Open()) + using (var reader = XmlReader.Create(stream, xmlSettings)) + { + while (reader.Read()) + { + cancelToken.ThrowIfCancellationRequested(); + if (reader.NodeType != XmlNodeType.Element) + { + continue; + } + + if (reader.LocalName.Equals("Game", StringComparison.OrdinalIgnoreCase)) + { + using (var subtree = reader.ReadSubtree()) + { + var game = ReadGame(ReadElementValues(subtree)); + if (game != null && !games.ContainsKey(game.DatabaseId)) + { + games.Add(game.DatabaseId, game); + } + } + } + else if (reader.LocalName.Equals("GameAlternateName", StringComparison.OrdinalIgnoreCase)) + { + using (var subtree = reader.ReadSubtree()) + { + ReadAlternateName(ReadElementValues(subtree), alternateNames); + } + } + else if (reader.LocalName.Equals("GameImage", StringComparison.OrdinalIgnoreCase)) + { + using (var subtree = reader.ReadSubtree()) + { + ReadImage(ReadElementValues(subtree), logos); + } + } + } + } + } + + foreach (var alternateName in alternateNames) + { + LaunchBoxGameEntry game; + if (games.TryGetValue(alternateName.Key, out game)) + { + game.AlternateNames = alternateName.Value + .Where(x => !x.IsNullOrWhiteSpace()) + .Distinct(StringComparer.OrdinalIgnoreCase) + .OrderBy(x => x) + .ToList(); + } + } + + foreach (var logoGroup in logos) + { + LaunchBoxGameEntry game; + if (games.TryGetValue(logoGroup.Key, out game)) + { + game.Logos = logoGroup.Value + .GroupBy(x => x.FileName, StringComparer.OrdinalIgnoreCase) + .Select(x => x.First()) + .OrderByDescending(GetLogoArea) + .ThenBy(x => x.FileName) + .ToList(); + } + } + + var index = new LaunchBoxMetadataIndex + { + SchemaVersion = CacheSchemaVersion, + CreatedUtc = DateTime.UtcNow, + Games = games.Values + .Where(x => x.Logos != null && x.Logos.Count > 0) + .OrderBy(x => x.Name) + .ThenBy(x => x.Platform) + .ToList() + }; + + _logger.Debug($"LaunchBox metadata parsed. Games with clear logos: {index.Games.Count}."); + return index; + } + + private Dictionary ReadElementValues(XmlReader reader) + { + var values = new Dictionary(StringComparer.OrdinalIgnoreCase); + if (!reader.Read()) + { + return values; + } + + while (!reader.EOF) + { + if (reader.NodeType == XmlNodeType.EndElement && reader.Depth == 0) + { + break; + } + + if (reader.NodeType != XmlNodeType.Element) + { + if (!reader.Read()) + { + break; + } + + continue; + } + + if (reader.Depth != 1) + { + if (!reader.Read()) + { + break; + } + + continue; + } + + var key = reader.LocalName; + if (reader.IsEmptyElement) + { + values[key] = string.Empty; + if (!reader.Read()) + { + break; + } + + continue; + } + + try + { + values[key] = reader.ReadElementContentAsString(); + } + catch (XmlException ex) + { + _logger.Debug($"LaunchBox metadata XML element '{key}' could not be read. Error: {ex.Message}"); + } + } + + return values; + } + + private LaunchBoxGameEntry ReadGame(Dictionary values) + { + var databaseId = GetValue(values, "DatabaseID", "DatabaseId", "GameID", "GameId", "ID", "Id"); + var name = GetValue(values, "Name", "Title"); + if (databaseId.IsNullOrWhiteSpace() || name.IsNullOrWhiteSpace()) + { + return null; + } + + return new LaunchBoxGameEntry + { + DatabaseId = databaseId, + Name = name, + Platform = GetValue(values, "Platform", "PlatformName"), + ReleaseYear = GetReleaseYear(GetValue(values, "ReleaseDate", "ReleaseYear", "Year")), + Developer = GetValue(values, "Developer", "Developers"), + Publisher = GetValue(values, "Publisher", "Publishers") + }; + } + + private void ReadAlternateName(Dictionary values, Dictionary> alternateNames) + { + var databaseId = GetValue(values, "DatabaseID", "DatabaseId", "GameID", "GameId", "ID", "Id"); + var name = GetValue(values, "Name", "AlternateName", "Alternate Name", "Title"); + if (databaseId.IsNullOrWhiteSpace() || name.IsNullOrWhiteSpace()) + { + return; + } + + List names; + if (!alternateNames.TryGetValue(databaseId, out names)) + { + names = new List(); + alternateNames.Add(databaseId, names); + } + + names.Add(name); + } + + private void ReadImage(Dictionary values, Dictionary> logos) + { + var databaseId = GetValue(values, "DatabaseID", "DatabaseId", "GameID", "GameId", "ID", "Id"); + var imageType = GetValue(values, "Type", "ImageType", "Image Type"); + var fileName = GetValue(values, "FileName", "File Name", "Name"); + if (databaseId.IsNullOrWhiteSpace() || !IsClearLogoType(imageType) || !IsValidImageFileName(fileName)) + { + return; + } + + List gameLogos; + if (!logos.TryGetValue(databaseId, out gameLogos)) + { + gameLogos = new List(); + logos.Add(databaseId, gameLogos); + } + + gameLogos.Add(new LaunchBoxLogoEntry + { + FileName = fileName, + Region = GetValue(values, "Region"), + Width = GetNullableInt(GetValue(values, "Width")), + Height = GetNullableInt(GetValue(values, "Height")) + }); + } + + internal static bool IsValidImageFileName(string fileName) + { + if (fileName.IsNullOrWhiteSpace() || fileName.Contains("..") || fileName.Contains("/") || fileName.Contains("\\")) + { + return false; + } + + if (fileName.Any(char.IsControl)) + { + return false; + } + + var extension = Path.GetExtension(fileName); + return extension.Equals(".png", StringComparison.OrdinalIgnoreCase) || + extension.Equals(".jpg", StringComparison.OrdinalIgnoreCase) || + extension.Equals(".jpeg", StringComparison.OrdinalIgnoreCase) || + extension.Equals(".gif", StringComparison.OrdinalIgnoreCase) || + extension.Equals(".webp", StringComparison.OrdinalIgnoreCase); + } + + private static bool IsClearLogoType(string imageType) + { + return imageType?.Satinize() == "clearlogo"; + } + + private static string GetValue(Dictionary values, params string[] keys) + { + foreach (var key in keys) + { + string value; + if (values.TryGetValue(key, out value)) + { + return value?.Trim(); + } + } + + return null; + } + + private static int? GetNullableInt(string value) + { + int parsedValue; + if (int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out parsedValue)) + { + return parsedValue; + } + + return null; + } + + private static int? GetReleaseYear(string releaseDate) + { + if (releaseDate.IsNullOrWhiteSpace()) + { + return null; + } + + int year; + if (int.TryParse(releaseDate, NumberStyles.Integer, CultureInfo.InvariantCulture, out year) && + year > 0) + { + return year; + } + + DateTime parsedDate; + if (DateTime.TryParse(releaseDate, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out parsedDate)) + { + return parsedDate.Year; + } + + var match = Regex.Match(releaseDate, @"\b(?\d{4})\b"); + if (match.Success && int.TryParse(match.Groups["year"].Value, out year)) + { + return year; + } + + return null; + } + + private static long GetLogoArea(LaunchBoxLogoEntry logo) + { + if (logo.Width.HasValue && logo.Height.HasValue) + { + return (long)logo.Width.Value * logo.Height.Value; + } + + return 0; + } + + private static void CopyStream(Stream source, Stream target, CancellationToken cancelToken) + { + var buffer = new byte[81920]; + int bytesRead; + while ((bytesRead = source.Read(buffer, 0, buffer.Length)) > 0) + { + cancelToken.ThrowIfCancellationRequested(); + target.Write(buffer, 0, bytesRead); + } + } + + private static LaunchBoxMetadataIndex CreateEmptyIndex() + { + return new LaunchBoxMetadataIndex + { + SchemaVersion = CacheSchemaVersion, + CreatedUtc = DateTime.UtcNow, + Games = new List() + }; + } + } +} diff --git a/source/Generic/ExtraMetadataLoader/MetadataProviders/LaunchBox/LaunchBoxModels.cs b/source/Generic/ExtraMetadataLoader/MetadataProviders/LaunchBox/LaunchBoxModels.cs new file mode 100644 index 000000000..84cd5bfc2 --- /dev/null +++ b/source/Generic/ExtraMetadataLoader/MetadataProviders/LaunchBox/LaunchBoxModels.cs @@ -0,0 +1,51 @@ +using System; +using System.Collections.Generic; + +namespace ExtraMetadataLoader.MetadataProviders.LaunchBox +{ + internal class LaunchBoxMetadataIndex + { + public int SchemaVersion { get; set; } + public DateTime CreatedUtc { get; set; } + public List Games { get; set; } = new List(); + } + + internal class LaunchBoxGameEntry + { + public string DatabaseId { get; set; } + public string Name { get; set; } + public string Platform { get; set; } + public int? ReleaseYear { get; set; } + public string Developer { get; set; } + public string Publisher { get; set; } + public List AlternateNames { get; set; } = new List(); + public List Logos { get; set; } = new List(); + } + + internal class LaunchBoxLogoEntry + { + public string FileName { get; set; } + public string Region { get; set; } + public int? Width { get; set; } + public int? Height { get; set; } + } + + internal class LaunchBoxMetadataCacheState + { + public int SchemaVersion { get; set; } + public DateTime LastCheckedUtc { get; set; } + public DateTime SourceTimestampUtc { get; set; } + public DateTime MetadataDownloadedUtc { get; set; } + public string MetadataETag { get; set; } + public string MetadataLastModified { get; set; } + public long? MetadataContentLength { get; set; } + } + + internal class LaunchBoxMatch + { + public LaunchBoxGameEntry Game { get; set; } + public int Score { get; set; } + public int NameScore { get; set; } + public string MatchReason { get; set; } + } +} diff --git a/source/Generic/ExtraMetadataLoader/MetadataProviders/Steam/SteamMetadataProvider.cs b/source/Generic/ExtraMetadataLoader/MetadataProviders/Steam/SteamMetadataProvider.cs index b1c1c5834..9ef5daa46 100644 --- a/source/Generic/ExtraMetadataLoader/MetadataProviders/Steam/SteamMetadataProvider.cs +++ b/source/Generic/ExtraMetadataLoader/MetadataProviders/Steam/SteamMetadataProvider.cs @@ -107,7 +107,7 @@ public Result GetVideo(Game game, VideoDownloadOptions downloadOpti videoUrl = string.Format(_steamMicrotrailerUrlTemplate, steamAppDetails.data.Movies[0].Id); } - var videoResult = VideoResult.FromFilePath(videoUrl); + var videoResult = VideoResult.FromUrl(videoUrl); return Result.Success(videoResult); @@ -149,4 +149,4 @@ private string GetSteamIdFromSearch(Game game, bool isBackgroundDownload) } -} \ No newline at end of file +} diff --git a/source/Generic/ExtraMetadataLoader/Services/MetadataDownloadService.cs b/source/Generic/ExtraMetadataLoader/Services/MetadataDownloadService.cs index 7915daea7..4ce085973 100644 --- a/source/Generic/ExtraMetadataLoader/Services/MetadataDownloadService.cs +++ b/source/Generic/ExtraMetadataLoader/Services/MetadataDownloadService.cs @@ -25,7 +25,7 @@ internal class MetadataDownloadService private readonly VideoProcessor _videoProcessor; private readonly EventAggregator _eventAggregator; - internal MetadataDownloadService( + public MetadataDownloadService( IEnumerable logoProviders, IEnumerable videoProviders, ExtraMetadataLoaderSettings settings, @@ -53,7 +53,22 @@ public IVideoProvider GetVideoProviderById(string id) return _videoProviders.FirstOrDefault(x => x.Id == id); } + public Task DownloadLogoAsync(Game game, bool isBackgroundDownload, CancellationToken cancelToken) + { + return DownloadLogoAsync(game, _logoProviders, isBackgroundDownload, false, true, cancelToken); + } + public async Task DownloadLogoAsync(Game game, bool isBackgroundDownload, bool overwrite, CancellationToken cancelToken) + { + return await DownloadLogoAsync(game, _logoProviders, isBackgroundDownload, overwrite, false, cancelToken); + } + + public async Task DownloadLogoAsync(ILogoProvider logoProvider, Game game, bool isBackgroundDownload, bool overwrite, CancellationToken cancelToken) + { + return await DownloadLogoAsync(game, new[] { logoProvider }, isBackgroundDownload, overwrite, false, cancelToken); + } + + private async Task DownloadLogoAsync(Game game, IEnumerable logoProviders, bool isBackgroundDownload, bool overwrite, bool isLibraryUpdateDownload, CancellationToken cancelToken) { var logoDownloadPath = ExtraMetadataHelper.GetGameLogoPath(game); if (!overwrite && FileSystem.FileExists(logoDownloadPath)) @@ -61,11 +76,17 @@ public async Task DownloadLogoAsync(Game game, bool isBackgroundDownload, return true; } - var downloadOptions = new LogoDownloadOptions(isBackgroundDownload); - foreach (var provider in _logoProviders) + logoDownloadPath = ExtraMetadataHelper.GetGameLogoPath(game, true); + var downloadOptions = new LogoDownloadOptions(isBackgroundDownload, isLibraryUpdateDownload); + foreach (var provider in logoProviders) { try { + if (provider == null) + { + continue; + } + var logoUrl = provider.GetLogoUrl(game, downloadOptions, cancelToken); if (logoUrl.IsNullOrEmpty()) { @@ -75,6 +96,7 @@ public async Task DownloadLogoAsync(Game game, bool isBackgroundDownload, var downloadIsSuccess = await DownloadFile(logoUrl, logoDownloadPath, cancelToken); if (!downloadIsSuccess) { + _logger.Debug($"Logo download failed for {provider.Id}. Url: {logoUrl}"); continue; } @@ -84,6 +106,11 @@ public async Task DownloadLogoAsync(Game game, bool isBackgroundDownload, } OnLogoUpdated(game); + if (provider.Id == "launchBoxProvider") + { + _logger.Debug($"LaunchBox logo downloaded successfully for '{game.Name}'."); + } + break; } catch (Exception ex) @@ -174,4 +201,4 @@ public async Task DownloadVideoAsync(Game game, bool isBackgroundDownload, return false; } } -} \ No newline at end of file +} diff --git a/source/Generic/ExtraMetadataLoader/Services/VideosDownloader.cs b/source/Generic/ExtraMetadataLoader/Services/VideosDownloader.cs index 6827c660b..c98f1415e 100644 --- a/source/Generic/ExtraMetadataLoader/Services/VideosDownloader.cs +++ b/source/Generic/ExtraMetadataLoader/Services/VideosDownloader.cs @@ -1,19 +1,12 @@ -using ExtraMetadataLoader.Helpers; -using ExtraMetadataLoader.Models; -using Newtonsoft.Json; +using ExtraMetadataLoader.Helpers; +using ExtraMetadataLoader.MetadataProviders; +using ExtraMetadataLoader.VideosProcessor; +using FlowHttp; using Playnite.SDK; using Playnite.SDK.Models; -using PlayniteUtilitiesCommon; using PluginsCommon; -using FlowHttp; -using SteamCommon; using System; -using System.Collections.Generic; -using System.Globalization; using System.IO; -using System.Linq; -using System.Text; -using System.Threading.Tasks; using System.Threading; namespace ExtraMetadataLoader.Services @@ -23,28 +16,80 @@ class VideosDownloader private readonly IPlayniteAPI playniteApi; private static readonly ILogger logger = LogManager.GetLogger(); private readonly ExtraMetadataLoaderSettings settings; - + private readonly VideoProcessor videoProcessor; + private readonly string tempDownloadPath = Path.Combine(Path.GetTempPath(), "VideoTemp.mp4"); public VideosDownloader(IPlayniteAPI playniteApi, ExtraMetadataLoaderSettings settings) { this.playniteApi = playniteApi; this.settings = settings; + videoProcessor = new VideoProcessor(playniteApi, logger, settings); } public bool DownloadSteamVideo(Game game, bool overwrite, bool isBackgroundDownload, CancellationToken cancelToken, bool downloadVideo = false, bool downloadVideoMicro = false) { + var success = false; + if (downloadVideo) + { + success |= DownloadSteamVideo(game, overwrite, isBackgroundDownload, cancelToken, VideoType.Trailer); + } + if (downloadVideoMicro) + { + success |= DownloadSteamVideo(game, overwrite, isBackgroundDownload, cancelToken, VideoType.Microtrailer); + } + + return success; + } + + private bool DownloadSteamVideo(Game game, bool overwrite, bool isBackgroundDownload, CancellationToken cancelToken, VideoType videoType) + { + var videoPath = videoType == VideoType.Trailer + ? ExtraMetadataHelper.GetGameVideoPath(game, true) + : ExtraMetadataHelper.GetGameVideoMicroPath(game, true); + if (!overwrite && FileSystem.FileExists(videoPath)) + { + return true; + } + + var steamProvider = new SteamMetadataProvider(playniteApi, settings); + var getResult = steamProvider.GetVideo(game, new VideoDownloadOptions(videoPath, isBackgroundDownload, videoType), cancelToken); + if (!getResult.IsSuccess) + { + return false; + } + + var result = getResult.Value; + var sourcePath = result.FilePath; + var deleteSource = false; + if (result.IsUrl) + { + FileSystem.DeleteFile(tempDownloadPath, true); + var downloadResult = HttpRequestFactory.GetHttpFileRequest() + .WithUrl(result.Url) + .WithDownloadTo(tempDownloadPath) + .DownloadFile(cancelToken); + if (!downloadResult.IsSuccess) + { + return false; + } + + sourcePath = tempDownloadPath; + deleteSource = true; + } + + return videoProcessor.ProcessVideo(sourcePath, videoPath, false, deleteSource); } public bool SelectedDialogFileToVideo(Game game) { logger.Debug($"SelectedDialogFileToVideo starting for game {game.Name}"); - var videoPath = extraMetadataHelper.GetGameVideoPath(game, true); + var videoPath = ExtraMetadataHelper.GetGameVideoPath(game, true); var selectedVideoPath = playniteApi.Dialogs.SelectFile("Video file|*.mp4;*.avi;*.mkv;*.webm;*.flv;*.wmv;*.mov;*.m4v"); if (!selectedVideoPath.IsNullOrEmpty()) { - return ProcessVideo(selectedVideoPath, videoPath, true, false); + return videoProcessor.ProcessVideo(selectedVideoPath, videoPath, true, false); } else { @@ -54,11 +99,11 @@ public bool SelectedDialogFileToVideo(Game game) public bool DownloadYoutubeVideoById(Game game, string videoId, bool overwrite) { - var youtubeDlPath = extraMetadataHelper.ExpandVariables(game, settings.YoutubeDlPath); - var ffmpegPath = extraMetadataHelper.ExpandVariables(game, settings.FfmpegPath); - var youtubeCookiesPath = extraMetadataHelper.ExpandVariables(game, settings.YoutubeCookiesPath); + var youtubeDlPath = ExtraMetadataHelper.ExpandVariables(game, settings.YoutubeDlPath); + var ffmpegPath = ExtraMetadataHelper.ExpandVariables(game, settings.FfmpegPath); + var youtubeCookiesPath = ExtraMetadataHelper.ExpandVariables(game, settings.YoutubeCookiesPath); - var videoPath = extraMetadataHelper.GetGameVideoPath(game, true); + var videoPath = ExtraMetadataHelper.GetGameVideoPath(game, true); if (FileSystem.FileExists(videoPath) && !overwrite) { return false; @@ -69,6 +114,7 @@ public bool DownloadYoutubeVideoById(Game game, string videoId, bool overwrite) { args = string.Format("-v --force-overwrites -o \"{0}\" --cookies \"{1}\" -f \"mp4\" \"{2}\"", tempDownloadPath, youtubeCookiesPath, $"https://www.youtube.com/watch?v={videoId}"); } + var result = ProcessStarter.StartProcessWait(youtubeDlPath, args, Path.GetDirectoryName(ffmpegPath), true, out var stdOut, out var stdErr); if (result != 0) { @@ -82,9 +128,13 @@ public bool DownloadYoutubeVideoById(Game game, string videoId, bool overwrite) } else { - return ProcessVideo(tempDownloadPath, videoPath, false, false); + return videoProcessor.ProcessVideo(tempDownloadPath, videoPath, false, false); } } + public bool ConvertVideoToMicro(Game game, bool overwrite) + { + return videoProcessor.ConvertVideoToMicro(game, overwrite); + } } -} \ No newline at end of file +} diff --git a/source/Generic/ExtraMetadataLoader/VideosProcessor/VideoProcessor.cs b/source/Generic/ExtraMetadataLoader/VideosProcessor/VideoProcessor.cs index dfea709ed..c707065e2 100644 --- a/source/Generic/ExtraMetadataLoader/VideosProcessor/VideoProcessor.cs +++ b/source/Generic/ExtraMetadataLoader/VideosProcessor/VideoProcessor.cs @@ -20,7 +20,7 @@ public class VideoProcessor private readonly ExtraMetadataLoaderSettings _settings; private readonly ILogger _logger; - internal VideoProcessor( + public VideoProcessor( IPlayniteAPI playniteApi, ILogger logger, ExtraMetadataLoaderSettings settings)