@kelvdra/scraper
Version:
A simple scraper by kelvdra.
755 lines (660 loc) • 22.6 kB
JavaScript
const axios = require('axios');
const yts = require('yt-search');
const { createDecipheriv } = require('crypto');
const cheerio = require('cheerio');
const formData = require('form-data');
const { lookup } = require('mime-types');
const qs = require('qs');
async function Tiktok(url) {
try {
const postData = qs.stringify({
url: url,
count: 12,
cursor: 0,
web: 1,
hd: 1
});
const headers = {
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
'Accept': 'application/json, text/javascript, */*; q=0.01',
'X-Requested-With': 'XMLHttpRequest'
};
const res = await axios.post('https://www.tikwm.com/api/', postData, { headers });
const data = res.data.data;
const videoId = data.id;
const isSlide = Array.isArray(data.images) && data.images.length > 0;
const result = {
status: true,
creator: '@kelvdra/scraper',
result: {
title: data.title,
taken_at: formatDate(data.create_time),
region: data.region,
id: videoId,
durations: data.duration || 0,
duration: (data.duration ? `${data.duration} Seconds` : 'Photo Slide'),
cover: `https://www.tikwm.com/video/cover/${videoId}.webp`,
size_nowm: data.size || 0,
size_nowm_hd: data.hd_size || 0,
data: isSlide
? data.images.map((url, i) => ({
type: `slide_${i + 1}`,
url: url
}))
: [
{
type: "nowatermark",
url: `https://www.tikwm.com/video/media/play/${videoId}.mp4`
},
{
type: "nowatermark_hd",
url: `https://www.tikwm.com/video/media/hdplay/${videoId}.mp4`
}
],
music_info: {
id: data.music_info?.id || "",
title: data.music_info?.title || "-",
author: data.music_info?.author || "-",
album: data.music_info?.album || "Unknown",
url: `https://www.tikwm.com/video/music/${videoId}.mp3`
},
stats: {
views: formatNumber(data.play_count),
likes: formatNumber(data.digg_count),
comment: formatNumber(data.comment_count),
share: formatNumber(data.share_count),
download: formatNumber(data.download_count)
},
author: {
id: data.author?.id || "",
fullname: data.author?.nickname || "",
nickname: data.author?.unique_id || "",
avatar: `https://www.tikwm.com/video/avatar/${videoId}.jpeg`
}
}
};
return result
} catch (err) {
return { status: false, message: err.message };
}
}
function formatNumber(num) {
if (num >= 1000000) return (num / 1000000).toFixed(2) + ' jt';
if (num >= 1000) return (num / 1000).toFixed(2) + ' rb';
return num.toString();
}
function formatDate(timestamp) {
const date = new Date(timestamp * 1000);
return date.toLocaleString('id-ID', {
weekday: 'long',
day: 'numeric',
month: 'long',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false
});
}
/**
* @param {string} url - URL YouTube
* @param {string} quality - Bisa resolusi (360, 480, 720, 1080) atau bitrate (mp3, opus, m4a, webm)
*/
// --- OceanSaver
const SUPPORTED_AUDIO_FORMATS = ['mp3', 'm4a', 'opus', 'webm'];
const SUPPORTED_VIDEO_QUALITIES = ['144', '240', '360', '480', '720', '1080'];
const ytdl = {
request: async (url, formatOrQuality) => {
try {
const encodedUrl = encodeURIComponent(url);
const isAudio = SUPPORTED_AUDIO_FORMATS.includes(formatOrQuality.toLowerCase());
const isVideo = SUPPORTED_VIDEO_QUALITIES.includes(formatOrQuality);
if (!isAudio && !isVideo) {
return {
status: false,
message: `Format/quality tidak valid. Format audio yang didukung: ${SUPPORTED_AUDIO_FORMATS.join(", ")}, resolusi video: ${SUPPORTED_VIDEO_QUALITIES.join(", ")}`
};
}
const type = isAudio ? 'audio' : 'video';
const formatParam = formatOrQuality;
const { data } = await axios.get(
`https://p.oceansaver.in/ajax/download.php?format=${formatParam}&url=${encodedUrl}`
);
if (!data.success || !data.id) {
return { status: false, message: 'Gagal mendapatkan task ID dari oceansaver.' };
}
return {
status: true,
taskId: data.id,
type,
quality: isAudio ? formatParam : `${formatParam}p`
};
} catch (error) {
return { status: false, message: `Request error: ${error.message}` };
}
},
convert: async (taskId) => {
try {
const { data } = await axios.get(
`https://p.oceansaver.in/api/progress?id=${taskId}`
);
return data;
} catch (error) {
return { success: false, message: `Convert error: ${error.message}` };
}
},
repeatRequest: async (taskId, type, quality) => {
for (let i = 0; i < 20; i++) {
const response = await ytdl.convert(taskId);
if (response && response.download_url) {
return {
status: true,
type,
quality,
url: response.download_url
};
}
await new Promise(resolve => setTimeout(resolve, 3000));
}
return { status: false, message: 'Timeout menunggu link download.' };
},
// Fungsi utama untuk dipanggil
download: async (url, formatOrQuality) => {
const init = await ytdl.request(url, formatOrQuality);
if (!init.status) return init;
return await ytdl.repeatRequest(init.taskId, init.type, init.quality);
}
};
// Y2mate
const y2mate = {
headers: {
"Referer": "https://y2mate.nu/",
"Origin": "https://y2mate.nu/",
"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36 Edg/137.0.0.0"
},
hit: async (url, description, returnType = "text") => {
try {
const listReturnType = ["text", "json"]
if (!listReturnType.includes(returnType)) throw Error(`return type ${returnType} is invalid. `)
let result
const response = await fetch(url, {
headers: y2mate.headers,
})
const data = await response.text()
result = data
if(!response.ok) throw Error(`${response.status} ${response.statusText}\n${data.split("\n").slice(0,4).join("\n") + "\n...." || null}`)
try {
if (returnType == listReturnType[1]) {
result = JSON.parse(data)
}
} catch (error) {
throw Error(`gagal mengubah return type menjadi ${returnType}. ${error.message}`)
}
return {result, response}
} catch (error) {
throw Error("hit gagal pada " + description
+ "\n" + error.message
)
}
},
getAuthCode: async () => {
console.log("[y2mate] downloading homepage")
const {result: html, response} = await y2mate.hit("https://y2mate.nu","hit homepage y2mate")
const valueOnHtml = html.match(/<script>(.*?)<\/script>/)?.[1]
if (!valueOnHtml) throw Error(`gagal mendapatkan match regex untuk code value di html`)
try {
eval(valueOnHtml)
} catch (error) {
throw Error(`eval lu gagal bos di yang eval valueOnHtml\n${error.message}`)
}
const srcPath = html.match(/src="(.*?)"/)?.[1]
if (!srcPath) throw Error(`gagal mendapatkan srcPath untuk download file javascript`)
const url = new URL(response.url).origin + srcPath
console.log("[y2mate] downloading js file")
const {result : jsCode} = await y2mate.hit(url, "download js file y2mate")
const authCode = jsCode.match(/authorization\(\){(.*?)}function/)?.[1]
if (!authCode) throw Error(`gagal mendapatkan match regex untuk auth function code`)
const newAuthCode = authCode.replace("id(\"y2mate\").src", `"${url}"`)
let authString
try {
authString = eval(`(()=>{${newAuthCode}})()`)
} catch (error) {
throw Error(`eval lu gagal bos pas nyoba buat dapetin authString\n${error.message}`)
}
return authString
},
getYoutubeId: async (youtubeUrl) => {
// capek gw regek jir wkwk bomat lah fetch head saja >:v
console.log("[youtube.com] get video id from your youtube url")
const headers = {
"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36 Edg/137.0.0.0"
}
const resp = await fetch(youtubeUrl, {
"method": "HEAD",
headers
})
if (!resp.ok) throw Error(`gagal mendapatkan id video ${resp.status} ${resp.statusText}`)
let videoId
videoId = new URL(resp.url)?.searchParams?.get("v")
// maybe short youtube
if (!videoId) {
videoId = resp.url.match(/https:\/\/www.youtube.com\/shorts\/(.*?)(?:\?|$)/)?.[1]
if (!videoId) throw Error(`bruh lu kirim link apa tuh bro`)
}
return { videoId, url : resp.url }
},
download: async (youtubeUrl, format="mp3") => {
const validFormats = ["mp3","mp4"]
if(!validFormats.includes(format)) throw Error (`${format} is invalid format. available format ${validFormats.join(", ")}`)
const delay = async (ms) => new Promise(r => setTimeout(r,ms))
const { videoId, url } = await y2mate.getYoutubeId(youtubeUrl)
const authCode = await y2mate.getAuthCode()
console.log("[y2mate] hit init api")
const url1 = `https://d.ecoe.cc/api/v1/init?a=${authCode}&_=${Math.random()}`
const {result: resultInit} = await y2mate.hit(url1, "init api", "json")
if (resultInit.error != "0") throw Error (`ada error di init api. proses di hentikan\n${resultInit}`)
console.log("[y2mate] hit convert url")
const url2 = new URL (resultInit.convertURL)
url2.searchParams.append("v",videoId)
url2.searchParams.append("f", format)
url2.searchParams.append("_", Math.random())
const {result : resultConvert} = await y2mate.hit(url2, "hit convert", "json")
let { downloadURL, progressURL, redirectURL, error: errorFromConvertUrl } = resultConvert
if (errorFromConvertUrl) throw Error(`there was error found after fetch convertURL probably bad youtube video id`)
if (redirectURL) {
({ downloadURL, progressURL } = (await y2mate.hit(redirectURL, "fetch redirectURL","json")).result)
console.log(`[y2mate] got directed`)
}
let { error, progress, title } = {}
while (progress != 3) {
const api3 = new URL(progressURL)
api3.searchParams.append("_", Math.random());
({ error, progress, title } = (await y2mate.hit(api3, "cek progressURL", "json")).result)
let status = progress == 3 ? "UwU sukses 🎉" :
progress == 2 ? "(👉゚ヮ゚)👉 poke server" :
progress == 1 ? "(👉゚ヮ゚)👉 poke server" :
progress == 0 ? "(👉゚ヮ゚)👉 poke server" : "❌ tetot"
console.log(status)
if (error) throw Error(`there was an error value while doing loop check. the error code is ${error}. probably the video is too looong. or not compatible or > 45 mins`)
if (progress != 3) await delay(5000)
}
const result = { title, downloadURL, url }
return result
},
}
const ytmp3v2 = async (link, format = "mp3") => {
try {
const info = await yts(link);
const result = await y2mate.download(link, format);
return {
status: true,
creator: '@kelvdra/scraper',
metadata: info.all[0],
download: result
};
} catch (e) {
return { status: false, message: e.message };
}
};
const ytmp3 = async (link, quality = "mp3") => {
try {
const info = await yts(link);
const result = await ytdl.download(link, quality);
return {
status: true,
creator: '@kelvdra/scraper',
metadata: info.all[0],
download: result
};
} catch (e) {
return { status: false, message: e.message };
}
};
const ytmp4v2 = async (link, quality = "mp4") => {
if (!link.includes('youtube.com') && !link.includes('youtu.be')) {
return { status: false, message: 'URL YouTube tidak valid' };
}
try {
const info = await yts(link);
const data = await y2mate.download(link, quality);
return {
status: true,
creator: '@kelvdra/scraper',
metadata: info.all[0],
download: data
};
} catch (e) {
return { status: false, message: e.message };
}
};
const ytmp4 = async (link, quality = "360") => {
if (!link.includes('youtube.com') && !link.includes('youtu.be')) {
return { status: false, message: 'URL YouTube tidak valid' };
}
try {
const info = await yts(link);
const data = await ytdl.download(link, quality);
return {
status: true,
creator: '@kelvdra/scraper',
metadata: info.all[0],
download: data
};
} catch (e) {
return { status: false, message: e.message };
}
};
const transcript = async (url) => {
try {
let res = await axios.get('https://yts.kooska.xyz/', {
params: { url: url },
headers: {
'Content-Type': 'application/json',
'User-Agent': 'Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Mobile Safari/537.36',
'Referer': 'https://kooska.xyz/'
}
}).then(i=>i.data)
return {
status: true,
creator: "@kelvdra/scraper",
video_id: res.video_id,
summarize: res.ai_response,
transcript: res.transcript
}
} catch(e) {
return {
status: false,
msg: `Gagal mendapatkan respon, dengan pesan: ${e.message}`
}
}
}
const playmp3 = async (query, quality = "mp3") => {
try {
const searchResult = await search(query);
if (!searchResult.status || !searchResult.results.length)
return { status: false, message: 'Video tidak ditemukan' };
const results = [];
for (let video of searchResult.results.slice(0, 5)) {
const downloadInfo = await ytdl.download(video.url, quality)
results.push({
title: video.title,
author: video.author.name,
duration: video.timestamp,
url: video.url,
thumbnail: video.thumbnail,
download: downloadInfo
});
}
return {
status: true,
creator: '@kelvdra/scraper',
type: 'audio',
results
};
} catch (err) {
return { status: false, message: err.message };
}
};
const playmp4 = async (query, quality = "360") => {
try {
const searchResult = await search(query);
if (!searchResult.status || !searchResult.results.length)
return { status: false, message: 'Video tidak ditemukan' };
const results = [];
for (let video of searchResult.results.slice(0, 5)) {
const downloadInfo = await ytdl.download(video.url, quality);
results.push({
title: video.title,
author: video.author.name,
duration: video.timestamp,
url: video.url,
thumbnail: video.thumbnail,
download: downloadInfo
});
}
return {
status: true,
creator: '@kelvdra/scraper',
type: 'video',
results
};
} catch (err) {
return { status: false, message: err.message };
}
};
const pindl = async (url) => {
try {
let a = await axios.get(url, {
headers: {
'User-Agent': "Mozilla/5.0 (Linux; Android 12; SAMSUNG SM-S908B) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/17.0 Chrome/96.0.4664.104 Mobile Safari/537.36",
'Accept-Language': 'id-ID,id;q=0.9,en-US;q=0.8,en;q=0.7',
}
});
let $ = cheerio.load(a.data);
let x = $('script[data-test-id="leaf-snippet"]').text();
let y = $('script[data-test-id="video-snippet"]').text();
let g = {
status: true,
creator: "@kelvdra/scraper",
isVideo: !!y,
info: JSON.parse(x),
image: JSON.parse(x).image,
video: y ? JSON.parse(y).contentUrl : ''
};
return g;
} catch (e) {
return {
status: false,
mess: "failed download"
};
}
};
const igdl = async (url) => {
try {
let result = {
status: true,
creator: "@kelvdra/scraper",
media: []
}
const {
data
} = await axios(`https://www.y2mate.com/mates/analyzeV2/ajax`, {
method: "post",
data: {
k_query: url,
k_page: "Instagram",
hl: "id",
q_auto: 0
},
headers: {
"content-type": "application/x-www-form-urlencoded",
"user-agent": "PostmanRuntime/7.32.2"
}
})
await data.links.video.map((video) => result.media.push(video.url))
return result
} catch (err) {
const result = {
status: false,
message: `Media not found`
}
return result
}
}
const mfdl = async (url) => {
try {
const res = await fetch(`https://rianofc-bypass.hf.space/scrape?url=${encodeURIComponent(url)}`);
const html = await res.json();
const $ = cheerio.load(html.html);
const result = {
filename: $('.dl-info').find('.intro .filename').text().trim(),
type: $('.dl-btn-label').find('.filetype > span').text().trim(),
size: $('.details li:contains("File size:") span').text().trim(),
uploaded: $('.details li:contains("Uploaded:") span').text().trim(),
ext: /\.(.*?)/.exec($('.dl-info').find('.filetype > span').eq(1).text())?.[1]?.trim() || 'bin',
download: $('.input').attr('href')
};
result.mimetype = lookup(result.ext.toLowerCase()) || 'application/octet-stream';
return {
status: true,
creator: '@kelvdra/scraper',
result
};
} catch (err) {
return {
status: false,
message: err.message
};
}
}
const fbPhoto = async (url) => {
try {
async function getNonce() {
const { data: nonce } = await axios.get(
'https://thefdownloader.com/facebook-photo-downloader/',
)
const _ = cheerio.load(nonce)
const skripKontent = _('#hmd-facebook-downloader-js-extra').html()
const match = /"nonce":"([a-zA-Z0-9]+)"/.exec(skripKontent)
return match?.[1]
}
const nonce = await getNonce()
const base = {
url: {
admin: 'https://thefdownloader.com/wp-admin/admin-ajax.php',
},
}
let data = new FormData()
data.append('action', 'facebook_photo_action')
data.append('facebook', `facebook_photo_url=${url}`)
data.append('nonce', nonce)
let response = await axios.post(base.url.admin, data, {
headers: {
...data.getHeaders(),
},
})
let $ = cheerio.load(response.data)
let imageUrl = $('.facebook__media img').attr('src')
return {
status: true,
creator: "@kelvdra/scraper",
imageUrl
}
} catch (err) {
return {
status: false,
message: err.message
};
}
}
const fbVideo = async (u) => {
try {
if (!/^https?:\/\/(www\.)?facebook\.com/.test(u)) {
throw new Error('Invalid Facebook URL');
}
function generatePayload(y) {
return { id: y, locale: 'id' };
}
const pylox = generatePayload(u);
const { data } = await axios.post('https://getmyfb.com/process', pylox);
const $ = cheerio.load(data);
const downloadLinks = [];
const items = $('.results-list-item');
if (!items.length) {
throw new Error('No download links found. The video might be private or unavailable.');
}
items.each((_, el) => {
const quality = $(el).text().trim();
const link = $(el).find('a').attr('href');
const filename = $(el).find('a').attr('download');
if (link) {
downloadLinks.push({ quality, link, ...(filename && { filename }) });
}
});
return {
status: true,
creator: "@kelvdra/scraper",
downloadLinks
}
} catch (err) {
return {
status: false,
message: err.message
};
}
};
const ttdl = async (tiktokUrl) => {
try {
const hydra = await Tiktok(tiktokUrl)
const payload = new URLSearchParams();
payload.append('q', tiktokUrl);
payload.append('lang', 'en');
payload.append('cftoken', '');
const res = await axios.post(
'https://savetik.co/api/ajaxSearch',
payload.toString(),
{
headers: {
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
'X-Requested-With': 'XMLHttpRequest',
},
}
);
const data = res.data.data;
const $ = cheerio.load(data);
const title = $('.content h3').text().trim();
const thumbnail = $('.thumbnail img').attr('src') || null;
const videoDownloads = [];
$('.dl-action a').each((i, el) => {
const type = $(el).text().trim().toLowerCase();
const url = $(el).attr('href');
if (url && url.startsWith('http')) {
videoDownloads.push({ type, url });
}
});
const photos = [];
$('.photo-list .download-box li').each((i, el) => {
const img = $(el).find('img').attr('src');
const url = $(el).find('a').attr('href');
if (img && url) {
photos.push({ preview: img, url });
}
});
return {
status: true,
creator: "@kelvdra/scraper",
title,
taken_at: hydra.result.taken_at,
region: hydra.result.region,
id: hydra.result.id,
durations: hydra.result.durations || 0,
duration: hydra.result.duration,
cover: hydra.result.cover,
thumbnail,
video: videoDownloads,
photos,
music_info: hydra.result.music_info,
stats: hydra.result.stats,
author: hydra.result.author
};
} catch (err) {
return {
status: false,
message: err.message
};
}
}
module.exports = {
ttdl,
playmp3,
playmp4,
ytmp3v2,
ytmp4v2,
ytmp3,
ytmp4,
transcript,
pindl,
igdl,
mfdl,
fbPhoto,
fbVideo
};