diff options
| -rwxr-xr-x | abs/core/LinHES-system/PKGBUILD | 5 | ||||
| -rwxr-xr-x | abs/core/LinHES-system/mythvidexport.py | 439 | 
2 files changed, 442 insertions, 2 deletions
| diff --git a/abs/core/LinHES-system/PKGBUILD b/abs/core/LinHES-system/PKGBUILD index 97d7fa0..387793b 100755 --- a/abs/core/LinHES-system/PKGBUILD +++ b/abs/core/LinHES-system/PKGBUILD @@ -1,6 +1,6 @@  pkgname=LinHES-system  pkgver=8.1 -pkgrel=15 +pkgrel=16  arch=('i686' 'x86_64')  install=system.install  pkgdesc="Everything that makes LinHES an automated system" @@ -18,7 +18,7 @@ binfiles="LinHES-start optimize_mythdb.py myth_mtc.py   lh_system_backup lh_system_backup_job lh_system_restore_job   lh_system_host_update lh_system_all_host_update   add_storage.py diskspace.sh cacheclean lh_backend_control.sh - create_media_dirs.sh msg_client.py msg_daemon.py + create_media_dirs.sh msg_client.py msg_daemon.py mythvidexport.py   gen_is_xml.py gen_lib_xml.py gen_light_include.py gen_game_xml.py   misc_recent_recordings.pl misc_status_config.py misc_status_info.sh   misc_upcoming_recordings.pl misc_which_recorder.pl jobqueue_helper.py @@ -102,6 +102,7 @@ md5sums=('7ab2a2c643d2b286811d8303d08982ad'           '7f7c49d859abdaa0b5fca399241d4998'           '3e60b17892e5b8214d47dcfddf5215a4'           '57ec994cc3964a10c00580e89ebcae35' +         '7ba5e774bfebc3ec2469c9fe9a76e2ce'           '0c3509b48f11af0dc1bf989721fe9ca7'           'ac61cc460d9e97ba1f5ef69e92cdfbe5'           'f3502bb7c665750da0ecdf6918f7c838' diff --git a/abs/core/LinHES-system/mythvidexport.py b/abs/core/LinHES-system/mythvidexport.py new file mode 100755 index 0000000..5fdfb6d --- /dev/null +++ b/abs/core/LinHES-system/mythvidexport.py @@ -0,0 +1,439 @@ +#!/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. +# https://github.com/wagnerrp/mythtv-scripts/blob/master/python/mythvidexport.py +# http://www.mythtv.org/wiki/Mythvidexport.py +#--------------------------- +__title__  = "MythVidExport" +__author__ = "Raymond Wagner" +__version__= "v0.7.5" + +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) +        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.vid.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="string", 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() + | 
