From 6bebd3c51f6b44a7ea3f22fa1a855aa9cde48377 Mon Sep 17 00:00:00 2001 From: Joe Huss Date: Mon, 1 Jun 2026 13:49:10 -0400 Subject: [PATCH] feat(B-1): add library-query and media-item schemas for movie-list API Co-Authored-By: Claude Opus 4.8 (1M context) --- CHANGELOG.md | 19 +++++++ schemas/library-query.schema.json | 81 ++++++++++++++++++++++++++++ schemas/media-item.schema.json | 88 +++++++++++++++++++++++++++++++ 3 files changed, 188 insertions(+) create mode 100644 schemas/library-query.schema.json create mode 100644 schemas/media-item.schema.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 49cda15..a23fff3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,25 @@ All notable changes to `detain/phlix-shared` are documented here. This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.8.0] — 2026-06-01 + +### Added +- `schemas/library-query.schema.json` — JSON Schema (draft 2020-12) for the + query parameters of the movie-list browse API (GET /api/v1/media). Covers + `search`, `genres[]`, `yearFrom`, `yearTo`, `ratings[]`, `actors[]`, + `sort`, `order`, `limit`, and `offset` — all optional, with `genres[]` and + `ratings[]` using OR logic across multiple values, year ranges being + inclusive, and sensible `default`/`minimum`/`maximum`/`maxLength` bounds on + each field. Consumed by the Phase-B `ItemRepository::query()` implementation + and the Vue browse page in Phase C. +- `schemas/media-item.schema.json` — JSON Schema (draft 2020-12) for a single + media item returned by the browse API. Flattens and renormalizes the raw + `metadata_json` column into stable, consumer-friendly top-level fields: + `poster_url`, `genres`, `year`, `rating`, `runtime`, `overview`, `actors`, + `director`, `created_at`, `updated_at`. `poster_url` is always included so + cards render without additional data fetches. Consumed by the Phase-B API + serializer and the Phase-C `MediaCard.vue` component. + ## [0.7.0] — 2026-05-27 ### Added diff --git a/schemas/library-query.schema.json b/schemas/library-query.schema.json new file mode 100644 index 0000000..70e92fe --- /dev/null +++ b/schemas/library-query.schema.json @@ -0,0 +1,81 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://phlix.tv/schemas/library-query.schema.json", + "title": "Phlix library query parameters", + "description": "Schema describing the query parameters for the movie-list browse API endpoint (GET /api/v1/media). All parameters are optional — omitting a parameter means 'no filter on this dimension'. Multiple values in array parameters are OR-ed together (e.g. genres[]=Action&genres[]=Drama returns items that are Action OR Drama). Year ranges are inclusive. Sort defaults to name ASC when not specified.", + "type": "object", + "properties": { + "search": { + "type": "string", + "description": "Full-text or fuzzy search query applied to the media item name. Uses MySQL FULLTEXT with boolean fallback to LIKE %% matching.", + "maxLength": 200 + }, + "genres": { + "type": "array", + "description": "Filter to items that contain ANY of the listed genres (OR logic). Items with no genres set are excluded when this filter is applied.", + "items": { + "type": "string", + "minLength": 1, + "maxLength": 50 + }, + "uniqueItems": true + }, + "yearFrom": { + "type": "integer", + "description": "Minimum release year (inclusive). Items with no year set are excluded when this filter is applied.", + "minimum": 1800, + "maximum": 2100 + }, + "yearTo": { + "type": "integer", + "description": "Maximum release year (inclusive). Items with no year set are excluded when this filter is applied.", + "minimum": 1800, + "maximum": 2100 + }, + "ratings": { + "type": "array", + "description": "Filter to items with any of the listed content ratings (MPAA-style: G, PG, PG-13, R, NC-17, X, UNRATED). Items with no rating set are excluded when this filter is applied.", + "items": { + "type": "string", + "enum": ["G", "PG", "PG-13", "R", "NC-17", "X", "UNRATED"] + }, + "uniqueItems": true + }, + "actors": { + "type": "array", + "description": "Filter to items that feature ANY of the listed actors (partial name match on the actors array in metadata_json). Items with no actors set are excluded when this filter is applied.", + "items": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "uniqueItems": true + }, + "sort": { + "type": "string", + "description": "Sort field for the result set.", + "enum": ["name", "year", "rating", "date_added", "runtime"], + "default": "name" + }, + "order": { + "type": "string", + "description": "Sort direction.", + "enum": ["asc", "desc"], + "default": "asc" + }, + "limit": { + "type": "integer", + "description": "Maximum number of items to return per page.", + "minimum": 1, + "maximum": 100, + "default": 50 + }, + "offset": { + "type": "integer", + "description": "Number of items to skip for pagination (offset = page * limit).", + "minimum": 0, + "default": 0 + } + }, + "additionalProperties": false +} diff --git a/schemas/media-item.schema.json b/schemas/media-item.schema.json new file mode 100644 index 0000000..ffe6e19 --- /dev/null +++ b/schemas/media-item.schema.json @@ -0,0 +1,88 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://phlix.tv/schemas/media-item.schema.json", + "title": "Phlix media item", + "description": "Schema describing a single media item returned by the browse API endpoints (GET /api/v1/media, GET /api/v1/libraries/{id}/items). This is the canonical client-facing shape — it flattens and renormalizes the raw database columns (particularly metadata_json) into stable, consumer-friendly top-level fields. The item always includes poster_url so cards render without additional data fetches.", + "type": "object", + "required": ["id", "name", "type"], + "properties": { + "id": { + "type": "string", + "description": "Unique identifier (UUID) of the media item.", + "format": "uuid" + }, + "name": { + "type": "string", + "description": "Display name of the media item, as stored in the database (not the raw filename).", + "maxLength": 500 + }, + "type": { + "type": "string", + "description": "Media type discriminator.", + "enum": ["movie", "series", "episode", "audio", "image"] + }, + "path": { + "type": "string", + "description": "Absolute filesystem path to the media file.", + "maxLength": 2000 + }, + "poster_url": { + "type": ["string", "null"], + "description": "URL to the item's poster image. Null when no metadata has been fetched or the item has no poster.", + "format": "uri" + }, + "genres": { + "type": "array", + "description": "Array of genre strings associated with this item, sourced from metadata_json.genres. Empty array when no genres are set.", + "items": { + "type": "string", + "maxLength": 50 + } + }, + "year": { + "type": ["integer", "null"], + "description": "Four-digit release year. Null when unknown.", + "minimum": 1800, + "maximum": 2100 + }, + "rating": { + "type": ["string", "null"], + "description": "MPAA-style content rating (G, PG, PG-13, R, NC-17, X, UNRATED). Null when not set.", + "enum": ["G", "PG", "PG-13", "R", "NC-17", "X", "UNRATED", null] + }, + "runtime": { + "type": ["integer", "null"], + "description": "Total runtime in seconds. Null when unknown.", + "minimum": 0 + }, + "overview": { + "type": ["string", "null"], + "description": "Short synopsis or plot summary. Null when no overview is available.", + "maxLength": 5000 + }, + "actors": { + "type": "array", + "description": "Array of actor name strings associated with this item, sourced from metadata_json.actors. Empty array when no actors are set.", + "items": { + "type": "string", + "maxLength": 100 + } + }, + "director": { + "type": ["string", "null"], + "description": "Director name. Null when unknown.", + "maxLength": 100 + }, + "created_at": { + "type": ["string", "null"], + "description": "ISO 8601 timestamp when the item was first added to the library.", + "format": "date-time" + }, + "updated_at": { + "type": ["string", "null"], + "description": "ISO 8601 timestamp when the item was last modified.", + "format": "date-time" + } + }, + "additionalProperties": false +}