refactor Paths and update build workflow

+ rename app_dir -> config_dir and config_dir to config_parent
+ bundle web client as zip
+ bundle and extract client zip when running pyinstaller builds
+ installer pyinstaller as main dependency
+ remove fallback client flag
+ handle already used port
+ add assethandler class
+ remove some startup logs
+ ignore wheels and client.zip files
This commit is contained in:
cwilvx
2025-08-28 16:38:49 +03:00
parent e770606567
commit ebc740a5a5
15 changed files with 1037 additions and 955 deletions
+81 -35
View File
@@ -1,16 +1,41 @@
name: Build and Upload
name: New Release
run-name: Release v${{ github.event.inputs.tag }}
on:
workflow_dispatch:
release:
types:
- prereleased
- released
inputs:
tag:
description: "Version number"
required: true
default: "0.0.0"
binary_build:
description: "Build binaries"
required: true
default: "true"
type: choice
options:
- true
- false
is_latest:
description: "Set as latest"
required: true
default: "false"
type: choice
options:
- true
- false
build_docker:
description: "Build Docker image"
required: true
type: choice
default: "true"
options:
- true
- false
env:
PIP_USE_PEP517: true
jobs:
build-client:
runs-on: ubuntu-latest
name: Build client
@@ -18,7 +43,7 @@ jobs:
- name: Clone client
uses: actions/checkout@v4
with:
repository: 'swingmx/webclient'
repository: "swingmx/webclient"
path: swingmusic-client
- name: Setup Node 20
@@ -42,11 +67,12 @@ jobs:
with:
path: "client/"
compression-level: 0
name: 'client'
name: "client"
build-wheels:
name: Build wheels
runs-on: ubuntu-latest
needs: [build-client]
steps:
- name: Checkout swingmusic
@@ -54,9 +80,25 @@ jobs:
with:
fetch-depth: 0
- name: Download client artifact
uses: actions/download-artifact@v4
with:
name: client
path: client
- name: Compress client and copy to src/swingmusic/client.zip
run: |
zip -r client.zip client
rm -r client
cp client.zip src/swingmusic/client.zip
- uses: actions/setup-python@v5
with:
python-version: '3.11'
python-version: "3.11"
- name: Create git tag
run: |
git tag v${{ github.event.inputs.tag }}
- name: Build wheels
run: pip wheel . -w wheelhouse --no-deps
@@ -66,22 +108,23 @@ jobs:
# name: cibw-wheels-${{ matrix.os }}-${{ strategy.job-index }}
path: ./wheelhouse/*.whl
compression-level: 0
name: 'wheels'
name: "wheels"
build-appimage:
name: Build Appimage
runs-on: ${{ matrix.os }}
needs: [ build-client ]
needs: [build-client]
if: ${{ github.event.inputs.binary_build == 'true' }}
strategy:
fail-fast: false
matrix:
os: [ ubuntu-latest, ubuntu-24.04-arm]
os: [ubuntu-latest, ubuntu-24.04-arm]
steps:
- uses: actions/setup-python@v5
with:
python-version: '3.11'
python-version: "3.11"
- name: Install linux dependencies
run: sudo apt-get install libev-dev libfuse-dev -y > /dev/null
@@ -134,8 +177,9 @@ jobs:
docker:
name: Build and push Docker image
runs-on: ubuntu-latest
needs: [ build-client, build-wheels ]
needs: [build-client, build-wheels]
permissions: write-all
if: ${{ github.event.inputs.build_docker == 'true' }}
steps:
- name: Checkout into repo
@@ -166,32 +210,33 @@ jobs:
images: |
ghcr.io/${{ github.repository }}
- name: Determine if image should be uploaded
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
echo "UPLOAD=false" >> $GITHUB_ENV
else
echo "UPLOAD=true" >> $GITHUB_ENV
fi
- name: Build and push
uses: docker/build-push-action@v6
with:
context: .
platforms: linux/amd64, linux/arm64 #,linux/arm
push: ${{ env.UPLOAD }}
push: true
tags: ghcr.io/${{github.repository}}:${{format('{0}', github.ref_name)}}, ghcr.io/${{github.repository}}:latest
labels: org.opencontainers.image.title=Docker
build-pyinstaller:
name: Build binary on ${{ matrix.os }}
runs-on: ${{ matrix.os }}
needs: [ build-client, build-wheels ]
needs: [build-client, build-wheels]
if: ${{ github.event.inputs.binary_build == 'true' }}
strategy:
fail-fast: false
matrix:
os: [ ubuntu-latest, ubuntu-24.04-arm, windows-latest, windows-11-arm, macos-13, macos-latest ]
os:
[
ubuntu-latest,
ubuntu-24.04-arm,
windows-latest,
windows-11-arm,
macos-13,
macos-latest,
]
steps:
- name: Checkout swingmusic
@@ -212,6 +257,10 @@ jobs:
name: client
path: client
- name: Compress client
run: |
zip -r client.zip client
- name: Download wheel artifact
uses: actions/download-artifact@v4
with:
@@ -251,7 +300,7 @@ jobs:
upload-builds:
name: Uploading builds to release
runs-on: ubuntu-latest
needs: [ build-client, build-wheels, build-pyinstaller, build-appimage ]
needs: [build-client, build-wheels, build-pyinstaller, build-appimage]
steps:
- name: Download client artifact
@@ -286,18 +335,15 @@ jobs:
path: appimage
merge-multiple: true
- name: Determine if current run is draft
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
echo "DRAFT=true" >> $GITHUB_ENV
else
echo "DRAFT=false" >> $GITHUB_ENV
fi
- name: Upload artifacts to GitHub Release
uses: softprops/action-gh-release@v2
with:
draft: ${{ env.DRAFT }}
draft: true
name: ${{ format('v{0}',github.event.inputs.tag) }}
body_path: .github/changelog.md
fail_on_unmatched_files: true
target_commitish: ${{ github.sha }}
make_latest: ${{github.event.inputs.is_latest == 'true' }}
files: |
client.zip
wheels/**
+2
View File
@@ -38,3 +38,5 @@ nohup.out
.DS_Store
*.egg-info
/wheels/
client.zip
*.whl
+1 -1
View File
@@ -1 +1 @@
exec "${APPDIR}/usr/bin/python" -m swingmusic --fallback-client "${APPDIR}/client" "$@"
exec "${APPDIR}/usr/bin/python" -m swingmusic --client "${APPDIR}/client" "$@"
+4 -3
View File
@@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta"
name = "swingmusic"
description = "Swing Music"
readme = "README.md"
requires-python = ">=3.11, <=3.12"
requires-python = ">=3.11, <=3.12.9"
dynamic = ["version"]
dependencies = [
@@ -38,7 +38,8 @@ dependencies = [
"pendulum>=3.0.0",
"pystray>=0.19.5",
"waitress>=3.0.2; sys_platform == 'win32'",
"bjoern >=3.2.2; sys_platform != 'win32'"
"bjoern >=3.2.2; sys_platform != 'win32'",
"pyinstaller>=6.12.0",
]
[project.optional-dependencies]
@@ -66,4 +67,4 @@ Issues = "https://github.com/swingmx/swingmusic/issues"
[tool.setuptools_scm]
version_scheme = "only-version"
local_scheme = "no-local-version"
fallback_version = "v0.0.0"
fallback_version = "v0.0.0"
+14 -6
View File
@@ -1,16 +1,24 @@
# Launcher script
import swingmusic.__main__ as app
import sys
import zipfile
import multiprocessing
from pathlib import Path
import swingmusic.__main__ as app
if __name__ == "__main__":
# this entry should only be used by pyinstaller.
# add freeze support here as pyinstaller uses this entry only
# add freeze support here as pyinstaller uses this entry
if getattr(sys, 'frozen', False) and hasattr(sys, '_MEIPASS'):
client = sys._MEIPASS + "/client"
sys.argv.extend(["--fallback-client", client])
sys.orig_argv.extend(["--fallback-client", client])
if getattr(sys, "frozen", False) and hasattr(sys, "_MEIPASS"):
# INFO: extract client.zip to sys._MEIPASS
with zipfile.ZipFile(sys._MEIPASS + "/client.zip", "r") as zip_ref:
zip_ref.extractall(sys._MEIPASS)
client = Path(sys._MEIPASS) / "client"
client_str = str(client)
sys.argv.extend(["--client", client_str])
sys.orig_argv.extend(["--client", client_str])
multiprocessing.freeze_support()
app.run()
+47 -56
View File
@@ -1,67 +1,45 @@
import argparse
import sys
import pathlib
import argparse
import multiprocessing
from importlib.metadata import version
import multiprocessing
from swingmusic import settings
from swingmusic.logger import setup_logger
from swingmusic.settings import default_base_path
from swingmusic.start_swingmusic import start_swingmusic
from swingmusic import tools as swing_tools
from swingmusic.settings import AssetHandler
from swingmusic.start_swingmusic import start_swingmusic
parser = argparse.ArgumentParser(
prog='swingmusic',
description='Awesome Music',
formatter_class=argparse.ArgumentDefaultsHelpFormatter
prog="swingmusic",
description="Awesome Music",
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
)
parser.add_argument(
'-v', '--version',
action='version',
version=f"swingmusic v{version('swingmusic')}")
parser.add_argument(
"--host",
default="0.0.0.0",
help="Host to run the app on."
"-v", "--version", action="version", version=f"swingmusic v{version('swingmusic')}"
)
parser.add_argument("--host", default="0.0.0.0", help="Host to run the app on.")
parser.add_argument(
"--port",
default=1970,
help="HTTP port to run the app on.",
type=int
"--port", default=1970, help="HTTP port to run the app on.", type=int
)
parser.add_argument(
"--debug",
default=False,
action="store_true",
help="If swingmusic should start in debug mode"
help="If swingmusic should start in debug mode",
)
parser.add_argument(
"--config",
default=default_base_path(),
help="Path to the config file.",
type=pathlib.Path
)
parser.add_argument(
"--client",
help="Path to the Web UI folder.",
type=pathlib.Path
default=settings.Paths.get_default_config_parent_dir(),
help="The directory to setup the config folder.",
type=pathlib.Path,
)
parser.add_argument("--client", help="Path to the Web UI folder.", type=pathlib.Path)
parser.add_argument(
"--fallback-client",
help="Path to the Web UI folder if no valid client is found. Used in pyinstaller and appimage.",
type=pathlib.Path
)
tools = parser.add_argument_group(title="Tools")
tools.add_argument("--password-reset", help="Reset the password.", action="store_true")
tools = parser.add_argument_group(
title="Tools"
)
tools.add_argument(
"--password-reset",
help="Reset the password.",
action='store_true'
)
def run(*args, **kwargs):
"""
@@ -70,26 +48,39 @@ def run(*args, **kwargs):
args = parser.parse_args()
args = vars(args)
path = {
"config": args["config"],
"client": args["client"],
"fallback": args["fallback_client"]
}
config_parent = args["config"]
client_path = args["client"]
setup_logger(debug=args["debug"], app_dir=path["config"])
# INFO: Validate client path
if client_path is not None:
client_path = pathlib.Path(client_path).resolve()
if not client_path.exists():
print(
f"Client path {client_path} does not exist. Please provide a valid path"
)
sys.exit(1)
else:
# INFO: check if client path has index.html
if not (client_path / "index.html").exists():
print(
f"Client path {client_path} does not contain an index.html file. Please provide a valid path"
)
sys.exit(1)
# check tools
settings.Paths(config_parent=config_parent, client_dir=client_path)
AssetHandler.copy_assets_dir()
AssetHandler.setup_default_client()
setup_logger(debug=args["debug"], app_dir=settings.Paths().config_dir)
# handle tools
if args["password_reset"]:
swing_tools.handle_password_reset(path)
swing_tools.handle_password_reset(config_parent)
sys.exit(0)
# else start swingmusic
else:
start_swingmusic(
host=args["host"],
port=args["port"],
path=path
)
# start swingmusic
start_swingmusic(host=args["host"], port=args["port"])
if __name__ == "__main__":
+3 -4
View File
@@ -60,7 +60,7 @@ def config_jwt(web):
return user.todict()
def load_endpoints(web):
def load_endpoints(web: OpenAPI):
# Register all the API blueprints
with web.app_context():
web.register_api(swing_api.album.api)
@@ -88,7 +88,7 @@ def load_endpoints(web):
web.register_api(swing_api.auth.api)
def load_plugins(web):
def load_plugins(web: OpenAPI):
# TODO: rework plugin support
# Plugins
web.register_api(swing_api.plugins.api)
@@ -165,7 +165,7 @@ def serve_client_files(path: str):
# INFO: Safari doesn't support gzip encoding
# See issue: https://github.com/swingmx/swingmusic/issues/155
user_agent = request.headers.get("User-Agent", "")
if "Safari" in user_agent and not "Chrome" in user_agent:
if "Safari" in user_agent and "Chrome" not in user_agent:
return app.send_static_file(path)
if "gzip" in request.headers.get("Accept-Encoding", ""):
@@ -200,7 +200,6 @@ def build() -> OpenAPI:
# set late state config
app.static_folder = Paths().client_path
log.info(f"Serving client from '{app.static_folder}'")
@app.before_request
def verify_auth():
+1 -1
View File
@@ -36,7 +36,7 @@ def load_user_artist_ignore_list() -> set[str]:
Loads the user-defined artist ignore list from the config directory.
Returns an empty set if the file doesn't exist.
"""
user_file = Paths().app_dir / "artist_split_ignore.txt"
user_file = Paths().config_dir / "artist_split_ignore.txt"
if user_file.exists():
lines = user_file.read_text().splitlines()
return set([ line.strip() for line in lines if line.strip()])
-1
View File
@@ -229,4 +229,3 @@ def setup_logger(app_dir:Path, debug=False):
global log
log = logging.getLogger(__name__)
log.info("setup successfully")
+187 -180
View File
@@ -4,10 +4,12 @@ All Variables should be read only after an initial set.
Contains default configs
"""
import io
import multiprocessing
import pathlib
import shutil
import sys
import tempfile
import zipfile
from pathlib import Path
@@ -23,6 +25,7 @@ log = logging.getLogger(__name__)
# Meta-classes #
# # # # # # # # #
class Singleton(type):
_instances = {}
@@ -31,144 +34,135 @@ class Singleton(type):
cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs)
return cls._instances[cls]
# # # # # # # #
# Downloader #
# # # # # # # #
def populate_client(path:Path) -> bool:
"""
Checks if client folder contains content.
Client needs to have at least an index.html file.
If not, latest client is parsed from GitHub builds.
:param path: path to client folder
:return: True if successful else False
class AssetHandler:
"""
Handles all assets configuration
"""
CLIENT_RELEASES_URL = (
"https://api.github.com/repos/swingmx/swingmusic/releases/latest"
)
@staticmethod
def copy_assets_dir():
"""
Copies assets to the app directory.
"""
CLIENT_RELEASES_URL = "https://api.github.com/repos/michilyy/swingmusic/releases/latest"
# TODO: update to real client repo
assets_source = imres.files("swingmusic") / "assets"
assets_path = Paths().assets_path
# INFO: this only works for wheels and source
# TODO: Handle this for pyinstaller builds
# TODO: check for new releases. Currently only download when client is not found
# TODO: move this outside. `Paths` only does path routing.
if assets_path.exists():
# no need to copy what's already copied?
return
index = path / "index.html"
if not index.exists():
log.warning(f"'index.html' could not be found in '{path.as_posix()}'.")
log.warning("Try downloading latest client from GitHub.")
try:
if assets_source.exists():
shutil.copytree(
Path(assets_source),
assets_path,
ignore=shutil.ignore_patterns(
"*.pyc",
),
copy_function=shutil.copy2,
dirs_exist_ok=True,
)
else:
log.error(f"Assets dir could not be found: {assets_source.as_posix()}")
answer = requests.get(CLIENT_RELEASES_URL).json()
@staticmethod
def extract_default_client(path: Path) -> bool:
"""
Extracts the default client which is bundled with the wheel
into the swingmusic client folder.
"""
# INFO: Locate the client.zip file using imres, extract it to the swingmusic client folder
client_zip_path = imres.files("swingmusic") / "client.zip"
if not client_zip_path.exists():
log.error("Client zip could not be found. Please provide a valid path.")
return False
for asset in answer["assets"]:
if asset["name"] == "client.zip":
# download and convert client
client = requests.get(asset["browser_download_url"])
mem_file = io.BytesIO(client.content)
file = zipfile.ZipFile(mem_file)
# create new dir for extraction
log.info(f"Storing client in '{path.as_posix()}'.")
with tempfile.TemporaryDirectory() as temp_folder:
file.extractall(temp_folder)
shutil.copytree(
Path(temp_folder) / "client",
path,
copy_function=shutil.copy2,
dirs_exist_ok=True,
)
break
except (requests.exceptions.RequestException, KeyError, requests.exceptions.ConnectionError)as e:
log.error(f"Client could not be downloaded from releases. NETWORK ERROR", exc_info=e)
return False
except requests.exceptions.InvalidJSONError as e:
log.error(f"Client could not be downloaded from releases. JSON ERROR", exc_info=e)
return False
except zipfile.BadZipfile as e:
log.error(f"Client could not be unpacked. ZIP ERROR", exc_info=e)
return False
with zipfile.ZipFile(client_zip_path, "r") as zip_ref:
zip_ref.extractall(path)
return True
# # # # # # # # #
# Path Logic #
# # # # # # # # #
@staticmethod
def download_client_from_github():
"""
Downloads the latest supported client from Github
and places it in the swingmusic client folder.
"""
path = Paths().config_parent / "client"
def default_client_path(app_dir:Path, fallback_client:Path|None=None) -> Path:
"""
| Calculates the default config path for ``client``.
| Checks for the first valid path.
try:
answer = requests.get(AssetHandler.CLIENT_RELEASES_URL).json()
Check order:
for asset in answer["assets"]:
if asset["name"] == "client.zip":
# download and convert client
client = requests.get(asset["browser_download_url"])
mem_file = io.BytesIO(client.content)
file = zipfile.ZipFile(mem_file)
1. Env:``SWINGMUSIC_CLIENT_DIR``
2. if ``<app_dir>/client/`` exists
1. use ``<app_dir>/client/``
3. if ``<app_dir>/client/`` not exists
1. try downloading client from GitHub
2. if successful
1. use ``<app_dir>/client/``
3. if not successful
1. use ``<fallback_client>``
# create new dir for extraction
log.info(f"Storing client in '{path.as_posix()}'.")
with tempfile.TemporaryDirectory() as temp_folder:
file.extractall(temp_folder)
:param app_dir:
:param fallback_client: optional path to client. Used in pyinstaller/AppImage build
:return: Calculated Path
"""
shutil.copytree(
Path(temp_folder) / "client",
path,
copy_function=shutil.copy2,
dirs_exist_ok=True,
)
client_path = app_dir / 'client'
break
env_client_dir = os.environ.get("SWINGMUSIC_CLIENT_DIR")
except (
requests.exceptions.RequestException,
KeyError,
requests.exceptions.ConnectionError,
) as e:
log.error(
"Client could not be downloaded from releases. NETWORK ERROR",
exc_info=e,
)
return False
except requests.exceptions.InvalidJSONError as e:
log.error(
"Client could not be downloaded from releases. JSON ERROR",
exc_info=e,
)
return False
except zipfile.BadZipfile as e:
log.error("Client could not be unpacked. ZIP ERROR", exc_info=e)
return False
if not env_client_dir is None:
return Path(env_client_dir)
@classmethod
def setup_default_client(cls):
"""
Runs on startup to ensure the default client is present.
"""
client_path = Paths().client_path
extracted = False
if not client_path.exists() or not (client_path / "index.html").exists():
extracted = cls.extract_default_client(Paths().config_dir)
if (client_path / "index.html").exists():
return client_path
else:
if populate_client(client_path):
return client_path
elif fallback_client is not None:
return fallback_client
else:
raise NotImplementedError(f"Client could not be determined. Neither download or fallback.")
if not extracted:
extracted = cls.download_client_from_github()
def default_base_path() -> pathlib.Path:
"""
| Calculates the default config path for ``swingmusic``.
| Checks for the first valid path.
| If no Path is valid, will use Home dir (4.)
Check order:
1. Env:``SWINGMUSIC_CONFIG_DIR``
2. Env:``xdg_config_home``
3. <User Home>/.config
4. <User Home>
:return: Calculated Path
"""
swing_xdg_config_home = os.environ.get("SWINGMUSIC_CONFIG_DIR")
xdg_config_home = os.environ.get("xdg_config_home")
alt_dir = pathlib.Path.home() / ".config"
base_path = pathlib.Path.home()
if not swing_xdg_config_home is None:
base_path = pathlib.Path(swing_xdg_config_home)
elif not xdg_config_home is None:
base_path = pathlib.Path(xdg_config_home)
elif alt_dir.exists():
base_path = alt_dir
return base_path
if not (client_path / "index.html").exists():
log.error("Web client not found. Exiting ...")
sys.exit(1)
class Paths(metaclass=Singleton):
@@ -183,13 +177,21 @@ class Paths(metaclass=Singleton):
You cannot change the config path later.
"""
base_path:Path = Path.home().resolve()
config_parent: Path = Path.home().resolve()
"""
The parent directory of the config folder.
This is the directory where the config folder is located.
"""
USER_HOME_DIR = Path.home().resolve()
APP_DB_NAME = "swingmusic.db"
USER_DATA_DB_NAME = "userdata.db"
def __init__(self, config:Path|None=None, client:Path|None=None, fallback:Path|None=None):
def __init__(
self,
config_parent: Path | None = None,
client_dir: Path | None = None,
):
"""
Create config-folder structure and check permissions.
Copy all assets if needed.
@@ -210,34 +212,63 @@ class Paths(metaclass=Singleton):
user's home directory.
"""
if config is not None:
self.base_path = config.resolve()
if config_parent is not None:
self.config_parent = config_parent.resolve()
else:
self.base_path = default_base_path()
env_client_dir = os.environ.get("SWINGMUSIC_CLIENT_DIR")
if client is not None:
self.client_path = client.resolve()
elif not env_client_dir is None:
self.client_path = Path(env_client_dir)
else:
self.client_path = default_client_path(self.app_dir, fallback)
self.config_parent = Paths.get_default_config_parent_dir()
if multiprocessing.current_process().name == "MainProcess":
# INFO: Setup client path
env_client_dir = os.environ.get("SWINGMUSIC_CLIENT_DIR")
if client_dir is not None:
self.client_path = client_dir.resolve()
elif env_client_dir is not None:
self.client_path = Path(env_client_dir).resolve()
else:
self.client_path = self.config_dir / "client"
# Path copy only on MainProcess
if not self.app_dir.exists():
self.app_dir.mkdir(parents=True)
if not self.config_dir.exists():
self.config_dir.mkdir(parents=True)
# TODO: find a platform independent way to access module globals like `Paths`
# TODO: move this into multithreading management class
os.environ["SWINGMUSIC_CONFIG_DIR"] = self.base_path.resolve().as_posix()
os.environ["SWINGMUSIC_CONFIG_DIR"] = (
self.config_parent.resolve().as_posix()
)
os.environ["SWINGMUSIC_CLIENT_DIR"] = self.client_path.resolve().as_posix()
self.mkdir_config_folders()
self.copy_assets_dir()
self.setup_config_dirs()
@classmethod
def get_default_config_parent_dir(cls) -> pathlib.Path:
"""
Determines the default config path in the following order:
def mkdir_config_folders(self):
1. Env:``SWINGMUSIC_CONFIG_DIR``
2. Env:``xdg_config_home``
3. <User Home>/.config
4. <User Home>
:return: First valid path
"""
config_dir_from_env = os.environ.get("SWINGMUSIC_CONFIG_DIR")
xdg_config_home = os.environ.get("XDG_CONFIG_HOME")
if config_dir_from_env is not None:
return pathlib.Path(config_dir_from_env)
if xdg_config_home is not None:
return pathlib.Path(xdg_config_home)
fallback_dir = pathlib.Path.home() / ".config"
if fallback_dir.exists():
return fallback_dir
return pathlib.Path.home()
def setup_config_dirs(self):
"""
Create the config/cache folder structure.
@@ -262,10 +293,9 @@ class Paths(metaclass=Singleton):
└───lyrics
"""
# all dirs relative to `swingmusic` config dir
dirs = [
"", # `swingmusic` or `.swingmusic`
"", # `swingmusic` or `.swingmusic`
"plugins/lyrics",
"images/playlists",
"images/thumbnails/small",
@@ -282,7 +312,7 @@ class Paths(metaclass=Singleton):
]
for folder in dirs:
path = self.base_path / self.config_folder_name / folder
path = self.config_parent / self.config_folder_name / folder
if not path.exists():
path.mkdir(parents=True)
path.chmod(mode=0o755)
@@ -290,7 +320,9 @@ class Paths(metaclass=Singleton):
# Empty files to create
empty_files = [
# artist split ignore list
self.app_dir / "data" / "artist_split_ignore.txt" # TODO: use USERCONFIG -> circular import error
self.config_dir
/ "data"
/ "artist_split_ignore.txt" # TODO: use USERCONFIG -> circular import error
]
for file in empty_files:
@@ -301,32 +333,6 @@ class Paths(metaclass=Singleton):
file.parent.mkdir(parents=True, exist_ok=True)
file.touch()
def copy_assets_dir(self):
"""
Copies assets to the app directory.
"""
assets_source = imres.files("swingmusic") / "assets"
if self.assets_path.exists():
# no need to copy what's already copied
return
if assets_source.exists():
shutil.copytree(
Path(assets_source),
self.assets_path,
ignore=shutil.ignore_patterns(
"*.pyc",
),
copy_function=shutil.copy2,
dirs_exist_ok=True,
)
else:
log.error(f"Assets dir could not be found: {assets_source.as_posix()}")
@property
def config_folder_name(self) -> str:
"""
@@ -335,18 +341,18 @@ class Paths(metaclass=Singleton):
When the base path is the same as the home dir,
it returns `.swingmusic` else `swingmusic`
"""
if self.base_path == self.USER_HOME_DIR:
if self.config_parent == self.USER_HOME_DIR:
return ".swingmusic"
else:
return "swingmusic"
@property
def app_dir(self) -> Path:
return self.base_path / self.config_folder_name
def config_dir(self) -> Path:
return self.config_parent / self.config_folder_name
@property
def img_path(self) -> Path:
return self.app_dir / "images"
return self.config_dir / "images"
# ARTISTS
@property
@@ -384,7 +390,7 @@ class Paths(metaclass=Singleton):
@property
def lg_thumb_path(self) -> pathlib.Path:
return self.thumbs_path/ "large"
return self.thumbs_path / "large"
# OTHERS
@property
@@ -393,11 +399,11 @@ class Paths(metaclass=Singleton):
@property
def assets_path(self) -> pathlib.Path:
return self.app_dir / "assets"
return self.config_dir / "assets"
@property
def plugins_path(self) -> pathlib.Path:
return self.app_dir / "plugins"
return self.config_dir / "plugins"
@property
def lyrics_plugins_path(self) -> pathlib.Path:
@@ -405,23 +411,23 @@ class Paths(metaclass=Singleton):
@property
def config_file_path(self) -> pathlib.Path:
return self.app_dir/ "settings.json"
return self.config_dir / "settings.json"
@property
def mixes_img_path(self) -> pathlib.Path:
return self.img_path/ "mixes"
return self.img_path / "mixes"
@property
def artist_mixes_img_path(self) -> pathlib.Path:
return self.mixes_img_path/ "artists"
return self.mixes_img_path / "artists"
@property
def og_mixes_img_path(self) -> pathlib.Path:
return self.mixes_img_path/ "original"
return self.mixes_img_path / "original"
@property
def md_mixes_img_path(self) -> pathlib.Path:
return self.mixes_img_path/ "medium"
return self.mixes_img_path / "medium"
@property
def sm_mixes_img_path(self) -> pathlib.Path:
@@ -433,15 +439,15 @@ class Paths(metaclass=Singleton):
@property
def app_db_path(self):
return Paths().app_dir / self.APP_DB_NAME
return Paths().config_dir / self.APP_DB_NAME
@property
def userdata_db_path(self):
return Paths().app_dir / self.USER_DATA_DB_NAME
return Paths().config_dir / self.USER_DATA_DB_NAME
@property
def json_config_path(self):
return Paths().app_dir / "config.json"
return Paths().config_dir / "config.json"
# # # # # # # # # # # # #
@@ -478,6 +484,7 @@ class Defaults:
API_TRACKNAME = "Martin & Gina"
API_CARD_LIMIT = 6
class TCOLOR:
"""
Terminal colors
@@ -492,4 +499,4 @@ class TCOLOR:
ENDC = "\033[0m"
BOLD = "\033[1m"
UNDERLINE = "\033[4m"
# credits: https://stackoverflow.com/a/287944
# credits: https://stackoverflow.com/a/287944
+1 -1
View File
@@ -18,4 +18,4 @@ def log_startup_info(host: str, port: int):
f"{TCOLOR.OKGREEN}http://{address}:{port}{TCOLOR.ENDC}"
)
print(f"\n{TCOLOR.YELLOW}Data folder: {Paths().app_dir}{TCOLOR.ENDC}\n")
print(f"\n{TCOLOR.YELLOW}Data folder: {Paths().config_dir}{TCOLOR.ENDC}\n")
+31 -9
View File
@@ -1,18 +1,17 @@
from swingmusic import settings, app_builder
import socket
import sys
from swingmusic import app_builder
from swingmusic.crons import start_cron_jobs
from swingmusic.plugins.register import register_plugins
from swingmusic.setup import load_into_mem, run_setup
from swingmusic.start_info_logger import log_startup_info
from swingmusic.utils.threading import background
from swingmusic.logger import setup_logger
import pathlib
import setproctitle
import mimetypes
def config_mimetypes():
# Load mimetypes for the web client's static files
# Loading mimetypes should happen automaticaly but
@@ -35,7 +34,24 @@ def config_mimetypes():
mimetypes.add_type("application/manifest+json", ".webmanifest")
def start_swingmusic(host: str, port: int, path:dict[str,pathlib.Path|None]):
class PortManager:
def __init__(self, host: str):
self.host = host
def test_port(self, port: int):
try:
http_server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
http_server.bind((self.host, port))
http_server.close()
return True
except socket.error as e:
if e.errno == 48:
return False
else:
raise e
def start_swingmusic(host: str, port: int):
"""
Creates and starts the Flask application server for Swing Music.
@@ -51,13 +67,20 @@ def start_swingmusic(host: str, port: int, path:dict[str,pathlib.Path|None]):
:param host: The host address to bind the server to (e.g., 'localhost' or '0.0.0.0')
:param port: The port number to run the server on
:param path: dict with all path config
"""
port_manager = PortManager(host)
# Try starting a server on port 1970
# If it fails, exit with error
if not port_manager.test_port(port):
print(f"Error 48: Port {port} already in use.")
print("Please specify a different port using the --port argument.")
sys.exit(1)
# Example: Setting up dirs, database, and loading stuff into memory.
# TIP: Be careful with the order of the setup functions.
# NOTE: concurrent and multithreading create own sys.modules -> no globals
settings.Paths(**path)
config_mimetypes()
run_setup()
@@ -69,7 +92,6 @@ def start_swingmusic(host: str, port: int, path:dict[str,pathlib.Path|None]):
setproctitle.setproctitle(f"swingmusic {host}:{port}")
start_cron_jobs()
app = app_builder.build()
log_startup_info(host, port)
@@ -95,4 +117,4 @@ def start_swingmusic(host: str, port: int, path:dict[str,pathlib.Path|None]):
threads=100,
ipv6=True,
ipv4=True,
)
)
+4 -3
View File
@@ -10,12 +10,13 @@ from swingmusic.settings import Paths
from pathlib import Path
from PIL import Image
def handle_password_reset(path:dict[str,Path]):
def handle_password_reset(config_parent: Path):
"""
Handles the --password-reset argument. Resets the password.
"""
Paths(**path)
Paths(config_parent=config_parent)
setup_sqlite()
@@ -64,4 +65,4 @@ def create_image(width, height, color1, color2):
# Paste the resized image onto the padded image
padded_image.paste(image, (x, y), image)
return padded_image
return padded_image
+1 -1
View File
@@ -5,7 +5,7 @@ import pathlib
hiddenimports =[]
# hiddenimports += collect_submodules('swingmusic')
datas = [('client', 'client')]
datas = [('client.zip', '.')]
datas += collect_data_files('swingmusic', True, excludes=['**/*.py'], includes=['**/*.*'])
datas += collect_data_files('flask_openapi3', True, excludes=['**/*.py'], includes=['**/*.*'])
Generated
+660 -654
View File
File diff suppressed because it is too large Load Diff