mirror of
https://github.com/Dvorinka/swingmusic-extended.git
synced 2026-06-03 20:13:02 +00:00
merge refactors pr #364 from @michilyy
* Save to DB only unique trackhashes * Add check if track already exists in playlist * replace all paths with `pathlib.Path` * `architecture.md`: * add config folder layout `config.py`: * fix bug where `pathlib.Path` cannot be serialized `files.py`: * remove unused imports * update path concatenation to `pathlib.Path` * add config-folder creation `imgserver.py`: * fix serialisation bug `playlistlib.py`: * update path concatenation to `pathlib.Path` * update all `settings.Paths` usages to new singleton `Paths` class. * update all usages of `settings.Paths` * `files.py`: * rework assets copy function. * remove unused loop and unused `shutil.copy2` function `settings.py` * fix recursion exception in `Paths` * `settings.py`: * remove Singleton and `@property` todos from `Paths` * `__init__.py`: * remove now unused function `create_config_dir()` `setup.files`: * remove because merged into `settings.Paths()` for more central and clear flow how the base path gets decided `settings.py`: * add `copy_assets` function `start_swingmusic.py`: * add configurable settings.Paths class `__main__.py`: * update click to used correct default path * remove wrong commited egg files * remove change in the wrong branch * add forgotten `property` decorator update `get_files_and_dirs` to use pathlib where possible `config.py`: * update type annotation `folders.py`: * convert `pathlib` to posix path where needed for sub-functions `folderlib.py`: * rework `get_files_and_dirs` to use `pathlib` where possible `settings.py`: * add forgotten `@property` `start_swingmusic.py`: * remove second `log_startup_info()` * `artistlib.py`: * fix calling property `tagger.py`: * fix comparing elements in `pathlib.Path` * add support for repeating lyrics. * rework lyrics api and lib * update most path functions. add type-hint pathlib where needed * for serialization paths are converted to posix path * use `open` instead of `os.open` update `metaclass` with constant * fix initial config exception if empty file existed * update `userConfig` with `InitVar` to be excluded from `asdict` * remove `is_windows_slash()` rework path function to use pathlib * convert `pathlib.Path` to `str` for serialization * fixing bug with str + pathlib * `__main__.py`: * update click to use package version * remove now unused function `print_version` `filesystem.py`: * rework `CWD` to use importlib `pyproject.toml`: * disable namespace for `importlib.resources` to work correctly * update `lyrics.py`: * remove unused functions * simplify functions * fix bug where assets get created on root * remove unused code * update lyrics for clearer structure. * add support for unsynced lyrics * fix wrong return type in unsynced lyrics * update `/check` to use `send_lyrics` * prefer tags to duplicates * `lyrics.py`: * add docs to a function group * `logger.py`: * add logging config dict. * combine Logging into one file * add socket logger * add debug mode to logger * add JSONL formater * `logger.py`: * update config to directly use the formater. resolves circular import exception `__main__.py`: * add logger setup to main `start_swingmusic.py`: * add debug option to cli * `lyrics.py`: * add offset support * add `setuptools-scm` to get version from git * add support for docker build with scm * add support for docker build with scm need someone who can test the changes workflow * update all usage of `version.txt` to `metadata.version()` * 2x update all usage of `version.txt` to `metadata.version()` * update to no local_scheme version * provide fix for #331. convert `sql.Row` and `TrackTable` to dict before converting to dataclass. * fix `__main__.py`: * wrong import and uncommited changes * add debug and base_path parameter * fix logger pathlib * add client build workflow * set name * split client from build * try fixing builds * try another fix * try also another fix * try again something new * try again something new * change runner * fix failed run because of malformed runner * add wheel builds * remove systems from pure python build * add isolated pyinstaller build * artifacts with names * wrong wheel path * try fetch-depth for tag fetch * disable fail-fast. add wheel installation * add install system packages * add debug * fix wheel install fix pyinstaller spec file * try fix for pyinstaller * try another fix * build on release * add concrete release types * only run on released or pre-released * try release upload * reformat upload * fix needs tag * identifiable pyinstaller builds * compress client folder before uploading * update to src build * remove no more needed aarch64 build script rename pyinstaller assets to lowercase * remove unneeded code * fix: save to DB only unique track hashes * replace click with argparse * set concrete types in argparse * replace manuall path usages with pathlib * remove unused `configs.py` file * reformat `start_swingmusic.py` * fix empty set startup exception * optimizing static files serve function * fixing bug in optimisation of static files serve function * fix folder view bug * colorlib.py: * fix wrong type exception * remove singe use Index_everything class * update logging of populate.py * cleanup files * fix settings.py Paths copy function. Created folder on file. * add exist check to folder * remove unused `INFO` class * fix multiprocessing bug on windows * potential icon fix for pyinstaller fix multiple logging bug * fix argparse config path bug add jobs file * cleanup code fragments fix logging issue add notes to function * note that concurrent creates own sys.modules * refactor some lyrics plugin condition remove unused import from hashing * refactor taglib.py * update import statements to be static * playlistlib.py: * refactoring and more doc strings populate.py: * add poc bugfix settings.py: * add typehint * possible bugfix for multitreading globals * folder.py: * add check if provided path is absolute populate.py: * add bug note settings.py: * add possible error from Singleton implementation start_swingmusic.py: * correct spelling * pass resolved path to Paths tagger.py: * add logging * trying out fixes for multithreading * only upload results not metadata * fix build action again * folder.py: * strictly use pathlib where possible folderlib.py: * add missing docstring to function, who really need it. track.py: * refactor some code folder.py: * refactor some more code * Merge DBPath class and Paths class. Update all usages of DBPath folderslib.py: * fix bug with logging taglib.py: * add missing docstring settings.py: * merge classes * refactor * network.py: * add more docstring config.py: * update pathlib usage tools.py: * refactor * add docstrings * colorlib.py: * add docstring Refactor App builder into grouped config settings. * update assets access for migration * Update FUNDING.yml * Update FUNDING.yml * upgrade tinytag in requirements.txt * update readme * update license * update readme * Update README.md * Update README.md * cleanup requirements.txt remove unused import in audio_segment.py add entrypoint.sh for appimage support update pyproject.toml for optional dependencies add appimage to github workflow * fix invalid workflow file * AppImage build needs more research. Commenting for now * testing a new build workflow * add libev installation * update workflow to new optional dependencies * trying again another fix * finally fix all optional deps installation correctly * remove AppImage poc * albumslib.py: * add docstring folder.py: * add unix path fix update logger name to `__name__` * update build with docker update Dockerfile with git fix typo in lyrics.py add dynamic deps back * add log for static folder * add missing import * add some more todos * add support for AppImages even when it's not perfect. * quick bugfix for wrong appimage config path * fix uploading not finding AppImages builds aka wrong pattern * optimise docker build by using artifacts. Add client path option. change docstring to sphinx format * add todos * Now support AppImages for real: manually build AppImage as we are building a complex project. * fix missing dep in AppImage build * add full AppImage metadata * add missing image file. * only update swingmusic appimage not tool * add todo and fix AppImage build again. * Try fixing some path mixup in AppImage build * add debug tag to action * correct path to appimage folder * do not download tool before checkout * Another fix for path in appimage build * extend config files with more information * default client dir is now inside the config dir. TODOs updated. * default client dir is now inside the config dir. TODOs updated. Add priority todos. * Auto download client when client not found. Respects user provided dir. * rename `requests` submodule to `request` * poc for arm AppImage builds * try out another fix * fix typo in build.yml * add missing arch tag * fix uploading double names * unique naming * enable fallback version for project. * do not download client into readonly dir. * fix relative client download path. Client was resolved into parent of config. * remove client backup path as client is now downloadable * `Paths` checks if config folder exists and creates it if necessary. logger no more creates the config folder. `app_builder.py`: static route no more with '/client' * path are only created in MainProcess. fix gz file not found. * move assets into src and update usages accordingly * remove solved todos * Only upload artefacts if not draft/master aka only on tag * wrong type in assets copy * update log with correct priority * add debug statements and logging to Paths * remove debugging statement * remove double version tag from docker build * fork save release protection * fix typo * add fallback client dir for static builds. * update argparse to new param * add missing import pathlib * add sparse checkout as we do not need everything downloaded * add assets copy check * init logger bevor Paths * remove unused import * check if logdir exists and create if not * only add exec info to file * remove exception log from cli * move logging into main. Allows tools support again. * UserConfig now correctly uses _finished key. Bug where _finished was never written * double save serverId. update root_dir to trow no exception on init. remove debug param * clean up TODOs --------- Co-authored-by: skilletfun <skilletfun.laptew.sergey@yandex.ru> Co-authored-by: Mungai Njoroge <geoffreymungai45@gmail.com>
This commit is contained in:
@@ -0,0 +1,244 @@
|
||||
from importlib import metadata
|
||||
import datetime as dt
|
||||
import pathlib
|
||||
import logging
|
||||
|
||||
from flask import Response, request
|
||||
from flask_cors import CORS
|
||||
from flask_compress import Compress
|
||||
from flask_openapi3 import Info
|
||||
from flask_openapi3 import OpenAPI
|
||||
from flask_jwt_extended import JWTManager, create_access_token, get_jwt, get_jwt_identity, set_access_cookies, verify_jwt_in_request
|
||||
|
||||
from swingmusic import api as swing_api
|
||||
from swingmusic.config import UserConfig
|
||||
from swingmusic.db.userdata import UserTable
|
||||
from swingmusic.settings import Paths
|
||||
from swingmusic.utils.paths import get_client_files_extensions
|
||||
|
||||
from swingmusic.api.plugins import lyrics as lyrics_plugin
|
||||
from swingmusic.api.plugins import mixes as mixes_plugin
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
# # # # # # # # # # # # # # # # # #
|
||||
# Grouped configuration function #
|
||||
# # # # # # # # # # # # # # # # # #
|
||||
|
||||
def config_app(web):
|
||||
|
||||
# CORS
|
||||
CORS(web, origins="*", supports_credentials=True)
|
||||
|
||||
# RESPONSE COMPRESSION
|
||||
# Only compress JSON responses
|
||||
Compress(web)
|
||||
web.config["COMPRESS_MIMETYPES"] = [
|
||||
"application/json",
|
||||
]
|
||||
|
||||
|
||||
def config_jwt(web):
|
||||
# JWT CONFIGS
|
||||
web.config["JWT_VERIFY_SUB"] = False
|
||||
web.config["JWT_SECRET_KEY"] = UserConfig().serverId
|
||||
web.config["JWT_TOKEN_LOCATION"] = ["cookies", "headers"]
|
||||
web.config["JWT_COOKIE_CSRF_PROTECT"] = False
|
||||
web.config["JWT_SESSION_COOKIE"] = False
|
||||
|
||||
jwt_expiry = int(dt.timedelta(days=30).total_seconds())
|
||||
web.config["JWT_ACCESS_TOKEN_EXPIRES"] = jwt_expiry
|
||||
|
||||
jwt = JWTManager(web)
|
||||
|
||||
@jwt.user_lookup_loader
|
||||
def user_lookup_callback(_jwt_header, jwt_data):
|
||||
identity = jwt_data["sub"]
|
||||
userid = identity["id"]
|
||||
user = UserTable.get_by_id(userid)
|
||||
|
||||
if user:
|
||||
return user.todict()
|
||||
|
||||
|
||||
def load_endpoints(web):
|
||||
# Register all the API blueprints
|
||||
with web.app_context():
|
||||
web.register_api(swing_api.album.api)
|
||||
web.register_api(swing_api.artist.api)
|
||||
web.register_api(swing_api.stream.api)
|
||||
web.register_api(swing_api.search.api)
|
||||
web.register_api(swing_api.folder.api)
|
||||
web.register_api(swing_api.playlist.api)
|
||||
web.register_api(swing_api.favorites.api)
|
||||
web.register_api(swing_api.imgserver.api)
|
||||
web.register_api(swing_api.settings.api)
|
||||
web.register_api(swing_api.colors.api)
|
||||
web.register_api(swing_api.lyrics.api)
|
||||
web.register_api(swing_api.backup_and_restore.api)
|
||||
web.register_api(swing_api.collections.api)
|
||||
|
||||
# Logger
|
||||
web.register_api(swing_api.scrobble.api)
|
||||
|
||||
# Home
|
||||
web.register_api(swing_api.home.api)
|
||||
web.register_api(swing_api.getall.api)
|
||||
|
||||
# Auth
|
||||
web.register_api(swing_api.auth.api)
|
||||
|
||||
|
||||
def load_plugins(web):
|
||||
# TODO: rework plugin support
|
||||
# Plugins
|
||||
web.register_api(swing_api.plugins.api)
|
||||
web.register_api(lyrics_plugin.api)
|
||||
web.register_api(mixes_plugin.api)
|
||||
|
||||
|
||||
# # # # # # # # # # #
|
||||
# Create App object #
|
||||
# # # # # # # # # # #
|
||||
|
||||
api_info = Info(
|
||||
title="Swing Music",
|
||||
version=f"v{metadata.version('swingmusic')}",
|
||||
description="The REST API exposed by your Swing Music server",
|
||||
)
|
||||
|
||||
app = OpenAPI(__name__, info=api_info, doc_prefix="/docs")
|
||||
|
||||
|
||||
def check_auth_need() -> bool:
|
||||
"""
|
||||
Check if the current request is for a static file.
|
||||
We do not need auth for index or static images of index.
|
||||
|
||||
:return: True if static file else False
|
||||
"""
|
||||
|
||||
# INFO: Routes that don't need authentication
|
||||
urls = {
|
||||
"/auth/login",
|
||||
"/auth/users",
|
||||
"/auth/pair",
|
||||
"/auth/logout",
|
||||
"/auth/refresh",
|
||||
"/docs",
|
||||
}
|
||||
files = {
|
||||
".webp",
|
||||
".jpg",
|
||||
*get_client_files_extensions()
|
||||
}
|
||||
|
||||
urls = tuple(urls)
|
||||
files = tuple(files)
|
||||
|
||||
if request.path == "/" or request.path.endswith(files):
|
||||
return True
|
||||
|
||||
# if request path starts with any of the blacklisted routes, don't verify jwt
|
||||
if request.path.startswith(urls):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
# # # # # # # # # # # # #
|
||||
# global endpoint logic #
|
||||
# # # # # # # # # # # # #
|
||||
|
||||
@app.route("/<path:path>")
|
||||
def serve_client_files(path: str):
|
||||
"""
|
||||
Serves the static files in the client folder.
|
||||
"""
|
||||
|
||||
# TODO: rule out possible double /client path.
|
||||
# path sometimes prepended with /client like '/client/some.js' resolves to '/client/client/some.js'
|
||||
|
||||
js_or_css = path.endswith(".js") or path.endswith(".css")
|
||||
|
||||
if not js_or_css:
|
||||
return app.send_static_file(path)
|
||||
|
||||
# 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:
|
||||
return app.send_static_file(path)
|
||||
|
||||
if "gzip" in request.headers.get("Accept-Encoding", ""):
|
||||
gz_name = path + ".gz"
|
||||
gzipped_path = pathlib.Path(app.static_folder or "") / gz_name
|
||||
|
||||
if gzipped_path.exists():
|
||||
response = app.make_response(app.send_static_file(gz_name))
|
||||
response.headers["Content-Encoding"] = "gzip"
|
||||
return response
|
||||
|
||||
return app.send_static_file(path)
|
||||
|
||||
|
||||
@app.route("/")
|
||||
def serve_client():
|
||||
"""
|
||||
Serves the index.html file at `client/index.html`.
|
||||
"""
|
||||
return app.send_static_file("index.html")
|
||||
|
||||
|
||||
def build() -> OpenAPI:
|
||||
"""
|
||||
Call this function to obtain the final flask/openapi object.
|
||||
|
||||
Do not import app directly as the static_folder can only be set
|
||||
when cli args are parsed.
|
||||
|
||||
:return: OpenApi object with all config set
|
||||
"""
|
||||
|
||||
# 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():
|
||||
"""
|
||||
Verifies the JWT token before each request.
|
||||
"""
|
||||
|
||||
if check_auth_need():
|
||||
return
|
||||
|
||||
verify_jwt_in_request()
|
||||
|
||||
@app.after_request
|
||||
def refresh_expiring_jwt(response: Response):
|
||||
"""
|
||||
Refreshes the cookies JWT token after each request.
|
||||
"""
|
||||
|
||||
# INFO: If the request has an Authorization header, don't refresh the jwt
|
||||
# Request is probably from the mobile client or a third party
|
||||
if check_auth_need() or request.headers.get("Authorization"):
|
||||
return response
|
||||
|
||||
try:
|
||||
exp_timestamp = get_jwt()["exp"]
|
||||
until = dt.datetime.now(dt.timezone.utc) + dt.timedelta(days=7)
|
||||
|
||||
if until.timestamp() > exp_timestamp:
|
||||
access_token = create_access_token(identity=get_jwt_identity())
|
||||
set_access_cookies(response, access_token)
|
||||
|
||||
return response
|
||||
except (RuntimeError, KeyError):
|
||||
return response
|
||||
|
||||
config_app(app)
|
||||
config_jwt(app)
|
||||
load_endpoints(app)
|
||||
load_plugins(app)
|
||||
|
||||
return app
|
||||
Reference in New Issue
Block a user