
    =jR?                        S r SSKrSSKrSSKrSSKrSSKrSSKrSSKrSSK	r
SSKr
SrSrSr1 SkrSSS.rS	rS
rSrSrSrSrSrSrSSSSSSS.r0 SS0SS0SS0/rSqS\SS4S jrS rS rS rS  r S! r!S" r"S# r#S$ r$S*S% jr%S& r&\'S':X  a	   \&" 5         gg! \( a%  r)\*" S(\)5        \RV                  " S)5         Sr)C)gSr)C)ff = f)+a  
fetch_players.py  --  Pull the latest NFL rosters (name, team, position, headshot)
from ESPN's free public API, enrich them with real fantasy ADP and team logos,
add one draftable D/ST per team, and write players.json next to draftboard.html.

Run this on draft day (double-click 1_fetch_players.bat on Windows, or `python fetch_players.py`).
No API key required.

Data sources (all free, no key):
  * ESPN rosters + team logos ....... site.api.espn.com
  * Fantasy ADP ..................... fantasyfootballcalculator.com public API

Only offensive/kicking skill positions relevant to fantasy are kept by default, plus a
D/ST for every team. Flip KEEP_ALL = True to include everyone (IDP leagues, etc.).
    NFT>   KQBRBTEWRr   r   )PKFB   z@https://site.api.espn.com/apis/site/v2/sports/football/nfl/teamszMhttps://site.api.espn.com/apis/site/v2/sports/football/nfl/teams/{tid}/rosterz<https://a.espncdn.com/i/headshots/nfl/players/full/{pid}.pngz4https://a.espncdn.com/i/teamlogos/nfl/500/{abbr}.pngzGhttps://fantasyfootballcalculator.com/api/v1/adp/ppr?teams=12&year=2026g33333?@zXhttps://site.web.api.espn.com/apis/common/v3/sports/football/nfl/athletes/{pid}/overviewWSHJAXLARLVLAC)WASJACLAOAKSDSTLz
User-AgentzoMozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36zMozilla/5.0zDraftBoard/1.0   c                 T   Sn[        U5       H  n[        [        [        [        5      5      S S9nU Hq  n[	        [        U   5      n	U	R                  U=(       d    0 5         [        R                  R                  X	US9n
[        R                  R                  XS9nUq
Us  s  $    XdS-
  :  d  M  [        R                  " SS	U-  -   [         R                   " 5       -   5        M     Ue! [        R                  R                   a  nUnUR                  S;   a   SnAM  e SnAff = f)
zurlopen that survives BOTH of ESPN's defences:
* shifting User-Agent blocking -> rotate header profiles, lock onto what works
* rolling rate limits on sustained volume -> back off and retry the whole set
Nc                     U [         :X  a  S$ S$ )Nr      )_profile)is    IC:\Users\Matth\OneDrive\Projects\AI\FantasyFootball\2026\fetch_players.py<lambda>_open.<locals>.<lambda>C   s    1=a>WVW>W    key)headersmethodtimeout)i  i  r   g      ?g       @)rangesortedlenUA_PROFILESdictupdateurllibrequestRequesturlopenr   error	HTTPErrorcodetimesleeprandom)urlr"   r%   r#   roundslastrndorderr   hreqres                r   _openr?   ;   s     DV}uS-.4WXA[^$AHHW]#	nn,,SF,KNN**3*@  !JJsS3Y89! " J <<)) 66Z'	s   $>C++D'	D"!D""D'c                 z    [        U 5       n[        R                  " U5      sS S S 5        $ ! , (       d  f       g = f)N)r?   jsonload)r6   r=   s     r   getrC   V   s     	sqyy| 
s   ,
:c                 2   U =(       d    SR                  5       R                  SS5      R                  SS5      R                  SS5      n [        R                  " SSU 5      n [        R                  " SSU 5      n SR	                  U R                  5       5      $ )zENormalize a player name so it matches across ESPN and the ADP source. .'- z\b(jr|sr|ii|iii|iv|v)\bz[^a-z ])lowerreplaceresubjoinsplit)ss    r   	norm_namerQ   [   ss    	
b!!#r*223;CCCMA
)2q1A
z2q!A88AGGIr   c            
         0 0 0 p!n  [        [        5      nUR                  S/ 5       H  n[        R                  UR                  SS5      UR                  SS5      5      nUR                  S5      (       a  U(       a  UR                  S5      X&'   UR                  S5      nUc  M  UR                  S	5      S
:X  a  XqU'   M  [        UR                  S5      5      nU(       d  M  X;  d
  XpU   :  d  M  XpU'   M     [        S[        U 5       S[        U5       S[        U5       S35        XU4$ ! [         a  n[        SU S35        XU4s SnA$ SnAff = f)zOReturn (by_name, by_def, by_bye): name->adp, team->defense adp, team->bye week.z  !! ADP fetch failed (z); players will load unrankedNplayersteamrE   byeadppositionDEFnamez  ADP loaded: z players + z defenses + z byes)rC   ADP_URL	ExceptionprintTEAM_FIXrQ   r(   )	by_nameby_defby_byedatar>   ptmrV   r!   s	            r   	fetch_adprd   c   s7    "bVG'7| XXi$\\!%%+QUU62->?55<<BuFJeeEl;55%2JAEE&M*Cs*cCL.@" % 
N3w<.CK=SQW[MY^
_`F""#  ''s*GHI&&'s   D0 0
E:EEEc                     [        [        R                  U S9SS9 n[        R                  " U5      nSSS5        WR                  S5      =(       d    0 nUR                  S5      UR                  S5      pTU(       dH  UR                  S5      =(       d    / nU(       a'  US	   R                  S5      US	   R                  S5      pTU(       d  gS
R                  UR                  5       5      n[        U5      S:  a  USS R                  5       S-   nXE=(       d    SSS S.$ ! , (       d  f       N= f! [
         a     gf = f)zMReturn {'news': headline, 'newsDate': 'YYYY-MM-DD'} for one athlete, or None.pid   r$   Nrotowireheadline	publishednewsr   rI         u   …rE   
   )rl   newsDate)r?   NEWS_URLformatrA   rB   r[   rC   rN   rO   r(   rstrip)rg   r=   drwheaddatearrs          r   player_newsry   |   s   8??s?+R8A		!A 9 
z		 bB
#RVVK%8$eeFm!rQJ/QK1H$88DJJL!D
4y3DSz  "U*zr3B&788 98 s-   D3 D"D3 "
D0,D3 0D3 3
E ?E c           	         U  Vs/ s H6  oS   [         :  d  M  [        US   5      R                  5       (       d  M4  UPM8     nn[        S[	        U5       S35        Sn[
        R                  R                  SS9 nU Vs0 s H  oR                  [        US   5      U_M     nn[
        R                  R                  U5       H6  nUR                  5       nU(       d  M  US   US	   sXV   S'   XV   S	'   US
-  nM8     SSS5        [	        U5      U-
  n[        SU S3U(       a  SU S3OS-   5        gs  snf s  snf ! , (       d  f       NF= f)zMAttach recent news to ranked, real-athlete players using a small thread pool.rV   idzFetching news for z ranked players ...r      max_workersrl   rp   r   Nz  got news for  playersz  (!! u/    failed — ESPN throttling; re-run to fill in)rE   )ADP_UNRANKEDstrisdigitr\   r(   
concurrentfuturesThreadPoolExecutorsubmitry   as_completedresult)	rS   rb   targetsgotexfutsfutresmisss	            r   add_newsr      s5   !X'QuX%<qQtWAUAUAWq'GX	s7|n,?
@A
C				.	.1	.	=<CDGq		+qw/2GD%%2248C**,Cs;>v;J8	&!49Z#8q	 9 
> w<#D	OC5
)osvdV;j-ky{
|} Y E 
>	=s3   D4D4D4;D> #D9#9D> D>9D>>
Ec                      [        U SSS9 n[        USS5      S:H  sSSS5        $ ! , (       d  f       g= f! [        R                  R                   a  nUR
                  S:g  s SnA$ SnAf[         a     gf = f)	zCTrue if the headshot exists; only a definite 404 counts as missing.   HEAD)r%   r#   status   Ni  T)r?   getattrr,   r0   r1   r2   r[   )r6   r=   r>   s      r   headshot_okr      sd    3&1Q1h,3 211<<!! vv} s5   8 '	8 
58 8 A:A*$A:*A:9A:c           	         U  Vs/ s H@  n[        US   5      R                  5       (       d  M&  UR                  S5      (       d  M>  UPMB     nn[        S[	        U5       S35        S[        5       pC[        R                  R                  SS9 nU Vs0 s H  oR                  [        US   5      U_M     nn[        R                  R                  U5       HE  nUR                  5       (       a  M  SXg   S'   UR                  [        Xg   S   5      5        US	-  nMG     S
S
S
5        [        SU S35        U$ s  snf s  snf ! , (       d  f       N)= f)zYBlank headshots that 404 so the board doesn't spew console errors / broken-image flicker.r{   imgzValidating z headshots ...r   r   r}   rE   r   Nz
  blanked z missing headshots)r   r   rC   r\   r(   setr   r   r   r   r   r   r   add)rS   rb   r   removedblankedr   r   r   s           r   validate_headshotsr      s   !M'QS4\%9%9%;qeq'GM	KG~^
45#%W				.	.1	.	==DEW		+qx0!3WE%%2248C::<<#%	% C	$011	 9 
> 
Jwi1
23N N F 
>	=s4   %D9D9D9E#D>67E1/E>E
Ec            	          [        [        R                  R                  [        R                  R	                  [        R                  R                  [        5      5      S5      SS9 n [        R                  " U 5      S    Vs0 s H  n[        US   5      U_M     snsSSS5        $ s  snf ! , (       d  f       g= f! [         a    0 s $ f = f)zYPrevious players.json keyed by id, so a THROTTLED re-run never loses data it already had.players.jsonutf-8encodingrS   r{   N)openospathrN   dirnameabspath__file__rA   rB   r   r[   )frb   s     r   
load_priorr      s    "'',,rwwrwwx/HI>Z"$'(-1YYq\)-DE-DC$L!O-DE$ $E$ $  	sB   A&B? (B.B)B.	B? )B..
B<8B? <B? ?CCc                    U(       d  U $ SnU Vs0 s H  oUS_M     nnS=px[        5       n	U  GH+  n
U	R                  [        U
S   5      5        UR                  [        U
S   5      5      nU(       d  MG  U HE  nU
R                  U5      (       a  M  UR                  U5      (       d  M3  X   X'   Xe==   S-  ss'   MG     U
R                  S5      (       d5  UR                  S5      (       a  [        U
S   5      U;  a  US   U
S'   US-  nU
R                  S[        5      [        :  d  M  UR                  S[        5      [        :  d  GM  US   U
S'   US-  nGM.     SnUR                  5        HB  u  pX;   a  M  UR                  S5      U;   d  M#  U R                  [        U5      5        US-  nMD     UR                  5        VVs/ s H  u  p^U(       d  M  U SU 3PM     nnnU(       a  UR                  U S	35        U(       a  UR                  U S
35        U(       a  UR                  U S35        [        SU(       a  SR                  U5      OS-   5        U $ s  snf s  snnf )a	  NEVER lose data. Anything this run failed to pull (ESPN throttles sustained volume, or a
whole team's roster request can fail) falls back to what we already had, so re-running only
ever ADDS. The one exception: headshots proven missing by a 404 THIS run stay blank.)rl   rp   logorU   inj	injDetailinjRetageexpjerseyteamNamerT   rY   r   r{   r   r   rV   rT   rI   z
 headshotsz ADP valuesz players from failed rostersz%  carried forward from the last run: z, znothing needed)
r   r   r   rC   r   itemsappendr*   r\   rN   )rS   priorr   failed_teamsCARRYkfilledimg_backadp_backhaverb   oldreaddedrg   vbitss                   r   merge_priorr      s
    AE!"EqdEF"H5DQtWiiAdG%A5588

v	Q	 
 uuU||3qw<w3N5zAeHMH55%5#''%:VYe:e5zAeHMH! $ GKKM;776?l*NN49%qLG " $*<<>7>41QJqc1#J>D7xj
+,xj,-wi;<=	
1TYYt_Rb
cdNI #8 8s   I I%I%c                  \   [        5       n [        S5        [        [        5      nUS   S   S   S   S   n[        S[	        U5       S35        [        S5        [        5       u  p4n/ n[        5       n[        5       n/ n	U GH  n
U
S	   nUS
   nUR                  SS5      nUR                  SS5      nUR                  S5      =(       d    0 /S   R                  S5      =(       d!    [        R                  UR                  5       S9nU	R                  XU45         [        [        R                  US95      nSnUR                  S/ 5       GH  nUR                  S5      S;   a  M  UR                  S/ 5       GH  nUR                  S
5      nUR                  S5      =(       d    0 R                  SS5      n[        R                  UU5      n[        (       d  U[         ;  a  Ml  UU;   a  Mt  UR                  U5        UR                  S5      =(       d    UR                  SS5      nSu  nnnUR                  S5      =(       d    /  H  nUR                  S5      =(       d    SR#                  5       nU(       d  M4  UnUR                  S5      =(       d    0 nUR                  S5      UR                  S5      UR                  S 5      /nS!R%                  S" U 5       5      nUR                  S#5      =(       d    Sn  O   U(       d]  UR                  S5      =(       d    0 R                  S$5      =(       d    SR#                  5       nU(       a  UR                  5       S%:w  a  UnUR                  UUUUUUR                  S&S5      [&        R                  US'9UUR                  [)        U5      [*        5      UR                  US5      UUUUR                  S(5      =(       d    SUR                  S)5      =(       d    0 R                  S*S5      S+.5        US,-  nGM     GM     [        S-US. S!US/ S!U S035        [,        R.                  " S15        GM     U	 HM  u  pnUR                  S2U-   US3-   UUS4SUUUR                  U[*        5      UR                  US5      SS5.5        MO     [0        (       a  [3        U5        [4        (       a  [7        U5      O	[        5       n [9        X`U U5      nUR;                  S6 S79  [=        S8 U 5       5      n![,        R>                  " S95      S:[	        U5      U!US;.n"[@        RB                  R%                  [@        RB                  RE                  [@        RB                  RG                  [H        5      5      S<5      n#[K        U#S=S>S?9 n$[L        RN                  " U"U$S,S@9  S S S 5        [        SA[	        U5       SBU! SCU# 35        [        SD5        g ! [         a/  n[        SU SU S35        UR                  U5         S nAGM  S nAff = f! , (       d  f       Ns= f)ENz Fetching team list from ESPN ...sportsr   leaguesteamsz  found z teamszFetching fantasy ADP ...rT   r{   abbreviationrE   displayNamelogoshref)abbr)tidz  !! z: z/  (will reuse last run's players for this team)athletesrW   )injuredReserveOrOut	suspendedpracticeSquadr   fullName)rE   rE   rE   injuriesr   detailssidetypedetailrI   c              3      #    U  H7  o(       d  M  [        U5      R                  5       S :w  d  M*  [        U5      v   M9     g7f)znot specifiedN)r   rJ   ).0bs     r   	<genexpr>main.<locals>.<genexpr>(  s/     -ndafCPQFLLN^mLmfc!ffds   
AAA
returnDaterY   activer   rf   r   
experienceyears)r{   rY   rT   r   posr   r   r   rV   rU   r   r   r   r   r   r   z  z>3z<26r   g333333?DST_z D/STDST)r{   rY   rT   r   r   r   r   r   rV   rU   r   c                     U S   U S   U S   4$ )NrV   r   rY    )rb   s    r   r   main.<locals>.<lambda>Z  s    %!E(AfI>r   r    c              3   B   #    U  H  oS    [         :  d  M  Sv   M     g7f)rV   r   N)r   )r   rb   s     r   r   r   [  s     ?Gqx,'>Gs   	z%Y-%m-%d %H:%M:%Sz	2026-2027)	generatedseasoncount	adpRankedrS   r   wr   r   )indentz
Wrote z
 players (z with ADP) -> zLNow open draftboard.html in your browser and click 'Load / Refresh Players'.)(r   r\   rC   	TEAMS_URLr(   rd   r   LOGOrr   rJ   r   
ROSTER_URLr[   r   POS_MAPKEEP_ALLFANTASY_POSstriprN   HEADSHOTrQ   r   r3   r4   
FETCH_NEWSr   VALIDATE_HEADSHOTSr   r   sortsumstrftimer   r   r   r   r   r   rA   dump)%r   tdatar   adp_by_name
adp_by_defbye_by_teamrS   seenr   	team_metatrT   r   r   tnamer   rdatar>   cntgrparg   raw_posr   rY   r   
inj_detailinj_retjstdetr   r   rankedoutr   r   s%                                        r   mainr     sG   LE	
,-	NE(OAy)!,W5E	HSZL
'(	
$%+4;(K[G5D5LIy4jxx++!)bT1-11&9[T[[djjl[=[$t,-	
))c)23E
 99Z,Cwwz"&[[WWWb)eeDk55,277Kkk'73xC{$:$;uuZ(DAEE-,D ,6(Z%%
+1r1A%%//R668Br eeI.4" ##''(BST%(XX-nd-n%n
#&77<#8#>B 2 55?0b55f=CJJLBbhhjH4    %eeHb1#??s?3 &??9T?LI&??44!+%55<-2EE,/52::7BG  " qW *	 -` 	4)1U3KqX67

4 D 'T4-GO>>$5??4,
 	 ' z-?-? )SUG''<@G LL>L??G??F]]#67WC 77<<(ABNSD	dC'	*a		#q# 
+	HS\N*VHN4&
IJ	
XYw  	E%1#%TUVT"	p 
+	*s$   W!X!
X+#XX
X+__main__zERROR:r   )r   r   ),__doc__concurrent.futuresr   rA   r   r5   rL   sysr3   urllib.errorr,   urllib.requestr   r   r   r   r   TIMEOUTr   r   r   r   rZ   r   rq   r]   r)   r   r?   rC   rQ   rd   ry   r   r   r   r   r   r  __name__r[   r>   r\   exitr   r   r   <module>r     s9   Y X X X X
 +
 D
!
N	\
I= Te eDV[\  H I=!#$  WT! 6
#29*~  ,^rZj z   hs   B CB<<C