Compare commits
17 commits
Author | SHA1 | Date | |
---|---|---|---|
28748df3c1 | |||
c2f67c4781 | |||
413df0979d | |||
7dfd3f85e4 | |||
b41339a8c5 | |||
b9039b2ad4 | |||
41f5369b2f | |||
e1156b47de | |||
452867699e | |||
3ef3112014 | |||
c29f4b9b27 | |||
68609f3d07 | |||
085bca7974 | |||
dbd507bccb | |||
012bc0b025 | |||
d9c8e0fe28 | |||
b8bcdc5a83 |
40 changed files with 1277 additions and 561 deletions
pdm.lockpyproject.toml
schemata
src/mpd_now_playable
cache.py
config
mpd
playback
player.pyreceivers
song
song_receiver.pytools
stubs
AppKit
MediaPlayer
aiocache
boltons
mpd
516
pdm.lock
generated
516
pdm.lock
generated
|
@ -2,46 +2,49 @@
|
|||
# It is not intended for manual editing.
|
||||
|
||||
[metadata]
|
||||
groups = ["default", "all", "dev"]
|
||||
strategy = ["cross_platform"]
|
||||
lock_version = "4.4.1"
|
||||
content_hash = "sha256:ddedd388cce9ed181dc2f3786240fc14e19ceb4539f0a360eeb2efb28da63ebe"
|
||||
groups = ["default", "all", "dev", "memcached", "redis", "websockets"]
|
||||
strategy = []
|
||||
lock_version = "4.5.0"
|
||||
content_hash = "sha256:b84a0925a81adb7c4ca5a1a947ccb0db6950a18955bd92f08a605ff06cd0c26c"
|
||||
|
||||
[[metadata.targets]]
|
||||
requires_python = ">=3.12"
|
||||
|
||||
[[package]]
|
||||
name = "aiocache"
|
||||
version = "0.12.2"
|
||||
version = "0.12.3"
|
||||
summary = "multi backend asyncio cache"
|
||||
files = [
|
||||
{file = "aiocache-0.12.2-py2.py3-none-any.whl", hash = "sha256:9b6fa30634ab0bfc3ecc44928a91ff07c6ea16d27d55469636b296ebc6eb5918"},
|
||||
{file = "aiocache-0.12.2.tar.gz", hash = "sha256:b41c9a145b050a5dcbae1599f847db6dd445193b1f3bd172d8e0fe0cb9e96684"},
|
||||
{file = "aiocache-0.12.3-py2.py3-none-any.whl", hash = "sha256:889086fc24710f431937b87ad3720a289f7fc31c4fd8b68e9f918b9bacd8270d"},
|
||||
{file = "aiocache-0.12.3.tar.gz", hash = "sha256:f528b27bf4d436b497a1d0d1a8f59a542c153ab1e37c3621713cb376d44c4713"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aiocache"
|
||||
version = "0.12.2"
|
||||
version = "0.12.3"
|
||||
extras = ["memcached"]
|
||||
summary = "multi backend asyncio cache"
|
||||
dependencies = [
|
||||
"aiocache==0.12.2",
|
||||
"aiocache==0.12.3",
|
||||
"aiomcache>=0.5.2",
|
||||
]
|
||||
files = [
|
||||
{file = "aiocache-0.12.2-py2.py3-none-any.whl", hash = "sha256:9b6fa30634ab0bfc3ecc44928a91ff07c6ea16d27d55469636b296ebc6eb5918"},
|
||||
{file = "aiocache-0.12.2.tar.gz", hash = "sha256:b41c9a145b050a5dcbae1599f847db6dd445193b1f3bd172d8e0fe0cb9e96684"},
|
||||
{file = "aiocache-0.12.3-py2.py3-none-any.whl", hash = "sha256:889086fc24710f431937b87ad3720a289f7fc31c4fd8b68e9f918b9bacd8270d"},
|
||||
{file = "aiocache-0.12.3.tar.gz", hash = "sha256:f528b27bf4d436b497a1d0d1a8f59a542c153ab1e37c3621713cb376d44c4713"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aiocache"
|
||||
version = "0.12.2"
|
||||
version = "0.12.3"
|
||||
extras = ["redis"]
|
||||
summary = "multi backend asyncio cache"
|
||||
dependencies = [
|
||||
"aiocache==0.12.2",
|
||||
"aiocache==0.12.3",
|
||||
"redis>=4.2.0",
|
||||
]
|
||||
files = [
|
||||
{file = "aiocache-0.12.2-py2.py3-none-any.whl", hash = "sha256:9b6fa30634ab0bfc3ecc44928a91ff07c6ea16d27d55469636b296ebc6eb5918"},
|
||||
{file = "aiocache-0.12.2.tar.gz", hash = "sha256:b41c9a145b050a5dcbae1599f847db6dd445193b1f3bd172d8e0fe0cb9e96684"},
|
||||
{file = "aiocache-0.12.3-py2.py3-none-any.whl", hash = "sha256:889086fc24710f431937b87ad3720a289f7fc31c4fd8b68e9f918b9bacd8270d"},
|
||||
{file = "aiocache-0.12.3.tar.gz", hash = "sha256:f528b27bf4d436b497a1d0d1a8f59a542c153ab1e37c3621713cb376d44c4713"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -49,6 +52,9 @@ name = "aiomcache"
|
|||
version = "0.8.2"
|
||||
requires_python = ">=3.8"
|
||||
summary = "Minimal pure python memcached client"
|
||||
dependencies = [
|
||||
"typing-extensions>=4; python_version < \"3.11\"",
|
||||
]
|
||||
files = [
|
||||
{file = "aiomcache-0.8.2-py3-none-any.whl", hash = "sha256:9d78d6b6e74e775df18b350b1cddfa96bd2f0a44d49ad27fa87759a3469cef5e"},
|
||||
{file = "aiomcache-0.8.2.tar.gz", hash = "sha256:43b220d7f499a32a71871c4f457116eb23460fa216e69c1d32b81e3209e51359"},
|
||||
|
@ -59,29 +65,22 @@ name = "annotated-types"
|
|||
version = "0.7.0"
|
||||
requires_python = ">=3.8"
|
||||
summary = "Reusable constraint types to use with typing.Annotated"
|
||||
dependencies = [
|
||||
"typing-extensions>=4.0.0; python_version < \"3.9\"",
|
||||
]
|
||||
files = [
|
||||
{file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"},
|
||||
{file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "attrs"
|
||||
version = "23.2.0"
|
||||
requires_python = ">=3.7"
|
||||
summary = "Classes Without Boilerplate"
|
||||
files = [
|
||||
{file = "attrs-23.2.0-py3-none-any.whl", hash = "sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1"},
|
||||
{file = "attrs-23.2.0.tar.gz", hash = "sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "boltons"
|
||||
version = "24.0.0"
|
||||
version = "24.1.0"
|
||||
requires_python = ">=3.7"
|
||||
summary = "When they're not builtins, they're boltons."
|
||||
files = [
|
||||
{file = "boltons-24.0.0-py3-none-any.whl", hash = "sha256:9618695a6ec4f50412e7072e5d78910a00b4111d0b9b549e4a3d60bc321e7807"},
|
||||
{file = "boltons-24.0.0.tar.gz", hash = "sha256:7153feccaea1ff2e1472f68d4b57fadb796a2ee49d29f638f1c9cd8fb5ebd916"},
|
||||
{file = "boltons-24.1.0-py3-none-any.whl", hash = "sha256:a1776d47fdc387fb730fba1fe245f405ee184ee0be2fb447dd289773a84aed3b"},
|
||||
{file = "boltons-24.1.0.tar.gz", hash = "sha256:4a49b7d57ee055b83a458c8682a2a6f199d263a8aa517098bda9bab813554b87"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -167,21 +166,29 @@ files = [
|
|||
|
||||
[[package]]
|
||||
name = "mypy"
|
||||
version = "1.10.1"
|
||||
version = "1.14.1"
|
||||
requires_python = ">=3.8"
|
||||
summary = "Optional static typing for Python"
|
||||
dependencies = [
|
||||
"mypy-extensions>=1.0.0",
|
||||
"typing-extensions>=4.1.0",
|
||||
"tomli>=1.1.0; python_version < \"3.11\"",
|
||||
"typing-extensions>=4.6.0",
|
||||
]
|
||||
files = [
|
||||
{file = "mypy-1.10.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d8681909f7b44d0b7b86e653ca152d6dff0eb5eb41694e163c6092124f8246d7"},
|
||||
{file = "mypy-1.10.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:378c03f53f10bbdd55ca94e46ec3ba255279706a6aacaecac52ad248f98205d3"},
|
||||
{file = "mypy-1.10.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6bacf8f3a3d7d849f40ca6caea5c055122efe70e81480c8328ad29c55c69e93e"},
|
||||
{file = "mypy-1.10.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:701b5f71413f1e9855566a34d6e9d12624e9e0a8818a5704d74d6b0402e66c04"},
|
||||
{file = "mypy-1.10.1-cp312-cp312-win_amd64.whl", hash = "sha256:3c4c2992f6ea46ff7fce0072642cfb62af7a2484efe69017ed8b095f7b39ef31"},
|
||||
{file = "mypy-1.10.1-py3-none-any.whl", hash = "sha256:71d8ac0b906354ebda8ef1673e5fde785936ac1f29ff6987c7483cfbd5a4235a"},
|
||||
{file = "mypy-1.10.1.tar.gz", hash = "sha256:1f8f492d7db9e3593ef42d4f115f04e556130f2819ad33ab84551403e97dd4c0"},
|
||||
{file = "mypy-1.14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:30ff5ef8519bbc2e18b3b54521ec319513a26f1bba19a7582e7b1f58a6e69f14"},
|
||||
{file = "mypy-1.14.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cb9f255c18052343c70234907e2e532bc7e55a62565d64536dbc7706a20b78b9"},
|
||||
{file = "mypy-1.14.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b4e3413e0bddea671012b063e27591b953d653209e7a4fa5e48759cda77ca11"},
|
||||
{file = "mypy-1.14.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:553c293b1fbdebb6c3c4030589dab9fafb6dfa768995a453d8a5d3b23784af2e"},
|
||||
{file = "mypy-1.14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fad79bfe3b65fe6a1efaed97b445c3d37f7be9fdc348bdb2d7cac75579607c89"},
|
||||
{file = "mypy-1.14.1-cp312-cp312-win_amd64.whl", hash = "sha256:8fa2220e54d2946e94ab6dbb3ba0a992795bd68b16dc852db33028df2b00191b"},
|
||||
{file = "mypy-1.14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:92c3ed5afb06c3a8e188cb5da4984cab9ec9a77ba956ee419c68a388b4595255"},
|
||||
{file = "mypy-1.14.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:dbec574648b3e25f43d23577309b16534431db4ddc09fda50841f1e34e64ed34"},
|
||||
{file = "mypy-1.14.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8c6d94b16d62eb3e947281aa7347d78236688e21081f11de976376cf010eb31a"},
|
||||
{file = "mypy-1.14.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d4b19b03fdf54f3c5b2fa474c56b4c13c9dbfb9a2db4370ede7ec11a2c5927d9"},
|
||||
{file = "mypy-1.14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0c911fde686394753fff899c409fd4e16e9b294c24bfd5e1ea4675deae1ac6fd"},
|
||||
{file = "mypy-1.14.1-cp313-cp313-win_amd64.whl", hash = "sha256:8b21525cb51671219f5307be85f7e646a153e5acc656e5cebf64bfa076c50107"},
|
||||
{file = "mypy-1.14.1-py3-none-any.whl", hash = "sha256:b66a60cc4073aeb8ae00057f9c1f64d49e90f918fbcef9a977eb121da8b8f1d1"},
|
||||
{file = "mypy-1.14.1.tar.gz", hash = "sha256:7ec88144fe9b510e8475ec2f5f251992690fcf89ccb4500b214b4226abcd32d6"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -196,84 +203,118 @@ files = [
|
|||
|
||||
[[package]]
|
||||
name = "ormsgpack"
|
||||
version = "1.5.0"
|
||||
requires_python = ">=3.8"
|
||||
version = "1.7.0"
|
||||
requires_python = ">=3.9"
|
||||
summary = "Fast, correct Python msgpack library supporting dataclasses, datetimes, and numpy"
|
||||
files = [
|
||||
{file = "ormsgpack-1.5.0-cp312-cp312-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:a921b0d54b5fb5ba1ea4e87c65caa8992736224f1fc5ce8f46a882e918c8e22d"},
|
||||
{file = "ormsgpack-1.5.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c6d423668e2c3abdbc474562b1c73360ff7326f06cb9532dcb73254b5b63dae4"},
|
||||
{file = "ormsgpack-1.5.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eeb2dd4ed3e503a8266dcbfbb8d810a36baa34e4bb4229e90e9c213058a06d74"},
|
||||
{file = "ormsgpack-1.5.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f13bd643df1324e8797caba4c5c0168a87524df8424e8413ba29723e89a586a"},
|
||||
{file = "ormsgpack-1.5.0-cp312-none-win_amd64.whl", hash = "sha256:e016da381a126478c4bafab0ae19d3a2537f6471341ecced4bb61471e8841cad"},
|
||||
{file = "ormsgpack-1.5.0.tar.gz", hash = "sha256:00c0743ebaa8d21f1c868fbb609c99151ea79e67fec98b51a29077efd91ce348"},
|
||||
{file = "ormsgpack-1.7.0-cp312-cp312-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:77bc2ea387d85cfad045b9bcb8040bae43ad32dafe9363360f732cc19d489bbe"},
|
||||
{file = "ormsgpack-1.7.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ec763096d978d35eedcef0af13991a10741717c2e236b26f4c2047b0740ea7b"},
|
||||
{file = "ormsgpack-1.7.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:22418a4d399027a72fb2e6b873559b1886cf2e63323ca7afc17b222c454413b7"},
|
||||
{file = "ormsgpack-1.7.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:97723786755a7df85fcf6e68d7b5359dacea98d5c26b1d9af219a3cc05df4734"},
|
||||
{file = "ormsgpack-1.7.0-cp312-cp312-win_amd64.whl", hash = "sha256:7e6ada21f5c7a20ff7cf9b061c44e3814352f819947a12022ad8cb52a9f2a809"},
|
||||
{file = "ormsgpack-1.7.0-cp313-cp313-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:462089a419dbde654915ccb0b859c0dbe3c178b0ac580018e82befea6ccd73f4"},
|
||||
{file = "ormsgpack-1.7.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4b353204e99b56c1d33f1cf4767bd1fe1195596181a1cc789f25aa26c0b50f3d"},
|
||||
{file = "ormsgpack-1.7.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a5e12b51a590be47ccef67907905653e679fc2f920854b456edc216690ecc09c"},
|
||||
{file = "ormsgpack-1.7.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a6a97937d2cf21496d7689b90a43df83c5062bbe846aaa39197cc9ad73eaa7b"},
|
||||
{file = "ormsgpack-1.7.0-cp313-cp313-win_amd64.whl", hash = "sha256:8d301e47565fe0e52a60052e730a9bb7669dfbd2a94643b8be925e3928c64c15"},
|
||||
{file = "ormsgpack-1.7.0.tar.gz", hash = "sha256:6b4c98839cb7fc2a212037d2258f3a22857155249eb293d45c45cb974cfba834"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "propcache"
|
||||
version = "0.2.1"
|
||||
requires_python = ">=3.9"
|
||||
summary = "Accelerated property cache"
|
||||
files = [
|
||||
{file = "propcache-0.2.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:081a430aa8d5e8876c6909b67bd2d937bfd531b0382d3fdedb82612c618bc41a"},
|
||||
{file = "propcache-0.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d2ccec9ac47cf4e04897619c0e0c1a48c54a71bdf045117d3a26f80d38ab1fb0"},
|
||||
{file = "propcache-0.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:14d86fe14b7e04fa306e0c43cdbeebe6b2c2156a0c9ce56b815faacc193e320d"},
|
||||
{file = "propcache-0.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:049324ee97bb67285b49632132db351b41e77833678432be52bdd0289c0e05e4"},
|
||||
{file = "propcache-0.2.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1cd9a1d071158de1cc1c71a26014dcdfa7dd3d5f4f88c298c7f90ad6f27bb46d"},
|
||||
{file = "propcache-0.2.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98110aa363f1bb4c073e8dcfaefd3a5cea0f0834c2aab23dda657e4dab2f53b5"},
|
||||
{file = "propcache-0.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:647894f5ae99c4cf6bb82a1bb3a796f6e06af3caa3d32e26d2350d0e3e3faf24"},
|
||||
{file = "propcache-0.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bfd3223c15bebe26518d58ccf9a39b93948d3dcb3e57a20480dfdd315356baff"},
|
||||
{file = "propcache-0.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d71264a80f3fcf512eb4f18f59423fe82d6e346ee97b90625f283df56aee103f"},
|
||||
{file = "propcache-0.2.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:e73091191e4280403bde6c9a52a6999d69cdfde498f1fdf629105247599b57ec"},
|
||||
{file = "propcache-0.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3935bfa5fede35fb202c4b569bb9c042f337ca4ff7bd540a0aa5e37131659348"},
|
||||
{file = "propcache-0.2.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:f508b0491767bb1f2b87fdfacaba5f7eddc2f867740ec69ece6d1946d29029a6"},
|
||||
{file = "propcache-0.2.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:1672137af7c46662a1c2be1e8dc78cb6d224319aaa40271c9257d886be4363a6"},
|
||||
{file = "propcache-0.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b74c261802d3d2b85c9df2dfb2fa81b6f90deeef63c2db9f0e029a3cac50b518"},
|
||||
{file = "propcache-0.2.1-cp312-cp312-win32.whl", hash = "sha256:d09c333d36c1409d56a9d29b3a1b800a42c76a57a5a8907eacdbce3f18768246"},
|
||||
{file = "propcache-0.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:c214999039d4f2a5b2073ac506bba279945233da8c786e490d411dfc30f855c1"},
|
||||
{file = "propcache-0.2.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aca405706e0b0a44cc6bfd41fbe89919a6a56999157f6de7e182a990c36e37bc"},
|
||||
{file = "propcache-0.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:12d1083f001ace206fe34b6bdc2cb94be66d57a850866f0b908972f90996b3e9"},
|
||||
{file = "propcache-0.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d93f3307ad32a27bda2e88ec81134b823c240aa3abb55821a8da553eed8d9439"},
|
||||
{file = "propcache-0.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba278acf14471d36316159c94a802933d10b6a1e117b8554fe0d0d9b75c9d536"},
|
||||
{file = "propcache-0.2.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4e6281aedfca15301c41f74d7005e6e3f4ca143584ba696ac69df4f02f40d629"},
|
||||
{file = "propcache-0.2.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5b750a8e5a1262434fb1517ddf64b5de58327f1adc3524a5e44c2ca43305eb0b"},
|
||||
{file = "propcache-0.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf72af5e0fb40e9babf594308911436c8efde3cb5e75b6f206c34ad18be5c052"},
|
||||
{file = "propcache-0.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b2d0a12018b04f4cb820781ec0dffb5f7c7c1d2a5cd22bff7fb055a2cb19ebce"},
|
||||
{file = "propcache-0.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e800776a79a5aabdb17dcc2346a7d66d0777e942e4cd251defeb084762ecd17d"},
|
||||
{file = "propcache-0.2.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:4160d9283bd382fa6c0c2b5e017acc95bc183570cd70968b9202ad6d8fc48dce"},
|
||||
{file = "propcache-0.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:30b43e74f1359353341a7adb783c8f1b1c676367b011709f466f42fda2045e95"},
|
||||
{file = "propcache-0.2.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:58791550b27d5488b1bb52bc96328456095d96206a250d28d874fafe11b3dfaf"},
|
||||
{file = "propcache-0.2.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:0f022d381747f0dfe27e99d928e31bc51a18b65bb9e481ae0af1380a6725dd1f"},
|
||||
{file = "propcache-0.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:297878dc9d0a334358f9b608b56d02e72899f3b8499fc6044133f0d319e2ec30"},
|
||||
{file = "propcache-0.2.1-cp313-cp313-win32.whl", hash = "sha256:ddfab44e4489bd79bda09d84c430677fc7f0a4939a73d2bba3073036f487a0a6"},
|
||||
{file = "propcache-0.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:556fc6c10989f19a179e4321e5d678db8eb2924131e64652a51fe83e4c3db0e1"},
|
||||
{file = "propcache-0.2.1-py3-none-any.whl", hash = "sha256:52277518d6aae65536e9cea52d4e7fd2f7a66f4aa2d30ed3f2fcea620ace3c54"},
|
||||
{file = "propcache-0.2.1.tar.gz", hash = "sha256:3f77ce728b19cb537714499928fe800c3dda29e8d9428778fc7c186da4c09a64"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pydantic"
|
||||
version = "2.8.2"
|
||||
version = "2.10.5"
|
||||
requires_python = ">=3.8"
|
||||
summary = "Data validation using Python type hints"
|
||||
dependencies = [
|
||||
"annotated-types>=0.4.0",
|
||||
"pydantic-core==2.20.1",
|
||||
"typing-extensions>=4.12.2; python_version >= \"3.13\"",
|
||||
"typing-extensions>=4.6.1; python_version < \"3.13\"",
|
||||
"annotated-types>=0.6.0",
|
||||
"pydantic-core==2.27.2",
|
||||
"typing-extensions>=4.12.2",
|
||||
]
|
||||
files = [
|
||||
{file = "pydantic-2.8.2-py3-none-any.whl", hash = "sha256:73ee9fddd406dc318b885c7a2eab8a6472b68b8fb5ba8150949fc3db939f23c8"},
|
||||
{file = "pydantic-2.8.2.tar.gz", hash = "sha256:6f62c13d067b0755ad1c21a34bdd06c0c12625a22b0fc09c6b149816604f7c2a"},
|
||||
{file = "pydantic-2.10.5-py3-none-any.whl", hash = "sha256:4dd4e322dbe55472cb7ca7e73f4b63574eecccf2835ffa2af9021ce113c83c53"},
|
||||
{file = "pydantic-2.10.5.tar.gz", hash = "sha256:278b38dbbaec562011d659ee05f63346951b3a248a6f3642e1bc68894ea2b4ff"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pydantic-core"
|
||||
version = "2.20.1"
|
||||
version = "2.27.2"
|
||||
requires_python = ">=3.8"
|
||||
summary = "Core functionality for Pydantic validation and serialization"
|
||||
dependencies = [
|
||||
"typing-extensions!=4.7.0,>=4.6.0",
|
||||
]
|
||||
files = [
|
||||
{file = "pydantic_core-2.20.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:595ba5be69b35777474fa07f80fc260ea71255656191adb22a8c53aba4479231"},
|
||||
{file = "pydantic_core-2.20.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a4f55095ad087474999ee28d3398bae183a66be4823f753cd7d67dd0153427c9"},
|
||||
{file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f9aa05d09ecf4c75157197f27cdc9cfaeb7c5f15021c6373932bf3e124af029f"},
|
||||
{file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e97fdf088d4b31ff4ba35db26d9cc472ac7ef4a2ff2badeabf8d727b3377fc52"},
|
||||
{file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bc633a9fe1eb87e250b5c57d389cf28998e4292336926b0b6cdaee353f89a237"},
|
||||
{file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d573faf8eb7e6b1cbbcb4f5b247c60ca8be39fe2c674495df0eb4318303137fe"},
|
||||
{file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26dc97754b57d2fd00ac2b24dfa341abffc380b823211994c4efac7f13b9e90e"},
|
||||
{file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:33499e85e739a4b60c9dac710c20a08dc73cb3240c9a0e22325e671b27b70d24"},
|
||||
{file = "pydantic_core-2.20.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:bebb4d6715c814597f85297c332297c6ce81e29436125ca59d1159b07f423eb1"},
|
||||
{file = "pydantic_core-2.20.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:516d9227919612425c8ef1c9b869bbbee249bc91912c8aaffb66116c0b447ebd"},
|
||||
{file = "pydantic_core-2.20.1-cp312-none-win32.whl", hash = "sha256:469f29f9093c9d834432034d33f5fe45699e664f12a13bf38c04967ce233d688"},
|
||||
{file = "pydantic_core-2.20.1-cp312-none-win_amd64.whl", hash = "sha256:035ede2e16da7281041f0e626459bcae33ed998cca6a0a007a5ebb73414ac72d"},
|
||||
{file = "pydantic_core-2.20.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:0827505a5c87e8aa285dc31e9ec7f4a17c81a813d45f70b1d9164e03a813a686"},
|
||||
{file = "pydantic_core-2.20.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:19c0fa39fa154e7e0b7f82f88ef85faa2a4c23cc65aae2f5aea625e3c13c735a"},
|
||||
{file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa223cd1e36b642092c326d694d8bf59b71ddddc94cdb752bbbb1c5c91d833b"},
|
||||
{file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c336a6d235522a62fef872c6295a42ecb0c4e1d0f1a3e500fe949415761b8a19"},
|
||||
{file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7eb6a0587eded33aeefea9f916899d42b1799b7b14b8f8ff2753c0ac1741edac"},
|
||||
{file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:70c8daf4faca8da5a6d655f9af86faf6ec2e1768f4b8b9d0226c02f3d6209703"},
|
||||
{file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e9fa4c9bf273ca41f940bceb86922a7667cd5bf90e95dbb157cbb8441008482c"},
|
||||
{file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:11b71d67b4725e7e2a9f6e9c0ac1239bbc0c48cce3dc59f98635efc57d6dac83"},
|
||||
{file = "pydantic_core-2.20.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:270755f15174fb983890c49881e93f8f1b80f0b5e3a3cc1394a255706cabd203"},
|
||||
{file = "pydantic_core-2.20.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:c81131869240e3e568916ef4c307f8b99583efaa60a8112ef27a366eefba8ef0"},
|
||||
{file = "pydantic_core-2.20.1-cp313-none-win32.whl", hash = "sha256:b91ced227c41aa29c672814f50dbb05ec93536abf8f43cd14ec9521ea09afe4e"},
|
||||
{file = "pydantic_core-2.20.1-cp313-none-win_amd64.whl", hash = "sha256:65db0f2eefcaad1a3950f498aabb4875c8890438bc80b19362cf633b87a8ab20"},
|
||||
{file = "pydantic_core-2.20.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:a45f84b09ac9c3d35dfcf6a27fd0634d30d183205230a0ebe8373a0e8cfa0906"},
|
||||
{file = "pydantic_core-2.20.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d02a72df14dfdbaf228424573a07af10637bd490f0901cee872c4f434a735b94"},
|
||||
{file = "pydantic_core-2.20.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d2b27e6af28f07e2f195552b37d7d66b150adbaa39a6d327766ffd695799780f"},
|
||||
{file = "pydantic_core-2.20.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:084659fac3c83fd674596612aeff6041a18402f1e1bc19ca39e417d554468482"},
|
||||
{file = "pydantic_core-2.20.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:242b8feb3c493ab78be289c034a1f659e8826e2233786e36f2893a950a719bb6"},
|
||||
{file = "pydantic_core-2.20.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:38cf1c40a921d05c5edc61a785c0ddb4bed67827069f535d794ce6bcded919fc"},
|
||||
{file = "pydantic_core-2.20.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:e0bbdd76ce9aa5d4209d65f2b27fc6e5ef1312ae6c5333c26db3f5ade53a1e99"},
|
||||
{file = "pydantic_core-2.20.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:254ec27fdb5b1ee60684f91683be95e5133c994cc54e86a0b0963afa25c8f8a6"},
|
||||
{file = "pydantic_core-2.20.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:407653af5617f0757261ae249d3fba09504d7a71ab36ac057c938572d1bc9331"},
|
||||
{file = "pydantic_core-2.20.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:c693e916709c2465b02ca0ad7b387c4f8423d1db7b4649c551f27a529181c5ad"},
|
||||
{file = "pydantic_core-2.20.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b5ff4911aea936a47d9376fd3ab17e970cc543d1b68921886e7f64bd28308d1"},
|
||||
{file = "pydantic_core-2.20.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:177f55a886d74f1808763976ac4efd29b7ed15c69f4d838bbd74d9d09cf6fa86"},
|
||||
{file = "pydantic_core-2.20.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:964faa8a861d2664f0c7ab0c181af0bea66098b1919439815ca8803ef136fc4e"},
|
||||
{file = "pydantic_core-2.20.1-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:4dd484681c15e6b9a977c785a345d3e378d72678fd5f1f3c0509608da24f2ac0"},
|
||||
{file = "pydantic_core-2.20.1-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f6d6cff3538391e8486a431569b77921adfcdef14eb18fbf19b7c0a5294d4e6a"},
|
||||
{file = "pydantic_core-2.20.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a6d511cc297ff0883bc3708b465ff82d7560193169a8b93260f74ecb0a5e08a7"},
|
||||
{file = "pydantic_core-2.20.1.tar.gz", hash = "sha256:26ca695eeee5f9f1aeeb211ffc12f10bcb6f71e2989988fda61dabd65db878d4"},
|
||||
{file = "pydantic_core-2.27.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9e0c8cfefa0ef83b4da9588448b6d8d2a2bf1a53c3f1ae5fca39eb3061e2f0b0"},
|
||||
{file = "pydantic_core-2.27.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:83097677b8e3bd7eaa6775720ec8e0405f1575015a463285a92bfdfe254529ef"},
|
||||
{file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:172fce187655fece0c90d90a678424b013f8fbb0ca8b036ac266749c09438cb7"},
|
||||
{file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:519f29f5213271eeeeb3093f662ba2fd512b91c5f188f3bb7b27bc5973816934"},
|
||||
{file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05e3a55d124407fffba0dd6b0c0cd056d10e983ceb4e5dbd10dda135c31071d6"},
|
||||
{file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c3ed807c7b91de05e63930188f19e921d1fe90de6b4f5cd43ee7fcc3525cb8c"},
|
||||
{file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fb4aadc0b9a0c063206846d603b92030eb6f03069151a625667f982887153e2"},
|
||||
{file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28ccb213807e037460326424ceb8b5245acb88f32f3d2777427476e1b32c48c4"},
|
||||
{file = "pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:de3cd1899e2c279b140adde9357c4495ed9d47131b4a4eaff9052f23398076b3"},
|
||||
{file = "pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:220f892729375e2d736b97d0e51466252ad84c51857d4d15f5e9692f9ef12be4"},
|
||||
{file = "pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a0fcd29cd6b4e74fe8ddd2c90330fd8edf2e30cb52acda47f06dd615ae72da57"},
|
||||
{file = "pydantic_core-2.27.2-cp312-cp312-win32.whl", hash = "sha256:1e2cb691ed9834cd6a8be61228471d0a503731abfb42f82458ff27be7b2186fc"},
|
||||
{file = "pydantic_core-2.27.2-cp312-cp312-win_amd64.whl", hash = "sha256:cc3f1a99a4f4f9dd1de4fe0312c114e740b5ddead65bb4102884b384c15d8bc9"},
|
||||
{file = "pydantic_core-2.27.2-cp312-cp312-win_arm64.whl", hash = "sha256:3911ac9284cd8a1792d3cb26a2da18f3ca26c6908cc434a18f730dc0db7bfa3b"},
|
||||
{file = "pydantic_core-2.27.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7d14bd329640e63852364c306f4d23eb744e0f8193148d4044dd3dacdaacbd8b"},
|
||||
{file = "pydantic_core-2.27.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82f91663004eb8ed30ff478d77c4d1179b3563df6cdb15c0817cd1cdaf34d154"},
|
||||
{file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71b24c7d61131bb83df10cc7e687433609963a944ccf45190cfc21e0887b08c9"},
|
||||
{file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fa8e459d4954f608fa26116118bb67f56b93b209c39b008277ace29937453dc9"},
|
||||
{file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce8918cbebc8da707ba805b7fd0b382816858728ae7fe19a942080c24e5b7cd1"},
|
||||
{file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eda3f5c2a021bbc5d976107bb302e0131351c2ba54343f8a496dc8783d3d3a6a"},
|
||||
{file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd8086fa684c4775c27f03f062cbb9eaa6e17f064307e86b21b9e0abc9c0f02e"},
|
||||
{file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8d9b3388db186ba0c099a6d20f0604a44eabdeef1777ddd94786cdae158729e4"},
|
||||
{file = "pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7a66efda2387de898c8f38c0cf7f14fca0b51a8ef0b24bfea5849f1b3c95af27"},
|
||||
{file = "pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:18a101c168e4e092ab40dbc2503bdc0f62010e95d292b27827871dc85450d7ee"},
|
||||
{file = "pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ba5dd002f88b78a4215ed2f8ddbdf85e8513382820ba15ad5ad8955ce0ca19a1"},
|
||||
{file = "pydantic_core-2.27.2-cp313-cp313-win32.whl", hash = "sha256:1ebaf1d0481914d004a573394f4be3a7616334be70261007e47c2a6fe7e50130"},
|
||||
{file = "pydantic_core-2.27.2-cp313-cp313-win_amd64.whl", hash = "sha256:953101387ecf2f5652883208769a79e48db18c6df442568a0b5ccd8c2723abee"},
|
||||
{file = "pydantic_core-2.27.2-cp313-cp313-win_arm64.whl", hash = "sha256:ac4dbfd1691affb8f48c2c13241a2e3b60ff23247cbcf981759c768b6633cf8b"},
|
||||
{file = "pydantic_core-2.27.2.tar.gz", hash = "sha256:eb026e5a4c1fee05726072337ff51d1efb6f59090b7da90d30ea58625b1ffb39"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -288,106 +329,111 @@ files = [
|
|||
|
||||
[[package]]
|
||||
name = "pyobjc-core"
|
||||
version = "10.3.1"
|
||||
version = "11.0"
|
||||
requires_python = ">=3.8"
|
||||
summary = "Python<->ObjC Interoperability Module"
|
||||
files = [
|
||||
{file = "pyobjc_core-10.3.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:6ff5823d13d0a534cdc17fa4ad47cf5bee4846ce0fd27fc40012e12b46db571b"},
|
||||
{file = "pyobjc_core-10.3.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2581e8e68885bcb0e11ec619e81ef28e08ee3fac4de20d8cc83bc5af5bcf4a90"},
|
||||
{file = "pyobjc_core-10.3.1.tar.gz", hash = "sha256:b204a80ccc070f9ab3f8af423a3a25a6fd787e228508d00c4c30f8ac538ba720"},
|
||||
{file = "pyobjc_core-11.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a03061d4955c62ddd7754224a80cdadfdf17b6b5f60df1d9169a3b1b02923f0b"},
|
||||
{file = "pyobjc_core-11.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:c338c1deb7ab2e9436d4175d1127da2eeed4a1b564b3d83b9f3ae4844ba97e86"},
|
||||
{file = "pyobjc_core-11.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b4e9dc4296110f251a4033ff3f40320b35873ea7f876bd29a1c9705bb5e08c59"},
|
||||
{file = "pyobjc_core-11.0.tar.gz", hash = "sha256:63bced211cb8a8fb5c8ff46473603da30e51112861bd02c438fbbbc8578d9a70"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyobjc-framework-avfoundation"
|
||||
version = "10.3.1"
|
||||
requires_python = ">=3.8"
|
||||
version = "11.0"
|
||||
requires_python = ">=3.9"
|
||||
summary = "Wrappers for the framework AVFoundation on macOS"
|
||||
dependencies = [
|
||||
"pyobjc-core>=10.3.1",
|
||||
"pyobjc-framework-Cocoa>=10.3.1",
|
||||
"pyobjc-framework-CoreAudio>=10.3.1",
|
||||
"pyobjc-framework-CoreMedia>=10.3.1",
|
||||
"pyobjc-framework-Quartz>=10.3.1",
|
||||
"pyobjc-core>=11.0",
|
||||
"pyobjc-framework-Cocoa>=11.0",
|
||||
"pyobjc-framework-CoreAudio>=11.0",
|
||||
"pyobjc-framework-CoreMedia>=11.0",
|
||||
"pyobjc-framework-Quartz>=11.0",
|
||||
]
|
||||
files = [
|
||||
{file = "pyobjc_framework_AVFoundation-10.3.1-cp36-abi3-macosx_10_13_universal2.whl", hash = "sha256:0896f6650df35f0229d1fb3aa3fbf632647dd815d4921cb61d9eb7fa26be6237"},
|
||||
{file = "pyobjc_framework_AVFoundation-10.3.1-cp36-abi3-macosx_10_9_universal2.whl", hash = "sha256:0cb27cc95288d95df7504adf474596f8855de7fa7798bbc1bbfbdfbbcb940952"},
|
||||
{file = "pyobjc_framework_AVFoundation-10.3.1-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ccb606ef0806d952a04db45ae691167678121df1d8d7c2f8cc73745695097033"},
|
||||
{file = "pyobjc_framework_AVFoundation-10.3.1-cp36-abi3-macosx_11_0_universal2.whl", hash = "sha256:00889eb915479aa9ea392cdd241e4b635ae0fa3114f043d08cf3e1d1b5a23bd4"},
|
||||
{file = "pyobjc_framework_avfoundation-10.3.1.tar.gz", hash = "sha256:2f94bee3a4217b46d9416cad066e4f357bf0f344079c328736114451ae19ae94"},
|
||||
{file = "pyobjc_framework_AVFoundation-11.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:6bb6f4be53c0fb42bee3f46cf0bb5396a8fd13f92d47a01f6b77037a1134f26b"},
|
||||
{file = "pyobjc_framework_AVFoundation-11.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d9d2497acf3e7c5ae4a8175832af249754847b415494422727ac43efe14cc776"},
|
||||
{file = "pyobjc_framework_AVFoundation-11.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:da932d77e29e3f4112d0526918a47c978381d00af23133cb06e0a5f76e92a9b6"},
|
||||
{file = "pyobjc_framework_avfoundation-11.0.tar.gz", hash = "sha256:269a592bdaf8a16948d8935f0cf7c8cb9a53e7ea609a963ada0e55f749ddb530"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyobjc-framework-cocoa"
|
||||
version = "10.3.1"
|
||||
requires_python = ">=3.8"
|
||||
version = "11.0"
|
||||
requires_python = ">=3.9"
|
||||
summary = "Wrappers for the Cocoa frameworks on macOS"
|
||||
dependencies = [
|
||||
"pyobjc-core>=10.3.1",
|
||||
"pyobjc-core>=11.0",
|
||||
]
|
||||
files = [
|
||||
{file = "pyobjc_framework_Cocoa-10.3.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:11b4e0bad4bbb44a4edda128612f03cdeab38644bbf174de0c13129715497296"},
|
||||
{file = "pyobjc_framework_Cocoa-10.3.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:de5e62e5ccf2871a94acf3bf79646b20ea893cc9db78afa8d1fe1b0d0f7cbdb0"},
|
||||
{file = "pyobjc_framework_cocoa-10.3.1.tar.gz", hash = "sha256:1cf20714daaa986b488fb62d69713049f635c9d41a60c8da97d835710445281a"},
|
||||
{file = "pyobjc_framework_Cocoa-11.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:280a577b83c68175a28b2b7138d1d2d3111f2b2b66c30e86f81a19c2b02eae71"},
|
||||
{file = "pyobjc_framework_Cocoa-11.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:15b2bd977ed340074f930f1330f03d42912d5882b697d78bd06f8ebe263ef92e"},
|
||||
{file = "pyobjc_framework_Cocoa-11.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:5750001db544e67f2b66f02067d8f0da96bb2ef71732bde104f01b8628f9d7ea"},
|
||||
{file = "pyobjc_framework_cocoa-11.0.tar.gz", hash = "sha256:00346a8cb81ad7b017b32ff7bf596000f9faa905807b1bd234644ebd47f692c5"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyobjc-framework-coreaudio"
|
||||
version = "10.3.1"
|
||||
requires_python = ">=3.8"
|
||||
version = "11.0"
|
||||
requires_python = ">=3.9"
|
||||
summary = "Wrappers for the framework CoreAudio on macOS"
|
||||
dependencies = [
|
||||
"pyobjc-core>=10.3.1",
|
||||
"pyobjc-framework-Cocoa>=10.3.1",
|
||||
"pyobjc-core>=11.0",
|
||||
"pyobjc-framework-Cocoa>=11.0",
|
||||
]
|
||||
files = [
|
||||
{file = "pyobjc_framework_CoreAudio-10.3.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:e0aeca61a425d846afc92350ffba970e1e503469182f5f0ea436de98cfd00d96"},
|
||||
{file = "pyobjc_framework_CoreAudio-10.3.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:21cecd1b023b6960d1071c106345656de45a399196701b07c7e5c076321f25ad"},
|
||||
{file = "pyobjc_framework_coreaudio-10.3.1.tar.gz", hash = "sha256:c81c709bf955aea474a4de380b187f3c2e56c864ca7de520b08362b73070c795"},
|
||||
{file = "pyobjc_framework_CoreAudio-11.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d26eac5bc325bf046fc0bfdaa3322ddc828690dab726275f1c4c118bb888cc00"},
|
||||
{file = "pyobjc_framework_CoreAudio-11.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:272388af86809f7a81250d931e99f650f62878410d4e1cfcd8adf0bbfb0d4581"},
|
||||
{file = "pyobjc_framework_CoreAudio-11.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:764873ec0724e42844ed2f0ca95ab4654c5ba59f883799207a3eecd4f5b444df"},
|
||||
{file = "pyobjc_framework_coreaudio-11.0.tar.gz", hash = "sha256:38b6b531381119be6998cf704d04c9ea475aaa33f6dd460e0584351475acd0ae"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyobjc-framework-coremedia"
|
||||
version = "10.3.1"
|
||||
requires_python = ">=3.8"
|
||||
version = "11.0"
|
||||
requires_python = ">=3.9"
|
||||
summary = "Wrappers for the framework CoreMedia on macOS"
|
||||
dependencies = [
|
||||
"pyobjc-core>=10.3.1",
|
||||
"pyobjc-framework-Cocoa>=10.3.1",
|
||||
"pyobjc-core>=11.0",
|
||||
"pyobjc-framework-Cocoa>=11.0",
|
||||
]
|
||||
files = [
|
||||
{file = "pyobjc_framework_CoreMedia-10.3.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:c6eaf48f202becab10679e3b5dd62607ddec2739495db45882524592cabf3997"},
|
||||
{file = "pyobjc_framework_CoreMedia-10.3.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:98b8ab02e6ec979007b706e05166e16bd61121e47fbc6e449f4b2de2c58f3cb6"},
|
||||
{file = "pyobjc_framework_coremedia-10.3.1.tar.gz", hash = "sha256:bc3e0cddf5f546b5d8407d8f46b203f1bd4396ad5dbfdc0d064a560b3fe31221"},
|
||||
{file = "pyobjc_framework_CoreMedia-11.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:afd8eb59f5ce0730ff15476ad3989aa84ffb8d8d02c9b8b2c9c1248b0541dbff"},
|
||||
{file = "pyobjc_framework_CoreMedia-11.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:88b26ca9a1333ddbe2a6dfa9a8c2d2be712cb717c3e9e1174fed66bf8d7af067"},
|
||||
{file = "pyobjc_framework_CoreMedia-11.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:ab18a7fbc5003e0929fc8380f371bb580e6ecd6be26333bf88b4a7f51a9c0789"},
|
||||
{file = "pyobjc_framework_coremedia-11.0.tar.gz", hash = "sha256:a414db97ba30b43c9dd96213459d6efb169f9e92ce1ad7a75516a679b181ddfb"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyobjc-framework-mediaplayer"
|
||||
version = "10.3.1"
|
||||
requires_python = ">=3.8"
|
||||
version = "11.0"
|
||||
requires_python = ">=3.9"
|
||||
summary = "Wrappers for the framework MediaPlayer on macOS"
|
||||
dependencies = [
|
||||
"pyobjc-core>=10.3.1",
|
||||
"pyobjc-framework-AVFoundation>=10.3.1",
|
||||
"pyobjc-core>=11.0",
|
||||
"pyobjc-framework-AVFoundation>=11.0",
|
||||
]
|
||||
files = [
|
||||
{file = "pyobjc_framework_MediaPlayer-10.3.1-py2.py3-none-any.whl", hash = "sha256:5b428cc28e57c1778bd431156c3adb948650f7503f266689559d0ece94b34e8a"},
|
||||
{file = "pyobjc_framework_mediaplayer-10.3.1.tar.gz", hash = "sha256:97043df5ef89d4fbe217813e8f4ee1e226d8a43dee4eac00fff95e6b8a7772be"},
|
||||
{file = "pyobjc_framework_MediaPlayer-11.0-py2.py3-none-any.whl", hash = "sha256:b124b0f18444b69b64142bad2579287d0b1a4a35cb6b14526523a822066d527d"},
|
||||
{file = "pyobjc_framework_MediaPlayer-11.0-py3-none-any.whl", hash = "sha256:1a051624b536666feb5fd1a4bb54000ab45dac0c8aea4cd4707cbde1773acf57"},
|
||||
{file = "pyobjc_framework_mediaplayer-11.0.tar.gz", hash = "sha256:c61be0ba6c648db6b1d013a52f9afb8901a8d7fbabd983df2175c1b1fbff81e5"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyobjc-framework-quartz"
|
||||
version = "10.3.1"
|
||||
requires_python = ">=3.8"
|
||||
version = "11.0"
|
||||
requires_python = ">=3.9"
|
||||
summary = "Wrappers for the Quartz frameworks on macOS"
|
||||
dependencies = [
|
||||
"pyobjc-core>=10.3.1",
|
||||
"pyobjc-framework-Cocoa>=10.3.1",
|
||||
"pyobjc-core>=11.0",
|
||||
"pyobjc-framework-Cocoa>=11.0",
|
||||
]
|
||||
files = [
|
||||
{file = "pyobjc_framework_Quartz-10.3.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:ca35f92486869a41847a1703bb176aab8a53dbfd8e678d1f4d68d8e6e1581c71"},
|
||||
{file = "pyobjc_framework_Quartz-10.3.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:00a0933267e3a46ea4afcc35d117b2efb920f06de797fa66279c52e7057e3590"},
|
||||
{file = "pyobjc_framework_quartz-10.3.1.tar.gz", hash = "sha256:b6d7e346d735c9a7f147cd78e6da79eeae416a0b7d3874644c83a23786c6f886"},
|
||||
{file = "pyobjc_framework_Quartz-11.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:cb4a9f2d9d580ea15e25e6b270f47681afb5689cafc9e25712445ce715bcd18e"},
|
||||
{file = "pyobjc_framework_Quartz-11.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:973b4f9b8ab844574461a038bd5269f425a7368d6e677e3cc81fcc9b27b65498"},
|
||||
{file = "pyobjc_framework_Quartz-11.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:66ab58d65348863b8707e63b2ec5cdc54569ee8189d1af90d52f29f5fdf6272c"},
|
||||
{file = "pyobjc_framework_quartz-11.0.tar.gz", hash = "sha256:3205bf7795fb9ae34747f701486b3db6dfac71924894d1f372977c4d70c3c619"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -405,18 +451,6 @@ name = "pytomlpp"
|
|||
version = "1.0.13"
|
||||
summary = "A python wrapper for toml++"
|
||||
files = [
|
||||
{file = "pytomlpp-1.0.13-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:4710c72456c10a90e58084174312abef8f9652b0f91c240c008903c1bd99814d"},
|
||||
{file = "pytomlpp-1.0.13-pp37-pypy37_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b59acc12339a992404289ab7294f28ba06c7df3c2562e81d316a0e744ab4103b"},
|
||||
{file = "pytomlpp-1.0.13-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:252e31a5e013a74b898784f4ffb8aa8068e136b910ad11f2af1ee8a5700e6e1e"},
|
||||
{file = "pytomlpp-1.0.13-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:09e716c0f462d15f2334cecc736957777dd30f8a5bfa5cf8150679da7577d2fd"},
|
||||
{file = "pytomlpp-1.0.13-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:19dbded2995370e802105fa6dce54ed60f79e58b4eb35fee7ef33f1fb5958f6c"},
|
||||
{file = "pytomlpp-1.0.13-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9f87f6c958309e4c2358b778902c80bd33611d1c392f1abe2c226e3a62909ca4"},
|
||||
{file = "pytomlpp-1.0.13-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e285aca948b419301fdda1927723287ef28482752782c44c9ee8c57eae7a1dc8"},
|
||||
{file = "pytomlpp-1.0.13-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:aad6ae19c056ea62a43fec82427ad4675b5c773dc255c4bdcf6da659cd7edff6"},
|
||||
{file = "pytomlpp-1.0.13-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:0e0b34b7a132856567714342e9a622f7be0b4c9bac561a6252f0f85626c1aa4b"},
|
||||
{file = "pytomlpp-1.0.13-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ac06ca7683f5a2737b3888ea1e38d6968abb24fab703bc7ceccbe589d5420e0c"},
|
||||
{file = "pytomlpp-1.0.13-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:35225c1d9d674df87b4682f04af97856049351c38822455b78258248d9309363"},
|
||||
{file = "pytomlpp-1.0.13-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:dbc9208ac58ea2a9d5ebb77e69d54d146744007f4a704a3f4e56d9881d41ee1c"},
|
||||
{file = "pytomlpp-1.0.13.tar.gz", hash = "sha256:a0bd639a8f624d1bdf5b3ea94363ca23dbfef38ab7b5b9348881a84afab434ad"},
|
||||
]
|
||||
|
||||
|
@ -425,6 +459,11 @@ name = "redis"
|
|||
version = "5.0.7"
|
||||
requires_python = ">=3.7"
|
||||
summary = "Python client for Redis database and key-value store"
|
||||
dependencies = [
|
||||
"async-timeout>=4.0.3; python_full_version < \"3.11.3\"",
|
||||
"importlib-metadata>=1.0; python_version < \"3.8\"",
|
||||
"typing-extensions; python_version < \"3.8\"",
|
||||
]
|
||||
files = [
|
||||
{file = "redis-5.0.7-py3-none-any.whl", hash = "sha256:0e479e24da960c690be5d9b96d21f7b918a98c0cf49af3b6fafaa0753f93a0db"},
|
||||
{file = "redis-5.0.7.tar.gz", hash = "sha256:8f611490b93c8109b50adc317b31bfd84fff31def3475b92e7e80bf39f48175b"},
|
||||
|
@ -432,42 +471,43 @@ files = [
|
|||
|
||||
[[package]]
|
||||
name = "rich"
|
||||
version = "13.7.1"
|
||||
requires_python = ">=3.7.0"
|
||||
version = "13.9.4"
|
||||
requires_python = ">=3.8.0"
|
||||
summary = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal"
|
||||
dependencies = [
|
||||
"markdown-it-py>=2.2.0",
|
||||
"pygments<3.0.0,>=2.13.0",
|
||||
"typing-extensions<5.0,>=4.0.0; python_version < \"3.11\"",
|
||||
]
|
||||
files = [
|
||||
{file = "rich-13.7.1-py3-none-any.whl", hash = "sha256:4edbae314f59eb482f54e9e30bf00d33350aaa94f4bfcd4e9e3110e64d0d7222"},
|
||||
{file = "rich-13.7.1.tar.gz", hash = "sha256:9be308cb1fe2f1f57d67ce99e95af38a1e2bc71ad9813b0e247cf7ffbcc3a432"},
|
||||
{file = "rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90"},
|
||||
{file = "rich-13.9.4.tar.gz", hash = "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.5.1"
|
||||
version = "0.9.2"
|
||||
requires_python = ">=3.7"
|
||||
summary = "An extremely fast Python linter and code formatter, written in Rust."
|
||||
files = [
|
||||
{file = "ruff-0.5.1-py3-none-linux_armv6l.whl", hash = "sha256:6ecf968fcf94d942d42b700af18ede94b07521bd188aaf2cd7bc898dd8cb63b6"},
|
||||
{file = "ruff-0.5.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:204fb0a472f00f2e6280a7c8c7c066e11e20e23a37557d63045bf27a616ba61c"},
|
||||
{file = "ruff-0.5.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d235968460e8758d1e1297e1de59a38d94102f60cafb4d5382033c324404ee9d"},
|
||||
{file = "ruff-0.5.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38beace10b8d5f9b6bdc91619310af6d63dd2019f3fb2d17a2da26360d7962fa"},
|
||||
{file = "ruff-0.5.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5e478d2f09cf06add143cf8c4540ef77b6599191e0c50ed976582f06e588c994"},
|
||||
{file = "ruff-0.5.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f0368d765eec8247b8550251c49ebb20554cc4e812f383ff9f5bf0d5d94190b0"},
|
||||
{file = "ruff-0.5.1-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:3a9a9a1b582e37669b0138b7c1d9d60b9edac880b80eb2baba6d0e566bdeca4d"},
|
||||
{file = "ruff-0.5.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bdd9f723e16003623423affabcc0a807a66552ee6a29f90eddad87a40c750b78"},
|
||||
{file = "ruff-0.5.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:be9fd62c1e99539da05fcdc1e90d20f74aec1b7a1613463ed77870057cd6bd96"},
|
||||
{file = "ruff-0.5.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e216fc75a80ea1fbd96af94a6233d90190d5b65cc3d5dfacf2bd48c3e067d3e1"},
|
||||
{file = "ruff-0.5.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:c4c2112e9883a40967827d5c24803525145e7dab315497fae149764979ac7929"},
|
||||
{file = "ruff-0.5.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:dfaf11c8a116394da3b65cd4b36de30d8552fa45b8119b9ef5ca6638ab964fa3"},
|
||||
{file = "ruff-0.5.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:d7ceb9b2fe700ee09a0c6b192c5ef03c56eb82a0514218d8ff700f6ade004108"},
|
||||
{file = "ruff-0.5.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:bac6288e82f6296f82ed5285f597713acb2a6ae26618ffc6b429c597b392535c"},
|
||||
{file = "ruff-0.5.1-py3-none-win32.whl", hash = "sha256:5c441d9c24ec09e1cb190a04535c5379b36b73c4bc20aa180c54812c27d1cca4"},
|
||||
{file = "ruff-0.5.1-py3-none-win_amd64.whl", hash = "sha256:b1789bf2cd3d1b5a7d38397cac1398ddf3ad7f73f4de01b1e913e2abc7dfc51d"},
|
||||
{file = "ruff-0.5.1-py3-none-win_arm64.whl", hash = "sha256:2875b7596a740cbbd492f32d24be73e545a4ce0a3daf51e4f4e609962bfd3cd2"},
|
||||
{file = "ruff-0.5.1.tar.gz", hash = "sha256:3164488aebd89b1745b47fd00604fb4358d774465f20d1fcd907f9c0fc1b0655"},
|
||||
{file = "ruff-0.9.2-py3-none-linux_armv6l.whl", hash = "sha256:80605a039ba1454d002b32139e4970becf84b5fee3a3c3bf1c2af6f61a784347"},
|
||||
{file = "ruff-0.9.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b9aab82bb20afd5f596527045c01e6ae25a718ff1784cb92947bff1f83068b00"},
|
||||
{file = "ruff-0.9.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:fbd337bac1cfa96be615f6efcd4bc4d077edbc127ef30e2b8ba2a27e18c054d4"},
|
||||
{file = "ruff-0.9.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82b35259b0cbf8daa22a498018e300b9bb0174c2bbb7bcba593935158a78054d"},
|
||||
{file = "ruff-0.9.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8b6a9701d1e371bf41dca22015c3f89769da7576884d2add7317ec1ec8cb9c3c"},
|
||||
{file = "ruff-0.9.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9cc53e68b3c5ae41e8faf83a3b89f4a5d7b2cb666dff4b366bb86ed2a85b481f"},
|
||||
{file = "ruff-0.9.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:8efd9da7a1ee314b910da155ca7e8953094a7c10d0c0a39bfde3fcfd2a015684"},
|
||||
{file = "ruff-0.9.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3292c5a22ea9a5f9a185e2d131dc7f98f8534a32fb6d2ee7b9944569239c648d"},
|
||||
{file = "ruff-0.9.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1a605fdcf6e8b2d39f9436d343d1f0ff70c365a1e681546de0104bef81ce88df"},
|
||||
{file = "ruff-0.9.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c547f7f256aa366834829a08375c297fa63386cbe5f1459efaf174086b564247"},
|
||||
{file = "ruff-0.9.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:d18bba3d3353ed916e882521bc3e0af403949dbada344c20c16ea78f47af965e"},
|
||||
{file = "ruff-0.9.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:b338edc4610142355ccf6b87bd356729b62bf1bc152a2fad5b0c7dc04af77bfe"},
|
||||
{file = "ruff-0.9.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:492a5e44ad9b22a0ea98cf72e40305cbdaf27fac0d927f8bc9e1df316dcc96eb"},
|
||||
{file = "ruff-0.9.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:af1e9e9fe7b1f767264d26b1075ac4ad831c7db976911fa362d09b2d0356426a"},
|
||||
{file = "ruff-0.9.2-py3-none-win32.whl", hash = "sha256:71cbe22e178c5da20e1514e1e01029c73dc09288a8028a5d3446e6bba87a5145"},
|
||||
{file = "ruff-0.9.2-py3-none-win_amd64.whl", hash = "sha256:c5e1d6abc798419cf46eed03f54f2e0c3adb1ad4b801119dedf23fcaf69b55b5"},
|
||||
{file = "ruff-0.9.2-py3-none-win_arm64.whl", hash = "sha256:a1b63fa24149918f8b37cef2ee6fff81f24f0d74b6f0bdc37bc3e1f2143e41c6"},
|
||||
{file = "ruff-0.9.2.tar.gz", hash = "sha256:b5eceb334d55fae5f316f783437392642ae18e16dcf4f1858d55d3c2a0f8f5d0"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -482,75 +522,89 @@ files = [
|
|||
|
||||
[[package]]
|
||||
name = "websockets"
|
||||
version = "12.0"
|
||||
requires_python = ">=3.8"
|
||||
version = "14.2"
|
||||
requires_python = ">=3.9"
|
||||
summary = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)"
|
||||
files = [
|
||||
{file = "websockets-12.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0e6e2711d5a8e6e482cacb927a49a3d432345dfe7dea8ace7b5790df5932e4df"},
|
||||
{file = "websockets-12.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:dbcf72a37f0b3316e993e13ecf32f10c0e1259c28ffd0a85cee26e8549595fbc"},
|
||||
{file = "websockets-12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:12743ab88ab2af1d17dd4acb4645677cb7063ef4db93abffbf164218a5d54c6b"},
|
||||
{file = "websockets-12.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b645f491f3c48d3f8a00d1fce07445fab7347fec54a3e65f0725d730d5b99cb"},
|
||||
{file = "websockets-12.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9893d1aa45a7f8b3bc4510f6ccf8db8c3b62120917af15e3de247f0780294b92"},
|
||||
{file = "websockets-12.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f38a7b376117ef7aff996e737583172bdf535932c9ca021746573bce40165ed"},
|
||||
{file = "websockets-12.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:f764ba54e33daf20e167915edc443b6f88956f37fb606449b4a5b10ba42235a5"},
|
||||
{file = "websockets-12.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:1e4b3f8ea6a9cfa8be8484c9221ec0257508e3a1ec43c36acdefb2a9c3b00aa2"},
|
||||
{file = "websockets-12.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:9fdf06fd06c32205a07e47328ab49c40fc1407cdec801d698a7c41167ea45113"},
|
||||
{file = "websockets-12.0-cp312-cp312-win32.whl", hash = "sha256:baa386875b70cbd81798fa9f71be689c1bf484f65fd6fb08d051a0ee4e79924d"},
|
||||
{file = "websockets-12.0-cp312-cp312-win_amd64.whl", hash = "sha256:ae0a5da8f35a5be197f328d4727dbcfafa53d1824fac3d96cdd3a642fe09394f"},
|
||||
{file = "websockets-12.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:248d8e2446e13c1d4326e0a6a4e9629cb13a11195051a73acf414812700badbd"},
|
||||
{file = "websockets-12.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f44069528d45a933997a6fef143030d8ca8042f0dfaad753e2906398290e2870"},
|
||||
{file = "websockets-12.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c4e37d36f0d19f0a4413d3e18c0d03d0c268ada2061868c1e6f5ab1a6d575077"},
|
||||
{file = "websockets-12.0-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d829f975fc2e527a3ef2f9c8f25e553eb7bc779c6665e8e1d52aa22800bb38b"},
|
||||
{file = "websockets-12.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:2c71bd45a777433dd9113847af751aae36e448bc6b8c361a566cb043eda6ec30"},
|
||||
{file = "websockets-12.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:0bee75f400895aef54157b36ed6d3b308fcab62e5260703add87f44cee9c82a6"},
|
||||
{file = "websockets-12.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:423fc1ed29f7512fceb727e2d2aecb952c46aa34895e9ed96071821309951123"},
|
||||
{file = "websockets-12.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:27a5e9964ef509016759f2ef3f2c1e13f403725a5e6a1775555994966a66e931"},
|
||||
{file = "websockets-12.0-pp38-pypy38_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c3181df4583c4d3994d31fb235dc681d2aaad744fbdbf94c4802485ececdecf2"},
|
||||
{file = "websockets-12.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:b067cb952ce8bf40115f6c19f478dc71c5e719b7fbaa511359795dfd9d1a6468"},
|
||||
{file = "websockets-12.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:00700340c6c7ab788f176d118775202aadea7602c5cc6be6ae127761c16d6b0b"},
|
||||
{file = "websockets-12.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e469d01137942849cff40517c97a30a93ae79917752b34029f0ec72df6b46399"},
|
||||
{file = "websockets-12.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffefa1374cd508d633646d51a8e9277763a9b78ae71324183693959cf94635a7"},
|
||||
{file = "websockets-12.0-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba0cab91b3956dfa9f512147860783a1829a8d905ee218a9837c18f683239611"},
|
||||
{file = "websockets-12.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2cb388a5bfb56df4d9a406783b7f9dbefb888c09b71629351cc6b036e9259370"},
|
||||
{file = "websockets-12.0-py3-none-any.whl", hash = "sha256:dc284bbc8d7c78a6c69e0c7325ab46ee5e40bb4d50e494d8131a07ef47500e9e"},
|
||||
{file = "websockets-12.0.tar.gz", hash = "sha256:81df9cbcbb6c260de1e007e58c011bfebe2dafc8435107b0537f393dd38c8b1b"},
|
||||
{file = "websockets-14.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1f20522e624d7ffbdbe259c6b6a65d73c895045f76a93719aa10cd93b3de100c"},
|
||||
{file = "websockets-14.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:647b573f7d3ada919fd60e64d533409a79dcf1ea21daeb4542d1d996519ca967"},
|
||||
{file = "websockets-14.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6af99a38e49f66be5a64b1e890208ad026cda49355661549c507152113049990"},
|
||||
{file = "websockets-14.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:091ab63dfc8cea748cc22c1db2814eadb77ccbf82829bac6b2fbe3401d548eda"},
|
||||
{file = "websockets-14.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b374e8953ad477d17e4851cdc66d83fdc2db88d9e73abf755c94510ebddceb95"},
|
||||
{file = "websockets-14.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a39d7eceeea35db85b85e1169011bb4321c32e673920ae9c1b6e0978590012a3"},
|
||||
{file = "websockets-14.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0a6f3efd47ffd0d12080594f434faf1cd2549b31e54870b8470b28cc1d3817d9"},
|
||||
{file = "websockets-14.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:065ce275e7c4ffb42cb738dd6b20726ac26ac9ad0a2a48e33ca632351a737267"},
|
||||
{file = "websockets-14.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e9d0e53530ba7b8b5e389c02282f9d2aa47581514bd6049d3a7cffe1385cf5fe"},
|
||||
{file = "websockets-14.2-cp312-cp312-win32.whl", hash = "sha256:20e6dd0984d7ca3037afcb4494e48c74ffb51e8013cac71cf607fffe11df7205"},
|
||||
{file = "websockets-14.2-cp312-cp312-win_amd64.whl", hash = "sha256:44bba1a956c2c9d268bdcdf234d5e5ff4c9b6dc3e300545cbe99af59dda9dcce"},
|
||||
{file = "websockets-14.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6f1372e511c7409a542291bce92d6c83320e02c9cf392223272287ce55bc224e"},
|
||||
{file = "websockets-14.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4da98b72009836179bb596a92297b1a61bb5a830c0e483a7d0766d45070a08ad"},
|
||||
{file = "websockets-14.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8a86a269759026d2bde227652b87be79f8a734e582debf64c9d302faa1e9f03"},
|
||||
{file = "websockets-14.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:86cf1aaeca909bf6815ea714d5c5736c8d6dd3a13770e885aafe062ecbd04f1f"},
|
||||
{file = "websockets-14.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9b0f6c3ba3b1240f602ebb3971d45b02cc12bd1845466dd783496b3b05783a5"},
|
||||
{file = "websockets-14.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:669c3e101c246aa85bc8534e495952e2ca208bd87994650b90a23d745902db9a"},
|
||||
{file = "websockets-14.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:eabdb28b972f3729348e632ab08f2a7b616c7e53d5414c12108c29972e655b20"},
|
||||
{file = "websockets-14.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2066dc4cbcc19f32c12a5a0e8cc1b7ac734e5b64ac0a325ff8353451c4b15ef2"},
|
||||
{file = "websockets-14.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ab95d357cd471df61873dadf66dd05dd4709cae001dd6342edafc8dc6382f307"},
|
||||
{file = "websockets-14.2-cp313-cp313-win32.whl", hash = "sha256:a9e72fb63e5f3feacdcf5b4ff53199ec8c18d66e325c34ee4c551ca748623bbc"},
|
||||
{file = "websockets-14.2-cp313-cp313-win_amd64.whl", hash = "sha256:b439ea828c4ba99bb3176dc8d9b933392a2413c0f6b149fdcba48393f573377f"},
|
||||
{file = "websockets-14.2-py3-none-any.whl", hash = "sha256:7a6ceec4ea84469f15cf15807a747e9efe57e369c384fa86e022b3bea679b79b"},
|
||||
{file = "websockets-14.2.tar.gz", hash = "sha256:5059ed9c54945efb321f097084b4c7e52c246f2c869815876a69d1efc4ad6eb5"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "xdg-base-dirs"
|
||||
version = "6.0.1"
|
||||
requires_python = ">=3.10,<4.0"
|
||||
version = "6.0.2"
|
||||
requires_python = "<4.0,>=3.10"
|
||||
summary = "Variables defined by the XDG Base Directory Specification"
|
||||
files = [
|
||||
{file = "xdg_base_dirs-6.0.1-py3-none-any.whl", hash = "sha256:63f6ebc1721ced2e86c340856e004ef829501a30a37e17079c52cfaf0e1741b9"},
|
||||
{file = "xdg_base_dirs-6.0.1.tar.gz", hash = "sha256:b4c8f4ba72d1286018b25eea374ec6fbf4fddda3d4137edf50de95de53e195a6"},
|
||||
{file = "xdg_base_dirs-6.0.2-py3-none-any.whl", hash = "sha256:3c01d1b758ed4ace150ac960ac0bd13ce4542b9e2cdf01312dcda5012cfebabe"},
|
||||
{file = "xdg_base_dirs-6.0.2.tar.gz", hash = "sha256:950504e14d27cf3c9cb37744680a43bf0ac42efefc4ef4acf98dc736cab2bced"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yarl"
|
||||
version = "1.9.4"
|
||||
requires_python = ">=3.7"
|
||||
version = "1.18.3"
|
||||
requires_python = ">=3.9"
|
||||
summary = "Yet another URL library"
|
||||
dependencies = [
|
||||
"idna>=2.0",
|
||||
"multidict>=4.0",
|
||||
"propcache>=0.2.0",
|
||||
]
|
||||
files = [
|
||||
{file = "yarl-1.9.4-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0d2454f0aef65ea81037759be5ca9947539667eecebca092733b2eb43c965a81"},
|
||||
{file = "yarl-1.9.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:44d8ffbb9c06e5a7f529f38f53eda23e50d1ed33c6c869e01481d3fafa6b8142"},
|
||||
{file = "yarl-1.9.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:aaaea1e536f98754a6e5c56091baa1b6ce2f2700cc4a00b0d49eca8dea471074"},
|
||||
{file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3777ce5536d17989c91696db1d459574e9a9bd37660ea7ee4d3344579bb6f129"},
|
||||
{file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9fc5fc1eeb029757349ad26bbc5880557389a03fa6ada41703db5e068881e5f2"},
|
||||
{file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea65804b5dc88dacd4a40279af0cdadcfe74b3e5b4c897aa0d81cf86927fee78"},
|
||||
{file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa102d6d280a5455ad6a0f9e6d769989638718e938a6a0a2ff3f4a7ff8c62cc4"},
|
||||
{file = "yarl-1.9.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09efe4615ada057ba2d30df871d2f668af661e971dfeedf0c159927d48bbeff0"},
|
||||
{file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:008d3e808d03ef28542372d01057fd09168419cdc8f848efe2804f894ae03e51"},
|
||||
{file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:6f5cb257bc2ec58f437da2b37a8cd48f666db96d47b8a3115c29f316313654ff"},
|
||||
{file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:992f18e0ea248ee03b5a6e8b3b4738850ae7dbb172cc41c966462801cbf62cf7"},
|
||||
{file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:0e9d124c191d5b881060a9e5060627694c3bdd1fe24c5eecc8d5d7d0eb6faabc"},
|
||||
{file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:3986b6f41ad22988e53d5778f91855dc0399b043fc8946d4f2e68af22ee9ff10"},
|
||||
{file = "yarl-1.9.4-cp312-cp312-win32.whl", hash = "sha256:4b21516d181cd77ebd06ce160ef8cc2a5e9ad35fb1c5930882baff5ac865eee7"},
|
||||
{file = "yarl-1.9.4-cp312-cp312-win_amd64.whl", hash = "sha256:a9bd00dc3bc395a662900f33f74feb3e757429e545d831eef5bb280252631984"},
|
||||
{file = "yarl-1.9.4-py3-none-any.whl", hash = "sha256:928cecb0ef9d5a7946eb6ff58417ad2fe9375762382f1bf5c55e61645f2c43ad"},
|
||||
{file = "yarl-1.9.4.tar.gz", hash = "sha256:566db86717cf8080b99b58b083b773a908ae40f06681e87e589a976faf8246bf"},
|
||||
{file = "yarl-1.18.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1dd4bdd05407ced96fed3d7f25dbbf88d2ffb045a0db60dbc247f5b3c5c25d50"},
|
||||
{file = "yarl-1.18.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7c33dd1931a95e5d9a772d0ac5e44cac8957eaf58e3c8da8c1414de7dd27c576"},
|
||||
{file = "yarl-1.18.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:25b411eddcfd56a2f0cd6a384e9f4f7aa3efee14b188de13048c25b5e91f1640"},
|
||||
{file = "yarl-1.18.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:436c4fc0a4d66b2badc6c5fc5ef4e47bb10e4fd9bf0c79524ac719a01f3607c2"},
|
||||
{file = "yarl-1.18.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e35ef8683211db69ffe129a25d5634319a677570ab6b2eba4afa860f54eeaf75"},
|
||||
{file = "yarl-1.18.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:84b2deecba4a3f1a398df819151eb72d29bfeb3b69abb145a00ddc8d30094512"},
|
||||
{file = "yarl-1.18.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00e5a1fea0fd4f5bfa7440a47eff01d9822a65b4488f7cff83155a0f31a2ecba"},
|
||||
{file = "yarl-1.18.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d0e883008013c0e4aef84dcfe2a0b172c4d23c2669412cf5b3371003941f72bb"},
|
||||
{file = "yarl-1.18.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5a3f356548e34a70b0172d8890006c37be92995f62d95a07b4a42e90fba54272"},
|
||||
{file = "yarl-1.18.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:ccd17349166b1bee6e529b4add61727d3f55edb7babbe4069b5764c9587a8cc6"},
|
||||
{file = "yarl-1.18.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b958ddd075ddba5b09bb0be8a6d9906d2ce933aee81100db289badbeb966f54e"},
|
||||
{file = "yarl-1.18.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c7d79f7d9aabd6011004e33b22bc13056a3e3fb54794d138af57f5ee9d9032cb"},
|
||||
{file = "yarl-1.18.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4891ed92157e5430874dad17b15eb1fda57627710756c27422200c52d8a4e393"},
|
||||
{file = "yarl-1.18.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ce1af883b94304f493698b00d0f006d56aea98aeb49d75ec7d98cd4a777e9285"},
|
||||
{file = "yarl-1.18.3-cp312-cp312-win32.whl", hash = "sha256:f91c4803173928a25e1a55b943c81f55b8872f0018be83e3ad4938adffb77dd2"},
|
||||
{file = "yarl-1.18.3-cp312-cp312-win_amd64.whl", hash = "sha256:7e2ee16578af3b52ac2f334c3b1f92262f47e02cc6193c598502bd46f5cd1477"},
|
||||
{file = "yarl-1.18.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:90adb47ad432332d4f0bc28f83a5963f426ce9a1a8809f5e584e704b82685dcb"},
|
||||
{file = "yarl-1.18.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:913829534200eb0f789d45349e55203a091f45c37a2674678744ae52fae23efa"},
|
||||
{file = "yarl-1.18.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ef9f7768395923c3039055c14334ba4d926f3baf7b776c923c93d80195624782"},
|
||||
{file = "yarl-1.18.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88a19f62ff30117e706ebc9090b8ecc79aeb77d0b1f5ec10d2d27a12bc9f66d0"},
|
||||
{file = "yarl-1.18.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e17c9361d46a4d5addf777c6dd5eab0715a7684c2f11b88c67ac37edfba6c482"},
|
||||
{file = "yarl-1.18.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1a74a13a4c857a84a845505fd2d68e54826a2cd01935a96efb1e9d86c728e186"},
|
||||
{file = "yarl-1.18.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:41f7ce59d6ee7741af71d82020346af364949314ed3d87553763a2df1829cc58"},
|
||||
{file = "yarl-1.18.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f52a265001d830bc425f82ca9eabda94a64a4d753b07d623a9f2863fde532b53"},
|
||||
{file = "yarl-1.18.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:82123d0c954dc58db301f5021a01854a85bf1f3bb7d12ae0c01afc414a882ca2"},
|
||||
{file = "yarl-1.18.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:2ec9bbba33b2d00999af4631a3397d1fd78290c48e2a3e52d8dd72db3a067ac8"},
|
||||
{file = "yarl-1.18.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:fbd6748e8ab9b41171bb95c6142faf068f5ef1511935a0aa07025438dd9a9bc1"},
|
||||
{file = "yarl-1.18.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:877d209b6aebeb5b16c42cbb377f5f94d9e556626b1bfff66d7b0d115be88d0a"},
|
||||
{file = "yarl-1.18.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b464c4ab4bfcb41e3bfd3f1c26600d038376c2de3297760dfe064d2cb7ea8e10"},
|
||||
{file = "yarl-1.18.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8d39d351e7faf01483cc7ff7c0213c412e38e5a340238826be7e0e4da450fdc8"},
|
||||
{file = "yarl-1.18.3-cp313-cp313-win32.whl", hash = "sha256:61ee62ead9b68b9123ec24bc866cbef297dd266175d53296e2db5e7f797f902d"},
|
||||
{file = "yarl-1.18.3-cp313-cp313-win_amd64.whl", hash = "sha256:578e281c393af575879990861823ef19d66e2b1d0098414855dd367e234f5b3c"},
|
||||
{file = "yarl-1.18.3-py3-none-any.whl", hash = "sha256:b57f4f58099328dfb26c6a771d09fb20dbbae81d20cfb66141251ea063bd101b"},
|
||||
{file = "yarl-1.18.3.tar.gz", hash = "sha256:ac1801c45cbf77b6c99242eeff4fffb5e4e73a800b5c4ad4fc0be5def634d2e1"},
|
||||
]
|
||||
|
|
|
@ -7,7 +7,6 @@ authors = [
|
|||
]
|
||||
dependencies = [
|
||||
"aiocache>=0.12.2",
|
||||
"attrs>=23.1.0",
|
||||
"pyobjc-framework-MediaPlayer>=10.0 ; sys_platform == 'darwin'",
|
||||
"python-mpd2>=3.1.0",
|
||||
"xdg-base-dirs>=6.0.1",
|
||||
|
@ -56,6 +55,7 @@ build-backend = "pdm.backend"
|
|||
[tool.mypy]
|
||||
mypy_path = 'stubs'
|
||||
plugins = ['pydantic.mypy', 'mpd_now_playable.tools.schema.plugin']
|
||||
enable_incomplete_feature = 'NewGenericSyntax'
|
||||
|
||||
[tool.ruff.lint]
|
||||
select = [
|
||||
|
@ -104,7 +104,7 @@ excludes = ["**/.mypy_cache"]
|
|||
|
||||
[tool.pdm.dev-dependencies]
|
||||
dev = [
|
||||
"mypy>=1.7.1",
|
||||
"mypy>=1.11.0",
|
||||
"ruff>=0.1.6",
|
||||
"class-doc>=0.2.6",
|
||||
]
|
||||
|
|
|
@ -5,9 +5,6 @@
|
|||
"kind": {
|
||||
"const": "cocoa",
|
||||
"default": "cocoa",
|
||||
"enum": [
|
||||
"cocoa"
|
||||
],
|
||||
"title": "Kind",
|
||||
"type": "string"
|
||||
}
|
||||
|
@ -24,6 +21,12 @@
|
|||
"title": "Host",
|
||||
"type": "string"
|
||||
},
|
||||
"music_directory": {
|
||||
"description": "Your music directory, just as it's set up in your mpd.conf. mpd-now-playable uses this setting to figure out an absolute file:// URL for the current song, which MPNowPlayingInfoCenter will use to display cool stuff like audio waveforms. It'll still work fine without setting this, though.",
|
||||
"format": "directory-path",
|
||||
"title": "Music Directory",
|
||||
"type": "string"
|
||||
},
|
||||
"password": {
|
||||
"description": "The password required to connect to your MPD instance, if you need one.",
|
||||
"format": "password",
|
||||
|
@ -46,28 +49,14 @@
|
|||
"WebsocketsReceiverConfig": {
|
||||
"properties": {
|
||||
"host": {
|
||||
"anyOf": [
|
||||
{
|
||||
"format": "hostname",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"items": {
|
||||
"format": "hostname",
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
],
|
||||
"description": "The hostname you'd like your WebSockets server to listen on. In most cases the default behaviour, which binds to all network interfaces, will be fine.",
|
||||
"title": "Host"
|
||||
"format": "hostname",
|
||||
"title": "Host",
|
||||
"type": "string"
|
||||
},
|
||||
"kind": {
|
||||
"const": "websockets",
|
||||
"default": "websockets",
|
||||
"enum": [
|
||||
"websockets"
|
||||
],
|
||||
"title": "Kind",
|
||||
"type": "string"
|
||||
},
|
||||
|
|
378
schemata/playback-v1.json
Normal file
378
schemata/playback-v1.json
Normal file
|
@ -0,0 +1,378 @@
|
|||
{
|
||||
"$defs": {
|
||||
"HasArtwork": {
|
||||
"properties": {
|
||||
"data": {
|
||||
"format": "binary",
|
||||
"title": "Data",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"data"
|
||||
],
|
||||
"title": "HasArtwork",
|
||||
"type": "object"
|
||||
},
|
||||
"MixRamp": {
|
||||
"properties": {
|
||||
"db": {
|
||||
"description": "The volume threshold at which MPD will overlap MixRamp-analysed songs, measured in decibels. Can be set to any float, but sensible values are typically negative.",
|
||||
"title": "Db",
|
||||
"type": "number"
|
||||
},
|
||||
"delay": {
|
||||
"description": "A delay time in seconds which will be subtracted from the MixRamp overlap. Must be set to a positive value for MixRamp to work at all - will be zero if it's disabled.",
|
||||
"title": "Delay",
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"db",
|
||||
"delay"
|
||||
],
|
||||
"title": "MixRamp",
|
||||
"type": "object"
|
||||
},
|
||||
"MusicBrainzIds": {
|
||||
"properties": {
|
||||
"artist": {
|
||||
"description": "A MusicBrainz artist is pretty intuitively the artist who recorded the song. This particular ID refers to the individual recording's artist or artists, which may be distinct from the release artist below when a release contains recordings from many different artists. https://musicbrainz.org/doc/Artist",
|
||||
"items": {
|
||||
"format": "uuid",
|
||||
"type": "string"
|
||||
},
|
||||
"title": "Artist",
|
||||
"type": "array"
|
||||
},
|
||||
"recording": {
|
||||
"description": "A MusicBrainz recording represents audio from a specific performance. For example, if the same song was released as a studio recording and as a live performance, those two versions of the song are different recordings. The song itself is considered a \"work\", of which two recordings were made. However, recordings are not always associated with a work in the MusicBrainz database, and Picard won't load work IDs by default (you have to enable \"use track relationships\" in the options), so recording IDs are a much more reliable way to identify a particular song. https://musicbrainz.org/doc/Recording",
|
||||
"format": "uuid",
|
||||
"title": "Recording",
|
||||
"type": "string"
|
||||
},
|
||||
"release": {
|
||||
"description": "A MusicBrainz release roughly corresponds to an \"album\", and indeed is stored in a tag called MUSICBRAINZ_ALBUMID. The more general name is meant to encompass all the different ways music can be released. https://musicbrainz.org/doc/Release",
|
||||
"items": {
|
||||
"format": "uuid",
|
||||
"type": "string"
|
||||
},
|
||||
"title": "Release",
|
||||
"type": "array"
|
||||
},
|
||||
"release_artist": {
|
||||
"description": "Again, the release artist corresponds to an \"album artist\". These MBIDs refer to the same artists in the MusicBrainz database that individual recordings' artist MBIDs do.",
|
||||
"items": {
|
||||
"format": "uuid",
|
||||
"type": "string"
|
||||
},
|
||||
"title": "Release Artist",
|
||||
"type": "array"
|
||||
},
|
||||
"release_group": {
|
||||
"description": "A MusicBrainz release group roughly corresponds to \"all the editions of a particular album\". For example, if the same album were released on CD, vinyl records, and as a digital download, then all of those would be different releases but share a release group. Note that MPD's support for this tag is relatively new (July 2023) and doesn't seem especially reliable, so it might be missing here even if your music has been tagged with it. Not sure why. https://musicbrainz.org/doc/Release_Group",
|
||||
"format": "uuid",
|
||||
"title": "Release Group",
|
||||
"type": "string"
|
||||
},
|
||||
"track": {
|
||||
"description": "A MusicBrainz track represents a specific instance of a recording appearing as part of some release. For example, if the same song appears on both two-CD and four-CD versions of a soundtrack, then it will be considered the same \"recording\" in both cases, but different \"tracks\". https://musicbrainz.org/doc/Track",
|
||||
"format": "uuid",
|
||||
"title": "Track",
|
||||
"type": "string"
|
||||
},
|
||||
"work": {
|
||||
"description": "A MusicBrainz work represents the idea of a particular song or creation (it doesn't have to be audio). Each work may have multiple recordings (studio versus live, different performers, etc.), with the work ID grouping them together. https://musicbrainz.org/doc/Work",
|
||||
"format": "uuid",
|
||||
"title": "Work",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"artist",
|
||||
"release",
|
||||
"release_artist"
|
||||
],
|
||||
"title": "MusicBrainzIds",
|
||||
"type": "object"
|
||||
},
|
||||
"NoArtwork": {
|
||||
"properties": {},
|
||||
"title": "NoArtwork",
|
||||
"type": "object"
|
||||
},
|
||||
"Queue": {
|
||||
"properties": {
|
||||
"current": {
|
||||
"description": "The zero-based index of the current song in MPD's queue. If MPD is currently stopped, then there is no current song in the queue, indicated by None.",
|
||||
"title": "Current",
|
||||
"type": "integer"
|
||||
},
|
||||
"length": {
|
||||
"description": "The total length of MPD's queue - the last song in the queue will have the index one less than this, since queue indices are zero-based.",
|
||||
"title": "Length",
|
||||
"type": "integer"
|
||||
},
|
||||
"next": {
|
||||
"description": "The index of the next song to be played, taking into account random and repeat playback settings.",
|
||||
"title": "Next",
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"current",
|
||||
"next",
|
||||
"length"
|
||||
],
|
||||
"title": "Queue",
|
||||
"type": "object"
|
||||
},
|
||||
"Settings": {
|
||||
"properties": {
|
||||
"consume": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "boolean"
|
||||
},
|
||||
{
|
||||
"const": "oneshot",
|
||||
"type": "string"
|
||||
}
|
||||
],
|
||||
"description": "Remove songs from the queue as they're played. This flag can also be set to \"oneshot\", which means the currently playing song will be consumed, and then the flag will automatically be switched off.",
|
||||
"title": "Consume"
|
||||
},
|
||||
"crossfade": {
|
||||
"description": "The number of seconds to overlap songs when cross-fading between the current song and the next. Will be zero when the cross-fading feature is disabled entirely. Curiously, fractional seconds are not supported here, unlike many other places MPD uses seconds.",
|
||||
"minimum": 0,
|
||||
"title": "Crossfade",
|
||||
"type": "integer"
|
||||
},
|
||||
"mixramp": {
|
||||
"$ref": "#/$defs/MixRamp",
|
||||
"description": "Settings for MixRamp-powered cross-fading, which analyses your songs' volume levels to choose optimal places for cross-fading. This requires either that the songs have previously been analysed and tagged with MixRamp information, or that MPD's on the fly mixramp_analyzer has been enabled."
|
||||
},
|
||||
"random": {
|
||||
"description": "Play the queued songs in random order. This is distinct from shuffling the queue, which randomises the queue's order once when you send the shuffle command and will then play the queue in that new order repeatedly if asked. If MPD is asked to both repeat and randomise, the queue is effectively shuffled each time it loops.",
|
||||
"title": "Random",
|
||||
"type": "boolean"
|
||||
},
|
||||
"repeat": {
|
||||
"description": "Repeat playback of the queued songs. This setting normally means the entire queue will be played on repeat, but its behaviour can be influenced by the other playback mode flags.",
|
||||
"title": "Repeat",
|
||||
"type": "boolean"
|
||||
},
|
||||
"single": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "boolean"
|
||||
},
|
||||
{
|
||||
"const": "oneshot",
|
||||
"type": "string"
|
||||
}
|
||||
],
|
||||
"description": "Play only a single song. If MPD is asked to repeat, then the current song will be played repeatedly. Otherwise, when the current song ends MPD will simply stop playback. Like the consume flag, the single flag can also be set to \"oneshot\", which will cause the single flag to be switched off after it takes effect once (either the current song will repeat just once, or playback will stop but the single flag will be switched off).",
|
||||
"title": "Single"
|
||||
},
|
||||
"volume": {
|
||||
"description": "The playback volume ranging from 0 to 100 - it will only be available if MPD has a volume mixer configured.",
|
||||
"title": "Volume",
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"volume",
|
||||
"repeat",
|
||||
"random",
|
||||
"single",
|
||||
"consume",
|
||||
"crossfade",
|
||||
"mixramp"
|
||||
],
|
||||
"title": "Settings",
|
||||
"type": "object"
|
||||
},
|
||||
"Song": {
|
||||
"properties": {
|
||||
"album": {
|
||||
"description": "The name of the song's containing album, which may be multivalued.",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"title": "Album",
|
||||
"type": "array"
|
||||
},
|
||||
"album_artist": {
|
||||
"description": "The album's artists. This is often used to group together songs from a single album that featured different artists.",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"title": "Album Artist",
|
||||
"type": "array"
|
||||
},
|
||||
"art": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/$defs/HasArtwork"
|
||||
},
|
||||
{
|
||||
"$ref": "#/$defs/NoArtwork"
|
||||
}
|
||||
],
|
||||
"description": "The song's cover art, if it has any - the art will be available as bytes if present, ready to be displayed directly by receivers.",
|
||||
"title": "Art"
|
||||
},
|
||||
"artist": {
|
||||
"description": "The song's artists. Will be an empty list if the song has not been tagged with an artist, and may contain multiple values if the song has been tagged with several artists.",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"title": "Artist",
|
||||
"type": "array"
|
||||
},
|
||||
"composer": {
|
||||
"description": "The song's composers. Again, this is permitted to be multivalued.",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"title": "Composer",
|
||||
"type": "array"
|
||||
},
|
||||
"disc": {
|
||||
"description": "The disc number of the song on its album. As with the track number, this is usually one-based, but it doesn't have to be.",
|
||||
"title": "Disc",
|
||||
"type": "integer"
|
||||
},
|
||||
"duration": {
|
||||
"description": "The song's duration as read from its tags, measured in seconds. Fractional seconds are allowed. The duration may be unavailable for some sources, such as internet radio streams.",
|
||||
"title": "Duration",
|
||||
"type": "number"
|
||||
},
|
||||
"elapsed": {
|
||||
"description": "How far into the song MPD is, measured in seconds. Fractional seconds are allowed. This is usually going to be less than or equal to the song's duration, but because the duration is tagged as metadata and this value represents the actual elapsed time, it might go higher if the song's duration tag is inaccurate.",
|
||||
"title": "Elapsed",
|
||||
"type": "number"
|
||||
},
|
||||
"file": {
|
||||
"description": "The relative path to the current song inside the music directory. MPD itself uses this path as a stable identifier for the audio file in many places, so you can safely do the same.",
|
||||
"format": "path",
|
||||
"title": "File",
|
||||
"type": "string"
|
||||
},
|
||||
"genre": {
|
||||
"description": "The song's genre or genres. These are completely arbitrary descriptions and don't follow any particular standard.",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"title": "Genre",
|
||||
"type": "array"
|
||||
},
|
||||
"musicbrainz": {
|
||||
"$ref": "#/$defs/MusicBrainzIds",
|
||||
"description": "The MusicBrainz IDs associated with the song and with its artist and album, which if present are an extremely accurate way to identify a given song. They're not always present, though."
|
||||
},
|
||||
"state": {
|
||||
"description": "Whether MPD is currently playing or paused. Pretty simple.",
|
||||
"enum": [
|
||||
"play",
|
||||
"pause"
|
||||
],
|
||||
"title": "State",
|
||||
"type": "string"
|
||||
},
|
||||
"title": {
|
||||
"description": "The song's title, if it's been tagged with one. Currently only one title is supported, since it doesn't make a lot of sense to tag a single audio file with multiple titles.",
|
||||
"title": "Title",
|
||||
"type": "string"
|
||||
},
|
||||
"track": {
|
||||
"description": "The track number the song has on its album. This is usually one-based, but it's just an arbitrary audio tag so a particular album might start at zero or do something weird with it.",
|
||||
"title": "Track",
|
||||
"type": "integer"
|
||||
},
|
||||
"url": {
|
||||
"description": "An absolute URL referring to the current song, if available. If the song's a local file and its absolute path can be determined (mpd-now-playable has been configured with your music directory), then this field will contain a file:// URL. If the song's remote, then MPD itself returns an absolute URL in the first place.",
|
||||
"format": "uri",
|
||||
"title": "Url",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"state",
|
||||
"file",
|
||||
"title",
|
||||
"artist",
|
||||
"composer",
|
||||
"album",
|
||||
"album_artist",
|
||||
"track",
|
||||
"disc",
|
||||
"genre",
|
||||
"duration",
|
||||
"elapsed",
|
||||
"art",
|
||||
"musicbrainz"
|
||||
],
|
||||
"title": "Song",
|
||||
"type": "object"
|
||||
},
|
||||
"Stopped": {
|
||||
"properties": {
|
||||
"state": {
|
||||
"const": "stop",
|
||||
"default": "stop",
|
||||
"title": "State",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"title": "Stopped",
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"$id": "https://static.00dani.me/m/schemata/mpd-now-playable/playback-v1.json",
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"properties": {
|
||||
"partition": {
|
||||
"description": "The MPD partition this playback information came from. Essentially, MPD can act as multiple music player servers simultaneously, distinguished by name. For most users, this will always be \"default\".",
|
||||
"title": "Partition",
|
||||
"type": "string"
|
||||
},
|
||||
"queue": {
|
||||
"$ref": "#/$defs/Queue",
|
||||
"description": "Stats about MPD's song queue, including the current song and next song's indices in it."
|
||||
},
|
||||
"settings": {
|
||||
"$ref": "#/$defs/Settings",
|
||||
"description": "Playback settings such as volume and repeat mode."
|
||||
},
|
||||
"song": {
|
||||
"description": "Information about the current song itself. MPD provides none of this information if its playback is currently stopped, so mpd-now-playable doesn't either and will give you a Stopped instead in that case.",
|
||||
"discriminator": {
|
||||
"mapping": {
|
||||
"pause": "#/$defs/Song",
|
||||
"play": "#/$defs/Song",
|
||||
"stop": "#/$defs/Stopped"
|
||||
},
|
||||
"propertyName": "state"
|
||||
},
|
||||
"oneOf": [
|
||||
{
|
||||
"$ref": "#/$defs/Song"
|
||||
},
|
||||
{
|
||||
"$ref": "#/$defs/Stopped"
|
||||
}
|
||||
],
|
||||
"title": "Song"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"song",
|
||||
"partition",
|
||||
"queue",
|
||||
"settings"
|
||||
],
|
||||
"title": "Playback",
|
||||
"type": "object"
|
||||
}
|
|
@ -80,15 +80,6 @@
|
|||
"properties": {},
|
||||
"title": "NoArtwork",
|
||||
"type": "object"
|
||||
},
|
||||
"PlaybackState": {
|
||||
"enum": [
|
||||
"play",
|
||||
"pause",
|
||||
"stop"
|
||||
],
|
||||
"title": "PlaybackState",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"$id": "https://cdn.00dani.me/m/schemata/mpd-now-playable/song-v1.json",
|
||||
|
@ -144,7 +135,7 @@
|
|||
"type": "integer"
|
||||
},
|
||||
"duration": {
|
||||
"description": "The song's duration as read from its tags, measured in seconds. Fractional seconds are allowed.",
|
||||
"description": "The song's duration as read from its tags, measured in seconds. Fractional seconds are allowed. The duration may be unavailable for some sources, such as internet radio streams.",
|
||||
"title": "Duration",
|
||||
"type": "number"
|
||||
},
|
||||
|
@ -171,19 +162,14 @@
|
|||
"$ref": "#/$defs/MusicBrainzIds",
|
||||
"description": "The MusicBrainz IDs associated with the song and with its artist and album, which if present are an extremely accurate way to identify a given song. They're not always present, though."
|
||||
},
|
||||
"queue_index": {
|
||||
"description": "The zero-based index of the current song in MPD's queue.",
|
||||
"title": "Queue Index",
|
||||
"type": "integer"
|
||||
},
|
||||
"queue_length": {
|
||||
"description": "The total length of MPD's queue - the last song in the queue will have the index one less than this, since queue indices are zero-based.",
|
||||
"title": "Queue Length",
|
||||
"type": "integer"
|
||||
},
|
||||
"state": {
|
||||
"$ref": "#/$defs/PlaybackState",
|
||||
"description": "Whether MPD is currently playing, paused, or stopped. Pretty simple."
|
||||
"description": "Whether MPD is currently playing or paused. Pretty simple.",
|
||||
"enum": [
|
||||
"play",
|
||||
"pause"
|
||||
],
|
||||
"title": "State",
|
||||
"type": "string"
|
||||
},
|
||||
"title": {
|
||||
"description": "The song's title, if it's been tagged with one. Currently only one title is supported, since it doesn't make a lot of sense to tag a single audio file with multiple titles.",
|
||||
|
@ -194,12 +180,16 @@
|
|||
"description": "The track number the song has on its album. This is usually one-based, but it's just an arbitrary audio tag so a particular album might start at zero or do something weird with it.",
|
||||
"title": "Track",
|
||||
"type": "integer"
|
||||
},
|
||||
"url": {
|
||||
"description": "An absolute URL referring to the current song, if available. If the song's a local file and its absolute path can be determined (mpd-now-playable has been configured with your music directory), then this field will contain a file:// URL. If the song's remote, then MPD itself returns an absolute URL in the first place.",
|
||||
"format": "uri",
|
||||
"title": "Url",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"state",
|
||||
"queue_index",
|
||||
"queue_length",
|
||||
"file",
|
||||
"title",
|
||||
"artist",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Generic, Optional, TypeVar
|
||||
from typing import Any, Optional
|
||||
|
||||
import ormsgpack
|
||||
from aiocache import Cache
|
||||
|
@ -8,10 +8,8 @@ from aiocache.serializers import BaseSerializer
|
|||
from pydantic.type_adapter import TypeAdapter
|
||||
from yarl import URL
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
class OrmsgpackSerializer(BaseSerializer, Generic[T]):
|
||||
class OrmsgpackSerializer[T](BaseSerializer):
|
||||
DEFAULT_ENCODING = None
|
||||
|
||||
def __init__(self, schema: TypeAdapter[T]):
|
||||
|
@ -28,7 +26,7 @@ class OrmsgpackSerializer(BaseSerializer, Generic[T]):
|
|||
return self.schema.validate_python(data)
|
||||
|
||||
|
||||
def make_cache(schema: TypeAdapter[T], url: URL, namespace: str = "") -> Cache[T]:
|
||||
def make_cache[T](schema: TypeAdapter[T], url: URL, namespace: str = "") -> Cache[T]:
|
||||
backend = Cache.get_scheme_class(url.scheme)
|
||||
if backend == Cache.MEMORY:
|
||||
return Cache(backend)
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
from collections.abc import Mapping
|
||||
from os import environ
|
||||
from typing import TypeVar
|
||||
|
||||
from boltons.iterutils import remap
|
||||
from pytomlpp import load
|
||||
|
@ -9,8 +8,6 @@ from xdg_base_dirs import xdg_config_home
|
|||
from .model import Config
|
||||
|
||||
__all__ = ("loadConfig",)
|
||||
K = TypeVar("K")
|
||||
V = TypeVar("V")
|
||||
|
||||
|
||||
# Sadly this is the kind of function that's incredibly easy to type statically
|
||||
|
@ -24,7 +21,7 @@ V = TypeVar("V")
|
|||
# type like NonNullable<T>. Python's type system also doesn't infer a
|
||||
# dictionary literal as having a structural type by default in the way
|
||||
# TypeScript does, of course, so that part wouldn't work anyway.
|
||||
def withoutNones(data: Mapping[K, V | None]) -> Mapping[K, V]:
|
||||
def withoutNones[K, V](data: Mapping[K, V | None]) -> Mapping[K, V]:
|
||||
return remap(data, lambda p, k, v: v is not None)
|
||||
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ from typing import Annotated, Literal, Optional, Protocol
|
|||
from pydantic import Field
|
||||
|
||||
from ..tools.schema.define import schema
|
||||
from .fields import Host, Password, Port, Url
|
||||
from ..tools.schema.fields import DirectoryPath, Host, Password, Port, Url
|
||||
|
||||
__all__ = (
|
||||
"Config",
|
||||
|
@ -35,7 +35,7 @@ class WebsocketsReceiverConfig(BaseReceiverConfig):
|
|||
#: The hostname you'd like your WebSockets server to listen on. In most
|
||||
#: cases the default behaviour, which binds to all network interfaces, will
|
||||
#: be fine.
|
||||
host: Optional[Host | tuple[Host, ...]] = None
|
||||
host: Optional[Host] = None
|
||||
|
||||
|
||||
ReceiverConfig = Annotated[
|
||||
|
@ -55,6 +55,12 @@ class MpdConfig:
|
|||
#: servers on one machine for some reason, you probably haven't changed this
|
||||
#: from the default port, 6600.
|
||||
port: Port = Port(6600)
|
||||
#: Your music directory, just as it's set up in your mpd.conf.
|
||||
#: mpd-now-playable uses this setting to figure out an absolute file:// URL
|
||||
#: for the current song, which MPNowPlayingInfoCenter will use to display
|
||||
#: cool stuff like audio waveforms. It'll still work fine without setting
|
||||
#: this, though.
|
||||
music_directory: Optional[DirectoryPath] = None
|
||||
|
||||
|
||||
@schema("https://cdn.00dani.me/m/schemata/mpd-now-playable/config-v1.json")
|
||||
|
|
0
src/mpd_now_playable/mpd/convert/__init__.py
Normal file
0
src/mpd_now_playable/mpd/convert/__init__.py
Normal file
49
src/mpd_now_playable/mpd/convert/to_playback.py
Normal file
49
src/mpd_now_playable/mpd/convert/to_playback.py
Normal file
|
@ -0,0 +1,49 @@
|
|||
from ...config.model import MpdConfig
|
||||
from ...playback import Playback
|
||||
from ...playback.queue import Queue
|
||||
from ...playback.settings import MixRamp, Settings, to_oneshot
|
||||
from ...tools.types import option_fmap
|
||||
from ..types import MpdState
|
||||
from .to_song import to_song
|
||||
|
||||
|
||||
def to_queue(mpd: MpdState) -> Queue:
|
||||
return Queue(
|
||||
current=option_fmap(int, mpd.current.get("pos")),
|
||||
next=int(mpd.status.get("nextsong", 0)),
|
||||
length=int(mpd.status["playlistlength"]),
|
||||
)
|
||||
|
||||
|
||||
def to_mixramp(mpd: MpdState) -> MixRamp:
|
||||
delay = mpd.status.get("mixrampdelay", 0)
|
||||
if delay == "nan":
|
||||
delay = 0
|
||||
return MixRamp(
|
||||
db=float(mpd.status.get("mixrampdb", 0)),
|
||||
delay=float(delay),
|
||||
)
|
||||
|
||||
|
||||
def to_settings(mpd: MpdState) -> Settings:
|
||||
return Settings(
|
||||
volume=option_fmap(int, mpd.status.get("volume")),
|
||||
repeat=mpd.status["repeat"] == "1",
|
||||
random=mpd.status["random"] == "1",
|
||||
single=to_oneshot(mpd.status["single"]),
|
||||
consume=to_oneshot(mpd.status["consume"]),
|
||||
crossfade=int(mpd.status.get("xfade", 0)),
|
||||
mixramp=to_mixramp(mpd),
|
||||
)
|
||||
|
||||
|
||||
def to_playback(config: MpdConfig, mpd: MpdState) -> Playback:
|
||||
partition = mpd.status["partition"]
|
||||
queue = to_queue(mpd)
|
||||
settings = to_settings(mpd)
|
||||
return Playback(
|
||||
partition=partition,
|
||||
queue=queue,
|
||||
settings=settings,
|
||||
song=to_song(config, mpd),
|
||||
)
|
51
src/mpd_now_playable/mpd/convert/to_song.py
Normal file
51
src/mpd_now_playable/mpd/convert/to_song.py
Normal file
|
@ -0,0 +1,51 @@
|
|||
from pathlib import Path
|
||||
|
||||
from yarl import URL
|
||||
|
||||
from ...config.model import MpdConfig
|
||||
from ...playback.state import PlaybackState
|
||||
from ...song import Song, Stopped, to_artwork, to_brainz
|
||||
from ...tools.types import option_fmap, un_maybe_plural
|
||||
from ..types import MpdState
|
||||
|
||||
|
||||
def file_to_url(config: MpdConfig, file: str) -> URL | None:
|
||||
url = URL(file)
|
||||
if url.scheme != "":
|
||||
# We already got an absolute URL - probably a stream? - so we can just return it.
|
||||
return url
|
||||
|
||||
if not config.music_directory:
|
||||
# We have a relative song URI, but we can't make it absolute since no music directory is configured.
|
||||
return None
|
||||
|
||||
# Prepend the configured music directory, then turn the whole path into a file:// URL.
|
||||
abs_file = config.music_directory / file
|
||||
return URL(abs_file.as_uri())
|
||||
|
||||
|
||||
def to_song(config: MpdConfig, mpd: MpdState) -> Song | Stopped:
|
||||
state = PlaybackState(mpd.status["state"])
|
||||
if state == PlaybackState.stop:
|
||||
return Stopped()
|
||||
|
||||
file = mpd.current["file"]
|
||||
url = file_to_url(config, file)
|
||||
|
||||
return Song(
|
||||
state=state,
|
||||
file=Path(file),
|
||||
url=url,
|
||||
title=mpd.current.get("title"),
|
||||
artist=un_maybe_plural(mpd.current.get("artist")),
|
||||
album=un_maybe_plural(mpd.current.get("album")),
|
||||
album_artist=un_maybe_plural(mpd.current.get("albumartist")),
|
||||
composer=un_maybe_plural(mpd.current.get("composer")),
|
||||
genre=un_maybe_plural(mpd.current.get("genre")),
|
||||
track=option_fmap(int, mpd.current.get("track")),
|
||||
disc=option_fmap(int, mpd.current.get("disc")),
|
||||
duration=option_fmap(float, mpd.status.get("duration")),
|
||||
elapsed=float(mpd.status["elapsed"]),
|
||||
musicbrainz=to_brainz(mpd.current),
|
||||
art=to_artwork(mpd.art),
|
||||
)
|
|
@ -1,6 +1,5 @@
|
|||
import asyncio
|
||||
from collections.abc import Iterable
|
||||
from pathlib import Path
|
||||
|
||||
from mpd.asyncio import MPDClient
|
||||
from mpd.base import CommandError
|
||||
|
@ -8,38 +7,18 @@ from rich import print as rprint
|
|||
from yarl import URL
|
||||
|
||||
from ..config.model import MpdConfig
|
||||
from ..playback import Playback
|
||||
from ..playback.state import PlaybackState
|
||||
from ..player import Player
|
||||
from ..song import Artwork, PlaybackState, Song, to_artwork, to_brainz
|
||||
from ..song_receiver import Receiver
|
||||
from ..tools.types import option_fmap, un_maybe_plural
|
||||
from ..tools.asyncio import run_background_task
|
||||
from .artwork_cache import MpdArtworkCache
|
||||
from .types import CurrentSongResponse, StatusResponse
|
||||
|
||||
|
||||
def mpd_current_to_song(
|
||||
status: StatusResponse, current: CurrentSongResponse, art: Artwork
|
||||
) -> Song:
|
||||
return Song(
|
||||
state=PlaybackState(status["state"]),
|
||||
queue_index=int(current["pos"]),
|
||||
queue_length=int(status["playlistlength"]),
|
||||
file=Path(current["file"]),
|
||||
title=current.get("title"),
|
||||
artist=un_maybe_plural(current.get("artist")),
|
||||
album=un_maybe_plural(current.get("album")),
|
||||
album_artist=un_maybe_plural(current.get("albumartist")),
|
||||
composer=un_maybe_plural(current.get("composer")),
|
||||
genre=un_maybe_plural(current.get("genre")),
|
||||
track=option_fmap(int, current.get("track")),
|
||||
disc=option_fmap(int, current.get("disc")),
|
||||
duration=float(status["duration"]),
|
||||
elapsed=float(status["elapsed"]),
|
||||
musicbrainz=to_brainz(current),
|
||||
art=art,
|
||||
)
|
||||
from .convert.to_playback import to_playback
|
||||
from .types import MpdState
|
||||
|
||||
|
||||
class MpdStateListener(Player):
|
||||
config: MpdConfig
|
||||
client: MPDClient
|
||||
receivers: Iterable[Receiver]
|
||||
art_cache: MpdArtworkCache
|
||||
|
@ -52,22 +31,32 @@ class MpdStateListener(Player):
|
|||
)
|
||||
|
||||
async def start(self, conf: MpdConfig) -> None:
|
||||
self.config = conf
|
||||
print(f"Connecting to MPD server {conf.host}:{conf.port}...")
|
||||
await self.client.connect(conf.host, conf.port)
|
||||
if conf.password is not None:
|
||||
print("Authorising to MPD with your password...")
|
||||
await self.client.password(conf.password.get_secret_value())
|
||||
print(f"Connected to MPD v{self.client.mpd_version}")
|
||||
run_background_task(self.heartbeat())
|
||||
|
||||
async def heartbeat(self) -> None:
|
||||
while True:
|
||||
await self.client.ping()
|
||||
await asyncio.sleep(10)
|
||||
|
||||
async def refresh(self) -> None:
|
||||
await self.update_receivers()
|
||||
|
||||
async def loop(self, receivers: Iterable[Receiver]) -> None:
|
||||
self.receivers = receivers
|
||||
# notify our receivers of the initial state MPD is in when this script loads up.
|
||||
# Notify our receivers of the initial state MPD is in when this script loads up.
|
||||
await self.update_receivers()
|
||||
# then wait for stuff to change in MPD. :)
|
||||
async for _ in self.client.idle():
|
||||
# And then wait for stuff to change in MPD. :)
|
||||
async for subsystems in self.client.idle():
|
||||
# If no subsystems actually changed, we don't need to update the receivers.
|
||||
if not subsystems:
|
||||
continue
|
||||
self.idle_count += 1
|
||||
await self.update_receivers()
|
||||
|
||||
|
@ -77,25 +66,24 @@ class MpdStateListener(Player):
|
|||
status, current = await asyncio.gather(
|
||||
self.client.status(), self.client.currentsong()
|
||||
)
|
||||
state = MpdState(status, current)
|
||||
|
||||
if starting_idle_count != self.idle_count:
|
||||
return
|
||||
|
||||
if status["state"] == "stop":
|
||||
print("Nothing playing")
|
||||
await self.update(None)
|
||||
return
|
||||
art = None
|
||||
if status["state"] != "stop":
|
||||
art = await self.art_cache.get_cached_artwork(current)
|
||||
if starting_idle_count != self.idle_count:
|
||||
return
|
||||
|
||||
art = await self.art_cache.get_cached_artwork(current)
|
||||
if starting_idle_count != self.idle_count:
|
||||
return
|
||||
state = MpdState(status, current, art)
|
||||
pb = to_playback(self.config, state)
|
||||
rprint(pb)
|
||||
await self.update(pb)
|
||||
|
||||
song = mpd_current_to_song(status, current, to_artwork(art))
|
||||
rprint(song)
|
||||
await self.update(song)
|
||||
|
||||
async def update(self, song: Song | None) -> None:
|
||||
await asyncio.gather(*(r.update(song) for r in self.receivers))
|
||||
async def update(self, playback: Playback) -> None:
|
||||
await asyncio.gather(*(r.update(playback) for r in self.receivers))
|
||||
|
||||
async def get_art(self, file: str) -> bytes | None:
|
||||
picture = await self.readpicture(file)
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
from dataclasses import dataclass
|
||||
from typing import Literal, NotRequired, Protocol, TypedDict
|
||||
|
||||
from ..song.musicbrainz import MusicBrainzTags
|
||||
|
@ -19,8 +20,11 @@ OneshotFlag = Literal[BooleanFlag, "oneshot"]
|
|||
class StatusResponse(TypedDict):
|
||||
state: Literal["play", "stop", "pause"]
|
||||
|
||||
# The total duration and elapsed playback of the current song, measured in seconds. Fractional seconds are allowed.
|
||||
duration: str
|
||||
# The total duration and elapsed playback of the current song, measured in
|
||||
# seconds. Fractional seconds are allowed. The duration field may be
|
||||
# omitted because MPD cannot determine the duration of certain sources,
|
||||
# such as Internet radio streams.
|
||||
duration: NotRequired[str]
|
||||
elapsed: str
|
||||
|
||||
# The volume value ranges from 0-100. It may be omitted from
|
||||
|
@ -33,6 +37,20 @@ class StatusResponse(TypedDict):
|
|||
single: OneshotFlag
|
||||
consume: OneshotFlag
|
||||
|
||||
# The configured crossfade time in seconds. Omitted if crossfading isn't
|
||||
# enabled. Fractional seconds are *not* allowed for this field.
|
||||
xfade: NotRequired[str]
|
||||
|
||||
# The volume threshold at which MixRamp-compatible songs will be
|
||||
# overlapped, measured in decibels. Will usually be negative, and is
|
||||
# permitted to be fractional.
|
||||
mixrampdb: NotRequired[str]
|
||||
|
||||
# A number of seconds to subtract from the overlap computed by MixRamp.
|
||||
# Must be positive for MixRamp to work and is permitted to be fractional.
|
||||
# Can be set to "nan" to disable MixRamp and use basic crossfading instead.
|
||||
mixrampdelay: NotRequired[str]
|
||||
|
||||
# Partitions essentially let one MPD server act as multiple music players.
|
||||
# For most folks, this will just be "default", but mpd-now-playable will
|
||||
# eventually support addressing specific partitions. Eventually.
|
||||
|
@ -41,6 +59,10 @@ class StatusResponse(TypedDict):
|
|||
# The total number of items in the play queue, which is called the "playlist" throughout the MPD protocol for legacy reasons.
|
||||
playlistlength: str
|
||||
|
||||
# The zero-based index of the song that will play when the current song
|
||||
# ends, taking into account repeat and random playback settings.
|
||||
nextsong: str
|
||||
|
||||
# The format of decoded audio MPD is producing, expressed as a string in the form "samplerate:bits:channels".
|
||||
audio: str
|
||||
|
||||
|
@ -76,3 +98,10 @@ class CurrentSongResponse(CurrentSongTags):
|
|||
|
||||
|
||||
ReadPictureResponse = TypedDict("ReadPictureResponse", {"binary": bytes})
|
||||
|
||||
|
||||
@dataclass
|
||||
class MpdState:
|
||||
status: StatusResponse
|
||||
current: CurrentSongResponse
|
||||
art: bytes | None = None
|
||||
|
|
3
src/mpd_now_playable/playback/__init__.py
Normal file
3
src/mpd_now_playable/playback/__init__.py
Normal file
|
@ -0,0 +1,3 @@
|
|||
from .playback import Playback
|
||||
|
||||
__all__ = ("Playback",)
|
36
src/mpd_now_playable/playback/playback.py
Normal file
36
src/mpd_now_playable/playback/playback.py
Normal file
|
@ -0,0 +1,36 @@
|
|||
from dataclasses import dataclass
|
||||
|
||||
from pydantic import Field
|
||||
|
||||
from ..song.song import Song
|
||||
from ..song.stopped import Stopped
|
||||
from ..tools.schema.define import schema
|
||||
from .queue import Queue
|
||||
from .settings import Settings
|
||||
|
||||
|
||||
@schema("https://static.00dani.me/m/schemata/mpd-now-playable/playback-v1.json")
|
||||
@dataclass(slots=True, kw_only=True)
|
||||
class Playback:
|
||||
#: The MPD partition this playback information came from. Essentially, MPD
|
||||
#: can act as multiple music player servers simultaneously, distinguished
|
||||
#: by name. For most users, this will always be "default".
|
||||
partition: str
|
||||
|
||||
#: Stats about MPD's song queue, including the current song and next song's
|
||||
#: indices in it.
|
||||
queue: Queue
|
||||
|
||||
#: Playback settings such as volume and repeat mode.
|
||||
settings: Settings
|
||||
|
||||
#: Information about the current song itself. MPD provides none of this
|
||||
#: information if its playback is currently stopped, so mpd-now-playable
|
||||
#: doesn't either and will give you a Stopped instead in that case.
|
||||
song: Song | Stopped = Field(discriminator="state")
|
||||
|
||||
@property
|
||||
def active_song(self) -> Song | None:
|
||||
if isinstance(self.song, Song):
|
||||
return self.song
|
||||
return None
|
15
src/mpd_now_playable/playback/queue.py
Normal file
15
src/mpd_now_playable/playback/queue.py
Normal file
|
@ -0,0 +1,15 @@
|
|||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class Queue:
|
||||
#: The zero-based index of the current song in MPD's queue. If MPD is
|
||||
#: currently stopped, then there is no current song in the queue, indicated
|
||||
#: by None.
|
||||
current: int | None
|
||||
#: The index of the next song to be played, taking into account random and
|
||||
#: repeat playback settings.
|
||||
next: int
|
||||
#: The total length of MPD's queue - the last song in the queue will have
|
||||
#: the index one less than this, since queue indices are zero-based.
|
||||
length: int
|
76
src/mpd_now_playable/playback/settings.py
Normal file
76
src/mpd_now_playable/playback/settings.py
Normal file
|
@ -0,0 +1,76 @@
|
|||
from dataclasses import dataclass
|
||||
from typing import Annotated, Literal
|
||||
|
||||
from annotated_types import Ge
|
||||
|
||||
OneShotFlag = bool | Literal["oneshot"]
|
||||
|
||||
|
||||
def to_oneshot(value: str) -> OneShotFlag:
|
||||
match value:
|
||||
case "1":
|
||||
return True
|
||||
case "0":
|
||||
return False
|
||||
case "oneshot":
|
||||
return "oneshot"
|
||||
return False
|
||||
|
||||
|
||||
@dataclass(slots=True, kw_only=True)
|
||||
class MixRamp:
|
||||
#: The volume threshold at which MPD will overlap MixRamp-analysed songs,
|
||||
#: measured in decibels. Can be set to any float, but sensible values are
|
||||
#: typically negative.
|
||||
db: float
|
||||
|
||||
#: A delay time in seconds which will be subtracted from the MixRamp
|
||||
#: overlap. Must be set to a positive value for MixRamp to work at all -
|
||||
#: will be zero if it's disabled.
|
||||
delay: float
|
||||
|
||||
|
||||
@dataclass(slots=True, kw_only=True)
|
||||
class Settings:
|
||||
#: The playback volume ranging from 0 to 100 - it will only be available if
|
||||
#: MPD has a volume mixer configured.
|
||||
volume: int | None
|
||||
|
||||
#: Repeat playback of the queued songs. This setting normally means the
|
||||
#: entire queue will be played on repeat, but its behaviour can be
|
||||
#: influenced by the other playback mode flags.
|
||||
repeat: bool
|
||||
|
||||
#: Play the queued songs in random order. This is distinct from shuffling
|
||||
#: the queue, which randomises the queue's order once when you send the
|
||||
#: shuffle command and will then play the queue in that new order
|
||||
#: repeatedly if asked. If MPD is asked to both repeat and randomise, the
|
||||
#: queue is effectively shuffled each time it loops.
|
||||
random: bool
|
||||
|
||||
#: Play only a single song. If MPD is asked to repeat, then the current
|
||||
#: song will be played repeatedly. Otherwise, when the current song ends
|
||||
#: MPD will simply stop playback. Like the consume flag, the single flag
|
||||
#: can also be set to "oneshot", which will cause the single flag to be
|
||||
#: switched off after it takes effect once (either the current song will
|
||||
#: repeat just once, or playback will stop but the single flag will be
|
||||
#: switched off).
|
||||
single: OneShotFlag
|
||||
|
||||
#: Remove songs from the queue as they're played. This flag can also be set
|
||||
#: to "oneshot", which means the currently playing song will be consumed,
|
||||
#: and then the flag will automatically be switched off.
|
||||
consume: OneShotFlag
|
||||
|
||||
#: The number of seconds to overlap songs when cross-fading between the
|
||||
#: current song and the next. Will be zero when the cross-fading feature is
|
||||
#: disabled entirely. Curiously, fractional seconds are not supported here,
|
||||
#: unlike many other places MPD uses seconds.
|
||||
crossfade: Annotated[int, Ge(0)]
|
||||
|
||||
#: Settings for MixRamp-powered cross-fading, which analyses your songs'
|
||||
#: volume levels to choose optimal places for cross-fading. This requires
|
||||
#: either that the songs have previously been analysed and tagged with
|
||||
#: MixRamp information, or that MPD's on the fly mixramp_analyzer has been
|
||||
#: enabled.
|
||||
mixramp: MixRamp
|
7
src/mpd_now_playable/playback/state.py
Normal file
7
src/mpd_now_playable/playback/state.py
Normal file
|
@ -0,0 +1,7 @@
|
|||
from enum import StrEnum
|
||||
|
||||
|
||||
class PlaybackState(StrEnum):
|
||||
play = "play"
|
||||
pause = "pause"
|
||||
stop = "stop"
|
|
@ -1,26 +1,19 @@
|
|||
from typing import Protocol
|
||||
|
||||
from .song import PlaybackState
|
||||
from .playback.state import PlaybackState
|
||||
|
||||
|
||||
class Player(Protocol):
|
||||
async def on_play_pause(self) -> PlaybackState:
|
||||
...
|
||||
async def on_play_pause(self) -> PlaybackState: ...
|
||||
|
||||
async def on_play(self) -> PlaybackState:
|
||||
...
|
||||
async def on_play(self) -> PlaybackState: ...
|
||||
|
||||
async def on_pause(self) -> PlaybackState:
|
||||
...
|
||||
async def on_pause(self) -> PlaybackState: ...
|
||||
|
||||
async def on_stop(self) -> PlaybackState:
|
||||
...
|
||||
async def on_stop(self) -> PlaybackState: ...
|
||||
|
||||
async def on_next(self) -> None:
|
||||
...
|
||||
async def on_next(self) -> None: ...
|
||||
|
||||
async def on_prev(self) -> None:
|
||||
...
|
||||
async def on_prev(self) -> None: ...
|
||||
|
||||
async def on_seek(self, position: float) -> None:
|
||||
...
|
||||
async def on_seek(self, position: float) -> None: ...
|
||||
|
|
0
src/mpd_now_playable/receivers/cocoa/convert/__init__.py
Normal file
0
src/mpd_now_playable/receivers/cocoa/convert/__init__.py
Normal file
|
@ -0,0 +1,33 @@
|
|||
from Foundation import NSMutableDictionary
|
||||
from MediaPlayer import (
|
||||
MPMediaItemPropertyArtwork,
|
||||
MPMediaItemPropertyTitle,
|
||||
MPNowPlayingInfoMediaTypeNone,
|
||||
MPNowPlayingInfoPropertyMediaType,
|
||||
MPNowPlayingInfoPropertyPlaybackQueueCount,
|
||||
MPNowPlayingInfoPropertyPlaybackQueueIndex,
|
||||
MPNowPlayingInfoPropertyPlaybackRate,
|
||||
)
|
||||
|
||||
from ....playback import Playback
|
||||
from .song_to_media_item import song_to_media_item
|
||||
from .to_nsimage import MPD_LOGO
|
||||
|
||||
|
||||
def playback_to_media_item(playback: Playback) -> NSMutableDictionary:
|
||||
nowplaying_info = nothing_to_media_item()
|
||||
if song := playback.active_song:
|
||||
nowplaying_info = song_to_media_item(song)
|
||||
nowplaying_info[MPNowPlayingInfoPropertyPlaybackQueueCount] = playback.queue.length
|
||||
nowplaying_info[MPNowPlayingInfoPropertyPlaybackQueueIndex] = playback.queue.current
|
||||
return nowplaying_info
|
||||
|
||||
|
||||
def nothing_to_media_item() -> NSMutableDictionary:
|
||||
nowplaying_info = NSMutableDictionary.dictionary()
|
||||
nowplaying_info[MPNowPlayingInfoPropertyMediaType] = MPNowPlayingInfoMediaTypeNone
|
||||
nowplaying_info[MPMediaItemPropertyArtwork] = MPD_LOGO
|
||||
nowplaying_info[MPMediaItemPropertyTitle] = "MPD (stopped)"
|
||||
nowplaying_info[MPNowPlayingInfoPropertyPlaybackRate] = 0.0
|
||||
|
||||
return nowplaying_info
|
|
@ -0,0 +1,61 @@
|
|||
from Foundation import NSMutableDictionary
|
||||
from MediaPlayer import (
|
||||
MPMediaItemPropertyAlbumTitle,
|
||||
MPMediaItemPropertyAlbumTrackNumber,
|
||||
MPMediaItemPropertyArtist,
|
||||
MPMediaItemPropertyArtwork,
|
||||
MPMediaItemPropertyComposer,
|
||||
MPMediaItemPropertyDiscNumber,
|
||||
MPMediaItemPropertyGenre,
|
||||
MPMediaItemPropertyPersistentID,
|
||||
MPMediaItemPropertyPlaybackDuration,
|
||||
MPMediaItemPropertyTitle,
|
||||
MPNowPlayingInfoMediaTypeAudio,
|
||||
MPNowPlayingInfoPropertyAssetURL,
|
||||
MPNowPlayingInfoPropertyElapsedPlaybackTime,
|
||||
MPNowPlayingInfoPropertyExternalContentIdentifier,
|
||||
MPNowPlayingInfoPropertyMediaType,
|
||||
MPNowPlayingInfoPropertyPlaybackRate,
|
||||
)
|
||||
|
||||
from ....playback.state import PlaybackState
|
||||
from ....song import Song
|
||||
from ..persistent_id import song_to_persistent_id
|
||||
from .to_nsimage import data_to_media_item_artwork
|
||||
|
||||
|
||||
def join_plural_field(field: list[str]) -> str | None:
|
||||
if field:
|
||||
return ", ".join(field)
|
||||
return None
|
||||
|
||||
|
||||
def song_to_media_item(song: Song) -> NSMutableDictionary:
|
||||
nowplaying_info = NSMutableDictionary.dictionary()
|
||||
nowplaying_info[MPNowPlayingInfoPropertyMediaType] = MPNowPlayingInfoMediaTypeAudio
|
||||
nowplaying_info[MPNowPlayingInfoPropertyElapsedPlaybackTime] = song.elapsed
|
||||
nowplaying_info[MPNowPlayingInfoPropertyExternalContentIdentifier] = str(song.file)
|
||||
nowplaying_info[MPMediaItemPropertyPersistentID] = song_to_persistent_id(song)
|
||||
|
||||
nowplaying_info[MPMediaItemPropertyTitle] = song.title
|
||||
nowplaying_info[MPMediaItemPropertyArtist] = join_plural_field(song.artist)
|
||||
nowplaying_info[MPMediaItemPropertyAlbumTitle] = join_plural_field(song.album)
|
||||
nowplaying_info[MPMediaItemPropertyAlbumTrackNumber] = song.track
|
||||
nowplaying_info[MPMediaItemPropertyDiscNumber] = song.disc
|
||||
nowplaying_info[MPMediaItemPropertyGenre] = join_plural_field(song.genre)
|
||||
nowplaying_info[MPMediaItemPropertyComposer] = join_plural_field(song.composer)
|
||||
nowplaying_info[MPMediaItemPropertyPlaybackDuration] = song.duration
|
||||
|
||||
if song.url is not None:
|
||||
nowplaying_info[MPNowPlayingInfoPropertyAssetURL] = song.url.human_repr()
|
||||
|
||||
# MPD can't play back music at different rates, so we just want to set it
|
||||
# to 1.0 if the song is playing. (Set it to 0.0 if the song is paused.)
|
||||
rate = 1.0 if song.state == PlaybackState.play else 0.0
|
||||
nowplaying_info[MPNowPlayingInfoPropertyPlaybackRate] = rate
|
||||
|
||||
if song.art:
|
||||
artwork = data_to_media_item_artwork(song.art.data)
|
||||
nowplaying_info[MPMediaItemPropertyArtwork] = artwork
|
||||
|
||||
return nowplaying_info
|
40
src/mpd_now_playable/receivers/cocoa/convert/to_nsimage.py
Normal file
40
src/mpd_now_playable/receivers/cocoa/convert/to_nsimage.py
Normal file
|
@ -0,0 +1,40 @@
|
|||
from pathlib import Path
|
||||
|
||||
from AppKit import NSCompositingOperationCopy, NSImage, NSMakeRect
|
||||
from Foundation import CGSize
|
||||
from MediaPlayer import MPMediaItemArtwork
|
||||
|
||||
|
||||
def logo_to_ns_image() -> NSImage:
|
||||
return NSImage.alloc().initByReferencingFile_(
|
||||
str(Path(__file__).parents[3] / "mpd/logo.svg")
|
||||
)
|
||||
|
||||
|
||||
def data_to_ns_image(data: bytes) -> NSImage:
|
||||
return NSImage.alloc().initWithData_(data)
|
||||
|
||||
|
||||
def data_to_media_item_artwork(data: bytes) -> MPMediaItemArtwork:
|
||||
return ns_image_to_media_item_artwork(data_to_ns_image(data))
|
||||
|
||||
|
||||
def ns_image_to_media_item_artwork(img: NSImage) -> MPMediaItemArtwork:
|
||||
def resize(size: CGSize) -> NSImage:
|
||||
new = NSImage.alloc().initWithSize_(size)
|
||||
new.lockFocus()
|
||||
img.drawInRect_fromRect_operation_fraction_(
|
||||
NSMakeRect(0, 0, size.width, size.height),
|
||||
NSMakeRect(0, 0, img.size().width, img.size().height),
|
||||
NSCompositingOperationCopy,
|
||||
1.0,
|
||||
)
|
||||
new.unlockFocus()
|
||||
return new
|
||||
|
||||
return MPMediaItemArtwork.alloc().initWithBoundsSize_requestHandler_(
|
||||
img.size(), resize
|
||||
)
|
||||
|
||||
|
||||
MPD_LOGO = ns_image_to_media_item_artwork(logo_to_ns_image())
|
19
src/mpd_now_playable/receivers/cocoa/convert/to_state.py
Normal file
19
src/mpd_now_playable/receivers/cocoa/convert/to_state.py
Normal file
|
@ -0,0 +1,19 @@
|
|||
from MediaPlayer import (
|
||||
MPMusicPlaybackState,
|
||||
MPMusicPlaybackStatePaused,
|
||||
MPMusicPlaybackStatePlaying,
|
||||
MPMusicPlaybackStateStopped,
|
||||
)
|
||||
|
||||
from ....playback.state import PlaybackState
|
||||
|
||||
__all__ = ("playback_state_to_cocoa",)
|
||||
|
||||
|
||||
def playback_state_to_cocoa(state: PlaybackState) -> MPMusicPlaybackState:
|
||||
mapping: dict[PlaybackState, MPMusicPlaybackState] = {
|
||||
PlaybackState.play: MPMusicPlaybackStatePlaying,
|
||||
PlaybackState.pause: MPMusicPlaybackStatePaused,
|
||||
PlaybackState.stop: MPMusicPlaybackStateStopped,
|
||||
}
|
||||
return mapping[state]
|
|
@ -1,134 +1,27 @@
|
|||
from collections.abc import Callable, Coroutine
|
||||
from pathlib import Path
|
||||
from typing import Literal
|
||||
|
||||
from AppKit import NSCompositingOperationCopy, NSImage, NSMakeRect
|
||||
from corefoundationasyncio import CoreFoundationEventLoop
|
||||
from Foundation import CGSize, NSMutableDictionary
|
||||
from AppKit import NSApplication, NSApplicationActivationPolicyAccessory
|
||||
from MediaPlayer import (
|
||||
MPChangePlaybackPositionCommandEvent,
|
||||
MPMediaItemArtwork,
|
||||
MPMediaItemPropertyAlbumTitle,
|
||||
MPMediaItemPropertyAlbumTrackNumber,
|
||||
MPMediaItemPropertyArtist,
|
||||
MPMediaItemPropertyArtwork,
|
||||
MPMediaItemPropertyComposer,
|
||||
MPMediaItemPropertyDiscNumber,
|
||||
MPMediaItemPropertyGenre,
|
||||
MPMediaItemPropertyPersistentID,
|
||||
MPMediaItemPropertyPlaybackDuration,
|
||||
MPMediaItemPropertyTitle,
|
||||
MPMusicPlaybackState,
|
||||
MPMusicPlaybackStatePaused,
|
||||
MPMusicPlaybackStatePlaying,
|
||||
MPMusicPlaybackStateStopped,
|
||||
MPNowPlayingInfoCenter,
|
||||
MPNowPlayingInfoMediaTypeAudio,
|
||||
MPNowPlayingInfoMediaTypeNone,
|
||||
MPNowPlayingInfoPropertyElapsedPlaybackTime,
|
||||
MPNowPlayingInfoPropertyExternalContentIdentifier,
|
||||
MPNowPlayingInfoPropertyMediaType,
|
||||
MPNowPlayingInfoPropertyPlaybackQueueCount,
|
||||
MPNowPlayingInfoPropertyPlaybackQueueIndex,
|
||||
MPNowPlayingInfoPropertyPlaybackRate,
|
||||
MPRemoteCommandCenter,
|
||||
MPRemoteCommandEvent,
|
||||
MPRemoteCommandHandlerStatus,
|
||||
MPRemoteCommandHandlerStatusSuccess,
|
||||
)
|
||||
|
||||
from corefoundationasyncio import CoreFoundationEventLoop
|
||||
|
||||
from ...config.model import CocoaReceiverConfig
|
||||
from ...playback import Playback
|
||||
from ...playback.state import PlaybackState
|
||||
from ...player import Player
|
||||
from ...song import PlaybackState, Song
|
||||
from ...song_receiver import LoopFactory, Receiver
|
||||
from ...tools.asyncio import run_background_task
|
||||
from .persistent_id import song_to_persistent_id
|
||||
|
||||
|
||||
def logo_to_ns_image() -> NSImage:
|
||||
return NSImage.alloc().initByReferencingFile_(
|
||||
str(Path(__file__).parent.parent.parent / "mpd/logo.svg")
|
||||
)
|
||||
|
||||
|
||||
def data_to_ns_image(data: bytes) -> NSImage:
|
||||
return NSImage.alloc().initWithData_(data)
|
||||
|
||||
|
||||
def ns_image_to_media_item_artwork(img: NSImage) -> MPMediaItemArtwork:
|
||||
def resize(size: CGSize) -> NSImage:
|
||||
new = NSImage.alloc().initWithSize_(size)
|
||||
new.lockFocus()
|
||||
img.drawInRect_fromRect_operation_fraction_(
|
||||
NSMakeRect(0, 0, size.width, size.height),
|
||||
NSMakeRect(0, 0, img.size().width, img.size().height),
|
||||
NSCompositingOperationCopy,
|
||||
1.0,
|
||||
)
|
||||
new.unlockFocus()
|
||||
return new
|
||||
|
||||
return MPMediaItemArtwork.alloc().initWithBoundsSize_requestHandler_(
|
||||
img.size(), resize
|
||||
)
|
||||
|
||||
|
||||
def playback_state_to_cocoa(state: PlaybackState) -> MPMusicPlaybackState:
|
||||
mapping: dict[PlaybackState, MPMusicPlaybackState] = {
|
||||
PlaybackState.play: MPMusicPlaybackStatePlaying,
|
||||
PlaybackState.pause: MPMusicPlaybackStatePaused,
|
||||
PlaybackState.stop: MPMusicPlaybackStateStopped,
|
||||
}
|
||||
return mapping[state]
|
||||
|
||||
|
||||
def join_plural_field(field: list[str]) -> str | None:
|
||||
if field:
|
||||
return ", ".join(field)
|
||||
return None
|
||||
|
||||
|
||||
def song_to_media_item(song: Song) -> NSMutableDictionary:
|
||||
nowplaying_info = nothing_to_media_item()
|
||||
nowplaying_info[MPNowPlayingInfoPropertyMediaType] = MPNowPlayingInfoMediaTypeAudio
|
||||
nowplaying_info[MPNowPlayingInfoPropertyElapsedPlaybackTime] = song.elapsed
|
||||
nowplaying_info[MPNowPlayingInfoPropertyExternalContentIdentifier] = str(song.file)
|
||||
nowplaying_info[MPNowPlayingInfoPropertyPlaybackQueueCount] = song.queue_length
|
||||
nowplaying_info[MPNowPlayingInfoPropertyPlaybackQueueIndex] = song.queue_index
|
||||
nowplaying_info[MPMediaItemPropertyPersistentID] = song_to_persistent_id(song)
|
||||
|
||||
nowplaying_info[MPMediaItemPropertyTitle] = song.title
|
||||
nowplaying_info[MPMediaItemPropertyArtist] = join_plural_field(song.artist)
|
||||
nowplaying_info[MPMediaItemPropertyAlbumTitle] = join_plural_field(song.album)
|
||||
nowplaying_info[MPMediaItemPropertyAlbumTrackNumber] = song.track
|
||||
nowplaying_info[MPMediaItemPropertyDiscNumber] = song.disc
|
||||
nowplaying_info[MPMediaItemPropertyGenre] = join_plural_field(song.genre)
|
||||
nowplaying_info[MPMediaItemPropertyComposer] = join_plural_field(song.composer)
|
||||
nowplaying_info[MPMediaItemPropertyPlaybackDuration] = song.duration
|
||||
|
||||
# MPD can't play back music at different rates, so we just want to set it
|
||||
# to 1.0 if the song is playing. (Leave it at 0.0 if the song is paused.)
|
||||
if song.state == PlaybackState.play:
|
||||
nowplaying_info[MPNowPlayingInfoPropertyPlaybackRate] = 1.0
|
||||
|
||||
if song.art:
|
||||
nowplaying_info[MPMediaItemPropertyArtwork] = ns_image_to_media_item_artwork(
|
||||
data_to_ns_image(song.art.data)
|
||||
)
|
||||
return nowplaying_info
|
||||
|
||||
|
||||
def nothing_to_media_item() -> NSMutableDictionary:
|
||||
nowplaying_info = NSMutableDictionary.dictionary()
|
||||
nowplaying_info[MPNowPlayingInfoPropertyMediaType] = MPNowPlayingInfoMediaTypeNone
|
||||
nowplaying_info[MPMediaItemPropertyArtwork] = MPD_LOGO
|
||||
nowplaying_info[MPMediaItemPropertyTitle] = "MPD (stopped)"
|
||||
nowplaying_info[MPNowPlayingInfoPropertyPlaybackRate] = 0.0
|
||||
|
||||
return nowplaying_info
|
||||
|
||||
|
||||
MPD_LOGO = ns_image_to_media_item_artwork(logo_to_ns_image())
|
||||
from .convert.playback_to_media_item import playback_to_media_item
|
||||
from .convert.to_state import playback_state_to_cocoa
|
||||
|
||||
|
||||
class CocoaLoopFactory(LoopFactory[CoreFoundationEventLoop]):
|
||||
|
@ -150,6 +43,9 @@ class CocoaNowPlayingReceiver(Receiver):
|
|||
pass
|
||||
|
||||
async def start(self, player: Player) -> None:
|
||||
NSApplication.sharedApplication().setActivationPolicy_(
|
||||
NSApplicationActivationPolicyAccessory
|
||||
)
|
||||
self.cmd_center = MPRemoteCommandCenter.sharedCommandCenter()
|
||||
self.info_center = MPNowPlayingInfoCenter.defaultCenter()
|
||||
|
||||
|
@ -187,13 +83,9 @@ class CocoaNowPlayingReceiver(Receiver):
|
|||
# unpause with remote commands.
|
||||
self.info_center.setPlaybackState_(MPMusicPlaybackStatePlaying)
|
||||
|
||||
async def update(self, song: Song | None) -> None:
|
||||
if song:
|
||||
self.info_center.setNowPlayingInfo_(song_to_media_item(song))
|
||||
self.info_center.setPlaybackState_(playback_state_to_cocoa(song.state))
|
||||
else:
|
||||
self.info_center.setNowPlayingInfo_(nothing_to_media_item())
|
||||
self.info_center.setPlaybackState_(MPMusicPlaybackStateStopped)
|
||||
async def update(self, playback: Playback) -> None:
|
||||
self.info_center.setNowPlayingInfo_(playback_to_media_item(playback))
|
||||
self.info_center.setPlaybackState_(playback_state_to_cocoa(playback.song.state))
|
||||
|
||||
def _create_handler(
|
||||
self, player: Callable[[], Coroutine[None, None, PlaybackState | None]]
|
||||
|
|
|
@ -2,11 +2,12 @@ from pathlib import Path
|
|||
|
||||
import ormsgpack
|
||||
from websockets import broadcast
|
||||
from websockets.server import WebSocketServerProtocol, serve
|
||||
from websockets.asyncio.server import Server, ServerConnection, serve
|
||||
from yarl import URL
|
||||
|
||||
from ...config.model import WebsocketsReceiverConfig
|
||||
from ...playback import Playback
|
||||
from ...player import Player
|
||||
from ...song import Song
|
||||
from ...song_receiver import DefaultLoopFactory, Receiver
|
||||
|
||||
MSGPACK_NULL = ormsgpack.packb(None)
|
||||
|
@ -15,18 +16,19 @@ MSGPACK_NULL = ormsgpack.packb(None)
|
|||
def default(value: object) -> object:
|
||||
if isinstance(value, Path):
|
||||
return str(value)
|
||||
if isinstance(value, URL):
|
||||
return value.human_repr()
|
||||
raise TypeError
|
||||
|
||||
|
||||
class WebsocketsReceiver(Receiver):
|
||||
config: WebsocketsReceiverConfig
|
||||
player: Player
|
||||
connections: set[WebSocketServerProtocol]
|
||||
server: Server
|
||||
last_status: bytes = MSGPACK_NULL
|
||||
|
||||
def __init__(self, config: WebsocketsReceiverConfig):
|
||||
self.config = config
|
||||
self.connections = set()
|
||||
|
||||
@classmethod
|
||||
def loop_factory(cls) -> DefaultLoopFactory:
|
||||
|
@ -34,19 +36,14 @@ class WebsocketsReceiver(Receiver):
|
|||
|
||||
async def start(self, player: Player) -> None:
|
||||
self.player = player
|
||||
await serve(self.handle, host=self.config.host, port=self.config.port, reuse_port=True)
|
||||
self.server = await serve(
|
||||
self.handle, host=self.config.host, port=self.config.port, reuse_port=True
|
||||
)
|
||||
|
||||
async def handle(self, conn: WebSocketServerProtocol) -> None:
|
||||
self.connections.add(conn)
|
||||
async def handle(self, conn: ServerConnection) -> None:
|
||||
await conn.send(self.last_status)
|
||||
try:
|
||||
await conn.wait_closed()
|
||||
finally:
|
||||
self.connections.remove(conn)
|
||||
await conn.wait_closed()
|
||||
|
||||
async def update(self, song: Song | None) -> None:
|
||||
if song is None:
|
||||
self.last_status = MSGPACK_NULL
|
||||
else:
|
||||
self.last_status = ormsgpack.packb(song, default=default)
|
||||
broadcast(self.connections, self.last_status)
|
||||
async def update(self, playback: Playback) -> None:
|
||||
self.last_status = ormsgpack.packb(playback, default=default)
|
||||
broadcast(self.server.connections, self.last_status)
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
from .artwork import Artwork, ArtworkSchema, to_artwork
|
||||
from .musicbrainz import to_brainz
|
||||
from .song import PlaybackState, Song
|
||||
from .song import Song
|
||||
from .stopped import Stopped
|
||||
|
||||
__all__ = (
|
||||
"Artwork",
|
||||
"ArtworkSchema",
|
||||
"to_artwork",
|
||||
"to_brainz",
|
||||
"PlaybackState",
|
||||
"Song",
|
||||
"Stopped",
|
||||
)
|
||||
|
|
|
@ -1,35 +1,32 @@
|
|||
from dataclasses import dataclass
|
||||
from enum import StrEnum
|
||||
from pathlib import Path
|
||||
from typing import Literal
|
||||
|
||||
from ..playback.state import PlaybackState
|
||||
from ..tools.schema.define import schema
|
||||
from ..tools.schema.fields import Url
|
||||
from .artwork import Artwork
|
||||
from .musicbrainz import MusicBrainzIds
|
||||
|
||||
|
||||
class PlaybackState(StrEnum):
|
||||
play = "play"
|
||||
pause = "pause"
|
||||
stop = "stop"
|
||||
|
||||
|
||||
@schema("https://cdn.00dani.me/m/schemata/mpd-now-playable/song-v1.json")
|
||||
@dataclass(slots=True)
|
||||
@dataclass(slots=True, kw_only=True)
|
||||
class Song:
|
||||
#: Whether MPD is currently playing, paused, or stopped. Pretty simple.
|
||||
state: PlaybackState
|
||||
|
||||
#: The zero-based index of the current song in MPD's queue.
|
||||
queue_index: int
|
||||
#: The total length of MPD's queue - the last song in the queue will have
|
||||
#: the index one less than this, since queue indices are zero-based.
|
||||
queue_length: int
|
||||
#: Whether MPD is currently playing or paused. Pretty simple.
|
||||
state: Literal[PlaybackState.play, PlaybackState.pause]
|
||||
|
||||
#: The relative path to the current song inside the music directory. MPD
|
||||
#: itself uses this path as a stable identifier for the audio file in many
|
||||
#: places, so you can safely do the same.
|
||||
file: Path
|
||||
|
||||
#: An absolute URL referring to the current song, if available. If the
|
||||
#: song's a local file and its absolute path can be determined
|
||||
#: (mpd-now-playable has been configured with your music directory), then
|
||||
#: this field will contain a file:// URL. If the song's remote, then MPD
|
||||
#: itself returns an absolute URL in the first place.
|
||||
url: Url | None = None
|
||||
|
||||
#: The song's title, if it's been tagged with one. Currently only one title
|
||||
#: is supported, since it doesn't make a lot of sense to tag a single audio
|
||||
#: file with multiple titles.
|
||||
|
@ -61,8 +58,9 @@ class Song:
|
|||
genre: list[str]
|
||||
|
||||
#: The song's duration as read from its tags, measured in seconds.
|
||||
#: Fractional seconds are allowed.
|
||||
duration: float
|
||||
#: Fractional seconds are allowed. The duration may be unavailable for some
|
||||
#: sources, such as internet radio streams.
|
||||
duration: float | None
|
||||
|
||||
#: How far into the song MPD is, measured in seconds. Fractional seconds
|
||||
#: are allowed. This is usually going to be less than or equal to the
|
||||
|
|
9
src/mpd_now_playable/song/stopped.py
Normal file
9
src/mpd_now_playable/song/stopped.py
Normal file
|
@ -0,0 +1,9 @@
|
|||
from dataclasses import dataclass, field
|
||||
from typing import Literal
|
||||
|
||||
from ..playback.state import PlaybackState
|
||||
|
||||
|
||||
@dataclass(slots=True, kw_only=True)
|
||||
class Stopped:
|
||||
state: Literal[PlaybackState.stop] = field(default=PlaybackState.stop, repr=False)
|
|
@ -1,17 +1,15 @@
|
|||
from asyncio import AbstractEventLoop, new_event_loop
|
||||
from dataclasses import dataclass
|
||||
from importlib import import_module
|
||||
from typing import Generic, Iterable, Literal, Protocol, TypeVar, cast
|
||||
from typing import Iterable, Literal, Protocol, cast
|
||||
|
||||
from .config.model import BaseReceiverConfig
|
||||
from .playback import Playback
|
||||
from .player import Player
|
||||
from .song import Song
|
||||
from .tools.types import not_none
|
||||
|
||||
T = TypeVar("T", bound=AbstractEventLoop, covariant=True)
|
||||
|
||||
|
||||
class LoopFactory(Generic[T], Protocol):
|
||||
class LoopFactory[T: AbstractEventLoop](Protocol):
|
||||
@property
|
||||
def is_replaceable(self) -> bool: ...
|
||||
|
||||
|
@ -25,7 +23,7 @@ class Receiver(Protocol):
|
|||
def loop_factory(cls) -> LoopFactory[AbstractEventLoop]: ...
|
||||
|
||||
async def start(self, player: Player) -> None: ...
|
||||
async def update(self, song: Song | None) -> None: ...
|
||||
async def update(self, playback: Playback) -> None: ...
|
||||
|
||||
|
||||
class ReceiverModule(Protocol):
|
||||
|
|
|
@ -1,10 +1,8 @@
|
|||
from typing import Callable, Protocol, Self, TypeVar
|
||||
from typing import Callable, Protocol, Self
|
||||
|
||||
from pydantic.type_adapter import TypeAdapter
|
||||
from yarl import URL
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
class ModelWithSchema(Protocol):
|
||||
@property
|
||||
|
@ -13,7 +11,7 @@ class ModelWithSchema(Protocol):
|
|||
def schema(self) -> TypeAdapter[Self]: ...
|
||||
|
||||
|
||||
def schema(schema_id: str) -> Callable[[type[T]], type[T]]:
|
||||
def schema[T](schema_id: str) -> Callable[[type[T]], type[T]]:
|
||||
def decorate(clazz: type[T]) -> type[T]:
|
||||
type.__setattr__(clazz, "id", URL(schema_id))
|
||||
type.__setattr__(clazz, "schema", TypeAdapter(clazz))
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
from os.path import expanduser
|
||||
from typing import Annotated, NewType
|
||||
|
||||
from annotated_types import Ge, Le
|
||||
from pydantic import (
|
||||
BeforeValidator,
|
||||
Field,
|
||||
PlainSerializer,
|
||||
PlainValidator,
|
||||
|
@ -9,9 +11,12 @@ from pydantic import (
|
|||
Strict,
|
||||
WithJsonSchema,
|
||||
)
|
||||
from pydantic import (
|
||||
DirectoryPath as DirectoryType,
|
||||
)
|
||||
from yarl import URL as Yarl
|
||||
|
||||
__all__ = ("Host", "Password", "Port", "Url")
|
||||
__all__ = ("DirectoryPath", "Host", "Password", "Port", "Url")
|
||||
|
||||
|
||||
def from_yarl(url: Yarl) -> str:
|
||||
|
@ -24,6 +29,7 @@ def to_yarl(value: object) -> Yarl:
|
|||
raise NotImplementedError(f"Cannot convert {type(object)} to URL")
|
||||
|
||||
|
||||
DirectoryPath = Annotated[DirectoryType, BeforeValidator(expanduser)]
|
||||
Host = NewType(
|
||||
"Host", Annotated[str, Strict(), Field(json_schema_extra={"format": "hostname"})]
|
||||
)
|
|
@ -11,8 +11,8 @@ from .define import ModelWithSchema
|
|||
__all__ = ("write",)
|
||||
|
||||
|
||||
def write(model: ModelWithSchema) -> None:
|
||||
schema = model.schema.json_schema(schema_generator=MyGenerateJsonSchema)
|
||||
def write(model: ModelWithSchema, mode: JsonSchemaMode = "validation") -> None:
|
||||
schema = model.schema.json_schema(schema_generator=MyGenerateJsonSchema, mode=mode)
|
||||
schema["$id"] = model.id.human_repr()
|
||||
schema_file = Path(__file__).parents[4] / "schemata" / model.id.name
|
||||
print(f"Writing this schema to {schema_file}")
|
||||
|
@ -46,8 +46,12 @@ class MyGenerateJsonSchema(GenerateJsonSchema):
|
|||
def nullable_schema(self, schema: s.NullableSchema) -> JsonSchemaValue:
|
||||
return self.generate_inner(schema["schema"])
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
if __name__ == "__main__":
|
||||
from ...config.model import Config
|
||||
from ...playback import Playback
|
||||
from ...song import Song
|
||||
|
||||
write(Config)
|
||||
write(Song)
|
||||
write(Playback, mode="serialization")
|
||||
write(Song, mode="serialization")
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
from collections.abc import Callable
|
||||
from typing import Any, TypeAlias, TypeVar
|
||||
from typing import Any
|
||||
|
||||
__all__ = (
|
||||
"AnyExceptList",
|
||||
|
@ -28,27 +28,22 @@ AnyExceptList = (
|
|||
)
|
||||
|
||||
|
||||
U = TypeVar("U")
|
||||
V = TypeVar("V")
|
||||
|
||||
|
||||
def not_none(value: U | None) -> U:
|
||||
def not_none[U](value: U | None) -> U:
|
||||
if value is None:
|
||||
raise ValueError("None should not be possible here.")
|
||||
return value
|
||||
|
||||
|
||||
def option_fmap(f: Callable[[U], V], value: U | None) -> V | None:
|
||||
def option_fmap[U, V](f: Callable[[U], V], value: U | None) -> V | None:
|
||||
if value is None:
|
||||
return None
|
||||
return f(value)
|
||||
|
||||
|
||||
T = TypeVar("T", bound=AnyExceptList)
|
||||
MaybePlural: TypeAlias = list[T] | T
|
||||
type MaybePlural[T: AnyExceptList] = list[T] | T
|
||||
|
||||
|
||||
def un_maybe_plural(value: MaybePlural[T] | None) -> list[T]:
|
||||
def un_maybe_plural[T: AnyExceptList](value: MaybePlural[T] | None) -> list[T]:
|
||||
match value:
|
||||
case None:
|
||||
return []
|
||||
|
|
|
@ -2,32 +2,42 @@ from typing import Final, Literal
|
|||
|
||||
from Foundation import CGSize
|
||||
|
||||
NSApplicationActivationPolicyRegular: Final = 0
|
||||
NSApplicationActivationPolicyAccessory: Final = 1
|
||||
NSApplicationActivationPolicyProhibited: Final = 2
|
||||
NSApplicationActivationPolicy = Literal[0, 1, 2]
|
||||
|
||||
class NSApplication:
|
||||
@staticmethod
|
||||
def sharedApplication() -> NSApplication: ...
|
||||
def setActivationPolicy_(self, policy: NSApplicationActivationPolicy) -> bool: ...
|
||||
|
||||
# There are many other operations available but we only actually use copy, so we don't need all of them here.
|
||||
NSCompositingOperationClear: Final = 0
|
||||
NSCompositingOperationCopy: Final = 1
|
||||
NSCompositingOperation = Literal[0, 1]
|
||||
|
||||
class NSRect:
|
||||
pass
|
||||
pass
|
||||
|
||||
def NSMakeRect(x: float, y: float, w: float, h: float) -> NSRect: ...
|
||||
|
||||
class NSImage:
|
||||
@staticmethod
|
||||
def alloc() -> type[NSImage]: ...
|
||||
|
||||
@staticmethod
|
||||
def initByReferencingFile_(file: str) -> NSImage: ...
|
||||
|
||||
@staticmethod
|
||||
def initWithData_(data: bytes) -> NSImage: ...
|
||||
|
||||
@staticmethod
|
||||
def initWithSize_(size: CGSize) -> NSImage: ...
|
||||
|
||||
def size(self) -> CGSize: ...
|
||||
|
||||
def lockFocus(self) -> None: ...
|
||||
def unlockFocus(self) -> None: ...
|
||||
|
||||
def drawInRect_fromRect_operation_fraction_(self, inRect: NSRect, fromRect: NSRect, operation: NSCompositingOperation, fraction: float) -> None: ...
|
||||
@staticmethod
|
||||
def alloc() -> type[NSImage]: ...
|
||||
@staticmethod
|
||||
def initByReferencingFile_(file: str) -> NSImage: ...
|
||||
@staticmethod
|
||||
def initWithData_(data: bytes) -> NSImage: ...
|
||||
@staticmethod
|
||||
def initWithSize_(size: CGSize) -> NSImage: ...
|
||||
def size(self) -> CGSize: ...
|
||||
def lockFocus(self) -> None: ...
|
||||
def unlockFocus(self) -> None: ...
|
||||
def drawInRect_fromRect_operation_fraction_(
|
||||
self,
|
||||
inRect: NSRect,
|
||||
fromRect: NSRect,
|
||||
operation: NSCompositingOperation,
|
||||
fraction: float,
|
||||
) -> None: ...
|
||||
|
|
|
@ -34,6 +34,7 @@ MPNowPlayingInfoPropertyPlaybackQueueIndex: Final = (
|
|||
MPNowPlayingInfoPropertyElapsedPlaybackTime: Final = (
|
||||
"MPNowPlayingInfoPropertyElapsedPlaybackTime"
|
||||
)
|
||||
MPNowPlayingInfoPropertyAssetURL: Final = "MPNowPlayingInfoPropertyAssetURL"
|
||||
MPNowPlayingInfoPropertyExternalContentIdentifier: Final = (
|
||||
"MPNowPlayingInfoPropertyExternalContentIdentifier"
|
||||
)
|
||||
|
|
|
@ -1,7 +1,3 @@
|
|||
from typing import Generic, TypeVar
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
class BaseCache(Generic[T]):
|
||||
class BaseCache[T]:
|
||||
@staticmethod
|
||||
def parse_uri_path(path: str) -> dict[str, str]: ...
|
||||
|
|
|
@ -1,11 +1,9 @@
|
|||
from typing import ClassVar, Optional, TypeVar
|
||||
from typing import ClassVar, Optional
|
||||
|
||||
from .base import BaseCache
|
||||
from .serializers import BaseSerializer
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
class Cache(BaseCache[T]):
|
||||
class Cache[T](BaseCache[T]):
|
||||
MEMORY: ClassVar[type[BaseCache]]
|
||||
REDIS: ClassVar[type[BaseCache] | None]
|
||||
MEMCACHED: ClassVar[type[BaseCache] | None]
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
from collections.abc import Callable, Mapping
|
||||
from typing import TypeAlias, TypeVar
|
||||
from typing import TypeVar
|
||||
|
||||
# Apparently you need Python 3.13 for type var defaults to work. But since this
|
||||
# is just a stub file, it's okay if they aren't supported at runtime.
|
||||
|
@ -8,7 +8,7 @@ KOut = TypeVar("KOut", default=KIn)
|
|||
VIn = TypeVar("VIn")
|
||||
VOut = TypeVar("VOut", default=VIn)
|
||||
|
||||
Path: TypeAlias = tuple[KIn, ...]
|
||||
type Path[KIn] = tuple[KIn, ...]
|
||||
|
||||
# remap() is Complicated and really difficult to define a type for, so I'm not
|
||||
# surprised the boltons package doesn't try to type it for you. This particular
|
||||
|
|
|
@ -9,6 +9,7 @@ class MPDClient(MPDClientBase):
|
|||
|
||||
def __init__(self) -> None: ...
|
||||
async def connect(self, host: str, port: int = ...) -> None: ...
|
||||
async def ping(self) -> None: ...
|
||||
async def password(self, password: str) -> None: ...
|
||||
def idle(self, subsystems: Sequence[str] = ...) -> AsyncIterator[Sequence[str]]: ...
|
||||
async def status(self) -> types.StatusResponse: ...
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue