diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/index.ts | 61 | ||||
| -rw-r--r-- | src/instagram_task.ts | 134 | ||||
| -rw-r--r-- | src/tasks.ts | 44 | ||||
| -rw-r--r-- | src/tiktok_task.ts | 147 | ||||
| -rw-r--r-- | src/utils.ts | 17 |
5 files changed, 73 insertions, 330 deletions
diff --git a/src/index.ts b/src/index.ts index 0b13af9..a246292 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,10 +1,13 @@ -import { addTask, getTask, numOfTasksInQueue } from "./tasks"; -import { runDownloadInstagramTask } from "./instagram_task"; +import { + addTask, + getTask, + numOfTasksInQueue, + runDownloadUrlTask, +} from "./tasks"; import { runWithRetries, wait } from "./utils"; import { AppOptions, App as SlackApp } from "@slack/bolt"; import fs from "fs"; import dotenv from "dotenv"; -import { runDownloadTiktokTask } from "./tiktok_task"; dotenv.config(); const QUEUE_MAX_LENGTH = process.env.QUEUE_MAX_LENGTH @@ -36,12 +39,10 @@ app.command("/veeti", async ({ command, ack, respond, client }) => { console.log("Received command: /veeti", command.text); const url = command.text.trim(); - const respondWithFile = async (filePath: string) => { + const respondWithFile = async (fileName: string) => { try { - console.log("Uploading file", filePath); - const fileContent = fs.readFileSync(filePath); - const extension = filePath.split(".").pop(); - const fileName = `veeti.${extension}`; + console.log("Uploading file", fileName); + const fileContent = fs.readFileSync(`downloads/${fileName}`); console.log("Sending message to channel", command.channel_id); await client.filesUploadV2({ channel_id: command.channel_id, @@ -70,38 +71,12 @@ app.command("/veeti", async ({ command, ack, respond, client }) => { }); }; - if (url.includes("instagram.com")) { - void respond({ - text: "Sharing instagram video, this can take a minute...", - thread_ts: command.ts, - response_type: "ephemeral", - }); - - addTask({ - type: "download_instagram", - url, - respondWithFile, - respondTaskFailed, - }); - } else if (url.includes("tiktok.com")) { - void respond({ - text: "Sharing tiktok video, this can take a minute...", - thread_ts: command.ts, - response_type: "ephemeral", - }); - - addTask({ - type: "download_tiktok", - url, - respondWithFile, - respondTaskFailed, - }); - } else { - void respond({ - text: "Invalid URL, please provide an Instagram or TikTok video URL.", - response_type: "ephemeral", - }); - } + addTask({ + type: "download_url", + url, + respondWithFile, + respondTaskFailed, + }); }); const taskLoop = async () => { @@ -112,10 +87,8 @@ const taskLoop = async () => { if (task) { try { await runWithRetries(async () => { - if (task.type === "download_instagram") { - await runDownloadInstagramTask(task); - } else if (task.type === "download_tiktok") { - await runDownloadTiktokTask(task); + if (task.type === "download_url") { + await runDownloadUrlTask(task); } else { console.log("Unknown task type, skipping", task); } diff --git a/src/instagram_task.ts b/src/instagram_task.ts deleted file mode 100644 index 2c6c6c8..0000000 --- a/src/instagram_task.ts +++ /dev/null @@ -1,134 +0,0 @@ -import fs from "fs"; -import puppeteer, { TimeoutError } from "puppeteer"; -import { wait } from "./utils"; -import { getBrowser } from "./browser"; - -export interface InstagramTask { - type: "download_instagram"; - url: string; - respondWithFile: (filePath: string) => Promise<void>; - respondTaskFailed: (err: unknown) => Promise<void>; -} - -export const runDownloadInstagramTask = async (task: InstagramTask) => { - const browser = await getBrowser(); - const page = await browser.newPage(); - const client = await page.createCDPSession(); - await client.send("Page.setDownloadBehavior", { - behavior: "allow", - downloadPath: "./downloads", - }); - - try { - console.log("Starting task:", task.url); - - const downloadDirFiles = fs.readdirSync("./downloads"); - console.log("Emptying downloads folder"); - downloadDirFiles.forEach((file) => { - fs.unlinkSync(`./downloads/${file}`); - }); - - await page.goto("https://snapins.ai/"); - - try { - const consent = await page.waitForSelector("button.fc-cta-consent", { - timeout: 3000, - }); - if (consent) { - console.log("Looks like a cookie consent dialog, clicking agree"); - await consent.click(); - } - } catch (e) { - if (e instanceof TimeoutError) { - console.log("No cookie consent dialog found"); - } else { - throw e; - } - } - - console.log("Waiting for a bit"); - await wait(1000); - console.log("Done waiting"); - - // try { - // const adModalClose = await page.waitForSelector("#adOverlay button", { - // timeout: 3000, - // }); - // console.log("Looks like an ad modal, clicking close"); - // await adModalClose?.click(); - // } catch (e) { - // if (e instanceof TimeoutError) { - // console.log("No ad modal"); - // } else { - // throw e; - // } - // } - - try { - const secondAdModalClose = await page.waitForSelector("#dismiss-button", { - timeout: 3000, - }); - console.log("Looks like a second ad modal, clicking close"); - await secondAdModalClose?.click(); - } catch (e) { - if (e instanceof TimeoutError) { - console.log("No second ad modal"); - } else { - throw e; - } - } - - await page.type("input#input-url", task.url); - await page.click("#submit-btn"); - - const downloadButton = await page.waitForSelector("a[download]", { - timeout: 10_000, - }); - if (!downloadButton) { - throw new Error("Could not find download button"); - } - - console.log("Found download button, pressing enter"); - await downloadButton?.scrollIntoView(); - await downloadButton?.focus(); - await downloadButton?.press("Enter"); - - console.log("Waiting to see if one more ad pops up"); - await wait(1000); - - // Click outside shadow-dommed ad modal to close it and start the download - console.log( - "Clicking outside the possible ad modal to close it and start the download", - ); - await page.mouse.click(10, 10); - - console.log("Waiting for download to start"); - await wait(3000); - - let downloadTimeout = 5 * 60 * 1000; // 5 minutes - let downloadedFiles = fs.readdirSync("./downloads"); - while (downloadedFiles.some((file) => file.endsWith(".crdownload"))) { - if (downloadTimeout <= 0) { - throw new Error("Download timed out"); - } - - console.log("Download still in progress, waiting"); - await wait(1000); - downloadTimeout -= 1000; - downloadedFiles = fs.readdirSync("./downloads"); - } - - console.log("Downloaded files", downloadedFiles); - if (downloadedFiles.length === 0) { - throw new Error("No files downloaded"); - } else if (downloadedFiles.length > 1) { - throw new Error("More than one file downloaded, not sure what to do"); - } - - const downloadedFile = downloadedFiles[0]; - - await task.respondWithFile(`./downloads/${downloadedFile}`); - } finally { - await page.close(); - } -}; diff --git a/src/tasks.ts b/src/tasks.ts index 8fb1b2f..0646da5 100644 --- a/src/tasks.ts +++ b/src/tasks.ts @@ -1,8 +1,44 @@ -import { InstagramTask } from "./instagram_task"; -import { TikTokTask } from "./tiktok_task"; +import { asyncExec } from "./utils"; +import fs from "fs"; +export const checkIfYtdlpIsInstalled = async () => { + try { + await asyncExec("yt-dlp --version"); + return true; + } catch (e) { + console.error("yt-dlp is not installed or available in PATH", e); + return false; + } +}; + +export const runDownloadUrlTask = async (task: Task) => { + try { + const filename = "veeti.mp4"; + // remove the file if it exists + try { + fs.rmSync(filename); + } catch (e: any) { + if (e.code !== "ENOENT") { + console.error("Error deleting file", e); + } + } -// union of possible task types -export type Task = InstagramTask | TikTokTask; + const { stdout } = await asyncExec( + `yt-dlp -o "downloads/${filename}" "${task.url}"`, + ); + console.log(stdout); + task.respondWithFile(filename); + } catch (e) { + console.error("Error downloading URL", e); + throw e; + } +}; + +export type Task = { + url: string; + type: "download_url"; + respondWithFile: (file: string) => void; + respondTaskFailed: (err: unknown) => void; +}; const taskQueue: Task[] = []; diff --git a/src/tiktok_task.ts b/src/tiktok_task.ts deleted file mode 100644 index 15e30e0..0000000 --- a/src/tiktok_task.ts +++ /dev/null @@ -1,147 +0,0 @@ -import fs from "fs"; -import puppeteer, { TimeoutError } from "puppeteer"; -import { wait } from "./utils"; -import { getBrowser } from "./browser"; - -export interface TikTokTask { - type: "download_tiktok"; - url: string; - respondWithFile: (filePath: string) => Promise<void>; - respondTaskFailed: (err: unknown) => Promise<void>; -} - -export const runDownloadTiktokTask = async (task: TikTokTask) => { - const browser = await getBrowser(); - const page = await browser.newPage(); - const client = await page.createCDPSession(); - await client.send("Page.setDownloadBehavior", { - behavior: "allow", - downloadPath: "./downloads", - }); - - try { - console.log("Starting task:", task.url); - - const downloadDirFiles = fs.readdirSync("./downloads"); - console.log("Emptying downloads folder"); - downloadDirFiles.forEach((file) => { - fs.unlinkSync(`./downloads/${file}`); - }); - - await page.goto("https://snaptik.app/"); - - try { - const consent = await page.waitForSelector("button.fc-cta-consent", { - timeout: 3000, - }); - if (consent) { - console.log("Looks like a cookie consent dialog, clicking agree"); - await consent.click(); - } - } catch (e) { - if (e instanceof TimeoutError) { - console.log("No cookie consent dialog found"); - } else { - throw e; - } - } - - try { - const cont = await page.waitForSelector("button.continue-web", { - timeout: 3000, - }); - if (cont) { - console.log( - "Looks like a 'continue using the web' dialog, clicking continue", - ); - await cont.click(); - } - } catch (e) { - if (e instanceof TimeoutError) { - console.log("No 'continue using the web' dialog found"); - } else { - throw e; - } - } - - await page.type("input#url", task.url); - await page.click('button[type="submit"]'); - - console.log("Waiting for a bit"); - await wait(1000); - console.log("Done waiting"); - - // try { - // const adModalClose = await page.waitForSelector("#adOverlay button", { - // timeout: 3000, - // }); - // console.log("Looks like an ad modal, clicking close"); - // await adModalClose?.click(); - // } catch (e) { - // if (e instanceof TimeoutError) { - // console.log("No ad modal"); - // } else { - // throw e; - // } - // } - - // try { - // const secondAdModalClose = await page.waitForSelector("#dismiss-button", { - // timeout: 3000, - // }); - // console.log("Looks like a second ad modal, clicking close"); - // await secondAdModalClose?.click(); - // } catch (e) { - // if (e instanceof TimeoutError) { - // console.log("No second ad modal"); - // } else { - // throw e; - // } - // } - - const downloadButton = await page.waitForSelector( - "a[data-event=server01_file]", - { timeout: 10_000 }, - ); - if (!downloadButton) { - throw new Error("Could not find download button"); - } - - console.log("Found download button, pressing enter"); - await downloadButton?.scrollIntoView(); - await downloadButton?.focus(); - await downloadButton?.press("Enter"); - - console.log("Waiting to see if one more ad pops up"); - await wait(1000); - - // Click outside shadow-dommed ad modal to close it and start the download - // console.log( - // "Clicking outside the possible ad modal to close it and start the download" - // ); - // await page.mouse.click(10, 10); - - console.log("Waiting for download to start"); - await wait(3000); - - let downloadedFiles = fs.readdirSync("./downloads"); - while (downloadedFiles.some((file) => file.endsWith(".crdownload"))) { - console.log("Download still in progress, waiting"); - await wait(1000); - downloadedFiles = fs.readdirSync("./downloads"); - } - - console.log("Downloaded files", downloadedFiles); - if (downloadedFiles.length === 0) { - throw new Error("No files downloaded"); - } else if (downloadedFiles.length > 1) { - throw new Error("More than one file downloaded, not sure what to do"); - } - - const downloadedFile = downloadedFiles[0]; - - await task.respondWithFile(`./downloads/${downloadedFile}`); - } finally { - await page.close(); - } -}; diff --git a/src/utils.ts b/src/utils.ts index b9b5cfa..fd8a26d 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,10 +1,25 @@ +import { exec } from "child_process"; + +export const asyncExec = (command: string) => { + return new Promise<{ stdout: string; stderr: string }>((resolve, reject) => { + exec(command, (error, stdout, stderr) => { + if (error) { + console.error(`exec error: ${error}`); + reject(error); + } else { + resolve({ stdout, stderr }); + } + }); + }); +}; + export const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); export const runWithRetries = async <T>( fn: () => Promise<T>, retries: number, - backoff: number = 2000 + backoff: number = 2000, ): Promise<T> => { let attempts = 0; while (true) { |
