feat(main): commit

This commit is contained in:
2026-03-08 16:48:58 -04:00
parent 567766eaed
commit f37382d2b8
29 changed files with 3735 additions and 223 deletions

View File

@@ -0,0 +1,52 @@
"""
management command: cache_upcoming
Delegates to core.services.cache.run_cache() — the same logic exposed
by the API endpoint, so CLI and web UI behavior are always in sync.
Usage:
python manage.py cache_upcoming # default: next 24 hours
python manage.py cache_upcoming --hours 48
python manage.py cache_upcoming --prune-only
"""
from django.core.management.base import BaseCommand
from core.services.cache import run_cache
class Command(BaseCommand):
help = "Download YouTube videos for upcoming airings and prune old cache files."
def add_arguments(self, parser):
parser.add_argument(
"--hours",
type=int,
default=24,
help="How many hours ahead to scan for upcoming airings (default: 24).",
)
parser.add_argument(
"--prune-only",
action="store_true",
default=False,
help="Only delete expired cache files; do not download anything new.",
)
def handle(self, *args, **options):
hours = options["hours"]
prune_only = options["prune_only"]
self.stdout.write(f"▶ Running cache worker (window: {hours}h, prune-only: {prune_only})")
result = run_cache(hours=hours, prune_only=prune_only)
self.stdout.write(self.style.SUCCESS(f" 🗑 Pruned: {result['pruned']}"))
self.stdout.write(self.style.SUCCESS(f" ↓ Downloaded: {result['downloaded']}"))
self.stdout.write(self.style.SUCCESS(f" ✓ Already cached: {result['already_cached']}"))
if result["failed"]:
self.stderr.write(self.style.ERROR(f" ✗ Failed: {result['failed']}"))
for item in result["items"]:
icon = {"downloaded": "", "cached": "", "failed": ""}.get(item["status"], "?")
line = f" {icon} [{item['status']:10}] {item['title'][:70]}"
if item.get("error"):
line += f"{item['error']}"
self.stdout.write(line)

View File

@@ -1,16 +1,29 @@
from django.core.management.base import BaseCommand
from core.models import AppUser, Library, Channel, MediaItem, Airing, ScheduleTemplate
from core.services.scheduler import ScheduleGenerator
from django.utils import timezone
from datetime import timedelta
from datetime import timedelta, date
import textwrap
class Command(BaseCommand):
help = "Displays a beautifully formatted terminal dashboard of the current backend state."
def add_arguments(self, parser):
parser.add_argument('--channel', type=int, help='Inspect specific channel schedule')
parser.add_argument('--test-generate', action='store_true', help='Trigger generation for today if inspecting a channel')
def get_color(self, text, code):
"""Helper to wrap string in bash color codes"""
return f"\033[{code}m{text}\033[0m"
def handle(self, *args, **options):
channel_id = options.get('channel')
test_generate = options.get('test_generate')
if channel_id:
self.inspect_channel(channel_id, test_generate)
return
# 1. Gather Aggregate Metrics
total_users = AppUser.objects.count()
total_libraries = Library.objects.count()
@@ -46,7 +59,7 @@ class Command(BaseCommand):
for c in channels:
status_color = "1;32" if c.is_active else "1;31"
status_text = "ACTIVE" if c.is_active else "INACTIVE"
self.stdout.write(f"\n 📺 [{c.channel_number or '-'}] {c.name} ({self.get_color(status_text, status_color)})")
self.stdout.write(f"\n 📺 [{c.id}] {c.name} (Ch {c.channel_number or '-'}) ({self.get_color(status_text, status_color)})")
# Show templates
templates = c.scheduletemplate_set.filter(is_active=True).order_by('-priority')
@@ -57,4 +70,40 @@ class Command(BaseCommand):
blocks_count = t.scheduleblock_set.count()
self.stdout.write(f" 📄 Template: {t.name} (Priority {t.priority}) -> {blocks_count} Blocks")
self.stdout.write(f"\nUse {self.get_color('--channel <id>', '1;37')} to inspect detailed schedule.\n")
def inspect_channel(self, channel_id, test_generate):
try:
channel = Channel.objects.get(id=channel_id)
except Channel.DoesNotExist:
self.stdout.write(self.get_color(f"Error: Channel {channel_id} not found.", "1;31"))
return
if test_generate:
self.stdout.write(self.get_color(f"\nTriggering schedule generation for {channel.name}...", "1;33"))
generator = ScheduleGenerator(channel)
count = generator.generate_for_date(date.today())
self.stdout.write(f"Done. Created {self.get_color(str(count), '1;32')} new airings.")
now = timezone.now()
end_window = now + timedelta(hours=12)
airings = Airing.objects.filter(
channel=channel,
ends_at__gt=now,
starts_at__lt=end_window
).select_related('media_item').order_by('starts_at')
self.stdout.write(self.get_color(f"\n=== Schedule for {channel.name} (Next 12h) ===", "1;34"))
if not airings:
self.stdout.write(self.get_color(" (No airings scheduled in this window)", "1;33"))
else:
for a in airings:
time_str = f"{a.starts_at.strftime('%H:%M')} - {a.ends_at.strftime('%H:%M')}"
if a.starts_at <= now <= a.ends_at:
self.stdout.write(f" {self.get_color('▶ ON AIR', '1;32')} {self.get_color(time_str, '1;37')} | {a.media_item.title}")
else:
self.stdout.write(f" {time_str} | {a.media_item.title}")
self.stdout.write("\n")