diff --git a/launch.py b/launch.py index ae2e778..955e128 100644 --- a/launch.py +++ b/launch.py @@ -3,6 +3,7 @@ import sys import signal import mythtv import mythboxee +from mythboxee import MythBoxee # DEBUG # #mc.GetApp().GetLocalConfig().SetValue("dbconn", "") @@ -12,5 +13,5 @@ import mythboxee mc.ActivateWindow(14001) # Lets go ahead and launch the app -mythboxee.Launch() +MythBoxee() diff --git a/mythboxee.py b/mythboxee.py index ec237c7..14cce54 100644 --- a/mythboxee.py +++ b/mythboxee.py @@ -4,267 +4,344 @@ import mythtv from mythtv import MythError from operator import itemgetter, attrgetter -mbbe = None -mbdb = None -config = mc.GetApp().GetLocalConfig() +class MythBoxee: + logLevel = 1 + version = "4.0.beta" -titles = [] -recordings = [] -idbanners = {} -shows = {} + userAgent = "MythBoxee v4.0.beta" + tvdb_apikey = "6BEAB4CB5157AAE0" + + be = None + db = None + + recs = None + titles = [] + recordings = [] + banners = {} + shows = {} + series = {} -def DiscoverBackend(): - mc.ShowDialogNotification("DiscoverBackend") - - pin = config.GetValue("pin") - dbconn = config.GetValue("dbconn") - - if not pin: - pin = 0000 - - try: - db = mythtv.MythDB(SecurityPin=pin) - except Exception, e: - mc.ShowDialogNotification(e.message) - if e.ename == 'DB_CREDENTIALS' and count < 3: - mc.ActivateWindow(14002) - mc.GetWindow(14002).GetControl(6020).SetVisible(False) - mc.GetWindow(14002).GetControl(6010).SetVisible(True) - mc.GetWindow(14002).GetControl(6011).SetFocus() - elif e.ename == 'DB_CONNECTION' or e.ename == 'DB_CREDENTIALS' and count > 3: - mc.ActivateWindow(14002) - mc.GetWindow(14002).GetControl(6010).SetVisible(False) - mc.GetWindow(14002).GetControl(6020).SetVisible(True) - mc.GetWindow(14002).GetControl(6021).SetFocus() - return False - else: - mc.ShowDialogNotification(str(db.dbconn)) - config.SetValue("dbconn", str(db.dbconn)) - return True - - -def Launch(): - # If dbconn isn't set, we'll assume we haven't found the backend. - if not config.GetValue("dbconn"): - discoverBackend = False - while discoverBackend is False: - discoverBackend = DiscoverBackend() - - # Parse our DB info - dbconn = config.GetValue("dbconn") - dbconf = eval(dbconn) - - # Now that the backend has been discovered, lets connect. - try: - mbdb = mythtv.MythDB(**dbconf) - except MythError, e: - print e.message - mc.ShowDialogNotification("Failed to connect to the MythTV Backend") - else: - mbbe = mythtv.MythBE(db=mbdb) - mc.ActivateWindow(14010) - + """ + DiscoverBackend - just as it sounds -def LoadShows(): - del titles[:] - del recordings[:] - idbanners.clear() - shows.clear() + Attempt to discover the MythTV Backend using UPNP protocol, once found + try and gather MySQL database connection information using default PIN + via the XML interface. If that fails then prompt user to enter their + custom SecurityPin, if we fail to gather database information that way + finally prompt user to enter their credentials manually. + """ + def DiscoverBackend(): + self.log("def(DiscoverBackend)") - config = mc.GetApp().GetLocalConfig() - sg = mc.Http() - html = sg.Get("http://" + config.GetValue("server") + ":6544/Myth/GetRecorded") - results = re.compile("(.*?)").findall(html) - for title,subtitle,endtime,airdate,starttime,desc,chanid in results: - if title not in titles: - titles.append(title) - idbanners[title] = GetSeriesIDBanner(title) - shows[title] = [] + pin = self.config.GetValue("pin") + dbconn = self.config.GetValue("dbconn") - single = [title,subtitle,desc,chanid,airdate,starttime,endtime] - recordings.append(single) + if not pin: + pin = 0000 + + try: + self.db = mythtv.MythDB(SecurityPin=pin) + except Exception, e: + if e.ename == 'DB_CREDENTIALS' and count < 2: + mc.ActivateWindow(14002) + mc.GetWindow(14002).GetControl(6020).SetVisible(False) + mc.GetWindow(14002).GetControl(6010).SetVisible(True) + mc.GetWindow(14002).GetControl(6011).SetFocus() + elif e.ename == 'DB_CONNECTION' or e.ename == 'DB_CREDENTIALS' and count > 3: + mc.ActivateWindow(14002) + mc.GetWindow(14002).GetControl(6010).SetVisible(False) + mc.GetWindow(14002).GetControl(6020).SetVisible(True) + mc.GetWindow(14002).GetControl(6021).SetFocus() + return False + else: + self.config.SetValue("dbconn", str(self.db.dbconn)) + return True + + + """ + Lets make a connection to the backend! + """ + def __init__(self): + self.config = mc.GetApp().GetLocalConfig() + + self.log("def(__init__)") + + # If this is the first time the app is being run, lets set some default options. + if not self.config.GetValue("firstrun"): + self.config.SetValue("SortBy", "Original Air Date") + self.config.SetValue("SortDir", "Descending") + self.config.SetValue("firstrun", "1") + + # If dbconn isn't set, we'll assume we haven't found the backend. + if not self.config.GetValue("dbconn"): + discoverBackend = False + while discoverBackend is False: + print "discover" + discoverBackend = self.DiscoverBackend() + + # Parse our DB info + dbconn = self.config.GetValue("dbconn") + dbconf = eval(dbconn) + + # Now that the backend has been discovered, lets connect. + try: + self.db = mythtv.MythDB(**dbconf) + except MythError, e: + print e.message + mc.ShowDialogNotification("Failed to connect to the MythTV Backend") + else: + self.be = mythtv.MythBE(db=self.db) + + """ + GetRecordings - Pulls all of the recordings out of the backend. + + This function also creates some dictionarys and lists of information + that is used throughout the app for different functions. + """ + def GetRecordings(self): + self.titles = [] + self.banners = {} + self.series = {} + self.shows = {} + + self.log("def(GetRecordings)") + + self.recs = self.be.getRecordings() - shows[title].append(single) + x=0 + for recording in self.recs: + if recording.title not in self.titles: + self.titles.append(str(recording.title)) + self.banners[str(recording.title)] = self.GetRecordingArtwork(str(recording.title)) + self.series[str(recording.title)] = self.GetRecordingSeriesID(str(recording.title)) + self.shows[str(recording.title)] = [] + + single = [str(recording.title), str(recording.subtitle), str(recording.description), str(recording.chanid), str(recording.airdate), str(recording.starttime), str(recording.endtime), x] + self.shows[str(recording.title)].append(single) + x = x + 1 + + self.titles.sort() - titles.sort() + items = mc.ListItems() + for title in self.titles: + item = mc.ListItem( mc.ListItem.MEDIA_UNKNOWN ) + item.SetLabel(str(title)) + item.SetThumbnail(self.banners[title]) + item.SetProperty("videos", str(len(self.shows[title]))) + item.SetProperty("seriesid", str(self.series[title])) + items.append(item) + + mc.GetWindow(14001).GetList(1030).SetItems(items) + + + def GetRecordingArtwork(self, title): + sg = mc.Http() + sg.SetUserAgent('MythBoxee v4.0.beta') + html = sg.Get("http://www.thetvdb.com/api/GetSeries.php?seriesname=" + str(title.replace(" ", "%20"))) + banners = re.compile("(.*?)").findall(html) + + try: + artwork = "http://www.thetvdb.com/banners/" + banners[0] + except: + artwork = "mb_artwork_error.png" + + self.log("def(GetRecordingArtwork): " + str(artwork)) + + return artwork + + def GetRecordingSeriesID(self, title): + sg = mc.Http() + sg.SetUserAgent(self.userAgent) + html = sg.Get("http://www.thetvdb.com/api/GetSeries.php?seriesname=" + title.replace(" ", "%20")) + series = re.compile("(.*?)").findall(html) + + try: + seriesid = series[0] + except: + seriesid = 00000 + + self.log("def(GetRecordingSeriesID): title[" + title + "] - seriesid[" + str(seriesid) + "]") + + return seriesid + + + """ + DisplayShow + """ + def DisplayShow(self): + recordingList = mc.GetWindow(14001).GetList(1030) + item = recordingList.GetItem(recordingList.GetFocusedItem()) + title = item.GetLabel() + + # Save the Latest Show Title to what was clicked + # this way the show window has a way to load the data. + self.config.SetValue("LatestShowTitle", title) + self.config.SetValue("LatestShowID", item.GetProperty("seriesid")) + + self.log("def(DisplaySingleShow): Title[" + title + "]") + + # Show the Single Show Window + mc.ActivateWindow(14002) + + itemList = mc.ListItems() + itemList.append(item) + + mc.GetWindow(14002).GetList(2070).SetItems(itemList) + + + """ + LoadShow - items = mc.ListItems() - for title in titles: + Launch function to gather and setup the recordings for a single show. + """ + def LoadShow(self): + title = self.config.GetValue("LatestShowTitle") + seriesid = self.config.GetValue("LatestShowID") + + self.log("def(LoadSingleShow): Title[" + title + "]") + + self.SetSortableOptions() + self.SetSeriesDetails(title, seriesid) + self.LoadShowRecordings(title) + + + """ + LoadShowRecordings + + Determine which show is being displayed and find all the recordings for it. + Then populate the recording list for the singular show for viewer to watch. + """ + def LoadShowRecordings(self, title): + sortBy = self.config.GetValue("SortBy") + sortDir = self.config.GetValue("SortDir") + + episodes = None + + if sortBy == "Original Air Date" and sortDir == "Ascending": + episodes = sorted(self.shows[title], key=itemgetter(4)) + elif sortBy == "Original Air Date" and sortDir == "Descending": + episodes = sorted(self.shows[title], key=itemgetter(4), reverse=True) + elif sortBy == "Recorded Date" and sortDir == "Ascending": + episodes = sorted(self.shows[title], key=itemgetter(5)) + elif sortBy == "Recorded Date" and sortDir == "Descending": + episodes = sorted(self.shows[title], key=itemgetter(5), reverse=True) + elif sortBy == "Title" and sortDir == "Ascending": + episodes = sorted(self.shows[title], key=itemgetter(1)) + elif sortBy == "Title" and sortDir == "Descending": + episodes = sorted(self.shows[title], key=itemgetter(1), reverse=True) + else: + episodes = self.shows[title] + + showitems = mc.ListItems() + for title,subtitle,desc,chanid,airdate,starttime,endtime,ref in episodes: + print title + recording = self.recs[ref] + #showitem = mc.ListItem( mc.ListItem.MEDIA_VIDEO_EPISODE ) + #showitem = mc.ListItem( mc.ListItem.MEDIA_UNKNOWN ) + showitem = mc.ListItem() + showitem.SetLabel(str(recording.subtitle)) + showitem.SetTitle(str(recording.subtitle)) + showitem.SetTVShowTitle(str(recording.title)) + showitem.SetDescription(str(desc)) + showitem.SetProperty("starttime", str(starttime)) + showitem.SetProperty("ref", str(ref)) + + try: + date = str(airdate).split("-") + showitem.SetDate(int(date[0]), int(date[1]), int(date[2])) + except: + showitem.SetDate(2010, 01, 01) + + dbconf = eval(self.config.GetValue("dbconn")) + + #showitem.SetThumbnail("http://192.168.1.210:6544/Myth/GetPreviewImage?ChanId=1050&StartTime=2010-08-05%2021:00:00") + #showitem.SetThumbnail("http://192.168.1.210:6544/Myth/GetPreviewImage?ChanId=" + chanid + "&StartTime=" + starttime.replace("T", "%20")) + showitem.SetThumbnail("http://" + dbconf['DBHostName'] + ":6544/Myth/GetPreviewImage?ChanId=" + chanid + "&StartTime=" + starttime.replace(" ", "%20")) + + #showitem.SetPath("http://" + dbconf['DBHostName'] + ":6544/Myth/GetRecording?ChanId=" + chanid + "&StartTime=" + starttime.replace("T", "%20")) + showitem.SetPath("smb://guest:guest@192.168.1.210/recordings/1050_20100709010000.mpg") + + showitems.append(showitem) + + mc.GetWindow(14002).GetList(2040).SetItems(mc.ListItems()) + mc.GetWindow(14002).GetList(2040).SetItems(showitems) + + + """ + SetShowOptions + + Setup the show options; sort by, sort direction, watched vs unwatched + """ + def SetSortableOptions(self): + sortable = ['Original Air Date', 'Recorded Date', 'Title'] + items = mc.ListItems() + for sorttype in sortable: + item = mc.ListItem( mc.ListItem.MEDIA_UNKNOWN ) + item.SetLabel(sorttype) + items.append(item) + + mc.GetWindow(14002).GetList(2051).SetItems(items) + mc.GetWindow(14002).GetList(2051).SetSelected(1, True) + + sortableby = ['Ascending', 'Descending'] + items = mc.ListItems() + for sorttype in sortableby: + item = mc.ListItem( mc.ListItem.MEDIA_UNKNOWN ) + item.SetLabel(sorttype) + items.append(item) + + mc.GetWindow(14002).GetList(2061).SetItems(items) + mc.GetWindow(14002).GetList(2061).SetSelected(1, True) + + + def SetSeriesDetails(self, title, seriesid): + sg = mc.Http() + sg.SetUserAgent(self.userAgent) + html = sg.Get("http://thetvdb.com/api/" + self.tvdb_apikey + "/series/" + seriesid + "/") + overview = re.compile("(.*?)").findall(html) + poster = re.compile("(.*?)").findall(html) + items = mc.ListItems() item = mc.ListItem( mc.ListItem.MEDIA_UNKNOWN ) item.SetLabel(title) - item.SetThumbnail(idbanners[title][1]) - item.SetProperty("seriesid", idbanners[title][0]) - items.append(item) - - mc.GetWindow(14000).GetList(13).SetItems(items) - - -def LoadSingleShow(): - config = mc.GetApp().GetLocalConfig() - ilist = mc.GetActiveWindow().GetList(13) - item = ilist.GetItem(ilist.GetFocusedItem()) - name = item.GetLabel() - config.SetValue("seriesid", item.GetProperty("seriesid")) - config.SetValue("show", name) - mc.ActivateWindow(14001) - - SetSortables() - GetSetSeriesDetails(name, item.GetProperty("seriesid")) - LoadSeriesEpisodes(name) - - -def SetSortables(): - config.SetValue("SortBy", "Recorded Date") - config.SetValue("SortDir", "Descending") - sortable = ['Original Air Date', 'Recorded Date', 'Title'] - items = mc.ListItems() - for sorttype in sortable: - item = mc.ListItem( mc.ListItem.MEDIA_UNKNOWN ) - item.SetLabel(sorttype) - items.append(item) - - mc.GetActiveWindow().GetList(2014).SetItems(items) - mc.GetActiveWindow().GetList(2014).SetSelected(1, True) - - sortableby = ['Ascending', 'Descending'] - items = mc.ListItems() - for sorttype in sortableby: - item = mc.ListItem( mc.ListItem.MEDIA_UNKNOWN ) - item.SetLabel(sorttype) - items.append(item) + item.SetTitle(title) + try: + item.SetDescription(overview[0]) + item.SetProperty("description", overview[0]) + except: + item.SetDescription("No Description") + item.SetProperty("description", "No Description") - mc.GetActiveWindow().GetList(2015).SetItems(items) - mc.GetActiveWindow().GetList(2015).SetSelected(1, True) - - -def ShowEpisodeDetails(): - print "ShowEpisodeDetails" - - -def SortBySeriesEpisodes(): - sortByItems = sortByItemNumber = mc.GetWindow(14001).GetList(2014).GetSelected() - sortDirectionItems = sortDirectionItemNumber = mc.GetWindow(14001).GetList(2015).GetSelected() - - mc.GetActiveWindow().GetList(2014).UnselectAll() - mc.GetActiveWindow().GetList(2014).SetSelected(mc.GetActiveWindow().GetList(2014).GetFocusedItem(), True) - - config.SetValue("SortBy", mc.GetActiveWindow().GetList(2014).GetItem(mc.GetActiveWindow().GetList(2014).GetFocusedItem()).GetLabel()) - - LoadSeriesEpisodes(config.GetValue("name")) - - -def SortDirSeriesEpisodes(): - sortByItems = sortByItemNumber = mc.GetWindow(14001).GetList(2014).GetSelected() - - mc.GetActiveWindow().GetList(2015).UnselectAll() - mc.GetActiveWindow().GetList(2015).SetSelected(mc.GetActiveWindow().GetList(2015).GetFocusedItem(), True) - - config.SetValue("SortDir", mc.GetActiveWindow().GetList(2015).GetItem(mc.GetActiveWindow().GetList(2015).GetFocusedItem()).GetLabel()) - - LoadSeriesEpisodes(config.GetValue("name")) - -def GetSeriesIDBanner(name): - sg = mc.Http() - sg.SetUserAgent('MythBoxee v3.0.beta') - html = sg.Get("http://www.thetvdb.com/api/GetSeries.php?seriesname=" + name.replace(" ", "%20")) - series = re.compile("(.*?)").findall(html) - banners = re.compile("(.*?)").findall(html) - show = [] - if series: - show.append(series[0]) - show.append("http://www.thetvdb.com/banners/" + banners[0]) - else: - show.append("00000") - show.append("http://192.168.1.210/") - return show - - -def GetSetSeriesDetails(name, seriesid): - sg = mc.Http() - sg.SetUserAgent('MythBoxee v3.0.beta') - html = sg.Get("http://thetvdb.com/api/6BEAB4CB5157AAE0/series/" + seriesid + "/") - overview = re.compile("(.*?)").findall(html) - poster = re.compile("(.*?)").findall(html) - items = mc.ListItems() - item = mc.ListItem( mc.ListItem.MEDIA_UNKNOWN ) - item.SetLabel(name) - item.SetTitle(name) - if overview: - item.SetDescription(overview[0]) - item.SetProperty("description", overview[0]) - item.SetThumbnail("http://www.thetvdb.com/banners/" + poster[0]) - items.append(item) - - mc.GetWindow(14001).GetList(21).SetItems(items) - - -def LoadSeriesEpisodes(name): - config = mc.GetApp().GetLocalConfig() - config.SetValue("name", name) - showitems = mc.ListItems() - - sortBy = config.GetValue("SortBy") - sortDir = config.GetValue("SortDir") - - print shows[name] - - if sortBy == "Original Air Date" and sortDir == "Ascending": - episodes = sorted(shows[name], key=itemgetter(4)) - elif sortBy == "Original Air Date" and sortDir == "Descending": - episodes = sorted(shows[name], key=itemgetter(4), reverse=True) - elif sortBy == "Recorded Date" and sortDir == "Ascending": - episodes = sorted(shows[name], key=itemgetter(5)) - elif sortBy == "Recorded Date" and sortDir == "Descending": - episodes = sorted(shows[name], key=itemgetter(5), reverse=True) - elif sortBy == "Title" and sortDir == "Ascending": - episodes = sorted(shows[name], key=itemgetter(1)) - elif sortBy == "Title" and sortDir == "Descending": - episodes = sorted(shows[name], key=itemgetter(1), reverse=True) - else: - episodes = shows[name] - - for title,subtitle,desc,chanid,airdate,starttime,endtime in episodes: - showitem = mc.ListItem( mc.ListItem.MEDIA_VIDEO_EPISODE ) - showitem.SetLabel(subtitle) - showitem.SetTitle(subtitle) - showitem.SetTVShowTitle(name) - showitem.SetDescription(desc) - date = airdate.split("-") - showitem.SetProperty("starttime", starttime) - showitem.SetDate(int(date[0]), int(date[1]), int(date[2])) - showitem.SetThumbnail("http://" + config.GetValue("server") + ":6544/Myth/GetPreviewImage?ChanId=" + chanid + "&StartTime=" + starttime.replace("T", "%20")) - showitem.SetPath("http://" + config.GetValue("server") + ":6544/Myth/GetRecording?ChanId=" + chanid + "&StartTime=" + starttime.replace("T", "%20")) - showitems.append(showitem) - - mc.GetActiveWindow().GetList(2013).SetItems(showitems) - - - -def GetServer(): - config = mc.GetApp().GetLocalConfig() - server = config.GetValue("server") - response = mc.ShowDialogKeyboard("Enter IP Address of MythTV Backend Server", server, False) - url = "http://" + response + ":6544/Myth/GetServDesc" - if VerifyServer(url) == True: - config.SetValue("server", response) - -def VerifyServer(url): - config = mc.GetApp().GetLocalConfig() - http = mc.Http() - data = http.Get(url) - if http.GetHttpResponseCode() == 200: - config.SetValue("verified", "1") - return True - else: - return False - - + try: + item.SetThumbnail("http://www.thetvdb.com/banners/" + poster[0]) + except: + item.SetThumbnail("mb_poster_error.png") + + items.append(item) + + mc.GetWindow(14002).GetList(2070).SetItems(items) + def PlayRecording(self): + self.log("def(PlayRecording): ") + + sl = mc.GetWindow(14002).GetList(2040) + item = sl.GetItem(sl.GetFocusedItem()) + ref = item.GetProperty("ref") + + file = self.recs[int(ref)].open('r', self.db) + mc.ShowDialogNotification(item.GetProperty("ref")) + mc.ShowDialogNotification("Playing: " + item.GetLabel()) + def log(self, message): + if self.logLevel >= 2: + mc.ShowDialogNotification(message) + + if self.logLevel >= 1: + mc.LogInfo(">>> MythBoxee (" + self.version + ")\: " + message) + print ">>> MythBoxee (" + self.version + ")\: " + message diff --git a/mythboxy.py b/mythboxy.py index 1346ef0..bbd63c6 100644 --- a/mythboxy.py +++ b/mythboxy.py @@ -1,21 +1,329 @@ import mc +import re +import mythtv +from mythtv import MythError +from operator import itemgetter, attrgetter + + +class MythBoxee: + be = None + db = None + + def DiscoverBackend(): + config = mc.GetApp().GetLocalConfig() + mc.ShowDialogNotification("DiscoverBackend") + + pin = config.GetValue("pin") + dbconn = config.GetValue("dbconn") -def CreateConnection(): - try: - pin = mc.GetApp().GetLocalConfig().GetValue("pin") if not pin: pin = 0000 + + try: + self.db = mythtv.MythDB(SecurityPin=pin) + except Exception, e: + mc.ShowDialogNotification(e.message) + if e.ename == 'DB_CREDENTIALS' and count < 3: + mc.ActivateWindow(14002) + mc.GetWindow(14002).GetControl(6020).SetVisible(False) + mc.GetWindow(14002).GetControl(6010).SetVisible(True) + mc.GetWindow(14002).GetControl(6011).SetFocus() + elif e.ename == 'DB_CONNECTION' or e.ename == 'DB_CREDENTIALS' and count > 3: + mc.ActivateWindow(14002) + mc.GetWindow(14002).GetControl(6010).SetVisible(False) + mc.GetWindow(14002).GetControl(6020).SetVisible(True) + mc.GetWindow(14002).GetControl(6021).SetFocus() + return False + else: + mc.ShowDialogNotification(str(self.db.dbconn)) + config.SetValue("dbconn", str(self.db.dbconn)) + return True + + def __init__(self): + config = mc.GetApp().GetLocalConfig() + # If dbconn isn't set, we'll assume we haven't found the backend. + if not config.GetValue("dbconn"): + discoverBackend = False + while discoverBackend is False: + discoverBackend = self.DiscoverBackend() + + # Parse our DB info + dbconn = config.GetValue("dbconn") + dbconf = eval(dbconn) + + # Now that the backend has been discovered, lets connect. + try: + self.db = mythtv.MythDB(**dbconf) + except MythError, e: + print e.message + mc.ShowDialogNotification("Failed to connect to the MythTV Backend") + else: + self.be = mythtv.MythBE(db=self.db) + mc.ActivateWindow(14010) + + + +mbbe = None +mbdb = None + +config = mc.GetApp().GetLocalConfig() + +titles = [] +recordings = [] +idbanners = {} +shows = {} + + +def DiscoverBackend1(): + mc.ShowDialogNotification("DiscoverBackend") + + pin = config.GetValue("pin") + dbconn = config.GetValue("dbconn") + + if not pin: + pin = 0000 + + try: db = mythtv.MythDB(SecurityPin=pin) except Exception, e: - if e.ename == 'DB_CREDENTIALS': - mc.ShowDialogNotification("Unable to connect, try to manually set the security pin.") - mc.ActivateWindow(14006) - mc.GetWindow(14006).GetControl(6010).SetVisible(True) - mc.GetWindow(14006).GetControl(6020).SetVisible(False) - mc.GetWindow(14006).GetControl(6011).SetFocus() - elif e.ename == 'DB_CONNECTION': - mc.ActivateWindow(14006) - mc.GetWindow(14006).GetControl(6010).SetVisible(False) - mc.GetWindow(14006).GetControl(6020).SetVisible(True) - mc.GetWindow(14006).GetControl(6021).SetFocus() + mc.ShowDialogNotification(e.message) + if e.ename == 'DB_CREDENTIALS' and count < 3: + mc.ActivateWindow(14002) + mc.GetWindow(14002).GetControl(6020).SetVisible(False) + mc.GetWindow(14002).GetControl(6010).SetVisible(True) + mc.GetWindow(14002).GetControl(6011).SetFocus() + elif e.ename == 'DB_CONNECTION' or e.ename == 'DB_CREDENTIALS' and count > 3: + mc.ActivateWindow(14002) + mc.GetWindow(14002).GetControl(6010).SetVisible(False) + mc.GetWindow(14002).GetControl(6020).SetVisible(True) + mc.GetWindow(14002).GetControl(6021).SetFocus() + return False + else: + mc.ShowDialogNotification(str(db.dbconn)) + config.SetValue("dbconn", str(db.dbconn)) + return True + + +def Launch(): + # If dbconn isn't set, we'll assume we haven't found the backend. + if not config.GetValue("dbconn"): + discoverBackend = False + while discoverBackend is False: + discoverBackend = DiscoverBackend() + + # Parse our DB info + dbconn = config.GetValue("dbconn") + dbconf = eval(dbconn) + + # Now that the backend has been discovered, lets connect. + try: + mbdb = mythtv.MythDB(**dbconf) + except MythError, e: + print e.message + mc.ShowDialogNotification("Failed to connect to the MythTV Backend") + else: + mbbe = mythtv.MythBE(db=mbdb) + mc.ActivateWindow(14010) + + +def LoadShows(): + del titles[:] + del recordings[:] + idbanners.clear() + shows.clear() + + config = mc.GetApp().GetLocalConfig() + sg = mc.Http() + html = sg.Get("http://" + config.GetValue("server") + ":6544/Myth/GetRecorded") + results = re.compile("(.*?)").findall(html) + for title,subtitle,endtime,airdate,starttime,desc,chanid in results: + if title not in titles: + titles.append(title) + idbanners[title] = GetSeriesIDBanner(title) + shows[title] = [] + + single = [title,subtitle,desc,chanid,airdate,starttime,endtime] + recordings.append(single) + + shows[title].append(single) + + titles.sort() + + items = mc.ListItems() + for title in titles: + item = mc.ListItem( mc.ListItem.MEDIA_UNKNOWN ) + item.SetLabel(title) + item.SetThumbnail(idbanners[title][1]) + item.SetProperty("seriesid", idbanners[title][0]) + items.append(item) + + mc.GetWindow(14000).GetList(13).SetItems(items) + + +def LoadSingleShow(): + config = mc.GetApp().GetLocalConfig() + ilist = mc.GetActiveWindow().GetList(13) + item = ilist.GetItem(ilist.GetFocusedItem()) + name = item.GetLabel() + config.SetValue("seriesid", item.GetProperty("seriesid")) + config.SetValue("show", name) + mc.ActivateWindow(14001) + + SetSortables() + GetSetSeriesDetails(name, item.GetProperty("seriesid")) + LoadSeriesEpisodes(name) + + +def SetSortables(): + config.SetValue("SortBy", "Recorded Date") + config.SetValue("SortDir", "Descending") + sortable = ['Original Air Date', 'Recorded Date', 'Title'] + items = mc.ListItems() + for sorttype in sortable: + item = mc.ListItem( mc.ListItem.MEDIA_UNKNOWN ) + item.SetLabel(sorttype) + items.append(item) + + mc.GetActiveWindow().GetList(2014).SetItems(items) + mc.GetActiveWindow().GetList(2014).SetSelected(1, True) + + sortableby = ['Ascending', 'Descending'] + items = mc.ListItems() + for sorttype in sortableby: + item = mc.ListItem( mc.ListItem.MEDIA_UNKNOWN ) + item.SetLabel(sorttype) + items.append(item) + + mc.GetActiveWindow().GetList(2015).SetItems(items) + mc.GetActiveWindow().GetList(2015).SetSelected(1, True) + + +def ShowEpisodeDetails(): + print "ShowEpisodeDetails" + + +def SortBySeriesEpisodes(): + sortByItems = sortByItemNumber = mc.GetWindow(14001).GetList(2014).GetSelected() + sortDirectionItems = sortDirectionItemNumber = mc.GetWindow(14001).GetList(2015).GetSelected() + + mc.GetActiveWindow().GetList(2014).UnselectAll() + mc.GetActiveWindow().GetList(2014).SetSelected(mc.GetActiveWindow().GetList(2014).GetFocusedItem(), True) + + config.SetValue("SortBy", mc.GetActiveWindow().GetList(2014).GetItem(mc.GetActiveWindow().GetList(2014).GetFocusedItem()).GetLabel()) + + LoadSeriesEpisodes(config.GetValue("name")) + + +def SortDirSeriesEpisodes(): + sortByItems = sortByItemNumber = mc.GetWindow(14001).GetList(2014).GetSelected() + + mc.GetActiveWindow().GetList(2015).UnselectAll() + mc.GetActiveWindow().GetList(2015).SetSelected(mc.GetActiveWindow().GetList(2015).GetFocusedItem(), True) + + config.SetValue("SortDir", mc.GetActiveWindow().GetList(2015).GetItem(mc.GetActiveWindow().GetList(2015).GetFocusedItem()).GetLabel()) + + LoadSeriesEpisodes(config.GetValue("name")) + +def GetSeriesIDBanner(name): + sg = mc.Http() + sg.SetUserAgent('MythBoxee v3.0.beta') + html = sg.Get("http://www.thetvdb.com/api/GetSeries.php?seriesname=" + name.replace(" ", "%20")) + series = re.compile("(.*?)").findall(html) + banners = re.compile("(.*?)").findall(html) + show = [] + if series: + show.append(series[0]) + show.append("http://www.thetvdb.com/banners/" + banners[0]) + else: + show.append("00000") + show.append("http://192.168.1.210/") + return show + + +def GetSetSeriesDetails(name, seriesid): + sg = mc.Http() + sg.SetUserAgent('MythBoxee v3.0.beta') + html = sg.Get("http://thetvdb.com/api/6BEAB4CB5157AAE0/series/" + seriesid + "/") + overview = re.compile("(.*?)").findall(html) + poster = re.compile("(.*?)").findall(html) + items = mc.ListItems() + item = mc.ListItem( mc.ListItem.MEDIA_UNKNOWN ) + item.SetLabel(name) + item.SetTitle(name) + if overview: + item.SetDescription(overview[0]) + item.SetProperty("description", overview[0]) + item.SetThumbnail("http://www.thetvdb.com/banners/" + poster[0]) + items.append(item) + + mc.GetWindow(14001).GetList(21).SetItems(items) + + +def LoadSeriesEpisodes(name): + config = mc.GetApp().GetLocalConfig() + config.SetValue("name", name) + showitems = mc.ListItems() + + sortBy = config.GetValue("SortBy") + sortDir = config.GetValue("SortDir") + + print shows[name] + + if sortBy == "Original Air Date" and sortDir == "Ascending": + episodes = sorted(shows[name], key=itemgetter(4)) + elif sortBy == "Original Air Date" and sortDir == "Descending": + episodes = sorted(shows[name], key=itemgetter(4), reverse=True) + elif sortBy == "Recorded Date" and sortDir == "Ascending": + episodes = sorted(shows[name], key=itemgetter(5)) + elif sortBy == "Recorded Date" and sortDir == "Descending": + episodes = sorted(shows[name], key=itemgetter(5), reverse=True) + elif sortBy == "Title" and sortDir == "Ascending": + episodes = sorted(shows[name], key=itemgetter(1)) + elif sortBy == "Title" and sortDir == "Descending": + episodes = sorted(shows[name], key=itemgetter(1), reverse=True) + else: + episodes = shows[name] + + for title,subtitle,desc,chanid,airdate,starttime,endtime in episodes: + showitem = mc.ListItem( mc.ListItem.MEDIA_VIDEO_EPISODE ) + showitem.SetLabel(subtitle) + showitem.SetTitle(subtitle) + showitem.SetTVShowTitle(name) + showitem.SetDescription(desc) + date = airdate.split("-") + showitem.SetProperty("starttime", starttime) + showitem.SetDate(int(date[0]), int(date[1]), int(date[2])) + showitem.SetThumbnail("http://" + config.GetValue("server") + ":6544/Myth/GetPreviewImage?ChanId=" + chanid + "&StartTime=" + starttime.replace("T", "%20")) + showitem.SetPath("http://" + config.GetValue("server") + ":6544/Myth/GetRecording?ChanId=" + chanid + "&StartTime=" + starttime.replace("T", "%20")) + showitems.append(showitem) + + mc.GetActiveWindow().GetList(2013).SetItems(showitems) + + + +def GetServer(): + config = mc.GetApp().GetLocalConfig() + server = config.GetValue("server") + response = mc.ShowDialogKeyboard("Enter IP Address of MythTV Backend Server", server, False) + url = "http://" + response + ":6544/Myth/GetServDesc" + if VerifyServer(url) == True: + config.SetValue("server", response) + +def VerifyServer(url): + config = mc.GetApp().GetLocalConfig() + http = mc.Http() + data = http.Get(url) + if http.GetHttpResponseCode() == 200: + config.SetValue("verified", "1") + return True + else: + return False + + + + + + + + diff --git a/mythtv/.svn/all-wcprops b/mythtv/.svn/all-wcprops index 4b52ac5..edc90f4 100644 --- a/mythtv/.svn/all-wcprops +++ b/mythtv/.svn/all-wcprops @@ -1,35 +1,35 @@ K 25 svn:wc:ra_dav:version-url -V 67 -/svn/!svn/ver/24521/tags/release-0-23/mythtv/bindings/python/MythTV +V 69 +/svn/!svn/ver/25397/tags/release-0-23-1/mythtv/bindings/python/MythTV END MythStatic.py K 25 svn:wc:ra_dav:version-url -V 81 -/svn/!svn/ver/24521/tags/release-0-23/mythtv/bindings/python/MythTV/MythStatic.py +V 83 +/svn/!svn/ver/25397/tags/release-0-23-1/mythtv/bindings/python/MythTV/MythStatic.py END MythData.py K 25 svn:wc:ra_dav:version-url -V 79 -/svn/!svn/ver/24521/tags/release-0-23/mythtv/bindings/python/MythTV/MythData.py +V 81 +/svn/!svn/ver/25397/tags/release-0-23-1/mythtv/bindings/python/MythTV/MythData.py END MythBase.py K 25 svn:wc:ra_dav:version-url -V 79 -/svn/!svn/ver/24521/tags/release-0-23/mythtv/bindings/python/MythTV/MythBase.py +V 81 +/svn/!svn/ver/25397/tags/release-0-23-1/mythtv/bindings/python/MythTV/MythBase.py END MythFunc.py K 25 svn:wc:ra_dav:version-url -V 79 -/svn/!svn/ver/24521/tags/release-0-23/mythtv/bindings/python/MythTV/MythFunc.py +V 81 +/svn/!svn/ver/25397/tags/release-0-23-1/mythtv/bindings/python/MythTV/MythFunc.py END __init__.py K 25 svn:wc:ra_dav:version-url -V 79 -/svn/!svn/ver/24521/tags/release-0-23/mythtv/bindings/python/MythTV/__init__.py +V 81 +/svn/!svn/ver/25397/tags/release-0-23-1/mythtv/bindings/python/MythTV/__init__.py END diff --git a/mythtv/.svn/entries b/mythtv/.svn/entries index 5048e1f..8690d9b 100644 --- a/mythtv/.svn/entries +++ b/mythtv/.svn/entries @@ -1,15 +1,15 @@ 10 dir -25361 -http://svn.mythtv.org/svn/tags/release-0-23/mythtv/bindings/python/MythTV +25726 +http://svn.mythtv.org/svn/tags/release-0-23-1/mythtv/bindings/python/MythTV http://svn.mythtv.org/svn -2010-05-05T00:45:58.150174Z -24420 -wagnerrp +2010-07-22T02:21:00.112292Z +25396 +gigem @@ -32,11 +32,11 @@ file -2010-07-16T22:31:04.000000Z -c362740b39721bf47554a27b8dcb7255 -2010-01-30T01:25:12.140142Z -23365 -wagnerrp +2010-08-18T01:49:43.000000Z +2645324c83c882ef4d85662fe2c3c9d7 +2010-07-22T02:21:00.112292Z +25396 +gigem has-props @@ -58,7 +58,7 @@ has-props -233 +261 MythData.py file @@ -66,10 +66,10 @@ file -2010-07-16T22:31:04.000000Z -ea13afe13bb398e11e7895be6654e06e -2010-04-21T04:46:55.439637Z -24220 +2010-08-18T01:49:43.000000Z +f973584ab4647d8e5f7a0eb16d51bf16 +2010-06-07T00:03:02.104550Z +25014 wagnerrp has-props @@ -92,7 +92,7 @@ has-props -57313 +57441 ttvdb dir @@ -103,7 +103,7 @@ file -2010-07-16T22:31:04.000000Z +2010-08-18T01:49:43.000000Z 4e2f2010af14b04d6ebb3ead0606faa1 2010-05-05T00:45:58.150174Z 24420 @@ -137,10 +137,10 @@ file -2010-07-16T22:31:04.000000Z -80c438b974b0dadaed423f9308c04912 -2010-03-31T19:07:44.868318Z -23878 +2010-08-18T01:49:43.000000Z +3f6f4d43c4dc47e8c6ab2ab747d8042f +2010-05-23T19:52:40.992320Z +24818 wagnerrp has-props @@ -163,7 +163,7 @@ has-props -41463 +41550 tmdb dir @@ -174,10 +174,10 @@ file -2010-07-16T22:31:04.000000Z -5c15a4df6a8f262fb7bdd3f91b32119a -2010-05-03T05:03:57.011023Z -24346 +2010-08-18T01:49:43.000000Z +fc69a94c478228940c6be7eee98a8903 +2010-06-22T03:35:41.632640Z +25154 wagnerrp has-props @@ -200,5 +200,5 @@ has-props -1464 +1532 diff --git a/mythtv/.svn/text-base/MythData.py.svn-base b/mythtv/.svn/text-base/MythData.py.svn-base index 14b2f34..d1e4ac5 100644 --- a/mythtv/.svn/text-base/MythData.py.svn-base +++ b/mythtv/.svn/text-base/MythData.py.svn-base @@ -175,7 +175,7 @@ class FileTransfer( MythBEConn ): if not self.open: return self.control.backendCommand('QUERY_FILETRANSFER '\ - +BACKEND_SEP.join([str(self.sockno), 'JOIN'])) + +BACKEND_SEP.join([str(self.sockno), 'DONE'])) self.socket.shutdown(1) self.socket.close() self.open = False @@ -526,6 +526,8 @@ class Program( DictData ): if type != 'r': raise MythFileError(MythError.FILE_FAILED_WRITE, self.filename, 'Program () objects cannot be opened for writing') + if not self.filename.startswith('myth://'): + self.filename = 'myth://%s/%s' % (self.hostname, self.filename) return ftopen(self.filename, 'r') class Record( DBDataWrite ): diff --git a/mythtv/.svn/text-base/MythFunc.py.svn-base b/mythtv/.svn/text-base/MythFunc.py.svn-base index 2fe1dff..89b43e2 100644 --- a/mythtv/.svn/text-base/MythFunc.py.svn-base +++ b/mythtv/.svn/text-base/MythFunc.py.svn-base @@ -392,13 +392,16 @@ class MythBE( FileOps ): 'filenames' is a dictionary, where the values are the file sizes. """ def walk(self, host, sg, root, path): - dn, fn, fs = self.getSGList(host, sg, root+path+'/') - res = [list(dn), dict(zip(fn, fs))] + res = self.getSGList(host, sg, root+path+'/') + if res < 0: + return {} + dlist = list(res[0]) + res = [dlist, dict(zip(res[1],res[2]))] if path == '': res = {'/':res} else: res = {path:res} - for d in dn: + for d in dlist: res.update(walk(self, host, sg, root, path+'/'+d)) return res @@ -820,7 +823,7 @@ class MythDB( MythDBBase ): def getGuideData(self, chanid, date): return self.searchGuide(chanid=chanid, - custom=('DATE(starttime)=%s',date)) + custom=(('DATE(starttime)=%s',date),)) def getSetting(self, value, hostname=None): if not hostname: @@ -996,7 +999,7 @@ class MythVideo( MythDBBase ): tpath = sgfold[0][1:]+'/'+sgfile # filter by extension - if tpath.rsplit('.',1)[1].lower() not in extensions: + if tpath.rsplit('.',1)[-1].lower() not in extensions: #print 'skipping: '+tpath continue diff --git a/mythtv/.svn/text-base/MythStatic.py.svn-base b/mythtv/.svn/text-base/MythStatic.py.svn-base index 11f7ea6..b101dc2 100644 --- a/mythtv/.svn/text-base/MythStatic.py.svn-base +++ b/mythtv/.svn/text-base/MythStatic.py.svn-base @@ -4,9 +4,10 @@ Contains any static and global variables for MythTV Python Bindings """ +OWN_VERSION = (0,23,1,0) SCHEMA_VERSION = 1254 MVSCHEMA_VERSION = 1032 NVSCHEMA_VERSION = 1004 -PROTO_VERSION = 56 +PROTO_VERSION = 23056 PROGRAM_FIELDS = 47 BACKEND_SEP = '[]:[]' diff --git a/mythtv/.svn/text-base/__init__.py.svn-base b/mythtv/.svn/text-base/__init__.py.svn-base index d8115ff..e39b5bb 100644 --- a/mythtv/.svn/text-base/__init__.py.svn-base +++ b/mythtv/.svn/text-base/__init__.py.svn-base @@ -36,6 +36,9 @@ if version_info >= (2, 6): # 2.6 or newer else: exec(import25) +__version__ = OWN_VERSION +MythStatic.mysqldb = MySQLdb.__version__ + if __name__ == '__main__': banner = 'MythTV Python interactive shell.' import code diff --git a/mythtv/MythBase.pyc b/mythtv/MythBase.pyc deleted file mode 100755 index a3586d4..0000000 Binary files a/mythtv/MythBase.pyc and /dev/null differ diff --git a/mythtv/MythData.py b/mythtv/MythData.py index 14b2f34..982172a 100755 --- a/mythtv/MythData.py +++ b/mythtv/MythData.py @@ -521,12 +521,12 @@ class Program( DictData ): """Program.getRecorded() -> Recorded object""" return Recorded((self.chanid,self.recstartts), db=self.db) - def open(self, type='r'): + def open(self, type='r', dba=None): """Program.open(type='r') -> file or FileTransfer object""" if type != 'r': raise MythFileError(MythError.FILE_FAILED_WRITE, self.filename, 'Program () objects cannot be opened for writing') - return ftopen(self.filename, 'r') + return ftopen(self.filename, 'r', forceremote=False, nooverwrite=False, db=dba) class Record( DBDataWrite ): """ diff --git a/mythtv/MythData.pyc b/mythtv/MythData.pyc deleted file mode 100755 index fd12f30..0000000 Binary files a/mythtv/MythData.pyc and /dev/null differ diff --git a/mythtv/MythFunc.py b/mythtv/MythFunc.py index 2fe1dff..89b43e2 100755 --- a/mythtv/MythFunc.py +++ b/mythtv/MythFunc.py @@ -392,13 +392,16 @@ class MythBE( FileOps ): 'filenames' is a dictionary, where the values are the file sizes. """ def walk(self, host, sg, root, path): - dn, fn, fs = self.getSGList(host, sg, root+path+'/') - res = [list(dn), dict(zip(fn, fs))] + res = self.getSGList(host, sg, root+path+'/') + if res < 0: + return {} + dlist = list(res[0]) + res = [dlist, dict(zip(res[1],res[2]))] if path == '': res = {'/':res} else: res = {path:res} - for d in dn: + for d in dlist: res.update(walk(self, host, sg, root, path+'/'+d)) return res @@ -820,7 +823,7 @@ class MythDB( MythDBBase ): def getGuideData(self, chanid, date): return self.searchGuide(chanid=chanid, - custom=('DATE(starttime)=%s',date)) + custom=(('DATE(starttime)=%s',date),)) def getSetting(self, value, hostname=None): if not hostname: @@ -996,7 +999,7 @@ class MythVideo( MythDBBase ): tpath = sgfold[0][1:]+'/'+sgfile # filter by extension - if tpath.rsplit('.',1)[1].lower() not in extensions: + if tpath.rsplit('.',1)[-1].lower() not in extensions: #print 'skipping: '+tpath continue diff --git a/mythtv/MythFunc.pyc b/mythtv/MythFunc.pyc deleted file mode 100755 index d92269c..0000000 Binary files a/mythtv/MythFunc.pyc and /dev/null differ diff --git a/mythtv/MythStatic.py b/mythtv/MythStatic.py index 11f7ea6..b101dc2 100755 --- a/mythtv/MythStatic.py +++ b/mythtv/MythStatic.py @@ -4,9 +4,10 @@ Contains any static and global variables for MythTV Python Bindings """ +OWN_VERSION = (0,23,1,0) SCHEMA_VERSION = 1254 MVSCHEMA_VERSION = 1032 NVSCHEMA_VERSION = 1004 -PROTO_VERSION = 56 +PROTO_VERSION = 23056 PROGRAM_FIELDS = 47 BACKEND_SEP = '[]:[]' diff --git a/mythtv/MythStatic.pyc b/mythtv/MythStatic.pyc deleted file mode 100755 index c77b5a3..0000000 Binary files a/mythtv/MythStatic.pyc and /dev/null differ diff --git a/mythtv/__init__.py b/mythtv/__init__.py index d8115ff..06e3475 100755 --- a/mythtv/__init__.py +++ b/mythtv/__init__.py @@ -36,6 +36,9 @@ if version_info >= (2, 6): # 2.6 or newer else: exec(import25) +__version__ = OWN_VERSION +#MythStatic.mysqldb = MySQLdb.__version__ + if __name__ == '__main__': banner = 'MythTV Python interactive shell.' import code diff --git a/mythtv/__init__.pyc b/mythtv/__init__.pyc deleted file mode 100755 index 8abb1d0..0000000 Binary files a/mythtv/__init__.pyc and /dev/null differ diff --git a/mythtv/tmdb/.svn/all-wcprops b/mythtv/tmdb/.svn/all-wcprops index b66c7a7..c7e3a95 100644 --- a/mythtv/tmdb/.svn/all-wcprops +++ b/mythtv/tmdb/.svn/all-wcprops @@ -1,29 +1,29 @@ K 25 svn:wc:ra_dav:version-url -V 72 -/svn/!svn/ver/24521/tags/release-0-23/mythtv/bindings/python/MythTV/tmdb +V 74 +/svn/!svn/ver/25397/tags/release-0-23-1/mythtv/bindings/python/MythTV/tmdb END __init__.py K 25 svn:wc:ra_dav:version-url -V 84 -/svn/!svn/ver/24521/tags/release-0-23/mythtv/bindings/python/MythTV/tmdb/__init__.py +V 86 +/svn/!svn/ver/25397/tags/release-0-23-1/mythtv/bindings/python/MythTV/tmdb/__init__.py END tmdb_api.py K 25 svn:wc:ra_dav:version-url -V 84 -/svn/!svn/ver/24521/tags/release-0-23/mythtv/bindings/python/MythTV/tmdb/tmdb_api.py +V 86 +/svn/!svn/ver/25397/tags/release-0-23-1/mythtv/bindings/python/MythTV/tmdb/tmdb_api.py END tmdb_ui.py K 25 svn:wc:ra_dav:version-url -V 83 -/svn/!svn/ver/24521/tags/release-0-23/mythtv/bindings/python/MythTV/tmdb/tmdb_ui.py +V 85 +/svn/!svn/ver/25397/tags/release-0-23-1/mythtv/bindings/python/MythTV/tmdb/tmdb_ui.py END tmdb_exceptions.py K 25 svn:wc:ra_dav:version-url -V 91 -/svn/!svn/ver/24521/tags/release-0-23/mythtv/bindings/python/MythTV/tmdb/tmdb_exceptions.py +V 93 +/svn/!svn/ver/25397/tags/release-0-23-1/mythtv/bindings/python/MythTV/tmdb/tmdb_exceptions.py END diff --git a/mythtv/tmdb/.svn/entries b/mythtv/tmdb/.svn/entries index dc72375..54fb221 100644 --- a/mythtv/tmdb/.svn/entries +++ b/mythtv/tmdb/.svn/entries @@ -1,8 +1,8 @@ 10 dir -25361 -http://svn.mythtv.org/svn/tags/release-0-23/mythtv/bindings/python/MythTV/tmdb +25726 +http://svn.mythtv.org/svn/tags/release-0-23-1/mythtv/bindings/python/MythTV/tmdb http://svn.mythtv.org/svn @@ -32,7 +32,7 @@ file -2010-07-16T22:31:04.000000Z +2010-08-18T01:49:43.000000Z d41d8cd98f00b204e9800998ecf8427e 2010-01-29T01:39:37.380922Z 23354 @@ -66,7 +66,7 @@ file -2010-07-16T22:31:04.000000Z +2010-08-18T01:49:43.000000Z 80afb5dbadea4de7d5ed5d0e1fa956bf 2010-04-29T22:38:50.878564Z 24305 @@ -100,7 +100,7 @@ file -2010-07-16T22:31:04.000000Z +2010-08-18T01:49:43.000000Z 89285d26d830a7a0fc71f92e4f60f815 2010-04-12T22:36:01.726283Z 24097 @@ -134,7 +134,7 @@ file -2010-07-16T22:31:04.000000Z +2010-08-18T01:49:43.000000Z d6d57ccfa5baa9fd79eb04eb3b6e7aff 2010-01-29T01:39:37.380922Z 23354 diff --git a/mythtv/ttvdb/.svn/all-wcprops b/mythtv/ttvdb/.svn/all-wcprops index 9ea226f..8302346 100644 --- a/mythtv/ttvdb/.svn/all-wcprops +++ b/mythtv/ttvdb/.svn/all-wcprops @@ -1,41 +1,41 @@ K 25 svn:wc:ra_dav:version-url -V 73 -/svn/!svn/ver/24521/tags/release-0-23/mythtv/bindings/python/MythTV/ttvdb +V 75 +/svn/!svn/ver/25397/tags/release-0-23-1/mythtv/bindings/python/MythTV/ttvdb END tvdb_api.py K 25 svn:wc:ra_dav:version-url -V 85 -/svn/!svn/ver/24521/tags/release-0-23/mythtv/bindings/python/MythTV/ttvdb/tvdb_api.py +V 87 +/svn/!svn/ver/25397/tags/release-0-23-1/mythtv/bindings/python/MythTV/ttvdb/tvdb_api.py END ttvdb-example.conf K 25 svn:wc:ra_dav:version-url -V 92 -/svn/!svn/ver/24521/tags/release-0-23/mythtv/bindings/python/MythTV/ttvdb/ttvdb-example.conf +V 94 +/svn/!svn/ver/25397/tags/release-0-23-1/mythtv/bindings/python/MythTV/ttvdb/ttvdb-example.conf END tvdb_ui.py K 25 svn:wc:ra_dav:version-url -V 84 -/svn/!svn/ver/24521/tags/release-0-23/mythtv/bindings/python/MythTV/ttvdb/tvdb_ui.py +V 86 +/svn/!svn/ver/25397/tags/release-0-23-1/mythtv/bindings/python/MythTV/ttvdb/tvdb_ui.py END __init__.py K 25 svn:wc:ra_dav:version-url -V 85 -/svn/!svn/ver/24521/tags/release-0-23/mythtv/bindings/python/MythTV/ttvdb/__init__.py +V 87 +/svn/!svn/ver/25397/tags/release-0-23-1/mythtv/bindings/python/MythTV/ttvdb/__init__.py END tvdb_exceptions.py K 25 svn:wc:ra_dav:version-url -V 92 -/svn/!svn/ver/24521/tags/release-0-23/mythtv/bindings/python/MythTV/ttvdb/tvdb_exceptions.py +V 94 +/svn/!svn/ver/25397/tags/release-0-23-1/mythtv/bindings/python/MythTV/ttvdb/tvdb_exceptions.py END cache.py K 25 svn:wc:ra_dav:version-url -V 82 -/svn/!svn/ver/24521/tags/release-0-23/mythtv/bindings/python/MythTV/ttvdb/cache.py +V 84 +/svn/!svn/ver/25397/tags/release-0-23-1/mythtv/bindings/python/MythTV/ttvdb/cache.py END diff --git a/mythtv/ttvdb/.svn/entries b/mythtv/ttvdb/.svn/entries index 50556bb..2afbb0b 100644 --- a/mythtv/ttvdb/.svn/entries +++ b/mythtv/ttvdb/.svn/entries @@ -1,8 +1,8 @@ 10 dir -25361 -http://svn.mythtv.org/svn/tags/release-0-23/mythtv/bindings/python/MythTV/ttvdb +25726 +http://svn.mythtv.org/svn/tags/release-0-23-1/mythtv/bindings/python/MythTV/ttvdb http://svn.mythtv.org/svn @@ -32,7 +32,7 @@ file -2010-07-16T22:31:04.000000Z +2010-08-18T01:49:43.000000Z 554f84950a1f7cc2bab09477c3798f18 2010-01-29T01:39:37.380922Z 23354 @@ -66,7 +66,7 @@ file -2010-07-16T22:31:04.000000Z +2010-08-18T01:49:43.000000Z 8a9a6d831a1892828eb38318b114f0b0 2010-01-29T01:39:37.380922Z 23354 @@ -100,7 +100,7 @@ file -2010-07-16T22:31:04.000000Z +2010-08-18T01:49:43.000000Z 305a22b10a8be8dfef8ff90c399d9a38 2010-01-29T01:39:37.380922Z 23354 @@ -134,7 +134,7 @@ file -2010-07-16T22:31:04.000000Z +2010-08-18T01:49:43.000000Z d41d8cd98f00b204e9800998ecf8427e 2010-01-29T01:39:37.380922Z 23354 @@ -168,7 +168,7 @@ file -2010-07-16T22:31:04.000000Z +2010-08-18T01:49:43.000000Z 90ce82c602de1cc41d603c01563d3bfb 2010-01-29T01:39:37.380922Z 23354 @@ -202,7 +202,7 @@ file -2010-07-16T22:31:04.000000Z +2010-08-18T01:49:43.000000Z e904998a5a3c1e088dc5b00a57947127 2010-01-29T01:39:37.380922Z 23354 diff --git a/patches/MythBase.py.orig b/patches/MythBase.py.orig new file mode 100755 index 0000000..d42c32b --- /dev/null +++ b/patches/MythBase.py.orig @@ -0,0 +1,1868 @@ +# -*- coding: utf-8 -*- + +""" +Provides base classes for accessing MythTV +""" + +from MythStatic import * + +import os, re, socket, sys, locale, weakref +import xml.etree.cElementTree as etree +from datetime import datetime +from time import sleep, time +from urllib import urlopen +from subprocess import Popen +from sys import version_info + +import MySQLdb, MySQLdb.cursors +MySQLdb.__version__ = tuple([v for v in MySQLdb.__version__.split('.')]) + +class DictData( object ): + """ + DictData.__init__(raw) -> DictData object + + Basic class for providing management of data resembling dictionaries. + Must be subclassed for use. + + Subclasses must provide: + field_order + string list of field names + field_type + integer list of field types by: + 0: integer - int(value) + 1: float - float(value) + 2: bool - bool(value) + 3: string - value + 4: date - datetime.fromtimestamp(int(value)) + can be the single string 'Pass', to pass all values untouched + + Data can be accessed as: + obj.field_name + -- or -- + obj['field_name'] + """ + + class DictIterator( object ): + def __init__(self, mode, parent): + # modes = 1:keys, 2:values, 3:items + self.index = 0 + self.mode = mode + self.data = parent + def __iter__(self): return self + def next(self): + self.index += 1 + if self.index > len(self.data.field_order): + raise StopIteration + key = self.data.field_order[self.index-1] + if self.mode == 1: + return key + elif self.mode == 2: + return self.data[key] + elif self.mode == 3: + return (key, self.data[key]) + + logmodule = 'Python DictData' + # class emulation functions + def __getattr__(self,name): + # function for class attribute access of data fields + # accesses real attributes, falls back to data fields, and errors + if name in self.__dict__: + return self.__dict__[name] + elif name in self.field_order: + return self.data[name] + else: + raise AttributeError("'DictData' object has no attribute '%s'"%name) + + def __setattr__(self,name,value): + # function for class attribute access of data fields + # sets value in data fields, falling back to real attributes + if name in self.field_order: + self.data[name] = value + else: + self.__dict__[name] = value + + # container emulation functions + def __getitem__(self,key): + # function for and dist-like access to content + # as a side effect of dual operation, dict data cannot be indexed + # keyed off integer values + if key in self.data: + return self.data[key] + else: + raise KeyError("'DictData' object has no key '%s'" %key) + + def __setitem__(self,key,value): + # function for dist-like access to content + # does not allow setting of data not already in field_order + if key in self.field_order: + self.data[key] = value + else: + raise KeyError("'DictData' does not allow new data") + + def __contains__(self,item): + return bool(item in self.data.keys()) + + def __iter__(self): + return self.DictIterator(2,self) + + def iterkeys(self): + """ + obj.iterkeys() -> an iterator over the keys of obj + ordered by self.field_order + """ + return self.DictIterator(1,self) + + def itervalues(self): + """ + obj.itervalues() -> an iterator over the values of obj + ordered by self.field_order + """ + return self.DictIterator(2,self) + + def iteritems(self): + """ + obj.iteritems() -> an iterator of over the (key,value) pairs of obj + ordered by self.field_order + """ + return self.DictIterator(3,self) + + def keys(self): + """obj.keys() -> list of self.field_order""" + return list(self.iterkeys()) + + def has_key(self,item): + return self.__contains__(item) + + def values(self): + """obj.values() -> list of values, ordered by self.field_order""" + return list(self.itervalues()) + + def get(self,key): + return self.data[key] + + def items(self): + """ + obj.items() -> list of (key,value) pairs, ordered by self.field_order + """ + return list(self.iteritems()) + + def pop(self,key): + raise NotImplementedError + + def popitem(self): + raise NotImplementedError + + def update(self, *args, **keywords): + self.data.update(*args, **keywords) + + ### additional management functions + def _setDefs(self): + self.data = {} + self.field = 0 + self.log = MythLog(self.logmodule) + + def __init__(self, raw): + self._setDefs() + self.data.update(self._process(raw)) + + def _process(self, data): + data = list(data) + for i in range(0, len(data)): + if data[i] == '': + data[i] = None + elif self.field_type == 'Pass': + data[i] = data[i] + elif self.field_type[i] == 0: + data[i] = int(data[i]) + elif self.field_type[i] == 1: + data[i] = locale.atof(data[i]) + elif self.field_type[i] == 2: + data[i] = bool(data[i]) + elif self.field_type[i] == 3: + data[i] = data[i] + elif self.field_type[i] == 4: + data[i] = datetime.fromtimestamp(int(data[i])) + return dict(zip(self.field_order,data)) + + @staticmethod + def joinInt(high,low): + """obj.joinInt(high, low) -> 64-bit int, from two signed ints""" + return (high + (low<0))*2**32 + low + + @staticmethod + def splitInt(integer): + """obj.joinInt(64-bit int) -> (high, low)""" + return integer/(2**32),integer%2**32 - (integer%2**32 > 2**31)*2**32 + +class DBData( DictData ): + """ + DBData.__init__(data=None, db=None, raw=None) --> DBData object + + Altered DictData adding several functions for dealing with individual rows. + Must be subclassed for use. + + Subclasses must provide: + table + Name of database table to be accessed + where + String defining WHERE clause for database lookup + setwheredat + String of comma separated variables to be evaluated to set + 'wheredat'. 'eval(setwheredat)' must return a tuple, so string must + contain at least one comma. + + Subclasses may provide: + allow_empty + Controls whether DBData() will allow an empty instance. + schema_value, schema_local, schema_name + Allows checking of alternate plugin schemas + + Can be populated in two manners: + data + Tuple used to perform a SQL query, using the subclass provided + 'where' clause + raw + Raw list as returned by 'select * from mytable' + """ + field_type = 'Pass' + allow_empty = False + logmodule = 'Python DBData' + schema_value = 'DBSchemaVer' + schema_local = SCHEMA_VERSION + schema_name = 'Database' + + def getAllEntries(self): + """obj.getAllEntries() -> tuple of DBData objects""" + c = self.db.cursor(self.log) + query = """SELECT * FROM %s""" % self.table + self.log(MythLog.DATABASE, query) + if c.execute(query) == 0: + return () + objs = [] + for row in c.fetchall(): + objs.append(self.__class__(db=self.db, raw=row)) + c.close() + return objs + + def _setDefs(self): + self.__dict__['field_order'] = self.db.tablefields[self.table] + DictData._setDefs(self) + self._fillNone() + self.wheredat = None + self.log = MythLog(self.logmodule, db=self.db) + + def __init__(self, data=None, db=None, raw=None): + self.__dict__['db'] = MythDBBase(db) + self.db._check_schema(self.schema_value, + self.schema_local, self.schema_name) + self._setDefs() + + if raw is not None: + if len(raw) == len(self.field_order): + self.data.update(self._process(raw)) + self.wheredat = eval(self.setwheredat) + else: + raise MythError('Incorrect raw input length to DBData()') + elif data is not None: + if None not in data: + self.wheredat = tuple(data) + self._pull() + else: + if self.allow_empty: + self._fillNone() + else: + raise MythError('DBData() not given sufficient information') + + def _pull(self): + """Updates table with data pulled from database.""" + c = self.db.cursor(self.log) + c.execute("""SELECT * FROM %s WHERE %s""" \ + % (self.table, self.where), self.wheredat) + data = c.fetchone() + c.close() + if data is None: + return + self.data.update(self._process(data)) + + def _fillNone(self): + """Fills out dictionary fields with empty data""" + self.field_order = self.db.tablefields[self.table] + for field in self.field_order: + self.data[field] = None + +class DBDataWrite( DBData ): + """ + DBDataWrite.__init__(data=None, db=None, raw=None) --> DBDataWrite object + + Altered DBData, with support for writing back to the database. + Must be subclassed for use. + + Subclasses must provide: + table + Name of database table to be accessed + where + String defining WHERE clause for database lookup + setwheredat + String of comma separated variables to be evaluated to set + 'wheredat'. 'eval(setwheredat)' must return a tuple, so string must + contain at least one comma. + + Subclasses may provide: + defaults + Dictionary of default values to be used when creating new + database entries. Additionally, values of 'None' will be stripped + and not used to alter the database. + + Can be populated in two manners: + data + Tuple used to perform a SQL query, using the subclass provided + 'where' clause + raw + Raw list as returned by 'select * from mytable' + Additionally, can be left uninitialized to allow creation of a new entry + """ + defaults = None + allow_empty = True + logmodule = 'Python DBDataWrite' + + def _sanitize(self, data, fill=True): + """Remove fields from dictionary that are not in database table.""" + data = data.copy() + for key in data.keys(): + if key not in self.field_order: + del data[key] + if self.defaults is not None: + for key in self.defaults: + if key in data: + if self.defaults[key] is None: + del data[key] + elif data[key] is None: + if fill: + data[key] = self.defaults[key] + return data + + def _setDefs(self): + DBData._setDefs(self) + self._fillDefs() + + def _fillDefs(self): + self._fillNone() + self.data.update(self.defaults) + + def __init__(self, data=None, db=None, raw=None): + DBData.__init__(self, data, db, raw) + if raw is not None: + self.origdata = self.data.copy() + + def create(self,data=None): + """ + obj.create(data=None) -> new database row + + Creates a new database entry using given information. + Will add any information in 'data' dictionary to local information + before pushing the entire set onto the database. + Will only function with an uninitialized object. + """ + if self.wheredat is not None: + raise MythError('DBDataWrite object already bound to '+\ + 'existing instance') + + if data is not None: + data = self._sanitize(data, False) + self.data.update(data) + data = self._sanitize(self.data) + for key in data.keys(): + if data[key] is None: + del data[key] + c = self.db.cursor(self.log) + fields = ', '.join(data.keys()) + format_string = ', '.join(['%s' for d in data.values()]) + c.execute("""INSERT INTO %s (%s) VALUES(%s)""" \ + % (self.table, fields, format_string), data.values()) + intid = c.lastrowid + c.close() + return intid + + def _pull(self): + DBData._pull(self) + self.origdata = self.data.copy() + + def _push(self): + if (self.where is None) or (self.wheredat is None): + return + c = self.db.cursor(self.log) + data = self._sanitize(self.data) + for key, value in data.items(): + if value == self.origdata[key]: + # filter unchanged data + del data[key] + if len(data) == 0: + # no updates + return + format_string = ', '.join(['%s = %%s' % d for d in data]) + sql_values = data.values() + sql_values.extend(self.wheredat) + + c.execute("""UPDATE %s SET %s WHERE %s""" \ + % (self.table, format_string, self.where), sql_values) + c.close() + self._pull() + + def update(self, *args, **keywords): + """ + obj.update(*args, **keywords) -> None + + Follows dict.update() syntax. Updates local information, and pushes + changes onto the database. + Will only function on an initialized object. + """ + + data = {} + data.update(*args, **keywords) + self.data.update(self._sanitize(data)) + self._push() + + def delete(self): + """ + obj.delete() -> None + + Delete video entry from database. + """ + if (self.where is None) or \ + (self.wheredat is None) or \ + (self.data is None): + return + c = self.db.cursor(self.log) + c.execute("""DELETE FROM %s WHERE %s""" \ + % (self.table, self.where), self.wheredat) + c.close() + +class DBDataRef( object ): + """ + DBDataRef.__init__(where, db=None) --> DBDataRef object + + Class for managing lists of referenced data, such as recordedmarkup + Subclasses must provide: + table + Name of database table to be accessed + wfield + list of fields for WHERE argument in lookup + + Can be populated by supplying a tuple, matching the fields specified in wfield + """ + logmodule = 'Python DBDataRef' + + class SubData( DictData ): + def _setDefs(self): + self.__dict__['field_order'] = [] + DictData._setDefs(self) + def __repr__(self): + return str(tuple(self)) + def __init__(self,data,fields): + self._setDefs() + self.field_order = fields + self.field_type = 'Pass' + self.data.update(self._process(data)) + def __hash__(self): + dat = self.data.copy() + keys = dat.keys() + keys.sort() + return hash(str([hash(dat[k]) for k in keys])) + def __cmp__(self, x): + return cmp(hash(self),hash(x)) + + def _setDefs(self): + self.data = None + self.hash = None + self.log = MythLog(self.logmodule, db=self.db) + + def __repr__(self): + if self.data is None: + self._fill() + return "" % \ + (str(tuple(self.data)), hex(id(self))) + + def __getitem__(self,index): + if self.data is None: + self._fill() + if index in range(-len(self.data),0)+range(len(self.data)): + return self.data[index] + else: + raise IndexError + + def __iter__(self): + self.field = 0 + if self.data is None: + self._fill() + return self + + def next(self): + if self.field == len(self.data): + del self.field + raise StopIteration + self.field += 1 + return self[self.field-1] + + def __init__(self,where,db=None): + self.db = MythDBBase(db) + self._setDefs() + self.where = tuple(where) + self.dfield = list(self.db.tablefields[self.table]) + for field in self.wfield: + del self.dfield[self.dfield.index(field)] + + def _fill(self): + self.data = [] + self.hash = [] + fields = ','.join(self.dfield) + where = ' AND '.join('%s=%%s' % d for d in self.wfield) + c = self.db.cursor(self.log) + c.execute("""SELECT %s FROM %s WHERE %s""" \ + % (fields, self.table, where), self.where) + for entry in c.fetchall(): + self.data.append(self.SubData(entry, self.dfield)) + self.hash.append(hash(self.data[-1])) + + def add(self, data): + """obj.add(data) -> None""" + if self.data is None: + self._fill() + if len(data) != len(self.wfield): + return + + dat = self.SubData(data, self.dfield) + if hash(dat) in self.hash: + return + + c = self.db.cursor(self.log) + fields = ', '.join(self.wfield+self.dfield) + format = ', '.join(['%s' for d in fields]) + c.execute("""INSERT INTO %s (%s) VALUES(%s)""" \ + % (self.table, fields, format), + self.where+list(data)) + c.close() + + self.data.append(dat) + self.hash.append(hash(dat)) + + def delete(self, index): + """obj.delete(data) -> None""" + if self.data is None: + self._fill() + if index not in range(0, len(self.data)): + return + + where = ' AND '.join(['%s=%%s' % d for d in self.wfield+self.dfield]) + values = self.where+list(self.data[index]) + c = self.db.cursor(self.log) + c.execute("""DELETE FROM %s WHERE %s""" \ + % (self.table, where), values) + c.close() + + del self.data[index] + del self.hash[index] + + +class DBDataCRef( object ): + """ + DBDataRef.__init__(where, db=None) --> DBDataRef object + + Class for managing lists of crossreferenced data, such as recordedcredits + Subclasses must provide: + table - primary data table + rtable - cross reference table + t_ref - primary table field to align with cross reference table + r_ref - cross reference table field to align with primary table + t_add - list of fields for data stored in primary table + r_add - list of fields for data stored in cross reference table + w_field - list of fields for WHERE argument in lookup + """ + logmodule = 'Python DBDataCRef' + + class SubData( DictData ): + def _setDefs(self): + self.__dict__['field_order'] = [] + DictData._setDefs(self) + def __repr__(self): + return str(tuple(self.data.values())) + def __init__(self,data,fields): + self._setDefs() + self.field_order = ['cross']+fields + self.field_type = 'Pass' + self.data.update(self._process(data)) + def __hash__(self): + dat = self.data.copy() + del dat['cross'] + keys = dat.keys() + keys.sort() + return hash(str([hash(dat[k]) for k in keys])) + def __cmp__(self, x): + return cmp(hash(self),hash(x)) + + def _setDefs(self): + self.data = None + self.hash = None + self.log = MythLog(self.logmodule, db=self.db) + + def __repr__(self): + if self.data is None: + self._fill() + return "" % \ + (str(tuple(self.data)), hex(id(self))) + + def __getitem__(self,index): + if self.data is None: + self._fill() + if index in range(-len(self.data),0)+range(len(self.data)): + return self.data[index] + else: + raise IndexError + + def __iter__(self): + self.field = 0 + if self.data is None: + self._fill() + return self + + def next(self): + if self.field == len(self.data): + del self.field + raise StopIteration + self.field += 1 + return self[self.field-1] + + def __init__(self,where,db=None): + self.db = MythDBBase(db) + self._setDefs() + self.w_dat = tuple(where) + + def _fill(self): + self.data = [] + self.hash = [] + fields = ','.join([self.table+'.'+self.t_ref]+self.t_add+self.r_add) + where = ' AND '.join('%s=%%s' % d for d in self.w_field) + join = '%s.%s=%s.%s' % (self.table,self.t_ref,self.rtable,self.r_ref) + c = self.db.cursor(self.log) + c.execute("""SELECT %s FROM %s JOIN %s ON %s WHERE %s"""\ + % (fields, self.table, self.rtable, join, where), + self.w_dat) + for entry in c.fetchall(): + self.data.append(self.SubData(entry, self.t_add+self.r_add)) + self.hash.append(hash(self.data[-1])) + + def _tdata(self, sub): + return tuple([sub[key] for key in self.t_add]) + + def _rdata(self, sub): + return tuple([sub[key] for key in self.r_add+['cross']]+list(self.w_dat)) + + def _search(self, sub): + for i in range(len(self.data)): + if self.data[i] == sub: + return i + + def add(self,data): + """obj.add(data) -> None""" + if self.data is None: + self._fill() + if len(data) != len(self.t_add+self.r_add): + return + + # check for existing + dat = self.SubData([0]+list(data),self.t_add+self.r_add) + if dat in self.data: + return + + tdata = self._tdata(dat) + + c = self.db.cursor(self.log) + # search for existing table data, add if needed + where = ' AND '.join('%s=%%s' % d for d in self.t_add) + if c.execute("""SELECT %s FROM %s WHERE %s""" \ + % (self.t_ref, self.table, where), + self._tdata(dat)) > 0: + id = c.fetchone()[0] + else: + fields = ', '.join(self.t_add) + format = ', '.join(['%s' for d in self.t_add]) + c.execute("""INSERT INTO %s (%s) VALUES(%s)""" \ + % (self.table, fields, format), self._tdata(dat)) + id = c.lastrowid + + dat = self.SubData([id]+list(data),self.t_add+self.r_add) + + # double check for existing + fields = self.r_add+[self.r_ref]+self.w_field + where = ' AND '.join('%s=%%s' % d for d in fields) + if c.execute("""SELECT * FROM %s WHERE %s""" % (self.rtable, where), + self._rdata(dat)) > 0: + return + + # add cross reference + fields = ', '.join(self.r_add+[self.r_ref]+self.w_field) + format = ', '.join(['%s' for d in fields.split(', ')]) + c.execute("""INSERT INTO %s (%s) VALUES(%s)"""\ + % (self.rtable, fields, format), self._rdata(dat)) + c.close() + + # add local entry + self.data.append(dat) + + def delete(self,data): + """obj.delete(data) -> None""" + if self.data is None: + self._fill() + # check for local entry + dat = self.SubData([0]+list(data),self.t_add+self.r_add) + if dat not in self.data: + return + # remove local entry + index = self._search(dat) + dat = self.data.pop(index) + + # remove database cross reference + fields = self.r_add+[self.r_ref]+self.w_field + where = ' AND '.join('%s=%%s' % d for d in fields) + c = self.db.cursor(self.log) + c.execute("""DELETE FROM %s WHERE %s""" % (self.rtable, where), + self._rdata(dat)) + + # remove primary entry if unused + if c.execute("""SELECT %s FROM %s WHERE %s=%%s""" \ + % (self.r_ref, self.rtable, self.r_ref), + dat.cross) == 0: + c.execute("""DELETE FROM %s WHERE %s=%%s""" \ + % (self.table, self.t_ref), dat.cross) + c.close() + + index = self.hash.index[dat] + del self.hash[index] + del self.data[index] + +class MythDBCursor( MySQLdb.cursors.Cursor ): + """ + Custom cursor, offering logging and error handling + """ + def __init__(self, connection): + self.log = None + MySQLdb.cursors.Cursor.__init__(self, connection) + + def execute(self, query, args=None): + if MySQLdb.__version__ >= ('1','2','2'): + self.connection.ping(True) + else: + self.connection.ping() + if self.log == None: + self.log = MythLog('Python Database Connection') + if args: + self.log(self.log.DATABASE, ' '.join(query.split()), str(args)) + else: + self.log(self.log.DATABASE, ' '.join(query.split())) + try: + return MySQLdb.cursors.Cursor.execute(self, query, args) + except Exception, e: + raise MythDBError(MythDBError.DB_RAW, e.args) + + def executemany(self, query, args): + if MySQLdb.__version__ >= ('1','2','2'): + self.connection.ping(True) + else: + self.connection.ping() + if self.log == None: + self.log = MythLog('Python Database Connection') + for arg in args: + self.log(self.log.DATABASE, ' '.join(query.split()), str(arg)) + try: + return MySQLdb.cursors.Cursor.executemany(self, query, args) + except Exception, e: + raise MythDBError(MythDBError.DB_RAW, e.args) + +class MythDBConn( object ): + """ + This is the basic database connection object. + You dont want to use this directly. + """ + + def __init__(self, dbconn): + self.log = MythLog('Python Database Connection') + self.tablefields = {} + self.settings = {} + try: + self.log(MythLog.DATABASE, "Attempting connection", + str(dbconn)) + self.db = MySQLdb.connect( user= dbconn['DBUserName'], + host= dbconn['DBHostName'], + passwd= dbconn['DBPassword'], + db= dbconn['DBName'], + port= dbconn['DBPort'], + use_unicode=True, + charset='utf8') + except: + raise MythDBError(MythError.DB_CONNECTION, dbconn) + + def cursor(self, log=None, type=MythDBCursor): + if MySQLdb.__version__ >= ('1','2','2'): + self.db.ping(True) + else: + self.db.ping() + c = self.db.cursor(type) + if log: + c.log = log + else: + c.log = self.log + return c + +class MythDBBase( object ): + """ + MythDBBase(db=None, args=None, **kwargs) -> database connection object + + Basic connection to the mythtv database + Offers connection caching to prevent multiple connections + + 'db' will accept an existing MythDBBase object, or any subclass there of + 'args' will accept a tuple of 2-tuples for connection settings + 'kwargs' will accept a series of keyword arguments for connection settings + 'args' and 'kwargs' accept the following values: + DBHostName + DBName + DBUserName + DBPassword + SecurityPin + + The class will first use an existing connection if provided, use the 'args' + and 'kwargs' values if sufficient information is available, falling + back to '~/.mythtv/config.xml' and finally UPnP detection. If no + 'SecurityPin' is given, UPnP detection will default to 0000, and if + successful, will populate '~/.mythtv/config.xml' with the necessary + information. + + Available methods: + obj.cursor() - open a cursor for direct database + manipulation + obj.getStorageGroup() - return a tuple of StorageGroup objects + """ + logmodule = 'Python Database Connection' + cursorclass = MythDBCursor + shared = weakref.WeakValueDictionary() + + def __repr__(self): + return "<%s '%s' at %s>" % \ + (str(self.__class__).split("'")[1].split(".")[-1], + self.ident, hex(id(self))) + + class _TableFields(object): + """Provides a dictionary-like list of table fieldnames""" + def __init__(self, db, log): + self._db = db + self._log = log + def __getattr__(self,key): + if key in self.__dict__: + return self.__dict__[key] + elif key in self._db.tablefields: + return self._db.tablefields[key] + else: + return self.__getitem__(key) + def __getitem__(self,key): + if key in self._db.tablefields: + return self._db.tablefields[key] + table_names = [] + c = self._db.cursor(self._log) + try: + c.execute("DESC %s" % (key,)) + except: + return None + + for name in c.fetchall(): + table_names.append(name[0]) + c.close() + self._db.tablefields[key] = tuple(table_names) + return table_names + + class _Settings(object): + """Provides dictionary-like list of hosts""" + class __Settings(object): + """Provides dictionary-like list of settings""" + def __repr__(self): + return str(self._shared.keys()) + def __init__(self, db, log, host): + self._db = db + self._log = log + self._host = host + def __getattr__(self,name): + if name in self.__dict__: + return self.__dict__[name] + else: + return self.__getitem__(name) + def __setattr__(self,name,value): + if name in ('_db','_host','_log'): + self.__dict__[name] = value + else: + self.__setitem__(name,value) + def __getitem__(self,key): + if key in self._db.settings[self._host]: + return self._db.settings[self._host][key] + if self._host == 'NULL': + where = 'IS NULL' + wheredat = (key,) + else: + where = 'LIKE(%s)' + wheredat = (key, self._host) + c = self._db.cursor(self._log) + c.execute("""SELECT data FROM settings + WHERE value=%%s AND hostname %s + LIMIT 1""" % where, wheredat) + row = c.fetchone() + c.close() + if row: + self._db.settings[self._host][key] = row[0] + return row[0] + else: + return None + def __setitem__(self, key, value): + if key not in self._db.settings[self._host]: + self.__getitem__(key) + c = self._db.cursor(self._log) + if key not in self._db.settings[self._host]: + host = self._host + if host is 'NULL': + host = None + c.execute("""INSERT INTO settings (value,data,hostname) + VALUES (%s,%s,%s)""", (key, value, host)) + else: + query = """UPDATE settings SET data=%s + WHERE value=%s AND""" + dat = [value, key] + if self._host == 'NULL': + query += ' hostname IS NULL' + else: + query += ' hostname=%s' + dat.append(self._host) + c.execute(query, dat) + self._db.settings[self._host][key] = value + def __repr__(self): + return str(self._db.settings.keys()) + def __init__(self, db, log): + self._db = db + self._log = log + def __getattr__(self,key): + if key in self.__dict__: + return self.__dict__[key] + else: + return self.__getitem__(key) + def __getitem__(self,key): + if key not in self._db.settings: + self._db.settings[key] = {} + return self.__Settings(self._db, self._log, key) + + def __init__(self, db=None, args=None, **dbconn): + self.db = None + self.log = MythLog(self.logmodule) + self.settings = None + if db is not None: + # load existing database connection + self.log(MythLog.DATABASE, "Loading existing connection", + str(db.dbconn)) + dbconn.update(db.dbconn) + if args is not None: + # load user defined arguments (takes dict, or key/value tuple) + self.log(MythLog.DATABASE, "Loading user settings", str(args)) + dbconn.update(args) + if 'SecurityPin' not in dbconn: + dbconn['SecurityPin'] = 0 + if not self._check_dbconn(dbconn): + # insufficient information for connection given + # try to read from config.xml + config_files = [ os.path.expanduser('~/.mythtv/config.xml') ] + if 'MYTHCONFDIR' in os.environ: + config_files.append('%s/config.xml' %os.environ['MYTHCONFDIR']) + + for config_file in config_files: + dbconn.update({ 'DBHostName':None, 'DBName':None, + 'DBUserName':None, 'DBPassword':None, + 'DBPort':0}) + try: + config = etree.parse(config_file).getroot() + for child in config.find('UPnP').find('MythFrontend').\ + find('DefaultBackend').getchildren(): + if child.tag in dbconn: + dbconn[child.tag] = child.text + except: + continue + + if self._check_dbconn(dbconn): + self.log(MythLog.IMPORTANT|MythLog.DATABASE, + "Using connection settings from %s" % config_file) + break + else: + # fall back to UPnP + dbconn = self._listenUPNP(dbconn['SecurityPin'], 5.0) + + # push data to new settings file + settings = [dbconn[key] for key in \ + ('SecurityPin','DBHostName','DBUserName', + 'DBPassword','DBName','DBPort')] + config = """ + + + + + + + + %s + %s + %s + %s + %s + %s + + + + +""" % tuple(settings) + mythdir = os.path.expanduser('~/.mythtv') + if not os.access(mythdir, os.F_OK): + os.mkdir(mythdir,0755) + fp = open(mythdir+'/config.xml', 'w') + fp.write(config) + fp.close() + + if 'DBPort' not in dbconn: + dbconn['DBPort'] = 3306 + else: + dbconn['DBPort'] = int(dbconn['DBPort']) + if dbconn['DBPort'] == 0: + dbconn['DBPort'] = 3306 + + self.dbconn = dbconn + self.ident = "sql://%s@%s:%d/" % \ + (dbconn['DBName'],dbconn['DBHostName'],dbconn['DBPort']) + if self.ident in self.shared: + # reuse existing database connection and update count + self.db = self.shared[self.ident] + else: + # attempt new connection + self.db = MythDBConn(dbconn) + + # check schema version + self._check_schema('DBSchemaVer',SCHEMA_VERSION) + + # add connection to cache + self.shared[self.ident] = self.db + + # connect to table name cache + self.tablefields = self._TableFields(self.shared[self.ident], self.log) + self.settings = self._Settings(self.shared[self.ident], self.log) + + def _listenUPNP(self, pin, timeout): + # open outbound socket + + upnpport = 1900 + upnptup = ('239.255.255.250', upnpport) + sreq = '\r\n'.join(['M-SEARCH * HTTP/1.1', + 'HOST: %s:%s' % upnptup, + 'MAN: "ssdp:discover"', + 'MX: 5', + 'ST: ssdp:all','']) + + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, + socket.IPPROTO_UDP) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + try: + sock.bind(('', upnpport)) + except: + raise MythDBError(MythError.DB_CREDENTIALS) + sock.setblocking(0) + + # spam the request a couple times + sock.sendto(sreq, upnptup) + sock.sendto(sreq, upnptup) + sock.sendto(sreq, upnptup) + + reLOC = re.compile('http://(?P[0-9\.]+):(?P[0-9]+)/.*') + + # listen for + atime = time()+timeout + while time() < atime: + sleep(0.1) + try: + sdata, saddr = sock.recvfrom(2048) + except socket.error: + continue # on fault from nonblocking recv + + lines = sdata.split('\n') + sdict = {'request':lines[0].strip()} + for line in lines[1:]: + fields = line.split(':',1) + if len(fields) == 2: + sdict[fields[0].strip().lower()] = fields[1].strip() + + if 'st' not in sdict: + continue + if sdict['st'] not in \ + ('urn:schemas-mythtv-org:device:MasterMediaServer:1', + 'urn:schemas-mythtv-org:device:SlaveMediaServer:1', + 'urn:schemas-upnp-org:device:MediaServer:1'): + continue + ip, port = reLOC.match(sdict['location']).group(1,2) + xml = MythXMLConn(backend=ip, port=port) + dbconn = xml.getConnectionInfo(pin) + if self._check_dbconn(dbconn): + break + + else: + sock.close() + raise MythDBError(MythError.DB_CREDENTIALS) + + sock.close() + return dbconn + + def _check_dbconn(self,dbconn): + reqs = ['DBHostName','DBName','DBUserName','DBPassword'] + for req in reqs: + if req not in dbconn: + return False + if dbconn[req] is None: + return False + return True + + def _check_schema(self, value, local, name='Database'): + if self.settings is None: + c = self.cursor(self.log) + lines = c.execute("""SELECT data FROM settings + WHERE value LIKE(%s)""",(value,)) + if lines == 0: + c.close() + raise MythDBError(MythError.DB_SETTING, value) + + sver = int(c.fetchone()[0]) + c.close() + else: + sver = int(self.settings['NULL'][value]) + + if local != sver: + self.log(MythLog.DATABASE|MythLog.IMPORTANT, + "%s schema mismatch: we speak %d but database speaks %s" \ + % (name, local, sver)) + self.db = None + raise MythDBError(MythError.DB_SCHEMAMISMATCH, value, sver, local) + + def getStorageGroup(self, groupname=None, hostname=None): + """ + obj.getStorageGroup(groupname=None, hostname=None) + -> tuple of StorageGroup objects + groupname and hostname can be used as optional filters + """ + c = self.cursor(self.log) + where = [] + wheredat = [] + if groupname: + where.append("groupname=%s") + wheredat.append(groupname) + if hostname: + where.append("hostname=%s") + wheredat.append(hostname) + if len(where): + where = 'WHERE '+' AND '.join(where) + c.execute("""SELECT * FROM storagegroup %s ORDER BY id""" \ + % where, wheredat) + else: + c.execute("""SELECT * FROM storagegroup ORDER BY id""") + + ret = [] + for row in c.fetchall(): + ret.append(StorageGroup(raw=row,db=self)) + return ret + + def cursor(self, log=None): + if not log: + log = self.log + return self.db.cursor(log, self.cursorclass) + +class MythBEConn( object ): + """ + This is the basic backend connection object. + You probably dont want to use this directly. + """ + logmodule = 'Python Backend Connection' + + def __init__(self, backend, type, db=None): + """ + MythBEConn(backend, type, db=None) -> backend socket connection + + 'backend' can be either a hostname or IP address, or will default + to the master backend if None. + 'type' is any value, as required by obj.announce(type). The stock + method passes 'Monitor' or 'Playback' during connection to + backend. There is no checking on this input. + 'db' will accept an existing MythDBBase object, + or any subclass there of. + """ + self.connected = False + self.db = MythDBBase(db) + self.log = MythLog(self.logmodule, db=self.db) + if backend is None: + # use master backend, no sanity checks, these should always be set + self.host = self.db.settings.NULL.MasterServerIP + self.port = self.db.settings.NULL.MasterServerPort + else: + if re.match('(?:\d{1,3}\.){3}\d{1,3}',backend): + # process ip address + c = self.db.cursor(self.log) + if c.execute("""SELECT hostname FROM settings + WHERE value='BackendServerIP' + AND data=%s""", backend) == 0: + c.close() + raise MythError(MythError.DB_SETTING, + 'BackendServerIP', backend) + backend = c.fetchone()[0] + c.close() + self.host = self.db.settings[backend].BackendServerIP + if not self.host: + raise MythDBError(MythError.DB_SETTING, + 'BackendServerIP', backend) + self.port = self.db.settings[backend].BackendServerPort + if not self.port: + raise MythDBError(MythError.DB_SETTING, + 'BackendServerPort', backend) + self.port = int(self.port) + + try: + self.log(MythLog.SOCKET|MythLog.NETWORK, + "Connecting to backend %s:%d" % (self.host, self.port)) + self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.socket.settimeout(10) + self.socket.connect((self.host, self.port)) + self.check_version() + self.announce(type) + self.connected = True + except socket.error, e: + self.log(MythLog.IMPORTANT|MythLog.SOCKET, + "Couldn't connect to backend %s:%d" \ + % (self.host, self.port)) + raise MythBEError(MythError.PROTO_CONNECTION, self.host, self.port) + except: + raise + + def announce(self,type): + res = self.backendCommand('ANN %s %s 0' % (type, socket.gethostname())) + if res != 'OK': + self.log(MythLog.IMPORTANT|MythLog.NETWORK, + "Unexpected answer to ANN", res) + raise MythBEError(MythError.PROTO_ANNOUNCE, + self.host, self.port, res) + else: + self.log(MythLog.SOCKET,"Successfully connected to backend", + "%s:%d" % (self.host, self.port)) + self.hostname = self.backendCommand('QUERY_HOSTNAME') + + def check_version(self): + res = self.backendCommand('MYTH_PROTO_VERSION %s' \ + % PROTO_VERSION).split(BACKEND_SEP) + if res[0] == 'REJECT': + self.log(MythLog.IMPORTANT|MythLog.NETWORK, + "Backend has version %s, and we speak %d" %\ + (res[1], PROTO_VERSION)) + raise MythBEError(MythError.PROTO_MISMATCH, + int(res[1]), PROTO_VERSION) + + def backendCommand(self, data): + """ + obj.backendCommand(data) -> response string + + Sends a formatted command via a socket to the mythbackend. + """ + def recv(): + """ + Reads the data returned from the backend. + """ + # The first 8 bytes of the response gives us the length + data = self.socket.recv(8) + try: + length = int(data) + except: + return u'' + data = [] + while length > 0: + chunk = self.socket.recv(length) + length = length - len(chunk) + data.append(chunk) + try: + return unicode(''.join(data),'utf8') + except: + return u''.join(data) + + data = data.encode('utf-8') + command = '%-8d%s' % (len(data), data) + self.log(MythLog.NETWORK, "Sending command", command) + self.socket.send(command) + return recv() + + def __del__(self): + if self.connected: + self.log(MythLog.SOCKET|MythLog.NETWORK, + "Terminating connection to %s:%d" % (self.host, self.port)) + self.backendCommand('DONE') + self.socket.shutdown(1) + self.socket.close() + self.connected = False + +class MythBEBase( object ): + """ + MythBEBase(backend=None, type='Monitor', db=None) + -> MythBackend connection object + + Basic class for mythbackend socket connections. + Offers connection caching to prevent multiple connections. + + 'backend' allows a hostname or IP address to connect to. If not provided, + connect will be made to the master backend. Port is always + taken from the database. + 'db' allows an existing database object to be supplied. + 'type' specifies the type of connection to declare to the backend. Accepts + 'Monitor' or 'Playback'. + + Available methods: + joinInt() - convert two signed ints to one 64-bit + signed int + splitInt() - convert one 64-bit signed int to two + signed ints + backendCommand() - Sends a formatted command to the backend + and returns the response. + """ + logmodule = 'Python Backend Connection' + shared = weakref.WeakValueDictionary() + + def __repr__(self): + return "<%s 'myth://%s:%d/' at %s>" % \ + (str(self.__class__).split("'")[1].split(".")[-1], + self.hostname, self.port, hex(id(self))) + + def __init__(self, backend=None, type='Monitor', db=None): + self.db = MythDBBase(db) + self.log = MythLog(self.logmodule, db=self.db) + self._ident = '%s@%s' % (type, backend) + if self._ident in self.shared: + self.be = self.shared[self._ident] + else: + self.be = MythBEConn(backend, type, db) + self.shared[self._ident] = self.be + self.hostname = self.be.hostname + self.port = self.be.port + + def backendCommand(self, data): + """ + obj.backendCommand(data) -> response string + + Sends a formatted command via a socket to the mythbackend. + """ + return self.be.backendCommand(data) + + def joinInt(self,high,low): + """obj.joinInt(high, low) -> 64-bit int, from two signed ints""" + return (high + (low<0))*2**32 + low + + def splitInt(self,integer): + """obj.joinInt(64-bit int) -> (high, low)""" + return integer/(2**32),integer%2**32 - (integer%2**32 > 2**31)*2**32 + + +class MythXMLConn( object ): + """ + MythXMLConn(backend=None, db=None, port=None) -> Backend status object + + Basic access to MythBackend status page and XML data server + + 'backend' allows a hostname or IP, defaulting to the master backend. + 'port' defines the port used to access the backend, retrieved from the + database if not given. + 'db' allows an existing database connection. Will only be used if + either 'backend' or 'port' is not defined. + """ + def __repr__(self): + return "<%s 'http://%s:%d/' at %s>" % \ + (str(self.__class__).split("'")[1].split(".")[-1], + self.host, self.port, hex(id(self))) + + def __init__(self, backend=None, db=None, port=None): + if backend and port: + self.db = None + self.host, self.port = backend, int(port) + return + self.db = MythDBBase(db) + self.log = MythLog('Python XML Connection', db=self.db) + if backend is None: + # use master backend + backend = self.db.settings.NULL.MasterServerIP + if re.match('(?:\d{1,3}\.){3}\d{1,3}',backend): + # process ip address + c = self.db.cursor(self.log) + if c.execute("""SELECT hostname FROM settings + WHERE value='BackendServerIP' + AND data=%s""", backend) == 0: + raise MythDBError(MythError.DB_SETTING, + backend+': BackendServerIP') + self.host = c.fetchone()[0] + self.port = int(self.db.settings[self.host].BackendStatusPort) + c.close() + else: + # assume given a hostname + self.host = backend + self.port = int(self.db.settings[self.host].BackendStatusPort) + if not self.port: + # try a truncated hostname + self.host = backend.split('.')[0] + self.port = int(self.db.setting[self.host].BackendStatusPort) + if not self.port: + raise MythDBError(MythError.DB_SETTING, + backend+': BackendStatusPort') + + def _query(self, path=None, **keyvars): + """ + obj._query(path=None, **keyvars) -> xml string + + 'path' is an optional page to access. + 'keyvars' are a series of optional variables to specify on the URL. + """ + url = 'http://%s:%d/' % (self.host, self.port) + if path == 'xml': + url += 'xml' + elif path is not None: + url += 'Myth/%s' % path + if len(keyvars) > 0: + fields = [] + for key in keyvars: + fields.append('%s=%s' % (key, keyvars[key])) + url += '?'+'&'.join(fields) + ufd = urlopen(url) + res = ufd.read() + ufd.close() + return res + + def _queryTree(self, path=None, **keyvars): + """obj._queryTree(path=None, **keyvars) -> xml element tree""" + xmlstr = self._query(path, **keyvars) + if xmlstr.find('xmlns') >= 0: + ind1 = xmlstr.find('xmlns') + ind2 = xmlstr.find('"', ind1+7) + 1 + xmlstr = xmlstr[:ind1] + xmlstr[ind2:] + return etree.fromstring(xmlstr) + + def getConnectionInfo(self, pin=0): + """Return dbconn dict from backend connection info.""" + dbconn = {'SecurityPin':pin} + conv = {'Host':'DBHostName', 'Port':'DBPort', + 'UserName':'DBUserName','Password':'DBPassword', + 'Name':'DBName'} + tree = self._queryTree('GetConnectionInfo', Pin=pin) + if tree.tag == 'GetConnectionInfoResponse': + for child in tree.find('Info').find('Database'): + if child.tag in conv: + dbconn[conv[child.tag]] = child.text + if 'DBPort' in dbconn: + dbconn['DBPort'] = int(dbconn['DBPort']) + return dbconn + + +class MythLog( object ): + """ + MythLog(module='pythonbindings', lstr=None, lbit=None, \ + db=None, logfile=None) -> logging object + + 'module' defines the source of the message in the logs + 'lstr' and 'lbit' define the message filter + 'lbit' takes a bitwise value + 'lstr' takes a string in the normal '-v level' form + default is set to 'important,general' + 'logfile' sets a file to open and subsequently log to + + The filter level and logfile are global values, shared between all logging + instances. Furthermode, the logfile can only be set once, and cannot + be changed. + + The logging object is callable, and implements the MythLog.log() method. + """ + + ALL = int('1111111111111111111111111111', 2) + MOST = int('0011111111101111111111111111', 2) + NONE = int('0000000000000000000000000000', 2) + + IMPORTANT = int('0000000000000000000000000001', 2) + GENERAL = int('0000000000000000000000000010', 2) + RECORD = int('0000000000000000000000000100', 2) + PLAYBACK = int('0000000000000000000000001000', 2) + CHANNEL = int('0000000000000000000000010000', 2) + OSD = int('0000000000000000000000100000', 2) + FILE = int('0000000000000000000001000000', 2) + SCHEDULE = int('0000000000000000000010000000', 2) + NETWORK = int('0000000000000000000100000000', 2) + COMMFLAG = int('0000000000000000001000000000', 2) + AUDIO = int('0000000000000000010000000000', 2) + LIBAV = int('0000000000000000100000000000', 2) + JOBQUEUE = int('0000000000000001000000000000', 2) + SIPARSER = int('0000000000000010000000000000', 2) + EIT = int('0000000000000100000000000000', 2) + VBI = int('0000000000001000000000000000', 2) + DATABASE = int('0000000000010000000000000000', 2) + DSMCC = int('0000000000100000000000000000', 2) + MHEG = int('0000000001000000000000000000', 2) + UPNP = int('0000000010000000000000000000', 2) + SOCKET = int('0000000100000000000000000000', 2) + XMLTV = int('0000001000000000000000000000', 2) + DVBCAM = int('0000010000000000000000000000', 2) + MEDIA = int('0000100000000000000000000000', 2) + IDLE = int('0001000000000000000000000000', 2) + CHANNELSCAN = int('0010000000000000000000000000', 2) + EXTRA = int('0100000000000000000000000000', 2) + TIMESTAMP = int('1000000000000000000000000000', 2) + + DBALL = 8 + DBDEBUG = 7 + DBINFO = 6 + DBNOTICE = 5 + DBWARNING = 4 + DBERROR = 3 + DBCRITICAL = 2 + DBALERT = 1 + DBEMERGENCY = 0 + + LOGLEVEL = IMPORTANT|GENERAL + LOGFILE = None + + helptext = """Verbose debug levels. + Accepts any combination (separated by comma) of: + + " all " - ALL available debug output + " most " - Most debug (nodatabase,notimestamp,noextra) + " important " - Errors or other very important messages + " general " - General info + " record " - Recording related messages + " playback " - Playback related messages + " channel " - Channel related messages + " osd " - On-Screen Display related messages + " file " - File and AutoExpire related messages + " schedule " - Scheduling related messages + " network " - Network protocol related messages + " commflag " - Commercial Flagging related messages + " audio " - Audio related messages + " libav " - Enables libav debugging + " jobqueue " - JobQueue related messages + " siparser " - Siparser related messages + " eit " - EIT related messages + " vbi " - VBI related messages + " database " - Display all SQL commands executed + " dsmcc " - DSMCC carousel related messages + " mheg " - MHEG debugging messages + " upnp " - upnp debugging messages + " socket " - socket debugging messages + " xmltv " - xmltv output and related messages + " dvbcam " - DVB CAM debugging messages + " media " - Media Manager debugging messages + " idle " - System idle messages + " channelscan " - Channel Scanning messages + " extra " - More detailed messages in selected levels + " timestamp " - Conditional data driven messages + " none " - NO debug output + + The default for this program appears to be: '-v "important,general" ' + + Most options are additive except for none, all, and important. + These three are semi-exclusive and take precedence over any + prior options given. You can however use something like + '-v none,jobqueue' to get only JobQueue related messages + and override the default verbosity level. + + The additive options may also be subtracted from 'all' by + prefixing them with 'no', so you may use '-v all,nodatabase' + to view all but database debug messages. + + Some debug levels may not apply to this program. +""" + + def __repr__(self): + return "<%s '%s','%s' at %s>" % \ + (str(self.__class__).split("'")[1].split(".")[-1], + self.module, bin(self.LOGLEVEL), hex(id(self))) + + def __init__(self, module='pythonbindings', lstr=None, lbit=None, \ + db=None, logfile=None): + self._setlevel(lstr, lbit) + self.module = module + self.db = db + if self.db: + self.db = MythDBBase(self.db) + if (logfile is not None) and (self.LOGFILE is None): + self.LOGFILE = open(logfile,'w') + + def _setlevel(lstr=None, lbit=None): + if lstr: + MythLog.LOGLEVEL = MythLog._parselevel(lstr) + elif lbit: + MythLog.LOGLEVEL = lbit + _setlevel = staticmethod(_setlevel) + + def _parselevel(lstr): + level = MythLog.NONE + bwlist = ( 'important','general','record','playback','channel','osd', + 'file','schedule','network','commflag','audio','libav', + 'jobqueue','siparser','eit','vbi','database','dsmcc', + 'mheg','upnp','socket','xmltv','dvbcam','media','idle', + 'channelscan','extra','timestamp') + for l in lstr.split(','): + if l in ('all','most','none'): + # set initial bitfield + level = eval('MythLog.'+l.upper()) + elif l in bwlist: + # update bitfield OR + level |= eval('MythLog.'+l.upper()) + elif len(l) > 2: + if l[0:2] == 'no': + if l[2:] in bwlist: + # update bitfield NOT + level &= level^eval('MythLog.'+l[2:].upper()) + return level + _parselevel = staticmethod(_parselevel) + + def log(self, level, message, detail=None, dblevel=None): + """ + MythLog.log(level, message, detail=None, dblevel=None) -> None + + 'level' sets the bitwise log level, to be matched against the log + filter. If any bits match true, the message will be logged. + 'message' and 'detail' set the log message content using the format: + : + ---- or ---- + : -- + """ + if level&self.LOGLEVEL: + if (version_info[0]>2) | (version_info[1]>5): # 2.6 or newer + nowstr = datetime.now().strftime('%Y-%m-%d %H:%M:%S.%f')[:-3] + else: + nowstr = datetime.now().strftime('%Y-%m-%d %H:%M:%S.000') + if detail is not None: + lstr = "%s %s: %s -- %s"%(nowstr, self.module, message, detail) + else: + lstr = "%s %s: %s" % (nowstr, self.module, message) + if self.LOGFILE: + self.LOGFILE.write(lstr+'\n') + self.LOGFILE.flush() + os.fsync(self.LOGFILE.fileno()) + else: + print lstr + return + + if dblevel is not None: + if self.db is None: + self.db = MythDBBase() + c = self.db.cursor(self.log) + c.execute("""INSERT INTO mythlog (module, priority, logdate, + host, message, details) + VALUES (%s, %s, %s, %s, %s, %s)""", + (self.module, dblevel, now(), + self.host, message, detail)) + c.close() + + def __call__(self, level, message, detail=None, dblevel=None): + self.log(level, message, detail, dblevel) + +class MythError( Exception ): + """ + MythError('Generic Error Code') -> Exception + MythError(error_code, additional_arguments) -> Exception + + Objects will have an error string available at obj.args as well as an + error code at obj.ecode. Additional attributes may be available + depending on the error code. + """ + GENERIC = 0 + SYSTEM = 1 + DB_RAW = 50 + DB_CONNECTION = 51 + DB_CREDENTIALS = 52 + DB_SETTING = 53 + DB_SCHEMAMISMATCH = 54 + PROTO_CONNECTION = 100 + PROTO_ANNOUNCE = 101 + PROTO_MISMATCH = 102 + FE_CONNECTION = 150 + FE_ANNOUNCE = 151 + FILE_ERROR = 200 + FILE_FAILED_READ = 201 + FILE_FAILED_WRITE = 202 + def __init__(self, *args): + if args[0] == self.SYSTEM: + self.ename = 'SYSTEM' + self.ecode, self.retcode, self.command, self.stderr = args + self.args = ("External system call failed: code %d" %self.retcode,) + elif args[0] == self.DB_RAW: + self.ename = 'DB_RAW' + self.ecode, sqlerr = args + if len(sqlerr) == 1: + self.sqlcode = 0 + self.sqlerr = sqlerr[0] + self.args = ("MySQL error: %s" % self.sqlerr,) + else: + self.sqlcode, self.sqlerr = sqlerr + self.args = ("MySQL error %d: %s" % sqlerr,) + elif args[0] == self.DB_CONNECTION: + self.ename = 'DB_CONNECTION' + self.ecode, self.dbconn = args + self.args = ("Failed to connect to database at '%s'@'%s'" \ + % (self.dbconn['DBName'], self.dbconn['DBHostName']) \ + +"for user '%s' with password '%s'." \ + % (self.dbconn['DBUserName'], self.dbconn['DBPassword']),) + elif args[0] == self.DB_CREDENTIALS: + self.ename = 'DB_CREDENTIALS' + self.ecode = args + self.args = ("Could not find database login credentials",) + elif args[0] == self.DB_SETTING: + self.ename = 'DB_SETTING' + self.ecode, self.setting, self.hostname = args + self.args = ("Could not find setting '%s' on host '%s'" \ + % (self.setting, self.hostname),) + elif args[0] == self.DB_SCHEMAMISMATCH: + self.ename = 'DB_SCHEMAMISMATCH' + self.ecode, self.setting, self.remote, self.local = args + self.args = ("Mismatched schema version for '%s': " % self.setting \ + + "database speaks version %d, we speak version %d"\ + % (self.remote, self.local),) + elif args[0] == self.PROTO_CONNECTION: + self.ename = 'PROTO_CONNECTION' + self.ecode, self.backend, self.port = args + self.args = ("Failed to connect to backend at %s:%d" % \ + (self.backend, self.port),) + elif args[0] == self.PROTO_ANNOUNCE: + self.ename = 'PROTO_ANNOUNCE' + self.ecode, self.backend, self.port, self.response = args + self.args = ("Unexpected response to ANN on %s:%d - %s" \ + % (self.backend, self.port, self.response),) + elif args[0] == self.PROTO_MISMATCH: + self.ename = 'PROTO_MISMATCH' + self.ecode, self.remote, self.local = args + self.args = ("Backend speaks version %s, we speak version %s" % \ + (self.remote, self.local),) + elif args[0] == self.FE_CONNECTION: + self.ename = 'FE_CONNECTION' + self.ecode, self.frontend, self.port = args + self.args = ('Connection to frontend %s:%d failed' \ + % (self.frontend, self.port),) + elif args[0] == self.FE_ANNOUNCE: + self.ename = 'FE_ANNOUNCE' + self.ecode, self.frontend, self.port = args + self.args = ('Open socket at %s:%d not recognized as mythfrontend'\ + % (self.frontend, self.port),) + elif args[0] == self.FILE_ERROR: + self.ename = 'FILE_ERROR' + self.ecode, self.reason = args + self.args = ("Error accessing file: " % self.reason,) + elif args[0] == self.FILE_FAILED_READ: + self.ename = 'FILE_FAILED_READ' + self.ecode, self.file = args + self.args = ("Error accessing %s" %(self.file,self.mode),) + elif args[0] == self.FILE_FAILED_WRITE: + self.ename = 'FILE_FAILED_WRITE' + self.ecode, self.file, self.reason = args + self.args = ("Error writing to %s, %s" % (self.file, self.reason),) + else: + self.ename = 'GENERIC' + self.ecode = self.GENERIC + self.args = args + self.message = str(self.args[0]) + +class MythDBError( MythError ): pass +class MythBEError( MythError ): pass +class MythFEError( MythError ): pass +class MythFileError( MythError ): pass + +class StorageGroup( DBData ): + """ + StorageGroup(id=None, db=None, raw=None) -> StorageGroup object + Represents a single storage group entry + """ + table = 'storagegroup' + where = 'id=%s' + setwheredat = 'self.id,' + logmodule = 'Python StorageGroup' + + def __str__(self): + return u" Grabber object + + 'path' sets the object to use a path to an external command + 'setting' pulls the external command from a setting in the database + """ + logmodule = 'Python Metadata Grabber' + + def __init__(self, path=None, setting=None, db=None): + MythDBBase.__init__(self, db=db) + self.log = MythLog(self.logmodule, db=self) + self.path = '' + if path is not None: + self.path = path + elif setting is not None: + host = socket.gethostname() + self.path = self.settings[host][setting] + if self.path is None: + raise MythDBError(MythError.DB_SETTING, setting, host) + else: + raise MythError('Invalid input to Grabber()') + self.returncode = 0 + self.stderr = '' + + def __str__(self): + return "<%s '%s' at %s>" % \ + (str(self.__class__).split("'")[1].split(".")[-1], + self.path, hex(id(self))) + + def __repr__(self): + return str(self).encode('utf-8') + + def append(self, *args): + """ + obj.append(*args) -> None + + Permenantly appends one or more strings to the command + path, separated by spaces. + """ + self.path += ' '+' '.join(['%s' % a for a in args]) + + def command(self, *args): + """ + obj.command(*args) -> output string + + Executes external command, adding one or more strings to the + command during the call. If call exits with a code not + equal to 0, a MythError will be raised. The error code and + stderr will be available in the exception and this object + as attributes 'returncode' and 'stderr'. + """ + if self.path is '': + return '' + arg = ' '+' '.join(['%s' % a for a in args]) + fd = Popen('%s %s' % (self.path, arg), stdout=-1, stderr=-1, + shell=True) + self.returncode = fd.wait() + stdout,self.stderr = fd.communicate() + + if self.returncode: + raise MythError(MythError.SYSTEM,self.returncode,arg,self.stderr) + return stdout + + diff --git a/patches/MythData.py.orig b/patches/MythData.py.orig new file mode 100755 index 0000000..d1e4ac5 --- /dev/null +++ b/patches/MythData.py.orig @@ -0,0 +1,1504 @@ +# -*- coding: utf-8 -*- + +""" +Provides data access classes for accessing and managing MythTV data +""" + +from MythStatic import * +from MythBase import * + +import re, sys, socket, os +import xml.etree.cElementTree as etree +from time import mktime, strftime, strptime +from datetime import date, time, datetime +from socket import gethostbyaddr, gethostname + +#### FILE ACCESS #### + +def ftopen(file, type, forceremote=False, nooverwrite=False, db=None): + """ + ftopen(file, type, forceremote=False, nooverwrite=False, db=None) + -> FileTransfer object + -> file object + Method will attempt to open file locally, falling back to remote access + over mythprotocol if necessary. + 'forceremote' will force a FileTransfer object if possible. + 'file' takes a standard MythURI: + myth://@:/ + 'type' takes a 'r' or 'w' + 'nooverwrite' will refuse to open a file writable, if a local file is found. + """ + db = MythDBBase(db) + log = MythLog('Python File Transfer', db=db) + reuri = re.compile(\ + 'myth://((?P.*)@)?(?P[a-zA-Z0-9\.]*)(:[0-9]*)?/(?P.*)') + reip = re.compile('(?:\d{1,3}\.){3}\d{1,3}') + + if type not in ('r','w'): + raise TypeError("File I/O must be of type 'r' or 'w'") + + # process URI (myth://@[:]/) + match = reuri.match(file) + if match is None: + raise MythError('Invalid FileTransfer input string: '+file) + host = match.group('host') + filename = match.group('file') + sgroup = match.group('group') + if sgroup is None: + sgroup = 'Default' + + # get full system name + if reip.match(host): + c = db.cursor(log) + if c.execute("""SELECT hostname FROM settings + WHERE value='BackendServerIP' + AND data=%s""", host) == 0: + c.close() + raise MythDBError(MythError.DB_SETTING, 'BackendServerIP', backend) + host = c.fetchone()[0] + c.close() + + # user forced to remote access + if forceremote: + if (type == 'w') and (filename.find('/') != -1): + raise MythFileError(MythError.FILE_FAILED_WRITE, file, + 'attempting remote write outside base path') + return FileTransfer(host, filename, sgroup, type, db) + + sgs = db.getStorageGroup(groupname=sgroup) + + if type == 'w': + # prefer local storage always + for i in range(len(sgs)-1,-1,-1): + if not sgs[i].local: + sgs.pop(i) + else: + st = os.statvfs(sgs[i].dirname) + sgs[i].free = st[0]*st[3] + if len(sgs) > 1: + # choose path with most free space + sg = sgs.pop() + while len(sgs): + if sgs[0].free > sg.free: sg = sgs.pop() + else: sgs.pop() + # check that folder exists + if filename.find('/') != -1: + path = sg.dirname+filename.rsplit('/',1)[0] + if not os.access(path, os.F_OK): + os.makedirs(path) + if nooverwrite: + if os.access(sg.dirname+filename, os.F_OK): + raise MythDBError(MythError.FILE_FAILED_WRITE, file, + 'refusing to overwrite existing file') + log(log.FILE, 'Opening local file (w)', sg.dirname+filename) + return open(sg.dirname+filename, 'w') + elif len(sgs) == 1: + if filename.find('/') != -1: + path = sgs[0].dirname+filename.rsplit('/',1)[0] + if not os.access(path, os.F_OK): + os.makedirs(path) + if nooverwrite: + if os.access(sgs[0].dirname+filename, os.F_OK): + raise MythFileError(MythError.FILE_FAILED_WRITE, file, + 'refusing to overwrite existing file') + log(log.FILE, 'Opening local file (w)', sgs[0].dirname+filename) + return open(sgs[0].dirname+filename, 'w') + else: + if filename.find('/') != -1: + raise MythFileError(MythError.FILE_FAILED_WRITE, file, + 'attempting remote write outside base path') + return FileTransfer(host, filename, sgroup, 'w', db) + else: + # search for file in local directories + for sg in sgs: + if sg.local: + if os.access(sg.dirname+filename, os.F_OK): + # file found, open local + log(log.FILE, 'Opening local file (r)', + sg.dirname+filename) + return open(sg.dirname+filename, type) + # file not found, open remote + return FileTransfer(host, filename, sgroup, type, db) + +class FileTransfer( MythBEConn ): + """ + A connection to mythbackend intended for file transfers. + Emulates the primary functionality of the local 'file' object. + """ + logmodule = 'Python FileTransfer' + def __repr__(self): + return "" % \ + (self.sgroup, self.host, self.filename, \ + self.type, hex(id(self))) + + def __init__(self, host, filename, sgroup, type, db=None): + if type not in ('r','w'): + raise MythError("FileTransfer type must be read ('r') "+ + "or write ('w')") + + self.host = host + self.filename = filename + self.sgroup = sgroup + self.type = type + + self.tsize = 2**15 + self.tmax = 2**17 + self.count = 0 + self.step = 2**12 + + self.open = False + # create control socket + self.control = MythBEBase(host, 'Playback', db=db) + self.control.log.module = 'Python FileTransfer Control' + # continue normal Backend initialization + MythBEConn.__init__(self, host, type, db=db) + self.open = True + + def announce(self, type): + # replacement announce for Backend object + if type == 'w': + self.w = True + elif type == 'r': + self.w = False + res = self.backendCommand('ANN FileTransfer %s %d %d %s' \ + % (socket.gethostname(), self.w, False, + BACKEND_SEP.join(['-1',self.filename,self.sgroup]))) + if res.split(BACKEND_SEP)[0] != 'OK': + raise MythError(MythError.PROTO_ANNOUNCE, self.host, self.port, res) + else: + sp = res.split(BACKEND_SEP) + self.sockno = int(sp[1]) + self.pos = 0 + self.size = (int(sp[2]) + (int(sp[3])<0))*2**32 + int(sp[3]) + + def __del__(self): + if not self.open: + return + self.control.backendCommand('QUERY_FILETRANSFER '\ + +BACKEND_SEP.join([str(self.sockno), 'DONE'])) + self.socket.shutdown(1) + self.socket.close() + self.open = False + + def tell(self): + """FileTransfer.tell() -> current offset in file""" + return self.pos + + def close(self): + """FileTransfer.close() -> None""" + self.__del__() + + def rewind(self): + """FileTransfer.rewind() -> None""" + self.seek(0) + + def read(self, size): + """ + FileTransfer.read(size) -> string of characters + Requests over 128KB will be buffered internally. + """ + + def recv(self, size): + buff = '' + while len(buff) < size: + buff += self.socket.recv(size-len(buff)) + return buff + + if self.w: + raise MythFileError('attempting to read from a write-only socket') + if size == 0: + return '' + + final = self.pos+size + if final > self.size: + final = self.size + + buff = '' + while self.pos < final: + self.count += 1 + size = final - self.pos + if size > self.tsize: + size = self.tsize + res = self.control.backendCommand('QUERY_FILETRANSFER '+\ + BACKEND_SEP.join([ str(self.sockno), + 'REQUEST_BLOCK', + str(size)])) + #print '%s - %s' % (int(res), size) + if int(res) == size: + if (self.count == 10) and (self.tsize < self.tmax) : + self.count = 0 + self.tsize += self.step + else: + if int(res) == -1: + self.seek(self.pos) + continue + size = int(res) + self.count = 0 + self.tsize -= self.step + if self.tsize < self.step: + self.tsize = self.step + + buff += recv(self, size) + self.pos += size + return buff + + def write(self, data): + """ + FileTransfer.write(data) -> None + Requests over 128KB will be buffered internally + """ + if not self.w: + raise MythFileError('attempting to write to a read-only socket') + while len(data) > 0: + size = len(data) + if size > self.tsize: + buff = data[self.tsize:] + data = data[:self.tsize] + else: + buff = data + data = '' + self.pos += int(self.socket.send(data)) + self.control.backendCommand('QUERY_FILETRANSFER '+BACKEND_SEP\ + .join([str(self.sockno),'WRITE_BLOCK',str(size)])) + return + + + def seek(self, offset, whence=0): + """ + FileTransfer.seek(offset, whence=0) -> None + Seek 'offset' number of bytes + whence == 0 - from start of file + 1 - from current position + 2 - from end of file + """ + if whence == 0: + if offset < 0: + offset = 0 + elif offset > self.size: + offset = self.size + elif whence == 1: + if offset + self.pos < 0: + offset = -self.pos + elif offset + self.pos > self.size: + offset = self.size - self.pos + elif whence == 2: + if offset > 0: + offset = 0 + elif offset < -self.size: + offset = -self.size + whence = 0 + offset = self.size+offset + + curhigh,curlow = self.control.splitInt(self.pos) + offhigh,offlow = self.control.splitInt(offset) + + res = self.control.backendCommand('QUERY_FILETRANSFER '+BACKEND_SEP\ + .join([str(self.sockno),'SEEK',str(offhigh), + str(offlow),str(whence),str(curhigh), + str(curlow)])\ + ).split(BACKEND_SEP) + self.pos = self.control.joinInt(int(res[0]),int(res[1])) + +class FileOps( MythBEBase ): + __doc__ = MythBEBase.__doc__+""" + getRecording() - return a Program object for a recording + deleteRecording() - notify the backend to delete a recording + forgetRecording() - allow a recording to re-record + deleteFile() - notify the backend to delete a file + in a storage group + getHash() - return the hash of a file in a storage group + reschedule() - trigger a run of the scheduler + """ + logmodule = 'Python Backend FileOps' + + def getRecording(self, chanid, starttime): + """FileOps.getRecording(chanid, starttime) -> Program object""" + res = self.backendCommand('QUERY_RECORDING TIMESLOT %d %d' \ + % (chanid, starttime)).split(BACKEND_SEP) + if res[0] == 'ERROR': + return None + else: + return Program(res[1:], db=self.db) + + def deleteRecording(self, program, force=False): + """ + FileOps.deleteRecording(program, force=False) -> retcode + 'force' will force a delete even if the file cannot be found + retcode will be -1 on success, -2 on failure + """ + command = 'DELETE_RECORDING' + if force: + command = 'FORCE_DELETE_RECORDING' + return self.backendCommand(BACKEND_SEP.join(\ + [command,program.toString()])) + + def forgetRecording(self, program): + """FileOps.forgetRecording(program) -> None""" + self.backendCommand(BACKEND_SEP.join(['FORGET_RECORDING', + program.toString()])) + + def deleteFile(self, file, sgroup): + """FileOps.deleteFile(file, storagegroup) -> retcode""" + return self.backendCommand(BACKEND_SEP.join(\ + ['DELETE_FILE',file,sgroup])) + + def getHash(self, file, sgroup): + """FileOps.getHash(file, storagegroup) -> hash string""" + return self.backendCommand(BACKEND_SEP.join((\ + 'QUERY_FILE_HASH',file, sgroup))) + + def reschedule(self, recordid=-1): + """FileOps.reschedule() -> None""" + self.backendCommand('RESCHEDULE_RECORDINGS '+str(recordid)) + + +class FreeSpace( DictData ): + """Represents a FreeSpace entry.""" + field_order = [ 'host', 'path', 'islocal', + 'disknumber', 'sgroupid', 'blocksize', + 'ts_high', 'ts_low', 'us_high', + 'us_low'] + field_type = [3, 3, 2, 0, 0, 0, 0, 0, 0, 0] + def __str__(self): + return ""\ + % (self.path, self.host, hex(id(self))) + + def __repr__(self): + return str(self).encode('utf-8') + + def __init__(self, raw): + DictData.__init__(self, raw) + self.totalspace = self.joinInt(self.ts_high, self.ts_low) + self.usedspace = self.joinInt(self.us_high, self.us_low) + self.freespace = self.totalspace - self.usedspace + + +#### RECORDING ACCESS #### + +class Program( DictData ): + """Represents a program with all detail returned by the backend.""" + + # recstatus + TUNERBUSY = -8 + LOWDISKSPACE = -7 + CANCELLED = -6 + DELETED = -5 + ABORTED = -4 + RECORDED = -3 + RECORDING = -2 + WILLRECORD = -1 + UNKNOWN = 0 + DONTRECORD = 1 + PREVIOUSRECORDING = 2 + CURRENTRECORDING = 3 + EARLIERSHOWING = 4 + TOOMANYRECORDINGS = 5 + NOTLISTED = 6 + CONFLICT = 7 + LATERSHOWING = 8 + REPEAT = 9 + INACTIVE = 10 + NEVERRECORD = 11 + + field_order = [ 'title', 'subtitle', 'description', + 'category', 'chanid', 'channum', + 'callsign', 'channame', 'filename', + 'fs_high', 'fs_low', 'starttime', + 'endtime', 'duplicate', 'shareable', + 'findid', 'hostname', 'sourceid', + 'cardid', 'inputid', 'recpriority', + 'recstatus', 'recordid', 'rectype', + 'dupin', 'dupmethod', 'recstartts', + 'recendts', 'repeat', 'programflags', + 'recgroup', 'commfree', 'outputfilters', + 'seriesid', 'programid', 'lastmodified', + 'stars', 'airdate', 'hasairdate', + 'playgroup', 'recpriority2', 'parentid', + 'storagegroup', 'audio_props', 'video_props', + 'subtitle_type','year'] + field_type = [ 3, 3, 3, + 3, 0, 3, + 3, 3, 3, + 0, 0, 4, + 4, 0, 0, + 0, 3, 0, + 0, 0, 0, + 0, 0, 3, + 0, 0, 4, + 4, 0, 3, + 3, 0, 3, + 3, 3, 3, + 1, 3, 0, + 3, 0, 3, + 3, 0, 0, + 0, 0] + def __str__(self): + return u"" % (self.title, + self.starttime.strftime('%Y-%m-%d %H:%M:%S'), hex(id(self))) + + def __repr__(self): + return str(self).encode('utf-8') + + def __init__(self, raw=None, db=None, etree=None): + if raw: + DictData.__init__(self, raw) + elif etree: + xmldat = etree.attrib + xmldat.update(etree.find('Channel').attrib) + if etree.find('Recording'): + xmldat.update(etree.find('Recording').attrib) + + dat = {} + if etree.text: + dat['description'] = etree.text.strip() + for key in ('title','subTitle','seriesId','programId','airdate', + 'category','hostname','chanNum','callSign','playGroup', + 'recGroup','rectype','programFlags','chanId','recStatus', + 'commFree','stars'): + if key in xmldat: + dat[key.lower()] = xmldat[key] + for key in ('startTime','endTime','lastModified', + 'recStartTs','recEndTs'): + if key in xmldat: + dat[key.lower()] = str(int(mktime(strptime( + xmldat[key], '%Y-%m-%dT%H:%M:%S')))) + if 'fileSize' in xmldat: + dat['fs_high'],dat['fs_low'] = \ + self.splitInt(int(xmldat['fileSize'])) + + raw = [] + defs = (0,0,0,'',0) + for i in range(len(self.field_order)): + if self.field_order[i] in dat: + raw.append(dat[self.field_order[i]]) + else: + raw.append(defs[self.field_type[i]]) + DictData.__init__(self, raw) + else: + raise InputError("Either 'raw' or 'etree' must be provided") + self.db = MythDBBase(db) + self.filesize = self.joinInt(self.fs_high,self.fs_low) + + def toString(self): + """ + Program.toString() -> string representation + for use with backend protocol commands + """ + data = [] + for i in range(0,PROGRAM_FIELDS): + if self.data[self.field_order[i]] == None: + datum = '' + elif self.field_type[i] == 0: + datum = str(self.data[self.field_order[i]]) + elif self.field_type[i] == 1: + datum = locale.format("%0.6f", self.data[self.field_order[i]]) + elif self.field_type[i] == 2: + datum = str(int(self.data[self.field_order[i]])) + elif self.field_type[i] == 3: + datum = self.data[self.field_order[i]] + elif self.field_type[i] == 4: + datum = str(int(mktime(self.data[self.field_order[i]].\ + timetuple()))) + data.append(datum) + return BACKEND_SEP.join(data) + + def delete(self, force=False, rerecord=False): + """ + Program.delete(force=False, rerecord=False) -> retcode + Informs backend to delete recording and all relevent data. + 'force' forces a delete if the file cannot be found. + 'rerecord' sets the file as recordable in oldrecorded + """ + be = FileOps(db=self.db) + res = int(be.deleteRecording(self, force=force)) + if res < -1: + raise MythBEError('Failed to delete file') + if rerecord: + be.forgetRecording(self) + return res + + def getRecorded(self): + """Program.getRecorded() -> Recorded object""" + return Recorded((self.chanid,self.recstartts), db=self.db) + + def open(self, type='r'): + """Program.open(type='r') -> file or FileTransfer object""" + if type != 'r': + raise MythFileError(MythError.FILE_FAILED_WRITE, self.filename, + 'Program () objects cannot be opened for writing') + if not self.filename.startswith('myth://'): + self.filename = 'myth://%s/%s' % (self.hostname, self.filename) + return ftopen(self.filename, 'r') + +class Record( DBDataWrite ): + """ + Record(id=None, db=None, raw=None) -> Record object + """ + + kNotRecording = 0 + kSingleRecord = 1 + kTimeslotRecord = 2 + kChannelRecord = 3 + kAllRecord = 4 + kWeekslotRecord = 5 + kFindOneRecord = 6 + kOverrideRecord = 7 + kDontRecord = 8 + kFindDailyRecord = 9 + kFindWeeklyRecord = 10 + + table = 'record' + where = 'recordid=%s' + setwheredat = 'self.recordid,' + defaults = {'recordid':None, 'type':kAllRecord, 'title':u'Unknown', + 'subtitle':'', 'description':'', 'category':'', + 'station':'', 'seriesid':'', 'search':0, + 'last_record':datetime(1900,1,1), + 'next_record':datetime(1900,1,1), + 'last_delete':datetime(1900,1,1)} + logmodule = 'Python Record' + + def __str__(self): + if self.wheredat is None: + return u"" % hex(id(self)) + return u"" \ + % (self.title, self.type, hex(id(self))) + + def __repr__(self): + return str(self).encode('utf-8') + + def __init__(self, id=None, db=None, raw=None): + DBDataWrite.__init__(self, (id,), db, raw) + + def create(self, data=None): + """Record.create(data=None) -> Record object""" + self.wheredat = (DBDataWrite.create(self, data),) + self._pull() + FileOps(db=self.db).reschedule(self.recordid) + return self + + def update(self, *args, **keywords): + DBDataWrite.update(*args, **keywords) + FileOps(db=self.db).reschedule(self.recordid) + +class Recorded( DBDataWrite ): + """ + Recorded(data=None, db=None, raw=None) -> Recorded object + 'data' is a tuple containing (chanid, storagegroup) + """ + table = 'recorded' + where = 'chanid=%s AND starttime=%s' + setwheredat = 'self.chanid,self.starttime' + defaults = {'title':u'Unknown', 'subtitle':'', 'description':'', + 'category':'', 'hostname':'', 'bookmark':0, + 'editing':0, 'cutlist':0, 'autoexpire':0, + 'commflagged':0, 'recgroup':'Default', 'seriesid':'', + 'programid':'', 'lastmodified':'CURRENT_TIMESTAMP', + 'filesize':0, 'stars':0, 'previouslyshown':0, + 'preserve':0, 'bookmarkupdate':0, + 'findid':0, 'deletepending':0, 'transcoder':0, + 'timestretch':1, 'recpriority':0, 'playgroup':'Default', + 'profile':'No', 'duplicate':1, 'transcoded':0, + 'watched':0, 'storagegroup':'Default'} + logmodule = 'Python Recorded' + + class _Cast( DBDataCRef ): + table = 'people' + rtable = 'recordedcredits' + t_ref = 'person' + t_add = ['name'] + r_ref = 'person' + r_add = ['role'] + w_field = ['chanid','starttime'] + def __repr__(self): + if self.data is None: + self._fill() + if len(self.data) == 0: + return 'No cast' + cast = [] + for member in self.data: + cast.append(member.name) + return ', '.join(cast).encode('utf-8') + + class _Seek( DBDataRef ): + table = 'recordedseek' + wfield = ['chanid','starttime'] + + class _Markup( DBDataRef ): + table = 'recordedmarkup' + wfield = ['chanid','starttime'] + + def __str__(self): + if self.wheredat is None: + return u"" % hex(id(self)) + return u"" % (self.title, + self.starttime.strftime('%Y-%m-%d %H:%M:%S'), hex(id(self))) + + def __repr__(self): + return str(self).encode('utf-8') + + def __init__(self, data=None, db=None, raw=None): + DBDataWrite.__init__(self, data, db, raw) + if (data is not None) or (raw is not None): + self.cast = self._Cast(self.wheredat, self.db) + self.seek = self._Seek(self.wheredat, self.db) + self.markup = self._Markup(self.wheredat, self.db) + + def create(self, data=None): + """Recorded.create(data=None) -> Recorded object""" + DBDataWrite.create(self, data) + self.wheredat = (self.chanid,self.starttime) + self._pull() + self.cast = self._Cast(self.wheredat, self.db) + self.seek = self._Seek(self.wheredat, self.db) + self.markup = self._Markup(self.wheredat, self.db) + return self + + def delete(self, force=False, rerecord=False): + """ + Recorded.delete(force=False, rerecord=False) -> retcode + Informs backend to delete recording and all relevent data. + 'force' forces a delete if the file cannot be found. + 'rerecord' sets the file as recordable in oldrecorded + """ + return self.getProgram().delete(force, rerecord) + + def open(self, type='r'): + """Recorded.open(type='r') -> file or FileTransfer object""" + return ftopen("myth://%s@%s/%s" % ( self.storagegroup, \ + self.hostname,\ + self.basename), type, db=self.db) + + def getProgram(self): + """Recorded.getProgram() -> Program object""" + be = FileOps(db=self.db) + return be.getRecording(self.chanid, + int(self.starttime.strftime('%Y%m%d%H%M%S'))) + + def getRecordedProgram(self): + """Recorded.getRecordedProgram() -> RecordedProgram object""" + return RecordedProgram((self.chanid,self.progstart), db=self.db) + + def formatPath(self, path, replace=None): + """ + Recorded.formatPath(path, replace=None) -> formatted path string + 'path' string is formatted as per mythrename.pl + """ + for (tag, data) in (('T','title'), ('S','subtitle'), + ('R','description'), ('C','category'), + ('U','recgroup'), ('hn','hostname'), + ('c','chanid') ): + tmp = unicode(self[data]).replace('/','-') + path = path.replace('%'+tag, tmp) + for (data, pre) in ( ('starttime','%'), ('endtime','%e'), + ('progstart','%p'),('progend','%pe') ): + for (tag, format) in (('y','%y'),('Y','%Y'),('n','%m'),('m','%m'), + ('j','%d'),('d','%d'),('g','%I'),('G','%H'), + ('h','%I'),('H','%H'),('i','%M'),('s','%S'), + ('a','%p'),('A','%p') ): + path = path.replace(pre+tag, self[data].strftime(format)) + for (tag, format) in (('y','%y'),('Y','%Y'),('n','%m'),('m','%m'), + ('j','%d'),('d','%d')): + path = path.replace('%o'+tag, + self['originalairdate'].strftime(format)) + path = path.replace('%-','-') + path = path.replace('%%','%') + path += '.'+self['basename'].split('.')[-1] + + # clean up for windows + if replace is not None: + for char in ('\\',':','*','?','"','<','>','|'): + path = path.replace(char, replace) + return path + +class RecordedProgram( DBDataWrite ): + """ + RecordedProgram(data=None, db=None, raw=None) -> RecordedProgram object + 'data' is a tuple containing (chanid, storagegroup) + """ + table = 'recordedprogram' + where = 'chanid=%s AND starttime=%s' + setwheredat = 'self.chanid,self.starttime' + defaults = {'title':'', 'subtitle':'', + 'category':'', 'category_type':'', 'airdate':0, + 'stars':0, 'previouslyshown':0, 'title_pronounce':'', + 'stereo':0, 'subtitled':0, 'hdtv':0, + 'partnumber':0, 'closecaptioned':0, 'parttotal':0, + 'seriesid':'', 'originalairdate':'', 'showtype':u'', + 'colorcode':'', 'syndicatedepisodenumber':'', + 'programid':'', 'manualid':0, 'generic':0, + 'first':0, 'listingsource':0, 'last':0, + 'audioprop':u'','videoprop':u'', + 'subtitletypes':u''} + logmodule = 'Python RecordedProgram' + + def __str__(self): + if self.wheredat is None: + return u"" % hex(id(self)) + return u"" % (self.title, + self.starttime.strftime('%Y-%m-%d %H:%M:%S'), hex(id(self))) + + def __repr__(self): + return str(self).encode('utf-8') + + def create(self, data=None): + """RecordedProgram.create(data=None) -> RecordedProgram object""" + DBDataWrite.create(self, data) + self.wheredat = (self.chanid, self.starttime) + self._pull() + return self + +class OldRecorded( DBDataWrite ): + """ + OldRecorded(data=None, db=None, raw=None) -> OldRecorded object + 'data' is a tuple containing (chanid, storagegroup) + """ + table = 'oldrecorded' + where = 'chanid=%s AND starttime=%s' + setwheredat = 'self.chanid,self.starttime' + defaults = {'title':'', 'subtitle':'', + 'category':'', 'seriesid':'', 'programid':'', + 'findid':0, 'recordid':0, 'station':'', + 'rectype':0, 'duplicate':0, 'recstatus':-3, + 'reactivate':0, 'generic':0} + logmodule = 'Python OldRecorded' + + def __str__(self): + if self.wheredat is None: + return u"" % hex(id(self)) + return u"" % (self.title, + self.starttime.strftime('%Y-%m-%d %H:%M:%S'), hex(id(self))) + + def __repr__(self): + return str(self).encode('utf-8') + + def create(self, data=None): + """OldRecorded.create(data=None) -> OldRecorded object""" + DBDataWrite.create(self, data) + self.wheredat = (self.chanid, self.starttime) + self._pull() + return self + + def setDuplicate(self, record=False): + """ + OldRecorded.setDuplicate(record=False) -> None + Toggles re-recordability + """ + c = self.db.cursor(self.log) + c.execute("""UPDATE oldrecorded SET duplicate=%%s + WHERE %s""" % self.where, \ + tuple([record]+list(self.wheredat))) + FileOps(db=self.db).reschedule(0) + + def update(self, *args, **keywords): + """OldRecorded entries can not be altered""" + return + def delete(self): + """OldRecorded entries cannot be deleted""" + return + +class Job( DBDataWrite ): + """ + Job(id=None, chanid=None, starttime=None, db=None, raw=None) -> Job object + Can be initialized with a Job id, or chanid and starttime. + """ + # types + NONE = 0x0000 + SYSTEMJOB = 0x00ff + TRANSCODE = 0x0001 + COMMFLAG = 0x0002 + USERJOB = 0xff00 + USERJOB1 = 0x0100 + USERJOB2 = 0x0200 + USERJOB3 = 0x0300 + USERJOB4 = 0x0400 + # cmds + RUN = 0x0000 + PAUSE = 0x0001 + RESUME = 0x0002 + STOP = 0x0004 + RESTART = 0x0008 + # flags + NO_FLAGS = 0x0000 + USE_CUTLIST = 0x0001 + LIVE_REC = 0x0002 + EXTERNAL = 0x0004 + # statuses + UNKNOWN = 0x0000 + QUEUED = 0x0001 + PENDING = 0x0002 + STARTING = 0x0003 + RUNNING = 0x0004 + STOPPING = 0x0005 + PAUSED = 0x0006 + RETRY = 0x0007 + ERRORING = 0x0008 + ABORTING = 0x0008 + DONE = 0x0100 + FINISHED = 0x0110 + ABORTED = 0x0120 + ERRORED = 0x0130 + CANCELLED = 0x0140 + + table = 'jobqueue' + logmodule = 'Python Jobqueue' + defaults = {'id': None} + + def __str__(self): + if self.wheredat is None: + return u"" % hex(id(self)) + return u"" % (self.id, hex(id(self))) + + def __repr__(self): + return str(self).encode('utf-8') + + def __init__(self, id=None, chanid=None, starttime=None, \ + db=None, raw=None): + self.__dict__['where'] = 'id=%s' + self.__dict__['setwheredat'] = 'self.id,' + + if raw is not None: + DBDataWrite.__init__(self, None, db, raw) + elif id is not None: + DBDataWrite.__init__(self, (id,), db, None) + elif (chanid is not None) and (starttime is not None): + self.__dict__['where'] = 'chanid=%s AND starttime=%s' + DBDataWrite.__init__(self, (chanid,starttime), db, None) + else: + DBDataWrite.__init__(self, None, db, None) + + def create(self, data=None): + """Job.create(data=None) -> Job object""" + id = DBDataWrite.create(self, data) + self.where = 'id=%s' + self.wheredat = (id,) + return self + + def setComment(self,comment): + """Job.setComment(comment) -> None, updates comment""" + self.comment = comment + self.update() + + def setStatus(self,status): + """Job.setStatus(Status) -> None, updates status""" + self.status = status + self.update() + +class Channel( DBDataWrite ): + """Channel(chanid=None, data=None, raw=None) -> Channel object""" + table = 'channel' + where = 'chanid=%s' + setwheredat = 'self.chanid,' + defaults = {'icon':'none', 'videofilters':'', 'callsign':u'', + 'xmltvid':'', 'recpriority':0, 'contrast':32768, + 'brightness':32768, 'colour':32768, 'hue':32768, + 'tvformat':u'Default', 'visible':1, 'outputfilters':'', + 'useonairguide':0, 'atsc_major_chan':0, + 'tmoffset':0, 'default_authority':'', + 'commmethod':-1, 'atsc_minor_chan':0, + 'last_record':datetime(1900,1,1)} + logmodule = 'Python Channel' + + def __str__(self): + if self.wheredat is None: + return u"" % hex(id(self)) + return u"" % \ + (self.chanid, self.name, hex(id(self))) + + def __repr__(self): + return str(self).encode('utf-8') + + def __init__(self, chanid=None, db=None, raw=None): + DBDataWrite.__init__(self, (chanid,), db, raw) + + def create(self, data=None): + """Channel.create(data=None) -> Channel object""" + DBDataWrite.create(self, data) + self.wheredat = (self.chanid,) + self._pull() + return self + +class Guide( DBData ): + """ + Guide(data=None, db=None, raw=None) -> Guide object + Data is a tuple of (chanid, starttime). + """ + table = 'program' + where = 'chanid=%s AND starttime=%s' + setwheredat = 'self.chanid,self.starttime' + logmodule = 'Python Guide' + + def __str__(self): + if self.wheredat is None: + return u"" % hex(id(self)) + return u"" % (self.title, + self.starttime.strftime('%Y-%m-%d %H:%M:%S'), hex(id(self))) + + def __repr__(self): + return str(self).encode('utf-8') + + def __init__(self, data=None, db=None, raw=None, etree=None): + if etree: + db = MythDBBase(db) + dat = {'chanid':etree[0]} + attrib = etree[1].attrib + for key in ('title','subTitle','category','seriesId', + 'hostname','programId','airdate'): + if key in attrib: + dat[key.lower()] = attrib[key] + if 'stars' in attrib: + dat['stars'] = locale.atof(attrib['stars']) + if etree[1].text: + dat['description'] = etree[1].text.strip() + for key in ('startTime','endTime','lastModified'): + if key in attrib: + dat[key.lower()] = datetime.strptime( + attrib[key],'%Y-%m-%dT%H:%M:%S') + + raw = [] + for key in db.tablefields.program: + if key in dat: + raw.append(dat[key]) + else: + raw.append(None) + DBData.__init__(self, db=db, raw=raw) + else: + DBData.__init__(self, data=data, db=db, raw=raw) + + def record(self, type=Record.kAllRecord): + rec = Record(db=self.db) + for key in ('chanid','title','subtitle','description', 'category', + 'seriesid','programid'): + rec[key] = self[key] + + rec.startdate = self.starttime.date() + rec.starttime = self.starttime-datetime.combine(rec.startdate, time()) + rec.enddate = self.endtime.date() + rec.endtime = self.endtime-datetime.combine(rec.enddate, time()) + + rec.station = Channel(self.chanid, db=self.db).callsign + rec.type = type + return rec.create() + + +#### MYTHVIDEO #### + +class Video( DBDataWrite ): + """Video(id=None, db=None, raw=None) -> Video object""" + table = 'videometadata' + where = 'intid=%s' + setwheredat = 'self.intid,' + defaults = {'subtitle':u'', 'director':u'Unknown', + 'rating':u'NR', 'inetref':u'00000000', + 'year':1895, 'userrating':0.0, + 'length':0, 'showlevel':1, + 'coverfile':u'No Cover', 'host':u'', + 'intid':None, 'homepage':u'', + 'watched':False, 'category':'none', + 'browse':True, 'hash':u'', + 'season':0, 'episode':0, + 'releasedate':date(1,1,1), 'childid':-1, + 'insertdate': datetime.now()} + logmodule = 'Python Video' + schema_value = 'mythvideo.DBSchemaVer' + schema_local = MVSCHEMA_VERSION + schema_name = 'MythVideo' + category_map = [{'None':0},{0:'None'}] + + def _fill_cm(self, name=None, id=None): + if name: + if name not in self.category_map[0]: + c = self.db.cursor(self.log) + q1 = """SELECT intid FROM videocategory WHERE category=%s""" + q2 = """INSERT INTO videocategory SET category=%s""" + if c.execute(q1, name) == 0: + c.execute(q2, name) + c.execute(q1, name) + id = c.fetchone()[0] + self.category_map[0][name] = id + self.category_map[1][id] = name + + elif id: + if id not in self.category_map[1]: + c = self.db.cursor(self.log) + if c.execute("""SELECT category FROM videocategory + WHERE intid=%s""", id) == 0: + raise MythDBError('Invalid ID found in videometadata.category') + else: + name = c.fetchone()[0] + self.category_map[0][name] = id + self.category_map[1][id] = name + + def _pull(self): + DBDataWrite._pull(self) + self._fill_cm(id=self.category) + self.category = self.category_map[1][self.category] + + def _push(self): + name = self.category + self._fill_cm(name=name) + self.category = self.category_map[0][name] + DBDataWrite._push(self) + self.category = name + + def __repr__(self): + return str(self).encode('utf-8') + + def __str__(self): + if self.wheredat is None: + return u"" % hex(id(self)) + res = self.title + if self.season and self.episode: + res += u' - %dx%02d' % (self.season, self.episode) + if self.subtitle: + res += u' - '+self.subtitle + return u"