MythTV master
metadatagrabber.cpp
Go to the documentation of this file.
1// Qt headers
2#include <QChar> // Fix Qt6 GCC SFINAE warning
3#include <QDateTime>
4#include <QDir>
5#include <QMap>
6#include <QMutex>
7#include <QMutexLocker>
8#include <QRegularExpression>
9#include <utility>
10
11// MythTV headers
18
19#include "metadatacommon.h"
20#include "metadatagrabber.h"
21
22#define LOC QString("Metadata Grabber: ")
23static constexpr std::chrono::seconds kGrabberRefresh { 60s };
24
25static const QRegularExpression kRetagRef { R"(^([a-zA-Z0-9_\-\.]+\.[a-zA-Z0-9]{1,3})[:_](.*))" };
26
28static QMutex s_grabberLock;
29static QDateTime s_grabberAge;
30
32 QString m_path;
33 QString m_setting;
34 QString m_def;
35};
36
37static const QMap<GrabberType, GrabberOpts> grabberTypes {
38 { kGrabberMovie, { .m_path="%1metadata/Movie/",
39 .m_setting="MovieGrabber",
40 .m_def="metadata/Movie/tmdb3.py" } },
41 { kGrabberTelevision, { .m_path="%1metadata/Television/",
42 .m_setting="TelevisionGrabber",
43 .m_def="metadata/Television/ttvdb4.py" } },
44 { kGrabberGame, { .m_path="%1metadata/Game/",
45 .m_setting="mythgame.MetadataGrabber",
46 .m_def="metadata/Game/giantbomb.py" } },
47 { kGrabberMusic, { .m_path="%1metadata/Music",
48 .m_setting="",
49 .m_def="" } }
50};
51
52static QMap<QString, GrabberType> grabberTypeStrings {
53 { "movie", kGrabberMovie },
54 { "television", kGrabberTelevision },
55 { "game", kGrabberGame },
56 { "music", kGrabberMusic }
57};
58
60{
62}
63
64GrabberList MetaGrabberScript::GetList(const QString &type, bool refresh)
65{
66 QString tmptype = type.toLower();
67 if (!grabberTypeStrings.contains(tmptype))
68 // unknown type, return empty list
69 return {};
70
71 return MetaGrabberScript::GetList(grabberTypeStrings[tmptype], refresh);
72}
73
75 bool refresh)
76{
77 GrabberList tmpGrabberList;
78 GrabberList retGrabberList;
79 {
80 QMutexLocker listLock(&s_grabberLock);
81 QDateTime now = MythDate::current();
82
83 // refresh grabber scripts every 60 seconds
84 // this might have to be revised, or made more intelligent if
85 // the delay during refreshes is too great
86 if (refresh || !s_grabberAge.isValid() ||
87 (s_grabberAge.secsTo(now) > kGrabberRefresh.count()))
88 {
89 s_grabberList.clear();
90 LOG(VB_GENERAL, LOG_DEBUG, LOC + "Clearing grabber cache");
91
92 // loop through different types of grabber scripts and the
93 // directories they are stored in
94 for (const auto& grabberType : std::as_const(grabberTypes))
95 {
96 QString path = (grabberType.m_path).arg(GetShareDir());
97 QDir dir = QDir(path);
98 if (!dir.exists())
99 {
100 LOG(VB_GENERAL, LOG_DEBUG, LOC +
101 QString("No script directory %1").arg(path));
102 continue;
103 }
104 QStringList scripts = dir.entryList(QDir::Executable | QDir::Files);
105 LOG(VB_GENERAL, LOG_DEBUG, LOC +
106 QString("Found %1 scripts in %2").arg(scripts.count()).arg(path));
107 if (scripts.count() == 0)
108 // no scripts found
109 continue;
110
111 // loop through discovered scripts
112 for (const auto& name : std::as_const(scripts))
113 {
114 QString cmd = QDir(path).filePath(name);
115 MetaGrabberScript script(cmd);
116
117 if (script.IsValid())
118 {
119 LOG(VB_GENERAL, LOG_DEBUG, LOC + "Adding " + script.m_command);
120 s_grabberList.append(script);
121 }
122 else
123 {
124 LOG(VB_GENERAL, LOG_DEBUG, LOC + "Failed " + name);
125 }
126 }
127 }
128
129 s_grabberAge = now;
130 }
131
132 tmpGrabberList = s_grabberList;
133 }
134
135 for (const auto& item : std::as_const(tmpGrabberList))
136 {
137 if ((type == kGrabberAll) || (item.GetType() == type))
138 retGrabberList.append(item);
139 }
140
141 return retGrabberList;
142}
143
145 const MetadataLookup *lookup)
146{
147 if (lookup &&
148 !lookup->GetInetref().isEmpty() &&
149 lookup->GetInetref() != "00000000")
150 {
151 // inetref is defined, see if we have a pre-defined grabber
152 MetaGrabberScript grabber = FromInetref(lookup->GetInetref());
153
154 if (grabber.IsValid())
155 {
156 return grabber;
157 }
158 // matching grabber was not found, just use the default
159 // fall through
160 }
161
162 auto grabber = GetType(defaultType);
163 if (!grabber.m_valid)
164 {
165 QString name = grabberTypes[defaultType].m_setting;
166 if (name.isEmpty())
167 name = QString("Type %1").arg(defaultType);
168 LOG(VB_GENERAL, LOG_INFO,
169 QString("Grabber '%1' is not configured. Do you need to set PYTHONPATH?").arg(name));
170 }
171 return grabber;
172}
173
175{
176 QString tmptype = type.toLower();
177 if (!grabberTypeStrings.contains(tmptype))
178 // unknown type, return empty grabber
179 return {};
180
182}
183
185{
186 QString cmd = gCoreContext->GetSetting(grabberTypes[type].m_setting,
187 grabberTypes[type].m_def);
188
189 if (cmd.isEmpty())
190 {
191 // should the python bindings had not been installed at any stage
192 // the settings could have been set to an empty string, so use default
193 cmd = grabberTypes[type].m_def;
194 }
195
196 // just pull it from the cache
197 GrabberList list = GetList(type);
198 for (const auto& item : std::as_const(list))
199 if (item.GetPath().endsWith(cmd))
200 return item;
201
202 // polling the cache will cause a refresh, so lets just grab and
203 // process the script directly
204 QString fullcmd = QString("%1%2").arg(GetShareDir(), cmd);
205 MetaGrabberScript script(fullcmd);
206
207 if (script.IsValid())
208 {
209 return script;
210 }
211
212 return {};
213}
214
216 bool absolute)
217{
218 GrabberList list = GetList();
219
220 // search for direct match on tag
221 for (const auto& item : std::as_const(list))
222 {
223 if (item.GetCommand() == tag)
224 {
225 return item;
226 }
227 }
228
229 // no direct match. do we require a direct match? search for one that works
230 if (!absolute)
231 {
232 for (const auto& item : std::as_const(list))
233 {
234 if (item.Accepts(tag))
235 {
236 return item;
237 }
238 }
239 }
240
241 // no working match. return a blank
242 return {};
243}
244
246 bool absolute)
247{
248 static QMutex s_reLock;
249 QMutexLocker lock(&s_reLock);
250 QString tag;
251 auto match = kRetagRef.match(inetref);
252 if (match.hasMatch())
253 tag = match.captured(1);
254 if (!tag.isEmpty())
255 {
256 // match found, pull out the grabber
257 MetaGrabberScript script = MetaGrabberScript::FromTag(tag, absolute);
258 if (script.IsValid())
259 return script;
260 }
261
262 // no working match, return a blank
263 return {};
264}
265
266QString MetaGrabberScript::CleanedInetref(const QString &inetref)
267{
268 static QMutex s_reLock;
269 QMutexLocker lock(&s_reLock);
270
271 // try to strip grabber tag from inetref
272 auto match = kRetagRef.match(inetref);
273 if (match.hasMatch())
274 return match.captured(2);
275 return inetref;
276}
277
278MetaGrabberScript::MetaGrabberScript(QString path, const QDomElement &dom) :
279 m_fullcommand(std::move(path))
280{
282}
283
285{
287}
288
290{
291 if (path.isEmpty())
292 return;
293 m_fullcommand = path;
294 if (path[0] != '/')
295 m_fullcommand.prepend(QString("%1metadata").arg(GetShareDir()));
296
297 MythSystemLegacy grabber(path, QStringList() << "-v",
299 grabber.Run();
300 if (grabber.Wait() != GENERIC_EXIT_OK)
301 // script failed
302 return;
303
304 QByteArray result = grabber.ReadAll();
305 if (result.isEmpty())
306 // no output
307 return;
308
309 QDomDocument doc;
310#if QT_VERSION < QT_VERSION_CHECK(6,5,0)
311 doc.setContent(result, true);
312#else
313 doc.setContent(result, QDomDocument::ParseOption::UseNamespaceProcessing);
314#endif
315 QDomElement root = doc.documentElement();
316 if (root.isNull())
317 // no valid XML
318 return;
319
321 if (m_name.isEmpty())
322 // XML not processed correctly
323 return;
324
325 m_valid = true;
326}
327
329{
330 if (this != &other)
331 {
332 m_name = other.m_name;
333 m_author = other.m_author;
334 m_thumbnail = other.m_thumbnail;
335 m_command = other.m_command;
337 m_type = other.m_type;
340 m_accepts = other.m_accepts;
341 m_version = other.m_version;
342 m_valid = other.m_valid;
343 }
344
345 return *this;
346}
347
348
349void MetaGrabberScript::ParseGrabberVersion(const QDomElement &item)
350{
351 m_name = item.firstChildElement("name").text();
352 m_author = item.firstChildElement("author").text();
353 m_thumbnail = item.firstChildElement("thumbnail").text();
354 m_command = item.firstChildElement("command").text();
355 m_description = item.firstChildElement("description").text();
356 m_version = item.firstChildElement("version").text().toFloat();
357 m_typestring = item.firstChildElement("type").text().toLower();
358
359 if (!m_typestring.isEmpty() && grabberTypeStrings.contains(m_typestring))
361 else
363
364 QDomElement accepts = item.firstChildElement("accepts");
365 if (!accepts.isNull())
366 {
367 while (!accepts.isNull())
368 {
369 m_accepts.append(accepts.text());
370 accepts = accepts.nextSiblingElement("accepts");
371 }
372 }
373}
374
376{
377 if (!m_valid || m_fullcommand.isEmpty())
378 return false;
379
380 QStringList args; args << "-t";
382
383 grabber.Run();
384 return grabber.Wait() == GENERIC_EXIT_OK;
385}
386
387// TODO
388// using the MetadataLookup object as both argument input, and parsed output,
389// is clumsy. break the inputs out into a separate object, and spawn a new
390// MetadataLookup object in ParseMetadataItem, rather than requiring an
391// existing one to reuse.
393 MetadataLookup *lookup, bool passseas)
394{
397
398 LOG(VB_GENERAL, LOG_INFO, QString("Running Grabber: %1 %2")
399 .arg(m_fullcommand, args.join(" ")));
400
401 grabber.Run();
402 if (grabber.Wait(180s) != GENERIC_EXIT_OK)
403 return list;
404
405 QByteArray result = grabber.ReadAll();
406 if (!result.isEmpty())
407 {
408 QDomDocument doc;
409#if QT_VERSION < QT_VERSION_CHECK(6,5,0)
410 doc.setContent(result, true);
411#else
412 doc.setContent(result, QDomDocument::ParseOption::UseNamespaceProcessing);
413#endif
414 QDomElement root = doc.documentElement();
415 QDomElement item = root.firstChildElement("item");
416
417 while (!item.isNull())
418 {
419 MetadataLookup *tmp = ParseMetadataItem(item, lookup, passseas);
420 tmp->SetInetref(QString("%1_%2").arg(m_command,tmp->GetInetref()));
421 if (!tmp->GetCollectionref().isEmpty())
422 {
423 tmp->SetCollectionref(QString("%1_%2")
424 .arg(m_command, tmp->GetCollectionref()));
425 }
426 list.append(tmp);
427 // MetadataLookup is to be owned by the list
428 tmp->DecrRef();
429 item = item.nextSiblingElement("item");
430 }
431 }
432 return list;
433}
434
436{
437 QString share = GetShareDir();
438 if (m_fullcommand.startsWith(share))
439 return m_fullcommand.right(m_fullcommand.size() - share.size());
440
441 return {};
442}
443
444void MetaGrabberScript::toMap(InfoMap &metadataMap) const
445{
446 metadataMap["name"] = m_name;
447 metadataMap["author"] = m_author;
448 metadataMap["thumbnailfilename"] = m_thumbnail;
449 metadataMap["command"] = m_command;
450 metadataMap["description"] = m_description;
451 metadataMap["version"] = QString::number(m_version);
452 metadataMap["type"] = m_typestring;
453}
454
456{
457 args << "-l"
459 << "-a"
461}
462
464 MetadataLookup *lookup, bool passseas)
465{
466 QStringList args;
468
469 args << "-M"
470 << title;
471
472 return RunGrabber(args, lookup, passseas);
473}
474
476 const QString &subtitle, MetadataLookup *lookup,
477 bool passseas)
478{
479 QStringList args;
481
482 args << "-N"
483 << title
484 << subtitle;
485
486 return RunGrabber(args, lookup, passseas);
487}
488
490 [[maybe_unused]] const QString &title,
491 const QString &subtitle,
492 MetadataLookup *lookup, bool passseas)
493{
494 QStringList args;
496
497 args << "-N"
498 << CleanedInetref(inetref)
499 << subtitle;
500
501 return RunGrabber(args, lookup, passseas);
502}
503
505 MetadataLookup *lookup, bool passseas)
506{
507 QStringList args;
509
510 args << "-D"
511 << CleanedInetref(inetref);
512
513 return RunGrabber(args, lookup, passseas);
514}
515
517 int season, int episode, MetadataLookup *lookup,
518 bool passseas)
519{
520 QStringList args;
522
523 args << "-D"
524 << CleanedInetref(inetref)
525 << QString::number(season)
526 << QString::number(episode);
527
528 return RunGrabber(args, lookup, passseas);
529}
530
532 const QString &collectionref, MetadataLookup *lookup,
533 bool passseas)
534{
535 QStringList args;
537
538 args << "-C"
539 << CleanedInetref(collectionref);
540
541 return RunGrabber(args, lookup, passseas);
542}
MetadataLookupList LookupData(const QString &inetref, MetadataLookup *lookup, bool passseas=true)
static MetaGrabberScript GetGrabber(GrabberType defaultType, const MetadataLookup *lookup=nullptr)
bool IsValid(void) const
MetadataLookupList RunGrabber(const QStringList &args, MetadataLookup *lookup, bool passseas)
GrabberType GetType(void) const
QStringList m_accepts
static MetaGrabberScript FromInetref(const QString &inetref, bool absolute=false)
MetadataLookupList SearchSubtitle(const QString &title, const QString &subtitle, MetadataLookup *lookup, bool passseas=true)
MetaGrabberScript()=default
MetadataLookupList LookupCollection(const QString &collectionref, MetadataLookup *lookup, bool passseas=true)
MetaGrabberScript & operator=(const MetaGrabberScript &other)
QString GetRelPath(void) const
void ParseGrabberVersion(const QDomElement &item)
MetadataLookupList Search(const QString &title, MetadataLookup *lookup, bool passseas=true)
static GrabberList GetList(bool refresh=false)
static MetaGrabberScript FromTag(const QString &tag, bool absolute=false)
static QString CleanedInetref(const QString &inetref)
void toMap(InfoMap &metadataMap) const
GrabberType m_type
static void SetDefaultArgs(QStringList &args)
QString GetCollectionref() const
void SetInetref(const QString &inetref)
QString GetInetref() const
void SetCollectionref(const QString &collectionref)
QString GetSetting(const QString &key, const QString &defaultval="")
QString GetLanguage(void)
Returns two character ISO-639 language descriptor for UI language.
MythLocale * GetLocale(void) const
QString GetCountryCode() const
Definition: mythlocale.cpp:59
uint Wait(std::chrono::seconds timeout=0s)
void Run(std::chrono::seconds timeout=0s)
Runs a command inside the /bin/sh shell. Returns immediately.
QByteArray & ReadAll()
virtual int DecrRef(void)
Decrements reference count and deletes on 0.
@ GENERIC_EXIT_OK
Exited with no error.
Definition: exitcodes.h:13
MetadataLookup * ParseMetadataItem(const QDomElement &item, MetadataLookup *lookup, bool passseas)
static QMutex s_grabberLock
#define LOC
static GrabberList s_grabberList
static const QRegularExpression kRetagRef
static QDateTime s_grabberAge
static QMap< QString, GrabberType > grabberTypeStrings
static constexpr std::chrono::seconds kGrabberRefresh
static const QMap< GrabberType, GrabberOpts > grabberTypes
GrabberType
@ kGrabberMusic
@ kGrabberAll
@ kGrabberMovie
@ kGrabberInvalid
@ kGrabberTelevision
@ kGrabberGame
QList< MetaGrabberScript > GrabberList
MythCoreContext * gCoreContext
This global variable contains the MythCoreContext instance for the app.
QString GetShareDir(void)
Definition: mythdirs.cpp:280
#define LOG(_MASK_, _LEVEL_, _QSTRING_)
Definition: mythlogging.h:39
@ kMSStdOut
allow access to stdout
Definition: mythsystem.h:41
@ kMSRunShell
run process through shell
Definition: mythsystem.h:43
static QMutex listLock
QHash< QString, QString > InfoMap
Definition: mythtypes.h:15
QDateTime current(bool stripped)
Returns current Date and Time in UTC.
Definition: mythdate.cpp:15