📄 AGENTS.md

Slopper — Agent Guide

Slopper picks a random piece of media from a Jellyfin database, uses AI to find a clippable moment, cuts it with FFmpeg, and stores the clip. Clips are viewable on the web frontend and can be uploaded to short-form platforms.

Project Structure

src/
  Api/            # ASP.NET Core REST API (port 5055)
  Cli/            # .NET console app — primary local experimentation tool
  Domain/         # Core domain models and abstractions
  Frontend/       # Vue 3 + TypeScript SPA (Vite)
  Infrastructure/
    Ai/           # Ollama integration
    Database/     # EF Core / SQLite
    Ffmpeg/       # FFmpeg media processing
    YouTube/      # YouTube API integration

Build Commands

# .NET
dotnet build

# Frontend
pnpm -C src/Frontend build

Running Services

# API
dotnet run --project src/Api/Api.csproj

# Frontend dev server (port printed on start)
pnpm -C src/Frontend dev

# CLI (experimentation)
dotnet run --project src/Cli/Cli.csproj

For full E2E testing, build the frontend into the API's static files directory:

pnpm -C src/Frontend build --emptyOutDir --outDir ../Api/wwwroot/
dotnet run --project src/Api/Api.csproj

Database Migrations

dotnet ef migrations add <MigrationName> --project src/Infrastructure/Database

Migrations run automatically on startup of Api and Cli.

Key Notes for Agents

Project layout & build

  • Domain and Infrastructure are pure class libraries — no entry point, built implicitly by Api/Cli.
  • Cli/Program.cs is the experimentation scratchpad. Edit it freely; checked-in experiments are welcome.
  • The project targets .NET 11 (preview SDK — global.json pins the exact version).
  • The pnpm workspace root is at the repo root; the only workspace member is src/Frontend.
  • Never manually edit *.csproj, package.json, or pnpm-lock.yaml. Use dotnet add package and pnpm add so lock files stay consistent.

Architecture & DI patterns

  • Service registration lives in extension() blocks — a C# 14 syntax. Each Infrastructure project has a ServiceCollectionExtensions file with AddXxx() extension methods.
  • All options classes are validated at startup via [OptionsValidator]-generated validators and ValidateOnStart(). Missing or malformed config will crash the host immediately, not lazily.
  • Cli and Api share the same secrets schema but have different User Secrets IDs. The Cli omits Quartz, web middleware, and authentication — it's a plain IHost.
  • Api and Cli both run SlopperStartupMigration (a IHostedService) on startup, which applies any pending EF migrations automatically.

API surface

  • All routes are under /api.
  • GET /api/clips — paginated, cursor-based via after (Guid). Max limit is 64.
  • GET /api/clips/{id}/stream — returns video/mp4 with range request support (seekable).
  • GET/PUT /api/jobs — gets job status / manually triggers ClipGenerationJob.
  • GET/PUT /api/youtube/upload — YouTube upload job status / trigger. Require Google authentication.
  • YouTube OAuth callback lands at /admin/redirect/youtube. The frontend admin page is at /admin/youtube.

Background jobs (Quartz)

  • Three Quartz jobs: ClipGenerationJob, CleanupJob, UploadJob.
  • ClipGenerationJob: [DisallowConcurrentExecution]. In Production it fires immediately on startup; in Development it only fires when manually triggered via PUT /api/jobs. If ClipGenerationJob.Interval is set (nullable TimeSpan?), the job reschedules itself after each run, it is not set in Development.
  • CleanupJob: runs on cron "0 0 * * * ?" (top of every hour). Marks old clips as removed and deletes files from disk.
  • UploadJob: resolves IUploader keyed by platform name from context.MergedJobDataMap — currently only "YouTube" is registered.

Domain model

  • Clip is a sealed record. Tags is IReadOnlySet<Tag> and is init-only. Uploads is a mutable collection appended to when a clip is uploaded.
  • Tag and Upload are sealed records (value objects). Upload.CanonicalUrl is a Uri stored as a string in SQLite.
  • ClipSelector is a singleton — it holds a pre-computed embedding of the configured ClippableQuotes and uses cosine similarity to pick the best subtitle moment.
  • ClipGenerator is transient. It orchestrates: select media → extract subtitles → find clippable moment → extract clip → extract frame → generate AI description → save.

Database

  • Two separate SQLite files: Jellyfin (read-only, external schema) and Slopper (R/W, owned schema).
  • Jellyfin uses JellyfinDbContext from the Jellyfin.Database.Implementations NuGet package — do not modify its schema or migrations.
  • IClipRepository is registered as transient. Key query methods: GetLatest (cursor paginated), GetCreatedBefore (for cleanup), GetNotUploadedTo (for upload scheduling).
  • EF migrations live in src/Infrastructure/Database. Always pass --project src/Infrastructure/Database when running dotnet ef.

Infrastructure details

  • Ffmpeg: IClipExtractor, ISubtitleReader, IFrameExtractor — all transient, all use a memory cache. Relies on FFMpegCore and SubtitlesParserV2.
  • Ai: Ollama HTTP client with a 10-minute timeout. Both IChatClient and IEmbeddingGenerator<string, Embedding<float>> are registered and decorated with OpenTelemetry.
  • YouTube: YouTubeUploader registers clips as PrivacyStatus="private" with a future PublishAt time controlled by Uploader.PublishInterval. Videos are categorized as CategoryId="1" (Film & Animation).

Frontend

  • Vue 3 + TypeScript; router has two routes: / (ClipFeed) and /admin/youtube (AdminYouTubePage).
  • API client is src/services/api.ts — plain fetch, handles 403 as unauthenticated.
  • Build script runs vue-tsc -b && vite build (type-checks before bundling). A type error will fail the build.
  • The Vite dev server has no proxy config — in dev, the frontend expects the API to be running separately on port 5055.
  • Use Playwright to confirm the UI looks as it should, or ask the user for feedback.

Observability

  • Both Api and Cli configure OpenTelemetry with OTLP export. Instrumented: ASP.NET Core (Api only), EF Core, HTTP clients, Quartz (Api only), runtime metrics.

Testing

  • Manual E2E tests use Playwright. Config is at .playwright/cli.config.json.
  • To run E2E tests the frontend must be built into src/Api/wwwroot/ and the Api must be running.
  • Chrome/Chromium is the default for Playwright, if it fails try --browser msedge.