Merge pull request #124 from hansputera/feat/fasttoksave-provider

feat: added fasttok provider
This commit is contained in:
ハンニフ
2025-09-21 03:39:06 +07:00
committed by GitHub
13 changed files with 184 additions and 74 deletions

View File

@@ -1,5 +1,5 @@
import {Got} from 'got'; import {Got} from 'got';
import { ZodObject } from 'zod'; import {ZodObject} from 'zod';
export interface ExtractedInfo { export interface ExtractedInfo {
error?: string; error?: string;

View File

@@ -1,7 +1,7 @@
import {BaseProvider, ExtractedInfo} from './base'; import {BaseProvider, ExtractedInfo} from './base';
import {getFetch} from '../fetch'; import {getFetch} from '../fetch';
import {matchCustomDownload, matchLink, runObfuscatedScript} from './utils'; import {matchCustomDownload, matchLink, runObfuscatedScript} from './utils';
import { ZodObject } from 'zod'; import {ZodObject} from 'zod';
/** /**
* @class DownTikProvider * @class DownTikProvider

View File

@@ -0,0 +1,95 @@
import {ZodObject} from 'zod';
import {getFetch} from '../fetch';
import {BaseProvider, ExtractedInfo, MaintenanceProvider} from './base';
/**
* @class FasttokSaveProvider
*/
export class FasttokSaveProvider extends BaseProvider {
/**
* Get provider resource name
* @return {string}
*/
public resourceName(): string {
return 'fasttoksave';
}
public client = getFetch('https://www.fasttoksave.com/');
public maintenance?: MaintenanceProvider | undefined = undefined;
/**
* Fetch tiktok video resource
* @param {string} url TikTok URL
* @return {Promise<ExtractedInfo>}
*/
async fetch(url: string): Promise<ExtractedInfo> {
const response = await this.client
.post('./en/wp-json/tiktok-downloader/v1/fetch', {
json: {
url,
},
})
.json<{
code: number;
msg: string;
data?: {
author: {
nickname: string;
unique_id: string;
};
comment_count: number;
play_count: number;
cover: string;
play: string;
music_info: {
author: string;
cover: string;
title: string;
};
wmplay: string;
title: string;
duration: number;
};
}>();
if (response.code === -1 || !response.data) {
return {
error: 'Video not found.',
};
}
return {
video: {
urls: [response.data.play, response.data.wmplay],
thumb: response.data.cover,
duration: (response.data.duration * 1000).toString(),
},
music: {
url: '',
author: response.data.music_info.author,
cover: response.data.music_info.cover,
title: response.data.music_info.title,
},
commentsCount: response.data.comment_count,
playsCount: response.data.play_count,
caption: response.data.title,
};
}
/**
* Extract contents from HTML raw
* @param {string} html HTML Raw
* @return {{}}
*/
public extract(html: string): ExtractedInfo | Promise<ExtractedInfo> {
return {};
}
/**
* Get params
* @return {undefined}
*/
public getParams(): ZodObject | undefined {
return undefined;
}
}

View File

@@ -12,6 +12,7 @@ import {DownTikProvider} from './downTikProvider';
// import {DDDTikProvider} from './dddTikProvider'; // import {DDDTikProvider} from './dddTikProvider';
// import {DownloadOne} from './downloaderOneProvider'; // import {DownloadOne} from './downloaderOneProvider';
import {NativeProvider} from './nativeProvider'; import {NativeProvider} from './nativeProvider';
import {FasttokSaveProvider} from './fasttokSaveProvider';
// import {GetVidTikProvider} from './getVidTikProvider'; // import {GetVidTikProvider} from './getVidTikProvider';
export const Providers: BaseProvider[] = [ export const Providers: BaseProvider[] = [
@@ -28,12 +29,12 @@ export const Providers: BaseProvider[] = [
// new DownloadOne(), // new DownloadOne(),
new NativeProvider(), new NativeProvider(),
// new GetVidTikProvider(), // new GetVidTikProvider(),
new FasttokSaveProvider(),
]; ];
export const getRandomProvider = (): BaseProvider => { export const getRandomProvider = (): BaseProvider => {
const provider = Providers[Math.floor(Math.random() * Providers.length)] const provider = Providers[Math.floor(Math.random() * Providers.length)];
while(provider.resourceName() === 'native') while (provider.resourceName() === 'native') {
{
return getRandomProvider(); return getRandomProvider();
} }

View File

@@ -1,7 +1,7 @@
import got from 'got'; import got from 'got';
import {getFetch} from '../fetch'; import {getFetch} from '../fetch';
import {BaseProvider, ExtractedInfo} from './base'; import {BaseProvider, ExtractedInfo} from './base';
import { extractMusicalyDownImages, matchLink } from './utils'; import {extractMusicalyDownImages, matchLink} from './utils';
/** /**
* @class MusicalyDown * @class MusicalyDown
@@ -29,7 +29,8 @@ export class MusicalyDown extends BaseProvider {
Accept: '*/*', Accept: '*/*',
Referer: this.client.defaults.options.prefixUrl.toString(), Referer: this.client.defaults.options.prefixUrl.toString(),
Origin: this.client.defaults.options.prefixUrl.toString(), Origin: this.client.defaults.options.prefixUrl.toString(),
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36' 'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36',
}, },
}); });
@@ -48,20 +49,24 @@ export class MusicalyDown extends BaseProvider {
Accept: '*/*', Accept: '*/*',
Referer: this.client.defaults.options.prefixUrl.toString(), Referer: this.client.defaults.options.prefixUrl.toString(),
Origin: this.client.defaults.options.prefixUrl.toString(), Origin: this.client.defaults.options.prefixUrl.toString(),
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36', 'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36',
}, },
}); });
return this.extract(JSON.stringify({ return this.extract(
html: response.body, JSON.stringify({
headers: { html: response.body,
Cookie: res.headers['set-cookie']?.toString(), headers: {
Accept: '*/*', Cookie: res.headers['set-cookie']?.toString(),
Referer: this.client.defaults.options.prefixUrl.toString(), Accept: '*/*',
Origin: this.client.defaults.options.prefixUrl.toString(), Referer: this.client.defaults.options.prefixUrl.toString(),
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36', Origin: this.client.defaults.options.prefixUrl.toString(),
}, 'User-Agent':
})); 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36',
},
}),
);
} }
/** /**
@@ -70,15 +75,15 @@ export class MusicalyDown extends BaseProvider {
* @return {ExtractedInfo} * @return {ExtractedInfo}
*/ */
public async extract(body: string): Promise<ExtractedInfo> { public async extract(body: string): Promise<ExtractedInfo> {
const { html, headers } = JSON.parse(body); const {html, headers} = JSON.parse(body);
const urls = matchLink(html); const urls = matchLink(html);
const matchedUrls = urls?.filter(url => /muscdn/gi.test(url)) ?? []; const matchedUrls = urls?.filter((url) => /muscdn/gi.test(url)) ?? [];
const musicalyDownUrls = extractMusicalyDownImages(html); const musicalyDownUrls = extractMusicalyDownImages(html);
const isSlide = musicalyDownUrls.length > 2; const isSlide = musicalyDownUrls.length > 2;
const nonImages = matchedUrls.filter(u => u.includes('images')); const nonImages = matchedUrls.filter((u) => u.includes('images'));
const image = matchedUrls.find(u => u.includes('images')); const image = matchedUrls.find((u) => u.includes('images'));
const info: ExtractedInfo = { const info: ExtractedInfo = {
video: { video: {
@@ -86,27 +91,33 @@ export class MusicalyDown extends BaseProvider {
thumb: image, thumb: image,
}, },
slides: isSlide ? musicalyDownUrls.slice(1) : undefined, slides: isSlide ? musicalyDownUrls.slice(1) : undefined,
author: !isSlide ? { author: !isSlide
thumb: musicalyDownUrls[0], ? {
} : undefined, thumb: musicalyDownUrls[0],
music: !isSlide ? { }
url: nonImages[0], : undefined,
} : undefined, music: !isSlide
? {
url: nonImages[0],
}
: undefined,
}; };
if (isSlide) { if (isSlide) {
const tokenRenderRegex = /data:\s*"([^"]+)"/; const tokenRenderRegex = /data:\s*"([^"]+)"/;
const token = tokenRenderRegex.exec(html)?.[1]; const token = tokenRenderRegex.exec(html)?.[1];
const response = await got.post('https://render.muscdn.app/slider', { const response = await got
form: { .post('https://render.muscdn.app/slider', {
data: token, form: {
}, data: token,
headers, },
}).json<{ headers,
success: boolean; })
url?: string; .json<{
}>(); success: boolean;
url?: string;
}>();
if (response.success && response.url?.length) { if (response.success && response.url?.length) {
info.video = { info.video = {

View File

@@ -99,6 +99,6 @@ export class NativeProvider extends BaseProvider {
public getParams(): z.ZodObject { public getParams(): z.ZodObject {
return z.object({ return z.object({
'user-agent': z.string().min(5), 'user-agent': z.string().min(5),
}) });
} }
} }

View File

@@ -47,7 +47,8 @@ export class SaveFromProvider extends BaseProvider {
headers: { headers: {
Origin: 'https://en1.savefrom.net', Origin: 'https://en1.savefrom.net',
Referer: 'https://en1.savefrom.net', Referer: 'https://en1.savefrom.net',
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36', 'User-Agent':
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36',
Cookies: responseFirst.headers['set-cookie']?.toString(), Cookies: responseFirst.headers['set-cookie']?.toString(),
}, },
}); });

View File

@@ -1,6 +1,6 @@
import {BaseProvider, ExtractedInfo} from './base'; import {BaseProvider, ExtractedInfo} from './base';
import {getFetch} from '../fetch'; import {getFetch} from '../fetch';
import { ZodObject } from 'zod'; import {ZodObject} from 'zod';
// import {matchLink, runObfuscatedReplaceEvalScript} from './utils'; // import {matchLink, runObfuscatedReplaceEvalScript} from './utils';
/** /**
@@ -32,8 +32,7 @@ export class SaveTikProvider extends BaseProvider {
throwHttpErrors: false, throwHttpErrors: false,
}); });
if (response.statusCode === 400) if (response.statusCode === 400) {
{
return { return {
error: 'Video not found', error: 'Video not found',
}; };
@@ -72,10 +71,7 @@ export class SaveTikProvider extends BaseProvider {
return { return {
video: { video: {
urls: [ urls: [json.downloadUrl, json.hdDownloadUrl],
json.downloadUrl,
json.hdDownloadUrl,
],
title: json.postinfo.media_title, title: json.postinfo.media_title,
duration: json.duration.toString(), duration: json.duration.toString(),
}, },
@@ -89,11 +85,11 @@ export class SaveTikProvider extends BaseProvider {
sharesCount: json.stats.shareCount, sharesCount: json.stats.shareCount,
playsCount: json.stats.playCount, playsCount: json.stats.playCount,
commentsCount: json.stats.commentCount, commentsCount: json.stats.commentCount,
} };
} catch { } catch {
return { return {
error: 'Video not found', error: 'Video not found',
} };
} }
} }

View File

@@ -1,4 +1,4 @@
import { ZodObject } from 'zod'; import {ZodObject} from 'zod';
import {getFetch} from '../fetch'; import {getFetch} from '../fetch';
import {BaseProvider, ExtractedInfo, MaintenanceProvider} from './base'; import {BaseProvider, ExtractedInfo, MaintenanceProvider} from './base';
import {matchLink, runObfuscatedReplaceEvalScript} from './utils'; import {matchLink, runObfuscatedReplaceEvalScript} from './utils';
@@ -16,7 +16,7 @@ export class SnaptikProvider extends BaseProvider {
return 'snaptik'; return 'snaptik';
} }
public maintenance?: MaintenanceProvider | undefined = undefined public maintenance?: MaintenanceProvider | undefined = undefined;
/** /**
* *

View File

@@ -1,6 +1,6 @@
import {BaseProvider, ExtractedInfo} from './base'; import {BaseProvider, ExtractedInfo} from './base';
import {getFetch} from '../fetch'; import {getFetch} from '../fetch';
import { ZodObject } from 'zod'; import {ZodObject} from 'zod';
/** /**
* @class TikDownProvider * @class TikDownProvider
@@ -32,8 +32,7 @@ export class TikDownProvider extends BaseProvider {
}); });
const body = response.body; const body = response.body;
if (/please double/gi.test(body)) if (/please double/gi.test(body)) {
{
return { return {
error: 'Video not found', error: 'Video not found',
}; };
@@ -50,9 +49,9 @@ export class TikDownProvider extends BaseProvider {
if (!responseVideo.body.length) { if (!responseVideo.body.length) {
return { return {
error: 'Couldnt find downloaded URL', error: 'Couldnt find downloaded URL',
} };
} }
return this.extract(responseVideo.body); return this.extract(responseVideo.body);
} }
@@ -63,9 +62,14 @@ export class TikDownProvider extends BaseProvider {
extract(html: string): ExtractedInfo { extract(html: string): ExtractedInfo {
return { return {
video: { video: {
urls: [new URL(`./${html}`, this.client.defaults.options.prefixUrl.toString()).href], urls: [
} new URL(
} `./${html}`,
this.client.defaults.options.prefixUrl.toString(),
).href,
],
},
};
} }
/** /**

View File

@@ -1,4 +1,4 @@
import { ZodObject } from 'zod'; import {ZodObject} from 'zod';
import {getFetch} from '../fetch'; import {getFetch} from '../fetch';
import {BaseProvider, ExtractedInfo} from './base'; import {BaseProvider, ExtractedInfo} from './base';
import {deObfuscate, matchLink} from './utils'; import {deObfuscate, matchLink} from './utils';
@@ -28,8 +28,9 @@ export class TikmateProvider extends BaseProvider {
const response = await this.client('./', { const response = await this.client('./', {
headers: { headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36', 'User-Agent':
} 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36',
},
}); });
const matchs = response.body.match( const matchs = response.body.match(
@@ -53,7 +54,8 @@ export class TikmateProvider extends BaseProvider {
Origin: this.client.defaults.options.prefixUrl.toString(), Origin: this.client.defaults.options.prefixUrl.toString(),
Referer: this.client.defaults.options.prefixUrl.toString(), Referer: this.client.defaults.options.prefixUrl.toString(),
Cookie: cookies, Cookie: cookies,
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36', 'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36',
}, },
}); });

View File

@@ -1,4 +1,4 @@
import { ZodObject } from 'zod'; import {ZodObject} from 'zod';
import {getFetch} from '../fetch'; import {getFetch} from '../fetch';
import {BaseProvider, ExtractedInfo} from './base'; import {BaseProvider, ExtractedInfo} from './base';
import {matchLink} from './utils'; import {matchLink} from './utils';
@@ -54,14 +54,16 @@ export class TTDownloader extends BaseProvider {
const urls = matchLink(html); const urls = matchLink(html);
urls?.pop(); // remove 'https://snaptik.fans' urls?.pop(); // remove 'https://snaptik.fans'
const musicUrl = urls?.find(u => /mp3/gi.test(u)); const musicUrl = urls?.find((u) => /mp3/gi.test(u));
return { return {
video: { video: {
urls: urls?.filter(u => u !== musicUrl) ?? [], urls: urls?.filter((u) => u !== musicUrl) ?? [],
}, },
music: musicUrl ? { music: musicUrl
url: musicUrl, ? {
} : undefined, url: musicUrl,
}
: undefined,
}; };
} }

View File

@@ -18,11 +18,11 @@ export const matchTikTokData = (html: string): string => {
export const runObfuscatedReplaceEvalScript = (jsCode: string): string => { export const runObfuscatedReplaceEvalScript = (jsCode: string): string => {
return runObfuscatedScript(jsCode.replace('eval', 'module.exports = ')); return runObfuscatedScript(jsCode.replace('eval', 'module.exports = '));
} };
export const extractMusicalyDownImages = (html: string): string[] => { export const extractMusicalyDownImages = (html: string): string[] => {
const regex = /<img[^>]+src="(https[^"]+)"/gi; const regex = /<img[^>]+src="(https[^"]+)"/gi;
return [...html.matchAll(regex)].map(m => m[1]); return [...html.matchAll(regex)].map((m) => m[1]);
}; };
export const runObfuscatedScript = (jsCode: string): string => { export const runObfuscatedScript = (jsCode: string): string => {
@@ -92,9 +92,7 @@ export const matchCustomDownload = (
export const deObfuscateSaveFromScript = (scriptContent: string): string => { export const deObfuscateSaveFromScript = (scriptContent: string): string => {
const safeScript = const safeScript =
'let result = ' + 'let result = ' + scriptContent.replace(/\/\*js\-response\*\//gi, '');
scriptContent
.replace(/\/\*js\-response\*\//gi, '');
const vm = new NodeVM({ const vm = new NodeVM({
compiler: 'javascript', compiler: 'javascript',