rewrite options with click

+ fix cpu count multiprocessing errors
This commit is contained in:
cwilvx
2025-03-23 23:54:13 +03:00
parent bea853fcd3
commit f891c28c2e
14 changed files with 344 additions and 465 deletions
+89 -203
View File
@@ -2,239 +2,125 @@
Handles arguments passed to the program. Handles arguments passed to the program.
""" """
from getpass import getpass
import os.path
import sys import sys
from getpass import getpass
import click
import PyInstaller.__main__ as bundler import PyInstaller.__main__ as bundler
from app import settings from app import settings
from app.config import UserConfig
from app.db.userdata import UserTable from app.db.userdata import UserTable
from app.logger import log from app.logger import log
from app.print_help import HELP_MESSAGE
from app.setup.sqlite import setup_sqlite from app.setup.sqlite import setup_sqlite
from app.utils.auth import hash_password from app.utils.auth import hash_password
from app.utils.paths import getFlaskOpenApiPath from app.utils.paths import getFlaskOpenApiPath
from app.utils.xdg_utils import get_xdg_config_dir
from app.utils.wintools import is_windows from app.utils.wintools import is_windows
ALLARGS = settings.ALLARGS ALLARGS = settings.ALLARGS
ARGS = sys.argv[1:] ARGS = sys.argv[1:]
class ProcessArgs: def handle_build(*args, **kwargs):
""" """
Processes the arguments passed to the program. Handles the --build argument. Builds the project into a single executable.
""" """
if not args[2]:
return
def __init__(self) -> None: if settings.IS_BUILD:
# resolve config path click.echo("Can't build the project. Exiting ...")
self.handle_config_path() # 1 sys.exit(0)
# handles that exit info_keys = [
self.handle_password_recovery() "SWINGMUSIC_APP_VERSION",
self.handle_build() "GIT_LATEST_COMMIT_HASH",
self.handle_help() "GIT_CURRENT_BRANCH",
self.handle_version() ]
# non-exiting handles lines = []
self.handle_host()
self.handle_port()
self.handle_periodic_scan()
self.handle_periodic_scan_interval()
@staticmethod for key in info_keys:
def handle_build(): value = settings.Info.get(key)
"""
Runs Pyinstaller.
"""
if ALLARGS.build not in ARGS: if not value:
return log.error(
f"WARNING: {key} not resolved. Can't build the project. Exiting ..."
if settings.IS_BUILD:
print("Do the cha cha slide instead!")
print("https://www.youtube.com/watch?v=wZv62ShoStY")
sys.exit(0)
info_keys = [
"SWINGMUSIC_APP_VERSION",
"GIT_LATEST_COMMIT_HASH",
"GIT_CURRENT_BRANCH",
]
lines = []
for key in info_keys:
value = settings.Info.get(key)
if not value:
log.error(f"WARNING: {key} not resolved. Exiting ...")
sys.exit(0)
lines.append(f'{key} = "{value}"\n')
try:
# write the info to the config file
with open("./app/configs.py", "w", encoding="utf-8") as file:
# copy the api keys to the config file
file.writelines(lines)
_s = ";" if is_windows() else ":"
flask_openapi_path = getFlaskOpenApiPath()
bundler.run(
[
"manage.py",
"--onefile",
"--name",
"swingmusic",
"--clean",
f"--add-data=assets{_s}assets",
f"--add-data=client{_s}client",
f"--add-data={flask_openapi_path}/templates/static{_s}flask_openapi3/templates/static",
f"--icon=assets/logo-fill.light.ico",
"-y",
]
)
finally:
# revert and remove the api keys for dev mode
with open("./app/configs.py", "w", encoding="utf-8") as file:
lines = [f'{key} = ""\n' for key in info_keys]
file.writelines(lines)
sys.exit(0)
@staticmethod
def handle_port():
if ALLARGS.port in ARGS:
index = ARGS.index(ALLARGS.port)
try:
port = ARGS[index + 1]
except IndexError:
print("ERROR: Port not specified")
sys.exit(0)
try:
settings.FLASKVARS.FLASK_PORT = int(port) # type: ignore
except ValueError:
print("ERROR: Port should be a number")
sys.exit(0)
@staticmethod
def handle_host():
if ALLARGS.host in ARGS:
index = ARGS.index(ALLARGS.host)
try:
host = ARGS[index + 1]
except IndexError:
print("ERROR: Host not specified")
sys.exit(0)
settings.FLASKVARS.set_flask_host(host) # type: ignore
@staticmethod
def handle_config_path():
"""
Modifies the config path.
"""
if ALLARGS.config in ARGS:
index = ARGS.index(ALLARGS.config)
try:
config_path = ARGS[index + 1]
resolved = os.path.abspath(config_path)
if os.path.exists(resolved):
settings.Paths.set_config_dir(resolved)
return
log.warn(f"Config path {resolved} doesn't exist")
sys.exit(0)
except IndexError:
pass
settings.Paths.set_config_dir(get_xdg_config_dir())
@staticmethod
def handle_periodic_scan():
if any((a in ARGS for a in ALLARGS.no_periodic_scan)):
UserConfig().enablePeriodicScans = False
@staticmethod
def handle_periodic_scan_interval():
if any((a in ARGS for a in ALLARGS.periodic_scan_interval)):
index = [
ARGS.index(a) for a in ALLARGS.periodic_scan_interval if a in ARGS
][0]
try:
interval = ARGS[index + 1]
except IndexError:
print("ERROR: Interval not specified")
sys.exit(0)
try:
psi = int(interval)
except ValueError:
print("ERROR: Interval should be a number")
sys.exit(0)
if psi < 0:
print("WHAT ARE YOU TRYING?")
sys.exit(0)
UserConfig().scanInterval = psi
@staticmethod
def handle_help():
if any((a in ARGS for a in ALLARGS.help)):
print(HELP_MESSAGE)
sys.exit(0)
@staticmethod
def handle_version():
if any((a in ARGS for a in ALLARGS.version)):
print(f"VERSION: v{settings.Info.SWINGMUSIC_APP_VERSION}")
print(
f"COMMIT#: {settings.Info.GIT_CURRENT_BRANCH}/{settings.Info.GIT_LATEST_COMMIT_HASH}"
) )
sys.exit(0) sys.exit(0)
@staticmethod lines.append(f'{key} = "{value}"\n')
def handle_password_recovery():
if ALLARGS.pswd in ARGS:
print("SWING MUSIC v2.0.0 ")
print("PASSWORD RECOVERY \n")
setup_sqlite()
username: str = "" try:
password: str = "" # write the info to the config file
with open("./app/configs.py", "w", encoding="utf-8") as file:
# copy the api keys to the config file
file.writelines(lines)
# collect username _s = ";" if is_windows() else ":"
try:
username = input("Enter username: ")
except KeyboardInterrupt:
print("\nOperation cancelled! Exiting ...")
sys.exit(0)
username = username.strip() flask_openapi_path = getFlaskOpenApiPath()
user = UserTable.get_by_username(username)
if not user: bundler.run(
print(f"User {username} not found") [
sys.exit(0) "main.py",
"--onefile",
"--name",
"swingmusic",
"--clean",
f"--add-data=assets{_s}assets",
f"--add-data=client{_s}client",
f"--add-data={flask_openapi_path}/templates/static{_s}flask_openapi3/templates/static",
f"--icon=assets/logo-fill.light.ico",
"-y",
]
)
finally:
# revert and remove the api keys for dev mode
with open("./app/configs.py", "w", encoding="utf-8") as file:
lines = [f'{key} = ""\n' for key in info_keys]
file.writelines(lines)
# collect password sys.exit(0)
try:
password = getpass("Enter new password: ")
except KeyboardInterrupt:
print("\nOperation cancelled! Exiting ...")
sys.exit(0)
UserTable.update_one({"id": user.id, "password": hash_password(password)})
sys.exit(0) def handle_password_reset(*args, **kwargs):
"""
Handles the --password-reset argument. Resets the password.
"""
if not args[2]:
return
setup_sqlite()
username: str = ""
password: str = ""
# collect username
try:
username = input("Enter username: ")
except KeyboardInterrupt:
click.echo("\nOperation cancelled! Exiting ...")
sys.exit(0)
username = username.strip()
user = UserTable.get_by_username(username)
if not user:
click.echo(f"User {username} not found")
sys.exit(0)
# collect password
try:
password = getpass("Enter new password: ")
except KeyboardInterrupt:
click.echo("\nOperation cancelled! Exiting ...")
sys.exit(0)
try:
UserTable.update_one({"id": user.id, "password": hash_password(password)})
click.echo("Password reset successfully!")
except Exception as e:
click.echo(f"Error resetting password: {e}")
sys.exit(0)
sys.exit(0)
+1 -2
View File
@@ -157,8 +157,7 @@ class CheckArtistImages:
] ]
# Use number of CPU cores minus 1 to leave one core free for system processes # Use number of CPU cores minus 1 to leave one core free for system processes
num_workers = max(1, math.floor(multiprocessing.cpu_count() / 2)) num_workers = max(1, os.cpu_count() // 2)
print("num_workers", num_workers)
with ProcessPoolExecutor(max_workers=num_workers) as executor: with ProcessPoolExecutor(max_workers=num_workers) as executor:
res = list( res = list(
+1 -3
View File
@@ -5,12 +5,10 @@ from app.lib.sortlib import sort_folders, sort_tracks
from app.logger import log from app.logger import log
from app.models import Folder from app.models import Folder
from app.serializers.track import serialize_tracks from app.serializers.track import serialize_tracks
from app.settings import SUPPORTED_FILES from app.utils.filesystem import SUPPORTED_FILES
from app.store.folder import FolderStore from app.store.folder import FolderStore
from app.utils.wintools import win_replace_slash from app.utils.wintools import win_replace_slash
# from app.db.libdata import TrackTable as TrackDB
def create_folder(path: str, trackcount=0) -> Folder: def create_folder(path: str, trackcount=0) -> Folder:
""" """
+3 -10
View File
@@ -22,6 +22,7 @@ from app.utils.progressbar import tqdm
from app.db.userdata import SimilarArtistTable from app.db.userdata import SimilarArtistTable
class CordinateMedia: class CordinateMedia:
""" """
Cordinates the extracting of thumbnails Cordinates the extracting of thumbnails
@@ -72,14 +73,6 @@ def get_image(album: Album):
return return
def get_cpu_count():
"""
Returns the number of CPUs on the machine.
"""
cpu_count = os.cpu_count() or 0
return cpu_count // 2 if cpu_count > 2 else cpu_count
class ProcessTrackThumbnails: class ProcessTrackThumbnails:
""" """
Extracts the album art from all albums in album store. Extracts the album art from all albums in album store.
@@ -93,7 +86,7 @@ class ProcessTrackThumbnails:
if platform.system() == "Linux": if platform.system() == "Linux":
# INFO: Processess are forked with access to global stores # INFO: Processess are forked with access to global stores
# It's "safe" to use a process pool # It's "safe" to use a process pool
cpus = math.floor(get_cpu_count() / 2) cpus = max(1, os.cpu_count() // 2)
with ProcessPoolExecutor(max_workers=cpus) as executor: with ProcessPoolExecutor(max_workers=cpus) as executor:
results = list( results = list(
tqdm( tqdm(
@@ -160,7 +153,7 @@ class FetchSimilarArtistsLastFM:
) )
artists = list(artists) artists = list(artists)
with ProcessPoolExecutor(max_workers=get_cpu_count()) as executor: with ProcessPoolExecutor(max_workers=max(1, os.cpu_count() // 2)) as executor:
try: try:
print("Processing similar artists") print("Processing similar artists")
results = list( results = list(
-35
View File
@@ -1,35 +0,0 @@
from app.settings import ALLARGS, Info
from tabulate import tabulate
args = ALLARGS
help_args_list = [
["--help", "-h", "Show this help message"],
["--version", "-v", "Show the app version"],
["--host", "", "Set the host"],
["--port", "", "Set the port"],
["--config", "", "Set the config path"],
["--no-periodic-scan", "-nps", "Disable periodic scan"],
["--pswd", "", "Recover a password"],
[
"--scan-interval",
"-psi",
"Set the scan interval in seconds. Default 600s (10 minutes)",
],
[
"--build",
"",
"Build the application (in development)",
],
]
HELP_MESSAGE = f"""
Swing Music v{Info.SWINGMUSIC_APP_VERSION}
A beautiful, self-hosted music player for your local audio files.
Like Spotify ... but bring your own music.
Usage: ./swingmusic [options] [args]
{tabulate(help_args_list, headers=["Option", "Alias", "Description"], tablefmt="psql", maxcolwidths=[None, None, 40])}
"""
+10 -4
View File
@@ -7,6 +7,7 @@ import subprocess
import sys import sys
from app import configs from app import configs
from app.utils.filesystem import get_home_res_path
join = os.path.join join = os.path.join
@@ -159,10 +160,6 @@ class Defaults:
API_CARD_LIMIT = 6 API_CARD_LIMIT = 6
FILES = ["flac", "mp3", "wav", "m4a", "ogg", "wma", "opus", "alac", "aiff"]
SUPPORTED_FILES = tuple(f".{file}" for file in FILES)
# ===== SQLite ===== # ===== SQLite =====
class DbPaths: class DbPaths:
APP_DB_NAME = "swingmusic.db" APP_DB_NAME = "swingmusic.db"
@@ -304,6 +301,15 @@ class Info:
""" """
SWINGMUSIC_APP_VERSION = os.environ.get("SWINGMUSIC_APP_VERSION") SWINGMUSIC_APP_VERSION = os.environ.get("SWINGMUSIC_APP_VERSION")
if not SWINGMUSIC_APP_VERSION:
path = get_home_res_path("version.txt")
if not path:
raise ValueError("Version file not found")
with open(path, "r") as f:
SWINGMUSIC_APP_VERSION = f.read().strip()
GIT_LATEST_COMMIT_HASH = "<unset>" GIT_LATEST_COMMIT_HASH = "<unset>"
GIT_CURRENT_BRANCH = "<unset>" GIT_CURRENT_BRANCH = "<unset>"
+14 -18
View File
@@ -1,30 +1,26 @@
import os from app.settings import TCOLOR, Info, Paths
from app.settings import FLASKVARS, TCOLOR, Info, Paths
from app.utils.network import get_ip from app.utils.network import get_ip
import click
def log_startup_info(): def log_startup_info(host: str, port: int):
lines = "------------------------------" lines = "-"*30
# clears terminal 👇 # clears terminal 👇
# os.system("cls" if os.name == "nt" else "echo -e \\\\033c") # os.system("cls" if os.name == "nt" else "echo -e \\\\033c")
print(lines) click.echo(f"{TCOLOR.HEADER}Swing Music v{Info.SWINGMUSIC_APP_VERSION} {TCOLOR.ENDC}")
print(f"{TCOLOR.HEADER}Swing Music v{Info.SWINGMUSIC_APP_VERSION} {TCOLOR.ENDC}")
adresses = [FLASKVARS.get_flask_host()] addresses = [host]
if FLASKVARS.get_flask_host() == "0.0.0.0": if host == "0.0.0.0":
remote_ip = get_ip() remote_ip = get_ip()
adresses = ["localhost"] + ([remote_ip] if remote_ip else []) addresses.extend(["127.0.0.1"] + ([remote_ip] if remote_ip else []))
print("Started app on:") click.echo("Server running on:\n")
for address in adresses: for address in addresses:
# noinspection HttpUrlsUsage click.echo(
print( f"{TCOLOR.OKGREEN}http://{address}:{port}{TCOLOR.ENDC}"
f"{TCOLOR.OKGREEN}http://{address}:{FLASKVARS.get_flask_port()}{TCOLOR.ENDC}"
) )
print(lines + "\n") click.echo("")
click.echo(f"{TCOLOR.YELLOW}Data folder: {Paths.get_app_dir()}{TCOLOR.ENDC}\n")
print(f"{TCOLOR.YELLOW}Data folder: {Paths.get_app_dir()}{TCOLOR.ENDC}")
+2
View File
@@ -114,6 +114,8 @@ class TrackStore:
else: else:
cls.trackhashmap[track.trackhash].append(track) cls.trackhashmap[track.trackhash].append(track)
print("Done!")
@classmethod @classmethod
def add_track(cls, track: Track): def add_track(cls, track: Track):
""" """
+3 -1
View File
@@ -1,11 +1,13 @@
import os import os
from pathlib import Path from pathlib import Path
from app.settings import SUPPORTED_FILES
from app.utils.wintools import win_replace_slash from app.utils.wintools import win_replace_slash
CWD = Path(__file__).parent.resolve() CWD = Path(__file__).parent.resolve()
FILES = ["flac", "mp3", "wav", "m4a", "ogg", "wma", "opus", "alac", "aiff"]
SUPPORTED_FILES = tuple(f".{file}" for file in FILES)
def run_fast_scandir(_dir: str, full=False) -> tuple[list[str], list[str]]: def run_fast_scandir(_dir: str, full=False) -> tuple[list[str], list[str]]:
""" """
+74
View File
@@ -0,0 +1,74 @@
import multiprocessing
import pathlib
import click
import sys
from app.arg_handler import handle_build, handle_password_reset
from app.utils.filesystem import get_home_res_path
from app.utils.xdg_utils import get_xdg_config_dir
from manage import run_app
def version(*args, **kwargs):
if not args[2]:
return
path = get_home_res_path("version.txt")
if not path:
click.echo("Version file not found.")
sys.exit(1)
with open(path, "r") as f:
version = f.read()
click.echo(version)
sys.exit(0)
@click.command(options_metavar="<options>", context_settings={"show_default": True})
@click.option(
"--build",
default=False,
help="Build the project.",
is_eager=True,
callback=handle_build,
is_flag=True,
)
@click.option("--host", default="0.0.0.0", help="Host to run the app on.")
@click.option("--port", default=1970, help="HTTP port to run the app on.")
@click.option(
"--config",
default=lambda: get_xdg_config_dir(),
show_default="XDG_CONFIG_HOME",
help="Path to the config file.",
type=click.Path(
exists=True,
file_okay=False,
dir_okay=True,
writable=True,
resolve_path=True,
allow_dash=False,
path_type=pathlib.Path,
),
)
@click.option(
"--password-reset",
is_flag=True,
help="Reset the password.",
is_eager=True,
callback=handle_password_reset,
)
@click.option(
"--version",
is_flag=True,
default=False,
callback=version,
help="Show the version and exit",
is_eager=True,
)
def run(*args, **kwargs):
run_app(kwargs["host"], kwargs["port"], kwargs["config"])
if __name__ == "__main__":
multiprocessing.freeze_support()
run()
+134 -187
View File
@@ -2,14 +2,11 @@
This file is used to run the application. This file is used to run the application.
""" """
import logging
import os import os
import psutil import logging
import waitress
# import bjoern
import mimetypes import mimetypes
import setproctitle import setproctitle
import multiprocessing from pathlib import Path
from flask_jwt_extended import ( from flask_jwt_extended import (
create_access_token, create_access_token,
@@ -21,231 +18,181 @@ from flask_jwt_extended import (
from datetime import datetime, timezone from datetime import datetime, timezone
from flask import Response, request from flask import Response, request
from app import settings
from app.api import create_api from app.api import create_api
from app.crons import start_cron_jobs from app.crons import start_cron_jobs
from app.arg_handler import ProcessArgs
from app.lib.index import IndexEverything
from app.utils.threading import background from app.utils.threading import background
from app.setup import load_into_mem, run_setup from app.setup import load_into_mem, run_setup
from app.settings import FLASKVARS, TCOLOR, Info
from app.plugins.register import register_plugins from app.plugins.register import register_plugins
from app.start_info_logger import log_startup_info from app.start_info_logger import log_startup_info
from app.utils.filesystem import get_home_res_path from app.utils.filesystem import get_home_res_path
from app.utils.paths import getClientFilesExtensions from app.utils.paths import getClientFilesExtensions
# Load mimetypes for the web client's static files
# Loading mimetypes should happen automatically but
# sometimes the mimetypes are not loaded correctly
# eg. when the Registry is messed up on Windows.
# See the following issues: def run_app(host: str, port: int, config: Path):
# https://github.com/swingmx/swingmusic/issues/137 settings.Paths.set_config_dir(config)
mimetypes.add_type("text/css", ".css") # Load mimetypes for the web client's static files
mimetypes.add_type("text/javascript", ".js") # Loading mimetypes should happen automatically but
mimetypes.add_type("text/plain", ".txt") # sometimes the mimetypes are not loaded correctly
mimetypes.add_type("text/html", ".html") # eg. when the Registry is messed up on Windows.
mimetypes.add_type("image/webp", ".webp")
mimetypes.add_type("image/svg+xml", ".svg")
mimetypes.add_type("image/png", ".png")
mimetypes.add_type("image/vnd.microsoft.icon", ".ico")
mimetypes.add_type("image/gif", ".gif")
mimetypes.add_type("font/woff", ".woff")
mimetypes.add_type("application/manifest+json", ".webmanifest")
# logging.disable(logging.CRITICAL) # See the following issues:
# werkzeug = logging.getLogger("werkzeug") # https://github.com/swingmx/swingmusic/issues/137
# werkzeug.setLevel(logging.ERROR)
waitress_logger = logging.getLogger("waitress") mimetypes.add_type("text/css", ".css")
waitress_logger.setLevel(logging.ERROR) mimetypes.add_type("text/javascript", ".js")
mimetypes.add_type("text/plain", ".txt")
mimetypes.add_type("text/html", ".html")
mimetypes.add_type("image/webp", ".webp")
mimetypes.add_type("image/svg+xml", ".svg")
mimetypes.add_type("image/png", ".png")
mimetypes.add_type("image/vnd.microsoft.icon", ".ico")
mimetypes.add_type("image/gif", ".gif")
mimetypes.add_type("font/woff", ".woff")
mimetypes.add_type("application/manifest+json", ".webmanifest")
# # logging.basicConfig() # logging.disable(logging.CRITICAL)
# logging.getLogger("sqlalchemy.engine").setLevel(logging.ERROR) # werkzeug = logging.getLogger("werkzeug")
# werkzeug.setLevel(logging.ERROR)
waitress_logger = logging.getLogger("waitress")
waitress_logger.setLevel(logging.ERROR)
# Background tasks log_startup_info(host, port)
# @background
def bg_run_setup():
IndexEverything()
@background
def run_swingmusic():
register_plugins()
# @background setproctitle.setproctitle(f"swingmusic {host}:{port}")
# def start_watchdog(): start_cron_jobs()
# WatchDog().run()
# Setup function calls
settings.Info.load()
run_setup()
@background # Create the Flask app
def run_swingmusic(): app = create_api()
log_startup_info() app.static_folder = get_home_res_path("client")
register_plugins()
# start_watchdog() # INFO: Routes that don't need authentication
whitelisted_routes = {
"/auth/login",
"/auth/users",
"/auth/pair",
"/auth/logout",
"/auth/refresh",
"/docs",
}
blacklist_extensions = {".webp", ".jpg"}.union(getClientFilesExtensions())
setproctitle.setproctitle(f"swingmusic ::{FLASKVARS.get_flask_port()}") def skipAuthAction():
# bg_run_setup() """
start_cron_jobs() Skips the JWT verification for the current request.
"""
if request.path == "/" or any(
request.path.endswith(ext) for ext in blacklist_extensions
):
return True
# if request path starts with any of the blacklisted routes, don't verify jwt
if any(request.path.startswith(route) for route in whitelisted_routes):
return True
# Setup function calls return False
Info.load()
ProcessArgs()
run_setup()
@app.before_request
def verify_auth():
"""
Verifies the JWT token before each request.
"""
if skipAuthAction():
return
# Create the Flask app verify_jwt_in_request()
app = create_api() @app.after_request
app.static_folder = get_home_res_path("client") def refresh_expiring_jwt(response: Response):
"""
Refreshes the cookies JWT token after each request.
"""
# INFO: Routes that don't need authentication # INFO: If the request has an Authorization header, don't refresh the jwt
whitelisted_routes = { # Request is probably from the mobile client or a third party
"/auth/login", if skipAuthAction() or request.headers.get("Authorization"):
"/auth/users",
"/auth/pair",
"/auth/logout",
"/auth/refresh",
"/docs",
}
blacklist_extensions = {".webp", ".jpg"}.union(getClientFilesExtensions())
def skipAuthAction():
"""
Skips the JWT verification for the current request.
"""
if request.path == "/" or any(
request.path.endswith(ext) for ext in blacklist_extensions
):
return True
# if request path starts with any of the blacklisted routes, don't verify jwt
if any(request.path.startswith(route) for route in whitelisted_routes):
return True
return False
@app.before_request
def verify_auth():
"""
Verifies the JWT token before each request.
"""
if skipAuthAction():
return
verify_jwt_in_request()
@app.after_request
def refresh_expiring_jwt(response: Response):
"""
Refreshes the cookies JWT token after each request.
"""
# INFO: If the request has an Authorization header, don't refresh the jwt
# Request is probably from the mobile client or a third party
if skipAuthAction() or request.headers.get("Authorization"):
return response
try:
exp_timestamp = get_jwt()["exp"]
now = datetime.now(timezone.utc)
target_timestamp = datetime.timestamp(now) + 60 * 60 * 24 * 7 # 7 days
if target_timestamp > exp_timestamp:
access_token = create_access_token(identity=get_jwt_identity())
set_access_cookies(response, access_token)
return response
except (RuntimeError, KeyError):
return response
@app.route("/<path:path>")
def serve_client_files(path: str):
"""
Serves the static files in the client folder.
"""
js_or_css = path.endswith(".js") or path.endswith(".css")
if not js_or_css:
return app.send_static_file(path)
gzipped_path = path + ".gz"
user_agent = request.headers.get("User-Agent")
# INFO: Safari doesn't support gzip encoding
# See issue: https://github.com/swingmx/swingmusic/issues/155
is_safari = (
user_agent and user_agent.find("Safari") >= 0 and user_agent.find("Chrome") < 0
)
if is_safari:
return app.send_static_file(path)
accepts_gzip = request.headers.get("Accept-Encoding", "").find("gzip") >= 0
if accepts_gzip:
if os.path.exists(os.path.join(app.static_folder or "", gzipped_path)):
response = app.make_response(app.send_static_file(gzipped_path))
response.headers["Content-Encoding"] = "gzip"
return response return response
return app.send_static_file(path) try:
exp_timestamp = get_jwt()["exp"]
now = datetime.now(timezone.utc)
target_timestamp = datetime.timestamp(now) + 60 * 60 * 24 * 7 # 7 days
if target_timestamp > exp_timestamp:
access_token = create_access_token(identity=get_jwt_identity())
set_access_cookies(response, access_token)
@app.route("/") return response
def serve_client(): except (RuntimeError, KeyError):
""" return response
Serves the index.html file at `client/index.html`.
"""
return app.send_static_file("index.html")
@app.route("/<path:path>")
def serve_client_files(path: str):
"""
Serves the static files in the client folder.
"""
js_or_css = path.endswith(".js") or path.endswith(".css")
if not js_or_css:
return app.send_static_file(path)
prev_memory = 0 gzipped_path = path + ".gz"
user_agent = request.headers.get("User-Agent")
# INFO: Safari doesn't support gzip encoding
# See issue: https://github.com/swingmx/swingmusic/issues/155
is_safari = (
user_agent
and user_agent.find("Safari") >= 0
and user_agent.find("Chrome") < 0
)
# INFO: For debugging memory usage if is_safari:
# @app.after_request return app.send_static_file(path)
def print_memory_usage(response: Response):
# INFO: Ignore assets
if (
request.path.startswith("/img")
or request.path.endswith(".js")
or request.path.endswith(".css")
):
return response
process = psutil.Process(os.getpid()) accepts_gzip = request.headers.get("Accept-Encoding", "").find("gzip") >= 0
global prev_memory
current_mem = process.memory_info().rss
diff = (current_mem - prev_memory) / 1024**2
prev_memory = current_mem
# INFO: Print memory usage (highlights if diff is more than 0.1 MB) if accepts_gzip:
print( if os.path.exists(os.path.join(app.static_folder or "", gzipped_path)):
f"\n{request.path} | TOTAL: {current_mem/1024**2} MB | DIFF: {TCOLOR.FAIL if diff > 0.1 else ''}{diff} MB{TCOLOR.ENDC if diff > 0.1 else ''} \n" response = app.make_response(app.send_static_file(gzipped_path))
) response.headers["Content-Encoding"] = "gzip"
return response
return response return app.send_static_file(path)
@app.route("/")
def serve_client():
"""
Serves the index.html file at `client/index.html`.
"""
return app.send_static_file("index.html")
if __name__ == "__main__":
multiprocessing.freeze_support()
load_into_mem() load_into_mem()
run_swingmusic() run_swingmusic()
# TrackStore.export() # TrackStore.export()
# ArtistStore.export() # ArtistStore.export()
host = FLASKVARS.get_flask_host() try:
port = FLASKVARS.get_flask_port() import bjoern
waitress.serve( bjoern.run(app, host, port)
app, except ImportError:
host=host, import waitress
port=port,
threads=100, waitress.serve(
ipv6=True, app,
ipv4=True, host=host,
) port=port,
# app.run(host=host, port=port, debug=False) threads=100,
# bjoern.run(app, host, port) ipv6=True,
ipv4=True,
)
+1
View File
@@ -33,6 +33,7 @@ dependencies = [
"rapidfuzz==3.11.0", "rapidfuzz==3.11.0",
"waitress>=3.0.2", "waitress>=3.0.2",
"pendulum>=3.0.0", "pendulum>=3.0.0",
"bjoern>=3.2.2",
] ]
[dependency-groups] [dependency-groups]
Generated
+9
View File
@@ -1,4 +1,5 @@
version = 1 version = 1
revision = 1
requires-python = "==3.11.*" requires-python = "==3.11.*"
resolution-markers = [ resolution-markers = [
"platform_python_implementation != 'PyPy'", "platform_python_implementation != 'PyPy'",
@@ -23,6 +24,12 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 }, { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 },
] ]
[[package]]
name = "bjoern"
version = "3.2.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d5/a0/fba55eb58a502dabc0915137ff44eaacaba58a60196fd94dc8cd4e9fe67d/bjoern-3.2.2.tar.gz", hash = "sha256:16e5a02a9a17a7f5f8bea0d7c58650e78ab80ead6fe3e390037573d4355baf31", size = 45716 }
[[package]] [[package]]
name = "blinker" name = "blinker"
version = "1.9.0" version = "1.9.0"
@@ -826,6 +833,7 @@ name = "swingmusic"
version = "2.0.2" version = "2.0.2"
source = { virtual = "." } source = { virtual = "." }
dependencies = [ dependencies = [
{ name = "bjoern" },
{ name = "colorgram-py" }, { name = "colorgram-py" },
{ name = "ffmpeg-python" }, { name = "ffmpeg-python" },
{ name = "flask" }, { name = "flask" },
@@ -861,6 +869,7 @@ dev = [
[package.metadata] [package.metadata]
requires-dist = [ requires-dist = [
{ name = "bjoern", specifier = ">=3.2.2" },
{ name = "colorgram-py", specifier = ">=1.2.0" }, { name = "colorgram-py", specifier = ">=1.2.0" },
{ name = "ffmpeg-python", specifier = ">=0.2.0" }, { name = "ffmpeg-python", specifier = ">=0.2.0" },
{ name = "flask", specifier = ">=3.1.0" }, { name = "flask", specifier = ">=3.1.0" },
+1
View File
@@ -0,0 +1 @@
2.0.3