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
+21 -135
View File
@@ -2,60 +2,33 @@
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]:
def __init__(self) -> None:
# resolve config path
self.handle_config_path() # 1
# handles that exit
self.handle_password_recovery()
self.handle_build()
self.handle_help()
self.handle_version()
# non-exiting handles
self.handle_host()
self.handle_port()
self.handle_periodic_scan()
self.handle_periodic_scan_interval()
@staticmethod
def handle_build():
"""
Runs Pyinstaller.
"""
if ALLARGS.build not in ARGS:
return return
if settings.IS_BUILD: if settings.IS_BUILD:
print("Do the cha cha slide instead!") click.echo("Can't build the project. Exiting ...")
print("https://www.youtube.com/watch?v=wZv62ShoStY")
sys.exit(0) sys.exit(0)
info_keys = [ info_keys = [
@@ -70,7 +43,9 @@ class ProcessArgs:
value = settings.Info.get(key) value = settings.Info.get(key)
if not value: if not value:
log.error(f"WARNING: {key} not resolved. Exiting ...") log.error(
f"WARNING: {key} not resolved. Can't build the project. Exiting ..."
)
sys.exit(0) sys.exit(0)
lines.append(f'{key} = "{value}"\n') lines.append(f'{key} = "{value}"\n')
@@ -87,7 +62,7 @@ class ProcessArgs:
bundler.run( bundler.run(
[ [
"manage.py", "main.py",
"--onefile", "--onefile",
"--name", "--name",
"swingmusic", "swingmusic",
@@ -107,108 +82,14 @@ class ProcessArgs:
sys.exit(0) 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: def handle_password_reset(*args, **kwargs):
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. Handles the --password-reset argument. Resets the password.
""" """
if ALLARGS.config in ARGS: if not args[2]:
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 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)
@staticmethod
def handle_password_recovery():
if ALLARGS.pswd in ARGS:
print("SWING MUSIC v2.0.0 ")
print("PASSWORD RECOVERY \n")
setup_sqlite() setup_sqlite()
username: str = "" username: str = ""
@@ -218,23 +99,28 @@ class ProcessArgs:
try: try:
username = input("Enter username: ") username = input("Enter username: ")
except KeyboardInterrupt: except KeyboardInterrupt:
print("\nOperation cancelled! Exiting ...") click.echo("\nOperation cancelled! Exiting ...")
sys.exit(0) sys.exit(0)
username = username.strip() username = username.strip()
user = UserTable.get_by_username(username) user = UserTable.get_by_username(username)
if not user: if not user:
print(f"User {username} not found") click.echo(f"User {username} not found")
sys.exit(0) sys.exit(0)
# collect password # collect password
try: try:
password = getpass("Enter new password: ") password = getpass("Enter new password: ")
except KeyboardInterrupt: except KeyboardInterrupt:
print("\nOperation cancelled! Exiting ...") click.echo("\nOperation cancelled! Exiting ...")
sys.exit(0) sys.exit(0)
try:
UserTable.update_one({"id": user.id, "password": hash_password(password)}) 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) 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()
+19 -72
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,18 +18,20 @@ 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
def run_app(host: str, port: int, config: Path):
settings.Paths.set_config_dir(config)
# Load mimetypes for the web client's static files # Load mimetypes for the web client's static files
# Loading mimetypes should happen automatically but # Loading mimetypes should happen automatically but
# sometimes the mimetypes are not loaded correctly # sometimes the mimetypes are not loaded correctly
@@ -60,41 +59,20 @@ mimetypes.add_type("application/manifest+json", ".webmanifest")
waitress_logger = logging.getLogger("waitress") waitress_logger = logging.getLogger("waitress")
waitress_logger.setLevel(logging.ERROR) waitress_logger.setLevel(logging.ERROR)
# # logging.basicConfig() log_startup_info(host, port)
# logging.getLogger("sqlalchemy.engine").setLevel(logging.ERROR)
# Background tasks
# @background
def bg_run_setup():
IndexEverything()
# @background
# def start_watchdog():
# WatchDog().run()
@background @background
def run_swingmusic(): def run_swingmusic():
log_startup_info()
register_plugins() register_plugins()
# start_watchdog() setproctitle.setproctitle(f"swingmusic {host}:{port}")
setproctitle.setproctitle(f"swingmusic ::{FLASKVARS.get_flask_port()}")
# bg_run_setup()
start_cron_jobs() start_cron_jobs()
# Setup function calls # Setup function calls
Info.load() settings.Info.load()
ProcessArgs()
run_setup() run_setup()
# Create the Flask app # Create the Flask app
app = create_api() app = create_api()
app.static_folder = get_home_res_path("client") app.static_folder = get_home_res_path("client")
@@ -109,7 +87,6 @@ whitelisted_routes = {
} }
blacklist_extensions = {".webp", ".jpg"}.union(getClientFilesExtensions()) blacklist_extensions = {".webp", ".jpg"}.union(getClientFilesExtensions())
def skipAuthAction(): def skipAuthAction():
""" """
Skips the JWT verification for the current request. Skips the JWT verification for the current request.
@@ -125,7 +102,6 @@ def skipAuthAction():
return False return False
@app.before_request @app.before_request
def verify_auth(): def verify_auth():
""" """
@@ -136,7 +112,6 @@ def verify_auth():
verify_jwt_in_request() verify_jwt_in_request()
@app.after_request @app.after_request
def refresh_expiring_jwt(response: Response): def refresh_expiring_jwt(response: Response):
""" """
@@ -161,7 +136,6 @@ def refresh_expiring_jwt(response: Response):
except (RuntimeError, KeyError): except (RuntimeError, KeyError):
return response return response
@app.route("/<path:path>") @app.route("/<path:path>")
def serve_client_files(path: str): def serve_client_files(path: str):
""" """
@@ -177,7 +151,9 @@ def serve_client_files(path: str):
# INFO: Safari doesn't support gzip encoding # INFO: Safari doesn't support gzip encoding
# See issue: https://github.com/swingmx/swingmusic/issues/155 # See issue: https://github.com/swingmx/swingmusic/issues/155
is_safari = ( is_safari = (
user_agent and user_agent.find("Safari") >= 0 and user_agent.find("Chrome") < 0 user_agent
and user_agent.find("Safari") >= 0
and user_agent.find("Chrome") < 0
) )
if is_safari: if is_safari:
@@ -193,7 +169,6 @@ def serve_client_files(path: str):
return app.send_static_file(path) return app.send_static_file(path)
@app.route("/") @app.route("/")
def serve_client(): def serve_client():
""" """
@@ -201,43 +176,17 @@ def serve_client():
""" """
return app.send_static_file("index.html") return app.send_static_file("index.html")
prev_memory = 0
# INFO: For debugging memory usage
# @app.after_request
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())
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)
print(
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"
)
return response
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
bjoern.run(app, host, port)
except ImportError:
import waitress
waitress.serve( waitress.serve(
app, app,
@@ -247,5 +196,3 @@ if __name__ == "__main__":
ipv6=True, ipv6=True,
ipv4=True, ipv4=True,
) )
# app.run(host=host, port=port, debug=False)
# bjoern.run(app, host, port)
+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