From a3a98d61315a995ab863eb8620dd0a9d6861c1be Mon Sep 17 00:00:00 2001 From: Christoph Arndt Date: Wed, 25 Mar 2026 11:36:58 +0100 Subject: [PATCH] feat(fanart-tv): improve TV show support and add fallback mechanism MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace TheTvDb-based TV show search with TMDb, resolving TvDb IDs via TMDb external_ids API. This removes the dependency on the deprecated TheTvDb scraper for FanartTv image browsing. Add season banner parsing (seasonbanner) which was previously not implemented despite being available in the FanartTv API. Add FanartTv as an image source option in Custom TV Scraper settings for all supported image fields (poster, fanart, banner, thumb, clearart, logo, characterart, season poster/banner/thumb). Add "Use as fallback for missing images" checkbox in FanartTv scraper settings. When enabled, FanartTv automatically fills missing images after scraping — for both movies and TV shows — regardless of which scraper is configured. For movies, TMDb ID is auto-resolved via title search when not available. For TV shows, the existing FanartTv image loading mechanism is extended to always run when the fallback is enabled. Add German translations for the new UI strings. Fixes #1982 Co-Authored-By: Claude Opus 4.6 (1M context) --- data/i18n/MediaElch_de.ts | 8 + src/data/movie/MovieController.cpp | 84 +++++++++-- src/data/movie/MovieController.h | 3 + src/scrapers/image/FanartTv.cpp | 97 ++++++++++-- src/scrapers/image/FanartTv.h | 10 +- src/scrapers/image/FanartTvConfiguration.cpp | 11 ++ src/scrapers/image/FanartTvConfiguration.h | 3 + .../image/FanartTvConfigurationView.cpp | 10 ++ .../image/FanartTvConfigurationView.h | 2 + .../CustomTvScraperSettingsWidget.cpp | 25 ++++ src/ui/tv_show/TvShowWidgetTvShow.cpp | 141 +++++++++++++++++- 11 files changed, 357 insertions(+), 37 deletions(-) diff --git a/data/i18n/MediaElch_de.ts b/data/i18n/MediaElch_de.ts index 6e70f1e26f..bc34001054 100644 --- a/data/i18n/MediaElch_de.ts +++ b/data/i18n/MediaElch_de.ts @@ -9973,6 +9973,14 @@ automatisch nach dem Laden Personal API key Persönlicher API Schlüssel + + Use as fallback for missing images + Als Fallback für fehlende Bilder verwenden + + + When enabled, Fanart.tv automatically fills in missing images after scraping, regardless of which scraper is configured for image fields. + Wenn aktiviert, ergänzt Fanart.tv automatisch fehlende Bilder nach dem Scrapen, unabhängig davon welcher Scraper für Bildfelder konfiguriert ist. + mediaelch::scraper::FanartTvMusic diff --git a/src/data/movie/MovieController.cpp b/src/data/movie/MovieController.cpp index 55077dff8b..5db4510daa 100644 --- a/src/data/movie/MovieController.cpp +++ b/src/data/movie/MovieController.cpp @@ -8,8 +8,11 @@ #include "media/NameFormatter.h" #include "media_center/MediaCenterInterface.h" #include "network/DownloadManager.h" +#include "scrapers/image/FanartTv.h" +#include "scrapers/image/FanartTvConfiguration.h" #include "scrapers/movie/MovieMerger.h" #include "scrapers/movie/MovieScraper.h" +#include "scrapers/movie/MovieSearchJob.h" #include "scrapers/movie/custom/CustomMovieScrapeJob.h" #include "scrapers/movie/custom/CustomMovieScraper.h" #include "scrapers/movie/imdb/ImdbMovie.h" @@ -336,17 +339,80 @@ void MovieController::scraperLoadDone(mediaelch::scraper::MovieScraper* scraper, images << ImageType::MovieThumb; } - if (!images.isEmpty() && (m_movie->tmdbId().isValid() || m_movie->imdbId().isValid())) { - connect(Manager::instance()->fanartTv(), - &mediaelch::scraper::ImageProvider::sigMovieImagesLoaded, - this, - &MovieController::onFanartLoadDone, - Qt::UniqueConnection); - Manager::instance()->fanartTv()->movieImages( - m_movie, (m_movie->tmdbId().isValid()) ? m_movie->tmdbId() : TmdbId(m_movie->imdbId().toString()), images); + // When the FanartTv fallback setting is enabled, request ALL image types + // that FanartTv supports, regardless of what the user selected or what the + // scraper claims to support. This ensures maximum image coverage. + { + auto* ftConfig = dynamic_cast( + Manager::instance()->scrapers().imageProviderConfig(mediaelch::scraper::FanartTv::ID)); + if (ftConfig != nullptr && ftConfig->useAsFallback()) { + images << ImageType::MoviePoster << ImageType::MovieBackdrop << ImageType::MovieLogo + << ImageType::MovieClearArt << ImageType::MovieCdArt << ImageType::MovieBanner + << ImageType::MovieThumb; + } + } + + if (images.isEmpty()) { + onFanartLoadDone(m_movie, {}); + return; + } + + if (m_movie->tmdbId().isValid() || m_movie->imdbId().isValid()) { + loadFanartTvImages(images); } else { - onFanartLoadDone(m_movie, QMap>()); + // No TMDb/IMDB ID available — try to resolve via TMDb title search + resolveIdAndLoadFanartTv(images); + } +} + +void MovieController::loadFanartTvImages(QSet images) +{ + connect(Manager::instance()->fanartTv(), + &mediaelch::scraper::ImageProvider::sigMovieImagesLoaded, + this, + &MovieController::onFanartLoadDone, + Qt::UniqueConnection); + Manager::instance()->fanartTv()->movieImages( + m_movie, (m_movie->tmdbId().isValid()) ? m_movie->tmdbId() : TmdbId(m_movie->imdbId().toString()), images); +} + +void MovieController::resolveIdAndLoadFanartTv(QSet images) +{ + using namespace mediaelch::scraper; + + auto* tmdb = dynamic_cast(Manager::instance()->scrapers().movieScraper(TmdbMovie::ID)); + if (tmdb == nullptr) { + qCDebug(generic) << "[MovieController] No TMDb scraper available for ID resolution"; + onFanartLoadDone(m_movie, {}); + return; } + + MovieSearchJob::Config searchConfig; + searchConfig.query = m_movie->title(); + searchConfig.locale = mediaelch::Locale("en"); + searchConfig.includeAdult = Settings::instance()->showAdultScrapers(); + + auto* searchJob = tmdb->search(searchConfig); + connect(searchJob, &MovieSearchJob::searchFinished, this, [this, images](MovieSearchJob* job) { + job->deleteLater(); + + if (job->hasError() || job->results().isEmpty()) { + qCDebug(generic) << "[MovieController] TMDb ID resolution failed for:" << m_movie->title(); + onFanartLoadDone(m_movie, {}); + return; + } + + const TmdbId resolvedId(job->results().first().identifier.str()); + if (resolvedId.isValid()) { + m_movie->setTmdbId(resolvedId); + qCDebug(generic) << "[MovieController] Resolved TMDb ID:" << resolvedId.toString() + << "for:" << m_movie->title(); + loadFanartTvImages(images); + } else { + onFanartLoadDone(m_movie, {}); + } + }); + searchJob->start(); } void MovieController::onFanartLoadDone(Movie* movie, QMap> posters) diff --git a/src/data/movie/MovieController.h b/src/data/movie/MovieController.h index 03173176a4..65fc2e86cc 100644 --- a/src/data/movie/MovieController.h +++ b/src/data/movie/MovieController.h @@ -83,6 +83,9 @@ private slots: void onDownloadFinished(DownloadManagerElement elem); private: + void loadFanartTvImages(QSet images); + void resolveIdAndLoadFanartTv(QSet images); + Movie* m_movie; bool m_infoLoaded; bool m_infoFromNfoLoaded; diff --git a/src/scrapers/image/FanartTv.cpp b/src/scrapers/image/FanartTv.cpp index b21c9d39b5..54f247ed38 100644 --- a/src/scrapers/image/FanartTv.cpp +++ b/src/scrapers/image/FanartTv.cpp @@ -5,13 +5,16 @@ #include "network/NetworkRequest.h" #include "scrapers/image/FanartTvConfiguration.h" #include "scrapers/movie/tmdb/TmdbMovie.h" -#include "scrapers/tv_show/thetvdb/TheTvDb.h" +#include "scrapers/tv_show/tmdb/TmdbTv.h" +#include "scrapers/tv_show/tmdb/TmdbTvConfiguration.h" #include "ui/main/MainWindow.h" #include #include #include #include +#include +#include #include namespace mediaelch { @@ -41,6 +44,7 @@ FanartTv::FanartTv(FanartTvConfiguration& settings, QObject* parent) : ImageProv ImageType::TvShowThumb, ImageType::TvShowSeasonThumb, ImageType::TvShowSeasonPoster, + ImageType::TvShowSeasonBanner, ImageType::TvShowLogos, ImageType::TvShowCharacterArt, ImageType::TvShowPoster, @@ -58,6 +62,7 @@ FanartTv::FanartTv(FanartTvConfiguration& settings, QObject* parent) : ImageProv m_apiKey = "842f7a5d1cc7396f142b8dd47c4ba42b"; m_tmdbConfig = std::make_unique(*Settings::instance()); m_tmdb = new TmdbMovie(*m_tmdbConfig, this); + m_tmdbTvConfig = std::make_unique(*Settings::instance()); } const ImageProvider::ScraperMeta& FanartTv::meta() const @@ -419,37 +424,96 @@ QVector FanartTv::parseMovieData(QString json, ImageType type) } /** - * \brief Searches for a TV show + * \brief Searches for a TV show using TMDb, then resolves TvDbIds for Fanart.tv * \param searchStr The TV show name/search string * \param limit Number of results, if zero, all results are returned - * \see FanartTv::onSearchTvShowFinished */ void FanartTv::searchTvShow(QString searchStr, mediaelch::Locale locale, int limit) { - using namespace mediaelch; using namespace mediaelch::scraper; m_searchResultLimit = limit; - auto* tvdb = dynamic_cast(Manager::instance()->scrapers().tvScraper(TheTvDb::ID)); - if (tvdb == nullptr) { - qFatal("[FanartTv] Cast to TheTvDb* failed!"); + auto* tmdbTv = dynamic_cast(Manager::instance()->scrapers().tvScraper(TmdbTv::ID)); + if (tmdbTv == nullptr) { + qCCritical(generic) << "[FanartTv] Could not get TmdbTv scraper for TV show search"; + emit sigSearchDone( + {}, {ScraperError::Type::InternalError, tr("TMDb scraper not available for TV show search")}); + return; } + ShowSearchJob::Config config{searchStr, locale, false}; - auto* searchJob = tvdb->search(config); + auto* searchJob = tmdbTv->search(config); connect(searchJob, &ShowSearchJob::searchFinished, this, &FanartTv::onSearchTvShowFinished, Qt::UniqueConnection); searchJob->start(); } void FanartTv::onSearchTvShowFinished(ShowSearchJob* searchJob) { - const auto results = toOldScraperSearchResult(searchJob->results()); + auto results = toOldScraperSearchResult(searchJob->results()); const ScraperError error = searchJob->scraperError(); searchJob->deleteLater(); - if (m_searchResultLimit == 0) { - emit sigSearchDone(results, error); - } else { - emit sigSearchDone(results.mid(0, m_searchResultLimit), error); + if (error.hasError()) { + emit sigSearchDone({}, error); + return; + } + + if (m_searchResultLimit > 0) { + results = results.mid(0, m_searchResultLimit); + } + + // TMDb search returns TMDb IDs, but Fanart.tv needs TvDb IDs. + // Resolve TvDbIds via TMDb external_ids for each result. + resolveTvDbIds(results, error); +} + +/** + * \brief Resolves TvDbIds for TMDb search results via TMDb external_ids API. + * Fanart.tv's TV API only accepts TvDb IDs, so each TMDb search result needs + * an extra API call to get the corresponding TvDb ID. + */ +void FanartTv::resolveTvDbIds(QVector results, const ScraperError& searchError) +{ + if (results.isEmpty()) { + emit sigSearchDone(results, searchError); + return; + } + + struct ResolveState + { + QVector resolved; + int pending = 0; + }; + auto state = QSharedPointer::create(); + state->pending = results.size(); + + for (int i = 0; i < results.size(); ++i) { + const TmdbId tmdbId(results[i].id); + const QString name = results[i].name; + const QDate released = results[i].released; + + m_tmdbApi.sendGetRequest(m_tmdbTvConfig->language(), + m_tmdbApi.makeApiUrl( + QStringLiteral("/tv/%1/external_ids").arg(tmdbId.toString()), m_tmdbTvConfig->language(), QUrlQuery()), + [this, state, name, released, searchError](QJsonDocument json, ScraperError error) { + ScraperSearchResult result; + result.name = name; + result.released = released; + + if (!error.hasError()) { + const int tvdbIdInt = json.object().value("tvdb_id").toInt(-1); + if (tvdbIdInt > 0) { + result.id = QStringLiteral("id%1").arg(tvdbIdInt); + } + } + + state->resolved.append(result); + + state->pending--; + if (state->pending <= 0) { + emit sigSearchDone(state->resolved, searchError); + } + }); } } @@ -620,9 +684,8 @@ void FanartTv::tvShowSeason(TvDbId tvdbId, SeasonNumber season, const mediaelch: void FanartTv::tvShowSeasonBanners(TvDbId tvdbId, SeasonNumber season, const mediaelch::Locale& locale) { - Q_UNUSED(tvdbId); - Q_UNUSED(season); Q_UNUSED(locale); + loadTvShowData(tvdbId, ImageType::TvShowSeasonBanner, season); } void FanartTv::tvShowSeasonBackdrops(TvDbId tvdbId, SeasonNumber season, const mediaelch::Locale& locale) @@ -657,6 +720,7 @@ QVector FanartTv::parseTvShowData(QString json, ImageType type, SeasonNu map.insert(ImageType::TvShowThumb, QStringList() << "tvthumb"); map.insert(ImageType::TvShowSeasonThumb, QStringList() << "seasonthumb"); map.insert(ImageType::TvShowSeasonPoster, QStringList() << "seasonposter"); + map.insert(ImageType::TvShowSeasonBanner, QStringList() << "seasonbanner"); map.insert(ImageType::TvShowPoster, QStringList() << "tvposter"); // clang-format on @@ -681,7 +745,8 @@ QVector FanartTv::parseTvShowData(QString json, ImageType type, SeasonNu continue; } - if ((type == ImageType::TvShowSeasonThumb || type == ImageType::TvShowSeasonPoster) + if ((type == ImageType::TvShowSeasonThumb || type == ImageType::TvShowSeasonPoster + || type == ImageType::TvShowSeasonBanner) && season != SeasonNumber::NoSeason && !poster.value("season").toString().isEmpty() && poster.value("season").toString().toInt() != season.toInt()) { continue; diff --git a/src/scrapers/image/FanartTv.h b/src/scrapers/image/FanartTv.h index 96e77ffe40..5b490e44bd 100644 --- a/src/scrapers/image/FanartTv.h +++ b/src/scrapers/image/FanartTv.h @@ -2,10 +2,13 @@ #include "globals/Globals.h" #include "network/NetworkManager.h" +#include "scrapers/ScraperResult.h" #include "scrapers/image/ImageProvider.h" #include "scrapers/movie/MovieScraper.h" #include "scrapers/movie/tmdb/TmdbMovieConfiguration.h" +#include "scrapers/tmdb/TmdbApi.h" #include "scrapers/tv_show/TvScraper.h" +#include "scrapers/tv_show/tmdb/TmdbTvConfiguration.h" #include #include @@ -18,7 +21,6 @@ namespace mediaelch { namespace scraper { -class TheTvDb; class TmdbMovie; class FanartTvConfiguration; @@ -104,11 +106,10 @@ private slots: QString m_apiKey; mediaelch::network::NetworkManager m_network; int m_searchResultLimit = 0; - mediaelch::scraper::TheTvDb* m_tvdb = nullptr; - mediaelch::scraper::ShowSearchJob* m_currentSearchJob = nullptr; std::unique_ptr m_tmdbConfig; mediaelch::scraper::TmdbMovie* m_tmdb; - + std::unique_ptr m_tmdbTvConfig; + TmdbApi m_tmdbApi; mediaelch::network::NetworkManager* network(); QVector parseMovieData(QString json, ImageType type); @@ -118,6 +119,7 @@ private slots: QVector parseTvShowData(QString json, ImageType type, SeasonNumber season = SeasonNumber::NoSeason); void loadTvShowData(TvDbId tvdbId, ImageType type, SeasonNumber season = SeasonNumber::NoSeason); void loadTvShowData(TvDbId tvdbId, QSet types, TvShow* show); + void resolveTvDbIds(QVector results, const ScraperError& searchError); QString keyParameter(); }; diff --git a/src/scrapers/image/FanartTvConfiguration.cpp b/src/scrapers/image/FanartTvConfiguration.cpp index 8ff1305a4d..248863cdf9 100644 --- a/src/scrapers/image/FanartTvConfiguration.cpp +++ b/src/scrapers/image/FanartTvConfiguration.cpp @@ -8,6 +8,7 @@ static constexpr char moduleName[] = "scrapers"; static const Settings::Key KEY_SCRAPERS_LANGUAGE(moduleName, "Scrapers/images.fanarttv/Language"); static const Settings::Key KEY_SCRAPERS_DISC_TYPE(moduleName, "Scrapers/images.fanarttv/DiscType"); static const Settings::Key KEY_SCRAPERS_PERSONAL_API_KEY(moduleName, "Scrapers/images.fanarttv/PersonalApiKey"); +static const Settings::Key KEY_SCRAPERS_IMAGE_FALLBACK(moduleName, "Scrapers/images.fanarttv/UseAsFallback"); } // namespace @@ -30,6 +31,7 @@ void FanartTvConfiguration::init() settings().setDefaultValue(KEY_SCRAPERS_LANGUAGE, defaultLocale().toString()); settings().setDefaultValue(KEY_SCRAPERS_DISC_TYPE, QStringLiteral("BluRay")); settings().setDefaultValue(KEY_SCRAPERS_PERSONAL_API_KEY, QStringLiteral("")); + settings().setDefaultValue(KEY_SCRAPERS_IMAGE_FALLBACK, false); } mediaelch::Locale FanartTvConfiguration::defaultLocale() @@ -98,6 +100,15 @@ void FanartTvConfiguration::setPersonalApiKey(const QString& value) settings().setValue(KEY_SCRAPERS_PERSONAL_API_KEY, value); } +bool FanartTvConfiguration::useAsFallback() +{ + return settings().value(KEY_SCRAPERS_IMAGE_FALLBACK).toBool(); +} + +void FanartTvConfiguration::setUseAsFallback(bool value) +{ + settings().setValue(KEY_SCRAPERS_IMAGE_FALLBACK, value); +} } // namespace scraper } // namespace mediaelch diff --git a/src/scrapers/image/FanartTvConfiguration.h b/src/scrapers/image/FanartTvConfiguration.h index 10afacb616..b19a546837 100644 --- a/src/scrapers/image/FanartTvConfiguration.h +++ b/src/scrapers/image/FanartTvConfiguration.h @@ -36,6 +36,9 @@ class FanartTvConfiguration : public QObject, public ScraperConfiguration ELCH_NODISCARD QString personalApiKey(); void setPersonalApiKey(const QString& value); + + ELCH_NODISCARD bool useAsFallback(); + void setUseAsFallback(bool value); }; } // namespace scraper diff --git a/src/ui/scrapers/image/FanartTvConfigurationView.cpp b/src/ui/scrapers/image/FanartTvConfigurationView.cpp index e6e51702ca..283ccac17b 100644 --- a/src/ui/scrapers/image/FanartTvConfigurationView.cpp +++ b/src/ui/scrapers/image/FanartTvConfigurationView.cpp @@ -1,5 +1,6 @@ #include "ui/scrapers/image/FanartTvConfigurationView.h" +#include #include #include @@ -20,6 +21,11 @@ FanartTvConfigurationView::FanartTvConfigurationView(FanartTvConfiguration& sett m_personalApiKeyEdit = new QLineEdit(this); + m_fallbackCheckBox = new QCheckBox(tr("Use as fallback for missing images"), this); + m_fallbackCheckBox->setToolTip(tr("When enabled, Fanart.tv automatically fills in missing images after scraping, " + "regardless of which scraper is configured for image fields.")); + m_fallbackCheckBox->setChecked(m_settings.useAsFallback()); + auto* layout = new QGridLayout(this); layout->addWidget(new QLabel(tr("Language")), 0, 0); layout->addWidget(m_languageBox, 0, 1); @@ -27,6 +33,7 @@ FanartTvConfigurationView::FanartTvConfigurationView(FanartTvConfiguration& sett layout->addWidget(m_discBox, 1, 1); layout->addWidget(new QLabel(tr("Personal API key")), 2, 0); layout->addWidget(m_personalApiKeyEdit, 2, 1); + layout->addWidget(m_fallbackCheckBox, 3, 0, 1, 2); layout->setColumnStretch(2, 1); layout->setContentsMargins(12, 0, 12, 12); @@ -58,6 +65,9 @@ FanartTvConfigurationView::FanartTvConfigurationView(FanartTvConfiguration& sett m_personalApiKeyEdit->setText(apiKey); m_personalApiKeyEdit->blockSignals(blocked); }); + + connect( + m_fallbackCheckBox, &QCheckBox::toggled, this, [this](bool checked) { m_settings.setUseAsFallback(checked); }); } void FanartTvConfigurationView::setPreferredDiscType(const QString& discType) diff --git a/src/ui/scrapers/image/FanartTvConfigurationView.h b/src/ui/scrapers/image/FanartTvConfigurationView.h index dbfd87c517..8501f4f19c 100644 --- a/src/ui/scrapers/image/FanartTvConfigurationView.h +++ b/src/ui/scrapers/image/FanartTvConfigurationView.h @@ -3,6 +3,7 @@ #include "scrapers/image/FanartTvConfiguration.h" #include "ui/small_widgets/LanguageCombo.h" +#include #include #include #include @@ -26,6 +27,7 @@ class FanartTvConfigurationView : public QWidget LanguageCombo* m_languageBox{nullptr}; QComboBox* m_discBox{nullptr}; QLineEdit* m_personalApiKeyEdit{nullptr}; + QCheckBox* m_fallbackCheckBox{nullptr}; }; } // namespace scraper diff --git a/src/ui/settings/CustomTvScraperSettingsWidget.cpp b/src/ui/settings/CustomTvScraperSettingsWidget.cpp index 3781066a70..eb9baa5eec 100644 --- a/src/ui/settings/CustomTvScraperSettingsWidget.cpp +++ b/src/ui/settings/CustomTvScraperSettingsWidget.cpp @@ -4,6 +4,7 @@ #include "globals/Manager.h" #include "log/Log.h" #include "scrapers/ScraperInfos.h" +#include "scrapers/image/FanartTv.h" #include "scrapers/tv_show/custom/CustomTvScraper.h" #include "settings/Settings.h" @@ -156,6 +157,30 @@ QComboBox* CustomTvScraperSettingsWidget::comboForTvScraperInfo(ShowScraperInfo } } + // For image-related fields, also offer Fanart.tv as an image provider. + // Only fields that FanartTv's API actually supports are listed here. + static const QSet imageInfos{ + ShowScraperInfo::Banner, // + ShowScraperInfo::Fanart, // + ShowScraperInfo::Poster, // + ShowScraperInfo::Thumb, // + ShowScraperInfo::ExtraArts, // + ShowScraperInfo::SeasonPoster, // + ShowScraperInfo::SeasonBanner, // + ShowScraperInfo::SeasonThumb, // + }; + + if (imageInfos.contains(info)) { + for (auto* const img : Manager::instance()->scrapers().imageProviders()) { + if (img->meta().identifier == mediaelch::scraper::FanartTv::ID) { + box->addItem(img->meta().name, img->meta().identifier); + box->setItemData(0, infoInt, Qt::UserRole + 1); + ++scraperCount; + break; + } + } + } + if (scraperCount == 0) { box->addItem(tr("No Scraper Available"), "noscraper"); box->setItemData(0, -1, Qt::UserRole + 1); diff --git a/src/ui/tv_show/TvShowWidgetTvShow.cpp b/src/ui/tv_show/TvShowWidgetTvShow.cpp index d7bbcac6a0..1030f11318 100644 --- a/src/ui/tv_show/TvShowWidgetTvShow.cpp +++ b/src/ui/tv_show/TvShowWidgetTvShow.cpp @@ -8,6 +8,9 @@ #include "media/ImageCache.h" #include "media/ImageUtils.h" #include "scrapers/ScraperInfos.h" +#include "scrapers/image/FanartTv.h" +#include "scrapers/image/FanartTvConfiguration.h" +#include "settings/Settings.h" #include "ui/UiUtils.h" #include "ui/image/ImageDialog.h" #include "ui/main/MainWindow.h" @@ -537,13 +540,62 @@ void TvShowWidgetTvShow::onInfoLoadDone(TvShow* show, QSet deta show->fillMissingEpisodes(); } - QSet types{ImageType::TvShowClearArt, - ImageType::TvShowLogos, - ImageType::TvShowCharacterArt, - ImageType::TvShowThumb, - ImageType::TvShowSeasonThumb}; + // Determine which image types FanartTv should load. + // Three triggers: 1) ExtraArts selected, 2) specific fields assigned to FanartTv + // in Custom TV Scraper settings, 3) global fallback checkbox. + QSet types; + bool needFanartTv = false; - if (show->tvdbId().isValid() && !types.isEmpty() && details.contains(ShowScraperInfo::ExtraArts)) { + // 1) ExtraArts: always load these art types from FanartTv (existing behavior) + if (details.contains(ShowScraperInfo::ExtraArts)) { + types.insert(ImageType::TvShowClearArt); + types.insert(ImageType::TvShowLogos); + types.insert(ImageType::TvShowCharacterArt); + types.insert(ImageType::TvShowThumb); + types.insert(ImageType::TvShowSeasonThumb); + needFanartTv = true; + } + + // 2) Check Custom TV Scraper field assignments for FanartTv + // clang-format off + static const QMap infoToImageType{ + {ShowScraperInfo::Banner, ImageType::TvShowBanner}, + {ShowScraperInfo::Fanart, ImageType::TvShowBackdrop}, + {ShowScraperInfo::Poster, ImageType::TvShowPoster}, + {ShowScraperInfo::Thumb, ImageType::TvShowThumb}, + {ShowScraperInfo::SeasonPoster, ImageType::TvShowSeasonPoster}, + {ShowScraperInfo::SeasonBanner, ImageType::TvShowSeasonBanner}, + {ShowScraperInfo::SeasonThumb, ImageType::TvShowSeasonThumb}, + }; + // clang-format on + + const auto customShowSettings = Settings::instance()->customTvScraperShow(); + for (auto it = infoToImageType.constBegin(); it != infoToImageType.constEnd(); ++it) { + if (customShowSettings.value(it.key()) == mediaelch::scraper::FanartTv::ID && details.contains(it.key())) { + types.insert(it.value()); + needFanartTv = true; + } + } + + // 3) Global fallback: load ALL image types FanartTv supports + auto* ftConfig = dynamic_cast( + Manager::instance()->scrapers().imageProviderConfig(mediaelch::scraper::FanartTv::ID)); + const bool fanartTvFallback = (ftConfig != nullptr && ftConfig->useAsFallback()); + if (fanartTvFallback) { + types.insert(ImageType::TvShowClearArt); + types.insert(ImageType::TvShowLogos); + types.insert(ImageType::TvShowCharacterArt); + types.insert(ImageType::TvShowThumb); + types.insert(ImageType::TvShowBanner); + types.insert(ImageType::TvShowPoster); + types.insert(ImageType::TvShowBackdrop); + types.insert(ImageType::TvShowSeasonPoster); + types.insert(ImageType::TvShowSeasonBanner); + types.insert(ImageType::TvShowSeasonThumb); + needFanartTv = true; + } + + if (show->tvdbId().isValid() && !types.isEmpty() && needFanartTv) { Manager::instance()->fanartTv()->tvShowImages(show, show->tvdbId(), types, locale); connect(Manager::instance()->fanartTv(), &mediaelch::scraper::ImageProvider::sigTvShowImagesLoaded, @@ -601,7 +653,10 @@ void TvShowWidgetTvShow::onLoadDone(TvShow* show, QMapbanners().isEmpty() && show->infosToLoad().contains(ShowScraperInfo::Banner)) { + auto* ftCfg = dynamic_cast( + Manager::instance()->scrapers().imageProviderConfig(mediaelch::scraper::FanartTv::ID)); + const bool ftFallback = (ftCfg != nullptr && ftCfg->useAsFallback()); + if (!show->banners().isEmpty() && (show->infosToLoad().contains(ShowScraperInfo::Banner) || ftFallback)) { emit sigSetActionSaveEnabled(false, MainWidgets::TvShows); DownloadManagerElement d; d.imageType = ImageType::TvShowBanner; @@ -618,7 +673,39 @@ void TvShowWidgetTvShow::onLoadDone(TvShow* show, QMap> it(posters); while (it.hasNext()) { it.next(); - if (it.key() == ImageType::TvShowClearArt && !it.value().isEmpty()) { + if (it.key() == ImageType::TvShowPoster && !it.value().isEmpty() && show->posters().isEmpty()) { + // FanartTv poster as fallback — only if the scraper didn't provide one + DownloadManagerElement d; + d.imageType = ImageType::TvShowPoster; + d.url = it.value().at(0).originalUrl; + d.show = show; + m_imageDownloadManager->addDownload(d); + if (m_show == show) { + ui->poster->setLoading(true); + } + downloadsSize++; + } else if (it.key() == ImageType::TvShowBackdrop && !it.value().isEmpty() && show->backdrops().isEmpty()) { + // FanartTv backdrop as fallback — only if the scraper didn't provide one + DownloadManagerElement d; + d.imageType = ImageType::TvShowBackdrop; + d.url = it.value().at(0).originalUrl; + d.show = show; + m_imageDownloadManager->addDownload(d); + if (m_show == show) { + ui->backdrop->setLoading(true); + } + downloadsSize++; + } else if (it.key() == ImageType::TvShowBanner && !it.value().isEmpty()) { + DownloadManagerElement d; + d.imageType = ImageType::TvShowBanner; + d.url = it.value().at(0).originalUrl; + d.show = show; + m_imageDownloadManager->addDownload(d); + if (m_show == show) { + ui->banner->setLoading(true); + } + downloadsSize++; + } else if (it.key() == ImageType::TvShowClearArt && !it.value().isEmpty()) { DownloadManagerElement d; d.imageType = ImageType::TvShowClearArt; d.url = it.value().at(0).originalUrl; @@ -676,6 +763,44 @@ void TvShowWidgetTvShow::onLoadDone(TvShow* show, QMap bannersForSeasons; + for (const Poster& p : it.value()) { + if (bannersForSeasons.contains(p.season)) { + continue; + } + if (!show->seasons().contains(p.season)) { + continue; + } + + DownloadManagerElement d; + d.imageType = ImageType::TvShowSeasonBanner; + d.url = p.originalUrl; + d.show = show; + d.season = p.season; + m_imageDownloadManager->addDownload(d); + downloadsSize++; + bannersForSeasons.append(p.season); + } + } else if (it.key() == ImageType::TvShowSeasonPoster && !it.value().isEmpty()) { + QVector postersForSeasons; + for (const Poster& p : it.value()) { + if (postersForSeasons.contains(p.season)) { + continue; + } + if (!show->seasons().contains(p.season)) { + continue; + } + + DownloadManagerElement d; + d.imageType = ImageType::TvShowSeasonPoster; + d.url = p.originalUrl; + d.show = show; + d.season = p.season; + m_imageDownloadManager->addDownload(d); + downloadsSize++; + postersForSeasons.append(p.season); + } } }