Ticket #6680: mythvidexport.12.py

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