Skip to content
Merged
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
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
81 changes: 81 additions & 0 deletions schemas/library-query.schema.json
Original file line number Diff line number Diff line change
@@ -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
}
88 changes: 88 additions & 0 deletions schemas/media-item.schema.json
Original file line number Diff line number Diff line change
@@ -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
}
Loading