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