mirror of
https://github.com/Dvorinka/swingmusic-extended.git
synced 2026-06-04 20:43:04 +00:00
rewrite options with click
+ fix cpu count multiprocessing errors
This commit is contained in:
+89
-203
@@ -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)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
@@ -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(
|
||||
|
||||
@@ -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
@@ -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
@@ -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")
|
||||
|
||||
@@ -114,6 +114,8 @@ class TrackStore:
|
||||
else:
|
||||
cls.trackhashmap[track.trackhash].append(track)
|
||||
|
||||
print("Done!")
|
||||
|
||||
@classmethod
|
||||
def add_track(cls, track: Track):
|
||||
"""
|
||||
|
||||
@@ -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]]:
|
||||
"""
|
||||
|
||||
Reference in New Issue
Block a user