Ticket #6680: mythvidexport.11.py

File mythvidexport.11.py, 18.0 KB (added by Raymond Wagner <raymond@…>, 16 years ago)

This is identical to the previous, except it functions without #6885.

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        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                c = self.mythdb.cursor()
259                res = c.execute("""SELECT dirname FROM storagegroup WHERE groupname='Videos' and hostname='%s' LIMIT 1""" % self.jobhost)
260                res = self.mythdb.getStorageGroup('Videos',self.jobhost)
261                if res:
262                        self.dest = c.fetchone()[0]+subpath
263                        self.destdb = subpath
264                else:
265                        self.dest = self.mythdb.getSetting("VideoStartupDir",self.jobhost)+'/'+subpath
266                        self.destdb = self.dest
267
268                tmppath = self.dest[0:self.dest.rfind('/')]
269                if not os.access(tmppath,os.F_OK):
270                        os.makedirs(tmppath)
271
272        def get_fmt(self):
273                self.tfmt = self.mythdb.getSetting('mythvideo.TVexportfmt')
274                if not self.tfmt:
275                        self.tfmt = 'Television/%TITLE%/Season %SEASON%/%TITLE% - S%SEASON%E%EPISODEPAD% - %SUBTITLE%'
276                self.mfmt = self.mythdb.getSetting('mythvideo.MOVIEexportfmt')
277                if not self.mfmt:
278                        self.mfmt = 'Movies/%TITLE%'
279                self.gfmt = self.mythdb.getSetting('mythvideo.GENERICexportfmt')
280                if not self.gfmt:
281                        self.gfmt = 'Videos/%TITLE%'
282
283        def process_fmt(self, fmt):
284                # replace fields from viddata
285                rep = ( ('%TITLE%','title','%s'),('%SUBTITLE%','subtitle','%s'),
286                        ('%SEASON%','season','%d'),('%SEASONPAD%','season','%02d'),
287                        ('%EPISODE%','episode','%d'),('%EPISODEPAD%','episode','%02d'),
288                        ('%YEAR%','year','%s'),('%DIRECTOR%','director','%s'))
289                for (tag, data, format) in rep:
290                        if data in self.viddata.keys():
291                                fmt = fmt.replace(tag,format % self.viddata[data])
292                        else:
293                                fmt = fmt.replace(tag,'')
294
295                # replace fields from program data
296                rep = ( ('%HOSTNAME','hostname','%s'),('%STORAGEGROUP%','storagegroup','%s'))
297                for (tag, data, format) in rep:
298                        data = eval('self.prog.%s' % data)
299                        fmt = fmt.replace(tag,format % data)
300
301#               fmt = fmt.replace('%CARDID%',self.prog.cardid)
302#               fmt = fmt.replace('%CARDNAME%',self.prog.cardid)
303#               fmt = fmt.replace('%SOURCEID%',self.prog.cardid)
304#               fmt = fmt.replace('%SOURCENAME%',self.prog.cardid)
305#               fmt = fmt.replace('%CHANNUM%',self.prog.channum)
306#               fmt = fmt.replace('%CHANNAME%',self.prog.cardid)
307
308                if len(self.genre):
309                        fmt = fmt.replace('%GENRE%',self.genre[0])
310                else:
311                        fmt = fmt.replace('%GENRE%','')
312#               if len(self.country):
313#                       fmt = fmt.replace('%COUNTRY%',self.country[0])
314#               else:
315#                       fmt = fmt.replace('%COUNTRY%','')
316                return fmt
317
318        def copy(self):
319                if self.opts.skip:
320                        return
321                srcp = None
322                dtime = None
323
324                #log.notice('Starting transfer','from "%s:%s" to "%s"' % (self.srchost,self.source,self.dest))
325                stime = time.time()
326                ctime = time.time()
327                if os.access(self.source,os.F_OK):
328                        srcp = open(self.source,'r')
329                else:
330                        srcp = FileTransfer(self.prop)
331                destp = open(self.dest,'w')
332                srcsize = self.prog.filesize
333                destsize = [0,0,0,0,0,0,0,0,0,0]
334                if self.job:
335                        self.job.setStatus(4)
336                tsize = 2**18
337                while tsize == 2**18:
338                        if (srcsize - destp.tell()) < tsize:
339                                tsize = srcsize - destp.tell()
340                        if time.time() - ctime > 4:
341                                ctime = time.time()
342                                destsize.append(destp.tell())
343                                rem = srcsize - destp.tell()
344                                if destsize[0]:
345                                        dtime = 40
346                                else:
347                                        dtime = int(ctime - stime)
348                                rate = (destp.tell() - destsize.pop(0))/dtime
349                                remt = rem/rate
350                                if self.job:
351                                        self.job.setComment("%02d%% complete - %s seconds remaining" % (destsize[9]*100/srcsize, remt))
352                        destp.write(srcp.read(tsize))
353
354                srcp.close()
355                destp.close()
356                #log.notice('Transfer complete','%d seconds elapsed' % int(time.time()-stime))
357                if self.job:
358                        self.job.setComment("Complete - %d seconds elapsed" % (int(time.time()-stime)))
359                        self.job.setStatus(256)
360
361
362        def write_meta(self):
363                mythvid = MythVideo()
364                mythtv = MythTV()
365                self.viddata['filename'] = self.destdb
366                self.viddata['host'] = self.jobhost
367
368
369                intid = mythvid.getMetadataId(self.destdb)
370                if intid:
371                        mythvid.setMetadata(self.viddata,intid)
372                else:
373                        intid = mythvid.setMetadata(self.viddata)
374                for name in self.cast:
375                        mythvid.setCast(name, intid)
376                self.write_image('coverfile',intid)
377                self.write_image('banner',intid)
378                self.write_image('fanart',intid)
379                if self.job:
380                        self.job.setStatus(272)
381
382        def write_image(self,mode,intid):
383                if not self.image[mode]:
384                        return
385                path = None
386                dbpath = None
387                SG = {'coverfile':'Coverart','banner':'Banners','fanart':'Fanart'}
388                DB = {'coverfile':'VideoArtworkDir','banner':'mythvideo.bannerDir','fanart':'mythvideo.fanartDir'}
389                mythvid = MythVideo()
390
391                file = self.viddata['inetref']+'_'+self.image[mode][self.image[mode].rfind('/')+1:]
392
393                c = self.mythdb.cursor()
394                res = c.execute("""SELECT dirname FROM storagegroup WHERE groupname='%s' and hostname='%s' LIMIT 1""" % (SG[mode],self.jobhost))
395                if res:
396                        path = c.fetchone()[0]+file
397                        dbpath = file
398                else:
399                        path = self.mythdb.getSetting(DB[mode],self.jobhost)+'/'+file
400                        dbpath = path
401
402                if not os.access(path,os.F_OK):
403                        urlretrieve(self.image[mode],path)
404                if (mode == 'coverfile') & (self.type == 'TV'):
405                        vdir = self.dest[:self.dest.rfind('/')]
406                        if not os.access(vdir+'folder.jpg',os.F_OK):
407                                urlretrieve(self.image[mode],vdir+'/folder.jpg')
408
409                mythvid.setMetadata({mode:dbpath}, intid)
410
411
412def usage():
413        print("mythvidexport.py [options] [--chanid=<chanid> --starttime=<starttime> | <jobid>]")
414        print("        This script can be run by specifing the channel and start time directly")
415        print("            or by specifing the ID of a job in jobqueue")
416        print("")
417        print("        Run from the command line through the former:")
418        print("            mythvidexport.py --chanid=1002 --starttime=200907010000")
419        print("        Or from a user script through the latter:")
420        print("            mythvidexport.py %JOBID%")
421        print("")
422        print("    Options:")
423        print("        -h/--help and -f/--helpformat:")
424        print("             return this help, or a listing of available formatting strings")
425        print("        --fformat='<string>' and --dformat='<string>':")
426        print("             override the stored formatting string in the database")
427        print("             if no recording is specified, store format string to the database")
428
429def usage_format():
430        print("The default strings are:")
431        print("    Television: 'Television/%TITLE%/Season %SEASON%/%TITLE% - S%SEASON%E%EPISODEPAD% - %SUBTITLE%'")
432        print("    Movie:      'Movies/%TITLE%'")
433        print("    Generic:    'Videos/%TITLE%'")
434        print("")
435        print("Available strings:")
436        print("    %TITLE%:         series title")
437        print("    %SUBTITLE%:      episode title")
438        print("    %SEASON%:        season number")
439        print("    %SEASONPAD%:     season number, padded to 2 digits")
440        print("    %EPISODE%:       episode number")
441        print("    %EPISODEPAD%:    episode number, padded to 2 digits")
442        print("    %YEAR%:          year")
443        print("    %DIRECTOR%:      director")
444#       print("    %CARDID%:        ID of tuner card used to record show")
445#       print("    %CARDNAME%:      name of tuner card used to record show")
446#       print("    %SOURCEID%:      ID of video source used to record show")
447#       print("    %SOURCENAME%:    name of video source used to record show")
448        print("    %HOSTNAME%:      backend used to record show")
449        print("    %STORAGEGROUP%:  storage group containing recorded show")
450#       print("    %CHANNUM%:       ID of channel used to record show")
451#       print("    %CHANNAME%:      name of channel used to record show")
452        print("    %GENRE%:         first genre listed for recording")
453#       print("    %COUNTRY%:       first country listed for recording")
454
455def print_format():
456        db = MythDB()
457        tfmt = db.getSetting('mythvideo.TVexportfmt')
458        if not tfmt:
459                tfmt = 'Television/%TITLE%/%TITLE% - S%SEASON%E%EPISODEPAD% - %SUBTITLE%'
460        mfmt = db.getSetting('mythvideo.MOVIEexportfmt')
461        if not mfmt:
462                mfmt = 'Movies/%TITLE%'
463        gfmt = db.getSetting('mythvideo.GENERICexportfmt')
464        if not gfmt:
465                gfmt = 'Videos/%TITLE%'
466        print "Current output formats:"
467        print "    TV:      "+tfmt
468        print "    Movies:  "+mfmt
469        print "    Generic: "+gfmt
470
471def main():
472        parser = OptionParser(usage="usage: %prog [options] [jobid]")
473
474        parser.add_option("-f", "--helpformat", action="store_true", default=False, dest="fmthelp",
475                        help="Print explination of file format string.")
476        parser.add_option("-p", "--printformat", action="store_true", default=False, dest="fmtprint",
477                        help="Print current file format string.")
478        parser.add_option("--tformat", action="store", type="string", dest="tformat",
479                        help="Use TV format for current task. If no task, store in database.")
480        parser.add_option("--mformat", action="store", type="string", dest="mformat",
481                        help="Use Movie format for current task. If no task, store in database.")
482        parser.add_option("--gformat", action="store", type="string", dest="gformat",
483                        help="Use Generic format for current task. If no task, store in database.")
484        parser.add_option("--chanid", action="store", type="int", dest="chanid",
485                        help="Use chanid for manual operation")
486        parser.add_option("--starttime", action="store", type="int", dest="starttime",
487                        help="Use starttime for manual operation")
488        parser.add_option("--listingonly", action="store_true", default=False, dest="listingonly",
489                        help="Use data from listing provider, rather than grabber")
490        parser.add_option("--skip", action="store_true", default=False, dest="skip") # debugging use only
491
492        opts, args = parser.parse_args()
493
494        if opts.fmthelp:
495                usage_format()
496                sys.exit(0)
497
498        if opts.fmtprint:
499                usage_current()
500                sys.exit(0)
501
502        if opts.chanid and opts.starttime:
503                export = VIDEO(opts,0)
504        elif len(args) == 1:
505                export = VIDEO(opts,int(args[0]))
506        else:
507                if opts.tformat or opts.mformat or opts.gformat:
508                        db = MythDB()
509                        if opts.tformat:
510                                print "Changing TV format to: "+opts.tformat
511                                db.setSetting('mythvideo.TVexportfmt',opts.tformat)
512                        if opts.mformat:
513                                print "Changing Movie format to: "+opts.mformat
514                                db.setSetting('mythvideo.MOVIEexportfmt',opts.mformat)
515                        if opts.gformat:
516                                print "Changing Generic format to: "+opts.gformat
517                                db.setSetting('mythvideo.GENERICexportfmt',opts.gformat)
518                        sys.exit(0)
519                else:
520                        parser.print_help()
521                        sys.exit(2)
522
523if __name__ == "__main__":
524        main()