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
6 changes: 1 addition & 5 deletions src/providers/02moviedownloader/02moviedownloader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ export class MovieDownloader extends BaseProvider {
: this.HEADERS
),
type: 'mp4',
quality: download.resolution.toString() + 'p',
quality: download.resolution.toString(),
audioTracks: [
{
language: 'eng',
Expand Down Expand Up @@ -285,10 +285,6 @@ export class MovieDownloader extends BaseProvider {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}

this.console.log(
`Fetched data for ${media.title} from ${url}. returned ${response.status}`
);
return await response.json();
} catch (error) {
throw new Error(
Expand Down
2 changes: 1 addition & 1 deletion src/providers/rgshows/rgshows.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ export class RgShowsProvider extends BaseProvider {
diagnostics: [
{
code: 'PROVIDER_ERROR',
message: `${this.name}: ${message}`,
message: `${this.name}: ${message}. As of the last CinePro update, RgShows is experiencing issues. This may be due to changes on their end or temporary server problems. CinePro will continue to monitor the situation and update the provider as needed.`,
field: '',
severity: 'error'
}
Expand Down
18 changes: 9 additions & 9 deletions src/providers/streammafia/streammafia.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ export class StreamMafiaProvider extends BaseProvider {
diagnostics.push({
code: 'LANGUAGE_INFERRED',
message: `${this.name}: Audio language inferred from selected payload`,
field: 'selected.lang',
field: '',
severity: 'info'
});
}
Expand All @@ -149,7 +149,7 @@ export class StreamMafiaProvider extends BaseProvider {
diagnostics.push({
code: 'PROVIDER_ERROR',
message: `${this.name}: No playable sources found in API response`,
field: 'stream',
field: '',
severity: 'error'
});
}
Expand Down Expand Up @@ -260,20 +260,20 @@ export class StreamMafiaProvider extends BaseProvider {
if (clean.endsWith('.mkv')) return 'mkv';
if (clean.endsWith('.webm')) return 'webm';

return 'http';
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';
if (v.includes('2160')) return '2160';
if (v.includes('1080')) return '1080';
if (v.includes('720')) return '720';
if (v.includes('480')) return '480';
if (v.includes('360')) return '360';
if (v.includes('240')) return '240';

return value;
}
Expand Down
155 changes: 105 additions & 50 deletions src/providers/vidzee/decrypt.ts
Original file line number Diff line number Diff line change
@@ -1,56 +1,111 @@
import type { StreamUrl } from './vidzee.types.js';
export async function decrypt(
encryptedData: string,
decryptionKey: string
): Promise<string> {
try {
if (!encryptedData || !decryptionKey) return '';

// Step 1: decode outer base64
const decoded = atob(encryptedData);
const [ivBase64, cipherBase64] = decoded.split(':');

if (!ivBase64 || !cipherBase64) return '';

// Step 2: decode IV and ciphertext
const iv = Uint8Array.from(atob(ivBase64), (c) => c.charCodeAt(0));
const cipherBytes = Uint8Array.from(atob(cipherBase64), (c) =>
c.charCodeAt(0)
);

// Step 3: correct key handling (IMPORTANT)
const keyBytes = getKeyBytes(decryptionKey);

const cryptoKey = await crypto.subtle.importKey(
'raw',
keyBytes,
{ name: 'AES-CBC' },
false,
['decrypt']
);

// Step 4: decrypt
const decrypted = await crypto.subtle.decrypt(
{ name: 'AES-CBC', iv },
cryptoKey,
cipherBytes
);

const res = new TextDecoder().decode(decrypted);

return res;
} catch (err) {
return '';
}
}

const KEY = 'YWxvb2tlcGFyYXRoZXdpdGhsYXNzaQ==';
function getKeyBytes(key: string): Uint8Array {
// Treat key as UTF-8 string (LIKE CryptoJS)
const encoded = new TextEncoder().encode(key);

export default async function decrypt(urls: StreamUrl[]): Promise<string[]> {
const results: string[] = [];
// CryptoJS pads/truncates to 32 bytes
const result = new Uint8Array(32);
result.set(encoded.slice(0, 32));

return result;
}

export async function deriveKey(e: string): Promise<string> {
try {
for (const streamurl of urls) {
// atob(e)
const decoded = Buffer.from(streamurl.link, 'base64').toString(
'utf8'
);

const [ivBase64, cipherBase64] = decoded.split(':');
if (!ivBase64 || !cipherBase64) continue;

// Base64.parse(a)
const iv = Buffer.from(ivBase64, 'base64');

// ciphertext
const ciphertext = Buffer.from(cipherBase64, 'base64');

// key.padEnd(32, "\0")
const paddedKey = Buffer.from(KEY, 'base64')
.toString()
.padEnd(32, '\0');
const keyBytes = new TextEncoder().encode(paddedKey);

const cryptoKey = await crypto.subtle.importKey(
'raw',
keyBytes,
{ name: 'AES-CBC' },
false,
['decrypt']
);

const decryptedBuffer = await crypto.subtle.decrypt(
{
name: 'AES-CBC',
iv
},
cryptoKey,
ciphertext
);

const decrypted = new TextDecoder().decode(decryptedBuffer).trim();

if (decrypted) results.push(decrypted);
}

return results;
} catch (error) {
throw new Error('Vidzee Decrypt failed: ' + (error as Error).message);
if (!e) return '';

const base64ToBytes = (e: string) => {
const t = atob(e.replace(/\s+/g, ''));
const n = t.length;
const r = new Uint8Array(n);
for (let i = 0; i < n; i++) {
r[i] = t.charCodeAt(i);
}
return r;
};

let t = base64ToBytes(e);

if (t.length <= 28) return '';

let n = t.slice(0, 12);
let r = t.slice(12, 28);
let a = t.slice(28);

let i = new Uint8Array(a.length + r.length);
i.set(a, 0);
i.set(r, a.length);

let encoder = new TextEncoder();
let l = await crypto.subtle.digest(
'SHA-256',
encoder.encode('4f2a9c7d1e8b3a6f0d5c2e9a7b1f4d8c')
);

let o = await crypto.subtle.importKey(
'raw',
l,
{ name: 'AES-GCM' },
false,
['decrypt']
);

let c = await crypto.subtle.decrypt(
{
name: 'AES-GCM',
iv: n,
tagLength: 128
},
o,
i
);

return new TextDecoder().decode(c);
} catch (err) {
return '';
}
}
52 changes: 40 additions & 12 deletions src/providers/vidzee/vidzee.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,22 @@ import type {
Source
} from '@omss/framework';
import type { StreamResponse } from './vidzee.types.js';
import axios from 'axios';
import decrypt from './decrypt.js';
import axios, { AxiosResponse } from 'axios';
import { decrypt, deriveKey } from './decrypt.js';

export class VidZeeProvider extends BaseProvider {
readonly id = 'vidzee';
readonly name = 'VidZee';
readonly enabled = true;
readonly BASE_URL = 'https://player.vidzee.wtf';
readonly BASE_URL = 'https://core.vidzee.wtf';
readonly PLAYER_URL = 'https://player.vidzee.wtf';
readonly HEADERS = {
'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) 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
Referer: this.PLAYER_URL,
Origin: this.PLAYER_URL
};

readonly capabilities: ProviderCapabilities = {
Expand All @@ -46,7 +47,7 @@ export class VidZeeProvider extends BaseProvider {
}

/**
* Main scraping logic - Parallel servers + decryption
* Main scraping logic - Parallel servers + FULL parallel decryption
*/
private async getSources(
media: ProviderMediaObject,
Expand All @@ -55,15 +56,24 @@ export class VidZeeProvider extends BaseProvider {
try {
const tmdbId = media.tmdbId;

// 0. Start fetching decryption key early (non-blocking)
const decKey = await this.fetchDecryptionKey();
if (!decKey) {
return this.emptyResult(
'Failed to fetch decryption key',
media
);
}

// 1. Parallel server requests
const serverPromises = Array.from({ length: 14 }, (_, serverId) =>
this.fetchServer(tmdbId, serverId, params)
);

const results = await Promise.allSettled(serverPromises);
const successfulResponses: StreamResponse[] = [];

for (let i = 0; i < results.length; i++) {
const result = results[i];
for (const result of results) {
if (result.status === 'fulfilled' && result.value) {
successfulResponses.push(result.value);
}
Expand All @@ -72,15 +82,15 @@ export class VidZeeProvider extends BaseProvider {
if (successfulResponses.length === 0) {
return this.emptyResult('No working servers', media);
}

// 2. Parallel decryption of ALL urls from ALL successful servers
// 2. FULL parallel decryption (servers × urls)
const decryptPromises = successfulResponses.map((response) =>
decrypt(response.url).then((decryptedLinks) => ({
Promise.all(
response.url.map((u) => decrypt(u.link, decKey))
).then((decryptedLinks) => ({
response,
decryptedLinks
}))
);

const decryptionResults = await Promise.all(decryptPromises);

// Flatten and deduplicate decrypted links
Expand Down Expand Up @@ -188,6 +198,24 @@ export class VidZeeProvider extends BaseProvider {
}
}

private async fetchDecryptionKey(): Promise<string | null> {
try {
const response = await fetch(`${this.BASE_URL}/api-key`, {
headers: this.HEADERS
});
if (response.status === 200) {
const data = await response.text();
if (data) {
const key = await deriveKey(data);
return key;
}
}
throw new Error('Failed to retrieve decryption key');
} catch (error) {
return null;
}
}

/**
* Return empty result with diagnostic
*/
Expand Down
Loading
Loading