mirror of
https://github.com/tcsenpai/swingmusic.git
synced 2025-06-06 03:05:35 +00:00
load settings from db, use api to change settings
+ add route to get all settings + add route to set any setting + add untested migration to add settings into settings db + compress json in api responses using FlaskCompress + serve gziped assets if browser accepts encoded files + misc
This commit is contained in:
parent
e3a61c109b
commit
71cab5f5ea
@ -3,18 +3,34 @@ This module combines all API blueprints into a single Flask app instance.
|
||||
"""
|
||||
|
||||
from flask import Flask
|
||||
from flask_compress import Compress
|
||||
from flask_cors import CORS
|
||||
|
||||
from app.api import (album, artist, favorites, folder, imgserver, playlist,
|
||||
search, settings, track, colors)
|
||||
from app.api import (
|
||||
album,
|
||||
artist,
|
||||
colors,
|
||||
favorites,
|
||||
folder,
|
||||
imgserver,
|
||||
playlist,
|
||||
search,
|
||||
settings,
|
||||
track,
|
||||
)
|
||||
|
||||
|
||||
def create_api():
|
||||
"""
|
||||
Creates the Flask instance, registers modules and registers all the API blueprints.
|
||||
"""
|
||||
app = Flask(__name__, static_url_path="")
|
||||
app = Flask(__name__)
|
||||
CORS(app)
|
||||
Compress(app)
|
||||
|
||||
app.config["COMPRESS_MIMETYPES"] = [
|
||||
"application/json",
|
||||
]
|
||||
|
||||
with app.app_context():
|
||||
app.register_blueprint(album.api)
|
||||
|
@ -7,8 +7,7 @@ from collections import deque
|
||||
from flask import Blueprint, request
|
||||
|
||||
from app.db.sqlite.favorite import SQLiteFavoriteMethods as favdb
|
||||
from app.db.sqlite.lastfm.similar_artists import \
|
||||
SQLiteLastFMSimilarArtists as fmdb
|
||||
from app.db.sqlite.lastfm.similar_artists import SQLiteLastFMSimilarArtists as fmdb
|
||||
from app.models import Album, FavType, Track
|
||||
from app.serializers.album import serialize_for_card_many
|
||||
from app.serializers.track import serialize_tracks
|
||||
@ -49,8 +48,8 @@ class ArtistsCache:
|
||||
artists: deque[CacheEntry] = deque(maxlen=1)
|
||||
# THE ABOVE IS SET TO MAXLEN=1 TO AVOID A BUG THAT I WAS TOO LAZY TO INVESTIGATE
|
||||
# ARTIST TRACKS SOMEHOW DISAPPEARED FOR SOME REASON I COULDN'T UNDERSTAND. BY
|
||||
# DISAPPEARING I MEAN AN ARTIST YOU ARE SURE HAS 150 TRACKS ONLY SHOWING LIKE 3 IN
|
||||
# THE ARTIST PAGE. 🤷🏿 (TODO: MAYBE FIX THIS BUG?)
|
||||
# DISAPPEARING I MEAN AN ARTIST YOU ARE SURE HAS 150 TRACKS ONLY SHOWING
|
||||
# LIKE 3 IN THE ARTIST PAGE. 🤷🏿 (TODO: MAYBE FIX THIS BUG?)
|
||||
|
||||
@classmethod
|
||||
def get_albums_by_artisthash(cls, artisthash: str) -> tuple[list[Album], int]:
|
||||
@ -325,4 +324,5 @@ def get_similar_artists(artisthash: str):
|
||||
|
||||
return {"artists": similar[:limit]}
|
||||
|
||||
# TODO: Rewrite this file using generators where possible
|
||||
|
||||
# TODO: Rewrite this file using generators where possible
|
||||
|
@ -1,17 +1,16 @@
|
||||
from flask import Blueprint, request
|
||||
from app import settings
|
||||
|
||||
from app.logger import log
|
||||
from app.db.sqlite.settings import SettingsSQLMethods as sdb
|
||||
from app.lib import populate
|
||||
from app.lib.watchdogg import Watcher as WatchDog
|
||||
from app.db.sqlite.settings import SettingsSQLMethods as sdb
|
||||
from app.logger import log
|
||||
from app.settings import ParserFlags, Paths, set_flag
|
||||
from app.store.albums import AlbumStore
|
||||
from app.store.artists import ArtistStore
|
||||
from app.store.tracks import TrackStore
|
||||
from app.utils.generators import get_random_str
|
||||
from app.utils.threading import background
|
||||
|
||||
from app.store.albums import AlbumStore
|
||||
from app.store.tracks import TrackStore
|
||||
from app.store.artists import ArtistStore
|
||||
|
||||
api = Blueprint("settings", __name__, url_prefix="/")
|
||||
|
||||
|
||||
@ -21,13 +20,24 @@ def get_child_dirs(parent: str, children: list[str]):
|
||||
return [_dir for _dir in children if _dir.startswith(parent) and _dir != parent]
|
||||
|
||||
|
||||
def reload_everything():
|
||||
def reload_everything(instance_key: str):
|
||||
"""
|
||||
Reloads all stores using the current database items
|
||||
"""
|
||||
TrackStore.load_all_tracks()
|
||||
AlbumStore.load_albums()
|
||||
ArtistStore.load_artists()
|
||||
try:
|
||||
TrackStore.load_all_tracks(instance_key)
|
||||
except Exception as e:
|
||||
log.error(e)
|
||||
|
||||
try:
|
||||
AlbumStore.load_albums(instance_key=instance_key)
|
||||
except Exception as e:
|
||||
log.error(e)
|
||||
|
||||
try:
|
||||
ArtistStore.load_artists(instance_key)
|
||||
except Exception as e:
|
||||
log.error(e)
|
||||
|
||||
|
||||
@background
|
||||
@ -35,15 +45,16 @@ def rebuild_store(db_dirs: list[str]):
|
||||
"""
|
||||
Restarts the watchdog and rebuilds the music library.
|
||||
"""
|
||||
instance_key = get_random_str()
|
||||
|
||||
log.info("Rebuilding library...")
|
||||
TrackStore.remove_tracks_by_dir_except(db_dirs)
|
||||
reload_everything()
|
||||
reload_everything(instance_key)
|
||||
|
||||
key = get_random_str()
|
||||
try:
|
||||
populate.Populate(key=key)
|
||||
populate.Populate(instance_key=instance_key)
|
||||
except populate.PopulateCancelledError:
|
||||
reload_everything()
|
||||
reload_everything(instance_key)
|
||||
return
|
||||
|
||||
WatchDog().restart()
|
||||
@ -51,6 +62,7 @@ def rebuild_store(db_dirs: list[str]):
|
||||
log.info("Rebuilding library... ✅")
|
||||
|
||||
|
||||
# I freaking don't know what this function does anymore
|
||||
def finalize(new_: list[str], removed_: list[str], db_dirs_: list[str]):
|
||||
"""
|
||||
Params:
|
||||
@ -96,7 +108,7 @@ def add_root_dirs():
|
||||
sdb.remove_root_dirs(db_dirs)
|
||||
|
||||
if incoming_home:
|
||||
finalize([_h], [], [settings.Paths.USER_HOME_DIR])
|
||||
finalize([_h], [], [Paths.USER_HOME_DIR])
|
||||
return {"root_dirs": [_h]}
|
||||
|
||||
# ---
|
||||
@ -127,3 +139,78 @@ def get_root_dirs():
|
||||
dirs = sdb.get_root_dirs()
|
||||
|
||||
return {"dirs": dirs}
|
||||
|
||||
|
||||
# maps settings to their parser flags
|
||||
mapp = {
|
||||
"artist_separators": ParserFlags.ARTIST_SEPARATORS,
|
||||
"extract_feat": ParserFlags.EXTRACT_FEAT,
|
||||
"remove_prod": ParserFlags.REMOVE_PROD,
|
||||
"clean_album_title": ParserFlags.CLEAN_ALBUM_TITLE,
|
||||
"remove_remaster": ParserFlags.REMOVE_REMASTER_FROM_TRACK,
|
||||
"merge_albums": ParserFlags.MERGE_ALBUM_VERSIONS,
|
||||
}
|
||||
|
||||
|
||||
@api.route("/settings/", methods=["GET"])
|
||||
def get_all_settings():
|
||||
"""
|
||||
Get all settings from the database.
|
||||
"""
|
||||
|
||||
settings = sdb.get_all_settings()
|
||||
|
||||
key_list = list(mapp.keys())
|
||||
s = {}
|
||||
|
||||
for key in key_list:
|
||||
val_index = key_list.index(key)
|
||||
|
||||
try:
|
||||
s[key] = settings[val_index]
|
||||
|
||||
if type(s[key]) == int:
|
||||
s[key] = bool(s[key])
|
||||
if type(s[key]) == str:
|
||||
s[key] = str(s[key]).split(",")
|
||||
|
||||
except IndexError:
|
||||
s[key] = None
|
||||
|
||||
root_dirs = sdb.get_root_dirs()
|
||||
s["root_dirs"] = root_dirs
|
||||
|
||||
return {
|
||||
"settings": s,
|
||||
}
|
||||
|
||||
|
||||
@background
|
||||
def reload_all_for_set_setting():
|
||||
reload_everything(get_random_str())
|
||||
|
||||
|
||||
@api.route("/settings/set", methods=["POST"])
|
||||
def set_setting():
|
||||
key = request.get_json().get("key")
|
||||
value = request.get_json().get("value")
|
||||
|
||||
if key is None or value is None or key == "root_dirs":
|
||||
return {"msg": "Invalid arguments!"}, 400
|
||||
|
||||
root_dir = sdb.get_root_dirs()
|
||||
|
||||
if not root_dir:
|
||||
return {"msg": "No root directories set!"}, 400
|
||||
|
||||
if key not in mapp:
|
||||
return {"msg": "Invalid key!"}, 400
|
||||
|
||||
sdb.set_setting(key, value)
|
||||
|
||||
if mapp[key] is not False:
|
||||
flag = mapp[key]
|
||||
set_flag(flag, value)
|
||||
reload_all_for_set_setting()
|
||||
|
||||
return {"result": value}
|
||||
|
@ -1,61 +0,0 @@
|
||||
"""
|
||||
Module for managing the JSON config file.
|
||||
"""
|
||||
|
||||
import json
|
||||
from enum import Enum
|
||||
from typing import Type
|
||||
|
||||
from app.settings import Db
|
||||
|
||||
|
||||
class ConfigKeys(Enum):
|
||||
ROOT_DIRS = ("root_dirs", list[str])
|
||||
PLAYLIST_DIRS = ("playlist_dirs", list[str])
|
||||
USE_ART_COLORS = ("use_art_colors", bool)
|
||||
DEFAULT_ART_COLOR = ("default_art_color", str)
|
||||
SHUFFLE_MODE = ("shuffle_mode", str)
|
||||
REPEAT_MODE = ("repeat_mode", str)
|
||||
AUTOPLAY_ON_START = ("autoplay_on_start", bool)
|
||||
VOLUME = ("volume", int)
|
||||
|
||||
def __init__(self, key_name: str, data_type: Type):
|
||||
self.key_name = key_name
|
||||
self.data_type = data_type
|
||||
|
||||
def get_data_type(self) -> Type:
|
||||
return self.data_type
|
||||
|
||||
|
||||
class ConfigManager:
|
||||
def __init__(self, config_file_path: str):
|
||||
self.config_file_path = config_file_path
|
||||
|
||||
def read_config(self):
|
||||
try:
|
||||
with open(self.config_file_path) as f:
|
||||
return json.load(f)
|
||||
except FileNotFoundError:
|
||||
return {}
|
||||
|
||||
# in case of errors, return an empty dict
|
||||
|
||||
def write_config(self, config_data):
|
||||
with open(self.config_file_path, "w") as f:
|
||||
json.dump(config_data, f, indent=4)
|
||||
|
||||
def get_value(self, key: ConfigKeys):
|
||||
config_data = self.read_config()
|
||||
value = config_data.get(key.key_name)
|
||||
|
||||
if value is not None:
|
||||
return key.get_data_type()(value)
|
||||
|
||||
def set_value(self, key: ConfigKeys, value):
|
||||
config_data = self.read_config()
|
||||
config_data[key.key_name] = value
|
||||
self.write_config(config_data)
|
||||
|
||||
|
||||
settings = ConfigManager(Db.get_json_config_path())
|
||||
a = settings.get_value(ConfigKeys.ROOT_DIRS)
|
@ -26,7 +26,12 @@ CREATE TABLE IF NOT EXISTS settings (
|
||||
id integer PRIMARY KEY,
|
||||
root_dirs text NOT NULL,
|
||||
exclude_dirs text,
|
||||
artist_separators text
|
||||
artist_separators text NOT NULL default '/,;,&',
|
||||
extract_feat integer NOT NULL DEFAULT 1,
|
||||
remove_prod integer NOT NULL DEFAULT 1,
|
||||
clean_album_title integer NOT NULL DEFAULT 1,
|
||||
remove_remaster integer NOT NULL DEFAULT 1,
|
||||
merge_albums integer NOT NULL DEFAULT 0
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS lastfm_similar_artists (
|
||||
|
@ -1,5 +1,8 @@
|
||||
from pprint import pprint
|
||||
from typing import Any
|
||||
from app.db.sqlite.utils import SQLiteManager
|
||||
from app.utils.wintools import win_replace_slash
|
||||
from app.settings import FromFlags
|
||||
|
||||
|
||||
class SettingsSQLMethods:
|
||||
@ -7,6 +10,28 @@ class SettingsSQLMethods:
|
||||
Methods for interacting with the settings table.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def get_all_settings():
|
||||
"""
|
||||
Gets all settings from the database.
|
||||
"""
|
||||
|
||||
sql = "SELECT * FROM settings WHERE id = 1"
|
||||
|
||||
with SQLiteManager(userdata_db=True) as cur:
|
||||
cur.execute(sql)
|
||||
settings = cur.fetchone()
|
||||
cur.close()
|
||||
|
||||
# if root_dirs not set
|
||||
if settings is None:
|
||||
return []
|
||||
|
||||
# print
|
||||
|
||||
# omit id, root_dirs, and exclude_dirs
|
||||
return settings[3:]
|
||||
|
||||
@staticmethod
|
||||
def get_root_dirs() -> list[str]:
|
||||
"""
|
||||
@ -90,25 +115,34 @@ class SettingsSQLMethods:
|
||||
return [_dir[0] for _dir in dirs]
|
||||
|
||||
@staticmethod
|
||||
def add_artist_separators(seps: set[str]):
|
||||
"""
|
||||
Adds a set of artist separators to the userdata table.
|
||||
"""
|
||||
# TODO: Implement
|
||||
def get_settings() -> dict[str, Any]:
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def get_artist_separators() -> set[str]:
|
||||
"""
|
||||
Gets a set of artist separators from the userdata table.
|
||||
"""
|
||||
# TODO: Implement
|
||||
pass
|
||||
def set_setting(key: str, value: Any):
|
||||
sql = f"UPDATE settings SET {key} = :value WHERE id = 1"
|
||||
|
||||
@staticmethod
|
||||
def remove_artist_separators(seps: set[str]):
|
||||
"""
|
||||
Removes a set of artist separators from the userdata table.
|
||||
"""
|
||||
# TODO: Implement
|
||||
pass
|
||||
if type(value) == bool:
|
||||
value = str(int(value))
|
||||
|
||||
with SQLiteManager(userdata_db=True) as cur:
|
||||
cur.execute(sql, {"value": value})
|
||||
|
||||
|
||||
def load_settings():
|
||||
s = SettingsSQLMethods.get_all_settings()
|
||||
|
||||
# artist separators
|
||||
db_separators: str = s[0]
|
||||
db_separators = db_separators.replace(" ", "")
|
||||
separators = db_separators.split(",")
|
||||
|
||||
separators = set(db_separators)
|
||||
FromFlags.ARTIST_SEPARATORS = separators
|
||||
|
||||
# boolean settings
|
||||
FromFlags.EXTRACT_FEAT = bool(s[1])
|
||||
FromFlags.REMOVE_PROD = bool(s[2])
|
||||
FromFlags.CLEAN_ALBUM_TITLE = bool(s[3])
|
||||
FromFlags.REMOVE_REMASTER_FROM_TRACK = bool(s[4])
|
||||
FromFlags.MERGE_ALBUM_VERSIONS = bool(s[5])
|
||||
|
@ -8,7 +8,7 @@ import time
|
||||
from typing import Optional
|
||||
|
||||
from app.models import Album, Playlist, Track
|
||||
from app.settings import Db
|
||||
from app import settings
|
||||
|
||||
|
||||
def tuple_to_track(track: tuple):
|
||||
@ -88,10 +88,10 @@ class SQLiteManager:
|
||||
if self.test_db_path:
|
||||
db_path = self.test_db_path
|
||||
else:
|
||||
db_path = Db.get_app_db_path()
|
||||
db_path = settings.Db.get_app_db_path()
|
||||
|
||||
if self.userdata_db:
|
||||
db_path = Db.get_userdata_db_path()
|
||||
db_path = settings.Db.get_userdata_db_path()
|
||||
|
||||
self.conn = sqlite3.connect(
|
||||
db_path,
|
||||
|
@ -45,9 +45,9 @@ class Populate:
|
||||
also checks if the album art exists in the image path, if not tries to extract it.
|
||||
"""
|
||||
|
||||
def __init__(self, key: str) -> None:
|
||||
def __init__(self, instance_key: str) -> None:
|
||||
global POPULATE_KEY
|
||||
POPULATE_KEY = key
|
||||
POPULATE_KEY = instance_key
|
||||
|
||||
validate_tracks()
|
||||
validate_albums()
|
||||
@ -80,7 +80,7 @@ class Populate:
|
||||
untagged = files - unmodified
|
||||
|
||||
if len(untagged) != 0:
|
||||
self.tag_untagged(untagged, key)
|
||||
self.tag_untagged(untagged, instance_key)
|
||||
|
||||
self.extract_thumb_with_overwrite(modified_tracks)
|
||||
|
||||
@ -110,7 +110,7 @@ class Populate:
|
||||
if Ping()():
|
||||
FetchSimilarArtistsLastFM()
|
||||
|
||||
ArtistStore.load_artists()
|
||||
ArtistStore.load_artists(instance_key)
|
||||
|
||||
@staticmethod
|
||||
def remove_modified(tracks: Generator[Track, None, None]):
|
||||
@ -273,12 +273,17 @@ class FetchSimilarArtistsLastFM:
|
||||
artists = ArtistStore.artists
|
||||
|
||||
with Pool(processes=cpu_count()) as pool:
|
||||
results = list(
|
||||
tqdm(
|
||||
pool.imap_unordered(save_similar_artists, artists),
|
||||
total=len(artists),
|
||||
desc="Fetching similar artists",
|
||||
try:
|
||||
results = list(
|
||||
tqdm(
|
||||
pool.imap_unordered(save_similar_artists, artists),
|
||||
total=len(artists),
|
||||
desc="Fetching similar artists",
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
list(results)
|
||||
list(results)
|
||||
|
||||
# any exception that can be raised by the pool
|
||||
except:
|
||||
pass
|
||||
|
@ -242,7 +242,6 @@ class TopResults:
|
||||
|
||||
if item["type"] == "artist":
|
||||
t = TrackStore.get_tracks_by_artisthash(item["item"].artisthash)
|
||||
t.sort(key=lambda x: x.last_mod)
|
||||
|
||||
# if there are less than the limit, get more tracks
|
||||
if len(t) < limit:
|
||||
|
@ -25,6 +25,7 @@ migrations: list[list[Migration]] = [
|
||||
v1_3_0.AddLastUpdatedToTrackTable,
|
||||
v1_3_0.MovePlaylistsAndFavoritesTo10BitHashes,
|
||||
v1_3_0.RemoveAllTracks,
|
||||
v1_3_0.UpdateAppSettingsTable,
|
||||
]
|
||||
]
|
||||
|
||||
|
@ -277,3 +277,26 @@ class RemoveAllTracks(Migration):
|
||||
with SQLiteManager() as cur:
|
||||
cur.execute(sql)
|
||||
cur.close()
|
||||
|
||||
|
||||
class UpdateAppSettingsTable(Migration):
|
||||
@staticmethod
|
||||
def migrate():
|
||||
drop_table_sql = "DROP TABLE settings"
|
||||
create_table_sql = """
|
||||
CREATE TABLE IF NOT EXISTS settings (
|
||||
id integer PRIMARY KEY,
|
||||
root_dirs text NOT NULL,
|
||||
exclude_dirs text,
|
||||
artist_separators text NOT NULL default '/,;,&',
|
||||
extract_feat integer NOT NULL DEFAULT 1,
|
||||
remove_prod integer NOT NULL DEFAULT 1,
|
||||
clean_album_title integer NOT NULL DEFAULT 1,
|
||||
remove_remaster integer NOT NULL DEFAULT 1,
|
||||
merge_albums integer NOT NULL DEFAULT 0
|
||||
);
|
||||
"""
|
||||
|
||||
with SQLiteManager(userdata_db=True) as cur:
|
||||
cur.execute(drop_table_sql)
|
||||
cur.execute(create_table_sql)
|
@ -124,7 +124,7 @@ class Album:
|
||||
if "various artists" in artists:
|
||||
return True
|
||||
|
||||
substrings = [
|
||||
substrings = {
|
||||
"the essential",
|
||||
"best of",
|
||||
"greatest hits",
|
||||
@ -136,7 +136,7 @@ class Album:
|
||||
"great hits",
|
||||
"biggest hits",
|
||||
"the hits",
|
||||
]
|
||||
}
|
||||
|
||||
for substring in substrings:
|
||||
if substring in self.title.lower():
|
||||
|
@ -3,14 +3,10 @@ This module contains functions for the server
|
||||
"""
|
||||
import time
|
||||
|
||||
from app.logger import log
|
||||
from app.lib.populate import Populate, PopulateCancelledError
|
||||
|
||||
from app.utils.generators import get_random_str
|
||||
from app.utils.network import Ping
|
||||
from app.utils.threading import background
|
||||
|
||||
from app.settings import ParserFlags, get_flag, get_scan_sleep_time
|
||||
from app.utils.generators import get_random_str
|
||||
from app.utils.threading import background
|
||||
|
||||
|
||||
@background
|
||||
@ -21,14 +17,13 @@ def run_periodic_scans():
|
||||
# ValidateAlbumThumbs()
|
||||
# ValidatePlaylistThumbs()
|
||||
|
||||
try:
|
||||
Populate(key=get_random_str())
|
||||
except PopulateCancelledError:
|
||||
pass
|
||||
run_periodic_scan = True
|
||||
|
||||
while run_periodic_scan:
|
||||
run_periodic_scan = get_flag(ParserFlags.DO_PERIODIC_SCANS)
|
||||
|
||||
while get_flag(ParserFlags.DO_PERIODIC_SCANS):
|
||||
try:
|
||||
Populate(key=get_random_str())
|
||||
Populate(instance_key=get_random_str())
|
||||
except PopulateCancelledError:
|
||||
pass
|
||||
|
||||
|
@ -2,6 +2,7 @@
|
||||
Contains default configs
|
||||
"""
|
||||
import os
|
||||
from typing import Any
|
||||
|
||||
join = os.path.join
|
||||
|
||||
@ -162,29 +163,34 @@ class FromFlags:
|
||||
|
||||
CLEAN_ALBUM_TITLE = True
|
||||
REMOVE_REMASTER_FROM_TRACK = True
|
||||
SHOW_ALBUM_VERSION = True
|
||||
|
||||
DO_PERIODIC_SCANS = True
|
||||
PERIODIC_SCAN_INTERVAL = 300 # seconds
|
||||
|
||||
MERGE_ALBUM_VERSIONS = False
|
||||
ARTIST_SEPARATORS = {",", "/", ";", "&"}
|
||||
|
||||
|
||||
class ParserFlags():
|
||||
# TODO: Find a way to eliminate this class without breaking typings
|
||||
class ParserFlags:
|
||||
EXTRACT_FEAT = "EXTRACT_FEAT"
|
||||
REMOVE_PROD = "REMOVE_PROD"
|
||||
CLEAN_ALBUM_TITLE = "CLEAN_ALBUM_TITLE"
|
||||
SHOW_ALBUM_VERSION = "SHOW_ALBUM_VERSION"
|
||||
REMOVE_REMASTER_FROM_TRACK = "REMOVE_REMASTER_FROM_TRACK"
|
||||
DO_PERIODIC_SCANS = "DO_PERIODIC_SCANS"
|
||||
PERIODIC_SCAN_INTERVAL = "PERIODIC_SCAN_INTERVAL"
|
||||
MERGE_ALBUM_VERSIONS = "MERGE_ALBUM_VERSIONS"
|
||||
ARTIST_SEPARATORS = "ARTIST_SEPARATORS"
|
||||
|
||||
|
||||
def get_flag(flag: ParserFlags) -> bool:
|
||||
return getattr(FromFlags, flag)
|
||||
|
||||
|
||||
def set_flag(flag: ParserFlags, value: Any):
|
||||
setattr(FromFlags, flag, value)
|
||||
|
||||
|
||||
def get_scan_sleep_time() -> int:
|
||||
return FromFlags.PERIODIC_SCAN_INTERVAL
|
||||
|
||||
|
@ -1,11 +1,13 @@
|
||||
"""
|
||||
Prepares the server for use.
|
||||
"""
|
||||
from app.db.sqlite.settings import load_settings
|
||||
from app.setup.files import create_config_dir
|
||||
from app.setup.sqlite import run_migrations, setup_sqlite
|
||||
from app.store.albums import AlbumStore
|
||||
from app.store.artists import ArtistStore
|
||||
from app.store.tracks import TrackStore
|
||||
from app.utils.generators import get_random_str
|
||||
|
||||
|
||||
def run_setup():
|
||||
@ -13,6 +15,14 @@ def run_setup():
|
||||
setup_sqlite()
|
||||
run_migrations()
|
||||
|
||||
TrackStore.load_all_tracks()
|
||||
AlbumStore.load_albums()
|
||||
ArtistStore.load_artists()
|
||||
try:
|
||||
load_settings()
|
||||
except IndexError:
|
||||
# settings table is empty
|
||||
pass
|
||||
|
||||
instance_key = get_random_str()
|
||||
|
||||
TrackStore.load_all_tracks(instance_key)
|
||||
AlbumStore.load_albums(instance_key)
|
||||
ArtistStore.load_artists(instance_key)
|
||||
|
@ -9,6 +9,8 @@ from app.models import Album, Track
|
||||
from ..utils.hashing import create_hash
|
||||
from .tracks import TrackStore
|
||||
|
||||
ALBUM_LOAD_KEY = ""
|
||||
|
||||
|
||||
class AlbumStore:
|
||||
albums: list[Album] = []
|
||||
@ -25,16 +27,21 @@ class AlbumStore:
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def load_albums(cls):
|
||||
def load_albums(cls, instance_key: str):
|
||||
"""
|
||||
Loads all albums from the database into the store.
|
||||
"""
|
||||
global ALBUM_LOAD_KEY
|
||||
ALBUM_LOAD_KEY = instance_key
|
||||
|
||||
cls.albums = []
|
||||
|
||||
albumhashes = set(t.albumhash for t in TrackStore.tracks)
|
||||
|
||||
for albumhash in tqdm(albumhashes, desc="Loading albums"):
|
||||
for albumhash in tqdm(albumhashes, desc=f"Loading {instance_key}"):
|
||||
if instance_key != ALBUM_LOAD_KEY:
|
||||
return
|
||||
|
||||
for track in TrackStore.tracks:
|
||||
if track.albumhash == albumhash:
|
||||
cls.albums.append(cls.create_album(track))
|
||||
@ -67,15 +74,21 @@ class AlbumStore:
|
||||
|
||||
@classmethod
|
||||
def get_albums_by_albumartist(
|
||||
cls, artisthash: str, limit: int, exclude: str
|
||||
cls, artisthash: str, limit: int, exclude: str
|
||||
) -> list[Album]:
|
||||
"""
|
||||
Returns N albums by the given albumartist, excluding the specified album.
|
||||
"""
|
||||
|
||||
albums = [album for album in cls.albums if artisthash in album.albumartists_hashes]
|
||||
albums = [
|
||||
album for album in cls.albums if artisthash in album.albumartists_hashes
|
||||
]
|
||||
|
||||
albums = [album for album in albums if create_hash(album.base_title) != create_hash(exclude)]
|
||||
albums = [
|
||||
album
|
||||
for album in albums
|
||||
if create_hash(album.base_title) != create_hash(exclude)
|
||||
]
|
||||
|
||||
if len(albums) > limit:
|
||||
random.shuffle(albums)
|
||||
@ -110,7 +123,9 @@ class AlbumStore:
|
||||
"""
|
||||
Returns all albums by the given artist.
|
||||
"""
|
||||
return [album for album in cls.albums if artisthash in album.albumartists_hashes]
|
||||
return [
|
||||
album for album in cls.albums if artisthash in album.albumartists_hashes
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def count_albums_by_artisthash(cls, artisthash: str):
|
||||
|
@ -10,20 +10,28 @@ from app.utils.bisection import UseBisection
|
||||
from .albums import AlbumStore
|
||||
from .tracks import TrackStore
|
||||
|
||||
ARTIST_LOAD_KEY = ""
|
||||
|
||||
|
||||
class ArtistStore:
|
||||
artists: list[Artist] = []
|
||||
|
||||
@classmethod
|
||||
def load_artists(cls):
|
||||
def load_artists(cls, instance_key: str):
|
||||
"""
|
||||
Loads all artists from the database into the store.
|
||||
"""
|
||||
global ARTIST_LOAD_KEY
|
||||
ARTIST_LOAD_KEY = instance_key
|
||||
|
||||
cls.artists = get_all_artists(TrackStore.tracks, AlbumStore.albums)
|
||||
|
||||
# db_artists: list[tuple] = list(ardb.get_all_artists())
|
||||
|
||||
for artist in tqdm(ardb.get_all_artists(), desc="Loading artists"):
|
||||
if instance_key != ARTIST_LOAD_KEY:
|
||||
return
|
||||
|
||||
cls.map_artist_color(artist)
|
||||
|
||||
@classmethod
|
||||
|
@ -6,15 +6,19 @@ from app.models import Track
|
||||
from app.utils.bisection import UseBisection
|
||||
from app.utils.remove_duplicates import remove_duplicates
|
||||
|
||||
TRACKS_LOAD_KEY = ""
|
||||
|
||||
|
||||
class TrackStore:
|
||||
tracks: list[Track] = []
|
||||
|
||||
@classmethod
|
||||
def load_all_tracks(cls):
|
||||
def load_all_tracks(cls, instance_key: str):
|
||||
"""
|
||||
Loads all tracks from the database into the store.
|
||||
"""
|
||||
global TRACKS_LOAD_KEY
|
||||
TRACKS_LOAD_KEY = instance_key
|
||||
|
||||
cls.tracks = list(tdb.get_all_tracks())
|
||||
|
||||
@ -22,6 +26,9 @@ class TrackStore:
|
||||
fav_hashes = " ".join([t[1] for t in fav_hashes])
|
||||
|
||||
for track in tqdm(cls.tracks, desc="Loading tracks"):
|
||||
if instance_key != TRACKS_LOAD_KEY:
|
||||
return
|
||||
|
||||
if track.trackhash in fav_hashes:
|
||||
track.is_favorite = True
|
||||
|
||||
@ -153,12 +160,14 @@ class TrackStore:
|
||||
return remove_duplicates(tracks)
|
||||
|
||||
@classmethod
|
||||
def get_tracks_by_artisthash(cls, artisthash: str) -> list[Track]:
|
||||
def get_tracks_by_artisthash(cls, artisthash: str):
|
||||
"""
|
||||
Returns all tracks matching the given artist. Duplicate tracks are removed.
|
||||
"""
|
||||
tracks = [t for t in cls.tracks if artisthash in t.artist_hashes]
|
||||
return remove_duplicates(tracks)
|
||||
tracks = remove_duplicates(tracks)
|
||||
tracks.sort(key=lambda x: x.last_mod)
|
||||
return tracks
|
||||
|
||||
@classmethod
|
||||
def get_tracks_in_path(cls, path: str):
|
||||
|
@ -1,13 +1,14 @@
|
||||
import re
|
||||
|
||||
from app.enums.album_versions import AlbumVersionEnum
|
||||
from app.settings import get_flag, ParserFlags
|
||||
|
||||
|
||||
def split_artists(src: str, custom_seps: set[str] = {}):
|
||||
def split_artists(src: str):
|
||||
"""
|
||||
Splits a string of artists into a list of artists.
|
||||
"""
|
||||
separators = {",", ";", "/"}.union(custom_seps)
|
||||
separators = get_flag(ParserFlags.ARTIST_SEPARATORS)
|
||||
|
||||
for sep in separators:
|
||||
src = src.replace(sep, "߸")
|
||||
|
36
manage.py
36
manage.py
@ -3,6 +3,9 @@ This file is used to run the application.
|
||||
"""
|
||||
import logging
|
||||
import mimetypes
|
||||
import os
|
||||
|
||||
from flask import request
|
||||
|
||||
from app.api import create_api
|
||||
from app.arg_handler import HandleArgs
|
||||
@ -34,12 +37,25 @@ app = create_api()
|
||||
app.static_folder = get_home_res_path("client")
|
||||
|
||||
|
||||
# @app.route("/", defaults={"path": ""})
|
||||
@app.route("/<path:path>")
|
||||
def serve_client_files(path):
|
||||
def serve_client_files(path: str):
|
||||
"""
|
||||
Serves the static files in the client folder.
|
||||
"""
|
||||
return app.send_static_file(path)
|
||||
js_or_css = path.endswith(".js") or path.endswith(".css")
|
||||
if not js_or_css:
|
||||
return app.send_static_file(path)
|
||||
|
||||
gzipped_path = path + ".gz"
|
||||
|
||||
if request.headers.get("Accept-Encoding", "").find("gzip") >= 0:
|
||||
if os.path.exists(os.path.join(app.static_folder, gzipped_path)):
|
||||
response = app.make_response(app.send_static_file(gzipped_path))
|
||||
response.headers["Content-Encoding"] = "gzip"
|
||||
return response
|
||||
else:
|
||||
return app.send_static_file(path)
|
||||
|
||||
|
||||
@app.route("/")
|
||||
@ -75,12 +91,10 @@ if __name__ == "__main__":
|
||||
bg_run_setup()
|
||||
start_watchdog()
|
||||
|
||||
app.run(
|
||||
debug=False,
|
||||
threaded=True,
|
||||
host=FLASKVARS.get_flask_host(),
|
||||
port=FLASKVARS.get_flask_port(),
|
||||
use_reloader=False,
|
||||
)
|
||||
|
||||
# TODO: Organize code in this file: move args to new file, etc.
|
||||
app.run(
|
||||
debug=False,
|
||||
threaded=True,
|
||||
host=FLASKVARS.get_flask_host(),
|
||||
port=FLASKVARS.get_flask_port(),
|
||||
use_reloader=False,
|
||||
)
|
||||
|
108
poetry.lock
generated
108
poetry.lock
generated
@ -119,6 +119,97 @@ files = [
|
||||
{file = "blinker-1.6.2.tar.gz", hash = "sha256:4afd3de66ef3a9f8067559fb7a1cbe555c17dcbe15971b05d1b625c3e7abe213"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "brotli"
|
||||
version = "1.0.9"
|
||||
description = "Python bindings for the Brotli compression library"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
files = [
|
||||
{file = "Brotli-1.0.9-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:268fe94547ba25b58ebc724680609c8ee3e5a843202e9a381f6f9c5e8bdb5c70"},
|
||||
{file = "Brotli-1.0.9-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:c2415d9d082152460f2bd4e382a1e85aed233abc92db5a3880da2257dc7daf7b"},
|
||||
{file = "Brotli-1.0.9-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:5913a1177fc36e30fcf6dc868ce23b0453952c78c04c266d3149b3d39e1410d6"},
|
||||
{file = "Brotli-1.0.9-cp27-cp27m-win32.whl", hash = "sha256:afde17ae04d90fbe53afb628f7f2d4ca022797aa093e809de5c3cf276f61bbfa"},
|
||||
{file = "Brotli-1.0.9-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:7cb81373984cc0e4682f31bc3d6be9026006d96eecd07ea49aafb06897746452"},
|
||||
{file = "Brotli-1.0.9-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:db844eb158a87ccab83e868a762ea8024ae27337fc7ddcbfcddd157f841fdfe7"},
|
||||
{file = "Brotli-1.0.9-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:9744a863b489c79a73aba014df554b0e7a0fc44ef3f8a0ef2a52919c7d155031"},
|
||||
{file = "Brotli-1.0.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a72661af47119a80d82fa583b554095308d6a4c356b2a554fdc2799bc19f2a43"},
|
||||
{file = "Brotli-1.0.9-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ee83d3e3a024a9618e5be64648d6d11c37047ac48adff25f12fa4226cf23d1c"},
|
||||
{file = "Brotli-1.0.9-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:19598ecddd8a212aedb1ffa15763dd52a388518c4550e615aed88dc3753c0f0c"},
|
||||
{file = "Brotli-1.0.9-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:44bb8ff420c1d19d91d79d8c3574b8954288bdff0273bf788954064d260d7ab0"},
|
||||
{file = "Brotli-1.0.9-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e23281b9a08ec338469268f98f194658abfb13658ee98e2b7f85ee9dd06caa91"},
|
||||
{file = "Brotli-1.0.9-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:3496fc835370da351d37cada4cf744039616a6db7d13c430035e901443a34daa"},
|
||||
{file = "Brotli-1.0.9-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b83bb06a0192cccf1eb8d0a28672a1b79c74c3a8a5f2619625aeb6f28b3a82bb"},
|
||||
{file = "Brotli-1.0.9-cp310-cp310-win32.whl", hash = "sha256:26d168aac4aaec9a4394221240e8a5436b5634adc3cd1cdf637f6645cecbf181"},
|
||||
{file = "Brotli-1.0.9-cp310-cp310-win_amd64.whl", hash = "sha256:622a231b08899c864eb87e85f81c75e7b9ce05b001e59bbfbf43d4a71f5f32b2"},
|
||||
{file = "Brotli-1.0.9-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:cc0283a406774f465fb45ec7efb66857c09ffefbe49ec20b7882eff6d3c86d3a"},
|
||||
{file = "Brotli-1.0.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:11d3283d89af7033236fa4e73ec2cbe743d4f6a81d41bd234f24bf63dde979df"},
|
||||
{file = "Brotli-1.0.9-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c1306004d49b84bd0c4f90457c6f57ad109f5cc6067a9664e12b7b79a9948ad"},
|
||||
{file = "Brotli-1.0.9-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b1375b5d17d6145c798661b67e4ae9d5496920d9265e2f00f1c2c0b5ae91fbde"},
|
||||
{file = "Brotli-1.0.9-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cab1b5964b39607a66adbba01f1c12df2e55ac36c81ec6ed44f2fca44178bf1a"},
|
||||
{file = "Brotli-1.0.9-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:8ed6a5b3d23ecc00ea02e1ed8e0ff9a08f4fc87a1f58a2530e71c0f48adf882f"},
|
||||
{file = "Brotli-1.0.9-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:cb02ed34557afde2d2da68194d12f5719ee96cfb2eacc886352cb73e3808fc5d"},
|
||||
{file = "Brotli-1.0.9-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:b3523f51818e8f16599613edddb1ff924eeb4b53ab7e7197f85cbc321cdca32f"},
|
||||
{file = "Brotli-1.0.9-cp311-cp311-win32.whl", hash = "sha256:ba72d37e2a924717990f4d7482e8ac88e2ef43fb95491eb6e0d124d77d2a150d"},
|
||||
{file = "Brotli-1.0.9-cp311-cp311-win_amd64.whl", hash = "sha256:3ffaadcaeafe9d30a7e4e1e97ad727e4f5610b9fa2f7551998471e3736738679"},
|
||||
{file = "Brotli-1.0.9-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:c83aa123d56f2e060644427a882a36b3c12db93727ad7a7b9efd7d7f3e9cc2c4"},
|
||||
{file = "Brotli-1.0.9-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:6b2ae9f5f67f89aade1fab0f7fd8f2832501311c363a21579d02defa844d9296"},
|
||||
{file = "Brotli-1.0.9-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:68715970f16b6e92c574c30747c95cf8cf62804569647386ff032195dc89a430"},
|
||||
{file = "Brotli-1.0.9-cp35-cp35m-win32.whl", hash = "sha256:defed7ea5f218a9f2336301e6fd379f55c655bea65ba2476346340a0ce6f74a1"},
|
||||
{file = "Brotli-1.0.9-cp35-cp35m-win_amd64.whl", hash = "sha256:88c63a1b55f352b02c6ffd24b15ead9fc0e8bf781dbe070213039324922a2eea"},
|
||||
{file = "Brotli-1.0.9-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:503fa6af7da9f4b5780bb7e4cbe0c639b010f12be85d02c99452825dd0feef3f"},
|
||||
{file = "Brotli-1.0.9-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:40d15c79f42e0a2c72892bf407979febd9cf91f36f495ffb333d1d04cebb34e4"},
|
||||
{file = "Brotli-1.0.9-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:93130612b837103e15ac3f9cbacb4613f9e348b58b3aad53721d92e57f96d46a"},
|
||||
{file = "Brotli-1.0.9-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87fdccbb6bb589095f413b1e05734ba492c962b4a45a13ff3408fa44ffe6479b"},
|
||||
{file = "Brotli-1.0.9-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:6d847b14f7ea89f6ad3c9e3901d1bc4835f6b390a9c71df999b0162d9bb1e20f"},
|
||||
{file = "Brotli-1.0.9-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:495ba7e49c2db22b046a53b469bbecea802efce200dffb69b93dd47397edc9b6"},
|
||||
{file = "Brotli-1.0.9-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:4688c1e42968ba52e57d8670ad2306fe92e0169c6f3af0089be75bbac0c64a3b"},
|
||||
{file = "Brotli-1.0.9-cp36-cp36m-win32.whl", hash = "sha256:61a7ee1f13ab913897dac7da44a73c6d44d48a4adff42a5701e3239791c96e14"},
|
||||
{file = "Brotli-1.0.9-cp36-cp36m-win_amd64.whl", hash = "sha256:1c48472a6ba3b113452355b9af0a60da5c2ae60477f8feda8346f8fd48e3e87c"},
|
||||
{file = "Brotli-1.0.9-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:3b78a24b5fd13c03ee2b7b86290ed20efdc95da75a3557cc06811764d5ad1126"},
|
||||
{file = "Brotli-1.0.9-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:9d12cf2851759b8de8ca5fde36a59c08210a97ffca0eb94c532ce7b17c6a3d1d"},
|
||||
{file = "Brotli-1.0.9-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:6c772d6c0a79ac0f414a9f8947cc407e119b8598de7621f39cacadae3cf57d12"},
|
||||
{file = "Brotli-1.0.9-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29d1d350178e5225397e28ea1b7aca3648fcbab546d20e7475805437bfb0a130"},
|
||||
{file = "Brotli-1.0.9-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7bbff90b63328013e1e8cb50650ae0b9bac54ffb4be6104378490193cd60f85a"},
|
||||
{file = "Brotli-1.0.9-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:ec1947eabbaf8e0531e8e899fc1d9876c179fc518989461f5d24e2223395a9e3"},
|
||||
{file = "Brotli-1.0.9-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:12effe280b8ebfd389022aa65114e30407540ccb89b177d3fbc9a4f177c4bd5d"},
|
||||
{file = "Brotli-1.0.9-cp37-cp37m-win32.whl", hash = "sha256:f909bbbc433048b499cb9db9e713b5d8d949e8c109a2a548502fb9aa8630f0b1"},
|
||||
{file = "Brotli-1.0.9-cp37-cp37m-win_amd64.whl", hash = "sha256:97f715cf371b16ac88b8c19da00029804e20e25f30d80203417255d239f228b5"},
|
||||
{file = "Brotli-1.0.9-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e16eb9541f3dd1a3e92b89005e37b1257b157b7256df0e36bd7b33b50be73bcb"},
|
||||
{file = "Brotli-1.0.9-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:160c78292e98d21e73a4cc7f76a234390e516afcd982fa17e1422f7c6a9ce9c8"},
|
||||
{file = "Brotli-1.0.9-cp38-cp38-manylinux1_i686.whl", hash = "sha256:b663f1e02de5d0573610756398e44c130add0eb9a3fc912a09665332942a2efb"},
|
||||
{file = "Brotli-1.0.9-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:5b6ef7d9f9c38292df3690fe3e302b5b530999fa90014853dcd0d6902fb59f26"},
|
||||
{file = "Brotli-1.0.9-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8a674ac10e0a87b683f4fa2b6fa41090edfd686a6524bd8dedbd6138b309175c"},
|
||||
{file = "Brotli-1.0.9-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:e2d9e1cbc1b25e22000328702b014227737756f4b5bf5c485ac1d8091ada078b"},
|
||||
{file = "Brotli-1.0.9-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:b336c5e9cf03c7be40c47b5fd694c43c9f1358a80ba384a21969e0b4e66a9b17"},
|
||||
{file = "Brotli-1.0.9-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:85f7912459c67eaab2fb854ed2bc1cc25772b300545fe7ed2dc03954da638649"},
|
||||
{file = "Brotli-1.0.9-cp38-cp38-win32.whl", hash = "sha256:35a3edbe18e876e596553c4007a087f8bcfd538f19bc116917b3c7522fca0429"},
|
||||
{file = "Brotli-1.0.9-cp38-cp38-win_amd64.whl", hash = "sha256:269a5743a393c65db46a7bb982644c67ecba4b8d91b392403ad8a861ba6f495f"},
|
||||
{file = "Brotli-1.0.9-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2aad0e0baa04517741c9bb5b07586c642302e5fb3e75319cb62087bd0995ab19"},
|
||||
{file = "Brotli-1.0.9-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5cb1e18167792d7d21e21365d7650b72d5081ed476123ff7b8cac7f45189c0c7"},
|
||||
{file = "Brotli-1.0.9-cp39-cp39-manylinux1_i686.whl", hash = "sha256:16d528a45c2e1909c2798f27f7bf0a3feec1dc9e50948e738b961618e38b6a7b"},
|
||||
{file = "Brotli-1.0.9-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:56d027eace784738457437df7331965473f2c0da2c70e1a1f6fdbae5402e0389"},
|
||||
{file = "Brotli-1.0.9-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9bf919756d25e4114ace16a8ce91eb340eb57a08e2c6950c3cebcbe3dff2a5e7"},
|
||||
{file = "Brotli-1.0.9-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:e4c4e92c14a57c9bd4cb4be678c25369bf7a092d55fd0866f759e425b9660806"},
|
||||
{file = "Brotli-1.0.9-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:e48f4234f2469ed012a98f4b7874e7f7e173c167bed4934912a29e03167cf6b1"},
|
||||
{file = "Brotli-1.0.9-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9ed4c92a0665002ff8ea852353aeb60d9141eb04109e88928026d3c8a9e5433c"},
|
||||
{file = "Brotli-1.0.9-cp39-cp39-win32.whl", hash = "sha256:cfc391f4429ee0a9370aa93d812a52e1fee0f37a81861f4fdd1f4fb28e8547c3"},
|
||||
{file = "Brotli-1.0.9-cp39-cp39-win_amd64.whl", hash = "sha256:854c33dad5ba0fbd6ab69185fec8dab89e13cda6b7d191ba111987df74f38761"},
|
||||
{file = "Brotli-1.0.9-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:9749a124280a0ada4187a6cfd1ffd35c350fb3af79c706589d98e088c5044267"},
|
||||
{file = "Brotli-1.0.9-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:73fd30d4ce0ea48010564ccee1a26bfe39323fde05cb34b5863455629db61dc7"},
|
||||
{file = "Brotli-1.0.9-pp37-pypy37_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:02177603aaca36e1fd21b091cb742bb3b305a569e2402f1ca38af471777fb019"},
|
||||
{file = "Brotli-1.0.9-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:76ffebb907bec09ff511bb3acc077695e2c32bc2142819491579a695f77ffd4d"},
|
||||
{file = "Brotli-1.0.9-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:b43775532a5904bc938f9c15b77c613cb6ad6fb30990f3b0afaea82797a402d8"},
|
||||
{file = "Brotli-1.0.9-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:5bf37a08493232fbb0f8229f1824b366c2fc1d02d64e7e918af40acd15f3e337"},
|
||||
{file = "Brotli-1.0.9-pp38-pypy38_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:330e3f10cd01da535c70d09c4283ba2df5fb78e915bea0a28becad6e2ac010be"},
|
||||
{file = "Brotli-1.0.9-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:e1abbeef02962596548382e393f56e4c94acd286bd0c5afba756cffc33670e8a"},
|
||||
{file = "Brotli-1.0.9-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:3148362937217b7072cf80a2dcc007f09bb5ecb96dae4617316638194113d5be"},
|
||||
{file = "Brotli-1.0.9-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:336b40348269f9b91268378de5ff44dc6fbaa2268194f85177b53463d313842a"},
|
||||
{file = "Brotli-1.0.9-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3b8b09a16a1950b9ef495a0f8b9d0a87599a9d1f179e2d4ac014b2ec831f87e7"},
|
||||
{file = "Brotli-1.0.9-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:c8e521a0ce7cf690ca84b8cc2272ddaf9d8a50294fd086da67e517439614c755"},
|
||||
{file = "Brotli-1.0.9.zip", hash = "sha256:4d1b810aa0ed773f81dceda2cc7b403d01057458730e309856356d4ef4188438"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "certifi"
|
||||
version = "2023.7.22"
|
||||
@ -303,6 +394,21 @@ Werkzeug = ">=2.3.3"
|
||||
async = ["asgiref (>=3.2)"]
|
||||
dotenv = ["python-dotenv"]
|
||||
|
||||
[[package]]
|
||||
name = "flask-compress"
|
||||
version = "1.13"
|
||||
description = "Compress responses in your Flask app with gzip, deflate or brotli."
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
files = [
|
||||
{file = "Flask-Compress-1.13.tar.gz", hash = "sha256:ee96f18bf9b00f2deb4e3406ca4a05093aa80e2ef0578525a3b4d32ecdff129d"},
|
||||
{file = "Flask_Compress-1.13-py3-none-any.whl", hash = "sha256:1128f71fbd788393ce26830c51f8b5a1a7a4d085e79a21a5cddf4c057dcd559b"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
brotli = "*"
|
||||
flask = "*"
|
||||
|
||||
[[package]]
|
||||
name = "flask-cors"
|
||||
version = "3.0.10"
|
||||
@ -1372,4 +1478,4 @@ files = [
|
||||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = ">=3.10,<3.12"
|
||||
content-hash = "da0e11b5066258d0a56917ea1143fa7196c6de88bd7d6b9f2fc060d84e6bf36f"
|
||||
content-hash = "4ad9b92ccf22a264b35e57ff935827a5717ca3dc023d03f0bdaa83a39ef11c2a"
|
||||
|
@ -21,6 +21,7 @@ psutil = "^5.9.4"
|
||||
show-in-file-manager = "^1.1.4"
|
||||
pendulum = "^2.1.2"
|
||||
alive-progress = "^3.1.4"
|
||||
flask-compress = "^1.13"
|
||||
|
||||
[tool.poetry.dev-dependencies]
|
||||
pylint = "^2.15.5"
|
||||
|
Loading…
x
Reference in New Issue
Block a user