From 8f6f1d1c0f08cdd7e4b0dd6602985373334b5554 Mon Sep 17 00:00:00 2001 From: geoffrey45 Date: Sat, 8 Apr 2023 20:26:45 +0300 Subject: [PATCH 1/7] use create_hash to compare album titles for is_single + add a few string checks for album.is_single --- app/lib/taglib.py | 1 + app/models/album.py | 13 ++++++++++--- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/app/lib/taglib.py b/app/lib/taglib.py index 9102b99..832f114 100644 --- a/app/lib/taglib.py +++ b/app/lib/taglib.py @@ -68,6 +68,7 @@ def extract_date(date_str: str | None, filepath: str) -> int: try: return int(date_str.split("-")[0]) except: # pylint: disable=bare-except + # TODO: USE FILEPATH LAST-MOD DATE instead of current date return datetime.date.today().today().year diff --git a/app/models/album.py b/app/models/album.py index b01d33c..2c5778f 100644 --- a/app/models/album.py +++ b/app/models/album.py @@ -3,6 +3,7 @@ from dataclasses import dataclass from .track import Track from .artist import Artist +from ..utils.hashing import create_hash @dataclass(slots=True) @@ -105,16 +106,22 @@ class Album: return self.title.strip().endswith(" EP") def check_is_single(self, tracks: list[Track]): + """ Checks if the album is a single. """ + keywords = ["single version", "- single"] + for keyword in keywords: + if keyword in self.title.lower(): + self.is_single = True + return + if ( len(tracks) == 1 - and tracks[0].title.lower() == self.title.lower() - + and create_hash(tracks[0].title) == create_hash(self.title) # if they have the same title # and tracks[0].track == 1 # and tracks[0].disc == 1 - # Todo: Are the above commented checks necessary? + # TODO: Review -> Are the above commented checks necessary? ): self.is_single = True From 1e4484b1c80f14738c5280836436088bbabc35bc Mon Sep 17 00:00:00 2001 From: geoffrey45 Date: Sun, 9 Apr 2023 01:01:48 +0300 Subject: [PATCH 2/7] add --config flag to modify config path + use getters instead of constants in settings classes + refactor previous references + move get_xdg_config_dir() from settings.py to app.utils.xdg_utils.py --- app/api/imgserver.py | 72 +++--------- app/arg_handler.py | 29 ++++- app/config.py | 2 +- app/db/sqlite/__init__.py | 8 -- app/db/sqlite/utils.py | 4 +- app/lib/artistlib.py | 6 +- app/lib/colorlib.py | 2 +- app/lib/playlistlib.py | 6 +- app/lib/taglib.py | 4 +- .../__preinit/move_to_xdg_folder.py | 2 +- app/models/playlist.py | 2 +- app/print_help.py | 3 +- app/settings.py | 108 ++++++++++-------- app/setup/files.py | 4 +- app/setup/sqlite.py | 4 +- app/start_info_logger.py | 2 +- app/utils/xdg_utils.py | 21 ++++ 17 files changed, 149 insertions(+), 130 deletions(-) create mode 100644 app/utils/xdg_utils.py diff --git a/app/api/imgserver.py b/app/api/imgserver.py index ad66986..c429897 100644 --- a/app/api/imgserver.py +++ b/app/api/imgserver.py @@ -5,21 +5,6 @@ from flask import Blueprint, send_from_directory from app.settings import Paths api = Blueprint("imgserver", __name__, url_prefix="/img") -SUPPORTED_IMAGES = (".jpg", ".png", ".webp", ".jpeg") - -APP_DIR = Path(Paths.APP_DIR) -IMG_PATH = APP_DIR / "images" -ASSETS_PATH = APP_DIR / "assets" - -THUMB_PATH = IMG_PATH / "thumbnails" -LG_THUMB_PATH = THUMB_PATH / "large" -SM_THUMB_PATH = THUMB_PATH / "small" - -ARTIST_PATH = IMG_PATH / "artists" -ARTIST_LG_PATH = ARTIST_PATH / "large" -ARTIST_SM_PATH = ARTIST_PATH / "small" - -PLAYLIST_PATH = IMG_PATH / "playlists" @api.route("/") @@ -28,86 +13,65 @@ def hello(): def send_fallback_img(filename: str = "default.webp"): - img = ASSETS_PATH / filename + path = Paths.get_assets_path() + img = Path(path) / filename if not img.exists(): return "", 404 - return send_from_directory(ASSETS_PATH, filename) + return send_from_directory(path, filename) @api.route("/t/") def send_lg_thumbnail(imgpath: str): - fpath = LG_THUMB_PATH / imgpath + path = Paths.get_lg_thumb_path() + fpath = Path(path) / imgpath if fpath.exists(): - return send_from_directory(LG_THUMB_PATH, imgpath) + return send_from_directory(path, imgpath) return send_fallback_img() @api.route("/t/s/") def send_sm_thumbnail(imgpath: str): - fpath = SM_THUMB_PATH / imgpath + path = Paths.get_sm_thumb_path() + fpath = Path(path) / imgpath if fpath.exists(): - return send_from_directory(SM_THUMB_PATH, imgpath) + return send_from_directory(path, imgpath) return send_fallback_img() @api.route("/a/") def send_lg_artist_image(imgpath: str): - fpath = ARTIST_LG_PATH / imgpath + path = Paths.get_artist_img_lg_path() + fpath = Path(path) / imgpath if fpath.exists(): - return send_from_directory(ARTIST_LG_PATH, imgpath) + return send_from_directory(path, imgpath) return send_fallback_img("artist.webp") @api.route("/a/s/") def send_sm_artist_image(imgpath: str): - fpath = ARTIST_SM_PATH / imgpath + path = Paths.get_artist_img_sm_path() + fpath = Path(path) / imgpath if fpath.exists(): - return send_from_directory(ARTIST_SM_PATH, imgpath) + return send_from_directory(path, imgpath) return send_fallback_img("artist.webp") @api.route("/p/") def send_playlist_image(imgpath: str): - fpath = PLAYLIST_PATH / imgpath + path = Paths.get_playlist_img_path() + fpath = Path(path) / imgpath if fpath.exists(): - return send_from_directory(PLAYLIST_PATH, imgpath) + return send_from_directory(path, imgpath) return send_fallback_img("playlist.svg") - - -# @app.route("/raw") -# @app.route("/raw/") -# def send_from_filepath(imgpath: str = ""): -# imgpath = "/" + imgpath -# filename = path.basename(imgpath) - -# def verify_is_image(): -# _, ext = path.splitext(filename) -# return ext in SUPPORTED_IMAGES - -# verified = verify_is_image() - -# if not verified: -# return imgpath, 404 - -# exists = path.exists(imgpath) - -# if verified and exists: -# return send_from_directory(path.dirname(imgpath), filename) - -# return imgpath, 404 - - -# def serve_imgs(): -# app.run(threaded=True, port=1971, host="0.0.0.0", debug=True) diff --git a/app/arg_handler.py b/app/arg_handler.py index cd82f3e..ac0a368 100644 --- a/app/arg_handler.py +++ b/app/arg_handler.py @@ -1,7 +1,7 @@ """ Handles arguments passed to the program. """ - +import os.path import sys from configparser import ConfigParser @@ -10,6 +10,10 @@ import PyInstaller.__main__ as bundler from app import settings from app.print_help import HELP_MESSAGE from app.utils.wintools import is_windows +from app.logger import log +from app.utils.xdg_utils import get_xdg_config_dir + +# from app.api.imgserver import set_app_dir config = ConfigParser() config.read("pyinstaller.config.ini") @@ -23,6 +27,7 @@ class HandleArgs: self.handle_build() self.handle_host() self.handle_port() + self.handle_config_path() self.handle_no_feat() self.handle_remove_prod() self.handle_help() @@ -89,6 +94,28 @@ class HandleArgs: settings.FLASKVARS.FLASK_HOST = host # type: ignore + @staticmethod + def handle_config_path(): + """ + Modifies the config path. + """ + if ALLARGS.config in ARGS: + index = ARGS.index(ALLARGS.config) + + try: + config_path = ARGS[index + 1] + + if os.path.exists(config_path): + settings.Paths.set_config_dir(config_path) + return + + log.warn(f"Config path {config_path} doesn't exist") + sys.exit(0) + except IndexError: + pass + + settings.Paths.set_config_dir(get_xdg_config_dir()) + @staticmethod def handle_no_feat(): # if ArgsEnum.no_feat in ARGS: diff --git a/app/config.py b/app/config.py index 8ea6e8c..999fdcd 100644 --- a/app/config.py +++ b/app/config.py @@ -57,5 +57,5 @@ class ConfigManager: self.write_config(config_data) -settings = ConfigManager(Db.JSON_CONFIG_PATH) +settings = ConfigManager(Db.get_json_config_path()) a = settings.get_value(ConfigKeys.ROOT_DIRS) diff --git a/app/db/sqlite/__init__.py b/app/db/sqlite/__init__.py index f7c5d97..25eaee9 100644 --- a/app/db/sqlite/__init__.py +++ b/app/db/sqlite/__init__.py @@ -17,14 +17,6 @@ def create_connection(db_file: str) -> SqlConn: return conn -def get_sqlite_conn(): - """ - It opens a connection to the database - :return: A connection to the database. - """ - return create_connection(Db.APP_DB_PATH) - - def create_tables(conn: SqlConn, sql_query: str): """ Executes the specifiend SQL file to create database tables. diff --git a/app/db/sqlite/utils.py b/app/db/sqlite/utils.py index 6c2c87c..53a4dae 100644 --- a/app/db/sqlite/utils.py +++ b/app/db/sqlite/utils.py @@ -78,10 +78,10 @@ class SQLiteManager: if self.conn is not None: return self.conn.cursor() - db_path = Db.APP_DB_PATH + db_path = Db.get_app_db_path() if self.userdata_db: - db_path = Db.USERDATA_DB_PATH + db_path = Db.get_userdata_db_path() self.conn = sqlite3.connect( db_path, diff --git a/app/lib/artistlib.py b/app/lib/artistlib.py index e36d70c..f519fc8 100644 --- a/app/lib/artistlib.py +++ b/app/lib/artistlib.py @@ -43,8 +43,8 @@ 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.ARTIST_IMG_SM_PATH) / name - lg_path = Path(settings.Paths.ARTIST_IMG_LG_PATH) / name + 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) @@ -90,7 +90,7 @@ class CheckArtistImages: :param artist: The artist name """ - img_path = Path(settings.Paths.ARTIST_IMG_SM_PATH) / f"{artist.artisthash}.webp" + img_path = Path(settings.Paths.get_artist_img_sm_path()) / f"{artist.artisthash}.webp" if img_path.exists(): return diff --git a/app/lib/colorlib.py b/app/lib/colorlib.py index a7999cb..245dfe7 100644 --- a/app/lib/colorlib.py +++ b/app/lib/colorlib.py @@ -34,7 +34,7 @@ def get_image_colors(image: str, count=1) -> list[str]: def process_color(item_hash: str, is_album=True): - path = settings.Paths.SM_THUMB_PATH if is_album else settings.Paths.ARTIST_IMG_SM_PATH + path = settings.Paths.get_sm_thumb_path() if is_album else settings.Paths.get_artist_img_sm_path() path = Path(path) / (item_hash + ".webp") if not path.exists(): diff --git a/app/lib/playlistlib.py b/app/lib/playlistlib.py index b5c50ed..dfceb0d 100644 --- a/app/lib/playlistlib.py +++ b/app/lib/playlistlib.py @@ -16,7 +16,7 @@ def create_thumbnail(image: Any, img_path: str) -> str: Creates a 250 x 250 thumbnail from a playlist image """ thumb_path = "thumb_" + img_path - full_thumb_path = os.path.join(settings.Paths.APP_DIR, "images", "playlists", thumb_path) + full_thumb_path = os.path.join(settings.Paths.get_app_dir(), "images", "playlists", thumb_path) aspect_ratio = image.width / image.height @@ -33,7 +33,7 @@ def create_gif_thumbnail(image: Any, img_path: str): Creates a 250 x 250 thumbnail from a playlist image """ thumb_path = "thumb_" + img_path - full_thumb_path = os.path.join(settings.Paths.APP_DIR, "images", "playlists", thumb_path) + full_thumb_path = os.path.join(settings.Paths.get_app_dir(), "images", "playlists", thumb_path) frames = [] @@ -60,7 +60,7 @@ def save_p_image(file, pid: str): filename = pid + str(random_str) + ".webp" - full_img_path = os.path.join(settings.Paths.PLAYLIST_IMG_PATH, filename) + full_img_path = os.path.join(settings.Paths.get_playlist_img_path(), filename) if file.content_type == "image/gif": frames = [] diff --git a/app/lib/taglib.py b/app/lib/taglib.py index 832f114..d0f5cab 100644 --- a/app/lib/taglib.py +++ b/app/lib/taglib.py @@ -27,8 +27,8 @@ def extract_thumb(filepath: str, webp_path: str) -> bool: """ Extracts the thumbnail from an audio file. Returns the path to the thumbnail. """ - img_path = os.path.join(Paths.LG_THUMBS_PATH, webp_path) - sm_img_path = os.path.join(Paths.SM_THUMB_PATH, webp_path) + 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) tsize = Defaults.THUMB_SIZE sm_tsize = Defaults.SM_THUMB_SIZE diff --git a/app/migrations/__preinit/move_to_xdg_folder.py b/app/migrations/__preinit/move_to_xdg_folder.py index 0d8cc45..0c7ab01 100644 --- a/app/migrations/__preinit/move_to_xdg_folder.py +++ b/app/migrations/__preinit/move_to_xdg_folder.py @@ -17,7 +17,7 @@ class MoveToXdgFolder: @staticmethod def migrate(): old_config_dir = os.path.join(Paths.USER_HOME_DIR, ".swing") - new_config_dir = Paths.APP_DIR + new_config_dir = Paths.get_app_dir() if not os.path.exists(old_config_dir): log.info("No old config folder found. Skipping migration.") diff --git a/app/models/playlist.py b/app/models/playlist.py index 8e4fefd..2d1e3c9 100644 --- a/app/models/playlist.py +++ b/app/models/playlist.py @@ -33,7 +33,7 @@ class Playlist: self.count = len(self.trackhashes) self.has_gif = bool(int(self.has_gif)) - self.has_image = (Path(settings.Paths.PLAYLIST_IMG_PATH) / str(self.image)).exists() + self.has_image = (Path(settings.Paths.get_playlist_img_path()) / str(self.image)).exists() if self.image is not None: self.thumb = "thumb_" + self.image diff --git a/app/print_help.py b/app/print_help.py index 016724b..4a53797 100644 --- a/app/print_help.py +++ b/app/print_help.py @@ -9,7 +9,8 @@ Options: {args.build}: Build the application (in development) {args.host}: Set the host {args.port}: Set the port - + {args.config}: Set the config path + {', '.join(args.show_feat)}: Do not extract featured artists from the song title {', '.join(args.show_prod)}: Do not hide producers in the song title diff --git a/app/settings.py b/app/settings.py index a5f2c0e..5cb8d4b 100644 --- a/app/settings.py +++ b/app/settings.py @@ -6,67 +6,71 @@ import os join = os.path.join -# ------- HELPER METHODS -------- -def get_xdg_config_dir(): - """ - Returns the XDG_CONFIG_HOME environment variable if it exists, otherwise - returns the default config directory. If none of those exist, returns the - user's home directory. - """ - xdg_config_home = os.environ.get("XDG_CONFIG_HOME") - - if xdg_config_home: - return xdg_config_home - - try: - alt_dir = join(os.environ.get("HOME"), ".config") - - if os.path.exists(alt_dir): - return alt_dir - except TypeError: - return os.path.expanduser("~") - - -# !------- HELPER METHODS --------! - class Release: - APP_VERSION = "v1.2.0" + APP_VERSION = "v1.2.1" class Paths: - XDG_CONFIG_DIR = get_xdg_config_dir() + XDG_CONFIG_DIR = "" USER_HOME_DIR = os.path.expanduser("~") - CONFIG_FOLDER = "swingmusic" if XDG_CONFIG_DIR != USER_HOME_DIR else ".swingmusic" + # TODO: Break this down into getter methods for each path - APP_DIR = join(XDG_CONFIG_DIR, CONFIG_FOLDER) - IMG_PATH = join(APP_DIR, "images") + @classmethod + def set_config_dir(cls, path: str): + cls.XDG_CONFIG_DIR = path - ARTIST_IMG_PATH = join(IMG_PATH, "artists") - ARTIST_IMG_SM_PATH = join(ARTIST_IMG_PATH, "small") - ARTIST_IMG_LG_PATH = join(ARTIST_IMG_PATH, "large") + @classmethod + def get_config_dir(cls): + return cls.XDG_CONFIG_DIR - PLAYLIST_IMG_PATH = join(IMG_PATH, "playlists") + @classmethod + def get_config_folder(cls): + return "swingmusic" if cls.get_config_dir() != cls.USER_HOME_DIR else ".swingmusic" - THUMBS_PATH = join(IMG_PATH, "thumbnails") - SM_THUMB_PATH = join(THUMBS_PATH, "small") - LG_THUMBS_PATH = join(THUMBS_PATH, "large") + @classmethod + def get_app_dir(cls): + return join(cls.get_config_dir(), cls.get_config_folder()) - # TEST_DIR = "/home/cwilvx/Downloads/Telegram Desktop" - # TEST_DIR = "/mnt/dfc48e0f-103b-426e-9bf9-f25d3743bc96/Music/Chill/Wolftyla Radio" - # HOME_DIR = TEST_DIR + @classmethod + def get_img_path(cls): + return join(cls.get_app_dir(), "images") + @classmethod + def get_artist_img_path(cls): + return join(cls.get_img_path(), "artists") -class Urls: - IMG_BASE_URI = "http://127.0.0.1:8900/images/" - IMG_ARTIST_URI = IMG_BASE_URI + "artists/" - IMG_THUMB_URI = IMG_BASE_URI + "thumbnails/" - IMG_PLAYLIST_URI = IMG_BASE_URI + "playlists/" + @classmethod + def get_artist_img_sm_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") + + @classmethod + def get_playlist_img_path(cls): + return join(cls.get_img_path(), "playlists") + + @classmethod + def get_thumbs_path(cls): + return join(cls.get_img_path(), "thumbnails") + + @classmethod + def get_sm_thumb_path(cls): + return join(cls.get_thumbs_path(), "small") + + @classmethod + def get_lg_thumb_path(cls): + return join(cls.get_thumbs_path(), "large") + + @classmethod + def get_assets_path(cls): + return join(Paths.get_app_dir(), "assets") # defaults class Defaults: - DEFAULT_ARTIST_IMG = Urls.IMG_ARTIST_URI + "0.webp" THUMB_SIZE = 400 SM_THUMB_SIZE = 64 SM_ARTIST_IMG_SIZE = 64 @@ -83,9 +87,18 @@ SUPPORTED_FILES = tuple(f".{file}" for file in FILES) class Db: APP_DB_NAME = "swing.db" USER_DATA_DB_NAME = "userdata.db" - APP_DB_PATH = join(Paths.APP_DIR, APP_DB_NAME) - USERDATA_DB_PATH = join(Paths.APP_DIR, USER_DATA_DB_NAME) - JSON_CONFIG_PATH = join(Paths.APP_DIR, "config.json") + + @classmethod + def get_app_db_path(cls): + return join(Paths.get_app_dir(), cls.APP_DB_NAME) + + @classmethod + def get_userdata_db_path(cls): + return join(Paths.get_app_dir(), cls.USER_DATA_DB_NAME) + + @classmethod + def get_json_config_path(cls): + return join(Paths.get_app_dir(), "config.json") class FLASKVARS: @@ -101,6 +114,7 @@ class ALLARGS: build = "--build" port = "--port" host = "--host" + config = "--config" show_feat = ["--show-feat", "-sf"] show_prod = ["--show-prod", "-sp"] help = ["--help", "-h"] diff --git a/app/setup/files.py b/app/setup/files.py index d54273c..bcdbb9c 100644 --- a/app/setup/files.py +++ b/app/setup/files.py @@ -33,7 +33,7 @@ class CopyFiles: files = [ { "src": assets_dir, - "dest": os.path.join(settings.Paths.APP_DIR, "assets"), + "dest": os.path.join(settings.Paths.get_app_dir(), "assets"), "is_dir": True, } ] @@ -83,7 +83,7 @@ def create_config_dir() -> None: ] for _dir in dirs: - path = os.path.join(settings.Paths.APP_DIR, _dir) + path = os.path.join(settings.Paths.get_app_dir(), _dir) exists = os.path.exists(path) if not exists: diff --git a/app/setup/sqlite.py b/app/setup/sqlite.py index a1c158e..f558e73 100644 --- a/app/setup/sqlite.py +++ b/app/setup/sqlite.py @@ -19,8 +19,8 @@ def setup_sqlite(): run_preinit_migrations() - app_db_conn = create_connection(Db.APP_DB_PATH) - playlist_db_conn = create_connection(Db.USERDATA_DB_PATH) + app_db_conn = create_connection(Db.get_app_db_path()) + playlist_db_conn = create_connection(Db.get_userdata_db_path()) create_tables(app_db_conn, queries.CREATE_APPDB_TABLES) create_tables(playlist_db_conn, queries.CREATE_USERDATA_TABLES) diff --git a/app/start_info_logger.py b/app/start_info_logger.py index 89b0818..4d43753 100644 --- a/app/start_info_logger.py +++ b/app/start_info_logger.py @@ -44,7 +44,7 @@ def log_startup_info(): ) print( - f"{TCOLOR.YELLOW}Data folder: {Paths.APP_DIR}{TCOLOR.ENDC}" + f"{TCOLOR.YELLOW}Data folder: {Paths.get_app_dir()}{TCOLOR.ENDC}" ) print("\n") diff --git a/app/utils/xdg_utils.py b/app/utils/xdg_utils.py new file mode 100644 index 0000000..5037d11 --- /dev/null +++ b/app/utils/xdg_utils.py @@ -0,0 +1,21 @@ +import os + + +def get_xdg_config_dir(): + """ + Returns the XDG_CONFIG_HOME environment variable if it exists, otherwise + returns the default config directory. If none of those exist, returns the + user's home directory. + """ + xdg_config_home = os.environ.get("XDG_CONFIG_HOME") + + if xdg_config_home: + return xdg_config_home + + try: + alt_dir = os.path.join(os.environ.get("HOME"), ".config") + + if os.path.exists(alt_dir): + return alt_dir + except TypeError: + return os.path.expanduser("~") From 74f52ce2e3cb66ce7ad51c704f1193bd40c21604 Mon Sep 17 00:00:00 2001 From: geoffrey45 Date: Sat, 15 Apr 2023 11:41:24 +0300 Subject: [PATCH 3/7] fix duplicate tracks due to use of extract_feat ~ caused duplicate tracks to have different track hashes --- app/db/sqlite/migrations.py | 9 ++++++--- app/models/track.py | 8 ++++++++ 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/app/db/sqlite/migrations.py b/app/db/sqlite/migrations.py index 93763f5..71a0865 100644 --- a/app/db/sqlite/migrations.py +++ b/app/db/sqlite/migrations.py @@ -2,14 +2,17 @@ Reads and saves the latest database migrations version. """ - from app.db.sqlite.utils import SQLiteManager class MigrationManager: all_get_sql = "SELECT * FROM migrations" - pre_init_set_sql = "UPDATE migrations SET pre_init_version = ? WHERE id = 1" - post_init_set_sql = "UPDATE migrations SET post_init_version = ? WHERE id = 1" + + _base = "UPDATE migrations SET" + _end = "= ? WHERE id = 1" + + pre_init_set_sql = f"{_base} pre_init_version {_end}" + post_init_set_sql = f"{_base} post_init_version {_end}" @classmethod def get_preinit_version(cls) -> int: diff --git a/app/models/track.py b/app/models/track.py index 81e91ce..0d1acf4 100644 --- a/app/models/track.py +++ b/app/models/track.py @@ -68,3 +68,11 @@ class Track: self.genre = str(self.genre).replace("/", ",").replace(";", ",") self.genre = str(self.genre).lower().split(",") self.genre = [g.strip() for g in self.genre] + + self.recreate_hash() + + def recreate_hash(self): + if self.og_title == self.title: + return + + self.trackhash = create_hash(", ".join([a.name for a in self.artist]), self.album, self.title) From f5615f4d31548b686720dbf2777402fc3b2fcd56 Mon Sep 17 00:00:00 2001 From: geoffrey45 Date: Sun, 16 Apr 2023 17:45:13 +0300 Subject: [PATCH 4/7] extract feat from album titles --- app/api/artist.py | 4 ++-- app/lib/searchlib.py | 2 +- app/models/album.py | 20 ++++++++++++++++++-- app/models/track.py | 19 +++++++++++++++++-- app/store/albums.py | 4 ++-- app/store/tracks.py | 35 +++++++++++++++++++++++------------ 6 files changed, 63 insertions(+), 21 deletions(-) diff --git a/app/api/artist.py b/app/api/artist.py index 0a93000..fb46cf3 100644 --- a/app/api/artist.py +++ b/app/api/artist.py @@ -242,7 +242,7 @@ def get_artist_albums(artisthash: str): albums = [a for a in albums if not a.is_single] return albums - albums = filter(lambda a: artisthash in a.albumartisthash, all_albums) + albums = filter(lambda a: artisthash in a.albumartists_hashes, all_albums) albums = list(albums) albums = remove_EPs_and_singles(albums) @@ -250,7 +250,7 @@ def get_artist_albums(artisthash: str): for c in compilations: albums.remove(c) - appearances = filter(lambda a: artisthash not in a.albumartisthash, all_albums) + appearances = filter(lambda a: artisthash not in a.albumartists_hashes, all_albums) appearances = list(appearances) appearances = remove_EPs_and_singles(appearances) diff --git a/app/lib/searchlib.py b/app/lib/searchlib.py index e2374ca..465ab9b 100644 --- a/app/lib/searchlib.py +++ b/app/lib/searchlib.py @@ -93,7 +93,7 @@ class SearchAlbums: Gets all albums with a given title. """ - albums = [unidecode(a.title).lower() for a in self.albums] + albums = [unidecode(a.og_title).lower() for a in self.albums] results = process.extract( self.query, diff --git a/app/models/album.py b/app/models/album.py index 2c5778f..9794639 100644 --- a/app/models/album.py +++ b/app/models/album.py @@ -4,6 +4,9 @@ from dataclasses import dataclass from .track import Track from .artist import Artist from ..utils.hashing import create_hash +from ..utils.parsers import parse_feat_from_title + +from app.settings import FromFlags @dataclass(slots=True) @@ -16,13 +19,14 @@ class Album: title: str albumartists: list[Artist] - albumartisthash: str = "" + albumartists_hashes: str = "" image: str = "" count: int = 0 duration: int = 0 colors: list[str] = dataclasses.field(default_factory=list) date: str = "" + og_title: str = "" is_soundtrack: bool = False is_compilation: bool = False is_single: bool = False @@ -32,8 +36,20 @@ class Album: genres: list[str] = dataclasses.field(default_factory=list) def __post_init__(self): + self.og_title = self.title self.image = self.albumhash + ".webp" - self.albumartisthash = "-".join(a.artisthash for a in self.albumartists) + + if FromFlags.EXTRACT_FEAT: + featured, self.title = parse_feat_from_title(self.title) + + if len(featured) > 0: + original_lower = "-".join([a.name.lower() for a in self.albumartists]) + self.albumartists.extend([Artist(a) for a in featured if a.lower() not in original_lower]) + + from ..store.tracks import TrackStore + TrackStore.append_track_artists(self.albumhash, featured) + + self.albumartists_hashes = "-".join(a.artisthash for a in self.albumartists) def set_colors(self, colors: list[str]): self.colors = colors diff --git a/app/models/track.py b/app/models/track.py index 0d1acf4..c2ac466 100644 --- a/app/models/track.py +++ b/app/models/track.py @@ -31,12 +31,13 @@ class Track: filetype: str = "" image: str = "" - artist_hashes: list[str] = dataclasses.field(default_factory=list) + artist_hashes: str = "" is_favorite: bool = False og_title: str = "" def __post_init__(self): self.og_title = self.title + if self.artist is not None: artists = split_artists(self.artist) new_title = self.title @@ -55,7 +56,7 @@ class Track: self.title = new_title - self.artist_hashes = [create_hash(a, decode=True) for a in artists] + self.artist_hashes = "-".join(create_hash(a, decode=True) for a in artists) self.artist = [ArtistMinimal(a) for a in artists] albumartists = split_artists(self.albumartist) @@ -72,7 +73,21 @@ class Track: self.recreate_hash() def recreate_hash(self): + """ + Recreates a track hash if the track title was altered + to prevent duplicate tracks having different hashes. + """ if self.og_title == self.title: return self.trackhash = create_hash(", ".join([a.name for a in self.artist]), self.album, self.title) + + def recreate_artists_hash(self): + self.artist_hashes = "-".join(a.artisthash for a in self.artist) + + def add_artists(self, artists: list[str]): + for artist in artists: + if create_hash(artist) not in self.artist_hashes: + self.artist.append(ArtistMinimal(artist)) + + self.recreate_artists_hash() diff --git a/app/store/albums.py b/app/store/albums.py index 743a341..d992008 100644 --- a/app/store/albums.py +++ b/app/store/albums.py @@ -71,7 +71,7 @@ class AlbumStore: Returns N albums by the given albumartist, excluding the specified album. """ - albums = [album for album in cls.albums if artisthash in album.albumartisthash] + albums = [album for album in cls.albums if artisthash in album.albumartists_hashes] albums = [album for album in albums if album.albumhash != exclude] @@ -108,7 +108,7 @@ class AlbumStore: """ Returns all albums by the given artist. """ - return [album for album in cls.albums if artisthash in album.albumartisthash] + return [album for album in cls.albums if artisthash in album.albumartists_hashes] @classmethod def count_albums_by_artisthash(cls, artisthash: str): diff --git a/app/store/tracks.py b/app/store/tracks.py index c781e1b..0b3bd6b 100644 --- a/app/store/tracks.py +++ b/app/store/tracks.py @@ -41,18 +41,6 @@ class TrackStore: cls.tracks.extend(tracks) - @classmethod - def get_tracks_by_trackhashes(cls, trackhashes: list[str]) -> list[Track]: - """ - Returns a list of tracks by their hashes. - """ - - trackhashes = " ".join(trackhashes) - tracks = [track for track in cls.tracks if track.trackhash in trackhashes] - - tracks.sort(key=lambda t: trackhashes.index(t.trackhash)) - return tracks - @classmethod def remove_track_by_filepath(cls, filepath: str): """ @@ -109,6 +97,29 @@ class TrackStore: if track.trackhash == trackhash: track.is_favorite = False + @classmethod + def append_track_artists(cls, albumhash: str, artists: list[str]): + tracks = cls.get_tracks_by_albumhash(albumhash) + + for track in tracks: + track.add_artists(artists) + + # ================================================ + # ================== GETTERS ===================== + # ================================================ + + @classmethod + def get_tracks_by_trackhashes(cls, trackhashes: list[str]) -> list[Track]: + """ + Returns a list of tracks by their hashes. + """ + + trackhashes = " ".join(trackhashes) + tracks = [track for track in cls.tracks if track.trackhash in trackhashes] + + tracks.sort(key=lambda t: trackhashes.index(t.trackhash)) + return tracks + @classmethod def get_tracks_by_filepaths(cls, paths: list[str]) -> list[Track]: """ From aa54a9bc0c75b86b44e1a66b6b1b9e5cf9d6b7ea Mon Sep 17 00:00:00 2001 From: geoffrey45 Date: Sun, 16 Apr 2023 17:47:47 +0300 Subject: [PATCH 5/7] fix --show-feat and --show-prod flags --- app/arg_handler.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/arg_handler.py b/app/arg_handler.py index ac0a368..1030be4 100644 --- a/app/arg_handler.py +++ b/app/arg_handler.py @@ -120,12 +120,12 @@ class HandleArgs: def handle_no_feat(): # if ArgsEnum.no_feat in ARGS: if any((a in ARGS for a in ALLARGS.show_feat)): - settings.EXTRACT_FEAT = False + settings.FromFlags.EXTRACT_FEAT = False @staticmethod def handle_remove_prod(): if any((a in ARGS for a in ALLARGS.show_prod)): - settings.REMOVE_PROD = False + settings.FromFlags.REMOVE_PROD = False @staticmethod def handle_help(): From 994711b887a23d29e2504e912d9d4b68344ab118 Mon Sep 17 00:00:00 2001 From: geoffrey45 Date: Sun, 16 Apr 2023 18:05:52 +0300 Subject: [PATCH 6/7] rename album title in track object after extract feat from album title --- app/models/album.py | 2 +- app/models/track.py | 6 +++++- app/store/tracks.py | 4 ++-- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/app/models/album.py b/app/models/album.py index 9794639..42693c8 100644 --- a/app/models/album.py +++ b/app/models/album.py @@ -47,7 +47,7 @@ class Album: self.albumartists.extend([Artist(a) for a in featured if a.lower() not in original_lower]) from ..store.tracks import TrackStore - TrackStore.append_track_artists(self.albumhash, featured) + TrackStore.append_track_artists(self.albumhash, featured, self.title) self.albumartists_hashes = "-".join(a.artisthash for a in self.albumartists) diff --git a/app/models/track.py b/app/models/track.py index c2ac466..d3a73a5 100644 --- a/app/models/track.py +++ b/app/models/track.py @@ -85,9 +85,13 @@ class Track: def recreate_artists_hash(self): self.artist_hashes = "-".join(a.artisthash for a in self.artist) - def add_artists(self, artists: list[str]): + def rename_album(self, new_album: str): + self.album = new_album + + def add_artists(self, artists: list[str], new_album_title: str): for artist in artists: if create_hash(artist) not in self.artist_hashes: self.artist.append(ArtistMinimal(artist)) self.recreate_artists_hash() + self.rename_album(new_album_title) diff --git a/app/store/tracks.py b/app/store/tracks.py index 0b3bd6b..74aea45 100644 --- a/app/store/tracks.py +++ b/app/store/tracks.py @@ -98,11 +98,11 @@ class TrackStore: track.is_favorite = False @classmethod - def append_track_artists(cls, albumhash: str, artists: list[str]): + def append_track_artists(cls, albumhash: str, artists: list[str], new_album_title:str): tracks = cls.get_tracks_by_albumhash(albumhash) for track in tracks: - track.add_artists(artists) + track.add_artists(artists, new_album_title) # ================================================ # ================== GETTERS ===================== From 6e3404e66ee164e0991e28128308b4dc46c9b115 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Ralph?= <41462117+tralph3@users.noreply.github.com> Date: Sun, 16 Apr 2023 21:22:29 -0300 Subject: [PATCH 7/7] Containerize application with Docker (#116) * add dockerfile * use latest tag instead of latest commit * Added docker instructions * define entrypoint * specify to cd into directory after git clone * include config flag --- Dockerfile | 33 +++++++++++++++++++++++++++++++++ README.md | 49 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 82 insertions(+) create mode 100644 Dockerfile diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..86abd56 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,33 @@ +FROM node:latest AS CLIENT + +RUN git clone https://github.com/geoffrey45/swing-client.git client + +WORKDIR /client + +RUN git checkout $(git describe --tags $(git rev-list --tags --max-count=1)) + +RUN yarn install + +RUN yarn build + +FROM python:latest + +WORKDIR /app/swingmusic + +COPY . . + +COPY --from=CLIENT /client/dist/ client + +EXPOSE 1970/tcp + +VOLUME /music + +VOLUME /config + +RUN pip install poetry + +RUN poetry config virtualenvs.create false + +RUN poetry install + +ENTRYPOINT ["poetry", "run", "python", "manage.py", "--host", "0.0.0.0", "--config", "/config"] diff --git a/README.md b/README.md index 6d7d961..870f7d7 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,55 @@ swingmusic --host 0.0.0.0 The link to access the app will be printed on your terminal. Copy it and open it in your browser. +### Docker + +You can run Swing in a Docker container. To do so, clone the repository and build the image: + + git clone https://github.com/swing-opensource/swingmusic.git --depth 1 + cd swingmusic + docker build . -t swingmusic + +Then create the container. Here are some example snippets to help you get started creating a container. + +#### docker-compose + +```yaml +--- +version: "3.8" +services: + swing: + image: swingmusic + container_name: swingmusic + volumes: + - /path/to/music:/music + - /path/to/config:/config + ports: + - 1970:1970 + restart: unless-stopped +``` + +#### docker cli + +```bash +docker run -d \ + --name=swingmusic \ + -p 1970:1970 \ + -v /path/to/music:/music \ + -v /path/to/config:/config \ + --restart unless-stopped \ + swingmusic +``` + +#### Parameters + +Container images are configured using parameters passed at runtime (such as those above). These parameters are separated by a colon and indicate `:` respectively. For example, `-p 8080:80` would expose port `80` from inside the container to be accessible from the host's IP on port `8080` outside the container. + +| Parameter | Function | +| :----: | --- | +| `-p 1970` | WebUI | +| `-v /music` | Recommended directory to store your music collection. You can bind other folder if you wish. | +| `-v /config` | Configuration files. | + ### Development This project is broken down into 2. The client and the server. The client comprises of the user interface code. This part is written in Typescript, Vue 3 and SCSS. To setup the client, checkout the [swing client repo ](https://github.com/geoffrey45/swing-client) on GitHub.