Compare commits
2 commits
c6c3849a82
...
e131574393
Author | SHA1 | Date | |
---|---|---|---|
e131574393 | |||
833751c9b6 |
9 changed files with 137 additions and 23 deletions
|
@ -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
|
||||
|
@ -43,6 +38,7 @@ _ data
|
|||
|
||||
### Video
|
||||
|
||||
- ID: str
|
||||
- Title: str
|
||||
- Slug: str (YYMMDD_Title, used as filename)
|
||||
- Published: datetime
|
||||
|
|
14
poetry.lock
generated
14
poetry.lock
generated
|
@ -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"},
|
||||
|
|
|
@ -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"]
|
||||
|
|
|
@ -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 <Video-ID>
|
||||
|
||||
@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)
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
3
ucast/model.py
Normal file
3
ucast/model.py
Normal file
|
@ -0,0 +1,3 @@
|
|||
# coding=utf-8
|
||||
|
||||
|
44
ucast/storage.py
Normal file
44
ucast/storage.py
Normal 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
|
|
@ -3,4 +3,3 @@ from os import PathLike
|
|||
from typing import Tuple, Union
|
||||
|
||||
Color = Tuple[int, int, int]
|
||||
Path = Union[str, bytes, PathLike]
|
||||
|
|
Loading…
Reference in a new issue