MythTV  master
mythplugins/mytharchive/mytharchivehelper/main.cpp
Go to the documentation of this file.
1 /* -*- Mode: c++ -*-
2  * vim: set expandtab tabstop=4 shiftwidth=4:
3  *
4  * Original Project
5  * MythTV http://www.mythtv.org
6  *
7  * Copyright (c) 2004, 2005 John Pullan <john@pullan.org>
8  * Copyright (c) 2009, Janne Grunau <janne-mythtv@grunau.be>
9  *
10  * This program is free software; you can redistribute it and/or
11  * modify it under the terms of the GNU General Public License
12  * as published by the Free Software Foundation; either version 2
13  * of the License, or (at your option) any later version.
14  *
15  * This program is distributed in the hope that it will be useful,
16  * but WITHOUT ANY WARRANTY; without even the implied warranty of
17  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18  * GNU General Public License for more details.
19  *
20  * You should have received a copy of the GNU General Public License
21  * along with this program; if not, write to the Free Software
22  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
23  * Or, point your browser to http://www.gnu.org/copyleft/gpl.html
24  *
25  */
26 
27 #include <cstdint>
28 #include <cstdlib>
29 #include <iostream>
30 #include <sys/wait.h> // for WIFEXITED and WEXITSTATUS
31 #include <unistd.h>
32 
33 #include <mythconfig.h>
34 #if CONFIG_DARWIN or defined(__FreeBSD__)
35 #include <sys/param.h>
36 #include <sys/mount.h>
37 #elif __linux__
38 #include <sys/vfs.h>
39 #endif
40 
41 using namespace std;
42 
43 
44 // Qt headers
45 #include <QApplication>
46 #include <QFile>
47 #include <QDir>
48 #include <QDomElement>
49 #include <QImage>
50 #include <QMutex>
51 #include <QMutexLocker>
52 #include <QTextStream>
53 
54 // MythTV headers
55 #include <mythcommandlineparser.h>
56 #include <mythmiscutil.h>
57 #include <mythcoreutil.h>
58 #include <mythcontext.h>
59 #include <mythversion.h>
60 #include <exitcodes.h>
61 #include <mythdb.h>
62 #include <programinfo.h>
63 #include <mythdirs.h>
64 #include <mythconfig.h>
65 #include <mythsystemlegacy.h>
66 #include <mythdate.h>
67 #include <mythlogging.h>
68 #include <mythavutil.h>
69 
70 extern "C" {
71 #include <libavcodec/avcodec.h>
72 #include <libavformat/avformat.h>
73 #include "external/pxsup2dast.h"
74 #include "libavutil/imgutils.h"
75 }
76 
77 // mytharchive headers
78 #include "../mytharchive/archiveutil.h"
79 #include "../mytharchive/remoteavformatcontext.h"
80 
82 {
83  public:
84  NativeArchive(void);
85  ~NativeArchive(void);
86 
87  int doNativeArchive(const QString &jobFile);
88  int doImportArchive(const QString &xmlFile, int chanID);
89  bool copyFile(const QString &source, const QString &destination);
90  int importRecording(const QDomElement &itemNode,
91  const QString &xmlFile, int chanID);
92  int importVideo(const QDomElement &itemNode, const QString &xmlFile);
93  int exportRecording(QDomElement &itemNode, const QString &saveDirectory);
94  int exportVideo(QDomElement &itemNode, const QString &saveDirectory);
95  private:
96  QString findNodeText(const QDomElement &elem, const QString &nodeName);
97  int getFieldList(QStringList &fieldList, const QString &tableName);
98 };
99 
101 {
102  // create the lock file so the UI knows we're running
103  QString tempDir = getTempDirectory();
104  QFile file(tempDir + "/logs/mythburn.lck");
105 
106  if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate))
107  LOG(VB_GENERAL, LOG_ERR, "NativeArchive: Failed to create lock file");
108 
109  QString pid = QString("%1").arg(getpid());
110  file.write(pid.toLatin1());
111  file.close();
112 }
113 
115 {
116  // remove lock file
117  QString tempDir = getTempDirectory();
118  if (QFile::exists(tempDir + "/logs/mythburn.lck"))
119  QFile::remove(tempDir + "/logs/mythburn.lck");
120 }
121 
122 bool NativeArchive::copyFile(const QString &source, const QString &destination)
123 {
124  QString command = QString("mythutil --copyfile --infile '%1' --outfile '%2'")
125  .arg(source).arg(destination);
126  uint res = myth_system(command);
127  if (res != GENERIC_EXIT_OK)
128  {
129  LOG(VB_JOBQUEUE, LOG_ERR,
130  QString("Failed while running %1. Result: %2").arg(command).arg(res));
131  return false;
132  }
133 
134  return true;
135 }
136 
137 static bool createISOImage(QString &sourceDirectory)
138 {
139  LOG(VB_JOBQUEUE, LOG_INFO, "Creating ISO image");
140 
141  QString tempDirectory = getTempDirectory();
142 
143  tempDirectory += "work/";
144 
145  QString mkisofs = gCoreContext->GetSetting("MythArchiveMkisofsCmd", "mkisofs");
146  QString command = mkisofs + " -R -J -V 'MythTV Archive' -o ";
147  command += tempDirectory + "mythburn.iso " + sourceDirectory;
148 
149  uint res = myth_system(command);
150  if (res != GENERIC_EXIT_OK)
151  {
152  LOG(VB_JOBQUEUE, LOG_ERR,
153  QString("Failed while running mkisofs. Result: %1") .arg(res));
154  return false;
155  }
156 
157  LOG(VB_JOBQUEUE, LOG_INFO, "Finished creating ISO image");
158  return true;
159 }
160 
161 static int burnISOImage(int mediaType, bool bEraseDVDRW, bool nativeFormat)
162 {
163  QString dvdDrive = gCoreContext->GetSetting("MythArchiveDVDLocation",
164  "/dev/dvd");
165  LOG(VB_JOBQUEUE, LOG_INFO, "Burning ISO image to " + dvdDrive);
166 
167  int driveSpeed = gCoreContext->GetNumSetting("MythArchiveDriveSpeed");
168  QString tempDirectory = getTempDirectory();
169 
170  tempDirectory += "work/";
171 
172  QString command = gCoreContext->GetSetting("MythArchiveGrowisofsCmd",
173  "growisofs");
174 
175  if (driveSpeed)
176  command += " -speed=" + QString::number(driveSpeed);
177 
178  if (nativeFormat)
179  {
180  if (mediaType == AD_DVD_RW && bEraseDVDRW == true)
181  {
182  command += " -use-the-force-luke -Z " + dvdDrive;
183  command += " -V 'MythTV Archive' -R -J " + tempDirectory;
184  }
185  else
186  {
187  command += " -Z " + dvdDrive;
188  command += " -V 'MythTV Archive' -R -J " + tempDirectory;
189  }
190  }
191  else
192  {
193  if (mediaType == AD_DVD_RW && bEraseDVDRW == true)
194  {
195  command += " -dvd-compat -use-the-force-luke -Z " + dvdDrive;
196  command += " -dvd-video -V 'MythTV DVD' " + tempDirectory + "/dvd";
197  }
198  else
199  {
200  command += " -dvd-compat -Z " + dvdDrive;
201  command += " -dvd-video -V 'MythTV DVD' " + tempDirectory + "/dvd";
202  }
203  }
204 
205  uint res = myth_system(command);
206  if (res != GENERIC_EXIT_OK)
207  LOG(VB_JOBQUEUE, LOG_ERR,
208  QString("Failed while running growisofs. Result: %1") .arg(res));
209  else
210  LOG(VB_JOBQUEUE, LOG_INFO, "Finished burning ISO image");
211 
212  return res;
213 }
214 
215 static int doBurnDVD(int mediaType, bool bEraseDVDRW, bool nativeFormat)
216 {
218  "MythArchiveLastRunStart",
220  gCoreContext->SaveSetting("MythArchiveLastRunStatus", "Running");
221 
222  int res = burnISOImage(mediaType, bEraseDVDRW, nativeFormat);
223 
225  "MythArchiveLastRunEnd",
227  gCoreContext->SaveSetting("MythArchiveLastRunStatus", "Success");
228  return res;
229 }
230 
231 int NativeArchive::doNativeArchive(const QString &jobFile)
232 {
233  QString tempDir = getTempDirectory();
234 
235  QDomDocument doc("archivejob");
236  QFile file(jobFile);
237  if (!file.open(QIODevice::ReadOnly))
238  {
239  LOG(VB_JOBQUEUE, LOG_ERR, "Could not open job file: " + jobFile);
240  return 1;
241  }
242 
243  if (!doc.setContent(&file))
244  {
245  LOG(VB_JOBQUEUE, LOG_ERR, "Could not load job file: " + jobFile);
246  file.close();
247  return 1;
248  }
249 
250  file.close();
251 
252  // get options from job file
253  bool bCreateISO = false;
254  bool bEraseDVDRW = false;
255  bool bDoBurn = false;
256  QString saveDirectory;
257  int mediaType = 0;
258 
259  QDomNodeList nodeList = doc.elementsByTagName("options");
260  if (nodeList.count() == 1)
261  {
262  QDomNode node = nodeList.item(0);
263  QDomElement options = node.toElement();
264  if (!options.isNull())
265  {
266  bCreateISO = (options.attribute("createiso", "0") == "1");
267  bEraseDVDRW = (options.attribute("erasedvdrw", "0") == "1");
268  bDoBurn = (options.attribute("doburn", "0") == "1");
269  mediaType = options.attribute("mediatype", "0").toInt();
270  saveDirectory = options.attribute("savedirectory", "");
271  if (!saveDirectory.endsWith("/"))
272  saveDirectory += "/";
273  }
274  }
275  else
276  {
277  LOG(VB_JOBQUEUE, LOG_ERR,
278  QString("Found %1 options nodes - should be 1")
279  .arg(nodeList.count()));
280  return 1;
281  }
282  LOG(VB_JOBQUEUE, LOG_INFO,
283  QString("Options - createiso: %1,"
284  " doburn: %2, mediatype: %3, erasedvdrw: %4")
285  .arg(bCreateISO).arg(bDoBurn).arg(mediaType).arg(bEraseDVDRW));
286  LOG(VB_JOBQUEUE, LOG_INFO, QString("savedirectory: %1").arg(saveDirectory));
287 
288  // figure out where to save files
289  if (mediaType != AD_FILE)
290  {
291  saveDirectory = tempDir;
292  if (!saveDirectory.endsWith("/"))
293  saveDirectory += "/";
294 
295  saveDirectory += "work/";
296 
297  QDir dir(saveDirectory);
298  if (dir.exists())
299  {
300  if (!MythRemoveDirectory(dir))
301  LOG(VB_GENERAL, LOG_ERR,
302  "NativeArchive: Failed to clear work directory");
303  }
304  dir.mkpath(saveDirectory);
305  }
306 
307  LOG(VB_JOBQUEUE, LOG_INFO,
308  QString("Saving files to : %1").arg(saveDirectory));
309 
310  // get list of file nodes from the job file
311  nodeList = doc.elementsByTagName("file");
312  if (nodeList.count() < 1)
313  {
314  LOG(VB_JOBQUEUE, LOG_ERR, "Cannot find any file nodes?");
315  return 1;
316  }
317 
318  // loop though file nodes and archive each file
319  QDomNode node;
320  QDomElement elem;
321  QString type = "";
322 
323  for (int x = 0; x < nodeList.count(); x++)
324  {
325  node = nodeList.item(x);
326  elem = node.toElement();
327  if (!elem.isNull())
328  {
329  type = elem.attribute("type");
330 
331  if (type.toLower() == "recording")
332  exportRecording(elem, saveDirectory);
333  else if (type.toLower() == "video")
334  exportVideo(elem, saveDirectory);
335  else
336  {
337  LOG(VB_JOBQUEUE, LOG_ERR,
338  QString("Don't know how to archive items of type '%1'")
339  .arg(type.toLower()));
340  continue;
341  }
342  }
343  }
344 
345  // burn the dvd if needed
346  if (mediaType != AD_FILE && bDoBurn)
347  {
348  if (!burnISOImage(mediaType, bEraseDVDRW, true))
349  {
350  LOG(VB_JOBQUEUE, LOG_ERR,
351  "Native archive job failed to completed");
352  return 1;
353  }
354  }
355 
356  // create an iso image if needed
357  if (bCreateISO)
358  {
359  if (!createISOImage(saveDirectory))
360  {
361  LOG(VB_JOBQUEUE, LOG_ERR, "Native archive job failed to completed");
362  return 1;
363  }
364  }
365 
366  LOG(VB_JOBQUEUE, LOG_INFO, "Native archive job completed OK");
367 
368  return 0;
369 }
370 
371 static QRegExp badChars = QRegExp("(/|\\\\|:|\'|\"|\\?|\\|)");
372 
373 static QString fixFilename(const QString &filename)
374 {
375  QString ret = filename;
376  ret.replace(badChars, "_");
377  return ret;
378 }
379 
380 int NativeArchive::getFieldList(QStringList &fieldList, const QString &tableName)
381 {
382  fieldList.clear();
383 
384  MSqlQuery query(MSqlQuery::InitCon());
385  if (query.exec("DESCRIBE " + tableName))
386  {
387  while (query.next())
388  {
389  fieldList.append(query.value(0).toString());
390  }
391  }
392  else
393  MythDB::DBError("describe table", query);
394 
395  return fieldList.count();
396 }
397 
398 int NativeArchive::exportRecording(QDomElement &itemNode,
399  const QString &saveDirectory)
400 {
401  QString chanID, startTime;
402  QString dbVersion = gCoreContext->GetSetting("DBSchemaVer", "");
403 
404  QString title = fixFilename(itemNode.attribute("title"));
405  QString filename = itemNode.attribute("filename");
406  bool doDelete = (itemNode.attribute("delete", "0") == "0");
407  LOG(VB_JOBQUEUE, LOG_INFO, QString("Archiving %1 (%2), do delete: %3")
408  .arg(title).arg(filename).arg(doDelete));
409 
410  if (title == "" || filename == "")
411  {
412  LOG(VB_JOBQUEUE, LOG_ERR, "Bad title or filename");
413  return 0;
414  }
415 
416  if (!extractDetailsFromFilename(filename, chanID, startTime))
417  {
418  LOG(VB_JOBQUEUE, LOG_ERR,
419  QString("Failed to extract chanID and startTime from '%1'")
420  .arg(filename));
421  return 0;
422  }
423 
424  // create the directory to hold this items files
425  QDir dir(saveDirectory + title);
426  if (!dir.exists())
427  dir.mkpath(saveDirectory + title);
428  if (!dir.exists())
429  LOG(VB_GENERAL, LOG_ERR, "Failed to create savedir: " + ENO);
430 
431  LOG(VB_JOBQUEUE, LOG_INFO, "Creating xml file for " + title);
432  QDomDocument doc("MYTHARCHIVEITEM");
433 
434  QDomElement root = doc.createElement("item");
435  doc.appendChild(root);
436  root.setAttribute("type", "recording");
437  root.setAttribute("databaseversion", dbVersion);
438 
439  QDomElement recorded = doc.createElement("recorded");
440  root.appendChild(recorded);
441 
442  // get details from recorded
443  QStringList fieldList;
444  getFieldList(fieldList, "recorded");
445 
446  MSqlQuery query(MSqlQuery::InitCon());
447  query.prepare("SELECT " + fieldList.join(",")
448  + " FROM recorded"
449  " WHERE chanid = :CHANID and starttime = :STARTTIME;");
450  query.bindValue(":CHANID", chanID);
451  query.bindValue(":STARTTIME", startTime);
452 
453  if (query.exec() && query.next())
454  {
455  QDomElement elem;
456  QDomText text;
457 
458  for (int x = 0; x < fieldList.size(); x++)
459  {
460  elem = doc.createElement(fieldList[x]);
461  text = doc.createTextNode(query.value(x).toString());
462  elem.appendChild(text);
463  recorded.appendChild(elem);
464  }
465 
466  LOG(VB_JOBQUEUE, LOG_INFO, "Created recorded element for " + title);
467  }
468  else
469  {
470  LOG(VB_JOBQUEUE, LOG_INFO, "Failed to get recorded field list");
471  }
472 
473  // add channel details
474  query.prepare("SELECT chanid, channum, callsign, name "
475  "FROM channel WHERE chanid = :CHANID;");
476  query.bindValue(":CHANID", chanID);
477 
478  if (query.exec() && query.next())
479  {
480  QDomElement channel = doc.createElement("channel");
481  channel.setAttribute("chanid", query.value(0).toString());
482  channel.setAttribute("channum", query.value(1).toString());
483  channel.setAttribute("callsign", query.value(2).toString());
484  channel.setAttribute("name", query.value(3).toString());
485  root.appendChild(channel);
486  LOG(VB_JOBQUEUE, LOG_INFO, "Created channel element for " + title);
487  }
488  else
489  {
490  // cannot find the original channel so create a default channel element
491  LOG(VB_JOBQUEUE, LOG_ERR,
492  "Cannot find channel details for chanid " + chanID);
493  QDomElement channel = doc.createElement("channel");
494  channel.setAttribute("chanid", chanID);
495  channel.setAttribute("channum", "unknown");
496  channel.setAttribute("callsign", "unknown");
497  channel.setAttribute("name", "unknown");
498  root.appendChild(channel);
499  LOG(VB_JOBQUEUE, LOG_INFO,
500  "Created a default channel element for " + title);
501  }
502 
503  // add any credits
504  query.prepare("SELECT credits.person, role, people.name "
505  "FROM recordedcredits AS credits "
506  "LEFT JOIN people ON credits.person = people.person "
507  "WHERE chanid = :CHANID AND starttime = :STARTTIME;");
508  query.bindValue(":CHANID", chanID);
509  query.bindValue(":STARTTIME", startTime);
510 
511  if (query.exec() && query.size())
512  {
513  QDomElement credits = doc.createElement("credits");
514  while (query.next())
515  {
516  QDomElement credit = doc.createElement("credit");
517  credit.setAttribute("personid", query.value(0).toString());
518  credit.setAttribute("name", query.value(2).toString());
519  credit.setAttribute("role", query.value(1).toString());
520  credits.appendChild(credit);
521  }
522  root.appendChild(credits);
523  LOG(VB_JOBQUEUE, LOG_INFO, "Created credits element for " + title);
524  }
525 
526  // add any rating
527  query.prepare("SELECT system, rating FROM recordedrating "
528  "WHERE chanid = :CHANID AND starttime = :STARTTIME;");
529  query.bindValue(":CHANID", chanID);
530  query.bindValue(":STARTTIME", startTime);
531 
532  if (query.exec() && query.next())
533  {
534  QDomElement rating = doc.createElement("rating");
535  rating.setAttribute("system", query.value(0).toString());
536  rating.setAttribute("rating", query.value(1).toString());
537  root.appendChild(rating);
538  LOG(VB_JOBQUEUE, LOG_INFO, "Created rating element for " + title);
539  }
540 
541  // add the recordedmarkup table
542  QDomElement recordedmarkup = doc.createElement("recordedmarkup");
543  query.prepare("SELECT chanid, starttime, mark, type, data "
544  "FROM recordedmarkup "
545  "WHERE chanid = :CHANID and starttime = :STARTTIME;");
546  query.bindValue(":CHANID", chanID);
547  query.bindValue(":STARTTIME", startTime);
548  if (query.exec() && query.size())
549  {
550  while (query.next())
551  {
552  QDomElement mark = doc.createElement("mark");
553  mark.setAttribute("mark", query.value(2).toString());
554  mark.setAttribute("type", query.value(3).toString());
555  mark.setAttribute("data", query.value(4).toString());
556  recordedmarkup.appendChild(mark);
557  }
558  root.appendChild(recordedmarkup);
559  LOG(VB_JOBQUEUE, LOG_INFO, "Created recordedmarkup element for " + title);
560  }
561 
562  // add the recordedseek table
563  QDomElement recordedseek = doc.createElement("recordedseek");
564  query.prepare("SELECT chanid, starttime, mark, offset, type "
565  "FROM recordedseek "
566  "WHERE chanid = :CHANID and starttime = :STARTTIME;");
567  query.bindValue(":CHANID", chanID);
568  query.bindValue(":STARTTIME", startTime);
569  if (query.exec() && query.size())
570  {
571  while (query.next())
572  {
573  QDomElement mark = doc.createElement("mark");
574  mark.setAttribute("mark", query.value(2).toString());
575  mark.setAttribute("offset", query.value(3).toString());
576  mark.setAttribute("type", query.value(4).toString());
577  recordedseek.appendChild(mark);
578  }
579  root.appendChild(recordedseek);
580  LOG(VB_JOBQUEUE, LOG_INFO,
581  "Created recordedseek element for " + title);
582  }
583 
584  // finally save the xml to the file
585  QString baseName = getBaseName(filename);
586  QString xmlFile = saveDirectory + title + "/" + baseName + ".xml";
587  QFile f(xmlFile);
588  if (!f.open(QIODevice::WriteOnly))
589  {
590  LOG(VB_JOBQUEUE, LOG_ERR,
591  "MythNativeWizard: Failed to open file for writing - " + xmlFile);
592  return 0;
593  }
594 
595  QTextStream t(&f);
596  t << doc.toString(4);
597  f.close();
598 
599  // copy the file
600  LOG(VB_JOBQUEUE, LOG_INFO, "Copying video file");
601  bool res = copyFile(filename, saveDirectory + title + "/" + baseName);
602  if (!res)
603  return 0;
604 
605  // copy preview image
606  if (QFile::exists(filename + ".png"))
607  {
608  LOG(VB_JOBQUEUE, LOG_INFO, "Copying preview image");
609  res = copyFile(filename + ".png", saveDirectory
610  + title + "/" + baseName + ".png");
611  if (!res)
612  return 0;
613  }
614 
615  LOG(VB_JOBQUEUE, LOG_INFO, "Item Archived OK");
616 
617  return 1;
618 }
619 
620 int NativeArchive::exportVideo(QDomElement &itemNode,
621  const QString &saveDirectory)
622 {
623  QString dbVersion = gCoreContext->GetSetting("DBSchemaVer", "");
624  int intID = 0, categoryID = 0;
625  QString coverFile = "";
626 
627  QString title = fixFilename(itemNode.attribute("title"));
628  QString filename = itemNode.attribute("filename");
629  bool doDelete = (itemNode.attribute("delete", "0") == "0");
630  LOG(VB_JOBQUEUE, LOG_INFO, QString("Archiving %1 (%2), do delete: %3")
631  .arg(title).arg(filename).arg(doDelete));
632 
633  if (title == "" || filename == "")
634  {
635  LOG(VB_JOBQUEUE, LOG_ERR, "Bad title or filename");
636  return 0;
637  }
638 
639  // create the directory to hold this items files
640  QDir dir(saveDirectory + title);
641  if (!dir.exists())
642  dir.mkdir(saveDirectory + title);
643 
644  LOG(VB_JOBQUEUE, LOG_INFO, "Creating xml file for " + title);
645  QDomDocument doc("MYTHARCHIVEITEM");
646 
647  QDomElement root = doc.createElement("item");
648  doc.appendChild(root);
649  root.setAttribute("type", "video");
650  root.setAttribute("databaseversion", dbVersion);
651 
652  QDomElement video = doc.createElement("videometadata");
653  root.appendChild(video);
654 
655  // get details from videometadata
656  MSqlQuery query(MSqlQuery::InitCon());
657  query.prepare("SELECT intid, title, director, plot, rating, inetref, "
658  "year, userrating, length, showlevel, filename, coverfile, "
659  "childid, browse, playcommand, category "
660  "FROM videometadata WHERE filename = :FILENAME;");
661  query.bindValue(":FILENAME", filename);
662 
663  if (query.exec() && query.next())
664  {
665  QDomElement elem;
666  QDomText text;
667 
668  elem = doc.createElement("intid");
669  text = doc.createTextNode(query.value(0).toString());
670  intID = query.value(0).toInt();
671  elem.appendChild(text);
672  video.appendChild(elem);
673 
674  elem = doc.createElement("title");
675  text = doc.createTextNode(query.value(1).toString());
676  elem.appendChild(text);
677  video.appendChild(elem);
678 
679  elem = doc.createElement("director");
680  text = doc.createTextNode(query.value(2).toString());
681  elem.appendChild(text);
682  video.appendChild(elem);
683 
684  elem = doc.createElement("plot");
685  text = doc.createTextNode(query.value(3).toString());
686  elem.appendChild(text);
687  video.appendChild(elem);
688 
689  elem = doc.createElement("rating");
690  text = doc.createTextNode(query.value(4).toString());
691  elem.appendChild(text);
692  video.appendChild(elem);
693 
694  elem = doc.createElement("inetref");
695  text = doc.createTextNode(query.value(5).toString());
696  elem.appendChild(text);
697  video.appendChild(elem);
698 
699  elem = doc.createElement("year");
700  text = doc.createTextNode(query.value(6).toString());
701  elem.appendChild(text);
702  video.appendChild(elem);
703 
704  elem = doc.createElement("userrating");
705  text = doc.createTextNode(query.value(7).toString());
706  elem.appendChild(text);
707  video.appendChild(elem);
708 
709  elem = doc.createElement("length");
710  text = doc.createTextNode(query.value(8).toString());
711  elem.appendChild(text);
712  video.appendChild(elem);
713 
714  elem = doc.createElement("showlevel");
715  text = doc.createTextNode(query.value(9).toString());
716  elem.appendChild(text);
717  video.appendChild(elem);
718 
719  // remove the VideoStartupDir part of the filename
720  QString fname = query.value(10).toString();
721  if (fname.startsWith(gCoreContext->GetSetting("VideoStartupDir")))
722  fname = fname.remove(gCoreContext->GetSetting("VideoStartupDir"));
723 
724  elem = doc.createElement("filename");
725  text = doc.createTextNode(fname);
726  elem.appendChild(text);
727  video.appendChild(elem);
728 
729  elem = doc.createElement("coverfile");
730  text = doc.createTextNode(query.value(11).toString());
731  coverFile = query.value(11).toString();
732  elem.appendChild(text);
733  video.appendChild(elem);
734 
735  elem = doc.createElement("childid");
736  text = doc.createTextNode(query.value(12).toString());
737  elem.appendChild(text);
738  video.appendChild(elem);
739 
740  elem = doc.createElement("browse");
741  text = doc.createTextNode(query.value(13).toString());
742  elem.appendChild(text);
743  video.appendChild(elem);
744 
745  elem = doc.createElement("playcommand");
746  text = doc.createTextNode(query.value(14).toString());
747  elem.appendChild(text);
748  video.appendChild(elem);
749 
750  elem = doc.createElement("categoryid");
751  text = doc.createTextNode(query.value(15).toString());
752  categoryID = query.value(15).toInt();
753  elem.appendChild(text);
754  video.appendChild(elem);
755 
756  LOG(VB_JOBQUEUE, LOG_INFO,
757  "Created videometadata element for " + title);
758  }
759 
760  // add category details
761  query.prepare("SELECT intid, category "
762  "FROM videocategory WHERE intid = :INTID;");
763  query.bindValue(":INTID", categoryID);
764 
765  if (query.exec() && query.next())
766  {
767  QDomElement category = doc.createElement("category");
768  category.setAttribute("intid", query.value(0).toString());
769  category.setAttribute("category", query.value(1).toString());
770  root.appendChild(category);
771  LOG(VB_JOBQUEUE, LOG_INFO,
772  "Created videocategory element for " + title);
773  }
774 
775  //add video country details
776  QDomElement countries = doc.createElement("countries");
777  root.appendChild(countries);
778 
779  query.prepare("SELECT intid, country "
780  "FROM videometadatacountry INNER JOIN videocountry "
781  "ON videometadatacountry.idcountry = videocountry.intid "
782  "WHERE idvideo = :INTID;");
783  query.bindValue(":INTID", intID);
784 
785  if (!query.exec())
786  MythDB::DBError("select countries", query);
787 
788  if (query.isActive() && query.size())
789  {
790  while (query.next())
791  {
792  QDomElement country = doc.createElement("country");
793  country.setAttribute("intid", query.value(0).toString());
794  country.setAttribute("country", query.value(1).toString());
795  countries.appendChild(country);
796  }
797  LOG(VB_JOBQUEUE, LOG_INFO, "Created videocountry element for " + title);
798  }
799 
800  // add video genre details
801  QDomElement genres = doc.createElement("genres");
802  root.appendChild(genres);
803 
804  query.prepare("SELECT intid, genre "
805  "FROM videometadatagenre INNER JOIN videogenre "
806  "ON videometadatagenre.idgenre = videogenre.intid "
807  "WHERE idvideo = :INTID;");
808  query.bindValue(":INTID", intID);
809 
810  if (!query.exec())
811  MythDB::DBError("select genres", query);
812 
813  if (query.isActive() && query.size())
814  {
815  while (query.next())
816  {
817  QDomElement genre = doc.createElement("genre");
818  genre.setAttribute("intid", query.value(0).toString());
819  genre.setAttribute("genre", query.value(1).toString());
820  genres.appendChild(genre);
821  }
822  LOG(VB_JOBQUEUE, LOG_INFO, "Created videogenre element for " + title);
823  }
824 
825  // finally save the xml to the file
826  QFileInfo fileInfo(filename);
827  QString xmlFile = saveDirectory + title + "/"
828  + fileInfo.fileName() + ".xml";
829  QFile f(xmlFile);
830  if (!f.open(QIODevice::WriteOnly))
831  {
832  LOG(VB_JOBQUEUE, LOG_INFO,
833  "MythNativeWizard: Failed to open file for writing - " + xmlFile);
834  return 0;
835  }
836 
837  QTextStream t(&f);
838  t << doc.toString(4);
839  f.close();
840 
841  // copy the file
842  LOG(VB_JOBQUEUE, LOG_INFO, "Copying video file");
843  bool res = copyFile(filename, saveDirectory + title
844  + "/" + fileInfo.fileName());
845  if (!res)
846  {
847  return 0;
848  }
849 
850  // copy the cover image
851  fileInfo.setFile(coverFile);
852  if (fileInfo.exists())
853  {
854  LOG(VB_JOBQUEUE, LOG_INFO, "Copying cover file");
855  res = copyFile(coverFile, saveDirectory + title
856  + "/" + fileInfo.fileName());
857  if (!res)
858  {
859  return 0;
860  }
861  }
862 
863  LOG(VB_JOBQUEUE, LOG_INFO, "Item Archived OK");
864 
865  return 1;
866 }
867 
868 int NativeArchive::doImportArchive(const QString &xmlFile, int chanID)
869 {
870  // open xml file
871  QDomDocument doc("mydocument");
872  QFile file(xmlFile);
873  if (!file.open(QIODevice::ReadOnly))
874  {
875  LOG(VB_JOBQUEUE, LOG_ERR,
876  "Failed to open file for reading - " + xmlFile);
877  return 1;
878  }
879 
880  if (!doc.setContent(&file))
881  {
882  file.close();
883  LOG(VB_JOBQUEUE, LOG_ERR,
884  "Failed to read from xml file - " + xmlFile);
885  return 1;
886  }
887  file.close();
888 
889  QString docType = doc.doctype().name();
890  QString type, dbVersion;
891  QDomNodeList itemNodeList;
892  QDomNode node;
893  QDomElement itemNode;
894 
895  if (docType == "MYTHARCHIVEITEM")
896  {
897  itemNodeList = doc.elementsByTagName("item");
898 
899  if (itemNodeList.count() < 1)
900  {
901  LOG(VB_JOBQUEUE, LOG_ERR,
902  "Couldn't find an 'item' element in XML file");
903  return 1;
904  }
905 
906  node = itemNodeList.item(0);
907  itemNode = node.toElement();
908  type = itemNode.attribute("type");
909  dbVersion = itemNode.attribute("databaseversion");
910 
911  LOG(VB_JOBQUEUE, LOG_INFO,
912  QString("Archive DB version: %1, Local DB version: %2")
913  .arg(dbVersion).arg(gCoreContext->GetSetting("DBSchemaVer")));
914  }
915  else
916  {
917  LOG(VB_JOBQUEUE, LOG_ERR, "Not a native archive xml file - " + xmlFile);
918  return 1;
919  }
920 
921  if (type == "recording")
922  {
923  return importRecording(itemNode, xmlFile, chanID);
924  }
925  else if (type == "video")
926  {
927  return importVideo(itemNode, xmlFile);
928  }
929 
930  return 1;
931 }
932 
933 int NativeArchive::importRecording(const QDomElement &itemNode,
934  const QString &xmlFile, int chanID)
935 {
936  LOG(VB_JOBQUEUE, LOG_INFO,
937  QString("Import recording using chanID: %1").arg(chanID));
938  LOG(VB_JOBQUEUE, LOG_INFO,
939  QString("Archived recording xml file: %1").arg(xmlFile));
940 
941  QString videoFile = xmlFile.left(xmlFile.length() - 4);
942  QString basename = videoFile;
943  int pos = videoFile.lastIndexOf('/');
944  if (pos > 0)
945  basename = videoFile.mid(pos + 1);
946 
947  QDomNodeList nodeList = itemNode.elementsByTagName("recorded");
948  if (nodeList.count() < 1)
949  {
950  LOG(VB_JOBQUEUE, LOG_ERR,
951  "Couldn't find a 'recorded' element in XML file");
952  return 1;
953  }
954 
955  QDomNode n = nodeList.item(0);
956  QDomElement recordedNode = n.toElement();
957  QString startTime = findNodeText(recordedNode, "starttime");
958  // check this recording doesn't already exist
959  MSqlQuery query(MSqlQuery::InitCon());
960  query.prepare("SELECT * FROM recorded "
961  "WHERE chanid = :CHANID AND starttime = :STARTTIME;");
962  query.bindValue(":CHANID", chanID);
963  query.bindValue(":STARTTIME", startTime);
964  if (query.exec())
965  {
966  if (query.isActive() && query.size())
967  {
968  LOG(VB_JOBQUEUE, LOG_ERR,
969  "This recording appears to already exist!!");
970  return 1;
971  }
972  }
973 
974  QString destFile = gCoreContext->GenMythURL(gCoreContext->GetMasterHostName(),
976  basename , "Default");
977 
978  // copy file to recording directory
979  LOG(VB_JOBQUEUE, LOG_INFO, "Copying video file to: " + destFile);
980  if (!copyFile(videoFile, destFile))
981  return 1;
982 
983  // copy any preview image to recording directory
984  if (QFile::exists(videoFile + ".png"))
985  {
986  LOG(VB_JOBQUEUE, LOG_INFO, "Copying preview image file to: " + destFile + ".png");
987  if (!copyFile(videoFile + ".png", destFile + ".png"))
988  return 1;
989  }
990 
991  // get a list of fields from the xmlFile
992  QStringList fieldList;
993  QStringList bindList;
994  QDomNodeList nodes = recordedNode.childNodes();
995 
996  for (int x = 0; x < nodes.count(); x++)
997  {
998  QDomNode n2 = nodes.item(x);
999  QString field = n2.nodeName();
1000  fieldList.append(field);
1001  bindList.append(":" + field.toUpper());
1002  }
1003 
1004  // copy recorded to database
1005  query.prepare("INSERT INTO recorded (" + fieldList.join(",") + ") "
1006  "VALUES (" + bindList.join(",") + ");");
1007  query.bindValue(":CHANID", chanID);
1008  query.bindValue(":STARTTIME", startTime);
1009 
1010  for (int x = 0; x < fieldList.count(); x++)
1011  query.bindValue(bindList.at(x), findNodeText(recordedNode, fieldList.at(x)));
1012 
1013  if (query.exec())
1014  LOG(VB_JOBQUEUE, LOG_INFO, "Inserted recorded details into database");
1015  else
1016  MythDB::DBError("recorded insert", query);
1017 
1018  // copy recordedmarkup to db
1019  nodeList = itemNode.elementsByTagName("recordedmarkup");
1020  if (nodeList.count() < 1)
1021  {
1022  LOG(VB_JOBQUEUE, LOG_WARNING,
1023  "Couldn't find a 'recordedmarkup' element in XML file");
1024  }
1025  else
1026  {
1027  QDomNode n3 = nodeList.item(0);
1028  QDomElement markupNode = n3.toElement();
1029 
1030  nodeList = markupNode.elementsByTagName("mark");
1031  if (nodeList.count() < 1)
1032  {
1033  LOG(VB_JOBQUEUE, LOG_WARNING,
1034  "Couldn't find any 'mark' elements in XML file");
1035  }
1036  else
1037  {
1038  // delete any records for this recordings
1039  query.prepare("DELETE FROM recordedmarkup "
1040  "WHERE chanid = :CHANID AND starttime = :STARTTIME;");
1041  query.bindValue(":CHANID", chanID);
1042  query.bindValue(":STARTTIME", startTime);
1043 
1044  if (!query.exec())
1045  MythDB::DBError("recordedmarkup delete", query);
1046 
1047  // add any new records for this recording
1048  for (int x = 0; x < nodeList.count(); x++)
1049  {
1050  QDomNode n4 = nodeList.item(x);
1051  QDomElement e = n4.toElement();
1052  query.prepare("INSERT INTO recordedmarkup (chanid, starttime, "
1053  "mark, type, data)"
1054  "VALUES(:CHANID,:STARTTIME,:MARK,:TYPE,:DATA);");
1055  query.bindValue(":CHANID", chanID);
1056  query.bindValue(":STARTTIME", startTime);
1057  query.bindValue(":MARK", e.attribute("mark"));
1058  query.bindValue(":TYPE", e.attribute("type"));
1059  query.bindValue(":DATA", e.attribute("data"));
1060 
1061  if (!query.exec())
1062  {
1063  MythDB::DBError("recordedmark insert", query);
1064  return 1;
1065  }
1066  }
1067 
1068  LOG(VB_JOBQUEUE, LOG_INFO,
1069  "Inserted recordedmarkup details into database");
1070  }
1071  }
1072 
1073  // copy recordedseek to db
1074  nodeList = itemNode.elementsByTagName("recordedseek");
1075  if (nodeList.count() < 1)
1076  {
1077  LOG(VB_JOBQUEUE, LOG_WARNING,
1078  "Couldn't find a 'recordedseek' element in XML file");
1079  }
1080  else
1081  {
1082  QDomNode n5 = nodeList.item(0);
1083  QDomElement markupNode = n5.toElement();
1084 
1085  nodeList = markupNode.elementsByTagName("mark");
1086  if (nodeList.count() < 1)
1087  {
1088  LOG(VB_JOBQUEUE, LOG_WARNING,
1089  "Couldn't find any 'mark' elements in XML file");
1090  }
1091  else
1092  {
1093  // delete any records for this recordings
1094  query.prepare("DELETE FROM recordedseek "
1095  "WHERE chanid = :CHANID AND starttime = :STARTTIME;");
1096  query.bindValue(":CHANID", chanID);
1097  query.bindValue(":STARTTIME", startTime);
1098  query.exec();
1099 
1100  // add the new records for this recording
1101  for (int x = 0; x < nodeList.count(); x++)
1102  {
1103  QDomNode n6 = nodeList.item(x);
1104  QDomElement e = n6.toElement();
1105  query.prepare("INSERT INTO recordedseek (chanid, starttime, "
1106  "mark, offset, type)"
1107  "VALUES(:CHANID,:STARTTIME,:MARK,:OFFSET,:TYPE);");
1108  query.bindValue(":CHANID", chanID);
1109  query.bindValue(":STARTTIME", startTime);
1110  query.bindValue(":MARK", e.attribute("mark"));
1111  query.bindValue(":OFFSET", e.attribute("offset"));
1112  query.bindValue(":TYPE", e.attribute("type"));
1113 
1114  if (!query.exec())
1115  {
1116  MythDB::DBError("recordedseek insert", query);
1117  return 1;
1118  }
1119  }
1120 
1121  LOG(VB_JOBQUEUE, LOG_INFO,
1122  "Inserted recordedseek details into database");
1123  }
1124  }
1125 
1126  // FIXME are these needed?
1127  // copy credits to DB
1128  // copy rating to DB
1129 
1130  LOG(VB_JOBQUEUE, LOG_INFO, "Import completed OK");
1131 
1132  return 0;
1133 }
1134 
1135 int NativeArchive::importVideo(const QDomElement &itemNode, const QString &xmlFile)
1136 {
1137  LOG(VB_JOBQUEUE, LOG_INFO, "Importing video");
1138  LOG(VB_JOBQUEUE, LOG_INFO,
1139  QString("Archived video xml file: %1").arg(xmlFile));
1140 
1141  QString videoFile = xmlFile.left(xmlFile.length() - 4);
1142  QFileInfo fileInfo(videoFile);
1143  QString basename = fileInfo.fileName();
1144 
1145  QDomNodeList nodeList = itemNode.elementsByTagName("videometadata");
1146  if (nodeList.count() < 1)
1147  {
1148  LOG(VB_JOBQUEUE, LOG_ERR,
1149  "Couldn't find a 'videometadata' element in XML file");
1150  return 1;
1151  }
1152 
1153  QDomNode n = nodeList.item(0);
1154  QDomElement videoNode = n.toElement();
1155 
1156  // copy file to video directory
1157  QString path = gCoreContext->GetSetting("VideoStartupDir");
1158  QString origFilename = findNodeText(videoNode, "filename");
1159  QStringList dirList = origFilename.split("/", QString::SkipEmptyParts);
1160  QDir dir;
1161  for (int x = 0; x < dirList.count() - 1; x++)
1162  {
1163  path += "/" + dirList[x];
1164  if (!dir.exists(path))
1165  {
1166  if (!dir.mkdir(path))
1167  {
1168  LOG(VB_JOBQUEUE, LOG_ERR,
1169  QString("Couldn't create directory '%1'").arg(path));
1170  return 1;
1171  }
1172  }
1173  }
1174 
1175  LOG(VB_JOBQUEUE, LOG_INFO, "Copying video file");
1176  if (!copyFile(videoFile, path + "/" + basename))
1177  {
1178  return 1;
1179  }
1180 
1181  // copy cover image to Video Artwork dir
1182  QString artworkDir = gCoreContext->GetSetting("VideoArtworkDir");
1183  // get archive path
1184  fileInfo.setFile(videoFile);
1185  QString archivePath = fileInfo.absolutePath();
1186  // get coverfile filename
1187  QString coverFilename = findNodeText(videoNode, "coverfile");
1188  fileInfo.setFile(coverFilename);
1189  coverFilename = fileInfo.fileName();
1190  //check file exists
1191  fileInfo.setFile(archivePath + "/" + coverFilename);
1192  if (fileInfo.exists())
1193  {
1194  LOG(VB_JOBQUEUE, LOG_INFO, "Copying cover file");
1195 
1196  if (!copyFile(archivePath + "/" + coverFilename, artworkDir + "/" + coverFilename))
1197  {
1198  return 1;
1199  }
1200  }
1201  else
1202  coverFilename = "No Cover";
1203 
1204  // copy videometadata to database
1205  MSqlQuery query(MSqlQuery::InitCon());
1206  query.prepare("INSERT INTO videometadata (title, director, plot, rating, inetref, "
1207  "year, userrating, length, showlevel, filename, coverfile, "
1208  "childid, browse, playcommand, category) "
1209  "VALUES(:TITLE,:DIRECTOR,:PLOT,:RATING,:INETREF,:YEAR,"
1210  ":USERRATING,:LENGTH,:SHOWLEVEL,:FILENAME,:COVERFILE,"
1211  ":CHILDID,:BROWSE,:PLAYCOMMAND,:CATEGORY);");
1212  query.bindValue(":TITLE", findNodeText(videoNode, "title"));
1213  query.bindValue(":DIRECTOR", findNodeText(videoNode, "director"));
1214  query.bindValue(":PLOT", findNodeText(videoNode, "plot"));
1215  query.bindValue(":RATING", findNodeText(videoNode, "rating"));
1216  query.bindValue(":INETREF", findNodeText(videoNode, "inetref"));
1217  query.bindValue(":YEAR", findNodeText(videoNode, "year"));
1218  query.bindValue(":USERRATING", findNodeText(videoNode, "userrating"));
1219  query.bindValue(":LENGTH", findNodeText(videoNode, "length"));
1220  query.bindValue(":SHOWLEVEL", findNodeText(videoNode, "showlevel"));
1221  query.bindValue(":FILENAME", path + "/" + basename);
1222  query.bindValue(":COVERFILE", artworkDir + "/" + coverFilename);
1223  query.bindValue(":CHILDID", findNodeText(videoNode, "childid"));
1224  query.bindValue(":BROWSE", findNodeText(videoNode, "browse"));
1225  query.bindValue(":PLAYCOMMAND", findNodeText(videoNode, "playcommand"));
1226  query.bindValue(":CATEGORY", 0);
1227 
1228  if (query.exec())
1229  LOG(VB_JOBQUEUE, LOG_INFO,
1230  "Inserted videometadata details into database");
1231  else
1232  {
1233  MythDB::DBError("videometadata insert", query);
1234  return 1;
1235  }
1236 
1237  // get intid field for inserted record
1238  int intid;
1239  query.prepare("SELECT intid FROM videometadata WHERE filename = :FILENAME;");
1240  query.bindValue(":FILENAME", path + "/" + basename);
1241  if (query.exec() && query.next())
1242  {
1243  intid = query.value(0).toInt();
1244  }
1245  else
1246  {
1247  MythDB::DBError("Failed to get intid", query);
1248  return 1;
1249  }
1250 
1251  LOG(VB_JOBQUEUE, LOG_INFO,
1252  QString("'intid' of inserted video is: %1").arg(intid));
1253 
1254  // copy genre to db
1255  nodeList = itemNode.elementsByTagName("genres");
1256  if (nodeList.count() < 1)
1257  {
1258  LOG(VB_JOBQUEUE, LOG_ERR, "No 'genres' element found in XML file");
1259  }
1260  else
1261  {
1262  n = nodeList.item(0);
1263  QDomElement genresNode = n.toElement();
1264 
1265  nodeList = genresNode.elementsByTagName("genre");
1266  if (nodeList.count() < 1)
1267  {
1268  LOG(VB_JOBQUEUE, LOG_WARNING,
1269  "Couldn't find any 'genre' elements in XML file");
1270  }
1271  else
1272  {
1273  for (int x = 0; x < nodeList.count(); x++)
1274  {
1275  n = nodeList.item(x);
1276  QDomElement e = n.toElement();
1277  int genreID = e.attribute("intid").toInt();
1278  QString genre = e.attribute("genre");
1279 
1280  // see if this genre already exists
1281  query.prepare("SELECT intid FROM videogenre "
1282  "WHERE genre = :GENRE");
1283  query.bindValue(":GENRE", genre);
1284  if (query.exec() && query.next())
1285  {
1286  genreID = query.value(0).toInt();
1287  }
1288  else
1289  {
1290  // genre doesn't exist so add it
1291  query.prepare("INSERT INTO videogenre (genre) VALUES(:GENRE);");
1292  query.bindValue(":GENRE", genre);
1293  if (!query.exec())
1294  MythDB::DBError("NativeArchive::importVideo - "
1295  "insert videogenre", query);
1296 
1297  // get new intid of genre
1298  query.prepare("SELECT intid FROM videogenre "
1299  "WHERE genre = :GENRE");
1300  query.bindValue(":GENRE", genre);
1301  if (query.exec() && query.next())
1302  {
1303  genreID = query.value(0).toInt();
1304  }
1305  else
1306  {
1307  LOG(VB_JOBQUEUE, LOG_ERR,
1308  "Couldn't add genre to database");
1309  continue;
1310  }
1311  }
1312 
1313  // now link the genre to the videometadata
1314  query.prepare("INSERT INTO videometadatagenre (idvideo, idgenre)"
1315  "VALUES (:IDVIDEO, :IDGENRE);");
1316  query.bindValue(":IDVIDEO", intid);
1317  query.bindValue(":IDGENRE", genreID);
1318  if (!query.exec())
1319  MythDB::DBError("NativeArchive::importVideo - "
1320  "insert videometadatagenre", query);
1321  }
1322 
1323  LOG(VB_JOBQUEUE, LOG_INFO, "Inserted genre details into database");
1324  }
1325  }
1326 
1327  // copy country to db
1328  nodeList = itemNode.elementsByTagName("countries");
1329  if (nodeList.count() < 1)
1330  {
1331  LOG(VB_JOBQUEUE, LOG_INFO, "No 'countries' element found in XML file");
1332  }
1333  else
1334  {
1335  n = nodeList.item(0);
1336  QDomElement countriesNode = n.toElement();
1337 
1338  nodeList = countriesNode.elementsByTagName("country");
1339  if (nodeList.count() < 1)
1340  {
1341  LOG(VB_JOBQUEUE, LOG_WARNING,
1342  "Couldn't find any 'country' elements in XML file");
1343  }
1344  else
1345  {
1346  for (int x = 0; x < nodeList.count(); x++)
1347  {
1348  n = nodeList.item(x);
1349  QDomElement e = n.toElement();
1350  int countryID = e.attribute("intid").toInt();
1351  QString country = e.attribute("country");
1352 
1353  // see if this country already exists
1354  query.prepare("SELECT intid FROM videocountry "
1355  "WHERE country = :COUNTRY");
1356  query.bindValue(":COUNTRY", country);
1357  if (query.exec() && query.next())
1358  {
1359  countryID = query.value(0).toInt();
1360  }
1361  else
1362  {
1363  // country doesn't exist so add it
1364  query.prepare("INSERT INTO videocountry (country) VALUES(:COUNTRY);");
1365  query.bindValue(":COUNTRY", country);
1366  if (!query.exec())
1367  MythDB::DBError("NativeArchive::importVideo - "
1368  "insert videocountry", query);
1369 
1370  // get new intid of country
1371  query.prepare("SELECT intid FROM videocountry "
1372  "WHERE country = :COUNTRY");
1373  query.bindValue(":COUNTRY", country);
1374  if (query.exec() && query.next())
1375  {
1376  countryID = query.value(0).toInt();
1377  }
1378  else
1379  {
1380  LOG(VB_JOBQUEUE, LOG_ERR,
1381  "Couldn't add country to database");
1382  continue;
1383  }
1384  }
1385 
1386  // now link the country to the videometadata
1387  query.prepare("INSERT INTO videometadatacountry (idvideo, idcountry)"
1388  "VALUES (:IDVIDEO, :IDCOUNTRY);");
1389  query.bindValue(":IDVIDEO", intid);
1390  query.bindValue(":IDCOUNTRY", countryID);
1391  if (!query.exec())
1392  MythDB::DBError("NativeArchive::importVideo - "
1393  "insert videometadatacountry", query);
1394  }
1395 
1396  LOG(VB_JOBQUEUE, LOG_INFO,
1397  "Inserted country details into database");
1398  }
1399  }
1400 
1401  // fix the category id
1402  nodeList = itemNode.elementsByTagName("category");
1403  if (nodeList.count() < 1)
1404  {
1405  LOG(VB_JOBQUEUE, LOG_ERR, "No 'category' element found in XML file");
1406  }
1407  else
1408  {
1409  n = nodeList.item(0);
1410  QDomElement e = n.toElement();
1411  int categoryID = e.attribute("intid").toInt();
1412  QString category = e.attribute("category");
1413  // see if this category already exists
1414  query.prepare("SELECT intid FROM videocategory "
1415  "WHERE category = :CATEGORY");
1416  query.bindValue(":CATEGORY", category);
1417  if (query.exec() && query.next())
1418  {
1419  categoryID = query.value(0).toInt();
1420  }
1421  else
1422  {
1423  // category doesn't exist so add it
1424  query.prepare("INSERT INTO videocategory (category) VALUES(:CATEGORY);");
1425  query.bindValue(":CATEGORY", category);
1426  if (!query.exec())
1427  MythDB::DBError("NativeArchive::importVideo - "
1428  "insert videocategory", query);
1429 
1430  // get new intid of category
1431  query.prepare("SELECT intid FROM videocategory "
1432  "WHERE category = :CATEGORY");
1433  query.bindValue(":CATEGORY", category);
1434  if (query.exec() && query.next())
1435  {
1436  categoryID = query.value(0).toInt();
1437  }
1438  else
1439  {
1440  LOG(VB_JOBQUEUE, LOG_ERR, "Couldn't add category to database");
1441  categoryID = 0;
1442  }
1443  }
1444 
1445  // now fix the categoryid in the videometadata
1446  query.prepare("UPDATE videometadata "
1447  "SET category = :CATEGORY "
1448  "WHERE intid = :INTID;");
1449  query.bindValue(":CATEGORY", categoryID);
1450  query.bindValue(":INTID", intid);
1451  if (!query.exec())
1452  MythDB::DBError("NativeArchive::importVideo - "
1453  "update category", query);
1454 
1455  LOG(VB_JOBQUEUE, LOG_INFO, "Fixed the category in the database");
1456  }
1457 
1458  LOG(VB_JOBQUEUE, LOG_INFO, "Import completed OK");
1459 
1460  return 0;
1461 }
1462 
1463 QString NativeArchive::findNodeText(const QDomElement &elem, const QString &nodeName)
1464 {
1465  QDomNodeList nodeList = elem.elementsByTagName(nodeName);
1466  if (nodeList.count() < 1)
1467  {
1468  LOG(VB_GENERAL, LOG_ERR,
1469  QString("Couldn't find a '%1' element in XML file") .arg(nodeName));
1470  return "";
1471  }
1472 
1473  QDomNode n = nodeList.item(0);
1474  QDomElement e = n.toElement();
1475  QString res = "";
1476 
1477  for (QDomNode node = e.firstChild(); !node.isNull();
1478  node = node.nextSibling())
1479  {
1480  QDomText t = node.toText();
1481  if (!t.isNull())
1482  {
1483  res = t.data();
1484  break;
1485  }
1486  }
1487 
1488  // some fixups
1489  // FIXME could be a lot smarter
1490  if (nodeName == "recgroup")
1491  {
1492  res = "Default";
1493  }
1494  else if (nodeName == "recordid")
1495  {
1496  res = "";
1497  }
1498  else if (nodeName == "seriesid")
1499  {
1500  res = "";
1501  }
1502  else if (nodeName == "programid")
1503  {
1504  res = "";
1505  }
1506  else if (nodeName == "playgroup")
1507  {
1508  res = "Default";
1509  }
1510  else if (nodeName == "profile")
1511  {
1512  res = "";
1513  }
1514 
1515  return res;
1516 }
1517 
1518 static void clearArchiveTable(void)
1519 {
1520  MSqlQuery query(MSqlQuery::InitCon());
1521  query.prepare("DELETE FROM archiveitems;");
1522 
1523  if (!query.exec())
1524  MythDB::DBError("delete archiveitems", query);
1525 }
1526 
1527 static int doNativeArchive(const QString &jobFile)
1528 {
1529  gCoreContext->SaveSetting("MythArchiveLastRunType", "Native Export");
1531  "MythArchiveLastRunStart",
1533  gCoreContext->SaveSetting("MythArchiveLastRunStatus", "Running");
1534 
1535  NativeArchive na;
1536  int res = na.doNativeArchive(jobFile);
1538  "MythArchiveLastRunEnd",
1540  gCoreContext->SaveSetting("MythArchiveLastRunStatus",
1541  (res == 0 ? "Success" : "Failed"));
1542 
1543  // clear the archiveitems table if succesful
1544  if (res == 0)
1546 
1547  return res;
1548 }
1549 
1550 static int doImportArchive(const QString &inFile, int chanID)
1551 {
1552  NativeArchive na;
1553  return na.doImportArchive(inFile, chanID);
1554 }
1555 
1556 static int grabThumbnail(QString inFile, QString thumbList, QString outFile, int frameCount)
1557 {
1558  // Open recording
1559  LOG(VB_JOBQUEUE, LOG_INFO, QString("grabThumbnail(): Opening '%1'")
1560  .arg(inFile));
1561 
1562  RemoteAVFormatContext inputFC(inFile);
1563  if (!inputFC.isOpen())
1564  {
1565  LOG(VB_JOBQUEUE, LOG_ERR, "grabThumbnail(): Couldn't open input file" +
1566  ENO);
1567  return 1;
1568  }
1569 
1570  // Getting stream information
1571  int ret = avformat_find_stream_info(inputFC, nullptr);
1572  if (ret < 0)
1573  {
1574  LOG(VB_JOBQUEUE, LOG_ERR,
1575  QString("Couldn't get stream info, error #%1").arg(ret));
1576  return 1;
1577  }
1578 
1579  // find the first video stream
1580  int videostream = -1, width, height;
1581  float fps;
1582 
1583  for (uint i = 0; i < inputFC->nb_streams; i++)
1584  {
1585  AVStream *st = inputFC->streams[i];
1586  if (inputFC->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO)
1587  {
1588  videostream = i;
1589  width = st->codecpar->width;
1590  height = st->codecpar->height;
1591  if (st->r_frame_rate.den && st->r_frame_rate.num)
1592  fps = av_q2d(st->r_frame_rate);
1593  else
1594  fps = 1/av_q2d(st->time_base);
1595  break;
1596  }
1597  }
1598 
1599  if (videostream == -1)
1600  {
1601  LOG(VB_JOBQUEUE, LOG_ERR, "Couldn't find a video stream");
1602  return 1;
1603  }
1604 
1605  // get the codec context for the video stream
1606  AVCodecContext *codecCtx = gCodecMap->getCodecContext
1607  (inputFC->streams[videostream]);
1608 
1609  // get decoder for video stream
1610  AVCodec * codec = avcodec_find_decoder(codecCtx->codec_id);
1611 
1612  if (codec == nullptr)
1613  {
1614  LOG(VB_JOBQUEUE, LOG_ERR, "Couldn't find codec for video stream");
1615  return 1;
1616  }
1617 
1618  // open codec
1619  if (avcodec_open2(codecCtx, codec, nullptr) < 0)
1620  {
1621  LOG(VB_JOBQUEUE, LOG_ERR, "Couldn't open codec for video stream");
1622  return 1;
1623  }
1624 
1625  // get list of required thumbs
1626  QStringList list = thumbList.split(",", QString::SkipEmptyParts);
1627  MythAVFrame frame;
1628  if (!frame)
1629  {
1630  return 1;
1631  }
1632  AVPacket pkt;
1633  AVFrame orig;
1634  AVFrame retbuf;
1635  memset(&orig, 0, sizeof(AVFrame));
1636  memset(&retbuf, 0, sizeof(AVFrame));
1637  MythAVCopy copyframe;
1638  MythPictureDeinterlacer deinterlacer(codecCtx->pix_fmt, width, height);
1639 
1640  int bufflen = width * height * 4;
1641  unsigned char *outputbuf = new unsigned char[bufflen];
1642 
1643  int frameNo = -1, thumbCount = 0;
1644  int frameFinished = 0;
1645  int keyFrame;
1646 
1647  while (av_read_frame(inputFC, &pkt) >= 0)
1648  {
1649  if (pkt.stream_index == videostream)
1650  {
1651  frameNo++;
1652  if (list[thumbCount].toInt() == (int)(frameNo / fps))
1653  {
1654  thumbCount++;
1655 
1656  avcodec_flush_buffers(codecCtx);
1657  av_frame_unref(frame);
1658  frameFinished = 0;
1659  ret = avcodec_receive_frame(codecCtx, frame);
1660  if (ret == 0)
1661  frameFinished = 1;
1662  if (ret == 0 || ret == AVERROR(EAGAIN))
1663  ret = avcodec_send_packet(codecCtx, &pkt);
1664  keyFrame = frame->key_frame;
1665 
1666  while (!frameFinished || !keyFrame)
1667  {
1668  av_packet_unref(&pkt);
1669  int res = av_read_frame(inputFC, &pkt);
1670  if (res < 0)
1671  break;
1672  if (pkt.stream_index == videostream)
1673  {
1674  frameNo++;
1675  av_frame_unref(frame);
1676  ret = avcodec_receive_frame(codecCtx, frame);
1677  if (ret == 0)
1678  frameFinished = 1;
1679  if (ret == 0 || ret == AVERROR(EAGAIN))
1680  ret = avcodec_send_packet(codecCtx, &pkt);
1681  keyFrame = frame->key_frame;
1682  }
1683  }
1684 
1685  if (frameFinished)
1686  {
1687  // work out what format to save to
1688  QString saveFormat = "JPEG";
1689  if (outFile.right(4) == ".png")
1690  saveFormat = "PNG";
1691 
1692  int count = 0;
1693  while (count < frameCount)
1694  {
1695  QString filename = outFile;
1696  if (filename.contains("%1") && filename.contains("%2"))
1697  filename = filename.arg(thumbCount).arg(count+1);
1698  else if (filename.contains("%1"))
1699  filename = filename.arg(thumbCount);
1700 
1701  av_image_fill_arrays(retbuf.data, retbuf.linesize, outputbuf,
1702  AV_PIX_FMT_RGB32, width, height, IMAGE_ALIGN);
1703 
1704  AVFrame *tmp = frame;
1705  deinterlacer.DeinterlaceSingle(tmp, tmp);
1706 
1707  copyframe.Copy(&retbuf, AV_PIX_FMT_RGB32, tmp,
1708  codecCtx->pix_fmt, width, height);
1709 
1710  QImage img(outputbuf, width, height,
1711  QImage::Format_RGB32);
1712 
1713  if (!img.save(filename, qPrintable(saveFormat)))
1714  {
1715  LOG(VB_GENERAL, LOG_ERR,
1716  QString("grabThumbnail(): Failed to save "
1717  "thumb: '%1'")
1718  .arg(filename));
1719  }
1720 
1721  count++;
1722 
1723  if (count <= frameCount)
1724  {
1725  //grab next frame
1726  frameFinished = false;
1727  while (!frameFinished)
1728  {
1729  int res = av_read_frame(inputFC, &pkt);
1730  if (res < 0)
1731  break;
1732  if (pkt.stream_index == videostream)
1733  {
1734  frameNo++;
1735  ret = avcodec_receive_frame(codecCtx, frame);
1736  if (ret == 0)
1737  frameFinished = 1;
1738  if (ret == 0 || ret == AVERROR(EAGAIN))
1739  ret = avcodec_send_packet(codecCtx, &pkt);
1740  }
1741  }
1742  }
1743  }
1744  }
1745 
1746  if (thumbCount >= list.count())
1747  break;
1748  }
1749  }
1750 
1751  av_packet_unref(&pkt);
1752  }
1753 
1754  if (outputbuf)
1755  delete[] outputbuf;
1756 
1757  // close the codec
1759  (inputFC->streams[videostream]);
1760 
1761  return 0;
1762 }
1763 
1764 static int64_t getFrameCount(AVFormatContext *inputFC, int vid_id)
1765 {
1766  AVPacket pkt;
1767  int64_t count = 0;
1768 
1769  LOG(VB_JOBQUEUE, LOG_INFO, "Calculating frame count");
1770 
1771  av_init_packet(&pkt);
1772 
1773  while (av_read_frame(inputFC, &pkt) >= 0)
1774  {
1775  if (pkt.stream_index == vid_id)
1776  {
1777  count++;
1778  }
1779  av_packet_unref(&pkt);
1780  }
1781 
1782  return count;
1783 }
1784 
1785 static int64_t getCutFrames(const QString &filename, int64_t lastFrame)
1786 {
1787  // only wont the filename
1788  QString basename = filename;
1789  int pos = filename.lastIndexOf('/');
1790  if (pos > 0)
1791  basename = filename.mid(pos + 1);
1792 
1793  ProgramInfo *progInfo = getProgramInfoForFile(basename);
1794  if (!progInfo)
1795  return 0;
1796 
1797  if (progInfo->IsVideo())
1798  {
1799  delete progInfo;
1800  return 0;
1801  }
1802 
1803  frm_dir_map_t cutlist;
1804  frm_dir_map_t::iterator it;
1805  uint64_t frames = 0;
1806 
1807  progInfo->QueryCutList(cutlist);
1808 
1809  if (cutlist.size() == 0)
1810  {
1811  delete progInfo;
1812  return 0;
1813  }
1814 
1815  for (it = cutlist.begin(); it != cutlist.end();)
1816  {
1817  uint64_t start = 0, end = 0;
1818 
1819  if (it.value() == MARK_CUT_START)
1820  {
1821  start = it.key();
1822  ++it;
1823  if (it != cutlist.end())
1824  {
1825  end = it.key();
1826  ++it;
1827  }
1828  else
1829  end = lastFrame;
1830  }
1831  else if (it.value() == MARK_CUT_END)
1832  {
1833  start = 0;
1834  end = it.key();
1835  ++it;
1836  }
1837  else
1838  {
1839  ++it;
1840  continue;
1841  }
1842 
1843  frames += end - start;
1844  }
1845 
1846  delete progInfo;
1847  return frames;
1848 }
1849 
1850 static int64_t getFrameCount(const QString &filename, float fps)
1851 {
1852  // only wont the filename
1853  QString basename = filename;
1854  int pos = filename.lastIndexOf('/');
1855  if (pos > 0)
1856  basename = filename.mid(pos + 1);
1857 
1858  int keyframedist = -1;
1859  frm_pos_map_t posMap;
1860 
1861  ProgramInfo *progInfo = getProgramInfoForFile(basename);
1862  if (!progInfo)
1863  return 0;
1864 
1865  progInfo->QueryPositionMap(posMap, MARK_GOP_BYFRAME);
1866  if (!posMap.empty())
1867  {
1868  keyframedist = 1;
1869  }
1870  else
1871  {
1872  progInfo->QueryPositionMap(posMap, MARK_GOP_START);
1873  if (!posMap.empty())
1874  {
1875  keyframedist = 15;
1876  if (fps < 26 && fps > 24)
1877  keyframedist = 12;
1878  }
1879  else
1880  {
1881  progInfo->QueryPositionMap(posMap, MARK_KEYFRAME);
1882  if (!posMap.empty())
1883  {
1884  // keyframedist should be set in the fileheader so no
1885  // need to try to determine it in this case
1886  return 0;
1887  }
1888  }
1889  }
1890 
1891  if (posMap.empty())
1892  return 0; // no position map in recording
1893 
1894  frm_pos_map_t::const_iterator it = posMap.end();
1895  --it;
1896  uint64_t totframes = it.key() * keyframedist;
1897  return totframes;
1898 }
1899 
1900 static int getFileInfo(QString inFile, QString outFile, int lenMethod)
1901 {
1902  // Open recording
1903  LOG(VB_JOBQUEUE , LOG_INFO, QString("getFileInfo(): Opening '%1'")
1904  .arg(inFile));
1905 
1906  RemoteAVFormatContext inputFC(inFile);
1907  if (!inputFC.isOpen())
1908  {
1909  LOG(VB_JOBQUEUE, LOG_ERR, "getFileInfo(): Couldn't open input file" +
1910  ENO);
1911  return 1;
1912  }
1913 
1914  // Getting stream information
1915  int ret = avformat_find_stream_info(inputFC, nullptr);
1916 
1917  if (ret < 0)
1918  {
1919  LOG(VB_JOBQUEUE, LOG_ERR,
1920  QString("Couldn't get stream info, error #%1").arg(ret));
1921  return 1;
1922  }
1923 
1924  // Dump stream information
1925  av_dump_format(inputFC, 0, qPrintable(inFile), 0);
1926 
1927  QDomDocument doc("FILEINFO");
1928 
1929  QDomElement root = doc.createElement("file");
1930  doc.appendChild(root);
1931  root.setAttribute("type", inputFC->iformat->name);
1932  root.setAttribute("filename", inFile);
1933 
1934  QDomElement streams = doc.createElement("streams");
1935 
1936  root.appendChild(streams);
1937  streams.setAttribute("count", inputFC->nb_streams);
1938  int ffmpegIndex = 0;
1939  uint duration = 0;
1940 
1941  for (uint i = 0; i < inputFC->nb_streams; i++)
1942  {
1943  AVStream *st = inputFC->streams[i];
1944  char buf[256];
1945  AVCodecContext *avctx = gCodecMap->getCodecContext(st);
1946  AVCodecParameters *par = st->codecpar;
1947 
1948  buf[0]=0;
1949  if (avctx)
1950  avcodec_string(buf, sizeof(buf), avctx, false);
1951 
1952  switch (st->codecpar->codec_type)
1953  {
1954  case AVMEDIA_TYPE_VIDEO:
1955  {
1956  QStringList param = QString(buf).split(',', QString::SkipEmptyParts);
1957  QString codec = param[0].remove("Video:", Qt::CaseInsensitive);
1958  QDomElement stream = doc.createElement("video");
1959  stream.setAttribute("streamindex", i);
1960  stream.setAttribute("ffmpegindex", ffmpegIndex++);
1961  stream.setAttribute("codec", codec.trimmed());
1962  stream.setAttribute("width", par->width);
1963  stream.setAttribute("height", par->height);
1964  stream.setAttribute("bitrate", (qlonglong)par->bit_rate);
1965 
1966  float fps;
1967  if (st->r_frame_rate.den && st->r_frame_rate.num)
1968  fps = av_q2d(st->r_frame_rate);
1969  else
1970  fps = 1/av_q2d(st->time_base);
1971 
1972  stream.setAttribute("fps", fps);
1973 
1974  if (par->sample_aspect_ratio.den && par->sample_aspect_ratio.num)
1975  {
1976  float aspect_ratio = av_q2d(par->sample_aspect_ratio);
1977  if (QString(inputFC->iformat->name) != "nuv")
1978  aspect_ratio = ((float)par->width
1979  / par->height) * aspect_ratio;
1980 
1981  stream.setAttribute("aspectratio", aspect_ratio);
1982  }
1983  else
1984  stream.setAttribute("aspectratio", "N/A");
1985 
1986  stream.setAttribute("id", st->id);
1987 
1988  if (st->start_time != (int) AV_NOPTS_VALUE)
1989  {
1990  int secs, us;
1991  secs = st->start_time / AV_TIME_BASE;
1992  us = st->start_time % AV_TIME_BASE;
1993  stream.setAttribute("start_time", QString("%1.%2")
1994  .arg(secs).arg(av_rescale(us, 1000000, AV_TIME_BASE)));
1995  }
1996  else
1997  stream.setAttribute("start_time", 0);
1998 
1999  streams.appendChild(stream);
2000 
2001  // TODO: probably should add a better way to choose which
2002  // video stream we use to calc the duration
2003  if (duration == 0)
2004  {
2005  int64_t frameCount = 0;
2006 
2007  switch (lenMethod)
2008  {
2009  case 0:
2010  {
2011  // use duration guess from avformat
2012  if (inputFC->duration != (uint) AV_NOPTS_VALUE)
2013  {
2014  duration = (uint) (inputFC->duration / AV_TIME_BASE);
2015  root.setAttribute("duration", duration);
2016  LOG(VB_JOBQUEUE, LOG_INFO,
2017  QString("duration = %1") .arg(duration));
2018  frameCount = (int64_t)(duration * fps);
2019  }
2020  else
2021  root.setAttribute("duration", "N/A");
2022  break;
2023  }
2024  case 1:
2025  {
2026  // calc duration of the file by counting the video frames
2027  frameCount = getFrameCount(inputFC, i);
2028  LOG(VB_JOBQUEUE, LOG_INFO,
2029  QString("frames = %1").arg(frameCount));
2030  duration = (uint)(frameCount / fps);
2031  LOG(VB_JOBQUEUE, LOG_INFO,
2032  QString("duration = %1").arg(duration));
2033  root.setAttribute("duration", duration);
2034  break;
2035  }
2036  case 2:
2037  {
2038  // use info from pos map in db
2039  // (only useful if the file is a myth recording)
2040  frameCount = getFrameCount(inFile, fps);
2041  if (frameCount)
2042  {
2043  LOG(VB_JOBQUEUE, LOG_INFO,
2044  QString("frames = %1").arg(frameCount));
2045  duration = (uint)(frameCount / fps);
2046  LOG(VB_JOBQUEUE, LOG_INFO,
2047  QString("duration = %1").arg(duration));
2048  root.setAttribute("duration", duration);
2049  }
2050  else if (inputFC->duration != (uint) AV_NOPTS_VALUE)
2051  {
2052  duration = (uint) (inputFC->duration / AV_TIME_BASE);
2053  root.setAttribute("duration", duration);
2054  LOG(VB_JOBQUEUE, LOG_INFO,
2055  QString("duration = %1").arg(duration));
2056  frameCount = (int64_t)(duration * fps);
2057  }
2058  else
2059  root.setAttribute("duration", "N/A");
2060  break;
2061  }
2062  default:
2063  root.setAttribute("duration", "N/A");
2064  LOG(VB_JOBQUEUE, LOG_ERR,
2065  QString("Unknown lenMethod (%1)")
2066  .arg(lenMethod));
2067  }
2068 
2069  // add duration after all cuts are removed
2070  int64_t cutFrames = getCutFrames(inFile, frameCount);
2071  LOG(VB_JOBQUEUE, LOG_INFO,
2072  QString("cutframes = %1").arg(cutFrames));
2073  int cutduration = (int)(cutFrames / fps);
2074  LOG(VB_JOBQUEUE, LOG_INFO,
2075  QString("cutduration = %1").arg(cutduration));
2076  root.setAttribute("cutduration", duration - cutduration);
2077  }
2078 
2079  break;
2080  }
2081 
2082  case AVMEDIA_TYPE_AUDIO:
2083  {
2084  QStringList param = QString(buf).split(',', QString::SkipEmptyParts);
2085  QString codec = param[0].remove("Audio:", Qt::CaseInsensitive);
2086 
2087  QDomElement stream = doc.createElement("audio");
2088  stream.setAttribute("streamindex", i);
2089  stream.setAttribute("ffmpegindex", ffmpegIndex++);
2090 
2091  // change any streams identified as "liba52" to "AC3" which is what
2092  // the mythburn.py script expects to get.
2093  if (codec.trimmed().toLower() == "liba52")
2094  stream.setAttribute("codec", "AC3");
2095  else
2096  stream.setAttribute("codec", codec.trimmed());
2097 
2098  stream.setAttribute("channels", par->channels);
2099 
2100  AVDictionaryEntry *metatag =
2101  av_dict_get(st->metadata, "language", nullptr, 0);
2102  if (metatag)
2103  stream.setAttribute("language", metatag->value);
2104  else
2105  stream.setAttribute("language", "N/A");
2106 
2107  stream.setAttribute("id", st->id);
2108 
2109  stream.setAttribute("samplerate", par->sample_rate);
2110  stream.setAttribute("bitrate", (qlonglong)par->bit_rate);
2111 
2112  if (st->start_time != (int) AV_NOPTS_VALUE)
2113  {
2114  int secs, us;
2115  secs = st->start_time / AV_TIME_BASE;
2116  us = st->start_time % AV_TIME_BASE;
2117  stream.setAttribute("start_time", QString("%1.%2")
2118  .arg(secs).arg(av_rescale(us, 1000000, AV_TIME_BASE)));
2119  }
2120  else
2121  stream.setAttribute("start_time", 0);
2122 
2123  streams.appendChild(stream);
2124 
2125  break;
2126  }
2127 
2128  case AVMEDIA_TYPE_SUBTITLE:
2129  {
2130  QStringList param = QString(buf).split(',', QString::SkipEmptyParts);
2131  QString codec = param[0].remove("Subtitle:", Qt::CaseInsensitive);
2132 
2133  QDomElement stream = doc.createElement("subtitle");
2134  stream.setAttribute("streamindex", i);
2135  stream.setAttribute("ffmpegindex", ffmpegIndex++);
2136  stream.setAttribute("codec", codec.trimmed());
2137 
2138  AVDictionaryEntry *metatag =
2139  av_dict_get(st->metadata, "language", nullptr, 0);
2140  if (metatag)
2141  stream.setAttribute("language", metatag->value);
2142  else
2143  stream.setAttribute("language", "N/A");
2144 
2145  stream.setAttribute("id", st->id);
2146 
2147  streams.appendChild(stream);
2148 
2149  break;
2150  }
2151 
2152  case AVMEDIA_TYPE_DATA:
2153  {
2154  QDomElement stream = doc.createElement("data");
2155  stream.setAttribute("streamindex", i);
2156  stream.setAttribute("codec", buf);
2157  streams.appendChild(stream);
2158 
2159  break;
2160  }
2161 
2162  default:
2163  LOG(VB_JOBQUEUE, LOG_ERR,
2164  QString("Skipping unsupported codec %1 on stream %2")
2165  .arg(inputFC->streams[i]->codecpar->codec_type).arg(i));
2166  break;
2167  }
2169  }
2170 
2171  // finally save the xml to the file
2172  QFile f(outFile);
2173  if (!f.open(QIODevice::WriteOnly))
2174  {
2175  LOG(VB_JOBQUEUE, LOG_ERR,
2176  "Failed to open file for writing - " + outFile);
2177  return 1;
2178  }
2179 
2180  QTextStream t(&f);
2181  t << doc.toString(4);
2182  f.close();
2183 
2184  return 0;
2185 }
2186 
2187 static int getDBParamters(QString outFile)
2188 {
2190 
2191  // save the db paramters to the file
2192  QFile f(outFile);
2193  if (!f.open(QIODevice::WriteOnly))
2194  {
2195  LOG(VB_GENERAL, LOG_ERR,
2196  QString("MythArchiveHelper: Failed to open file for writing - %1")
2197  .arg(outFile));
2198  return 1;
2199  }
2200 
2201  QTextStream t(&f);
2202  t << params.dbHostName << endl;
2203  t << params.dbUserName << endl;
2204  t << params.dbPassword << endl;
2205  t << params.dbName << endl;
2206  t << gCoreContext->GetHostName() << endl;
2207  t << GetInstallPrefix() << endl;
2208  f.close();
2209 
2210  return 0;
2211 }
2212 
2213 static int isRemote(QString filename)
2214 {
2215  if (filename.startsWith("myth://"))
2216  return 3;
2217 
2218  // check if the file exists
2219  if (!QFile::exists(filename))
2220  return 0;
2221 
2222  struct statfs statbuf;
2223  memset(&statbuf, 0, sizeof(statbuf));
2224 
2225 #if CONFIG_DARWIN
2226  if ((statfs(qPrintable(filename), &statbuf) == 0) &&
2227  ((!strcmp(statbuf.f_fstypename, "nfs")) || // NFS|FTP
2228  (!strcmp(statbuf.f_fstypename, "afpfs")) || // ApplShr
2229  (!strcmp(statbuf.f_fstypename, "smbfs")))) // SMB
2230  return 2;
2231 #elif __linux__
2232  if ((statfs(qPrintable(filename), &statbuf) == 0) &&
2233  ((statbuf.f_type == 0x6969) || // NFS
2234  (statbuf.f_type == 0x517B))) // SMB
2235  return 2;
2236 #endif
2237 
2238  return 1;
2239 }
2240 
2242 {
2243  public:
2245  void LoadArguments(void) override; // MythCommandLineParser
2246 };
2247 
2249  MythCommandLineParser("mytharchivehelper")
2250 { LoadArguments(); }
2251 
2253 {
2254  addHelp();
2255  addVersion();
2256  addLogging();
2257 
2258  add(QStringList{"-t", "--createthumbnail"},
2259  "createthumbnail", false,
2260  "Create one or more thumbnails\n"
2261  "Requires: --infile, --thumblist, --outfile\n"
2262  "Optional: --framecount", "");
2263  add("--infile", "infile", "",
2264  "Input file name\n"
2265  "Used with: --createthumbnail, --getfileinfo, --isremote, "
2266  "--sup2dast, --importarchive", "");
2267  add("--outfile", "outfile", "",
2268  "Output file name\n"
2269  "Used with: --createthumbnail, --getfileinfo, --getdbparameters, "
2270  "--nativearchive\n"
2271  "When used with --createthumbnail: eg 'thumb%1-%2.jpg'\n"
2272  " %1 will be replaced with the no. of the thumb\n"
2273  " %2 will be replaced with the frame no.", "");
2274  add("--thumblist", "thumblist", "",
2275  "Comma-separated list of required thumbs (in seconds)\n"
2276  "Used with: --createthumbnail","");
2277  add("--framecount", "framecount", 1,
2278  "Number of frames to grab (default 1)\n"
2279  "Used with: --createthumbnail", "");
2280 
2281  add(QStringList{"-i", "--getfileinfo"},
2282  "getfileinfo", false,
2283  "Write file info about infile to outfile\n"
2284  "Requires: --infile, --outfile, --method", "");
2285  add("--method", "method", 0,
2286  "Method of file duration calculation\n"
2287  "Used with: --getfileinfo\n"
2288  " 0 = use av_estimate_timings() (quick but not very accurate - "
2289  "default)\n"
2290  " 1 = read all frames (most accurate but slow)\n"
2291  " 2 = use position map in DB (quick, only works for MythTV "
2292  "recordings)", "");
2293 
2294  add(QStringList{"-p", "--getdbparameters"},
2295  "getdbparameters", false,
2296  "Write the mysql database parameters to outfile\n"
2297  "Requires: --outfile", "");
2298 
2299  add(QStringList{"-n", "--nativearchive"},
2300  "nativearchive", false,
2301  "Archive files to a native archive format\n"
2302  "Requires: --outfile", "");
2303 
2304  add(QStringList{"-f", "--importarchive"},
2305  "importarchive", false,
2306  "Import an archived file\n"
2307  "Requires: --infile, --chanid", "");
2308  add("--chanid", "chanid", -1,
2309  "Channel ID to use when inserting records in DB\n"
2310  "Used with: --importarchive", "");
2311 
2312  add(QStringList{"-r", "--isremote"},
2313  "isremote", false,
2314  "Check if infile is on a remote filesystem\n"
2315  "Requires: --infile\n"
2316  "Returns: 0 on error or file not found\n"
2317  " - 1 file is on a local filesystem\n"
2318  " - 2 file is on a remote filesystem", "");
2319 
2320  add(QStringList{"-b", "--burndvd"},
2321  "burndvd", false,
2322  "Burn a created DVD to a blank disc\n"
2323  "Optional: --mediatype, --erasedvdrw, --nativeformat", "");
2324  add("--mediatype", "mediatype", 0,
2325  "Type of media to burn\n"
2326  "Used with: --burndvd\n"
2327  " 0 = single layer DVD (default)\n"
2328  " 1 = dual layer DVD\n"
2329  " 2 = rewritable DVD", "");
2330  add("--erasedvdrw", "erasedvdrw", false,
2331  "Force an erase of DVD-R/W Media\n"
2332  "Used with: --burndvd (optional)", "");
2333  add("--nativeformat", "nativeformat", false,
2334  "Archive is a native archive format\n"
2335  "Used with: --burndvd (optional)", "");
2336 
2337  add(QStringList{"-s", "--sup2dast"},
2338  "sup2dast", false,
2339  "Convert projectX subtitles to DVD subtitles\n"
2340  "Requires: --infile, --ifofile, --delay", "");
2341  add("--ifofile", "ifofile", "",
2342  "Filename of ifo file\n"
2343  "Used with: --sup2dast", "");
2344  add("--delay", "delay", 0,
2345  "Delay in ms to add to subtitles (default 0)\n"
2346  "Used with: --sup2dast", "");
2347 }
2348 
2349 
2350 
2351 int main(int argc, char **argv)
2352 {
2354  if (!cmdline.Parse(argc, argv))
2355  {
2356  cmdline.PrintHelp();
2358  }
2359 
2360  if (cmdline.toBool("showhelp"))
2361  {
2362  cmdline.PrintHelp();
2363  return GENERIC_EXIT_OK;
2364  }
2365 
2366  if (cmdline.toBool("showversion"))
2367  {
2369  return GENERIC_EXIT_OK;
2370  }
2371 
2372  QCoreApplication a(argc, argv);
2373  QCoreApplication::setApplicationName("mytharchivehelper");
2374 
2375  // by default we only output our messages
2376  int retval;
2377  QString mask("jobqueue");
2378  if ((retval = cmdline.ConfigureLogging(mask)) != GENERIC_EXIT_OK)
2379  return retval;
2380 
2382  // Don't listen to console input
2383  close(0);
2384 
2386  if (!gContext->Init(false))
2387  {
2388  LOG(VB_GENERAL, LOG_ERR, "Failed to init MythContext, exiting.");
2389  delete gContext;
2390  gContext = nullptr;
2392  }
2393 
2394  int res = 0;
2395  bool bGrabThumbnail = cmdline.toBool("createthumbnail");
2396  bool bGetDBParameters = cmdline.toBool("getdbparameters");
2397  bool bNativeArchive = cmdline.toBool("nativearchive");
2398  bool bImportArchive = cmdline.toBool("importarchive");
2399  bool bGetFileInfo = cmdline.toBool("getfileinfo");
2400  bool bIsRemote = cmdline.toBool("isremote");
2401  bool bDoBurn = cmdline.toBool("burndvd");
2402  bool bEraseDVDRW = cmdline.toBool("erasedvdrw");
2403  bool bNativeFormat = cmdline.toBool("nativeformat");;
2404  bool bSup2Dast = cmdline.toBool("sup2dast");
2405 
2406  QString thumbList = cmdline.toString("thumblist");
2407  QString inFile = cmdline.toString("infile");
2408  QString outFile = cmdline.toString("outfile");
2409  QString ifoFile = cmdline.toString("ifofile");
2410 
2411  int mediaType = cmdline.toUInt("mediatype");
2412  int lenMethod = cmdline.toUInt("method");
2413  int chanID = cmdline.toInt("chanid");
2414  int frameCount = cmdline.toUInt("framecount");
2415  int delay = cmdline.toUInt("delay");
2416 
2417  // Check command line arguments
2418  if (bGrabThumbnail)
2419  {
2420  if (inFile.isEmpty())
2421  {
2422  LOG(VB_GENERAL, LOG_ERR, "Missing --infile in -t/--grabthumbnail "
2423  "option");
2425  }
2426 
2427  if (thumbList.isEmpty())
2428  {
2429  LOG(VB_GENERAL, LOG_ERR, "Missing --thumblist in -t/--grabthumbnail"
2430  " option");
2432  }
2433 
2434  if (outFile.isEmpty())
2435  {
2436  LOG(VB_GENERAL, LOG_ERR, "Missing --outfile in -t/--grabthumbnail "
2437  "option");
2439  }
2440  }
2441 
2442  if (bGetDBParameters)
2443  {
2444  if (outFile.isEmpty())
2445  {
2446  LOG(VB_GENERAL, LOG_ERR, "Missing argument to -p/--getdbparameters "
2447  "option");
2449  }
2450  }
2451 
2452  if (bIsRemote)
2453  {
2454  if (inFile.isEmpty())
2455  {
2456  LOG(VB_GENERAL, LOG_ERR,
2457  "Missing argument to -r/--isremote option");
2459  }
2460  }
2461 
2462  if (bDoBurn)
2463  {
2464  if (mediaType < 0 || mediaType > 2)
2465  {
2466  LOG(VB_GENERAL, LOG_ERR, QString("Invalid mediatype given: %1")
2467  .arg(mediaType));
2469  }
2470  }
2471 
2472  if (bNativeArchive)
2473  {
2474  if (outFile.isEmpty())
2475  {
2476  LOG(VB_GENERAL, LOG_ERR, "Missing argument to -n/--nativearchive "
2477  "option");
2479  }
2480  }
2481 
2482  if (bImportArchive)
2483  {
2484  if (inFile.isEmpty())
2485  {
2486  LOG(VB_GENERAL, LOG_ERR, "Missing --infile argument to "
2487  "-f/--importarchive option");
2489  }
2490  }
2491 
2492  if (bGetFileInfo)
2493  {
2494  if (inFile.isEmpty())
2495  {
2496  LOG(VB_GENERAL, LOG_ERR, "Missing --infile in -i/--getfileinfo "
2497  "option");
2499  }
2500 
2501  if (outFile.isEmpty())
2502  {
2503  LOG(VB_GENERAL, LOG_ERR, "Missing --outfile in -i/--getfileinfo "
2504  "option");
2506  }
2507  }
2508 
2509  if (bSup2Dast)
2510  {
2511  if (inFile.isEmpty())
2512  {
2513  LOG(VB_GENERAL, LOG_ERR,
2514  "Missing --infile in -s/--sup2dast option");
2516  }
2517 
2518  if (ifoFile.isEmpty())
2519  {
2520  LOG(VB_GENERAL, LOG_ERR,
2521  "Missing --ifofile in -s/--sup2dast option");
2523  }
2524  }
2525 
2526  if (bGrabThumbnail)
2527  res = grabThumbnail(inFile, thumbList, outFile, frameCount);
2528  else if (bGetDBParameters)
2529  res = getDBParamters(outFile);
2530  else if (bNativeArchive)
2531  res = doNativeArchive(outFile);
2532  else if (bImportArchive)
2533  res = doImportArchive(inFile, chanID);
2534  else if (bGetFileInfo)
2535  res = getFileInfo(inFile, outFile, lenMethod);
2536  else if (bIsRemote)
2537  res = isRemote(inFile);
2538  else if (bDoBurn)
2539  res = doBurnDVD(mediaType, bEraseDVDRW, bNativeFormat);
2540  else if (bSup2Dast)
2541  {
2542  QByteArray inFileBA = inFile.toLocal8Bit();
2543  QByteArray ifoFileBA = ifoFile.toLocal8Bit();
2544  res = sup2dast(inFileBA.constData(), ifoFileBA.constData(), delay);
2545  }
2546  else
2547  cmdline.PrintHelp();
2548 
2549  delete gContext;
2550  gContext = nullptr;
2551 
2552  exit(res);
2553 }
2554 
2555 
bool next(void)
Wrap QSqlQuery::next() so we can display the query results.
Definition: mythdbcon.cpp:794
Startup context for MythTV.
Definition: mythcontext.h:42
static int isRemote(QString filename)
CommandLineArg * add(QString arg, QString name, bool def, QString help, QString longhelp)
void bindValue(const QString &placeholder, const QVariant &val)
Definition: mythdbcon.cpp:875
QString dbName
database name
Definition: mythdbparams.h:26
static bool createISOImage(QString &sourceDirectory)
QString GenMythURL(QString host=QString(), QString port=QString(), QString path=QString(), QString storageGroup=QString())
#define GENERIC_EXIT_OK
Exited with no error.
Definition: exitcodes.h:10
int Copy(VideoFrame *dst, const VideoFrame *src)
Definition: mythavutil.cpp:196
#define MPUBLIC
Definition: mythexp.h:10
bool IsVideo(void) const
Definition: programinfo.h:478
bool extractDetailsFromFilename(const QString &inFile, QString &chanID, QString &startTime)
QString getTempDirectory(bool showError)
Definition: archiveutil.cpp:71
QString getBaseName(const QString &filename)
void SaveSetting(const QString &key, int newValue)
int GetMasterServerPort(void)
Returns the Master Backend control port If no master server port has been defined in the database,...
struct AVFrame AVFrame
QSqlQuery wrapper that fetches a DB connection from the connection pool.
Definition: mythdbcon.h:125
MythAVCopy Copy AVFrame<->frame, performing the required conversion if any.
Definition: mythavutil.h:114
void PrintHelp(void) const
Print command line option help.
void freeCodecContext(const AVStream *)
Definition: mythavutil.cpp:427
bool QueryCutList(frm_dir_map_t &, bool loadAutosave=false) const
QString GetInstallPrefix(void)
Definition: mythdirs.cpp:220
int size(void) const
Definition: mythdbcon.h:187
Parent class for defining application command line parsers.
QMap< uint64_t, MarkTypes > frm_dir_map_t
Frame # -> Mark map.
Definition: programtypes.h:81
unsigned int uint
Definition: compat.h:140
MythCoreContext * gCoreContext
This global variable contains the MythCoreContext instance for the app.
Definition: lang.c:20
void addVersion(void)
Canned argument definition for –version.
void addLogging(const QString &defaultVerbosity="general", LogLevel_t defaultLogLevel=LOG_INFO)
Canned argument definition for all logging options, including –verbose, –logpath, –quiet,...
void addHelp(void)
Canned argument definition for –help.
DatabaseParams GetDatabaseParams(void)
bool toBool(QString key) const
Returns stored QVariant as a boolean.
static QString fixFilename(const QString &filename)
MythContext * gContext
This global variable contains the MythContext instance for the application.
Definition: mythcontext.cpp:63
static guint32 * tmp
Definition: goom_core.c:35
QString dbPassword
DB password.
Definition: mythdbparams.h:25
#define IMAGE_ALIGN
Definition: mythconfig.h:19
QVariant value(int i) const
Definition: mythdbcon.h:182
static int doNativeArchive(const QString &jobFile)
Holds information on recordings and videos.
Definition: programinfo.h:66
QString dbUserName
DB user name.
Definition: mythdbparams.h:24
int statfs(const char *path, struct statfs *buffer)
Definition: compat.h:170
bool Init(const bool gui=true, const bool promptForBackend=false, const bool bypassAutoDiscovery=false, const bool ignoreDB=false)
def rating(profile, smoonURL, gate)
Definition: scan.py:25
#define close
Definition: compat.h:16
MythCodecMap * gCodecMap
This global variable contains the MythCodecMap instance for the app.
Definition: mythavutil.cpp:377
static int doImportArchive(const QString &inFile, int chanID)
static int grabThumbnail(QString inFile, QString thumbList, QString outFile, int frameCount)
QDateTime current(bool stripped)
Returns current Date and Time in UTC.
Definition: mythdate.cpp:10
QString GetSetting(const QString &key, const QString &defaultval="")
int doImportArchive(const QString &xmlFile, int chanID)
int exportVideo(QDomElement &itemNode, const QString &saveDirectory)
unsigned char t
Definition: ParseText.cpp:340
static int64_t getFrameCount(AVFormatContext *inputFC, int vid_id)
bool isActive(void) const
Definition: mythdbcon.h:188
int DeinterlaceSingle(AVFrame *dst, const AVFrame *src)
Definition: mythavutil.cpp:303
QString GetMasterHostName(void)
Definition: compat.h:158
int importRecording(const QDomElement &itemNode, const QString &xmlFile, int chanID)
QString toString(QString key) const
Returns stored QVariant as a QString, falling to default if not provided.
static MSqlQueryInfo InitCon(ConnectionReuse=kNormalConnection)
Only use this in combination with MSqlQuery constructor.
Definition: mythdbcon.cpp:547
static int64_t getCutFrames(const QString &filename, int64_t lastFrame)
MythCommFlagCommandLineParser cmdline
static int getDBParamters(QString outFile)
uint myth_system(const QString &command, uint flags, uint timeout)
#define ENO
This can be appended to the LOG args with "+".
Definition: mythlogging.h:99
static int burnISOImage(int mediaType, bool bEraseDVDRW, bool nativeFormat)
AVCodecContext * getCodecContext(const AVStream *, const AVCodec *pCodec=nullptr, bool nullCodec=false)
Definition: mythavutil.cpp:388
QString toString(const QDateTime &raw_dt, uint format)
Returns formatted string representing the time.
Definition: mythdate.cpp:101
int main(int argc, char **argv)
bool copyFile(const QString &source, const QString &destination)
int importVideo(const QDomElement &itemNode, const QString &xmlFile)
MythPictureDeinterlacer simple deinterlacer based on FFmpeg's yadif filter.
Definition: mythavutil.h:168
int GetNumSetting(const QString &key, int defaultval=0)
QString findNodeText(const QDomElement &elem, const QString &nodeName)
bool prepare(const QString &query)
QSqlQuery::prepare() is not thread safe in Qt <= 3.3.2.
Definition: mythdbcon.cpp:819
Structure containing the basic Database parameters.
Definition: mythdbparams.h:9
#define LOG(_MASK_, _LEVEL_, _STRING_)
Definition: mythlogging.h:41
QString dbHostName
database server
Definition: mythdbparams.h:21
string dbVersion
Definition: mythburn.py:143
virtual bool Parse(int argc, const char *const *argv)
Loop through argv and populate arguments with values.
int getFieldList(QStringList &fieldList, const QString &tableName)
#define MYTH_BINARY_VERSION
Update this whenever the plug-in ABI changes.
Definition: mythversion.h:16
Default UTC, database format.
Definition: mythdate.h:24
const char * frames[3]
Definition: element.c:46
int exportRecording(QDomElement &itemNode, const QString &saveDirectory)
int toInt(QString key) const
Returns stored QVariant as an integer, falling to default if not provided.
static int doBurnDVD(int mediaType, bool bEraseDVDRW, bool nativeFormat)
MythAVFrame little utility class that act as a safe way to allocate an AVFrame which can then be allo...
Definition: mythavutil.h:42
bool MythRemoveDirectory(QDir &aDir)
int sup2dast(const char *supfile, const char *ifofile, int delay_ms)
Definition: pxsup2dast.c:878
bool exec(void)
Wrap QSqlQuery::exec() so we can display SQL.
Definition: mythdbcon.cpp:615
int ConfigureLogging(QString mask="general", unsigned int progress=0)
Read in logging options and initialize the logging interface.
ProgramInfo * getProgramInfoForFile(const QString &inFile)
#define GENERIC_EXIT_NO_MYTHCONTEXT
No MythContext available.
Definition: exitcodes.h:13
static void DBError(const QString &where, const MSqlQuery &query)
Definition: mythdb.cpp:179
QMap< long long, long long > frm_pos_map_t
Frame # -> File offset map.
Definition: programtypes.h:46
#define GENERIC_EXIT_INVALID_CMDLINE
Command line parse error.
Definition: exitcodes.h:15
void QueryPositionMap(frm_pos_map_t &, MarkTypes type) const
static int getFileInfo(QString inFile, QString outFile, int lenMethod)
QString GetHostName(void)
void PrintVersion(void) const
Print application version information.
uint toUInt(QString key) const
Returns stored QVariant as an unsigned integer, falling to default if not provided.