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 { ZodObject } from 'zod';
import {ZodObject} from 'zod';
export interface ExtractedInfo {
error?: string;

View File

@@ -1,7 +1,7 @@
import {BaseProvider, ExtractedInfo} from './base';
import {getFetch} from '../fetch';
import {matchCustomDownload, matchLink, runObfuscatedScript} from './utils';
import { ZodObject } from 'zod';
import {ZodObject} from 'zod';
/**
* @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 {DownloadOne} from './downloaderOneProvider';
import {NativeProvider} from './nativeProvider';
import {FasttokSaveProvider} from './fasttokSaveProvider';
// import {GetVidTikProvider} from './getVidTikProvider';
export const Providers: BaseProvider[] = [
@@ -28,12 +29,12 @@ export const Providers: BaseProvider[] = [
// new DownloadOne(),
new NativeProvider(),
// new GetVidTikProvider(),
new FasttokSaveProvider(),
];
export const getRandomProvider = (): BaseProvider => {
const provider = Providers[Math.floor(Math.random() * Providers.length)]
while(provider.resourceName() === 'native')
{
const provider = Providers[Math.floor(Math.random() * Providers.length)];
while (provider.resourceName() === 'native') {
return getRandomProvider();
}

View File

@@ -1,7 +1,7 @@
import got from 'got';
import {getFetch} from '../fetch';
import {BaseProvider, ExtractedInfo} from './base';
import { extractMusicalyDownImages, matchLink } from './utils';
import {extractMusicalyDownImages, matchLink} from './utils';
/**
* @class MusicalyDown
@@ -29,7 +29,8 @@ export class MusicalyDown extends BaseProvider {
Accept: '*/*',
Referer: 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: '*/*',
Referer: 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(
JSON.stringify({
html: response.body,
headers: {
Cookie: res.headers['set-cookie']?.toString(),
Accept: '*/*',
Referer: 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',
},
}));
}),
);
}
/**
@@ -70,15 +75,15 @@ export class MusicalyDown extends BaseProvider {
* @return {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 matchedUrls = urls?.filter(url => /muscdn/gi.test(url)) ?? [];
const matchedUrls = urls?.filter((url) => /muscdn/gi.test(url)) ?? [];
const musicalyDownUrls = extractMusicalyDownImages(html);
const isSlide = musicalyDownUrls.length > 2;
const nonImages = matchedUrls.filter(u => u.includes('images'));
const image = matchedUrls.find(u => u.includes('images'));
const nonImages = matchedUrls.filter((u) => u.includes('images'));
const image = matchedUrls.find((u) => u.includes('images'));
const info: ExtractedInfo = {
video: {
@@ -86,24 +91,30 @@ export class MusicalyDown extends BaseProvider {
thumb: image,
},
slides: isSlide ? musicalyDownUrls.slice(1) : undefined,
author: !isSlide ? {
author: !isSlide
? {
thumb: musicalyDownUrls[0],
} : undefined,
music: !isSlide ? {
}
: undefined,
music: !isSlide
? {
url: nonImages[0],
} : undefined,
}
: undefined,
};
if (isSlide) {
const tokenRenderRegex = /data:\s*"([^"]+)"/;
const token = tokenRenderRegex.exec(html)?.[1];
const response = await got.post('https://render.muscdn.app/slider', {
const response = await got
.post('https://render.muscdn.app/slider', {
form: {
data: token,
},
headers,
}).json<{
})
.json<{
success: boolean;
url?: string;
}>();

View File

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

View File

@@ -47,7 +47,8 @@ export class SaveFromProvider extends BaseProvider {
headers: {
Origin: '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(),
},
});

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
import {BaseProvider, ExtractedInfo} from './base';
import {getFetch} from '../fetch';
import { ZodObject } from 'zod';
import {ZodObject} from 'zod';
/**
* @class TikDownProvider
@@ -32,8 +32,7 @@ export class TikDownProvider extends BaseProvider {
});
const body = response.body;
if (/please double/gi.test(body))
{
if (/please double/gi.test(body)) {
return {
error: 'Video not found',
};
@@ -50,7 +49,7 @@ export class TikDownProvider extends BaseProvider {
if (!responseVideo.body.length) {
return {
error: 'Couldnt find downloaded URL',
}
};
}
return this.extract(responseVideo.body);
@@ -63,9 +62,14 @@ export class TikDownProvider extends BaseProvider {
extract(html: string): ExtractedInfo {
return {
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 {BaseProvider, ExtractedInfo} from './base';
import {deObfuscate, matchLink} from './utils';
@@ -28,8 +28,9 @@ export class TikmateProvider extends BaseProvider {
const response = await this.client('./', {
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(
@@ -53,7 +54,8 @@ export class TikmateProvider extends BaseProvider {
Origin: this.client.defaults.options.prefixUrl.toString(),
Referer: this.client.defaults.options.prefixUrl.toString(),
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 {BaseProvider, ExtractedInfo} from './base';
import {matchLink} from './utils';
@@ -54,14 +54,16 @@ export class TTDownloader extends BaseProvider {
const urls = matchLink(html);
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 {
video: {
urls: urls?.filter(u => u !== musicUrl) ?? [],
urls: urls?.filter((u) => u !== musicUrl) ?? [],
},
music: musicUrl ? {
music: musicUrl
? {
url: musicUrl,
} : undefined,
}
: undefined,
};
}

View File

@@ -18,11 +18,11 @@ export const matchTikTokData = (html: string): string => {
export const runObfuscatedReplaceEvalScript = (jsCode: string): string => {
return runObfuscatedScript(jsCode.replace('eval', 'module.exports = '));
}
};
export const extractMusicalyDownImages = (html: string): string[] => {
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 => {
@@ -92,9 +92,7 @@ export const matchCustomDownload = (
export const deObfuscateSaveFromScript = (scriptContent: string): string => {
const safeScript =
'let result = ' +
scriptContent
.replace(/\/\*js\-response\*\//gi, '');
'let result = ' + scriptContent.replace(/\/\*js\-response\*\//gi, '');
const vm = new NodeVM({
compiler: 'javascript',