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)
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)
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  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;
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  LOG(VB_JOBQUEUE, LOG_ERR,
1304  "Couldn't add genre to database");
1305  continue;
1306  }
1307  genreID = query.value(0).toInt();
1308  }
1309 
1310  // now link the genre to the videometadata
1311  query.prepare("INSERT INTO videometadatagenre (idvideo, idgenre)"
1312  "VALUES (:IDVIDEO, :IDGENRE);");
1313  query.bindValue(":IDVIDEO", intid);
1314  query.bindValue(":IDGENRE", genreID);
1315  if (!query.exec())
1316  MythDB::DBError("NativeArchive::importVideo - "
1317  "insert videometadatagenre", query);
1318  }
1319 
1320  LOG(VB_JOBQUEUE, LOG_INFO, "Inserted genre details into database");
1321  }
1322  }
1323 
1324  // copy country to db
1325  nodeList = itemNode.elementsByTagName("countries");
1326  if (nodeList.count() < 1)
1327  {
1328  LOG(VB_JOBQUEUE, LOG_INFO, "No 'countries' element found in XML file");
1329  }
1330  else
1331  {
1332  n = nodeList.item(0);
1333  QDomElement countriesNode = n.toElement();
1334 
1335  nodeList = countriesNode.elementsByTagName("country");
1336  if (nodeList.count() < 1)
1337  {
1338  LOG(VB_JOBQUEUE, LOG_WARNING,
1339  "Couldn't find any 'country' elements in XML file");
1340  }
1341  else
1342  {
1343  for (int x = 0; x < nodeList.count(); x++)
1344  {
1345  n = nodeList.item(x);
1346  QDomElement e = n.toElement();
1347  int countryID;
1348  QString country = e.attribute("country");
1349 
1350  // see if this country already exists
1351  query.prepare("SELECT intid FROM videocountry "
1352  "WHERE country = :COUNTRY");
1353  query.bindValue(":COUNTRY", country);
1354  if (query.exec() && query.next())
1355  {
1356  countryID = query.value(0).toInt();
1357  }
1358  else
1359  {
1360  // country doesn't exist so add it
1361  query.prepare("INSERT INTO videocountry (country) VALUES(:COUNTRY);");
1362  query.bindValue(":COUNTRY", country);
1363  if (!query.exec())
1364  MythDB::DBError("NativeArchive::importVideo - "
1365  "insert videocountry", query);
1366 
1367  // get new intid of country
1368  query.prepare("SELECT intid FROM videocountry "
1369  "WHERE country = :COUNTRY");
1370  query.bindValue(":COUNTRY", country);
1371  if (!query.exec() || !query.next())
1372  {
1373  LOG(VB_JOBQUEUE, LOG_ERR,
1374  "Couldn't add country to database");
1375  continue;
1376  }
1377  countryID = query.value(0).toInt();
1378  }
1379 
1380  // now link the country to the videometadata
1381  query.prepare("INSERT INTO videometadatacountry (idvideo, idcountry)"
1382  "VALUES (:IDVIDEO, :IDCOUNTRY);");
1383  query.bindValue(":IDVIDEO", intid);
1384  query.bindValue(":IDCOUNTRY", countryID);
1385  if (!query.exec())
1386  MythDB::DBError("NativeArchive::importVideo - "
1387  "insert videometadatacountry", query);
1388  }
1389 
1390  LOG(VB_JOBQUEUE, LOG_INFO,
1391  "Inserted country details into database");
1392  }
1393  }
1394 
1395  // fix the category id
1396  nodeList = itemNode.elementsByTagName("category");
1397  if (nodeList.count() < 1)
1398  {
1399  LOG(VB_JOBQUEUE, LOG_ERR, "No 'category' element found in XML file");
1400  }
1401  else
1402  {
1403  n = nodeList.item(0);
1404  QDomElement e = n.toElement();
1405  int categoryID;
1406  QString category = e.attribute("category");
1407  // see if this category already exists
1408  query.prepare("SELECT intid FROM videocategory "
1409  "WHERE category = :CATEGORY");
1410  query.bindValue(":CATEGORY", category);
1411  if (query.exec() && query.next())
1412  {
1413  categoryID = query.value(0).toInt();
1414  }
1415  else
1416  {
1417  // category doesn't exist so add it
1418  query.prepare("INSERT INTO videocategory (category) VALUES(:CATEGORY);");
1419  query.bindValue(":CATEGORY", category);
1420  if (!query.exec())
1421  MythDB::DBError("NativeArchive::importVideo - "
1422  "insert videocategory", query);
1423 
1424  // get new intid of category
1425  query.prepare("SELECT intid FROM videocategory "
1426  "WHERE category = :CATEGORY");
1427  query.bindValue(":CATEGORY", category);
1428  if (query.exec() && query.next())
1429  {
1430  categoryID = query.value(0).toInt();
1431  }
1432  else
1433  {
1434  LOG(VB_JOBQUEUE, LOG_ERR, "Couldn't add category to database");
1435  categoryID = 0;
1436  }
1437  }
1438 
1439  // now fix the categoryid in the videometadata
1440  query.prepare("UPDATE videometadata "
1441  "SET category = :CATEGORY "
1442  "WHERE intid = :INTID;");
1443  query.bindValue(":CATEGORY", categoryID);
1444  query.bindValue(":INTID", intid);
1445  if (!query.exec())
1446  MythDB::DBError("NativeArchive::importVideo - "
1447  "update category", query);
1448 
1449  LOG(VB_JOBQUEUE, LOG_INFO, "Fixed the category in the database");
1450  }
1451 
1452  LOG(VB_JOBQUEUE, LOG_INFO, "Import completed OK");
1453 
1454  return 0;
1455 }
1456 
1457 QString NativeArchive::findNodeText(const QDomElement &elem, const QString &nodeName)
1458 {
1459  QDomNodeList nodeList = elem.elementsByTagName(nodeName);
1460  if (nodeList.count() < 1)
1461  {
1462  LOG(VB_GENERAL, LOG_ERR,
1463  QString("Couldn't find a '%1' element in XML file") .arg(nodeName));
1464  return "";
1465  }
1466 
1467  QDomNode n = nodeList.item(0);
1468  QDomElement e = n.toElement();
1469  QString res = "";
1470 
1471  for (QDomNode node = e.firstChild(); !node.isNull();
1472  node = node.nextSibling())
1473  {
1474  QDomText t = node.toText();
1475  if (!t.isNull())
1476  {
1477  res = t.data();
1478  break;
1479  }
1480  }
1481 
1482  // some fixups
1483  // FIXME could be a lot smarter
1484  if (nodeName == "recgroup")
1485  {
1486  res = "Default";
1487  }
1488  else if (nodeName == "recordid")
1489  {
1490  res = "";
1491  }
1492  else if (nodeName == "seriesid")
1493  {
1494  res = "";
1495  }
1496  else if (nodeName == "programid")
1497  {
1498  res = "";
1499  }
1500  else if (nodeName == "playgroup")
1501  {
1502  res = "Default";
1503  }
1504  else if (nodeName == "profile")
1505  {
1506  res = "";
1507  }
1508 
1509  return res;
1510 }
1511 
1512 static void clearArchiveTable(void)
1513 {
1514  MSqlQuery query(MSqlQuery::InitCon());
1515  query.prepare("DELETE FROM archiveitems;");
1516 
1517  if (!query.exec())
1518  MythDB::DBError("delete archiveitems", query);
1519 }
1520 
1521 static int doNativeArchive(const QString &jobFile)
1522 {
1523  gCoreContext->SaveSetting("MythArchiveLastRunType", "Native Export");
1525  "MythArchiveLastRunStart",
1527  gCoreContext->SaveSetting("MythArchiveLastRunStatus", "Running");
1528 
1529  NativeArchive na;
1530  int res = na.doNativeArchive(jobFile);
1532  "MythArchiveLastRunEnd",
1534  gCoreContext->SaveSetting("MythArchiveLastRunStatus",
1535  (res == 0 ? "Success" : "Failed"));
1536 
1537  // clear the archiveitems table if succesful
1538  if (res == 0)
1540 
1541  return res;
1542 }
1543 
1544 static int doImportArchive(const QString &inFile, int chanID)
1545 {
1546  NativeArchive na;
1547  return na.doImportArchive(inFile, chanID);
1548 }
1549 
1550 static int grabThumbnail(const QString& inFile, const QString& thumbList, const QString& outFile, int frameCount)
1551 {
1552  // Open recording
1553  LOG(VB_JOBQUEUE, LOG_INFO, QString("grabThumbnail(): Opening '%1'")
1554  .arg(inFile));
1555 
1556  RemoteAVFormatContext inputFC(inFile);
1557  if (!inputFC.isOpen())
1558  {
1559  LOG(VB_JOBQUEUE, LOG_ERR, "grabThumbnail(): Couldn't open input file" +
1560  ENO);
1561  return 1;
1562  }
1563 
1564  // Getting stream information
1565  int ret = avformat_find_stream_info(inputFC, nullptr);
1566  if (ret < 0)
1567  {
1568  LOG(VB_JOBQUEUE, LOG_ERR,
1569  QString("Couldn't get stream info, error #%1").arg(ret));
1570  return 1;
1571  }
1572 
1573  // find the first video stream
1574  int videostream = -1, width, height;
1575  float fps;
1576 
1577  for (uint i = 0; i < inputFC->nb_streams; i++)
1578  {
1579  AVStream *st = inputFC->streams[i];
1580  if (inputFC->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO)
1581  {
1582  videostream = i;
1583  width = st->codecpar->width;
1584  height = st->codecpar->height;
1585  if (st->r_frame_rate.den && st->r_frame_rate.num)
1586  fps = av_q2d(st->r_frame_rate);
1587  else
1588  fps = 1/av_q2d(st->time_base);
1589  break;
1590  }
1591  }
1592 
1593  if (videostream == -1)
1594  {
1595  LOG(VB_JOBQUEUE, LOG_ERR, "Couldn't find a video stream");
1596  return 1;
1597  }
1598 
1599  // get the codec context for the video stream
1600  AVCodecContext *codecCtx = gCodecMap->getCodecContext
1601  (inputFC->streams[videostream]);
1602 
1603  // get decoder for video stream
1604  AVCodec * codec = avcodec_find_decoder(codecCtx->codec_id);
1605 
1606  if (codec == nullptr)
1607  {
1608  LOG(VB_JOBQUEUE, LOG_ERR, "Couldn't find codec for video stream");
1609  return 1;
1610  }
1611 
1612  // open codec
1613  if (avcodec_open2(codecCtx, codec, nullptr) < 0)
1614  {
1615  LOG(VB_JOBQUEUE, LOG_ERR, "Couldn't open codec for video stream");
1616  return 1;
1617  }
1618 
1619  // get list of required thumbs
1620  QStringList list = thumbList.split(",", QString::SkipEmptyParts);
1621  MythAVFrame frame;
1622  if (!frame)
1623  {
1624  return 1;
1625  }
1626  AVPacket pkt;
1627  AVFrame orig;
1628  AVFrame retbuf;
1629  memset(&orig, 0, sizeof(AVFrame));
1630  memset(&retbuf, 0, sizeof(AVFrame));
1631  MythAVCopy copyframe;
1632  MythPictureDeinterlacer deinterlacer(codecCtx->pix_fmt, width, height);
1633 
1634  int bufflen = width * height * 4;
1635  unsigned char *outputbuf = new unsigned char[bufflen];
1636 
1637  int frameNo = -1, thumbCount = 0;
1638  bool frameFinished = false;
1639  int keyFrame;
1640 
1641  while (av_read_frame(inputFC, &pkt) >= 0)
1642  {
1643  if (pkt.stream_index == videostream)
1644  {
1645  frameNo++;
1646  if (list[thumbCount].toInt() == (int)(frameNo / fps))
1647  {
1648  thumbCount++;
1649 
1650  avcodec_flush_buffers(codecCtx);
1651  av_frame_unref(frame);
1652  frameFinished = false;
1653  ret = avcodec_receive_frame(codecCtx, frame);
1654  if (ret == 0)
1655  frameFinished = true;
1656  if (ret == 0 || ret == AVERROR(EAGAIN))
1657  avcodec_send_packet(codecCtx, &pkt);
1658  keyFrame = frame->key_frame;
1659 
1660  while (!frameFinished || !keyFrame)
1661  {
1662  av_packet_unref(&pkt);
1663  int res = av_read_frame(inputFC, &pkt);
1664  if (res < 0)
1665  break;
1666  if (pkt.stream_index == videostream)
1667  {
1668  frameNo++;
1669  av_frame_unref(frame);
1670  ret = avcodec_receive_frame(codecCtx, frame);
1671  if (ret == 0)
1672  frameFinished = true;
1673  if (ret == 0 || ret == AVERROR(EAGAIN))
1674  avcodec_send_packet(codecCtx, &pkt);
1675  keyFrame = frame->key_frame;
1676  }
1677  }
1678 
1679  if (frameFinished)
1680  {
1681  // work out what format to save to
1682  QString saveFormat = "JPEG";
1683  if (outFile.right(4) == ".png")
1684  saveFormat = "PNG";
1685 
1686  int count = 0;
1687  while (count < frameCount)
1688  {
1689  QString filename = outFile;
1690  if (filename.contains("%1") && filename.contains("%2"))
1691  filename = filename.arg(thumbCount).arg(count+1);
1692  else if (filename.contains("%1"))
1693  filename = filename.arg(thumbCount);
1694 
1695  av_image_fill_arrays(retbuf.data, retbuf.linesize, outputbuf,
1696  AV_PIX_FMT_RGB32, width, height, IMAGE_ALIGN);
1697 
1698  AVFrame *tmp = frame;
1699  deinterlacer.DeinterlaceSingle(tmp, tmp);
1700 
1701  copyframe.Copy(&retbuf, AV_PIX_FMT_RGB32, tmp,
1702  codecCtx->pix_fmt, width, height);
1703 
1704  QImage img(outputbuf, width, height,
1705  QImage::Format_RGB32);
1706 
1707  if (!img.save(filename, qPrintable(saveFormat)))
1708  {
1709  LOG(VB_GENERAL, LOG_ERR,
1710  QString("grabThumbnail(): Failed to save "
1711  "thumb: '%1'")
1712  .arg(filename));
1713  }
1714 
1715  count++;
1716 
1717  if (count <= frameCount)
1718  {
1719  //grab next frame
1720  frameFinished = false;
1721  while (!frameFinished)
1722  {
1723  int res = av_read_frame(inputFC, &pkt);
1724  if (res < 0)
1725  break;
1726  if (pkt.stream_index == videostream)
1727  {
1728  frameNo++;
1729  ret = avcodec_receive_frame(codecCtx, frame);
1730  if (ret == 0)
1731  frameFinished = true;
1732  if (ret == 0 || ret == AVERROR(EAGAIN))
1733  avcodec_send_packet(codecCtx, &pkt);
1734  }
1735  }
1736  }
1737  }
1738  }
1739 
1740  if (thumbCount >= list.count())
1741  break;
1742  }
1743  }
1744 
1745  av_packet_unref(&pkt);
1746  }
1747 
1748  delete[] outputbuf;
1749 
1750  // close the codec
1752  (inputFC->streams[videostream]);
1753 
1754  return 0;
1755 }
1756 
1757 static int64_t getFrameCount(AVFormatContext *inputFC, int vid_id)
1758 {
1759  AVPacket pkt;
1760  int64_t count = 0;
1761 
1762  LOG(VB_JOBQUEUE, LOG_INFO, "Calculating frame count");
1763 
1764  av_init_packet(&pkt);
1765 
1766  while (av_read_frame(inputFC, &pkt) >= 0)
1767  {
1768  if (pkt.stream_index == vid_id)
1769  {
1770  count++;
1771  }
1772  av_packet_unref(&pkt);
1773  }
1774 
1775  return count;
1776 }
1777 
1778 static int64_t getCutFrames(const QString &filename, int64_t lastFrame)
1779 {
1780  // only wont the filename
1781  QString basename = filename;
1782  int pos = filename.lastIndexOf('/');
1783  if (pos > 0)
1784  basename = filename.mid(pos + 1);
1785 
1786  ProgramInfo *progInfo = getProgramInfoForFile(basename);
1787  if (!progInfo)
1788  return 0;
1789 
1790  if (progInfo->IsVideo())
1791  {
1792  delete progInfo;
1793  return 0;
1794  }
1795 
1796  frm_dir_map_t cutlist;
1797  frm_dir_map_t::iterator it;
1798  uint64_t frames = 0;
1799 
1800  progInfo->QueryCutList(cutlist);
1801 
1802  if (cutlist.empty())
1803  {
1804  delete progInfo;
1805  return 0;
1806  }
1807 
1808  for (it = cutlist.begin(); it != cutlist.end();)
1809  {
1810  uint64_t start = 0, end = 0;
1811 
1812  if (it.value() == MARK_CUT_START)
1813  {
1814  start = it.key();
1815  ++it;
1816  if (it != cutlist.end())
1817  {
1818  end = it.key();
1819  ++it;
1820  }
1821  else
1822  end = lastFrame;
1823  }
1824  else if (it.value() == MARK_CUT_END)
1825  {
1826  start = 0;
1827  end = it.key();
1828  ++it;
1829  }
1830  else
1831  {
1832  ++it;
1833  continue;
1834  }
1835 
1836  frames += end - start;
1837  }
1838 
1839  delete progInfo;
1840  return frames;
1841 }
1842 
1843 static int64_t getFrameCount(const QString &filename, float fps)
1844 {
1845  // only wont the filename
1846  QString basename = filename;
1847  int pos = filename.lastIndexOf('/');
1848  if (pos > 0)
1849  basename = filename.mid(pos + 1);
1850 
1851  int keyframedist = -1;
1852  frm_pos_map_t posMap;
1853 
1854  ProgramInfo *progInfo = getProgramInfoForFile(basename);
1855  if (!progInfo)
1856  return 0;
1857 
1858  progInfo->QueryPositionMap(posMap, MARK_GOP_BYFRAME);
1859  if (!posMap.empty())
1860  {
1861  keyframedist = 1;
1862  }
1863  else
1864  {
1865  progInfo->QueryPositionMap(posMap, MARK_GOP_START);
1866  if (!posMap.empty())
1867  {
1868  keyframedist = 15;
1869  if (fps < 26 && fps > 24)
1870  keyframedist = 12;
1871  }
1872  else
1873  {
1874  progInfo->QueryPositionMap(posMap, MARK_KEYFRAME);
1875  if (!posMap.empty())
1876  {
1877  // keyframedist should be set in the fileheader so no
1878  // need to try to determine it in this case
1879  delete progInfo;
1880  return 0;
1881  }
1882  }
1883  }
1884 
1885  delete progInfo;
1886  if (posMap.empty())
1887  return 0; // no position map in recording
1888 
1889  frm_pos_map_t::const_iterator it = posMap.end();
1890  --it;
1891  uint64_t totframes = it.key() * keyframedist;
1892  return totframes;
1893 }
1894 
1895 static int getFileInfo(const QString& inFile, const QString& outFile, int lenMethod)
1896 {
1897  // Open recording
1898  LOG(VB_JOBQUEUE , LOG_INFO, QString("getFileInfo(): Opening '%1'")
1899  .arg(inFile));
1900 
1901  RemoteAVFormatContext inputFC(inFile);
1902  if (!inputFC.isOpen())
1903  {
1904  LOG(VB_JOBQUEUE, LOG_ERR, "getFileInfo(): Couldn't open input file" +
1905  ENO);
1906  return 1;
1907  }
1908 
1909  // Getting stream information
1910  int ret = avformat_find_stream_info(inputFC, nullptr);
1911 
1912  if (ret < 0)
1913  {
1914  LOG(VB_JOBQUEUE, LOG_ERR,
1915  QString("Couldn't get stream info, error #%1").arg(ret));
1916  return 1;
1917  }
1918 
1919  // Dump stream information
1920  av_dump_format(inputFC, 0, qPrintable(inFile), 0);
1921 
1922  QDomDocument doc("FILEINFO");
1923 
1924  QDomElement root = doc.createElement("file");
1925  doc.appendChild(root);
1926  root.setAttribute("type", inputFC->iformat->name);
1927  root.setAttribute("filename", inFile);
1928 
1929  QDomElement streams = doc.createElement("streams");
1930 
1931  root.appendChild(streams);
1932  streams.setAttribute("count", inputFC->nb_streams);
1933  int ffmpegIndex = 0;
1934  uint duration = 0;
1935 
1936  for (uint i = 0; i < inputFC->nb_streams; i++)
1937  {
1938  AVStream *st = inputFC->streams[i];
1939  char buf[256];
1940  AVCodecContext *avctx = gCodecMap->getCodecContext(st);
1941  AVCodecParameters *par = st->codecpar;
1942 
1943  buf[0]=0;
1944  if (avctx)
1945  avcodec_string(buf, sizeof(buf), avctx, static_cast<int>(false));
1946 
1947  switch (st->codecpar->codec_type)
1948  {
1949  case AVMEDIA_TYPE_VIDEO:
1950  {
1951  QStringList param = QString(buf).split(',', QString::SkipEmptyParts);
1952  QString codec = param[0].remove("Video:", Qt::CaseInsensitive);
1953  QDomElement stream = doc.createElement("video");
1954  stream.setAttribute("streamindex", i);
1955  stream.setAttribute("ffmpegindex", ffmpegIndex++);
1956  stream.setAttribute("codec", codec.trimmed());
1957  stream.setAttribute("width", par->width);
1958  stream.setAttribute("height", par->height);
1959  stream.setAttribute("bitrate", (qlonglong)par->bit_rate);
1960 
1961  float fps;
1962  if (st->r_frame_rate.den && st->r_frame_rate.num)
1963  fps = av_q2d(st->r_frame_rate);
1964  else
1965  fps = 1/av_q2d(st->time_base);
1966 
1967  stream.setAttribute("fps", fps);
1968 
1969  if (par->sample_aspect_ratio.den && par->sample_aspect_ratio.num)
1970  {
1971  float aspect_ratio = av_q2d(par->sample_aspect_ratio);
1972  if (QString(inputFC->iformat->name) != "nuv")
1973  aspect_ratio = ((float)par->width
1974  / par->height) * aspect_ratio;
1975 
1976  stream.setAttribute("aspectratio", aspect_ratio);
1977  }
1978  else
1979  stream.setAttribute("aspectratio", "N/A");
1980 
1981  stream.setAttribute("id", st->id);
1982 
1983  if (st->start_time != (int) AV_NOPTS_VALUE)
1984  {
1985  int secs, us;
1986  secs = st->start_time / AV_TIME_BASE;
1987  us = st->start_time % AV_TIME_BASE;
1988  stream.setAttribute("start_time", QString("%1.%2")
1989  .arg(secs).arg(av_rescale(us, 1000000, AV_TIME_BASE)));
1990  }
1991  else
1992  stream.setAttribute("start_time", 0);
1993 
1994  streams.appendChild(stream);
1995 
1996  // TODO: probably should add a better way to choose which
1997  // video stream we use to calc the duration
1998  if (duration == 0)
1999  {
2000  int64_t frameCount = 0;
2001 
2002  switch (lenMethod)
2003  {
2004  case 0:
2005  {
2006  // use duration guess from avformat
2007  if (inputFC->duration != (uint) AV_NOPTS_VALUE)
2008  {
2009  duration = (uint) (inputFC->duration / AV_TIME_BASE);
2010  root.setAttribute("duration", duration);
2011  LOG(VB_JOBQUEUE, LOG_INFO,
2012  QString("duration = %1") .arg(duration));
2013  frameCount = (int64_t)(duration * fps);
2014  }
2015  else
2016  root.setAttribute("duration", "N/A");
2017  break;
2018  }
2019  case 1:
2020  {
2021  // calc duration of the file by counting the video frames
2022  frameCount = getFrameCount(inputFC, i);
2023  LOG(VB_JOBQUEUE, LOG_INFO,
2024  QString("frames = %1").arg(frameCount));
2025  duration = (uint)(frameCount / fps);
2026  LOG(VB_JOBQUEUE, LOG_INFO,
2027  QString("duration = %1").arg(duration));
2028  root.setAttribute("duration", duration);
2029  break;
2030  }
2031  case 2:
2032  {
2033  // use info from pos map in db
2034  // (only useful if the file is a myth recording)
2035  frameCount = getFrameCount(inFile, fps);
2036  if (frameCount)
2037  {
2038  LOG(VB_JOBQUEUE, LOG_INFO,
2039  QString("frames = %1").arg(frameCount));
2040  duration = (uint)(frameCount / fps);
2041  LOG(VB_JOBQUEUE, LOG_INFO,
2042  QString("duration = %1").arg(duration));
2043  root.setAttribute("duration", duration);
2044  }
2045  else if (inputFC->duration != (uint) AV_NOPTS_VALUE)
2046  {
2047  duration = (uint) (inputFC->duration / AV_TIME_BASE);
2048  root.setAttribute("duration", duration);
2049  LOG(VB_JOBQUEUE, LOG_INFO,
2050  QString("duration = %1").arg(duration));
2051  frameCount = (int64_t)(duration * fps);
2052  }
2053  else
2054  root.setAttribute("duration", "N/A");
2055  break;
2056  }
2057  default:
2058  root.setAttribute("duration", "N/A");
2059  LOG(VB_JOBQUEUE, LOG_ERR,
2060  QString("Unknown lenMethod (%1)")
2061  .arg(lenMethod));
2062  }
2063 
2064  // add duration after all cuts are removed
2065  int64_t cutFrames = getCutFrames(inFile, frameCount);
2066  LOG(VB_JOBQUEUE, LOG_INFO,
2067  QString("cutframes = %1").arg(cutFrames));
2068  int cutduration = (int)(cutFrames / fps);
2069  LOG(VB_JOBQUEUE, LOG_INFO,
2070  QString("cutduration = %1").arg(cutduration));
2071  root.setAttribute("cutduration", duration - cutduration);
2072  }
2073 
2074  break;
2075  }
2076 
2077  case AVMEDIA_TYPE_AUDIO:
2078  {
2079  QStringList param = QString(buf).split(',', QString::SkipEmptyParts);
2080  QString codec = param[0].remove("Audio:", Qt::CaseInsensitive);
2081 
2082  QDomElement stream = doc.createElement("audio");
2083  stream.setAttribute("streamindex", i);
2084  stream.setAttribute("ffmpegindex", ffmpegIndex++);
2085 
2086  // change any streams identified as "liba52" to "AC3" which is what
2087  // the mythburn.py script expects to get.
2088  if (codec.trimmed().toLower() == "liba52")
2089  stream.setAttribute("codec", "AC3");
2090  else
2091  stream.setAttribute("codec", codec.trimmed());
2092 
2093  stream.setAttribute("channels", par->channels);
2094 
2095  AVDictionaryEntry *metatag =
2096  av_dict_get(st->metadata, "language", nullptr, 0);
2097  if (metatag)
2098  stream.setAttribute("language", metatag->value);
2099  else
2100  stream.setAttribute("language", "N/A");
2101 
2102  stream.setAttribute("id", st->id);
2103 
2104  stream.setAttribute("samplerate", par->sample_rate);
2105  stream.setAttribute("bitrate", (qlonglong)par->bit_rate);
2106 
2107  if (st->start_time != (int) AV_NOPTS_VALUE)
2108  {
2109  int secs, us;
2110  secs = st->start_time / AV_TIME_BASE;
2111  us = st->start_time % AV_TIME_BASE;
2112  stream.setAttribute("start_time", QString("%1.%2")
2113  .arg(secs).arg(av_rescale(us, 1000000, AV_TIME_BASE)));
2114  }
2115  else
2116  stream.setAttribute("start_time", 0);
2117 
2118  streams.appendChild(stream);
2119 
2120  break;
2121  }
2122 
2123  case AVMEDIA_TYPE_SUBTITLE:
2124  {
2125  QStringList param = QString(buf).split(',', QString::SkipEmptyParts);
2126  QString codec = param[0].remove("Subtitle:", Qt::CaseInsensitive);
2127 
2128  QDomElement stream = doc.createElement("subtitle");
2129  stream.setAttribute("streamindex", i);
2130  stream.setAttribute("ffmpegindex", ffmpegIndex++);
2131  stream.setAttribute("codec", codec.trimmed());
2132 
2133  AVDictionaryEntry *metatag =
2134  av_dict_get(st->metadata, "language", nullptr, 0);
2135  if (metatag)
2136  stream.setAttribute("language", metatag->value);
2137  else
2138  stream.setAttribute("language", "N/A");
2139 
2140  stream.setAttribute("id", st->id);
2141 
2142  streams.appendChild(stream);
2143 
2144  break;
2145  }
2146 
2147  case AVMEDIA_TYPE_DATA:
2148  {
2149  QDomElement stream = doc.createElement("data");
2150  stream.setAttribute("streamindex", i);
2151  stream.setAttribute("codec", buf);
2152  streams.appendChild(stream);
2153 
2154  break;
2155  }
2156 
2157  default:
2158  LOG(VB_JOBQUEUE, LOG_ERR,
2159  QString("Skipping unsupported codec %1 on stream %2")
2160  .arg(inputFC->streams[i]->codecpar->codec_type).arg(i));
2161  break;
2162  }
2164  }
2165 
2166  // finally save the xml to the file
2167  QFile f(outFile);
2168  if (!f.open(QIODevice::WriteOnly))
2169  {
2170  LOG(VB_JOBQUEUE, LOG_ERR,
2171  "Failed to open file for writing - " + outFile);
2172  return 1;
2173  }
2174 
2175  QTextStream t(&f);
2176  t << doc.toString(4);
2177  f.close();
2178 
2179  return 0;
2180 }
2181 
2182 static int getDBParamters(const QString& outFile)
2183 {
2185 
2186  // save the db paramters to the file
2187  QFile f(outFile);
2188  if (!f.open(QIODevice::WriteOnly))
2189  {
2190  LOG(VB_GENERAL, LOG_ERR,
2191  QString("MythArchiveHelper: Failed to open file for writing - %1")
2192  .arg(outFile));
2193  return 1;
2194  }
2195 
2196  QTextStream t(&f);
2197  t << params.dbHostName << endl;
2198  t << params.dbUserName << endl;
2199  t << params.dbPassword << endl;
2200  t << params.dbName << endl;
2201  t << gCoreContext->GetHostName() << endl;
2202  t << GetInstallPrefix() << endl;
2203  f.close();
2204 
2205  return 0;
2206 }
2207 
2208 static int isRemote(const QString& filename)
2209 {
2210  if (filename.startsWith("myth://"))
2211  return 3;
2212 
2213  // check if the file exists
2214  if (!QFile::exists(filename))
2215  return 0;
2216 
2217  struct statfs statbuf;
2218  memset(&statbuf, 0, sizeof(statbuf));
2219 
2220 #if CONFIG_DARWIN
2221  if ((statfs(qPrintable(filename), &statbuf) == 0) &&
2222  ((!strcmp(statbuf.f_fstypename, "nfs")) || // NFS|FTP
2223  (!strcmp(statbuf.f_fstypename, "afpfs")) || // ApplShr
2224  (!strcmp(statbuf.f_fstypename, "smbfs")))) // SMB
2225  return 2;
2226 #elif __linux__
2227  if ((statfs(qPrintable(filename), &statbuf) == 0) &&
2228  ((statbuf.f_type == 0x6969) || // NFS
2229  (statbuf.f_type == 0x517B))) // SMB
2230  return 2;
2231 #endif
2232 
2233  return 1;
2234 }
2235 
2237 {
2238  public:
2240  void LoadArguments(void) override; // MythCommandLineParser
2241 };
2242 
2244  MythCommandLineParser("mytharchivehelper")
2246 
2248 {
2249  addHelp();
2250  addVersion();
2251  addLogging();
2252 
2253  add(QStringList{"-t", "--createthumbnail"},
2254  "createthumbnail", false,
2255  "Create one or more thumbnails\n"
2256  "Requires: --infile, --thumblist, --outfile\n"
2257  "Optional: --framecount", "");
2258  add("--infile", "infile", "",
2259  "Input file name\n"
2260  "Used with: --createthumbnail, --getfileinfo, --isremote, "
2261  "--sup2dast, --importarchive", "");
2262  add("--outfile", "outfile", "",
2263  "Output file name\n"
2264  "Used with: --createthumbnail, --getfileinfo, --getdbparameters, "
2265  "--nativearchive\n"
2266  "When used with --createthumbnail: eg 'thumb%1-%2.jpg'\n"
2267  " %1 will be replaced with the no. of the thumb\n"
2268  " %2 will be replaced with the frame no.", "");
2269  add("--thumblist", "thumblist", "",
2270  "Comma-separated list of required thumbs (in seconds)\n"
2271  "Used with: --createthumbnail","");
2272  add("--framecount", "framecount", 1,
2273  "Number of frames to grab (default 1)\n"
2274  "Used with: --createthumbnail", "");
2275 
2276  add(QStringList{"-i", "--getfileinfo"},
2277  "getfileinfo", false,
2278  "Write file info about infile to outfile\n"
2279  "Requires: --infile, --outfile, --method", "");
2280  add("--method", "method", 0,
2281  "Method of file duration calculation\n"
2282  "Used with: --getfileinfo\n"
2283  " 0 = use av_estimate_timings() (quick but not very accurate - "
2284  "default)\n"
2285  " 1 = read all frames (most accurate but slow)\n"
2286  " 2 = use position map in DB (quick, only works for MythTV "
2287  "recordings)", "");
2288 
2289  add(QStringList{"-p", "--getdbparameters"},
2290  "getdbparameters", false,
2291  "Write the mysql database parameters to outfile\n"
2292  "Requires: --outfile", "");
2293 
2294  add(QStringList{"-n", "--nativearchive"},
2295  "nativearchive", false,
2296  "Archive files to a native archive format\n"
2297  "Requires: --outfile", "");
2298 
2299  add(QStringList{"-f", "--importarchive"},
2300  "importarchive", false,
2301  "Import an archived file\n"
2302  "Requires: --infile, --chanid", "");
2303  add("--chanid", "chanid", -1,
2304  "Channel ID to use when inserting records in DB\n"
2305  "Used with: --importarchive", "");
2306 
2307  add(QStringList{"-r", "--isremote"},
2308  "isremote", false,
2309  "Check if infile is on a remote filesystem\n"
2310  "Requires: --infile\n"
2311  "Returns: 0 on error or file not found\n"
2312  " - 1 file is on a local filesystem\n"
2313  " - 2 file is on a remote filesystem", "");
2314 
2315  add(QStringList{"-b", "--burndvd"},
2316  "burndvd", false,
2317  "Burn a created DVD to a blank disc\n"
2318  "Optional: --mediatype, --erasedvdrw, --nativeformat", "");
2319  add("--mediatype", "mediatype", 0,
2320  "Type of media to burn\n"
2321  "Used with: --burndvd\n"
2322  " 0 = single layer DVD (default)\n"
2323  " 1 = dual layer DVD\n"
2324  " 2 = rewritable DVD", "");
2325  add("--erasedvdrw", "erasedvdrw", false,
2326  "Force an erase of DVD-R/W Media\n"
2327  "Used with: --burndvd (optional)", "");
2328  add("--nativeformat", "nativeformat", false,
2329  "Archive is a native archive format\n"
2330  "Used with: --burndvd (optional)", "");
2331 
2332  add(QStringList{"-s", "--sup2dast"},
2333  "sup2dast", false,
2334  "Convert projectX subtitles to DVD subtitles\n"
2335  "Requires: --infile, --ifofile, --delay", "");
2336  add("--ifofile", "ifofile", "",
2337  "Filename of ifo file\n"
2338  "Used with: --sup2dast", "");
2339  add("--delay", "delay", 0,
2340  "Delay in ms to add to subtitles (default 0)\n"
2341  "Used with: --sup2dast", "");
2342 }
2343 
2344 
2345 
2346 int main(int argc, char **argv)
2347 {
2349  if (!cmdline.Parse(argc, argv))
2350  {
2351  cmdline.PrintHelp();
2353  }
2354 
2355  if (cmdline.toBool("showhelp"))
2356  {
2357  cmdline.PrintHelp();
2358  return GENERIC_EXIT_OK;
2359  }
2360 
2361  if (cmdline.toBool("showversion"))
2362  {
2364  return GENERIC_EXIT_OK;
2365  }
2366 
2367  QCoreApplication a(argc, argv);
2368  QCoreApplication::setApplicationName("mytharchivehelper");
2369 
2370  // by default we only output our messages
2371  int retval;
2372  QString mask("jobqueue");
2373  if ((retval = cmdline.ConfigureLogging(mask)) != GENERIC_EXIT_OK)
2374  return retval;
2375 
2377  // Don't listen to console input
2378  close(0);
2379 
2381  if (!gContext->Init(false))
2382  {
2383  LOG(VB_GENERAL, LOG_ERR, "Failed to init MythContext, exiting.");
2384  delete gContext;
2385  gContext = nullptr;
2387  }
2388 
2389  int res = 0;
2390  bool bGrabThumbnail = cmdline.toBool("createthumbnail");
2391  bool bGetDBParameters = cmdline.toBool("getdbparameters");
2392  bool bNativeArchive = cmdline.toBool("nativearchive");
2393  bool bImportArchive = cmdline.toBool("importarchive");
2394  bool bGetFileInfo = cmdline.toBool("getfileinfo");
2395  bool bIsRemote = cmdline.toBool("isremote");
2396  bool bDoBurn = cmdline.toBool("burndvd");
2397  bool bEraseDVDRW = cmdline.toBool("erasedvdrw");
2398  bool bNativeFormat = cmdline.toBool("nativeformat");;
2399  bool bSup2Dast = cmdline.toBool("sup2dast");
2400 
2401  QString thumbList = cmdline.toString("thumblist");
2402  QString inFile = cmdline.toString("infile");
2403  QString outFile = cmdline.toString("outfile");
2404  QString ifoFile = cmdline.toString("ifofile");
2405 
2406  int mediaType = cmdline.toUInt("mediatype");
2407  int lenMethod = cmdline.toUInt("method");
2408  int chanID = cmdline.toInt("chanid");
2409  int frameCount = cmdline.toUInt("framecount");
2410  int delay = cmdline.toUInt("delay");
2411 
2412  // Check command line arguments
2413  if (bGrabThumbnail)
2414  {
2415  if (inFile.isEmpty())
2416  {
2417  LOG(VB_GENERAL, LOG_ERR, "Missing --infile in -t/--grabthumbnail "
2418  "option");
2420  }
2421 
2422  if (thumbList.isEmpty())
2423  {
2424  LOG(VB_GENERAL, LOG_ERR, "Missing --thumblist in -t/--grabthumbnail"
2425  " option");
2427  }
2428 
2429  if (outFile.isEmpty())
2430  {
2431  LOG(VB_GENERAL, LOG_ERR, "Missing --outfile in -t/--grabthumbnail "
2432  "option");
2434  }
2435  }
2436 
2437  if (bGetDBParameters)
2438  {
2439  if (outFile.isEmpty())
2440  {
2441  LOG(VB_GENERAL, LOG_ERR, "Missing argument to -p/--getdbparameters "
2442  "option");
2444  }
2445  }
2446 
2447  if (bIsRemote)
2448  {
2449  if (inFile.isEmpty())
2450  {
2451  LOG(VB_GENERAL, LOG_ERR,
2452  "Missing argument to -r/--isremote option");
2454  }
2455  }
2456 
2457  if (bDoBurn)
2458  {
2459  if (mediaType < 0 || mediaType > 2)
2460  {
2461  LOG(VB_GENERAL, LOG_ERR, QString("Invalid mediatype given: %1")
2462  .arg(mediaType));
2464  }
2465  }
2466 
2467  if (bNativeArchive)
2468  {
2469  if (outFile.isEmpty())
2470  {
2471  LOG(VB_GENERAL, LOG_ERR, "Missing argument to -n/--nativearchive "
2472  "option");
2474  }
2475  }
2476 
2477  if (bImportArchive)
2478  {
2479  if (inFile.isEmpty())
2480  {
2481  LOG(VB_GENERAL, LOG_ERR, "Missing --infile argument to "
2482  "-f/--importarchive option");
2484  }
2485  }
2486 
2487  if (bGetFileInfo)
2488  {
2489  if (inFile.isEmpty())
2490  {
2491  LOG(VB_GENERAL, LOG_ERR, "Missing --infile in -i/--getfileinfo "
2492  "option");
2494  }
2495 
2496  if (outFile.isEmpty())
2497  {
2498  LOG(VB_GENERAL, LOG_ERR, "Missing --outfile in -i/--getfileinfo "
2499  "option");
2501  }
2502  }
2503 
2504  if (bSup2Dast)
2505  {
2506  if (inFile.isEmpty())
2507  {
2508  LOG(VB_GENERAL, LOG_ERR,
2509  "Missing --infile in -s/--sup2dast option");
2511  }
2512 
2513  if (ifoFile.isEmpty())
2514  {
2515  LOG(VB_GENERAL, LOG_ERR,
2516  "Missing --ifofile in -s/--sup2dast option");
2518  }
2519  }
2520 
2521  if (bGrabThumbnail)
2522  res = grabThumbnail(inFile, thumbList, outFile, frameCount);
2523  else if (bGetDBParameters)
2524  res = getDBParamters(outFile);
2525  else if (bNativeArchive)
2526  res = doNativeArchive(outFile);
2527  else if (bImportArchive)
2528  res = doImportArchive(inFile, chanID);
2529  else if (bGetFileInfo)
2530  res = getFileInfo(inFile, outFile, lenMethod);
2531  else if (bIsRemote)
2532  res = isRemote(inFile);
2533  else if (bDoBurn)
2534  res = doBurnDVD(mediaType, bEraseDVDRW, bNativeFormat);
2535  else if (bSup2Dast)
2536  {
2537  QByteArray inFileBA = inFile.toLocal8Bit();
2538  QByteArray ifoFileBA = ifoFile.toLocal8Bit();
2539  res = sup2dast(inFileBA.constData(), ifoFileBA.constData(), delay);
2540  }
2541  else
2542  cmdline.PrintHelp();
2543 
2544  delete gContext;
2545  gContext = nullptr;
2546 
2547  exit(res);
2548 }
2549 
2550 
bool next(void)
Wrap QSqlQuery::next() so we can display the query results.
Definition: mythdbcon.cpp:782
Startup context for MythTV.
Definition: mythcontext.h:42
CommandLineArg * add(QString arg, QString name, bool def, QString help, QString longhelp)
void bindValue(const QString &placeholder, const QVariant &val)
Add a single binding.
Definition: mythdbcon.cpp:863
QString dbName
database name
Definition: mythdbparams.h:26
static bool createISOImage(QString &sourceDirectory)
#define GENERIC_EXIT_OK
Exited with no error.
Definition: exitcodes.h:10
int Copy(VideoFrame *dst, const VideoFrame *src)
Definition: mythavutil.cpp:200
#define MPUBLIC
Definition: mythexp.h:10
bool IsVideo(void) const
Definition: programinfo.h:476
bool toBool(const QString &key) const
Returns stored QVariant as a boolean.
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:431
bool QueryCutList(frm_dir_map_t &, bool loadAutosave=false) const
QString GetInstallPrefix(void)
Definition: mythdirs.cpp:220
int size(void) const
Definition: mythdbcon.h:203
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)
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
QVariant value(int i) const
Definition: mythdbcon.h:198
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:177
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:381
static int doImportArchive(const QString &inFile, int chanID)
QString GenMythURL(const QString &host=QString(), int port=0, QString path=QString(), const QString &storageGroup=QString())
QDateTime current(bool stripped)
Returns current Date and Time in UTC.
Definition: mythdate.cpp:10
QString GetSetting(const QString &key, const QString &defaultval="")
static int getFileInfo(const QString &inFile, const QString &outFile, int lenMethod)
int doImportArchive(const QString &xmlFile, int chanID)
int exportVideo(QDomElement &itemNode, const QString &saveDirectory)
QString toString(const QString &key) const
Returns stored QVariant as a QString, falling to default if not provided.
unsigned char t
Definition: ParseText.cpp:329
static int64_t getFrameCount(AVFormatContext *inputFC, int vid_id)
static int grabThumbnail(const QString &inFile, const QString &thumbList, const QString &outFile, int frameCount)
bool isActive(void) const
Definition: mythdbcon.h:204
int DeinterlaceSingle(AVFrame *dst, const AVFrame *src)
Definition: mythavutil.cpp:307
QString GetMasterHostName(void)
Definition: compat.h:165
int importRecording(const QDomElement &itemNode, const QString &xmlFile, int chanID)
static MSqlQueryInfo InitCon(ConnectionReuse=kNormalConnection)
Only use this in combination with MSqlQuery constructor.
Definition: mythdbcon.cpp:535
static int64_t getCutFrames(const QString &filename, int64_t lastFrame)
MythCommFlagCommandLineParser cmdline
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 isRemote(const QString &filename)
static int burnISOImage(int mediaType, bool bEraseDVDRW, bool nativeFormat)
AVCodecContext * getCodecContext(const AVStream *, const AVCodec *pCodec=nullptr, bool nullCodec=false)
Definition: mythavutil.cpp:392
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:170
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:807
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.
static int getDBParamters(const QString &outFile)
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)
bool Init(const bool gui=true, const bool promptForBackend=false, const bool disableAutoDiscovery=false, const bool ignoreDB=false)
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:882
bool exec(void)
Wrap QSqlQuery::exec() so we can display SQL.
Definition: mythdbcon.cpp:603
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
uint toUInt(const QString &key) const
Returns stored QVariant as an unsigned integer, falling to default if not provided.
QString GetHostName(void)
void PrintVersion(void) const
Print application version information.
int toInt(const QString &key) const
Returns stored QVariant as an integer, falling to default if not provided.