Compare commits
5 commits
5def0f3c61
...
9d4781165f
Author | SHA1 | Date | |
---|---|---|---|
9d4781165f | |||
2d279ba4b4 | |||
e2189f84f4 | |||
219032cb61 | |||
fdaca6b132 |
11
.drone.yml
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
kind: pipeline
|
||||||
|
name: default
|
||||||
|
type: docker
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Test
|
||||||
|
image: d21d3q/python-poetry:3.10
|
||||||
|
commands:
|
||||||
|
- poetry install
|
||||||
|
- poetry run invoke lint
|
||||||
|
- poetry run invoke test
|
|
@ -14,11 +14,9 @@ abrufen kann.
|
||||||
|
|
||||||
## Technik
|
## Technik
|
||||||
|
|
||||||
Der Server sollte mit dem Webframework [Flask](https://flask.palletsprojects.com/)
|
Der Server sollte mit dem Webframework [Django](https://djangoproject.com/)
|
||||||
realisiert werden.
|
realisiert werden.
|
||||||
|
|
||||||
Daten sollten entweder in einer SQLite-Datenbank oder in JSON-Dateien abgelegt werden.
|
|
||||||
|
|
||||||
Die Weboberfläche wird mit Jinja-Templates gerendert, auf ein JS-Framework kann vorerst verzichtet werden.
|
Die Weboberfläche wird mit Jinja-Templates gerendert, auf ein JS-Framework kann vorerst verzichtet werden.
|
||||||
Für ein ansehnliches Ansehen sorgt Bootstrap.
|
Für ein ansehnliches Ansehen sorgt Bootstrap.
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,14 @@
|
||||||
# Coverbilder
|
# Coverbilder
|
||||||
|
|
||||||
Podcast-Cover sind quadratisch.
|
Podcast-Cover sind quadratisch, während YT-Thumbnails das Seitenverhältnis
|
||||||
|
16:9 haben. Da Thumbnails häufig Textelemente beinhalten, ist es nicht
|
||||||
|
vorteilhaft, das Thumbnail einfach quadratisch zuzuschneiden.
|
||||||
|
|
||||||
|
Stattdessen sollte Ucast das Thumbnail nach oben und unten farblich
|
||||||
|
passend erweitern und den Videotitel und Kanalnamen einfügen.
|
||||||
|
|
||||||
|
![](../tests/testfiles/thumbnail/t2.webp)
|
||||||
|
![](../tests/testfiles/cover/c2.png)
|
||||||
|
|
||||||
- Durchschnittliche Farbe der oberen und unteren 20% des Bilds berechnen
|
- Durchschnittliche Farbe der oberen und unteren 20% des Bilds berechnen
|
||||||
- Farbverlauf zwischen diesen Farben als Hintergrund verwenden
|
- Farbverlauf zwischen diesen Farben als Hintergrund verwenden
|
||||||
|
|
|
@ -3,13 +3,9 @@
|
||||||
## Verzeichnisstruktur
|
## Verzeichnisstruktur
|
||||||
|
|
||||||
```txt
|
```txt
|
||||||
_ config
|
|
||||||
|_ config.toml
|
|
||||||
_ data
|
_ data
|
||||||
|_ LinusTechTips
|
|_ LinusTechTips
|
||||||
|_ .ucast
|
|_ .ucast
|
||||||
|_ videos.json # IDs und Metadaten aller heruntergeladenen Videos
|
|
||||||
|_ options.json # Kanalspezifische Optionen (ID, LastScan)
|
|
||||||
|_ avatar.png # Profilbild des Kanals
|
|_ avatar.png # Profilbild des Kanals
|
||||||
|_ feed.xml # RSS-Feed
|
|_ feed.xml # RSS-Feed
|
||||||
|_ covers # Cover-Bilder
|
|_ covers # Cover-Bilder
|
||||||
|
@ -30,28 +26,26 @@ _ data
|
||||||
|
|
||||||
### ChannelOptions
|
### ChannelOptions
|
||||||
|
|
||||||
- ID: str
|
- ID: `str, max_length=30`
|
||||||
- Active: bool = True
|
- Active: `bool = True`
|
||||||
- LastScan: datetime
|
- LastScan: `datetime`
|
||||||
- SkipLivestreams: bool = True
|
- SkipLivestreams: `bool = True`
|
||||||
- SkipShorts: bool = True
|
- SkipShorts: `bool = True`
|
||||||
- KeepVideos: int = -1
|
- KeepVideos: `int, nullable`
|
||||||
|
- Videos: `-> Video (1->n)`
|
||||||
|
|
||||||
### Videos
|
|
||||||
|
|
||||||
- Videos: dict[id: str -> Video]
|
|
||||||
|
|
||||||
### Video
|
### Video
|
||||||
|
|
||||||
- ID: str
|
- ID: `str, max_length=30`
|
||||||
- Title: str
|
- Title: `str, max_length=200`
|
||||||
- Slug: str (YYMMDD_Title, used as filename)
|
- Slug: `str, max_length=209` (YYYYMMDD_Title, used as filename)
|
||||||
- Published: datetime
|
- Published: `datetime`
|
||||||
- Description: str
|
- Downloaded: `datetime, nullable`
|
||||||
|
- Description: `text`
|
||||||
|
|
||||||
### Config
|
### Config
|
||||||
|
|
||||||
- RedisURL: str
|
- RedisURL: str
|
||||||
- ScanInterval: 1h
|
- ScanInterval: 1h
|
||||||
- DefaultChannelOptions: ChannelOptions
|
|
||||||
- AppriseUrl: str (für Benachrichtigungen, https://github.com/caronc/apprise/wiki)
|
- AppriseUrl: str (für Benachrichtigungen, https://github.com/caronc/apprise/wiki)
|
||||||
|
|
209
poetry.lock
generated
|
@ -31,6 +31,17 @@ docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"]
|
||||||
tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "cloudpickle"]
|
tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "cloudpickle"]
|
||||||
tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "cloudpickle"]
|
tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "cloudpickle"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bordercrop"
|
||||||
|
version = "1.0.0"
|
||||||
|
description = "A black borders cropping module"
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.2, <4"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
Pillow = "*"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "brotli"
|
name = "brotli"
|
||||||
version = "1.0.9"
|
version = "1.0.9"
|
||||||
|
@ -69,6 +80,14 @@ python-versions = "*"
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
pycparser = "*"
|
pycparser = "*"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cfgv"
|
||||||
|
version = "3.3.1"
|
||||||
|
description = "Validate configuration and produce human readable error messages."
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.6.1"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "charset-normalizer"
|
name = "charset-normalizer"
|
||||||
version = "2.0.12"
|
version = "2.0.12"
|
||||||
|
@ -113,6 +132,14 @@ tomli = {version = "*", optional = true, markers = "extra == \"toml\""}
|
||||||
[package.extras]
|
[package.extras]
|
||||||
toml = ["tomli"]
|
toml = ["tomli"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "distlib"
|
||||||
|
version = "0.3.4"
|
||||||
|
description = "Distribution utilities"
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = "*"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "django"
|
name = "django"
|
||||||
version = "4.0.4"
|
version = "4.0.4"
|
||||||
|
@ -141,6 +168,18 @@ python-versions = ">=3.6"
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
sgmllib3k = "*"
|
sgmllib3k = "*"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "filelock"
|
||||||
|
version = "3.6.0"
|
||||||
|
description = "A platform independent file lock."
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
docs = ["furo (>=2021.8.17b43)", "sphinx (>=4.1)", "sphinx-autodoc-typehints (>=1.12)"]
|
||||||
|
testing = ["covdefaults (>=1.2.0)", "coverage (>=4)", "pytest (>=4)", "pytest-cov", "pytest-timeout (>=1.4.2)"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "font-source-sans-pro"
|
name = "font-source-sans-pro"
|
||||||
version = "0.0.1"
|
version = "0.0.1"
|
||||||
|
@ -157,6 +196,17 @@ category = "main"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = "*"
|
python-versions = "*"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "identify"
|
||||||
|
version = "2.5.0"
|
||||||
|
description = "File identification library for Python"
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
license = ["ukkonen"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "idna"
|
name = "idna"
|
||||||
version = "3.3"
|
version = "3.3"
|
||||||
|
@ -189,6 +239,14 @@ category = "main"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.5, <4"
|
python-versions = ">=3.5, <4"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "nodeenv"
|
||||||
|
version = "1.6.0"
|
||||||
|
description = "Node.js virtual environment builder"
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = "*"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "packaging"
|
name = "packaging"
|
||||||
version = "21.3"
|
version = "21.3"
|
||||||
|
@ -212,6 +270,18 @@ python-versions = ">=3.7"
|
||||||
docs = ["olefile", "sphinx (>=2.4)", "sphinx-copybutton", "sphinx-issues (>=3.0.1)", "sphinx-removed-in", "sphinx-rtd-theme (>=1.0)", "sphinxext-opengraph"]
|
docs = ["olefile", "sphinx (>=2.4)", "sphinx-copybutton", "sphinx-issues (>=3.0.1)", "sphinx-removed-in", "sphinx-rtd-theme (>=1.0)", "sphinxext-opengraph"]
|
||||||
tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"]
|
tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "platformdirs"
|
||||||
|
version = "2.5.2"
|
||||||
|
description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
docs = ["furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)", "sphinx (>=4)"]
|
||||||
|
test = ["appdirs (==1.4.4)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)", "pytest (>=6)"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pluggy"
|
name = "pluggy"
|
||||||
version = "1.0.0"
|
version = "1.0.0"
|
||||||
|
@ -224,6 +294,22 @@ python-versions = ">=3.6"
|
||||||
dev = ["pre-commit", "tox"]
|
dev = ["pre-commit", "tox"]
|
||||||
testing = ["pytest", "pytest-benchmark"]
|
testing = ["pytest", "pytest-benchmark"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pre-commit"
|
||||||
|
version = "2.19.0"
|
||||||
|
description = "A framework for managing and maintaining multi-language pre-commit hooks."
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
cfgv = ">=2.0.0"
|
||||||
|
identify = ">=1.0.0"
|
||||||
|
nodeenv = ">=0.11.1"
|
||||||
|
pyyaml = ">=5.1"
|
||||||
|
toml = "*"
|
||||||
|
virtualenv = ">=20.0.8"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "py"
|
name = "py"
|
||||||
version = "1.11.0"
|
version = "1.11.0"
|
||||||
|
@ -310,6 +396,14 @@ pytest = ">=5.4.0"
|
||||||
docs = ["sphinx", "sphinx-rtd-theme"]
|
docs = ["sphinx", "sphinx-rtd-theme"]
|
||||||
testing = ["django", "django-configurations (>=2.0)"]
|
testing = ["django", "django-configurations (>=2.0)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pyyaml"
|
||||||
|
version = "6.0"
|
||||||
|
description = "YAML parser and emitter for Python"
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.6"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "requests"
|
name = "requests"
|
||||||
version = "2.27.1"
|
version = "2.27.1"
|
||||||
|
@ -356,6 +450,14 @@ category = "main"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = "*"
|
python-versions = "*"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "six"
|
||||||
|
version = "1.16.0"
|
||||||
|
description = "Python 2 and 3 compatibility utilities"
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sqlparse"
|
name = "sqlparse"
|
||||||
version = "0.4.2"
|
version = "0.4.2"
|
||||||
|
@ -364,6 +466,14 @@ category = "main"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.5"
|
python-versions = ">=3.5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml"
|
||||||
|
version = "0.10.2"
|
||||||
|
description = "Python Library for Tom's Obvious, Minimal Language"
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tomli"
|
name = "tomli"
|
||||||
version = "2.0.1"
|
version = "2.0.1"
|
||||||
|
@ -401,6 +511,24 @@ brotli = ["brotlicffi (>=0.8.0)", "brotli (>=1.0.9)", "brotlipy (>=0.6.0)"]
|
||||||
secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"]
|
secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"]
|
||||||
socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"]
|
socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "virtualenv"
|
||||||
|
version = "20.14.1"
|
||||||
|
description = "Virtual Python Environment builder"
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
distlib = ">=0.3.1,<1"
|
||||||
|
filelock = ">=3.2,<4"
|
||||||
|
platformdirs = ">=2,<3"
|
||||||
|
six = ">=1.9.0,<2"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
docs = ["proselint (>=0.10.2)", "sphinx (>=3)", "sphinx-argparse (>=0.2.5)", "sphinx-rtd-theme (>=0.4.3)", "towncrier (>=21.3)"]
|
||||||
|
testing = ["coverage (>=4)", "coverage-enable-subprocess (>=1)", "flaky (>=3)", "pytest (>=4)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.1)", "pytest-mock (>=2)", "pytest-randomly (>=1)", "pytest-timeout (>=1)", "packaging (>=20.0)"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wcag-contrast-ratio"
|
name = "wcag-contrast-ratio"
|
||||||
version = "0.9"
|
version = "0.9"
|
||||||
|
@ -436,7 +564,7 @@ websockets = "*"
|
||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "1.1"
|
lock-version = "1.1"
|
||||||
python-versions = "^3.10"
|
python-versions = "^3.10"
|
||||||
content-hash = "0288eb9eca14b78ee0cfac28cef0dcf17701165b6aa778255bfa73b94d5b2c4f"
|
content-hash = "99e2a5970962f1e936da2010b8ec997026f5afe4762af4345da287125e6b7771"
|
||||||
|
|
||||||
[metadata.files]
|
[metadata.files]
|
||||||
asgiref = [
|
asgiref = [
|
||||||
|
@ -451,6 +579,10 @@ attrs = [
|
||||||
{file = "attrs-21.4.0-py2.py3-none-any.whl", hash = "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4"},
|
{file = "attrs-21.4.0-py2.py3-none-any.whl", hash = "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4"},
|
||||||
{file = "attrs-21.4.0.tar.gz", hash = "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd"},
|
{file = "attrs-21.4.0.tar.gz", hash = "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd"},
|
||||||
]
|
]
|
||||||
|
bordercrop = [
|
||||||
|
{file = "bordercrop-1.0.0-py3-none-any.whl", hash = "sha256:50342a4a7d3b37bd1188faf3bedcb4d4b264c3d7cc51a59d082d3afeaab86c0f"},
|
||||||
|
{file = "bordercrop-1.0.0.tar.gz", hash = "sha256:2cfd078f8214fcecc304ee9bc8e96b38c9decc3db96ee5301e31e60678322990"},
|
||||||
|
]
|
||||||
brotli = [
|
brotli = [
|
||||||
{file = "Brotli-1.0.9-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:268fe94547ba25b58ebc724680609c8ee3e5a843202e9a381f6f9c5e8bdb5c70"},
|
{file = "Brotli-1.0.9-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:268fe94547ba25b58ebc724680609c8ee3e5a843202e9a381f6f9c5e8bdb5c70"},
|
||||||
{file = "Brotli-1.0.9-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:c2415d9d082152460f2bd4e382a1e85aed233abc92db5a3880da2257dc7daf7b"},
|
{file = "Brotli-1.0.9-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:c2415d9d082152460f2bd4e382a1e85aed233abc92db5a3880da2257dc7daf7b"},
|
||||||
|
@ -603,6 +735,10 @@ cffi = [
|
||||||
{file = "cffi-1.15.0-cp39-cp39-win_amd64.whl", hash = "sha256:3773c4d81e6e818df2efbc7dd77325ca0dcb688116050fb2b3011218eda36139"},
|
{file = "cffi-1.15.0-cp39-cp39-win_amd64.whl", hash = "sha256:3773c4d81e6e818df2efbc7dd77325ca0dcb688116050fb2b3011218eda36139"},
|
||||||
{file = "cffi-1.15.0.tar.gz", hash = "sha256:920f0d66a896c2d99f0adbb391f990a84091179542c205fa53ce5787aff87954"},
|
{file = "cffi-1.15.0.tar.gz", hash = "sha256:920f0d66a896c2d99f0adbb391f990a84091179542c205fa53ce5787aff87954"},
|
||||||
]
|
]
|
||||||
|
cfgv = [
|
||||||
|
{file = "cfgv-3.3.1-py2.py3-none-any.whl", hash = "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426"},
|
||||||
|
{file = "cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736"},
|
||||||
|
]
|
||||||
charset-normalizer = [
|
charset-normalizer = [
|
||||||
{file = "charset-normalizer-2.0.12.tar.gz", hash = "sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597"},
|
{file = "charset-normalizer-2.0.12.tar.gz", hash = "sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597"},
|
||||||
{file = "charset_normalizer-2.0.12-py3-none-any.whl", hash = "sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df"},
|
{file = "charset_normalizer-2.0.12-py3-none-any.whl", hash = "sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df"},
|
||||||
|
@ -658,6 +794,10 @@ coverage = [
|
||||||
{file = "coverage-6.3.2-pp36.pp37.pp38-none-any.whl", hash = "sha256:18d520c6860515a771708937d2f78f63cc47ab3b80cb78e86573b0a760161faf"},
|
{file = "coverage-6.3.2-pp36.pp37.pp38-none-any.whl", hash = "sha256:18d520c6860515a771708937d2f78f63cc47ab3b80cb78e86573b0a760161faf"},
|
||||||
{file = "coverage-6.3.2.tar.gz", hash = "sha256:03e2a7826086b91ef345ff18742ee9fc47a6839ccd517061ef8fa1976e652ce9"},
|
{file = "coverage-6.3.2.tar.gz", hash = "sha256:03e2a7826086b91ef345ff18742ee9fc47a6839ccd517061ef8fa1976e652ce9"},
|
||||||
]
|
]
|
||||||
|
distlib = [
|
||||||
|
{file = "distlib-0.3.4-py2.py3-none-any.whl", hash = "sha256:6564fe0a8f51e734df6333d08b8b94d4ea8ee6b99b5ed50613f731fd4089f34b"},
|
||||||
|
{file = "distlib-0.3.4.zip", hash = "sha256:e4b58818180336dc9c529bfb9a0b58728ffc09ad92027a3f30b7cd91e3458579"},
|
||||||
|
]
|
||||||
django = [
|
django = [
|
||||||
{file = "Django-4.0.4-py3-none-any.whl", hash = "sha256:07c8638e7a7f548dc0acaaa7825d84b7bd42b10e8d22268b3d572946f1e9b687"},
|
{file = "Django-4.0.4-py3-none-any.whl", hash = "sha256:07c8638e7a7f548dc0acaaa7825d84b7bd42b10e8d22268b3d572946f1e9b687"},
|
||||||
{file = "Django-4.0.4.tar.gz", hash = "sha256:4e8177858524417563cc0430f29ea249946d831eacb0068a1455686587df40b5"},
|
{file = "Django-4.0.4.tar.gz", hash = "sha256:4e8177858524417563cc0430f29ea249946d831eacb0068a1455686587df40b5"},
|
||||||
|
@ -666,6 +806,10 @@ feedparser = [
|
||||||
{file = "feedparser-6.0.8-py3-none-any.whl", hash = "sha256:1b7f57841d9cf85074deb316ed2c795091a238adb79846bc46dccdaf80f9c59a"},
|
{file = "feedparser-6.0.8-py3-none-any.whl", hash = "sha256:1b7f57841d9cf85074deb316ed2c795091a238adb79846bc46dccdaf80f9c59a"},
|
||||||
{file = "feedparser-6.0.8.tar.gz", hash = "sha256:5ce0410a05ab248c8c7cfca3a0ea2203968ee9ff4486067379af4827a59f9661"},
|
{file = "feedparser-6.0.8.tar.gz", hash = "sha256:5ce0410a05ab248c8c7cfca3a0ea2203968ee9ff4486067379af4827a59f9661"},
|
||||||
]
|
]
|
||||||
|
filelock = [
|
||||||
|
{file = "filelock-3.6.0-py3-none-any.whl", hash = "sha256:f8314284bfffbdcfa0ff3d7992b023d4c628ced6feb957351d4c48d059f56bc0"},
|
||||||
|
{file = "filelock-3.6.0.tar.gz", hash = "sha256:9cd540a9352e432c7246a48fe4e8712b10acb1df2ad1f30e8c070b82ae1fed85"},
|
||||||
|
]
|
||||||
font-source-sans-pro = [
|
font-source-sans-pro = [
|
||||||
{file = "font-source-sans-pro-0.0.1.tar.gz", hash = "sha256:3f81d8e52b0d7e930e2c867c0d3ee549312d03f97b71b664a8361006311f72e5"},
|
{file = "font-source-sans-pro-0.0.1.tar.gz", hash = "sha256:3f81d8e52b0d7e930e2c867c0d3ee549312d03f97b71b664a8361006311f72e5"},
|
||||||
{file = "font_source_sans_pro-0.0.1-py2-none-any.whl", hash = "sha256:685c8813d59941e84ea326f46d638871adbc825a0aa5205a72ee9ed9c5fbb471"},
|
{file = "font_source_sans_pro-0.0.1-py2-none-any.whl", hash = "sha256:685c8813d59941e84ea326f46d638871adbc825a0aa5205a72ee9ed9c5fbb471"},
|
||||||
|
@ -676,6 +820,10 @@ fonts = [
|
||||||
{file = "fonts-0.0.3-py3-none-any.whl", hash = "sha256:e5f551379088ab260c2537980c3ccdff8af93408d9d4fa3319388d2ee25b7b6d"},
|
{file = "fonts-0.0.3-py3-none-any.whl", hash = "sha256:e5f551379088ab260c2537980c3ccdff8af93408d9d4fa3319388d2ee25b7b6d"},
|
||||||
{file = "fonts-0.0.3.tar.gz", hash = "sha256:c626655b75a60715e118e44e270656fd22fd8f54252901ff6ebf1308ad01c405"},
|
{file = "fonts-0.0.3.tar.gz", hash = "sha256:c626655b75a60715e118e44e270656fd22fd8f54252901ff6ebf1308ad01c405"},
|
||||||
]
|
]
|
||||||
|
identify = [
|
||||||
|
{file = "identify-2.5.0-py2.py3-none-any.whl", hash = "sha256:3acfe15a96e4272b4ec5662ee3e231ceba976ef63fd9980ed2ce9cc415df393f"},
|
||||||
|
{file = "identify-2.5.0.tar.gz", hash = "sha256:c83af514ea50bf2be2c4a3f2fb349442b59dc87284558ae9ff54191bff3541d2"},
|
||||||
|
]
|
||||||
idna = [
|
idna = [
|
||||||
{file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"},
|
{file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"},
|
||||||
{file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"},
|
{file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"},
|
||||||
|
@ -692,6 +840,10 @@ mutagen = [
|
||||||
{file = "mutagen-1.45.1-py3-none-any.whl", hash = "sha256:9c9f243fcec7f410f138cb12c21c84c64fde4195481a30c9bfb05b5f003adfed"},
|
{file = "mutagen-1.45.1-py3-none-any.whl", hash = "sha256:9c9f243fcec7f410f138cb12c21c84c64fde4195481a30c9bfb05b5f003adfed"},
|
||||||
{file = "mutagen-1.45.1.tar.gz", hash = "sha256:6397602efb3c2d7baebd2166ed85731ae1c1d475abca22090b7141ff5034b3e1"},
|
{file = "mutagen-1.45.1.tar.gz", hash = "sha256:6397602efb3c2d7baebd2166ed85731ae1c1d475abca22090b7141ff5034b3e1"},
|
||||||
]
|
]
|
||||||
|
nodeenv = [
|
||||||
|
{file = "nodeenv-1.6.0-py2.py3-none-any.whl", hash = "sha256:621e6b7076565ddcacd2db0294c0381e01fd28945ab36bcf00f41c5daf63bef7"},
|
||||||
|
{file = "nodeenv-1.6.0.tar.gz", hash = "sha256:3ef13ff90291ba2a4a7a4ff9a979b63ffdd00a464dbe04acf0ea6471517a4c2b"},
|
||||||
|
]
|
||||||
packaging = [
|
packaging = [
|
||||||
{file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"},
|
{file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"},
|
||||||
{file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"},
|
{file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"},
|
||||||
|
@ -736,10 +888,18 @@ pillow = [
|
||||||
{file = "Pillow-9.1.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:8d79c6f468215d1a8415aa53d9868a6b40c4682165b8cb62a221b1baa47db458"},
|
{file = "Pillow-9.1.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:8d79c6f468215d1a8415aa53d9868a6b40c4682165b8cb62a221b1baa47db458"},
|
||||||
{file = "Pillow-9.1.0.tar.gz", hash = "sha256:f401ed2bbb155e1ade150ccc63db1a4f6c1909d3d378f7d1235a44e90d75fb97"},
|
{file = "Pillow-9.1.0.tar.gz", hash = "sha256:f401ed2bbb155e1ade150ccc63db1a4f6c1909d3d378f7d1235a44e90d75fb97"},
|
||||||
]
|
]
|
||||||
|
platformdirs = [
|
||||||
|
{file = "platformdirs-2.5.2-py3-none-any.whl", hash = "sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788"},
|
||||||
|
{file = "platformdirs-2.5.2.tar.gz", hash = "sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19"},
|
||||||
|
]
|
||||||
pluggy = [
|
pluggy = [
|
||||||
{file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"},
|
{file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"},
|
||||||
{file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"},
|
{file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"},
|
||||||
]
|
]
|
||||||
|
pre-commit = [
|
||||||
|
{file = "pre_commit-2.19.0-py2.py3-none-any.whl", hash = "sha256:10c62741aa5704faea2ad69cb550ca78082efe5697d6f04e5710c3c229afdd10"},
|
||||||
|
{file = "pre_commit-2.19.0.tar.gz", hash = "sha256:4233a1e38621c87d9dda9808c6606d7e7ba0e087cd56d3fe03202a01d2919615"},
|
||||||
|
]
|
||||||
py = [
|
py = [
|
||||||
{file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"},
|
{file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"},
|
||||||
{file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"},
|
{file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"},
|
||||||
|
@ -793,6 +953,41 @@ pytest-django = [
|
||||||
{file = "pytest-django-4.5.2.tar.gz", hash = "sha256:d9076f759bb7c36939dbdd5ae6633c18edfc2902d1a69fdbefd2426b970ce6c2"},
|
{file = "pytest-django-4.5.2.tar.gz", hash = "sha256:d9076f759bb7c36939dbdd5ae6633c18edfc2902d1a69fdbefd2426b970ce6c2"},
|
||||||
{file = "pytest_django-4.5.2-py3-none-any.whl", hash = "sha256:c60834861933773109334fe5a53e83d1ef4828f2203a1d6a0fa9972f4f75ab3e"},
|
{file = "pytest_django-4.5.2-py3-none-any.whl", hash = "sha256:c60834861933773109334fe5a53e83d1ef4828f2203a1d6a0fa9972f4f75ab3e"},
|
||||||
]
|
]
|
||||||
|
pyyaml = [
|
||||||
|
{file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"},
|
||||||
|
{file = "PyYAML-6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c"},
|
||||||
|
{file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc"},
|
||||||
|
{file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b"},
|
||||||
|
{file = "PyYAML-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5"},
|
||||||
|
{file = "PyYAML-6.0-cp310-cp310-win32.whl", hash = "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513"},
|
||||||
|
{file = "PyYAML-6.0-cp310-cp310-win_amd64.whl", hash = "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a"},
|
||||||
|
{file = "PyYAML-6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86"},
|
||||||
|
{file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f"},
|
||||||
|
{file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92"},
|
||||||
|
{file = "PyYAML-6.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4"},
|
||||||
|
{file = "PyYAML-6.0-cp36-cp36m-win32.whl", hash = "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293"},
|
||||||
|
{file = "PyYAML-6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57"},
|
||||||
|
{file = "PyYAML-6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c"},
|
||||||
|
{file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0"},
|
||||||
|
{file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4"},
|
||||||
|
{file = "PyYAML-6.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9"},
|
||||||
|
{file = "PyYAML-6.0-cp37-cp37m-win32.whl", hash = "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737"},
|
||||||
|
{file = "PyYAML-6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d"},
|
||||||
|
{file = "PyYAML-6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b"},
|
||||||
|
{file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba"},
|
||||||
|
{file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34"},
|
||||||
|
{file = "PyYAML-6.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287"},
|
||||||
|
{file = "PyYAML-6.0-cp38-cp38-win32.whl", hash = "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78"},
|
||||||
|
{file = "PyYAML-6.0-cp38-cp38-win_amd64.whl", hash = "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07"},
|
||||||
|
{file = "PyYAML-6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b"},
|
||||||
|
{file = "PyYAML-6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174"},
|
||||||
|
{file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803"},
|
||||||
|
{file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3"},
|
||||||
|
{file = "PyYAML-6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0"},
|
||||||
|
{file = "PyYAML-6.0-cp39-cp39-win32.whl", hash = "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb"},
|
||||||
|
{file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"},
|
||||||
|
{file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"},
|
||||||
|
]
|
||||||
requests = [
|
requests = [
|
||||||
{file = "requests-2.27.1-py2.py3-none-any.whl", hash = "sha256:f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d"},
|
{file = "requests-2.27.1-py2.py3-none-any.whl", hash = "sha256:f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d"},
|
||||||
{file = "requests-2.27.1.tar.gz", hash = "sha256:68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61"},
|
{file = "requests-2.27.1.tar.gz", hash = "sha256:68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61"},
|
||||||
|
@ -806,10 +1001,18 @@ scrapetube = [
|
||||||
sgmllib3k = [
|
sgmllib3k = [
|
||||||
{file = "sgmllib3k-1.0.0.tar.gz", hash = "sha256:7868fb1c8bfa764c1ac563d3cf369c381d1325d36124933a726f29fcdaa812e9"},
|
{file = "sgmllib3k-1.0.0.tar.gz", hash = "sha256:7868fb1c8bfa764c1ac563d3cf369c381d1325d36124933a726f29fcdaa812e9"},
|
||||||
]
|
]
|
||||||
|
six = [
|
||||||
|
{file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
|
||||||
|
{file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
|
||||||
|
]
|
||||||
sqlparse = [
|
sqlparse = [
|
||||||
{file = "sqlparse-0.4.2-py3-none-any.whl", hash = "sha256:48719e356bb8b42991bdbb1e8b83223757b93789c00910a616a071910ca4a64d"},
|
{file = "sqlparse-0.4.2-py3-none-any.whl", hash = "sha256:48719e356bb8b42991bdbb1e8b83223757b93789c00910a616a071910ca4a64d"},
|
||||||
{file = "sqlparse-0.4.2.tar.gz", hash = "sha256:0c00730c74263a94e5a9919ade150dfc3b19c574389985446148402998287dae"},
|
{file = "sqlparse-0.4.2.tar.gz", hash = "sha256:0c00730c74263a94e5a9919ade150dfc3b19c574389985446148402998287dae"},
|
||||||
]
|
]
|
||||||
|
toml = [
|
||||||
|
{file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"},
|
||||||
|
{file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"},
|
||||||
|
]
|
||||||
tomli = [
|
tomli = [
|
||||||
{file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"},
|
{file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"},
|
||||||
{file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
|
{file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
|
||||||
|
@ -826,6 +1029,10 @@ urllib3 = [
|
||||||
{file = "urllib3-1.26.9-py2.py3-none-any.whl", hash = "sha256:44ece4d53fb1706f667c9bd1c648f5469a2ec925fcf3a776667042d645472c14"},
|
{file = "urllib3-1.26.9-py2.py3-none-any.whl", hash = "sha256:44ece4d53fb1706f667c9bd1c648f5469a2ec925fcf3a776667042d645472c14"},
|
||||||
{file = "urllib3-1.26.9.tar.gz", hash = "sha256:aabaf16477806a5e1dd19aa41f8c2b7950dd3c746362d7e3223dbe6de6ac448e"},
|
{file = "urllib3-1.26.9.tar.gz", hash = "sha256:aabaf16477806a5e1dd19aa41f8c2b7950dd3c746362d7e3223dbe6de6ac448e"},
|
||||||
]
|
]
|
||||||
|
virtualenv = [
|
||||||
|
{file = "virtualenv-20.14.1-py2.py3-none-any.whl", hash = "sha256:e617f16e25b42eb4f6e74096b9c9e37713cf10bf30168fb4a739f3fa8f898a3a"},
|
||||||
|
{file = "virtualenv-20.14.1.tar.gz", hash = "sha256:ef589a79795589aada0c1c5b319486797c03b67ac3984c48c669c0e4f50df3a5"},
|
||||||
|
]
|
||||||
wcag-contrast-ratio = [
|
wcag-contrast-ratio = [
|
||||||
{file = "wcag-contrast-ratio-0.9.tar.gz", hash = "sha256:69192b8e5c0a7d0dc5ff1187eeb3e398141633a4bde51c69c87f58fe87ed361c"},
|
{file = "wcag-contrast-ratio-0.9.tar.gz", hash = "sha256:69192b8e5c0a7d0dc5ff1187eeb3e398141633a4bde51c69c87f58fe87ed361c"},
|
||||||
]
|
]
|
||||||
|
|
|
@ -20,12 +20,14 @@ colorthief = "^0.2.1"
|
||||||
wcag-contrast-ratio = "^0.9"
|
wcag-contrast-ratio = "^0.9"
|
||||||
font-source-sans-pro = "^0.0.1"
|
font-source-sans-pro = "^0.0.1"
|
||||||
fonts = "^0.0.3"
|
fonts = "^0.0.3"
|
||||||
|
bordercrop = "^1.0.0"
|
||||||
|
|
||||||
[tool.poetry.dev-dependencies]
|
[tool.poetry.dev-dependencies]
|
||||||
pytest = "^7.1.1"
|
pytest = "^7.1.1"
|
||||||
pytest-cov = "^3.0.0"
|
pytest-cov = "^3.0.0"
|
||||||
invoke = "^1.7.0"
|
invoke = "^1.7.0"
|
||||||
pytest-django = "^4.5.2"
|
pytest-django = "^4.5.2"
|
||||||
|
pre-commit = "^2.19.0"
|
||||||
|
|
||||||
[tool.poetry.scripts]
|
[tool.poetry.scripts]
|
||||||
"ucast-manage" = "ucast.manage:main"
|
"ucast-manage" = "ucast.manage:main"
|
||||||
|
|
27
tasks.py
|
@ -11,7 +11,17 @@ os.chdir(Path(__file__).absolute().parent)
|
||||||
|
|
||||||
@task
|
@task
|
||||||
def test(c):
|
def test(c):
|
||||||
c.run("pytest tests", pty=True)
|
c.run("pytest", pty=True)
|
||||||
|
|
||||||
|
|
||||||
|
@task
|
||||||
|
def lint(c):
|
||||||
|
c.run("pre-commit run -a", pty=True)
|
||||||
|
|
||||||
|
|
||||||
|
@task
|
||||||
|
def format(c):
|
||||||
|
c.run("pre-commit run black -a", pty=True)
|
||||||
|
|
||||||
|
|
||||||
@task
|
@task
|
||||||
|
@ -24,19 +34,24 @@ def get_cover(c, vid=""):
|
||||||
vinfo = youtube.get_video_info(vid)
|
vinfo = youtube.get_video_info(vid)
|
||||||
title = vinfo["fulltitle"]
|
title = vinfo["fulltitle"]
|
||||||
channel_name = vinfo["uploader"]
|
channel_name = vinfo["uploader"]
|
||||||
thumbnail_url = youtube.get_thumbnail_url(vinfo)
|
|
||||||
channel_url = vinfo["channel_url"]
|
channel_url = vinfo["channel_url"]
|
||||||
channel_metadata = youtube.get_channel_metadata(channel_url)
|
channel_metadata = youtube.get_channel_metadata(channel_url)
|
||||||
|
|
||||||
ti = 1
|
ti = 1
|
||||||
while os.path.exists(tests.DIR_TESTFILES / "cover" / f"c{ti}.png"):
|
while os.path.exists(tests.DIR_TESTFILES / "cover" / f"c{ti}_classic.png"):
|
||||||
ti += 1
|
ti += 1
|
||||||
|
|
||||||
tn_file = tests.DIR_TESTFILES / "thumbnail" / f"t{ti}.webp"
|
tn_file = tests.DIR_TESTFILES / "thumbnail" / f"t{ti}.webp"
|
||||||
av_file = tests.DIR_TESTFILES / "avatar" / f"a{ti}.jpg"
|
av_file = tests.DIR_TESTFILES / "avatar" / f"a{ti}.jpg"
|
||||||
cv_file = tests.DIR_TESTFILES / "cover" / f"c{ti}.png"
|
cv_file = tests.DIR_TESTFILES / "cover" / f"c{ti}_classic.png"
|
||||||
|
cv_blur_file = tests.DIR_TESTFILES / "cover" / f"c{ti}_blur.png"
|
||||||
|
|
||||||
util.download_file(thumbnail_url, tn_file)
|
youtube.download_thumbnail(vinfo, tn_file)
|
||||||
util.download_file(channel_metadata.avatar_url, av_file)
|
util.download_file(channel_metadata.avatar_url, av_file)
|
||||||
|
|
||||||
cover.create_cover_file(tn_file, av_file, title, channel_name, cv_file)
|
cover.create_cover_file(
|
||||||
|
tn_file, av_file, title, channel_name, cover.CoverStyle.CLASSIC, cv_file
|
||||||
|
)
|
||||||
|
cover.create_cover_file(
|
||||||
|
tn_file, av_file, title, channel_name, cover.CoverStyle.BLUR, cv_blur_file
|
||||||
|
)
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
from django.contrib import admin
|
from django.contrib import admin # noqa: F401
|
||||||
|
|
||||||
# Register your models here.
|
# Register your models here.
|
||||||
|
|
|
@ -1,12 +1,14 @@
|
||||||
# coding=utf-8
|
# coding=utf-8
|
||||||
|
import enum
|
||||||
import math
|
import math
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import List, Optional, Tuple
|
from typing import List, Optional, Tuple
|
||||||
|
|
||||||
import wcag_contrast_ratio
|
import wcag_contrast_ratio
|
||||||
|
from bordercrop import bordercrop
|
||||||
from colorthief import ColorThief
|
from colorthief import ColorThief
|
||||||
from fonts.ttf import SourceSansPro
|
from fonts.ttf import SourceSansPro
|
||||||
from PIL import Image, ImageDraw, ImageFont
|
from PIL import Image, ImageDraw, ImageFilter, ImageFont
|
||||||
|
|
||||||
from yt2podcast.service import typ
|
from yt2podcast.service import typ
|
||||||
|
|
||||||
|
@ -14,6 +16,11 @@ CHAR_ELLIPSIS = "…"
|
||||||
COVER_WIDTH = 500
|
COVER_WIDTH = 500
|
||||||
|
|
||||||
|
|
||||||
|
class CoverStyle(enum.Enum):
|
||||||
|
CLASSIC = enum.auto()
|
||||||
|
BLUR = enum.auto()
|
||||||
|
|
||||||
|
|
||||||
def _split_text(
|
def _split_text(
|
||||||
height: int, width: int, text: str, font: ImageFont.FreeTypeFont, line_spacing=0
|
height: int, width: int, text: str, font: ImageFont.FreeTypeFont, line_spacing=0
|
||||||
) -> List[str]:
|
) -> List[str]:
|
||||||
|
@ -110,20 +117,22 @@ def _get_text_color(bg_color) -> typ.Color:
|
||||||
return 0, 0, 0
|
return 0, 0, 0
|
||||||
|
|
||||||
|
|
||||||
def _create_cover_image(
|
def _get_baseimage(
|
||||||
thumbnail: Image.Image, avatar: Optional[Image.Image], title: str, channel: str
|
thumbnail: Image.Image,
|
||||||
) -> Image.Image:
|
top_color: typ.Color,
|
||||||
# Scale the thumbnail image down to cover size
|
bottom_color: typ.Color,
|
||||||
tn_height = int(COVER_WIDTH / thumbnail.width * thumbnail.height)
|
style: CoverStyle,
|
||||||
tn = thumbnail.resize((COVER_WIDTH, tn_height), Image.Resampling.LANCZOS)
|
):
|
||||||
|
if style == CoverStyle.BLUR:
|
||||||
# Get dominant colors from the top and bottom 20% of the thumbnail image
|
ctn_width = int(COVER_WIDTH / thumbnail.height * thumbnail.width)
|
||||||
top_part = tn.crop((0, 0, COVER_WIDTH, int(tn_height * 0.2)))
|
ctn_l = int((ctn_width - COVER_WIDTH) / 2)
|
||||||
bottom_part = tn.crop((0, int(tn_height * 0.8), COVER_WIDTH, tn_height))
|
ctn_r = ctn_width - ctn_l
|
||||||
top_color = _get_dominant_color(top_part)
|
cover = (
|
||||||
bottom_color = _get_dominant_color(bottom_part)
|
thumbnail.resize((ctn_width, COVER_WIDTH), Image.Resampling.LANCZOS)
|
||||||
|
.crop((ctn_l, 0, ctn_r, COVER_WIDTH))
|
||||||
# Create new cover image
|
.filter(ImageFilter.GaussianBlur(20))
|
||||||
|
)
|
||||||
|
else:
|
||||||
cover = Image.new("RGB", (COVER_WIDTH, COVER_WIDTH))
|
cover = Image.new("RGB", (COVER_WIDTH, COVER_WIDTH))
|
||||||
cover_draw = ImageDraw.Draw(cover)
|
cover_draw = ImageDraw.Draw(cover)
|
||||||
|
|
||||||
|
@ -133,8 +142,45 @@ def _create_cover_image(
|
||||||
):
|
):
|
||||||
cover_draw.line(((0, i), (cover.width, i)), tuple(color), 1)
|
cover_draw.line(((0, i), (cover.width, i)), tuple(color), 1)
|
||||||
|
|
||||||
|
return cover
|
||||||
|
|
||||||
|
|
||||||
|
def _create_cover_image(
|
||||||
|
thumbnail: Image.Image,
|
||||||
|
avatar: Optional[Image.Image],
|
||||||
|
title: str,
|
||||||
|
channel: str,
|
||||||
|
style: CoverStyle,
|
||||||
|
) -> Image.Image:
|
||||||
|
# Remove black bars from thumbnail
|
||||||
|
thumbnail = bordercrop.crop(
|
||||||
|
thumbnail,
|
||||||
|
MINIMUM_ROWS=int(thumbnail.height * 0.1),
|
||||||
|
MINIMUM_THRESHOLD_HITTING=int(thumbnail.width * 0.3),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Scale the thumbnail image down to cover size
|
||||||
|
tn_resize_height = int(COVER_WIDTH / thumbnail.width * thumbnail.height)
|
||||||
|
tn_16_9_height = int(COVER_WIDTH / 16 * 9)
|
||||||
|
tn_height = min(tn_resize_height, tn_16_9_height)
|
||||||
|
tn_crop_t = int((tn_resize_height - tn_height) / 2)
|
||||||
|
tn_crop_b = tn_resize_height - tn_crop_t
|
||||||
|
tn = thumbnail.resize(
|
||||||
|
(COVER_WIDTH, tn_resize_height), Image.Resampling.LANCZOS
|
||||||
|
).crop((0, tn_crop_t, COVER_WIDTH, tn_crop_b))
|
||||||
|
|
||||||
|
# Get dominant colors from the top and bottom 20% of the thumbnail image
|
||||||
|
top_part = tn.crop((0, 0, COVER_WIDTH, int(tn_height * 0.2)))
|
||||||
|
bottom_part = tn.crop((0, int(tn_height * 0.8), COVER_WIDTH, tn_height))
|
||||||
|
top_color = _get_dominant_color(top_part)
|
||||||
|
bottom_color = _get_dominant_color(bottom_part)
|
||||||
|
|
||||||
|
cover = _get_baseimage(thumbnail, top_color, bottom_color, style)
|
||||||
|
cover_draw = ImageDraw.Draw(cover)
|
||||||
|
|
||||||
# Insert thumbnail image in the middle
|
# Insert thumbnail image in the middle
|
||||||
tn_margin = int((COVER_WIDTH - tn_height) / 2)
|
tn_margin = int((COVER_WIDTH - tn_height) / 2)
|
||||||
|
tn_16_9_margin = int((COVER_WIDTH - tn_16_9_height) / 2)
|
||||||
cover.paste(tn, (0, tn_margin))
|
cover.paste(tn, (0, tn_margin))
|
||||||
|
|
||||||
# Add channel avatar
|
# Add channel avatar
|
||||||
|
@ -142,8 +188,8 @@ def _create_cover_image(
|
||||||
avt_size = 0
|
avt_size = 0
|
||||||
|
|
||||||
if avatar:
|
if avatar:
|
||||||
avt_margin = int(tn_margin * 0.05)
|
avt_margin = int(tn_16_9_margin * 0.05)
|
||||||
avt_size = tn_margin - 2 * avt_margin
|
avt_size = tn_16_9_margin - 2 * avt_margin
|
||||||
|
|
||||||
avt = avatar.resize((avt_size, avt_size), Image.Resampling.LANCZOS)
|
avt = avatar.resize((avt_size, avt_size), Image.Resampling.LANCZOS)
|
||||||
|
|
||||||
|
@ -169,7 +215,7 @@ def _create_cover_image(
|
||||||
text_margin_topleft,
|
text_margin_topleft,
|
||||||
text_vertical_offset,
|
text_vertical_offset,
|
||||||
COVER_WIDTH - text_margin_x,
|
COVER_WIDTH - text_margin_x,
|
||||||
tn_margin,
|
tn_16_9_margin,
|
||||||
),
|
),
|
||||||
channel,
|
channel,
|
||||||
fnt,
|
fnt,
|
||||||
|
@ -180,7 +226,7 @@ def _create_cover_image(
|
||||||
cover_draw,
|
cover_draw,
|
||||||
(
|
(
|
||||||
text_margin_x,
|
text_margin_x,
|
||||||
COVER_WIDTH - tn_margin + text_vertical_offset,
|
COVER_WIDTH - tn_16_9_margin + text_vertical_offset,
|
||||||
COVER_WIDTH - text_margin_x,
|
COVER_WIDTH - text_margin_x,
|
||||||
COVER_WIDTH,
|
COVER_WIDTH,
|
||||||
),
|
),
|
||||||
|
@ -198,6 +244,7 @@ def create_cover_file(
|
||||||
avatar_path: Optional[Path],
|
avatar_path: Optional[Path],
|
||||||
title: str,
|
title: str,
|
||||||
channel: str,
|
channel: str,
|
||||||
|
style: CoverStyle,
|
||||||
cover_path: Path,
|
cover_path: Path,
|
||||||
):
|
):
|
||||||
thumbnail = Image.open(thumbnail_path)
|
thumbnail = Image.open(thumbnail_path)
|
||||||
|
@ -206,5 +253,5 @@ def create_cover_file(
|
||||||
if avatar_path:
|
if avatar_path:
|
||||||
avatar = Image.open(avatar_path)
|
avatar = Image.open(avatar_path)
|
||||||
|
|
||||||
cvr = _create_cover_image(thumbnail, avatar, title, channel)
|
cvr = _create_cover_image(thumbnail, avatar, title, channel, style)
|
||||||
cvr.save(cover_path)
|
cvr.save(cover_path)
|
||||||
|
|
|
@ -4,4 +4,5 @@ import requests
|
||||||
|
|
||||||
def download_file(url: str, download_path):
|
def download_file(url: str, download_path):
|
||||||
r = requests.get(url, allow_redirects=True)
|
r = requests.get(url, allow_redirects=True)
|
||||||
|
r.raise_for_status()
|
||||||
open(download_path, "wb").write(r.content)
|
open(download_path, "wb").write(r.content)
|
||||||
|
|
|
@ -7,12 +7,24 @@ import requests
|
||||||
from scrapetube import scrapetube
|
from scrapetube import scrapetube
|
||||||
from yt_dlp import YoutubeDL
|
from yt_dlp import YoutubeDL
|
||||||
|
|
||||||
|
from yt2podcast.service import util
|
||||||
|
|
||||||
|
|
||||||
def get_thumbnail_url(vinfo):
|
def get_thumbnail_url(vinfo):
|
||||||
"""Get the best quality thumbnail"""
|
"""Get the best quality thumbnail"""
|
||||||
return max(vinfo["thumbnails"], key=itemgetter("preference"))["url"]
|
return max(vinfo["thumbnails"], key=itemgetter("preference"))["url"]
|
||||||
|
|
||||||
|
|
||||||
|
def download_thumbnail(vinfo, download_path):
|
||||||
|
best_url = get_thumbnail_url(vinfo)
|
||||||
|
|
||||||
|
try:
|
||||||
|
util.download_file(best_url, download_path)
|
||||||
|
except requests.exceptions.HTTPError:
|
||||||
|
default_url = vinfo["thumbnail"]
|
||||||
|
util.download_file(default_url, download_path)
|
||||||
|
|
||||||
|
|
||||||
def get_video_info(video_id):
|
def get_video_info(video_id):
|
||||||
with YoutubeDL() as ydl:
|
with YoutubeDL() as ydl:
|
||||||
return ydl.extract_info(video_id, download=False)
|
return ydl.extract_info(video_id, download=False)
|
||||||
|
|
|
@ -50,7 +50,7 @@ def test_split_text(height: int, width: int, text: str, expect: List[str]):
|
||||||
"file_name,color",
|
"file_name,color",
|
||||||
[
|
[
|
||||||
("t1.webp", (63, 63, 62)),
|
("t1.webp", (63, 63, 62)),
|
||||||
("t2.webp", (74, 45, 37)),
|
("t2.webp", (17, 14, 15)),
|
||||||
("t3.webp", (54, 24, 28)),
|
("t3.webp", (54, 24, 28)),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
@ -73,23 +73,50 @@ def test_get_text_color(bg_color: typ.Color, text_color: typ.Color):
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"n_image,title,channel",
|
"n_image,title,channel,style",
|
||||||
[
|
[
|
||||||
(1, "ThetaDev @ Embedded World 2019", "ThetaDev"),
|
(1, "ThetaDev @ Embedded World 2019", "ThetaDev", cover.CoverStyle.CLASSIC),
|
||||||
(2, "Sintel - Open Movie by Blender Foundation", "Blender"),
|
(1, "ThetaDev @ Embedded World 2019", "ThetaDev", cover.CoverStyle.BLUR),
|
||||||
(3, "Systemabsturz Teaser zur DiVOC bb3", "media.ccc.de"),
|
(
|
||||||
|
2,
|
||||||
|
"Sintel - Open Movie by Blender Foundation",
|
||||||
|
"Blender",
|
||||||
|
cover.CoverStyle.CLASSIC,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
2,
|
||||||
|
"Sintel - Open Movie by Blender Foundation",
|
||||||
|
"Blender",
|
||||||
|
cover.CoverStyle.BLUR,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
3,
|
||||||
|
"Systemabsturz Teaser zur DiVOC bb3",
|
||||||
|
"media.ccc.de",
|
||||||
|
cover.CoverStyle.CLASSIC,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
3,
|
||||||
|
"Systemabsturz Teaser zur DiVOC bb3",
|
||||||
|
"media.ccc.de",
|
||||||
|
cover.CoverStyle.BLUR,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_create_cover_image(n_image: int, title: str, channel: str):
|
def test_create_cover_image(
|
||||||
|
n_image: int, title: str, channel: str, style: cover.CoverStyle
|
||||||
|
):
|
||||||
tn_file = tests.DIR_TESTFILES / "thumbnail" / f"t{n_image}.webp"
|
tn_file = tests.DIR_TESTFILES / "thumbnail" / f"t{n_image}.webp"
|
||||||
av_file = tests.DIR_TESTFILES / "avatar" / f"a{n_image}.jpg"
|
av_file = tests.DIR_TESTFILES / "avatar" / f"a{n_image}.jpg"
|
||||||
expected_cv_file = tests.DIR_TESTFILES / "cover" / f"c{n_image}.png"
|
expected_cv_file = (
|
||||||
|
tests.DIR_TESTFILES / "cover" / f"c{n_image}_{style.name.lower()}.png"
|
||||||
|
)
|
||||||
|
|
||||||
tn_image = Image.open(tn_file)
|
tn_image = Image.open(tn_file)
|
||||||
av_image = Image.open(av_file)
|
av_image = Image.open(av_file)
|
||||||
expected_cv_image = Image.open(expected_cv_file)
|
expected_cv_image = Image.open(expected_cv_file)
|
||||||
|
|
||||||
cv_image = cover._create_cover_image(tn_image, av_image, title, channel)
|
cv_image = cover._create_cover_image(tn_image, av_image, title, channel, style)
|
||||||
|
|
||||||
diff = ImageChops.difference(cv_image, expected_cv_image)
|
diff = ImageChops.difference(cv_image, expected_cv_image)
|
||||||
assert diff.getbbox() is None
|
assert diff.getbbox() is None
|
||||||
|
@ -98,14 +125,19 @@ def test_create_cover_image(n_image: int, title: str, channel: str):
|
||||||
def test_create_cover_file():
|
def test_create_cover_file():
|
||||||
tn_file = tests.DIR_TESTFILES / "thumbnail" / "t1.webp"
|
tn_file = tests.DIR_TESTFILES / "thumbnail" / "t1.webp"
|
||||||
av_file = tests.DIR_TESTFILES / "avatar" / "a1.jpg"
|
av_file = tests.DIR_TESTFILES / "avatar" / "a1.jpg"
|
||||||
expected_cv_file = tests.DIR_TESTFILES / "cover" / "c1.png"
|
expected_cv_file = tests.DIR_TESTFILES / "cover" / "c1_classic.png"
|
||||||
|
|
||||||
tmpdir_o = tempfile.TemporaryDirectory()
|
tmpdir_o = tempfile.TemporaryDirectory()
|
||||||
tmpdir = Path(tmpdir_o.name)
|
tmpdir = Path(tmpdir_o.name)
|
||||||
cv_file = tmpdir / "cover.png"
|
cv_file = tmpdir / "cover.png"
|
||||||
|
|
||||||
cover.create_cover_file(
|
cover.create_cover_file(
|
||||||
tn_file, av_file, "ThetaDev @ Embedded World 2019", "ThetaDev", cv_file
|
tn_file,
|
||||||
|
av_file,
|
||||||
|
"ThetaDev @ Embedded World 2019",
|
||||||
|
"ThetaDev",
|
||||||
|
cover.CoverStyle.CLASSIC,
|
||||||
|
cv_file,
|
||||||
)
|
)
|
||||||
|
|
||||||
cv_image = Image.open(cv_file)
|
cv_image = Image.open(cv_file)
|
||||||
|
|
BIN
yt2podcast/tests/testfiles/cover/c1_blur.png
Normal file
After Width: | Height: | Size: 275 KiB |
Before Width: | Height: | Size: 234 KiB After Width: | Height: | Size: 234 KiB |
Before Width: | Height: | Size: 229 KiB |
BIN
yt2podcast/tests/testfiles/cover/c2_blur.png
Normal file
After Width: | Height: | Size: 245 KiB |
BIN
yt2podcast/tests/testfiles/cover/c2_classic.png
Normal file
After Width: | Height: | Size: 180 KiB |
BIN
yt2podcast/tests/testfiles/cover/c3_blur.png
Normal file
After Width: | Height: | Size: 216 KiB |
Before Width: | Height: | Size: 173 KiB After Width: | Height: | Size: 173 KiB |
Before Width: | Height: | Size: 101 KiB After Width: | Height: | Size: 17 KiB |
|
@ -1,3 +1,3 @@
|
||||||
from django.shortcuts import render
|
from django.shortcuts import render # noqa: F401
|
||||||
|
|
||||||
# Create your views here.
|
# Create your views here.
|
||||||
|
|