fix client: download fallback to github release client

+ add fallback release version data to version.txt
+ move classproperty class to utils
+ update Dockerfile to install from source using pip install
+ move version info to Metadata class in settings.py
This commit is contained in:
wanji
2025-12-07 23:19:34 +03:00
parent d2b2ba6e02
commit 506c45c4fa
10 changed files with 86 additions and 61 deletions
+3
View File
@@ -193,6 +193,9 @@ jobs:
- name: Checkout into repo - name: Checkout into repo
uses: actions/checkout@v3 uses: actions/checkout@v3
- name: Create version.txt
run: echo ${{ github.event.inputs.tag }} > version.txt
- name: Download artifact - name: Download artifact
uses: actions/download-artifact@v4 uses: actions/download-artifact@v4
+12 -11
View File
@@ -1,21 +1,22 @@
FROM python:3.11-slim FROM python:3.11-slim
WORKDIR /app/swingmusic WORKDIR /app
# Copy the files in the current dir into the container
# copy wheelhouse and client
COPY wheels wheels
LABEL "author"="swing music" LABEL "author"="swing music"
EXPOSE 1970/tcp EXPOSE 1970/tcp
VOLUME /music VOLUME /music
VOLUME /config VOLUME /config
RUN apt-get update && apt-get install -y gcc git libev-dev python3-dev ffmpeg libavcodec-extra && \ RUN apt-get update
apt-get clean && \
rm -rf /var/lib/apt/lists/*
RUN pip install --no-cache-dir --find-links=wheels/ swingmusic RUN apt-get install -y gcc libev-dev
Run rm -rf /app/swingmusic/wheels RUN apt-get install -y ffmpeg libavcodec-extra
RUN apt-get clean && rm -rf /var/lib/apt/lists/*
# Copy repo root files needed for installation
COPY pyproject.toml requirements.txt version.txt ./
COPY src/ ./src/
# Install the package and its dependencies
RUN pip install --no-cache-dir .
ENTRYPOINT ["python", "-m", "swingmusic", "--host", "0.0.0.0", "--config", "/config"] ENTRYPOINT ["python", "-m", "swingmusic", "--host", "0.0.0.0", "--config", "/config"]
+6 -2
View File
@@ -1,5 +1,4 @@
from dataclasses import asdict from dataclasses import asdict
from importlib import metadata
from typing import Any from typing import Any
from flask_openapi3 import Tag from flask_openapi3 import Tag
from flask_openapi3 import APIBlueprint from flask_openapi3 import APIBlueprint
@@ -9,6 +8,7 @@ from swingmusic.api.auth import admin_required
from swingmusic.db.userdata import PluginTable from swingmusic.db.userdata import PluginTable
from swingmusic.lib.index import index_everything from swingmusic.lib.index import index_everything
from swingmusic.config import UserConfig from swingmusic.config import UserConfig
from swingmusic.settings import Metadata
from swingmusic.utils.auth import get_current_userid from swingmusic.utils.auth import get_current_userid
bp_tag = Tag(name="Settings", description="Customize stuff") bp_tag = Tag(name="Settings", description="Customize stuff")
@@ -102,7 +102,11 @@ def get_all_settings():
config[key] = sorted(list(value)) config[key] = sorted(list(value))
config["plugins"] = [p for p in PluginTable.get_all()] config["plugins"] = [p for p in PluginTable.get_all()]
config["version"] = metadata.version("swingmusic") config["version"] = Metadata.version
if config["version"] == "0.0.0":
# fallback to version.txt (useful for docker builds)
config["version"] = open("version.txt", "r").read().strip()
# only return lastfmSessionKey for the current user # only return lastfmSessionKey for the current user
current_user = get_current_userid() current_user = get_current_userid()
+2 -3
View File
@@ -1,4 +1,3 @@
from importlib import metadata
import datetime as dt import datetime as dt
import pathlib import pathlib
import logging import logging
@@ -13,7 +12,7 @@ from flask_jwt_extended import JWTManager, create_access_token, get_jwt, get_jwt
from swingmusic import api as swing_api from swingmusic import api as swing_api
from swingmusic.config import UserConfig from swingmusic.config import UserConfig
from swingmusic.db.userdata import UserTable from swingmusic.db.userdata import UserTable
from swingmusic.settings import Paths from swingmusic.settings import Metadata, Paths
from swingmusic.utils.paths import get_client_files_extensions from swingmusic.utils.paths import get_client_files_extensions
from swingmusic.api.plugins import lyrics as lyrics_plugin from swingmusic.api.plugins import lyrics as lyrics_plugin
@@ -102,7 +101,7 @@ def load_plugins(web: OpenAPI):
api_info = Info( api_info = Info(
title="Swing Music", title="Swing Music",
version=f"v{metadata.version('swingmusic')}", version=f"v{Metadata.version}",
description="The REST API exposed by your Swing Music server", description="The REST API exposed by your Swing Music server",
) )
+35 -18
View File
@@ -16,7 +16,9 @@ from pathlib import Path
import os import os
import logging import logging
import requests import requests
from importlib import resources as imres from importlib import metadata, resources as imres
from swingmusic.utils import classproperty
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@@ -45,9 +47,7 @@ class AssetHandler:
Handles all assets configuration Handles all assets configuration
""" """
CLIENT_RELEASES_URL = ( RELEASES_URL = "https://api.github.com/repos/swingmx/swingmusic/releases"
"https://api.github.com/repos/swingmx/swingmusic/releases/latest"
)
@staticmethod @staticmethod
def copy_assets_dir(): def copy_assets_dir():
@@ -104,20 +104,25 @@ class AssetHandler:
Downloads the latest supported client from Github Downloads the latest supported client from Github
and places it in the swingmusic client folder. and places it in the swingmusic client folder.
""" """
path = Paths().config_parent / "client" log.error("Default client not found. Downloading from GitHub ...")
path = Paths().client_path
try: try:
answer = requests.get(AssetHandler.CLIENT_RELEASES_URL).json() # INFO: downlaod the current version of the client from GitHub
releases = requests.get(AssetHandler.RELEASES_URL).json()
for asset in answer["assets"]: # INFO: find the release for the current version
for release in releases:
if release["tag_name"] == f"v{Metadata.version}":
# INFO: find the client.zip asset
for asset in release["assets"]:
if asset["name"] == "client.zip": if asset["name"] == "client.zip":
# download and convert client # download and extract client
client = requests.get(asset["browser_download_url"]) clientzip = requests.get(asset["browser_download_url"])
mem_file = io.BytesIO(client.content) mem_file = io.BytesIO(clientzip.content)
file = zipfile.ZipFile(mem_file) file = zipfile.ZipFile(mem_file)
# create new dir for extraction # create new dir for extraction
log.info(f"Storing client in '{path.as_posix()}'.")
with tempfile.TemporaryDirectory() as temp_folder: with tempfile.TemporaryDirectory() as temp_folder:
file.extractall(temp_folder) file.extractall(temp_folder)
@@ -128,6 +133,8 @@ class AssetHandler:
dirs_exist_ok=True, dirs_exist_ok=True,
) )
log.info("Client downloaded successfully.")
break
break break
except ( except (
@@ -140,12 +147,6 @@ class AssetHandler:
exc_info=e, exc_info=e,
) )
return False 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: except zipfile.BadZipfile as e:
log.error("Client could not be unpacked. ZIP ERROR", exc_info=e) log.error("Client could not be unpacked. ZIP ERROR", exc_info=e)
return False return False
@@ -155,8 +156,9 @@ class AssetHandler:
""" """
Runs on startup to ensure the default client is present. Runs on startup to ensure the default client is present.
""" """
extracted = True
client_path = Paths().client_path client_path = Paths().client_path
extracted = False
if not client_path.exists() or not (client_path / "index.html").exists(): if not client_path.exists() or not (client_path / "index.html").exists():
extracted = cls.extract_default_client(Paths().config_dir) extracted = cls.extract_default_client(Paths().config_dir)
@@ -504,3 +506,18 @@ class TCOLOR:
BOLD = "\033[1m" BOLD = "\033[1m"
UNDERLINE = "\033[4m" UNDERLINE = "\033[4m"
# credits: https://stackoverflow.com/a/287944 # credits: https://stackoverflow.com/a/287944
class Metadata:
"""
Contains metadata for the application.
"""
@classproperty
def version(self) -> str:
version = metadata.version("swingmusic")
if version == "0.0.0":
return open("version.txt", "r").read().strip()
return version
+3 -6
View File
@@ -1,10 +1,9 @@
from swingmusic.settings import TCOLOR, Paths from swingmusic.settings import TCOLOR, Metadata, Paths
from swingmusic.utils.network import get_ip from swingmusic.utils.network import get_ip
from importlib import metadata
def log_startup_info(host: str, port: int): def log_startup_info(host: str, port: int):
print(f"{TCOLOR.HEADER}Swing Music v{metadata.version('swingmusic')} {TCOLOR.ENDC}") print(f"{TCOLOR.HEADER}Swing Music v{Metadata.version} {TCOLOR.ENDC}")
addresses = [host] addresses = [host]
@@ -14,8 +13,6 @@ def log_startup_info(host: str, port: int):
print("Server running on:\n") print("Server running on:\n")
for address in addresses: for address in addresses:
print( print(f"{TCOLOR.OKGREEN}http://{address}:{port}{TCOLOR.ENDC}")
f"{TCOLOR.OKGREEN}http://{address}:{port}{TCOLOR.ENDC}"
)
print(f"\n{TCOLOR.YELLOW}Data folder: {Paths().config_dir}{TCOLOR.ENDC}\n") print(f"\n{TCOLOR.YELLOW}Data folder: {Paths().config_dir}{TCOLOR.ENDC}\n")
-1
View File
@@ -1,5 +1,4 @@
import socket import socket
import sys
from swingmusic import app_builder 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
+1 -7
View File
@@ -6,6 +6,7 @@ from typing import Callable, Iterable
from swingmusic.db.libdata import TrackTable from swingmusic.db.libdata import TrackTable
from swingmusic.models import Track from swingmusic.models import Track
from swingmusic.utils import classproperty
from swingmusic.utils.auth import get_current_userid from swingmusic.utils.auth import get_current_userid
from swingmusic.utils.remove_duplicates import remove_duplicates from swingmusic.utils.remove_duplicates import remove_duplicates
@@ -61,14 +62,7 @@ class TrackGroup:
return len(self.tracks) return len(self.tracks)
class classproperty(property):
"""
A class property decorator.
"""
def __get__(self, owner_self, owner_cls):
if self.fget:
return self.fget(owner_cls)
class TrackStore: class TrackStore:
+10
View File
@@ -19,3 +19,13 @@ def flatten(list_: Iterable[list[T]]) -> list[T]:
Flattens a list of lists into a single list. Flattens a list of lists into a single list.
""" """
return [item for sublist in list_ for item in sublist] return [item for sublist in list_ for item in sublist]
class classproperty(property):
"""
A class property decorator.
"""
def __get__(self, owner_self, owner_cls):
if self.fget:
return self.fget(owner_cls)
+1
View File
@@ -0,0 +1 @@
2.1.0