diff --git a/src/runners/individualRunner.ts b/src/runners/individualRunner.ts index ad3004b..6c7e452 100644 --- a/src/runners/individualRunner.ts +++ b/src/runners/individualRunner.ts @@ -6,7 +6,7 @@ import { EmbedOutput, SourcererOutput } from '@/providers/base'; import { ProviderList } from '@/providers/get'; import { ScrapeContext } from '@/utils/context'; import { NotFoundError } from '@/utils/errors'; -import { isValidStream } from '@/utils/valid'; +import { isValidStream, validatePlayableStreams } from '@/utils/valid'; export type IndividualSourceRunnerOptions = { features: FeatureMap; @@ -68,6 +68,13 @@ export async function scrapeInvidualSource( if ((!output.stream || output.stream.length === 0) && output.embeds.length === 0) throw new NotFoundError('No streams found'); + + // only check for playable streams if there are streams, and if there are no embeds + if (output.stream && output.stream.length > 0 && output.embeds.length === 0) { + const playableStreams = await validatePlayableStreams(output.stream, ops); + if (playableStreams.length === 0) throw new NotFoundError('No playable streams found'); + output.stream = playableStreams; + } return output; } @@ -105,5 +112,9 @@ export async function scrapeIndividualEmbed( .filter((stream) => flagsAllowedInFeatures(ops.features, stream.flags)); if (output.stream.length === 0) throw new NotFoundError('No streams found'); + const playableStreams = await validatePlayableStreams(output.stream, ops); + if (playableStreams.length === 0) throw new NotFoundError('No playable streams found'); + output.stream = playableStreams; + return output; } diff --git a/src/runners/runner.ts b/src/runners/runner.ts index c351174..c3bc9eb 100644 --- a/src/runners/runner.ts +++ b/src/runners/runner.ts @@ -8,7 +8,7 @@ import { Stream } from '@/providers/streams'; import { ScrapeContext } from '@/utils/context'; import { NotFoundError } from '@/utils/errors'; import { reorderOnIdList } from '@/utils/list'; -import { isValidStream } from '@/utils/valid'; +import { isValidStream, validatePlayableStream } from '@/utils/valid'; export type RunOutput = { sourceId: string; @@ -104,9 +104,11 @@ export async function runAllProviders(list: ProviderList, ops: ProviderRunnerOpt // return stream is there are any if (output.stream?.[0]) { + const playableStream = await validatePlayableStream(output.stream[0], ops); + if (!playableStream) throw new NotFoundError('No streams found'); return { sourceId: source.id, - stream: output.stream[0], + stream: playableStream, }; } @@ -149,6 +151,9 @@ export async function runAllProviders(list: ProviderList, ops: ProviderRunnerOpt if (embedOutput.stream.length === 0) { throw new NotFoundError('No streams found'); } + const playableStream = await validatePlayableStream(embedOutput.stream[0], ops); + if (!playableStream) throw new NotFoundError('No streams found'); + embedOutput.stream = [playableStream]; } catch (error) { const updateParams: UpdateEvent = { id: source.id, diff --git a/src/utils/valid.ts b/src/utils/valid.ts index 62347c3..65db1ea 100644 --- a/src/utils/valid.ts +++ b/src/utils/valid.ts @@ -1,4 +1,6 @@ import { Stream } from '@/providers/streams'; +import { IndividualEmbedRunnerOptions } from '@/runners/individualRunner'; +import { ProviderRunnerOptions } from '@/runners/runner'; export function isValidStream(stream: Stream | undefined): boolean { if (!stream) return false; @@ -15,3 +17,54 @@ export function isValidStream(stream: Stream | undefined): boolean { // unknown file type return false; } + +export async function validatePlayableStream( + stream: Stream, + ops: ProviderRunnerOptions | IndividualEmbedRunnerOptions, +): Promise { + const fetcher = stream.flags.length === 1 && stream.flags.includes('cors-allowed') ? ops.fetcher : ops.proxiedFetcher; + if (stream.type === 'hls') { + const headResult = await fetcher.full(stream.playlist, { + method: 'HEAD', + headers: { + ...stream.preferredHeaders, + ...stream.headers, + }, + }); + if (headResult.statusCode !== 200) return null; + return stream; + } + if (stream.type === 'file') { + const validQualitiesResults = await Promise.all( + Object.values(stream.qualities).map((quality) => + fetcher.full(quality.url, { + method: 'HEAD', + headers: { + ...stream.preferredHeaders, + ...stream.headers, + }, + }), + ), + ); + // remove invalid qualities from the stream + const validQualities = stream.qualities; + Object.keys(stream.qualities).forEach((quality, index) => { + if (validQualitiesResults[index].statusCode !== 200) { + delete validQualities[quality as keyof typeof stream.qualities]; + } + }); + + if (Object.keys(validQualities).length === 0) return null; + return { ...stream, qualities: validQualities }; + } + return null; +} + +export async function validatePlayableStreams( + streams: Stream[], + ops: ProviderRunnerOptions | IndividualEmbedRunnerOptions, +): Promise { + return (await Promise.all(streams.map((stream) => validatePlayableStream(stream, ops)))).filter( + (v) => v !== null, + ) as Stream[]; +}