Viewing Keeper Players
Keeper data is fetched from the Yahoo Fantasy API and organized by team:View Your Keepers
Your team’s keepers appear at the top in a highlighted section with player headshots.
API Endpoint
The keeper data comes from the/api/draft/keepers endpoint:
# main.py:502-695
@app.route("/api/draft/keepers")
def api_draft_keepers():
if "token" not in session:
return jsonify({"error": "authentication required"}), 401
league_key = session.get("league_key")
if not league_key:
return jsonify({"error": "no league chosen"}), 400
log.info("Fetching keeper players for league %s", league_key)
try:
# Fetch current season keepers
keepers_payload = yahoo_api(f"fantasy/v2/league/{league_key}/players;status=K;out=ownership")
teams_payload = yahoo_api(f"fantasy/v2/league/{league_key}/teams")
except requests.exceptions.HTTPError as e:
status_code = e.response.status_code if e.response is not None else 502
return jsonify({"error": error_detail}), status_code
teams_meta = _parse_teams_meta(teams_payload)
keepers = _parse_keeper_players(keepers_payload)
Keeper Player Parsing
The backend parses keeper information including NBA player ID lookup:# main.py:316-363
def _parse_keeper_players(keepers_payload: Dict[str, Any]) -> List[Dict[str, Any]]:
players_section = _league_section(keepers_payload, 'players')
keepers: List[Dict[str, Any]] = []
for key, entry in players_section.items():
if key == 'count':
continue
player_sections = entry.get('player')
# Extract player core data and ownership
player_core = None
ownership_blob = None
if isinstance(player_sections, list):
for section in player_sections:
if isinstance(section, list) and player_core is None:
player_core = section
elif isinstance(section, dict) and 'ownership' in section:
ownership_blob = section['ownership']
owner_team_key = ownership_blob.get('owner_team_key') if ownership_blob else None
name_block = _first(player_core, 'name')
name_full = name_block.get('full') or ' '.join([name_block.get('first'), name_block.get('last')])
display_position = _first(player_core, 'display_position')
# Look up NBA player ID for headshot images
nba_player_id = _lookup_nba_player_id(name_full)
keepers.append({
'player_key': _first(player_core, 'player_key'),
'player_id': _first(player_core, 'player_id'),
'name_full': name_full or '',
'display_position': display_position or '',
'owner_team_key': owner_team_key,
'badge': 'Keeper',
'nba_id': nba_player_id,
'is_keeper': True,
})
return keepers
Previous Season Comparison
The endpoint automatically fetches previous season keepers for comparison:# main.py:607-646
previous_league_key = _previous_league_key(teams_payload)
if previous_league_key and previous_league_key != league_key:
log.info("Attempting to fetch previous season keepers via league %s", previous_league_key)
try:
prev_keepers_payload = yahoo_api(
f"fantasy/v2/league/{previous_league_key}/players;status=K;out=ownership"
)
prev_teams_payload = yahoo_api(
f"fantasy/v2/league/{previous_league_key}/teams"
)
except Exception as e:
log.exception("Unexpected error while fetching previous keepers")
else:
prev_teams_meta = _parse_teams_meta(prev_teams_payload)
# Match teams across seasons by ID, GUID, or name
for team in prev_teams_meta:
team_id = str(team.get('team_id') or '').strip()
manager_guid = str(team.get('manager_guid') or '').strip()
team_name = str(team.get('team_name') or '').strip()
if team_id and team_id in id_set:
team['is_current_login'] = True
elif manager_guid and manager_guid in guid_set:
team['is_current_login'] = True
elif team_name and team_name.casefold() in name_set:
team['is_current_login'] = True
prev_keepers = _parse_keeper_players(prev_keepers_payload)
prev_grouped, prev_orphans = _organize_rosters(prev_keepers)
previous_data = {
'league_key': previous_league_key,
'season': _league_meta_value(prev_teams_payload, 'season'),
'teams': prev_teams_meta,
'keepers_by_team': prev_grouped,
'orphans': prev_orphans,
}
Response Structure
The API returns a comprehensive response:{
"teams": [
{
"team_key": "418.l.12345.t.1",
"team_name": "My Team",
"manager_name": "John Doe",
"is_current_login": true
}
],
"keepers_by_team": {
"418.l.12345.t.1": [
{
"player_key": "418.p.6583",
"name_full": "Giannis Antetokounmpo",
"display_position": "PF,SF",
"nba_id": "203507",
"badge": "Keeper"
}
]
},
"metadata": {
"league_key": "418.l.12345",
"season": "2024",
"current_keeper_count": 24,
"previous_keeper_count": 22,
"previous_season": "2023"
},
"previous_season": {
"season": "2023",
"keepers_by_team": { /* previous keepers */ }
}
}
Frontend Rendering
The JavaScript module handles the keeper display:// keeper.js:89-136
async function loadKeepers() {
state.loading = true;
toggleLoading(true);
showError('');
try {
const response = await fetch('/api/draft/keepers');
const payload = await response.json();
if (!response.ok) {
throw new Error((payload && payload.error) || 'Unable to load keeper data.');
}
const normalized = normalizePayload(payload);
state.data = normalized.current;
state.previous = normalized.previous;
state.metadata = normalized.metadata;
// Build user team identification sets
state.userTeamKeys = new Set(state.metadata.user_team_keys || []);
state.userTeamIds = new Set(state.metadata.user_team_ids || []);
state.userTeamNames = new Set(state.metadata.user_team_names || []);
state.loaded = true;
populateTeamSelect();
updateScoringNote(state.metadata);
renderKeepers();
updateFallbackNotice();
} catch (error) {
showError(error.message || 'Unable to load keeper data.');
} finally {
state.loading = false;
toggleLoading(false);
}
}
Player Headshots
Keeper cards display NBA headshots with fallback initials:// keeper.js:258-311
function createKeeperHeadshot(player) {
const wrapper = document.createElement('div');
wrapper.className = 'keeper-player-photo-wrapper';
const spinner = document.createElement('div');
spinner.className = 'keeper-player-photo-spinner';
wrapper.appendChild(spinner);
const renderFallback = () => {
const fallback = document.createElement('div');
fallback.className = 'keeper-player-photo-fallback';
fallback.textContent = getKeeperInitials(player && player.name_full);
wrapper.appendChild(fallback);
};
const nbaId = resolveKeeperHeadshotId(player);
if (nbaId) {
const image = document.createElement('img');
image.className = 'keeper-player-photo';
image.alt = `${player.name_full} headshot`;
image.loading = 'lazy';
image.addEventListener('load', () => {
image.classList.add('keeper-player-photo-visible');
});
image.addEventListener('error', renderFallback);
image.src = `https://cdn.nba.com/headshots/nba/latest/260x190/${nbaId}.png`;
wrapper.appendChild(image);
} else {
renderFallback();
}
return wrapper;
}
CSV Export Functionality
Export keeper data for analysis or record-keeping:// keeper.js:1165-1182
function handleExport() {
if (!state.data || !DOM.exportBtn || DOM.exportBtn.disabled) return;
const rows = collectExportRows();
if (!rows.length) return;
const header = ['Team', 'Manager', 'Player', 'Positions', 'Player Key', 'Player ID'];
const csvLines = [header, ...rows].map((line) => line.map(formatCsvCell).join(','));
const csvContent = csvLines.join('\r\n');
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
const link = document.createElement('a');
const seasonSuffix = state.metadata?.season ? `_${state.metadata.season}` : '';
link.href = window.URL.createObjectURL(blob);
link.download = `keepers${seasonSuffix}`.replace(/\s+/g, '_') + '.csv';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(link.href);
}
Export Data Collection
// keeper.js:1184-1205
function collectExportRows() {
if (!state.data) return [];
const rows = [];
const selectedKey = state.selectedTeam || 'ALL';
const teams = selectedKey === 'ALL'
? getAugmentedTeamList(state.data)
: [state.teamMap[selectedKey]].filter(Boolean);
teams.forEach((team) => {
const keepers = state.data.keepersByTeam[team.team_key] || [];
keepers.forEach((player) => {
rows.push([
team.team_name || '',
team.manager_name || '',
player.name_full || '',
player.display_position || '',
player.player_key || '',
player.player_id || ''
]);
});
});
return rows;
}
Team Filtering
Filter keepers by team using the dropdown:// keeper.js:65-70
if (DOM.teamSelect) {
DOM.teamSelect.addEventListener('change', () => {
state.selectedTeam = DOM.teamSelect.value || 'ALL';
renderKeepers();
});
}
The keeper view automatically identifies your team(s) using multiple matching strategies: team key, team ID, manager GUID, and team name comparison.
Fallback Behavior
If no keepers exist for the current season, the previous season is shown:// keeper.js:731-742
function updateFallbackNotice() {
if (!DOM.fallbackNotice) return;
if (!state.metadata || !state.metadata.fallback_to_previous) {
DOM.fallbackNotice.hidden = true;
return;
}
const currentSeason = state.metadata.season || 'the current season';
const previousSeason = state.metadata.previous_season || 'last season';
DOM.fallbackNotice.hidden = false;
DOM.fallbackNotice.textContent =
`No approved keepers yet for season ${currentSeason}. Showing ${previousSeason} keepers below.`;
}
Keeper data requires Yahoo Fantasy API access. If you receive an authentication error, log out and log back in to refresh your session.