diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 00000000..0f5163e3 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,32 @@ +--- +name: Bug report +about: Create a report to help us improve +title: "[BUG] - A bug" +labels: bug, question +assignees: hansputera + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Fill query '...' with '...' +3. Do request +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Query Params (please complete the following information):** + - URL: [e.g. https://www.tiktok.com/@raphaeltangg/video/7027045794377190658] + - Browser [e.g. chrome, safari] + - Type [e.g. snaptik, tikdown] (Provider) + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 00000000..57a3e011 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: "[FEATURE]" +labels: enhancement +assignees: hansputera + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 00000000..146233d6 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,128 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the + overall community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or + advances of any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email + address, without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +hanifdwyputrasembiring@gmail.com. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series +of actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or +permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within +the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.0, available at +https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. + +Community Impact Guidelines were inspired by [Mozilla's code of conduct +enforcement ladder](https://github.com/mozilla/diversity). + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see the FAQ at +https://www.contributor-covenant.org/faq. Translations are available at +https://www.contributor-covenant.org/translations. diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..daf3a3b3 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Hanif Dwy Putra S + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/api/download.ts b/api/download.ts index c1c89d1d..3705ef24 100644 --- a/api/download.ts +++ b/api/download.ts @@ -9,7 +9,7 @@ const providersType = Providers.map((p) => p.resourceName()); export default async (req: VercelRequest, res: VercelResponse) => { try { - ow(req.query, ow.object.partialShape({ + ow(req.method === 'POST' ? req.body : req.query, ow.object.partialShape({ 'url': ow.string.url.validate((v) => ({ 'validator': /^http(s?)(:\/\/)([a-z]+\.)*tiktok\.com\/(.*)?\/(.*)?$/gi .test(v), @@ -21,10 +21,13 @@ export default async (req: VercelRequest, res: VercelResponse) => { 'message': 'Invalid Provider, available provider is: ' + Providers.map((x) => x.resourceName()).join(', '), })), - 'nocache': ow.optional.string, + 'nocache': req.method === 'POST' ? + ow.optional.boolean : ow.optional.string, + 'rotateOnError': req.method === 'POST' ? + ow.optional.boolean : ow.optional.string, })); - const provider = getProvider(req.query.type ?? 'random'); + const provider = getProvider((req.query.type || req.body.type) ?? 'random'); if (!provider) { return res.status(400).json({ 'error': 'Invalid provider', @@ -32,7 +35,11 @@ export default async (req: VercelRequest, res: VercelResponse) => { }); } const result = await rotateProvider( - provider as BaseProvider, req.query.url, !!req.query.nocache); + provider as BaseProvider, req.query.url || req.body.url, + req.method === 'POST' ? + req.body.url : req.query.url, req.method === 'POST' ? + req.body.rotateOnError : + !!req.query.rotateOnError); await ratelimitMiddleware(req); return res.status(200).json(result); } catch (e) { diff --git a/api/providers.ts b/api/providers.ts new file mode 100644 index 00000000..8136ed20 --- /dev/null +++ b/api/providers.ts @@ -0,0 +1,12 @@ +import type {VercelRequest, VercelResponse} from '@vercel/node'; +import {Providers} from '../lib'; + +export default async (_: VercelRequest, res: VercelResponse) => { + const providers = Providers.map((p) => ({ + 'name': p.resourceName(), + 'url': p.client.defaults.options.prefixUrl, + 'maintenance': p.maintenance, + })); + + return res.send(providers); +}; diff --git a/config.ts b/config.ts index f454463f..ed742c25 100644 --- a/config.ts +++ b/config.ts @@ -15,5 +15,6 @@ export const rateLimitConfig = { /** * Provider response data will stored on redis. + * Default: 1 hour */ -export const providerCache = 600; +export const providerCache = 3600; diff --git a/lib/decorators/handleException.ts b/lib/decorators/handleException.ts deleted file mode 100644 index 6d9f2c52..00000000 --- a/lib/decorators/handleException.ts +++ /dev/null @@ -1,30 +0,0 @@ -/** - * Decorator to handle exception. - * @param {any} target - * @param {string} _ - * @param {PropertyDescriptor} descriptor - * @return {void} - */ -export const handleException = - (target: object, _: string, descriptor: TypedPropertyDescriptor): -TypedPropertyDescriptor | void => { - return { - configurable: true, - get(this: T): T { - try { - const bound: T = descriptor.value?.bind(this); - Object.defineProperty(this, _, { - value: bound, - configurable: true, - writable: true, - }); - - return bound; - } catch (err) { - return { - 'error': (err as Error).message, - } as unknown as T; - } - }, - }; -}; diff --git a/lib/decorators/index.ts b/lib/decorators/index.ts deleted file mode 100644 index 101f8b6b..00000000 --- a/lib/decorators/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './handleException'; diff --git a/lib/providers/DLTikProvider.ts b/lib/providers/DLTikProvider.ts index 77d5df05..7f11eeff 100644 --- a/lib/providers/DLTikProvider.ts +++ b/lib/providers/DLTikProvider.ts @@ -1,5 +1,4 @@ import {getFetch} from '..'; -import {handleException} from '../decorators'; import {BaseProvider, ExtractedInfo} from './baseProvider'; /** @@ -15,14 +14,18 @@ export class DLTikProvider extends BaseProvider { public client = getFetch('https://dltik.com'); + public maintenance = { + 'reason': 'My prediction is that DLTik needs an active session to use.', + }; + /** - * * @param {string} url - Video TikTok URL + * @return {Promise} */ - @handleException public async fetch(url: string): Promise { // getting verification token - const response = await this.client('./'); + const response = await this.client('./#url=' + + encodeURIComponent(url)); const token = ( response.body.match(/type="hidden" value="([^""]+)"/) as string[] )[1]; @@ -30,13 +33,17 @@ export class DLTikProvider extends BaseProvider { const dlResponse = await this.client.post('./', { 'form': { 'm': 'getlink', - 'url': url, + 'url': `https://m.tiktok.com/v/${ + (/predownload\('([0-9]+)'\)/gi.exec(response.body) as string[])[1] + }.html`, '__RequestVerificationToken': token, }, 'headers': { 'Origin': this.client.defaults.options.prefixUrl, - 'Referer': this.client.defaults.options.prefixUrl, + 'Referer': response.url, 'Cookie': response.headers['set-cookie']?.toString(), + 'Content-Type': 'application/x-www-form-urlencoded', + 'x-requested-with': 'XMLHttpRequest', }, }); @@ -61,16 +68,15 @@ export class DLTikProvider extends BaseProvider { // }; // } return { - 'error': undefined, - 'result': { + 'video': { + 'id': json.data.videoId, + 'urls': [json.data.watermarkVideoUrl, json.data.destinationUrl], 'thumb': json.data.dynamicCover, - 'urls': [json.data.destinationUrl, json.data.watermarkVideoUrl], - 'advanced': { - 'musicUrl': json.data.musicUrl, - 'videoId': json.data.videoId, - 'description': json.data.desc, - }, }, + 'music': { + 'url': json.data.musicUrl, + }, + 'caption': json.data.desc, }; } } diff --git a/lib/providers/baseProvider.ts b/lib/providers/baseProvider.ts index fb676d5e..facc5fee 100644 --- a/lib/providers/baseProvider.ts +++ b/lib/providers/baseProvider.ts @@ -2,11 +2,36 @@ import {Got} from 'got'; export interface ExtractedInfo { error?: string; - result?: { + video?: { + id?: string; thumb?: string; - advanced?: Record; urls: string[]; - } + title?: string; + duration?: string; + }; + music?: { + url: string; + title?: string; + author?: string; + id?: string; + cover?: string; + }; + author?: { + username?: string; + thumb?: string; + id?: string; + }; + caption?: string; + playsCount?: number; + sharesCount?: number; + commentsCount?: number; + likesCount?: number; + uploadedAt?: string; + updatedAt?: string; +}; + +export interface MaintenanceProvider { + reason: string; }; /** @@ -14,6 +39,7 @@ export interface ExtractedInfo { */ export abstract class BaseProvider { abstract client: Got; + abstract maintenance?: MaintenanceProvider; abstract resourceName(): string; abstract fetch(url: string): Promise; abstract extract(html: string): ExtractedInfo; diff --git a/lib/providers/dddTikProvider.ts b/lib/providers/dddTikProvider.ts index 03461460..6faeed89 100644 --- a/lib/providers/dddTikProvider.ts +++ b/lib/providers/dddTikProvider.ts @@ -1,6 +1,5 @@ import {BaseProvider, ExtractedInfo} from './baseProvider'; import {getFetch} from '..'; -import {handleException} from '../decorators'; import {matchLink} from './util'; /** @@ -18,11 +17,12 @@ export class DDDTikProvider extends BaseProvider { public client = getFetch('https://dddtik.com'); - /** - * @param {string} url + public maintenance = undefined; + + /** + * @param {string} url Tiktok video url * @return {Promise} */ - @handleException async fetch(url: string): Promise { const response = await this.client.post('./down.php', { 'form': { @@ -33,20 +33,20 @@ export class DDDTikProvider extends BaseProvider { return this.extract(response.body); } - /** + /** * @param {string} html * @return {ExtractedInfo} */ - extract(html: string): ExtractedInfo { - const urls = matchLink(html) as string[]; - urls.pop(); + extract(html: string): ExtractedInfo { + const urls = matchLink(html) as string[]; + urls.pop(); - const t = urls[1]; - return { - 'result': { - 'urls': urls.filter((u) => u !== t), - 'thumb': t, - }, - }; - } + const t = urls[1]; + return { + 'video': { + 'urls': urls.filter((u) => u !== t), + 'thumb': t, + }, + }; + } } diff --git a/lib/providers/downTikProvider.ts b/lib/providers/downTikProvider.ts index 3d0c0a50..4162e884 100644 --- a/lib/providers/downTikProvider.ts +++ b/lib/providers/downTikProvider.ts @@ -1,6 +1,5 @@ import {BaseProvider, ExtractedInfo} from './baseProvider'; import {getFetch} from '..'; -import {handleException} from '../decorators'; import {matchCustomDownload} from './util'; /** @@ -18,12 +17,13 @@ export class DownTikProvider extends BaseProvider { public client = getFetch('https://downtik.net'); - /** + public maintenance = undefined; + + /** * @param {string} url * * @return {Promise} */ - @handleException async fetch(url: string): Promise { const response = await this.client('./'); @@ -54,21 +54,21 @@ export class DownTikProvider extends BaseProvider { return this.extract(JSON.parse(responseAction.body).data); } - /** + /** * @param {string} html * @return {ExtractedInfo} */ - extract(html: string): ExtractedInfo { - const urls = matchCustomDownload('downtik', html); + extract(html: string): ExtractedInfo { + const urls = matchCustomDownload('downtik', html); - return { - 'result': { - 'thumb': urls?.shift(), - 'advanced': { - 'musicUrl': urls?.pop(), - }, - 'urls': urls as string[], - }, - }; - } + return { + 'music': { + 'url': urls.pop() as string, + }, + 'video': { + 'thumb': urls?.shift(), + 'urls': urls as string[], + }, + }; + } } diff --git a/lib/providers/downloaderOneProvider.ts b/lib/providers/downloaderOneProvider.ts index 1960d02a..26e54cca 100644 --- a/lib/providers/downloaderOneProvider.ts +++ b/lib/providers/downloaderOneProvider.ts @@ -1,6 +1,5 @@ import {BaseProvider, ExtractedInfo} from './baseProvider'; import {getFetch} from '../fetch'; -import {handleException} from '../decorators'; /** * @class DownloadOne @@ -16,12 +15,13 @@ export class DownloadOne extends BaseProvider { public client = getFetch('http://tiktokdownloader.one'); + public maintenance = undefined; + /** * Fetch ttdownloader.one * @param {string} url Video TikTok URL * @return {Promise} */ - @handleException public async fetch( url: string, ): Promise { @@ -60,30 +60,32 @@ export class DownloadOne extends BaseProvider { const json = JSON.parse(html); return { - 'result': { + 'video': { 'urls': [ json.url, json.url_nwm, ], 'thumb': json.cover, - 'advanced': { - 'videoId': json.video_id, - 'musicUrl': json.music.url, - 'musicTitle': json.music.title, - 'musicAuthor': json.music.author, - 'musicCover': json.music.cover, - 'author': json.user.username, - 'authorId': json.user.name, - 'authorThumb': json.user.cover, - 'uploadedAt': json.uploaded_at, - 'updatedAt': json.updated_at ?? '-', - 'caption': json.caption, - 'commentsCount': json.stats.comment, - 'sharesCount': json.stats.shares, - 'likesCount': json.stats.like, - 'playsCount': json.stats.play, - }, + 'id': json.video_id, + }, + 'music': { + 'url': json.music.url, + 'title': json.music.title, + 'cover': json.music.cover, + 'author': json.music.author, + }, + 'author': { + 'id': json.user.name, + 'username': json.user.username, + 'thumb': json.user.cover, }, + 'caption': json.caption, + 'updatedAt': json.updatedAt ?? '-', + 'uploadedAt': json.uploaded_at, + 'commentsCount': json.stats.comment, + 'sharesCount': json.stats.shares, + 'likesCount': json.stats.likes, + 'playsCount': json.stats.play, }; } } diff --git a/lib/providers/loveTikProvider.ts b/lib/providers/loveTikProvider.ts index b3b5b7c6..65424f9e 100644 --- a/lib/providers/loveTikProvider.ts +++ b/lib/providers/loveTikProvider.ts @@ -1,6 +1,5 @@ import {BaseProvider, ExtractedInfo} from './baseProvider'; import {getFetch} from '..'; -import {handleException} from '../decorators'; /** * @class LoveTikProvider @@ -17,10 +16,12 @@ export class LoveTikProvider extends BaseProvider { public client = getFetch('https://lovetik.com'); - /** - * @param {string} url + public maintenance = undefined; + + /** + * @param {string} url Video TikTok URL + * @return {Promise} */ - @handleException async fetch(url: string): Promise { const response = await this.client.post( './api/ajax/search', { @@ -37,28 +38,30 @@ export class LoveTikProvider extends BaseProvider { return this.extract(response.body); } - /** + /** * @param {string} jsonString * @return {ExtractedInfo} */ - extract(jsonString: string): ExtractedInfo { - const json = JSON.parse(jsonString); - - if (json.mess) { - return { - 'error': json.mess, - }; - } + extract(jsonString: string): ExtractedInfo { + const json = JSON.parse(jsonString); + if (json.mess) { return { - 'result': { - 'thumb': json.cover, - 'advanced': { - 'author': json.author.replace(/(<([^>]+)>)/ig, ''), - 'musicUrl': json.links.pop().a, - }, - 'urls': json.links.map((l: Record) => l.a), - }, + 'error': json.mess, }; } + + return { + 'music': { + 'url': json.links.pop().a, + }, + 'video': { + 'thumb': json.cover, + 'urls': json.links.map((l: Record) => l.a), + }, + 'author': { + 'username': json.author.replace(/(<([^>]+)>)/ig, ''), + }, + }; + } } diff --git a/lib/providers/musicalyDownProvider.ts b/lib/providers/musicalyDownProvider.ts index c595e5c1..92394a79 100644 --- a/lib/providers/musicalyDownProvider.ts +++ b/lib/providers/musicalyDownProvider.ts @@ -1,5 +1,4 @@ import {getFetch} from '..'; -import {handleException} from '../decorators'; import {BaseProvider, ExtractedInfo} from './baseProvider'; /** @@ -15,12 +14,13 @@ export class MusicalyDown extends BaseProvider { return 'musicalydown'; } - /** + public maintenance = undefined; + + /** * * @param {string} url - Video Tiktok URL - * @return {string} + * @return {Promise} */ - @handleException public async fetch(url: string): Promise { const res = await this.client('./', { 'headers': { @@ -52,22 +52,21 @@ export class MusicalyDown extends BaseProvider { return this.extract(response.body); } - /** + /** * * @param {string} html - Raw HTML * @return {ExtractedInfo} */ - public extract(html: string): ExtractedInfo { - const matchUrls = (html - .match(//gi) as string[]); - const urls = matchUrls.map((url) => + public extract(html: string): ExtractedInfo { + const matchUrls = (html + .match(//gi) as string[]); + const urls = matchUrls.map((url) => //gi.exec(url)?.[1] as string); - return { - 'error': undefined, - 'result': { - urls, - 'thumb': /img class="responsive-img" src="(.*?)"/gi.exec(html)?.[1], - }, - }; - } + return { + 'video': { + 'urls': urls, + 'thumb': /img class="responsive-img" src="(.*?)"/gi.exec(html)?.[1], + }, + }; + } } diff --git a/lib/providers/saveFromProvider.ts b/lib/providers/saveFromProvider.ts index 09029979..8d075b77 100644 --- a/lib/providers/saveFromProvider.ts +++ b/lib/providers/saveFromProvider.ts @@ -1,5 +1,4 @@ import {getFetch} from '..'; -import {handleException} from '../decorators'; import {BaseProvider, ExtractedInfo} from './baseProvider'; import {deObfuscateSaveFromScript} from './util'; @@ -17,11 +16,13 @@ export class SaveFromProvider extends BaseProvider { public client = getFetch('https://worker-as.sf-tools.com'); + public maintenance = undefined; + /** * * @param {string} url - Video TikTok URL + * @return {Promise} */ - @handleException public async fetch(url: string): Promise { const response = await this.client.post('./savefrom.php', { 'form': { @@ -49,7 +50,6 @@ export class SaveFromProvider extends BaseProvider { * @param {string} html - HTML Raw * @return {ExtractedInfo} */ - @handleException extract(html: string): ExtractedInfo { const deobfuscated = deObfuscateSaveFromScript(html); const json = JSON.parse( @@ -57,21 +57,12 @@ export class SaveFromProvider extends BaseProvider { .replace(/(\(|\))/g, ''), ); return { - 'error': undefined, - 'result': { + 'video': { 'thumb': json.thumb, - 'advanced': { - 'videoId': json.id, - 'videoTitle': json.meta.title, - 'videoDuration': json.meta.duration, - 'urls': json.url.map((x: - { type: string; subname: string; }, index: number) => ({ - 'pos': index, - 'type': x.type, - 'resolution': x.subname, - })), - }, + 'id': json.id, 'urls': json.url.map((x: { url: string; }) => x.url), + 'duration': json.meta.duration, + 'title': json.meta.title, }, }; } diff --git a/lib/providers/saveTikProvider.ts b/lib/providers/saveTikProvider.ts index b7e5fcfc..0d801520 100644 --- a/lib/providers/saveTikProvider.ts +++ b/lib/providers/saveTikProvider.ts @@ -1,6 +1,5 @@ import {BaseProvider, ExtractedInfo} from './baseProvider'; import {getFetch} from '..'; -import {handleException} from '../decorators'; import {matchCustomDownload} from './util'; /** @@ -18,12 +17,12 @@ export class SaveTikProvider extends BaseProvider { public client = getFetch('https://savetik.net'); - /** - * @param {string} url - * + public maintenance = undefined; + + /** + * @param {string} url Video TikTok URL * @return {Promise} */ - @handleException async fetch(url: string): Promise { const response = await this.client('./'); @@ -54,21 +53,21 @@ export class SaveTikProvider extends BaseProvider { return this.extract(JSON.parse(responseAction.body).data); } - /** + /** * @param {string} html * @return {ExtractedInfo} */ - extract(html: string): ExtractedInfo { - const urls = matchCustomDownload('savetik', html); + extract(html: string): ExtractedInfo { + const urls = matchCustomDownload('savetik', html); - return { - 'result': { - 'thumb': urls?.shift(), - 'advanced': { - 'musicUrl': urls?.pop(), - }, - 'urls': urls as string[], - }, - }; - } + return { + 'music': { + 'url': urls?.pop() as string, + }, + 'video': { + 'thumb': urls?.shift(), + 'urls': urls as string[], + }, + }; + } } diff --git a/lib/providers/snaptikProvider.ts b/lib/providers/snaptikProvider.ts index ff9e1de5..8ab73371 100644 --- a/lib/providers/snaptikProvider.ts +++ b/lib/providers/snaptikProvider.ts @@ -1,5 +1,4 @@ import {getFetch} from '..'; -import {handleException} from '../decorators'; import {BaseProvider, ExtractedInfo} from './baseProvider'; import {deObfuscate, matchLink} from './util'; @@ -16,12 +15,13 @@ export class SnaptikProvider extends BaseProvider { return 'snaptik'; } + public maintenance = undefined; + /** * * @param {string} url - TikTok Video URL * @return {Promise} */ - @handleException public async fetch(url: string): Promise { const response = await this.client('./abc.php', { searchParams: { @@ -38,14 +38,12 @@ export class SnaptikProvider extends BaseProvider { * @param {string} html - Raw HTML * @return {ExtractedInfo} */ - @handleException extract(html: string): ExtractedInfo { const results = matchLink(deObfuscate(html)); if (!results || !results.length) throw new Error('Broken'); return { - 'error': undefined, - 'result': { + 'video': { 'thumb': results?.shift(), 'urls': [...new Set(results)], }, diff --git a/lib/providers/tikDownProvider.ts b/lib/providers/tikDownProvider.ts index bb9b43fb..36cfc787 100644 --- a/lib/providers/tikDownProvider.ts +++ b/lib/providers/tikDownProvider.ts @@ -17,6 +17,8 @@ export class TikDownProvider extends BaseProvider { public client = getFetch('https://tikdown.org'); + public maintenance = undefined; + /** * @param {string} url * @@ -58,7 +60,7 @@ export class TikDownProvider extends BaseProvider { extract(html: string): ExtractedInfo { const urls = matchLink(html) as string[]; return { - 'result': { + 'video': { 'thumb': urls.shift(), 'urls': urls, }, diff --git a/lib/providers/tikmateProvider.ts b/lib/providers/tikmateProvider.ts index 52b349b0..9704336f 100644 --- a/lib/providers/tikmateProvider.ts +++ b/lib/providers/tikmateProvider.ts @@ -1,5 +1,4 @@ import {getFetch} from '..'; -import {handleException} from '../decorators'; import {BaseProvider, ExtractedInfo} from './baseProvider'; import {deObfuscate, matchCustomDownload} from './util'; @@ -16,20 +15,13 @@ export class TikmateProvider extends BaseProvider { return 'tikmate'; } - /** - * - * @return {string} - */ - public getURI(): string { - return this.client.defaults.options.prefixUrl; - } - + public maintenance = undefined; /** * * @param {string} url - Video TikTok URL + * @return {Promise} */ - @handleException public async fetch(url: string): Promise { // we need to get the token @@ -62,12 +54,10 @@ export class TikmateProvider extends BaseProvider { * @param {string} html - Raw HTML * @return {ExtractedInfo} */ - @handleException extract(html: string): ExtractedInfo { const matchs = matchCustomDownload('tikmate', deObfuscate(html)); return { - 'error': undefined, - 'result': { + 'video': { 'thumb': matchs.shift(), 'urls': matchs, }, diff --git a/lib/providers/tokupProvider.ts b/lib/providers/tokupProvider.ts index ecc19648..7c2e48d6 100644 --- a/lib/providers/tokupProvider.ts +++ b/lib/providers/tokupProvider.ts @@ -1,6 +1,5 @@ import {BaseProvider, ExtractedInfo} from './baseProvider'; import {getFetch} from '../fetch'; -import {handleException} from '../decorators'; /** * @class TokupProvider @@ -16,12 +15,15 @@ export class TokupProvider extends BaseProvider { public client = getFetch('https://tokup.app'); + public maintenance = { + reason: 'Tokup site returned \'Oops! Something went wrong!\'', + }; + /** * Fetch tokup * @param {string} url - TikTok Video URL * @return {Promise} */ - @handleException public async fetch( url: string, ): Promise { @@ -34,6 +36,7 @@ export class TokupProvider extends BaseProvider { 'Origin': this.client.defaults.options.prefixUrl, 'Referer': this.client.defaults.options.prefixUrl, }, + 'timeout': 3000, }, ); @@ -44,6 +47,10 @@ export class TokupProvider extends BaseProvider { return { 'error': 'Video Not Found', }; + } else if (/oops/gi.test(response.body)) { + return { + 'error': 'Tokup Error', + }; } else { return this.extract( response.body, @@ -57,6 +64,7 @@ export class TokupProvider extends BaseProvider { * @return {ExtractedInfo} */ extract(html: string): ExtractedInfo { + console.log(html); const authorProfile = (/http(s)?(:\/\/(.*)\.tiktokcdn\.com\/(.*))/gi.exec( html, ) as string[])[0]; @@ -69,22 +77,22 @@ export class TokupProvider extends BaseProvider { )][0]; return { - 'result': { + 'video': { 'urls': [url, url + '?hd=1'], - 'advanced': { - 'authorThumb': authorProfile - .substring(0, authorProfile.length-1), - 'author': (/target="_blank"\>(.*)\(.+)<\/p>/, - ) as string[])[1], - 'likesCount': nums[0], - 'commentsCount': nums[1], - 'sharesCount': nums[2], - }, }, + 'author': { + 'username': (/target="_blank"\>(.*)\(.+)<\/p>/, + ) as string[])[1], + 'likesCount': nums[0] as unknown as number, + 'commentsCount': nums[1] as unknown as number, + 'sharesCount': nums[2] as unknown as number, }; } } diff --git a/lib/providers/ttDownloaderProvider.ts b/lib/providers/ttDownloaderProvider.ts index d36de64d..482e098d 100644 --- a/lib/providers/ttDownloaderProvider.ts +++ b/lib/providers/ttDownloaderProvider.ts @@ -1,5 +1,4 @@ import {getFetch} from '..'; -import {handleException} from '../decorators'; import {BaseProvider, ExtractedInfo} from './baseProvider'; import {matchLink} from './util'; @@ -16,12 +15,13 @@ export class TTDownloader extends BaseProvider { public client = getFetch('https://ttdownloader.com'); + public maintenance = undefined; + /** * * @param {string} url - Video TikTok URL * @return {Promise} */ - @handleException public async fetch(url: string): Promise { // getting token and cookies const firstResponse = await this.client('./'); @@ -52,9 +52,7 @@ export class TTDownloader extends BaseProvider { const urls = matchLink(html); urls?.pop(); // remove 'https://snaptik.fans' return { - 'error': undefined, - 'result': { - 'thumb': undefined, + 'video': { 'urls': urls as string[], }, }; diff --git a/lib/providers/ttSaveProvider.ts b/lib/providers/ttSaveProvider.ts index 083eb949..312f3ede 100644 --- a/lib/providers/ttSaveProvider.ts +++ b/lib/providers/ttSaveProvider.ts @@ -1,5 +1,4 @@ import {getFetch} from '..'; -import {handleException} from '../decorators'; import {BaseProvider, ExtractedInfo} from './baseProvider'; import {keyGeneratorTTSave, matchLink} from './util'; @@ -17,12 +16,15 @@ export class TTSave extends BaseProvider { public client = getFetch('https://ttsave.app'); + public maintenance = { + reason: 'TTSave doesn\'t returned cookie to manipulate the session', + }; + /** * * @param {string} url - TikTok Video URL * @return {Promise} */ - @handleException public async fetch(url: string): Promise { // getting token const response = await this.client('./'); @@ -61,13 +63,12 @@ export class TTSave extends BaseProvider { const videoCDNs = tiktokCDNs.filter((x) => !/jpeg/gi.test(x)); return { - 'error': undefined, - 'result': { + 'video': { 'thumb': tiktokCDNs.find((x) => /jpeg/gi.test(x)), 'urls': videoCDNs.filter((x) => !/music/gi.test(x)), - 'advanced': { - 'musicUrl': videoCDNs.find((x) => /music/gi.test(x)), - }, + }, + 'music': { + 'url': videoCDNs.find((x) => /music/gi.test(x)) as string, }, }; } diff --git a/lib/rotator.ts b/lib/rotator.ts index a9ae59bd..bc989867 100644 --- a/lib/rotator.ts +++ b/lib/rotator.ts @@ -1,31 +1,52 @@ import {getRandomProvider} from '.'; import {providerCache} from '../config'; import {BaseProvider, ExtractedInfo} from './providers/baseProvider'; -import {client} from './redis'; - -const redisClient = client; +import {client as redisClient} from './redis'; +/** + * Rotate provider. + * @param {BaseProvider} provider Provider instance + * @param {string} url Video TikTok URL + * @param {boolean?} noCache NoCache option + * @param {boolean?} skipOnError Rotate when error + * @return {Promise} + */ export const rotateProvider = async ( provider: BaseProvider, url: string, - noCache: boolean = false): + noCache: boolean = false, skipOnError: boolean = true): Promise => { +// await redisClient.del(url); +// console.log(provider.resourceName()); + if (provider.maintenance) { + return await rotateProvider(getRandomProvider(), url, noCache, skipOnError); + } const cachedData = await redisClient.get(url); - // await redisClient.del(url); if (!cachedData) { - const data = await provider.fetch(url); - if (data.error) { - // switching to other provider - return await rotateProvider(getRandomProvider(), url); - } else if (data.result && !data.result.urls.length) { - return await rotateProvider(getRandomProvider(), url); - } else { - if (!noCache) { - redisClient.set(url, - JSON.stringify( - {...data, provider: provider.resourceName()}), 'ex', - providerCache); + try { + const data = await provider.fetch(url); + if (data.error) { + // switching to other provider + return await rotateProvider(getRandomProvider(), url); + } else if (data.video && !data.video.urls.length) { + return await rotateProvider(getRandomProvider(), url); + } else { + if (!noCache) { + redisClient.set(url, + JSON.stringify( + {...data, provider: provider.resourceName()}), 'ex', + providerCache); + } + return {...data, provider: provider.resourceName()}; + } + } catch (e) { + if (skipOnError) { + return await rotateProvider(getRandomProvider(), url); + } else { + return { + error: (e as Error).message, + provider: provider.resourceName(), + }; } - return {...data, provider: provider.resourceName()}; } } else { return JSON.parse(cachedData);