From 45062c31a1d030864644849f371033cae57b29d7 Mon Sep 17 00:00:00 2001 From: Cliff Hill Date: Wed, 29 Sep 2021 16:20:10 -0400 Subject: [PATCH] models now are inherently SQL tables with SQLAlchemy's tricks. Signed-off-by: Cliff Hill --- poetry.lock | 214 ++++++++++++++++++------------ pyproject.toml | 4 +- src/playlist/data/base.py | 2 + src/playlist/{ => data}/const.py | 17 ++- src/playlist/data/models.py | 65 +++++++-- src/playlist/data/settings.py | 80 +++++++++-- src/playlist/plex/server.py | 219 ++++++++++++++++++++++--------- src/playlist/sql/__init__.py | 1 - src/playlist/utils.py | 26 ++++ 9 files changed, 459 insertions(+), 169 deletions(-) rename src/playlist/{ => data}/const.py (56%) delete mode 100644 src/playlist/sql/__init__.py diff --git a/poetry.lock b/poetry.lock index 9387d66..9172f95 100644 --- a/poetry.lock +++ b/poetry.lock @@ -211,30 +211,26 @@ python-versions = ">=2.7" [[package]] name = "filelock" -version = "3.0.12" +version = "3.1.0" description = "A platform independent file lock." category = "dev" optional = false -python-versions = "*" - -[[package]] -name = "gino" -version = "1.0.1" -description = "GINO Is Not ORM - a Python asyncio ORM on SQLAlchemy core." -category = "main" -optional = false -python-versions = ">=3.5,<4.0" - -[package.dependencies] -asyncpg = ">=0.18,<1.0" -SQLAlchemy = ">=1.2.16,<1.4" +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" [package.extras] -starlette = ["gino-starlette (>=0.1.1,<0.2.0)"] -aiohttp = ["gino-aiohttp (>=0.1.0,<0.2.0)"] -tornado = ["gino-tornado (>=0.1.0,<0.2.0)"] -sanic = ["gino-sanic (>=0.1.0,<0.2.0)"] -quart = ["gino-quart (>=0.1.0,<0.2.0)"] +docs = ["furo (>=2021.8.17b43)", "sphinx (>=4.1)", "sphinx-autodoc-typehints (>=1.12)"] +testing = ["coverage (>=4)", "pytest (>=4)", "pytest-cov"] + +[[package]] +name = "greenlet" +version = "1.1.2" +description = "Lightweight in-process concurrent programming" +category = "main" +optional = false +python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*" + +[package.extras] +docs = ["sphinx"] [[package]] name = "identify" @@ -286,7 +282,7 @@ test = ["pytest (!=5.3.4)", "pytest-cov", "flaky", "nose", "ipyparallel"] [[package]] name = "ipython" -version = "7.27.0" +version = "7.28.0" description = "IPython: Productive Interactive Computing" category = "dev" optional = false @@ -408,7 +404,7 @@ qtconsole = "*" [[package]] name = "jupyter-client" -version = "7.0.3" +version = "7.0.5" description = "Jupyter protocol implementation and client libraries" category = "dev" optional = false @@ -546,7 +542,7 @@ test = ["codecov", "coverage", "ipython", "ipykernel", "ipywidgets", "pytest (>= [[package]] name = "nbconvert" -version = "6.1.0" +version = "6.2.0" description = "Converting Jupyter Notebooks" category = "dev" optional = false @@ -568,11 +564,11 @@ testpath = "*" traitlets = ">=5.0" [package.extras] -all = ["pytest", "pytest-cov", "pytest-dependency", "ipykernel", "ipywidgets (>=7)", "pyppeteer (==0.2.2)", "tornado (>=4.0)", "sphinx (>=1.5.1)", "sphinx-rtd-theme", "nbsphinx (>=0.2.12)", "ipython"] +all = ["pytest", "pytest-cov", "pytest-dependency", "ipykernel", "ipywidgets (>=7)", "pyppeteer (==0.2.6)", "tornado (>=4.0)", "sphinx (>=1.5.1)", "sphinx-rtd-theme", "nbsphinx (>=0.2.12)", "ipython"] docs = ["sphinx (>=1.5.1)", "sphinx-rtd-theme", "nbsphinx (>=0.2.12)", "ipython"] serve = ["tornado (>=4.0)"] -test = ["pytest", "pytest-cov", "pytest-dependency", "ipykernel", "ipywidgets (>=7)", "pyppeteer (==0.2.2)"] -webpdf = ["pyppeteer (==0.2.2)"] +test = ["pytest", "pytest-cov", "pytest-dependency", "ipykernel", "ipywidgets (>=7)", "pyppeteer (==0.2.6)"] +webpdf = ["pyppeteer (==0.2.6)"] [[package]] name = "nbformat" @@ -697,7 +693,7 @@ python-versions = "*" [[package]] name = "platformdirs" -version = "2.3.0" +version = "2.4.0" description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." category = "dev" optional = false @@ -1018,23 +1014,35 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" [[package]] name = "sqlalchemy" -version = "1.3.24" +version = "1.4.25" description = "Database Abstraction Library" category = "main" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" + +[package.dependencies] +greenlet = {version = "!=0.4.17", markers = "python_version >= \"3\" and (platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\")"} [package.extras] +aiomysql = ["greenlet (!=0.4.17)", "aiomysql"] +aiosqlite = ["typing_extensions (!=3.10.0.1)", "greenlet (!=0.4.17)", "aiosqlite"] +asyncio = ["greenlet (!=0.4.17)"] +asyncmy = ["greenlet (!=0.4.17)", "asyncmy (>=0.2.0)"] +mariadb_connector = ["mariadb (>=1.0.1)"] mssql = ["pyodbc"] mssql_pymssql = ["pymssql"] mssql_pyodbc = ["pyodbc"] -mysql = ["mysqlclient"] -oracle = ["cx-oracle"] -postgresql = ["psycopg2"] -postgresql_pg8000 = ["pg8000 (<1.16.6)"] +mypy = ["sqlalchemy2-stubs", "mypy (>=0.910)"] +mysql = ["mysqlclient (>=1.4.0,<2)", "mysqlclient (>=1.4.0)"] +mysql_connector = ["mysql-connector-python"] +oracle = ["cx_oracle (>=7,<8)", "cx_oracle (>=7)"] +postgresql = ["psycopg2 (>=2.7)"] +postgresql_asyncpg = ["greenlet (!=0.4.17)", "asyncpg"] +postgresql_pg8000 = ["pg8000 (>=1.16.6)"] postgresql_psycopg2binary = ["psycopg2-binary"] postgresql_psycopg2cffi = ["psycopg2cffi"] pymysql = ["pymysql (<1)", "pymysql"] +sqlcipher = ["sqlcipher3-binary"] [[package]] name = "terminado" @@ -1196,8 +1204,8 @@ notebook = ">=4.4.1" [metadata] lock-version = "1.1" -python-versions = ">=3.9,<3.11" -content-hash = "ddaa8d2b25ca8be048674b27b4e49951e5cf9271898f709761dff967f7cc48f3" +python-versions = ">=3.9,<3.10" +content-hash = "1f2c2176ef71a0bf6bcab39041763c91bc92735c595f0eb6d572f8a0f702b41b" [metadata.files] appdirs = [ @@ -1366,12 +1374,60 @@ entrypoints = [ {file = "entrypoints-0.3.tar.gz", hash = "sha256:c70dd71abe5a8c85e55e12c19bd91ccfeec11a6e99044204511f9ed547d48451"}, ] filelock = [ - {file = "filelock-3.0.12-py3-none-any.whl", hash = "sha256:929b7d63ec5b7d6b71b0fa5ac14e030b3f70b75747cef1b10da9b879fef15836"}, - {file = "filelock-3.0.12.tar.gz", hash = "sha256:18d82244ee114f543149c66a6e0c14e9c4f8a1044b5cdaadd0f82159d6a6ff59"}, + {file = "filelock-3.1.0-py2.py3-none-any.whl", hash = "sha256:d9e9c7d8191e915339843c81c90d3e44f7c84e5fb03bdc6b1b4d019025cf953b"}, + {file = "filelock-3.1.0.tar.gz", hash = "sha256:78925788ce8c8945fac28a68c1d05cf33a6a6c4fba14fe02835122c53268ceef"}, ] -gino = [ - {file = "gino-1.0.1-py3-none-any.whl", hash = "sha256:56df57cfdefbaf897a7c4897c265a0e91a8cca80716fb64f7d3cf6d501fdfb3d"}, - {file = "gino-1.0.1.tar.gz", hash = "sha256:fe4189e82fe9d20c4a5f03fc775fb91c168061c5176b4c95623caeef22316150"}, +greenlet = [ + {file = "greenlet-1.1.2-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:58df5c2a0e293bf665a51f8a100d3e9956febfbf1d9aaf8c0677cf70218910c6"}, + {file = "greenlet-1.1.2-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:aec52725173bd3a7b56fe91bc56eccb26fbdff1386ef123abb63c84c5b43b63a"}, + {file = "greenlet-1.1.2-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:833e1551925ed51e6b44c800e71e77dacd7e49181fdc9ac9a0bf3714d515785d"}, + {file = "greenlet-1.1.2-cp27-cp27m-win32.whl", hash = "sha256:aa5b467f15e78b82257319aebc78dd2915e4c1436c3c0d1ad6f53e47ba6e2713"}, + {file = "greenlet-1.1.2-cp27-cp27m-win_amd64.whl", hash = "sha256:40b951f601af999a8bf2ce8c71e8aaa4e8c6f78ff8afae7b808aae2dc50d4c40"}, + {file = "greenlet-1.1.2-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:95e69877983ea39b7303570fa6760f81a3eec23d0e3ab2021b7144b94d06202d"}, + {file = "greenlet-1.1.2-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:356b3576ad078c89a6107caa9c50cc14e98e3a6c4874a37c3e0273e4baf33de8"}, + {file = "greenlet-1.1.2-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:8639cadfda96737427330a094476d4c7a56ac03de7265622fcf4cfe57c8ae18d"}, + {file = "greenlet-1.1.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:97e5306482182170ade15c4b0d8386ded995a07d7cc2ca8f27958d34d6736497"}, + {file = "greenlet-1.1.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e6a36bb9474218c7a5b27ae476035497a6990e21d04c279884eb10d9b290f1b1"}, + {file = "greenlet-1.1.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:abb7a75ed8b968f3061327c433a0fbd17b729947b400747c334a9c29a9af6c58"}, + {file = "greenlet-1.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:14d4f3cd4e8b524ae9b8aa567858beed70c392fdec26dbdb0a8a418392e71708"}, + {file = "greenlet-1.1.2-cp35-cp35m-macosx_10_14_x86_64.whl", hash = "sha256:17ff94e7a83aa8671a25bf5b59326ec26da379ace2ebc4411d690d80a7fbcf23"}, + {file = "greenlet-1.1.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:9f3cba480d3deb69f6ee2c1825060177a22c7826431458c697df88e6aeb3caee"}, + {file = "greenlet-1.1.2-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:fa877ca7f6b48054f847b61d6fa7bed5cebb663ebc55e018fda12db09dcc664c"}, + {file = "greenlet-1.1.2-cp35-cp35m-win32.whl", hash = "sha256:7cbd7574ce8e138bda9df4efc6bf2ab8572c9aff640d8ecfece1b006b68da963"}, + {file = "greenlet-1.1.2-cp35-cp35m-win_amd64.whl", hash = "sha256:903bbd302a2378f984aef528f76d4c9b1748f318fe1294961c072bdc7f2ffa3e"}, + {file = "greenlet-1.1.2-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:049fe7579230e44daef03a259faa24511d10ebfa44f69411d99e6a184fe68073"}, + {file = "greenlet-1.1.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:dd0b1e9e891f69e7675ba5c92e28b90eaa045f6ab134ffe70b52e948aa175b3c"}, + {file = "greenlet-1.1.2-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:7418b6bfc7fe3331541b84bb2141c9baf1ec7132a7ecd9f375912eca810e714e"}, + {file = "greenlet-1.1.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f9d29ca8a77117315101425ec7ec2a47a22ccf59f5593378fc4077ac5b754fce"}, + {file = "greenlet-1.1.2-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:21915eb821a6b3d9d8eefdaf57d6c345b970ad722f856cd71739493ce003ad08"}, + {file = "greenlet-1.1.2-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eff9d20417ff9dcb0d25e2defc2574d10b491bf2e693b4e491914738b7908168"}, + {file = "greenlet-1.1.2-cp36-cp36m-win32.whl", hash = "sha256:32ca72bbc673adbcfecb935bb3fb1b74e663d10a4b241aaa2f5a75fe1d1f90aa"}, + {file = "greenlet-1.1.2-cp36-cp36m-win_amd64.whl", hash = "sha256:f0214eb2a23b85528310dad848ad2ac58e735612929c8072f6093f3585fd342d"}, + {file = "greenlet-1.1.2-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:b92e29e58bef6d9cfd340c72b04d74c4b4e9f70c9fa7c78b674d1fec18896dc4"}, + {file = "greenlet-1.1.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:fdcec0b8399108577ec290f55551d926d9a1fa6cad45882093a7a07ac5ec147b"}, + {file = "greenlet-1.1.2-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:93f81b134a165cc17123626ab8da2e30c0455441d4ab5576eed73a64c025b25c"}, + {file = "greenlet-1.1.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e12bdc622676ce47ae9abbf455c189e442afdde8818d9da983085df6312e7a1"}, + {file = "greenlet-1.1.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8c790abda465726cfb8bb08bd4ca9a5d0a7bd77c7ac1ca1b839ad823b948ea28"}, + {file = "greenlet-1.1.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f276df9830dba7a333544bd41070e8175762a7ac20350786b322b714b0e654f5"}, + {file = "greenlet-1.1.2-cp37-cp37m-win32.whl", hash = "sha256:64e6175c2e53195278d7388c454e0b30997573f3f4bd63697f88d855f7a6a1fc"}, + {file = "greenlet-1.1.2-cp37-cp37m-win_amd64.whl", hash = "sha256:b11548073a2213d950c3f671aa88e6f83cda6e2fb97a8b6317b1b5b33d850e06"}, + {file = "greenlet-1.1.2-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:9633b3034d3d901f0a46b7939f8c4d64427dfba6bbc5a36b1a67364cf148a1b0"}, + {file = "greenlet-1.1.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:eb6ea6da4c787111adf40f697b4e58732ee0942b5d3bd8f435277643329ba627"}, + {file = "greenlet-1.1.2-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:f3acda1924472472ddd60c29e5b9db0cec629fbe3c5c5accb74d6d6d14773478"}, + {file = "greenlet-1.1.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e859fcb4cbe93504ea18008d1df98dee4f7766db66c435e4882ab35cf70cac43"}, + {file = "greenlet-1.1.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:00e44c8afdbe5467e4f7b5851be223be68adb4272f44696ee71fe46b7036a711"}, + {file = "greenlet-1.1.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec8c433b3ab0419100bd45b47c9c8551248a5aee30ca5e9d399a0b57ac04651b"}, + {file = "greenlet-1.1.2-cp38-cp38-win32.whl", hash = "sha256:288c6a76705dc54fba69fbcb59904ae4ad768b4c768839b8ca5fdadec6dd8cfd"}, + {file = "greenlet-1.1.2-cp38-cp38-win_amd64.whl", hash = "sha256:8d2f1fb53a421b410751887eb4ff21386d119ef9cde3797bf5e7ed49fb51a3b3"}, + {file = "greenlet-1.1.2-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:166eac03e48784a6a6e0e5f041cfebb1ab400b394db188c48b3a84737f505b67"}, + {file = "greenlet-1.1.2-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:572e1787d1460da79590bf44304abbc0a2da944ea64ec549188fa84d89bba7ab"}, + {file = "greenlet-1.1.2-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:be5f425ff1f5f4b3c1e33ad64ab994eed12fc284a6ea71c5243fd564502ecbe5"}, + {file = "greenlet-1.1.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b1692f7d6bc45e3200844be0dba153612103db241691088626a33ff1f24a0d88"}, + {file = "greenlet-1.1.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7227b47e73dedaa513cdebb98469705ef0d66eb5a1250144468e9c3097d6b59b"}, + {file = "greenlet-1.1.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ff61ff178250f9bb3cd89752df0f1dd0e27316a8bd1465351652b1b4a4cdfd3"}, + {file = "greenlet-1.1.2-cp39-cp39-win32.whl", hash = "sha256:f70a9e237bb792c7cc7e44c531fd48f5897961701cdaa06cf22fc14965c496cf"}, + {file = "greenlet-1.1.2-cp39-cp39-win_amd64.whl", hash = "sha256:013d61294b6cd8fe3242932c1c5e36e5d1db2c8afb58606c5a67efce62c1f5fd"}, + {file = "greenlet-1.1.2.tar.gz", hash = "sha256:e30f5ea4ae2346e62cedde8794a56858a67b878dd79f7df76a0767e356b1744a"}, ] identify = [ {file = "identify-2.2.15-py2.py3-none-any.whl", hash = "sha256:de83a84d774921669774a2000bf87ebba46b4d1c04775f4a5d37deff0cf39f73"}, @@ -1390,8 +1446,8 @@ ipykernel = [ {file = "ipykernel-6.4.1.tar.gz", hash = "sha256:df3355e5eec23126bc89767a676c5f0abfc7f4c3497d118c592b83b316e8c0cd"}, ] ipython = [ - {file = "ipython-7.27.0-py3-none-any.whl", hash = "sha256:75b5e060a3417cf64f138e0bb78e58512742c57dc29db5a5058a2b1f0c10df02"}, - {file = "ipython-7.27.0.tar.gz", hash = "sha256:58b55ebfdfa260dad10d509702dc2857cb25ad82609506b070cf2d7b7df5af13"}, + {file = "ipython-7.28.0-py3-none-any.whl", hash = "sha256:f16148f9163e1e526f1008d7c8d966d9c15600ca20d1a754287cf96d00ba6f1d"}, + {file = "ipython-7.28.0.tar.gz", hash = "sha256:2097be5c814d1b974aea57673176a924c4c8c9583890e7a5f082f547b9975b11"}, ] ipython-genutils = [ {file = "ipython_genutils-0.2.0-py2.py3-none-any.whl", hash = "sha256:72dd37233799e619666c9f639a9da83c34013a73e8bbc79a7a6348d93c61fab8"}, @@ -1419,8 +1475,8 @@ jupyter = [ {file = "jupyter-1.0.0.zip", hash = "sha256:3e1f86076bbb7c8c207829390305a2b1fe836d471ed54be66a3b8c41e7f46cc7"}, ] jupyter-client = [ - {file = "jupyter_client-7.0.3-py3-none-any.whl", hash = "sha256:b07ceecb8f845f908bbd0f78bb17c0abac7b393de9d929bd92190e36c24c201e"}, - {file = "jupyter_client-7.0.3.tar.gz", hash = "sha256:bb58e3218d74e072673948bd1e2a6bb3b65f32447b3e8c143eeca16b946ee230"}, + {file = "jupyter_client-7.0.5-py3-none-any.whl", hash = "sha256:124a6e6979c38999d9153b1c4d1808c4c820a45066d5ed1857a5b59c04ffccb3"}, + {file = "jupyter_client-7.0.5.tar.gz", hash = "sha256:382aca66dcaf96d7eaaa6c546d57cdf8b3b1cf5bc1f2704c58a1d8d244f1163d"}, ] jupyter-console = [ {file = "jupyter_console-6.4.0-py3-none-any.whl", hash = "sha256:7799c4ea951e0e96ba8260575423cb323ea5a03fcf5503560fa3e15748869e27"}, @@ -1515,8 +1571,8 @@ nbclient = [ {file = "nbclient-0.5.4.tar.gz", hash = "sha256:6c8ad36a28edad4562580847f9f1636fe5316a51a323ed85a24a4ad37d4aefce"}, ] nbconvert = [ - {file = "nbconvert-6.1.0-py3-none-any.whl", hash = "sha256:37cd92ff2ae6a268e62075ff8b16129e0be4939c4dfcee53dc77cc8a7e06c684"}, - {file = "nbconvert-6.1.0.tar.gz", hash = "sha256:d22a8ff202644d31db254d24d52c3a96c82156623fcd7c7f987bba2612303ec9"}, + {file = "nbconvert-6.2.0-py3-none-any.whl", hash = "sha256:b1b9dc4f1ff6cafae0e6d91f42fb9046fdc32e6beb6d7e2fa2cd7191ad535240"}, + {file = "nbconvert-6.2.0.tar.gz", hash = "sha256:16ceecd0afaa8fd26c245fa32e2c52066c02f13aa73387fffafd84750baea863"}, ] nbformat = [ {file = "nbformat-5.1.3-py3-none-any.whl", hash = "sha256:eb8447edd7127d043361bc17f2f5a807626bc8e878c7709a1c647abda28a9171"}, @@ -1587,8 +1643,8 @@ pickleshare = [ {file = "pickleshare-0.7.5.tar.gz", hash = "sha256:87683d47965c1da65cdacaf31c8441d12b8044cdec9aca500cd78fc2c683afca"}, ] platformdirs = [ - {file = "platformdirs-2.3.0-py3-none-any.whl", hash = "sha256:8003ac87717ae2c7ee1ea5a84a1a61e87f3fbd16eb5aadba194ea30a9019f648"}, - {file = "platformdirs-2.3.0.tar.gz", hash = "sha256:15b056538719b1c94bdaccb29e5f81879c7f7f0f4a153f46086d155dffcd4f0f"}, + {file = "platformdirs-2.4.0-py3-none-any.whl", hash = "sha256:8868bbe3c3c80d42f20156f22e7131d2fb321f5bc86a2a345375c6481a67021d"}, + {file = "platformdirs-2.4.0.tar.gz", hash = "sha256:367a5e80b3d04d2428ffa76d33f124cf11e8fff2acdaa9b43d545f5c7d661ef2"}, ] plexapi = [ {file = "PlexAPI-4.7.1-py3-none-any.whl", hash = "sha256:ecd1b15271d704b13a1df7ea2b2ffc9595d24da4733bf6224ec1131776776dcb"}, @@ -1810,40 +1866,36 @@ six = [ {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, ] sqlalchemy = [ - {file = "SQLAlchemy-1.3.24-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:87a2725ad7d41cd7376373c15fd8bf674e9c33ca56d0b8036add2d634dba372e"}, - {file = "SQLAlchemy-1.3.24-cp27-cp27m-win32.whl", hash = "sha256:f597a243b8550a3a0b15122b14e49d8a7e622ba1c9d29776af741f1845478d79"}, - {file = "SQLAlchemy-1.3.24-cp27-cp27m-win_amd64.whl", hash = "sha256:fc4cddb0b474b12ed7bdce6be1b9edc65352e8ce66bc10ff8cbbfb3d4047dbf4"}, - {file = "SQLAlchemy-1.3.24-cp35-cp35m-macosx_10_14_x86_64.whl", hash = "sha256:f1149d6e5c49d069163e58a3196865e4321bad1803d7886e07d8710de392c548"}, - {file = "SQLAlchemy-1.3.24-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:14f0eb5db872c231b20c18b1e5806352723a3a89fb4254af3b3e14f22eaaec75"}, - {file = "SQLAlchemy-1.3.24-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:e98d09f487267f1e8d1179bf3b9d7709b30a916491997137dd24d6ae44d18d79"}, - {file = "SQLAlchemy-1.3.24-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:fc1f2a5a5963e2e73bac4926bdaf7790c4d7d77e8fc0590817880e22dd9d0b8b"}, - {file = "SQLAlchemy-1.3.24-cp35-cp35m-win32.whl", hash = "sha256:f3c5c52f7cb8b84bfaaf22d82cb9e6e9a8297f7c2ed14d806a0f5e4d22e83fb7"}, - {file = "SQLAlchemy-1.3.24-cp35-cp35m-win_amd64.whl", hash = "sha256:0352db1befcbed2f9282e72843f1963860bf0e0472a4fa5cf8ee084318e0e6ab"}, - {file = "SQLAlchemy-1.3.24-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:2ed6343b625b16bcb63c5b10523fd15ed8934e1ed0f772c534985e9f5e73d894"}, - {file = "SQLAlchemy-1.3.24-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:34fcec18f6e4b24b4a5f6185205a04f1eab1e56f8f1d028a2a03694ebcc2ddd4"}, - {file = "SQLAlchemy-1.3.24-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:e47e257ba5934550d7235665eee6c911dc7178419b614ba9e1fbb1ce6325b14f"}, - {file = "SQLAlchemy-1.3.24-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:816de75418ea0953b5eb7b8a74933ee5a46719491cd2b16f718afc4b291a9658"}, - {file = "SQLAlchemy-1.3.24-cp36-cp36m-win32.whl", hash = "sha256:26155ea7a243cbf23287f390dba13d7927ffa1586d3208e0e8d615d0c506f996"}, - {file = "SQLAlchemy-1.3.24-cp36-cp36m-win_amd64.whl", hash = "sha256:f03bd97650d2e42710fbe4cf8a59fae657f191df851fc9fc683ecef10746a375"}, - {file = "SQLAlchemy-1.3.24-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:a006d05d9aa052657ee3e4dc92544faae5fcbaafc6128217310945610d862d39"}, - {file = "SQLAlchemy-1.3.24-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:1e2f89d2e5e3c7a88e25a3b0e43626dba8db2aa700253023b82e630d12b37109"}, - {file = "SQLAlchemy-1.3.24-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:0d5d862b1cfbec5028ce1ecac06a3b42bc7703eb80e4b53fceb2738724311443"}, - {file = "SQLAlchemy-1.3.24-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:0172423a27fbcae3751ef016663b72e1a516777de324a76e30efa170dbd3dd2d"}, - {file = "SQLAlchemy-1.3.24-cp37-cp37m-win32.whl", hash = "sha256:d37843fb8df90376e9e91336724d78a32b988d3d20ab6656da4eb8ee3a45b63c"}, - {file = "SQLAlchemy-1.3.24-cp37-cp37m-win_amd64.whl", hash = "sha256:c10ff6112d119f82b1618b6dc28126798481b9355d8748b64b9b55051eb4f01b"}, - {file = "SQLAlchemy-1.3.24-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:861e459b0e97673af6cc5e7f597035c2e3acdfb2608132665406cded25ba64c7"}, - {file = "SQLAlchemy-1.3.24-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:5de2464c254380d8a6c20a2746614d5a436260be1507491442cf1088e59430d2"}, - {file = "SQLAlchemy-1.3.24-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:d375d8ccd3cebae8d90270f7aa8532fe05908f79e78ae489068f3b4eee5994e8"}, - {file = "SQLAlchemy-1.3.24-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:014ea143572fee1c18322b7908140ad23b3994036ef4c0d630110faf942652f8"}, - {file = "SQLAlchemy-1.3.24-cp38-cp38-win32.whl", hash = "sha256:6607ae6cd3a07f8a4c3198ffbf256c261661965742e2b5265a77cd5c679c9bba"}, - {file = "SQLAlchemy-1.3.24-cp38-cp38-win_amd64.whl", hash = "sha256:fcb251305fa24a490b6a9ee2180e5f8252915fb778d3dafc70f9cc3f863827b9"}, - {file = "SQLAlchemy-1.3.24-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:01aa5f803db724447c1d423ed583e42bf5264c597fd55e4add4301f163b0be48"}, - {file = "SQLAlchemy-1.3.24-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:4d0e3515ef98aa4f0dc289ff2eebb0ece6260bbf37c2ea2022aad63797eacf60"}, - {file = "SQLAlchemy-1.3.24-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:bce28277f308db43a6b4965734366f533b3ff009571ec7ffa583cb77539b84d6"}, - {file = "SQLAlchemy-1.3.24-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:8110e6c414d3efc574543109ee618fe2c1f96fa31833a1ff36cc34e968c4f233"}, - {file = "SQLAlchemy-1.3.24-cp39-cp39-win32.whl", hash = "sha256:ee5f5188edb20a29c1cc4a039b074fdc5575337c9a68f3063449ab47757bb064"}, - {file = "SQLAlchemy-1.3.24-cp39-cp39-win_amd64.whl", hash = "sha256:09083c2487ca3c0865dc588e07aeaa25416da3d95f7482c07e92f47e080aa17b"}, - {file = "SQLAlchemy-1.3.24.tar.gz", hash = "sha256:ebbb777cbf9312359b897bf81ba00dae0f5cb69fba2a18265dcc18a6f5ef7519"}, + {file = "SQLAlchemy-1.4.25-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:a36ea43919e51b0de0c0bc52bcfdad7683f6ea9fb81b340cdabb9df0e045e0f7"}, + {file = "SQLAlchemy-1.4.25-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:75cd5d48389a7635393ff5a9214b90695c06b3d74912109c3b00ce7392b69c6c"}, + {file = "SQLAlchemy-1.4.25-cp27-cp27m-win32.whl", hash = "sha256:16ef07e102d2d4f974ba9b0d4ac46345a411ad20ad988b3654d59ff08e553b1c"}, + {file = "SQLAlchemy-1.4.25-cp27-cp27m-win_amd64.whl", hash = "sha256:a79abdb404d9256afb8aeaa0d3a4bc7d3b6d8b66103d8b0f2f91febd3909976e"}, + {file = "SQLAlchemy-1.4.25-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:7ad59e2e16578b6c1a2873e4888134112365605b08a6067dd91e899e026efa1c"}, + {file = "SQLAlchemy-1.4.25-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:a505ecc0642f52e7c65afb02cc6181377d833b7df0994ecde15943b18d0fa89c"}, + {file = "SQLAlchemy-1.4.25-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a28fe28c359835f3be20c89efd517b35e8f97dbb2ca09c6cf0d9ac07f62d7ef6"}, + {file = "SQLAlchemy-1.4.25-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:41a916d815a3a23cb7fff8d11ad0c9b93369ac074e91e428075e088fe57d5358"}, + {file = "SQLAlchemy-1.4.25-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:842c49dd584aedd75c2ee05f6c950730c3ffcddd21c5824ed0f820808387e1e3"}, + {file = "SQLAlchemy-1.4.25-cp36-cp36m-win32.whl", hash = "sha256:6b602e3351f59f3999e9fb8b87e5b95cb2faab6a6ecdb482382ac6fdfbee5266"}, + {file = "SQLAlchemy-1.4.25-cp36-cp36m-win_amd64.whl", hash = "sha256:6400b22e4e41cc27623a9a75630b7719579cd9a3a2027bcf16ad5aaa9a7806c0"}, + {file = "SQLAlchemy-1.4.25-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:dd4ed12a775f2cde4519f4267d3601990a97d8ecde5c944ab06bfd6e8e8ea177"}, + {file = "SQLAlchemy-1.4.25-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b7778a205f956755e05721eebf9f11a6ac18b2409bff5db53ce5fe7ede79831"}, + {file = "SQLAlchemy-1.4.25-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:08d9396a2a38e672133266b31ed39b2b1f2b5ec712b5bff5e08033970563316a"}, + {file = "SQLAlchemy-1.4.25-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e93978993a2ad0af43f132be3ea8805f56b2f2cd223403ec28d3e7d5c6d39ed1"}, + {file = "SQLAlchemy-1.4.25-cp37-cp37m-win32.whl", hash = "sha256:0566a6e90951590c0307c75f9176597c88ef4be2724958ca1d28e8ae05ec8822"}, + {file = "SQLAlchemy-1.4.25-cp37-cp37m-win_amd64.whl", hash = "sha256:0b08a53e40b34205acfeb5328b832f44437956d673a6c09fce55c66ab0e54916"}, + {file = "SQLAlchemy-1.4.25-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:33a1e86abad782e90976de36150d910748b58e02cd7d35680d441f9a76806c18"}, + {file = "SQLAlchemy-1.4.25-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2ed67aae8cde4d32aacbdba4f7f38183d14443b714498eada5e5a7a37769c0b7"}, + {file = "SQLAlchemy-1.4.25-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1ebd69365717becaa1b618220a3df97f7c08aa68e759491de516d1c3667bba54"}, + {file = "SQLAlchemy-1.4.25-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26b0cd2d5c7ea96d3230cb20acac3d89de3b593339c1447b4d64bfcf4eac1110"}, + {file = "SQLAlchemy-1.4.25-cp38-cp38-win32.whl", hash = "sha256:c211e8ec81522ce87b0b39f0cf0712c998d4305a030459a0e115a2b3dc71598f"}, + {file = "SQLAlchemy-1.4.25-cp38-cp38-win_amd64.whl", hash = "sha256:9a1df8c93a0dd9cef0839917f0c6c49f46c75810cf8852be49884da4a7de3c59"}, + {file = "SQLAlchemy-1.4.25-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:1b38db2417b9f7005d6ceba7ce2a526bf10e3f6f635c0f163e6ed6a42b5b62b2"}, + {file = "SQLAlchemy-1.4.25-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e37621b37c73b034997b5116678862f38ee70e5a054821c7b19d0e55df270dec"}, + {file = "SQLAlchemy-1.4.25-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:91cd87d1de0111eaca11ccc3d31af441c753fa2bc22df72e5009cfb0a1af5b03"}, + {file = "SQLAlchemy-1.4.25-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90fe429285b171bcc252e21515703bdc2a4721008d1f13aa5b7150336f8a8493"}, + {file = "SQLAlchemy-1.4.25-cp39-cp39-win32.whl", hash = "sha256:6003771ea597346ab1e97f2f58405c6cacbf6a308af3d28a9201a643c0ac7bb3"}, + {file = "SQLAlchemy-1.4.25-cp39-cp39-win_amd64.whl", hash = "sha256:9ebe49c3960aa2219292ea2e5df6acdc425fc828f2f3d50b4cfae1692bcb5f02"}, + {file = "SQLAlchemy-1.4.25.tar.gz", hash = "sha256:1adf3d25e2e33afbcd48cfad8076f9378793be43e7fec3e4334306cac6bec138"}, ] terminado = [ {file = "terminado-0.12.1-py3-none-any.whl", hash = "sha256:09fdde344324a1c9c6e610ee4ca165c4bb7f5bbf982fceeeb38998a988ef8452"}, diff --git a/pyproject.toml b/pyproject.toml index 37978de..ccb5cab 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,16 +22,16 @@ classifiers = [ Changelog = "https://gitlab.com/xlorepdarkhelm/plex-playlist/releases" [tool.poetry.dependencies] -python = ">=3.9,<3.11" +python = ">=3.9,<3.10" click = "^7.0" PlexAPI = "^4.5.2" appdirs = "^1.4.4" PyYAML = "^5.4.1" -gino = "^1.0.1" asyncpg = "^0.23.0" uvloop = "^0.15.2" desert = "^2020.11.18" numpy = "^1.21.2" +SQLAlchemy = "^1.4.25" [tool.poetry.dev-dependencies] typeguard = "^2.12.0" diff --git a/src/playlist/data/base.py b/src/playlist/data/base.py index 31310b0..f29df80 100644 --- a/src/playlist/data/base.py +++ b/src/playlist/data/base.py @@ -52,6 +52,8 @@ class DataMeta(type): class BaseData(metaclass=DataMeta): """Base class for data classes in the application.""" + __sa_dataclass_metadata_key__ = "sa" + @classmethod def load(cls: type[BaseData], data: DataDict) -> DataSubtype: """Load the given data dictionary into a class instance.""" diff --git a/src/playlist/const.py b/src/playlist/data/const.py similarity index 56% rename from src/playlist/const.py rename to src/playlist/data/const.py index d728a82..5758a05 100644 --- a/src/playlist/const.py +++ b/src/playlist/data/const.py @@ -1,5 +1,8 @@ """Application-level constants.""" +import concurrent.futures import dataclasses +import datetime +import enum import pathlib import appdirs # type: ignore @@ -9,7 +12,19 @@ __all__ = ["APPNAME", "APPAUTHOR", "PATHS"] APPNAME = "plex-playlist" APPAUTHOR = "Cliff Hill" -DOWNLOAD_BATCH_SIZE = 1000 + +class Default(enum.Enum): + """Contains default values used for initializing settings.""" + + BATCH_SIZE: int = 1000 + PROCESS_DELAY: int = 0 + DURATION: int = 0 + PLAYTIME: datetime.timedelta = datetime.timedelta(days=1) + MAX_TRACKS: int = 0 + + +PROCESS_POOL = concurrent.futures.ProcessPoolExecutor() +MAX_PROCESSES = PROCESS_POOL._max_workers # type: ignore [attr-defined] @dataclasses.dataclass diff --git a/src/playlist/data/models.py b/src/playlist/data/models.py index b92f8e2..5952f89 100644 --- a/src/playlist/data/models.py +++ b/src/playlist/data/models.py @@ -3,23 +3,62 @@ import dataclasses import datetime import typing +import sqlalchemy.orm # type: ignore [import] + from playlist.data import base +mapper_registry = sqlalchemy.orm.registry() + +@mapper_registry.mapped @dataclasses.dataclass class Track(base.BaseData): """Model defining a Track object.""" - id: int - track_num: int - title: str - artist: str - album_num: int - album: str - album_artist: str - duration: int - rating: typing.Optional[int] - comments: str - added: datetime.datetime - play_count: int - played: typing.Optional[datetime.datetime] + __tablename__ = "tracks" + + id: int = dataclasses.field( + init=False, + metadata={"sa": sqlalchemy.Column(sqlalchemy.Integer, primary_key=True)}, + ) + plex_id: int = dataclasses.field( + metadata={"sa": sqlalchemy.Column(sqlalchemy.Integer, nullable=False)}, + ) + track_num: int = dataclasses.field( + metadata={"sa": sqlalchemy.Column(sqlalchemy.Integer, nullable=False)}, + ) + title: str = dataclasses.field( + metadata={"sa": sqlalchemy.Column(sqlalchemy.String(100), nullable=False)}, + ) + artist: str = dataclasses.field( + metadata={"sa": sqlalchemy.Column(sqlalchemy.String(100), nullable=False)}, + ) + album_num: int = dataclasses.field( + metadata={"sa": sqlalchemy.Column(sqlalchemy.Integer, nullable=False)}, + ) + album: str = dataclasses.field( + metadata={"sa": sqlalchemy.Column(sqlalchemy.String(100), nullable=False)}, + ) + album_artist: str = dataclasses.field( + metadata={"sa": sqlalchemy.Column(sqlalchemy.String(100), nullable=False)}, + ) + duration: int = dataclasses.field( + metadata={"sa": sqlalchemy.Column(sqlalchemy.Integer, nullable=False)}, + ) + comments: str = dataclasses.field( + metadata={"sa": sqlalchemy.Column(sqlalchemy.String(2000), nullable=False)}, + ) + added: datetime.datetime = dataclasses.field( + metadata={"sa": sqlalchemy.Column(sqlalchemy.DateTime, nullable=False)}, + ) + play_count: int = dataclasses.field( + metadata={"sa": sqlalchemy.Column(sqlalchemy.Integer, nullable=False)}, + ) + rating: typing.Optional[int] = dataclasses.field( + default=None, + metadata={"sa": sqlalchemy.Column(sqlalchemy.Integer)}, + ) + played: typing.Optional[datetime.datetime] = dataclasses.field( + default=None, + metadata={"sa": sqlalchemy.Column(sqlalchemy.DateTime)}, + ) diff --git a/src/playlist/data/settings.py b/src/playlist/data/settings.py index 3e9015f..7e5b6db 100644 --- a/src/playlist/data/settings.py +++ b/src/playlist/data/settings.py @@ -2,42 +2,93 @@ from __future__ import annotations import dataclasses +import datetime import functools -from playlist import const from playlist.data import base +from playlist.data import const -__all__ = ["get"] +__all__ = ["get", "write"] SETTINGS_PATH = const.PATHS.CONFIG / "settings.yaml" @dataclasses.dataclass -class Creds(base.BaseData): +class CredentialSettings(base.BaseData): """Credentials component for Settings object.""" baseurl: str token: str + @classmethod + def create(cls: type[CredentialSettings]) -> CredentialSettings: + import getpass -def _make_creds() -> Creds: - import getpass + from playlist.plex import server - from playlist.plex import server + username = input("Plex Username: ") + password = getpass.getpass("Plex Password: ") + servername = input("Plex Server: ") - username = input("Plex Username: ") - password = getpass.getpass("Plex Password: ") - servername = input("Plex Server: ") + data = server.get_creds(username, password, servername) + return cls.load(data) - data = server.get_creds(username, password, servername) - return Creds.load(data) + +@dataclasses.dataclass +class DownloadSettings(base.BaseData): + """DownloadSettingser options for Settings object.""" + + batch_size: int + process_delay: float + + @classmethod + def create(cls: type[DownloadSettings]) -> DownloadSettings: + return cls( + batch_size=const.Default.BATCH_SIZE.value, + process_delay=const.Default.PROCESS_DELAY.value, + ) + + +@dataclasses.dataclass +class TrackSettings(base.BaseData): + """Track options for Settings object.""" + + duration: float + + @classmethod + def create(cls: type[TrackSettings]) -> TrackSettings: + return cls(duration=const.Default.DURATION.value) + + +@dataclasses.dataclass +class PlaylistSettings(base.BaseData): + """Playlist options for Settings object.""" + + playtime: datetime.timedelta + max_tracks: int + + @classmethod + def create(cls: type[PlaylistSettings]) -> PlaylistSettings: + return cls( + playtime=const.Default.PLAYTIME.value, + max_tracks=const.Default.MAX_TRACKS.value, + ) @dataclasses.dataclass class Settings(base.YAMLData): """Settings object, loaded from settings.yaml file.""" - creds: Creds = dataclasses.field(default_factory=_make_creds) + creds: CredentialSettings = dataclasses.field( + default_factory=CredentialSettings.create, + ) + download: DownloadSettings = dataclasses.field( + default_factory=DownloadSettings.create, + ) + track: TrackSettings = dataclasses.field(default_factory=TrackSettings.create) + playlist: PlaylistSettings = dataclasses.field( + default_factory=PlaylistSettings.create, + ) @functools.cache @@ -51,3 +102,8 @@ def get() -> Settings: instance = Settings.yaml_create(SETTINGS_PATH) return instance + + +def write() -> None: + """Store the current Settings object.""" + get().yaml_write(SETTINGS_PATH) diff --git a/src/playlist/plex/server.py b/src/playlist/plex/server.py index bcfd018..28f0c9d 100644 --- a/src/playlist/plex/server.py +++ b/src/playlist/plex/server.py @@ -1,94 +1,72 @@ """Contains the code to communicate to the Plex server.""" import asyncio import collections.abc -import functools +import statistics +import time import typing import plexapi.audio # type: ignore [import] import plexapi.myplex # type: ignore [import] import plexapi.server # type: ignore [import] -from playlist import const +from playlist import utils +from playlist.data import const from playlist.data import models from playlist.data import settings __all__ = ["gen_tracks", "total_track_count", "get_creds"] -server = plexapi.server.PlexServer(**settings.get().creds.dump()) - - -def _downloader( - pos: int, - size: int, - loop: asyncio.AbstractEventLoop, - search_tracks: collections.abc.Callable, # type: ignore [type-arg] -) -> typing.Awaitable[list[plexapi.audio.Track]]: - return loop.run_in_executor( - None, - functools.partial( - search_tracks, - maxresults=size, - container_start=pos, - container_size=size, - ), - ) - - -def _track_dump(track: plexapi.audio.Track) -> models.Track.Dict: # type: ignore [name-defined] - return { - "id": track.ratingKey, - "track_num": track.index, - "title": track.title, - "artist": track.artist().title, - "album_num": track.parentIndex, - "album": track.parentTitle, - "album_artist": track.grandparentTitle, - "duration": track.duration, - "rating": track.userRating, - "comments": track.summary, - "added": str(track.addedAt), - "play_count": track.viewCount, - "played": str(track.lastViewedAt) if track.lastViewedAt is not None else None, - } - - -def get_creds( - username: str, - password: str, - server: str, -) -> settings.Creds.Dict: # type: ignore [name-defined] - """Get the credentials to store in the Settings instance.""" - account = plexapi.myplex.MyPlexAccount(username, password) - plex = account.reource(server).connect() - return {"baseurl": plex._baseurl, "token": plex._token} +throttle = asyncio.BoundedSemaphore(const.MAX_PROCESSES) async def gen_tracks( *, - batch_size: int = const.DOWNLOAD_BATCH_SIZE, + batch_size: int = settings.get().download.batch_size, ) -> typing.AsyncGenerator[models.Track, None]: """Generate all Tracks from the Server, asynchronously. Keyword Args: batch_size: determines how many Tracks are pulled from the Server at a time. + + Yields: + models.Track: The Track that was pulled from Plex. """ quotient, remainder = divmod(await total_track_count(), batch_size) loop = asyncio.get_running_loop() - search_tracks = server.library.section("Music").searchTracks batches = [ - _downloader(ndx, batch_size, loop, search_tracks) for ndx in range(quotient) + asyncio.create_task(_downloader(ndx, size, loop)) + async for ndx, size in _gen_batch_params(batch_size) ] - batches.append(_downloader(quotient, remainder, loop, search_tracks)) - for batch in asyncio.as_completed(batches): - batch_gen: collections.abc.Generator[models.Track, None, None] = ( - models.Track.load(_track_dump(plex_track)) for plex_track in await batch - ) - for track in batch_gen: + times = [] + weights = [] + durations = [] + for batch_task in asyncio.as_completed(batches): + process_time, batch = await batch_task + weight = len(batch) / batch_size + times.append(process_time * weight) + weights.append(weight) + for track_data in batch: + track: models.Track = models.Track.load(track_data) + durations.append(track.duration) yield track + avg_time_per_batch = utils.harmonic_mean(times, weights=weights) + avg_track_duration = statistics.geometric_mean(durations) + delay = avg_time_per_batch / const.MAX_PROCESSES + settings.get().download.process_delay = round(delay, ndigits=6) + settings.get().track.duration = round(avg_track_duration, ndigits=6) + playtime_seconds = settings.get().playlist.playtime.total_seconds() + settings.get().playlist.max_tracks = int(playtime_seconds / avg_track_duration) + settings.write() + async def total_track_count() -> int: - """Get the total number of tracks in the server.""" + """Get the total number of tracks in the server. + + Returns: + int: The total number of tracks in plex. + """ + server = plexapi.server.PlexServer(**settings.get().creds.dump()) loop = asyncio.get_running_loop() num_tracks: int = await loop.run_in_executor( None, @@ -100,3 +78,126 @@ async def total_track_count() -> int: ), ) return num_tracks + + +def get_creds( + username: str, + password: str, + server: str, +) -> settings.CredentialSettings.Dict: # type: ignore [name-defined] + """Get the credentials to store in the Settings instance. + + Args: + username: The username to log in as. + password: The password to log in with. + server: The server alias in Plex to use to connect to. + + Returns: + settings.CredentialSettings.Dict: The dictionary containing the baseurl and token string + to connect with. + """ + account = plexapi.myplex.MyPlexAccount(username, password) + plex = account.reource(server).connect() + return {"baseurl": plex._baseurl, "token": plex._token} + + +async def _downloader( + pos: int, + size: int, + loop: asyncio.AbstractEventLoop, +) -> tuple[float, list[models.Track.Dict]]: # type: ignore [name-defined] + """Download a batch of tracks from Plex, by running it inside a Process Pool. + + Args: + pos: The index number of the batch to retrieve. + size: The size of the batch to retrieve. + loop: The asyncio event loop to use. + + Returns: + tuple[float, list[models.Track.Dict]]: The time (in seconds) to process, and + the batch of Tracks that were retrieved, in dictionary form. + """ + async with throttle: + start = time.time() + batch = await loop.run_in_executor( + const.PROCESS_POOL, + _get_track_batch, + size, + pos, + ) + end = time.time() + process_time = end - start + return process_time, batch + + +def _get_track_batch( + size: int, + pos: int, +) -> list[models.Track.Dict]: # type: ignore [name-defined] + """Get a batch of Tracks from the Plex Server. + + Args: + size: The size of the batch to retrieve. + pos: The index number of the batch to retrieve. + + Returns: + list[models.Track.Dict]: The process time and list of Tracks retrieved, + in dictionary form. + """ + server = plexapi.server.PlexServer(**settings.get().creds.dump()) + batch = server.library.section("Music").searchTracks( + maxresults=size, + container_start=pos, + container_size=size, + ) + return [_track_dump(track) for track in batch] + + +def _track_dump(track: plexapi.audio.Track) -> models.Track.Dict: # type: ignore [name-defined] + """Convert a PlexAPI Audio Track object into a Track in dictionary form. + + Args: + track: The PlexAPI Audio Track to convert from. + + Returns: + models.Track.Dict: The dictionary form for the Track object converted to. + """ + return { + "plex_id": track.ratingKey, + "track_num": track.index, + "title": track.title, + "artist": track.artist().title, + "album_num": track.parentIndex, + "album": track.parentTitle, + "album_artist": track.grandparentTitle, + "duration": round(track.duration / 1000, ndigits=6), + "rating": track.userRating, + "comments": track.summary, + "added": str(track.addedAt), + "play_count": track.viewCount, + "played": str(track.lastViewedAt) if track.lastViewedAt is not None else None, + } + + +async def _gen_batch_params( + batch_size: int, +) -> collections.abc.AsyncGenerator[tuple[int, int], None]: + """Generate parameters for batches to be processed with. + + This slows down processing of the first few batches to allow for different process + pool workers to be staggered, and make the processing more smooth. + + Args: + batch_size: determines how many Tracks are pulled from the Plex Server at a time. + + Yields: + tuple[int, int]: The batch index and batch size to process. + """ + quotient, remainder = divmod(await total_track_count(), batch_size) + for ndx in range(quotient): + if ndx and ndx < const.MAX_PROCESSES: + await asyncio.sleep(settings.get().download.process_delay) + yield ndx, batch_size + if quotient and quotient < const.MAX_PROCESSES: + await asyncio.sleep(settings.get().download.process_delay) + yield quotient, remainder diff --git a/src/playlist/sql/__init__.py b/src/playlist/sql/__init__.py deleted file mode 100644 index a6f76bf..0000000 --- a/src/playlist/sql/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Package conaining the full SQL api/implementation.""" diff --git a/src/playlist/utils.py b/src/playlist/utils.py index e1c4905..62211f0 100644 --- a/src/playlist/utils.py +++ b/src/playlist/utils.py @@ -1,6 +1,7 @@ """Utility functions & classes used in the application.""" from __future__ import annotations +import collections.abc import contextlib import datetime import typing @@ -15,3 +16,28 @@ def time_this() -> typing.Generator[None, None, None]: finally: end = datetime.datetime.now() print(f"Elapsed time: {end - start}") + + +def harmonic_mean( + data: collections.abc.Sequence[float], + *, + weights: typing.Optional[collections.abc.Sequence[float]] = None, +) -> float: + """Calculate the [weighted] harmonic mean of the given data. + + Args: + data: The sequence of values to calculate. + + Keyword Args: + weights: The sequence of weights to apply to the calculation. If not + given it assumes all weights are 1. + + Returns: + float: The [weighted] harmonic mean of the data. + """ + if weights is None: + weights = [1] * len(data) + + numerator = sum(weights) + denominator = sum(weight / value for weight, value in zip(weights, data)) + return numerator / denominator