diff --git a/FPLbot/bot.py b/FPLbot/bot.py index 7143f0a..7d07ec2 100644 --- a/FPLbot/bot.py +++ b/FPLbot/bot.py @@ -12,7 +12,9 @@ from pymongo import MongoClient from constants import fpl_team_names, versus_pattern -from utils import create_logger, get_player_table, to_fpl_team, update_players +from utils import (create_logger, find_player, get_player_table, + player_vs_player_table, player_vs_team_table, to_fpl_team, + update_players, get_relevant_fixtures) dirname = os.path.dirname(os.path.realpath(__file__)) logger = create_logger() @@ -81,75 +83,58 @@ async def post_price_changes(self): self.subreddit.submit(post_title, selftext=post_body) await update_players() - def player_vs_team_table(self, fixtures): - """Returns a Markdown table showing the player's performance in the - given fixtures. - """ - table = ("|Fixture|Date|MP|G|xG|A|xA|NPG|NPxG|KP|\n" - "|:-|:-:|-:|-:|-:|-:|-:|-:|-:|-:|\n") - - for fixture in fixtures: - home_team = f"{fixture['h_team']} {fixture['h_goals']}" - away_team = f"{fixture['a_goals']} {fixture['a_team']}" - - # Highlight the winning team - if int(fixture["h_goals"]) > int(fixture["a_goals"]): - home_team = f"**{home_team}**" - elif int(fixture["h_goals"]) < int(fixture["a_goals"]): - away_team = f"**{away_team}**" - - # Highlight whether the player was a starter or not - if fixture["position"].lower() != "sub": - fixture["time"] = f"**{fixture['time']}**" - - table += ( - f"|{home_team}-{away_team}" - f"|{fixture['date']}" - f"|{fixture['time']}" - f"|{fixture['goals']}" - f"|{float(fixture['xG']):.2f}" - f"|{fixture['assists']}" - f"|{float(fixture['xA']):.2f}" - f"|{fixture['npg']}" - f"|{float(fixture['npxG']):.2f}" - f"|{fixture['key_passes']}|\n" - ) - - return table + def versus_player_handler(self, player_A_name, player_B_name, + number_of_fixtures): + """Function for handling player vs. player comment.""" + player_A = find_player(player_A_name) + player_B = find_player(player_B_name) + + if not player_A or not player_B: + return + + player_A_fixtures = get_relevant_fixtures(player_A) + player_B_fixtures = get_relevant_fixtures(player_B) + + if not number_of_fixtures: + number_of_fixtures = max(len(player_A_fixtures), + len(player_B_fixtures)) + + fixtures = zip(player_A_fixtures[:number_of_fixtures], + player_B_fixtures[:number_of_fixtures]) + + post_template = open(f"{dirname}/../comment_template.md").read() + table_header = ( + f"# {player_A['web_name']} (£{player_A['now_cost'] / 10.0:.1f}) " + f"vs. {player_B['web_name']} (£{player_B['now_cost'] / 10.0:.1f}) " + f"(last {number_of_fixtures} fixtures)") + table_body = player_vs_player_table(fixtures) + + return post_template.format( + comment_header=table_header, + comment_body=table_body + ) def versus_team_handler(self, player_name, team_name, number_of_fixtures): """Function for handling player vs. team comment.""" - # Find most relevant player using text search - players = self.database.players.find( - {"$text": {"$search": player_name}}, - {"score": {"$meta": "textScore"}} - ).sort([("score", {"$meta": "textScore"})]) - - try: - player = list(players.limit(1))[0] - except IndexError: - logger.error(f"Player {player_name} could not be found!") + player = find_player(player_name) + if not player: return if not number_of_fixtures: number_of_fixtures = len(player["understat_history"]) - fixture_count = 0 - relevant_fixtures = [] - team_name = to_fpl_team(team_name) - for fixture in player["understat_history"]: - if fixture_count >= int(number_of_fixtures): - break - - if (team_name != fixture["h_team"].lower() and - team_name != fixture["a_team"].lower()): - continue - - fixture_count += 1 - relevant_fixtures.append(fixture) - - player_vs_team_table = self.player_vs_team_table(relevant_fixtures) - return player_vs_team_table + fixtures = get_relevant_fixtures( + player, team_name=to_fpl_team(team_name))[:number_of_fixtures] + post_template = open(f"{dirname}/../comment_template.md").read() + table_header = ( + f"# {player_name.title()} vs. {team_name.title()} (last " + f"{len(fixtures)} fixtures)") + table_body = player_vs_team_table(fixtures) + + return post_template.format( + comment_header=table_header, + comment_body=table_body + ) def add_comment_to_database(self, comment): logger.info(f"Adding comment with ID {comment.id} to the database.") @@ -172,11 +157,15 @@ def comment_handler(self, comment): opponent_name = match.group(2).lower().replace(".", "").strip() number = match.group(3) + if number: + number = int(number) + if to_fpl_team(opponent_name) in fpl_team_names: reply_text = self.versus_team_handler( player_name, opponent_name, number) else: - return + reply_text = self.versus_player_handler( + player_name, opponent_name, number) if reply_text: logger.info(f"Replying ({player_name} vs. {opponent_name}) to " diff --git a/FPLbot/constants.py b/FPLbot/constants.py index cfa0e5b..84d3048 100644 --- a/FPLbot/constants.py +++ b/FPLbot/constants.py @@ -78,7 +78,7 @@ "understat_history" ] -versus_pattern = re.compile(r"!fplbot\s+([^\W\d]+(?:[\s-][^\W\d]+)*)\s+(?:vs.|vs)\s+([a-zA-Z ]+)(\d+)?") +versus_pattern = re.compile(r"!fplbot\s+([A-zÀ-ÿ]+(?:[\s-][A-zÀ-ÿ]+)*)\s+(?:vs.|vs)\s+([A-zÀ-ÿ]+(?:[\s-][A-zÀ-ÿ]+)*)\s*(\d+)?") to_fpl_team_dict = { "arsenal fc": "arsenal", diff --git a/FPLbot/init.py b/FPLbot/init.py new file mode 100644 index 0000000..462417f --- /dev/null +++ b/FPLbot/init.py @@ -0,0 +1,21 @@ +import asyncio + +from pymongo import MongoClient + +from utils import update_players + +client = MongoClient() +database = client.fpl + + +async def main(): + await update_players() + + +if __name__ == "__main__": + try: + asyncio.run(main()) + except AttributeError: + loop = asyncio.get_event_loop() + loop.run_until_complete(main()) + loop.close() diff --git a/FPLbot/price_changes.py b/FPLbot/price_changes.py new file mode 100644 index 0000000..c326a91 --- /dev/null +++ b/FPLbot/price_changes.py @@ -0,0 +1,25 @@ +import asyncio +import json +import os + +import aiohttp + +from bot import FPLBot + +dirname = os.path.dirname(os.path.realpath(__file__)) + +async def main(config): + async with aiohttp.ClientSession() as session: + fpl_bot = FPLBot(config, session) + + await fpl_bot.post_price_changes() + + +if __name__ == "__main__": + config = json.loads(open(f"{dirname}/../config.json").read()) + try: + asyncio.run(main(config)) + except AttributeError: + loop = asyncio.get_event_loop() + loop.run_until_complete(main(config)) + loop.close() diff --git a/FPLbot/utils.py b/FPLbot/utils.py index cf04072..f13fd43 100644 --- a/FPLbot/utils.py +++ b/FPLbot/utils.py @@ -11,8 +11,8 @@ from fpl.utils import position_converter, team_converter from pymongo import MongoClient, ReplaceOne -from constants import (desired_attributes, player_dict, team_dict, - to_fpl_team_dict) +from constants import (desired_attributes, fpl_team_names, player_dict, + team_dict, to_fpl_team_dict) client = MongoClient() database = client.fpl @@ -125,6 +125,14 @@ async def get_understat_players(): return players +def create_text_indexes(): + database.players.create_index([ + ("web_name", "text"), + ("first_name", "text"), + ("second_name", "text") + ]) + + async def update_players(): """Updates all players in the database.""" logger.info("Updating FPL players in database.") @@ -137,6 +145,7 @@ async def update_players(): requests = [ReplaceOne({"id": player["id"]}, player, upsert=True) for player in players] database.players.bulk_write(requests) + create_text_indexes() logger.info("Adding Understat data to players in database.") understat_players = await get_understat_players() @@ -180,6 +189,145 @@ def get_player_table(players, risers=True): return table_header + table_body +def get_total(total, fixture): + for key, value in fixture.items(): + total.setdefault(key, 0) + try: + total[key] += float(value) + except ValueError: + continue + return total + + +def player_vs_player_table(fixtures): + table = ("|xA|A|xG|G|MP|Fixture|Fixture|MP|G|xG|A|xA|\n" + "|-:|-:|-:|-:|-:|:-|-:|-:|-:|-:|-:|-:|\n") + + total_A = {} + total_B = {} + for fixture in fixtures: + fixture_A = fixture[0] + fixture_B = fixture[1] + + minutes_played_A = fixture_A["time"] + minutes_played_B = fixture_B["time"] + + # Highlight whether the player was a starter or not + if fixture_A["position"].lower() != "sub": + minutes_played_A = f"**{minutes_played_A}**" + + if fixture_B["position"].lower() != "sub": + minutes_played_B = f"**{minutes_played_B}**" + + table += ( + f"|{float(fixture_A['xA']):.2f}" + f"|{fixture_A['assists']}" + f"|{float(fixture_A['xG']):.2f}" + f"|{fixture_A['goals']}" + f"|{minutes_played_A}" + f"|{fixture_A['h_team']} {fixture_A['h_goals']}-" + f"{fixture_A['a_goals']} {fixture_A['a_team']}" + f"|{fixture_B['h_team']} {fixture_B['h_goals']}-" + f"{fixture_B['a_goals']} {fixture_B['a_team']}" + f"|{minutes_played_B}" + f"|{fixture_B['goals']}" + f"|{float(fixture_B['xG']):.2f}" + f"|{fixture_B['assists']}" + f"|{float(fixture_B['xA']):.2f}|\n" + ) + total_A = get_total(total_A, fixture_A) + total_B = get_total(total_B, fixture_B) + + table_footer = ( + f"|**{total_A['xA']:.2f}**" + f"|**{int(total_A['assists'])}**" + f"|**{total_A['xG']:.2f}**" + f"|**{int(total_A['goals'])}**" + f"|**{int(total_A['time'])}**||" + f"|**{int(total_B['time'])}**" + f"|**{int(total_B['goals'])}**" + f"|**{total_B['xG']:.2f}**" + f"|**{int(total_B['assists'])}**" + f"|**{total_B['xA']:.2f}**|\n" + ) + + return table + table_footer + + +def player_vs_team_table(fixtures): + """Returns a Markdown table showing the player's performance in the + given fixtures. + """ + table = ("|Fixture|Date|MP|G|xG|A|xA|NPG|NPxG|KP|\n" + "|:-|:-|-:|-:|-:|-:|-:|-:|-:|-:|\n") + + total = {} + + for fixture in fixtures: + home_team = f"{fixture['h_team']} {fixture['h_goals']}" + away_team = f"{fixture['a_goals']} {fixture['a_team']}" + minutes_played = fixture["time"] + + # Highlight the winning team + if int(fixture["h_goals"]) > int(fixture["a_goals"]): + home_team = f"**{fixture['h_team']}** {fixture['h_goals']}" + elif int(fixture["h_goals"]) < int(fixture["a_goals"]): + away_team = f"**{fixture['a_goals']}** {fixture['a_team']}" + + # Highlight whether the player was a starter or not + if fixture["position"].lower() != "sub": + minutes_played = f"**{minutes_played}**" + + table += ( + f"|{home_team}-{away_team}" + f"|{fixture['date']}" + f"|{minutes_played}" + f"|{fixture['goals']}" + f"|{float(fixture['xG']):.2f}" + f"|{fixture['assists']}" + f"|{float(fixture['xA']):.2f}" + f"|{fixture['npg']}" + f"|{float(fixture['npxG']):.2f}" + f"|{fixture['key_passes']}|\n" + ) + + for key, value in fixture.items(): + total.setdefault(key, 0) + try: + total[key] += float(value) + except ValueError: + continue + + # Add footer with totals + table_footer = ( + f"|||**{int(total['time'])}**" + f"|**{int(total['goals'])}**" + f"|**{total['xG']:.2f}**" + f"|**{int(total['assists'])}**" + f"|**{total['xA']:.2f}**" + f"|**{total['npg']}**" + f"|**{total['npxG']:.2f}**" + f"|**{int(total['key_passes'])}**|\n" + ) + + return table + table_footer + + +def find_player(player_name): + # Find most relevant player using text search + players = database.players.find( + {"$text": {"$search": player_name}}, + {"score": {"$meta": "textScore"}} + ).sort([("score", {"$meta": "textScore"})]) + + try: + player = list(players.limit(1))[0] + except IndexError: + logger.error(f"Player {player_name} could not be found!") + return None + return player + + def to_fpl_team(team_name): try: return to_fpl_team_dict[team_name] @@ -201,6 +349,27 @@ def understat_team_converter(team_name): return team_name +def get_relevant_fixtures(player, team_name=None): + """Return all fixtures that the player has played for his current team + (optionally) against the given team. + """ + fixtures = [ + fixture for fixture in player["understat_history"] + if (to_fpl_team(fixture["h_team"].lower()) in fpl_team_names or + to_fpl_team(fixture["a_team"].lower()) in fpl_team_names) and + int(fixture["time"]) > 0 + ] + + if team_name: + fixtures = [ + fixture for fixture in fixtures + if team_name == fixture["h_team"].lower() or + team_name == fixture["a_team"].lower() + ] + + return fixtures + + if __name__ == "__main__": try: asyncio.run(update_players()) diff --git a/README.md b/README.md index 73734f5..e2fbd30 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,8 @@ configuration file. Its current features are: * Posting the price changes of Fantasy Premier League players +* Comparing the performance of a player vs. a team +* Comparing the performance of a palyer vs. another player ## Installation @@ -19,11 +21,58 @@ Python 3.6+. cd FPLbot pip install -r requirements.txt -To initialise the database you should do the following: +To initialise the database with text indexes you should do the following: - python FPLbot/utils.py + python FPLbot/init.py -Once this has been done, you can schedule a cron job to run the bot whenever you want! +Once this has been done, you should create your own `config.json` with the correct values (see [configuration](#configuration)). +With this filled in, you can run the bot using + + python FPLbot/bot.py + +As for the price changes, you should schedule a cron job, like this for example: + + 25 1 * * * /home/amos/FPLbot/venv/bin/python /home/amos/FPLbot/FPLbot/price_changes.py + +## Usage + +The bot can be called on [/r/FantasyPL](https://www.reddit.com/r/FantasyPL/) using the following two commands: + +1. `!fplbot vs. ` +2. `!fplbot vs. ` + +The bot uses text indexes to search for the player(s) and using a manually created mapping (so you don't have to use e.g. "man utd" exactly, but other variations are fine as well, like "man u" or "manchester united"). The number of fixtures is completely optional, and if not specified, it simply uses *all* fixtures that are considered relevant. Here are two examples: + +1. + + !fplbot heung-min son vs. mane 5 + +### Son (£8.9) vs. Mané (£9.6) (last 5 fixtures) + +|xA|A|xG|G|MP|Fixture|Fixture|MP|G|xG|A|xA| +|-:|-:|-:|-:|-:|:-|-:|-:|-:|-:|-:|-:| +|0.13|0|0.42|1|**90**|Spurs 3-1 Leicester|Liverpool 3-0 Bournemouth|**89**|1|0.58|0|0.12| +|0.70|0|0.19|1|**90**|Spurs 1-0 Newcastle United|West Ham 1-1 Liverpool|**90**|1|0.64|0|0.00| +|0.00|0|0.20|1|**90**|Spurs 2-1 Watford|Liverpool 1-1 Leicester|**90**|1|0.18|0|0.10| +|0.38|0|0.05|0|**90**|Spurs 0-1 Man Utd|Liverpool 4-3 Crystal Palace|**90**|1|0.47|0|0.01| +|0.08|1|0.50|1|**77**|Cardiff 0-3 Spurs|Brighton 0-1 Liverpool|**90**|0|0.10|0|0.10| +|**1.29**|**1**|**1.36**|**4**|**437**|||**449**|**4**|**1.97**|**0**|**0.34**| + +--- + +2. + + !fplbot rashford vs. liverpool + +### Rashford vs. Liverpool (last 4 fixtures) + +|Fixture|Date|MP|G|xG|A|xA|NPG|NPxG|KP| +|:-|:-|-:|-:|-:|-:|-:|-:|-:|-:| +|**Liverpool** 3-1 Man Utd|2018-12-16|**90**|0|0.02|0|0.00|0|0.02|0| +|**Man Utd** 2-1 Liverpool|2018-03-10|**72**|2|0.17|0|0.00|2|0.17|0| +|Liverpool 0-0 Man Utd|2017-10-14|24|0|0.00|0|0.00|0|0.00|0| +|Liverpool 0-0 Man Utd|2016-10-17|**78**|0|0.00|0|0.00|0|0.00|0| +|||**264**|**2**|**0.19**|**0**|**0.00**|**2.0**|**0.19**|**0**| ## Configuration @@ -35,5 +84,6 @@ Once this has been done, you can schedule a cron job to run the bot whenever you |CLIENT_SECRET|The bot's client secret| |USER_AGENT|A unique identifier that helps Reddit determine the source of network requests| |SUBREDDIT|The subreddit the bot will post to| +|BOT_PREFIX|The prefix used to call the bot, e.g.: "!fplbot"| For more information about how to set up a bot see [Reddit's guide](https://github.com/reddit-archive/reddit/wiki/OAuth2-Quick-Start-Example#first-steps). diff --git a/comment_template.md b/comment_template.md new file mode 100644 index 0000000..124f1ae --- /dev/null +++ b/comment_template.md @@ -0,0 +1,7 @@ +{comment_header} + +{comment_body} + +--- + +^Made ^by ^/u/esoemah. ^Source: ^https://github.com/amosbastian/FPLbot diff --git a/requirements.txt b/requirements.txt index 80a427e..24e1054 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,6 +3,8 @@ appdirs==1.4.3 async-timeout==3.0.1 atomicwrites==1.2.1 attrs==18.2.0 +beautifulsoup4==4.7.1 +bs4==0.0.1 certifi==2018.11.29 chardet==3.0.4 Click==7.0 @@ -22,6 +24,7 @@ pytest==4.2.0 pytest-aiohttp==0.3.0 requests==2.21.0 six==1.12.0 +soupsieve==1.7.3 update-checker==0.16 urllib3==1.24.1 websocket-client==0.54.0