mirror of
https://github.com/Viren070/MediaFusion.git
synced 2025-12-01 23:21:11 +01:00
Add title to sport event posters
This commit is contained in:
+12
-3
@@ -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"])
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user