/* 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) { for (const [key, value] of Object.entries(params)) { if (value === undefined || value === null || value === '') continue; url.searchParams.set(key, String(value)); } } let res; try { res = await fetch(url.toString()); } catch (e) { throw new Error(`cannot reach the API at ${SR.config.getBase()} — check the base URL in Settings`); } let data = null; try { data = await res.json(); } catch (e) { /* non-JSON body */ } if (!res.ok) { throw new Error((data && data.error) || `API error: HTTP ${res.status}`); } 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(); q.set('url', target); q.set('h', JSON.stringify(headers || {})); 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, // — 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, type, season, episode, providers: Array.isArray(providers) && providers.length ? providers.join(',') : '', }), providers: () => request('/providers'), }; })();