Add strategy to pick ranked scores of a given star level that haven't been played in awhile
This commit is contained in:
parent
3b1d66cb30
commit
faf42af274
@ -40,7 +40,7 @@ all_beatleader_ranked_maps = beatleader_api.get_player_scores(
|
|||||||
```python
|
```python
|
||||||
from helpers.SimpleScoreSaberAPI import SimpleScoreSaberAPI
|
from helpers.SimpleScoreSaberAPI import SimpleScoreSaberAPI
|
||||||
scoresaber_api = SimpleScoreSaberAPI()
|
scoresaber_api = SimpleScoreSaberAPI()
|
||||||
scoresaber_all_ranked_maps = scoresaber_api.get_ranked_maps(use_cache=False)
|
scoresaber_ranked_maps = scoresaber_api.get_ranked_maps()
|
||||||
```
|
```
|
||||||
|
|
||||||
## BeatSaverClient
|
## BeatSaverClient
|
||||||
@ -57,7 +57,10 @@ map_data = beatsaver_api.get_maps(year=2024, month=9)
|
|||||||
from helpers.SimpleBeatLeaderAPI import SimpleBeatLeaderAPI
|
from helpers.SimpleBeatLeaderAPI import SimpleBeatLeaderAPI
|
||||||
player_id = "76561199407393962"
|
player_id = "76561199407393962"
|
||||||
beatleader_api = SimpleBeatLeaderAPI()
|
beatleader_api = SimpleBeatLeaderAPI()
|
||||||
data = beatleader_api.get_player_accgraph(player_id)
|
|
||||||
|
scores_data = beatleader_api.get_player_scores(player_id).get('data', [])
|
||||||
|
|
||||||
|
acc_graph = beatleader_api.get_player_accgraph(player_id)
|
||||||
data[0]
|
data[0]
|
||||||
filtered_data = [{'acc': item['acc'], 'stars': item['stars'], 'hash': item['hash']} for item in data]
|
filtered_data = [{'acc': item['acc'], 'stars': item['stars'], 'hash': item['hash']} for item in data]
|
||||||
filtered_data[0]
|
filtered_data[0]
|
||||||
|
@ -22,23 +22,21 @@ from helpers.SimpleScoreSaberAPI import SimpleScoreSaberAPI
|
|||||||
from helpers.SimpleBeatSaverAPI import SimpleBeatSaverAPI
|
from helpers.SimpleBeatSaverAPI import SimpleBeatSaverAPI
|
||||||
|
|
||||||
from saberlist.utils import reset_history
|
from saberlist.utils import reset_history
|
||||||
from saberlist.playlist_strategies.oldscores import (
|
from saberlist.playlist_strategies.beatleader import (
|
||||||
playlist_strategy_beatleader_oldscores,
|
playlist_strategy_beatleader_oldscores,
|
||||||
playlist_strategy_scoresaber_oldscores,
|
playlist_strategy_beatleader_oldscores_stars,
|
||||||
playlist_strategy_ranked_both,
|
playlist_strategy_ranked_both,
|
||||||
)
|
)
|
||||||
|
from saberlist.playlist_strategies.scoresaber import (
|
||||||
|
playlist_strategy_scoresaber_oldscores,
|
||||||
|
playlist_strategy_scoresaber_ranked,
|
||||||
|
)
|
||||||
from saberlist.playlist_strategies.accuracy import (
|
from saberlist.playlist_strategies.accuracy import (
|
||||||
playlist_strategy_beatleader_lowest_acc,
|
|
||||||
playlist_strategy_beatleader_accuracy_gaps,
|
playlist_strategy_beatleader_accuracy_gaps,
|
||||||
playlist_strategy_scoresaber_accuracy_gaps,
|
playlist_strategy_scoresaber_accuracy_gaps,
|
||||||
playlist_strategy_beatleader_accuracy_gaps_star_range,
|
playlist_strategy_beatleader_accuracy_gaps_star_range,
|
||||||
)
|
)
|
||||||
from saberlist.playlist_strategies.performance import (
|
|
||||||
playlist_strategy_beatleader_lowest_pp,
|
|
||||||
playlist_strategy_scoresaber_lowest_pp,
|
|
||||||
)
|
|
||||||
from saberlist.playlist_strategies.beatsaver import (
|
from saberlist.playlist_strategies.beatsaver import (
|
||||||
playlist_strategy_beatsaver_acc,
|
|
||||||
playlist_strategy_beatsaver_curated,
|
playlist_strategy_beatsaver_curated,
|
||||||
playlist_strategy_beatsaver_mappers,
|
playlist_strategy_beatsaver_mappers,
|
||||||
)
|
)
|
||||||
@ -63,12 +61,26 @@ def saberlist() -> None:
|
|||||||
)
|
)
|
||||||
playlist_builder = PlaylistBuilder()
|
playlist_builder = PlaylistBuilder()
|
||||||
|
|
||||||
|
elif strategy == 'beatleader_oldscores_stars':
|
||||||
|
stars = input("Enter star level (Default: 6.0): ") or 6.0
|
||||||
|
playlist_data, playlist_title = playlist_strategy_beatleader_oldscores_stars(
|
||||||
|
SimpleBeatLeaderAPI(cache_expiry_days=CACHE_EXPIRY_DAYS),
|
||||||
|
stars=float(stars)
|
||||||
|
)
|
||||||
|
playlist_builder = PlaylistBuilder(covers_dir='./covers/pajamas')
|
||||||
|
|
||||||
elif strategy == 'beatleader_oldscores':
|
elif strategy == 'beatleader_oldscores':
|
||||||
playlist_data, playlist_title = playlist_strategy_beatleader_oldscores(
|
playlist_data, playlist_title = playlist_strategy_beatleader_oldscores(
|
||||||
BeatLeaderAPI(cache_expiry_days=CACHE_EXPIRY_DAYS)
|
BeatLeaderAPI(cache_expiry_days=CACHE_EXPIRY_DAYS)
|
||||||
)
|
)
|
||||||
playlist_builder = PlaylistBuilder()
|
playlist_builder = PlaylistBuilder()
|
||||||
|
|
||||||
|
elif strategy == 'scoresaber_ranked':
|
||||||
|
playlist_data, playlist_title = playlist_strategy_scoresaber_ranked(
|
||||||
|
SimpleScoreSaberAPI(cache_expiry_days=CACHE_EXPIRY_DAYS)
|
||||||
|
)
|
||||||
|
playlist_builder = PlaylistBuilder(covers_dir='./covers/scoresaber')
|
||||||
|
|
||||||
elif strategy == 'ranked_both':
|
elif strategy == 'ranked_both':
|
||||||
playlist_data, playlist_title = playlist_strategy_ranked_both(
|
playlist_data, playlist_title = playlist_strategy_ranked_both(
|
||||||
SimpleBeatLeaderAPI(cache_expiry_days=CACHE_EXPIRY_DAYS),
|
SimpleBeatLeaderAPI(cache_expiry_days=CACHE_EXPIRY_DAYS),
|
||||||
@ -143,6 +155,13 @@ def parse_args_subcommands():
|
|||||||
action="store_true",
|
action="store_true",
|
||||||
help="Reset the history for scoresaber_oldscores")
|
help="Reset the history for scoresaber_oldscores")
|
||||||
|
|
||||||
|
# -------- beatleader_oldscores_stars --------
|
||||||
|
parser_bl_old_stars = subparsers.add_parser("beatleader_oldscores_stars",
|
||||||
|
help="Generate a playlist using BeatLeader old-scores-stars strategy")
|
||||||
|
parser_bl_old_stars.add_argument("-r", "--reset",
|
||||||
|
action="store_true",
|
||||||
|
help="Reset the history for beatleader_oldscores_stars")
|
||||||
|
|
||||||
# -------- beatleader_oldscores --------
|
# -------- beatleader_oldscores --------
|
||||||
parser_bl_old = subparsers.add_parser("beatleader_oldscores",
|
parser_bl_old = subparsers.add_parser("beatleader_oldscores",
|
||||||
help="Generate a playlist using BeatLeader old-scores strategy")
|
help="Generate a playlist using BeatLeader old-scores strategy")
|
||||||
@ -150,6 +169,13 @@ def parse_args_subcommands():
|
|||||||
action="store_true",
|
action="store_true",
|
||||||
help="Reset the history for beatleader_oldscores")
|
help="Reset the history for beatleader_oldscores")
|
||||||
|
|
||||||
|
# -------- scoresaber_ranked --------
|
||||||
|
parser_ss_ranked = subparsers.add_parser("scoresaber_ranked",
|
||||||
|
help="Generate a playlist using scoresaber_ranked strategy")
|
||||||
|
parser_ss_ranked.add_argument("-r", "--reset",
|
||||||
|
action="store_true",
|
||||||
|
help="Reset the history for scoresaber_ranked")
|
||||||
|
|
||||||
# -------- ranked_both --------
|
# -------- ranked_both --------
|
||||||
parser_ranked_both = subparsers.add_parser("ranked_both",
|
parser_ranked_both = subparsers.add_parser("ranked_both",
|
||||||
help="Generate a playlist using ranked_both strategy")
|
help="Generate a playlist using ranked_both strategy")
|
||||||
|
@ -13,7 +13,6 @@ logging.basicConfig(
|
|||||||
level=LOG_LEVEL
|
level=LOG_LEVEL
|
||||||
)
|
)
|
||||||
|
|
||||||
from helpers.ScoreSaberAPI import ScoreSaberAPI
|
|
||||||
from helpers.BeatLeaderAPI import BeatLeaderAPI
|
from helpers.BeatLeaderAPI import BeatLeaderAPI
|
||||||
from helpers.SimpleBeatLeaderAPI import SimpleBeatLeaderAPI
|
from helpers.SimpleBeatLeaderAPI import SimpleBeatLeaderAPI
|
||||||
from helpers.SimpleScoreSaberAPI import SimpleScoreSaberAPI
|
from helpers.SimpleScoreSaberAPI import SimpleScoreSaberAPI
|
||||||
@ -105,6 +104,127 @@ def playlist_strategy_ranked_both(
|
|||||||
|
|
||||||
return playlist_data, f"ranked_both-{new_count:02d}"
|
return playlist_data, f"ranked_both-{new_count:02d}"
|
||||||
|
|
||||||
|
def playlist_strategy_beatleader_oldscores_stars(
|
||||||
|
beatleader_api: SimpleBeatLeaderAPI,
|
||||||
|
song_count: int = 40,
|
||||||
|
stars: float = 6.0
|
||||||
|
) -> List[Dict[str, Any]]:
|
||||||
|
"""
|
||||||
|
Build and format a list of songs from BeatLeader that have a star rating
|
||||||
|
between 'stars' (default: 6) and stars+1 (exclusive). Avoid reusing the same
|
||||||
|
song+difficulty from history.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
beatleader_api (SimpleBeatLeaderAPI): API interface to fetch player scores.
|
||||||
|
song_count (int, optional): Number of songs to return in the playlist. Defaults to 20.
|
||||||
|
stars (float, optional): Starting range of star rating (x). Only songs with
|
||||||
|
x <= star < x+1 are included. Defaults to 6.0.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List[Dict[str, Any]]: A playlist list of dictionary items suitable for building a playlist
|
||||||
|
(along with a formatted playlist identifier).
|
||||||
|
"""
|
||||||
|
player_id = prompt_for_player_id()
|
||||||
|
history = load_history()
|
||||||
|
history.setdefault('beatleader_oldscores_stars', {})
|
||||||
|
history.setdefault('playlist_counts', {})
|
||||||
|
|
||||||
|
# Get the current count for the star-based strategy and increment it
|
||||||
|
count_key = 'beatleader_oldscores_stars'
|
||||||
|
current_count = history['playlist_counts'].get(count_key, 0)
|
||||||
|
new_count = current_count + 1
|
||||||
|
history['playlist_counts'][count_key] = new_count
|
||||||
|
|
||||||
|
# Fetch player scores
|
||||||
|
scores_data = beatleader_api.get_player_scores(player_id).get('data', [])
|
||||||
|
if not scores_data:
|
||||||
|
logging.warning(f"No scores found for player ID {player_id} on BeatLeader.")
|
||||||
|
return [], ""
|
||||||
|
|
||||||
|
logging.debug(f"Found {len(scores_data)} total scores for player ID {player_id} on BeatLeader.")
|
||||||
|
|
||||||
|
# Sort scores by epochTime in ascending order (if you want "oldest" style, similar to oldscores).
|
||||||
|
# Remove this sort if you prefer a different ordering.
|
||||||
|
scores_data.sort(key=lambda x: x.get('timepost', 0))
|
||||||
|
|
||||||
|
playlist_data = []
|
||||||
|
current_time = datetime.now(timezone.utc)
|
||||||
|
|
||||||
|
for score_entry in scores_data:
|
||||||
|
if len(playlist_data) >= song_count:
|
||||||
|
break # Stop if we've reached the desired number of songs
|
||||||
|
|
||||||
|
leaderboard = score_entry.get('leaderboard', {})
|
||||||
|
difficulty_data = leaderboard.get('difficulty', {})
|
||||||
|
star_rating = difficulty_data.get('stars')
|
||||||
|
song_hash = leaderboard.get('song', {}).get('hash')
|
||||||
|
difficulty_raw = difficulty_data.get('value')
|
||||||
|
game_mode = difficulty_data.get('modeName', 'Standard')
|
||||||
|
epoch_time = score_entry.get('timepost')
|
||||||
|
|
||||||
|
# Skip if we have missing data
|
||||||
|
if not song_hash or not difficulty_raw or star_rating is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Filter by star rating range [stars, stars + 1)
|
||||||
|
if not (stars <= star_rating < stars + 1):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Normalize difficulty name
|
||||||
|
difficulty = normalize_difficulty_name(difficulty_raw)
|
||||||
|
|
||||||
|
# Avoid reusing song+difficulty
|
||||||
|
if (song_hash in history['beatleader_oldscores_stars'] and
|
||||||
|
difficulty in history['beatleader_oldscores_stars'][song_hash]):
|
||||||
|
logging.debug(f"Skipping song {song_hash} (difficulty {difficulty}) as it's in history.")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Calculate how long ago the score was set
|
||||||
|
try:
|
||||||
|
time_set = datetime.fromtimestamp(epoch_time, tz=timezone.utc)
|
||||||
|
except (ValueError, OSError) as e:
|
||||||
|
logging.error(f"Invalid epochTime for this score: {e}")
|
||||||
|
continue
|
||||||
|
time_difference = current_time - time_set
|
||||||
|
time_ago = format_time_ago(time_difference)
|
||||||
|
|
||||||
|
# Format the song data for PlaylistBuilder
|
||||||
|
song_dict = {
|
||||||
|
'hash': song_hash,
|
||||||
|
'difficulties': [
|
||||||
|
{
|
||||||
|
'name': difficulty,
|
||||||
|
'characteristic': game_mode
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
# Add to playlist
|
||||||
|
playlist_data.append(song_dict)
|
||||||
|
logging.debug(
|
||||||
|
f"Selected song for playlist: Hash={song_hash}, Difficulty={difficulty}, "
|
||||||
|
f"Stars={star_rating:.2f}. Last played {time_ago} ago."
|
||||||
|
)
|
||||||
|
|
||||||
|
# Update history
|
||||||
|
history['beatleader_oldscores_stars'].setdefault(song_hash, []).append(difficulty)
|
||||||
|
|
||||||
|
if len(playlist_data) >= song_count:
|
||||||
|
break
|
||||||
|
|
||||||
|
if not playlist_data:
|
||||||
|
logging.info("No new songs found to add to the playlist based on history for BeatLeader (star-based).")
|
||||||
|
else:
|
||||||
|
for song in playlist_data:
|
||||||
|
song_hash = song['hash']
|
||||||
|
diff_name = song['difficulties'][0]['name']
|
||||||
|
logging.info(f"Song added: Hash={song_hash}, Difficulty={diff_name}.")
|
||||||
|
logging.info(f"Total songs added to playlist from BeatLeader (star-based): {len(playlist_data)}")
|
||||||
|
|
||||||
|
save_history(history)
|
||||||
|
|
||||||
|
return playlist_data, f"beatleader_oldscores_stars-{new_count:02d}"
|
||||||
|
|
||||||
def playlist_strategy_beatleader_oldscores(
|
def playlist_strategy_beatleader_oldscores(
|
||||||
api: BeatLeaderAPI,
|
api: BeatLeaderAPI,
|
||||||
song_count: int = 20
|
song_count: int = 20
|
||||||
@ -196,106 +316,3 @@ def playlist_strategy_beatleader_oldscores(
|
|||||||
save_history(history)
|
save_history(history)
|
||||||
|
|
||||||
return playlist_data, f"beatleader_oldscores-{new_count:02d}"
|
return playlist_data, f"beatleader_oldscores-{new_count:02d}"
|
||||||
|
|
||||||
def playlist_strategy_scoresaber_oldscores(
|
|
||||||
api: ScoreSaberAPI,
|
|
||||||
song_count: int = 20
|
|
||||||
) -> 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', {})
|
|
||||||
history.setdefault('playlist_counts', {})
|
|
||||||
|
|
||||||
# Get the current count for ScoreSaber old scores and increment it
|
|
||||||
count_key = 'scoresaber_oldscores'
|
|
||||||
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}.")
|
|
||||||
|
|
||||||
# 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, f"scoresaber_oldscores-{new_count:02d}"
|
|
214
src/saberlist/playlist_strategies/scoresaber.py
Normal file
214
src/saberlist/playlist_strategies/scoresaber.py
Normal file
@ -0,0 +1,214 @@
|
|||||||
|
from typing import Dict, Any, List, Tuple
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
load_dotenv()
|
||||||
|
LOG_LEVEL = os.environ.get('LOG_LEVEL', 'INFO').upper()
|
||||||
|
|
||||||
|
import logging
|
||||||
|
logging.basicConfig(
|
||||||
|
format='%(asctime)s %(levelname)s: %(message)s',
|
||||||
|
datefmt='%Y-%m-%d %H:%M:%S',
|
||||||
|
level=LOG_LEVEL
|
||||||
|
)
|
||||||
|
|
||||||
|
from helpers.ScoreSaberAPI import ScoreSaberAPI
|
||||||
|
from helpers.SimpleScoreSaberAPI import SimpleScoreSaberAPI
|
||||||
|
|
||||||
|
from saberlist.utils import load_history, save_history, normalize_difficulty_name, format_time_ago, prompt_for_player_id
|
||||||
|
|
||||||
|
def playlist_strategy_scoresaber_ranked(
|
||||||
|
scoresaber_api: SimpleScoreSaberAPI,
|
||||||
|
min_star: float = 7.0,
|
||||||
|
max_star: float = 10.0,
|
||||||
|
song_count: int = 40
|
||||||
|
) -> Tuple[List[Dict[str, Any]], str]:
|
||||||
|
"""
|
||||||
|
Build and format a list of the latest ranked songs from ScoreSaber within the given star range,
|
||||||
|
avoiding reusing the same song+difficulty.
|
||||||
|
Returns:
|
||||||
|
(playlist_data, playlist_identifier):
|
||||||
|
playlist_data: A list of playlist songs.
|
||||||
|
playlist_identifier: A formatted string identifying this playlist version.
|
||||||
|
"""
|
||||||
|
history = load_history()
|
||||||
|
# Set up the history keys if they don't exist
|
||||||
|
history.setdefault('scoresaber_ranked', {})
|
||||||
|
history.setdefault('playlist_counts', {})
|
||||||
|
|
||||||
|
# Prepare a unique key for this strategy and update its usage count
|
||||||
|
count_key = 'scoresaber_ranked'
|
||||||
|
current_count = history['playlist_counts'].get(count_key, 0)
|
||||||
|
new_count = current_count + 1
|
||||||
|
history['playlist_counts'][count_key] = new_count
|
||||||
|
|
||||||
|
# Fetch ScoreSaber ranked maps within the desired star range
|
||||||
|
logging.debug(f"Fetching ScoreSaber ranked maps with star range [{min_star}, {max_star}]...")
|
||||||
|
ranked_maps = scoresaber_api.get_ranked_maps(
|
||||||
|
min_star=min_star,
|
||||||
|
max_star=max_star
|
||||||
|
)
|
||||||
|
|
||||||
|
# Filter maps that are actually ranked, and sort by rankedDate descending (latest first)
|
||||||
|
filtered_maps = [
|
||||||
|
m for m in ranked_maps
|
||||||
|
if m.get('ranked') is True and m.get('rankedDate') is not None
|
||||||
|
]
|
||||||
|
filtered_maps.sort(key=lambda x: x['rankedDate'], reverse=True)
|
||||||
|
logging.info(f"Retrieved {len(filtered_maps)} ranked maps from ScoreSaber in the star range.")
|
||||||
|
|
||||||
|
playlist_data = []
|
||||||
|
|
||||||
|
for map_data in filtered_maps:
|
||||||
|
if len(playlist_data) >= song_count:
|
||||||
|
logging.debug(f"Reached the desired song count: {song_count}.")
|
||||||
|
break
|
||||||
|
|
||||||
|
song_hash = map_data.get('songHash')
|
||||||
|
difficulty_info = map_data.get('difficulty', {})
|
||||||
|
difficulty_raw = difficulty_info.get('difficultyRaw')
|
||||||
|
game_mode = difficulty_info.get('gameMode', 'Standard')
|
||||||
|
|
||||||
|
if not song_hash or not difficulty_raw:
|
||||||
|
logging.debug(f"Skipping map due to missing hash or difficulty info: {map_data}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Normalize the difficulty name (reusing the helper from oldscores)
|
||||||
|
difficulty = normalize_difficulty_name(difficulty_raw)
|
||||||
|
|
||||||
|
# Avoid reusing the same song + difficulty
|
||||||
|
if song_hash in history['scoresaber_ranked'] and difficulty in history['scoresaber_ranked'][song_hash]:
|
||||||
|
logging.debug(f"Skipping song {song_hash} - {difficulty} as it's in history.")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Prepare the data for the playlist
|
||||||
|
song_dict = {
|
||||||
|
'hash': song_hash,
|
||||||
|
'songName': map_data.get('songName', 'Unknown'),
|
||||||
|
'difficulties': [
|
||||||
|
{
|
||||||
|
'name': difficulty,
|
||||||
|
'characteristic': 'Standard' if 'Standard' in game_mode else game_mode
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
playlist_data.append(song_dict)
|
||||||
|
logging.debug(f"Added {song_dict['songName']} (Difficulty Raw: {difficulty_raw}) to the playlist.")
|
||||||
|
|
||||||
|
# Update history to mark this song + difficulty combination as used
|
||||||
|
history['scoresaber_ranked'].setdefault(song_hash, []).append(difficulty)
|
||||||
|
|
||||||
|
# Log final results
|
||||||
|
if playlist_data:
|
||||||
|
logging.info(f"Added {len(playlist_data)} new songs to the playlist.")
|
||||||
|
else:
|
||||||
|
logging.info("No new ranked songs were added to the playlist based on history.")
|
||||||
|
|
||||||
|
save_history(history)
|
||||||
|
|
||||||
|
return playlist_data, f"scoresaber_ranked-{new_count:02d}"
|
||||||
|
|
||||||
|
|
||||||
|
def playlist_strategy_scoresaber_oldscores(
|
||||||
|
api: ScoreSaberAPI,
|
||||||
|
song_count: int = 20
|
||||||
|
) -> 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', {})
|
||||||
|
history.setdefault('playlist_counts', {})
|
||||||
|
|
||||||
|
# Get the current count for ScoreSaber old scores and increment it
|
||||||
|
count_key = 'scoresaber_oldscores'
|
||||||
|
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}.")
|
||||||
|
|
||||||
|
# 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, f"scoresaber_oldscores-{new_count:02d}"
|
Loading…
x
Reference in New Issue
Block a user