From 8423b86b6bca8a26122dc91061e87d436afe19dd Mon Sep 17 00:00:00 2001 From: Brian Lee Date: Mon, 1 Jul 2024 09:49:45 -0700 Subject: [PATCH] New helper tool to build playlists of previously played scoresaber songs. --- .gitignore | 2 + README.md | 1 + pyproject.toml | 5 +- src/saberlist/beatleader.py | 11 +++ src/saberlist/oldscript.py | 155 ------------------------------------ src/saberlist/scoresaber.py | 82 +++++++------------ 6 files changed, 47 insertions(+), 209 deletions(-) create mode 100644 src/saberlist/beatleader.py delete mode 100755 src/saberlist/oldscript.py diff --git a/.gitignore b/.gitignore index f6f9dd2..45e8ef6 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,5 @@ __pycache__/ *.pyc *.egg-info/ dist/ +*.json +archive/ \ No newline at end of file diff --git a/README.md b/README.md index 81bd867..2cf626f 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ pip install --editable . ## Resources +* [Beatleader API](https://beatleader.xyz/developer) * [ScoreSaber API](https://docs.scoresaber.com/) ## Tips diff --git a/pyproject.toml b/pyproject.toml index 4bfcaa5..c3694fe 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,7 +14,7 @@ dependencies = [ 'build>=1.2.1', 'requests>=2.31.0', 'pytest>=8.1.1', - 'pyscorescaber>=1.0.10' + 'PyScoreSaber>=1.0.10' ] requires-python = ">=3.8.10" classifiers = [ @@ -33,4 +33,5 @@ Homepage = "https://github.com/bleetube/reddit_export" # https://setuptools.pypa.io/en/latest/userguide/entry_point.html [project.scripts] -reddit_export = "reddit_export.fetch:main" \ No newline at end of file +replay_ranked_ss = "saberlist.scoresaber:replay_ranked" +#replay_ranked_bl = "saberlist.beatleader:replay_ranked" \ No newline at end of file diff --git a/src/saberlist/beatleader.py b/src/saberlist/beatleader.py new file mode 100644 index 0000000..3fae872 --- /dev/null +++ b/src/saberlist/beatleader.py @@ -0,0 +1,11 @@ +import requests +import json + +# Specify the URL +blee_id = '76561199407393962' +#url = f'https://api.beatleader.xyz/player/{blee_id}/rankedMaps' +url = f'https://api.beatleader.xyz/player/{blee_id}/scores' + +response = requests.get(url) + +response_content = json.loads(response.content) diff --git a/src/saberlist/oldscript.py b/src/saberlist/oldscript.py deleted file mode 100755 index 8cb844d..0000000 --- a/src/saberlist/oldscript.py +++ /dev/null @@ -1,155 +0,0 @@ -import base64, json -from pathlib import Path # Nice tutorial: https://realpython.com/python-pathlib/ - -# for get_json_data() -from urllib.request import Request, urlopen, urlretrieve, build_opener, install_opener - -# for ss_leaderboard_by_stars() -from time import sleep -import decimal # for rounding down - -default_leaderboard_results = 14 -difficulty_range = 0.09 # star difficulties to include in a single playlist, e.g. 7.1 to 7.19 -playlist_author = "ScoreSaber" - -# setup urllib user-agent (required for urlretrieve) -opener=build_opener() -opener.addheaders=[('User-Agent','Mozilla/5.0')] -install_opener(opener) - -difficulty_level = { - 9: 'ExpertPlus', - 7: 'Expert', - 5: 'Hard', - 3: 'Normal', - 1: 'Easy', -} - -max_pages = 100 - -def get_json_data( uri, params = {} ): - """Build an http request using custom user-agent, otherwise target site will block the request with a 403 error. - Returns a dictionary object on success.""" -# problem with params: data turns the request into a POST -# http_req = Request( uri, data=params, headers=http_headers ) -# http_req = Request( uri, headers=http_headers ) - http_req = Request( uri ) - try: - with urlopen( http_req ) as http_response: - return json.loads( http_response.read().decode() ) - except Exception as e: - print(e) - -@click.command() -@click.option('--star', type=int, - help='Star difficulty rating.') -def ss_leaderboard_by_stars( star: int ): - """Build separate playlists for every block of n.1 for a given star level""" - stars = float( star ) - ss_leaderboard = {} - - # https://docs.scoresaber.com/#/Leaderboards/get_api_leaderboards - uri = 'https://scoresaber.com/api/leaderboards' - params = { - "category": 3, # sort by scores - "sort": 1, # sort ascending - "maxStar": star + 1, - "minStar": star, - "page": 1, - "qualified": 0, - "ranked": 1, - } - ss_playlists = {} - # Configure Decimal to help us truncate a float - decimal.getcontext().rounding = decimal.ROUND_DOWN - - # sanity check: never paginate more than 100 times - while params['page'] < max_pages: - - # TODO: call get_json_data using the params dict instead of doing this ugly ass string manipulation - ss_request = "https://scoresaber.com/api/leaderboards?" + \ - f"category=3&maxStar={params['maxStar']}&minStar={params['minStar']}&qualified=0&ranked=0&sort=1&verified=1&page={params['page']}" - ss_response = get_json_data( ss_request ) - - # save results - for song in ss_response['leaderboards']: - # maxStar is necessarily one integer higher, but we don't want songs on that difficulty. - if song['stars'] == params['maxStar']: - print( f"Skipping: {song['stars']}★ [{difficulty_level[ song['difficulty']['difficulty'] ]}] : {song['songName']} by {song['songAuthorName']}") - continue - - print( f"Adding: {song['stars']}★ [{difficulty_level[ song['difficulty']['difficulty'] ]}] : {song['songName']} by {song['songAuthorName']}") - - # Group songs by rounding down to one decimal place to truncate the star difficulty. - playlist_group = round( decimal.Decimal( song['stars'] ), 1 ) - playlist_title = f"{playlist_group:.1f}" - - if playlist_title not in ss_playlists: - # initialize the playlist - ss_playlists[ playlist_title ] = [] - # add song to playlist - ss_playlists[ playlist_title ].append( song ) - - # if we got less than the typical number of results, we've reached the last page of results. - leaderboard_results = len( ss_response['leaderboards'] ) - if leaderboard_results < default_leaderboard_results: - print( f"Got {leaderboard_results} results on page {params['page']}, ending search." ) - break - - print( f"Got {leaderboard_results} songs on page {params['page']}/{max_pages}, continuing to next page in 500 milliseconds." ) - sleep(0.5) - params['page'] += 1 - - # END WHILE - - # build all the playlists - for title, playlist in ss_playlists.items(): - build_playlist( title, playlist ) - -def build_playlist(title: str, playlist: list): - """Create a new playlist file for Beat Saber.""" - # Initialize a new playlist. - # \u2605 is the unicode star emoji. - bplist = { 'playlistTitle': f"Ranked {title}\u2605", - 'playlistAuthor': playlist_author, - 'playlistDescription': 'Automatically generated playlist', - 'songs': [], - 'image': "" - } - for song in playlist: - bplist['songs'].append({ - 'key': song['id'], - 'hash': song['songHash'], - 'name': song['songName'], - 'uploader': song['songAuthorName'], - 'difficulties': [ - { - 'characteristic': "Standard", - 'name': difficulty_level[ song['difficulty']['difficulty'] ] - } - ] - }) - # check for cover art and add it - coverart_path = Path.cwd() / "coverart" / f"{title}.jpg" - if coverart_path.is_file(): - with open( coverart_path, 'rb') as cover: - cover_data = base64.b64encode(cover.read()).decode() - bplist["image"] = f"base64,{cover_data}" - print( "INFO: Cover art was added.") - else: - print( "NOTICE: Cover art not found, skipping.") - - # create a subdir named "playlists" if it doesn't already exist - playlist_path = Path.cwd() / "playlists" - playlist_path.mkdir( exist_ok = True ) - playlist_file = Path( playlist_path / f"{title}.bplist" ) - try: - with open(playlist_file, "w") as playlist: - playlist.write( json.dumps( bplist )) - print( f"INFO: Created new playlist: {title}.bplist" ) - except Exception as e: - print(f"ERROR: Failed to create playlist: {playlist_file}") - print(e) - -if __name__ == '__main__': - ss_leaderboard_by_stars() diff --git a/src/saberlist/scoresaber.py b/src/saberlist/scoresaber.py index 1becc06..7a2e73b 100644 --- a/src/saberlist/scoresaber.py +++ b/src/saberlist/scoresaber.py @@ -1,19 +1,7 @@ import json import asyncio from pyscoresaber import ScoreSaberAPI, ScoreSort - -blee_id = '76561199407393962' -isuldor_id = '76561197962107242' - -async def show_player(player_id): - async with ScoreSaberAPI() as scoresaber: - return await scoresaber.player_full(player_id) - -async def get_all_player_scores(scoresaber: ScoreSaberAPI, player_id: int, score_sort: ScoreSort): - all_scores = [] - async for player_scores in scoresaber.player_scores_all(player_id, score_sort): - all_scores.extend(player_scores) - return all_scores +import requests def filter_and_sort_scores_by_stars(scores, min_stars=0.1, max_stars=float('inf')): # Exclude scores outside the specified star range @@ -22,33 +10,7 @@ def filter_and_sort_scores_by_stars(scores, min_stars=0.1, max_stars=float('inf' sorted_scores = sorted(filtered_scores, key=lambda x: x.leaderboard.stars) return sorted_scores -def scores_to_playlist(scores, playlist_title, playlist_author, filename): - playlist = { - "playlistTitle": playlist_title, - "playlistAuthor": playlist_author, - "songs": [] - } - - for score in scores: - song_entry = { - "hash": score.leaderboard.song_hash, - "songName": score.leaderboard.song_name, - "difficulties": [ - { - "name": score.leaderboard.difficulty.difficulty.name.lower(), - "characteristic": score.leaderboard.difficulty.game_mode.value - } - ], - "levelAuthorName": score.leaderboard.level_author_name - } - playlist["songs"].append(song_entry) - - playlist_json = json.dumps(playlist) - - with open(filename, 'w') as file: - file.write(playlist_json) - -def scores_to_playlist(scores, playlist_title, playlist_author, filename): +def scores_to_playlist(scores, playlist_title, playlist_author = "SaberList Tool"): playlist = { "playlistTitle": playlist_title, "playlistAuthor": playlist_author, @@ -71,21 +33,37 @@ def scores_to_playlist(scores, playlist_title, playlist_author, filename): playlist_json = json.dumps(playlist, indent=4) - with open(filename, 'w') as file: + with open(f"{playlist_title}.json", 'w') as file: file.write(playlist_json) return playlist_json -async def build_playlists(player_id): - async with ScoreSaberAPI() as api: - scores = await get_all_player_scores(api, player_id, ScoreSort.TOP) - return scores +async def async_replay_ranked(): + scoresaber = ScoreSaberAPI() + try: + await scoresaber.start() # Initialize the API client + + score_sort = ScoreSort.TOP + scores = [] + default_player_id = '76561199407393962' + player_id = input(f"Enter the playerid (Default: {default_player_id}): ") or default_player_id + default_min_stars = 5 + min_stars = float(input(f"Enter the minimum starlevel to include on the playlist (Default: {default_min_stars}): ") or default_min_stars) + default_max_stars = min_stars + 1 + max_stars = float(input(f"Enter the maximum starlevel to include on the playlist (Default: {default_max_stars}): ") or default_max_stars) + default_title = f"Replay SS {min_stars}★" + playlist_title = input(f"Enter the filename for the playlist (Default: {default_title}): ") or default_title - scores = asyncio.run(build_playlists(blee_id)) + async for player_scores in scoresaber.player_scores_all(player_id, score_sort): + scores.extend(player_scores) - filtered_sorted_scores = filter_and_sort_scores_by_stars(scores, min_stars=2, max_stars=3) - scores_to_playlist(filtered_sorted_scores, "Played SS TWO\u2605", "ScoreSaber", "Played SS TWO.bplist") - filtered_sorted_scores = filter_and_sort_scores_by_stars(scores, min_stars=3, max_stars=4) - scores_to_playlist(filtered_sorted_scores, "Played SS THREE\u2605", "ScoreSaber", "Played SS THREE.bplist") - filtered_sorted_scores = filter_and_sort_scores_by_stars(scores, min_stars=4, max_stars=5) - scores_to_playlist(filtered_sorted_scores, "Played SS FOUR\u2605", "ScoreSaber", "Played SS FOUR.bplist") + filtered_sorted_scores = filter_and_sort_scores_by_stars(scores, min_stars, max_stars) + scores_to_playlist(filtered_sorted_scores, playlist_title) + finally: + await scoresaber._http_client.close() # Ensure the session is closed + +def replay_ranked(): + try: + asyncio.run(async_replay_ranked()) + except Exception as e: + print(f"An error occurred: {e}")