Rewrite BeatLeader class and add an oldscore strategy for it.
This commit is contained in:
parent
76e796eb16
commit
2d87cdd59e
@ -2,6 +2,12 @@
|
||||
|
||||
SaberList is a tool for generating custom Beat Saber playlists based on a player's performance data from Beat Leader.
|
||||
|
||||
## Resources
|
||||
|
||||
* Beat Leader [swagger](https://api.beatleader.xyz/swagger/index.html), [GitHub](https://github.com/BeatLeader)
|
||||
* Score Saber [swagger](https://docs.scoresaber.com/), [Github](https://github.com/ScoreSaber) (backend remains closed-source)
|
||||
* Beat Saver [swagger](https://api.beatsaver.com/docs/), [GitHub](https://github.com/beatmaps-io/beatsaver-main)
|
||||
|
||||
## Features
|
||||
|
||||
- Fetches player scores from Beat Leader API
|
||||
|
26
docs/ClientWrapperUsage.md
Normal file
26
docs/ClientWrapperUsage.md
Normal file
@ -0,0 +1,26 @@
|
||||
# API Client Wrapper Usage
|
||||
|
||||
## BeatLeaderClient
|
||||
|
||||
```python
|
||||
from helpers.BeatLeaderAPI import BeatLeaderAPI
|
||||
from clients.beatleader.models.scores_sort_by import ScoresSortBy
|
||||
from clients.beatleader.models.order import Order
|
||||
|
||||
# Instantiate the API client
|
||||
beatleader_api = BeatLeaderAPI()
|
||||
|
||||
# Specify the player ID you want to fetch scores for
|
||||
player_id = "76561199407393962"
|
||||
|
||||
# Fetch player scores
|
||||
scores_data = beatleader_api.get_player_scores(
|
||||
player_id=player_id,
|
||||
use_cache=True, # Use cached data if available
|
||||
count=100, # Number of scores per page
|
||||
sort_by=ScoresSortBy.DATE, # Sort scores by date
|
||||
order=Order.DESC, # In descending order
|
||||
max_pages=2 # Maximum number of pages to fetch
|
||||
)
|
||||
print(f"Got {len(scores_data.get('playerScores'))} scores for player {player_id}")
|
||||
```
|
11
docs/Python.md
Normal file
11
docs/Python.md
Normal file
@ -0,0 +1,11 @@
|
||||
# Python
|
||||
|
||||
Paginate a large dictionary:
|
||||
|
||||
```python
|
||||
from IPython.core.page import page
|
||||
from pprint import pformat
|
||||
def paged_print(obj):
|
||||
page(pformat(obj))
|
||||
paged_print(scores)
|
||||
```
|
78
docs/SimpleBeatLeaderAPI.md
Normal file
78
docs/SimpleBeatLeaderAPI.md
Normal file
@ -0,0 +1,78 @@
|
||||
# SimpleBeatLeaderAPI Python Wrapper
|
||||
|
||||
This simple Python class provides a convenient wrapper for interacting with the BeatLeader API, specifically for retrieving player scores and song data for the game Beat Saber.
|
||||
|
||||
## Features
|
||||
|
||||
- Fetch player scores and song data from the BeatLeader API
|
||||
- Local caching of API responses to reduce API calls and improve performance
|
||||
- Automatic pagination handling to retrieve all available data
|
||||
- Configurable cache expiration
|
||||
- Methods to retrieve both full and minimal song data
|
||||
|
||||
## Usage
|
||||
|
||||
### Basic Usage
|
||||
|
||||
```python
|
||||
from saberlist.SimpleBeatLeaderAPI import BeatLeaderAPI
|
||||
|
||||
# Initialize the API wrapper
|
||||
api = SimpleBeatLeaderAPI(cache_expiry_days=1)
|
||||
|
||||
# Fetch player scores
|
||||
player_id = '76561199407393962'
|
||||
scores = api.get_player_scores(player_id)
|
||||
scores.keys()
|
||||
scores['data'][0]
|
||||
|
||||
# Get full song data
|
||||
songs = api.get_player_songs(player_id)
|
||||
|
||||
```
|
||||
|
||||
### Caching
|
||||
|
||||
The class uses a local cache to store API responses. By default, the cache is located at:
|
||||
|
||||
- `~/.cache/saberlist/` on Unix-like systems (if `~/.cache/` exists)
|
||||
- `./.cache/` in the current working directory (as a fallback)
|
||||
|
||||
You can control cache behavior:
|
||||
|
||||
```python
|
||||
# Set cache expiry (in days)
|
||||
api = SimpleBeatLeaderAPI(cache_expiry_days=7)
|
||||
|
||||
# Force a fresh API call (ignore cache)
|
||||
fresh_scores = api.get_player_scores(player_id, use_cache=False)
|
||||
|
||||
# Clear cache for a specific player
|
||||
api.clear_cache(player_id)
|
||||
|
||||
# Clear all cache
|
||||
api.clear_cache()
|
||||
|
||||
# Get the current cache directory
|
||||
cache_dir = api.get_cache_dir()
|
||||
```
|
||||
|
||||
### Pagination
|
||||
|
||||
The `get_player_scores` method automatically handles pagination to retrieve all available scores. You can control this behavior:
|
||||
|
||||
```python
|
||||
# Set a custom page size (default is 100)
|
||||
scores = api.get_player_scores(player_id, page_size=50)
|
||||
|
||||
# Limit the number of pages fetched
|
||||
scores = api.get_player_scores(player_id, max_pages=5)
|
||||
```
|
||||
|
||||
## Methods
|
||||
|
||||
- `get_player_scores(player_id, use_cache=True, page_size=100, max_pages=None)`: Retrieves all scores for a player.
|
||||
- `get_player_songs(player_id, page=1, count=100, use_cache=True)`: Retrieves full song data for all unique songs a player has played.
|
||||
- `get_player_songs_minimal(player_id, page=1, count=100, use_cache=True)`: Retrieves minimal song data (id, name, author, mapper, hash, bpm, duration) for all unique songs a player has played.
|
||||
- `clear_cache(player_id=None)`: Clears the cache for a specific player or all cached data.
|
||||
- `get_cache_dir()`: Returns the path to the current cache directory.
|
@ -1,392 +0,0 @@
|
||||
# Python coding
|
||||
|
||||
We used openapi-python-client to generate client libraries for the scoresaber api. It's in clients/scoresaber in our python project:
|
||||
|
||||
```
|
||||
treegit
|
||||
.
|
||||
├── docs
|
||||
│ ├── api.md
|
||||
│ ├── capture.md
|
||||
│ └── PlaylistBuilder.md
|
||||
├── src
|
||||
│ ├── clients
|
||||
│ │ ├── beatleader
|
||||
│ │ │ ├── api
|
||||
│ │ │ │ ├── beast_saber
|
||||
│ │ │ │ │ ├── beast_saber_get_all.py
|
||||
│ │ │ │ │ ├── beast_saber_nominate.py
|
||||
│ │ │ │ │ └── __init__.py
|
||||
│ │ │ │ ├── clan
|
||||
│ │ │ │ │ ├── clan_get_all.py
|
||||
│ │ │ │ │ ├── clan_get_clan_by_id.py
|
||||
│ │ │ │ │ ├── clan_get_clan.py
|
||||
│ │ │ │ │ ├── clan_get_clan_with_maps_by_id.py
|
||||
│ │ │ │ │ ├── clan_get_clan_with_maps.py
|
||||
│ │ │ │ │ ├── clan_get_history.py
|
||||
│ │ │ │ │ ├── clan_global_map.py
|
||||
│ │ │ │ │ └── __init__.py
|
||||
│ │ │ │ ├── leaderboard
|
||||
│ │ │ │ │ ├── __init__.py
|
||||
│ │ │ │ │ ├── leaderboard_get_all.py
|
||||
│ │ │ │ │ ├── leaderboard_get_clan_rankings.py
|
||||
│ │ │ │ │ ├── leaderboard_get.py
|
||||
│ │ │ │ │ └── leaderboard_get_scoregraph.py
|
||||
│ │ │ │ ├── modifiers
|
||||
│ │ │ │ │ ├── __init__.py
|
||||
│ │ │ │ │ └── modifiers_get_modifiers.py
|
||||
│ │ │ │ ├── patreon
|
||||
│ │ │ │ │ ├── __init__.py
|
||||
│ │ │ │ │ └── patreon_refresh_my_patreon.py
|
||||
│ │ │ │ ├── player
|
||||
│ │ │ │ │ ├── __init__.py
|
||||
│ │ │ │ │ ├── player_get_beat_saver.py
|
||||
│ │ │ │ │ ├── player_get_discord.py
|
||||
│ │ │ │ │ ├── player_get_followers_info.py
|
||||
│ │ │ │ │ ├── player_get_followers.py
|
||||
│ │ │ │ │ ├── player_get_founded_clan.py
|
||||
│ │ │ │ │ ├── player_get_participating_events.py
|
||||
│ │ │ │ │ ├── player_get_patreon.py
|
||||
│ │ │ │ │ ├── player_get_players.py
|
||||
│ │ │ │ │ ├── player_get.py
|
||||
│ │ │ │ │ └── player_get_ranked_maps.py
|
||||
│ │ │ │ ├── player_scores
|
||||
│ │ │ │ │ ├── __init__.py
|
||||
│ │ │ │ │ ├── player_scores_acc_graph.py
|
||||
│ │ │ │ │ ├── player_scores_get_compact_history.py
|
||||
│ │ │ │ │ ├── player_scores_get_compact_scores.py
|
||||
│ │ │ │ │ ├── player_scores_get_history.py
|
||||
│ │ │ │ │ ├── player_scores_get_pinned_scores.py
|
||||
│ │ │ │ │ ├── player_scores_get_scores.py
|
||||
│ │ │ │ │ └── player_scores_get_score_value.py
|
||||
│ │ │ │ ├── song
|
||||
│ │ │ │ │ ├── __init__.py
|
||||
│ │ │ │ │ └── song_get_all.py
|
||||
│ │ │ │ └── __init__.py
|
||||
│ │ │ ├── models
|
||||
│ │ │ │ ├── __pycache__
|
||||
│ │ │ │ ├── achievement_description.py
|
||||
│ │ │ │ ├── achievement_level.py
|
||||
│ │ │ │ ├── achievement.py
|
||||
│ │ │ │ ├── badge.py
|
||||
│ │ │ │ ├── ban.py
|
||||
│ │ │ │ ├── beasties_nomination.py
|
||||
│ │ │ │ ├── besties_nomination_response.py
|
||||
│ │ │ │ ├── clan_bigger_response.py
|
||||
│ │ │ │ ├── clan_global_map_point.py
|
||||
│ │ │ │ ├── clan_global_map.py
|
||||
│ │ │ │ ├── clan_map_connection.py
|
||||
│ │ │ │ ├── clan_maps_sort_by.py
|
||||
│ │ │ │ ├── clan_point.py
|
||||
│ │ │ │ ├── clan.py
|
||||
│ │ │ │ ├── clan_ranking_response_clan_response_full_response_with_metadata_and_container.py
|
||||
│ │ │ │ ├── clan_ranking_response.py
|
||||
│ │ │ │ ├── clan_response_full.py
|
||||
│ │ │ │ ├── clan_response_full_response_with_metadata.py
|
||||
│ │ │ │ ├── clan_response.py
|
||||
│ │ │ │ ├── clan_sort_by.py
|
||||
│ │ │ │ ├── compact_leaderboard.py
|
||||
│ │ │ │ ├── compact_leaderboard_response.py
|
||||
│ │ │ │ ├── compact_score.py
|
||||
│ │ │ │ ├── compact_score_response.py
|
||||
│ │ │ │ ├── compact_score_response_response_with_metadata.py
|
||||
│ │ │ │ ├── compact_song_response.py
|
||||
│ │ │ │ ├── controller_enum.py
|
||||
│ │ │ │ ├── criteria_commentary.py
|
||||
│ │ │ │ ├── difficulty_description.py
|
||||
│ │ │ │ ├── difficulty_response.py
|
||||
│ │ │ │ ├── difficulty_status.py
|
||||
│ │ │ │ ├── event_player.py
|
||||
│ │ │ │ ├── event_ranking.py
|
||||
│ │ │ │ ├── external_status.py
|
||||
│ │ │ │ ├── featured_playlist.py
|
||||
│ │ │ │ ├── featured_playlist_response.py
|
||||
│ │ │ │ ├── follower_type.py
|
||||
│ │ │ │ ├── global_map_history.py
|
||||
│ │ │ │ ├── history_compact_response.py
|
||||
│ │ │ │ ├── hmd.py
|
||||
│ │ │ │ ├── info_to_highlight.py
|
||||
│ │ │ │ ├── __init__.py
|
||||
│ │ │ │ ├── leaderboard_change.py
|
||||
│ │ │ │ ├── leaderboard_clan_ranking_response.py
|
||||
│ │ │ │ ├── leaderboard_contexts.py
|
||||
│ │ │ │ ├── leaderboard_group_entry.py
|
||||
│ │ │ │ ├── leaderboard_info_response.py
|
||||
│ │ │ │ ├── leaderboard_info_response_response_with_metadata.py
|
||||
│ │ │ │ ├── leaderboard.py
|
||||
│ │ │ │ ├── leaderboard_response.py
|
||||
│ │ │ │ ├── leaderboard_sort_by.py
|
||||
│ │ │ │ ├── legacy_modifiers.py
|
||||
│ │ │ │ ├── link_response.py
|
||||
│ │ │ │ ├── map_diff_response.py
|
||||
│ │ │ │ ├── map_info_response.py
|
||||
│ │ │ │ ├── map_info_response_response_with_metadata.py
|
||||
│ │ │ │ ├── mapper.py
|
||||
│ │ │ │ ├── mapper_response.py
|
||||
│ │ │ │ ├── map_quality.py
|
||||
│ │ │ │ ├── map_sort_by.py
|
||||
│ │ │ │ ├── maps_type.py
|
||||
│ │ │ │ ├── metadata.py
|
||||
│ │ │ │ ├── modifiers_map.py
|
||||
│ │ │ │ ├── modifiers_rating.py
|
||||
│ │ │ │ ├── my_type.py
|
||||
│ │ │ │ ├── operation.py
|
||||
│ │ │ │ ├── order.py
|
||||
│ │ │ │ ├── participating_event_response.py
|
||||
│ │ │ │ ├── patreon_features.py
|
||||
│ │ │ │ ├── player_change.py
|
||||
│ │ │ │ ├── player_context_extension.py
|
||||
│ │ │ │ ├── player_follower.py
|
||||
│ │ │ │ ├── player_followers_info_response.py
|
||||
│ │ │ │ ├── player.py
|
||||
│ │ │ │ ├── player_response_clan_response_full_response_with_metadata_and_container.py
|
||||
│ │ │ │ ├── player_response_full.py
|
||||
│ │ │ │ ├── player_response.py
|
||||
│ │ │ │ ├── player_response_with_stats.py
|
||||
│ │ │ │ ├── player_response_with_stats_response_with_metadata.py
|
||||
│ │ │ │ ├── player_score_stats_history.py
|
||||
│ │ │ │ ├── player_score_stats.py
|
||||
│ │ │ │ ├── player_search.py
|
||||
│ │ │ │ ├── player_social.py
|
||||
│ │ │ │ ├── player_sort_by.py
|
||||
│ │ │ │ ├── pp_type.py
|
||||
│ │ │ │ ├── profile_settings.py
|
||||
│ │ │ │ ├── qualification_change.py
|
||||
│ │ │ │ ├── qualification_commentary.py
|
||||
│ │ │ │ ├── qualification_vote.py
|
||||
│ │ │ │ ├── ranked_mapper_response.py
|
||||
│ │ │ │ ├── ranked_map.py
|
||||
│ │ │ │ ├── rank_qualification.py
|
||||
│ │ │ │ ├── rank_update_change.py
|
||||
│ │ │ │ ├── rank_update.py
|
||||
│ │ │ │ ├── rank_voting.py
|
||||
│ │ │ │ ├── replay_offsets.py
|
||||
│ │ │ │ ├── requirements.py
|
||||
│ │ │ │ ├── score_filter_status.py
|
||||
│ │ │ │ ├── score_graph_entry.py
|
||||
│ │ │ │ ├── score_improvement.py
|
||||
│ │ │ │ ├── score_metadata.py
|
||||
│ │ │ │ ├── score_response.py
|
||||
│ │ │ │ ├── score_response_with_acc.py
|
||||
│ │ │ │ ├── score_response_with_my_score.py
|
||||
│ │ │ │ ├── score_response_with_my_score_response_with_metadata.py
|
||||
│ │ │ │ ├── scores_sort_by.py
|
||||
│ │ │ │ ├── song.py
|
||||
│ │ │ │ ├── song_response.py
|
||||
│ │ │ │ ├── song_status.py
|
||||
│ │ │ │ ├── type.py
|
||||
│ │ │ │ └── voter_feedback.py
|
||||
│ │ │ ├── __pycache__
|
||||
│ │ │ ├── client.py
|
||||
│ │ │ ├── errors.py
|
||||
│ │ │ ├── __init__.py
|
||||
│ │ │ ├── py.typed
|
||||
│ │ │ └── types.py
|
||||
│ │ ├── beatsaver
|
||||
│ │ │ ├── api
|
||||
│ │ │ │ ├── maps
|
||||
│ │ │ │ │ ├── get_maps_collaborations_id.py
|
||||
│ │ │ │ │ ├── get_maps_hash_hash.py
|
||||
│ │ │ │ │ ├── get_maps_id_id.py
|
||||
│ │ │ │ │ ├── get_maps_ids_ids.py
|
||||
│ │ │ │ │ ├── get_maps_latest.py
|
||||
│ │ │ │ │ ├── get_maps_plays_page.py
|
||||
│ │ │ │ │ ├── get_maps_uploader_id_page.py
|
||||
│ │ │ │ │ └── __init__.py
|
||||
│ │ │ │ ├── playlists
|
||||
│ │ │ │ │ ├── get_playlists_id_id_page.py
|
||||
│ │ │ │ │ ├── get_playlists_latest.py
|
||||
│ │ │ │ │ ├── get_playlists_search_page.py
|
||||
│ │ │ │ │ ├── get_playlists_user_user_id_page.py
|
||||
│ │ │ │ │ ├── __init__.py
|
||||
│ │ │ │ │ └── post_playlists_id_id_batch.py
|
||||
│ │ │ │ ├── search
|
||||
│ │ │ │ │ ├── get_search_text_page.py
|
||||
│ │ │ │ │ └── __init__.py
|
||||
│ │ │ │ ├── users
|
||||
│ │ │ │ │ ├── get_users_id_id.py
|
||||
│ │ │ │ │ ├── get_users_ids_ids.py
|
||||
│ │ │ │ │ ├── get_users_name_name.py
|
||||
│ │ │ │ │ ├── __init__.py
|
||||
│ │ │ │ │ └── post_users_verify.py
|
||||
│ │ │ │ ├── vote
|
||||
│ │ │ │ │ ├── get_vote.py
|
||||
│ │ │ │ │ ├── __init__.py
|
||||
│ │ │ │ │ └── post_vote.py
|
||||
│ │ │ │ └── __init__.py
|
||||
│ │ │ ├── models
|
||||
│ │ │ │ ├── __pycache__
|
||||
│ │ │ │ ├── action_response.py
|
||||
│ │ │ │ ├── auth_request.py
|
||||
│ │ │ │ ├── get_maps_latest_sort.py
|
||||
│ │ │ │ ├── get_playlists_latest_sort.py
|
||||
│ │ │ │ ├── get_playlists_search_page_sort_order.py
|
||||
│ │ │ │ ├── get_search_text_page_leaderboard.py
|
||||
│ │ │ │ ├── get_search_text_page_sort_order.py
|
||||
│ │ │ │ ├── __init__.py
|
||||
│ │ │ │ ├── map_detail_declared_ai.py
|
||||
│ │ │ │ ├── map_detail_metadata.py
|
||||
│ │ │ │ ├── map_detail.py
|
||||
│ │ │ │ ├── map_detail_tags_item.py
|
||||
│ │ │ │ ├── map_detail_with_order.py
|
||||
│ │ │ │ ├── map_difficulty_characteristic.py
|
||||
│ │ │ │ ├── map_difficulty_difficulty.py
|
||||
│ │ │ │ ├── map_difficulty.py
|
||||
│ │ │ │ ├── map_parity_summary.py
|
||||
│ │ │ │ ├── map_stats.py
|
||||
│ │ │ │ ├── map_stats_sentiment.py
|
||||
│ │ │ │ ├── map_testplay.py
|
||||
│ │ │ │ ├── map_version.py
|
||||
│ │ │ │ ├── map_version_state.py
|
||||
│ │ │ │ ├── playlist_batch_request.py
|
||||
│ │ │ │ ├── playlist_full.py
|
||||
│ │ │ │ ├── playlist_full_type.py
|
||||
│ │ │ │ ├── playlist_page.py
|
||||
│ │ │ │ ├── playlist_search_response.py
|
||||
│ │ │ │ ├── playlist_stats.py
|
||||
│ │ │ │ ├── search_response.py
|
||||
│ │ │ │ ├── user_detail_patreon.py
|
||||
│ │ │ │ ├── user_detail.py
|
||||
│ │ │ │ ├── user_detail_type.py
|
||||
│ │ │ │ ├── user_diff_stats.py
|
||||
│ │ │ │ ├── user_follow_data.py
|
||||
│ │ │ │ ├── user_stats.py
|
||||
│ │ │ │ ├── vote_request.py
|
||||
│ │ │ │ └── vote_summary.py
|
||||
│ │ │ ├── __pycache__
|
||||
│ │ │ ├── client.py
|
||||
│ │ │ ├── errors.py
|
||||
│ │ │ ├── __init__.py
|
||||
│ │ │ ├── py.typed
|
||||
│ │ │ └── types.py
|
||||
│ │ ├── __pycache__
|
||||
│ │ ├── scoresaber
|
||||
│ │ │ ├── api
|
||||
│ │ │ │ ├── leaderboards
|
||||
│ │ │ │ │ ├── get_api_leaderboard_by_hash_hash_info.py
|
||||
│ │ │ │ │ ├── get_api_leaderboard_by_hash_hash_scores.py
|
||||
│ │ │ │ │ ├── get_api_leaderboard_by_id_leaderboard_id_info.py
|
||||
│ │ │ │ │ ├── get_api_leaderboard_by_id_leaderboard_id_scores.py
|
||||
│ │ │ │ │ ├── get_api_leaderboard_get_difficulties_hash.py
|
||||
│ │ │ │ │ ├── get_api_leaderboards.py
|
||||
│ │ │ │ │ └── __init__.py
|
||||
│ │ │ │ ├── nomination_assessment_team
|
||||
│ │ │ │ │ ├── __init__.py
|
||||
│ │ │ │ │ ├── post_api_ranking_request_action_nat_deny.py
|
||||
│ │ │ │ │ ├── post_api_ranking_request_action_nat_qualify.py
|
||||
│ │ │ │ │ └── post_api_ranking_request_action_nat_replace.py
|
||||
│ │ │ │ ├── players
|
||||
│ │ │ │ │ ├── get_api_player_player_id_basic.py
|
||||
│ │ │ │ │ ├── get_api_player_player_id_full.py
|
||||
│ │ │ │ │ ├── get_api_player_player_id_scores.py
|
||||
│ │ │ │ │ ├── get_api_players_count.py
|
||||
│ │ │ │ │ ├── get_api_players.py
|
||||
│ │ │ │ │ └── __init__.py
|
||||
│ │ │ │ ├── public_ranking
|
||||
│ │ │ │ │ ├── get_api_ranking_request_by_id_leaderboard_id.py
|
||||
│ │ │ │ │ ├── get_api_ranking_request_request_id.py
|
||||
│ │ │ │ │ ├── get_api_ranking_requests_below_top.py
|
||||
│ │ │ │ │ ├── get_api_ranking_requests_top.py
|
||||
│ │ │ │ │ └── __init__.py
|
||||
│ │ │ │ ├── quality_assurance_team
|
||||
│ │ │ │ │ ├── __init__.py
|
||||
│ │ │ │ │ ├── post_api_ranking_request_action_qat_comment.py
|
||||
│ │ │ │ │ └── post_api_ranking_request_action_qat_vote.py
|
||||
│ │ │ │ ├── ranking_team
|
||||
│ │ │ │ │ ├── __init__.py
|
||||
│ │ │ │ │ ├── post_api_ranking_request_action_rt_comment.py
|
||||
│ │ │ │ │ ├── post_api_ranking_request_action_rt_create.py
|
||||
│ │ │ │ │ └── post_api_ranking_request_action_rt_vote.py
|
||||
│ │ │ │ ├── website_auth
|
||||
│ │ │ │ │ ├── get_api_auth_get_token.py
|
||||
│ │ │ │ │ ├── get_api_auth_logout.py
|
||||
│ │ │ │ │ ├── __init__.py
|
||||
│ │ │ │ │ └── post_api_auth_check_token.py
|
||||
│ │ │ │ ├── website_user
|
||||
│ │ │ │ │ ├── get_api_user_me.py
|
||||
│ │ │ │ │ ├── get_api_user_player_id_refresh.py
|
||||
│ │ │ │ │ ├── get_api_user_quest_key.py
|
||||
│ │ │ │ │ └── __init__.py
|
||||
│ │ │ │ └── __init__.py
|
||||
│ │ │ ├── models
|
||||
│ │ │ │ ├── __pycache__
|
||||
│ │ │ │ ├── badge.py
|
||||
│ │ │ │ ├── check_token_body.py
|
||||
│ │ │ │ ├── comment.py
|
||||
│ │ │ │ ├── difficulty.py
|
||||
│ │ │ │ ├── get_api_player_player_id_scores_sort.py
|
||||
│ │ │ │ ├── i_get_token_response.py
|
||||
│ │ │ │ ├── __init__.py
|
||||
│ │ │ │ ├── leaderboard_info_collection.py
|
||||
│ │ │ │ ├── leaderboard_info.py
|
||||
│ │ │ │ ├── leaderboard_player.py
|
||||
│ │ │ │ ├── metadata.py
|
||||
│ │ │ │ ├── nat_deny_body.py
|
||||
│ │ │ │ ├── nat_qualify_body.py
|
||||
│ │ │ │ ├── nat_replace_body.py
|
||||
│ │ │ │ ├── player_badges_type_1.py
|
||||
│ │ │ │ ├── player_collection.py
|
||||
│ │ │ │ ├── player.py
|
||||
│ │ │ │ ├── player_score_collection.py
|
||||
│ │ │ │ ├── player_score.py
|
||||
│ │ │ │ ├── qat_comment_body.py
|
||||
│ │ │ │ ├── qat_vote_body.py
|
||||
│ │ │ │ ├── ranking_difficulty.py
|
||||
│ │ │ │ ├── rank_request_information.py
|
||||
│ │ │ │ ├── rank_request_listing.py
|
||||
│ │ │ │ ├── rt_comment_body.py
|
||||
│ │ │ │ ├── rt_create_body.py
|
||||
│ │ │ │ ├── rt_vote_body.py
|
||||
│ │ │ │ ├── score_collection.py
|
||||
│ │ │ │ ├── score.py
|
||||
│ │ │ │ ├── score_saber_error.py
|
||||
│ │ │ │ ├── score_stats.py
|
||||
│ │ │ │ ├── user_data.py
|
||||
│ │ │ │ └── vote_group.py
|
||||
│ │ │ ├── __pycache__
|
||||
│ │ │ ├── client.py
|
||||
│ │ │ ├── errors.py
|
||||
│ │ │ ├── __init__.py
|
||||
│ │ │ ├── py.typed
|
||||
│ │ │ └── types.py
|
||||
│ │ └── __init__.py
|
||||
│ ├── pleb_saberlist.egg-info
|
||||
│ │ ├── dependency_links.txt
|
||||
│ │ ├── entry_points.txt
|
||||
│ │ ├── PKG-INFO
|
||||
│ │ ├── requires.txt
|
||||
│ │ ├── SOURCES.txt
|
||||
│ │ └── top_level.txt
|
||||
│ ├── saberlist
|
||||
│ │ ├── __pycache__
|
||||
│ │ ├── beatleader.py
|
||||
│ │ ├── __init__.py
|
||||
│ │ ├── PlaylistBuilder.py
|
||||
│ │ └── scoresaber.py
|
||||
│ └── saberlist.egg-info
|
||||
│ ├── dependency_links.txt
|
||||
│ ├── entry_points.txt
|
||||
│ ├── PKG-INFO
|
||||
│ ├── requires.txt
|
||||
│ ├── SOURCES.txt
|
||||
│ └── top_level.txt
|
||||
├── temp_covers
|
||||
├── tests
|
||||
│ ├── archive
|
||||
│ │ ├── __pycache__
|
||||
│ │ └── saberlist
|
||||
│ │ ├── __pycache__
|
||||
│ │ └── playlist_builder.py
|
||||
│ ├── assets
|
||||
│ │ └── sample_cover.jpg
|
||||
│ ├── __pycache__
|
||||
│ └── playlist_builder.py
|
||||
├── convert-comyfui-outupt.sh
|
||||
├── LICENSE
|
||||
├── pyproject.toml
|
||||
└── README.md
|
||||
|
||||
53 directories, 328 files
|
||||
```
|
||||
|
||||
We are trying to learn how to use the scoresaber client in `clients/scoresaber` that we generated using openapi-python-client.
|
543
docs/prompts/01-scratchpad.md
Normal file
543
docs/prompts/01-scratchpad.md
Normal file
@ -0,0 +1,543 @@
|
||||
# Python coding
|
||||
|
||||
We used openapi-python-client to generate client libraries for the beatleader.xyz api. It's in clients/beatleader in our python project:
|
||||
|
||||
```
|
||||
.
|
||||
├── docs/
|
||||
│ ├── prompts/
|
||||
│ └── *.md
|
||||
├── src/
|
||||
│ ├── clients/
|
||||
│ │ ├── beatleader/
|
||||
│ │ │ ├── api/ (various API endpoints)
|
||||
│ │ │ ├── models/ (data models)
|
||||
│ │ │ └── client.py, errors.py, __init__.py, types.py
|
||||
│ │ ├── beatsaver/ (similar structure to beatleader)
|
||||
│ │ └── scoresaber/ (similar structure to beatleader)
|
||||
│ ├── helpers/
|
||||
│ │ └── *.py
|
||||
│ └── saberlist/
|
||||
│ └── *.py
|
||||
├── tests/
|
||||
│ ├── assets/
|
||||
│ └── playlist_builder.py
|
||||
├── pyproject.toml
|
||||
└── README.md
|
||||
```
|
||||
|
||||
Here's the clients/beatleader dir:
|
||||
|
||||
```
|
||||
treegit src/clients/beatleader/
|
||||
src/clients/beatleader/
|
||||
├── api
|
||||
│ ├── beast_saber
|
||||
│ │ ├── beast_saber_get_all.py
|
||||
│ │ ├── beast_saber_nominate.py
|
||||
│ │ └── __init__.py
|
||||
│ ├── clan
|
||||
│ │ ├── clan_get_all.py
|
||||
│ │ ├── clan_get_clan_by_id.py
|
||||
│ │ ├── clan_get_clan.py
|
||||
│ │ ├── clan_get_clan_with_maps_by_id.py
|
||||
│ │ ├── clan_get_clan_with_maps.py
|
||||
│ │ ├── clan_get_history.py
|
||||
│ │ ├── clan_global_map.py
|
||||
│ │ └── __init__.py
|
||||
│ ├── leaderboard
|
||||
│ │ ├── __init__.py
|
||||
│ │ ├── leaderboard_get_all.py
|
||||
│ │ ├── leaderboard_get_clan_rankings.py
|
||||
│ │ ├── leaderboard_get.py
|
||||
│ │ └── leaderboard_get_scoregraph.py
|
||||
│ ├── modifiers
|
||||
│ │ ├── __init__.py
|
||||
│ │ └── modifiers_get_modifiers.py
|
||||
│ ├── patreon
|
||||
│ │ ├── __init__.py
|
||||
│ │ └── patreon_refresh_my_patreon.py
|
||||
│ ├── player
|
||||
│ │ ├── __init__.py
|
||||
│ │ ├── player_get_beat_saver.py
|
||||
│ │ ├── player_get_discord.py
|
||||
│ │ ├── player_get_followers_info.py
|
||||
│ │ ├── player_get_followers.py
|
||||
│ │ ├── player_get_founded_clan.py
|
||||
│ │ ├── player_get_participating_events.py
|
||||
│ │ ├── player_get_patreon.py
|
||||
│ │ ├── player_get_players.py
|
||||
│ │ ├── player_get.py
|
||||
│ │ └── player_get_ranked_maps.py
|
||||
│ ├── player_scores
|
||||
│ │ ├── __init__.py
|
||||
│ │ ├── player_scores_acc_graph.py
|
||||
│ │ ├── player_scores_get_compact_history.py
|
||||
│ │ ├── player_scores_get_compact_scores.py
|
||||
│ │ ├── player_scores_get_history.py
|
||||
│ │ ├── player_scores_get_pinned_scores.py
|
||||
│ │ ├── player_scores_get_scores.py
|
||||
│ │ └── player_scores_get_score_value.py
|
||||
│ ├── song
|
||||
│ │ ├── __init__.py
|
||||
│ │ └── song_get_all.py
|
||||
│ └── __init__.py
|
||||
├── models
|
||||
│ ├── __pycache__
|
||||
│ ├── achievement_description.py
|
||||
│ ├── achievement_level.py
|
||||
│ ├── achievement.py
|
||||
│ ├── badge.py
|
||||
│ ├── ban.py
|
||||
│ ├── beasties_nomination.py
|
||||
│ ├── besties_nomination_response.py
|
||||
│ ├── clan_bigger_response.py
|
||||
│ ├── clan_global_map_point.py
|
||||
│ ├── clan_global_map.py
|
||||
│ ├── clan_map_connection.py
|
||||
│ ├── clan_maps_sort_by.py
|
||||
│ ├── clan_point.py
|
||||
│ ├── clan.py
|
||||
│ ├── clan_ranking_response_clan_response_full_response_with_metadata_and_container.py
|
||||
│ ├── clan_ranking_response.py
|
||||
│ ├── clan_response_full.py
|
||||
│ ├── clan_response_full_response_with_metadata.py
|
||||
│ ├── clan_response.py
|
||||
│ ├── clan_sort_by.py
|
||||
│ ├── compact_leaderboard.py
|
||||
│ ├── compact_leaderboard_response.py
|
||||
│ ├── compact_score.py
|
||||
│ ├── compact_score_response.py
|
||||
│ ├── compact_score_response_response_with_metadata.py
|
||||
│ ├── compact_song_response.py
|
||||
│ ├── controller_enum.py
|
||||
│ ├── criteria_commentary.py
|
||||
│ ├── difficulty_description.py
|
||||
│ ├── difficulty_response.py
|
||||
│ ├── difficulty_status.py
|
||||
│ ├── event_player.py
|
||||
│ ├── event_ranking.py
|
||||
│ ├── external_status.py
|
||||
│ ├── featured_playlist.py
|
||||
│ ├── featured_playlist_response.py
|
||||
│ ├── follower_type.py
|
||||
│ ├── global_map_history.py
|
||||
│ ├── history_compact_response.py
|
||||
│ ├── hmd.py
|
||||
│ ├── info_to_highlight.py
|
||||
│ ├── __init__.py
|
||||
│ ├── leaderboard_change.py
|
||||
│ ├── leaderboard_clan_ranking_response.py
|
||||
│ ├── leaderboard_contexts.py
|
||||
│ ├── leaderboard_group_entry.py
|
||||
│ ├── leaderboard_info_response.py
|
||||
│ ├── leaderboard_info_response_response_with_metadata.py
|
||||
│ ├── leaderboard.py
|
||||
│ ├── leaderboard_response.py
|
||||
│ ├── leaderboard_sort_by.py
|
||||
│ ├── legacy_modifiers.py
|
||||
│ ├── link_response.py
|
||||
│ ├── map_diff_response.py
|
||||
│ ├── map_info_response.py
|
||||
│ ├── map_info_response_response_with_metadata.py
|
||||
│ ├── mapper.py
|
||||
│ ├── mapper_response.py
|
||||
│ ├── map_quality.py
|
||||
│ ├── map_sort_by.py
|
||||
│ ├── maps_type.py
|
||||
│ ├── metadata.py
|
||||
│ ├── modifiers_map.py
|
||||
│ ├── modifiers_rating.py
|
||||
│ ├── my_type.py
|
||||
│ ├── operation.py
|
||||
│ ├── order.py
|
||||
│ ├── participating_event_response.py
|
||||
│ ├── patreon_features.py
|
||||
│ ├── player_change.py
|
||||
│ ├── player_context_extension.py
|
||||
│ ├── player_follower.py
|
||||
│ ├── player_followers_info_response.py
|
||||
│ ├── player.py
|
||||
│ ├── player_response_clan_response_full_response_with_metadata_and_container.py
|
||||
│ ├── player_response_full.py
|
||||
│ ├── player_response.py
|
||||
│ ├── player_response_with_stats.py
|
||||
│ ├── player_response_with_stats_response_with_metadata.py
|
||||
│ ├── player_score_stats_history.py
|
||||
│ ├── player_score_stats.py
|
||||
│ ├── player_search.py
|
||||
│ ├── player_social.py
|
||||
│ ├── player_sort_by.py
|
||||
│ ├── pp_type.py
|
||||
│ ├── profile_settings.py
|
||||
│ ├── qualification_change.py
|
||||
│ ├── qualification_commentary.py
|
||||
│ ├── qualification_vote.py
|
||||
│ ├── ranked_mapper_response.py
|
||||
│ ├── ranked_map.py
|
||||
│ ├── rank_qualification.py
|
||||
│ ├── rank_update_change.py
|
||||
│ ├── rank_update.py
|
||||
│ ├── rank_voting.py
|
||||
│ ├── replay_offsets.py
|
||||
│ ├── requirements.py
|
||||
│ ├── score_filter_status.py
|
||||
│ ├── score_graph_entry.py
|
||||
│ ├── score_improvement.py
|
||||
│ ├── score_metadata.py
|
||||
│ ├── score_response.py
|
||||
│ ├── score_response_with_acc.py
|
||||
│ ├── score_response_with_my_score.py
|
||||
│ ├── score_response_with_my_score_response_with_metadata.py
|
||||
│ ├── scores_sort_by.py
|
||||
│ ├── song.py
|
||||
│ ├── song_response.py
|
||||
│ ├── song_status.py
|
||||
│ ├── type.py
|
||||
│ └── voter_feedback.py
|
||||
├── __pycache__
|
||||
├── client.py
|
||||
├── errors.py
|
||||
├── __init__.py
|
||||
├── py.typed
|
||||
└── types.py
|
||||
|
||||
13 directories, 158 files
|
||||
```
|
||||
|
||||
Here's the contents of `src/clients/beatleader/client.py`:
|
||||
|
||||
```python
|
||||
import ssl
|
||||
from typing import Any, Dict, Optional, Union
|
||||
|
||||
import httpx
|
||||
from attrs import define, evolve, field
|
||||
|
||||
|
||||
@define
|
||||
class Client:
|
||||
"""A class for keeping track of data related to the API
|
||||
|
||||
The following are accepted as keyword arguments and will be used to construct httpx Clients internally:
|
||||
|
||||
``base_url``: The base URL for the API, all requests are made to a relative path to this URL
|
||||
|
||||
``cookies``: A dictionary of cookies to be sent with every request
|
||||
|
||||
``headers``: A dictionary of headers to be sent with every request
|
||||
|
||||
``timeout``: The maximum amount of a time a request can take. API functions will raise
|
||||
httpx.TimeoutException if this is exceeded.
|
||||
|
||||
``verify_ssl``: Whether or not to verify the SSL certificate of the API server. This should be True in production,
|
||||
but can be set to False for testing purposes.
|
||||
|
||||
``follow_redirects``: Whether or not to follow redirects. Default value is False.
|
||||
|
||||
``httpx_args``: A dictionary of additional arguments to be passed to the ``httpx.Client`` and ``httpx.AsyncClient`` constructor.
|
||||
|
||||
|
||||
Attributes:
|
||||
raise_on_unexpected_status: Whether or not to raise an errors.UnexpectedStatus if the API returns a
|
||||
status code that was not documented in the source OpenAPI document. Can also be provided as a keyword
|
||||
argument to the constructor.
|
||||
"""
|
||||
|
||||
raise_on_unexpected_status: bool = field(default=False, kw_only=True)
|
||||
_base_url: str = field(alias="base_url")
|
||||
_cookies: Dict[str, str] = field(factory=dict, kw_only=True, alias="cookies")
|
||||
_headers: Dict[str, str] = field(factory=dict, kw_only=True, alias="headers")
|
||||
_timeout: Optional[httpx.Timeout] = field(default=None, kw_only=True, alias="timeout")
|
||||
_verify_ssl: Union[str, bool, ssl.SSLContext] = field(default=True, kw_only=True, alias="verify_ssl")
|
||||
_follow_redirects: bool = field(default=False, kw_only=True, alias="follow_redirects")
|
||||
_httpx_args: Dict[str, Any] = field(factory=dict, kw_only=True, alias="httpx_args")
|
||||
_client: Optional[httpx.Client] = field(default=None, init=False)
|
||||
_async_client: Optional[httpx.AsyncClient] = field(default=None, init=False)
|
||||
|
||||
def with_headers(self, headers: Dict[str, str]) -> "Client":
|
||||
"""Get a new client matching this one with additional headers"""
|
||||
if self._client is not None:
|
||||
self._client.headers.update(headers)
|
||||
if self._async_client is not None:
|
||||
self._async_client.headers.update(headers)
|
||||
return evolve(self, headers={**self._headers, **headers})
|
||||
|
||||
def with_cookies(self, cookies: Dict[str, str]) -> "Client":
|
||||
"""Get a new client matching this one with additional cookies"""
|
||||
if self._client is not None:
|
||||
self._client.cookies.update(cookies)
|
||||
if self._async_client is not None:
|
||||
self._async_client.cookies.update(cookies)
|
||||
return evolve(self, cookies={**self._cookies, **cookies})
|
||||
|
||||
def with_timeout(self, timeout: httpx.Timeout) -> "Client":
|
||||
"""Get a new client matching this one with a new timeout (in seconds)"""
|
||||
if self._client is not None:
|
||||
self._client.timeout = timeout
|
||||
if self._async_client is not None:
|
||||
self._async_client.timeout = timeout
|
||||
return evolve(self, timeout=timeout)
|
||||
|
||||
def set_httpx_client(self, client: httpx.Client) -> "Client":
|
||||
"""Manually the underlying httpx.Client
|
||||
|
||||
**NOTE**: This will override any other settings on the client, including cookies, headers, and timeout.
|
||||
"""
|
||||
self._client = client
|
||||
return self
|
||||
|
||||
def get_httpx_client(self) -> httpx.Client:
|
||||
"""Get the underlying httpx.Client, constructing a new one if not previously set"""
|
||||
if self._client is None:
|
||||
self._client = httpx.Client(
|
||||
base_url=self._base_url,
|
||||
cookies=self._cookies,
|
||||
headers=self._headers,
|
||||
timeout=self._timeout,
|
||||
verify=self._verify_ssl,
|
||||
follow_redirects=self._follow_redirects,
|
||||
**self._httpx_args,
|
||||
)
|
||||
return self._client
|
||||
|
||||
def __enter__(self) -> "Client":
|
||||
"""Enter a context manager for self.client—you cannot enter twice (see httpx docs)"""
|
||||
self.get_httpx_client().__enter__()
|
||||
return self
|
||||
|
||||
def __exit__(self, *args: Any, **kwargs: Any) -> None:
|
||||
"""Exit a context manager for internal httpx.Client (see httpx docs)"""
|
||||
self.get_httpx_client().__exit__(*args, **kwargs)
|
||||
|
||||
def set_async_httpx_client(self, async_client: httpx.AsyncClient) -> "Client":
|
||||
"""Manually the underlying httpx.AsyncClient
|
||||
|
||||
**NOTE**: This will override any other settings on the client, including cookies, headers, and timeout.
|
||||
"""
|
||||
self._async_client = async_client
|
||||
return self
|
||||
|
||||
def get_async_httpx_client(self) -> httpx.AsyncClient:
|
||||
"""Get the underlying httpx.AsyncClient, constructing a new one if not previously set"""
|
||||
if self._async_client is None:
|
||||
self._async_client = httpx.AsyncClient(
|
||||
base_url=self._base_url,
|
||||
cookies=self._cookies,
|
||||
headers=self._headers,
|
||||
timeout=self._timeout,
|
||||
verify=self._verify_ssl,
|
||||
follow_redirects=self._follow_redirects,
|
||||
**self._httpx_args,
|
||||
)
|
||||
return self._async_client
|
||||
|
||||
async def __aenter__(self) -> "Client":
|
||||
"""Enter a context manager for underlying httpx.AsyncClient—you cannot enter twice (see httpx docs)"""
|
||||
await self.get_async_httpx_client().__aenter__()
|
||||
return self
|
||||
|
||||
async def __aexit__(self, *args: Any, **kwargs: Any) -> None:
|
||||
"""Exit a context manager for underlying httpx.AsyncClient (see httpx docs)"""
|
||||
await self.get_async_httpx_client().__aexit__(*args, **kwargs)
|
||||
|
||||
|
||||
@define
|
||||
class AuthenticatedClient:
|
||||
"""A Client which has been authenticated for use on secured endpoints
|
||||
|
||||
The following are accepted as keyword arguments and will be used to construct httpx Clients internally:
|
||||
|
||||
``base_url``: The base URL for the API, all requests are made to a relative path to this URL
|
||||
|
||||
``cookies``: A dictionary of cookies to be sent with every request
|
||||
|
||||
``headers``: A dictionary of headers to be sent with every request
|
||||
|
||||
``timeout``: The maximum amount of a time a request can take. API functions will raise
|
||||
httpx.TimeoutException if this is exceeded.
|
||||
|
||||
``verify_ssl``: Whether or not to verify the SSL certificate of the API server. This should be True in production,
|
||||
but can be set to False for testing purposes.
|
||||
|
||||
``follow_redirects``: Whether or not to follow redirects. Default value is False.
|
||||
|
||||
``httpx_args``: A dictionary of additional arguments to be passed to the ``httpx.Client`` and ``httpx.AsyncClient`` constructor.
|
||||
|
||||
|
||||
Attributes:
|
||||
raise_on_unexpected_status: Whether or not to raise an errors.UnexpectedStatus if the API returns a
|
||||
status code that was not documented in the source OpenAPI document. Can also be provided as a keyword
|
||||
argument to the constructor.
|
||||
token: The token to use for authentication
|
||||
prefix: The prefix to use for the Authorization header
|
||||
auth_header_name: The name of the Authorization header
|
||||
"""
|
||||
|
||||
raise_on_unexpected_status: bool = field(default=False, kw_only=True)
|
||||
_base_url: str = field(alias="base_url")
|
||||
_cookies: Dict[str, str] = field(factory=dict, kw_only=True, alias="cookies")
|
||||
_headers: Dict[str, str] = field(factory=dict, kw_only=True, alias="headers")
|
||||
_timeout: Optional[httpx.Timeout] = field(default=None, kw_only=True, alias="timeout")
|
||||
_verify_ssl: Union[str, bool, ssl.SSLContext] = field(default=True, kw_only=True, alias="verify_ssl")
|
||||
_follow_redirects: bool = field(default=False, kw_only=True, alias="follow_redirects")
|
||||
_httpx_args: Dict[str, Any] = field(factory=dict, kw_only=True, alias="httpx_args")
|
||||
_client: Optional[httpx.Client] = field(default=None, init=False)
|
||||
_async_client: Optional[httpx.AsyncClient] = field(default=None, init=False)
|
||||
|
||||
token: str
|
||||
prefix: str = "Bearer"
|
||||
auth_header_name: str = "Authorization"
|
||||
|
||||
def with_headers(self, headers: Dict[str, str]) -> "AuthenticatedClient":
|
||||
"""Get a new client matching this one with additional headers"""
|
||||
if self._client is not None:
|
||||
self._client.headers.update(headers)
|
||||
if self._async_client is not None:
|
||||
self._async_client.headers.update(headers)
|
||||
return evolve(self, headers={**self._headers, **headers})
|
||||
|
||||
def with_cookies(self, cookies: Dict[str, str]) -> "AuthenticatedClient":
|
||||
"""Get a new client matching this one with additional cookies"""
|
||||
if self._client is not None:
|
||||
self._client.cookies.update(cookies)
|
||||
if self._async_client is not None:
|
||||
self._async_client.cookies.update(cookies)
|
||||
return evolve(self, cookies={**self._cookies, **cookies})
|
||||
|
||||
def with_timeout(self, timeout: httpx.Timeout) -> "AuthenticatedClient":
|
||||
"""Get a new client matching this one with a new timeout (in seconds)"""
|
||||
if self._client is not None:
|
||||
self._client.timeout = timeout
|
||||
if self._async_client is not None:
|
||||
self._async_client.timeout = timeout
|
||||
return evolve(self, timeout=timeout)
|
||||
|
||||
def set_httpx_client(self, client: httpx.Client) -> "AuthenticatedClient":
|
||||
"""Manually the underlying httpx.Client
|
||||
|
||||
**NOTE**: This will override any other settings on the client, including cookies, headers, and timeout.
|
||||
"""
|
||||
self._client = client
|
||||
return self
|
||||
|
||||
def get_httpx_client(self) -> httpx.Client:
|
||||
"""Get the underlying httpx.Client, constructing a new one if not previously set"""
|
||||
if self._client is None:
|
||||
self._headers[self.auth_header_name] = f"{self.prefix} {self.token}" if self.prefix else self.token
|
||||
self._client = httpx.Client(
|
||||
base_url=self._base_url,
|
||||
cookies=self._cookies,
|
||||
headers=self._headers,
|
||||
timeout=self._timeout,
|
||||
verify=self._verify_ssl,
|
||||
follow_redirects=self._follow_redirects,
|
||||
**self._httpx_args,
|
||||
)
|
||||
return self._client
|
||||
|
||||
def __enter__(self) -> "AuthenticatedClient":
|
||||
"""Enter a context manager for self.client—you cannot enter twice (see httpx docs)"""
|
||||
self.get_httpx_client().__enter__()
|
||||
return self
|
||||
|
||||
def __exit__(self, *args: Any, **kwargs: Any) -> None:
|
||||
"""Exit a context manager for internal httpx.Client (see httpx docs)"""
|
||||
self.get_httpx_client().__exit__(*args, **kwargs)
|
||||
|
||||
def set_async_httpx_client(self, async_client: httpx.AsyncClient) -> "AuthenticatedClient":
|
||||
"""Manually the underlying httpx.AsyncClient
|
||||
|
||||
**NOTE**: This will override any other settings on the client, including cookies, headers, and timeout.
|
||||
"""
|
||||
self._async_client = async_client
|
||||
return self
|
||||
|
||||
def get_async_httpx_client(self) -> httpx.AsyncClient:
|
||||
"""Get the underlying httpx.AsyncClient, constructing a new one if not previously set"""
|
||||
if self._async_client is None:
|
||||
self._headers[self.auth_header_name] = f"{self.prefix} {self.token}" if self.prefix else self.token
|
||||
self._async_client = httpx.AsyncClient(
|
||||
base_url=self._base_url,
|
||||
cookies=self._cookies,
|
||||
headers=self._headers,
|
||||
timeout=self._timeout,
|
||||
verify=self._verify_ssl,
|
||||
follow_redirects=self._follow_redirects,
|
||||
**self._httpx_args,
|
||||
)
|
||||
return self._async_client
|
||||
|
||||
async def __aenter__(self) -> "AuthenticatedClient":
|
||||
"""Enter a context manager for underlying httpx.AsyncClient—you cannot enter twice (see httpx docs)"""
|
||||
await self.get_async_httpx_client().__aenter__()
|
||||
return self
|
||||
|
||||
async def __aexit__(self, *args: Any, **kwargs: Any) -> None:
|
||||
"""Exit a context manager for underlying httpx.AsyncClient (see httpx docs)"""
|
||||
await self.get_async_httpx_client().__aexit__(*args, **kwargs)
|
||||
|
||||
```
|
||||
|
||||
Here is our attempt at using this client in ipython:
|
||||
|
||||
```python
|
||||
import json
|
||||
import os
|
||||
import logging
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Optional, Dict, Any
|
||||
|
||||
from clients.beatleader import client as beatleader_client
|
||||
from clients.beatleader.api.player_scores import player_scores_get_compact_scores
|
||||
from clients.beatleader.models.score_response_with_my_score_response_with_metadata import ScoreResponseWithMyScoreResponseWithMetadata
|
||||
|
||||
logging.basicConfig(
|
||||
format='%(asctime)s %(levelname)s: %(message)s',
|
||||
datefmt='%Y-%m-%d %H:%M:%S',
|
||||
level=logging.DEBUG
|
||||
)
|
||||
|
||||
player_id = '76561199407393962'
|
||||
|
||||
BASE_URL = "https://api.beatleader.xyz"
|
||||
client = beatleader_client.Client(base_url=BASE_URL)
|
||||
|
||||
response: ScoreResponseWithMyScoreResponseWithMetadata = player_scores_get_compact_scores.sync(
|
||||
client=client,
|
||||
id=player_id)
|
||||
```
|
||||
|
||||
And the result:
|
||||
|
||||
```
|
||||
---------------------------------------------------------------------------
|
||||
AttributeError Traceback (most recent call last)
|
||||
Cell In[1], line 21
|
||||
17 player_id = '76561199407393962'
|
||||
19 BASE_URL = "https://api.beatleader.xyz"
|
||||
---> 21 response: ScoreResponseWithMyScoreResponseWithMetadata = player_scores_get_compact_scores.sync_detailed(
|
||||
22 client=beatleader_client,
|
||||
23 id=player_id)
|
||||
|
||||
File ~/ops/beatsaber/playlist-tool/src/clients/beatleader/api/player_scores/player_scores_get_compact_scores.py:216, in sync_detailed(id, client, sort_by, order, page, count, search, diff, mode, requirements, score_status, leaderboard_context, type, modifiers, stars_from, stars_to, time_from, time_to, event_id)
|
||||
162 """Retrieve player's scores in a compact form
|
||||
163
|
||||
164 Fetches a paginated list of scores for a specified player ID. Returns less info to save bandwith or
|
||||
(...)
|
||||
192 Response[Union[Any, CompactScoreResponseResponseWithMetadata]]
|
||||
193 """
|
||||
195 kwargs = _get_kwargs(
|
||||
196 id=id,
|
||||
197 sort_by=sort_by,
|
||||
(...)
|
||||
213 event_id=event_id,
|
||||
214 )
|
||||
--> 216 response = client.get_httpx_client().request(
|
||||
217 **kwargs,
|
||||
218 )
|
||||
220 return _build_response(client=client, response=response)
|
||||
|
||||
AttributeError: module 'clients.beatleader.client' has no attribute 'get_httpx_client'
|
||||
|
||||
```
|
216
docs/prompts/01-template.md
Normal file
216
docs/prompts/01-template.md
Normal file
@ -0,0 +1,216 @@
|
||||
# Python coding
|
||||
|
||||
We used openapi-python-client to generate client libraries for the beatleader.xyz api. It's in clients/beatleader in our python project:
|
||||
|
||||
```
|
||||
.
|
||||
├── docs/
|
||||
│ ├── prompts/
|
||||
│ └── *.md
|
||||
├── src/
|
||||
│ ├── clients/
|
||||
│ │ ├── beatleader/
|
||||
│ │ │ ├── api/ (various API endpoints)
|
||||
│ │ │ ├── models/ (data models)
|
||||
│ │ │ └── client.py, errors.py, __init__.py, types.py
|
||||
│ │ ├── beatsaver/ (similar structure to beatleader)
|
||||
│ │ └── scoresaber/ (similar structure to beatleader)
|
||||
│ ├── helpers/
|
||||
│ │ └── *.py
|
||||
│ └── saberlist/
|
||||
│ └── *.py
|
||||
├── tests/
|
||||
│ ├── assets/
|
||||
│ └── playlist_builder.py
|
||||
├── pyproject.toml
|
||||
└── README.md
|
||||
```
|
||||
|
||||
Here's the clients/beatleader dir:
|
||||
|
||||
```
|
||||
treegit src/clients/beatleader/
|
||||
src/clients/beatleader/
|
||||
├── api
|
||||
│ ├── beast_saber
|
||||
│ │ ├── beast_saber_get_all.py
|
||||
│ │ ├── beast_saber_nominate.py
|
||||
│ │ └── __init__.py
|
||||
│ ├── clan
|
||||
│ │ ├── clan_get_all.py
|
||||
│ │ ├── clan_get_clan_by_id.py
|
||||
│ │ ├── clan_get_clan.py
|
||||
│ │ ├── clan_get_clan_with_maps_by_id.py
|
||||
│ │ ├── clan_get_clan_with_maps.py
|
||||
│ │ ├── clan_get_history.py
|
||||
│ │ ├── clan_global_map.py
|
||||
│ │ └── __init__.py
|
||||
│ ├── leaderboard
|
||||
│ │ ├── __init__.py
|
||||
│ │ ├── leaderboard_get_all.py
|
||||
│ │ ├── leaderboard_get_clan_rankings.py
|
||||
│ │ ├── leaderboard_get.py
|
||||
│ │ └── leaderboard_get_scoregraph.py
|
||||
│ ├── modifiers
|
||||
│ │ ├── __init__.py
|
||||
│ │ └── modifiers_get_modifiers.py
|
||||
│ ├── patreon
|
||||
│ │ ├── __init__.py
|
||||
│ │ └── patreon_refresh_my_patreon.py
|
||||
│ ├── player
|
||||
│ │ ├── __init__.py
|
||||
│ │ ├── player_get_beat_saver.py
|
||||
│ │ ├── player_get_discord.py
|
||||
│ │ ├── player_get_followers_info.py
|
||||
│ │ ├── player_get_followers.py
|
||||
│ │ ├── player_get_founded_clan.py
|
||||
│ │ ├── player_get_participating_events.py
|
||||
│ │ ├── player_get_patreon.py
|
||||
│ │ ├── player_get_players.py
|
||||
│ │ ├── player_get.py
|
||||
│ │ └── player_get_ranked_maps.py
|
||||
│ ├── player_scores
|
||||
│ │ ├── __init__.py
|
||||
│ │ ├── player_scores_acc_graph.py
|
||||
│ │ ├── player_scores_get_compact_history.py
|
||||
│ │ ├── player_scores_get_compact_scores.py
|
||||
│ │ ├── player_scores_get_history.py
|
||||
│ │ ├── player_scores_get_pinned_scores.py
|
||||
│ │ ├── player_scores_get_scores.py
|
||||
│ │ └── player_scores_get_score_value.py
|
||||
│ ├── song
|
||||
│ │ ├── __init__.py
|
||||
│ │ └── song_get_all.py
|
||||
│ └── __init__.py
|
||||
├── models
|
||||
│ ├── __pycache__
|
||||
│ ├── achievement_description.py
|
||||
│ ├── achievement_level.py
|
||||
│ ├── achievement.py
|
||||
│ ├── badge.py
|
||||
│ ├── ban.py
|
||||
│ ├── beasties_nomination.py
|
||||
│ ├── besties_nomination_response.py
|
||||
│ ├── clan_bigger_response.py
|
||||
│ ├── clan_global_map_point.py
|
||||
│ ├── clan_global_map.py
|
||||
│ ├── clan_map_connection.py
|
||||
│ ├── clan_maps_sort_by.py
|
||||
│ ├── clan_point.py
|
||||
│ ├── clan.py
|
||||
│ ├── clan_ranking_response_clan_response_full_response_with_metadata_and_container.py
|
||||
│ ├── clan_ranking_response.py
|
||||
│ ├── clan_response_full.py
|
||||
│ ├── clan_response_full_response_with_metadata.py
|
||||
│ ├── clan_response.py
|
||||
│ ├── clan_sort_by.py
|
||||
│ ├── compact_leaderboard.py
|
||||
│ ├── compact_leaderboard_response.py
|
||||
│ ├── compact_score.py
|
||||
│ ├── compact_score_response.py
|
||||
│ ├── compact_score_response_response_with_metadata.py
|
||||
│ ├── compact_song_response.py
|
||||
│ ├── controller_enum.py
|
||||
│ ├── criteria_commentary.py
|
||||
│ ├── difficulty_description.py
|
||||
│ ├── difficulty_response.py
|
||||
│ ├── difficulty_status.py
|
||||
│ ├── event_player.py
|
||||
│ ├── event_ranking.py
|
||||
│ ├── external_status.py
|
||||
│ ├── featured_playlist.py
|
||||
│ ├── featured_playlist_response.py
|
||||
│ ├── follower_type.py
|
||||
│ ├── global_map_history.py
|
||||
│ ├── history_compact_response.py
|
||||
│ ├── hmd.py
|
||||
│ ├── info_to_highlight.py
|
||||
│ ├── __init__.py
|
||||
│ ├── leaderboard_change.py
|
||||
│ ├── leaderboard_clan_ranking_response.py
|
||||
│ ├── leaderboard_contexts.py
|
||||
│ ├── leaderboard_group_entry.py
|
||||
│ ├── leaderboard_info_response.py
|
||||
│ ├── leaderboard_info_response_response_with_metadata.py
|
||||
│ ├── leaderboard.py
|
||||
│ ├── leaderboard_response.py
|
||||
│ ├── leaderboard_sort_by.py
|
||||
│ ├── legacy_modifiers.py
|
||||
│ ├── link_response.py
|
||||
│ ├── map_diff_response.py
|
||||
│ ├── map_info_response.py
|
||||
│ ├── map_info_response_response_with_metadata.py
|
||||
│ ├── mapper.py
|
||||
│ ├── mapper_response.py
|
||||
│ ├── map_quality.py
|
||||
│ ├── map_sort_by.py
|
||||
│ ├── maps_type.py
|
||||
│ ├── metadata.py
|
||||
│ ├── modifiers_map.py
|
||||
│ ├── modifiers_rating.py
|
||||
│ ├── my_type.py
|
||||
│ ├── operation.py
|
||||
│ ├── order.py
|
||||
│ ├── participating_event_response.py
|
||||
│ ├── patreon_features.py
|
||||
│ ├── player_change.py
|
||||
│ ├── player_context_extension.py
|
||||
│ ├── player_follower.py
|
||||
│ ├── player_followers_info_response.py
|
||||
│ ├── player.py
|
||||
│ ├── player_response_clan_response_full_response_with_metadata_and_container.py
|
||||
│ ├── player_response_full.py
|
||||
│ ├── player_response.py
|
||||
│ ├── player_response_with_stats.py
|
||||
│ ├── player_response_with_stats_response_with_metadata.py
|
||||
│ ├── player_score_stats_history.py
|
||||
│ ├── player_score_stats.py
|
||||
│ ├── player_search.py
|
||||
│ ├── player_social.py
|
||||
│ ├── player_sort_by.py
|
||||
│ ├── pp_type.py
|
||||
│ ├── profile_settings.py
|
||||
│ ├── qualification_change.py
|
||||
│ ├── qualification_commentary.py
|
||||
│ ├── qualification_vote.py
|
||||
│ ├── ranked_mapper_response.py
|
||||
│ ├── ranked_map.py
|
||||
│ ├── rank_qualification.py
|
||||
│ ├── rank_update_change.py
|
||||
│ ├── rank_update.py
|
||||
│ ├── rank_voting.py
|
||||
│ ├── replay_offsets.py
|
||||
│ ├── requirements.py
|
||||
│ ├── score_filter_status.py
|
||||
│ ├── score_graph_entry.py
|
||||
│ ├── score_improvement.py
|
||||
│ ├── score_metadata.py
|
||||
│ ├── score_response.py
|
||||
│ ├── score_response_with_acc.py
|
||||
│ ├── score_response_with_my_score.py
|
||||
│ ├── score_response_with_my_score_response_with_metadata.py
|
||||
│ ├── scores_sort_by.py
|
||||
│ ├── song.py
|
||||
│ ├── song_response.py
|
||||
│ ├── song_status.py
|
||||
│ ├── type.py
|
||||
│ └── voter_feedback.py
|
||||
├── __pycache__
|
||||
├── client.py
|
||||
├── errors.py
|
||||
├── __init__.py
|
||||
├── py.typed
|
||||
└── types.py
|
||||
|
||||
13 directories, 158 files
|
||||
```
|
||||
|
||||
Here's the contents of ``:
|
||||
|
||||
```python
|
||||
```
|
||||
|
||||
Here our attempt at using this client in ipython:
|
||||
|
||||
```python
|
||||
```
|
File diff suppressed because one or more lines are too long
180
docs/prompts/02-scratchpad.md
Normal file
180
docs/prompts/02-scratchpad.md
Normal file
File diff suppressed because one or more lines are too long
599
docs/prompts/03-scratchpad.md
Normal file
599
docs/prompts/03-scratchpad.md
Normal file
@ -0,0 +1,599 @@
|
||||
# New Python Class
|
||||
|
||||
We are working on this new Python class:
|
||||
|
||||
|
||||
```python
|
||||
import json
|
||||
import os
|
||||
import logging
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Optional, Dict, Any
|
||||
|
||||
from clients.beatleader import client as beatleader_client
|
||||
from clients.beatleader.api.player_scores import player_scores_get_compact_scores
|
||||
from clients.beatleader.models.compact_score_response_response_with_metadata import CompactScoreResponseResponseWithMetadata
|
||||
|
||||
logging.basicConfig(
|
||||
format='%(asctime)s %(levelname)s: %(message)s',
|
||||
datefmt='%Y-%m-%d %H:%M:%S',
|
||||
level=logging.DEBUG
|
||||
)
|
||||
|
||||
class BeatLeaderAPI:
|
||||
BASE_URL = "https://api.beatleader.xyz"
|
||||
|
||||
def __init__(self, cache_expiry_days: int = 1, cache_dir: Optional[str] = None):
|
||||
self.client = beatleader_client.Client(base_url=self.BASE_URL)
|
||||
self.cache_expiry_days = cache_expiry_days
|
||||
self.CACHE_DIR = cache_dir or self._determine_cache_dir()
|
||||
if not os.path.exists(self.CACHE_DIR):
|
||||
os.makedirs(self.CACHE_DIR)
|
||||
logging.info(f"Created cache directory: {self.CACHE_DIR}")
|
||||
|
||||
def _determine_cache_dir(self) -> str:
|
||||
home_cache = os.path.expanduser("~/.cache")
|
||||
beatleader_cache = os.path.join(home_cache, "beatleader")
|
||||
|
||||
if os.path.exists(home_cache):
|
||||
if not os.path.exists(beatleader_cache):
|
||||
try:
|
||||
os.makedirs(beatleader_cache)
|
||||
logging.info(f"Created cache directory: {beatleader_cache}")
|
||||
except OSError as e:
|
||||
logging.warning(f"Failed to create {beatleader_cache}: {e}")
|
||||
return os.path.join(os.getcwd(), ".cache")
|
||||
return beatleader_cache
|
||||
else:
|
||||
logging.info("~/.cache doesn't exist, using local .cache directory")
|
||||
return os.path.join(os.getcwd(), ".cache")
|
||||
|
||||
def _get_cache_filename(self, player_id: str) -> str:
|
||||
return os.path.join(self.CACHE_DIR, f"player_{player_id}_scores.json")
|
||||
|
||||
def _is_cache_valid(self, cache_file: str) -> bool:
|
||||
if not os.path.exists(cache_file):
|
||||
return False
|
||||
file_modified_time = datetime.fromtimestamp(os.path.getmtime(cache_file))
|
||||
return datetime.now() - file_modified_time < timedelta(days=self.cache_expiry_days)
|
||||
|
||||
def get_player_scores(
|
||||
self,
|
||||
player_id: str,
|
||||
use_cache: bool = True,
|
||||
count: int = 100,
|
||||
sort: str = "recent",
|
||||
max_pages: Optional[int] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Fetches all player scores for a given player ID, handling pagination and caching.
|
||||
|
||||
:param player_id: The ScoreSaber player ID.
|
||||
:param use_cache: Whether to use cached data if available.
|
||||
:param limit: Number of scores per page.
|
||||
:param sort: Sorting criteria.
|
||||
:param max_pages: Maximum number of pages to fetch. Fetch all if None.
|
||||
:return: A dictionary containing metadata and a list of player scores.
|
||||
"""
|
||||
cache_file = self._get_cache_filename(player_id)
|
||||
|
||||
if use_cache and self._is_cache_valid(cache_file):
|
||||
logging.debug(f"Using cached data for player {player_id}")
|
||||
with open(cache_file, 'r') as f:
|
||||
return json.load(f)
|
||||
|
||||
logging.debug(f"Fetching fresh data for player {player_id}")
|
||||
|
||||
all_scores = []
|
||||
page = 1
|
||||
total_items = None
|
||||
|
||||
while max_pages is None or page <= max_pages:
|
||||
try:
|
||||
response: CompactScoreResponseResponseWithMetadata = player_scores_get_compact_scores.sync(
|
||||
client=self.client,
|
||||
id=player_id,
|
||||
page=page,
|
||||
count=count,
|
||||
sort=sort
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"Error fetching page {page} for player {player_id}: {e}")
|
||||
return {"metadata": {}, "playerScores": []}
|
||||
|
||||
all_scores.extend(response.data)
|
||||
|
||||
if total_items is None:
|
||||
total_items = response.metadata.total
|
||||
logging.debug(f"Total scores to fetch: {total_items}")
|
||||
|
||||
logging.debug(f"Fetched page {page}: {len(response.data)} scores")
|
||||
|
||||
if len(all_scores) >= total_items:
|
||||
break
|
||||
|
||||
page += 1
|
||||
|
||||
result = {
|
||||
'metadata': {
|
||||
'itemsPerPage': response.metadata.items_per_page,
|
||||
'page': response.metadata.page,
|
||||
'total': response.metadata.total
|
||||
},
|
||||
'playerScores': all_scores
|
||||
}
|
||||
|
||||
with open(cache_file, 'w') as f:
|
||||
json.dump(result, f, default=str) # default=str to handle datetime serialization
|
||||
|
||||
logging.info(f"Cached scores for player {player_id} at {cache_file}")
|
||||
|
||||
return result
|
||||
```
|
||||
|
||||
Here is `src/clients/beatleader/api/player_scores/player_scores_get_compact_scores.py`:
|
||||
|
||||
```python
|
||||
from http import HTTPStatus
|
||||
from typing import Any, Dict, Optional, Union, cast
|
||||
|
||||
import httpx
|
||||
|
||||
from ... import errors
|
||||
from ...client import AuthenticatedClient, Client
|
||||
from ...models.compact_score_response_response_with_metadata import CompactScoreResponseResponseWithMetadata
|
||||
from ...models.difficulty_status import DifficultyStatus
|
||||
from ...models.leaderboard_contexts import LeaderboardContexts
|
||||
from ...models.order import Order
|
||||
from ...models.requirements import Requirements
|
||||
from ...models.score_filter_status import ScoreFilterStatus
|
||||
from ...models.scores_sort_by import ScoresSortBy
|
||||
from ...types import UNSET, Response, Unset
|
||||
|
||||
|
||||
def _get_kwargs(
|
||||
id: str,
|
||||
*,
|
||||
sort_by: Union[Unset, ScoresSortBy] = UNSET,
|
||||
order: Union[Unset, Order] = UNSET,
|
||||
page: Union[Unset, int] = 1,
|
||||
count: Union[Unset, int] = 8,
|
||||
search: Union[Unset, str] = UNSET,
|
||||
diff: Union[Unset, str] = UNSET,
|
||||
mode: Union[Unset, str] = UNSET,
|
||||
requirements: Union[Unset, Requirements] = UNSET,
|
||||
score_status: Union[Unset, ScoreFilterStatus] = UNSET,
|
||||
leaderboard_context: Union[Unset, LeaderboardContexts] = UNSET,
|
||||
type: Union[Unset, DifficultyStatus] = UNSET,
|
||||
modifiers: Union[Unset, str] = UNSET,
|
||||
stars_from: Union[Unset, float] = UNSET,
|
||||
stars_to: Union[Unset, float] = UNSET,
|
||||
time_from: Union[Unset, int] = UNSET,
|
||||
time_to: Union[Unset, int] = UNSET,
|
||||
event_id: Union[Unset, int] = UNSET,
|
||||
) -> Dict[str, Any]:
|
||||
params: Dict[str, Any] = {}
|
||||
|
||||
json_sort_by: Union[Unset, str] = UNSET
|
||||
if not isinstance(sort_by, Unset):
|
||||
json_sort_by = sort_by.value
|
||||
|
||||
params["sortBy"] = json_sort_by
|
||||
|
||||
json_order: Union[Unset, str] = UNSET
|
||||
if not isinstance(order, Unset):
|
||||
json_order = order.value
|
||||
|
||||
params["order"] = json_order
|
||||
|
||||
params["page"] = page
|
||||
|
||||
params["count"] = count
|
||||
|
||||
params["search"] = search
|
||||
|
||||
params["diff"] = diff
|
||||
|
||||
params["mode"] = mode
|
||||
|
||||
json_requirements: Union[Unset, str] = UNSET
|
||||
if not isinstance(requirements, Unset):
|
||||
json_requirements = requirements.value
|
||||
|
||||
params["requirements"] = json_requirements
|
||||
|
||||
json_score_status: Union[Unset, str] = UNSET
|
||||
if not isinstance(score_status, Unset):
|
||||
json_score_status = score_status.value
|
||||
|
||||
params["scoreStatus"] = json_score_status
|
||||
|
||||
json_leaderboard_context: Union[Unset, str] = UNSET
|
||||
if not isinstance(leaderboard_context, Unset):
|
||||
json_leaderboard_context = leaderboard_context.value
|
||||
|
||||
params["leaderboardContext"] = json_leaderboard_context
|
||||
|
||||
json_type: Union[Unset, str] = UNSET
|
||||
if not isinstance(type, Unset):
|
||||
json_type = type.value
|
||||
|
||||
params["type"] = json_type
|
||||
|
||||
params["modifiers"] = modifiers
|
||||
|
||||
params["stars_from"] = stars_from
|
||||
|
||||
params["stars_to"] = stars_to
|
||||
|
||||
params["time_from"] = time_from
|
||||
|
||||
params["time_to"] = time_to
|
||||
|
||||
params["eventId"] = event_id
|
||||
|
||||
params = {k: v for k, v in params.items() if v is not UNSET and v is not None}
|
||||
|
||||
_kwargs: Dict[str, Any] = {
|
||||
"method": "get",
|
||||
"url": f"/player/{id}/scores/compact",
|
||||
"params": params,
|
||||
}
|
||||
|
||||
return _kwargs
|
||||
|
||||
|
||||
def _parse_response(
|
||||
*, client: Union[AuthenticatedClient, Client], response: httpx.Response
|
||||
) -> Optional[Union[Any, CompactScoreResponseResponseWithMetadata]]:
|
||||
if response.status_code == HTTPStatus.OK:
|
||||
response_200 = CompactScoreResponseResponseWithMetadata.from_dict(response.json())
|
||||
|
||||
return response_200
|
||||
if response.status_code == HTTPStatus.BAD_REQUEST:
|
||||
response_400 = cast(Any, None)
|
||||
return response_400
|
||||
if response.status_code == HTTPStatus.NOT_FOUND:
|
||||
response_404 = cast(Any, None)
|
||||
return response_404
|
||||
if client.raise_on_unexpected_status:
|
||||
raise errors.UnexpectedStatus(response.status_code, response.content)
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def _build_response(
|
||||
*, client: Union[AuthenticatedClient, Client], response: httpx.Response
|
||||
) -> Response[Union[Any, CompactScoreResponseResponseWithMetadata]]:
|
||||
return Response(
|
||||
status_code=HTTPStatus(response.status_code),
|
||||
content=response.content,
|
||||
headers=response.headers,
|
||||
parsed=_parse_response(client=client, response=response),
|
||||
)
|
||||
|
||||
|
||||
def sync_detailed(
|
||||
id: str,
|
||||
*,
|
||||
client: Union[AuthenticatedClient, Client],
|
||||
sort_by: Union[Unset, ScoresSortBy] = UNSET,
|
||||
order: Union[Unset, Order] = UNSET,
|
||||
page: Union[Unset, int] = 1,
|
||||
count: Union[Unset, int] = 8,
|
||||
search: Union[Unset, str] = UNSET,
|
||||
diff: Union[Unset, str] = UNSET,
|
||||
mode: Union[Unset, str] = UNSET,
|
||||
requirements: Union[Unset, Requirements] = UNSET,
|
||||
score_status: Union[Unset, ScoreFilterStatus] = UNSET,
|
||||
leaderboard_context: Union[Unset, LeaderboardContexts] = UNSET,
|
||||
type: Union[Unset, DifficultyStatus] = UNSET,
|
||||
modifiers: Union[Unset, str] = UNSET,
|
||||
stars_from: Union[Unset, float] = UNSET,
|
||||
stars_to: Union[Unset, float] = UNSET,
|
||||
time_from: Union[Unset, int] = UNSET,
|
||||
time_to: Union[Unset, int] = UNSET,
|
||||
event_id: Union[Unset, int] = UNSET,
|
||||
) -> Response[Union[Any, CompactScoreResponseResponseWithMetadata]]:
|
||||
"""Retrieve player's scores in a compact form
|
||||
|
||||
Fetches a paginated list of scores for a specified player ID. Returns less info to save bandwith or
|
||||
processing time
|
||||
|
||||
Args:
|
||||
id (str):
|
||||
sort_by (Union[Unset, ScoresSortBy]):
|
||||
order (Union[Unset, Order]): Represents the order in which values will be sorted.
|
||||
page (Union[Unset, int]): Default: 1.
|
||||
count (Union[Unset, int]): Default: 8.
|
||||
search (Union[Unset, str]):
|
||||
diff (Union[Unset, str]):
|
||||
mode (Union[Unset, str]):
|
||||
requirements (Union[Unset, Requirements]):
|
||||
score_status (Union[Unset, ScoreFilterStatus]):
|
||||
leaderboard_context (Union[Unset, LeaderboardContexts]):
|
||||
type (Union[Unset, DifficultyStatus]): Represents the difficulty status of a map.
|
||||
modifiers (Union[Unset, str]):
|
||||
stars_from (Union[Unset, float]):
|
||||
stars_to (Union[Unset, float]):
|
||||
time_from (Union[Unset, int]):
|
||||
time_to (Union[Unset, int]):
|
||||
event_id (Union[Unset, int]):
|
||||
|
||||
Raises:
|
||||
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
|
||||
httpx.TimeoutException: If the request takes longer than Client.timeout.
|
||||
|
||||
Returns:
|
||||
Response[Union[Any, CompactScoreResponseResponseWithMetadata]]
|
||||
"""
|
||||
|
||||
kwargs = _get_kwargs(
|
||||
id=id,
|
||||
sort_by=sort_by,
|
||||
order=order,
|
||||
page=page,
|
||||
count=count,
|
||||
search=search,
|
||||
diff=diff,
|
||||
mode=mode,
|
||||
requirements=requirements,
|
||||
score_status=score_status,
|
||||
leaderboard_context=leaderboard_context,
|
||||
type=type,
|
||||
modifiers=modifiers,
|
||||
stars_from=stars_from,
|
||||
stars_to=stars_to,
|
||||
time_from=time_from,
|
||||
time_to=time_to,
|
||||
event_id=event_id,
|
||||
)
|
||||
|
||||
response = client.get_httpx_client().request(
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
return _build_response(client=client, response=response)
|
||||
|
||||
|
||||
def sync(
|
||||
id: str,
|
||||
*,
|
||||
client: Union[AuthenticatedClient, Client],
|
||||
sort_by: Union[Unset, ScoresSortBy] = UNSET,
|
||||
order: Union[Unset, Order] = UNSET,
|
||||
page: Union[Unset, int] = 1,
|
||||
count: Union[Unset, int] = 8,
|
||||
search: Union[Unset, str] = UNSET,
|
||||
diff: Union[Unset, str] = UNSET,
|
||||
mode: Union[Unset, str] = UNSET,
|
||||
requirements: Union[Unset, Requirements] = UNSET,
|
||||
score_status: Union[Unset, ScoreFilterStatus] = UNSET,
|
||||
leaderboard_context: Union[Unset, LeaderboardContexts] = UNSET,
|
||||
type: Union[Unset, DifficultyStatus] = UNSET,
|
||||
modifiers: Union[Unset, str] = UNSET,
|
||||
stars_from: Union[Unset, float] = UNSET,
|
||||
stars_to: Union[Unset, float] = UNSET,
|
||||
time_from: Union[Unset, int] = UNSET,
|
||||
time_to: Union[Unset, int] = UNSET,
|
||||
event_id: Union[Unset, int] = UNSET,
|
||||
) -> Optional[Union[Any, CompactScoreResponseResponseWithMetadata]]:
|
||||
"""Retrieve player's scores in a compact form
|
||||
|
||||
Fetches a paginated list of scores for a specified player ID. Returns less info to save bandwith or
|
||||
processing time
|
||||
|
||||
Args:
|
||||
id (str):
|
||||
sort_by (Union[Unset, ScoresSortBy]):
|
||||
order (Union[Unset, Order]): Represents the order in which values will be sorted.
|
||||
page (Union[Unset, int]): Default: 1.
|
||||
count (Union[Unset, int]): Default: 8.
|
||||
search (Union[Unset, str]):
|
||||
diff (Union[Unset, str]):
|
||||
mode (Union[Unset, str]):
|
||||
requirements (Union[Unset, Requirements]):
|
||||
score_status (Union[Unset, ScoreFilterStatus]):
|
||||
leaderboard_context (Union[Unset, LeaderboardContexts]):
|
||||
type (Union[Unset, DifficultyStatus]): Represents the difficulty status of a map.
|
||||
modifiers (Union[Unset, str]):
|
||||
stars_from (Union[Unset, float]):
|
||||
stars_to (Union[Unset, float]):
|
||||
time_from (Union[Unset, int]):
|
||||
time_to (Union[Unset, int]):
|
||||
event_id (Union[Unset, int]):
|
||||
|
||||
Raises:
|
||||
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
|
||||
httpx.TimeoutException: If the request takes longer than Client.timeout.
|
||||
|
||||
Returns:
|
||||
Union[Any, CompactScoreResponseResponseWithMetadata]
|
||||
"""
|
||||
|
||||
return sync_detailed(
|
||||
id=id,
|
||||
client=client,
|
||||
sort_by=sort_by,
|
||||
order=order,
|
||||
page=page,
|
||||
count=count,
|
||||
search=search,
|
||||
diff=diff,
|
||||
mode=mode,
|
||||
requirements=requirements,
|
||||
score_status=score_status,
|
||||
leaderboard_context=leaderboard_context,
|
||||
type=type,
|
||||
modifiers=modifiers,
|
||||
stars_from=stars_from,
|
||||
stars_to=stars_to,
|
||||
time_from=time_from,
|
||||
time_to=time_to,
|
||||
event_id=event_id,
|
||||
).parsed
|
||||
|
||||
|
||||
async def asyncio_detailed(
|
||||
id: str,
|
||||
*,
|
||||
client: Union[AuthenticatedClient, Client],
|
||||
sort_by: Union[Unset, ScoresSortBy] = UNSET,
|
||||
order: Union[Unset, Order] = UNSET,
|
||||
page: Union[Unset, int] = 1,
|
||||
count: Union[Unset, int] = 8,
|
||||
search: Union[Unset, str] = UNSET,
|
||||
diff: Union[Unset, str] = UNSET,
|
||||
mode: Union[Unset, str] = UNSET,
|
||||
requirements: Union[Unset, Requirements] = UNSET,
|
||||
score_status: Union[Unset, ScoreFilterStatus] = UNSET,
|
||||
leaderboard_context: Union[Unset, LeaderboardContexts] = UNSET,
|
||||
type: Union[Unset, DifficultyStatus] = UNSET,
|
||||
modifiers: Union[Unset, str] = UNSET,
|
||||
stars_from: Union[Unset, float] = UNSET,
|
||||
stars_to: Union[Unset, float] = UNSET,
|
||||
time_from: Union[Unset, int] = UNSET,
|
||||
time_to: Union[Unset, int] = UNSET,
|
||||
event_id: Union[Unset, int] = UNSET,
|
||||
) -> Response[Union[Any, CompactScoreResponseResponseWithMetadata]]:
|
||||
"""Retrieve player's scores in a compact form
|
||||
|
||||
Fetches a paginated list of scores for a specified player ID. Returns less info to save bandwith or
|
||||
processing time
|
||||
|
||||
Args:
|
||||
id (str):
|
||||
sort_by (Union[Unset, ScoresSortBy]):
|
||||
order (Union[Unset, Order]): Represents the order in which values will be sorted.
|
||||
page (Union[Unset, int]): Default: 1.
|
||||
count (Union[Unset, int]): Default: 8.
|
||||
search (Union[Unset, str]):
|
||||
diff (Union[Unset, str]):
|
||||
mode (Union[Unset, str]):
|
||||
requirements (Union[Unset, Requirements]):
|
||||
score_status (Union[Unset, ScoreFilterStatus]):
|
||||
leaderboard_context (Union[Unset, LeaderboardContexts]):
|
||||
type (Union[Unset, DifficultyStatus]): Represents the difficulty status of a map.
|
||||
modifiers (Union[Unset, str]):
|
||||
stars_from (Union[Unset, float]):
|
||||
stars_to (Union[Unset, float]):
|
||||
time_from (Union[Unset, int]):
|
||||
time_to (Union[Unset, int]):
|
||||
event_id (Union[Unset, int]):
|
||||
|
||||
Raises:
|
||||
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
|
||||
httpx.TimeoutException: If the request takes longer than Client.timeout.
|
||||
|
||||
Returns:
|
||||
Response[Union[Any, CompactScoreResponseResponseWithMetadata]]
|
||||
"""
|
||||
|
||||
kwargs = _get_kwargs(
|
||||
id=id,
|
||||
sort_by=sort_by,
|
||||
order=order,
|
||||
page=page,
|
||||
count=count,
|
||||
search=search,
|
||||
diff=diff,
|
||||
mode=mode,
|
||||
requirements=requirements,
|
||||
score_status=score_status,
|
||||
leaderboard_context=leaderboard_context,
|
||||
type=type,
|
||||
modifiers=modifiers,
|
||||
stars_from=stars_from,
|
||||
stars_to=stars_to,
|
||||
time_from=time_from,
|
||||
time_to=time_to,
|
||||
event_id=event_id,
|
||||
)
|
||||
|
||||
response = await client.get_async_httpx_client().request(**kwargs)
|
||||
|
||||
return _build_response(client=client, response=response)
|
||||
|
||||
|
||||
async def asyncio(
|
||||
id: str,
|
||||
*,
|
||||
client: Union[AuthenticatedClient, Client],
|
||||
sort_by: Union[Unset, ScoresSortBy] = UNSET,
|
||||
order: Union[Unset, Order] = UNSET,
|
||||
page: Union[Unset, int] = 1,
|
||||
count: Union[Unset, int] = 8,
|
||||
search: Union[Unset, str] = UNSET,
|
||||
diff: Union[Unset, str] = UNSET,
|
||||
mode: Union[Unset, str] = UNSET,
|
||||
requirements: Union[Unset, Requirements] = UNSET,
|
||||
score_status: Union[Unset, ScoreFilterStatus] = UNSET,
|
||||
leaderboard_context: Union[Unset, LeaderboardContexts] = UNSET,
|
||||
type: Union[Unset, DifficultyStatus] = UNSET,
|
||||
modifiers: Union[Unset, str] = UNSET,
|
||||
stars_from: Union[Unset, float] = UNSET,
|
||||
stars_to: Union[Unset, float] = UNSET,
|
||||
time_from: Union[Unset, int] = UNSET,
|
||||
time_to: Union[Unset, int] = UNSET,
|
||||
event_id: Union[Unset, int] = UNSET,
|
||||
) -> Optional[Union[Any, CompactScoreResponseResponseWithMetadata]]:
|
||||
"""Retrieve player's scores in a compact form
|
||||
|
||||
Fetches a paginated list of scores for a specified player ID. Returns less info to save bandwith or
|
||||
processing time
|
||||
|
||||
Args:
|
||||
id (str):
|
||||
sort_by (Union[Unset, ScoresSortBy]):
|
||||
order (Union[Unset, Order]): Represents the order in which values will be sorted.
|
||||
page (Union[Unset, int]): Default: 1.
|
||||
count (Union[Unset, int]): Default: 8.
|
||||
search (Union[Unset, str]):
|
||||
diff (Union[Unset, str]):
|
||||
mode (Union[Unset, str]):
|
||||
requirements (Union[Unset, Requirements]):
|
||||
score_status (Union[Unset, ScoreFilterStatus]):
|
||||
leaderboard_context (Union[Unset, LeaderboardContexts]):
|
||||
type (Union[Unset, DifficultyStatus]): Represents the difficulty status of a map.
|
||||
modifiers (Union[Unset, str]):
|
||||
stars_from (Union[Unset, float]):
|
||||
stars_to (Union[Unset, float]):
|
||||
time_from (Union[Unset, int]):
|
||||
time_to (Union[Unset, int]):
|
||||
event_id (Union[Unset, int]):
|
||||
|
||||
Raises:
|
||||
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
|
||||
httpx.TimeoutException: If the request takes longer than Client.timeout.
|
||||
|
||||
Returns:
|
||||
Union[Any, CompactScoreResponseResponseWithMetadata]
|
||||
"""
|
||||
|
||||
return (
|
||||
await asyncio_detailed(
|
||||
id=id,
|
||||
client=client,
|
||||
sort_by=sort_by,
|
||||
order=order,
|
||||
page=page,
|
||||
count=count,
|
||||
search=search,
|
||||
diff=diff,
|
||||
mode=mode,
|
||||
requirements=requirements,
|
||||
score_status=score_status,
|
||||
leaderboard_context=leaderboard_context,
|
||||
type=type,
|
||||
modifiers=modifiers,
|
||||
stars_from=stars_from,
|
||||
stars_to=stars_to,
|
||||
time_from=time_from,
|
||||
time_to=time_to,
|
||||
event_id=event_id,
|
||||
)
|
||||
).parsed
|
||||
```
|
||||
|
||||
Please review get_player_scores(), we wonder if the sort option is done correctly.
|
@ -39,7 +39,7 @@ Homepage = "https://git.satstack.dev/blee/beatsaber-playlist-tool"
|
||||
#replay_all_by_acc = "saberlist.scoresaber:replay_all_by_acc"
|
||||
#leaderboard_songs_by_stars = "saberlist.scoresaber:leaderboard_songs"
|
||||
#saberlist_replay_bl = "saberlist.beatleader:saberlist_replay_bl"
|
||||
settle_old_scores_ss = "saberlist.make:create_playlist"
|
||||
saberlist = "saberlist.make:saberlist"
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
pythonpath = ["src"]
|
@ -111,7 +111,7 @@ def _parse_response(
|
||||
*, client: Union[AuthenticatedClient, Client], response: httpx.Response
|
||||
) -> Optional[Union[Any, CompactScoreResponseResponseWithMetadata]]:
|
||||
if response.status_code == HTTPStatus.OK:
|
||||
response_200 = CompactScoreResponseResponseWithMetadata.from_dict(response.text)
|
||||
response_200 = CompactScoreResponseResponseWithMetadata.from_dict(response.json())
|
||||
|
||||
return response_200
|
||||
if response.status_code == HTTPStatus.BAD_REQUEST:
|
||||
|
@ -111,7 +111,7 @@ def _parse_response(
|
||||
*, client: Union[AuthenticatedClient, Client], response: httpx.Response
|
||||
) -> Optional[Union[Any, ScoreResponseWithMyScoreResponseWithMetadata]]:
|
||||
if response.status_code == HTTPStatus.OK:
|
||||
response_200 = ScoreResponseWithMyScoreResponseWithMetadata.from_dict(response.text)
|
||||
response_200 = ScoreResponseWithMyScoreResponseWithMetadata.from_dict(response.json())
|
||||
|
||||
return response_200
|
||||
if response.status_code == HTTPStatus.BAD_REQUEST:
|
||||
|
130
src/helpers/BeatLeaderAPI.py
Normal file
130
src/helpers/BeatLeaderAPI.py
Normal file
@ -0,0 +1,130 @@
|
||||
import json
|
||||
import os
|
||||
import logging
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Optional, Dict, Any
|
||||
|
||||
from clients.beatleader import client as beatleader_client
|
||||
from clients.beatleader.api.player_scores import player_scores_get_compact_scores
|
||||
from clients.beatleader.models.compact_score_response_response_with_metadata import CompactScoreResponseResponseWithMetadata
|
||||
from clients.beatleader.models.scores_sort_by import ScoresSortBy
|
||||
from clients.beatleader.models.order import Order
|
||||
|
||||
logging.basicConfig(
|
||||
format='%(asctime)s %(levelname)s: %(message)s',
|
||||
datefmt='%Y-%m-%d %H:%M:%S',
|
||||
level=logging.DEBUG
|
||||
)
|
||||
|
||||
class BeatLeaderAPI:
|
||||
BASE_URL = "https://api.beatleader.xyz"
|
||||
|
||||
def __init__(self, cache_expiry_days: int = 1, cache_dir: Optional[str] = None):
|
||||
self.client = beatleader_client.Client(base_url=self.BASE_URL)
|
||||
self.cache_expiry_days = cache_expiry_days
|
||||
self.CACHE_DIR = cache_dir or self._determine_cache_dir()
|
||||
if not os.path.exists(self.CACHE_DIR):
|
||||
os.makedirs(self.CACHE_DIR)
|
||||
logging.info(f"Created cache directory: {self.CACHE_DIR}")
|
||||
|
||||
def _determine_cache_dir(self) -> str:
|
||||
home_cache = os.path.expanduser("~/.cache")
|
||||
beatleader_cache = os.path.join(home_cache, "beatleader")
|
||||
|
||||
if os.path.exists(home_cache):
|
||||
if not os.path.exists(beatleader_cache):
|
||||
try:
|
||||
os.makedirs(beatleader_cache)
|
||||
logging.info(f"Created cache directory: {beatleader_cache}")
|
||||
except OSError as e:
|
||||
logging.warning(f"Failed to create {beatleader_cache}: {e}")
|
||||
return os.path.join(os.getcwd(), ".cache")
|
||||
return beatleader_cache
|
||||
else:
|
||||
logging.info("~/.cache doesn't exist, using local .cache directory")
|
||||
return os.path.join(os.getcwd(), ".cache")
|
||||
|
||||
def _get_cache_filename(self, player_id: str) -> str:
|
||||
return os.path.join(self.CACHE_DIR, f"player_{player_id}_scores.json")
|
||||
|
||||
def _is_cache_valid(self, cache_file: str) -> bool:
|
||||
if not os.path.exists(cache_file):
|
||||
return False
|
||||
file_modified_time = datetime.fromtimestamp(os.path.getmtime(cache_file))
|
||||
return datetime.now() - file_modified_time < timedelta(days=self.cache_expiry_days)
|
||||
|
||||
def get_player_scores(
|
||||
self,
|
||||
player_id: str,
|
||||
use_cache: bool = True,
|
||||
count: int = 100,
|
||||
sort_by: ScoresSortBy = ScoresSortBy.DATE,
|
||||
order: Order = Order.DESC,
|
||||
max_pages: Optional[int] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Fetches all player scores for a given player ID, handling pagination and caching.
|
||||
|
||||
:param player_id: The ScoreSaber player ID.
|
||||
:param use_cache: Whether to use cached data if available.
|
||||
:param limit: Number of scores per page.
|
||||
:param sort: Sorting criteria.
|
||||
:param max_pages: Maximum number of pages to fetch. Fetch all if None.
|
||||
:return: A dictionary containing metadata and a list of player scores.
|
||||
"""
|
||||
cache_file = self._get_cache_filename(player_id)
|
||||
|
||||
if use_cache and self._is_cache_valid(cache_file):
|
||||
logging.debug(f"Using cached data for player {player_id}")
|
||||
with open(cache_file, 'r') as f:
|
||||
return json.load(f)
|
||||
|
||||
logging.debug(f"Fetching fresh data for player {player_id}")
|
||||
|
||||
all_scores = []
|
||||
page = 1
|
||||
total_items = None
|
||||
|
||||
while max_pages is None or page <= max_pages:
|
||||
try:
|
||||
response: CompactScoreResponseResponseWithMetadata = player_scores_get_compact_scores.sync(
|
||||
client=self.client,
|
||||
id=player_id,
|
||||
page=page,
|
||||
count=count,
|
||||
sort_by=sort_by,
|
||||
order=order
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"Error fetching page {page} for player {player_id}: {e}")
|
||||
return {"metadata": {}, "playerScores": []}
|
||||
|
||||
all_scores.extend(response.data)
|
||||
|
||||
if total_items is None:
|
||||
total_items = response.metadata.total
|
||||
logging.debug(f"Total scores to fetch: {total_items}")
|
||||
|
||||
logging.debug(f"Fetched page {page}: {len(response.data)} scores")
|
||||
|
||||
if len(all_scores) >= total_items:
|
||||
break
|
||||
|
||||
page += 1
|
||||
|
||||
result = {
|
||||
'metadata': {
|
||||
'itemsPerPage': response.metadata.items_per_page,
|
||||
'page': response.metadata.page,
|
||||
'total': response.metadata.total
|
||||
},
|
||||
'playerScores': all_scores
|
||||
}
|
||||
|
||||
with open(cache_file, 'w') as f:
|
||||
json.dump(result, f, default=str) # default=str to handle datetime serialization
|
||||
|
||||
logging.info(f"Cached scores for player {player_id} at {cache_file}")
|
||||
|
||||
return result
|
@ -77,7 +77,6 @@ class ScoreSaberAPI:
|
||||
return json.load(f)
|
||||
|
||||
logging.debug(f"Fetching fresh data for player {player_id}")
|
||||
url_player_scores = f"/api/player/{player_id}/scores"
|
||||
|
||||
all_scores = []
|
||||
page = 1
|
||||
@ -94,7 +93,7 @@ class ScoreSaberAPI:
|
||||
)
|
||||
except Exception as e:
|
||||
logging.error(f"Error fetching page {page} for player {player_id}: {e}")
|
||||
break
|
||||
return {"metadata": {}, "playerScores": []}
|
||||
|
||||
all_scores.extend([score.dict() for score in response.player_scores])
|
||||
|
||||
|
133
src/helpers/SimpleBeatLeaderAPI.py
Normal file
133
src/helpers/SimpleBeatLeaderAPI.py
Normal file
@ -0,0 +1,133 @@
|
||||
from datetime import datetime, timedelta
|
||||
import base64
|
||||
import json
|
||||
import os
|
||||
import random
|
||||
import requests
|
||||
import time
|
||||
|
||||
import logging
|
||||
logging.basicConfig(
|
||||
format='%(asctime)s %(levelname)s: %(message)s',
|
||||
datefmt='%Y-%m-%d %H:%M:%S',
|
||||
level=logging.DEBUG
|
||||
)
|
||||
|
||||
class SimpleBeatLeaderAPI:
|
||||
BASE_URL = "https://api.beatleader.xyz"
|
||||
|
||||
def __init__(self, cache_expiry_days=1):
|
||||
self.session = requests.Session()
|
||||
self.cache_expiry_days = cache_expiry_days
|
||||
self.CACHE_DIR = self._determine_cache_dir()
|
||||
if not os.path.exists(self.CACHE_DIR):
|
||||
os.makedirs(self.CACHE_DIR)
|
||||
|
||||
def _determine_cache_dir(self):
|
||||
home_cache = os.path.expanduser("~/.cache")
|
||||
saberlist_cache = os.path.join(home_cache, "saberlist")
|
||||
|
||||
if os.path.exists(home_cache):
|
||||
if not os.path.exists(saberlist_cache):
|
||||
try:
|
||||
os.makedirs(saberlist_cache)
|
||||
logging.info(f"Created cache directory: {saberlist_cache}")
|
||||
except OSError as e:
|
||||
logging.warning(f"Failed to create {saberlist_cache}: {e}")
|
||||
return os.path.join(os.getcwd(), ".cache")
|
||||
return saberlist_cache
|
||||
else:
|
||||
logging.info("~/.cache doesn't exist, using local .cache directory")
|
||||
return os.path.join(os.getcwd(), ".cache")
|
||||
|
||||
def _get_cache_filename(self, player_id):
|
||||
return os.path.join(self.CACHE_DIR, f"player_{player_id}_scores.json")
|
||||
|
||||
def _is_cache_valid(self, cache_file):
|
||||
if not os.path.exists(cache_file):
|
||||
return False
|
||||
file_modified_time = datetime.fromtimestamp(os.path.getmtime(cache_file))
|
||||
return datetime.now() - file_modified_time < timedelta(days=self.cache_expiry_days)
|
||||
|
||||
def get_player_scores(self, player_id, use_cache=True, page_size=100, max_pages=None):
|
||||
cache_file = self._get_cache_filename(player_id)
|
||||
|
||||
if use_cache and self._is_cache_valid(cache_file):
|
||||
logging.debug(f"Using cached data for player {player_id}")
|
||||
with open(cache_file, 'r') as f:
|
||||
return json.load(f)
|
||||
|
||||
logging.debug(f"Fetching fresh data for player {player_id}")
|
||||
url = f"{self.BASE_URL}/player/{player_id}/scores"
|
||||
|
||||
all_scores = []
|
||||
page = 1
|
||||
total_items = None
|
||||
|
||||
while max_pages is None or page <= max_pages:
|
||||
params = {
|
||||
"page": page,
|
||||
"count": page_size
|
||||
}
|
||||
response = self.session.get(url, params=params)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
|
||||
all_scores.extend(data['data'])
|
||||
|
||||
if total_items is None:
|
||||
total_items = data['metadata']['total']
|
||||
|
||||
if len(all_scores) >= total_items:
|
||||
break
|
||||
|
||||
page += 1
|
||||
time.sleep(1) # Add a small delay to avoid rate limiting
|
||||
|
||||
result = {
|
||||
'metadata': {
|
||||
'total': total_items,
|
||||
'itemsPerPage': page_size,
|
||||
'page': page
|
||||
},
|
||||
'data': all_scores
|
||||
}
|
||||
|
||||
with open(cache_file, 'w') as f:
|
||||
json.dump(result, f)
|
||||
|
||||
return result
|
||||
|
||||
def clear_cache(self, player_id=None):
|
||||
if player_id:
|
||||
cache_file = self._get_cache_filename(player_id)
|
||||
if os.path.exists(cache_file):
|
||||
os.remove(cache_file)
|
||||
logging.debug(f"Cleared cache for player {player_id}")
|
||||
else:
|
||||
for file in os.listdir(self.CACHE_DIR):
|
||||
os.remove(os.path.join(self.CACHE_DIR, file))
|
||||
logging.debug("Cleared all cache")
|
||||
|
||||
def get_cache_dir(self):
|
||||
return self.CACHE_DIR
|
||||
|
||||
|
||||
def get_player_info(self, player_id):
|
||||
"""
|
||||
Retrieve information for a specific player.
|
||||
|
||||
:param player_id: ID of the player
|
||||
:return: Dictionary containing player information
|
||||
"""
|
||||
url = f"{self.BASE_URL}/player/{player_id}"
|
||||
|
||||
try:
|
||||
response = self.session.get(url)
|
||||
response.raise_for_status()
|
||||
player_data = response.json()
|
||||
|
||||
return player_data
|
||||
except requests.exceptions.RequestException as e:
|
||||
logging.error(f"Error fetching player info for ID {player_id}: {e}")
|
||||
return None
|
@ -1,112 +0,0 @@
|
||||
"""
|
||||
from collections import defaultdict
|
||||
from datetime import datetime
|
||||
from saberlist.BeatLeaderAPI import BeatLeaderAPI
|
||||
from archive.PlaylistBuilder import PlaylistBuilder
|
||||
import json
|
||||
import os
|
||||
|
||||
import logging
|
||||
logging.basicConfig(
|
||||
format='%(asctime)s %(levelname)s: %(message)s',
|
||||
datefmt='%Y-%m-%d %H:%M:%S',
|
||||
level=logging.DEBUG
|
||||
)
|
||||
|
||||
def prompt_for_player_id(default_id='76561199407393962'):
|
||||
prompt = f"Enter player ID (press Enter for default '{default_id}'): "
|
||||
user_input = input(prompt).strip()
|
||||
return user_input if user_input else default_id
|
||||
|
||||
def build_difficulty_based_playlist(api, player_id, history, playlist_name, song_count=5):
|
||||
scores_data = api.get_player_scores(player_id, use_cache=True)
|
||||
all_scores = scores_data['data']
|
||||
all_scores.sort(key=lambda x: x['timepost'])
|
||||
|
||||
difficulty_groups = defaultdict(list)
|
||||
no_stars_group = []
|
||||
for score in all_scores:
|
||||
stars = score['leaderboard']['difficulty'].get('stars')
|
||||
song_id = score['leaderboard']['song']['id']
|
||||
difficulty_name = score['leaderboard']['difficulty']['difficultyName']
|
||||
|
||||
# Check if this song:difficulty combination is in history
|
||||
if song_id not in history or difficulty_name not in history[song_id]:
|
||||
if stars is None or stars == 0:
|
||||
no_stars_group.append(score)
|
||||
elif 0 < stars <= 3:
|
||||
difficulty_groups[0].append(score)
|
||||
elif 4 <= stars < 6:
|
||||
difficulty_groups[1].append(score)
|
||||
elif 6 <= stars < 7:
|
||||
difficulty_groups[2].append(score)
|
||||
elif stars >= 7:
|
||||
difficulty_groups[3].append(score)
|
||||
|
||||
playlist_scores = []
|
||||
for difficulty, count in [(0, song_count), (1, song_count), (2, song_count), (3, song_count)]:
|
||||
unique_songs = {}
|
||||
for score in difficulty_groups[difficulty]:
|
||||
song_id = score['leaderboard']['song']['id']
|
||||
if song_id not in unique_songs or score['timepost'] < unique_songs[song_id]['timepost']:
|
||||
unique_songs[song_id] = score
|
||||
|
||||
playlist_scores.extend(sorted(unique_songs.values(), key=lambda x: x['timepost'])[:count])
|
||||
|
||||
# Update history
|
||||
for score in playlist_scores:
|
||||
song_id = score['leaderboard']['song']['id']
|
||||
difficulty_name = score['leaderboard']['difficulty']['difficultyName']
|
||||
if song_id not in history:
|
||||
history[song_id] = []
|
||||
history[song_id].append(difficulty_name)
|
||||
|
||||
# Prepare the custom playlist
|
||||
custom_playlist = []
|
||||
for score in playlist_scores:
|
||||
custom_playlist.append({
|
||||
'song': score['leaderboard']['song'],
|
||||
'difficulty': score['leaderboard']['difficulty']
|
||||
})
|
||||
|
||||
playlist_builder = PlaylistBuilder()
|
||||
playlist_file, used_cover = playlist_builder.create_player_playlist_with_random_cover(
|
||||
playlist_scores,
|
||||
playlist_name,
|
||||
"SaberList Tool",
|
||||
used_covers=set(history['used_covers'])
|
||||
)
|
||||
|
||||
if used_cover:
|
||||
history['used_covers'].append(used_cover)
|
||||
|
||||
return playlist_file, playlist_scores, used_cover
|
||||
|
||||
def saberlist_replay_bl():
|
||||
api = BeatLeaderAPI()
|
||||
player_id = prompt_for_player_id()
|
||||
player_info = api.get_player_info(player_id)
|
||||
|
||||
if player_info:
|
||||
logging.info(f"Fetching score history for player: {player_info.get('name', 'N/A')}")
|
||||
|
||||
history = load_history()
|
||||
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
playlist_name = f"star_ladder-{timestamp}"
|
||||
|
||||
playlist_file, playlist_scores, used_cover = build_difficulty_based_playlist(api, player_id, history, playlist_name, song_count=10)
|
||||
save_history(history)
|
||||
|
||||
print(f"Playlist created: {playlist_file}")
|
||||
if used_cover:
|
||||
print(f"Cover image used: {used_cover}")
|
||||
print("Playlist contents:")
|
||||
for i, score in enumerate(playlist_scores, 1):
|
||||
song = score['leaderboard']['song']
|
||||
difficulty = score['leaderboard']['difficulty']
|
||||
print(f"{i}. {song['name']} by {song['author']} (Mapper: {song['mapper']}) - {difficulty['stars']:.2f} stars - Last played: {datetime.fromtimestamp(score['timepost'])}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
saberlist_replay_bl()
|
||||
"""
|
@ -9,7 +9,7 @@ from dotenv import load_dotenv
|
||||
load_dotenv()
|
||||
LOG_LEVEL = os.environ.get('LOG_LEVEL', 'INFO').upper()
|
||||
HISTORY_FILE = os.environ.get('HISTORY_FILE', "playlist_history.json")
|
||||
CACHE_EXPIRY_DAYS = int(os.environ.get('CACHE_EXPIRY_DAYS', 2))
|
||||
CACHE_EXPIRY_DAYS = int(os.environ.get('CACHE_EXPIRY_DAYS', 7))
|
||||
|
||||
import logging
|
||||
logging.basicConfig(
|
||||
@ -20,6 +20,7 @@ logging.basicConfig(
|
||||
|
||||
from helpers.PlaylistBuilder import PlaylistBuilder
|
||||
from helpers.ScoreSaberAPI import ScoreSaberAPI
|
||||
from helpers.BeatLeaderAPI import BeatLeaderAPI
|
||||
|
||||
def load_history() -> Dict[str, Any]:
|
||||
"""
|
||||
@ -43,7 +44,7 @@ def save_history(history: Dict[str, Any]) -> None:
|
||||
|
||||
def prompt_for_player_id(default_id: str = '76561199407393962') -> str:
|
||||
"""
|
||||
Prompt the user to enter a ScoreSaber player ID.
|
||||
Prompt the user to enter a ScoreSaber or BeatLeader player ID.
|
||||
Uses a default ID if the user presses Enter without input.
|
||||
|
||||
:param default_id: The default player ID to use.
|
||||
@ -63,7 +64,7 @@ def format_time_ago(time_difference: timedelta) -> str:
|
||||
days = time_difference.days
|
||||
|
||||
if days < 7:
|
||||
return f"{days} days(s)"
|
||||
return f"{days} day(s)"
|
||||
elif days < 30:
|
||||
weeks = days // 7
|
||||
return f"{weeks} week(s)"
|
||||
@ -74,171 +75,138 @@ def format_time_ago(time_difference: timedelta) -> str:
|
||||
years = days // 365
|
||||
return f"{years} year(s)"
|
||||
|
||||
def normalize_difficulty(difficulty_name):
|
||||
# ScoreSaber difficulty naming convention
|
||||
difficulty_map = {
|
||||
def normalize_difficulty_name(difficulty_name):
|
||||
difficulty_names = {
|
||||
# ScoreSaber
|
||||
'_ExpertPlus_SoloStandard': 'expertplus',
|
||||
'_Expert_SoloStandard': 'expert',
|
||||
'_Hard_SoloStandard': 'hard',
|
||||
'_Normal_SoloStandard': 'normal',
|
||||
'_Easy_SoloStandard': 'easy'
|
||||
'_Easy_SoloStandard': 'easy',
|
||||
# BeatLeader
|
||||
1: 'easy',
|
||||
3: 'normal',
|
||||
5: 'hard',
|
||||
7: 'expert',
|
||||
9: 'expertplus',
|
||||
}
|
||||
|
||||
return difficulty_map.get(difficulty_name, difficulty_name.lower().replace('_solostandard', ''))
|
||||
# Return the mapped value or the original name if there is no mapping
|
||||
return difficulty_names.get(difficulty_name, difficulty_name)
|
||||
|
||||
def playlist_strategy_oldscores(
|
||||
api: ScoreSaberAPI,
|
||||
song_count: int = 10
|
||||
def playlist_strategy_beatleader_oldscores(
|
||||
api: BeatLeaderAPI,
|
||||
song_count: int = 40
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""Build and format a list of songs based on old scores, avoiding reusing the same song+difficulty."""
|
||||
"""
|
||||
Build and format a list of songs based on old scores from BeatLeader,
|
||||
avoiding reusing the same song+difficulty.
|
||||
|
||||
The playlist will consist of song hashes and their corresponding difficulties.
|
||||
"""
|
||||
|
||||
player_id = prompt_for_player_id()
|
||||
history = load_history()
|
||||
history.setdefault('oldscores', {})
|
||||
history.setdefault('beatleader_oldscores', {})
|
||||
|
||||
scores_data = api.get_player_scores(player_id, use_cache=True)
|
||||
scores_data = api.get_player_scores(player_id)
|
||||
all_scores = scores_data.get('playerScores', [])
|
||||
if not all_scores:
|
||||
logging.warning(f"No scores found for player ID {player_id}.")
|
||||
logging.warning(f"No scores found for player ID {player_id} on BeatLeader.")
|
||||
return []
|
||||
logging.debug(f"Found {len(all_scores)} scores for player ID {player_id}.")
|
||||
logging.debug(f"Found {len(all_scores)} scores for player ID {player_id} on BeatLeader.")
|
||||
|
||||
# Sort scores by timeSet in ascending order (oldest first)
|
||||
all_scores.sort(key=lambda x: x['score'].get('timeSet', ''))
|
||||
# Sort scores by epochTime in ascending order (oldest first)
|
||||
all_scores.sort(key=lambda x: x.get('score', {}).get('epochTime', 0))
|
||||
|
||||
difficulty_groups = defaultdict(list)
|
||||
no_stars_group = []
|
||||
playlist_data = []
|
||||
current_time = datetime.now(timezone.utc)
|
||||
|
||||
for score in all_scores:
|
||||
leaderboard = score.get('leaderboard', {})
|
||||
stars = leaderboard.get('stars', 0)
|
||||
song_id = leaderboard.get('songHash')
|
||||
difficulty_raw = leaderboard.get('difficulty', {}).get('difficultyRaw', '')
|
||||
for score_entry in all_scores:
|
||||
if len(playlist_data) >= song_count:
|
||||
break # Stop if we've reached the desired number of songs
|
||||
|
||||
score = score_entry.get('score', {})
|
||||
leaderboard = score_entry.get('leaderboard', {})
|
||||
|
||||
if not song_id or not difficulty_raw:
|
||||
logging.debug(f"Skipping score due to missing song_id or difficulty_raw: {score}")
|
||||
continue # Skip if essential data is missing
|
||||
song_hash = leaderboard.get('songHash')
|
||||
difficulty_raw = int(leaderboard.get('difficulty', ''))
|
||||
game_mode = leaderboard.get('modeName', 'Standard')
|
||||
epoch_time = score.get('epochTime')
|
||||
|
||||
if not song_hash or not difficulty_raw or not epoch_time:
|
||||
logging.debug(f"Skipping score due to missing song_hash or difficulty_raw: {score_entry}")
|
||||
continue
|
||||
|
||||
difficulty = normalize_difficulty_name(difficulty_raw)
|
||||
|
||||
# avoid reusing song+difficulty
|
||||
if song_hash in history['beatleader_oldscores'] and difficulty in history['beatleader_oldscores'][song_hash]:
|
||||
logging.debug(f"Skipping song {song_hash} with difficulty {difficulty} as it's in history.")
|
||||
continue # Skip if already used
|
||||
|
||||
# Calculate time ago
|
||||
time_set_str = score['score'].get('timeSet')
|
||||
if not time_set_str:
|
||||
logging.debug(f"Skipping score due to missing timeSet: {score}")
|
||||
continue # Skip if time_set is missing
|
||||
try:
|
||||
time_set = datetime.fromisoformat(time_set_str.replace('Z', '+00:00'))
|
||||
except ValueError as e:
|
||||
logging.error(f"Invalid time format for score ID {score['score'].get('id')}: {e}")
|
||||
time_set = datetime.fromtimestamp(epoch_time, tz=timezone.utc)
|
||||
except (ValueError, OSError) as e:
|
||||
logging.error(f"Invalid epochTime for score ID {score.get('id')}: {e}")
|
||||
continue
|
||||
time_difference = current_time - time_set
|
||||
time_ago = format_time_ago(time_difference)
|
||||
|
||||
# Normalize the difficulty name
|
||||
difficulty = normalize_difficulty(difficulty_raw)
|
||||
|
||||
# Check history to avoid reusing song+difficulty
|
||||
if song_id in history['oldscores'] and difficulty in history['oldscores'][song_id]:
|
||||
logging.debug(f"Skipping song {song_id} with difficulty {difficulty} as it's in history.")
|
||||
continue # Skip if already used
|
||||
|
||||
# Format the song data as expected by PlaylistBuilder
|
||||
# Format the song data for PlaylistBuilder
|
||||
song_dict = {
|
||||
'hash': song_id,
|
||||
'songName': leaderboard.get('songName', 'Unknown'),
|
||||
'hash': song_hash,
|
||||
'difficulties': [
|
||||
{
|
||||
'name': difficulty,
|
||||
'characteristic': leaderboard.get('difficulty', {}).get('gameMode', 'Standard')
|
||||
'characteristic': game_mode
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
# Group songs based on stars
|
||||
if stars is None or stars == 0:
|
||||
no_stars_group.append(song_dict)
|
||||
elif 0 < stars <= 3:
|
||||
difficulty_groups[0].append(song_dict)
|
||||
elif 4 <= stars < 6:
|
||||
difficulty_groups[1].append(song_dict)
|
||||
elif 6 <= stars < 7:
|
||||
difficulty_groups[2].append(song_dict)
|
||||
elif stars >= 7:
|
||||
difficulty_groups[3].append(song_dict)
|
||||
# Add the song to the playlist
|
||||
playlist_data.append(song_dict)
|
||||
logging.debug(f"Selected song for playlist: Hash={song_hash}, Difficulty={difficulty}. Last played {time_ago} ago.")
|
||||
|
||||
playlist_data = []
|
||||
# Update history
|
||||
history['beatleader_oldscores'].setdefault(song_hash, []).append(difficulty)
|
||||
|
||||
# Define the order and counts for each difficulty group
|
||||
for difficulty_level, count in [(0, song_count), (1, song_count), (2, song_count), (3, song_count)]:
|
||||
group = difficulty_groups.get(difficulty_level, [])
|
||||
if not group:
|
||||
logging.debug(f"No songs found for difficulty level {difficulty_level}.")
|
||||
continue
|
||||
# Sort by difficulty name to ensure consistent ordering
|
||||
sorted_group = sorted(group, key=lambda x: x['difficulties'][0]['name'])
|
||||
logging.debug(f"Sorted group for difficulty {difficulty_level}: {[song['songName'] for song in sorted_group]}")
|
||||
# Select unique songs up to the desired count
|
||||
unique_songs = {}
|
||||
for song in sorted_group:
|
||||
song_id = song['hash']
|
||||
if song_id not in unique_songs:
|
||||
unique_songs[song_id] = song
|
||||
logging.debug(f"Selected song for playlist: {song['songName']} ({song['difficulties'][0]['name']})")
|
||||
if len(unique_songs) >= count:
|
||||
break
|
||||
playlist_data.extend(unique_songs.values())
|
||||
logging.debug(f"Added {len(unique_songs)} songs from difficulty level {difficulty_level} to playlist.")
|
||||
|
||||
# Add no-stars songs separately
|
||||
if no_stars_group:
|
||||
unique_no_stars_songs = {}
|
||||
sorted_no_stars = sorted(no_stars_group, key=lambda x: x['difficulties'][0]['name']) # Adjust sorting as needed
|
||||
logging.debug(f"Sorted no_stars_group: {[song['songName'] for song in sorted_no_stars]}")
|
||||
for song in sorted_no_stars:
|
||||
song_id = song['hash']
|
||||
if song_id not in unique_no_stars_songs:
|
||||
unique_no_stars_songs[song_id] = song
|
||||
logging.debug(f"Selected no-star song for playlist: {song['songName']} ({song['difficulties'][0]['name']})")
|
||||
if len(unique_no_stars_songs) >= 5: # Limit to 5 no-stars songs
|
||||
break
|
||||
playlist_data.extend(unique_no_stars_songs.values())
|
||||
logging.debug(f"Added {len(unique_no_stars_songs)} no-star songs to playlist.")
|
||||
|
||||
# Log if no songs were added
|
||||
# Log the final playlist
|
||||
if not playlist_data:
|
||||
logging.info("No new songs found to add to the playlist based on history.")
|
||||
logging.info("No new songs found to add to the playlist based on history for BeatLeader.")
|
||||
else:
|
||||
# Log details of each song added to the playlist
|
||||
for song in playlist_data:
|
||||
song_name = song['songName']
|
||||
mapper = "Unknown" # Since mapper info was used earlier but not included in the formatted data
|
||||
# To include mapper, you may need to adjust the formatting in playlist_strategy_oldscores()
|
||||
# For now, it's omitted or you need to include it differently
|
||||
song_hash = song['hash']
|
||||
difficulty = song['difficulties'][0]['name']
|
||||
logging.info(f"Song added: {song_name} ({difficulty}), mapped by {mapper}. Last played {time_ago} ago.")
|
||||
logging.info(f"Total songs added to playlist: {len(playlist_data)}")
|
||||
logging.info(f"Song added: Hash={song_hash}, Difficulty={difficulty}.")
|
||||
logging.info(f"Total songs added to playlist from BeatLeader: {len(playlist_data)}")
|
||||
|
||||
# Update history to avoid reusing the same song+difficulty
|
||||
for song in playlist_data:
|
||||
song_id = song['hash']
|
||||
difficulty_name = song['difficulties'][0]['name']
|
||||
history['oldscores'].setdefault(song_id, []).append(difficulty_name)
|
||||
save_history(history)
|
||||
|
||||
return playlist_data
|
||||
|
||||
def create_playlist(strategy='oldscores') -> None:
|
||||
def saberlist(strategy='beatleader_oldscores') -> None:
|
||||
"""
|
||||
Generate a playlist of songs from a range of difficulties, all with scores previously set a long time ago.
|
||||
The range of difficulties ensures that the first few songs are good for warming up.
|
||||
Avoids reusing the same song+difficulty in a playlist based on history.
|
||||
"""
|
||||
api = ScoreSaberAPI(cache_expiry_days=CACHE_EXPIRY_DAYS)
|
||||
if strategy == 'scoresaber_oldscores':
|
||||
api = ScoreSaberAPI(cache_expiry_days=CACHE_EXPIRY_DAYS)
|
||||
elif strategy == 'beatleader_oldscores':
|
||||
api = BeatLeaderAPI(cache_expiry_days=CACHE_EXPIRY_DAYS)
|
||||
else:
|
||||
logging.error(f"Unknown strategy '{strategy}'")
|
||||
return
|
||||
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
timestamp = datetime.now().strftime("%y%m%d_%H%M")
|
||||
playlist_name = f"{strategy}-{timestamp}"
|
||||
|
||||
if strategy == 'oldscores':
|
||||
playlist_data = playlist_strategy_oldscores(api, song_count=10)
|
||||
if strategy == 'scoresaber_oldscores':
|
||||
playlist_data = playlist_strategy_scoresaber_oldscores(api)
|
||||
elif strategy == 'beatleader_oldscores':
|
||||
playlist_data = playlist_strategy_beatleader_oldscores(api, song_count=40)
|
||||
|
||||
if not playlist_data:
|
||||
logging.info("No new scores found to add to the playlist.")
|
||||
@ -249,3 +217,20 @@ def create_playlist(strategy='oldscores') -> None:
|
||||
playlist_title=playlist_name,
|
||||
playlist_author="SaberList Tool"
|
||||
)
|
||||
|
||||
"""
|
||||
if __name__ == "__main__":
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(description="Generate a playlist based on player scores.")
|
||||
parser.add_argument(
|
||||
'--strategy',
|
||||
type=str,
|
||||
default='beatleader_oldscores',
|
||||
choices=['scoresaber_oldscores', 'beatleader_oldscores'],
|
||||
help='Strategy to use for building the playlist.'
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
saberlist(strategy=args.strategy)
|
||||
"""
|
@ -4,7 +4,7 @@ import os
|
||||
import json
|
||||
from pathlib import Path
|
||||
from unittest import mock
|
||||
from saberlist.PlaylistBuilder import PlaylistBuilder
|
||||
from helpers.PlaylistBuilder import PlaylistBuilder
|
||||
|
||||
@pytest.fixture
|
||||
def temp_environment(tmp_path):
|
||||
|
Loading…
Reference in New Issue
Block a user