diff --git a/.drone.yml b/.drone.yml deleted file mode 100644 index 155b785..0000000 --- a/.drone.yml +++ /dev/null @@ -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 diff --git a/README.md b/README.md index 329bbb3..6195dbd 100644 --- a/README.md +++ b/README.md @@ -14,9 +14,11 @@ abrufen kann. ## Technik -Der Server sollte mit dem Webframework [Django](https://djangoproject.com/) +Der Server sollte mit dem Webframework [Flask](https://flask.palletsprojects.com/) 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. Für ein ansehnliches Ansehen sorgt Bootstrap. diff --git a/notes/Coverbilder.md b/notes/Coverbilder.md index c5ff94a..18bba24 100644 --- a/notes/Coverbilder.md +++ b/notes/Coverbilder.md @@ -1,14 +1,6 @@ # Coverbilder -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) +Podcast-Cover sind quadratisch. - Durchschnittliche Farbe der oberen und unteren 20% des Bilds berechnen - Farbverlauf zwischen diesen Farben als Hintergrund verwenden diff --git a/notes/Speicher.md b/notes/Speicher.md index 7b3857e..503587c 100644 --- a/notes/Speicher.md +++ b/notes/Speicher.md @@ -3,9 +3,13 @@ ## Verzeichnisstruktur ```txt +_ config + |_ config.toml _ data |_ LinusTechTips |_ .ucast + |_ videos.json # IDs und Metadaten aller heruntergeladenen Videos + |_ options.json # Kanalspezifische Optionen (ID, LastScan) |_ avatar.png # Profilbild des Kanals |_ feed.xml # RSS-Feed |_ covers # Cover-Bilder @@ -26,26 +30,28 @@ _ data ### ChannelOptions -- ID: `str, max_length=30` -- Active: `bool = True` -- LastScan: `datetime` -- SkipLivestreams: `bool = True` -- SkipShorts: `bool = True` -- KeepVideos: `int, nullable` -- Videos: `-> Video (1->n)` +- ID: str +- Active: bool = True +- LastScan: datetime +- SkipLivestreams: bool = True +- SkipShorts: bool = True +- KeepVideos: int = -1 +### Videos + +- Videos: dict[id: str -> Video] ### Video -- ID: `str, max_length=30` -- Title: `str, max_length=200` -- Slug: `str, max_length=209` (YYYYMMDD_Title, used as filename) -- Published: `datetime` -- Downloaded: `datetime, nullable` -- Description: `text` +- ID: str +- Title: str +- Slug: str (YYMMDD_Title, used as filename) +- Published: datetime +- Description: str ### Config - RedisURL: str - ScanInterval: 1h +- DefaultChannelOptions: ChannelOptions - AppriseUrl: str (für Benachrichtigungen, https://github.com/caronc/apprise/wiki) diff --git a/poetry.lock b/poetry.lock index 195374a..025af55 100644 --- a/poetry.lock +++ b/poetry.lock @@ -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_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]] name = "brotli" version = "1.0.9" @@ -80,14 +69,6 @@ python-versions = "*" [package.dependencies] 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]] name = "charset-normalizer" version = "2.0.12" @@ -132,14 +113,6 @@ tomli = {version = "*", optional = true, markers = "extra == \"toml\""} [package.extras] toml = ["tomli"] -[[package]] -name = "distlib" -version = "0.3.4" -description = "Distribution utilities" -category = "dev" -optional = false -python-versions = "*" - [[package]] name = "django" version = "4.0.4" @@ -168,18 +141,6 @@ python-versions = ">=3.6" [package.dependencies] 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]] name = "font-source-sans-pro" version = "0.0.1" @@ -196,17 +157,6 @@ category = "main" optional = false 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]] name = "idna" version = "3.3" @@ -239,14 +189,6 @@ category = "main" optional = false 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]] name = "packaging" 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"] 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]] name = "pluggy" version = "1.0.0" @@ -294,22 +224,6 @@ python-versions = ">=3.6" dev = ["pre-commit", "tox"] 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]] name = "py" version = "1.11.0" @@ -396,14 +310,6 @@ pytest = ">=5.4.0" docs = ["sphinx", "sphinx-rtd-theme"] 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]] name = "requests" version = "2.27.1" @@ -450,14 +356,6 @@ category = "main" optional = false 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]] name = "sqlparse" version = "0.4.2" @@ -466,14 +364,6 @@ category = "main" optional = false 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]] name = "tomli" 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"] 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]] name = "wcag-contrast-ratio" version = "0.9" @@ -564,7 +436,7 @@ websockets = "*" [metadata] lock-version = "1.1" python-versions = "^3.10" -content-hash = "99e2a5970962f1e936da2010b8ec997026f5afe4762af4345da287125e6b7771" +content-hash = "0288eb9eca14b78ee0cfac28cef0dcf17701165b6aa778255bfa73b94d5b2c4f" [metadata.files] asgiref = [ @@ -579,10 +451,6 @@ attrs = [ {file = "attrs-21.4.0-py2.py3-none-any.whl", hash = "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4"}, {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 = [ {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"}, @@ -735,10 +603,6 @@ cffi = [ {file = "cffi-1.15.0-cp39-cp39-win_amd64.whl", hash = "sha256:3773c4d81e6e818df2efbc7dd77325ca0dcb688116050fb2b3011218eda36139"}, {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 = [ {file = "charset-normalizer-2.0.12.tar.gz", hash = "sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597"}, {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.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 = [ {file = "Django-4.0.4-py3-none-any.whl", hash = "sha256:07c8638e7a7f548dc0acaaa7825d84b7bd42b10e8d22268b3d572946f1e9b687"}, {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.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 = [ {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"}, @@ -820,10 +676,6 @@ fonts = [ {file = "fonts-0.0.3-py3-none-any.whl", hash = "sha256:e5f551379088ab260c2537980c3ccdff8af93408d9d4fa3319388d2ee25b7b6d"}, {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 = [ {file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"}, {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.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 = [ {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, {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.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 = [ {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, {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 = [ {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, {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-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 = [ {file = "requests-2.27.1-py2.py3-none-any.whl", hash = "sha256:f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d"}, {file = "requests-2.27.1.tar.gz", hash = "sha256:68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61"}, @@ -1001,18 +806,10 @@ scrapetube = [ sgmllib3k = [ {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 = [ {file = "sqlparse-0.4.2-py3-none-any.whl", hash = "sha256:48719e356bb8b42991bdbb1e8b83223757b93789c00910a616a071910ca4a64d"}, {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 = [ {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, {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.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 = [ {file = "wcag-contrast-ratio-0.9.tar.gz", hash = "sha256:69192b8e5c0a7d0dc5ff1187eeb3e398141633a4bde51c69c87f58fe87ed361c"}, ] diff --git a/pyproject.toml b/pyproject.toml index 0323b68..ba41970 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,14 +20,12 @@ colorthief = "^0.2.1" wcag-contrast-ratio = "^0.9" font-source-sans-pro = "^0.0.1" fonts = "^0.0.3" -bordercrop = "^1.0.0" [tool.poetry.dev-dependencies] pytest = "^7.1.1" pytest-cov = "^3.0.0" invoke = "^1.7.0" pytest-django = "^4.5.2" -pre-commit = "^2.19.0" [tool.poetry.scripts] "ucast-manage" = "ucast.manage:main" diff --git a/tasks.py b/tasks.py index 832c200..d803b2d 100644 --- a/tasks.py +++ b/tasks.py @@ -11,17 +11,7 @@ os.chdir(Path(__file__).absolute().parent) @task def test(c): - 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) + c.run("pytest tests", pty=True) @task @@ -34,24 +24,19 @@ def get_cover(c, vid=""): vinfo = youtube.get_video_info(vid) title = vinfo["fulltitle"] channel_name = vinfo["uploader"] + thumbnail_url = youtube.get_thumbnail_url(vinfo) channel_url = vinfo["channel_url"] channel_metadata = youtube.get_channel_metadata(channel_url) 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 tn_file = tests.DIR_TESTFILES / "thumbnail" / f"t{ti}.webp" av_file = tests.DIR_TESTFILES / "avatar" / f"a{ti}.jpg" - cv_file = tests.DIR_TESTFILES / "cover" / f"c{ti}_classic.png" - cv_blur_file = tests.DIR_TESTFILES / "cover" / f"c{ti}_blur.png" + cv_file = tests.DIR_TESTFILES / "cover" / f"c{ti}.png" - youtube.download_thumbnail(vinfo, tn_file) + util.download_file(thumbnail_url, tn_file) util.download_file(channel_metadata.avatar_url, av_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 - ) + cover.create_cover_file(tn_file, av_file, title, channel_name, cv_file) diff --git a/yt2podcast/admin.py b/yt2podcast/admin.py index 4fd5490..8c38f3f 100644 --- a/yt2podcast/admin.py +++ b/yt2podcast/admin.py @@ -1,3 +1,3 @@ -from django.contrib import admin # noqa: F401 +from django.contrib import admin # Register your models here. diff --git a/yt2podcast/service/cover.py b/yt2podcast/service/cover.py index 8049b79..38db4da 100644 --- a/yt2podcast/service/cover.py +++ b/yt2podcast/service/cover.py @@ -1,14 +1,12 @@ # coding=utf-8 -import enum import math from pathlib import Path from typing import List, Optional, Tuple import wcag_contrast_ratio -from bordercrop import bordercrop from colorthief import ColorThief from fonts.ttf import SourceSansPro -from PIL import Image, ImageDraw, ImageFilter, ImageFont +from PIL import Image, ImageDraw, ImageFont from yt2podcast.service import typ @@ -16,11 +14,6 @@ CHAR_ELLIPSIS = "…" COVER_WIDTH = 500 -class CoverStyle(enum.Enum): - CLASSIC = enum.auto() - BLUR = enum.auto() - - def _split_text( height: int, width: int, text: str, font: ImageFont.FreeTypeFont, line_spacing=0 ) -> List[str]: @@ -117,57 +110,12 @@ def _get_text_color(bg_color) -> typ.Color: 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( - thumbnail: Image.Image, - avatar: Optional[Image.Image], - title: str, - channel: str, - style: CoverStyle, + thumbnail: Image.Image, avatar: Optional[Image.Image], title: str, channel: str ) -> 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)) + tn_height = int(COVER_WIDTH / thumbnail.width * thumbnail.height) + tn = thumbnail.resize((COVER_WIDTH, tn_height), Image.Resampling.LANCZOS) # 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))) @@ -175,12 +123,18 @@ def _create_cover_image( top_color = _get_dominant_color(top_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) + # 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 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)) # Add channel avatar @@ -188,8 +142,8 @@ def _create_cover_image( avt_size = 0 if avatar: - avt_margin = int(tn_16_9_margin * 0.05) - avt_size = tn_16_9_margin - 2 * avt_margin + avt_margin = int(tn_margin * 0.05) + avt_size = tn_margin - 2 * avt_margin avt = avatar.resize((avt_size, avt_size), Image.Resampling.LANCZOS) @@ -215,7 +169,7 @@ def _create_cover_image( text_margin_topleft, text_vertical_offset, COVER_WIDTH - text_margin_x, - tn_16_9_margin, + tn_margin, ), channel, fnt, @@ -226,7 +180,7 @@ def _create_cover_image( cover_draw, ( 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, ), @@ -244,7 +198,6 @@ def create_cover_file( avatar_path: Optional[Path], title: str, channel: str, - style: CoverStyle, cover_path: Path, ): thumbnail = Image.open(thumbnail_path) @@ -253,5 +206,5 @@ def create_cover_file( if 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) diff --git a/yt2podcast/service/util.py b/yt2podcast/service/util.py index c58a39c..a65bc97 100644 --- a/yt2podcast/service/util.py +++ b/yt2podcast/service/util.py @@ -4,5 +4,4 @@ import requests def download_file(url: str, download_path): r = requests.get(url, allow_redirects=True) - r.raise_for_status() open(download_path, "wb").write(r.content) diff --git a/yt2podcast/service/youtube.py b/yt2podcast/service/youtube.py index a880ef2..8e56d79 100644 --- a/yt2podcast/service/youtube.py +++ b/yt2podcast/service/youtube.py @@ -7,24 +7,12 @@ import requests from scrapetube import scrapetube from yt_dlp import YoutubeDL -from yt2podcast.service import util - def get_thumbnail_url(vinfo): """Get the best quality thumbnail""" 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): with YoutubeDL() as ydl: return ydl.extract_info(video_id, download=False) diff --git a/yt2podcast/tests/test_cover.py b/yt2podcast/tests/test_cover.py index 8a4d457..518ac25 100644 --- a/yt2podcast/tests/test_cover.py +++ b/yt2podcast/tests/test_cover.py @@ -50,7 +50,7 @@ def test_split_text(height: int, width: int, text: str, expect: List[str]): "file_name,color", [ ("t1.webp", (63, 63, 62)), - ("t2.webp", (17, 14, 15)), + ("t2.webp", (74, 45, 37)), ("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( - "n_image,title,channel,style", + "n_image,title,channel", [ - (1, "ThetaDev @ Embedded World 2019", "ThetaDev", cover.CoverStyle.CLASSIC), - (1, "ThetaDev @ Embedded World 2019", "ThetaDev", cover.CoverStyle.BLUR), - ( - 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, - ), + (1, "ThetaDev @ Embedded World 2019", "ThetaDev"), + (2, "Sintel - Open Movie by Blender Foundation", "Blender"), + (3, "Systemabsturz Teaser zur DiVOC bb3", "media.ccc.de"), ], ) -def test_create_cover_image( - n_image: int, title: str, channel: str, style: cover.CoverStyle -): +def test_create_cover_image(n_image: int, title: str, channel: str): tn_file = tests.DIR_TESTFILES / "thumbnail" / f"t{n_image}.webp" av_file = tests.DIR_TESTFILES / "avatar" / f"a{n_image}.jpg" - expected_cv_file = ( - tests.DIR_TESTFILES / "cover" / f"c{n_image}_{style.name.lower()}.png" - ) + expected_cv_file = tests.DIR_TESTFILES / "cover" / f"c{n_image}.png" tn_image = Image.open(tn_file) av_image = Image.open(av_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) assert diff.getbbox() is None @@ -125,19 +98,14 @@ def test_create_cover_image( def test_create_cover_file(): tn_file = tests.DIR_TESTFILES / "thumbnail" / "t1.webp" 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 = Path(tmpdir_o.name) cv_file = tmpdir / "cover.png" cover.create_cover_file( - tn_file, - av_file, - "ThetaDev @ Embedded World 2019", - "ThetaDev", - cover.CoverStyle.CLASSIC, - cv_file, + tn_file, av_file, "ThetaDev @ Embedded World 2019", "ThetaDev", cv_file ) cv_image = Image.open(cv_file) diff --git a/yt2podcast/tests/testfiles/cover/c1_classic.png b/yt2podcast/tests/testfiles/cover/c1.png similarity index 100% rename from yt2podcast/tests/testfiles/cover/c1_classic.png rename to yt2podcast/tests/testfiles/cover/c1.png diff --git a/yt2podcast/tests/testfiles/cover/c1_blur.png b/yt2podcast/tests/testfiles/cover/c1_blur.png deleted file mode 100644 index 1ea1caf..0000000 Binary files a/yt2podcast/tests/testfiles/cover/c1_blur.png and /dev/null differ diff --git a/yt2podcast/tests/testfiles/cover/c2.png b/yt2podcast/tests/testfiles/cover/c2.png new file mode 100644 index 0000000..ab4d221 Binary files /dev/null and b/yt2podcast/tests/testfiles/cover/c2.png differ diff --git a/yt2podcast/tests/testfiles/cover/c2_blur.png b/yt2podcast/tests/testfiles/cover/c2_blur.png deleted file mode 100644 index 453d455..0000000 Binary files a/yt2podcast/tests/testfiles/cover/c2_blur.png and /dev/null differ diff --git a/yt2podcast/tests/testfiles/cover/c2_classic.png b/yt2podcast/tests/testfiles/cover/c2_classic.png deleted file mode 100644 index 3fb1cae..0000000 Binary files a/yt2podcast/tests/testfiles/cover/c2_classic.png and /dev/null differ diff --git a/yt2podcast/tests/testfiles/cover/c3_classic.png b/yt2podcast/tests/testfiles/cover/c3.png similarity index 100% rename from yt2podcast/tests/testfiles/cover/c3_classic.png rename to yt2podcast/tests/testfiles/cover/c3.png diff --git a/yt2podcast/tests/testfiles/cover/c3_blur.png b/yt2podcast/tests/testfiles/cover/c3_blur.png deleted file mode 100644 index 24b24bd..0000000 Binary files a/yt2podcast/tests/testfiles/cover/c3_blur.png and /dev/null differ diff --git a/yt2podcast/tests/testfiles/thumbnail/t2.webp b/yt2podcast/tests/testfiles/thumbnail/t2.webp index b54d902..fead8a8 100644 Binary files a/yt2podcast/tests/testfiles/thumbnail/t2.webp and b/yt2podcast/tests/testfiles/thumbnail/t2.webp differ diff --git a/yt2podcast/views.py b/yt2podcast/views.py index 7a02d1f..91ea44a 100644 --- a/yt2podcast/views.py +++ b/yt2podcast/views.py @@ -1,3 +1,3 @@ -from django.shortcuts import render # noqa: F401 +from django.shortcuts import render # Create your views here.