Compare commits
3 commits
a4cb344091
...
5def0f3c61
Author | SHA1 | Date | |
---|---|---|---|
5def0f3c61 | |||
cd0bcccc62 | |||
25fe3eb824 |
3
.gitignore
vendored
|
@ -2,6 +2,7 @@
|
|||
|
||||
# Python
|
||||
venv
|
||||
dist
|
||||
.tox
|
||||
__pycache__
|
||||
*.egg-info
|
||||
|
@ -17,3 +18,5 @@ __pycache__
|
|||
|
||||
# Application data
|
||||
/_run
|
||||
.env
|
||||
*.sqlite3
|
||||
|
|
21
.pre-commit-config.yaml
Normal 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
|
1
manage.py
Symbolic link
|
@ -0,0 +1 @@
|
|||
ucast/manage.py
|
|
@ -4,64 +4,50 @@
|
|||
|
||||
```txt
|
||||
_ config
|
||||
|_ config.py
|
||||
|_ config.toml
|
||||
_ data
|
||||
|_ LinusTechTips
|
||||
| |_ .ucast
|
||||
| | |_ videos.json # IDs und Metadaten aller heruntergeladenen Videos
|
||||
| | |_ options.json # Kanalspezifische Optionen (ID, enabled)
|
||||
| | |_ 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
|
||||
| |_ 220409_Building a _1_000_000 Computer.mp3
|
||||
| |_ 220410_Apple makes GREAT Gaming Computers.mp3
|
||||
|
|
||||
|_ Andreas Spiess
|
||||
|_ ...
|
||||
```
|
||||
|_ .ucast
|
||||
|_ videos.json # IDs und Metadaten aller heruntergeladenen Videos
|
||||
|_ options.json # Kanalspezifische Optionen (ID, LastScan)
|
||||
|_ avatar.png # Profilbild des Kanals
|
||||
|_ feed.xml # RSS-Feed
|
||||
|_ covers # Cover-Bilder
|
||||
|_ 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
|
||||
|
||||
## Verzeichnisstruktur (mit Datenbank)
|
||||
|
||||
```txt
|
||||
_ config
|
||||
|_ config.py
|
||||
_ data
|
||||
|_ ucast.db
|
||||
|
|
||||
|_ LinusTechTips
|
||||
| |_ .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
|
||||
| |_ 220409_Building a _1_000_000 Computer.mp3
|
||||
| |_ 220410_Apple makes GREAT Gaming Computers.mp3
|
||||
|
|
||||
|_ Andreas Spiess
|
||||
|_ ...
|
||||
```
|
||||
|
||||
## Datenmodelle
|
||||
|
||||
### Channel
|
||||
### LastScan
|
||||
|
||||
- ID: str, VARCHAR(30), PKEY
|
||||
- Name: str, VARCHAR(100)
|
||||
- LastScan: datetime
|
||||
|
||||
### ChannelOptions
|
||||
|
||||
- ID: str
|
||||
- Active: bool = True
|
||||
- LastScan: datetime
|
||||
- SkipLivestreams: bool = True
|
||||
- SkipShorts: bool = True
|
||||
- KeepVideos: int = -1
|
||||
|
||||
### Videos
|
||||
|
||||
- Videos: dict[id: str -> Video]
|
||||
|
||||
### Video
|
||||
|
||||
- ID: str, VARCHAR(30), PKEY
|
||||
- Title: str, VARCHAR(200)
|
||||
- Slug: str (YYMMDD_Title, used as filename), VARCHAR(207)
|
||||
- ID: str
|
||||
- Title: str
|
||||
- Slug: str (YYMMDD_Title, used as filename)
|
||||
- Published: datetime
|
||||
- Description: str, VARCHAR(1000)
|
||||
- Description: str
|
||||
|
||||
### Config
|
||||
|
||||
|
|
413
poetry.lock
generated
|
@ -1,17 +1,13 @@
|
|||
[[package]]
|
||||
name = "alembic"
|
||||
version = "1.7.7"
|
||||
description = "A database migration tool for SQLAlchemy."
|
||||
name = "asgiref"
|
||||
version = "3.5.1"
|
||||
description = "ASGI specs, helper code, and adapters"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
|
||||
[package.dependencies]
|
||||
Mako = "*"
|
||||
SQLAlchemy = ">=1.3.0"
|
||||
python-versions = ">=3.7"
|
||||
|
||||
[package.extras]
|
||||
tz = ["python-dateutil"]
|
||||
tests = ["pytest", "pytest-asyncio", "mypy (>=0.800)"]
|
||||
|
||||
[[package]]
|
||||
name = "atomicwrites"
|
||||
|
@ -84,22 +80,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,6 +113,23 @@ tomli = {version = "*", optional = true, markers = "extra == \"toml\""}
|
|||
[package.extras]
|
||||
toml = ["tomli"]
|
||||
|
||||
[[package]]
|
||||
name = "django"
|
||||
version = "4.0.4"
|
||||
description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
|
||||
[package.dependencies]
|
||||
asgiref = ">=3.4.1,<4"
|
||||
sqlparse = ">=0.2.2"
|
||||
tzdata = {version = "*", markers = "sys_platform == \"win32\""}
|
||||
|
||||
[package.extras]
|
||||
argon2 = ["argon2-cffi (>=19.1.0)"]
|
||||
bcrypt = ["bcrypt"]
|
||||
|
||||
[[package]]
|
||||
name = "feedparser"
|
||||
version = "6.0.8"
|
||||
|
@ -139,50 +141,6 @@ python-versions = ">=3.6"
|
|||
[package.dependencies]
|
||||
sgmllib3k = "*"
|
||||
|
||||
[[package]]
|
||||
name = "flask"
|
||||
version = "2.1.2"
|
||||
description = "A simple framework for building complex web applications."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
|
||||
[package.dependencies]
|
||||
click = ">=8.0"
|
||||
itsdangerous = ">=2.0"
|
||||
Jinja2 = ">=3.0"
|
||||
Werkzeug = ">=2.0"
|
||||
|
||||
[package.extras]
|
||||
async = ["asgiref (>=3.2)"]
|
||||
dotenv = ["python-dotenv"]
|
||||
|
||||
[[package]]
|
||||
name = "flask-alembic"
|
||||
version = "2.0.1"
|
||||
description = "Flask extension to integrate Alembic migrations"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
|
||||
[package.dependencies]
|
||||
alembic = ">=0.8"
|
||||
Flask = ">=0.10"
|
||||
Flask-SQLAlchemy = "*"
|
||||
SQLAlchemy = "*"
|
||||
|
||||
[[package]]
|
||||
name = "flask-sqlalchemy"
|
||||
version = "2.5.1"
|
||||
description = "Adds SQLAlchemy support to your Flask application."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">= 2.7, != 3.0.*, != 3.1.*, != 3.2.*, != 3.3.*"
|
||||
|
||||
[package.dependencies]
|
||||
Flask = ">=0.10"
|
||||
SQLAlchemy = ">=0.8.0"
|
||||
|
||||
[[package]]
|
||||
name = "font-source-sans-pro"
|
||||
version = "0.0.1"
|
||||
|
@ -199,17 +157,6 @@ category = "main"
|
|||
optional = false
|
||||
python-versions = "*"
|
||||
|
||||
[[package]]
|
||||
name = "greenlet"
|
||||
version = "1.1.2"
|
||||
description = "Lightweight in-process concurrent programming"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*"
|
||||
|
||||
[package.extras]
|
||||
docs = ["sphinx"]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "3.3"
|
||||
|
@ -234,52 +181,6 @@ category = "dev"
|
|||
optional = false
|
||||
python-versions = "*"
|
||||
|
||||
[[package]]
|
||||
name = "itsdangerous"
|
||||
version = "2.1.2"
|
||||
description = "Safely pass data to untrusted environments and back."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
|
||||
[[package]]
|
||||
name = "jinja2"
|
||||
version = "3.1.2"
|
||||
description = "A very fast and expressive template engine."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
|
||||
[package.dependencies]
|
||||
MarkupSafe = ">=2.0"
|
||||
|
||||
[package.extras]
|
||||
i18n = ["Babel (>=2.7)"]
|
||||
|
||||
[[package]]
|
||||
name = "mako"
|
||||
version = "1.2.0"
|
||||
description = "A super-fast templating language that borrows the best ideas from the existing templating languages."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
|
||||
[package.dependencies]
|
||||
MarkupSafe = ">=0.9.2"
|
||||
|
||||
[package.extras]
|
||||
babel = ["babel"]
|
||||
lingua = ["lingua"]
|
||||
testing = ["pytest"]
|
||||
|
||||
[[package]]
|
||||
name = "markupsafe"
|
||||
version = "2.1.1"
|
||||
description = "Safely add untrusted strings to HTML/XML markup."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
|
||||
[[package]]
|
||||
name = "mutagen"
|
||||
version = "1.45.1"
|
||||
|
@ -394,6 +295,21 @@ pytest = ">=4.6"
|
|||
[package.extras]
|
||||
testing = ["fields", "hunter", "process-tests", "six", "pytest-xdist", "virtualenv"]
|
||||
|
||||
[[package]]
|
||||
name = "pytest-django"
|
||||
version = "4.5.2"
|
||||
description = "A Django plugin for pytest."
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.5"
|
||||
|
||||
[package.dependencies]
|
||||
pytest = ">=5.4.0"
|
||||
|
||||
[package.extras]
|
||||
docs = ["sphinx", "sphinx-rtd-theme"]
|
||||
testing = ["django", "django-configurations (>=2.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "requests"
|
||||
version = "2.27.1"
|
||||
|
@ -441,36 +357,12 @@ optional = false
|
|||
python-versions = "*"
|
||||
|
||||
[[package]]
|
||||
name = "sqlalchemy"
|
||||
version = "1.4.36"
|
||||
description = "Database Abstraction Library"
|
||||
name = "sqlparse"
|
||||
version = "0.4.2"
|
||||
description = "A non-validating SQL parser."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7"
|
||||
|
||||
[package.dependencies]
|
||||
greenlet = {version = "!=0.4.17", markers = "python_version >= \"3\" and (platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\")"}
|
||||
|
||||
[package.extras]
|
||||
aiomysql = ["greenlet (!=0.4.17)", "aiomysql"]
|
||||
aiosqlite = ["typing_extensions (!=3.10.0.1)", "greenlet (!=0.4.17)", "aiosqlite"]
|
||||
asyncio = ["greenlet (!=0.4.17)"]
|
||||
asyncmy = ["greenlet (!=0.4.17)", "asyncmy (>=0.2.3,!=0.2.4)"]
|
||||
mariadb_connector = ["mariadb (>=1.0.1)"]
|
||||
mssql = ["pyodbc"]
|
||||
mssql_pymssql = ["pymssql"]
|
||||
mssql_pyodbc = ["pyodbc"]
|
||||
mypy = ["sqlalchemy2-stubs", "mypy (>=0.910)"]
|
||||
mysql = ["mysqlclient (>=1.4.0,<2)", "mysqlclient (>=1.4.0)"]
|
||||
mysql_connector = ["mysql-connector-python"]
|
||||
oracle = ["cx_oracle (>=7,<8)", "cx_oracle (>=7)"]
|
||||
postgresql = ["psycopg2 (>=2.7)"]
|
||||
postgresql_asyncpg = ["greenlet (!=0.4.17)", "asyncpg"]
|
||||
postgresql_pg8000 = ["pg8000 (>=1.16.6)"]
|
||||
postgresql_psycopg2binary = ["psycopg2-binary"]
|
||||
postgresql_psycopg2cffi = ["psycopg2cffi"]
|
||||
pymysql = ["pymysql (<1)", "pymysql"]
|
||||
sqlcipher = ["sqlcipher3-binary"]
|
||||
python-versions = ">=3.5"
|
||||
|
||||
[[package]]
|
||||
name = "tomli"
|
||||
|
@ -488,6 +380,14 @@ category = "main"
|
|||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
|
||||
[[package]]
|
||||
name = "tzdata"
|
||||
version = "2022.1"
|
||||
description = "Provider of IANA time zone data"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=2"
|
||||
|
||||
[[package]]
|
||||
name = "urllib3"
|
||||
version = "1.26.9"
|
||||
|
@ -501,14 +401,6 @@ brotli = ["brotlicffi (>=0.8.0)", "brotli (>=1.0.9)", "brotlipy (>=0.6.0)"]
|
|||
secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"]
|
||||
socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "uwsgi"
|
||||
version = "2.0.20"
|
||||
description = "The uWSGI server"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
|
||||
[[package]]
|
||||
name = "wcag-contrast-ratio"
|
||||
version = "0.9"
|
||||
|
@ -525,17 +417,6 @@ category = "main"
|
|||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
|
||||
[[package]]
|
||||
name = "werkzeug"
|
||||
version = "2.1.2"
|
||||
description = "The comprehensive WSGI web application library."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
|
||||
[package.extras]
|
||||
watchdog = ["watchdog"]
|
||||
|
||||
[[package]]
|
||||
name = "yt-dlp"
|
||||
version = "2022.4.8"
|
||||
|
@ -555,12 +436,12 @@ websockets = "*"
|
|||
[metadata]
|
||||
lock-version = "1.1"
|
||||
python-versions = "^3.10"
|
||||
content-hash = "a87f6de89028dd491dd5c09ef629084997f799bd9f80d773d80ff2102f1dc0ea"
|
||||
content-hash = "0288eb9eca14b78ee0cfac28cef0dcf17701165b6aa778255bfa73b94d5b2c4f"
|
||||
|
||||
[metadata.files]
|
||||
alembic = [
|
||||
{file = "alembic-1.7.7-py3-none-any.whl", hash = "sha256:29be0856ec7591c39f4e1cb10f198045d890e6e2274cf8da80cb5e721a09642b"},
|
||||
{file = "alembic-1.7.7.tar.gz", hash = "sha256:4961248173ead7ce8a21efb3de378f13b8398e6630fab0eb258dc74a8af24c58"},
|
||||
asgiref = [
|
||||
{file = "asgiref-3.5.1-py3-none-any.whl", hash = "sha256:45a429524fba18aba9d512498b19d220c4d628e75b40cf5c627524dbaebc5cc1"},
|
||||
{file = "asgiref-3.5.1.tar.gz", hash = "sha256:fddeea3c53fa99d0cdb613c3941cc6e52d822491fc2753fba25768fb5bf4e865"},
|
||||
]
|
||||
atomicwrites = [
|
||||
{file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"},
|
||||
|
@ -726,10 +607,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"},
|
||||
|
@ -781,22 +658,14 @@ 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"},
|
||||
]
|
||||
django = [
|
||||
{file = "Django-4.0.4-py3-none-any.whl", hash = "sha256:07c8638e7a7f548dc0acaaa7825d84b7bd42b10e8d22268b3d572946f1e9b687"},
|
||||
{file = "Django-4.0.4.tar.gz", hash = "sha256:4e8177858524417563cc0430f29ea249946d831eacb0068a1455686587df40b5"},
|
||||
]
|
||||
feedparser = [
|
||||
{file = "feedparser-6.0.8-py3-none-any.whl", hash = "sha256:1b7f57841d9cf85074deb316ed2c795091a238adb79846bc46dccdaf80f9c59a"},
|
||||
{file = "feedparser-6.0.8.tar.gz", hash = "sha256:5ce0410a05ab248c8c7cfca3a0ea2203968ee9ff4486067379af4827a59f9661"},
|
||||
]
|
||||
flask = [
|
||||
{file = "Flask-2.1.2-py3-none-any.whl", hash = "sha256:fad5b446feb0d6db6aec0c3184d16a8c1f6c3e464b511649c8918a9be100b4fe"},
|
||||
{file = "Flask-2.1.2.tar.gz", hash = "sha256:315ded2ddf8a6281567edb27393010fe3406188bafbfe65a3339d5787d89e477"},
|
||||
]
|
||||
flask-alembic = [
|
||||
{file = "Flask-Alembic-2.0.1.tar.gz", hash = "sha256:05a1e6f4148dbfcc9280a393373bfbd250af6f9f4f0ca9f744ef8f7376a3deec"},
|
||||
{file = "Flask_Alembic-2.0.1-py2.py3-none-any.whl", hash = "sha256:7e67740b0b08d58dcae0c701d56b56e60f5fa4af907bb82b4cb0469229ba94ff"},
|
||||
]
|
||||
flask-sqlalchemy = [
|
||||
{file = "Flask-SQLAlchemy-2.5.1.tar.gz", hash = "sha256:2bda44b43e7cacb15d4e05ff3cc1f8bc97936cc464623424102bfc2c35e95912"},
|
||||
{file = "Flask_SQLAlchemy-2.5.1-py2.py3-none-any.whl", hash = "sha256:f12c3d4cc5cc7fdcc148b9527ea05671718c3ea45d50c7e732cceb33f574b390"},
|
||||
]
|
||||
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"},
|
||||
|
@ -807,63 +676,6 @@ fonts = [
|
|||
{file = "fonts-0.0.3-py3-none-any.whl", hash = "sha256:e5f551379088ab260c2537980c3ccdff8af93408d9d4fa3319388d2ee25b7b6d"},
|
||||
{file = "fonts-0.0.3.tar.gz", hash = "sha256:c626655b75a60715e118e44e270656fd22fd8f54252901ff6ebf1308ad01c405"},
|
||||
]
|
||||
greenlet = [
|
||||
{file = "greenlet-1.1.2-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:58df5c2a0e293bf665a51f8a100d3e9956febfbf1d9aaf8c0677cf70218910c6"},
|
||||
{file = "greenlet-1.1.2-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:aec52725173bd3a7b56fe91bc56eccb26fbdff1386ef123abb63c84c5b43b63a"},
|
||||
{file = "greenlet-1.1.2-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:833e1551925ed51e6b44c800e71e77dacd7e49181fdc9ac9a0bf3714d515785d"},
|
||||
{file = "greenlet-1.1.2-cp27-cp27m-win32.whl", hash = "sha256:aa5b467f15e78b82257319aebc78dd2915e4c1436c3c0d1ad6f53e47ba6e2713"},
|
||||
{file = "greenlet-1.1.2-cp27-cp27m-win_amd64.whl", hash = "sha256:40b951f601af999a8bf2ce8c71e8aaa4e8c6f78ff8afae7b808aae2dc50d4c40"},
|
||||
{file = "greenlet-1.1.2-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:95e69877983ea39b7303570fa6760f81a3eec23d0e3ab2021b7144b94d06202d"},
|
||||
{file = "greenlet-1.1.2-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:356b3576ad078c89a6107caa9c50cc14e98e3a6c4874a37c3e0273e4baf33de8"},
|
||||
{file = "greenlet-1.1.2-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:8639cadfda96737427330a094476d4c7a56ac03de7265622fcf4cfe57c8ae18d"},
|
||||
{file = "greenlet-1.1.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:97e5306482182170ade15c4b0d8386ded995a07d7cc2ca8f27958d34d6736497"},
|
||||
{file = "greenlet-1.1.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e6a36bb9474218c7a5b27ae476035497a6990e21d04c279884eb10d9b290f1b1"},
|
||||
{file = "greenlet-1.1.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:abb7a75ed8b968f3061327c433a0fbd17b729947b400747c334a9c29a9af6c58"},
|
||||
{file = "greenlet-1.1.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b336501a05e13b616ef81ce329c0e09ac5ed8c732d9ba7e3e983fcc1a9e86965"},
|
||||
{file = "greenlet-1.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:14d4f3cd4e8b524ae9b8aa567858beed70c392fdec26dbdb0a8a418392e71708"},
|
||||
{file = "greenlet-1.1.2-cp35-cp35m-macosx_10_14_x86_64.whl", hash = "sha256:17ff94e7a83aa8671a25bf5b59326ec26da379ace2ebc4411d690d80a7fbcf23"},
|
||||
{file = "greenlet-1.1.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:9f3cba480d3deb69f6ee2c1825060177a22c7826431458c697df88e6aeb3caee"},
|
||||
{file = "greenlet-1.1.2-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:fa877ca7f6b48054f847b61d6fa7bed5cebb663ebc55e018fda12db09dcc664c"},
|
||||
{file = "greenlet-1.1.2-cp35-cp35m-win32.whl", hash = "sha256:7cbd7574ce8e138bda9df4efc6bf2ab8572c9aff640d8ecfece1b006b68da963"},
|
||||
{file = "greenlet-1.1.2-cp35-cp35m-win_amd64.whl", hash = "sha256:903bbd302a2378f984aef528f76d4c9b1748f318fe1294961c072bdc7f2ffa3e"},
|
||||
{file = "greenlet-1.1.2-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:049fe7579230e44daef03a259faa24511d10ebfa44f69411d99e6a184fe68073"},
|
||||
{file = "greenlet-1.1.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:dd0b1e9e891f69e7675ba5c92e28b90eaa045f6ab134ffe70b52e948aa175b3c"},
|
||||
{file = "greenlet-1.1.2-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:7418b6bfc7fe3331541b84bb2141c9baf1ec7132a7ecd9f375912eca810e714e"},
|
||||
{file = "greenlet-1.1.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f9d29ca8a77117315101425ec7ec2a47a22ccf59f5593378fc4077ac5b754fce"},
|
||||
{file = "greenlet-1.1.2-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:21915eb821a6b3d9d8eefdaf57d6c345b970ad722f856cd71739493ce003ad08"},
|
||||
{file = "greenlet-1.1.2-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eff9d20417ff9dcb0d25e2defc2574d10b491bf2e693b4e491914738b7908168"},
|
||||
{file = "greenlet-1.1.2-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:b8c008de9d0daba7b6666aa5bbfdc23dcd78cafc33997c9b7741ff6353bafb7f"},
|
||||
{file = "greenlet-1.1.2-cp36-cp36m-win32.whl", hash = "sha256:32ca72bbc673adbcfecb935bb3fb1b74e663d10a4b241aaa2f5a75fe1d1f90aa"},
|
||||
{file = "greenlet-1.1.2-cp36-cp36m-win_amd64.whl", hash = "sha256:f0214eb2a23b85528310dad848ad2ac58e735612929c8072f6093f3585fd342d"},
|
||||
{file = "greenlet-1.1.2-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:b92e29e58bef6d9cfd340c72b04d74c4b4e9f70c9fa7c78b674d1fec18896dc4"},
|
||||
{file = "greenlet-1.1.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:fdcec0b8399108577ec290f55551d926d9a1fa6cad45882093a7a07ac5ec147b"},
|
||||
{file = "greenlet-1.1.2-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:93f81b134a165cc17123626ab8da2e30c0455441d4ab5576eed73a64c025b25c"},
|
||||
{file = "greenlet-1.1.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e12bdc622676ce47ae9abbf455c189e442afdde8818d9da983085df6312e7a1"},
|
||||
{file = "greenlet-1.1.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8c790abda465726cfb8bb08bd4ca9a5d0a7bd77c7ac1ca1b839ad823b948ea28"},
|
||||
{file = "greenlet-1.1.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f276df9830dba7a333544bd41070e8175762a7ac20350786b322b714b0e654f5"},
|
||||
{file = "greenlet-1.1.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c5d5b35f789a030ebb95bff352f1d27a93d81069f2adb3182d99882e095cefe"},
|
||||
{file = "greenlet-1.1.2-cp37-cp37m-win32.whl", hash = "sha256:64e6175c2e53195278d7388c454e0b30997573f3f4bd63697f88d855f7a6a1fc"},
|
||||
{file = "greenlet-1.1.2-cp37-cp37m-win_amd64.whl", hash = "sha256:b11548073a2213d950c3f671aa88e6f83cda6e2fb97a8b6317b1b5b33d850e06"},
|
||||
{file = "greenlet-1.1.2-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:9633b3034d3d901f0a46b7939f8c4d64427dfba6bbc5a36b1a67364cf148a1b0"},
|
||||
{file = "greenlet-1.1.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:eb6ea6da4c787111adf40f697b4e58732ee0942b5d3bd8f435277643329ba627"},
|
||||
{file = "greenlet-1.1.2-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:f3acda1924472472ddd60c29e5b9db0cec629fbe3c5c5accb74d6d6d14773478"},
|
||||
{file = "greenlet-1.1.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e859fcb4cbe93504ea18008d1df98dee4f7766db66c435e4882ab35cf70cac43"},
|
||||
{file = "greenlet-1.1.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:00e44c8afdbe5467e4f7b5851be223be68adb4272f44696ee71fe46b7036a711"},
|
||||
{file = "greenlet-1.1.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec8c433b3ab0419100bd45b47c9c8551248a5aee30ca5e9d399a0b57ac04651b"},
|
||||
{file = "greenlet-1.1.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2bde6792f313f4e918caabc46532aa64aa27a0db05d75b20edfc5c6f46479de2"},
|
||||
{file = "greenlet-1.1.2-cp38-cp38-win32.whl", hash = "sha256:288c6a76705dc54fba69fbcb59904ae4ad768b4c768839b8ca5fdadec6dd8cfd"},
|
||||
{file = "greenlet-1.1.2-cp38-cp38-win_amd64.whl", hash = "sha256:8d2f1fb53a421b410751887eb4ff21386d119ef9cde3797bf5e7ed49fb51a3b3"},
|
||||
{file = "greenlet-1.1.2-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:166eac03e48784a6a6e0e5f041cfebb1ab400b394db188c48b3a84737f505b67"},
|
||||
{file = "greenlet-1.1.2-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:572e1787d1460da79590bf44304abbc0a2da944ea64ec549188fa84d89bba7ab"},
|
||||
{file = "greenlet-1.1.2-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:be5f425ff1f5f4b3c1e33ad64ab994eed12fc284a6ea71c5243fd564502ecbe5"},
|
||||
{file = "greenlet-1.1.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b1692f7d6bc45e3200844be0dba153612103db241691088626a33ff1f24a0d88"},
|
||||
{file = "greenlet-1.1.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7227b47e73dedaa513cdebb98469705ef0d66eb5a1250144468e9c3097d6b59b"},
|
||||
{file = "greenlet-1.1.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ff61ff178250f9bb3cd89752df0f1dd0e27316a8bd1465351652b1b4a4cdfd3"},
|
||||
{file = "greenlet-1.1.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:0051c6f1f27cb756ffc0ffbac7d2cd48cb0362ac1736871399a739b2885134d3"},
|
||||
{file = "greenlet-1.1.2-cp39-cp39-win32.whl", hash = "sha256:f70a9e237bb792c7cc7e44c531fd48f5897961701cdaa06cf22fc14965c496cf"},
|
||||
{file = "greenlet-1.1.2-cp39-cp39-win_amd64.whl", hash = "sha256:013d61294b6cd8fe3242932c1c5e36e5d1db2c8afb58606c5a67efce62c1f5fd"},
|
||||
{file = "greenlet-1.1.2.tar.gz", hash = "sha256:e30f5ea4ae2346e62cedde8794a56858a67b878dd79f7df76a0767e356b1744a"},
|
||||
]
|
||||
idna = [
|
||||
{file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"},
|
||||
{file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"},
|
||||
|
@ -876,60 +688,6 @@ invoke = [
|
|||
{file = "invoke-1.7.0-py3-none-any.whl", hash = "sha256:a5159fc63dba6ca2a87a1e33d282b99cea69711b03c64a35bb4e1c53c6c4afa0"},
|
||||
{file = "invoke-1.7.0.tar.gz", hash = "sha256:e332e49de40463f2016315f51df42313855772be86435686156bc18f45b5cc6c"},
|
||||
]
|
||||
itsdangerous = [
|
||||
{file = "itsdangerous-2.1.2-py3-none-any.whl", hash = "sha256:2c2349112351b88699d8d4b6b075022c0808887cb7ad10069318a8b0bc88db44"},
|
||||
{file = "itsdangerous-2.1.2.tar.gz", hash = "sha256:5dbbc68b317e5e42f327f9021763545dc3fc3bfe22e6deb96aaf1fc38874156a"},
|
||||
]
|
||||
jinja2 = [
|
||||
{file = "Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"},
|
||||
{file = "Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852"},
|
||||
]
|
||||
mako = [
|
||||
{file = "Mako-1.2.0-py3-none-any.whl", hash = "sha256:23aab11fdbbb0f1051b93793a58323ff937e98e34aece1c4219675122e57e4ba"},
|
||||
{file = "Mako-1.2.0.tar.gz", hash = "sha256:9a7c7e922b87db3686210cf49d5d767033a41d4010b284e747682c92bddd8b39"},
|
||||
]
|
||||
markupsafe = [
|
||||
{file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:86b1f75c4e7c2ac2ccdaec2b9022845dbb81880ca318bb7a0a01fbf7813e3812"},
|
||||
{file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f121a1420d4e173a5d96e47e9a0c0dcff965afdf1626d28de1460815f7c4ee7a"},
|
||||
{file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a49907dd8420c5685cfa064a1335b6754b74541bbb3706c259c02ed65b644b3e"},
|
||||
{file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10c1bfff05d95783da83491be968e8fe789263689c02724e0c691933c52994f5"},
|
||||
{file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b7bd98b796e2b6553da7225aeb61f447f80a1ca64f41d83612e6139ca5213aa4"},
|
||||
{file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b09bf97215625a311f669476f44b8b318b075847b49316d3e28c08e41a7a573f"},
|
||||
{file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:694deca8d702d5db21ec83983ce0bb4b26a578e71fbdbd4fdcd387daa90e4d5e"},
|
||||
{file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:efc1913fd2ca4f334418481c7e595c00aad186563bbc1ec76067848c7ca0a933"},
|
||||
{file = "MarkupSafe-2.1.1-cp310-cp310-win32.whl", hash = "sha256:4a33dea2b688b3190ee12bd7cfa29d39c9ed176bda40bfa11099a3ce5d3a7ac6"},
|
||||
{file = "MarkupSafe-2.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:dda30ba7e87fbbb7eab1ec9f58678558fd9a6b8b853530e176eabd064da81417"},
|
||||
{file = "MarkupSafe-2.1.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:671cd1187ed5e62818414afe79ed29da836dde67166a9fac6d435873c44fdd02"},
|
||||
{file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3799351e2336dc91ea70b034983ee71cf2f9533cdff7c14c90ea126bfd95d65a"},
|
||||
{file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e72591e9ecd94d7feb70c1cbd7be7b3ebea3f548870aa91e2732960fa4d57a37"},
|
||||
{file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6fbf47b5d3728c6aea2abb0589b5d30459e369baa772e0f37a0320185e87c980"},
|
||||
{file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d5ee4f386140395a2c818d149221149c54849dfcfcb9f1debfe07a8b8bd63f9a"},
|
||||
{file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:bcb3ed405ed3222f9904899563d6fc492ff75cce56cba05e32eff40e6acbeaa3"},
|
||||
{file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:e1c0b87e09fa55a220f058d1d49d3fb8df88fbfab58558f1198e08c1e1de842a"},
|
||||
{file = "MarkupSafe-2.1.1-cp37-cp37m-win32.whl", hash = "sha256:8dc1c72a69aa7e082593c4a203dcf94ddb74bb5c8a731e4e1eb68d031e8498ff"},
|
||||
{file = "MarkupSafe-2.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:97a68e6ada378df82bc9f16b800ab77cbf4b2fada0081794318520138c088e4a"},
|
||||
{file = "MarkupSafe-2.1.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e8c843bbcda3a2f1e3c2ab25913c80a3c5376cd00c6e8c4a86a89a28c8dc5452"},
|
||||
{file = "MarkupSafe-2.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0212a68688482dc52b2d45013df70d169f542b7394fc744c02a57374a4207003"},
|
||||
{file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e576a51ad59e4bfaac456023a78f6b5e6e7651dcd383bcc3e18d06f9b55d6d1"},
|
||||
{file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b9fe39a2ccc108a4accc2676e77da025ce383c108593d65cc909add5c3bd601"},
|
||||
{file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:96e37a3dc86e80bf81758c152fe66dbf60ed5eca3d26305edf01892257049925"},
|
||||
{file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6d0072fea50feec76a4c418096652f2c3238eaa014b2f94aeb1d56a66b41403f"},
|
||||
{file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:089cf3dbf0cd6c100f02945abeb18484bd1ee57a079aefd52cffd17fba910b88"},
|
||||
{file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6a074d34ee7a5ce3effbc526b7083ec9731bb3cbf921bbe1d3005d4d2bdb3a63"},
|
||||
{file = "MarkupSafe-2.1.1-cp38-cp38-win32.whl", hash = "sha256:421be9fbf0ffe9ffd7a378aafebbf6f4602d564d34be190fc19a193232fd12b1"},
|
||||
{file = "MarkupSafe-2.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:fc7b548b17d238737688817ab67deebb30e8073c95749d55538ed473130ec0c7"},
|
||||
{file = "MarkupSafe-2.1.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e04e26803c9c3851c931eac40c695602c6295b8d432cbe78609649ad9bd2da8a"},
|
||||
{file = "MarkupSafe-2.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b87db4360013327109564f0e591bd2a3b318547bcef31b468a92ee504d07ae4f"},
|
||||
{file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99a2a507ed3ac881b975a2976d59f38c19386d128e7a9a18b7df6fff1fd4c1d6"},
|
||||
{file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56442863ed2b06d19c37f94d999035e15ee982988920e12a5b4ba29b62ad1f77"},
|
||||
{file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3ce11ee3f23f79dbd06fb3d63e2f6af7b12db1d46932fe7bd8afa259a5996603"},
|
||||
{file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:33b74d289bd2f5e527beadcaa3f401e0df0a89927c1559c8566c066fa4248ab7"},
|
||||
{file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:43093fb83d8343aac0b1baa75516da6092f58f41200907ef92448ecab8825135"},
|
||||
{file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8e3dcf21f367459434c18e71b2a9532d96547aef8a871872a5bd69a715c15f96"},
|
||||
{file = "MarkupSafe-2.1.1-cp39-cp39-win32.whl", hash = "sha256:d4306c36ca495956b6d568d276ac11fdd9c30a36f1b6eb928070dc5360b22e1c"},
|
||||
{file = "MarkupSafe-2.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:46d00d6cfecdde84d40e572d63735ef81423ad31184100411e6e3388d405e247"},
|
||||
{file = "MarkupSafe-2.1.1.tar.gz", hash = "sha256:7f91197cc9e48f989d12e4e6fbc46495c446636dfc81b9ccf50bb0ec74b91d4b"},
|
||||
]
|
||||
mutagen = [
|
||||
{file = "mutagen-1.45.1-py3-none-any.whl", hash = "sha256:9c9f243fcec7f410f138cb12c21c84c64fde4195481a30c9bfb05b5f003adfed"},
|
||||
{file = "mutagen-1.45.1.tar.gz", hash = "sha256:6397602efb3c2d7baebd2166ed85731ae1c1d475abca22090b7141ff5034b3e1"},
|
||||
|
@ -1031,6 +789,10 @@ pytest-cov = [
|
|||
{file = "pytest-cov-3.0.0.tar.gz", hash = "sha256:e7f0f5b1617d2210a2cabc266dfe2f4c75a8d32fb89eafb7ad9d06f6d076d470"},
|
||||
{file = "pytest_cov-3.0.0-py3-none-any.whl", hash = "sha256:578d5d15ac4a25e5f961c938b85a05b09fdaae9deef3bb6de9a6e766622ca7a6"},
|
||||
]
|
||||
pytest-django = [
|
||||
{file = "pytest-django-4.5.2.tar.gz", hash = "sha256:d9076f759bb7c36939dbdd5ae6633c18edfc2902d1a69fdbefd2426b970ce6c2"},
|
||||
{file = "pytest_django-4.5.2-py3-none-any.whl", hash = "sha256:c60834861933773109334fe5a53e83d1ef4828f2203a1d6a0fa9972f4f75ab3e"},
|
||||
]
|
||||
requests = [
|
||||
{file = "requests-2.27.1-py2.py3-none-any.whl", hash = "sha256:f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d"},
|
||||
{file = "requests-2.27.1.tar.gz", hash = "sha256:68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61"},
|
||||
|
@ -1044,43 +806,9 @@ scrapetube = [
|
|||
sgmllib3k = [
|
||||
{file = "sgmllib3k-1.0.0.tar.gz", hash = "sha256:7868fb1c8bfa764c1ac563d3cf369c381d1325d36124933a726f29fcdaa812e9"},
|
||||
]
|
||||
sqlalchemy = [
|
||||
{file = "SQLAlchemy-1.4.36-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:81e53bd383c2c33de9d578bfcc243f559bd3801a0e57f2bcc9a943c790662e0c"},
|
||||
{file = "SQLAlchemy-1.4.36-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:6e1fe00ee85c768807f2a139b83469c1e52a9ffd58a6eb51aa7aeb524325ab18"},
|
||||
{file = "SQLAlchemy-1.4.36-cp27-cp27m-win32.whl", hash = "sha256:d57ac32f8dc731fddeb6f5d1358b4ca5456e72594e664769f0a9163f13df2a31"},
|
||||
{file = "SQLAlchemy-1.4.36-cp27-cp27m-win_amd64.whl", hash = "sha256:fca8322e04b2dde722fcb0558682740eebd3bd239bea7a0d0febbc190e99dc15"},
|
||||
{file = "SQLAlchemy-1.4.36-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:53d2d9ee93970c969bc4e3c78b1277d7129554642f6ffea039c282c7dc4577bc"},
|
||||
{file = "SQLAlchemy-1.4.36-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:f0394a3acfb8925db178f7728adb38c027ed7e303665b225906bfa8099dc1ce8"},
|
||||
{file = "SQLAlchemy-1.4.36-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09c606d8238feae2f360b8742ffbe67741937eb0a05b57f536948d198a3def96"},
|
||||
{file = "SQLAlchemy-1.4.36-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8d07fe2de0325d06e7e73281e9a9b5e259fbd7cbfbe398a0433cbb0082ad8fa7"},
|
||||
{file = "SQLAlchemy-1.4.36-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5041474dcab7973baa91ec1f3112049a9dd4652898d6a95a6a895ff5c58beb6b"},
|
||||
{file = "SQLAlchemy-1.4.36-cp310-cp310-win32.whl", hash = "sha256:be094460930087e50fd08297db9d7aadaed8408ad896baf758e9190c335632da"},
|
||||
{file = "SQLAlchemy-1.4.36-cp310-cp310-win_amd64.whl", hash = "sha256:64d796e9af522162f7f2bf7a3c5531a0a550764c426782797bbeed809d0646c5"},
|
||||
{file = "SQLAlchemy-1.4.36-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:a0ae3aa2e86a4613f2d4c49eb7da23da536e6ce80b2bfd60bbb2f55fc02b0b32"},
|
||||
{file = "SQLAlchemy-1.4.36-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d50cb71c1dbed70646d521a0975fb0f92b7c3f84c61fa59e07be23a1aaeecfc"},
|
||||
{file = "SQLAlchemy-1.4.36-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:16abf35af37a3d5af92725fc9ec507dd9e9183d261c2069b6606d60981ed1c6e"},
|
||||
{file = "SQLAlchemy-1.4.36-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5864a83bd345871ad9699ce466388f836db7572003d67d9392a71998092210e3"},
|
||||
{file = "SQLAlchemy-1.4.36-cp36-cp36m-win32.whl", hash = "sha256:fbf8c09fe9728168f8cc1b40c239eab10baf9c422c18be7f53213d70434dea43"},
|
||||
{file = "SQLAlchemy-1.4.36-cp36-cp36m-win_amd64.whl", hash = "sha256:6e859fa96605027bd50d8e966db1c4e1b03e7b3267abbc4b89ae658c99393c58"},
|
||||
{file = "SQLAlchemy-1.4.36-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:166a3887ec355f7d2f12738f7fa25dc8ac541867147a255f790f2f41f614cb44"},
|
||||
{file = "SQLAlchemy-1.4.36-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e885548da361aa3f8a9433db4cfb335b2107e533bf314359ae3952821d84b3e"},
|
||||
{file = "SQLAlchemy-1.4.36-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:5c90ef955d429966d84326d772eb34333178737ebb669845f1d529eb00c75e72"},
|
||||
{file = "SQLAlchemy-1.4.36-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a052bd9f53004f8993c624c452dfad8ec600f572dd0ed0445fbe64b22f5570e"},
|
||||
{file = "SQLAlchemy-1.4.36-cp37-cp37m-win32.whl", hash = "sha256:dce3468bf1fc12374a1a732c9efd146ce034f91bb0482b602a9311cb6166a920"},
|
||||
{file = "SQLAlchemy-1.4.36-cp37-cp37m-win_amd64.whl", hash = "sha256:6cb4c4f57a20710cea277edf720d249d514e587f796b75785ad2c25e1c0fed26"},
|
||||
{file = "SQLAlchemy-1.4.36-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:e74ce103b81c375c3853b436297952ef8d7863d801dcffb6728d01544e5191b5"},
|
||||
{file = "SQLAlchemy-1.4.36-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b20c4178ead9bc398be479428568ff31b6c296eb22e75776273781a6551973f"},
|
||||
{file = "SQLAlchemy-1.4.36-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:af2587ae11400157753115612d6c6ad255143efba791406ad8a0cbcccf2edcb3"},
|
||||
{file = "SQLAlchemy-1.4.36-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:83cf3077712be9f65c9aaa0b5bc47bc1a44789fd45053e2e3ecd59ff17c63fe9"},
|
||||
{file = "SQLAlchemy-1.4.36-cp38-cp38-win32.whl", hash = "sha256:ce20f5da141f8af26c123ebaa1b7771835ca6c161225ce728962a79054f528c3"},
|
||||
{file = "SQLAlchemy-1.4.36-cp38-cp38-win_amd64.whl", hash = "sha256:316c7e5304dda3e3ad711569ac5d02698bbc71299b168ac56a7076b86259f7ea"},
|
||||
{file = "SQLAlchemy-1.4.36-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:f522214f6749bc073262529c056f7dfd660f3b5ec4180c5354d985eb7219801e"},
|
||||
{file = "SQLAlchemy-1.4.36-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2ecac4db8c1aa4a269f5829df7e706639a24b780d2ac46b3e485cbbd27ec0028"},
|
||||
{file = "SQLAlchemy-1.4.36-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:b3db741beaa983d4cbf9087558620e7787106319f7e63a066990a70657dd6b35"},
|
||||
{file = "SQLAlchemy-1.4.36-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ec89bf98cc6a0f5d1e28e3ad28e9be6f3b4bdbd521a4053c7ae8d5e1289a8a1"},
|
||||
{file = "SQLAlchemy-1.4.36-cp39-cp39-win32.whl", hash = "sha256:e12532c4d3f614678623da5d852f038ace1f01869b89f003ed6fe8c793f0c6a3"},
|
||||
{file = "SQLAlchemy-1.4.36-cp39-cp39-win_amd64.whl", hash = "sha256:cb441ca461bf97d00877b607f132772644b623518b39ced54da433215adce691"},
|
||||
{file = "SQLAlchemy-1.4.36.tar.gz", hash = "sha256:64678ac321d64a45901ef2e24725ec5e783f1f4a588305e196431447e7ace243"},
|
||||
sqlparse = [
|
||||
{file = "sqlparse-0.4.2-py3-none-any.whl", hash = "sha256:48719e356bb8b42991bdbb1e8b83223757b93789c00910a616a071910ca4a64d"},
|
||||
{file = "sqlparse-0.4.2.tar.gz", hash = "sha256:0c00730c74263a94e5a9919ade150dfc3b19c574389985446148402998287dae"},
|
||||
]
|
||||
tomli = [
|
||||
{file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"},
|
||||
|
@ -1090,13 +818,14 @@ 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"},
|
||||
]
|
||||
urllib3 = [
|
||||
{file = "urllib3-1.26.9-py2.py3-none-any.whl", hash = "sha256:44ece4d53fb1706f667c9bd1c648f5469a2ec925fcf3a776667042d645472c14"},
|
||||
{file = "urllib3-1.26.9.tar.gz", hash = "sha256:aabaf16477806a5e1dd19aa41f8c2b7950dd3c746362d7e3223dbe6de6ac448e"},
|
||||
]
|
||||
uwsgi = [
|
||||
{file = "uwsgi-2.0.20.tar.gz", hash = "sha256:88ab9867d8973d8ae84719cf233b7dafc54326fcaec89683c3f9f77c002cdff9"},
|
||||
]
|
||||
wcag-contrast-ratio = [
|
||||
{file = "wcag-contrast-ratio-0.9.tar.gz", hash = "sha256:69192b8e5c0a7d0dc5ff1187eeb3e398141633a4bde51c69c87f58fe87ed361c"},
|
||||
]
|
||||
|
@ -1150,10 +879,6 @@ websockets = [
|
|||
{file = "websockets-10.3-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:3eda1cb7e9da1b22588cefff09f0951771d6ee9fa8dbe66f5ae04cc5f26b2b55"},
|
||||
{file = "websockets-10.3.tar.gz", hash = "sha256:fc06cc8073c8e87072138ba1e431300e2d408f054b27047d047b549455066ff4"},
|
||||
]
|
||||
werkzeug = [
|
||||
{file = "Werkzeug-2.1.2-py3-none-any.whl", hash = "sha256:72a4b735692dd3135217911cbeaa1be5fa3f62bffb8745c5215420a03dc55255"},
|
||||
{file = "Werkzeug-2.1.2.tar.gz", hash = "sha256:1ce08e8093ed67d638d63879fd1ba3735817f7a80de3674d293f5984f25fb6e6"},
|
||||
]
|
||||
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"},
|
||||
|
|
|
@ -1,12 +1,16 @@
|
|||
[tool.poetry]
|
||||
name = "ucast"
|
||||
version = "0.1.0"
|
||||
version = "0.0.1"
|
||||
description = "YouTube to Podcast converter"
|
||||
authors = ["Theta-Dev <t.testboy@gmail.com>"]
|
||||
packages = [
|
||||
{ include = "ucast" },
|
||||
{ include = "yt2podcast" },
|
||||
]
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.10"
|
||||
Flask = "^2.1.1"
|
||||
Django = "^4.0.4"
|
||||
yt-dlp = "^2022.3.8"
|
||||
scrapetube = "^2.2.2"
|
||||
rfeed = "^1.1.1"
|
||||
|
@ -16,15 +20,26 @@ colorthief = "^0.2.1"
|
|||
wcag-contrast-ratio = "^0.9"
|
||||
font-source-sans-pro = "^0.0.1"
|
||||
fonts = "^0.0.3"
|
||||
Flask-SQLAlchemy = "^2.5.1"
|
||||
Flask-Alembic = "^2.0.1"
|
||||
uWSGI = "^2.0.20"
|
||||
|
||||
[tool.poetry.dev-dependencies]
|
||||
pytest = "^7.1.2"
|
||||
pytest = "^7.1.1"
|
||||
pytest-cov = "^3.0.0"
|
||||
invoke = "^1.7.0"
|
||||
pytest-django = "^4.5.2"
|
||||
|
||||
[tool.poetry.scripts]
|
||||
"ucast-manage" = "ucast.manage:main"
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core>=1.0.0"]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
|
||||
[tool.flake8]
|
||||
max-line-length = 88
|
||||
per-file-ignores = [
|
||||
"settings.py:E501",
|
||||
]
|
||||
|
||||
[tool.black]
|
||||
line-length = 88
|
||||
target-version = ['py310']
|
||||
|
|
30
tasks.py
|
@ -1,32 +1,40 @@
|
|||
import os
|
||||
from pathlib import Path
|
||||
|
||||
from invoke import task
|
||||
|
||||
from ucast import youtube, util, cover
|
||||
import tests
|
||||
from yt2podcast import tests
|
||||
from yt2podcast.service import cover, util, youtube
|
||||
|
||||
os.chdir(Path(__file__).absolute().parent)
|
||||
|
||||
|
||||
@task
|
||||
def test(c):
|
||||
c.run('pytest tests', pty=True)
|
||||
c.run("pytest tests", pty=True)
|
||||
|
||||
|
||||
@task
|
||||
def get_cover(c, vid=''):
|
||||
def makemigrations(c):
|
||||
c.run("python manage.py makemigrations yt2podcast")
|
||||
|
||||
|
||||
@task
|
||||
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)
|
||||
|
|
|
@ -1,84 +0,0 @@
|
|||
# coding=utf-8
|
||||
from typing import List
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from PIL import Image, ImageFont, ImageChops
|
||||
from fonts.ttf import SourceSansPro
|
||||
|
||||
import tests
|
||||
from ucast import cover, types
|
||||
|
||||
|
||||
@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)),
|
||||
])
|
||||
def test_get_dominant_color(file_name: str, color: types.Color):
|
||||
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)),
|
||||
])
|
||||
def test_get_text_color(bg_color: types.Color, text_color: types.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'),
|
||||
])
|
||||
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_image = Image.open(tn_file)
|
||||
av_image = Image.open(av_file)
|
||||
expected_cv_image = Image.open(expected_cv_file)
|
||||
|
||||
cv_image = cover._create_cover_image(tn_image, av_image, title, channel)
|
||||
|
||||
diff = ImageChops.difference(cv_image, expected_cv_image)
|
||||
assert diff.getbbox() is None
|
||||
|
||||
|
||||
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'
|
||||
|
||||
tmpdir_o = tempfile.TemporaryDirectory()
|
||||
tmpdir = Path(tmpdir_o.name)
|
||||
cv_file = tmpdir / 'cover.png'
|
||||
|
||||
cover.create_cover_file(tn_file, av_file, 'ThetaDev @ Embedded World 2019', 'ThetaDev', cv_file)
|
||||
|
||||
cv_image = Image.open(cv_file)
|
||||
expected_cv_image = Image.open(expected_cv_file)
|
||||
|
||||
diff = ImageChops.difference(cv_image, expected_cv_image)
|
||||
assert diff.getbbox() is None
|
27
ucast/app.py
|
@ -1,27 +0,0 @@
|
|||
# coding=utf-8
|
||||
from flask import Flask
|
||||
|
||||
from ucast.views import view
|
||||
|
||||
|
||||
def create_app(test_config=None):
|
||||
# create and configure the app
|
||||
app = Flask(__name__, instance_relative_config=True)
|
||||
|
||||
if test_config is None:
|
||||
# load the instance config, if it exists, when not testing
|
||||
app.config.from_pyfile('config.py', silent=True)
|
||||
else:
|
||||
# load the test config if passed in
|
||||
app.config.from_mapping(test_config)
|
||||
|
||||
print(app.instance_path)
|
||||
|
||||
app.register_blueprint(view)
|
||||
|
||||
return app
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
application = create_app()
|
||||
application.run()
|
16
ucast/asgi.py
Normal file
|
@ -0,0 +1,16 @@
|
|||
"""
|
||||
ASGI config for ucast project.
|
||||
|
||||
It exposes the ASGI callable as a module-level variable named ``application``.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/4.0/howto/deployment/asgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from django.core.asgi import get_asgi_application
|
||||
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "ucast.settings")
|
||||
|
||||
application = get_asgi_application()
|
22
ucast/manage.py
Executable file
|
@ -0,0 +1,22 @@
|
|||
#!/usr/bin/env python
|
||||
"""Django's command-line utility for administrative tasks."""
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
def main():
|
||||
"""Run administrative tasks."""
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "ucast.settings")
|
||||
try:
|
||||
from django.core.management import execute_from_command_line
|
||||
except ImportError as exc:
|
||||
raise ImportError(
|
||||
"Couldn't import Django. Are you sure it's installed and "
|
||||
"available on your PYTHONPATH environment variable? Did you "
|
||||
"forget to activate a virtual environment?"
|
||||
) from exc
|
||||
execute_from_command_line(sys.argv)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
121
ucast/settings.py
Normal file
|
@ -0,0 +1,121 @@
|
|||
"""
|
||||
Django settings for ucast project.
|
||||
|
||||
Generated by 'django-admin startproject' using Django 4.0.4.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/4.0/topics/settings/
|
||||
|
||||
For the full list of settings and their values, see
|
||||
https://docs.djangoproject.com/en/4.0/ref/settings/
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent / "_run"
|
||||
|
||||
# Quick-start development settings - unsuitable for production
|
||||
# See https://docs.djangoproject.com/en/4.0/howto/deployment/checklist/
|
||||
|
||||
# SECURITY WARNING: keep the secret key used in production secret!
|
||||
SECRET_KEY = "django-insecure-$b25pq-s+(_zx2!2$+i+^0$kft0&y3kwmj7j5a#d_jop)$d061"
|
||||
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG = True
|
||||
|
||||
ALLOWED_HOSTS = []
|
||||
|
||||
# Application definition
|
||||
|
||||
INSTALLED_APPS = [
|
||||
"yt2podcast.apps.Yt2PodcastConfig",
|
||||
"django.contrib.admin",
|
||||
"django.contrib.auth",
|
||||
"django.contrib.contenttypes",
|
||||
"django.contrib.sessions",
|
||||
"django.contrib.messages",
|
||||
"django.contrib.staticfiles",
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
"django.middleware.security.SecurityMiddleware",
|
||||
"django.contrib.sessions.middleware.SessionMiddleware",
|
||||
"django.middleware.common.CommonMiddleware",
|
||||
"django.middleware.csrf.CsrfViewMiddleware",
|
||||
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
||||
"django.contrib.messages.middleware.MessageMiddleware",
|
||||
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
||||
]
|
||||
|
||||
ROOT_URLCONF = "ucast.urls"
|
||||
|
||||
TEMPLATES = [
|
||||
{
|
||||
"BACKEND": "django.template.backends.django.DjangoTemplates",
|
||||
"DIRS": [],
|
||||
"APP_DIRS": True,
|
||||
"OPTIONS": {
|
||||
"context_processors": [
|
||||
"django.template.context_processors.debug",
|
||||
"django.template.context_processors.request",
|
||||
"django.contrib.auth.context_processors.auth",
|
||||
"django.contrib.messages.context_processors.messages",
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
WSGI_APPLICATION = "ucast.wsgi.application"
|
||||
|
||||
# Database
|
||||
# https://docs.djangoproject.com/en/4.0/ref/settings/#databases
|
||||
|
||||
DATABASES = {
|
||||
"default": {
|
||||
"ENGINE": "django.db.backends.sqlite3",
|
||||
"NAME": BASE_DIR / "db.sqlite3",
|
||||
}
|
||||
}
|
||||
|
||||
# Password validation
|
||||
# https://docs.djangoproject.com/en/4.0/ref/settings/#auth-password-validators
|
||||
|
||||
AUTH_PASSWORD_VALIDATORS = [
|
||||
{
|
||||
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
|
||||
},
|
||||
{
|
||||
"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
|
||||
},
|
||||
{
|
||||
"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
|
||||
},
|
||||
{
|
||||
"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
|
||||
},
|
||||
]
|
||||
|
||||
# Internationalization
|
||||
# https://docs.djangoproject.com/en/4.0/topics/i18n/
|
||||
|
||||
LANGUAGE_CODE = "en-us"
|
||||
|
||||
TIME_ZONE = "UTC"
|
||||
|
||||
USE_I18N = True
|
||||
|
||||
USE_TZ = True
|
||||
|
||||
# Static files (CSS, JavaScript, Images)
|
||||
# https://docs.djangoproject.com/en/4.0/howto/static-files/
|
||||
|
||||
STATIC_URL = "static/"
|
||||
STATIC_ROOT = BASE_DIR / "static"
|
||||
|
||||
DATA_ROOT = BASE_DIR / "data"
|
||||
|
||||
# Default primary key field type
|
||||
# https://docs.djangoproject.com/en/4.0/ref/settings/#default-auto-field
|
||||
|
||||
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
|
|
@ -1,44 +0,0 @@
|
|||
# coding=utf-8
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
UCAST_DIRNAME = '.ucast'
|
||||
|
||||
|
||||
class ChannelFolder:
|
||||
def __init__(self, dir_root: Path):
|
||||
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'
|
||||
|
||||
def does_exist(self) -> bool:
|
||||
return os.path.isdir(self.dir_covers)
|
||||
|
||||
def create(self):
|
||||
os.makedirs(self.dir_covers, exist_ok=True)
|
||||
|
||||
|
||||
class Storage:
|
||||
def __init__(self, config_dir: Path, data_dir: Path):
|
||||
self.dir_config = config_dir
|
||||
self.dir_data = data_dir
|
||||
|
||||
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')
|
||||
|
||||
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')
|
||||
|
||||
cf.create()
|
||||
return cf
|
|
@ -1,5 +0,0 @@
|
|||
# coding=utf-8
|
||||
from os import PathLike
|
||||
from typing import Tuple, Union
|
||||
|
||||
Color = Tuple[int, int, int]
|
21
ucast/urls.py
Normal file
|
@ -0,0 +1,21 @@
|
|||
"""ucast URL Configuration
|
||||
|
||||
The `urlpatterns` list routes URLs to views. For more information please see:
|
||||
https://docs.djangoproject.com/en/4.0/topics/http/urls/
|
||||
Examples:
|
||||
Function views
|
||||
1. Add an import: from my_app import views
|
||||
2. Add a URL to urlpatterns: path('', views.home, name='home')
|
||||
Class-based views
|
||||
1. Add an import: from other_app.views import Home
|
||||
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
|
||||
Including another URLconf
|
||||
1. Import the include() function: from django.urls import include, path
|
||||
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
||||
"""
|
||||
from django.contrib import admin
|
||||
from django.urls import path
|
||||
|
||||
urlpatterns = [
|
||||
path("admin/", admin.site.urls),
|
||||
]
|
|
@ -1,9 +0,0 @@
|
|||
# coding=utf-8
|
||||
import requests
|
||||
|
||||
from ucast import types
|
||||
|
||||
|
||||
def download_file(url: str, download_path: types.Path):
|
||||
r = requests.get(url, allow_redirects=True)
|
||||
open(download_path, 'wb').write(r.content)
|
|
@ -1,14 +0,0 @@
|
|||
# coding=utf-8
|
||||
from flask import Blueprint
|
||||
|
||||
view = Blueprint("view", __name__)
|
||||
|
||||
|
||||
@view.route('/')
|
||||
def hello():
|
||||
return 'Hello, World!'
|
||||
|
||||
|
||||
@view.route('/err')
|
||||
def errtest():
|
||||
raise Exception('This is an error')
|
|
@ -1,4 +1,16 @@
|
|||
# coding=utf-8
|
||||
from ucast import app
|
||||
"""
|
||||
WSGI config for ucast project.
|
||||
|
||||
application = app.create_app()
|
||||
It exposes the WSGI callable as a module-level variable named ``application``.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/4.0/howto/deployment/wsgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from django.core.wsgi import get_wsgi_application
|
||||
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "ucast.settings")
|
||||
|
||||
application = get_wsgi_application()
|
||||
|
|
0
yt2podcast/__init__.py
Normal file
3
yt2podcast/admin.py
Normal file
|
@ -0,0 +1,3 @@
|
|||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
6
yt2podcast/apps.py
Normal file
|
@ -0,0 +1,6 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class Yt2PodcastConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "yt2podcast"
|
41
yt2podcast/migrations/0001_initial.py
Normal file
|
@ -0,0 +1,41 @@
|
|||
# Generated by Django 4.0.4 on 2022-05-05 00:02
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = []
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="Channel",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.CharField(max_length=30, primary_key=True, serialize=False),
|
||||
),
|
||||
("name", models.CharField(max_length=100)),
|
||||
("active", models.BooleanField(default=True)),
|
||||
("skip_livestreams", models.BooleanField(default=True)),
|
||||
("skip_shorts", models.BooleanField(default=True)),
|
||||
("keep_videos", models.IntegerField(default=None, null=True)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="Video",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.CharField(max_length=30, primary_key=True, serialize=False),
|
||||
),
|
||||
("title", models.CharField(max_length=200)),
|
||||
("slug", models.CharField(max_length=209)),
|
||||
("published", models.DateTimeField()),
|
||||
("downloaded", models.DateTimeField()),
|
||||
("description", models.TextField()),
|
||||
],
|
||||
),
|
||||
]
|
0
yt2podcast/migrations/__init__.py
Normal file
19
yt2podcast/models.py
Normal file
|
@ -0,0 +1,19 @@
|
|||
from django.db import models
|
||||
|
||||
|
||||
class Channel(models.Model):
|
||||
id = models.CharField(max_length=30, primary_key=True)
|
||||
name = models.CharField(max_length=100)
|
||||
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)
|
||||
|
||||
|
||||
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)
|
||||
published = models.DateTimeField()
|
||||
downloaded = models.DateTimeField()
|
||||
description = models.TextField()
|
|
@ -1,3 +1 @@
|
|||
# coding=utf-8
|
||||
|
||||
|
|
@ -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 types
|
||||
from yt2podcast.service 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: types.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
|
||||
|
@ -86,13 +95,13 @@ def _get_dominant_color(img: Image.Image):
|
|||
return thief.get_color()
|
||||
|
||||
|
||||
def _interpolate_color(color_from: types.Color, color_to: types.Color, interval: int):
|
||||
def _interpolate_color(color_from: typ.Color, color_to: typ.Color, interval: int):
|
||||
det_co = [(t - f) / interval for f, t in zip(color_from, color_to)]
|
||||
for i in range(interval):
|
||||
yield [round(f + det * i) for f, det in zip(color_from, det_co)]
|
||||
|
||||
|
||||
def _get_text_color(bg_color) -> types.Color:
|
||||
def _get_text_color(bg_color) -> typ.Color:
|
||||
color_decimal = tuple([c / 255 for c in bg_color])
|
||||
c_blk = wcag_contrast_ratio.rgb((0, 0, 0), color_decimal)
|
||||
c_wht = wcag_contrast_ratio.rgb((1, 1, 1), color_decimal)
|
||||
|
@ -101,7 +110,9 @@ def _get_text_color(bg_color) -> types.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),
|
||||
_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)
|
||||
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
|
4
yt2podcast/service/typ.py
Normal file
|
@ -0,0 +1,4 @@
|
|||
# coding=utf-8
|
||||
from typing import Tuple
|
||||
|
||||
Color = Tuple[int, int, int]
|
7
yt2podcast/service/util.py
Normal file
|
@ -0,0 +1,7 @@
|
|||
# coding=utf-8
|
||||
import requests
|
||||
|
||||
|
||||
def download_file(url: str, download_path):
|
||||
r = requests.get(url, allow_redirects=True)
|
||||
open(download_path, "wb").write(r.content)
|
|
@ -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([
|
||||
ydl_params["postprocessors"].extend(
|
||||
[
|
||||
{
|
||||
'key': 'SponsorBlock',
|
||||
'categories': ['sponsor'],
|
||||
'when': 'after_filter'
|
||||
"key": "SponsorBlock",
|
||||
"categories": ["sponsor"],
|
||||
"when": "after_filter",
|
||||
},
|
||||
{
|
||||
'key': 'ModifyChapters',
|
||||
'remove_sponsor_segments': ['sponsor']
|
||||
}
|
||||
])
|
||||
{"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)
|
|
@ -1,4 +1,4 @@
|
|||
# coding=utf-8
|
||||
from importlib.resources import files
|
||||
|
||||
DIR_TESTFILES = files('tests.testfiles')
|
||||
DIR_TESTFILES = files("yt2podcast.tests.testfiles")
|
115
yt2podcast/tests/test_cover.py
Normal file
|
@ -0,0 +1,115 @@
|
|||
# coding=utf-8
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from typing import List
|
||||
|
||||
import pytest
|
||||
from fonts.ttf import SourceSansPro
|
||||
from PIL import Image, ImageChops, ImageFont
|
||||
|
||||
from yt2podcast import tests
|
||||
from yt2podcast.service 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!",
|
||||
],
|
||||
),
|
||||
],
|
||||
)
|
||||
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)),
|
||||
],
|
||||
)
|
||||
def test_get_dominant_color(file_name: str, color: typ.Color):
|
||||
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)),
|
||||
],
|
||||
)
|
||||
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"),
|
||||
],
|
||||
)
|
||||
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_image = Image.open(tn_file)
|
||||
av_image = Image.open(av_file)
|
||||
expected_cv_image = Image.open(expected_cv_file)
|
||||
|
||||
cv_image = cover._create_cover_image(tn_image, av_image, title, channel)
|
||||
|
||||
diff = ImageChops.difference(cv_image, expected_cv_image)
|
||||
assert diff.getbbox() is None
|
||||
|
||||
|
||||
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"
|
||||
|
||||
tmpdir_o = tempfile.TemporaryDirectory()
|
||||
tmpdir = Path(tmpdir_o.name)
|
||||
cv_file = tmpdir / "cover.png"
|
||||
|
||||
cover.create_cover_file(
|
||||
tn_file, av_file, "ThetaDev @ Embedded World 2019", "ThetaDev", cv_file
|
||||
)
|
||||
|
||||
cv_image = Image.open(cv_file)
|
||||
expected_cv_image = Image.open(expected_cv_file)
|
||||
|
||||
diff = ImageChops.difference(cv_image, expected_cv_image)
|
||||
assert diff.getbbox() is None
|
Before Width: | Height: | Size: 186 KiB After Width: | Height: | Size: 186 KiB |
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 32 KiB |
Before Width: | Height: | Size: 53 KiB After Width: | Height: | Size: 53 KiB |
Before Width: | Height: | Size: 234 KiB After Width: | Height: | Size: 234 KiB |
Before Width: | Height: | Size: 229 KiB After Width: | Height: | Size: 229 KiB |
Before Width: | Height: | Size: 173 KiB After Width: | Height: | Size: 173 KiB |
Before Width: | Height: | Size: 92 KiB After Width: | Height: | Size: 92 KiB |
Before Width: | Height: | Size: 101 KiB After Width: | Height: | Size: 101 KiB |
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 28 KiB |
3
yt2podcast/views.py
Normal file
|
@ -0,0 +1,3 @@
|
|||
from django.shortcuts import render
|
||||
|
||||
# Create your views here.
|