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.
"""
from getpass import getpass
import os.path
import sys
from getpass import getpass
import click
import PyInstaller.__main__ as bundler
from app import settings
from app.config import UserConfig
from app.db.userdata import UserTable
from app.logger import log
from app.print_help import HELP_MESSAGE
from app.setup.sqlite import setup_sqlite
from app.utils.auth import hash_password
from app.utils.paths import getFlaskOpenApiPath
from app.utils.xdg_utils import get_xdg_config_dir
from app.utils.wintools import is_windows
ALLARGS = settings.ALLARGS
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:
# resolve config path
self.handle_config_path() # 1
if settings.IS_BUILD:
click.echo("Can't build the project. Exiting ...")
sys.exit(0)
# handles that exit
self.handle_password_recovery()
self.handle_build()
self.handle_help()
self.handle_version()
info_keys = [
"SWINGMUSIC_APP_VERSION",
"GIT_LATEST_COMMIT_HASH",
"GIT_CURRENT_BRANCH",
]
# non-exiting handles
self.handle_host()
self.handle_port()
self.handle_periodic_scan()
self.handle_periodic_scan_interval()
lines = []
@staticmethod
def handle_build():
"""
Runs Pyinstaller.
"""
for key in info_keys:
value = settings.Info.get(key)
if ALLARGS.build not in ARGS:
return
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}"
if not value:
log.error(
f"WARNING: {key} not resolved. Can't build the project. Exiting ..."
)
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()
lines.append(f'{key} = "{value}"\n')
username: str = ""
password: str = ""
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)
# collect username
try:
username = input("Enter username: ")
except KeyboardInterrupt:
print("\nOperation cancelled! Exiting ...")
sys.exit(0)
_s = ";" if is_windows() else ":"
username = username.strip()
user = UserTable.get_by_username(username)
flask_openapi_path = getFlaskOpenApiPath()
if not user:
print(f"User {username} not found")
sys.exit(0)
bundler.run(
[
"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
try:
password = getpass("Enter new password: ")
except KeyboardInterrupt:
print("\nOperation cancelled! Exiting ...")
sys.exit(0)
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
num_workers = max(1, math.floor(multiprocessing.cpu_count() / 2))
print("num_workers", num_workers)
num_workers = max(1, os.cpu_count() // 2)
with ProcessPoolExecutor(max_workers=num_workers) as executor:
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.models import Folder
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.utils.wintools import win_replace_slash
# from app.db.libdata import TrackTable as TrackDB
def create_folder(path: str, trackcount=0) -> Folder:
"""
+5 -12
View File
@@ -22,6 +22,7 @@ from app.utils.progressbar import tqdm
from app.db.userdata import SimilarArtistTable
class CordinateMedia:
"""
Cordinates the extracting of thumbnails
@@ -64,22 +65,14 @@ def get_image(album: Album):
:return: None
"""
matching_tracks = AlbumStore.get_album_tracks(album.albumhash)
for track in matching_tracks:
extracted = extract_thumb(track.filepath, track.albumhash + ".webp")
if extracted:
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:
"""
Extracts the album art from all albums in album store.
@@ -93,7 +86,7 @@ class ProcessTrackThumbnails:
if platform.system() == "Linux":
# INFO: Processess are forked with access to global stores
# 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:
results = list(
tqdm(
@@ -160,7 +153,7 @@ class FetchSimilarArtistsLastFM:
)
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:
print("Processing similar artists")
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
from app import configs
from app.utils.filesystem import get_home_res_path
join = os.path.join
@@ -159,10 +160,6 @@ class Defaults:
API_CARD_LIMIT = 6
FILES = ["flac", "mp3", "wav", "m4a", "ogg", "wma", "opus", "alac", "aiff"]
SUPPORTED_FILES = tuple(f".{file}" for file in FILES)
# ===== SQLite =====
class DbPaths:
APP_DB_NAME = "swingmusic.db"
@@ -304,6 +301,15 @@ class Info:
"""
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_CURRENT_BRANCH = "<unset>"
+14 -18
View File
@@ -1,30 +1,26 @@
import os
from app.settings import FLASKVARS, TCOLOR, Info, Paths
from app.settings import TCOLOR, Info, Paths
from app.utils.network import get_ip
import click
def log_startup_info():
lines = "------------------------------"
def log_startup_info(host: str, port: int):
lines = "-"*30
# clears terminal 👇
# os.system("cls" if os.name == "nt" else "echo -e \\\\033c")
print(lines)
print(f"{TCOLOR.HEADER}Swing Music v{Info.SWINGMUSIC_APP_VERSION} {TCOLOR.ENDC}")
click.echo(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()
adresses = ["localhost"] + ([remote_ip] if remote_ip else [])
addresses.extend(["127.0.0.1"] + ([remote_ip] if remote_ip else []))
print("Started app on:")
for address in adresses:
# noinspection HttpUrlsUsage
print(
f"{TCOLOR.OKGREEN}http://{address}:{FLASKVARS.get_flask_port()}{TCOLOR.ENDC}"
click.echo("Server running on:\n")
for address in addresses:
click.echo(
f"{TCOLOR.OKGREEN}http://{address}:{port}{TCOLOR.ENDC}"
)
print(lines + "\n")
print(f"{TCOLOR.YELLOW}Data folder: {Paths.get_app_dir()}{TCOLOR.ENDC}")
click.echo("")
click.echo(f"{TCOLOR.YELLOW}Data folder: {Paths.get_app_dir()}{TCOLOR.ENDC}\n")
+2
View File
@@ -114,6 +114,8 @@ class TrackStore:
else:
cls.trackhashmap[track.trackhash].append(track)
print("Done!")
@classmethod
def add_track(cls, track: Track):
"""
+3 -1
View File
@@ -1,11 +1,13 @@
import os
from pathlib import Path
from app.settings import SUPPORTED_FILES
from app.utils.wintools import win_replace_slash
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]]:
"""