diff --git a/src/saberlist/make.py b/src/saberlist/make.py index 96cd19d..51dd936 100644 --- a/src/saberlist/make.py +++ b/src/saberlist/make.py @@ -1,5 +1,7 @@ +import argparse import json import os +import sys import logging from collections import defaultdict from datetime import datetime, timedelta, timezone @@ -94,9 +96,105 @@ def normalize_difficulty_name(difficulty_name): # Return the mapped value or the original name if there is no mapping return difficulty_names.get(difficulty_name, difficulty_name) +def playlist_strategy_scoresaber_oldscores( + api: ScoreSaberAPI, + song_count: int = 20 # Total number of songs to select +) -> List[Dict[str, Any]]: + """Build and format a list of songs based on old scores from ScoreSaber, avoiding reusing the same song+difficulty.""" + + player_id = prompt_for_player_id() + history = load_history() + history.setdefault('scoresaber_oldscores', {}) + + scores_data = api.get_player_scores(player_id, use_cache=True) + all_scores = scores_data.get('playerScores', []) + if not all_scores: + logging.warning(f"No scores found for player ID {player_id}.") + return [] + logging.debug(f"Found {len(all_scores)} scores for player ID {player_id}.") + + # Sort scores by timeSet in ascending order (oldest first) + all_scores.sort(key=lambda x: x['score'].get('timeSet', '')) + + playlist_data = [] + current_time = datetime.now(timezone.utc) + + for score in all_scores: + leaderboard = score.get('leaderboard', {}) + song_id = leaderboard.get('songHash') + difficulty_raw = leaderboard.get('difficulty', {}).get('difficultyRaw', '') + + if not song_id or not difficulty_raw: + logging.debug(f"Skipping score due to missing song_id or difficulty_raw: {score}") + continue # Skip if essential data is missing + + # Calculate time ago + time_set_str = score['score'].get('timeSet') + if not time_set_str: + logging.debug(f"Skipping score due to missing timeSet: {score}") + continue # Skip if time_set is missing + try: + time_set = datetime.fromisoformat(time_set_str.replace('Z', '+00:00')) + except ValueError as e: + logging.error(f"Invalid time format for score ID {score['score'].get('id')}: {e}") + continue + time_difference = current_time - time_set + time_ago = format_time_ago(time_difference) + + # Normalize the difficulty name + difficulty = normalize_difficulty_name(difficulty_raw) + game_mode = leaderboard.get('difficulty', {}).get('gameMode', 'Standard') + if 'Standard' in game_mode: + game_mode = 'Standard' + + # Check history to avoid reusing song+difficulty + if song_id in history['scoresaber_oldscores'] and difficulty in history['scoresaber_oldscores'][song_id]: + logging.debug(f"Skipping song {song_id} with difficulty {difficulty} as it's in history.") + continue # Skip if already used + + # Format the song data as expected by PlaylistBuilder + song_dict = { + 'hash': song_id, + 'songName': leaderboard.get('songName', 'Unknown'), + 'difficulties': [ + { + 'name': difficulty, + 'characteristic': game_mode + } + ] + } + + # Add the song to the playlist + playlist_data.append(song_dict) + logging.debug(f"Selected song for playlist: {song_dict['songName']} ({difficulty})") + + # Log the song addition + mapper = "Unknown" # Mapper information can be added if available + logging.info(f"Song added: {song_dict['songName']} ({difficulty}), mapped by {mapper}. Last played {time_ago} ago.") + + # Check if the desired number of songs has been reached + if len(playlist_data) >= song_count: + logging.debug(f"Reached the desired song count: {song_count}.") + break + + # Log if no songs were added + if not playlist_data: + logging.info("No new songs found to add to the playlist based on history.") + else: + logging.info(f"Total songs added to playlist: {len(playlist_data)}") + + # Update history to avoid reusing the same song+difficulty + for song in playlist_data: + song_id = song['hash'] + difficulty_name = song['difficulties'][0]['name'] + history['scoresaber_oldscores'].setdefault(song_id, []).append(difficulty_name) + save_history(history) + + return playlist_data + def playlist_strategy_beatleader_oldscores( api: BeatLeaderAPI, - song_count: int = 40 + song_count: int = 20 ) -> List[Dict[str, Any]]: """ Build and format a list of songs based on old scores from BeatLeader, @@ -186,12 +284,13 @@ def playlist_strategy_beatleader_oldscores( return playlist_data -def saberlist(strategy='beatleader_oldscores') -> None: +def saberlist() -> None: """ Generate a playlist of songs from a range of difficulties, all with scores previously set a long time ago. The range of difficulties ensures that the first few songs are good for warming up. Avoids reusing the same song+difficulty in a playlist based on history. """ + strategy = get_strategy() if strategy == 'scoresaber_oldscores': api = ScoreSaberAPI(cache_expiry_days=CACHE_EXPIRY_DAYS) elif strategy == 'beatleader_oldscores': @@ -206,7 +305,7 @@ def saberlist(strategy='beatleader_oldscores') -> None: if strategy == 'scoresaber_oldscores': playlist_data = playlist_strategy_scoresaber_oldscores(api) elif strategy == 'beatleader_oldscores': - playlist_data = playlist_strategy_beatleader_oldscores(api, song_count=40) + playlist_data = playlist_strategy_beatleader_oldscores(api) if not playlist_data: logging.info("No new scores found to add to the playlist.") @@ -218,19 +317,16 @@ def saberlist(strategy='beatleader_oldscores') -> None: playlist_author="SaberList Tool" ) -""" -if __name__ == "__main__": - import argparse - - parser = argparse.ArgumentParser(description="Generate a playlist based on player scores.") - parser.add_argument( - '--strategy', - type=str, - default='beatleader_oldscores', - choices=['scoresaber_oldscores', 'beatleader_oldscores'], - help='Strategy to use for building the playlist.' - ) +def get_strategy(): + parser = argparse.ArgumentParser(description="Generate Beat Saber playlists") + parser.add_argument("-s", "--strategy", + choices=["scoresaber_oldscores", "beatleader_oldscores"], + help="Specify the playlist generation strategy", + required=True) + + if len(sys.argv) == 1: + parser.print_help() + sys.exit(1) + args = parser.parse_args() - - saberlist(strategy=args.strategy) -""" \ No newline at end of file + return args.strategy \ No newline at end of file