diff --git a/src/providers/all.ts b/src/providers/all.ts index ebaaebf..f35c067 100644 --- a/src/providers/all.ts +++ b/src/providers/all.ts @@ -18,6 +18,7 @@ import { autoembedScraper } from '@/providers/sources/autoembed'; import { catflixScraper } from '@/providers/sources/catflix'; import { ee3Scraper } from '@/providers/sources/ee3'; import { flixhqScraper } from '@/providers/sources/flixhq/index'; +import { fsharetvScraper } from '@/providers/sources/fsharetv'; import { goMoviesScraper } from '@/providers/sources/gomovies/index'; import { insertunitScraper } from '@/providers/sources/insertunit'; import { kissAsianScraper } from '@/providers/sources/kissasian/index'; @@ -96,6 +97,7 @@ export function gatherAllSources(): Array { tugaflixScraper, ee3Scraper, whvxScraper, + fsharetvScraper, ]; } diff --git a/src/providers/sources/fsharetv.ts b/src/providers/sources/fsharetv.ts new file mode 100644 index 0000000..7043396 --- /dev/null +++ b/src/providers/sources/fsharetv.ts @@ -0,0 +1,90 @@ +import { load } from 'cheerio'; + +import { SourcererOutput, makeSourcerer } from '@/providers/base'; +import { FileBasedStream } from '@/providers/streams'; +import { compareMedia } from '@/utils/compare'; +import { MovieScrapeContext, ShowScrapeContext } from '@/utils/context'; +import { NotFoundError } from '@/utils/errors'; +import { getValidQualityFromString } from '@/utils/quality'; + +const baseUrl = 'https://fsharetv.co'; + +async function comboScraper(ctx: ShowScrapeContext | MovieScrapeContext): Promise { + const searchPage = await ctx.proxiedFetcher('/search', { + baseUrl, + query: { + q: ctx.media.title, + }, + }); + + const search$ = load(searchPage); + const searchResults: { title: string; year?: number; url: string }[] = []; + + search$('.movie-item').each((_, element) => { + const [, title, year] = + search$(element) + .find('b') + .text() + ?.match(/^(.*?)\s*(?:\(?\s*(\d{4})(?:\s*-\s*\d{0,4})?\s*\)?)?\s*$/) || []; + const url = search$(element).find('a').attr('href'); + if (!title || !url) return; + + searchResults.push({ title, year: Number(year) ?? undefined, url }); + }); + + const watchPageUrl = searchResults.find((x) => x && compareMedia(ctx.media, x.title, x.year))?.url; + if (!watchPageUrl) throw new NotFoundError('No watchable item found'); + + const watchPage = await ctx.proxiedFetcher(watchPageUrl.replace('/movie', '/w'), { baseUrl }); + + const fileId = watchPage.match(/Movie\.setSource\('([^']*)'/)?.[1]; + if (!fileId) throw new Error('File ID not found'); + + const apiRes: { data: { file: { sources: { src: string; quality: string | number }[] } } } = await ctx.proxiedFetcher( + `/api/file/${fileId}/source`, + { + baseUrl, + query: { + type: 'watch', + }, + }, + ); + if (!apiRes.data.file.sources.length) throw new Error('No sources found'); + + const qualities = apiRes.data.file.sources.reduce( + (acc, source) => { + const quality = typeof source.quality === 'number' ? source.quality.toString() : source.quality; + const validQuality = getValidQualityFromString(quality); + acc[validQuality] = { + type: 'mp4', + url: `${baseUrl}${source.src}`, + }; + return acc; + }, + {} as FileBasedStream['qualities'], + ); + + return { + embeds: [], + stream: [ + { + id: 'primary', + type: 'file', + flags: [], + headers: { + referer: 'https://fsharetv.co', + }, + qualities, + captions: [], + }, + ], + }; +} + +export const fsharetvScraper = makeSourcerer({ + id: 'fsharetv', + name: 'FshareTV', + rank: 93, + flags: [], + scrapeMovie: comboScraper, +});