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