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