diff --git a/config.json b/config.json index e57cf7c..0ad6ebd 100644 --- a/config.json +++ b/config.json @@ -1,7 +1,9 @@ { "tmdbKey": "ac014b130a8e6344c91dff4e68b18d47", "enabledProviders": [ - "cineby" + "cineby", + "videasy", + "vidfast" ], "timeoutMs": 30000, "filters": { diff --git a/web/css/components.css b/web/css/components.css index 3987073..4debeac 100644 --- a/web/css/components.css +++ b/web/css/components.css @@ -676,6 +676,11 @@ select.field { color: rgba(255, 255, 255, 0.55); } +.player-sources__group--error { + color: var(--color-danger); + cursor: help; +} + .source-item { display: flex; align-items: center; diff --git a/web/js/config.js b/web/js/config.js index 875260d..fe4145e 100644 --- a/web/js/config.js +++ b/web/js/config.js @@ -82,16 +82,45 @@ } } + const PREFERRED_PROVIDERS = ['cineby', 'videasy', 'vidfast']; + // Merge the server's provider lists with the user's toggles. // Returns the enabled provider names, in the server's `all` order. function enabledProviders(allNames, serverEnabled) { const all = Array.isArray(allNames) ? allNames : []; const overrides = getProviders(); - if (!overrides) { - const base = Array.isArray(serverEnabled) && serverEnabled.length ? serverEnabled : all; - return all.filter((n) => base.includes(n)); + if (overrides) { + return Object.keys(overrides).filter((n) => overrides[n] !== false); } - return all.filter((n) => overrides[n] !== false); + const server = Array.isArray(serverEnabled) ? serverEnabled : []; + const wanted = new Set([...PREFERRED_PROVIDERS, ...server]); + return (all.length ? all : PREFERRED_PROVIDERS.slice()).filter((n) => + wanted.has(n) + ); + } + + // Watch-page resolver. Re-reads localStorage on every call so provider + // changes made in Settings apply to the next lookup without a reload. + function resolveProviders(catalog) { + const all = Array.isArray(catalog && catalog.all) ? catalog.all : []; + const serverEnabled = Array.isArray(catalog && catalog.enabled) ? catalog.enabled : []; + const overrides = getProviders(); + if (overrides) { + return { + names: Object.keys(overrides).filter((n) => overrides[n] !== false), + customized: true, + useServerDefaults: false, + }; + } + const wanted = new Set([...PREFERRED_PROVIDERS, ...serverEnabled]); + const names = all.length + ? all.filter((n) => wanted.has(n)) + : PREFERRED_PROVIDERS.slice(); + return { + names, + customized: false, + useServerDefaults: !names.length, + }; } window.SR = window.SR || {}; @@ -106,5 +135,6 @@ setProviders, clearProviders, enabledProviders, + resolveProviders, }; })(); diff --git a/web/js/settings.js b/web/js/settings.js index 5b44ae5..4c5ca36 100644 --- a/web/js/settings.js +++ b/web/js/settings.js @@ -54,8 +54,9 @@ providerList.replaceChildren(); boxes.clear(); const overrides = SR.config.getProviders(); + const enabled = SR.config.enabledProviders(allProviders, serverEnabled); for (const name of allProviders) { - const checked = overrides ? overrides[name] !== false : serverEnabled.includes(name); + const checked = enabled.includes(name); const label = document.createElement('label'); label.className = 'provider'; const input = document.createElement('input'); @@ -71,7 +72,7 @@ providerList.appendChild(label); } const on = boxes.size ? Array.from(boxes.values()).filter((b) => b.checked).length : 0; - const source = overrides ? 'your selection' : 'server defaults'; + const source = overrides ? 'your selection' : 'default sources'; let host; try { host = new URL(SR.config.getBase()).host; } catch (e) { host = SR.config.getBase(); } setStatus(providersStatus, `${on} of ${allProviders.length} enabled — ${source} · ${host}`); @@ -96,7 +97,7 @@ }); document.getElementById('providersReset').addEventListener('click', () => - resetProviders('Using server defaults')); + resetProviders('Using default sources')); async function initSources() { try { @@ -147,7 +148,7 @@ document.getElementById('resetProviders').addEventListener('click', () => { resetProviders(''); - setStatus(dataStatus, 'Source toggles reset to server defaults.'); + setStatus(dataStatus, 'Source toggles reset to default sources.'); }); initSources(); diff --git a/web/js/util.js b/web/js/util.js index 90f78ce..0eeba25 100644 --- a/web/js/util.js +++ b/web/js/util.js @@ -50,6 +50,21 @@ return url.replace(/\/t\/p\/[A-Za-z0-9._-]+/, `/t/p/${size}`); } + // True when the in-browser player can load the stream through the proxy. + function webPlayable(stream) { + const s = stream || {}; + const url = String(s.url || ''); + if (!/^https?:\/\//i.test(url)) return false; + const type = String(s.type || '').toLowerCase(); + if (['magnet', 'torrent', 'dash', 'mkv', 'avi'].includes(type)) return false; + const isHls = type === 'm3u8' || /\.m3u8($|\?)/i.test(url); + const isDirect = + type === 'mp4' || + type === 'webm' || + /\.(mp4|m4v|webm)(\?|#|$)/i.test(url); + return isHls || isDirect; + } + window.SR = window.SR || {}; - SR.util = { escapeHtml, getParam, formatSeconds, runtimeLabel, debounce, tmdbResize }; + SR.util = { escapeHtml, getParam, formatSeconds, runtimeLabel, debounce, tmdbResize, webPlayable }; })(); diff --git a/web/js/watch.js b/web/js/watch.js index 18f88ef..fab340c 100644 --- a/web/js/watch.js +++ b/web/js/watch.js @@ -50,6 +50,9 @@ seasons: [], episodes: [], streams: [], + unsupported: [], + providerErrors: {}, + loadErrors: {}, sourceIndex: -1, season: null, episode: null, @@ -380,21 +383,39 @@ return SR.storage.keyFor({ type, tmdbId: Number(id), season: state.season, episode: state.episode }); } - let providerNames = null; // cached per page load; null → ask server again + let providerCatalog = null; // cached per page load; enabled names are re-read from localStorage every lookup - async function getProviderNames() { - if (providerNames) return providerNames; + async function getProviderCatalog() { + if (providerCatalog) return providerCatalog; try { const data = await SR.api.providers(); - providerNames = SR.config.enabledProviders(data.all || [], data.enabled || []); + providerCatalog = { all: data.all || [], enabled: data.enabled || [] }; } catch (e) { - providerNames = null; // unreachable → let /streams use server defaults + return { all: [], enabled: [] }; } - return providerNames; + return providerCatalog; + } + + async function getProviderInfo() { + if (SR.config.getProviders()) return SR.config.resolveProviders(null); + return SR.config.resolveProviders(await getProviderCatalog()); } async function loadStreams() { - const query = { tmdb: id, type, providers: await getProviderNames() }; + const providerInfo = await getProviderInfo(); + if (!providerInfo.useServerDefaults && providerInfo.names.length === 0) { + closeSources(); + pcQuality.textContent = ''; + sourceLabel.textContent = ''; + resumeNote.textContent = ''; + showStatus('No sources are enabled. Open Settings → Sources and enable at least one provider.', true); + return; + } + const query = { + tmdb: id, + type, + providers: providerInfo.useServerDefaults ? null : providerInfo.names, + }; if (type === 'tv') { if (state.season == null || state.episode == null) return; query.season = state.season; @@ -407,11 +428,26 @@ resumeNote.textContent = ''; try { const data = await SR.api.streams(query); - state.streams = data.streams || []; + const allStreams = data.streams || []; + state.streams = allStreams.filter(SR.util.webPlayable); + state.unsupported = allStreams.filter((s) => !SR.util.webPlayable(s)); + state.providerErrors = data.providerErrors || {}; + state.loadErrors = data.loadErrors || {}; renderSources(); pcGear.hidden = state.streams.length === 0; if (state.streams.length) playSource(0); - else showStatus('No streams found for this title.', true); + else { + const failed = Object.keys({ ...state.loadErrors, ...state.providerErrors }); + const hidden = state.unsupported.length; + if (failed.length || hidden) { + const parts = ['No web-playable streams found.']; + if (failed.length) parts.push(`Provider failures: ${failed.join(', ')}.`); + if (hidden) parts.push(`Hidden ${hidden} unsupported source(s).`); + showStatus(parts.join(' '), true); + } else { + showStatus('No streams found for this title.', true); + } + } } catch (e) { SR.app.showError(e, 'stream lookup failed'); showStatus('Stream lookup failed.', true); @@ -454,8 +490,31 @@ b.addEventListener('click', () => playSource(i)); list.appendChild(b); } - document.getElementById('sourcesCount').textContent = - `${state.streams.length} source${state.streams.length === 1 ? '' : 's'}`; + const failures = { ...state.loadErrors, ...state.providerErrors }; + const failed = Object.keys(failures); + if (failed.length) { + const note = document.createElement('div'); + note.className = 'player-sources__group player-sources__group--error'; + note.textContent = `Failed: ${failed.join(', ')}`; + note.title = failed.map((name) => `${name}: ${failures[name] || 'no streams'}`).join('\n'); + list.appendChild(note); + } + if (state.unsupported.length) { + const types = [ + ...new Set(state.unsupported.map((s) => String(s.type || 'unknown').toLowerCase())), + ]; + const note = document.createElement('div'); + note.className = 'player-sources__group player-sources__group--error'; + note.textContent = `Hidden unsupported: ${state.unsupported.length} (${types.join(', ')})`; + note.title = state.unsupported + .map((s) => `${s.provider || 'Other'}: ${s.name || 'stream'} (${s.type || 'unknown'})`) + .join('\n'); + list.appendChild(note); + } + const count = `${state.streams.length} source${state.streams.length === 1 ? '' : 's'}`; + document.getElementById('sourcesCount').textContent = state.unsupported.length + ? `${count} · ${state.unsupported.length} hidden` + : count; } function playSource(i) { diff --git a/web/settings.html b/web/settings.html index 40a59ca..dc9b093 100644 --- a/web/settings.html +++ b/web/settings.html @@ -39,7 +39,7 @@
Choose which providers to search for streams. Saved in this browser; applies to new lookups.
+Choose which providers to search for streams. Saved in this browser; applies from the next stream lookup.