MythTV master
videoscan.cpp
Go to the documentation of this file.
1
2#include "videoscan.h"
3
4#include <QApplication>
5#include <QImageReader>
6#include <QUrl>
7#include <utility>
8
9// mythtv
18
19// libmythmetadata
21#include "videoutils.h"
22#include "globals.h"
23#include "dbaccess.h"
24#include "dirscan.h"
25
26const QEvent::Type VideoScanChanges::kEventType =
27 (QEvent::Type) QEvent::registerEventType();
28
29namespace
30{
31 template <typename DirListType>
33 {
34 public:
35 dirhandler(DirListType &video_files,
36 const QStringList &image_extensions) :
37 m_videoFiles(video_files)
38 {
39 for (const auto& ext : std::as_const(image_extensions))
40 m_imageExt.insert(ext.toLower());
41 }
42
43 DirectoryHandler *newDir([[maybe_unused]] const QString &dir_name,
44 [[maybe_unused]] const QString &fq_dir_name) override // DirectoryHandler
45 {
46 return this;
47 }
48
49 void handleFile([[maybe_unused]] const QString &file_name,
50 const QString &fq_file_name,
51 const QString &extension,
52 const QString &host) override // DirectoryHandler
53
54 {
55#if 0
56 LOG(VB_GENERAL, LOG_DEBUG,
57 QString("handleFile: %1 :: %2").arg(fq_file_name).arg(host));
58#endif
59 if (m_imageExt.find(extension.toLower()) == m_imageExt.end())
60 {
61 m_videoFiles[fq_file_name].check = false;
62 m_videoFiles[fq_file_name].host = host;
63 }
64 }
65
66 private:
67 using image_ext = std::set<QString>;
69 DirListType &m_videoFiles;
70 };
71}
72
75
77 MThread("VideoScanner"),
78 m_parent(parent),
79 m_hasGUI(gCoreContext->HasGUI()),
80 m_dbMetadata(new VideoMetadataListManager)
81{
82 m_listUnknown = gCoreContext->GetBoolSetting("VideoListUnknownFiletypes", false);
83}
84
86{
87 delete m_dbMetadata;
88}
89
90void VideoScannerThread::SetHosts(const QStringList &hosts)
91{
92 m_liveSGHosts.clear();
93 for (const auto& host : std::as_const(hosts))
94 m_liveSGHosts << host.toLower();
95}
96
97void VideoScannerThread::SetDirs(QStringList dirs)
98{
99 QString master = gCoreContext->GetMasterHostName().toLower();
100 QStringList searchhosts;
101 QStringList mdirs;
102 m_offlineSGHosts.clear();
103
104 QStringList::iterator iter = dirs.begin();
105 QStringList::iterator iter2;
106 while ( iter != dirs.end() )
107 {
108 if (iter->startsWith("myth://"))
109 {
110 QUrl sgurl = *iter;
111 QString host = sgurl.host().toLower();
112 QString path = sgurl.path();
113
114 if (!m_liveSGHosts.contains(host))
115 {
116 // mark host as offline to warn user
117 if (!m_offlineSGHosts.contains(host))
118 m_offlineSGHosts.append(host);
119 // erase from directory list to skip scanning
120 iter = dirs.erase(iter);
121 continue;
122 }
123 if ((host == master) && (!mdirs.contains(path)))
124 {
125 // collect paths defined on master backend so other
126 // online backends can be set to fall through to them
127 mdirs.append(path);
128 }
129 else if (!searchhosts.contains(host))
130 {
131 // mark host as having directories defined so it
132 // does not fall through to those on the master
133 searchhosts.append(host);
134 }
135 }
136
137 ++iter;
138 }
139
140 for (iter = m_liveSGHosts.begin(); iter != m_liveSGHosts.end(); ++iter)
141 {
142 if ((!searchhosts.contains(*iter)) && (master != *iter))
143 {
144 for (iter2 = mdirs.begin(); iter2 != mdirs.end(); ++iter2)
145 {
146 // backend is online, but has no directories listed
147 // fall back to those on the master backend
148 dirs.append(MythCoreContext::GenMythURL(*iter,
149 0, *iter2, "Videos"));
150 }
151 }
152 }
153
154 m_directories = dirs;
155}
156
158{
159 RunProlog();
160
164
165 QList<QByteArray> image_types = QImageReader::supportedImageFormats();
166 QStringList imageExtensions;
167 for (const auto & format : std::as_const(image_types))
168 imageExtensions.push_back(QString(format));
169
170 LOG(VB_GENERAL, LOG_INFO, QString("Beginning Video Scan."));
171
172 uint counter = 0;
173 FileCheckList fs_files;
174
175 if (m_hasGUI)
176 SendProgressEvent(counter, (uint)m_directories.size(),
177 tr("Searching for video files"));
178 for (const auto & dir : std::as_const(m_directories))
179 {
180 if (!buildFileList(dir, imageExtensions, fs_files))
181 {
182 if (dir.startsWith("myth://"))
183 {
184 QUrl sgurl = dir;
185 QString host = sgurl.host().toLower();
186
187 m_liveSGHosts.removeAll(host);
188
189 LOG(VB_GENERAL, LOG_ERR,
190 QString("Failed to scan :%1:").arg(dir));
191 }
192 }
193 if (m_hasGUI)
194 SendProgressEvent(++counter);
195 }
196
197 PurgeList db_remove;
198 verifyFiles(fs_files, db_remove);
199 m_dbDataChanged = updateDB(fs_files, db_remove);
200
201 if (m_dbDataChanged)
202 {
203 QCoreApplication::postEvent(m_parent,
205 m_delList));
206
207 QStringList slist;
208
209 for (int id : std::as_const(m_addList))
210 slist << QString("added::%1").arg(id);
211 for (int id : std::as_const(m_movList))
212 slist << QString("moved::%1").arg(id);
213 for (int id : std::as_const(m_delList))
214 slist << QString("deleted::%1").arg(id);
215
216 MythEvent me("VIDEO_LIST_CHANGE", slist);
217
219 }
220 else
221 {
222 gCoreContext->SendMessage("VIDEO_LIST_NO_CHANGE");
223 }
224
225 RunEpilog();
226}
227
228
230 [[maybe_unused]] const QString &filename)
231{
232 // TODO: use single DB connection for all calls
233 if (m_removeAll)
235
236 if (!m_keepAll && !m_removeAll)
237 {
238 m_removeAll = true;
240 }
241}
242
244 PurgeList &remove)
245{
246 int counter = 0;
247 FileCheckList::iterator iter;
248
249 if (m_hasGUI)
250 SendProgressEvent(counter, (uint)m_dbMetadata->getList().size(),
251 tr("Verifying video files"));
252
253 // For every file we know about, check to see if it still exists.
254 for (const auto & file : m_dbMetadata->getList())
255 {
256 QString lname = file->GetFilename();
257 QString lhost = file->GetHost().toLower();
258 if (!lname.isEmpty())
259 {
260 iter = files.find(lname);
261 if (iter != files.end())
262 {
263 if (lhost != iter->second.host)
264 {
265 // file has changed hosts
266 // add to delete list for further processing
267 remove.emplace_back(file->GetID(), lname);
268 }
269 else
270 {
271 // file is on disk on the proper host and in the database
272 // we're done with it
273 iter->second.check = true;
274 }
275 }
276 else if (lhost.isEmpty())
277 {
278 // If it's only in the database, and not on a host we
279 // cannot reach, mark it as for removal later.
280 remove.emplace_back(file->GetID(), lname);
281 }
282 else if (m_liveSGHosts.contains(lhost))
283 {
284 LOG(VB_GENERAL, LOG_INFO,
285 QString("Removing file SG(%1) :%2:")
286 .arg(lhost, lname));
287 remove.emplace_back(file->GetID(), lname);
288 }
289 else
290 {
291 LOG(VB_GENERAL, LOG_WARNING,
292 QString("SG(%1) not available. Not removing file :%2:")
293 .arg(lhost, lname));
294 if (!m_offlineSGHosts.contains(lhost))
295 m_offlineSGHosts.append(lhost);
296 }
297 }
298 if (m_hasGUI)
299 SendProgressEvent(++counter);
300 }
301}
302
304{
305 int ret = 0;
306 uint counter = 0;
307 if (m_hasGUI)
308 SendProgressEvent(counter, (uint)(add.size() + remove.size()),
309 tr("Updating video database"));
310
311 for (auto p = add.cbegin(); p != add.cend(); ++p)
312 {
313 // add files not already in the DB
314 if (!p->second.check)
315 {
316 int id = -1;
317
318 // Are we sure this needs adding? Let's check our Hash list.
319 QString hash = VideoMetadata::VideoFileHash(p->first, p->second.host);
320 if (hash != "NULL" && !hash.isEmpty())
321 {
322 id = VideoMetadata::UpdateHashedDBRecord(hash, p->first, p->second.host);
323 if (id != -1)
324 {
325 // Whew, that was close. Let's remove that thing from
326 // our purge list, too.
327 LOG(VB_GENERAL, LOG_ERR,
328 QString("Hash %1 already exists in the "
329 "database, updating record %2 "
330 "with new filename %3")
331 .arg(hash).arg(id).arg(p->first));
332 m_movList.append(id);
333 }
334 }
335 if (id == -1)
336 {
337 VideoMetadata newFile(
338 p->first, QString(), hash,
344 QString(), QString(), QString(), QString(),
345 QString(),
347 QDate::fromString("0000-00-00","YYYY-MM-DD"),
348 VIDEO_INETREF_DEFAULT, 0, QString(),
350 0.0, VIDEO_RATING_DEFAULT, 0, 0,
351 0, 0,
352 MythDate::current().date(),
354
355 LOG(VB_GENERAL, LOG_INFO, QString("Adding : %1 : %2 : %3")
356 .arg(newFile.GetHost(), newFile.GetFilename(), hash));
357 newFile.SetHost(p->second.host);
358 newFile.SaveToDatabase();
359 m_addList << newFile.GetID();
360 }
361 ret += 1;
362 }
363 if (m_hasGUI)
364 SendProgressEvent(++counter);
365 }
366
367 // When prompting is restored, account for the answer here.
368 ret += remove.size();
369 for (const auto & item : remove)
370 {
371 if (!m_movList.contains(item.first))
372 {
373 removeOrphans(item.first, item.second);
374 m_delList << item.first;
375 }
376 if (m_hasGUI)
377 SendProgressEvent(++counter);
378 }
379
380 return ret > 0;
381}
382
383bool VideoScannerThread::buildFileList(const QString &directory,
384 const QStringList &imageExtensions,
385 FileCheckList &filelist) const
386{
387 // TODO: FileCheckList is a std::map, keyed off the filename. In the event
388 // multiple backends have access to shared storage, the potential exists
389 // for files to be scanned onto the wrong host. Add in some logic to prefer
390 // the backend with the content stored in a storage group determined to be
391 // local.
392
393 LOG(VB_GENERAL,LOG_INFO, QString("buildFileList directory = %1")
394 .arg(directory));
397
398 dirhandler<FileCheckList> dh(filelist, imageExtensions);
399 return ScanVideoDirectory(directory, &dh, ext_list, m_listUnknown);
400}
401
403 QString messsage)
404{
405 if (!m_dialog)
406 return;
407
408 auto *pue = new ProgressUpdateEvent(progress, total, std::move(messsage));
409 QApplication::postEvent(m_dialog, pue);
410}
411
413 : m_scanThread(new VideoScannerThread(this))
414{
415}
416
418{
420 delete m_scanThread;
421}
422
423void VideoScanner::doScan(const QStringList &dirs)
424{
425 if (m_scanThread->isRunning())
426 return;
427
428 if (gCoreContext->HasGUI())
429 {
430 MythScreenStack *popupStack = GetMythMainWindow()->GetStack("popup stack");
431
432 auto *progressDlg = new MythUIProgressDialog("", popupStack,
433 "videoscanprogressdialog");
434
435 if (progressDlg->Create())
436 {
437 popupStack->AddScreen(progressDlg, false);
438 connect(m_scanThread->qthread(), &QThread::finished,
439 progressDlg, &MythScreenType::Close);
440 connect(m_scanThread->qthread(), &QThread::finished,
442 }
443 else
444 {
445 delete progressDlg;
446 progressDlg = nullptr;
447 }
448 m_scanThread->SetProgressDialog(progressDlg);
449 }
450
451 QStringList hosts;
452 if (!RemoteGetActiveBackends(&hosts))
453 {
454 LOG(VB_GENERAL, LOG_WARNING, "Could not retrieve list of "
455 "available backends.");
456 hosts.clear();
457 }
458 m_scanThread->SetHosts(hosts);
459 m_scanThread->SetDirs(dirs);
461}
462
464{
466}
467
469{
470 QStringList failedHosts = m_scanThread->GetOfflineSGHosts();
471 if (!failedHosts.empty())
472 {
473 QString hosts = failedHosts.join(" ");
474 QString msg = tr("Failed to Scan SG Video Hosts:\n\n%1\n\n"
475 "If they no longer exist please remove them")
476 .arg(hosts);
477
478 ShowOkPopup(msg);
479 }
480
482}
483
487bool RemoteGetActiveBackends(QStringList *list)
488{
489 list->clear();
490 *list << "QUERY_ACTIVE_BACKENDS";
491
493 return false;
494
495 list->removeFirst();
496 return true;
497}
void getExtensionIgnoreList(ext_ignore_list &ext_ignore) const
Definition: dbaccess.cpp:816
std::vector< std::pair< QString, bool > > ext_ignore_list
Definition: dbaccess.h:155
static FileAssociations & getFileAssociation()
Definition: dbaccess.cpp:836
This is a wrapper around QThread that does several additional things.
Definition: mthread.h:49
bool isRunning(void) const
Definition: mthread.cpp:263
void RunProlog(void)
Sets up a thread, call this if you reimplement run().
Definition: mthread.cpp:196
void start(QThread::Priority p=QThread::InheritPriority)
Tell MThread to start running the thread in the near future.
Definition: mthread.cpp:283
void RunEpilog(void)
Cleans up a thread's resources, call this if you reimplement run().
Definition: mthread.cpp:209
bool wait(std::chrono::milliseconds time=std::chrono::milliseconds::max())
Wait for the MThread to exit, with a maximum timeout.
Definition: mthread.cpp:300
QThread * qthread(void)
Returns the thread, this will always return the same pointer no matter how often you restart the thre...
Definition: mthread.cpp:233
void SendMessage(const QString &message)
bool SendReceiveStringList(QStringList &strlist, bool quickTimeout=false, bool block=true)
Send a message to the backend and wait for a response.
static QString GenMythURL(const QString &host=QString(), int port=0, QString path=QString(), const QString &storageGroup=QString())
QString GetMasterHostName(void)
void SendEvent(const MythEvent &event)
bool GetBoolSetting(const QString &key, bool defaultval=false)
bool HasGUI(void) const
This class is used as a container for messages.
Definition: mythevent.h:17
MythScreenStack * GetStack(const QString &Stackname)
virtual void AddScreen(MythScreenType *screen, bool allowFade=true)
virtual void Close()
MythUIProgressDialog(QString message, MythScreenStack *parent, const char *name)
MythUIType * m_parent
Definition: mythuitype.h:297
void setList(metadata_list &list)
std::list< VideoMetadataPtr > metadata_list
bool purgeByID(unsigned int db_id)
static void loadAllFromDatabase(metadata_list &items, const QString &sql="", const QStringList &bindValues=QStringList())
Load videometadata database into memory.
const metadata_list & getList() const
const QString & GetHost() const
unsigned int GetID() const
static QString VideoFileHash(const QString &file_name, const QString &host)
static int UpdateHashedDBRecord(const QString &hash, const QString &file_name, const QString &host)
void SetHost(const QString &host)
const QString & GetFilename() const
static const Type kEventType
Definition: videoscan.h:57
std::vector< std::pair< int, QString > > PurgeList
Definition: videoscan.h:85
QList< int > m_addList
Definition: videoscan.h:112
bool getDataChanged() const
Definition: videoscan.h:73
void SetDirs(QStringList dirs)
Definition: videoscan.cpp:97
bool updateDB(const FileCheckList &add, const PurgeList &remove)
Definition: videoscan.cpp:303
std::map< QString, CheckStruct > FileCheckList
Definition: videoscan.h:86
~VideoScannerThread() override
Definition: videoscan.cpp:85
QObject * m_parent
Definition: videoscan.h:99
void SendProgressEvent(uint progress, uint total=0, QString messsage=QString())
Definition: videoscan.cpp:402
QStringList GetOfflineSGHosts(void)
Definition: videoscan.h:72
QList< int > m_movList
Definition: videoscan.h:113
MythUIProgressDialog * m_dialog
Definition: videoscan.h:110
void SetProgressDialog(MythUIProgressDialog *dialog)
Definition: videoscan.h:71
VideoMetadataListManager * m_dbMetadata
Definition: videoscan.h:109
void SetHosts(const QStringList &hosts)
Definition: videoscan.cpp:90
void run() override
Runs the Qt event loop unless we have a QRunnable, in which case we run the runnable run instead.
Definition: videoscan.cpp:157
void verifyFiles(FileCheckList &files, PurgeList &remove)
Definition: videoscan.cpp:243
QList< int > m_delList
Definition: videoscan.h:114
QStringList m_offlineSGHosts
Definition: videoscan.h:107
VideoScannerThread(QObject *parent)
Definition: videoscan.cpp:76
bool buildFileList(const QString &directory, const QStringList &imageExtensions, FileCheckList &filelist) const
Definition: videoscan.cpp:383
void removeOrphans(unsigned int id, const QString &filename)
Definition: videoscan.cpp:229
QStringList m_liveSGHosts
Definition: videoscan.h:106
QStringList m_directories
Definition: videoscan.h:105
void doScanAll(void)
Definition: videoscan.cpp:463
class VideoScannerThread * m_scanThread
Definition: videoscan.h:40
void finished(bool)
void doScan(const QStringList &dirs)
Definition: videoscan.cpp:423
~VideoScanner() override
Definition: videoscan.cpp:417
void finishedScan()
Definition: videoscan.cpp:468
void handleFile(const QString &file_name, const QString &fq_file_name, const QString &extension, const QString &host) override
Definition: videoscan.cpp:49
DirectoryHandler * newDir(const QString &dir_name, const QString &fq_dir_name) override
Definition: videoscan.cpp:43
dirhandler(DirListType &video_files, const QStringList &image_extensions)
Definition: videoscan.cpp:35
unsigned int uint
Definition: compat.h:68
bool ScanVideoDirectory(const QString &start_path, DirectoryHandler *handler, const FileAssociations::ext_ignore_list &ext_disposition, bool list_unknown_extensions)
Definition: dirscan.cpp:227
const QString VIDEO_INETREF_DEFAULT
Definition: globals.cpp:24
const QString VIDEO_PLOT_DEFAULT
Definition: globals.cpp:32
const QString VIDEO_TRAILER_DEFAULT
Definition: globals.cpp:26
const QString VIDEO_BANNER_DEFAULT
Definition: globals.cpp:28
const QString VIDEO_SCREENSHOT_DEFAULT
Definition: globals.cpp:27
const QString VIDEO_FANART_DEFAULT
Definition: globals.cpp:29
const QString VIDEO_RATING_DEFAULT
Definition: globals.cpp:30
const QString VIDEO_DIRECTOR_DEFAULT
Definition: globals.cpp:23
const QString VIDEO_COVERFILE_DEFAULT
Definition: globals.cpp:25
MythCoreContext * gCoreContext
This global variable contains the MythCoreContext instance for the app.
MythConfirmationDialog * ShowOkPopup(const QString &message, bool showCancel)
Non-blocking version of MythPopupBox::showOkPopup()
#define LOG(_MASK_, _LEVEL_, _QSTRING_)
Definition: mythlogging.h:39
MythMainWindow * GetMythMainWindow(void)
QDateTime fromString(const QString &dtstr)
Converts kFilename && kISODate formats to QDateTime.
Definition: mythdate.cpp:39
QDateTime current(bool stripped)
Returns current Date and Time in UTC.
Definition: mythdate.cpp:15
static constexpr uint16_t VIDEO_YEAR_DEFAULT
Definition: videometadata.h:18
bool RemoteGetActiveBackends(QStringList *list)
return list of backends currently connected to the master
Definition: videoscan.cpp:487
QStringList GetVideoDirs()
Definition: videoutils.cpp:116