From e3174df9cf2f0c13528a8bde5cc32803ccc9ed9d Mon Sep 17 00:00:00 2001 From: elParaguayo Date: Sun, 31 Dec 2017 12:16:37 +0000 Subject: [PATCH] Alpha version of new service addon --- addon.xml | 10 +- default.py | 747 ++++++++------ fanart.jpg | Bin 89280 -> 0 bytes helper.py | 264 ++--- resources/language/English/strings.po | 8 + resources/lib/api/footballscoresapi.py | 84 -- resources/lib/fixtures.py | 322 ------ resources/lib/footballscores.py | 1289 ------------------------ resources/lib/league_tables.py | 293 ------ resources/lib/live_scores_detail.py | 383 ------- resources/lib/menu.py | 108 -- resources/lib/results.py | 332 ------ resources/lib/settings.py | 375 ++++--- resources/settings.xml | 13 +- 14 files changed, 827 insertions(+), 3401 deletions(-) delete mode 100644 fanart.jpg delete mode 100644 resources/lib/api/footballscoresapi.py delete mode 100644 resources/lib/fixtures.py delete mode 100755 resources/lib/footballscores.py delete mode 100644 resources/lib/league_tables.py delete mode 100644 resources/lib/live_scores_detail.py delete mode 100644 resources/lib/menu.py delete mode 100644 resources/lib/results.py diff --git a/addon.xml b/addon.xml index 17dbf36..e5d3684 100644 --- a/addon.xml +++ b/addon.xml @@ -2,21 +2,21 @@ - + - + - - + all diff --git a/default.py b/default.py index 70f6c87..082d58a 100644 --- a/default.py +++ b/default.py @@ -32,8 +32,8 @@ import xbmcaddon import xbmcgui -from resources.lib.footballscores import League from resources.lib.notificationqueue import NotificationQueue +from resources.lib.footballscores import FootballMatch # Set the addon environment _A_ = xbmcaddon.Addon("service.bbclivefootballscores") @@ -41,6 +41,9 @@ _SET_ = _A_.setSetting pluginPath = _A_.getAddonInfo("path") +SETTING_TEAMS = "watchedteams" +SETTING_LEAGUES = "watchedleagues" + # Set some constants # Define our images @@ -52,17 +55,10 @@ IMG_YELLOW = os.path.join(pluginPath, "resources", "media", "yellow-card.png") IMG_RED = os.path.join(pluginPath, "resources", "media", "red-card.png") -# Notification display time -n = int(_GET_("DisplayTime")) -NOTIFY_TIME = n * 1000 - -# Additional detail -d = _GET_("AdditionalDetail") == "true" -SHOW_GOALSCORER = _GET_("ShowGoalscorer") == "true" -SHOW_BOOKINGS = int(_GET_("ShowBookings")) -SHOW_YELLOW = bool(SHOW_BOOKINGS == 2) -SHOW_RED = bool(SHOW_BOOKINGS != 0) -DETAILED = all([d, any([SHOW_GOALSCORER, SHOW_BOOKINGS])]) +# What bookings to show +BOOKINGS = {0: "OFF", + 1: "RED ONLY", + 2: "ALL"} # STATUS_DICT object # Format is {status: [status text, image path]} @@ -71,6 +67,9 @@ "L": ["Latest", IMG_LATEST], "Fixture": ["Fixture", IMG_FIXTURE]} + +# Define core generic funcitons + def localise(id): '''Gets localised string. @@ -85,306 +84,458 @@ def debug(msg): Takes one argument: msg: debug message to send to XBMC log ''' - msg = u"bbclivefootballscores: {0}".format(msg).encode("ascii", "ignore") - xbmc.log(msg,xbmc.LOGDEBUG) - -def getSelectedLeagues(): - '''Returns list of leagues selected by user in settings file.''' - - # Try to get list of selected leagues from settings file - try: - - # Get the settings value and convert to a list - watchedleagues = json.loads(str(_GET_("watchedleagues"))) - - # if there's a problem - except: - - # Create an empty list (stops service from crashing) - watchedleagues = [] - - # Return this list - return watchedleagues + xbmc.log(msg, xbmc.LOGDEBUG) -def checkAlerts(): - '''Setting is "True" when alerts are disabled. - Returns boolean: - True: Service should display alerts - False: Alerts are disabled +class SettingsMonitor(xbmc.Monitor): + '''Handler to checking when settings are updated and triggering an + appropriate callback. ''' - return _GET_("Alerts") != "true" + def __init__(self, *args, **kwargs): + xbmc.Monitor.__init__(self) + self.action = kwargs['action'] -def checkNotificationDetailLevel(): - '''Sets certain constants to determine how much detail is required for - notifications. + def onSettingsChanged(self): + debug("Detected change in settings (NB may not be this addon)") + self.action() - Returns a tuple which should set the following variables: - SHOW_GOALSCORER - SHOW_BOOKINGS - SHOW_YELLOW - SHOW_RED - DETAILED - ''' - d = _GET_("AdditionalDetail") == "true" - SHOW_GOALSCORER = _GET_("ShowGoalscorer") == "true" - SHOW_BOOKINGS = int(_GET_("ShowBookings")) - SHOW_YELLOW = bool(SHOW_BOOKINGS == 2) - SHOW_RED = bool(SHOW_BOOKINGS != 0) - DETAILED = all([d, any([SHOW_GOALSCORER, SHOW_BOOKINGS])]) - - return SHOW_GOALSCORER, SHOW_BOOKINGS, SHOW_YELLOW, SHOW_RED, DETAILED - -def serviceRunning(): - '''User should be able to deactivate alerts (rather than deactivating - the service) via setting. - - Returns a boolean as to whether we should provide alerts or not. - ''' - return True +class FootballScoresService(object): + '''Class definition for the football scoress service. -def updateWatchedLeagues(matchdict, selectedleagues): - '''Updates our active leagues to make sure that we're just looking at the - leagues that the user wants. + Service will run on starting Kodi and will stop on exit. - Takes 2 arguments: - matchdict: dictionary of leagues being watched - selectedleagues: list of league IDs chosen by user - - Returns updated dictionary object. + Settings are read on starting the service. A separate monitor instance is + needed to update settings if a user changes them while the script is + running. ''' - # Build a list of leagues selected by user that are not in - # our current dictionary - newleagues = [l for l in selectedleagues if l not in matchdict] + def __init__(self): + '''Initialises the service object but does not start it. - # Build a list of leagues in our dictionary that are no longer in - # list of leagues selected by users - removedleagues = [l for l in matchdict if l not in selectedleagues] + Reads settings and defines the necessary variables and objects. + ''' + debug("Initialisings service...") - # Loop through new leagues - for l in newleagues: + # Create a notification queue object for handling notifications + debug("Creating queue") + self.queue = NotificationQueue() - # Add a League object to the dictioanary + # Define required constants and objects + self.leaguedict = {} + self.teamdict = {} + self.ticker = "" + self.SHOW_ALERTS = True + # self.SHOW_GOALSCORER = -1 + # self.SHOW_BOOKINGS = -1 + # self.SHOW_YELLOW = -1 + # self.SHOW_RED = -1 + # self.DETAILED = -1 + self.NOTIFY_TIME = 5000 + + # Read the addon settings + self.getSettings() + + + + # Create a settings monitor + debug("Starting settings monitor...") + self.monitor = SettingsMonitor(action=self.getSettings) + + # Clear old tickers + # debug("Clearing tickers") + # self.checkTickers() + + def Notify(self, subject, message, image=None, timeout=2000): + '''Displays match notification. + + Take 4 arguments: + subject: subject line + message: message line + image: path to icon + timeoute: display time in milliseconds + ''' + self.queue.add(subject, message, image, timeout) + + def _goal(self, event): + + subject = u"GOAL! ({})".format(event.Scorer.AbbreviatedName) + message = u"{}".format(unicode(event.match)) + + self.Notify(subject, message, IMG_GOAL, timeout=self.NOTIFY_TIME) + + def _status(self, event): + + subject = u"{}".format(event.match.LongStatus) + message = u"{}".format(unicode(event.match)) + + self.Notify(subject, message, IMG_FT, timeout=self.NOTIFY_TIME) + + def _fixture(self, event): + + subject = u"New Match" + message = u"{}".format(unicode(event.match)) + + self.Notify(subject, message, IMG_FIXTURE, timeout=self.NOTIFY_TIME) + + def _red(self, event): + + subject = u"Red Card! ({})".format(event.RedCard.AbbreviatedName) + message = u"{}".format(unicode(event.match)) + + self.Notify(subject, message, IMG_RED, timeout=self.NOTIFY_TIME) + + def getSettings(self): + '''Reads the addon settings and updates the scipt settings accordingly. + + This method should be handled by a monitor instance so that any + changes made to settings while the service is running are also + updated. + ''' + debug("Checking settings...") + self.updateWatchedTeams() + # self.checkAlerts() + # self.checkNotificationDetailLevel() + # self.updateWatchedLeagues() + # self.checkNotificationTime() + + def checkNotificationTime(self): + '''Sets the length of time for which notifications should be + displayed. + ''' + # try: + # n = int(_GET_("DisplayTime")) + # except ValueError: + # # Default is 2 seconds + # n = 2 + + n = 2 + + if n != (self.NOTIFY_TIME / 1000): + debug("Notification time now {0} seconds".format(n)) + self.NOTIFY_TIME = n * 1000 + + # def checkAlerts(self): + # '''Setting is "True" when alerts are disabled. + # ''' + # alerts = _GET_("Alerts") != "true" + # + # if alerts != self.SHOW_ALERTS: + # debug("Alerts now {0}.".format("ON" if alerts else "OFF")) + # self.SHOW_ALERTS = alerts + # + # def checkNotificationDetailLevel(self): + # '''Sets certain constants to determine how much detail is required for + # notifications. + # ''' + # d = _GET_("AdditionalDetail") == "true" + # + # gs = _GET_("ShowGoalscorer") == "true" + # if gs != self.SHOW_GOALSCORER: + # debug("Goal scorer alerts now {0}.".format("ON" if gs else "OFF")) + # self.SHOW_GOALSCORER = gs + # + # try: + # bk = int(_GET_("ShowBookings")) + # except ValueError: + # bk = 0 + # + # if bk != self.SHOW_BOOKINGS: + # debug("Bookings are now {0}.".format(BOOKINGS[bk])) + # self.SHOW_YELLOW = bool(bk == 2) + # self.SHOW_RED = bool(bk != 0) + # self.SHOW_BOOKINGS = bk + # + # dt = all([d, any([self.SHOW_GOALSCORER, self.SHOW_BOOKINGS])]) + # + # if dt != self.DETAILED: + # level = "ON" if dt else "OFF" + # debug("Showing additional detail is now {0}.".format(level)) + # self.DETAILED = dt + + def updateWatchedTeams(self): + + selected_teams = self.getUserSelected(SETTING_TEAMS) + + newteams = [t for t in selected_teams if t not in self.teamdict] + removedteams = [t for t in self.teamdict if t not in selectedteams] + + for team in newteams: + + obj = FootballMatch(team, + on_goal=self._goal, + on_red=self._red, + on_status_change=self._status, + on_new_match=self._fixture) + + self.teamdict[team] = obj + + # Loop through leaues to be removed + for l in removedteams: + + # Remove league from the dictionary + self.teamdict.pop(l) + + + # def updateWatchedLeagues(self): + # '''Updates our active leagues to make sure that we're just looking at + # the leagues that the user wants. + # ''' + # selectedleagues = self.getSelectedLeagues() + # + # # Build a list of leagues selected by user that are not in + # # our current dictionary + # newleagues = [l for l in selectedleagues if l not in self.matchdict] + # + # # Build a list of leagues in our dictionary that are no longer in + # # list of leagues selected by users + # removedleagues = [l for l in self.matchdict if l not in selectedleagues] + # + # # Loop through new leagues + # for l in newleagues: + # + # # Add a League object to the dictioanary + # try: + # self.matchdict[l] = League(l, detailed=self.DETAILED) + # except TypeError: + # pass + # + # # Loop through leaues to be removed + # for l in removedleagues: + # + # # Remove league from the dictionary + # self.matchdict.pop(l) + # + # if newleagues: + # debug("Added new leagues: {0}".format(newleagues)) + # + # if removedleagues: + # debug("Removed leagues: {0}".format(removedleagues)) + # + # if newleagues or removedleagues: + # debug(u"LeagueList - {0}".format(self.matchdict)) + + def getUserSelected(self, setting): + '''Returns list of leagues selected by user in settings file.''' + + # Try to get list of selected leagues from settings file try: - matchdict[l] = League(l, detailed=DETAILED) - except TypeError: - pass - - # Loop through leaues to be removed - for l in removedleagues: - - # Remove league from the dictionary - matchdict.pop(l) - - # Return the dictionary - return matchdict - -def Notify(subject, message, image=None, timeout=2000): - '''Displays match notification. - - Take 4 arguments: - subject: subject line - message: message line - image: path to icon - timeoute: display time in milliseconds - ''' - queue.add(subject, message, image, timeout) - -def checkMatch(match): - '''Look at the match and work out what notification we want to show. - - Takes one argument: - match: footballscores.FootballMatch object - ''' - - if match.booking: - - # Should we show notification? - if (SHOW_YELLOW and DETAILED): - yellow = u" {1} ({0})".format(*match.LastYellowCard) - else: - yellow = None - - Notify(u"YELLOW!{0}".format(yellow if yellow else u""), - str(match), - IMG_YELLOW, - timeout=NOTIFY_TIME) - debug(u"Yellow Card: %s" % (unicode(match))) - - if match.redcard: - - # Should we show notification? - if (SHOW_RED and DETAILED): - red = u" {1} ({0})".format(*match.LastRedCard) - else: - red = None - - Notify(u"RED!{0}".format(red if red else u""), - str(match), - IMG_RED, - timeout=NOTIFY_TIME) - debug(u"Red Card: %s" % (unicode(match))) - - # Has there been a goal? - if match.Goal: - - # Gooooooooooooooooooooooooooooollllllllllllllll! - - # Should we show goalscorer? - if (SHOW_GOALSCORER and DETAILED): - scorer = u" {0}".format(match.LastGoalScorer[1]) - else: - scorer = None - - Notify(u"GOAL!{0}".format(scorer if scorer else u""), - str(match), - IMG_GOAL, - timeout=NOTIFY_TIME) - debug(u"GOAL: %s" % (unicode(match))) - - - # Has the status changed? e.g. kick-off, half-time, full-time? - if match.StatusChanged: - - # Get the relevant status info - info = STATUS_DICT.get(match.status, STATUS_DICT["Fixture"]) - - # Send the notification - Notify(info[0], unicode(match), info[1], timeout=NOTIFY_TIME) - debug(u"STATUS: {0}".format(unicode(match))) - -def checkTickers(): - - try: - tickers = json.loads(_GET_("currenttickers")) - except ValueError: - tickers = {} - - d = [] - - for k in tickers: - - w = xbmcgui.Window(int(k)) - try: - c = w.getControl(tickers[k]) - except RuntimeError: - d.append(k) - - for k in d: - tickers.pop(k) - - _SET_("currenttickers", json.dumps(tickers)) - - -def updateTickers(text): - - try: - tickers = json.loads(_GET_("currenttickers")) - except ValueError: - tickers = {} - - for k in tickers: - - w = xbmcgui.Window(int(k)) - c = w.getControl(tickers[k]) - c.reset() - c.addLabel(text) - - - _SET_("ticker", text) - - -def doUpdates(matchdict): - '''Main function to updated leagues and check matches for updates. - - Takes one argument: - matchdict: dictionary of leagues being watchedleagues - - Returns updated dictionary - ''' - - ticker = u"" - - # Loop through each league that we're following - for league in matchdict: - - # Make sure we only get additional information if we need it. - for m in matchdict[league].LeagueMatches: - m.detailed = DETAILED - - # Get the league to update each match - matchdict[league].Update() - - if matchdict[league]: - if ticker: - ticker += " " - ticker += u"[B]{0}[/B]: ".format(matchdict[league].LeagueName) - ticker += u", ".join(unicode(m) for m in matchdict[league].LeagueMatches) - - # Loop through the matches - for match in matchdict[league].LeagueMatches: - - # and check it for updates - checkMatch(match) - - debug(ticker) - updateTickers(ticker) - # xbmc.executebuiltin(u"skin.setstring(tickertext,{0})".format(ticker)) - - # Return the updated dicitonary object - return matchdict - - -# Script starts here. -# Let's get some initial data before we enter main service loop - -# Build dictionary of leagues we want to follow -matchdict = updateWatchedLeagues({}, getSelectedLeagues()) -debug(u"LeagueList - {0}".format(matchdict)) - -# Check if we need to show alerts or not. -alerts = checkAlerts() - -# Clear old tickers -checkTickers() - -# Variable for counting loop iterations -i = 0 - -# Create a notification queue object -queue = NotificationQueue() - -# Main service loop - need to exit script cleanly if XBMC is shutting down -debug("Entering main loop...") -while not xbmc.abortRequested: - - # 5 seconds before we do main update, let's check and see if there are any - # new leagues that we need to follow. - # Also, check whether the user has enabled/disabled alerts - if i == 11: - matchdict = updateWatchedLeagues(matchdict, getSelectedLeagues()) - alerts = checkAlerts() - (SHOW_GOALSCORER, - SHOW_BOOKINGS, - SHOW_YELLOW, - SHOW_RED, - DETAILED) = checkNotificationDetailLevel() - - # If user wants alerts and we've reached ou desired loop number... - if alerts and not i: - - # Update our match dictionary and check for updates. - debug("Checking scores...") - matchdict = doUpdates(matchdict) - - # Sleep for 5 seconds (if this is longer, XBMC may not shut down cleanly.) - xbmc.sleep(5000) - # Increment our counter - # 12 x 5000 = 60,000 i.e. scores update every 1 minute - # Currently hard-coded - may look to change this. - i = (i + 1) % 12 + # Get the settings value and convert to a list + selection = json.loads(str(_GET_(setting))) + + # if there's a problem + except: + + # Create an empty list (stops service from crashing) + selection = [] + + # Return this list + return selection + + # def checkMatch(self, match): + # '''Look at the match and work out what notification we want to show. + # + # Takes one argument: + # match: footballscores.FootballMatch object + # ''' + # + # if match.booking: + # + # # Should we show notification? + # if (self.SHOW_YELLOW and self.DETAILED): + # debug(u"yellow card: {0}".format(match.LastYellowCard)) + # try: + # yellow = u" {1} ({0})".format(*match.LastYellowCard) + # except AttributeError: + # yellow = None + # else: + # yellow = None + # + # if self.SHOW_YELLOW: + # + # self.Notify(u"YELLOW!{0}".format(yellow if yellow else u""), + # unicode(match), + # IMG_YELLOW, + # timeout=self.NOTIFY_TIME) + # debug(u"Yellow Card: {0}, {1}".format(match, yellow)) + # + # if match.redcard: + # + # # Should we show notification? + # if (self.SHOW_RED and self.DETAILED): + # debug(u"red card: {0}".format(match.LastRedCard)) + # try: + # red = u" {1} ({0})".format(*match.LastRedCard) + # except AttributeError: + # red = None + # else: + # red = None + # + # if self.SHOW_RED: + # + # self.Notify(u"RED!{0}".format(red if red else u""), + # unicode(match), + # IMG_RED, + # timeout=self.NOTIFY_TIME) + # debug(u"Red Card: {0}, {1}".format(match, red)) + # + # # Has there been a goal? + # if match.Goal: + # + # # Gooooooooooooooooooooooooooooollllllllllllllll! + # + # # Should we show goalscorer? + # if (self.SHOW_GOALSCORER and self.DETAILED): + # debug(u"goalscorer: {0}".format(match.LastGoalScorer)) + # try: + # scorer = u" {0}".format(match.LastGoalScorer[1]) + # except AttributeError: + # scorer = None + # else: + # scorer = None + # + # self.Notify(u"GOAL!{0}".format(scorer if scorer else u""), + # unicode(match), + # IMG_GOAL, + # timeout=self.NOTIFY_TIME) + # debug(u"GOAL: {0}, {1}".format(match, scorer)) + # + # # Has the status changed? e.g. kick-off, half-time, full-time? + # if match.StatusChanged: + # + # # Get the relevant status info + # info = STATUS_DICT.get(match.status, STATUS_DICT["Fixture"]) + # + # # Send the notification + # self.Notify(info[0], unicode(match), info[1], + # timeout=self.NOTIFY_TIME) + # debug(u"STATUS: {0}".format(unicode(match))) + + # def checkTickers(self): + # '''Tickers are not a class property because they are implemented by a + # separate script. + # + # We therefore need to manually maintain a list of which windows have + # tickers and check to see it's correct. + # + # If a ticker cannot be found (i.e. it was implemented in a separate + # Kodi session) then it needs to be removed from the list. + # ''' + # try: + # tickers = json.loads(_GET_("currenttickers")) + # except ValueError: + # tickers = {} + # + # d = [] + # + # for k in tickers: + # + # w = xbmcgui.Window(int(k)) + # try: + # c = w.getControl(tickers[k]) + # except RuntimeError: + # d.append(k) + # + # for k in d: + # tickers.pop(k) + # + # _SET_("currenttickers", json.dumps(tickers)) + # + # def updateTickers(self): + # '''Updates the ticker text on all known tickers.''' + # + # try: + # tickers = json.loads(_GET_("currenttickers")) + # except ValueError: + # tickers = {} + # + # for k in tickers: + # + # w = xbmcgui.Window(int(k)) + # c = w.getControl(tickers[k]) + # c.reset() + # c.addLabel(self.ticker.decode("utf-8").replace("|", ",")) + + def doUpdates(self): + + for team in self.teamdict: + self.teamdict[team].update() + + # + # ticker = u"" + # + # # Loop through each league that we're following + # for league in self.matchdict: + # + # # Make sure we only get additional information if we need it. + # for m in self.matchdict[league].LeagueMatches: + # m.detailed = self.DETAILED + # + # # Get the league to update each match + # self.matchdict[league].Update() + # + # if self.matchdict[league]: + # if ticker: + # ticker += u" " + # lgn = u"[B]{0}[/B]: ".format(self.matchdict[league].LeagueName) + # mtc = u", ".join(unicode(m) for m + # in self.matchdict[league].LeagueMatches) + # ticker += lgn + # ticker += mtc + # + # # If we're showing alerts then let's check each match for updates + # if self.SHOW_ALERTS: + # + # # Loop through the matches + # for match in self.matchdict[league].LeagueMatches: + # + # # and check it for updates + # self.checkMatch(match) + # + # # If there have been any changes then we need to update the tickers + # if ticker != self.ticker: + # debug(u"Ticker: {0}".format(ticker)) + # self.ticker = ticker.replace(",", "|").encode("utf-8") + # xbmc.executebuiltin("Skin.SetString(bbcscorestickertext, {0})".format(self.ticker)) + # self.updateTickers() + + def run(self): + '''Method to start the service. + + Service runs in a loop which is terminated when the user exits Kodi. + ''' + + # Variable for counting loop iterations + i = 0 + + # Main service loop - need to exit script cleanly if XBMC is shutting + # down + debug("Entering main loop...") + while not xbmc.abortRequested: + + # If user wants alerts and we've reached our desired loop number... + if self.SHOW_ALERTS and not i: + + # Update our match dictionary and check for updates. + debug("Checking scores...") + self.doUpdates() + + # Sleep for 5 seconds + # (if this is longer, XBMC may not shut down cleanly.) + xbmc.sleep(5000) + + # Increment our counter + # 12 x 5000 = 60,000 i.e. scores update every 1 minute + # Currently hard-coded - may look to change this. + i = (i + 1) % 12 + + +if __name__ == "__main__": + scores_service = FootballScoresService() + scores_service.run() + + # Clean exit + scores_service = None diff --git a/fanart.jpg b/fanart.jpg deleted file mode 100644 index 9f124279f4f6f6e1f113e68cb84651f59dffa06c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 89280 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 diff --git a/helper.py b/helper.py index bc7137c..abefbec 100644 --- a/helper.py +++ b/helper.py @@ -38,138 +38,152 @@ import xbmcaddon # Import service specific objects -from resources.lib.settings import selectLeagues, toggleNotification -from resources.lib.league_tables import XBMCLeagueTable -from resources.lib.live_scores_detail import XBMCLiveScoresDetail -from resources.lib.results import XBMCResults -from resources.lib.fixtures import XBMCFixtures -from resources.lib.utils import closeAddonSettings -from resources.lib.menu import FootballHelperMenu -from resources.lib.ticker import TickerOverlay - -# Import PyXBMCt module. -from pyxbmct.addonwindow import * +from resources.lib.settings import selectTeams +# from resources.lib.league_tables import XBMCLeagueTable +# from resources.lib.live_scores_detail import XBMCLiveScoresDetail +# from resources.lib.results import XBMCResults +# from resources.lib.fixtures import XBMCFixtures +# from resources.lib.utils import closeAddonSettings +# from resources.lib.menu import FootballHelperMenu +# from resources.lib.ticker import TickerOverlay +# +# # Import PyXBMCt module. +# from pyxbmct.addonwindow import * _A_ = xbmcaddon.Addon("service.bbclivefootballscores") _GET_ = _A_.getSetting _SET_ = _A_.setSetting -getwin = {"jsonrpc":"2.0", - "id":1, - "method":"GUI.GetProperties", - "params": - {"properties":["currentwindow"]} - } - -def ToggleTicker(): - - try: - tickers = json.loads(_GET_("currenttickers")) - except ValueError: - tickers = {} - - if not tickers: - tickers = {} - - # Get the current window ID - current_window = xbmc.executeJSONRPC(json.dumps(getwin)) - window_id = json.loads(current_window)["result"]["currentwindow"]["id"] - - # json doesn't like integer keys so we need to look for a unicode object - key = unicode(window_id) - - if key in tickers: - - # There's already a ticker on this window - # Remove it from our list but get the ID of the ticker first - tickerid = tickers.pop(key) - - # Get the window - w = xbmcgui.Window(window_id) - - # Find the ticker - t = w.getControl(tickerid) - - # and remove it - w.removeControl(t) - - else: - - # No ticker, so create one - ScoreTicker = TickerOverlay(window_id) - - # Show it - ScoreTicker.show() - - # Give it current text - tickertext = _GET_("ticker") - ScoreTicker.update(tickertext) - - # Add to our list of current active tickers - tickers[ScoreTicker.windowid] = ScoreTicker.id - - # Save our list - _SET_("currenttickers", json.dumps(tickers)) - +# getwin = {"jsonrpc":"2.0", +# "id":1, +# "method":"GUI.GetProperties", +# "params": +# {"properties":["currentwindow"]} +# } +# +# def ToggleTicker(): +# +# try: +# tickers = json.loads(_GET_("currenttickers")) +# except ValueError: +# tickers = {} +# +# if not tickers: +# tickers = {} +# +# # Get the current window ID +# current_window = xbmc.executeJSONRPC(json.dumps(getwin)) +# window_id = json.loads(current_window)["result"]["currentwindow"]["id"] +# +# # json doesn't like integer keys so we need to look for a unicode object +# key = unicode(window_id) +# +# if key in tickers: +# +# # There's already a ticker on this window +# # Remove it from our list but get the ID of the ticker first +# tickerid = tickers.pop(key) +# +# # Get the window +# w = xbmcgui.Window(window_id) +# +# # Find the ticker +# t = w.getControl(tickerid) +# +# # and remove it +# w.removeControl(t) +# +# else: +# +# # No ticker, so create one +# ScoreTicker = TickerOverlay(window_id) +# +# # Show it +# ScoreTicker.show() +# +# # Give it current text +# tickertext = xbmc.getInfoLabel("Skin.String(bbcscorestickertext)") +# tickertext = tickertext.decode("utf-8").replace("|", ",") +# ScoreTicker.update(unicode(tickertext)) +# +# # Add to our list of current active tickers +# tickers[ScoreTicker.windowid] = ScoreTicker.id +# +# # Save our list +# _SET_("currenttickers", json.dumps(tickers)) +# try: params = dict((x.split("=") for x in sys.argv[1].lower().split(";"))) except (ValueError, AttributeError, IndexError): params = {} - -# If no parameters are passed then we show default menu -if not params: - - menu = FootballHelperMenu() - menu.show() - - +# +# # If no parameters are passed then we show default menu +# if not params: +# +# menu = FootballHelperMenu() +# menu.show() +# menu = None +# +# # If there are parameters, let's see what we want to do... -if params.get("mode") == "selectleague": - - selectLeagues() - -elif params.get("mode") == "leaguetable": - - # Close addon setting window (if open) - closeAddonSettings() - - # Create an instance of the XBMC League Table - xlt = XBMCLeagueTable() - - # and display it! - xlt.start() - -elif params.get("mode") == "matchdetail": - - # Close addon setting window (if open) - closeAddonSettings() - - # Create an instance of the XBMC League Table - xlsd = XBMCLiveScoresDetail() - - # and display it! - xlsd.start() - -elif params.get("mode") == "results": - # Close addon setting window (if open) - closeAddonSettings() - - # Create an instance of the XBMC Results - xr = XBMCResults() - - # and display it! - xr.start() - -elif params.get("mode") == "fixtures": - # Close addon setting window (if open) - closeAddonSettings() - - # Create an instance of the XBMC Fixtures - xf = XBMCFixtures() - - # and display it! - xf.start() - -elif params.get("mode") == "toggleticker": - - ToggleTicker() +if params.get("mode") == "selectteams": + + selectTeams() +# +# elif params.get("mode") == "leaguetable": +# +# # Close addon setting window (if open) +# closeAddonSettings() +# +# # Create an instance of the XBMC League Table +# xlt = XBMCLeagueTable() +# +# # and display it! +# xlt.start() +# +# # Get rid of it when we're finished +# xlt = None +# +# elif params.get("mode") == "matchdetail": +# +# # Close addon setting window (if open) +# closeAddonSettings() +# +# # Create an instance of the XBMC League Table +# xlsd = XBMCLiveScoresDetail() +# +# # and display it! +# xlsd.start() +# +# # Get rid of it when we're finished +# xlsd = None +# +# elif params.get("mode") == "results": +# # Close addon setting window (if open) +# closeAddonSettings() +# +# # Create an instance of the XBMC Results +# xr = XBMCResults() +# +# # and display it! +# xr.start() +# +# # Get rid of it when we're finished +# xr = None +# +# elif params.get("mode") == "fixtures": +# # Close addon setting window (if open) +# closeAddonSettings() +# +# # Create an instance of the XBMC Fixtures +# xf = XBMCFixtures() +# +# # and display it! +# xf.start() +# +# # Get rid of it when we're finished +# xf = None +# +# elif params.get("mode") == "toggleticker": +# +# ToggleTicker() diff --git a/resources/language/English/strings.po b/resources/language/English/strings.po index 6e01a5e..772e58c 100644 --- a/resources/language/English/strings.po +++ b/resources/language/English/strings.po @@ -34,6 +34,14 @@ msgctxt "#32003" msgid "Notification display time (secs)" msgstr "" +msgctxt "#32004" +msgid "Select teams..." +msgstr "" + +msgctxt "#32005" +msgid "You must press 'ok' after selecting teams to save the selection" +msgstr "" + #empty ids from 32003 through 32019 #settings script messages diff --git a/resources/lib/api/footballscoresapi.py b/resources/lib/api/footballscoresapi.py deleted file mode 100644 index 3eaefeb..0000000 --- a/resources/lib/api/footballscoresapi.py +++ /dev/null @@ -1,84 +0,0 @@ -''' - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . -''' - -''' This script is part of the BBC Football Scores service by elParaguayo - -''' -import sys - -if sys.version_info >= (2, 7): - import json as json -else: - import simplejson as json - -import xbmc -import xbmcgui -import xbmcaddon - -class LiveScoresAPI(object): - - def __init__(self): - - self.__addon = xbmcaddon.Addon("service.bbclivefootballscores") - self._getS = self.__addon.getSetting - - def __loadLeagues(self): - '''See if there are any previously selected leagues. - - Returns list of league IDs. - ''' - - try: - watchedleagues = json.loads(str(self._getS("watchedleagues"))) - except: - watchedleagues = [] - - return watchedleagues - - def __saveLeagues(self, leagues): - '''Converts list to JSON compatible string and saves it to our - user's settings. - ''' - - rawdata = json.dumps(leagues) - self.__addon.setSetting(id="watchedleagues",value=rawdata) - - def isFollowing(self, leagueID): - - return leagueID in self.__loadLeagues() - - def addLeague(self, leagueID): - - all = self.__loadLeagues() - - if not leagueID in all: - all.append(leagueID) - self.__saveLeagues(all) - return True - - else: - return False - - def removeLeague(self, leagueID): - - all = self.__loadLeagues() - - if leagueID in all: - all.remove(leagueID) - self.__saveLeagues(all) - return True - - else: - return False diff --git a/resources/lib/fixtures.py b/resources/lib/fixtures.py deleted file mode 100644 index 1db95ca..0000000 --- a/resources/lib/fixtures.py +++ /dev/null @@ -1,322 +0,0 @@ -''' - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . -''' - -''' This script is part of the BBC Football Scores service by elParaguayo - - It allows users to select which leagues they wish to receive updates - for. - - It is called via the script configuration screen or by passing - parameters to trigger specific functions. - - The script accepts the following parameters: - toggle: Turns score notifications on and off - reset: Resets watched league data - - NB only one parameter should be passed at a time. -''' -import sys - -if sys.version_info >= (2, 7): - import json as json - from collections import OrderedDict -else: - import simplejson as json - from resources.lib.ordereddict import OrderedDict - -import xbmc -import xbmcgui -import xbmcaddon - -from resources.lib.footballscores import Fixtures -from resources.lib.utils import closeAddonSettings - -# Import PyXBMCt module. -from pyxbmct.addonwindow import * - -_A_ = xbmcaddon.Addon("service.bbclivefootballscores") -_S_ = _A_.getSetting - -def localise(id): - '''Gets localised string. - - Shamelessly copied from service.xbmc.versioncheck - ''' - string = _A_.getLocalizedString(id).encode( 'utf-8', 'ignore' ) - return string - -class XBMCFixtures(object): - - def __init__(self): - - # It may take a bit of time to display menus/fixtures so let's - # make sure the user knows what's going on - self.prog = xbmcgui.DialogProgressBG() - self.prog.create(localise(32114), localise(32107)) - - # variables for league fixtures display - self.redraw = False - self.offset = 0 - self.menu = True - self.leagueid = 0 - - # variables for root menu - self.showall = True - self.active = True - - # Get our favourite leagues - self.watchedleagues = json.loads(str(_S_("watchedleagues"))) - - # Create a Fixtures instance - self.fixtures = Fixtures() - - self.prog.update(25, localise(32108)) - - allcomps = self.fixtures.getCompetitions() - - allcomps = [x for x in allcomps if x["id"][:11] == "competition"] - - # Get all of the available leagues, store it in an Ordered Dict - # key=League name - # value=League ID - self.allleagues = OrderedDict((x["name"], - x["id"][-9:]) - for x in allcomps) - - self.prog.update(75, localise(32109)) - - # Create a similar Ordered Dict for just those leagues that we're - # currently followin - lgid = x["id"][-9:] - self.watchedleagues = OrderedDict((x["name"], x["id"][-9:]) - for x in allcomps - if (unicode(lgid).isnumeric() and - int(lgid) in self.watchedleagues)) - - self.prog.close() - - - def showMenu(self, all_leagues=False): - - # Setting this to False means that the menu won't display if - # we hit escape - self.active = False - - # Set the title and menu size - window = AddonDialogWindow(localise(32100)) - window.setGeometry(450,300,5,4) - - # Create a List object - self.leaguelist = List() - - # Get the appropriate list of leagues depending on what mode we're in - displaylist = self.allleagues if self.showall else self.watchedleagues - - #self.prog.update(92) - - # Add the List to the menu - window.placeControl(self.leaguelist, 0, 0, rowspan=4, columnspan=4) - self.leaguelist.addItems(displaylist.keys()) - - # Bind the list action - p = self.leaguelist.getSelectedPosition - window.connect(self.leaguelist, - lambda w = window: - self.setID(self.leaguelist.getListItem(p()).getLabel(), - w)) - - # Don't think these are needed, but what the hell... - window.connect(ACTION_PREVIOUS_MENU, lambda w=window: self.finish(w)) - window.connect(ACTION_NAV_BACK, lambda w=window: self.finish(w)) - - #self.prog.update(94) - - # Create the button to toggle mode - leaguetext = localise(32101) if self.showall else localise(32102) - self.leaguebutton = Button(leaguetext) - window.placeControl(self.leaguebutton, 4, 0, columnspan=2) - - # Bind the button - window.connect(self.leaguebutton, lambda w=window: self.toggleMode(w)) - - #self.prog.update(96) - - # Add the close button - self.closebutton = Button(localise(32103)) - window.placeControl(self.closebutton, 4, 2, columnspan=2) - window.setFocus(self.leaguelist) - # Connect the button to a function. - window.connect(self.closebutton, lambda w=window:self.finish(w)) - - #self.prog.update(98) - - # Handle navigation to make user experience better - self.leaguelist.controlLeft(self.leaguebutton) - self.leaguelist.controlRight(self.closebutton) - self.closebutton.controlUp(self.leaguelist) - self.closebutton.controlLeft(self.leaguebutton) - self.leaguebutton.controlRight(self.closebutton) - self.leaguebutton.controlUp(self.leaguelist) - - # Ready to go... - window.doModal() - - def showFixtures(self): - - # Basic variables - self.redraw = False - - # If there are multiple fixture dates for a competition - # Let's just get the required one - fixtures = self.rawdata[self.offset] - - #self.prog.update(92) - - # How many fixtures are there on this date? - # We'll need this to set the size of the display - n = len(fixtures["fixtures"]) - - # Create a window instance and size it - window = AddonDialogWindow(fixtures["date"]) - window.setGeometry(450, (n + 4) * 30, n + 3, 11) - - #self.prog.update(94) - - # Add the teams - for i,f in enumerate(fixtures["fixtures"]): - - home = Label(u"{hometeam}".format(**f), alignment=1) - vlab = Label("v", alignment=2) - away = Label(u"{awayteam}".format(**f)) - - window.placeControl(home, i+1, 0, columnspan=5) - window.placeControl(vlab, i+1, 5) - window.placeControl(away, i+1, 6, columnspan=5) - - - #self.prog.update(94) - - # Add the close button - closebutton = Button(localise(32103)) - window.placeControl(closebutton, n+2, 4, columnspan=3) - window.setFocus(closebutton) - # Connect the button to a function. - window.connect(closebutton, lambda w=window: self.finish(w)) - - # Not sure we need these... - window.connect(ACTION_PREVIOUS_MENU, lambda w=window: self.finish(w)) - window.connect(ACTION_NAV_BACK, lambda w=window: self.finish(w)) - - #self.prog.update(96) - - # We may need some extra buttons - nextbutton = Button(localise(32104)) - prevbutton = Button(localise(32105)) - - # There are more fixtures after the ones we're showing - if self.offset < (len(self.rawdata) - 1): - window.placeControl(nextbutton, n+2, 7, columnspan=4) - window.connect(nextbutton, lambda w=window: self.next(w)) - nextbutton.controlLeft(closebutton) - closebutton.controlRight(nextbutton) - - # There are more fixtures before the ones we're showing - if self.offset > 0: - window.placeControl(prevbutton, n+2, 0, columnspan=4) - window.connect(prevbutton, lambda w=window: self.previous(w)) - prevbutton.controlRight(closebutton) - closebutton.controlLeft(prevbutton) - - #self.prog.close() - - # Ready to go... - window.doModal() - - def getFixturesData(self, ID): - - self.prog.create(localise(32114), localise(32115)) - try: - raw = self.fixtures.getFixtures("competition-%s" - % (self.leagueid)) - - except: - print "ERROR" - raw = None - - self.prog.close() - - return raw - - def setID(self, ID, w): - # Gets the ID of the selected league - ID = self.allleagues[ID] - self.setleague(ID,w) - - def next(self,w): - # Display the next fixture day in the competion - self.offset += 1 - self.redraw = True - w.close() - - def previous(self,w): - # Display the previous fixture day the competition - self.offset -= 1 - self.redraw = True - w.close() - - def finish(self,w): - # We're done. Gracefully close down menu. - self.redraw = False - self.menu = False - self.active = False - w.close() - - def setleague(self,lg, w): - # Set up the variables to display the league fixtures - self.leagueid = lg - self.offset = 0 - self.redraw = True - w.close() - self.rawdata = self.getFixturesData(self.leagueid) - self.prog.update(90) - - def toggleMode(self,w): - # Toggle between showing all competitions and just our favourites - self.showall = not self.showall - self.active = True - w.close() - - def start(self): - - # Let's begin - while self.active: - # Show the main menu - self.showMenu() - - while self.redraw: - - # Show fixtures - self.showFixtures() - -if __name__ == "__main__": - - # Close addon setting window (if open) - closeAddonSettings() - - # Create an instance of the XBMC Fixtures - xf = XBMCFixtures() - - # and display it! - xf.start() diff --git a/resources/lib/footballscores.py b/resources/lib/footballscores.py deleted file mode 100755 index 70bfc08..0000000 --- a/resources/lib/footballscores.py +++ /dev/null @@ -1,1289 +0,0 @@ -''' - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . -''' - -import urllib2 -import string -from BeautifulSoup import BeautifulSoup -import re -from datetime import datetime, time -import json -import codecs - -__version__ = "0.3.0" - - -class matchcommon(object): - '''class for common functions for match classes.''' - - livescoreslink = ("http://www.bbc.co.uk/sport/shared/football/" - "live-scores/matches/{comp}/today") - - def getPage(self, url, sendresponse = False): - page = None - try: - user_agent = ('Mozilla/5.0 (Windows; U; Windows NT 6.1; ' - 'en-US; rv:1.9.1.5) Gecko/20091102 Firefox') - headers = { 'User-Agent' : user_agent } - request = urllib2.Request(url) - response = urllib2.urlopen(request) - page = response.read() - except: - pass - - if sendresponse: - return response - else: - # Fixed this line to handle accented team namess - return codecs.decode(page, "utf-8") if page else None - -class FootballMatch(matchcommon): - '''Class for getting details of individual football matches. - Data is pulled from BBC live scores page. - ''' - # self.accordionlink = "http://polling.bbc.co.uk/sport/shared/football/accordion/partial/collated" - - detailprefix = ("http://www.bbc.co.uk/sport/football/live/" - "partial/{id}") - - def __init__(self, team, detailed = False, data = None): - '''Creates an instance of the Match object. - Must be created by passing the name of one team. - - data - User can also send data to the class e.g. if multiple instances - of class are being run thereby saving http requests. Otherwise class - can handle request on its own. - - detailed - Do we want additional data (e.g. goal scorers, bookings)? - ''' - self.detailed = detailed - - # Set the relevant urls - self.detailedmatchpage = None - self.scorelink = None - - # Boolean to notify user if there is a valid match - self.matchfound = False - - # Which team am I following? - self.myteam = team - - self.__resetMatch() - - # Let's try and load some data - data = self.__loadData(data) - - # If our team is found or we have data - if data: - - # Update the class properties - self.__update(data) - # No notifications for now - self.goal = False - self.statuschange = False - self.newmatch = False - - def __getUKTime(self): - #api.geonames.org/timezoneJSON?formatted=true&lat=51.51&lng=0.13&username=demo&style=full - rawbbctime = self.getPage("http://api.geonames.org/timezoneJSON" - "?formatted=true&lat=51.51&lng=0.13&" - "username=elParaguayo&style=full") - - bbctime = json.loads(rawbbctime).get("time") if rawbbctime else None - - if bbctime: - servertime = datetime.strptime(bbctime, - "%Y-%m-%d %H:%M") - return servertime - - else: - - return None - - def __resetMatch(self): - '''Clear all variables''' - self.hometeam = None - self.awayteam = None - self.homescore = None - self.awayscore = None - self.scorelink = None - self.homescorers = None - self.awayscorers = None - self.homeyellowcards = [] - self.awayyellowcards = [] - self.homeredcards = [] - self.awayredcards = [] - self.competition = None - self.matchtime = None - self.status = None - self.goal = False - self.statuschange = False - self.newmatch = False - self.homebadge = None - self.awaybadge = None - self.matchid = None - self.matchlink = None - self.rawincidents = [] - self.booking = False - self.redcard = False - self.leagueid = None - - - def __findMatch(self): - leaguepage = self.getPage(self.livescoreslink.format(comp="")) - data = None - teamfound = False - - if leaguepage: - - # Start with the default page so we can get list of active leagues - raw = BeautifulSoup(leaguepage) - - # Find the list of active leagues - selection = raw.find("div", {"class": - "drop-down-filter live-scores-fixtures"}) - - - # Loop throught the active leagues - for option in selection.findAll("option"): - - # Build the link for that competition - league = option.get("value")[12:] - - if league: - scorelink = self.livescoreslink.format(comp=league) - - scorepage = self.getPage(scorelink) - - if scorepage: - # Prepare to process page - optionhtml = BeautifulSoup(scorepage) - - # We just want the live games... - live = optionhtml.find("div", {"id": "matches-wrapper"}) - - # Let's look for our team - if live.find(text=self.myteam): - teamfound = True - self.scorelink = scorelink - self.competition = option.text.split("(")[0].strip() - self.leagueid = league - data = live - break - - self.matchfound = teamfound - - return data - - def __getScores(self, data, update = False): - - for match in data.findAll("tr", {"id": re.compile(r'^match-row')}): - if match.find(text=self.myteam): - - self.hometeam = match.find("span", {"class": "team-home"}).text ## ENCODE - - self.awayteam = match.find("span", {"class": "team-away"}).text - - linkrow = match.find("td", {"class": "match-link"}) - try: - link = linkrow.find("a").get("href") - self.matchlink = "http://www.bbc.co.uk%s" % (link) - except AttributeError: - self.matchlink = None - - if match.get("class") == "fixture": - status = "Fixture" - matchtime = match.find("span", - {"class": - "elapsed-time"}).text.strip()[:5] - - elif match.get("class") == "report": - status = "FT" - matchtime = None - - elif ("%s" % - (match.find("span", - {"class": "elapsed-time"}).text.strip()) == "Half Time"): - status = "HT" - matchtime = None - - else: - status = "L" - matchtime = match.find("span", - {"class": "elapsed-time"}).text.strip() - - matchid = match.get("id")[10:] - - score = match.find("span", - {"class": "score"}).text.strip().split(" - ") - - try: - homescore = int(score[0].strip()) - awayscore = int(score[1].strip()) - - except: - homescore = 0 - awayscore = 0 - - self.statuschange = False - self.newmatch = False - self.goal=False - - if update: - - if not status == self.status: - self.statuschange = True - - if not matchid == self.matchid: - self.newmatch = True - - if not (homescore == self.homescore and - awayscore == self.awayscore): - # Gooooooooooooaaaaaaaaaaaaaaaaallllllllllllllllll! - self.goal = True - - self.status = status if status else None ## ENCODE - self.matchtime = matchtime if matchtime else None ## ENCODE - self.matchid = matchid if matchid else None ## ENCODE - self.homescore = homescore - self.awayscore = awayscore - - - def __update(self, data = None): - - self.__getScores(data) - - if self.detailed: - self.__getDetails() - - def __loadData(self, data = None): - - self.matchfound = False - - if data: - if data.find(text=self.myteam): - self.matchfound = True - else: - data = None - - if not data and self.scorelink: - scorepage = self.getPage(self.scorelink) - if scorepage: - scorehtml = BeautifulSoup(scorepage) - data = scorehtml.find("div", {"id": "matches-wrapper"}) - if data.find(text=self.myteam): - self.matchfound = True - else: - data = None - else: - data = None - - if not data: - data = self.__findMatch() - - if not data: - self.__resetMatch() - - return data - - def Update(self, data = None): - - data = self.__loadData(data) - - if data: - self.__getScores(data, update = True) - - if self.detailed: - self.__getDetails() - - def __getDetails(self): - - if self.matchid: - # Prepare bautiful soup to scrape match page - - - # Let's get the home and away team detail sections - try: - bs = BeautifulSoup(self.getPage(self.detailprefix.format( - id=self.matchid))) - incidents = bs.find("table", - {"class": "incidents-table"}).findAll("tr") - except: - incidents = None - - # Get incidents - # This populates variables with details of scorers and bookings - # Incidents are stored in a list of tuples: format is: - # [(Player Name, [times of incidents])] - hsc = [] - asc = [] - hyc = [] - ayc = [] - hrc = [] - arc = [] - - if incidents: - - self.__goalscorers = [] - self.__yellowcards = [] - self.__redcards = [] - - for incident in incidents: - i = incident.find("td", - {"class": - re.compile(r"\bincident-type \b")}) - if i: - h = incident.find("td", - {"class": - "incident-player-home"}).text.strip() - - a = incident.find("td", - {"class": - "incident-player-away"}).text.strip() - - t = incident.find("td", - {"class": - "incident-time"}).text.strip() - - if "goal" in i.get("class"): - if h: - hsc = self.__addIncident(hsc, h, t) ## ENCODE - self.__goalscorers.append((self.hometeam, h, t)) - self.__addRawIncident("home", "goal", h, t) - else: - asc = self.__addIncident(asc, a, t) - self.__goalscorers.append((self.awayteam, a, t)) - self.__addRawIncident("away", "goal", a, t) - - elif "yellow-card" in i.get("class"): - if h: - hyc = self.__addIncident(hyc, h, t) - self.__yellowcards.append((self.hometeam, h, t)) - self.__addRawIncident("home", "yellow", h, t) - else: - ayc = self.__addIncident(ayc, a, t) - self.__yellowcards.append((self.awayteam, a, t)) - self.__addRawIncident("away", "yellow", a, t) - - elif "red-card" in i.get("class"): - if h: - hrc = self.__addIncident(hrc, h, t) - self.__redcards.append((self.hometeam, h, t)) - self.__addRawIncident("home", "red", h, t) - else: - arc = self.__addIncident(arc, a, t) - self.__redcards.append((self.awayteam, a, t)) - self.__addRawIncident("away", "red", a, t) - - self.booking = not (self.homeyellowcards == hyc and - self.awayyellowcards == ayc) - - self.redcard = not (self.homeredcards == hrc and - self.awayredcards == arc) - - self.homescorers = hsc - self.awayscorers = asc - self.homeyellowcards = hyc - self.awayyellowcards = ayc - self.homeredcards = hrc - self.awayredcards = arc - - def __addIncident(self, incidentlist, player, incidenttime): - '''method to add incident to list variable''' - found = False - for incident in incidentlist: - if incident[0] == player: - incident[1].append(incidenttime) - found = True - break - - if not found: - incidentlist.append((player, [incidenttime])) - - return incidentlist - - def __addRawIncident(self, team, incidenttype, player, incidenttime): - - incident = (team, incidenttype, player, incidenttime) - - if not incident in self.rawincidents: - self.rawincidents.append(incident) - - def formatIncidents(self, incidentlist, newline = False): - '''Incidents are in the following format: - List: - [Tuple: - (Player name, [list of times of incidents])] - - This function converts the list into a string. - ''' - temp = [] - incidentjoin = "\n" if newline else ", " - - for incident in incidentlist: - temp.append("%s (%s)" % (incident[0], - ", ".join(incident[1]))) - - return incidentjoin.join(temp) - - def getTeamBadges(self): - found = False - - if self.matchlink: - badgepage = self.getPage(self.matchlink) - if badgepage: - linkpage = BeautifulSoup(badgepage) - badges = linkpage.findAll("div", {"class": "team-badge"}) - if badges: - self.homebadge = badges[0].find("img").get("src") - self.awaybadge = badges[1].find("img").get("src") - found = True - - return found - - - def __nonzero__(self): - - return self.matchfound - - def __repr__(self): - - return "FootballMatch(\'%s\', detailed=%s)" % (self.myteam, - self.detailed) - - def __eq__(self, other): - if isinstance(other, self.__class__): - if not self.matchid is None: - return self.matchid == other.matchid - else: - return self.myteam == other.myteam - else: - return False - - # Neater functions to return data: - - @property - def HomeTeam(self): - """Returns string of the home team's name - - """ - return self.hometeam - - @property - def AwayTeam(self): - """Returns string of the away team's name - - """ - return self.awayteam - - @property - def HomeScore(self): - """Returns the number of goals scored by the home team - - """ - return self.homescore - - @property - def AwayScore(self): - """Returns the number of goals scored by the away team - - """ - return self.awayscore - - @property - def Competition(self): - """Returns the name of the competition to which the match belongs - - e.g. "Premier League", "FA Cup" etc - - """ - return self.competition - - @property - def Status(self): - """Returns the status of the match - - e.g. "L", "HT", "FT" - - """ - if self.status == "Fixture": - return self.matchtime - else: - return self.status - - @property - def Goal(self): - """Boolean. Returns True if score has changed since last update - - """ - return self.goal - - @property - def StatusChanged(self): - """Boolean. Returns True if status has changed since last update - - e.g. Match started, half-time started etc - - """ - return self.statuschange - - @property - def NewMatch(self): - """Boolean. Returns True if the match found since last update - - """ - return self.newmatch - - @property - def MatchFound(self): - """Boolean. Returns True if a match is found in JSON feed - - """ - return self.matchfound - - @property - def HomeBadge(self): - """Returns link to image for home team's badge - - """ - return self.homebadge - - @property - def AwayBadge(self): - """Returns link to image for away team's badge - - """ - return self.awaybadge - - @property - def HomeScorers(self): - """Returns list of goalscorers for home team - - """ - return self.homescorers - - @property - def AwayScorers(self): - """Returns list of goalscorers for away team - - """ - return self.awayscorers - - @property - def HomeYellowCards(self): - """Returns list of players receiving yellow cards for home team - - """ - return self.homeyellowcards - - @property - def AwayYellowCards(self): - """Returns list of players receiving yellow cards for away team - - """ - return self.awayyellowcards - - @property - def HomeRedCards(self): - """Returns list of players sent off for home team - - """ - return self.homeredcards - - @property - def AwayRedCards(self): - """Returns list of players sent off for away team - - """ - return self.awayredcards - - @property - def LastGoalScorer(self): - if self.detailed: - if self.__goalscorers: - return self.__goalscorers[-1] - else: - return None - else: - return None - - @property - def LastYellowCard(self): - if self.detailed: - if self.__yellowcards: - return self.__yellowcards[-1] - else: - return None - else: - return None - - @property - def LastRedCard(self): - if self.detailed: - if self.__redcards: - return self.__redcards[-1] - else: - return None - else: - return None - - @property - def MatchDate(self): - """Returns date of match i.e. today's date - - """ - d = datetime.now() - datestring = "%s %d %s" % ( - d.strftime("%A"), - d.day, - d.strftime("%B %Y") - ) - return datestring - - @property - def MatchTime(self): - """If detailed info available, returns match time in minutes. - - If not, returns Status. - - """ - if self.status=="L" and self.matchtime is not None: - return self.matchtime - else: - return self.Status - - def abbreviate(self, cut): - """Returns short formatted summary of match but team names are - truncated according to the cut parameter. - - e.g. abbreviate(3): - "Ars 1-1 Che (L)" - - Should handle accented characters. - - """ - return u"%s %s-%s %s (%s)" % ( - self.hometeam[:cut], - self.homescore, - self.awayscore, - self.awayteam[:cut], - self.Status - ) - - def __unicode__(self): - """Returns short formatted summary of match. - - e.g. "Arsenal 1-1 Chelsea (L)" - - Should handle accented characters. - - """ - if self.matchfound: - - return u"%s %s-%s %s (%s)" % ( - self.hometeam, - self.homescore, - self.awayscore, - self.awayteam, - self.Status - ) - - else: - - return u"%s are not playing today." % (self.myteam) - - def __str__(self): - """Returns short formatted summary of match. - - e.g. "Arsenal 1-1 Chelsea (L)" - - """ - return unicode(self).encode('utf-8') - - @property - def PrintDetail(self): - """Returns detailed summary of match (if available). - - e.g. "(L) Arsenal 1-1 Chelsea (Arsenal: Wilshere 10', - Chelsea: Lampard 48')" - """ - if self.detailed: - hscore = False - scorerstring = "" - - if self.homescorers or self.awayscorers: - scorerstring = " (" - if self.homescorers: - hscore = True - scorerstring += "%s: %s" % (self.hometeam, - self.formatIncidents(self.homescorers)) - - - if self.awayscorers: - if hscore: - scorerstring += " - " - scorerstring += "%s: %s" % (self.awayteam, - self.formatIncidents(self.awayscorers)) - - scorerstring += ")" - - return "(%s) %s %s-%s %s%s" % ( - self.MatchTime, - self.hometeam, - self.homescore, - self.awayscore, - self.awayteam, - scorerstring - ) - else: - return self.__str__() - - @property - def TimeToKickOff(self): - '''Returns a timedelta object for the time until the match kicks off. - - Returns None if unable to parse match time or if match in progress. - - Should be unaffected by timezones as it gets current time from bbc - server which *should* be the same timezone as matches shown. - ''' - if self.status == "Fixture": - try: - koh = int(self.matchtime[:2]) - kom = int(self.matchtime[3:5]) - kickoff = datetime.combine( - datetime.now().date(), - time(koh, kom, 0)) - timetokickoff = kickoff - self.__getUKTime() - except Exception, e: - timetokickoff = None - finally: - pass - else: - timetokickoff = None - - return timetokickoff - - @property - def matchdict(self): - return {"hometeam": self.hometeam, - "awayteam": self.awayteam, - "status": self.status, - "matchtime": self.MatchTime, - "homescore": self.homescore, - "awayscore": self.awayscore, - "homescorers": self.homescorers, - "awayscorers": self.awayscorers, - "homeyellow": self.homeyellowcards, - "awayyellow": self.awayyellowcards, - "homered": self.homeredcards, - "awayred": self.awayredcards, - "incidentlist": self.rawincidents} - - - -class League(matchcommon): - '''Get summary of matches for a given league. - - NOTE: this may need to be updated as currently uses the accordion - source data whereas main Match module uses more complete source. - ''' - - accordionlink = ("http://polling.bbc.co.uk/sport/shared/football/" - "accordion/partial/collated") - - def __init__(self, league, detailed=False): - - self.__leaguematches = self.__getMatches(league,detailed=detailed) - self.__leagueid = league - self.__leaguename = self.__getLeagueName(league) - self.__detailed = detailed - - def __getData(self, league): - - scorelink = self.livescoreslink.format(comp=league) - data = None - # Prepare to process page - optionpage = self.getPage(scorelink) - if optionpage: - optionhtml = BeautifulSoup(optionpage) - - # We just want the live games... - data = optionhtml.find("div", {"id": "matches-wrapper"}) - - return data - - def __getLeagueName(self, league): - - leaguename = None - rawpage = self.getPage(self.livescoreslink.format(comp=league)) - - if rawpage: - raw = BeautifulSoup(rawpage) - - # Find the list of active leagues - selection = raw.find("div", - {"class": - "drop-down-filter live-scores-fixtures"}) - - if selection: - - selectedleague = selection.find("option", - {"selected": "selected"}) - - if selectedleague: - leaguename = selectedleague.text.split("(")[0].strip() - - return leaguename - - - @staticmethod - def getLeagues(): - leagues = [] - # raw = BeautifulSoup(self.getPage(self.accordionlink)) - # # Loop through all the competitions being played today - # for option in raw.findAll("option"): - # league = {} - # league["name"] = option.text - # league["id"] = option.get("value") - # leagues.append(league) - - # return leagues - livescoreslink = matchcommon().livescoreslink - - # Start with the default page so we can get list of active leagues - rawpage = matchcommon().getPage(livescoreslink.format(comp="")) - if rawpage: - raw = BeautifulSoup(rawpage) - - # Find the list of active leagues - selection = raw.find("div", - {"class": - "drop-down-filter live-scores-fixtures"}) - - # Loop throught the active leagues - for option in selection.findAll("option"): - - # Build the link for that competition - # league = option.get("value")[12:] - league = {} - league["name"] = option.text.split("(")[0].strip() - league["id"] = option.get("value")[12:].strip() - if league["id"]: - leagues.append(league) - - return leagues - - def __getMatches(self, league, detailed=False, data = None): - - if data is None: - data = self.__getData(league) - - matches = [] - if data: - rawmatches = data.findAll("tr", {"id": re.compile(r'^match-row')}) - else: - rawmatches = None - - if rawmatches: - - for match in rawmatches: - team = match.find("span", {"class": "team-home"}).text - m = FootballMatch(team, detailed=detailed, data=data) - matches.append(m) - - return matches - - def __repr__(self): - return "League(\'%s\', detailed=%s)" % (self.__leagueid, - self.__detailed) - - def __str__(self): - if self.__leaguematches: - if len(self.__leaguematches) == 1: - matches = "(1 match)" - else: - matches = "(%d matches)" % (len(self.__leaguematches)) - return "%s %s" % (self.__leaguename, matches) - else: - return None - - def __nonzero__(self): - return bool(self.__leaguematches) - - def Update(self): - '''Updates all matches in the league. - - If there are no games (e.g. a new day) then the old macthes are removed. - - If there are new games, these are added. - ''' - - # Get the data for league - data = self.__getData(self.__leagueid) - - # We've found some data so let's process - if data: - # Get a list of the current matches from the new data - currentmatches = self.__getMatches(self.__leagueid, data=data) - - # If the match is already in our league, then we keep it - self.__leaguematches = [m for m in self.__leaguematches if m in currentmatches] - - # Check if there are any matches in the new data which aren't in our list - newmatches = [m for m in currentmatches if m not in self.__leaguematches] - - # If so... - if newmatches: - # If we want detailed info on each match - if self.__detailed: - for m in newmatches: - - # then we need to update the flag for that match - m.detailed = True - - # and add it to our list - self.__leaguematches.append(m) - else: - # If not, then we can just add the new matches to our list - self.__leaguematches += newmatches - - # If we've got matches in our list - if self.__leaguematches: - for match in self.__leaguematches: - - # Update the matches - # NB we need to update each match to ensure the "Goal" - # flag is updated appropriately, rather than just adding a new match - # object. - match.Update(data=data) - - else: - # If there's no data, there are no matches... - self.__leaguematches = [] - - # If we haven't managed to set the league name yet - # then we should be able to find it if there are some matches - if self.__leaguematches and self.LeagueName is None: - self.LeagueName = self.__getLeagueName(self.__leagueid) - - @property - def LeagueMatches(self): - return self.__leaguematches - - @property - def LeagueName(self): - return self.__leaguename - - @property - def LeagueID(self): - return self.__leagueid - -class LeagueTable(matchcommon): - '''class to convert BBC league table format into python list/dict.''' - - leaguebase = "http://www.bbc.co.uk/sport/football/tables" - leaguemethod = "filter" - - def __init__(self): - #self.availableLeague = self.getLeagues() - pass - - def getLeagues(self): - '''method for getting list of available leagues''' - - leaguelist = [] - raw = BeautifulSoup(self.getPage(self.leaguebase)) - form = raw.find("div", {"class": "drop-down-filter", - "id": "filter-fixtures-no-js"}) - self.leaguemethod = form.find("select").get("name") - leagues = form.findAll("option") - for league in leagues: - l = {} - if league.get("value") != "" and not league.get("value").endswith("competition-"): - l["name"] = league.text - l["id"] = league.get("value") - leaguelist.append(l) - return leaguelist - - def getLeagueTable(self, leagueid): - '''method for creating league table of selected league.''' - - result = [] - - class LeagueTableTeam(object): - - def __init__(self, team): - - f = team.find - mov = re.compile(r"no-movement|moving-up|moving-down") - movmap = {"No movement": "same", - "Moving up": "up", - "Moving down": "down"} - self.name = f("td", {"class": "team-name"}).text - self.movement = movmap.get(f("span", {"class": mov}).text) - self.position = int(f("span", - {"class": "position-number"}).text) - self.played = int(f("td", {"class": "played"}).text) - self.won = int(f("td", {"class": "won"}).text) - self.drawn = int(f("td", {"class": "drawn"}).text) - self.lost = int(f("td", {"class": "lost"}).text) - self.goalsfor = int(f("td", {"class": "for"}).text) - self.goalsagainst = int(f("td", {"class": "against"}).text) - self.goaldifference = int(f("td", - {"class": "goal-difference"}).text) - self.points = int(f("td", {"class": "points"}).text) - - try: - lastgames = f("td", {"class": "last-10-games"}) - lg = [] - for game in lastgames.findAll("li"): - g = {} - g["result"] = game.get("class") - g["score"] = game.get("data-result") - g["opponent"] = game.get("data-against") - g["date"] = game.get("data-date") - g["summary"] = game.get("title") - lg.append(g) - self.lasttengames = lg - - except: - self.lasttengames = [] - - def __repr__(self): - return "" % self.name - - def __str__(self): - return "%d %s %d" % (self.position, - self.name, - self.points) - - leaguepage = "%s?%s=%s" % (self.leaguebase, - self.leaguemethod, - leagueid) - - raw = BeautifulSoup(self.getPage(leaguepage)) - - for table in raw.findAll("div", {"class": "league-table full-table-wide"}): - - lg = {} - teamlist = [] - - leaguename = table.find("h2", {"class": "table-header"}) - - for tag in ["div", "script"]: - for nest in leaguename.findAll(tag): - nest.extract() - - lg["name"] = leaguename.text.strip() - - for team in table.findAll("tr", {"id": re.compile(r'team')}): - t = LeagueTableTeam(team) - teamlist.append(t) - - lg["table"] = teamlist - result.append(lg) - - return result - -class Teams(matchcommon): - - def getTeams(self): - # Start with the default page so we can get list of active leagues - rawpage = self.getPage(self.livescoreslink.format(comp="")) - teamlist = [] - - if rawpage: - raw = BeautifulSoup(rawpage) - - # Find the list of active leagues - selection = raw.find("div", {"class": - "drop-down-filter live-scores-fixtures"}) - - # Loop throught the active leagues - for option in selection.findAll("option"): - - # Build the link for that competition - league = option.get("value")[12:] - - if league: - scorelink = self.livescoreslink.format(comp=league) - - # Prepare to process page - scorepage = self.getPage(scorelink) - if scorepage: - optionhtml = BeautifulSoup(scorepage) - - # We just want the live games... - live = optionhtml.find("div", - {"id": "matches-wrapper"}) - - for match in live.findAll("tr", - {"id": re.compile(r'^match-row')}): - - teamlist.append(match.find("span", - {"class": "team-home"}).text) - - teamlist.append(match.find("span", - {"class": "team-away"}).text) - - - teamlist = sorted(teamlist) - - return teamlist - -class Results(matchcommon): - - '''class to convert BBC league table format into python list/dict.''' - - resultbase = "http://www.bbc.co.uk/sport/football/results" - resultmethod = "filter" - - def __init__(self): - pass - - def getCompetitions(self): - '''method for getting list of available results pages''' - - complist = [] - raw = BeautifulSoup(self.getPage(self.resultbase)) - form = raw.find("div", {"class": "drop-down-filter", - "id": "filter-fixtures-no-js"}) - self.resultmethod = form.find("select").get("name") - comps = form.findAll("option") - for comp in comps: - l = {} - if comp.get("value") <> "": - l["name"] = comp.text - l["id"] = comp.get("value") - complist.append(l) - return complist - - def getResults(self, compid): - '''method for creating league table of selected league.''' - - result = [] - - leaguepage = "%s?%s=%s" % (self.resultbase, - self.resultmethod, - compid) - - raw = BeautifulSoup(self.getPage(leaguepage)) - - raw = raw.find("div", {"class": re.compile(r"\bfixtures-table\b")}) - - while raw.find("h2", {"class": "table-header"}) is not None: - - - resultdate = raw.find("h2", {"class": "table-header"}) - matchdate = resultdate.text.strip() - resultdate.extract() - - results = raw.find("table", {"class": "table-stats"}) - - matches = [] - - for matchresult in results.findAll("tr", {"id": re.compile(r'^match-row')}): - hometeam = matchresult.find("span", {"class": re.compile(r'^team-home')}).text.strip() - awayteam = matchresult.find("span", {"class": re.compile(r'^team-away')}).text.strip() - score = matchresult.find("span", {"class": "score"}).text.strip() - matches.append({"hometeam": hometeam, - "awayteam": awayteam, - "score": score}) - - resultday = {"date": matchdate, - "results": matches} - - result.append(resultday) - - results.extract() - - return result - - -class Fixtures(matchcommon): - - '''class to convert BBC league table format into python list/dict.''' - - fixturebase = "http://www.bbc.co.uk/sport/football/fixtures" - fixturemethod = "filter" - - def __init__(self): - pass - - def getCompetitions(self): - '''method for getting list of available results pages''' - - complist = [] - raw = BeautifulSoup(self.getPage(self.fixturebase)) - form = raw.find("div", {"class": "drop-down-filter", - "id": "filter-fixtures-no-js"}) - self.fixturemethod = form.find("select").get("name") - comps = form.findAll("option") - for comp in comps: - l = {} - if comp.get("value") <> "": - l["name"] = comp.text - l["id"] = comp.get("value") - complist.append(l) - return complist - - def getFixtures(self, compid): - '''method for creating league table of selected league.''' - - result = [] - - leaguepage = "%s?%s=%s" % (self.fixturebase, - self.fixturemethod, - compid) - - raw = BeautifulSoup(self.getPage(leaguepage)) - - raw = raw.find("div", {"class": re.compile(r"\bfixtures-table\b")}) - - while raw.find("h2", {"class": "table-header"}) is not None: - - - fixturedate = raw.find("h2", {"class": "table-header"}) - matchdate = fixturedate.text.strip() - fixturedate.extract() - - results = raw.find("table", {"class": "table-stats"}) - - matches = [] - - for matchresult in results.findAll("tr", {"id": re.compile(r'^match-row')}): - hometeam = matchresult.find("span", {"class": re.compile(r'^team-home')}).text.strip() - awayteam = matchresult.find("span", {"class": re.compile(r'^team-away')}).text.strip() - matches.append({"hometeam": hometeam, - "awayteam": awayteam}) - - resultday = {"date": matchdate, - "fixtures": matches} - - result.append(resultday) - - results.extract() - - return result - -def getAllLeagues(): - - tableleagues = LeagueTable().getLeagues() - tableleagues = [{"name": x["name"], "id": x["id"][12:]} for x in tableleagues] - matchleagues = League.getLeagues() - - tableleagues += [x for x in matchleagues if x not in tableleagues] - - return tableleagues diff --git a/resources/lib/league_tables.py b/resources/lib/league_tables.py deleted file mode 100644 index 0eefe62..0000000 --- a/resources/lib/league_tables.py +++ /dev/null @@ -1,293 +0,0 @@ -''' - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . -''' - -''' This script is part of the BBC Football Scores service by elParaguayo - -''' -import sys - -if sys.version_info >= (2, 7): - import json as json - from collections import OrderedDict -else: - import simplejson as json - from resources.lib.ordereddict import OrderedDict - -import xbmc -import xbmcgui -import xbmcaddon - -from resources.lib.footballscores import LeagueTable -from resources.lib.utils import closeAddonSettings - -# Import PyXBMCt module. -from pyxbmct.addonwindow import * - -_A_ = xbmcaddon.Addon("service.bbclivefootballscores") -_S_ = _A_.getSetting - -def localise(id): - '''Gets localised string. - - Shamelessly copied from service.xbmc.versioncheck - ''' - string = _A_.getLocalizedString(id).encode( 'utf-8', 'ignore' ) - return string - -class XBMCLeagueTable(object): - - def __init__(self): - - # It may take a bit of time to display menus/tables so let's - # make sure the user knows what's going on - self.prog = xbmcgui.DialogProgressBG() - self.prog.create(localise(32106), localise(32107)) - - # variables for league table display - self.redraw = False - self.offset = 0 - self.menu = True - self.leagueid = 0 - - # variables for root menu - self.showall = True - self.active = True - - # Get our favourite leagues - self.watchedleagues = json.loads(str(_S_("watchedleagues"))) - - # Create a Leaue Table instance - self.league = LeagueTable() - - self.prog.update(25, localise(32108)) - - # Get all of the available leagues, store it in an Ordered Dict - # key=League name - # value=League ID - self.allleagues = OrderedDict((x["name"], - x["id"][-9:]) - for x in self.league.getLeagues()) - - self.prog.update(75, localise(32109)) - - # Create a similar Ordered Dict for just those leagues that we're - # currently followin - self.watchedleagues = OrderedDict((x["name"], x["id"][-9:]) - for x in self.league.getLeagues() - if int(x["id"][-9:]) - in self.watchedleagues) - - self.prog.close() - - - def showMenu(self, all_leagues=False): - - - - # Setting this to False means that the menu won't display if - # we hit escape - self.active = False - - # Set the title and menu size - window = AddonDialogWindow(localise(32100)) - window.setGeometry(450,300,5,4) - - # Create a List object - self.leaguelist = List() - - # Get the appropriate list of leagues depending on what mode we're in - displaylist = self.allleagues if self.showall else self.watchedleagues - - #self.prog.update(92) - - # Add the List to the menu - window.placeControl(self.leaguelist, 0, 0, rowspan=4, columnspan=4) - self.leaguelist.addItems(displaylist.keys()) - - # Bind the list action - p = self.leaguelist.getSelectedPosition - window.connect(self.leaguelist, - lambda w = window: - self.setID(self.leaguelist.getListItem(p()).getLabel(), - w)) - - # Don't think these are needed, but what the hell... - window.connect(ACTION_PREVIOUS_MENU, lambda w=window: self.finish(w)) - window.connect(ACTION_NAV_BACK, lambda w=window: self.finish(w)) - - #self.prog.update(94) - - # Create the button to toggle mode - leaguetext = localise(32101) if self.showall else localise(32102) - self.leaguebutton = Button(leaguetext) - window.placeControl(self.leaguebutton, 4, 0, columnspan=2) - - # Bind the button - window.connect(self.leaguebutton, lambda w=window: self.toggleMode(w)) - - #self.prog.update(96) - - # Add the close button - self.closebutton = Button(localise(32103)) - window.placeControl(self.closebutton, 4, 2, columnspan=2) - window.setFocus(self.leaguelist) - # Connect the button to a function. - window.connect(self.closebutton, lambda w=window:self.finish(w)) - - #self.prog.update(98) - - # Handle navigation to make user experience better - self.leaguelist.controlLeft(self.leaguebutton) - self.leaguelist.controlRight(self.closebutton) - self.closebutton.controlUp(self.leaguelist) - self.closebutton.controlLeft(self.leaguebutton) - self.leaguebutton.controlRight(self.closebutton) - self.leaguebutton.controlUp(self.leaguelist) - - # Ready to go... - window.doModal() - - def showLeagueTable(self): - - # Basic variables - self.redraw = False - - # If there are multiple tables for a competition (e.g. World Cup) - # Let's just get the required one - table = self.rawleaguedata[self.offset] - - #self.prog.update(92) - - # How many rows are in the table? - # We'll need this to set the size of the display - n = len(table["table"]) - - # Create a window instance and size it - window = AddonDialogWindow(table["name"]) - window.setGeometry(450, (n + 4) * 25, n + 3, 4) - - #self.prog.update(94) - - # Add the teams - for i, t in enumerate(table["table"]): - pos = Label(str(t.position)) - team = Label(t.name) - points = Label(str(t.points), alignment=ALIGN_RIGHT) - window.placeControl(pos,i+1,0) - window.placeControl(team,i+1,1, columnspan=2) - window.placeControl(points,i+1,3) - - #self.prog.update(94) - - # Add the close button - closebutton = Button(localise(32103)) - window.placeControl(closebutton, n+2, 1, columnspan=2) - window.setFocus(closebutton) - # Connect the button to a function. - window.connect(closebutton, lambda w=window: self.finish(w)) - - # Not sure we need these... - window.connect(ACTION_PREVIOUS_MENU, lambda w=window: self.finish(w)) - window.connect(ACTION_NAV_BACK, lambda w=window: self.finish(w)) - - #self.prog.update(96) - - # We may need some extra buttons (for multiple table competitions) - nextbutton = Button(localise(32104)) - prevbutton = Button(localise(32105)) - - # There are more leagues after the one we're showing - if self.offset < (len(self.rawleaguedata) - 1): - window.placeControl(nextbutton, n+2,3) - window.connect(nextbutton, lambda w=window: self.next(w)) - nextbutton.controlLeft(closebutton) - closebutton.controlRight(nextbutton) - - # There are more leagues before the one we're showing - if self.offset > 0: - window.placeControl(prevbutton, n+2,0) - window.connect(prevbutton, lambda w=window: self.previous(w)) - prevbutton.controlRight(closebutton) - closebutton.controlLeft(prevbutton) - - #self.prog.close() - - # Ready to go... - window.doModal() - - def getLeagueTableData(self, ID): - - self.prog.create(localise(32106), localise(32111)) - try: - raw = self.league.getLeagueTable("competition-%s" - % (self.leagueid)) - - except: - raw = None - - self.prog.close() - - return raw - - def setID(self, ID, w): - # Gets the ID of the selected league - ID = self.allleagues[ID] - self.setleague(ID,w) - - def next(self,w): - # Display the next table in the competion - self.offset += 1 - self.redraw = True - w.close() - - def previous(self,w): - # Display the previous tablein the competition - self.offset -= 1 - self.redraw = True - w.close() - - def finish(self,w): - # We're done. Gracefully close down menu. - self.redraw = False - self.menu = False - self.active = False - w.close() - - def setleague(self,lg, w): - # Set up the variables to display the league table - self.leagueid = lg - self.offset = 0 - self.redraw = True - w.close() - self.rawleaguedata = self.getLeagueTableData(self.leagueid) - self.prog.update(90) - - def toggleMode(self,w): - # Toggle between showing all competitions and just our favourites - self.showall = not self.showall - self.active = True - w.close() - - def start(self): - - # Let's begin - while self.active: - # Show the main menu - self.showMenu() - - while self.redraw: - - # Show a league table - self.showLeagueTable() diff --git a/resources/lib/live_scores_detail.py b/resources/lib/live_scores_detail.py deleted file mode 100644 index fdc44e9..0000000 --- a/resources/lib/live_scores_detail.py +++ /dev/null @@ -1,383 +0,0 @@ -''' - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . -''' - -''' This script is part of the BBC Football Scores service by elParaguayo - -''' -import sys -import os - -if sys.version_info >= (2, 7): - import json as json - from collections import OrderedDict -else: - import simplejson as json - from resources.lib.ordereddict import OrderedDict - -import xbmc -import xbmcgui -import xbmcaddon - -from resources.lib.footballscores import League -from resources.lib.utils import closeAddonSettings - -# Import PyXBMCt module. -from pyxbmct.addonwindow import * - -_A_ = xbmcaddon.Addon("service.bbclivefootballscores") -_S_ = _A_.getSetting -pluginPath = _A_.getAddonInfo("path") - -def imgloc(img): - return os.path.join(pluginPath, "resources", "media" , img) - -imagedict = {"goal": imgloc("ball-white.png"), - "yellow": imgloc("yellow-card.png"), - "red": imgloc("red-card.png")} - -def localise(id): - '''Gets localised string. - - Shamelessly copied from service.xbmc.versioncheck - ''' - string = _A_.getLocalizedString(id).encode( 'utf-8', 'ignore' ) - return string - -class XBMCLiveScoresDetail(object): - - def __init__(self): - - # It may take a bit of time to display menus/tables so let's - # make sure the user knows what's going on - self.prog = xbmcgui.DialogProgressBG() - self.prog.create(localise(32106), localise(32107)) - - # variables for league table display - self.redraw = False - self.menu = True - self.leagueid = 0 - - # variables for root menu - self.showall = True - self.active = True - - # Get our favourite leagues - self.watchedleagues = json.loads(str(_S_("watchedleagues"))) - - self.prog.update(25, localise(32108)) - self.activeleagues = League.getLeagues() - self.favouriteleagues = [x for x in self.activeleagues if int(x["id"]) in self.watchedleagues] - - # Get all of the available leagues, store it in an Ordered Dict - # key=League name - # value=League ID - self.allleagues = OrderedDict((x["name"], - x["id"]) - for x in self.activeleagues) - - self.prog.update(75, localise(32109)) - - # Create a similar Ordered Dict for just those leagues that we're - # currently followin - self.watchedleagues = OrderedDict((x["name"], x["id"]) - for x in self.favouriteleagues) - - self.prog.close() - - - def showMenu(self, all_leagues=False): - - - - # Setting this to False means that the menu won't display if - # we hit escape - self.active = False - - # Set the title and menu size - window = AddonDialogWindow(localise(32100)) - window.setGeometry(450,300,5,4) - - # Create a List object - self.leaguelist = List() - - # Get the appropriate list of leagues depending on what mode we're in - displaylist = self.allleagues if self.showall else self.watchedleagues - - #self.prog.update(92) - - # Add the List to the menu - window.placeControl(self.leaguelist, 0, 0, rowspan=4, columnspan=4) - self.leaguelist.addItems(displaylist.keys()) - - # Bind the list action - p = self.leaguelist.getSelectedPosition - window.connect(self.leaguelist, - lambda w = window: - self.setID(self.leaguelist.getListItem(p()).getLabel(), - w)) - - # Don't think these are needed, but what the hell... - window.connect(ACTION_PREVIOUS_MENU, lambda w=window: self.finish(w)) - window.connect(ACTION_NAV_BACK, lambda w=window: self.finish(w)) - - #self.prog.update(94) - - # Create the button to toggle mode - leaguetext = localise(32101) if self.showall else localise(32102) - self.leaguebutton = Button(leaguetext) - window.placeControl(self.leaguebutton, 4, 0, columnspan=2) - - # Bind the button - window.connect(self.leaguebutton, lambda w=window: self.toggleMode(w)) - - #self.prog.update(96) - - # Add the close button - self.closebutton = Button(localise(32103)) - window.placeControl(self.closebutton, 4, 2, columnspan=2) - window.setFocus(self.leaguelist) - # Connect the button to a function. - window.connect(self.closebutton, lambda w=window:self.finish(w)) - - #self.prog.update(98) - - # Handle navigation to make user experience better - self.leaguelist.controlLeft(self.leaguebutton) - self.leaguelist.controlRight(self.closebutton) - self.closebutton.controlUp(self.leaguelist) - self.closebutton.controlLeft(self.leaguebutton) - self.leaguebutton.controlRight(self.closebutton) - self.leaguebutton.controlUp(self.leaguelist) - - # self.prog.close() - - # Ready to go... - window.doModal() - - def showLiveMatches(self): - - # Basic variables - self.redraw = False - - # If there are multiple tables for a competition (e.g. World Cup) - # Let's just get the required one - matches = self.rawdata.LeagueMatches - - #self.prog.update(92) - - # How many rows are in the table? - # We'll need this to set the size of the display - n = min(len(matches),8) - - # Create a window instance and size it - window = AddonDialogWindow("Select Match") - window.setGeometry(450, 450, 8, 4) - - #self.prog.update(94) - - self.matchlist = List() - - window.placeControl(self.matchlist, 0, 0, rowspan=7, columnspan=4) - self.matchlist.addItems([unicode(x) for x in matches]) - - # Bind the list action - p = self.matchlist.getSelectedPosition - window.connect(self.matchlist, - lambda w = window: - self.setMatch(p(), - w)) - #self.matchlist.addItems(["2","3"]) - - # Add the teams - # for i, m in enumerate(matches): - # match = Button(unicode(m)) - # window.placeControl(match,i+1,0, columnspan=4) - - #self.prog.update(94) - - # Add the close button - closebutton = Button(localise(32103)) - window.placeControl(closebutton, 7, 1, columnspan=2) - window.setFocus(self.matchlist) - # Connect the button to a function. - window.connect(closebutton, lambda w=window: self.finish(w)) - - # Not sure we need these... - window.connect(ACTION_PREVIOUS_MENU, lambda w=window: self.finish(w)) - window.connect(ACTION_NAV_BACK, lambda w=window: self.finish(w)) - - #self.prog.update(96) - - self.matchlist.controlLeft(closebutton) - self.matchlist.controlRight(closebutton) - closebutton.controlUp(self.matchlist) - - # We may need some extra buttons (for multiple table competitions) - nextbutton = Button(localise(32104)) - prevbutton = Button(localise(32105)) - - # Ready to go... - window.doModal() - - def showMatchDetail(self, match): - - # Basic variables - self.redraw = False - - match.detailed = True - match.Update() - - homeincidents = [x for x in match.rawincidents if x[0]=="home"] - awayincidents = [x for x in match.rawincidents if x[0]=="away"] - - n = max(len(homeincidents), len(awayincidents)) - - # Create a window instance and size it - window = AddonDialogWindow("Match Detail") - window.setGeometry(700, 190 + (n * 30), n + 4, 11) - - #self.prog.update(94) - - homelabel = Label(u"[B]{0}[/B]".format(match.HomeTeam), alignment=ALIGN_CENTER) - awaylabel = Label(u"[B]{0}[/B]".format(match.AwayTeam), alignment=ALIGN_CENTER) - scorelabel = Label("[B]{homescore} - {awayscore}[/B]".format(**match.matchdict), alignment=ALIGN_CENTER) - - window.placeControl(homelabel, 0, 0, columnspan=5) - window.placeControl(awaylabel, 0, 6, columnspan=5) - window.placeControl(scorelabel, 1, 4, columnspan=3) - - # Add the incidents - for i, m in enumerate(homeincidents): - #t = Label(m[1][0], alignment=ALIGN_CENTER_X) - t = Image(imagedict[m[1]], aspectRatio=2) - p = Label(m[2], alignment=ALIGN_RIGHT) - c = Label(m[3], alignment=ALIGN_CENTER_X) - - window.placeControl(t,i+2,0) - window.placeControl(p,i+2,1, columnspan=3) - window.placeControl(c,i+2,4) - - for i, m in enumerate(awayincidents): - t = Image(imagedict[m[1]], aspectRatio=2) - p = Label(m[2], alignment=ALIGN_RIGHT) - c = Label(m[3], alignment=ALIGN_CENTER_X) - - window.placeControl(t,i+2,6) - window.placeControl(p,i+2,7, columnspan=3) - window.placeControl(c,i+2,10) - - #self.prog.update(94) - - # Add the close button - closebutton = Button(localise(32103)) - window.placeControl(closebutton, n+3, 6, columnspan=4) - window.setFocus(closebutton) - # Connect the button to a function. - window.connect(closebutton, lambda w=window: self.finish(w)) - - # Choose another competition - compbutton = Button("Different Game") - window.placeControl(compbutton, n+3, 1, columnspan=4) - # Connect the button to a function. - window.connect(compbutton, lambda w=window: self.back(w)) - - # Not sure we need these... - window.connect(ACTION_PREVIOUS_MENU, lambda w=window: self.finish(w)) - window.connect(ACTION_NAV_BACK, lambda w=window: self.finish(w)) - - #self.prog.update(96) - - # We may need some extra buttons (for multiple table competitions) - nextbutton = Button(localise(32104)) - prevbutton = Button(localise(32105)) - - closebutton.controlLeft(compbutton) - compbutton.controlRight(closebutton) - - # Ready to go... - window.doModal() - - def getLiveMatches(self, ID): - - self.prog.create(localise(32106), localise(32111)) - try: - raw = League(self.leagueid) - - except: - raw = None - - self.prog.close() - - return raw - - def back(self, w): - self.redraw = True - w.close() - - def setID(self, ID, w): - # Gets the ID of the selected league - ID = self.allleagues[ID] - self.setleague(ID,w) - - def setMatch(self, ID, w): - m = self.rawdata.LeagueMatches[ID] - w.close() - self.showMatchDetail(m) - - def next(self,w): - # Display the next table in the competion - self.offset += 1 - self.redraw = True - w.close() - - def previous(self,w): - # Display the previous tablein the competition - self.offset -= 1 - self.redraw = True - w.close() - - def finish(self,w): - # We're done. Gracefully close down menu. - self.redraw = False - self.menu = False - self.active = False - w.close() - - def setleague(self,lg, w): - # Set up the variables to display the league table - self.leagueid = lg - self.offset = 0 - self.redraw = True - w.close() - self.rawdata = self.getLiveMatches(self.leagueid) - self.prog.update(90) - - def toggleMode(self,w): - # Toggle between showing all competitions and just our favourites - self.showall = not self.showall - self.active = True - w.close() - - def start(self): - - # Let's begin - while self.active: - # Show the main menu - self.showMenu() - - while self.redraw: - - # Show a league table - self.showLiveMatches() diff --git a/resources/lib/menu.py b/resources/lib/menu.py deleted file mode 100644 index 04da85c..0000000 --- a/resources/lib/menu.py +++ /dev/null @@ -1,108 +0,0 @@ -''' - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . -''' - -''' This script is part of the BBC Football Scores service by elParaguayo - -''' -import xbmc -import xbmcaddon - -# Import PyXBMCt module. -from pyxbmct.addonwindow import * - -_A_ = xbmcaddon.Addon("service.bbclivefootballscores") -_S_ = _A_.getSetting - -def localise(id): - '''Gets localised string. - - Shamelessly copied from service.xbmc.versioncheck - ''' - string = _A_.getLocalizedString(id).encode( 'utf-8', 'ignore' ) - return string - -class FootballHelperMenu(object): - - def __init__(self): - - pass - - def show(self): - - self.control_list = [] - - # Set the title and menu size - self.window = AddonDialogWindow("BBC Football Scores") - self.window.setGeometry(450,300,5,2) - self.control_list.append(self.window) - - # ITEM 1 - LEAGUE TABLES - self.ltbutton = Button("Show League Tables") - self.window.placeControl(self.ltbutton, 0, 0, columnspan = 2) - - # ITEM 2 - MATCH DETAIL - self.mdbutton = Button("Show Match Detail") - self.window.placeControl(self.mdbutton, 1, 0, columnspan = 2) - - # ITEM 3 - MATCH DETAIL - self.resbutton = Button("Show Results") - self.window.placeControl(self.resbutton, 2, 0, columnspan = 2) - - # ITEM 4 - MATCH DETAIL - self.fixbutton = Button("Show Fixtures") - self.window.placeControl(self.fixbutton, 3, 0, columnspan = 2) - - # CLOSE BUTTON - - self.clbutton = Button("Close") - self.window.placeControl(self.clbutton, 4, 0, columnspan = 2) - - # add buttons to control_list - self.control_list += [self.ltbutton, self.mdbutton, - self.resbutton, self.fixbutton] - - # Bind actions - self.window.connect(ACTION_PREVIOUS_MENU, lambda: self.window.close()) - self.window.connect(ACTION_NAV_BACK, lambda: self.window.close()) - self.window.connect(self.clbutton, lambda: self.window.close()) - self.window.connect(self.ltbutton, lambda: self.open("leaguetable")) - self.window.connect(self.mdbutton, lambda: self.open("matchdetail")) - self.window.connect(self.resbutton, lambda: self.open("results")) - self.window.connect(self.fixbutton, lambda: self.open("fixtures")) - - self.window.setFocus(self.ltbutton) - - # Handle navigation to make user experience better - self.ltbutton.controlDown(self.mdbutton) - self.mdbutton.controlUp(self.ltbutton) - self.mdbutton.controlDown(self.resbutton) - self.resbutton.controlDown(self.fixbutton) - self.resbutton.controlUp(self.mdbutton) - self.fixbutton.controlDown(self.clbutton) - self.fixbutton.controlUp(self.resbutton) - self.clbutton.controlUp(self.fixbutton) - - - # Ready to go... - self.window.doModal() - - # clean up - for control in self.control_list: - del control - - def open(self, mode): - - self.window.close() - xbmc.executebuiltin("RunScript(service.bbclivefootballscores, mode={0})".format(mode)) diff --git a/resources/lib/results.py b/resources/lib/results.py deleted file mode 100644 index a9266c7..0000000 --- a/resources/lib/results.py +++ /dev/null @@ -1,332 +0,0 @@ -''' - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . -''' - -''' This script is part of the BBC Football Scores service by elParaguayo - - It allows users to select which leagues they wish to receive updates - for. - - It is called via the script configuration screen or by passing - parameters to trigger specific functions. - - The script accepts the following parameters: - toggle: Turns score notifications on and off - reset: Resets watched league data - - NB only one parameter should be passed at a time. -''' -import sys - -if sys.version_info >= (2, 7): - import json as json - from collections import OrderedDict -else: - import simplejson as json - from resources.lib.ordereddict import OrderedDict - -import xbmc -import xbmcgui -import xbmcaddon - -from resources.lib.footballscores import Results -from resources.lib.utils import closeAddonSettings - -# Import PyXBMCt module. -from pyxbmct.addonwindow import * - -_A_ = xbmcaddon.Addon("service.bbclivefootballscores") -_S_ = _A_.getSetting - -def localise(id): - '''Gets localised string. - - Shamelessly copied from service.xbmc.versioncheck - ''' - string = _A_.getLocalizedString(id).encode( 'utf-8', 'ignore' ) - return string - -class XBMCResults(object): - - def __init__(self): - - # It may take a bit of time to display menus/tables so let's - # make sure the user knows what's going on - self.prog = xbmcgui.DialogProgressBG() - self.prog.create(localise(32112), localise(32107)) - - # variables for league table display - self.redraw = False - self.offset = 0 - self.menu = True - self.leagueid = 0 - - # variables for root menu - self.showall = True - self.active = True - - # Get our favourite leagues - self.watchedleagues = json.loads(str(_S_("watchedleagues"))) - - # Create a Leaue Table instance - self.results = Results() - - self.prog.update(25, localise(32108)) - - allcomps = self.results.getCompetitions() - - allcomps = [x for x in allcomps if x["id"][:11] == "competition"] - - # Get all of the available leagues, store it in an Ordered Dict - # key=League name - # value=League ID - self.allleagues = OrderedDict((x["name"], - x["id"][-9:]) - for x in allcomps) - - xbmc.log(str(self.allleagues)) - - self.prog.update(75, localise(32109)) - - # Create a similar Ordered Dict for just those leagues that we're - # currently followin - lgid = x["id"][-9:] - self.watchedleagues = OrderedDict((x["name"], x["id"][-9:]) - for x in allcomps - if (unicode(lgid).isnumeric() and - int(lgid) in self.watchedleagues)) - - self.prog.close() - - - def showMenu(self, all_leagues=False): - - - - # Setting this to False means that the menu won't display if - # we hit escape - self.active = False - - # Set the title and menu size - window = AddonDialogWindow(localise(32100)) - window.setGeometry(450,300,5,4) - - # Create a List object - self.leaguelist = List() - - # Get the appropriate list of leagues depending on what mode we're in - displaylist = self.allleagues if self.showall else self.watchedleagues - - #self.prog.update(92) - - # Add the List to the menu - window.placeControl(self.leaguelist, 0, 0, rowspan=4, columnspan=4) - self.leaguelist.addItems(displaylist.keys()) - - # Bind the list action - p = self.leaguelist.getSelectedPosition - window.connect(self.leaguelist, - lambda w = window: - self.setID(self.leaguelist.getListItem(p()).getLabel(), - w)) - - # Don't think these are needed, but what the hell... - window.connect(ACTION_PREVIOUS_MENU, lambda w=window: self.finish(w)) - window.connect(ACTION_NAV_BACK, lambda w=window: self.finish(w)) - - #self.prog.update(94) - - # Create the button to toggle mode - leaguetext = localise(32101) if self.showall else localise(32102) - self.leaguebutton = Button(leaguetext) - window.placeControl(self.leaguebutton, 4, 0, columnspan=2) - - # Bind the button - window.connect(self.leaguebutton, lambda w=window: self.toggleMode(w)) - - #self.prog.update(96) - - # Add the close button - self.closebutton = Button(localise(32103)) - window.placeControl(self.closebutton, 4, 2, columnspan=2) - window.setFocus(self.leaguelist) - # Connect the button to a function. - window.connect(self.closebutton, lambda w=window:self.finish(w)) - - #self.prog.update(98) - - # Handle navigation to make user experience better - self.leaguelist.controlLeft(self.leaguebutton) - self.leaguelist.controlRight(self.closebutton) - self.closebutton.controlUp(self.leaguelist) - self.closebutton.controlLeft(self.leaguebutton) - self.leaguebutton.controlRight(self.closebutton) - self.leaguebutton.controlUp(self.leaguelist) - - # Ready to go... - window.doModal() - - def showResults(self): - - # Basic variables - self.redraw = False - - # If there are multiple tables for a competition (e.g. World Cup) - # Let's just get the required one - table = self.rawdata[self.offset] - - #self.prog.update(92) - - # How many rows are in the table? - # We'll need this to set the size of the display - n = len(table["results"]) - - # Create a window instance and size it - window = AddonDialogWindow(table["date"]) - window.setGeometry(450, (n + 4) * 30, n + 3, 15) - - #self.prog.update(94) - - # Add the teams - for i,r in enumerate(table["results"]): - try: - r["score"] = r["score"].replace("-", " - ") - except: - pass - - # result = Label(u"{hometeam} {score} {awayteam}".format(**r), - # alignment=2) - home = Label(u"{hometeam}".format(**r), alignment=1) - score = Label("{score}".format(**r), alignment=2) - away = Label(u"{awayteam}".format(**r)) - - window.placeControl(home, i+1, 0, columnspan=6) - window.placeControl(score, i+1, 6, columnspan=3) - window.placeControl(away, i+1, 9, columnspan=6) - - - #self.prog.update(94) - - # Add the close button - closebutton = Button(localise(32103)) - window.placeControl(closebutton, n+2, 5, columnspan=5) - window.setFocus(closebutton) - # Connect the button to a function. - window.connect(closebutton, lambda w=window: self.finish(w)) - - # Not sure we need these... - window.connect(ACTION_PREVIOUS_MENU, lambda w=window: self.finish(w)) - window.connect(ACTION_NAV_BACK, lambda w=window: self.finish(w)) - - #self.prog.update(96) - - # We may need some extra buttons (for multiple table competitions) - nextbutton = Button(localise(32104)) - prevbutton = Button(localise(32105)) - - # There are more leagues after the one we're showing - if self.offset < (len(self.rawdata) - 1): - window.placeControl(nextbutton, n+2, 10, columnspan=5) - window.connect(nextbutton, lambda w=window: self.next(w)) - nextbutton.controlLeft(closebutton) - closebutton.controlRight(nextbutton) - - # There are more leagues before the one we're showing - if self.offset > 0: - window.placeControl(prevbutton, n+2, 0, columnspan=5) - window.connect(prevbutton, lambda w=window: self.previous(w)) - prevbutton.controlRight(closebutton) - closebutton.controlLeft(prevbutton) - - #self.prog.close() - - # Ready to go... - window.doModal() - - def getResultsData(self, ID): - - self.prog.create(localise(32112), localise(32113)) - try: - raw = self.results.getResults("competition-%s" - % (self.leagueid)) - - except: - print "ERROR" - raw = None - - self.prog.close() - - return raw - - def setID(self, ID, w): - # Gets the ID of the selected league - ID = self.allleagues[ID] - self.setleague(ID,w) - - def next(self,w): - # Display the next table in the competion - self.offset += 1 - self.redraw = True - w.close() - - def previous(self,w): - # Display the previous tablein the competition - self.offset -= 1 - self.redraw = True - w.close() - - def finish(self,w): - # We're done. Gracefully close down menu. - self.redraw = False - self.menu = False - self.active = False - w.close() - - def setleague(self,lg, w): - # Set up the variables to display the league table - self.leagueid = lg - self.offset = 0 - self.redraw = True - w.close() - self.rawdata = self.getResultsData(self.leagueid) - self.prog.update(90) - - def toggleMode(self,w): - # Toggle between showing all competitions and just our favourites - self.showall = not self.showall - self.active = True - w.close() - - def start(self): - - # Let's begin - while self.active: - # Show the main menu - self.showMenu() - - while self.redraw: - - # Show a league table - self.showResults() - -if __name__ == "__main__": - - # Close addon setting window (if open) - closeAddonSettings() - - # Create an instance of the XBMC League Table - xlr = XBMCResults() - - # and display it! - xlr.start() diff --git a/resources/lib/settings.py b/resources/lib/settings.py index 221342f..23508c5 100644 --- a/resources/lib/settings.py +++ b/resources/lib/settings.py @@ -23,11 +23,12 @@ else: import simplejson as json + import xbmc import xbmcgui import xbmcaddon -from resources.lib.footballscores import getAllLeagues +from resources.lib.footballscores import getAllTeams _A_ = xbmcaddon.Addon("service.bbclivefootballscores") _S_ = _A_.getSetting @@ -37,6 +38,9 @@ RESET = 1 TOGGLE_NOTIFICATIONS = 2 +SETTING_TEAMS = "watchedteams" +SETTING_LEAGUES = "watchedleagues" + modes= {"standard": STANDARD, "reset": RESET, "toggle": TOGGLE_NOTIFICATIONS} @@ -59,159 +63,216 @@ def Notify(subject, message, image=None): ''' xbmcgui.Dialog().notification(subject, message, image, 2000) -def selectLeagues(): - '''Get list of available leagues and allow user to select those - leagues from which they want to receive updates. - ''' - - # Get list of leagues - # Format is [{"name": "Name of League", - # "id": "competition-xxxxxxx"}] - leagues = getMasterLeagueList() - - # Set a flag to keep displaying select dialog until flag changes - finishedSelection = False - - # Load the list of leagues currently selected by user - watchedleagues = loadLeagues() - - # Start loop - will be exited once user confirms selection or - # cancels - while not finishedSelection: - - # Empty list for select dialog - userchoice = [] - - # Loop through leagues - for league in leagues: - - try: - # Add league details to our list - # leagues.append([league["name"],int(league["id"][12:])]) - - # Check whether leagues is one we're following - if int(league["id"]) in watchedleagues: - - # Mark the league if it's one the user has previously - # selected and add it to the select dialog - userchoice.append("*" + league["name"]) - - else: - - # If not previously selected, we still need to add to - # select dialog - userchoice.append(league["name"]) - - # Hopefully we don't end up here... - except: - - # Tell the user there's a problem - userchoice.append(localise(32020)) - - # We only need to tell the user once! - break - - # Add an option to say we've finished selecting leagues - userchoice.append(localise(32022)) - - - # Display the list - inputchoice = xbmcgui.Dialog().select(localise(32021), - userchoice) - - - # Check whether the user has clicked on a league... - if (inputchoice >=0 and not userchoice[inputchoice] == localise(32022) - and not userchoice[inputchoice] == localise(32021)): - - # If it's one that's already in our watched league list... - if int(leagues[inputchoice]["id"]) in watchedleagues: - - # ...then we need to remove it - watchedleagues.remove(int(leagues[inputchoice]["id"])) - - # if not... - else: - - # ... then we need to add it - watchedleagues.append(int(leagues[inputchoice]["id"])) - - # If we're done - elif userchoice[inputchoice] == localise(32022): - - # Save our new list - saveLeagues(watchedleagues) - - # Set the flag to leave the select dialog loop - finishedSelection = True - - # If there's an error or we hit cancel - elif (inputchoice == -1 or - userchoice[inputchoice] == localise(32020)): - - - # end the selection (but don't save new settings) - finishedSelection = True - -def getMasterLeagueList(): - '''Returns master list of leagues/competitions for which we - can obtain data. - - Some competitions are only visible when matches are being - played so any new competitions are added to the master list - whenever this script is run. - - Returns: masterLeagueList - list of competitions in dict - format {"name": xx, "id", xx} - ''' - - currentleagues = getAllLeagues() - - try: - masterLeagueList = json.loads(_S_("masterlist")) - - except: - masterLeagueList = [] - - masterLeagueList += [x for x in currentleagues - if x not in masterLeagueList] - - _A_.setSetting(id="masterlist",value=json.dumps(masterLeagueList)) - - return masterLeagueList - -def loadLeagues(): - '''See if there are any previously selected leagues. - - Returns list of league IDs. - ''' - - try: - watchedleagues = json.loads(str(_S_("watchedleagues"))) - except: - watchedleagues = [] - - return watchedleagues - -def saveLeagues(leagues): - '''Converts list to JSON compatible string and saves it to our - user's settings. - ''' - - rawdata = json.dumps(leagues) - _A_.setSetting(id="watchedleagues",value=rawdata) - -def resetLeagues(): - '''Clears all saved league IDs. - - Useful if IDs change leading to duplicate menu entries. - ''' - _A_.setSetting(id="watchedleagues",value="[]") - _A_.setSetting(id="masterlist",value="[]") - ok = xbmcgui.Dialog().ok(localise(32023), localise(32027)) - -def toggleNotification(): - '''Toggles score notifications on or off.''' - state = not (_S_("Alerts") == "true") - Notify("BBC Football Scores", localise(32024) % (localise(32025) if state else localise(32026))) - _A_.setSetting(id="Alerts", value=str(state).lower()) +def getSetting(setting, is_json=True): + + setting = _S_(setting) + + if is_json: + try: + return json.loads(setting) + except: + return setting + + else: + return setting + +def doMultiselect(heading, items, preselect=None): + + ms = xbmcgui.Dialog().multiselect + + code = ms(heading, items, preselect=preselect) + + return code + + +def selectTeams(): + + teams = getAllTeams() + retry = 5 + + while teams is None: + xbmc.sleep(500) + teams = getAllTeams() + + retry -= 1 + + if retry == 0: + break + + if teams is None: + Notify("BBC Live Football Scores", localise(32020)) + return False + + teams = [x["name"] for x in teams] + + myteams = getSetting(SETTING_TEAMS) + + if type(myteams) != list: + myteams = [] + + preselect = [teams.index(x) for x in myteams if x in teams] + + selected = doMultiselect(localise(32004), teams, preselect=preselect) + + if selected is not None: + + selected_teams = [teams[x] for x in selected] + + saveSetting(SETTING_TEAMS, selected_teams) + +# def selectLeagues(): +# '''Get list of available leagues and allow user to select those +# leagues from which they want to receive updates. +# ''' +# +# # Get list of leagues +# # Format is [{"name": "Name of League", +# # "id": "competition-xxxxxxx"}] +# leagues = getMasterLeagueList() +# +# # Set a flag to keep displaying select dialog until flag changes +# finishedSelection = False +# +# # Load the list of leagues currently selected by user +# watchedleagues = loadLeagues() +# +# # Start loop - will be exited once user confirms selection or +# # cancels +# while not finishedSelection: +# +# # Empty list for select dialog +# userchoice = [] +# +# # Loop through leagues +# for league in leagues: +# +# try: +# # Add league details to our list +# # leagues.append([league["name"],int(league["id"][12:])]) +# +# # Check whether leagues is one we're following +# if int(league["id"]) in watchedleagues: +# +# # Mark the league if it's one the user has previously +# # selected and add it to the select dialog +# userchoice.append("*" + league["name"]) +# +# else: +# +# # If not previously selected, we still need to add to +# # select dialog +# userchoice.append(league["name"]) +# +# # Hopefully we don't end up here... +# except: +# +# # Tell the user there's a problem +# userchoice.append(localise(32020)) +# +# # We only need to tell the user once! +# break +# +# # Add an option to say we've finished selecting leagues +# userchoice.append(localise(32022)) +# +# +# # Display the list +# inputchoice = xbmcgui.Dialog().select(localise(32021), +# userchoice) +# +# +# # Check whether the user has clicked on a league... +# if (inputchoice >=0 and not userchoice[inputchoice] == localise(32022) +# and not userchoice[inputchoice] == localise(32021)): +# +# # If it's one that's already in our watched league list... +# if int(leagues[inputchoice]["id"]) in watchedleagues: +# +# # ...then we need to remove it +# watchedleagues.remove(int(leagues[inputchoice]["id"])) +# +# # if not... +# else: +# +# # ... then we need to add it +# watchedleagues.append(int(leagues[inputchoice]["id"])) +# +# # If we're done +# elif userchoice[inputchoice] == localise(32022): +# +# # Save our new list +# saveLeagues(watchedleagues) +# +# # Set the flag to leave the select dialog loop +# finishedSelection = True +# +# # If there's an error or we hit cancel +# elif (inputchoice == -1 or +# userchoice[inputchoice] == localise(32020)): +# +# +# # end the selection (but don't save new settings) +# finishedSelection = True +# +# def getMasterLeagueList(): +# '''Returns master list of leagues/competitions for which we +# can obtain data. +# +# Some competitions are only visible when matches are being +# played so any new competitions are added to the master list +# whenever this script is run. +# +# Returns: masterLeagueList - list of competitions in dict +# format {"name": xx, "id", xx} +# ''' +# +# currentleagues = getAllLeagues() +# +# try: +# masterLeagueList = json.loads(_S_("masterlist")) +# +# except: +# masterLeagueList = [] +# +# masterLeagueList += [x for x in currentleagues +# if x not in masterLeagueList] +# +# _A_.setSetting(id="masterlist",value=json.dumps(masterLeagueList)) +# +# return masterLeagueList +# +# def loadLeagues(): +# '''See if there are any previously selected leagues. +# +# Returns list of league IDs. +# ''' +# +# try: +# watchedleagues = json.loads(str(_S_("watchedleagues"))) +# except: +# watchedleagues = [] +# +# return watchedleagues +# +def saveSetting(setting, value, is_json=True): + + if is_json: + value = json.dumps(value) + + _A_.setSetting(id=setting,value=value) + +# +# def resetLeagues(): +# '''Clears all saved league IDs. +# +# Useful if IDs change leading to duplicate menu entries. +# ''' +# _A_.setSetting(id="watchedleagues",value="[]") +# _A_.setSetting(id="masterlist",value="[]") +# ok = xbmcgui.Dialog().ok(localise(32023), localise(32027)) +# +# def toggleNotification(): +# '''Toggles score notifications on or off.''' +# state = not (_S_("Alerts") == "true") +# Notify("BBC Football Scores", localise(32024) % (localise(32025) if state else localise(32026))) +# _A_.setSetting(id="Alerts", value=str(state).lower()) diff --git a/resources/settings.xml b/resources/settings.xml index 1f2128e..ec188d6 100644 --- a/resources/settings.xml +++ b/resources/settings.xml @@ -4,7 +4,9 @@ - + + + + - + - +