Add title to sport event posters

This commit is contained in:
mhdzumair
2024-03-24 00:48:15 +05:30
parent 1d39ebdd14
commit cc55bd5330
5 changed files with 165 additions and 28 deletions
+12 -3
View File
@@ -1,8 +1,10 @@
import asyncio
import json
import logging
from io import BytesIO
from typing import Literal
import aiohttp
import redis.asyncio as redis
from apscheduler.schedulers.asyncio import AsyncIOScheduler
from apscheduler.triggers.cron import CronTrigger
@@ -437,11 +439,18 @@ async def get_poster(
return StreamingResponse(
image_byte_io, media_type="image/jpeg", headers=const.DEFAULT_HEADERS
)
except asyncio.TimeoutError:
logging.error("Poster generation timeout.")
raise HTTPException(status_code=404, detail="Poster generation timeout.")
except aiohttp.ClientResponse as e:
logging.error(f"Failed to create poster: {e}, status: {e.status}")
if e.status != 404:
raise HTTPException(status_code=404, detail="Failed to create poster.")
except Exception as e:
logging.error(f"Unexpected error while creating poster: {e}")
mediafusion_data.is_poster_working = False
await mediafusion_data.save()
raise HTTPException(status_code=404, detail="Failed to create poster.")
mediafusion_data.is_poster_working = False
await mediafusion_data.save()
raise HTTPException(status_code=404, detail="Failed to create poster.")
@app.get("/scraper", tags=["scraper"])
+1
View File
@@ -72,6 +72,7 @@ class MediaFusionMetaData(Document):
year: Optional[int] = None
poster: Optional[str] = None
is_poster_working: Optional[bool] = True
is_add_title_to_poster: Optional[bool] = False
background: Optional[str] = None
streams: list[Link[TorrentStreams]]
type: str
@@ -100,6 +100,7 @@ class SportVideoSpider(scrapy.Spider):
"website": torrent_page_link,
"is_parse_ptn": False,
"source": "sport-video.org.ua",
"is_add_title_to_poster": True,
"catalog": category,
}
},
Binary file not shown.
+151 -25
View File
@@ -1,52 +1,64 @@
import asyncio
from concurrent.futures import ThreadPoolExecutor
from io import BytesIO
import aiohttp
from PIL import Image, ImageDraw, ImageFont, UnidentifiedImageError
from PIL import Image, ImageDraw, ImageFont, UnidentifiedImageError, ImageStat
from imdb import Cinemagoer
from db.models import MediaFusionMetaData
from utils import const
ia = Cinemagoer()
font_cache = {}
executor = ThreadPoolExecutor(max_workers=4)
async def create_poster(mediafusion_data: MediaFusionMetaData) -> BytesIO:
async def fetch_poster_image(url: str) -> bytes:
async with aiohttp.ClientSession() as session:
async with session.get(
mediafusion_data.poster, timeout=10, headers=const.UA_HEADER
) as response:
async with session.get(url, timeout=10, headers=const.UA_HEADER) as response:
response.raise_for_status()
# Check if the response content type is an image
if not response.headers["Content-Type"].lower().startswith("image/"):
raise ValueError(
f"Unexpected content type: {response.headers['Content-Type']} for URL: {mediafusion_data.poster}"
f"Unexpected content type: {response.headers['Content-Type']} for URL: {url}"
)
content = await response.read()
return await response.read()
# Check if the response content is not empty
if not content:
raise ValueError(f"Empty content for URL: {mediafusion_data.poster}")
# Synchronous function for CPU-bound task: image processing
def process_poster_image(
content: bytes, mediafusion_data: MediaFusionMetaData
) -> BytesIO:
try:
image = Image.open(BytesIO(content)).convert("RGBA")
image = image.resize((300, 450))
imdb_rating = None # Assume you fetch this rating elsewhere if needed
# The add_elements_to_poster function would be synchronous
image = add_elements_to_poster(image, imdb_rating)
if mediafusion_data.is_add_title_to_poster:
# The add_title_to_poster function would also be synchronous
image = add_title_to_poster(image, mediafusion_data.title)
image = image.convert("RGB")
byte_io = BytesIO()
image.save(byte_io, "JPEG")
byte_io.seek(0)
return byte_io
except UnidentifiedImageError:
raise ValueError(f"Cannot identify image from URL: {mediafusion_data.poster}")
image = image.resize((300, 450))
imdb_rating = None
if mediafusion_data.id.startswith("tt"):
try:
result = ia.get_movie(mediafusion_data.id[2:], info="main")
imdb_rating = result.get("rating")
except Exception:
pass
image = add_elements_to_poster(image, imdb_rating)
image = image.convert("RGB")
async def create_poster(mediafusion_data: MediaFusionMetaData) -> BytesIO:
content = await fetch_poster_image(mediafusion_data.poster)
byte_io = BytesIO()
image.save(byte_io, "JPEG")
byte_io.seek(0)
loop = asyncio.get_event_loop()
byte_io = await asyncio.wait_for(
loop.run_in_executor(executor, process_poster_image, content, mediafusion_data),
30,
)
return byte_io
@@ -59,7 +71,7 @@ def add_elements_to_poster(
# Adding IMDb rating at the bottom left with a semi-transparent background
if imdb_rating:
font = ImageFont.truetype("resources/fonts/IBMPlexSans-Medium.ttf", size=24)
font = load_font("resources/fonts/IBMPlexSans-Medium.ttf", 24)
imdb_text = f"IMDb: {imdb_rating}/10"
# Calculate text bounding box using the draw instance
@@ -95,3 +107,117 @@ def add_elements_to_poster(
image.paste(watermark, watermark_position, watermark)
return image
def load_font(font_path, font_size):
if (font_path, font_size) not in font_cache:
font_cache[(font_path, font_size)] = ImageFont.truetype(
font_path, size=font_size
)
return font_cache[(font_path, font_size)]
# Function to split the title into multiple lines
def split_title(title, font, draw, max_width):
lines = []
words = title.split()
average_character_width = sum(
draw.textbbox((0, 0), char, font=font)[2] for char in set(title)
) / len(set(title))
while words:
line = ""
line_width = 0
while words and line_width + average_character_width <= max_width:
word_width = draw.textbbox((0, 0), words[0], font=font)[2]
if line_width + word_width <= max_width:
line += words.pop(0) + " "
line_width += word_width + average_character_width # Add space width
else:
break
lines.append(line.strip())
return lines
# Function to adjust font size and split title
def adjust_font_and_split(title, font_path, max_width, initial_font_size, draw):
font_size = initial_font_size
font = load_font(font_path, font_size)
lines = split_title(title, font, draw, max_width)
while True:
# Get the bounding box of the last line of text
bbox = draw.textbbox((0, 0), lines[-1], font=font)
text_width = bbox[2] - bbox[0]
if len(lines) > 2 or text_width >= max_width:
font_size -= 1 # Decrease font size by 1
font = load_font(font_path, font_size)
lines = split_title(title, font, draw, max_width)
else:
break
return lines, font
def get_average_color(image, bbox):
# Crop the image to the bounding box
cropped_image = image.crop(bbox)
# Get the average color of the cropped image
stat = ImageStat.Stat(cropped_image)
return stat.mean
def text_color_based_on_background(average_color):
# Calculate the perceived brightness of the average color
brightness = sum(
[average_color[i] * v for i, v in enumerate([0.299, 0.587, 0.114])]
)
if brightness > 128:
return "black", "white" # Dark text, light outline
else:
return "white", "black" # Light text, dark outline
# Function to draw text with an outline
def draw_text_with_outline(draw, position, text, font, fill_color, outline_color):
x, y = position
# Slightly thicker outlines can be more performant than multiple thin ones
outline_width = 3
# Offset coordinates for the outline
outline_offsets = [
(dx, dy)
for dx in range(-outline_width, outline_width + 1)
for dy in range(-outline_width, outline_width + 1)
if dx or dy
]
for offset in outline_offsets:
draw.text((x + offset[0], y + offset[1]), text, font=font, fill=outline_color)
draw.text(position, text, font=font, fill=fill_color)
def add_title_to_poster(image: Image.Image, title_text: str) -> Image.Image:
draw = ImageDraw.Draw(image)
max_width = image.width - 20 # max width for the text
font_path = "resources/fonts/IBMPlexSans-Bold.ttf"
initial_font_size = 50 # Starting font size which will be adjusted dynamically
lines, font = adjust_font_and_split(
title_text, font_path, max_width, initial_font_size, draw
)
# Calculate the total height of the text block using textbbox
text_block_height = sum(draw.textbbox((0, 0), line, font=font)[3] for line in lines)
# Starting y position, centered vertically
y = (image.height - text_block_height) // 2
sample_area = (0, y, image.width, y + text_block_height)
average_color = get_average_color(image, sample_area)
text_color, outline_color = text_color_based_on_background(average_color)
# Draw each line of text
for line in lines:
bbox = draw.textbbox((0, 0), line, font=font)
line_width = bbox[2] - bbox[0]
line_height = bbox[3] - bbox[1]
x = (image.width - line_width) // 2 # Center horizontally
draw_text_with_outline(draw, (x, y), line, font, text_color, outline_color)
y += line_height # Move y position for next line
return image