Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

<img src="https://repository-images.githubusercontent.com/1138947882/af901757-a06b-442d-8976-c485fcafc230"></img>

### _Support CinePro's Development by starring this repo!_ ⭐

**OMSS-compliant streaming backend powering the CinePro ecosystem.**
Built with [@omss/framework](https://www.npmjs.com/package/@omss/framework) for extensible, type-safe media scraping.

Expand All @@ -17,7 +19,16 @@ Built with [@omss/framework](https://www.npmjs.com/package/@omss/framework) for

---

CinePro Core is the central scraping and streaming engine of the CinePro ecosystem. It exposes an [OMSS-compliant](https://github.com/omss-spec/omss-spec) HTTP API for resolving movie and TV show stream sources from multiple providers, with Redis caching and full Docker support. **Get up to 30+ unique sources for a single movie/tv show!**
CinePro Core is the central scraping and streaming engine of the CinePro ecosystem. It exposes an [OMSS-compliant](https://github.com/omss-spec/omss-spec) HTTP API for resolving movie and TV show stream sources from multiple providers, with Redis caching and full Docker support. **Get up to 100+ unique sources for a single movie/tv show!**

<details><summary>Proof!</summary>
<p>
With failing providers still got 88 sources. If the other providers worked (which they would I not be connected to a VPN) we would have gotten more then 100 sources!

![proof](docs/images/img.png)

</p>
</details>

> [!CAUTION]
> CinePro Core is designed for **personal and home use only.**
Expand Down
Binary file added docs/images/img.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
280 changes: 143 additions & 137 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"clean": "npx rimraf dist"
},
"dependencies": {
"@omss/framework": "^1.1.14",
"@omss/framework": "^1.1.15",
"cheerio": "^1.2.0",
"crypto-js": "^4.2.0",
"dotenv": "^16.4.5"
Expand Down
2 changes: 1 addition & 1 deletion src/providers/fmovies4u/fmovies4u.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ export class Fmovies4U extends BaseProvider {
}
),
type,
quality: stream.quality || 'HD',
quality: stream.quality || 'Auto',
provider: {
id: this.id,
name: this.name
Expand Down
262 changes: 262 additions & 0 deletions src/providers/streammafia/streammafia.ts
Comment thread
An0n-00 marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,262 @@
import type {
ProviderCapabilities,
ProviderMediaObject,
ProviderResult,
Subtitle,
AudioTrack,
Diagnostic,
Source,
SourceType
} from '@omss/framework';
import { BaseProvider } from '@omss/framework';
import axios from 'axios';
import { ApiResponse } from './streammafia.types.js';

export class StreamMafiaProvider extends BaseProvider {
readonly id = 'streammafia';
readonly name = 'MafiaEmbed';
readonly enabled = true;

readonly BASE_URL = 'https://streammafiacdn.in';
readonly EMBED_URL = 'https://embed.streammafia.to';

readonly HEADERS = {
'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/150 Safari/537.36',
Accept: 'application/json, text/javascript, */*; q=0.01',
'Accept-Language': 'en-US,en;q=0.9',
Referer: this.BASE_URL,
Origin: this.BASE_URL
};

readonly capabilities: ProviderCapabilities = {
supportedContentTypes: ['movies', 'tv']
};

async getMovieSources(media: ProviderMediaObject): Promise<ProviderResult> {
return this.getSources(media);
}

async getTVSources(media: ProviderMediaObject): Promise<ProviderResult> {
return this.getSources(media);
}

async healthCheck(): Promise<boolean> {
try {
const res = await axios.head(this.EMBED_URL, {
timeout: 5000
});
return res.status === 200;
} catch {
return false;
}
}

private async getSources(
media: ProviderMediaObject
): Promise<ProviderResult> {
try {
const url = this.buildPageUrl(media);
const api = await this.fetchPage(url);

if (!api || api.status !== 'success') {
return this.emptyResult('Invalid API response');
}

return await this.mapApiResponse(api);
} catch (err) {
return this.emptyResult(
err instanceof Error ? err.message : 'Unknown error'
);
}
}

private buildPageUrl(media: ProviderMediaObject): string {
if (media.type === 'movie') {
return `${this.BASE_URL}/goal/?movie_tmdb_id=${media.tmdbId}`;
}

return `${this.BASE_URL}/goal/?tv_tmdb_id=${media.tmdbId}&season=${media.s}&episode=${media.e}`;
}

private async fetchPage(url: string): Promise<ApiResponse | null> {
try {
const res = await axios.get(url, { headers: this.HEADERS });
return typeof res.data === 'string'
? JSON.parse(res.data)
: res.data;
} catch {
return null;
}
}

private async mapApiResponse(api: ApiResponse): Promise<ProviderResult> {
const sources: Source[] = [];
const subtitles: Subtitle[] = [];
const diagnostics: Diagnostic[] = [];

for (const file of api.data ?? []) {
const audioTracks = [this.extractAudioFromTitle(file.title)];

if (file.stream?.hls_streaming) {
const parsed = await this.parseHLS(file.stream.hls_streaming);

sources.push({
url: this.createProxyUrl(file.stream.hls_streaming, {
...this.HEADERS,
Referer: this.EMBED_URL,
Origin: this.EMBED_URL
}),
type: 'hls',
quality: parsed.quality,
audioTracks:
parsed.audioTracks.length > 0
? parsed.audioTracks
: audioTracks,
provider: {
id: this.id,
name: this.name
}
});
}

for (const download of file.stream?.download ?? []) {
sources.push({
url: this.createProxyUrl(download.url, {
...this.HEADERS,
Referer: this.EMBED_URL,
Origin: this.EMBED_URL
}),
type: this.inferSourceType(download.url),
quality: this.normalizeQuality(download.quality, 'auto'),
audioTracks,
provider: {
id: this.id,
name: this.name
}
});
}
}

return { sources, subtitles, diagnostics };
}

private extractAudioFromTitle(title: string): AudioTrack {
const match = title.match(/\(([^)]+)\)$/);
let lang = match?.[1]?.toLowerCase() ?? 'unknown';

if (lang === 'default') lang = 'en';
if (lang === 'original') lang = 'unknown';

return {
language: lang,
label: lang.toUpperCase()
};
}

private async parseHLS(url: string): Promise<{
quality: string;
audioTracks: AudioTrack[];
}> {
try {
const res = await axios.get(url, {
headers: {
...this.HEADERS,
Referer: this.EMBED_URL
}
});

const content: string = res.data;

const variants = this.parseVariants(content);
const audioTracks = this.parseAudioTracks(content);

if (variants.length === 0) {
return { quality: 'auto', audioTracks };
}

const best = variants.reduce((a, b) =>
b.resolution > a.resolution ? b : a
);

return {
quality: `${best.resolution}p`,
audioTracks
};
} catch {
return { quality: 'auto', audioTracks: [] };
}
}

private parseVariants(content: string): Array<{ resolution: number }> {
const variants: Array<{ resolution: number }> = [];

const regex = /RESOLUTION=\d+x(\d+)[^\n]*\n([^\n]+)/g;

let match;
while ((match = regex.exec(content)) !== null) {
variants.push({
resolution: parseInt(match[1], 10)
});
}

return variants;
}

private parseAudioTracks(content: string): AudioTrack[] {
const tracks: AudioTrack[] = [];
const lines = content.split('\n');

for (const line of lines) {
if (!line.includes('TYPE=AUDIO')) continue;

const lang = line.match(/LANGUAGE="([^"]+)"/)?.[1] ?? 'unknown';
const name = line.match(/NAME="([^"]+)"/)?.[1] ?? lang;

tracks.push({
language: lang,
label: name
});
}

return tracks;
}

private inferSourceType(url: string): SourceType {
const clean = url.toLowerCase().split('?')[0];

if (clean.endsWith('.m3u8')) return 'hls';
if (clean.endsWith('.mp4')) return 'mp4';
return 'hls';
}

private normalizeQuality(value?: string, fallback = 'unknown'): string {
if (!value) return fallback;

const v = value.toLowerCase();

if (v.includes('2160')) return '2160p';
if (v.includes('1080')) return '1080p';
if (v.includes('720')) return '720p';
if (v.includes('480')) return '480p';
if (v.includes('360')) return '360p';
if (v.includes('240')) return '240p';

return value.toLowerCase() + 'p';
}

private emptyResult(message: string): ProviderResult {
return {
sources: [],
subtitles: [],
diagnostics: [
{
code: 'PROVIDER_ERROR',
message: `${this.name}: ${message}`,
field: '',
severity: 'error'
}
]
};
}
}
55 changes: 55 additions & 0 deletions src/providers/streammafia/streammafia.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
export interface ApiResponse {
status: string;
data: File[];
}

export interface File {
id: number;
main_id: string;
secondary_id: string;
content_type: string;
tmdb_id: number;
title: string;
thumbnail: string;
embed_url: string;
season: any;
episode: any;
uploaded_at: string;
created_at: string;
stream: Stream;
audio_info: AudioInfo;
}

export interface Stream {
hls_streaming: string;
duration: string;
thumbnail_small: string;
thumbnail_medium: string;
download: Download[];
preview_video: PreviewVideo[];
}

export interface Download {
quality: string;
url: string;
}

export interface PreviewVideo {
url: string;
frequency: number;
height: number;
width: number;
count: number;
tileWidth: number;
tileHeight: number;
}

export interface AudioInfo {
type: string;
tracks: Track[];
}

export interface Track {
language: string;
file_code: string;
}
2 changes: 1 addition & 1 deletion src/providers/uembed/uembed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -282,7 +282,7 @@ export class UembedProvider extends BaseProvider {
'360p': 3,
'240p': 2,
HD: 2,
Unknown: 1
Auto: 1
};
return priorities[quality] || 1;
}
Expand Down
2 changes: 1 addition & 1 deletion src/providers/vidrock/vidrock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ export class VidRockProvider extends BaseProvider {
referrer: this.BASE_URL,
origin: this.BASE_URL.replace('net/', 'net')
}
: { ...this.HEADERS, Referer: pageUrl }
: { ...this.HEADERS, Referer: this.BASE_URL }
);
}

Expand Down
2 changes: 1 addition & 1 deletion src/providers/vidsrc/vidsrc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ export class VidSrcProvider extends BaseProvider {
Origin: 'https://cloudnestra.com' // Set Origin header to second URL's origin
}),
type: 'hls', // m3u8 = HLS streaming
quality: `up to HD`, // VidSrc does not provide explicit quality labels, so we use a generic one
quality: `Auto`, // VidSrc does not provide explicit quality labels, so we use a generic one
audioTracks: [
{
label: 'English',
Expand Down
3 changes: 2 additions & 1 deletion src/streamPatterns.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,6 @@ export const streamPatterns: RegExp[] = [
/wasabisys.com/,
/hakunaymatata.com/,
/streamflixserver.site/,
/tripplestream.online/
/tripplestream.online/,
/streamflixserver.site/
];
Loading