Nicer playlists names and log messages to indicate that leaderboards are being cached.

This commit is contained in:
Brian Lee 2024-10-15 18:44:00 -07:00
parent eb3e3f3054
commit 4f58daa29f

View File

@ -26,6 +26,8 @@ from helpers.PlaylistBuilder import PlaylistBuilder
from helpers.ScoreSaberAPI import ScoreSaberAPI
from helpers.BeatLeaderAPI import BeatLeaderAPI
import calendar
def load_history() -> Dict[str, Any]:
"""
Load the playlist history from a JSON file.
@ -34,8 +36,10 @@ def load_history() -> Dict[str, Any]:
"""
if os.path.exists(HISTORY_FILE):
with open(HISTORY_FILE, 'r') as f:
return json.load(f)
return {}
history = json.load(f)
history.setdefault('playlist_counts', {})
return history
return {'highest_accuracy': {}, 'playlist_counts': {}}
def save_history(history: Dict[str, Any]) -> None:
"""
@ -298,101 +302,165 @@ def playlist_strategy_beatleader_oldscores(
return playlist_data
def map_leaders_by_month(month: int = 9, year: int = 2024) -> 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,
prioritizes map difficulties where players have already set good scores,
and calculates the average accuracy for each map+difficulty.
Args:
month: The month to gather maps for.
year: The year to gather maps for.
game_modes: The game modes to include.
Returns:
A list of dictionaries, each containing:
- hash: Hash of the map
- difficulties: List of difficulties with their characteristics
- map_name: Name of the map
- difficulty: Difficulty level
- average_accuracy: Average accuracy of the leaderboard
"""
beatleader_api = SimpleBeatLeaderAPI()
beatsaver_api = BeatSaverAPI()
beatleader_api = SimpleBeatLeaderAPI(cache_expiry_days=30)
beatsaver_api = BeatSaverAPI(cache_expiry_days=30)
map_data = beatsaver_api.get_maps(year=year, month=month)
logging.debug(f"Fetching maps for {month}/{year}")
map_data = beatsaver_api.get_maps(year=year, month=month, page_size=100)
collected_data = []
for map_entry in map_data:
for i, map_entry in enumerate(map_data):
# Ensure there are versions available
if not map_entry.versions:
logging.warning(f"No versions found for map: {map_entry.name}")
continue
latest_version = max(map_entry.versions, key=lambda version: version.created_at)
# latest_version_hash = latest_version.hash_
song_hash = latest_version.hash_
for diff in latest_version.diffs:
if diff.characteristic != 'Standard':
if diff.characteristic not in game_modes:
continue
leaderboard_data = beatleader_api.get_leaderboard(latest_version.hash_, diff.difficulty)
logging.info(f"Getting leaderboard for {i+1}/{len(map_data)}: {map_entry.name}")
leaderboard_data = beatleader_api.get_leaderboard(song_hash, diff.difficulty)
if not leaderboard_data:
logging.warning(f"No leaderboard data for {map_entry.name} [{diff.difficulty}]")
logging.debug(f"No leaderboard data for {map_entry.name} [{diff.difficulty}], skipping")
continue
# Calculate average accuracy
accuracies = [entry.get('accuracy', 0) for entry in leaderboard_data if 'accuracy' in entry]
if not accuracies:
logging.warning(f"No accuracy data for {map_entry.name} [{diff.difficulty}]")
logging.debug(f"No accuracy data for {map_entry.name} [{diff.difficulty}]")
continue
avg_accuracy = mean(accuracies)
collected_data.append({
'hash': song_hash,
'difficulties': [
{
'name': diff.difficulty,
'characteristic': diff.characteristic
}
],
'map_name': map_entry.name,
'difficulty': diff.difficulty,
'average_accuracy': avg_accuracy
})
logging.info(f"Collected {len(collected_data)} map+difficulty combinations by average accuracy for {month}/{year}.")
logging.info(f"Collected leaderboards for {len(collected_data)} map+difficulty combinations, orderable by average accuracy of top ten plays for {month}/{year}.")
return collected_data
def playlist_strategy_highest_accuracy(count: int = 40) -> List[Dict]:
def playlist_strategy_highest_accuracy(
song_count: int = 40
) -> List[Dict[str, Any]]:
"""
Selects the top map+difficulty combinations with the highest average accuracy.
Args:
count: The number of map+difficulty combinations to select. Default is 40.
Returns:
A list of dictionaries containing the selected map+difficulty combinations,
each with:
- map_name: Name of the map
- difficulty: Difficulty level
- average_accuracy: Average accuracy of the leaderboard
Build and format a list of songs based on the highest average accuracy from recent maps.
Prompts the user for the month and year, defaulting to last month.
Excludes any map that's in the history, regardless of difficulty.
:param song_count: The number of songs to include in the playlist. Default is 40.
:return: A list of dictionaries containing song information for the playlist.
"""
# Retrieve the collected map+difficulty data with average accuracies
map_difficulty_data = map_leaders_by_month()
history = load_history()
history.setdefault('highest_accuracy', {})
history.setdefault('playlist_counts', {})
if not map_difficulty_data:
logging.error("No map+difficulty data available to create a playlist.")
# Get last month's date
today = datetime.now()
last_month = today.replace(day=1) - timedelta(days=1)
default_month = last_month.month
default_year = last_month.year
# Prompt for month and year
while True:
month_input = input(f"Enter month (1-12, default {default_month}): ").strip() or str(default_month)
year_input = input(f"Enter year (default {default_year}): ").strip() or str(default_year)
try:
month = int(month_input)
year = int(year_input)
if 1 <= month <= 12 and 2000 <= year <= datetime.now().year:
break
else:
print("Invalid month or year. Please try again.")
except ValueError:
print("Invalid input. Please enter numbers only.")
# Get the current count for this year/month and increment it
count_key = f"{year}-{month:02d}"
current_count = history['playlist_counts'].get(count_key, 0)
new_count = current_count + 1
history['playlist_counts'][count_key] = new_count
leaderboard_data = map_leaders_by_month(month=month, year=year)
if not leaderboard_data:
logging.error(f"No map+difficulty data available for {calendar.month_name[month]} {year}.")
return []
# Sort the data by average_accuracy in descending order
sorted_data = sorted(
map_difficulty_data,
leaderboard_data,
key=lambda x: x['average_accuracy'],
reverse=True
)
# Select the top 'count' entries
selected_playlist = sorted_data[:count]
playlist_data = []
for entry in sorted_data:
if len(playlist_data) >= song_count:
break
# Log the selected playlist
logging.info(f"Selected top {count} map+difficulty combinations by average accuracy:")
for idx, entry in enumerate(selected_playlist, start=1):
logging.info(
f"{idx}. {entry['map_name']} [{entry['difficulty']}] - "
f"Average Accuracy: {entry['average_accuracy'] * 100:.2f}%"
)
song_hash = entry['hash']
return selected_playlist
# Check history to avoid reusing any map, regardless of difficulty
if song_hash in history['highest_accuracy']:
logging.debug(f"Skipping song {song_hash} as it's in history.")
continue
playlist_data.append({
'hash': song_hash,
'difficulties': entry['difficulties'],
'songName': entry['map_name']
})
# Log the song addition
difficulty = entry['difficulties'][0]['name']
logging.info(f"Song added: {entry['map_name']} ({difficulty}) - Average Accuracy: {entry['average_accuracy'] * 100:.2f}%")
# Update history (now we're just adding the song hash, not the difficulty)
history['highest_accuracy'][song_hash] = True
# Log if no songs were added
if not playlist_data:
logging.info(f"No new songs found to add to the playlist for {calendar.month_name[month]} {year} based on history.")
else:
logging.info(f"Total songs added to playlist: {len(playlist_data)}")
save_history(history)
return playlist_data, f"highest_accuracy-{year}-{month:02d}-{new_count:02d}"
def saberlist() -> None:
"""
@ -401,36 +469,34 @@ def saberlist() -> None:
Avoids reusing the same song+difficulty in a playlist based on history.
"""
strategy = get_strategy()
timestamp = datetime.now().strftime("%y%m%d_%H%M%S")
playlist_title = f"{strategy}-{timestamp}"
if strategy == 'scoresaber_oldscores':
api = ScoreSaberAPI(cache_expiry_days=CACHE_EXPIRY_DAYS)
playlist_data = playlist_strategy_scoresaber_oldscores(ScoreSaberAPI(cache_expiry_days=CACHE_EXPIRY_DAYS))
elif strategy == 'beatleader_oldscores':
api = BeatLeaderAPI(cache_expiry_days=CACHE_EXPIRY_DAYS)
playlist_data = playlist_strategy_beatleader_oldscores(BeatLeaderAPI(cache_expiry_days=CACHE_EXPIRY_DAYS))
elif strategy == 'highest_accuracy':
playlist_data, playlist_title = playlist_strategy_highest_accuracy()
else:
logging.error(f"Unknown strategy '{strategy}'")
return
timestamp = datetime.now().strftime("%y%m%d_%H%M")
playlist_name = f"{strategy}-{timestamp}"
if strategy == 'scoresaber_oldscores':
playlist_data = playlist_strategy_scoresaber_oldscores(api)
elif strategy == 'beatleader_oldscores':
playlist_data = playlist_strategy_beatleader_oldscores(api)
if not playlist_data:
logging.info("No new scores found to add to the playlist.")
return
PlaylistBuilder().create_playlist(
playlist_data,
playlist_title=playlist_name,
playlist_title=playlist_title,
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"],
choices=["scoresaber_oldscores", "beatleader_oldscores", "highest_accuracy"],
help="Specify the playlist generation strategy",
required=True)
@ -439,4 +505,4 @@ def get_strategy():
sys.exit(1)
args = parser.parse_args()
return args.strategy
return args.strategy