From MythTV Official Wiki
Jump to: navigation, search

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

Author Jay Harbeston, using Raymond Wagner's version for python3 mods
Description A python script intended to be run as a user job allowing for exporting recordings into MythVideo.
Supports Version31.png 

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

NOTE that in this version metadatalookup has been removed from the code as it was causing the script to fail, and metadata wasn't important to me.

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

Options are:
        --mformat <format string>
        --tformat <format string>
        --gformat <format string>
            overrides the stored format string for a single run
            use EPG data rather than grabbers for metadata
            will still try to grab episode and season information from
        --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 <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


#!/usr/bin/env python3
# -*- coding: UTF-8 -*-
# Name:
# 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.
#   The code was modified to be compatible with python3.

#   NOTE, the metadatalookup has been removed as there were problems causing
#   the script to crash, and I didn't need them, so I removed them.

__title__  = "MythVidExport"
__author__ = "Raymond Wagner, jr3us"
__version__= "v0.7.7"

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 datetime
import hashlib

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

def hashfile(fd):
    hasher = hashlib.sha1()
    while True:
        buff =**16)
        if len(buff) == 0:
    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 = None
            self.chanid = opts.chanid
            self.starttime = opts.starttime

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

        # load setting strings

        # 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':'',

        # process data
        # 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
        if opts.seekdata:
        if opts.skiplist:
        if opts.cutlist:

        # delete old file
        if opts.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']
            self.tfmt = 'Television/%TITLE%/Season %SEASONPAD%/'+\
                            '%TITLE% - S%SEASONPAD%E%EPISODEPAD% - %SUBTITLE%'
   = "%02d" % self.starttime.hour
            self.minit = "%02d" % self.starttime.minute
            self.sec = "%02d" % self.starttime.second
            self.year = "%0d" % self.starttime.year
   = "%02d" % self.starttime.month
   = "%02d" %
            sttimestr = self.year + + +
            sttimestr = sttimestr + self.minit + self.sec
            self.tfmt = self.tfmt + "-%s" % self.chanid + "-" + sttimestr

        # 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']
            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']
            self.gfmt = 'Videos/%TITLE% - S%SEASONPAD%E%EPISODEPAD% - %SUBTITLE%-'
   = "%02d" % self.starttime.hour
            self.minit = "%02d" % self.starttime.minute
            self.sec = "%02d" % self.starttime.second
            self.year = "%0d" % self.starttime.year
   = "%02d" % self.starttime.month
   = "%02d" %
            sttimestr = self.year + + +
            sttimestr = sttimestr + self.minit + self.sec
            self.gfmt = self.gfmt + "-%s" % self.chanid + "-" + sttimestr

    def get_meta(self):
        self.vid.hostname = self.db.gethostname()
        if self.rec.inetref:
            # good data is available, use it
            if self.rec.season is not None:
                self.log(self.log.GENERAL, self.log.INFO,
                        'Performing TV export with local data.')
                self.type = 'TV'
                self.log(self.log.GENERAL, self.log.INFO,
                        'Performing Movie export with local data.')
                self.type = 'MOVIE'
            metadata = self.rec.exportMetadata()
        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'
                self.log(self.log.GENERAL, self.log.INFO,
                        'Forcing Movie export with local data.')
                self.type = 'MOVIE'
            metadata = self.rec.exportMetadata()
            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
                raise MythError('Multiple metadata matches found: '\
                self.log(self.log.GENERAL, self.log.INFO,
                        'Importing content from', match[0].inetref)
                metadata = grab.grabInetref(match[0])

        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)
            tmpjrh = self.process_fmt(self.tfmt)
        elif self.type == 'MOVIE':
            # self.vid.filename = self.process_fmt(self.mfmt)
            tmpjrh = self.process_fmt(self.mfmt)
        elif self.type == 'GENERIC':
            # self.vid.filename = self.process_fmt(self.gfmt)
            tmpjrh = self.process_fmt(self.gfmt)
        self.vid.filename = tmpjrh

    def process_fmt(self, fmt):
        # replace fields from viddata
        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:
            tmpjrh = self.vid[data]
            if data in ( 'title', 'subtitle') :
                tmpjrh = re.sub('[!@&?;:/]','',tmpjrh)
                # print ('fixed: ', tmpjrh )
            self.vid[data] = tmpjrh
            if self.vid[data]:
                fmt = fmt.replace(tag,format % self.vid[data])
                fmt = fmt.replace(tag,'')

        # replace fields from program data
        rep = ( ('%HOSTNAME%',    'hostname',    '%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)
            fmt = fmt.replace('%GENRE%','')
#       if len(
#           fmt = fmt.replace('%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]

        #sub spaces for utf8 -- jrh
        self.vid.filename = re.sub(r'[^\x00-\x7F]+',' ', self.vid.filename)

        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.filename))
        srcfp ='r')
        dstfp ='w')

        if self.job:
        tsize = 2**24
        while tsize == 2**24:
            tsize = min(tsize, srcsize - dstfp.tell())
            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))

        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('r'))
            dsthash = hashfile('r'))
            if srchash != dsthash:
                raise MythError('Source hash (%s) does not match destination hash (%s)' \
                            % (srchash, dsthash))
            self.log(MythLog.GENERAL|MythLog.FILE, MythLog.INFO, "Checking file sizes.")
            be = MythBE(db=self.vid._db)
                srcsize = be.getSGFile(self.rec.hostname, self.rec.storagegroup, \
                dstsize = be.getSGFile(, 'Videos', self.vid.filename)[1]
                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" % \

    def copy_seek(self):
        for seek in
            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")

    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")

    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.")

    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.")


    opts, args = parser.parse_args()

    if opts.verbose:
        if opts.verbose == 'help':
            print (MythLog.helptext)

    if opts.fmthelp:

    if opts.fmtprint:

    if opts.delete: = True

    if opts.chanid and opts.starttime:
        export = VIDEO(opts)
    elif len(args) == 1:
            export = VIDEO(opts,int(args[0]))
        except Exception as e:
                                      'comment':'ERROR: '+str(e.args[0])})
        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[hosts]['mythvideo.GENERICexportfmt'] = opts.gformat

if __name__ == "__main__":