// OpenAPI 3.0 spec for the streamreverse REST API. // Served at GET /api.json and rendered by Swagger UI at GET /. const spec = { openapi: '3.0.3', info: { title: 'bug.tv api', version: '1.0.0' }, tags: [ { name: 'meta', description: 'service and provider metadata' }, { name: 'search', description: 'TMDB search and episode browsing' }, { name: 'details', description: 'TMDB media details' }, { name: 'streams', description: 'm3u8 stream lookup from providers' }, ], paths: { '/providers': { get: { tags: ['meta'], summary: 'List all providers and which are enabled', operationId: 'listProviders', responses: { 200: { description: 'provider list', content: { 'application/json': { schema: { type: 'object', properties: { all: { type: 'array', items: { type: 'string' } }, enabled: { type: 'array', items: { type: 'string' } }, }, }, }, }, }, }, }, }, '/proxy/media': { get: { tags: ['meta'], summary: 'Proxy upstream media or m3u8 playlists with server-side headers', operationId: 'proxyMedia', parameters: [ { name: 'url', in: 'query', required: true, schema: { type: 'string' }, }, { name: 'h', in: 'query', required: false, description: 'JSON-encoded headers to send to the upstream URL', schema: { type: 'string' }, }, ], responses: { 200: { description: 'Proxied upstream response. m3u8 playlist URIs are rewritten to use the proxy.', }, 400: { $ref: '#/components/responses/BadRequest' }, 502: { $ref: '#/components/responses/Upstream' }, }, }, }, '/search': { get: { tags: ['search'], summary: 'Search movies + TV via TMDB', operationId: 'search', parameters: [ { name: 'q', in: 'query', required: true, description: 'search text', schema: { type: 'string' }, example: 'dune', }, ], responses: { 200: { description: 'up to 15 results', content: { 'application/json': { schema: { type: 'object', properties: { query: { type: 'string' }, count: { type: 'integer' }, results: { type: 'array', items: { $ref: '#/components/schemas/SearchResult' } }, }, }, }, }, }, 400: { $ref: '#/components/responses/BadRequest' }, 502: { $ref: '#/components/responses/Upstream' }, }, }, }, '/trending': { get: { tags: ['search'], summary: 'Trending movies and TV via TMDB', operationId: 'getTrending', parameters: [ { name: 'type', in: 'query', required: false, schema: { type: 'string', enum: ['all', 'movie', 'tv'], default: 'all' }, }, { name: 'window', in: 'query', required: false, schema: { type: 'string', enum: ['day', 'week'], default: 'week' }, }, ], responses: { 200: { description: 'up to 20 trending items', content: { 'application/json': { schema: { type: 'object', properties: { type: { type: 'string' }, window: { type: 'string' }, count: { type: 'integer' }, results: { type: 'array', items: { $ref: '#/components/schemas/SearchResult' } }, }, }, }, }, }, 400: { $ref: '#/components/responses/BadRequest' }, 502: { $ref: '#/components/responses/Upstream' }, }, }, }, '/movie/{id}': { get: { tags: ['details'], summary: 'Movie details (genres, rating, images, description)', description: 'Passes through the raw TMDB movie object, plus `poster_url`/`backdrop_url` convenience fields.', operationId: 'getMovieDetails', parameters: [{ $ref: '#/components/parameters/MovieId' }], responses: { 200: { description: 'TMDB movie object', content: { 'application/json': { schema: { $ref: '#/components/schemas/MediaDetails' } } }, }, 404: { $ref: '#/components/responses/NotFound' }, 502: { $ref: '#/components/responses/Upstream' }, }, }, }, '/tv/{id}': { get: { tags: ['details'], summary: 'TV show details (genres, rating, images, description)', description: 'Passes through the raw TMDB tv object, plus `poster_url`/`backdrop_url` convenience fields.', operationId: 'getTvDetails', parameters: [{ $ref: '#/components/parameters/TvId' }], responses: { 200: { description: 'TMDB tv object', content: { 'application/json': { schema: { $ref: '#/components/schemas/MediaDetails' } } }, }, 404: { $ref: '#/components/responses/NotFound' }, 502: { $ref: '#/components/responses/Upstream' }, }, }, }, '/tv/{id}/seasons': { get: { tags: ['search'], summary: 'List seasons for a TV show', operationId: 'getSeasons', parameters: [{ $ref: '#/components/parameters/TvId' }], responses: { 200: { description: 'season list', content: { 'application/json': { schema: { type: 'object', properties: { tmdb_id: { type: 'string' }, count: { type: 'integer' }, seasons: { type: 'array', items: { $ref: '#/components/schemas/Season' } }, }, }, }, }, }, 400: { $ref: '#/components/responses/BadRequest' }, 502: { $ref: '#/components/responses/Upstream' }, }, }, }, '/tv/{id}/seasons/{season}': { get: { tags: ['search'], summary: 'List episodes for a season', operationId: 'getEpisodes', parameters: [ { $ref: '#/components/parameters/TvId' }, { name: 'season', in: 'path', required: true, description: 'season number', schema: { type: 'integer' }, example: 1, }, ], responses: { 200: { description: 'episode list', content: { 'application/json': { schema: { type: 'object', properties: { tmdb_id: { type: 'string' }, season: { type: 'integer' }, count: { type: 'integer' }, episodes: { type: 'array', items: { $ref: '#/components/schemas/Episode' } }, }, }, }, }, }, 400: { $ref: '#/components/responses/BadRequest' }, 502: { $ref: '#/components/responses/Upstream' }, }, }, }, '/streams': { get: { tags: ['streams'], summary: 'Fetch m3u8 streams for a movie or TV episode', operationId: 'getStreams', description: 'Scrapes every enabled provider in parallel (override with `providers`) and returns deduped, normalized, quality-ranked streams. ' + 'For `type=tv` you must pass `season` and `episode`. Optional filters are applied before ranking.', parameters: [ { name: 'tmdb', in: 'query', required: true, description: 'TMDB numeric id', schema: { type: 'integer' }, example: 438631, }, { name: 'type', in: 'query', required: false, description: 'movie (default) or tv', schema: { type: 'string', enum: ['movie', 'tv'], default: 'movie' }, }, { name: 'season', in: 'query', required: false, description: 'season number (required when type=tv)', schema: { type: 'integer' }, }, { name: 'episode', in: 'query', required: false, description: 'episode number (required when type=tv)', schema: { type: 'integer' }, }, { name: 'providers', in: 'query', required: false, description: 'comma-separated provider names; defaults to the enabled set', schema: { type: 'string' }, example: 'videasy,vidfast', }, { name: 'minQuality', in: 'query', required: false, schema: { type: 'string', enum: ['any', '480', '720', '1080', '2160'], default: 'any' }, }, { name: 'audioCodec', in: 'query', required: false, schema: { type: 'string', enum: ['any', 'aac', 'ac3', 'dts', 'opus', 'mp3', 'flac'], default: 'any' }, }, { name: 'subtitles', in: 'query', required: false, schema: { type: 'string', enum: ['any', 'required', 'lang'], default: 'any' }, }, { name: 'subtitleLang', in: 'query', required: false, description: 'two-letter lang code, used when subtitles=lang', schema: { type: 'string' }, example: 'en', }, ], responses: { 200: { description: 'stream list (best quality first)', content: { 'application/json': { schema: { $ref: '#/components/schemas/StreamList' }, }, }, }, 400: { $ref: '#/components/responses/BadRequest' }, 500: { description: 'no providers available / internal error', content: { 'application/json': { schema: { $ref: '#/components/schemas/Error' } } }, }, }, }, }, }, components: { parameters: { MovieId: { name: 'id', in: 'path', required: true, description: 'TMDB movie id', schema: { type: 'integer' }, example: 438631, }, TvId: { name: 'id', in: 'path', required: true, description: 'TMDB tv id', schema: { type: 'integer' }, example: 1396, }, }, responses: { BadRequest: { description: 'missing or invalid parameters', content: { 'application/json': { schema: { $ref: '#/components/schemas/Error' } } }, }, NotFound: { description: 'media id not found on TMDB', content: { 'application/json': { schema: { $ref: '#/components/schemas/Error' } } }, }, Upstream: { description: 'TMDB request failed', content: { 'application/json': { schema: { $ref: '#/components/schemas/Error' } } }, }, }, schemas: { Error: { type: 'object', properties: { error: { type: 'string' } }, }, SearchResult: { type: 'object', properties: { tmdb_id: { type: 'integer', example: 438631 }, type: { type: 'string', enum: ['movie', 'tv'] }, name: { type: 'string', example: 'Dune' }, year: { type: 'string', example: '2021' }, poster: { type: 'string', format: 'uri' }, overview: { type: 'string' }, }, }, MediaDetails: { type: 'object', description: 'raw TMDB media object (movie or tv) with added poster_url/backdrop_url', properties: { id: { type: 'integer', example: 438631 }, title: { type: 'string', description: 'movie title' }, name: { type: 'string', description: 'tv show name' }, overview: { type: 'string', description: 'description' }, tagline: { type: 'string' }, genres: { type: 'array', items: { type: 'object', properties: { id: { type: 'integer' }, name: { type: 'string' }, }, }, }, vote_average: { type: 'number', description: 'rating, 0-10' }, vote_count: { type: 'integer' }, popularity: { type: 'number' }, status: { type: 'string' }, runtime: { type: 'integer', description: 'movie runtime in minutes' }, number_of_seasons: { type: 'integer', description: 'tv' }, number_of_episodes: { type: 'integer', description: 'tv' }, release_date: { type: 'string', format: 'date', description: 'movie' }, first_air_date: { type: 'string', format: 'date', description: 'tv' }, poster_path: { type: 'string' }, backdrop_path: { type: 'string' }, poster_url: { type: 'string', format: 'uri', description: 'full url (original size)' }, backdrop_url: { type: 'string', format: 'uri', description: 'full url (original size)' }, images: { type: 'object', description: 'all available images from TMDB (posters, backdrops, logos, profiles); every item includes `url` with the full image url', }, }, additionalProperties: true, }, Season: { type: 'object', properties: { season_number: { type: 'integer' }, name: { type: 'string' }, episode_count: { type: 'integer' }, air_date: { type: 'string', format: 'date' }, }, }, Episode: { type: 'object', properties: { episode_number: { type: 'integer' }, name: { type: 'string' }, air_date: { type: 'string', format: 'date' }, }, }, Subtitle: { type: 'object', properties: { url: { type: 'string', format: 'uri' }, language: { type: 'string' }, name: { type: 'string' }, }, }, Stream: { type: 'object', properties: { provider: { type: 'string', example: 'videasy' }, name: { type: 'string' }, url: { type: 'string', format: 'uri', description: 'the m3u8 (or direct) url' }, quality: { type: 'string', example: '2160p' }, pixels: { type: ['integer', 'null'], example: 2160 }, audio: { type: ['string', 'null'], example: 'aac' }, type: { type: 'string', example: 'm3u8' }, headers: { type: 'object', description: 'http headers the url needs (e.g. Referer)', additionalProperties: { type: 'string' }, }, subtitles: { type: 'array', items: { $ref: '#/components/schemas/Subtitle' } }, }, }, StreamList: { type: 'object', properties: { tmdb_id: { type: 'integer' }, type: { type: 'string', enum: ['movie', 'tv'] }, season: { type: ['integer', 'null'] }, episode: { type: ['integer', 'null'] }, total: { type: 'integer', description: 'streams before filters' }, matched: { type: 'integer', description: 'streams after filters' }, streams: { type: 'array', items: { $ref: '#/components/schemas/Stream' } }, providerErrors: { type: 'object', description: 'per-provider scrape errors', additionalProperties: { type: 'string' }, }, loadErrors: { type: 'object', description: 'per-provider load errors', additionalProperties: { type: 'string' }, }, }, }, }, }, }; module.exports = spec;