mirror of
https://github.com/Dvorinka/swingmusic-extended.git
synced 2026-06-03 20:13:02 +00:00
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:
+81
-35
@@ -1,16 +1,41 @@
|
|||||||
name: Build and Upload
|
name: New Release
|
||||||
|
run-name: Release v${{ github.event.inputs.tag }}
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
release:
|
inputs:
|
||||||
types:
|
tag:
|
||||||
- prereleased
|
description: "Version number"
|
||||||
- released
|
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:
|
env:
|
||||||
PIP_USE_PEP517: true
|
PIP_USE_PEP517: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
|
||||||
build-client:
|
build-client:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
name: Build client
|
name: Build client
|
||||||
@@ -18,7 +43,7 @@ jobs:
|
|||||||
- name: Clone client
|
- name: Clone client
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
repository: 'swingmx/webclient'
|
repository: "swingmx/webclient"
|
||||||
path: swingmusic-client
|
path: swingmusic-client
|
||||||
|
|
||||||
- name: Setup Node 20
|
- name: Setup Node 20
|
||||||
@@ -42,11 +67,12 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
path: "client/"
|
path: "client/"
|
||||||
compression-level: 0
|
compression-level: 0
|
||||||
name: 'client'
|
name: "client"
|
||||||
|
|
||||||
build-wheels:
|
build-wheels:
|
||||||
name: Build wheels
|
name: Build wheels
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
needs: [build-client]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout swingmusic
|
- name: Checkout swingmusic
|
||||||
@@ -54,9 +80,25 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
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
|
- uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: '3.11'
|
python-version: "3.11"
|
||||||
|
|
||||||
|
- name: Create git tag
|
||||||
|
run: |
|
||||||
|
git tag v${{ github.event.inputs.tag }}
|
||||||
|
|
||||||
- name: Build wheels
|
- name: Build wheels
|
||||||
run: pip wheel . -w wheelhouse --no-deps
|
run: pip wheel . -w wheelhouse --no-deps
|
||||||
@@ -66,22 +108,23 @@ jobs:
|
|||||||
# name: cibw-wheels-${{ matrix.os }}-${{ strategy.job-index }}
|
# name: cibw-wheels-${{ matrix.os }}-${{ strategy.job-index }}
|
||||||
path: ./wheelhouse/*.whl
|
path: ./wheelhouse/*.whl
|
||||||
compression-level: 0
|
compression-level: 0
|
||||||
name: 'wheels'
|
name: "wheels"
|
||||||
|
|
||||||
build-appimage:
|
build-appimage:
|
||||||
name: Build Appimage
|
name: Build Appimage
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
needs: [ build-client ]
|
needs: [build-client]
|
||||||
|
if: ${{ github.event.inputs.binary_build == 'true' }}
|
||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
os: [ ubuntu-latest, ubuntu-24.04-arm]
|
os: [ubuntu-latest, ubuntu-24.04-arm]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/setup-python@v5
|
- uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: '3.11'
|
python-version: "3.11"
|
||||||
|
|
||||||
- name: Install linux dependencies
|
- name: Install linux dependencies
|
||||||
run: sudo apt-get install libev-dev libfuse-dev -y > /dev/null
|
run: sudo apt-get install libev-dev libfuse-dev -y > /dev/null
|
||||||
@@ -134,8 +177,9 @@ jobs:
|
|||||||
docker:
|
docker:
|
||||||
name: Build and push Docker image
|
name: Build and push Docker image
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: [ build-client, build-wheels ]
|
needs: [build-client, build-wheels]
|
||||||
permissions: write-all
|
permissions: write-all
|
||||||
|
if: ${{ github.event.inputs.build_docker == 'true' }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout into repo
|
- name: Checkout into repo
|
||||||
@@ -166,32 +210,33 @@ jobs:
|
|||||||
images: |
|
images: |
|
||||||
ghcr.io/${{ github.repository }}
|
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
|
- name: Build and push
|
||||||
uses: docker/build-push-action@v6
|
uses: docker/build-push-action@v6
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
platforms: linux/amd64, linux/arm64 #,linux/arm
|
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
|
tags: ghcr.io/${{github.repository}}:${{format('{0}', github.ref_name)}}, ghcr.io/${{github.repository}}:latest
|
||||||
labels: org.opencontainers.image.title=Docker
|
labels: org.opencontainers.image.title=Docker
|
||||||
|
|
||||||
build-pyinstaller:
|
build-pyinstaller:
|
||||||
name: Build binary on ${{ matrix.os }}
|
name: Build binary on ${{ matrix.os }}
|
||||||
runs-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:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
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:
|
steps:
|
||||||
- name: Checkout swingmusic
|
- name: Checkout swingmusic
|
||||||
@@ -212,6 +257,10 @@ jobs:
|
|||||||
name: client
|
name: client
|
||||||
path: client
|
path: client
|
||||||
|
|
||||||
|
- name: Compress client
|
||||||
|
run: |
|
||||||
|
zip -r client.zip client
|
||||||
|
|
||||||
- name: Download wheel artifact
|
- name: Download wheel artifact
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
@@ -251,7 +300,7 @@ jobs:
|
|||||||
upload-builds:
|
upload-builds:
|
||||||
name: Uploading builds to release
|
name: Uploading builds to release
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: [ build-client, build-wheels, build-pyinstaller, build-appimage ]
|
needs: [build-client, build-wheels, build-pyinstaller, build-appimage]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Download client artifact
|
- name: Download client artifact
|
||||||
@@ -286,18 +335,15 @@ jobs:
|
|||||||
path: appimage
|
path: appimage
|
||||||
merge-multiple: true
|
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
|
- name: Upload artifacts to GitHub Release
|
||||||
uses: softprops/action-gh-release@v2
|
uses: softprops/action-gh-release@v2
|
||||||
with:
|
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: |
|
files: |
|
||||||
client.zip
|
client.zip
|
||||||
wheels/**
|
wheels/**
|
||||||
|
|||||||
@@ -38,3 +38,5 @@ nohup.out
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
*.egg-info
|
*.egg-info
|
||||||
/wheels/
|
/wheels/
|
||||||
|
client.zip
|
||||||
|
*.whl
|
||||||
@@ -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
@@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
name = "swingmusic"
|
name = "swingmusic"
|
||||||
description = "Swing Music"
|
description = "Swing Music"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.11, <=3.12"
|
requires-python = ">=3.11, <=3.12.9"
|
||||||
dynamic = ["version"]
|
dynamic = ["version"]
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
@@ -38,7 +38,8 @@ dependencies = [
|
|||||||
"pendulum>=3.0.0",
|
"pendulum>=3.0.0",
|
||||||
"pystray>=0.19.5",
|
"pystray>=0.19.5",
|
||||||
"waitress>=3.0.2; sys_platform == 'win32'",
|
"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]
|
[project.optional-dependencies]
|
||||||
@@ -66,4 +67,4 @@ Issues = "https://github.com/swingmx/swingmusic/issues"
|
|||||||
[tool.setuptools_scm]
|
[tool.setuptools_scm]
|
||||||
version_scheme = "only-version"
|
version_scheme = "only-version"
|
||||||
local_scheme = "no-local-version"
|
local_scheme = "no-local-version"
|
||||||
fallback_version = "v0.0.0"
|
fallback_version = "v0.0.0"
|
||||||
|
|||||||
@@ -1,16 +1,24 @@
|
|||||||
# Launcher script
|
# Launcher script
|
||||||
import swingmusic.__main__ as app
|
|
||||||
import sys
|
import sys
|
||||||
|
import zipfile
|
||||||
import multiprocessing
|
import multiprocessing
|
||||||
|
from pathlib import Path
|
||||||
|
import swingmusic.__main__ as app
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
# this entry should only be used by pyinstaller.
|
# 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'):
|
if getattr(sys, "frozen", False) and hasattr(sys, "_MEIPASS"):
|
||||||
client = sys._MEIPASS + "/client"
|
# INFO: extract client.zip to sys._MEIPASS
|
||||||
sys.argv.extend(["--fallback-client", client])
|
with zipfile.ZipFile(sys._MEIPASS + "/client.zip", "r") as zip_ref:
|
||||||
sys.orig_argv.extend(["--fallback-client", client])
|
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()
|
multiprocessing.freeze_support()
|
||||||
app.run()
|
app.run()
|
||||||
|
|||||||
+47
-56
@@ -1,67 +1,45 @@
|
|||||||
import argparse
|
import sys
|
||||||
import pathlib
|
import pathlib
|
||||||
|
import argparse
|
||||||
|
import multiprocessing
|
||||||
from importlib.metadata import version
|
from importlib.metadata import version
|
||||||
|
|
||||||
import multiprocessing
|
from swingmusic import settings
|
||||||
|
|
||||||
from swingmusic.logger import setup_logger
|
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 import tools as swing_tools
|
||||||
|
from swingmusic.settings import AssetHandler
|
||||||
|
from swingmusic.start_swingmusic import start_swingmusic
|
||||||
|
|
||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
prog='swingmusic',
|
prog="swingmusic",
|
||||||
description='Awesome Music',
|
description="Awesome Music",
|
||||||
formatter_class=argparse.ArgumentDefaultsHelpFormatter
|
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
||||||
)
|
)
|
||||||
|
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'-v', '--version',
|
"-v", "--version", action="version", version=f"swingmusic v{version('swingmusic')}"
|
||||||
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("--host", default="0.0.0.0", help="Host to run the app on.")
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--port",
|
"--port", default=1970, help="HTTP port to run the app on.", type=int
|
||||||
default=1970,
|
|
||||||
help="HTTP port to run the app on.",
|
|
||||||
type=int
|
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--debug",
|
"--debug",
|
||||||
default=False,
|
default=False,
|
||||||
action="store_true",
|
action="store_true",
|
||||||
help="If swingmusic should start in debug mode"
|
help="If swingmusic should start in debug mode",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--config",
|
"--config",
|
||||||
default=default_base_path(),
|
default=settings.Paths.get_default_config_parent_dir(),
|
||||||
help="Path to the config file.",
|
help="The directory to setup the config folder.",
|
||||||
type=pathlib.Path
|
type=pathlib.Path,
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--client",
|
|
||||||
help="Path to the Web UI folder.",
|
|
||||||
type=pathlib.Path
|
|
||||||
)
|
)
|
||||||
|
parser.add_argument("--client", help="Path to the Web UI folder.", type=pathlib.Path)
|
||||||
|
|
||||||
parser.add_argument(
|
tools = parser.add_argument_group(title="Tools")
|
||||||
"--fallback-client",
|
tools.add_argument("--password-reset", help="Reset the password.", action="store_true")
|
||||||
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'
|
|
||||||
)
|
|
||||||
|
|
||||||
def run(*args, **kwargs):
|
def run(*args, **kwargs):
|
||||||
"""
|
"""
|
||||||
@@ -70,26 +48,39 @@ def run(*args, **kwargs):
|
|||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
args = vars(args)
|
args = vars(args)
|
||||||
|
|
||||||
path = {
|
config_parent = args["config"]
|
||||||
"config": args["config"],
|
client_path = args["client"]
|
||||||
"client": args["client"],
|
|
||||||
"fallback": args["fallback_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"]:
|
if args["password_reset"]:
|
||||||
swing_tools.handle_password_reset(path)
|
swing_tools.handle_password_reset(config_parent)
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
# else start swingmusic
|
# start swingmusic
|
||||||
else:
|
start_swingmusic(host=args["host"], port=args["port"])
|
||||||
start_swingmusic(
|
|
||||||
host=args["host"],
|
|
||||||
port=args["port"],
|
|
||||||
path=path
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ def config_jwt(web):
|
|||||||
return user.todict()
|
return user.todict()
|
||||||
|
|
||||||
|
|
||||||
def load_endpoints(web):
|
def load_endpoints(web: OpenAPI):
|
||||||
# Register all the API blueprints
|
# Register all the API blueprints
|
||||||
with web.app_context():
|
with web.app_context():
|
||||||
web.register_api(swing_api.album.api)
|
web.register_api(swing_api.album.api)
|
||||||
@@ -88,7 +88,7 @@ def load_endpoints(web):
|
|||||||
web.register_api(swing_api.auth.api)
|
web.register_api(swing_api.auth.api)
|
||||||
|
|
||||||
|
|
||||||
def load_plugins(web):
|
def load_plugins(web: OpenAPI):
|
||||||
# TODO: rework plugin support
|
# TODO: rework plugin support
|
||||||
# Plugins
|
# Plugins
|
||||||
web.register_api(swing_api.plugins.api)
|
web.register_api(swing_api.plugins.api)
|
||||||
@@ -165,7 +165,7 @@ 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
|
||||||
user_agent = request.headers.get("User-Agent", "")
|
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)
|
return app.send_static_file(path)
|
||||||
|
|
||||||
if "gzip" in request.headers.get("Accept-Encoding", ""):
|
if "gzip" in request.headers.get("Accept-Encoding", ""):
|
||||||
@@ -200,7 +200,6 @@ def build() -> OpenAPI:
|
|||||||
|
|
||||||
# set late state config
|
# set late state config
|
||||||
app.static_folder = Paths().client_path
|
app.static_folder = Paths().client_path
|
||||||
log.info(f"Serving client from '{app.static_folder}'")
|
|
||||||
|
|
||||||
@app.before_request
|
@app.before_request
|
||||||
def verify_auth():
|
def verify_auth():
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ def load_user_artist_ignore_list() -> set[str]:
|
|||||||
Loads the user-defined artist ignore list from the config directory.
|
Loads the user-defined artist ignore list from the config directory.
|
||||||
Returns an empty set if the file doesn't exist.
|
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():
|
if user_file.exists():
|
||||||
lines = user_file.read_text().splitlines()
|
lines = user_file.read_text().splitlines()
|
||||||
return set([ line.strip() for line in lines if line.strip()])
|
return set([ line.strip() for line in lines if line.strip()])
|
||||||
|
|||||||
@@ -229,4 +229,3 @@ def setup_logger(app_dir:Path, debug=False):
|
|||||||
|
|
||||||
global log
|
global log
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
log.info("setup successfully")
|
|
||||||
|
|||||||
+187
-180
@@ -4,10 +4,12 @@ All Variables should be read only after an initial set.
|
|||||||
|
|
||||||
Contains default configs
|
Contains default configs
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import io
|
import io
|
||||||
import multiprocessing
|
import multiprocessing
|
||||||
import pathlib
|
import pathlib
|
||||||
import shutil
|
import shutil
|
||||||
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
import zipfile
|
import zipfile
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -23,6 +25,7 @@ log = logging.getLogger(__name__)
|
|||||||
# Meta-classes #
|
# Meta-classes #
|
||||||
# # # # # # # # #
|
# # # # # # # # #
|
||||||
|
|
||||||
|
|
||||||
class Singleton(type):
|
class Singleton(type):
|
||||||
_instances = {}
|
_instances = {}
|
||||||
|
|
||||||
@@ -31,144 +34,135 @@ class Singleton(type):
|
|||||||
cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs)
|
cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs)
|
||||||
return cls._instances[cls]
|
return cls._instances[cls]
|
||||||
|
|
||||||
|
|
||||||
# # # # # # # #
|
# # # # # # # #
|
||||||
# Downloader #
|
# 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
|
class AssetHandler:
|
||||||
:return: True if successful else False
|
"""
|
||||||
|
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"
|
assets_source = imres.files("swingmusic") / "assets"
|
||||||
# TODO: update to real client repo
|
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
|
if assets_path.exists():
|
||||||
# TODO: move this outside. `Paths` only does path routing.
|
# no need to copy what's already copied?
|
||||||
|
return
|
||||||
|
|
||||||
index = path / "index.html"
|
if assets_source.exists():
|
||||||
if not index.exists():
|
shutil.copytree(
|
||||||
log.warning(f"'index.html' could not be found in '{path.as_posix()}'.")
|
Path(assets_source),
|
||||||
log.warning("Try downloading latest client from GitHub.")
|
assets_path,
|
||||||
try:
|
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"]:
|
with zipfile.ZipFile(client_zip_path, "r") as zip_ref:
|
||||||
if asset["name"] == "client.zip":
|
zip_ref.extractall(path)
|
||||||
# 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
|
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# # # # # # # # #
|
@staticmethod
|
||||||
# Path Logic #
|
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:
|
try:
|
||||||
"""
|
answer = requests.get(AssetHandler.CLIENT_RELEASES_URL).json()
|
||||||
| Calculates the default config path for ``client``.
|
|
||||||
| Checks for the first valid path.
|
|
||||||
|
|
||||||
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``
|
# create new dir for extraction
|
||||||
2. if ``<app_dir>/client/`` exists
|
log.info(f"Storing client in '{path.as_posix()}'.")
|
||||||
1. use ``<app_dir>/client/``
|
with tempfile.TemporaryDirectory() as temp_folder:
|
||||||
3. if ``<app_dir>/client/`` not exists
|
file.extractall(temp_folder)
|
||||||
1. try downloading client from GitHub
|
|
||||||
2. if successful
|
|
||||||
1. use ``<app_dir>/client/``
|
|
||||||
3. if not successful
|
|
||||||
1. use ``<fallback_client>``
|
|
||||||
|
|
||||||
:param app_dir:
|
shutil.copytree(
|
||||||
:param fallback_client: optional path to client. Used in pyinstaller/AppImage build
|
Path(temp_folder) / "client",
|
||||||
:return: Calculated Path
|
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:
|
@classmethod
|
||||||
return Path(env_client_dir)
|
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():
|
if not extracted:
|
||||||
return client_path
|
extracted = cls.download_client_from_github()
|
||||||
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 (client_path / "index.html").exists():
|
||||||
def default_base_path() -> pathlib.Path:
|
log.error("Web client not found. Exiting ...")
|
||||||
"""
|
sys.exit(1)
|
||||||
| 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
|
|
||||||
|
|
||||||
|
|
||||||
class Paths(metaclass=Singleton):
|
class Paths(metaclass=Singleton):
|
||||||
@@ -183,13 +177,21 @@ class Paths(metaclass=Singleton):
|
|||||||
You cannot change the config path later.
|
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()
|
USER_HOME_DIR = Path.home().resolve()
|
||||||
APP_DB_NAME = "swingmusic.db"
|
APP_DB_NAME = "swingmusic.db"
|
||||||
USER_DATA_DB_NAME = "userdata.db"
|
USER_DATA_DB_NAME = "userdata.db"
|
||||||
|
|
||||||
|
def __init__(
|
||||||
def __init__(self, config:Path|None=None, client:Path|None=None, fallback:Path|None=None):
|
self,
|
||||||
|
config_parent: Path | None = None,
|
||||||
|
client_dir: Path | None = None,
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
Create config-folder structure and check permissions.
|
Create config-folder structure and check permissions.
|
||||||
Copy all assets if needed.
|
Copy all assets if needed.
|
||||||
@@ -210,34 +212,63 @@ class Paths(metaclass=Singleton):
|
|||||||
user's home directory.
|
user's home directory.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if config is not None:
|
if config_parent is not None:
|
||||||
self.base_path = config.resolve()
|
self.config_parent = config_parent.resolve()
|
||||||
else:
|
else:
|
||||||
self.base_path = default_base_path()
|
self.config_parent = Paths.get_default_config_parent_dir()
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
if multiprocessing.current_process().name == "MainProcess":
|
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
|
# Path copy only on MainProcess
|
||||||
if not self.app_dir.exists():
|
if not self.config_dir.exists():
|
||||||
self.app_dir.mkdir(parents=True)
|
self.config_dir.mkdir(parents=True)
|
||||||
|
|
||||||
# TODO: find a platform independent way to access module globals like `Paths`
|
# TODO: find a platform independent way to access module globals like `Paths`
|
||||||
# TODO: move this into multithreading management class
|
# 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()
|
os.environ["SWINGMUSIC_CLIENT_DIR"] = self.client_path.resolve().as_posix()
|
||||||
|
|
||||||
self.mkdir_config_folders()
|
self.setup_config_dirs()
|
||||||
self.copy_assets_dir()
|
|
||||||
|
|
||||||
|
@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.
|
Create the config/cache folder structure.
|
||||||
|
|
||||||
@@ -262,10 +293,9 @@ class Paths(metaclass=Singleton):
|
|||||||
└───lyrics
|
└───lyrics
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
# all dirs relative to `swingmusic` config dir
|
# all dirs relative to `swingmusic` config dir
|
||||||
dirs = [
|
dirs = [
|
||||||
"", # `swingmusic` or `.swingmusic`
|
"", # `swingmusic` or `.swingmusic`
|
||||||
"plugins/lyrics",
|
"plugins/lyrics",
|
||||||
"images/playlists",
|
"images/playlists",
|
||||||
"images/thumbnails/small",
|
"images/thumbnails/small",
|
||||||
@@ -282,7 +312,7 @@ class Paths(metaclass=Singleton):
|
|||||||
]
|
]
|
||||||
|
|
||||||
for folder in dirs:
|
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():
|
if not path.exists():
|
||||||
path.mkdir(parents=True)
|
path.mkdir(parents=True)
|
||||||
path.chmod(mode=0o755)
|
path.chmod(mode=0o755)
|
||||||
@@ -290,7 +320,9 @@ class Paths(metaclass=Singleton):
|
|||||||
# Empty files to create
|
# Empty files to create
|
||||||
empty_files = [
|
empty_files = [
|
||||||
# artist split ignore list
|
# 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:
|
for file in empty_files:
|
||||||
@@ -301,32 +333,6 @@ class Paths(metaclass=Singleton):
|
|||||||
file.parent.mkdir(parents=True, exist_ok=True)
|
file.parent.mkdir(parents=True, exist_ok=True)
|
||||||
file.touch()
|
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
|
@property
|
||||||
def config_folder_name(self) -> str:
|
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,
|
When the base path is the same as the home dir,
|
||||||
it returns `.swingmusic` else `swingmusic`
|
it returns `.swingmusic` else `swingmusic`
|
||||||
"""
|
"""
|
||||||
if self.base_path == self.USER_HOME_DIR:
|
if self.config_parent == self.USER_HOME_DIR:
|
||||||
return ".swingmusic"
|
return ".swingmusic"
|
||||||
else:
|
else:
|
||||||
return "swingmusic"
|
return "swingmusic"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def app_dir(self) -> Path:
|
def config_dir(self) -> Path:
|
||||||
return self.base_path / self.config_folder_name
|
return self.config_parent / self.config_folder_name
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def img_path(self) -> Path:
|
def img_path(self) -> Path:
|
||||||
return self.app_dir / "images"
|
return self.config_dir / "images"
|
||||||
|
|
||||||
# ARTISTS
|
# ARTISTS
|
||||||
@property
|
@property
|
||||||
@@ -384,7 +390,7 @@ class Paths(metaclass=Singleton):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def lg_thumb_path(self) -> pathlib.Path:
|
def lg_thumb_path(self) -> pathlib.Path:
|
||||||
return self.thumbs_path/ "large"
|
return self.thumbs_path / "large"
|
||||||
|
|
||||||
# OTHERS
|
# OTHERS
|
||||||
@property
|
@property
|
||||||
@@ -393,11 +399,11 @@ class Paths(metaclass=Singleton):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def assets_path(self) -> pathlib.Path:
|
def assets_path(self) -> pathlib.Path:
|
||||||
return self.app_dir / "assets"
|
return self.config_dir / "assets"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def plugins_path(self) -> pathlib.Path:
|
def plugins_path(self) -> pathlib.Path:
|
||||||
return self.app_dir / "plugins"
|
return self.config_dir / "plugins"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def lyrics_plugins_path(self) -> pathlib.Path:
|
def lyrics_plugins_path(self) -> pathlib.Path:
|
||||||
@@ -405,23 +411,23 @@ class Paths(metaclass=Singleton):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def config_file_path(self) -> pathlib.Path:
|
def config_file_path(self) -> pathlib.Path:
|
||||||
return self.app_dir/ "settings.json"
|
return self.config_dir / "settings.json"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def mixes_img_path(self) -> pathlib.Path:
|
def mixes_img_path(self) -> pathlib.Path:
|
||||||
return self.img_path/ "mixes"
|
return self.img_path / "mixes"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def artist_mixes_img_path(self) -> pathlib.Path:
|
def artist_mixes_img_path(self) -> pathlib.Path:
|
||||||
return self.mixes_img_path/ "artists"
|
return self.mixes_img_path / "artists"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def og_mixes_img_path(self) -> pathlib.Path:
|
def og_mixes_img_path(self) -> pathlib.Path:
|
||||||
return self.mixes_img_path/ "original"
|
return self.mixes_img_path / "original"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def md_mixes_img_path(self) -> pathlib.Path:
|
def md_mixes_img_path(self) -> pathlib.Path:
|
||||||
return self.mixes_img_path/ "medium"
|
return self.mixes_img_path / "medium"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def sm_mixes_img_path(self) -> pathlib.Path:
|
def sm_mixes_img_path(self) -> pathlib.Path:
|
||||||
@@ -433,15 +439,15 @@ class Paths(metaclass=Singleton):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def app_db_path(self):
|
def app_db_path(self):
|
||||||
return Paths().app_dir / self.APP_DB_NAME
|
return Paths().config_dir / self.APP_DB_NAME
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def userdata_db_path(self):
|
def userdata_db_path(self):
|
||||||
return Paths().app_dir / self.USER_DATA_DB_NAME
|
return Paths().config_dir / self.USER_DATA_DB_NAME
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def json_config_path(self):
|
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_TRACKNAME = "Martin & Gina"
|
||||||
API_CARD_LIMIT = 6
|
API_CARD_LIMIT = 6
|
||||||
|
|
||||||
|
|
||||||
class TCOLOR:
|
class TCOLOR:
|
||||||
"""
|
"""
|
||||||
Terminal colors
|
Terminal colors
|
||||||
@@ -492,4 +499,4 @@ class TCOLOR:
|
|||||||
ENDC = "\033[0m"
|
ENDC = "\033[0m"
|
||||||
BOLD = "\033[1m"
|
BOLD = "\033[1m"
|
||||||
UNDERLINE = "\033[4m"
|
UNDERLINE = "\033[4m"
|
||||||
# credits: https://stackoverflow.com/a/287944
|
# credits: https://stackoverflow.com/a/287944
|
||||||
|
|||||||
@@ -18,4 +18,4 @@ def log_startup_info(host: str, port: int):
|
|||||||
f"{TCOLOR.OKGREEN}http://{address}:{port}{TCOLOR.ENDC}"
|
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")
|
||||||
|
|||||||
@@ -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.crons import start_cron_jobs
|
||||||
from swingmusic.plugins.register import register_plugins
|
from swingmusic.plugins.register import register_plugins
|
||||||
from swingmusic.setup import load_into_mem, run_setup
|
from swingmusic.setup import load_into_mem, run_setup
|
||||||
from swingmusic.start_info_logger import log_startup_info
|
from swingmusic.start_info_logger import log_startup_info
|
||||||
from swingmusic.utils.threading import background
|
from swingmusic.utils.threading import background
|
||||||
from swingmusic.logger import setup_logger
|
|
||||||
|
|
||||||
import pathlib
|
|
||||||
import setproctitle
|
import setproctitle
|
||||||
|
|
||||||
import mimetypes
|
import mimetypes
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def config_mimetypes():
|
def config_mimetypes():
|
||||||
# Load mimetypes for the web client's static files
|
# Load mimetypes for the web client's static files
|
||||||
# Loading mimetypes should happen automaticaly but
|
# Loading mimetypes should happen automaticaly but
|
||||||
@@ -35,7 +34,24 @@ def config_mimetypes():
|
|||||||
mimetypes.add_type("application/manifest+json", ".webmanifest")
|
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.
|
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 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 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.
|
# Example: Setting up dirs, database, and loading stuff into memory.
|
||||||
# TIP: Be careful with the order of the setup functions.
|
# TIP: Be careful with the order of the setup functions.
|
||||||
# NOTE: concurrent and multithreading create own sys.modules -> no globals
|
# NOTE: concurrent and multithreading create own sys.modules -> no globals
|
||||||
settings.Paths(**path)
|
|
||||||
|
|
||||||
config_mimetypes()
|
config_mimetypes()
|
||||||
run_setup()
|
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}")
|
setproctitle.setproctitle(f"swingmusic {host}:{port}")
|
||||||
start_cron_jobs()
|
start_cron_jobs()
|
||||||
|
|
||||||
|
|
||||||
app = app_builder.build()
|
app = app_builder.build()
|
||||||
|
|
||||||
log_startup_info(host, port)
|
log_startup_info(host, port)
|
||||||
@@ -95,4 +117,4 @@ def start_swingmusic(host: str, port: int, path:dict[str,pathlib.Path|None]):
|
|||||||
threads=100,
|
threads=100,
|
||||||
ipv6=True,
|
ipv6=True,
|
||||||
ipv4=True,
|
ipv4=True,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -10,12 +10,13 @@ from swingmusic.settings import Paths
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from PIL import Image
|
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.
|
Handles the --password-reset argument. Resets the password.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
Paths(**path)
|
Paths(config_parent=config_parent)
|
||||||
|
|
||||||
setup_sqlite()
|
setup_sqlite()
|
||||||
|
|
||||||
@@ -64,4 +65,4 @@ def create_image(width, height, color1, color2):
|
|||||||
# Paste the resized image onto the padded image
|
# Paste the resized image onto the padded image
|
||||||
padded_image.paste(image, (x, y), image)
|
padded_image.paste(image, (x, y), image)
|
||||||
|
|
||||||
return padded_image
|
return padded_image
|
||||||
|
|||||||
+1
-1
@@ -5,7 +5,7 @@ import pathlib
|
|||||||
|
|
||||||
hiddenimports =[]
|
hiddenimports =[]
|
||||||
# hiddenimports += collect_submodules('swingmusic')
|
# hiddenimports += collect_submodules('swingmusic')
|
||||||
datas = [('client', 'client')]
|
datas = [('client.zip', '.')]
|
||||||
datas += collect_data_files('swingmusic', True, excludes=['**/*.py'], includes=['**/*.*'])
|
datas += collect_data_files('swingmusic', True, excludes=['**/*.py'], includes=['**/*.*'])
|
||||||
datas += collect_data_files('flask_openapi3', True, excludes=['**/*.py'], includes=['**/*.*'])
|
datas += collect_data_files('flask_openapi3', True, excludes=['**/*.py'], includes=['**/*.*'])
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user