From 3dc9bc1f15aeffbf2f37bb59f56267760593d5fb Mon Sep 17 00:00:00 2001 From: geoffrey45 Date: Fri, 20 Jan 2023 22:21:40 +0300 Subject: [PATCH 01/34] print local and remote app urls when app host is set to "0.0.0.0" + update app version in settings.py --- app/db/sqlite/queries.py | 6 + app/db/sqlite/settings.py | 50 ++ app/settings.py | 2 +- app/setup/__init__.py | 15 +- app/utils.py | 23 +- manage.py | 20 +- poetry.lock | 1319 +++++++++++++++++++------------------ pyproject.toml | 5 +- 8 files changed, 790 insertions(+), 650 deletions(-) create mode 100644 app/db/sqlite/settings.py diff --git a/app/db/sqlite/queries.py b/app/db/sqlite/queries.py index 003784a3..ac95a3be 100644 --- a/app/db/sqlite/queries.py +++ b/app/db/sqlite/queries.py @@ -20,6 +20,12 @@ CREATE TABLE IF NOT EXISTS favorites ( hash text not null, type text not null ); + +CREATE TABLE IF NOT EXISTS settings ( + id integer PRIMARY KEY, + root_dirs text NOT NULL, + exclude_dirs text +) """ CREATE_APPDB_TABLES = """ diff --git a/app/db/sqlite/settings.py b/app/db/sqlite/settings.py new file mode 100644 index 00000000..b31041a8 --- /dev/null +++ b/app/db/sqlite/settings.py @@ -0,0 +1,50 @@ +import json +from app.db.sqlite.utils import SQLiteManager + + +class SettingsSQLMethods: + """ + Methods for interacting with the settings table. + """ + + @staticmethod + def update_root_dirs(dirs: list[str]): + """ + Updates custom root directories in the database. + """ + + sql = "UPDATE settings SET root_dirs = ?" + dirs_str = json.dumps(dirs) + + with SQLiteManager(userdata_db=True) as cur: + cur.execute(sql, (dirs_str,)) + + @staticmethod + def get_root_dirs() -> list[str]: + """ + Gets custom root directories from the database. + """ + + sql = "SELECT value FROM settings" + + with SQLiteManager(userdata_db=True) as cur: + cur.execute(sql) + + data = cur.fetchone() + + if data is not None: + return json.loads(data[0]) + + return [] + + @staticmethod + def update_exclude_dirs(dirs: list[str]): + """ + Updates excluded directories in the database. + """ + + sql = "UPDATE settings SET exclude_dirs = ?" + dirs_str = json.dumps(dirs) + + with SQLiteManager(userdata_db=True) as cur: + cur.execute(sql, (dirs_str,)) diff --git a/app/settings.py b/app/settings.py index 3ae7d398..4481198a 100644 --- a/app/settings.py +++ b/app/settings.py @@ -4,7 +4,7 @@ Contains default configs import multiprocessing import os -APP_VERSION = "Swing v0.0.1.alpha" +APP_VERSION = "Swing v.1.0.0.beta.1" # paths CONFIG_FOLDER = ".swing" diff --git a/app/setup/__init__.py b/app/setup/__init__.py index 9e3c9ff4..5ad9cbf7 100644 --- a/app/setup/__init__.py +++ b/app/setup/__init__.py @@ -4,6 +4,7 @@ Contains the functions to prepare the server for use. import os import shutil from configparser import ConfigParser +import caribou # pylint: disable=import-error from app import settings from app.db.sqlite import create_connection, create_tables, queries @@ -11,7 +12,6 @@ from app.db.store import Store from app.settings import APP_DB_PATH, USERDATA_DB_PATH from app.utils import get_home_res_path - config = ConfigParser() config_path = get_home_res_path("pyinstaller.config.ini") @@ -114,6 +114,19 @@ def setup_sqlite(): create_tables(app_db_conn, queries.CREATE_APPDB_TABLES) create_tables(playlist_db_conn, queries.CREATE_USERDATA_TABLES) + userdb_migrations = get_home_res_path("app") / "migrations" / "userdata" + maindb_migrations = get_home_res_path("app") / "migrations" / "main" + + caribou.upgrade( + APP_DB_PATH, + maindb_migrations, + ) + + caribou.upgrade( + str(USERDATA_DB_PATH), + str(userdb_migrations), + ) + app_db_conn.close() playlist_db_conn.close() diff --git a/app/utils.py b/app/utils.py index 1f518b00..bf6a4e9f 100644 --- a/app/utils.py +++ b/app/utils.py @@ -1,14 +1,17 @@ """ This module contains mini functions for the server. """ -import os -import hashlib from pathlib import Path -import threading from datetime import datetime -from unidecode import unidecode + +import os +import socket as Socket +import hashlib +import threading import requests +from unidecode import unidecode + from app import models from app.settings import SUPPORTED_FILES @@ -224,3 +227,15 @@ def get_home_res_path(filename: str): Returns a path to resources in the home directory of this project. Used to resolve resources in builds. """ return (CWD / ".." / filename).resolve() + + +def get_ip(): + """ + Returns the IP address of this device. + """ + soc = Socket.socket(Socket.AF_INET, Socket.SOCK_DGRAM) + soc.connect(("8.8.8.8", 80)) + ip_address = str(soc.getsockname()[0]) + soc.close() + + return ip_address diff --git a/manage.py b/manage.py index 0fc5e203..549081ad 100644 --- a/manage.py +++ b/manage.py @@ -13,7 +13,7 @@ from app.functions import run_periodic_checks from app.lib.watchdogg import Watcher as WatchDog from app.settings import APP_VERSION, HELP_MESSAGE, TCOLOR from app.setup import run_setup -from app.utils import background, get_home_res_path +from app.utils import background, get_home_res_path, get_ip werkzeug = logging.getLogger("werkzeug") werkzeug.setLevel(logging.ERROR) @@ -154,13 +154,21 @@ def start_watchdog(): def log_info(): - lines = " -------------------------------------" + lines = " ---------------------------------------" os.system("cls" if os.name == "nt" else "echo -e \\\\033c") print(lines) print(f" {TCOLOR.HEADER}{APP_VERSION} {TCOLOR.ENDC}") - print( - f" Started app on: {TCOLOR.OKGREEN}http://{Variables.FLASK_HOST}:{Variables.FLASK_PORT}{TCOLOR.ENDC}" - ) + + adresses = [Variables.FLASK_HOST] + + if Variables.FLASK_HOST == "0.0.0.0": + adresses = ["localhost", get_ip()] + + for address in adresses: + print( + f" Started app on: {TCOLOR.OKGREEN}http://{address}:{Variables.FLASK_PORT}{TCOLOR.ENDC}" + ) + print(lines) print("\n") @@ -180,4 +188,4 @@ if __name__ == "__main__": # TODO: Find out how to print in color: red for errors, etc. # TODO: Find a way to verify the host string -# TODO: Organize code in this file: move args to new file, etc. \ No newline at end of file +# TODO: Organize code in this file: move args to new file, etc. diff --git a/poetry.lock b/poetry.lock index a21d58c2..2d205cff 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,3 +1,5 @@ +# This file is automatically @generated by Poetry and should not be changed by hand. + [[package]] name = "altgraph" version = "0.17.3" @@ -5,6 +7,22 @@ description = "Python graph (network) package" category = "main" optional = false python-versions = "*" +files = [ + {file = "altgraph-0.17.3-py2.py3-none-any.whl", hash = "sha256:c8ac1ca6772207179ed8003ce7687757c04b0b71536f81e2ac5755c6226458fe"}, + {file = "altgraph-0.17.3.tar.gz", hash = "sha256:ad33358114df7c9416cdb8fa1eaa5852166c505118717021c6a8c7c7abbd03dd"}, +] + +[[package]] +name = "argparse" +version = "1.4.0" +description = "Python command-line parsing library" +category = "main" +optional = false +python-versions = "*" +files = [ + {file = "argparse-1.4.0-py2.py3-none-any.whl", hash = "sha256:c31647edb69fd3d465a847ea3157d37bed1f95f19760b11a47aa91c04b666314"}, + {file = "argparse-1.4.0.tar.gz", hash = "sha256:62b089a55be1d8949cd2bc7e0df0bddb9e028faefc8c32038cc84862aefdd6e4"}, +] [[package]] name = "astroid" @@ -13,6 +31,10 @@ description = "An abstract syntax tree for Python with inference support." category = "main" optional = false python-versions = ">=3.7.2" +files = [ + {file = "astroid-2.12.12-py3-none-any.whl", hash = "sha256:72702205200b2a638358369d90c222d74ebc376787af8fb2f7f2a86f7b5cc85f"}, + {file = "astroid-2.12.12.tar.gz", hash = "sha256:1c00a14f5a3ed0339d38d2e2e5b74ea2591df5861c0936bb292b84ccf3a78d83"}, +] [package.dependencies] lazy-object-proxy = ">=1.4.0" @@ -28,12 +50,16 @@ description = "Classes Without Boilerplate" category = "main" optional = false python-versions = ">=3.5" +files = [ + {file = "attrs-22.1.0-py2.py3-none-any.whl", hash = "sha256:86efa402f67bf2df34f51a335487cf46b1ec130d02b8d39fd248abfd30da551c"}, + {file = "attrs-22.1.0.tar.gz", hash = "sha256:29adc2665447e5191d0e7c568fde78b21f9672d344281d0c6e1ab085429b22b6"}, +] [package.extras] -dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "mypy (>=0.900,!=0.940)", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit", "cloudpickle"] -docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"] -tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "mypy (>=0.900,!=0.940)", "pytest-mypy-plugins", "zope.interface", "cloudpickle"] -tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "mypy (>=0.900,!=0.940)", "pytest-mypy-plugins", "cloudpickle"] +dev = ["cloudpickle", "coverage[toml] (>=5.0.2)", "furo", "hypothesis", "mypy (>=0.900,!=0.940)", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "sphinx", "sphinx-notfound-page", "zope.interface"] +docs = ["furo", "sphinx", "sphinx-notfound-page", "zope.interface"] +tests = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "zope.interface"] +tests-no-zope = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins"] [[package]] name = "black" @@ -42,588 +68,7 @@ description = "The uncompromising code formatter." category = "dev" optional = false python-versions = ">=3.6.2" - -[package.dependencies] -click = ">=8.0.0" -mypy-extensions = ">=0.4.3" -pathspec = ">=0.9.0" -platformdirs = ">=2" -tomli = {version = ">=1.1.0", markers = "python_full_version < \"3.11.0a7\""} - -[package.extras] -colorama = ["colorama (>=0.4.3)"] -d = ["aiohttp (>=3.7.4)"] -jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] -uvloop = ["uvloop (>=0.15.2)"] - -[[package]] -name = "certifi" -version = "2022.5.18.1" -description = "Python package for providing Mozilla's CA Bundle." -category = "main" -optional = false -python-versions = ">=3.6" - -[[package]] -name = "charset-normalizer" -version = "2.0.12" -description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." -category = "main" -optional = false -python-versions = ">=3.5.0" - -[package.extras] -unicode_backport = ["unicodedata2"] - -[[package]] -name = "click" -version = "8.1.3" -description = "Composable command line interface toolkit" -category = "main" -optional = false -python-versions = ">=3.7" - -[package.dependencies] -colorama = {version = "*", markers = "platform_system == \"Windows\""} - -[[package]] -name = "colorama" -version = "0.4.6" -description = "Cross-platform colored terminal text." -category = "main" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" - -[[package]] -name = "colorgram.py" -version = "1.2.0" -description = "A Python module for extracting colors from images. Get a palette of any picture!" -category = "main" -optional = false -python-versions = "*" - -[package.dependencies] -pillow = ">=3.3.1" - -[[package]] -name = "dill" -version = "0.3.6" -description = "serialize all of python" -category = "main" -optional = false -python-versions = ">=3.7" - -[package.extras] -graph = ["objgraph (>=1.7.2)"] - -[[package]] -name = "exceptiongroup" -version = "1.0.0rc9" -description = "Backport of PEP 654 (exception groups)" -category = "main" -optional = false -python-versions = ">=3.7" - -[package.extras] -test = ["pytest (>=6)"] - -[[package]] -name = "flask" -version = "2.1.2" -description = "A simple framework for building complex web applications." -category = "main" -optional = false -python-versions = ">=3.7" - -[package.dependencies] -click = ">=8.0" -itsdangerous = ">=2.0" -Jinja2 = ">=3.0" -Werkzeug = ">=2.0" - -[package.extras] -async = ["asgiref (>=3.2)"] -dotenv = ["python-dotenv"] - -[[package]] -name = "flask-cors" -version = "3.0.10" -description = "A Flask extension adding a decorator for CORS support" -category = "main" -optional = false -python-versions = "*" - -[package.dependencies] -Flask = ">=0.9" -Six = "*" - -[[package]] -name = "future" -version = "0.18.2" -description = "Clean single-source support for Python 3 and 2" -category = "main" -optional = false -python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" - -[[package]] -name = "gunicorn" -version = "20.1.0" -description = "WSGI HTTP Server for UNIX" -category = "main" -optional = false -python-versions = ">=3.5" - -[package.extras] -eventlet = ["eventlet (>=0.24.1)"] -gevent = ["gevent (>=1.4.0)"] -setproctitle = ["setproctitle"] -tornado = ["tornado (>=0.2)"] - -[[package]] -name = "hypothesis" -version = "6.56.3" -description = "A library for property-based testing" -category = "main" -optional = false -python-versions = ">=3.7" - -[package.dependencies] -attrs = ">=19.2.0" -exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} -sortedcontainers = ">=2.1.0,<3.0.0" - -[package.extras] -all = ["black (>=19.10b0)", "click (>=7.0)", "django (>=3.2)", "dpcontracts (>=0.4)", "lark-parser (>=0.6.5)", "libcst (>=0.3.16)", "numpy (>=1.9.0)", "pandas (>=1.0)", "pytest (>=4.6)", "python-dateutil (>=1.4)", "pytz (>=2014.1)", "redis (>=3.0.0)", "rich (>=9.0.0)", "importlib-metadata (>=3.6)", "backports.zoneinfo (>=0.2.1)", "tzdata (>=2022.5)"] -cli = ["click (>=7.0)", "black (>=19.10b0)", "rich (>=9.0.0)"] -codemods = ["libcst (>=0.3.16)"] -dateutil = ["python-dateutil (>=1.4)"] -django = ["django (>=3.2)"] -dpcontracts = ["dpcontracts (>=0.4)"] -ghostwriter = ["black (>=19.10b0)"] -lark = ["lark-parser (>=0.6.5)"] -numpy = ["numpy (>=1.9.0)"] -pandas = ["pandas (>=1.0)"] -pytest = ["pytest (>=4.6)"] -pytz = ["pytz (>=2014.1)"] -redis = ["redis (>=3.0.0)"] -zoneinfo = ["backports.zoneinfo (>=0.2.1)", "tzdata (>=2022.5)"] - -[[package]] -name = "idna" -version = "3.3" -description = "Internationalized Domain Names in Applications (IDNA)" -category = "main" -optional = false -python-versions = ">=3.5" - -[[package]] -name = "iniconfig" -version = "1.1.1" -description = "iniconfig: brain-dead simple config-ini parsing" -category = "main" -optional = false -python-versions = "*" - -[[package]] -name = "isort" -version = "5.10.1" -description = "A Python utility / library to sort Python imports." -category = "main" -optional = false -python-versions = ">=3.6.1,<4.0" - -[package.extras] -pipfile_deprecated_finder = ["pipreqs", "requirementslib"] -requirements_deprecated_finder = ["pipreqs", "pip-api"] -colors = ["colorama (>=0.4.3,<0.5.0)"] -plugins = ["setuptools"] - -[[package]] -name = "itsdangerous" -version = "2.1.2" -description = "Safely pass data to untrusted environments and back." -category = "main" -optional = false -python-versions = ">=3.7" - -[[package]] -name = "jinja2" -version = "3.1.2" -description = "A very fast and expressive template engine." -category = "main" -optional = false -python-versions = ">=3.7" - -[package.dependencies] -MarkupSafe = ">=2.0" - -[package.extras] -i18n = ["Babel (>=2.7)"] - -[[package]] -name = "lazy-object-proxy" -version = "1.8.0" -description = "A fast and thorough lazy object proxy." -category = "main" -optional = false -python-versions = ">=3.7" - -[[package]] -name = "macholib" -version = "1.16.2" -description = "Mach-O header analysis and editing" -category = "main" -optional = false -python-versions = "*" - -[package.dependencies] -altgraph = ">=0.17" - -[[package]] -name = "markupsafe" -version = "2.1.1" -description = "Safely add untrusted strings to HTML/XML markup." -category = "main" -optional = false -python-versions = ">=3.7" - -[[package]] -name = "mccabe" -version = "0.7.0" -description = "McCabe checker, plugin for flake8" -category = "main" -optional = false -python-versions = ">=3.6" - -[[package]] -name = "mypy-extensions" -version = "0.4.3" -description = "Experimental type system extensions for programs checked with the mypy typechecker." -category = "dev" -optional = false -python-versions = "*" - -[[package]] -name = "packaging" -version = "21.3" -description = "Core utilities for Python packages" -category = "main" -optional = false -python-versions = ">=3.6" - -[package.dependencies] -pyparsing = ">=2.0.2,<3.0.5 || >3.0.5" - -[[package]] -name = "pathspec" -version = "0.9.0" -description = "Utility library for gitignore style pattern matching of file paths." -category = "dev" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" - -[[package]] -name = "pefile" -version = "2022.5.30" -description = "Python PE parsing module" -category = "main" -optional = false -python-versions = ">=3.6.0" - -[package.dependencies] -future = "*" - -[[package]] -name = "pillow" -version = "9.2.0" -description = "Python Imaging Library (Fork)" -category = "main" -optional = false -python-versions = ">=3.7" - -[package.extras] -tests = ["pytest-timeout", "pytest-cov", "pytest", "pyroma", "packaging", "olefile", "markdown2", "defusedxml", "coverage", "check-manifest"] -docs = ["sphinxext-opengraph", "sphinx-removed-in", "sphinx-issues (>=3.0.1)", "sphinx-copybutton", "sphinx (>=2.4)", "olefile", "furo"] - -[[package]] -name = "platformdirs" -version = "2.5.2" -description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." -category = "main" -optional = false -python-versions = ">=3.7" - -[package.extras] -docs = ["furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)", "sphinx (>=4)"] -test = ["appdirs (==1.4.4)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)", "pytest (>=6)"] - -[[package]] -name = "pluggy" -version = "1.0.0" -description = "plugin and hook calling mechanisms for python" -category = "main" -optional = false -python-versions = ">=3.6" - -[package.extras] -testing = ["pytest-benchmark", "pytest"] -dev = ["tox", "pre-commit"] - -[[package]] -name = "py" -version = "1.11.0" -description = "library with cross-python path, ini-parsing, io, code, log facilities" -category = "main" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" - -[[package]] -name = "pyinstaller" -version = "5.7.0" -description = "PyInstaller bundles a Python application and all its dependencies into a single package." -category = "main" -optional = false -python-versions = "<3.12,>=3.7" - -[package.dependencies] -altgraph = "*" -macholib = {version = ">=1.8", markers = "sys_platform == \"darwin\""} -pefile = {version = ">=2022.5.30", markers = "sys_platform == \"win32\""} -pyinstaller-hooks-contrib = ">=2021.4" -pywin32-ctypes = {version = ">=0.2.0", markers = "sys_platform == \"win32\""} - -[package.extras] -encryption = ["tinyaes (>=1.0.0)"] -hook_testing = ["pytest (>=2.7.3)", "execnet (>=1.5.0)", "psutil"] - -[[package]] -name = "pyinstaller-hooks-contrib" -version = "2022.14" -description = "Community maintained hooks for PyInstaller" -category = "main" -optional = false -python-versions = ">=3.7" - -[[package]] -name = "pylint" -version = "2.15.5" -description = "python code static checker" -category = "main" -optional = false -python-versions = ">=3.7.2" - -[package.dependencies] -astroid = ">=2.12.12,<=2.14.0-dev0" -colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} -dill = ">=0.2" -isort = ">=4.2.5,<6" -mccabe = ">=0.6,<0.8" -platformdirs = ">=2.2.0" -tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} -tomlkit = ">=0.10.1" - -[package.extras] -spelling = ["pyenchant (>=3.2,<4.0)"] -testutils = ["gitpython (>3)"] - -[[package]] -name = "pyparsing" -version = "3.0.9" -description = "pyparsing module - Classes and methods to define and execute parsing grammars" -category = "main" -optional = false -python-versions = ">=3.6.8" - -[package.extras] -diagrams = ["railroad-diagrams", "jinja2"] - -[[package]] -name = "pytest" -version = "7.1.3" -description = "pytest: simple powerful testing with Python" -category = "main" -optional = false -python-versions = ">=3.7" - -[package.dependencies] -attrs = ">=19.2.0" -colorama = {version = "*", markers = "sys_platform == \"win32\""} -iniconfig = "*" -packaging = "*" -pluggy = ">=0.12,<2.0" -py = ">=1.8.2" -tomli = ">=1.0.0" - -[package.extras] -testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"] - -[[package]] -name = "pywin32-ctypes" -version = "0.2.0" -description = "" -category = "main" -optional = false -python-versions = "*" - -[[package]] -name = "rapidfuzz" -version = "2.13.7" -description = "rapid fuzzy string matching" -category = "main" -optional = false -python-versions = ">=3.7" - -[package.extras] -full = ["numpy"] - -[[package]] -name = "requests" -version = "2.27.1" -description = "Python HTTP for Humans." -category = "main" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" - -[package.dependencies] -certifi = ">=2017.4.17" -charset-normalizer = {version = ">=2.0.0,<2.1.0", markers = "python_version >= \"3\""} -idna = {version = ">=2.5,<4", markers = "python_version >= \"3\""} -urllib3 = ">=1.21.1,<1.27" - -[package.extras] -socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"] -use_chardet_on_py3 = ["chardet (>=3.0.2,<5)"] - -[[package]] -name = "six" -version = "1.16.0" -description = "Python 2 and 3 compatibility utilities" -category = "main" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" - -[[package]] -name = "sortedcontainers" -version = "2.4.0" -description = "Sorted Containers -- Sorted List, Sorted Dict, Sorted Set" -category = "main" -optional = false -python-versions = "*" - -[[package]] -name = "tinytag" -version = "1.8.1" -description = "Read music meta data and length of MP3, OGG, OPUS, MP4, M4A, FLAC, WMA and Wave files" -category = "main" -optional = false -python-versions = ">=2.7" - -[package.extras] -tests = ["pytest", "pytest-cov", "flake8"] - -[[package]] -name = "tomli" -version = "2.0.1" -description = "A lil' TOML parser" -category = "main" -optional = false -python-versions = ">=3.7" - -[[package]] -name = "tomlkit" -version = "0.11.6" -description = "Style preserving TOML library" -category = "main" -optional = false -python-versions = ">=3.6" - -[[package]] -name = "tqdm" -version = "4.64.0" -description = "Fast, Extensible Progress Meter" -category = "main" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" - -[package.dependencies] -colorama = {version = "*", markers = "platform_system == \"Windows\""} - -[package.extras] -dev = ["py-make (>=0.1.0)", "twine", "wheel"] -notebook = ["ipywidgets (>=6)"] -slack = ["slack-sdk"] -telegram = ["requests"] - -[[package]] -name = "unidecode" -version = "1.3.6" -description = "ASCII transliterations of Unicode text" -category = "main" -optional = false -python-versions = ">=3.5" - -[[package]] -name = "urllib3" -version = "1.26.9" -description = "HTTP library with thread-safe connection pooling, file post, and more." -category = "main" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" - -[package.extras] -brotli = ["brotlicffi (>=0.8.0)", "brotli (>=1.0.9)", "brotlipy (>=0.6.0)"] -secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"] -socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] - -[[package]] -name = "watchdog" -version = "2.2.0" -description = "Filesystem events monitoring" -category = "main" -optional = false -python-versions = ">=3.6" - -[package.extras] -watchmedo = ["PyYAML (>=3.10)"] - -[[package]] -name = "werkzeug" -version = "2.1.2" -description = "The comprehensive WSGI web application library." -category = "main" -optional = false -python-versions = ">=3.7" - -[package.extras] -watchdog = ["watchdog"] - -[[package]] -name = "wrapt" -version = "1.14.1" -description = "Module for decorators, wrappers and monkey patching." -category = "main" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" - -[metadata] -lock-version = "1.1" -python-versions = ">=3.10" -content-hash = "950730e47c15dd241301184fe9a8ff302d7f4411589d5d0b27067cdf0a7d46f5" - -[metadata.files] -altgraph = [ - {file = "altgraph-0.17.3-py2.py3-none-any.whl", hash = "sha256:c8ac1ca6772207179ed8003ce7687757c04b0b71536f81e2ac5755c6226458fe"}, - {file = "altgraph-0.17.3.tar.gz", hash = "sha256:ad33358114df7c9416cdb8fa1eaa5852166c505118717021c6a8c7c7abbd03dd"}, -] -astroid = [ - {file = "astroid-2.12.12-py3-none-any.whl", hash = "sha256:72702205200b2a638358369d90c222d74ebc376787af8fb2f7f2a86f7b5cc85f"}, - {file = "astroid-2.12.12.tar.gz", hash = "sha256:1c00a14f5a3ed0339d38d2e2e5b74ea2591df5861c0936bb292b84ccf3a78d83"}, -] -attrs = [ - {file = "attrs-22.1.0-py2.py3-none-any.whl", hash = "sha256:86efa402f67bf2df34f51a335487cf46b1ec130d02b8d39fd248abfd30da551c"}, - {file = "attrs-22.1.0.tar.gz", hash = "sha256:29adc2665447e5191d0e7c568fde78b21f9672d344281d0c6e1ab085429b22b6"}, -] -black = [ +files = [ {file = "black-22.6.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f586c26118bc6e714ec58c09df0157fe2d9ee195c764f630eb0d8e7ccce72e69"}, {file = "black-22.6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b270a168d69edb8b7ed32c193ef10fd27844e5c60852039599f9184460ce0807"}, {file = "black-22.6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6797f58943fceb1c461fb572edbe828d811e719c24e03375fd25170ada53825e"}, @@ -648,74 +93,316 @@ black = [ {file = "black-22.6.0-py3-none-any.whl", hash = "sha256:ac609cf8ef5e7115ddd07d85d988d074ed00e10fbc3445aee393e70164a2219c"}, {file = "black-22.6.0.tar.gz", hash = "sha256:6c6d39e28aed379aec40da1c65434c77d75e65bb59a1e1c283de545fb4e7c6c9"}, ] -certifi = [ + +[package.dependencies] +click = ">=8.0.0" +mypy-extensions = ">=0.4.3" +pathspec = ">=0.9.0" +platformdirs = ">=2" +tomli = {version = ">=1.1.0", markers = "python_full_version < \"3.11.0a7\""} + +[package.extras] +colorama = ["colorama (>=0.4.3)"] +d = ["aiohttp (>=3.7.4)"] +jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] +uvloop = ["uvloop (>=0.15.2)"] + +[[package]] +name = "caribou" +version = "0.3" +description = "python migrations for sqlite databases" +category = "main" +optional = false +python-versions = "*" +files = [ + {file = "caribou-0.3.0.tar.gz", hash = "sha256:5ca6e6e6ad7d3175137c68d809e203fbd931f79163a5613808a789449fef7863"}, +] + +[package.dependencies] +argparse = ">=1.0.0" + +[[package]] +name = "certifi" +version = "2022.5.18.1" +description = "Python package for providing Mozilla's CA Bundle." +category = "main" +optional = false +python-versions = ">=3.6" +files = [ {file = "certifi-2022.5.18.1-py3-none-any.whl", hash = "sha256:f1d53542ee8cbedbe2118b5686372fb33c297fcd6379b050cca0ef13a597382a"}, {file = "certifi-2022.5.18.1.tar.gz", hash = "sha256:9c5705e395cd70084351dd8ad5c41e65655e08ce46f2ec9cf6c2c08390f71eb7"}, ] -charset-normalizer = [ + +[[package]] +name = "charset-normalizer" +version = "2.0.12" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +category = "main" +optional = false +python-versions = ">=3.5.0" +files = [ {file = "charset-normalizer-2.0.12.tar.gz", hash = "sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597"}, {file = "charset_normalizer-2.0.12-py3-none-any.whl", hash = "sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df"}, ] -click = [ + +[package.extras] +unicode-backport = ["unicodedata2"] + +[[package]] +name = "click" +version = "8.1.3" +description = "Composable command line interface toolkit" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, ] -colorama = [ + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +category = "main" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] -"colorgram.py" = [ + +[[package]] +name = "colorgram.py" +version = "1.2.0" +description = "A Python module for extracting colors from images. Get a palette of any picture!" +category = "main" +optional = false +python-versions = "*" +files = [ {file = "colorgram.py-1.2.0-py2.py3-none-any.whl", hash = "sha256:e990769fa6df7261a450c7d5bef3a1a062f09ba1214bff67b4d6f02970a1a27b"}, {file = "colorgram.py-1.2.0.tar.gz", hash = "sha256:e77766a5f9de7207bdef8f1c22a702cbf09630eae3bc46a450b9d9f12a7bfdbf"}, ] -dill = [ + +[package.dependencies] +pillow = ">=3.3.1" + +[[package]] +name = "dill" +version = "0.3.6" +description = "serialize all of python" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ {file = "dill-0.3.6-py3-none-any.whl", hash = "sha256:a07ffd2351b8c678dfc4a856a3005f8067aea51d6ba6c700796a4d9e280f39f0"}, {file = "dill-0.3.6.tar.gz", hash = "sha256:e5db55f3687856d8fbdab002ed78544e1c4559a130302693d839dfe8f93f2373"}, ] -exceptiongroup = [ + +[package.extras] +graph = ["objgraph (>=1.7.2)"] + +[[package]] +name = "exceptiongroup" +version = "1.0.0rc9" +description = "Backport of PEP 654 (exception groups)" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ {file = "exceptiongroup-1.0.0rc9-py3-none-any.whl", hash = "sha256:2e3c3fc1538a094aab74fad52d6c33fc94de3dfee3ee01f187c0e0c72aec5337"}, {file = "exceptiongroup-1.0.0rc9.tar.gz", hash = "sha256:9086a4a21ef9b31c72181c77c040a074ba0889ee56a7b289ff0afb0d97655f96"}, ] -flask = [ + +[package.extras] +test = ["pytest (>=6)"] + +[[package]] +name = "flask" +version = "2.1.2" +description = "A simple framework for building complex web applications." +category = "main" +optional = false +python-versions = ">=3.7" +files = [ {file = "Flask-2.1.2-py3-none-any.whl", hash = "sha256:fad5b446feb0d6db6aec0c3184d16a8c1f6c3e464b511649c8918a9be100b4fe"}, {file = "Flask-2.1.2.tar.gz", hash = "sha256:315ded2ddf8a6281567edb27393010fe3406188bafbfe65a3339d5787d89e477"}, ] -flask-cors = [ + +[package.dependencies] +click = ">=8.0" +itsdangerous = ">=2.0" +Jinja2 = ">=3.0" +Werkzeug = ">=2.0" + +[package.extras] +async = ["asgiref (>=3.2)"] +dotenv = ["python-dotenv"] + +[[package]] +name = "flask-cors" +version = "3.0.10" +description = "A Flask extension adding a decorator for CORS support" +category = "main" +optional = false +python-versions = "*" +files = [ {file = "Flask-Cors-3.0.10.tar.gz", hash = "sha256:b60839393f3b84a0f3746f6cdca56c1ad7426aa738b70d6c61375857823181de"}, {file = "Flask_Cors-3.0.10-py2.py3-none-any.whl", hash = "sha256:74efc975af1194fc7891ff5cd85b0f7478be4f7f59fe158102e91abb72bb4438"}, ] -future = [ + +[package.dependencies] +Flask = ">=0.9" +Six = "*" + +[[package]] +name = "future" +version = "0.18.2" +description = "Clean single-source support for Python 3 and 2" +category = "main" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" +files = [ {file = "future-0.18.2.tar.gz", hash = "sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d"}, ] -gunicorn = [ + +[[package]] +name = "gunicorn" +version = "20.1.0" +description = "WSGI HTTP Server for UNIX" +category = "main" +optional = false +python-versions = ">=3.5" +files = [ {file = "gunicorn-20.1.0-py3-none-any.whl", hash = "sha256:9dcc4547dbb1cb284accfb15ab5667a0e5d1881cc443e0677b4882a4067a807e"}, {file = "gunicorn-20.1.0.tar.gz", hash = "sha256:e0a968b5ba15f8a328fdfd7ab1fcb5af4470c28aaf7e55df02a99bc13138e6e8"}, ] -hypothesis = [ + +[package.dependencies] +setuptools = ">=3.0" + +[package.extras] +eventlet = ["eventlet (>=0.24.1)"] +gevent = ["gevent (>=1.4.0)"] +setproctitle = ["setproctitle"] +tornado = ["tornado (>=0.2)"] + +[[package]] +name = "hypothesis" +version = "6.56.3" +description = "A library for property-based testing" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ {file = "hypothesis-6.56.3-py3-none-any.whl", hash = "sha256:802d236d03dbd54e0e1c55c0daa2ec41aeaadc87a4dcbb41421b78bf3f7a7789"}, {file = "hypothesis-6.56.3.tar.gz", hash = "sha256:15dae5d993339aefa57e00f5cb5a5817ff300eeb661d96d1c9d094eb62b04c9a"}, ] -idna = [ + +[package.dependencies] +attrs = ">=19.2.0" +exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} +sortedcontainers = ">=2.1.0,<3.0.0" + +[package.extras] +all = ["backports.zoneinfo (>=0.2.1)", "black (>=19.10b0)", "click (>=7.0)", "django (>=3.2)", "dpcontracts (>=0.4)", "importlib-metadata (>=3.6)", "lark-parser (>=0.6.5)", "libcst (>=0.3.16)", "numpy (>=1.9.0)", "pandas (>=1.0)", "pytest (>=4.6)", "python-dateutil (>=1.4)", "pytz (>=2014.1)", "redis (>=3.0.0)", "rich (>=9.0.0)", "tzdata (>=2022.5)"] +cli = ["black (>=19.10b0)", "click (>=7.0)", "rich (>=9.0.0)"] +codemods = ["libcst (>=0.3.16)"] +dateutil = ["python-dateutil (>=1.4)"] +django = ["django (>=3.2)"] +dpcontracts = ["dpcontracts (>=0.4)"] +ghostwriter = ["black (>=19.10b0)"] +lark = ["lark-parser (>=0.6.5)"] +numpy = ["numpy (>=1.9.0)"] +pandas = ["pandas (>=1.0)"] +pytest = ["pytest (>=4.6)"] +pytz = ["pytz (>=2014.1)"] +redis = ["redis (>=3.0.0)"] +zoneinfo = ["backports.zoneinfo (>=0.2.1)", "tzdata (>=2022.5)"] + +[[package]] +name = "idna" +version = "3.3" +description = "Internationalized Domain Names in Applications (IDNA)" +category = "main" +optional = false +python-versions = ">=3.5" +files = [ {file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"}, {file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"}, ] -iniconfig = [ + +[[package]] +name = "iniconfig" +version = "1.1.1" +description = "iniconfig: brain-dead simple config-ini parsing" +category = "main" +optional = false +python-versions = "*" +files = [ {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, ] -isort = [ + +[[package]] +name = "isort" +version = "5.10.1" +description = "A Python utility / library to sort Python imports." +category = "main" +optional = false +python-versions = ">=3.6.1,<4.0" +files = [ {file = "isort-5.10.1-py3-none-any.whl", hash = "sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7"}, {file = "isort-5.10.1.tar.gz", hash = "sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951"}, ] -itsdangerous = [ + +[package.extras] +colors = ["colorama (>=0.4.3,<0.5.0)"] +pipfile-deprecated-finder = ["pipreqs", "requirementslib"] +plugins = ["setuptools"] +requirements-deprecated-finder = ["pip-api", "pipreqs"] + +[[package]] +name = "itsdangerous" +version = "2.1.2" +description = "Safely pass data to untrusted environments and back." +category = "main" +optional = false +python-versions = ">=3.7" +files = [ {file = "itsdangerous-2.1.2-py3-none-any.whl", hash = "sha256:2c2349112351b88699d8d4b6b075022c0808887cb7ad10069318a8b0bc88db44"}, {file = "itsdangerous-2.1.2.tar.gz", hash = "sha256:5dbbc68b317e5e42f327f9021763545dc3fc3bfe22e6deb96aaf1fc38874156a"}, ] -jinja2 = [ + +[[package]] +name = "jinja2" +version = "3.1.2" +description = "A very fast and expressive template engine." +category = "main" +optional = false +python-versions = ">=3.7" +files = [ {file = "Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"}, {file = "Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852"}, ] -lazy-object-proxy = [ + +[package.dependencies] +MarkupSafe = ">=2.0" + +[package.extras] +i18n = ["Babel (>=2.7)"] + +[[package]] +name = "lazy-object-proxy" +version = "1.8.0" +description = "A fast and thorough lazy object proxy." +category = "main" +optional = false +python-versions = ">=3.7" +files = [ {file = "lazy-object-proxy-1.8.0.tar.gz", hash = "sha256:c219a00245af0f6fa4e95901ed28044544f50152840c5b6a3e7b2568db34d156"}, {file = "lazy_object_proxy-1.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4fd031589121ad46e293629b39604031d354043bb5cdf83da4e93c2d7f3389fe"}, {file = "lazy_object_proxy-1.8.0-cp310-cp310-win32.whl", hash = "sha256:b70d6e7a332eb0217e7872a73926ad4fdc14f846e85ad6749ad111084e76df25"}, @@ -736,11 +423,30 @@ lazy-object-proxy = [ {file = "lazy_object_proxy-1.8.0-pp38-pypy38_pp73-any.whl", hash = "sha256:7e1561626c49cb394268edd00501b289053a652ed762c58e1081224c8d881cec"}, {file = "lazy_object_proxy-1.8.0-pp39-pypy39_pp73-any.whl", hash = "sha256:ce58b2b3734c73e68f0e30e4e725264d4d6be95818ec0a0be4bb6bf9a7e79aa8"}, ] -macholib = [ + +[[package]] +name = "macholib" +version = "1.16.2" +description = "Mach-O header analysis and editing" +category = "main" +optional = false +python-versions = "*" +files = [ {file = "macholib-1.16.2-py2.py3-none-any.whl", hash = "sha256:44c40f2cd7d6726af8fa6fe22549178d3a4dfecc35a9cd15ea916d9c83a688e0"}, {file = "macholib-1.16.2.tar.gz", hash = "sha256:557bbfa1bb255c20e9abafe7ed6cd8046b48d9525db2f9b77d3122a63a2a8bf8"}, ] -markupsafe = [ + +[package.dependencies] +altgraph = ">=0.17" + +[[package]] +name = "markupsafe" +version = "2.1.1" +description = "Safely add untrusted strings to HTML/XML markup." +category = "main" +optional = false +python-versions = ">=3.7" +files = [ {file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:86b1f75c4e7c2ac2ccdaec2b9022845dbb81880ca318bb7a0a01fbf7813e3812"}, {file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f121a1420d4e173a5d96e47e9a0c0dcff965afdf1626d28de1460815f7c4ee7a"}, {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a49907dd8420c5685cfa064a1335b6754b74541bbb3706c259c02ed65b644b3e"}, @@ -782,26 +488,80 @@ markupsafe = [ {file = "MarkupSafe-2.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:46d00d6cfecdde84d40e572d63735ef81423ad31184100411e6e3388d405e247"}, {file = "MarkupSafe-2.1.1.tar.gz", hash = "sha256:7f91197cc9e48f989d12e4e6fbc46495c446636dfc81b9ccf50bb0ec74b91d4b"}, ] -mccabe = [ + +[[package]] +name = "mccabe" +version = "0.7.0" +description = "McCabe checker, plugin for flake8" +category = "main" +optional = false +python-versions = ">=3.6" +files = [ {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, ] -mypy-extensions = [ + +[[package]] +name = "mypy-extensions" +version = "0.4.3" +description = "Experimental type system extensions for programs checked with the mypy typechecker." +category = "dev" +optional = false +python-versions = "*" +files = [ {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, ] -packaging = [ + +[[package]] +name = "packaging" +version = "21.3" +description = "Core utilities for Python packages" +category = "main" +optional = false +python-versions = ">=3.6" +files = [ {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, ] -pathspec = [ + +[package.dependencies] +pyparsing = ">=2.0.2,<3.0.5 || >3.0.5" + +[[package]] +name = "pathspec" +version = "0.9.0" +description = "Utility library for gitignore style pattern matching of file paths." +category = "dev" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" +files = [ {file = "pathspec-0.9.0-py2.py3-none-any.whl", hash = "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a"}, {file = "pathspec-0.9.0.tar.gz", hash = "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1"}, ] -pefile = [ + +[[package]] +name = "pefile" +version = "2022.5.30" +description = "Python PE parsing module" +category = "main" +optional = false +python-versions = ">=3.6.0" +files = [ {file = "pefile-2022.5.30.tar.gz", hash = "sha256:a5488a3dd1fd021ce33f969780b88fe0f7eebb76eb20996d7318f307612a045b"}, ] -pillow = [ + +[package.dependencies] +future = "*" + +[[package]] +name = "pillow" +version = "9.2.0" +description = "Python Imaging Library (Fork)" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ {file = "Pillow-9.2.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:a9c9bc489f8ab30906d7a85afac4b4944a572a7432e00698a7239f44a44e6efb"}, {file = "Pillow-9.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:510cef4a3f401c246cfd8227b300828715dd055463cdca6176c2e4036df8bd4f"}, {file = "Pillow-9.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7888310f6214f19ab2b6df90f3f06afa3df7ef7355fc025e78a3044737fab1f5"}, @@ -812,8 +572,8 @@ pillow = [ {file = "Pillow-9.2.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:37ff6b522a26d0538b753f0b4e8e164fdada12db6c6f00f62145d732d8a3152e"}, {file = "Pillow-9.2.0-cp310-cp310-win32.whl", hash = "sha256:c79698d4cd9318d9481d89a77e2d3fcaeff5486be641e60a4b49f3d2ecca4e28"}, {file = "Pillow-9.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:254164c57bab4b459f14c64e93df11eff5ded575192c294a0c49270f22c5d93d"}, - {file = "Pillow-9.2.0-cp311-cp311-macosx_10_10_universal2.whl", hash = "sha256:408673ed75594933714482501fe97e055a42996087eeca7e5d06e33218d05aa8"}, - {file = "Pillow-9.2.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:727dd1389bc5cb9827cbd1f9d40d2c2a1a0c9b32dd2261db522d22a604a6eec9"}, + {file = "Pillow-9.2.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:adabc0bce035467fb537ef3e5e74f2847c8af217ee0be0455d4fec8adc0462fc"}, + {file = "Pillow-9.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:336b9036127eab855beec9662ac3ea13a4544a523ae273cbf108b228ecac8437"}, {file = "Pillow-9.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50dff9cc21826d2977ef2d2a205504034e3a4563ca6f5db739b0d1026658e004"}, {file = "Pillow-9.2.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cb6259196a589123d755380b65127ddc60f4c64b21fc3bb46ce3a6ea663659b0"}, {file = "Pillow-9.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b0554af24df2bf96618dac71ddada02420f946be943b181108cac55a7a2dcd4"}, @@ -861,19 +621,63 @@ pillow = [ {file = "Pillow-9.2.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:0030fdbd926fb85844b8b92e2f9449ba89607231d3dd597a21ae72dc7fe26927"}, {file = "Pillow-9.2.0.tar.gz", hash = "sha256:75e636fd3e0fb872693f23ccb8a5ff2cd578801251f3a4f6854c6a5d437d3c04"}, ] -platformdirs = [ + +[package.extras] +docs = ["furo", "olefile", "sphinx (>=2.4)", "sphinx-copybutton", "sphinx-issues (>=3.0.1)", "sphinx-removed-in", "sphinxext-opengraph"] +tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"] + +[[package]] +name = "platformdirs" +version = "2.5.2" +description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +category = "main" +optional = false +python-versions = ">=3.7" +files = [ {file = "platformdirs-2.5.2-py3-none-any.whl", hash = "sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788"}, {file = "platformdirs-2.5.2.tar.gz", hash = "sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19"}, ] -pluggy = [ + +[package.extras] +docs = ["furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx (>=4)", "sphinx-autodoc-typehints (>=1.12)"] +test = ["appdirs (==1.4.4)", "pytest (>=6)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)"] + +[[package]] +name = "pluggy" +version = "1.0.0" +description = "plugin and hook calling mechanisms for python" +category = "main" +optional = false +python-versions = ">=3.6" +files = [ {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, ] -py = [ + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "py" +version = "1.11.0" +description = "library with cross-python path, ini-parsing, io, code, log facilities" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +files = [ {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, ] -pyinstaller = [ + +[[package]] +name = "pyinstaller" +version = "5.7.0" +description = "PyInstaller bundles a Python application and all its dependencies into a single package." +category = "main" +optional = false +python-versions = "<3.12,>=3.7" +files = [ {file = "pyinstaller-5.7.0-py3-none-macosx_10_13_universal2.whl", hash = "sha256:b967ae71ab7b05e18608dbb4518da5afa54f0835927cb7a5ce52ab8fffed03b6"}, {file = "pyinstaller-5.7.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:3180b9bf22263380adc5e2ee051b7c21463292877215bbe70c9155dc76f4b966"}, {file = "pyinstaller-5.7.0-py3-none-manylinux2014_i686.whl", hash = "sha256:0f80e2403e76630ad3392c71f09c1a4284e8d8a8a99fb55ff3a0aba0e06300ed"}, @@ -887,27 +691,116 @@ pyinstaller = [ {file = "pyinstaller-5.7.0-py3-none-win_arm64.whl", hash = "sha256:3e51e18a16dec0414079762843cf892a5d70749ad56ca7b3c7b5f8367dc50b1e"}, {file = "pyinstaller-5.7.0.tar.gz", hash = "sha256:0e5953937d35f0b37543cc6915dacaf3239bcbdf3fd3ecbb7866645468a16775"}, ] -pyinstaller-hooks-contrib = [ + +[package.dependencies] +altgraph = "*" +macholib = {version = ">=1.8", markers = "sys_platform == \"darwin\""} +pefile = {version = ">=2022.5.30", markers = "sys_platform == \"win32\""} +pyinstaller-hooks-contrib = ">=2021.4" +pywin32-ctypes = {version = ">=0.2.0", markers = "sys_platform == \"win32\""} +setuptools = ">=42.0.0" + +[package.extras] +encryption = ["tinyaes (>=1.0.0)"] +hook-testing = ["execnet (>=1.5.0)", "psutil", "pytest (>=2.7.3)"] + +[[package]] +name = "pyinstaller-hooks-contrib" +version = "2022.14" +description = "Community maintained hooks for PyInstaller" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ {file = "pyinstaller-hooks-contrib-2022.14.tar.gz", hash = "sha256:5ae8da3a92cf20e37b3e00604d0c3468896e7d746e5c1449473597a724331b0b"}, {file = "pyinstaller_hooks_contrib-2022.14-py2.py3-none-any.whl", hash = "sha256:1a125838a22d7b35a18993c6e56d3c5cc3ad7da00954f95bc5606523939203f2"}, ] -pylint = [ + +[[package]] +name = "pylint" +version = "2.15.5" +description = "python code static checker" +category = "main" +optional = false +python-versions = ">=3.7.2" +files = [ {file = "pylint-2.15.5-py3-none-any.whl", hash = "sha256:c2108037eb074334d9e874dc3c783752cc03d0796c88c9a9af282d0f161a1004"}, {file = "pylint-2.15.5.tar.gz", hash = "sha256:3b120505e5af1d06a5ad76b55d8660d44bf0f2fc3c59c2bdd94e39188ee3a4df"}, ] -pyparsing = [ + +[package.dependencies] +astroid = ">=2.12.12,<=2.14.0-dev0" +colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} +dill = ">=0.2" +isort = ">=4.2.5,<6" +mccabe = ">=0.6,<0.8" +platformdirs = ">=2.2.0" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +tomlkit = ">=0.10.1" + +[package.extras] +spelling = ["pyenchant (>=3.2,<4.0)"] +testutils = ["gitpython (>3)"] + +[[package]] +name = "pyparsing" +version = "3.0.9" +description = "pyparsing module - Classes and methods to define and execute parsing grammars" +category = "main" +optional = false +python-versions = ">=3.6.8" +files = [ {file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"}, {file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"}, ] -pytest = [ + +[package.extras] +diagrams = ["jinja2", "railroad-diagrams"] + +[[package]] +name = "pytest" +version = "7.1.3" +description = "pytest: simple powerful testing with Python" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ {file = "pytest-7.1.3-py3-none-any.whl", hash = "sha256:1377bda3466d70b55e3f5cecfa55bb7cfcf219c7964629b967c37cf0bda818b7"}, {file = "pytest-7.1.3.tar.gz", hash = "sha256:4f365fec2dff9c1162f834d9f18af1ba13062db0c708bf7b946f8a5c76180c39"}, ] -pywin32-ctypes = [ + +[package.dependencies] +attrs = ">=19.2.0" +colorama = {version = "*", markers = "sys_platform == \"win32\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=0.12,<2.0" +py = ">=1.8.2" +tomli = ">=1.0.0" + +[package.extras] +testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"] + +[[package]] +name = "pywin32-ctypes" +version = "0.2.0" +description = "" +category = "main" +optional = false +python-versions = "*" +files = [ {file = "pywin32-ctypes-0.2.0.tar.gz", hash = "sha256:24ffc3b341d457d48e8922352130cf2644024a4ff09762a2261fd34c36ee5942"}, {file = "pywin32_ctypes-0.2.0-py2.py3-none-any.whl", hash = "sha256:9dc2d991b3479cc2df15930958b674a48a227d5361d413827a4cfd0b5876fc98"}, ] -rapidfuzz = [ + +[[package]] +name = "rapidfuzz" +version = "2.13.7" +description = "rapid fuzzy string matching" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ {file = "rapidfuzz-2.13.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b75dd0928ce8e216f88660ab3d5c5ffe990f4dd682fd1709dba29d5dafdde6de"}, {file = "rapidfuzz-2.13.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:24d3fea10680d085fd0a4d76e581bfb2b1074e66e78fd5964d4559e1fcd2a2d4"}, {file = "rapidfuzz-2.13.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8109e0324d21993d5b2d111742bf5958f3516bf8c59f297c5d1cc25a2342eb66"}, @@ -998,42 +891,169 @@ rapidfuzz = [ {file = "rapidfuzz-2.13.7-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:b6389c50d8d214c9cd11a77f6d501529cb23279a9c9cafe519a3a4b503b5f72a"}, {file = "rapidfuzz-2.13.7.tar.gz", hash = "sha256:8d3e252d4127c79b4d7c2ae47271636cbaca905c8bb46d80c7930ab906cf4b5c"}, ] -requests = [ + +[package.extras] +full = ["numpy"] + +[[package]] +name = "requests" +version = "2.27.1" +description = "Python HTTP for Humans." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" +files = [ {file = "requests-2.27.1-py2.py3-none-any.whl", hash = "sha256:f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d"}, {file = "requests-2.27.1.tar.gz", hash = "sha256:68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61"}, ] -six = [ + +[package.dependencies] +certifi = ">=2017.4.17" +charset-normalizer = {version = ">=2.0.0,<2.1.0", markers = "python_version >= \"3\""} +idna = {version = ">=2.5,<4", markers = "python_version >= \"3\""} +urllib3 = ">=1.21.1,<1.27" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<5)"] + +[[package]] +name = "setuptools" +version = "66.0.0" +description = "Easily download, build, install, upgrade, and uninstall Python packages" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "setuptools-66.0.0-py3-none-any.whl", hash = "sha256:a78d01d1e2c175c474884671dde039962c9d74c7223db7369771fcf6e29ceeab"}, + {file = "setuptools-66.0.0.tar.gz", hash = "sha256:bd6eb2d6722568de6d14b87c44a96fac54b2a45ff5e940e639979a3d1792adb6"}, +] + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"] +testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8 (<5)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] +testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] + +[[package]] +name = "six" +version = "1.16.0" +description = "Python 2 and 3 compatibility utilities" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +files = [ {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, ] -sortedcontainers = [ + +[[package]] +name = "sortedcontainers" +version = "2.4.0" +description = "Sorted Containers -- Sorted List, Sorted Dict, Sorted Set" +category = "main" +optional = false +python-versions = "*" +files = [ {file = "sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0"}, {file = "sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88"}, ] -tinytag = [ + +[[package]] +name = "tinytag" +version = "1.8.1" +description = "Read music meta data and length of MP3, OGG, OPUS, MP4, M4A, FLAC, WMA and Wave files" +category = "main" +optional = false +python-versions = ">=2.7" +files = [ {file = "tinytag-1.8.1.tar.gz", hash = "sha256:363ab3107831a5598b68aaa061aba915fb1c7b4254d770232e65d5db8487636d"}, ] -tomli = [ + +[package.extras] +tests = ["flake8", "pytest", "pytest-cov"] + +[[package]] +name = "tomli" +version = "2.0.1" +description = "A lil' TOML parser" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, ] -tomlkit = [ + +[[package]] +name = "tomlkit" +version = "0.11.6" +description = "Style preserving TOML library" +category = "main" +optional = false +python-versions = ">=3.6" +files = [ {file = "tomlkit-0.11.6-py3-none-any.whl", hash = "sha256:07de26b0d8cfc18f871aec595fda24d95b08fef89d147caa861939f37230bf4b"}, {file = "tomlkit-0.11.6.tar.gz", hash = "sha256:71b952e5721688937fb02cf9d354dbcf0785066149d2855e44531ebdd2b65d73"}, ] -tqdm = [ + +[[package]] +name = "tqdm" +version = "4.64.0" +description = "Fast, Extensible Progress Meter" +category = "main" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" +files = [ {file = "tqdm-4.64.0-py2.py3-none-any.whl", hash = "sha256:74a2cdefe14d11442cedf3ba4e21a3b84ff9a2dbdc6cfae2c34addb2a14a5ea6"}, {file = "tqdm-4.64.0.tar.gz", hash = "sha256:40be55d30e200777a307a7585aee69e4eabb46b4ec6a4b4a5f2d9f11e7d5408d"}, ] -unidecode = [ + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[package.extras] +dev = ["py-make (>=0.1.0)", "twine", "wheel"] +notebook = ["ipywidgets (>=6)"] +slack = ["slack-sdk"] +telegram = ["requests"] + +[[package]] +name = "unidecode" +version = "1.3.6" +description = "ASCII transliterations of Unicode text" +category = "main" +optional = false +python-versions = ">=3.5" +files = [ {file = "Unidecode-1.3.6-py3-none-any.whl", hash = "sha256:547d7c479e4f377b430dd91ac1275d593308dce0fc464fb2ab7d41f82ec653be"}, {file = "Unidecode-1.3.6.tar.gz", hash = "sha256:fed09cf0be8cf415b391642c2a5addfc72194407caee4f98719e40ec2a72b830"}, ] -urllib3 = [ + +[[package]] +name = "urllib3" +version = "1.26.9" +description = "HTTP library with thread-safe connection pooling, file post, and more." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" +files = [ {file = "urllib3-1.26.9-py2.py3-none-any.whl", hash = "sha256:44ece4d53fb1706f667c9bd1c648f5469a2ec925fcf3a776667042d645472c14"}, {file = "urllib3-1.26.9.tar.gz", hash = "sha256:aabaf16477806a5e1dd19aa41f8c2b7950dd3c746362d7e3223dbe6de6ac448e"}, ] -watchdog = [ + +[package.extras] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"] +secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)"] +socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] + +[[package]] +name = "watchdog" +version = "2.2.0" +description = "Filesystem events monitoring" +category = "main" +optional = false +python-versions = ">=3.6" +files = [ {file = "watchdog-2.2.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ed91c3ccfc23398e7aa9715abf679d5c163394b8cad994f34f156d57a7c163dc"}, {file = "watchdog-2.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:76a2743402b794629a955d96ea2e240bd0e903aa26e02e93cd2d57b33900962b"}, {file = "watchdog-2.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:920a4bda7daa47545c3201a3292e99300ba81ca26b7569575bd086c865889090"}, @@ -1063,11 +1083,33 @@ watchdog = [ {file = "watchdog-2.2.0-py3-none-win_ia64.whl", hash = "sha256:ad0150536469fa4b693531e497ffe220d5b6cd76ad2eda474a5e641ee204bbb6"}, {file = "watchdog-2.2.0.tar.gz", hash = "sha256:83cf8bc60d9c613b66a4c018051873d6273d9e45d040eed06d6a96241bd8ec01"}, ] -werkzeug = [ + +[package.extras] +watchmedo = ["PyYAML (>=3.10)"] + +[[package]] +name = "werkzeug" +version = "2.1.2" +description = "The comprehensive WSGI web application library." +category = "main" +optional = false +python-versions = ">=3.7" +files = [ {file = "Werkzeug-2.1.2-py3-none-any.whl", hash = "sha256:72a4b735692dd3135217911cbeaa1be5fa3f62bffb8745c5215420a03dc55255"}, {file = "Werkzeug-2.1.2.tar.gz", hash = "sha256:1ce08e8093ed67d638d63879fd1ba3735817f7a80de3674d293f5984f25fb6e6"}, ] -wrapt = [ + +[package.extras] +watchdog = ["watchdog"] + +[[package]] +name = "wrapt" +version = "1.14.1" +description = "Module for decorators, wrappers and monkey patching." +category = "main" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" +files = [ {file = "wrapt-1.14.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:1b376b3f4896e7930f1f772ac4b064ac12598d1c38d04907e696cc4d794b43d3"}, {file = "wrapt-1.14.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:903500616422a40a98a5a3c4ff4ed9d0066f3b4c951fa286018ecdf0750194ef"}, {file = "wrapt-1.14.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:5a9a0d155deafd9448baff28c08e150d9b24ff010e899311ddd63c45c2445e28"}, @@ -1133,3 +1175,8 @@ wrapt = [ {file = "wrapt-1.14.1-cp39-cp39-win_amd64.whl", hash = "sha256:dee60e1de1898bde3b238f18340eec6148986da0455d8ba7848d50470a7a32fb"}, {file = "wrapt-1.14.1.tar.gz", hash = "sha256:380a85cf89e0e69b7cfbe2ea9f765f004ff419f34194018a6827ac0e3edfed4d"}, ] + +[metadata] +lock-version = "2.0" +python-versions = ">=3.10,<3.12" +content-hash = "83ce6d95a8e6bdefff93a3badca9e7b7b53174eaea85f908e9cea9a37afaec7c" diff --git a/pyproject.toml b/pyproject.toml index 9b1c7da9..fde02919 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ description = "" authors = ["geoffrey45 "] [tool.poetry.dependencies] -python = ">=3.10" +python = ">=3.10,<3.12" Flask = "^2.0.2" Flask-Cors = "^3.0.10" requests = "^2.27.1" @@ -18,11 +18,12 @@ rapidfuzz = "^2.13.7" tinytag = "^1.8.1" hypothesis = "^6.56.3" pytest = "^7.1.3" -pylint = "^2.15.5" Unidecode = "^1.3.6" pyinstaller = "^5.7.0" +caribou = "^0.3.0" [tool.poetry.dev-dependencies] +pylint = "^2.15.5" black = {version = "^22.6.0", allow-prereleases = true} [build-system] From 4e6e1f03dcb521f8278aae1d4bd8b21731c2b030 Mon Sep 17 00:00:00 2001 From: geoffrey45 Date: Sat, 21 Jan 2023 18:07:20 +0300 Subject: [PATCH 02/34] move imgserver to app/api folder + add sqlite methods to configure custom root directories + add sqlite.settings module + remove date and app name from logger messages + add api route to browse directories --- app/api/__init__.py | 16 +++- app/api/folder.py | 27 ++++++ .../__init__.py => api/imgserver.py} | 2 +- app/api/settings.py | 61 +++++++++++++ app/db/sqlite/settings.py | 85 +++++++++++++------ app/db/sqlite/tracks.py | 2 + app/lib/populate.py | 14 ++- app/logger.py | 2 +- manage.py | 1 - 9 files changed, 179 insertions(+), 31 deletions(-) rename app/{imgserver/__init__.py => api/imgserver.py} (97%) create mode 100644 app/api/settings.py diff --git a/app/api/__init__.py b/app/api/__init__.py index 5e7a5c7a..3d662b28 100644 --- a/app/api/__init__.py +++ b/app/api/__init__.py @@ -5,8 +5,17 @@ This module combines all API blueprints into a single Flask app instance. from flask import Flask from flask_cors import CORS -from app.api import album, artist, favorites, folder, playlist, search, track -from app.imgserver import imgbp as imgserver +from app.api import ( + album, + artist, + favorites, + folder, + playlist, + search, + track, + settings, + imgserver, +) def create_api(): @@ -25,6 +34,7 @@ def create_api(): app.register_blueprint(folder.folderbp) app.register_blueprint(playlist.playlistbp) app.register_blueprint(favorites.favbp) - app.register_blueprint(imgserver) + app.register_blueprint(imgserver.imgbp) + app.register_blueprint(settings.settingsbp) return app diff --git a/app/api/folder.py b/app/api/folder.py index 88ac6cb0..1cdcb057 100644 --- a/app/api/folder.py +++ b/app/api/folder.py @@ -1,11 +1,13 @@ """ Contains all the folder routes. """ +import os from flask import Blueprint, request from app import settings from app.lib.folderslib import GetFilesAndDirs + folderbp = Blueprint("folder", __name__, url_prefix="/") @@ -30,3 +32,28 @@ def get_folder_tree(): "tracks": tracks, "folders": sorted(folders, key=lambda i: i.name), } + + +@folderbp.route("/folder/dir-browser", methods=["POST"]) +def list_folders(): + """ + Returns a list of all the folders in the given folder. + """ + data = request.get_json() + + try: + req_dir: str = data["folder"] + except KeyError: + req_dir = settings.HOME_DIR + + if req_dir == "$home": + req_dir = settings.HOME_DIR + + entries = os.scandir(req_dir) + + dirs = [e.name for e in entries if e.is_dir() and not e.name.startswith(".")] + dirs = [{"name": d, "path": os.path.join(req_dir, d)} for d in dirs] + + return { + "folders": sorted(dirs, key=lambda i: i["name"]), + } diff --git a/app/imgserver/__init__.py b/app/api/imgserver.py similarity index 97% rename from app/imgserver/__init__.py rename to app/api/imgserver.py index 228ac360..ee582aee 100644 --- a/app/imgserver/__init__.py +++ b/app/api/imgserver.py @@ -1,7 +1,7 @@ import os from pathlib import Path -from flask import Blueprint, request, send_from_directory +from flask import Blueprint, send_from_directory imgbp = Blueprint("imgserver", __name__, url_prefix="/img") SUPPORTED_IMAGES = (".jpg", ".png", ".webp", ".jpeg") diff --git a/app/api/settings.py b/app/api/settings.py new file mode 100644 index 00000000..840d7154 --- /dev/null +++ b/app/api/settings.py @@ -0,0 +1,61 @@ +from flask import Blueprint, request +from app.db.sqlite.settings import SettingsSQLMethods as sdb + +settingsbp = Blueprint("settings", __name__, url_prefix="/") + + +@settingsbp.route("/settings/add-root-dirs", methods=["POST"]) +def add_root_dirs(): + """ + Add custom root directories to the database. + """ + msg = {"msg": "Failed! No directories were given."} + + data = request.get_json() + + if data is None: + return msg, 400 + + try: + new_dirs = data["new_dirs"] + removed_dirs = data["removed"] + except KeyError: + return msg, 400 + + sdb.add_root_dirs(new_dirs) + sdb.remove_root_dirs(removed_dirs) + + return {"msg": "Added root directories to the database."} + + +@settingsbp.route("/settings/get-root-dirs", methods=["GET"]) +def get_root_dirs(): + """ + Get custom root directories from the database. + """ + dirs = sdb.get_root_dirs() + + return {"dirs": dirs} + + +# CURRENTLY UNUSED ROUTE 👇 +@settingsbp.route("/settings/remove-root-dirs", methods=["POST"]) +def remove_root_dirs(): + """ + Remove custom root directories from the database. + """ + msg = {"msg": "Failed! No directories were given."} + + data = request.get_json() + + if data is None: + return msg, 400 + + try: + dirs = data["dirs"] + except KeyError: + return msg, 400 + + sdb.remove_root_dirs(dirs) + + return {"msg": "Removed root directories from the database."} diff --git a/app/db/sqlite/settings.py b/app/db/sqlite/settings.py index b31041a8..901aff10 100644 --- a/app/db/sqlite/settings.py +++ b/app/db/sqlite/settings.py @@ -7,44 +7,81 @@ class SettingsSQLMethods: Methods for interacting with the settings table. """ - @staticmethod - def update_root_dirs(dirs: list[str]): - """ - Updates custom root directories in the database. - """ - - sql = "UPDATE settings SET root_dirs = ?" - dirs_str = json.dumps(dirs) - - with SQLiteManager(userdata_db=True) as cur: - cur.execute(sql, (dirs_str,)) - @staticmethod def get_root_dirs() -> list[str]: """ Gets custom root directories from the database. """ - sql = "SELECT value FROM settings" + sql = "SELECT root_dirs FROM settings" with SQLiteManager(userdata_db=True) as cur: cur.execute(sql) + dirs = cur.fetchall() - data = cur.fetchone() - - if data is not None: - return json.loads(data[0]) - - return [] + return [dir[0] for dir in dirs] @staticmethod - def update_exclude_dirs(dirs: list[str]): + def add_root_dirs(dirs: list[str]): """ - Updates excluded directories in the database. + Add custom root directories to the database. """ - sql = "UPDATE settings SET exclude_dirs = ?" - dirs_str = json.dumps(dirs) + sql = "INSERT INTO settings (root_dirs) VALUES (?)" + existing_dirs = SettingsSQLMethods.get_root_dirs() + + dirs = [dir for dir in dirs if dir not in existing_dirs] + + if len(dirs) == 0: + return with SQLiteManager(userdata_db=True) as cur: - cur.execute(sql, (dirs_str,)) + for _dir in dirs: + cur.execute(sql, (_dir,)) + + @staticmethod + def remove_root_dirs(dirs: list[str]): + """ + Remove custom root directories from the database. + """ + + sql = "DELETE FROM settings WHERE root_dirs = ?" + + with SQLiteManager(userdata_db=True) as cur: + for _dir in dirs: + cur.execute(sql, (_dir,)) + + @staticmethod + def add_excluded_dirs(dirs: list[str]): + """ + Add custom exclude directories to the database. + """ + + sql = "INSERT INTO settings (exclude_dirs) VALUES (?)" + + with SQLiteManager(userdata_db=True) as cur: + cur.executemany(sql, dirs) + + @staticmethod + def remove_excluded_dirs(dirs: list[str]): + """ + Remove custom exclude directories from the database. + """ + + sql = "DELETE FROM settings WHERE exclude_dirs = ?" + + with SQLiteManager(userdata_db=True) as cur: + cur.executemany(sql, dirs) + + @staticmethod + def get_excluded_dirs() -> list[str]: + """ + Gets custom exclude directories from the database. + """ + + sql = "SELECT exclude_dirs FROM settings" + + with SQLiteManager(userdata_db=True) as cur: + cur.execute(sql) + dirs = cur.fetchall() + return [dir[0] for dir in dirs] diff --git a/app/db/sqlite/tracks.py b/app/db/sqlite/tracks.py index d83961c7..32abcabf 100644 --- a/app/db/sqlite/tracks.py +++ b/app/db/sqlite/tracks.py @@ -61,6 +61,8 @@ class SQLiteTrackMethods: ), ) + # TODO: rewrite the above code using an ordered dict and destructuring + @classmethod def insert_many_tracks(cls, tracks: list[dict]): """ diff --git a/app/lib/populate.py b/app/lib/populate.py index d5420750..7a6fce5a 100644 --- a/app/lib/populate.py +++ b/app/lib/populate.py @@ -3,6 +3,7 @@ from tqdm import tqdm from app import settings from app.db.sqlite.tracks import SQLiteTrackMethods +from app.db.sqlite.settings import SettingsSQLMethods as sdb from app.db.store import Store from app.lib.taglib import extract_thumb, get_tags @@ -27,7 +28,18 @@ class Populate: tracks = get_all_tracks() tracks = list(tracks) - files = run_fast_scandir(settings.HOME_DIR, full=True)[1] + dirs_to_scan = sdb.get_root_dirs() + + if len(dirs_to_scan) == 0: + log.error( + "The root directory is not set. No folders will be scanned for music files. Open the app in your web browser to configure." + ) + return + + files = [] + + for _dir in dirs_to_scan: + files.extend(run_fast_scandir(_dir, full=True)[1]) untagged = self.filter_untagged(tracks, files) diff --git a/app/logger.py b/app/logger.py index aec72636..9a82b2b1 100644 --- a/app/logger.py +++ b/app/logger.py @@ -18,7 +18,7 @@ class CustomFormatter(logging.Formatter): # format = ( # "%(asctime)s - %(name)s - %(levelname)s - %(message)s (%(filename)s:%(lineno)d)" # ) - format_ = "[%(asctime)s]@%(name)s • %(message)s" + format_ = "%(message)s" FORMATS = { logging.DEBUG: grey + format_ + reset, diff --git a/manage.py b/manage.py index 549081ad..d9eef369 100644 --- a/manage.py +++ b/manage.py @@ -186,6 +186,5 @@ if __name__ == "__main__": use_reloader=False, ) -# TODO: Find out how to print in color: red for errors, etc. # TODO: Find a way to verify the host string # TODO: Organize code in this file: move args to new file, etc. From bcc4873766b08a31eae1bf8b4fddcf84b9d22e2b Mon Sep 17 00:00:00 2001 From: geoffrey45 Date: Sun, 22 Jan 2023 23:57:12 +0300 Subject: [PATCH 03/34] handle XDG_CONFIG_HOME specification ... + fix bug that caused duplicate artist color entries to db + check if app is windows (prep for windows build) + remove caribou migrations lib + rename all api blueprints to "api" + unregister child directories when customizing root dirs + misc --- app/api/__init__.py | 31 ++++++++--------------- app/api/album.py | 9 +++---- app/api/artist.py | 10 ++++---- app/api/favorites.py | 17 +++++++------ app/api/folder.py | 16 ++++++------ app/api/imgserver.py | 21 ++++++++-------- app/api/playlist.py | 16 ++++++------ app/api/search.py | 12 ++++----- app/api/settings.py | 29 ++++++++++++++++------ app/api/track.py | 5 ++-- app/lib/colorlib.py | 9 +++---- app/migrations/__init__.py | 3 +++ app/settings.py | 51 ++++++++++++++++++++++---------------- app/setup/__init__.py | 20 +-------------- app/utils.py | 35 +++++++++++++++++--------- manage.py | 11 +++++--- poetry.lock | 46 ++++++++-------------------------- pyproject.toml | 1 - 18 files changed, 163 insertions(+), 179 deletions(-) create mode 100644 app/migrations/__init__.py diff --git a/app/api/__init__.py b/app/api/__init__.py index 3d662b28..8fb2011a 100644 --- a/app/api/__init__.py +++ b/app/api/__init__.py @@ -5,17 +5,8 @@ This module combines all API blueprints into a single Flask app instance. from flask import Flask from flask_cors import CORS -from app.api import ( - album, - artist, - favorites, - folder, - playlist, - search, - track, - settings, - imgserver, -) +from app.api import (album, artist, favorites, folder, imgserver, playlist, + search, settings, track) def create_api(): @@ -27,14 +18,14 @@ def create_api(): with app.app_context(): - app.register_blueprint(album.albumbp) - app.register_blueprint(artist.artistbp) - app.register_blueprint(track.trackbp) - app.register_blueprint(search.searchbp) - app.register_blueprint(folder.folderbp) - app.register_blueprint(playlist.playlistbp) - app.register_blueprint(favorites.favbp) - app.register_blueprint(imgserver.imgbp) - app.register_blueprint(settings.settingsbp) + app.register_blueprint(album.api) + app.register_blueprint(artist.api) + app.register_blueprint(track.api) + app.register_blueprint(search.api) + app.register_blueprint(folder.api) + app.register_blueprint(playlist.api) + app.register_blueprint(favorites.api) + app.register_blueprint(imgserver.api) + app.register_blueprint(settings.api) return app diff --git a/app/api/album.py b/app/api/album.py index 46ca8174..99e3bb51 100644 --- a/app/api/album.py +++ b/app/api/album.py @@ -12,15 +12,14 @@ from app.db.sqlite.favorite import SQLiteFavoriteMethods as favdb from app.db.store import Store from app.models import FavType, Track - get_album_by_id = adb.get_album_by_id get_albums_by_albumartist = adb.get_albums_by_albumartist check_is_fav = favdb.check_is_favorite -albumbp = Blueprint("album", __name__, url_prefix="") +api = Blueprint("album", __name__, url_prefix="") -@albumbp.route("/album", methods=["POST"]) +@api.route("/album", methods=["POST"]) def get_album(): """Returns all the tracks in the given album.""" @@ -88,7 +87,7 @@ def get_album(): return {"tracks": tracks, "info": album} -@albumbp.route("/album//tracks", methods=["GET"]) +@api.route("/album//tracks", methods=["GET"]) def get_album_tracks(albumhash: str): """ Returns all the tracks in the given album. @@ -105,7 +104,7 @@ def get_album_tracks(albumhash: str): return {"tracks": tracks} -@albumbp.route("/album/from-artist", methods=["POST"]) +@api.route("/album/from-artist", methods=["POST"]) def get_artist_albums(): data = request.get_json() diff --git a/app/api/artist.py b/app/api/artist.py index 19c664c3..f03e2967 100644 --- a/app/api/artist.py +++ b/app/api/artist.py @@ -5,12 +5,12 @@ from collections import deque from flask import Blueprint, request +from app.db.sqlite.favorite import SQLiteFavoriteMethods as favdb from app.db.store import Store from app.models import Album, FavType, Track from app.utils import remove_duplicates -from app.db.sqlite.favorite import SQLiteFavoriteMethods as favdb -artistbp = Blueprint("artist", __name__, url_prefix="/") +api = Blueprint("artist", __name__, url_prefix="/") class CacheEntry: @@ -156,7 +156,7 @@ def add_albums_to_cache(artisthash: str): # ======================================================= -@artistbp.route("/artist/", methods=["GET"]) +@api.route("/artist/", methods=["GET"]) def get_artist(artisthash: str): """ Get artist data. @@ -203,7 +203,7 @@ def get_artist(artisthash: str): return {"artist": artist, "tracks": tracks[:limit]} -@artistbp.route("/artist//albums", methods=["GET"]) +@api.route("/artist//albums", methods=["GET"]) def get_artist_albums(artisthash: str): limit = request.args.get("limit") @@ -261,7 +261,7 @@ def get_artist_albums(artisthash: str): } -@artistbp.route("/artist//tracks", methods=["GET"]) +@api.route("/artist//tracks", methods=["GET"]) def get_artist_tracks(artisthash: str): """ Returns all artists by a given artist. diff --git a/app/api/favorites.py b/app/api/favorites.py index 8ab70872..b8fbfb92 100644 --- a/app/api/favorites.py +++ b/app/api/favorites.py @@ -1,17 +1,18 @@ from flask import Blueprint, request + from app.db.sqlite.favorite import SQLiteFavoriteMethods as favdb from app.db.store import Store from app.models import FavType from app.utils import UseBisection -favbp = Blueprint("favorite", __name__, url_prefix="/") +api = Blueprint("favorite", __name__, url_prefix="/") def remove_none(items: list): return [i for i in items if i is not None] -@favbp.route("/favorite/add", methods=["POST"]) +@api.route("/favorite/add", methods=["POST"]) def add_favorite(): """ Adds a favorite to the database. @@ -32,7 +33,7 @@ def add_favorite(): return {"msg": "Added to favorites"} -@favbp.route("/favorite/remove", methods=["POST"]) +@api.route("/favorite/remove", methods=["POST"]) def remove_favorite(): """ Removes a favorite from the database. @@ -53,7 +54,7 @@ def remove_favorite(): return {"msg": "Removed from favorites"} -@favbp.route("/albums/favorite") +@api.route("/albums/favorite") def get_favorite_albums(): limit = request.args.get("limit") @@ -77,7 +78,7 @@ def get_favorite_albums(): return {"albums": fav_albums[:limit]} -@favbp.route("/tracks/favorite") +@api.route("/tracks/favorite") def get_favorite_tracks(): limit = request.args.get("limit") @@ -100,7 +101,7 @@ def get_favorite_tracks(): return {"tracks": tracks[:limit]} -@favbp.route("/artists/favorite") +@api.route("/artists/favorite") def get_favorite_artists(): limit = request.args.get("limit") @@ -124,7 +125,7 @@ def get_favorite_artists(): return {"artists": artists[:limit]} -@favbp.route("/favorites") +@api.route("/favorites") def get_all_favorites(): """ Returns all the favorites in the database. @@ -191,7 +192,7 @@ def get_all_favorites(): } -@favbp.route("/favorites/check") +@api.route("/favorites/check") def check_favorite(): """ Checks if a favorite exists in the database. diff --git a/app/api/folder.py b/app/api/folder.py index 1cdcb057..28547f7a 100644 --- a/app/api/folder.py +++ b/app/api/folder.py @@ -2,16 +2,16 @@ Contains all the folder routes. """ import os + from flask import Blueprint, request from app import settings from app.lib.folderslib import GetFilesAndDirs - -folderbp = Blueprint("folder", __name__, url_prefix="/") +api = Blueprint("folder", __name__, url_prefix="/") -@folderbp.route("/folder", methods=["POST"]) +@api.route("/folder", methods=["POST"]) def get_folder_tree(): """ Returns a list of all the folders and tracks in the given folder. @@ -21,10 +21,10 @@ def get_folder_tree(): if data is not None: req_dir: str = data["folder"] else: - req_dir = settings.HOME_DIR + req_dir = settings.USER_HOME_DIR if req_dir == "$home": - req_dir = settings.HOME_DIR + req_dir = settings.USER_HOME_DIR tracks, folders = GetFilesAndDirs(req_dir)() @@ -34,7 +34,7 @@ def get_folder_tree(): } -@folderbp.route("/folder/dir-browser", methods=["POST"]) +@api.route("/folder/dir-browser", methods=["POST"]) def list_folders(): """ Returns a list of all the folders in the given folder. @@ -44,10 +44,10 @@ def list_folders(): try: req_dir: str = data["folder"] except KeyError: - req_dir = settings.HOME_DIR + req_dir = settings.USER_HOME_DIR if req_dir == "$home": - req_dir = settings.HOME_DIR + req_dir = settings.USER_HOME_DIR entries = os.scandir(req_dir) diff --git a/app/api/imgserver.py b/app/api/imgserver.py index ee582aee..621f31be 100644 --- a/app/api/imgserver.py +++ b/app/api/imgserver.py @@ -1,14 +1,13 @@ -import os from pathlib import Path from flask import Blueprint, send_from_directory -imgbp = Blueprint("imgserver", __name__, url_prefix="/img") +from app.settings import APP_DIR + +api = Blueprint("imgserver", __name__, url_prefix="/img") SUPPORTED_IMAGES = (".jpg", ".png", ".webp", ".jpeg") -HOME = os.path.expanduser("~") - -APP_DIR = Path(HOME) / ".swing" +APP_DIR = Path(APP_DIR) IMG_PATH = APP_DIR / "images" ASSETS_PATH = APP_DIR / "assets" @@ -23,7 +22,7 @@ ARTIST_SM_PATH = ARTIST_PATH / "small" PLAYLIST_PATH = IMG_PATH / "playlists" -@imgbp.route("/") +@api.route("/") def hello(): return "

Image Server

" @@ -37,7 +36,7 @@ def send_fallback_img(filename: str = "default.webp"): return send_from_directory(ASSETS_PATH, filename) -@imgbp.route("/t/") +@api.route("/t/") def send_lg_thumbnail(imgpath: str): fpath = LG_THUMB_PATH / imgpath @@ -47,7 +46,7 @@ def send_lg_thumbnail(imgpath: str): return send_fallback_img() -@imgbp.route("/t/s/") +@api.route("/t/s/") def send_sm_thumbnail(imgpath: str): fpath = SM_THUMB_PATH / imgpath @@ -57,7 +56,7 @@ def send_sm_thumbnail(imgpath: str): return send_fallback_img() -@imgbp.route("/a/") +@api.route("/a/") def send_lg_artist_image(imgpath: str): fpath = ARTIST_LG_PATH / imgpath @@ -67,7 +66,7 @@ def send_lg_artist_image(imgpath: str): return send_fallback_img("artist.webp") -@imgbp.route("/a/s/") +@api.route("/a/s/") def send_sm_artist_image(imgpath: str): fpath = ARTIST_SM_PATH / imgpath @@ -77,7 +76,7 @@ def send_sm_artist_image(imgpath: str): return send_fallback_img("artist.webp") -@imgbp.route("/p/") +@api.route("/p/") def send_playlist_image(imgpath: str): fpath = PLAYLIST_PATH / imgpath diff --git a/app/api/playlist.py b/app/api/playlist.py index cd1d5343..4686378a 100644 --- a/app/api/playlist.py +++ b/app/api/playlist.py @@ -13,7 +13,7 @@ from app.db.store import Store from app.lib import playlistlib from app.utils import create_new_date, remove_duplicates -playlistbp = Blueprint("playlist", __name__, url_prefix="/") +api = Blueprint("playlist", __name__, url_prefix="/") PL = SQLitePlaylistMethods @@ -30,7 +30,7 @@ delete_playlist = PL.delete_playlist # get_tracks_by_trackhashes = SQLiteTrackMethods.get_tracks_by_trackhashes -@playlistbp.route("/playlists", methods=["GET"]) +@api.route("/playlists", methods=["GET"]) def send_all_playlists(): """ Gets all the playlists. @@ -46,7 +46,7 @@ def send_all_playlists(): return {"data": playlists} -@playlistbp.route("/playlist/new", methods=["POST"]) +@api.route("/playlist/new", methods=["POST"]) def create_playlist(): """ Creates a new playlist. Accepts POST method with a JSON body. @@ -79,7 +79,7 @@ def create_playlist(): return {"playlist": playlist}, 201 -@playlistbp.route("/playlist//add", methods=["POST"]) +@api.route("/playlist//add", methods=["POST"]) def add_track_to_playlist(playlist_id: str): """ Takes a playlist ID and a track hash, and adds the track to the playlist @@ -102,7 +102,7 @@ def add_track_to_playlist(playlist_id: str): return {"msg": "Done"}, 200 -@playlistbp.route("/playlist/") +@api.route("/playlist/") def get_playlist(playlistid: str): """ Gets a playlist by id, and if it exists, it gets all the tracks in the playlist and returns them. @@ -123,7 +123,7 @@ def get_playlist(playlistid: str): return {"info": playlist, "tracks": tracks} -@playlistbp.route("/playlist//update", methods=["PUT"]) +@api.route("/playlist//update", methods=["PUT"]) def update_playlist_info(playlistid: str): if playlistid is None: return {"error": "Playlist ID not provided"}, 400 @@ -188,7 +188,7 @@ def update_playlist_info(playlistid: str): # return {"data": artists} -@playlistbp.route("/playlist/delete", methods=["POST"]) +@api.route("/playlist/delete", methods=["POST"]) def remove_playlist(): """ Deletes a playlist by ID. @@ -209,7 +209,7 @@ def remove_playlist(): return {"msg": "Done"}, 200 -@playlistbp.route("/playlist//set-image-pos", methods=["POST"]) +@api.route("/playlist//set-image-pos", methods=["POST"]) def update_image_position(pid: int): data = request.get_json() message = {"msg": "No data provided"} diff --git a/app/api/search.py b/app/api/search.py index 92463d13..afd35156 100644 --- a/app/api/search.py +++ b/app/api/search.py @@ -8,7 +8,7 @@ from app import models, utils from app.db.store import Store from app.lib import searchlib -searchbp = Blueprint("search", __name__, url_prefix="/") +api = Blueprint("search", __name__, url_prefix="/") SEARCH_COUNT = 12 @@ -95,7 +95,7 @@ class DoSearch: # self.search_playlists() -@searchbp.route("/search/tracks", methods=["GET"]) +@api.route("/search/tracks", methods=["GET"]) def search_tracks(): """ Searches for tracks that match the search query. @@ -113,7 +113,7 @@ def search_tracks(): } -@searchbp.route("/search/albums", methods=["GET"]) +@api.route("/search/albums", methods=["GET"]) def search_albums(): """ Searches for albums. @@ -131,7 +131,7 @@ def search_albums(): } -@searchbp.route("/search/artists", methods=["GET"]) +@api.route("/search/artists", methods=["GET"]) def search_artists(): """ Searches for artists. @@ -167,7 +167,7 @@ def search_artists(): # } -@searchbp.route("/search/top", methods=["GET"]) +@api.route("/search/top", methods=["GET"]) def get_top_results(): """ Returns the top results for the search query. @@ -188,7 +188,7 @@ def get_top_results(): } -@searchbp.route("/search/loadmore") +@api.route("/search/loadmore") def search_load_more(): """ Returns more songs, albums or artists from a search query. diff --git a/app/api/settings.py b/app/api/settings.py index 840d7154..45cbebf4 100644 --- a/app/api/settings.py +++ b/app/api/settings.py @@ -1,10 +1,17 @@ from flask import Blueprint, request + from app.db.sqlite.settings import SettingsSQLMethods as sdb -settingsbp = Blueprint("settings", __name__, url_prefix="/") +api = Blueprint("settings", __name__, url_prefix="/") -@settingsbp.route("/settings/add-root-dirs", methods=["POST"]) +def get_child_dirs(parent: str, children: list[str]): + """Returns child directories in a list, given a parent directory""" + + return [dir for dir in children if dir.startswith(parent)] + + +@api.route("/settings/add-root-dirs", methods=["POST"]) def add_root_dirs(): """ Add custom root directories to the database. @@ -17,18 +24,26 @@ def add_root_dirs(): return msg, 400 try: - new_dirs = data["new_dirs"] - removed_dirs = data["removed"] + new_dirs: list[str] = data["new_dirs"] + removed_dirs: list[str] = data["removed"] except KeyError: return msg, 400 + # --- Unregister child directories --- + db_dirs = sdb.get_root_dirs() + + for _dir in new_dirs: + children = get_child_dirs(_dir, db_dirs) + removed_dirs.extend(children) + # ------------------------------------ + sdb.add_root_dirs(new_dirs) sdb.remove_root_dirs(removed_dirs) - return {"msg": "Added root directories to the database."} + return {"msg": "Updated!"} -@settingsbp.route("/settings/get-root-dirs", methods=["GET"]) +@api.route("/settings/get-root-dirs", methods=["GET"]) def get_root_dirs(): """ Get custom root directories from the database. @@ -39,7 +54,7 @@ def get_root_dirs(): # CURRENTLY UNUSED ROUTE 👇 -@settingsbp.route("/settings/remove-root-dirs", methods=["POST"]) +@api.route("/settings/remove-root-dirs", methods=["POST"]) def remove_root_dirs(): """ Remove custom root directories from the database. diff --git a/app/api/track.py b/app/api/track.py index 9262cfe5..a7eb33bc 100644 --- a/app/api/track.py +++ b/app/api/track.py @@ -2,12 +2,13 @@ Contains all the track routes. """ from flask import Blueprint, send_file + from app.db.store import Store -trackbp = Blueprint("track", __name__, url_prefix="/") +api = Blueprint("track", __name__, url_prefix="/") -@trackbp.route("/file/") +@api.route("/file/") def send_track_file(trackhash: str): """ Returns an audio file that matches the passed id to the client. diff --git a/app/lib/colorlib.py b/app/lib/colorlib.py index 1e6dba9e..43241aea 100644 --- a/app/lib/colorlib.py +++ b/app/lib/colorlib.py @@ -69,13 +69,12 @@ class ProcessArtistColors: """ def __init__(self) -> None: + db_colors: list[tuple] = list(adb.get_all_artists()) + db_artisthashes = "-".join([artist[1] for artist in db_colors]) all_artists = Store.artists - if all_artists is None: - return - for artist in tqdm(all_artists, desc="Processing artist colors"): - if len(artist.colors) == 0: + if artist.artisthash not in db_artisthashes: self.process_color(artist) @staticmethod @@ -90,5 +89,3 @@ class ProcessArtistColors: if len(colors) > 0: adb.insert_one_artist(artisthash=artist.artisthash, colors=colors) Store.map_artist_color((0, artist.artisthash, json.dumps(colors))) - - # TODO: Load album and artist colors into the store. diff --git a/app/migrations/__init__.py b/app/migrations/__init__.py new file mode 100644 index 00000000..6048a197 --- /dev/null +++ b/app/migrations/__init__.py @@ -0,0 +1,3 @@ +""" +Migrations module +""" diff --git a/app/settings.py b/app/settings.py index 4481198a..1fd3e0d2 100644 --- a/app/settings.py +++ b/app/settings.py @@ -1,16 +1,42 @@ """ Contains default configs """ -import multiprocessing import os + +# ------- HELPER METHODS -------- +def get_xdg_config_dir(): + """ + Returns the XDG_CONFIG_HOME environment variable if it exists, otherwise + returns the default config directory. If none of those exist, returns the + user's home directory. + """ + xdg_config_home = os.environ.get("XDG_CONFIG_HOME") + + if xdg_config_home: + return xdg_config_home + + try: + alt_dir = os.path.join(os.environ.get("HOME"), ".config") + + if os.path.exists(alt_dir): + return alt_dir + except TypeError: + return os.path.expanduser("~") + + +# ------- HELPER METHODS -------- + + APP_VERSION = "Swing v.1.0.0.beta.1" # paths -CONFIG_FOLDER = ".swing" -HOME_DIR = os.path.expanduser("~") +XDG_CONFIG_DIR = get_xdg_config_dir() +USER_HOME_DIR = os.path.expanduser("~") -APP_DIR = os.path.join(HOME_DIR, CONFIG_FOLDER) +CONFIG_FOLDER = "swing" if XDG_CONFIG_DIR != USER_HOME_DIR else ".swing" + +APP_DIR = os.path.join(XDG_CONFIG_DIR, CONFIG_FOLDER) IMG_PATH = os.path.join(APP_DIR, "images") ARTIST_IMG_PATH = os.path.join(IMG_PATH, "artists") @@ -37,8 +63,6 @@ DEFAULT_ARTIST_IMG = IMG_ARTIST_URI + "0.webp" LAST_FM_API_KEY = "762db7a44a9e6fb5585661f5f2bdf23a" -CPU_COUNT = multiprocessing.cpu_count() - THUMB_SIZE = 400 SM_THUMB_SIZE = 64 SM_ARTIST_IMG_SIZE = 64 @@ -46,24 +70,9 @@ SM_ARTIST_IMG_SIZE = 64 The size of extracted images in pixels """ -LOGGER_ENABLE: bool = True - FILES = ["flac", "mp3", "wav", "m4a"] SUPPORTED_FILES = tuple(f".{file}" for file in FILES) -SUPPORTED_IMAGES = (".jpg", ".png", ".webp", ".jpeg") - -SUPPORTED_DIR_IMAGES = [ - "folder", - "cover", - "album", - "front", -] - -# ===== DB ========= -USE_MONGO = False - - # ===== SQLite ===== APP_DB_NAME = "swing.db" USER_DATA_DB_NAME = "userdata.db" diff --git a/app/setup/__init__.py b/app/setup/__init__.py index 5ad9cbf7..b31a87a6 100644 --- a/app/setup/__init__.py +++ b/app/setup/__init__.py @@ -4,7 +4,6 @@ Contains the functions to prepare the server for use. import os import shutil from configparser import ConfigParser -import caribou # pylint: disable=import-error from app import settings from app.db.sqlite import create_connection, create_tables, queries @@ -64,10 +63,6 @@ def create_config_dir() -> None: """ Creates the config directory if it doesn't exist. """ - - home_dir = os.path.expanduser("~") - config_folder = os.path.join(home_dir, settings.CONFIG_FOLDER) - 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") @@ -91,7 +86,7 @@ def create_config_dir() -> None: ] for _dir in dirs: - path = os.path.join(config_folder, _dir) + path = os.path.join(settings.APP_DIR, _dir) exists = os.path.exists(path) if not exists: @@ -114,19 +109,6 @@ def setup_sqlite(): create_tables(app_db_conn, queries.CREATE_APPDB_TABLES) create_tables(playlist_db_conn, queries.CREATE_USERDATA_TABLES) - userdb_migrations = get_home_res_path("app") / "migrations" / "userdata" - maindb_migrations = get_home_res_path("app") / "migrations" / "main" - - caribou.upgrade( - APP_DB_PATH, - maindb_migrations, - ) - - caribou.upgrade( - str(USERDATA_DB_PATH), - str(userdb_migrations), - ) - app_db_conn.close() playlist_db_conn.close() diff --git a/app/utils.py b/app/utils.py index bf6a4e9f..76672866 100644 --- a/app/utils.py +++ b/app/utils.py @@ -5,6 +5,7 @@ from pathlib import Path from datetime import datetime import os +import platform import socket as Socket import hashlib import threading @@ -38,19 +39,22 @@ def run_fast_scandir(__dir: str, full=False) -> tuple[list[str], list[str]]: subfolders = [] files = [] - for f in os.scandir(__dir): - if f.is_dir() and not f.name.startswith("."): - subfolders.append(f.path) - if f.is_file(): - ext = os.path.splitext(f.name)[1].lower() - if ext in SUPPORTED_FILES: - files.append(f.path) + try: + for _files in os.scandir(__dir): + if _files.is_dir() and not _files.name.startswith("."): + subfolders.append(_files.path) + if _files.is_file(): + ext = os.path.splitext(_files.name)[1].lower() + if ext in SUPPORTED_FILES: + files.append(_files.path) - if full or len(files) == 0: - for _dir in list(subfolders): - sf, f = run_fast_scandir(_dir, full=True) - subfolders.extend(sf) - files.extend(f) + if full or len(files) == 0: + for _dir in list(subfolders): + sub_dirs, _files = run_fast_scandir(_dir, full=True) + subfolders.extend(sub_dirs) + files.extend(_files) + except PermissionError: + return [], [] return subfolders, files @@ -239,3 +243,10 @@ def get_ip(): soc.close() return ip_address + + +def is_windows(): + """ + Returns True if the OS is Windows. + """ + return platform.system() == "Windows" diff --git a/manage.py b/manage.py index d9eef369..03d9849e 100644 --- a/manage.py +++ b/manage.py @@ -13,7 +13,7 @@ from app.functions import run_periodic_checks from app.lib.watchdogg import Watcher as WatchDog from app.settings import APP_VERSION, HELP_MESSAGE, TCOLOR from app.setup import run_setup -from app.utils import background, get_home_res_path, get_ip +from app.utils import background, get_home_res_path, get_ip, is_windows werkzeug = logging.getLogger("werkzeug") werkzeug.setLevel(logging.ERROR) @@ -80,6 +80,8 @@ class HandleArgs: config["DEFAULT"]["BUILD"] = "True" config.write(file) + _s = ";" if is_windows() else ":" + bundler.run( [ "manage.py", @@ -87,9 +89,10 @@ class HandleArgs: "--name", "swingmusic", "--clean", - "--add-data=assets:assets", - "--add-data=client:client", - "--add-data=pyinstaller.config.ini:.", + f"--add-data=assets{_s}assets", + f"--add-data=client{_s}client", + f"--add-data=app/migrations{_s}app/migrations", + f"--add-data=pyinstaller.config.ini{_s}.", "-y", ] ) diff --git a/poetry.lock b/poetry.lock index 2d205cff..963b7a27 100644 --- a/poetry.lock +++ b/poetry.lock @@ -12,23 +12,11 @@ files = [ {file = "altgraph-0.17.3.tar.gz", hash = "sha256:ad33358114df7c9416cdb8fa1eaa5852166c505118717021c6a8c7c7abbd03dd"}, ] -[[package]] -name = "argparse" -version = "1.4.0" -description = "Python command-line parsing library" -category = "main" -optional = false -python-versions = "*" -files = [ - {file = "argparse-1.4.0-py2.py3-none-any.whl", hash = "sha256:c31647edb69fd3d465a847ea3157d37bed1f95f19760b11a47aa91c04b666314"}, - {file = "argparse-1.4.0.tar.gz", hash = "sha256:62b089a55be1d8949cd2bc7e0df0bddb9e028faefc8c32038cc84862aefdd6e4"}, -] - [[package]] name = "astroid" version = "2.12.12" description = "An abstract syntax tree for Python with inference support." -category = "main" +category = "dev" optional = false python-versions = ">=3.7.2" files = [ @@ -107,20 +95,6 @@ d = ["aiohttp (>=3.7.4)"] jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] uvloop = ["uvloop (>=0.15.2)"] -[[package]] -name = "caribou" -version = "0.3" -description = "python migrations for sqlite databases" -category = "main" -optional = false -python-versions = "*" -files = [ - {file = "caribou-0.3.0.tar.gz", hash = "sha256:5ca6e6e6ad7d3175137c68d809e203fbd931f79163a5613808a789449fef7863"}, -] - -[package.dependencies] -argparse = ">=1.0.0" - [[package]] name = "certifi" version = "2022.5.18.1" @@ -194,7 +168,7 @@ pillow = ">=3.3.1" name = "dill" version = "0.3.6" description = "serialize all of python" -category = "main" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -351,7 +325,7 @@ files = [ name = "isort" version = "5.10.1" description = "A Python utility / library to sort Python imports." -category = "main" +category = "dev" optional = false python-versions = ">=3.6.1,<4.0" files = [ @@ -399,7 +373,7 @@ i18n = ["Babel (>=2.7)"] name = "lazy-object-proxy" version = "1.8.0" description = "A fast and thorough lazy object proxy." -category = "main" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -493,7 +467,7 @@ files = [ name = "mccabe" version = "0.7.0" description = "McCabe checker, plugin for flake8" -category = "main" +category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -630,7 +604,7 @@ tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "pa name = "platformdirs" version = "2.5.2" description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." -category = "main" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -720,7 +694,7 @@ files = [ name = "pylint" version = "2.15.5" description = "python code static checker" -category = "main" +category = "dev" optional = false python-versions = ">=3.7.2" files = [ @@ -988,7 +962,7 @@ files = [ name = "tomlkit" version = "0.11.6" description = "Style preserving TOML library" -category = "main" +category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -1106,7 +1080,7 @@ watchdog = ["watchdog"] name = "wrapt" version = "1.14.1" description = "Module for decorators, wrappers and monkey patching." -category = "main" +category = "dev" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" files = [ @@ -1179,4 +1153,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = ">=3.10,<3.12" -content-hash = "83ce6d95a8e6bdefff93a3badca9e7b7b53174eaea85f908e9cea9a37afaec7c" +content-hash = "6f6126fbaa8f114c90cef637e0bc996d63d3dc93f6ed2e90e36ef696cdd0cfc5" diff --git a/pyproject.toml b/pyproject.toml index fde02919..e19ec185 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,7 +20,6 @@ hypothesis = "^6.56.3" pytest = "^7.1.3" Unidecode = "^1.3.6" pyinstaller = "^5.7.0" -caribou = "^0.3.0" [tool.poetry.dev-dependencies] pylint = "^2.15.5" From d676459b9a74a8ad103637b2bffa016a738dea60 Mon Sep 17 00:00:00 2001 From: geoffrey45 Date: Mon, 23 Jan 2023 10:19:21 +0300 Subject: [PATCH 04/34] feat: if no root dir is set, try ~/Music if there's music in there, add it as a root dir --- app/api/settings.py | 23 ----------------------- app/lib/populate.py | 43 +++++++++++++++++++++++++++++++++++++++---- app/settings.py | 1 + 3 files changed, 40 insertions(+), 27 deletions(-) diff --git a/app/api/settings.py b/app/api/settings.py index 45cbebf4..2f065a5f 100644 --- a/app/api/settings.py +++ b/app/api/settings.py @@ -51,26 +51,3 @@ def get_root_dirs(): dirs = sdb.get_root_dirs() return {"dirs": dirs} - - -# CURRENTLY UNUSED ROUTE 👇 -@api.route("/settings/remove-root-dirs", methods=["POST"]) -def remove_root_dirs(): - """ - Remove custom root directories from the database. - """ - msg = {"msg": "Failed! No directories were given."} - - data = request.get_json() - - if data is None: - return msg, 400 - - try: - dirs = data["dirs"] - except KeyError: - return msg, 400 - - sdb.remove_root_dirs(dirs) - - return {"msg": "Removed root directories from the database."} diff --git a/app/lib/populate.py b/app/lib/populate.py index 7a6fce5a..ef1d99ef 100644 --- a/app/lib/populate.py +++ b/app/lib/populate.py @@ -1,4 +1,5 @@ from concurrent.futures import ThreadPoolExecutor +import os from tqdm import tqdm from app import settings @@ -24,17 +25,31 @@ class Populate: """ def __init__(self) -> None: + messages = { + "root_unset": "The root directory is not set. Trying to scan the default directory: %s", + "default_not_exists": "The directory: %s does not exist. Please open the app in your web browser to set the root directory.", + "no_tracks": "No tracks found in: %s. Please open the app in your web browser to set the root directory.", + } tracks = get_all_tracks() tracks = list(tracks) dirs_to_scan = sdb.get_root_dirs() + initial_dirs_count = len(dirs_to_scan) + + def_dir = "~/Music" if len(dirs_to_scan) == 0: - log.error( - "The root directory is not set. No folders will be scanned for music files. Open the app in your web browser to configure." - ) - return + log.warning(messages["root_unset"], def_dir) + print("...") + + exists = os.path.exists(settings.MUSIC_DIR) + + if not exists: + log.warning(messages["default_not_exists"], def_dir) + return + + dirs_to_scan = [settings.MUSIC_DIR] files = [] @@ -43,6 +58,26 @@ class Populate: untagged = self.filter_untagged(tracks, files) + if initial_dirs_count == 0 and len(untagged) == 0: + log.warning(messages["no_tracks"], def_dir) + return + + if initial_dirs_count == 0 and len(untagged) > 0: + log.info( + "%sFound %s tracks 💪 %s", + settings.TCOLOR.OKGREEN, + len(untagged), + settings.TCOLOR.ENDC, + ) + log.info( + "%s%s saved as the default root directory. 😶%s", + settings.TCOLOR.OKGREEN, + def_dir, + settings.TCOLOR.ENDC, + ) + sdb.add_root_dirs(dirs_to_scan) + return + if len(untagged) == 0: log.info("All clear, no unread files.") return diff --git a/app/settings.py b/app/settings.py index 1fd3e0d2..529332fa 100644 --- a/app/settings.py +++ b/app/settings.py @@ -46,6 +46,7 @@ ARTIST_IMG_LG_PATH = os.path.join(ARTIST_IMG_PATH, "large") THUMBS_PATH = os.path.join(IMG_PATH, "thumbnails") SM_THUMB_PATH = os.path.join(THUMBS_PATH, "small") LG_THUMBS_PATH = os.path.join(THUMBS_PATH, "large") +MUSIC_DIR = os.path.join(USER_HOME_DIR, "Music") # TEST_DIR = "/home/cwilvx/Downloads/Telegram Desktop" From 22fa3eef4011d4c79a91068c9f3a597b3898cfaa Mon Sep 17 00:00:00 2001 From: geoffrey45 Date: Mon, 23 Jan 2023 17:10:05 +0300 Subject: [PATCH 05/34] handle watching ~/ dir + fix bug that caused duplicate album colors in db --- app/api/folder.py | 30 +++++++++++++++++++--- app/api/settings.py | 57 +++++++++++++++++++++++++++++++++++++---- app/db/sqlite/tracks.py | 16 ++++-------- app/db/store.py | 22 +++++++++++++--- app/lib/colorlib.py | 10 +++++--- app/lib/folderslib.py | 1 - app/lib/populate.py | 16 ++++++++---- 7 files changed, 121 insertions(+), 31 deletions(-) diff --git a/app/api/folder.py b/app/api/folder.py index 28547f7a..e1de1f79 100644 --- a/app/api/folder.py +++ b/app/api/folder.py @@ -3,10 +3,14 @@ Contains all the folder routes. """ import os +from pathlib import Path from flask import Blueprint, request from app import settings from app.lib.folderslib import GetFilesAndDirs +from app.db.sqlite.settings import SettingsSQLMethods as db +from app.models import Folder +from app.utils import create_folder_hash api = Blueprint("folder", __name__, url_prefix="/") @@ -19,12 +23,32 @@ def get_folder_tree(): data = request.get_json() if data is not None: - req_dir: str = data["folder"] - else: + try: + req_dir: str = data["folder"] + except KeyError: + req_dir = "$home" + + root_dirs = db.get_root_dirs() + + if req_dir == "$home" and root_dirs[0] == "$home": req_dir = settings.USER_HOME_DIR if req_dir == "$home": - req_dir = settings.USER_HOME_DIR + folders = [Path(f) for f in root_dirs] + + return { + "folders": [ + Folder( + name=f.name, + path=str(f), + has_tracks=True, + is_sym=f.is_symlink(), + path_hash=create_folder_hash(*f.parts[1:]), + ) + for f in folders + ], + "tracks": [], + } tracks, folders = GetFilesAndDirs(req_dir)() diff --git a/app/api/settings.py b/app/api/settings.py index 2f065a5f..358fc4e0 100644 --- a/app/api/settings.py +++ b/app/api/settings.py @@ -1,6 +1,12 @@ from flask import Blueprint, request +from app import settings from app.db.sqlite.settings import SettingsSQLMethods as sdb +from app.lib import populate +from app.logger import log + +from app.db.store import Store +from app.utils import background api = Blueprint("settings", __name__, url_prefix="/") @@ -8,7 +14,22 @@ api = Blueprint("settings", __name__, url_prefix="/") def get_child_dirs(parent: str, children: list[str]): """Returns child directories in a list, given a parent directory""" - return [dir for dir in children if dir.startswith(parent)] + return [dir for dir in children if dir.startswith(parent) and dir != parent] + + +@background +def rebuild_store(db_dirs: list[str]): + log.info("Rebuilding library...") + Store.remove_tracks_by_dir_except(db_dirs) + + Store.load_all_tracks() + Store.process_folders() + Store.load_albums() + Store.load_artists() + + populate.Populate() + + log.info("Rebuilding library... ✅") @api.route("/settings/add-root-dirs", methods=["POST"]) @@ -29,16 +50,42 @@ def add_root_dirs(): except KeyError: return msg, 400 - # --- Unregister child directories --- + def finalize(new_dirs: list[str], removed_dirs: list[str], db_dirs: list[str]): + sdb.remove_root_dirs(removed_dirs) + sdb.add_root_dirs(new_dirs) + rebuild_store(db_dirs) + + # --- db_dirs = sdb.get_root_dirs() + if db_dirs[0] == "$home" and new_dirs[0] == "$home".strip(): + return {"msg": "Not changed!"} + + if db_dirs[0] == "$home": + sdb.remove_root_dirs(db_dirs) + + try: + if new_dirs[0] == "$home": + finalize(["$home"], db_dirs, [settings.USER_HOME_DIR]) + + return {"msg": "Updated!"} + except IndexError: + pass + for _dir in new_dirs: children = get_child_dirs(_dir, db_dirs) removed_dirs.extend(children) - # ------------------------------------ + # --- - sdb.add_root_dirs(new_dirs) - sdb.remove_root_dirs(removed_dirs) + for _dir in removed_dirs: + try: + db_dirs.remove(_dir) + except ValueError: + pass + + db_dirs.extend(new_dirs) + + finalize(new_dirs, removed_dirs, db_dirs) return {"msg": "Updated!"} diff --git a/app/db/sqlite/tracks.py b/app/db/sqlite/tracks.py index 32abcabf..ea1b7607 100644 --- a/app/db/sqlite/tracks.py +++ b/app/db/sqlite/tracks.py @@ -130,15 +130,9 @@ class SQLiteTrackMethods: cur.execute("DELETE FROM tracks WHERE filepath=?", (filepath,)) @staticmethod - def track_exists(filepath: str): - """ - Checks if a track exists in the database using its filepath. - """ + def remove_tracks_by_folders(folders: list[str]): + sql = "DELETE FROM tracks WHERE folder = ?" + with SQLiteManager() as cur: - cur.execute("SELECT * FROM tracks WHERE filepath=?", (filepath,)) - row = cur.fetchone() - - if row is not None: - return True - - return False + for folder in folders: + cur.execute(sql, (folder,)) diff --git a/app/db/store.py b/app/db/store.py index 13d7ea0b..0c7fca11 100644 --- a/app/db/store.py +++ b/app/db/store.py @@ -88,6 +88,17 @@ class Store: cls.tracks.remove(track) break + @classmethod + def remove_tracks_by_dir_except(cls, dirs: list[str]): + """Removes all tracks not in the root directories.""" + to_remove = set() + + for track in cls.tracks: + if not track.folder.startswith(tuple(dirs)): + to_remove.add(track.folder) + + tdb.remove_tracks_by_folders(to_remove) + @classmethod def count_tracks_by_hash(cls, trackhash: str) -> int: """ @@ -197,6 +208,8 @@ class Store: """ Creates a list of folders from the tracks in the store. """ + cls.folders.clear() + all_folders = [track.folder for track in cls.tracks] all_folders = set(all_folders) @@ -212,6 +225,7 @@ class Store: cls.folders.append(folder) + @classmethod def get_folder(cls, path: str): # type: ignore """ @@ -277,6 +291,8 @@ class Store: Loads all albums from the database into the store. """ + cls.albums = [] + albumhashes = set(t.albumhash for t in cls.tracks) for albumhash in tqdm(albumhashes, desc="Loading albums"): @@ -291,9 +307,9 @@ class Store: albumhash = album[1] colors = json.loads(album[2]) - for al in cls.albums: - if al.albumhash == albumhash: - al.set_colors(colors) + for _al in cls.albums: + if _al.albumhash == albumhash: + _al.set_colors(colors) break @classmethod diff --git a/app/lib/colorlib.py b/app/lib/colorlib.py index 43241aea..e3609c68 100644 --- a/app/lib/colorlib.py +++ b/app/lib/colorlib.py @@ -38,9 +38,13 @@ class ProcessAlbumColors: """ def __init__(self) -> None: + db_colors = db.get_all_albums() + db_albumhashes = "-".join([album[1] for album in db_colors]) + + albums = [a for a in Store.albums if a.albumhash not in db_albumhashes] with SQLiteManager() as cur: - for album in tqdm(Store.albums, desc="Processing album colors"): + for album in tqdm(albums, desc="Processing unprocessed album colors"): if len(album.colors) == 0: colors = self.process_color(album) @@ -71,9 +75,9 @@ class ProcessArtistColors: def __init__(self) -> None: db_colors: list[tuple] = list(adb.get_all_artists()) db_artisthashes = "-".join([artist[1] for artist in db_colors]) - all_artists = Store.artists + all_artists = [a for a in Store.artists if a.artisthash not in db_artisthashes] - for artist in tqdm(all_artists, desc="Processing artist colors"): + for artist in tqdm(all_artists, desc="Processing unprocessed artist colors"): if artist.artisthash not in db_artisthashes: self.process_color(artist) diff --git a/app/lib/folderslib.py b/app/lib/folderslib.py index 547ac75b..9062e0cf 100644 --- a/app/lib/folderslib.py +++ b/app/lib/folderslib.py @@ -1,5 +1,4 @@ import os -import pathlib from concurrent.futures import ThreadPoolExecutor from app.db.store import Store diff --git a/app/lib/populate.py b/app/lib/populate.py index ef1d99ef..3f767f78 100644 --- a/app/lib/populate.py +++ b/app/lib/populate.py @@ -25,7 +25,7 @@ class Populate: """ def __init__(self) -> None: - messages = { + text = { "root_unset": "The root directory is not set. Trying to scan the default directory: %s", "default_not_exists": "The directory: %s does not exist. Please open the app in your web browser to set the root directory.", "no_tracks": "No tracks found in: %s. Please open the app in your web browser to set the root directory.", @@ -40,17 +40,23 @@ class Populate: def_dir = "~/Music" if len(dirs_to_scan) == 0: - log.warning(messages["root_unset"], def_dir) + log.warning(text["root_unset"], def_dir) print("...") exists = os.path.exists(settings.MUSIC_DIR) if not exists: - log.warning(messages["default_not_exists"], def_dir) + log.warning(text["default_not_exists"], def_dir) return dirs_to_scan = [settings.MUSIC_DIR] + try: + if dirs_to_scan[0] == "$home": + dirs_to_scan = [settings.USER_HOME_DIR] + except IndexError: + pass + files = [] for _dir in dirs_to_scan: @@ -59,7 +65,7 @@ class Populate: untagged = self.filter_untagged(tracks, files) if initial_dirs_count == 0 and len(untagged) == 0: - log.warning(messages["no_tracks"], def_dir) + log.warning(text["no_tracks"], def_dir) return if initial_dirs_count == 0 and len(untagged) > 0: @@ -76,7 +82,7 @@ class Populate: settings.TCOLOR.ENDC, ) sdb.add_root_dirs(dirs_to_scan) - return + # return if len(untagged) == 0: log.info("All clear, no unread files.") From 29e61b31c3c55d92e9a8414530261f075109021a Mon Sep 17 00:00:00 2001 From: geoffrey45 Date: Tue, 24 Jan 2023 02:10:58 +0300 Subject: [PATCH 06/34] fix: remove favorite tracks whose values are None when getting favs + sync is_fav state when populating --- app/api/favorites.py | 23 +++++++++++++---------- app/api/playlist.py | 1 - app/api/settings.py | 14 ++++++++------ app/db/sqlite/utils.py | 23 +++++++++++++++++++---- app/db/store.py | 3 +-- app/lib/populate.py | 5 +++++ 6 files changed, 46 insertions(+), 23 deletions(-) diff --git a/app/api/favorites.py b/app/api/favorites.py index b8fbfb92..26a84ea8 100644 --- a/app/api/favorites.py +++ b/app/api/favorites.py @@ -150,6 +150,8 @@ def get_all_favorites(): favs = favdb.get_all() favs.reverse() + favs = [fav for fav in favs if fav[1] != ""] + tracks = [] albums = [] artists = [] @@ -162,21 +164,22 @@ def get_all_favorites(): ): break - if fav[2] == FavType.track: - tracks.append(fav[1]) - elif fav[2] == FavType.album: - albums.append(fav[1]) - elif fav[2] == FavType.artist: - artists.append(fav[1]) + if not len(tracks) >= track_limit: + if fav[2] == FavType.track: + tracks.append(fav[1]) + + if not len(albums) >= album_limit: + if fav[2] == FavType.album: + albums.append(fav[1]) + + if not len(artists) >= artist_limit: + if fav[2] == FavType.artist: + artists.append(fav[1]) src_tracks = sorted(Store.tracks, key=lambda x: x.trackhash) src_albums = sorted(Store.albums, key=lambda x: x.albumhash) src_artists = sorted(Store.artists, key=lambda x: x.artisthash) - tracks = tracks[:track_limit] - albums = albums[:album_limit] - artists = artists[:artist_limit] - tracks = UseBisection(src_tracks, "trackhash", tracks)() albums = UseBisection(src_albums, "albumhash", albums)() artists = UseBisection(src_artists, "artisthash", artists)() diff --git a/app/api/playlist.py b/app/api/playlist.py index 4686378a..bfae7682 100644 --- a/app/api/playlist.py +++ b/app/api/playlist.py @@ -166,7 +166,6 @@ def update_playlist_info(playlistid: str): return {"error": "Failed: Invalid image"}, 400 p_tuple = (*playlist.values(),) - print("banner pos:", playlist["banner_pos"]) update_playlist(int(playlistid), playlist) diff --git a/app/api/settings.py b/app/api/settings.py index 358fc4e0..b2de4d6c 100644 --- a/app/api/settings.py +++ b/app/api/settings.py @@ -57,18 +57,19 @@ def add_root_dirs(): # --- db_dirs = sdb.get_root_dirs() + _h = "$home" - if db_dirs[0] == "$home" and new_dirs[0] == "$home".strip(): + if db_dirs[0] == _h and new_dirs[0] == _h.strip(): return {"msg": "Not changed!"} - if db_dirs[0] == "$home": + if db_dirs[0] == _h: sdb.remove_root_dirs(db_dirs) try: - if new_dirs[0] == "$home": - finalize(["$home"], db_dirs, [settings.USER_HOME_DIR]) + if new_dirs[0] == _h: + finalize([_h], db_dirs, [settings.USER_HOME_DIR]) - return {"msg": "Updated!"} + return {"root_dirs": [_h]} except IndexError: pass @@ -84,10 +85,11 @@ def add_root_dirs(): pass db_dirs.extend(new_dirs) + db_dirs = [dir for dir in db_dirs if dir != _h] finalize(new_dirs, removed_dirs, db_dirs) - return {"msg": "Updated!"} + return {"root_dirs": db_dirs} @api.route("/settings/get-root-dirs", methods=["GET"]) diff --git a/app/db/sqlite/utils.py b/app/db/sqlite/utils.py index 6e51120b..900f27ad 100644 --- a/app/db/sqlite/utils.py +++ b/app/db/sqlite/utils.py @@ -4,6 +4,7 @@ Helper functions for use with the SQLite database. import sqlite3 from sqlite3 import Connection, Cursor +import time from app.models import Album, Playlist, Track from app.settings import APP_DB_PATH, USERDATA_DB_PATH @@ -82,12 +83,26 @@ class SQLiteManager: if self.userdata_db: db_path = USERDATA_DB_PATH - self.conn = sqlite3.connect(db_path) + self.conn = sqlite3.connect( + db_path, + timeout=15, + ) return self.conn.cursor() def __exit__(self, exc_type, exc_value, exc_traceback): if self.conn: - self.conn.commit() + trial_count = 0 - if self.CLOSE_CONN: - self.conn.close() + while trial_count < 10: + try: + self.conn.commit() + + if self.CLOSE_CONN: + self.conn.close() + + return + except sqlite3.OperationalError: + trial_count += 1 + time.sleep(3) + + self.conn.close() diff --git a/app/db/store.py b/app/db/store.py index 0c7fca11..f6e2bb60 100644 --- a/app/db/store.py +++ b/app/db/store.py @@ -40,7 +40,7 @@ class Store: cls.tracks = list(tdb.get_all_tracks()) fav_hashes = favdb.get_fav_tracks() - fav_hashes = [t[1] for t in fav_hashes] + fav_hashes = " ".join([t[1] for t in fav_hashes]) for track in tqdm(cls.tracks, desc="Loading tracks"): if track.trackhash in fav_hashes: @@ -225,7 +225,6 @@ class Store: cls.folders.append(folder) - @classmethod def get_folder(cls, path: str): # type: ignore """ diff --git a/app/lib/populate.py b/app/lib/populate.py index 3f767f78..b71eb00d 100644 --- a/app/lib/populate.py +++ b/app/lib/populate.py @@ -5,6 +5,7 @@ from tqdm import tqdm from app import settings from app.db.sqlite.tracks import SQLiteTrackMethods from app.db.sqlite.settings import SettingsSQLMethods as sdb +from app.db.sqlite.favorite import SQLiteFavoriteMethods as favdb from app.db.store import Store from app.lib.taglib import extract_thumb, get_tags @@ -101,12 +102,16 @@ class Populate: tagged_tracks: list[dict] = [] tagged_count = 0 + fav_tracks = favdb.get_fav_tracks() + fav_tracks = "-".join([t[1] for t in fav_tracks]) + for file in tqdm(untagged, desc="Reading files"): tags = get_tags(file) if tags is not None: tagged_tracks.append(tags) track = Track(**tags) + track.is_favorite = track.trackhash in fav_tracks Store.add_track(track) Store.add_folder(track.folder) From df6609e7f4da645329dcca439f591a5878fc5264 Mon Sep 17 00:00:00 2001 From: geoffrey45 Date: Tue, 24 Jan 2023 16:30:17 +0300 Subject: [PATCH 07/34] feat: support watching symlinks in watchdogg.py + remove code for auto-adding ~/Home to root_dirs during populate --- app/api/folder.py | 7 ++- app/api/settings.py | 24 +++++--- app/lib/populate.py | 43 ++------------- app/lib/watchdogg.py | 129 ++++++++++++++++++++++++++++++++++--------- app/logger.py | 5 +- app/settings.py | 2 +- 6 files changed, 131 insertions(+), 79 deletions(-) diff --git a/app/api/folder.py b/app/api/folder.py index e1de1f79..fa05fce1 100644 --- a/app/api/folder.py +++ b/app/api/folder.py @@ -30,8 +30,11 @@ def get_folder_tree(): root_dirs = db.get_root_dirs() - if req_dir == "$home" and root_dirs[0] == "$home": - req_dir = settings.USER_HOME_DIR + try: + if req_dir == "$home" and root_dirs[0] == "$home": + req_dir = settings.USER_HOME_DIR + except IndexError: + pass if req_dir == "$home": folders = [Path(f) for f in root_dirs] diff --git a/app/api/settings.py b/app/api/settings.py index b2de4d6c..bc2c8e02 100644 --- a/app/api/settings.py +++ b/app/api/settings.py @@ -1,12 +1,13 @@ from flask import Blueprint, request from app import settings -from app.db.sqlite.settings import SettingsSQLMethods as sdb -from app.lib import populate -from app.logger import log +from app.logger import log +from app.lib import populate from app.db.store import Store from app.utils import background +from app.lib.watchdogg import Watcher as WatchDog +from app.db.sqlite.settings import SettingsSQLMethods as sdb api = Blueprint("settings", __name__, url_prefix="/") @@ -19,6 +20,10 @@ def get_child_dirs(parent: str, children: list[str]): @background def rebuild_store(db_dirs: list[str]): + """ + Restarts the watchdog and rebuilds the music library. + """ + log.info("Rebuilding library...") Store.remove_tracks_by_dir_except(db_dirs) @@ -28,6 +33,7 @@ def rebuild_store(db_dirs: list[str]): Store.load_artists() populate.Populate() + WatchDog().restart() log.info("Rebuilding library... ✅") @@ -59,13 +65,13 @@ def add_root_dirs(): db_dirs = sdb.get_root_dirs() _h = "$home" - if db_dirs[0] == _h and new_dirs[0] == _h.strip(): - return {"msg": "Not changed!"} - - if db_dirs[0] == _h: - sdb.remove_root_dirs(db_dirs) - try: + if db_dirs[0] == _h and new_dirs[0] == _h.strip(): + return {"msg": "Not changed!"} + + if db_dirs[0] == _h: + sdb.remove_root_dirs(db_dirs) + if new_dirs[0] == _h: finalize([_h], db_dirs, [settings.USER_HOME_DIR]) diff --git a/app/lib/populate.py b/app/lib/populate.py index b71eb00d..d818fae6 100644 --- a/app/lib/populate.py +++ b/app/lib/populate.py @@ -26,31 +26,16 @@ class Populate: """ def __init__(self) -> None: - text = { - "root_unset": "The root directory is not set. Trying to scan the default directory: %s", - "default_not_exists": "The directory: %s does not exist. Please open the app in your web browser to set the root directory.", - "no_tracks": "No tracks found in: %s. Please open the app in your web browser to set the root directory.", - } - tracks = get_all_tracks() tracks = list(tracks) dirs_to_scan = sdb.get_root_dirs() - initial_dirs_count = len(dirs_to_scan) - - def_dir = "~/Music" if len(dirs_to_scan) == 0: - log.warning(text["root_unset"], def_dir) - print("...") - - exists = os.path.exists(settings.MUSIC_DIR) - - if not exists: - log.warning(text["default_not_exists"], def_dir) - return - - dirs_to_scan = [settings.MUSIC_DIR] + log.warning( + "The root directory is not configured. Open the app in your web browser to configure." + ) + return try: if dirs_to_scan[0] == "$home": @@ -65,26 +50,6 @@ class Populate: untagged = self.filter_untagged(tracks, files) - if initial_dirs_count == 0 and len(untagged) == 0: - log.warning(text["no_tracks"], def_dir) - return - - if initial_dirs_count == 0 and len(untagged) > 0: - log.info( - "%sFound %s tracks 💪 %s", - settings.TCOLOR.OKGREEN, - len(untagged), - settings.TCOLOR.ENDC, - ) - log.info( - "%s%s saved as the default root directory. 😶%s", - settings.TCOLOR.OKGREEN, - def_dir, - settings.TCOLOR.ENDC, - ) - sdb.add_root_dirs(dirs_to_scan) - # return - if len(untagged) == 0: log.info("All clear, no unread files.") return diff --git a/app/lib/watchdogg.py b/app/lib/watchdogg.py index 276f6baf..ccef335a 100644 --- a/app/lib/watchdogg.py +++ b/app/lib/watchdogg.py @@ -2,17 +2,22 @@ This library contains the classes and functions related to the watchdog file watcher. """ import os +import sqlite3 import time from watchdog.events import PatternMatchingEventHandler from watchdog.observers import Observer -from app.db.sqlite.tracks import SQLiteManager -from app.db.sqlite.tracks import SQLiteTrackMethods as db + +from app.logger import log from app.db.store import Store from app.lib.taglib import get_tags -from app.logger import log from app.models import Artist, Track +from app import settings + +from app.db.sqlite.tracks import SQLiteManager +from app.db.sqlite.tracks import SQLiteTrackMethods as db +from app.db.sqlite.settings import SettingsSQLMethods as sdb class Watcher: @@ -20,39 +25,96 @@ class Watcher: Contains the methods for initializing and starting watchdog. """ - home_dir = os.path.expanduser("~") - dirs = [home_dir] observers: list[Observer] = [] def __init__(self): self.observer = Observer() def run(self): - event_handler = Handler() + """ + Starts watchers for each dir in root_dirs + """ - for dir_ in self.dirs: + trials = 0 + + while trials < 10: + try: + dirs = sdb.get_root_dirs() + print(dirs) + dir_map = [ + {"original": d, "realpath": os.path.realpath(d)} for d in dirs + ] + break + except sqlite3.OperationalError: + trials += 1 + time.sleep(1) + else: + log.error( + "WatchDogError: Failed to start Watchdog. Waiting for database timed out!" + ) + return + + if len(dirs) == 0: + log.warning( + "WatchDogInfo: No root directories configured. Watchdog not started." + ) + return + + dir_map = [d for d in dir_map if d['realpath'] != d['original']] + + if len(dirs) > 0 and dirs[0] == "$home": + dirs = [settings.USER_HOME_DIR] + + event_handler = Handler(root_dirs=dirs, dir_map=dir_map) + + for _dir in dirs: + exists = os.path.exists(_dir) + + if not exists: + log.error("WatchdogError: Directory not found: %s", _dir) + + for _dir in dirs: self.observer.schedule( - event_handler, os.path.realpath(dir_), recursive=True + event_handler, os.path.realpath(_dir), recursive=True ) self.observers.append(self.observer) try: self.observer.start() - except OSError: - log.error("Could not start watchdog.") + log.info("Started watchdog") + except FileNotFoundError: + log.error( + "WatchdogError: Failed to start watchdog, root directories could not be resolved." + ) return try: while True: time.sleep(1) except KeyboardInterrupt: - for obsv in self.observers: - obsv.unschedule_all() - obsv.stop() + self.stop_all() for obsv in self.observers: obsv.join() + def stop_all(self): + """ + Unschedules and stops all existing watchers. + """ + log.info("Stopping all watchdog observers") + for obsv in self.observers: + obsv.unschedule_all() + obsv.stop() + + def restart(self): + """ + Stops all existing watchers, refetches root_dirs from the db + and restarts the watchers. + """ + log.info("🔃 Restarting watchdog") + self.stop_all() + self.run() + def add_track(filepath: str) -> None: """ @@ -118,9 +180,13 @@ def remove_track(filepath: str) -> None: class Handler(PatternMatchingEventHandler): files_to_process = [] + root_dirs = [] + dir_map = [] + + def __init__(self, root_dirs: list[str], dir_map: dict[str:str]): + self.root_dirs = root_dirs + self.dir_map = dir_map - def __init__(self): - log.info("✅ started watchdog") PatternMatchingEventHandler.__init__( self, patterns=["*.flac", "*.mp3"], @@ -128,6 +194,16 @@ class Handler(PatternMatchingEventHandler): case_sensitive=False, ) + def get_abs_path(self, path: str): + """ + Convert a realpath to a path relative to the matching root directory. + """ + for d in self.dir_map: + if d["realpath"] in path: + return path.replace(d["realpath"], d["original"]) + + return path + def on_created(self, event): """ Fired when a supported file is created. @@ -138,8 +214,8 @@ class Handler(PatternMatchingEventHandler): """ Fired when a delete event occurs on a supported file. """ - - remove_track(event.src_path) + path = self.get_abs_path(event.src_path) + remove_track(path) def on_moved(self, event): """ @@ -148,14 +224,19 @@ class Handler(PatternMatchingEventHandler): trash = "share/Trash" if trash in event.dest_path: - remove_track(event.src_path) + path = self.get_abs_path(event.src_path) + remove_track(path) elif trash in event.src_path: - add_track(event.dest_path) + path = self.get_abs_path(event.dest_path) + add_track(path) elif trash not in event.dest_path and trash not in event.src_path: - add_track(event.dest_path) - remove_track(event.src_path) + dest_path = self.get_abs_path(event.dest_path) + src_path = self.get_abs_path(event.src_path) + + add_track(dest_path) + remove_track(src_path) def on_closed(self, event): """ @@ -164,9 +245,7 @@ class Handler(PatternMatchingEventHandler): try: self.files_to_process.remove(event.src_path) if os.path.getsize(event.src_path) > 0: - add_track(event.src_path) + path = self.get_abs_path(event.src_path) + add_track(path) except ValueError: pass - - -# watcher = Watcher() diff --git a/app/logger.py b/app/logger.py index 9a82b2b1..81391288 100644 --- a/app/logger.py +++ b/app/logger.py @@ -10,9 +10,9 @@ class CustomFormatter(logging.Formatter): Custom log formatter """ - grey = "\x1b[38;20m" + grey = "\033[92m" yellow = "\x1b[33;20m" - red = "\x1b[31;20m" + red = "\033[41m" bold_red = "\x1b[31;1m" reset = "\x1b[0m" # format = ( @@ -45,5 +45,4 @@ handler.setLevel(logging.DEBUG) handler.setFormatter(CustomFormatter()) log.addHandler(handler) - # copied from: https://stackoverflow.com/a/56944256: diff --git a/app/settings.py b/app/settings.py index 529332fa..e3462874 100644 --- a/app/settings.py +++ b/app/settings.py @@ -105,7 +105,7 @@ class TCOLOR: OKBLUE = "\033[94m" OKCYAN = "\033[96m" OKGREEN = "\033[92m" - WARNING = "\033[93m" + YELLOW = "\033[93m" FAIL = "\033[91m" ENDC = "\033[0m" BOLD = "\033[1m" From 2ba1b0386e74b92e38667275e88cecdb499decc5 Mon Sep 17 00:00:00 2001 From: geoffrey45 Date: Tue, 24 Jan 2023 18:53:30 +0300 Subject: [PATCH 08/34] feat: extract featured artists from track title --- .gitignore | 3 +-- app/api/artist.py | 3 +-- app/api/settings.py | 13 ++++++------- app/models.py | 18 +++++++++++------- app/utils.py | 28 +++++++++++++++++++++++++++- tests/__init__.py | 0 tests/test.py | 18 ++++++++++++++++++ 7 files changed, 64 insertions(+), 19 deletions(-) create mode 100644 tests/__init__.py create mode 100644 tests/test.py diff --git a/.gitignore b/.gitignore index f87f9f0f..9164181e 100644 --- a/.gitignore +++ b/.gitignore @@ -16,10 +16,9 @@ __pycache__ .hypothesis sqllib.py encoderx.py -tests .pytest_cache # pyinstaller files dist build -client \ No newline at end of file +client diff --git a/app/api/artist.py b/app/api/artist.py index f03e2967..a25e3007 100644 --- a/app/api/artist.py +++ b/app/api/artist.py @@ -19,7 +19,7 @@ class CacheEntry: """ def __init__( - self, artisthash: str, albumhashes: set[str], tracks: list[Track] + self, artisthash: str, albumhashes: set[str], tracks: list[Track] ) -> None: self.albums: list[Album] = [] self.tracks: list[Track] = [] @@ -275,7 +275,6 @@ def get_artist_tracks(artisthash: str): # return {"albums": albums[:limit]} - # @artist_bp.route("/artist/") # @cache.cached() # def get_artist_data(artist: str): diff --git a/app/api/settings.py b/app/api/settings.py index bc2c8e02..2d998946 100644 --- a/app/api/settings.py +++ b/app/api/settings.py @@ -1,7 +1,6 @@ from flask import Blueprint, request from app import settings - from app.logger import log from app.lib import populate from app.db.store import Store @@ -15,7 +14,7 @@ api = Blueprint("settings", __name__, url_prefix="/") def get_child_dirs(parent: str, children: list[str]): """Returns child directories in a list, given a parent directory""" - return [dir for dir in children if dir.startswith(parent) and dir != parent] + return [_dir for _dir in children if _dir.startswith(parent) and _dir != parent] @background @@ -56,10 +55,10 @@ def add_root_dirs(): except KeyError: return msg, 400 - def finalize(new_dirs: list[str], removed_dirs: list[str], db_dirs: list[str]): - sdb.remove_root_dirs(removed_dirs) - sdb.add_root_dirs(new_dirs) - rebuild_store(db_dirs) + def finalize(new_: list[str], removed_: list[str], db_dirs_: list[str]): + sdb.remove_root_dirs(removed_) + sdb.add_root_dirs(new_) + rebuild_store(db_dirs_) # --- db_dirs = sdb.get_root_dirs() @@ -91,7 +90,7 @@ def add_root_dirs(): pass db_dirs.extend(new_dirs) - db_dirs = [dir for dir in db_dirs if dir != _h] + db_dirs = [dir_ for dir_ in db_dirs if dir_ != _h] finalize(new_dirs, removed_dirs, db_dirs) diff --git a/app/models.py b/app/models.py index 650ad8fc..a16cad67 100644 --- a/app/models.py +++ b/app/models.py @@ -58,10 +58,14 @@ class Track: def __post_init__(self): if self.artist is not None: - artist_str = str(self.artist).split(", ") - self.artist_hashes = [utils.create_hash(a, decode=True) for a in artist_str] + artists = utils.split_artists(self.artist) + featured = utils.extract_featured_artists_from_title(self.title) + artists.extend(featured) + artists = set(artists) - self.artist = [Artist(a) for a in artist_str] + self.artist_hashes = [utils.create_hash(a, decode=True) for a in artists] + + self.artist = [Artist(a) for a in artists] albumartists = str(self.albumartist).split(", ") self.albumartist = [Artist(a) for a in albumartists] @@ -150,10 +154,10 @@ class Album: Checks if the album is a single. """ if ( - len(tracks) == 1 - and tracks[0].title == self.title - and tracks[0].track == 1 - and tracks[0].disc == 1 + len(tracks) == 1 + and tracks[0].title == self.title + and tracks[0].track == 1 + and tracks[0].disc == 1 ): self.is_single = True diff --git a/app/utils.py b/app/utils.py index 76672866..2fae6c47 100644 --- a/app/utils.py +++ b/app/utils.py @@ -1,6 +1,7 @@ """ This module contains mini functions for the server. """ +import re from pathlib import Path from datetime import datetime @@ -187,7 +188,7 @@ def get_albumartists(albums: list[models.Album]) -> set[str]: def get_all_artists( - tracks: list[models.Track], albums: list[models.Album] + tracks: list[models.Track], albums: list[models.Album] ) -> list[models.Artist]: artists_from_tracks = get_artists_from_tracks(tracks) artist_from_albums = get_albumartists(albums) @@ -250,3 +251,28 @@ def is_windows(): Returns True if the OS is Windows. """ return platform.system() == "Windows" + + +def split_artists(src: str): + artists = re.split(r"\s*[&,;/]\s*", src) + return [a.strip() for a in artists] + + + + + +def extract_featured_artists_from_title(title: str) -> list[str]: + """ + Extracts featured artists from a song title using regex. + """ + regex = r"\((?:feat|ft|featuring|with)\.?\s+(.+?)\)" + match = re.search(regex, title, re.IGNORECASE) + + if not match: + return [] + + artists = match.group(1) + artists = split_artists(artists) + return artists + + diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/test.py b/tests/test.py new file mode 100644 index 00000000..29b12905 --- /dev/null +++ b/tests/test.py @@ -0,0 +1,18 @@ +from app.utils import extract_featured_artists_from_title + + +def test_extract_featured_artists_from_title(): + test_titles = [ + "Own it (Featuring Ed Sheeran & Stormzy)", + "Godzilla (Deluxe)(Feat. Juice Wrld)(Deluxe)", + "Simmer (with Burna Boy)", + ] + + expected_test_artists = [ + ["Ed Sheeran", "Stormzy"], + ['Juice Wrld'], + ["Burna Boy"] + ] + + for title, expected in zip(test_titles, expected_test_artists): + assert extract_featured_artists_from_title(title) == expected From af4221e0c76e74ee1d8024bc4c5ac26c5adec99b Mon Sep 17 00:00:00 2001 From: geoffrey45 Date: Tue, 24 Jan 2023 22:40:19 +0300 Subject: [PATCH 09/34] feat: exit the Populate function when another one is started + add test for the extract_fetured_artists_from_title function --- app/api/settings.py | 29 +- app/functions.py | 7 +- app/lib/populate.py | 18 +- app/lib/watchdogg.py | 1 - app/utils.py | 26 +- poetry.lock | 750 +++++++++++++++++++++++++------------------ pyproject.toml | 13 +- start.sh | 18 +- tests/test.py | 18 -- tests/test_utils.py | 33 ++ 10 files changed, 528 insertions(+), 385 deletions(-) delete mode 100644 tests/test.py create mode 100644 tests/test_utils.py diff --git a/app/api/settings.py b/app/api/settings.py index 2d998946..0c1b2a83 100644 --- a/app/api/settings.py +++ b/app/api/settings.py @@ -4,7 +4,7 @@ from app import settings from app.logger import log from app.lib import populate from app.db.store import Store -from app.utils import background +from app.utils import background, get_random_str from app.lib.watchdogg import Watcher as WatchDog from app.db.sqlite.settings import SettingsSQLMethods as sdb @@ -17,21 +17,32 @@ def get_child_dirs(parent: str, children: list[str]): return [_dir for _dir in children if _dir.startswith(parent) and _dir != parent] -@background -def rebuild_store(db_dirs: list[str]): +def reload_everything(): """ - Restarts the watchdog and rebuilds the music library. + Reloads all stores using the current database items """ - - log.info("Rebuilding library...") - Store.remove_tracks_by_dir_except(db_dirs) - Store.load_all_tracks() Store.process_folders() Store.load_albums() Store.load_artists() - populate.Populate() + +@background +def rebuild_store(db_dirs: list[str]): + """ + Restarts the watchdog and rebuilds the music library. + """ + log.info("Rebuilding library...") + Store.remove_tracks_by_dir_except(db_dirs) + reload_everything() + + key = get_random_str() + try: + populate.Populate(key=key) + except populate.PopulateCancelledError: + reload_everything() + return + WatchDog().restart() log.info("Rebuilding library... ✅") diff --git a/app/functions.py b/app/functions.py index 52e458b8..21405502 100644 --- a/app/functions.py +++ b/app/functions.py @@ -8,7 +8,7 @@ from requests import ReadTimeout from app import utils from app.lib.artistlib import CheckArtistImages from app.lib.colorlib import ProcessAlbumColors, ProcessArtistColors -from app.lib.populate import Populate, ProcessTrackThumbnails +from app.lib.populate import Populate, ProcessTrackThumbnails, PopulateCancelledError from app.lib.trackslib import validate_tracks from app.logger import log @@ -23,8 +23,11 @@ def run_periodic_checks(): validate_tracks() while True: + try: + Populate(key=utils.get_random_str()) + except PopulateCancelledError: + pass - Populate() ProcessTrackThumbnails() ProcessAlbumColors() ProcessArtistColors() diff --git a/app/lib/populate.py b/app/lib/populate.py index d818fae6..9e657dc5 100644 --- a/app/lib/populate.py +++ b/app/lib/populate.py @@ -16,6 +16,12 @@ from app.utils import run_fast_scandir get_all_tracks = SQLiteTrackMethods.get_all_tracks insert_many_tracks = SQLiteTrackMethods.insert_many_tracks +POPULATE_KEY = "" + + +class PopulateCancelledError(Exception): + pass + class Populate: """ @@ -25,7 +31,10 @@ class Populate: also checks if the album art exists in the image path, if not tries to extract it. """ - def __init__(self) -> None: + def __init__(self, key: str) -> None: + global POPULATE_KEY + POPULATE_KEY = key + tracks = get_all_tracks() tracks = list(tracks) @@ -54,7 +63,7 @@ class Populate: log.info("All clear, no unread files.") return - self.tag_untagged(untagged) + self.tag_untagged(untagged, key) @staticmethod def filter_untagged(tracks: list[Track], files: list[str]): @@ -62,7 +71,7 @@ class Populate: return set(files) - set(tagged_files) @staticmethod - def tag_untagged(untagged: set[str]): + def tag_untagged(untagged: set[str], key: str): log.info("Found %s new tracks", len(untagged)) tagged_tracks: list[dict] = [] tagged_count = 0 @@ -71,6 +80,9 @@ class Populate: fav_tracks = "-".join([t[1] for t in fav_tracks]) for file in tqdm(untagged, desc="Reading files"): + if POPULATE_KEY != key: + raise PopulateCancelledError('Populate key changed') + tags = get_tags(file) if tags is not None: diff --git a/app/lib/watchdogg.py b/app/lib/watchdogg.py index ccef335a..27ffa7cd 100644 --- a/app/lib/watchdogg.py +++ b/app/lib/watchdogg.py @@ -40,7 +40,6 @@ class Watcher: while trials < 10: try: dirs = sdb.get_root_dirs() - print(dirs) dir_map = [ {"original": d, "realpath": os.path.realpath(d)} for d in dirs ] diff --git a/app/utils.py b/app/utils.py index 2fae6c47..95d78869 100644 --- a/app/utils.py +++ b/app/utils.py @@ -1,7 +1,9 @@ """ This module contains mini functions for the server. """ +import random import re +import string from pathlib import Path from datetime import datetime @@ -32,16 +34,19 @@ def background(func): return background_func -def run_fast_scandir(__dir: str, full=False) -> tuple[list[str], list[str]]: +def run_fast_scandir(_dir: str, full=False) -> tuple[list[str], list[str]]: """ Scans a directory for files with a specific extension. Returns a list of files and folders in the directory. """ + if _dir == "": + return [], [] + subfolders = [] files = [] try: - for _files in os.scandir(__dir): + for _files in os.scandir(_dir): if _files.is_dir() and not _files.name.startswith("."): subfolders.append(_files.path) if _files.is_file(): @@ -54,7 +59,7 @@ def run_fast_scandir(__dir: str, full=False) -> tuple[list[str], list[str]]: sub_dirs, _files = run_fast_scandir(_dir, full=True) subfolders.extend(sub_dirs) files.extend(_files) - except PermissionError: + except (PermissionError, FileNotFoundError, ValueError): return [], [] return subfolders, files @@ -177,13 +182,11 @@ def get_artists_from_tracks(tracks: list[models.Track]) -> set[str]: def get_albumartists(albums: list[models.Album]) -> set[str]: artists = set() - # master_artist_list = [a.albumartists for a in albums] for album in albums: albumartists = [a.name for a in album.albumartists] # type: ignore artists.update(albumartists) - # return [models.Artist(a) for a in artists] return artists @@ -231,7 +234,10 @@ def get_home_res_path(filename: str): """ Returns a path to resources in the home directory of this project. Used to resolve resources in builds. """ - return (CWD / ".." / filename).resolve() + try: + return (CWD / ".." / filename).resolve() + except ValueError: + return None def get_ip(): @@ -258,9 +264,6 @@ def split_artists(src: str): return [a.strip() for a in artists] - - - def extract_featured_artists_from_title(title: str) -> list[str]: """ Extracts featured artists from a song title using regex. @@ -276,3 +279,8 @@ def extract_featured_artists_from_title(title: str) -> list[str]: return artists +def get_random_str(length=5): + """ + Generates a random string of length `length`. + """ + return "".join(random.choices(string.ascii_letters + string.digits, k=length)) diff --git a/poetry.lock b/poetry.lock index 963b7a27..69546dab 100644 --- a/poetry.lock +++ b/poetry.lock @@ -4,7 +4,7 @@ name = "altgraph" version = "0.17.3" description = "Python graph (network) package" -category = "main" +category = "dev" optional = false python-versions = "*" files = [ @@ -14,18 +14,19 @@ files = [ [[package]] name = "astroid" -version = "2.12.12" +version = "2.13.3" description = "An abstract syntax tree for Python with inference support." category = "dev" optional = false python-versions = ">=3.7.2" files = [ - {file = "astroid-2.12.12-py3-none-any.whl", hash = "sha256:72702205200b2a638358369d90c222d74ebc376787af8fb2f7f2a86f7b5cc85f"}, - {file = "astroid-2.12.12.tar.gz", hash = "sha256:1c00a14f5a3ed0339d38d2e2e5b74ea2591df5861c0936bb292b84ccf3a78d83"}, + {file = "astroid-2.13.3-py3-none-any.whl", hash = "sha256:14c1603c41cc61aae731cad1884a073c4645e26f126d13ac8346113c95577f3b"}, + {file = "astroid-2.13.3.tar.gz", hash = "sha256:6afc22718a48a689ca24a97981ad377ba7fb78c133f40335dfd16772f29bcfb1"}, ] [package.dependencies] lazy-object-proxy = ">=1.4.0" +typing-extensions = {version = ">=4.0.0", markers = "python_version < \"3.11\""} wrapt = [ {version = ">=1.11,<2", markers = "python_version < \"3.11\""}, {version = ">=1.14,<2", markers = "python_version >= \"3.11\""}, @@ -33,53 +34,43 @@ wrapt = [ [[package]] name = "attrs" -version = "22.1.0" +version = "22.2.0" description = "Classes Without Boilerplate" -category = "main" +category = "dev" optional = false -python-versions = ">=3.5" +python-versions = ">=3.6" files = [ - {file = "attrs-22.1.0-py2.py3-none-any.whl", hash = "sha256:86efa402f67bf2df34f51a335487cf46b1ec130d02b8d39fd248abfd30da551c"}, - {file = "attrs-22.1.0.tar.gz", hash = "sha256:29adc2665447e5191d0e7c568fde78b21f9672d344281d0c6e1ab085429b22b6"}, + {file = "attrs-22.2.0-py3-none-any.whl", hash = "sha256:29e95c7f6778868dbd49170f98f8818f78f3dc5e0e37c0b1f474e3561b240836"}, + {file = "attrs-22.2.0.tar.gz", hash = "sha256:c9227bfc2f01993c03f68db37d1d15c9690188323c067c641f1a35ca58185f99"}, ] [package.extras] -dev = ["cloudpickle", "coverage[toml] (>=5.0.2)", "furo", "hypothesis", "mypy (>=0.900,!=0.940)", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "sphinx", "sphinx-notfound-page", "zope.interface"] -docs = ["furo", "sphinx", "sphinx-notfound-page", "zope.interface"] -tests = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "zope.interface"] -tests-no-zope = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins"] +cov = ["attrs[tests]", "coverage-enable-subprocess", "coverage[toml] (>=5.3)"] +dev = ["attrs[docs,tests]"] +docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope.interface"] +tests = ["attrs[tests-no-zope]", "zope.interface"] +tests-no-zope = ["cloudpickle", "cloudpickle", "hypothesis", "hypothesis", "mypy (>=0.971,<0.990)", "mypy (>=0.971,<0.990)", "pympler", "pympler", "pytest (>=4.3.0)", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-mypy-plugins", "pytest-xdist[psutil]", "pytest-xdist[psutil]"] [[package]] name = "black" -version = "22.6.0" +version = "22.12.0" description = "The uncompromising code formatter." category = "dev" optional = false -python-versions = ">=3.6.2" +python-versions = ">=3.7" files = [ - {file = "black-22.6.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f586c26118bc6e714ec58c09df0157fe2d9ee195c764f630eb0d8e7ccce72e69"}, - {file = "black-22.6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b270a168d69edb8b7ed32c193ef10fd27844e5c60852039599f9184460ce0807"}, - {file = "black-22.6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6797f58943fceb1c461fb572edbe828d811e719c24e03375fd25170ada53825e"}, - {file = "black-22.6.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c85928b9d5f83b23cee7d0efcb310172412fbf7cb9d9ce963bd67fd141781def"}, - {file = "black-22.6.0-cp310-cp310-win_amd64.whl", hash = "sha256:f6fe02afde060bbeef044af7996f335fbe90b039ccf3f5eb8f16df8b20f77666"}, - {file = "black-22.6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:cfaf3895a9634e882bf9d2363fed5af8888802d670f58b279b0bece00e9a872d"}, - {file = "black-22.6.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94783f636bca89f11eb5d50437e8e17fbc6a929a628d82304c80fa9cd945f256"}, - {file = "black-22.6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:2ea29072e954a4d55a2ff58971b83365eba5d3d357352a07a7a4df0d95f51c78"}, - {file = "black-22.6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e439798f819d49ba1c0bd9664427a05aab79bfba777a6db94fd4e56fae0cb849"}, - {file = "black-22.6.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:187d96c5e713f441a5829e77120c269b6514418f4513a390b0499b0987f2ff1c"}, - {file = "black-22.6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:074458dc2f6e0d3dab7928d4417bb6957bb834434516f21514138437accdbe90"}, - {file = "black-22.6.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:a218d7e5856f91d20f04e931b6f16d15356db1c846ee55f01bac297a705ca24f"}, - {file = "black-22.6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:568ac3c465b1c8b34b61cd7a4e349e93f91abf0f9371eda1cf87194663ab684e"}, - {file = "black-22.6.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:6c1734ab264b8f7929cef8ae5f900b85d579e6cbfde09d7387da8f04771b51c6"}, - {file = "black-22.6.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9a3ac16efe9ec7d7381ddebcc022119794872abce99475345c5a61aa18c45ad"}, - {file = "black-22.6.0-cp38-cp38-win_amd64.whl", hash = "sha256:b9fd45787ba8aa3f5e0a0a98920c1012c884622c6c920dbe98dbd05bc7c70fbf"}, - {file = "black-22.6.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7ba9be198ecca5031cd78745780d65a3f75a34b2ff9be5837045dce55db83d1c"}, - {file = "black-22.6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a3db5b6409b96d9bd543323b23ef32a1a2b06416d525d27e0f67e74f1446c8f2"}, - {file = "black-22.6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:560558527e52ce8afba936fcce93a7411ab40c7d5fe8c2463e279e843c0328ee"}, - {file = "black-22.6.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b154e6bbde1e79ea3260c4b40c0b7b3109ffcdf7bc4ebf8859169a6af72cd70b"}, - {file = "black-22.6.0-cp39-cp39-win_amd64.whl", hash = "sha256:4af5bc0e1f96be5ae9bd7aaec219c901a94d6caa2484c21983d043371c733fc4"}, - {file = "black-22.6.0-py3-none-any.whl", hash = "sha256:ac609cf8ef5e7115ddd07d85d988d074ed00e10fbc3445aee393e70164a2219c"}, - {file = "black-22.6.0.tar.gz", hash = "sha256:6c6d39e28aed379aec40da1c65434c77d75e65bb59a1e1c283de545fb4e7c6c9"}, + {file = "black-22.12.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9eedd20838bd5d75b80c9f5487dbcb06836a43833a37846cf1d8c1cc01cef59d"}, + {file = "black-22.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:159a46a4947f73387b4d83e87ea006dbb2337eab6c879620a3ba52699b1f4351"}, + {file = "black-22.12.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d30b212bffeb1e252b31dd269dfae69dd17e06d92b87ad26e23890f3efea366f"}, + {file = "black-22.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:7412e75863aa5c5411886804678b7d083c7c28421210180d67dfd8cf1221e1f4"}, + {file = "black-22.12.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c116eed0efb9ff870ded8b62fe9f28dd61ef6e9ddd28d83d7d264a38417dcee2"}, + {file = "black-22.12.0-cp37-cp37m-win_amd64.whl", hash = "sha256:1f58cbe16dfe8c12b7434e50ff889fa479072096d79f0a7f25e4ab8e94cd8350"}, + {file = "black-22.12.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77d86c9f3db9b1bf6761244bc0b3572a546f5fe37917a044e02f3166d5aafa7d"}, + {file = "black-22.12.0-cp38-cp38-win_amd64.whl", hash = "sha256:82d9fe8fee3401e02e79767016b4907820a7dc28d70d137eb397b92ef3cc5bfc"}, + {file = "black-22.12.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:101c69b23df9b44247bd88e1d7e90154336ac4992502d4197bdac35dd7ee3320"}, + {file = "black-22.12.0-cp39-cp39-win_amd64.whl", hash = "sha256:559c7a1ba9a006226f09e4916060982fd27334ae1998e7a38b3f33a37f7a2148"}, + {file = "black-22.12.0-py3-none-any.whl", hash = "sha256:436cc9167dd28040ad90d3b404aec22cedf24a6e4d7de221bec2730ec0c97bcf"}, + {file = "black-22.12.0.tar.gz", hash = "sha256:229351e5a18ca30f447bf724d007f890f97e13af070bb6ad4c0a441cd7596a2f"}, ] [package.dependencies] @@ -97,31 +88,114 @@ uvloop = ["uvloop (>=0.15.2)"] [[package]] name = "certifi" -version = "2022.5.18.1" +version = "2022.12.7" description = "Python package for providing Mozilla's CA Bundle." category = "main" optional = false python-versions = ">=3.6" files = [ - {file = "certifi-2022.5.18.1-py3-none-any.whl", hash = "sha256:f1d53542ee8cbedbe2118b5686372fb33c297fcd6379b050cca0ef13a597382a"}, - {file = "certifi-2022.5.18.1.tar.gz", hash = "sha256:9c5705e395cd70084351dd8ad5c41e65655e08ce46f2ec9cf6c2c08390f71eb7"}, + {file = "certifi-2022.12.7-py3-none-any.whl", hash = "sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18"}, + {file = "certifi-2022.12.7.tar.gz", hash = "sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3"}, ] [[package]] name = "charset-normalizer" -version = "2.0.12" +version = "3.0.1" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." category = "main" optional = false -python-versions = ">=3.5.0" +python-versions = "*" files = [ - {file = "charset-normalizer-2.0.12.tar.gz", hash = "sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597"}, - {file = "charset_normalizer-2.0.12-py3-none-any.whl", hash = "sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df"}, + {file = "charset-normalizer-3.0.1.tar.gz", hash = "sha256:ebea339af930f8ca5d7a699b921106c6e29c617fe9606fa7baa043c1cdae326f"}, + {file = "charset_normalizer-3.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:88600c72ef7587fe1708fd242b385b6ed4b8904976d5da0893e31df8b3480cb6"}, + {file = "charset_normalizer-3.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c75ffc45f25324e68ab238cb4b5c0a38cd1c3d7f1fb1f72b5541de469e2247db"}, + {file = "charset_normalizer-3.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:db72b07027db150f468fbada4d85b3b2729a3db39178abf5c543b784c1254539"}, + {file = "charset_normalizer-3.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62595ab75873d50d57323a91dd03e6966eb79c41fa834b7a1661ed043b2d404d"}, + {file = "charset_normalizer-3.0.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ff6f3db31555657f3163b15a6b7c6938d08df7adbfc9dd13d9d19edad678f1e8"}, + {file = "charset_normalizer-3.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:772b87914ff1152b92a197ef4ea40efe27a378606c39446ded52c8f80f79702e"}, + {file = "charset_normalizer-3.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70990b9c51340e4044cfc394a81f614f3f90d41397104d226f21e66de668730d"}, + {file = "charset_normalizer-3.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:292d5e8ba896bbfd6334b096e34bffb56161c81408d6d036a7dfa6929cff8783"}, + {file = "charset_normalizer-3.0.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:2edb64ee7bf1ed524a1da60cdcd2e1f6e2b4f66ef7c077680739f1641f62f555"}, + {file = "charset_normalizer-3.0.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:31a9ddf4718d10ae04d9b18801bd776693487cbb57d74cc3458a7673f6f34639"}, + {file = "charset_normalizer-3.0.1-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:44ba614de5361b3e5278e1241fda3dc1838deed864b50a10d7ce92983797fa76"}, + {file = "charset_normalizer-3.0.1-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:12db3b2c533c23ab812c2b25934f60383361f8a376ae272665f8e48b88e8e1c6"}, + {file = "charset_normalizer-3.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c512accbd6ff0270939b9ac214b84fb5ada5f0409c44298361b2f5e13f9aed9e"}, + {file = "charset_normalizer-3.0.1-cp310-cp310-win32.whl", hash = "sha256:502218f52498a36d6bf5ea77081844017bf7982cdbe521ad85e64cabee1b608b"}, + {file = "charset_normalizer-3.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:601f36512f9e28f029d9481bdaf8e89e5148ac5d89cffd3b05cd533eeb423b59"}, + {file = "charset_normalizer-3.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0298eafff88c99982a4cf66ba2efa1128e4ddaca0b05eec4c456bbc7db691d8d"}, + {file = "charset_normalizer-3.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a8d0fc946c784ff7f7c3742310cc8a57c5c6dc31631269876a88b809dbeff3d3"}, + {file = "charset_normalizer-3.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:87701167f2a5c930b403e9756fab1d31d4d4da52856143b609e30a1ce7160f3c"}, + {file = "charset_normalizer-3.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14e76c0f23218b8f46c4d87018ca2e441535aed3632ca134b10239dfb6dadd6b"}, + {file = "charset_normalizer-3.0.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0c0a590235ccd933d9892c627dec5bc7511ce6ad6c1011fdf5b11363022746c1"}, + {file = "charset_normalizer-3.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c7fe7afa480e3e82eed58e0ca89f751cd14d767638e2550c77a92a9e749c317"}, + {file = "charset_normalizer-3.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:79909e27e8e4fcc9db4addea88aa63f6423ebb171db091fb4373e3312cb6d603"}, + {file = "charset_normalizer-3.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ac7b6a045b814cf0c47f3623d21ebd88b3e8cf216a14790b455ea7ff0135d18"}, + {file = "charset_normalizer-3.0.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:72966d1b297c741541ca8cf1223ff262a6febe52481af742036a0b296e35fa5a"}, + {file = "charset_normalizer-3.0.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:f9d0c5c045a3ca9bedfc35dca8526798eb91a07aa7a2c0fee134c6c6f321cbd7"}, + {file = "charset_normalizer-3.0.1-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:5995f0164fa7df59db4746112fec3f49c461dd6b31b841873443bdb077c13cfc"}, + {file = "charset_normalizer-3.0.1-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4a8fcf28c05c1f6d7e177a9a46a1c52798bfe2ad80681d275b10dcf317deaf0b"}, + {file = "charset_normalizer-3.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:761e8904c07ad053d285670f36dd94e1b6ab7f16ce62b9805c475b7aa1cffde6"}, + {file = "charset_normalizer-3.0.1-cp311-cp311-win32.whl", hash = "sha256:71140351489970dfe5e60fc621ada3e0f41104a5eddaca47a7acb3c1b851d6d3"}, + {file = "charset_normalizer-3.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:9ab77acb98eba3fd2a85cd160851816bfce6871d944d885febf012713f06659c"}, + {file = "charset_normalizer-3.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:84c3990934bae40ea69a82034912ffe5a62c60bbf6ec5bc9691419641d7d5c9a"}, + {file = "charset_normalizer-3.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:74292fc76c905c0ef095fe11e188a32ebd03bc38f3f3e9bcb85e4e6db177b7ea"}, + {file = "charset_normalizer-3.0.1-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c95a03c79bbe30eec3ec2b7f076074f4281526724c8685a42872974ef4d36b72"}, + {file = "charset_normalizer-3.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f4c39b0e3eac288fedc2b43055cfc2ca7a60362d0e5e87a637beac5d801ef478"}, + {file = "charset_normalizer-3.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:df2c707231459e8a4028eabcd3cfc827befd635b3ef72eada84ab13b52e1574d"}, + {file = "charset_normalizer-3.0.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:93ad6d87ac18e2a90b0fe89df7c65263b9a99a0eb98f0a3d2e079f12a0735837"}, + {file = "charset_normalizer-3.0.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:59e5686dd847347e55dffcc191a96622f016bc0ad89105e24c14e0d6305acbc6"}, + {file = "charset_normalizer-3.0.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:cd6056167405314a4dc3c173943f11249fa0f1b204f8b51ed4bde1a9cd1834dc"}, + {file = "charset_normalizer-3.0.1-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:083c8d17153ecb403e5e1eb76a7ef4babfc2c48d58899c98fcaa04833e7a2f9a"}, + {file = "charset_normalizer-3.0.1-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:f5057856d21e7586765171eac8b9fc3f7d44ef39425f85dbcccb13b3ebea806c"}, + {file = "charset_normalizer-3.0.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:7eb33a30d75562222b64f569c642ff3dc6689e09adda43a082208397f016c39a"}, + {file = "charset_normalizer-3.0.1-cp36-cp36m-win32.whl", hash = "sha256:95dea361dd73757c6f1c0a1480ac499952c16ac83f7f5f4f84f0658a01b8ef41"}, + {file = "charset_normalizer-3.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:eaa379fcd227ca235d04152ca6704c7cb55564116f8bc52545ff357628e10602"}, + {file = "charset_normalizer-3.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:3e45867f1f2ab0711d60c6c71746ac53537f1684baa699f4f668d4c6f6ce8e14"}, + {file = "charset_normalizer-3.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cadaeaba78750d58d3cc6ac4d1fd867da6fc73c88156b7a3212a3cd4819d679d"}, + {file = "charset_normalizer-3.0.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:911d8a40b2bef5b8bbae2e36a0b103f142ac53557ab421dc16ac4aafee6f53dc"}, + {file = "charset_normalizer-3.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:503e65837c71b875ecdd733877d852adbc465bd82c768a067badd953bf1bc5a3"}, + {file = "charset_normalizer-3.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a60332922359f920193b1d4826953c507a877b523b2395ad7bc716ddd386d866"}, + {file = "charset_normalizer-3.0.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:16a8663d6e281208d78806dbe14ee9903715361cf81f6d4309944e4d1e59ac5b"}, + {file = "charset_normalizer-3.0.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:a16418ecf1329f71df119e8a65f3aa68004a3f9383821edcb20f0702934d8087"}, + {file = "charset_normalizer-3.0.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:9d9153257a3f70d5f69edf2325357251ed20f772b12e593f3b3377b5f78e7ef8"}, + {file = "charset_normalizer-3.0.1-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:02a51034802cbf38db3f89c66fb5d2ec57e6fe7ef2f4a44d070a593c3688667b"}, + {file = "charset_normalizer-3.0.1-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:2e396d70bc4ef5325b72b593a72c8979999aa52fb8bcf03f701c1b03e1166918"}, + {file = "charset_normalizer-3.0.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:11b53acf2411c3b09e6af37e4b9005cba376c872503c8f28218c7243582df45d"}, + {file = "charset_normalizer-3.0.1-cp37-cp37m-win32.whl", hash = "sha256:0bf2dae5291758b6f84cf923bfaa285632816007db0330002fa1de38bfcb7154"}, + {file = "charset_normalizer-3.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:2c03cc56021a4bd59be889c2b9257dae13bf55041a3372d3295416f86b295fb5"}, + {file = "charset_normalizer-3.0.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:024e606be3ed92216e2b6952ed859d86b4cfa52cd5bc5f050e7dc28f9b43ec42"}, + {file = "charset_normalizer-3.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:4b0d02d7102dd0f997580b51edc4cebcf2ab6397a7edf89f1c73b586c614272c"}, + {file = "charset_normalizer-3.0.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:358a7c4cb8ba9b46c453b1dd8d9e431452d5249072e4f56cfda3149f6ab1405e"}, + {file = "charset_normalizer-3.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:81d6741ab457d14fdedc215516665050f3822d3e56508921cc7239f8c8e66a58"}, + {file = "charset_normalizer-3.0.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8b8af03d2e37866d023ad0ddea594edefc31e827fee64f8de5611a1dbc373174"}, + {file = "charset_normalizer-3.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9cf4e8ad252f7c38dd1f676b46514f92dc0ebeb0db5552f5f403509705e24753"}, + {file = "charset_normalizer-3.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e696f0dd336161fca9adbb846875d40752e6eba585843c768935ba5c9960722b"}, + {file = "charset_normalizer-3.0.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c22d3fe05ce11d3671297dc8973267daa0f938b93ec716e12e0f6dee81591dc1"}, + {file = "charset_normalizer-3.0.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:109487860ef6a328f3eec66f2bf78b0b72400280d8f8ea05f69c51644ba6521a"}, + {file = "charset_normalizer-3.0.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:37f8febc8ec50c14f3ec9637505f28e58d4f66752207ea177c1d67df25da5aed"}, + {file = "charset_normalizer-3.0.1-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:f97e83fa6c25693c7a35de154681fcc257c1c41b38beb0304b9c4d2d9e164479"}, + {file = "charset_normalizer-3.0.1-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:a152f5f33d64a6be73f1d30c9cc82dfc73cec6477ec268e7c6e4c7d23c2d2291"}, + {file = "charset_normalizer-3.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:39049da0ffb96c8cbb65cbf5c5f3ca3168990adf3551bd1dee10c48fce8ae820"}, + {file = "charset_normalizer-3.0.1-cp38-cp38-win32.whl", hash = "sha256:4457ea6774b5611f4bed5eaa5df55f70abde42364d498c5134b7ef4c6958e20e"}, + {file = "charset_normalizer-3.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:e62164b50f84e20601c1ff8eb55620d2ad25fb81b59e3cd776a1902527a788af"}, + {file = "charset_normalizer-3.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8eade758719add78ec36dc13201483f8e9b5d940329285edcd5f70c0a9edbd7f"}, + {file = "charset_normalizer-3.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8499ca8f4502af841f68135133d8258f7b32a53a1d594aa98cc52013fff55678"}, + {file = "charset_normalizer-3.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3fc1c4a2ffd64890aebdb3f97e1278b0cc72579a08ca4de8cd2c04799a3a22be"}, + {file = "charset_normalizer-3.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:00d3ffdaafe92a5dc603cb9bd5111aaa36dfa187c8285c543be562e61b755f6b"}, + {file = "charset_normalizer-3.0.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c2ac1b08635a8cd4e0cbeaf6f5e922085908d48eb05d44c5ae9eabab148512ca"}, + {file = "charset_normalizer-3.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f6f45710b4459401609ebebdbcfb34515da4fc2aa886f95107f556ac69a9147e"}, + {file = "charset_normalizer-3.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ae1de54a77dc0d6d5fcf623290af4266412a7c4be0b1ff7444394f03f5c54e3"}, + {file = "charset_normalizer-3.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3b590df687e3c5ee0deef9fc8c547d81986d9a1b56073d82de008744452d6541"}, + {file = "charset_normalizer-3.0.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ab5de034a886f616a5668aa5d098af2b5385ed70142090e2a31bcbd0af0fdb3d"}, + {file = "charset_normalizer-3.0.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9cb3032517f1627cc012dbc80a8ec976ae76d93ea2b5feaa9d2a5b8882597579"}, + {file = "charset_normalizer-3.0.1-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:608862a7bf6957f2333fc54ab4399e405baad0163dc9f8d99cb236816db169d4"}, + {file = "charset_normalizer-3.0.1-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:0f438ae3532723fb6ead77e7c604be7c8374094ef4ee2c5e03a3a17f1fca256c"}, + {file = "charset_normalizer-3.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:356541bf4381fa35856dafa6a965916e54bed415ad8a24ee6de6e37deccf2786"}, + {file = "charset_normalizer-3.0.1-cp39-cp39-win32.whl", hash = "sha256:39cf9ed17fe3b1bc81f33c9ceb6ce67683ee7526e65fde1447c772afc54a1bb8"}, + {file = "charset_normalizer-3.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:0a11e971ed097d24c534c037d298ad32c6ce81a45736d31e0ff0ad37ab437d59"}, + {file = "charset_normalizer-3.0.1-py3-none-any.whl", hash = "sha256:7e189e2e1d3ed2f4aebabd2d5b0f931e883676e51c7624826e0a4e5fe8a0bf24"}, ] -[package.extras] -unicode-backport = ["unicodedata2"] - [[package]] name = "click" version = "8.1.3" @@ -150,7 +224,7 @@ files = [ ] [[package]] -name = "colorgram.py" +name = "colorgram-py" version = "1.2.0" description = "A Python module for extracting colors from images. Get a palette of any picture!" category = "main" @@ -181,14 +255,14 @@ graph = ["objgraph (>=1.7.2)"] [[package]] name = "exceptiongroup" -version = "1.0.0rc9" +version = "1.1.0" description = "Backport of PEP 654 (exception groups)" -category = "main" +category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "exceptiongroup-1.0.0rc9-py3-none-any.whl", hash = "sha256:2e3c3fc1538a094aab74fad52d6c33fc94de3dfee3ee01f187c0e0c72aec5337"}, - {file = "exceptiongroup-1.0.0rc9.tar.gz", hash = "sha256:9086a4a21ef9b31c72181c77c040a074ba0889ee56a7b289ff0afb0d97655f96"}, + {file = "exceptiongroup-1.1.0-py3-none-any.whl", hash = "sha256:327cbda3da756e2de031a3107b81ab7b3770a602c4d16ca618298c526f4bec1e"}, + {file = "exceptiongroup-1.1.0.tar.gz", hash = "sha256:bcb67d800a4497e1b404c2dd44fca47d3b7a5e5433dbab67f96c1a685cdfdf23"}, ] [package.extras] @@ -196,21 +270,21 @@ test = ["pytest (>=6)"] [[package]] name = "flask" -version = "2.1.2" +version = "2.2.2" description = "A simple framework for building complex web applications." category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "Flask-2.1.2-py3-none-any.whl", hash = "sha256:fad5b446feb0d6db6aec0c3184d16a8c1f6c3e464b511649c8918a9be100b4fe"}, - {file = "Flask-2.1.2.tar.gz", hash = "sha256:315ded2ddf8a6281567edb27393010fe3406188bafbfe65a3339d5787d89e477"}, + {file = "Flask-2.2.2-py3-none-any.whl", hash = "sha256:b9c46cc36662a7949f34b52d8ec7bb59c0d74ba08ba6cb9ce9adc1d8676d9526"}, + {file = "Flask-2.2.2.tar.gz", hash = "sha256:642c450d19c4ad482f96729bd2a8f6d32554aa1e231f4f6b4e7e5264b16cca2b"}, ] [package.dependencies] click = ">=8.0" itsdangerous = ">=2.0" Jinja2 = ">=3.0" -Werkzeug = ">=2.0" +Werkzeug = ">=2.2.2" [package.extras] async = ["asgiref (>=3.2)"] @@ -234,13 +308,13 @@ Six = "*" [[package]] name = "future" -version = "0.18.2" +version = "0.18.3" description = "Clean single-source support for Python 3 and 2" -category = "main" +category = "dev" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" files = [ - {file = "future-0.18.2.tar.gz", hash = "sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d"}, + {file = "future-0.18.3.tar.gz", hash = "sha256:34a17436ed1e96697a86f9de3d15a3b0be01d8bc8de9c1dffd59fb8234ed5307"}, ] [[package]] @@ -266,71 +340,71 @@ tornado = ["tornado (>=0.2)"] [[package]] name = "hypothesis" -version = "6.56.3" +version = "6.65.0" description = "A library for property-based testing" -category = "main" +category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "hypothesis-6.56.3-py3-none-any.whl", hash = "sha256:802d236d03dbd54e0e1c55c0daa2ec41aeaadc87a4dcbb41421b78bf3f7a7789"}, - {file = "hypothesis-6.56.3.tar.gz", hash = "sha256:15dae5d993339aefa57e00f5cb5a5817ff300eeb661d96d1c9d094eb62b04c9a"}, + {file = "hypothesis-6.65.0-py3-none-any.whl", hash = "sha256:24e3219b0b181414c06bb7a62649a6edb471f148d25c9c9687f47505b0f50b1c"}, + {file = "hypothesis-6.65.0.tar.gz", hash = "sha256:d25914dd4008b0292d116ac315f01f6691c5460c494a0291c01d96f4bc17fe68"}, ] [package.dependencies] attrs = ">=19.2.0" -exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} +exceptiongroup = {version = ">=1.0.0", markers = "python_version < \"3.11\""} sortedcontainers = ">=2.1.0,<3.0.0" [package.extras] -all = ["backports.zoneinfo (>=0.2.1)", "black (>=19.10b0)", "click (>=7.0)", "django (>=3.2)", "dpcontracts (>=0.4)", "importlib-metadata (>=3.6)", "lark-parser (>=0.6.5)", "libcst (>=0.3.16)", "numpy (>=1.9.0)", "pandas (>=1.0)", "pytest (>=4.6)", "python-dateutil (>=1.4)", "pytz (>=2014.1)", "redis (>=3.0.0)", "rich (>=9.0.0)", "tzdata (>=2022.5)"] +all = ["backports.zoneinfo (>=0.2.1)", "black (>=19.10b0)", "click (>=7.0)", "django (>=3.2)", "dpcontracts (>=0.4)", "importlib-metadata (>=3.6)", "lark (>=0.10.1)", "libcst (>=0.3.16)", "numpy (>=1.9.0)", "pandas (>=1.0)", "pytest (>=4.6)", "python-dateutil (>=1.4)", "pytz (>=2014.1)", "redis (>=3.0.0)", "rich (>=9.0.0)", "tzdata (>=2022.7)"] cli = ["black (>=19.10b0)", "click (>=7.0)", "rich (>=9.0.0)"] codemods = ["libcst (>=0.3.16)"] dateutil = ["python-dateutil (>=1.4)"] django = ["django (>=3.2)"] dpcontracts = ["dpcontracts (>=0.4)"] ghostwriter = ["black (>=19.10b0)"] -lark = ["lark-parser (>=0.6.5)"] +lark = ["lark (>=0.10.1)"] numpy = ["numpy (>=1.9.0)"] pandas = ["pandas (>=1.0)"] pytest = ["pytest (>=4.6)"] pytz = ["pytz (>=2014.1)"] redis = ["redis (>=3.0.0)"] -zoneinfo = ["backports.zoneinfo (>=0.2.1)", "tzdata (>=2022.5)"] +zoneinfo = ["backports.zoneinfo (>=0.2.1)", "tzdata (>=2022.7)"] [[package]] name = "idna" -version = "3.3" +version = "3.4" description = "Internationalized Domain Names in Applications (IDNA)" category = "main" optional = false python-versions = ">=3.5" files = [ - {file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"}, - {file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"}, + {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, + {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, ] [[package]] name = "iniconfig" -version = "1.1.1" -description = "iniconfig: brain-dead simple config-ini parsing" -category = "main" +version = "2.0.0" +description = "brain-dead simple config-ini parsing" +category = "dev" optional = false -python-versions = "*" +python-versions = ">=3.7" files = [ - {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, - {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, ] [[package]] name = "isort" -version = "5.10.1" +version = "5.11.4" description = "A Python utility / library to sort Python imports." category = "dev" optional = false -python-versions = ">=3.6.1,<4.0" +python-versions = ">=3.7.0" files = [ - {file = "isort-5.10.1-py3-none-any.whl", hash = "sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7"}, - {file = "isort-5.10.1.tar.gz", hash = "sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951"}, + {file = "isort-5.11.4-py3-none-any.whl", hash = "sha256:c033fd0edb91000a7f09527fe5c75321878f98322a77ddcc81adbd83724afb7b"}, + {file = "isort-5.11.4.tar.gz", hash = "sha256:6db30c5ded9815d813932c04c2f85a360bcdd35fed496f4d8f35495ef0a261b6"}, ] [package.extras] @@ -371,38 +445,55 @@ i18n = ["Babel (>=2.7)"] [[package]] name = "lazy-object-proxy" -version = "1.8.0" +version = "1.9.0" description = "A fast and thorough lazy object proxy." category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "lazy-object-proxy-1.8.0.tar.gz", hash = "sha256:c219a00245af0f6fa4e95901ed28044544f50152840c5b6a3e7b2568db34d156"}, - {file = "lazy_object_proxy-1.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4fd031589121ad46e293629b39604031d354043bb5cdf83da4e93c2d7f3389fe"}, - {file = "lazy_object_proxy-1.8.0-cp310-cp310-win32.whl", hash = "sha256:b70d6e7a332eb0217e7872a73926ad4fdc14f846e85ad6749ad111084e76df25"}, - {file = "lazy_object_proxy-1.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:eb329f8d8145379bf5dbe722182410fe8863d186e51bf034d2075eb8d85ee25b"}, - {file = "lazy_object_proxy-1.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4e2d9f764f1befd8bdc97673261b8bb888764dfdbd7a4d8f55e4fbcabb8c3fb7"}, - {file = "lazy_object_proxy-1.8.0-cp311-cp311-win32.whl", hash = "sha256:e20bfa6db17a39c706d24f82df8352488d2943a3b7ce7d4c22579cb89ca8896e"}, - {file = "lazy_object_proxy-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:14010b49a2f56ec4943b6cf925f597b534ee2fe1f0738c84b3bce0c1a11ff10d"}, - {file = "lazy_object_proxy-1.8.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:6850e4aeca6d0df35bb06e05c8b934ff7c533734eb51d0ceb2d63696f1e6030c"}, - {file = "lazy_object_proxy-1.8.0-cp37-cp37m-win32.whl", hash = "sha256:5b51d6f3bfeb289dfd4e95de2ecd464cd51982fe6f00e2be1d0bf94864d58acd"}, - {file = "lazy_object_proxy-1.8.0-cp37-cp37m-win_amd64.whl", hash = "sha256:6f593f26c470a379cf7f5bc6db6b5f1722353e7bf937b8d0d0b3fba911998858"}, - {file = "lazy_object_proxy-1.8.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0c1c7c0433154bb7c54185714c6929acc0ba04ee1b167314a779b9025517eada"}, - {file = "lazy_object_proxy-1.8.0-cp38-cp38-win32.whl", hash = "sha256:d176f392dbbdaacccf15919c77f526edf11a34aece58b55ab58539807b85436f"}, - {file = "lazy_object_proxy-1.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:afcaa24e48bb23b3be31e329deb3f1858f1f1df86aea3d70cb5c8578bfe5261c"}, - {file = "lazy_object_proxy-1.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:71d9ae8a82203511a6f60ca5a1b9f8ad201cac0fc75038b2dc5fa519589c9288"}, - {file = "lazy_object_proxy-1.8.0-cp39-cp39-win32.whl", hash = "sha256:8f6ce2118a90efa7f62dd38c7dbfffd42f468b180287b748626293bf12ed468f"}, - {file = "lazy_object_proxy-1.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:eac3a9a5ef13b332c059772fd40b4b1c3d45a3a2b05e33a361dee48e54a4dad0"}, - {file = "lazy_object_proxy-1.8.0-pp37-pypy37_pp73-any.whl", hash = "sha256:ae032743794fba4d171b5b67310d69176287b5bf82a21f588282406a79498891"}, - {file = "lazy_object_proxy-1.8.0-pp38-pypy38_pp73-any.whl", hash = "sha256:7e1561626c49cb394268edd00501b289053a652ed762c58e1081224c8d881cec"}, - {file = "lazy_object_proxy-1.8.0-pp39-pypy39_pp73-any.whl", hash = "sha256:ce58b2b3734c73e68f0e30e4e725264d4d6be95818ec0a0be4bb6bf9a7e79aa8"}, + {file = "lazy-object-proxy-1.9.0.tar.gz", hash = "sha256:659fb5809fa4629b8a1ac5106f669cfc7bef26fbb389dda53b3e010d1ac4ebae"}, + {file = "lazy_object_proxy-1.9.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b40387277b0ed2d0602b8293b94d7257e17d1479e257b4de114ea11a8cb7f2d7"}, + {file = "lazy_object_proxy-1.9.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8c6cfb338b133fbdbc5cfaa10fe3c6aeea827db80c978dbd13bc9dd8526b7d4"}, + {file = "lazy_object_proxy-1.9.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:721532711daa7db0d8b779b0bb0318fa87af1c10d7fe5e52ef30f8eff254d0cd"}, + {file = "lazy_object_proxy-1.9.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:66a3de4a3ec06cd8af3f61b8e1ec67614fbb7c995d02fa224813cb7afefee701"}, + {file = "lazy_object_proxy-1.9.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1aa3de4088c89a1b69f8ec0dcc169aa725b0ff017899ac568fe44ddc1396df46"}, + {file = "lazy_object_proxy-1.9.0-cp310-cp310-win32.whl", hash = "sha256:f0705c376533ed2a9e5e97aacdbfe04cecd71e0aa84c7c0595d02ef93b6e4455"}, + {file = "lazy_object_proxy-1.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:ea806fd4c37bf7e7ad82537b0757999264d5f70c45468447bb2b91afdbe73a6e"}, + {file = "lazy_object_proxy-1.9.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:946d27deaff6cf8452ed0dba83ba38839a87f4f7a9732e8f9fd4107b21e6ff07"}, + {file = "lazy_object_proxy-1.9.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79a31b086e7e68b24b99b23d57723ef7e2c6d81ed21007b6281ebcd1688acb0a"}, + {file = "lazy_object_proxy-1.9.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f699ac1c768270c9e384e4cbd268d6e67aebcfae6cd623b4d7c3bfde5a35db59"}, + {file = "lazy_object_proxy-1.9.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:bfb38f9ffb53b942f2b5954e0f610f1e721ccebe9cce9025a38c8ccf4a5183a4"}, + {file = "lazy_object_proxy-1.9.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:189bbd5d41ae7a498397287c408617fe5c48633e7755287b21d741f7db2706a9"}, + {file = "lazy_object_proxy-1.9.0-cp311-cp311-win32.whl", hash = "sha256:81fc4d08b062b535d95c9ea70dbe8a335c45c04029878e62d744bdced5141586"}, + {file = "lazy_object_proxy-1.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:f2457189d8257dd41ae9b434ba33298aec198e30adf2dcdaaa3a28b9994f6adb"}, + {file = "lazy_object_proxy-1.9.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:d9e25ef10a39e8afe59a5c348a4dbf29b4868ab76269f81ce1674494e2565a6e"}, + {file = "lazy_object_proxy-1.9.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cbf9b082426036e19c6924a9ce90c740a9861e2bdc27a4834fd0a910742ac1e8"}, + {file = "lazy_object_proxy-1.9.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f5fa4a61ce2438267163891961cfd5e32ec97a2c444e5b842d574251ade27d2"}, + {file = "lazy_object_proxy-1.9.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:8fa02eaab317b1e9e03f69aab1f91e120e7899b392c4fc19807a8278a07a97e8"}, + {file = "lazy_object_proxy-1.9.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:e7c21c95cae3c05c14aafffe2865bbd5e377cfc1348c4f7751d9dc9a48ca4bda"}, + {file = "lazy_object_proxy-1.9.0-cp37-cp37m-win32.whl", hash = "sha256:f12ad7126ae0c98d601a7ee504c1122bcef553d1d5e0c3bfa77b16b3968d2734"}, + {file = "lazy_object_proxy-1.9.0-cp37-cp37m-win_amd64.whl", hash = "sha256:edd20c5a55acb67c7ed471fa2b5fb66cb17f61430b7a6b9c3b4a1e40293b1671"}, + {file = "lazy_object_proxy-1.9.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2d0daa332786cf3bb49e10dc6a17a52f6a8f9601b4cf5c295a4f85854d61de63"}, + {file = "lazy_object_proxy-1.9.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cd077f3d04a58e83d04b20e334f678c2b0ff9879b9375ed107d5d07ff160171"}, + {file = "lazy_object_proxy-1.9.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:660c94ea760b3ce47d1855a30984c78327500493d396eac4dfd8bd82041b22be"}, + {file = "lazy_object_proxy-1.9.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:212774e4dfa851e74d393a2370871e174d7ff0ebc980907723bb67d25c8a7c30"}, + {file = "lazy_object_proxy-1.9.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:f0117049dd1d5635bbff65444496c90e0baa48ea405125c088e93d9cf4525b11"}, + {file = "lazy_object_proxy-1.9.0-cp38-cp38-win32.whl", hash = "sha256:0a891e4e41b54fd5b8313b96399f8b0e173bbbfc03c7631f01efbe29bb0bcf82"}, + {file = "lazy_object_proxy-1.9.0-cp38-cp38-win_amd64.whl", hash = "sha256:9990d8e71b9f6488e91ad25f322898c136b008d87bf852ff65391b004da5e17b"}, + {file = "lazy_object_proxy-1.9.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9e7551208b2aded9c1447453ee366f1c4070602b3d932ace044715d89666899b"}, + {file = "lazy_object_proxy-1.9.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f83ac4d83ef0ab017683d715ed356e30dd48a93746309c8f3517e1287523ef4"}, + {file = "lazy_object_proxy-1.9.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7322c3d6f1766d4ef1e51a465f47955f1e8123caee67dd641e67d539a534d006"}, + {file = "lazy_object_proxy-1.9.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:18b78ec83edbbeb69efdc0e9c1cb41a3b1b1ed11ddd8ded602464c3fc6020494"}, + {file = "lazy_object_proxy-1.9.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:09763491ce220c0299688940f8dc2c5d05fd1f45af1e42e636b2e8b2303e4382"}, + {file = "lazy_object_proxy-1.9.0-cp39-cp39-win32.whl", hash = "sha256:9090d8e53235aa280fc9239a86ae3ea8ac58eff66a705fa6aa2ec4968b95c821"}, + {file = "lazy_object_proxy-1.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:db1c1722726f47e10e0b5fdbf15ac3b8adb58c091d12b3ab713965795036985f"}, ] [[package]] name = "macholib" version = "1.16.2" description = "Mach-O header analysis and editing" -category = "main" +category = "dev" optional = false python-versions = "*" files = [ @@ -415,52 +506,62 @@ altgraph = ">=0.17" [[package]] name = "markupsafe" -version = "2.1.1" +version = "2.1.2" description = "Safely add untrusted strings to HTML/XML markup." category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:86b1f75c4e7c2ac2ccdaec2b9022845dbb81880ca318bb7a0a01fbf7813e3812"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f121a1420d4e173a5d96e47e9a0c0dcff965afdf1626d28de1460815f7c4ee7a"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a49907dd8420c5685cfa064a1335b6754b74541bbb3706c259c02ed65b644b3e"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10c1bfff05d95783da83491be968e8fe789263689c02724e0c691933c52994f5"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b7bd98b796e2b6553da7225aeb61f447f80a1ca64f41d83612e6139ca5213aa4"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b09bf97215625a311f669476f44b8b318b075847b49316d3e28c08e41a7a573f"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:694deca8d702d5db21ec83983ce0bb4b26a578e71fbdbd4fdcd387daa90e4d5e"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:efc1913fd2ca4f334418481c7e595c00aad186563bbc1ec76067848c7ca0a933"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-win32.whl", hash = "sha256:4a33dea2b688b3190ee12bd7cfa29d39c9ed176bda40bfa11099a3ce5d3a7ac6"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:dda30ba7e87fbbb7eab1ec9f58678558fd9a6b8b853530e176eabd064da81417"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:671cd1187ed5e62818414afe79ed29da836dde67166a9fac6d435873c44fdd02"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3799351e2336dc91ea70b034983ee71cf2f9533cdff7c14c90ea126bfd95d65a"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e72591e9ecd94d7feb70c1cbd7be7b3ebea3f548870aa91e2732960fa4d57a37"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6fbf47b5d3728c6aea2abb0589b5d30459e369baa772e0f37a0320185e87c980"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d5ee4f386140395a2c818d149221149c54849dfcfcb9f1debfe07a8b8bd63f9a"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:bcb3ed405ed3222f9904899563d6fc492ff75cce56cba05e32eff40e6acbeaa3"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:e1c0b87e09fa55a220f058d1d49d3fb8df88fbfab58558f1198e08c1e1de842a"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-win32.whl", hash = "sha256:8dc1c72a69aa7e082593c4a203dcf94ddb74bb5c8a731e4e1eb68d031e8498ff"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:97a68e6ada378df82bc9f16b800ab77cbf4b2fada0081794318520138c088e4a"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e8c843bbcda3a2f1e3c2ab25913c80a3c5376cd00c6e8c4a86a89a28c8dc5452"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0212a68688482dc52b2d45013df70d169f542b7394fc744c02a57374a4207003"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e576a51ad59e4bfaac456023a78f6b5e6e7651dcd383bcc3e18d06f9b55d6d1"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b9fe39a2ccc108a4accc2676e77da025ce383c108593d65cc909add5c3bd601"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:96e37a3dc86e80bf81758c152fe66dbf60ed5eca3d26305edf01892257049925"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6d0072fea50feec76a4c418096652f2c3238eaa014b2f94aeb1d56a66b41403f"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:089cf3dbf0cd6c100f02945abeb18484bd1ee57a079aefd52cffd17fba910b88"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6a074d34ee7a5ce3effbc526b7083ec9731bb3cbf921bbe1d3005d4d2bdb3a63"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-win32.whl", hash = "sha256:421be9fbf0ffe9ffd7a378aafebbf6f4602d564d34be190fc19a193232fd12b1"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:fc7b548b17d238737688817ab67deebb30e8073c95749d55538ed473130ec0c7"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e04e26803c9c3851c931eac40c695602c6295b8d432cbe78609649ad9bd2da8a"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b87db4360013327109564f0e591bd2a3b318547bcef31b468a92ee504d07ae4f"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99a2a507ed3ac881b975a2976d59f38c19386d128e7a9a18b7df6fff1fd4c1d6"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56442863ed2b06d19c37f94d999035e15ee982988920e12a5b4ba29b62ad1f77"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3ce11ee3f23f79dbd06fb3d63e2f6af7b12db1d46932fe7bd8afa259a5996603"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:33b74d289bd2f5e527beadcaa3f401e0df0a89927c1559c8566c066fa4248ab7"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:43093fb83d8343aac0b1baa75516da6092f58f41200907ef92448ecab8825135"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8e3dcf21f367459434c18e71b2a9532d96547aef8a871872a5bd69a715c15f96"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-win32.whl", hash = "sha256:d4306c36ca495956b6d568d276ac11fdd9c30a36f1b6eb928070dc5360b22e1c"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:46d00d6cfecdde84d40e572d63735ef81423ad31184100411e6e3388d405e247"}, - {file = "MarkupSafe-2.1.1.tar.gz", hash = "sha256:7f91197cc9e48f989d12e4e6fbc46495c446636dfc81b9ccf50bb0ec74b91d4b"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:665a36ae6f8f20a4676b53224e33d456a6f5a72657d9c83c2aa00765072f31f7"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:340bea174e9761308703ae988e982005aedf427de816d1afe98147668cc03036"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22152d00bf4a9c7c83960521fc558f55a1adbc0631fbb00a9471e097b19d72e1"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28057e985dace2f478e042eaa15606c7efccb700797660629da387eb289b9323"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca244fa73f50a800cf8c3ebf7fd93149ec37f5cb9596aa8873ae2c1d23498601"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d9d971ec1e79906046aa3ca266de79eac42f1dbf3612a05dc9368125952bd1a1"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:7e007132af78ea9df29495dbf7b5824cb71648d7133cf7848a2a5dd00d36f9ff"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7313ce6a199651c4ed9d7e4cfb4aa56fe923b1adf9af3b420ee14e6d9a73df65"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-win32.whl", hash = "sha256:c4a549890a45f57f1ebf99c067a4ad0cb423a05544accaf2b065246827ed9603"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:835fb5e38fd89328e9c81067fd642b3593c33e1e17e2fdbf77f5676abb14a156"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2ec4f2d48ae59bbb9d1f9d7efb9236ab81429a764dedca114f5fdabbc3788013"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:608e7073dfa9e38a85d38474c082d4281f4ce276ac0010224eaba11e929dd53a"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:65608c35bfb8a76763f37036547f7adfd09270fbdbf96608be2bead319728fcd"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2bfb563d0211ce16b63c7cb9395d2c682a23187f54c3d79bfec33e6705473c6"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:da25303d91526aac3672ee6d49a2f3db2d9502a4a60b55519feb1a4c7714e07d"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:9cad97ab29dfc3f0249b483412c85c8ef4766d96cdf9dcf5a1e3caa3f3661cf1"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:085fd3201e7b12809f9e6e9bc1e5c96a368c8523fad5afb02afe3c051ae4afcc"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1bea30e9bf331f3fef67e0a3877b2288593c98a21ccb2cf29b74c581a4eb3af0"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-win32.whl", hash = "sha256:7df70907e00c970c60b9ef2938d894a9381f38e6b9db73c5be35e59d92e06625"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:e55e40ff0cc8cc5c07996915ad367fa47da6b3fc091fdadca7f5403239c5fec3"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a6e40afa7f45939ca356f348c8e23048e02cb109ced1eb8420961b2f40fb373a"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf877ab4ed6e302ec1d04952ca358b381a882fbd9d1b07cccbfd61783561f98a"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63ba06c9941e46fa389d389644e2d8225e0e3e5ebcc4ff1ea8506dce646f8c8a"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f1cd098434e83e656abf198f103a8207a8187c0fc110306691a2e94a78d0abb2"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:55f44b440d491028addb3b88f72207d71eeebfb7b5dbf0643f7c023ae1fba619"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:a6f2fcca746e8d5910e18782f976489939d54a91f9411c32051b4aab2bd7c513"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:0b462104ba25f1ac006fdab8b6a01ebbfbce9ed37fd37fd4acd70c67c973e460"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-win32.whl", hash = "sha256:7668b52e102d0ed87cb082380a7e2e1e78737ddecdde129acadb0eccc5423859"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-win_amd64.whl", hash = "sha256:6d6607f98fcf17e534162f0709aaad3ab7a96032723d8ac8750ffe17ae5a0666"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:a806db027852538d2ad7555b203300173dd1b77ba116de92da9afbc3a3be3eed"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a4abaec6ca3ad8660690236d11bfe28dfd707778e2442b45addd2f086d6ef094"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f03a532d7dee1bed20bc4884194a16160a2de9ffc6354b3878ec9682bb623c54"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4cf06cdc1dda95223e9d2d3c58d3b178aa5dacb35ee7e3bbac10e4e1faacb419"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:22731d79ed2eb25059ae3df1dfc9cb1546691cc41f4e3130fe6bfbc3ecbbecfa"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:f8ffb705ffcf5ddd0e80b65ddf7bed7ee4f5a441ea7d3419e861a12eaf41af58"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8db032bf0ce9022a8e41a22598eefc802314e81b879ae093f36ce9ddf39ab1ba"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2298c859cfc5463f1b64bd55cb3e602528db6fa0f3cfd568d3605c50678f8f03"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-win32.whl", hash = "sha256:50c42830a633fa0cf9e7d27664637532791bfc31c731a87b202d2d8ac40c3ea2"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-win_amd64.whl", hash = "sha256:bb06feb762bade6bf3c8b844462274db0c76acc95c52abe8dbed28ae3d44a147"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:99625a92da8229df6d44335e6fcc558a5037dd0a760e11d84be2260e6f37002f"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8bca7e26c1dd751236cfb0c6c72d4ad61d986e9a41bbf76cb445f69488b2a2bd"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40627dcf047dadb22cd25ea7ecfe9cbf3bbbad0482ee5920b582f3809c97654f"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40dfd3fefbef579ee058f139733ac336312663c6706d1163b82b3003fb1925c4"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:090376d812fb6ac5f171e5938e82e7f2d7adc2b629101cec0db8b267815c85e2"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:2e7821bffe00aa6bd07a23913b7f4e01328c3d5cc0b40b36c0bd81d362faeb65"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:c0a33bc9f02c2b17c3ea382f91b4db0e6cde90b63b296422a939886a7a80de1c"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:b8526c6d437855442cdd3d87eede9c425c4445ea011ca38d937db299382e6fa3"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-win32.whl", hash = "sha256:137678c63c977754abe9086a3ec011e8fd985ab90631145dfb9294ad09c102a7"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-win_amd64.whl", hash = "sha256:0576fe974b40a400449768941d5d0858cc624e3249dfd1e0c33674e5c7ca7aed"}, + {file = "MarkupSafe-2.1.2.tar.gz", hash = "sha256:abcabc8c2b26036d62d4c746381a6f7cf60aafcc653198ad678306986b09450d"}, ] [[package]] @@ -489,36 +590,33 @@ files = [ [[package]] name = "packaging" -version = "21.3" +version = "23.0" description = "Core utilities for Python packages" -category = "main" +category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" files = [ - {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, - {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, + {file = "packaging-23.0-py3-none-any.whl", hash = "sha256:714ac14496c3e68c99c29b00845f7a2b85f3bb6f1078fd9f72fd20f0570002b2"}, + {file = "packaging-23.0.tar.gz", hash = "sha256:b6ad297f8907de0fa2fe1ccbd26fdaf387f5f47c7275fedf8cce89f99446cf97"}, ] -[package.dependencies] -pyparsing = ">=2.0.2,<3.0.5 || >3.0.5" - [[package]] name = "pathspec" -version = "0.9.0" +version = "0.10.3" description = "Utility library for gitignore style pattern matching of file paths." category = "dev" optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" +python-versions = ">=3.7" files = [ - {file = "pathspec-0.9.0-py2.py3-none-any.whl", hash = "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a"}, - {file = "pathspec-0.9.0.tar.gz", hash = "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1"}, + {file = "pathspec-0.10.3-py3-none-any.whl", hash = "sha256:3c95343af8b756205e2aba76e843ba9520a24dd84f68c22b9f93251507509dd6"}, + {file = "pathspec-0.10.3.tar.gz", hash = "sha256:56200de4077d9d0791465aa9095a01d421861e405b5096955051deefd697d6f6"}, ] [[package]] name = "pefile" version = "2022.5.30" description = "Python PE parsing module" -category = "main" +category = "dev" optional = false python-versions = ">=3.6.0" files = [ @@ -530,97 +628,116 @@ future = "*" [[package]] name = "pillow" -version = "9.2.0" +version = "9.4.0" description = "Python Imaging Library (Fork)" category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "Pillow-9.2.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:a9c9bc489f8ab30906d7a85afac4b4944a572a7432e00698a7239f44a44e6efb"}, - {file = "Pillow-9.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:510cef4a3f401c246cfd8227b300828715dd055463cdca6176c2e4036df8bd4f"}, - {file = "Pillow-9.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7888310f6214f19ab2b6df90f3f06afa3df7ef7355fc025e78a3044737fab1f5"}, - {file = "Pillow-9.2.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:831e648102c82f152e14c1a0938689dbb22480c548c8d4b8b248b3e50967b88c"}, - {file = "Pillow-9.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1cc1d2451e8a3b4bfdb9caf745b58e6c7a77d2e469159b0d527a4554d73694d1"}, - {file = "Pillow-9.2.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:136659638f61a251e8ed3b331fc6ccd124590eeff539de57c5f80ef3a9594e58"}, - {file = "Pillow-9.2.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:6e8c66f70fb539301e064f6478d7453e820d8a2c631da948a23384865cd95544"}, - {file = "Pillow-9.2.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:37ff6b522a26d0538b753f0b4e8e164fdada12db6c6f00f62145d732d8a3152e"}, - {file = "Pillow-9.2.0-cp310-cp310-win32.whl", hash = "sha256:c79698d4cd9318d9481d89a77e2d3fcaeff5486be641e60a4b49f3d2ecca4e28"}, - {file = "Pillow-9.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:254164c57bab4b459f14c64e93df11eff5ded575192c294a0c49270f22c5d93d"}, - {file = "Pillow-9.2.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:adabc0bce035467fb537ef3e5e74f2847c8af217ee0be0455d4fec8adc0462fc"}, - {file = "Pillow-9.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:336b9036127eab855beec9662ac3ea13a4544a523ae273cbf108b228ecac8437"}, - {file = "Pillow-9.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50dff9cc21826d2977ef2d2a205504034e3a4563ca6f5db739b0d1026658e004"}, - {file = "Pillow-9.2.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cb6259196a589123d755380b65127ddc60f4c64b21fc3bb46ce3a6ea663659b0"}, - {file = "Pillow-9.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b0554af24df2bf96618dac71ddada02420f946be943b181108cac55a7a2dcd4"}, - {file = "Pillow-9.2.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:15928f824870535c85dbf949c09d6ae7d3d6ac2d6efec80f3227f73eefba741c"}, - {file = "Pillow-9.2.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:bdd0de2d64688ecae88dd8935012c4a72681e5df632af903a1dca8c5e7aa871a"}, - {file = "Pillow-9.2.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d5b87da55a08acb586bad5c3aa3b86505f559b84f39035b233d5bf844b0834b1"}, - {file = "Pillow-9.2.0-cp311-cp311-win32.whl", hash = "sha256:b6d5e92df2b77665e07ddb2e4dbd6d644b78e4c0d2e9272a852627cdba0d75cf"}, - {file = "Pillow-9.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:6bf088c1ce160f50ea40764f825ec9b72ed9da25346216b91361eef8ad1b8f8c"}, - {file = "Pillow-9.2.0-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:2c58b24e3a63efd22554c676d81b0e57f80e0a7d3a5874a7e14ce90ec40d3069"}, - {file = "Pillow-9.2.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eef7592281f7c174d3d6cbfbb7ee5984a671fcd77e3fc78e973d492e9bf0eb3f"}, - {file = "Pillow-9.2.0-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dcd7b9c7139dc8258d164b55696ecd16c04607f1cc33ba7af86613881ffe4ac8"}, - {file = "Pillow-9.2.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a138441e95562b3c078746a22f8fca8ff1c22c014f856278bdbdd89ca36cff1b"}, - {file = "Pillow-9.2.0-cp37-cp37m-manylinux_2_28_aarch64.whl", hash = "sha256:93689632949aff41199090eff5474f3990b6823404e45d66a5d44304e9cdc467"}, - {file = "Pillow-9.2.0-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:f3fac744f9b540148fa7715a435d2283b71f68bfb6d4aae24482a890aed18b59"}, - {file = "Pillow-9.2.0-cp37-cp37m-win32.whl", hash = "sha256:fa768eff5f9f958270b081bb33581b4b569faabf8774726b283edb06617101dc"}, - {file = "Pillow-9.2.0-cp37-cp37m-win_amd64.whl", hash = "sha256:69bd1a15d7ba3694631e00df8de65a8cb031911ca11f44929c97fe05eb9b6c1d"}, - {file = "Pillow-9.2.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:030e3460861488e249731c3e7ab59b07c7853838ff3b8e16aac9561bb345da14"}, - {file = "Pillow-9.2.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:74a04183e6e64930b667d321524e3c5361094bb4af9083db5c301db64cd341f3"}, - {file = "Pillow-9.2.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d33a11f601213dcd5718109c09a52c2a1c893e7461f0be2d6febc2879ec2402"}, - {file = "Pillow-9.2.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1fd6f5e3c0e4697fa7eb45b6e93996299f3feee73a3175fa451f49a74d092b9f"}, - {file = "Pillow-9.2.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a647c0d4478b995c5e54615a2e5360ccedd2f85e70ab57fbe817ca613d5e63b8"}, - {file = "Pillow-9.2.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:4134d3f1ba5f15027ff5c04296f13328fecd46921424084516bdb1b2548e66ff"}, - {file = "Pillow-9.2.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:bc431b065722a5ad1dfb4df354fb9333b7a582a5ee39a90e6ffff688d72f27a1"}, - {file = "Pillow-9.2.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:1536ad017a9f789430fb6b8be8bf99d2f214c76502becc196c6f2d9a75b01b76"}, - {file = "Pillow-9.2.0-cp38-cp38-win32.whl", hash = "sha256:2ad0d4df0f5ef2247e27fc790d5c9b5a0af8ade9ba340db4a73bb1a4a3e5fb4f"}, - {file = "Pillow-9.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:ec52c351b35ca269cb1f8069d610fc45c5bd38c3e91f9ab4cbbf0aebc136d9c8"}, - {file = "Pillow-9.2.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:0ed2c4ef2451de908c90436d6e8092e13a43992f1860275b4d8082667fbb2ffc"}, - {file = "Pillow-9.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4ad2f835e0ad81d1689f1b7e3fbac7b01bb8777d5a985c8962bedee0cc6d43da"}, - {file = "Pillow-9.2.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ea98f633d45f7e815db648fd7ff0f19e328302ac36427343e4432c84432e7ff4"}, - {file = "Pillow-9.2.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7761afe0126d046974a01e030ae7529ed0ca6a196de3ec6937c11df0df1bc91c"}, - {file = "Pillow-9.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a54614049a18a2d6fe156e68e188da02a046a4a93cf24f373bffd977e943421"}, - {file = "Pillow-9.2.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:5aed7dde98403cd91d86a1115c78d8145c83078e864c1de1064f52e6feb61b20"}, - {file = "Pillow-9.2.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:13b725463f32df1bfeacbf3dd197fb358ae8ebcd8c5548faa75126ea425ccb60"}, - {file = "Pillow-9.2.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:808add66ea764ed97d44dda1ac4f2cfec4c1867d9efb16a33d158be79f32b8a4"}, - {file = "Pillow-9.2.0-cp39-cp39-win32.whl", hash = "sha256:337a74fd2f291c607d220c793a8135273c4c2ab001b03e601c36766005f36885"}, - {file = "Pillow-9.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:fac2d65901fb0fdf20363fbd345c01958a742f2dc62a8dd4495af66e3ff502a4"}, - {file = "Pillow-9.2.0-pp37-pypy37_pp73-macosx_10_10_x86_64.whl", hash = "sha256:ad2277b185ebce47a63f4dc6302e30f05762b688f8dc3de55dbae4651872cdf3"}, - {file = "Pillow-9.2.0-pp37-pypy37_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7c7b502bc34f6e32ba022b4a209638f9e097d7a9098104ae420eb8186217ebbb"}, - {file = "Pillow-9.2.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d1f14f5f691f55e1b47f824ca4fdcb4b19b4323fe43cc7bb105988cad7496be"}, - {file = "Pillow-9.2.0-pp37-pypy37_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:dfe4c1fedfde4e2fbc009d5ad420647f7730d719786388b7de0999bf32c0d9fd"}, - {file = "Pillow-9.2.0-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:f07f1f00e22b231dd3d9b9208692042e29792d6bd4f6639415d2f23158a80013"}, - {file = "Pillow-9.2.0-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1802f34298f5ba11d55e5bb09c31997dc0c6aed919658dfdf0198a2fe75d5490"}, - {file = "Pillow-9.2.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17d4cafe22f050b46d983b71c707162d63d796a1235cdf8b9d7a112e97b15bac"}, - {file = "Pillow-9.2.0-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:96b5e6874431df16aee0c1ba237574cb6dff1dcb173798faa6a9d8b399a05d0e"}, - {file = "Pillow-9.2.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:0030fdbd926fb85844b8b92e2f9449ba89607231d3dd597a21ae72dc7fe26927"}, - {file = "Pillow-9.2.0.tar.gz", hash = "sha256:75e636fd3e0fb872693f23ccb8a5ff2cd578801251f3a4f6854c6a5d437d3c04"}, + {file = "Pillow-9.4.0-1-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:1b4b4e9dda4f4e4c4e6896f93e84a8f0bcca3b059de9ddf67dac3c334b1195e1"}, + {file = "Pillow-9.4.0-1-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:fb5c1ad6bad98c57482236a21bf985ab0ef42bd51f7ad4e4538e89a997624e12"}, + {file = "Pillow-9.4.0-1-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:f0caf4a5dcf610d96c3bd32932bfac8aee61c96e60481c2a0ea58da435e25acd"}, + {file = "Pillow-9.4.0-1-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:3f4cc516e0b264c8d4ccd6b6cbc69a07c6d582d8337df79be1e15a5056b258c9"}, + {file = "Pillow-9.4.0-1-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:b8c2f6eb0df979ee99433d8b3f6d193d9590f735cf12274c108bd954e30ca858"}, + {file = "Pillow-9.4.0-1-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:b70756ec9417c34e097f987b4d8c510975216ad26ba6e57ccb53bc758f490dab"}, + {file = "Pillow-9.4.0-1-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:43521ce2c4b865d385e78579a082b6ad1166ebed2b1a2293c3be1d68dd7ca3b9"}, + {file = "Pillow-9.4.0-2-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:9d9a62576b68cd90f7075876f4e8444487db5eeea0e4df3ba298ee38a8d067b0"}, + {file = "Pillow-9.4.0-2-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:87708d78a14d56a990fbf4f9cb350b7d89ee8988705e58e39bdf4d82c149210f"}, + {file = "Pillow-9.4.0-2-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:8a2b5874d17e72dfb80d917213abd55d7e1ed2479f38f001f264f7ce7bae757c"}, + {file = "Pillow-9.4.0-2-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:83125753a60cfc8c412de5896d10a0a405e0bd88d0470ad82e0869ddf0cb3848"}, + {file = "Pillow-9.4.0-2-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:9e5f94742033898bfe84c93c831a6f552bb629448d4072dd312306bab3bd96f1"}, + {file = "Pillow-9.4.0-2-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:013016af6b3a12a2f40b704677f8b51f72cb007dac785a9933d5c86a72a7fe33"}, + {file = "Pillow-9.4.0-2-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:99d92d148dd03fd19d16175b6d355cc1b01faf80dae93c6c3eb4163709edc0a9"}, + {file = "Pillow-9.4.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:2968c58feca624bb6c8502f9564dd187d0e1389964898f5e9e1fbc8533169157"}, + {file = "Pillow-9.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c5c1362c14aee73f50143d74389b2c158707b4abce2cb055b7ad37ce60738d47"}, + {file = "Pillow-9.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd752c5ff1b4a870b7661234694f24b1d2b9076b8bf337321a814c612665f343"}, + {file = "Pillow-9.4.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9a3049a10261d7f2b6514d35bbb7a4dfc3ece4c4de14ef5876c4b7a23a0e566d"}, + {file = "Pillow-9.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:16a8df99701f9095bea8a6c4b3197da105df6f74e6176c5b410bc2df2fd29a57"}, + {file = "Pillow-9.4.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:94cdff45173b1919350601f82d61365e792895e3c3a3443cf99819e6fbf717a5"}, + {file = "Pillow-9.4.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:ed3e4b4e1e6de75fdc16d3259098de7c6571b1a6cc863b1a49e7d3d53e036070"}, + {file = "Pillow-9.4.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d5b2f8a31bd43e0f18172d8ac82347c8f37ef3e0b414431157718aa234991b28"}, + {file = "Pillow-9.4.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:09b89ddc95c248ee788328528e6a2996e09eaccddeeb82a5356e92645733be35"}, + {file = "Pillow-9.4.0-cp310-cp310-win32.whl", hash = "sha256:f09598b416ba39a8f489c124447b007fe865f786a89dbfa48bb5cf395693132a"}, + {file = "Pillow-9.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:f6e78171be3fb7941f9910ea15b4b14ec27725865a73c15277bc39f5ca4f8391"}, + {file = "Pillow-9.4.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:3fa1284762aacca6dc97474ee9c16f83990b8eeb6697f2ba17140d54b453e133"}, + {file = "Pillow-9.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:eaef5d2de3c7e9b21f1e762f289d17b726c2239a42b11e25446abf82b26ac132"}, + {file = "Pillow-9.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a4dfdae195335abb4e89cc9762b2edc524f3c6e80d647a9a81bf81e17e3fb6f0"}, + {file = "Pillow-9.4.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6abfb51a82e919e3933eb137e17c4ae9c0475a25508ea88993bb59faf82f3b35"}, + {file = "Pillow-9.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:451f10ef963918e65b8869e17d67db5e2f4ab40e716ee6ce7129b0cde2876eab"}, + {file = "Pillow-9.4.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:6663977496d616b618b6cfa43ec86e479ee62b942e1da76a2c3daa1c75933ef4"}, + {file = "Pillow-9.4.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:60e7da3a3ad1812c128750fc1bc14a7ceeb8d29f77e0a2356a8fb2aa8925287d"}, + {file = "Pillow-9.4.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:19005a8e58b7c1796bc0167862b1f54a64d3b44ee5d48152b06bb861458bc0f8"}, + {file = "Pillow-9.4.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f715c32e774a60a337b2bb8ad9839b4abf75b267a0f18806f6f4f5f1688c4b5a"}, + {file = "Pillow-9.4.0-cp311-cp311-win32.whl", hash = "sha256:b222090c455d6d1a64e6b7bb5f4035c4dff479e22455c9eaa1bdd4c75b52c80c"}, + {file = "Pillow-9.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:ba6612b6548220ff5e9df85261bddc811a057b0b465a1226b39bfb8550616aee"}, + {file = "Pillow-9.4.0-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:5f532a2ad4d174eb73494e7397988e22bf427f91acc8e6ebf5bb10597b49c493"}, + {file = "Pillow-9.4.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5dd5a9c3091a0f414a963d427f920368e2b6a4c2f7527fdd82cde8ef0bc7a327"}, + {file = "Pillow-9.4.0-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ef21af928e807f10bf4141cad4746eee692a0dd3ff56cfb25fce076ec3cc8abe"}, + {file = "Pillow-9.4.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:847b114580c5cc9ebaf216dd8c8dbc6b00a3b7ab0131e173d7120e6deade1f57"}, + {file = "Pillow-9.4.0-cp37-cp37m-manylinux_2_28_aarch64.whl", hash = "sha256:653d7fb2df65efefbcbf81ef5fe5e5be931f1ee4332c2893ca638c9b11a409c4"}, + {file = "Pillow-9.4.0-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:46f39cab8bbf4a384ba7cb0bc8bae7b7062b6a11cfac1ca4bc144dea90d4a9f5"}, + {file = "Pillow-9.4.0-cp37-cp37m-win32.whl", hash = "sha256:7ac7594397698f77bce84382929747130765f66406dc2cd8b4ab4da68ade4c6e"}, + {file = "Pillow-9.4.0-cp37-cp37m-win_amd64.whl", hash = "sha256:46c259e87199041583658457372a183636ae8cd56dbf3f0755e0f376a7f9d0e6"}, + {file = "Pillow-9.4.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:0e51f608da093e5d9038c592b5b575cadc12fd748af1479b5e858045fff955a9"}, + {file = "Pillow-9.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:765cb54c0b8724a7c12c55146ae4647e0274a839fb6de7bcba841e04298e1011"}, + {file = "Pillow-9.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:519e14e2c49fcf7616d6d2cfc5c70adae95682ae20f0395e9280db85e8d6c4df"}, + {file = "Pillow-9.4.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d197df5489004db87d90b918033edbeee0bd6df3848a204bca3ff0a903bef837"}, + {file = "Pillow-9.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0845adc64fe9886db00f5ab68c4a8cd933ab749a87747555cec1c95acea64b0b"}, + {file = "Pillow-9.4.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:e1339790c083c5a4de48f688b4841f18df839eb3c9584a770cbd818b33e26d5d"}, + {file = "Pillow-9.4.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:a96e6e23f2b79433390273eaf8cc94fec9c6370842e577ab10dabdcc7ea0a66b"}, + {file = "Pillow-9.4.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:7cfc287da09f9d2a7ec146ee4d72d6ea1342e770d975e49a8621bf54eaa8f30f"}, + {file = "Pillow-9.4.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d7081c084ceb58278dd3cf81f836bc818978c0ccc770cbbb202125ddabec6628"}, + {file = "Pillow-9.4.0-cp38-cp38-win32.whl", hash = "sha256:df41112ccce5d47770a0c13651479fbcd8793f34232a2dd9faeccb75eb5d0d0d"}, + {file = "Pillow-9.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:7a21222644ab69ddd9967cfe6f2bb420b460dae4289c9d40ff9a4896e7c35c9a"}, + {file = "Pillow-9.4.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:0f3269304c1a7ce82f1759c12ce731ef9b6e95b6df829dccd9fe42912cc48569"}, + {file = "Pillow-9.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:cb362e3b0976dc994857391b776ddaa8c13c28a16f80ac6522c23d5257156bed"}, + {file = "Pillow-9.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a2e0f87144fcbbe54297cae708c5e7f9da21a4646523456b00cc956bd4c65815"}, + {file = "Pillow-9.4.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:28676836c7796805914b76b1837a40f76827ee0d5398f72f7dcc634bae7c6264"}, + {file = "Pillow-9.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0884ba7b515163a1a05440a138adeb722b8a6ae2c2b33aea93ea3118dd3a899e"}, + {file = "Pillow-9.4.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:53dcb50fbdc3fb2c55431a9b30caeb2f7027fcd2aeb501459464f0214200a503"}, + {file = "Pillow-9.4.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:e8c5cf126889a4de385c02a2c3d3aba4b00f70234bfddae82a5eaa3ee6d5e3e6"}, + {file = "Pillow-9.4.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:6c6b1389ed66cdd174d040105123a5a1bc91d0aa7059c7261d20e583b6d8cbd2"}, + {file = "Pillow-9.4.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:0dd4c681b82214b36273c18ca7ee87065a50e013112eea7d78c7a1b89a739153"}, + {file = "Pillow-9.4.0-cp39-cp39-win32.whl", hash = "sha256:6d9dfb9959a3b0039ee06c1a1a90dc23bac3b430842dcb97908ddde05870601c"}, + {file = "Pillow-9.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:54614444887e0d3043557d9dbc697dbb16cfb5a35d672b7a0fcc1ed0cf1c600b"}, + {file = "Pillow-9.4.0-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:b9b752ab91e78234941e44abdecc07f1f0d8f51fb62941d32995b8161f68cfe5"}, + {file = "Pillow-9.4.0-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d3b56206244dc8711f7e8b7d6cad4663917cd5b2d950799425076681e8766286"}, + {file = "Pillow-9.4.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aabdab8ec1e7ca7f1434d042bf8b1e92056245fb179790dc97ed040361f16bfd"}, + {file = "Pillow-9.4.0-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:db74f5562c09953b2c5f8ec4b7dfd3f5421f31811e97d1dbc0a7c93d6e3a24df"}, + {file = "Pillow-9.4.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:e9d7747847c53a16a729b6ee5e737cf170f7a16611c143d95aa60a109a59c336"}, + {file = "Pillow-9.4.0-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:b52ff4f4e002f828ea6483faf4c4e8deea8d743cf801b74910243c58acc6eda3"}, + {file = "Pillow-9.4.0-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:575d8912dca808edd9acd6f7795199332696d3469665ef26163cd090fa1f8bfa"}, + {file = "Pillow-9.4.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c3c4ed2ff6760e98d262e0cc9c9a7f7b8a9f61aa4d47c58835cdaf7b0b8811bb"}, + {file = "Pillow-9.4.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e621b0246192d3b9cb1dc62c78cfa4c6f6d2ddc0ec207d43c0dedecb914f152a"}, + {file = "Pillow-9.4.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:8f127e7b028900421cad64f51f75c051b628db17fb00e099eb148761eed598c9"}, + {file = "Pillow-9.4.0.tar.gz", hash = "sha256:a1c2d7780448eb93fbcc3789bf3916aa5720d942e37945f4056680317f1cd23e"}, ] [package.extras] -docs = ["furo", "olefile", "sphinx (>=2.4)", "sphinx-copybutton", "sphinx-issues (>=3.0.1)", "sphinx-removed-in", "sphinxext-opengraph"] +docs = ["furo", "olefile", "sphinx (>=2.4)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinx-issues (>=3.0.1)", "sphinx-removed-in", "sphinxext-opengraph"] tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"] [[package]] name = "platformdirs" -version = "2.5.2" -description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +version = "2.6.2" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "platformdirs-2.5.2-py3-none-any.whl", hash = "sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788"}, - {file = "platformdirs-2.5.2.tar.gz", hash = "sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19"}, + {file = "platformdirs-2.6.2-py3-none-any.whl", hash = "sha256:83c8f6d04389165de7c9b6f0c682439697887bca0aa2f1c87ef1826be3584490"}, + {file = "platformdirs-2.6.2.tar.gz", hash = "sha256:e1fea1fe471b9ff8332e229df3cb7de4f53eeea4998d3b6bfff542115e998bd2"}, ] [package.extras] -docs = ["furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx (>=4)", "sphinx-autodoc-typehints (>=1.12)"] -test = ["appdirs (==1.4.4)", "pytest (>=6)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)"] +docs = ["furo (>=2022.12.7)", "proselint (>=0.13)", "sphinx (>=5.3)", "sphinx-autodoc-typehints (>=1.19.5)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.2.2)", "pytest (>=7.2)", "pytest-cov (>=4)", "pytest-mock (>=3.10)"] [[package]] name = "pluggy" version = "1.0.0" description = "plugin and hook calling mechanisms for python" -category = "main" +category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -632,23 +749,11 @@ files = [ dev = ["pre-commit", "tox"] testing = ["pytest", "pytest-benchmark"] -[[package]] -name = "py" -version = "1.11.0" -description = "library with cross-python path, ini-parsing, io, code, log facilities" -category = "main" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -files = [ - {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, - {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, -] - [[package]] name = "pyinstaller" version = "5.7.0" description = "PyInstaller bundles a Python application and all its dependencies into a single package." -category = "main" +category = "dev" optional = false python-versions = "<3.12,>=3.7" files = [ @@ -680,32 +785,35 @@ hook-testing = ["execnet (>=1.5.0)", "psutil", "pytest (>=2.7.3)"] [[package]] name = "pyinstaller-hooks-contrib" -version = "2022.14" +version = "2022.15" description = "Community maintained hooks for PyInstaller" -category = "main" +category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "pyinstaller-hooks-contrib-2022.14.tar.gz", hash = "sha256:5ae8da3a92cf20e37b3e00604d0c3468896e7d746e5c1449473597a724331b0b"}, - {file = "pyinstaller_hooks_contrib-2022.14-py2.py3-none-any.whl", hash = "sha256:1a125838a22d7b35a18993c6e56d3c5cc3ad7da00954f95bc5606523939203f2"}, + {file = "pyinstaller-hooks-contrib-2022.15.tar.gz", hash = "sha256:73fd4051dc1620f3ae9643291cd9e2f47bfed582ade2eb05e3247ecab4a4f5f3"}, + {file = "pyinstaller_hooks_contrib-2022.15-py2.py3-none-any.whl", hash = "sha256:55c1def8066d0279d06cd67eea30c12ffcdb961a5edeeaf361adac0164baef30"}, ] [[package]] name = "pylint" -version = "2.15.5" +version = "2.15.10" description = "python code static checker" category = "dev" optional = false python-versions = ">=3.7.2" files = [ - {file = "pylint-2.15.5-py3-none-any.whl", hash = "sha256:c2108037eb074334d9e874dc3c783752cc03d0796c88c9a9af282d0f161a1004"}, - {file = "pylint-2.15.5.tar.gz", hash = "sha256:3b120505e5af1d06a5ad76b55d8660d44bf0f2fc3c59c2bdd94e39188ee3a4df"}, + {file = "pylint-2.15.10-py3-none-any.whl", hash = "sha256:9df0d07e8948a1c3ffa3b6e2d7e6e63d9fb457c5da5b961ed63106594780cc7e"}, + {file = "pylint-2.15.10.tar.gz", hash = "sha256:b3dc5ef7d33858f297ac0d06cc73862f01e4f2e74025ec3eff347ce0bc60baf5"}, ] [package.dependencies] -astroid = ">=2.12.12,<=2.14.0-dev0" +astroid = ">=2.12.13,<=2.14.0-dev0" colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} -dill = ">=0.2" +dill = [ + {version = ">=0.2", markers = "python_version < \"3.11\""}, + {version = ">=0.3.6", markers = "python_version >= \"3.11\""}, +] isort = ">=4.2.5,<6" mccabe = ">=0.6,<0.8" platformdirs = ">=2.2.0" @@ -716,41 +824,26 @@ tomlkit = ">=0.10.1" spelling = ["pyenchant (>=3.2,<4.0)"] testutils = ["gitpython (>3)"] -[[package]] -name = "pyparsing" -version = "3.0.9" -description = "pyparsing module - Classes and methods to define and execute parsing grammars" -category = "main" -optional = false -python-versions = ">=3.6.8" -files = [ - {file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"}, - {file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"}, -] - -[package.extras] -diagrams = ["jinja2", "railroad-diagrams"] - [[package]] name = "pytest" -version = "7.1.3" +version = "7.2.1" description = "pytest: simple powerful testing with Python" -category = "main" +category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "pytest-7.1.3-py3-none-any.whl", hash = "sha256:1377bda3466d70b55e3f5cecfa55bb7cfcf219c7964629b967c37cf0bda818b7"}, - {file = "pytest-7.1.3.tar.gz", hash = "sha256:4f365fec2dff9c1162f834d9f18af1ba13062db0c708bf7b946f8a5c76180c39"}, + {file = "pytest-7.2.1-py3-none-any.whl", hash = "sha256:c7c6ca206e93355074ae32f7403e8ea12163b1163c976fee7d4d84027c162be5"}, + {file = "pytest-7.2.1.tar.gz", hash = "sha256:d45e0952f3727241918b8fd0f376f5ff6b301cc0777c6f9a556935c92d8a7d42"}, ] [package.dependencies] attrs = ">=19.2.0" colorama = {version = "*", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} iniconfig = "*" packaging = "*" pluggy = ">=0.12,<2.0" -py = ">=1.8.2" -tomli = ">=1.0.0" +tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} [package.extras] testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"] @@ -759,7 +852,7 @@ testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2. name = "pywin32-ctypes" version = "0.2.0" description = "" -category = "main" +category = "dev" optional = false python-versions = "*" files = [ @@ -871,36 +964,36 @@ full = ["numpy"] [[package]] name = "requests" -version = "2.27.1" +version = "2.28.2" description = "Python HTTP for Humans." category = "main" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" +python-versions = ">=3.7, <4" files = [ - {file = "requests-2.27.1-py2.py3-none-any.whl", hash = "sha256:f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d"}, - {file = "requests-2.27.1.tar.gz", hash = "sha256:68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61"}, + {file = "requests-2.28.2-py3-none-any.whl", hash = "sha256:64299f4909223da747622c030b781c0d7811e359c37124b4bd368fb8c6518baa"}, + {file = "requests-2.28.2.tar.gz", hash = "sha256:98b1b2782e3c6c4904938b84c0eb932721069dfdb9134313beff7c83c2df24bf"}, ] [package.dependencies] certifi = ">=2017.4.17" -charset-normalizer = {version = ">=2.0.0,<2.1.0", markers = "python_version >= \"3\""} -idna = {version = ">=2.5,<4", markers = "python_version >= \"3\""} +charset-normalizer = ">=2,<4" +idna = ">=2.5,<4" urllib3 = ">=1.21.1,<1.27" [package.extras] -socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"] -use-chardet-on-py3 = ["chardet (>=3.0.2,<5)"] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] [[package]] name = "setuptools" -version = "66.0.0" +version = "66.1.1" description = "Easily download, build, install, upgrade, and uninstall Python packages" category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "setuptools-66.0.0-py3-none-any.whl", hash = "sha256:a78d01d1e2c175c474884671dde039962c9d74c7223db7369771fcf6e29ceeab"}, - {file = "setuptools-66.0.0.tar.gz", hash = "sha256:bd6eb2d6722568de6d14b87c44a96fac54b2a45ff5e940e639979a3d1792adb6"}, + {file = "setuptools-66.1.1-py3-none-any.whl", hash = "sha256:6f590d76b713d5de4e49fe4fbca24474469f53c83632d5d0fd056f7ff7e8112b"}, + {file = "setuptools-66.1.1.tar.gz", hash = "sha256:ac4008d396bc9cd983ea483cb7139c0240a07bbc74ffb6232fceffedc6cf03a8"}, ] [package.extras] @@ -924,7 +1017,7 @@ files = [ name = "sortedcontainers" version = "2.4.0" description = "Sorted Containers -- Sorted List, Sorted Dict, Sorted Set" -category = "main" +category = "dev" optional = false python-versions = "*" files = [ @@ -950,7 +1043,7 @@ tests = ["flake8", "pytest", "pytest-cov"] name = "tomli" version = "2.0.1" description = "A lil' TOML parser" -category = "main" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -972,14 +1065,14 @@ files = [ [[package]] name = "tqdm" -version = "4.64.0" +version = "4.64.1" description = "Fast, Extensible Progress Meter" category = "main" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" files = [ - {file = "tqdm-4.64.0-py2.py3-none-any.whl", hash = "sha256:74a2cdefe14d11442cedf3ba4e21a3b84ff9a2dbdc6cfae2c34addb2a14a5ea6"}, - {file = "tqdm-4.64.0.tar.gz", hash = "sha256:40be55d30e200777a307a7585aee69e4eabb46b4ec6a4b4a5f2d9f11e7d5408d"}, + {file = "tqdm-4.64.1-py2.py3-none-any.whl", hash = "sha256:6fee160d6ffcd1b1c68c65f14c829c22832bc401726335ce92c52d395944a6a1"}, + {file = "tqdm-4.64.1.tar.gz", hash = "sha256:5f4f682a004951c1b450bc753c710e9280c5746ce6ffedee253ddbcbf54cf1e4"}, ] [package.dependencies] @@ -991,6 +1084,18 @@ notebook = ["ipywidgets (>=6)"] slack = ["slack-sdk"] telegram = ["requests"] +[[package]] +name = "typing-extensions" +version = "4.4.0" +description = "Backported and Experimental Type Hints for Python 3.7+" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "typing_extensions-4.4.0-py3-none-any.whl", hash = "sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e"}, + {file = "typing_extensions-4.4.0.tar.gz", hash = "sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa"}, +] + [[package]] name = "unidecode" version = "1.3.6" @@ -1005,57 +1110,57 @@ files = [ [[package]] name = "urllib3" -version = "1.26.9" +version = "1.26.14" description = "HTTP library with thread-safe connection pooling, file post, and more." category = "main" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" files = [ - {file = "urllib3-1.26.9-py2.py3-none-any.whl", hash = "sha256:44ece4d53fb1706f667c9bd1c648f5469a2ec925fcf3a776667042d645472c14"}, - {file = "urllib3-1.26.9.tar.gz", hash = "sha256:aabaf16477806a5e1dd19aa41f8c2b7950dd3c746362d7e3223dbe6de6ac448e"}, + {file = "urllib3-1.26.14-py2.py3-none-any.whl", hash = "sha256:75edcdc2f7d85b137124a6c3c9fc3933cdeaa12ecb9a6a959f22797a0feca7e1"}, + {file = "urllib3-1.26.14.tar.gz", hash = "sha256:076907bf8fd355cde77728471316625a4d2f7e713c125f51953bb5b3eecf4f72"}, ] [package.extras] brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"] -secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)"] +secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"] socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] [[package]] name = "watchdog" -version = "2.2.0" +version = "2.2.1" description = "Filesystem events monitoring" category = "main" optional = false python-versions = ">=3.6" files = [ - {file = "watchdog-2.2.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ed91c3ccfc23398e7aa9715abf679d5c163394b8cad994f34f156d57a7c163dc"}, - {file = "watchdog-2.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:76a2743402b794629a955d96ea2e240bd0e903aa26e02e93cd2d57b33900962b"}, - {file = "watchdog-2.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:920a4bda7daa47545c3201a3292e99300ba81ca26b7569575bd086c865889090"}, - {file = "watchdog-2.2.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ceaa9268d81205876bedb1069f9feab3eccddd4b90d9a45d06a0df592a04cae9"}, - {file = "watchdog-2.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1893d425ef4fb4f129ee8ef72226836619c2950dd0559bba022b0818c63a7b60"}, - {file = "watchdog-2.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9e99c1713e4436d2563f5828c8910e5ff25abd6ce999e75f15c15d81d41980b6"}, - {file = "watchdog-2.2.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:a5bd9e8656d07cae89ac464ee4bcb6f1b9cecbedc3bf1334683bed3d5afd39ba"}, - {file = "watchdog-2.2.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:3a048865c828389cb06c0bebf8a883cec3ae58ad3e366bcc38c61d8455a3138f"}, - {file = "watchdog-2.2.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e722755d995035dd32177a9c633d158f2ec604f2a358b545bba5bed53ab25bca"}, - {file = "watchdog-2.2.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:af4b5c7ba60206759a1d99811b5938ca666ea9562a1052b410637bb96ff97512"}, - {file = "watchdog-2.2.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:619d63fa5be69f89ff3a93e165e602c08ed8da402ca42b99cd59a8ec115673e1"}, - {file = "watchdog-2.2.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:1f2b0665c57358ce9786f06f5475bc083fea9d81ecc0efa4733fd0c320940a37"}, - {file = "watchdog-2.2.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:441024df19253bb108d3a8a5de7a186003d68564084576fecf7333a441271ef7"}, - {file = "watchdog-2.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1a410dd4d0adcc86b4c71d1317ba2ea2c92babaf5b83321e4bde2514525544d5"}, - {file = "watchdog-2.2.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:28704c71afdb79c3f215c90231e41c52b056ea880b6be6cee035c6149d658ed1"}, - {file = "watchdog-2.2.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:2ac0bd7c206bb6df78ef9e8ad27cc1346f2b41b1fef610395607319cdab89bc1"}, - {file = "watchdog-2.2.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:27e49268735b3c27310883012ab3bd86ea0a96dcab90fe3feb682472e30c90f3"}, - {file = "watchdog-2.2.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:2af1a29fd14fc0a87fb6ed762d3e1ae5694dcde22372eebba50e9e5be47af03c"}, - {file = "watchdog-2.2.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:c7bd98813d34bfa9b464cf8122e7d4bec0a5a427399094d2c17dd5f70d59bc61"}, - {file = "watchdog-2.2.0-py3-none-manylinux2014_i686.whl", hash = "sha256:56fb3f40fc3deecf6e518303c7533f5e2a722e377b12507f6de891583f1b48aa"}, - {file = "watchdog-2.2.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:74535e955359d79d126885e642d3683616e6d9ab3aae0e7dcccd043bd5a3ff4f"}, - {file = "watchdog-2.2.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:cf05e6ff677b9655c6e9511d02e9cc55e730c4e430b7a54af9c28912294605a4"}, - {file = "watchdog-2.2.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:d6ae890798a3560688b441ef086bb66e87af6b400a92749a18b856a134fc0318"}, - {file = "watchdog-2.2.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:e5aed2a700a18c194c39c266900d41f3db0c1ebe6b8a0834b9995c835d2ca66e"}, - {file = "watchdog-2.2.0-py3-none-win32.whl", hash = "sha256:d0fb5f2b513556c2abb578c1066f5f467d729f2eb689bc2db0739daf81c6bb7e"}, - {file = "watchdog-2.2.0-py3-none-win_amd64.whl", hash = "sha256:1f8eca9d294a4f194ce9df0d97d19b5598f310950d3ac3dd6e8d25ae456d4c8a"}, - {file = "watchdog-2.2.0-py3-none-win_ia64.whl", hash = "sha256:ad0150536469fa4b693531e497ffe220d5b6cd76ad2eda474a5e641ee204bbb6"}, - {file = "watchdog-2.2.0.tar.gz", hash = "sha256:83cf8bc60d9c613b66a4c018051873d6273d9e45d040eed06d6a96241bd8ec01"}, + {file = "watchdog-2.2.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a09483249d25cbdb4c268e020cb861c51baab2d1affd9a6affc68ffe6a231260"}, + {file = "watchdog-2.2.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5100eae58133355d3ca6c1083a33b81355c4f452afa474c2633bd2fbbba398b3"}, + {file = "watchdog-2.2.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e618a4863726bc7a3c64f95c218437f3349fb9d909eb9ea3a1ed3b567417c661"}, + {file = "watchdog-2.2.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:102a60093090fc3ff76c983367b19849b7cc24ec414a43c0333680106e62aae1"}, + {file = "watchdog-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:748ca797ff59962e83cc8e4b233f87113f3cf247c23e6be58b8a2885c7337aa3"}, + {file = "watchdog-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6ccd8d84b9490a82b51b230740468116b8205822ea5fdc700a553d92661253a3"}, + {file = "watchdog-2.2.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:6e01d699cd260d59b84da6bda019dce0a3353e3fcc774408ae767fe88ee096b7"}, + {file = "watchdog-2.2.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8586d98c494690482c963ffb24c49bf9c8c2fe0589cec4dc2f753b78d1ec301d"}, + {file = "watchdog-2.2.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:adaf2ece15f3afa33a6b45f76b333a7da9256e1360003032524d61bdb4c422ae"}, + {file = "watchdog-2.2.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:83a7cead445008e880dbde833cb9e5cc7b9a0958edb697a96b936621975f15b9"}, + {file = "watchdog-2.2.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f8ac23ff2c2df4471a61af6490f847633024e5aa120567e08d07af5718c9d092"}, + {file = "watchdog-2.2.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:d0f29fd9f3f149a5277929de33b4f121a04cf84bb494634707cfa8ea8ae106a8"}, + {file = "watchdog-2.2.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:967636031fa4c4955f0f3f22da3c5c418aa65d50908d31b73b3b3ffd66d60640"}, + {file = "watchdog-2.2.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:96cbeb494e6cbe3ae6aacc430e678ce4b4dd3ae5125035f72b6eb4e5e9eb4f4e"}, + {file = "watchdog-2.2.1-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:61fdb8e9c57baf625e27e1420e7ca17f7d2023929cd0065eb79c83da1dfbeacd"}, + {file = "watchdog-2.2.1-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:4cb5ecc332112017fbdb19ede78d92e29a8165c46b68a0b8ccbd0a154f196d5e"}, + {file = "watchdog-2.2.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a480d122740debf0afac4ddd583c6c0bb519c24f817b42ed6f850e2f6f9d64a8"}, + {file = "watchdog-2.2.1-py3-none-manylinux2014_aarch64.whl", hash = "sha256:978a1aed55de0b807913b7482d09943b23a2d634040b112bdf31811a422f6344"}, + {file = "watchdog-2.2.1-py3-none-manylinux2014_armv7l.whl", hash = "sha256:8c28c23972ec9c524967895ccb1954bc6f6d4a557d36e681a36e84368660c4ce"}, + {file = "watchdog-2.2.1-py3-none-manylinux2014_i686.whl", hash = "sha256:c27d8c1535fd4474e40a4b5e01f4ba6720bac58e6751c667895cbc5c8a7af33c"}, + {file = "watchdog-2.2.1-py3-none-manylinux2014_ppc64.whl", hash = "sha256:d6b87477752bd86ac5392ecb9eeed92b416898c30bd40c7e2dd03c3146105646"}, + {file = "watchdog-2.2.1-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:cece1aa596027ff56369f0b50a9de209920e1df9ac6d02c7f9e5d8162eb4f02b"}, + {file = "watchdog-2.2.1-py3-none-manylinux2014_s390x.whl", hash = "sha256:8b5cde14e5c72b2df5d074774bdff69e9b55da77e102a91f36ef26ca35f9819c"}, + {file = "watchdog-2.2.1-py3-none-manylinux2014_x86_64.whl", hash = "sha256:e038be858425c4f621900b8ff1a3a1330d9edcfeaa1c0468aeb7e330fb87693e"}, + {file = "watchdog-2.2.1-py3-none-win32.whl", hash = "sha256:bc43c1b24d2f86b6e1cc15f68635a959388219426109233e606517ff7d0a5a73"}, + {file = "watchdog-2.2.1-py3-none-win_amd64.whl", hash = "sha256:17f1708f7410af92ddf591e94ae71a27a13974559e72f7e9fde3ec174b26ba2e"}, + {file = "watchdog-2.2.1-py3-none-win_ia64.whl", hash = "sha256:195ab1d9d611a4c1e5311cbf42273bc541e18ea8c32712f2fb703cfc6ff006f9"}, + {file = "watchdog-2.2.1.tar.gz", hash = "sha256:cdcc23c9528601a8a293eb4369cbd14f6b4f34f07ae8769421252e9c22718b6f"}, ] [package.extras] @@ -1063,16 +1168,19 @@ watchmedo = ["PyYAML (>=3.10)"] [[package]] name = "werkzeug" -version = "2.1.2" +version = "2.2.2" description = "The comprehensive WSGI web application library." category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "Werkzeug-2.1.2-py3-none-any.whl", hash = "sha256:72a4b735692dd3135217911cbeaa1be5fa3f62bffb8745c5215420a03dc55255"}, - {file = "Werkzeug-2.1.2.tar.gz", hash = "sha256:1ce08e8093ed67d638d63879fd1ba3735817f7a80de3674d293f5984f25fb6e6"}, + {file = "Werkzeug-2.2.2-py3-none-any.whl", hash = "sha256:f979ab81f58d7318e064e99c4506445d60135ac5cd2e177a2de0089bfd4c9bd5"}, + {file = "Werkzeug-2.2.2.tar.gz", hash = "sha256:7ea2d48322cc7c0f8b3a215ed73eabd7b5d75d0b50e31ab006286ccff9e00b8f"}, ] +[package.dependencies] +MarkupSafe = ">=2.1.1" + [package.extras] watchdog = ["watchdog"] @@ -1153,4 +1261,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = ">=3.10,<3.12" -content-hash = "6f6126fbaa8f114c90cef637e0bc996d63d3dc93f6ed2e90e36ef696cdd0cfc5" +content-hash = "15b3fba920faab237353240b2b3dbb32603744f6a8ff19e77fe6296d5252c2d7" diff --git a/pyproject.toml b/pyproject.toml index e19ec185..14d7090b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,21 +9,24 @@ python = ">=3.10,<3.12" Flask = "^2.0.2" Flask-Cors = "^3.0.10" requests = "^2.27.1" -watchdog = "^2.2.0" +watchdog = "^2.2.1" gunicorn = "^20.1.0" Pillow = "^9.0.1" "colorgram.py" = "^1.2.0" tqdm = "^4.64.0" rapidfuzz = "^2.13.7" tinytag = "^1.8.1" -hypothesis = "^6.56.3" -pytest = "^7.1.3" Unidecode = "^1.3.6" -pyinstaller = "^5.7.0" [tool.poetry.dev-dependencies] pylint = "^2.15.5" -black = {version = "^22.6.0", allow-prereleases = true} +pytest = "^7.1.3" +hypothesis = "^6.56.3" +pyinstaller = "^5.7.0" + +[tool.poetry.dev-dependencies.black] +version = "^22.6.0" +allow-prereleases = true [build-system] requires = ["poetry-core>=1.0.0"] diff --git a/start.sh b/start.sh index bdbb0c71..2d56a51c 100755 --- a/start.sh +++ b/start.sh @@ -5,21 +5,5 @@ gpath=$(poetry run which gunicorn) # $pytest # -q -while getopts ':s' opt; do - case $opt in - s) - echo "Starting image server" - cd "./app" - "$gpath" -b 0.0.0.0:1971 -w 1 --threads=1 "imgserver:app" & - cd ../ - echo "Done ✅" - ;; - \?) - echo "Invalid option: -$OPTARG" >&2 - ;; - esac -done - - echo "Starting swing" -"$gpath" -b 0.0.0.0:1970 --threads=2 "manage:create_api()" \ No newline at end of file +"$gpath" -b 0.0.0.0:1970 --threads=2 "manage:create_api()" diff --git a/tests/test.py b/tests/test.py deleted file mode 100644 index 29b12905..00000000 --- a/tests/test.py +++ /dev/null @@ -1,18 +0,0 @@ -from app.utils import extract_featured_artists_from_title - - -def test_extract_featured_artists_from_title(): - test_titles = [ - "Own it (Featuring Ed Sheeran & Stormzy)", - "Godzilla (Deluxe)(Feat. Juice Wrld)(Deluxe)", - "Simmer (with Burna Boy)", - ] - - expected_test_artists = [ - ["Ed Sheeran", "Stormzy"], - ['Juice Wrld'], - ["Burna Boy"] - ] - - for title, expected in zip(test_titles, expected_test_artists): - assert extract_featured_artists_from_title(title) == expected diff --git a/tests/test_utils.py b/tests/test_utils.py new file mode 100644 index 00000000..79de5973 --- /dev/null +++ b/tests/test_utils.py @@ -0,0 +1,33 @@ +import app.utils +from hypothesis import given, strategies as st +from app.utils import extract_featured_artists_from_title + + +def test_extract_featured_artists_from_title(): + test_titles = [ + "Own it (Featuring Ed Sheeran & Stormzy)", + "Autograph (On my line)(Feat. Lil Peep)(Deluxe)", + "Why so sad? (with Juice Wrld, Lil Peep)", + "Why so sad? (with Juice Wrld/Lil Peep)", + "Simmer (with Burna Boy)", + "Simmer (without Burna Boy)" + ] + + results = [ + ["Ed Sheeran", "Stormzy"], + ['Lil Peep'], + ["Juice Wrld", "Lil Peep"], + ["Juice Wrld", "Lil Peep"], + ["Burna Boy"], + [] + ] + + for title, expected in zip(test_titles, results): + assert extract_featured_artists_from_title(title) == expected + + +# === HYPOTHESIS GHOSTWRITER TESTS === + +@given(__dir=st.text(), full=st.booleans()) +def test_fuzz_run_fast_scandir(__dir: str, full) -> None: + app.utils.run_fast_scandir(_dir=__dir, full=full) From 5b71b95d662ea9a51af7ef80da791bc5cfce766c Mon Sep 17 00:00:00 2001 From: geoffrey45 Date: Wed, 25 Jan 2023 09:41:07 +0300 Subject: [PATCH 10/34] prevent different cased featured artist names. remove "/" as artist separator --- app/models.py | 5 +++-- app/utils.py | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/app/models.py b/app/models.py index a16cad67..f5e57790 100644 --- a/app/models.py +++ b/app/models.py @@ -59,9 +59,10 @@ class Track: def __post_init__(self): if self.artist is not None: artists = utils.split_artists(self.artist) + featured = utils.extract_featured_artists_from_title(self.title) - artists.extend(featured) - artists = set(artists) + original_lower = "-".join([a.lower() for a in artists]) + artists.extend([a for a in featured if a.lower() not in original_lower]) self.artist_hashes = [utils.create_hash(a, decode=True) for a in artists] diff --git a/app/utils.py b/app/utils.py index 95d78869..8be18367 100644 --- a/app/utils.py +++ b/app/utils.py @@ -260,7 +260,7 @@ def is_windows(): def split_artists(src: str): - artists = re.split(r"\s*[&,;/]\s*", src) + artists = re.split(r"\s*[&,;]\s*", src) return [a.strip() for a in artists] From 6818f9b0e855e530848db79dae19c9a57eaac04c Mon Sep 17 00:00:00 2001 From: geoffrey45 Date: Wed, 25 Jan 2023 13:43:09 +0300 Subject: [PATCH 11/34] handle watchdog's file created event using the on_modified handler + move processing thumbnails and album colors to the populate class + move processing artist colors behind the populate call in run_periodic_checks --- app/functions.py | 6 ++---- app/lib/populate.py | 4 ++++ app/lib/watchdogg.py | 32 +++++++++++++++++++++++++++++++- 3 files changed, 37 insertions(+), 5 deletions(-) diff --git a/app/functions.py b/app/functions.py index 21405502..375b1c14 100644 --- a/app/functions.py +++ b/app/functions.py @@ -28,10 +28,6 @@ def run_periodic_checks(): except PopulateCancelledError: pass - ProcessTrackThumbnails() - ProcessAlbumColors() - ProcessArtistColors() - if utils.Ping()(): try: CheckArtistImages() @@ -40,4 +36,6 @@ def run_periodic_checks(): "Internet connection lost. Downloading artist images stopped." ) + ProcessArtistColors() + time.sleep(300) diff --git a/app/lib/populate.py b/app/lib/populate.py index 9e657dc5..92849612 100644 --- a/app/lib/populate.py +++ b/app/lib/populate.py @@ -7,6 +7,7 @@ from app.db.sqlite.tracks import SQLiteTrackMethods from app.db.sqlite.settings import SettingsSQLMethods as sdb from app.db.sqlite.favorite import SQLiteFavoriteMethods as favdb from app.db.store import Store +from app.lib.colorlib import ProcessAlbumColors from app.lib.taglib import extract_thumb, get_tags from app.logger import log @@ -65,6 +66,9 @@ class Populate: self.tag_untagged(untagged, key) + ProcessTrackThumbnails() + ProcessAlbumColors() + @staticmethod def filter_untagged(tracks: list[Track], files: list[str]): tagged_files = [t.filepath for t in tracks] diff --git a/app/lib/watchdogg.py b/app/lib/watchdogg.py index 27ffa7cd..57875e75 100644 --- a/app/lib/watchdogg.py +++ b/app/lib/watchdogg.py @@ -40,6 +40,8 @@ class Watcher: while trials < 10: try: dirs = sdb.get_root_dirs() + dirs = [rf"{d}" for d in dirs] + dir_map = [ {"original": d, "realpath": os.path.realpath(d)} for d in dirs ] @@ -59,7 +61,7 @@ class Watcher: ) return - dir_map = [d for d in dir_map if d['realpath'] != d['original']] + dir_map = [d for d in dir_map if d["realpath"] != d["original"]] if len(dirs) > 0 and dirs[0] == "$home": dirs = [settings.USER_HOME_DIR] @@ -179,6 +181,8 @@ def remove_track(filepath: str) -> None: class Handler(PatternMatchingEventHandler): files_to_process = [] + files_to_process_windows = [] + root_dirs = [] dir_map = [] @@ -208,6 +212,7 @@ class Handler(PatternMatchingEventHandler): Fired when a supported file is created. """ self.files_to_process.append(event.src_path) + self.files_to_process_windows.append(event.src_path) def on_deleted(self, event): """ @@ -240,6 +245,7 @@ class Handler(PatternMatchingEventHandler): def on_closed(self, event): """ Fired when a created file is closed. + NOT FIRED IN WINDOWS """ try: self.files_to_process.remove(event.src_path) @@ -248,3 +254,27 @@ class Handler(PatternMatchingEventHandler): add_track(path) except ValueError: pass + + def on_modified(self, event): + # this event handler is triggered twice on windows + # for copy events. We need to test how this behaves in + # Linux. + + if event.src_path not in self.files_to_process_windows: + return + + file_size = -1 + + while file_size != os.path.getsize(event.src_path): + file_size = os.path.getsize(event.src_path) + time.sleep(0.1) + + try: + os.rename(event.src_path, event.src_path) + path = self.get_abs_path(event.src_path) + remove_track(path) + add_track(path) + self.files_to_process_windows.remove(event.src_path) + except OSError: + print("File is locked, skipping") + pass From 93a04ba04182e0fd652996026fb53dee2321efaf Mon Sep 17 00:00:00 2001 From: geoffrey45 Date: Wed, 25 Jan 2023 15:28:54 +0300 Subject: [PATCH 12/34] remove migrations folder from build assets --- manage.py | 1 - swingmusic.spec | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/manage.py b/manage.py index 03d9849e..7bfe7769 100644 --- a/manage.py +++ b/manage.py @@ -91,7 +91,6 @@ class HandleArgs: "--clean", f"--add-data=assets{_s}assets", f"--add-data=client{_s}client", - f"--add-data=app/migrations{_s}app/migrations", f"--add-data=pyinstaller.config.ini{_s}.", "-y", ] diff --git a/swingmusic.spec b/swingmusic.spec index efd0e879..97b51115 100644 --- a/swingmusic.spec +++ b/swingmusic.spec @@ -8,7 +8,7 @@ a = Analysis( ['manage.py'], pathex=[], binaries=[], - datas=[('assets', 'assets'), ('client', 'client'), ('pyinstaller.config.ini', '.')], + datas=[('assets', 'assets'), ('client', 'client'), ('app/migrations', 'app/migrations'), ('pyinstaller.config.ini', '.')], hiddenimports=[], hookspath=[], hooksconfig={}, From 7e15680f266226d21fdcf0ed51c7c455e4d6a4e2 Mon Sep 17 00:00:00 2001 From: geoffrey45 Date: Mon, 30 Jan 2023 15:59:28 +0300 Subject: [PATCH 13/34] update supported audio files in settings.py + add win_replace_slash function to format win path strings + misc --- app/api/folder.py | 45 ++++++++++++++--- app/api/settings.py | 42 +++++++++------- app/db/sqlite/settings.py | 5 +- app/db/store.py | 16 ++++-- app/lib/folderslib.py | 25 +++++++--- app/lib/populate.py | 7 ++- app/lib/taglib.py | 40 +++++++++++---- app/lib/watchdogg.py | 10 ++-- app/models.py | 2 +- app/settings.py | 2 +- app/utils.py | 101 ++++++++++++++++++++++++++++---------- manage.py | 2 +- poetry.lock | 41 +++++++++++++--- pyproject.toml | 2 + tests/test_utils.py | 24 +++++---- 15 files changed, 268 insertions(+), 96 deletions(-) diff --git a/app/api/folder.py b/app/api/folder.py index fa05fce1..c87c8e38 100644 --- a/app/api/folder.py +++ b/app/api/folder.py @@ -2,6 +2,7 @@ Contains all the folder routes. """ import os +import psutil from pathlib import Path from flask import Blueprint, request @@ -10,7 +11,7 @@ from app import settings from app.lib.folderslib import GetFilesAndDirs from app.db.sqlite.settings import SettingsSQLMethods as db from app.models import Folder -from app.utils import create_folder_hash +from app.utils import create_folder_hash, is_windows, win_replace_slash api = Blueprint("folder", __name__, url_prefix="/") @@ -42,8 +43,8 @@ def get_folder_tree(): return { "folders": [ Folder( - name=f.name, - path=str(f), + name=f.name if f.name != "" else str(f).replace("\\", "/"), + path=win_replace_slash(str(f)), has_tracks=True, is_sym=f.is_symlink(), path_hash=create_folder_hash(*f.parts[1:]), @@ -61,26 +62,56 @@ def get_folder_tree(): } +def get_all_drives(): + """ + Returns a list of all the drives on a windows machine. + """ + drives = psutil.disk_partitions() + return [d.mountpoint for d in drives] + + @api.route("/folder/dir-browser", methods=["POST"]) def list_folders(): """ Returns a list of all the folders in the given folder. """ data = request.get_json() + is_win = is_windows() try: req_dir: str = data["folder"] except KeyError: - req_dir = settings.USER_HOME_DIR + req_dir = "$home" if req_dir == "$home": - req_dir = settings.USER_HOME_DIR + # req_dir = settings.USER_HOME_DIR + if is_win: + return { + "folders": [ + {"name": win_replace_slash(d), "path": win_replace_slash(d)} + for d in get_all_drives() + ] + } - entries = os.scandir(req_dir) + req_dir = req_dir + "/" + + try: + entries = os.scandir(req_dir) + except PermissionError: + return {"folders": []} dirs = [e.name for e in entries if e.is_dir() and not e.name.startswith(".")] - dirs = [{"name": d, "path": os.path.join(req_dir, d)} for d in dirs] + dirs = [ + {"name": d, "path": win_replace_slash(os.path.join(req_dir, d))} for d in dirs + ] return { "folders": sorted(dirs, key=lambda i: i["name"]), } + + +# todo: + +# - handle showing windows disks in root_dir configuration +# - handle the above, but for all partitions mounted in linux. +# - handle the "\" in client's folder page breadcrumb diff --git a/app/api/settings.py b/app/api/settings.py index 0c1b2a83..69427065 100644 --- a/app/api/settings.py +++ b/app/api/settings.py @@ -48,6 +48,19 @@ def rebuild_store(db_dirs: list[str]): log.info("Rebuilding library... ✅") +def finalize(new_: list[str], removed_: list[str], db_dirs_: list[str]): + """ + Params: + new_: will be added to the database + removed_: will be removed from the database + db_dirs_: will be used to remove tracks that + are outside these directories from the database and store. + """ + sdb.remove_root_dirs(removed_) + sdb.add_root_dirs(new_) + rebuild_store(db_dirs_) + + @api.route("/settings/add-root-dirs", methods=["POST"]) def add_root_dirs(): """ @@ -66,33 +79,28 @@ def add_root_dirs(): except KeyError: return msg, 400 - def finalize(new_: list[str], removed_: list[str], db_dirs_: list[str]): - sdb.remove_root_dirs(removed_) - sdb.add_root_dirs(new_) - rebuild_store(db_dirs_) - - # --- db_dirs = sdb.get_root_dirs() _h = "$home" - try: - if db_dirs[0] == _h and new_dirs[0] == _h.strip(): - return {"msg": "Not changed!"} + db_home = any([d == _h for d in db_dirs]) # if $home is in db + incoming_home = any([d == _h for d in new_dirs]) # if $home is in incoming - if db_dirs[0] == _h: - sdb.remove_root_dirs(db_dirs) + # handle $home case + if db_home and incoming_home: + return {"msg": "Not changed!"} - if new_dirs[0] == _h: - finalize([_h], db_dirs, [settings.USER_HOME_DIR]) + if db_home or incoming_home: + sdb.remove_root_dirs(db_dirs) - return {"root_dirs": [_h]} - except IndexError: - pass + if incoming_home: + finalize([_h], [], [settings.USER_HOME_DIR]) + return {"root_dirs": [_h]} + + # --- for _dir in new_dirs: children = get_child_dirs(_dir, db_dirs) removed_dirs.extend(children) - # --- for _dir in removed_dirs: try: diff --git a/app/db/sqlite/settings.py b/app/db/sqlite/settings.py index 901aff10..d6e0ea41 100644 --- a/app/db/sqlite/settings.py +++ b/app/db/sqlite/settings.py @@ -1,5 +1,5 @@ -import json from app.db.sqlite.utils import SQLiteManager +from app.utils import win_replace_slash class SettingsSQLMethods: @@ -19,7 +19,8 @@ class SettingsSQLMethods: cur.execute(sql) dirs = cur.fetchall() - return [dir[0] for dir in dirs] + dirs = [dir[0] for dir in dirs] + return [win_replace_slash(d) for d in dirs] @staticmethod def add_root_dirs(dirs: list[str]): diff --git a/app/db/store.py b/app/db/store.py index f6e2bb60..26571126 100644 --- a/app/db/store.py +++ b/app/db/store.py @@ -17,6 +17,7 @@ from app.utils import ( create_folder_hash, get_all_artists, remove_duplicates, + win_replace_slash, ) @@ -174,7 +175,7 @@ class Store: return Folder( name=folder.name, - path=str(folder), + path=win_replace_slash(str(folder)), is_sym=folder.is_symlink(), has_tracks=True, path_hash=create_folder_hash(*folder.parts[1:]), @@ -218,9 +219,18 @@ class Store: ] all_folders = [Path(f) for f in all_folders] - all_folders = [f for f in all_folders if f.exists()] + # all_folders = [f for f in all_folders if f.exists()] - for path in tqdm(all_folders, desc="Processing folders"): + valid_folders = [] + + for folder in all_folders: + try: + if folder.exists(): + valid_folders.append(folder) + except PermissionError: + pass + + for path in tqdm(valid_folders, desc="Processing folders"): folder = cls.create_folder(str(path)) cls.folders.append(folder) diff --git a/app/lib/folderslib.py b/app/lib/folderslib.py index 9062e0cf..64906784 100644 --- a/app/lib/folderslib.py +++ b/app/lib/folderslib.py @@ -4,6 +4,8 @@ from concurrent.futures import ThreadPoolExecutor from app.db.store import Store from app.models import Folder, Track from app.settings import SUPPORTED_FILES +from app.logger import log +from app.utils import win_replace_slash class GetFilesAndDirs: @@ -26,14 +28,25 @@ class GetFilesAndDirs: ext = os.path.splitext(entry.name)[1].lower() if entry.is_dir() and not entry.name.startswith("."): - dirs.append(entry.path) + dirs.append(win_replace_slash(entry.path)) elif entry.is_file() and ext in SUPPORTED_FILES: - files.append(entry.path) + files.append(win_replace_slash(entry.path)) - # sort files by modified time - files.sort( - key=lambda f: os.path.getmtime(f) # pylint: disable=unnecessary-lambda - ) + files_ = [] + + for file in files: + try: + files_.append( + { + "path": file, + "time": os.path.getmtime(file), + } + ) + except OSError as e: + log.error(e) + + files_.sort(key=lambda f: f["time"]) + files = [f["path"] for f in files_] tracks = Store.get_tracks_by_filepaths(files) diff --git a/app/lib/populate.py b/app/lib/populate.py index 92849612..b1a4df1a 100644 --- a/app/lib/populate.py +++ b/app/lib/populate.py @@ -43,7 +43,10 @@ class Populate: if len(dirs_to_scan) == 0: log.warning( - "The root directory is not configured. Open the app in your web browser to configure." + ( + "The root directory is not configured. " + + "Open the app in your webbrowser to configure." + ) ) return @@ -85,7 +88,7 @@ class Populate: for file in tqdm(untagged, desc="Reading files"): if POPULATE_KEY != key: - raise PopulateCancelledError('Populate key changed') + raise PopulateCancelledError("Populate key changed") tags = get_tags(file) diff --git a/app/lib/taglib.py b/app/lib/taglib.py index 5d975d20..4f1f16e8 100644 --- a/app/lib/taglib.py +++ b/app/lib/taglib.py @@ -1,13 +1,17 @@ -import os import datetime +import os from io import BytesIO -from tinytag import TinyTag from PIL import Image, UnidentifiedImageError +from tinytag import TinyTag from app import settings -from app.utils import create_hash - +from app.utils import ( + create_hash, + parse_artist_from_filename, + parse_title_from_filename, + win_replace_slash, +) def parse_album_art(filepath: str): @@ -81,7 +85,7 @@ def get_tags(filepath: str): try: tags = TinyTag.get(filepath) - except: # pylint: disable=bare-except + except: # noqa: E722 return None no_albumartist: bool = (tags.albumartist == "") or (tags.albumartist is None) @@ -97,9 +101,22 @@ def get_tags(filepath: str): for tag in to_filename: p = getattr(tags, tag) if p == "" or p is None: - setattr(tags, tag, filename) + maybe = parse_title_from_filename(filename) + setattr(tags, tag, maybe) - to_check = ["album", "artist", "year", "albumartist"] + parse = ["artist", "albumartist"] + for tag in parse: + p = getattr(tags, tag) + + if p == "" or p is None: + maybe = parse_artist_from_filename(filename) + + if maybe != []: + setattr(tags, tag, ", ".join(maybe)) + else: + setattr(tags, tag, "Unknown") + + to_check = ["album", "year", "albumartist"] for prop in to_check: p = getattr(tags, prop) if (p is None) or (p == ""): @@ -127,10 +144,10 @@ def get_tags(filepath: str): tags.albumhash = create_hash(tags.album, tags.albumartist) tags.trackhash = create_hash(tags.artist, tags.album, tags.title) tags.image = f"{tags.albumhash}.webp" - tags.folder = os.path.dirname(filepath) + tags.folder = win_replace_slash(os.path.dirname(filepath)) tags.date = extract_date(tags.year) - tags.filepath = filepath + tags.filepath = win_replace_slash(filepath) tags.filetype = filetype tags = tags.__dict__ @@ -157,3 +174,8 @@ def get_tags(filepath: str): del tags[tag] return tags + + for tag in to_delete: + del tags[tag] + + return tags diff --git a/app/lib/watchdogg.py b/app/lib/watchdogg.py index 57875e75..f4003399 100644 --- a/app/lib/watchdogg.py +++ b/app/lib/watchdogg.py @@ -63,7 +63,10 @@ class Watcher: dir_map = [d for d in dir_map if d["realpath"] != d["original"]] - if len(dirs) > 0 and dirs[0] == "$home": + # if len(dirs) > 0 and dirs[0] == "$home": + # dirs = [settings.USER_HOME_DIR] + + if any([d == "$home" for d in dirs]): dirs = [settings.USER_HOME_DIR] event_handler = Handler(root_dirs=dirs, dir_map=dir_map) @@ -83,7 +86,7 @@ class Watcher: try: self.observer.start() log.info("Started watchdog") - except FileNotFoundError: + except (FileNotFoundError, PermissionError): log.error( "WatchdogError: Failed to start watchdog, root directories could not be resolved." ) @@ -189,10 +192,11 @@ class Handler(PatternMatchingEventHandler): def __init__(self, root_dirs: list[str], dir_map: dict[str:str]): self.root_dirs = root_dirs self.dir_map = dir_map + patterns = [f"*{f}" for f in settings.SUPPORTED_FILES] PatternMatchingEventHandler.__init__( self, - patterns=["*.flac", "*.mp3"], + patterns=patterns, ignore_directories=True, case_sensitive=False, ) diff --git a/app/models.py b/app/models.py index f5e57790..7911ee5c 100644 --- a/app/models.py +++ b/app/models.py @@ -60,7 +60,7 @@ class Track: if self.artist is not None: artists = utils.split_artists(self.artist) - featured = utils.extract_featured_artists_from_title(self.title) + featured = utils.parse_feat_from_title(self.title) original_lower = "-".join([a.lower() for a in artists]) artists.extend([a for a in featured if a.lower() not in original_lower]) diff --git a/app/settings.py b/app/settings.py index e3462874..ea2924cc 100644 --- a/app/settings.py +++ b/app/settings.py @@ -71,7 +71,7 @@ SM_ARTIST_IMG_SIZE = 64 The size of extracted images in pixels """ -FILES = ["flac", "mp3", "wav", "m4a"] +FILES = ["flac", "mp3", "wav", "m4a", "ogg", "wma", "opus", "alac", "aiff"] SUPPORTED_FILES = tuple(f".{file}" for file in FILES) # ===== SQLite ===== diff --git a/app/utils.py b/app/utils.py index 8be18367..39b86250 100644 --- a/app/utils.py +++ b/app/utils.py @@ -1,19 +1,18 @@ """ This module contains mini functions for the server. """ -import random -import re -import string -from pathlib import Path -from datetime import datetime - +import hashlib import os import platform +import random +import re import socket as Socket -import hashlib +import string import threading -import requests +from datetime import datetime +from pathlib import Path +import requests from unidecode import unidecode from app import models @@ -36,7 +35,8 @@ def background(func): def run_fast_scandir(_dir: str, full=False) -> tuple[list[str], list[str]]: """ - Scans a directory for files with a specific extension. Returns a list of files and folders in the directory. + Scans a directory for files with a specific extension. + Returns a list of files and folders in the directory. """ if _dir == "": @@ -46,20 +46,20 @@ def run_fast_scandir(_dir: str, full=False) -> tuple[list[str], list[str]]: files = [] try: - for _files in os.scandir(_dir): - if _files.is_dir() and not _files.name.startswith("."): - subfolders.append(_files.path) - if _files.is_file(): - ext = os.path.splitext(_files.name)[1].lower() + for _file in os.scandir(_dir): + if _file.is_dir() and not _file.name.startswith("."): + subfolders.append(_file.path) + if _file.is_file(): + ext = os.path.splitext(_file.name)[1].lower() if ext in SUPPORTED_FILES: - files.append(_files.path) + files.append(win_replace_slash(_file.path)) if full or len(files) == 0: for _dir in list(subfolders): - sub_dirs, _files = run_fast_scandir(_dir, full=True) + sub_dirs, _file = run_fast_scandir(_dir, full=True) subfolders.extend(sub_dirs) - files.extend(_files) - except (PermissionError, FileNotFoundError, ValueError): + files.extend(_file) + except (OSError, PermissionError, FileNotFoundError, ValueError): return [], [] return subfolders, files @@ -191,7 +191,7 @@ def get_albumartists(albums: list[models.Album]) -> set[str]: def get_all_artists( - tracks: list[models.Track], albums: list[models.Album] + tracks: list[models.Track], albums: list[models.Album] ) -> list[models.Artist]: artists_from_tracks = get_artists_from_tracks(tracks) artist_from_albums = get_albumartists(albums) @@ -232,7 +232,8 @@ def bisection_search_string(strings: list[str], target: str) -> str | None: def get_home_res_path(filename: str): """ - Returns a path to resources in the home directory of this project. Used to resolve resources in builds. + Returns a path to resources in the home directory of this project. + Used to resolve resources in builds. """ try: return (CWD / ".." / filename).resolve() @@ -259,12 +260,7 @@ def is_windows(): return platform.system() == "Windows" -def split_artists(src: str): - artists = re.split(r"\s*[&,;]\s*", src) - return [a.strip() for a in artists] - - -def extract_featured_artists_from_title(title: str) -> list[str]: +def parse_feat_from_title(title: str) -> list[str]: """ Extracts featured artists from a song title using regex. """ @@ -275,7 +271,7 @@ def extract_featured_artists_from_title(title: str) -> list[str]: return [] artists = match.group(1) - artists = split_artists(artists) + artists = split_artists(artists, with_and=True) return artists @@ -284,3 +280,54 @@ def get_random_str(length=5): Generates a random string of length `length`. """ return "".join(random.choices(string.ascii_letters + string.digits, k=length)) + + +def win_replace_slash(path: str): + if is_windows(): + return path.replace("\\", "/").replace("//", "/") + + return path + + +def split_artists(src: str, with_and: bool = False): + exp = r"\s*(?:and|&|,|;)\s*" if with_and else r"\s*[,;]\s*" + + artists = re.split(exp, src) + return [a.strip() for a in artists] + +def parse_artist_from_filename(title: str): + """ + Extracts artist names from a song title using regex. + """ + + regex = r"^(.+?)\s*[-–—]\s*(?:.+?)$" + match = re.search(regex, title, re.IGNORECASE) + + if not match: + return [] + + artists = match.group(1) + artists = split_artists(artists) + return artists + + +def parse_title_from_filename(title: str): + """ + Extracts track title from a song title using regex. + """ + + regex = r"^(?:.+?)\s*[-–—]\s*(.+?)$" + match = re.search(regex, title, re.IGNORECASE) + + if not match: + return title + + res = match.group(1) + # remove text in brackets starting with "official" case insensitive + res = re.sub(r"\s*\([^)]*official[^)]*\)", "", res, flags=re.IGNORECASE) + return res.strip() + + +# for title in sample_titles: +# print(parse_artist_from_filename(title)) +# print(parse_title_from_filename(title)) diff --git a/manage.py b/manage.py index 7bfe7769..02b28f11 100644 --- a/manage.py +++ b/manage.py @@ -18,7 +18,6 @@ from app.utils import background, get_home_res_path, get_ip, is_windows werkzeug = logging.getLogger("werkzeug") werkzeug.setLevel(logging.ERROR) - class Variables: FLASK_PORT = 1970 FLASK_HOST = "localhost" @@ -180,6 +179,7 @@ if __name__ == "__main__": log_info() run_bg_checks() start_watchdog() + app.run( debug=True, threaded=True, diff --git a/poetry.lock b/poetry.lock index 69546dab..922dda6c 100644 --- a/poetry.lock +++ b/poetry.lock @@ -340,14 +340,14 @@ tornado = ["tornado (>=0.2)"] [[package]] name = "hypothesis" -version = "6.65.0" +version = "6.65.1" description = "A library for property-based testing" category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "hypothesis-6.65.0-py3-none-any.whl", hash = "sha256:24e3219b0b181414c06bb7a62649a6edb471f148d25c9c9687f47505b0f50b1c"}, - {file = "hypothesis-6.65.0.tar.gz", hash = "sha256:d25914dd4008b0292d116ac315f01f6691c5460c494a0291c01d96f4bc17fe68"}, + {file = "hypothesis-6.65.1-py3-none-any.whl", hash = "sha256:4b7ae16db09151d17e5feebea07f4f84693cc1573c25e280bc92e619df24182b"}, + {file = "hypothesis-6.65.1.tar.gz", hash = "sha256:fb9757f4b556fc73c2eaa2c1b7d39d0184c75e4cb77dadaf6fa59373838bd629"}, ] [package.dependencies] @@ -602,14 +602,14 @@ files = [ [[package]] name = "pathspec" -version = "0.10.3" +version = "0.11.0" description = "Utility library for gitignore style pattern matching of file paths." category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "pathspec-0.10.3-py3-none-any.whl", hash = "sha256:3c95343af8b756205e2aba76e843ba9520a24dd84f68c22b9f93251507509dd6"}, - {file = "pathspec-0.10.3.tar.gz", hash = "sha256:56200de4077d9d0791465aa9095a01d421861e405b5096955051deefd697d6f6"}, + {file = "pathspec-0.11.0-py3-none-any.whl", hash = "sha256:3a66eb970cbac598f9e5ccb5b2cf58930cd8e3ed86d393d541eaf2d8b1705229"}, + {file = "pathspec-0.11.0.tar.gz", hash = "sha256:64d338d4e0914e91c1792321e6907b5a593f1ab1851de7fc269557a21b30ebbc"}, ] [[package]] @@ -749,6 +749,33 @@ files = [ dev = ["pre-commit", "tox"] testing = ["pytest", "pytest-benchmark"] +[[package]] +name = "psutil" +version = "5.9.4" +description = "Cross-platform lib for process and system monitoring in Python." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "psutil-5.9.4-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:c1ca331af862803a42677c120aff8a814a804e09832f166f226bfd22b56feee8"}, + {file = "psutil-5.9.4-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:68908971daf802203f3d37e78d3f8831b6d1014864d7a85937941bb35f09aefe"}, + {file = "psutil-5.9.4-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:3ff89f9b835100a825b14c2808a106b6fdcc4b15483141482a12c725e7f78549"}, + {file = "psutil-5.9.4-cp27-cp27m-win32.whl", hash = "sha256:852dd5d9f8a47169fe62fd4a971aa07859476c2ba22c2254d4a1baa4e10b95ad"}, + {file = "psutil-5.9.4-cp27-cp27m-win_amd64.whl", hash = "sha256:9120cd39dca5c5e1c54b59a41d205023d436799b1c8c4d3ff71af18535728e94"}, + {file = "psutil-5.9.4-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:6b92c532979bafc2df23ddc785ed116fced1f492ad90a6830cf24f4d1ea27d24"}, + {file = "psutil-5.9.4-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:efeae04f9516907be44904cc7ce08defb6b665128992a56957abc9b61dca94b7"}, + {file = "psutil-5.9.4-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:54d5b184728298f2ca8567bf83c422b706200bcbbfafdc06718264f9393cfeb7"}, + {file = "psutil-5.9.4-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:16653106f3b59386ffe10e0bad3bb6299e169d5327d3f187614b1cb8f24cf2e1"}, + {file = "psutil-5.9.4-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:54c0d3d8e0078b7666984e11b12b88af2db11d11249a8ac8920dd5ef68a66e08"}, + {file = "psutil-5.9.4-cp36-abi3-win32.whl", hash = "sha256:149555f59a69b33f056ba1c4eb22bb7bf24332ce631c44a319cec09f876aaeff"}, + {file = "psutil-5.9.4-cp36-abi3-win_amd64.whl", hash = "sha256:fd8522436a6ada7b4aad6638662966de0d61d241cb821239b2ae7013d41a43d4"}, + {file = "psutil-5.9.4-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:6001c809253a29599bc0dfd5179d9f8a5779f9dffea1da0f13c53ee568115e1e"}, + {file = "psutil-5.9.4.tar.gz", hash = "sha256:3d7f9739eb435d4b1338944abe23f49584bde5395f27487d2ee25ad9a8774a62"}, +] + +[package.extras] +test = ["enum34", "ipaddress", "mock", "pywin32", "wmi"] + [[package]] name = "pyinstaller" version = "5.7.0" @@ -1261,4 +1288,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = ">=3.10,<3.12" -content-hash = "15b3fba920faab237353240b2b3dbb32603744f6a8ff19e77fe6296d5252c2d7" +content-hash = "54e3995dc11627cb8d20d27ba6d593e4d6d102698c9bd3c29d5505287d22b9e2" diff --git a/pyproject.toml b/pyproject.toml index 14d7090b..f5ee0b8b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,6 +17,7 @@ tqdm = "^4.64.0" rapidfuzz = "^2.13.7" tinytag = "^1.8.1" Unidecode = "^1.3.6" +psutil = "^5.9.4" [tool.poetry.dev-dependencies] pylint = "^2.15.5" @@ -28,6 +29,7 @@ pyinstaller = "^5.7.0" version = "^22.6.0" allow-prereleases = true + [build-system] requires = ["poetry-core>=1.0.0"] build-backend = "poetry.core.masonry.api" diff --git a/tests/test_utils.py b/tests/test_utils.py index 79de5973..765467a1 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,33 +1,37 @@ +from hypothesis import given +from hypothesis import strategies as st + import app.utils -from hypothesis import given, strategies as st -from app.utils import extract_featured_artists_from_title +from app.utils import parse_feat_from_title def test_extract_featured_artists_from_title(): test_titles = [ "Own it (Featuring Ed Sheeran & Stormzy)", + "Own it (Featuring Ed Sheeran and Stormzy)", "Autograph (On my line)(Feat. Lil Peep)(Deluxe)", "Why so sad? (with Juice Wrld, Lil Peep)", "Why so sad? (with Juice Wrld/Lil Peep)", "Simmer (with Burna Boy)", - "Simmer (without Burna Boy)" + "Simmer (without Burna Boy)", ] results = [ ["Ed Sheeran", "Stormzy"], - ['Lil Peep'], - ["Juice Wrld", "Lil Peep"], + ["Ed Sheeran", "Stormzy"], + ["Lil Peep"], ["Juice Wrld", "Lil Peep"], + ["Juice Wrld/Lil Peep"], ["Burna Boy"], - [] + [], ] for title, expected in zip(test_titles, results): - assert extract_featured_artists_from_title(title) == expected + assert parse_feat_from_title(title) == expected # === HYPOTHESIS GHOSTWRITER TESTS === -@given(__dir=st.text(), full=st.booleans()) -def test_fuzz_run_fast_scandir(__dir: str, full) -> None: - app.utils.run_fast_scandir(_dir=__dir, full=full) +# @given(__dir=st.text(), full=st.booleans()) +# def test_fuzz_run_fast_scandir(__dir: str, full) -> None: +# app.utils.run_fast_scandir(_dir=__dir, full=full) From 95c1524b68fcca896bb8073d9659c97039e83e22 Mon Sep 17 00:00:00 2001 From: geoffrey45 Date: Wed, 1 Feb 2023 13:34:53 +0300 Subject: [PATCH 14/34] feat: add --no-feat flag to disable extracting featured artists + support square brackets when extracting featured artists + remove feat artists from track title + fix dir browser for linux --- app/api/album.py | 10 +++------- app/api/folder.py | 44 +++++++++++++++++++++++++++++++------------- app/lib/searchlib.py | 2 +- app/models.py | 24 +++++++++++++++++------- app/settings.py | 12 +++++++----- app/utils.py | 20 +++++++++++++++----- manage.py | 25 ++++++++++++++++++++----- swingmusic.spec | 2 +- 8 files changed, 95 insertions(+), 44 deletions(-) diff --git a/app/api/album.py b/app/api/album.py index 99e3bb51..01c0f9a2 100644 --- a/app/api/album.py +++ b/app/api/album.py @@ -72,12 +72,9 @@ def get_album(): except AttributeError: album.duration = 0 - if ( - album.count == 1 - and tracks[0].title == album.title - # and tracks[0].track == 1 - # and tracks[0].disc == 1 - ): + album.check_is_single(tracks) + + if album.is_single: album.is_single = True else: album.check_type() @@ -129,7 +126,6 @@ def get_artist_albums(): return {"data": albums} - # @album_bp.route("/album/bio", methods=["POST"]) # def get_album_bio(): # """Returns the album bio for the given album.""" diff --git a/app/api/folder.py b/app/api/folder.py index c87c8e38..370070a9 100644 --- a/app/api/folder.py +++ b/app/api/folder.py @@ -54,6 +54,13 @@ def get_folder_tree(): "tracks": [], } + if is_windows(): + req_dir = req_dir + "/" + # TODO: Test this on Windows + else: + req_dir = "/" + req_dir + "/" if not req_dir.startswith("/") else req_dir + "/" + + print(req_dir) tracks, folders = GetFilesAndDirs(req_dir)() return { @@ -62,12 +69,20 @@ def get_folder_tree(): } -def get_all_drives(): +def get_all_drives(is_win: bool = False): """ - Returns a list of all the drives on a windows machine. + Returns a list of all the drives on a Windows machine. """ - drives = psutil.disk_partitions() - return [d.mountpoint for d in drives] + drives = psutil.disk_partitions(all=False) + drives = [d.mountpoint for d in drives] + + if is_win: + drives = [win_replace_slash(d) for d in drives] + else: + remove = ["/boot", "/boot/efi", "/tmp"] + drives = [d for d in drives if d not in remove] + + return drives @api.route("/folder/dir-browser", methods=["POST"]) @@ -85,15 +100,19 @@ def list_folders(): if req_dir == "$home": # req_dir = settings.USER_HOME_DIR - if is_win: - return { - "folders": [ - {"name": win_replace_slash(d), "path": win_replace_slash(d)} - for d in get_all_drives() - ] - } + # if is_win: + return { + "folders": [ + {"name": d, "path": d} + for d in get_all_drives(is_win=is_win) + ] + } - req_dir = req_dir + "/" + if is_win: + req_dir = req_dir + "/" + else: + req_dir = "/" + req_dir + "/" + req_dir = str(Path(req_dir).resolve()) try: entries = os.scandir(req_dir) @@ -109,7 +128,6 @@ def list_folders(): "folders": sorted(dirs, key=lambda i: i["name"]), } - # todo: # - handle showing windows disks in root_dir configuration diff --git a/app/lib/searchlib.py b/app/lib/searchlib.py index b77c125f..f952e859 100644 --- a/app/lib/searchlib.py +++ b/app/lib/searchlib.py @@ -43,7 +43,7 @@ class SearchTracks: Gets all songs with a given title. """ - tracks = [track.title for track in self.tracks] + tracks = [track.og_title for track in self.tracks] results = process.extract( self.query, tracks, diff --git a/app/models.py b/app/models.py index 7911ee5c..2600cf35 100644 --- a/app/models.py +++ b/app/models.py @@ -5,7 +5,7 @@ import dataclasses import json from dataclasses import dataclass -from app import utils +from app import utils, settings @dataclass(slots=True) @@ -55,20 +55,28 @@ class Track: image: str = "" artist_hashes: list[str] = dataclasses.field(default_factory=list) is_favorite: bool = False + og_title: str = "" def __post_init__(self): + self.og_title = self.title if self.artist is not None: artists = utils.split_artists(self.artist) - featured = utils.parse_feat_from_title(self.title) - original_lower = "-".join([a.lower() for a in artists]) - artists.extend([a for a in featured if a.lower() not in original_lower]) + if settings.EXTRACT_FEAT: + featured, new_title = utils.parse_feat_from_title(self.title) + original_lower = "-".join([a.lower() for a in artists]) + artists.extend([a for a in featured if a.lower() not in original_lower]) + + self.title = new_title + + if self.og_title == self.album: + self.album = new_title self.artist_hashes = [utils.create_hash(a, decode=True) for a in artists] self.artist = [Artist(a) for a in artists] - albumartists = str(self.albumartist).split(", ") + albumartists = utils.split_artists(self.albumartist) self.albumartist = [Artist(a) for a in albumartists] self.filetype = self.filepath.rsplit(".", maxsplit=1)[-1] @@ -157,8 +165,10 @@ class Album: if ( len(tracks) == 1 and tracks[0].title == self.title - and tracks[0].track == 1 - and tracks[0].disc == 1 + + # and tracks[0].track == 1 + # and tracks[0].disc == 1 + # Todo: Are the above commented checks necessary? ): self.is_single = True diff --git a/app/settings.py b/app/settings.py index ea2924cc..5a9dba37 100644 --- a/app/settings.py +++ b/app/settings.py @@ -48,7 +48,6 @@ SM_THUMB_PATH = os.path.join(THUMBS_PATH, "small") LG_THUMBS_PATH = os.path.join(THUMBS_PATH, "large") MUSIC_DIR = os.path.join(USER_HOME_DIR, "Music") - # TEST_DIR = "/home/cwilvx/Downloads/Telegram Desktop" # TEST_DIR = "/mnt/dfc48e0f-103b-426e-9bf9-f25d3743bc96/Music/Chill/Wolftyla Radio" # HOME_DIR = TEST_DIR @@ -80,10 +79,6 @@ USER_DATA_DB_NAME = "userdata.db" APP_DB_PATH = os.path.join(APP_DIR, APP_DB_NAME) USERDATA_DB_PATH = os.path.join(APP_DIR, USER_DATA_DB_NAME) - -# ===== Store ===== -USE_STORE = True - HELP_MESSAGE = """ Usage: swingmusic [options] @@ -91,10 +86,17 @@ Options: --build: Build the application --host: Set the host --port: Set the port + --no-feat: Do not extract featured artists from the song title --help, -h: Show this help message --version, -v: Show the version """ +EXTRACT_FEAT = True +""" +Whether to extract the featured artists from the song title +Changed using the --no-feat flag +""" + class TCOLOR: """ diff --git a/app/utils.py b/app/utils.py index 39b86250..0bca37d2 100644 --- a/app/utils.py +++ b/app/utils.py @@ -191,7 +191,7 @@ def get_albumartists(albums: list[models.Album]) -> set[str]: def get_all_artists( - tracks: list[models.Track], albums: list[models.Album] + tracks: list[models.Track], albums: list[models.Album] ) -> list[models.Artist]: artists_from_tracks = get_artists_from_tracks(tracks) artist_from_albums = get_albumartists(albums) @@ -260,19 +260,29 @@ def is_windows(): return platform.system() == "Windows" -def parse_feat_from_title(title: str) -> list[str]: +def parse_feat_from_title(title: str) -> tuple[list[str], str]: """ Extracts featured artists from a song title using regex. """ regex = r"\((?:feat|ft|featuring|with)\.?\s+(.+?)\)" + # regex for square brackets 👇 + sqr_regex = r"\[(?:feat|ft|featuring|with)\.?\s+(.+?)\]" + match = re.search(regex, title, re.IGNORECASE) if not match: - return [] + match = re.search(sqr_regex, title, re.IGNORECASE) + regex = sqr_regex + + if not match: + return [], title artists = match.group(1) artists = split_artists(artists, with_and=True) - return artists + + # remove "feat" group from title + new_title = re.sub(regex, "", title, flags=re.IGNORECASE) + return artists, new_title def get_random_str(length=5): @@ -295,6 +305,7 @@ def split_artists(src: str, with_and: bool = False): artists = re.split(exp, src) return [a.strip() for a in artists] + def parse_artist_from_filename(title: str): """ Extracts artist names from a song title using regex. @@ -327,7 +338,6 @@ def parse_title_from_filename(title: str): res = re.sub(r"\s*\([^)]*official[^)]*\)", "", res, flags=re.IGNORECASE) return res.strip() - # for title in sample_titles: # print(parse_artist_from_filename(title)) # print(parse_title_from_filename(title)) diff --git a/manage.py b/manage.py index 02b28f11..9af1c69a 100644 --- a/manage.py +++ b/manage.py @@ -8,6 +8,7 @@ from configparser import ConfigParser import PyInstaller.__main__ as bundler +from app import settings from app.api import create_api from app.functions import run_periodic_checks from app.lib.watchdogg import Watcher as WatchDog @@ -18,6 +19,7 @@ from app.utils import background, get_home_res_path, get_ip, is_windows werkzeug = logging.getLogger("werkzeug") werkzeug.setLevel(logging.ERROR) + class Variables: FLASK_PORT = 1970 FLASK_HOST = "localhost" @@ -57,6 +59,7 @@ class ArgsEnum: build = "--build" port = "--port" host = "--host" + no_feat = "--no-feat" help = ["--help", "-h"] version = ["--version", "-v"] @@ -66,6 +69,7 @@ class HandleArgs: self.handle_build() self.handle_host() self.handle_port() + self.handle_no_feat() self.handle_help() self.handle_version() @@ -130,6 +134,11 @@ class HandleArgs: Variables.FLASK_HOST = host # type: ignore + @staticmethod + def handle_no_feat(): + if ArgsEnum.no_feat in ARGS: + settings.EXTRACT_FEAT = False + @staticmethod def handle_help(): if any((a in ARGS for a in ArgsEnum.help)): @@ -154,11 +163,17 @@ def start_watchdog(): WatchDog().run() -def log_info(): - lines = " ---------------------------------------" +def log_startup_info(): + lines = "---------------------------------------" + # clears terminal 👇 os.system("cls" if os.name == "nt" else "echo -e \\\\033c") + # TODO: Check whether the line above breaks Windows terminal's CTRL D + print(lines) - print(f" {TCOLOR.HEADER}{APP_VERSION} {TCOLOR.ENDC}") + print(f"{TCOLOR.HEADER}{APP_VERSION} {TCOLOR.ENDC}") + + if not settings.EXTRACT_FEAT: + print(f"{TCOLOR.OKBLUE}Extracting featured artists from track titles: {TCOLOR.FAIL}DISABLED!{TCOLOR.ENDC}") adresses = [Variables.FLASK_HOST] @@ -167,7 +182,7 @@ def log_info(): for address in adresses: print( - f" Started app on: {TCOLOR.OKGREEN}http://{address}:{Variables.FLASK_PORT}{TCOLOR.ENDC}" + f"Started app on: {TCOLOR.OKGREEN}http://{address}:{Variables.FLASK_PORT}{TCOLOR.ENDC}" ) print(lines) @@ -176,7 +191,7 @@ def log_info(): if __name__ == "__main__": HandleArgs() - log_info() + log_startup_info() run_bg_checks() start_watchdog() diff --git a/swingmusic.spec b/swingmusic.spec index 97b51115..efd0e879 100644 --- a/swingmusic.spec +++ b/swingmusic.spec @@ -8,7 +8,7 @@ a = Analysis( ['manage.py'], pathex=[], binaries=[], - datas=[('assets', 'assets'), ('client', 'client'), ('app/migrations', 'app/migrations'), ('pyinstaller.config.ini', '.')], + datas=[('assets', 'assets'), ('client', 'client'), ('pyinstaller.config.ini', '.')], hiddenimports=[], hookspath=[], hooksconfig={}, From 838e19cf0fcd30b0b0c528ce79cb5803df4a6f6a Mon Sep 17 00:00:00 2001 From: geoffrey45 Date: Wed, 1 Feb 2023 14:00:21 +0300 Subject: [PATCH 15/34] fix: errors raised by Pycharm --- app/api/artist.py | 5 ++--- app/api/folder.py | 8 +------- app/api/playlist.py | 2 +- app/api/search.py | 6 +++--- app/db/sqlite/playlists.py | 20 +++----------------- app/db/sqlite/tracks.py | 2 +- app/functions.py | 4 ++-- app/lib/artistlib.py | 2 +- app/lib/folderslib.py | 4 ++-- app/lib/populate.py | 3 +-- app/lib/taglib.py | 7 +------ manage.py | 1 + tests/test_utils.py | 3 --- wsgi.py | 2 +- 14 files changed, 20 insertions(+), 49 deletions(-) diff --git a/app/api/artist.py b/app/api/artist.py index a25e3007..4106c05b 100644 --- a/app/api/artist.py +++ b/app/api/artist.py @@ -48,9 +48,9 @@ class ArtistsCache: """ for (index, albums) in enumerate(cls.artists): if albums.artisthash == artisthash: - return (albums.albums, index) + return albums.albums, index - return ([], -1) + return [], -1 @classmethod def albums_cached(cls, artisthash: str) -> bool: @@ -214,7 +214,6 @@ def get_artist_albums(artisthash: str): limit = int(limit) - all_albums = [] is_cached = ArtistsCache.albums_cached(artisthash) if not is_cached: diff --git a/app/api/folder.py b/app/api/folder.py index 370070a9..36132c77 100644 --- a/app/api/folder.py +++ b/app/api/folder.py @@ -22,6 +22,7 @@ def get_folder_tree(): Returns a list of all the folders and tracks in the given folder. """ data = request.get_json() + req_dir = "$home" if data is not None: try: @@ -60,7 +61,6 @@ def get_folder_tree(): else: req_dir = "/" + req_dir + "/" if not req_dir.startswith("/") else req_dir + "/" - print(req_dir) tracks, folders = GetFilesAndDirs(req_dir)() return { @@ -127,9 +127,3 @@ def list_folders(): return { "folders": sorted(dirs, key=lambda i: i["name"]), } - -# todo: - -# - handle showing windows disks in root_dir configuration -# - handle the above, but for all partitions mounted in linux. -# - handle the "\" in client's folder page breadcrumb diff --git a/app/api/playlist.py b/app/api/playlist.py index bfae7682..c9100ed0 100644 --- a/app/api/playlist.py +++ b/app/api/playlist.py @@ -97,7 +97,7 @@ def add_track_to_playlist(playlist_id: str): return {"error": "Track already exists in playlist"}, 409 add_artist_to_playlist(int(playlist_id), trackhash) - PL.update_last_updated(playlist_id) + PL.update_last_updated(int(playlist_id)) return {"msg": "Done"}, 200 diff --git a/app/api/search.py b/app/api/search.py index afd35156..62188201 100644 --- a/app/api/search.py +++ b/app/api/search.py @@ -199,20 +199,20 @@ def search_load_more(): if s_type == "tracks": t = SearchResults.tracks return { - "tracks": t[index : index + SEARCH_COUNT], + "tracks": t[index: index + SEARCH_COUNT], "more": len(t) > index + SEARCH_COUNT, } elif s_type == "albums": a = SearchResults.albums return { - "albums": a[index : index + SEARCH_COUNT], + "albums": a[index: index + SEARCH_COUNT], "more": len(a) > index + SEARCH_COUNT, } elif s_type == "artists": a = SearchResults.artists return { - "artists": a[index : index + SEARCH_COUNT], + "artists": a[index: index + SEARCH_COUNT], "more": len(a) > index + SEARCH_COUNT, } diff --git a/app/db/sqlite/playlists.py b/app/db/sqlite/playlists.py index f367c785..0e38ad26 100644 --- a/app/db/sqlite/playlists.py +++ b/app/db/sqlite/playlists.py @@ -90,23 +90,9 @@ class SQLitePlaylistMethods: @staticmethod def add_item_to_json_list(playlist_id: int, field: str, items: list[str]): """ - Adds a string item to a json dumped list using a playlist id and field name. Takes the playlist ID, a field name, an item to add to the field, and an error to raise if the item is already in the field. - - Parameters - ---------- - playlist_id : int - The ID of the playlist to add the item to. - field : str - The field in the database that you want to add the item to. - item : str - The item to add to the list. - error : Exception - The error to raise if the item is already in the list. - - Returns - ------- - A list of strings. - + Adds a string item to a json dumped list using a playlist id and field name. + Takes the playlist ID, a field name, + an item to add to the field, and an error to raise if the item is already in the field. """ sql = f"SELECT {field} FROM playlists WHERE id = ?" diff --git a/app/db/sqlite/tracks.py b/app/db/sqlite/tracks.py index ea1b7607..8ec8a00f 100644 --- a/app/db/sqlite/tracks.py +++ b/app/db/sqlite/tracks.py @@ -130,7 +130,7 @@ class SQLiteTrackMethods: cur.execute("DELETE FROM tracks WHERE filepath=?", (filepath,)) @staticmethod - def remove_tracks_by_folders(folders: list[str]): + def remove_tracks_by_folders(folders: set[str]): sql = "DELETE FROM tracks WHERE folder = ?" with SQLiteManager() as cur: diff --git a/app/functions.py b/app/functions.py index 375b1c14..3b039df9 100644 --- a/app/functions.py +++ b/app/functions.py @@ -7,8 +7,8 @@ from requests import ReadTimeout from app import utils from app.lib.artistlib import CheckArtistImages -from app.lib.colorlib import ProcessAlbumColors, ProcessArtistColors -from app.lib.populate import Populate, ProcessTrackThumbnails, PopulateCancelledError +from app.lib.colorlib import ProcessArtistColors +from app.lib.populate import Populate, PopulateCancelledError from app.lib.trackslib import validate_tracks from app.logger import log diff --git a/app/lib/artistlib.py b/app/lib/artistlib.py index 77f78356..1e4ac736 100644 --- a/app/lib/artistlib.py +++ b/app/lib/artistlib.py @@ -82,7 +82,7 @@ class CheckArtistImages: """ Checks if an artist image exists and downloads it if not. - :param artistname: The artist name + :param artist: The artist name """ img_path = Path(settings.ARTIST_IMG_SM_PATH) / f"{artist.artisthash}.webp" diff --git a/app/lib/folderslib.py b/app/lib/folderslib.py index 64906784..18ee2f71 100644 --- a/app/lib/folderslib.py +++ b/app/lib/folderslib.py @@ -20,7 +20,7 @@ class GetFilesAndDirs: try: entries = os.scandir(self.path) except FileNotFoundError: - return ([], []) + return [], [] dirs, files = [], [] @@ -56,4 +56,4 @@ class GetFilesAndDirs: folders = filter(lambda f: f.has_tracks, folders) - return (tracks, folders) # type: ignore + return tracks, folders # type: ignore diff --git a/app/lib/populate.py b/app/lib/populate.py index b1a4df1a..39cf853e 100644 --- a/app/lib/populate.py +++ b/app/lib/populate.py @@ -1,5 +1,4 @@ from concurrent.futures import ThreadPoolExecutor -import os from tqdm import tqdm from app import settings @@ -139,4 +138,4 @@ class ProcessTrackThumbnails: ) ) - results = [r for r in results] + list(results) diff --git a/app/lib/taglib.py b/app/lib/taglib.py index 4f1f16e8..afa7af69 100644 --- a/app/lib/taglib.py +++ b/app/lib/taglib.py @@ -111,7 +111,7 @@ def get_tags(filepath: str): if p == "" or p is None: maybe = parse_artist_from_filename(filename) - if maybe != []: + if maybe: setattr(tags, tag, ", ".join(maybe)) else: setattr(tags, tag, "Unknown") @@ -174,8 +174,3 @@ def get_tags(filepath: str): del tags[tag] return tags - - for tag in to_delete: - del tags[tag] - - return tags diff --git a/manage.py b/manage.py index 9af1c69a..90e361d6 100644 --- a/manage.py +++ b/manage.py @@ -181,6 +181,7 @@ def log_startup_info(): adresses = ["localhost", get_ip()] for address in adresses: + # noinspection HttpUrlsUsage print( f"Started app on: {TCOLOR.OKGREEN}http://{address}:{Variables.FLASK_PORT}{TCOLOR.ENDC}" ) diff --git a/tests/test_utils.py b/tests/test_utils.py index 765467a1..9c9e1ebf 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,7 +1,4 @@ from hypothesis import given -from hypothesis import strategies as st - -import app.utils from app.utils import parse_feat_from_title diff --git a/wsgi.py b/wsgi.py index 1ea1dcc9..bc1e420b 100644 --- a/wsgi.py +++ b/wsgi.py @@ -1,4 +1,4 @@ -from app import create_api +from app.api import create_api if __name__ == '__main__': app = create_api() From 7640f2cc1aaf03012752b437081d36d64f77dfc4 Mon Sep 17 00:00:00 2001 From: geoffrey45 Date: Wed, 1 Feb 2023 14:48:23 +0300 Subject: [PATCH 16/34] add TODO: Move parsing title, album and artist to startup. --- app/lib/taglib.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/lib/taglib.py b/app/lib/taglib.py index afa7af69..39c80645 100644 --- a/app/lib/taglib.py +++ b/app/lib/taglib.py @@ -116,6 +116,8 @@ def get_tags(filepath: str): else: setattr(tags, tag, "Unknown") + # TODO: Move parsing title, album and artist to startup. + to_check = ["album", "year", "albumartist"] for prop in to_check: p = getattr(tags, prop) From 43732ba3809ae39be92f187ab4967106f741d71e Mon Sep 17 00:00:00 2001 From: geoffrey45 Date: Wed, 1 Feb 2023 16:04:33 +0300 Subject: [PATCH 17/34] fix: artist and album colors not being assigned when root dir is changed --- app/lib/colorlib.py | 38 ++++++++++++++++++++------------------ app/lib/populate.py | 4 ++-- 2 files changed, 22 insertions(+), 20 deletions(-) diff --git a/app/lib/colorlib.py b/app/lib/colorlib.py index e3609c68..abfebda5 100644 --- a/app/lib/colorlib.py +++ b/app/lib/colorlib.py @@ -38,23 +38,19 @@ class ProcessAlbumColors: """ def __init__(self) -> None: - db_colors = db.get_all_albums() - db_albumhashes = "-".join([album[1] for album in db_colors]) - - albums = [a for a in Store.albums if a.albumhash not in db_albumhashes] + albums = [a for a in Store.albums if len(a.colors) == 0] with SQLiteManager() as cur: - for album in tqdm(albums, desc="Processing unprocessed album colors"): - if len(album.colors) == 0: - colors = self.process_color(album) + for album in tqdm(albums, desc="Processing missing album colors"): + colors = self.process_color(album) - if colors is None: - continue + if colors is None: + continue - album.set_colors(colors) + album.set_colors(colors) - color_str = json.dumps(colors) - db.insert_one_album(cur, album.albumhash, color_str) + color_str = json.dumps(colors) + db.insert_one_album(cur, album.albumhash, color_str) @staticmethod def process_color(album: Album): @@ -73,13 +69,10 @@ class ProcessArtistColors: """ def __init__(self) -> None: - db_colors: list[tuple] = list(adb.get_all_artists()) - db_artisthashes = "-".join([artist[1] for artist in db_colors]) - all_artists = [a for a in Store.artists if a.artisthash not in db_artisthashes] + all_artists = [a for a in Store.artists if len(a.colors) == 0] - for artist in tqdm(all_artists, desc="Processing unprocessed artist colors"): - if artist.artisthash not in db_artisthashes: - self.process_color(artist) + for artist in tqdm(all_artists, desc="Processing missing artist colors"): + self.process_color(artist) @staticmethod def process_color(artist: Artist): @@ -93,3 +86,12 @@ class ProcessArtistColors: if len(colors) > 0: adb.insert_one_artist(artisthash=artist.artisthash, colors=colors) Store.map_artist_color((0, artist.artisthash, json.dumps(colors))) + +# TODO: If item color is in db, get it, assign it to the item and continue. +# - Format all colors in the format: rgb(123, 123, 123) +# - Each digit should be 3 digits long. +# - Format all db colors into a master string of the format "-itemhash:colorhash-" +# - Find the item hash using index() and get the color using the index + number, where number +# is the length of the rgb string + 1 +# - Assign the color to the item and continue. +# - If the color is not in the db, extract it and add it to the db. diff --git a/app/lib/populate.py b/app/lib/populate.py index 39cf853e..0d77b049 100644 --- a/app/lib/populate.py +++ b/app/lib/populate.py @@ -43,8 +43,8 @@ class Populate: if len(dirs_to_scan) == 0: log.warning( ( - "The root directory is not configured. " - + "Open the app in your webbrowser to configure." + "The root directory is not configured. " + + "Open the app in your webbrowser to configure." ) ) return From 9d288db2de054898067402c53894f568bf8fda72 Mon Sep 17 00:00:00 2001 From: geoffrey45 Date: Wed, 1 Feb 2023 17:55:30 +0300 Subject: [PATCH 18/34] =?UTF-8?q?refactor:=20LASTFM=5FAPI=5FKEY=20->=20NOT?= =?UTF-8?q?=5FLASTFM=5FAPI=5FKEY=20=F0=9F=A4=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/settings.py b/app/settings.py index 5a9dba37..076e1700 100644 --- a/app/settings.py +++ b/app/settings.py @@ -61,7 +61,7 @@ IMG_PLAYLIST_URI = IMG_BASE_URI + "playlists/" # defaults DEFAULT_ARTIST_IMG = IMG_ARTIST_URI + "0.webp" -LAST_FM_API_KEY = "762db7a44a9e6fb5585661f5f2bdf23a" +NOT_LASTFM_API_KEY = "762db7a44a9e6fb5585661f5f2bdf23a" THUMB_SIZE = 400 SM_THUMB_SIZE = 64 From 7b9f5fdb1381ba5c0c056283a055ba2ea025855e Mon Sep 17 00:00:00 2001 From: geoffrey45 Date: Wed, 1 Feb 2023 21:03:30 +0300 Subject: [PATCH 19/34] fix: disable flask debug remove and revoke last fm api key --- app/settings.py | 3 --- manage.py | 2 +- wsgi.py | 2 +- 3 files changed, 2 insertions(+), 5 deletions(-) diff --git a/app/settings.py b/app/settings.py index 076e1700..18fa097f 100644 --- a/app/settings.py +++ b/app/settings.py @@ -60,9 +60,6 @@ IMG_PLAYLIST_URI = IMG_BASE_URI + "playlists/" # defaults DEFAULT_ARTIST_IMG = IMG_ARTIST_URI + "0.webp" - -NOT_LASTFM_API_KEY = "762db7a44a9e6fb5585661f5f2bdf23a" - THUMB_SIZE = 400 SM_THUMB_SIZE = 64 SM_ARTIST_IMG_SIZE = 64 diff --git a/manage.py b/manage.py index 90e361d6..cfb2d10a 100644 --- a/manage.py +++ b/manage.py @@ -197,7 +197,7 @@ if __name__ == "__main__": start_watchdog() app.run( - debug=True, + debug=False, threaded=True, host=Variables.FLASK_HOST, port=Variables.FLASK_PORT, diff --git a/wsgi.py b/wsgi.py index bc1e420b..6cde14e5 100644 --- a/wsgi.py +++ b/wsgi.py @@ -2,4 +2,4 @@ from app.api import create_api if __name__ == '__main__': app = create_api() - app.run(debug=True, threaded=True) + app.run(debug=False, threaded=True) From b1ac3e9a0787366cea611bcebb953f587771f1c9 Mon Sep 17 00:00:00 2001 From: geoffrey45 Date: Fri, 3 Feb 2023 23:13:40 +0300 Subject: [PATCH 20/34] check todo at api/folder.py line:60 ~ everything seems to work fine in Windows too + move ProcessArtistColors to Populate --- app/api/folder.py | 12 +++++------- app/functions.py | 2 -- app/lib/populate.py | 4 +++- app/lib/taglib.py | 2 +- app/lib/watchdogg.py | 2 +- app/settings.py | 8 ++++---- manage.py | 22 +++++++++++++++------- 7 files changed, 29 insertions(+), 23 deletions(-) diff --git a/app/api/folder.py b/app/api/folder.py index 36132c77..dd20bbfd 100644 --- a/app/api/folder.py +++ b/app/api/folder.py @@ -56,8 +56,9 @@ def get_folder_tree(): } if is_windows(): + # Trailing slash needed when drive letters are passed, + # Remember, the trailing slash is removed in the client. req_dir = req_dir + "/" - # TODO: Test this on Windows else: req_dir = "/" + req_dir + "/" if not req_dir.startswith("/") else req_dir + "/" @@ -96,16 +97,13 @@ def list_folders(): try: req_dir: str = data["folder"] except KeyError: - req_dir = "$home" + req_dir = "$root" - if req_dir == "$home": + if req_dir == "$root": # req_dir = settings.USER_HOME_DIR # if is_win: return { - "folders": [ - {"name": d, "path": d} - for d in get_all_drives(is_win=is_win) - ] + "folders": [{"name": d, "path": d} for d in get_all_drives(is_win=is_win)] } if is_win: diff --git a/app/functions.py b/app/functions.py index 3b039df9..e0567d62 100644 --- a/app/functions.py +++ b/app/functions.py @@ -36,6 +36,4 @@ def run_periodic_checks(): "Internet connection lost. Downloading artist images stopped." ) - ProcessArtistColors() - time.sleep(300) diff --git a/app/lib/populate.py b/app/lib/populate.py index 0d77b049..73490218 100644 --- a/app/lib/populate.py +++ b/app/lib/populate.py @@ -6,7 +6,7 @@ from app.db.sqlite.tracks import SQLiteTrackMethods from app.db.sqlite.settings import SettingsSQLMethods as sdb from app.db.sqlite.favorite import SQLiteFavoriteMethods as favdb from app.db.store import Store -from app.lib.colorlib import ProcessAlbumColors +from app.lib.colorlib import ProcessAlbumColors, ProcessArtistColors from app.lib.taglib import extract_thumb, get_tags from app.logger import log @@ -70,6 +70,8 @@ class Populate: ProcessTrackThumbnails() ProcessAlbumColors() + ProcessArtistColors() + @staticmethod def filter_untagged(tracks: list[Track], files: list[str]): diff --git a/app/lib/taglib.py b/app/lib/taglib.py index 39c80645..cb71be9e 100644 --- a/app/lib/taglib.py +++ b/app/lib/taglib.py @@ -116,7 +116,7 @@ def get_tags(filepath: str): else: setattr(tags, tag, "Unknown") - # TODO: Move parsing title, album and artist to startup. + # TODO: Move parsing title, album and artist to startup. (Maybe!) to_check = ["album", "year", "albumartist"] for prop in to_check: diff --git a/app/lib/watchdogg.py b/app/lib/watchdogg.py index f4003399..1fde5449 100644 --- a/app/lib/watchdogg.py +++ b/app/lib/watchdogg.py @@ -280,5 +280,5 @@ class Handler(PatternMatchingEventHandler): add_track(path) self.files_to_process_windows.remove(event.src_path) except OSError: - print("File is locked, skipping") + # File is locked, skipping pass diff --git a/app/settings.py b/app/settings.py index 18fa097f..1bf459db 100644 --- a/app/settings.py +++ b/app/settings.py @@ -28,13 +28,13 @@ def get_xdg_config_dir(): # ------- HELPER METHODS -------- -APP_VERSION = "Swing v.1.0.0.beta.1" +APP_VERSION = "v.1.1.0.beta" # paths XDG_CONFIG_DIR = get_xdg_config_dir() USER_HOME_DIR = os.path.expanduser("~") -CONFIG_FOLDER = "swing" if XDG_CONFIG_DIR != USER_HOME_DIR else ".swing" +CONFIG_FOLDER = "swingmusic" if XDG_CONFIG_DIR != USER_HOME_DIR else ".swingmusic" APP_DIR = os.path.join(XDG_CONFIG_DIR, CONFIG_FOLDER) IMG_PATH = os.path.join(APP_DIR, "images") @@ -90,8 +90,8 @@ Options: EXTRACT_FEAT = True """ -Whether to extract the featured artists from the song title -Changed using the --no-feat flag +Whether to extract the featured artists from the song title. +Changed using the `--no-feat` flag """ diff --git a/manage.py b/manage.py index cfb2d10a..3fa65a2d 100644 --- a/manage.py +++ b/manage.py @@ -164,31 +164,39 @@ def start_watchdog(): def log_startup_info(): - lines = "---------------------------------------" + lines = "------------------------------" # clears terminal 👇 os.system("cls" if os.name == "nt" else "echo -e \\\\033c") - # TODO: Check whether the line above breaks Windows terminal's CTRL D print(lines) - print(f"{TCOLOR.HEADER}{APP_VERSION} {TCOLOR.ENDC}") - - if not settings.EXTRACT_FEAT: - print(f"{TCOLOR.OKBLUE}Extracting featured artists from track titles: {TCOLOR.FAIL}DISABLED!{TCOLOR.ENDC}") + print(f"{TCOLOR.HEADER}SwingMusic {APP_VERSION} {TCOLOR.ENDC}") adresses = [Variables.FLASK_HOST] if Variables.FLASK_HOST == "0.0.0.0": adresses = ["localhost", get_ip()] + print("Started app on:") for address in adresses: # noinspection HttpUrlsUsage print( - f"Started app on: {TCOLOR.OKGREEN}http://{address}:{Variables.FLASK_PORT}{TCOLOR.ENDC}" + f"➤ {TCOLOR.OKGREEN}http://{address}:{Variables.FLASK_PORT}{TCOLOR.ENDC}" ) print(lines) print("\n") + if not settings.EXTRACT_FEAT: + print( + f"{TCOLOR.OKBLUE}Extracting featured artists from track titles: {TCOLOR.FAIL}DISABLED!{TCOLOR.ENDC}" + ) + + print( + f"{TCOLOR.OKBLUE}App data folder: {settings.APP_DIR}{TCOLOR.OKGREEN}{TCOLOR.ENDC}" + ) + + print("\n") + if __name__ == "__main__": HandleArgs() From 9754c1a5227ce077d680cebf5534b4a37fd28e39 Mon Sep 17 00:00:00 2001 From: geoffrey45 Date: Tue, 7 Feb 2023 12:31:00 +0300 Subject: [PATCH 21/34] feat: mark albums that start with "the essential" as compilations --- app/models.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/models.py b/app/models.py index 2600cf35..82350212 100644 --- a/app/models.py +++ b/app/models.py @@ -130,7 +130,7 @@ class Album: if self.is_compilation: return - self.is_EP = self.check_is_EP() + self.is_EP = self.check_is_ep() def check_is_soundtrack(self) -> bool: """ @@ -150,9 +150,9 @@ class Album: artists = [a.name for a in self.albumartists] # type: ignore artists = "".join(artists).lower() - return "various artists" in artists + return ("various artists" in artists) or self.title.lower().startswith('the essential') - def check_is_EP(self) -> bool: + def check_is_ep(self) -> bool: """ Checks if the album is an EP. """ From bf0073fcf8e08abce48f825118a8c2b7fc23bb50 Mon Sep 17 00:00:00 2001 From: geoffrey45 Date: Tue, 7 Feb 2023 12:40:17 +0300 Subject: [PATCH 22/34] feat: mark albums that contain "best of", "greatest hits", "#1 hits", "number ones" as compilations --- app/models.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/app/models.py b/app/models.py index 82350212..50d2642a 100644 --- a/app/models.py +++ b/app/models.py @@ -150,7 +150,15 @@ class Album: artists = [a.name for a in self.albumartists] # type: ignore artists = "".join(artists).lower() - return ("various artists" in artists) or self.title.lower().startswith('the essential') + if "various artists" in artists: + return True + + substrings = ["the essential", "best of", "greatest hits", "#1 hits", "number ones"] + for substring in substrings: + if substring in self.title.lower(): + return True + + return False def check_is_ep(self) -> bool: """ From 7675c0e5c9971e6e041009473afb85efce5289d8 Mon Sep 17 00:00:00 2001 From: geoffrey45 Date: Tue, 7 Feb 2023 12:50:50 +0300 Subject: [PATCH 23/34] feat: check if album is a concert --- app/models.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/app/models.py b/app/models.py index 50d2642a..cbf5b15c 100644 --- a/app/models.py +++ b/app/models.py @@ -109,6 +109,7 @@ class Album: is_single: bool = False is_EP: bool = False is_favorite: bool = False + is_live: bool = False genres: list[str] = dataclasses.field(default_factory=list) def __post_init__(self): @@ -126,6 +127,10 @@ class Album: if self.is_soundtrack: return + self.is_live = self.check_is_live_album() + if self.is_live: + return + self.is_compilation = self.check_is_compilation() if self.is_compilation: return @@ -160,6 +165,17 @@ class Album: return False + def check_is_live_album(self): + """ + Checks if the album is a live album. + """ + keywords = ["live from", "live at"] + for keyword in keywords: + if keyword in self.title.lower(): + return True + + return False + def check_is_ep(self) -> bool: """ Checks if the album is an EP. From 7335882365f7d87cd26e1613b6b9bce7577eb649 Mon Sep 17 00:00:00 2001 From: geoffrey45 Date: Tue, 7 Feb 2023 14:19:32 +0300 Subject: [PATCH 24/34] add "live in" to live album check strings --- app/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models.py b/app/models.py index cbf5b15c..3bfac663 100644 --- a/app/models.py +++ b/app/models.py @@ -169,7 +169,7 @@ class Album: """ Checks if the album is a live album. """ - keywords = ["live from", "live at"] + keywords = ["live from", "live at", "live in"] for keyword in keywords: if keyword in self.title.lower(): return True From c098025f0e2ed164b66b642af53232cfd77a8267 Mon Sep 17 00:00:00 2001 From: geoffrey45 Date: Tue, 7 Feb 2023 15:29:21 +0300 Subject: [PATCH 25/34] feat: support ";" as genre separator --- app/models.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/models.py b/app/models.py index 3bfac663..2fe5dd1b 100644 --- a/app/models.py +++ b/app/models.py @@ -83,8 +83,9 @@ class Track: self.image = self.albumhash + ".webp" if self.genre is not None: - self.genre = str(self.genre).replace("/", ", ") - self.genre = str(self.genre).lower().split(", ") + self.genre = str(self.genre).replace("/", ",").replace(";", ",") + self.genre = str(self.genre).lower().split(",") + self.genre = [g.strip() for g in self.genre] @dataclass From 0b0ff4218acc8138ad9bb88f4346a45b6184ee9c Mon Sep 17 00:00:00 2001 From: geoffrey45 Date: Tue, 7 Feb 2023 21:27:55 +0300 Subject: [PATCH 26/34] add "super hits" to compilation album trigger words --- app/models.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/models.py b/app/models.py index 2fe5dd1b..bce545d0 100644 --- a/app/models.py +++ b/app/models.py @@ -159,7 +159,8 @@ class Album: if "various artists" in artists: return True - substrings = ["the essential", "best of", "greatest hits", "#1 hits", "number ones"] + substrings = ["the essential", "best of", "greatest hits", "#1 hits", "number ones", "super hits"] + for substring in substrings: if substring in self.title.lower(): return True From 1be60f73e450d462286eb07987289e2dbbe3bd12 Mon Sep 17 00:00:00 2001 From: geoffrey45 Date: Tue, 7 Feb 2023 23:48:59 +0300 Subject: [PATCH 27/34] write sample migration structure --- app/migrations/__init__.py | 19 ++++++++++++++++++- app/migrations/main/__init__.py | 4 ++++ app/migrations/main/sample.py | 6 ++++++ app/migrations/userdata/__init__.py | 4 ++++ app/migrations/userdata/sample.py | 6 ++++++ app/setup/__init__.py | 4 +++- 6 files changed, 41 insertions(+), 2 deletions(-) create mode 100644 app/migrations/main/__init__.py create mode 100644 app/migrations/main/sample.py create mode 100644 app/migrations/userdata/__init__.py create mode 100644 app/migrations/userdata/sample.py diff --git a/app/migrations/__init__.py b/app/migrations/__init__.py index 6048a197..cf470dde 100644 --- a/app/migrations/__init__.py +++ b/app/migrations/__init__.py @@ -1,3 +1,20 @@ """ -Migrations module +Migrations module. + +Reads and applies the latest database migrations. """ +from .main import main_db_migrations +from .userdata import userdata_db_migrations + + +def apply_migrations(): + userdb_version = 0 + maindb_version = 0 + + for migration in main_db_migrations: + if migration.version > maindb_version: + migration.migrate() + + for migration in userdata_db_migrations: + if migration.version > userdb_version: + migration.migrate() diff --git a/app/migrations/main/__init__.py b/app/migrations/main/__init__.py new file mode 100644 index 00000000..2bbd1c0b --- /dev/null +++ b/app/migrations/main/__init__.py @@ -0,0 +1,4 @@ +from .sample import SampleMigrationModel + +main_db_migrations = [SampleMigrationModel] + diff --git a/app/migrations/main/sample.py b/app/migrations/main/sample.py new file mode 100644 index 00000000..ec24e3fa --- /dev/null +++ b/app/migrations/main/sample.py @@ -0,0 +1,6 @@ +class SampleMigrationModel: + version = 1 + + @staticmethod + def migrate(): + print("executing sample main db migration") diff --git a/app/migrations/userdata/__init__.py b/app/migrations/userdata/__init__.py new file mode 100644 index 00000000..980d343c --- /dev/null +++ b/app/migrations/userdata/__init__.py @@ -0,0 +1,4 @@ +from .sample import SampleMigrationModel + +userdata_db_migrations = [SampleMigrationModel] + diff --git a/app/migrations/userdata/sample.py b/app/migrations/userdata/sample.py new file mode 100644 index 00000000..5205a662 --- /dev/null +++ b/app/migrations/userdata/sample.py @@ -0,0 +1,6 @@ +class SampleMigrationModel: + version = 1 + + @staticmethod + def migrate(): + print("executing sample userdata db migration") diff --git a/app/setup/__init__.py b/app/setup/__init__.py index b31a87a6..f0c6fad2 100644 --- a/app/setup/__init__.py +++ b/app/setup/__init__.py @@ -10,13 +10,13 @@ from app.db.sqlite import create_connection, create_tables, queries from app.db.store import Store from app.settings import APP_DB_PATH, USERDATA_DB_PATH from app.utils import get_home_res_path +from app.migrations import apply_migrations config = ConfigParser() config_path = get_home_res_path("pyinstaller.config.ini") config.read(config_path) - try: IS_BUILD = config["DEFAULT"]["BUILD"] == "True" except KeyError: @@ -112,6 +112,8 @@ def setup_sqlite(): app_db_conn.close() playlist_db_conn.close() + apply_migrations() + Store.load_all_tracks() Store.process_folders() Store.load_albums() From 97e29c3254ac49057bc099424ed96c5559fff081 Mon Sep 17 00:00:00 2001 From: geoffrey45 Date: Thu, 9 Feb 2023 21:15:07 +0300 Subject: [PATCH 28/34] feat: add album dates to artist albums --- app/api/album.py | 6 +----- app/api/artist.py | 8 +++++++- app/models.py | 9 ++++++++- 3 files changed, 16 insertions(+), 7 deletions(-) diff --git a/app/api/album.py b/app/api/album.py index 01c0f9a2..4b0e826a 100644 --- a/app/api/album.py +++ b/app/api/album.py @@ -61,11 +61,7 @@ def get_album(): tracks = utils.remove_duplicates(tracks) album.count = len(tracks) - - for track in tracks: - if track.date != "Unknown": - album.date = track.date - break + album.get_date_from_tracks(tracks) try: album.duration = sum((t.duration for t in tracks)) diff --git a/app/api/artist.py b/app/api/artist.py index 4106c05b..486c0442 100644 --- a/app/api/artist.py +++ b/app/api/artist.py @@ -39,7 +39,7 @@ class ArtistsCache: Holds artist page cache. """ - artists: deque[CacheEntry] = deque(maxlen=6) + artists: deque[CacheEntry] = deque(maxlen=1) @classmethod def get_albums_by_artisthash(cls, artisthash: str): @@ -131,6 +131,7 @@ class ArtistsCache: album_tracks = Store.get_tracks_by_albumhash(album.albumhash) album_tracks = remove_duplicates(album_tracks) + album.get_date_from_tracks(album_tracks) album.check_is_single(album_tracks) entry.type_checked = True @@ -241,6 +242,10 @@ def get_artist_albums(artisthash: str): albums = list(albums) albums = remove_EPs_and_singles(albums) + compilations = [a for a in albums if a.is_compilation] + for c in compilations: + albums.remove(c) + appearances = filter(lambda a: artisthash not in a.albumartisthash, all_albums) appearances = list(appearances) @@ -257,6 +262,7 @@ def get_artist_albums(artisthash: str): "singles": singles[:limit], "eps": eps[:limit], "appearances": appearances[:limit], + "compilations": compilations[:limit] } diff --git a/app/models.py b/app/models.py index bce545d0..8297e1d7 100644 --- a/app/models.py +++ b/app/models.py @@ -159,7 +159,8 @@ class Album: if "various artists" in artists: return True - substrings = ["the essential", "best of", "greatest hits", "#1 hits", "number ones", "super hits"] + substrings = ["the essential", "best of", "greatest hits", "#1 hits", "number ones", "super hits", + "ultimate collection"] for substring in substrings: if substring in self.title.lower(): @@ -198,6 +199,12 @@ class Album: ): self.is_single = True + def get_date_from_tracks(self, tracks: list[Track]): + for track in tracks: + if track.date != "Unknown": + self.date = track.date + break + @dataclass class Playlist: From b77b1747f148cf1bb69d2fc9aee4068865bcc480 Mon Sep 17 00:00:00 2001 From: geoffrey45 Date: Sun, 12 Feb 2023 03:22:21 +0300 Subject: [PATCH 29/34] feat: add migration to move old files to xdg directory + add db column for migration version + handle pre-init migrations + handle post-init migration --- app/db/sqlite/migrations.py | 64 +++++++++++++++++++ app/db/sqlite/queries.py | 10 +++ app/migrations/__init__.py | 28 +++++++- app/migrations/_preinit/__init__.py | 38 +++++++++++ app/migrations/_preinit/move_to_xdg_folder.py | 49 ++++++++++++++ app/migrations/main/__init__.py | 10 ++- app/migrations/main/sample.py | 6 -- app/migrations/userdata/__init__.py | 10 ++- app/migrations/userdata/sample.py | 6 -- app/setup/__init__.py | 13 +++- 10 files changed, 215 insertions(+), 19 deletions(-) create mode 100644 app/db/sqlite/migrations.py create mode 100644 app/migrations/_preinit/__init__.py create mode 100644 app/migrations/_preinit/move_to_xdg_folder.py delete mode 100644 app/migrations/main/sample.py delete mode 100644 app/migrations/userdata/sample.py diff --git a/app/db/sqlite/migrations.py b/app/db/sqlite/migrations.py new file mode 100644 index 00000000..93763f59 --- /dev/null +++ b/app/db/sqlite/migrations.py @@ -0,0 +1,64 @@ +""" +Reads and saves the latest database migrations version. +""" + + +from app.db.sqlite.utils import SQLiteManager + + +class MigrationManager: + all_get_sql = "SELECT * FROM migrations" + pre_init_set_sql = "UPDATE migrations SET pre_init_version = ? WHERE id = 1" + post_init_set_sql = "UPDATE migrations SET post_init_version = ? WHERE id = 1" + + @classmethod + def get_preinit_version(cls) -> int: + """ + Returns the latest userdata pre-init database version. + """ + with SQLiteManager() as cur: + cur.execute(cls.all_get_sql) + return int(cur.fetchone()[1]) + + @classmethod + def get_maindb_postinit_version(cls) -> int: + """ + Returns the latest maindb post-init database version. + """ + with SQLiteManager() as cur: + cur.execute(cls.all_get_sql) + return int(cur.fetchone()[2]) + + @classmethod + def get_userdatadb_postinit_version(cls) -> int: + """ + Returns the latest userdata post-init database version. + """ + with SQLiteManager(userdata_db=True) as cur: + cur.execute(cls.all_get_sql) + return cur.fetchone()[2] + + # 👇 Setters 👇 + @classmethod + def set_preinit_version(cls, version: int): + """ + Sets the userdata pre-init database version. + """ + with SQLiteManager() as cur: + cur.execute(cls.pre_init_set_sql, (version,)) + + @classmethod + def set_maindb_postinit_version(cls, version: int): + """ + Sets the maindb post-init database version. + """ + with SQLiteManager() as cur: + cur.execute(cls.post_init_set_sql, (version,)) + + @classmethod + def set_userdatadb_postinit_version(cls, version: int): + """ + Sets the userdata post-init database version. + """ + with SQLiteManager(userdata_db=True) as cur: + cur.execute(cls.post_init_set_sql, (version,)) diff --git a/app/db/sqlite/queries.py b/app/db/sqlite/queries.py index ac95a3be..a12fe123 100644 --- a/app/db/sqlite/queries.py +++ b/app/db/sqlite/queries.py @@ -69,3 +69,13 @@ CREATE TABLE IF NOT EXISTS folders ( trackcount integer NOT NULL ); """ + +CREATE_MIGRATIONS_TABLE = """ +CREATE TABLE IF NOT EXISTS migrations ( + id integer PRIMARY KEY, + pre_init_version integer NOT NULL DEFAULT 0, + post_init_version integer NOT NULL DEFAULT 0 +); + +INSERT INTO migrations (pre_init_version, post_init_version) VALUES (0, 0); +""" diff --git a/app/migrations/__init__.py b/app/migrations/__init__.py index cf470dde..d3a1ab32 100644 --- a/app/migrations/__init__.py +++ b/app/migrations/__init__.py @@ -2,19 +2,43 @@ 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]. """ + + +from app.db.sqlite.migrations import MigrationManager +from app.logger import log + from .main import main_db_migrations from .userdata import userdata_db_migrations def apply_migrations(): - userdb_version = 0 - maindb_version = 0 + """ + Applies the latest database migrations. + """ + + userdb_version = MigrationManager.get_userdatadb_postinit_version() + maindb_version = MigrationManager.get_maindb_postinit_version() for migration in main_db_migrations: if migration.version > maindb_version: + log.info("Running new MAIN-DB post-init migration: %s", migration.name) migration.migrate() for migration in userdata_db_migrations: if migration.version > userdb_version: + log.info("Running new USERDATA-DB post-init migration: %s", migration.name) migration.migrate() + + +def set_postinit_migration_versions(): + """ + Sets the post-init migration versions. + """ + # TODO: Don't forget to remove the zeros below when you add a valid migration 👇. + MigrationManager.set_maindb_postinit_version(0) + MigrationManager.set_userdatadb_postinit_version(0) diff --git a/app/migrations/_preinit/__init__.py b/app/migrations/_preinit/__init__.py new file mode 100644 index 00000000..6b379386 --- /dev/null +++ b/app/migrations/_preinit/__init__.py @@ -0,0 +1,38 @@ +""" +Pre-init migrations are executed before the database is created. +Useful when you need to move files or folders before the database is created. + +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]. +""" +from sqlite3 import OperationalError + +from app.db.sqlite.migrations import MigrationManager +from app.logger import log + +from .move_to_xdg_folder import MoveToXdgFolder + +all_preinits = [MoveToXdgFolder] + + +def run_preinit_migrations(): + """ + Runs all pre-init migrations. + """ + try: + userdb_version = MigrationManager.get_preinit_version() + except (OperationalError): + userdb_version = 0 + + for migration in all_preinits: + if migration.version > userdb_version: + log.warn("Running new pre-init migration: %s", migration.name) + migration.migrate() + + +def set_preinit_migration_versions(): + """ + Sets the migration versions. + """ + MigrationManager.set_preinit_version(all_preinits[-1].version) diff --git a/app/migrations/_preinit/move_to_xdg_folder.py b/app/migrations/_preinit/move_to_xdg_folder.py new file mode 100644 index 00000000..78c5f0e7 --- /dev/null +++ b/app/migrations/_preinit/move_to_xdg_folder.py @@ -0,0 +1,49 @@ +""" +This migration handles moving the config folder to the XDG standard location. +It also handles moving the userdata and the downloaded artist images to the new location. +""" + + +import os +import shutil +from app.settings import APP_DIR, USER_HOME_DIR +from app.logger import log + + +class MoveToXdgFolder: + version = 1 + name = "MoveToXdgFolder" + + @staticmethod + def migrate(): + old_config_dir = os.path.join(USER_HOME_DIR, ".swing") + new_config_dir = APP_DIR + + if not os.path.exists(old_config_dir): + log.info("No old config folder found. Skipping migration.") + return + + log.info("Found old config folder: %s", old_config_dir) + old_imgs_dir = os.path.join(old_config_dir, "images") + + # move images to new location + if os.path.exists(old_imgs_dir): + shutil.copytree( + old_imgs_dir, + os.path.join(new_config_dir, "images"), + copy_function=shutil.copy2, + dirs_exist_ok=True, + ) + + log.warn("Moved artist images to: %s", new_config_dir) + + # move userdata.db to new location + userdata_db = os.path.join(old_config_dir, "userdata.db") + if os.path.exists(userdata_db): + shutil.copy2(userdata_db, new_config_dir) + + log.warn("Moved userdata.db to: %s", new_config_dir) + log.warn("Migration complete. ✅") + + # swing.db is not moved because the new code fixes bugs which require + # the whole database to be recreated anyway. (ie. the bug which caused duplicate album and artist color entries) diff --git a/app/migrations/main/__init__.py b/app/migrations/main/__init__.py index 2bbd1c0b..d1cbfb57 100644 --- a/app/migrations/main/__init__.py +++ b/app/migrations/main/__init__.py @@ -1,4 +1,10 @@ -from .sample import SampleMigrationModel +""" +Migrations for the main database. -main_db_migrations = [SampleMigrationModel] +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]. +""" + +main_db_migrations = [] diff --git a/app/migrations/main/sample.py b/app/migrations/main/sample.py deleted file mode 100644 index ec24e3fa..00000000 --- a/app/migrations/main/sample.py +++ /dev/null @@ -1,6 +0,0 @@ -class SampleMigrationModel: - version = 1 - - @staticmethod - def migrate(): - print("executing sample main db migration") diff --git a/app/migrations/userdata/__init__.py b/app/migrations/userdata/__init__.py index 980d343c..d7ad1765 100644 --- a/app/migrations/userdata/__init__.py +++ b/app/migrations/userdata/__init__.py @@ -1,4 +1,10 @@ -from .sample import SampleMigrationModel +""" +Migrations for the userdata database. -userdata_db_migrations = [SampleMigrationModel] +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]. +""" + +userdata_db_migrations = [] diff --git a/app/migrations/userdata/sample.py b/app/migrations/userdata/sample.py deleted file mode 100644 index 5205a662..00000000 --- a/app/migrations/userdata/sample.py +++ /dev/null @@ -1,6 +0,0 @@ -class SampleMigrationModel: - version = 1 - - @staticmethod - def migrate(): - print("executing sample userdata db migration") diff --git a/app/setup/__init__.py b/app/setup/__init__.py index f0c6fad2..1a6eb7de 100644 --- a/app/setup/__init__.py +++ b/app/setup/__init__.py @@ -3,14 +3,19 @@ Contains the functions to prepare the server for use. """ import os import shutil +import time from configparser import ConfigParser from app import settings from app.db.sqlite import create_connection, create_tables, queries from app.db.store import Store +from app.migrations import apply_migrations, set_postinit_migration_versions +from app.migrations._preinit import ( + run_preinit_migrations, + set_preinit_migration_versions, +) from app.settings import APP_DB_PATH, USERDATA_DB_PATH from app.utils import get_home_res_path -from app.migrations import apply_migrations config = ConfigParser() @@ -102,6 +107,7 @@ def setup_sqlite(): """ # if os.path.exists(DB_PATH): # os.remove(DB_PATH) + run_preinit_migrations() app_db_conn = create_connection(APP_DB_PATH) playlist_db_conn = create_connection(USERDATA_DB_PATH) @@ -109,10 +115,15 @@ def setup_sqlite(): create_tables(app_db_conn, queries.CREATE_APPDB_TABLES) create_tables(playlist_db_conn, queries.CREATE_USERDATA_TABLES) + create_tables(app_db_conn, queries.CREATE_MIGRATIONS_TABLE) + create_tables(playlist_db_conn, queries.CREATE_MIGRATIONS_TABLE) + app_db_conn.close() playlist_db_conn.close() apply_migrations() + set_preinit_migration_versions() + set_postinit_migration_versions() Store.load_all_tracks() Store.process_folders() From a01501b9461c7cd37c3093e3daca7011434e87b1 Mon Sep 17 00:00:00 2001 From: geoffrey45 Date: Sun, 12 Feb 2023 23:36:08 +0300 Subject: [PATCH 30/34] fix: duplicate migration version entries on each startup --- app/db/sqlite/queries.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/db/sqlite/queries.py b/app/db/sqlite/queries.py index a12fe123..648ef691 100644 --- a/app/db/sqlite/queries.py +++ b/app/db/sqlite/queries.py @@ -77,5 +77,7 @@ CREATE TABLE IF NOT EXISTS migrations ( post_init_version integer NOT NULL DEFAULT 0 ); -INSERT INTO migrations (pre_init_version, post_init_version) VALUES (0, 0); -""" +INSERT INTO migrations (pre_init_version, post_init_version) +SELECT 0, 0 +WHERE NOT EXISTS (SELECT 1 FROM migrations); +""" \ No newline at end of file From 9cb8d372ec6442e783857d4b9c56b71c31ca986b Mon Sep 17 00:00:00 2001 From: geoffrey45 Date: Sun, 12 Feb 2023 23:40:33 +0300 Subject: [PATCH 31/34] remove unused files --- alice.spec | 44 ---------------------------------------- app/db/sqlite/queries.py | 2 +- manage.spec | 44 ---------------------------------------- requirements.txt | 30 --------------------------- roadmap.md | 39 ----------------------------------- start.sh | 9 -------- swing.spec | 44 ---------------------------------------- wsgi.py | 5 ----- 8 files changed, 1 insertion(+), 216 deletions(-) delete mode 100644 alice.spec delete mode 100644 manage.spec delete mode 100644 requirements.txt delete mode 100644 roadmap.md delete mode 100755 start.sh delete mode 100644 swing.spec delete mode 100644 wsgi.py diff --git a/alice.spec b/alice.spec deleted file mode 100644 index 5a0fc6f2..00000000 --- a/alice.spec +++ /dev/null @@ -1,44 +0,0 @@ -# -*- mode: python ; coding: utf-8 -*- - - -block_cipher = None - - -a = Analysis( - ['manage.py'], - pathex=[], - binaries=[], - datas=[('assets', 'assets'), ('client', 'client'), ('pyinstaller.config.ini', '.')], - hiddenimports=[], - hookspath=[], - hooksconfig={}, - runtime_hooks=[], - excludes=[], - win_no_prefer_redirects=False, - win_private_assemblies=False, - cipher=block_cipher, - noarchive=False, -) -pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher) - -exe = EXE( - pyz, - a.scripts, - a.binaries, - a.zipfiles, - a.datas, - [], - name='swing', - debug=False, - bootloader_ignore_signals=False, - strip=False, - upx=True, - upx_exclude=[], - runtime_tmpdir=None, - console=True, - disable_windowed_traceback=False, - argv_emulation=False, - target_arch=None, - codesign_identity=None, - entitlements_file=None, -) diff --git a/app/db/sqlite/queries.py b/app/db/sqlite/queries.py index 648ef691..94d48db3 100644 --- a/app/db/sqlite/queries.py +++ b/app/db/sqlite/queries.py @@ -80,4 +80,4 @@ CREATE TABLE IF NOT EXISTS migrations ( INSERT INTO migrations (pre_init_version, post_init_version) SELECT 0, 0 WHERE NOT EXISTS (SELECT 1 FROM migrations); -""" \ No newline at end of file +""" diff --git a/manage.spec b/manage.spec deleted file mode 100644 index 69183a79..00000000 --- a/manage.spec +++ /dev/null @@ -1,44 +0,0 @@ -# -*- mode: python ; coding: utf-8 -*- - - -block_cipher = None - - -a = Analysis( - ['manage.py'], - pathex=[], - binaries=[], - datas=[], - hiddenimports=[], - hookspath=[], - hooksconfig={}, - runtime_hooks=[], - excludes=[], - win_no_prefer_redirects=False, - win_private_assemblies=False, - cipher=block_cipher, - noarchive=False, -) -pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher) - -exe = EXE( - pyz, - a.scripts, - a.binaries, - a.zipfiles, - a.datas, - [], - name='manage', - debug=False, - bootloader_ignore_signals=False, - strip=False, - upx=True, - upx_exclude=[], - runtime_tmpdir=None, - console=True, - disable_windowed_traceback=False, - argv_emulation=False, - target_arch=None, - codesign_identity=None, - entitlements_file=None, -) diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index c312b0e5..00000000 --- a/requirements.txt +++ /dev/null @@ -1,30 +0,0 @@ -black @ file:///home/cwilvx/.cache/pypoetry/artifacts/6a/ca/67/2501f462728be2eb38d33f074ba5e8c08d49867e154b321b3f0b41db86/black-22.6.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl -cachelib @ file:///home/cwilvx/.cache/pypoetry/artifacts/cd/93/0e/3cc9b898ce11816a06c6fdc2c82b4f32443ad17db9ac94b2b74380ebdf/cachelib-0.7.0-py3-none-any.whl -certifi @ file:///home/cwilvx/.cache/pypoetry/artifacts/8c/ef/5f/67cf35ca016dcd84174e483e20fc3cfcd8bf39b6852e96236e852f31ba/certifi-2022.5.18.1-py3-none-any.whl -charset-normalizer @ file:///home/cwilvx/.cache/pypoetry/artifacts/d5/27/31/db4fb74906e3a7f55f720e0079ac1850dd86e30651cdfa5e1f04c53cfa/charset_normalizer-2.0.12-py3-none-any.whl -click @ file:///home/cwilvx/.cache/pypoetry/artifacts/63/f3/4c/2270b95f4d37b9ea73cd401abe68b6e9ede30380533cd4e7118a8e3aa3/click-8.1.3-py3-none-any.whl -colorgram.py @ file:///home/cwilvx/.cache/pypoetry/artifacts/b9/4f/19/0bfe8f89dd3c5df77fd3399df1820ed195abdb2f850e3d64336a672d1b/colorgram.py-1.2.0-py2.py3-none-any.whl -Flask @ file:///home/cwilvx/.cache/pypoetry/artifacts/61/b9/1a/04191a9edc7415cae23e0e84b682bd895d55cc79f68018278adbca71c8/Flask-2.1.2-py3-none-any.whl -Flask-Caching @ file:///home/cwilvx/.cache/pypoetry/artifacts/e9/38/2f/8faf7982cf117a9058f8e8c2140c686f929bf8911986c0ab697cae8448/Flask_Caching-1.11.1-py3-none-any.whl -Flask-Cors @ file:///home/cwilvx/.cache/pypoetry/artifacts/b7/c4/f4/3606582505f2ade21c9f72607db37c2bd347d83951df4749019c3d39f8/Flask_Cors-3.0.10-py2.py3-none-any.whl -gunicorn @ file:///home/cwilvx/.cache/pypoetry/artifacts/9f/68/9f/f1166be9473b4fe2cc59c98fac616db1f94b18662b9055d1ac940374e3/gunicorn-20.1.0-py3-none-any.whl -idna @ file:///home/cwilvx/.cache/pypoetry/artifacts/90/36/8c/81eabf6ac88608721ab27f439c9a6b9a8e6a21cc58c59ebb1a42720199/idna-3.3-py3-none-any.whl -itsdangerous @ file:///home/cwilvx/.cache/pypoetry/artifacts/2e/15/8d/e1a5243416994d875e03f548c0c5af64a08970297056408d4e67e6bc28/itsdangerous-2.1.2-py3-none-any.whl -jarowinkler @ file:///home/cwilvx/.cache/pypoetry/artifacts/37/93/e8/2c0fb4589d71bd0c06ac156569ba434fb917ce65e3fc77353dc1960e7a/jarowinkler-1.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl -Jinja2 @ file:///home/cwilvx/.cache/pypoetry/artifacts/49/36/ae/943f6cd852641f7249acddef711eb97d0c9ed91d7f435c798b6d7041ca/Jinja2-3.1.2-py3-none-any.whl -MarkupSafe @ file:///home/cwilvx/.cache/pypoetry/artifacts/dd/cc/d7/91f68383c04a15a87f0a2b31599de891c89b1d15e309273f759daf132c/MarkupSafe-2.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl -mutagen @ file:///home/cwilvx/.cache/pypoetry/artifacts/b4/fa/ad/d30a69658cc841ca38b77185eed0d97982259ce1cf1da32af87b376d4e/mutagen-1.45.1-py3-none-any.whl -mypy-extensions @ file:///home/cwilvx/.cache/pypoetry/artifacts/2f/c6/09/3e1afdcb75322c65b786e63cd7e879b6be3db36dea78ca376db5483ae4/mypy_extensions-0.4.3-py2.py3-none-any.whl -pathspec @ file:///home/cwilvx/.cache/pypoetry/artifacts/48/a0/9f/f5128d9e11d591bca7a942dd80ec44f9b2de8294775c68e0b99beeef93/pathspec-0.9.0-py2.py3-none-any.whl -Pillow @ file:///home/cwilvx/.cache/pypoetry/artifacts/95/cd/1a/99053885d95d74defc6d40d0bd7518f83fd74b133dfd762dfb523db565/Pillow-9.2.0-cp310-cp310-manylinux_2_28_x86_64.whl -platformdirs @ file:///home/cwilvx/.cache/pypoetry/artifacts/ea/8e/52/e5ac2f14474cef8f0fd44b4aa7d6968bfa89442d1b88ab567c446eae70/platformdirs-2.5.2-py3-none-any.whl -progress @ file:///home/cwilvx/.cache/pypoetry/artifacts/79/c2/d7/2a7bb2708100a9ccc186a9d3b9376c85fb53798080a3fe7480454fb17b/progress-1.6.tar.gz -pymongo @ file:///home/cwilvx/.cache/pypoetry/artifacts/64/8f/c6/6c691a87845035107c96bbd25b59f19d5cc716c2d46cbbdeb4ec149795/pymongo-4.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl -rapidfuzz @ file:///home/cwilvx/.cache/pypoetry/artifacts/9d/5e/96/b127feb34cd55e8eedc2ba19c53199ebceb9252389ec7a95cd4eb6e154/rapidfuzz-2.0.11-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl -requests @ file:///home/cwilvx/.cache/pypoetry/artifacts/d2/b2/c6/a04ce59140c6739203837d8dd0f518e29051b7ab61d2f34d4fd4241d30/requests-2.27.1-py2.py3-none-any.whl -six @ file:///home/cwilvx/.cache/pypoetry/artifacts/89/b2/f8/fd92b6d5daa0f8889429b2fc67ec21eedc5cae5d531ee2853828ced6c7/six-1.16.0-py2.py3-none-any.whl -tomli @ file:///home/cwilvx/.cache/pypoetry/artifacts/62/12/b6/6db9ebb9c8e1a6c5aa8a92ae73098d8119816b5e8507490916621bc305/tomli-2.0.1-py3-none-any.whl -tqdm @ file:///home/cwilvx/.cache/pypoetry/artifacts/9c/70/8c/d9fd60c1049cc4dba00815d66d598e9d5f265d4d59489e074827e331a9/tqdm-4.64.0-py2.py3-none-any.whl -urllib3 @ file:///home/cwilvx/.cache/pypoetry/artifacts/88/1c/d5/a55ed0245e5d7cd3a9f40dd75733644cbf7b7d94a6c521eb6c027a326c/urllib3-1.26.9-py2.py3-none-any.whl -watchdog @ file:///home/cwilvx/.cache/pypoetry/artifacts/0f/af/c3/b6575b0b5cab70c439d50980bd9673762b57878772f930d2d908ca83fc/watchdog-2.1.8-py3-none-manylinux2014_x86_64.whl -Werkzeug @ file:///home/cwilvx/.cache/pypoetry/artifacts/34/38/89/78911cfcd7dec75796d6056c730d94f730967bfe4fb4c5192b8d0d81ec/Werkzeug-2.1.2-py3-none-any.whl diff --git a/roadmap.md b/roadmap.md deleted file mode 100644 index 895a6410..00000000 --- a/roadmap.md +++ /dev/null @@ -1,39 +0,0 @@ -# Fixes ! - -- [ ] Click on artist image to go to artist page ⚠ -- [ ] Play next song if current song can't be loaded ⚠ - -- [ ] Removing song duplicates from queries -- [ ] Add support for WAV files -- [ ] Compress thumbnails - -# Features + - -## Needed features - -- [ ] Adding songs to queue - -- [ ] Add keyboard shortcuts -- [ ] Adjust volume -- [ ] Add listening statistics for all songs -- [ ] Extract color from artist image [for use with artist card gradient] -- [ ] Adding songs to favorites -- [ ] Playing song radio - -## Future features - -- [ ] Toggle shuffle -- [ ] Toggle repeat -- [ ] Suggest similar artists -- [ ] Getting artist info -- [ ] Create a Python script to build, bundle and serve the app -- [ ] Getting extra song info (probably from genius) -- [ ] Getting lyrics -- [ ] Sorting songs -- [ ] Suggest undiscorvered artists, albums and songs -- [ ] Remember last played song -- [ ] Add next and previous song transition and progress bar reset animations -- [ ] Add playlist to folder -- [ ] Add functionality to 'Listen now' button -- [ ] Paginated requests for songs -- [ ] Package app as installable PWA diff --git a/start.sh b/start.sh deleted file mode 100755 index 2d56a51c..00000000 --- a/start.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/zsh - -gpath=$(poetry run which gunicorn) -# pytest=$(poetry run which pytest) - -# $pytest # -q - -echo "Starting swing" -"$gpath" -b 0.0.0.0:1970 --threads=2 "manage:create_api()" diff --git a/swing.spec b/swing.spec deleted file mode 100644 index 5a0fc6f2..00000000 --- a/swing.spec +++ /dev/null @@ -1,44 +0,0 @@ -# -*- mode: python ; coding: utf-8 -*- - - -block_cipher = None - - -a = Analysis( - ['manage.py'], - pathex=[], - binaries=[], - datas=[('assets', 'assets'), ('client', 'client'), ('pyinstaller.config.ini', '.')], - hiddenimports=[], - hookspath=[], - hooksconfig={}, - runtime_hooks=[], - excludes=[], - win_no_prefer_redirects=False, - win_private_assemblies=False, - cipher=block_cipher, - noarchive=False, -) -pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher) - -exe = EXE( - pyz, - a.scripts, - a.binaries, - a.zipfiles, - a.datas, - [], - name='swing', - debug=False, - bootloader_ignore_signals=False, - strip=False, - upx=True, - upx_exclude=[], - runtime_tmpdir=None, - console=True, - disable_windowed_traceback=False, - argv_emulation=False, - target_arch=None, - codesign_identity=None, - entitlements_file=None, -) diff --git a/wsgi.py b/wsgi.py deleted file mode 100644 index 6cde14e5..00000000 --- a/wsgi.py +++ /dev/null @@ -1,5 +0,0 @@ -from app.api import create_api - -if __name__ == '__main__': - app = create_api() - app.run(debug=False, threaded=True) From 6b1cac38921671eec7aadbf60d0e3bc07600cb66 Mon Sep 17 00:00:00 2001 From: geoffrey45 Date: Sun, 12 Feb 2023 23:54:28 +0300 Subject: [PATCH 32/34] add comment to config --- manage.py | 2 +- pyinstaller.config.ini | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/manage.py b/manage.py index 3fa65a2d..04212e54 100644 --- a/manage.py +++ b/manage.py @@ -43,7 +43,7 @@ def serve_client_files(path): @app.route("/") def serve_client(): """ - Serves the index.html file at client/index.html. + Serves the index.html file at `client/index.html`. """ return app.send_static_file("index.html") diff --git a/pyinstaller.config.ini b/pyinstaller.config.ini index 99815867..eb477f3e 100644 --- a/pyinstaller.config.ini +++ b/pyinstaller.config.ini @@ -1,3 +1,6 @@ +; This file is used to determine whether the app is running in development or build mode. +; TODO: Find a better name for this file. + [DEFAULT] build = False From 728f4b864999e14e604668c8c71b470eabb05a77 Mon Sep 17 00:00:00 2001 From: geoffrey45 Date: Mon, 13 Feb 2023 08:59:24 +0300 Subject: [PATCH 33/34] fix: prevent duplicate favorite items --- app/db/sqlite/favorite.py | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/app/db/sqlite/favorite.py b/app/db/sqlite/favorite.py index 4db97b9a..56573a36 100644 --- a/app/db/sqlite/favorite.py +++ b/app/db/sqlite/favorite.py @@ -5,11 +5,26 @@ from .utils import SQLiteManager class SQLiteFavoriteMethods: """THis class contains methods for interacting with the favorites table.""" + @classmethod + def check_is_favorite(cls, itemhash: str, fav_type: str): + """ + Checks if an item is favorited. + """ + sql = """SELECT * FROM favorites WHERE hash = ? AND type = ?""" + with SQLiteManager(userdata_db=True) as cur: + cur.execute(sql, (itemhash, fav_type)) + items = cur.fetchall() + return len(items) > 0 + @classmethod def insert_one_favorite(cls, fav_type: str, fav_hash: str): """ Inserts a single favorite into the database. """ + # try to find the favorite in the database, if it exists, don't insert it + if cls.check_is_favorite(fav_hash, fav_type): + return + sql = """INSERT INTO favorites(type, hash) VALUES(?,?)""" with SQLiteManager(userdata_db=True) as cur: cur.execute(sql, (fav_type, fav_hash)) @@ -64,14 +79,3 @@ class SQLiteFavoriteMethods: with SQLiteManager(userdata_db=True) as cur: cur.execute(sql, (fav_hash, fav_type)) - - @classmethod - def check_is_favorite(cls, itemhash: str, fav_type: str): - """ - Checks if an item is favorited. - """ - sql = """SELECT * FROM favorites WHERE hash = ? AND type = ?""" - with SQLiteManager(userdata_db=True) as cur: - cur.execute(sql, (itemhash, fav_type)) - items = cur.fetchall() - return len(items) > 0 From 5cec16b5c1c76689c8ba79f223d5a53769d516f2 Mon Sep 17 00:00:00 2001 From: geoffrey45 Date: Wed, 15 Feb 2023 17:26:26 +0300 Subject: [PATCH 34/34] add comment --- pyinstaller.config.ini | 3 --- 1 file changed, 3 deletions(-) diff --git a/pyinstaller.config.ini b/pyinstaller.config.ini index eb477f3e..99815867 100644 --- a/pyinstaller.config.ini +++ b/pyinstaller.config.ini @@ -1,6 +1,3 @@ -; This file is used to determine whether the app is running in development or build mode. -; TODO: Find a better name for this file. - [DEFAULT] build = False