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.ScoreSaberAPI import ScoreSaberAPI
|
||||||
from helpers.BeatLeaderAPI import BeatLeaderAPI
|
from helpers.BeatLeaderAPI import BeatLeaderAPI
|
||||||
|
|
||||||
|
import calendar
|
||||||
|
|
||||||
def load_history() -> Dict[str, Any]:
|
def load_history() -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
Load the playlist history from a JSON file.
|
Load the playlist history from a JSON file.
|
||||||
@ -34,8 +36,10 @@ def load_history() -> Dict[str, Any]:
|
|||||||
"""
|
"""
|
||||||
if os.path.exists(HISTORY_FILE):
|
if os.path.exists(HISTORY_FILE):
|
||||||
with open(HISTORY_FILE, 'r') as f:
|
with open(HISTORY_FILE, 'r') as f:
|
||||||
return json.load(f)
|
history = json.load(f)
|
||||||
return {}
|
history.setdefault('playlist_counts', {})
|
||||||
|
return history
|
||||||
|
return {'highest_accuracy': {}, 'playlist_counts': {}}
|
||||||
|
|
||||||
def save_history(history: Dict[str, Any]) -> None:
|
def save_history(history: Dict[str, Any]) -> None:
|
||||||
"""
|
"""
|
||||||
@ -298,101 +302,165 @@ def playlist_strategy_beatleader_oldscores(
|
|||||||
|
|
||||||
return playlist_data
|
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,
|
Gathers a month's worth of maps using the BeatSaver latest maps endpoint,
|
||||||
prioritizes map difficulties where players have already set good scores,
|
prioritizes map difficulties where players have already set good scores,
|
||||||
and calculates the average accuracy for each map+difficulty.
|
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:
|
Returns:
|
||||||
A list of dictionaries, each containing:
|
A list of dictionaries, each containing:
|
||||||
|
- hash: Hash of the map
|
||||||
|
- difficulties: List of difficulties with their characteristics
|
||||||
- map_name: Name of the map
|
- map_name: Name of the map
|
||||||
- difficulty: Difficulty level
|
|
||||||
- average_accuracy: Average accuracy of the leaderboard
|
- average_accuracy: Average accuracy of the leaderboard
|
||||||
"""
|
"""
|
||||||
beatleader_api = SimpleBeatLeaderAPI()
|
beatleader_api = SimpleBeatLeaderAPI(cache_expiry_days=30)
|
||||||
beatsaver_api = BeatSaverAPI()
|
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 = []
|
collected_data = []
|
||||||
|
|
||||||
for map_entry in map_data:
|
for i, map_entry in enumerate(map_data):
|
||||||
|
|
||||||
# Ensure there are versions available
|
# Ensure there are versions available
|
||||||
if not map_entry.versions:
|
if not map_entry.versions:
|
||||||
logging.warning(f"No versions found for map: {map_entry.name}")
|
logging.warning(f"No versions found for map: {map_entry.name}")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
latest_version = max(map_entry.versions, key=lambda version: version.created_at)
|
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:
|
for diff in latest_version.diffs:
|
||||||
if diff.characteristic != 'Standard':
|
if diff.characteristic not in game_modes:
|
||||||
continue
|
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:
|
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
|
continue
|
||||||
|
|
||||||
# Calculate average accuracy
|
# Calculate average accuracy
|
||||||
accuracies = [entry.get('accuracy', 0) for entry in leaderboard_data if 'accuracy' in entry]
|
accuracies = [entry.get('accuracy', 0) for entry in leaderboard_data if 'accuracy' in entry]
|
||||||
if not accuracies:
|
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
|
continue
|
||||||
|
|
||||||
avg_accuracy = mean(accuracies)
|
avg_accuracy = mean(accuracies)
|
||||||
|
|
||||||
collected_data.append({
|
collected_data.append({
|
||||||
|
'hash': song_hash,
|
||||||
|
'difficulties': [
|
||||||
|
{
|
||||||
|
'name': diff.difficulty,
|
||||||
|
'characteristic': diff.characteristic
|
||||||
|
}
|
||||||
|
],
|
||||||
'map_name': map_entry.name,
|
'map_name': map_entry.name,
|
||||||
'difficulty': diff.difficulty,
|
|
||||||
'average_accuracy': avg_accuracy
|
'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
|
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.
|
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.
|
||||||
Args:
|
Excludes any map that's in the history, regardless of difficulty.
|
||||||
count: The number of map+difficulty combinations to select. Default is 40.
|
|
||||||
|
:param song_count: The number of songs to include in the playlist. Default is 40.
|
||||||
Returns:
|
:return: A list of dictionaries containing song information for the playlist.
|
||||||
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
|
|
||||||
"""
|
"""
|
||||||
# Retrieve the collected map+difficulty data with average accuracies
|
history = load_history()
|
||||||
map_difficulty_data = map_leaders_by_month()
|
history.setdefault('highest_accuracy', {})
|
||||||
|
history.setdefault('playlist_counts', {})
|
||||||
|
|
||||||
if not map_difficulty_data:
|
# Get last month's date
|
||||||
logging.error("No map+difficulty data available to create a playlist.")
|
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 []
|
return []
|
||||||
|
|
||||||
# Sort the data by average_accuracy in descending order
|
# Sort the data by average_accuracy in descending order
|
||||||
sorted_data = sorted(
|
sorted_data = sorted(
|
||||||
map_difficulty_data,
|
leaderboard_data,
|
||||||
key=lambda x: x['average_accuracy'],
|
key=lambda x: x['average_accuracy'],
|
||||||
reverse=True
|
reverse=True
|
||||||
)
|
)
|
||||||
|
|
||||||
# Select the top 'count' entries
|
playlist_data = []
|
||||||
selected_playlist = sorted_data[:count]
|
for entry in sorted_data:
|
||||||
|
if len(playlist_data) >= song_count:
|
||||||
|
break
|
||||||
|
|
||||||
# Log the selected playlist
|
song_hash = entry['hash']
|
||||||
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}%"
|
|
||||||
)
|
|
||||||
|
|
||||||
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:
|
def saberlist() -> None:
|
||||||
"""
|
"""
|
||||||
@ -401,36 +469,34 @@ def saberlist() -> None:
|
|||||||
Avoids reusing the same song+difficulty in a playlist based on history.
|
Avoids reusing the same song+difficulty in a playlist based on history.
|
||||||
"""
|
"""
|
||||||
strategy = get_strategy()
|
strategy = get_strategy()
|
||||||
|
|
||||||
|
timestamp = datetime.now().strftime("%y%m%d_%H%M%S")
|
||||||
|
playlist_title = f"{strategy}-{timestamp}"
|
||||||
|
|
||||||
if strategy == 'scoresaber_oldscores':
|
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':
|
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:
|
else:
|
||||||
logging.error(f"Unknown strategy '{strategy}'")
|
logging.error(f"Unknown strategy '{strategy}'")
|
||||||
return
|
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:
|
if not playlist_data:
|
||||||
logging.info("No new scores found to add to the playlist.")
|
logging.info("No new scores found to add to the playlist.")
|
||||||
return
|
return
|
||||||
|
|
||||||
PlaylistBuilder().create_playlist(
|
PlaylistBuilder().create_playlist(
|
||||||
playlist_data,
|
playlist_data,
|
||||||
playlist_title=playlist_name,
|
playlist_title=playlist_title,
|
||||||
playlist_author="SaberList Tool"
|
playlist_author="SaberList Tool"
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_strategy():
|
def get_strategy():
|
||||||
parser = argparse.ArgumentParser(description="Generate Beat Saber playlists")
|
parser = argparse.ArgumentParser(description="Generate Beat Saber playlists")
|
||||||
parser.add_argument("-s", "--strategy",
|
parser.add_argument("-s", "--strategy",
|
||||||
choices=["scoresaber_oldscores", "beatleader_oldscores"],
|
choices=["scoresaber_oldscores", "beatleader_oldscores", "highest_accuracy"],
|
||||||
help="Specify the playlist generation strategy",
|
help="Specify the playlist generation strategy",
|
||||||
required=True)
|
required=True)
|
||||||
|
|
||||||
@ -439,4 +505,4 @@ def get_strategy():
|
|||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
return args.strategy
|
return args.strategy
|
||||||
|
Loading…
Reference in New Issue
Block a user