From 833751c9b61b029d8531f8d982254db040a658d2 Mon Sep 17 00:00:00 2001 From: Theta-Dev Date: Sat, 16 Apr 2022 16:40:10 +0200 Subject: [PATCH 1/2] add tests for cover generation --- notes/Speicher.md | 1 + poetry.lock | 14 +++++- pyproject.toml | 1 + tests/testfiles/get_cover.py => tasks.py | 20 ++++---- tests/test_cover.py | 64 +++++++++++++++++++++++- 5 files changed, 86 insertions(+), 14 deletions(-) rename tests/testfiles/get_cover.py => tasks.py (70%) diff --git a/notes/Speicher.md b/notes/Speicher.md index d0ed3b0..503587c 100644 --- a/notes/Speicher.md +++ b/notes/Speicher.md @@ -43,6 +43,7 @@ _ data ### Video +- ID: str - Title: str - Slug: str (YYMMDD_Title, used as filename) - Published: datetime diff --git a/poetry.lock b/poetry.lock index ae17884..7909b86 100644 --- a/poetry.lock +++ b/poetry.lock @@ -174,6 +174,14 @@ category = "dev" optional = false python-versions = "*" +[[package]] +name = "invoke" +version = "1.7.0" +description = "Pythonic task execution" +category = "dev" +optional = false +python-versions = "*" + [[package]] name = "itsdangerous" version = "2.1.2" @@ -439,7 +447,7 @@ websockets = "*" [metadata] lock-version = "1.1" python-versions = "^3.10" -content-hash = "df5be5b98bd03da41732b908331f0a731408f65a96b078c0139773f0759afac3" +content-hash = "cf8899258dac046f0ed3d0492161db330ab735dc8dcbe1c46d2c8d4e48b66342" [metadata.files] atomicwrites = [ @@ -687,6 +695,10 @@ iniconfig = [ {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, {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 = [ {file = "itsdangerous-2.1.2-py3-none-any.whl", hash = "sha256:2c2349112351b88699d8d4b6b075022c0808887cb7ad10069318a8b0bc88db44"}, {file = "itsdangerous-2.1.2.tar.gz", hash = "sha256:5dbbc68b317e5e42f327f9021763545dc3fc3bfe22e6deb96aaf1fc38874156a"}, diff --git a/pyproject.toml b/pyproject.toml index 353ea37..ba48a57 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,6 +20,7 @@ fonts = "^0.0.3" [tool.poetry.dev-dependencies] pytest = "^7.1.1" pytest-cov = "^3.0.0" +invoke = "^1.7.0" [build-system] requires = ["poetry-core>=1.0.0"] diff --git a/tests/testfiles/get_cover.py b/tasks.py similarity index 70% rename from tests/testfiles/get_cover.py rename to tasks.py index 3bbec0d..cafe839 100644 --- a/tests/testfiles/get_cover.py +++ b/tasks.py @@ -1,21 +1,19 @@ -# coding=utf-8 -import sys import os +from invoke import task + from ucast import youtube, util, cover import tests -# Mit diesem Skript kann man Coverbilder zum Testen erzeugen -# python tests/testfiles/get_cover.py + +@task +def test(c): + c.run('pytest tests', pty=True) -if __name__ == '__main__': - if len(sys.argv) <= 1: - print('No video id given') - sys.exit(1) - - video_id = sys.argv[1] - vinfo = youtube.get_video_info(video_id) +@task +def get_cover(c, vid=''): + vinfo = youtube.get_video_info(vid) title = vinfo['fulltitle'] channel_name = vinfo['uploader'] thumbnail_url = youtube.get_thumbnail_url(vinfo) diff --git a/tests/test_cover.py b/tests/test_cover.py index d2e1407..745c2e6 100644 --- a/tests/test_cover.py +++ b/tests/test_cover.py @@ -1,12 +1,14 @@ # coding=utf-8 from typing import List +import tempfile +from pathlib import Path import pytest -from PIL import ImageFont +from PIL import Image, ImageFont, ImageChops from fonts.ttf import SourceSansPro import tests -from ucast import cover +from ucast import cover, types @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) 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 From e1315743931f43f9649f4f46b54d54cd95e8f783 Mon Sep 17 00:00:00 2001 From: Theta-Dev Date: Thu, 28 Apr 2022 11:47:38 +0200 Subject: [PATCH 2/2] add storage.py --- notes/Speicher.md | 7 +------ ucast/cover.py | 5 +++-- ucast/model.py | 3 +++ ucast/storage.py | 44 ++++++++++++++++++++++++++++++++++++++++++++ ucast/types.py | 1 - 5 files changed, 51 insertions(+), 9 deletions(-) create mode 100644 ucast/model.py create mode 100644 ucast/storage.py diff --git a/notes/Speicher.md b/notes/Speicher.md index 503587c..f2222cd 100644 --- a/notes/Speicher.md +++ b/notes/Speicher.md @@ -9,7 +9,7 @@ _ data |_ LinusTechTips |_ .ucast |_ 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 |_ feed.xml # RSS-Feed |_ covers # Cover-Bilder @@ -24,15 +24,10 @@ _ data ## Datenmodelle -### LastScan - -- LastScan: datetime - ### ChannelOptions - ID: str - Active: bool = True -- LastScan: datetime - SkipLivestreams: bool = True - SkipShorts: bool = True - KeepVideos: int = -1 diff --git a/ucast/cover.py b/ucast/cover.py index e4a4aa7..7dd523b 100644 --- a/ucast/cover.py +++ b/ucast/cover.py @@ -1,5 +1,6 @@ # coding=utf-8 import math +from pathlib import Path from typing import Tuple, List, Optional from PIL import Image, ImageDraw, ImageFont @@ -159,8 +160,8 @@ def _create_cover_image(thumbnail: Image.Image, avatar: Optional[Image.Image], t return cover -def create_cover_file(thumbnail_path: types.Path, avatar_path: Optional[types.Path], title: str, channel: str, - cover_path: types.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 diff --git a/ucast/model.py b/ucast/model.py new file mode 100644 index 0000000..87062c3 --- /dev/null +++ b/ucast/model.py @@ -0,0 +1,3 @@ +# coding=utf-8 + + diff --git a/ucast/storage.py b/ucast/storage.py new file mode 100644 index 0000000..266aca2 --- /dev/null +++ b/ucast/storage.py @@ -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 diff --git a/ucast/types.py b/ucast/types.py index 2b54726..99876b6 100644 --- a/ucast/types.py +++ b/ucast/types.py @@ -3,4 +3,3 @@ from os import PathLike from typing import Tuple, Union Color = Tuple[int, int, int] -Path = Union[str, bytes, PathLike]