383 lines
15 KiB
Python
Executable File
383 lines
15 KiB
Python
Executable File
#!/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.7.2"
|
|
|
|
usage_txt = """
|
|
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
|
|
--seektable 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
|
|
"""
|
|
|
|
from MythTV import MythDB, Job, Recorded, Video, VideoGrabber,\
|
|
MythLog, MythError, static
|
|
from optparse import OptionParser
|
|
from socket import gethostname
|
|
|
|
import sys, re, os, time
|
|
|
|
MYVER = (0,24,0)
|
|
import MythTV
|
|
if MythTV.__version__[:3] != MYVER:
|
|
raise Exception('This script expects bindings of version %s. The currently installed version is %s.'\
|
|
% ('.'.join(MYVER), '.'.join(MythTV.__version__))
|
|
|
|
def create_dummy_video(db=None):
|
|
db = MythDB(db)
|
|
|
|
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=3)
|
|
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.IMPORTANT, 'Using recording',
|
|
'%s - %s' % (self.rec.title, self.rec.subtitle))
|
|
self.vid = Video(db=self.db).create({'title':'', 'filename':'', 'host':gethostname()})
|
|
|
|
# process data
|
|
self.get_meta()
|
|
self.get_dest()
|
|
|
|
# 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()
|
|
|
|
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.subtitle: # subtitle exists, assume tv show
|
|
self.type = 'TV'
|
|
self.log(self.log.IMPORTANT, '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.IMPORTANT, 'Attempting Movie export.')
|
|
grab = VideoGrabber(self.type)
|
|
match = grab.sortedSearch(self.rec.title)
|
|
|
|
if len(match) == 0:
|
|
# no match found
|
|
self.log(self.log.IMPORTANT, 'Falling back to generic export.')
|
|
self.get_generic()
|
|
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.IMPORTANT, 'Importing content from', match[0].inetref)
|
|
self.vid.importMetadata(grab.grabInetref(match[0]))
|
|
self.log(self.log.IMPORTANT, 'Import complete')
|
|
|
|
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.vid.cast.append(member.name)
|
|
self.type = 'GENERIC'
|
|
|
|
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.log(MythLog.IMPORTANT|MythLog.FILE, "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')
|
|
|
|
if self.job:
|
|
self.job.setStatus(4)
|
|
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.IMPORTANT|MythLog.FILE, "Transfer Complete",
|
|
"%d seconds elapsed" % int(time.time()-stime))
|
|
if self.job:
|
|
self.job.setComment("Complete - %d seconds elapsed" % \
|
|
(int(time.time()-stime)))
|
|
self.job.setStatus(256)
|
|
|
|
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]")
|
|
|
|
parser.add_option("-f", "--helpformat", action="store_true", default=False, dest="fmthelp",
|
|
help="Print explination of file format string.")
|
|
parser.add_option("-p", "--printformat", action="store_true", default=False, dest="fmtprint",
|
|
help="Print current file format string.")
|
|
parser.add_option("--tformat", action="store", type="string", dest="tformat",
|
|
help="Use TV format for current task. If no task, store in database.")
|
|
parser.add_option("--mformat", action="store", type="string", dest="mformat",
|
|
help="Use Movie format for current task. If no task, store in database.")
|
|
parser.add_option("--gformat", action="store", type="string", dest="gformat",
|
|
help="Use Generic format for current task. If no task, store in database.")
|
|
parser.add_option("--chanid", action="store", type="int", dest="chanid",
|
|
help="Use chanid for manual operation")
|
|
parser.add_option("--starttime", action="store", type="int", dest="starttime",
|
|
help="Use starttime for manual operation")
|
|
parser.add_option("--listingonly", action="store_true", default=False, dest="listingonly",
|
|
help="Use data from listing provider, rather than grabber")
|
|
parser.add_option("--seekdata", action="store_true", default=False, dest="seekdata",
|
|
help="Copy seekdata from source recording.")
|
|
parser.add_option("--skiplist", action="store_true", default=False, dest="skiplist",
|
|
help="Copy commercial detection from source recording.")
|
|
parser.add_option("--cutlist", action="store_true", default=False, dest="cutlist",
|
|
help="Copy manual commercial cuts from source recording.")
|
|
parser.add_option('-v', '--verbose', action='store', type='string', dest='verbose',
|
|
help='Verbosity level')
|
|
|
|
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.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':304,
|
|
'comment':'ERROR: '+e.args[0]})
|
|
raise
|
|
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.setting[host]['mythvideo.TVexportfmt'] = opts.tformat
|
|
if opts.mformat:
|
|
print "Changing Movie format to: "+opts.mformat
|
|
db.setting[host]['mythvideo.MOVIEexportfmt'] = opts.mformat
|
|
if opts.gformat:
|
|
print "Changing Generic format to: "+opts.gformat
|
|
db.setting[hosts]['mythvideo.GENERICexportfmt'] = opts.gformat
|
|
sys.exit(0)
|
|
else:
|
|
parser.print_help()
|
|
sys.exit(2)
|
|
|
|
if __name__ == "__main__":
|
|
main()
|
|
|