Compare commits
2 Commits
a099898e4e
...
b8184c7e51
Author | SHA1 | Date | |
---|---|---|---|
b8184c7e51 | |||
ff6e2e4968 |
@ -21,53 +21,92 @@ from helpers.SimpleBeatLeaderAPI import SimpleBeatLeaderAPI
|
||||
from helpers.SimpleBeatSaverAPI import SimpleBeatSaverAPI
|
||||
|
||||
from saberlist.utils import reset_history
|
||||
from saberlist.playlist_strategies.oldscores import playlist_strategy_beatleader_oldscores, playlist_strategy_scoresaber_oldscores
|
||||
from saberlist.playlist_strategies.accuracy import playlist_strategy_beatleader_lowest_acc, playlist_strategy_beatleader_accuracy_gaps, playlist_strategy_scoresaber_accuracy_gaps
|
||||
from saberlist.playlist_strategies.performance import playlist_strategy_beatleader_lowest_pp, playlist_strategy_scoresaber_lowest_pp
|
||||
from saberlist.playlist_strategies.beatsaver import playlist_strategy_beatsaver_acc, playlist_strategy_beatsaver_curated, playlist_strategy_beatsaver_mappers
|
||||
from saberlist.playlist_strategies.oldscores import (
|
||||
playlist_strategy_beatleader_oldscores,
|
||||
playlist_strategy_scoresaber_oldscores,
|
||||
)
|
||||
from saberlist.playlist_strategies.accuracy import (
|
||||
playlist_strategy_beatleader_lowest_acc,
|
||||
playlist_strategy_beatleader_accuracy_gaps,
|
||||
playlist_strategy_scoresaber_accuracy_gaps,
|
||||
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 (
|
||||
playlist_strategy_beatsaver_acc,
|
||||
playlist_strategy_beatsaver_curated,
|
||||
playlist_strategy_beatsaver_mappers,
|
||||
)
|
||||
|
||||
def saberlist() -> None:
|
||||
"""
|
||||
Generate a playlist of songs using a specified strategy.
|
||||
Avoids reusing the same song+difficulty in a playlist based on history.
|
||||
"""
|
||||
strategy = get_strategy()
|
||||
args = parse_args_subcommands()
|
||||
strategy = args.subcommand
|
||||
|
||||
# If the user requested a reset, do that before anything else
|
||||
if getattr(args, 'reset', False):
|
||||
reset_history(strategy)
|
||||
sys.exit(0)
|
||||
|
||||
# Then call the strategy-based logic
|
||||
if strategy == 'scoresaber_oldscores':
|
||||
playlist_data, playlist_title = playlist_strategy_scoresaber_oldscores(ScoreSaberAPI(cache_expiry_days=CACHE_EXPIRY_DAYS))
|
||||
playlist_data, playlist_title = playlist_strategy_scoresaber_oldscores(
|
||||
ScoreSaberAPI(cache_expiry_days=CACHE_EXPIRY_DAYS)
|
||||
)
|
||||
playlist_builder = PlaylistBuilder()
|
||||
|
||||
elif strategy == 'beatleader_oldscores':
|
||||
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)
|
||||
)
|
||||
playlist_builder = PlaylistBuilder()
|
||||
elif strategy == 'beatsaver_acc':
|
||||
playlist_data, playlist_title = playlist_strategy_beatsaver_acc()
|
||||
playlist_builder = PlaylistBuilder(covers_dir='./covers/beatsavers')
|
||||
elif strategy == 'beatleader_lowest_pp':
|
||||
playlist_data, playlist_title = playlist_strategy_beatleader_lowest_pp(BeatLeaderAPI(cache_expiry_days=CACHE_EXPIRY_DAYS))
|
||||
playlist_builder = PlaylistBuilder(covers_dir='./covers/beatleader')
|
||||
elif strategy == 'scoresaber_lowest_pp':
|
||||
playlist_data, playlist_title = playlist_strategy_scoresaber_lowest_pp(ScoreSaberAPI(cache_expiry_days=CACHE_EXPIRY_DAYS))
|
||||
playlist_builder = PlaylistBuilder(covers_dir='./covers/scoresaber')
|
||||
elif strategy == 'beatleader_lowest_acc':
|
||||
playlist_data, playlist_title = playlist_strategy_beatleader_lowest_acc(BeatLeaderAPI(cache_expiry_days=CACHE_EXPIRY_DAYS))
|
||||
playlist_builder = PlaylistBuilder(covers_dir='./covers/kaiju')
|
||||
|
||||
elif strategy == 'beatleader_accuracy_gaps':
|
||||
playlist_data, playlist_title = playlist_strategy_beatleader_accuracy_gaps(SimpleBeatLeaderAPI(cache_expiry_days=CACHE_EXPIRY_DAYS))
|
||||
playlist_data, playlist_title = playlist_strategy_beatleader_accuracy_gaps(
|
||||
SimpleBeatLeaderAPI(cache_expiry_days=CACHE_EXPIRY_DAYS)
|
||||
)
|
||||
playlist_builder = PlaylistBuilder(covers_dir='./covers/pajamas')
|
||||
|
||||
elif strategy == 'beatleader_accuracy_gaps_star_range':
|
||||
# Here we demonstrate how sub-command arguments
|
||||
# can hold extra parameters, like star_level:
|
||||
star_level = float(args.star_level or 6)
|
||||
playlist_data, playlist_title = playlist_strategy_beatleader_accuracy_gaps_star_range(
|
||||
SimpleBeatLeaderAPI(cache_expiry_days=CACHE_EXPIRY_DAYS),
|
||||
star_level=star_level
|
||||
)
|
||||
playlist_builder = PlaylistBuilder(covers_dir='./covers/pajamas')
|
||||
|
||||
elif strategy == 'scoresaber_accuracy_gaps':
|
||||
playlist_data, playlist_title = playlist_strategy_scoresaber_accuracy_gaps(ScoreSaberAPI(cache_expiry_days=CACHE_EXPIRY_DAYS))
|
||||
playlist_data, playlist_title = playlist_strategy_scoresaber_accuracy_gaps(
|
||||
ScoreSaberAPI(cache_expiry_days=CACHE_EXPIRY_DAYS)
|
||||
)
|
||||
playlist_builder = PlaylistBuilder(covers_dir='./covers/scoresaber')
|
||||
|
||||
elif strategy == 'beatsaver_curated':
|
||||
playlist_data, playlist_title = playlist_strategy_beatsaver_curated(SimpleBeatSaverAPI())
|
||||
playlist_builder = PlaylistBuilder(covers_dir='./covers/curated')
|
||||
|
||||
elif strategy == 'beatsaver_mappers':
|
||||
playlist_data, playlist_title = playlist_strategy_beatsaver_mappers(SimpleBeatSaverAPI())
|
||||
playlist_builder = PlaylistBuilder(covers_dir='./covers/pajamas')
|
||||
|
||||
elif strategy == 'blank_playlist':
|
||||
playlist_data = []
|
||||
playlist_title = input("Enter playlist title: ")
|
||||
playlist_builder = PlaylistBuilder(covers_dir='./covers/pajamas')
|
||||
|
||||
else:
|
||||
logging.error(f"Unknown strategy '{strategy}'")
|
||||
return
|
||||
|
||||
if not playlist_data:
|
||||
if not playlist_data and strategy != 'blank_playlist':
|
||||
logging.info("No new scores found to add to the playlist.")
|
||||
return
|
||||
|
||||
@ -77,39 +116,82 @@ def saberlist() -> None:
|
||||
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",
|
||||
# "beatsaver_acc",
|
||||
# "beatleader_lowest_pp",
|
||||
# "scoresaber_lowest_pp",
|
||||
# "beatleader_lowest_acc",
|
||||
"beatleader_accuracy_gaps",
|
||||
"scoresaber_accuracy_gaps",
|
||||
"beatsaver_curated",
|
||||
"beatsaver_mappers"
|
||||
],
|
||||
help="Specify the playlist generation strategy")
|
||||
parser.add_argument("-r", "--reset",
|
||||
action="store_true",
|
||||
help="Reset the history for the specified strategy")
|
||||
|
||||
def parse_args_subcommands():
|
||||
"""
|
||||
Parse sub-commands for each strategy instead of using a single `-s/--strategy`.
|
||||
"""
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Generate Beat Saber playlists"
|
||||
)
|
||||
|
||||
subparsers = parser.add_subparsers(
|
||||
title="Available Strategies",
|
||||
dest="subcommand",
|
||||
help="Choose which sub-command (strategy) to run"
|
||||
)
|
||||
|
||||
# 1) -------- scoresaber_oldscores --------
|
||||
parser_ss_old = subparsers.add_parser("scoresaber_oldscores",
|
||||
help="Generate a playlist using ScoreSaber old-scores strategy")
|
||||
parser_ss_old.add_argument("-r", "--reset",
|
||||
action="store_true",
|
||||
help="Reset the history for scoresaber_oldscores")
|
||||
|
||||
# 2) -------- beatleader_oldscores --------
|
||||
parser_bl_old = subparsers.add_parser("beatleader_oldscores",
|
||||
help="Generate a playlist using BeatLeader old-scores strategy")
|
||||
parser_bl_old.add_argument("-r", "--reset",
|
||||
action="store_true",
|
||||
help="Reset the history for beatleader_oldscores")
|
||||
|
||||
# 3) -------- beatleader_accuracy_gaps --------
|
||||
parser_bl_acc_gaps = subparsers.add_parser("beatleader_accuracy_gaps",
|
||||
help="Generate a playlist using BeatLeader accuracy gaps strategy")
|
||||
parser_bl_acc_gaps.add_argument("-r", "--reset",
|
||||
action="store_true",
|
||||
help="Reset the history for beatleader_accuracy_gaps")
|
||||
|
||||
# 4) -------- beatleader_accuracy_gaps_star_range --------
|
||||
parser_bl_acc_stars = subparsers.add_parser("beatleader_accuracy_gaps_star_range",
|
||||
help="Generate a playlist for accuracy gaps within a star range (BeatLeader)")
|
||||
parser_bl_acc_stars.add_argument("-r", "--reset",
|
||||
action="store_true",
|
||||
help="Reset the history for beatleader_accuracy_gaps_star_range")
|
||||
parser_bl_acc_stars.add_argument("--star-level",
|
||||
type=float,
|
||||
help="Star level to filter on")
|
||||
|
||||
# 5) -------- scoresaber_accuracy_gaps --------
|
||||
parser_ss_acc_gaps = subparsers.add_parser("scoresaber_accuracy_gaps",
|
||||
help="Generate a playlist using ScoreSaber accuracy gap strategy")
|
||||
parser_ss_acc_gaps.add_argument("-r", "--reset",
|
||||
action="store_true",
|
||||
help="Reset the history for scoresaber_accuracy_gaps")
|
||||
|
||||
# 6) -------- beatsaver_curated --------
|
||||
parser_bs_curated = subparsers.add_parser("beatsaver_curated",
|
||||
help="Generate a curated BeatSaver playlist")
|
||||
parser_bs_curated.add_argument("-r", "--reset",
|
||||
action="store_true",
|
||||
help="Reset the history for beatsaver_curated")
|
||||
|
||||
# 7) -------- beatsaver_mappers --------
|
||||
parser_bs_mappers = subparsers.add_parser("beatsaver_mappers",
|
||||
help="Generate a playlist for specified BeatSaver mappers")
|
||||
parser_bs_mappers.add_argument("-r", "--reset",
|
||||
action="store_true",
|
||||
help="Reset the history for beatsaver_mappers")
|
||||
|
||||
# 8) -------- blank_playlist --------
|
||||
parser_blank = subparsers.add_parser("blank_playlist",
|
||||
help="Generate a blank playlist (no songs, just a descriptor)")
|
||||
parser_blank.add_argument("-r", "--reset",
|
||||
action="store_true",
|
||||
help="Reset the history for blank_playlist (usually unnecessary)")
|
||||
|
||||
# If no arguments passed, print help
|
||||
if len(sys.argv) == 1:
|
||||
parser.print_help()
|
||||
parser.print_help(sys.stderr)
|
||||
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
|
||||
|
||||
return parser.parse_args()
|
||||
|
@ -391,3 +391,159 @@ def playlist_strategy_beatleader_lowest_acc(
|
||||
save_history(history)
|
||||
|
||||
return playlist_data, f"beatleader_lowest_acc-{new_count:02d}"
|
||||
|
||||
def playlist_strategy_beatleader_accuracy_gaps_star_range(
|
||||
api: SimpleBeatLeaderAPI,
|
||||
star_level: float,
|
||||
song_count: int = 40,
|
||||
bin_size: float = 0.25,
|
||||
bin_sort: bool = False
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Build a playlist of songs where the player's accuracy is furthest below the median accuracy
|
||||
for a specified star rating range. Songs are grouped into bins by star rating within the
|
||||
specified range to ensure fair comparison.
|
||||
Like beatleader_accuracy_gaps, but only for a specific star rating range.
|
||||
|
||||
:param api: SimpleBeatLeaderAPI instance for making API calls
|
||||
:param star_level: Specific star level to filter songs (e.g., 6.0 for songs ranging from 6.00 to 6.99)
|
||||
:param song_count: Number of songs to include in the playlist
|
||||
:param bin_size: Size of star rating bins for grouping similar difficulty songs
|
||||
:param bin_sort: Whether to sort the bins by star rating
|
||||
:return: A tuple containing (list of song dictionaries, playlist title string)
|
||||
"""
|
||||
player_id = prompt_for_player_id()
|
||||
history = load_history()
|
||||
history.setdefault('beatleader_accuracy_gaps_star_range', {})
|
||||
history.setdefault('playlist_counts', {})
|
||||
|
||||
if not star_level:
|
||||
input_star_level = input("Enter star level (Default: 6)") or 6
|
||||
star_level = float(input_star_level)
|
||||
if not isinstance(star_level, (int, float)) or star_level < 0:
|
||||
logging.error("Invalid star_level provided. It must be a non-negative number.")
|
||||
return [], ""
|
||||
|
||||
# Define star range based on the specified star_level
|
||||
star_min = star_level
|
||||
star_max = star_level + 0.99 # Inclusive of the specified star_level up to the next whole number minus one cent
|
||||
|
||||
# Get the current count and increment it
|
||||
count_key = f'beatleader_accuracy_gaps_star_{star_level}'
|
||||
current_count = history['playlist_counts'].get(count_key, 0)
|
||||
new_count = current_count + 1
|
||||
history['playlist_counts'][count_key] = new_count
|
||||
|
||||
# Fetch accuracy graph data
|
||||
all_scores = api.get_player_accgraph(player_id)
|
||||
if not all_scores:
|
||||
logging.warning(f"No accgraph data found for player ID {player_id} on BeatLeader.")
|
||||
return [], ""
|
||||
logging.debug(f"Found {len(all_scores)} accgraph entries for player ID {player_id} on BeatLeader.")
|
||||
|
||||
# Filter scores within the specified star range
|
||||
filtered_scores = [
|
||||
entry for entry in all_scores
|
||||
if entry.get('stars') is not None and star_min <= entry['stars'] <= star_max
|
||||
]
|
||||
|
||||
if not filtered_scores:
|
||||
logging.warning(f"No accgraph entries found within star range {star_min} to {star_max}.")
|
||||
return [], ""
|
||||
logging.debug(f"Found {len(filtered_scores)} accgraph entries within star range {star_min} to {star_max}.")
|
||||
|
||||
# Collect all star ratings within the specified range
|
||||
star_ratings = [entry['stars'] for entry in filtered_scores if entry.get('stars') is not None]
|
||||
min_stars = min(star_ratings)
|
||||
max_stars = max(star_ratings)
|
||||
star_range = max_stars - min_stars
|
||||
|
||||
# Determine number of bins
|
||||
num_bins = math.ceil(star_range / bin_size)
|
||||
logging.info(f"Using bin size: {bin_size}, resulting in {num_bins} bins within star range {star_min} to {star_max}.")
|
||||
|
||||
# Group accuracies by bins
|
||||
bin_to_accuracies = defaultdict(list)
|
||||
for entry in filtered_scores:
|
||||
stars = entry.get('stars')
|
||||
acc = entry.get('acc')
|
||||
if stars is None or acc is None:
|
||||
continue
|
||||
bin_index = int((stars - min_stars) / bin_size)
|
||||
bin_to_accuracies[bin_index].append(acc)
|
||||
|
||||
# Calculate median accuracy for each bin
|
||||
bin_to_median = {}
|
||||
for bin_index, accs in bin_to_accuracies.items():
|
||||
bin_to_median[bin_index] = median(accs)
|
||||
bin_start = min_stars + bin_index * bin_size
|
||||
bin_end = bin_start + bin_size
|
||||
logging.debug(f"Median accuracy for bin {bin_index} (stars {bin_start:.2f} to {bin_end:.2f}): {bin_to_median[bin_index]:.4f}")
|
||||
|
||||
# Compute difference from median for each score
|
||||
for entry in filtered_scores:
|
||||
stars = entry.get('stars')
|
||||
acc = entry.get('acc')
|
||||
if stars is not None and acc is not None:
|
||||
bin_index = int((stars - min_stars) / bin_size)
|
||||
median_acc = bin_to_median.get(bin_index)
|
||||
if median_acc is not None:
|
||||
entry['diff_from_median'] = acc - median_acc
|
||||
else:
|
||||
entry['diff_from_median'] = float('inf') # Place entries with missing data at the end
|
||||
else:
|
||||
entry['diff_from_median'] = float('inf') # Place entries with missing data at the end
|
||||
|
||||
# Sort scores by difference from median (ascending: most below median first)
|
||||
filtered_scores.sort(key=lambda x: x.get('diff_from_median', float('inf')))
|
||||
|
||||
playlist_data = []
|
||||
for score_entry in filtered_scores:
|
||||
if len(playlist_data) >= song_count:
|
||||
break
|
||||
|
||||
acc = score_entry.get('acc', 0)
|
||||
stars = score_entry.get('stars')
|
||||
song_hash = score_entry.get('hash')
|
||||
|
||||
if not song_hash or stars is None:
|
||||
logging.debug(f"Skipping entry due to missing hash or stars: {score_entry}")
|
||||
continue
|
||||
|
||||
difficulty = score_entry.get('diff', '')
|
||||
difficulty_characteristic = score_entry.get('mode', 'Standard')
|
||||
|
||||
if song_hash in history['beatleader_accuracy_gaps_star_range'] and difficulty in history['beatleader_accuracy_gaps_star_range'][song_hash]:
|
||||
logging.debug(f"Skipping song {song_hash} with difficulty {difficulty} as it's in history.")
|
||||
continue
|
||||
|
||||
song_dict = {
|
||||
'hash': song_hash,
|
||||
'difficulties': [
|
||||
{
|
||||
'name': difficulty,
|
||||
'characteristic': difficulty_characteristic
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
playlist_data.append(song_dict)
|
||||
logging.debug(f"Selected song for playlist: Hash={song_hash}, Difficulty={difficulty}, "
|
||||
f"Accuracy={acc*100:.2f}%, Diff from Median={score_entry['diff_from_median']*100:.2f}%")
|
||||
|
||||
# Update history
|
||||
history['beatleader_accuracy_gaps_star_range'].setdefault(song_hash, []).append(difficulty)
|
||||
|
||||
if not playlist_data:
|
||||
logging.info(f"No new songs found to add to the playlist based on history for BeatLeader accuracy gaps within star range {star_min} to {star_max}.")
|
||||
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 accuracy gaps within star range {star_min} to {star_max}: {len(playlist_data)}")
|
||||
|
||||
save_history(history)
|
||||
playlist_title = f"beatleader_accgraph_star_{star_level}-{new_count:02d}"
|
||||
|
||||
return playlist_data, playlist_title
|
Loading…
x
Reference in New Issue
Block a user