Compare commits
2 commits
fb6d830897
...
8127242487
Author | SHA1 | Date | |
---|---|---|---|
8127242487 | |||
9d41e6d5c3 |
16 changed files with 142 additions and 59 deletions
|
@ -1,5 +1,5 @@
|
||||||
[bumpversion]
|
[bumpversion]
|
||||||
current_version = 0.4.1
|
current_version = 0.4.2
|
||||||
commit = True
|
commit = True
|
||||||
tag = True
|
tag = True
|
||||||
|
|
||||||
|
|
|
@ -44,8 +44,8 @@ steps:
|
||||||
# commands:
|
# commands:
|
||||||
# - buildah login -u $DOCKER_USER -p $DOCKER_PASS -- $DOCKER_REGISTRY
|
# - buildah login -u $DOCKER_USER -p $DOCKER_PASS -- $DOCKER_REGISTRY
|
||||||
# - buildah manifest create ucast
|
# - buildah manifest create ucast
|
||||||
# - buildah bud --tag code.thetadev.de/hsa/ucast:latest --manifest ucast --arch amd64 -f deploy/Dockerfile .
|
# - buildah bud --tag code.thetadev.de/hsa/ucast:latest --manifest ucast --arch amd64 --build-arg TARGETPLATFORM=linux/amd64 -f deploy/Dockerfile .
|
||||||
# - buildah bud --tag code.thetadev.de/hsa/ucast:latest --manifest ucast --arch arm64 -f deploy/Dockerfile .
|
# - buildah bud --tag code.thetadev.de/hsa/ucast:latest --manifest ucast --arch arm64 --build-arg TARGETPLATFORM=linux/arm64 -f deploy/Dockerfile .
|
||||||
# - buildah manifest push --all ucast docker://code.thetadev.de/hsa/ucast:latest
|
# - buildah manifest push --all ucast docker://code.thetadev.de/hsa/ucast:latest
|
||||||
# environment:
|
# environment:
|
||||||
# DOCKER_REGISTRY:
|
# DOCKER_REGISTRY:
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "ucast"
|
name = "ucast"
|
||||||
version = "0.4.1"
|
version = "0.4.2"
|
||||||
description = "YouTube to Podcast converter"
|
description = "YouTube to Podcast converter"
|
||||||
authors = ["Theta-Dev <t.testboy@gmail.com>"]
|
authors = ["Theta-Dev <t.testboy@gmail.com>"]
|
||||||
packages = [
|
packages = [
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
__version__ = "0.4.1"
|
__version__ = "0.4.2"
|
||||||
|
|
||||||
|
|
||||||
def template_context(request):
|
def template_context(request):
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import shutil
|
import shutil
|
||||||
|
|
||||||
from ucast.models import Channel, Video
|
from ucast.models import Channel, Video
|
||||||
from ucast.service import storage, util, youtube
|
from ucast.service import storage, util, videoutil, youtube
|
||||||
|
|
||||||
|
|
||||||
class ChannelAlreadyExistsException(Exception):
|
class ChannelAlreadyExistsException(Exception):
|
||||||
|
@ -12,8 +12,10 @@ class ChannelAlreadyExistsException(Exception):
|
||||||
def download_channel_avatar(channel: Channel):
|
def download_channel_avatar(channel: Channel):
|
||||||
store = storage.Storage()
|
store = storage.Storage()
|
||||||
channel_folder = store.get_or_create_channel_folder(channel.slug)
|
channel_folder = store.get_or_create_channel_folder(channel.slug)
|
||||||
util.download_image_file(channel.avatar_url, channel_folder.file_avatar)
|
util.download_image_file(
|
||||||
util.resize_avatar(channel_folder.file_avatar, channel_folder.file_avatar_sm)
|
channel.avatar_url, channel_folder.file_avatar, videoutil.AVATAR_SIZE
|
||||||
|
)
|
||||||
|
videoutil.resize_avatar(channel_folder.file_avatar, channel_folder.file_avatar_sm)
|
||||||
|
|
||||||
|
|
||||||
def create_channel(channel_str: str) -> Channel:
|
def create_channel(channel_str: str) -> Channel:
|
||||||
|
|
|
@ -4,7 +4,7 @@ import json
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Union
|
from typing import Any, Optional, Tuple, Union
|
||||||
from urllib import parse
|
from urllib import parse
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
@ -12,9 +12,6 @@ import slugify
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
|
||||||
AVATAR_SM_WIDTH = 100
|
|
||||||
THUMBNAIL_SM_WIDTH = 360
|
|
||||||
|
|
||||||
EMOJI_PATTERN = re.compile(
|
EMOJI_PATTERN = re.compile(
|
||||||
"["
|
"["
|
||||||
"\U0001F1E0-\U0001F1FF" # flags (iOS)
|
"\U0001F1E0-\U0001F1FF" # flags (iOS)
|
||||||
|
@ -39,13 +36,38 @@ def download_file(url: str, download_path: Path):
|
||||||
open(download_path, "wb").write(r.content)
|
open(download_path, "wb").write(r.content)
|
||||||
|
|
||||||
|
|
||||||
def download_image_file(url: str, download_path: Path):
|
def resize_image(img: Image, resize: Tuple[int, int]):
|
||||||
|
if img.size == resize:
|
||||||
|
return img
|
||||||
|
|
||||||
|
w_ratio = resize[0] / img.width
|
||||||
|
h_ratio = resize[1] / img.height
|
||||||
|
box = None
|
||||||
|
|
||||||
|
# Too tall
|
||||||
|
if h_ratio < w_ratio:
|
||||||
|
crop_height = int(img.width / resize[0] * resize[1])
|
||||||
|
border = int((img.height - crop_height) / 2)
|
||||||
|
box = (0, border, img.width, img.height - border)
|
||||||
|
# Too wide
|
||||||
|
elif w_ratio < h_ratio:
|
||||||
|
crop_width = int(img.height / resize[1] * resize[0])
|
||||||
|
border = int((img.width - crop_width) / 2)
|
||||||
|
box = (border, 0, img.width - border, img.height)
|
||||||
|
|
||||||
|
return img.resize(resize, Image.Resampling.LANCZOS, box)
|
||||||
|
|
||||||
|
|
||||||
|
def download_image_file(
|
||||||
|
url: str, download_path: Path, resize: Optional[Tuple[int, int]] = None
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
Download an image and convert it to the type given
|
Download an image and convert it to the type given
|
||||||
by the path.
|
by the path.
|
||||||
|
|
||||||
:param url: Image URL
|
:param url: Image URL
|
||||||
:param download_path: Download path
|
:param download_path: Download path
|
||||||
|
:param resize: target image size (set to None for no resizing)
|
||||||
"""
|
"""
|
||||||
r = requests.get(url, allow_redirects=True)
|
r = requests.get(url, allow_redirects=True)
|
||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
|
@ -55,30 +77,15 @@ def download_image_file(url: str, download_path: Path):
|
||||||
if img_ext == "jpeg":
|
if img_ext == "jpeg":
|
||||||
img_ext = "jpg"
|
img_ext = "jpg"
|
||||||
|
|
||||||
|
if resize:
|
||||||
|
img = resize_image(img, resize)
|
||||||
|
|
||||||
if "." + img_ext == download_path.suffix:
|
if "." + img_ext == download_path.suffix:
|
||||||
open(download_path, "wb").write(r.content)
|
open(download_path, "wb").write(r.content)
|
||||||
else:
|
else:
|
||||||
img.save(download_path)
|
img.save(download_path)
|
||||||
|
|
||||||
|
|
||||||
def resize_avatar(original_file: Path, new_file: Path):
|
|
||||||
avatar = Image.open(original_file)
|
|
||||||
avatar_new_height = int(AVATAR_SM_WIDTH / avatar.width * avatar.height)
|
|
||||||
avatar = avatar.resize(
|
|
||||||
(AVATAR_SM_WIDTH, avatar_new_height), Image.Resampling.LANCZOS
|
|
||||||
)
|
|
||||||
avatar.save(new_file)
|
|
||||||
|
|
||||||
|
|
||||||
def resize_thumbnail(original_file: Path, new_file: Path):
|
|
||||||
thumbnail = Image.open(original_file)
|
|
||||||
tn_new_height = int(THUMBNAIL_SM_WIDTH / thumbnail.width * thumbnail.height)
|
|
||||||
thumbnail = thumbnail.resize(
|
|
||||||
(THUMBNAIL_SM_WIDTH, tn_new_height), Image.Resampling.LANCZOS
|
|
||||||
)
|
|
||||||
thumbnail.save(new_file)
|
|
||||||
|
|
||||||
|
|
||||||
def get_slug(text: str) -> str:
|
def get_slug(text: str) -> str:
|
||||||
return slugify.slugify(text, lowercase=False, separator="_")
|
return slugify.slugify(text, lowercase=False, separator="_")
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,12 @@ from datetime import date
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from mutagen import id3
|
from mutagen import id3
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
AVATAR_SM_WIDTH = 100
|
||||||
|
THUMBNAIL_SM_WIDTH = 360
|
||||||
|
THUMBNAIL_SIZE = (1280, 720)
|
||||||
|
AVATAR_SIZE = (900, 900)
|
||||||
|
|
||||||
|
|
||||||
def tag_audio(
|
def tag_audio(
|
||||||
|
@ -26,3 +32,21 @@ def tag_audio(
|
||||||
encoding=3, mime="image/png", type=3, desc="Cover", data=albumart.read()
|
encoding=3, mime="image/png", type=3, desc="Cover", data=albumart.read()
|
||||||
)
|
)
|
||||||
tag.save()
|
tag.save()
|
||||||
|
|
||||||
|
|
||||||
|
def resize_avatar(original_file: Path, new_file: Path):
|
||||||
|
avatar = Image.open(original_file)
|
||||||
|
avatar_new_height = int(AVATAR_SM_WIDTH / avatar.width * avatar.height)
|
||||||
|
avatar = avatar.resize(
|
||||||
|
(AVATAR_SM_WIDTH, avatar_new_height), Image.Resampling.LANCZOS
|
||||||
|
)
|
||||||
|
avatar.save(new_file)
|
||||||
|
|
||||||
|
|
||||||
|
def resize_thumbnail(original_file: Path, new_file: Path):
|
||||||
|
thumbnail = Image.open(original_file)
|
||||||
|
tn_new_height = int(THUMBNAIL_SM_WIDTH / thumbnail.width * thumbnail.height)
|
||||||
|
thumbnail = thumbnail.resize(
|
||||||
|
(THUMBNAIL_SM_WIDTH, tn_new_height), Image.Resampling.LANCZOS
|
||||||
|
)
|
||||||
|
thumbnail.save(new_file)
|
||||||
|
|
|
@ -115,6 +115,7 @@ def download_thumbnail(vinfo: VideoDetails, download_path: Path):
|
||||||
logging.info(f"downloading thumbnail {url}...")
|
logging.info(f"downloading thumbnail {url}...")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
# util.download_image_file(url, download_path, videoutil.THUMBNAIL_SIZE)
|
||||||
util.download_image_file(url, download_path)
|
util.download_image_file(url, download_path)
|
||||||
return
|
return
|
||||||
except requests.HTTPError:
|
except requests.HTTPError:
|
||||||
|
|
|
@ -7,7 +7,7 @@ from yt_dlp.utils import DownloadError
|
||||||
|
|
||||||
from ucast import queue
|
from ucast import queue
|
||||||
from ucast.models import Channel, Video
|
from ucast.models import Channel, Video
|
||||||
from ucast.service import controller, cover, storage, util, videoutil, youtube
|
from ucast.service import controller, cover, storage, videoutil, youtube
|
||||||
|
|
||||||
|
|
||||||
def _load_scraped_video(vid: youtube.VideoScraped, channel: Channel):
|
def _load_scraped_video(vid: youtube.VideoScraped, channel: Channel):
|
||||||
|
@ -105,7 +105,7 @@ def download_video(v_id: int):
|
||||||
# Download/convert thumbnails
|
# Download/convert thumbnails
|
||||||
tn_path = channel_folder.get_thumbnail(video.slug)
|
tn_path = channel_folder.get_thumbnail(video.slug)
|
||||||
youtube.download_thumbnail(details, tn_path)
|
youtube.download_thumbnail(details, tn_path)
|
||||||
util.resize_thumbnail(tn_path, channel_folder.get_thumbnail(video.slug, True))
|
videoutil.resize_thumbnail(tn_path, channel_folder.get_thumbnail(video.slug, True))
|
||||||
cover_file = channel_folder.get_cover(video.slug)
|
cover_file = channel_folder.get_cover(video.slug)
|
||||||
|
|
||||||
if not os.path.isfile(channel_folder.file_avatar):
|
if not os.path.isfile(channel_folder.file_avatar):
|
||||||
|
|
|
@ -2,6 +2,7 @@ import os
|
||||||
|
|
||||||
from django.db.models import ObjectDoesNotExist
|
from django.db.models import ObjectDoesNotExist
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
from ucast import queue
|
from ucast import queue
|
||||||
from ucast.models import Channel, Video
|
from ucast.models import Channel, Video
|
||||||
|
@ -51,6 +52,32 @@ def recreate_covers():
|
||||||
queue.enqueue(recreate_cover, video.id)
|
queue.enqueue(recreate_cover, video.id)
|
||||||
|
|
||||||
|
|
||||||
|
def resize_thumbnail(v_id: int):
|
||||||
|
try:
|
||||||
|
video = Video.objects.get(id=v_id)
|
||||||
|
except ObjectDoesNotExist:
|
||||||
|
return
|
||||||
|
|
||||||
|
store = storage.Storage()
|
||||||
|
cf = store.get_channel_folder(video.channel.slug)
|
||||||
|
|
||||||
|
tn_path = cf.get_thumbnail(video.slug)
|
||||||
|
tn_img = Image.open(tn_path)
|
||||||
|
if tn_img.size != videoutil.THUMBNAIL_SIZE:
|
||||||
|
tn_img = util.resize_image(tn_img, videoutil.THUMBNAIL_SIZE)
|
||||||
|
tn_img.save(tn_path)
|
||||||
|
videoutil.resize_thumbnail(tn_path, cf.get_thumbnail(video.slug, True))
|
||||||
|
|
||||||
|
|
||||||
|
def resize_thumbnails():
|
||||||
|
"""
|
||||||
|
Used to unify thumbnail sizes for the existing collection before v0.4.2.
|
||||||
|
Needs to be triggered manually: ``manage.py rqenqueue ucast.tasks.library.resize_thumbnails``.
|
||||||
|
"""
|
||||||
|
for video in Video.objects.filter(downloaded__isnull=False):
|
||||||
|
queue.enqueue(resize_thumbnail, video.id)
|
||||||
|
|
||||||
|
|
||||||
def update_file_storage():
|
def update_file_storage():
|
||||||
store = storage.Storage()
|
store = storage.Storage()
|
||||||
|
|
||||||
|
@ -75,7 +102,7 @@ def update_file_storage():
|
||||||
return
|
return
|
||||||
|
|
||||||
if not os.path.isfile(tn_file_sm):
|
if not os.path.isfile(tn_file_sm):
|
||||||
util.resize_thumbnail(tn_file, tn_file_sm)
|
videoutil.resize_thumbnail(tn_file, tn_file_sm)
|
||||||
|
|
||||||
if not os.path.isfile(cover_file):
|
if not os.path.isfile(cover_file):
|
||||||
recreate_cover(video)
|
recreate_cover(video)
|
||||||
|
@ -101,8 +128,12 @@ def update_channel_info(ch_id: int):
|
||||||
store = storage.Storage()
|
store = storage.Storage()
|
||||||
channel_folder = store.get_or_create_channel_folder(channel.slug)
|
channel_folder = store.get_or_create_channel_folder(channel.slug)
|
||||||
|
|
||||||
util.download_image_file(channel_data.avatar_url, channel_folder.file_avatar)
|
util.download_image_file(
|
||||||
util.resize_avatar(channel_folder.file_avatar, channel_folder.file_avatar_sm)
|
channel_data.avatar_url, channel_folder.file_avatar, videoutil.AVATAR_SIZE
|
||||||
|
)
|
||||||
|
videoutil.resize_avatar(
|
||||||
|
channel_folder.file_avatar, channel_folder.file_avatar_sm
|
||||||
|
)
|
||||||
|
|
||||||
channel.avatar_url = channel_data.avatar_url
|
channel.avatar_url = channel_data.avatar_url
|
||||||
|
|
||||||
|
|
BIN
ucast/tests/_testfiles/img/normal.png
Normal file
BIN
ucast/tests/_testfiles/img/normal.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 196 KiB |
BIN
ucast/tests/_testfiles/img/tall.png
Normal file
BIN
ucast/tests/_testfiles/img/tall.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 197 KiB |
BIN
ucast/tests/_testfiles/img/wide.png
Normal file
BIN
ucast/tests/_testfiles/img/wide.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 199 KiB |
|
@ -48,7 +48,7 @@ def _create_download_dir() -> Tuple[Path, TemporaryDirectory]:
|
||||||
shutil.copyfile(
|
shutil.copyfile(
|
||||||
tests.DIR_TESTFILES / "avatar" / f"{avatar}.jpg", cf.file_avatar
|
tests.DIR_TESTFILES / "avatar" / f"{avatar}.jpg", cf.file_avatar
|
||||||
)
|
)
|
||||||
util.resize_avatar(cf.file_avatar, cf.file_avatar_sm)
|
videoutil.resize_avatar(cf.file_avatar, cf.file_avatar_sm)
|
||||||
|
|
||||||
return tmpdir, tmpdir_o
|
return tmpdir, tmpdir_o
|
||||||
|
|
||||||
|
@ -75,7 +75,7 @@ def _add_download_dir_content():
|
||||||
|
|
||||||
shutil.copyfile(tests.DIR_TESTFILES / "audio" / "audio1.mp3", file_audio)
|
shutil.copyfile(tests.DIR_TESTFILES / "audio" / "audio1.mp3", file_audio)
|
||||||
shutil.copyfile(tests.DIR_TESTFILES / "thumbnail" / f"{vid}.webp", file_tn)
|
shutil.copyfile(tests.DIR_TESTFILES / "thumbnail" / f"{vid}.webp", file_tn)
|
||||||
util.resize_thumbnail(file_tn, cf.get_thumbnail(video_slug, True))
|
videoutil.resize_thumbnail(file_tn, cf.get_thumbnail(video_slug, True))
|
||||||
cover.create_cover_file(
|
cover.create_cover_file(
|
||||||
file_tn,
|
file_tn,
|
||||||
cf.file_avatar,
|
cf.file_avatar,
|
||||||
|
|
|
@ -55,28 +55,22 @@ def test_download_image_file_conv():
|
||||||
assert diff.getbbox() is None
|
assert diff.getbbox() is None
|
||||||
|
|
||||||
|
|
||||||
def test_resize_avatar():
|
@pytest.mark.parametrize(
|
||||||
tmpdir_o = tempfile.TemporaryDirectory()
|
"src_file",
|
||||||
tmpdir = Path(tmpdir_o.name)
|
[
|
||||||
source_file = tests.DIR_TESTFILES / "avatar" / "a1.jpg"
|
"normal",
|
||||||
resized_file = tmpdir / "avatar.webp"
|
"tall",
|
||||||
|
"wide",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_resize_image(src_file: str):
|
||||||
|
src_path = tests.DIR_TESTFILES / "img" / f"{src_file}.png"
|
||||||
|
src_img = Image.open(src_path)
|
||||||
|
resized = util.resize_image(src_img, (500, 250))
|
||||||
|
|
||||||
util.resize_avatar(source_file, resized_file)
|
normal_img = Image.open(tests.DIR_TESTFILES / "img" / "normal.png")
|
||||||
|
diff = ImageChops.difference(resized, normal_img)
|
||||||
resized_avatar = Image.open(resized_file)
|
assert diff.getbbox() is None
|
||||||
assert resized_avatar.size == (100, 100)
|
|
||||||
|
|
||||||
|
|
||||||
def test_resize_thumbnail():
|
|
||||||
tmpdir_o = tempfile.TemporaryDirectory()
|
|
||||||
tmpdir = Path(tmpdir_o.name)
|
|
||||||
source_file = tests.DIR_TESTFILES / "thumbnail" / "t1.webp"
|
|
||||||
resized_file = tmpdir / "thumbnail.webp"
|
|
||||||
|
|
||||||
util.resize_thumbnail(source_file, resized_file)
|
|
||||||
|
|
||||||
resized_thumbnail = Image.open(resized_file)
|
|
||||||
assert resized_thumbnail.size == (360, 202)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
|
|
|
@ -57,3 +57,27 @@ https://youtu.be/ZPxEr4YdWt8"""
|
||||||
expected_cover_img = Image.open(cover_file)
|
expected_cover_img = Image.open(cover_file)
|
||||||
diff = ImageChops.difference(tag_cover_img, expected_cover_img)
|
diff = ImageChops.difference(tag_cover_img, expected_cover_img)
|
||||||
assert diff.getbbox() is None
|
assert diff.getbbox() is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_resize_avatar():
|
||||||
|
tmpdir_o = tempfile.TemporaryDirectory()
|
||||||
|
tmpdir = Path(tmpdir_o.name)
|
||||||
|
source_file = tests.DIR_TESTFILES / "avatar" / "a1.jpg"
|
||||||
|
resized_file = tmpdir / "avatar.webp"
|
||||||
|
|
||||||
|
videoutil.resize_avatar(source_file, resized_file)
|
||||||
|
|
||||||
|
resized_avatar = Image.open(resized_file)
|
||||||
|
assert resized_avatar.size == (100, 100)
|
||||||
|
|
||||||
|
|
||||||
|
def test_resize_thumbnail():
|
||||||
|
tmpdir_o = tempfile.TemporaryDirectory()
|
||||||
|
tmpdir = Path(tmpdir_o.name)
|
||||||
|
source_file = tests.DIR_TESTFILES / "thumbnail" / "t1.webp"
|
||||||
|
resized_file = tmpdir / "thumbnail.webp"
|
||||||
|
|
||||||
|
videoutil.resize_thumbnail(source_file, resized_file)
|
||||||
|
|
||||||
|
resized_thumbnail = Image.open(resized_file)
|
||||||
|
assert resized_thumbnail.size == (360, 202)
|
||||||
|
|
Loading…
Add table
Reference in a new issue