Nicer playlists names and log messages to indicate that leaderboards are being cached.
This commit is contained in:
parent
eb3e3f3054
commit
4f58daa29f
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user