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:
mungai-njoroge 2023-08-24 15:52:09 +03:00
parent e3a61c109b
commit 71cab5f5ea
22 changed files with 437 additions and 163 deletions

View File

@ -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)

View File

@ -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

View File

@ -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}

View File

@ -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)

View File

@ -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 (

View File

@ -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])

View File

@ -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,

View File

@ -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

View File

@ -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:

View File

@ -25,6 +25,7 @@ migrations: list[list[Migration]] = [
v1_3_0.AddLastUpdatedToTrackTable,
v1_3_0.MovePlaylistsAndFavoritesTo10BitHashes,
v1_3_0.RemoveAllTracks,
v1_3_0.UpdateAppSettingsTable,
]
]

View File

@ -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)

View File

@ -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():

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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):

View File

@ -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

View File

@ -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):

View File

@ -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, "߸")

View File

@ -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
View File

@ -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"

View File

@ -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"