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