Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 36 additions & 1 deletion anthias_app/admin.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
from django.contrib import admin

from anthias_app.models import Asset
from anthias_app.models import (
Asset,
ScheduleSlot,
ScheduleSlotItem,
)


@admin.register(Asset)
Expand All @@ -21,3 +25,34 @@ class AssetAdmin(admin.ModelAdmin):
'play_order',
'skip_asset_check',
)


class ScheduleSlotItemInline(admin.TabularInline):
model = ScheduleSlotItem
extra = 0


@admin.register(ScheduleSlot)
class ScheduleSlotAdmin(admin.ModelAdmin):
list_display = (
'slot_id',
'name',
'slot_type',
'time_from',
'time_to',
'days_of_week',
'is_default',
'sort_order',
)
inlines = [ScheduleSlotItemInline]


@admin.register(ScheduleSlotItem)
class ScheduleSlotItemAdmin(admin.ModelAdmin):
list_display = (
'item_id',
'slot',
'asset',
'sort_order',
'duration_override',
)
127 changes: 127 additions & 0 deletions anthias_app/migrations/0003_schedule_slots.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
from django.db import migrations, models
import django.db.models.deletion

import anthias_app.models


class Migration(migrations.Migration):

dependencies = [
('anthias_app', '0002_auto_20241015_1524'),
]

operations = [
migrations.CreateModel(
name='ScheduleSlot',
fields=[
(
'slot_id',
models.TextField(
default=anthias_app.models.generate_asset_id,
editable=False,
primary_key=True,
serialize=False,
),
),
('name', models.TextField(default='')),
(
'slot_type',
models.CharField(
choices=[
('default', 'Default'),
('time', 'Time'),
('event', 'Event'),
],
default='time',
max_length=10,
),
),
(
'time_from',
models.TimeField(default='00:00'),
),
(
'time_to',
models.TimeField(default='23:59'),
),
(
'days_of_week',
models.TextField(
default=anthias_app.models._default_all_days,
),
),
(
'is_default',
models.BooleanField(default=False),
),
(
'start_date',
models.DateField(blank=True, null=True),
),
(
'end_date',
models.DateField(blank=True, null=True),
),
(
'no_loop',
models.BooleanField(default=False),
),
('sort_order', models.IntegerField(default=0)),
],
options={
'db_table': 'schedule_slots',
'ordering': ['sort_order', 'time_from'],
},
),
migrations.CreateModel(
name='ScheduleSlotItem',
fields=[
(
'item_id',
models.TextField(
default=anthias_app.models.generate_asset_id,
editable=False,
primary_key=True,
serialize=False,
),
),
('sort_order', models.IntegerField(default=0)),
(
'duration_override',
models.BigIntegerField(
blank=True,
help_text=(
'If set, overrides the asset'
' duration for this slot.'
),
null=True,
),
),
(
'asset',
models.ForeignKey(
on_delete=(
django.db.models.deletion.CASCADE
),
related_name='slot_items',
to='anthias_app.asset',
),
),
(
'slot',
models.ForeignKey(
on_delete=(
django.db.models.deletion.CASCADE
),
related_name='items',
to='anthias_app.scheduleslot',
),
),
],
options={
'db_table': 'schedule_slot_items',
'ordering': ['sort_order'],
'unique_together': {('slot', 'asset')},
},
),
]
182 changes: 182 additions & 0 deletions anthias_app/models.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import json
import uuid

from django.db import models
Expand All @@ -8,6 +9,17 @@ def generate_asset_id():
return uuid.uuid4().hex


def _default_all_days():
return '[1,2,3,4,5,6,7]'


SLOT_TYPE_CHOICES = [
('default', 'Default'),
('time', 'Time'),
('event', 'Event'),
]


class Asset(models.Model):
asset_id = models.TextField(
primary_key=True, default=generate_asset_id, editable=False
Expand Down Expand Up @@ -37,3 +49,173 @@ def is_active(self):
return self.start_date < current_time < self.end_date

return False


class ScheduleSlot(models.Model):
"""A time-of-day slot in the playback schedule.

When at least one ScheduleSlot exists the viewer switches to
"schedule mode": only assets linked via ScheduleSlotItem to the
currently-active slot are played. When the table is empty the
viewer falls back to legacy behaviour (asset.is_active()).

``is_default=True`` marks the fallback slot whose content plays
whenever no other slot covers the current time. At most one
default slot may exist.

Overnight slots are supported: if ``time_from > time_to`` the
window wraps past midnight (e.g. 22:00 -> 06:00).
``days_of_week`` refers to the **start** day of such a slot.
"""

slot_id = models.TextField(
primary_key=True,
default=generate_asset_id,
editable=False,
)
name = models.TextField(default='')
slot_type = models.CharField(
max_length=10,
choices=SLOT_TYPE_CHOICES,
default='time',
)
time_from = models.TimeField(default='00:00')
time_to = models.TimeField(default='23:59')
days_of_week = models.TextField(default=_default_all_days)
is_default = models.BooleanField(default=False)
start_date = models.DateField(null=True, blank=True)
end_date = models.DateField(null=True, blank=True)
no_loop = models.BooleanField(default=False)
sort_order = models.IntegerField(default=0)

class Meta:
db_table = 'schedule_slots'
ordering = ['sort_order', 'time_from']

def __str__(self):
if self.is_default:
return f'{self.name} (default)'
if self.slot_type == 'event':
return f'{self.name} (event @ {self.time_from})'
return f'{self.name} {self.time_from}-{self.time_to}'

def get_days_of_week(self):
"""Return days_of_week as a Python list of ints."""
if isinstance(self.days_of_week, list):
return self.days_of_week
try:
return json.loads(self.days_of_week)
except (TypeError, json.JSONDecodeError):
return [1, 2, 3, 4, 5, 6, 7]

@property
def is_overnight(self):
"""True when the slot wraps past midnight."""
return self.time_from > self.time_to

def is_currently_active(self):
"""Return True if this slot covers the current local time."""
if self.is_default:
return False

now = timezone.localtime()
current_time = now.time()
current_weekday = now.isoweekday()

if self.slot_type == 'event':
return self._is_event_active(
now,
current_time,
current_weekday,
)

days = self.get_days_of_week()

if not self.is_overnight:
return (
current_weekday in days
and self.time_from <= current_time < self.time_to
)

if current_time >= self.time_from:
return current_weekday in days
elif current_time < self.time_to:
yesterday = current_weekday - 1 if current_weekday > 1 else 7
return yesterday in days

return False

def _is_event_active(
self,
now,
current_time,
current_weekday,
):
"""Check if an event slot is currently active.

Supports three recurrence modes:
- One-time: start_date set, end_date null -> only that date.
- Daily/recurring with range: start_date + end_date.
- Weekly (selected days): days_of_week subset.
"""
today = now.date()

if self.start_date and self.end_date:
if not (self.start_date <= today <= self.end_date):
return False
elif self.start_date:
if today != self.start_date:
return False
elif self.end_date:
if today > self.end_date:
return False

days = self.get_days_of_week()
if days and current_weekday not in days:
return False

return self.time_from <= current_time < self.time_to


class ScheduleSlotItem(models.Model):
"""Links an Asset to a ScheduleSlot with optional duration override.

The same asset may appear in multiple *different* slots but only
once per slot (enforced by ``unique_together``).
"""

item_id = models.TextField(
primary_key=True,
default=generate_asset_id,
editable=False,
)
slot = models.ForeignKey(
ScheduleSlot,
on_delete=models.CASCADE,
related_name='items',
)
asset = models.ForeignKey(
Asset,
on_delete=models.CASCADE,
related_name='slot_items',
)
sort_order = models.IntegerField(default=0)
duration_override = models.BigIntegerField(
blank=True,
null=True,
help_text=('If set, overrides the asset duration for this slot.'),
)

class Meta:
db_table = 'schedule_slot_items'
ordering = ['sort_order']
unique_together = [['slot', 'asset']]

def __str__(self):
return f'{self.slot.name} -> {self.asset.name}'

@property
def effective_duration(self):
if self.duration_override is not None:
return self.duration_override
return self.asset.duration
Loading
Loading