Myth-Rec-to-Vid.py

From MythTV Official Wiki
Jump to: navigation, search


Author Scott Morton
Description This python script migrates videos from Myth Recordings to Myth Video.
Supports Version26.png Version27.png Version28.png 


Features (among others)

   - Adjustable destination formats
   - Duplication detection at destination
   - National Character support

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

Keep up to date at https://github.com/spmorton/Myth-Scripts

Run this script from a terminal/CL with no options for usage information.

ex. python Myth-Rec-to-Vid.py

Instructions

   - Copy this script to a folder of your choosing that is accessable to the BE
   - Be sure to chmod +x Myth-Rec-to-Vid.py to make the script executable(Linux)
   - Configure a user job to run this script with your options plus %JOBID% to capture the job number
   - Add %VERBOSEMODE% to the command line to have script logging in a separate file for each job (otherwise it is discarded)


PythonIcon.png Myth-Rec-to-Vid.py

#!/usr/bin/env python2
# -*- coding: utf-8 -*-
#---------------------------
#   Name: Myth-Rec-to-Vid.py
#   Python Script
#   Author: Scott Morton
# 
#   This is a rewrite of a script by Raymond Wagner
#   The objective is to clean it up and streamline 
#   the code for use with Myth 26


#   Migrates MythTV Recordings to MythVideo in Version .26.
#---------------------------


__title__  = "Myth-Rec-to-Vid"
__author__ = "Scott Morton"
__version__= "v1.2.1"

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

import sys, time

# Global Constants

# Modify these setting to your prefered defaults
TVFMT = 'Television/%TITLE%/Season %SEASON%/'+\
                    '%TITLE% - S%SEASON%E%EPISODEPAD% - %SUBTITLE%'

MVFMT = 'Movies/%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

class VIDEO:
    def __init__(self, opts, jobid=None):                           
        
        # Setup for the job to run
        if jobid:
            self.thisJob = Job(jobid)
            self.chanID = self.thisJob.chanid
            self.startTime = self.thisJob.starttime
            self.thisJob.update(status=Job.STARTING)
        
        # If no job ID given, must be a command line run
        else:
            self.thisJob = jobid
            self.chanID = opts.chanid
            self.startTime = opts.startdate + " " + opts.starttime + opts.offset
        self.opts = opts
        self.type = "none"
        self.db = MythDB()
        self.log = MythLog(module='Myth-Rec-to-Vid.py', db=self.db)
        

        # Capture the backend host name
        self.host = self.db.gethostname()

        # prep objects
        self.rec = Recorded((self.chanID,self.startTime), db=self.db)
        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':self.host})

        self.bend = MythBE(db=self.db)
        
        
    def check_hash(self):
        self.log(self.log.GENERAL, self.log.INFO,
                 'Performing copy validation.')
        srchash = self.bend.getHash(self.rec.basename, self.rec.storagegroup)
        dsthash = self.bend.getHash(self.vid.filename, 'Videos')
        if srchash != dsthash:
            return False
        else:
            return True
               
    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.host, self.vid.filename))
        
 
        srcfp = self.rec.open('r')
        dstfp = self.vid.open('w')

        if self.thisJob:
            self.set_job_status(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.thisJob:
                self.thisJob.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.thisJob:
            self.thisJob.setComment("Complete - %d seconds elapsed" % \
        	      (int(time.time()-stime)))

    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 copy_seek(self):
        for seek in self.rec.seek:
            self.vid.markup.add(seek.mark, seek.offset, seek.type)
                
    def delete_vid(self):
        self.vid.delete()
        
    def delete_rec(self):
        self.rec.delete()
        
    def dup_check(self):
        self.log(MythLog.GENERAL, MythLog.INFO, 'Processing new file name ',
                    '%s' % (self.vid.filename))
        self.log(MythLog.GENERAL, MythLog.INFO, 'Checking for duplication of ',
                    '%s - %s' % (self.rec.title.encode('utf-8'), 
                                 self.rec.subtitle.encode('utf-8')))
        if self.bend.fileExists(self.vid.filename, 'Videos'):
            self.log(MythLog.GENERAL, MythLog.INFO, 'Recording already exists in Myth Videos')
            if self.thisJob:
                self.thisJob.setComment("Action would result in duplicate entry" )
            return True
          
        else:
            self.log(MythLog.GENERAL, MythLog.INFO, 'No duplication found for ',
                    '%s - %s' % (self.rec.title.encode('utf-8'), 
                                 self.rec.subtitle.encode('utf-8')))
            return False 

    def get_dest(self):
        if self.type == 'TV':
            self.vid.filename = self.process_fmt(TVFMT)
        elif self.type == 'MOVIE':
            self.vid.filename = self.process_fmt(MVFMT)
        
        self.vid.markup._refdat = (self.vid.filename,)

    def get_meta(self):
        metadata = self.rec.exportMetadata()
        yrInfo = self.rec.getProgram()
        metadata['year'] = yrInfo.get('year')
        self.vid.importMetadata(metadata)
        if self.type == 'MOVIE':
            grab = VideoGrabber('Movie')
            results = grab.sortedSearch(self.rec.title)
            if len(results) > 0:
                for i in results:
                    if i.year == yrInfo.get('year') and i.title == self.rec.get('title'):
                        self.vid.importMetadata(i)
                        match = grab.grabInetref(i.get('inetref'))
                        length = len(match.people)
                        for p in range(length-2):
                            self.vid.cast.add(match.people[p].get('name'))
                        self.vid.director = match.people[length - 1].get('name')
                        import_info = 'Full MetaData Import complete'
            elif len(results) == 0:
                import_info = 'Listing only MetaData import complete'
        else:
            grab = VideoGrabber('TV')
            results = grab.sortedSearch(self.rec.title, self.rec.subtitle)
            if len(results) > 0:
                for i in results:
                    if  i.title == self.rec.get('title') and i.subtitle == self.rec.get('subtitle'):
                        self.vid.importMetadata(i)
                        match = grab.grabInetref(grab.grabInetref(i.get('inetref'), \
                                season=i.get('season'),episode=i.get('episode')))
                        length = len(match.people)
                        for p in range(length-2):
                            self.vid.cast.add(match.people[p].get('name'))
                        self.vid.director = match.people[length - 1].get('name')
                        import_info = 'Full MetaData Import complete'
            elif len(results) == 0:
                import_info = 'Listing only MetaData import complete'
            
        
        self.vid.category = self.rec.get('category')

        self.log(self.log.GENERAL, self.log.INFO, import_info)

    def get_type(self):
        if self.rec.seriesid != None and self.rec.programid[:2] != 'MV':
            self.type = 'TV'
            self.log(self.log.GENERAL, self.log.INFO,
                    'Performing TV type migration.')
        else:
            self.type = 'MOVIE'
            self.log(self.log.GENERAL, self.log.INFO,
                    'Performing Movie type migration.')

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


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

    def set_job_status(self, status):
        self.thisJob.setStatus(status)
        
    def set_vid_hash(self):
        self.vid.hash = self.vid.getHash()
    
    def update_vid(self):
        self.vid.update()
        
#---END CLASS--------------------------------------------------

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

    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, format interger")
    sourcegroup.add_option("--startdate", action="store", type="string", dest="startdate",
            help="Use startdate for manual operation, format is year-mm-dd")
    sourcegroup.add_option("--starttime", action="store", type="string", dest="starttime",
            help="Use starttime for manual operation, format is hh:mm:ss in UTC")
    sourcegroup.add_option("--offset", action="store", type="string", dest="offset",
            help="Use offset(timezone) for manual operation, format is [+/-]hh:mm. Do not adjust for DST")
    parser.add_option_group(sourcegroup)

    actiongroup = OptionGroup(parser, "Additional Actions",
                    "These options perform additional actions after the recording has been migrated. "+\
                    "A safe copy is always performed in that the file is checked to match the "+\
                    "MythBE hash. The safe copy option will abort the entire process if selected "+\
                    "along with Other Data and an exception occurs in the process")
    actiongroup.add_option('--safe', action='store_true', default=False, dest='safe',
            help='If other data is copied and a failure occurs this will abort the whole process.')
    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()

    def error_out():
        export.delete_vid()
        if export.thisJob:        
            export.set_job_status(Job.ERRORED)
        sys.exit(1)
        
    if opts.verbose:
        if opts.verbose == 'help':
            print(MythLog.helptext)
            sys.exit(0)
        MythLog._setlevel(opts.verbose)

    if opts.delete:
        opts.safe = True

    # if a manual channel and time entry then setup the export with opts
    if opts.chanid and opts.startdate and opts.starttime and opts.offset:
        try:
            export = VIDEO(opts)

        except Exception as e:
            sys.exit(1)

    # If an auto or manual job entry then setup the export with the jobID
    elif len(args) == 1:
        try:
            export = VIDEO(opts,int(args[0]))

        except Exception as e:
            Job(int(args[0])).update({'status':Job.ERRORED,
                                      'comment':'ERROR: '+e.args[0]})
            MythLog(module='Myth-Rec-to-Vid.py').logTB(MythLog.GENERAL)
            sys.exit(1)

    # else bomb the job and return an error code
    else:
        parser.print_help()
        sys.exit(2)

    # Export object created so process the job
    try:
        export.get_type()
        export.get_meta()
        export.get_dest()

    except Exception as e:
        export.log(MythLog.GENERAL|MythLog.FILE, MythLog.INFO, "ERROR in Processing ",
    			      "Message was: %s" % e.message)
        error_out()

    if (export.dup_check()):
        export.delete_vid()
        if export.thisJob:
            export.set_job_status(Job.FINISHED)
        sys.exit(0)

    else:
        try:
            export.copy()
            export.vid.update()
            export.set_vid_hash()

        except Exception as e:
            export.log(MythLog.GENERAL|MythLog.FILE, MythLog.INFO, "ERROR in Processing ",
        			      "Message was: %s" % e.message)
            error_out()

        try:
            if not export.check_hash():
                error_out()
                
        except Exception as e:
            export.log(MythLog.GENERAL|MythLog.FILE, MythLog.INFO, "ERROR in Hash Check",
        			      "Message was: %s" % e.message)
            error_out()

    if opts.seekdata:
        try:
            export.copy_seek()

        except Exception as e:
            export.log(MythLog.GENERAL|MythLog.FILE, MythLog.INFO, "ERROR in Seek Data",
        			      "Message was: %s" % e.message)
            if opts.safe:
                error_out()

    if opts.skiplist:
        try:
            export.copy_markup(static.MARKUP.MARK_COMM_START,
                         static.MARKUP.MARK_COMM_END)

        except Exception as e:
            export.log(MythLog.GENERAL|MythLog.FILE, MythLog.INFO, "ERROR in Skip List",
        			      "Message was: %s" % e.message)
            if opts.safe:
                error_out()

    if opts.cutlist:
        try:
            export.copy_markup(static.MARKUP.MARK_CUT_START,
                         static.MARKUP.MARK_CUT_END)
        except Exception as e:
            export.log(MythLog.GENERAL|MythLog.FILE, MythLog.INFO, "ERROR in Cut List",
        			      "Message was: %s" % e.message)
            if opts.safe:
                error_out()

    # delete old file
    if opts.delete:
        try:
            export.delete_rec()

        except Exception as e:
            export.log(MythLog.GENERAL|MythLog.FILE, MythLog.INFO, "ERROR in Delete Orig",
        			      "Message was: %s" % e.message)
            if opts.safe:
                error_out()
    
    export.set_job_status(Job.FINISHED)
    

if __name__ == "__main__":
    main()