2023-03-03 16:44:40 +00:00
const fs = require ( 'node:fs' )
2023-10-03 19:29:44 +00:00
const crypto = require ( 'node:crypto' )
const { RedisRateLimiter } = require ( 'rolling-rate-limiter' )
2024-03-30 19:05:11 +00:00
const rtm = require ( 'readable-to-ms' )
2023-03-03 16:44:40 +00:00
const upload = require ( '../utils/upload.js' )
const ytdlp = require ( '../utils/ytdlp.js' )
const redis = require ( '../utils/redis.js' )
const validate = require ( '../utils/validate.js' )
const metadata = require ( '../utils/metadata.js' )
const websocket = require ( '../utils/websocket.js' )
const captcha = require ( "../utils/captcha.js" )
const logger = require ( "../utils/logger.js" )
const { PrismaClient } = require ( '@prisma/client' )
const prisma = new PrismaClient ( )
2023-10-03 19:29:44 +00:00
const limiter = new RedisRateLimiter ( {
client : redis ,
namespace : 'autodownload:' ,
interval : 24 * 60 * 60 * 1000 ,
maxInInterval : 5
} )
2023-03-03 16:44:40 +00:00
exports . save = async ( ws , req ) => {
2023-06-28 20:06:58 +00:00
logger . info ( { message : ` ${ req . path } ${ JSON . stringify ( req . query ) } ` } )
2023-03-03 16:44:40 +00:00
const id = await validate . validateVideoInput ( req . query . url )
if ( id . fail ) {
ws . send ( ` ERROR - ${ id . message } ` )
return ws . close ( )
}
if ( await redis . get ( id ) ) {
ws . send ( 'DATA - Someone is already downloading this video...' )
2023-03-25 13:13:52 +00:00
return ws . close ( )
}
if ( await redis . get ( ` blacklist: ${ id } ` ) ) {
ws . send ( 'DATA - You can\'t download that. The video is blacklisted.' )
return ws . close ( )
2023-03-03 16:44:40 +00:00
}
const already = await prisma . videos . findFirst ( {
where : {
id : id
}
} )
if ( already ) return ws . send ( ` DONE - ${ process . env . FRONTEND } /watch?v= ${ id } ` )
2024-01-18 18:54:35 +00:00
ws . send ( 'DATA - This process is automatic. Your video will start archiving shortly.' )
ws . send ( 'CAPTCHA - Solving a cryptographic challenge before downloading.' )
2023-03-03 16:44:40 +00:00
ws . on ( 'message' , async function ( msg ) {
2023-09-30 15:32:52 +00:00
if ( msg == 'alive' ) return
2023-03-03 16:44:40 +00:00
if ( await redis . get ( id ) != 'downloading' ) {
2024-06-07 16:13:45 +00:00
await redis . set ( id , 'downloading' , 'EX' , 300 )
2024-06-29 09:23:05 +00:00
const confirm = await captcha . checkCaptcha ( msg )
2023-03-03 16:44:40 +00:00
if ( confirm ) startDownloading ( )
else {
2023-03-05 10:44:45 +00:00
await redis . del ( id )
2023-03-03 16:44:40 +00:00
ws . send ( 'DATA - You little goofy goober tried to mess with the captcha...' )
ws . close ( )
}
} else {
ws . send ( 'DATA - You already sent captcha reply...' )
}
} )
async function startDownloading ( ) {
2024-03-29 14:32:12 +00:00
const download = await ytdlp . downloadVideo ( ` https://www.youtube.com/watch?v= ${ id } ` , ws , id )
2023-03-03 16:44:40 +00:00
if ( download . fail ) {
await redis . del ( id )
ws . send ( ` DATA - ${ download . message } ` )
ws . close ( )
} else {
const file = fs . readdirSync ( "videos" ) . find ( f => f . includes ( id ) )
2024-03-29 17:48:59 +00:00
if ( file ) {
2024-01-01 17:04:49 +00:00
ws . send ( 'DATA - Uploading file...' )
2024-03-30 13:55:10 +00:00
const videoUrl = await upload . uploadVideo ( ` ./videos/ ${ id } .mp4 ` )
fs . unlinkSync ( ` ./videos/ ${ id } .mp4 ` )
2024-01-01 17:04:49 +00:00
2024-05-25 13:34:00 +00:00
const uploaded = await websocket . createDatabaseVideo ( id , videoUrl )
2024-05-25 13:37:12 +00:00
if ( uploaded != 'success' ) {
2024-05-25 13:34:00 +00:00
ws . send ( ` DATA - Error while uploading - ${ JSON . stringify ( uploaded ) } ` )
} else {
ws . send ( ` DONE - ${ process . env . FRONTEND } /watch?v= ${ id } ` )
}
2023-03-03 16:44:40 +00:00
}
await redis . del ( id )
ws . close ( ) ;
}
}
}
exports . playlist = async ( ws , req ) => {
2023-06-28 20:06:58 +00:00
logger . info ( { message : ` ${ req . path } ${ JSON . stringify ( req . query ) } ` } )
2023-03-03 16:44:40 +00:00
const playlistId = await validate . validatePlaylistInput ( req . query . url )
if ( playlistId . fail ) {
ws . send ( ` ERROR - ${ playlistId . message } ` )
return ws . close ( )
}
let status = 'captcha'
2024-01-18 18:54:35 +00:00
ws . send ( 'DATA - This process is automatic. Your video will start archiving shortly.' )
ws . send ( 'CAPTCHA - Solving a cryptographic challenge before downloading.' )
2023-03-03 16:44:40 +00:00
ws . on ( 'message' , async function ( msg ) {
2023-09-30 15:32:52 +00:00
if ( msg == 'alive' ) return
2023-03-03 16:44:40 +00:00
if ( status == 'captcha' ) {
status = 'downloading'
const confirm = await captcha . checkCaptcha ( msg )
if ( confirm ) startDownloading ( )
else {
2023-03-05 10:44:45 +00:00
await redis . del ( id )
2023-03-03 16:44:40 +00:00
ws . send ( 'DATA - You little goofy goober tried to mess with the captcha...' )
ws . close ( )
}
} else {
ws . send ( 'DATA - You already sent captcha reply...' )
}
} )
async function startDownloading ( ) {
2023-03-24 14:20:06 +00:00
const playlist = await metadata . getPlaylistVideos ( playlistId )
2024-03-30 15:51:13 +00:00
for ( video of playlist . relatedStreams . slice ( 0 , 5 ) ) {
2023-07-07 19:31:25 +00:00
if ( ws . readyState !== ws . OPEN ) {
return logger . info ( { message : ` Stopped downloading ${ playlistId } , websocket is closed ` } )
}
2023-03-03 16:44:40 +00:00
const id = video . url . match ( /[?&]v=([^&]+)/ ) [ 1 ]
const already = await prisma . videos . findFirst ( {
where : {
id : id
}
} )
if ( already ) {
ws . send ( ` DATA - Already downloaded ${ video . title } ` )
await prisma . videos . updateMany ( {
where : {
id : id
} ,
data : {
playlist : playlistId
}
} )
continue
}
if ( await redis . get ( id ) ) {
ws . send ( ` DATA - Someone is already downloading ${ video . title } , skipping. ` )
continue
}
2023-03-25 13:13:52 +00:00
if ( await redis . get ( ` blacklist: ${ id } ` ) ) {
ws . send ( ` DATA - ${ video . title } is blacklisted from downloading, skipping ` )
continue
}
2023-03-03 16:44:40 +00:00
ws . send ( ` INFO - Downloading ${ video . title } <br><br> ` )
2024-06-07 16:13:45 +00:00
await redis . set ( id , 'downloading' , 'EX' , 300 )
2023-03-03 16:44:40 +00:00
2024-03-29 14:32:12 +00:00
const download = await ytdlp . downloadVideo ( 'https://www.youtube.com' + video . url , ws , id )
2023-03-03 16:44:40 +00:00
if ( download . fail ) {
ws . send ( ` DATA - ${ download . message } ` )
await redis . del ( id )
continue
} else {
const file = fs . readdirSync ( "./videos" ) . find ( f => f . includes ( id ) )
if ( file ) {
2023-07-07 19:31:25 +00:00
try {
ws . send ( ` DATA - Downloaded ${ video . title } ` )
ws . send ( ` DATA - Uploading ${ video . title } ` )
2024-03-30 13:55:10 +00:00
const videoUrl = await upload . uploadVideo ( ` ./videos/ ${ id } .mp4 ` )
2023-07-07 19:31:25 +00:00
ws . send ( ` DATA - Uploaded ${ video . title } ` )
2024-03-30 13:55:10 +00:00
fs . unlinkSync ( ` ./videos/ ${ id } .mp4 ` )
2023-07-07 19:31:25 +00:00
await websocket . createDatabaseVideo ( id , videoUrl , playlistId )
ws . send ( ` DATA - Created video page for ${ video . title } ` )
} catch ( e ) {
ws . send ( ` DATA - Failed downloading video ${ video . title } . Going to next video ` )
logger . error ( e )
}
2023-03-03 16:44:40 +00:00
} else {
ws . send ( ` DATA - Failed to find file for ${ video . title } . Going to next video in the playlist ` )
}
2023-03-10 18:24:51 +00:00
await redis . del ( id )
2023-03-03 16:44:40 +00:00
}
}
ws . send ( ` DONE - ${ process . env . FRONTEND } /playlist?list= ${ playlistId } ` )
}
2023-03-05 14:38:25 +00:00
}
exports . channel = async ( ws , req ) => {
2023-06-28 20:06:58 +00:00
logger . info ( { message : ` ${ req . path } ${ JSON . stringify ( req . query ) } ` } )
2023-03-05 14:38:25 +00:00
const channelId = await validate . validateChannelInput ( req . query . url )
if ( channelId . fail ) {
ws . send ( ` ERROR - ${ channelId . message } ` )
return ws . close ( )
}
let status = 'captcha'
2024-01-18 18:54:35 +00:00
ws . send ( 'DATA - This process is automatic. Your video will start archiving shortly.' )
ws . send ( 'CAPTCHA - Solving a cryptographic challenge before downloading.' )
2023-03-05 14:38:25 +00:00
ws . on ( 'message' , async function ( msg ) {
2023-09-30 15:32:52 +00:00
if ( msg == 'alive' ) return
2023-03-05 14:38:25 +00:00
if ( status == 'captcha' ) {
status = 'downloading'
const confirm = await captcha . checkCaptcha ( msg )
if ( confirm ) startDownloading ( )
else {
ws . send ( 'DATA - You little goofy goober tried to mess with the captcha...' )
ws . close ( )
}
} else {
ws . send ( 'DATA - You already sent captcha reply...' )
}
} )
async function startDownloading ( ) {
2023-03-24 14:20:06 +00:00
const videos = await metadata . getChannelVideos ( channelId )
2023-03-23 16:01:46 +00:00
2024-03-30 19:05:11 +00:00
for ( const video of videos . slice ( 0 , 5 ) ) {
2023-07-07 19:31:25 +00:00
if ( ws . readyState !== ws . OPEN ) {
return logger . info ( { message : ` Stopped downloading ${ channelId } , websocket is closed ` } )
}
2023-03-05 14:38:25 +00:00
const already = await prisma . videos . findFirst ( {
where : {
2024-03-30 19:05:11 +00:00
id : video . id
2023-03-05 14:38:25 +00:00
}
} )
if ( already ) {
2024-03-30 19:05:11 +00:00
ws . send ( ` DATA - Already downloaded ${ video . title . text } ` )
2023-03-05 14:38:25 +00:00
continue
}
2024-03-30 19:05:11 +00:00
if ( await redis . get ( video . id ) ) {
ws . send ( ` DATA - Someone is already downloading ${ video . title . text } , skipping. ` )
2023-03-05 14:38:25 +00:00
continue
}
2024-03-30 19:05:11 +00:00
if ( await redis . get ( ` blacklist: ${ video . id } ` ) ) {
ws . send ( ` DATA - ${ video . title . text } is blacklisted from downloading, skipping ` )
2023-03-25 13:13:52 +00:00
continue
}
2024-03-30 19:05:11 +00:00
ws . send ( ` INFO - Downloading ${ video . title . text } <br><br> ` )
2024-06-07 16:13:45 +00:00
await redis . set ( video . id , 'downloading' , 'EX' , 300 )
2023-03-05 14:38:25 +00:00
2024-03-30 19:05:11 +00:00
const download = await ytdlp . downloadVideo ( ` https://www.youtube.com/watch?v= ${ video . id } ` , ws , video . id )
2023-03-05 14:38:25 +00:00
if ( download . fail ) {
ws . send ( ` DATA - ${ download . message } ` )
2024-03-30 19:05:11 +00:00
await redis . del ( video . id )
2023-03-05 14:38:25 +00:00
continue
} else {
2024-03-30 19:05:11 +00:00
const file = fs . readdirSync ( "./videos" ) . find ( f => f . includes ( video . id ) )
2023-03-05 14:38:25 +00:00
if ( file ) {
2023-07-07 19:31:25 +00:00
try {
2024-03-30 19:05:11 +00:00
ws . send ( ` DATA - Downloaded ${ video . title . text } ` )
ws . send ( ` DATA - Uploading ${ video . title . text } ` )
2023-07-07 19:31:25 +00:00
2024-03-30 19:05:11 +00:00
const videoUrl = await upload . uploadVideo ( ` ./videos/ ${ video . id } .mp4 ` )
ws . send ( ` DATA - Uploaded ${ video . title . text } ` )
fs . unlinkSync ( ` ./videos/ ${ video . id } .mp4 ` )
2023-07-07 19:31:25 +00:00
2024-03-30 19:05:11 +00:00
await websocket . createDatabaseVideo ( video . id , videoUrl )
ws . send ( ` DATA - Created video page for ${ video . title . text } ` )
2023-07-07 19:31:25 +00:00
} catch ( e ) {
2024-03-30 19:05:11 +00:00
ws . send ( ` DATA - Failed downloading video ${ video . title . text } . Going to next video ` )
2023-07-07 19:31:25 +00:00
logger . error ( e )
}
2023-03-05 14:38:25 +00:00
} else {
2024-03-30 19:05:11 +00:00
ws . send ( ` DATA - Failed to find file for ${ video . title . text } . Going to next video ` )
2023-03-05 14:38:25 +00:00
}
2023-03-10 18:24:51 +00:00
2024-03-30 19:05:11 +00:00
await redis . del ( video . id )
2023-03-05 14:38:25 +00:00
}
}
ws . send ( ` DONE - ${ process . env . FRONTEND } /channel/ ${ channelId } ` )
}
}
exports . addAutodownload = async ( req , res ) => {
const confirm = await captcha . checkCaptcha ( req . query . captcha )
2023-03-05 14:40:28 +00:00
if ( ! confirm ) return res . status ( 500 ) . send ( 'You little goofy goober tried to mess with the captcha...' )
2023-03-05 14:38:25 +00:00
const channelId = await validate . validateChannelInput ( req . query . url )
if ( channelId . fail ) {
2023-03-05 14:40:28 +00:00
return res . status ( 500 ) . send ( channelId . message )
2023-03-05 14:38:25 +00:00
}
const already = await prisma . autodownload . findFirst ( {
where : {
channel : channelId
}
} )
if ( already ) {
res . status ( 500 ) . send ( ` This channel is already being automatically downloaded... ` )
} else {
2023-10-03 19:29:44 +00:00
const ipHash = crypto . createHash ( 'sha256' ) . update ( req . headers [ 'x-forwarded-for' ] || req . connection . remoteAddress ) . digest ( 'hex' )
const isLimited = await limiter . limit ( ipHash )
if ( isLimited ) return res . status ( 420 ) . send ( ` Hey! You have reached the limit of 5 queued auto-download channels per day. Sadly, hard drives don't grow on trees, so rate limits are necessary. The "Save Channel" feature has no limits, so feel free to use that.<br><br>
Are you planning something awesome ? Feel free to email me at admin [ @ ] preservetube . com . ` )
2023-03-05 14:38:25 +00:00
await prisma . autodownload . create ( {
data : {
channel : channelId
}
} )
res . send ( 'Perfect! Each time this channel uploads their videos will be downloaded' )
}
2023-03-03 16:44:40 +00:00
}