def GetNewCoronationStreetFilename(self, baseFile, _queuedFile): fileExtension = baseFile[baseFile.rfind('.') + 1:] baseFile = baseFile[0:baseFile.rfind('.')] if "Coronation Street" in baseFile: baseParts = baseFile.split(' - ') dtFormat = "%Y-%m-%d %H %M %S" locCreateDt = self.__ltz.localize( datetime.datetime.strptime(baseParts[1], dtFormat)).astimezone(self.__london) metaInfo = self.GetCorrieIndex(locCreateDt) baseParts[1] = locCreateDt.strftime(dtFormat) if "19 30" in baseParts[1]: baseParts[2] += " - pt1" if "20 30" in baseParts[1]: baseParts[2] += " - pt2" if metaInfo is not None: epInfo = 's' + str(metaInfo[2]) + 'e' + str(metaInfo[3]) baseParts.insert(1, epInfo) baseFile = ' - '.join(baseParts) if Settings.GetConfig("Applications", "handbrake", "false").lower in [ 'true', '1', 't', 'y', 'yes', 'yeah', 'yup', 'certainly', 'uh-huh', 'on' ]: return baseFile + ".mp4" else: return baseFile + ".done." + fileExtension
def ProcessQueuedFile(self, i, queuedFile): print(' Processing ' + str(i) + queuedFile.GetFilename() + ' in state ' + queuedFile.GetState().name) if queuedFile.GetState() == PlexPostProcessState.INITIAL: if queuedFile.GetFiletype() == 'm4v': if Settings.GetConfig("Applications", "handbrake", "false").lower in ['true', '1', 't', 'y', 'yes', 'yeah', 'yup', 'certainly', 'uh-huh', 'on' ]: queuedFile.SetState(PlexPostProcessState.COMMSKIP) else: queuedFile.SetState(PlexPostProcessState.TRANSCODING) # Comskip no longer needed self.GetDatabaseInteraction().UpdateQFState(queuedFile, "Startup", "Started commskip") else: queuedFile.SetState(PlexPostProcessState.TRANSCODING) self.GetDatabaseInteraction().UpdateQFState(queuedFile, "Comskip", "Started processing") elif queuedFile.GetState() == PlexPostProcessState.COMMSKIP: Commskip(self).Commskip(i, queuedFile) elif queuedFile.GetState() == PlexPostProcessState.TRANSCODING: Transcode(self).Transcode(i, queuedFile) elif queuedFile.GetState() == PlexPostProcessState.ADD_META: AddMeta(self).AddMeta(i, queuedFile) elif queuedFile.GetState() == PlexPostProcessState.MOVING_FILES: self.MoveFiles(i, queuedFile) elif queuedFile.GetState() == PlexPostProcessState.DELETING_ORIGINAL_FILE: self.DeleteOriginalFile(i, queuedFile) elif queuedFile.GetState() == PlexPostProcessState.PENDING_DELETE_DUPLICATE: self.DeleteDuplicateFile(i, queuedFile) else: raise Exception("Damn, invalid state " + queuedFile.GetState().name)
def Transcode(self, _i, queuedFile): filenameHandler = DetermineFilename(self.GetPlexPostProcess()) print(" PlexPostProcess to " + filenameHandler.GetTempFilename(queuedFile)) self.GetPlexPostProcess().GetDatabaseInteraction().AddQFHistory( queuedFile, "Transcode", " PlexPostProcess to " + filenameHandler.GetTempFilename(queuedFile)) command = [] if queuedFile.GetFiletype() == 'm4v': command = [ Settings.GetConfig('Applications', 'handbrake', '/usr/local/bin/HandBrakeCLI'), '--preset-import-file', os.path.join(Settings.GetRootPath(), 'handbrake_preset.json'), '-i', queuedFile.GetFilename(), '-o', filenameHandler.GetTempFilename(queuedFile), '--preset', 'Super HQ 1080p30 Surround MP3', '--decomb', 'bob' ] if Settings.GetConfig("Applications", "handbrake", "false").lower not in [ 'true', '1', 't', 'y', 'yes', 'yeah', 'yup', 'certainly', 'uh-huh', 'on' ]: command = None else: command = [ Settings.GetConfig('Applications', 'ffmpeg', '/usr/local/bin/ffmpeg'), '-i', queuedFile.GetFilename(), '-vn', '-acodec', 'copy', filenameHandler.GetTempFilename(queuedFile) ] if command is not None: self.RunCommand(queuedFile, command) else: queuedFile.SetState(PlexPostProcessState.MOVING_FILES) self.GetPlexPostProcess().GetDatabaseInteraction().UpdateQFState( queuedFile, "Transcode", "Transcode skipped")
def __init__(self, plexPostProcess): self.__plexPostProcess = plexPostProcess self.__london = pytz.timezone( 'Europe/London') #This is where corrie is aired self.__ltz = get_localzone() #The servers timezone self.__ltz = pytz.timezone( 'Europe/London' ) #For some reason plex writes it in pacific time on freenas self.__tmpDir = Settings.GetConfig('Paths', 'backup', '/mnt/PlexRecordings/BackupMP2') self.__plexLibraryPath = "/usr/local/plexdata-plexpass/Plex Media Server/Plug-in Support/Databases/com.plexapp.plugins.library.db"
def GetIDCommand(self, tempFilename): return [ Settings.GetConfig("Applications", "id3v2", '/usr/local/bin/id3v2'), '--TYER', self.GetYear(), '--TALB', self.GetShow(), '--TIT2', self.GetFilename(), '--TRCK', self.GetEpisode(), '--TPOS', self.GetSeason(), '--TPE1', self.GetShow(), '--TPE2', self.GetShow(), '--TALB', self.GetSeasonStr(), tempFilename ]
def Reconnect(self): self.__connection = pymysql.connect( host=Settings.GetConfig('Database', 'host', 'localhost'), user=Settings.GetConfig('Database', 'user', 'root'), password=Settings.GetConfig('Database', 'password', 'password'), db=Settings.GetConfig('Database', 'db', 'plex_post_process'), unix_socket=Settings.GetConfig('Database', 'unix_socket', '/tmp/mysql.sock'), charset=Settings.GetConfig('Database', 'charset', 'utf8mb4'), cursorclass=pymysql.cursors.DictCursor)
def GetTempFilename(self, queuedFile): if queuedFile.GetFiletype() == 'm4v': if Settings.GetConfig("Applications", "handbrake", "false").lower not in [ 'true', '1', 't', 'y', 'yes', 'yeah', 'yup', 'certainly', 'uh-huh', 'on' ]: return queuedFile.GetFilename() return os.path.join( self.__tmpDir, ntpath.basename(queuedFile.GetFilename()) + "." + str(queuedFile.GetId()) + ".mp4") else: return os.path.join( self.__tmpDir, ntpath.basename(queuedFile.GetFilename()) + "." + str(queuedFile.GetId()) + ".mp3")
def MoveFiles(self, _i, queuedFile): filenameHandler = DetermineFilename(self) self.GetDatabaseInteraction().AddQFHistory(queuedFile, "Move Files", "Moving from '" + filenameHandler.GetTempFilename(queuedFile) + "' to '" + filenameHandler.GetDestFilename(queuedFile) + "'") try: shutil.move(filenameHandler.GetTempFilename(queuedFile), filenameHandler.GetDestFilename(queuedFile)) except Exception as e: queuedFile.SetState(PlexPostProcessState.ERROR) print(e.__doc__) print(e.message) sys.exit(2) self.GetDatabaseInteraction().UpdateQFState(queuedFile, "Move Files", "Error " + str(sys.exc_info()[0])) return; if Settings.GetConfig("Applications", "handbrake", "false").lower in ['true', '1', 't', 'y', 'yes', 'yeah', 'yup', 'certainly', 'uh-huh', 'on' ]: queuedFile.SetState(PlexPostProcessState.DELETING_ORIGINAL_FILE) else: queuedFile.SetState(PlexPostProcessState.SUCCESS) self.GetDatabaseInteraction().UpdateQFState(queuedFile, "Move Files", "Finished moving files with success!")
def GetCorrieIndex(self, locCreateDt): locale.setlocale(locale.LC_TIME, "en_GB.UTF-8") conn = sqlite3.connect( Settings.GetConfig("Paths", "plexLibrary", self.GetPlexLibraryPath())) c = conn.cursor() formattedDate = locCreateDt.strftime("%A,%%" + ordinal(locCreateDt.day) + " %B %Y") sql = "select \"index\",title,guid from metadata_items where library_section_id=17 and guid like '%/71565/%' and metadata_type = 4 and title like '%" + formattedDate + "%';" possibilities = [] for row in c.execute(sql): possibilities.append(self.GetCorriePosibility(row)) conn.close() if len(possibilities) == 0: return None hasPart = False for posibility in possibilities: hasPart = hasPart or posibility[4] != None if hasPart: desired = possibilities[0][4] if locCreateDt.hour == 19: desired = 1 elif locCreateDt.hour == 20: desired = 2 if possibilities[0][4] != desired: possibilities[0][ 3] = possibilities[0][3] - possibilities[0][4] + desired possibilities[0][4] = desired return possibilities[0]
class PlexPostProcessDaemon(Daemon): def __init__(self, pidfile): Daemon.__init__(self, pidfile) self.__config = [] if len(sys.argv) >= 3: configFile = sys.argv[2] with open(configFile, 'r+') as configFileStream: self.__config = configFileStream.read().splitlines() self.__config.append('--daemon') def run(self): PlexPostProcessShared().mainWithArgs(self.__config) if __name__ == "__main__": daemon = PlexPostProcessDaemon(Settings.GetConfig('Paths','daemonLinePidFile','/tmp/daemon_plex_post_process.pid')) if len(sys.argv) > 1: if 'start' == sys.argv[1]: daemon.start() elif 'stop' == sys.argv[1]: daemon.stop() elif 'restart' == sys.argv[1]: daemon.restart() elif 'status' == sys.argv[1]: print('Current state: ' + daemon.status().name) else: print("Unknown command '" + sys.argv[1] + "'") sys.exit(2) sys.exit(0) else: print("usage: %s start|stop|restart|status" % sys.argv[0])
def mainWithArgs(self, argv): global DEBUG global TESTRUN global PROFILE global __all__ global __version__ global __date__ global __updated__ global wakeUp program_name = os.path.basename(sys.argv[0]) program_version = "v%s" % __version__ program_build_date = str(__updated__) program_version_message = '%%(prog)s %s (%s)' % (program_version, program_build_date) program_shortdesc = 'Scan and PlexPostProcess' program_license = '''%s %s MIT License Copyright (c) 2018 Christopher Dawes Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ''' % (program_shortdesc, str(__date__)) try: # Setup argument parser parser = ArgumentParser( description=program_license, formatter_class=RawDescriptionHelpFormatter) parser.add_argument( "-r", "--recursive", dest="recurse", action="store_true", help="recurse into subfolders [default: %(default)s]", default=True) parser.add_argument( "-v", "--verbose", dest="verbose", action="count", help="set verbosity level [default: %(default)s]", default=0) parser.add_argument( "-i", "--include", dest="include", help= "only include paths matching this regex pattern. Note: exclude is given preference over include. [default: %(default)s]", metavar="RE", default="*.ts") parser.add_argument( "-e", "--exclude", dest="exclude", help= "exclude paths matching this regex pattern. [default: %(default)s]", metavar="RE", default="*.done.ts") parser.add_argument('-V', '--version', action='version', version=program_version_message) parser.add_argument( '-D', '--daemon', dest='daemon', action="store_true", help="Run in daemon mode [default: %(default)s]", default=False) parser.add_argument( '-C', '--debug-corrie', dest='debugCorrie', action="store_true", help="Debug coronation street [default: %(default)s]", default=False) parser.add_argument( dest="paths", help= "paths to folder(s) with source file(s) [default: %(default)s]", metavar="path", nargs='*') # Process arguments args = parser.parse_args(argv) print(args) paths = args.paths + list( Settings.GetConfig('FileScanner').values()) verbose = args.verbose > 0 recurse = args.recurse inpat = args.include expat = args.exclude daemon = args.daemon if daemon: signal.signal(signal.SIGUSR1, WakeUpNow) if verbose: print("Verbose mode on") if recurse: print("Recursive mode on") else: print("Recursive mode off") if inpat and expat and inpat == expat: raise Exception( "include and exclude pattern are equal! Nothing will be processed." ) fileScanner = FileScanner(recurse=recurse, paths=paths, inpat=inpat, expat=expat, verbose=verbose) with DatabaseInteraction() as databaseInteraction: if args.debugCorrie: x = DetermineFilename( PlexPostProcessStateMachine(databaseInteraction)) print( x.GetNewCoronationStreetFilename( "Coronation Street (1960) - 2018-11-30 20 30 00 - Episode 11-30.ts", None)) return 0 running = True first = True updateDb = True while running: nrNewFiles = 0 if not first: updateDb = fileScanner.Rescan() if updateDb: print("Starting to update database...") nrNewFiles = databaseInteraction.UpdateWithFiles( fileScanner) print("Database update complete with " + str(nrNewFiles) + " new files.") if nrNewFiles > 0 or first: plexPostProcessr = PlexPostProcessStateMachine( databaseInteraction) DetermineFiletype( plexPostProcessr).DetermineFiletypes() plexPostProcessr.PlexPostProcess() if nrNewFiles == 0: wakeUp.clear() if wakeUp.wait(3600): #sleep for one hour time.sleep( 5 ) #sleep for 5 seconds to prevent race condition on plex running = daemon first = False return 0 except KeyboardInterrupt: ### handle keyboard interrupt ### return 0 except Exception as e: if DEBUG or TESTRUN: raise (e) indent = len(program_name) * " " sys.stderr.write(program_name + ": " + repr(e) + "\n") sys.stderr.write(indent + " for help use --help") return 2
PlexPostProcessShared().mainWithArgs(argv) if __name__ == "__main__": if DEBUG: sys.argv.append("-v") sys.argv.append("-r") if TESTRUN: import doctest doctest.testmod() if PROFILE: import cProfile import pstats profile_filename = 'com.camding.plexpostprocess.PlexPostProcess_profile.txt' cProfile.run('main()', profile_filename) statsfile = open("profile_stats.txt", "wb") p = pstats.Stats(profile_filename, stream=statsfile) stats = p.strip_dirs().sort_stats('cumulative') stats.print_stats() statsfile.close() sys.exit(0) sys.exit(main()) pid_file = Settings.GetConfig('Paths', 'cmdLinePidFile', '/tmp/cmd_plex_post_process.pid') fp = open(pid_file, 'w') try: fcntl.lockf(fp, fcntl.LOCK_EX | fcntl.LOCK_NB) except IOError: # another instance is running sys.exit(0)
def Commskip(self, _i, queuedFile): filenameHandler = DetermineFilename(self.GetPlexPostProcess()) print(" Commskip to " + filenameHandler.GetTempFilename(queuedFile)); self.GetPlexPostProcess().GetDatabaseInteraction().AddQFHistory(queuedFile, "Commskip", " Commskip to " + filenameHandler.GetTempFilename(queuedFile)); if "The X Factor (2004)" in queuedFile.GetFilename(): queuedFile.SetState(PlexPostProcessState.TRANSCODING) self.GetPlexPostProcess().GetDatabaseInteraction().UpdateQFState(queuedFile, "Comskip", "XFactor cannot be comskipped; it's too much like an advert ;)") return command = [ Settings.GetConfig('Applications', 'bash', '/usr/local/bin/bash'), os.path.join(Settings.GetRootPath(), 'plex_commskip.sh'), queuedFile.GetFilename() ] logfile_queue = Queue() logfile_queue.put(" ".join(command).encode('utf-8')) sys.stdout.write(" ".join(command)) sys.stdout.flush() # Launch the process with PIPE stdout and stderr process = self.GetPlexPostProcess().RunProcess(command, None, sys.stdin, PIPE, PIPE) # Function for reader threads, echo lines from in_fh to out_fh and out_queue def read_handler(in_fh, out_fh, out_queue): while True: line = in_fh.readline() if not line: return out_fh.buffer.write(line) out_fh.flush() out_queue.put(line) # Function for writer thread, echo lines from in_queue to out_fh def write_handler(in_queue, queuedFile, databaseInteraction): while True: line = in_queue.get() if line is None: return databaseInteraction.AddQFHistory(queuedFile, "Commskip", line) # Launch a thread for reading stdout, reading stderr, and writing the logfile stdout_thread = Thread(target=read_handler, args=(process.stdout, sys.stdout, logfile_queue)) stderr_thread = Thread(target=read_handler, args=(process.stderr, sys.stderr, logfile_queue)) logfile_thread = Thread(target=write_handler, args=(logfile_queue, queuedFile, self.GetPlexPostProcess().GetDatabaseInteraction())) for thread in stdout_thread, stderr_thread, logfile_thread: thread.daemon = True thread.start() # Wait for the process to complete process.wait() # Wait for stdout and stderr threads to complete for thread in stdout_thread, stderr_thread: thread.join() # Signal and wait for the logfile thread to complete logfile_queue.put(None) logfile_thread.join() if process.returncode == 0: queuedFile.SetState(PlexPostProcessState.TRANSCODING) self.GetPlexPostProcess().GetDatabaseInteraction().UpdateQFState(queuedFile, "Comskip", "Started processing") else: queuedFile.SetState(PlexPostProcessState.TRANSCODING) self.GetPlexPostProcess().GetDatabaseInteraction().UpdateQFState(queuedFile, "Comskip", "Error " + str(process.returncode))
def DetermineFiletypeForFile(self, i, queuedFile): if queuedFile.GetState() == PlexPostProcessState.PENDING_DELETE_DUPLICATE: return print(' Determining filetype for ' + str(i) + queuedFile.GetFilename() + ' in state ' + queuedFile.GetState().name) self.GetPlexPostProcess().GetDatabaseInteraction().AddQFHistory(queuedFile, "DetermineFiletype", " Determine file type for " + queuedFile.GetFilename()); command = [ Settings.GetConfig('Applications', 'ffprobe', '/usr/local/bin/ffprobe'), '-v', 'quiet', '-show_streams', '-hide_banner', queuedFile.GetFilename()] logfile_queue = Queue() logfile_queue.put(" ".join(command).encode('utf-8')) sys.stdout.write(" ".join(command)) sys.stdout.flush() # Launch the process with PIPE stdout and stderr process = self.GetPlexPostProcess().RunProcess(command, None, sys.stdin, PIPE, PIPE) # Function for reader threads, echo lines from in_fh to out_fh and out_queue def read_handler(in_fh, out_fh, out_queue): while True: line = in_fh.readline() if not line: return out_fh.buffer.write(line) out_fh.flush() out_queue.put(line) # Function for writer thread, echo lines from in_queue to out_fh def write_handler(in_queue, queuedFile, databaseInteraction, texts): while True: line = in_queue.get() if line is None: return databaseInteraction.AddQFHistory(queuedFile, "DetermineFiletype", line) if "codec_type=video" in line.decode("utf-8"): texts.append(line.decode("utf-8")) texts = [] # Launch a thread for reading stdout, reading stderr, and writing the logfile stdout_thread = Thread(target=read_handler, args=(process.stdout, sys.stdout, logfile_queue)) stderr_thread = Thread(target=read_handler, args=(process.stderr, sys.stderr, logfile_queue)) logfile_thread = Thread(target=write_handler, args=(logfile_queue, queuedFile, self.GetPlexPostProcess().GetDatabaseInteraction(), texts)) for thread in stdout_thread, stderr_thread, logfile_thread: thread.daemon = True thread.start() # Wait for the process to complete process.wait() # Wait for stdout and stderr threads to complete for thread in stdout_thread, stderr_thread: thread.join() # Signal and wait for the logfile thread to complete logfile_queue.put(None) logfile_thread.join() if process.returncode == 0: queuedFile.SetFiletype('m4v' if len(texts) > 0 else 'mp3') self.GetPlexPostProcess().GetDatabaseInteraction().UpdateQFFiletype(queuedFile, "DetermineFiletype", "Determined output format as " + queuedFile.GetFiletype()) else: queuedFile.SetState(PlexPostProcessState.ERROR) self.GetPlexPostProcess().GetDatabaseInteraction().UpdateQFState(queuedFile, "DetermineFiletype", "ffprobe error " + str(process.returncode))