Compare commits

..

No commits in common. "9d4781165f8b7e70dd7526411708a86fb25786c3" and "5def0f3c61958d1c67ad898547e62d5ce35342a9" have entirely different histories.

21 changed files with 59 additions and 386 deletions

View file

@ -1,11 +0,0 @@
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

View file

@ -14,9 +14,11 @@ abrufen kann.
## Technik ## Technik
Der Server sollte mit dem Webframework [Django](https://djangoproject.com/) Der Server sollte mit dem Webframework [Flask](https://flask.palletsprojects.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.

View file

@ -1,14 +1,6 @@
# Coverbilder # Coverbilder
Podcast-Cover sind quadratisch, während YT-Thumbnails das Seitenverhältnis Podcast-Cover sind quadratisch.
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

View file

@ -3,9 +3,13 @@
## 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
@ -26,26 +30,28 @@ _ data
### ChannelOptions ### ChannelOptions
- ID: `str, max_length=30` - ID: str
- Active: `bool = True` - Active: bool = True
- LastScan: `datetime` - LastScan: datetime
- SkipLivestreams: `bool = True` - SkipLivestreams: bool = True
- SkipShorts: `bool = True` - SkipShorts: bool = True
- KeepVideos: `int, nullable` - KeepVideos: int = -1
- Videos: `-> Video (1->n)`
### Videos
- Videos: dict[id: str -> Video]
### Video ### Video
- ID: `str, max_length=30` - ID: str
- Title: `str, max_length=200` - Title: str
- Slug: `str, max_length=209` (YYYYMMDD_Title, used as filename) - Slug: str (YYMMDD_Title, used as filename)
- Published: `datetime` - Published: datetime
- Downloaded: `datetime, nullable` - Description: str
- 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
View file

@ -31,17 +31,6 @@ 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"
@ -80,14 +69,6 @@ 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"
@ -132,14 +113,6 @@ 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"
@ -168,18 +141,6 @@ 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"
@ -196,17 +157,6 @@ 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"
@ -239,14 +189,6 @@ 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"
@ -270,18 +212,6 @@ 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"
@ -294,22 +224,6 @@ 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"
@ -396,14 +310,6 @@ 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"
@ -450,14 +356,6 @@ 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"
@ -466,14 +364,6 @@ 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"
@ -511,24 +401,6 @@ 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"
@ -564,7 +436,7 @@ websockets = "*"
[metadata] [metadata]
lock-version = "1.1" lock-version = "1.1"
python-versions = "^3.10" python-versions = "^3.10"
content-hash = "99e2a5970962f1e936da2010b8ec997026f5afe4762af4345da287125e6b7771" content-hash = "0288eb9eca14b78ee0cfac28cef0dcf17701165b6aa778255bfa73b94d5b2c4f"
[metadata.files] [metadata.files]
asgiref = [ asgiref = [
@ -579,10 +451,6 @@ 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"},
@ -735,10 +603,6 @@ 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"},
@ -794,10 +658,6 @@ 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"},
@ -806,10 +666,6 @@ 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"},
@ -820,10 +676,6 @@ 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"},
@ -840,10 +692,6 @@ 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"},
@ -888,18 +736,10 @@ 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"},
@ -953,41 +793,6 @@ 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"},
@ -1001,18 +806,10 @@ 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"},
@ -1029,10 +826,6 @@ 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"},
] ]

View file

@ -20,14 +20,12 @@ 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"

View file

@ -11,17 +11,7 @@ os.chdir(Path(__file__).absolute().parent)
@task @task
def test(c): def test(c):
c.run("pytest", pty=True) c.run("pytest tests", 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
@ -34,24 +24,19 @@ 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}_classic.png"): while os.path.exists(tests.DIR_TESTFILES / "cover" / f"c{ti}.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}_classic.png" cv_file = tests.DIR_TESTFILES / "cover" / f"c{ti}.png"
cv_blur_file = tests.DIR_TESTFILES / "cover" / f"c{ti}_blur.png"
youtube.download_thumbnail(vinfo, tn_file) util.download_file(thumbnail_url, tn_file)
util.download_file(channel_metadata.avatar_url, av_file) util.download_file(channel_metadata.avatar_url, av_file)
cover.create_cover_file( cover.create_cover_file(tn_file, av_file, title, channel_name, cv_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
)

View file

@ -1,3 +1,3 @@
from django.contrib import admin # noqa: F401 from django.contrib import admin
# Register your models here. # Register your models here.

View file

@ -1,14 +1,12 @@
# 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, ImageFilter, ImageFont from PIL import Image, ImageDraw, ImageFont
from yt2podcast.service import typ from yt2podcast.service import typ
@ -16,11 +14,6 @@ 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]:
@ -117,57 +110,12 @@ def _get_text_color(bg_color) -> typ.Color:
return 0, 0, 0 return 0, 0, 0
def _get_baseimage(
thumbnail: Image.Image,
top_color: typ.Color,
bottom_color: typ.Color,
style: CoverStyle,
):
if style == CoverStyle.BLUR:
ctn_width = int(COVER_WIDTH / thumbnail.height * thumbnail.width)
ctn_l = int((ctn_width - COVER_WIDTH) / 2)
ctn_r = ctn_width - ctn_l
cover = (
thumbnail.resize((ctn_width, COVER_WIDTH), Image.Resampling.LANCZOS)
.crop((ctn_l, 0, ctn_r, COVER_WIDTH))
.filter(ImageFilter.GaussianBlur(20))
)
else:
cover = Image.new("RGB", (COVER_WIDTH, COVER_WIDTH))
cover_draw = ImageDraw.Draw(cover)
# Draw background gradient
for i, color in enumerate(
_interpolate_color(top_color, bottom_color, cover.height)
):
cover_draw.line(((0, i), (cover.width, i)), tuple(color), 1)
return cover
def _create_cover_image( def _create_cover_image(
thumbnail: Image.Image, thumbnail: Image.Image, avatar: Optional[Image.Image], title: str, channel: str
avatar: Optional[Image.Image],
title: str,
channel: str,
style: CoverStyle,
) -> Image.Image: ) -> 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 # Scale the thumbnail image down to cover size
tn_resize_height = int(COVER_WIDTH / thumbnail.width * thumbnail.height) tn_height = int(COVER_WIDTH / thumbnail.width * thumbnail.height)
tn_16_9_height = int(COVER_WIDTH / 16 * 9) tn = thumbnail.resize((COVER_WIDTH, tn_height), Image.Resampling.LANCZOS)
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 # 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))) top_part = tn.crop((0, 0, COVER_WIDTH, int(tn_height * 0.2)))
@ -175,12 +123,18 @@ def _create_cover_image(
top_color = _get_dominant_color(top_part) top_color = _get_dominant_color(top_part)
bottom_color = _get_dominant_color(bottom_part) bottom_color = _get_dominant_color(bottom_part)
cover = _get_baseimage(thumbnail, top_color, bottom_color, style) # Create new cover image
cover = Image.new("RGB", (COVER_WIDTH, COVER_WIDTH))
cover_draw = ImageDraw.Draw(cover) cover_draw = ImageDraw.Draw(cover)
# Draw background gradient
for i, color in enumerate(
_interpolate_color(top_color, bottom_color, cover.height)
):
cover_draw.line(((0, i), (cover.width, i)), tuple(color), 1)
# 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
@ -188,8 +142,8 @@ def _create_cover_image(
avt_size = 0 avt_size = 0
if avatar: if avatar:
avt_margin = int(tn_16_9_margin * 0.05) avt_margin = int(tn_margin * 0.05)
avt_size = tn_16_9_margin - 2 * avt_margin avt_size = tn_margin - 2 * avt_margin
avt = avatar.resize((avt_size, avt_size), Image.Resampling.LANCZOS) avt = avatar.resize((avt_size, avt_size), Image.Resampling.LANCZOS)
@ -215,7 +169,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_16_9_margin, tn_margin,
), ),
channel, channel,
fnt, fnt,
@ -226,7 +180,7 @@ def _create_cover_image(
cover_draw, cover_draw,
( (
text_margin_x, text_margin_x,
COVER_WIDTH - tn_16_9_margin + text_vertical_offset, COVER_WIDTH - tn_margin + text_vertical_offset,
COVER_WIDTH - text_margin_x, COVER_WIDTH - text_margin_x,
COVER_WIDTH, COVER_WIDTH,
), ),
@ -244,7 +198,6 @@ 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)
@ -253,5 +206,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, style) cvr = _create_cover_image(thumbnail, avatar, title, channel)
cvr.save(cover_path) cvr.save(cover_path)

View file

@ -4,5 +4,4 @@ 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)

View file

@ -7,24 +7,12 @@ 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)

View file

@ -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", (17, 14, 15)), ("t2.webp", (74, 45, 37)),
("t3.webp", (54, 24, 28)), ("t3.webp", (54, 24, 28)),
], ],
) )
@ -73,50 +73,23 @@ def test_get_text_color(bg_color: typ.Color, text_color: typ.Color):
@pytest.mark.parametrize( @pytest.mark.parametrize(
"n_image,title,channel,style", "n_image,title,channel",
[ [
(1, "ThetaDev @ Embedded World 2019", "ThetaDev", cover.CoverStyle.CLASSIC), (1, "ThetaDev @ Embedded World 2019", "ThetaDev"),
(1, "ThetaDev @ Embedded World 2019", "ThetaDev", cover.CoverStyle.BLUR), (2, "Sintel - Open Movie by Blender Foundation", "Blender"),
( (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( def test_create_cover_image(n_image: int, title: str, channel: str):
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 = ( expected_cv_file = tests.DIR_TESTFILES / "cover" / f"c{n_image}.png"
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, style) cv_image = cover._create_cover_image(tn_image, av_image, title, channel)
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
@ -125,19 +98,14 @@ def test_create_cover_image(
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_classic.png" expected_cv_file = tests.DIR_TESTFILES / "cover" / "c1.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, tn_file, av_file, "ThetaDev @ Embedded World 2019", "ThetaDev", cv_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)

View file

Before

Width:  |  Height:  |  Size: 234 KiB

After

Width:  |  Height:  |  Size: 234 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 275 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 229 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 245 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 180 KiB

View file

Before

Width:  |  Height:  |  Size: 173 KiB

After

Width:  |  Height:  |  Size: 173 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 216 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 101 KiB

View file

@ -1,3 +1,3 @@
from django.shortcuts import render # noqa: F401 from django.shortcuts import render
# Create your views here. # Create your views here.