Ticket #6680: mythvidexport.10.py

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