109 lines
4.4 KiB
Python
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
|