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