diff --git a/flake.nix b/flake.nix index 3e3349f..11ccd35 100644 --- a/flake.nix +++ b/flake.nix @@ -22,13 +22,13 @@ }; npmDeps = pkgs.fetchNpmDeps { - name = "streamreverse-npm-deps"; + name = "bug-tv-npm-deps"; inherit src; hash = "sha256-pMxVG+OHiqjz1GHWDhhNOsrhoVPzVJ5kXhiRsP0Rflg="; }; in pkgs.stdenvNoCC.mkDerivation { - pname = "streamreverse"; + pname = "bug-tv"; version = "0.1.0"; inherit src npmDeps; @@ -111,7 +111,7 @@ enable = lib.mkEnableOption "the bug.tv multi-provider stream finder API"; package = lib.mkOption { - description = "The streamreverse package to run."; + description = "The bug-tv package to run."; type = lib.types.package; }; diff --git a/package-lock.json b/package-lock.json index a5374fd..d55279f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,11 +1,11 @@ { - "name": "streamreverse", + "name": "bug-tv", "version": "0.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "streamreverse", + "name": "bug-tv", "version": "0.1.0", "dependencies": { "cheerio-without-node-native": "^0.20.2", diff --git a/package.json b/package.json index d0caba3..edb5e57 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "streamreverse", + "name": "bug-tv", "version": "0.1.0", "private": true, "description": "Multi-provider stream finder with TMDB search and REST API", diff --git a/rest/openapi.js b/rest/openapi.js index c35a994..da143e3 100644 --- a/rest/openapi.js +++ b/rest/openapi.js @@ -1,5 +1,3 @@ -// 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', diff --git a/web/index.html b/web/index.html index 35365ca..2e38628 100644 --- a/web/index.html +++ b/web/index.html @@ -36,8 +36,9 @@ diff --git a/web/js/api.js b/web/js/api.js index 6c4081f..8901d9f 100644 --- a/web/js/api.js +++ b/web/js/api.js @@ -1,7 +1,21 @@ +/* + API — two upstreams: + 1. the bug-tv REST server (SR.config.getBase()): provider catalog, + /streams lookup, and the /proxy/media tunnel the player needs (CORS, + headers, m3u8 rewrite). + 2. TMDB, called straight from the browser (it is CORS-enabled): search, + trending, details, seasons, episodes. Each returns the same shape the + server's endpoints used to, so the pages are unchanged. + Keeping catalog traffic off the server means it only does provider scraping + and stream proxying. +*/ (function () { 'use strict'; + const TMDB_BASE = 'https://api.themoviedb.org/3'; + const TMDB_IMG = 'https://image.tmdb.org/t/p/'; + async function request(path, params) { const url = new URL(SR.config.getBase() + path); if (params) { @@ -28,6 +42,38 @@ return data; } + // Direct TMDB call. `key` overrides the configured key (Settings "Test"). + async function tmdb(pathname, params, key) { + const url = new URL(TMDB_BASE + pathname); + url.searchParams.set('api_key', key || SR.config.getTmdbKey()); + url.searchParams.set('language', 'en-US'); + if (params) { + for (const [k, v] of Object.entries(params)) { + if (v === undefined || v === null || v === '') continue; + url.searchParams.set(k, String(v)); + } + } + let res; + try { + res = await fetch(url.toString()); + } catch (e) { + throw new Error('cannot reach the TMDB API — check your connection or the TMDB API key in Settings'); + } + let data = null; + try { + data = await res.json(); + } catch (e) { + /* non-JSON body */ + } + if (!res.ok) { + if (res.status === 401) { + throw new Error('TMDB rejected the API key — check "TMDB API key" in Settings'); + } + throw new Error((data && (data.status_message || data.error)) || `TMDB error: HTTP ${res.status}`); + } + return data; + } + // route a media/subtitle URL through the API proxy (CORS, headers, m3u8 rewrite) function proxyUrl(target, headers) { const q = new URLSearchParams(); @@ -36,15 +82,87 @@ return SR.config.getBase() + '/proxy/media?' + q.toString(); } + // Same shapes the server endpoints used to return. + function searchResult(m) { + const isTv = m.media_type === 'tv'; + const date = isTv ? m.first_air_date : m.release_date; + return { + tmdb_id: m.id, + type: isTv ? 'tv' : 'movie', + name: m.title || m.name, + year: date ? String(date).slice(0, 4) : '', + poster: m.poster_path ? `${TMDB_IMG}w342${m.poster_path}` : '', + backdrop: m.backdrop_path ? `${TMDB_IMG}w1280${m.backdrop_path}` : '', + rating: m.vote_average ? Number(m.vote_average).toFixed(1) : '', + overview: m.overview || '', + }; + } + + function fullImage(pathname) { + return pathname ? `${TMDB_IMG}original${pathname}` : ''; + } + window.SR = window.SR || {}; SR.api = { request, + tmdb, proxyUrl, - search: (q) => request('/search', { q }), - trending: (type, timeWindow) => request('/trending', { type, window: timeWindow }), - details: (type, id) => request(`/${type}/${id}`), - seasons: (id) => request(`/tv/${id}/seasons`), - episodes: (id, season) => request(`/tv/${id}/seasons/${season}`), + + // — TMDB, straight from the browser — + search: (q) => + tmdb('/search/multi', { query: q, include_adult: 'false', page: 1 }).then((r) => { + const results = (r.results || []) + .filter((m) => m.media_type === 'movie' || m.media_type === 'tv') + .slice(0, 15) + .map(searchResult); + return { query: q, count: results.length, results }; + }), + trending: (type, timeWindow) => { + const t = ['movie', 'tv', 'all'].includes(type) ? type : 'all'; + const w = timeWindow === 'day' ? 'day' : 'week'; + return tmdb(`/trending/${t}/${w}`).then((r) => { + const results = (r.results || []).slice(0, 20).map(searchResult); + return { type: t, window: w, count: results.length, results }; + }); + }, + details: (type, id) => + tmdb(`/${type}/${id}`, { append_to_response: 'images' }).then((d) => { + d.poster_url = fullImage(d.poster_path); + d.backdrop_url = fullImage(d.backdrop_path); + if (d.images) { + for (const key of Object.keys(d.images)) { + d.images[key] = (d.images[key] || []).map((im) => ({ ...im, url: fullImage(im.file_path) })); + } + } + return d; + }), + seasons: (id) => + tmdb(`/tv/${id}`).then((r) => { + const seasons = (r.seasons || []) + .filter((s) => s.season_number > 0 && (s.episode_count || 0) > 0) + .map((s) => ({ + season_number: s.season_number, + name: s.name, + episode_count: s.episode_count, + air_date: s.air_date || '', + })); + return { tmdb_id: id, count: seasons.length, seasons }; + }), + episodes: (id, season) => + tmdb(`/tv/${id}/season/${season}`).then((r) => { + const episodes = (r.episodes || []) + .slice() + .sort((a, b) => a.episode_number - b.episode_number) + .map((e) => ({ + episode_number: e.episode_number, + name: e.name, + air_date: e.air_date || '', + still: e.still_path ? `${TMDB_IMG}w300${e.still_path}` : '', + })); + return { tmdb_id: id, season: parseInt(season, 10), count: episodes.length, episodes }; + }), + + // — bug-tv server (providers, streams, proxy) — streams: ({ tmdb, type, season, episode, providers }) => request('/streams', { tmdb, diff --git a/web/js/config.js b/web/js/config.js index 49f0ae2..393776a 100644 --- a/web/js/config.js +++ b/web/js/config.js @@ -9,13 +9,22 @@ Providers: until the user toggles anything in Settings, the server's own enabled list is used. Once customized, a {name: bool} map is stored in localStorage; names missing from the map default to enabled. + + TMDB key: search/trending/details/images go straight to TMDB from the + browser (the server only serves providers, streams and the media proxy). + The key resolves from localStorage (Settings) or the built-in default. */ (function () { 'use strict'; const DEFAULT_BASE = 'https://api-tv.bug.tools'; - const STORAGE_KEY = 'streamreverse.apiBase.v1'; - const PROVIDER_KEY = 'streamreverse.providers.v1'; + const STORAGE_KEY = 'bug-tv.apiBase.v1'; + const PROVIDER_KEY = 'bug-tv.providers.v1'; + + // Search/details/images go straight to TMDB from the browser; this key + // only ever reaches api.themoviedb.org. + const DEFAULT_TMDB_KEY = 'ac014b130a8e6344c91dff4e68b18d47'; + const TMDB_KEY_STORAGE_KEY = 'bug-tv.tmdbKey.v1'; function normalize(value) { let v = String(value || '').trim(); @@ -54,6 +63,34 @@ } } + function getTmdbKey() { + try { + const stored = localStorage.getItem(TMDB_KEY_STORAGE_KEY); + if (stored) return stored.trim(); + } catch (e) { + /* private mode etc. */ + } + return DEFAULT_TMDB_KEY; + } + + function setTmdbKey(value) { + const v = String(value || '').trim(); + try { + localStorage.setItem(TMDB_KEY_STORAGE_KEY, v); + } catch (e) { + /* ignore */ + } + return v; + } + + function clearTmdbKey() { + try { + localStorage.removeItem(TMDB_KEY_STORAGE_KEY); + } catch (e) { + /* ignore */ + } + } + // null → user has not customized; caller should fall back to server defaults function getProviders() { try { @@ -128,9 +165,14 @@ DEFAULT_BASE, STORAGE_KEY, PROVIDER_KEY, + DEFAULT_TMDB_KEY, + TMDB_KEY_STORAGE_KEY, getBase, setBase, clearBase, + getTmdbKey, + setTmdbKey, + clearTmdbKey, getProviders, setProviders, clearProviders, diff --git a/web/js/settings.js b/web/js/settings.js index 4c5ca36..80050be 100644 --- a/web/js/settings.js +++ b/web/js/settings.js @@ -1,5 +1,5 @@ /* - SETTINGS — source provider toggles, API server base URL, local data. + SETTINGS — source provider toggles, API server base URL, TMDB key, local data. */ (function () { 'use strict'; @@ -10,6 +10,8 @@ const providersStatus = document.getElementById('providersStatus'); const apiBaseInput = document.getElementById('apiBaseInput'); const connStatus = document.getElementById('connStatus'); + const tmdbKeyInput = document.getElementById('tmdbKeyInput'); + const tmdbStatus = document.getElementById('tmdbStatus'); const dataStatus = document.getElementById('dataStatus'); /* ---- nav --------------------------------------------------------------------- */ @@ -139,6 +141,36 @@ await testConnection(); }); + /* ---- TMDB key ------------------------------------------------------------------- */ + + tmdbKeyInput.value = SR.config.getTmdbKey(); + + async function testTmdbKey(key) { + setStatus(tmdbStatus, 'Testing TMDB key…'); + try { + await SR.api.tmdb('/search/movie', { query: 'The Matrix', page: 1 }, key); + setStatus(tmdbStatus, 'TMDB key works — search, details and images load directly from TMDB.', 'ok'); + } catch (e) { + setStatus(tmdbStatus, e.message, 'err'); + } + } + + document.getElementById('tmdbKeySave').addEventListener('click', async () => { + SR.config.setTmdbKey(tmdbKeyInput.value); + tmdbKeyInput.value = SR.config.getTmdbKey(); + await testTmdbKey(SR.config.getTmdbKey()); + }); + + document.getElementById('tmdbKeyTest').addEventListener('click', async () => { + await testTmdbKey(tmdbKeyInput.value.trim()); + }); + + document.getElementById('tmdbKeyReset').addEventListener('click', async () => { + SR.config.clearTmdbKey(); + tmdbKeyInput.value = SR.config.getTmdbKey(); + await testTmdbKey(SR.config.getTmdbKey()); + }); + /* ---- data ---------------------------------------------------------------------------- */ document.getElementById('clearHistory').addEventListener('click', () => { diff --git a/web/js/storage.js b/web/js/storage.js index 6286b28..e47e116 100644 --- a/web/js/storage.js +++ b/web/js/storage.js @@ -24,7 +24,7 @@ (function () { 'use strict'; - const KEY = 'streamreverse.resume.v1'; + const KEY = 'bug-tv.resume.v1'; const MAX_RECORDS = 200; function readAll() { diff --git a/web/js/watch.js b/web/js/watch.js index 8b5c2f0..3085110 100644 --- a/web/js/watch.js +++ b/web/js/watch.js @@ -714,7 +714,7 @@ b.type = 'button'; b.role = 'tab'; b.className = s.season_number === state.season ? 'chip chip--active' : 'chip'; - b.textContent = `S${s.season_number}`; + b.textContent = `Season ${s.season_number}`; b.title = `${s.name || `Season ${s.season_number}`} (${s.episode_count || 0})`; b.addEventListener('click', () => { if (s.season_number !== state.season) loadEpisodes(s.season_number, null); diff --git a/web/search.html b/web/search.html index 079953d..818ad7e 100644 --- a/web/search.html +++ b/web/search.html @@ -44,8 +44,9 @@ diff --git a/web/settings.html b/web/settings.html index 441fe4d..565d255 100644 --- a/web/settings.html +++ b/web/settings.html @@ -50,7 +50,7 @@

Server

-

The REST API that bug.tv queries. Change this only if you run your own instance.

+

The REST API that bug.tv uses for providers, stream lookups and the media proxy. Change this only if you run your own instance.

@@ -58,6 +58,15 @@

+

Search, details and images go straight to TMDB from your browser, using the key below — your server is not involved.

+ + +
+ + + +
+

@@ -75,8 +84,9 @@ diff --git a/web/watch.html b/web/watch.html index 4a8a3df..11929f7 100644 --- a/web/watch.html +++ b/web/watch.html @@ -39,7 +39,7 @@

@@ -116,8 +116,9 @@