diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..35755d6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +*.pyc +.spyderproject + diff --git a/DoEncode.py b/DoEncode.py deleted file mode 100755 index 846a726..0000000 --- a/DoEncode.py +++ /dev/null @@ -1,125 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Created on Sat Jun 29 23:25:55 2013 - -@author: shane -""" - -import glob -import logging -import os -import shutil -import subprocess -from xml.etree import ElementTree - -SETTINGSFILE = "settings.xml" -HANDBRAKECOMMAND = ['HandBrakeCLI', '--verbose', '-i', '"{0}"', '-o', '"{1}"', - '-f', 'mkv', '-e', 'x264', '-x264-preset', 'slower', - '-x264-tune', 'animation', '-q', '20', - '--loose-anamorphic', '--decomb', '--detelecine', - '--denoise="2:1:2:3"', '--deblock'] - -TVRECORDINGSDIR = "/srv/storage2/videos/TVRecordings/" - -LOGFILE = "encoding.log" -ACTIONLOG = "needsAction.log" - -class TVShow: - def __init__(self, name, inputDirectory, outputDirectory): - self.name = name - self.inputDirectory = inputDirectory - self.outputDirectory = outputDirectory - -def LoadSettings(source): - shows = [] - settingsXml = ElementTree.parse(source).getroot() - - for show in settingsXml.findall('show'): - newShow = TVShow(show[0].text, show[1].text, show[2].text) - shows.append(newShow) - return shows - -def FindSeason(path, fileName): - season = "Season {0}".format(fileName[1:3]) - seasonPath = os.path.join(path, season) - if not os.path.exists(seasonPath): - os.makedirs(seasonPath) - - return seasonPath - -def GetRecordingFile(file): - return os.path.join(TVRECORDINGSDIR, os.path.dirname(inputFile).split("/")[-1] + ".mpg") - -def CheckOldOutputFileExists(file, myLogger): - myLogger.debug("Searching for existing file: {0}".format(file[:-3]+"*")) - return glob.glob(file[:-3]+"*") - -def CreateLogger(name, filename, level): - logger = logging.getLogger(name) - handler = logging.FileHandler(filename) - formatter = logging.Formatter('%(asctime)s %(message)s') - handler.setFormatter(formatter) - handler.setLevel(level) - logger.addHandler(handler) - return logger - -#generalLogger.basicConfig(filename=LOGFILE, level=logging.DEBUG, format='%(asctime)s %(message)s') - -#actionLogger = logging.getLogger("action") -#actionLogger.basicConfig(filename=ACTIONLOG, level=logging.INFO, format='%(asctime)s %(message)s') - -logging.basicConfig(level=logging.DEBUG) -generalLogger = CreateLogger("general", LOGFILE, logging.DEBUG) -actionLogger = CreateLogger("action", ACTIONLOG, logging.INFO) - -generalLogger.debug("Loading settings from {0}".format(SETTINGSFILE)) -shows = LoadSettings(SETTINGSFILE) - -for show in shows: - generalLogger.info("Processing {0}".format(show.name)) - fileList = [] - - for r,d,f in os.walk(show.inputDirectory): - for files in f: - if files.endswith(".mpg"): - fileList.append(os.path.join(r,files)) - - for inputFile in fileList: - generalLogger.info("Processing file {0}".format(inputFile)) - - inFile = os.path.basename(inputFile) - outFilename = inFile[:-3]+"mkv" - outPath = FindSeason(show.outputDirectory, outFilename) - outFile = os.path.join(outPath, outFilename) - generalLogger.debug("Output file is {0}".format(outFile)) - - if os.path.isfile(outFile): - message = "File {0} already exists. Not processing any further.".format(outFile) - generalLogger.warning(message) - actionLogger.info(message) - else: - existingFile = CheckOldOutputFileExists(outFile, generalLogger) - generalLogger.debug("Search returned {0}".format(existingFile)) - if len(existingFile) > 0: - message = "There is an existing version of {0} at {1}.".format(outFilename, existingFile[0]) - generalLogger.info(message) - actionLogger.info(message) - - HANDBRAKECOMMAND[3] = inputFile - HANDBRAKECOMMAND[5] = outFile - generalLogger.debug("Handbrake command is: {0}".format(HANDBRAKECOMMAND)) - po = subprocess.Popen(HANDBRAKECOMMAND) - po.wait() - generalLogger.info("Handbrake completed with return code {0}".format(po.returncode)) - - generalLogger.info("Deleting input files from {0}".format(os.path.dirname(inputFile))) - shutil.rmtree(os.path.dirname(inputFile)) - - linkAddress = GetRecordingFile(inputFile) - generalLogger.info("Deleting original file from {0}".format(linkAddress)) - os.remove(linkAddress) - - generalLogger.info("Creating symlink from {0} to {1}".format(linkAddress, outFile)) - os.symlink(outFile, linkAddress) - - generalLogger.info("Processing completed for {0}".format(inputFile)) diff --git a/EmailSettings.cfg b/EmailSettings.cfg new file mode 100644 index 0000000..90c69ad --- /dev/null +++ b/EmailSettings.cfg @@ -0,0 +1,5 @@ +SMTPServer = "" +SMTPUser = "" +SMTPPassword = "" +From = "" +To = "" diff --git a/ListFilesToEncode.py b/ListFilesToEncode.py deleted file mode 100755 index d587abd..0000000 --- a/ListFilesToEncode.py +++ /dev/null @@ -1,32 +0,0 @@ -import os -from xml.etree import ElementTree - -SETTINGSFILE = "settings.xml" - -class TVShow: - def __init__(self, name, inputDirectory, outputDirectory): - self.name = name - self.inputDirectory = inputDirectory - self.outputDirectory = outputDirectory - -def LoadSettings(source): - shows = [] - settingsXml = ElementTree.parse(source).getroot() - - for show in settingsXml.findall('show'): - newShow = TVShow(show[0].text, show[1].text, show[2].text) - shows.append(newShow) - return shows - -shows = LoadSettings(SETTINGSFILE) - -for show in shows: - fileList = [] - - for r,d,f in os.walk(show.inputDirectory): - for files in f: - if files.endswith(".mpg"): - fileList.append(os.path.join(r,files)) - - for inputFile in fileList: - print inputFile diff --git a/ProcessRecordings.py b/ProcessRecordings.py deleted file mode 100755 index abf40cf..0000000 --- a/ProcessRecordings.py +++ /dev/null @@ -1,154 +0,0 @@ -import os -import shutil -import MySQLdb as mdb -import glob -import json -from urllib import urlopen -from fuzzywuzzy import fuzz -from operator import itemgetter - -PROCESSDIR="/srv/storage2/files/VideoProcessing/" -THOMAS="Thomas" -CHUGGINGTON="Chuggington" -MIKE="MikeTheKnight" -OCTONAUTS="Octonauts" -NIGHTGARDEN="InTheNightGarden" -RAARAA="RaaRaa" -INPUTDIR="Input" -SICKBEARDAPI="http://192.168.0.2:8081/api/3678177136222bf5002be209220ccb20/" - -class TVShow: - def __init__(self, episode, season, title, subtitle, description): - self.episode = episode - self.season = season - self.title = title - self.subtitle = subtitle - self.description = description - -def FindShowId(showName): - jsonurl = urlopen(SICKBEARDAPI+"?cmd=shows") - result = json.loads(jsonurl.read()) - - shows = [] - for show in result['data']: - shows.append((show, fuzz.partial_ratio(showName.lower(), result['data'][show]['show_name'].lower()))) - - shows = sorted(shows, key=itemgetter(1), reverse=True) - - if shows[0][1] > 85: - return shows[0][0] - -def FindEpisode(showId, name=None, description=None): - jsonurl = urlopen("{0}?cmd=show.seasons&tvdbid={1}".format(SICKBEARDAPI, showId)) - result = json.loads(jsonurl.read()) - - for season in result['data']: - for episode in result['data'][season]: - if name is not None and name.lower() == result['data'][season][episode]['name'].lower(): - return (season, episode) - elif description is not None: - result = FindEpisodeByDescription(showId, season, episode, description) - if result is not None: - return result - - return (0, 0) - -def GetEpisodeName(subtitle, showName): - if subtitle[:len(showName)].lower() == showName.lower(): - return subtitle[len(showName + ' and the '):] - else: - return subtitle - -def FindEpisodeByDescription(showId, season, episode, description): - jsonEpisodeUrl = urlopen("{0}?cmd=episode&tvdbid={1}&season={2}&episode={3}".format(SICKBEARDAPI, showId, season, episode)) - episodeResult = json.loads(jsonEpisodeUrl.read()) - - if fuzz.ratio(episodeResult['data']['description'].lower(), description.lower()) > 85: - return (season, episode) - - return None - -def DetermineTargetFilename(directory, filename, inputFilename): - dir = os.path.join(directory, inputFilename[:-4]) - - if not os.path.exists(dir): - os.makedirs(dir) - - return os.path.join(dir, filename) - -def ProcessKnownEpisode(directory, filename, inputFilename): - target = DetermineTargetFilename(directory, filename, inputFilename) - shutil.move(inputFilename, target) - -def ProcessUnknownEpisode(inputFilename): - print "do this" - -def RetrieveEpisodeData(inputFile): - con = mdb.connect('localhost', 'script', 'script', 'mythconverg') - - with con: - cur = con.cursor(mdb.cursors.DictCursor) - cur.execute("select episode, season, title, subtitle, description from mythconverg.recorded where basename = '{0}'".format(inputFile)) - result = cur.fetchone() - - return TVShow(result['episode'], result['season'], result['title'], result['subtitle'], result['description']) - -def FixEpisodeSeasonNumber(number): - if number < 10: - return "0{0}".format(number) - else: - return str(number) - -def GetDirectory(title, season): - directory = "" - if title == "Thomas and Friends" or title == "Thomas the Tank Engine & Friends": - directory = THOMAS - elif title == "Chuggington": - directory = CHUGGINGTON - elif title == "Mike the Knight": - directory = MIKE - elif title == "Octonauts" or title == "The Octonauts": - directory = OCTONAUTS - elif title == "In the Night Garden": - directory = NIGHTGARDEN - elif title == "Raa Raa! The Noisy Lion": - directory = RAARAA - else: - print "Didn't match" - - return os.path.join(PROCESSDIR, directory, INPUTDIR, season) - - -def ProcessEpisode(inputFile): - show = RetrieveEpisodeData(inputFile) - - if show.title: - if show.subtitle: - show.subtitle = GetEpisodeName(show.subtitle, show.title) - - if (show.season == '0' or show.episode == '0'): - showId = FindShowId(show.title) - - result = FindEpisode(showId, show.subtitle, show.description) - show.season = result[0] - show.episode = result[1] - - if show.season != "0" and show.episode != "0": - show.season = FixEpisodeSeasonNumber(show.season) - show.episode = FixEpisodeSeasonNumber(show.episode) - - seasonFolder = "Season {0}".format(show.season) - season = "S{0}".format(show.season) - episode = "E{0}".format(show.episode) - renamedFile = "{0}{1} - {2} - SD TV_.mpg".format(season, episode, show.subtitle) - - directory = GetDirectory(show.title, seasonFolder) - ProcessKnownEpisode(directory, renamedFile, os.path.basename(inputFile)) - else: - print "no show name" - -def GetFilesToProcess(): - return glob.glob("*.mpg") - -for file in GetFilesToProcess(): - ProcessEpisode(file) diff --git a/ProcessRecordingsTest.py b/ProcessRecordingsTest.py deleted file mode 100755 index 7f1b30b..0000000 --- a/ProcessRecordingsTest.py +++ /dev/null @@ -1,67 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Created on Thu Jul 04 14:40:38 2013 - -@author: shane -""" -import ProcessRecordings -import unittest -from minimock import mock -import os.path - - -class ProcessRecordingsTest(unittest.TestCase): - def test_fixnumber(self): - result = ProcessRecordings.FixEpisodeSeasonNumber(1) - self.assertEqual("01", result) - - def test_fixnumber2(self): - result = ProcessRecordings.FixEpisodeSeasonNumber(9) - self.assertEqual("09", result) - - def test_fixnumber3(self): - result = ProcessRecordings.FixEpisodeSeasonNumber(11) - self.assertEqual("11", result) - - def test_episodeName(self): - subtitle = 'Mike the Knight and the Test Case' - title = 'Mike the Knight' - result = ProcessRecordings.GetEpisodeName(subtitle, title) - self.assertEqual('Test Case', result) - - def test_episodeName2(self): - subtitle = 'Test Case 2' - title = 'Mike the Knight' - result = ProcessRecordings.GetEpisodeName(subtitle, title) - self.assertEqual('Test Case 2', result) - - def test_GetDirectoryThomas(self): - title = 'Thomas and Friends' - season = 'Season 01' - result = ProcessRecordings.GetDirectory(title, season) - self.assertEqual("/srv/storage2/files/VideoProcessing/Thomas/Input/Season 01", result) - - def test_GetDirectoryThomas2(self): - title = 'Thomas the Tank Engine & Friends' - season = 'Season 01' - result = ProcessRecordings.GetDirectory(title, season) - self.assertEqual("/srv/storage2/files/VideoProcessing/Thomas/Input/Season 01", result) - - def test_GetDirectoryChuggington(self): - title = 'Chuggington' - season = 'Season 02' - result = ProcessRecordings.GetDirectory(title, season) - self.assertEqual("/srv/storage2/files/VideoProcessing/Chuggington/Input/Season 02", result) - - def test_DetermineTargetFilename(self): - directory = '/srv/storage2/test/Input' - filename = 'S01E02 - test episode - SD TV_.mpg' - inputFilename = '123456.mpg' - - mock('os.path.exists', returns=True) - result = ProcessRecordings.DetermineTargetFilename(directory, filename, inputFilename) - self.assertEqual('/srv/storage2/test/Input/123456/S01E02 - test episode - SD TV_.mpg', result) - -if __name__ == '__main__': - suite = unittest.TestLoader().loadTestsFromTestCase(ProcessRecordingsTest) - unittest.TextTestRunner(verbosity=2).run(suite) \ No newline at end of file diff --git a/README b/README index e69de29..f6c95b5 100644 --- a/README +++ b/README @@ -0,0 +1 @@ +Testing README file diff --git a/TVEncoder.py b/TVEncoder.py new file mode 100644 index 0000000..152a36d --- /dev/null +++ b/TVEncoder.py @@ -0,0 +1,204 @@ +# -*- coding: utf-8 -*- +""" +Created on Fri Jul 5 14:14:22 2013 + +@author: shanef +""" + +import sys +import getopt +from libfilemanager import FileManager +from libsettings import Settings +import libhandbrake +import libemail +from libtvdatasource import TVData +from collections import namedtuple +from termcolor import colored +import logging + +SETTINGS = "settings.cfg" +EMAIL_SETTINGS = "EmailSettings.cfg" + + +def showhelp(): + """ + Prints the command lines switches that are valid for the program. + """ + + print 'TVEncoder.py -p -n ' \ + '- prepare n recordings' + print 'TVEncoder.py -p -l -n - lists the ' \ + 'files that will be processed without actually encoding them' + print 'TVEncoder.py -e - encode the files that have been processed' + print 'TVEncoder.py -e -l - list the files that would be encoded' + print 'TVEncoder.py -c - check the output directories for duplicates' + + +def print_shows(shows): + """ + Prints he details of the shows. + """ + okshows = [] + noepisodes = [] + existingfiles = [] + + for show in shows: + showstr = str(show) + + errors = show.checkproblems() + if not errors: + okshows.append(showstr) + elif "NO_EPISODE" in errors: + noepisodes.append(showstr) + elif "FILE_EXISTS" in errors: + existingfiles.append(showstr) + + for show in okshows: + print show + + if noepisodes: + print colored("\nDetails of the episode could not be determined for " + "the following shows:", 'red') + for show in noepisodes: + print colored(show, 'red') + + if existingfiles: + print colored("\nThe following shows have a pre-existing " + "output file:", 'red') + for show in existingfiles: + print colored(show, 'red') + + +def processarguments(options): + """ + Determine the actions required from the input flags + """ + + inputoptions = namedtuple("inputoptions", + "numfiles doencode readonly dolist " + "checkduplicates") + + inputoptions.readonly = False + inputoptions.checkduplicates = False + + for opt, arg in options: + if opt == '-h': + showhelp() + sys.exit() + elif opt == "-p": + inputoptions.doencode = False + elif opt == "-e": + inputoptions.doencode = True + elif opt == "-n": + inputoptions.numfiles = arg + elif opt == "-l": + inputoptions.readonly = True + elif opt == "-c": + inputoptions.checkduplicates = True + + return inputoptions + + +def main(argv): + """ + The main program for TVEncoder. + """ + try: + opts, _ = getopt.getopt(argv, "hlpecn:") + except getopt.GetoptError: + showhelp() + sys.exit(2) + inputoptions = processarguments(opts) + + settings = Settings(SETTINGS) + filemanager = FileManager(settings) + + if inputoptions.checkduplicates: + print "Searching for duplicates..." + duplicates = filemanager.checkexistingduplicates() + if duplicates: + for duplicate in duplicates: + print duplicate + else: + print "No duplicates found." + return + + if inputoptions.readonly: + if inputoptions.doencode: + #Generate the list of files that would be encoded + showdata = filemanager.getencodingfiles(inputoptions.readonly) + print_shows(showdata) + else: + # Generate the list of files to process + shows = filemanager.getfilestoprepare(inputoptions.numfiles) + print "num results: {0}".format(len(shows)) + print_shows(shows) + else: + if inputoptions.doencode: + #Encode the files and move them to their final destination + + logging.basicConfig(level=logging.DEBUG) + generallogger = createlogger("general", settings.generallogfile(), + logging.DEBUG) + actionlogger = createlogger("action", settings.actionlogfile(), + logging.INFO) + + showdata = filemanager.getencodingfiles(inputoptions.readonly) + generallogger.info("There are {0} files to process." + .format(len(showdata))) + for show in showdata: + generallogger.info("========================================") + generallogger.info("Processing {0} of {1}, {2}".format( + showdata.index(show) + 1, len(showdata), str(show))) + + if filemanager.checkfileexists(show.outputfile): + message = "File {0} already exists. Cannot process." \ + .format(show.outputfile) + generallogger.warning(message) + actionlogger.warning(message) + else: + result = libhandbrake.encode(settings.handbrakecommand(), + show.inputfile, + show.outputfile) + + generallogger.info("Encode finished with result: {0}" + .format(result)) + filemanager.performpostencodefileoperations( + show.inputfile, show.outputfile) + + if filemanager.checkduplicates(show.outputfile): + actionlogger.info("There is an existing video file" + "present for {0}" + .format(show.outputfile)) + + generallogger.info("Processing finished.") + generallogger.info("===========================" + "=============\n\n") + + libemail.sendemail(EMAIL_SETTINGS, "Encoding Complete", + "Finished encoding {0} shows." + .format(len(showdata))) + else: + # Process files for encoding + shows = filemanager.getfilestoprepare(inputoptions.numfiles) + print "Preparing {0} files".format(len(shows)) + tvdata = TVData(settings) + tvdata.prepareepisodes(shows) + + +def createlogger(name, filename, level): + """ + Create a logger named that will write to the file + """ + + logger = logging.getLogger(name) + handler = logging.FileHandler(filename, mode='w') + formatter = logging.Formatter('%(asctime)s %(message)s') + handler.setFormatter(formatter) + handler.setLevel(level) + logger.addHandler(handler) + return logger + + +if __name__ == "__main__": + main(sys.argv[1:]) diff --git a/libemail.py b/libemail.py new file mode 100644 index 0000000..a886880 --- /dev/null +++ b/libemail.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- +""" +Created on Sat Jul 20 20:48:10 2013 + +@author: shanef +""" + +from libsettings import EmailSettings + +import smtplib +from email.mime.text import MIMEText + + +def sendemail(settingsfilename, subject, body): + """ + Send an email using the settings defined in settingsfilename + """ + + settings = EmailSettings(settingsfilename) + + msg = MIMEText(body, "plain") + msg["Subject"] = subject + msg["From"] = settings.getfromaddress() + msg["To"] = settings.gettoaddress() + + smtp = smtplib.SMTP(settings.getsmtpserver()) + smtp.ehlo() + smtp.starttls() + smtp.login(settings.getsmtpuser(), settings.getsmtppassword()) + smtp.sendmail(settings.getfromaddress(), [settings.gettoaddress()], + msg.as_string()) + smtp.quit() diff --git a/libfilemanager.py b/libfilemanager.py new file mode 100644 index 0000000..db51349 --- /dev/null +++ b/libfilemanager.py @@ -0,0 +1,244 @@ +# -*- coding: utf-8 -*- +""" +Created on Fri Jul 5 14:11:31 2013 + +@author: shanef +""" + +import glob +from libtvdatasource import TVData +import os +import shutil + + +class EncodeData: + """ + Contains detais of files to encode. + inputfile - The source file + outputfile - The destination file + show - The name of the show + """ + + def __init__(self, show=None, inputfile='', outputfile=''): + self.inputfile = inputfile + self.show = show + self.outputfile = outputfile + + def __str__(self): + return "Show: {0}\nInput: {1}\nOutput: " \ + "{2}\n".format(self.show, self.inputfile, self.outputfile) + + def checkproblems(self): + """ + Check the EncodeData object for any potential problems. + """ + + errors = [] + + if checkfileexists(self.outputfile, False): + errors.append("FILE_EXISTS") + + if self.outputfile[-5:-4] == "_": + tempoutfile = self.outputfile[:-5] + self.outputfile[-4:] + if checkfileexists(tempoutfile, False): + errors.append("FILE_EXISTS") + + return errors + + +class FileManager: + """ + Perform file operations + """ + + def __init__(self, settings): + self.__settings = settings + + def getencodingfiles(self, readonly=True): + """ + Get the details of the shows that are ready for encoding + """ + + showsdata = self.__getinputfilestoencode() + for showdata in showsdata: + showdata.outputfile = self.__getencodeoutputfile( + showdata.inputfile, showdata.show, readonly) + + return showsdata + + def performpostencodefileoperations(self, inputfilename, outputfilename): + """ + Delete the input file, and the original recorded file. Then create a + symlink from the new encoded file to the original mythtv file. + """ + + shutil.rmtree(os.path.dirname(inputfilename)) + + linkaddress = self.__getrecordingfile(inputfilename) + + os.remove(linkaddress) + + os.symlink(outputfilename, linkaddress) + + def getfilestoprepare(self, numberoffiles): + """ + Get the details of the first to prepare for encoding. + If there are less files than available, it will + return the details of the number available. + """ + + path = self.__settings.tvrecordingdirectory() + potentialfiles = glob.glob("{0}*.mpg".format(path)) + potentialfiles = sorted(potentialfiles, key=os.path.getctime) + potentialfiles = [potentialfile for potentialfile in potentialfiles + if not os.path.islink(potentialfile)] + + #files is now a list of unprocessed files, but contains shows other + #than those we are interested in + showstoprocess = [] + i = 0 + print "Found {0} potential files".format(len(potentialfiles)) + + tvdata = TVData(self.__settings) + + for potentialfile in potentialfiles: + showdata = tvdata.retrieveepisodedata(potentialfile) + if showdata: + showstoprocess.append(showdata) + i = i + 1 + if i == int(numberoffiles): + return showstoprocess + + #will reach here if there were less than numberofFiles found + return showstoprocess + + def checkexistingduplicates(self): + """ + Check the existing files in the output directories for duplicate + files, typically in different formats + """ + + duplicates = [] + for show in self.__settings.getshownames(): + outputdir = self.__settings.getshowoutputdirectory(show) + + for rootdir, dirnames, filenames in os.walk(outputdir): + for fle in filenames: + filename = os.path.join(rootdir, fle) + if os.path.splitext(fle)[1].lower() in [".avi", ".mpg", ".mpeg", + "mp4", ".mkv"]: + if self.checkduplicates(filename): + duplicates.append(filename) + + return sorted(duplicates) + + @staticmethod + def checkduplicates(filename): + """ + Check to see if there are any other video files existing for the + episode + """ + + dirname = os.path.dirname(filename) + filename = os.path.basename(filename) + fileseasonepisode = filename[:6] + fileextension = os.path.splitext(filename)[1] + + for _, _, filenames in os.walk(dirname): + for show in filenames: + extension = os.path.splitext(show)[1] + if (extension.lower() in [".avi", ".mpg", ".mpeg", "mp4", ".mkv"] and + show[:6] == fileseasonepisode + and fileextension != extension): + return True + + return False + + @staticmethod + def checkfileexists(filename, casesensitive=True): + """ + Check to see if a file currently exists + """ + if casesensitive: + return os.path.exists(filename) + else: + filename = os.path.basename(filename) + for dirfile in os.listdir(os.path.dirname(filename)): + if (filename.lower() == dirfile.lower()): + return True + + return False + + def __getinputfilestoencode(self): + """ + Get the details of the files that are waiting to be encoded + """ + + filelist = [] + + for show in self.__settings.getshownames(): + for dirpath, _, filenames in os.walk( + self.__settings.getshowinputdirectory(show)): + for inputfile in filenames: + if inputfile.endswith(".mpg"): + data = EncodeData(show, os.path.join( + dirpath, inputfile)) + filelist.append(data) + + return filelist + + def __getencodeoutputfile(self, inputfile, showname, readonly): + """ + Get the full path of the output filename to save the encoded video to + """ + + infile = os.path.basename(inputfile) + outfilename = infile[:-3]+"mkv" + outpath = findseason(self.__settings.getshowoutputdirectory( + showname), outfilename, readonly) + return os.path.join(outpath, outfilename) + + def __getrecordingfile(self, filename): + """ + Get the name of the mythtv recording based on the filename. The + filename contains the name of the mythtv recording as the + final directory in it's path. + """ + + return os.path.join(self.__settings.tvrecordingdirectory(), + os.path.dirname(filename).split("/")[-1] + ".mpg") + + +def findseason(path, filename, readonly): + """ + Get the name of the season folder. eg. Season 01 + """ + + season = "Season {0}".format(filename[1:3]) + seasonpath = os.path.join(path, season) + + if not readonly: + if not os.path.exists(seasonpath): + os.makedirs(seasonpath) + + return seasonpath + + +def checkfileexists(filename, casesensitive=True): + """ + Check to see if a file currently exists + """ + dirname = os.path.dirname(filename) + + if casesensitive: + return os.path.exists(filename) + else: + if not os.path.exists(dirname): + return False + + basename = os.path.basename(filename) + for dirfile in os.listdir(dirname): + if (basename.lower() == dirfile.lower()): + return True + + return False diff --git a/libhandbrake.py b/libhandbrake.py new file mode 100644 index 0000000..0e865f7 --- /dev/null +++ b/libhandbrake.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- +""" +Created on Fri Jul 5 14:11:00 2013 + +@author: shanef + +Library to interface with handbrake to encode video files +""" + +import subprocess + + +def encode(handbrakecommand, inputfile, outputfile, waitforcompletion=True, + logger=None): + """ + Encode inputfile and save the result to outputfile. handbrakecommand is + a list of strings containing the arguments to handbrakecli. + """ + + handbrakecommand[3] = inputfile + handbrakecommand[5] = outputfile + + if logger: + logger.debug("Handbrake command is: {0}".format(handbrakecommand)) + + process = subprocess.Popen(handbrakecommand) + + if waitforcompletion: + process.wait() + + if logger is not None: + logger.info("Handbrake completed with return code {0}".format( + process.returncode)) + return process.returncode + + return None diff --git a/libmythtv.py b/libmythtv.py new file mode 100644 index 0000000..a65cd7d --- /dev/null +++ b/libmythtv.py @@ -0,0 +1,53 @@ +# -*- coding: utf-8 -*- +""" +Created on Fri Jul 5 14:10:47 2013 + +@author: shanef +""" + +import MySQLdb as mdb +from libtvshow import TVShow + + +class MythTV: + """ + Contains methods used for interacting with mythtv + """ + + def __init__(self, settings): + self.__settings = settings + + def retrieveepisodedata(self, inputfile): + """ + Retrieve the data that mythtv knows about the recorded file. + """ + con = mdb.connect(self.__settings.mythtvaddress(), + self.__settings.mythtvuser(), + self.__settings.mythtvpassword(), + self.__settings.mythtvdatabase()) + + with con: + cur = con.cursor(mdb.cursors.DictCursor) + cur.execute("select episode, season, title, subtitle, " + "description from mythconverg.recorded where " + "basename = '{0}'".format(inputfile)) + result = cur.fetchone() + + return TVShow(result['episode'], result['season'], + result['title'], result['subtitle'], + result['description']) + + def fixmythtvepisodename(self, showname, episodetitle): + """ + Look for any prefixes listed in the configuration file. If there are + any and the episide title starts with the prefix, remove the prefix + from the episode title. The searching is done in the order that the + prefixes are listed in the configuration file. + """ + + for prefix in self.__settings.getshowmythtvepisodeprefix(showname): + if episodetitle.lower().startswith(prefix.lower()): + return episodetitle[len(prefix):] + + #didn't find anything so return the episode title + return episodetitle diff --git a/libsettings.py b/libsettings.py new file mode 100644 index 0000000..efc5db6 --- /dev/null +++ b/libsettings.py @@ -0,0 +1,287 @@ +# -*- coding: utf-8 -*- +""" +Created on Fri Jul 5 20:14:15 2013 + +@author: shanef +""" + +from configobj import ConfigObj + + +class Settings: + """ + Accessor for the configuration file + """ + + def __init__(self, settingsfile): + """ + Initialise settingsfile as a configobj + """ + + self.__config = ConfigObj(settingsfile) + + def tvrecordingdirectory(self): + """ + Get the TVRecordings setting + """ + + return self.__config["TVRecordings"] + + def handbrakecommand(self): + """ + Get the HandbrakeCommand setting + """ + + return self.__config["HandbrakeCommand"] + + def illegalcharacters(self): + """ + Get a list of illegal characters for filenames + """ + + return self.__config["IllegalCharacters"] + + def generallogfile(self): + """ + Get the filename to save general log messages to + """ + + return self.__config["Logging"]["General"] + + def actionlogfile(self): + """ + Get the filename to save the action log messages to + """ + + return self.__config["Logging"]["Action"] + + def mythtvaddress(self): + """ + Get the MythTV/address setting + """ + + return self.__config["MythTV"]["address"] + + def mythtvuser(self): + """ + Get the MythTV/user setting + """ + + return self.__config["MythTV"]["user"] + + def mythtvpassword(self): + """ + Get the MythTV/password setting + """ + + return self.__config["MythTV"]["password"] + + def mythtvdatabase(self): + """ + Get the MythTV/database setting + """ + + return self.__config["MythTV"]["database"] + + def sickbeardaddress(self): + """ + Get the Sickbeard/address setting + """ + + return self.__config["Sickbeard"]["address"] + + def sickbeardport(self): + """ + Get the Sickbeard/port setting + """ + + return int(self.__config["Sickbeard"]["port"]) + + def sickbeardapikey(self): + """ + Get the Sickbeard/APIKey setting + """ + + return self.__config["Sickbeard"]["APIKey"] + + def unknowndirectory(self): + """ + Get the Shows/UnknownInput directory. It is the directory used for + episodes where nothing is known about it + """ + + return self.__config["Shows"]["UnknownInput"] + + def getshownames(self, includealias=False): + """ + Get a list of the names of the shows that are specified in the + settings file. If includealias is True, it will also include any + defined aliases in the list. + """ + + shows = self.__config["Shows"].sections + result = shows[:] + if includealias: + for show in shows: + for alias in self.__config["Shows"][show]["alias"]: + result.append(alias) + return result + + def findshownameforalias(self, aliasname): + """ + Find the name of the show. If the supplied aliasname is an alias, it + will return the show name. If aliasname is the name of a show, it will + return aliasname + """ + + if aliasname in self.getshownames(): + # aliasname is the name of an actual show + return aliasname + + # search for the show that the alias belongs to + for showsettings in self.__config["Shows"]: + if aliasname in showsettings["alias"]: + return showsettings.name + + # Could not find it anywhere + return None + + def getshowinputdirectory(self, showname): + """ + Get the InputDirectory setting for the show, showname. + """ + + show = self.__getshowsubsection(showname) + if show is None: + return "" + else: + return show["InputDirectory"] + + def getshowunknowndirectory(self, showname): + """ + Get the UnknownDirectory setting for the show, showname. It is used + when the show is known, but the season or episode are not. + """ + + show = self.__getshowsubsection(showname) + if show is None: + return "" + else: + return show["UnknownDirectory"] + + def getshowoutputdirectory(self, showname): + """ + Get the OutputDirectory setting for the show, showname. + """ + + show = self.__getshowsubsection(showname) + if show is None: + return "" + else: + return show["OutputDirectory"] + + def getshowalias(self, showname): + """ + Get the alias setting for the show, showname. It returns a list of + aliases. + """ + + show = self.__getshowsubsection(showname) + if show is None: + return "" + else: + return show["alias"] + + def getshowmythtvepisodeprefix(self, showname): + """ + Get the MythTVEpisodePrefix setting for the show, showname. + """ + + show = self.__getshowsubsection(showname) + if show is None: + return "" + else: + return show["MythTvEpisodePrefix"] + + def getshowsickbeardepisodeprefix(self, showname): + """ + Get the SickbeardPrefix setting for the show, showname. + """ + + show = self.__getshowsubsection(showname) + if show is None: + return "" + else: + return show["SickbeardPrefix"] + + def getshow(self, showname): + """ + Get the name of the show, showname. + """ + showsection = self.__getshowsubsection(showname) + if showsection is None: + return None + else: + return showsection.name + + def __getshowsubsection(self, showname): + """ + Get the configuration options for the show, showname. + """ + + if showname in self.getshownames(): + return self.__config["Shows"][showname] + else: # check liases + for show in self.getshownames(): + if showname in self.__config["Shows"][show]["alias"]: + return self.__config["Shows"][show] + + return None + + +class EmailSettings: + """ + Accessor for the email configuration file + """ + + def __init__(self, settingsfile): + """ + Initialise settingsfile as a configobj + """ + + self.__config = ConfigObj(settingsfile) + + def getsmtpserver(self): + """ + Get the address of the smtp server + """ + + return self.__config["SMTPServer"] + + def getsmtpuser(self): + """ + Get the username for the smtp server + """ + + return self.__config["SMTPUser"] + + def getsmtppassword(self): + """ + Get the username for the smtp server + """ + + return self.__config["SMTPPassword"] + + def getfromaddress(self): + """ + Get the from address for emails + """ + + return self.__config["From"] + + def gettoaddress(self): + """ + Get the to address for emails + """ + + return self.__config["To"] diff --git a/libsickbeard.py b/libsickbeard.py new file mode 100644 index 0000000..3ef2ad1 --- /dev/null +++ b/libsickbeard.py @@ -0,0 +1,153 @@ +# -*- coding: utf-8 -*- +""" +Created on Fri Jul 5 14:10:37 2013 + +@author: shanef +""" + +import json +from urllib import urlopen +from fuzzywuzzy import fuzz +from operator import itemgetter + + +class Sickbeard: + """ + Contains operations used to interact with sickbeard + """ + + def __init__(self, settings): + self.__settings = settings + self.__address = settings.sickbeardaddress() + self.__port = settings.sickbeardport() + self.__apikey = settings.sickbeardapikey() + + def findshowid(self, showname): + """ + Get the tvdb show id for the show + """ + + jsonurl = urlopen(self.__getapiurl()+"?cmd=shows") + result = json.loads(jsonurl.read()) + + showname = self.__settings.findshownameforalias(showname) + + shows = [] + for show in result['data']: + shows.append((show, fuzz.partial_ratio(showname.lower(), + result['data'][show] + ['show_name'].lower()))) + + shows = sorted(shows, key=itemgetter(1), reverse=True) + + if shows[0][1] > 85: + return shows[0][0] + + def findepisodename(self, showid, season, episode): + """ + Get the name of an episode, given it's season and episode numbers + """ + + jsonurl = urlopen("{0}?cmd=episode&tvdbid={1}&season={2}" + "&episode={3}".format(self.__getapiurl(), showid, + int(season), int(episode))) + + result = json.loads(jsonurl.read()) + + if result['result'] == 'error': + return "" + else: + return result['data']['name'] + + def findepisode(self, showid, name=None, description=None): + """ + Find an episode, either by it's name or it's description. This is used + when the season and episode numbers are not known + """ + + jsonurl = urlopen("{0}?cmd=show.seasons&tvdbid={1}".format( + self.__getapiurl(), showid)) + + result = json.loads(jsonurl.read()) + + for season in result['data']: + for episode in result['data'][season]: + episodename = result['data'][season][episode]['name'] + if name is not None and fuzz.ratio(name.lower(), + episodename.lower()) > 85: + return (season, episode, episodename) + elif description is not None: + descriptionqueryresult = \ + self.__findepisodebydescription(showid, season, + episode, description) + if descriptionqueryresult is not None: + return descriptionqueryresult + + return (0, 0, '') + + def fixepisodetitle(self, showname, episodetitle): + """ + Check to see if there is a prefix specified for the show. If there is, + add the prefix to the start of the episode title + """ + + sickbeardprefix = \ + self.__settings.getshowsickbeardepisodeprefix(showname) + + if sickbeardprefix != "": + if not episodetitle.lower().startswith(sickbeardprefix.lower()): + return "{0} {1}".format(sickbeardprefix.rstrip(), + episodetitle.lstrip()) + + return episodetitle + + def __getapiurl(self): + """ + Get the url of the sickbeard api, substituting the values from the + settings + """ + + return "http://{0}:{1}/api/{2}/".format(self.__address, self.__port, + self.__apikey) + + def __findepisodebydescription(self, showid, season, episode, description): + """ + Find the details of an episode by searching for it's description + """ + + jsonepisodeurl = urlopen("{0}?cmd=episode&tvdbid={1}&season={2}" + "&episode={3}".format(self.__getapiurl(), + showid, season, + episode)) + episoderesult = json.loads(jsonepisodeurl.read()) + + sickbearddescription = episoderesult['data']['description'] + + if fuzzystringcompare(sickbearddescription, description): + return (season, episode, episoderesult['data']['name']) + + return None + + +def fuzzystringcompare(string1, string2, matchvalue=85, casesensitive=False): + """ + Compare two strings to see if they match it first does a straight + comparison. Secondly, it concatenates the longer string to the length of + the shorter one, and tries to compare them again. + """ + + if not casesensitive: + string1 = string1.lower() + string2 = string2.lower() + + if fuzz.ratio(string1, string2) > matchvalue: + return True + + if len(string1) > len(string2): + if fuzz.ratio(string1[:len(string2)], string2) > matchvalue: + return True + elif len(string2) > len(string1): + if fuzz.ratio(string1, string2[:len(string1)]) > matchvalue: + return True + + return False diff --git a/libtvdatasource.py b/libtvdatasource.py new file mode 100644 index 0000000..ddffeec --- /dev/null +++ b/libtvdatasource.py @@ -0,0 +1,144 @@ +# -*- coding: utf-8 -*- +""" +Created on Fri Jul 5 14:42:47 2013 + +@author: shanef +""" + +from libmythtv import MythTV +from libsickbeard import Sickbeard +import os +import shutil + + +def fixepisodeseasonnumber(number): + """ + If the number is single digit, return a string with 0 in front of it. + """ + + if len(number) == 1: + return "0{0}".format(number) + else: + return number + + +class TVData: + """ + Class contains logic for processing information about tv episodes + """ + + def __init__(self, settings): + self.__settings = settings + + def getdirectory(self, title, seasonfolder, season, episode): + """ + Get the directory where prepared episodes will be located. + """ + + show = self.__settings.getshow(title) + if not show or show == "": + print "Couldn't find show for {0}".format(title) + return self.__settings.unknowndirectory() + elif season == "S00" or episode == "E00": + return self.__settings.getshowunknowndirectory(show) + else: + return os.path.join(self.__settings.getshowinputdirectory(show), + seasonfolder) + + def retrieveepisodedata(self, inputfile): + """ + Retrieve the details of an episode. It first looks up the details that + mythtv recorded about it, then looks up sickbeard to attempt to find + any missing details. Finally it determined the output file for it. + """ + + inputfilename = os.path.basename(inputfile) + + mythtv = MythTV(self.__settings) + show = mythtv.retrieveepisodedata(inputfilename) + + showstoprocess = self.__settings.getshownames(True) + + if show.title and show.title in showstoprocess: + show.title = self.__settings.getshow(show.title) + + if (show.season == "0" or show.episode == "0"): + sickbeard = Sickbeard(self.__settings) + showid = sickbeard.findshowid(show.title) + + if show.subtitle is not None and show.subtitle: + show.subtitle = mythtv.fixmythtvepisodename(show.title, + show.subtitle) + show.subtitle = sickbeard.fixepisodetitle(show.title, + show.subtitle) + + result = sickbeard.findepisode(showid, show.subtitle, + show.description) + show.season = str(result[0]) + show.episode = str(result[1]) + show.subtitle = result[2] + + if show.subtitle is None or show.subtitle == "": + show.subtitle = sickbeard.findepisodename(showid, show.season, + show.episode) + + show.season = fixepisodeseasonnumber(show.season) + show.episode = fixepisodeseasonnumber(show.episode) + + seasonfolder = "Season {0}".format(show.season) + season = "S{0}".format(show.season) + episode = "E{0}".format(show.episode) + renamedfile = self.getoutputfilename(season, episode, + show.subtitle) + + directory = self.getdirectory(show.title, seasonfolder, + season, episode) + + show.outputfile = os.path.join(directory, inputfilename[:-4], + renamedfile) + show.inputfile = inputfile + + return show + else: + return None + + def getoutputfilename(self, season, episode, name): + """ + Get the output filename, and remove any illegal characters + """ + + filename = "{0}{1} - {2} - SD TV_.mpg".format(season, episode, name) + + for illegalcharacter in self.__settings.illegalcharacters(): + filename = filename.replace(illegalcharacter, "") + + return filename + + @staticmethod + def processepisode(inputfile, outputfile): + """ + Copy inputfile to outputfile, creating the path for outputfile if + required. + """ + + outputdir = os.path.dirname(outputfile) + if not os.path.exists(outputdir): + os.makedirs(outputdir) + + shutil.copyfile(inputfile, outputfile) + + def prepareepisodes(self, showsdata): + """ + Copy the files in showsdata from their input directory to their output + directory. + """ + + for showdata in showsdata: + print "========================================" + print "Copying {0} to {1}".format(showdata.inputfile, + showdata.outputfile) + + self.processepisode(showdata.inputfile, showdata.outputfile) + + print "Finished copy" + print "========================================\n\n" diff --git a/libtvshow.py b/libtvshow.py new file mode 100644 index 0000000..73903a9 --- /dev/null +++ b/libtvshow.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- +""" +Created on Sat Jul 6 20:26:22 2013 + +@author: shanef +""" + +import os +#from libfilemanager import FileManager + + +class TVShow(object): + """ + Describes the details of a tv episode + """ + + def __init__(self, episode, season, title, subtitle, description, + inputfile='', outputfile=''): + self.episode = str(episode) + self.season = str(season) + self.title = title + self.subtitle = subtitle + self.description = description + self.inputfile = inputfile + self.outputfile = outputfile + + def __str__(self): + return "Input: {0} -> Output: {1}".format(self.inputfile, + self.outputfile) + + def checkproblems(self): + """ + Check the TVShow object for any potential problems. + """ + + errors = [] + if self.episode == "E00" or self.season == "S00" or not self.subtitle: + errors.append("NO_EPISODE") + + if os.path.exists(self.outputfile): + errors.append("FILE_EXISTS") + + return errors diff --git a/pep8.sh b/pep8.sh new file mode 100755 index 0000000..ce2b244 --- /dev/null +++ b/pep8.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +pep8 libemail.py TVEncoder.py libfilemanager.py libhandbrake.py libmythtv.py libsettings.py libsickbeard.py libtvdatasource.py libtvshow.py diff --git a/pylint.sh b/pylint.sh new file mode 100755 index 0000000..5204ecc --- /dev/null +++ b/pylint.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +pylint TVEncoder.py libfilemanager.py libhandbrake.py libmythtv.py libsettings.py libsickbeard.py libtvdatasource.py libtvshow.py libemail.py diff --git a/settings.cfg b/settings.cfg new file mode 100644 index 0000000..b84c6ca --- /dev/null +++ b/settings.cfg @@ -0,0 +1,72 @@ +TVRecordings = "/Volumes/TV Recordings/" +HandbrakeCommand = "HandBrakeCLI", "--verbose", "-i", "SUBSTITUTE WITH INPUT FILE", "-o", "SUBSTITUDE WITH OUTPUT FILE", "-f", "mkv", "-e", "x264", "-x264-preset", "slower", "-x264-tune", "animation", "-q", "20", "--loose-anamorphic", "--decomb", "--detelecine", '--denoise="2:1:2:3"', "--deblock" +IllegalCharacters = "?", ":" + +[ "Logging" ] + General = "logs/encoding.log" + Action = "logs/needsaction.log" + +[ "MythTV" ] + address = 192.168.0.2 + user = script + password = script + database = mythconverg + +[ "Sickbeard" ] + address = 192.168.0.2 + port = 8081 + APIKey = 3678177136222bf5002be209220ccb20 + +[ "Shows" ] + VideoProcessingDir = "/srv/storage2/files/VideoProcessing/" + KidsTVDir = "/srv/storage2/videos/Kids/TV/" + UnknownInput = "%(VideoProcessingDir)sUnknown/" + [[ "Thomas the Tank Engine & Friends" ]] + InputDirectory = "%(VideoProcessingDir)sThomas/" + UnknownDirectory = "%(UnknownInput)sThomas/" + OutputDirectory = "%(KidsTVDir)sThomas The Tank Engine & Friends/" + alias = "Thomas and Friends", + MythTvEpisodePrefix = , + SickbeardPrefix = "" + [[ "Chuggington" ]] + InputDirectory = "%(VideoProcessingDir)sChuggington/" + UnknownDirectory = "%(UnknownInput)sChuggington/" + OutputDirectory = "%(KidsTVDir)sChuggington/" + alias = , + MythTvEpisodePrefix = , + SickbeardPrefix = "" + [[ "Mike the Knight" ]] + InputDirectory = "%(VideoProcessingDir)sMikeTheKnight/" + UnknownDirectory = "%(UnknownInput)sMikeTheKnight/" + OutputDirectory = "%(KidsTVDir)sMike the Knight/" + alias = , + MythTvEpisodePrefix = "Mike the Knight and the ", Mike the Knight and " + SickbeardPrefix = "" + [[ "Octonauts" ]] + InputDirectory = "%(VideoProcessingDir)sOctonauts/" + UnknownDirectory = "%(UnknownInput)sOctonauts/" + OutputDirectory = "%(KidsTVDir)sOctonauts/" + alias = "The Octonauts", + MythTvEpisodePrefix = "The Octonauts and ", + SickbeardPrefix = "The" + [[ "In the Night Garden" ]] + InputDirectory = "%(VideoProcessingDir)sInTheNightGarden/" + UnknownDirectory = "%(UnknownInput)sInTheNightGarden/" + OutputDirectory = "%(KidsTVDir)sIn The Night Garden/" + alias = , + MythTvEpisodePrefix = , + SickbeardPrefix = "" + [[ "Raa Raa! The Noisy Lion" ]] + InputDirectory = "%(VideoProcessingDir)sRaaRaa/" + UnknownDirectory = "%(UnknownInput)sRaaRaa/" + OutputDirectory = "%(KidsTVDir)sRaa Raa the Noisy Lion/" + alias = , + MythTvEpisodePrefix = , + SickbeardPrefix = "" + [[ "Fireman Sam" ]] + InputDirectory = "%(VideoProcessingDir)sFiremanSam/" + UnknownDirectory = "%(UnknownInput)sFiremanSam/" + OutputDirectory = "%(KidsTVDir)sFireman Sam/" + alias = , + MythTvEpisodePrefix = , + SickbeardPrefix = "" \ No newline at end of file diff --git a/sickbeard.py b/sickbeard.py deleted file mode 100755 index 31a835d..0000000 --- a/sickbeard.py +++ /dev/null @@ -1,68 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Created on Tue Jul 2 21:00:35 2013 - -@author: shane -""" -import json -from urllib import urlopen -from fuzzywuzzy import fuzz -from operator import itemgetter - -def FindShowId(showName): - jsonurl = urlopen("http://192.168.0.2:8081/api/3678177136222bf5002be209220ccb20/?cmd=shows") - result = json.loads(jsonurl.read()) - - shows = [] - for show in result['data']: - shows.append((show, fuzz.partial_ratio(showName.lower(), result['data'][show]['show_name'].lower()))) - - shows = sorted(shows, key=itemgetter(1), reverse=True) - - if shows[0][1] > 85: - return shows[0][0] - -def FindEpisode(showId, name=None, description=None): - jsonurl = urlopen("http://192.168.0.2:8081/api/3678177136222bf5002be209220ccb20/?cmd=show.seasons&tvdbid={0}".format(showId)) - result = json.loads(jsonurl.read()) - - for season in result['data']: - for episode in result['data'][season]: - if name is not None and name.lower() == result['data'][season][episode]['name'].lower(): - return (season, episode) - elif description is not None: - result = FindEpisodeByDescription(showId, season, episode, description) - if result is not None: - return result - - return (0, 0) - -def GetEpisodeName(subtitle, showName): - if subtitle[:len(showName)].lower() == showName.lower(): - return subtitle[len(showName + ' and the '):] - else: - return subtitle - -def FindEpisodeByDescription(showId, season, episode, description): - jsonEpisodeUrl = urlopen("http://192.168.0.2:8081/api/3678177136222bf5002be209220ccb20/?cmd=episode&tvdbid={0}&season={1}&episode={2}".format(showId, season, episode)) - episodeResult = json.loads(jsonEpisodeUrl.read()) - - if fuzz.ratio(episodeResult['data']['description'].lower(), description.lower()) > 85: - return (season, episode) - - return None - -showId = FindShowId('Mike the Knight') -#showId = FindShowId("Octonauts") -#print showId - -subtitle = 'Mike the Knight and the Knightly Campout' - -description = "When the Octopod's waterworks are flooded with frightened Humuhumu fish, the Octonauts have to find a way to flush them out!" - -episodeName = GetEpisodeName(subtitle, 'Mike the Knight') - -result = FindEpisode(showId, episodeName) -#result = FindEpisodeByDescription(showId, description) -print result[0] -print result[1] \ No newline at end of file diff --git a/tests/TVEncodertest.py b/tests/TVEncodertest.py new file mode 100644 index 0000000..d415f05 --- /dev/null +++ b/tests/TVEncodertest.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8 -*- +""" +Created on Sat Jul 13 20:37:47 2013 + +@author: shanef +""" + +import unittest +import os +import sys +parentdir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +sys.path.insert(0, parentdir) +import TVEncoder + + +class TVEncoderTest(unittest.TestCase): + def test_processarguments_encodereadonly(self): + args = [] + args.append(('-e', '')) + args.append(('-l', '')) + result = TVEncoder.processarguments(args) + + self.assertTrue(result.doencode) + self.assertTrue(result.readonly) + + def test_processarguments_encodereadonlyreverse(self): + args = [] + args.append(('-l', '')) + args.append(('-e', '')) + result = TVEncoder.processarguments(args) + + self.assertTrue(result.doencode) + self.assertTrue(result.readonly) + + def test_processarguments_encode(self): + args = [] + args.append(('-e', '')) + result = TVEncoder.processarguments(args) + + self.assertTrue(result.doencode) + self.assertFalse(result.readonly) + + +if __name__ == '__main__': + suite = unittest.TestLoader().loadTestsFromTestCase(TVEncoderTest) + unittest.TextTestRunner(verbosity=2).run(suite) \ No newline at end of file diff --git a/tests/emailtest.py b/tests/emailtest.py new file mode 100644 index 0000000..92cc000 --- /dev/null +++ b/tests/emailtest.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- +""" +Created on Fri Jul 19 23:31:16 2013 + +@author: shanef +""" + +from minimock import Mock, mock +import unittest +import os +import sys +parentdir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +sys.path.insert(0, parentdir) +import libemail +from libsettings import EmailSettings +import smtplib + + +class libemailtest(unittest.TestCase): + def test_SendEmail(self): + mock("EmailSettings.getfromaddress", returns="from@email.com") + mock("EmailSettings.gettoaddress", returns="to@email.com") + mock("EmailSettings.getsmtpserver", returns="smtp.test") + mock("EmailSettings.getsmtpuser", returns="user") + mock("EmailSettings.getsmtppassword", returns="password") + smtplib.SMTP = Mock('smtplib.SMTP') + smtplib.SMTP.mock_returns = Mock('smtp_connection') + + libemail.sendemail("test", "subject", "body") + +if __name__ == '__main__': + suite = unittest.TestLoader().loadTestsFromTestCase(libemailtest) + unittest.TextTestRunner(verbosity=2).run(suite) diff --git a/tests/libfilemanagertest.py b/tests/libfilemanagertest.py new file mode 100644 index 0000000..c8b8f9b --- /dev/null +++ b/tests/libfilemanagertest.py @@ -0,0 +1,117 @@ +# -*- coding: utf-8 -*- +""" +Created on Fri Jul 5 14:12:26 2013 + +@author: shanef +""" + +import unittest +import os +import sys +import minimock +from minimock import mock, Mock +parentdir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +sys.path.insert(0, parentdir) +from libfilemanager import EncodeData, FileManager + + +class libfilemanagertest(unittest.TestCase): + def test_EncodeDataPrint(self): + showname = "test show" + inputname = "test input" + outputname = "test output" + data = EncodeData(showname, inputname, outputname) + result = str(data) + expected = "Show: {0}\nInput: {1}\nOutput: " \ + "{2}\n".format(showname, inputname, outputname) + self.assertEqual(result, expected) + + def test_EncodeDataCheckProblemsFileExists(self): + showname = "test show" + inputname = "test input" + outputname = "test_output.mkv" + data = EncodeData(showname, inputname, outputname) + mock("os.path.exists", returns=True) + + result = data.checkproblems() + + self.assertIn("FILE_EXISTS", result) + minimock.restore() + + def test_EncodeDataCheckProblemsFile_Exists(self): + showname = "test show" + inputname = "test input" + outputname = "test_output_.mkv" + data = EncodeData(showname, inputname, outputname) + mock("os.path.exists", returns_iter=[False, True]) + result = data.checkproblems() + self.assertIn("FILE_EXISTS", result) + minimock.restore() + + def test_checkfileexistscaseinsensitive(self): + settings = Mock('libsettings.Settings') + filemanager = FileManager(settings) + + mock("os.listdir", returns=["filename.test"]) + + result = filemanager.checkfileexists("/path/to/fiLename.test", False) + + self.assertTrue(result) + minimock.restore() + + def test_checkduplicateavi(self): + settings = Mock('libsettings.Settings') + filemanager = FileManager(settings) + + os.walk = dummywalk + + result = filemanager.checkduplicates("/path/to/S03E14 - Test - SD TV.mkv") + + self.assertTrue(result) + minimock.restore() + + def test_checkduplicatethomas(self): + settings = Mock('libsettings.Settings') + filemanager = FileManager(settings) + + os.walk = thomaswalk + + result = filemanager.checkduplicates("/path/to/S12E05 - Henry Gets It Wrong - SD TV.mkv") + + self.assertTrue(result) + minimock.restore() + + def test_checkduplicatenomatch(self): + settings = Mock('libsettings.Settings') + filemanager = FileManager(settings) + + os.walk = dummywalk + + result = filemanager.checkduplicates("/path/to/S03E13 - Test - SD TV.mkv") + + self.assertFalse(result) + minimock.restore() + + def test_checkduplicatesameextension(self): + settings = Mock('libsettings.Settings') + filemanager = FileManager(settings) + + os.walk = dummywalk + + result = filemanager.checkduplicates("/path/to/S03E14 - Test - SD TV.avi") + + self.assertFalse(result) + minimock.restore() + + + +def dummywalk(arg): + return [("/path/to/", [], ["S03E14 - Test - SD TV.avi"])] + +def thomaswalk(arg): + return [(("/path/to/", [], ["S12E05 - Henry Gets It Wrong - Unknown.AVI"]))] + +if __name__ == '__main__': + suite = unittest.TestLoader().loadTestsFromTestCase(libfilemanagertest) + unittest.TextTestRunner(verbosity=2).run(suite) + minimock.restore() diff --git a/tests/libhandbraketest.py b/tests/libhandbraketest.py new file mode 100644 index 0000000..993692d --- /dev/null +++ b/tests/libhandbraketest.py @@ -0,0 +1,7 @@ +# -*- coding: utf-8 -*- +""" +Created on Fri Jul 5 14:13:19 2013 + +@author: shanef +""" + diff --git a/tests/libmythtvtest.py b/tests/libmythtvtest.py new file mode 100644 index 0000000..893cdd4 --- /dev/null +++ b/tests/libmythtvtest.py @@ -0,0 +1,55 @@ +# -*- coding: utf-8 -*- +""" +Created on Fri Jul 5 14:12:53 2013 + +@author: shanef +""" + +import unittest +from minimock import Mock +import os +import sys +parentdir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +sys.path.insert(0, parentdir) +import libmythtv + + +class MythTVTest(unittest.TestCase): + def test_FixEpisodeNameNoPrefix(self): + settings = Mock('libsettings.Settings') + settings.getshowmythtvepisodeprefix.mock_returns = "" + mythtv = libmythtv.MythTV(settings) + result = mythtv.fixmythtvepisodename("Show", "episode") + self.assertEqual(result, "episode") + + def test_FixEpisodeNameNonMatchingPrefix(self): + settings = Mock('libsettings.Settings') + settings.getshowmythtvepisodeprefix.mock_returns = [ "BloohBlah" ] + mythtv = libmythtv.MythTV(settings) + result = mythtv.fixmythtvepisodename("Show", "episode") + self.assertEqual(result, "episode") + + def test_FixEpisodeNameMatchingPrefix(self): + settings = Mock('libsettings.Settings') + settings.getshowmythtvepisodeprefix.mock_returns = [ "Match " ] + mythtv = libmythtv.MythTV(settings) + result = mythtv.fixmythtvepisodename("Show", "Match episode") + self.assertEqual(result, "episode") + + def test_FixEpisodeNameMatchingFirstPrefix(self): + settings = Mock('libsettings.Settings') + settings.getshowmythtvepisodeprefix.mock_returns = [ "Match and ", "Match the " ] + mythtv = libmythtv.MythTV(settings) + result = mythtv.fixmythtvepisodename("Show", "Match and episode") + self.assertEqual(result, "episode") + + def test_FixEpisodeNameMatchingSecondPrefix(self): + settings = Mock('libsettings.Settings') + settings.getshowmythtvepisodeprefix.mock_returns = [ "Match and ", "Match the " ] + mythtv = libmythtv.MythTV(settings) + result = mythtv.fixmythtvepisodename("Show", "Match the episode") + self.assertEqual(result, "episode") + +if __name__ == '__main__': + suite = unittest.TestLoader().loadTestsFromTestCase(MythTVTest) + unittest.TextTestRunner(verbosity=2).run(suite) \ No newline at end of file diff --git a/tests/libsickbeardtest.py b/tests/libsickbeardtest.py new file mode 100644 index 0000000..2015aaa --- /dev/null +++ b/tests/libsickbeardtest.py @@ -0,0 +1,51 @@ +# -*- coding: utf-8 -*- +""" +Created on Fri Jul 5 14:12:38 2013 + +@author: shanef +""" + +import unittest +from minimock import Mock +import os +import sys +parentdir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +sys.path.insert(0, parentdir) +import libsickbeard +import urllib + + +class SickbeardTest(unittest.TestCase): + def test_findepisodeCloseSubtitle(self): + settings = Mock('libsettings.Settings') + settings.sickbeardaddress.mock_returns = "test" + settings.sickbeardport.mock_returns = "123" + settings.sickbeardapikey.mock_returns = "test" + + urllib.urlopen = dummy_urlopen + + sickbeard = libsickbeard.Sickbeard(settings) + + result = sickbeard.findepisode("78949", "Splish, Splash, Splosh") + + self.assertEqual("13", result[0]) + self.assertEqual("15", result[1]) + self.assertEqual("Splish, Splash, Splosh!", result[2]) + + +def dummy_urlopen(arg): + class TmpClass: + def read(arg): + jsonresult = '{ "data": {"13": { "15": { "airdate": "2010-02-12", ' \ + '"name": "Splish, Splash, Splosh!", "quality": "N/A", ' \ + '"status": "Wanted" } } }, "message": "", ' \ + '"result": "success" }' + + return jsonresult + + return TmpClass() + + +if __name__ == '__main__': + suite = unittest.TestLoader().loadTestsFromTestCase(SickbeardTest) + unittest.TextTestRunner(verbosity=2).run(suite) diff --git a/tests/libtvdatasourcetest.py b/tests/libtvdatasourcetest.py new file mode 100644 index 0000000..cba96bb --- /dev/null +++ b/tests/libtvdatasourcetest.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- +""" +Created on Thu Jul 18 23:13:15 2013 + +@author: shanef +""" + +import unittest +from minimock import Mock +import os +import sys +parentdir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +sys.path.insert(0, parentdir) +import libtvdatasource + + +class tvdatasourceTest(unittest.TestCase): + def test_GetOutputFilenameNoIllegals(self): + result = self._dooutputfilenametest("S01", "E02", "test name", "") + self.assertEqual(result, "S01E02 - test name - SD TV_.mpg") + + def test_GetOutputFilenameOneIllegals(self): + result = self._dooutputfilenametest("S01", "E02", "test name?", "?") + self.assertEqual(result, "S01E02 - test name - SD TV_.mpg") + + def test_GetOutputFilenameTwoIllegals(self): + result = self._dooutputfilenametest("S01", "E02", "tes>t name?", ["?", ">"]) + self.assertEqual(result, "S01E02 - test name - SD TV_.mpg") + + def _dooutputfilenametest(self, season, episode, name, illegals): + settings = Mock('libsettings.Settings') + settings.illegalcharacters.mock_returns = illegals + tvdatasource = libtvdatasource.TVData(settings) + return tvdatasource.getoutputfilename(season, episode, name) + +if __name__ == '__main__': + suite = unittest.TestLoader().loadTestsFromTestCase(tvdatasourceTest) + unittest.TextTestRunner(verbosity=2).run(suite) \ No newline at end of file