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); + } } }