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)
- Slug: str (YYYYMMDD_Title, used as filename), VARCHAR(209)
- Published: datetime
- Downloaded: datetime
- Description: str, VARCHAR(1000)
### Config

175
poetry.lock generated
View file

@ -101,6 +101,14 @@ python-versions = "*"
[package.dependencies]
pycparser = "*"
[[package]]
name = "cfgv"
version = "3.3.1"
description = "Validate configuration and produce human readable error messages."
category = "dev"
optional = false
python-versions = ">=3.6.1"
[[package]]
name = "charset-normalizer"
version = "2.0.12"
@ -156,6 +164,14 @@ tomli = {version = "*", optional = true, markers = "extra == \"toml\""}
[package.extras]
toml = ["tomli"]
[[package]]
name = "distlib"
version = "0.3.4"
description = "Distribution utilities"
category = "dev"
optional = false
python-versions = "*"
[[package]]
name = "feedparser"
version = "6.0.8"
@ -167,6 +183,18 @@ python-versions = ">=3.6"
[package.dependencies]
sgmllib3k = "*"
[[package]]
name = "filelock"
version = "3.6.0"
description = "A platform independent file lock."
category = "dev"
optional = false
python-versions = ">=3.7"
[package.extras]
docs = ["furo (>=2021.8.17b43)", "sphinx (>=4.1)", "sphinx-autodoc-typehints (>=1.12)"]
testing = ["covdefaults (>=1.2.0)", "coverage (>=4)", "pytest (>=4)", "pytest-cov", "pytest-timeout (>=1.4.2)"]
[[package]]
name = "font-source-sans-pro"
version = "0.0.1"
@ -202,6 +230,17 @@ category = "main"
optional = false
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]]
name = "idna"
version = "3.3"
@ -280,6 +319,22 @@ category = "main"
optional = false
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]]
name = "packaging"
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"]
tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"]
[[package]]
name = "platformdirs"
version = "2.5.2"
description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
category = "dev"
optional = false
python-versions = ">=3.7"
[package.extras]
docs = ["furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)", "sphinx (>=4)"]
test = ["appdirs (==1.4.4)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)", "pytest (>=6)"]
[[package]]
name = "pluggy"
version = "1.0.0"
@ -315,6 +382,30 @@ python-versions = ">=3.6"
dev = ["pre-commit", "tox"]
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]]
name = "py"
version = "1.11.0"
@ -595,6 +686,14 @@ category = "main"
optional = false
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]]
name = "tomli"
version = "2.0.1"
@ -648,6 +747,24 @@ h11 = ">=0.8"
[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)"]
[[package]]
name = "virtualenv"
version = "20.14.1"
description = "Virtual Python Environment builder"
category = "dev"
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7"
[package.dependencies]
distlib = ">=0.3.1,<1"
filelock = ">=3.2,<4"
platformdirs = ">=2,<3"
six = ">=1.9.0,<2"
[package.extras]
docs = ["proselint (>=0.10.2)", "sphinx (>=3)", "sphinx-argparse (>=0.2.5)", "sphinx-rtd-theme (>=0.4.3)", "towncrier (>=21.3)"]
testing = ["coverage (>=4)", "coverage-enable-subprocess (>=1)", "flaky (>=3)", "pytest (>=4)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.1)", "pytest-mock (>=2)", "pytest-randomly (>=1)", "pytest-timeout (>=1)", "packaging (>=20.0)"]
[[package]]
name = "wcag-contrast-ratio"
version = "0.9"
@ -697,7 +814,7 @@ websockets = "*"
[metadata]
lock-version = "1.1"
python-versions = "^3.10"
content-hash = "3e8f9ea28feca928ee5e81f883e97e3d4390c859dc6a7850544003aff517dade"
content-hash = "ff7d3c1941f088222a945efb832db370bad4daeee7298d08752aa4061f7c4846"
[metadata.files]
alembic = [
@ -872,6 +989,10 @@ cffi = [
{file = "cffi-1.15.0-cp39-cp39-win_amd64.whl", hash = "sha256:3773c4d81e6e818df2efbc7dd77325ca0dcb688116050fb2b3011218eda36139"},
{file = "cffi-1.15.0.tar.gz", hash = "sha256:920f0d66a896c2d99f0adbb391f990a84091179542c205fa53ce5787aff87954"},
]
cfgv = [
{file = "cfgv-3.3.1-py2.py3-none-any.whl", hash = "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426"},
{file = "cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736"},
]
charset-normalizer = [
{file = "charset-normalizer-2.0.12.tar.gz", hash = "sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597"},
{file = "charset_normalizer-2.0.12-py3-none-any.whl", hash = "sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df"},
@ -931,10 +1052,18 @@ coverage = [
{file = "coverage-6.3.2-pp36.pp37.pp38-none-any.whl", hash = "sha256:18d520c6860515a771708937d2f78f63cc47ab3b80cb78e86573b0a760161faf"},
{file = "coverage-6.3.2.tar.gz", hash = "sha256:03e2a7826086b91ef345ff18742ee9fc47a6839ccd517061ef8fa1976e652ce9"},
]
distlib = [
{file = "distlib-0.3.4-py2.py3-none-any.whl", hash = "sha256:6564fe0a8f51e734df6333d08b8b94d4ea8ee6b99b5ed50613f731fd4089f34b"},
{file = "distlib-0.3.4.zip", hash = "sha256:e4b58818180336dc9c529bfb9a0b58728ffc09ad92027a3f30b7cd91e3458579"},
]
feedparser = [
{file = "feedparser-6.0.8-py3-none-any.whl", hash = "sha256:1b7f57841d9cf85074deb316ed2c795091a238adb79846bc46dccdaf80f9c59a"},
{file = "feedparser-6.0.8.tar.gz", hash = "sha256:5ce0410a05ab248c8c7cfca3a0ea2203968ee9ff4486067379af4827a59f9661"},
]
filelock = [
{file = "filelock-3.6.0-py3-none-any.whl", hash = "sha256:f8314284bfffbdcfa0ff3d7992b023d4c628ced6feb957351d4c48d059f56bc0"},
{file = "filelock-3.6.0.tar.gz", hash = "sha256:9cd540a9352e432c7246a48fe4e8712b10acb1df2ad1f30e8c070b82ae1fed85"},
]
font-source-sans-pro = [
{file = "font-source-sans-pro-0.0.1.tar.gz", hash = "sha256:3f81d8e52b0d7e930e2c867c0d3ee549312d03f97b71b664a8361006311f72e5"},
{file = "font_source_sans_pro-0.0.1-py2-none-any.whl", hash = "sha256:685c8813d59941e84ea326f46d638871adbc825a0aa5205a72ee9ed9c5fbb471"},
@ -1006,6 +1135,10 @@ h11 = [
{file = "h11-0.13.0-py3-none-any.whl", hash = "sha256:8ddd78563b633ca55346c8cd41ec0af27d3c79931828beffb46ce70a379e7442"},
{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 = [
{file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"},
{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.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 = [
{file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"},
{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.tar.gz", hash = "sha256:f401ed2bbb155e1ade150ccc63db1a4f6c1909d3d378f7d1235a44e90d75fb97"},
]
platformdirs = [
{file = "platformdirs-2.5.2-py3-none-any.whl", hash = "sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788"},
{file = "platformdirs-2.5.2.tar.gz", hash = "sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19"},
]
pluggy = [
{file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"},
{file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"},
]
pre-commit = [
{file = "pre_commit-2.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 = [
{file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"},
{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-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 = [
{file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"},
{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.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 = [
{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"
click = "^8.1.3"
python-dotenv = "^0.20.0"
mysqlclient = "^2.1.0"
psycopg2 = "^2.9.3"
[tool.poetry.dev-dependencies]
pytest = "^7.1.2"
pytest-cov = "^3.0.0"
invoke = "^1.7.0"
pre-commit = "^2.18.1"
virtualenv = "20.14.1"
[tool.poetry.scripts]
ucast = "ucast.__main__:cli"
@ -34,3 +38,10 @@ ucast = "ucast.__main__:cli"
[build-system]
requires = ["poetry-core>=1.0.0"]
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 ucast import youtube, util, cover
import tests
from ucast import cover, util, youtube
os.chdir(Path(__file__).absolute().parent)
db_file = Path("_run/ucast.db").absolute()
@ -18,7 +18,7 @@ os.environ["DATABASE_URL"] = f"sqlite:///{db_file}"
@task
def test(c):
c.run('pytest tests', pty=True)
c.run("pytest tests", pty=True)
@task
@ -29,21 +29,21 @@ def run(c):
@task
def get_cover(c, vid=''):
def get_cover(c, vid=""):
vinfo = youtube.get_video_info(vid)
title = vinfo['fulltitle']
channel_name = vinfo['uploader']
title = vinfo["fulltitle"]
channel_name = vinfo["uploader"]
thumbnail_url = youtube.get_thumbnail_url(vinfo)
channel_url = vinfo['channel_url']
channel_url = vinfo["channel_url"]
channel_metadata = youtube.get_channel_metadata(channel_url)
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
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}.png'
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}.png"
util.download_file(thumbnail_url, tn_file)
util.download_file(channel_metadata.avatar_url, av_file)
@ -52,7 +52,7 @@ def get_cover(c, vid=''):
@task
def add_migration(c, m=''):
def add_migration(c, m=""):
if not m:
raise Exception("please input migration name")
@ -65,4 +65,4 @@ def add_migration(c, m=''):
os.chdir("ucast")
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
from importlib.resources import files
DIR_TESTFILES = files('tests.testfiles')
DIR_TESTFILES = files("tests.testfiles")

View file

@ -1,60 +1,89 @@
# coding=utf-8
from typing import List
import tempfile
from pathlib import Path
from typing import List
import pytest
from PIL import Image, ImageFont, ImageChops
from fonts.ttf import SourceSansPro
from PIL import Image, ImageChops, ImageFont
import tests
from ucast import cover, typ
@pytest.mark.parametrize('height,width,text,expect', [
(40, 300, 'Hello', ['Hello']),
(40, 300, 'Hello World, this is me', ['Hello World,…']),
(90, 300, 'Hello World, this is me', ['Hello World, this', 'is me']),
(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!']),
])
@pytest.mark.parametrize(
"height,width,text,expect",
[
(40, 300, "Hello", ["Hello"]),
(40, 300, "Hello World, this is me", ["Hello World,…"]),
(90, 300, "Hello World, this is me", ["Hello World, this", "is me"]),
(
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]):
font = ImageFont.truetype(SourceSansPro, 40)
lines = cover._split_text(height, width, text, font, 8)
assert lines == expect
@pytest.mark.parametrize('file_name,color', [
('t1.webp', (63, 63, 62)),
('t2.webp', (74, 45, 37)),
('t3.webp', (54, 24, 28)),
])
@pytest.mark.parametrize(
"file_name,color",
[
("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):
img = Image.open(tests.DIR_TESTFILES / 'thumbnail' / file_name)
img = Image.open(tests.DIR_TESTFILES / "thumbnail" / file_name)
c = cover._get_dominant_color(img)
assert c == color
@pytest.mark.parametrize('bg_color,text_color', [
((100, 0, 0), (255, 255, 255)),
((200, 200, 0), (0, 0, 0)),
])
@pytest.mark.parametrize(
"bg_color,text_color",
[
((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):
c = cover._get_text_color(bg_color)
assert c == text_color
@pytest.mark.parametrize('n_image,title,channel', [
(1, 'ThetaDev @ Embedded World 2019', 'ThetaDev'),
(2, 'Sintel - Open Movie by Blender Foundation', 'Blender'),
(3, 'Systemabsturz Teaser zur DiVOC bb3', 'media.ccc.de'),
])
@pytest.mark.parametrize(
"n_image,title,channel",
[
(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):
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}.png'
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}.png"
tn_image = Image.open(tn_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():
tn_file = tests.DIR_TESTFILES / 'thumbnail' / 't1.webp'
av_file = tests.DIR_TESTFILES / 'avatar' / 'a1.jpg'
expected_cv_file = tests.DIR_TESTFILES / 'cover' / 'c1.png'
tn_file = tests.DIR_TESTFILES / "thumbnail" / "t1.webp"
av_file = tests.DIR_TESTFILES / "avatar" / "a1.jpg"
expected_cv_file = tests.DIR_TESTFILES / "cover" / "c1.png"
tmpdir_o = tempfile.TemporaryDirectory()
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)
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

@ -1,11 +1,11 @@
import os
import sys
from alembic import config as alembic_cmd
from pathlib import Path
from importlib import resources
from pathlib import Path
import uvicorn
import dotenv
import uvicorn
from alembic import config as alembic_cmd
import ucast
@ -24,29 +24,36 @@ def print_banner():
def print_help():
print_banner()
print("""
print(
"""
Available commands:
run: start the server
migrate: apply database migrations
alembic: run the alembic migrator
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():
print_banner()
load_dotenv()
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):
load_dotenv()
alembic_ini_path = resources.path("ucast", "alembic.ini")
os.environ["ALEMBIC_CONFIG"] = str(alembic_ini_path)
os.chdir(alembic_ini_path.parent)
alembic_cmd.main(args, f"{sys.argv[0]} alembic")

View file

@ -2,7 +2,7 @@
[alembic]
# path to migration scripts
script_location = migrations
script_location = ucast:migrations
# template used to generate migration files
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.routing import Route
from ucast import views, config
from ucast import config, views
def create_app():
app = Starlette(config.DEBUG, routes=[
Route("/", views.homepage),
Route("/err", views.error),
])
app = Starlette(
config.DEBUG,
routes=[
Route("/", views.homepage),
Route("/err", views.error),
],
)
if app.debug:
print("Debug mode enabled.")

View file

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

View file

@ -1,41 +1,43 @@
# coding=utf-8
import math
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
from colorthief import ColorThief
from fonts.ttf import SourceSansPro
from PIL import Image, ImageDraw, ImageFont
from ucast import typ
CHAR_ELLIPSIS = ''
CHAR_ELLIPSIS = ""
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:
return []
max_lines = math.floor((height - font.size) / (font.size + line_spacing)) + 1
lines = []
line = ''
line = ""
for word in text.split(' '):
for word in text.split(" "):
if len(lines) >= max_lines:
line = word
break
if line == '':
if line == "":
nline = word
else:
nline = line + ' ' + word
nline = line + " " + word
if font.getsize(nline)[0] <= width:
line = nline
elif line != '':
elif line != "":
lines.append(line)
line = word
else:
@ -47,14 +49,14 @@ def _split_text(height: int, width: int, text: str, font: ImageFont.FreeTypeFont
lines.append(nline_e)
break
if line != '':
if line != "":
if len(lines) >= max_lines:
# Drop the last line and add ... to the end
lastline = lines[-1] + CHAR_ELLIPSIS
if font.getsize(lastline)[0] <= width:
lines[-1] = lastline
else:
i_last_space = lines[-1].rfind(' ')
i_last_space = lines[-1].rfind(" ")
lines[-1] = lines[-1][:i_last_space] + CHAR_ELLIPSIS
else:
lines.append(line)
@ -62,8 +64,15 @@ def _split_text(height: int, width: int, text: str, font: ImageFont.FreeTypeFont
return lines
def _draw_text_box(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):
def _draw_text_box(
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
height = y_br - y_tl
width = x_br - x_tl
@ -101,7 +110,9 @@ def _get_text_color(bg_color) -> typ.Color:
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
tn_height = int(COVER_WIDTH / thumbnail.width * thumbnail.height)
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)
# 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)
# 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)
# 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)
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.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)
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),
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)
_draw_text_box(
cover_draw,
(
text_margin_topleft,
text_vertical_offset,
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
def create_cover_file(thumbnail_path: Path, avatar_path: Optional[Path], title: str, channel: str,
cover_path: Path):
def create_cover_file(
thumbnail_path: Path,
avatar_path: Optional[Path],
title: str,
channel: str,
cover_path: Path,
):
thumbnail = Image.open(thumbnail_path)
avatar = None

View file

@ -1,8 +1,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 import models
# set db config options
if DATABASE_URL.driver == "psycopg2":

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,8 +1,9 @@
# coding=utf-8
import requests
from pathlib import Path
import requests
def download_file(url: str, download_path: Path):
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.responses import Response
from ucast import db
# from ucast import db
async def homepage(request: Request) -> Response:

View file

@ -1,16 +1,16 @@
# coding=utf-8
from operator import itemgetter
import json
from dataclasses import dataclass
from operator import itemgetter
from yt_dlp import YoutubeDL
from scrapetube import scrapetube
import requests
from scrapetube import scrapetube
from yt_dlp import YoutubeDL
def get_thumbnail_url(vinfo):
"""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):
@ -20,29 +20,25 @@ def get_video_info(video_id):
def download_video(video_id, download_path, sponsorblock=False):
ydl_params = {
'format': 'bestaudio',
'postprocessors': [
{
'key': 'FFmpegExtractAudio',
'preferredcodec': 'mp3'
},
"format": "bestaudio",
"postprocessors": [
{"key": "FFmpegExtractAudio", "preferredcodec": "mp3"},
],
'outtmpl': download_path,
"outtmpl": download_path,
}
if sponsorblock:
# noinspection PyTypeChecker
ydl_params['postprocessors'].extend([
{
'key': 'SponsorBlock',
'categories': ['sponsor'],
'when': 'after_filter'
},
{
'key': 'ModifyChapters',
'remove_sponsor_segments': ['sponsor']
}
])
ydl_params["postprocessors"].extend(
[
{
"key": "SponsorBlock",
"categories": ["sponsor"],
"when": "after_filter",
},
{"key": "ModifyChapters", "remove_sponsor_segments": ["sponsor"]},
]
)
with YoutubeDL(ydl_params) as ydl:
# extract_info downloads the video and returns its metadata
@ -61,7 +57,8 @@ def get_channel_metadata(channel_url):
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"
] = "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"
@ -69,11 +66,11 @@ def get_channel_metadata(channel_url):
data = json.loads(
scrapetube.get_json_from_html(html, "var ytInitialData = ", 0, "};") + "}"
)
metadata = data['metadata']['channelMetadataRenderer']
metadata = data["metadata"]["channelMetadataRenderer"]
channel_id = metadata['externalId']
name = metadata['title']
description = metadata['description']
avatar = metadata['avatar']['thumbnails'][0]['url']
channel_id = metadata["externalId"]
name = metadata["title"]
description = metadata["description"]
avatar = metadata["avatar"]["thumbnails"][0]["url"]
return ChannelMetadata(channel_id, name, description, avatar)