Compare commits
70 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 132d63bb5d | |||
| 49bf57dd58 | |||
| 506de848d7 | |||
| d05256f249 | |||
| 646c7ab99c | |||
| 7fc3705455 | |||
| cbe4abfb5f | |||
| 13bdc201f0 | |||
| 4f5ea7cd25 | |||
| b3b3b28b92 | |||
| 9ed3d034cf | |||
| 8e4a41a279 | |||
| 0cdde59de4 | |||
| 65e713e43e | |||
| 0d95f8fee8 | |||
| 6712e38689 | |||
| 4ed5fde672 | |||
| 1c5f721723 | |||
| 39547c6e5c | |||
| 7447a97117 | |||
| 6aa933d13d | |||
| 3bb73ae4be | |||
| a57269b09a | |||
| 68423488ff | |||
| e75c22d583 | |||
| a0af0bce05 | |||
| fd984d7460 | |||
| 065fc98a87 | |||
| 6db5a00917 | |||
| 734aa6073b | |||
| 77362d3207 | |||
| 9d4db65b3c | |||
| e850d46539 | |||
| 907ef802bc | |||
| d700b581a1 | |||
| 7605c672f6 | |||
| 8d1df806d7 | |||
| 0f562b7c58 | |||
| fe53b68714 | |||
| 7e2915850f | |||
| 90687a6b43 | |||
| 07cfb03eb6 | |||
| 58be8d26a0 | |||
| 0fede269b1 | |||
| 6cdcf4ff6f | |||
| 0634b94368 | |||
| 0ab7c563cf | |||
| 363a132df2 | |||
| c484905d11 | |||
| 0378dfe6eb | |||
| c39443e35b | |||
| bb3dfdf7ba | |||
| fdfb8a635e | |||
| 290e6dc8d9 | |||
| 499546503c | |||
| bd3a381346 | |||
| e206a7fbf3 | |||
| 6313da9868 | |||
| f7c69a6763 | |||
| ab88fcb9a7 | |||
| 1d868d3075 | |||
| 5b07c70ca2 | |||
| 9607fb2d1e | |||
| e6cf126f5c | |||
| eeee6eea4e | |||
| 1f26931215 | |||
| 610ec63cbd | |||
| 72fded4097 | |||
| d5eea53a01 | |||
| a49eb31276 |
7
.coveragerc
Normal file
7
.coveragerc
Normal file
@ -0,0 +1,7 @@
|
||||
[run]
|
||||
omit=
|
||||
vrobbler/wsgi.py
|
||||
vrobbler/asgi.py
|
||||
vrobbler/cli.py
|
||||
*admin.py
|
||||
migrations/*
|
||||
@ -8,15 +8,15 @@ name: run_tests
|
||||
|
||||
steps:
|
||||
# Run tests against Python/Flask engine backend (with pytest)
|
||||
- name: django_tests
|
||||
- name: pytest with coverage
|
||||
image: python:3.10.4
|
||||
commands:
|
||||
# Install dependencies
|
||||
- cp vrobbler.conf.example vrobbler.conf
|
||||
- cp vrobbler.conf.test vrobbler.conf
|
||||
- pip install poetry
|
||||
- poetry install
|
||||
# Start with a fresh database (which is already running as a service from Drone)
|
||||
- poetry run python manage.py test
|
||||
- poetry run pytest --cov-report term:skip-covered --cov=vrobbler tests
|
||||
environment:
|
||||
VROBBLER_DATABASE_URL: sqlite:///test.db
|
||||
volumes:
|
||||
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@ -1,4 +1,5 @@
|
||||
db.sqlite3
|
||||
vrobbler.conf
|
||||
/media/
|
||||
/dist/
|
||||
media/
|
||||
dist/
|
||||
.coverage
|
||||
|
||||
@ -1,4 +1,7 @@
|
||||
#+title: Readme
|
||||
Vrobbler
|
||||
========
|
||||
|
||||
[](https://ci.unbl.ink/secstate/vrobbler)
|
||||
|
||||
Vrobbler is a pretty simple Django-powered web app for scrobbling video plays from you favorite Jellyfin installation.
|
||||
|
||||
701
poetry.lock
generated
701
poetry.lock
generated
@ -200,14 +200,27 @@ pycparser = "*"
|
||||
|
||||
[[package]]
|
||||
name = "charset-normalizer"
|
||||
version = "2.1.1"
|
||||
version = "3.0.1"
|
||||
description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.6.0"
|
||||
python-versions = "*"
|
||||
|
||||
[[package]]
|
||||
name = "cinemagoer"
|
||||
version = "2022.12.27"
|
||||
description = "Python package to access the IMDb's database"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
|
||||
[package.dependencies]
|
||||
lxml = "*"
|
||||
SQLAlchemy = "*"
|
||||
|
||||
[package.extras]
|
||||
unicode-backport = ["unicodedata2"]
|
||||
dev = ["flake8", "flake8-isort", "pytest", "pytest-cov", "tox"]
|
||||
doc = ["sphinx", "sphinx-rtd-theme"]
|
||||
|
||||
[[package]]
|
||||
name = "click"
|
||||
@ -282,7 +295,7 @@ development = ["black", "flake8", "mypy", "pytest", "types-colorama"]
|
||||
|
||||
[[package]]
|
||||
name = "coverage"
|
||||
version = "7.0.3"
|
||||
version = "7.0.5"
|
||||
description = "Code coverage measurement for Python"
|
||||
category = "dev"
|
||||
optional = false
|
||||
@ -362,6 +375,17 @@ python3-openid = ">=3.0.8"
|
||||
requests = "*"
|
||||
requests-oauthlib = ">=0.3.0"
|
||||
|
||||
[[package]]
|
||||
name = "django-cachalot"
|
||||
version = "2.5.2"
|
||||
description = "Caches your Django ORM queries and automatically invalidates them."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
|
||||
[package.dependencies]
|
||||
Django = ">=2.2,<4.2"
|
||||
|
||||
[[package]]
|
||||
name = "django-celery-results"
|
||||
version = "2.4.0"
|
||||
@ -471,17 +495,6 @@ mccabe = ">=0.7.0,<0.8.0"
|
||||
pycodestyle = ">=2.9.0,<2.10.0"
|
||||
pyflakes = ">=2.5.0,<2.6.0"
|
||||
|
||||
[[package]]
|
||||
name = "freezegun"
|
||||
version = "1.2.2"
|
||||
description = "Let your Python tests travel through time"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
|
||||
[package.dependencies]
|
||||
python-dateutil = ">=2.7"
|
||||
|
||||
[[package]]
|
||||
name = "gitdb"
|
||||
version = "4.0.10"
|
||||
@ -504,6 +517,18 @@ python-versions = ">=3.7"
|
||||
[package.dependencies]
|
||||
gitdb = ">=4.0.1,<5"
|
||||
|
||||
[[package]]
|
||||
name = "greenlet"
|
||||
version = "2.0.1"
|
||||
description = "Lightweight in-process concurrent programming"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*"
|
||||
|
||||
[package.extras]
|
||||
docs = ["Sphinx", "docutils (<0.18)"]
|
||||
test = ["faulthandler", "objgraph", "psutil"]
|
||||
|
||||
[[package]]
|
||||
name = "gunicorn"
|
||||
version = "20.1.0"
|
||||
@ -555,11 +580,11 @@ testing = ["flake8 (<5)", "flufl.flake8", "importlib-resources (>=1.3)", "packag
|
||||
|
||||
[[package]]
|
||||
name = "iniconfig"
|
||||
version = "1.1.1"
|
||||
description = "iniconfig: brain-dead simple config-ini parsing"
|
||||
version = "2.0.0"
|
||||
description = "brain-dead simple config-ini parsing"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
python-versions = ">=3.7"
|
||||
|
||||
[[package]]
|
||||
name = "isort"
|
||||
@ -603,6 +628,20 @@ sqs = ["boto3 (>=1.9.12)", "pycurl (>=7.44.1,<7.45.0)", "urllib3 (>=1.26.7)"]
|
||||
yaml = ["PyYAML (>=3.10)"]
|
||||
zookeeper = ["kazoo (>=1.3.1)"]
|
||||
|
||||
[[package]]
|
||||
name = "lxml"
|
||||
version = "4.9.2"
|
||||
description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, != 3.4.*"
|
||||
|
||||
[package.extras]
|
||||
cssselect = ["cssselect (>=0.7)"]
|
||||
html5 = ["html5lib"]
|
||||
htmlsoup = ["BeautifulSoup4"]
|
||||
source = ["Cython (>=0.29.7)"]
|
||||
|
||||
[[package]]
|
||||
name = "markdown"
|
||||
version = "3.4.1"
|
||||
@ -625,6 +664,19 @@ category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
|
||||
[[package]]
|
||||
name = "mock"
|
||||
version = "5.0.1"
|
||||
description = "Rolling backport of unittest.mock for all Pythons"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
|
||||
[package.extras]
|
||||
build = ["blurb", "twine", "wheel"]
|
||||
docs = ["sphinx"]
|
||||
test = ["pytest", "pytest-cov"]
|
||||
|
||||
[[package]]
|
||||
name = "musicbrainzngs"
|
||||
version = "0.7.1"
|
||||
@ -685,7 +737,7 @@ attrs = ">=19.2.0"
|
||||
|
||||
[[package]]
|
||||
name = "packaging"
|
||||
version = "22.0"
|
||||
version = "23.0"
|
||||
description = "Core utilities for Python packages"
|
||||
category = "dev"
|
||||
optional = false
|
||||
@ -701,7 +753,7 @@ python-versions = ">=3.7"
|
||||
|
||||
[[package]]
|
||||
name = "pbr"
|
||||
version = "5.11.0"
|
||||
version = "5.11.1"
|
||||
description = "Python Build Reasonableness"
|
||||
category = "dev"
|
||||
optional = false
|
||||
@ -819,9 +871,21 @@ category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
||||
|
||||
[[package]]
|
||||
name = "pysportsdb"
|
||||
version = "0.1.0"
|
||||
description = "An simple Python interface to thesportsdb.com's API"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.7,<4.0"
|
||||
|
||||
[package.dependencies]
|
||||
mock = ">=5.0.1,<6.0.0"
|
||||
requests = ">=2.28.2,<3.0.0"
|
||||
|
||||
[[package]]
|
||||
name = "pytest"
|
||||
version = "7.2.0"
|
||||
version = "7.2.1"
|
||||
description = "pytest: simple powerful testing with Python"
|
||||
category = "dev"
|
||||
optional = false
|
||||
@ -879,6 +943,21 @@ pytest = ">=4.6"
|
||||
[package.extras]
|
||||
testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"]
|
||||
|
||||
[[package]]
|
||||
name = "pytest-django"
|
||||
version = "4.5.2"
|
||||
description = "A Django plugin for pytest."
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.5"
|
||||
|
||||
[package.dependencies]
|
||||
pytest = ">=5.4.0"
|
||||
|
||||
[package.extras]
|
||||
docs = ["sphinx", "sphinx-rtd-theme"]
|
||||
testing = ["Django", "django-configurations (>=2.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "pytest-flake8"
|
||||
version = "1.1.1"
|
||||
@ -1022,7 +1101,7 @@ postgresql = ["psycopg2"]
|
||||
|
||||
[[package]]
|
||||
name = "pytz"
|
||||
version = "2022.7"
|
||||
version = "2022.7.1"
|
||||
description = "World timezone definitions, modern and historical"
|
||||
category = "main"
|
||||
optional = false
|
||||
@ -1038,7 +1117,7 @@ python-versions = ">=3.6"
|
||||
|
||||
[[package]]
|
||||
name = "redis"
|
||||
version = "4.4.0"
|
||||
version = "4.4.2"
|
||||
description = "Python client for Redis database and key-value store"
|
||||
category = "main"
|
||||
optional = false
|
||||
@ -1053,7 +1132,7 @@ ocsp = ["cryptography (>=36.0.1)", "pyopenssl (==20.0.1)", "requests (>=2.26.0)"
|
||||
|
||||
[[package]]
|
||||
name = "requests"
|
||||
version = "2.28.1"
|
||||
version = "2.28.2"
|
||||
description = "Python HTTP for Humans."
|
||||
category = "main"
|
||||
optional = false
|
||||
@ -1061,7 +1140,7 @@ python-versions = ">=3.7, <4"
|
||||
|
||||
[package.dependencies]
|
||||
certifi = ">=2017.4.17"
|
||||
charset-normalizer = ">=2,<3"
|
||||
charset-normalizer = ">=2,<4"
|
||||
idna = ">=2.5,<4"
|
||||
urllib3 = ">=1.21.1,<1.27"
|
||||
|
||||
@ -1100,14 +1179,14 @@ urllib3 = {version = ">=1.26,<2.0", extras = ["socks"]}
|
||||
|
||||
[[package]]
|
||||
name = "setuptools"
|
||||
version = "65.6.3"
|
||||
version = "66.0.0"
|
||||
description = "Easily download, build, install, upgrade, and uninstall Python packages"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
|
||||
[package.extras]
|
||||
docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"]
|
||||
docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"]
|
||||
testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8 (<5)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"]
|
||||
testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"]
|
||||
|
||||
@ -1143,6 +1222,38 @@ category = "dev"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
|
||||
[[package]]
|
||||
name = "sqlalchemy"
|
||||
version = "1.4.46"
|
||||
description = "Database Abstraction Library"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7"
|
||||
|
||||
[package.dependencies]
|
||||
greenlet = {version = "!=0.4.17", markers = "python_version >= \"3\" and (platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\")"}
|
||||
|
||||
[package.extras]
|
||||
aiomysql = ["aiomysql", "greenlet (!=0.4.17)"]
|
||||
aiosqlite = ["aiosqlite", "greenlet (!=0.4.17)", "typing_extensions (!=3.10.0.1)"]
|
||||
asyncio = ["greenlet (!=0.4.17)"]
|
||||
asyncmy = ["asyncmy (>=0.2.3,!=0.2.4)", "greenlet (!=0.4.17)"]
|
||||
mariadb-connector = ["mariadb (>=1.0.1,!=1.1.2)"]
|
||||
mssql = ["pyodbc"]
|
||||
mssql-pymssql = ["pymssql"]
|
||||
mssql-pyodbc = ["pyodbc"]
|
||||
mypy = ["mypy (>=0.910)", "sqlalchemy2-stubs"]
|
||||
mysql = ["mysqlclient (>=1.4.0)", "mysqlclient (>=1.4.0,<2)"]
|
||||
mysql-connector = ["mysql-connector-python"]
|
||||
oracle = ["cx_oracle (>=7)", "cx_oracle (>=7,<8)"]
|
||||
postgresql = ["psycopg2 (>=2.7)"]
|
||||
postgresql-asyncpg = ["asyncpg", "greenlet (!=0.4.17)"]
|
||||
postgresql-pg8000 = ["pg8000 (>=1.16.6,!=1.29.0)"]
|
||||
postgresql-psycopg2binary = ["psycopg2-binary"]
|
||||
postgresql-psycopg2cffi = ["psycopg2cffi"]
|
||||
pymysql = ["pymysql", "pymysql (<1)"]
|
||||
sqlcipher = ["sqlcipher3_binary"]
|
||||
|
||||
[[package]]
|
||||
name = "sqlparse"
|
||||
version = "0.4.3"
|
||||
@ -1176,6 +1287,17 @@ six = ">=1.9.0"
|
||||
[package.extras]
|
||||
doc = ["reno", "sphinx", "tornado (>=4.5)"]
|
||||
|
||||
[[package]]
|
||||
name = "time-machine"
|
||||
version = "2.9.0"
|
||||
description = "Travel through time in your tests."
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
|
||||
[package.dependencies]
|
||||
python-dateutil = "*"
|
||||
|
||||
[[package]]
|
||||
name = "tinycss2"
|
||||
version = "1.1.1"
|
||||
@ -1238,17 +1360,9 @@ async-generator = ">=1.10"
|
||||
trio = ">=0.11"
|
||||
wsproto = ">=0.14"
|
||||
|
||||
[[package]]
|
||||
name = "types-freezegun"
|
||||
version = "1.1.10"
|
||||
description = "Typing stubs for freezegun"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
|
||||
[[package]]
|
||||
name = "types-pytz"
|
||||
version = "2022.7.0.0"
|
||||
version = "2022.7.1.0"
|
||||
description = "Typing stubs for pytz"
|
||||
category = "dev"
|
||||
optional = false
|
||||
@ -1256,7 +1370,7 @@ python-versions = "*"
|
||||
|
||||
[[package]]
|
||||
name = "types-requests"
|
||||
version = "2.28.11.7"
|
||||
version = "2.28.11.8"
|
||||
description = "Typing stubs for requests"
|
||||
category = "dev"
|
||||
optional = false
|
||||
@ -1291,7 +1405,7 @@ python-versions = ">=2"
|
||||
|
||||
[[package]]
|
||||
name = "urllib3"
|
||||
version = "1.26.13"
|
||||
version = "1.26.14"
|
||||
description = "HTTP library with thread-safe connection pooling, file post, and more."
|
||||
category = "main"
|
||||
optional = false
|
||||
@ -1315,7 +1429,7 @@ python-versions = ">=3.6"
|
||||
|
||||
[[package]]
|
||||
name = "wcwidth"
|
||||
version = "0.2.5"
|
||||
version = "0.2.6"
|
||||
description = "Measures the displayed width of unicode strings in a terminal"
|
||||
category = "main"
|
||||
optional = false
|
||||
@ -1377,7 +1491,7 @@ testing = ["flake8 (<5)", "func-timeout", "jaraco.functools", "jaraco.itertools"
|
||||
[metadata]
|
||||
lock-version = "1.1"
|
||||
python-versions = "^3.8"
|
||||
content-hash = "f8d6b69cfb8ac53b7e03533e8fede68d323363bd73b9c7902d958dc433b1210e"
|
||||
content-hash = "32f195ac129f5d8984ac82d42fa970f127ebea8f86f42d6d7cefc443c788b46a"
|
||||
|
||||
[metadata.files]
|
||||
amqp = [
|
||||
@ -1519,8 +1633,98 @@ cffi = [
|
||||
{file = "cffi-1.15.1.tar.gz", hash = "sha256:d400bfb9a37b1351253cb402671cea7e89bdecc294e8016a707f6d1d8ac934f9"},
|
||||
]
|
||||
charset-normalizer = [
|
||||
{file = "charset-normalizer-2.1.1.tar.gz", hash = "sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845"},
|
||||
{file = "charset_normalizer-2.1.1-py3-none-any.whl", hash = "sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f"},
|
||||
{file = "charset-normalizer-3.0.1.tar.gz", hash = "sha256:ebea339af930f8ca5d7a699b921106c6e29c617fe9606fa7baa043c1cdae326f"},
|
||||
{file = "charset_normalizer-3.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:88600c72ef7587fe1708fd242b385b6ed4b8904976d5da0893e31df8b3480cb6"},
|
||||
{file = "charset_normalizer-3.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c75ffc45f25324e68ab238cb4b5c0a38cd1c3d7f1fb1f72b5541de469e2247db"},
|
||||
{file = "charset_normalizer-3.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:db72b07027db150f468fbada4d85b3b2729a3db39178abf5c543b784c1254539"},
|
||||
{file = "charset_normalizer-3.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62595ab75873d50d57323a91dd03e6966eb79c41fa834b7a1661ed043b2d404d"},
|
||||
{file = "charset_normalizer-3.0.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ff6f3db31555657f3163b15a6b7c6938d08df7adbfc9dd13d9d19edad678f1e8"},
|
||||
{file = "charset_normalizer-3.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:772b87914ff1152b92a197ef4ea40efe27a378606c39446ded52c8f80f79702e"},
|
||||
{file = "charset_normalizer-3.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70990b9c51340e4044cfc394a81f614f3f90d41397104d226f21e66de668730d"},
|
||||
{file = "charset_normalizer-3.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:292d5e8ba896bbfd6334b096e34bffb56161c81408d6d036a7dfa6929cff8783"},
|
||||
{file = "charset_normalizer-3.0.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:2edb64ee7bf1ed524a1da60cdcd2e1f6e2b4f66ef7c077680739f1641f62f555"},
|
||||
{file = "charset_normalizer-3.0.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:31a9ddf4718d10ae04d9b18801bd776693487cbb57d74cc3458a7673f6f34639"},
|
||||
{file = "charset_normalizer-3.0.1-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:44ba614de5361b3e5278e1241fda3dc1838deed864b50a10d7ce92983797fa76"},
|
||||
{file = "charset_normalizer-3.0.1-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:12db3b2c533c23ab812c2b25934f60383361f8a376ae272665f8e48b88e8e1c6"},
|
||||
{file = "charset_normalizer-3.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c512accbd6ff0270939b9ac214b84fb5ada5f0409c44298361b2f5e13f9aed9e"},
|
||||
{file = "charset_normalizer-3.0.1-cp310-cp310-win32.whl", hash = "sha256:502218f52498a36d6bf5ea77081844017bf7982cdbe521ad85e64cabee1b608b"},
|
||||
{file = "charset_normalizer-3.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:601f36512f9e28f029d9481bdaf8e89e5148ac5d89cffd3b05cd533eeb423b59"},
|
||||
{file = "charset_normalizer-3.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0298eafff88c99982a4cf66ba2efa1128e4ddaca0b05eec4c456bbc7db691d8d"},
|
||||
{file = "charset_normalizer-3.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a8d0fc946c784ff7f7c3742310cc8a57c5c6dc31631269876a88b809dbeff3d3"},
|
||||
{file = "charset_normalizer-3.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:87701167f2a5c930b403e9756fab1d31d4d4da52856143b609e30a1ce7160f3c"},
|
||||
{file = "charset_normalizer-3.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14e76c0f23218b8f46c4d87018ca2e441535aed3632ca134b10239dfb6dadd6b"},
|
||||
{file = "charset_normalizer-3.0.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0c0a590235ccd933d9892c627dec5bc7511ce6ad6c1011fdf5b11363022746c1"},
|
||||
{file = "charset_normalizer-3.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c7fe7afa480e3e82eed58e0ca89f751cd14d767638e2550c77a92a9e749c317"},
|
||||
{file = "charset_normalizer-3.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:79909e27e8e4fcc9db4addea88aa63f6423ebb171db091fb4373e3312cb6d603"},
|
||||
{file = "charset_normalizer-3.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ac7b6a045b814cf0c47f3623d21ebd88b3e8cf216a14790b455ea7ff0135d18"},
|
||||
{file = "charset_normalizer-3.0.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:72966d1b297c741541ca8cf1223ff262a6febe52481af742036a0b296e35fa5a"},
|
||||
{file = "charset_normalizer-3.0.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:f9d0c5c045a3ca9bedfc35dca8526798eb91a07aa7a2c0fee134c6c6f321cbd7"},
|
||||
{file = "charset_normalizer-3.0.1-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:5995f0164fa7df59db4746112fec3f49c461dd6b31b841873443bdb077c13cfc"},
|
||||
{file = "charset_normalizer-3.0.1-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4a8fcf28c05c1f6d7e177a9a46a1c52798bfe2ad80681d275b10dcf317deaf0b"},
|
||||
{file = "charset_normalizer-3.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:761e8904c07ad053d285670f36dd94e1b6ab7f16ce62b9805c475b7aa1cffde6"},
|
||||
{file = "charset_normalizer-3.0.1-cp311-cp311-win32.whl", hash = "sha256:71140351489970dfe5e60fc621ada3e0f41104a5eddaca47a7acb3c1b851d6d3"},
|
||||
{file = "charset_normalizer-3.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:9ab77acb98eba3fd2a85cd160851816bfce6871d944d885febf012713f06659c"},
|
||||
{file = "charset_normalizer-3.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:84c3990934bae40ea69a82034912ffe5a62c60bbf6ec5bc9691419641d7d5c9a"},
|
||||
{file = "charset_normalizer-3.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:74292fc76c905c0ef095fe11e188a32ebd03bc38f3f3e9bcb85e4e6db177b7ea"},
|
||||
{file = "charset_normalizer-3.0.1-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c95a03c79bbe30eec3ec2b7f076074f4281526724c8685a42872974ef4d36b72"},
|
||||
{file = "charset_normalizer-3.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f4c39b0e3eac288fedc2b43055cfc2ca7a60362d0e5e87a637beac5d801ef478"},
|
||||
{file = "charset_normalizer-3.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:df2c707231459e8a4028eabcd3cfc827befd635b3ef72eada84ab13b52e1574d"},
|
||||
{file = "charset_normalizer-3.0.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:93ad6d87ac18e2a90b0fe89df7c65263b9a99a0eb98f0a3d2e079f12a0735837"},
|
||||
{file = "charset_normalizer-3.0.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:59e5686dd847347e55dffcc191a96622f016bc0ad89105e24c14e0d6305acbc6"},
|
||||
{file = "charset_normalizer-3.0.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:cd6056167405314a4dc3c173943f11249fa0f1b204f8b51ed4bde1a9cd1834dc"},
|
||||
{file = "charset_normalizer-3.0.1-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:083c8d17153ecb403e5e1eb76a7ef4babfc2c48d58899c98fcaa04833e7a2f9a"},
|
||||
{file = "charset_normalizer-3.0.1-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:f5057856d21e7586765171eac8b9fc3f7d44ef39425f85dbcccb13b3ebea806c"},
|
||||
{file = "charset_normalizer-3.0.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:7eb33a30d75562222b64f569c642ff3dc6689e09adda43a082208397f016c39a"},
|
||||
{file = "charset_normalizer-3.0.1-cp36-cp36m-win32.whl", hash = "sha256:95dea361dd73757c6f1c0a1480ac499952c16ac83f7f5f4f84f0658a01b8ef41"},
|
||||
{file = "charset_normalizer-3.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:eaa379fcd227ca235d04152ca6704c7cb55564116f8bc52545ff357628e10602"},
|
||||
{file = "charset_normalizer-3.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:3e45867f1f2ab0711d60c6c71746ac53537f1684baa699f4f668d4c6f6ce8e14"},
|
||||
{file = "charset_normalizer-3.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cadaeaba78750d58d3cc6ac4d1fd867da6fc73c88156b7a3212a3cd4819d679d"},
|
||||
{file = "charset_normalizer-3.0.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:911d8a40b2bef5b8bbae2e36a0b103f142ac53557ab421dc16ac4aafee6f53dc"},
|
||||
{file = "charset_normalizer-3.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:503e65837c71b875ecdd733877d852adbc465bd82c768a067badd953bf1bc5a3"},
|
||||
{file = "charset_normalizer-3.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a60332922359f920193b1d4826953c507a877b523b2395ad7bc716ddd386d866"},
|
||||
{file = "charset_normalizer-3.0.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:16a8663d6e281208d78806dbe14ee9903715361cf81f6d4309944e4d1e59ac5b"},
|
||||
{file = "charset_normalizer-3.0.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:a16418ecf1329f71df119e8a65f3aa68004a3f9383821edcb20f0702934d8087"},
|
||||
{file = "charset_normalizer-3.0.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:9d9153257a3f70d5f69edf2325357251ed20f772b12e593f3b3377b5f78e7ef8"},
|
||||
{file = "charset_normalizer-3.0.1-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:02a51034802cbf38db3f89c66fb5d2ec57e6fe7ef2f4a44d070a593c3688667b"},
|
||||
{file = "charset_normalizer-3.0.1-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:2e396d70bc4ef5325b72b593a72c8979999aa52fb8bcf03f701c1b03e1166918"},
|
||||
{file = "charset_normalizer-3.0.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:11b53acf2411c3b09e6af37e4b9005cba376c872503c8f28218c7243582df45d"},
|
||||
{file = "charset_normalizer-3.0.1-cp37-cp37m-win32.whl", hash = "sha256:0bf2dae5291758b6f84cf923bfaa285632816007db0330002fa1de38bfcb7154"},
|
||||
{file = "charset_normalizer-3.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:2c03cc56021a4bd59be889c2b9257dae13bf55041a3372d3295416f86b295fb5"},
|
||||
{file = "charset_normalizer-3.0.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:024e606be3ed92216e2b6952ed859d86b4cfa52cd5bc5f050e7dc28f9b43ec42"},
|
||||
{file = "charset_normalizer-3.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:4b0d02d7102dd0f997580b51edc4cebcf2ab6397a7edf89f1c73b586c614272c"},
|
||||
{file = "charset_normalizer-3.0.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:358a7c4cb8ba9b46c453b1dd8d9e431452d5249072e4f56cfda3149f6ab1405e"},
|
||||
{file = "charset_normalizer-3.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:81d6741ab457d14fdedc215516665050f3822d3e56508921cc7239f8c8e66a58"},
|
||||
{file = "charset_normalizer-3.0.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8b8af03d2e37866d023ad0ddea594edefc31e827fee64f8de5611a1dbc373174"},
|
||||
{file = "charset_normalizer-3.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9cf4e8ad252f7c38dd1f676b46514f92dc0ebeb0db5552f5f403509705e24753"},
|
||||
{file = "charset_normalizer-3.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e696f0dd336161fca9adbb846875d40752e6eba585843c768935ba5c9960722b"},
|
||||
{file = "charset_normalizer-3.0.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c22d3fe05ce11d3671297dc8973267daa0f938b93ec716e12e0f6dee81591dc1"},
|
||||
{file = "charset_normalizer-3.0.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:109487860ef6a328f3eec66f2bf78b0b72400280d8f8ea05f69c51644ba6521a"},
|
||||
{file = "charset_normalizer-3.0.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:37f8febc8ec50c14f3ec9637505f28e58d4f66752207ea177c1d67df25da5aed"},
|
||||
{file = "charset_normalizer-3.0.1-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:f97e83fa6c25693c7a35de154681fcc257c1c41b38beb0304b9c4d2d9e164479"},
|
||||
{file = "charset_normalizer-3.0.1-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:a152f5f33d64a6be73f1d30c9cc82dfc73cec6477ec268e7c6e4c7d23c2d2291"},
|
||||
{file = "charset_normalizer-3.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:39049da0ffb96c8cbb65cbf5c5f3ca3168990adf3551bd1dee10c48fce8ae820"},
|
||||
{file = "charset_normalizer-3.0.1-cp38-cp38-win32.whl", hash = "sha256:4457ea6774b5611f4bed5eaa5df55f70abde42364d498c5134b7ef4c6958e20e"},
|
||||
{file = "charset_normalizer-3.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:e62164b50f84e20601c1ff8eb55620d2ad25fb81b59e3cd776a1902527a788af"},
|
||||
{file = "charset_normalizer-3.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8eade758719add78ec36dc13201483f8e9b5d940329285edcd5f70c0a9edbd7f"},
|
||||
{file = "charset_normalizer-3.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8499ca8f4502af841f68135133d8258f7b32a53a1d594aa98cc52013fff55678"},
|
||||
{file = "charset_normalizer-3.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3fc1c4a2ffd64890aebdb3f97e1278b0cc72579a08ca4de8cd2c04799a3a22be"},
|
||||
{file = "charset_normalizer-3.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:00d3ffdaafe92a5dc603cb9bd5111aaa36dfa187c8285c543be562e61b755f6b"},
|
||||
{file = "charset_normalizer-3.0.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c2ac1b08635a8cd4e0cbeaf6f5e922085908d48eb05d44c5ae9eabab148512ca"},
|
||||
{file = "charset_normalizer-3.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f6f45710b4459401609ebebdbcfb34515da4fc2aa886f95107f556ac69a9147e"},
|
||||
{file = "charset_normalizer-3.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ae1de54a77dc0d6d5fcf623290af4266412a7c4be0b1ff7444394f03f5c54e3"},
|
||||
{file = "charset_normalizer-3.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3b590df687e3c5ee0deef9fc8c547d81986d9a1b56073d82de008744452d6541"},
|
||||
{file = "charset_normalizer-3.0.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ab5de034a886f616a5668aa5d098af2b5385ed70142090e2a31bcbd0af0fdb3d"},
|
||||
{file = "charset_normalizer-3.0.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9cb3032517f1627cc012dbc80a8ec976ae76d93ea2b5feaa9d2a5b8882597579"},
|
||||
{file = "charset_normalizer-3.0.1-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:608862a7bf6957f2333fc54ab4399e405baad0163dc9f8d99cb236816db169d4"},
|
||||
{file = "charset_normalizer-3.0.1-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:0f438ae3532723fb6ead77e7c604be7c8374094ef4ee2c5e03a3a17f1fca256c"},
|
||||
{file = "charset_normalizer-3.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:356541bf4381fa35856dafa6a965916e54bed415ad8a24ee6de6e37deccf2786"},
|
||||
{file = "charset_normalizer-3.0.1-cp39-cp39-win32.whl", hash = "sha256:39cf9ed17fe3b1bc81f33c9ceb6ce67683ee7526e65fde1447c772afc54a1bb8"},
|
||||
{file = "charset_normalizer-3.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:0a11e971ed097d24c534c037d298ad32c6ce81a45736d31e0ff0ad37ab437d59"},
|
||||
{file = "charset_normalizer-3.0.1-py3-none-any.whl", hash = "sha256:7e189e2e1d3ed2f4aebabd2d5b0f931e883676e51c7624826e0a4e5fe8a0bf24"},
|
||||
]
|
||||
cinemagoer = [
|
||||
{file = "cinemagoer-2022.12.27-py3-none-any.whl", hash = "sha256:9e295e2ed49fb93f2983d9b5e991e052ec48502a14bd8c53bf1ac88f95b67e6e"},
|
||||
{file = "cinemagoer-2022.12.27.tar.gz", hash = "sha256:b94abfe948a3bfa92b04d09ae5fb41586fee60bb3fe692f20ce34bbc1a318d8a"},
|
||||
]
|
||||
click = [
|
||||
{file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"},
|
||||
@ -1547,57 +1751,57 @@ colorlog = [
|
||||
{file = "colorlog-6.7.0.tar.gz", hash = "sha256:bd94bd21c1e13fac7bd3153f4bc3a7dc0eb0974b8bc2fdf1a989e474f6e582e5"},
|
||||
]
|
||||
coverage = [
|
||||
{file = "coverage-7.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f7c51b6074a8a3063c341953dffe48fd6674f8e4b1d3c8aa8a91f58d6e716a8"},
|
||||
{file = "coverage-7.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:628f47eaf66727fc986d3b190d6fa32f5e6b7754a243919d28bc0fd7974c449f"},
|
||||
{file = "coverage-7.0.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e89d5abf86c104de808108a25d171ad646c07eda96ca76c8b237b94b9c71e518"},
|
||||
{file = "coverage-7.0.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:75e43c6f4ea4d122dac389aabdf9d4f0e160770a75e63372f88005d90f5bcc80"},
|
||||
{file = "coverage-7.0.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49da0ff241827ebb52d5d6d5a36d33b455fa5e721d44689c95df99fd8db82437"},
|
||||
{file = "coverage-7.0.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0bce4ad5bdd0b02e177a085d28d2cea5fc57bb4ba2cead395e763e34cf934eb1"},
|
||||
{file = "coverage-7.0.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:f79691335257d60951638dd43576b9bcd6f52baa5c1c2cd07a509bb003238372"},
|
||||
{file = "coverage-7.0.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:5722269ed05fbdb94eef431787c66b66260ff3125d1a9afcc00facff8c45adf9"},
|
||||
{file = "coverage-7.0.3-cp310-cp310-win32.whl", hash = "sha256:bdbda870e0fda7dd0fe7db7135ca226ec4c1ade8aa76e96614829b56ca491012"},
|
||||
{file = "coverage-7.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:e56fae4292e216b8deeee38ace84557b9fa85b52db005368a275427cdabb8192"},
|
||||
{file = "coverage-7.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b82343a5bc51627b9d606f0b6b6b9551db7b6311a5dd920fa52a94beae2e8959"},
|
||||
{file = "coverage-7.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fd0a8aa431f9b7ad9eb8264f55ef83cbb254962af3775092fb6e93890dea9ca2"},
|
||||
{file = "coverage-7.0.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:112cfead1bd22eada8a8db9ed387bd3e8be5528debc42b5d3c1f7da4ffaf9fb5"},
|
||||
{file = "coverage-7.0.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:af87e906355fa42447be5c08c5d44e6e1c005bf142f303f726ddf5ed6e0c8a4d"},
|
||||
{file = "coverage-7.0.3-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f30090e22a301952c5abd0e493a1c8358b4f0b368b49fa3e4568ed3ed68b8d1f"},
|
||||
{file = "coverage-7.0.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ae871d09901911eedda1981ea6fd0f62a999107293cdc4c4fd612321c5b34745"},
|
||||
{file = "coverage-7.0.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:ed7c9debf7bfc63c9b9f8b595409237774ff4b061bf29fba6f53b287a2fdeab9"},
|
||||
{file = "coverage-7.0.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:13121fa22dcd2c7b19c5161e3fd725692448f05377b788da4502a383573227b3"},
|
||||
{file = "coverage-7.0.3-cp311-cp311-win32.whl", hash = "sha256:037b51ee86bc600f99b3b957c20a172431c35c2ef9c1ca34bc813ab5b51fd9f5"},
|
||||
{file = "coverage-7.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:25fde928306034e8deecd5fc91a07432dcc282c8acb76749581a28963c9f4f3f"},
|
||||
{file = "coverage-7.0.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:7e8b0642c38b3d3b3c01417643ccc645345b03c32a2e84ef93cdd6844d6fe530"},
|
||||
{file = "coverage-7.0.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18b09811f849cc958d23f733a350a66b54a8de3fed1e6128ba55a5c97ffb6f65"},
|
||||
{file = "coverage-7.0.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:349d0b545520e8516f7b4f12373afc705d17d901e1de6a37a20e4ec9332b61f7"},
|
||||
{file = "coverage-7.0.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b5b38813eee5b4739f505d94247604c72eae626d5088a16dd77b08b8b1724ab3"},
|
||||
{file = "coverage-7.0.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:ba9af1218fa01b1f11c72271bc7290b701d11ad4dbc2ae97c445ecacf6858dba"},
|
||||
{file = "coverage-7.0.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c5648c7eec5cf1ba5db1cf2d6c10036a582d7f09e172990474a122e30c841361"},
|
||||
{file = "coverage-7.0.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d0df04495b76a885bfef009f45eebe8fe2fbf815ad7a83dabcf5aced62f33162"},
|
||||
{file = "coverage-7.0.3-cp37-cp37m-win32.whl", hash = "sha256:af6cef3796b8068713a48dd67d258dc9a6e2ebc3bd4645bfac03a09672fa5d20"},
|
||||
{file = "coverage-7.0.3-cp37-cp37m-win_amd64.whl", hash = "sha256:62ef3800c4058844e2e3fa35faa9dd0ccde8a8aba6c763aae50342e00d4479d4"},
|
||||
{file = "coverage-7.0.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:acef7f3a3825a2d218a03dd02f5f3cc7f27aa31d882dd780191d1ad101120d74"},
|
||||
{file = "coverage-7.0.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a530663a361eb27375cec28aea5cd282089b5e4b022ae451c4c3493b026a68a5"},
|
||||
{file = "coverage-7.0.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c58cd6bb46dcb922e0d5792850aab5964433d511b3a020867650f8d930dde4f4"},
|
||||
{file = "coverage-7.0.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f918e9ef4c98f477a5458238dde2a1643aed956c7213873ab6b6b82e32b8ef61"},
|
||||
{file = "coverage-7.0.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2b865aa679bee7fbd1c55960940dbd3252621dd81468268786c67122bbd15343"},
|
||||
{file = "coverage-7.0.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:c5d9b480ebae60fc2cbc8d6865194136bc690538fa542ba58726433bed6e04cc"},
|
||||
{file = "coverage-7.0.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:985ad2af5ec3dbb4fd75d5b0735752c527ad183455520055a08cf8d6794cabfc"},
|
||||
{file = "coverage-7.0.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:ca15308ef722f120967af7474ba6a453e0f5b6f331251e20b8145497cf1bc14a"},
|
||||
{file = "coverage-7.0.3-cp38-cp38-win32.whl", hash = "sha256:c1cee10662c25c94415bbb987f2ec0e6ba9e8fce786334b10be7e6a7ab958f69"},
|
||||
{file = "coverage-7.0.3-cp38-cp38-win_amd64.whl", hash = "sha256:44d6a556de4418f1f3bfd57094b8c49f0408df5a433cf0d253eeb3075261c762"},
|
||||
{file = "coverage-7.0.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e6dcc70a25cb95df0ae33dfc701de9b09c37f7dd9f00394d684a5b57257f8246"},
|
||||
{file = "coverage-7.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:bf76d79dfaea802f0f28f50153ffbc1a74ae1ee73e480baeda410b4f3e7ab25f"},
|
||||
{file = "coverage-7.0.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88834e5d56d01c141c29deedacba5773fe0bed900b1edc957595a8a6c0da1c3c"},
|
||||
{file = "coverage-7.0.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ef001a60e888f8741e42e5aa79ae55c91be73761e4df5e806efca1ddd62fd400"},
|
||||
{file = "coverage-7.0.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4959dc506be74e4963bd2c42f7b87d8e4b289891201e19ec551e64c6aa5441f8"},
|
||||
{file = "coverage-7.0.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b791beb17b32ac019a78cfbe6184f992b6273fdca31145b928ad2099435e2fcb"},
|
||||
{file = "coverage-7.0.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:b07651e3b9af8f1a092861d88b4c74d913634a7f1f2280fca0ad041ad84e9e96"},
|
||||
{file = "coverage-7.0.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:55e46fa4168ccb7497c9be78627fcb147e06f474f846a10d55feeb5108a24ef0"},
|
||||
{file = "coverage-7.0.3-cp39-cp39-win32.whl", hash = "sha256:e3f1cd1cd65695b1540b3cf7828d05b3515974a9d7c7530f762ac40f58a18161"},
|
||||
{file = "coverage-7.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:d8249666c23683f74f8f93aeaa8794ac87cc61c40ff70374a825f3352a4371dc"},
|
||||
{file = "coverage-7.0.3-pp37.pp38.pp39-none-any.whl", hash = "sha256:b1ffc8f58b81baed3f8962e28c30d99442079b82ce1ec836a1f67c0accad91c1"},
|
||||
{file = "coverage-7.0.3.tar.gz", hash = "sha256:d5be4e93acce64f516bf4fd239c0e6118fc913c93fa1a3f52d15bdcc60d97b2d"},
|
||||
{file = "coverage-7.0.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2a7f23bbaeb2a87f90f607730b45564076d870f1fb07b9318d0c21f36871932b"},
|
||||
{file = "coverage-7.0.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c18d47f314b950dbf24a41787ced1474e01ca816011925976d90a88b27c22b89"},
|
||||
{file = "coverage-7.0.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ef14d75d86f104f03dea66c13188487151760ef25dd6b2dbd541885185f05f40"},
|
||||
{file = "coverage-7.0.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66e50680e888840c0995f2ad766e726ce71ca682e3c5f4eee82272c7671d38a2"},
|
||||
{file = "coverage-7.0.5-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a9fed35ca8c6e946e877893bbac022e8563b94404a605af1d1e6accc7eb73289"},
|
||||
{file = "coverage-7.0.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d8d04e755934195bdc1db45ba9e040b8d20d046d04d6d77e71b3b34a8cc002d0"},
|
||||
{file = "coverage-7.0.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:7e109f1c9a3ece676597831874126555997c48f62bddbcace6ed17be3e372de8"},
|
||||
{file = "coverage-7.0.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0a1890fca2962c4f1ad16551d660b46ea77291fba2cc21c024cd527b9d9c8809"},
|
||||
{file = "coverage-7.0.5-cp310-cp310-win32.whl", hash = "sha256:be9fcf32c010da0ba40bf4ee01889d6c737658f4ddff160bd7eb9cac8f094b21"},
|
||||
{file = "coverage-7.0.5-cp310-cp310-win_amd64.whl", hash = "sha256:cbfcba14a3225b055a28b3199c3d81cd0ab37d2353ffd7f6fd64844cebab31ad"},
|
||||
{file = "coverage-7.0.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:30b5fec1d34cc932c1bc04017b538ce16bf84e239378b8f75220478645d11fca"},
|
||||
{file = "coverage-7.0.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1caed2367b32cc80a2b7f58a9f46658218a19c6cfe5bc234021966dc3daa01f0"},
|
||||
{file = "coverage-7.0.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d254666d29540a72d17cc0175746cfb03d5123db33e67d1020e42dae611dc196"},
|
||||
{file = "coverage-7.0.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:19245c249aa711d954623d94f23cc94c0fd65865661f20b7781210cb97c471c0"},
|
||||
{file = "coverage-7.0.5-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b05ed4b35bf6ee790832f68932baf1f00caa32283d66cc4d455c9e9d115aafc"},
|
||||
{file = "coverage-7.0.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:29de916ba1099ba2aab76aca101580006adfac5646de9b7c010a0f13867cba45"},
|
||||
{file = "coverage-7.0.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:e057e74e53db78122a3979f908973e171909a58ac20df05c33998d52e6d35757"},
|
||||
{file = "coverage-7.0.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:411d4ff9d041be08fdfc02adf62e89c735b9468f6d8f6427f8a14b6bb0a85095"},
|
||||
{file = "coverage-7.0.5-cp311-cp311-win32.whl", hash = "sha256:52ab14b9e09ce052237dfe12d6892dd39b0401690856bcfe75d5baba4bfe2831"},
|
||||
{file = "coverage-7.0.5-cp311-cp311-win_amd64.whl", hash = "sha256:1f66862d3a41674ebd8d1a7b6f5387fe5ce353f8719040a986551a545d7d83ea"},
|
||||
{file = "coverage-7.0.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b69522b168a6b64edf0c33ba53eac491c0a8f5cc94fa4337f9c6f4c8f2f5296c"},
|
||||
{file = "coverage-7.0.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:436e103950d05b7d7f55e39beeb4d5be298ca3e119e0589c0227e6d0b01ee8c7"},
|
||||
{file = "coverage-7.0.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b8c56bec53d6e3154eaff6ea941226e7bd7cc0d99f9b3756c2520fc7a94e6d96"},
|
||||
{file = "coverage-7.0.5-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a38362528a9115a4e276e65eeabf67dcfaf57698e17ae388599568a78dcb029"},
|
||||
{file = "coverage-7.0.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:f67472c09a0c7486e27f3275f617c964d25e35727af952869dd496b9b5b7f6a3"},
|
||||
{file = "coverage-7.0.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:220e3fa77d14c8a507b2d951e463b57a1f7810a6443a26f9b7591ef39047b1b2"},
|
||||
{file = "coverage-7.0.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ecb0f73954892f98611e183f50acdc9e21a4653f294dfbe079da73c6378a6f47"},
|
||||
{file = "coverage-7.0.5-cp37-cp37m-win32.whl", hash = "sha256:d8f3e2e0a1d6777e58e834fd5a04657f66affa615dae61dd67c35d1568c38882"},
|
||||
{file = "coverage-7.0.5-cp37-cp37m-win_amd64.whl", hash = "sha256:9e662e6fc4f513b79da5d10a23edd2b87685815b337b1a30cd11307a6679148d"},
|
||||
{file = "coverage-7.0.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:790e4433962c9f454e213b21b0fd4b42310ade9c077e8edcb5113db0818450cb"},
|
||||
{file = "coverage-7.0.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:49640bda9bda35b057b0e65b7c43ba706fa2335c9a9896652aebe0fa399e80e6"},
|
||||
{file = "coverage-7.0.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d66187792bfe56f8c18ba986a0e4ae44856b1c645336bd2c776e3386da91e1dd"},
|
||||
{file = "coverage-7.0.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:276f4cd0001cd83b00817c8db76730938b1ee40f4993b6a905f40a7278103b3a"},
|
||||
{file = "coverage-7.0.5-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95304068686545aa368b35dfda1cdfbbdbe2f6fe43de4a2e9baa8ebd71be46e2"},
|
||||
{file = "coverage-7.0.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:17e01dd8666c445025c29684d4aabf5a90dc6ef1ab25328aa52bedaa95b65ad7"},
|
||||
{file = "coverage-7.0.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:ea76dbcad0b7b0deb265d8c36e0801abcddf6cc1395940a24e3595288b405ca0"},
|
||||
{file = "coverage-7.0.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:50a6adc2be8edd7ee67d1abc3cd20678987c7b9d79cd265de55941e3d0d56499"},
|
||||
{file = "coverage-7.0.5-cp38-cp38-win32.whl", hash = "sha256:e4ce984133b888cc3a46867c8b4372c7dee9cee300335e2925e197bcd45b9e16"},
|
||||
{file = "coverage-7.0.5-cp38-cp38-win_amd64.whl", hash = "sha256:4a950f83fd3f9bca23b77442f3a2b2ea4ac900944d8af9993743774c4fdc57af"},
|
||||
{file = "coverage-7.0.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3c2155943896ac78b9b0fd910fb381186d0c345911f5333ee46ac44c8f0e43ab"},
|
||||
{file = "coverage-7.0.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:54f7e9705e14b2c9f6abdeb127c390f679f6dbe64ba732788d3015f7f76ef637"},
|
||||
{file = "coverage-7.0.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ee30375b409d9a7ea0f30c50645d436b6f5dfee254edffd27e45a980ad2c7f4"},
|
||||
{file = "coverage-7.0.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b78729038abea6a5df0d2708dce21e82073463b2d79d10884d7d591e0f385ded"},
|
||||
{file = "coverage-7.0.5-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13250b1f0bd023e0c9f11838bdeb60214dd5b6aaf8e8d2f110c7e232a1bff83b"},
|
||||
{file = "coverage-7.0.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:2c407b1950b2d2ffa091f4e225ca19a66a9bd81222f27c56bd12658fc5ca1209"},
|
||||
{file = "coverage-7.0.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:c76a3075e96b9c9ff00df8b5f7f560f5634dffd1658bafb79eb2682867e94f78"},
|
||||
{file = "coverage-7.0.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:f26648e1b3b03b6022b48a9b910d0ae209e2d51f50441db5dce5b530fad6d9b1"},
|
||||
{file = "coverage-7.0.5-cp39-cp39-win32.whl", hash = "sha256:ba3027deb7abf02859aca49c865ece538aee56dcb4871b4cced23ba4d5088904"},
|
||||
{file = "coverage-7.0.5-cp39-cp39-win_amd64.whl", hash = "sha256:949844af60ee96a376aac1ded2a27e134b8c8d35cc006a52903fc06c24a3296f"},
|
||||
{file = "coverage-7.0.5-pp37.pp38.pp39-none-any.whl", hash = "sha256:b9727ac4f5cf2cbf87880a63870b5b9730a8ae3a4a360241a0fdaa2f71240ff0"},
|
||||
{file = "coverage-7.0.5.tar.gz", hash = "sha256:051afcbd6d2ac39298d62d340f94dbb6a1f31de06dfaf6fcef7b759dd3860c45"},
|
||||
]
|
||||
cryptography = [
|
||||
{file = "cryptography-39.0.0-cp36-abi3-macosx_10_12_universal2.whl", hash = "sha256:c52a1a6f81e738d07f43dab57831c29e57d21c81a942f4602fac7ee21b27f288"},
|
||||
@ -1639,6 +1843,10 @@ django = [
|
||||
django-allauth = [
|
||||
{file = "django-allauth-0.50.0.tar.gz", hash = "sha256:ee3a174e249771caeb1d037e64b2704dd3c56cfec44f2058fae2214b224d35e8"},
|
||||
]
|
||||
django-cachalot = [
|
||||
{file = "django-cachalot-2.5.2.tar.gz", hash = "sha256:bf0420cb8fa5cb4ceebd373a069cf91bb7afb995850cc35e91ccdadfa6cc41b1"},
|
||||
{file = "django_cachalot-2.5.2-py3-none-any.whl", hash = "sha256:7db091320ae352e9ad88c22336cff8d64fb928571287f1fae5299a187fc1a17e"},
|
||||
]
|
||||
django-celery-results = [
|
||||
{file = "django_celery_results-2.4.0-py3-none-any.whl", hash = "sha256:be91307c02fbbf0dda21993c3001c60edb74595444ccd6ad696552fe3689e85b"},
|
||||
{file = "django_celery_results-2.4.0.tar.gz", hash = "sha256:75aa51970db5691cbf242c6a0ff50c8cdf419e265cd0e9b772335d06436c4b99"},
|
||||
@ -1679,10 +1887,6 @@ flake8 = [
|
||||
{file = "flake8-5.0.4-py2.py3-none-any.whl", hash = "sha256:7a1cf6b73744f5806ab95e526f6f0d8c01c66d7bbe349562d22dfca20610b248"},
|
||||
{file = "flake8-5.0.4.tar.gz", hash = "sha256:6fbe320aad8d6b95cec8b8e47bc933004678dc63095be98528b7bdd2a9f510db"},
|
||||
]
|
||||
freezegun = [
|
||||
{file = "freezegun-1.2.2-py3-none-any.whl", hash = "sha256:ea1b963b993cb9ea195adbd893a48d573fda951b0da64f60883d7e988b606c9f"},
|
||||
{file = "freezegun-1.2.2.tar.gz", hash = "sha256:cd22d1ba06941384410cd967d8a99d5ae2442f57dfafeff2fda5de8dc5c05446"},
|
||||
]
|
||||
gitdb = [
|
||||
{file = "gitdb-4.0.10-py3-none-any.whl", hash = "sha256:c286cf298426064079ed96a9e4a9d39e7f3e9bf15ba60701e95f5492f28415c7"},
|
||||
{file = "gitdb-4.0.10.tar.gz", hash = "sha256:6eb990b69df4e15bad899ea868dc46572c3f75339735663b81de79b06f17eb9a"},
|
||||
@ -1691,6 +1895,68 @@ gitpython = [
|
||||
{file = "GitPython-3.1.30-py3-none-any.whl", hash = "sha256:cd455b0000615c60e286208ba540271af9fe531fa6a87cc590a7298785ab2882"},
|
||||
{file = "GitPython-3.1.30.tar.gz", hash = "sha256:769c2d83e13f5d938b7688479da374c4e3d49f71549aaf462b646db9602ea6f8"},
|
||||
]
|
||||
greenlet = [
|
||||
{file = "greenlet-2.0.1-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:9ed358312e63bf683b9ef22c8e442ef6c5c02973f0c2a939ec1d7b50c974015c"},
|
||||
{file = "greenlet-2.0.1-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:4f09b0010e55bec3239278f642a8a506b91034f03a4fb28289a7d448a67f1515"},
|
||||
{file = "greenlet-2.0.1-cp27-cp27m-win32.whl", hash = "sha256:1407fe45246632d0ffb7a3f4a520ba4e6051fc2cbd61ba1f806900c27f47706a"},
|
||||
{file = "greenlet-2.0.1-cp27-cp27m-win_amd64.whl", hash = "sha256:3001d00eba6bbf084ae60ec7f4bb8ed375748f53aeaefaf2a37d9f0370558524"},
|
||||
{file = "greenlet-2.0.1-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:d566b82e92ff2e09dd6342df7e0eb4ff6275a3f08db284888dcd98134dbd4243"},
|
||||
{file = "greenlet-2.0.1-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:0722c9be0797f544a3ed212569ca3fe3d9d1a1b13942d10dd6f0e8601e484d26"},
|
||||
{file = "greenlet-2.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4d37990425b4687ade27810e3b1a1c37825d242ebc275066cfee8cb6b8829ccd"},
|
||||
{file = "greenlet-2.0.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:be35822f35f99dcc48152c9839d0171a06186f2d71ef76dc57fa556cc9bf6b45"},
|
||||
{file = "greenlet-2.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c140e7eb5ce47249668056edf3b7e9900c6a2e22fb0eaf0513f18a1b2c14e1da"},
|
||||
{file = "greenlet-2.0.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d21681f09e297a5adaa73060737e3aa1279a13ecdcfcc6ef66c292cb25125b2d"},
|
||||
{file = "greenlet-2.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fb412b7db83fe56847df9c47b6fe3f13911b06339c2aa02dcc09dce8bbf582cd"},
|
||||
{file = "greenlet-2.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:c6a08799e9e88052221adca55741bf106ec7ea0710bca635c208b751f0d5b617"},
|
||||
{file = "greenlet-2.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9e112e03d37987d7b90c1e98ba5e1b59e1645226d78d73282f45b326f7bddcb9"},
|
||||
{file = "greenlet-2.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:56961cfca7da2fdd178f95ca407fa330c64f33289e1804b592a77d5593d9bd94"},
|
||||
{file = "greenlet-2.0.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:13ba6e8e326e2116c954074c994da14954982ba2795aebb881c07ac5d093a58a"},
|
||||
{file = "greenlet-2.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bf633a50cc93ed17e494015897361010fc08700d92676c87931d3ea464123ce"},
|
||||
{file = "greenlet-2.0.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:9f2c221eecb7ead00b8e3ddb913c67f75cba078fd1d326053225a3f59d850d72"},
|
||||
{file = "greenlet-2.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:13ebf93c343dd8bd010cd98e617cb4c1c1f352a0cf2524c82d3814154116aa82"},
|
||||
{file = "greenlet-2.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:6f61d71bbc9b4a3de768371b210d906726535d6ca43506737682caa754b956cd"},
|
||||
{file = "greenlet-2.0.1-cp35-cp35m-macosx_10_14_x86_64.whl", hash = "sha256:2d0bac0385d2b43a7bd1d651621a4e0f1380abc63d6fb1012213a401cbd5bf8f"},
|
||||
{file = "greenlet-2.0.1-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:f6327b6907b4cb72f650a5b7b1be23a2aab395017aa6f1adb13069d66360eb3f"},
|
||||
{file = "greenlet-2.0.1-cp35-cp35m-win32.whl", hash = "sha256:81b0ea3715bf6a848d6f7149d25bf018fd24554a4be01fcbbe3fdc78e890b955"},
|
||||
{file = "greenlet-2.0.1-cp35-cp35m-win_amd64.whl", hash = "sha256:38255a3f1e8942573b067510f9611fc9e38196077b0c8eb7a8c795e105f9ce77"},
|
||||
{file = "greenlet-2.0.1-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:04957dc96669be041e0c260964cfef4c77287f07c40452e61abe19d647505581"},
|
||||
{file = "greenlet-2.0.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:4aeaebcd91d9fee9aa768c1b39cb12214b30bf36d2b7370505a9f2165fedd8d9"},
|
||||
{file = "greenlet-2.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:974a39bdb8c90a85982cdb78a103a32e0b1be986d411303064b28a80611f6e51"},
|
||||
{file = "greenlet-2.0.1-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8dca09dedf1bd8684767bc736cc20c97c29bc0c04c413e3276e0962cd7aeb148"},
|
||||
{file = "greenlet-2.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a4c0757db9bd08470ff8277791795e70d0bf035a011a528ee9a5ce9454b6cba2"},
|
||||
{file = "greenlet-2.0.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:5067920de254f1a2dee8d3d9d7e4e03718e8fd2d2d9db962c8c9fa781ae82a39"},
|
||||
{file = "greenlet-2.0.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:5a8e05057fab2a365c81abc696cb753da7549d20266e8511eb6c9d9f72fe3e92"},
|
||||
{file = "greenlet-2.0.1-cp36-cp36m-win32.whl", hash = "sha256:3d75b8d013086b08e801fbbb896f7d5c9e6ccd44f13a9241d2bf7c0df9eda928"},
|
||||
{file = "greenlet-2.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:097e3dae69321e9100202fc62977f687454cd0ea147d0fd5a766e57450c569fd"},
|
||||
{file = "greenlet-2.0.1-cp37-cp37m-macosx_10_15_x86_64.whl", hash = "sha256:cb242fc2cda5a307a7698c93173d3627a2a90d00507bccf5bc228851e8304963"},
|
||||
{file = "greenlet-2.0.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:72b00a8e7c25dcea5946692a2485b1a0c0661ed93ecfedfa9b6687bd89a24ef5"},
|
||||
{file = "greenlet-2.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5b0ff9878333823226d270417f24f4d06f235cb3e54d1103b71ea537a6a86ce"},
|
||||
{file = "greenlet-2.0.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:be9e0fb2ada7e5124f5282d6381903183ecc73ea019568d6d63d33f25b2a9000"},
|
||||
{file = "greenlet-2.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b493db84d124805865adc587532ebad30efa68f79ad68f11b336e0a51ec86c2"},
|
||||
{file = "greenlet-2.0.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:0459d94f73265744fee4c2d5ec44c6f34aa8a31017e6e9de770f7bcf29710be9"},
|
||||
{file = "greenlet-2.0.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:a20d33124935d27b80e6fdacbd34205732660e0a1d35d8b10b3328179a2b51a1"},
|
||||
{file = "greenlet-2.0.1-cp37-cp37m-win32.whl", hash = "sha256:ea688d11707d30e212e0110a1aac7f7f3f542a259235d396f88be68b649e47d1"},
|
||||
{file = "greenlet-2.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:afe07421c969e259e9403c3bb658968702bc3b78ec0b6fde3ae1e73440529c23"},
|
||||
{file = "greenlet-2.0.1-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:cd4ccc364cf75d1422e66e247e52a93da6a9b73cefa8cad696f3cbbb75af179d"},
|
||||
{file = "greenlet-2.0.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:4c8b1c43e75c42a6cafcc71defa9e01ead39ae80bd733a2608b297412beede68"},
|
||||
{file = "greenlet-2.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:659f167f419a4609bc0516fb18ea69ed39dbb25594934bd2dd4d0401660e8a1e"},
|
||||
{file = "greenlet-2.0.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:356e4519d4dfa766d50ecc498544b44c0249b6de66426041d7f8b751de4d6b48"},
|
||||
{file = "greenlet-2.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:811e1d37d60b47cb8126e0a929b58c046251f28117cb16fcd371eed61f66b764"},
|
||||
{file = "greenlet-2.0.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:d38ffd0e81ba8ef347d2be0772e899c289b59ff150ebbbbe05dc61b1246eb4e0"},
|
||||
{file = "greenlet-2.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:0109af1138afbfb8ae647e31a2b1ab030f58b21dd8528c27beaeb0093b7938a9"},
|
||||
{file = "greenlet-2.0.1-cp38-cp38-win32.whl", hash = "sha256:88c8d517e78acdf7df8a2134a3c4b964415b575d2840a2746ddb1cc6175f8608"},
|
||||
{file = "greenlet-2.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:d6ee1aa7ab36475035eb48c01efae87d37936a8173fc4d7b10bb02c2d75dd8f6"},
|
||||
{file = "greenlet-2.0.1-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:b1992ba9d4780d9af9726bbcef6a1db12d9ab1ccc35e5773685a24b7fb2758eb"},
|
||||
{file = "greenlet-2.0.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:b5e83e4de81dcc9425598d9469a624826a0b1211380ac444c7c791d4a2137c19"},
|
||||
{file = "greenlet-2.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:505138d4fa69462447a562a7c2ef723c6025ba12ac04478bc1ce2fcc279a2db5"},
|
||||
{file = "greenlet-2.0.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cce1e90dd302f45716a7715517c6aa0468af0bf38e814ad4eab58e88fc09f7f7"},
|
||||
{file = "greenlet-2.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e9744c657d896c7b580455e739899e492a4a452e2dd4d2b3e459f6b244a638d"},
|
||||
{file = "greenlet-2.0.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:662e8f7cad915ba75d8017b3e601afc01ef20deeeabf281bd00369de196d7726"},
|
||||
{file = "greenlet-2.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:41b825d65f31e394b523c84db84f9383a2f7eefc13d987f308f4663794d2687e"},
|
||||
{file = "greenlet-2.0.1-cp39-cp39-win32.whl", hash = "sha256:db38f80540083ea33bdab614a9d28bcec4b54daa5aff1668d7827a9fc769ae0a"},
|
||||
{file = "greenlet-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:b23d2a46d53210b498e5b701a1913697671988f4bf8e10f935433f6e7c332fb6"},
|
||||
{file = "greenlet-2.0.1.tar.gz", hash = "sha256:42e602564460da0e8ee67cb6d7236363ee5e131aa15943b6670e44e5c2ed0f67"},
|
||||
]
|
||||
gunicorn = [
|
||||
{file = "gunicorn-20.1.0-py3-none-any.whl", hash = "sha256:9dcc4547dbb1cb284accfb15ab5667a0e5d1881cc443e0677b4882a4067a807e"},
|
||||
{file = "gunicorn-20.1.0.tar.gz", hash = "sha256:e0a968b5ba15f8a328fdfd7ab1fcb5af4470c28aaf7e55df02a99bc13138e6e8"},
|
||||
@ -1708,8 +1974,8 @@ importlib-metadata = [
|
||||
{file = "importlib_metadata-6.0.0.tar.gz", hash = "sha256:e354bedeb60efa6affdcc8ae121b73544a7aa74156d047311948f6d711cd378d"},
|
||||
]
|
||||
iniconfig = [
|
||||
{file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"},
|
||||
{file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"},
|
||||
{file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"},
|
||||
{file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"},
|
||||
]
|
||||
isort = [
|
||||
{file = "isort-5.11.4-py3-none-any.whl", hash = "sha256:c033fd0edb91000a7f09527fe5c75321878f98322a77ddcc81adbd83724afb7b"},
|
||||
@ -1719,6 +1985,85 @@ kombu = [
|
||||
{file = "kombu-5.2.4-py3-none-any.whl", hash = "sha256:8b213b24293d3417bcf0d2f5537b7f756079e3ea232a8386dcc89a59fd2361a4"},
|
||||
{file = "kombu-5.2.4.tar.gz", hash = "sha256:37cee3ee725f94ea8bb173eaab7c1760203ea53bbebae226328600f9d2799610"},
|
||||
]
|
||||
lxml = [
|
||||
{file = "lxml-4.9.2-cp27-cp27m-macosx_10_15_x86_64.whl", hash = "sha256:76cf573e5a365e790396a5cc2b909812633409306c6531a6877c59061e42c4f2"},
|
||||
{file = "lxml-4.9.2-cp27-cp27m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b1f42b6921d0e81b1bcb5e395bc091a70f41c4d4e55ba99c6da2b31626c44892"},
|
||||
{file = "lxml-4.9.2-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:9f102706d0ca011de571de32c3247c6476b55bb6bc65a20f682f000b07a4852a"},
|
||||
{file = "lxml-4.9.2-cp27-cp27m-win32.whl", hash = "sha256:8d0b4612b66ff5d62d03bcaa043bb018f74dfea51184e53f067e6fdcba4bd8de"},
|
||||
{file = "lxml-4.9.2-cp27-cp27m-win_amd64.whl", hash = "sha256:4c8f293f14abc8fd3e8e01c5bd86e6ed0b6ef71936ded5bf10fe7a5efefbaca3"},
|
||||
{file = "lxml-4.9.2-cp27-cp27mu-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2899456259589aa38bfb018c364d6ae7b53c5c22d8e27d0ec7609c2a1ff78b50"},
|
||||
{file = "lxml-4.9.2-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:6749649eecd6a9871cae297bffa4ee76f90b4504a2a2ab528d9ebe912b101975"},
|
||||
{file = "lxml-4.9.2-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:a08cff61517ee26cb56f1e949cca38caabe9ea9fbb4b1e10a805dc39844b7d5c"},
|
||||
{file = "lxml-4.9.2-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:85cabf64adec449132e55616e7ca3e1000ab449d1d0f9d7f83146ed5bdcb6d8a"},
|
||||
{file = "lxml-4.9.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:8340225bd5e7a701c0fa98284c849c9b9fc9238abf53a0ebd90900f25d39a4e4"},
|
||||
{file = "lxml-4.9.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:1ab8f1f932e8f82355e75dda5413a57612c6ea448069d4fb2e217e9a4bed13d4"},
|
||||
{file = "lxml-4.9.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:699a9af7dffaf67deeae27b2112aa06b41c370d5e7633e0ee0aea2e0b6c211f7"},
|
||||
{file = "lxml-4.9.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b9cc34af337a97d470040f99ba4282f6e6bac88407d021688a5d585e44a23184"},
|
||||
{file = "lxml-4.9.2-cp310-cp310-win32.whl", hash = "sha256:d02a5399126a53492415d4906ab0ad0375a5456cc05c3fc0fc4ca11771745cda"},
|
||||
{file = "lxml-4.9.2-cp310-cp310-win_amd64.whl", hash = "sha256:a38486985ca49cfa574a507e7a2215c0c780fd1778bb6290c21193b7211702ab"},
|
||||
{file = "lxml-4.9.2-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:c83203addf554215463b59f6399835201999b5e48019dc17f182ed5ad87205c9"},
|
||||
{file = "lxml-4.9.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:2a87fa548561d2f4643c99cd13131acb607ddabb70682dcf1dff5f71f781a4bf"},
|
||||
{file = "lxml-4.9.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:d6b430a9938a5a5d85fc107d852262ddcd48602c120e3dbb02137c83d212b380"},
|
||||
{file = "lxml-4.9.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:3efea981d956a6f7173b4659849f55081867cf897e719f57383698af6f618a92"},
|
||||
{file = "lxml-4.9.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:df0623dcf9668ad0445e0558a21211d4e9a149ea8f5666917c8eeec515f0a6d1"},
|
||||
{file = "lxml-4.9.2-cp311-cp311-win32.whl", hash = "sha256:da248f93f0418a9e9d94b0080d7ebc407a9a5e6d0b57bb30db9b5cc28de1ad33"},
|
||||
{file = "lxml-4.9.2-cp311-cp311-win_amd64.whl", hash = "sha256:3818b8e2c4b5148567e1b09ce739006acfaa44ce3156f8cbbc11062994b8e8dd"},
|
||||
{file = "lxml-4.9.2-cp35-cp35m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ca989b91cf3a3ba28930a9fc1e9aeafc2a395448641df1f387a2d394638943b0"},
|
||||
{file = "lxml-4.9.2-cp35-cp35m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:822068f85e12a6e292803e112ab876bc03ed1f03dddb80154c395f891ca6b31e"},
|
||||
{file = "lxml-4.9.2-cp35-cp35m-win32.whl", hash = "sha256:be7292c55101e22f2a3d4d8913944cbea71eea90792bf914add27454a13905df"},
|
||||
{file = "lxml-4.9.2-cp35-cp35m-win_amd64.whl", hash = "sha256:998c7c41910666d2976928c38ea96a70d1aa43be6fe502f21a651e17483a43c5"},
|
||||
{file = "lxml-4.9.2-cp36-cp36m-macosx_10_15_x86_64.whl", hash = "sha256:b26a29f0b7fc6f0897f043ca366142d2b609dc60756ee6e4e90b5f762c6adc53"},
|
||||
{file = "lxml-4.9.2-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:ab323679b8b3030000f2be63e22cdeea5b47ee0abd2d6a1dc0c8103ddaa56cd7"},
|
||||
{file = "lxml-4.9.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:689bb688a1db722485e4610a503e3e9210dcc20c520b45ac8f7533c837be76fe"},
|
||||
{file = "lxml-4.9.2-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:f49e52d174375a7def9915c9f06ec4e569d235ad428f70751765f48d5926678c"},
|
||||
{file = "lxml-4.9.2-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:36c3c175d34652a35475a73762b545f4527aec044910a651d2bf50de9c3352b1"},
|
||||
{file = "lxml-4.9.2-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:a35f8b7fa99f90dd2f5dc5a9fa12332642f087a7641289ca6c40d6e1a2637d8e"},
|
||||
{file = "lxml-4.9.2-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:58bfa3aa19ca4c0f28c5dde0ff56c520fbac6f0daf4fac66ed4c8d2fb7f22e74"},
|
||||
{file = "lxml-4.9.2-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:bc718cd47b765e790eecb74d044cc8d37d58562f6c314ee9484df26276d36a38"},
|
||||
{file = "lxml-4.9.2-cp36-cp36m-win32.whl", hash = "sha256:d5bf6545cd27aaa8a13033ce56354ed9e25ab0e4ac3b5392b763d8d04b08e0c5"},
|
||||
{file = "lxml-4.9.2-cp36-cp36m-win_amd64.whl", hash = "sha256:3ab9fa9d6dc2a7f29d7affdf3edebf6ece6fb28a6d80b14c3b2fb9d39b9322c3"},
|
||||
{file = "lxml-4.9.2-cp37-cp37m-macosx_10_15_x86_64.whl", hash = "sha256:05ca3f6abf5cf78fe053da9b1166e062ade3fa5d4f92b4ed688127ea7d7b1d03"},
|
||||
{file = "lxml-4.9.2-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:a5da296eb617d18e497bcf0a5c528f5d3b18dadb3619fbdadf4ed2356ef8d941"},
|
||||
{file = "lxml-4.9.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:04876580c050a8c5341d706dd464ff04fd597095cc8c023252566a8826505726"},
|
||||
{file = "lxml-4.9.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:c9ec3eaf616d67db0764b3bb983962b4f385a1f08304fd30c7283954e6a7869b"},
|
||||
{file = "lxml-4.9.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2a29ba94d065945944016b6b74e538bdb1751a1db6ffb80c9d3c2e40d6fa9894"},
|
||||
{file = "lxml-4.9.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:a82d05da00a58b8e4c0008edbc8a4b6ec5a4bc1e2ee0fb6ed157cf634ed7fa45"},
|
||||
{file = "lxml-4.9.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:223f4232855ade399bd409331e6ca70fb5578efef22cf4069a6090acc0f53c0e"},
|
||||
{file = "lxml-4.9.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d17bc7c2ccf49c478c5bdd447594e82692c74222698cfc9b5daae7ae7e90743b"},
|
||||
{file = "lxml-4.9.2-cp37-cp37m-win32.whl", hash = "sha256:b64d891da92e232c36976c80ed7ebb383e3f148489796d8d31a5b6a677825efe"},
|
||||
{file = "lxml-4.9.2-cp37-cp37m-win_amd64.whl", hash = "sha256:a0a336d6d3e8b234a3aae3c674873d8f0e720b76bc1d9416866c41cd9500ffb9"},
|
||||
{file = "lxml-4.9.2-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:da4dd7c9c50c059aba52b3524f84d7de956f7fef88f0bafcf4ad7dde94a064e8"},
|
||||
{file = "lxml-4.9.2-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:821b7f59b99551c69c85a6039c65b75f5683bdc63270fec660f75da67469ca24"},
|
||||
{file = "lxml-4.9.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:e5168986b90a8d1f2f9dc1b841467c74221bd752537b99761a93d2d981e04889"},
|
||||
{file = "lxml-4.9.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:8e20cb5a47247e383cf4ff523205060991021233ebd6f924bca927fcf25cf86f"},
|
||||
{file = "lxml-4.9.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:13598ecfbd2e86ea7ae45ec28a2a54fb87ee9b9fdb0f6d343297d8e548392c03"},
|
||||
{file = "lxml-4.9.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:880bbbcbe2fca64e2f4d8e04db47bcdf504936fa2b33933efd945e1b429bea8c"},
|
||||
{file = "lxml-4.9.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:7d2278d59425777cfcb19735018d897ca8303abe67cc735f9f97177ceff8027f"},
|
||||
{file = "lxml-4.9.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5344a43228767f53a9df6e5b253f8cdca7dfc7b7aeae52551958192f56d98457"},
|
||||
{file = "lxml-4.9.2-cp38-cp38-win32.whl", hash = "sha256:925073b2fe14ab9b87e73f9a5fde6ce6392da430f3004d8b72cc86f746f5163b"},
|
||||
{file = "lxml-4.9.2-cp38-cp38-win_amd64.whl", hash = "sha256:9b22c5c66f67ae00c0199f6055705bc3eb3fcb08d03d2ec4059a2b1b25ed48d7"},
|
||||
{file = "lxml-4.9.2-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:5f50a1c177e2fa3ee0667a5ab79fdc6b23086bc8b589d90b93b4bd17eb0e64d1"},
|
||||
{file = "lxml-4.9.2-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:090c6543d3696cbe15b4ac6e175e576bcc3f1ccfbba970061b7300b0c15a2140"},
|
||||
{file = "lxml-4.9.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:63da2ccc0857c311d764e7d3d90f429c252e83b52d1f8f1d1fe55be26827d1f4"},
|
||||
{file = "lxml-4.9.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:5b4545b8a40478183ac06c073e81a5ce4cf01bf1734962577cf2bb569a5b3bbf"},
|
||||
{file = "lxml-4.9.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2e430cd2824f05f2d4f687701144556646bae8f249fd60aa1e4c768ba7018947"},
|
||||
{file = "lxml-4.9.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:6804daeb7ef69e7b36f76caddb85cccd63d0c56dedb47555d2fc969e2af6a1a5"},
|
||||
{file = "lxml-4.9.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:a6e441a86553c310258aca15d1c05903aaf4965b23f3bc2d55f200804e005ee5"},
|
||||
{file = "lxml-4.9.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ca34efc80a29351897e18888c71c6aca4a359247c87e0b1c7ada14f0ab0c0fb2"},
|
||||
{file = "lxml-4.9.2-cp39-cp39-win32.whl", hash = "sha256:6b418afe5df18233fc6b6093deb82a32895b6bb0b1155c2cdb05203f583053f1"},
|
||||
{file = "lxml-4.9.2-cp39-cp39-win_amd64.whl", hash = "sha256:f1496ea22ca2c830cbcbd473de8f114a320da308438ae65abad6bab7867fe38f"},
|
||||
{file = "lxml-4.9.2-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:b264171e3143d842ded311b7dccd46ff9ef34247129ff5bf5066123c55c2431c"},
|
||||
{file = "lxml-4.9.2-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:0dc313ef231edf866912e9d8f5a042ddab56c752619e92dfd3a2c277e6a7299a"},
|
||||
{file = "lxml-4.9.2-pp38-pypy38_pp73-macosx_10_15_x86_64.whl", hash = "sha256:16efd54337136e8cd72fb9485c368d91d77a47ee2d42b057564aae201257d419"},
|
||||
{file = "lxml-4.9.2-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:0f2b1e0d79180f344ff9f321327b005ca043a50ece8713de61d1cb383fb8ac05"},
|
||||
{file = "lxml-4.9.2-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:7b770ed79542ed52c519119473898198761d78beb24b107acf3ad65deae61f1f"},
|
||||
{file = "lxml-4.9.2-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:efa29c2fe6b4fdd32e8ef81c1528506895eca86e1d8c4657fda04c9b3786ddf9"},
|
||||
{file = "lxml-4.9.2-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7e91ee82f4199af8c43d8158024cbdff3d931df350252288f0d4ce656df7f3b5"},
|
||||
{file = "lxml-4.9.2-pp39-pypy39_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:b23e19989c355ca854276178a0463951a653309fb8e57ce674497f2d9f208746"},
|
||||
{file = "lxml-4.9.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:01d36c05f4afb8f7c20fd9ed5badca32a2029b93b1750f571ccc0b142531caf7"},
|
||||
{file = "lxml-4.9.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7b515674acfdcadb0eb5d00d8a709868173acece5cb0be3dd165950cbfdf5409"},
|
||||
{file = "lxml-4.9.2.tar.gz", hash = "sha256:2455cfaeb7ac70338b3257f41e21f0724f4b5b0c0e7702da67ee6c3640835b67"},
|
||||
]
|
||||
markdown = [
|
||||
{file = "Markdown-3.4.1-py3-none-any.whl", hash = "sha256:08fb8465cffd03d10b9dd34a5c3fea908e20391a2a90b88d66362cb05beed186"},
|
||||
{file = "Markdown-3.4.1.tar.gz", hash = "sha256:3b809086bb6efad416156e00a0da66fe47618a5d6918dd688f53f40c8e4cfeff"},
|
||||
@ -1727,6 +2072,10 @@ mccabe = [
|
||||
{file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"},
|
||||
{file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"},
|
||||
]
|
||||
mock = [
|
||||
{file = "mock-5.0.1-py3-none-any.whl", hash = "sha256:c41cfb1e99ba5d341fbcc5308836e7d7c9786d302f995b2c271ce2144dece9eb"},
|
||||
{file = "mock-5.0.1.tar.gz", hash = "sha256:e3ea505c03babf7977fd21674a69ad328053d414f05e6433c30d8fa14a534a6b"},
|
||||
]
|
||||
musicbrainzngs = [
|
||||
{file = "musicbrainzngs-0.7.1-py2.py3-none-any.whl", hash = "sha256:e841a8f975104c0a72290b09f59326050194081a5ae62ee512f41915090e1a10"},
|
||||
{file = "musicbrainzngs-0.7.1.tar.gz", hash = "sha256:ab1c0100fd0b305852e65f2ed4113c6de12e68afd55186987b8ed97e0f98e627"},
|
||||
@ -1769,16 +2118,16 @@ outcome = [
|
||||
{file = "outcome-1.2.0.tar.gz", hash = "sha256:6f82bd3de45da303cf1f771ecafa1633750a358436a8bb60e06a1ceb745d2672"},
|
||||
]
|
||||
packaging = [
|
||||
{file = "packaging-22.0-py3-none-any.whl", hash = "sha256:957e2148ba0e1a3b282772e791ef1d8083648bc131c8ab0c1feba110ce1146c3"},
|
||||
{file = "packaging-22.0.tar.gz", hash = "sha256:2198ec20bd4c017b8f9717e00f0c8714076fc2fd93816750ab48e2c41de2cfd3"},
|
||||
{file = "packaging-23.0-py3-none-any.whl", hash = "sha256:714ac14496c3e68c99c29b00845f7a2b85f3bb6f1078fd9f72fd20f0570002b2"},
|
||||
{file = "packaging-23.0.tar.gz", hash = "sha256:b6ad297f8907de0fa2fe1ccbd26fdaf387f5f47c7275fedf8cce89f99446cf97"},
|
||||
]
|
||||
pathspec = [
|
||||
{file = "pathspec-0.10.3-py3-none-any.whl", hash = "sha256:3c95343af8b756205e2aba76e843ba9520a24dd84f68c22b9f93251507509dd6"},
|
||||
{file = "pathspec-0.10.3.tar.gz", hash = "sha256:56200de4077d9d0791465aa9095a01d421861e405b5096955051deefd697d6f6"},
|
||||
]
|
||||
pbr = [
|
||||
{file = "pbr-5.11.0-py2.py3-none-any.whl", hash = "sha256:db2317ff07c84c4c63648c9064a79fe9d9f5c7ce85a9099d4b6258b3db83225a"},
|
||||
{file = "pbr-5.11.0.tar.gz", hash = "sha256:b97bc6695b2aff02144133c2e7399d5885223d42b7912ffaec2ca3898e673bfe"},
|
||||
{file = "pbr-5.11.1-py2.py3-none-any.whl", hash = "sha256:567f09558bae2b3ab53cb3c1e2e33e726ff3338e7bae3db5dc954b3a44eef12b"},
|
||||
{file = "pbr-5.11.1.tar.gz", hash = "sha256:aefc51675b0b533d56bb5fd1c8c6c0522fe31896679882e1c4c63d5e4a0fccb3"},
|
||||
]
|
||||
pillow = [
|
||||
{file = "Pillow-9.4.0-1-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:1b4b4e9dda4f4e4c4e6896f93e84a8f0bcca3b059de9ddf67dac3c334b1195e1"},
|
||||
@ -1904,9 +2253,13 @@ pysocks = [
|
||||
{file = "PySocks-1.7.1-py3-none-any.whl", hash = "sha256:2725bd0a9925919b9b51739eea5f9e2bae91e83288108a9ad338b2e3a4435ee5"},
|
||||
{file = "PySocks-1.7.1.tar.gz", hash = "sha256:3f8804571ebe159c380ac6de37643bb4685970655d3bba243530d6558b799aa0"},
|
||||
]
|
||||
pysportsdb = [
|
||||
{file = "pysportsdb-0.1.0-py3-none-any.whl", hash = "sha256:9e3a6654e1270a176cdb4bfcec123240bb9061f80db5bf421e5422b547efe7aa"},
|
||||
{file = "pysportsdb-0.1.0.tar.gz", hash = "sha256:d495ec5d1c416af8192be127ece500d4d2fd6224bb9a001044b823ac158d22b5"},
|
||||
]
|
||||
pytest = [
|
||||
{file = "pytest-7.2.0-py3-none-any.whl", hash = "sha256:892f933d339f068883b6fd5a459f03d85bfcb355e4981e146d2c7616c21fef71"},
|
||||
{file = "pytest-7.2.0.tar.gz", hash = "sha256:c4014eb40e10f11f355ad4e3c2fb2c6c6d1919c73f3b5a433de4708202cade59"},
|
||||
{file = "pytest-7.2.1-py3-none-any.whl", hash = "sha256:c7c6ca206e93355074ae32f7403e8ea12163b1163c976fee7d4d84027c162be5"},
|
||||
{file = "pytest-7.2.1.tar.gz", hash = "sha256:d45e0952f3727241918b8fd0f376f5ff6b301cc0777c6f9a556935c92d8a7d42"},
|
||||
]
|
||||
pytest-base-url = [
|
||||
{file = "pytest-base-url-2.0.0.tar.gz", hash = "sha256:e1e88a4fd221941572ccdcf3bf6c051392d2f8b6cef3e0bc7da95abec4b5346e"},
|
||||
@ -1919,6 +2272,10 @@ pytest-cov = [
|
||||
{file = "pytest-cov-3.0.0.tar.gz", hash = "sha256:e7f0f5b1617d2210a2cabc266dfe2f4c75a8d32fb89eafb7ad9d06f6d076d470"},
|
||||
{file = "pytest_cov-3.0.0-py3-none-any.whl", hash = "sha256:578d5d15ac4a25e5f961c938b85a05b09fdaae9deef3bb6de9a6e766622ca7a6"},
|
||||
]
|
||||
pytest-django = [
|
||||
{file = "pytest-django-4.5.2.tar.gz", hash = "sha256:d9076f759bb7c36939dbdd5ae6633c18edfc2902d1a69fdbefd2426b970ce6c2"},
|
||||
{file = "pytest_django-4.5.2-py3-none-any.whl", hash = "sha256:c60834861933773109334fe5a53e83d1ef4828f2203a1d6a0fa9972f4f75ab3e"},
|
||||
]
|
||||
pytest-flake8 = [
|
||||
{file = "pytest-flake8-1.1.1.tar.gz", hash = "sha256:ba4f243de3cb4c2486ed9e70752c80dd4b636f7ccb27d4eba763c35ed0cd316e"},
|
||||
{file = "pytest_flake8-1.1.1-py2.py3-none-any.whl", hash = "sha256:e0661a786f8cbf976c185f706fdaf5d6df0b1667c3bcff8e823ba263618627e7"},
|
||||
@ -1964,8 +2321,8 @@ python3-openid = [
|
||||
{file = "python3_openid-3.2.0-py3-none-any.whl", hash = "sha256:6626f771e0417486701e0b4daff762e7212e820ca5b29fcc0d05f6f8736dfa6b"},
|
||||
]
|
||||
pytz = [
|
||||
{file = "pytz-2022.7-py2.py3-none-any.whl", hash = "sha256:93007def75ae22f7cd991c84e02d434876818661f8df9ad5df9e950ff4e52cfd"},
|
||||
{file = "pytz-2022.7.tar.gz", hash = "sha256:7ccfae7b4b2c067464a6733c6261673fdb8fd1be905460396b97a073e9fa683a"},
|
||||
{file = "pytz-2022.7.1-py2.py3-none-any.whl", hash = "sha256:78f4f37d8198e0627c5f1143240bb0206b8691d8d7ac6d78fee88b78733f8c4a"},
|
||||
{file = "pytz-2022.7.1.tar.gz", hash = "sha256:01a0681c4b9684a28304615eba55d1ab31ae00bf68ec157ec3708a8182dbbcd0"},
|
||||
]
|
||||
pyyaml = [
|
||||
{file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"},
|
||||
@ -2010,12 +2367,12 @@ pyyaml = [
|
||||
{file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"},
|
||||
]
|
||||
redis = [
|
||||
{file = "redis-4.4.0-py3-none-any.whl", hash = "sha256:cae3ee5d1f57d8caf534cd8764edf3163c77e073bdd74b6f54a87ffafdc5e7d9"},
|
||||
{file = "redis-4.4.0.tar.gz", hash = "sha256:7b8c87d19c45d3f1271b124858d2a5c13160c4e74d4835e28273400fa34d5228"},
|
||||
{file = "redis-4.4.2-py3-none-any.whl", hash = "sha256:e6206448e2f8a432871d07d432c13ed6c2abcf6b74edb436c99752b1371be387"},
|
||||
{file = "redis-4.4.2.tar.gz", hash = "sha256:a010f6cb7378065040a02839c3f75c7e0fb37a87116fb4a95be82a95552776c7"},
|
||||
]
|
||||
requests = [
|
||||
{file = "requests-2.28.1-py3-none-any.whl", hash = "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349"},
|
||||
{file = "requests-2.28.1.tar.gz", hash = "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983"},
|
||||
{file = "requests-2.28.2-py3-none-any.whl", hash = "sha256:64299f4909223da747622c030b781c0d7811e359c37124b4bd368fb8c6518baa"},
|
||||
{file = "requests-2.28.2.tar.gz", hash = "sha256:98b1b2782e3c6c4904938b84c0eb932721069dfdb9134313beff7c83c2df24bf"},
|
||||
]
|
||||
requests-oauthlib = [
|
||||
{file = "requests-oauthlib-1.3.1.tar.gz", hash = "sha256:75beac4a47881eeb94d5ea5d6ad31ef88856affe2332b9aafb52c6452ccf0d7a"},
|
||||
@ -2026,8 +2383,8 @@ selenium = [
|
||||
{file = "selenium-4.7.2.tar.gz", hash = "sha256:3aefa14a28a42e520550c1cd0f29cf1d566328186ea63aa9a3e01fb265b5894d"},
|
||||
]
|
||||
setuptools = [
|
||||
{file = "setuptools-65.6.3-py3-none-any.whl", hash = "sha256:57f6f22bde4e042978bcd50176fdb381d7c21a9efa4041202288d3737a0c6a54"},
|
||||
{file = "setuptools-65.6.3.tar.gz", hash = "sha256:a7620757bf984b58deaf32fc8a4577a9bbc0850cf92c20e1ce41c38c19e5fb75"},
|
||||
{file = "setuptools-66.0.0-py3-none-any.whl", hash = "sha256:a78d01d1e2c175c474884671dde039962c9d74c7223db7369771fcf6e29ceeab"},
|
||||
{file = "setuptools-66.0.0.tar.gz", hash = "sha256:bd6eb2d6722568de6d14b87c44a96fac54b2a45ff5e940e639979a3d1792adb6"},
|
||||
]
|
||||
six = [
|
||||
{file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
|
||||
@ -2045,6 +2402,49 @@ sortedcontainers = [
|
||||
{file = "sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0"},
|
||||
{file = "sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88"},
|
||||
]
|
||||
sqlalchemy = [
|
||||
{file = "SQLAlchemy-1.4.46-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:7001f16a9a8e06488c3c7154827c48455d1c1507d7228d43e781afbc8ceccf6d"},
|
||||
{file = "SQLAlchemy-1.4.46-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:c7a46639ba058d320c9f53a81db38119a74b8a7a1884df44d09fbe807d028aaf"},
|
||||
{file = "SQLAlchemy-1.4.46-cp27-cp27m-win32.whl", hash = "sha256:c04144a24103135ea0315d459431ac196fe96f55d3213bfd6d39d0247775c854"},
|
||||
{file = "SQLAlchemy-1.4.46-cp27-cp27m-win_amd64.whl", hash = "sha256:7b81b1030c42b003fc10ddd17825571603117f848814a344d305262d370e7c34"},
|
||||
{file = "SQLAlchemy-1.4.46-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:939f9a018d2ad04036746e15d119c0428b1e557470361aa798e6e7d7f5875be0"},
|
||||
{file = "SQLAlchemy-1.4.46-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:b7f4b6aa6e87991ec7ce0e769689a977776db6704947e562102431474799a857"},
|
||||
{file = "SQLAlchemy-1.4.46-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5dbf17ac9a61e7a3f1c7ca47237aac93cabd7f08ad92ac5b96d6f8dea4287fc1"},
|
||||
{file = "SQLAlchemy-1.4.46-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:7f8267682eb41a0584cf66d8a697fef64b53281d01c93a503e1344197f2e01fe"},
|
||||
{file = "SQLAlchemy-1.4.46-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64cb0ad8a190bc22d2112001cfecdec45baffdf41871de777239da6a28ed74b6"},
|
||||
{file = "SQLAlchemy-1.4.46-cp310-cp310-win32.whl", hash = "sha256:5f752676fc126edc1c4af0ec2e4d2adca48ddfae5de46bb40adbd3f903eb2120"},
|
||||
{file = "SQLAlchemy-1.4.46-cp310-cp310-win_amd64.whl", hash = "sha256:31de1e2c45e67a5ec1ecca6ec26aefc299dd5151e355eb5199cd9516b57340be"},
|
||||
{file = "SQLAlchemy-1.4.46-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d68e1762997bfebf9e5cf2a9fd0bcf9ca2fdd8136ce7b24bbd3bbfa4328f3e4a"},
|
||||
{file = "SQLAlchemy-1.4.46-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4d112b0f3c1bc5ff70554a97344625ef621c1bfe02a73c5d97cac91f8cd7a41e"},
|
||||
{file = "SQLAlchemy-1.4.46-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:69fac0a7054d86b997af12dc23f581cf0b25fb1c7d1fed43257dee3af32d3d6d"},
|
||||
{file = "SQLAlchemy-1.4.46-cp311-cp311-win32.whl", hash = "sha256:887865924c3d6e9a473dc82b70977395301533b3030d0f020c38fd9eba5419f2"},
|
||||
{file = "SQLAlchemy-1.4.46-cp311-cp311-win_amd64.whl", hash = "sha256:984ee13543a346324319a1fb72b698e521506f6f22dc37d7752a329e9cd00a32"},
|
||||
{file = "SQLAlchemy-1.4.46-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:9167d4227b56591a4cc5524f1b79ccd7ea994f36e4c648ab42ca995d28ebbb96"},
|
||||
{file = "SQLAlchemy-1.4.46-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d61e9ecc849d8d44d7f80894ecff4abe347136e9d926560b818f6243409f3c86"},
|
||||
{file = "SQLAlchemy-1.4.46-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3ec187acf85984263299a3f15c34a6c0671f83565d86d10f43ace49881a82718"},
|
||||
{file = "SQLAlchemy-1.4.46-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9883f5fae4fd8e3f875adc2add69f8b945625811689a6c65866a35ee9c0aea23"},
|
||||
{file = "SQLAlchemy-1.4.46-cp36-cp36m-win32.whl", hash = "sha256:535377e9b10aff5a045e3d9ada8a62d02058b422c0504ebdcf07930599890eb0"},
|
||||
{file = "SQLAlchemy-1.4.46-cp36-cp36m-win_amd64.whl", hash = "sha256:18cafdb27834fa03569d29f571df7115812a0e59fd6a3a03ccb0d33678ec8420"},
|
||||
{file = "SQLAlchemy-1.4.46-cp37-cp37m-macosx_10_15_x86_64.whl", hash = "sha256:a1ad90c97029cc3ab4ffd57443a20fac21d2ec3c89532b084b073b3feb5abff3"},
|
||||
{file = "SQLAlchemy-1.4.46-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4847f4b1d822754e35707db913396a29d874ee77b9c3c3ef3f04d5a9a6209618"},
|
||||
{file = "SQLAlchemy-1.4.46-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c5a99282848b6cae0056b85da17392a26b2d39178394fc25700bcf967e06e97a"},
|
||||
{file = "SQLAlchemy-1.4.46-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d4b1cc7835b39835c75cf7c20c926b42e97d074147c902a9ebb7cf2c840dc4e2"},
|
||||
{file = "SQLAlchemy-1.4.46-cp37-cp37m-win32.whl", hash = "sha256:c522e496f9b9b70296a7675272ec21937ccfc15da664b74b9f58d98a641ce1b6"},
|
||||
{file = "SQLAlchemy-1.4.46-cp37-cp37m-win_amd64.whl", hash = "sha256:ae067ab639fa499f67ded52f5bc8e084f045d10b5ac7bb928ae4ca2b6c0429a5"},
|
||||
{file = "SQLAlchemy-1.4.46-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:e3c1808008124850115a3f7e793a975cfa5c8a26ceeeb9ff9cbb4485cac556df"},
|
||||
{file = "SQLAlchemy-1.4.46-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4d164df3d83d204c69f840da30b292ac7dc54285096c6171245b8d7807185aa"},
|
||||
{file = "SQLAlchemy-1.4.46-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:b33ffbdbbf5446cf36cd4cc530c9d9905d3c2fe56ed09e25c22c850cdb9fac92"},
|
||||
{file = "SQLAlchemy-1.4.46-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d94682732d1a0def5672471ba42a29ff5e21bb0aae0afa00bb10796fc1e28dd"},
|
||||
{file = "SQLAlchemy-1.4.46-cp38-cp38-win32.whl", hash = "sha256:f8cb80fe8d14307e4124f6fad64dfd87ab749c9d275f82b8b4ec84c84ecebdbe"},
|
||||
{file = "SQLAlchemy-1.4.46-cp38-cp38-win_amd64.whl", hash = "sha256:07e48cbcdda6b8bc7a59d6728bd3f5f574ffe03f2c9fb384239f3789c2d95c2e"},
|
||||
{file = "SQLAlchemy-1.4.46-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:1b1e5e96e2789d89f023d080bee432e2fef64d95857969e70d3cadec80bd26f0"},
|
||||
{file = "SQLAlchemy-1.4.46-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3714e5b33226131ac0da60d18995a102a17dddd42368b7bdd206737297823ad"},
|
||||
{file = "SQLAlchemy-1.4.46-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:955162ad1a931fe416eded6bb144ba891ccbf9b2e49dc7ded39274dd9c5affc5"},
|
||||
{file = "SQLAlchemy-1.4.46-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b6e4cb5c63f705c9d546a054c60d326cbde7421421e2d2565ce3e2eee4e1a01f"},
|
||||
{file = "SQLAlchemy-1.4.46-cp39-cp39-win32.whl", hash = "sha256:51e1ba2884c6a2b8e19109dc08c71c49530006c1084156ecadfaadf5f9b8b053"},
|
||||
{file = "SQLAlchemy-1.4.46-cp39-cp39-win_amd64.whl", hash = "sha256:315676344e3558f1f80d02535f410e80ea4e8fddba31ec78fe390eff5fb8f466"},
|
||||
{file = "SQLAlchemy-1.4.46.tar.gz", hash = "sha256:6913b8247d8a292ef8315162a51931e2b40ce91681f1b6f18f697045200c4a30"},
|
||||
]
|
||||
sqlparse = [
|
||||
{file = "sqlparse-0.4.3-py3-none-any.whl", hash = "sha256:0323c0ec29cd52bceabc1b4d9d579e311f3e4961b98d174201d5622a23b85e34"},
|
||||
{file = "sqlparse-0.4.3.tar.gz", hash = "sha256:69ca804846bb114d2ec380e4360a8a340db83f0ccf3afceeb1404df028f57268"},
|
||||
@ -2057,6 +2457,61 @@ tenacity = [
|
||||
{file = "tenacity-6.3.1-py2.py3-none-any.whl", hash = "sha256:baed357d9f35ec64264d8a4bbf004c35058fad8795c5b0d8a7dc77ecdcbb8f39"},
|
||||
{file = "tenacity-6.3.1.tar.gz", hash = "sha256:e14d191fb0a309b563904bbc336582efe2037de437e543b38da749769b544d7f"},
|
||||
]
|
||||
time-machine = [
|
||||
{file = "time-machine-2.9.0.tar.gz", hash = "sha256:60222d43f6e93a926adc36ed37a54bc8e4d0d8d1c4d449096afcfe85086129c2"},
|
||||
{file = "time_machine-2.9.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:fd72c0b2e7443fff6e4481991742b72c17f73735e5fdd176406ca48df187a5c9"},
|
||||
{file = "time_machine-2.9.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5657e0e6077cf15b37f0d8cf78e868113bbb3ecccc60064c40fe52d8166ca8b1"},
|
||||
{file = "time_machine-2.9.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bfa82614a98ecee70272bb6038d210b2ad7b2a6b8a678b400c34bdaf776802a7"},
|
||||
{file = "time_machine-2.9.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d4380bd6697cc7db3c9e6843f24779ac0550affa9d9a8e5f9e5d5cc139cb6583"},
|
||||
{file = "time_machine-2.9.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6211beee9f5dace08b1bbbb1fb09e34a69c52d87eea676729f14c8660481dff6"},
|
||||
{file = "time_machine-2.9.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:68ec8b83197db32c7a12da5f6b83c91271af3ed7f5dc122d2900a8de01dff9f0"},
|
||||
{file = "time_machine-2.9.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:c5dbc8b87cdc7be070a499f2bd1cd405c7f647abeb3447dfd397639df040bc64"},
|
||||
{file = "time_machine-2.9.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:948ca690f9770ad4a93fa183061c11346505598f5f0b721965bc85ec83bb103d"},
|
||||
{file = "time_machine-2.9.0-cp310-cp310-win32.whl", hash = "sha256:f92d5d2eb119a6518755c4c9170112094c706d1c604460f50afc1308eeb97f0e"},
|
||||
{file = "time_machine-2.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:cb51432652ad663b4cbd631c73c90f9e94f463382b86c0b6b854173700512a70"},
|
||||
{file = "time_machine-2.9.0-cp310-cp310-win_arm64.whl", hash = "sha256:8976b7b1f7de13598b655d459f5640f90f3cd587283e1b914a22e45946c5485b"},
|
||||
{file = "time_machine-2.9.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6463e302c96eb8c691c4340e281bd54327a213b924fa189aea81accf7e7f78df"},
|
||||
{file = "time_machine-2.9.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6b632d60aa0883dc7292ac3d32050604d26ec2bbd5c4d42fb0de3b4ef17343e2"},
|
||||
{file = "time_machine-2.9.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d329578abe47ce95baa015ef3825acebb1b73b5fa6f818fdf2d4685a00ca457f"},
|
||||
{file = "time_machine-2.9.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9ba5fc2655749066d68986de8368984dad4082db2fbeade78f40506dc5b65672"},
|
||||
{file = "time_machine-2.9.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49df5eea2160068e5b2bf28c22fc4c5aea00862ad88ddc3b62fc0f0683e97538"},
|
||||
{file = "time_machine-2.9.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:8830510adbf0a231184da277db9de1d55ef93ed228a575d217aaee295505abf1"},
|
||||
{file = "time_machine-2.9.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:b16a2129f9146faa080bfd1b53447761f7386ec5c72890c827a65f33ab200336"},
|
||||
{file = "time_machine-2.9.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a2cf80e5deaaa68c6cefb25303a4c870490b4e7591ed8e2435a65728920bc097"},
|
||||
{file = "time_machine-2.9.0-cp311-cp311-win32.whl", hash = "sha256:fe013942ab7f3241fcbe66ee43222d47f499d1e0cb69e913791c52e638ddd7f0"},
|
||||
{file = "time_machine-2.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:1d0ab46ce8a60baf9d86525694bf698fed9efefd22b8cbe1ca3e74abbb3239e1"},
|
||||
{file = "time_machine-2.9.0-cp311-cp311-win_arm64.whl", hash = "sha256:4f3755d9342ca1f1019418db52072272dfd75eb818fa4726fa8aabe208b38c26"},
|
||||
{file = "time_machine-2.9.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:9ee553f7732fa51e019e3329a6984593184c4e0410af1e73d91ce38a5d4b34ab"},
|
||||
{file = "time_machine-2.9.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:359c806e5b9a7a3c73dbb808d19dca297f5504a5eefdc5d031db8d918f43e364"},
|
||||
{file = "time_machine-2.9.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8e2a90b8300812d8d774f2d2fc216fec3c7d94132ac589e062489c395061f16c"},
|
||||
{file = "time_machine-2.9.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:36dde844d28549929fab171d683c28a8db1c206547bcf6b7aca77319847d2046"},
|
||||
{file = "time_machine-2.9.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:728263611d7940fda34d21573bd2b3f1491bdb52dbf75c5fe6c226dfe4655201"},
|
||||
{file = "time_machine-2.9.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:8bcc86b5a07ea9745f26dfad958dde0a4f56748c2ae0c9a96200a334d1b55055"},
|
||||
{file = "time_machine-2.9.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:0b9c36240876622b7f2f9e11bf72f100857c0a1e1a59af2da3d5067efea62c37"},
|
||||
{file = "time_machine-2.9.0-cp37-cp37m-win32.whl", hash = "sha256:eaf334477bc0a9283d5150a56be8670a07295ef676e5b5a7f086952929d1a56b"},
|
||||
{file = "time_machine-2.9.0-cp37-cp37m-win_amd64.whl", hash = "sha256:8e797e5a2a99d1b237183e52251abfc1ad85c376278b39d1aca76a451a97861a"},
|
||||
{file = "time_machine-2.9.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:69898aed9b2315a90f5855343d9aa34d05fa06032e2e3bb14f2528941ec89dc1"},
|
||||
{file = "time_machine-2.9.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c01dbc3671d0649023daf623e952f9f0b4d904d57ab546d6d35a4aeb14915e8d"},
|
||||
{file = "time_machine-2.9.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f080f6f7ca8cfca43bc5639288aebd0a273b4b5bd0acff609c2318728b13a18"},
|
||||
{file = "time_machine-2.9.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8670cb5cfda99f483d60de6ce56ceb0ec5d359193e79e4688e1c3c9db3937383"},
|
||||
{file = "time_machine-2.9.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f97ed8bc5b517844a71030f74e9561de92f4902c306e6ccc8331a5b0c8dd0e00"},
|
||||
{file = "time_machine-2.9.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:bdbe785e046d124f73cca603ee37d5fae0b15dc4c13702488ad19de56aae08ba"},
|
||||
{file = "time_machine-2.9.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:fcdef7687aed5c4331c9808f4a414a41987441c3e7a2ba554e4dccfa4218e788"},
|
||||
{file = "time_machine-2.9.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:f6e79643368828d4651146a486be5a662846ac223ab5e2c73ddd519acfcc243c"},
|
||||
{file = "time_machine-2.9.0-cp38-cp38-win32.whl", hash = "sha256:bb15b2b79b00d3f6cf7d62096f5e782fa740ecedfe0540c09f1d1e4d3d7b81ba"},
|
||||
{file = "time_machine-2.9.0-cp38-cp38-win_amd64.whl", hash = "sha256:3ff5148e2e73392db8418a1fe2f0b06f4a0e76772933502fb61e4c3000b5324e"},
|
||||
{file = "time_machine-2.9.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8367fd03f2d7349c7fc20f14de186974eaca2502c64b948212de663742c8fd11"},
|
||||
{file = "time_machine-2.9.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4b55654aaeaba380fcd6c004b8ada2978fdd4ece1e61e6b9717c6d4cc7fbbcd9"},
|
||||
{file = "time_machine-2.9.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ae4e3f02ab5dabb35adca606237c7e1a515c86d69c0b7092bbe0e1cfe5cffc61"},
|
||||
{file = "time_machine-2.9.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:010a58a8de1120308befae19e6c9de2ef5ca5206635cea33cb264998725cc027"},
|
||||
{file = "time_machine-2.9.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b32addbf56639a9a8261fb62f8ea83473447671c83ca2c017ab1eabf4841157f"},
|
||||
{file = "time_machine-2.9.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:372a97da01db89533d2f4ce50bbd908e5c56df7b8cfd6a005b177d0b14dc2938"},
|
||||
{file = "time_machine-2.9.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:b8faff03231ee55d5a216ce3e9171c5205459f866f54d4b5ee8aa1d860e4ce11"},
|
||||
{file = "time_machine-2.9.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:748d701228e646c224f2adfa6a11b986cd4aa90f1b8c13ef4534a3919c796bc0"},
|
||||
{file = "time_machine-2.9.0-cp39-cp39-win32.whl", hash = "sha256:d79d374e32488c76cdb06fbdd4464083aeaa715ddca3e864bac7c7760eb03729"},
|
||||
{file = "time_machine-2.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:cc6bf01211b5ea40f633d5502c5aa495b415ebaff66e041820997dae70a508e1"},
|
||||
{file = "time_machine-2.9.0-cp39-cp39-win_arm64.whl", hash = "sha256:3ce445775fcf7cb4040cfdba4b7c4888e7fd98bbcccfe1dc3fa8a798ed1f1d24"},
|
||||
]
|
||||
tinycss2 = [
|
||||
{file = "tinycss2-1.1.1-py3-none-any.whl", hash = "sha256:fe794ceaadfe3cf3e686b22155d0da5780dd0e273471a51846d0a02bc204fec8"},
|
||||
{file = "tinycss2-1.1.1.tar.gz", hash = "sha256:b2e44dd8883c360c35dd0d1b5aad0b610e5156c2cb3b33434634e539ead9d8bf"},
|
||||
@ -2077,17 +2532,13 @@ trio-websocket = [
|
||||
{file = "trio-websocket-0.9.2.tar.gz", hash = "sha256:a3d34de8fac26023eee701ed1e7bf4da9a8326b61a62934ec9e53b64970fd8fe"},
|
||||
{file = "trio_websocket-0.9.2-py3-none-any.whl", hash = "sha256:5b558f6e83cc20a37c3b61202476c5295d1addf57bd65543364e0337e37ed2bc"},
|
||||
]
|
||||
types-freezegun = [
|
||||
{file = "types-freezegun-1.1.10.tar.gz", hash = "sha256:cb3a2d2eee950eacbaac0673ab50499823365ceb8c655babb1544a41446409ec"},
|
||||
{file = "types_freezegun-1.1.10-py3-none-any.whl", hash = "sha256:fadebe72213e0674036153366205038e1f95c8ca96deb4ef9b71ddc15413543e"},
|
||||
]
|
||||
types-pytz = [
|
||||
{file = "types-pytz-2022.7.0.0.tar.gz", hash = "sha256:4f20c2953b3a3a0587e94489ec4c9e02c3d3aedb9ba5cd7e796e12f4cfa7027e"},
|
||||
{file = "types_pytz-2022.7.0.0-py3-none-any.whl", hash = "sha256:1509f182f686ab76e9a8234f22b00b8f50d239974db0cf924b7ae8674bb31a6f"},
|
||||
{file = "types-pytz-2022.7.1.0.tar.gz", hash = "sha256:918f9c3e7a950ba7e7d6f84b18a7cacabc8886cb7125fb1927ff1c752b4b59de"},
|
||||
{file = "types_pytz-2022.7.1.0-py3-none-any.whl", hash = "sha256:10ec7d009a02340f1cecd654ac03f0c29b6088a03b63d164401fc52df45936b2"},
|
||||
]
|
||||
types-requests = [
|
||||
{file = "types-requests-2.28.11.7.tar.gz", hash = "sha256:0ae38633734990d019b80f5463dfa164ebd3581998ac8435f526da6fe4d598c3"},
|
||||
{file = "types_requests-2.28.11.7-py3-none-any.whl", hash = "sha256:b6a2fca8109f4fdba33052f11ed86102bddb2338519e1827387137fefc66a98b"},
|
||||
{file = "types-requests-2.28.11.8.tar.gz", hash = "sha256:e67424525f84adfbeab7268a159d3c633862dafae15c5b19547ce1b55954f0a3"},
|
||||
{file = "types_requests-2.28.11.8-py3-none-any.whl", hash = "sha256:61960554baca0008ae7e2db2bd3b322ca9a144d3e80ce270f5fb640817e40994"},
|
||||
]
|
||||
types-urllib3 = [
|
||||
{file = "types-urllib3-1.26.25.4.tar.gz", hash = "sha256:eec5556428eec862b1ac578fb69aab3877995a99ffec9e5a12cf7fbd0cc9daee"},
|
||||
@ -2102,16 +2553,16 @@ tzdata = [
|
||||
{file = "tzdata-2022.7.tar.gz", hash = "sha256:fe5f866eddd8b96e9fcba978f8e503c909b19ea7efda11e52e39494bad3a7bfa"},
|
||||
]
|
||||
urllib3 = [
|
||||
{file = "urllib3-1.26.13-py2.py3-none-any.whl", hash = "sha256:47cc05d99aaa09c9e72ed5809b60e7ba354e64b59c9c173ac3018642d8bb41fc"},
|
||||
{file = "urllib3-1.26.13.tar.gz", hash = "sha256:c083dd0dce68dbfbe1129d5271cb90f9447dea7d52097c6e0126120c521ddea8"},
|
||||
{file = "urllib3-1.26.14-py2.py3-none-any.whl", hash = "sha256:75edcdc2f7d85b137124a6c3c9fc3933cdeaa12ecb9a6a959f22797a0feca7e1"},
|
||||
{file = "urllib3-1.26.14.tar.gz", hash = "sha256:076907bf8fd355cde77728471316625a4d2f7e713c125f51953bb5b3eecf4f72"},
|
||||
]
|
||||
vine = [
|
||||
{file = "vine-5.0.0-py2.py3-none-any.whl", hash = "sha256:4c9dceab6f76ed92105027c49c823800dd33cacce13bdedc5b914e3514b7fb30"},
|
||||
{file = "vine-5.0.0.tar.gz", hash = "sha256:7d3b1624a953da82ef63462013bbd271d3eb75751489f9807598e8f340bd637e"},
|
||||
]
|
||||
wcwidth = [
|
||||
{file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"},
|
||||
{file = "wcwidth-0.2.5.tar.gz", hash = "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"},
|
||||
{file = "wcwidth-0.2.6-py2.py3-none-any.whl", hash = "sha256:795b138f6875577cd91bba52baf9e445cd5118fd32723b460e30a0af30ea230e"},
|
||||
{file = "wcwidth-0.2.6.tar.gz", hash = "sha256:a5220780a404dbe3353789870978e472cfe477761f06ee55077256e509b156d0"},
|
||||
]
|
||||
webencodings = [
|
||||
{file = "webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78"},
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "vrobbler"
|
||||
version = "0.4.2"
|
||||
version = "0.7.0"
|
||||
description = ""
|
||||
authors = ["Colin Powell <colin@unbl.ink>"]
|
||||
|
||||
@ -28,24 +28,35 @@ gunicorn = "^20.1.0"
|
||||
django-simple-history = "^3.1.1"
|
||||
whitenoise = "^6.3.0"
|
||||
musicbrainzngs = "^0.7.1"
|
||||
cinemagoer = "^2022.12.27"
|
||||
pysportsdb = "^0.1.0"
|
||||
django-cachalot = "^2.5.2"
|
||||
pytz = "^2022.7.1"
|
||||
|
||||
[tool.poetry.dev-dependencies]
|
||||
Werkzeug = "2.0.3"
|
||||
black = "^22.3"
|
||||
freezegun = "^1.2"
|
||||
coverage = "^7.0.5"
|
||||
mypy = "^0.961"
|
||||
pytest = "^7.1"
|
||||
pytest-black = "^0.3.12"
|
||||
pytest-cov = "^3.0"
|
||||
pytest-django = "^4.5.2"
|
||||
pytest-flake8 = "^1.1"
|
||||
pytest-isort = "^3.0"
|
||||
pytest-runner = "^6.0"
|
||||
pytest-selenium = "^2.0.1"
|
||||
time-machine = "^2.9.0"
|
||||
types-pytz = "^2022.1"
|
||||
types-requests = "^2.27"
|
||||
types-freezegun = "^1.1"
|
||||
bandit = "^1.7.4"
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
minversion = "6.0"
|
||||
addopts = "-ra -q"
|
||||
testpaths = ["tests"]
|
||||
DJANGO_SETTINGS_MODULE='vrobbler.settings'
|
||||
|
||||
[tool.black]
|
||||
line-length = 79
|
||||
skip-string-normalization = true
|
||||
|
||||
0
tests/scrobbles_tests/__init__.py
Normal file
0
tests/scrobbles_tests/__init__.py
Normal file
0
tests/scrobbles_tests/__init__py
Normal file
0
tests/scrobbles_tests/__init__py
Normal file
74
tests/scrobbles_tests/conftest.py
Normal file
74
tests/scrobbles_tests/conftest.py
Normal file
@ -0,0 +1,74 @@
|
||||
import json
|
||||
import pytest
|
||||
|
||||
from scrobbles.models import Scrobble
|
||||
|
||||
|
||||
class MopidyRequest:
|
||||
name = "Same in the End"
|
||||
artist = "Sublime"
|
||||
album = "Sublime"
|
||||
track_number = 4
|
||||
run_time_ticks = 156604
|
||||
run_time = "156"
|
||||
playback_time_ticks = 15045
|
||||
musicbrainz_track_id = "54214d63-5adf-4909-87cd-c65c37a6d558"
|
||||
musicbrainz_album_id = "03b864cd-7761-314c-a892-05a89ddff00d"
|
||||
musicbrainz_artist_id = "95f5b748-d370-47fe-85bd-0af2dc450bc0"
|
||||
mopidy_uri = "local:track:Sublime%20-%20Sublime/Disc%201%20-%2004%20-%20Same%20in%20the%20End.mp3"
|
||||
status = "resumed"
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self.request_data = {
|
||||
"name": kwargs.get('name', self.name),
|
||||
"artist": kwargs.get("artist", self.artist),
|
||||
"album": kwargs.get("album", self.album),
|
||||
"track_number": int(kwargs.get("track_number", self.track_number)),
|
||||
"run_time_ticks": int(
|
||||
kwargs.get("run_time_ticks", self.run_time_ticks)
|
||||
),
|
||||
"run_time": int(kwargs.get("run_time", self.run_time)),
|
||||
"playback_time_ticks": int(
|
||||
kwargs.get("playback_time_ticks", self.playback_time_ticks)
|
||||
),
|
||||
"musicbrainz_track_id": kwargs.get(
|
||||
"musicbrainz_track_id", self.musicbrainz_track_id
|
||||
),
|
||||
"musicbrainz_album_id": kwargs.get(
|
||||
"musicbrainz_album_id", self.musicbrainz_album_id
|
||||
),
|
||||
"musicbrainz_artist_id": kwargs.get(
|
||||
"musicbrainz_artist_id", self.musicbrainz_artist_id
|
||||
),
|
||||
"mopidy_uri": kwargs.get("mopidy_uri", self.mopidy_uri),
|
||||
"status": kwargs.get("status", self.status),
|
||||
}
|
||||
|
||||
def __eq__(self, other):
|
||||
for key in self.request_data.keys():
|
||||
if self.request_data[key] != getattr(self, key):
|
||||
return False
|
||||
return True
|
||||
|
||||
@property
|
||||
def request_json(self):
|
||||
return json.dumps(self.request_data)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mopidy_track_request_data():
|
||||
return MopidyRequest().request_json
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mopidy_track_diff_album_request_data(**kwargs):
|
||||
mb_album_id = "0c56c457-afe1-4679-baab-759ba8dd2a58"
|
||||
return MopidyRequest(
|
||||
album="Gold", musicbrainz_album_id=mb_album_id
|
||||
).request_json
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mopidy_podcast_request_data():
|
||||
mopidy_uri = "local:podcast:Up%20First/2022-01-01%20Up%20First.mp3"
|
||||
return MopidyRequest(mopidy_uri=mopidy_uri).request_json
|
||||
107
tests/scrobbles_tests/test_aggregators.py
Normal file
107
tests/scrobbles_tests/test_aggregators.py
Normal file
@ -0,0 +1,107 @@
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
import pytest
|
||||
import time_machine
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from music.aggregators import (
|
||||
scrobble_counts,
|
||||
top_artists,
|
||||
top_tracks,
|
||||
week_of_scrobbles,
|
||||
)
|
||||
from profiles.models import UserProfile
|
||||
from scrobbles.models import Scrobble
|
||||
|
||||
|
||||
def build_scrobbles(client, request_data, num=7, spacing=2):
|
||||
url = reverse('scrobbles:mopidy-websocket')
|
||||
user = get_user_model().objects.create(username='Test User')
|
||||
UserProfile.objects.create(user=user, timezone='US/Eastern')
|
||||
for i in range(num):
|
||||
client.post(url, request_data, content_type='application/json')
|
||||
s = Scrobble.objects.last()
|
||||
s.user = user
|
||||
s.timestamp = timezone.now() - timedelta(days=i * spacing)
|
||||
s.played_to_completion = True
|
||||
s.save()
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@time_machine.travel(datetime(2022, 3, 4, 1, 24))
|
||||
def test_scrobble_counts_data(client, mopidy_track_request_data):
|
||||
build_scrobbles(client, mopidy_track_request_data)
|
||||
user = get_user_model().objects.first()
|
||||
count_dict = scrobble_counts(user)
|
||||
assert count_dict == {
|
||||
'alltime': 7,
|
||||
'month': 2,
|
||||
'today': 1,
|
||||
'week': 3,
|
||||
'year': 7,
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_week_of_scrobbles_data(client, mopidy_track_request_data):
|
||||
build_scrobbles(client, mopidy_track_request_data, 7, 1)
|
||||
user = get_user_model().objects.first()
|
||||
week = week_of_scrobbles(user)
|
||||
assert list(week.values()) == [1, 1, 1, 1, 1, 1, 1]
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_top_tracks_by_day(client, mopidy_track_request_data):
|
||||
build_scrobbles(client, mopidy_track_request_data, 7, 1)
|
||||
user = get_user_model().objects.first()
|
||||
tops = top_tracks(user)
|
||||
assert tops[0].title == "Same in the End"
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_top_tracks_by_week(client, mopidy_track_request_data):
|
||||
build_scrobbles(client, mopidy_track_request_data, 7, 1)
|
||||
user = get_user_model().objects.first()
|
||||
tops = top_tracks(user, filter='week')
|
||||
assert tops[0].title == "Same in the End"
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_top_tracks_by_month(client, mopidy_track_request_data):
|
||||
build_scrobbles(client, mopidy_track_request_data, 7, 1)
|
||||
user = get_user_model().objects.first()
|
||||
tops = top_tracks(user, filter='month')
|
||||
assert tops[0].title == "Same in the End"
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_top_tracks_by_year(client, mopidy_track_request_data):
|
||||
build_scrobbles(client, mopidy_track_request_data, 7, 1)
|
||||
user = get_user_model().objects.first()
|
||||
tops = top_tracks(user, filter='year')
|
||||
assert tops[0].title == "Same in the End"
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_top__artists_by_week(client, mopidy_track_request_data):
|
||||
build_scrobbles(client, mopidy_track_request_data, 7, 1)
|
||||
user = get_user_model().objects.first()
|
||||
tops = top_artists(user, filter='week')
|
||||
assert tops[0].name == "Sublime"
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_top__artists_by_month(client, mopidy_track_request_data):
|
||||
build_scrobbles(client, mopidy_track_request_data, 7, 1)
|
||||
user = get_user_model().objects.first()
|
||||
tops = top_artists(user, filter='month')
|
||||
assert tops[0].name == "Sublime"
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_top__artists_by_year(client, mopidy_track_request_data):
|
||||
build_scrobbles(client, mopidy_track_request_data, 7, 1)
|
||||
user = get_user_model().objects.first()
|
||||
tops = top_artists(user, filter='year')
|
||||
assert tops[0].name == "Sublime"
|
||||
12
tests/scrobbles_tests/test_imdb.py
Normal file
12
tests/scrobbles_tests/test_imdb.py
Normal file
@ -0,0 +1,12 @@
|
||||
import pytest
|
||||
import imdb
|
||||
from mock import patch
|
||||
|
||||
from vrobbler.apps.scrobbles.imdb import lookup_video_from_imdb
|
||||
|
||||
|
||||
def test_lookup_imdb_bad_id(caplog):
|
||||
data = lookup_video_from_imdb('3409324')
|
||||
assert data is None
|
||||
assert caplog.records[0].levelname == "WARNING"
|
||||
assert caplog.records[0].msg == "IMDB ID should begin with 'tt' 3409324"
|
||||
77
tests/scrobbles_tests/test_views.py
Normal file
77
tests/scrobbles_tests/test_views.py
Normal file
@ -0,0 +1,77 @@
|
||||
import json
|
||||
import pytest
|
||||
|
||||
from django.urls import reverse
|
||||
|
||||
from scrobbles.models import Scrobble
|
||||
from music.models import Track
|
||||
from podcasts.models import Episode
|
||||
|
||||
|
||||
def test_get_not_allowed_from_mopidy(client):
|
||||
url = reverse('scrobbles:mopidy-websocket')
|
||||
response = client.get(url)
|
||||
assert response.status_code == 405
|
||||
|
||||
|
||||
def test_bad_mopidy_request_data(client):
|
||||
url = reverse('scrobbles:mopidy-websocket')
|
||||
response = client.post(url)
|
||||
assert response.status_code == 400
|
||||
assert (
|
||||
response.data['detail']
|
||||
== 'JSON parse error - Expecting value: line 1 column 1 (char 0)'
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_scrobble_mopidy_track(client, mopidy_track_request_data):
|
||||
url = reverse('scrobbles:mopidy-websocket')
|
||||
response = client.post(
|
||||
url, mopidy_track_request_data, content_type='application/json'
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.data == {'scrobble_id': 1}
|
||||
|
||||
scrobble = Scrobble.objects.get(id=1)
|
||||
assert scrobble.media_obj.__class__ == Track
|
||||
assert scrobble.media_obj.title == "Same in the End"
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_scrobble_mopidy_same_track_different_album(
|
||||
client, mopidy_track_request_data, mopidy_track_diff_album_request_data
|
||||
):
|
||||
url = reverse('scrobbles:mopidy-websocket')
|
||||
response = client.post(
|
||||
url, mopidy_track_request_data, content_type='application/json'
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.data == {'scrobble_id': 1}
|
||||
scrobble = Scrobble.objects.get(id=1)
|
||||
assert scrobble.media_obj.album.name == "Sublime"
|
||||
|
||||
response = client.post(
|
||||
url,
|
||||
mopidy_track_diff_album_request_data,
|
||||
content_type='application/json',
|
||||
)
|
||||
|
||||
scrobble = Scrobble.objects.get(id=2)
|
||||
assert scrobble.media_obj.__class__ == Track
|
||||
assert scrobble.media_obj.album.name == "Gold"
|
||||
assert scrobble.media_obj.title == "Same in the End"
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_scrobble_mopidy_podcast(client, mopidy_podcast_request_data):
|
||||
url = reverse('scrobbles:mopidy-websocket')
|
||||
response = client.post(
|
||||
url, mopidy_podcast_request_data, content_type='application/json'
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.data == {'scrobble_id': 1}
|
||||
|
||||
scrobble = Scrobble.objects.get(id=1)
|
||||
assert scrobble.media_obj.__class__ == Episode
|
||||
assert scrobble.media_obj.title == "Up First"
|
||||
8
vrobbler.conf.test
Normal file
8
vrobbler.conf.test
Normal file
@ -0,0 +1,8 @@
|
||||
# Local configuration for Emus
|
||||
|
||||
VROBBLER_DUMP_REQUEST_DATA=True
|
||||
VROBBLER_LOG_TO_CONSOLE=True
|
||||
VROBBLER_DEBUG=True
|
||||
VROBBLER_LOG_LEVEL="DEBUG"
|
||||
VROBBLER_MEDIA_ROOT = "/tmp/media/"
|
||||
VROBBLER_KEEP_DETAILED_SCROBBLE_LOGS=True
|
||||
@ -2,6 +2,8 @@ from django.contrib import admin
|
||||
|
||||
from music.models import Artist, Album, Track
|
||||
|
||||
from scrobbles.admin import ScrobbleInline
|
||||
|
||||
|
||||
@admin.register(Album)
|
||||
class AlbumAdmin(admin.ModelAdmin):
|
||||
@ -33,3 +35,6 @@ class TrackAdmin(admin.ModelAdmin):
|
||||
)
|
||||
list_filter = ("album", "artist")
|
||||
ordering = ("-created",)
|
||||
inlines = [
|
||||
ScrobbleInline,
|
||||
]
|
||||
|
||||
@ -1,12 +1,13 @@
|
||||
from django.db.models import Q, Count, Sum
|
||||
from typing import List, Optional
|
||||
from scrobbles.models import Scrobble
|
||||
from music.models import Track, Artist
|
||||
from videos.models import Video
|
||||
|
||||
from django.utils import timezone
|
||||
from datetime import datetime, timedelta
|
||||
from typing import List, Optional
|
||||
|
||||
import pytz
|
||||
from django.db.models import Count, Q, Sum
|
||||
from django.utils import timezone
|
||||
from music.models import Artist, Track
|
||||
from scrobbles.models import Scrobble
|
||||
from videos.models import Video
|
||||
from vrobbler.apps.profiles.utils import now_user_timezone
|
||||
|
||||
NOW = timezone.now()
|
||||
START_OF_TODAY = datetime.combine(NOW.date(), datetime.min.time(), NOW.tzinfo)
|
||||
@ -17,67 +18,114 @@ STARTING_DAY_OF_CURRENT_MONTH = NOW.date().replace(day=1)
|
||||
STARTING_DAY_OF_CURRENT_YEAR = NOW.date().replace(month=1, day=1)
|
||||
|
||||
|
||||
def scrobble_counts():
|
||||
finished_scrobbles_qs = Scrobble.objects.filter(played_to_completion=True)
|
||||
def scrobble_counts(user=None):
|
||||
|
||||
now = timezone.now()
|
||||
user_filter = Q()
|
||||
if user and user.is_authenticated:
|
||||
now = now_user_timezone(user.profile)
|
||||
user_filter = Q(user=user)
|
||||
|
||||
start_of_today = datetime.combine(
|
||||
now.date(), datetime.min.time(), now.tzinfo
|
||||
)
|
||||
starting_day_of_current_week = now.date() - timedelta(
|
||||
days=now.today().isoweekday() % 7
|
||||
)
|
||||
starting_day_of_current_month = now.date().replace(day=1)
|
||||
starting_day_of_current_year = now.date().replace(month=1, day=1)
|
||||
|
||||
finished_scrobbles_qs = Scrobble.objects.filter(
|
||||
user_filter, played_to_completion=True
|
||||
)
|
||||
data = {}
|
||||
data['today'] = finished_scrobbles_qs.filter(
|
||||
timestamp__gte=START_OF_TODAY
|
||||
timestamp__gte=start_of_today
|
||||
).count()
|
||||
data['week'] = finished_scrobbles_qs.filter(
|
||||
timestamp__gte=STARTING_DAY_OF_CURRENT_WEEK
|
||||
timestamp__gte=starting_day_of_current_week
|
||||
).count()
|
||||
data['month'] = finished_scrobbles_qs.filter(
|
||||
timestamp__gte=STARTING_DAY_OF_CURRENT_MONTH
|
||||
timestamp__gte=starting_day_of_current_month
|
||||
).count()
|
||||
data['year'] = finished_scrobbles_qs.filter(
|
||||
timestamp__gte=STARTING_DAY_OF_CURRENT_YEAR
|
||||
timestamp__gte=starting_day_of_current_year
|
||||
).count()
|
||||
data['alltime'] = finished_scrobbles_qs.count()
|
||||
return data
|
||||
|
||||
|
||||
def week_of_scrobbles(media: str = 'tracks') -> dict[str, int]:
|
||||
def week_of_scrobbles(user=None, media: str = 'tracks') -> dict[str, int]:
|
||||
|
||||
now = timezone.now()
|
||||
user_filter = Q()
|
||||
if user and user.is_authenticated:
|
||||
now = now_user_timezone(user.profile)
|
||||
user_filter = Q(user=user)
|
||||
|
||||
start_of_today = datetime.combine(
|
||||
now.date(), datetime.min.time(), now.tzinfo
|
||||
)
|
||||
|
||||
scrobble_day_dict = {}
|
||||
base_qs = Scrobble.objects.filter(user_filter, played_to_completion=True)
|
||||
|
||||
media_filter = Q(track__isnull=False)
|
||||
if media == 'movies':
|
||||
media_filter = Q(video__video_type=Video.VideoType.MOVIE)
|
||||
if media == 'series':
|
||||
media_filter = Q(video__video_type=Video.VideoType.TV_EPISODE)
|
||||
|
||||
for day in range(6, -1, -1):
|
||||
start = START_OF_TODAY - timedelta(days=day)
|
||||
end = datetime.combine(start, datetime.max.time(), NOW.tzinfo)
|
||||
start = start_of_today - timedelta(days=day)
|
||||
end = datetime.combine(start, datetime.max.time(), now.tzinfo)
|
||||
day_of_week = start.strftime('%A')
|
||||
if media == 'movies':
|
||||
media_filter = Q(video__videotype=Video.VideoType.MOVIE)
|
||||
if media == 'series':
|
||||
media_filter = Q(video__videotype=Video.VideoType.TV_EPISODE)
|
||||
scrobble_day_dict[day_of_week] = (
|
||||
Scrobble.objects.filter(media_filter)
|
||||
.filter(
|
||||
timestamp__gte=start,
|
||||
timestamp__lte=end,
|
||||
played_to_completion=True,
|
||||
)
|
||||
.count()
|
||||
)
|
||||
|
||||
scrobble_day_dict[day_of_week] = base_qs.filter(
|
||||
media_filter,
|
||||
timestamp__gte=start,
|
||||
timestamp__lte=end,
|
||||
played_to_completion=True,
|
||||
).count()
|
||||
|
||||
return scrobble_day_dict
|
||||
|
||||
|
||||
def top_tracks(filter: str = "today", limit: int = 15) -> List["Track"]:
|
||||
time_filter = Q(scrobble__timestamp__gte=START_OF_TODAY)
|
||||
def top_tracks(
|
||||
user: "User", filter: str = "today", limit: int = 15
|
||||
) -> List["Track"]:
|
||||
|
||||
now = timezone.now()
|
||||
if user.is_authenticated:
|
||||
now = now_user_timezone(user.profile)
|
||||
|
||||
start_of_today = datetime.combine(
|
||||
now.date(), datetime.min.time(), now.tzinfo
|
||||
)
|
||||
starting_day_of_current_week = now.date() - timedelta(
|
||||
days=now.today().isoweekday() % 7
|
||||
)
|
||||
starting_day_of_current_month = now.date().replace(day=1)
|
||||
starting_day_of_current_year = now.date().replace(month=1, day=1)
|
||||
|
||||
time_filter = Q(scrobble__timestamp__gte=start_of_today)
|
||||
if filter == "week":
|
||||
time_filter = Q(scrobble__timestamp__gte=STARTING_DAY_OF_CURRENT_WEEK)
|
||||
time_filter = Q(scrobble__timestamp__gte=starting_day_of_current_week)
|
||||
if filter == "month":
|
||||
time_filter = Q(scrobble__timestamp__gte=STARTING_DAY_OF_CURRENT_MONTH)
|
||||
time_filter = Q(scrobble__timestamp__gte=starting_day_of_current_month)
|
||||
if filter == "year":
|
||||
time_filter = Q(scrobble__timestamp__gte=STARTING_DAY_OF_CURRENT_YEAR)
|
||||
time_filter = Q(scrobble__timestamp__gte=starting_day_of_current_year)
|
||||
|
||||
return (
|
||||
Track.objects.annotate(num_scrobbles=Count("scrobble", distinct=True))
|
||||
.filter(time_filter)
|
||||
Track.objects.filter(time_filter)
|
||||
.annotate(num_scrobbles=Count("scrobble", distinct=True))
|
||||
.order_by("-num_scrobbles")[:limit]
|
||||
)
|
||||
|
||||
|
||||
def top_artists(filter: str = "today", limit: int = 15) -> List["Artist"]:
|
||||
def top_artists(
|
||||
user: "User", filter: str = "today", limit: int = 15
|
||||
) -> List["Artist"]:
|
||||
time_filter = Q(track__scrobble__timestamp__gte=START_OF_TODAY)
|
||||
if filter == "week":
|
||||
time_filter = Q(
|
||||
@ -93,10 +141,8 @@ def top_artists(filter: str = "today", limit: int = 15) -> List["Artist"]:
|
||||
)
|
||||
|
||||
return (
|
||||
Artist.objects.annotate(
|
||||
num_scrobbles=Count("track__scrobble", distinct=True)
|
||||
)
|
||||
.filter(time_filter)
|
||||
Artist.objects.filter(time_filter)
|
||||
.annotate(num_scrobbles=Count("track__scrobble", distinct=True))
|
||||
.order_by("-num_scrobbles")[:limit]
|
||||
)
|
||||
|
||||
|
||||
@ -0,0 +1,22 @@
|
||||
# Generated by Django 4.1.5 on 2023-01-19 20:03
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('music', '0008_alter_track_options'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='track',
|
||||
name='musicbrainz_id',
|
||||
field=models.CharField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='track',
|
||||
unique_together={('album', 'musicbrainz_id')},
|
||||
),
|
||||
]
|
||||
@ -4,6 +4,7 @@ from uuid import uuid4
|
||||
|
||||
import musicbrainzngs
|
||||
from django.apps.config import cached_property
|
||||
from django.conf import settings
|
||||
from django.core.files.base import ContentFile
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
@ -58,7 +59,13 @@ class Album(TimeStampedModel):
|
||||
'artist-credit'
|
||||
][0]['artist']['id']
|
||||
if not self.year:
|
||||
self.year = mb_data['release']['date'][0:4]
|
||||
try:
|
||||
self.year = mb_data['release']['date'][0:4]
|
||||
except KeyError:
|
||||
pass
|
||||
except IndexError:
|
||||
pass
|
||||
|
||||
self.save(update_fields=['musicbrainz_albumartist_id', 'year'])
|
||||
|
||||
new_artist = Artist.objects.filter(
|
||||
@ -87,6 +94,8 @@ class Album(TimeStampedModel):
|
||||
|
||||
|
||||
class Track(ScrobblableMixin):
|
||||
COMPLETION_PERCENT = getattr(settings, 'MUSIC_COMPLETION_PERCENT', 90)
|
||||
|
||||
class Opinion(models.IntegerChoices):
|
||||
DOWN = -1, 'Thumbs down'
|
||||
NEUTRAL = 0, 'No opinion'
|
||||
@ -94,7 +103,10 @@ class Track(ScrobblableMixin):
|
||||
|
||||
artist = models.ForeignKey(Artist, on_delete=models.DO_NOTHING)
|
||||
album = models.ForeignKey(Album, on_delete=models.DO_NOTHING, **BNULL)
|
||||
musicbrainz_id = models.CharField(max_length=255, unique=True, **BNULL)
|
||||
musicbrainz_id = models.CharField(max_length=255, **BNULL)
|
||||
|
||||
class Meta:
|
||||
unique_together = [['album', 'musicbrainz_id']]
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.title} by {self.artist}"
|
||||
@ -126,7 +138,7 @@ class Track(ScrobblableMixin):
|
||||
|
||||
artist, artist_created = Artist.objects.get_or_create(**artist_dict)
|
||||
if artist_created:
|
||||
logger.debug(f"Created new album {artist}")
|
||||
logger.debug(f"Created new artist {artist}")
|
||||
else:
|
||||
logger.debug(f"Found album {artist}")
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
from django.contrib import admin
|
||||
from podcasts.models import Episode, Podcast, Producer
|
||||
from scrobbles.admin import ScrobbleInline
|
||||
|
||||
|
||||
@admin.register(Producer)
|
||||
@ -31,3 +31,6 @@ class EpisodeAdmin(admin.ModelAdmin):
|
||||
)
|
||||
list_filter = ("podcast",)
|
||||
ordering = ("-created",)
|
||||
inlines = [
|
||||
ScrobbleInline,
|
||||
]
|
||||
|
||||
@ -2,6 +2,7 @@ import logging
|
||||
from typing import Dict, Optional
|
||||
from uuid import uuid4
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django_extensions.db.models import TimeStampedModel
|
||||
@ -34,6 +35,8 @@ class Podcast(TimeStampedModel):
|
||||
|
||||
|
||||
class Episode(ScrobblableMixin):
|
||||
COMPLETION_PERCENT = getattr(settings, 'PODCAST_COMPLETION_PERCENT', 90)
|
||||
|
||||
podcast = models.ForeignKey(Podcast, on_delete=models.DO_NOTHING)
|
||||
number = models.IntegerField(**BNULL)
|
||||
pub_date = models.DateField(**BNULL)
|
||||
|
||||
0
vrobbler/apps/profiles/__init__.py
Normal file
0
vrobbler/apps/profiles/__init__.py
Normal file
9
vrobbler/apps/profiles/admin.py
Normal file
9
vrobbler/apps/profiles/admin.py
Normal file
@ -0,0 +1,9 @@
|
||||
from django.contrib import admin
|
||||
|
||||
from profiles.models import UserProfile
|
||||
|
||||
|
||||
@admin.register(UserProfile)
|
||||
class UserProfileAdmin(admin.ModelAdmin):
|
||||
date_hierarchy = "created"
|
||||
ordering = ("-created",)
|
||||
17
vrobbler/apps/profiles/constants.py
Normal file
17
vrobbler/apps/profiles/constants.py
Normal file
@ -0,0 +1,17 @@
|
||||
from datetime import datetime
|
||||
|
||||
import pytz
|
||||
|
||||
ALL_TIMEZONE_CHOICES = tuple(zip(pytz.all_timezones, pytz.all_timezones))
|
||||
COMMON_TIMEZONE_CHOICES = tuple(
|
||||
zip(pytz.common_timezones, pytz.common_timezones)
|
||||
)
|
||||
PRETTY_TIMEZONE_CHOICES = []
|
||||
|
||||
for tz in pytz.common_timezones:
|
||||
now = datetime.now(pytz.timezone(tz))
|
||||
ofs = now.strftime("%z")
|
||||
PRETTY_TIMEZONE_CHOICES.append((int(ofs), tz, "(GMT%s) %s" % (ofs, tz)))
|
||||
PRETTY_TIMEZONE_CHOICES.sort()
|
||||
for i in range(len(PRETTY_TIMEZONE_CHOICES)):
|
||||
PRETTY_TIMEZONE_CHOICES[i] = PRETTY_TIMEZONE_CHOICES[i][1:]
|
||||
1035
vrobbler/apps/profiles/migrations/0001_initial.py
Normal file
1035
vrobbler/apps/profiles/migrations/0001_initial.py
Normal file
File diff suppressed because it is too large
Load Diff
0
vrobbler/apps/profiles/migrations/__init__.py
Normal file
0
vrobbler/apps/profiles/migrations/__init__.py
Normal file
18
vrobbler/apps/profiles/models.py
Normal file
18
vrobbler/apps/profiles/models.py
Normal file
@ -0,0 +1,18 @@
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.db import models
|
||||
from django_extensions.db.models import TimeStampedModel
|
||||
from profiles.constants import PRETTY_TIMEZONE_CHOICES
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
class UserProfile(TimeStampedModel):
|
||||
user = models.OneToOneField(
|
||||
User, on_delete=models.CASCADE, related_name="profile"
|
||||
)
|
||||
timezone = models.CharField(
|
||||
max_length=255, choices=PRETTY_TIMEZONE_CHOICES
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return f"User profile for {self.user}"
|
||||
13
vrobbler/apps/profiles/signals.py
Normal file
13
vrobbler/apps/profiles/signals.py
Normal file
@ -0,0 +1,13 @@
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.db.models.base import post_save
|
||||
from django.dispatch import receiver
|
||||
|
||||
from profiles.models import UserProfile
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
@receiver(post_save, sender=User)
|
||||
def create_profile(sender, instance, created, **kwargs):
|
||||
if created:
|
||||
UserProfile.objects.create(user=instance)
|
||||
33
vrobbler/apps/profiles/utils.py
Normal file
33
vrobbler/apps/profiles/utils.py
Normal file
@ -0,0 +1,33 @@
|
||||
import datetime
|
||||
|
||||
import pytz
|
||||
from django.conf import settings
|
||||
from django.utils import timezone
|
||||
|
||||
|
||||
# need to translate to a non-naive timezone, even if timezone == settings.TIME_ZONE, so we can compare two dates
|
||||
def to_user_timezone(date, profile):
|
||||
timezone = profile.timezone if profile.timezone else settings.TIME_ZONE
|
||||
return date.replace(tzinfo=pytz.timezone(settings.TIME_ZONE)).astimezone(
|
||||
pytz.timezone(timezone)
|
||||
)
|
||||
|
||||
|
||||
def to_system_timezone(date, profile):
|
||||
timezone = profile.timezone if profile.timezone else settings.TIME_ZONE
|
||||
return date.replace(tzinfo=pytz.timezone(timezone)).astimezone(
|
||||
pytz.timezone(settings.TIME_ZONE)
|
||||
)
|
||||
|
||||
|
||||
def now_user_timezone(profile):
|
||||
timezone.activate(pytz.timezone(profile.timezone))
|
||||
return timezone.localtime(timezone.now())
|
||||
|
||||
|
||||
def now_system_timezone():
|
||||
return (
|
||||
datetime.datetime.now()
|
||||
.replace(tzinfo=pytz.timezone(settings.TIME_ZONE))
|
||||
.astimezone(pytz.timezone(settings.TIME_ZONE))
|
||||
)
|
||||
@ -3,6 +3,13 @@ from django.contrib import admin
|
||||
from scrobbles.models import Scrobble
|
||||
|
||||
|
||||
class ScrobbleInline(admin.TabularInline):
|
||||
model = Scrobble
|
||||
extra = 0
|
||||
raw_id_fields = ('video', 'podcast_episode', 'track')
|
||||
exclude = ('source_id', 'scrobble_log')
|
||||
|
||||
|
||||
@admin.register(Scrobble)
|
||||
class ScrobbleAdmin(admin.ModelAdmin):
|
||||
date_hierarchy = "timestamp"
|
||||
@ -16,6 +23,7 @@ class ScrobbleAdmin(admin.ModelAdmin):
|
||||
"is_paused",
|
||||
"played_to_completion",
|
||||
)
|
||||
raw_id_fields = ('video', 'podcast_episode', 'track', 'sport_event')
|
||||
list_filter = ("is_paused", "in_progress", "source", "track__artist")
|
||||
ordering = ("-timestamp",)
|
||||
|
||||
@ -26,6 +34,8 @@ class ScrobbleAdmin(admin.ModelAdmin):
|
||||
return obj.track
|
||||
if obj.podcast_episode:
|
||||
return obj.podcast_episode
|
||||
if obj.sport_event:
|
||||
return obj.sport_event
|
||||
|
||||
def media_type(self, obj):
|
||||
if obj.video:
|
||||
@ -34,6 +44,8 @@ class ScrobbleAdmin(admin.ModelAdmin):
|
||||
return "Track"
|
||||
if obj.podcast_episode:
|
||||
return "Podcast"
|
||||
if obj.sport_event:
|
||||
return "Sport Event"
|
||||
|
||||
def playback_percent(self, obj):
|
||||
return obj.percent_played
|
||||
|
||||
@ -1,3 +1,2 @@
|
||||
#!/usr/bin/env python3
|
||||
JELLYFIN_VIDEO_ITEM_TYPES = ["Episode", "Movie"]
|
||||
JELLYFIN_AUDIO_ITEM_TYPES = ["Audio"]
|
||||
|
||||
14
vrobbler/apps/scrobbles/forms.py
Normal file
14
vrobbler/apps/scrobbles/forms.py
Normal file
@ -0,0 +1,14 @@
|
||||
from django import forms
|
||||
|
||||
|
||||
class ScrobbleForm(forms.Form):
|
||||
item_id = forms.CharField(
|
||||
label="",
|
||||
widget=forms.TextInput(
|
||||
attrs={
|
||||
'class': "form-control form-control-dark w-100",
|
||||
'placeholder': "Scrobble something (IMDB ID, String, TVDB ID ...)",
|
||||
'aria-label': "Scrobble something",
|
||||
}
|
||||
),
|
||||
)
|
||||
63
vrobbler/apps/scrobbles/imdb.py
Normal file
63
vrobbler/apps/scrobbles/imdb.py
Normal file
@ -0,0 +1,63 @@
|
||||
import logging
|
||||
from typing import Optional
|
||||
from django.utils import timezone
|
||||
|
||||
from imdb import Cinemagoer
|
||||
from videos.models import Video
|
||||
|
||||
imdb_client = Cinemagoer()
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def lookup_video_from_imdb(imdb_id: str) -> dict:
|
||||
|
||||
if 'tt' not in imdb_id:
|
||||
logger.warning(f"IMDB ID should begin with 'tt' {imdb_id}")
|
||||
return
|
||||
|
||||
lookup_id = imdb_id.strip('tt')
|
||||
media = imdb_client.get_movie(lookup_id)
|
||||
|
||||
run_time_seconds = 60 * 60
|
||||
runtimes = media.get("runtimes")
|
||||
if runtimes:
|
||||
run_time_seconds = int(runtimes[0]) * 60
|
||||
|
||||
# Ticks otherwise known as miliseconds
|
||||
run_time_ticks = run_time_seconds * 1000 * 1000
|
||||
|
||||
item_type = "Movie"
|
||||
if media.get('series title'):
|
||||
item_type = "Episode"
|
||||
|
||||
try:
|
||||
plot = media.get('plot')[0]
|
||||
except TypeError:
|
||||
plot = ""
|
||||
except IndexError:
|
||||
plot = ""
|
||||
|
||||
logger.debug(f"Received data from IMDB: {media.__dict__}")
|
||||
# Build a rough approximation of a Jellyfin data response
|
||||
data_dict = {
|
||||
"ItemType": item_type,
|
||||
"Name": media.get('title'),
|
||||
"Overview": plot,
|
||||
"Tagline": media.get('tagline'),
|
||||
"Year": media.get('year'),
|
||||
"Provider_imdb": imdb_id,
|
||||
"RunTime": run_time_seconds,
|
||||
"RunTimeTicks": run_time_ticks,
|
||||
"SeriesName": media.get('series title'),
|
||||
"EpisodeNumber": media.get('episode'),
|
||||
"SeasonNumber": media.get('season'),
|
||||
"PlaybackPositionTicks": 1,
|
||||
"PlaybackPosition": 1,
|
||||
"UtcTimestamp": timezone.now().strftime('%Y-%m-%d %H:%M:%S.%f%z'),
|
||||
"IsPaused": False,
|
||||
"PlayedToCompletion": False,
|
||||
}
|
||||
logger.debug(f"Parsed data from IMDB data: {data_dict}")
|
||||
|
||||
return data_dict
|
||||
@ -0,0 +1,25 @@
|
||||
# Generated by Django 4.1.5 on 2023-01-14 21:23
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('sports', '0002_rename_start_utc_sportevent_start'),
|
||||
('scrobbles', '0007_scrobble_podcast_episode'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='scrobble',
|
||||
name='sport_event',
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.DO_NOTHING,
|
||||
to='sports.sportevent',
|
||||
),
|
||||
),
|
||||
]
|
||||
35
vrobbler/apps/scrobbles/migrations/0009_scrobble_uuid.py
Normal file
35
vrobbler/apps/scrobbles/migrations/0009_scrobble_uuid.py
Normal file
@ -0,0 +1,35 @@
|
||||
# Generated by Django 4.1.5 on 2023-01-20 18:40
|
||||
|
||||
from uuid import uuid4
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
def generate_uuids(apps, schema_editor):
|
||||
"""Force uuid generation for old scrobbles"""
|
||||
Scrobble = apps.get_model('scrobbles', 'Scrobble')
|
||||
for scrobble in Scrobble.objects.all():
|
||||
if not scrobble.uuid:
|
||||
scrobble.uuid = uuid4()
|
||||
scrobble.save(update_fields=['uuid'])
|
||||
|
||||
|
||||
def reverse_generate_uuids(apps, schema_editor):
|
||||
pass
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('scrobbles', '0008_scrobble_sport_event'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='scrobble',
|
||||
name='uuid',
|
||||
field=models.UUIDField(blank=True, editable=False, null=True),
|
||||
),
|
||||
migrations.RunPython(
|
||||
code=generate_uuids, reverse_code=reverse_generate_uuids
|
||||
),
|
||||
]
|
||||
@ -1,8 +1,7 @@
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
from typing import Optional
|
||||
from uuid import uuid4
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.db import models
|
||||
from django.utils import timezone
|
||||
@ -10,23 +9,24 @@ from django_extensions.db.models import TimeStampedModel
|
||||
from music.models import Track
|
||||
from podcasts.models import Episode
|
||||
from scrobbles.utils import check_scrobble_for_finish
|
||||
from sports.models import SportEvent
|
||||
from videos.models import Video
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
User = get_user_model()
|
||||
BNULL = {"blank": True, "null": True}
|
||||
VIDEO_BACKOFF = getattr(settings, 'VIDEO_BACKOFF_MINUTES')
|
||||
TRACK_BACKOFF = getattr(settings, 'MUSIC_BACKOFF_SECONDS')
|
||||
VIDEO_WAIT_PERIOD = getattr(settings, 'VIDEO_WAIT_PERIOD_DAYS')
|
||||
TRACK_WAIT_PERIOD = getattr(settings, 'MUSIC_WAIT_PERIOD_MINUTES')
|
||||
|
||||
|
||||
class Scrobble(TimeStampedModel):
|
||||
uuid = models.UUIDField(editable=False, **BNULL)
|
||||
video = models.ForeignKey(Video, on_delete=models.DO_NOTHING, **BNULL)
|
||||
track = models.ForeignKey(Track, on_delete=models.DO_NOTHING, **BNULL)
|
||||
podcast_episode = models.ForeignKey(
|
||||
Episode, on_delete=models.DO_NOTHING, **BNULL
|
||||
)
|
||||
sport_event = models.ForeignKey(
|
||||
SportEvent, on_delete=models.DO_NOTHING, **BNULL
|
||||
)
|
||||
user = models.ForeignKey(
|
||||
User, blank=True, null=True, on_delete=models.DO_NOTHING
|
||||
)
|
||||
@ -40,26 +40,39 @@ class Scrobble(TimeStampedModel):
|
||||
in_progress = models.BooleanField(default=True)
|
||||
scrobble_log = models.TextField(**BNULL)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if not self.uuid:
|
||||
self.uuid = uuid4()
|
||||
|
||||
return super(Scrobble, self).save(*args, **kwargs)
|
||||
|
||||
@property
|
||||
def status(self) -> str:
|
||||
if self.is_paused:
|
||||
return 'paused'
|
||||
if self.played_to_completion:
|
||||
return 'finished'
|
||||
if self.in_progress:
|
||||
return 'in-progress'
|
||||
return 'zombie'
|
||||
|
||||
@property
|
||||
def percent_played(self) -> int:
|
||||
if (
|
||||
self.playback_position_ticks
|
||||
and self.media_obj.run_time_ticks
|
||||
and self.source != 'Mopidy'
|
||||
):
|
||||
return int(
|
||||
(self.playback_position_ticks / self.media_obj.run_time_ticks)
|
||||
* 100
|
||||
)
|
||||
# If we don't have media_obj.run_time_ticks, let's guess from created time
|
||||
now = timezone.now()
|
||||
playback_duration = (now - self.created).seconds
|
||||
if playback_duration and self.media_obj.run_time:
|
||||
return int(
|
||||
(playback_duration / int(self.media_obj.run_time)) * 100
|
||||
if not self.media_obj.run_time_ticks:
|
||||
logger.warning(
|
||||
f"{self} has no run_time_ticks value, cannot show percent played"
|
||||
)
|
||||
return 100
|
||||
|
||||
return 0
|
||||
playback_ticks = self.playback_position_ticks
|
||||
if not playback_ticks:
|
||||
playback_ticks = (timezone.now() - self.timestamp).seconds * 1000
|
||||
|
||||
if self.played_to_completion:
|
||||
playback_ticks = self.media_obj.run_time_ticks
|
||||
|
||||
percent = int((playback_ticks / self.media_obj.run_time_ticks) * 100)
|
||||
return percent
|
||||
|
||||
@property
|
||||
def media_obj(self):
|
||||
@ -70,162 +83,199 @@ class Scrobble(TimeStampedModel):
|
||||
media_obj = self.track
|
||||
if self.podcast_episode:
|
||||
media_obj = self.podcast_episode
|
||||
if self.sport_event:
|
||||
media_obj = self.sport_event
|
||||
return media_obj
|
||||
|
||||
def __str__(self):
|
||||
return f"Scrobble of {self.media_obj} {self.timestamp.year}-{self.timestamp.month}"
|
||||
timestamp = self.timestamp.strftime('%Y-%m-%d')
|
||||
return f"Scrobble of {self.media_obj} ({timestamp})"
|
||||
|
||||
@classmethod
|
||||
def create_or_update_for_video(
|
||||
cls, video: "Video", user_id: int, jellyfin_data: dict
|
||||
cls, video: "Video", user_id: int, scrobble_data: dict
|
||||
) -> "Scrobble":
|
||||
jellyfin_data['video_id'] = video.id
|
||||
logger.debug(
|
||||
f"Creating or updating scrobble for video {video} with data {jellyfin_data}"
|
||||
)
|
||||
scrobble_data['video_id'] = video.id
|
||||
|
||||
scrobble = (
|
||||
cls.objects.filter(video=video, user_id=user_id)
|
||||
cls.objects.filter(
|
||||
video=video,
|
||||
user_id=user_id,
|
||||
)
|
||||
.order_by('-modified')
|
||||
.first()
|
||||
)
|
||||
if scrobble and scrobble.playback_perecnt <= 100:
|
||||
logger.info(
|
||||
f"Found existing scrobble for video {video}, updating",
|
||||
{"scrobble_data": scrobble_data},
|
||||
)
|
||||
return cls.update(scrobble, scrobble_data)
|
||||
|
||||
# Backoff is how long until we consider this a new scrobble
|
||||
backoff = timezone.now() + timedelta(minutes=VIDEO_BACKOFF)
|
||||
wait_period = timezone.now() + timedelta(days=VIDEO_WAIT_PERIOD)
|
||||
|
||||
return cls.update_or_create(
|
||||
scrobble, backoff, wait_period, jellyfin_data
|
||||
logger.debug(
|
||||
f"No existing scrobble for video {video}, creating",
|
||||
{"scrobble_data": scrobble_data},
|
||||
)
|
||||
# If creating a new scrobble, we don't need status
|
||||
scrobble_data.pop('jellyfin_status')
|
||||
return cls.create(scrobble_data)
|
||||
|
||||
@classmethod
|
||||
def create_or_update_for_track(
|
||||
cls, track: "Track", user_id: int, scrobble_data: dict
|
||||
) -> "Scrobble":
|
||||
"""Look up any existing scrobbles for a track and compare
|
||||
the appropriate backoff time for music tracks to the setting
|
||||
so we can avoid duplicating scrobbles."""
|
||||
scrobble_data['track_id'] = track.id
|
||||
|
||||
scrobble = (
|
||||
cls.objects.filter(track=track, user_id=user_id)
|
||||
cls.objects.filter(
|
||||
track=track,
|
||||
user_id=user_id,
|
||||
played_to_completion=False,
|
||||
)
|
||||
.order_by('-modified')
|
||||
.first()
|
||||
)
|
||||
if scrobble:
|
||||
logger.debug(
|
||||
f"Found existing scrobble for track {track}, updating",
|
||||
{"scrobble_data": scrobble_data},
|
||||
)
|
||||
return cls.update(scrobble, scrobble_data)
|
||||
|
||||
logger.debug(
|
||||
f"Found existing scrobble for track {track}, updating",
|
||||
f"No existing scrobble for track {track}, creating",
|
||||
{"scrobble_data": scrobble_data},
|
||||
)
|
||||
|
||||
backoff = timezone.now() + timedelta(seconds=TRACK_BACKOFF)
|
||||
wait_period = timezone.now() + timedelta(minutes=TRACK_WAIT_PERIOD)
|
||||
|
||||
return cls.update_or_create(
|
||||
scrobble, backoff, wait_period, scrobble_data
|
||||
)
|
||||
# If creating a new scrobble, we don't need status
|
||||
scrobble_data.pop('mopidy_status', None)
|
||||
scrobble_data.pop('jellyfin_status', None)
|
||||
return cls.create(scrobble_data)
|
||||
|
||||
@classmethod
|
||||
def create_or_update_for_podcast_episode(
|
||||
cls, episode: "Episode", user_id: int, scrobble_data: dict
|
||||
) -> "Scrobble":
|
||||
scrobble_data['podcast_episode_id'] = episode.id
|
||||
|
||||
scrobble = (
|
||||
cls.objects.filter(podcast_episode=episode, user_id=user_id)
|
||||
cls.objects.filter(
|
||||
podcast_episode=episode,
|
||||
user_id=user_id,
|
||||
played_to_completion=False,
|
||||
)
|
||||
.order_by('-modified')
|
||||
.first()
|
||||
)
|
||||
if scrobble:
|
||||
logger.debug(
|
||||
f"Found existing scrobble for podcast {episode}, updating",
|
||||
{"scrobble_data": scrobble_data},
|
||||
)
|
||||
return cls.update(scrobble, scrobble_data)
|
||||
|
||||
logger.debug(
|
||||
f"Found existing scrobble for podcast {episode}, updating",
|
||||
f"No existing scrobble for podcast epsiode {episode}, creating",
|
||||
{"scrobble_data": scrobble_data},
|
||||
)
|
||||
|
||||
backoff = timezone.now() + timedelta(seconds=TRACK_BACKOFF)
|
||||
wait_period = timezone.now() + timedelta(minutes=TRACK_WAIT_PERIOD)
|
||||
|
||||
return cls.update_or_create(
|
||||
scrobble, backoff, wait_period, scrobble_data
|
||||
)
|
||||
# If creating a new scrobble, we don't need status
|
||||
scrobble_data.pop('mopidy_status')
|
||||
return cls.create(scrobble_data)
|
||||
|
||||
@classmethod
|
||||
def update_or_create(
|
||||
cls,
|
||||
scrobble: Optional["Scrobble"],
|
||||
backoff,
|
||||
wait_period,
|
||||
scrobble_data: dict,
|
||||
) -> Optional["Scrobble"]:
|
||||
def create_or_update_for_sport_event(
|
||||
cls, event: "SportEvent", user_id: int, scrobble_data: dict
|
||||
) -> "Scrobble":
|
||||
scrobble_data['sport_event_id'] = event.id
|
||||
scrobble = (
|
||||
cls.objects.filter(
|
||||
sport_event=event,
|
||||
user_id=user_id,
|
||||
played_to_completion=False,
|
||||
)
|
||||
.order_by('-modified')
|
||||
.first()
|
||||
)
|
||||
if scrobble:
|
||||
logger.debug(
|
||||
f"Found existing scrobble for sport event {event}, updating",
|
||||
{"scrobble_data": scrobble_data},
|
||||
)
|
||||
return cls.update(scrobble, scrobble_data)
|
||||
|
||||
logger.debug(
|
||||
f"No existing scrobble for sport event {event}, creating",
|
||||
{"scrobble_data": scrobble_data},
|
||||
)
|
||||
# If creating a new scrobble, we don't need status
|
||||
scrobble_data.pop('jellyfin_status')
|
||||
return cls.create(scrobble_data)
|
||||
|
||||
@classmethod
|
||||
def update(cls, scrobble: "Scrobble", scrobble_data: dict) -> "Scrobble":
|
||||
# Status is a field we get from Mopidy, which refuses to poll us
|
||||
scrobble_status = scrobble_data.pop('mopidy_status', None)
|
||||
if not scrobble_status:
|
||||
scrobble_status = scrobble_data.pop('jellyfin_status', None)
|
||||
if not scrobble_status:
|
||||
logger.warning(
|
||||
f"No status update found in message, not scrobbling"
|
||||
)
|
||||
return
|
||||
|
||||
logger.debug(f"Scrobbling to {scrobble} with status {scrobble_status}")
|
||||
if scrobble:
|
||||
scrobble.update_ticks(scrobble_data)
|
||||
scrobble.update_ticks(scrobble_data)
|
||||
|
||||
# On stop, stop progress and send it to the check for completion
|
||||
if scrobble_status == "stopped":
|
||||
return scrobble.stop()
|
||||
|
||||
# On pause, set is_paused and stop scrobbling
|
||||
if scrobble_status == "paused":
|
||||
return scrobble.pause()
|
||||
|
||||
if scrobble_status == "resumed":
|
||||
return scrobble.resume()
|
||||
|
||||
# We're not changing the scrobble, but we don't want to walk over an existing one
|
||||
scrobble_is_finished = (
|
||||
not scrobble.in_progress and scrobble.modified < backoff
|
||||
)
|
||||
if scrobble_is_finished:
|
||||
logger.info(
|
||||
'Found a very recent scrobble for this item, holding off scrobbling again'
|
||||
)
|
||||
return
|
||||
|
||||
if not scrobble:
|
||||
# If we default this to "" we can probably remove this
|
||||
scrobble_data['scrobble_log'] = ""
|
||||
scrobble = cls.objects.create(
|
||||
**scrobble_data,
|
||||
)
|
||||
else:
|
||||
for key, value in scrobble_data.items():
|
||||
setattr(scrobble, key, value)
|
||||
scrobble.save()
|
||||
|
||||
# If we hit our completion threshold, save it and get ready
|
||||
# to scrobble again if we re-watch this.
|
||||
scrobble = check_scrobble_for_finish(scrobble)
|
||||
# On stop, stop progress and send it to the check for completion
|
||||
if scrobble_status == "stopped":
|
||||
scrobble.stop()
|
||||
# On pause, set is_paused and stop scrobbling
|
||||
if scrobble_status == "paused":
|
||||
scrobble.pause()
|
||||
if scrobble_status == "resumed":
|
||||
scrobble.resume()
|
||||
|
||||
for key, value in scrobble_data.items():
|
||||
setattr(scrobble, key, value)
|
||||
scrobble.save()
|
||||
check_scrobble_for_finish(scrobble)
|
||||
return scrobble
|
||||
|
||||
def stop(self):
|
||||
@classmethod
|
||||
def create(
|
||||
cls,
|
||||
scrobble_data: dict,
|
||||
) -> "Scrobble":
|
||||
scrobble_data['scrobble_log'] = ""
|
||||
scrobble = cls.objects.create(
|
||||
**scrobble_data,
|
||||
)
|
||||
return scrobble
|
||||
|
||||
def stop(self, force_finish=False) -> None:
|
||||
if not self.in_progress:
|
||||
logger.warning("Scrobble already stopped")
|
||||
return
|
||||
self.in_progress = False
|
||||
self.save(update_fields=['in_progress'])
|
||||
return check_scrobble_for_finish(self)
|
||||
check_scrobble_for_finish(self, force_finish)
|
||||
|
||||
def pause(self):
|
||||
def pause(self) -> None:
|
||||
if self.is_paused:
|
||||
logger.warning("Scrobble already paused")
|
||||
return
|
||||
self.is_paused = True
|
||||
self.save(update_fields=["is_paused"])
|
||||
return check_scrobble_for_finish(self)
|
||||
check_scrobble_for_finish(self)
|
||||
|
||||
def resume(self):
|
||||
def resume(self) -> None:
|
||||
if self.is_paused or not self.in_progress:
|
||||
self.is_paused = False
|
||||
self.in_progress = True
|
||||
return self.save(update_fields=["is_paused", "in_progress"])
|
||||
return self
|
||||
|
||||
def update_ticks(self, data):
|
||||
def cancel(self) -> None:
|
||||
check_scrobble_for_finish(self, force_finish=True)
|
||||
self.delete()
|
||||
|
||||
def update_ticks(self, data) -> None:
|
||||
self.playback_position_ticks = data.get("playback_position_ticks")
|
||||
self.playback_position = data.get("playback_position")
|
||||
logger.debug(
|
||||
@ -234,4 +284,3 @@ class Scrobble(TimeStampedModel):
|
||||
self.save(
|
||||
update_fields=['playback_position_ticks', 'playback_position']
|
||||
)
|
||||
return self
|
||||
|
||||
@ -9,6 +9,7 @@ from podcasts.models import Episode
|
||||
from scrobbles.models import Scrobble
|
||||
from scrobbles.utils import convert_to_seconds, parse_mopidy_uri
|
||||
from videos.models import Video
|
||||
from sports.models import SportEvent
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -85,15 +86,12 @@ def mopidy_scrobble_track(
|
||||
"mopidy_status": data_dict.get("status"),
|
||||
}
|
||||
|
||||
scrobble = None
|
||||
# Jellyfin MB ids suck, so always overwrite with Mopidy if they're offering
|
||||
track.musicbrainz_id = data_dict.get("musicbrainz_track_id")
|
||||
track.save()
|
||||
|
||||
scrobble = Scrobble.create_or_update_for_track(track, user_id, mopidy_data)
|
||||
|
||||
if track:
|
||||
# Jellyfin MB ids suck, so always overwrite with Mopidy if they're offering
|
||||
track.musicbrainz_id = data_dict.get("musicbrainz_track_id")
|
||||
track.save()
|
||||
scrobble = Scrobble.create_or_update_for_track(
|
||||
track, user_id, mopidy_data
|
||||
)
|
||||
return scrobble
|
||||
|
||||
|
||||
@ -104,15 +102,24 @@ def create_jellyfin_scrobble_dict(data_dict: dict, user_id: int) -> dict:
|
||||
if data_dict.get("PlayedToCompletion"):
|
||||
jellyfin_status = "stopped"
|
||||
|
||||
playback_position_ticks = None
|
||||
if data_dict.get("PlaybackPositionTicks"):
|
||||
playback_position_ticks = (
|
||||
data_dict.get("PlaybackPositionTicks") // 10000
|
||||
)
|
||||
if playback_position_ticks <= 0:
|
||||
playback_position_ticks = None
|
||||
|
||||
playback_position = data_dict.get("PlaybackPosition")
|
||||
if playback_position:
|
||||
playback_position = convert_to_seconds(playback_position)
|
||||
|
||||
return {
|
||||
"user_id": user_id,
|
||||
"timestamp": parse(data_dict.get("UtcTimestamp")),
|
||||
"playback_position_ticks": data_dict.get("PlaybackPositionTicks")
|
||||
// 10000,
|
||||
"playback_position": convert_to_seconds(
|
||||
data_dict.get("PlaybackPosition")
|
||||
),
|
||||
"source": "Jellyfin",
|
||||
"playback_position_ticks": playback_position_ticks,
|
||||
"playback_position": playback_position,
|
||||
"source": data_dict.get("ClientName", "Vrobbler"),
|
||||
"source_id": data_dict.get('MediaSourceId'),
|
||||
"jellyfin_status": jellyfin_status,
|
||||
}
|
||||
@ -176,3 +183,31 @@ def jellyfin_scrobble_video(data_dict: dict, user_id: Optional[int]):
|
||||
scrobble_dict = create_jellyfin_scrobble_dict(data_dict, user_id)
|
||||
|
||||
return Scrobble.create_or_update_for_video(video, user_id, scrobble_dict)
|
||||
|
||||
|
||||
def manual_scrobble_video(data_dict: dict, user_id: Optional[int]):
|
||||
if not data_dict.get("Provider_imdb", None):
|
||||
logger.error(
|
||||
"No IMDB ID received. This is likely because all metadata is bad, not scrobbling"
|
||||
)
|
||||
return
|
||||
video = Video.find_or_create(data_dict)
|
||||
|
||||
scrobble_dict = create_jellyfin_scrobble_dict(data_dict, user_id)
|
||||
|
||||
return Scrobble.create_or_update_for_video(video, user_id, scrobble_dict)
|
||||
|
||||
|
||||
def manual_scrobble_event(data_dict: dict, user_id: Optional[int]):
|
||||
if not data_dict.get("Provider_thesportsdb", None):
|
||||
logger.error(
|
||||
"No TheSportsDB ID received. This is likely because all metadata is bad, not scrobbling"
|
||||
)
|
||||
return
|
||||
event = SportEvent.find_or_create(data_dict)
|
||||
|
||||
scrobble_dict = create_jellyfin_scrobble_dict(data_dict, user_id)
|
||||
|
||||
return Scrobble.create_or_update_for_sport_event(
|
||||
event, user_id, scrobble_dict
|
||||
)
|
||||
|
||||
50
vrobbler/apps/scrobbles/thesportsdb.py
Normal file
50
vrobbler/apps/scrobbles/thesportsdb.py
Normal file
@ -0,0 +1,50 @@
|
||||
import logging
|
||||
|
||||
from dateutil.parser import parse
|
||||
from django.conf import settings
|
||||
from django.utils import timezone
|
||||
from pysportsdb import TheSportsDbClient
|
||||
from sports.models import Sport
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
API_KEY = getattr(settings, "THESPORTSDB_API_KEY", "2")
|
||||
client = TheSportsDbClient(api_key=API_KEY)
|
||||
|
||||
|
||||
def lookup_event_from_thesportsdb(event_id: str) -> dict:
|
||||
|
||||
event = client.lookup_event(event_id)['events'][0]
|
||||
if not event or type(event) != dict:
|
||||
return {}
|
||||
league = {} # client.lookup_league(league_id=event.get('idLeague'))
|
||||
event_type = "Game"
|
||||
sport = Sport.objects.filter(thesportsdb_id=event.get('strSport')).first()
|
||||
|
||||
logger.debug(event)
|
||||
data_dict = {
|
||||
"ItemType": sport.default_event_type,
|
||||
"Name": event.get('strEvent'),
|
||||
"AltName": event.get('strEventAlternate'),
|
||||
"Start": parse(event.get('strTimestamp')),
|
||||
"Provider_thesportsdb": event.get('idEvent'),
|
||||
"RunTime": sport.default_event_run_time,
|
||||
"RunTimeTicks": sport.default_event_run_time_ticks,
|
||||
"Sport": event.get('strSport'),
|
||||
"Season": event.get('strSeason'),
|
||||
"LeagueId": event.get('idLeague'),
|
||||
"LeagueName": event.get('strLeague'),
|
||||
"HomeTeamId": event.get('idHomeTeam'),
|
||||
"HomeTeamName": event.get('strHomeTeam'),
|
||||
"AwayTeamId": event.get('idAwayTeam'),
|
||||
"AwayTeamName": event.get('strAwayTeam'),
|
||||
"RoundId": event.get('intRound'),
|
||||
"PlaybackPositionTicks": None,
|
||||
"PlaybackPosition": None,
|
||||
"UtcTimestamp": timezone.now().strftime('%Y-%m-%d %H:%M:%S.%f%z'),
|
||||
"IsPaused": False,
|
||||
"PlayedToCompletion": False,
|
||||
"Source": "Vrobbler",
|
||||
}
|
||||
|
||||
return data_dict
|
||||
@ -4,7 +4,9 @@ from scrobbles import views
|
||||
app_name = 'scrobbles'
|
||||
|
||||
urlpatterns = [
|
||||
path('', views.scrobble_endpoint, name='scrobble-list'),
|
||||
path('', views.scrobble_endpoint, name='api-list'),
|
||||
path('finish/<slug:uuid>', views.scrobble_finish, name='finish'),
|
||||
path('cancel/<slug:uuid>', views.scrobble_cancel, name='cancel'),
|
||||
path('jellyfin/', views.jellyfin_websocket, name='jellyfin-websocket'),
|
||||
path('mopidy/', views.mopidy_websocket, name='mopidy-websocket'),
|
||||
]
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import logging
|
||||
from typing import Any, Optional
|
||||
from urllib.parse import unquote
|
||||
|
||||
from dateutil.parser import ParserError, parse
|
||||
@ -10,10 +9,17 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
def convert_to_seconds(run_time: str) -> int:
|
||||
"""Jellyfin sends run time as 00:00:00 string. We want the run time to
|
||||
actually be in seconds so we'll convert it"""
|
||||
if ":" in run_time:
|
||||
actually be in seconds so we'll convert it"
|
||||
|
||||
This is actually deprecated, as we now convert to seconds before saving.
|
||||
But for older videos, we'll leave this here.
|
||||
"""
|
||||
if ":" in str(run_time):
|
||||
run_time_list = run_time.split(":")
|
||||
run_time = (int(run_time_list[1]) * 60) + int(run_time_list[2])
|
||||
hours = int(run_time_list[0])
|
||||
minutes = int(run_time_list[1])
|
||||
seconds = int(run_time_list[2])
|
||||
run_time = (((hours * 60) + minutes) * 60) + seconds
|
||||
return int(run_time)
|
||||
|
||||
|
||||
@ -60,15 +66,14 @@ def parse_mopidy_uri(uri: str) -> dict:
|
||||
}
|
||||
|
||||
|
||||
def check_scrobble_for_finish(scrobble: "Scrobble") -> None:
|
||||
completion_percent = getattr(settings, "MUSIC_COMPLETION_PERCENT", 95)
|
||||
if scrobble.video:
|
||||
completion_percent = getattr(settings, "VIDEO_COMPLETION_PERCENT", 90)
|
||||
if scrobble.podcast_episode:
|
||||
completion_percent = getattr(
|
||||
settings, "PODCAST_COMPLETION_PERCENT", 25
|
||||
)
|
||||
if scrobble.percent_played >= completion_percent:
|
||||
def check_scrobble_for_finish(
|
||||
scrobble: "Scrobble", force_finish=False
|
||||
) -> None:
|
||||
completion_percent = scrobble.media_obj.COMPLETION_PERCENT
|
||||
|
||||
if scrobble.percent_played >= completion_percent or force_finish:
|
||||
logger.debug(f"Completion percent {completion_percent} met, finishing")
|
||||
|
||||
scrobble.in_progress = False
|
||||
scrobble.is_paused = False
|
||||
scrobble.played_to_completion = True
|
||||
@ -80,5 +85,3 @@ def check_scrobble_for_finish(scrobble: "Scrobble") -> None:
|
||||
if getattr(settings, "KEEP_DETAILED_SCROBBLE_LOGS", False):
|
||||
scrobble.scrobble_log += f"\n{str(scrobble.timestamp)} - {scrobble.playback_position} - {str(scrobble.playback_position_ticks)} - {str(scrobble.percent_played)}%"
|
||||
scrobble.save(update_fields=['scrobble_log'])
|
||||
|
||||
return scrobble
|
||||
|
||||
@ -1,32 +1,36 @@
|
||||
import json
|
||||
import logging
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from dateutil.parser import parse
|
||||
import pytz
|
||||
from django.conf import settings
|
||||
from django.db.models.fields import timezone
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django.views.generic import FormView
|
||||
from django.views.generic.list import ListView
|
||||
from music.constants import JELLYFIN_POST_KEYS as KEYS
|
||||
from music.models import Track
|
||||
from rest_framework import status
|
||||
from rest_framework.decorators import api_view
|
||||
from rest_framework.decorators import api_view, permission_classes
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
from scrobbles.constants import (
|
||||
JELLYFIN_AUDIO_ITEM_TYPES,
|
||||
JELLYFIN_VIDEO_ITEM_TYPES,
|
||||
)
|
||||
from scrobbles.forms import ScrobbleForm
|
||||
from scrobbles.imdb import lookup_video_from_imdb
|
||||
from scrobbles.models import Scrobble
|
||||
from scrobbles.scrobblers import (
|
||||
jellyfin_scrobble_track,
|
||||
jellyfin_scrobble_video,
|
||||
manual_scrobble_event,
|
||||
manual_scrobble_video,
|
||||
mopidy_scrobble_podcast,
|
||||
mopidy_scrobble_track,
|
||||
)
|
||||
from scrobbles.serializers import ScrobbleSerializer
|
||||
from scrobbles.utils import convert_to_seconds
|
||||
from videos.models import Video
|
||||
from scrobbles.thesportsdb import lookup_event_from_thesportsdb
|
||||
|
||||
from vrobbler.apps.music.aggregators import (
|
||||
scrobble_counts,
|
||||
@ -37,49 +41,52 @@ from vrobbler.apps.music.aggregators import (
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
TRUTHY_VALUES = [
|
||||
'true',
|
||||
'1',
|
||||
't',
|
||||
'y',
|
||||
'yes',
|
||||
'yeah',
|
||||
'yup',
|
||||
'certainly',
|
||||
'uh-huh',
|
||||
]
|
||||
|
||||
|
||||
class RecentScrobbleList(ListView):
|
||||
model = Scrobble
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
data = super().get_context_data(**kwargs)
|
||||
user = self.request.user
|
||||
now = timezone.now()
|
||||
last_eight_minutes = timezone.now() - timedelta(minutes=8)
|
||||
# Find scrobbles from the last 10 minutes
|
||||
data['now_playing_list'] = Scrobble.objects.filter(
|
||||
in_progress=True,
|
||||
is_paused=False,
|
||||
modified__gte=last_eight_minutes,
|
||||
timestamp__lte=now,
|
||||
)
|
||||
data['video_scrobble_list'] = Scrobble.objects.filter(
|
||||
video__isnull=False, played_to_completion=True
|
||||
).order_by('-timestamp')[:10]
|
||||
data['podcast_scrobble_list'] = Scrobble.objects.filter(
|
||||
podcast_episode__isnull=False, played_to_completion=True
|
||||
).order_by('-timestamp')[:10]
|
||||
data['top_daily_tracks'] = top_tracks()
|
||||
data['top_weekly_tracks'] = top_tracks(filter='week')
|
||||
data['top_monthly_tracks'] = top_tracks(filter='month')
|
||||
if user.is_authenticated:
|
||||
if user.profile:
|
||||
timezone.activate(pytz.timezone(user.profile.timezone))
|
||||
now = timezone.localtime(timezone.now())
|
||||
data['now_playing_list'] = Scrobble.objects.filter(
|
||||
in_progress=True,
|
||||
is_paused=False,
|
||||
timestamp__lte=now,
|
||||
user=user,
|
||||
)
|
||||
|
||||
data['top_daily_artists'] = top_artists()
|
||||
data['top_weekly_artists'] = top_artists(filter='week')
|
||||
data['top_monthly_artists'] = top_artists(filter='month')
|
||||
completed_for_user = Scrobble.objects.filter(
|
||||
played_to_completion=True, user=user
|
||||
)
|
||||
data['video_scrobble_list'] = completed_for_user.filter(
|
||||
video__isnull=False
|
||||
).order_by('-timestamp')[:15]
|
||||
|
||||
data["weekly_data"] = week_of_scrobbles()
|
||||
data['counts'] = scrobble_counts()
|
||||
data['podcast_scrobble_list'] = completed_for_user.filter(
|
||||
podcast_episode__isnull=False
|
||||
).order_by('-timestamp')[:15]
|
||||
|
||||
data['sport_scrobble_list'] = completed_for_user.filter(
|
||||
sport_event__isnull=False
|
||||
).order_by('-timestamp')[:15]
|
||||
|
||||
# data['top_daily_tracks'] = top_tracks()
|
||||
data['top_weekly_tracks'] = top_tracks(user, filter='week')
|
||||
data['top_monthly_tracks'] = top_tracks(user, filter='month')
|
||||
|
||||
# data['top_daily_artists'] = top_artists()
|
||||
data['top_weekly_artists'] = top_artists(user, filter='week')
|
||||
data['top_monthly_artists'] = top_artists(user, filter='month')
|
||||
|
||||
data["weekly_data"] = week_of_scrobbles(user=user)
|
||||
|
||||
data['counts'] = scrobble_counts(user)
|
||||
data['imdb_form'] = ScrobbleForm
|
||||
return data
|
||||
|
||||
def get_queryset(self):
|
||||
@ -88,6 +95,32 @@ class RecentScrobbleList(ListView):
|
||||
).order_by('-timestamp')[:15]
|
||||
|
||||
|
||||
class ManualScrobbleView(FormView):
|
||||
form_class = ScrobbleForm
|
||||
template_name = 'scrobbles/manual_form.html'
|
||||
|
||||
def form_valid(self, form):
|
||||
|
||||
item_id = form.cleaned_data.get('item_id')
|
||||
data_dict = None
|
||||
if 'tt' in item_id:
|
||||
data_dict = lookup_video_from_imdb(
|
||||
form.cleaned_data.get('item_id')
|
||||
)
|
||||
if data_dict:
|
||||
manual_scrobble_video(data_dict, self.request.user.id)
|
||||
|
||||
if not data_dict:
|
||||
logger.debug(f"Looking for sport event with ID {item_id}")
|
||||
data_dict = lookup_event_from_thesportsdb(
|
||||
form.cleaned_data.get('item_id')
|
||||
)
|
||||
if data_dict:
|
||||
manual_scrobble_event(data_dict, self.request.user.id)
|
||||
|
||||
return HttpResponseRedirect(reverse("home"))
|
||||
|
||||
|
||||
@csrf_exempt
|
||||
@api_view(['GET'])
|
||||
def scrobble_endpoint(request):
|
||||
@ -99,6 +132,7 @@ def scrobble_endpoint(request):
|
||||
|
||||
@csrf_exempt
|
||||
@api_view(['POST'])
|
||||
@permission_classes([IsAuthenticated])
|
||||
def jellyfin_websocket(request):
|
||||
data_dict = request.data
|
||||
|
||||
@ -119,15 +153,18 @@ def jellyfin_websocket(request):
|
||||
if not scrobble:
|
||||
return Response({}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
return Response(
|
||||
{'scrobble_id': scrobble.id}, status=status.HTTP_201_CREATED
|
||||
)
|
||||
return Response({'scrobble_id': scrobble.id}, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
@csrf_exempt
|
||||
@api_view(['POST'])
|
||||
@permission_classes([IsAuthenticated])
|
||||
def mopidy_websocket(request):
|
||||
data_dict = json.loads(request.data)
|
||||
try:
|
||||
data_dict = json.loads(request.data)
|
||||
except TypeError:
|
||||
logger.warning('Received Mopidy data as dict, rather than a string')
|
||||
data_dict = request.data
|
||||
|
||||
# For making things easier to build new input processors
|
||||
if getattr(settings, "DUMP_REQUEST_DATA", False):
|
||||
@ -142,6 +179,39 @@ def mopidy_websocket(request):
|
||||
if not scrobble:
|
||||
return Response({}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
return Response({'scrobble_id': scrobble.id}, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
@csrf_exempt
|
||||
@api_view(['GET'])
|
||||
@permission_classes([IsAuthenticated])
|
||||
def scrobble_finish(request, uuid):
|
||||
user = request.user
|
||||
if not user.is_authenticated:
|
||||
return Response({}, status=status.HTTP_403_FORBIDDEN)
|
||||
|
||||
scrobble = Scrobble.objects.filter(user=user, uuid=uuid).first()
|
||||
if not scrobble:
|
||||
return Response({}, status=status.HTTP_404_NOT_FOUND)
|
||||
scrobble.stop(force_finish=True)
|
||||
return Response(
|
||||
{'scrobble_id': scrobble.id}, status=status.HTTP_201_CREATED
|
||||
{'id': scrobble.id, 'status': scrobble.status},
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
|
||||
@csrf_exempt
|
||||
@api_view(['GET'])
|
||||
@permission_classes([IsAuthenticated])
|
||||
def scrobble_cancel(request, uuid):
|
||||
user = request.user
|
||||
if not user.is_authenticated:
|
||||
return Response({}, status=status.HTTP_403_FORBIDDEN)
|
||||
|
||||
scrobble = Scrobble.objects.filter(user=user, uuid=uuid).first()
|
||||
if not scrobble:
|
||||
return Response({}, status=status.HTTP_404_NOT_FOUND)
|
||||
scrobble.cancel()
|
||||
return Response(
|
||||
{'id': scrobble.id, 'status': 'cancelled'}, status=status.HTTP_200_OK
|
||||
)
|
||||
|
||||
0
vrobbler/apps/sports/__init__.py
Normal file
0
vrobbler/apps/sports/__init__.py
Normal file
77
vrobbler/apps/sports/admin.py
Normal file
77
vrobbler/apps/sports/admin.py
Normal file
@ -0,0 +1,77 @@
|
||||
from django.contrib import admin
|
||||
|
||||
from sports.models import (
|
||||
League,
|
||||
Player,
|
||||
Round,
|
||||
Season,
|
||||
Sport,
|
||||
SportEvent,
|
||||
Team,
|
||||
)
|
||||
|
||||
from scrobbles.admin import ScrobbleInline
|
||||
|
||||
|
||||
@admin.register(Sport)
|
||||
class SportAdmin(admin.ModelAdmin):
|
||||
date_hierarchy = "created"
|
||||
ordering = ("name",)
|
||||
|
||||
|
||||
@admin.register(League)
|
||||
class LeagueAdmin(admin.ModelAdmin):
|
||||
date_hierarchy = "created"
|
||||
list_display = ("name", "abbreviation_str")
|
||||
ordering = ("name",)
|
||||
|
||||
|
||||
@admin.register(Player)
|
||||
class PlayerAdmin(admin.ModelAdmin):
|
||||
date_hierarchy = "created"
|
||||
list_display = ("name", "league", "team")
|
||||
ordering = ("name",)
|
||||
|
||||
|
||||
@admin.register(Season)
|
||||
class SeasonAdmin(admin.ModelAdmin):
|
||||
date_hierarchy = "created"
|
||||
list_display = ("name", "league")
|
||||
ordering = ("name",)
|
||||
|
||||
|
||||
@admin.register(Round)
|
||||
class RoundAdmin(admin.ModelAdmin):
|
||||
date_hierarchy = "created"
|
||||
list_display = ("name", "season")
|
||||
ordering = ("name",)
|
||||
|
||||
|
||||
@admin.register(Team)
|
||||
class TeamAdmin(admin.ModelAdmin):
|
||||
date_hierarchy = "created"
|
||||
list_display = ("name", "league")
|
||||
ordering = ("name",)
|
||||
|
||||
|
||||
@admin.register(SportEvent)
|
||||
class SportEventAdmin(admin.ModelAdmin):
|
||||
date_hierarchy = "created"
|
||||
list_display = (
|
||||
"title",
|
||||
"event_type",
|
||||
"start",
|
||||
"comp_str",
|
||||
"round",
|
||||
)
|
||||
list_filter = ("round__season", "home_team", "away_team")
|
||||
ordering = ("-created",)
|
||||
inlines = [
|
||||
ScrobbleInline,
|
||||
]
|
||||
|
||||
def comp_str(self, obj):
|
||||
if obj.home_team:
|
||||
return f'{obj.away_team} @ {obj.home_team}'
|
||||
if obj.player_one:
|
||||
return f'{obj.player_one} v {obj.player_two}'
|
||||
5
vrobbler/apps/sports/apps.py
Normal file
5
vrobbler/apps/sports/apps.py
Normal file
@ -0,0 +1,5 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class SportsConfig(AppConfig):
|
||||
name = "sports"
|
||||
214
vrobbler/apps/sports/migrations/0001_initial.py
Normal file
214
vrobbler/apps/sports/migrations/0001_initial.py
Normal file
@ -0,0 +1,214 @@
|
||||
# Generated by Django 4.1.5 on 2023-01-14 21:14
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import django_extensions.db.fields
|
||||
import uuid
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = []
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='League',
|
||||
fields=[
|
||||
(
|
||||
'id',
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name='ID',
|
||||
),
|
||||
),
|
||||
(
|
||||
'created',
|
||||
django_extensions.db.fields.CreationDateTimeField(
|
||||
auto_now_add=True, verbose_name='created'
|
||||
),
|
||||
),
|
||||
(
|
||||
'modified',
|
||||
django_extensions.db.fields.ModificationDateTimeField(
|
||||
auto_now=True, verbose_name='modified'
|
||||
),
|
||||
),
|
||||
('name', models.CharField(max_length=255)),
|
||||
(
|
||||
'uuid',
|
||||
models.UUIDField(
|
||||
blank=True,
|
||||
default=uuid.uuid4,
|
||||
editable=False,
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
(
|
||||
'logo',
|
||||
models.ImageField(
|
||||
blank=True, null=True, upload_to='sports/league-logos/'
|
||||
),
|
||||
),
|
||||
(
|
||||
'abbreviation_str',
|
||||
models.CharField(blank=True, max_length=10, null=True),
|
||||
),
|
||||
('thesportsdb_id', models.IntegerField(blank=True, null=True)),
|
||||
],
|
||||
options={
|
||||
'get_latest_by': 'modified',
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Team',
|
||||
fields=[
|
||||
(
|
||||
'id',
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name='ID',
|
||||
),
|
||||
),
|
||||
(
|
||||
'created',
|
||||
django_extensions.db.fields.CreationDateTimeField(
|
||||
auto_now_add=True, verbose_name='created'
|
||||
),
|
||||
),
|
||||
(
|
||||
'modified',
|
||||
django_extensions.db.fields.ModificationDateTimeField(
|
||||
auto_now=True, verbose_name='modified'
|
||||
),
|
||||
),
|
||||
('name', models.CharField(max_length=255)),
|
||||
(
|
||||
'uuid',
|
||||
models.UUIDField(
|
||||
blank=True,
|
||||
default=uuid.uuid4,
|
||||
editable=False,
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
('thesportsdb_id', models.IntegerField(blank=True, null=True)),
|
||||
(
|
||||
'league',
|
||||
models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.DO_NOTHING,
|
||||
to='sports.league',
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
'get_latest_by': 'modified',
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='SportEvent',
|
||||
fields=[
|
||||
(
|
||||
'id',
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name='ID',
|
||||
),
|
||||
),
|
||||
(
|
||||
'created',
|
||||
django_extensions.db.fields.CreationDateTimeField(
|
||||
auto_now_add=True, verbose_name='created'
|
||||
),
|
||||
),
|
||||
(
|
||||
'modified',
|
||||
django_extensions.db.fields.ModificationDateTimeField(
|
||||
auto_now=True, verbose_name='modified'
|
||||
),
|
||||
),
|
||||
(
|
||||
'uuid',
|
||||
models.UUIDField(
|
||||
blank=True,
|
||||
default=uuid.uuid4,
|
||||
editable=False,
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
(
|
||||
'title',
|
||||
models.CharField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
(
|
||||
'run_time',
|
||||
models.CharField(blank=True, max_length=8, null=True),
|
||||
),
|
||||
(
|
||||
'run_time_ticks',
|
||||
models.PositiveBigIntegerField(blank=True, null=True),
|
||||
),
|
||||
(
|
||||
'event_type',
|
||||
models.CharField(
|
||||
choices=[
|
||||
('UK', 'Unknown'),
|
||||
('GA', 'Game'),
|
||||
('MA', 'Match'),
|
||||
('ME', 'Meet'),
|
||||
],
|
||||
default='UK',
|
||||
max_length=2,
|
||||
),
|
||||
),
|
||||
('start_utc', models.DateTimeField(blank=True, null=True)),
|
||||
(
|
||||
'season',
|
||||
models.CharField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
(
|
||||
'away_team',
|
||||
models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.DO_NOTHING,
|
||||
related_name='away_event_set',
|
||||
to='sports.team',
|
||||
),
|
||||
),
|
||||
(
|
||||
'home_team',
|
||||
models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.DO_NOTHING,
|
||||
related_name='home_event_set',
|
||||
to='sports.team',
|
||||
),
|
||||
),
|
||||
(
|
||||
'league',
|
||||
models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.DO_NOTHING,
|
||||
to='sports.league',
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,18 @@
|
||||
# Generated by Django 4.1.5 on 2023-01-14 21:17
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('sports', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
model_name='sportevent',
|
||||
old_name='start_utc',
|
||||
new_name='start',
|
||||
),
|
||||
]
|
||||
71
vrobbler/apps/sports/migrations/0003_sport_league_sport.py
Normal file
71
vrobbler/apps/sports/migrations/0003_sport_league_sport.py
Normal file
@ -0,0 +1,71 @@
|
||||
# Generated by Django 4.1.5 on 2023-01-21 04:21
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import django_extensions.db.fields
|
||||
import uuid
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('sports', '0002_rename_start_utc_sportevent_start'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Sport',
|
||||
fields=[
|
||||
(
|
||||
'id',
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name='ID',
|
||||
),
|
||||
),
|
||||
(
|
||||
'created',
|
||||
django_extensions.db.fields.CreationDateTimeField(
|
||||
auto_now_add=True, verbose_name='created'
|
||||
),
|
||||
),
|
||||
(
|
||||
'modified',
|
||||
django_extensions.db.fields.ModificationDateTimeField(
|
||||
auto_now=True, verbose_name='modified'
|
||||
),
|
||||
),
|
||||
('name', models.CharField(max_length=255)),
|
||||
(
|
||||
'uuid',
|
||||
models.UUIDField(
|
||||
blank=True,
|
||||
default=uuid.uuid4,
|
||||
editable=False,
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
('thesportsdb_id', models.IntegerField(blank=True, null=True)),
|
||||
(
|
||||
'default_event_run_time',
|
||||
models.IntegerField(blank=True, null=True),
|
||||
),
|
||||
],
|
||||
options={
|
||||
'get_latest_by': 'modified',
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='league',
|
||||
name='sport',
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.DO_NOTHING,
|
||||
to='sports.sport',
|
||||
),
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,254 @@
|
||||
# Generated by Django 4.1.5 on 2023-01-22 20:20
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import django_extensions.db.fields
|
||||
import uuid
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('sports', '0003_sport_league_sport'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='league',
|
||||
options={},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='sport',
|
||||
options={},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='team',
|
||||
options={},
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='sportevent',
|
||||
name='league',
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='league',
|
||||
name='thesportsdb_id',
|
||||
field=models.CharField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='sport',
|
||||
name='thesportsdb_id',
|
||||
field=models.CharField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='team',
|
||||
name='thesportsdb_id',
|
||||
field=models.CharField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Season',
|
||||
fields=[
|
||||
(
|
||||
'id',
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name='ID',
|
||||
),
|
||||
),
|
||||
(
|
||||
'created',
|
||||
django_extensions.db.fields.CreationDateTimeField(
|
||||
auto_now_add=True, verbose_name='created'
|
||||
),
|
||||
),
|
||||
(
|
||||
'modified',
|
||||
django_extensions.db.fields.ModificationDateTimeField(
|
||||
auto_now=True, verbose_name='modified'
|
||||
),
|
||||
),
|
||||
('name', models.CharField(max_length=255)),
|
||||
(
|
||||
'uuid',
|
||||
models.UUIDField(
|
||||
blank=True,
|
||||
default=uuid.uuid4,
|
||||
editable=False,
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
(
|
||||
'thesportsdb_id',
|
||||
models.CharField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
(
|
||||
'league',
|
||||
models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.DO_NOTHING,
|
||||
to='sports.league',
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Round',
|
||||
fields=[
|
||||
(
|
||||
'id',
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name='ID',
|
||||
),
|
||||
),
|
||||
(
|
||||
'created',
|
||||
django_extensions.db.fields.CreationDateTimeField(
|
||||
auto_now_add=True, verbose_name='created'
|
||||
),
|
||||
),
|
||||
(
|
||||
'modified',
|
||||
django_extensions.db.fields.ModificationDateTimeField(
|
||||
auto_now=True, verbose_name='modified'
|
||||
),
|
||||
),
|
||||
('name', models.CharField(max_length=255)),
|
||||
(
|
||||
'uuid',
|
||||
models.UUIDField(
|
||||
blank=True,
|
||||
default=uuid.uuid4,
|
||||
editable=False,
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
(
|
||||
'thesportsdb_id',
|
||||
models.CharField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
(
|
||||
'season',
|
||||
models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.DO_NOTHING,
|
||||
to='sports.season',
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Player',
|
||||
fields=[
|
||||
(
|
||||
'id',
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name='ID',
|
||||
),
|
||||
),
|
||||
(
|
||||
'created',
|
||||
django_extensions.db.fields.CreationDateTimeField(
|
||||
auto_now_add=True, verbose_name='created'
|
||||
),
|
||||
),
|
||||
(
|
||||
'modified',
|
||||
django_extensions.db.fields.ModificationDateTimeField(
|
||||
auto_now=True, verbose_name='modified'
|
||||
),
|
||||
),
|
||||
('name', models.CharField(max_length=255)),
|
||||
(
|
||||
'uuid',
|
||||
models.UUIDField(
|
||||
blank=True,
|
||||
default=uuid.uuid4,
|
||||
editable=False,
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
(
|
||||
'thesportsdb_id',
|
||||
models.CharField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
(
|
||||
'league',
|
||||
models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.DO_NOTHING,
|
||||
to='sports.league',
|
||||
),
|
||||
),
|
||||
(
|
||||
'team',
|
||||
models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.DO_NOTHING,
|
||||
to='sports.team',
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='sportevent',
|
||||
name='player_one',
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.DO_NOTHING,
|
||||
related_name='player_one_set',
|
||||
to='sports.player',
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='sportevent',
|
||||
name='player_two',
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.DO_NOTHING,
|
||||
related_name='player_two_set',
|
||||
to='sports.player',
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='sportevent',
|
||||
name='round',
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.DO_NOTHING,
|
||||
to='sports.round',
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='sportevent',
|
||||
name='season',
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.DO_NOTHING,
|
||||
to='sports.season',
|
||||
),
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,17 @@
|
||||
# Generated by Django 4.1.5 on 2023-01-22 20:34
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('sports', '0004_alter_league_options_alter_sport_options_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='sportevent',
|
||||
name='season',
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,22 @@
|
||||
# Generated by Django 4.1.5 on 2023-01-22 21:48
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('sports', '0005_remove_sportevent_season'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='sportevent',
|
||||
name='event_type',
|
||||
field=models.CharField(
|
||||
choices=[('UK', 'Unknown'), ('GA', 'Game'), ('MA', 'Match')],
|
||||
default='UK',
|
||||
max_length=2,
|
||||
),
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,22 @@
|
||||
# Generated by Django 4.1.5 on 2023-01-22 22:11
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('sports', '0006_alter_sportevent_event_type'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='sport',
|
||||
name='default_event_type',
|
||||
field=models.CharField(
|
||||
choices=[('UK', 'Unknown'), ('GA', 'Game'), ('MA', 'Match')],
|
||||
default='UK',
|
||||
max_length=2,
|
||||
),
|
||||
),
|
||||
]
|
||||
0
vrobbler/apps/sports/migrations/__init__.py
Normal file
0
vrobbler/apps/sports/migrations/__init__.py
Normal file
223
vrobbler/apps/sports/models.py
Normal file
223
vrobbler/apps/sports/models.py
Normal file
@ -0,0 +1,223 @@
|
||||
import logging
|
||||
from typing import Dict
|
||||
from uuid import uuid4
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import models
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django_extensions.db.models import TimeStampedModel
|
||||
from sqlalchemy import update
|
||||
from scrobbles.mixins import ScrobblableMixin
|
||||
from vrobbler.apps.sports.utils import (
|
||||
get_players_from_event,
|
||||
get_round_name_from_event,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
BNULL = {"blank": True, "null": True}
|
||||
|
||||
|
||||
class SportEventType(models.TextChoices):
|
||||
UNKNOWN = 'UK', _('Unknown')
|
||||
GAME = 'GA', _('Game')
|
||||
MATCH = 'MA', _('Match')
|
||||
|
||||
|
||||
class TheSportsDbMixin(TimeStampedModel):
|
||||
name = models.CharField(max_length=255)
|
||||
uuid = models.UUIDField(default=uuid4, editable=False, **BNULL)
|
||||
thesportsdb_id = models.CharField(max_length=255, **BNULL)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class Sport(TheSportsDbMixin):
|
||||
default_event_run_time = models.IntegerField(**BNULL)
|
||||
default_event_type = models.CharField(
|
||||
max_length=2,
|
||||
choices=SportEventType.choices,
|
||||
default=SportEventType.UNKNOWN,
|
||||
)
|
||||
|
||||
# TODO Add these to the default run_time for Football
|
||||
# run_time_seconds = 11700
|
||||
# run_time_ticks = run_time_seconds * 1000
|
||||
@property
|
||||
def default_event_run_time_ticks(self):
|
||||
return self.default_event_run_time * 1000
|
||||
|
||||
|
||||
class League(TheSportsDbMixin):
|
||||
logo = models.ImageField(upload_to="sports/league-logos/", **BNULL)
|
||||
abbreviation_str = models.CharField(max_length=10, **BNULL)
|
||||
sport = models.ForeignKey(Sport, on_delete=models.DO_NOTHING, **BNULL)
|
||||
|
||||
@property
|
||||
def abbreviation(self):
|
||||
return self.abbreviation_str
|
||||
|
||||
|
||||
class Season(TheSportsDbMixin):
|
||||
league = models.ForeignKey(League, on_delete=models.DO_NOTHING, **BNULL)
|
||||
|
||||
def __str__(self):
|
||||
return f'{self.name} season of {self.league}'
|
||||
|
||||
|
||||
class Team(TheSportsDbMixin):
|
||||
league = models.ForeignKey(League, on_delete=models.DO_NOTHING, **BNULL)
|
||||
|
||||
|
||||
class Player(TheSportsDbMixin):
|
||||
league = models.ForeignKey(League, on_delete=models.DO_NOTHING, **BNULL)
|
||||
team = models.ForeignKey(Team, on_delete=models.DO_NOTHING, **BNULL)
|
||||
|
||||
|
||||
class Round(TheSportsDbMixin):
|
||||
season = models.ForeignKey(Season, on_delete=models.DO_NOTHING, **BNULL)
|
||||
|
||||
def __str__(self):
|
||||
return f'{self.name} of {self.season}'
|
||||
|
||||
|
||||
class SportEvent(ScrobblableMixin):
|
||||
COMPLETION_PERCENT = getattr(settings, 'SPORT_COMPLETION_PERCENT', 90)
|
||||
|
||||
event_type = models.CharField(
|
||||
max_length=2,
|
||||
choices=SportEventType.choices,
|
||||
default=SportEventType.UNKNOWN,
|
||||
)
|
||||
round = models.ForeignKey(Round, on_delete=models.DO_NOTHING, **BNULL)
|
||||
start = models.DateTimeField(**BNULL)
|
||||
home_team = models.ForeignKey(
|
||||
Team,
|
||||
on_delete=models.DO_NOTHING,
|
||||
related_name='home_event_set',
|
||||
**BNULL,
|
||||
)
|
||||
away_team = models.ForeignKey(
|
||||
Team,
|
||||
on_delete=models.DO_NOTHING,
|
||||
related_name='away_event_set',
|
||||
**BNULL,
|
||||
)
|
||||
player_one = models.ForeignKey(
|
||||
Player,
|
||||
on_delete=models.DO_NOTHING,
|
||||
related_name='player_one_set',
|
||||
**BNULL,
|
||||
)
|
||||
player_two = models.ForeignKey(
|
||||
Player,
|
||||
on_delete=models.DO_NOTHING,
|
||||
related_name='player_two_set',
|
||||
**BNULL,
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.start.date()} - {self.round} - {self.home_team} v {self.away_team}"
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse("sports:event_detail", kwargs={'slug': self.uuid})
|
||||
|
||||
@classmethod
|
||||
def find_or_create(cls, data_dict: Dict) -> "Event":
|
||||
"""Given a data dict from Jellyfin, does the heavy lifting of looking up
|
||||
the video and, if need, TV Series, creating both if they don't yet
|
||||
exist.
|
||||
|
||||
"""
|
||||
# Find or create our Sport
|
||||
sid = data_dict.get("Sport")
|
||||
sport, s_created = Sport.objects.get_or_create(thesportsdb_id=sid)
|
||||
if s_created:
|
||||
sport.name = sid
|
||||
sport.save(update_fields=['name'])
|
||||
|
||||
# Find or create our League
|
||||
lid = data_dict.get("LeagueId")
|
||||
league, l_created = League.objects.get_or_create(
|
||||
thesportsdb_id=lid, sport=sport
|
||||
)
|
||||
if l_created:
|
||||
league.sport = sport
|
||||
league.name = data_dict.get("LeagueName", "")
|
||||
league.save(update_fields=['sport', 'name'])
|
||||
|
||||
# Find or create our Season
|
||||
seid = data_dict.get('Season')
|
||||
season, se_created = Season.objects.get_or_create(
|
||||
thesportsdb_id=seid, league=league
|
||||
)
|
||||
if se_created:
|
||||
season.name = seid
|
||||
season.save(update_fields['name'])
|
||||
|
||||
# Find or create our Round
|
||||
rid = data_dict.get('RoundId')
|
||||
round, r_created = Round.objects.get_or_create(
|
||||
thesportsdb_id=rid, season=season
|
||||
)
|
||||
if r_created:
|
||||
round.season = season
|
||||
round.save(update_fields=['season'])
|
||||
|
||||
# Set some special data for Tennis
|
||||
player_one = None
|
||||
player_two = None
|
||||
if data_dict.get('Sport') == 'Tennis':
|
||||
event_name = data_dict.get('Name', '')
|
||||
if not round.name:
|
||||
round.name = get_round_name_from_event(event_name)
|
||||
round.save(update_fields=['name'])
|
||||
|
||||
players_list = get_players_from_event(event_name)
|
||||
player_one = Player.objects.filter(
|
||||
name__icontains=players_list[0]
|
||||
).first()
|
||||
if not player_one:
|
||||
player_one = Player.objects.create(name=players_list[0])
|
||||
player_two = Player.objects.filter(
|
||||
name__icontains=players_list[1]
|
||||
).first()
|
||||
if not player_two:
|
||||
player_two = Player.objects.create(name=players_list[1])
|
||||
|
||||
home_team = None
|
||||
away_team = None
|
||||
if data_dict.get("HomeTeamName"):
|
||||
home_team_dict = {
|
||||
"name": data_dict.get("HomeTeamName", ""),
|
||||
"thesportsdb_id": data_dict.get("HomeTeamId", ""),
|
||||
"league": league,
|
||||
}
|
||||
home_team, _created = Team.objects.get_or_create(**home_team_dict)
|
||||
|
||||
away_team_dict = {
|
||||
"name": data_dict.get("AwayTeamName", ""),
|
||||
"thesportsdb_id": data_dict.get("AwayTeamId", ""),
|
||||
"league": league,
|
||||
}
|
||||
away_team, _created = Team.objects.get_or_create(**away_team_dict)
|
||||
|
||||
event_dict = {
|
||||
"title": data_dict.get("Name"),
|
||||
"event_type": sport.default_event_type,
|
||||
"home_team": home_team,
|
||||
"away_team": away_team,
|
||||
"player_one": player_one,
|
||||
"player_two": player_two,
|
||||
"start": data_dict['Start'],
|
||||
"round": round,
|
||||
"run_time_ticks": data_dict.get("RunTimeTicks"),
|
||||
"run_time": data_dict.get("RunTime", ""),
|
||||
}
|
||||
event, _created = cls.objects.get_or_create(**event_dict)
|
||||
|
||||
return event
|
||||
11
vrobbler/apps/sports/utils.py
Normal file
11
vrobbler/apps/sports/utils.py
Normal file
@ -0,0 +1,11 @@
|
||||
def get_round_name_from_event(event: str) -> str:
|
||||
return ' '.join(event.split(' ')[:2])
|
||||
|
||||
|
||||
def get_players_from_event(event: str) -> list[str]:
|
||||
players = []
|
||||
event_name = get_round_name_from_event(event)
|
||||
players_list = event.split(event_name)[1:][0].split('vs')
|
||||
players.append(players_list[0].strip())
|
||||
players.append(players_list[1].strip())
|
||||
return players
|
||||
@ -1,16 +1,20 @@
|
||||
from django.contrib import admin
|
||||
|
||||
from scrobbles.models import Scrobble
|
||||
from videos.models import Series, Video
|
||||
from scrobbles.admin import ScrobbleInline
|
||||
|
||||
|
||||
@admin.register(Series)
|
||||
class SeriesAdmin(admin.ModelAdmin):
|
||||
date_hierarchy = "created"
|
||||
list_display = ("name", "tagline")
|
||||
ordering = ("-created",)
|
||||
|
||||
|
||||
@admin.register(Video)
|
||||
class VideoAdmin(admin.ModelAdmin):
|
||||
date_hierarchy = "created"
|
||||
raw_id_fields = ('tv_series',)
|
||||
list_display = (
|
||||
"title",
|
||||
"video_type",
|
||||
@ -22,7 +26,6 @@ class VideoAdmin(admin.ModelAdmin):
|
||||
)
|
||||
list_filter = ("year", "tv_series", "video_type")
|
||||
ordering = ("-created",)
|
||||
|
||||
|
||||
admin.site.register(Series, SeriesAdmin)
|
||||
admin.site.register(Video, VideoAdmin)
|
||||
inlines = [
|
||||
ScrobbleInline,
|
||||
]
|
||||
|
||||
@ -2,6 +2,7 @@ import logging
|
||||
from typing import Dict
|
||||
from uuid import uuid4
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import models
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
@ -32,6 +33,8 @@ class Series(TimeStampedModel):
|
||||
|
||||
|
||||
class Video(ScrobblableMixin):
|
||||
COMPLETION_PERCENT = getattr(settings, 'VIDEO_COMPLETION_PERCENT', 90)
|
||||
|
||||
class VideoType(models.TextChoices):
|
||||
UNKNOWN = 'U', _('Unknown')
|
||||
TV_EPISODE = 'E', _('TV Episode')
|
||||
@ -82,6 +85,7 @@ class Video(ScrobblableMixin):
|
||||
"video_type": Video.VideoType.MOVIE,
|
||||
}
|
||||
|
||||
series = None
|
||||
if data_dict.get("ItemType", "") == "Episode":
|
||||
series_name = data_dict.get("SeriesName", "")
|
||||
series, series_created = Series.objects.get_or_create(
|
||||
@ -95,20 +99,26 @@ class Video(ScrobblableMixin):
|
||||
|
||||
video, created = cls.objects.get_or_create(**video_dict)
|
||||
|
||||
run_time_ticks = data_dict.get("RunTimeTicks", None)
|
||||
if run_time_ticks:
|
||||
run_time_ticks = run_time_ticks // 10000
|
||||
|
||||
video_extra_dict = {
|
||||
"year": data_dict.get("Year", ""),
|
||||
"overview": data_dict.get("Overview", None),
|
||||
"tagline": data_dict.get("Tagline", None),
|
||||
"run_time_ticks": data_dict.get("RunTimeTicks", 0) // 10000,
|
||||
"run_time_ticks": run_time_ticks,
|
||||
"run_time": convert_to_seconds(data_dict.get("RunTime", "")),
|
||||
"tv_series_id": series.id,
|
||||
"tvdb_id": data_dict.get("Provider_tvdb", None),
|
||||
"tvrage_id": data_dict.get("Provider_tvrage", None),
|
||||
"episode_number": data_dict.get("EpisodeNumber", ""),
|
||||
"season_number": data_dict.get("SeasonNumber", ""),
|
||||
"episode_number": data_dict.get("EpisodeNumber", None),
|
||||
"season_number": data_dict.get("SeasonNumber", None),
|
||||
}
|
||||
|
||||
if created:
|
||||
if series:
|
||||
video_extra_dict["tv_series_id"] = series.id
|
||||
|
||||
if not video.run_time_ticks:
|
||||
logger.debug(f"Created new video: {video}")
|
||||
for key, value in video_extra_dict.items():
|
||||
setattr(video, key, value)
|
||||
|
||||
@ -37,10 +37,6 @@ KEEP_DETAILED_SCROBBLE_LOGS = os.getenv(
|
||||
"VROBBLER_KEEP_DETAILED_SCROBBLE_LOGS", False
|
||||
)
|
||||
|
||||
PODCAST_COMPLETION_PERCENT = os.getenv(
|
||||
"VROBBLER_PODCAST_COMPLETION_PERCENT", 25
|
||||
)
|
||||
MUSIC_COMPLETION_PERCENT = os.getenv("VROBBLER_MUSIC_COMPLETION_PERCENT", 90)
|
||||
|
||||
# Should we cull old in-progress scrobbles that are beyond the wait period for resuming?
|
||||
DELETE_STALE_SCROBBLES = os.getenv("VROBBLER_DELETE_STALE_SCROBBLES", True)
|
||||
@ -48,34 +44,26 @@ DELETE_STALE_SCROBBLES = os.getenv("VROBBLER_DELETE_STALE_SCROBBLES", True)
|
||||
# Used to dump data coming from srobbling sources, helpful for building new inputs
|
||||
DUMP_REQUEST_DATA = os.getenv("VROBBLER_DUMP_REQUEST_DATA", False)
|
||||
|
||||
VIDEO_BACKOFF_MINUTES = os.getenv("VROBBLER_VIDEO_BACKOFF_MINUTES", 15)
|
||||
MUSIC_BACKOFF_SECONDS = os.getenv("VROBBLER_VIDEO_BACKOFF_SECONDS", 1)
|
||||
|
||||
# If you stop waching or listening to a track, how long should we wait before we
|
||||
# give up on the old scrobble and start a new one? This could also be considered
|
||||
# a "continue in progress scrobble" time period. So if you pause the media and
|
||||
# start again, should it be a new scrobble.
|
||||
VIDEO_WAIT_PERIOD_DAYS = os.getenv("VROBBLER_VIDEO_WAIT_PERIOD_DAYS", 1)
|
||||
MUSIC_WAIT_PERIOD_MINUTES = os.getenv("VROBBLER_VIDEO_BACKOFF_MINUTES", 1)
|
||||
|
||||
THESPORTSDB_API_KEY = os.getenv("VROBBLER_THESPORTSDB_API_KEY", "2")
|
||||
THESPORTSDB_BASE_URL = os.getenv(
|
||||
"VROBBLER_THESPORTSDB_BASE_URL", "https://www.thesportsdb.com/api/v1/json/"
|
||||
)
|
||||
TMDB_API_KEY = os.getenv("VROBBLER_TMDB_API_KEY", "")
|
||||
|
||||
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
||||
|
||||
TIME_ZONE = os.getenv("VROBBLER_TIME_ZONE", "US/Eastern")
|
||||
|
||||
ALLOWED_HOSTS = ["*"]
|
||||
CSRF_TRUSTED_ORIGINS = [
|
||||
os.getenv("VROBBLER_TRUSTED_ORIGINS", "http://localhost:8000")
|
||||
]
|
||||
X_FRAME_OPTIONS = "SAMEORIGIN"
|
||||
|
||||
CACHALOT_TIMEOUT = os.getenv("VROBBLER_CACHALOT_TIMEOUT", 3600)
|
||||
REDIS_URL = os.getenv("VROBBLER_REDIS_URL", None)
|
||||
|
||||
CELERY_TASK_ALWAYS_EAGER = os.getenv("VROBBLER_SKIP_CELERY", False)
|
||||
CELERY_BROKER_URL = REDIS_URL if REDIS_URL else "memory://localhost/"
|
||||
CELERY_RESULT_BACKEND = "django-db"
|
||||
CELERY_TIMEZONE = os.getenv("VROBBLER_TIME_ZONE", "EST")
|
||||
CELERY_TASK_TRACK_STARTED = True
|
||||
|
||||
INSTALLED_APPS = [
|
||||
"django.contrib.admin",
|
||||
"django.contrib.auth",
|
||||
@ -88,10 +76,13 @@ INSTALLED_APPS = [
|
||||
"django_filters",
|
||||
"django_extensions",
|
||||
'rest_framework.authtoken',
|
||||
"cachalot",
|
||||
"profiles",
|
||||
"scrobbles",
|
||||
"videos",
|
||||
"music",
|
||||
"podcasts",
|
||||
"sports",
|
||||
"rest_framework",
|
||||
"allauth",
|
||||
"allauth.account",
|
||||
@ -169,8 +160,8 @@ AUTHENTICATION_BACKENDS = [
|
||||
REST_FRAMEWORK = {
|
||||
"DEFAULT_PERMISSION_CLASSES": ("rest_framework.permissions.AllowAny",),
|
||||
'DEFAULT_AUTHENTICATION_CLASSES': [
|
||||
#'rest_framework.authentication.BasicAuthentication',
|
||||
#'rest_framework.authentication.TokenAuthentication',
|
||||
'rest_framework.authentication.BasicAuthentication',
|
||||
'rest_framework.authentication.TokenAuthentication',
|
||||
'rest_framework.authentication.SessionAuthentication',
|
||||
],
|
||||
"DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination",
|
||||
@ -211,8 +202,6 @@ TIME_ZONE = os.getenv("VROBBLER_TIME_ZONE", "EST")
|
||||
|
||||
USE_I18N = True
|
||||
|
||||
USE_L10N = True
|
||||
|
||||
USE_TZ = True
|
||||
|
||||
|
||||
@ -223,10 +212,6 @@ STATIC_URL = "static/"
|
||||
STATIC_ROOT = os.getenv(
|
||||
"VROBBLER_STATIC_ROOT", os.path.join(PROJECT_ROOT, "static")
|
||||
)
|
||||
if not DEBUG:
|
||||
STATICFILES_STORAGE = (
|
||||
"whitenoise.storage.CompressedManifestStaticFilesStorage"
|
||||
)
|
||||
|
||||
MEDIA_URL = "/media/"
|
||||
MEDIA_ROOT = os.getenv(
|
||||
@ -281,17 +266,17 @@ LOGGING = {
|
||||
"class": "logging.NullHandler",
|
||||
"level": LOG_LEVEL,
|
||||
},
|
||||
"file": {
|
||||
"class": "logging.handlers.RotatingFileHandler",
|
||||
"filename": "".join([LOG_FILE_PATH, "vrobbler.log"]),
|
||||
"formatter": LOG_TYPE,
|
||||
"level": LOG_LEVEL,
|
||||
'sql': {
|
||||
'class': 'logging.handlers.RotatingFileHandler',
|
||||
'filename': ''.join([LOG_FILE_PATH, 'vrobbler_sql.', LOG_TYPE]),
|
||||
'formatter': LOG_TYPE,
|
||||
'level': LOG_LEVEL,
|
||||
},
|
||||
"requests_file": {
|
||||
"class": "logging.handlers.RotatingFileHandler",
|
||||
"filename": "".join([LOG_FILE_PATH, "vrobbler_requests.log"]),
|
||||
"formatter": LOG_TYPE,
|
||||
"level": LOG_LEVEL,
|
||||
'file': {
|
||||
'class': 'logging.handlers.RotatingFileHandler',
|
||||
'filename': ''.join([LOG_FILE_PATH, 'vrobbler.', LOG_TYPE]),
|
||||
'formatter': LOG_TYPE,
|
||||
'level': LOG_LEVEL,
|
||||
},
|
||||
},
|
||||
"loggers": {
|
||||
@ -301,13 +286,15 @@ LOGGING = {
|
||||
"propagate": True,
|
||||
},
|
||||
"django.db.backends": {"handlers": ["null"]},
|
||||
"django.server": {"handlers": ["null"]},
|
||||
"vrobbler": {
|
||||
"handlers": ["console", "file"],
|
||||
"handlers": ["file"],
|
||||
"propagate": True,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if DEBUG:
|
||||
# We clear out a db with lots of games all the time in dev
|
||||
DATA_UPLOAD_MAX_NUMBER_FIELDS = 3000
|
||||
LOG_TO_CONSOLE = os.getenv("VROBBLER_LOG_TO_CONSOLE", False)
|
||||
if LOG_TO_CONSOLE:
|
||||
LOGGING['loggers']['django']['handlers'] = ["console"]
|
||||
LOGGING['loggers']['vrobbler']['handlers'] = ["console"]
|
||||
|
||||
BIN
vrobbler/static/images/apple-touch-icon.png
Normal file
BIN
vrobbler/static/images/apple-touch-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.2 KiB |
BIN
vrobbler/static/images/favicon.ico
Normal file
BIN
vrobbler/static/images/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.2 KiB |
@ -8,7 +8,9 @@
|
||||
<meta http-equiv="x-ua-compatible" content="ie=edge">
|
||||
<meta name="description" content="">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="shortcut icon" type="image/png" href="{% static 'images/favicon.ico' %}"/>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
|
||||
<script src="https://cdn.jsdelivr.net/npm/popper.js@1.12.9/dist/umd/popper.min.js" crossorigin="anonymous"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM" crossorigin="anonymous"></script>
|
||||
<script src="https://code.jquery.com/jquery-3.3.1.js" integrity="sha256-2Kok7MbOyxpgUVvAk/HJ2jigOSYS2auK4Pfzbm7uH60=" crossorigin="anonymous"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/clipboard.js/2.0.10/clipboard.min.js"></script>
|
||||
@ -161,12 +163,11 @@
|
||||
border-color: transparent;
|
||||
box-shadow: 0 0 0 3px rgba(255, 255, 255, .25);
|
||||
}
|
||||
|
||||
#scrobble-form { width: 100% }
|
||||
</style>
|
||||
{% block head_extra %}{% endblock %}
|
||||
|
||||
<link rel="apple-touch-icon" href="/apple-touch-icon.png">
|
||||
<!-- Place favicon.ico in the root directory -->
|
||||
<link rel="apple-touch-icon" href="{% static 'images/apple-touch-icon.png' %}">
|
||||
</head>
|
||||
<body>
|
||||
<header class="navbar navbar-dark sticky-top bg-dark flex-md-nowrap p-0 shadow">
|
||||
@ -174,13 +175,19 @@
|
||||
<button class="navbar-toggler position-absolute d-md-none collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#sidebarMenu" aria-controls="sidebarMenu" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<input class="form-control form-control-dark w-100" type="text" placeholder="Search" aria-label="Search">
|
||||
|
||||
{% if user.is_authenticated %}
|
||||
<form id="scrobble-form" action="{% url 'imdb-manual-scrobble' %}" method="post">
|
||||
{% csrf_token %}
|
||||
{{ imdb_form }}
|
||||
</form>
|
||||
{% endif %}
|
||||
<div class="navbar-nav">
|
||||
<div class="nav-item text-nowrap">
|
||||
{% if user.is_authenticated %}
|
||||
<a class="nav-link px-3" href="{% url "account_logout" %}">Sign out</a>
|
||||
{% else %}
|
||||
<a class="nav-link px-3" href="{% url "account_login" %}">Sign in</a>
|
||||
{% if user.is_authenticated %}
|
||||
<a class="nav-link px-3" href="{% url "account_logout" %}">Sign out</a>
|
||||
{% else %}
|
||||
<a class="nav-link px-3" href="{% url "account_login" %}">Sign in</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
@ -190,7 +197,7 @@
|
||||
<div class="row">
|
||||
<nav id="sidebarMenu" class="col-md-3 col-lg-2 d-md-block bg-light sidebar collapse">
|
||||
<div class="position-sticky pt-3">
|
||||
{% if now_playing_list %}
|
||||
{% if now_playing_list and user.is_authenticated %}
|
||||
<ul style="padding-right:10px;">
|
||||
<b>Now playing</b>
|
||||
{% for scrobble in now_playing_list %}
|
||||
@ -199,11 +206,14 @@
|
||||
{% if scrobble.track %}<em>{{scrobble.track.artist}}</em><br/>{% endif %}
|
||||
{% if scrobble.podcast_episode%}<em>{{scrobble.podcast_episode.podcast}}</em><br/>{% endif %}
|
||||
{% if scrobble.video.tv_series %}<em>{{scrobble.video.tv_series }}</em><br/>{% endif %}
|
||||
<small>{{scrobble.created|naturaltime}}<br/>
|
||||
{% if scrobble.sport_event %}<em>{{scrobble.sport_event.round.season.league}}</em><br/>{% endif %}
|
||||
<small>{{scrobble.timestamp|naturaltime}}<br/>
|
||||
from {{scrobble.source}}</small>
|
||||
<div class="progress-bar" style="margin-right:5px;">
|
||||
<span class="progress-bar-fill" style="width: {{scrobble.percent_played}}%;"></span>
|
||||
</div>
|
||||
<a href="{% url "scrobbles:cancel" scrobble.uuid %}">Cancel</a>
|
||||
<a href="{% url "scrobbles:finish" scrobble.uuid %}">Finish</a>
|
||||
</div>
|
||||
<hr/>
|
||||
{% endfor %}
|
||||
@ -213,7 +223,7 @@
|
||||
|
||||
<ul class="nav flex-column">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" aria-current="page" href="#">
|
||||
<a class="nav-link active" aria-current="page" href="/">
|
||||
<span data-feather="music"></span>
|
||||
Dashboard
|
||||
</a>
|
||||
|
||||
13
vrobbler/templates/scrobbles/manual_form.html
Normal file
13
vrobbler/templates/scrobbles/manual_form.html
Normal file
@ -0,0 +1,13 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<main class="col-md-4 ms-sm-auto col-lg-10 px-md-4">
|
||||
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
|
||||
<h1 class="h2">Manual scrobble</h1>
|
||||
<form action="#" method="post">
|
||||
{% csrf_token %}
|
||||
{{ form }}
|
||||
<input type="submit" value="Submit">
|
||||
</form>
|
||||
</div>
|
||||
</main>
|
||||
{% endblock %}
|
||||
@ -7,14 +7,21 @@
|
||||
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
|
||||
<h1 class="h2">Dashboard</h1>
|
||||
<div class="btn-toolbar mb-2 mb-md-0">
|
||||
<div class="btn-group me-2">
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary">Share</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary">Export</button>
|
||||
</div>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary dropdown-toggle">
|
||||
<span data-feather="calendar"></span>
|
||||
This week
|
||||
</button>
|
||||
<div class="btn-group me-2">
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary">Share</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary">Export</button>
|
||||
</div>
|
||||
<div class="dropdown">
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary dropdown-toggle" id="graphDateButton" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<span data-feather="calendar"></span>
|
||||
This week
|
||||
</button>
|
||||
<div class="dropdown-menu" aria-labelledby="graphDateButton">
|
||||
<a class="dropdown-item" href="#">Action</a>
|
||||
<a class="dropdown-item" href="#">Another action</a>
|
||||
<a class="dropdown-item" href="#">Something else here</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -22,6 +29,7 @@
|
||||
|
||||
<div class="container">
|
||||
|
||||
{% if user.is_authenticated %}
|
||||
<div class="row">
|
||||
<p>Today <b>{{counts.today}}</b> | This Week <b>{{counts.week}}</b> | This Month <b>{{counts.month}}</b> | This Year <b>{{counts.year}}</b> | All Time <b>{{counts.alltime}}</b></p>
|
||||
</div>
|
||||
@ -31,13 +39,19 @@
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link active" id="home-tab" data-bs-toggle="tab" data-bs-target="#artists-week" type="button" role="tab" aria-controls="home" aria-selected="true">Weekly Artists</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="artist-month-tab" data-bs-toggle="tab" data-bs-target="#artists-month" type="button" role="tab" aria-controls="home" aria-selected="true">Monthly Artists</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="profile-tab" data-bs-toggle="tab" data-bs-target="#tracks-week" type="button" role="tab" aria-controls="profile" aria-selected="false">Weekly Tracks</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="profile-tab" data-bs-toggle="tab" data-bs-target="#tracks-month" type="button" role="tab" aria-controls="profile" aria-selected="false">Monthly Tracks</button>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="tab-content" id="myTabContent">
|
||||
<div class="tab-pane fade show" id="artists-week" role="tabpanel" aria-labelledby="artists-week-tab">
|
||||
<div class="tab-pane fade show active" id="artists-week" role="tabpanel" aria-labelledby="artists-week-tab">
|
||||
<h2>Top artists this week</h2>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-sm">
|
||||
@ -59,7 +73,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tab-pane fade show active" id="tracks-week" role="tabpanel" aria-labelledby="tracks-week-tab">
|
||||
<div class="tab-pane fade show" id="tracks-week" role="tabpanel" aria-labelledby="tracks-week-tab">
|
||||
<h2>Top tracks this week</h2>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-sm">
|
||||
@ -82,15 +96,69 @@
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="tab-pane fade show" id="tracks-month" role="tabpanel" aria-labelledby="tracks-month-tab">
|
||||
<h2>Top tracks this month</h2>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">#</th>
|
||||
<th scope="col">Track</th>
|
||||
<th scope="col">Artist</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for track in top_monthly_tracks %}
|
||||
<tr>
|
||||
<td>{{track.num_scrobbles}}</td>
|
||||
<td>{{track.title}}</td>
|
||||
<td>{{track.artist.name}}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tab-pane fade show " id="artists-month" role="tabpanel" aria-labelledby="artists-month-tab">
|
||||
<h2>Top artists this month</h2>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">#</th>
|
||||
<th scope="col">Artist</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for artist in top_monthly_artists %}
|
||||
<tr>
|
||||
<td>{{artist.num_scrobbles}}</td>
|
||||
<td>{{artist.name}}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md">
|
||||
<ul class="nav nav-tabs" id="myTab" role="tablist">
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link active" id="home-tab" data-bs-toggle="tab" data-bs-target="#latest-listened" type="button" role="tab" aria-controls="home" aria-selected="true">Latest Listened</button>
|
||||
<button class="nav-link active" id="home-tab" data-bs-toggle="tab" data-bs-target="#latest-listened" type="button" role="tab" aria-controls="home" aria-selected="true">Tracks</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="profile-tab" data-bs-toggle="tab" data-bs-target="#latest-watched" type="button" role="tab" aria-controls="profile" aria-selected="false">Latest Watched</button>
|
||||
<button class="nav-link" id="profile-tab" data-bs-toggle="tab" data-bs-target="#latest-watched" type="button" role="tab" aria-controls="profile" aria-selected="false">Videos</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="profile-tab" data-bs-toggle="tab" data-bs-target="#latest-podcasted" type="button" role="tab" aria-controls="profile" aria-selected="false">Podcasts</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="profile-tab" data-bs-toggle="tab" data-bs-target="#latest-sports" type="button" role="tab" aria-controls="profile" aria-selected="false">Sports</button>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@ -128,24 +196,74 @@
|
||||
<th scope="col">Time</th>
|
||||
<th scope="col">Title</th>
|
||||
<th scope="col">Series</th>
|
||||
<th scope="col">Source</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for scrobble in video_scrobble_list %}
|
||||
<tr>
|
||||
<td>{{scrobble.timestamp|naturaltime}}</td>
|
||||
<td>{% if scrobble.video.tv_series %}E{{scrobble.video.season_number}}S{{scrobble.video.season_number}} -{% endif %} {{scrobble.video.title}}</td>
|
||||
<td>{% if scrobble.video.tv_series %}S{{scrobble.video.season_number}}E{{scrobble.video.episode_number}} -{% endif %} {{scrobble.video.title}}</td>
|
||||
<td>{% if scrobble.video.tv_series %}{{scrobble.video.tv_series}}{% endif %}</td>
|
||||
<td>{{scrobble.source}}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tab-pane fade show" id="latest-sports" role="tabpanel" aria-labelledby="latest-sports-tab">
|
||||
<h2>Latest Sports</h2>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Date</th>
|
||||
<th scope="col">Title</th>
|
||||
<th scope="col">League</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for scrobble in sport_scrobble_list %}
|
||||
<tr>
|
||||
<td>{{scrobble.timestamp|naturaltime}}</td>
|
||||
<td>{{scrobble.sport_event.title}}</td>
|
||||
<td>{{scrobble.sport_event.league.abbreviation}}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tab-pane fade show" id="latest-podcasted" role="tabpanel" aria-labelledby="latest-podcasted-tab">
|
||||
<h2>Latest Podcasted</h2>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Date</th>
|
||||
<th scope="col">Title</th>
|
||||
<th scope="col">Podcast</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for scrobble in podcast_scrobble_list %}
|
||||
<tr>
|
||||
<td>{{scrobble.timestamp|naturaltime}}</td>
|
||||
<td>{{scrobble.podcast_episode.title}}</td>
|
||||
<td>{{scrobble.podcast_episode.podcast}}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
|
||||
{% endif %}
|
||||
</div>
|
||||
</main>
|
||||
{% endblock %}
|
||||
|
||||
@ -1,12 +1,28 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<main class="col-md-4 ms-sm-auto col-lg-10 px-md-4">
|
||||
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
|
||||
<h1 class="h2">Movies</h1>
|
||||
<div class="btn-toolbar mb-2 mb-md-0">
|
||||
<div class="btn-group me-2">
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary">Share</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary">Export</button>
|
||||
</div>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary dropdown-toggle">
|
||||
<span data-feather="calendar"></span>
|
||||
This week
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2>Movies</h2>
|
||||
<div class="container">
|
||||
|
||||
<ul>
|
||||
{% for movie in object_list %}
|
||||
<li>{{movie}}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</main>
|
||||
{% endblock %}
|
||||
|
||||
22
vrobbler/templates/videos/series_list.html
Normal file
22
vrobbler/templates/videos/series_list.html
Normal file
@ -0,0 +1,22 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<main class="col-md-4 ms-sm-auto col-lg-10 px-md-4">
|
||||
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
|
||||
<h1 class="h2">Series</h1>
|
||||
<div class="btn-toolbar mb-2 mb-md-0">
|
||||
<div class="btn-group me-2">
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary">Export</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<ul>
|
||||
{% for movie in object_list %}
|
||||
<li>{{movie}}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</main>
|
||||
{% endblock %}
|
||||
@ -1,12 +1,11 @@
|
||||
import scrobbles.views as scrobbles_views
|
||||
from django.conf import settings
|
||||
from django.conf.urls.static import static
|
||||
from django.contrib import admin
|
||||
from django.urls import include, path
|
||||
from rest_framework import routers
|
||||
from scrobbles.views import RecentScrobbleList
|
||||
from videos import urls as video_urls
|
||||
|
||||
from scrobbles import urls as scrobble_urls
|
||||
from videos import urls as video_urls
|
||||
|
||||
urlpatterns = [
|
||||
path("admin/", admin.site.urls),
|
||||
@ -15,8 +14,13 @@ urlpatterns = [
|
||||
# path("movies/", include(movies, namespace="movies")),
|
||||
# path("shows/", include(shows, namespace="shows")),
|
||||
path("api/v1/scrobbles/", include(scrobble_urls, namespace="scrobbles")),
|
||||
path(
|
||||
'manual/imdb/',
|
||||
scrobbles_views.ManualScrobbleView.as_view(),
|
||||
name='imdb-manual-scrobble',
|
||||
),
|
||||
path("", include(video_urls, namespace="videos")),
|
||||
path("", RecentScrobbleList.as_view(), name="home"),
|
||||
path("", scrobbles_views.RecentScrobbleList.as_view(), name="home"),
|
||||
]
|
||||
|
||||
if settings.DEBUG:
|
||||
|
||||
Reference in New Issue
Block a user