📄
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
DomainandInfrastructureare pure class libraries — no entry point, built implicitly byApi/Cli.Cli/Program.csis the experimentation scratchpad. Edit it freely; checked-in experiments are welcome.- The project targets .NET 11 (preview SDK —
global.jsonpins 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, orpnpm-lock.yaml. Usedotnet add packageandpnpm addso lock files stay consistent.
Architecture & DI patterns
- Service registration lives in
extension()blocks — a C# 14 syntax. Each Infrastructure project has aServiceCollectionExtensionsfile withAddXxx()extension methods. - All options classes are validated at startup via
[OptionsValidator]-generated validators andValidateOnStart(). Missing or malformed config will crash the host immediately, not lazily. CliandApishare the same secrets schema but have different User Secrets IDs. TheCliomits Quartz, web middleware, and authentication — it's a plainIHost.ApiandCliboth runSlopperStartupMigration(aIHostedService) on startup, which applies any pending EF migrations automatically.
API surface
- All routes are under
/api. GET /api/clips— paginated, cursor-based viaafter(Guid). Maxlimitis 64.GET /api/clips/{id}/stream— returnsvideo/mp4with range request support (seekable).GET/PUT /api/jobs— gets job status / manually triggersClipGenerationJob.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 viaPUT /api/jobs. IfClipGenerationJob.Intervalis set (nullableTimeSpan?), 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: resolvesIUploaderkeyed by platform name fromcontext.MergedJobDataMap— currently only"YouTube"is registered.
Domain model
Clipis a sealed record.TagsisIReadOnlySet<Tag>and is init-only.Uploadsis a mutable collection appended to when a clip is uploaded.TagandUploadare sealed records (value objects).Upload.CanonicalUrlis aUristored as a string in SQLite.ClipSelectoris a singleton — it holds a pre-computed embedding of the configuredClippableQuotesand uses cosine similarity to pick the best subtitle moment.ClipGeneratoris 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
JellyfinDbContextfrom theJellyfin.Database.ImplementationsNuGet package — do not modify its schema or migrations. IClipRepositoryis 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/Databasewhen runningdotnet 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
IChatClientandIEmbeddingGenerator<string, Embedding<float>>are registered and decorated with OpenTelemetry. - YouTube:
YouTubeUploaderregisters clips asPrivacyStatus="private"with a futurePublishAttime controlled byUploader.PublishInterval. Videos are categorized asCategoryId="1"(Film & Animation).
Frontend
- Vue 3 + TypeScript; router has two routes:
/(ClipFeed) and/admin/youtube(AdminYouTubePage). - API client is
src/services/api.ts— plainfetch, 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
ApiandCliconfigure 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.