Merge branch 'main' into develop
This commit is contained in:
@ -16,7 +16,7 @@ steps:
|
|||||||
- pip install poetry
|
- pip install poetry
|
||||||
- poetry install --with test
|
- poetry install --with test
|
||||||
# Start with a fresh database (which is already running as a service from Drone)
|
# Start with a fresh database (which is already running as a service from Drone)
|
||||||
- poetry run pytest --cov-report term:skip-covered --cov=vrobbler tests
|
- poetry run pytest -n 5 --cov-report term:skip-covered --cov=vrobbler tests
|
||||||
environment:
|
environment:
|
||||||
VROBBLER_DATABASE_URL: sqlite:///test.db
|
VROBBLER_DATABASE_URL: sqlite:///test.db
|
||||||
volumes:
|
volumes:
|
||||||
@ -30,7 +30,7 @@ steps:
|
|||||||
- vrobbler.service
|
- vrobbler.service
|
||||||
username: root
|
username: root
|
||||||
ssh_key:
|
ssh_key:
|
||||||
from_secret: jail_key
|
from_secret: unbl_ink_key
|
||||||
command_timeout: 2m
|
command_timeout: 2m
|
||||||
script:
|
script:
|
||||||
- pip uninstall -y vrobbler
|
- pip uninstall -y vrobbler
|
||||||
|
|||||||
2
Makefile
2
Makefile
@ -2,3 +2,5 @@ deploy:
|
|||||||
ssh vrobbler.service "rm -rf /usr/local/lib/python3.11/site-packages/vrobbler-0.15.4.dist-info/ && pip install git+https://code.unbl.ink/secstate/vrobbler.git@develop && immortalctl restart vrobbler && immortalctl restart vrobbler-celery && vrobbler migrate"
|
ssh vrobbler.service "rm -rf /usr/local/lib/python3.11/site-packages/vrobbler-0.15.4.dist-info/ && pip install git+https://code.unbl.ink/secstate/vrobbler.git@develop && immortalctl restart vrobbler && immortalctl restart vrobbler-celery && vrobbler migrate"
|
||||||
logs:
|
logs:
|
||||||
ssh life.unbl.ink tail -n 100 -f /var/log/vrobbler.json
|
ssh life.unbl.ink tail -n 100 -f /var/log/vrobbler.json
|
||||||
|
test:
|
||||||
|
pytest vrobbler
|
||||||
|
|||||||
354
poetry.lock
generated
354
poetry.lock
generated
@ -1,4 +1,4 @@
|
|||||||
# This file is automatically @generated by Poetry 2.1.1 and should not be changed by hand.
|
# This file is automatically @generated by Poetry 2.1.2 and should not be changed by hand.
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aiohappyeyeballs"
|
name = "aiohappyeyeballs"
|
||||||
@ -1345,6 +1345,21 @@ files = [
|
|||||||
[package.extras]
|
[package.extras]
|
||||||
test = ["pytest (>=6)"]
|
test = ["pytest (>=6)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "execnet"
|
||||||
|
version = "2.1.1"
|
||||||
|
description = "execnet: rapid multi-Python deployment"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.8"
|
||||||
|
groups = ["test"]
|
||||||
|
files = [
|
||||||
|
{file = "execnet-2.1.1-py3-none-any.whl", hash = "sha256:26dee51f1b80cebd6d0ca8e74dd8745419761d3bef34163928cbebbdc4749fdc"},
|
||||||
|
{file = "execnet-2.1.1.tar.gz", hash = "sha256:5189b52c6121c24feae288166ab41b32549c7e2348652736540b9e6e7d4e72e3"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
testing = ["hatch", "pre-commit", "pytest", "tox"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "executing"
|
name = "executing"
|
||||||
version = "2.2.0"
|
version = "2.2.0"
|
||||||
@ -1501,7 +1516,7 @@ description = "Lightweight in-process concurrent programming"
|
|||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7"
|
python-versions = ">=3.7"
|
||||||
groups = ["main"]
|
groups = ["main"]
|
||||||
markers = "python_version < \"3.14\" and (platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\")"
|
markers = "platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\""
|
||||||
files = [
|
files = [
|
||||||
{file = "greenlet-3.1.1-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:0bbae94a29c9e5c7e4a2b7f0aae5c17e8e90acbfd3bf6270eeba60c39fce3563"},
|
{file = "greenlet-3.1.1-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:0bbae94a29c9e5c7e4a2b7f0aae5c17e8e90acbfd3bf6270eeba60c39fce3563"},
|
||||||
{file = "greenlet-3.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fde093fb93f35ca72a556cf72c92ea3ebfda3d79fc35bb19fbe685853869a83"},
|
{file = "greenlet-3.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fde093fb93f35ca72a556cf72c92ea3ebfda3d79fc35bb19fbe685853869a83"},
|
||||||
@ -1746,7 +1761,7 @@ description = "Read metadata from Python packages"
|
|||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.9"
|
python-versions = ">=3.9"
|
||||||
groups = ["main"]
|
groups = ["main"]
|
||||||
markers = "python_version < \"3.10\""
|
markers = "python_version == \"3.9\""
|
||||||
files = [
|
files = [
|
||||||
{file = "importlib_metadata-8.6.1-py3-none-any.whl", hash = "sha256:02a89390c1e15fdfdc0d7c6b25cb3e62650d0494005c97d6f148bf5b9787525e"},
|
{file = "importlib_metadata-8.6.1-py3-none-any.whl", hash = "sha256:02a89390c1e15fdfdc0d7c6b25cb3e62650d0494005c97d6f148bf5b9787525e"},
|
||||||
{file = "importlib_metadata-8.6.1.tar.gz", hash = "sha256:310b41d755445d74569f993ccfc22838295d9fe005425094fad953d7f15c8580"},
|
{file = "importlib_metadata-8.6.1.tar.gz", hash = "sha256:310b41d755445d74569f993ccfc22838295d9fe005425094fad953d7f15c8580"},
|
||||||
@ -1771,7 +1786,7 @@ description = "Read resources from Python packages"
|
|||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.9"
|
python-versions = ">=3.9"
|
||||||
groups = ["main"]
|
groups = ["main"]
|
||||||
markers = "python_version < \"3.10\""
|
markers = "python_version == \"3.9\""
|
||||||
files = [
|
files = [
|
||||||
{file = "importlib_resources-6.5.2-py3-none-any.whl", hash = "sha256:789cfdc3ed28c78b67a06acb8126751ced69a3d5f79c095a98298cd8a760ccec"},
|
{file = "importlib_resources-6.5.2-py3-none-any.whl", hash = "sha256:789cfdc3ed28c78b67a06acb8126751ced69a3d5f79c095a98298cd8a760ccec"},
|
||||||
{file = "importlib_resources-6.5.2.tar.gz", hash = "sha256:185f87adef5bcc288449d98fb4fba07cea78bc036455dd44c5fc4a2fe78fed2c"},
|
{file = "importlib_resources-6.5.2.tar.gz", hash = "sha256:185f87adef5bcc288449d98fb4fba07cea78bc036455dd44c5fc4a2fe78fed2c"},
|
||||||
@ -2499,103 +2514,38 @@ setuptools = "*"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pendulum"
|
name = "pendulum"
|
||||||
version = "3.0.0"
|
version = "2.1.2"
|
||||||
description = "Python datetimes made easy"
|
description = "Python datetimes made easy"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
||||||
groups = ["main"]
|
groups = ["main"]
|
||||||
files = [
|
files = [
|
||||||
{file = "pendulum-3.0.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2cf9e53ef11668e07f73190c805dbdf07a1939c3298b78d5a9203a86775d1bfd"},
|
{file = "pendulum-2.1.2-cp27-cp27m-macosx_10_15_x86_64.whl", hash = "sha256:b6c352f4bd32dff1ea7066bd31ad0f71f8d8100b9ff709fb343f3b86cee43efe"},
|
||||||
{file = "pendulum-3.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fb551b9b5e6059377889d2d878d940fd0bbb80ae4810543db18e6f77b02c5ef6"},
|
{file = "pendulum-2.1.2-cp27-cp27m-win_amd64.whl", hash = "sha256:318f72f62e8e23cd6660dbafe1e346950281a9aed144b5c596b2ddabc1d19739"},
|
||||||
{file = "pendulum-3.0.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c58227ac260d5b01fc1025176d7b31858c9f62595737f350d22124a9a3ad82d"},
|
{file = "pendulum-2.1.2-cp35-cp35m-macosx_10_15_x86_64.whl", hash = "sha256:0731f0c661a3cb779d398803655494893c9f581f6488048b3fb629c2342b5394"},
|
||||||
{file = "pendulum-3.0.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:60fb6f415fea93a11c52578eaa10594568a6716602be8430b167eb0d730f3332"},
|
{file = "pendulum-2.1.2-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:3481fad1dc3f6f6738bd575a951d3c15d4b4ce7c82dce37cf8ac1483fde6e8b0"},
|
||||||
{file = "pendulum-3.0.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b69f6b4dbcb86f2c2fe696ba991e67347bcf87fe601362a1aba6431454b46bde"},
|
{file = "pendulum-2.1.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:9702069c694306297ed362ce7e3c1ef8404ac8ede39f9b28b7c1a7ad8c3959e3"},
|
||||||
{file = "pendulum-3.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:138afa9c373ee450ede206db5a5e9004fd3011b3c6bbe1e57015395cd076a09f"},
|
{file = "pendulum-2.1.2-cp35-cp35m-win_amd64.whl", hash = "sha256:fb53ffa0085002ddd43b6ca61a7b34f2d4d7c3ed66f931fe599e1a531b42af9b"},
|
||||||
{file = "pendulum-3.0.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:83d9031f39c6da9677164241fd0d37fbfc9dc8ade7043b5d6d62f56e81af8ad2"},
|
{file = "pendulum-2.1.2-cp36-cp36m-macosx_10_15_x86_64.whl", hash = "sha256:c501749fdd3d6f9e726086bf0cd4437281ed47e7bca132ddb522f86a1645d360"},
|
||||||
{file = "pendulum-3.0.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0c2308af4033fa534f089595bcd40a95a39988ce4059ccd3dc6acb9ef14ca44a"},
|
{file = "pendulum-2.1.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:c807a578a532eeb226150d5006f156632df2cc8c5693d778324b43ff8c515dd0"},
|
||||||
{file = "pendulum-3.0.0-cp310-none-win_amd64.whl", hash = "sha256:9a59637cdb8462bdf2dbcb9d389518c0263799189d773ad5c11db6b13064fa79"},
|
{file = "pendulum-2.1.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:2d1619a721df661e506eff8db8614016f0720ac171fe80dda1333ee44e684087"},
|
||||||
{file = "pendulum-3.0.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:3725245c0352c95d6ca297193192020d1b0c0f83d5ee6bb09964edc2b5a2d508"},
|
{file = "pendulum-2.1.2-cp36-cp36m-win_amd64.whl", hash = "sha256:f888f2d2909a414680a29ae74d0592758f2b9fcdee3549887779cd4055e975db"},
|
||||||
{file = "pendulum-3.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6c035f03a3e565ed132927e2c1b691de0dbf4eb53b02a5a3c5a97e1a64e17bec"},
|
{file = "pendulum-2.1.2-cp37-cp37m-macosx_10_15_x86_64.whl", hash = "sha256:e95d329384717c7bf627bf27e204bc3b15c8238fa8d9d9781d93712776c14002"},
|
||||||
{file = "pendulum-3.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:597e66e63cbd68dd6d58ac46cb7a92363d2088d37ccde2dae4332ef23e95cd00"},
|
{file = "pendulum-2.1.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:4c9c689747f39d0d02a9f94fcee737b34a5773803a64a5fdb046ee9cac7442c5"},
|
||||||
{file = "pendulum-3.0.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:99a0f8172e19f3f0c0e4ace0ad1595134d5243cf75985dc2233e8f9e8de263ca"},
|
{file = "pendulum-2.1.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:1245cd0075a3c6d889f581f6325dd8404aca5884dea7223a5566c38aab94642b"},
|
||||||
{file = "pendulum-3.0.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:77d8839e20f54706aed425bec82a83b4aec74db07f26acd039905d1237a5e1d4"},
|
{file = "pendulum-2.1.2-cp37-cp37m-win_amd64.whl", hash = "sha256:db0a40d8bcd27b4fb46676e8eb3c732c67a5a5e6bfab8927028224fbced0b40b"},
|
||||||
{file = "pendulum-3.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afde30e8146292b059020fbc8b6f8fd4a60ae7c5e6f0afef937bbb24880bdf01"},
|
{file = "pendulum-2.1.2-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:f5e236e7730cab1644e1b87aca3d2ff3e375a608542e90fe25685dae46310116"},
|
||||||
{file = "pendulum-3.0.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:660434a6fcf6303c4efd36713ca9212c753140107ee169a3fc6c49c4711c2a05"},
|
{file = "pendulum-2.1.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:de42ea3e2943171a9e95141f2eecf972480636e8e484ccffaf1e833929e9e052"},
|
||||||
{file = "pendulum-3.0.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:dee9e5a48c6999dc1106eb7eea3e3a50e98a50651b72c08a87ee2154e544b33e"},
|
{file = "pendulum-2.1.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7c5ec650cb4bec4c63a89a0242cc8c3cebcec92fcfe937c417ba18277d8560be"},
|
||||||
{file = "pendulum-3.0.0-cp311-none-win_amd64.whl", hash = "sha256:d4cdecde90aec2d67cebe4042fd2a87a4441cc02152ed7ed8fb3ebb110b94ec4"},
|
{file = "pendulum-2.1.2-cp38-cp38-win_amd64.whl", hash = "sha256:33fb61601083f3eb1d15edeb45274f73c63b3c44a8524703dc143f4212bf3269"},
|
||||||
{file = "pendulum-3.0.0-cp311-none-win_arm64.whl", hash = "sha256:773c3bc4ddda2dda9f1b9d51fe06762f9200f3293d75c4660c19b2614b991d83"},
|
{file = "pendulum-2.1.2-cp39-cp39-manylinux1_i686.whl", hash = "sha256:29c40a6f2942376185728c9a0347d7c0f07905638c83007e1d262781f1e6953a"},
|
||||||
{file = "pendulum-3.0.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:409e64e41418c49f973d43a28afe5df1df4f1dd87c41c7c90f1a63f61ae0f1f7"},
|
{file = "pendulum-2.1.2-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:94b1fc947bfe38579b28e1cccb36f7e28a15e841f30384b5ad6c5e31055c85d7"},
|
||||||
{file = "pendulum-3.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a38ad2121c5ec7c4c190c7334e789c3b4624798859156b138fcc4d92295835dc"},
|
{file = "pendulum-2.1.2.tar.gz", hash = "sha256:b06a0ca1bfe41c990bbf0c029f0b6501a7f2ec4e38bfec730712015e8860f207"},
|
||||||
{file = "pendulum-3.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fde4d0b2024b9785f66b7f30ed59281bd60d63d9213cda0eb0910ead777f6d37"},
|
|
||||||
{file = "pendulum-3.0.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4b2c5675769fb6d4c11238132962939b960fcb365436b6d623c5864287faa319"},
|
|
||||||
{file = "pendulum-3.0.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8af95e03e066826f0f4c65811cbee1b3123d4a45a1c3a2b4fc23c4b0dff893b5"},
|
|
||||||
{file = "pendulum-3.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2165a8f33cb15e06c67070b8afc87a62b85c5a273e3aaa6bc9d15c93a4920d6f"},
|
|
||||||
{file = "pendulum-3.0.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ad5e65b874b5e56bd942546ea7ba9dd1d6a25121db1c517700f1c9de91b28518"},
|
|
||||||
{file = "pendulum-3.0.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:17fe4b2c844bbf5f0ece69cfd959fa02957c61317b2161763950d88fed8e13b9"},
|
|
||||||
{file = "pendulum-3.0.0-cp312-none-win_amd64.whl", hash = "sha256:78f8f4e7efe5066aca24a7a57511b9c2119f5c2b5eb81c46ff9222ce11e0a7a5"},
|
|
||||||
{file = "pendulum-3.0.0-cp312-none-win_arm64.whl", hash = "sha256:28f49d8d1e32aae9c284a90b6bb3873eee15ec6e1d9042edd611b22a94ac462f"},
|
|
||||||
{file = "pendulum-3.0.0-cp37-cp37m-macosx_10_12_x86_64.whl", hash = "sha256:d4e2512f4e1a4670284a153b214db9719eb5d14ac55ada5b76cbdb8c5c00399d"},
|
|
||||||
{file = "pendulum-3.0.0-cp37-cp37m-macosx_11_0_arm64.whl", hash = "sha256:3d897eb50883cc58d9b92f6405245f84b9286cd2de6e8694cb9ea5cb15195a32"},
|
|
||||||
{file = "pendulum-3.0.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e169cc2ca419517f397811bbe4589cf3cd13fca6dc38bb352ba15ea90739ebb"},
|
|
||||||
{file = "pendulum-3.0.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f17c3084a4524ebefd9255513692f7e7360e23c8853dc6f10c64cc184e1217ab"},
|
|
||||||
{file = "pendulum-3.0.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:826d6e258052715f64d05ae0fc9040c0151e6a87aae7c109ba9a0ed930ce4000"},
|
|
||||||
{file = "pendulum-3.0.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2aae97087872ef152a0c40e06100b3665d8cb86b59bc8471ca7c26132fccd0f"},
|
|
||||||
{file = "pendulum-3.0.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:ac65eeec2250d03106b5e81284ad47f0d417ca299a45e89ccc69e36130ca8bc7"},
|
|
||||||
{file = "pendulum-3.0.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:a5346d08f3f4a6e9e672187faa179c7bf9227897081d7121866358af369f44f9"},
|
|
||||||
{file = "pendulum-3.0.0-cp37-none-win_amd64.whl", hash = "sha256:235d64e87946d8f95c796af34818c76e0f88c94d624c268693c85b723b698aa9"},
|
|
||||||
{file = "pendulum-3.0.0-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:6a881d9c2a7f85bc9adafcfe671df5207f51f5715ae61f5d838b77a1356e8b7b"},
|
|
||||||
{file = "pendulum-3.0.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:d7762d2076b9b1cb718a6631ad6c16c23fc3fac76cbb8c454e81e80be98daa34"},
|
|
||||||
{file = "pendulum-3.0.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e8e36a8130819d97a479a0e7bf379b66b3b1b520e5dc46bd7eb14634338df8c"},
|
|
||||||
{file = "pendulum-3.0.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7dc843253ac373358ffc0711960e2dd5b94ab67530a3e204d85c6e8cb2c5fa10"},
|
|
||||||
{file = "pendulum-3.0.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0a78ad3635d609ceb1e97d6aedef6a6a6f93433ddb2312888e668365908c7120"},
|
|
||||||
{file = "pendulum-3.0.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b30a137e9e0d1f751e60e67d11fc67781a572db76b2296f7b4d44554761049d6"},
|
|
||||||
{file = "pendulum-3.0.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:c95984037987f4a457bb760455d9ca80467be792236b69d0084f228a8ada0162"},
|
|
||||||
{file = "pendulum-3.0.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d29c6e578fe0f893766c0d286adbf0b3c726a4e2341eba0917ec79c50274ec16"},
|
|
||||||
{file = "pendulum-3.0.0-cp38-none-win_amd64.whl", hash = "sha256:deaba8e16dbfcb3d7a6b5fabdd5a38b7c982809567479987b9c89572df62e027"},
|
|
||||||
{file = "pendulum-3.0.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:b11aceea5b20b4b5382962b321dbc354af0defe35daa84e9ff3aae3c230df694"},
|
|
||||||
{file = "pendulum-3.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a90d4d504e82ad236afac9adca4d6a19e4865f717034fc69bafb112c320dcc8f"},
|
|
||||||
{file = "pendulum-3.0.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:825799c6b66e3734227756fa746cc34b3549c48693325b8b9f823cb7d21b19ac"},
|
|
||||||
{file = "pendulum-3.0.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad769e98dc07972e24afe0cff8d365cb6f0ebc7e65620aa1976fcfbcadc4c6f3"},
|
|
||||||
{file = "pendulum-3.0.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a6fc26907eb5fb8cc6188cc620bc2075a6c534d981a2f045daa5f79dfe50d512"},
|
|
||||||
{file = "pendulum-3.0.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c717eab1b6d898c00a3e0fa7781d615b5c5136bbd40abe82be100bb06df7a56"},
|
|
||||||
{file = "pendulum-3.0.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:3ddd1d66d1a714ce43acfe337190be055cdc221d911fc886d5a3aae28e14b76d"},
|
|
||||||
{file = "pendulum-3.0.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:822172853d7a9cf6da95d7b66a16c7160cb99ae6df55d44373888181d7a06edc"},
|
|
||||||
{file = "pendulum-3.0.0-cp39-none-win_amd64.whl", hash = "sha256:840de1b49cf1ec54c225a2a6f4f0784d50bd47f68e41dc005b7f67c7d5b5f3ae"},
|
|
||||||
{file = "pendulum-3.0.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3b1f74d1e6ffe5d01d6023870e2ce5c2191486928823196f8575dcc786e107b1"},
|
|
||||||
{file = "pendulum-3.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:729e9f93756a2cdfa77d0fc82068346e9731c7e884097160603872686e570f07"},
|
|
||||||
{file = "pendulum-3.0.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e586acc0b450cd21cbf0db6bae386237011b75260a3adceddc4be15334689a9a"},
|
|
||||||
{file = "pendulum-3.0.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22e7944ffc1f0099a79ff468ee9630c73f8c7835cd76fdb57ef7320e6a409df4"},
|
|
||||||
{file = "pendulum-3.0.0-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:fa30af36bd8e50686846bdace37cf6707bdd044e5cb6e1109acbad3277232e04"},
|
|
||||||
{file = "pendulum-3.0.0-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:440215347b11914ae707981b9a57ab9c7b6983ab0babde07063c6ee75c0dc6e7"},
|
|
||||||
{file = "pendulum-3.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:314c4038dc5e6a52991570f50edb2f08c339debdf8cea68ac355b32c4174e820"},
|
|
||||||
{file = "pendulum-3.0.0-pp37-pypy37_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5acb1d386337415f74f4d1955c4ce8d0201978c162927d07df8eb0692b2d8533"},
|
|
||||||
{file = "pendulum-3.0.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a789e12fbdefaffb7b8ac67f9d8f22ba17a3050ceaaa635cd1cc4645773a4b1e"},
|
|
||||||
{file = "pendulum-3.0.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:860aa9b8a888e5913bd70d819306749e5eb488e6b99cd6c47beb701b22bdecf5"},
|
|
||||||
{file = "pendulum-3.0.0-pp37-pypy37_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:5ebc65ea033ef0281368217fbf59f5cb05b338ac4dd23d60959c7afcd79a60a0"},
|
|
||||||
{file = "pendulum-3.0.0-pp37-pypy37_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:d9fef18ab0386ef6a9ac7bad7e43ded42c83ff7ad412f950633854f90d59afa8"},
|
|
||||||
{file = "pendulum-3.0.0-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:1c134ba2f0571d0b68b83f6972e2307a55a5a849e7dac8505c715c531d2a8795"},
|
|
||||||
{file = "pendulum-3.0.0-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:385680812e7e18af200bb9b4a49777418c32422d05ad5a8eb85144c4a285907b"},
|
|
||||||
{file = "pendulum-3.0.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9eec91cd87c59fb32ec49eb722f375bd58f4be790cae11c1b70fac3ee4f00da0"},
|
|
||||||
{file = "pendulum-3.0.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4386bffeca23c4b69ad50a36211f75b35a4deb6210bdca112ac3043deb7e494a"},
|
|
||||||
{file = "pendulum-3.0.0-pp38-pypy38_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:dfbcf1661d7146d7698da4b86e7f04814221081e9fe154183e34f4c5f5fa3bf8"},
|
|
||||||
{file = "pendulum-3.0.0-pp38-pypy38_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:04a1094a5aa1daa34a6b57c865b25f691848c61583fb22722a4df5699f6bf74c"},
|
|
||||||
{file = "pendulum-3.0.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:5b0ec85b9045bd49dd3a3493a5e7ddfd31c36a2a60da387c419fa04abcaecb23"},
|
|
||||||
{file = "pendulum-3.0.0-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:0a15b90129765b705eb2039062a6daf4d22c4e28d1a54fa260892e8c3ae6e157"},
|
|
||||||
{file = "pendulum-3.0.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:bb8f6d7acd67a67d6fedd361ad2958ff0539445ef51cbe8cd288db4306503cd0"},
|
|
||||||
{file = "pendulum-3.0.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fd69b15374bef7e4b4440612915315cc42e8575fcda2a3d7586a0d88192d0c88"},
|
|
||||||
{file = "pendulum-3.0.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc00f8110db6898360c53c812872662e077eaf9c75515d53ecc65d886eec209a"},
|
|
||||||
{file = "pendulum-3.0.0-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:83a44e8b40655d0ba565a5c3d1365d27e3e6778ae2a05b69124db9e471255c4a"},
|
|
||||||
{file = "pendulum-3.0.0-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:1a3604e9fbc06b788041b2a8b78f75c243021e0f512447806a6d37ee5214905d"},
|
|
||||||
{file = "pendulum-3.0.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:92c307ae7accebd06cbae4729f0ba9fa724df5f7d91a0964b1b972a22baa482b"},
|
|
||||||
{file = "pendulum-3.0.0.tar.gz", hash = "sha256:5d034998dea404ec31fae27af6b22cff1708f830a1ed7353be4d1019bb9f584e"},
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
python-dateutil = ">=2.6"
|
python-dateutil = ">=2.6,<3.0"
|
||||||
tzdata = ">=2020.1"
|
pytzdata = ">=2020.1"
|
||||||
|
|
||||||
[package.extras]
|
|
||||||
test = ["time-machine (>=2.6.0) ; implementation_name != \"pypy\""]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pexpect"
|
name = "pexpect"
|
||||||
@ -2630,92 +2580,83 @@ Pillow = ">=7.0"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pillow"
|
name = "pillow"
|
||||||
version = "11.1.0"
|
version = "9.5.0"
|
||||||
description = "Python Imaging Library (Fork)"
|
description = "Python Imaging Library (Fork)"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.9"
|
python-versions = ">=3.7"
|
||||||
groups = ["main"]
|
groups = ["main"]
|
||||||
files = [
|
files = [
|
||||||
{file = "pillow-11.1.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:e1abe69aca89514737465752b4bcaf8016de61b3be1397a8fc260ba33321b3a8"},
|
{file = "Pillow-9.5.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:ace6ca218308447b9077c14ea4ef381ba0b67ee78d64046b3f19cf4e1139ad16"},
|
||||||
{file = "pillow-11.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c640e5a06869c75994624551f45e5506e4256562ead981cce820d5ab39ae2192"},
|
{file = "Pillow-9.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d3d403753c9d5adc04d4694d35cf0391f0f3d57c8e0030aac09d7678fa8030aa"},
|
||||||
{file = "pillow-11.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a07dba04c5e22824816b2615ad7a7484432d7f540e6fa86af60d2de57b0fcee2"},
|
{file = "Pillow-9.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ba1b81ee69573fe7124881762bb4cd2e4b6ed9dd28c9c60a632902fe8db8b38"},
|
||||||
{file = "pillow-11.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e267b0ed063341f3e60acd25c05200df4193e15a4a5807075cd71225a2386e26"},
|
{file = "Pillow-9.5.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fe7e1c262d3392afcf5071df9afa574544f28eac825284596ac6db56e6d11062"},
|
||||||
{file = "pillow-11.1.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:bd165131fd51697e22421d0e467997ad31621b74bfc0b75956608cb2906dda07"},
|
{file = "Pillow-9.5.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f36397bf3f7d7c6a3abdea815ecf6fd14e7fcd4418ab24bae01008d8d8ca15e"},
|
||||||
{file = "pillow-11.1.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:abc56501c3fd148d60659aae0af6ddc149660469082859fa7b066a298bde9482"},
|
{file = "Pillow-9.5.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:252a03f1bdddce077eff2354c3861bf437c892fb1832f75ce813ee94347aa9b5"},
|
||||||
{file = "pillow-11.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:54ce1c9a16a9561b6d6d8cb30089ab1e5eb66918cb47d457bd996ef34182922e"},
|
{file = "Pillow-9.5.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:85ec677246533e27770b0de5cf0f9d6e4ec0c212a1f89dfc941b64b21226009d"},
|
||||||
{file = "pillow-11.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:73ddde795ee9b06257dac5ad42fcb07f3b9b813f8c1f7f870f402f4dc54b5269"},
|
{file = "Pillow-9.5.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b416f03d37d27290cb93597335a2f85ed446731200705b22bb927405320de903"},
|
||||||
{file = "pillow-11.1.0-cp310-cp310-win32.whl", hash = "sha256:3a5fe20a7b66e8135d7fd617b13272626a28278d0e578c98720d9ba4b2439d49"},
|
{file = "Pillow-9.5.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1781a624c229cb35a2ac31cc4a77e28cafc8900733a864870c49bfeedacd106a"},
|
||||||
{file = "pillow-11.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:b6123aa4a59d75f06e9dd3dac5bf8bc9aa383121bb3dd9a7a612e05eabc9961a"},
|
{file = "Pillow-9.5.0-cp310-cp310-win32.whl", hash = "sha256:8507eda3cd0608a1f94f58c64817e83ec12fa93a9436938b191b80d9e4c0fc44"},
|
||||||
{file = "pillow-11.1.0-cp310-cp310-win_arm64.whl", hash = "sha256:a76da0a31da6fcae4210aa94fd779c65c75786bc9af06289cd1c184451ef7a65"},
|
{file = "Pillow-9.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:d3c6b54e304c60c4181da1c9dadf83e4a54fd266a99c70ba646a9baa626819eb"},
|
||||||
{file = "pillow-11.1.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:e06695e0326d05b06833b40b7ef477e475d0b1ba3a6d27da1bb48c23209bf457"},
|
{file = "Pillow-9.5.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:7ec6f6ce99dab90b52da21cf0dc519e21095e332ff3b399a357c187b1a5eee32"},
|
||||||
{file = "pillow-11.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:96f82000e12f23e4f29346e42702b6ed9a2f2fea34a740dd5ffffcc8c539eb35"},
|
{file = "Pillow-9.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:560737e70cb9c6255d6dcba3de6578a9e2ec4b573659943a5e7e4af13f298f5c"},
|
||||||
{file = "pillow-11.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3cd561ded2cf2bbae44d4605837221b987c216cff94f49dfeed63488bb228d2"},
|
{file = "Pillow-9.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:96e88745a55b88a7c64fa49bceff363a1a27d9a64e04019c2281049444a571e3"},
|
||||||
{file = "pillow-11.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f189805c8be5ca5add39e6f899e6ce2ed824e65fb45f3c28cb2841911da19070"},
|
{file = "Pillow-9.5.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d9c206c29b46cfd343ea7cdfe1232443072bbb270d6a46f59c259460db76779a"},
|
||||||
{file = "pillow-11.1.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:dd0052e9db3474df30433f83a71b9b23bd9e4ef1de13d92df21a52c0303b8ab6"},
|
{file = "Pillow-9.5.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cfcc2c53c06f2ccb8976fb5c71d448bdd0a07d26d8e07e321c103416444c7ad1"},
|
||||||
{file = "pillow-11.1.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:837060a8599b8f5d402e97197d4924f05a2e0d68756998345c829c33186217b1"},
|
{file = "Pillow-9.5.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:a0f9bb6c80e6efcde93ffc51256d5cfb2155ff8f78292f074f60f9e70b942d99"},
|
||||||
{file = "pillow-11.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:aa8dd43daa836b9a8128dbe7d923423e5ad86f50a7a14dc688194b7be5c0dea2"},
|
{file = "Pillow-9.5.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:8d935f924bbab8f0a9a28404422da8af4904e36d5c33fc6f677e4c4485515625"},
|
||||||
{file = "pillow-11.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0a2f91f8a8b367e7a57c6e91cd25af510168091fb89ec5146003e424e1558a96"},
|
{file = "Pillow-9.5.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:fed1e1cf6a42577953abbe8e6cf2fe2f566daebde7c34724ec8803c4c0cda579"},
|
||||||
{file = "pillow-11.1.0-cp311-cp311-win32.whl", hash = "sha256:c12fc111ef090845de2bb15009372175d76ac99969bdf31e2ce9b42e4b8cd88f"},
|
{file = "Pillow-9.5.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c1170d6b195555644f0616fd6ed929dfcf6333b8675fcca044ae5ab110ded296"},
|
||||||
{file = "pillow-11.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:fbd43429d0d7ed6533b25fc993861b8fd512c42d04514a0dd6337fb3ccf22761"},
|
{file = "Pillow-9.5.0-cp311-cp311-win32.whl", hash = "sha256:54f7102ad31a3de5666827526e248c3530b3a33539dbda27c6843d19d72644ec"},
|
||||||
{file = "pillow-11.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:f7955ecf5609dee9442cbface754f2c6e541d9e6eda87fad7f7a989b0bdb9d71"},
|
{file = "Pillow-9.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:cfa4561277f677ecf651e2b22dc43e8f5368b74a25a8f7d1d4a3a243e573f2d4"},
|
||||||
{file = "pillow-11.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2062ffb1d36544d42fcaa277b069c88b01bb7298f4efa06731a7fd6cc290b81a"},
|
{file = "Pillow-9.5.0-cp311-cp311-win_arm64.whl", hash = "sha256:965e4a05ef364e7b973dd17fc765f42233415974d773e82144c9bbaaaea5d089"},
|
||||||
{file = "pillow-11.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a85b653980faad27e88b141348707ceeef8a1186f75ecc600c395dcac19f385b"},
|
{file = "Pillow-9.5.0-cp312-cp312-win32.whl", hash = "sha256:22baf0c3cf0c7f26e82d6e1adf118027afb325e703922c8dfc1d5d0156bb2eeb"},
|
||||||
{file = "pillow-11.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9409c080586d1f683df3f184f20e36fb647f2e0bc3988094d4fd8c9f4eb1b3b3"},
|
{file = "Pillow-9.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:432b975c009cf649420615388561c0ce7cc31ce9b2e374db659ee4f7d57a1f8b"},
|
||||||
{file = "pillow-11.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7fdadc077553621911f27ce206ffcbec7d3f8d7b50e0da39f10997e8e2bb7f6a"},
|
{file = "Pillow-9.5.0-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:5d4ebf8e1db4441a55c509c4baa7a0587a0210f7cd25fcfe74dbbce7a4bd1906"},
|
||||||
{file = "pillow-11.1.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:93a18841d09bcdd774dcdc308e4537e1f867b3dec059c131fde0327899734aa1"},
|
{file = "Pillow-9.5.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:375f6e5ee9620a271acb6820b3d1e94ffa8e741c0601db4c0c4d3cb0a9c224bf"},
|
||||||
{file = "pillow-11.1.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:9aa9aeddeed452b2f616ff5507459e7bab436916ccb10961c4a382cd3e03f47f"},
|
{file = "Pillow-9.5.0-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:99eb6cafb6ba90e436684e08dad8be1637efb71c4f2180ee6b8f940739406e78"},
|
||||||
{file = "pillow-11.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3cdcdb0b896e981678eee140d882b70092dac83ac1cdf6b3a60e2216a73f2b91"},
|
{file = "Pillow-9.5.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2dfaaf10b6172697b9bceb9a3bd7b951819d1ca339a5ef294d1f1ac6d7f63270"},
|
||||||
{file = "pillow-11.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:36ba10b9cb413e7c7dfa3e189aba252deee0602c86c309799da5a74009ac7a1c"},
|
{file = "Pillow-9.5.0-cp37-cp37m-manylinux_2_28_aarch64.whl", hash = "sha256:763782b2e03e45e2c77d7779875f4432e25121ef002a41829d8868700d119392"},
|
||||||
{file = "pillow-11.1.0-cp312-cp312-win32.whl", hash = "sha256:cfd5cd998c2e36a862d0e27b2df63237e67273f2fc78f47445b14e73a810e7e6"},
|
{file = "Pillow-9.5.0-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:35f6e77122a0c0762268216315bf239cf52b88865bba522999dc38f1c52b9b47"},
|
||||||
{file = "pillow-11.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:a697cd8ba0383bba3d2d3ada02b34ed268cb548b369943cd349007730c92bddf"},
|
{file = "Pillow-9.5.0-cp37-cp37m-win32.whl", hash = "sha256:aca1c196f407ec7cf04dcbb15d19a43c507a81f7ffc45b690899d6a76ac9fda7"},
|
||||||
{file = "pillow-11.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:4dd43a78897793f60766563969442020e90eb7847463eca901e41ba186a7d4a5"},
|
{file = "Pillow-9.5.0-cp37-cp37m-win_amd64.whl", hash = "sha256:322724c0032af6692456cd6ed554bb85f8149214d97398bb80613b04e33769f6"},
|
||||||
{file = "pillow-11.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ae98e14432d458fc3de11a77ccb3ae65ddce70f730e7c76140653048c71bfcbc"},
|
{file = "Pillow-9.5.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:a0aa9417994d91301056f3d0038af1199eb7adc86e646a36b9e050b06f526597"},
|
||||||
{file = "pillow-11.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cc1331b6d5a6e144aeb5e626f4375f5b7ae9934ba620c0ac6b3e43d5e683a0f0"},
|
{file = "Pillow-9.5.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f8286396b351785801a976b1e85ea88e937712ee2c3ac653710a4a57a8da5d9c"},
|
||||||
{file = "pillow-11.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:758e9d4ef15d3560214cddbc97b8ef3ef86ce04d62ddac17ad39ba87e89bd3b1"},
|
{file = "Pillow-9.5.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c830a02caeb789633863b466b9de10c015bded434deb3ec87c768e53752ad22a"},
|
||||||
{file = "pillow-11.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b523466b1a31d0dcef7c5be1f20b942919b62fd6e9a9be199d035509cbefc0ec"},
|
{file = "Pillow-9.5.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fbd359831c1657d69bb81f0db962905ee05e5e9451913b18b831febfe0519082"},
|
||||||
{file = "pillow-11.1.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:9044b5e4f7083f209c4e35aa5dd54b1dd5b112b108648f5c902ad586d4f945c5"},
|
{file = "Pillow-9.5.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8fc330c3370a81bbf3f88557097d1ea26cd8b019d6433aa59f71195f5ddebbf"},
|
||||||
{file = "pillow-11.1.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:3764d53e09cdedd91bee65c2527815d315c6b90d7b8b79759cc48d7bf5d4f114"},
|
{file = "Pillow-9.5.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:7002d0797a3e4193c7cdee3198d7c14f92c0836d6b4a3f3046a64bd1ce8df2bf"},
|
||||||
{file = "pillow-11.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:31eba6bbdd27dde97b0174ddf0297d7a9c3a507a8a1480e1e60ef914fe23d352"},
|
{file = "Pillow-9.5.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:229e2c79c00e85989a34b5981a2b67aa079fd08c903f0aaead522a1d68d79e51"},
|
||||||
{file = "pillow-11.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b5d658fbd9f0d6eea113aea286b21d3cd4d3fd978157cbf2447a6035916506d3"},
|
{file = "Pillow-9.5.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:9adf58f5d64e474bed00d69bcd86ec4bcaa4123bfa70a65ce72e424bfb88ed96"},
|
||||||
{file = "pillow-11.1.0-cp313-cp313-win32.whl", hash = "sha256:f86d3a7a9af5d826744fabf4afd15b9dfef44fe69a98541f666f66fbb8d3fef9"},
|
{file = "Pillow-9.5.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:662da1f3f89a302cc22faa9f14a262c2e3951f9dbc9617609a47521c69dd9f8f"},
|
||||||
{file = "pillow-11.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:593c5fd6be85da83656b93ffcccc2312d2d149d251e98588b14fbc288fd8909c"},
|
{file = "Pillow-9.5.0-cp38-cp38-win32.whl", hash = "sha256:6608ff3bf781eee0cd14d0901a2b9cc3d3834516532e3bd673a0a204dc8615fc"},
|
||||||
{file = "pillow-11.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:11633d58b6ee5733bde153a8dafd25e505ea3d32e261accd388827ee987baf65"},
|
{file = "Pillow-9.5.0-cp38-cp38-win_amd64.whl", hash = "sha256:e49eb4e95ff6fd7c0c402508894b1ef0e01b99a44320ba7d8ecbabefddcc5569"},
|
||||||
{file = "pillow-11.1.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:70ca5ef3b3b1c4a0812b5c63c57c23b63e53bc38e758b37a951e5bc466449861"},
|
{file = "Pillow-9.5.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:482877592e927fd263028c105b36272398e3e1be3269efda09f6ba21fd83ec66"},
|
||||||
{file = "pillow-11.1.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:8000376f139d4d38d6851eb149b321a52bb8893a88dae8ee7d95840431977081"},
|
{file = "Pillow-9.5.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3ded42b9ad70e5f1754fb7c2e2d6465a9c842e41d178f262e08b8c85ed8a1d8e"},
|
||||||
{file = "pillow-11.1.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ee85f0696a17dd28fbcfceb59f9510aa71934b483d1f5601d1030c3c8304f3c"},
|
{file = "Pillow-9.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c446d2245ba29820d405315083d55299a796695d747efceb5717a8b450324115"},
|
||||||
{file = "pillow-11.1.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:dd0e081319328928531df7a0e63621caf67652c8464303fd102141b785ef9547"},
|
{file = "Pillow-9.5.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8aca1152d93dcc27dc55395604dcfc55bed5f25ef4c98716a928bacba90d33a3"},
|
||||||
{file = "pillow-11.1.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e63e4e5081de46517099dc30abe418122f54531a6ae2ebc8680bcd7096860eab"},
|
{file = "Pillow-9.5.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:608488bdcbdb4ba7837461442b90ea6f3079397ddc968c31265c1e056964f1ef"},
|
||||||
{file = "pillow-11.1.0-cp313-cp313t-win32.whl", hash = "sha256:dda60aa465b861324e65a78c9f5cf0f4bc713e4309f83bc387be158b077963d9"},
|
{file = "Pillow-9.5.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:60037a8db8750e474af7ffc9faa9b5859e6c6d0a50e55c45576bf28be7419705"},
|
||||||
{file = "pillow-11.1.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ad5db5781c774ab9a9b2c4302bbf0c1014960a0a7be63278d13ae6fdf88126fe"},
|
{file = "Pillow-9.5.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:07999f5834bdc404c442146942a2ecadd1cb6292f5229f4ed3b31e0a108746b1"},
|
||||||
{file = "pillow-11.1.0-cp313-cp313t-win_arm64.whl", hash = "sha256:67cd427c68926108778a9005f2a04adbd5e67c442ed21d95389fe1d595458756"},
|
{file = "Pillow-9.5.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:a127ae76092974abfbfa38ca2d12cbeddcdeac0fb71f9627cc1135bedaf9d51a"},
|
||||||
{file = "pillow-11.1.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:bf902d7413c82a1bfa08b06a070876132a5ae6b2388e2712aab3a7cbc02205c6"},
|
{file = "Pillow-9.5.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:489f8389261e5ed43ac8ff7b453162af39c3e8abd730af8363587ba64bb2e865"},
|
||||||
{file = "pillow-11.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c1eec9d950b6fe688edee07138993e54ee4ae634c51443cfb7c1e7613322718e"},
|
{file = "Pillow-9.5.0-cp39-cp39-win32.whl", hash = "sha256:9b1af95c3a967bf1da94f253e56b6286b50af23392a886720f563c547e48e964"},
|
||||||
{file = "pillow-11.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e275ee4cb11c262bd108ab2081f750db2a1c0b8c12c1897f27b160c8bd57bbc"},
|
{file = "Pillow-9.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:77165c4a5e7d5a284f10a6efaa39a0ae8ba839da344f20b111d62cc932fa4e5d"},
|
||||||
{file = "pillow-11.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4db853948ce4e718f2fc775b75c37ba2efb6aaea41a1a5fc57f0af59eee774b2"},
|
{file = "Pillow-9.5.0-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:833b86a98e0ede388fa29363159c9b1a294b0905b5128baf01db683672f230f5"},
|
||||||
{file = "pillow-11.1.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:ab8a209b8485d3db694fa97a896d96dd6533d63c22829043fd9de627060beade"},
|
{file = "Pillow-9.5.0-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aaf305d6d40bd9632198c766fb64f0c1a83ca5b667f16c1e79e1661ab5060140"},
|
||||||
{file = "pillow-11.1.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:54251ef02a2309b5eec99d151ebf5c9904b77976c8abdcbce7891ed22df53884"},
|
{file = "Pillow-9.5.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0852ddb76d85f127c135b6dd1f0bb88dbb9ee990d2cd9aa9e28526c93e794fba"},
|
||||||
{file = "pillow-11.1.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:5bb94705aea800051a743aa4874bb1397d4695fb0583ba5e425ee0328757f196"},
|
{file = "Pillow-9.5.0-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:91ec6fe47b5eb5a9968c79ad9ed78c342b1f97a091677ba0e012701add857829"},
|
||||||
{file = "pillow-11.1.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:89dbdb3e6e9594d512780a5a1c42801879628b38e3efc7038094430844e271d8"},
|
{file = "Pillow-9.5.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:cb841572862f629b99725ebaec3287fc6d275be9b14443ea746c1dd325053cbd"},
|
||||||
{file = "pillow-11.1.0-cp39-cp39-win32.whl", hash = "sha256:e5449ca63da169a2e6068dd0e2fcc8d91f9558aba89ff6d02121ca8ab11e79e5"},
|
{file = "Pillow-9.5.0-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:c380b27d041209b849ed246b111b7c166ba36d7933ec6e41175fd15ab9eb1572"},
|
||||||
{file = "pillow-11.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:3362c6ca227e65c54bf71a5f88b3d4565ff1bcbc63ae72c34b07bbb1cc59a43f"},
|
{file = "Pillow-9.5.0-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7c9af5a3b406a50e313467e3565fc99929717f780164fe6fbb7704edba0cebbe"},
|
||||||
{file = "pillow-11.1.0-cp39-cp39-win_arm64.whl", hash = "sha256:b20be51b37a75cc54c2c55def3fa2c65bb94ba859dde241cd0a4fd302de5ae0a"},
|
{file = "Pillow-9.5.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5671583eab84af046a397d6d0ba25343c00cd50bce03787948e0fff01d4fd9b1"},
|
||||||
{file = "pillow-11.1.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:8c730dc3a83e5ac137fbc92dfcfe1511ce3b2b5d7578315b63dbbb76f7f51d90"},
|
{file = "Pillow-9.5.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:84a6f19ce086c1bf894644b43cd129702f781ba5751ca8572f08aa40ef0ab7b7"},
|
||||||
{file = "pillow-11.1.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:7d33d2fae0e8b170b6a6c57400e077412240f6f5bb2a342cf1ee512a787942bb"},
|
{file = "Pillow-9.5.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:1e7723bd90ef94eda669a3c2c19d549874dd5badaeefabefd26053304abe5799"},
|
||||||
{file = "pillow-11.1.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a8d65b38173085f24bc07f8b6c505cbb7418009fa1a1fcb111b1f4961814a442"},
|
{file = "Pillow-9.5.0.tar.gz", hash = "sha256:bf548479d336726d7a0eceb6e767e179fbde37833ae42794602631a070d630f1"},
|
||||||
{file = "pillow-11.1.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:015c6e863faa4779251436db398ae75051469f7c903b043a48f078e437656f83"},
|
|
||||||
{file = "pillow-11.1.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:d44ff19eea13ae4acdaaab0179fa68c0c6f2f45d66a4d8ec1eda7d6cecbcc15f"},
|
|
||||||
{file = "pillow-11.1.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:d3d8da4a631471dfaf94c10c85f5277b1f8e42ac42bade1ac67da4b4a7359b73"},
|
|
||||||
{file = "pillow-11.1.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:4637b88343166249fe8aa94e7c4a62a180c4b3898283bb5d3d2fd5fe10d8e4e0"},
|
|
||||||
{file = "pillow-11.1.0.tar.gz", hash = "sha256:368da70808b36d73b4b390a8ffac11069f8a5c85f29eff1f1b01bcf3ef5b2a20"},
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.extras]
|
[package.extras]
|
||||||
docs = ["furo", "olefile", "sphinx (>=8.1)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinxext-opengraph"]
|
docs = ["furo", "olefile", "sphinx (>=2.4)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinx-removed-in", "sphinxext-opengraph"]
|
||||||
fpx = ["olefile"]
|
tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"]
|
||||||
mic = ["olefile"]
|
|
||||||
tests = ["check-manifest", "coverage (>=7.4.2)", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout", "trove-classifiers (>=2024.10.12)"]
|
|
||||||
typing = ["typing-extensions ; python_version < \"3.10\""]
|
|
||||||
xmp = ["defusedxml"]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "platformdirs"
|
name = "platformdirs"
|
||||||
@ -2905,6 +2846,18 @@ files = [
|
|||||||
[package.extras]
|
[package.extras]
|
||||||
tests = ["pytest"]
|
tests = ["pytest"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "py"
|
||||||
|
version = "1.11.0"
|
||||||
|
description = "library with cross-python path, ini-parsing, io, code, log facilities"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
||||||
|
groups = ["test"]
|
||||||
|
files = [
|
||||||
|
{file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"},
|
||||||
|
{file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"},
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pycodestyle"
|
name = "pycodestyle"
|
||||||
version = "2.12.1"
|
version = "2.12.1"
|
||||||
@ -3155,6 +3108,22 @@ enabler = ["pytest-enabler (>=2.2)"]
|
|||||||
test = ["pytest (>=6,!=8.1.*)"]
|
test = ["pytest (>=6,!=8.1.*)"]
|
||||||
type = ["pytest-mypy"]
|
type = ["pytest-mypy"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pytest-forked"
|
||||||
|
version = "1.6.0"
|
||||||
|
description = "run tests in isolated forked subprocesses"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
groups = ["test"]
|
||||||
|
files = [
|
||||||
|
{file = "pytest-forked-1.6.0.tar.gz", hash = "sha256:4dafd46a9a600f65d822b8f605133ecf5b3e1941ebb3588e943b4e3eb71a5a3f"},
|
||||||
|
{file = "pytest_forked-1.6.0-py3-none-any.whl", hash = "sha256:810958f66a91afb1a1e2ae83089d8dc1cd2437ac96b12963042fbb9fb4d16af0"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
py = "*"
|
||||||
|
pytest = ">=3.10"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pytest-isort"
|
name = "pytest-isort"
|
||||||
version = "3.1.0"
|
version = "3.1.0"
|
||||||
@ -3187,6 +3156,27 @@ files = [
|
|||||||
docs = ["jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx"]
|
docs = ["jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx"]
|
||||||
testing = ["pytest (>=6)", "pytest-black (>=0.3.7) ; platform_python_implementation != \"PyPy\"", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.0.1)", "pytest-flake8", "pytest-mypy (>=0.9.1) ; platform_python_implementation != \"PyPy\"", "pytest-virtualenv", "types-setuptools"]
|
testing = ["pytest (>=6)", "pytest-black (>=0.3.7) ; platform_python_implementation != \"PyPy\"", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.0.1)", "pytest-flake8", "pytest-mypy (>=0.9.1) ; platform_python_implementation != \"PyPy\"", "pytest-virtualenv", "types-setuptools"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pytest-xdist"
|
||||||
|
version = "1.34.0"
|
||||||
|
description = "pytest xdist plugin for distributed testing and loop-on-failing modes"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
||||||
|
groups = ["test"]
|
||||||
|
files = [
|
||||||
|
{file = "pytest-xdist-1.34.0.tar.gz", hash = "sha256:340e8e83e2a4c0d861bdd8d05c5d7b7143f6eea0aba902997db15c2a86be04ee"},
|
||||||
|
{file = "pytest_xdist-1.34.0-py2.py3-none-any.whl", hash = "sha256:ba5d10729372d65df3ac150872f9df5d2ed004a3b0d499cc0164aafedd8c7b66"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
execnet = ">=1.1"
|
||||||
|
pytest = ">=4.4.0"
|
||||||
|
pytest-forked = "*"
|
||||||
|
six = "*"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
testing = ["filelock"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "python-dateutil"
|
name = "python-dateutil"
|
||||||
version = "2.9.0.post0"
|
version = "2.9.0.post0"
|
||||||
@ -3260,6 +3250,18 @@ files = [
|
|||||||
{file = "pytz-2022.7.1.tar.gz", hash = "sha256:01a0681c4b9684a28304615eba55d1ab31ae00bf68ec157ec3708a8182dbbcd0"},
|
{file = "pytz-2022.7.1.tar.gz", hash = "sha256:01a0681c4b9684a28304615eba55d1ab31ae00bf68ec157ec3708a8182dbbcd0"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pytzdata"
|
||||||
|
version = "2020.1"
|
||||||
|
description = "The Olson timezone database for Python."
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
||||||
|
groups = ["main"]
|
||||||
|
files = [
|
||||||
|
{file = "pytzdata-2020.1-py2.py3-none-any.whl", hash = "sha256:e1e14750bcf95016381e4d472bad004eef710f2d6417240904070b3d6654485f"},
|
||||||
|
{file = "pytzdata-2020.1.tar.gz", hash = "sha256:3efa13b335a00a8de1d345ae41ec78dd11c9f8807f522d39850f2dd828681540"},
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pyyaml"
|
name = "pyyaml"
|
||||||
version = "6.0.2"
|
version = "6.0.2"
|
||||||
@ -4423,7 +4425,7 @@ description = "Backport of pathlib-compatible object wrapper for zip files"
|
|||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.9"
|
python-versions = ">=3.9"
|
||||||
groups = ["main"]
|
groups = ["main"]
|
||||||
markers = "python_version < \"3.10\""
|
markers = "python_version == \"3.9\""
|
||||||
files = [
|
files = [
|
||||||
{file = "zipp-3.21.0-py3-none-any.whl", hash = "sha256:ac1bbe05fd2991f160ebce24ffbac5f6d11d83dc90891255885223d42b3cd931"},
|
{file = "zipp-3.21.0-py3-none-any.whl", hash = "sha256:ac1bbe05fd2991f160ebce24ffbac5f6d11d83dc90891255885223d42b3cd931"},
|
||||||
{file = "zipp-3.21.0.tar.gz", hash = "sha256:2c9958f6430a2040341a52eb608ed6dd93ef4392e02ffe219417c1b28b5dd1f4"},
|
{file = "zipp-3.21.0.tar.gz", hash = "sha256:2c9958f6430a2040341a52eb608ed6dd93ef4392e02ffe219417c1b28b5dd1f4"},
|
||||||
@ -4439,5 +4441,5 @@ type = ["pytest-mypy"]
|
|||||||
|
|
||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "2.1"
|
lock-version = "2.1"
|
||||||
python-versions = ">=3.9,<4.0"
|
python-versions = ">=3.9,<3.12"
|
||||||
content-hash = "887ed21d459eee37807de2edf318cf7fe27de718ff8543c9ed660d3b344aa516"
|
content-hash = "7f26416c6bd7b8fc7ab08ef771ef2aee06c3fbc8de4b5c75ec5d9494d7a85245"
|
||||||
|
|||||||
@ -5,7 +5,7 @@ description = ""
|
|||||||
authors = ["Colin Powell <colin@unbl.ink>"]
|
authors = ["Colin Powell <colin@unbl.ink>"]
|
||||||
|
|
||||||
[tool.poetry.dependencies]
|
[tool.poetry.dependencies]
|
||||||
python = ">=3.9,<4.0"
|
python = ">=3.9,<3.12"
|
||||||
Django = "^4.0.3"
|
Django = "^4.0.3"
|
||||||
django-extensions = "^3.1.5"
|
django-extensions = "^3.1.5"
|
||||||
python-dateutil = "^2.8.2"
|
python-dateutil = "^2.8.2"
|
||||||
@ -16,7 +16,7 @@ httpx = "<=0.27.2"
|
|||||||
djangorestframework = "^3.13.1"
|
djangorestframework = "^3.13.1"
|
||||||
Markdown = "^3.3.6"
|
Markdown = "^3.3.6"
|
||||||
django-filter = "^21.1"
|
django-filter = "^21.1"
|
||||||
Pillow = "^11.1.0"
|
Pillow = "^9.0.1"
|
||||||
psycopg2 = "^2.9.3"
|
psycopg2 = "^2.9.3"
|
||||||
dj-database-url = "^0.5.0"
|
dj-database-url = "^0.5.0"
|
||||||
django-mathfilters = "^1.0.0"
|
django-mathfilters = "^1.0.0"
|
||||||
@ -41,7 +41,7 @@ beautifulsoup4 = "^4.11.2"
|
|||||||
django-storages = "^1.13.2"
|
django-storages = "^1.13.2"
|
||||||
stream-sqlite = "^0.0.41"
|
stream-sqlite = "^0.0.41"
|
||||||
ipython = "^8.14.0"
|
ipython = "^8.14.0"
|
||||||
pendulum = "^3.0.0"
|
pendulum = "^2.1.2"
|
||||||
trafilatura = "^1.6.3"
|
trafilatura = "^1.6.3"
|
||||||
django-imagekit = "^5.0.0"
|
django-imagekit = "^5.0.0"
|
||||||
thefuzz = "^0.22.1"
|
thefuzz = "^0.22.1"
|
||||||
@ -65,6 +65,7 @@ pytest = "^7.1"
|
|||||||
pytest-black = "^0.3.12"
|
pytest-black = "^0.3.12"
|
||||||
pytest-cov = "^3.0"
|
pytest-cov = "^3.0"
|
||||||
pytest-django = "^4.5.2"
|
pytest-django = "^4.5.2"
|
||||||
|
pytest-xdist= "^1.0.0"
|
||||||
pytest-flake8 = "^1.1"
|
pytest-flake8 = "^1.1"
|
||||||
pytest-isort = "^3.0"
|
pytest-isort = "^3.0"
|
||||||
pytest-runner = "^6.0"
|
pytest-runner = "^6.0"
|
||||||
|
|||||||
@ -4,8 +4,9 @@ import pytest
|
|||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from rest_framework.authtoken.models import Token
|
from rest_framework.authtoken.models import Token
|
||||||
|
|
||||||
from scrobbles.models import Scrobble
|
|
||||||
from boardgames.models import BoardGame
|
from boardgames.models import BoardGame
|
||||||
|
from music.models import Track, Artist
|
||||||
|
from scrobbles.models import Scrobble
|
||||||
|
|
||||||
User = get_user_model()
|
User = get_user_model()
|
||||||
|
|
||||||
@ -27,6 +28,15 @@ def boardgame_scrobble():
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def test_track():
|
||||||
|
Track.objects.create(
|
||||||
|
title="Emotion",
|
||||||
|
artist=Artist.objects.create(name="Carly Rae Jepsen"),
|
||||||
|
run_time_seconds=60,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class MopidyRequest:
|
class MopidyRequest:
|
||||||
name = "Same in the End"
|
name = "Same in the End"
|
||||||
artist = "Sublime"
|
artist = "Sublime"
|
||||||
@ -98,9 +108,11 @@ def mopidy_track_diff_album_request_data(**kwargs):
|
|||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def mopidy_podcast():
|
def mopidy_podcast_request_data():
|
||||||
mopidy_uri = "local:podcast:Up%20First/2022-01-01%20Up%20First.mp3"
|
mopidy_uri = "local:podcast:Up%20First/2022-01-01%20Up%20First.mp3"
|
||||||
return MopidyRequest(mopidy_uri=mopidy_uri)
|
return MopidyRequest(
|
||||||
|
mopidy_uri=mopidy_uri, artist="NPR", album="Up First"
|
||||||
|
).request_json
|
||||||
|
|
||||||
|
|
||||||
class JellyfinTrackRequest:
|
class JellyfinTrackRequest:
|
||||||
|
|||||||
@ -30,55 +30,25 @@ def test_bad_mopidy_request_data(client, valid_auth_token):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.django_db
|
||||||
"seconds, expected_percent_played, expected_scrobble_id",
|
@patch("music.utils.lookup_artist_from_mb", return_value={})
|
||||||
[
|
@patch(
|
||||||
(1, 1, 1),
|
"music.utils.lookup_album_dict_from_mb",
|
||||||
(58, 96, 1),
|
return_value={"year": "1999", "mb_group_id": 1},
|
||||||
(59, 98, 1),
|
|
||||||
(60, 100, 1),
|
|
||||||
(1, 1, 1),
|
|
||||||
(1, 1, 1),
|
|
||||||
],
|
|
||||||
)
|
)
|
||||||
@pytest.mark.django_db
|
@patch("music.utils.lookup_track_from_mb", return_value={})
|
||||||
def test_scrobble_mopidy_track(
|
@patch("music.models.lookup_artist_from_tadb", return_value={})
|
||||||
client,
|
@patch("music.models.lookup_album_from_tadb", return_value={"year": "1999"})
|
||||||
mopidy_track,
|
@patch("music.models.Album.fetch_artwork", return_value=None)
|
||||||
valid_auth_token,
|
@patch("music.models.Album.scrape_allmusic", return_value=None)
|
||||||
seconds,
|
|
||||||
expected_percent_played,
|
|
||||||
expected_scrobble_id,
|
|
||||||
):
|
|
||||||
url = reverse("scrobbles:mopidy-webhook")
|
|
||||||
headers = {"Authorization": f"Token {valid_auth_token}"}
|
|
||||||
|
|
||||||
# Start new scrobble
|
|
||||||
minutes = 0
|
|
||||||
calc_seconds = seconds
|
|
||||||
if seconds >= 60:
|
|
||||||
minutes = 1
|
|
||||||
calc_seconds = calc_seconds % 10
|
|
||||||
with time_machine.travel(datetime(2024, 1, 14, 12, minutes, calc_seconds)):
|
|
||||||
mopidy_track.request_data["playback_time_ticks"] = seconds * 1000
|
|
||||||
response = client.post(
|
|
||||||
url,
|
|
||||||
mopidy_track.request_json,
|
|
||||||
content_type="application/json",
|
|
||||||
headers=headers,
|
|
||||||
)
|
|
||||||
assert response.status_code == 200
|
|
||||||
assert response.data == {"scrobble_id": expected_scrobble_id}
|
|
||||||
|
|
||||||
scrobble = Scrobble.objects.get(id=1)
|
|
||||||
assert scrobble.percent_played == expected_percent_played
|
|
||||||
assert scrobble.media_obj.__class__ == Track
|
|
||||||
assert scrobble.media_obj.title == "Same in the End"
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skip(reason="Allmusic API is unstable")
|
|
||||||
@pytest.mark.django_db
|
|
||||||
def test_scrobble_mopidy_same_track_different_album(
|
def test_scrobble_mopidy_same_track_different_album(
|
||||||
|
mock_lookup_artist,
|
||||||
|
mock_lookup_album,
|
||||||
|
mock_lookup_track,
|
||||||
|
mock_lookup_artist_tadb,
|
||||||
|
mock_lookup_album_tadb,
|
||||||
|
mock_fetch_artwork,
|
||||||
|
mock_scrape_allmusic,
|
||||||
client,
|
client,
|
||||||
mopidy_track,
|
mopidy_track,
|
||||||
mopidy_track_diff_album_request_data,
|
mopidy_track_diff_album_request_data,
|
||||||
@ -107,14 +77,17 @@ def test_scrobble_mopidy_same_track_different_album(
|
|||||||
assert response.data == {"scrobble_id": 2}
|
assert response.data == {"scrobble_id": 2}
|
||||||
scrobble = Scrobble.objects.last()
|
scrobble = Scrobble.objects.last()
|
||||||
assert scrobble.media_obj.__class__ == Track
|
assert scrobble.media_obj.__class__ == Track
|
||||||
assert scrobble.media_obj.album.name == "Gold"
|
assert scrobble.media_obj.album.name == "Sublime"
|
||||||
assert scrobble.media_obj.title == "Same in the End"
|
assert scrobble.media_obj.title == "Same in the End"
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skip("Need to add a mock podcast request data, tho Google Podcasts is gone :thinking:")
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
|
@patch(
|
||||||
|
"podcasts.sources.podcastindex.lookup_podcast_from_podcastindex",
|
||||||
|
return_value={},
|
||||||
|
)
|
||||||
def test_scrobble_mopidy_podcast(
|
def test_scrobble_mopidy_podcast(
|
||||||
client, mopidy_podcast_request_data, valid_auth_token
|
mock_lookup_podcast, client, mopidy_podcast_request_data, valid_auth_token
|
||||||
):
|
):
|
||||||
url = reverse("scrobbles:mopidy-webhook")
|
url = reverse("scrobbles:mopidy-webhook")
|
||||||
headers = {"Authorization": f"Token {valid_auth_token}"}
|
headers = {"Authorization": f"Token {valid_auth_token}"}
|
||||||
@ -175,38 +148,101 @@ def test_scrobble_jellyfin_track(
|
|||||||
assert scrobble.media_obj.__class__ == Track
|
assert scrobble.media_obj.__class__ == Track
|
||||||
assert scrobble.media_obj.title == "Emotion"
|
assert scrobble.media_obj.title == "Emotion"
|
||||||
|
|
||||||
with time_machine.travel(datetime(2024, 1, 14, 12, 0, 58)):
|
|
||||||
jellyfin_track.request_data["UtcTimestamp"] = timezone.now().strftime(
|
|
||||||
"%Y-%m-%d %H:%M:%S"
|
|
||||||
)
|
|
||||||
response = client.post(
|
|
||||||
url,
|
|
||||||
jellyfin_track.request_json,
|
|
||||||
content_type="application/json",
|
|
||||||
headers=headers,
|
|
||||||
)
|
|
||||||
|
|
||||||
assert response.status_code == 200
|
@pytest.mark.django_db
|
||||||
assert response.data == {"scrobble_id": 1}
|
@patch("music.utils.lookup_artist_from_mb", return_value={})
|
||||||
|
@patch(
|
||||||
|
"music.utils.lookup_album_dict_from_mb",
|
||||||
|
return_value={"year": "1999", "mb_group_id": 1},
|
||||||
|
)
|
||||||
|
@patch("music.utils.lookup_track_from_mb", return_value={})
|
||||||
|
@patch("music.models.lookup_artist_from_tadb", return_value={})
|
||||||
|
@patch("music.models.lookup_album_from_tadb", return_value={"year": "1999"})
|
||||||
|
@patch("music.models.Album.fetch_artwork", return_value=None)
|
||||||
|
@patch("music.models.Album.scrape_allmusic", return_value=None)
|
||||||
|
def test_scrobble_jellyfin_track_update(
|
||||||
|
mock_lookup_artist,
|
||||||
|
mock_lookup_album,
|
||||||
|
mock_lookup_track,
|
||||||
|
mock_lookup_artist_tadb,
|
||||||
|
mock_lookup_album_tadb,
|
||||||
|
mock_fetch_artwork,
|
||||||
|
mock_scrape_allmusic,
|
||||||
|
test_track,
|
||||||
|
client,
|
||||||
|
jellyfin_track,
|
||||||
|
valid_auth_token,
|
||||||
|
):
|
||||||
|
Scrobble.objects.create(
|
||||||
|
timestamp=timezone.now() - timedelta(minutes=0.5),
|
||||||
|
track=Track.objects.first(),
|
||||||
|
user_id=1,
|
||||||
|
)
|
||||||
|
url = reverse("scrobbles:jellyfin-webhook")
|
||||||
|
headers = {"Authorization": f"Token {valid_auth_token}"}
|
||||||
|
|
||||||
scrobble = Scrobble.objects.get(id=1)
|
jellyfin_track.request_data["UtcTimestamp"] = timezone.now().strftime(
|
||||||
assert scrobble.media_obj.__class__ == Track
|
"%Y-%m-%d %H:%M:%S"
|
||||||
assert scrobble.media_obj.title == "Emotion"
|
)
|
||||||
|
response = client.post(
|
||||||
|
url,
|
||||||
|
jellyfin_track.request_json,
|
||||||
|
content_type="application/json",
|
||||||
|
headers=headers,
|
||||||
|
)
|
||||||
|
|
||||||
with time_machine.travel(datetime(2024, 1, 14, 12, 1, 1)):
|
assert response.status_code == 200
|
||||||
jellyfin_track.request_data["UtcTimestamp"] = timezone.now().strftime(
|
assert response.data == {"scrobble_id": 1}
|
||||||
"%Y-%m-%d %H:%M:%S"
|
|
||||||
)
|
|
||||||
response = client.post(
|
|
||||||
url,
|
|
||||||
jellyfin_track.request_json,
|
|
||||||
content_type="application/json",
|
|
||||||
headers=headers,
|
|
||||||
)
|
|
||||||
|
|
||||||
assert response.status_code == 200
|
scrobble = Scrobble.objects.get(id=1)
|
||||||
assert response.data == {"scrobble_id": 2}
|
assert scrobble.media_obj.__class__ == Track
|
||||||
|
assert scrobble.media_obj.title == "Emotion"
|
||||||
|
|
||||||
scrobble = Scrobble.objects.get(id=1)
|
|
||||||
assert scrobble.media_obj.__class__ == Track
|
@pytest.mark.django_db
|
||||||
assert scrobble.media_obj.title == "Emotion"
|
@patch("music.utils.lookup_artist_from_mb", return_value={})
|
||||||
|
@patch(
|
||||||
|
"music.utils.lookup_album_dict_from_mb",
|
||||||
|
return_value={"year": "1999", "mb_group_id": 1},
|
||||||
|
)
|
||||||
|
@patch("music.utils.lookup_track_from_mb", return_value={})
|
||||||
|
@patch("music.models.lookup_artist_from_tadb", return_value={})
|
||||||
|
@patch("music.models.lookup_album_from_tadb", return_value={"year": "1999"})
|
||||||
|
@patch("music.models.Album.fetch_artwork", return_value=None)
|
||||||
|
@patch("music.models.Album.scrape_allmusic", return_value=None)
|
||||||
|
def test_scrobble_jellyfin_track_create_new(
|
||||||
|
mock_lookup_artist,
|
||||||
|
mock_lookup_album,
|
||||||
|
mock_lookup_track,
|
||||||
|
mock_lookup_artist_tadb,
|
||||||
|
mock_lookup_album_tadb,
|
||||||
|
mock_fetch_artwork,
|
||||||
|
mock_scrape_allmusic,
|
||||||
|
test_track,
|
||||||
|
client,
|
||||||
|
jellyfin_track,
|
||||||
|
valid_auth_token,
|
||||||
|
):
|
||||||
|
url = reverse("scrobbles:jellyfin-webhook")
|
||||||
|
headers = {"Authorization": f"Token {valid_auth_token}"}
|
||||||
|
Scrobble.objects.create(
|
||||||
|
timestamp=timezone.now() - timedelta(minutes=1),
|
||||||
|
track=Track.objects.first(),
|
||||||
|
user_id=1,
|
||||||
|
)
|
||||||
|
jellyfin_track.request_data["UtcTimestamp"] = timezone.now().strftime(
|
||||||
|
"%Y-%m-%d %H:%M:%S"
|
||||||
|
)
|
||||||
|
response = client.post(
|
||||||
|
url,
|
||||||
|
jellyfin_track.request_json,
|
||||||
|
content_type="application/json",
|
||||||
|
headers=headers,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.data == {"scrobble_id": 2}
|
||||||
|
|
||||||
|
scrobble = Scrobble.objects.get(id=1)
|
||||||
|
assert scrobble.media_obj.__class__ == Track
|
||||||
|
assert scrobble.media_obj.title == "Emotion"
|
||||||
|
|||||||
16
vrobbler/apps/books/migrations/0028_delete_page.py
Normal file
16
vrobbler/apps/books/migrations/0028_delete_page.py
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
# Generated by Django 4.2.19 on 2025-04-07 17:16
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("books", "0027_remove_paper_num_citations_paper_tldr"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.DeleteModel(
|
||||||
|
name="Page",
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -5,6 +5,7 @@ import pytest
|
|||||||
from books.openlibrary import lookup_book_from_openlibrary
|
from books.openlibrary import lookup_book_from_openlibrary
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skip()
|
||||||
def test_lookup_modern_book():
|
def test_lookup_modern_book():
|
||||||
book = lookup_book_from_openlibrary("Matrix", "Lauren Groff")
|
book = lookup_book_from_openlibrary("Matrix", "Lauren Groff")
|
||||||
assert book.get("title") == "Matrix"
|
assert book.get("title") == "Matrix"
|
||||||
@ -12,6 +13,7 @@ def test_lookup_modern_book():
|
|||||||
assert book.get("ol_author_id") == "OL3675729A"
|
assert book.get("ol_author_id") == "OL3675729A"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skip()
|
||||||
def test_lookup_classic_book():
|
def test_lookup_classic_book():
|
||||||
book = lookup_book_from_openlibrary(
|
book = lookup_book_from_openlibrary(
|
||||||
"The Life of Castruccio Castracani", "Machiavelli"
|
"The Life of Castruccio Castracani", "Machiavelli"
|
||||||
@ -21,6 +23,7 @@ def test_lookup_classic_book():
|
|||||||
assert book.get("ol_author_id") == "OL23135A"
|
assert book.get("ol_author_id") == "OL23135A"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skip()
|
||||||
def test_lookup_foreign_book():
|
def test_lookup_foreign_book():
|
||||||
book = lookup_book_from_openlibrary("Ravagé", "René Barjavel")
|
book = lookup_book_from_openlibrary("Ravagé", "René Barjavel")
|
||||||
assert book.get("title") == "Ravage"
|
assert book.get("title") == "Ravage"
|
||||||
|
|||||||
@ -1,14 +1,10 @@
|
|||||||
import logging
|
import logging
|
||||||
import time
|
from datetime import datetime, timedelta
|
||||||
from datetime import datetime, timedelta, UTC
|
|
||||||
|
|
||||||
import pylast
|
import pylast
|
||||||
import pytz
|
import pytz
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.utils import timezone
|
from music.models import Track
|
||||||
from music.utils import (
|
|
||||||
get_or_create_track,
|
|
||||||
)
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -47,14 +43,10 @@ class LastFM:
|
|||||||
lastfm_scrobbles = self.get_last_scrobbles(time_from=last_processed)
|
lastfm_scrobbles = self.get_last_scrobbles(time_from=last_processed)
|
||||||
|
|
||||||
for lfm_scrobble in lastfm_scrobbles:
|
for lfm_scrobble in lastfm_scrobbles:
|
||||||
track = get_or_create_track(
|
track = Track.find_or_create(
|
||||||
lfm_scrobble,
|
title=lfm_scrobble.get("title"),
|
||||||
{
|
artist_name=lfm_scrobble.get("artist"),
|
||||||
"TRACK_TITLE": "title",
|
album_name=lfm_scrobble.get("album"),
|
||||||
"ARTIST_NAME": "artist",
|
|
||||||
"ALBUM_NAME": "album",
|
|
||||||
"RUN_TIME": "run_time_seconds",
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
timezone = settings.TIME_ZONE
|
timezone = settings.TIME_ZONE
|
||||||
@ -149,7 +141,7 @@ class LastFM:
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
# TODO figure out if this will actually work
|
# TODO figure out if this will actually work
|
||||||
#timestamp = datetime.fromtimestamp(int(scrobble.timestamp), UTC)
|
# timestamp = datetime.fromtimestamp(int(scrobble.timestamp), UTC)
|
||||||
timestamp = datetime.utcfromtimestamp(
|
timestamp = datetime.utcfromtimestamp(
|
||||||
int(scrobble.timestamp)
|
int(scrobble.timestamp)
|
||||||
).replace(tzinfo=pytz.utc)
|
).replace(tzinfo=pytz.utc)
|
||||||
|
|||||||
18
vrobbler/apps/music/migrations/0025_artist_alt_names.py
Normal file
18
vrobbler/apps/music/migrations/0025_artist_alt_names.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 4.2.19 on 2025-04-07 00:23
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("music", "0024_alter_track_run_time_seconds"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="artist",
|
||||||
|
name="alt_names",
|
||||||
|
field=models.TextField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
]
|
||||||
18
vrobbler/apps/music/migrations/0026_album_alt_names.py
Normal file
18
vrobbler/apps/music/migrations/0026_album_alt_names.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 4.2.19 on 2025-04-07 00:34
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("music", "0025_artist_alt_names"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="album",
|
||||||
|
name="alt_names",
|
||||||
|
field=models.TextField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -1,13 +1,11 @@
|
|||||||
import logging
|
import logging
|
||||||
from tempfile import NamedTemporaryFile
|
|
||||||
from typing import Dict, Optional
|
from typing import Dict, Optional
|
||||||
from urllib.request import urlopen
|
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
import musicbrainzngs
|
import musicbrainzngs
|
||||||
import requests
|
import requests
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.files.base import ContentFile, File
|
from django.core.files.base import ContentFile
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
@ -16,6 +14,7 @@ from imagekit.models import ImageSpecField
|
|||||||
from imagekit.processors import ResizeToFit
|
from imagekit.processors import ResizeToFit
|
||||||
from music.allmusic import get_allmusic_slug, scrape_data_from_allmusic
|
from music.allmusic import get_allmusic_slug, scrape_data_from_allmusic
|
||||||
from music.bandcamp import get_bandcamp_slug
|
from music.bandcamp import get_bandcamp_slug
|
||||||
|
from music.musicbrainz import lookup_album_dict_from_mb, lookup_track_from_mb
|
||||||
from music.theaudiodb import lookup_album_from_tadb, lookup_artist_from_tadb
|
from music.theaudiodb import lookup_album_from_tadb, lookup_artist_from_tadb
|
||||||
from scrobbles.mixins import ScrobblableConstants, ScrobblableMixin
|
from scrobbles.mixins import ScrobblableConstants, ScrobblableMixin
|
||||||
|
|
||||||
@ -24,6 +23,16 @@ BNULL = {"blank": True, "null": True}
|
|||||||
|
|
||||||
|
|
||||||
class Artist(TimeStampedModel):
|
class Artist(TimeStampedModel):
|
||||||
|
"""Represents a music artist.
|
||||||
|
|
||||||
|
# Lookup or create by title alone
|
||||||
|
>>> Artist.find_or_create(name="Bon Iver")
|
||||||
|
|
||||||
|
# Lookup or create by MB id alone
|
||||||
|
>>> Artist.find_or_create(musicbrainz_id="0307edfc-437c-4b48-8700-80680e66a228")
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
uuid = models.UUIDField(default=uuid4, editable=False, **BNULL)
|
uuid = models.UUIDField(default=uuid4, editable=False, **BNULL)
|
||||||
name = models.CharField(max_length=255)
|
name = models.CharField(max_length=255)
|
||||||
biography = models.TextField(**BNULL)
|
biography = models.TextField(**BNULL)
|
||||||
@ -46,6 +55,7 @@ class Artist(TimeStampedModel):
|
|||||||
format="JPEG",
|
format="JPEG",
|
||||||
options={"quality": 75},
|
options={"quality": 75},
|
||||||
)
|
)
|
||||||
|
alt_names = models.TextField(**BNULL)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
unique_together = [["name", "musicbrainz_id"]]
|
unique_together = [["name", "musicbrainz_id"]]
|
||||||
@ -62,8 +72,10 @@ class Artist(TimeStampedModel):
|
|||||||
return ""
|
return ""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def mb_link(self):
|
def mb_link(self) -> str:
|
||||||
return f"https://musicbrainz.org/artist/{self.musicbrainz_id}"
|
if self.musicbrainz_id:
|
||||||
|
return f"https://musicbrainz.org/artist/{self.musicbrainz_id}"
|
||||||
|
return ""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def allmusic_link(self):
|
def allmusic_link(self):
|
||||||
@ -104,7 +116,9 @@ class Artist(TimeStampedModel):
|
|||||||
if not self.allmusic_id or force:
|
if not self.allmusic_id or force:
|
||||||
slug = get_allmusic_slug(self.name)
|
slug = get_allmusic_slug(self.name)
|
||||||
if not slug:
|
if not slug:
|
||||||
logger.info(f"No allmsuic link for {self}")
|
logger.info(
|
||||||
|
"No allmusic link found", extra={"track_id": self.id}
|
||||||
|
)
|
||||||
return
|
return
|
||||||
self.allmusic_id = slug
|
self.allmusic_id = slug
|
||||||
self.save(update_fields=["allmusic_id"])
|
self.save(update_fields=["allmusic_id"])
|
||||||
@ -113,7 +127,9 @@ class Artist(TimeStampedModel):
|
|||||||
if not self.bandcamp_id or force:
|
if not self.bandcamp_id or force:
|
||||||
slug = get_bandcamp_slug(self.name)
|
slug = get_bandcamp_slug(self.name)
|
||||||
if not slug:
|
if not slug:
|
||||||
logger.info(f"No bandcamp link for {self}")
|
logger.info(
|
||||||
|
"No bandcamp link found", extra={"track_id": self.id}
|
||||||
|
)
|
||||||
return
|
return
|
||||||
self.bandcamp_id = slug
|
self.bandcamp_id = slug
|
||||||
self.save(update_fields=["bandcamp_id"])
|
self.save(update_fields=["bandcamp_id"])
|
||||||
@ -153,6 +169,61 @@ class Artist(TimeStampedModel):
|
|||||||
artist = self.name.lower()
|
artist = self.name.lower()
|
||||||
return f"https://bandcamp.com/search?q={artist}&item_type=b"
|
return f"https://bandcamp.com/search?q={artist}&item_type=b"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def find_or_create(cls, name: str, musicbrainz_id: str = "") -> "Artist":
|
||||||
|
from music.musicbrainz import lookup_artist_from_mb
|
||||||
|
from music.utils import clean_artist_name
|
||||||
|
|
||||||
|
if not name:
|
||||||
|
raise Exception("Must have name to lookup artist")
|
||||||
|
|
||||||
|
artist = None
|
||||||
|
name = clean_artist_name(name)
|
||||||
|
|
||||||
|
# Check for name/mbid combo, just mbid and then just name
|
||||||
|
if musicbrainz_id:
|
||||||
|
artist = cls.objects.filter(
|
||||||
|
name=name, musicbrainz_id=musicbrainz_id
|
||||||
|
).first()
|
||||||
|
if not artist:
|
||||||
|
artist = cls.objects.filter(musicbrainz_id=musicbrainz_id).first()
|
||||||
|
if not artist:
|
||||||
|
artist = cls.objects.filter(
|
||||||
|
models.Q(name=name) | models.Q(alt_names__icontains=name)
|
||||||
|
).first()
|
||||||
|
|
||||||
|
# Does not exist, look it up from Musicbrainz
|
||||||
|
if not artist:
|
||||||
|
alt_name = None
|
||||||
|
try:
|
||||||
|
artist_dict = lookup_artist_from_mb(name)
|
||||||
|
musicbrainz_id = musicbrainz_id or artist_dict.get("id", "")
|
||||||
|
if name != artist_dict.get("name", ""):
|
||||||
|
alt_name = name
|
||||||
|
name = artist_dict.get("name", "")
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if musicbrainz_id:
|
||||||
|
artist = cls.objects.filter(
|
||||||
|
musicbrainz_id=musicbrainz_id
|
||||||
|
).first()
|
||||||
|
if artist and alt_name:
|
||||||
|
if not artist.alt_names:
|
||||||
|
artist.alt_names = alt_name
|
||||||
|
else:
|
||||||
|
artist.alt_names += f"\\{alt_name}"
|
||||||
|
artist.save(update_fields=["alt_names"])
|
||||||
|
|
||||||
|
if not artist:
|
||||||
|
artist = cls.objects.create(
|
||||||
|
name=name, musicbrainz_id=musicbrainz_id, alt_names=alt_name
|
||||||
|
)
|
||||||
|
# TODO maybe this should be spun off into an async task?
|
||||||
|
artist.fix_metadata()
|
||||||
|
|
||||||
|
return artist
|
||||||
|
|
||||||
|
|
||||||
class Album(TimeStampedModel):
|
class Album(TimeStampedModel):
|
||||||
uuid = models.UUIDField(default=uuid4, editable=False, **BNULL)
|
uuid = models.UUIDField(default=uuid4, editable=False, **BNULL)
|
||||||
@ -196,9 +267,10 @@ class Album(TimeStampedModel):
|
|||||||
wikipedia_slug = models.CharField(max_length=255, **BNULL)
|
wikipedia_slug = models.CharField(max_length=255, **BNULL)
|
||||||
discogs_id = models.CharField(max_length=255, **BNULL)
|
discogs_id = models.CharField(max_length=255, **BNULL)
|
||||||
wikidata_id = models.CharField(max_length=255, **BNULL)
|
wikidata_id = models.CharField(max_length=255, **BNULL)
|
||||||
|
alt_names = models.TextField(**BNULL)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self) -> str:
|
||||||
return self.name
|
return "{} by {}".format(self.name, self.album_artist)
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse("music:album_detail", kwargs={"slug": self.uuid})
|
return reverse("music:album_detail", kwargs={"slug": self.uuid})
|
||||||
@ -402,6 +474,69 @@ class Album(TimeStampedModel):
|
|||||||
album = self.name.lower()
|
album = self.name.lower()
|
||||||
return f"https://bandcamp.com/search?q={album} {artist}&item_type=a"
|
return f"https://bandcamp.com/search?q={album} {artist}&item_type=a"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def find_or_create(
|
||||||
|
cls, name: str, artist_name: str, musicbrainz_id: str = ""
|
||||||
|
) -> "Album":
|
||||||
|
if not name or not artist_name:
|
||||||
|
raise Exception(
|
||||||
|
"Must have at least name and artist name to lookup album"
|
||||||
|
)
|
||||||
|
|
||||||
|
album = None
|
||||||
|
if musicbrainz_id:
|
||||||
|
album = cls.objects.filter(
|
||||||
|
musicbrainz_id=musicbrainz_id,
|
||||||
|
name=name,
|
||||||
|
album_artist__name=artist_name,
|
||||||
|
).first()
|
||||||
|
if not album and musicbrainz_id:
|
||||||
|
album = cls.objects.filter(
|
||||||
|
musicbrainz_id=musicbrainz_id,
|
||||||
|
).first()
|
||||||
|
if not album:
|
||||||
|
album = cls.objects.filter(
|
||||||
|
models.Q(name=name) | models.Q(alt_names__icontains=name),
|
||||||
|
album_artist__name=artist_name,
|
||||||
|
).first()
|
||||||
|
|
||||||
|
if not album:
|
||||||
|
alt_name = None
|
||||||
|
try:
|
||||||
|
album_dict = lookup_album_dict_from_mb(
|
||||||
|
name, artist_name=artist_name
|
||||||
|
)
|
||||||
|
musicbrainz_id = musicbrainz_id or album_dict.get("mb_id", "")
|
||||||
|
found_name = album_dict.get("title", "")
|
||||||
|
if found_name and name != found_name:
|
||||||
|
alt_name = name
|
||||||
|
name = found_name
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if musicbrainz_id:
|
||||||
|
album = cls.objects.filter(
|
||||||
|
musicbrainz_id=musicbrainz_id
|
||||||
|
).first()
|
||||||
|
if album and alt_name:
|
||||||
|
if not album.alt_names:
|
||||||
|
album.alt_names = alt_name
|
||||||
|
else:
|
||||||
|
album.alt_names += f"\\{alt_name}"
|
||||||
|
album.save(update_fields=["alt_names"])
|
||||||
|
if not album:
|
||||||
|
artist = Artist.find_or_create(name=artist_name)
|
||||||
|
album = cls.objects.create(
|
||||||
|
name=name,
|
||||||
|
album_artist=artist,
|
||||||
|
musicbrainz_id=musicbrainz_id,
|
||||||
|
alt_names=alt_name,
|
||||||
|
)
|
||||||
|
# TODO maybe do this in a separate process?
|
||||||
|
album.fix_metadata()
|
||||||
|
|
||||||
|
return album
|
||||||
|
|
||||||
|
|
||||||
class Track(ScrobblableMixin):
|
class Track(ScrobblableMixin):
|
||||||
COMPLETION_PERCENT = getattr(settings, "MUSIC_COMPLETION_PERCENT", 100)
|
COMPLETION_PERCENT = getattr(settings, "MUSIC_COMPLETION_PERCENT", 100)
|
||||||
@ -425,8 +560,8 @@ class Track(ScrobblableMixin):
|
|||||||
return reverse("music:track_detail", kwargs={"slug": self.uuid})
|
return reverse("music:track_detail", kwargs={"slug": self.uuid})
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def subtitle(self):
|
def subtitle(self) -> str:
|
||||||
return self.artist
|
return str(self.artist)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def strings(self) -> ScrobblableConstants:
|
def strings(self) -> ScrobblableConstants:
|
||||||
@ -451,31 +586,85 @@ class Track(ScrobblableMixin):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def find_or_create(
|
def find_or_create(
|
||||||
cls, artist_dict: Dict, album_dict: Dict, track_dict: Dict
|
cls,
|
||||||
) -> Optional["Track"]:
|
title: str = "",
|
||||||
"""Given a data dict from Jellyfin, does the heavy lifting of looking up
|
musicbrainz_id: str = "",
|
||||||
the video and, if need, TV Series, creating both if they don't yet
|
album_name: str = "",
|
||||||
exist.
|
artist_name: str = "",
|
||||||
|
enrich: bool = True,
|
||||||
|
run_time_seconds: Optional[int] = None,
|
||||||
|
) -> "Track":
|
||||||
|
# TODO we can use Q to build queries here based on whether we have mbid and album name
|
||||||
|
track = None
|
||||||
|
# Full look up with MB ID
|
||||||
|
if album_name:
|
||||||
|
track = cls.objects.filter(
|
||||||
|
musicbrainz_id=musicbrainz_id,
|
||||||
|
title=title,
|
||||||
|
artist__name=artist_name,
|
||||||
|
album__name=album_name,
|
||||||
|
).first()
|
||||||
|
# Full look up without album
|
||||||
|
if not track:
|
||||||
|
track = cls.objects.filter(
|
||||||
|
musicbrainz_id=musicbrainz_id,
|
||||||
|
title=title,
|
||||||
|
artist__name=artist_name,
|
||||||
|
).first()
|
||||||
|
|
||||||
"""
|
# Full look up without MB ID
|
||||||
if not artist_dict.get("name") or not artist_dict.get(
|
if not track:
|
||||||
"musicbrainz_id"
|
track = cls.objects.filter(
|
||||||
):
|
title=title,
|
||||||
logger.warning(
|
artist__name=artist_name,
|
||||||
f"No artist or artist musicbrainz ID found in message from source, not scrobbling"
|
album__name=album_name,
|
||||||
)
|
).first()
|
||||||
return
|
# Base look up without MB ID or album
|
||||||
|
if not track:
|
||||||
|
track = cls.objects.filter(
|
||||||
|
title=title,
|
||||||
|
artist__name=artist_name,
|
||||||
|
).first()
|
||||||
|
|
||||||
artist, artist_created = Artist.objects.get_or_create(**artist_dict)
|
if not track and enrich:
|
||||||
album, album_created = Album.objects.get_or_create(**album_dict)
|
track_dict = lookup_track_from_mb(title, artist_name, album_name)
|
||||||
|
musicbrainz_id = musicbrainz_id or track_dict.get("id", "")
|
||||||
|
# TODO This only works some of the time
|
||||||
|
# try:
|
||||||
|
# album_name = album_name or track_dict.get("release-list")[
|
||||||
|
# 0
|
||||||
|
# ].get("title", "")
|
||||||
|
# except IndexError:
|
||||||
|
# pass
|
||||||
|
if not run_time_seconds:
|
||||||
|
run_time_seconds = int(
|
||||||
|
int(track_dict.get("length", 900000)) / 1000
|
||||||
|
)
|
||||||
|
if title != track_dict.get("name", "") and track_dict.get(
|
||||||
|
"name", False
|
||||||
|
):
|
||||||
|
|
||||||
album.fix_metadata()
|
title = track_dict.get("name", "")
|
||||||
if not album.cover_image:
|
|
||||||
album.fetch_artwork()
|
|
||||||
|
|
||||||
track_dict["album_id"] = getattr(album, "id", None)
|
if musicbrainz_id:
|
||||||
track_dict["artist_id"] = artist.id
|
track = cls.objects.filter(
|
||||||
|
musicbrainz_id=musicbrainz_id
|
||||||
track, created = cls.objects.get_or_create(**track_dict)
|
).first()
|
||||||
|
if not track:
|
||||||
|
artist = Artist.find_or_create(name=artist_name)
|
||||||
|
album = None
|
||||||
|
if album_name:
|
||||||
|
album = Album.find_or_create(
|
||||||
|
name=album_name, artist_name=artist_name
|
||||||
|
)
|
||||||
|
track = cls.objects.create(
|
||||||
|
title=title,
|
||||||
|
album=album,
|
||||||
|
musicbrainz_id=musicbrainz_id,
|
||||||
|
artist=artist,
|
||||||
|
run_time_seconds=run_time_seconds,
|
||||||
|
)
|
||||||
|
# TODO maybe do this in a separate process?
|
||||||
|
track.fix_metadata()
|
||||||
|
|
||||||
return track
|
return track
|
||||||
|
|||||||
@ -16,9 +16,8 @@ logger = logging.getLogger(__name__)
|
|||||||
from music.models import Album, Artist, Track
|
from music.models import Album, Artist, Track
|
||||||
|
|
||||||
|
|
||||||
def get_or_create_artist(name: str, mbid: str = None) -> Artist:
|
def clean_artist_name(name: str) -> str:
|
||||||
artist = None
|
"""Remove featured names from artist string."""
|
||||||
|
|
||||||
if "feat." in name.lower():
|
if "feat." in name.lower():
|
||||||
name = re.split("feat.", name, flags=re.IGNORECASE)[0].strip()
|
name = re.split("feat.", name, flags=re.IGNORECASE)[0].strip()
|
||||||
if "featuring" in name.lower():
|
if "featuring" in name.lower():
|
||||||
@ -26,18 +25,44 @@ def get_or_create_artist(name: str, mbid: str = None) -> Artist:
|
|||||||
if "&" in name.lower():
|
if "&" in name.lower():
|
||||||
name = re.split("&", name, flags=re.IGNORECASE)[0].strip()
|
name = re.split("&", name, flags=re.IGNORECASE)[0].strip()
|
||||||
|
|
||||||
artist_dict = lookup_artist_from_mb(name)
|
return name
|
||||||
mbid = mbid or artist_dict.get("id", None)
|
|
||||||
|
|
||||||
if mbid:
|
|
||||||
|
# TODO These are depreacted, remove them eventually
|
||||||
|
def get_or_create_artist(name: str, mbid: str = "") -> Artist:
|
||||||
|
"""Get an Artist object from the database.
|
||||||
|
|
||||||
|
Check if an artist with this name or Musicbrainz ID already exists.
|
||||||
|
Otherwise, go lookup artist data from Musicbrainz and create one.
|
||||||
|
|
||||||
|
"""
|
||||||
|
artist = None
|
||||||
|
name = clean_artist_name(name)
|
||||||
|
|
||||||
|
# Check for name/mbid combo, just mbid and then just name
|
||||||
|
artist = Artist.objects.filter(name=name, mbid=mbid).first()
|
||||||
|
if not artist:
|
||||||
artist = Artist.objects.filter(musicbrainz_id=mbid).first()
|
artist = Artist.objects.filter(musicbrainz_id=mbid).first()
|
||||||
|
if not artist:
|
||||||
|
artist = Artist.objects.filter(name=name).first()
|
||||||
|
|
||||||
|
# Does not exist, look it up from Musicbrainz
|
||||||
|
if not artist:
|
||||||
|
artist_dict = lookup_artist_from_mb(name)
|
||||||
|
mbid = mbid or artist_dict.get("id", "")
|
||||||
|
|
||||||
|
if mbid:
|
||||||
|
artist = Artist.objects.filter(musicbrainz_id=mbid).first()
|
||||||
|
|
||||||
if not artist:
|
if not artist:
|
||||||
artist = Artist.objects.create(name=name, musicbrainz_id=mbid)
|
artist = Artist.objects.create(name=name, musicbrainz_id=mbid)
|
||||||
|
# TODO maybe this should be spun off into an async task?
|
||||||
artist.fix_metadata()
|
artist.fix_metadata()
|
||||||
|
|
||||||
return artist
|
return artist
|
||||||
|
|
||||||
|
|
||||||
|
# TODO These are depreacted, remove them eventually
|
||||||
def get_or_create_album(
|
def get_or_create_album(
|
||||||
name: str, artist: Artist, mbid: str = None
|
name: str, artist: Artist, mbid: str = None
|
||||||
) -> Optional[Album]:
|
) -> Optional[Album]:
|
||||||
@ -90,6 +115,7 @@ def get_or_create_album(
|
|||||||
return album
|
return album
|
||||||
|
|
||||||
|
|
||||||
|
# TODO These are depreacted, remove them eventually
|
||||||
def get_or_create_track(post_data: dict, post_keys: dict) -> Track:
|
def get_or_create_track(post_data: dict, post_keys: dict) -> Track:
|
||||||
try:
|
try:
|
||||||
track_run_time_seconds = int(
|
track_run_time_seconds = int(
|
||||||
@ -107,16 +133,12 @@ def get_or_create_track(post_data: dict, post_keys: dict) -> Track:
|
|||||||
track_title = post_data.get(post_keys.get("TRACK_TITLE"), "")
|
track_title = post_data.get(post_keys.get("TRACK_TITLE"), "")
|
||||||
track_mb_id = post_data.get(post_keys.get("TRACK_MB_ID"), "")
|
track_mb_id = post_data.get(post_keys.get("TRACK_MB_ID"), "")
|
||||||
|
|
||||||
artist = get_or_create_artist(
|
artist = Artist.find_or_create(artist_name, artist_mb_id)
|
||||||
artist_name,
|
|
||||||
mbid=artist_mb_id,
|
|
||||||
)
|
|
||||||
album = None
|
album = None
|
||||||
if album_mb_id:
|
# We may get no album ID or title, in which case, skip
|
||||||
album = get_or_create_album(
|
if album_mb_id or album_title:
|
||||||
album_title,
|
album = Album.find_or_create(
|
||||||
artist=artist,
|
album_title, str(artist.name), album_mb_id
|
||||||
mbid=album_mb_id,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
track = None
|
track = None
|
||||||
@ -154,7 +176,7 @@ def get_or_create_track(post_data: dict, post_keys: dict) -> Track:
|
|||||||
return track
|
return track
|
||||||
|
|
||||||
|
|
||||||
def get_or_create_various_artists():
|
def get_or_create_various_artists() -> Artist:
|
||||||
artist = Artist.objects.filter(name="Various Artists").first()
|
artist = Artist.objects.filter(name="Various Artists").first()
|
||||||
if not artist:
|
if not artist:
|
||||||
artist = Artist.objects.create(**VARIOUS_ARTIST_DICT)
|
artist = Artist.objects.create(**VARIOUS_ARTIST_DICT)
|
||||||
|
|||||||
@ -0,0 +1,40 @@
|
|||||||
|
# Generated by Django 4.2.19 on 2025-04-07 17:16
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("podcasts", "0014_alter_podcastepisode_run_time_seconds"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name="podcast",
|
||||||
|
name="google_podcasts_url",
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="podcast",
|
||||||
|
name="dead_date",
|
||||||
|
field=models.DateField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="podcast",
|
||||||
|
name="itunes_id",
|
||||||
|
field=models.TextField(blank=True, max_length=15, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="podcast",
|
||||||
|
name="null",
|
||||||
|
field=models.CharField(
|
||||||
|
default="", max_length=150, verbose_name="blank"
|
||||||
|
),
|
||||||
|
preserve_default=False,
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="podcast",
|
||||||
|
name="site_link",
|
||||||
|
field=models.URLField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
]
|
||||||
28
vrobbler/apps/podcasts/migrations/0016_podcast_genre.py
Normal file
28
vrobbler/apps/podcasts/migrations/0016_podcast_genre.py
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
# Generated by Django 4.2.19 on 2025-04-07 17:18
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
import taggit.managers
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("scrobbles", "0068_scrobble_paper_alter_scrobble_media_type"),
|
||||||
|
(
|
||||||
|
"podcasts",
|
||||||
|
"0015_remove_podcast_google_podcasts_url_podcast_dead_date_and_more",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="podcast",
|
||||||
|
name="genre",
|
||||||
|
field=taggit.managers.TaggableManager(
|
||||||
|
help_text="A comma-separated list of tags.",
|
||||||
|
through="scrobbles.ObjectWithGenres",
|
||||||
|
to="scrobbles.Genre",
|
||||||
|
verbose_name="Tags",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 4.2.19 on 2025-04-07 17:29
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("podcasts", "0016_podcast_genre"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="podcast",
|
||||||
|
name="podcastindex_id",
|
||||||
|
field=models.CharField(blank=True, max_length=100, null=True),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -1,5 +1,4 @@
|
|||||||
import logging
|
import logging
|
||||||
from typing import Dict, Optional
|
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
@ -10,8 +9,13 @@ from django.db import models
|
|||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django_extensions.db.models import TimeStampedModel
|
from django_extensions.db.models import TimeStampedModel
|
||||||
from podcasts.scrapers import scrape_data_from_google_podcasts
|
from podcasts.sources.podcastindex import lookup_podcast_from_podcastindex
|
||||||
from scrobbles.mixins import ScrobblableConstants, ScrobblableMixin
|
from scrobbles.mixins import (
|
||||||
|
ObjectWithGenres,
|
||||||
|
ScrobblableConstants,
|
||||||
|
ScrobblableMixin,
|
||||||
|
)
|
||||||
|
from taggit.managers import TaggableManager
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
BNULL = {"blank": True, "null": True}
|
BNULL = {"blank": True, "null": True}
|
||||||
@ -24,6 +28,13 @@ class Producer(TimeStampedModel):
|
|||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{self.name}"
|
return f"{self.name}"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def find_or_create(cls, name):
|
||||||
|
producer = cls.objects.filter(name__iexact=name).first()
|
||||||
|
if not producer:
|
||||||
|
producer = cls.objects.create(name=name)
|
||||||
|
return producer
|
||||||
|
|
||||||
|
|
||||||
class Podcast(TimeStampedModel):
|
class Podcast(TimeStampedModel):
|
||||||
name = models.CharField(max_length=255)
|
name = models.CharField(max_length=255)
|
||||||
@ -31,11 +42,17 @@ class Podcast(TimeStampedModel):
|
|||||||
producer = models.ForeignKey(
|
producer = models.ForeignKey(
|
||||||
Producer, on_delete=models.DO_NOTHING, **BNULL
|
Producer, on_delete=models.DO_NOTHING, **BNULL
|
||||||
)
|
)
|
||||||
|
podcastindex_id = models.CharField(max_length=100, **BNULL)
|
||||||
|
owner = models.CharField(max_length=150, *BNULL)
|
||||||
description = models.TextField(**BNULL)
|
description = models.TextField(**BNULL)
|
||||||
active = models.BooleanField(default=True)
|
active = models.BooleanField(default=True)
|
||||||
feed_url = models.URLField(**BNULL)
|
feed_url = models.URLField(**BNULL)
|
||||||
google_podcasts_url = models.URLField(**BNULL)
|
site_link = models.URLField(**BNULL)
|
||||||
|
description = models.TextField(**BNULL)
|
||||||
cover_image = models.ImageField(upload_to="podcasts/covers/", **BNULL)
|
cover_image = models.ImageField(upload_to="podcasts/covers/", **BNULL)
|
||||||
|
itunes_id = models.TextField(max_length=15, **BNULL)
|
||||||
|
dead_date = models.DateField(**BNULL)
|
||||||
|
genre = TaggableManager(through=ObjectWithGenres)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{self.name}"
|
return f"{self.name}"
|
||||||
@ -49,32 +66,43 @@ class Podcast(TimeStampedModel):
|
|||||||
user=user, podcast_episode__podcast=self
|
user=user, podcast_episode__podcast=self
|
||||||
).order_by("-timestamp")
|
).order_by("-timestamp")
|
||||||
|
|
||||||
def scrape_google_podcasts(self, force=False):
|
@property
|
||||||
podcast_dict = {}
|
def itunes_link(self) -> str:
|
||||||
if not self.cover_image or force:
|
if not self.itunes_id:
|
||||||
podcast_dict = scrape_data_from_google_podcasts(self.name)
|
return ""
|
||||||
if podcast_dict:
|
return f"https://podcasts.apple.com/us/podcast/id{self.itunes_id}"
|
||||||
if not self.producer:
|
|
||||||
self.producer, created = Producer.objects.get_or_create(
|
def fix_metadata(self, force=False):
|
||||||
name=podcast_dict["producer"]
|
if self.podcastindex_id and not force:
|
||||||
)
|
logger.warning(
|
||||||
self.description = podcast_dict.get("description")
|
"Podcast already has PodcastIndex ID, use force=True to overwrite"
|
||||||
self.google_podcasts_url = podcast_dict.get("google_url")
|
)
|
||||||
self.save(
|
return
|
||||||
update_fields=[
|
|
||||||
"description",
|
podcast_dict = lookup_podcast_from_podcastindex(self.name)
|
||||||
"producer",
|
|
||||||
"google_podcasts_url",
|
if not podcast_dict:
|
||||||
]
|
logger.info(
|
||||||
)
|
"No podcast data found from PodcastIndex. Are credentials setup?"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
genres = podcast_dict.pop("genres")
|
||||||
|
if genres:
|
||||||
|
self.genre.add(*genres)
|
||||||
|
|
||||||
|
cover_url = podcast_dict.pop("image_url")
|
||||||
|
|
||||||
cover_url = podcast_dict.get("image_url")
|
|
||||||
if (not self.cover_image or force) and cover_url:
|
if (not self.cover_image or force) and cover_url:
|
||||||
r = requests.get(cover_url)
|
r = requests.get(cover_url)
|
||||||
if r.status_code == 200:
|
if r.status_code == 200:
|
||||||
fname = f"{self.name}_{self.uuid}.jpg"
|
fname = f"{self.name}_{self.uuid}.jpg"
|
||||||
self.cover_image.save(fname, ContentFile(r.content), save=True)
|
self.cover_image.save(fname, ContentFile(r.content), save=True)
|
||||||
|
|
||||||
|
for attr, value in podcast_dict.items():
|
||||||
|
setattr(self, attr, value)
|
||||||
|
self.save()
|
||||||
|
|
||||||
|
|
||||||
class PodcastEpisode(ScrobblableMixin):
|
class PodcastEpisode(ScrobblableMixin):
|
||||||
COMPLETION_PERCENT = getattr(settings, "PODCAST_COMPLETION_PERCENT", 90)
|
COMPLETION_PERCENT = getattr(settings, "PODCAST_COMPLETION_PERCENT", 90)
|
||||||
@ -84,6 +112,11 @@ class PodcastEpisode(ScrobblableMixin):
|
|||||||
pub_date = models.DateField(**BNULL)
|
pub_date = models.DateField(**BNULL)
|
||||||
mopidy_uri = models.CharField(max_length=255, **BNULL)
|
mopidy_uri = models.CharField(max_length=255, **BNULL)
|
||||||
|
|
||||||
|
def get_absolute_url(self):
|
||||||
|
return reverse(
|
||||||
|
"podcasts:podcast_detail", kwargs={"slug": self.podcast.uuid}
|
||||||
|
)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{self.title}"
|
return f"{self.title}"
|
||||||
|
|
||||||
@ -108,42 +141,45 @@ class PodcastEpisode(ScrobblableMixin):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def find_or_create(
|
def find_or_create(
|
||||||
cls, podcast_dict: Dict, producer_dict: Dict, episode_dict: Dict
|
cls,
|
||||||
) -> Optional["Episode"]:
|
title: str,
|
||||||
|
podcast_name: str,
|
||||||
|
pub_date: str,
|
||||||
|
number: int = 0,
|
||||||
|
mopidy_uri: str = "",
|
||||||
|
producer_name: str = "",
|
||||||
|
run_time_seconds: int = 1800,
|
||||||
|
enrich: bool = True,
|
||||||
|
) -> "PodcastEpisode":
|
||||||
"""Given a data dict from Mopidy, finds or creates a podcast and
|
"""Given a data dict from Mopidy, finds or creates a podcast and
|
||||||
producer before saving the epsiode so it can be scrobbled.
|
producer before saving the epsiode so it can be scrobbled.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
if not podcast_dict.get("name"):
|
|
||||||
logger.warning(f"No name from source for podcast, not scrobbling")
|
|
||||||
return
|
|
||||||
|
|
||||||
producer = None
|
producer = None
|
||||||
if producer_dict.get("name"):
|
if producer_name:
|
||||||
producer, producer_created = Producer.objects.get_or_create(
|
producer = Producer.find_or_create(producer_name)
|
||||||
**producer_dict
|
|
||||||
|
podcast = Podcast.objects.filter(
|
||||||
|
name__iexact=podcast_name,
|
||||||
|
).first()
|
||||||
|
if not podcast:
|
||||||
|
podcast = Podcast.objects.create(
|
||||||
|
name=podcast_name, producer=producer
|
||||||
)
|
)
|
||||||
if producer_created:
|
if enrich:
|
||||||
logger.debug(f"Created new producer {producer}")
|
podcast.fix_metadata()
|
||||||
else:
|
|
||||||
logger.debug(f"Found producer {producer}")
|
|
||||||
|
|
||||||
if producer:
|
episode = cls.objects.filter(
|
||||||
podcast_dict["producer_id"] = producer.id
|
title__iexact=title, podcast=podcast
|
||||||
podcast, podcast_created = Podcast.objects.get_or_create(
|
).first()
|
||||||
**podcast_dict
|
if not episode:
|
||||||
)
|
episode = cls.objects.create(
|
||||||
if podcast_created:
|
title=title,
|
||||||
logger.debug(f"Created new podcast {podcast}")
|
podcast=podcast,
|
||||||
else:
|
run_time_seconds=run_time_seconds,
|
||||||
logger.debug(f"Found podcast {podcast}")
|
number=number,
|
||||||
|
pub_date=pub_date,
|
||||||
episode_dict["podcast_id"] = podcast.id
|
mopidy_uri=mopidy_uri,
|
||||||
|
)
|
||||||
episode, created = cls.objects.get_or_create(**episode_dict)
|
|
||||||
if created:
|
|
||||||
logger.debug(f"Created new episode: {episode}")
|
|
||||||
else:
|
|
||||||
logger.debug(f"Found episode {episode}")
|
|
||||||
|
|
||||||
return episode
|
return episode
|
||||||
|
|||||||
75
vrobbler/apps/podcasts/sources/podcastindex.py
Normal file
75
vrobbler/apps/podcasts/sources/podcastindex.py
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
import hashlib
|
||||||
|
import time
|
||||||
|
|
||||||
|
import pytz
|
||||||
|
import requests
|
||||||
|
from django.conf import settings
|
||||||
|
from django.utils import timezone
|
||||||
|
from scrobbles.utils import timestamp_user_tz_to_utc
|
||||||
|
|
||||||
|
PODCASTINDEX_API_KEY = getattr(settings, "PODCASTINDEX_API_KEY")
|
||||||
|
PODCASTINDEX_API_SECRET = getattr(settings, "PODCASTINDEX_API_SECRET")
|
||||||
|
|
||||||
|
|
||||||
|
def get_auth_headers():
|
||||||
|
now = int(time.time())
|
||||||
|
hash_data = hashlib.sha1(
|
||||||
|
(PODCASTINDEX_API_KEY + PODCASTINDEX_API_SECRET + str(now)).encode(
|
||||||
|
"utf-8"
|
||||||
|
)
|
||||||
|
).hexdigest()
|
||||||
|
|
||||||
|
return {
|
||||||
|
"User-Agent": "MyPodcastApp/1.0",
|
||||||
|
"X-Auth-Date": str(now),
|
||||||
|
"X-Auth-Key": PODCASTINDEX_API_KEY,
|
||||||
|
"Authorization": hash_data,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def lookup_podcast_from_podcastindex(
|
||||||
|
podcast_name: str, dump_raw_response: bool = False
|
||||||
|
) -> dict:
|
||||||
|
url = "https://api.podcastindex.org/api/1.0/search/byterm"
|
||||||
|
headers = get_auth_headers()
|
||||||
|
params = {"q": podcast_name}
|
||||||
|
|
||||||
|
response = requests.get(url, headers=headers, params=params)
|
||||||
|
|
||||||
|
if response.status_code == 200:
|
||||||
|
data = response.json()
|
||||||
|
if dump_raw_response:
|
||||||
|
return data.get("feeds")
|
||||||
|
if data.get("feeds"):
|
||||||
|
try:
|
||||||
|
top_feed_dict = data["feeds"][0]
|
||||||
|
|
||||||
|
newest_episode_date = timestamp_user_tz_to_utc(
|
||||||
|
top_feed_dict.get("newestItemPubdate"), pytz.UTC
|
||||||
|
)
|
||||||
|
days_since_last_episode = ()
|
||||||
|
dead_date = None
|
||||||
|
if (timezone.now() - newest_episode_date).days > 180:
|
||||||
|
dead_date = newest_episode_date
|
||||||
|
|
||||||
|
return {
|
||||||
|
"podcastindex_id": top_feed_dict.get("id"),
|
||||||
|
"title": top_feed_dict.get("title"),
|
||||||
|
"site_link": top_feed_dict.get("link"),
|
||||||
|
"description": top_feed_dict.get("description"),
|
||||||
|
"owner": top_feed_dict.get("ownerName"),
|
||||||
|
"image_url": top_feed_dict.get("artwork"),
|
||||||
|
"feed_url": top_feed_dict.get("url"),
|
||||||
|
"itunes_id": top_feed_dict.get("itunesId"),
|
||||||
|
"genres": list(top_feed_dict.get("categories").values()),
|
||||||
|
"dead_date": dead_date,
|
||||||
|
}
|
||||||
|
except IndexError:
|
||||||
|
return {}
|
||||||
|
else:
|
||||||
|
print("No podcasts found.")
|
||||||
|
return {}
|
||||||
|
else:
|
||||||
|
print("Failed to fetch data:", response.status_code, response.text)
|
||||||
|
return {}
|
||||||
@ -0,0 +1,601 @@
|
|||||||
|
# Generated by Django 4.2.19 on 2025-04-07 17:16
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("profiles", "0022_userprofile_task_context_tags_str_and_more"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="userprofile",
|
||||||
|
name="timezone",
|
||||||
|
field=models.CharField(
|
||||||
|
choices=[
|
||||||
|
("Pacific/Midway", "(GMT-1100) Pacific/Midway"),
|
||||||
|
("Pacific/Niue", "(GMT-1100) Pacific/Niue"),
|
||||||
|
("Pacific/Pago_Pago", "(GMT-1100) Pacific/Pago_Pago"),
|
||||||
|
("Pacific/Honolulu", "(GMT-1000) Pacific/Honolulu"),
|
||||||
|
("Pacific/Rarotonga", "(GMT-1000) Pacific/Rarotonga"),
|
||||||
|
("Pacific/Tahiti", "(GMT-1000) Pacific/Tahiti"),
|
||||||
|
("US/Hawaii", "(GMT-1000) US/Hawaii"),
|
||||||
|
("Pacific/Marquesas", "(GMT-0930) Pacific/Marquesas"),
|
||||||
|
("America/Adak", "(GMT-0900) America/Adak"),
|
||||||
|
("Pacific/Gambier", "(GMT-0900) Pacific/Gambier"),
|
||||||
|
("America/Anchorage", "(GMT-0800) America/Anchorage"),
|
||||||
|
("America/Juneau", "(GMT-0800) America/Juneau"),
|
||||||
|
("America/Metlakatla", "(GMT-0800) America/Metlakatla"),
|
||||||
|
("America/Nome", "(GMT-0800) America/Nome"),
|
||||||
|
("America/Sitka", "(GMT-0800) America/Sitka"),
|
||||||
|
("America/Yakutat", "(GMT-0800) America/Yakutat"),
|
||||||
|
("Pacific/Pitcairn", "(GMT-0800) Pacific/Pitcairn"),
|
||||||
|
("US/Alaska", "(GMT-0800) US/Alaska"),
|
||||||
|
("America/Creston", "(GMT-0700) America/Creston"),
|
||||||
|
("America/Dawson", "(GMT-0700) America/Dawson"),
|
||||||
|
(
|
||||||
|
"America/Dawson_Creek",
|
||||||
|
"(GMT-0700) America/Dawson_Creek",
|
||||||
|
),
|
||||||
|
("America/Fort_Nelson", "(GMT-0700) America/Fort_Nelson"),
|
||||||
|
("America/Hermosillo", "(GMT-0700) America/Hermosillo"),
|
||||||
|
("America/Los_Angeles", "(GMT-0700) America/Los_Angeles"),
|
||||||
|
("America/Mazatlan", "(GMT-0700) America/Mazatlan"),
|
||||||
|
("America/Phoenix", "(GMT-0700) America/Phoenix"),
|
||||||
|
("America/Tijuana", "(GMT-0700) America/Tijuana"),
|
||||||
|
("America/Vancouver", "(GMT-0700) America/Vancouver"),
|
||||||
|
("America/Whitehorse", "(GMT-0700) America/Whitehorse"),
|
||||||
|
("Canada/Pacific", "(GMT-0700) Canada/Pacific"),
|
||||||
|
("US/Arizona", "(GMT-0700) US/Arizona"),
|
||||||
|
("US/Pacific", "(GMT-0700) US/Pacific"),
|
||||||
|
(
|
||||||
|
"America/Bahia_Banderas",
|
||||||
|
"(GMT-0600) America/Bahia_Banderas",
|
||||||
|
),
|
||||||
|
("America/Belize", "(GMT-0600) America/Belize"),
|
||||||
|
("America/Boise", "(GMT-0600) America/Boise"),
|
||||||
|
(
|
||||||
|
"America/Cambridge_Bay",
|
||||||
|
"(GMT-0600) America/Cambridge_Bay",
|
||||||
|
),
|
||||||
|
("America/Chihuahua", "(GMT-0600) America/Chihuahua"),
|
||||||
|
(
|
||||||
|
"America/Ciudad_Juarez",
|
||||||
|
"(GMT-0600) America/Ciudad_Juarez",
|
||||||
|
),
|
||||||
|
("America/Costa_Rica", "(GMT-0600) America/Costa_Rica"),
|
||||||
|
("America/Denver", "(GMT-0600) America/Denver"),
|
||||||
|
("America/Edmonton", "(GMT-0600) America/Edmonton"),
|
||||||
|
("America/El_Salvador", "(GMT-0600) America/El_Salvador"),
|
||||||
|
("America/Guatemala", "(GMT-0600) America/Guatemala"),
|
||||||
|
("America/Inuvik", "(GMT-0600) America/Inuvik"),
|
||||||
|
("America/Managua", "(GMT-0600) America/Managua"),
|
||||||
|
("America/Merida", "(GMT-0600) America/Merida"),
|
||||||
|
("America/Mexico_City", "(GMT-0600) America/Mexico_City"),
|
||||||
|
("America/Monterrey", "(GMT-0600) America/Monterrey"),
|
||||||
|
("America/Regina", "(GMT-0600) America/Regina"),
|
||||||
|
(
|
||||||
|
"America/Swift_Current",
|
||||||
|
"(GMT-0600) America/Swift_Current",
|
||||||
|
),
|
||||||
|
("America/Tegucigalpa", "(GMT-0600) America/Tegucigalpa"),
|
||||||
|
("America/Yellowknife", "(GMT-0600) America/Yellowknife"),
|
||||||
|
("Canada/Mountain", "(GMT-0600) Canada/Mountain"),
|
||||||
|
("Pacific/Easter", "(GMT-0600) Pacific/Easter"),
|
||||||
|
("Pacific/Galapagos", "(GMT-0600) Pacific/Galapagos"),
|
||||||
|
("US/Mountain", "(GMT-0600) US/Mountain"),
|
||||||
|
("America/Atikokan", "(GMT-0500) America/Atikokan"),
|
||||||
|
("America/Bogota", "(GMT-0500) America/Bogota"),
|
||||||
|
("America/Cancun", "(GMT-0500) America/Cancun"),
|
||||||
|
("America/Cayman", "(GMT-0500) America/Cayman"),
|
||||||
|
("America/Chicago", "(GMT-0500) America/Chicago"),
|
||||||
|
("America/Eirunepe", "(GMT-0500) America/Eirunepe"),
|
||||||
|
("America/Guayaquil", "(GMT-0500) America/Guayaquil"),
|
||||||
|
(
|
||||||
|
"America/Indiana/Knox",
|
||||||
|
"(GMT-0500) America/Indiana/Knox",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"America/Indiana/Tell_City",
|
||||||
|
"(GMT-0500) America/Indiana/Tell_City",
|
||||||
|
),
|
||||||
|
("America/Jamaica", "(GMT-0500) America/Jamaica"),
|
||||||
|
("America/Lima", "(GMT-0500) America/Lima"),
|
||||||
|
("America/Matamoros", "(GMT-0500) America/Matamoros"),
|
||||||
|
("America/Menominee", "(GMT-0500) America/Menominee"),
|
||||||
|
(
|
||||||
|
"America/North_Dakota/Beulah",
|
||||||
|
"(GMT-0500) America/North_Dakota/Beulah",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"America/North_Dakota/Center",
|
||||||
|
"(GMT-0500) America/North_Dakota/Center",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"America/North_Dakota/New_Salem",
|
||||||
|
"(GMT-0500) America/North_Dakota/New_Salem",
|
||||||
|
),
|
||||||
|
("America/Ojinaga", "(GMT-0500) America/Ojinaga"),
|
||||||
|
("America/Panama", "(GMT-0500) America/Panama"),
|
||||||
|
(
|
||||||
|
"America/Rankin_Inlet",
|
||||||
|
"(GMT-0500) America/Rankin_Inlet",
|
||||||
|
),
|
||||||
|
("America/Resolute", "(GMT-0500) America/Resolute"),
|
||||||
|
("America/Rio_Branco", "(GMT-0500) America/Rio_Branco"),
|
||||||
|
("America/Winnipeg", "(GMT-0500) America/Winnipeg"),
|
||||||
|
("Canada/Central", "(GMT-0500) Canada/Central"),
|
||||||
|
("US/Central", "(GMT-0500) US/Central"),
|
||||||
|
("America/Anguilla", "(GMT-0400) America/Anguilla"),
|
||||||
|
("America/Antigua", "(GMT-0400) America/Antigua"),
|
||||||
|
("America/Aruba", "(GMT-0400) America/Aruba"),
|
||||||
|
("America/Asuncion", "(GMT-0400) America/Asuncion"),
|
||||||
|
("America/Barbados", "(GMT-0400) America/Barbados"),
|
||||||
|
(
|
||||||
|
"America/Blanc-Sablon",
|
||||||
|
"(GMT-0400) America/Blanc-Sablon",
|
||||||
|
),
|
||||||
|
("America/Boa_Vista", "(GMT-0400) America/Boa_Vista"),
|
||||||
|
(
|
||||||
|
"America/Campo_Grande",
|
||||||
|
"(GMT-0400) America/Campo_Grande",
|
||||||
|
),
|
||||||
|
("America/Caracas", "(GMT-0400) America/Caracas"),
|
||||||
|
("America/Cuiaba", "(GMT-0400) America/Cuiaba"),
|
||||||
|
("America/Curacao", "(GMT-0400) America/Curacao"),
|
||||||
|
("America/Detroit", "(GMT-0400) America/Detroit"),
|
||||||
|
("America/Dominica", "(GMT-0400) America/Dominica"),
|
||||||
|
("America/Grand_Turk", "(GMT-0400) America/Grand_Turk"),
|
||||||
|
("America/Grenada", "(GMT-0400) America/Grenada"),
|
||||||
|
("America/Guadeloupe", "(GMT-0400) America/Guadeloupe"),
|
||||||
|
("America/Guyana", "(GMT-0400) America/Guyana"),
|
||||||
|
("America/Havana", "(GMT-0400) America/Havana"),
|
||||||
|
(
|
||||||
|
"America/Indiana/Indianapolis",
|
||||||
|
"(GMT-0400) America/Indiana/Indianapolis",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"America/Indiana/Marengo",
|
||||||
|
"(GMT-0400) America/Indiana/Marengo",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"America/Indiana/Petersburg",
|
||||||
|
"(GMT-0400) America/Indiana/Petersburg",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"America/Indiana/Vevay",
|
||||||
|
"(GMT-0400) America/Indiana/Vevay",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"America/Indiana/Vincennes",
|
||||||
|
"(GMT-0400) America/Indiana/Vincennes",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"America/Indiana/Winamac",
|
||||||
|
"(GMT-0400) America/Indiana/Winamac",
|
||||||
|
),
|
||||||
|
("America/Iqaluit", "(GMT-0400) America/Iqaluit"),
|
||||||
|
(
|
||||||
|
"America/Kentucky/Louisville",
|
||||||
|
"(GMT-0400) America/Kentucky/Louisville",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"America/Kentucky/Monticello",
|
||||||
|
"(GMT-0400) America/Kentucky/Monticello",
|
||||||
|
),
|
||||||
|
("America/Kralendijk", "(GMT-0400) America/Kralendijk"),
|
||||||
|
("America/La_Paz", "(GMT-0400) America/La_Paz"),
|
||||||
|
(
|
||||||
|
"America/Lower_Princes",
|
||||||
|
"(GMT-0400) America/Lower_Princes",
|
||||||
|
),
|
||||||
|
("America/Manaus", "(GMT-0400) America/Manaus"),
|
||||||
|
("America/Marigot", "(GMT-0400) America/Marigot"),
|
||||||
|
("America/Martinique", "(GMT-0400) America/Martinique"),
|
||||||
|
("America/Montserrat", "(GMT-0400) America/Montserrat"),
|
||||||
|
("America/Nassau", "(GMT-0400) America/Nassau"),
|
||||||
|
("America/New_York", "(GMT-0400) America/New_York"),
|
||||||
|
(
|
||||||
|
"America/Port-au-Prince",
|
||||||
|
"(GMT-0400) America/Port-au-Prince",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"America/Port_of_Spain",
|
||||||
|
"(GMT-0400) America/Port_of_Spain",
|
||||||
|
),
|
||||||
|
("America/Porto_Velho", "(GMT-0400) America/Porto_Velho"),
|
||||||
|
("America/Puerto_Rico", "(GMT-0400) America/Puerto_Rico"),
|
||||||
|
("America/Santiago", "(GMT-0400) America/Santiago"),
|
||||||
|
(
|
||||||
|
"America/Santo_Domingo",
|
||||||
|
"(GMT-0400) America/Santo_Domingo",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"America/St_Barthelemy",
|
||||||
|
"(GMT-0400) America/St_Barthelemy",
|
||||||
|
),
|
||||||
|
("America/St_Kitts", "(GMT-0400) America/St_Kitts"),
|
||||||
|
("America/St_Lucia", "(GMT-0400) America/St_Lucia"),
|
||||||
|
("America/St_Thomas", "(GMT-0400) America/St_Thomas"),
|
||||||
|
("America/St_Vincent", "(GMT-0400) America/St_Vincent"),
|
||||||
|
("America/Toronto", "(GMT-0400) America/Toronto"),
|
||||||
|
("America/Tortola", "(GMT-0400) America/Tortola"),
|
||||||
|
("Canada/Eastern", "(GMT-0400) Canada/Eastern"),
|
||||||
|
("US/Eastern", "(GMT-0400) US/Eastern"),
|
||||||
|
("America/Araguaina", "(GMT-0300) America/Araguaina"),
|
||||||
|
(
|
||||||
|
"America/Argentina/Buenos_Aires",
|
||||||
|
"(GMT-0300) America/Argentina/Buenos_Aires",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"America/Argentina/Catamarca",
|
||||||
|
"(GMT-0300) America/Argentina/Catamarca",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"America/Argentina/Cordoba",
|
||||||
|
"(GMT-0300) America/Argentina/Cordoba",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"America/Argentina/Jujuy",
|
||||||
|
"(GMT-0300) America/Argentina/Jujuy",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"America/Argentina/La_Rioja",
|
||||||
|
"(GMT-0300) America/Argentina/La_Rioja",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"America/Argentina/Mendoza",
|
||||||
|
"(GMT-0300) America/Argentina/Mendoza",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"America/Argentina/Rio_Gallegos",
|
||||||
|
"(GMT-0300) America/Argentina/Rio_Gallegos",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"America/Argentina/Salta",
|
||||||
|
"(GMT-0300) America/Argentina/Salta",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"America/Argentina/San_Juan",
|
||||||
|
"(GMT-0300) America/Argentina/San_Juan",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"America/Argentina/San_Luis",
|
||||||
|
"(GMT-0300) America/Argentina/San_Luis",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"America/Argentina/Tucuman",
|
||||||
|
"(GMT-0300) America/Argentina/Tucuman",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"America/Argentina/Ushuaia",
|
||||||
|
"(GMT-0300) America/Argentina/Ushuaia",
|
||||||
|
),
|
||||||
|
("America/Bahia", "(GMT-0300) America/Bahia"),
|
||||||
|
("America/Belem", "(GMT-0300) America/Belem"),
|
||||||
|
("America/Cayenne", "(GMT-0300) America/Cayenne"),
|
||||||
|
("America/Fortaleza", "(GMT-0300) America/Fortaleza"),
|
||||||
|
("America/Glace_Bay", "(GMT-0300) America/Glace_Bay"),
|
||||||
|
("America/Goose_Bay", "(GMT-0300) America/Goose_Bay"),
|
||||||
|
("America/Halifax", "(GMT-0300) America/Halifax"),
|
||||||
|
("America/Maceio", "(GMT-0300) America/Maceio"),
|
||||||
|
("America/Moncton", "(GMT-0300) America/Moncton"),
|
||||||
|
("America/Montevideo", "(GMT-0300) America/Montevideo"),
|
||||||
|
("America/Paramaribo", "(GMT-0300) America/Paramaribo"),
|
||||||
|
(
|
||||||
|
"America/Punta_Arenas",
|
||||||
|
"(GMT-0300) America/Punta_Arenas",
|
||||||
|
),
|
||||||
|
("America/Recife", "(GMT-0300) America/Recife"),
|
||||||
|
("America/Santarem", "(GMT-0300) America/Santarem"),
|
||||||
|
("America/Sao_Paulo", "(GMT-0300) America/Sao_Paulo"),
|
||||||
|
("America/Thule", "(GMT-0300) America/Thule"),
|
||||||
|
("Antarctica/Palmer", "(GMT-0300) Antarctica/Palmer"),
|
||||||
|
("Antarctica/Rothera", "(GMT-0300) Antarctica/Rothera"),
|
||||||
|
("Atlantic/Bermuda", "(GMT-0300) Atlantic/Bermuda"),
|
||||||
|
("Atlantic/Stanley", "(GMT-0300) Atlantic/Stanley"),
|
||||||
|
("Canada/Atlantic", "(GMT-0300) Canada/Atlantic"),
|
||||||
|
("America/St_Johns", "(GMT-0230) America/St_Johns"),
|
||||||
|
("Canada/Newfoundland", "(GMT-0230) Canada/Newfoundland"),
|
||||||
|
("America/Miquelon", "(GMT-0200) America/Miquelon"),
|
||||||
|
("America/Noronha", "(GMT-0200) America/Noronha"),
|
||||||
|
("America/Nuuk", "(GMT-0200) America/Nuuk"),
|
||||||
|
(
|
||||||
|
"Atlantic/South_Georgia",
|
||||||
|
"(GMT-0200) Atlantic/South_Georgia",
|
||||||
|
),
|
||||||
|
("Atlantic/Cape_Verde", "(GMT-0100) Atlantic/Cape_Verde"),
|
||||||
|
("Africa/Abidjan", "(GMT+0000) Africa/Abidjan"),
|
||||||
|
("Africa/Accra", "(GMT+0000) Africa/Accra"),
|
||||||
|
("Africa/Bamako", "(GMT+0000) Africa/Bamako"),
|
||||||
|
("Africa/Banjul", "(GMT+0000) Africa/Banjul"),
|
||||||
|
("Africa/Bissau", "(GMT+0000) Africa/Bissau"),
|
||||||
|
("Africa/Conakry", "(GMT+0000) Africa/Conakry"),
|
||||||
|
("Africa/Dakar", "(GMT+0000) Africa/Dakar"),
|
||||||
|
("Africa/Freetown", "(GMT+0000) Africa/Freetown"),
|
||||||
|
("Africa/Lome", "(GMT+0000) Africa/Lome"),
|
||||||
|
("Africa/Monrovia", "(GMT+0000) Africa/Monrovia"),
|
||||||
|
("Africa/Nouakchott", "(GMT+0000) Africa/Nouakchott"),
|
||||||
|
("Africa/Ouagadougou", "(GMT+0000) Africa/Ouagadougou"),
|
||||||
|
("Africa/Sao_Tome", "(GMT+0000) Africa/Sao_Tome"),
|
||||||
|
(
|
||||||
|
"America/Danmarkshavn",
|
||||||
|
"(GMT+0000) America/Danmarkshavn",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"America/Scoresbysund",
|
||||||
|
"(GMT+0000) America/Scoresbysund",
|
||||||
|
),
|
||||||
|
("Atlantic/Azores", "(GMT+0000) Atlantic/Azores"),
|
||||||
|
("Atlantic/Reykjavik", "(GMT+0000) Atlantic/Reykjavik"),
|
||||||
|
("Atlantic/St_Helena", "(GMT+0000) Atlantic/St_Helena"),
|
||||||
|
("GMT", "(GMT+0000) GMT"),
|
||||||
|
("UTC", "(GMT+0000) UTC"),
|
||||||
|
("Africa/Algiers", "(GMT+0100) Africa/Algiers"),
|
||||||
|
("Africa/Bangui", "(GMT+0100) Africa/Bangui"),
|
||||||
|
("Africa/Brazzaville", "(GMT+0100) Africa/Brazzaville"),
|
||||||
|
("Africa/Casablanca", "(GMT+0100) Africa/Casablanca"),
|
||||||
|
("Africa/Douala", "(GMT+0100) Africa/Douala"),
|
||||||
|
("Africa/El_Aaiun", "(GMT+0100) Africa/El_Aaiun"),
|
||||||
|
("Africa/Kinshasa", "(GMT+0100) Africa/Kinshasa"),
|
||||||
|
("Africa/Lagos", "(GMT+0100) Africa/Lagos"),
|
||||||
|
("Africa/Libreville", "(GMT+0100) Africa/Libreville"),
|
||||||
|
("Africa/Luanda", "(GMT+0100) Africa/Luanda"),
|
||||||
|
("Africa/Malabo", "(GMT+0100) Africa/Malabo"),
|
||||||
|
("Africa/Ndjamena", "(GMT+0100) Africa/Ndjamena"),
|
||||||
|
("Africa/Niamey", "(GMT+0100) Africa/Niamey"),
|
||||||
|
("Africa/Porto-Novo", "(GMT+0100) Africa/Porto-Novo"),
|
||||||
|
("Africa/Tunis", "(GMT+0100) Africa/Tunis"),
|
||||||
|
("Atlantic/Canary", "(GMT+0100) Atlantic/Canary"),
|
||||||
|
("Atlantic/Faroe", "(GMT+0100) Atlantic/Faroe"),
|
||||||
|
("Atlantic/Madeira", "(GMT+0100) Atlantic/Madeira"),
|
||||||
|
("Europe/Dublin", "(GMT+0100) Europe/Dublin"),
|
||||||
|
("Europe/Guernsey", "(GMT+0100) Europe/Guernsey"),
|
||||||
|
("Europe/Isle_of_Man", "(GMT+0100) Europe/Isle_of_Man"),
|
||||||
|
("Europe/Jersey", "(GMT+0100) Europe/Jersey"),
|
||||||
|
("Europe/Lisbon", "(GMT+0100) Europe/Lisbon"),
|
||||||
|
("Europe/London", "(GMT+0100) Europe/London"),
|
||||||
|
("Africa/Blantyre", "(GMT+0200) Africa/Blantyre"),
|
||||||
|
("Africa/Bujumbura", "(GMT+0200) Africa/Bujumbura"),
|
||||||
|
("Africa/Cairo", "(GMT+0200) Africa/Cairo"),
|
||||||
|
("Africa/Ceuta", "(GMT+0200) Africa/Ceuta"),
|
||||||
|
("Africa/Gaborone", "(GMT+0200) Africa/Gaborone"),
|
||||||
|
("Africa/Harare", "(GMT+0200) Africa/Harare"),
|
||||||
|
("Africa/Johannesburg", "(GMT+0200) Africa/Johannesburg"),
|
||||||
|
("Africa/Juba", "(GMT+0200) Africa/Juba"),
|
||||||
|
("Africa/Khartoum", "(GMT+0200) Africa/Khartoum"),
|
||||||
|
("Africa/Kigali", "(GMT+0200) Africa/Kigali"),
|
||||||
|
("Africa/Lubumbashi", "(GMT+0200) Africa/Lubumbashi"),
|
||||||
|
("Africa/Lusaka", "(GMT+0200) Africa/Lusaka"),
|
||||||
|
("Africa/Maputo", "(GMT+0200) Africa/Maputo"),
|
||||||
|
("Africa/Maseru", "(GMT+0200) Africa/Maseru"),
|
||||||
|
("Africa/Mbabane", "(GMT+0200) Africa/Mbabane"),
|
||||||
|
("Africa/Tripoli", "(GMT+0200) Africa/Tripoli"),
|
||||||
|
("Africa/Windhoek", "(GMT+0200) Africa/Windhoek"),
|
||||||
|
("Antarctica/Troll", "(GMT+0200) Antarctica/Troll"),
|
||||||
|
("Arctic/Longyearbyen", "(GMT+0200) Arctic/Longyearbyen"),
|
||||||
|
("Europe/Amsterdam", "(GMT+0200) Europe/Amsterdam"),
|
||||||
|
("Europe/Andorra", "(GMT+0200) Europe/Andorra"),
|
||||||
|
("Europe/Belgrade", "(GMT+0200) Europe/Belgrade"),
|
||||||
|
("Europe/Berlin", "(GMT+0200) Europe/Berlin"),
|
||||||
|
("Europe/Bratislava", "(GMT+0200) Europe/Bratislava"),
|
||||||
|
("Europe/Brussels", "(GMT+0200) Europe/Brussels"),
|
||||||
|
("Europe/Budapest", "(GMT+0200) Europe/Budapest"),
|
||||||
|
("Europe/Busingen", "(GMT+0200) Europe/Busingen"),
|
||||||
|
("Europe/Copenhagen", "(GMT+0200) Europe/Copenhagen"),
|
||||||
|
("Europe/Gibraltar", "(GMT+0200) Europe/Gibraltar"),
|
||||||
|
("Europe/Kaliningrad", "(GMT+0200) Europe/Kaliningrad"),
|
||||||
|
("Europe/Ljubljana", "(GMT+0200) Europe/Ljubljana"),
|
||||||
|
("Europe/Luxembourg", "(GMT+0200) Europe/Luxembourg"),
|
||||||
|
("Europe/Madrid", "(GMT+0200) Europe/Madrid"),
|
||||||
|
("Europe/Malta", "(GMT+0200) Europe/Malta"),
|
||||||
|
("Europe/Monaco", "(GMT+0200) Europe/Monaco"),
|
||||||
|
("Europe/Oslo", "(GMT+0200) Europe/Oslo"),
|
||||||
|
("Europe/Paris", "(GMT+0200) Europe/Paris"),
|
||||||
|
("Europe/Podgorica", "(GMT+0200) Europe/Podgorica"),
|
||||||
|
("Europe/Prague", "(GMT+0200) Europe/Prague"),
|
||||||
|
("Europe/Rome", "(GMT+0200) Europe/Rome"),
|
||||||
|
("Europe/San_Marino", "(GMT+0200) Europe/San_Marino"),
|
||||||
|
("Europe/Sarajevo", "(GMT+0200) Europe/Sarajevo"),
|
||||||
|
("Europe/Skopje", "(GMT+0200) Europe/Skopje"),
|
||||||
|
("Europe/Stockholm", "(GMT+0200) Europe/Stockholm"),
|
||||||
|
("Europe/Tirane", "(GMT+0200) Europe/Tirane"),
|
||||||
|
("Europe/Vaduz", "(GMT+0200) Europe/Vaduz"),
|
||||||
|
("Europe/Vatican", "(GMT+0200) Europe/Vatican"),
|
||||||
|
("Europe/Vienna", "(GMT+0200) Europe/Vienna"),
|
||||||
|
("Europe/Warsaw", "(GMT+0200) Europe/Warsaw"),
|
||||||
|
("Europe/Zagreb", "(GMT+0200) Europe/Zagreb"),
|
||||||
|
("Europe/Zurich", "(GMT+0200) Europe/Zurich"),
|
||||||
|
("Africa/Addis_Ababa", "(GMT+0300) Africa/Addis_Ababa"),
|
||||||
|
("Africa/Asmara", "(GMT+0300) Africa/Asmara"),
|
||||||
|
(
|
||||||
|
"Africa/Dar_es_Salaam",
|
||||||
|
"(GMT+0300) Africa/Dar_es_Salaam",
|
||||||
|
),
|
||||||
|
("Africa/Djibouti", "(GMT+0300) Africa/Djibouti"),
|
||||||
|
("Africa/Kampala", "(GMT+0300) Africa/Kampala"),
|
||||||
|
("Africa/Mogadishu", "(GMT+0300) Africa/Mogadishu"),
|
||||||
|
("Africa/Nairobi", "(GMT+0300) Africa/Nairobi"),
|
||||||
|
("Antarctica/Syowa", "(GMT+0300) Antarctica/Syowa"),
|
||||||
|
("Asia/Aden", "(GMT+0300) Asia/Aden"),
|
||||||
|
("Asia/Amman", "(GMT+0300) Asia/Amman"),
|
||||||
|
("Asia/Baghdad", "(GMT+0300) Asia/Baghdad"),
|
||||||
|
("Asia/Bahrain", "(GMT+0300) Asia/Bahrain"),
|
||||||
|
("Asia/Beirut", "(GMT+0300) Asia/Beirut"),
|
||||||
|
("Asia/Damascus", "(GMT+0300) Asia/Damascus"),
|
||||||
|
("Asia/Famagusta", "(GMT+0300) Asia/Famagusta"),
|
||||||
|
("Asia/Gaza", "(GMT+0300) Asia/Gaza"),
|
||||||
|
("Asia/Hebron", "(GMT+0300) Asia/Hebron"),
|
||||||
|
("Asia/Jerusalem", "(GMT+0300) Asia/Jerusalem"),
|
||||||
|
("Asia/Kuwait", "(GMT+0300) Asia/Kuwait"),
|
||||||
|
("Asia/Nicosia", "(GMT+0300) Asia/Nicosia"),
|
||||||
|
("Asia/Qatar", "(GMT+0300) Asia/Qatar"),
|
||||||
|
("Asia/Riyadh", "(GMT+0300) Asia/Riyadh"),
|
||||||
|
("Europe/Athens", "(GMT+0300) Europe/Athens"),
|
||||||
|
("Europe/Bucharest", "(GMT+0300) Europe/Bucharest"),
|
||||||
|
("Europe/Chisinau", "(GMT+0300) Europe/Chisinau"),
|
||||||
|
("Europe/Helsinki", "(GMT+0300) Europe/Helsinki"),
|
||||||
|
("Europe/Istanbul", "(GMT+0300) Europe/Istanbul"),
|
||||||
|
("Europe/Kirov", "(GMT+0300) Europe/Kirov"),
|
||||||
|
("Europe/Kyiv", "(GMT+0300) Europe/Kyiv"),
|
||||||
|
("Europe/Mariehamn", "(GMT+0300) Europe/Mariehamn"),
|
||||||
|
("Europe/Minsk", "(GMT+0300) Europe/Minsk"),
|
||||||
|
("Europe/Moscow", "(GMT+0300) Europe/Moscow"),
|
||||||
|
("Europe/Riga", "(GMT+0300) Europe/Riga"),
|
||||||
|
("Europe/Simferopol", "(GMT+0300) Europe/Simferopol"),
|
||||||
|
("Europe/Sofia", "(GMT+0300) Europe/Sofia"),
|
||||||
|
("Europe/Tallinn", "(GMT+0300) Europe/Tallinn"),
|
||||||
|
("Europe/Vilnius", "(GMT+0300) Europe/Vilnius"),
|
||||||
|
("Europe/Volgograd", "(GMT+0300) Europe/Volgograd"),
|
||||||
|
("Indian/Antananarivo", "(GMT+0300) Indian/Antananarivo"),
|
||||||
|
("Indian/Comoro", "(GMT+0300) Indian/Comoro"),
|
||||||
|
("Indian/Mayotte", "(GMT+0300) Indian/Mayotte"),
|
||||||
|
("Asia/Tehran", "(GMT+0330) Asia/Tehran"),
|
||||||
|
("Asia/Baku", "(GMT+0400) Asia/Baku"),
|
||||||
|
("Asia/Dubai", "(GMT+0400) Asia/Dubai"),
|
||||||
|
("Asia/Muscat", "(GMT+0400) Asia/Muscat"),
|
||||||
|
("Asia/Tbilisi", "(GMT+0400) Asia/Tbilisi"),
|
||||||
|
("Asia/Yerevan", "(GMT+0400) Asia/Yerevan"),
|
||||||
|
("Europe/Astrakhan", "(GMT+0400) Europe/Astrakhan"),
|
||||||
|
("Europe/Samara", "(GMT+0400) Europe/Samara"),
|
||||||
|
("Europe/Saratov", "(GMT+0400) Europe/Saratov"),
|
||||||
|
("Europe/Ulyanovsk", "(GMT+0400) Europe/Ulyanovsk"),
|
||||||
|
("Indian/Mahe", "(GMT+0400) Indian/Mahe"),
|
||||||
|
("Indian/Mauritius", "(GMT+0400) Indian/Mauritius"),
|
||||||
|
("Indian/Reunion", "(GMT+0400) Indian/Reunion"),
|
||||||
|
("Asia/Kabul", "(GMT+0430) Asia/Kabul"),
|
||||||
|
("Antarctica/Mawson", "(GMT+0500) Antarctica/Mawson"),
|
||||||
|
("Asia/Aqtau", "(GMT+0500) Asia/Aqtau"),
|
||||||
|
("Asia/Aqtobe", "(GMT+0500) Asia/Aqtobe"),
|
||||||
|
("Asia/Ashgabat", "(GMT+0500) Asia/Ashgabat"),
|
||||||
|
("Asia/Atyrau", "(GMT+0500) Asia/Atyrau"),
|
||||||
|
("Asia/Dushanbe", "(GMT+0500) Asia/Dushanbe"),
|
||||||
|
("Asia/Karachi", "(GMT+0500) Asia/Karachi"),
|
||||||
|
("Asia/Oral", "(GMT+0500) Asia/Oral"),
|
||||||
|
("Asia/Qyzylorda", "(GMT+0500) Asia/Qyzylorda"),
|
||||||
|
("Asia/Samarkand", "(GMT+0500) Asia/Samarkand"),
|
||||||
|
("Asia/Tashkent", "(GMT+0500) Asia/Tashkent"),
|
||||||
|
("Asia/Yekaterinburg", "(GMT+0500) Asia/Yekaterinburg"),
|
||||||
|
("Indian/Kerguelen", "(GMT+0500) Indian/Kerguelen"),
|
||||||
|
("Indian/Maldives", "(GMT+0500) Indian/Maldives"),
|
||||||
|
("Asia/Colombo", "(GMT+0530) Asia/Colombo"),
|
||||||
|
("Asia/Kolkata", "(GMT+0530) Asia/Kolkata"),
|
||||||
|
("Asia/Kathmandu", "(GMT+0545) Asia/Kathmandu"),
|
||||||
|
("Antarctica/Vostok", "(GMT+0600) Antarctica/Vostok"),
|
||||||
|
("Asia/Almaty", "(GMT+0600) Asia/Almaty"),
|
||||||
|
("Asia/Bishkek", "(GMT+0600) Asia/Bishkek"),
|
||||||
|
("Asia/Dhaka", "(GMT+0600) Asia/Dhaka"),
|
||||||
|
("Asia/Omsk", "(GMT+0600) Asia/Omsk"),
|
||||||
|
("Asia/Qostanay", "(GMT+0600) Asia/Qostanay"),
|
||||||
|
("Asia/Thimphu", "(GMT+0600) Asia/Thimphu"),
|
||||||
|
("Asia/Urumqi", "(GMT+0600) Asia/Urumqi"),
|
||||||
|
("Indian/Chagos", "(GMT+0600) Indian/Chagos"),
|
||||||
|
("Asia/Yangon", "(GMT+0630) Asia/Yangon"),
|
||||||
|
("Indian/Cocos", "(GMT+0630) Indian/Cocos"),
|
||||||
|
("Antarctica/Davis", "(GMT+0700) Antarctica/Davis"),
|
||||||
|
("Asia/Bangkok", "(GMT+0700) Asia/Bangkok"),
|
||||||
|
("Asia/Barnaul", "(GMT+0700) Asia/Barnaul"),
|
||||||
|
("Asia/Ho_Chi_Minh", "(GMT+0700) Asia/Ho_Chi_Minh"),
|
||||||
|
("Asia/Hovd", "(GMT+0700) Asia/Hovd"),
|
||||||
|
("Asia/Jakarta", "(GMT+0700) Asia/Jakarta"),
|
||||||
|
("Asia/Krasnoyarsk", "(GMT+0700) Asia/Krasnoyarsk"),
|
||||||
|
("Asia/Novokuznetsk", "(GMT+0700) Asia/Novokuznetsk"),
|
||||||
|
("Asia/Novosibirsk", "(GMT+0700) Asia/Novosibirsk"),
|
||||||
|
("Asia/Phnom_Penh", "(GMT+0700) Asia/Phnom_Penh"),
|
||||||
|
("Asia/Pontianak", "(GMT+0700) Asia/Pontianak"),
|
||||||
|
("Asia/Tomsk", "(GMT+0700) Asia/Tomsk"),
|
||||||
|
("Asia/Vientiane", "(GMT+0700) Asia/Vientiane"),
|
||||||
|
("Indian/Christmas", "(GMT+0700) Indian/Christmas"),
|
||||||
|
("Asia/Brunei", "(GMT+0800) Asia/Brunei"),
|
||||||
|
("Asia/Choibalsan", "(GMT+0800) Asia/Choibalsan"),
|
||||||
|
("Asia/Hong_Kong", "(GMT+0800) Asia/Hong_Kong"),
|
||||||
|
("Asia/Irkutsk", "(GMT+0800) Asia/Irkutsk"),
|
||||||
|
("Asia/Kuala_Lumpur", "(GMT+0800) Asia/Kuala_Lumpur"),
|
||||||
|
("Asia/Kuching", "(GMT+0800) Asia/Kuching"),
|
||||||
|
("Asia/Macau", "(GMT+0800) Asia/Macau"),
|
||||||
|
("Asia/Makassar", "(GMT+0800) Asia/Makassar"),
|
||||||
|
("Asia/Manila", "(GMT+0800) Asia/Manila"),
|
||||||
|
("Asia/Shanghai", "(GMT+0800) Asia/Shanghai"),
|
||||||
|
("Asia/Singapore", "(GMT+0800) Asia/Singapore"),
|
||||||
|
("Asia/Taipei", "(GMT+0800) Asia/Taipei"),
|
||||||
|
("Asia/Ulaanbaatar", "(GMT+0800) Asia/Ulaanbaatar"),
|
||||||
|
("Australia/Perth", "(GMT+0800) Australia/Perth"),
|
||||||
|
("Australia/Eucla", "(GMT+0845) Australia/Eucla"),
|
||||||
|
("Asia/Chita", "(GMT+0900) Asia/Chita"),
|
||||||
|
("Asia/Dili", "(GMT+0900) Asia/Dili"),
|
||||||
|
("Asia/Jayapura", "(GMT+0900) Asia/Jayapura"),
|
||||||
|
("Asia/Khandyga", "(GMT+0900) Asia/Khandyga"),
|
||||||
|
("Asia/Pyongyang", "(GMT+0900) Asia/Pyongyang"),
|
||||||
|
("Asia/Seoul", "(GMT+0900) Asia/Seoul"),
|
||||||
|
("Asia/Tokyo", "(GMT+0900) Asia/Tokyo"),
|
||||||
|
("Asia/Yakutsk", "(GMT+0900) Asia/Yakutsk"),
|
||||||
|
("Pacific/Palau", "(GMT+0900) Pacific/Palau"),
|
||||||
|
("Australia/Adelaide", "(GMT+0930) Australia/Adelaide"),
|
||||||
|
(
|
||||||
|
"Australia/Broken_Hill",
|
||||||
|
"(GMT+0930) Australia/Broken_Hill",
|
||||||
|
),
|
||||||
|
("Australia/Darwin", "(GMT+0930) Australia/Darwin"),
|
||||||
|
(
|
||||||
|
"Antarctica/DumontDUrville",
|
||||||
|
"(GMT+1000) Antarctica/DumontDUrville",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Antarctica/Macquarie",
|
||||||
|
"(GMT+1000) Antarctica/Macquarie",
|
||||||
|
),
|
||||||
|
("Asia/Ust-Nera", "(GMT+1000) Asia/Ust-Nera"),
|
||||||
|
("Asia/Vladivostok", "(GMT+1000) Asia/Vladivostok"),
|
||||||
|
("Australia/Brisbane", "(GMT+1000) Australia/Brisbane"),
|
||||||
|
("Australia/Hobart", "(GMT+1000) Australia/Hobart"),
|
||||||
|
("Australia/Lindeman", "(GMT+1000) Australia/Lindeman"),
|
||||||
|
("Australia/Melbourne", "(GMT+1000) Australia/Melbourne"),
|
||||||
|
("Australia/Sydney", "(GMT+1000) Australia/Sydney"),
|
||||||
|
("Pacific/Chuuk", "(GMT+1000) Pacific/Chuuk"),
|
||||||
|
("Pacific/Guam", "(GMT+1000) Pacific/Guam"),
|
||||||
|
(
|
||||||
|
"Pacific/Port_Moresby",
|
||||||
|
"(GMT+1000) Pacific/Port_Moresby",
|
||||||
|
),
|
||||||
|
("Pacific/Saipan", "(GMT+1000) Pacific/Saipan"),
|
||||||
|
("Australia/Lord_Howe", "(GMT+1030) Australia/Lord_Howe"),
|
||||||
|
("Antarctica/Casey", "(GMT+1100) Antarctica/Casey"),
|
||||||
|
("Asia/Magadan", "(GMT+1100) Asia/Magadan"),
|
||||||
|
("Asia/Sakhalin", "(GMT+1100) Asia/Sakhalin"),
|
||||||
|
("Asia/Srednekolymsk", "(GMT+1100) Asia/Srednekolymsk"),
|
||||||
|
(
|
||||||
|
"Pacific/Bougainville",
|
||||||
|
"(GMT+1100) Pacific/Bougainville",
|
||||||
|
),
|
||||||
|
("Pacific/Efate", "(GMT+1100) Pacific/Efate"),
|
||||||
|
("Pacific/Guadalcanal", "(GMT+1100) Pacific/Guadalcanal"),
|
||||||
|
("Pacific/Kosrae", "(GMT+1100) Pacific/Kosrae"),
|
||||||
|
("Pacific/Norfolk", "(GMT+1100) Pacific/Norfolk"),
|
||||||
|
("Pacific/Noumea", "(GMT+1100) Pacific/Noumea"),
|
||||||
|
("Pacific/Pohnpei", "(GMT+1100) Pacific/Pohnpei"),
|
||||||
|
("Antarctica/McMurdo", "(GMT+1200) Antarctica/McMurdo"),
|
||||||
|
("Asia/Anadyr", "(GMT+1200) Asia/Anadyr"),
|
||||||
|
("Asia/Kamchatka", "(GMT+1200) Asia/Kamchatka"),
|
||||||
|
("Pacific/Auckland", "(GMT+1200) Pacific/Auckland"),
|
||||||
|
("Pacific/Fiji", "(GMT+1200) Pacific/Fiji"),
|
||||||
|
("Pacific/Funafuti", "(GMT+1200) Pacific/Funafuti"),
|
||||||
|
("Pacific/Kwajalein", "(GMT+1200) Pacific/Kwajalein"),
|
||||||
|
("Pacific/Majuro", "(GMT+1200) Pacific/Majuro"),
|
||||||
|
("Pacific/Nauru", "(GMT+1200) Pacific/Nauru"),
|
||||||
|
("Pacific/Tarawa", "(GMT+1200) Pacific/Tarawa"),
|
||||||
|
("Pacific/Wake", "(GMT+1200) Pacific/Wake"),
|
||||||
|
("Pacific/Wallis", "(GMT+1200) Pacific/Wallis"),
|
||||||
|
("Pacific/Chatham", "(GMT+1245) Pacific/Chatham"),
|
||||||
|
("Pacific/Apia", "(GMT+1300) Pacific/Apia"),
|
||||||
|
("Pacific/Fakaofo", "(GMT+1300) Pacific/Fakaofo"),
|
||||||
|
("Pacific/Kanton", "(GMT+1300) Pacific/Kanton"),
|
||||||
|
("Pacific/Tongatapu", "(GMT+1300) Pacific/Tongatapu"),
|
||||||
|
("Pacific/Kiritimati", "(GMT+1400) Pacific/Kiritimati"),
|
||||||
|
],
|
||||||
|
default="UTC",
|
||||||
|
max_length=255,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -124,12 +124,12 @@ class ScrobblableMixin(TimeStampedModel):
|
|||||||
logger.warning("fix_metadata() not implemented yet")
|
logger.warning("fix_metadata() not implemented yet")
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def find_or_create(cls) -> None:
|
def find_or_create(cls):
|
||||||
logger.warning("find_or_create() not implemented yet")
|
logger.warning("find_or_create() not implemented yet")
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self) -> str:
|
||||||
if self.title:
|
if self.title:
|
||||||
return self.title
|
return str(self.title)
|
||||||
return str(self.uuid)
|
return str(self.uuid)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -1034,7 +1034,6 @@ class Scrobble(TimeStampedModel):
|
|||||||
"scrobble_data": scrobble_data,
|
"scrobble_data": scrobble_data,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
scrobble_data["playback_status"] = scrobble_data.pop("status", None)
|
scrobble_data["playback_status"] = scrobble_data.pop("status", None)
|
||||||
# If it's marked as stopped, send it through our update mechanism, which will complete it
|
# If it's marked as stopped, send it through our update mechanism, which will complete it
|
||||||
if scrobble and (
|
if scrobble and (
|
||||||
@ -1140,9 +1139,9 @@ class Scrobble(TimeStampedModel):
|
|||||||
if existing_locations := location.in_proximity(named=True):
|
if existing_locations := location.in_proximity(named=True):
|
||||||
existing_location = existing_locations.first()
|
existing_location = existing_locations.first()
|
||||||
ts = int(pendulum.now().timestamp())
|
ts = int(pendulum.now().timestamp())
|
||||||
scrobble.log[ts] = (
|
scrobble.log[
|
||||||
f"Location {location.id} too close to this scrobble"
|
ts
|
||||||
)
|
] = f"Location {location.id} too close to this scrobble"
|
||||||
scrobble.save(update_fields=["log"])
|
scrobble.save(update_fields=["log"])
|
||||||
logger.info(
|
logger.info(
|
||||||
f"[scrobbling] finished - found existing named location",
|
f"[scrobbling] finished - found existing named location",
|
||||||
|
|||||||
@ -1,8 +1,7 @@
|
|||||||
from datetime import datetime
|
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
|
from datetime import datetime
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from urllib.parse import parse_qs, urlparse
|
|
||||||
|
|
||||||
import pendulum
|
import pendulum
|
||||||
import pytz
|
import pytz
|
||||||
@ -16,13 +15,15 @@ from locations.models import GeoLocation
|
|||||||
from music.constants import JELLYFIN_POST_KEYS, MOPIDY_POST_KEYS
|
from music.constants import JELLYFIN_POST_KEYS, MOPIDY_POST_KEYS
|
||||||
from music.models import Track
|
from music.models import Track
|
||||||
from music.utils import get_or_create_track
|
from music.utils import get_or_create_track
|
||||||
from podcasts.utils import get_or_create_podcast
|
from podcasts.models import PodcastEpisode
|
||||||
|
from podcasts.utils import parse_mopidy_uri
|
||||||
from scrobbles.constants import (
|
from scrobbles.constants import (
|
||||||
JELLYFIN_AUDIO_ITEM_TYPES,
|
JELLYFIN_AUDIO_ITEM_TYPES,
|
||||||
MANUAL_SCROBBLE_FNS,
|
MANUAL_SCROBBLE_FNS,
|
||||||
SCROBBLE_CONTENT_URLS,
|
SCROBBLE_CONTENT_URLS,
|
||||||
)
|
)
|
||||||
from scrobbles.models import Scrobble
|
from scrobbles.models import Scrobble
|
||||||
|
from scrobbles.utils import convert_to_seconds
|
||||||
from sports.models import SportEvent
|
from sports.models import SportEvent
|
||||||
from sports.thesportsdb import lookup_event_from_thesportsdb
|
from sports.thesportsdb import lookup_event_from_thesportsdb
|
||||||
from tasks.models import Task
|
from tasks.models import Task
|
||||||
@ -54,9 +55,26 @@ def mopidy_scrobble_media(post_data: dict, user_id: int) -> Scrobble:
|
|||||||
)
|
)
|
||||||
|
|
||||||
if media_type == Scrobble.MediaType.PODCAST_EPISODE:
|
if media_type == Scrobble.MediaType.PODCAST_EPISODE:
|
||||||
media_obj = get_or_create_podcast(post_data)
|
parsed_data = parse_mopidy_uri(post_data.get("mopidy_uri", ""))
|
||||||
|
podcast_name = post_data.get(
|
||||||
|
"album", parsed_data.get("podcast_name", "")
|
||||||
|
)
|
||||||
|
|
||||||
|
media_obj = PodcastEpisode.find_or_create(
|
||||||
|
title=parsed_data.get("episode_filename", ""),
|
||||||
|
podcast_name=podcast_name,
|
||||||
|
producer_name=post_data.get("artist", ""),
|
||||||
|
number=parsed_data.get("episode_num", ""),
|
||||||
|
pub_date=parsed_data.get("pub_date", ""),
|
||||||
|
mopidy_uri=post_data.get("mopidy_uri", ""),
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
media_obj = get_or_create_track(post_data, MOPIDY_POST_KEYS)
|
media_obj = Track.find_or_create(
|
||||||
|
title=post_data.get("name", ""),
|
||||||
|
artist_name=post_data.get("artist", ""),
|
||||||
|
album_name=post_data.get("album", ""),
|
||||||
|
run_time_seconds=post_data.get("run_time", 900000),
|
||||||
|
)
|
||||||
|
|
||||||
log = {}
|
log = {}
|
||||||
try:
|
try:
|
||||||
@ -109,8 +127,14 @@ def jellyfin_scrobble_media(
|
|||||||
post_data.get("Provider_imdb", "").replace("tt", "")
|
post_data.get("Provider_imdb", "").replace("tt", "")
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
media_obj = get_or_create_track(
|
media_obj = Track.find_or_create(
|
||||||
post_data, post_keys=JELLYFIN_POST_KEYS
|
title=post_data.get("Name", ""),
|
||||||
|
artist_name=post_data.get("Artist", ""),
|
||||||
|
album_name=post_data.get("Album", ""),
|
||||||
|
run_time_seconds=convert_to_seconds(
|
||||||
|
post_data.get("RunTime", 900000)
|
||||||
|
),
|
||||||
|
musicbrainz_id=post_data.get("Provider_musicbrainztrack", ""),
|
||||||
)
|
)
|
||||||
# A hack because we don't worry about updating music ... we either finish it or we don't
|
# A hack because we don't worry about updating music ... we either finish it or we don't
|
||||||
playback_position_seconds = 0
|
playback_position_seconds = 0
|
||||||
|
|||||||
@ -1,18 +1,12 @@
|
|||||||
import codecs
|
import codecs
|
||||||
import csv
|
import csv
|
||||||
import logging
|
import logging
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
import pytz
|
import pytz
|
||||||
import requests
|
import requests
|
||||||
from music.utils import (
|
from music.models import Track
|
||||||
get_or_create_album,
|
|
||||||
get_or_create_artist,
|
|
||||||
get_or_create_track,
|
|
||||||
)
|
|
||||||
from scrobbles.constants import AsTsvColumn
|
from scrobbles.constants import AsTsvColumn
|
||||||
from scrobbles.models import Scrobble
|
from scrobbles.models import Scrobble
|
||||||
from music.constants import MOPIDY_POST_KEYS
|
|
||||||
|
|
||||||
from scrobbles.utils import timestamp_user_tz_to_utc
|
from scrobbles.utils import timestamp_user_tz_to_utc
|
||||||
|
|
||||||
@ -50,27 +44,12 @@ def process_audioscrobbler_tsv_file(file_path, user_id, user_tz=None):
|
|||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
track = get_or_create_track(
|
track = Track.find_or_create(
|
||||||
{
|
title=row[AsTsvColumn["TRACK_NAME"].value],
|
||||||
"title": row[AsTsvColumn["TRACK_NAME"].value],
|
musicbrainz_id=row[AsTsvColumn["MB_ID"].value],
|
||||||
"mbid": row[AsTsvColumn["MB_ID"].value],
|
artist_name=row[AsTsvColumn["ARTIST_NAME"].value]
|
||||||
"artist_name": row[AsTsvColumn["ARTIST_NAME"].value],
|
album_name=row[AsTsvColumn["ALBUM_NAME"].value]
|
||||||
"album_name": row[AsTsvColumn["ALBUM_NAME"].value],
|
|
||||||
"run_time_seconds": int(
|
|
||||||
row[AsTsvColumn["RUN_TIME_SECONDS"].value]
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"TRACK_MB_ID": "mbid",
|
|
||||||
"TRACK_TITLE": "track_title",
|
|
||||||
"ALBUM_NAME": "album_name",
|
|
||||||
"ARTIST_NAME": "artist_name",
|
|
||||||
"RUN_TIME": "run_time_seconds",
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
if not track:
|
|
||||||
logger.info(f"Skipping track {track} because not found")
|
|
||||||
continue
|
|
||||||
|
|
||||||
# TODO Set all this up as constants
|
# TODO Set all this up as constants
|
||||||
if row[AsTsvColumn["COMPLETE"].value] == "S":
|
if row[AsTsvColumn["COMPLETE"].value] == "S":
|
||||||
|
|||||||
@ -60,6 +60,8 @@ DUMP_REQUEST_DATA = (
|
|||||||
|
|
||||||
THESPORTSDB_API_KEY = os.getenv("VROBBLER_THESPORTSDB_API_KEY", "2")
|
THESPORTSDB_API_KEY = os.getenv("VROBBLER_THESPORTSDB_API_KEY", "2")
|
||||||
THEAUDIODB_API_KEY = os.getenv("VROBBLER_THEAUDIODB_API_KEY", "2")
|
THEAUDIODB_API_KEY = os.getenv("VROBBLER_THEAUDIODB_API_KEY", "2")
|
||||||
|
PODCASTINDEX_API_KEY = os.getenv("VROBBLER_PODCASTINDEX_API_KEY", "")
|
||||||
|
PODCASTINDEX_API_SECRET = os.getenv("VROBBLER_PODCASTINDEX_API_SECRET", "")
|
||||||
TMDB_API_KEY = os.getenv("VROBBLER_TMDB_API_KEY", "")
|
TMDB_API_KEY = os.getenv("VROBBLER_TMDB_API_KEY", "")
|
||||||
LASTFM_API_KEY = os.getenv("VROBBLER_LASTFM_API_KEY")
|
LASTFM_API_KEY = os.getenv("VROBBLER_LASTFM_API_KEY")
|
||||||
LASTFM_SECRET_KEY = os.getenv("VROBBLER_LASTFM_SECRET_KEY")
|
LASTFM_SECRET_KEY = os.getenv("VROBBLER_LASTFM_SECRET_KEY")
|
||||||
|
|||||||
Reference in New Issue
Block a user