MythTV master
metadataimagedownload.cpp
Go to the documentation of this file.
1// C++
2#include <ranges>
3#include <utility>
4
5// qt
6#include <QCoreApplication>
7#include <QDir>
8#include <QEvent>
9#include <QFileInfo>
10#include <QImage>
11
12// myth
21
23
24const QEvent::Type ImageDLEvent::kEventType =
25 (QEvent::Type) QEvent::registerEventType();
26
27const QEvent::Type ImageDLFailureEvent::kEventType =
28 (QEvent::Type) QEvent::registerEventType();
29
30const QEvent::Type ThumbnailDLEvent::kEventType =
31 (QEvent::Type) QEvent::registerEventType();
32
34{
35 cancel();
36 wait();
37}
38
40 QString url, QVariant data)
41{
42 QMutexLocker lock(&m_mutex);
43
44 auto *id = new ThumbnailData();
45 id->title = std::move(title);
46 id->data = std::move(data);
47 id->url = std::move(url);
48 m_thumbnailList.append(id);
49 if (!isRunning())
50 start();
51}
52
58{
59 QMutexLocker lock(&m_mutex);
60
61 m_downloadList.append(lookup);
62 lookup->DecrRef();
63 if (!isRunning())
64 start();
65}
66
68{
69 QMutexLocker lock(&m_mutex);
70
71 qDeleteAll(m_thumbnailList);
72 m_thumbnailList.clear();
73 // clearing m_downloadList automatically delete all its content
74 m_downloadList.clear();
75}
76
78{
79 RunProlog();
80
81 // Always handle thumbnails first, they're higher priority.
82 ThumbnailData *thumb = nullptr;
83 while ((thumb = moreThumbs()) != nullptr)
84 {
85 QString sFilename = getDownloadFilename(thumb->title, thumb->url);
86
87 bool exists = QFile::exists(sFilename);
88 if (!exists && !thumb->url.isEmpty())
89 {
90 if (!GetMythDownloadManager()->download(thumb->url, sFilename))
91 {
92 LOG(VB_GENERAL, LOG_ERR,
93 QString("MetadataImageDownload: failed to download thumbnail from: %1")
94 .arg(thumb->url));
95
96 delete thumb;
97 continue;
98 }
99 }
100
101 // inform parent we have thumbnail ready for it
102 if (QFile::exists(sFilename) && m_parent)
103 {
104 LOG(VB_GENERAL, LOG_DEBUG,
105 QString("Threaded Image Thumbnail Download: %1")
106 .arg(sFilename));
107 thumb->url = sFilename;
108 QCoreApplication::postEvent(m_parent,
109 new ThumbnailDLEvent(thumb));
110 }
111 else
112 {
113 delete thumb;
114 }
115 }
116
117 while (true)
118 {
119 m_mutex.lock();
120 if (m_downloadList.isEmpty())
121 {
122 // no more to process, we're done
123 m_mutex.unlock();
124 break;
125 }
126 // Ref owns the MetadataLookup object for the duration of the loop
127 // and it will be deleted automatically when the loop completes
129 m_mutex.unlock();
130 MetadataLookup *lookup = ref;
131 DownloadMap downloads = lookup->GetDownloads();
132 DownloadMap downloaded;
133
134 bool errored = false;
135 for (DownloadMap::iterator i = downloads.begin();
136 i != downloads.end(); ++i)
137 {
138 VideoArtworkType type = i.key();
139 ArtworkInfo info = i.value();
140 QString filename = getDownloadFilename( type, lookup,
141 info.url );
142 if (lookup->GetHost().isEmpty())
143 {
144 QString path = getLocalWritePath(lookup->GetType(), type);
145 QDir dirPath(path);
146 if (!dirPath.exists())
147 {
148 if (!dirPath.mkpath(path))
149 {
150 LOG(VB_GENERAL, LOG_ERR,
151 QString("Metadata Image Download: Unable to create "
152 "path %1, aborting download.").arg(path));
153 errored = true;
154 break;
155 }
156 }
157 QString finalfile = path + "/" + filename;
158 QString oldurl = info.url;
159 info.url = finalfile;
160 if (!QFile::exists(finalfile) || lookup->GetAllowOverwrites())
161 {
162 QFile dest_file(finalfile);
163 if (dest_file.exists())
164 {
165 QFileInfo fi(finalfile);
166 GetMythUI()->RemoveFromCacheByFile(fi.fileName());
167 dest_file.remove();
168 }
169
170 LOG(VB_GENERAL, LOG_INFO,
171 QString("Metadata Image Download: %1 ->%2")
172 .arg(oldurl, finalfile));
173 QByteArray download;
174 GetMythDownloadManager()->download(oldurl, &download);
175
176 QImage testImage;
177 bool didLoad = testImage.loadFromData(download);
178 if (!didLoad)
179 {
180 LOG(VB_GENERAL, LOG_ERR,
181 QString("Tried to write %1, but it appears to be "
182 "an HTML redirect (filesize %2).")
183 .arg(oldurl).arg(download.size()));
184 errored = true;
185 break;
186 }
187
188 if (dest_file.open(QIODevice::WriteOnly))
189 {
190 off_t size = dest_file.write(download);
191 dest_file.close();
192 if (size != download.size())
193 {
194 // File creation failed for some reason, delete it
195 RemoteFile::DeleteFile(finalfile);
196 LOG(VB_GENERAL, LOG_ERR,
197 QString("Image Download: Error Writing Image "
198 "to file: %1").arg(finalfile));
199 errored = true;
200 break;
201 }
202 }
203 }
204 }
205 else
206 {
207 QString path = getStorageGroupURL(type, lookup->GetHost());
208 QString finalfile = path + filename;
209 QString oldurl = info.url;
210 info.url = finalfile;
211 bool exists = false;
212 bool onMaster = false;
213 QString resolvedFN;
215 gCoreContext->IsThisHost(lookup->GetHost()))
216 {
218 resolvedFN = sg.FindFile(filename);
219 exists = !resolvedFN.isEmpty() && QFile::exists(resolvedFN);
220 if (!exists)
221 {
222 resolvedFN = getLocalStorageGroupPath(type,
223 lookup->GetHost()) + "/" + filename;
224 }
225 onMaster = true;
226 }
227 else
228 {
229 exists = RemoteFile::Exists(finalfile);
230 }
231
232 if (!exists || lookup->GetAllowOverwrites())
233 {
234 if (exists && !onMaster)
235 {
236 QFileInfo fi(finalfile);
237 GetMythUI()->RemoveFromCacheByFile(fi.fileName());
238 RemoteFile::DeleteFile(finalfile);
239 }
240 else if (exists)
241 {
242 QFile::remove(resolvedFN);
243 }
244
245 LOG(VB_GENERAL, LOG_INFO,
246 QString("Metadata Image Download: %1 -> %2")
247 .arg(oldurl, finalfile));
248 QByteArray download;
249 GetMythDownloadManager()->download(oldurl, &download);
250
251 QImage testImage;
252 bool didLoad = testImage.loadFromData(download);
253 if (!didLoad)
254 {
255 LOG(VB_GENERAL, LOG_ERR,
256 QString("Tried to write %1, but it appears to be "
257 "an HTML redirect or corrupt file "
258 "(filesize %2).")
259 .arg(oldurl).arg(download.size()));
260 errored = true;
261 break;
262 }
263
264 if (!onMaster)
265 {
266 RemoteFile outFile(finalfile, true);
267
268 if (!outFile.isOpen())
269 {
270 LOG(VB_GENERAL, LOG_ERR,
271 QString("Image Download: Failed to open "
272 "remote file (%1) for write. Does "
273 "Storage Group Exist?")
274 .arg(finalfile));
275 errored = true;
276 break;
277 }
278 off_t written = outFile.Write(download.constData(),
279 download.size());
280 if (written != download.size())
281 {
282 // File creation failed for some reason, delete it
283 RemoteFile::DeleteFile(finalfile);
284
285 LOG(VB_GENERAL, LOG_ERR,
286 QString("Image Download: Error Writing Image "
287 "to file: %1").arg(finalfile));
288 errored = true;
289 break;
290 }
291 }
292 else
293 {
294 QFile dest_file(resolvedFN);
295 if (dest_file.open(QIODevice::WriteOnly))
296 {
297 off_t size = dest_file.write(download);
298 dest_file.close();
299 if (size != download.size())
300 {
301 // File creation failed for some reason, delete it
302 RemoteFile::DeleteFile(resolvedFN);
303 LOG(VB_GENERAL, LOG_ERR,
304 QString("Image Download: Error Writing Image "
305 "to file: %1").arg(finalfile));
306 errored = true;
307 break;
308 }
309 }
310 }
311 }
312 }
313 if (!errored)
314 {
315 // update future Artwork Map with what we've successfully
316 // retrieved (either downloaded or already existing
317 downloaded.insert(type, info);
318 }
319 }
320 if (errored)
321 {
322 QCoreApplication::postEvent(m_parent,
323 new ImageDLFailureEvent(lookup));
324 }
325 lookup->SetDownloads(downloaded);
326 QCoreApplication::postEvent(m_parent, new ImageDLEvent(lookup));
327 }
328
329 RunEpilog();
330}
331
333{
334 QMutexLocker lock(&m_mutex);
335 ThumbnailData *ret = nullptr;
336
337 if (!m_thumbnailList.isEmpty())
338 ret = m_thumbnailList.takeFirst();
339 return ret;
340}
341
342QString getDownloadFilename(const QString& title, const QString& url)
343{
344 QString fileprefix = GetConfDir();
345
346 QDir dir(fileprefix);
347 if (!dir.exists())
348 dir.mkdir(fileprefix);
349
350 fileprefix += "/cache/metadata-thumbcache";
351
352 dir.setPath(fileprefix);;
353 if (!dir.exists())
354 dir.mkdir(fileprefix);
355
356 QByteArray titlearr(title.toLatin1());
357 QByteArray urlarr(url.toLatin1());
358#if QT_VERSION < QT_VERSION_CHECK(6,0,0)
359 quint16 titleChecksum = qChecksum(titlearr.data(), titlearr.length());
360 quint16 urlChecksum = qChecksum(urlarr.data(), urlarr.length());
361#else
362 quint16 titleChecksum = qChecksum(titlearr);
363 quint16 urlChecksum = qChecksum(urlarr);
364#endif
365 QUrl qurl(url);
366 QString ext = QFileInfo(qurl.path()).suffix();
367 QString basefilename = QString("thumbnail_%1_%2.%3")
368 .arg(QString::number(urlChecksum),
369 QString::number(titleChecksum), ext);
370
371 QString outputfile = QString("%1/%2").arg(fileprefix, basefilename);
372
373 return outputfile;
374}
375
377 const QString& url)
378{
379 QString basefilename;
380 QString title;
381 QString inter;
382 uint tracknum = lookup->GetTrackNumber();
383 uint season = lookup->GetSeason();
384 uint episode = lookup->GetEpisode();
385 QString system = lookup->GetSystem();
386 if (!lookup->GetIsCollection() && (season > 0 || episode > 0))
387 {
388 title = lookup->GetTitle();
389 if (title.contains("/"))
390 title.replace("/", "-");
391 if (title.contains("?"))
392 title.replace("?", "");
393 if (title.contains("*"))
394 title.replace("*", "");
395 inter = QString(" Season %1").arg(QString::number(season));
397 inter += QString("x%1").arg(QString::number(episode));
398 }
399 else if (lookup->GetType() == kMetadataVideo ||
400 lookup->GetType() == kMetadataRecording)
401 {
402 title = lookup->GetInetref();
403 }
404 else if (lookup->GetType() == kMetadataGame)
405 {
406 title = QString("%1 (%2)").arg(lookup->GetTitle(), lookup->GetSystem());
407 }
408
409 if (tracknum > 0)
410 inter = QString(" Track %1").arg(QString::number(tracknum));
411 else if (!system.isEmpty())
412 inter = QString(" (%1)").arg(system);
413
414 QString suffix;
415 QUrl qurl(url);
416 QString ext = QFileInfo(qurl.path()).suffix();
417
418 if (type == kArtworkCoverart)
419 suffix = "_coverart";
420 else if (type == kArtworkFanart)
421 suffix = "_fanart";
422 else if (type == kArtworkBanner)
423 suffix = "_banner";
424 else if (type == kArtworkScreenshot)
425 suffix = "_screenshot";
426 else if (type == kArtworkPoster)
427 suffix = "_poster";
428 else if (type == kArtworkBackCover)
429 suffix = "_backcover";
430 else if (type == kArtworkInsideCover)
431 suffix = "_insidecover";
432 else if (type == kArtworkCDImage)
433 suffix = "_cdimage";
434
435 basefilename = title + inter + suffix + "." + ext;
436
437 return basefilename;
438}
439
441{
442 QString ret;
443
444 if (metadatatype == kMetadataVideo)
445 {
446 if (type == kArtworkCoverart)
447 ret = gCoreContext->GetSetting("VideoArtworkDir");
448 else if (type == kArtworkFanart)
449 ret = gCoreContext->GetSetting("mythvideo.fanartDir");
450 else if (type == kArtworkBanner)
451 ret = gCoreContext->GetSetting("mythvideo.bannerDir");
452 else if (type == kArtworkScreenshot)
453 ret = gCoreContext->GetSetting("mythvideo.screenshotDir");
454 }
455 else if (metadatatype == kMetadataMusic)
456 {
457 }
458 else if (metadatatype == kMetadataGame)
459 {
460 if (type == kArtworkCoverart)
461 ret = gCoreContext->GetSetting("mythgame.boxartdir");
462 else if (type == kArtworkFanart)
463 ret = gCoreContext->GetSetting("mythgame.fanartdir");
464 else if (type == kArtworkScreenshot)
465 ret = gCoreContext->GetSetting("mythgame.screenshotdir");
466 }
467
468 return ret;
469}
470
471QString getStorageGroupURL(VideoArtworkType type, const QString& host)
472{
473 QString sgroup = getStorageGroupName(type);
475
476 return MythCoreContext::GenMythURL(host, port, "", sgroup);
477}
478
479QString getLocalStorageGroupPath(VideoArtworkType type, const QString& host)
480{
481 QString path;
482
484
485 path = sg.FindNextDirMostFree();
486
487 return path;
488}
489
491{
492 switch (type)
493 {
494 case kArtworkCoverart:
495 return "Coverart";
496 case kArtworkFanart:
497 return "Fanart";
498 case kArtworkBanner:
499 return "Banners";
501 return "Screenshots";
502 default:
503 return "Default";
504 }
505}
506
508{
509 QString cache = QString("%1/cache/metadata-thumbcache")
510 .arg(GetConfDir());
511 QDir cacheDir(cache);
512 QStringList thumbs = cacheDir.entryList(QDir::Files);
513
514 for (const auto & thumb : std::ranges::reverse_view(thumbs))
515 {
516 QString filename = QString("%1/%2").arg(cache, thumb);
517 QFileInfo fi(filename);
518 QDateTime lastmod = fi.lastModified();
519 if (lastmod.addDays(2) < MythDate::current())
520 {
521 LOG(VB_GENERAL, LOG_DEBUG, QString("Deleting file %1")
522 .arg(filename));
523 QFile::remove(filename);
524 }
525 }
526}
527
static const Type kEventType
static const Type kEventType
bool isRunning(void) const
Definition: mthread.cpp:247
void RunProlog(void)
Sets up a thread, call this if you reimplement run().
Definition: mthread.cpp:180
void start(QThread::Priority p=QThread::InheritPriority)
Tell MThread to start running the thread in the near future.
Definition: mthread.cpp:267
void RunEpilog(void)
Cleans up a thread's resources, call this if you reimplement run().
Definition: mthread.cpp:193
bool wait(std::chrono::milliseconds time=std::chrono::milliseconds::max())
Wait for the MThread to exit, with a maximum timeout.
Definition: mthread.cpp:284
void addThumb(QString title, QString url, QVariant data)
MetadataLookupList m_downloadList
QList< ThumbnailData * > m_thumbnailList
void run() override
Runs the Qt event loop unless we have a QRunnable, in which case we run the runnable run instead.
void addDownloads(MetadataLookup *lookup)
addLookup: Add lookup to bottom of the queue MetadataDownload::m_downloadList takes ownership of the ...
uint GetSeason() const
QString GetHost() const
MetadataType GetType() const
bool GetAllowOverwrites() const
uint GetTrackNumber() const
bool GetIsCollection() const
QString GetTitle() const
QString GetSystem() const
void SetDownloads(DownloadMap map)
DownloadMap GetDownloads() const
QString GetInetref() const
uint GetEpisode() const
bool IsThisHost(const QString &addr)
is this address mapped to this host
QString GetSetting(const QString &key, const QString &defaultval="")
int GetBackendServerPort(void)
Returns the locally defined backend control port.
static QString GenMythURL(const QString &host=QString(), int port=0, QString path=QString(), const QString &storageGroup=QString())
bool IsMasterBackend(void)
is this the actual MBE process
bool download(const QString &url, const QString &dest, bool reload=false)
Downloads a URL to a file in blocking mode.
void RemoveFromCacheByFile(const QString &File)
RefCountHandler< T > takeFirstAndDecr(void)
Removes the first item in the list and returns it.
virtual int DecrRef(void)
Decrements reference count and deletes on 0.
static bool DeleteFile(const QString &url)
Definition: remotefile.cpp:420
static bool Exists(const QString &url, struct stat *fileinfo)
Definition: remotefile.cpp:463
bool isOpen(void) const
Definition: remotefile.cpp:247
int Write(const void *data, int size)
Definition: remotefile.cpp:838
QString FindFile(const QString &filename)
QString FindNextDirMostFree(void)
static const Type kEventType
unsigned int uint
Definition: compat.h:60
MetadataType
@ kMetadataGame
@ kMetadataRecording
@ kMetadataVideo
@ kMetadataMusic
QMap< VideoArtworkType, ArtworkInfo > DownloadMap
QString getDownloadFilename(const QString &title, const QString &url)
QString getLocalWritePath(MetadataType metadatatype, VideoArtworkType type)
void cleanThumbnailCacheDir()
QString getLocalStorageGroupPath(VideoArtworkType type, const QString &host)
QString getStorageGroupURL(VideoArtworkType type, const QString &host)
QString getStorageGroupName(VideoArtworkType type)
VideoArtworkType
@ kArtworkBackCover
@ kArtworkScreenshot
@ kArtworkInsideCover
@ kArtworkCDImage
@ kArtworkFanart
@ kArtworkPoster
@ kArtworkBanner
@ kArtworkCoverart
MythCoreContext * gCoreContext
This global variable contains the MythCoreContext instance for the app.
QString GetConfDir(void)
Definition: mythdirs.cpp:285
MythDownloadManager * GetMythDownloadManager(void)
Gets the pointer to the MythDownloadManager singleton.
#define LOG(_MASK_, _LEVEL_, _QSTRING_)
Definition: mythlogging.h:39
MythUIHelper * GetMythUI()
QDateTime current(bool stripped)
Returns current Date and Time in UTC.
Definition: mythdate.cpp:15
dictionary info
Definition: azlyrics.py:7
bool exists(str path)
Definition: xbmcvfs.py:51