Add cutoff date to mapper's maps, and other changes
This commit is contained in:
parent
362603d160
commit
0787ad6bee
61
docs/BeatLeaderPlayers.md
Normal file
61
docs/BeatLeaderPlayers.md
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
# BL Players
|
||||||
|
|
||||||
|
```python
|
||||||
|
from helpers.SimpleBeatLeaderAPI import SimpleBeatLeaderAPI
|
||||||
|
beatleader_api = SimpleBeatLeaderAPI()
|
||||||
|
|
||||||
|
player_data = beatleader_api.get_players()
|
||||||
|
|
||||||
|
# Filter players with high accuracy and rank better than 1000
|
||||||
|
high_accuracy_players = [
|
||||||
|
player for player in player_data
|
||||||
|
if player['scoreStats']['averageRankedAccuracy'] > 0.96 and
|
||||||
|
player['rank'] > 1000 and
|
||||||
|
player['scoreStats']['totalPlayCount'] > 1000
|
||||||
|
]
|
||||||
|
|
||||||
|
# Sort the filtered list by rank in descending order
|
||||||
|
high_accuracy_players_sorted = sorted(high_accuracy_players, key=lambda player: player['rank'])
|
||||||
|
|
||||||
|
for player in high_accuracy_players_sorted:
|
||||||
|
rank = player['rank']
|
||||||
|
name = player['name']
|
||||||
|
player_id = player['id']
|
||||||
|
accuracy = player['scoreStats']['averageRankedAccuracy'] * 100
|
||||||
|
total_plays = player['scoreStats']['totalPlayCount']
|
||||||
|
|
||||||
|
print(f" {rank}: {player_id} {accuracy:.2f}%, {total_plays} plays: {name}")
|
||||||
|
```
|
||||||
|
|
||||||
|
Results:
|
||||||
|
|
||||||
|
1002: 76561198108621262 96.84%, 2799 plays: Nano
|
||||||
|
1039: 76561198067254301 97.40%, 2083 plays: Muz
|
||||||
|
1106: 76561198020334769 96.70%, 1272 plays: Rexxz
|
||||||
|
1140: 76561198103016268 97.17%, 2635 plays: ACC | Wookie
|
||||||
|
1144: 76561198074878770 96.91%, 1007 plays: Jormungandr
|
||||||
|
1158: 76561198154593513 97.05%, 1283 plays: ZLQ
|
||||||
|
1182: 76561198398878358 96.51%, 1717 plays: Rimu
|
||||||
|
1221: 76561198141764746 97.71%, 1688 plays: ACC | Jani
|
||||||
|
1303: 76561198890357715 97.91%, 1824 plays: mojo
|
||||||
|
1333: 76561197977159578 96.35%, 1656 plays: Ando
|
||||||
|
1344: 76561198092977917 97.05%, 2906 plays: bluewingoo
|
||||||
|
1356: 76561199100866994 96.30%, 1800 plays: Dollface
|
||||||
|
1411: 76561198040296837 96.59%, 2043 plays: BSFR | Vred
|
||||||
|
1460: 76561198162613446 96.54%, 1581 plays: RockaX
|
||||||
|
1599: 76561198212831695 96.20%, 1274 plays: Xmpo
|
||||||
|
1798: 76561198139760882 96.13%, 1084 plays: W33talik
|
||||||
|
1907: 76561198084876475 96.51%, 1090 plays: NeroMJ
|
||||||
|
1932: 76561198163148965 96.37%, 1345 plays: Hygoto
|
||||||
|
2048: 76561198358170446 98.24%, 2130 plays: 19:47
|
||||||
|
2088: 76561198163772169 97.82%, 1239 plays: Kunya
|
||||||
|
2227: 76561198200272334 96.06%, 1442 plays: Arflic
|
||||||
|
2231: 76561197993806676 96.40%, 2296 plays: Taoh Rihze
|
||||||
|
2323: 76561199017645460 96.57%, 2033 plays: GF | Soogy
|
||||||
|
2353: 76561197999550197 96.42%, 1272 plays: ACC | Ixsen
|
||||||
|
2551: 3767819413276402 96.22%, 1192 plays: rileywip
|
||||||
|
2563: 76561198052914112 96.98%, 1365 plays: BlueFlame_MK
|
||||||
|
2607: 76561198023638450 96.23%, 22687 plays: Craedien
|
||||||
|
2763: 76561198017723775 96.63%, 1131 plays: ika
|
||||||
|
3272: 76561199157033165 96.09%, 1936 plays: Lumberjack462
|
||||||
|
3309: 76561199407393962 96.27%, 1777 plays: pleb
|
@ -55,9 +55,9 @@ map_data = beatsaver_api.get_maps(year=2024, month=9)
|
|||||||
|
|
||||||
```python
|
```python
|
||||||
from helpers.SimpleBeatLeaderAPI import SimpleBeatLeaderAPI
|
from helpers.SimpleBeatLeaderAPI import SimpleBeatLeaderAPI
|
||||||
player_id = "76561199407393962"
|
|
||||||
beatleader_api = SimpleBeatLeaderAPI()
|
beatleader_api = SimpleBeatLeaderAPI()
|
||||||
|
|
||||||
|
player_id = "76561199407393962"
|
||||||
scores_data = beatleader_api.get_player_scores(player_id).get('data', [])
|
scores_data = beatleader_api.get_player_scores(player_id).get('data', [])
|
||||||
|
|
||||||
acc_graph = beatleader_api.get_player_accgraph(player_id)
|
acc_graph = beatleader_api.get_player_accgraph(player_id)
|
||||||
@ -70,10 +70,13 @@ filtered_data[0]
|
|||||||
|
|
||||||
```python
|
```python
|
||||||
from helpers.SimpleBeatSaverAPI import SimpleBeatSaverAPI
|
from helpers.SimpleBeatSaverAPI import SimpleBeatSaverAPI
|
||||||
beat_saver_api = SimpleBeatSaverAPI()
|
from saberlist.playlist_strategies.beatsaver import *
|
||||||
|
beatsaver_api = SimpleBeatSaverAPI()
|
||||||
|
environment_maps = beatsaver_api.get_maps_by_environment()
|
||||||
|
|
||||||
|
curated_songs = beatsaver_api.get_curated_songs(use_cache=False)
|
||||||
|
mapper_maps = beatsaver_api.get_mapper_maps(mapper_id=4285738, use_cache=False)
|
||||||
|
|
||||||
curated_songs = beat_saver_api.get_curated_songs(use_cache=False)
|
|
||||||
mapper_maps = beat_saver_api.get_mapper_maps(mapper_id=4285738, use_cache=False)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## ScoreSaberAPI
|
## ScoreSaberAPI
|
||||||
|
@ -51,7 +51,10 @@ class SimpleBeatLeaderAPI:
|
|||||||
file_modified_time = datetime.fromtimestamp(os.path.getmtime(cache_file))
|
file_modified_time = datetime.fromtimestamp(os.path.getmtime(cache_file))
|
||||||
return datetime.now() - file_modified_time < timedelta(days=self.cache_expiry_days)
|
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):
|
def get_player_scores(self, player_id,
|
||||||
|
use_cache=True,
|
||||||
|
page_size=100,
|
||||||
|
max_pages=None):
|
||||||
cache_file = self._get_cache_filename(player_id)
|
cache_file = self._get_cache_filename(player_id)
|
||||||
|
|
||||||
if use_cache and self._is_cache_valid(cache_file):
|
if use_cache and self._is_cache_valid(cache_file):
|
||||||
@ -134,7 +137,12 @@ class SimpleBeatLeaderAPI:
|
|||||||
logging.error(f"Error fetching player info for ID {player_id}: {e}")
|
logging.error(f"Error fetching player info for ID {player_id}: {e}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def get_leaderboard(self, hash, diff="ExpertPlus", mode="Standard", use_cache=True, page=1, count=10) -> list[dict]:
|
def get_leaderboard(self, hash,
|
||||||
|
diff="ExpertPlus",
|
||||||
|
mode="Standard",
|
||||||
|
use_cache=True,
|
||||||
|
page=1,
|
||||||
|
count=10) -> list[dict]:
|
||||||
"""
|
"""
|
||||||
Retrieve leaderboard for a specific map, with caching.
|
Retrieve leaderboard for a specific map, with caching.
|
||||||
|
|
||||||
@ -178,7 +186,11 @@ class SimpleBeatLeaderAPI:
|
|||||||
logging.error(f"Error fetching leaderboard for hash {hash}, diff {diff}, mode {mode}: {e}")
|
logging.error(f"Error fetching leaderboard for hash {hash}, diff {diff}, mode {mode}: {e}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def get_player_accgraph(self, player_id, use_cache=True, context="general", include_unranked=False, type="acc"):
|
def get_player_accgraph(self, player_id,
|
||||||
|
use_cache=True,
|
||||||
|
context="general",
|
||||||
|
include_unranked=False,
|
||||||
|
type="acc"):
|
||||||
"""
|
"""
|
||||||
Retrieve graph data for a specific player.
|
Retrieve graph data for a specific player.
|
||||||
|
|
||||||
@ -220,7 +232,9 @@ class SimpleBeatLeaderAPI:
|
|||||||
logging.error(f"Error fetching acc graph for player {player_id}: {e}")
|
logging.error(f"Error fetching acc graph for player {player_id}: {e}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def get_ranked_maps(self, stars_from=5, stars_to=10, use_cache=True):
|
def get_ranked_maps(self, stars_from=5,
|
||||||
|
stars_to=10,
|
||||||
|
use_cache=True):
|
||||||
"""
|
"""
|
||||||
Retrieve all ranked maps within the specified star range, handling pagination and caching.
|
Retrieve all ranked maps within the specified star range, handling pagination and caching.
|
||||||
|
|
||||||
@ -292,4 +306,67 @@ class SimpleBeatLeaderAPI:
|
|||||||
json.dump(all_maps, f, indent=4)
|
json.dump(all_maps, f, indent=4)
|
||||||
logging.debug(f"Cached {len(all_maps)} ranked maps to {cache_file}")
|
logging.debug(f"Cached {len(all_maps)} ranked maps to {cache_file}")
|
||||||
|
|
||||||
return all_maps
|
return all_maps
|
||||||
|
|
||||||
|
def get_players(self,
|
||||||
|
max_pages=80,
|
||||||
|
count=50,
|
||||||
|
use_cache=True):
|
||||||
|
"""
|
||||||
|
Retrieve a list of players ordered by play rank.
|
||||||
|
|
||||||
|
This method fetches players from the API using sortBy=0 (ordered by play rank) and paginates through
|
||||||
|
the results up to a maximum of 'max_pages' pages. The 'count' parameter defines the number of players
|
||||||
|
per page (default: 50). Results are cached on disk in the configured cache directory.
|
||||||
|
|
||||||
|
:param max_pages: Maximum number of pages to retrieve (default: 80)
|
||||||
|
:param count: Number of players per page (default: 50)
|
||||||
|
:param use_cache: Whether to use cached data if available (default: True)
|
||||||
|
:return: List of player data.
|
||||||
|
"""
|
||||||
|
cache_file = os.path.join(self.CACHE_DIR, f"players_{max_pages}.json")
|
||||||
|
if use_cache and self._is_cache_valid(cache_file):
|
||||||
|
logging.debug("Using cached player data.")
|
||||||
|
with open(cache_file, 'r') as f:
|
||||||
|
return json.load(f)
|
||||||
|
|
||||||
|
logging.debug(f"Fetching fresh player data ordered by play rank (max_pages: {max_pages}, count: {count}).")
|
||||||
|
url = f"{self.BASE_URL}/players"
|
||||||
|
all_players = []
|
||||||
|
page = 1
|
||||||
|
total_items = None
|
||||||
|
|
||||||
|
while page <= max_pages:
|
||||||
|
params = {
|
||||||
|
"sortBy": 0,
|
||||||
|
"page": page,
|
||||||
|
"count": count
|
||||||
|
}
|
||||||
|
try:
|
||||||
|
response = self.session.get(url, params=params)
|
||||||
|
response.raise_for_status()
|
||||||
|
data = response.json()
|
||||||
|
|
||||||
|
# Set total_items from the metadata returned in the first request.
|
||||||
|
if total_items is None:
|
||||||
|
total_items = data.get("metadata", {}).get("total", 0)
|
||||||
|
|
||||||
|
players_page = data.get("data", [])
|
||||||
|
all_players.extend(players_page)
|
||||||
|
|
||||||
|
# Stop if we've fetched all available players or reached max_pages.
|
||||||
|
if len(all_players) >= total_items or not players_page:
|
||||||
|
break
|
||||||
|
|
||||||
|
page += 1
|
||||||
|
sleep(1) # Respect API rate limits
|
||||||
|
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
logging.error(f"Error fetching players on page {page}: {e}")
|
||||||
|
break
|
||||||
|
|
||||||
|
with open(cache_file, 'w') as f:
|
||||||
|
json.dump(all_players, f)
|
||||||
|
logging.debug(f"Cached player data to {cache_file}")
|
||||||
|
|
||||||
|
return all_players
|
@ -66,7 +66,10 @@ class SimpleBeatSaverAPI:
|
|||||||
return self.CACHE_DIR
|
return self.CACHE_DIR
|
||||||
|
|
||||||
|
|
||||||
def get_curated_songs(self, use_cache=True) -> list[dict]:
|
def get_curated_songs(
|
||||||
|
self,
|
||||||
|
use_cache=True
|
||||||
|
) -> list[dict]:
|
||||||
"""
|
"""
|
||||||
Retrieve curated songs from BeatSaver.
|
Retrieve curated songs from BeatSaver.
|
||||||
|
|
||||||
@ -127,7 +130,11 @@ class SimpleBeatSaverAPI:
|
|||||||
|
|
||||||
return processed_songs
|
return processed_songs
|
||||||
|
|
||||||
def get_followed_mappers(self, user_id: int = 243016, use_cache=True) -> list[dict]:
|
def get_followed_mappers(
|
||||||
|
self,
|
||||||
|
user_id: int = 243016,
|
||||||
|
use_cache=True
|
||||||
|
) -> list[dict]:
|
||||||
"""
|
"""
|
||||||
Retrieve list of mappers followed by a specific user.
|
Retrieve list of mappers followed by a specific user.
|
||||||
|
|
||||||
@ -159,7 +166,29 @@ class SimpleBeatSaverAPI:
|
|||||||
logging.error(f"Error fetching followed mappers: {e}")
|
logging.error(f"Error fetching followed mappers: {e}")
|
||||||
return []
|
return []
|
||||||
|
|
||||||
def get_mapper_maps(self, mapper_id: int, use_cache=True) -> list[dict]:
|
"""Paste this into ipython to get sample data:
|
||||||
|
from helpers.SimpleBeatSaverAPI import SimpleBeatSaverAPI
|
||||||
|
from saberlist.playlist_strategies.beatsaver import *
|
||||||
|
BASE_URL = "https://api.beatsaver.com"
|
||||||
|
import requests
|
||||||
|
mapper_id = 29945
|
||||||
|
page = 0
|
||||||
|
session = requests.Session()
|
||||||
|
url = f"{BASE_URL}/search/text/{page}"
|
||||||
|
params = {
|
||||||
|
'collaborator': str(mapper_id),
|
||||||
|
'automapper': 'true',
|
||||||
|
'sortOrder': 'Latest'
|
||||||
|
}
|
||||||
|
response = session.get(url, params=params)
|
||||||
|
data = response.json()
|
||||||
|
data['docs'][0]
|
||||||
|
"""
|
||||||
|
def get_mapper_maps(
|
||||||
|
self,
|
||||||
|
mapper_id: int,
|
||||||
|
use_cache=True
|
||||||
|
) -> list[dict]:
|
||||||
"""
|
"""
|
||||||
Retrieve all maps created by a specific mapper.
|
Retrieve all maps created by a specific mapper.
|
||||||
|
|
||||||
@ -197,6 +226,7 @@ class SimpleBeatSaverAPI:
|
|||||||
'hash': version['hash'],
|
'hash': version['hash'],
|
||||||
'key': song['id'],
|
'key': song['id'],
|
||||||
'songName': song['metadata']['songName'],
|
'songName': song['metadata']['songName'],
|
||||||
|
'date': song['lastPublishedAt'] # e.g. 2024-10-20T22:49:05.842454Z
|
||||||
}
|
}
|
||||||
processed_songs.append(song_info)
|
processed_songs.append(song_info)
|
||||||
|
|
||||||
@ -216,3 +246,118 @@ class SimpleBeatSaverAPI:
|
|||||||
json.dump(processed_songs, f)
|
json.dump(processed_songs, f)
|
||||||
|
|
||||||
return processed_songs
|
return processed_songs
|
||||||
|
|
||||||
|
def get_maps_by_environment(
|
||||||
|
self,
|
||||||
|
environment_name: str = None,
|
||||||
|
max_pages: int = 10,
|
||||||
|
use_cache: bool = True
|
||||||
|
) -> list[dict]:
|
||||||
|
"""
|
||||||
|
Retrieve all maps with a given environment name from BeatSaver.
|
||||||
|
|
||||||
|
:param environment_name: The name of the environment to filter maps by. If None, prompts user to select.
|
||||||
|
:param max_pages: Maximum number of pages to fetch. If None, fetch all available pages.
|
||||||
|
:param use_cache: Whether to use cached data if available (default: True)
|
||||||
|
:return: List of dictionaries containing map information
|
||||||
|
"""
|
||||||
|
if not environment_name:
|
||||||
|
environment_name = self.prompt_for_environment()
|
||||||
|
|
||||||
|
cache_file = os.path.join(self.CACHE_DIR, f"environment_{environment_name}_maps.json")
|
||||||
|
|
||||||
|
if use_cache and self._is_cache_valid(cache_file):
|
||||||
|
logging.debug(f"Using cached data for environment '{environment_name}'")
|
||||||
|
with open(cache_file, 'r') as f:
|
||||||
|
return json.load(f)
|
||||||
|
|
||||||
|
processed_maps = []
|
||||||
|
page = 0
|
||||||
|
|
||||||
|
while True:
|
||||||
|
url = f"{self.BASE_URL}/search/text/{page}"
|
||||||
|
params = {
|
||||||
|
"environments": f"{environment_name}Environment"
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = self.session.get(url, params=params)
|
||||||
|
response.raise_for_status()
|
||||||
|
data = response.json()
|
||||||
|
|
||||||
|
# Process the response to extract relevant information
|
||||||
|
for song in data.get('docs', []):
|
||||||
|
for version in song.get('versions', []):
|
||||||
|
map_info = {
|
||||||
|
'hash': version['hash'],
|
||||||
|
'key': song['id'],
|
||||||
|
'songName': song['metadata']['songName'],
|
||||||
|
'environment': environment_name,
|
||||||
|
}
|
||||||
|
processed_maps.append(map_info)
|
||||||
|
|
||||||
|
# Check if we've reached the last page
|
||||||
|
if page >= data['info']['pages'] - 1:
|
||||||
|
break
|
||||||
|
|
||||||
|
page += 1
|
||||||
|
|
||||||
|
if max_pages is not None and page >= max_pages:
|
||||||
|
break
|
||||||
|
|
||||||
|
sleep(1) # Respectful delay between requests
|
||||||
|
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
logging.error(f"Error fetching maps for environment '{environment_name}': {e}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
# Cache the results
|
||||||
|
with open(cache_file, 'w') as f:
|
||||||
|
json.dump(processed_maps, f)
|
||||||
|
logging.debug(f"Cached {len(processed_maps)} maps for environment '{environment_name}'")
|
||||||
|
|
||||||
|
return processed_maps
|
||||||
|
|
||||||
|
def prompt_for_environment(self) -> str:
|
||||||
|
"""
|
||||||
|
Prompt the user to select a map environment from a list of known environments.
|
||||||
|
|
||||||
|
:return: Selected environment name as a string
|
||||||
|
"""
|
||||||
|
# We're only interested in V3 environments and beyond
|
||||||
|
known_environments = [
|
||||||
|
'Weave',
|
||||||
|
'Pyro',
|
||||||
|
'EDM',
|
||||||
|
'TheSecond',
|
||||||
|
'Lizzo',
|
||||||
|
'TheWeeknd',
|
||||||
|
'RockMixtape',
|
||||||
|
'Dragons2',
|
||||||
|
'Panic2',
|
||||||
|
'Queen',
|
||||||
|
'LinkinPark2',
|
||||||
|
'TheRollingStones',
|
||||||
|
'Lattice',
|
||||||
|
'DaftPunk',
|
||||||
|
'HipHop',
|
||||||
|
'Collider',
|
||||||
|
'Britney',
|
||||||
|
'Monstercat2',
|
||||||
|
'Metallica',
|
||||||
|
]
|
||||||
|
print("Please select a map environment from the list below:")
|
||||||
|
for idx, env in enumerate(known_environments, start=1):
|
||||||
|
print(f"{idx}. {env}")
|
||||||
|
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
choice = int(input("Enter the number corresponding to your choice: "))
|
||||||
|
if 1 <= choice <= len(known_environments):
|
||||||
|
selected_environment = known_environments[choice - 1]
|
||||||
|
logging.debug(f"Selected environment: {selected_environment}")
|
||||||
|
return selected_environment
|
||||||
|
else:
|
||||||
|
print(f"Please enter a number between 1 and {len(known_environments)}.")
|
||||||
|
except ValueError:
|
||||||
|
print("Invalid input. Please enter a valid number.")
|
||||||
|
@ -39,6 +39,8 @@ from saberlist.playlist_strategies.accuracy import (
|
|||||||
from saberlist.playlist_strategies.beatsaver import (
|
from saberlist.playlist_strategies.beatsaver import (
|
||||||
playlist_strategy_beatsaver_curated,
|
playlist_strategy_beatsaver_curated,
|
||||||
playlist_strategy_beatsaver_mappers,
|
playlist_strategy_beatsaver_mappers,
|
||||||
|
playlist_strategy_beatsaver_environment,
|
||||||
|
playlist_strategy_beatsaver_all_environments,
|
||||||
)
|
)
|
||||||
|
|
||||||
def saberlist() -> None:
|
def saberlist() -> None:
|
||||||
@ -115,6 +117,30 @@ def saberlist() -> None:
|
|||||||
playlist_data, playlist_title = playlist_strategy_beatsaver_mappers(SimpleBeatSaverAPI())
|
playlist_data, playlist_title = playlist_strategy_beatsaver_mappers(SimpleBeatSaverAPI())
|
||||||
playlist_builder = PlaylistBuilder(covers_dir='./covers/pajamas')
|
playlist_builder = PlaylistBuilder(covers_dir='./covers/pajamas')
|
||||||
|
|
||||||
|
elif strategy == 'beatsaver_environment':
|
||||||
|
environment_name = args.environment_name
|
||||||
|
playlist_data, playlist_title = playlist_strategy_beatsaver_environment(
|
||||||
|
SimpleBeatSaverAPI(cache_expiry_days=CACHE_EXPIRY_DAYS),
|
||||||
|
environment_name=environment_name
|
||||||
|
)
|
||||||
|
playlist_builder = PlaylistBuilder(covers_dir='./covers/beatsaver')
|
||||||
|
|
||||||
|
elif strategy == 'beatsaver_all_environments':
|
||||||
|
playlists = playlist_strategy_beatsaver_all_environments(
|
||||||
|
SimpleBeatSaverAPI(cache_expiry_days=CACHE_EXPIRY_DAYS)
|
||||||
|
)
|
||||||
|
builder = PlaylistBuilder(covers_dir='./covers/beatsaver')
|
||||||
|
for env, (data, title) in playlists.items():
|
||||||
|
if data:
|
||||||
|
builder.create_playlist(
|
||||||
|
data,
|
||||||
|
playlist_title=f"{title}",
|
||||||
|
playlist_author="SaberList Tool"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
logging.info(f"No new songs to add for environment '{env}'.")
|
||||||
|
return
|
||||||
|
|
||||||
elif strategy == 'blank_playlist':
|
elif strategy == 'blank_playlist':
|
||||||
playlist_data = []
|
playlist_data = []
|
||||||
playlist_title = input("Enter playlist title: ")
|
playlist_title = input("Enter playlist title: ")
|
||||||
@ -228,6 +254,23 @@ def parse_args_subcommands():
|
|||||||
action="store_true",
|
action="store_true",
|
||||||
help="Reset the history for blank_playlist (usually unnecessary)")
|
help="Reset the history for blank_playlist (usually unnecessary)")
|
||||||
|
|
||||||
|
# -------- beatsaver_environment --------
|
||||||
|
parser_bs_env = subparsers.add_parser("beatsaver_environment",
|
||||||
|
help="Generate a playlist for a specific BeatSaver environment")
|
||||||
|
parser_bs_env.add_argument("-r", "--reset",
|
||||||
|
action="store_true",
|
||||||
|
help="Reset the history for beatsaver_environment")
|
||||||
|
parser_bs_env.add_argument("environment_name",
|
||||||
|
type=str,
|
||||||
|
help="Name of the BeatSaver environment (e.g., 'Weave', 'Pyro')")
|
||||||
|
|
||||||
|
# -------- beatsaver_all_environments --------
|
||||||
|
parser_bs_all_env = subparsers.add_parser("beatsaver_all_environments",
|
||||||
|
help="Generate playlists for all known BeatSaver environments")
|
||||||
|
parser_bs_all_env.add_argument("-r", "--reset",
|
||||||
|
action="store_true",
|
||||||
|
help="Reset the history for all beatsaver environments")
|
||||||
|
|
||||||
# If no arguments passed, print help
|
# If no arguments passed, print help
|
||||||
if len(sys.argv) == 1:
|
if len(sys.argv) == 1:
|
||||||
parser.print_help(sys.stderr)
|
parser.print_help(sys.stderr)
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
from statistics import mean
|
from statistics import mean
|
||||||
from typing import Dict, Any, List
|
from typing import Dict, Any, List, Tuple, Optional
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta, timezone
|
||||||
import calendar
|
import calendar
|
||||||
|
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
@ -235,16 +235,25 @@ def map_leaders_by_month(month: int = 9, year: int = 2024, game_modes: List[str]
|
|||||||
|
|
||||||
def playlist_strategy_beatsaver_mappers(
|
def playlist_strategy_beatsaver_mappers(
|
||||||
api: SimpleBeatSaverAPI,
|
api: SimpleBeatSaverAPI,
|
||||||
song_count: int = 50
|
song_count: int = 50,
|
||||||
|
date_threshold: Optional[datetime] = None
|
||||||
) -> List[Dict[str, Any]]:
|
) -> List[Dict[str, Any]]:
|
||||||
"""
|
"""
|
||||||
Build a playlist from maps created by specified mappers on BeatSaver,
|
Build a playlist from maps created by specified mappers on BeatSaver,
|
||||||
interleaving maps from different mappers for variety.
|
interleaving maps from different mappers for variety.
|
||||||
|
|
||||||
|
Maps with a publication date older than the date_threshold are skipped.
|
||||||
|
|
||||||
:param api: SimpleBeatSaverAPI instance for making API calls
|
:param api: SimpleBeatSaverAPI instance for making API calls
|
||||||
:param song_count: Number of songs to include in the playlist
|
:param song_count: Number of songs to include in the playlist
|
||||||
|
:param date_threshold: datetime threshold; maps published before this date are skipped.
|
||||||
|
Defaults to January 1, 2024, in UTC.
|
||||||
:return: Tuple of (playlist data, playlist title)
|
:return: Tuple of (playlist data, playlist title)
|
||||||
"""
|
"""
|
||||||
|
from datetime import timezone
|
||||||
|
if date_threshold is None:
|
||||||
|
date_threshold = datetime(2024, 1, 1, tzinfo=timezone.utc)
|
||||||
|
|
||||||
history = load_history()
|
history = load_history()
|
||||||
history.setdefault('beatsaver_mappers', {})
|
history.setdefault('beatsaver_mappers', {})
|
||||||
history.setdefault('playlist_counts', {})
|
history.setdefault('playlist_counts', {})
|
||||||
@ -256,11 +265,12 @@ def playlist_strategy_beatsaver_mappers(
|
|||||||
new_count = current_count + 1
|
new_count = current_count + 1
|
||||||
history['playlist_counts'][count_key] = new_count
|
history['playlist_counts'][count_key] = new_count
|
||||||
|
|
||||||
# Collect maps by mapper
|
# Collect maps by mapper with date filtering
|
||||||
maps_by_mapper = {}
|
maps_by_mapper = {}
|
||||||
for mapper_id in mapper_ids:
|
for mapper_id in mapper_ids:
|
||||||
logging.info(f"Fetching maps for mapper ID: {mapper_id}")
|
logging.info(f"Fetching maps for mapper ID: {mapper_id}")
|
||||||
mapper_maps = api.get_mapper_maps(mapper_id=mapper_id, use_cache=False)
|
# mapper_maps = api.get_mapper_maps(mapper_id=mapper_id, use_cache=False)
|
||||||
|
mapper_maps = api.get_mapper_maps(mapper_id=mapper_id)
|
||||||
|
|
||||||
if not mapper_maps:
|
if not mapper_maps:
|
||||||
logging.warning(f"No maps found for mapper ID: {mapper_id}")
|
logging.warning(f"No maps found for mapper ID: {mapper_id}")
|
||||||
@ -268,12 +278,31 @@ def playlist_strategy_beatsaver_mappers(
|
|||||||
|
|
||||||
logging.info(f"Found {len(mapper_maps)} maps from mapper ID: {mapper_id}")
|
logging.info(f"Found {len(mapper_maps)} maps from mapper ID: {mapper_id}")
|
||||||
|
|
||||||
# Filter out maps that are in history
|
eligible_maps = []
|
||||||
eligible_maps = [
|
for song in mapper_maps:
|
||||||
song for song in mapper_maps
|
# Skip if the song already exists in history
|
||||||
if song.get('hash') not in history['beatsaver_mappers']
|
if song.get('hash') in history['beatsaver_mappers']:
|
||||||
]
|
continue
|
||||||
|
|
||||||
|
# Filter out maps with a date older than the provided threshold.
|
||||||
|
date_str = song.get('date')
|
||||||
|
if not date_str:
|
||||||
|
logging.warning(f"Skipping song {song.get('songName')} as it has no date.")
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Convert the ISO date string (which ends with 'Z') into a timezone-aware datetime
|
||||||
|
song_date = datetime.fromisoformat(date_str.replace("Z", "+00:00"))
|
||||||
|
except Exception as e:
|
||||||
|
logging.warning(f"Could not parse date '{date_str}' for song {song.get('songName')}: {e}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
if song_date < date_threshold:
|
||||||
|
logging.debug(f"Skipping song {song.get('songName')} published back in {song_date.strftime('%b %Y')}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
eligible_maps.append(song)
|
||||||
|
|
||||||
if eligible_maps:
|
if eligible_maps:
|
||||||
maps_by_mapper[mapper_id] = eligible_maps
|
maps_by_mapper[mapper_id] = eligible_maps
|
||||||
|
|
||||||
@ -306,3 +335,154 @@ def playlist_strategy_beatsaver_mappers(
|
|||||||
playlist_title = f"mappers-{new_count:02d}"
|
playlist_title = f"mappers-{new_count:02d}"
|
||||||
|
|
||||||
return playlist_data, playlist_title
|
return playlist_data, playlist_title
|
||||||
|
|
||||||
|
def playlist_strategy_beatsaver_environment(
|
||||||
|
api: SimpleBeatSaverAPI,
|
||||||
|
environment_name: str,
|
||||||
|
song_count: int = 50,
|
||||||
|
max_pages: int = 10
|
||||||
|
) -> Tuple[List[Dict[str, Any]], str]:
|
||||||
|
"""
|
||||||
|
Build a playlist from maps that match the specified environment.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
api (SimpleBeatSaverAPI): Instance for BeatSaver API calls.
|
||||||
|
environment_name (str): The map environment to filter by.
|
||||||
|
song_count (int): Max number of songs to include (default: 50).
|
||||||
|
max_pages (int): Maximum number of pages to fetch from the API (default: 10).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A tuple containing:
|
||||||
|
- playlist_data: A list of dictionaries with song details.
|
||||||
|
- playlist_title: A unique playlist title in the format "<environment>-XX".
|
||||||
|
"""
|
||||||
|
history = load_history()
|
||||||
|
history.setdefault('beatsaver_environment', {})
|
||||||
|
history.setdefault('playlist_counts', {})
|
||||||
|
if environment_name not in history['beatsaver_environment']:
|
||||||
|
history['beatsaver_environment'][environment_name] = {}
|
||||||
|
|
||||||
|
count_key = f"beatsaver_environment-{environment_name}"
|
||||||
|
current_count = history['playlist_counts'].get(count_key, 0)
|
||||||
|
new_count = current_count + 1
|
||||||
|
history['playlist_counts'][count_key] = new_count
|
||||||
|
|
||||||
|
maps = api.get_maps_by_environment(
|
||||||
|
environment_name=environment_name,
|
||||||
|
max_pages=max_pages,
|
||||||
|
use_cache=True
|
||||||
|
)
|
||||||
|
|
||||||
|
if not maps:
|
||||||
|
logging.error(f"No maps found for environment: {environment_name}.")
|
||||||
|
return [], f"{environment_name.lower()}-{new_count:02d}"
|
||||||
|
|
||||||
|
playlist_data = []
|
||||||
|
for song in maps:
|
||||||
|
song_hash = song.get('hash')
|
||||||
|
if song_hash in history['beatsaver_environment'][environment_name]:
|
||||||
|
logging.debug(f"Skipping song {song_hash} for environment {environment_name} as it is in history.")
|
||||||
|
continue
|
||||||
|
|
||||||
|
playlist_data.append(song)
|
||||||
|
logging.info(f"Song added: {song.get('songName', 'Unknown')} from environment {environment_name}")
|
||||||
|
history['beatsaver_environment'][environment_name][song_hash] = True
|
||||||
|
|
||||||
|
if len(playlist_data) >= song_count:
|
||||||
|
break
|
||||||
|
|
||||||
|
if not playlist_data:
|
||||||
|
logging.info(f"No new songs found for environment {environment_name} based on history.")
|
||||||
|
else:
|
||||||
|
logging.info(f"Total songs added for environment {environment_name}: {len(playlist_data)}")
|
||||||
|
|
||||||
|
save_history(history)
|
||||||
|
playlist_title = f"{environment_name.lower()}-{new_count:02d}"
|
||||||
|
return playlist_data, playlist_title
|
||||||
|
|
||||||
|
def playlist_strategy_beatsaver_all_environments(
|
||||||
|
api: SimpleBeatSaverAPI,
|
||||||
|
song_count: int = 50,
|
||||||
|
max_pages: int = 10
|
||||||
|
) -> Dict[str, Tuple[List[Dict[str, Any]], str]]:
|
||||||
|
"""
|
||||||
|
Build playlists for every known environment.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
api (SimpleBeatSaverAPI): Instance for BeatSaver API calls.
|
||||||
|
song_count (int): Max number of songs per environment playlist (default: 50).
|
||||||
|
max_pages (int): Maximum pages to fetch from the API per environment (default: 10).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A dictionary mapping each environment (str) to a tuple:
|
||||||
|
(playlist_data, playlist_title)
|
||||||
|
"""
|
||||||
|
known_environments = [
|
||||||
|
'Weave',
|
||||||
|
'Pyro',
|
||||||
|
'EDM',
|
||||||
|
'TheSecond',
|
||||||
|
'Lizzo',
|
||||||
|
'TheWeeknd',
|
||||||
|
'RockMixtape',
|
||||||
|
'Dragons2',
|
||||||
|
'Panic2',
|
||||||
|
'Queen',
|
||||||
|
'LinkinPark2',
|
||||||
|
'TheRollingStones',
|
||||||
|
'Lattice',
|
||||||
|
'DaftPunk',
|
||||||
|
'HipHop',
|
||||||
|
'Collider',
|
||||||
|
'Britney',
|
||||||
|
'Monstercat2',
|
||||||
|
'Metallica',
|
||||||
|
]
|
||||||
|
playlists: Dict[str, Tuple[List[Dict[str, Any]], str]] = {}
|
||||||
|
history = load_history()
|
||||||
|
history.setdefault('beatsaver_environment', {})
|
||||||
|
history.setdefault('playlist_counts', {})
|
||||||
|
|
||||||
|
for environment in known_environments:
|
||||||
|
if environment not in history['beatsaver_environment']:
|
||||||
|
history['beatsaver_environment'][environment] = {}
|
||||||
|
|
||||||
|
count_key = f"beatsaver_environment-{environment}"
|
||||||
|
current_count = history['playlist_counts'].get(count_key, 0)
|
||||||
|
new_count = current_count + 1
|
||||||
|
history['playlist_counts'][count_key] = new_count
|
||||||
|
|
||||||
|
maps = api.get_maps_by_environment(
|
||||||
|
environment_name=environment,
|
||||||
|
max_pages=max_pages,
|
||||||
|
use_cache=True
|
||||||
|
)
|
||||||
|
if not maps:
|
||||||
|
logging.error(f"No maps found for environment: {environment}.")
|
||||||
|
playlists[environment] = ([], f"{environment.lower()}-{new_count:02d}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
playlist_data: List[Dict[str, Any]] = []
|
||||||
|
for song in maps:
|
||||||
|
song_hash = song.get('hash')
|
||||||
|
if song_hash in history['beatsaver_environment'][environment]:
|
||||||
|
logging.debug(f"Skipping song {song_hash} for environment {environment} as it is in history.")
|
||||||
|
continue
|
||||||
|
|
||||||
|
playlist_data.append(song)
|
||||||
|
logging.info(f"Song added: {song.get('songName', 'Unknown')} from environment {environment}")
|
||||||
|
history['beatsaver_environment'][environment][song_hash] = True
|
||||||
|
|
||||||
|
if len(playlist_data) >= song_count:
|
||||||
|
break
|
||||||
|
|
||||||
|
if not playlist_data:
|
||||||
|
logging.info(f"No new songs found for environment {environment} based on history.")
|
||||||
|
else:
|
||||||
|
logging.info(f"Total songs added for environment {environment}: {len(playlist_data)}")
|
||||||
|
|
||||||
|
playlist_title = f"{environment.lower()}-{new_count:02d}"
|
||||||
|
playlists[environment] = (playlist_data, playlist_title)
|
||||||
|
|
||||||
|
save_history(history)
|
||||||
|
return playlists
|
||||||
|
@ -53,12 +53,22 @@ def prompt_for_player_id(default_id: str = '76561199407393962') -> str:
|
|||||||
|
|
||||||
def prompt_for_mapper_ids() -> List[int]:
|
def prompt_for_mapper_ids() -> List[int]:
|
||||||
default_mapper_ids = [
|
default_mapper_ids = [
|
||||||
4285547, # Avexus
|
29945, # Uncouth
|
||||||
4330286, # VoltageO
|
104443, # Rxerti
|
||||||
4294118, # Spinvvy
|
113133, # Cush
|
||||||
|
120215, # Jonas_0_0
|
||||||
|
145971, # Bellus
|
||||||
|
202784, # Najoko
|
||||||
4284542, # PogU (ForsenCDPogU)
|
4284542, # PogU (ForsenCDPogU)
|
||||||
|
4284220, # Z-ANESaber
|
||||||
|
4284904, # xScaramouche
|
||||||
|
4285547, # Avexus
|
||||||
4285738, # Lekrkoekj
|
4285738, # Lekrkoekj
|
||||||
113133 # Cush
|
4287112, # Jabob
|
||||||
|
4293090, # Kassi
|
||||||
|
4294118, # Spinvvy
|
||||||
|
4326041, # minsiii
|
||||||
|
4330286, # VoltageO
|
||||||
]
|
]
|
||||||
prompt = f"Enter mapper IDs (Default: {default_mapper_ids}): "
|
prompt = f"Enter mapper IDs (Default: {default_mapper_ids}): "
|
||||||
mapper_ids = input(prompt).strip() or ",".join(map(str, default_mapper_ids))
|
mapper_ids = input(prompt).strip() or ",".join(map(str, default_mapper_ids))
|
||||||
|
Loading…
x
Reference in New Issue
Block a user