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
|
|_ 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
14
poetry.lock
generated
|
@ -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"},
|
||||||
|
|
|
@ -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"]
|
||||||
|
|
|
@ -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)
|
|
@ -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
|
||||||
|
|
|
@ -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
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
|
from typing import Tuple, Union
|
||||||
|
|
||||||
Color = Tuple[int, int, int]
|
Color = Tuple[int, int, int]
|
||||||
Path = Union[str, bytes, PathLike]
|
|
||||||
|
|
Loading…
Reference in a new issue