├── .idea
├── .gitignore
├── misc.xml
├── vcs.xml
├── inspectionProfiles
│ └── profiles_settings.xml
├── stake.com-Scraper-and-Autobetting-tool.iml
└── modules.xml
├── requirements.txt
├── LICENSE
├── main.py
├── stake_postgresql.py
├── README.md
├── stake_sport_id_updater.py
├── stake_scraper_markets.py
├── stake_anti_captcha.py
├── stake_autobet.py
├── stake_auth.py
└── stake_scraper.py
/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | pandas==1.3.4
2 | requests==2.25.1
3 | SQLAlchemy==1.4.31
4 | SQLAlchemy_Utils==0.38.2
5 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/profiles_settings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/stake.com-Scraper-and-Autobetting-tool.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 matthews-g
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/main.py:
--------------------------------------------------------------------------------
1 | from stake_auth import AuthClass
2 | from stake_scraper import EventScraper
3 | from stake_scraper_markets import MoneyLine
4 | from stake_autobet import MoneyLineBet
5 |
6 | if __name__ == "__main__":
7 | # SAMPLE USAGE....
8 |
9 | # Declaring variables, and creating objects #
10 |
11 | username = "STAKE_USERNAME"
12 | password = "STAKE_PASSWORD"
13 | anti_cpt_api_key = "ANTI_CAPTCHA_API_KEY"
14 |
15 | bet_currency = "ltc"
16 | bet_stake = 0.1
17 |
18 | main_user = AuthClass(username, password, anti_cpt_api_key)
19 | events_table_tennis_pre = EventScraper("table_tennis", False)
20 | market_table_tennis_moneyline_pre = MoneyLine("table_tennis", False)
21 | autobet_tabletennis_moneyline = MoneyLineBet(market_table_tennis_moneyline_pre.table_name, 0.1, "ltc", main_user)
22 |
23 | # Do the authentication, ETL processes...
24 | #main_user.cycle()
25 | events_table_tennis_pre.cycle()
26 | market_table_tennis_moneyline_pre.cycle()
27 |
28 | df = market_table_tennis_moneyline_pre.dataframe
29 | print(df)
30 |
31 | """start_time = df["TIME"][0]
32 | home_player = df["HOME"][0]
33 | away_player = df["AWAY"][0]
34 | autobet_tabletennis_moneyline.home(start_time, home_player, away_player)"""
35 |
36 | input("PRESS ENTER TO EXIT")
--------------------------------------------------------------------------------
/stake_postgresql.py:
--------------------------------------------------------------------------------
1 | import pandas as pd
2 | from sqlalchemy import create_engine
3 | from sqlalchemy_utils import database_exists, create_database
4 |
5 | POSTGRES_USERNAME = 'postgres'
6 | POSTGRES_PASSWORD = 'asd'
7 | POSTGRES_IP = 'localhost'
8 | POSTGRES_PORT = '5432'
9 | POSTGRES_DB_NAME = 'stake_data'
10 | POSTGRES_SCHEMA_NAME = 'public'
11 |
12 | DATABASE_URL = f'postgresql://{POSTGRES_USERNAME}:{POSTGRES_PASSWORD}@{POSTGRES_IP}:{POSTGRES_PORT}/{POSTGRES_DB_NAME}'
13 |
14 | ENGINE = create_engine(DATABASE_URL)
15 |
16 |
17 | def set_table(dataframe: pd.DataFrame, table):
18 | """
19 | Function to upload dataframe to database as a given table
20 | Drop the table first if it exists, then create a new one and load the data!
21 | """
22 | global ENGINE
23 | global POSTGRES_SCHEMA_NAME
24 |
25 | # First, clear the given table
26 | try:
27 | with ENGINE.connect() as con:
28 | # statement = f"""DELETE FROM public.{table}""" || Use this if you want to delete the existing table.
29 | statement = f"""DROP TABLE IF EXISTS public.{table}"""
30 | con.execute(statement)
31 | except Exception as e:
32 | print("Exception happened when deleting from the table")
33 | print(e)
34 |
35 | dataframe.to_sql(table, ENGINE, schema=POSTGRES_SCHEMA_NAME, if_exists='append',
36 | index=False)
37 |
38 |
39 | def get_table(table):
40 | global ENGINE
41 |
42 | dataframe: pd.DataFrame = pd.read_sql(f"SELECT * FROM {POSTGRES_SCHEMA_NAME}.{table}", ENGINE)
43 |
44 | return dataframe
45 |
46 |
47 | if __name__ == "__main__":
48 |
49 | """ If you run the script individually, it will create the database! """
50 |
51 | if not database_exists(DATABASE_URL):
52 | create_database(DATABASE_URL)
53 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # stake.com-Scraper-and-Autobetting-tool
2 |
3 | ## Scrape "Stake.com" odds easily, place bets, even bypassing their CAPTCHA protection with "anti-captcha"!
4 |
5 | *Personal note: This is was one of my actual Stake.com scrapers before my account was limited to pennies and two factor auth has gotten forcefully enabled, so I decided to abandon this project. The same thing is likely to happen to you, although two factor authentication can be bypassed with additional work. **I am not responsible for any account closures or lost funds with the use of this tool**!*
6 |
7 | ## REQUIREMENTS
8 |
9 | - Libraries: `pip install -r requirements.txt`
10 | - AntiCaptcha: [You need to have a funded account with API KEY](https://anti-captcha.com/)
11 | - [PostgreSQL](https://www.postgresql.org/download/)
12 |
13 | ## USAGE
14 | ### You need to create a database.
15 | - This can be done by simply executing *stake_postgresql.py* OR creating it manually via pgAdmin 4.
16 | - Make sure that the connection data is correct in *stake_postgresql.py*
17 | ```
18 | POSTGRES_USERNAME = 'postgres'
19 | POSTGRES_PASSWORD = 'asd'
20 | POSTGRES_IP = 'localhost'
21 | POSTGRES_PORT = '5432'
22 | POSTGRES_DB_NAME = 'stake_data'
23 | POSTGRES_SCHEMA_NAME = 'public'
24 | ```
25 | - The tables will be stored in the *public* schema, so no need to create an additional one.
26 |
27 | ### General usage, logic of the program
28 | - Just simply run [main.py](https://github.com/matthews-g/stake.com-Scraper-and-Autobetting-tool/blob/main/main.py) and check code to easily understand how it works, and how it should be extended.
29 | - The tables in the database are handled automatically using prewritten functions. No need to worry about creating them before.
30 | - For automatic sport id updates, add `import stake_sport_id_updater.py` to the main code, the update will get executed on every program launch.
31 |
32 | ### You should know
33 | - The authentication is only working for accounts **_without_** two factor authentication (2FA) enabled.
34 |
35 | - I recommend extracting out bits from this code and building your own tool, this one serving as a helper for CAPTCHA bypassing, scraping, data storing and automatic betting. It is very similiar for all bookmakers.
36 |
37 | ## TO DO (recommended)
38 | - 2FA bypass
39 | - OneXTwo, OverUnder, Handicap scraping & betting modules
40 | - Currency converter
41 |
42 | ## LEARN MORE ABOUT ODDS SCRAPING & PROGRAMMATIC BETTING! JOIN THE COMMUNITY!
43 | - [PRIMARY DISCORD (General)](https://discord.gg/NsSRzJk)
44 | - [SECONDARY DISCORD (Bet365)](https://discord.gg/MjFr2HvUtK)
45 |
46 |
47 |
--------------------------------------------------------------------------------
/stake_sport_id_updater.py:
--------------------------------------------------------------------------------
1 | import json
2 | import requests
3 | import pandas as pd
4 |
5 | from stake_auth import AuthClass
6 | from stake_postgresql import set_table, get_table
7 |
8 |
9 | class SportIdUpdater:
10 | """
11 | Bookmakers can create dynamically changing sport IDs in order to make scraping harder
12 | This script has been created to make sure the sport IDs are always up to date!
13 | """
14 |
15 | SPORT_DICT = {
16 | 'table_tennis': 'table-tennis', # Sport names to be used in API | Created for myself because of pref.
17 | 'football': 'soccer',
18 | 'tennis': 'tennis',
19 | 'basketball': 'basketball',
20 | }
21 |
22 | SPORT_ID_TABLE = "sport_ids"
23 |
24 |
25 |
26 | def __init__(self):
27 | self.__sport_id_dict = {
28 | "soccer": None,
29 | "table-tennis": None,
30 | "tennis": None,
31 | "basketball": None}
32 |
33 | self.sport_id_dataframe = pd.DataFrame()
34 |
35 | @staticmethod
36 | def get_sport_id(sport):
37 | """ Return the ID of the given sport """
38 |
39 | payload = json.dumps({"operationName": "AllSportGroups", "variables": {"sport": sport},
40 | "query": "query AllSportGroups($sport: String!) {\n slugSport(sport: $sport) "
41 | "{\n id\n allGroups {\n "
42 | " name\n translation\n rank\n id\n __typename\n }"
43 | "\n __typename\n }\n}\n"})
44 |
45 | resp = json.loads(
46 | requests.post(AuthClass.API_URL, headers=AuthClass.get_headers(), data=payload).text)
47 |
48 | return resp['data']['slugSport']['id']
49 |
50 | @staticmethod
51 | def get_sport_id_table():
52 | """ Return the sport IDs dataframe from the database """
53 | return get_table(f"public.{SportIdUpdater.SPORT_ID_TABLE}")
54 |
55 |
56 | def get_all_sport_ids(self):
57 | """ Iterate through sports and save their IDs """
58 |
59 | for a in self.__sport_id_dict:
60 | self.__sport_id_dict[a] = SportIdUpdater.get_sport_id(a)
61 |
62 | def build_dataframe(self):
63 | """ Build the sport ID dataframe, it will be uploaded to PostgreSQL! """
64 | data = {
65 | "SPORT": [s for s in SportIdUpdater.SPORT_DICT],
66 | "SPORT_API": [SportIdUpdater.SPORT_DICT[s] for s in SportIdUpdater.SPORT_DICT],
67 | "SPORT_ID": [self.__sport_id_dict[SportIdUpdater.SPORT_DICT[s]] for s in SportIdUpdater.SPORT_DICT]
68 | }
69 |
70 | return pd.DataFrame(data=data)
71 |
72 | def cycle(self):
73 | """ Helper function to do the full ETL process! """
74 | self.get_all_sport_ids()
75 | sport_id_df = self.build_dataframe()
76 | set_table(sport_id_df, SportIdUpdater.SPORT_ID_TABLE)
77 |
78 |
79 | sport_ids = SportIdUpdater()
80 | sport_ids.cycle()
--------------------------------------------------------------------------------
/stake_scraper_markets.py:
--------------------------------------------------------------------------------
1 | import pandas as pd
2 | from stake_scraper import DataParser
3 |
4 |
5 | class MoneyLine(DataParser):
6 | def __init__(self, sport, live):
7 | super().__init__(sport, live)
8 | self.__home_odds = []
9 | self.__away_odds = []
10 | self.__home_odds_ID = []
11 | self.__away_odds_ID = []
12 | if live:
13 | self.table_name = f"{self.sport}_live_moneyline"
14 | else:
15 | self.table_name = f"{self.sport}_prematch_moneyline"
16 |
17 | def cleaner_individual(self):
18 | self.__home_odds.clear()
19 | self.__away_odds.clear()
20 | self.__home_odds_ID.clear()
21 | self.__away_odds_ID.clear()
22 |
23 | def parse_data(self):
24 |
25 | fixture_list = self.get_fixture_list()
26 |
27 | for match in fixture_list:
28 | start_time = match['data']['startTime'][5:-4] # Sat, 21 Aug 2021 15:00:00 GMT
29 | for market_groups in match['groups']:
30 | if market_groups['name'] == 'winner': # The market of ML!
31 | for templates in market_groups['templates']:
32 | for markets in templates['markets']:
33 | if markets['name'] == 'Winner':
34 | if markets['status'] == 'active':
35 |
36 | try:
37 | home_player = markets['outcomes'][0]['name']
38 | home_odds = float(markets['outcomes'][0]['odds'])
39 | away_player = markets['outcomes'][1]['name']
40 | away_odds = float(markets['outcomes'][1]['odds'])
41 | home_odds_id = markets['outcomes'][0]['id']
42 | away_odds_id = markets['outcomes'][1]['id']
43 |
44 | self.start_time.append(start_time)
45 | self.home_team.append(home_player)
46 | self.away_team.append(away_player)
47 | self.__home_odds.append(home_odds)
48 | self.__away_odds.append(away_odds)
49 | self.__home_odds_ID.append(home_odds_id)
50 | self.__away_odds_ID.append(away_odds_id)
51 | except Exception as e:
52 | # Errors could happen due to incorrect data regarding the match
53 | print("Error while parsing! Skipping...")
54 | print(e)
55 | pass
56 |
57 | def build_dataframe(self):
58 | data = {
59 | "TIME": self.start_time,
60 | "HOME": self.home_team,
61 | "AWAY": self.away_team,
62 | "HOME_ODDS": self.__home_odds,
63 | "AWAY_ODDS": self.__away_odds,
64 | "HOME_ODDS_ID": self.__home_odds_ID,
65 | "AWAY_ODDS_ID": self.__away_odds_ID,
66 | }
67 |
68 | self.dataframe = pd.DataFrame(data=data)
69 |
70 | self.dataframe = self.dataframe[
71 | (self.dataframe["HOME_ODDS"] > 1.00) & (self.dataframe["AWAY_ODDS"] > 1.00)]
72 |
73 | self.dataframe = self.dataframe.sort_values(by=["TIME"], ignore_index=True)
74 |
--------------------------------------------------------------------------------
/stake_anti_captcha.py:
--------------------------------------------------------------------------------
1 | import json
2 | import time
3 | import requests
4 |
5 |
6 | class AntiCaptcha:
7 | HEADERS = {
8 | "Accept": "application/json",
9 | "Content-Type": "application/json"
10 | }
11 |
12 | CREATE_TASK_URL = "https://api.anti-captcha.com/createTask"
13 | GET_TASK_RESULT_URL = "https://api.anti-captcha.com/getTaskResult"
14 |
15 | STAKE_LOGIN_URL = "https://stake.com/?action=login&modal=auth"
16 | STAKE_SITE_KEY = "7830874c-13ad-4cfe-98d7-e8b019dc1742"
17 |
18 | def __init__(self, api_key):
19 | self.__api_key = api_key
20 |
21 | @property
22 | def api_key(self):
23 | return self.__api_key
24 |
25 | @api_key.setter
26 | def api_key(self, api_key):
27 | self.__api_key = api_key
28 |
29 | @staticmethod
30 | def post_request(json_payload):
31 | """Custom post request method for AntiCaptcha"""
32 | try:
33 | return json.loads(
34 | requests.post(AntiCaptcha.CREATE_TASK_URL, headers=AntiCaptcha.HEADERS, data=json_payload).text)
35 | except Exception as e:
36 | print("Error while sending request to the AntiCaptcha API!")
37 | print(e)
38 |
39 | def create_task(self):
40 | """" Creates the task returns the task id for checking..."""
41 | payload = json.dumps({
42 | "clientKey": self.__api_key,
43 | "task":
44 | {
45 | "type": "HCaptchaTaskProxyless",
46 | "websiteURL": AntiCaptcha.STAKE_LOGIN_URL,
47 | "websiteKey": AntiCaptcha.STAKE_SITE_KEY
48 | }
49 | })
50 |
51 | resp = requests.post(AntiCaptcha.CREATE_TASK_URL, headers=AntiCaptcha.HEADERS, data=payload)
52 | return json.loads(resp.text)['taskId']
53 |
54 | def get_task_result(self, task_id):
55 | """ Get the task result response """
56 | payload = json.dumps({"clientKey": self.__api_key,
57 | "taskId": str(task_id)})
58 |
59 | resp = requests.post(AntiCaptcha.GET_TASK_RESULT_URL, headers=AntiCaptcha.HEADERS, data=payload)
60 | return json.loads(resp.text)
61 |
62 | def wait_for_captcha_response(self, task_id, wait_seconds=240):
63 | """ Wait until the task is finished and return the result"""
64 | now = time.time()
65 | while time.time() - now <= wait_seconds: # wait_seconds mean try until the seconds...
66 | time.sleep(0.1)
67 | try:
68 | result = self.get_task_result(task_id)
69 | if result['status'] == "processing":
70 | print("The captcha is still being processed...")
71 | print("Waiting for 3 seconds before checking again...")
72 | time.sleep(3)
73 | elif result['status'] == "ready":
74 | if result['errorId'] == 0:
75 | print("Captcha has been processed, returning the response!")
76 | return result['solution']['gRecaptchaResponse']
77 | else:
78 | return False
79 | except Exception as e:
80 | print("Exception while waiting for captcha response!")
81 | print(e)
82 | print("Trying again to get task result in 3 seconds...")
83 | time.sleep(3)
84 |
85 | return False
86 |
87 | def cycle(self):
88 | # Aggregator function to get the solved captcha code required for login...
89 | task_id = self.create_task()
90 | # print(task_id)
91 | return self.wait_for_captcha_response(task_id)
92 |
--------------------------------------------------------------------------------
/stake_autobet.py:
--------------------------------------------------------------------------------
1 | import requests
2 | import json
3 | from stake_auth import AuthClass
4 | from stake_postgresql import get_table
5 | from abc import ABC, abstractmethod
6 | import pandas as pd
7 |
8 |
9 | class AutoBet(ABC):
10 |
11 | def __init__(self, table_name, stake, currency, auth_object):
12 | self.table_name = table_name
13 | self.stake = stake
14 | self.currency = currency
15 | self.auth_object = auth_object
16 |
17 | @abstractmethod
18 | def get_match(self):
19 | """ Function to get the given match from the database """
20 | pass
21 |
22 | def send_bet_request(self, odds, odds_id):
23 | # DIRTY CODE AHEAD, COPIED AND PASTED API DATA AS IS...
24 | payload = json.dumps({"operationName": "SportBet",
25 | "variables": {"multiplier": odds, "amount": self.stake, "currency": self.currency,
26 | "outcomeIds": [odds_id], "oddsChange": "none"},
27 | "query": "mutation SportBet($amount: Float!, $currency: CurrencyEnum!, $outcomeIds: "
28 | "[String!]!, $oddsChange: SportOddsChangeEnum!, $identifier: String, $multiplier:"
29 | " Float = 1) {\n sportBet(amount: $amount, currency: $currency, outcomeIds: "
30 | "$outcomeIds, oddsChange: $oddsChange, identifier: $identifier, multiplier: "
31 | "$multiplier) {\n id\n amount\n currency\n payoutMultiplier\n "
32 | "potentialMultiplier\n cashoutMultiplier\n outcomes {\n odds\n "
33 | "outcome {\n ...Outcome\n market {\n ...BetSlip\n "
34 | " __typename\n }\n __typename\n }\n __typename\n }\n "
35 | " __typename\n }\n}\n\nfragment Outcome on SportMarketOutcome {\n active\n "
36 | "id\n odds\n name\n __typename\n}\n\nfragment BetSlip on SportMarket {\n "
37 | "...MarketFragment\n fixture {\n id\n status\n slug\n tournament "
38 | "{\n ...TournamentTree\n __typename\n }\n data {\n "
39 | "...FixtureDataMatch\n ...FixtureDataOutright\n __typename\n }\n "
40 | "__typename\n }\n __typename\n}\n\nfragment MarketFragment on SportMarket {\n "
41 | "id\n name\n status\n extId\n specifiers\n outcomes {\n id\n active\n "
42 | " name\n odds\n __typename\n }\n __typename\n}\n\nfragment "
43 | "TournamentTree on SportTournament {\n id\n name\n slug\n category "
44 | "{\n id\n name\n slug\n contentNotes {\n id\n createdAt\n "
45 | " publishAt\n expireAt\n linkText\n linkUrl\n message\n "
46 | " publishAt\n __typename\n }\n sport {\n id\n name\n "
47 | "slug\n contentNotes {\n id\n createdAt\n publishAt\n "
48 | " expireAt\n linkText\n linkUrl\n message\n "
49 | "publishAt\n __typename\n }\n __typename\n }\n "
50 | "__typename\n }\n contentNotes {\n id\n createdAt\n publishAt\n "
51 | "expireAt\n linkText\n linkUrl\n message\n publishAt\n "
52 | "__typename\n }\n __typename\n}\n\nfragment FixtureDataMatch on "
53 | "SportFixtureDataMatch {\n startTime\n competitors {\n "
54 | "...Competitor\n __typename\n }\n __typename\n}\n\nfragment "
55 | "Competitor on SportFixtureCompetitor {\n name\n extId\n "
56 | "countryCode\n abbreviation\n __typename\n}\n\nfragment FixtureDataOutright on "
57 | "SportFixtureDataOutright {\n name\n startTime\n endTime\n __typename\n}\n"})
58 |
59 | response = requests.post(AuthClass.API_URL, headers=self.auth_object.get_headers(self.auth_object.read_token()),
60 | data=payload)
61 |
62 | json_resp = json.loads(response.text)
63 |
64 | try:
65 | print(json_resp)
66 | if json_resp['data']['sportBet']['id'] != "":
67 | print("Successful bet on STAKE.COM!")
68 | return True
69 | else:
70 | print("Bet unsuccessful on STAKE.COM!")
71 | return False
72 | except Exception as e:
73 | print("Bet unsuccessful on STAKE.COM!")
74 | print(e)
75 | return False
76 |
77 |
78 | class MoneyLineBet(AutoBet):
79 |
80 | def __init__(self, table_name, stake, currency, auth_object):
81 | super().__init__(table_name, stake, currency, auth_object)
82 |
83 | def get_match(self, start_time, home_player, away_player):
84 | match_df: pd.DataFrame = get_table(self.table_name)
85 |
86 | match = match_df[(match_df["HOME"] == home_player)
87 | &
88 | (match_df["AWAY"] == away_player)
89 | &
90 | (match_df["TIME"] == start_time)]
91 | return match.reset_index().iloc[0]
92 |
93 | def home(self, start_time, home_player, away_player):
94 | data = self.get_match(start_time, home_player, away_player)
95 | return self.send_bet_request(data.HOME_ODDS, data.HOME_ODDS_ID)
96 |
97 | def away(self, start_time, home_player, away_player):
98 | data = self.get_match(start_time, home_player, away_player)
99 | return self.send_bet_request(data.AWAY_ODDS, data.AWAY_ODDS_ID)
100 |
--------------------------------------------------------------------------------
/stake_auth.py:
--------------------------------------------------------------------------------
1 | import json
2 | import requests
3 | import pickle
4 |
5 | import stake_anti_captcha
6 |
7 |
8 | class AuthClass:
9 | API_URL = "https://api.stake.com/graphql"
10 |
11 | def __init__(self, username, password, anti_cpt_api_key):
12 | self.__username = username
13 | self.__password = password
14 | self.__recaptcha_code = ""
15 | self.__login_token = ""
16 | self.__final_token = ""
17 | self.__driver = None
18 | self.__captcha_object = stake_anti_captcha.AntiCaptcha(anti_cpt_api_key)
19 | self.__captcha_code = ""
20 |
21 | @staticmethod
22 | def get_headers(auth_token: str = "") -> dict:
23 | headers = {
24 | 'sec-ch-ua': '"Chromium";v="92", " Not A;Brand";v="99", "Google Chrome";v="92"',
25 | 'accept': '*/*',
26 | 'x-language': 'en',
27 | 'x-lockdown-token': '',
28 | 'sec-ch-ua-mobile': '?0',
29 | 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) '
30 | '/92.0.4515.159 Safari/537.36',
31 | 'content-type': 'application/json',
32 | 'Origin': 'https://stake.com',
33 | 'Referer': 'https://stake.com/'}
34 |
35 | if auth_token:
36 | headers['x-access-token'] = auth_token
37 |
38 | return headers
39 |
40 | def solve_captcha(self):
41 | self.__captcha_code = self.__captcha_object.cycle()
42 |
43 | def read_token(self) -> str:
44 | # FROM EXTERNAL FILE!
45 | try:
46 | with open(f"{self.__username}_token.pkl", "rb") as f:
47 | return pickle.load(f)
48 | except Exception as e:
49 | print("Error while trying to load 'token.pkl'")
50 | print(e)
51 | return ""
52 |
53 | def save_token(self):
54 | # FROM EXTERNAL FILE!
55 | try:
56 | with open(f"{self.__username}_token.pkl", "wb") as f:
57 | pickle.dump(self.__final_token, f)
58 | except Exception as e:
59 | print("Error while saving 'token.pkl'")
60 | print(e)
61 |
62 | def request_login_user(self): # login phase #1
63 | payload = json.dumps({
64 | "operationName": "RequestLoginUser",
65 | "variables": {
66 | "name": self.__username,
67 | "password": self.__password,
68 | "captcha": self.__captcha_code,
69 | },
70 | "query": "mutation RequestLoginUser($name: String, $email: String, $password: String!, "
71 | "$captcha: String) {\n requestLoginUser(name: $name, email: $email, password: "
72 | "$password, captcha: $captcha) {\n loginToken\n hasTfaEnabled\n "
73 | "requiresLoginCode\n user {\n id\n name\n __typename\n }\n"
74 | " __typename\n }\n}\n"
75 | })
76 |
77 | resp = json.loads(
78 | requests.post(AuthClass.API_URL, headers=AuthClass.get_headers(), data=payload).text)
79 | self.__login_token = resp['data']['requestLoginUser']['loginToken']
80 |
81 | def complete_login_user(self): # login phase 2
82 | # This is only optimized for single logins, not for 2FA or email code verifications...
83 | # If your account has 2FA it will throw an error
84 | # You need to add other functions for getting external data from emails/2FA...
85 | # You also need to modify the payload
86 | # Email logincode payload:
87 | # variables-> loginCode: "logincode_received_in_email"
88 |
89 | payload = json.dumps({
90 | "operationName": "CompleteLoginUser",
91 | "variables": {
92 | "loginToken": self.__login_token,
93 | "sessionName": "Chrome (Unknown)",
94 | "blackbox": "blackbox"
95 | },
96 | "query": "mutation CompleteLoginUser($loginToken: String, $tfaToken: String, $sessionName: String!, "
97 | "$blackbox: String, $loginCode: String) {\n completeLoginUser(loginToken: $loginToken, "
98 | "tfaToken: $tfaToken, sessionName: $sessionName, blackbox: $blackbox, loginCode: $loginCode) "
99 | "{\n ...UserAuthenticatedSession\n __typename\n }\n}\n\nfragment UserAuthenticatedSession "
100 | "on UserAuthenticatedSession {\n token\n session {\n ...UserSession\n "
101 | "user {\n ...UserAuth\n __typename\n }\n __typename\n }\n __typename\n}"
102 | "\n\nfragment UserSession on UserSession {\n id\n sessionName\n ip\n "
103 | "country\n city\n active\n updatedAt\n __typename\n}\n\nfragment UserAuth on "
104 | "User {\n id\n name\n email\n hasPhoneNumberVerified\n hasEmailVerified\n "
105 | "hasPassword\n intercomHash\n createdAt\n hasTfaEnabled\n mixpanelId\n "
106 | "hasOauth\n isKycBasicRequired\n isKycExtendedRequired\n isKycFullRequired\n "
107 | "kycBasic {\n id\n status\n __typename\n }\n kycExtended {\n id\n "
108 | "status\n __typename\n }\n kycFull {\n id\n status\n __typename\n }\n "
109 | "flags {\n flag\n __typename\n }\n roles {\n name\n __typename\n }\n "
110 | "balances {\n ...UserBalanceFragment\n __typename\n }\n activeClientSeed {\n id\n "
111 | "seed\n __typename\n }\n previousServerSeed {\n id\n seed\n __typename\n }\n "
112 | "activeServerSeed {\n id\n seedHash\n nextSeedHash\n nonce\n blocked\n "
113 | "__typename\n }\n __typename\n}\n\nfragment UserBalanceFragment on UserBalance {\n "
114 | " available {\n amount\n currency\n __typename\n }\n vault {\n amount\n "
115 | "currency\n __typename\n }\n __typename\n}\n"
116 | })
117 |
118 | resp = json.loads(
119 | requests.post(AuthClass.API_URL, headers=AuthClass.get_headers(), data=payload).text)
120 | self.__final_token = resp['data']['completeLoginUser']['token']
121 |
122 | def login_check(self) -> bool:
123 | if not self.read_token():
124 | print("Not logged in to Stake.com!")
125 | return False
126 |
127 | payload = json.dumps({"operationName": "UserVaultBalances", "variables": {},
128 | "query": "query UserVaultBalances {\n user {\n id\n "
129 | "balances {\n available {\n amount\n "
130 | " currency\n __typename\n }\n "
131 | "vault {\n amount\n currency\n __"
132 | "typename\n }\n __typename\n }\n __typename\n }\n}\n"})
133 |
134 | try:
135 | response = json.loads(
136 | requests.post(AuthClass.API_URL, headers=AuthClass.get_headers(self.read_token()), data=payload).text)
137 | if response['data']['user']['id']:
138 | print(f"Logged in as {self.__username} to stake.com!")
139 | return True
140 | else:
141 | print("Not logged in!")
142 | return False
143 | except Exception as e:
144 | # If the request fails, we just simply return False
145 | print("Not logged in!")
146 | print(e)
147 | return False
148 |
149 | def cycle(self):
150 | # Intended to use as a constant loop
151 | if self.login_check():
152 | return True
153 | else:
154 | self.solve_captcha()
155 | self.request_login_user()
156 | self.complete_login_user()
157 | self.save_token()
158 |
--------------------------------------------------------------------------------
/stake_scraper.py:
--------------------------------------------------------------------------------
1 | import json
2 | import requests
3 | import pickle
4 | import pandas as pd
5 | from abc import ABC, abstractmethod
6 |
7 | from stake_auth import AuthClass
8 | from stake_postgresql import get_table, set_table
9 |
10 |
11 | class EventFiles(ABC):
12 |
13 | # Class for grouping event file related functions and variables
14 |
15 | def __init__(self, sport, live):
16 | self.sport = sport
17 | self.live = live
18 |
19 | if self.live:
20 | self.__event_file_name = sport + "_event_live.pkl"
21 | else:
22 | self.__event_file_name = sport + "_event.pkl"
23 |
24 | self.dataframe = pd.DataFrame()
25 | self.match_events = {} # Must be used by EventScraper for storing event data
26 |
27 | def save_event_file(self):
28 | with open(self.__event_file_name, "wb") as f:
29 | pickle.dump(self.match_events, f)
30 |
31 | def get_event_file(self):
32 | with open(self.__event_file_name, "rb") as f:
33 | return pickle.load(f)
34 |
35 |
36 | class EventScraper(EventFiles):
37 |
38 | def __init__(self, sport, live):
39 | super().__init__(sport, live)
40 | self.__actual_sport_id = self.__get_sport_id()
41 |
42 | def __get_sport_id(self):
43 | """ Reads the sport_id from the database and returns it!"""
44 |
45 | sport_id_dataframe = get_table("sport_ids")
46 |
47 | sport_id = sport_id_dataframe[sport_id_dataframe["SPORT"] == self.sport].iloc[0].SPORT_ID
48 |
49 | return sport_id
50 |
51 | def get_event_payload(self):
52 |
53 | # API QUERY REPLICATED AS IS, DIRTY CODE AHEAD....
54 |
55 | if self.live:
56 | payload = json.dumps({"operationName": "liveSportFixtureList", "variables":
57 | {"tournamentLimit": 50, "sportId": self.__actual_sport_id, "groups": "winner"},
58 | "query":
59 | "query liveSportFixtureList"
60 | "($sportId: String!, $groups: String!, $tournamentLimit: Int = 25) "
61 | "{\n sport(sportId: $sportId) {\n id\n tournamentList(type: live, "
62 | "limit: $tournamentLimit) {\n ...TournamentTreeFragment\n "
63 | "fixtureList(type: live) {\n ...FixturePreviewFragment\n "
64 | "groups(groups: [$groups], status: [active, suspended, deactivated]) "
65 | "{\n "
66 | "...GroupFixtureFragment\n __typename\n }\n "
67 | "__typename\n "
68 | " }\n __typename\n }\n __typename\n }\n}\n\nfragment "
69 | "GroupFixtureFragment on SportGroup {\n ...Group\n templates {\n "
70 | "...TemplateFragment\n markets {\n ...MarketFragment\n "
71 | "__typename\n }\n __typename\n }\n "
72 | "__typename\n}\n\nfragment MarketFragment on "
73 | "SportMarket {\n id\n name\n status\n "
74 | "extId\n specifiers\n outcomes {\n "
75 | "id\n active\n name\n odds\n "
76 | "__typename\n }\n __typename\n}\n\nfragment "
77 | "TemplateFragment on SportGroupTemplate {\n "
78 | "extId\n rank\n __typename\n}\n\nfragment "
79 | "Group on SportGroup {\n name\n "
80 | "translation\n rank\n "
81 | "__typename\n}\n\nfragment "
82 | "FixturePreviewFragment "
83 | "on SportFixture {\n id\n extId\n status\n slug\n "
84 | "marketCount(status: [active, suspended])\n data "
85 | "{\n ...FixtureDataMatchFragment\n "
86 | "...FixtureDataOutrightFragment\n __typename\n }"
87 | "\n eventStatus {\n ...FixtureEventStatus\n "
88 | "__typename\n }\n tournament {\n "
89 | "...TournamentTreeFragment\n "
90 | "__typename\n }\n ...LiveStreamExistsFragment\n __typename\n}\n\nfragment "
91 | "FixtureDataMatchFragment on SportFixtureDataMatch {\n startTime\n competitors "
92 | "{\n ...CompetitorFragment\n __typename\n }\n __typename\n}\n\nfragment "
93 | "CompetitorFragment on SportFixtureCompetitor {\n name\n extId\n "
94 | "countryCode\n abbreviation\n __typename\n}\n\nfragment "
95 | "FixtureDataOutrightFragment on SportFixtureDataOutright "
96 | "{\n name\n startTime\n endTime\n "
97 | "__typename\n}\n\nfragment FixtureEventStatus on SportFixtureEventStatus "
98 | "{\n homeScore\n awayScore\n matchStatus\n clock {\n "
99 | "matchTime\n remainingTime\n __typename\n }\n periodScores "
100 | "{\n homeScore\n awayScore\n matchStatus\n __typename\n }"
101 | "\n currentServer {\n extId\n __typename\n }\n "
102 | "homeGameScore\n awayGameScore\n statistic {\n "
103 | "yellowCards {\n away\n home\n "
104 | "__typename\n }\n redCards {\n away\n "
105 | "home\n __typename\n }\n corners {\n "
106 | "home\n away\n __typename\n }\n "
107 | "__typename\n }\n __typename\n}\n\nfragment "
108 | "TournamentTreeFragment on SportTournament "
109 | "{\n id\n name\n slug\n category "
110 | "{\n id\n name\n "
111 | "slug\n sport {\n id\n name\n slug\n __typename\n }"
112 | "\n __typename\n }\n __typename\n}\n\nfragment LiveStreamExistsFragment on "
113 | "SportFixture {\n abiosStream {\n exists\n __typename\n }\n "
114 | "betradarStream {\n exists\n __typename\n }\n diceStream {\n "
115 | "exists\n __typename\n }\n __typename\n}\n"})
116 | else:
117 | payload = json.dumps({"operationName": "SportFixtureList",
118 | "variables": {"type": "upcoming", "sportId": self.__actual_sport_id,
119 | "groups": "winner", "limit": 50, "offset": 0},
120 | "query": "query SportFixtureList($type: SportSearchEnum!, $sportId: String!, "
121 | "$groups: String!, $limit: Int!, $offset: Int!) {\n sport(sportId: "
122 | "$sportId) {\n id\n name\n fixtureCount(type: $type)\n "
123 | "fixtureList(type: $type, limit: $limit, offset: $offset) {\n "
124 | "...FixturePreview\n groups(groups: [$groups], status: [active, "
125 | "suspended, deactivated]) {\n ...GroupFixture\n "
126 | "__typename\n }\n __typename\n }\n __typename\n }\n}\n\n"
127 | "fragment FixturePreview on SportFixture {\n id\n status\n slug\n "
128 | "betradarStream {\n exists\n __typename\n }\n marketCount(status: "
129 | "[active, suspended])\n data {\n ...FixtureDataMatch\n "
130 | "...FixtureDataOutright\n __typename\n }\n tournament {\n "
131 | "...TournamentTree\n __typename\n }\n eventStatus {\n "
132 | "...FixtureEventStatus\n __typename\n }\n __typename\n}\n\nfragment "
133 | "FixtureDataMatch on SportFixtureDataMatch {\n startTime\n "
134 | "competitors {\n ...Competitor\n __typename\n }\n "
135 | "__typename\n}\n\nfragment Competitor on "
136 | "SportFixtureCompetitor {\n name\n extId\n countryCode\n "
137 | "abbreviation\n __typename\n}\n\nfragment FixtureDataOutright "
138 | "on SportFixtureDataOutright {\n name\n startTime\n endTime\n "
139 | "__typename\n}\n\nfragment TournamentTree on SportTournament "
140 | "{\n id\n name\n slug\n category {\n id\n name\n "
141 | "slug\n contentNotes {\n id\n createdAt\n "
142 | "publishAt\n expireAt\n linkText\n linkUrl\n "
143 | "message\n publishAt\n __typename\n }\n "
144 | "sport {\n id\n name\n slug\n "
145 | "contentNotes {\n id\n createdAt\n "
146 | "publishAt\n expireAt\n linkText\n "
147 | "linkUrl\n message\n publishAt"
148 | "\n __typename\n }\n "
149 | " __typename\n }\n __typename\n }\n contentNotes {\n id\n "
150 | "createdAt\n publishAt\n expireAt\n linkText\n linkUrl\n "
151 | "message\n publishAt\n __typename\n }\n __typename\n}\n\nfragment "
152 | "FixtureEventStatus on SportFixtureEventStatus {\n homeScore\n "
153 | "awayScore\n matchStatus\n clock {\n matchTime\n "
154 | "remainingTime\n __typename\n }\n periodScores {\n "
155 | "homeScore\n awayScore\n matchStatus\n __typename\n }\n "
156 | "currentServer {\n extId\n __typename\n }\n homeGameScore\n "
157 | "awayGameScore\n statistic {\n yellowCards {\n away\n "
158 | "home\n __typename\n }\n redCards {\n away\n "
159 | "home\n __typename\n }\n corners {\n home\n "
160 | "away\n __typename\n }\n __typename\n }\n "
161 | "__typename\n}\n\nfragment GroupFixture on SportGroup {\n ..."
162 | "Group\n templates {\n ...Template\n markets {\n "
163 | "...MarketFragment\n __typename\n }\n __typename\n }\n "
164 | "__typename\n}\n\nfragment Group on SportGroup {\n name\n "
165 | "translation\n rank\n __typename\n}\n\nfragment Template on "
166 | "SportGroupTemplate {\n extId\n rank\n __typename\n}\n\nfragment "
167 | "MarketFragment on SportMarket {\n id\n name\n status\n extId\n "
168 | "specifiers\n outcomes {\n id\n active\n name\n odds\n "
169 | " __typename\n }\n __typename\n}\n"})
170 | return payload
171 |
172 | def scrape_events(self):
173 | resp = requests.post(AuthClass.API_URL, headers=AuthClass.get_headers(), data=self.get_event_payload(),
174 | timeout=5)
175 |
176 | self.match_events = json.loads(resp.text)
177 |
178 | def cycle(self):
179 | """ Helper function for the extract process! """
180 | self.scrape_events()
181 | self.save_event_file()
182 |
183 |
184 | class DataParser(EventFiles):
185 |
186 | def __init__(self, sport, live):
187 | super().__init__(sport, live)
188 |
189 | self.start_time = [] # List of scraped start times...
190 | self.home_team = [] # List of home players...
191 | self.away_team = [] # List of away players...
192 |
193 | self.table_name = "" # Table name for the database. Must be filled in every subclass!
194 |
195 | def get_fixture_list(self):
196 |
197 | # Both live and pre parsing are based on fixture lists..
198 | # So it could be wise to extract the relevant fixture list and return it to avoid code duplication
199 | # The get_event_file returns the data from the external file!
200 |
201 | fixture_list = []
202 | match_data = self.get_event_file()
203 | if self.live:
204 | for tournament in match_data['data']['sport']['tournamentList']:
205 | for match in tournament['fixtureList']:
206 | fixture_list.append(match)
207 | else:
208 | for match in match_data['data']['sport']['fixtureList']:
209 | fixture_list.append(match)
210 |
211 | return fixture_list
212 |
213 | def cleaner_base(self):
214 | # This function is to clean all common data lists
215 | self.start_time.clear()
216 | self.home_team.clear()
217 | self.away_team.clear()
218 | self.dataframe = pd.DataFrame()
219 |
220 | @abstractmethod
221 | def cleaner_individual(self):
222 | # This function is to scrape individual markets!
223 | # like self.market or etc...
224 | pass
225 |
226 | def cleaner(self):
227 | self.cleaner_base()
228 | self.cleaner_individual()
229 |
230 | @abstractmethod
231 | def parse_data(self):
232 | """ This function is to parse raw data from the fixture list! """
233 | pass
234 |
235 | @abstractmethod
236 | def build_dataframe(self):
237 | """ This function is to build the DataFrame which has the event and odds data """
238 | pass
239 |
240 | def load_dataframe(self):
241 | """ Upload the dataframe to the database! """
242 |
243 | set_table(self.dataframe, self.table_name)
244 |
245 | def cycle(self):
246 | """ Helper function to do the full ETL process """
247 |
248 | self.cleaner()
249 | self.parse_data()
250 | self.build_dataframe()
251 | self.load_dataframe()
252 |
--------------------------------------------------------------------------------