nba-playoffs-game-updater.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310
  1. #!/usr/bin/env python3
  2. import gspread
  3. import random
  4. import datetime
  5. import time
  6. from nba_api.stats.static import players
  7. from nba_api.stats.endpoints import playergamelog
  8. import urllib
  9. try:
  10. from nba_api.library.debug.debug import DEBUG_STORAGE
  11. except ImportError:
  12. DEBUG_STORAGE = False
  13. # spreadsheet_key = '1fmyoRvyaCflU2YeUD2YnuOthy76fDolBrtYxQF5Io20' # 2025 Test
  14. spreadsheet_key = '1ip3IFpvE2v9dOijwDeYp0EjzlFLfqFqfx4m8-nzJaoA' # 2025 Official
  15. json_keyfile = 'NBA Playoffs Game-1f9a46f0715c.json'
  16. day = 'today'
  17. # day = datetime.date(2025, 4, 16) # set date manually
  18. nba_cooldown = random.gammavariate(alpha=9, beta=0.4) # don't hammer the NBA.com API
  19. stats=['PTS', 'REB', 'AST', 'STL', 'BLK', 'TOV', 'WL'] # stats appear in this order
  20. STATS_HEADERS = {
  21. 'Host': 'stats.nba.com',
  22. 'Connection': 'keep-alive',
  23. 'Cache-Control': 'max-age=0',
  24. 'Upgrade-Insecure-Requests': '1',
  25. 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36',
  26. 'Accept': 'application/json, text/plain, */*',
  27. 'Accept-Encoding': 'gzip, deflate, br',
  28. 'Accept-Language': 'en-US,en;q=0.9',
  29. 'Referer': 'https://www.nba.com/',
  30. 'Origin': 'https://www.nba.com',
  31. }
  32. # Proxy URLs
  33. #proxy_url="https://raw.githubusercontent.com/clarketm/proxy-list/master/proxy-list-raw.txt"
  34. proxy_url="https://raw.githubusercontent.com/TheSpeedX/PROXY-List/refs/heads/master/http.txt"
  35. # Manual list of proxies
  36. proxies = []
  37. def buildProxyList(proxies=[], raw_text_url=proxy_url):
  38. good_proxy_list = []
  39. proxy_list = []
  40. r = urllib.request.urlopen(raw_text_url)
  41. for line in r:
  42. line = line.decode("utf-8")
  43. line = line.strip()
  44. proxy_list.append(line)
  45. random.shuffle(proxy_list)
  46. proxy_list = proxies + proxy_list
  47. return proxy_list, good_proxy_list
  48. """
  49. Returns a worksheet instance
  50. """
  51. def getWorksheet(spreadsheet_key, json_keyfile):
  52. try:
  53. gc = gspread.service_account(filename=json_keyfile)
  54. return gc.open_by_key(spreadsheet_key).worksheet("Picks")
  55. except Exception as e:
  56. print(f"Exception: {str(e)}")
  57. print("Could not retrieve worksheet!")
  58. print("Check your API key, credentials, or network!")
  59. raise(e)
  60. """
  61. Returns a list of lists containing the values of all cells in the worksheet by row
  62. """
  63. def getAllValues(worksheet):
  64. return worksheet.get_all_values()
  65. """
  66. Create various date variables based on "today's" day
  67. """
  68. def setDates(day):
  69. if day == 'today':
  70. # in case games go past midnight
  71. date = datetime.datetime.now() - datetime.timedelta(hours=3)
  72. date = date.date()
  73. else:
  74. date = day
  75. url_date = date.strftime('%m/%d/%Y')
  76. year = date.year
  77. season = f"{format(str(year - 1))}-{str(year)[2:]}"
  78. return url_date, season, date
  79. """
  80. Determines the number of players in the pool
  81. """
  82. def getNumberOfParticipants(all_values):
  83. start=3
  84. end=3
  85. count=0
  86. for row_num, row in enumerate(all_values):
  87. if row[0] != "" and row_num >= 3 and count == 0:
  88. start=row_num
  89. count+=1
  90. elif row[0] != "" and row_num >= 3 and count == 1:
  91. end=row_num
  92. break
  93. num_participants = end - start
  94. return num_participants
  95. """
  96. Determines the active day's first and last rows
  97. """
  98. def getFirstRowLastRow(all_values, num_participants, current_date):
  99. first_row = None
  100. last_row = None
  101. for row_num, row in enumerate(all_values, start=1):
  102. date=row[0]
  103. if date != "" and row_num >= 4:
  104. day = datetime.datetime.strptime('{} {}'.format(
  105. date,
  106. str(current_date.year)),
  107. '%A, %B %d %Y'
  108. )
  109. if day.date() == current_date:
  110. first_row = row_num
  111. last_row = first_row + num_participants - 1
  112. break
  113. return first_row, last_row
  114. """
  115. Rudimentary way to reduce player name errors
  116. """
  117. def cleanFirstNameLastName(player):
  118. first_name_last_name = player.split()
  119. first_name = first_name_last_name[0]
  120. first_name = first_name.replace('.', '')
  121. # New nickname for T.J. Warren should be "The Outlier"
  122. if first_name == "TJ":
  123. first_name = "T.J."
  124. elif first_name == "Donavan":
  125. first_name = "Donovan"
  126. elif first_name == "Domantis":
  127. first_name = "Domantas"
  128. last_name = first_name_last_name[1]
  129. player_clean = first_name + ' ' + last_name
  130. return player_clean
  131. """
  132. Create a unique list of players that have been selected today
  133. Also, append misspelled players to batch_update_list to autofix on next push if we can
  134. """
  135. def cleanPlayers(all_values, first_row, last_row, batch_update_list):
  136. players_unique = []
  137. for row_num, row in enumerate(all_values, start=1):
  138. if first_row <= row_num <= last_row:
  139. player = row[2]
  140. if player[-7:] != "-FIX!!!" and player != "":
  141. if len(players.find_players_by_full_name(player)) > 0:
  142. players_unique.append(player)
  143. else:
  144. player_clean = cleanFirstNameLastName(player)
  145. if len(players.find_players_by_full_name(player_clean)) > 0:
  146. all_values[row_num - 1][2] = player_clean
  147. batch_update_list.append({'range': f'{indexToLetter(2)}{row_num}', 'values': [[player_clean]]})
  148. players_unique.append(player_clean)
  149. else:
  150. print(f"Player: {player} not found, please fix name!")
  151. players_unique = list(dict.fromkeys(players_unique))
  152. return players_unique, batch_update_list, all_values
  153. """
  154. Pull player's gamelog from stats.nba.com based on the url_date and player_id
  155. """
  156. #@timeout_decorator.timeout(30)
  157. def getStats(players_unique, url_date, season, proxy_list=[], good_proxy_list=[]):
  158. stats_dict = {}
  159. for player in players_unique:
  160. player_info = players.find_players_by_full_name(player)
  161. player_id = player_info[0].get('id')
  162. print(f'Retrieving stats for: {player}')
  163. while True:
  164. # Move working proxies to the front of the list
  165. if len(good_proxy_list) > 0:
  166. proxy_list = good_proxy_list + proxy_list
  167. # Remove duplicate proxies
  168. proxy_list = list(dict.fromkeys(proxy_list))
  169. # Use the first proxy in the list
  170. request_proxy = proxy_list[0]
  171. try:
  172. print(f'Proxy: http://{request_proxy}')
  173. player_game_log = playergamelog.PlayerGameLog(
  174. player_id=player_id,
  175. proxy='http://' + request_proxy,
  176. season=season,
  177. timeout=10,
  178. league_id_nullable='00',
  179. season_type_all_star='Playoffs',
  180. date_from_nullable=url_date,
  181. date_to_nullable=url_date,
  182. )
  183. print('Success!')
  184. if request_proxy not in good_proxy_list:
  185. good_proxy_list.append(request_proxy)
  186. player_game_log_dict = player_game_log.get_dict()
  187. if DEBUG_STORAGE is False:
  188. time.sleep(nba_cooldown)
  189. break
  190. except OSError as e:
  191. print(e)
  192. if request_proxy in good_proxy_list:
  193. good_proxy_list.remove(request_proxy)
  194. else:
  195. print(f'Proxy refused, removing {request_proxy}')
  196. proxy_list.remove(request_proxy)
  197. continue
  198. except Exception as e:
  199. print(e)
  200. print('Could not connect to the NBA API, sleeping for 30 seconds')
  201. time.sleep(30)
  202. player_game_log_results = player_game_log_dict.get('resultSets')[0]
  203. player_game_log_headers = player_game_log_results.get('headers')
  204. # if player has no stats for this day, list will be empty
  205. if len(player_game_log_results.get('rowSet')) < 1:
  206. player_stats_dict = None
  207. else:
  208. player_game_log_stats = player_game_log_results.get('rowSet')[0]
  209. player_stats_dict = dict(zip(player_game_log_headers, player_game_log_stats))
  210. stats_dict[player] = player_stats_dict
  211. return stats_dict, good_proxy_list
  212. """
  213. Append stat cells that have changes to batch_update_list
  214. Also append player cells that need fixing to batch_update_list
  215. """
  216. def cellsToUpdate(all_values, first_row, last_row, stats_dict, stats, batch_update_list):
  217. for row_num, row in enumerate(all_values, start=1):
  218. if first_row <= row_num <= last_row:
  219. player_name = row[2]
  220. if player_name[-7:] != "-FIX!!!" and player_name in stats_dict.keys():
  221. if stats_dict[player_name] is not None:
  222. player_stats = stats_dict[player_name]
  223. if player_stats == "Fix!":
  224. batch_update_list.append({'range': f'{indexToLetter(2)}{row_num}', 'values': [[f'{player_name}-FIX!!!']]})
  225. continue
  226. for col_num, stat in enumerate(stats, start=3):
  227. pass
  228. #print(player_name, player_stats[stat])
  229. #print(player_name, f'{indexToLetter(col_num)}{row_num}', str(row[col_num]), f',', player_stats[stat])
  230. if str(player_stats[stat]) != str(row[col_num]) and player_stats[stat] is not None:
  231. #print('Update:', row_num, col_num, player_name, f'{indexToLetter(col_num)}{row_num}', str(row[col_num]), player_stats[stat])
  232. batch_update_list.append({'range': f'{indexToLetter(col_num)}{row_num}', 'values': [[f'{player_stats[stat]}']]}.copy())
  233. return batch_update_list
  234. """
  235. Convert zero-indexed column number to the appropriate column letter (A=0, B=1, C=2...)
  236. """
  237. def indexToLetter(index):
  238. return chr(ord('@')+int(index)+1)
  239. """
  240. Push changes to Google Sheet
  241. """
  242. def batchUpdate(batch_update_list):
  243. if len(batch_update_list) > 1:
  244. worksheet.batch_update(batch_update_list, value_input_option="USER_ENTERED")
  245. else:
  246. print('No update needed, sleeping for 1 minute')
  247. time.sleep(60)
  248. if __name__ == "__main__":
  249. # Use a combination of our good proxies with some fetched from the internet for variation
  250. proxy_list, good_proxy_list = buildProxyList(proxies=proxies, raw_text_url=proxy_url)
  251. while True:
  252. try:
  253. batch_update_list = []
  254. worksheet = getWorksheet(spreadsheet_key, json_keyfile)
  255. url_date, season, date = setDates(day)
  256. print("Date: " + str(date))
  257. all_values = getAllValues(worksheet)
  258. num_participants = getNumberOfParticipants(all_values)
  259. first_row, last_row = getFirstRowLastRow(all_values, num_participants, date)
  260. if first_row is None:
  261. print("No games today! Pausing for 1000 seconds...")
  262. time.sleep(1000)
  263. continue
  264. players_unique, batch_update_list, all_values = cleanPlayers(all_values, first_row, last_row, batch_update_list)
  265. stats_dict, good_proxy_list = getStats(players_unique, url_date, season, proxy_list=proxy_list, good_proxy_list=good_proxy_list)
  266. batch_update_list = cellsToUpdate(all_values, first_row, last_row, stats_dict, stats, batch_update_list)
  267. if len(batch_update_list) > 0:
  268. print(batch_update_list)
  269. batchUpdate(batch_update_list)
  270. except Exception as e:
  271. print(e)
  272. print('Sleeping for 10 seconds')
  273. time.sleep(10)
  274. continue