Ticket #6680: mythvidexport.13.py

File mythvidexport.13.py, 18.9 KB (added by Raymond Wagner <raymond@…>, 10 years ago)
Line 
1#!/usr/local/bin/python
2# -*- coding: UTF-8 -*-
3#---------------------------
4# Name: mythvidexport.py
5# Python Script
6# Author: Raymond Wagner
7# Purpose
8#   This python script is intended to function as a user job, run through
9#   mythjobqueue, capable of exporting recordings into MythVideo.
10#---------------------------
11__title__  = "MythVidExport"
12__author__ = "Raymond Wagner"
13__version__= "v0.5.0"
14
15usage_txt = """
16This script can be run from the command line, or called through the mythtv
17jobqueue.  The input format will be:
18  mythvidexport.py [options] <--chanid <channel id>> <--starttime <start time>>
19                 --- or ---
20  mythvidexport.py [options] %JOBID%
21
22Options are:
23        --mformat <format string>
24        --tformat <format string>
25        --gformat <format string>
26            overrides the stored format string for a single run
27        --listingonly
28            use EPG data rather than grabbers for metadata
29            will still try to grab episode and season information from ttvdb.py
30
31Additional functions are available beyond exporting video
32  mythvidexport.py <options>
33        -h, --help             show this help message
34        -p, --printformat      print existing format strings
35        -f, --helpformat       lengthy description for formatting strings
36        --mformat <string>     replace existing Movie format
37        --tformat <string>     replace existing TV format
38        --gformat <string>     replace existing Generic format
39"""
40
41from MythTV import MythTV, MythDB, MythLog, MythVideo, Program, FileTransfer, Job
42#from MythTV.MythLog import EMERGENCY, ALERT, CRITICAL, ERROR, WARNING, NOTICE, INFO, DEBUG, ALL
43from socket import gethostname
44from urllib import urlretrieve
45from optparse import OptionParser
46import sys, re, os, time
47
48
49#log = MythLog(NOTICE, 'MythVidExport.py')
50
51class VIDEO:
52        job = None
53        dest = None
54
55        viddata = {}
56        cast = []
57        genre = []
58        country = []
59
60        def __init__(self, opts, jobid):
61                if jobid:
62                        self.job = Job(jobid)
63                        self.chanid = self.job.chanid
64                        self.rtime = int("%04d%02d%02d%02d%02d%02d" % self.job.starttime.timetuple()[0:6])
65                        self.jobhost = self.job.host
66                        self.job.setStatus(3)
67                else:
68                        self.chanid = opts.chanid
69                        self.rtime = opts.starttime
70                        self.jobhost = gethostname()
71
72                self.opts = opts
73                self.mythdb = MythDB()
74                self.cursor = self.mythdb.cursor()
75
76                # load grabber scripts
77                self.get_grabber()
78
79                # load formatting strings
80                self.get_fmt()
81                if opts.tformat:
82                        selt.tfmt = opts.tformat
83                if opts.mformat:
84                        self.mfmt = opts.mformat
85                if opts.gformat:
86                        self.gfmt = opts.gformat
87
88                # process file
89                self.get_source()
90                self.get_meta()
91                self.get_dest()
92
93                # save file
94                self.copy()
95                self.write_meta()
96                self.seekdata()
97
98        def get_source(self):
99                ## connects to myth data socket and finds recording
100                mythinst = MythTV()
101                self.prog = mythinst.getRecording(self.chanid,self.rtime)
102
103                self.srchost = self.prog.hostname
104                self.source = mythinst.getCheckfile(self.prog)
105
106        def get_grabber(self):
107                # get tv grabber command
108                self.tvgrabber = self.mythdb.getSetting('mythvideo.TVGrabber', self.jobhost)
109
110                # insert optional ttvdb config file if using ttvdb grabber
111                if os.access(os.path.expanduser("~/.mythtv/ttvdb.conf"),os.F_OK):
112                        ttvdbconf = os.path.expanduser("~/.mythtv/ttvdb.conf")
113                        if self.tvgrabber.split('/')[-1] == "ttvdb.py":
114                                self.tvgrabber += ' -c '+ttvdbconf
115
116                # grab tmdb commands
117                self.moviegrabber = self.mythdb.getSetting('mythvideo.MovieGrabber', self.jobhost)
118
119        def get_meta(self):
120                if self.prog.subtitle:  # subtitle exists, assume tv show
121                        self.get_ttvdb()
122                else:                   # assume movie
123                        self.get_tmdb()
124
125        def get_ttvdb(self):
126                ## grab season and episode number from thetvdb.com, exit if failed
127                fp = os.popen("""%s -N "%s" "%s" """ % (self.tvgrabber,self.prog.title,self.prog.subtitle),'r')
128                match = re.search("S(?P<season>[0-9]*)E(?P<episode>[0-9]*)", fp.read())
129                fp.close()
130                if match is None:
131#                       log.warning('Grabber error', "TTVDB.py failed to get season/episode numbers for %s - %s. Reverting to generic export." % (self.prog.title, self.prog.subtitle))
132                        self.get_generic()
133                        return
134
135                self.viddata['season'] = int(match.group('season'))
136                self.viddata['episode'] = int(match.group('episode'))
137
138                if self.opts.listingonly:  # do not grab subsequent information from ttvdb
139                        self.get_generic()
140
141                else: # gather remaining data
142                        self.process_meta("""%s -D "%s" %d %d""" % (self.tvgrabber,self.prog.title,self.viddata['season'],self.viddata['episode']))
143                        self.viddata['title'] = self.prog.title
144                        if 'fanart' in self.viddata:
145                                fanart = self.viddata['fanart'].split(',')
146                                self.viddata['fanart'] = fanart[self.viddata['season']%len(fanart)]
147                        for type in ('coverfile','screenshot','banner'):
148                                if type in self.viddata:
149                                        self.viddata[type] = self.viddata[type].split(',')[0]
150
151                # set some common data         
152                self.country = ()
153                self.viddata['showlevel'] = 1
154                self.type = 'TV'
155
156        def get_tmdb(self):
157                if self.opts.listingonly:  # do not grab from tmdb
158                        self.get_generic()
159                else: # find inetref from tmdb
160                        match = None
161                        fp = os.popen("""%s "%s" """ % (self.tmdb['List'], self.prog.title))
162                        if self.prog.year:
163                                match = re.search("(?P<inetref>[0-9]*):%s[ ]*\(%s\)" % (self.prog.title, self.prog.year), fp.read())
164                        else:
165                                match = re.search("(?P<inetref>[0-9]*):%s[ ]*\([0-9]*\)" % self.prog.title, fp.read())
166                        fp.close()
167                        if match is None:
168#                               log.warning('Grabber error', "TMDB.py failed to find matching movie for %s. Reverting to generic export." % self.prog.title)
169                                self.get_generic()
170                                return
171
172                        # gather remanining data
173                        self.viddata['inetref'] = match.group('inetref')
174                        self.process_meta("%s -D %s" % (self.moviegrabber, match.group('inetref')))
175                        for type in ('coverfile','screenshot','banner','fanart'):
176                                if type in self.viddata:
177                                        self.viddata[type] = self.viddata[type].split(',')[0]
178
179
180                # set some common data
181                self.country = ()
182                self.viddata['showlevel'] = 1
183                self.type = 'MOVIE'
184
185        def get_generic(self):
186                # pull available program data
187                self.viddata['title'] = self.prog.title
188                if self.prog.subtitle:
189                        self.viddata['subtitle'] = self.prog.subtitle
190                if self.prog.description:
191                        self.viddata['plot'] = self.prog.description
192                if self.prog.year:
193                        self.viddata['year'] = self.prog.year
194                self.viddata['length'] = str(int((self.prog.recendts-self.prog.recstartts).seconds/60))
195                self.viddata['inetref'] = '00000000'
196                self.viddata['director'] = self.mythdb.getCast(self.chanid, self.rtime, roles='director')
197                if len(self.viddata['director']) == 0:
198                        self.viddata['director'] = 'NULL'
199                else:
200                        self.viddata['director'] = self.viddata['director'][0]
201                self.cast = self.mythdb.getCast(self.chanid, self.rtime, roles=('actor','guest_star','host','commentator','guest'))
202                self.country = ()
203                self.viddata['showlevel'] = 1
204                self.viddata['rating'] = 'NR'
205                self.viddata['coverfile'] = 'No Cover'
206                self.type = 'GENERIC'
207               
208
209        def process_meta(self, command):
210                # process grabber command and pull available data
211                fp = os.popen(command)
212                res = fp.read()
213                fp.close()
214                dict = {}
215                for point in res.split('\n')[:-1]:
216                        if point.find(':') == -1:
217                                continue
218                        key,dat = point.split(':',1)
219                        dict[key] = dat
220                for (dbkey, datkey) in (('title','Title'),('subtitle','Subtitle'),('director','Director'),
221                                        ('plot','Plot'),('inetref','Seriesid'), ('year','Year'),
222                                        ('userrating','UserRating'),('length','Runtime'),('inetref','InetRef'),
223                                        ('screenshot','Screenshot'),('fanart','Fanart'),('banner','Banner'),
224                                        ('coverfile','Coverart'),('homepage','URL')):
225                        if datkey in dict.keys():
226                                self.viddata[dbkey] = dict[datkey].strip()
227                if 'Cast' in dict.keys():
228                        self.cast = tuple(dict['Cast'].split(', '))
229                if 'Genres' in dict.keys():
230                        self.genre = tuple(dict['Genres'].split(', '))
231                if 'coverfile' not in self.viddata:
232                        self.viddata['coverfile'] = 'No Cover'
233                if 'rating' not in self.viddata:
234                        self.viddata['rating'] = 'NR'
235
236
237        def get_dest(self):
238               
239                subpath = None
240                if self.type == 'TV':
241                        subpath = self.process_fmt(self.tfmt)
242                elif self.type == 'MOVIE':
243                        subpath = self.process_fmt(self.mfmt)
244                elif self.type == 'GENERIC':
245                        subpath = self.process_fmt(self.gfmt)
246                subpath += self.source[self.source.rfind('.'):]
247
248                res = self.mythdb.getStorageGroup('Videos',self.jobhost)
249                if len(res):
250                        self.dest = res[0]['dirname']+subpath
251                        self.destdb = subpath
252                else:
253                        self.dest = self.mythdb.getSetting("VideoStartupDir",self.jobhost)+'/'+subpath
254                        self.destdb = self.dest
255
256                tmppath = self.dest[0:self.dest.rfind('/')]
257                if not os.access(tmppath,os.F_OK):
258                        os.makedirs(tmppath)
259
260        def get_fmt(self):
261                self.tfmt = self.mythdb.getSetting('mythvideo.TVexportfmt')
262                if not self.tfmt:
263                        self.tfmt = 'Television/%TITLE%/Season %SEASON%/%TITLE% - S%SEASON%E%EPISODEPAD% - %SUBTITLE%'
264                self.mfmt = self.mythdb.getSetting('mythvideo.MOVIEexportfmt')
265                if not self.mfmt:
266                        self.mfmt = 'Movies/%TITLE%'
267                self.gfmt = self.mythdb.getSetting('mythvideo.GENERICexportfmt')
268                if not self.gfmt:
269                        self.gfmt = 'Videos/%TITLE%'
270
271        def process_fmt(self, fmt):
272                # replace fields from viddata
273                rep = ( ('%TITLE%','title','%s'),('%SUBTITLE%','subtitle','%s'),
274                        ('%SEASON%','season','%d'),('%SEASONPAD%','season','%02d'),
275                        ('%EPISODE%','episode','%d'),('%EPISODEPAD%','episode','%02d'),
276                        ('%YEAR%','year','%s'),('%DIRECTOR%','director','%s'))
277                for (tag, data, format) in rep:
278                        if data in self.viddata.keys():
279                                fmt = fmt.replace(tag,format % self.viddata[data])
280                        else:
281                                fmt = fmt.replace(tag,'')
282
283                # replace fields from program data
284                rep = ( ('%HOSTNAME','hostname','%s'),('%STORAGEGROUP%','storagegroup','%s'))
285                for (tag, data, format) in rep:
286                        data = eval('self.prog.%s' % data)
287                        fmt = fmt.replace(tag,format % data)
288
289#               fmt = fmt.replace('%CARDID%',self.prog.cardid)
290#               fmt = fmt.replace('%CARDNAME%',self.prog.cardid)
291#               fmt = fmt.replace('%SOURCEID%',self.prog.cardid)
292#               fmt = fmt.replace('%SOURCENAME%',self.prog.cardid)
293#               fmt = fmt.replace('%CHANNUM%',self.prog.channum)
294#               fmt = fmt.replace('%CHANNAME%',self.prog.cardid)
295
296                if len(self.genre):
297                        fmt = fmt.replace('%GENRE%',self.genre[0])
298                else:
299                        fmt = fmt.replace('%GENRE%','')
300#               if len(self.country):
301#                       fmt = fmt.replace('%COUNTRY%',self.country[0])
302#               else:
303#                       fmt = fmt.replace('%COUNTRY%','')
304                return fmt
305
306        def copy(self):
307                if self.opts.skip:
308                        return
309                if self.opts.sim:
310                        print 'Transferring from "%s:%s" to "%s"' % (self.srchost,self.source,self.dest)
311                        return
312                srcp = None
313                dtime = None
314
315#               log.notice('Starting transfer','from "%s:%s" to "%s"' % (self.srchost,self.source,self.dest))
316                stime = time.time()
317                ctime = time.time()
318                if os.access(self.source,os.F_OK):
319                        srcp = open(self.source,'r')
320                else:
321                        srcp = FileTransfer(self.prop)
322                destp = open(self.dest,'w')
323                srcsize = self.prog.filesize
324                destsize = [0,0,0,0,0,0,0,0,0,0]
325                if self.job:
326                        self.job.setStatus(4)
327                tsize = 2**18
328                while tsize == 2**18:
329                        if (srcsize - destp.tell()) < tsize:
330                                tsize = srcsize - destp.tell()
331                        if time.time() - ctime > 4:
332                                ctime = time.time()
333                                destsize.append(destp.tell())
334                                rem = srcsize - destp.tell()
335                                if destsize[0]:
336                                        dtime = 40
337                                else:
338                                        dtime = int(ctime - stime)
339                                rate = (destp.tell() - destsize.pop(0))/dtime
340                                remt = rem/rate
341                                if self.job:
342                                        self.job.setComment("%02d%% complete - %s seconds remaining" % (destsize[9]*100/srcsize, remt))
343                        destp.write(srcp.read(tsize))
344
345                srcp.close()
346                destp.close()
347#               log.notice('Transfer complete','%d seconds elapsed' % int(time.time()-stime))
348                if self.job:
349                        self.job.setComment("Complete - %d seconds elapsed" % (int(time.time()-stime)))
350                        self.job.setStatus(256)
351
352
353        def write_meta(self):
354                mythvid = MythVideo()
355                self.viddata['filename'] = self.destdb
356                self.viddata['host'] = self.jobhost
357                self.write_images()
358
359                intid = mythvid.getMetadataId(self.destdb)
360                if intid:
361                        if self.opts.sim:
362                                print "Existing entry found at [%d], updating with:" % intid
363                                print self.viddata
364                        else:
365                                mythvid.setMetadata(self.viddata,intid)
366                else:
367                        if self.opts.sim:
368                                print "Creating new entry using:"
369                                print self.viddata
370                        else:
371                                intid = mythvid.setMetadata(self.viddata)
372                for name in self.cast:
373                        if self.opts.sim:
374                                print "Adding cast member: " +name
375                        else:
376                                mythvid.setCast(name, intid)
377                if self.job:
378                        self.job.setStatus(272)
379
380        def write_images(self):
381                path = None
382                dbpath = None
383                hash = {'coverfile':('Coverart','VideoArtworkDir','coverart'),
384                        'banner':('Banners','mythvideo.bannerDir','banner'),
385                        'fanart':('Fanart','mythvideo.fanartDir','fanart'),
386                        'screenshot':('Screenshots','mythvideo.screenshotDir','screenshot')}
387                for mode in hash.keys():
388                        if mode not in self.viddata:
389                                if self.opts.sim:
390                                        print "No %s found" % mode
391                                continue
392                        url = self.viddata[mode]
393                        if url == 'No Cover' or url == '':
394                                if self.opts.sim:
395                                        print "No %s found" % mode
396                                continue
397                        if self.type == 'TV':
398                                if mode == 'screenshot':
399                                        file = '%s Season %dx%d_%s.%s' % (self.viddata['title'],
400                                                                        self.viddata['season'],
401                                                                        self.viddata['episode'],
402                                                                        hash[mode][2],
403                                                                        url[url.rfind('.')+1:])
404                                else:
405                                        file = '%s Season %d_%s.%s' % (self.viddata['title'],
406                                                                        self.viddata['season'],
407                                                                        hash[mode][2],
408                                                                        url[url.rfind('.')+1:])
409                        else:
410                                file = '%s_%s.%s' % (self.viddata['title'],
411                                                        hash[mode][2],
412                                                        url[url.rfind('.')+1:])
413
414                        res = self.mythdb.getStorageGroup(hash[mode][0],self.jobhost)
415                        if len(res):
416                                path = res[0]['dirname']+file
417                                self.viddata[mode] = file
418                        else:
419                                path = self.mythdb.getSetting(hash[mode][1],self.jobhost)+'/'+file
420                                self.viddata[mode] = path
421
422                        if self.opts.sim:
423                                print "Found %s at %s" % (mode,self.image[mode])
424                                return
425
426                        if not os.access(path,os.F_OK):
427                                urlretrieve(url,path)
428                        if (mode == 'coverfile') & (self.type == 'TV'):
429                                vdir = self.dest[:self.dest.rfind('/')]
430                                if not os.access(vdir+'folder.jpg',os.F_OK):
431                                        urlretrieve(url,vdir+'/folder.jpg')
432
433        def seekdata(self):
434                if self.opts.sim:
435                        print "Running \"mythcommflag --rebuild --video '%s'\"" % self.dest
436                        return
437                fp = os.popen("mythcommflag --rebuild --video '%s'" % self.dest)
438                fp.close()
439
440def usage():
441        print("mythvidexport.py [options] [--chanid=<chanid> --starttime=<starttime> | <jobid>]")
442        print("        This script can be run by specifing the channel and start time directly")
443        print("            or by specifing the ID of a job in jobqueue")
444        print("")
445        print("        Run from the command line through the former:")
446        print("            mythvidexport.py --chanid=1002 --starttime=200907010000")
447        print("        Or from a user script through the latter:")
448        print("            mythvidexport.py %JOBID%")
449        print("")
450        print("    Options:")
451        print("        -h/--help and -f/--helpformat:")
452        print("             return this help, or a listing of available formatting strings")
453        print("        --fformat='<string>' and --dformat='<string>':")
454        print("             override the stored formatting string in the database")
455        print("             if no recording is specified, store format string to the database")
456
457def usage_format():
458        print("The default strings are:")
459        print("    Television: 'Television/%TITLE%/Season %SEASON%/%TITLE% - S%SEASON%E%EPISODEPAD% - %SUBTITLE%'")
460        print("    Movie:      'Movies/%TITLE%'")
461        print("    Generic:    'Videos/%TITLE%'")
462        print("")
463        print("Available strings:")
464        print("    %TITLE%:         series title")
465        print("    %SUBTITLE%:      episode title")
466        print("    %SEASON%:        season number")
467        print("    %SEASONPAD%:     season number, padded to 2 digits")
468        print("    %EPISODE%:       episode number")
469        print("    %EPISODEPAD%:    episode number, padded to 2 digits")
470        print("    %YEAR%:          year")
471        print("    %DIRECTOR%:      director")
472#       print("    %CARDID%:        ID of tuner card used to record show")
473#       print("    %CARDNAME%:      name of tuner card used to record show")
474#       print("    %SOURCEID%:      ID of video source used to record show")
475#       print("    %SOURCENAME%:    name of video source used to record show")
476        print("    %HOSTNAME%:      backend used to record show")
477        print("    %STORAGEGROUP%:  storage group containing recorded show")
478#       print("    %CHANNUM%:       ID of channel used to record show")
479#       print("    %CHANNAME%:      name of channel used to record show")
480        print("    %GENRE%:         first genre listed for recording")
481#       print("    %COUNTRY%:       first country listed for recording")
482
483def print_format():
484        db = MythDB()
485        tfmt = db.getSetting('mythvideo.TVexportfmt')
486        if not tfmt:
487                tfmt = 'Television/%TITLE%/%TITLE% - S%SEASON%E%EPISODEPAD% - %SUBTITLE%'
488        mfmt = db.getSetting('mythvideo.MOVIEexportfmt')
489        if not mfmt:
490                mfmt = 'Movies/%TITLE%'
491        gfmt = db.getSetting('mythvideo.GENERICexportfmt')
492        if not gfmt:
493                gfmt = 'Videos/%TITLE%'
494        print "Current output formats:"
495        print "    TV:      "+tfmt
496        print "    Movies:  "+mfmt
497        print "    Generic: "+gfmt
498
499def main():
500        parser = OptionParser(usage="usage: %prog [options] [jobid]")
501
502        parser.add_option("-f", "--helpformat", action="store_true", default=False, dest="fmthelp",
503                        help="Print explination of file format string.")
504        parser.add_option("-p", "--printformat", action="store_true", default=False, dest="fmtprint",
505                        help="Print current file format string.")
506        parser.add_option("--tformat", action="store", type="string", dest="tformat",
507                        help="Use TV format for current task. If no task, store in database.")
508        parser.add_option("--mformat", action="store", type="string", dest="mformat",
509                        help="Use Movie format for current task. If no task, store in database.")
510        parser.add_option("--gformat", action="store", type="string", dest="gformat",
511                        help="Use Generic format for current task. If no task, store in database.")
512        parser.add_option("--chanid", action="store", type="int", dest="chanid",
513                        help="Use chanid for manual operation")
514        parser.add_option("--starttime", action="store", type="int", dest="starttime",
515                        help="Use starttime for manual operation")
516        parser.add_option("--listingonly", action="store_true", default=False, dest="listingonly",
517                        help="Use data from listing provider, rather than grabber")
518        parser.add_option("-s", "--simulation", action="store_true", default=False, dest="sim",
519                        help="Simulation (dry run), no files are copied or new entries made")
520        parser.add_option("--skip", action="store_true", default=False, dest="skip") # debugging use only
521
522        opts, args = parser.parse_args()
523
524        if opts.fmthelp:
525                usage_format()
526                sys.exit(0)
527
528        if opts.fmtprint:
529                usage_current()
530                sys.exit(0)
531
532        if opts.chanid and opts.starttime:
533                export = VIDEO(opts,0)
534        elif len(args) == 1:
535                export = VIDEO(opts,int(args[0]))
536        else:
537                if opts.tformat or opts.mformat or opts.gformat:
538                        db = MythDB()
539                        if opts.tformat:
540                                print "Changing TV format to: "+opts.tformat
541                                db.setSetting('mythvideo.TVexportfmt',opts.tformat)
542                        if opts.mformat:
543                                print "Changing Movie format to: "+opts.mformat
544                                db.setSetting('mythvideo.MOVIEexportfmt',opts.mformat)
545                        if opts.gformat:
546                                print "Changing Generic format to: "+opts.gformat
547                                db.setSetting('mythvideo.GENERICexportfmt',opts.gformat)
548                        sys.exit(0)
549                else:
550                        parser.print_help()
551                        sys.exit(2)
552
553if __name__ == "__main__":
554        main()