From 1b8cb7f6e9c9709076092ebbf240de1f73f7c6ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=81nis=20L?= Date: Thu, 23 Jul 2026 14:23:47 +0300 Subject: [PATCH] add bcine --- manifest.json | 24 ++++++ providers/bcine.js | 191 +++++++++++++++++++++++++++++++++++++++++ src/bcine/extractor.js | 161 ++++++++++++++++++++++++++++++++++ src/bcine/http.js | 39 +++++++++ src/bcine/index.js | 28 ++++++ 5 files changed, 443 insertions(+) create mode 100644 providers/bcine.js create mode 100644 src/bcine/extractor.js create mode 100644 src/bcine/http.js create mode 100644 src/bcine/index.js diff --git a/manifest.json b/manifest.json index 5a76ffd..1c35520 100644 --- a/manifest.json +++ b/manifest.json @@ -2,6 +2,30 @@ "name": "Nuvio Providers", "version": "1.0.12", "scrapers": [ + { + "id": "bcine", + "name": "bCine", + "description": "Movies and TV streams from bCine.", + "version": "1.0.0", + "author": "Jānis L", + "supportedTypes": [ + "movie", + "tv" + ], + "filename": "providers/bcine.js", + "enabled": true, + "hasSettings": false, + "formats": [ + "m3u8" + ], + "logo": "https://bcine.ru/bcineicon.png", + "contentLanguage": [ + "en" + ], + "limited": false, + "disabledPlatforms": [], + "supportsExternalPlayer": true + }, { "id": "cineby", "name": "Cineby", diff --git a/providers/bcine.js b/providers/bcine.js new file mode 100644 index 0000000..cdc0d11 --- /dev/null +++ b/providers/bcine.js @@ -0,0 +1,191 @@ +// bcine - bundled from src/bcine/ +var __defProp = Object.defineProperty; +var __defProps = Object.defineProperties; +var __getOwnPropDescs = Object.getOwnPropertyDescriptors; +var __getOwnPropSymbols = Object.getOwnPropertySymbols; +var __hasOwnProp = Object.prototype.hasOwnProperty; +var __propIsEnum = Object.prototype.propertyIsEnumerable; +var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; +var __spreadValues = (a, b) => { + for (var prop in b || (b = {})) + if (__hasOwnProp.call(b, prop)) + __defNormalProp(a, prop, b[prop]); + if (__getOwnPropSymbols) + for (var prop of __getOwnPropSymbols(b)) { + if (__propIsEnum.call(b, prop)) + __defNormalProp(a, prop, b[prop]); + } + return a; +}; +var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b)); + +// src/bcine/http.js +var SITE_URL = "https://bcine.ru"; +var EMBED_URL = "https://1embed.cc"; +var HEADERS = { + "Accept": "*/*", + "Origin": EMBED_URL, + "Referer": `${SITE_URL}/`, + "User-Agent": "Mozilla/5.0 (Linux; Android 15) AppleWebKit/537.36 Chrome/140.0.0.0 Mobile Safari/537.36" +}; +function mergeHeaders(headers) { + return __spreadValues(__spreadValues({}, HEADERS), headers || {}); +} +async function fetchText(url, options = {}) { + const response = await fetch(url, __spreadProps(__spreadValues({}, options), { + headers: mergeHeaders(options.headers) + })); + if (!response.ok) { + const error = new Error(`HTTP ${response.status} for ${url}`); + error.status = response.status; + throw error; + } + return await response.text(); +} +async function fetchJson(url, options = {}) { + return JSON.parse(await fetchText(url, options)); +} + +// src/bcine/extractor.js +var BCINE_PROVIDER = "xpa"; +var CINEMETA_URL = "https://v3-cinemeta.strem.io/meta"; +var TMDB_ID_CACHE = {}; +function isPositiveInteger(value) { + return Number.isInteger(Number(value)) && Number(value) > 0; +} +async function resolveTmdbId(value, mediaType) { + const id = String(value || "").trim(); + if (/^\d+$/.test(id)) + return id; + if (!/^tt\d+$/i.test(id)) + return null; + const cacheKey = `${mediaType}:${id.toLowerCase()}`; + if (TMDB_ID_CACHE[cacheKey]) + return TMDB_ID_CACHE[cacheKey]; + const catalogType = mediaType === "movie" ? "movie" : "series"; + const metadata = await fetchJson( + `${CINEMETA_URL}/${catalogType}/${encodeURIComponent(id)}.json`, + { headers: { "Accept": "application/json" } } + ); + const resolved = metadata && metadata.meta && metadata.meta.moviedb_id; + if (!resolved || !/^\d+$/.test(String(resolved))) { + return null; + } + TMDB_ID_CACHE[cacheKey] = String(resolved); + return TMDB_ID_CACHE[cacheKey]; +} +function getEmbedUrl(tmdbId, mediaType, season, episode) { + const path = mediaType === "movie" ? `/embed/movie/${tmdbId}` : `/embed/tv/${tmdbId}/${season}/${episode}`; + return `${EMBED_URL}${path}?sr=buke&autoplay=1`; +} +function getQuality(server) { + const value = `${server && server.title || ""} ${server && server.quality || ""}`; + const match = value.match(/(2160|1440|1080|720|480|360)p?/i); + return match ? `${match[1]}p` : /4k/i.test(value) ? "4K" : "Auto"; +} +function getAbsoluteUrl(value) { + const url = String(value || "").trim(); + if (!url) + return ""; + if (/^https?:\/\//i.test(url)) + return url; + if (url.charAt(0) === "/") + return `${EMBED_URL}${url}`; + return ""; +} +async function getSessionToken(referer) { + const response = await fetchJson(`${EMBED_URL}/api/token`, { + headers: { + "Accept": "application/json", + "Referer": referer + } + }); + if (!response || typeof response.token !== "string" || !response.token) { + throw new Error("bCine player did not return a session token"); + } + return response.token; +} +async function getSourcePayload(tmdbId, mediaType, season, episode) { + const referer = getEmbedUrl(tmdbId, mediaType, season, episode); + const token = await getSessionToken(referer); + const request = { + type: mediaType, + id: String(tmdbId), + provider: BCINE_PROVIDER + }; + if (mediaType === "tv") { + request.season = Number(season); + request.episode = Number(episode); + } + const payload = await fetchJson(`${EMBED_URL}/api/sources`, { + method: "POST", + headers: { + "Accept": "application/json", + "Content-Type": "application/json", + "Referer": referer, + "x-bcine-key": token + }, + body: JSON.stringify(request) + }); + return { payload, referer }; +} +function normalizeStreams(payload, referer) { + const streams = []; + const seenUrls = {}; + const servers = payload && Array.isArray(payload.servers) ? payload.servers : []; + for (const server of servers) { + const url = getAbsoluteUrl(server && server.url); + if (!url || seenUrls[url] || !/\.m3u8|\/api\/proxy/i.test(url)) { + continue; + } + seenUrls[url] = true; + const quality = getQuality(server); + const label = String(server.title || server.name || "HLS").trim(); + streams.push({ + name: "bCine", + title: `bCine \xB7 ${label}`, + url, + quality, + headers: { + "Origin": EMBED_URL, + "Referer": referer, + "User-Agent": HEADERS["User-Agent"] + } + }); + } + return streams; +} +async function extractStreams(tmdbId, mediaType, season, episode) { + if (mediaType !== "movie" && mediaType !== "tv") { + return []; + } + if (mediaType === "tv" && (!isPositiveInteger(season) || !isPositiveInteger(episode))) { + return []; + } + const resolvedTmdbId = await resolveTmdbId(tmdbId, mediaType); + if (!resolvedTmdbId) { + return []; + } + const { payload, referer } = await getSourcePayload( + resolvedTmdbId, + mediaType, + Number(season), + Number(episode) + ); + return normalizeStreams(payload, referer); +} + +// src/bcine/index.js +async function getStreams(tmdbId, mediaType, season, episode) { + try { + const episodeLabel = mediaType === "tv" ? ` S${season}E${episode}` : ""; + console.log(`[bCine] Request: ${mediaType} ${tmdbId}${episodeLabel}`); + const streams = await extractStreams(tmdbId, mediaType, season, episode); + console.log(`[bCine] Found ${streams.length} stream(s)`); + return streams; + } catch (error) { + console.error(`[bCine] Error: ${error.message}`); + return []; + } +} +module.exports = { getStreams }; diff --git a/src/bcine/extractor.js b/src/bcine/extractor.js new file mode 100644 index 0000000..c746ca7 --- /dev/null +++ b/src/bcine/extractor.js @@ -0,0 +1,161 @@ +/** Resolve Nuvio TMDB requests through bCine's active 1Embed player source. */ + +import { EMBED_URL, fetchJson, HEADERS } from './http.js'; + +// bCine's embedded player uses `sr=buke`, which maps to 1Embed's XPA source. +const BCINE_PROVIDER = 'xpa'; +const CINEMETA_URL = 'https://v3-cinemeta.strem.io/meta'; +const TMDB_ID_CACHE = {}; + +function isPositiveInteger(value) { + return Number.isInteger(Number(value)) && Number(value) > 0; +} + +/** Resolve IMDb identifiers because bCine's source endpoint accepts TMDB IDs. */ +async function resolveTmdbId(value, mediaType) { + const id = String(value || '').trim(); + + if (/^\d+$/.test(id)) return id; + if (!/^tt\d+$/i.test(id)) return null; + + const cacheKey = `${mediaType}:${id.toLowerCase()}`; + if (TMDB_ID_CACHE[cacheKey]) return TMDB_ID_CACHE[cacheKey]; + + const catalogType = mediaType === 'movie' ? 'movie' : 'series'; + const metadata = await fetchJson( + `${CINEMETA_URL}/${catalogType}/${encodeURIComponent(id)}.json`, + { headers: { 'Accept': 'application/json' } } + ); + const resolved = metadata && metadata.meta && metadata.meta.moviedb_id; + + if (!resolved || !/^\d+$/.test(String(resolved))) { + return null; + } + + TMDB_ID_CACHE[cacheKey] = String(resolved); + return TMDB_ID_CACHE[cacheKey]; +} + +function getEmbedUrl(tmdbId, mediaType, season, episode) { + const path = mediaType === 'movie' + ? `/embed/movie/${tmdbId}` + : `/embed/tv/${tmdbId}/${season}/${episode}`; + + return `${EMBED_URL}${path}?sr=buke&autoplay=1`; +} + +function getQuality(server) { + const value = `${server && server.title || ''} ${server && server.quality || ''}`; + const match = value.match(/(2160|1440|1080|720|480|360)p?/i); + + return match ? `${match[1]}p` : /4k/i.test(value) ? '4K' : 'Auto'; +} + +function getAbsoluteUrl(value) { + const url = String(value || '').trim(); + + if (!url) return ''; + if (/^https?:\/\//i.test(url)) return url; + if (url.charAt(0) === '/') return `${EMBED_URL}${url}`; + + return ''; +} + +async function getSessionToken(referer) { + const response = await fetchJson(`${EMBED_URL}/api/token`, { + headers: { + 'Accept': 'application/json', + 'Referer': referer + } + }); + + if (!response || typeof response.token !== 'string' || !response.token) { + throw new Error('bCine player did not return a session token'); + } + + return response.token; +} + +async function getSourcePayload(tmdbId, mediaType, season, episode) { + const referer = getEmbedUrl(tmdbId, mediaType, season, episode); + const token = await getSessionToken(referer); + const request = { + type: mediaType, + id: String(tmdbId), + provider: BCINE_PROVIDER + }; + + if (mediaType === 'tv') { + request.season = Number(season); + request.episode = Number(episode); + } + + const payload = await fetchJson(`${EMBED_URL}/api/sources`, { + method: 'POST', + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json', + 'Referer': referer, + 'x-bcine-key': token + }, + body: JSON.stringify(request) + }); + + return { payload, referer }; +} + +function normalizeStreams(payload, referer) { + const streams = []; + const seenUrls = {}; + const servers = payload && Array.isArray(payload.servers) ? payload.servers : []; + + for (const server of servers) { + const url = getAbsoluteUrl(server && server.url); + + if (!url || seenUrls[url] || !/\.m3u8|\/api\/proxy/i.test(url)) { + continue; + } + + seenUrls[url] = true; + const quality = getQuality(server); + const label = String(server.title || server.name || 'HLS').trim(); + + streams.push({ + name: 'bCine', + title: `bCine · ${label}`, + url, + quality, + headers: { + 'Origin': EMBED_URL, + 'Referer': referer, + 'User-Agent': HEADERS['User-Agent'] + } + }); + } + + return streams; +} + +export async function extractStreams(tmdbId, mediaType, season, episode) { + if (mediaType !== 'movie' && mediaType !== 'tv') { + return []; + } + + if (mediaType === 'tv' && (!isPositiveInteger(season) || !isPositiveInteger(episode))) { + return []; + } + + const resolvedTmdbId = await resolveTmdbId(tmdbId, mediaType); + if (!resolvedTmdbId) { + return []; + } + + const { payload, referer } = await getSourcePayload( + resolvedTmdbId, + mediaType, + Number(season), + Number(episode) + ); + + return normalizeStreams(payload, referer); +} diff --git a/src/bcine/http.js b/src/bcine/http.js new file mode 100644 index 0000000..bd8a156 --- /dev/null +++ b/src/bcine/http.js @@ -0,0 +1,39 @@ +/** HTTP helpers for bCine's 1Embed-backed player API. */ + +export const SITE_URL = 'https://bcine.ru'; +export const EMBED_URL = 'https://1embed.cc'; + +export const HEADERS = { + 'Accept': '*/*', + 'Origin': EMBED_URL, + 'Referer': `${SITE_URL}/`, + 'User-Agent': 'Mozilla/5.0 (Linux; Android 15) AppleWebKit/537.36 Chrome/140.0.0.0 Mobile Safari/537.36' +}; + +function mergeHeaders(headers) { + return { + ...HEADERS, + ...(headers || {}) + }; +} + +/** Fetch text and preserve HTTP status codes for callers that need retries. */ +export async function fetchText(url, options = {}) { + const response = await fetch(url, { + ...options, + headers: mergeHeaders(options.headers) + }); + + if (!response.ok) { + const error = new Error(`HTTP ${response.status} for ${url}`); + error.status = response.status; + throw error; + } + + return await response.text(); +} + +/** Fetch and parse a JSON response. */ +export async function fetchJson(url, options = {}) { + return JSON.parse(await fetchText(url, options)); +} diff --git a/src/bcine/index.js b/src/bcine/index.js new file mode 100644 index 0000000..04133ab --- /dev/null +++ b/src/bcine/index.js @@ -0,0 +1,28 @@ +/** bCine provider entry point. */ + +import { extractStreams } from './extractor.js'; + +/** + * Resolve a Nuvio media request to bCine's current HLS source API. + * + * @param {string} tmdbId TMDB numeric ID + * @param {string} mediaType 'movie' or 'tv' + * @param {number} season TV season number + * @param {number} episode TV episode number + * @returns {Promise} + */ +async function getStreams(tmdbId, mediaType, season, episode) { + try { + const episodeLabel = mediaType === 'tv' ? ` S${season}E${episode}` : ''; + console.log(`[bCine] Request: ${mediaType} ${tmdbId}${episodeLabel}`); + + const streams = await extractStreams(tmdbId, mediaType, season, episode); + console.log(`[bCine] Found ${streams.length} stream(s)`); + return streams; + } catch (error) { + console.error(`[bCine] Error: ${error.message}`); + return []; + } +} + +module.exports = { getStreams };