diff --git a/.gitignore b/.gitignore index d4414bf..366f892 100644 --- a/.gitignore +++ b/.gitignore @@ -20,7 +20,7 @@ node_modules *.mp3 # Application data -/_run* +/_run *.sqlite3 # Generated assets diff --git a/deploy/docker-compose_develop.yml b/deploy/docker-compose.yml similarity index 100% rename from deploy/docker-compose_develop.yml rename to deploy/docker-compose.yml diff --git a/notes/Speicher.md b/notes/Speicher.md index 215bb94..7b3857e 100644 --- a/notes/Speicher.md +++ b/notes/Speicher.md @@ -5,19 +5,14 @@ ```txt _ data |_ LinusTechTips - |_ _ucast - |_ avatar.jpg # Profilbild des Kanals - |_ avatar_sm.webp + |_ .ucast + |_ avatar.png # Profilbild des Kanals + |_ feed.xml # RSS-Feed |_ covers # Cover-Bilder - |_ 220409_Building_a_1_000_000_Computer.png - |_ 220410_Apple_makes_GREAT_Gaming_Computers.png - |_ thumbnails - |_ 220409_Building_a_1_000_000_Computer.webp - |_ 220409_Building_a_1_000_000_Computer_sm.webp - |_ 220410_Apple_makes_GREAT_Gaming_Computers.webp - |_ 220410_Apple_makes_GREAT_Gaming_Computers_sm.webp - |_ 220409_Building_a_1_000_000_Computer.mp3 - |_ 220410_Apple_makes_GREAT_Gaming_Computers.mp3 + |_ 220409_Building a _1_000_000 Computer.png + |_ 220410_Apple makes GREAT Gaming Computers.png + |_ 220409_Building a _1_000_000 Computer.mp3 + |_ 220410_Apple makes GREAT Gaming Computers.mp3 |_ Andreas Spiess |_ ... diff --git a/poetry.lock b/poetry.lock index 6889046..eeea053 100644 --- a/poetry.lock +++ b/poetry.lock @@ -9,14 +9,6 @@ python-versions = ">=3.7" [package.extras] tests = ["pytest", "pytest-asyncio", "mypy (>=0.800)"] -[[package]] -name = "async-timeout" -version = "4.0.2" -description = "Timeout context manager for asyncio programs" -category = "main" -optional = false -python-versions = ">=3.6" - [[package]] name = "atomicwrites" version = "1.4.0" @@ -39,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_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" @@ -96,22 +99,11 @@ python-versions = ">=3.5.0" [package.extras] unicode_backport = ["unicodedata2"] -[[package]] -name = "click" -version = "8.1.3" -description = "Composable command line interface toolkit" -category = "main" -optional = false -python-versions = ">=3.7" - -[package.dependencies] -colorama = {version = "*", markers = "platform_system == \"Windows\""} - [[package]] name = "colorama" version = "0.4.4" description = "Cross-platform colored terminal text." -category = "main" +category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" @@ -128,7 +120,7 @@ Pillow = "*" [[package]] name = "coverage" -version = "6.3.3" +version = "6.3.2" description = "Code coverage measurement for Python" category = "dev" optional = false @@ -140,20 +132,6 @@ tomli = {version = "*", optional = true, markers = "extra == \"toml\""} [package.extras] toml = ["tomli"] -[[package]] -name = "deprecated" -version = "1.2.13" -description = "Python @deprecated decorator to deprecate old python classes, functions or methods." -category = "main" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" - -[package.dependencies] -wrapt = ">=1.10,<2" - -[package.extras] -dev = ["tox", "bump2version (<1)", "sphinx (<2)", "importlib-metadata (<3)", "importlib-resources (<4)", "configparser (<5)", "sphinxcontrib-websupport (<2)", "zipp (<2)", "PyTest (<5)", "PyTest-Cov (<2.6)", "pytest", "pytest-cov"] - [[package]] name = "distlib" version = "0.3.4" @@ -258,7 +236,7 @@ python-versions = "*" [[package]] name = "invoke" -version = "1.7.1" +version = "1.7.0" description = "Pythonic task execution" category = "dev" optional = false @@ -292,7 +270,7 @@ python-versions = "*" name = "packaging" version = "21.3" description = "Core utilities for Python packages" -category = "main" +category = "dev" optional = false python-versions = ">=3.6" @@ -385,9 +363,9 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [[package]] name = "pyparsing" -version = "3.0.9" +version = "3.0.8" description = "pyparsing module - Classes and methods to define and execute parsing grammars" -category = "main" +category = "dev" optional = false python-versions = ">=3.6.8" @@ -464,23 +442,6 @@ category = "dev" optional = false python-versions = ">=3.6" -[[package]] -name = "redis" -version = "4.3.1" -description = "Python client for Redis database and key-value store" -category = "main" -optional = false -python-versions = ">=3.6" - -[package.dependencies] -async-timeout = ">=4.0.2" -deprecated = ">=1.2.3" -packaging = ">=20.4" - -[package.extras] -hiredis = ["hiredis (>=1.0.0)"] -ocsp = ["cryptography (>=36.0.1)", "pyopenssl (==20.0.1)", "requests (>=2.26.0)"] - [[package]] name = "requests" version = "2.27.1" @@ -508,16 +469,16 @@ optional = false python-versions = "*" [[package]] -name = "rq" -version = "1.10.1" -description = "RQ is a simple, lightweight, library for creating background jobs, and processing them." +name = "scrapetube" +version = "2.2.2" +description = "Scrape youtube without the official youtube api and without selenium." category = "main" optional = false -python-versions = ">=3.5" +python-versions = ">=3.6" [package.dependencies] -click = ">=5.0.0" -redis = ">=3.5.0" +requests = "*" +typing-extensions = "*" [[package]] name = "sgmllib3k" @@ -535,14 +496,6 @@ category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" -[[package]] -name = "slugify" -version = "0.0.1" -description = "A generic slugifier." -category = "main" -optional = false -python-versions = "*" - [[package]] name = "sqlparse" version = "0.4.2" @@ -567,6 +520,14 @@ category = "dev" optional = false python-versions = ">=3.7" +[[package]] +name = "typing-extensions" +version = "4.2.0" +description = "Backported and Experimental Type Hints for Python 3.7+" +category = "main" +optional = false +python-versions = ">=3.7" + [[package]] name = "tzdata" version = "2022.1" @@ -622,14 +583,6 @@ category = "main" optional = false python-versions = ">=3.7" -[[package]] -name = "wrapt" -version = "1.14.1" -description = "Module for decorators, wrappers and monkey patching." -category = "main" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" - [[package]] name = "yt-dlp" version = "2022.4.8" @@ -649,17 +602,13 @@ websockets = "*" [metadata] lock-version = "1.1" python-versions = "^3.10" -content-hash = "2d9aa9c628676b6c9981964a7e01a8d0b0a291025b695c5d98441d29720bced0" +content-hash = "8609785f53a44a16f3c5c1d5042ab2627bb198f3c7daa8ea18e55bf1e66c4345" [metadata.files] asgiref = [ {file = "asgiref-3.5.1-py3-none-any.whl", hash = "sha256:45a429524fba18aba9d512498b19d220c4d628e75b40cf5c627524dbaebc5cc1"}, {file = "asgiref-3.5.1.tar.gz", hash = "sha256:fddeea3c53fa99d0cdb613c3941cc6e52d822491fc2753fba25768fb5bf4e865"}, ] -async-timeout = [ - {file = "async-timeout-4.0.2.tar.gz", hash = "sha256:2163e1640ddb52b7a8c80d0a67a08587e5d245cc9c553a74a847056bc2976b15"}, - {file = "async_timeout-4.0.2-py3-none-any.whl", hash = "sha256:8ca1e4fcf50d07413d66d1a5e416e42cfdf5851c981d679a09851a6853383b3c"}, -] atomicwrites = [ {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, @@ -668,6 +617,10 @@ 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"}, @@ -828,10 +781,6 @@ 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"}, ] -click = [ - {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, - {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, -] colorama = [ {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, @@ -841,51 +790,47 @@ colorthief = [ {file = "colorthief-0.2.1.tar.gz", hash = "sha256:079cb0c95bdd669c4643e2f7494de13b0b6029d5cdbe2d74d5d3c3386bd57221"}, ] coverage = [ - {file = "coverage-6.3.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df32ee0f4935a101e4b9a5f07b617d884a531ed5666671ff6ac66d2e8e8246d8"}, - {file = "coverage-6.3.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:75b5dbffc334e0beb4f6c503fb95e6d422770fd2d1b40a64898ea26d6c02742d"}, - {file = "coverage-6.3.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:114944e6061b68a801c5da5427b9173a0dd9d32cd5fcc18a13de90352843737d"}, - {file = "coverage-6.3.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2ab88a01cd180b5640ccc9c47232e31924d5f9967ab7edd7e5c91c68eee47a69"}, - {file = "coverage-6.3.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad8f9068f5972a46d50fe5f32c09d6ee11da69c560fcb1b4c3baea246ca4109b"}, - {file = "coverage-6.3.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4cd696aa712e6cd16898d63cf66139dc70d998f8121ab558f0e1936396dbc579"}, - {file = "coverage-6.3.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:c1a9942e282cc9d3ed522cd3e3cab081149b27ea3bda72d6f61f84eaf88c1a63"}, - {file = "coverage-6.3.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c06455121a089252b5943ea682187a4e0a5cf0a3fb980eb8e7ce394b144430a9"}, - {file = "coverage-6.3.3-cp310-cp310-win32.whl", hash = "sha256:cb5311d6ccbd22578c80028c5e292a7ab9adb91bd62c1982087fad75abe2e63d"}, - {file = "coverage-6.3.3-cp310-cp310-win_amd64.whl", hash = "sha256:6d4a6f30f611e657495cc81a07ff7aa8cd949144e7667c5d3e680d73ba7a70e4"}, - {file = "coverage-6.3.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:79bf405432428e989cad7b8bc60581963238f7645ae8a404f5dce90236cc0293"}, - {file = "coverage-6.3.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:338c417613f15596af9eb7a39353b60abec9d8ce1080aedba5ecee6a5d85f8d3"}, - {file = "coverage-6.3.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:db094a6a4ae6329ed322a8973f83630b12715654c197dd392410400a5bfa1a73"}, - {file = "coverage-6.3.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1414e8b124611bf4df8d77215bd32cba6e3425da8ce9c1f1046149615e3a9a31"}, - {file = "coverage-6.3.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:93b16b08f94c92cab88073ffd185070cdcb29f1b98df8b28e6649145b7f2c90d"}, - {file = "coverage-6.3.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:fbc86ae8cc129c801e7baaafe3addf3c8d49c9c1597c44bdf2d78139707c3c62"}, - {file = "coverage-6.3.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:b5ba058610e8289a07db2a57bce45a1793ec0d3d11db28c047aae2aa1a832572"}, - {file = "coverage-6.3.3-cp37-cp37m-win32.whl", hash = "sha256:8329635c0781927a2c6ae068461e19674c564e05b86736ab8eb29c420ee7dc20"}, - {file = "coverage-6.3.3-cp37-cp37m-win_amd64.whl", hash = "sha256:e5af1feee71099ae2e3b086ec04f57f9950e1be9ecf6c420696fea7977b84738"}, - {file = "coverage-6.3.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e814a4a5a1d95223b08cdb0f4f57029e8eab22ffdbae2f97107aeef28554517e"}, - {file = "coverage-6.3.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:61f4fbf3633cb0713437291b8848634ea97f89c7e849c2be17a665611e433f53"}, - {file = "coverage-6.3.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3401b0d2ed9f726fadbfa35102e00d1b3547b73772a1de5508ef3bdbcb36afe7"}, - {file = "coverage-6.3.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8586b177b4407f988731eb7f41967415b2197f35e2a6ee1a9b9b561f6323c8e9"}, - {file = "coverage-6.3.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:892e7fe32191960da559a14536768a62e83e87bbb867e1b9c643e7e0fbce2579"}, - {file = "coverage-6.3.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:afb03f981fadb5aed1ac6e3dd34f0488e1a0875623d557b6fad09b97a942b38a"}, - {file = "coverage-6.3.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:cbe91bc84be4e5ef0b1480d15c7b18e29c73bdfa33e07d3725da7d18e1b0aff2"}, - {file = "coverage-6.3.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:91502bf27cbd5c83c95cfea291ef387469f2387508645602e1ca0fd8a4ba7548"}, - {file = "coverage-6.3.3-cp38-cp38-win32.whl", hash = "sha256:c488db059848702aff30aa1d90ef87928d4e72e4f00717343800546fdbff0a94"}, - {file = "coverage-6.3.3-cp38-cp38-win_amd64.whl", hash = "sha256:ceb6534fcdfb5c503affb6b1130db7b5bfc8a0f77fa34880146f7a5c117987d0"}, - {file = "coverage-6.3.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:cc692c9ee18f0dd3214843779ba6b275ee4bb9b9a5745ba64265bce911aefd1a"}, - {file = "coverage-6.3.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:462105283de203df8de58a68c1bb4ba2a8a164097c2379f664fa81d6baf94b81"}, - {file = "coverage-6.3.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc972d829ad5ef4d4c5fcabd2bbe2add84ce8236f64ba1c0c72185da3a273130"}, - {file = "coverage-6.3.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:06f54765cdbce99901871d50fe9f41d58213f18e98b170a30ca34f47de7dd5e8"}, - {file = "coverage-6.3.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7835f76a081787f0ca62a53504361b3869840a1620049b56d803a8cb3a9eeea3"}, - {file = "coverage-6.3.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:6f5fee77ec3384b934797f1873758f796dfb4f167e1296dc00f8b2e023ce6ee9"}, - {file = "coverage-6.3.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:baa8be8aba3dd1e976e68677be68a960a633a6d44c325757aefaa4d66175050f"}, - {file = "coverage-6.3.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:4d06380e777dd6b35ee936f333d55b53dc4a8271036ff884c909cf6e94be8b6c"}, - {file = "coverage-6.3.3-cp39-cp39-win32.whl", hash = "sha256:f8cabc5fd0091976ab7b020f5708335033e422de25e20ddf9416bdce2b7e07d8"}, - {file = "coverage-6.3.3-cp39-cp39-win_amd64.whl", hash = "sha256:9c9441d57b0963cf8340268ad62fc83de61f1613034b79c2b1053046af0c5284"}, - {file = "coverage-6.3.3-pp36.pp37.pp38-none-any.whl", hash = "sha256:d522f1dc49127eab0bfbba4e90fa068ecff0899bbf61bf4065c790ddd6c177fe"}, - {file = "coverage-6.3.3.tar.gz", hash = "sha256:2781c43bffbbec2b8867376d4d61916f5e9c4cc168232528562a61d1b4b01879"}, -] -deprecated = [ - {file = "Deprecated-1.2.13-py2.py3-none-any.whl", hash = "sha256:64756e3e14c8c5eea9795d93c524551432a0be75629f8f29e67ab8caf076c76d"}, - {file = "Deprecated-1.2.13.tar.gz", hash = "sha256:43ac5335da90c31c24ba028af536a91d41d53f9e6901ddb021bcc572ce44e38d"}, + {file = "coverage-6.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9b27d894748475fa858f9597c0ee1d4829f44683f3813633aaf94b19cb5453cf"}, + {file = "coverage-6.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:37d1141ad6b2466a7b53a22e08fe76994c2d35a5b6b469590424a9953155afac"}, + {file = "coverage-6.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f9987b0354b06d4df0f4d3e0ec1ae76d7ce7cbca9a2f98c25041eb79eec766f1"}, + {file = "coverage-6.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:26e2deacd414fc2f97dd9f7676ee3eaecd299ca751412d89f40bc01557a6b1b4"}, + {file = "coverage-6.3.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4dd8bafa458b5c7d061540f1ee9f18025a68e2d8471b3e858a9dad47c8d41903"}, + {file = "coverage-6.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:46191097ebc381fbf89bdce207a6c107ac4ec0890d8d20f3360345ff5976155c"}, + {file = "coverage-6.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6f89d05e028d274ce4fa1a86887b071ae1755082ef94a6740238cd7a8178804f"}, + {file = "coverage-6.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:58303469e9a272b4abdb9e302a780072c0633cdcc0165db7eec0f9e32f901e05"}, + {file = "coverage-6.3.2-cp310-cp310-win32.whl", hash = "sha256:2fea046bfb455510e05be95e879f0e768d45c10c11509e20e06d8fcaa31d9e39"}, + {file = "coverage-6.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:a2a8b8bcc399edb4347a5ca8b9b87e7524c0967b335fbb08a83c8421489ddee1"}, + {file = "coverage-6.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:f1555ea6d6da108e1999b2463ea1003fe03f29213e459145e70edbaf3e004aaa"}, + {file = "coverage-6.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e5f4e1edcf57ce94e5475fe09e5afa3e3145081318e5fd1a43a6b4539a97e518"}, + {file = "coverage-6.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7a15dc0a14008f1da3d1ebd44bdda3e357dbabdf5a0b5034d38fcde0b5c234b7"}, + {file = "coverage-6.3.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21b7745788866028adeb1e0eca3bf1101109e2dc58456cb49d2d9b99a8c516e6"}, + {file = "coverage-6.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:8ce257cac556cb03be4a248d92ed36904a59a4a5ff55a994e92214cde15c5bad"}, + {file = "coverage-6.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:b0be84e5a6209858a1d3e8d1806c46214e867ce1b0fd32e4ea03f4bd8b2e3359"}, + {file = "coverage-6.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:acf53bc2cf7282ab9b8ba346746afe703474004d9e566ad164c91a7a59f188a4"}, + {file = "coverage-6.3.2-cp37-cp37m-win32.whl", hash = "sha256:8bdde1177f2311ee552f47ae6e5aa7750c0e3291ca6b75f71f7ffe1f1dab3dca"}, + {file = "coverage-6.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:b31651d018b23ec463e95cf10070d0b2c548aa950a03d0b559eaa11c7e5a6fa3"}, + {file = "coverage-6.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:07e6db90cd9686c767dcc593dff16c8c09f9814f5e9c51034066cad3373b914d"}, + {file = "coverage-6.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2c6dbb42f3ad25760010c45191e9757e7dce981cbfb90e42feef301d71540059"}, + {file = "coverage-6.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c76aeef1b95aff3905fb2ae2d96e319caca5b76fa41d3470b19d4e4a3a313512"}, + {file = "coverage-6.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8cf5cfcb1521dc3255d845d9dca3ff204b3229401994ef8d1984b32746bb45ca"}, + {file = "coverage-6.3.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8fbbdc8d55990eac1b0919ca69eb5a988a802b854488c34b8f37f3e2025fa90d"}, + {file = "coverage-6.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:ec6bc7fe73a938933d4178c9b23c4e0568e43e220aef9472c4f6044bfc6dd0f0"}, + {file = "coverage-6.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:9baff2a45ae1f17c8078452e9e5962e518eab705e50a0aa8083733ea7d45f3a6"}, + {file = "coverage-6.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fd9e830e9d8d89b20ab1e5af09b32d33e1a08ef4c4e14411e559556fd788e6b2"}, + {file = "coverage-6.3.2-cp38-cp38-win32.whl", hash = "sha256:f7331dbf301b7289013175087636bbaf5b2405e57259dd2c42fdcc9fcc47325e"}, + {file = "coverage-6.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:68353fe7cdf91f109fc7d474461b46e7f1f14e533e911a2a2cbb8b0fc8613cf1"}, + {file = "coverage-6.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b78e5afb39941572209f71866aa0b206c12f0109835aa0d601e41552f9b3e620"}, + {file = "coverage-6.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4e21876082ed887baed0146fe222f861b5815455ada3b33b890f4105d806128d"}, + {file = "coverage-6.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:34626a7eee2a3da12af0507780bb51eb52dca0e1751fd1471d0810539cefb536"}, + {file = "coverage-6.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1ebf730d2381158ecf3dfd4453fbca0613e16eaa547b4170e2450c9707665ce7"}, + {file = "coverage-6.3.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd6fe30bd519694b356cbfcaca9bd5c1737cddd20778c6a581ae20dc8c04def2"}, + {file = "coverage-6.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:96f8a1cb43ca1422f36492bebe63312d396491a9165ed3b9231e778d43a7fca4"}, + {file = "coverage-6.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:dd035edafefee4d573140a76fdc785dc38829fe5a455c4bb12bac8c20cfc3d69"}, + {file = "coverage-6.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5ca5aeb4344b30d0bec47481536b8ba1181d50dbe783b0e4ad03c95dc1296684"}, + {file = "coverage-6.3.2-cp39-cp39-win32.whl", hash = "sha256:f5fa5803f47e095d7ad8443d28b01d48c0359484fec1b9d8606d0e3282084bc4"}, + {file = "coverage-6.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:9548f10d8be799551eb3a9c74bbf2b4934ddb330e08a73320123c07f95cc2d92"}, + {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"}, @@ -930,8 +875,8 @@ iniconfig = [ {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, ] invoke = [ - {file = "invoke-1.7.1-py3-none-any.whl", hash = "sha256:2dc975b4f92be0c0a174ad2d063010c8a1fdb5e9389d69871001118b4fcac4fb"}, - {file = "invoke-1.7.1.tar.gz", hash = "sha256:7b6deaf585eee0a848205d0b8c0014b9bf6f287a8eb798818a642dff1df14b19"}, + {file = "invoke-1.7.0-py3-none-any.whl", hash = "sha256:a5159fc63dba6ca2a87a1e33d282b99cea69711b03c64a35bb4e1c53c6c4afa0"}, + {file = "invoke-1.7.0.tar.gz", hash = "sha256:e332e49de40463f2016315f51df42313855772be86435686156bc18f45b5cc6c"}, ] mutagen = [ {file = "mutagen-1.45.1-py3-none-any.whl", hash = "sha256:9c9f243fcec7f410f138cb12c21c84c64fde4195481a30c9bfb05b5f003adfed"}, @@ -1055,8 +1000,8 @@ pycryptodomex = [ {file = "pycryptodomex-3.14.1.tar.gz", hash = "sha256:2ce76ed0081fd6ac8c74edc75b9d14eca2064173af79843c24fa62573263c1f2"}, ] pyparsing = [ - {file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"}, - {file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"}, + {file = "pyparsing-3.0.8-py3-none-any.whl", hash = "sha256:ef7b523f6356f763771559412c0d7134753f037822dad1b16945b7b846f7ad06"}, + {file = "pyparsing-3.0.8.tar.gz", hash = "sha256:7bf433498c016c4314268d95df76c81b842a4cb2b276fa3312cfb1e1d85f6954"}, ] pytest = [ {file = "pytest-7.1.2-py3-none-any.whl", hash = "sha256:13d0e3ccfc2b6e26be000cb6568c832ba67ba32e719443bfe725814d3c42433c"}, @@ -1109,10 +1054,6 @@ pyyaml = [ {file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"}, {file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"}, ] -redis = [ - {file = "redis-4.3.1-py3-none-any.whl", hash = "sha256:84316970995a7adb907a56754d2b92d88fc2d252963dc5ac34c88f0f1a22c25d"}, - {file = "redis-4.3.1.tar.gz", hash = "sha256:94b617b4cd296e94991146f66fc5559756fbefe9493604f0312e4d3298ac63e9"}, -] requests = [ {file = "requests-2.27.1-py2.py3-none-any.whl", hash = "sha256:f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d"}, {file = "requests-2.27.1.tar.gz", hash = "sha256:68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61"}, @@ -1120,9 +1061,8 @@ requests = [ rfeed = [ {file = "rfeed-1.1.1.tar.gz", hash = "sha256:aa9506f2866b74f5a322d394a14a63c19a6825c2d94755ff19d46dd1e2434819"}, ] -rq = [ - {file = "rq-1.10.1-py2.py3-none-any.whl", hash = "sha256:92f4cf38b2364c1697b541e77c0fe62b7e5242fa864324f262be126ee2a07e3a"}, - {file = "rq-1.10.1.tar.gz", hash = "sha256:62d06b44c3acfa5d1933c5a4ec3fbc2484144a8af60e318d0b8447c5236271e2"}, +scrapetube = [ + {file = "scrapetube-2.2.2-py3-none-any.whl", hash = "sha256:73aef77d42aa182bcd3cc7f9ebee28bc01d6b34d615d205679ebc54be1f9807f"}, ] sgmllib3k = [ {file = "sgmllib3k-1.0.0.tar.gz", hash = "sha256:7868fb1c8bfa764c1ac563d3cf369c381d1325d36124933a726f29fcdaa812e9"}, @@ -1131,9 +1071,6 @@ six = [ {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, ] -slugify = [ - {file = "slugify-0.0.1.tar.gz", hash = "sha256:c5703cc11c1a6947536f3ce8bb306766b8bb5a84a53717f5a703ce0f18235e4c"}, -] sqlparse = [ {file = "sqlparse-0.4.2-py3-none-any.whl", hash = "sha256:48719e356bb8b42991bdbb1e8b83223757b93789c00910a616a071910ca4a64d"}, {file = "sqlparse-0.4.2.tar.gz", hash = "sha256:0c00730c74263a94e5a9919ade150dfc3b19c574389985446148402998287dae"}, @@ -1146,6 +1083,10 @@ tomli = [ {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, ] +typing-extensions = [ + {file = "typing_extensions-4.2.0-py3-none-any.whl", hash = "sha256:6657594ee297170d19f67d55c05852a874e7eb634f4f753dbd667855e07c1708"}, + {file = "typing_extensions-4.2.0.tar.gz", hash = "sha256:f1c24655a0da0d1b67f07e17a5e6b2a105894e6824b92096378bb3668ef02376"}, +] tzdata = [ {file = "tzdata-2022.1-py2.py3-none-any.whl", hash = "sha256:238e70234214138ed7b4e8a0fab0e5e13872edab3be586ab8198c407620e2ab9"}, {file = "tzdata-2022.1.tar.gz", hash = "sha256:8b536a8ec63dc0751342b3984193a3118f8fca2afe25752bb9b7fffd398552d3"}, @@ -1211,72 +1152,6 @@ websockets = [ {file = "websockets-10.3-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:3eda1cb7e9da1b22588cefff09f0951771d6ee9fa8dbe66f5ae04cc5f26b2b55"}, {file = "websockets-10.3.tar.gz", hash = "sha256:fc06cc8073c8e87072138ba1e431300e2d408f054b27047d047b549455066ff4"}, ] -wrapt = [ - {file = "wrapt-1.14.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:1b376b3f4896e7930f1f772ac4b064ac12598d1c38d04907e696cc4d794b43d3"}, - {file = "wrapt-1.14.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:903500616422a40a98a5a3c4ff4ed9d0066f3b4c951fa286018ecdf0750194ef"}, - {file = "wrapt-1.14.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:5a9a0d155deafd9448baff28c08e150d9b24ff010e899311ddd63c45c2445e28"}, - {file = "wrapt-1.14.1-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:ddaea91abf8b0d13443f6dac52e89051a5063c7d014710dcb4d4abb2ff811a59"}, - {file = "wrapt-1.14.1-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:36f582d0c6bc99d5f39cd3ac2a9062e57f3cf606ade29a0a0d6b323462f4dd87"}, - {file = "wrapt-1.14.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:7ef58fb89674095bfc57c4069e95d7a31cfdc0939e2a579882ac7d55aadfd2a1"}, - {file = "wrapt-1.14.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:e2f83e18fe2f4c9e7db597e988f72712c0c3676d337d8b101f6758107c42425b"}, - {file = "wrapt-1.14.1-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:ee2b1b1769f6707a8a445162ea16dddf74285c3964f605877a20e38545c3c462"}, - {file = "wrapt-1.14.1-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:833b58d5d0b7e5b9832869f039203389ac7cbf01765639c7309fd50ef619e0b1"}, - {file = "wrapt-1.14.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:80bb5c256f1415f747011dc3604b59bc1f91c6e7150bd7db03b19170ee06b320"}, - {file = "wrapt-1.14.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:07f7a7d0f388028b2df1d916e94bbb40624c59b48ecc6cbc232546706fac74c2"}, - {file = "wrapt-1.14.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:02b41b633c6261feff8ddd8d11c711df6842aba629fdd3da10249a53211a72c4"}, - {file = "wrapt-1.14.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2fe803deacd09a233e4762a1adcea5db5d31e6be577a43352936179d14d90069"}, - {file = "wrapt-1.14.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:257fd78c513e0fb5cdbe058c27a0624c9884e735bbd131935fd49e9fe719d310"}, - {file = "wrapt-1.14.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4fcc4649dc762cddacd193e6b55bc02edca674067f5f98166d7713b193932b7f"}, - {file = "wrapt-1.14.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:11871514607b15cfeb87c547a49bca19fde402f32e2b1c24a632506c0a756656"}, - {file = "wrapt-1.14.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8ad85f7f4e20964db4daadcab70b47ab05c7c1cf2a7c1e51087bfaa83831854c"}, - {file = "wrapt-1.14.1-cp310-cp310-win32.whl", hash = "sha256:a9a52172be0b5aae932bef82a79ec0a0ce87288c7d132946d645eba03f0ad8a8"}, - {file = "wrapt-1.14.1-cp310-cp310-win_amd64.whl", hash = "sha256:6d323e1554b3d22cfc03cd3243b5bb815a51f5249fdcbb86fda4bf62bab9e164"}, - {file = "wrapt-1.14.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:43ca3bbbe97af00f49efb06e352eae40434ca9d915906f77def219b88e85d907"}, - {file = "wrapt-1.14.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:6b1a564e6cb69922c7fe3a678b9f9a3c54e72b469875aa8018f18b4d1dd1adf3"}, - {file = "wrapt-1.14.1-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:00b6d4ea20a906c0ca56d84f93065b398ab74b927a7a3dbd470f6fc503f95dc3"}, - {file = "wrapt-1.14.1-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:a85d2b46be66a71bedde836d9e41859879cc54a2a04fad1191eb50c2066f6e9d"}, - {file = "wrapt-1.14.1-cp35-cp35m-win32.whl", hash = "sha256:dbcda74c67263139358f4d188ae5faae95c30929281bc6866d00573783c422b7"}, - {file = "wrapt-1.14.1-cp35-cp35m-win_amd64.whl", hash = "sha256:b21bb4c09ffabfa0e85e3a6b623e19b80e7acd709b9f91452b8297ace2a8ab00"}, - {file = "wrapt-1.14.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:9e0fd32e0148dd5dea6af5fee42beb949098564cc23211a88d799e434255a1f4"}, - {file = "wrapt-1.14.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9736af4641846491aedb3c3f56b9bc5568d92b0692303b5a305301a95dfd38b1"}, - {file = "wrapt-1.14.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5b02d65b9ccf0ef6c34cba6cf5bf2aab1bb2f49c6090bafeecc9cd81ad4ea1c1"}, - {file = "wrapt-1.14.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21ac0156c4b089b330b7666db40feee30a5d52634cc4560e1905d6529a3897ff"}, - {file = "wrapt-1.14.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:9f3e6f9e05148ff90002b884fbc2a86bd303ae847e472f44ecc06c2cd2fcdb2d"}, - {file = "wrapt-1.14.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:6e743de5e9c3d1b7185870f480587b75b1cb604832e380d64f9504a0535912d1"}, - {file = "wrapt-1.14.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:d79d7d5dc8a32b7093e81e97dad755127ff77bcc899e845f41bf71747af0c569"}, - {file = "wrapt-1.14.1-cp36-cp36m-win32.whl", hash = "sha256:81b19725065dcb43df02b37e03278c011a09e49757287dca60c5aecdd5a0b8ed"}, - {file = "wrapt-1.14.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b014c23646a467558be7da3d6b9fa409b2c567d2110599b7cf9a0c5992b3b471"}, - {file = "wrapt-1.14.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:88bd7b6bd70a5b6803c1abf6bca012f7ed963e58c68d76ee20b9d751c74a3248"}, - {file = "wrapt-1.14.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5901a312f4d14c59918c221323068fad0540e34324925c8475263841dbdfe68"}, - {file = "wrapt-1.14.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d77c85fedff92cf788face9bfa3ebaa364448ebb1d765302e9af11bf449ca36d"}, - {file = "wrapt-1.14.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d649d616e5c6a678b26d15ece345354f7c2286acd6db868e65fcc5ff7c24a77"}, - {file = "wrapt-1.14.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7d2872609603cb35ca513d7404a94d6d608fc13211563571117046c9d2bcc3d7"}, - {file = "wrapt-1.14.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:ee6acae74a2b91865910eef5e7de37dc6895ad96fa23603d1d27ea69df545015"}, - {file = "wrapt-1.14.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:2b39d38039a1fdad98c87279b48bc5dce2c0ca0d73483b12cb72aa9609278e8a"}, - {file = "wrapt-1.14.1-cp37-cp37m-win32.whl", hash = "sha256:60db23fa423575eeb65ea430cee741acb7c26a1365d103f7b0f6ec412b893853"}, - {file = "wrapt-1.14.1-cp37-cp37m-win_amd64.whl", hash = "sha256:709fe01086a55cf79d20f741f39325018f4df051ef39fe921b1ebe780a66184c"}, - {file = "wrapt-1.14.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8c0ce1e99116d5ab21355d8ebe53d9460366704ea38ae4d9f6933188f327b456"}, - {file = "wrapt-1.14.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e3fb1677c720409d5f671e39bac6c9e0e422584e5f518bfd50aa4cbbea02433f"}, - {file = "wrapt-1.14.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:642c2e7a804fcf18c222e1060df25fc210b9c58db7c91416fb055897fc27e8cc"}, - {file = "wrapt-1.14.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7b7c050ae976e286906dd3f26009e117eb000fb2cf3533398c5ad9ccc86867b1"}, - {file = "wrapt-1.14.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef3f72c9666bba2bab70d2a8b79f2c6d2c1a42a7f7e2b0ec83bb2f9e383950af"}, - {file = "wrapt-1.14.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:01c205616a89d09827986bc4e859bcabd64f5a0662a7fe95e0d359424e0e071b"}, - {file = "wrapt-1.14.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:5a0f54ce2c092aaf439813735584b9537cad479575a09892b8352fea5e988dc0"}, - {file = "wrapt-1.14.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2cf71233a0ed05ccdabe209c606fe0bac7379fdcf687f39b944420d2a09fdb57"}, - {file = "wrapt-1.14.1-cp38-cp38-win32.whl", hash = "sha256:aa31fdcc33fef9eb2552cbcbfee7773d5a6792c137b359e82879c101e98584c5"}, - {file = "wrapt-1.14.1-cp38-cp38-win_amd64.whl", hash = "sha256:d1967f46ea8f2db647c786e78d8cc7e4313dbd1b0aca360592d8027b8508e24d"}, - {file = "wrapt-1.14.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3232822c7d98d23895ccc443bbdf57c7412c5a65996c30442ebe6ed3df335383"}, - {file = "wrapt-1.14.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:988635d122aaf2bdcef9e795435662bcd65b02f4f4c1ae37fbee7401c440b3a7"}, - {file = "wrapt-1.14.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cca3c2cdadb362116235fdbd411735de4328c61425b0aa9f872fd76d02c4e86"}, - {file = "wrapt-1.14.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d52a25136894c63de15a35bc0bdc5adb4b0e173b9c0d07a2be9d3ca64a332735"}, - {file = "wrapt-1.14.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40e7bc81c9e2b2734ea4bc1aceb8a8f0ceaac7c5299bc5d69e37c44d9081d43b"}, - {file = "wrapt-1.14.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b9b7a708dd92306328117d8c4b62e2194d00c365f18eff11a9b53c6f923b01e3"}, - {file = "wrapt-1.14.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:6a9a25751acb379b466ff6be78a315e2b439d4c94c1e99cb7266d40a537995d3"}, - {file = "wrapt-1.14.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:34aa51c45f28ba7f12accd624225e2b1e5a3a45206aa191f6f9aac931d9d56fe"}, - {file = "wrapt-1.14.1-cp39-cp39-win32.whl", hash = "sha256:dee0ce50c6a2dd9056c20db781e9c1cfd33e77d2d569f5d1d9321c641bb903d5"}, - {file = "wrapt-1.14.1-cp39-cp39-win_amd64.whl", hash = "sha256:dee60e1de1898bde3b238f18340eec6148986da0455d8ba7848d50470a7a32fb"}, - {file = "wrapt-1.14.1.tar.gz", hash = "sha256:380a85cf89e0e69b7cfbe2ea9f765f004ff419f34194018a6827ac0e3edfed4d"}, -] yt-dlp = [ {file = "yt-dlp-2022.4.8.tar.gz", hash = "sha256:8758d016509d4574b90fbde975aa70adaef71ed5e7a195141588f6d6945205ba"}, {file = "yt_dlp-2022.4.8-py2.py3-none-any.whl", hash = "sha256:6edefe326b1e1478fdbe627a66203e5248a6b0dd50c101e682cf700ab70cdf72"}, diff --git a/pyproject.toml b/pyproject.toml index e7115ac..d783024 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,7 +12,7 @@ packages = [ python = "^3.10" Django = "^4.0.4" yt-dlp = "^2022.3.8" -requests = "^2.27.1" +scrapetube = "^2.2.2" rfeed = "^1.1.1" feedparser = "^6.0.8" Pillow = "^9.1.0" @@ -20,13 +20,11 @@ colorthief = "^0.2.1" wcag-contrast-ratio = "^0.9" font-source-sans-pro = "^0.0.1" fonts = "^0.0.3" +bordercrop = "^1.0.0" django-bulma = "^0.8.3" python-dotenv = "^0.20.0" psycopg2 = "^2.9.3" mysqlclient = "^2.1.0" -slugify = "^0.0.1" -rq = "^1.10.1" -mutagen = "^1.45.1" [tool.poetry.dev-dependencies] pytest = "^7.1.1" @@ -43,7 +41,10 @@ requires = ["poetry-core>=1.0.0"] build-backend = "poetry.core.masonry.api" [tool.flake8] -extend-ignore = "E501" +max-line-length = 88 +per-file-ignores = [ + "settings.py:E501", +] [tool.black] line-length = 88 diff --git a/tasks.py b/tasks.py index e699f29..3c9a2df 100644 --- a/tasks.py +++ b/tasks.py @@ -1,5 +1,4 @@ import os -import shutil from pathlib import Path from invoke import Responder, task @@ -9,11 +8,6 @@ from ucast.service import cover, util, youtube os.chdir(Path(__file__).absolute().parent) -DIR_RUN = Path("_run").absolute() -DIR_STATIC = DIR_RUN / "static" -DIR_DOWNLOAD = DIR_RUN / "data" -FILE_DB = DIR_RUN / "db.sqlite" - @task def test(c): @@ -72,46 +66,35 @@ def get_cover(c, vid=""): The images are stored in the ``ucast/tests/testfiles`` directory. """ - vinfo = youtube.get_video_details(vid) + vinfo = youtube.get_video_info(vid) title = vinfo.title channel_name = vinfo.channel_name - channel_id = vinfo.channel_id - channel_metadata = youtube.get_channel_metadata( - youtube.channel_url_from_id(channel_id) - ) + channel_url = vinfo.channel_url + channel_metadata = youtube.get_channel_metadata(channel_url) ti = 1 - while os.path.exists(tests.DIR_TESTFILES / "avatar" / f"a{ti}.jpg"): + while os.path.exists(tests.DIR_TESTFILES / "cover" / f"c{ti}_classic.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}_gradient.png" + cv_file = tests.DIR_TESTFILES / "cover" / f"c{ti}_classic.png" cv_blur_file = tests.DIR_TESTFILES / "cover" / f"c{ti}_blur.png" - tn_file = youtube.download_thumbnail(vinfo, tn_file) + youtube.download_thumbnail(vinfo, tn_file) util.download_file(channel_metadata.avatar_url, av_file) cover.create_cover_file( - tn_file, av_file, title, channel_name, cover.COVER_STYLE_GRADIENT, 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.COVER_STYLE_BLUR, cv_blur_file + tn_file, av_file, title, channel_name, cover.CoverStyle.BLUR, cv_blur_file ) @task def build_devcontainer(c): c.run( - "docker buildx build -t thetadev256/ucast-dev --push --platform amd64,arm64,armhf -f deploy/Devcontainer.Dockerfile deploy" + "docker buildx build -t thetadev256/ucast-dev --push \ +--platform amd64,arm64,armhf -f deploy/Devcontainer.Dockerfile deploy" ) - - -@task -def reset(c): - if DIR_DOWNLOAD.exists(): - shutil.rmtree(DIR_DOWNLOAD) - if FILE_DB.exists(): - os.remove(FILE_DB) - migrate(c) - create_testuser(c) diff --git a/ucast/migrations/0001_initial.py b/ucast/migrations/0001_initial.py index 014360e..d9d9bd5 100644 --- a/ucast/migrations/0001_initial.py +++ b/ucast/migrations/0001_initial.py @@ -1,6 +1,5 @@ # Generated by Django 4.0.4 on 2022-05-05 00:02 -import django.db.models.deletion from django.db import migrations, models @@ -19,8 +18,6 @@ class Migration(migrations.Migration): models.CharField(max_length=30, primary_key=True, serialize=False), ), ("name", models.CharField(max_length=100)), - ("slug", models.CharField(max_length=100)), - ("description", models.TextField()), ("active", models.BooleanField(default=True)), ("skip_livestreams", models.BooleanField(default=True)), ("skip_shorts", models.BooleanField(default=True)), @@ -36,18 +33,9 @@ class Migration(migrations.Migration): ), ("title", models.CharField(max_length=200)), ("slug", models.CharField(max_length=209)), - ( - "channel", - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, to="ucast.channel" - ), - ), ("published", models.DateTimeField()), ("downloaded", models.DateTimeField(null=True)), ("description", models.TextField()), - ("duration", models.IntegerField()), - ("is_livestream", models.BooleanField(default=False)), - ("is_short", models.BooleanField(default=False)), ], ), ] diff --git a/ucast/models.py b/ucast/models.py index f45b3cf..7f6711e 100644 --- a/ucast/models.py +++ b/ucast/models.py @@ -1,63 +1,19 @@ -import datetime - from django.db import models -from ucast.service import util - - -def _get_unique_slug( - str_in: str, objects: models.query.QuerySet, model_name: str -) -> str: - """ - Get a new, unique slug for a database item - - :param str_in: Input string to slugify - :param objects: Django query set - :return: Slug - """ - original_slug = util.get_slug(str_in) - slug = original_slug - - for i in range(1, objects.count() + 2): - if not objects.filter(slug=slug).exists(): - return slug - - slug = f"{original_slug}_{i}" - - raise Exception(f"unique {model_name} slug for {original_slug} could not be found") - class Channel(models.Model): id = models.CharField(max_length=30, primary_key=True) name = models.CharField(max_length=100) - slug = models.CharField(max_length=100) - description = models.TextField() active = models.BooleanField(default=True) skip_livestreams = models.BooleanField(default=True) skip_shorts = models.BooleanField(default=True) keep_videos = models.IntegerField(null=True, default=None) - @classmethod - def get_new_slug(cls, name: str) -> str: - return _get_unique_slug(name, cls.objects, "channel") - class Video(models.Model): id = models.CharField(max_length=30, primary_key=True) title = models.CharField(max_length=200) slug = models.CharField(max_length=209) - channel = models.ForeignKey(Channel, on_delete=models.CASCADE) published = models.DateTimeField() downloaded = models.DateTimeField(null=True) description = models.TextField() - duration = models.IntegerField() - is_livestream = models.BooleanField(default=False) - is_short = models.BooleanField(default=False) - - @classmethod - def get_new_slug(cls, title: str, date: datetime.date, channel_id: str) -> str: - title_w_date = f"{date.strftime('%Y%m%d')}_{title}" - - return _get_unique_slug( - title_w_date, cls.objects.filter(channel_id=channel_id), "video" - ) diff --git a/ucast/service/cover.py b/ucast/service/cover.py index 670268c..5e6444d 100644 --- a/ucast/service/cover.py +++ b/ucast/service/cover.py @@ -1,23 +1,26 @@ +import enum import math from importlib import resources from pathlib import Path -from typing import List, Literal, Optional, Tuple +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 ucast.service import typ -COVER_STYLE_BLUR = "blur" -COVER_STYLE_GRADIENT = "gradient" -CoverStyle = Literal["blur", "gradient"] - 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]: @@ -177,23 +180,22 @@ def _get_baseimage( """ cover = Image.new("RGB", (COVER_WIDTH, COVER_WIDTH)) - if style == COVER_STYLE_GRADIENT: - # Thumbnail with color gradient background + if style == CoverStyle.BLUR: + ctn_width = int(COVER_WIDTH / thumbnail.height * thumbnail.width) + ctn_x_left = int((ctn_width - COVER_WIDTH) / 2) + + ctn = thumbnail.resize( + (ctn_width, COVER_WIDTH), Image.Resampling.LANCZOS + ).filter(ImageFilter.GaussianBlur(20)) + cover.paste(ctn, (-ctn_x_left, 0)) + else: 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) - else: - # Thumbnail with blurred background - ctn_width = int(COVER_WIDTH / thumbnail.height * thumbnail.width) - ctn_x_left = int((ctn_width - COVER_WIDTH) / 2) - - ctn = thumbnail.resize((ctn_width, COVER_WIDTH), Image.LANCZOS).filter( - ImageFilter.GaussianBlur(20) - ) - cover.paste(ctn, (-ctn_x_left, 0)) return cover @@ -205,6 +207,12 @@ def _resize_thumbnail(thumbnail: Image.Image) -> Image.Image: :param thumbnail: Thumbnail image object :return: Resized thumbnail image object """ + 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) @@ -212,9 +220,9 @@ def _resize_thumbnail(thumbnail: Image.Image) -> Image.Image: tn_crop_y_top = int((tn_resize_height - tn_height) / 2) tn_crop_y_bottom = tn_resize_height - tn_crop_y_top - return thumbnail.resize((COVER_WIDTH, tn_resize_height), Image.LANCZOS).crop( - (0, tn_crop_y_top, COVER_WIDTH, tn_crop_y_bottom) - ) + return thumbnail.resize( + (COVER_WIDTH, tn_resize_height), Image.Resampling.LANCZOS + ).crop((0, tn_crop_y_top, COVER_WIDTH, tn_crop_y_bottom)) def _draw_text_avatar( @@ -238,7 +246,7 @@ def _draw_text_avatar( avt_margin = int(tn_16_9_margin * 0.05) avt_size = tn_16_9_margin - 2 * avt_margin - avt = avatar.resize((avt_size, avt_size), Image.LANCZOS) + avt = avatar.resize((avt_size, avt_size), Image.Resampling.LANCZOS) circle_mask = Image.new("L", (avt_size, avt_size)) circle_mask_draw = ImageDraw.Draw(circle_mask) diff --git a/ucast/service/scrapetube.py b/ucast/service/scrapetube.py deleted file mode 100644 index d6b5b29..0000000 --- a/ucast/service/scrapetube.py +++ /dev/null @@ -1,244 +0,0 @@ -""" -Based on the scrapetube package from dermasmid (MIT License) -https://github.com/dermasmid/scrapetube -""" -import json -import time -from typing import Generator, Literal, Optional - -import requests - - -def get_channel( - channel_url: str, - limit: int = None, - sleep: int = 1, - sort_by: Literal["newest", "oldest", "popular"] = "newest", -) -> Generator[dict, None, None]: - """ - Get videos for a channel. - - :param channel_url: The url of the channel you want to get the videos for. - :param limit: Limit the number of videos you want to get. - :param sleep: Seconds to sleep between API calls to youtube, in order to prevent - getting blocked. Defaults to ``1``. - :param sort_by: In what order to retrive to videos. Pass one of the following values. - ``"newest"``: Get the new videos first. - ``"oldest"``: Get the old videos first. - ``"popular"``: Get the popular videos first. - Defaults to ``"newest"``. - :return: Generator providing the videos - """ - - sort_by_map = {"newest": "dd", "oldest": "da", "popular": "p"} - url = "{url}/videos?view=0&sort={sort_by}&flow=grid".format( - url=channel_url, - sort_by=sort_by_map[sort_by], - ) - api_endpoint = "https://www.youtube.com/youtubei/v1/browse" - videos = _get_videos(url, api_endpoint, "gridVideoRenderer", limit, sleep) - for video in videos: - yield video - - -def get_channel_metadata(channel_url: str) -> dict: - """ - Get metadata of a channel. - - :param channel_url: Channel URL - :return: Raw channel metadata - """ - session = _new_session() - - url = f"{channel_url}/videos?view=0&flow=grid" - - html = _get_initial_data(session, url) - return json.loads(_get_json_from_html(html, "var ytInitialData = ", 0, "};") + "}") - - -def get_playlist( - playlist_id: str, limit: int = None, sleep: int = 1 -) -> Generator[dict, None, None]: - """ - Get videos for a playlist. - - :param playlist_id: The playlist id from the playlist you want to get the videos for. - :param limit: Limit the number of videos you want to get. - :param sleep: Seconds to sleep between API calls to youtube, in order to prevent - getting blocked. Defaults to ``1``. - :return: Generator providing the videos - """ - - url = f"https://www.youtube.com/playlist?list={playlist_id}" - api_endpoint = "https://www.youtube.com/youtubei/v1/browse" - videos = _get_videos(url, api_endpoint, "playlistVideoRenderer", limit, sleep) - for video in videos: - yield video - - -def get_search( - query: str, - limit: int = None, - sleep: int = 1, - sort_by: Literal["relevance", "upload_date", "view_count", "rating"] = "relevance", - results_type: Literal["video", "channel", "playlist", "movie"] = "video", -) -> Generator[dict, None, None]: - """ - Search youtube and get videos. - - :param query: The term you want to search for. - :param limit: Limit the number of videos you want to get. - :param sleep: Seconds to sleep between API calls to youtube, in order to prevent - getting blocked. Defaults to ``1``. - :param sort_by: In what order to retrive to videos. Pass one of the following values. - ``"relevance"``: Get the new videos in order of relevance. - ``"upload_date"``: Get the new videos first. - ``"view_count"``: Get the popular videos first. - ``"rating"``: Get videos with more likes first. - Defaults to ``"relevance"``. - :param results_type: What type you want to search for. - Pass one of the following values: ``"video"|"channel"| - "playlist"|"movie"``. Defaults to ``"video"``. - :return: Generator providing the videos - """ - - sort_by_map = { - "relevance": "A", - "upload_date": "I", - "view_count": "M", - "rating": "E", - } - - results_type_map = { - "video": ["B", "videoRenderer"], - "channel": ["C", "channelRenderer"], - "playlist": ["D", "playlistRenderer"], - "movie": ["E", "videoRenderer"], - } - - param_string = f"CA{sort_by_map[sort_by]}SAhA{results_type_map[results_type][0]}" - url = f"https://www.youtube.com/results?search_query={query}&sp={param_string}" - api_endpoint = "https://www.youtube.com/youtubei/v1/search" - videos = _get_videos( - url, api_endpoint, results_type_map[results_type][1], limit, sleep - ) - for video in videos: - yield video - - -def _get_videos( - url: str, api_endpoint: str, selector: str, limit: int, sleep: int -) -> Generator[dict, None, None]: - session = _new_session() - is_first = True - quit = False - count = 0 - while True: - if is_first: - html = _get_initial_data(session, url) - client = json.loads( - _get_json_from_html(html, "INNERTUBE_CONTEXT", 2, '"}},') + '"}}' - )["client"] - api_key = _get_json_from_html(html, "innertubeApiKey", 3) - session.headers["X-YouTube-Client-Name"] = "1" - session.headers["X-YouTube-Client-Version"] = client["clientVersion"] - data = json.loads( - _get_json_from_html(html, "var ytInitialData = ", 0, "};") + "}" - ) - next_data = _get_next_data(data) - is_first = False - else: - data = _get_ajax_data(session, api_endpoint, api_key, next_data, client) - next_data = _get_next_data(data) - for result in _get_videos_items(data, selector): - try: - count += 1 - yield result - if count == limit: - quit = True - break - except GeneratorExit: - quit = True - break - - if not next_data or quit: - break - - time.sleep(sleep) - - session.close() - - -def _new_session() -> requests.Session: - session = requests.Session() - session.headers[ - "User-Agent" - ] = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.101 Safari/537.36" - session.headers["Accept-Language"] = "en" - return session - - -def _get_initial_data(session: requests.Session, url: str) -> str: - response = session.get(url) - response.raise_for_status() - - if "uxe=" in response.request.url: - session.cookies.set("CONSENT", "YES+cb", domain=".youtube.com") - response = session.get(url) - - html = response.text - return html - - -def _get_ajax_data( - session: requests.Session, - api_endpoint: str, - api_key: str, - next_data: dict, - client: dict, -) -> dict: - data = { - "context": {"clickTracking": next_data["click_params"], "client": client}, - "continuation": next_data["token"], - } - response = session.post(api_endpoint, params={"key": api_key}, json=data) - return response.json() - - -def _get_json_from_html( - html: str, key: str, num_chars: int = 2, stop: str = '"' -) -> str: - pos_begin = html.find(key) + len(key) + num_chars - pos_end = html.find(stop, pos_begin) - return html[pos_begin:pos_end] - - -def _get_next_data(data: dict) -> Optional[dict]: - raw_next_data = next(_search_dict(data, "continuationEndpoint"), None) - if not raw_next_data: - return None - next_data = { - "token": raw_next_data["continuationCommand"]["token"], - "click_params": {"clickTrackingParams": raw_next_data["clickTrackingParams"]}, - } - - return next_data - - -def _search_dict(partial: dict, search_key: str) -> Generator[dict, None, None]: - stack = [partial] - while stack: - current_item = stack.pop(0) - if isinstance(current_item, dict): - for key, value in current_item.items(): - if key == search_key: - yield value - else: - stack.append(value) - elif isinstance(current_item, list): - for value in current_item: - stack.append(value) - - -def _get_videos_items(data: dict, selector: str) -> Generator[dict, None, None]: - return _search_dict(data, selector) diff --git a/ucast/service/storage.py b/ucast/service/storage.py deleted file mode 100644 index a8e41b7..0000000 --- a/ucast/service/storage.py +++ /dev/null @@ -1,79 +0,0 @@ -import os -from pathlib import Path -from typing import Tuple - -import slugify -from django.conf import settings - -UCAST_DIRNAME = "_ucast" - - -def _get_slug(str_in: str) -> str: - return slugify.slugify(str_in, lowercase=False, separator="_") - - -def _get_unique_slug(str_in: str, root_dir: Path, extension="") -> Tuple[Path, str]: - original_slug = _get_slug(str_in) - slug = original_slug - i = 0 - - while True: - testfile = root_dir / (slug + extension) - - if not testfile.exists(): - return testfile, slug - - i += 1 - slug = f"{original_slug}_{i}" - - -class ChannelFolder: - def __init__(self, dir_root: Path): - self.dir_root = dir_root - dir_ucast = self.dir_root / UCAST_DIRNAME - - self.file_avatar = dir_ucast / "avatar.jpg" - self.file_avatar_sm = dir_ucast / "avatar_sm.webp" - - self.dir_covers = dir_ucast / "covers" - self.dir_thumbnails = dir_ucast / "thumbnails" - - @staticmethod - def _glob_file(parent_dir: Path, glob: str, default_filename: str = None) -> Path: - try: - return parent_dir.glob(glob).__next__() - except StopIteration: - if default_filename: - return parent_dir / default_filename - raise FileNotFoundError(f"file {str(parent_dir)}/{glob} not found") - - def does_exist(self) -> bool: - return os.path.isdir(self.dir_covers) - - def create(self): - os.makedirs(self.dir_covers, exist_ok=True) - os.makedirs(self.dir_thumbnails, exist_ok=True) - - def get_cover(self, title_slug: str) -> Path: - return self.dir_covers / f"{title_slug}.png" - - def get_thumbnail(self, title_slug: str, sm=False) -> Path: - filename = title_slug - if sm: - filename += "_sm" - - return self._glob_file(self.dir_thumbnails, f"{filename}.*", f"{filename}.webp") - - def get_audio(self, title_slug: str) -> Path: - return self.dir_root / f"{title_slug}.mp3" - - -class Storage: - def __init__(self): - self.dir_data = settings.DOWNLOAD_ROOT - - def get_channel_folder(self, channel_slug: str) -> ChannelFolder: - cf = ChannelFolder(self.dir_data / channel_slug) - if not cf.does_exist(): - cf.create() - return cf diff --git a/ucast/service/util.py b/ucast/service/util.py index a918721..011bedd 100644 --- a/ucast/service/util.py +++ b/ucast/service/util.py @@ -1,47 +1,7 @@ -import shutil -from pathlib import Path - import requests -import slugify -from PIL import Image - -AVATAR_SM_WIDTH = 100 -THUMBNAIL_SM_WIDTH = 360 -def download_file(url: str, download_path: Path): +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) - - -def download_image_file(url: str, download_path: Path) -> Path: - download_file(url, download_path) - img = Image.open(download_path) - img_ext = img.format.lower() - img.close() - - if img_ext == "jpeg": - img_ext = "jpg" - - new_path = download_path.with_suffix("." + img_ext) - shutil.move(download_path, new_path) - return new_path - - -def resize_avatar(original_file: Path, new_file: Path): - avatar = Image.open(original_file) - avatar_new_height = int(AVATAR_SM_WIDTH / avatar.width * avatar.height) - avatar = avatar.resize((AVATAR_SM_WIDTH, avatar_new_height), Image.LANCZOS) - avatar.save(new_file) - - -def resize_thumbnail(original_file: Path, new_file: Path): - thumbnail = Image.open(original_file) - tn_new_height = int(THUMBNAIL_SM_WIDTH / thumbnail.width * thumbnail.height) - thumbnail = thumbnail.resize((THUMBNAIL_SM_WIDTH, tn_new_height), Image.LANCZOS) - thumbnail.save(new_file) - - -def get_slug(str_in: str) -> str: - return slugify.slugify(str_in, lowercase=False, separator="_") diff --git a/ucast/service/youtube.py b/ucast/service/youtube.py index 04df14c..b02a3cd 100644 --- a/ucast/service/youtube.py +++ b/ucast/service/youtube.py @@ -1,139 +1,94 @@ -import datetime -import logging -import re -import shutil +import json from dataclasses import dataclass +from datetime import datetime from operator import itemgetter -from pathlib import Path -from typing import List, Optional -import feedparser import requests -from django.conf import settings -from mutagen import id3 +from scrapetube import scrapetube from yt_dlp import YoutubeDL -from ucast.service import scrapetube, util +from ucast.service import util -class ItemNotFoundError(Exception): - pass +class VideoInfo: + """Mapping of YoutubeDL's video information""" + + def __init__(self, info: dict): + self._info = info + + self.id = info["id"] + self.title = info["title"] + self.description = info["description"] + self.channel_id = info["channel_id"] + self.channel_name = info["uploader"] + self.duration = info["duration"] + self.published = self.__approx_published_time( + datetime.strptime(info["upload_date"], "%Y%m%d") + ) + self.thumbnails = info["thumbnails"] + self.is_currently_live = bool(info.get("is_live")) + self.is_livestream = info.get("is_live") or info.get("was_live") + self.is_short = self.duration <= 60 and info["width"] < info["height"] + + @staticmethod + def __approx_published_time(time_in: datetime) -> datetime: + """ + Assume that a video published on the current day is published now. + Eventually add an option to get the exact upload time from Google's API. + + :param time_in: + :return: + """ + now = datetime.now() + if time_in.date() == now.date(): + return now + return time_in + + def __str__(self): + return f"{self.title} ({self.id})" class ThumbnailNotFoundError(Exception): pass -@dataclass -class VideoScraped: - """ - Video object, as it is scraped from the website/rss feed. - RSS feeds contain the second-accurate publishing date, which cannot - be scraped from the video info and is therefore included in this object. - """ - - id: str - published: Optional[datetime.datetime] - - def __str__(self): - return self.id - - -@dataclass -class VideoDetails: - """Mapping of YoutubeDL's video information""" - - id: str - title: str - description: str - channel_id: str - channel_name: str - duration: int - published: datetime.datetime - thumbnails: List[dict] - is_currently_live: bool - is_livestream: bool - is_short: bool - - @classmethod - def from_vinfo(cls, info: dict): - published_date = datetime.datetime.strptime( - info["upload_date"], "%Y%m%d" - ).replace(tzinfo=datetime.timezone.utc) - - return VideoDetails( - id=info["id"], - title=info["title"], - description=info["description"], - channel_id=info["channel_id"], - channel_name=info["uploader"], - duration=info["duration"], - published=published_date, - thumbnails=info["thumbnails"], - is_currently_live=bool(info.get("is_live")), - is_livestream=info.get("is_live") or info.get("was_live"), - is_short=info["duration"] <= 60 and info["width"] < info["height"], - ) - - def add_scraped_data(self, scraped: VideoScraped): - if scraped.id != self.id: - raise ValueError("scraped data does not belong to video") - - if scraped.published: - self.published = scraped.published - - -@dataclass -class ChannelMetadata: - """Channel information""" - - id: str - name: str - description: str - avatar_url: str - - -def download_thumbnail(vinfo: VideoDetails, download_path: Path) -> Path: +def download_thumbnail(vinfo: VideoInfo, download_path): """ Download the thumbnail image of a YouTube video and save it at the given filepath. - The thumbnail file ending is added to the path. + Does not add the correct file ending (jpg or webp), we are converting it with + Pillow anyway. :param vinfo: Video info (from ``get_video_info()``) :param download_path: Path of the thumbnail file :raise ThumbnailNotFoundError: if no thumbnail could be found (YT returned 404) - :return: Path with file ending """ - for tn in sorted(vinfo.thumbnails, key=itemgetter("preference"), reverse=True): url = tn["url"] - logging.info(f"downloading thumbnail {url}...") + print(f"downloading thumbnail {url}...") try: - return util.download_image_file(url, download_path) + util.download_file(url, download_path) + return except requests.HTTPError: - logging.warning(f"downloading thumbnail {url} failed") + print(f"downloading thumbnail {url} failed") pass raise ThumbnailNotFoundError(f"could not find thumbnail for video {vinfo}") -def get_video_details(video_id: str) -> VideoDetails: +def get_video_info(video_id) -> VideoInfo: with YoutubeDL() as ydl: info = ydl.extract_info(video_id, download=False) - return VideoDetails.from_vinfo(info) + return VideoInfo(info) -def download_audio( - video_id: str, download_path: Path, sponsorblock=False -) -> VideoDetails: - tmp_dld_file = download_path.with_suffix(".dld" + download_path.suffix) - +def download_video(video_id, download_path, sponsorblock=False) -> VideoInfo: ydl_params = { "format": "bestaudio", "postprocessors": [ {"key": "FFmpegExtractAudio", "preferredcodec": "mp3"}, ], - "outtmpl": str(tmp_dld_file), + "outtmpl": str(download_path), } if sponsorblock: @@ -152,76 +107,34 @@ def download_audio( with YoutubeDL(ydl_params) as ydl: # extract_info downloads the video and returns its metadata info = ydl.extract_info(video_id) - - shutil.move(tmp_dld_file, download_path) - return VideoDetails.from_vinfo(info) + return VideoInfo(info) -def tag_audio(audio_path: Path, vinfo: VideoDetails, cover_path: Path): - title_text = f"{vinfo.published.date().isoformat()} {vinfo.title}" - - audio = id3.ID3(audio_path) - audio["TPE1"] = id3.TPE1(encoding=3, text=vinfo.channel_name) # Artist - audio["TALB"] = id3.TALB(encoding=3, text=vinfo.channel_name) # Album - audio["TIT2"] = id3.TIT2(encoding=3, text=title_text) # Title - audio["TYER"] = id3.TYER(encoding=3, text=str(vinfo.published.year)) # Year - audio["TDAT"] = id3.TDAT(encoding=3, text=vinfo.published.strftime("%d%m")) # Date - audio["COMM"] = id3.COMM(encoding=3, text=f"YT-ID: {vinfo.id}") # Comment - - with open(cover_path, "rb") as albumart: - audio["APIC"] = id3.APIC( - encoding=3, mime="image/png", type=3, desc="Cover", data=albumart.read() - ) - audio.save() +@dataclass +class ChannelMetadata: + id: str + name: str + description: str + avatar_url: str def channel_url_from_id(channel_id: str) -> str: return "https://www.youtube.com/channel/" + channel_id -def channel_url_from_str(channel_str: str) -> str: - """ - Get the channel URL from user input. The following types are accepted: - - - Channel ID URL: https://www.youtube.com/channel/UCGiJh0NZ52wRhYKYnuZI08Q - - Vanity URL: https://www.youtube.com/c/MrBeast6000 - - User URL: https://www.youtube.com/user/LinusTechTips - - Channel ID: ``UCGiJh0NZ52wRhYKYnuZI08Q`` - - :param channel_str: Channel string - :return: Channel URL - """ - channel_url_regex = re.compile( - r"""(?:https?://)?[-a-zA-Z\d@:%._+~#=]+\.[a-zA-Z\d]{1,6}/(?:(channel|c|user)/)?([-_a-zA-Z\d]*)""" - ) - - match = channel_url_regex.match(channel_str) - if match: - url_type = match[1] - # Vanity URL - if not url_type or url_type == "c": - return "https://www.youtube.com/c/" + match[2] - # Username - if url_type == "user": - return "https://www.youtube.com/user/" + match[2] - # Channel ID - return "https://www.youtube.com/channel/" + match[2] - - chanid_regex = re.compile(r"""[-_a-zA-Z\d]{24}""") - if chanid_regex.match(channel_str): - return "https://www.youtube.com/channel/" + channel_str - - raise ValueError("invalid channel string") - - def get_channel_metadata(channel_url: str) -> ChannelMetadata: - """ - Get the metadata of a channel + session = requests.Session() + session.headers[ + "User-Agent" + ] = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 \ +(KHTML, like Gecko) Chrome/91.0.4472.101 Safari/537.36" - :param channel_url: Channel-URL - :return: Channel metadata - """ - data = scrapetube.get_channel_metadata(channel_url) + url = f"{channel_url}/videos?view=0&flow=grid" + + html = scrapetube.get_initial_data(session, url) + data = json.loads( + scrapetube.get_json_from_html(html, "var ytInitialData = ", 0, "};") + "}" + ) metadata = data["metadata"]["channelMetadataRenderer"] channel_id = metadata["externalId"] @@ -230,77 +143,3 @@ def get_channel_metadata(channel_url: str) -> ChannelMetadata: avatar = metadata["avatar"]["thumbnails"][0]["url"] return ChannelMetadata(channel_id, name, description, avatar) - - -def download_avatar(avatar_url: str, download_path: Path) -> Path: - """ - Download the avatar image of a channel. The .jpg file ending - is added to the path. - - :param avatar_url: Channel avatar URL - :param download_path: Download path - :return: Path with file ending - """ - logging.info(f"downloading avatar {avatar_url}...") - - download_path = download_path.with_suffix(".jpg") - util.download_file(avatar_url, download_path) - return download_path - - -def get_channel_videos_from_feed(channel_id: str) -> List[VideoScraped]: - """ - Return videos of a channel using YouTube's RSS feed. Using the feed is fast, - but you only get the 15 latest videos. - - :param channel_id: YouTube channel id - :return: Videos: video_id -> VideoScraped - """ - feed_url = f"https://www.youtube.com/feeds/videos.xml?channel_id={channel_id}" - feed = feedparser.parse(feed_url) - videos = [] - - for item in feed["entries"]: - video_id = item.get("yt_videoid") - if not video_id: - logging.warning( - f"found invalid item in rss feed of channel {channel_id}: {item}" - ) - continue - - publish_date_str = item.get("published") - publish_date = None - if publish_date_str: - publish_date = datetime.datetime.fromisoformat(publish_date_str) - - videos.append(VideoScraped(video_id, publish_date)) - - return videos - - -def get_channel_videos_from_scraper( - channel_id: str, limit: int = None -) -> List[VideoScraped]: - """ - Return all videos of a channel by scraping the YouTube website. - May take a while depending on the number of videos. - - :param channel_id: YouTube channel id - :param limit: Limit number of scraped videos - :return: Videos: video_id -> VideoScraped - """ - videos = [] - - for item in scrapetube.get_channel( - channel_url_from_id(channel_id), limit, settings.YOUTUBE_SCRAPE_DELAY - ): - video_id = item.get("videoId") - if not video_id: - logging.warning( - f"found invalid item in scraped feed of channel {channel_id}: {item}" - ) - continue - - videos.append(VideoScraped(video_id, None)) - - return videos diff --git a/ucast/tasks/__init__.py b/ucast/tasks/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/ucast/tasks/download.py b/ucast/tasks/download.py deleted file mode 100644 index 9e45081..0000000 --- a/ucast/tasks/download.py +++ /dev/null @@ -1,113 +0,0 @@ -from django.utils import timezone - -from ucast.models import Channel, Video -from ucast.service import cover, storage, util, youtube - -store = storage.Storage() - - -def _get_or_create_channel(channel_id: str) -> Channel: - try: - return Channel.objects.get(id=channel_id) - except Channel.DoesNotExist: - channel_data = youtube.get_channel_metadata( - youtube.channel_url_from_id(channel_id) - ) - channel_slug = Channel.get_new_slug(channel_data.name) - channel_folder = store.get_channel_folder(channel_slug) - - avatar_file = youtube.download_avatar( - channel_data.avatar_url, channel_folder.file_avatar - ) - util.resize_avatar(avatar_file, channel_folder.file_avatar_sm) - - channel = Channel( - id=channel_id, - name=channel_data.name, - slug=channel_slug, - description=channel_data.description, - ) - channel.save() - return channel - - -def _load_scraped_video(vid: youtube.VideoScraped, channel: Channel): - if Video.objects.filter(id=vid.id).exists(): - return - - details = youtube.get_video_details(vid.id) - - # Check filter - if ( - details.is_currently_live - or (details.is_short and channel.skip_shorts) - or (details.is_livestream and channel.skip_livestreams) - ): - return - - slug = Video.get_new_slug(details.title, details.published.date(), channel.id) - - video = Video( - id=details.id, - title=details.title, - slug=slug, - channel=channel, - published=details.published, - description=details.description, - duration=details.duration, - is_livestream=details.is_livestream, - is_short=details.is_short, - ) - video.save() - - -def download_video(video: Video): - channel_folder = store.get_channel_folder(video.channel.slug) - - audio_file = channel_folder.get_audio(video.slug) - details = youtube.download_audio(video.id, audio_file) - - # Download/convert thumbnails - tn_path = youtube.download_thumbnail( - details, channel_folder.get_thumbnail(video.slug) - ) - util.resize_thumbnail(tn_path, channel_folder.get_thumbnail(video.slug, True)) - cover_file = channel_folder.get_cover(video.slug) - cover.create_cover_file( - tn_path, - channel_folder.file_avatar, - details.title, - video.channel.name, - cover.COVER_STYLE_BLUR, - cover_file, - ) - - youtube.tag_audio(audio_file, details, cover_file) - - -def fetch_channel(channel_id: str, limit: int = None): - channel = _get_or_create_channel(channel_id) - - if limit == 0: - return - - videos = youtube.get_channel_videos_from_scraper(channel_id, limit) - - for vid in videos[:limit]: - _load_scraped_video(vid, channel) - - -def update_channels(): - for channel in Channel.objects.filter(active=True): - videos = youtube.get_channel_videos_from_feed(channel.id) - - for vid in videos: - _load_scraped_video(vid, channel) - - -def download_videos(): - for video in Video.objects.filter(downloaded=None): - download_video(video) - - video.downloaded = timezone.now() - video.save() diff --git a/ucast/tests/test_cover.py b/ucast/tests/test_cover.py index 475b5f3..ca50256 100644 --- a/ucast/tests/test_cover.py +++ b/ucast/tests/test_cover.py @@ -25,7 +25,8 @@ from ucast.service import cover, typ ( 1000, 300, - "Ha! du wärst Obrigkeit von Gott? Gott spendet Segen aus; du raubst! Du nicht von Gott, Tyrann!", + "Ha! du wärst Obrigkeit von Gott? Gott spendet Segen aus; du raubst! \ +Du nicht von Gott, Tyrann!", [ "Ha! du wärst", "Obrigkeit von", @@ -73,31 +74,31 @@ def test_get_text_color(bg_color: typ.Color, text_color: typ.Color): @pytest.mark.parametrize( "n_image,title,channel,style", [ - (1, "ThetaDev @ Embedded World 2019", "ThetaDev", cover.COVER_STYLE_GRADIENT), - (1, "ThetaDev @ Embedded World 2019", "ThetaDev", cover.COVER_STYLE_BLUR), + (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.COVER_STYLE_GRADIENT, + cover.CoverStyle.CLASSIC, ), ( 2, "Sintel - Open Movie by Blender Foundation", "Blender", - cover.COVER_STYLE_BLUR, + cover.CoverStyle.BLUR, ), ( 3, "Systemabsturz Teaser zur DiVOC bb3", "media.ccc.de", - cover.COVER_STYLE_GRADIENT, + cover.CoverStyle.CLASSIC, ), ( 3, "Systemabsturz Teaser zur DiVOC bb3", "media.ccc.de", - cover.COVER_STYLE_BLUR, + cover.CoverStyle.BLUR, ), ], ) @@ -106,7 +107,9 @@ def test_create_cover_image( ): 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}.png" + expected_cv_file = ( + tests.DIR_TESTFILES / "cover" / f"c{n_image}_{style.name.lower()}.png" + ) tn_image = Image.open(tn_file) av_image = Image.open(av_file) @@ -133,7 +136,7 @@ def test_create_cover_image_noavatar(): None, "ThetaDev @ Embedded World 2019", "ThetaDev", - cover.COVER_STYLE_GRADIENT, + cover.CoverStyle.CLASSIC, ) assert cv_image.width == cover.COVER_WIDTH @@ -162,7 +165,7 @@ def test_create_blank_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_gradient.png" + expected_cv_file = tests.DIR_TESTFILES / "cover" / "c1_classic.png" tmpdir_o = tempfile.TemporaryDirectory() tmpdir = Path(tmpdir_o.name) @@ -173,7 +176,7 @@ def test_create_cover_file(): av_file, "ThetaDev @ Embedded World 2019", "ThetaDev", - "gradient", + cover.CoverStyle.CLASSIC, cv_file, ) diff --git a/ucast/tests/test_util.py b/ucast/tests/test_util.py index eb10259..895b78d 100644 --- a/ucast/tests/test_util.py +++ b/ucast/tests/test_util.py @@ -6,7 +6,7 @@ from PIL import Image, ImageChops from ucast import tests from ucast.service import util -TEST_FILE_URL = "https://yt3.ggpht.com/ytc/AKedOLSnFfmpibLLoqyaYdsF6bJ-zaLPzomII__FrJve1w=s900-c-k-c0x00ffffff-no-rj" +TEST_FILE_URL = "https://yt3.ggpht.com/ytc/AKedOLSnFfmpibLLoqyaYdsF6bJ-zaLPzomII__FrJve1w=s900-c-k-c0x00ffffff-no-rj" # noqa: E501 def test_download_file(): diff --git a/ucast/tests/test_youtube.py b/ucast/tests/test_youtube.py index 4b3afce..151a3e6 100644 --- a/ucast/tests/test_youtube.py +++ b/ucast/tests/test_youtube.py @@ -1,7 +1,7 @@ -import datetime import re import subprocess import tempfile +from datetime import datetime from pathlib import Path import pytest @@ -20,8 +20,8 @@ CHANNEL_URL_BLENDER = "https://www.youtube.com/c/BlenderFoundation" @pytest.fixture(scope="module") -def video_info() -> youtube.VideoDetails: - return youtube.get_video_details(VIDEO_ID_SINTEL) +def video_info() -> youtube.VideoInfo: + return youtube.get_video_info(VIDEO_ID_SINTEL) def test_download_thumbnail(video_info): @@ -30,8 +30,7 @@ def test_download_thumbnail(video_info): tn_file = tmpdir / "thumbnail" expected_tn_file = tests.DIR_TESTFILES / "thumbnail" / "t2.webp" - tn_file = youtube.download_thumbnail(video_info, tn_file) - assert tn_file.suffix == ".webp" + youtube.download_thumbnail(video_info, tn_file) tn = Image.open(tn_file) expected_tn = Image.open(expected_tn_file) @@ -65,13 +64,11 @@ www.sintel.org""" assert not video_info.is_currently_live assert not video_info.is_livestream assert not video_info.is_short - assert video_info.published == datetime.datetime( - 2010, 9, 30, tzinfo=datetime.timezone.utc - ) + assert video_info.published == datetime(2010, 9, 30) def test_get_video_info_short(): - vinfo = youtube.get_video_details(VIDEO_ID_SHORT) + vinfo = youtube.get_video_info(VIDEO_ID_SHORT) assert vinfo.id == VIDEO_ID_SHORT assert ( vinfo.title @@ -88,7 +85,7 @@ def test_download_video(): tmpdir = Path(tmpdir_o.name) download_file = tmpdir / "download.mp3" - vinfo = youtube.download_audio(VIDEO_ID_PERSUASION, download_file) + vinfo = youtube.download_video(VIDEO_ID_PERSUASION, download_file) assert vinfo.id == VIDEO_ID_PERSUASION assert vinfo.title == "Persuasion (Instrumental) – RYYZN (No Copyright Music)" assert vinfo.duration == 100 @@ -112,13 +109,13 @@ def test_download_video(): youtube.channel_url_from_id(CHANNEL_ID_THETADEV), CHANNEL_ID_THETADEV, "ThetaDev", - "https://yt3.ggpht.com/ytc/AKedOLSnFfmpibLLoqyaYdsF6bJ-zaLPzomII__FrJve1w=s900-c-k-c0x00ffffff-no-rj", + "https://yt3.ggpht.com/ytc/AKedOLSnFfmpibLLoqyaYdsF6bJ-zaLPzomII__FrJve1w=s900-c-k-c0x00ffffff-no-rj", # noqa: E501 ), ( CHANNEL_URL_BLENDER, CHANNEL_ID_BLENDER, "Blender", - "https://yt3.ggpht.com/ytc/AKedOLT_31fFSD3FWEBnHZnyZeJx-GPHJwYCQKcEpaq8NQ=s900-c-k-c0x00ffffff-no-rj", + "https://yt3.ggpht.com/ytc/AKedOLT_31fFSD3FWEBnHZnyZeJx-GPHJwYCQKcEpaq8NQ=s900-c-k-c0x00ffffff-no-rj", # noqa: E501 ), ], ) diff --git a/ucast/tests/testfiles/cover/c1_gradient.png b/ucast/tests/testfiles/cover/c1_classic.png similarity index 100% rename from ucast/tests/testfiles/cover/c1_gradient.png rename to ucast/tests/testfiles/cover/c1_classic.png diff --git a/ucast/tests/testfiles/cover/c2_blur.png b/ucast/tests/testfiles/cover/c2_blur.png index a36d31c..152d2b5 100644 Binary files a/ucast/tests/testfiles/cover/c2_blur.png and b/ucast/tests/testfiles/cover/c2_blur.png differ diff --git a/ucast/tests/testfiles/cover/c2_classic.png b/ucast/tests/testfiles/cover/c2_classic.png new file mode 100644 index 0000000..7c38fff Binary files /dev/null and b/ucast/tests/testfiles/cover/c2_classic.png differ diff --git a/ucast/tests/testfiles/cover/c2_gradient.png b/ucast/tests/testfiles/cover/c2_gradient.png deleted file mode 100644 index ea128e3..0000000 Binary files a/ucast/tests/testfiles/cover/c2_gradient.png and /dev/null differ diff --git a/ucast/tests/testfiles/cover/c3_gradient.png b/ucast/tests/testfiles/cover/c3_classic.png similarity index 100% rename from ucast/tests/testfiles/cover/c3_gradient.png rename to ucast/tests/testfiles/cover/c3_classic.png diff --git a/ucast_project/settings.py b/ucast_project/settings.py index 460eeb9..018668d 100644 --- a/ucast_project/settings.py +++ b/ucast_project/settings.py @@ -11,7 +11,6 @@ https://docs.djangoproject.com/en/4.0/ref/settings/ """ import os -import time from importlib import resources from pathlib import Path @@ -176,11 +175,11 @@ AUTH_PASSWORD_VALIDATORS = [ LANGUAGE_CODE = "en-us" -TIME_ZONE = get_env("TZ", time.tzname[0]) +TIME_ZONE = "UTC" -USE_I18N = True +USE_I18N = False -USE_TZ = True +USE_TZ = False # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/4.0/howto/static-files/ @@ -195,6 +194,3 @@ STATICFILES_DIRS = [resources.path("ucast", "static")] # https://docs.djangoproject.com/en/4.0/ref/settings/#default-auto-field DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" - -# Delay between YouTube API calls -YOUTUBE_SCRAPE_DELAY = 1