Add new playlist strategy that selects the scores with the lowest pp for possible improvement.
This commit is contained in:
parent
92707add36
commit
b451f440d9
73
docs/Samples.md
Normal file
73
docs/Samples.md
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
# Samples
|
||||||
|
|
||||||
|
Sample score data from `ScoreSaberAPI.get_player_scores(player_id)`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
{'score': {'id': 85407530,
|
||||||
|
'rank': 67,
|
||||||
|
'baseScore': 386772,
|
||||||
|
'modifiedScore': 386772,
|
||||||
|
'pp': 224.1916,
|
||||||
|
'weight': 1.794356027547591e-07,
|
||||||
|
'modifiers': '',
|
||||||
|
'multiplier': 1,
|
||||||
|
'badCuts': 0,
|
||||||
|
'missedNotes': 0,
|
||||||
|
'maxCombo': 443,
|
||||||
|
'fullCombo': True,
|
||||||
|
'hmd': 0,
|
||||||
|
'hasReplay': True,
|
||||||
|
'timeSet': '2024-10-15T00:07:34+00:00',
|
||||||
|
'deviceHmd': 'Vive',
|
||||||
|
'deviceControllerLeft': 'Touch',
|
||||||
|
'deviceControllerRight': 'Touch',
|
||||||
|
'leaderboardPlayerInfo': None},
|
||||||
|
'leaderboard': {'id': 610340,
|
||||||
|
'songHash': '75A0E9F214BA9A7C2156D6E29920F112B6423CD0',
|
||||||
|
'songName': 'Retroscope',
|
||||||
|
'songSubName': '',
|
||||||
|
'songAuthorName': 'Adust Rain',
|
||||||
|
'levelAuthorName': 'ViSi',
|
||||||
|
'difficulty': {'leaderboardId': 610340,
|
||||||
|
'difficulty': 1,
|
||||||
|
'gameMode': 'SoloStandard',
|
||||||
|
'difficultyRaw': '_Easy_SoloStandard'},
|
||||||
|
'maxScore': 400315,
|
||||||
|
'createdDate': '2024-06-01T15:12:31+00:00',
|
||||||
|
'rankedDate': '2024-10-08T11:45:57+00:00',
|
||||||
|
'qualifiedDate': '2024-09-24T19:31:54+00:00',
|
||||||
|
'lovedDate': None,
|
||||||
|
'ranked': True,
|
||||||
|
'qualified': False,
|
||||||
|
'loved': False,
|
||||||
|
'maxPP': -1,
|
||||||
|
'stars': 4.52,
|
||||||
|
'positiveModifiers': False,
|
||||||
|
'plays': 150,
|
||||||
|
'dailyPlays': 6,
|
||||||
|
'coverImage': 'https://cdn.scoresaber.com/covers/75A0E9F214BA9A7C2156D6E29920F112B6423CD0.png',
|
||||||
|
'playerScore': None,
|
||||||
|
'difficulties': []}}
|
||||||
|
```
|
||||||
|
|
||||||
|
Sample score data from `BeatLeaderAPI.get_player_scores(player_id)`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
{'score': {'id': 18489479,
|
||||||
|
'baseScore': 386772,
|
||||||
|
'modifiedScore': 386772,
|
||||||
|
'modifiers': '',
|
||||||
|
'fullCombo': True,
|
||||||
|
'maxCombo': 443,
|
||||||
|
'missedNotes': 0,
|
||||||
|
'badCuts': 0,
|
||||||
|
'hmd': 512,
|
||||||
|
'controller': 1,
|
||||||
|
'accuracy': 0.9661691,
|
||||||
|
'pp': 157.62836,
|
||||||
|
'epochTime': 1728950853},
|
||||||
|
'leaderboard': {'id': '3d2c511',
|
||||||
|
'songHash': '75a0e9f214ba9a7c2156d6e29920f112b6423cd0',
|
||||||
|
'modeName': 'Standard',
|
||||||
|
'difficulty': 1}}
|
||||||
|
```
|
@ -309,6 +309,175 @@ def playlist_strategy_beatleader_oldscores(
|
|||||||
|
|
||||||
return playlist_data, f"beatleader_oldscores-{new_count:02d}"
|
return playlist_data, f"beatleader_oldscores-{new_count:02d}"
|
||||||
|
|
||||||
|
def playlist_strategy_beatleader_lowest_pp(
|
||||||
|
api: BeatLeaderAPI,
|
||||||
|
song_count: int = 20
|
||||||
|
) -> List[Dict[str, Any]]:
|
||||||
|
player_id = prompt_for_player_id()
|
||||||
|
history = load_history()
|
||||||
|
history.setdefault('beatleader_lowest_pp', {})
|
||||||
|
history.setdefault('playlist_counts', {})
|
||||||
|
|
||||||
|
# Get the current count for BeatLeader lowest PP and increment it
|
||||||
|
count_key = 'beatleader_lowest_pp'
|
||||||
|
current_count = history['playlist_counts'].get(count_key, 0)
|
||||||
|
new_count = current_count + 1
|
||||||
|
history['playlist_counts'][count_key] = new_count
|
||||||
|
|
||||||
|
scores_data = api.get_player_scores(player_id)
|
||||||
|
all_scores = scores_data.get('playerScores', [])
|
||||||
|
if not all_scores:
|
||||||
|
logging.warning(f"No scores found for player ID {player_id} on BeatLeader.")
|
||||||
|
return [], ""
|
||||||
|
logging.debug(f"Found {len(all_scores)} scores for player ID {player_id} on BeatLeader.")
|
||||||
|
|
||||||
|
# Filter out scores with zero PP and sort by PP in ascending order
|
||||||
|
ranked_scores = [s for s in all_scores if s.get('score', {}).get('pp', 0) > 0]
|
||||||
|
ranked_scores.sort(key=lambda x: x.get('score', {}).get('pp', float('inf')))
|
||||||
|
|
||||||
|
playlist_data = []
|
||||||
|
for score_entry in ranked_scores:
|
||||||
|
if len(playlist_data) >= song_count:
|
||||||
|
break # Stop if we've reached the desired number of songs
|
||||||
|
|
||||||
|
score = score_entry.get('score', {})
|
||||||
|
leaderboard = score_entry.get('leaderboard', {})
|
||||||
|
|
||||||
|
song_hash = leaderboard.get('songHash')
|
||||||
|
difficulty_raw = int(leaderboard.get('difficulty', ''))
|
||||||
|
game_mode = leaderboard.get('modeName', 'Standard')
|
||||||
|
pp = score.get('pp', 0)
|
||||||
|
|
||||||
|
if not song_hash or not difficulty_raw:
|
||||||
|
logging.debug(f"Skipping score due to missing song_hash or difficulty_raw: {score_entry}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
difficulty = normalize_difficulty_name(difficulty_raw)
|
||||||
|
|
||||||
|
# avoid reusing song+difficulty
|
||||||
|
if song_hash in history['beatleader_lowest_pp'] and difficulty in history['beatleader_lowest_pp'][song_hash]:
|
||||||
|
logging.debug(f"Skipping song {song_hash} with difficulty {difficulty} as it's in history.")
|
||||||
|
continue # Skip if already used
|
||||||
|
|
||||||
|
# Format the song data for PlaylistBuilder
|
||||||
|
song_dict = {
|
||||||
|
'hash': song_hash,
|
||||||
|
'difficulties': [
|
||||||
|
{
|
||||||
|
'name': difficulty,
|
||||||
|
'characteristic': game_mode
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
# Add the song to the playlist
|
||||||
|
playlist_data.append(song_dict)
|
||||||
|
logging.debug(f"Selected song for playlist: Hash={song_hash}, Difficulty={difficulty}, PP={pp:.2f}")
|
||||||
|
|
||||||
|
# Update history
|
||||||
|
history['beatleader_lowest_pp'].setdefault(song_hash, []).append(difficulty)
|
||||||
|
|
||||||
|
# Log the final playlist
|
||||||
|
if not playlist_data:
|
||||||
|
logging.info("No new songs found to add to the playlist based on history for BeatLeader lowest PP.")
|
||||||
|
else:
|
||||||
|
for song in playlist_data:
|
||||||
|
song_hash = song['hash']
|
||||||
|
difficulty = song['difficulties'][0]['name']
|
||||||
|
logging.info(f"Song added: Hash={song_hash}, Difficulty={difficulty}")
|
||||||
|
logging.info(f"Total songs added to playlist from BeatLeader lowest PP: {len(playlist_data)}")
|
||||||
|
|
||||||
|
save_history(history)
|
||||||
|
|
||||||
|
return playlist_data, f"beatleader_lowest_pp-{new_count:02d}"
|
||||||
|
|
||||||
|
def playlist_strategy_scoresaber_lowest_pp(
|
||||||
|
api: ScoreSaberAPI,
|
||||||
|
song_count: int = 20
|
||||||
|
) -> List[Dict[str, Any]]:
|
||||||
|
"""Build and format a list of songs based on lowest PP scores from ScoreSaber, avoiding reusing the same song+difficulty."""
|
||||||
|
|
||||||
|
player_id = prompt_for_player_id()
|
||||||
|
history = load_history()
|
||||||
|
history.setdefault('scoresaber_lowest_pp', {})
|
||||||
|
history.setdefault('playlist_counts', {})
|
||||||
|
|
||||||
|
# Get the current count for ScoreSaber lowest PP and increment it
|
||||||
|
count_key = 'scoresaber_lowest_pp'
|
||||||
|
current_count = history['playlist_counts'].get(count_key, 0)
|
||||||
|
new_count = current_count + 1
|
||||||
|
history['playlist_counts'][count_key] = new_count
|
||||||
|
|
||||||
|
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}.")
|
||||||
|
|
||||||
|
# Filter out scores with zero PP and sort by PP in ascending order
|
||||||
|
ranked_scores = [s for s in all_scores if s['score'].get('pp', 0) > 0]
|
||||||
|
ranked_scores.sort(key=lambda x: x['score'].get('pp', float('inf')))
|
||||||
|
|
||||||
|
playlist_data = []
|
||||||
|
|
||||||
|
for score in ranked_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
|
||||||
|
|
||||||
|
# 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_lowest_pp'] and difficulty in history['scoresaber_lowest_pp'][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)
|
||||||
|
pp_score = score['score'].get('pp', 0)
|
||||||
|
logging.info(f"Song added: {song_dict['songName']} ({difficulty}), PP: {pp_score:.2f}")
|
||||||
|
|
||||||
|
# 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_lowest_pp'].setdefault(song_id, []).append(difficulty_name)
|
||||||
|
save_history(history)
|
||||||
|
|
||||||
|
return playlist_data, f"scoresaber_lowest_pp-{new_count:02d}"
|
||||||
|
|
||||||
def map_leaders_by_month(month: int = 9, year: int = 2024, game_modes: List[str] = ['Standard']) -> List[Dict]:
|
def map_leaders_by_month(month: int = 9, year: int = 2024, game_modes: List[str] = ['Standard']) -> List[Dict]:
|
||||||
"""
|
"""
|
||||||
Gathers a month's worth of maps using the BeatSaver latest maps endpoint,
|
Gathers a month's worth of maps using the BeatSaver latest maps endpoint,
|
||||||
@ -469,6 +638,48 @@ def playlist_strategy_highest_accuracy(
|
|||||||
|
|
||||||
return playlist_data, f"highest_accuracy-{year}-{month:02d}-{new_count:02d}"
|
return playlist_data, f"highest_accuracy-{year}-{month:02d}-{new_count:02d}"
|
||||||
|
|
||||||
|
def reset_history(strategy: str) -> None:
|
||||||
|
"""
|
||||||
|
Reset the history for a given playlist strategy.
|
||||||
|
|
||||||
|
:param strategy: The strategy to reset history for.
|
||||||
|
"""
|
||||||
|
history = load_history()
|
||||||
|
if strategy in history:
|
||||||
|
del history[strategy]
|
||||||
|
if 'playlist_counts' in history and strategy in history['playlist_counts']:
|
||||||
|
history['playlist_counts'][strategy] = 0
|
||||||
|
save_history(history)
|
||||||
|
logging.info(f"History and playlist count for '{strategy}' have been reset.")
|
||||||
|
else:
|
||||||
|
logging.info(f"No history found for '{strategy}'. Nothing to reset.")
|
||||||
|
|
||||||
|
def get_strategy():
|
||||||
|
parser = argparse.ArgumentParser(description="Generate Beat Saber playlists")
|
||||||
|
parser.add_argument("-s", "--strategy",
|
||||||
|
choices=["scoresaber_oldscores", "beatleader_oldscores", "highest_accuracy", "beatleader_lowest_pp", "scoresaber_lowest_pp"],
|
||||||
|
help="Specify the playlist generation strategy")
|
||||||
|
parser.add_argument("-r", "--reset",
|
||||||
|
action="store_true",
|
||||||
|
help="Reset the history for the specified strategy")
|
||||||
|
|
||||||
|
if len(sys.argv) == 1:
|
||||||
|
parser.print_help()
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
if args.reset:
|
||||||
|
if not args.strategy:
|
||||||
|
parser.error("--reset requires --strategy to be specified")
|
||||||
|
reset_history(args.strategy)
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
if not args.strategy:
|
||||||
|
parser.error("--strategy is required unless --reset is used")
|
||||||
|
|
||||||
|
return args.strategy
|
||||||
|
|
||||||
def saberlist() -> None:
|
def saberlist() -> None:
|
||||||
"""
|
"""
|
||||||
Generate a playlist of songs from a range of difficulties, all with scores previously set a long time ago.
|
Generate a playlist of songs from a range of difficulties, all with scores previously set a long time ago.
|
||||||
@ -483,6 +694,10 @@ def saberlist() -> None:
|
|||||||
playlist_data, playlist_title = playlist_strategy_beatleader_oldscores(BeatLeaderAPI(cache_expiry_days=CACHE_EXPIRY_DAYS))
|
playlist_data, playlist_title = playlist_strategy_beatleader_oldscores(BeatLeaderAPI(cache_expiry_days=CACHE_EXPIRY_DAYS))
|
||||||
elif strategy == 'highest_accuracy':
|
elif strategy == 'highest_accuracy':
|
||||||
playlist_data, playlist_title = playlist_strategy_highest_accuracy()
|
playlist_data, playlist_title = playlist_strategy_highest_accuracy()
|
||||||
|
elif strategy == 'beatleader_lowest_pp':
|
||||||
|
playlist_data, playlist_title = playlist_strategy_beatleader_lowest_pp(BeatLeaderAPI(cache_expiry_days=CACHE_EXPIRY_DAYS))
|
||||||
|
elif strategy == 'scoresaber_lowest_pp':
|
||||||
|
playlist_data, playlist_title = playlist_strategy_scoresaber_lowest_pp(ScoreSaberAPI(cache_expiry_days=CACHE_EXPIRY_DAYS))
|
||||||
else:
|
else:
|
||||||
logging.error(f"Unknown strategy '{strategy}'")
|
logging.error(f"Unknown strategy '{strategy}'")
|
||||||
return
|
return
|
||||||
@ -496,17 +711,3 @@ def saberlist() -> None:
|
|||||||
playlist_title=playlist_title,
|
playlist_title=playlist_title,
|
||||||
playlist_author="SaberList Tool"
|
playlist_author="SaberList Tool"
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_strategy():
|
|
||||||
parser = argparse.ArgumentParser(description="Generate Beat Saber playlists")
|
|
||||||
parser.add_argument("-s", "--strategy",
|
|
||||||
choices=["scoresaber_oldscores", "beatleader_oldscores", "highest_accuracy"],
|
|
||||||
help="Specify the playlist generation strategy",
|
|
||||||
required=True)
|
|
||||||
|
|
||||||
if len(sys.argv) == 1:
|
|
||||||
parser.print_help()
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
args = parser.parse_args()
|
|
||||||
return args.strategy
|
|
||||||
|
Loading…
Reference in New Issue
Block a user