Compare commits

...

2 commits

Author SHA1 Message Date
e131574393 add storage.py 2022-04-28 11:47:38 +02:00
833751c9b6 add tests for cover generation 2022-04-16 16:40:10 +02:00
9 changed files with 137 additions and 23 deletions

View file

@ -9,7 +9,7 @@ _ data
|_ LinusTechTips |_ LinusTechTips
|_ .ucast |_ .ucast
|_ videos.json # IDs und Metadaten aller heruntergeladenen Videos |_ videos.json # IDs und Metadaten aller heruntergeladenen Videos
|_ options.json # Kanalspezifische Optionen (ID, LastScan) |_ options.json # Kanalspezifische Optionen (ID, enabled)
|_ avatar.png # Profilbild des Kanals |_ avatar.png # Profilbild des Kanals
|_ feed.xml # RSS-Feed |_ feed.xml # RSS-Feed
|_ covers # Cover-Bilder |_ covers # Cover-Bilder
@ -24,15 +24,10 @@ _ data
## Datenmodelle ## Datenmodelle
### LastScan
- LastScan: datetime
### ChannelOptions ### ChannelOptions
- ID: str - ID: str
- Active: bool = True - Active: bool = True
- LastScan: datetime
- SkipLivestreams: bool = True - SkipLivestreams: bool = True
- SkipShorts: bool = True - SkipShorts: bool = True
- KeepVideos: int = -1 - KeepVideos: int = -1
@ -43,6 +38,7 @@ _ data
### Video ### Video
- ID: str
- Title: str - Title: str
- Slug: str (YYMMDD_Title, used as filename) - Slug: str (YYMMDD_Title, used as filename)
- Published: datetime - Published: datetime

14
poetry.lock generated
View file

@ -174,6 +174,14 @@ category = "dev"
optional = false optional = false
python-versions = "*" python-versions = "*"
[[package]]
name = "invoke"
version = "1.7.0"
description = "Pythonic task execution"
category = "dev"
optional = false
python-versions = "*"
[[package]] [[package]]
name = "itsdangerous" name = "itsdangerous"
version = "2.1.2" version = "2.1.2"
@ -439,7 +447,7 @@ websockets = "*"
[metadata] [metadata]
lock-version = "1.1" lock-version = "1.1"
python-versions = "^3.10" python-versions = "^3.10"
content-hash = "df5be5b98bd03da41732b908331f0a731408f65a96b078c0139773f0759afac3" content-hash = "cf8899258dac046f0ed3d0492161db330ab735dc8dcbe1c46d2c8d4e48b66342"
[metadata.files] [metadata.files]
atomicwrites = [ atomicwrites = [
@ -687,6 +695,10 @@ iniconfig = [
{file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"},
{file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"},
] ]
invoke = [
{file = "invoke-1.7.0-py3-none-any.whl", hash = "sha256:a5159fc63dba6ca2a87a1e33d282b99cea69711b03c64a35bb4e1c53c6c4afa0"},
{file = "invoke-1.7.0.tar.gz", hash = "sha256:e332e49de40463f2016315f51df42313855772be86435686156bc18f45b5cc6c"},
]
itsdangerous = [ itsdangerous = [
{file = "itsdangerous-2.1.2-py3-none-any.whl", hash = "sha256:2c2349112351b88699d8d4b6b075022c0808887cb7ad10069318a8b0bc88db44"}, {file = "itsdangerous-2.1.2-py3-none-any.whl", hash = "sha256:2c2349112351b88699d8d4b6b075022c0808887cb7ad10069318a8b0bc88db44"},
{file = "itsdangerous-2.1.2.tar.gz", hash = "sha256:5dbbc68b317e5e42f327f9021763545dc3fc3bfe22e6deb96aaf1fc38874156a"}, {file = "itsdangerous-2.1.2.tar.gz", hash = "sha256:5dbbc68b317e5e42f327f9021763545dc3fc3bfe22e6deb96aaf1fc38874156a"},

View file

@ -20,6 +20,7 @@ fonts = "^0.0.3"
[tool.poetry.dev-dependencies] [tool.poetry.dev-dependencies]
pytest = "^7.1.1" pytest = "^7.1.1"
pytest-cov = "^3.0.0" pytest-cov = "^3.0.0"
invoke = "^1.7.0"
[build-system] [build-system]
requires = ["poetry-core>=1.0.0"] requires = ["poetry-core>=1.0.0"]

View file

@ -1,21 +1,19 @@
# coding=utf-8
import sys
import os import os
from invoke import task
from ucast import youtube, util, cover from ucast import youtube, util, cover
import tests import tests
# Mit diesem Skript kann man Coverbilder zum Testen erzeugen
# python tests/testfiles/get_cover.py <Video-ID> @task
def test(c):
c.run('pytest tests', pty=True)
if __name__ == '__main__': @task
if len(sys.argv) <= 1: def get_cover(c, vid=''):
print('No video id given') vinfo = youtube.get_video_info(vid)
sys.exit(1)
video_id = sys.argv[1]
vinfo = youtube.get_video_info(video_id)
title = vinfo['fulltitle'] title = vinfo['fulltitle']
channel_name = vinfo['uploader'] channel_name = vinfo['uploader']
thumbnail_url = youtube.get_thumbnail_url(vinfo) thumbnail_url = youtube.get_thumbnail_url(vinfo)

View file

@ -1,12 +1,14 @@
# coding=utf-8 # coding=utf-8
from typing import List from typing import List
import tempfile
from pathlib import Path
import pytest import pytest
from PIL import ImageFont from PIL import Image, ImageFont, ImageChops
from fonts.ttf import SourceSansPro from fonts.ttf import SourceSansPro
import tests import tests
from ucast import cover from ucast import cover, types
@pytest.mark.parametrize('height,width,text,expect', [ @pytest.mark.parametrize('height,width,text,expect', [
@ -22,3 +24,61 @@ def test_split_text(height: int, width: int, text: str, expect: List[str]):
font = ImageFont.truetype(SourceSansPro, 40) font = ImageFont.truetype(SourceSansPro, 40)
lines = cover._split_text(height, width, text, font, 8) lines = cover._split_text(height, width, text, font, 8)
assert lines == expect assert lines == expect
@pytest.mark.parametrize('file_name,color', [
('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

View file

@ -1,5 +1,6 @@
# coding=utf-8 # coding=utf-8
import math import math
from pathlib import Path
from typing import Tuple, List, Optional from typing import Tuple, List, Optional
from PIL import Image, ImageDraw, ImageFont from PIL import Image, ImageDraw, ImageFont
@ -159,8 +160,8 @@ def _create_cover_image(thumbnail: Image.Image, avatar: Optional[Image.Image], t
return cover return cover
def create_cover_file(thumbnail_path: types.Path, avatar_path: Optional[types.Path], title: str, channel: str, def create_cover_file(thumbnail_path: Path, avatar_path: Optional[Path], title: str, channel: str,
cover_path: types.Path): cover_path: Path):
thumbnail = Image.open(thumbnail_path) thumbnail = Image.open(thumbnail_path)
avatar = None avatar = None

3
ucast/model.py Normal file
View file

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

44
ucast/storage.py Normal file
View file

@ -0,0 +1,44 @@
# 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

View file

@ -3,4 +3,3 @@ from os import PathLike
from typing import Tuple, Union from typing import Tuple, Union
Color = Tuple[int, int, int] Color = Tuple[int, int, int]
Path = Union[str, bytes, PathLike]