Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions data/i18n/MediaElch_de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9973,6 +9973,14 @@ automatisch nach dem Laden</translation>
<source>Personal API key</source>
<translation>Persönlicher API Schlüssel</translation>
</message>
<message>
<source>Use as fallback for missing images</source>
<translation>Als Fallback für fehlende Bilder verwenden</translation>
</message>
<message>
<source>When enabled, Fanart.tv automatically fills in missing images after scraping, regardless of which scraper is configured for image fields.</source>
<translation>Wenn aktiviert, ergänzt Fanart.tv automatisch fehlende Bilder nach dem Scrapen, unabhängig davon welcher Scraper für Bildfelder konfiguriert ist.</translation>
</message>
</context>
<context>
<name>mediaelch::scraper::FanartTvMusic</name>
Expand Down
84 changes: 75 additions & 9 deletions src/data/movie/MovieController.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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<mediaelch::scraper::FanartTvConfiguration*>(
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<ImageType, QVector<Poster>>());
// No TMDb/IMDB ID available — try to resolve via TMDb title search
resolveIdAndLoadFanartTv(images);
}
}

void MovieController::loadFanartTvImages(QSet<ImageType> 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<ImageType> images)
{
using namespace mediaelch::scraper;

auto* tmdb = dynamic_cast<TmdbMovie*>(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<ImageType, QVector<Poster>> posters)
Expand Down
3 changes: 3 additions & 0 deletions src/data/movie/MovieController.h
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,9 @@ private slots:
void onDownloadFinished(DownloadManagerElement elem);

private:
void loadFanartTvImages(QSet<ImageType> images);
void resolveIdAndLoadFanartTv(QSet<ImageType> images);

Movie* m_movie;
bool m_infoLoaded;
bool m_infoFromNfoLoaded;
Expand Down
97 changes: 81 additions & 16 deletions src/scrapers/image/FanartTv.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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 <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonValue>
#include <QSharedPointer>
#include <QUrlQuery>
#include <QVariant>

namespace mediaelch {
Expand Down Expand Up @@ -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,
Expand All @@ -58,6 +62,7 @@ FanartTv::FanartTv(FanartTvConfiguration& settings, QObject* parent) : ImageProv
m_apiKey = "842f7a5d1cc7396f142b8dd47c4ba42b";
m_tmdbConfig = std::make_unique<mediaelch::scraper::TmdbMovieConfiguration>(*Settings::instance());
m_tmdb = new TmdbMovie(*m_tmdbConfig, this);
m_tmdbTvConfig = std::make_unique<mediaelch::scraper::TmdbTvConfiguration>(*Settings::instance());
}

const ImageProvider::ScraperMeta& FanartTv::meta() const
Expand Down Expand Up @@ -419,37 +424,96 @@ QVector<Poster> 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<TheTvDb*>(Manager::instance()->scrapers().tvScraper(TheTvDb::ID));
if (tvdb == nullptr) {
qFatal("[FanartTv] Cast to TheTvDb* failed!");
auto* tmdbTv = dynamic_cast<TmdbTv*>(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<ScraperSearchResult> results, const ScraperError& searchError)
{
if (results.isEmpty()) {
emit sigSearchDone(results, searchError);
return;
}

struct ResolveState
{
QVector<ScraperSearchResult> resolved;
int pending = 0;
};
auto state = QSharedPointer<ResolveState>::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);
}
});
}
}

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -657,6 +720,7 @@ QVector<Poster> 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

Expand All @@ -681,7 +745,8 @@ QVector<Poster> 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;
Expand Down
10 changes: 6 additions & 4 deletions src/scrapers/image/FanartTv.h
Original file line number Diff line number Diff line change
Expand Up @@ -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 <QMap>
#include <QNetworkReply>
Expand All @@ -18,7 +21,6 @@
namespace mediaelch {
namespace scraper {

class TheTvDb;
class TmdbMovie;
class FanartTvConfiguration;

Expand Down Expand Up @@ -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<mediaelch::scraper::TmdbMovieConfiguration> m_tmdbConfig;
mediaelch::scraper::TmdbMovie* m_tmdb;

std::unique_ptr<mediaelch::scraper::TmdbTvConfiguration> m_tmdbTvConfig;
TmdbApi m_tmdbApi;

mediaelch::network::NetworkManager* network();
QVector<Poster> parseMovieData(QString json, ImageType type);
Expand All @@ -118,6 +119,7 @@ private slots:
QVector<Poster> parseTvShowData(QString json, ImageType type, SeasonNumber season = SeasonNumber::NoSeason);
void loadTvShowData(TvDbId tvdbId, ImageType type, SeasonNumber season = SeasonNumber::NoSeason);
void loadTvShowData(TvDbId tvdbId, QSet<ImageType> types, TvShow* show);
void resolveTvDbIds(QVector<ScraperSearchResult> results, const ScraperError& searchError);
QString keyParameter();
};

Expand Down
11 changes: 11 additions & 0 deletions src/scrapers/image/FanartTvConfiguration.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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()
Expand Down Expand Up @@ -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
3 changes: 3 additions & 0 deletions src/scrapers/image/FanartTvConfiguration.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading