mirror of
https://github.com/Dvorinka/swingmusic-extended.git
synced 2026-06-03 20:13:02 +00:00
implement CLI password recovery (hacky :omg:)
+ rewrite migrations logic + rename encode_password to hash_password + update image sizes (add medium size) + rename image endpoints
This commit is contained in:
+1
-1
@@ -27,7 +27,7 @@ client
|
||||
logs.txt
|
||||
*.spec
|
||||
|
||||
TODO.md
|
||||
# TODO.md
|
||||
testdata.py
|
||||
test.py
|
||||
nohup.out
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
- Fix migrations!
|
||||
- Use total length instead of release version length
|
||||
+6
-5
@@ -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"]
|
||||
|
||||
+59
-39
@@ -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/<imgpath>")
|
||||
# TRACK THUMBNAILS
|
||||
@api.get("/thumbnail/<imgpath>")
|
||||
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/<imgpath>")
|
||||
@api.get("/thumbnail/xsmall/<imgpath>")
|
||||
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/<imgpath>")
|
||||
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/<imgpath>")
|
||||
@api.get("/thumbnail/medium/<imgpath>")
|
||||
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/<imgpath>")
|
||||
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/<imgpath>")
|
||||
@api.get("/artist/small/<imgpath>")
|
||||
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/<imgpath>")
|
||||
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/<imgpath>")
|
||||
@api.get("/playlist/<imgpath>")
|
||||
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")
|
||||
|
||||
+2
-1
@@ -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()
|
||||
)
|
||||
|
||||
+55
-8
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
+27
-11
@@ -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():
|
||||
|
||||
+1
-1
@@ -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")
|
||||
|
||||
|
||||
+10
-8
@@ -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)
|
||||
|
||||
+23
-18
@@ -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:
|
||||
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:
|
||||
m.migrate()
|
||||
log.info("Applied migration: %s", m.__name__)
|
||||
migration.migrate()
|
||||
log.info("Applied migration: %s", migration.__name__)
|
||||
except:
|
||||
log.error("Failed to run migration: %s", m.__name__)
|
||||
log.error("Failed to run migration: %s", migration.__name__)
|
||||
|
||||
print("Migrations applied successfully.")
|
||||
print("Current migration version: ", len(migrations))
|
||||
# bump migration version
|
||||
MigrationManager.set_version(len(migrations))
|
||||
MigrationManager.set_index(len(all_migrations))
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
+8
-5
@@ -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])}
|
||||
"""
|
||||
|
||||
+41
-17
@@ -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 = "<unset>"
|
||||
GIT_CURRENT_BRANCH = "<unset>"
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
+16
-14
@@ -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()
|
||||
|
||||
@@ -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()]
|
||||
|
||||
|
||||
+8
-8
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user