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