diff --git a/SoccerScores/README.md b/SoccerScores/README.md new file mode 100644 index 0000000..79401a5 --- /dev/null +++ b/SoccerScores/README.md @@ -0,0 +1 @@ +Fetches soccer scores and other information diff --git a/SoccerScores/__init__.py b/SoccerScores/__init__.py new file mode 100644 index 0000000..2d75e52 --- /dev/null +++ b/SoccerScores/__init__.py @@ -0,0 +1,52 @@ +### +# Copyright (c) 2018, cottongin +# All rights reserved. +# +# +### + +""" +SoccerScores: Fetches soccer scores and other information +""" + +import sys +import supybot +from supybot import world + +# Use this for the version of this plugin. You may wish to put a CVS keyword +# in here if you're keeping the plugin in CVS or some similar system. +__version__ = "" + +# XXX Replace this with an appropriate author or supybot.Author instance. +__author__ = supybot.Author('cottongin', 'cottongin', + 'cottongin@cottongin.club') +__maintainer__ = getattr(supybot.authors, 'oddluck', + supybot.Author('oddluck', 'oddluck', 'oddluck@riseup.net')) + +# This is a dictionary mapping supybot.Author instances to lists of +# contributions. +__contributors__ = {} + +# This is a url where the most recent plugin package can be downloaded. +__url__ = 'https://github.com/oddluck/limnoria-plugins/' + +from . import config +from . import plugin +if sys.version_info >= (3, 4): + from importlib import reload +else: + from imp import reload +# In case we're being reloaded. +reload(config) +reload(plugin) +# Add more reloads here if you add third-party modules and want them to be +# reloaded when this plugin is reloaded. Don't forget to import them as well! + +if world.testing: + from . import test + +Class = plugin.Class +configure = config.configure + + +# vim:set shiftwidth=4 tabstop=4 expandtab textwidth=79: diff --git a/SoccerScores/config.py b/SoccerScores/config.py new file mode 100644 index 0000000..c5f153d --- /dev/null +++ b/SoccerScores/config.py @@ -0,0 +1,33 @@ +### +# Copyright (c) 2018, cottongin +# All rights reserved. +# +# +### + +from supybot import conf, registry +try: + from supybot.i18n import PluginInternationalization + _ = PluginInternationalization('SoccerScores') +except: + # Placeholder that allows to run the plugin on a bot + # without the i18n module + _ = lambda x: x + + +def configure(advanced): + # This will be called by supybot to configure this module. advanced is + # a bool that specifies whether the user identified themself as an advanced + # user or not. You should effect your configuration by manipulating the + # registry as appropriate. + from supybot.questions import expect, anything, something, yn + conf.registerPlugin('SoccerScores', True) + + +Soccer = conf.registerPlugin('SoccerScores') +# This is where your configuration variables (if any) should go. For example: +# conf.registerGlobalValue(Soccer, 'someConfigVariableName', +# registry.Boolean(False, _("""Help for someConfigVariableName."""))) + + +# vim:set shiftwidth=4 tabstop=4 expandtab textwidth=79: diff --git a/SoccerScores/plugin.py b/SoccerScores/plugin.py new file mode 100644 index 0000000..ac7d063 --- /dev/null +++ b/SoccerScores/plugin.py @@ -0,0 +1,324 @@ +# Soccer +### +# Copyright (c) 2018, cottongin +# All rights reserved. +# +# See LICENSE.txt +# +### + +from supybot import utils, plugins, ircutils, callbacks, schedule, conf +from supybot.commands import * +try: + from supybot.i18n import PluginInternationalization + _ = PluginInternationalization('SoccerScores') +except ImportError: + # Placeholder that allows to run the plugin on a bot + # without the i18n module + _ = lambda x: x + +# Non-supybot imports +import requests +import pendulum +import pickle + + +class SoccerScores(callbacks.Plugin): + """Fetches soccer scores and other information""" + threaded = True + + def __init__(self, irc): + self.__parent = super(SoccerScores, self) + self.__parent.__init__(irc) + + self.PICKLEFILE = conf.supybot.directories.data.dirize("soccer-leagues.db") + + self.BASE_API_URL = ('http://site.api.espn.com/apis/site/v2/sports/' + 'soccer/{league}/scoreboard?lang=en®ion=us&' + 'dates={date}&league={league}') + # http://site.api.espn.com/apis/site/v2/sports/soccer/eng.2/scoreboard + # ?lang=en®ion=us&calendartype=whitelist + # &limit=100&dates=20181028&league=eng.2 + + self.FUZZY_DAYS = ['yesterday', 'tonight', 'today', 'tomorrow', + 'sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'] + + try: + with open(self.PICKLEFILE, 'rb') as handle: + self.LEAGUE_MAP = pickle.load(handle) + except: + self.LEAGUE_MAP = { + 'epl': 'eng.1', 'mls': 'usa.1', 'ecl': 'eng.2', 'uefac': 'uefa.champions', + 'uefae': 'uefa.europa', 'efac': 'eng.fa', 'carabao': 'eng.league_cup', + 'liga': 'esp.1', 'bundesliga': 'ger.1', 'seriea': 'ita.1', 'ligue': 'fra.1', + 'bbva': 'mex.1', 'fifawc': 'fifa.world', 'wc': 'fifa.world', 'nations': 'uefa.nations', + 'concacaf': 'concacaf.nations.league_qual', 'africa': 'caf.nations_qual', + 'cl': 'eng.2', + } + + # TO-DO / think about: + """ + def periodicCheckGames(): + self.CFB_GAMES = self._fetchGames(None, '') + + periodicCheckGames() + + try: # check scores. + schedule.addPeriodicEvent(periodicCheckGames, 20, now=False, name='fetchCFBscores') + except AssertionError: + try: + schedule.removeEvent('fetchCFBscores') + except KeyError: + pass + schedule.addPeriodicEvent(periodicCheckGames, 20, now=False, name='fetchCFBscores') + """ + def _dumpDB(self, db): + with open(self.PICKLEFILE, 'wb') as handle: + pickle.dump(db, handle, protocol=pickle.HIGHEST_PROTOCOL) + return + + @wrap(['admin','text']) + def addleague(self, irc, msg, args, league): + """ + Adds to bot's leagues database""" + + league = league.lower() + league = league.split() + + if len(league) > 2: + return + if league[0] in self.LEAGUE_MAP: + irc.reply('Already in database') + return + self.LEAGUE_MAP[league[0]] = league[1] + self._dumpDB(self.LEAGUE_MAP) + irc.replySuccess() + return + + @wrap(['admin','text']) + def remleague(self, irc, msg, args, league): + """ + Removes from bot's leagues database""" + + league = league.lower() + league_t = league.split() + + if len(league_t) > 1: + return + if league not in self.LEAGUE_MAP: + irc.reply('Not found in database') + return + self.LEAGUE_MAP.pop(league, None) + self._dumpDB(self.LEAGUE_MAP) + irc.replySuccess() + return + + @wrap([getopts({'date': 'somethingWithoutSpaces', + 'league': 'somethingWithoutSpaces', + 'tz': 'somethingWithoutSpaces'}), optional('text')]) + def soccer(self, irc, msg, args, options, filter_team=None): + """--league (--date ) (team) + Fetches soccer scores for given team on date in provided league, defaults to current + day if no date is provided and all teams in league if no team provided. + """ + + now = pendulum.now() + today = now.in_tz('US/Eastern').format('YYYYMMDD') + + options = dict(options) + date = options.get('date') + league = options.get('league') + tz = options.get('tz') or 'US/Eastern' + + if date: + date = self._parseDate(date) + date = pendulum.parse(date, strict=False).format('YYYYMMDD') + else: + date = today + + if filter_team: + filter_team = filter_team.lower() + if filter_team in self.LEAGUE_MAP and not league: + league = self.LEAGUE_MAP[filter_team] + filter_team = None + + if not league: + irc.reply('ERROR: You must provide a league via --league ') + doc = irc.getCallback('SoccerScores').soccer.__doc__ + doclines = doc.splitlines() + s = '%s' % (doclines.pop(0)) + if doclines: + help = ' '.join(doclines) + s = '(%s) -- %s' % (ircutils.bold(s), help) + s = utils.str.normalizeWhitespace(s) + irc.reply(s) + vl = ', '.join(k for k in self.LEAGUE_MAP) + irc.reply('Valid leagues: {}'.format(vl)) + return + + mapped_league = self.LEAGUE_MAP.get(league.lower()) + + if not mapped_league and '.' not in league: + irc.reply('ERROR: {} not found in valid leagues: {}'.format( + league, ', '.join(k for k in self.LEAGUE_MAP))) + return + elif not mapped_league: + mapped_league = league.lower() + + url = self.BASE_API_URL.format(date=date, league=mapped_league) + + try: + data = requests.get(url) + except: + irc.reply('Something went wrong fetching data from {}'.format( + data.url)) + return + + print(data.url) + data = data.json() + + if 'leagues' not in data: + irc.reply('ERROR: {} not found in valid leagues: {}'.format( + league, ', '.join(k for k in self.LEAGUE_MAP))) + return + + league_name = ircutils.bold(data['leagues'][0]['name']) + + if not data['events']: + irc.reply('No matches found') + return + + comps = [] + for event in data['events']: + comps.append(event['competitions'][0]) + + #print(comps) + single = False + if len(comps) == 1: + single = True + matches = [] + for match in comps: + #print(match) + time = pendulum.parse(match['date'], strict=False).in_tz(tz).format('h:mm A zz') + long_time = pendulum.parse(match['date'], strict=False).in_tz(tz).format('ddd MMM Do h:mm A zz') + teams_abbr = [match['competitors'][0]['team']['abbreviation'].lower(), + match['competitors'][1]['team']['abbreviation'].lower()] + for team in match['competitors']: + if team['homeAway'] == 'home': + home = team['team']['shortDisplayName'] + home_abbr = team['team']['abbreviation'] + home_score = team['score'] + elif team['homeAway'] == 'away': + away = team['team']['shortDisplayName'] + away_abbr = team['team']['abbreviation'] + away_score = team['score'] + clock = match['status']['displayClock'] + final = match['status']['type']['completed'] + status = match['status']['type']['shortDetail'] + if final: + status = ircutils.mircColor(status, 'red') + if status == 'HT': + status = ircutils.mircColor(status, 'orange') + state = match['status']['type']['state'] + + if state == 'pre': + # + if not filter_team and not single: + string = '{1} - {0} {2}'.format(away_abbr, home_abbr, time) + else: + string = '{1} - {0}, {2}'.format(away, home, long_time) + elif state == 'in': + # + if away_score > home_score: + away = ircutils.bold(away) + away_abbr = ircutils.bold(away_abbr) + away_score = ircutils.bold(away_score) + elif home_score > away_score: + home = ircutils.bold(home) + home_abbr = ircutils.bold(home_abbr) + home_score = ircutils.bold(home_score) + if not filter_team and not single: + string = '{2} {3}-{1} {0} {4}'.format(away_abbr, away_score, home_abbr, home_score, clock) + else: + string = '{2} {3}-{1} {0} {4}'.format(away, away_score, home, home_score, clock) + elif state == 'post': + # + if away_score > home_score: + away = ircutils.bold(away) + away_abbr = ircutils.bold(away_abbr) + away_score = ircutils.bold(away_score) + elif home_score > away_score: + home = ircutils.bold(home) + home_abbr = ircutils.bold(home_abbr) + home_score = ircutils.bold(home_score) + if not filter_team and not single: + string = '{2} {3}-{1} {0} {4}'.format(away_abbr, away_score, home_abbr, home_score, status) + else: + string = '{2} {3}-{1} {0} {4}'.format(away, away_score, home, home_score, status) + else: + if not filter_team and not single: + string = '{1} - {0} {2}'.format(away_abbr, home_abbr, time) + else: + string = '{1} - {0}, {2}'.format(away, home, long_time) + + if filter_team: + #print(filter_team, string) + if filter_team in string.lower() or filter_team in teams_abbr: + matches.append(string) + else: + matches.append(string) + + if not matches: + irc.reply('No matches found') + return + + irc.reply('{}: {}'.format(league_name, ' | '.join(s for s in matches))) + + return + + + def _parseDate(self, string): + """parse date""" + date = string[:3].lower() + if date in self.FUZZY_DAYS or string.lower() in self.FUZZY_DAYS: + if date == 'yes': + date_string = pendulum.yesterday('US/Eastern').format('YYYYMMDD') + return date_string + elif date == 'tod' or date == 'ton': + date_string = pendulum.now('US/Eastern').format('YYYYMMDD') + return date_string + elif date == 'tom': + date_string = pendulum.tomorrow('US/Eastern').format('YYYYMMDD') + return date_string + elif date == 'sun': + date_string = pendulum.now('US/Eastern').next(pendulum.SUNDAY).format('YYYYMMDD') + return date_string + elif date == 'mon': + date_string = pendulum.now('US/Eastern').next(pendulum.MONDAY).format('YYYYMMDD') + return date_string + elif date == 'tue': + date_string = pendulum.now('US/Eastern').next(pendulum.TUESDAY).format('YYYYMMDD') + return date_string + elif date == 'wed': + date_string = pendulum.now('US/Eastern').next(pendulum.WEDNESDAY).format('YYYYMMDD') + return date_string + elif date == 'thu': + date_string = pendulum.now('US/Eastern').next(pendulum.THURSDAY).format('YYYYMMDD') + return date_string + elif date == 'fri': + date_string = pendulum.now('US/Eastern').next(pendulum.FRIDAY).format('YYYYMMDD') + return date_string + elif date == 'sat': + date_string = pendulum.now('US/Eastern').next(pendulum.SATURDAY).format('YYYYMMDD') + return date_string + else: + return string + else: + return string + + + +Class = SoccerScores + + +# vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: diff --git a/SoccerScores/requirements.txt b/SoccerScores/requirements.txt new file mode 100644 index 0000000..e6d74d1 --- /dev/null +++ b/SoccerScores/requirements.txt @@ -0,0 +1,2 @@ +requests +pendulum diff --git a/SoccerScores/test.py b/SoccerScores/test.py new file mode 100644 index 0000000..9b6ae31 --- /dev/null +++ b/SoccerScores/test.py @@ -0,0 +1,15 @@ +### +# Copyright (c) 2018, cottongin +# All rights reserved. +# +# +### + +from supybot.test import * + + +class SoccerTestCase(PluginTestCase): + plugins = ('SoccerScores',) + + +# vim:set shiftwidth=4 tabstop=4 expandtab textwidth=79: