#!/usr/bin/python # # Version history # # 4/2/2010 # 1. Fixed decoding issues with UTF-8 strings coming from myth DB. # Fix assumes the latin_1 character set is sufficient. # 4/3/2010 # * Fixed bug where recorded movies were showing up in "Home Movies". # * They now all show up under the name specified by "movie_subdir" # * You should delete the folder "@Movies" if it exists # * Simplified creation of debug messages # * Prints # of new series and episodes added when done processing. # * Option to wake up XBMC machine and tell it to refresh library # if anything has been added. # 4/4/2010 # * Fixed bug where ".nfo" files for episodes were incorrectly marked # as used UTF-8. This only created problems for recordings whose # filenames contained non-ASCII characters. # 4/9/2010 # * Added creation of commercial cut lists via .txt files. # 4/15/2010 # * Added command line parser with many options. No need to edit the # file anymore. Use "--help" for details. # import sys import os import re import MySQLdb import time import cgi import xml.dom.minidom import struct, socket from optparse import OptionParser # # Author's privlidges -- different defaults # myhost=socket.gethostname() parser = OptionParser(usage="xbmc_mythlnk.py [options]") # # Parse command line options # parser.add_option("--outdir", dest="xbmc_subdir", default="XBMC", help="Name of subdirectory for XBMC-friendly links. Default is XBMC", metavar="SUBDIR") parser.add_option("--moviedir", dest="movie_subdir", default="", help="Name of subdirectory where all movies should be placed. Default is a separate subdirectory per movie.", metavar="SUBDIR") parser.add_option("--mysql", dest="mysql", default=("localhost","mythconverg","mythtv","mythtv"), nargs=4, help="Obtain mythtv data from mysql database DBNAME on HOST. Defaults are HOST=localhost, DBNAME=mythconverg, USERNAME=mythtv, PASSWORD=mythtv", metavar="HOST DBNAME USERNAME PASSWORD") parser.add_option("--update", dest="xbmc_machine", default=("","0"), nargs=2, help="Sends request to HOST to update its XBMC library if any new episodes have been added. Optionally send a wake-on-LAN packet first if MAC address is provided. If you don't want a WOL packet sent, specify a MAC address of 0. Default = no library update is done.", metavar="HOST MAC") parser.add_option("-d", "--debug", action="store_true", dest="debug", default=False, help="Turns on debug mode") parser.add_option("--noxqt", action="store_true", dest="noxqt", default=False, help="Don't create any directoreis or links -- just print out debug info.") parser.add_option("--marc", action="store_true", dest="marc", default=False, help="For use only by the original author. Sets defaults useful for my system.") # # Parse command line options. # (options, args) = parser.parse_args() # # Set defaults for author, if requested. # if options.marc: options.xbmc_machine=("192.168.0.12","90:FB:A6:2A:3C:8E") options.movie_subdir="000-Movies" bin_dir=os.path.dirname(sys.argv[0]) myname=os.path.basename(sys.argv[0]) if myname == "xbmc_mythlink.py.new": options.xbmc_subdir=options.xbmc_subdir+"_test" print "The name of this program has been changed to ",myname print "This name is used for testing and the output" print "will be redirected to ",options.xbmc_subdir # print "Also, no wakeup or lib update will occur." # # Misc functions # def debug_print(*message): if options.debug: line="" for msg in message: line=line+msg+" " print line return # # Wake on lan function # def WakeOnLan(macaddress): # Check macaddress format and try to compensate. if len(macaddress) == 12: pass elif len(macaddress) == 12 + 5: sep = macaddress[2] macaddress = macaddress.replace(sep, '') else: raise ValueError('Incorrect MAC address format') # Pad the synchronization stream. data = ''.join(['FFFFFFFFFFFF', macaddress * 20]) send_data = '' # Split up the hex values and pack. for i in range(0, len(data), 2): send_data = ''.join([send_data, struct.pack('B', int(data[i: i + 2], 16))]) # Broadcast it to the LAN. sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) sock.sendto(send_data, ('', 7)) sock.close() # # Function to generate nfo file # def gen_nfo (in_fullxbmcpath,in_title, in_subtitle, in_descr, in_airdate, in_starttime, in_epinum): nfo_dir=os.path.split(in_fullxbmcpath)[0] nfo_file=os.path.splitext(in_fullxbmcpath)[0] + ".nfo" nfo_series_file=nfo_dir+"/tvshow.nfo" tvdb_file=nfo_dir+"/.tvdb_knows_me.txt" # # Do not allow blank description or subtitles # if in_subtitle == "" or in_subtitle == in_starttime: subtitle="Aired " + in_starttime else: subtitle=in_subtitle if in_descr == "": descr="Aired " + in_starttime else: descr=in_descr # # Make strings XML safe # title=cgi.escape(in_title) subtitle=cgi.escape(subtitle) descr=cgi.escape(descr) pretty_airdate=in_airdate[0:4] + "-" + in_airdate[4:6] + "-" + in_airdate[6:8] # # Create NFO file for this episode. # nfo = open(nfo_file, 'w') # print >>nfo,'' print >>nfo, '' print >>nfo," %s" % (subtitle) print >>nfo," 8.500000" print >>nfo," 0" print >>nfo," 0" print >>nfo," 0" print >>nfo," %s" % (in_epinum) print >>nfo," 0" print >>nfo," %s" % (in_epinum) print >>nfo," " print >>nfo," " print >>nfo," %s" % (descr) print >>nfo," " print >>nfo," " print >>nfo," %s" % (pretty_airdate) print >>nfo," %s" % (pretty_airdate) print >>nfo,"" nfo.close # # Create nfo file for series of tvdb doesn't now about it. This is required because # if XBMC is unable to successfully look up the series, XBMC won't show this series in # the TV library. # if os.path.exists(tvdb_file) == False and os.path.exists(nfo_series_file) == False: if in_title == options.movie_subdir and options.movie_subdir != "": tvdb_stat=1 series_title=cgi.escape(options.movie_subdir) series_plot=cgi.escape("Movies recorded by MYTHTV") else: cmd=bin_dir+"/tvdb_get.sh \""+title+"\" \""+tvdb_file+"\" >/dev/null 2>&1 " tvdb_stat=os.system(cmd) series_title=title series_plot=cgi.escape("No synopsis is available.") if tvdb_stat != 0: nfo_series=open(nfo_series_file,'w') print >>nfo_series,"" print >>nfo_series," %s" % (series_title) print >>nfo_series," 8.500000" print >>nfo_series," 0" print >>nfo_series," 0" print >>nfo_series," -1" print >>nfo_series," 3" print >>nfo_series," -1" print >>nfo_series," -1" print >>nfo_series," " print >>nfo_series," " print >>nfo_series," %s" % (series_plot) print >>nfo_series," " print >>nfo_series," " print >>nfo_series," TV-14" print >>nfo_series," 0" print >>nfo_series," " print >>nfo_series," " print >>nfo_series," " print >>nfo_series," " print >>nfo_series," " print >>nfo_series," " print >>nfo_series," " print >>nfo_series," " print >>nfo_series," 1956-12-15" print >>nfo_series," " print >>nfo_series," " print >>nfo_series," " print >>nfo_series," Unknown" print >>nfo_series," " print >>nfo_series,"" nfo_series.close return # # Function to generate the commercial skip file. Per the XBMC wiki, # The name of the file must be the same as the video but with a # file extension of .txt. # def gen_commskip(db_conn, chanid, starttime, outfile): cursor2 = db_conn.cursor() cursor2.execute('SELECT mark, type from recordedmarkup where chanid='+chanid+' and starttime="'+starttime+'" order by mark') startmark=-1 stopmark=-1 # # Loop through markup table # firstline=1 while ( 1 ): row = cursor2.fetchone() if row == None: break mark=row[0] marktype=row[1] if marktype == 4: startmark=mark stopmark=-1 elif marktype == 5: stopmark=mark if startmark >= 0 and stopmark >= 0 and stopmark > startmark: if firstline==1: firstline=0 debug_print("Creating file ",outfile) outfile=open(outfile,'w') print >>outfile,"FILE PROCESSING COMPLETE" print >>outfile,"------------------------" print >>outfile,"%d\t%d" % (startmark, stopmark) startmark=-1 stopmark=-1 cursor2.close() if firstline==0: outfile.close() return 0 # # timestamp def # def timestamp(): lt = time.localtime(time.time()) return "%02d.%02d.%04d %02d:%02d:%02d " % (lt[2], lt[1], lt[0], lt[3], lt[4], lt[5]) # # # Start main program # # if options.debug: print options p = re.compile('\d{4}_\d{14}\.[mpg]{3}$') safe = re.compile("[/*\\\"\'\:\(\)\<\>\?]") # # Connect to mythtv database and extract recording data # try: conn = MySQLdb.connect (host = options.mysql[0], db = options.mysql[1], user = options.mysql[2], passwd = options.mysql[3]) except MySQLdb.Error, e: print "%sError %d: %s" % (timestamp(), e.args[0], e.args[1]) sys.exit (1) cursor = conn.cursor() cursor.execute("SELECT t1.chanid, t1.starttime, t1.title, t1.subtitle, t1.programid, t1.basename, t1.originalairdate, t1.deletepending, t1.description, t2.dirname FROM recorded t1,storagegroup t2 where t1.hostname=t2.hostname and t1.storagegroup=t2.groupname order by starttime") # # Loop through recordings # num_series_added=0 num_epi_added=0 while ( 1 ): row = cursor.fetchone() if row == None: break basename = str(row[5]) mythdir = os.path.normpath(str(row[9])) if os.path.lexists(mythdir+"/"+basename) == False: print "Skipping non-existant file ",mythdir,basename continue xbmcdir=mythdir+"/"+options.xbmc_subdir debug_print("mythdir,basename,xbmcdir is ",mythdir,basename,xbmcdir) # # Get fields into variables # chanid = str(row[0]) starttime = str(row[1]) title = row[2] # title = unicode(row[2],'utf-8','ignore').encode('latin_1','replace') subtitle = row[3] # subtitle = unicode(row[3],'utf-8','ignore').encode('latin_1','replace') programid = row[4] if row[6] == None: originalairdate = "0000-00-00" else: originalairdate = str(row[6]) deletepending = row[7] descr = row[8] # descr = unicode(row[8],'utf-8','ignore').encode('latin_1','replace') # # Place all moves under movie_subdir # programid_type=str(programid[0:2]) if programid_type == "MV" and options.movie_subdir != "": subtitle = title title = options.movie_subdir debug_print(" chanid, programid_type,title is ",chanid,programid_type,title) # # Massage and process data # if deletepending != 0: break intoriginalyear = int(originalairdate[0:4]) episode_year = str(starttime[0:4]) season_int = 00 epinum = str(programid[10:14]) airdate = starttime[0:4] airdate += starttime[5:7] airdate += starttime[8:10] if epinum == "0000" or epinum == "" or epinum == " ": epinum=starttime[2:4] + starttime[5:7] + starttime[8:10] + starttime[14:16] if subtitle == "": subtitle = starttime debug_print(" subtitle is ",subtitle) debug_print(" descr is ",descr) # # Create safe filenames from series title and episode title before generating file names # safe_title = safe.sub("_", unicode(title,'utf-8','ignore').encode('latin_1','replace')) safe_subtitle = safe.sub("_", unicode(subtitle,'utf-8','ignore').encode('latin_1','replace')) safe_starttime = safe.sub("_",starttime) filename = '%s.S%02dE%s.%s-%s-%s' % (safe_title, season_int, epinum, safe_subtitle, chanid,safe_starttime) fullxbmcdir = xbmcdir + "/" + safe_title fullxbmcpath_ts = fullxbmcdir + "/" + filename + ".ts" fullxbmcpath_tbn = fullxbmcdir + "/" + filename + ".tbn" fullxbmcpath_txt = fullxbmcdir + "/" + filename + ".txt" fullmythpath_rel = "../../" + basename fullmythpath_abs = mythdir + "/" + basename debug_print(" os.makedirs("+fullxbmcdir+")") debug_print(" os.symlink("+fullmythpath_rel+","+ fullxbmcpath_ts+")") debug_print(" gen_nfo ("+fullxbmcpath_ts+")") debug_print(" os.symlink("+fullmythpath_rel+".png,"+fullxbmcpath_tbn+")") debug_print("") if options.noxqt: continue # if os.path.exists(fullxbmcdir) != True: num_series_added+=1 os.makedirs(fullxbmcdir) if os.path.exists(fullxbmcpath_ts) != True: num_epi_added+=1 os.symlink(fullmythpath_rel, fullxbmcpath_ts) gen_nfo (fullxbmcpath_ts, title, subtitle, descr, airdate, starttime, epinum) if os.path.exists(fullxbmcpath_tbn) != True and os.path.exists(fullmythpath_abs+".png") == True: os.symlink(fullmythpath_rel+".png", fullxbmcpath_tbn) if os.path.exists(fullxbmcpath_txt) != True: gen_commskip(conn, chanid,starttime,fullxbmcpath_txt) # print "%s: Added %d series and %d episodes." % (myname,num_series_added, num_epi_added) cleanupcmd = "find " + xbmcdir + " -type l \! -xtype f -name \*.ts -exec " + bin_dir + "/xbmc_cleanup.sh \{\} \;" cleanupcmd2 = "find " + xbmcdir + " -depth -type d -empty -exec rmdir -v \{\} \;" if (options.xbmc_machine[0] != "") and (num_epi_added > 0 or options.debug): if options.xbmc_machine[1] != "": debug_print("Doing WOL for MAC address ",options.xbmc_machine[1]) WakeOnLan(options.xbmc_machine[1]) time.sleep(1) debug_print("Sending library update request to host ",options.xbmc_machine[0]) status=os.system('curl -s m1 "http://'+options.xbmc_machine[0]+'/xbmcCmds/xbmcHttp?command=ExecBuiltIn(UpdateLibrary(video))" > /dev/null 2>&1') print "%s: Status %d updating XBMC library on %s" % (myname,status,options.xbmc_machine[0]) cursor.close() conn.close() debug_print( "cleanupcmd=",cleanupcmd) debug_print( "cleanupcmd2=",cleanupcmd2) if options.noxqt == False: os.system(cleanupcmd) os.system(cleanupcmd2) sys.exit(1)