Difference between revisions of "Mythvidexport.py"

From MythTV Official Wiki
Jump to: navigation, search
m (added wiki link to mythjobqueue)
(Update version number to properly indicate a change)
(21 intermediate revisions by 5 users not shown)
Line 5: Line 5:
 
|long=A python script intended to be run as a user job allowing for exporting recordings into MythVideo.
 
|long=A python script intended to be run as a user job allowing for exporting recordings into MythVideo.
 
|category=User Job Scripts
 
|category=User Job Scripts
|file=mythvidexport.py}}
+
|file=mythvidexport.py
 +
|S25=yes|S26=yes|S27=yes}}
  
This python script is intended to function as a user job, run through [[Mythjobqueue|mythjobqueue]], capable of exporting recordings into MythVideo.
+
This python script is intended to function as a [[User_Jobs|user job]], capable of exporting recordings into [[MythVideo]].
  
{{Python|mythvidexport.py|
 
 
<pre>
 
<pre>
#!/usr/local/bin/python
 
# -*- coding: UTF-8 -*-
 
#---------------------------
 
# Name: mythvidexport.py
 
# Python Script
 
# Author: Raymond Wagner
 
# Purpose
 
#  This python script is intended to function as a user job, run through
 
#  mythjobqueue, capable of exporting recordings into MythVideo.
 
#---------------------------
 
__title__  = "MythVidExport"
 
__author__ = "Raymond Wagner"
 
__version__= "v0.6.0"
 
 
usage_txt = """
 
 
This script can be run from the command line, or called through the mythtv
 
This script can be run from the command line, or called through the mythtv
 
jobqueue.  The input format will be:
 
jobqueue.  The input format will be:
Line 40: Line 25:
 
             use EPG data rather than grabbers for metadata
 
             use EPG data rather than grabbers for metadata
 
             will still try to grab episode and season information from ttvdb.py
 
             will still try to grab episode and season information from ttvdb.py
 +
        --seekdata            copy seek data from recording
 +
        --skiplist            copy commercial detection from recording
 +
        --cutlist              copy manual commercial cutlist from recording
  
 
Additional functions are available beyond exporting video
 
Additional functions are available beyond exporting video
Line 49: Line 37:
 
         --tformat <string>    replace existing TV format
 
         --tformat <string>    replace existing TV format
 
         --gformat <string>    replace existing Generic format
 
         --gformat <string>    replace existing Generic format
"""
+
</pre>
 +
 
 +
{{Python|mythvidexport.py|
 +
<pre>
 +
#!/usr/bin/env python
 +
# -*- coding: UTF-8 -*-
 +
#---------------------------
 +
# Name: mythvidexport.py
 +
# Python Script
 +
# Author: Raymond Wagner
 +
# Purpose
 +
# This python script is intended to function as a user job, run through
 +
# mythjobqueue, capable of exporting recordings into MythVideo.
 +
#---------------------------
 +
__title__ = "MythVidExport"
 +
__author__ = "Raymond Wagner"
 +
__version__= "v0.7.6"
  
from MythTV import MythDB, Job, Video, VideoGrabber, MythLog, MythError
+
from MythTV import MythDB, Job, Recorded, Video, VideoGrabber,\
 +
                  MythLog, MythError, static, MythBE
 +
from optparse import OptionParser, OptionGroup
 
from socket import gethostname
 
from socket import gethostname
from urllib import urlopen
 
from optparse import OptionParser
 
from ConfigParser import SafeConfigParser
 
  
import sys, re, os, time
+
import os
 +
import re
 +
import sys
 +
import time
 +
import hashlib
  
 +
def create_dummy_video(db=None):
 +
    db = MythDB(db)
 +
 +
def hashfile(fd):
 +
    hasher = hashlib.sha1()
 +
    while True:
 +
        buff = fd.read(2**16)
 +
        if len(buff) == 0:
 +
            break
 +
        hasher.update(buff)
 +
    return hasher.hexdigest()
  
 
class VIDEO:
 
class VIDEO:
Line 65: Line 83:
 
             self.job = Job(jobid)
 
             self.job = Job(jobid)
 
             self.chanid = self.job.chanid
 
             self.chanid = self.job.chanid
             self.starttime = int("%04d%02d%02d%02d%02d%02d" \
+
             self.starttime = self.job.starttime
                        % self.job.starttime.timetuple()[0:6])
+
             self.job.update(status=Job.STARTING)
             self.job.update(status=3)
 
 
         else:
 
         else:
 
             self.job = None
 
             self.job = None
 
             self.chanid = opts.chanid
 
             self.chanid = opts.chanid
             self.rtime = opts.starttime
+
             self.starttime = opts.starttime
  
 
         self.opts = opts
 
         self.opts = opts
Line 78: Line 95:
  
 
         # load setting strings
 
         # load setting strings
        self.get_grabbers()
 
 
         self.get_format()
 
         self.get_format()
  
         # process file
+
         # prep objects
         self.cast = []
+
         self.rec = Recorded((self.chanid,self.starttime), db=self.db)
         self.genre = []
+
         if "_" in self.rec.inetref:
        self.country = []
+
                self.rec.inetref = self.rec.inetref.split("_")[1]
         self.rec = self.db.getRecorded(chanid=self.chanid,\
+
         self.log(MythLog.GENERAL, MythLog.INFO, 'Using recording',
                                    starttime=self.starttime)
+
                        '%s - %s' % (self.rec.title.encode('utf-8'),
         self.vid = Video()
+
                                    self.rec.subtitle.encode('utf-8')))
        self.vid.host = gethostname()
+
         self.vid = Video(db=self.db).create({'title':'', 'filename':'',
 +
                                            'host':gethostname()})
  
 +
        # process data
 
         self.get_meta()
 
         self.get_meta()
 
         self.get_dest()
 
         self.get_dest()
 +
        # bug fix to work around limitation in the bindings where DBDataRef classes
 +
        # are mapped to the filename at time of Video element creation. since the
 +
        # filename is specified as blank when the video is created, the markup
 +
        # handler is not properly initialized
 +
        self.vid.markup._refdat = (self.vid.filename,)
  
 
         # save file
 
         # save file
 
         self.copy()
 
         self.copy()
         self.write_images()
+
         if opts.seekdata:
        self.vid.create()
+
            self.copy_seek()
         self.write_cref()
+
         if opts.skiplist:
 
+
            self.copy_markup(static.MARKUP.MARK_COMM_START,
    def get_grabbers(self):
+
                            static.MARKUP.MARK_COMM_END)
        # TV Grabber
+
        if opts.cutlist:
        self.TVgrab = VideoGrabber('TV')
+
            self.copy_markup(static.MARKUP.MARK_CUT_START,
        # if ttvdb.py, optionally add config file
+
                            static.MARKUP.MARK_CUT_END)
        if 'ttvdb.py' in self.TVgrab.path:
+
        self.vid.update()
            path = os.path.expanduser('~/.mythtv/ttvdb.conf')
 
            if os.access(path, os.F_OK):
 
                # apply title overrides
 
                cfg = SafeConfigParser()
 
                cfg.read(path)
 
                if 'series_name_override' in cfg.sections():
 
                    ovr = [(title, cfg.get('series_name_override',title)) \
 
                            for title in cfg.options('series_name_override')]
 
                    self.TVgrab.setOverride(ovr)
 
                    self.TVgrab.append(' -c '+path)
 
  
         # Movie Grabber
+
         # delete old file
         self.Mgrab = VideoGrabber('Movie')
+
         if opts.delete:
 +
            self.rec.delete()
  
 
     def get_format(self):
 
     def get_format(self):
         host = gethostname()
+
         host = self.db.gethostname()
 
         # TV Format
 
         # TV Format
 
         if self.opts.tformat:
 
         if self.opts.tformat:
Line 146: Line 160:
  
 
     def get_meta(self):
 
     def get_meta(self):
         self.vid.hostname = gethostname()
+
         self.vid.hostname = self.db.gethostname()
         if self.rec.subtitle: # subtitle exists, assume tv show
+
         if self.rec.inetref:
             self.get_tv()
+
            # good data is available, use it
        else:                   # assume movie
+
             if self.rec.season > 0 or self.rec.episode > 0:
            self.get_movie()
+
                self.log(self.log.GENERAL, self.log.INFO,
 
+
                        'Performing TV export with local data.')
    def get_tv(self):
+
                self.type = 'TV'
        # grab season and episode number, run generic export if failed
+
                grab = VideoGrabber(self.type)
        match = self.TVgrab.searchTitle(self.rec.title)
+
                metadata = grab.grabInetref(self.rec.inetref, self.rec.season, self.rec.episode)
        if len(match) == 0:
+
            else:
            # no match found
+
                self.log(self.log.GENERAL, self.log.INFO,
            self.get_generic()
+
                        'Performing Movie export with local data.')
            return
+
                self.type = 'MOVIE'
         elif len(match) > 1:
+
                grab = VideoGrabber(self.type)
             # multiple matches found
+
                metadata = grab.grabInetref(self.rec.inetref)
             raise MythError('Multiple TV metadata matches found: '\
+
         elif self.opts.listingonly:
                                                    +self.rec.title)
+
             # force use of local data
        inetref = match[0][0]
+
             if self.rec.subtitle:
 
+
                self.log(self.log.GENERAL, self.log.INFO,
        season, episode = self.TVgrab.searchEpisode(self.rec.title, \
+
                        'Forcing TV export with local data.')
                                                    self.rec.subtitle)
+
                self.type = 'TV'
        if season is None:
+
             else:
             # no match found
+
                self.log(self.log.GENERAL, self.log.INFO,
            self.get_generic()
+
                        'Forcing Movie export with local data.')
            return
+
                self.type = 'MOVIE'
        self.vid.season, self.vid.episode = season, episode
+
             metadata = self.rec.exportMetadata()
 
 
        if self.opts.listingonly:
 
             self.get_generic()
 
 
         else:
 
         else:
             dat, self.cast, self.genre, self.country = \
+
             if self.rec.subtitle:
                    self.TVgrab.getData(inetref,\
+
                # subtitle exists, assume tv show
                                        self.vid.season,\
+
                self.type = 'TV'
                                        self.vid.episode)
+
                self.log(self.log.GENERAL, self.log.INFO,
            self.vid.data.update(dat)
+
                        'Attempting TV export.')
            self.vid.title = self.rec.title
+
                grab = VideoGrabber(self.type)
             self.vid.season, self.vid.episode = season, episode
+
                match = grab.sortedSearch(self.rec.title, self.rec.subtitle)
        self.type = 'TV'
+
             else: # assume movie
 +
                self.type = 'MOVIE'
 +
                self.log(self.log.GENERAL, self.log.INFO,
 +
                        'Attempting Movie export.')
 +
                grab = VideoGrabber(self.type)
 +
                match = grab.sortedSearch(self.rec.title)
  
    def get_movie(self):
+
            if len(match) == 0:
        inetref = self.Mgrab.searchTitle(self.rec.title,\
+
                # no match found
                            self.rec.originalairdate.year)
+
                self.log(self.log.GENERAL, self.log.INFO,
        if len(inetref) == 1:
+
                        'Falling back to generic export.')
            inetref = inetref[0][0]
+
                self.type = 'GENERIC'
        else:
+
                metadata = self.rec.exportMetadata()
            self.get_generic()
+
            elif (len(match) > 1) & (match[0].levenshtein > 0):
            return
+
                # multiple matches found, and closest is not exact
 +
                self.vid.delete()
 +
                raise MythError('Multiple metadata matches found: '\
 +
                                                  +self.rec.title)
 +
            else:
 +
                self.log(self.log.GENERAL, self.log.INFO,
 +
                        'Importing content from', match[0].inetref)
 +
                metadata = grab.grabInetref(match[0])
  
         if self.opts.listingonly:
+
         self.vid.importMetadata(metadata)
            self.get_generic()
+
         self.log(self.log.GENERAL, self.log.INFO, 'Import complete')
        else:
 
            dat, self.cast, self.genre, self.country = \
 
                    self.Mgrab.getData(inetref)
 
            self.vid.data.update(dat)
 
            self.vid.title = self.rec.title
 
         self.type = 'Movie'
 
 
 
    def get_generic(self):
 
        self.vid.title = self.rec.title
 
        if self.rec.subtitle:
 
            self.vid.subtitle = self.rec.subtitle
 
        if self.rec.description:
 
            self.vid.plot = self.rec.description
 
        if self.rec.originalairdate:
 
            self.vid.year = self.rec.originalairdate.year
 
            self.vid.releasedate = self.rec.originalairdate
 
        lsec = (self.rec.endtime-self.rec.starttime).seconds
 
        self.vid.length = str(lsec/60)
 
        for member in self.rec.cast:
 
            if member.role == 'director':
 
                self.vid.director = member.name
 
            elif member.role == 'actor':
 
                self.cast.append(member.name)
 
        self.type = 'GENERIC'
 
  
 
     def get_dest(self):
 
     def get_dest(self):
Line 233: Line 232:
 
         #print self.vid.data
 
         #print self.vid.data
 
         ext = '.'+self.rec.basename.rsplit('.',1)[1]
 
         ext = '.'+self.rec.basename.rsplit('.',1)[1]
         rep = ( ('%TITLE%','title','%s'),   ('%SUBTITLE%','subtitle','%s'),
+
         rep = ( ('%TITLE%','title','%s'), ('%SUBTITLE%','subtitle','%s'),
             ('%SEASON%','season','%d'),     ('%SEASONPAD%','season','%02d'),
+
             ('%SEASON%','season','%d'), ('%SEASONPAD%','season','%02d'),
             ('%EPISODE%','episode','%d'),   ('%EPISODEPAD%','episode','%02d'),
+
             ('%EPISODE%','episode','%d'), ('%EPISODEPAD%','episode','%02d'),
             ('%YEAR%','year','%s'),         ('%DIRECTOR%','director','%s'))
+
             ('%YEAR%','year','%s'), ('%DIRECTOR%','director','%s'))
 
         for tag, data, format in rep:
 
         for tag, data, format in rep:
 
             if self.vid[data]:
 
             if self.vid[data]:
Line 244: Line 243:
  
 
         # replace fields from program data
 
         # replace fields from program data
         rep = ( ('%HOSTNAME','hostname','%s'),('%STORAGEGROUP%','storagegroup','%s'))
+
         rep = ( ('%HOSTNAME%', 'hostname', '%s'),
 +
                ('%STORAGEGROUP%','storagegroup','%s'))
 
         for tag, data, format in rep:
 
         for tag, data, format in rep:
 
             data = getattr(self.rec, data)
 
             data = getattr(self.rec, data)
 
             fmt = fmt.replace(tag,format % data)
 
             fmt = fmt.replace(tag,format % data)
  
#       fmt = fmt.replace('%CARDID%',self.rec.cardid)
+
# fmt = fmt.replace('%CARDID%',self.rec.cardid)
#       fmt = fmt.replace('%CARDNAME%',self.rec.cardid)
+
# fmt = fmt.replace('%CARDNAME%',self.rec.cardid)
#       fmt = fmt.replace('%SOURCEID%',self.rec.cardid)
+
# fmt = fmt.replace('%SOURCEID%',self.rec.cardid)
#       fmt = fmt.replace('%SOURCENAME%',self.rec.cardid)
+
# fmt = fmt.replace('%SOURCENAME%',self.rec.cardid)
#       fmt = fmt.replace('%CHANNUM%',self.rec.channum)
+
# fmt = fmt.replace('%CHANNUM%',self.rec.channum)
#       fmt = fmt.replace('%CHANNAME%',self.rec.cardid)
+
# fmt = fmt.replace('%CHANNAME%',self.rec.cardid)
  
         if len(self.genre):
+
         if len(self.vid.genre):
             fmt = fmt.replace('%GENRE%',self.genre[0])
+
             fmt = fmt.replace('%GENRE%',self.vid.genre[0].genre)
 
         else:
 
         else:
 
             fmt = fmt.replace('%GENRE%','')
 
             fmt = fmt.replace('%GENRE%','')
#       if len(self.country):
+
# if len(self.country):
#           fmt = fmt.replace('%COUNTRY%',self.country[0])
+
# fmt = fmt.replace('%COUNTRY%',self.country[0])
#       else:
+
# else:
#           fmt = fmt.replace('%COUNTRY%','')
+
# fmt = fmt.replace('%COUNTRY%','')
 
         return fmt+ext
 
         return fmt+ext
  
 
     def copy(self):
 
     def copy(self):
        if self.opts.skip:
 
            self.vid.hash = self.vid.getHash()
 
            return
 
        if self.opts.sim:
 
            return
 
 
        #print self.vid.filename
 
 
 
         stime = time.time()
 
         stime = time.time()
 
         srcsize = self.rec.filesize
 
         srcsize = self.rec.filesize
 
         htime = [stime,stime,stime,stime]
 
         htime = [stime,stime,stime,stime]
  
         self.log.log(MythLog.IMPORTANT|MythLog.FILE, "Copying myth://%s@%s/%s"\
+
         self.log(MythLog.GENERAL|MythLog.FILE, MythLog.INFO, "Copying myth://%s@%s/%s"\
 
               % (self.rec.storagegroup, self.rec.hostname, self.rec.basename)\
 
               % (self.rec.storagegroup, self.rec.hostname, self.rec.basename)\
 
                                                     +" to myth://Videos@%s/%s"\
 
                                                     +" to myth://Videos@%s/%s"\
 
                                           % (self.vid.host, self.vid.filename))
 
                                           % (self.vid.host, self.vid.filename))
 
         srcfp = self.rec.open('r')
 
         srcfp = self.rec.open('r')
         dstfp = self.vid.open('w')
+
         dstfp = self.vid.open('w', nooverwrite=True)
  
 
         if self.job:
 
         if self.job:
             self.job.setStatus(4)
+
             self.job.setStatus(Job.RUNNING)
 
         tsize = 2**24
 
         tsize = 2**24
 
         while tsize == 2**24:
 
         while tsize == 2**24:
Line 303: Line 295:
 
         self.vid.hash = self.vid.getHash()
 
         self.vid.hash = self.vid.getHash()
  
         self.log.log(MythLog.IMPORTANT|MythLog.FILE, "Transfer Complete",
+
         self.log(MythLog.GENERAL|MythLog.FILE, MythLog.INFO, "Transfer Complete",
                             "% seconds elapsed" % int(time.time()-stime))
+
                             "%d seconds elapsed" % int(time.time()-stime))
 +
 
 +
        if self.opts.reallysafe:
 +
            if self.job:
 +
                self.job.setComment("Checking file hashes")
 +
            self.log(MythLog.GENERAL|MythLog.FILE, MythLog.INFO, "Checking file hashes.")
 +
            srchash = hashfile(self.rec.open('r'))
 +
            dsthash = hashfile(self.rec.open('r'))
 +
            if srchash != dsthash:
 +
                raise MythError('Source hash (%s) does not match destination hash (%s)' \
 +
                            % (srchash, dsthash))
 +
        elif self.opts.safe:
 +
            self.log(MythLog.GENERAL|MythLog.FILE, MythLog.INFO, "Checking file sizes.")
 +
            be = MythBE(db=self.vid._db)
 +
            try:
 +
                srcsize = be.getSGFile(self.rec.hostname, self.rec.storagegroup, \
 +
                                      self.rec.basename)[1]
 +
                dstsize = be.getSGFile(self.vid.host, 'Videos', self.vid.filename)[1]
 +
            except:
 +
                raise MythError('Could not query file size from backend')
 +
            if srcsize != dstsize:
 +
                raise MythError('Source size (%d) does not match destination size (%d)' \
 +
                            % (srcsize, dstsize))
 +
 
 
         if self.job:
 
         if self.job:
 
             self.job.setComment("Complete - %d seconds elapsed" % \
 
             self.job.setComment("Complete - %d seconds elapsed" % \
 
                             (int(time.time()-stime)))
 
                             (int(time.time()-stime)))
             self.job.setStatus(256)
+
             self.job.setStatus(Job.FINISHED)
 
 
    def write_images(self):
 
        for type in ('coverfile', 'screenshot', 'banner', 'fanart'):
 
            if self.vid[type] in ('No Cover','',None):
 
                continue
 
            if type == 'coverfile': name = 'coverart'
 
            else: name = type
 
            url = self.vid[type]
 
            if ',' in url:
 
                url = url.split(',')[0]
 
  
            if self.type == 'TV':
+
     def copy_seek(self):
                if type == 'screenshot':
+
         for seek in self.rec.seek:
                    self.vid[type] = '%s Season %dx%d_%s.%s' % \
+
             self.vid.markup.add(seek.mark, seek.offset, seek.type)
                                (self.vid.title, self.vid.season,
 
                                self.vid.episode, name, url.rsplit('.',1)[1])
 
                else:
 
                    self.vid[type] = '%s Season %d_%s.%s' % \
 
                                (self.vid.title, self.vid.season,
 
                                name, url.rsplit('.',1)[1])
 
            else:
 
                self.vid[type] = '%s_%s.%s' % \
 
                            (self.vid.title, name, url.rsplit('.',1)[1])
 
 
 
            try:
 
                dstfp = self.vid._open(type, 'w', True)
 
                srcfp = urlopen(url)
 
                dstfp.write(srcfp.read())
 
                srcfp.close()
 
                dstfp.close()
 
            except:
 
                #print 'existing images: ' + self.vid[type]
 
                pass
 
 
 
     def write_cref(self):
 
         for member in self.cast:
 
            self.vid.cast.add(member)
 
        for member in self.genre:
 
             self.vid.genre.add(member)
 
        for member in self.country:
 
            self.vid.country.add(member)
 
  
 +
    def copy_markup(self, start, stop):
 +
        for mark in self.rec.markup:
 +
            if mark.type in (start, stop):
 +
                self.vid.markup.add(mark.mark, 0, mark.type)
  
 
def usage_format():
 
def usage_format():
 
     usagestr = """The default strings are:
 
     usagestr = """The default strings are:
    Television: Television/%TITLE%/Season %SEASON%/%TITLE% - S%SEASON%E%EPISODEPAD% - %SUBTITLE%
+
Television: Television/%TITLE%/Season %SEASON%/%TITLE% - S%SEASON%E%EPISODEPAD% - %SUBTITLE%
    Movie:     Movies/%TITLE%
+
Movie: Movies/%TITLE%
    Generic:   Videos/%TITLE%
+
Generic: Videos/%TITLE%
  
 
Available strings:
 
Available strings:
    %TITLE%:         series title
+
%TITLE%: series title
    %SUBTITLE%:     episode title
+
%SUBTITLE%: episode title
    %SEASON%:       season number
+
%SEASON%: season number
    %SEASONPAD%:     season number, padded to 2 digits
+
%SEASONPAD%: season number, padded to 2 digits
    %EPISODE%:       episode number
+
%EPISODE%: episode number
    %EPISODEPAD%:   episode number, padded to 2 digits
+
%EPISODEPAD%: episode number, padded to 2 digits
    %YEAR%:         year
+
%YEAR%: year
    %DIRECTOR%:     director
+
%DIRECTOR%: director
    %HOSTNAME%:     backend used to record show
+
%HOSTNAME%: backend used to record show
    %STORAGEGROUP%: storage group containing recorded show
+
%STORAGEGROUP%: storage group containing recorded show
    %GENRE%:         first genre listed for recording
+
%GENRE%: first genre listed for recording
 
"""
 
"""
#   %CARDID%:       ID of tuner card used to record show
+
# %CARDID%: ID of tuner card used to record show
#   %CARDNAME%:     name of tuner card used to record show
+
# %CARDNAME%: name of tuner card used to record show
#   %SOURCEID%:     ID of video source used to record show
+
# %SOURCEID%: ID of video source used to record show
#   %SOURCENAME%:   name of video source used to record show
+
# %SOURCENAME%: name of video source used to record show
#   %CHANNUM%:       ID of channel used to record show
+
# %CHANNUM%: ID of channel used to record show
#   %CHANNAME%:     name of channel used to record show
+
# %CHANNAME%: name of channel used to record show
#   %COUNTRY%:       first country listed for recording
+
# %COUNTRY%: first country listed for recording
 
     print usagestr
 
     print usagestr
  
Line 393: Line 375:
 
         gfmt = 'Videos/%TITLE%'
 
         gfmt = 'Videos/%TITLE%'
 
     print "Current output formats:"
 
     print "Current output formats:"
     print "   TV:     "+tfmt
+
     print " TV: "+tfmt
     print "   Movies: "+mfmt
+
     print " Movies: "+mfmt
     print "   Generic: "+gfmt
+
     print " Generic: "+gfmt
  
 
def main():
 
def main():
 
     parser = OptionParser(usage="usage: %prog [options] [jobid]")
 
     parser = OptionParser(usage="usage: %prog [options] [jobid]")
  
     parser.add_option("-f", "--helpformat", action="store_true", default=False, dest="fmthelp",
+
     formatgroup = OptionGroup(parser, "Formatting Options",
 +
                    "These options are used to display and manipulate the output file formats.")
 +
    formatgroup.add_option("-f", "--helpformat", action="store_true", default=False, dest="fmthelp",
 
             help="Print explination of file format string.")
 
             help="Print explination of file format string.")
     parser.add_option("-p", "--printformat", action="store_true", default=False, dest="fmtprint",
+
     formatgroup.add_option("-p", "--printformat", action="store_true", default=False, dest="fmtprint",
 
             help="Print current file format string.")
 
             help="Print current file format string.")
     parser.add_option("--tformat", action="store", type="string", dest="tformat",
+
     formatgroup.add_option("--tformat", action="store", type="string", dest="tformat",
 
             help="Use TV format for current task. If no task, store in database.")
 
             help="Use TV format for current task. If no task, store in database.")
     parser.add_option("--mformat", action="store", type="string", dest="mformat",
+
     formatgroup.add_option("--mformat", action="store", type="string", dest="mformat",
 
             help="Use Movie format for current task. If no task, store in database.")
 
             help="Use Movie format for current task. If no task, store in database.")
     parser.add_option("--gformat", action="store", type="string", dest="gformat",
+
     formatgroup.add_option("--gformat", action="store", type="string", dest="gformat",
 
             help="Use Generic format for current task. If no task, store in database.")
 
             help="Use Generic format for current task. If no task, store in database.")
     parser.add_option("--chanid", action="store", type="int", dest="chanid",
+
    formatgroup.add_option("--listingonly", action="store_true", default=False, dest="listingonly",
 +
            help="Use data from listing provider, rather than grabber")
 +
     parser.add_option_group(formatgroup)
 +
 
 +
    sourcegroup = OptionGroup(parser, "Source Definition",
 +
                    "These options can be used to manually specify a recording to operate on "+\
 +
                    "in place of the job id.")
 +
    sourcegroup.add_option("--chanid", action="store", type="int", dest="chanid",
 
             help="Use chanid for manual operation")
 
             help="Use chanid for manual operation")
     parser.add_option("--starttime", action="store", type="int", dest="starttime",
+
     sourcegroup.add_option("--starttime", action="store", type="int", dest="starttime",
 
             help="Use starttime for manual operation")
 
             help="Use starttime for manual operation")
     parser.add_option("--listingonly", action="store_true", default=False, dest="listingonly",
+
     parser.add_option_group(sourcegroup)
             help="Use data from listing provider, rather than grabber")
+
 
     parser.add_option("-s", "--simulation", action="store_true", default=False, dest="sim",
+
    actiongroup = OptionGroup(parser, "Additional Actions",
             help="Simulation (dry run), no files are copied or new entries made")
+
                    "These options perform additional actions after the recording has been exported.")
     parser.add_option("--skip", action="store_true", default=False, dest="skip") # debugging use only
+
    actiongroup.add_option('--safe', action='store_true', default=False, dest='safe',
     parser.add_option('-v', '--verbose', action='store', type='string', dest='verbose',
+
            help='Perform quick sanity check of exported file using file size.')
             help='Verbosity level')
+
    actiongroup.add_option('--really-safe', action='store_true', default=False, dest='reallysafe',
 +
            help='Perform slow sanity check of exported file using SHA1 hash.')
 +
    actiongroup.add_option("--delete", action="store_true", default=False,
 +
             help="Delete source recording after successful export. Enforces use of --safe.")
 +
     parser.add_option_group(actiongroup)
 +
 
 +
    othergroup = OptionGroup(parser, "Other Data",
 +
                    "These options copy additional information from the source recording.")
 +
    othergroup.add_option("--seekdata", action="store_true", default=False, dest="seekdata",
 +
             help="Copy seekdata from source recording.")
 +
     othergroup.add_option("--skiplist", action="store_true", default=False, dest="skiplist",
 +
            help="Copy commercial detection from source recording.")
 +
     othergroup.add_option("--cutlist", action="store_true", default=False, dest="cutlist",
 +
             help="Copy manual commercial cuts from source recording.")
 +
    parser.add_option_group(othergroup)
 +
 
 +
    MythLog.loadOptParse(parser)
  
 
     opts, args = parser.parse_args()
 
     opts, args = parser.parse_args()
Line 437: Line 444:
 
         print_format()
 
         print_format()
 
         sys.exit(0)
 
         sys.exit(0)
 +
 +
    if opts.delete:
 +
        opts.safe = True
  
 
     if opts.chanid and opts.starttime:
 
     if opts.chanid and opts.starttime:
Line 444: Line 454:
 
             export = VIDEO(opts,int(args[0]))
 
             export = VIDEO(opts,int(args[0]))
 
         except Exception, e:
 
         except Exception, e:
             Job(int(args[0])).update({'status':304,
+
             Job(int(args[0])).update({'status':Job.ERRORED,
 
                                       'comment':'ERROR: '+e.args[0]})
 
                                       'comment':'ERROR: '+e.args[0]})
 +
            MythLog(module='mythvidexport.py').logTB(MythLog.GENERAL)
 
             sys.exit(1)
 
             sys.exit(1)
 
     else:
 
     else:
Line 453: Line 464:
 
             if opts.tformat:
 
             if opts.tformat:
 
                 print "Changing TV format to: "+opts.tformat
 
                 print "Changing TV format to: "+opts.tformat
                 db.setting[host]['mythvideo.TVexportfmt'] = opts.tformat
+
                 db.settings[host]['mythvideo.TVexportfmt'] = opts.tformat
 
             if opts.mformat:
 
             if opts.mformat:
 
                 print "Changing Movie format to: "+opts.mformat
 
                 print "Changing Movie format to: "+opts.mformat
                 db.setting[host]['mythvideo.MOVIEexportfmt'] = opts.mformat
+
                 db.settings[host]['mythvideo.MOVIEexportfmt'] = opts.mformat
 
             if opts.gformat:
 
             if opts.gformat:
 
                 print "Changing Generic format to: "+opts.gformat
 
                 print "Changing Generic format to: "+opts.gformat
                 db.setting[hosts]['mythvideo.GENERICexportfmt'] = opts.gformat
+
                 db.settings[host]['mythvideo.GENERICexportfmt'] = opts.gformat
 
             sys.exit(0)
 
             sys.exit(0)
 
         else:
 
         else:

Revision as of 00:57, 20 August 2014

Important.png Note: The correct title of this article is mythvidexport.py. It appears incorrectly here due to technical restrictions.


Author Raymond Wagner
Description A python script intended to be run as a user job allowing for exporting recordings into MythVideo.
Supports Version25.png  Version26.png Version27.png 


This python script is intended to function as a user job, capable of exporting recordings into MythVideo.

This script can be run from the command line, or called through the mythtv
jobqueue.  The input format will be:
  mythvidexport.py [options] <--chanid <channel id>> <--starttime <start time>>
                 --- or ---
  mythvidexport.py [options] %JOBID%

Options are:
        --mformat <format string>
        --tformat <format string>
        --gformat <format string>
            overrides the stored format string for a single run
        --listingonly
            use EPG data rather than grabbers for metadata
            will still try to grab episode and season information from ttvdb.py
        --seekdata             copy seek data from recording
        --skiplist             copy commercial detection from recording
        --cutlist              copy manual commercial cutlist from recording

Additional functions are available beyond exporting video
  mythvidexport.py <options>
        -h, --help             show this help message
        -p, --printformat      print existing format strings
        -f, --helpformat       lengthy description for formatting strings
        --mformat <string>     replace existing Movie format
        --tformat <string>     replace existing TV format
        --gformat <string>     replace existing Generic format


PythonIcon.png mythvidexport.py

#!/usr/bin/env python
# -*- coding: UTF-8 -*-
#---------------------------
# Name: mythvidexport.py
# Python Script
# Author: Raymond Wagner
# Purpose
# This python script is intended to function as a user job, run through
# mythjobqueue, capable of exporting recordings into MythVideo.
#---------------------------
__title__ = "MythVidExport"
__author__ = "Raymond Wagner"
__version__= "v0.7.6"

from MythTV import MythDB, Job, Recorded, Video, VideoGrabber,\
                   MythLog, MythError, static, MythBE
from optparse import OptionParser, OptionGroup
from socket import gethostname

import os
import re
import sys
import time
import hashlib

def create_dummy_video(db=None):
    db = MythDB(db)

def hashfile(fd):
    hasher = hashlib.sha1()
    while True:
        buff = fd.read(2**16)
        if len(buff) == 0:
            break
        hasher.update(buff)
    return hasher.hexdigest()

class VIDEO:
    def __init__(self, opts, jobid=None):
        if jobid:
            self.job = Job(jobid)
            self.chanid = self.job.chanid
            self.starttime = self.job.starttime
            self.job.update(status=Job.STARTING)
        else:
            self.job = None
            self.chanid = opts.chanid
            self.starttime = opts.starttime

        self.opts = opts
        self.db = MythDB()
        self.log = MythLog(module='mythvidexport.py', db=self.db)

        # load setting strings
        self.get_format()

        # prep objects
        self.rec = Recorded((self.chanid,self.starttime), db=self.db)
        if "_" in self.rec.inetref:
                self.rec.inetref = self.rec.inetref.split("_")[1]
        self.log(MythLog.GENERAL, MythLog.INFO, 'Using recording',
                        '%s - %s' % (self.rec.title.encode('utf-8'),
                                     self.rec.subtitle.encode('utf-8')))
        self.vid = Video(db=self.db).create({'title':'', 'filename':'',
                                             'host':gethostname()})

        # process data
        self.get_meta()
        self.get_dest()
        # bug fix to work around limitation in the bindings where DBDataRef classes
        # are mapped to the filename at time of Video element creation. since the
        # filename is specified as blank when the video is created, the markup
        # handler is not properly initialized
        self.vid.markup._refdat = (self.vid.filename,)

        # save file
        self.copy()
        if opts.seekdata:
            self.copy_seek()
        if opts.skiplist:
            self.copy_markup(static.MARKUP.MARK_COMM_START,
                             static.MARKUP.MARK_COMM_END)
        if opts.cutlist:
            self.copy_markup(static.MARKUP.MARK_CUT_START,
                             static.MARKUP.MARK_CUT_END)
        self.vid.update()

        # delete old file
        if opts.delete:
            self.rec.delete()

    def get_format(self):
        host = self.db.gethostname()
        # TV Format
        if self.opts.tformat:
            self.tfmt = self.opts.tformat
        elif self.db.settings[host]['mythvideo.TVexportfmt']:
            self.tfmt = self.db.settings[host]['mythvideo.TVexportfmt']
        else:
            self.tfmt = 'Television/%TITLE%/Season %SEASON%/'+\
                            '%TITLE% - S%SEASON%E%EPISODEPAD% - %SUBTITLE%'

        # Movie Format
        if self.opts.mformat:
            self.mfmt = self.opts.mformat
        elif self.db.settings[host]['mythvideo.MOVIEexportfmt']:
            self.mfmt = self.db.settings[host]['mythvideo.MOVIEexportfmt']
        else:
            self.mfmt = 'Movies/%TITLE%'

        # Generic Format
        if self.opts.gformat:
            self.gfmt = self.opts.gformat
        elif self.db.settings[host]['mythvideo.GENERICexportfmt']:
            self.gfmt = self.db.settings[host]['mythvideo.GENERICexportfmt']
        else:
            self.gfmt = 'Videos/%TITLE%'

    def get_meta(self):
        self.vid.hostname = self.db.gethostname()
        if self.rec.inetref:
            # good data is available, use it
            if self.rec.season > 0 or self.rec.episode > 0:
                self.log(self.log.GENERAL, self.log.INFO,
                        'Performing TV export with local data.')
                self.type = 'TV'
                grab = VideoGrabber(self.type)
                metadata = grab.grabInetref(self.rec.inetref, self.rec.season, self.rec.episode)
            else:
                self.log(self.log.GENERAL, self.log.INFO,
                        'Performing Movie export with local data.')
                self.type = 'MOVIE'
                grab = VideoGrabber(self.type)
                metadata = grab.grabInetref(self.rec.inetref)
        elif self.opts.listingonly:
            # force use of local data
            if self.rec.subtitle:
                self.log(self.log.GENERAL, self.log.INFO,
                        'Forcing TV export with local data.')
                self.type = 'TV'
            else:
                self.log(self.log.GENERAL, self.log.INFO,
                        'Forcing Movie export with local data.')
                self.type = 'MOVIE'
            metadata = self.rec.exportMetadata()
        else:
            if self.rec.subtitle:
                # subtitle exists, assume tv show
                self.type = 'TV'
                self.log(self.log.GENERAL, self.log.INFO,
                        'Attempting TV export.')
                grab = VideoGrabber(self.type)
                match = grab.sortedSearch(self.rec.title, self.rec.subtitle)
            else: # assume movie
                self.type = 'MOVIE'
                self.log(self.log.GENERAL, self.log.INFO,
                        'Attempting Movie export.')
                grab = VideoGrabber(self.type)
                match = grab.sortedSearch(self.rec.title)

            if len(match) == 0:
                # no match found
                self.log(self.log.GENERAL, self.log.INFO,
                        'Falling back to generic export.')
                self.type = 'GENERIC'
                metadata = self.rec.exportMetadata()
            elif (len(match) > 1) & (match[0].levenshtein > 0):
                # multiple matches found, and closest is not exact
                self.vid.delete()
                raise MythError('Multiple metadata matches found: '\
                                                   +self.rec.title)
            else:
                self.log(self.log.GENERAL, self.log.INFO,
                        'Importing content from', match[0].inetref)
                metadata = grab.grabInetref(match[0])

        self.vid.importMetadata(metadata)
        self.log(self.log.GENERAL, self.log.INFO, 'Import complete')

    def get_dest(self):
        if self.type == 'TV':
            self.vid.filename = self.process_fmt(self.tfmt)
        elif self.type == 'MOVIE':
            self.vid.filename = self.process_fmt(self.mfmt)
        elif self.type == 'GENERIC':
            self.vid.filename = self.process_fmt(self.gfmt)

    def process_fmt(self, fmt):
        # replace fields from viddata
        #print self.vid.data
        ext = '.'+self.rec.basename.rsplit('.',1)[1]
        rep = ( ('%TITLE%','title','%s'), ('%SUBTITLE%','subtitle','%s'),
            ('%SEASON%','season','%d'), ('%SEASONPAD%','season','%02d'),
            ('%EPISODE%','episode','%d'), ('%EPISODEPAD%','episode','%02d'),
            ('%YEAR%','year','%s'), ('%DIRECTOR%','director','%s'))
        for tag, data, format in rep:
            if self.vid[data]:
                fmt = fmt.replace(tag,format % self.vid[data])
            else:
                fmt = fmt.replace(tag,'')

        # replace fields from program data
        rep = ( ('%HOSTNAME%', 'hostname', '%s'),
                ('%STORAGEGROUP%','storagegroup','%s'))
        for tag, data, format in rep:
            data = getattr(self.rec, data)
            fmt = fmt.replace(tag,format % data)

# fmt = fmt.replace('%CARDID%',self.rec.cardid)
# fmt = fmt.replace('%CARDNAME%',self.rec.cardid)
# fmt = fmt.replace('%SOURCEID%',self.rec.cardid)
# fmt = fmt.replace('%SOURCENAME%',self.rec.cardid)
# fmt = fmt.replace('%CHANNUM%',self.rec.channum)
# fmt = fmt.replace('%CHANNAME%',self.rec.cardid)

        if len(self.vid.genre):
            fmt = fmt.replace('%GENRE%',self.vid.genre[0].genre)
        else:
            fmt = fmt.replace('%GENRE%','')
# if len(self.country):
# fmt = fmt.replace('%COUNTRY%',self.country[0])
# else:
# fmt = fmt.replace('%COUNTRY%','')
        return fmt+ext

    def copy(self):
        stime = time.time()
        srcsize = self.rec.filesize
        htime = [stime,stime,stime,stime]

        self.log(MythLog.GENERAL|MythLog.FILE, MythLog.INFO, "Copying myth://%s@%s/%s"\
               % (self.rec.storagegroup, self.rec.hostname, self.rec.basename)\
                                                    +" to myth://Videos@%s/%s"\
                                          % (self.vid.host, self.vid.filename))
        srcfp = self.rec.open('r')
        dstfp = self.vid.open('w', nooverwrite=True)

        if self.job:
            self.job.setStatus(Job.RUNNING)
        tsize = 2**24
        while tsize == 2**24:
            tsize = min(tsize, srcsize - dstfp.tell())
            dstfp.write(srcfp.read(tsize))
            htime.append(time.time())
            rate = float(tsize*4)/(time.time()-htime.pop(0))
            remt = (srcsize-dstfp.tell())/rate
            if self.job:
                self.job.setComment("%02d%% complete - %d seconds remaining" %\
                            (dstfp.tell()*100/srcsize, remt))
        srcfp.close()
        dstfp.close()

        self.vid.hash = self.vid.getHash()

        self.log(MythLog.GENERAL|MythLog.FILE, MythLog.INFO, "Transfer Complete",
                            "%d seconds elapsed" % int(time.time()-stime))

        if self.opts.reallysafe:
            if self.job:
                self.job.setComment("Checking file hashes")
            self.log(MythLog.GENERAL|MythLog.FILE, MythLog.INFO, "Checking file hashes.")
            srchash = hashfile(self.rec.open('r'))
            dsthash = hashfile(self.rec.open('r'))
            if srchash != dsthash:
                raise MythError('Source hash (%s) does not match destination hash (%s)' \
                            % (srchash, dsthash))
        elif self.opts.safe:
            self.log(MythLog.GENERAL|MythLog.FILE, MythLog.INFO, "Checking file sizes.")
            be = MythBE(db=self.vid._db)
            try:
                srcsize = be.getSGFile(self.rec.hostname, self.rec.storagegroup, \
                                       self.rec.basename)[1]
                dstsize = be.getSGFile(self.vid.host, 'Videos', self.vid.filename)[1]
            except:
                raise MythError('Could not query file size from backend')
            if srcsize != dstsize:
                raise MythError('Source size (%d) does not match destination size (%d)' \
                            % (srcsize, dstsize))

        if self.job:
            self.job.setComment("Complete - %d seconds elapsed" % \
                            (int(time.time()-stime)))
            self.job.setStatus(Job.FINISHED)

    def copy_seek(self):
        for seek in self.rec.seek:
            self.vid.markup.add(seek.mark, seek.offset, seek.type)

    def copy_markup(self, start, stop):
        for mark in self.rec.markup:
            if mark.type in (start, stop):
                self.vid.markup.add(mark.mark, 0, mark.type)

def usage_format():
    usagestr = """The default strings are:
Television: Television/%TITLE%/Season %SEASON%/%TITLE% - S%SEASON%E%EPISODEPAD% - %SUBTITLE%
Movie: Movies/%TITLE%
Generic: Videos/%TITLE%

Available strings:
%TITLE%: series title
%SUBTITLE%: episode title
%SEASON%: season number
%SEASONPAD%: season number, padded to 2 digits
%EPISODE%: episode number
%EPISODEPAD%: episode number, padded to 2 digits
%YEAR%: year
%DIRECTOR%: director
%HOSTNAME%: backend used to record show
%STORAGEGROUP%: storage group containing recorded show
%GENRE%: first genre listed for recording
"""
# %CARDID%: ID of tuner card used to record show
# %CARDNAME%: name of tuner card used to record show
# %SOURCEID%: ID of video source used to record show
# %SOURCENAME%: name of video source used to record show
# %CHANNUM%: ID of channel used to record show
# %CHANNAME%: name of channel used to record show
# %COUNTRY%: first country listed for recording
    print usagestr

def print_format():
    db = MythDB()
    host = gethostname()
    tfmt = db.settings[host]['mythvideo.TVexportfmt']
    if not tfmt:
        tfmt = 'Television/%TITLE%/Season %SEASON%/%TITLE% - S%SEASON%E%EPISODEPAD% - %SUBTITLE%'
    mfmt = db.settings[host]['mythvideo.MOVIEexportfmt']
    if not mfmt:
        mfmt = 'Movies/%TITLE%'
    gfmt = db.settings[host]['mythvideo.GENERICexportfmt']
    if not gfmt:
        gfmt = 'Videos/%TITLE%'
    print "Current output formats:"
    print " TV: "+tfmt
    print " Movies: "+mfmt
    print " Generic: "+gfmt

def main():
    parser = OptionParser(usage="usage: %prog [options] [jobid]")

    formatgroup = OptionGroup(parser, "Formatting Options",
                    "These options are used to display and manipulate the output file formats.")
    formatgroup.add_option("-f", "--helpformat", action="store_true", default=False, dest="fmthelp",
            help="Print explination of file format string.")
    formatgroup.add_option("-p", "--printformat", action="store_true", default=False, dest="fmtprint",
            help="Print current file format string.")
    formatgroup.add_option("--tformat", action="store", type="string", dest="tformat",
            help="Use TV format for current task. If no task, store in database.")
    formatgroup.add_option("--mformat", action="store", type="string", dest="mformat",
            help="Use Movie format for current task. If no task, store in database.")
    formatgroup.add_option("--gformat", action="store", type="string", dest="gformat",
            help="Use Generic format for current task. If no task, store in database.")
    formatgroup.add_option("--listingonly", action="store_true", default=False, dest="listingonly",
            help="Use data from listing provider, rather than grabber")
    parser.add_option_group(formatgroup)

    sourcegroup = OptionGroup(parser, "Source Definition",
                    "These options can be used to manually specify a recording to operate on "+\
                    "in place of the job id.")
    sourcegroup.add_option("--chanid", action="store", type="int", dest="chanid",
            help="Use chanid for manual operation")
    sourcegroup.add_option("--starttime", action="store", type="int", dest="starttime",
            help="Use starttime for manual operation")
    parser.add_option_group(sourcegroup)

    actiongroup = OptionGroup(parser, "Additional Actions",
                    "These options perform additional actions after the recording has been exported.")
    actiongroup.add_option('--safe', action='store_true', default=False, dest='safe',
            help='Perform quick sanity check of exported file using file size.')
    actiongroup.add_option('--really-safe', action='store_true', default=False, dest='reallysafe',
            help='Perform slow sanity check of exported file using SHA1 hash.')
    actiongroup.add_option("--delete", action="store_true", default=False,
            help="Delete source recording after successful export. Enforces use of --safe.")
    parser.add_option_group(actiongroup)

    othergroup = OptionGroup(parser, "Other Data",
                    "These options copy additional information from the source recording.")
    othergroup.add_option("--seekdata", action="store_true", default=False, dest="seekdata",
            help="Copy seekdata from source recording.")
    othergroup.add_option("--skiplist", action="store_true", default=False, dest="skiplist",
            help="Copy commercial detection from source recording.")
    othergroup.add_option("--cutlist", action="store_true", default=False, dest="cutlist",
            help="Copy manual commercial cuts from source recording.")
    parser.add_option_group(othergroup)

    MythLog.loadOptParse(parser)

    opts, args = parser.parse_args()

    if opts.verbose:
        if opts.verbose == 'help':
            print MythLog.helptext
            sys.exit(0)
        MythLog._setlevel(opts.verbose)

    if opts.fmthelp:
        usage_format()
        sys.exit(0)

    if opts.fmtprint:
        print_format()
        sys.exit(0)

    if opts.delete:
        opts.safe = True

    if opts.chanid and opts.starttime:
        export = VIDEO(opts)
    elif len(args) == 1:
        try:
            export = VIDEO(opts,int(args[0]))
        except Exception, e:
            Job(int(args[0])).update({'status':Job.ERRORED,
                                      'comment':'ERROR: '+e.args[0]})
            MythLog(module='mythvidexport.py').logTB(MythLog.GENERAL)
            sys.exit(1)
    else:
        if opts.tformat or opts.mformat or opts.gformat:
            db = MythDB()
            host = gethostname()
            if opts.tformat:
                print "Changing TV format to: "+opts.tformat
                db.settings[host]['mythvideo.TVexportfmt'] = opts.tformat
            if opts.mformat:
                print "Changing Movie format to: "+opts.mformat
                db.settings[host]['mythvideo.MOVIEexportfmt'] = opts.mformat
            if opts.gformat:
                print "Changing Generic format to: "+opts.gformat
                db.settings[host]['mythvideo.GENERICexportfmt'] = opts.gformat
            sys.exit(0)
        else:
            parser.print_help()
            sys.exit(2)

if __name__ == "__main__":
    main()