Files
PYTV/core/services/scheduler.py

109 lines
4.4 KiB
Python

from datetime import datetime, timedelta, date, time, timezone
from core.models import Channel, ScheduleTemplate, ScheduleBlock, Airing, MediaItem
import random
class ScheduleGenerator:
"""
A service that reads the latest ScheduleTemplate and Blocks for a given channel
and generates concrete Airings logic based on available matching MediaItems.
"""
def __init__(self, channel: Channel):
self.channel = channel
def generate_for_date(self, target_date: date) -> int:
"""
Idempotent generation of airings for a specific date on this channel.
Returns the number of new Airings created.
"""
# 1. Get the highest priority active template valid on this date
template = ScheduleTemplate.objects.filter(
channel=self.channel,
is_active=True
).filter(
# Start date is null or <= target_date
valid_from_date__isnull=True
).order_by('-priority').first()
# In a real app we'd construct complex Q objects for the valid dates,
# but for PYTV mock we will just grab the highest priority active template.
if not template:
template = ScheduleTemplate.objects.filter(channel=self.channel, is_active=True).order_by('-priority').first()
if not template:
return 0
# 2. Extract day of week mask
# Python weekday: 0=Monday, 6=Sunday
# Our mask: bit 0 = Monday, bit 6 = Sunday
target_weekday_bit = 1 << target_date.weekday()
blocks = template.scheduleblock_set.all()
airings_created = 0
for block in blocks:
# Check if block runs on this day
if not (block.day_of_week_mask & target_weekday_bit):
continue
# Naive time combining mapping local time to UTC timeline without specific tz logic for simplicity now
start_dt = datetime.combine(target_date, block.start_local_time, tzinfo=timezone.utc)
end_dt = datetime.combine(target_date, block.end_local_time, tzinfo=timezone.utc)
# If the block wraps past midnight (e.g. 23:00 to 02:00)
if end_dt <= start_dt:
end_dt += timedelta(days=1)
# Clear existing airings in this window to allow idempotency
Airing.objects.filter(
channel=self.channel,
starts_at__gte=start_dt,
starts_at__lt=end_dt
).delete()
# 3. Pull matching Media Items
# Simplistic matching: pull items from library matching the block's genre
items_query = MediaItem.objects.filter(media_source__library=self.channel.library)
if block.default_genre:
items_query = items_query.filter(genres=block.default_genre)
available_items = list(items_query.exclude(item_kind="bumper"))
if not available_items:
continue
# Shuffle randomly for basic scheduling variety
random.shuffle(available_items)
# 4. Fill the block
current_cursor = start_dt
item_index = 0
while current_cursor < end_dt and item_index < len(available_items):
item = available_items[item_index]
duration = timedelta(seconds=item.runtime_seconds or 3600)
# Check if this item fits
if current_cursor + duration > end_dt:
# Item doesn't strictly fit, but we'll squeeze it in and break if needed
# Real systems pad this out or trim the slot.
pass
import uuid
Airing.objects.create(
channel=self.channel,
schedule_template=template,
schedule_block=block,
media_item=item,
starts_at=current_cursor,
ends_at=current_cursor + duration,
slot_kind="program",
status="scheduled",
source_reason="template",
generation_batch_uuid=uuid.uuid4()
)
current_cursor += duration
item_index += 1
airings_created += 1
return airings_created