Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
saberio.egg-info/
pysaber/__pycache__
pysaber/utils/__pycache__
.cache
pysaber/utils/config.json
60 changes: 42 additions & 18 deletions pysaber/saberio.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
from argparse import ArgumentParser
from os import mkdir, path
from os import makedirs, path

from colorifix.colorifix import paint, ppaint
from pymortafix.utils import multisub, strict_input
from pysaber.utils.helpers import (SPINNER, download_song, get_info_by_code,
look_for_code, search_songs, songs_table)
look_for_code, search_songs, songs_table,
bookmark_song)
from pysaber.utils.spotify import(retrieve_params, get_spotify_songs)


def argparsing():
Expand All @@ -29,6 +31,16 @@ def argparsing():
search.add_argument(
"-f", "--file", type=str, help="text file with a songs list", metavar=("SONG")
)
bookmark = parser.add_argument_group()
bookmark.add_argument(
"-c", "--cookie", type=str, help="cookie for bookmarking."
)
bookmark.add_argument(
'-b', action="store_true", help="bookmark all songs searched from BeastSaber."
)
bookmark.add_argument(
"-o", action="store_true", help="bookmark songs without downloading."
)
mode = parser.add_mutually_exclusive_group()
mode.add_argument(
"--auto",
Expand All @@ -55,7 +67,15 @@ def argparsing():

def main():
args = argparsing()
spotify_playlist_link, playlist_name = retrieve_params(args)

# default
path_to_folder = args.dir and path.dirname(path.join((args.dir or "."), playlist_name)) or '.'
playlist_name = args.p or "songs"
automatic = not args.list or args.auto or args.test
is_test = args.test or False
mode_name = (is_test and "test") or (automatic and "auto") or "list"

# check
if args.dir and not path.exists(path.join(args.dir)):
ppaint(f"[#red]Path [@underline]{args.dir}[/@] doesn't exist![/]")
Expand All @@ -64,18 +84,6 @@ def main():
ppaint(f"[#red]File [@underline]{args.file}[/@] doesn't exist![/]")
exit(-1)

# default
path_to_folder = args.dir or "."
playlist_name = args.p or "songs"
automatic = not args.list or args.auto or args.test
is_test = args.test or False
mode_name = (is_test and "test") or (automatic and "auto") or "list"
ppaint(
f"> Folder: [#gray @bold]{path_to_folder}[/]\n"
f"> Playlist: [#gray @bold]{playlist_name}[/]\n"
f"> Mode: [#gray @bold]{mode_name}[/]"
)

# param: songs
songs = list()
if args.file:
Expand All @@ -86,8 +94,18 @@ def main():
elif args.song:
songs = [look_for_code(args.song)]
ppaint(f"> Search: [#gray @bold]{args.song}[/]")
if not is_test and not path.exists(path.join(path_to_folder, playlist_name)):
mkdir(path.join(path_to_folder, playlist_name))
elif spotify_playlist_link != None:
songs = [(None, f"{track} {artist}") for track, artist in get_spotify_songs(spotify_playlist_link)]


ppaint(
f"> Folder: [#gray @bold]{path_to_folder}[/]\n"
f"> Playlist: [#gray @bold]{playlist_name}[/]\n"
f"> Mode: [#gray @bold]{mode_name}[/]"
)

if not is_test and not args.o and not path.exists(path.join(path_to_folder, playlist_name)):
makedirs(path.join(path_to_folder, playlist_name))
print()

# searching
Expand All @@ -98,18 +116,24 @@ def main():
SPINNER.succeed(paint(f"Search complete for [#blue]{song_more}[/]"))
if bsaber_songs:
bsaber_songs = sorted(
bsaber_songs, key=lambda x: (-x[4] + 1) / (x[5] + 1)
bsaber_songs, key=lambda x: (-x[5] + 1) / (x[6] + 1)
)
n = 1
if not automatic:
print(songs_table(bsaber_songs))
n = strict_input(
paint("> Choose a song: [@underline][0:skip][/] "),
choices=list(map(str, range(len(bsaber_songs) + 1))),
flush=True,
flush=True
)
if int(n) > 0:
song_to_download = bsaber_songs[int(n) - 1]
if args.cookie and (args.b or strict_input(
paint('> Whether to add to the bookmark? [Y/n]'),
choices=['y', 'n', 'Y', 'N', ''],
flush=True) in ('y', 'Y', '')):
bookmark_song(song_to_download[3], song_to_download[1], args.cookie)
if args.o: continue
else:
SPINNER.fail(paint(f"Skipped [#blue]{song_more}[/]"))
else:
Expand Down
60 changes: 38 additions & 22 deletions pysaber/utils/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,38 +4,31 @@
from bs4 import BeautifulSoup as bs
from colorifix.colorifix import paint
from halo import Halo
from requests import get
from requests import get, post
from tabulate import tabulate

SPINNER = Halo()
MAX_CHAR = 45

# ---- Utils


def lines_splitting(name):
if len(name) <= MAX_CHAR:
return paint(f"[#white]{name}[/]")
return paint(f"[#white]{name[:MAX_CHAR]}[/]\n{lines_splitting(name[MAX_CHAR:])}")


def songs_table(songs):
fields = ["", "Code", "Song", "Mapper", "Up", "Down", "Difficulty", "Date"]
headers = [paint(f"[@bold]{field}[/]") for field in fields]
entries = [
(
i,
paint(f"[#magenta]{code}[/]"),
lines_splitting(name),
paint(name),
paint(f"[#blue]{mapper}[/]"),
paint(f"[#green]{up}[/]"),
paint(f"[#red]{down}[/]"),
paint(f"[#yellow]{diff}[/]"),
paint(f"[#gray]{date:%d.%m.%Y}[/]"),
)
for i, (code, name, _, diff, up, down, mapper, date) in enumerate(songs, 1)
for i, (code, name, _, _, diff, up, down, mapper, date) in enumerate(songs, 1)
]
return tabulate(entries, headers=headers, tablefmt="fancy_grid")
return tabulate(entries, headers=headers, tablefmt="fancy_grid", maxcolwidths=[None, None, 45])


def look_for_code(song):
Expand All @@ -51,29 +44,28 @@ def search_songs(query):
SPINNER.start(paint(f"Searching for [#blue]{query}[/]"))
query = sub(r"\s+", "+", query)
url = f"https://bsaber.com/?s={query}&orderby=relevance&order=DESC"
songs_html = bs(get(url).text, "html.parser").findAll("div", {"class": "row"})
songs_html = bs(get(url).text, "html.parser").find_all("div", {"class": "row"})
return [info for song in songs_html if (info := get_info(song))]


def get_info(song):
if not song.header:
return None
if song.find("div", {"class": ["widget", "small-2", "subfooter-menu-holder"]}):
return None
header = song.find("header")
sanitize = lambda string: sub("\n", "", string).strip()
code = (m := search(r"songs/(\w+)/", header.find("a").get("href"))) and m.group(1)
title = sanitize(header.text)
difficulties_html = song.findAll("a", {"class": "post-difficulty"})
code = song.header.a.get("href").split('/')[-2]
title = sanitize(song.header.text)
difficulties_html = song.find_all("a", {"class": "post-difficulty"})
trunc_difficulty = lambda df: df == "Expert+" and "Ex+" or df[:2]
difficulties = ", ".join(trunc_difficulty(df.text) for df in difficulties_html)
stats_html = song.findAll("span", {"class": "post-stat"})[1:]
stats = [int(sanitize(s.text)) for s in stats_html]
upvote, downvote = stats or (0, 0)
upvote = int(song.find("i", class_="fa fa-thumbs-up fa-fw").next_sibling)
downvote = int(song.find("i", class_="fa fa-thumbs-down fa-fw").next_sibling)
mapper_html = song.find("div", {"class": "post-bottom-meta post-mapper-id-meta"})
mapper = search(r"\n+(\w+)", mapper_html.text or "").group(1)
date = datetime.fromisoformat(song.find("time").get("content"))
dwn_link = (link := song.find("a", {"class": "-download-zip"})) and link.get("href")
return code, title, dwn_link, difficulties, upvote, downvote, mapper, date

bookmark_id = (id := song.find("a", {"class": "-bookmark"})) and id.get("data-id")
return code, title, dwn_link, bookmark_id, difficulties, upvote, downvote, mapper, date

def get_info_by_code(code):
soup = bs(get(f"https://bsaber.com/songs/{code}").text, "html.parser")
Expand All @@ -90,3 +82,27 @@ def download_song(song_name, link, filename):
zip_song = get(link)
open(filename, "wb").write(zip_song.content)
SPINNER.succeed(paint(f"Downloaded [#blue]{song_name}[/]"))

def bookmark_song(id, song_name, cookie):
if '200' in (ret := str(post("https://bsaber.com/wp-admin/admin-ajax.php",
headers={
"Host": "bsaber.com",
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/110.0",
"Accept": "*/*",
"Accept-Language": "en-US,en;q=0.5",
"Accept-Encoding": "gzip, deflate, br",
"Referer": "https://bsaber.com/?s=halo&orderby=relevance&order=DESC",
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
"X-Requested-With": "XMLHttpRequest",
"Content-Length": "61",
"Origin": "https://bsaber.com",
"Connection": "keep-alive",
"Cookie": cookie,
"Sec-Fetch-Dest": "empty",
"Sec-Fetch-Mode": "no-cors",
"Sec-Fetch-Site": "same-origin",
}, data={"action": "bsaber_bookmark_post",
"type": "add_bookmark",
"post_id": id,
}))): SPINNER.succeed(paint(f"Bookmarked [#blue]{song_name}[/]"))
else: SPINNER.fail(paint(f"Bookmark failed [#blue]{id}[/] [#yellow]{song_name}[/] [#red]{ret.strip('<Response []>')}[/]"))
117 changes: 117 additions & 0 deletions pysaber/utils/spotify.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
from re import search
from spotipy import Spotify
from spotipy.oauth2 import SpotifyClientCredentials, SpotifyOauthError
from colorifix.colorifix import paint
from pymortafix.utils import strict_input
from json import dump, load
from os import path
from halo import Halo
SPINNER = Halo()


# spotify config
def create_config():
paint("To get a key, go to [#magenta]https://developer.spotify.com/dashboard/applications[/] and create a new application.")
client_id = input("Client ID: ")
secret_id = input("Secret ID: ")
save_config(client_id=client_id, secret_id=secret_id)
try:
while True:
try:
client = Spotify(
client_credentials_manager=SpotifyClientCredentials(
client_id, secret_id
)
)
client.search("test")
SPINNER.succeed("Configuration successful!")
raise StopIteration
except SpotifyOauthError:
SPINNER.fail("Configuration failed.")
create_config()
except StopIteration:
pass

def get_config():
while True:
try:
m = load(open(f"{path.abspath(path.dirname(__file__))}/config.json", "r"))
return m
except FileNotFoundError: create_config()


def save_config(**kargs):
f = f"{path.abspath(path.dirname(__file__))}/config.json"
keys = kargs
if path.exists(f): keys = get_config() | kargs
dump(
keys, open(f, "w"), indent=4
)


def sanitize_song_name(name):
return (m := search(r"^([^\(,\-\[\.]+)", name)) and m.group(1).strip()


def get_spotify_songs(playlist_link):
config = get_config()
while not search("https://open.spotify.com/playlist/", playlist_link):
playlist_link = input(
paint("[#red]Bad link![/] [#white]Retry[/]: ")
)
while True:
try:
client = Spotify(
client_credentials_manager=SpotifyClientCredentials(
config.get("client_id"), config.get("secret_id")
)
)
playlist = client.playlist(playlist_link)
break
except SpotifyOauthError:
SPINNER.fail(paint("[#red]Error![/] Please update the configuration once."))
create_config()
return [
(
sanitize_song_name(song.get("track").get("name")), # Track
song.get("track").get("artists")[0].get("name"), # Artist
)
for song in playlist.get("tracks").get("items")
]


def retrieve_params(args):
spotify_playlist_link = None
if not args.file and not args.song:
if (m := input(paint("[#cyan]Type[/] the song or the playlist path right here. [#cyan]Or Press[/] [#green]ENTER[/] to enter spotify playlist link. ❯ "))) == '':
spotify_playlist_link = input(
paint("> [#green]Spotify[/] playlist link: ")
)
elif path.exists(m): args.file = m
else: args.song = m
if not (args.auto and args.list and args.test and args.c):
while (
choice := input(
paint("> Choose mode: [[#red]auto[/]|[#green]list[/]|[#magenta]test[/][#yellow]|[/][#red][auto][/] [#blue]bookmark[/] [#cyan][and download][/]]. ❯ ")
).lower()
) not in ("auto", "list", "test") + (b := ("bookmark", "auto bookmark",
"bookmark and download",
"auto bookmark and download")):
pass
if choice in b and not args.cookie:
if type(get_config().get("cookie")) != str:
args.cookie = strict_input(paint("Cookie from [#blue]B[/]saber.com\n ❯ "), wrong_text='', flush= True)
save_config(cookie=args.cookie)
else:
args.cookie = get_config()['cookie']
args.auto = args.b = (choice == "auto"
or choice == "auto bookmark"
or choice == "auto bookmark and download"
or choice == "test")
args.test = choice == "test"
args.list = (choice == "list" or choice == "bookmark" or choice == "bookmark and download")
args.o = (choice == "bookmark" or choice == "auto bookmark")
if not args.p and not args.test and not args.o:
playlist_name = args.p = input(paint("> Choose a name for the playlist. (songs)❯ "))
else: playlist_name = args.p
return spotify_playlist_link, playlist_name
2 changes: 2 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,5 @@ spinners==0.0.24
tabulate==0.8.10
termcolor==2.0.1
urllib3==1.26.12
wcwidth==0.2.6
spotipy==2.22.1
4 changes: 3 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
"pymortafix == 0.2.2",
"halo == 0.0.31",
"tabulate == 0.8.10",
"wcwidth == 0.2.6",
"spotipy== 2.22.1",
],
classifiers=[
"Programming Language :: Python :: 3.8",
Expand All @@ -27,7 +29,7 @@
"Development Status :: 3 - Alpha",
"Intended Audience :: Developers",
],
python_requires=">=3.8",
python_requires=">=3.9",
keywords=["beat saber", "bsaber", "beat saver"],
package_data={"pysaber": ["utils/helpers.py"]},
entry_points={"console_scripts": ["saberio=pysaber.saberio:main"]},
Expand Down