Compare commits

..

5 commits

Author SHA1 Message Date
89a190ad4a add mysql+postgres drivers 2022-05-03 21:43:39 +02:00
11c679975b add download time to video model 2022-05-03 18:17:24 +02:00
3eacc0ad8d fix alembic working directory 2022-05-03 18:01:11 +02:00
27eeac66f0 formatted with Black 2022-05-03 17:32:50 +02:00
ebe4ccf926 add pre-commit 2022-05-03 16:13:18 +02:00
23 changed files with 536 additions and 168 deletions

21
.pre-commit-config.yaml Normal file
View file

@ -0,0 +1,21 @@
# See https://pre-commit.com for more information
# See https://pre-commit.com/hooks.html for more hooks
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.2.0
hooks:
- id: check-added-large-files
- repo: https://github.com/pycqa/isort
rev: 5.10.1
hooks:
- id: isort
- repo: https://github.com/psf/black
rev: 22.3.0
hooks:
- id: black
- repo: https://github.com/pycqa/flake8
rev: 4.0.1
hooks:
- id: flake8
additional_dependencies: [ Flake8-pyproject ]
entry: flake8p

View file

@ -62,6 +62,7 @@ _ data
- Title: str, VARCHAR(200) - Title: str, VARCHAR(200)
- Slug: str (YYYYMMDD_Title, used as filename), VARCHAR(209) - Slug: str (YYYYMMDD_Title, used as filename), VARCHAR(209)
- Published: datetime - Published: datetime
- Downloaded: datetime
- Description: str, VARCHAR(1000) - Description: str, VARCHAR(1000)
### Config ### Config

175
poetry.lock generated
View file

@ -101,6 +101,14 @@ python-versions = "*"
[package.dependencies] [package.dependencies]
pycparser = "*" pycparser = "*"
[[package]]
name = "cfgv"
version = "3.3.1"
description = "Validate configuration and produce human readable error messages."
category = "dev"
optional = false
python-versions = ">=3.6.1"
[[package]] [[package]]
name = "charset-normalizer" name = "charset-normalizer"
version = "2.0.12" version = "2.0.12"
@ -156,6 +164,14 @@ tomli = {version = "*", optional = true, markers = "extra == \"toml\""}
[package.extras] [package.extras]
toml = ["tomli"] toml = ["tomli"]
[[package]]
name = "distlib"
version = "0.3.4"
description = "Distribution utilities"
category = "dev"
optional = false
python-versions = "*"
[[package]] [[package]]
name = "feedparser" name = "feedparser"
version = "6.0.8" version = "6.0.8"
@ -167,6 +183,18 @@ python-versions = ">=3.6"
[package.dependencies] [package.dependencies]
sgmllib3k = "*" sgmllib3k = "*"
[[package]]
name = "filelock"
version = "3.6.0"
description = "A platform independent file lock."
category = "dev"
optional = false
python-versions = ">=3.7"
[package.extras]
docs = ["furo (>=2021.8.17b43)", "sphinx (>=4.1)", "sphinx-autodoc-typehints (>=1.12)"]
testing = ["covdefaults (>=1.2.0)", "coverage (>=4)", "pytest (>=4)", "pytest-cov", "pytest-timeout (>=1.4.2)"]
[[package]] [[package]]
name = "font-source-sans-pro" name = "font-source-sans-pro"
version = "0.0.1" version = "0.0.1"
@ -202,6 +230,17 @@ category = "main"
optional = false optional = false
python-versions = ">=3.6" python-versions = ">=3.6"
[[package]]
name = "identify"
version = "2.5.0"
description = "File identification library for Python"
category = "dev"
optional = false
python-versions = ">=3.7"
[package.extras]
license = ["ukkonen"]
[[package]] [[package]]
name = "idna" name = "idna"
version = "3.3" version = "3.3"
@ -280,6 +319,22 @@ category = "main"
optional = false optional = false
python-versions = ">=3.5, <4" python-versions = ">=3.5, <4"
[[package]]
name = "mysqlclient"
version = "2.1.0"
description = "Python interface to MySQL"
category = "main"
optional = false
python-versions = ">=3.5"
[[package]]
name = "nodeenv"
version = "1.6.0"
description = "Node.js virtual environment builder"
category = "dev"
optional = false
python-versions = "*"
[[package]] [[package]]
name = "packaging" name = "packaging"
version = "21.3" version = "21.3"
@ -303,6 +358,18 @@ python-versions = ">=3.7"
docs = ["olefile", "sphinx (>=2.4)", "sphinx-copybutton", "sphinx-issues (>=3.0.1)", "sphinx-removed-in", "sphinx-rtd-theme (>=1.0)", "sphinxext-opengraph"] docs = ["olefile", "sphinx (>=2.4)", "sphinx-copybutton", "sphinx-issues (>=3.0.1)", "sphinx-removed-in", "sphinx-rtd-theme (>=1.0)", "sphinxext-opengraph"]
tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"] tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"]
[[package]]
name = "platformdirs"
version = "2.5.2"
description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
category = "dev"
optional = false
python-versions = ">=3.7"
[package.extras]
docs = ["furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)", "sphinx (>=4)"]
test = ["appdirs (==1.4.4)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)", "pytest (>=6)"]
[[package]] [[package]]
name = "pluggy" name = "pluggy"
version = "1.0.0" version = "1.0.0"
@ -315,6 +382,30 @@ python-versions = ">=3.6"
dev = ["pre-commit", "tox"] dev = ["pre-commit", "tox"]
testing = ["pytest", "pytest-benchmark"] testing = ["pytest", "pytest-benchmark"]
[[package]]
name = "pre-commit"
version = "2.18.1"
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 = "psycopg2"
version = "2.9.3"
description = "psycopg2 - Python-PostgreSQL Database Adapter"
category = "main"
optional = false
python-versions = ">=3.6"
[[package]] [[package]]
name = "py" name = "py"
version = "1.11.0" version = "1.11.0"
@ -595,6 +686,14 @@ category = "main"
optional = false optional = false
python-versions = "*" python-versions = "*"
[[package]]
name = "toml"
version = "0.10.2"
description = "Python Library for Tom's Obvious, Minimal Language"
category = "dev"
optional = false
python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
[[package]] [[package]]
name = "tomli" name = "tomli"
version = "2.0.1" version = "2.0.1"
@ -648,6 +747,24 @@ h11 = ">=0.8"
[package.extras] [package.extras]
standard = ["websockets (>=10.0)", "httptools (>=0.4.0)", "watchgod (>=0.6)", "python-dotenv (>=0.13)", "PyYAML (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "colorama (>=0.4)"] standard = ["websockets (>=10.0)", "httptools (>=0.4.0)", "watchgod (>=0.6)", "python-dotenv (>=0.13)", "PyYAML (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "colorama (>=0.4)"]
[[package]]
name = "virtualenv"
version = "20.14.1"
description = "Virtual Python Environment builder"
category = "dev"
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7"
[package.dependencies]
distlib = ">=0.3.1,<1"
filelock = ">=3.2,<4"
platformdirs = ">=2,<3"
six = ">=1.9.0,<2"
[package.extras]
docs = ["proselint (>=0.10.2)", "sphinx (>=3)", "sphinx-argparse (>=0.2.5)", "sphinx-rtd-theme (>=0.4.3)", "towncrier (>=21.3)"]
testing = ["coverage (>=4)", "coverage-enable-subprocess (>=1)", "flaky (>=3)", "pytest (>=4)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.1)", "pytest-mock (>=2)", "pytest-randomly (>=1)", "pytest-timeout (>=1)", "packaging (>=20.0)"]
[[package]] [[package]]
name = "wcag-contrast-ratio" name = "wcag-contrast-ratio"
version = "0.9" version = "0.9"
@ -697,7 +814,7 @@ websockets = "*"
[metadata] [metadata]
lock-version = "1.1" lock-version = "1.1"
python-versions = "^3.10" python-versions = "^3.10"
content-hash = "3e8f9ea28feca928ee5e81f883e97e3d4390c859dc6a7850544003aff517dade" content-hash = "ff7d3c1941f088222a945efb832db370bad4daeee7298d08752aa4061f7c4846"
[metadata.files] [metadata.files]
alembic = [ alembic = [
@ -872,6 +989,10 @@ cffi = [
{file = "cffi-1.15.0-cp39-cp39-win_amd64.whl", hash = "sha256:3773c4d81e6e818df2efbc7dd77325ca0dcb688116050fb2b3011218eda36139"}, {file = "cffi-1.15.0-cp39-cp39-win_amd64.whl", hash = "sha256:3773c4d81e6e818df2efbc7dd77325ca0dcb688116050fb2b3011218eda36139"},
{file = "cffi-1.15.0.tar.gz", hash = "sha256:920f0d66a896c2d99f0adbb391f990a84091179542c205fa53ce5787aff87954"}, {file = "cffi-1.15.0.tar.gz", hash = "sha256:920f0d66a896c2d99f0adbb391f990a84091179542c205fa53ce5787aff87954"},
] ]
cfgv = [
{file = "cfgv-3.3.1-py2.py3-none-any.whl", hash = "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426"},
{file = "cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736"},
]
charset-normalizer = [ charset-normalizer = [
{file = "charset-normalizer-2.0.12.tar.gz", hash = "sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597"}, {file = "charset-normalizer-2.0.12.tar.gz", hash = "sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597"},
{file = "charset_normalizer-2.0.12-py3-none-any.whl", hash = "sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df"}, {file = "charset_normalizer-2.0.12-py3-none-any.whl", hash = "sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df"},
@ -931,10 +1052,18 @@ coverage = [
{file = "coverage-6.3.2-pp36.pp37.pp38-none-any.whl", hash = "sha256:18d520c6860515a771708937d2f78f63cc47ab3b80cb78e86573b0a760161faf"}, {file = "coverage-6.3.2-pp36.pp37.pp38-none-any.whl", hash = "sha256:18d520c6860515a771708937d2f78f63cc47ab3b80cb78e86573b0a760161faf"},
{file = "coverage-6.3.2.tar.gz", hash = "sha256:03e2a7826086b91ef345ff18742ee9fc47a6839ccd517061ef8fa1976e652ce9"}, {file = "coverage-6.3.2.tar.gz", hash = "sha256:03e2a7826086b91ef345ff18742ee9fc47a6839ccd517061ef8fa1976e652ce9"},
] ]
distlib = [
{file = "distlib-0.3.4-py2.py3-none-any.whl", hash = "sha256:6564fe0a8f51e734df6333d08b8b94d4ea8ee6b99b5ed50613f731fd4089f34b"},
{file = "distlib-0.3.4.zip", hash = "sha256:e4b58818180336dc9c529bfb9a0b58728ffc09ad92027a3f30b7cd91e3458579"},
]
feedparser = [ feedparser = [
{file = "feedparser-6.0.8-py3-none-any.whl", hash = "sha256:1b7f57841d9cf85074deb316ed2c795091a238adb79846bc46dccdaf80f9c59a"}, {file = "feedparser-6.0.8-py3-none-any.whl", hash = "sha256:1b7f57841d9cf85074deb316ed2c795091a238adb79846bc46dccdaf80f9c59a"},
{file = "feedparser-6.0.8.tar.gz", hash = "sha256:5ce0410a05ab248c8c7cfca3a0ea2203968ee9ff4486067379af4827a59f9661"}, {file = "feedparser-6.0.8.tar.gz", hash = "sha256:5ce0410a05ab248c8c7cfca3a0ea2203968ee9ff4486067379af4827a59f9661"},
] ]
filelock = [
{file = "filelock-3.6.0-py3-none-any.whl", hash = "sha256:f8314284bfffbdcfa0ff3d7992b023d4c628ced6feb957351d4c48d059f56bc0"},
{file = "filelock-3.6.0.tar.gz", hash = "sha256:9cd540a9352e432c7246a48fe4e8712b10acb1df2ad1f30e8c070b82ae1fed85"},
]
font-source-sans-pro = [ font-source-sans-pro = [
{file = "font-source-sans-pro-0.0.1.tar.gz", hash = "sha256:3f81d8e52b0d7e930e2c867c0d3ee549312d03f97b71b664a8361006311f72e5"}, {file = "font-source-sans-pro-0.0.1.tar.gz", hash = "sha256:3f81d8e52b0d7e930e2c867c0d3ee549312d03f97b71b664a8361006311f72e5"},
{file = "font_source_sans_pro-0.0.1-py2-none-any.whl", hash = "sha256:685c8813d59941e84ea326f46d638871adbc825a0aa5205a72ee9ed9c5fbb471"}, {file = "font_source_sans_pro-0.0.1-py2-none-any.whl", hash = "sha256:685c8813d59941e84ea326f46d638871adbc825a0aa5205a72ee9ed9c5fbb471"},
@ -1006,6 +1135,10 @@ h11 = [
{file = "h11-0.13.0-py3-none-any.whl", hash = "sha256:8ddd78563b633ca55346c8cd41ec0af27d3c79931828beffb46ce70a379e7442"}, {file = "h11-0.13.0-py3-none-any.whl", hash = "sha256:8ddd78563b633ca55346c8cd41ec0af27d3c79931828beffb46ce70a379e7442"},
{file = "h11-0.13.0.tar.gz", hash = "sha256:70813c1135087a248a4d38cc0e1a0181ffab2188141a93eaf567940c3957ff06"}, {file = "h11-0.13.0.tar.gz", hash = "sha256:70813c1135087a248a4d38cc0e1a0181ffab2188141a93eaf567940c3957ff06"},
] ]
identify = [
{file = "identify-2.5.0-py2.py3-none-any.whl", hash = "sha256:3acfe15a96e4272b4ec5662ee3e231ceba976ef63fd9980ed2ce9cc415df393f"},
{file = "identify-2.5.0.tar.gz", hash = "sha256:c83af514ea50bf2be2c4a3f2fb349442b59dc87284558ae9ff54191bff3541d2"},
]
idna = [ idna = [
{file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"}, {file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"},
{file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"}, {file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"},
@ -1076,6 +1209,17 @@ mutagen = [
{file = "mutagen-1.45.1-py3-none-any.whl", hash = "sha256:9c9f243fcec7f410f138cb12c21c84c64fde4195481a30c9bfb05b5f003adfed"}, {file = "mutagen-1.45.1-py3-none-any.whl", hash = "sha256:9c9f243fcec7f410f138cb12c21c84c64fde4195481a30c9bfb05b5f003adfed"},
{file = "mutagen-1.45.1.tar.gz", hash = "sha256:6397602efb3c2d7baebd2166ed85731ae1c1d475abca22090b7141ff5034b3e1"}, {file = "mutagen-1.45.1.tar.gz", hash = "sha256:6397602efb3c2d7baebd2166ed85731ae1c1d475abca22090b7141ff5034b3e1"},
] ]
mysqlclient = [
{file = "mysqlclient-2.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:02c8826e6add9b20f4cb12dcf016485f7b1d6e30356a1204d05431867a1b3947"},
{file = "mysqlclient-2.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:b62d23c11c516cedb887377c8807628c1c65d57593b57853186a6ee18b0c6a5b"},
{file = "mysqlclient-2.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:2c8410f54492a3d2488a6a53e2d85b7e016751a1e7d116e7aea9c763f59f5e8c"},
{file = "mysqlclient-2.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:e6279263d5a9feca3e0edbc2b2a52c057375bf301d47da2089c075ff76331d14"},
{file = "mysqlclient-2.1.0.tar.gz", hash = "sha256:973235686f1b720536d417bf0a0d39b4ab3d5086b2b6ad5e6752393428c02b12"},
]
nodeenv = [
{file = "nodeenv-1.6.0-py2.py3-none-any.whl", hash = "sha256:621e6b7076565ddcacd2db0294c0381e01fd28945ab36bcf00f41c5daf63bef7"},
{file = "nodeenv-1.6.0.tar.gz", hash = "sha256:3ef13ff90291ba2a4a7a4ff9a979b63ffdd00a464dbe04acf0ea6471517a4c2b"},
]
packaging = [ packaging = [
{file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"},
{file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"},
@ -1120,10 +1264,31 @@ pillow = [
{file = "Pillow-9.1.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:8d79c6f468215d1a8415aa53d9868a6b40c4682165b8cb62a221b1baa47db458"}, {file = "Pillow-9.1.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:8d79c6f468215d1a8415aa53d9868a6b40c4682165b8cb62a221b1baa47db458"},
{file = "Pillow-9.1.0.tar.gz", hash = "sha256:f401ed2bbb155e1ade150ccc63db1a4f6c1909d3d378f7d1235a44e90d75fb97"}, {file = "Pillow-9.1.0.tar.gz", hash = "sha256:f401ed2bbb155e1ade150ccc63db1a4f6c1909d3d378f7d1235a44e90d75fb97"},
] ]
platformdirs = [
{file = "platformdirs-2.5.2-py3-none-any.whl", hash = "sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788"},
{file = "platformdirs-2.5.2.tar.gz", hash = "sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19"},
]
pluggy = [ pluggy = [
{file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"},
{file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"},
] ]
pre-commit = [
{file = "pre_commit-2.18.1-py2.py3-none-any.whl", hash = "sha256:02226e69564ebca1a070bd1f046af866aa1c318dbc430027c50ab832ed2b73f2"},
{file = "pre_commit-2.18.1.tar.gz", hash = "sha256:5d445ee1fa8738d506881c5d84f83c62bb5be6b2838e32207433647e8e5ebe10"},
]
psycopg2 = [
{file = "psycopg2-2.9.3-cp310-cp310-win32.whl", hash = "sha256:083707a696e5e1c330af2508d8fab36f9700b26621ccbcb538abe22e15485362"},
{file = "psycopg2-2.9.3-cp310-cp310-win_amd64.whl", hash = "sha256:d3ca6421b942f60c008f81a3541e8faf6865a28d5a9b48544b0ee4f40cac7fca"},
{file = "psycopg2-2.9.3-cp36-cp36m-win32.whl", hash = "sha256:9572e08b50aed176ef6d66f15a21d823bb6f6d23152d35e8451d7d2d18fdac56"},
{file = "psycopg2-2.9.3-cp36-cp36m-win_amd64.whl", hash = "sha256:a81e3866f99382dfe8c15a151f1ca5fde5815fde879348fe5a9884a7c092a305"},
{file = "psycopg2-2.9.3-cp37-cp37m-win32.whl", hash = "sha256:cb10d44e6694d763fa1078a26f7f6137d69f555a78ec85dc2ef716c37447e4b2"},
{file = "psycopg2-2.9.3-cp37-cp37m-win_amd64.whl", hash = "sha256:4295093a6ae3434d33ec6baab4ca5512a5082cc43c0505293087b8a46d108461"},
{file = "psycopg2-2.9.3-cp38-cp38-win32.whl", hash = "sha256:34b33e0162cfcaad151f249c2649fd1030010c16f4bbc40a604c1cb77173dcf7"},
{file = "psycopg2-2.9.3-cp38-cp38-win_amd64.whl", hash = "sha256:0762c27d018edbcb2d34d51596e4346c983bd27c330218c56c4dc25ef7e819bf"},
{file = "psycopg2-2.9.3-cp39-cp39-win32.whl", hash = "sha256:8cf3878353cc04b053822896bc4922b194792df9df2f1ad8da01fb3043602126"},
{file = "psycopg2-2.9.3-cp39-cp39-win_amd64.whl", hash = "sha256:06f32425949bd5fe8f625c49f17ebb9784e1e4fe928b7cce72edc36fb68e4c0c"},
{file = "psycopg2-2.9.3.tar.gz", hash = "sha256:8e841d1bf3434da985cc5ef13e6f75c8981ced601fd70cc6bf33351b91562981"},
]
py = [ py = [
{file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"},
{file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"},
@ -1294,6 +1459,10 @@ text-unidecode = [
{file = "text-unidecode-1.3.tar.gz", hash = "sha256:bad6603bb14d279193107714b288be206cac565dfa49aa5b105294dd5c4aab93"}, {file = "text-unidecode-1.3.tar.gz", hash = "sha256:bad6603bb14d279193107714b288be206cac565dfa49aa5b105294dd5c4aab93"},
{file = "text_unidecode-1.3-py2.py3-none-any.whl", hash = "sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8"}, {file = "text_unidecode-1.3-py2.py3-none-any.whl", hash = "sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8"},
] ]
toml = [
{file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"},
{file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"},
]
tomli = [ tomli = [
{file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"},
{file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
@ -1314,6 +1483,10 @@ uvicorn = [
{file = "uvicorn-0.17.6-py3-none-any.whl", hash = "sha256:19e2a0e96c9ac5581c01eb1a79a7d2f72bb479691acd2b8921fce48ed5b961a6"}, {file = "uvicorn-0.17.6-py3-none-any.whl", hash = "sha256:19e2a0e96c9ac5581c01eb1a79a7d2f72bb479691acd2b8921fce48ed5b961a6"},
{file = "uvicorn-0.17.6.tar.gz", hash = "sha256:5180f9d059611747d841a4a4c4ab675edf54c8489e97f96d0583ee90ac3bfc23"}, {file = "uvicorn-0.17.6.tar.gz", hash = "sha256:5180f9d059611747d841a4a4c4ab675edf54c8489e97f96d0583ee90ac3bfc23"},
] ]
virtualenv = [
{file = "virtualenv-20.14.1-py2.py3-none-any.whl", hash = "sha256:e617f16e25b42eb4f6e74096b9c9e37713cf10bf30168fb4a739f3fa8f898a3a"},
{file = "virtualenv-20.14.1.tar.gz", hash = "sha256:ef589a79795589aada0c1c5b319486797c03b67ac3984c48c669c0e4f50df3a5"},
]
wcag-contrast-ratio = [ wcag-contrast-ratio = [
{file = "wcag-contrast-ratio-0.9.tar.gz", hash = "sha256:69192b8e5c0a7d0dc5ff1187eeb3e398141633a4bde51c69c87f58fe87ed361c"}, {file = "wcag-contrast-ratio-0.9.tar.gz", hash = "sha256:69192b8e5c0a7d0dc5ff1187eeb3e398141633a4bde51c69c87f58fe87ed361c"},
] ]

View file

@ -22,11 +22,15 @@ python-slugify = "^6.1.2"
starlette-core = "^0.0.1" starlette-core = "^0.0.1"
click = "^8.1.3" click = "^8.1.3"
python-dotenv = "^0.20.0" python-dotenv = "^0.20.0"
mysqlclient = "^2.1.0"
psycopg2 = "^2.9.3"
[tool.poetry.dev-dependencies] [tool.poetry.dev-dependencies]
pytest = "^7.1.2" pytest = "^7.1.2"
pytest-cov = "^3.0.0" pytest-cov = "^3.0.0"
invoke = "^1.7.0" invoke = "^1.7.0"
pre-commit = "^2.18.1"
virtualenv = "20.14.1"
[tool.poetry.scripts] [tool.poetry.scripts]
ucast = "ucast.__main__:cli" ucast = "ucast.__main__:cli"
@ -34,3 +38,10 @@ ucast = "ucast.__main__:cli"
[build-system] [build-system]
requires = ["poetry-core>=1.0.0"] requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api" build-backend = "poetry.core.masonry.api"
[tool.flake8]
max-line-length = 88
[tool.black]
line-length = 88
target-version = ['py310']

View file

@ -4,8 +4,8 @@ from tempfile import TemporaryDirectory
from invoke import task from invoke import task
from ucast import youtube, util, cover
import tests import tests
from ucast import cover, util, youtube
os.chdir(Path(__file__).absolute().parent) os.chdir(Path(__file__).absolute().parent)
db_file = Path("_run/ucast.db").absolute() db_file = Path("_run/ucast.db").absolute()
@ -18,7 +18,7 @@ os.environ["DATABASE_URL"] = f"sqlite:///{db_file}"
@task @task
def test(c): def test(c):
c.run('pytest tests', pty=True) c.run("pytest tests", pty=True)
@task @task
@ -29,21 +29,21 @@ def run(c):
@task @task
def get_cover(c, vid=''): def get_cover(c, vid=""):
vinfo = youtube.get_video_info(vid) vinfo = youtube.get_video_info(vid)
title = vinfo['fulltitle'] title = vinfo["fulltitle"]
channel_name = vinfo['uploader'] channel_name = vinfo["uploader"]
thumbnail_url = youtube.get_thumbnail_url(vinfo) thumbnail_url = youtube.get_thumbnail_url(vinfo)
channel_url = vinfo['channel_url'] channel_url = vinfo["channel_url"]
channel_metadata = youtube.get_channel_metadata(channel_url) channel_metadata = youtube.get_channel_metadata(channel_url)
ti = 1 ti = 1
while os.path.exists(tests.DIR_TESTFILES / 'cover' / f'c{ti}.png'): while os.path.exists(tests.DIR_TESTFILES / "cover" / f"c{ti}.png"):
ti += 1 ti += 1
tn_file = tests.DIR_TESTFILES / 'thumbnail' / f't{ti}.webp' tn_file = tests.DIR_TESTFILES / "thumbnail" / f"t{ti}.webp"
av_file = tests.DIR_TESTFILES / 'avatar' / f'a{ti}.jpg' av_file = tests.DIR_TESTFILES / "avatar" / f"a{ti}.jpg"
cv_file = tests.DIR_TESTFILES / 'cover' / f'c{ti}.png' cv_file = tests.DIR_TESTFILES / "cover" / f"c{ti}.png"
util.download_file(thumbnail_url, tn_file) util.download_file(thumbnail_url, tn_file)
util.download_file(channel_metadata.avatar_url, av_file) util.download_file(channel_metadata.avatar_url, av_file)
@ -52,7 +52,7 @@ def get_cover(c, vid=''):
@task @task
def add_migration(c, m=''): def add_migration(c, m=""):
if not m: if not m:
raise Exception("please input migration name") raise Exception("please input migration name")
@ -65,4 +65,4 @@ def add_migration(c, m=''):
os.chdir("ucast") os.chdir("ucast")
c.run("alembic upgrade head") c.run("alembic upgrade head")
c.run(f"alembic revision --autogenerate -m {m}") c.run(f"alembic revision --autogenerate -m '{m}'")

View file

@ -1,4 +1,4 @@
# coding=utf-8 # coding=utf-8
from importlib.resources import files from importlib.resources import files
DIR_TESTFILES = files('tests.testfiles') DIR_TESTFILES = files("tests.testfiles")

View file

@ -1,60 +1,89 @@
# coding=utf-8 # coding=utf-8
from typing import List
import tempfile import tempfile
from pathlib import Path from pathlib import Path
from typing import List
import pytest import pytest
from PIL import Image, ImageFont, ImageChops
from fonts.ttf import SourceSansPro from fonts.ttf import SourceSansPro
from PIL import Image, ImageChops, ImageFont
import tests import tests
from ucast import cover, typ from ucast import cover, typ
@pytest.mark.parametrize('height,width,text,expect', [ @pytest.mark.parametrize(
(40, 300, 'Hello', ['Hello']), "height,width,text,expect",
(40, 300, 'Hello World, this is me', ['Hello World,…']), [
(90, 300, 'Hello World, this is me', ['Hello World, this', 'is me']), (40, 300, "Hello", ["Hello"]),
(90, 300, 'Rindfleischettikettierungsüberwachungsaufgabenübertragungsgesetz', ['Rindfleischettik…']), (40, 300, "Hello World, this is me", ["Hello World,…"]),
(1000, 300, 'Ha! du wärst Obrigkeit von Gott? Gott spendet Segen aus; du raubst! Du nicht von Gott, Tyrann!', (90, 300, "Hello World, this is me", ["Hello World, this", "is me"]),
['Ha! du wärst', 'Obrigkeit von', 'Gott? Gott', 'spendet Segen', 'aus; du raubst!', 'Du nicht von Gott,', (
'Tyrann!']), 90,
]) 300,
"Rindfleischettikettierungsüberwachungsaufgabenübertragungsgesetz",
["Rindfleischettik…"],
),
(
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!",
],
),
],
)
def test_split_text(height: int, width: int, text: str, expect: List[str]): def test_split_text(height: int, width: int, text: str, expect: List[str]):
font = ImageFont.truetype(SourceSansPro, 40) font = ImageFont.truetype(SourceSansPro, 40)
lines = cover._split_text(height, width, text, font, 8) lines = cover._split_text(height, width, text, font, 8)
assert lines == expect assert lines == expect
@pytest.mark.parametrize('file_name,color', [ @pytest.mark.parametrize(
('t1.webp', (63, 63, 62)), "file_name,color",
('t2.webp', (74, 45, 37)), [
('t3.webp', (54, 24, 28)), ("t1.webp", (63, 63, 62)),
]) ("t2.webp", (74, 45, 37)),
("t3.webp", (54, 24, 28)),
],
)
def test_get_dominant_color(file_name: str, color: typ.Color): def test_get_dominant_color(file_name: str, color: typ.Color):
img = Image.open(tests.DIR_TESTFILES / 'thumbnail' / file_name) img = Image.open(tests.DIR_TESTFILES / "thumbnail" / file_name)
c = cover._get_dominant_color(img) c = cover._get_dominant_color(img)
assert c == color assert c == color
@pytest.mark.parametrize('bg_color,text_color', [ @pytest.mark.parametrize(
((100, 0, 0), (255, 255, 255)), "bg_color,text_color",
((200, 200, 0), (0, 0, 0)), [
]) ((100, 0, 0), (255, 255, 255)),
((200, 200, 0), (0, 0, 0)),
],
)
def test_get_text_color(bg_color: typ.Color, text_color: typ.Color): def test_get_text_color(bg_color: typ.Color, text_color: typ.Color):
c = cover._get_text_color(bg_color) c = cover._get_text_color(bg_color)
assert c == text_color assert c == text_color
@pytest.mark.parametrize('n_image,title,channel', [ @pytest.mark.parametrize(
(1, 'ThetaDev @ Embedded World 2019', 'ThetaDev'), "n_image,title,channel",
(2, 'Sintel - Open Movie by Blender Foundation', 'Blender'), [
(3, 'Systemabsturz Teaser zur DiVOC bb3', 'media.ccc.de'), (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): def test_create_cover_image(n_image: int, title: str, channel: str):
tn_file = tests.DIR_TESTFILES / 'thumbnail' / f't{n_image}.webp' tn_file = tests.DIR_TESTFILES / "thumbnail" / f"t{n_image}.webp"
av_file = tests.DIR_TESTFILES / 'avatar' / f'a{n_image}.jpg' av_file = tests.DIR_TESTFILES / "avatar" / f"a{n_image}.jpg"
expected_cv_file = tests.DIR_TESTFILES / 'cover' / f'c{n_image}.png' expected_cv_file = tests.DIR_TESTFILES / "cover" / f"c{n_image}.png"
tn_image = Image.open(tn_file) tn_image = Image.open(tn_file)
av_image = Image.open(av_file) av_image = Image.open(av_file)
@ -67,15 +96,17 @@ def test_create_cover_image(n_image: int, title: str, channel: str):
def test_create_cover_file(): def test_create_cover_file():
tn_file = tests.DIR_TESTFILES / 'thumbnail' / 't1.webp' tn_file = tests.DIR_TESTFILES / "thumbnail" / "t1.webp"
av_file = tests.DIR_TESTFILES / 'avatar' / 'a1.jpg' av_file = tests.DIR_TESTFILES / "avatar" / "a1.jpg"
expected_cv_file = tests.DIR_TESTFILES / 'cover' / 'c1.png' expected_cv_file = tests.DIR_TESTFILES / "cover" / "c1.png"
tmpdir_o = tempfile.TemporaryDirectory() tmpdir_o = tempfile.TemporaryDirectory()
tmpdir = Path(tmpdir_o.name) tmpdir = Path(tmpdir_o.name)
cv_file = tmpdir / 'cover.png' cv_file = tmpdir / "cover.png"
cover.create_cover_file(tn_file, av_file, 'ThetaDev @ Embedded World 2019', 'ThetaDev', cv_file) cover.create_cover_file(
tn_file, av_file, "ThetaDev @ Embedded World 2019", "ThetaDev", cv_file
)
cv_image = Image.open(cv_file) cv_image = Image.open(cv_file)
expected_cv_image = Image.open(expected_cv_file) expected_cv_image = Image.open(expected_cv_file)

86
tests/test_database.py Normal file
View file

@ -0,0 +1,86 @@
# coding=utf-8
import os
from datetime import datetime
import pytest
import sqlalchemy
from sqlalchemy import orm
# from ucast import models
from ucast import db
def test_insert_channel(testdb):
c1 = db.models.Channel(id="UCE1PLliRk3urTjDG6kGByiA", name="Natalie Gold")
session.add(c1)
session.commit()
c2 = db.models.Channel(id="UCGiJh0NZ52wRhYKYnuZI08Q", name="ThetaDev")
session.add(c2)
session.commit()
# stmt = sqlalchemy.select(models.Channel).where(models.Channel.name == "LinusTechTips")
# stmt = sqlalchemy.select(models.Channel)
# res = testdb.execute(stmt)
res = session.query(models.Channel).all()
assert len(res) == 2
assert res[0].name == "Natalie Gold"
assert res[1].name == "ThetaDev"
"""
@pytest.fixture(scope="session", autouse=True)
def testdb():
try:
os.remove("test.db")
except:
pass
# url = "sqlite:///:memory:"
url = "sqlite:///test.db"
engine = sqlalchemy.create_engine(url)
models.metadata.create_all(engine) # Create the tables.
return engine
def test_insert_channel(testdb):
session_maker = orm.sessionmaker(bind=testdb)
session = session_maker()
c1 = models.Channel(id="UCE1PLliRk3urTjDG6kGByiA", name="Natalie Gold")
session.add(c1)
session.commit()
c2 = models.Channel(id="UCGiJh0NZ52wRhYKYnuZI08Q", name="ThetaDev")
session.add(c2)
session.commit()
# stmt = sqlalchemy.select(models.Channel).where(models.Channel.name == "LinusTechTips")
# stmt = sqlalchemy.select(models.Channel)
# res = testdb.execute(stmt)
res = session.query(models.Channel).all()
assert len(res) == 2
assert res[0].name == "Natalie Gold"
assert res[1].name == "ThetaDev"
def test_insert_video(testdb):
session_maker = orm.sessionmaker(bind=testdb)
session = session_maker()
c1 = models.Channel(id="UC0QEucPrn0-Ddi3JBTcs5Kw", name="Saria Delaney")
session.add(c1)
session.commit()
v1 = models.Video(id="Bxhxzj8R_i0",
channel=c1,
title="Verschwiegen. Verraten. Verstummt. [Reupload: 10.10.2018]",
published=datetime(2020, 7, 4, 12, 21, 30),
description="")
v1.slug = v1.get_slug()
session.add(v1)
session.commit()
"""

View file

@ -3,5 +3,5 @@ __version__ = "0.0.1"
UCAST_BANNER = """\ UCAST_BANNER = """\
""" """

View file

@ -1,11 +1,11 @@
import os import os
import sys import sys
from alembic import config as alembic_cmd
from pathlib import Path
from importlib import resources from importlib import resources
from pathlib import Path
import uvicorn
import dotenv import dotenv
import uvicorn
from alembic import config as alembic_cmd
import ucast import ucast
@ -24,29 +24,36 @@ def print_banner():
def print_help(): def print_help():
print_banner() print_banner()
print(""" print(
"""
Available commands: Available commands:
run: start the server run: start the server
migrate: apply database migrations migrate: apply database migrations
alembic: run the alembic migrator alembic: run the alembic migrator
Configuration is read from the .env file or environment variables. Configuration is read from the .env file or environment variables.
Refer to the project page for more information: https://code.thetadev.de/HSA/Ucast""") Refer to the project page for more information: https://code.thetadev.de/HSA/Ucast"""
)
def run(): def run():
print_banner() print_banner()
load_dotenv() load_dotenv()
from ucast import config from ucast import config
uvicorn.run('ucast.app:create_app',
host="0.0.0.0", port=config.HTTP_PORT, factory=True, reload=config.DEBUG) uvicorn.run(
"ucast.app:create_app",
host="0.0.0.0",
port=config.HTTP_PORT,
factory=True,
reload=config.DEBUG,
)
def alembic(args): def alembic(args):
load_dotenv() load_dotenv()
alembic_ini_path = resources.path("ucast", "alembic.ini") alembic_ini_path = resources.path("ucast", "alembic.ini")
os.environ["ALEMBIC_CONFIG"] = str(alembic_ini_path) os.environ["ALEMBIC_CONFIG"] = str(alembic_ini_path)
os.chdir(alembic_ini_path.parent)
alembic_cmd.main(args, f"{sys.argv[0]} alembic") alembic_cmd.main(args, f"{sys.argv[0]} alembic")

View file

@ -2,7 +2,7 @@
[alembic] [alembic]
# path to migration scripts # path to migration scripts
script_location = migrations script_location = ucast:migrations
# template used to generate migration files # template used to generate migration files
file_template = %%(year)d-%%(month).2d-%%(day).2d_%%(rev)s_%%(slug)s file_template = %%(year)d-%%(month).2d-%%(day).2d_%%(rev)s_%%(slug)s

View file

@ -2,14 +2,17 @@
from starlette.applications import Starlette from starlette.applications import Starlette
from starlette.routing import Route from starlette.routing import Route
from ucast import views, config from ucast import config, views
def create_app(): def create_app():
app = Starlette(config.DEBUG, routes=[ app = Starlette(
Route("/", views.homepage), config.DEBUG,
Route("/err", views.error), routes=[
]) Route("/", views.homepage),
Route("/err", views.error),
],
)
if app.debug: if app.debug:
print("Debug mode enabled.") print("Debug mode enabled.")

View file

@ -6,7 +6,7 @@ from starlette_core.database import DatabaseURL
config = Config() config = Config()
# Basic configuration # Basic configuration
DEBUG = config('DEBUG', cast=bool, default=False) DEBUG = config("DEBUG", cast=bool, default=False)
DATABASE_URL = config('DATABASE_URL', cast=DatabaseURL) DATABASE_URL = config("DATABASE_URL", cast=DatabaseURL)
SECRET_KEY = config('SECRET_KEY', cast=Secret) SECRET_KEY = config("SECRET_KEY", cast=Secret)
HTTP_PORT = config('HTTP_PORT', cast=int, default=8000) HTTP_PORT = config("HTTP_PORT", cast=int, default=8000)

View file

@ -1,41 +1,43 @@
# coding=utf-8 # coding=utf-8
import math import math
from pathlib import Path from pathlib import Path
from typing import Tuple, List, Optional from typing import List, Optional, Tuple
from PIL import Image, ImageDraw, ImageFont
from colorthief import ColorThief
import wcag_contrast_ratio import wcag_contrast_ratio
from colorthief import ColorThief
from fonts.ttf import SourceSansPro from fonts.ttf import SourceSansPro
from PIL import Image, ImageDraw, ImageFont
from ucast import typ from ucast import typ
CHAR_ELLIPSIS = '' CHAR_ELLIPSIS = ""
COVER_WIDTH = 500 COVER_WIDTH = 500
def _split_text(height: int, width: int, text: str, font: ImageFont.FreeTypeFont, line_spacing=0) -> List[str]: def _split_text(
height: int, width: int, text: str, font: ImageFont.FreeTypeFont, line_spacing=0
) -> List[str]:
if height < font.size: if height < font.size:
return [] return []
max_lines = math.floor((height - font.size) / (font.size + line_spacing)) + 1 max_lines = math.floor((height - font.size) / (font.size + line_spacing)) + 1
lines = [] lines = []
line = '' line = ""
for word in text.split(' '): for word in text.split(" "):
if len(lines) >= max_lines: if len(lines) >= max_lines:
line = word line = word
break break
if line == '': if line == "":
nline = word nline = word
else: else:
nline = line + ' ' + word nline = line + " " + word
if font.getsize(nline)[0] <= width: if font.getsize(nline)[0] <= width:
line = nline line = nline
elif line != '': elif line != "":
lines.append(line) lines.append(line)
line = word line = word
else: else:
@ -47,14 +49,14 @@ def _split_text(height: int, width: int, text: str, font: ImageFont.FreeTypeFont
lines.append(nline_e) lines.append(nline_e)
break break
if line != '': if line != "":
if len(lines) >= max_lines: if len(lines) >= max_lines:
# Drop the last line and add ... to the end # Drop the last line and add ... to the end
lastline = lines[-1] + CHAR_ELLIPSIS lastline = lines[-1] + CHAR_ELLIPSIS
if font.getsize(lastline)[0] <= width: if font.getsize(lastline)[0] <= width:
lines[-1] = lastline lines[-1] = lastline
else: else:
i_last_space = lines[-1].rfind(' ') i_last_space = lines[-1].rfind(" ")
lines[-1] = lines[-1][:i_last_space] + CHAR_ELLIPSIS lines[-1] = lines[-1][:i_last_space] + CHAR_ELLIPSIS
else: else:
lines.append(line) lines.append(line)
@ -62,8 +64,15 @@ def _split_text(height: int, width: int, text: str, font: ImageFont.FreeTypeFont
return lines return lines
def _draw_text_box(draw: ImageDraw.ImageDraw, box: Tuple[int, int, int, int], text: str, font: ImageFont.FreeTypeFont, def _draw_text_box(
color: typ.Color = (0, 0, 0), line_spacing=0, vertical_center=True): draw: ImageDraw.ImageDraw,
box: Tuple[int, int, int, int],
text: str,
font: ImageFont.FreeTypeFont,
color: typ.Color = (0, 0, 0),
line_spacing=0,
vertical_center=True,
):
x_tl, y_tl, x_br, y_br = box x_tl, y_tl, x_br, y_br = box
height = y_br - y_tl height = y_br - y_tl
width = x_br - x_tl width = x_br - x_tl
@ -101,7 +110,9 @@ def _get_text_color(bg_color) -> typ.Color:
return 0, 0, 0 return 0, 0, 0
def _create_cover_image(thumbnail: Image.Image, avatar: Optional[Image.Image], title: str, channel: str) -> Image.Image: def _create_cover_image(
thumbnail: Image.Image, avatar: Optional[Image.Image], title: str, channel: str
) -> Image.Image:
# Scale the thumbnail image down to cover size # Scale the thumbnail image down to cover size
tn_height = int(COVER_WIDTH / thumbnail.width * thumbnail.height) tn_height = int(COVER_WIDTH / thumbnail.width * thumbnail.height)
tn = thumbnail.resize((COVER_WIDTH, tn_height), Image.Resampling.LANCZOS) tn = thumbnail.resize((COVER_WIDTH, tn_height), Image.Resampling.LANCZOS)
@ -113,11 +124,13 @@ def _create_cover_image(thumbnail: Image.Image, avatar: Optional[Image.Image], t
bottom_color = _get_dominant_color(bottom_part) bottom_color = _get_dominant_color(bottom_part)
# Create new cover image # Create new cover image
cover = Image.new('RGB', (COVER_WIDTH, COVER_WIDTH)) cover = Image.new("RGB", (COVER_WIDTH, COVER_WIDTH))
cover_draw = ImageDraw.Draw(cover) cover_draw = ImageDraw.Draw(cover)
# Draw background gradient # Draw background gradient
for i, color in enumerate(_interpolate_color(top_color, bottom_color, cover.height)): for i, color in enumerate(
_interpolate_color(top_color, bottom_color, cover.height)
):
cover_draw.line(((0, i), (cover.width, i)), tuple(color), 1) cover_draw.line(((0, i), (cover.width, i)), tuple(color), 1)
# Insert thumbnail image in the middle # Insert thumbnail image in the middle
@ -134,7 +147,7 @@ def _create_cover_image(thumbnail: Image.Image, avatar: Optional[Image.Image], t
avt = avatar.resize((avt_size, avt_size), Image.Resampling.LANCZOS) avt = avatar.resize((avt_size, avt_size), Image.Resampling.LANCZOS)
circle_mask = Image.new('L', (avt_size, avt_size)) circle_mask = Image.new("L", (avt_size, avt_size))
circle_mask_draw = ImageDraw.Draw(circle_mask) circle_mask_draw = ImageDraw.Draw(circle_mask)
circle_mask_draw.ellipse((0, 0, avt_size, avt_size), 255) circle_mask_draw.ellipse((0, 0, avt_size, avt_size), 255)
@ -150,18 +163,43 @@ def _create_cover_image(thumbnail: Image.Image, avatar: Optional[Image.Image], t
top_text_color = _get_text_color(top_color) top_text_color = _get_text_color(top_color)
bottom_text_color = _get_text_color(bottom_color) bottom_text_color = _get_text_color(bottom_color)
_draw_text_box(cover_draw, (text_margin_topleft, text_vertical_offset, COVER_WIDTH - text_margin_x, tn_margin), _draw_text_box(
channel, cover_draw,
fnt, top_text_color, text_line_space) (
_draw_text_box(cover_draw, text_margin_topleft,
(text_margin_x, COVER_WIDTH - tn_margin + text_vertical_offset, text_vertical_offset,
COVER_WIDTH - text_margin_x, COVER_WIDTH), title, fnt, bottom_text_color, text_line_space) COVER_WIDTH - text_margin_x,
tn_margin,
),
channel,
fnt,
top_text_color,
text_line_space,
)
_draw_text_box(
cover_draw,
(
text_margin_x,
COVER_WIDTH - tn_margin + text_vertical_offset,
COVER_WIDTH - text_margin_x,
COVER_WIDTH,
),
title,
fnt,
bottom_text_color,
text_line_space,
)
return cover return cover
def create_cover_file(thumbnail_path: Path, avatar_path: Optional[Path], title: str, channel: str, def create_cover_file(
cover_path: Path): thumbnail_path: Path,
avatar_path: Optional[Path],
title: str,
channel: str,
cover_path: Path,
):
thumbnail = Image.open(thumbnail_path) thumbnail = Image.open(thumbnail_path)
avatar = None avatar = None

View file

@ -1,8 +1,8 @@
# coding=utf-8 # coding=utf-8
from starlette_core.database import Database, metadata from starlette_core.database import Database, metadata # noqa: F401
from ucast import models # noqa: F401
from ucast.config import DATABASE_URL from ucast.config import DATABASE_URL
from ucast import models
# set db config options # set db config options
if DATABASE_URL.driver == "psycopg2": if DATABASE_URL.driver == "psycopg2":

View file

@ -1,8 +1,7 @@
from logging.config import fileConfig from logging.config import fileConfig
from sqlalchemy import engine_from_config
from sqlalchemy import pool
from alembic import context from alembic import context
from sqlalchemy import engine_from_config, pool
from ucast import db from ucast import db
@ -10,7 +9,7 @@ from ucast import db
# access to the values within the .ini file in use. # access to the values within the .ini file in use.
config = context.config config = context.config
config.set_main_option('sqlalchemy.url', str(db.DATABASE_URL)) config.set_main_option("sqlalchemy.url", str(db.DATABASE_URL))
target_metadata = db.metadata target_metadata = db.metadata
# Interpret the config file for Python logging. # Interpret the config file for Python logging.
@ -63,9 +62,7 @@ def run_migrations_online():
) )
with connectable.connect() as connection: with connectable.connect() as connection:
context.configure( context.configure(connection=connection, target_metadata=target_metadata)
connection=connection, target_metadata=target_metadata
)
with context.begin_transaction(): with context.begin_transaction():
context.run_migrations() context.run_migrations()

View file

@ -1,16 +1,15 @@
"""Initial revision """Initial revision
Revision ID: 0ae786127cd8 Revision ID: 0ae786127cd8
Revises: Revises:
Create Date: 2022-05-03 10:03:42.224721 Create Date: 2022-05-03 10:03:42.224721
""" """
from alembic import op
import sqlalchemy as sa import sqlalchemy as sa
from alembic import op
# revision identifiers, used by Alembic. # revision identifiers, used by Alembic.
revision = '0ae786127cd8' revision = "0ae786127cd8"
down_revision = None down_revision = None
branch_labels = None branch_labels = None
depends_on = None depends_on = None
@ -18,30 +17,36 @@ depends_on = None
def upgrade(): def upgrade():
# ### commands auto generated by Alembic - please adjust! ### # ### commands auto generated by Alembic - please adjust! ###
op.create_table('channels', op.create_table(
sa.Column('id', sa.String(length=30), nullable=False), "channels",
sa.Column('name', sa.Unicode(length=100), nullable=False), sa.Column("id", sa.String(length=30), nullable=False),
sa.Column('active', sa.Boolean(), nullable=False), sa.Column("name", sa.Unicode(length=100), nullable=False),
sa.Column('skip_livestreams', sa.Boolean(), nullable=False), sa.Column("active", sa.Boolean(), nullable=False),
sa.Column('skip_shorts', sa.Boolean(), nullable=False), sa.Column("skip_livestreams", sa.Boolean(), nullable=False),
sa.Column('keep_videos', sa.Integer(), nullable=True), sa.Column("skip_shorts", sa.Boolean(), nullable=False),
sa.PrimaryKeyConstraint('id') sa.Column("keep_videos", sa.Integer(), nullable=True),
sa.PrimaryKeyConstraint("id"),
) )
op.create_table('videos', op.create_table(
sa.Column('id', sa.String(length=30), nullable=False), "videos",
sa.Column('channel_id', sa.String(length=30), nullable=False), sa.Column("id", sa.String(length=30), nullable=False),
sa.Column('title', sa.Unicode(length=200), nullable=False), sa.Column("channel_id", sa.String(length=30), nullable=False),
sa.Column('slug', sa.String(length=209), nullable=False), sa.Column("title", sa.Unicode(length=200), nullable=False),
sa.Column('published', sa.DateTime(), nullable=False), sa.Column("slug", sa.String(length=209), nullable=False),
sa.Column('description', sa.UnicodeText(), nullable=False), sa.Column("published", sa.DateTime(), nullable=False),
sa.ForeignKeyConstraint(['channel_id'], ['channels.id'], ), sa.Column("downloaded", sa.DateTime(), nullable=True),
sa.PrimaryKeyConstraint('id') sa.Column("description", sa.UnicodeText(), nullable=False),
sa.ForeignKeyConstraint(
["channel_id"],
["channels.id"],
),
sa.PrimaryKeyConstraint("id"),
) )
# ### end Alembic commands ### # ### end Alembic commands ###
def downgrade(): def downgrade():
# ### commands auto generated by Alembic - please adjust! ### # ### commands auto generated by Alembic - please adjust! ###
op.drop_table('videos') op.drop_table("videos")
op.drop_table('channels') op.drop_table("channels")
# ### end Alembic commands ### # ### end Alembic commands ###

View file

@ -1,3 +0,0 @@
# coding=utf-8

View file

@ -1,9 +1,8 @@
# coding=utf-8 # coding=utf-8
import slugify
import sqlalchemy as sa import sqlalchemy as sa
from sqlalchemy import orm from sqlalchemy import orm
from starlette_core.database import Base from starlette_core.database import Base
import slugify
# metadata = sa.MetaData() # metadata = sa.MetaData()
# Base = declarative_base(metadata=metadata) # Base = declarative_base(metadata=metadata)
@ -30,6 +29,7 @@ class Video(Base):
title = sa.Column(sa.Unicode(200), nullable=False) title = sa.Column(sa.Unicode(200), nullable=False)
slug = sa.Column(sa.String(209), nullable=False) slug = sa.Column(sa.String(209), nullable=False)
published = sa.Column(sa.DateTime, nullable=False) published = sa.Column(sa.DateTime, nullable=False)
downloaded = sa.Column(sa.DateTime, nullable=True)
description = sa.Column(sa.UnicodeText(), nullable=False, default="") description = sa.Column(sa.UnicodeText(), nullable=False, default="")
def get_slug(self) -> str: def get_slug(self) -> str:

View file

@ -2,7 +2,7 @@
import os import os
from pathlib import Path from pathlib import Path
UCAST_DIRNAME = '.ucast' UCAST_DIRNAME = ".ucast"
class ChannelFolder: class ChannelFolder:
@ -10,11 +10,11 @@ class ChannelFolder:
self.dir_root = dir_root self.dir_root = dir_root
dir_ucast = self.dir_root / UCAST_DIRNAME dir_ucast = self.dir_root / UCAST_DIRNAME
self.file_videos = dir_ucast / 'videos.json' self.file_videos = dir_ucast / "videos.json"
self.file_options = dir_ucast / 'options.json' self.file_options = dir_ucast / "options.json"
self.file_avatar = dir_ucast / 'avatar.png' self.file_avatar = dir_ucast / "avatar.png"
self.file_feed = dir_ucast / 'feed.xml' self.file_feed = dir_ucast / "feed.xml"
self.dir_covers = dir_ucast / 'covers' self.dir_covers = dir_ucast / "covers"
def does_exist(self) -> bool: def does_exist(self) -> bool:
return os.path.isdir(self.dir_covers) return os.path.isdir(self.dir_covers)
@ -31,14 +31,14 @@ class Storage:
def get_channel_folder(self, channel_name: str): def get_channel_folder(self, channel_name: str):
cf = ChannelFolder(self.dir_data / channel_name) cf = ChannelFolder(self.dir_data / channel_name)
if not cf.does_exist(): if not cf.does_exist():
raise FileNotFoundError('channel folder does not exist') raise FileNotFoundError("channel folder does not exist")
return cf return cf
def create_channel_folder(self, channel_name: str): def create_channel_folder(self, channel_name: str):
cf = ChannelFolder(self.dir_data / channel_name) cf = ChannelFolder(self.dir_data / channel_name)
if cf.does_exist(): if cf.does_exist():
raise FileExistsError('channel folder already exists') raise FileExistsError("channel folder already exists")
cf.create() cf.create()
return cf return cf

View file

@ -1,8 +1,9 @@
# coding=utf-8 # coding=utf-8
import requests
from pathlib import Path from pathlib import Path
import requests
def download_file(url: str, download_path: Path): def download_file(url: str, download_path: Path):
r = requests.get(url, allow_redirects=True) r = requests.get(url, allow_redirects=True)
open(download_path, 'wb').write(r.content) open(download_path, "wb").write(r.content)

View file

@ -2,7 +2,7 @@
from starlette.requests import Request from starlette.requests import Request
from starlette.responses import Response from starlette.responses import Response
from ucast import db # from ucast import db
async def homepage(request: Request) -> Response: async def homepage(request: Request) -> Response:

View file

@ -1,16 +1,16 @@
# coding=utf-8 # coding=utf-8
from operator import itemgetter
import json import json
from dataclasses import dataclass from dataclasses import dataclass
from operator import itemgetter
from yt_dlp import YoutubeDL
from scrapetube import scrapetube
import requests import requests
from scrapetube import scrapetube
from yt_dlp import YoutubeDL
def get_thumbnail_url(vinfo): def get_thumbnail_url(vinfo):
"""Get the best quality thumbnail""" """Get the best quality thumbnail"""
return max(vinfo['thumbnails'], key=itemgetter('preference'))['url'] return max(vinfo["thumbnails"], key=itemgetter("preference"))["url"]
def get_video_info(video_id): def get_video_info(video_id):
@ -20,29 +20,25 @@ def get_video_info(video_id):
def download_video(video_id, download_path, sponsorblock=False): def download_video(video_id, download_path, sponsorblock=False):
ydl_params = { ydl_params = {
'format': 'bestaudio', "format": "bestaudio",
'postprocessors': [ "postprocessors": [
{ {"key": "FFmpegExtractAudio", "preferredcodec": "mp3"},
'key': 'FFmpegExtractAudio',
'preferredcodec': 'mp3'
},
], ],
'outtmpl': download_path, "outtmpl": download_path,
} }
if sponsorblock: if sponsorblock:
# noinspection PyTypeChecker # noinspection PyTypeChecker
ydl_params['postprocessors'].extend([ ydl_params["postprocessors"].extend(
{ [
'key': 'SponsorBlock', {
'categories': ['sponsor'], "key": "SponsorBlock",
'when': 'after_filter' "categories": ["sponsor"],
}, "when": "after_filter",
{ },
'key': 'ModifyChapters', {"key": "ModifyChapters", "remove_sponsor_segments": ["sponsor"]},
'remove_sponsor_segments': ['sponsor'] ]
} )
])
with YoutubeDL(ydl_params) as ydl: with YoutubeDL(ydl_params) as ydl:
# extract_info downloads the video and returns its metadata # extract_info downloads the video and returns its metadata
@ -61,7 +57,8 @@ def get_channel_metadata(channel_url):
session = requests.Session() session = requests.Session()
session.headers[ session.headers[
"User-Agent" "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" ] = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 \
(KHTML, like Gecko) Chrome/91.0.4472.101 Safari/537.36"
url = f"{channel_url}/videos?view=0&flow=grid" url = f"{channel_url}/videos?view=0&flow=grid"
@ -69,11 +66,11 @@ def get_channel_metadata(channel_url):
data = json.loads( data = json.loads(
scrapetube.get_json_from_html(html, "var ytInitialData = ", 0, "};") + "}" scrapetube.get_json_from_html(html, "var ytInitialData = ", 0, "};") + "}"
) )
metadata = data['metadata']['channelMetadataRenderer'] metadata = data["metadata"]["channelMetadataRenderer"]
channel_id = metadata['externalId'] channel_id = metadata["externalId"]
name = metadata['title'] name = metadata["title"]
description = metadata['description'] description = metadata["description"]
avatar = metadata['avatar']['thumbnails'][0]['url'] avatar = metadata["avatar"]["thumbnails"][0]["url"]
return ChannelMetadata(channel_id, name, description, avatar) return ChannelMetadata(channel_id, name, description, avatar)