diff --git a/.gitignore b/.gitignore index 7cbee9ee..b2412028 100644 --- a/.gitignore +++ b/.gitignore @@ -27,7 +27,7 @@ client logs.txt *.spec -TODO.md +# TODO.md testdata.py test.py nohup.out diff --git a/TODO.md b/TODO.md new file mode 100644 index 00000000..c78efee4 --- /dev/null +++ b/TODO.md @@ -0,0 +1,2 @@ +- Fix migrations! + - Use total length instead of release version length \ No newline at end of file diff --git a/app/api/auth.py b/app/api/auth.py index 83dfda38..b8af863a 100644 --- a/app/api/auth.py +++ b/app/api/auth.py @@ -14,7 +14,7 @@ from flask_openapi3 import Tag from flask_openapi3 import APIBlueprint from app.db.sqlite.auth import SQLiteAuthMethods as authdb -from app.utils.auth import check_password, encode_password +from app.utils.auth import check_password, hash_password from app.config import UserConfig bp_tag = Tag(name="Auth", description="Authentication stuff") @@ -113,7 +113,7 @@ def update_profile(body: UpdateProfileBody): user["roles"] = json.dumps(body.roles) if user["password"]: - user["password"] = encode_password(user["password"]) + user["password"] = hash_password(user["password"]) # remove empty values clean_user = {k: v for k, v in user.items() if v} @@ -132,7 +132,7 @@ def create_user(body: UpdateProfileBody): user = { "username": body.username, - "password": encode_password(body.password), + "password": hash_password(body.password), "roles": json.dumps([]), } @@ -232,7 +232,9 @@ def get_all_users(query: GetAllUsersQuery): users = authdb.get_all_users() is_admin = current_user and "admin" in current_user["roles"] - settings['enableGuest'] = [user for user in users if user.username == "guest"].__len__() > 0 + settings["enableGuest"] = [ + user for user in users if user.username == "guest" + ].__len__() > 0 # if user is admin, also return settings if is_admin: @@ -252,7 +254,6 @@ def get_all_users(query: GetAllUsersQuery): ): return res - # remove guest user # if not settings["enableGuest"]: # users = [user for user in users if user.username != "guest"] diff --git a/app/api/imgserver.py b/app/api/imgserver.py index 680abba1..c5e2db49 100644 --- a/app/api/imgserver.py +++ b/app/api/imgserver.py @@ -13,6 +13,9 @@ api = APIBlueprint("imgserver", __name__, url_prefix="/img", abp_tags=[bp_tag]) def send_fallback_img(filename: str = "default.webp"): + """ + Returns the fallback image from the assets folder. + """ folder = Paths.get_assets_path() img = Path(folder) / filename @@ -22,6 +25,18 @@ def send_fallback_img(filename: str = "default.webp"): return send_from_directory(folder, filename) +def send_file_or_fallback(folder: str, filename: str, fallback: str = "default.webp"): + """ + Returns the file from the folder or the fallback image. + """ + fpath = Path(folder) / filename + + if fpath.exists(): + return send_from_directory(folder, filename) + + return send_fallback_img(fallback) + + class ImagePath(BaseModel): imgpath: str = Field( description="The image filename", @@ -43,62 +58,72 @@ class ImagePath(BaseModel): # return send_fallback_img() -@api.get("/t/") +# TRACK THUMBNAILS +@api.get("/thumbnail/") def send_lg_thumbnail(path: ImagePath): """ Get large thumbnail (500 x 500) """ folder = Paths.get_lg_thumb_path() - fpath = Path(folder) / path.imgpath - - if fpath.exists(): - return send_from_directory(folder, path.imgpath) - - return send_fallback_img() + return send_file_or_fallback(folder, path.imgpath) -@api.get("/t/s/") +@api.get("/thumbnail/xsmall/") +def send_xsm_thumbnail(path: ImagePath): + """ + Get extra small thumbnail (64px) + """ + folder = Paths.get_xsm_thumb_path() + return send_file_or_fallback(folder, path.imgpath) + + +@api.get("/thumbnail/small/") def send_sm_thumbnail(path: ImagePath): """ - Get small thumbnail (64 x 64) + Get small thumbnail (96px) """ folder = Paths.get_sm_thumb_path() - fpath = Path(folder) / path.imgpath - - if fpath.exists(): - return send_from_directory(folder, path.imgpath) - - return send_fallback_img() + return send_file_or_fallback(folder, path.imgpath) -@api.get("/a/") +@api.get("/thumbnail/medium/") +def send_md_thumbnail(path: ImagePath): + """ + Get medium thumbnail (256px) + """ + folder = Paths.get_md_thumb_path() + return send_file_or_fallback(folder, path.imgpath) + + +# ARTISTS +@api.get("/artist/") def send_lg_artist_image(path: ImagePath): """ Get large artist image (500 x 500) """ - folder = Paths.get_artist_img_lg_path() - fpath = Path(folder) / path.imgpath - - if fpath.exists(): - return send_from_directory(folder, path.imgpath) - - return send_fallback_img("artist.webp") + folder = Paths.get_lg_artist_img_path() + return send_file_or_fallback(folder, path.imgpath, "artist.webp") -@api.get("/a/s/") +@api.get("/artist/small/") def send_sm_artist_image(path: ImagePath): """ - Get small artist image (64 x 64) + Get small artist image (128) """ - folder = Paths.get_artist_img_sm_path() - fpath = Path(folder) / path.imgpath - - if fpath.exists(): - return send_from_directory(folder, path.imgpath) - - return send_fallback_img("artist.webp") + folder = Paths.get_sm_artist_img_path() + return send_file_or_fallback(folder, path.imgpath, "artist.webp") +@api.get("/artist/medium/") +def send_md_artist_image(path: ImagePath): + """ + Get medium artist image (256px) + """ + folder = Paths.get_md_artist_img_path() + return send_file_or_fallback(folder, path.imgpath, "artist.webp") + + +# PLAYLISTS class PlaylistImagePath(BaseModel): imgpath: str = Field( description="The image path", @@ -106,7 +131,7 @@ class PlaylistImagePath(BaseModel): ) -@api.get("/p/") +@api.get("/playlist/") def send_playlist_image(path: PlaylistImagePath): """ Get playlist image @@ -114,9 +139,4 @@ def send_playlist_image(path: PlaylistImagePath): Images are constructed as '{playlist_id}.webp' """ folder = Paths.get_playlist_img_path() - fpath = Path(folder) / path.imgpath - - if fpath.exists(): - return send_from_directory(folder, path.imgpath) - - return send_fallback_img("playlist.svg") + return send_file_or_fallback(folder, path.imgpath, "playlist.svg") diff --git a/app/api/playlist.py b/app/api/playlist.py index 91461fdf..8ccd1582 100644 --- a/app/api/playlist.py +++ b/app/api/playlist.py @@ -26,6 +26,7 @@ api = APIBlueprint("playlists", __name__, url_prefix="/playlists", abp_tags=[tag PL = SQLitePlaylistMethods + class SendAllPlaylistsQuery(BaseModel): no_images: bool = Field(False, description="Whether to include images") @@ -410,7 +411,7 @@ def save_item_as_playlist(body: SavePlaylistAsItemBody): filename = itemhash + ".webp" base_path = ( - Paths.get_artist_img_lg_path() + Paths.get_lg_artist_img_path() if itemtype == "artist" else Paths.get_lg_thumb_path() ) diff --git a/app/arg_handler.py b/app/arg_handler.py index a1aa5037..b74acfe4 100644 --- a/app/arg_handler.py +++ b/app/arg_handler.py @@ -2,6 +2,7 @@ Handles arguments passed to the program. """ +from getpass import getpass import os.path import sys @@ -10,27 +11,37 @@ import PyInstaller.__main__ as bundler from app import settings from app.logger import log from app.print_help import HELP_MESSAGE +from app.utils.auth import hash_password from app.utils.paths import getFlaskOpenApiPath from app.utils.xdg_utils import get_xdg_config_dir from app.utils.wintools import is_windows +from app.db.sqlite.auth import SQLiteAuthMethods as authdb ALLARGS = settings.ALLARGS ARGS = sys.argv[1:] class ProcessArgs: + """ + Processes the arguments passed to the program. + """ + def __init__(self) -> None: + # resolve config path + self.handle_config_path() # 1 + + # handles that exit + self.handle_password_recovery() self.handle_build() - self.handle_host() - self.handle_port() - self.handle_config_path() - - self.handle_periodic_scan() - self.handle_periodic_scan_interval() - self.handle_help() self.handle_version() + # non-exiting handles + self.handle_host() + self.handle_port() + self.handle_periodic_scan() + self.handle_periodic_scan_interval() + @staticmethod def handle_build(): """ @@ -57,12 +68,13 @@ class ProcessArgs: value = settings.Info.get(key) if not value: - log.error(f"WARNING: {key} not set in environment") + log.error(f"WARNING: {key} not resolved. Exiting ...") sys.exit(0) lines.append(f'{key} = "{value}"\n') try: + # write the info to the config file with open("./app/configs.py", "w", encoding="utf-8") as file: # copy the api keys to the config file file.writelines(lines) @@ -189,3 +201,38 @@ class ProcessArgs: f"COMMIT#: {settings.Info.GIT_CURRENT_BRANCH}/{settings.Info.GIT_LATEST_COMMIT_HASH}" ) sys.exit(0) + + @staticmethod + def handle_password_recovery(): + if ALLARGS.pswd in ARGS: + print("SWING MUSIC v2.0.0 ") + print("PASSWORD RECOVERY \n") + + username: str = "" + password: str = "" + + # collect username + try: + username = input("Enter username: ") + except KeyboardInterrupt: + print("\nOperation cancelled! Exiting ...") + sys.exit(0) + + username = username.strip() + user = authdb.get_user_by_username(username) + + if not user: + print(f"User {username} not found") + sys.exit(0) + + # collect password + try: + password = getpass("Enter new password: ") + except KeyboardInterrupt: + print("\nOperation cancelled! Exiting ...") + sys.exit(0) + + password = hash_password(password) + user = authdb.update_user({"id": user.id, "password": password}) + + sys.exit(0) diff --git a/app/db/sqlite/auth.py b/app/db/sqlite/auth.py index 2c36d183..3d9da234 100644 --- a/app/db/sqlite/auth.py +++ b/app/db/sqlite/auth.py @@ -1,6 +1,6 @@ import json from app.models.user import User -from app.utils.auth import encode_password +from app.utils.auth import hash_password from app.db.sqlite.utils import SQLiteManager @@ -44,7 +44,7 @@ class SQLiteAuthMethods: """ user = { "username": "admin", - "password": encode_password("admin"), + "password": hash_password("admin"), "roles": json.dumps(["admin"]), } return SQLiteAuthMethods.insert_user(user) @@ -56,7 +56,7 @@ class SQLiteAuthMethods: """ user = { "username": "guest", - "password": encode_password("guest"), + "password": hash_password("guest"), "roles": json.dumps(["guest"]), } @@ -67,7 +67,7 @@ class SQLiteAuthMethods: """ Update a user in the database. - :param user: A dict with the username, password and roles. + :param user: A dict with the user id and the fields to update. Ommited fields will not be updated. """ # get all user dict keys keys = list(user.keys()) @@ -75,6 +75,7 @@ class SQLiteAuthMethods: {', '.join([f"{key} = :{key}" for key in keys if key != 'id'])} WHERE id = :id """ + print(sql, user) with SQLiteManager(userdata_db=True) as cur: cur.execute(sql, user) diff --git a/app/db/sqlite/migrations.py b/app/db/sqlite/migrations.py index 86d36899..550673a0 100644 --- a/app/db/sqlite/migrations.py +++ b/app/db/sqlite/migrations.py @@ -7,9 +7,9 @@ from app.db.sqlite.utils import SQLiteManager class MigrationManager: @staticmethod - def get_version() -> int: + def get_index() -> int: """ - Returns the latest userdata database version. + Returns the latest databases migrations index. """ sql = "SELECT * FROM dbmigrations" with SQLiteManager() as cur: @@ -21,9 +21,9 @@ class MigrationManager: # 👇 Setters 👇 @staticmethod - def set_version(version: int): + def set_index(version: int): """ - Sets the userdata pre-init database version. + Updates the databases migrations index. """ sql = "UPDATE dbmigrations SET version = ? WHERE id = 1" with SQLiteManager() as cur: diff --git a/app/lib/artistlib.py b/app/lib/artistlib.py index f49fcf0c..b40cb56a 100644 --- a/app/lib/artistlib.py +++ b/app/lib/artistlib.py @@ -55,13 +55,22 @@ def get_artist_image_link(artist: str): # TODO: Move network calls to utils/network.py class DownloadImage: def __init__(self, url: str, name: str) -> None: - sm_path = Path(settings.Paths.get_artist_img_sm_path()) / name - lg_path = Path(settings.Paths.get_artist_img_lg_path()) / name - img = self.download(url) - if img is not None: - self.save_img(img, sm_path, lg_path) + if img is None: + return + + sm_path = Path(settings.Paths.get_sm_artist_img_path()) / name + lg_path = Path(settings.Paths.get_lg_artist_img_path()) / name + md_path = Path(settings.Paths.get_md_artist_img_path()) / name + + entries = [ + (lg_path, None), # save in the original size + (sm_path, settings.Defaults.SM_ARTIST_IMG_SIZE), + (md_path, settings.Defaults.MD_ARTIST_IMG_SIZE), + ] + + self.save_img(img, entries) @staticmethod def download(url: str) -> Image.Image | None: @@ -74,14 +83,21 @@ class DownloadImage: return None @staticmethod - def save_img(img: Image.Image, sm_path: Path, lg_path: Path): + def save_img(img: Image.Image, entries: list[tuple[Path, int | None]]): """ Saves the image to the destinations. """ - img.save(lg_path, format="webp") + ratio = img.width / img.height + for entry in entries: + path, size = entry - sm_size = settings.Defaults.SM_ARTIST_IMG_SIZE - img.resize((sm_size, sm_size), Image.ANTIALIAS).save(sm_path, format="webp") + if size is None: + img.save(path, format="webp") + continue + + img.resize((size, int(size / ratio)), Image.ANTIALIAS).save( + path, format="webp" + ) class CheckArtistImages: @@ -90,7 +106,7 @@ class CheckArtistImages: CHECK_ARTIST_IMAGES_KEY = instance_key # read all files in the artist image folder - path = settings.Paths.get_artist_img_sm_path() + path = settings.Paths.get_sm_artist_img_path() processed = "".join(os.listdir(path)).replace("webp", "") # filter out artists that already have an image @@ -126,7 +142,7 @@ class CheckArtistImages: return img_path = ( - Path(settings.Paths.get_artist_img_sm_path()) / f"{artist.artisthash}.webp" + Path(settings.Paths.get_sm_artist_img_path()) / f"{artist.artisthash}.webp" ) if img_path.exists(): diff --git a/app/lib/colorlib.py b/app/lib/colorlib.py index 17dd5c4c..086fcb2b 100644 --- a/app/lib/colorlib.py +++ b/app/lib/colorlib.py @@ -42,7 +42,7 @@ def process_color(item_hash: str, is_album=True): path = ( settings.Paths.get_sm_thumb_path() if is_album - else settings.Paths.get_artist_img_sm_path() + else settings.Paths.get_sm_artist_img_path() ) path = Path(path) / (item_hash + ".webp") diff --git a/app/lib/taglib.py b/app/lib/taglib.py index 2a523144..3d7e9f6d 100644 --- a/app/lib/taglib.py +++ b/app/lib/taglib.py @@ -33,20 +33,22 @@ def extract_thumb(filepath: str, webp_path: str, overwrite=False) -> bool: """ lg_img_path = os.path.join(Paths.get_lg_thumb_path(), webp_path) sm_img_path = os.path.join(Paths.get_sm_thumb_path(), webp_path) + xms_img_path = os.path.join(Paths.get_xsm_thumb_path(), webp_path) + md_img_path = os.path.join(Paths.get_md_thumb_path(), webp_path) - tsize = Defaults.THUMB_SIZE - sm_tsize = Defaults.SM_THUMB_SIZE + images = [ + (lg_img_path, Defaults.LG_THUMB_SIZE), + (sm_img_path, Defaults.SM_THUMB_SIZE), + (xms_img_path, Defaults.XSM_THUMB_SIZE), + (md_img_path, Defaults.MD_THUMB_SIZE), + ] def save_image(img: Image.Image): width, height = img.size ratio = width / height - img.resize((tsize, int(tsize / ratio)), Image.ANTIALIAS).save( - lg_img_path, "webp" - ) - img.resize((sm_tsize, int(sm_tsize / ratio)), Image.ANTIALIAS).save( - sm_img_path, "webp" - ) + for path, size in images: + img.resize((size, int(size / ratio)), Image.ANTIALIAS).save(path, "webp") if not overwrite and os.path.exists(sm_img_path): img_size = os.path.getsize(sm_img_path) diff --git a/app/migrations/__init__.py b/app/migrations/__init__.py index 58efc9f0..e6d5c431 100644 --- a/app/migrations/__init__.py +++ b/app/migrations/__init__.py @@ -2,12 +2,6 @@ Migrations module. Reads and applies the latest database migrations. - -PLEASE NOTE: OLDER MIGRATIONS CAN NEVER BE DELETED. -ONLY MODIFY OLD MIGRATIONS FOR BUG FIXES OR ENHANCEMENTS ONLY -[TRY NOT TO MODIFY BEHAVIOR, UNLESS YOU KNOW WHAT YOU'RE DOING]. - -PS: Fuck that! Do what you want. """ from app.db.sqlite.migrations import MigrationManager @@ -33,21 +27,32 @@ migrations: list[list[Migration]] = [ def apply_migrations(): """ Applies the latest database migrations. + + The length of all the migrations is stored in the database + and used to check for new migrations. When the length of the + migrations list is larger than the number stored in the db, + migrations past that index are applied and the new length + is stored as the new migration index. """ - version = MigrationManager.get_version() + index = MigrationManager.get_index() + all_migrations = [migration for sublist in migrations for migration in sublist] - if version != len(migrations): - # INFO: Apply new migrations - for migration in migrations[version:]: - for m in migration: - try: - m.migrate() - log.info("Applied migration: %s", m.__name__) - except: - log.error("Failed to run migration: %s", m.__name__) - - print("Migrations applied successfully.") - print("Current migration version: ", len(migrations)) - # bump migration version - MigrationManager.set_version(len(migrations)) + to_apply: list[Migration] = [] + + # if index is from old release, + # get migrations from the "migrations" list + if index < 3: + _migrations = migrations[index:] + to_apply = [migration for sublist in _migrations for migration in sublist] + else: + to_apply = all_migrations[index:] + + for migration in to_apply: + try: + migration.migrate() + log.info("Applied migration: %s", migration.__name__) + except: + log.error("Failed to run migration: %s", migration.__name__) + + MigrationManager.set_index(len(all_migrations)) diff --git a/app/migrations/v1_4_9/__init__.py b/app/migrations/v1_4_9/__init__.py index 0b37ee98..6f0de717 100644 --- a/app/migrations/v1_4_9/__init__.py +++ b/app/migrations/v1_4_9/__init__.py @@ -58,3 +58,19 @@ class DeleteOriginalThumbnails(Migration): if os.path.exists(og_imgpath): shutil.rmtree(og_imgpath) + +class DeleteOriginalThumbnailsa(Migration): + """ + Original thumbnails are too large and are not needed. + """ + + # TODO: Implement this migration + + @staticmethod + def migrate(): + imgpath = Paths.get_thumbs_path() + og_imgpath = os.path.join(imgpath, "original") + + if os.path.exists(og_imgpath): + shutil.rmtree(og_imgpath) + diff --git a/app/periodic_scan.py b/app/periodic_scan.py index bf7d8c79..7a0af7e3 100644 --- a/app/periodic_scan.py +++ b/app/periodic_scan.py @@ -13,6 +13,12 @@ from app.logger import log def run_periodic_scans(): """ Runs periodic scans. + + Periodic scans are checks that run every few minutes + in the background to do stuff like: + - checking for new music + - delete deleted entries + - downloading artist images, and other data. """ # ValidateAlbumThumbs() # ValidatePlaylistThumbs() diff --git a/app/print_help.py b/app/print_help.py index 78393340..2ce7825c 100644 --- a/app/print_help.py +++ b/app/print_help.py @@ -1,4 +1,4 @@ -from app.settings import ALLARGS +from app.settings import ALLARGS, Info from tabulate import tabulate args = ALLARGS @@ -10,6 +10,7 @@ help_args_list = [ ["--port", "", "Set the port"], ["--config", "", "Set the config path"], ["--no-periodic-scan", "-nps", "Disable periodic scan"], + ["--pswd", "", "Recover a password"], [ "--scan-interval", "-psi", @@ -23,10 +24,12 @@ help_args_list = [ ] HELP_MESSAGE = f""" -Swing Music is a beautiful, self-hosted music player for your -local audio files. Like a cooler Spotify ... but bring your own music. +Swing Music v{Info.SWINGMUSIC_APP_VERSION} -Usage: swingmusic [options] [args] +A beautiful, self-hosted music player for your local audio files. +Like Spotify ... but bring your own music. -{tabulate(help_args_list, headers=["Option", "Short", "Description"], tablefmt="simple_grid", maxcolwidths=[None, None, 40])} +Usage: ./swingmusic [options] [args] + +{tabulate(help_args_list, headers=["Option", "Alias", "Description"], tablefmt="psql", maxcolwidths=[None, None, 40])} """ diff --git a/app/settings.py b/app/settings.py index cbaf0b35..c3c308fd 100644 --- a/app/settings.py +++ b/app/settings.py @@ -45,22 +45,24 @@ class Paths: def get_img_path(cls): return join(cls.get_app_dir(), "images") + # ARTISTS @classmethod def get_artist_img_path(cls): return join(cls.get_img_path(), "artists") @classmethod - def get_artist_img_sm_path(cls): + def get_sm_artist_img_path(cls): return join(cls.get_artist_img_path(), "small") @classmethod - def get_artist_img_lg_path(cls): - return join(cls.get_artist_img_path(), "large") + def get_md_artist_img_path(cls): + return join(cls.get_artist_img_path(), "medium") @classmethod - def get_playlist_img_path(cls): - return join(cls.get_img_path(), "playlists") + def get_lg_artist_img_path(cls): + return join(cls.get_artist_img_path(), "large") + # TRACK THUMBNAILS @classmethod def get_thumbs_path(cls): return join(cls.get_img_path(), "thumbnails") @@ -69,10 +71,23 @@ class Paths: def get_sm_thumb_path(cls): return join(cls.get_thumbs_path(), "small") + @classmethod + def get_xsm_thumb_path(cls): + return join(cls.get_thumbs_path(), "xsmall") + + @classmethod + def get_md_thumb_path(cls): + return join(cls.get_thumbs_path(), "medium") + @classmethod def get_lg_thumb_path(cls): return join(cls.get_thumbs_path(), "large") + # OTHERS + @classmethod + def get_playlist_img_path(cls): + return join(cls.get_img_path(), "playlists") + @classmethod def get_assets_path(cls): return join(Paths.get_app_dir(), "assets") @@ -92,12 +107,25 @@ class Paths: # defaults class Defaults: - THUMB_SIZE = 512 - SM_THUMB_SIZE = 128 + """ + Contains default values for various settings. + + XSM_THUMB_SIZE: extra small thumbnail size for web client tracklist + SM_THUMB_SIZE: small thumbnail size for android client tracklist + MD_THUMB_SIZE: medium thumbnail size for web client album cards + LG_THUMB_SIZE: large thumbnail size for web client now playing album art + + NOTE: LG_ARTIST_IMG_SIZE is not defined as the images are saved in the original size (500px) + """ + + XSM_THUMB_SIZE = 64 + SM_THUMB_SIZE = 96 + MD_THUMB_SIZE = 256 + LG_THUMB_SIZE = 512 + SM_ARTIST_IMG_SIZE = 128 - """ - The size of extracted images in pixels - """ + MD_ARTIST_IMG_SIZE = 256 + HASH_LENGTH = 10 API_ALBUMHASH = "bfe300e966" API_ARTISTHASH = "cae59f1fc5" @@ -105,7 +133,6 @@ class Defaults: API_ALBUMNAME = "The Goat" API_ARTISTNAME = "Polo G" API_TRACKNAME = "Martin & Gina" - API_CARD_LIMIT = 6 @@ -162,6 +189,8 @@ class ALLARGS: host = "--host" config = "--config" + pswd = "--pswd" + show_feat = ("--show-feat", "-sf") show_prod = ("--show-prod", "-sp") dont_clean_albums = ("--no-clean-albums", "-nca") @@ -275,6 +304,7 @@ class Info: NOTE: This class initially written to load keys when running in build mode. TODO: Remove this class entirely, and implement functionality where needed. """ + SWINGMUSIC_APP_VERSION = os.environ.get("SWINGMUSIC_APP_VERSION") GIT_LATEST_COMMIT_HASH = "" GIT_CURRENT_BRANCH = "" @@ -289,12 +319,6 @@ class Info: cls.GIT_LATEST_COMMIT_HASH = getLatestCommitHash() cls.GIT_CURRENT_BRANCH = getCurrentBranch() - cls.verify_keys() - - @classmethod - def verify_keys(cls): - pass - @classmethod def get(cls, key: str): return getattr(cls, key, None) diff --git a/app/setup/__init__.py b/app/setup/__init__.py index 4f784bea..36e7e356 100644 --- a/app/setup/__init__.py +++ b/app/setup/__init__.py @@ -14,6 +14,9 @@ from app.config import UserConfig def run_setup(): + """ + Creates the config directory, runs migrations, and loads settings. + """ create_config_dir() # setup config file @@ -32,6 +35,11 @@ def run_setup(): # settings table is empty pass + +def load_into_mem(): + """ + Load all tracks, albums, and artists into memory. + """ instance_key = get_random_str() # INFO: Load all tracks, albums, and artists into memory diff --git a/app/setup/files.py b/app/setup/files.py index 46f6a001..4e875750 100644 --- a/app/setup/files.py +++ b/app/setup/files.py @@ -51,28 +51,28 @@ def create_config_dir() -> None: """ Creates the config directory if it doesn't exist. """ - thumb_path = os.path.join("images", "thumbnails") - small_thumb_path = os.path.join(thumb_path, "small") - large_thumb_path = os.path.join(thumb_path, "large") + sm_thumb_path = settings.Paths.get_sm_thumb_path() + lg_thumb_path = settings.Paths.get_lg_thumb_path() + md_thumb_path = settings.Paths.get_md_thumb_path() + xsm_thumb_path = settings.Paths.get_xsm_thumb_path() - artist_img_path = os.path.join("images", "artists") - small_artist_img_path = os.path.join(artist_img_path, "small") - large_artist_img_path = os.path.join(artist_img_path, "large") + small_artist_img_path = settings.Paths.get_sm_artist_img_path() + md_artist_img_path = settings.Paths.get_md_artist_img_path() + large_artist_img_path = settings.Paths.get_lg_artist_img_path() playlist_img_path = os.path.join("images", "playlists") dirs = [ "", # creates the config folder - "images", - "plugins", + sm_thumb_path, + lg_thumb_path, + md_thumb_path, + xsm_thumb_path, "plugins/lyrics", - thumb_path, - small_thumb_path, - large_thumb_path, - artist_img_path, + playlist_img_path, + md_artist_img_path, small_artist_img_path, large_artist_img_path, - playlist_img_path, ] for _dir in dirs: @@ -80,7 +80,9 @@ def create_config_dir() -> None: exists = os.path.exists(path) if not exists: - os.makedirs(path) + # exist_ok=True to create parent directories if they don't exist + os.makedirs(path, exist_ok=True) os.chmod(path, 0o755) + # copy assets to the app directory CopyFiles() diff --git a/app/start_info_logger.py b/app/start_info_logger.py index e27a361e..5b497341 100644 --- a/app/start_info_logger.py +++ b/app/start_info_logger.py @@ -10,7 +10,7 @@ def log_startup_info(): # os.system("cls" if os.name == "nt" else "echo -e \\\\033c") print(lines) - print(f"{TCOLOR.HEADER}SwingMusic {Info.SWINGMUSIC_APP_VERSION} {TCOLOR.ENDC}") + print(f"{TCOLOR.HEADER}Swing Music v{Info.SWINGMUSIC_APP_VERSION} {TCOLOR.ENDC}") adresses = [FLASKVARS.get_flask_host()] diff --git a/app/utils/auth.py b/app/utils/auth.py index e8b909af..87f20c75 100644 --- a/app/utils/auth.py +++ b/app/utils/auth.py @@ -4,13 +4,13 @@ import hashlib from app.config import UserConfig -def encode_password(password: str) -> str: +def hash_password(password: str) -> str: """ - This function encodes the given password. + Hashes the given password using sha256 algorithm and the user id as salt. - :param password: The password to encode. + :param password: The password to hash. - :return: The encoded password. + :return: The hashed password. """ return hashlib.pbkdf2_hmac( @@ -18,14 +18,14 @@ def encode_password(password: str) -> str: ).hex() -def check_password(password: str, encoded: str) -> bool: +def check_password(password: str, hashed: str) -> bool: """ - This function checks if the given password matches the encoded password. + This function checks if the given password matches the hashed password. :param password: The password to check. - :param encoded: The encoded password. + :param hashed: The hashed password. :return: Whether the password matches. """ - return hmac.compare_digest(encode_password(password), encoded) + return hmac.compare_digest(hash_password(password), hashed) diff --git a/manage.py b/manage.py index 4210463e..f27c26bd 100644 --- a/manage.py +++ b/manage.py @@ -25,7 +25,7 @@ from app.lib.watchdogg import Watcher as WatchDog from app.periodic_scan import run_periodic_scans from app.plugins.register import register_plugins from app.settings import FLASKVARS, TCOLOR, Info -from app.setup import run_setup +from app.setup import load_into_mem, run_setup from app.start_info_logger import log_startup_info from app.utils.filesystem import get_home_res_path from app.utils.paths import getClientFilesExtensions @@ -47,10 +47,9 @@ mimetypes.add_type("application/manifest+json", ".webmanifest") werkzeug = logging.getLogger("werkzeug") werkzeug.setLevel(logging.ERROR) - # Background tasks @background -def bg_run_setup() -> None: +def bg_run_setup(): run_periodic_scans() @@ -71,9 +70,10 @@ def run_swingmusic(): # Setup function calls +Info.load() ProcessArgs() run_setup() -Info.load() +load_into_mem() run_swingmusic()