Compare commits

14 Commits

Author SHA1 Message Date
Ben
5112ff5ae7 Updated the client 2020-06-22 00:14:28 +01:00
Ben
876fb9686b Resolverrrr 2020-06-21 22:42:22 +01:00
Ben Kyd
fff8845c9d bruh 2020-06-21 16:29:14 +00:00
Ben Kyd
55df78ff94 Video resolver and cache working 2020-06-18 02:13:54 +00:00
Ben Kyd
88fa7a0d93 removed log 2020-06-17 23:27:23 +00:00
Ben Kyd
80e36b02c3 resolution not really 2020-06-17 21:37:52 +00:00
Ben
67dd6f3f39 switching computers 2020-06-17 16:32:16 +01:00
Ben
e8c4ddcaaa error handling 2020-06-17 16:14:47 +01:00
Ben
f2f3fee52b amazing design work 2020-06-16 03:29:48 +01:00
Ben
e71de57d86 socket.io 2020-06-16 03:25:46 +01:00
Ben
db4f403e16 fixed the logger lmao 2020-06-16 03:06:51 +01:00
Ben Kyd
fc22ca529d boilerplate 2020-06-15 23:41:00 +00:00
Ben Kyd
32da4e4466 ITS REWRITE TIME 2020-06-15 22:50:08 +00:00
Benjamin Kyd
36491a761a Merge pull request #1 from plane000/add-license-1
Create LICENSE
2019-02-03 20:23:46 +00:00
33 changed files with 1455 additions and 454 deletions

5
.gitignore vendored
View File

@@ -1,4 +1,3 @@
node_modules
package-lock.json package-lock.json
*.mp4 node_modules/
*.mp3 logs.log

View File

@@ -1,6 +1,6 @@
MIT License MIT License
Copyright (c) 2019 Benjamin Kyd Copyright (c) 2020 Benjamin Kyd
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

BIN
Screenshot_27.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -1 +1 @@
require('./src/index').main(); require('./src/index').Main();

65
legacy/.gitignore vendored
View File

@@ -1,61 +1,4 @@
# Logs node_modules
logs package-lock.json
*.log *.mp4
npm-debug.log* *.mp3
yarn-debug.log*
yarn-error.log*
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# nyc test coverage
.nyc_output
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# TypeScript v1 declaration files
typings/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
# next.js build output
.next

View File

@@ -1,40 +1 @@
const fs = require('fs'); require('./src/index').main();
const ytdl = require('ytdl-core');
let videos = [];
if (!fs.existsSync('./videos.txt')) {
console.log('Could not find videos.txt, make sure it exists and has youtube links, each on new lines');
process.exit();
}
let text = fs.readFileSync('./videos.txt').toString();
text.split('\n').forEach((line) => {
videos.push(line.replace(/\r/, ''));
});
videos.forEach((url) => {
if (ytdl.validateURL(url)) {
ytdl.getInfo(url, (err, info) => {
if (err) {
console.log(`An error occured while downloading '${url}'`)
} else {
let title = info.title;
try {
let stream = ytdl(url, {quality: 'highest'}).pipe(fs.createWriteStream(`${title}.mp4`));
stream.on('finish', () => console.log(`Finish downloading ${title}`));
} catch (e) {
console.log(`An error occured while downloaing '${title}' retrying`)
try {
let stream = ytdl(url, {quality: 'highest'}).pipe(fs.createWriteStream(`${title}.mp4`));
stream.on('finish', () => console.log(`Finished downloading '${title}'`));
} catch (e) {
console.log(`Unable to download '${title}'`)
}
}
console.log(`Downloading '${title}'`);
}
})
} else console.log(`Video ${url} was not found`);
});

61
legacy/legacy/.gitignore vendored Normal file
View File

@@ -0,0 +1,61 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# nyc test coverage
.nyc_output
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# TypeScript v1 declaration files
typings/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
# next.js build output
.next

40
legacy/legacy/index.js Normal file
View File

@@ -0,0 +1,40 @@
const fs = require('fs');
const ytdl = require('ytdl-core');
let videos = [];
if (!fs.existsSync('./videos.txt')) {
console.log('Could not find videos.txt, make sure it exists and has youtube links, each on new lines');
process.exit();
}
let text = fs.readFileSync('./videos.txt').toString();
text.split('\n').forEach((line) => {
videos.push(line.replace(/\r/, ''));
});
videos.forEach((url) => {
if (ytdl.validateURL(url)) {
ytdl.getInfo(url, (err, info) => {
if (err) {
console.log(`An error occured while downloading '${url}'`)
} else {
let title = info.title;
try {
let stream = ytdl(url, {quality: 'highest'}).pipe(fs.createWriteStream(`${title}.mp4`));
stream.on('finish', () => console.log(`Finish downloading ${title}`));
} catch (e) {
console.log(`An error occured while downloaing '${title}' retrying`)
try {
let stream = ytdl(url, {quality: 'highest'}).pipe(fs.createWriteStream(`${title}.mp4`));
stream.on('finish', () => console.log(`Finished downloading '${title}'`));
} catch (e) {
console.log(`Unable to download '${title}'`)
}
}
console.log(`Downloading '${title}'`);
}
})
} else console.log(`Video ${url} was not found`);
});

View File

@@ -0,0 +1,15 @@
{
"name": "youtube-dowloader",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "Ben (plane000)",
"license": "MIT",
"dependencies": {
"fs": "0.0.1-security",
"ytdl-core": "^0.21.1"
}
}

View File

@@ -1,47 +0,0 @@
{
"name": "youtube-dowloader",
"version": "1.0.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"fs": {
"version": "0.0.1-security",
"resolved": "https://registry.npmjs.org/fs/-/fs-0.0.1-security.tgz",
"integrity": "sha1-invTcYa23d84E/I4WLV+yq9eQdQ="
},
"html-entities": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/html-entities/-/html-entities-1.2.1.tgz",
"integrity": "sha1-DfKTUfByEWNRXfueVUPl9u7VFi8="
},
"m3u8stream": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/m3u8stream/-/m3u8stream-0.3.0.tgz",
"integrity": "sha512-0tvjXDIa6BolPEGo9zioQiPqfQhjopZXN3L7vZH/rZQCOLd4rPXNZc1UBMdW3TRpjNBoD0+F1X41/f0iY23rlQ==",
"requires": {
"miniget": "1.2.0"
}
},
"miniget": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/miniget/-/miniget-1.2.0.tgz",
"integrity": "sha1-ADY3Oia71S2+aUX85sjAOR6eEkE="
},
"sax": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
"integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw=="
},
"ytdl-core": {
"version": "0.21.1",
"resolved": "https://registry.npmjs.org/ytdl-core/-/ytdl-core-0.21.1.tgz",
"integrity": "sha512-K6ysXuHW0X3MgeLpO4IHvHp5gdF2DDrgFPP7kQODBSpeaq4+/y8lvWy6ZpxtJdQGE0+GUaBZ2H1/kYcP327UdQ==",
"requires": {
"html-entities": "1.2.1",
"m3u8stream": "0.3.0",
"miniget": "1.2.0",
"sax": "1.2.4"
}
}
}
}

View File

@@ -1,15 +1,19 @@
{ {
"name": "youtube-dowloader", "name": "youtube-downloader",
"version": "1.0.0", "version": "1.0.0",
"description": "", "description": "",
"main": "index.js", "main": "main.js",
"scripts": { "repository": {
"test": "echo \"Error: no test specified\" && exit 1" "type": "git",
"url": "git+https://github.com/plane000/youtube-downloader.git"
}, },
"author": "Ben (plane000)", "author": "Ben (plane000)",
"license": "MIT", "license": "MIT",
"devDependencies": {},
"dependencies": { "dependencies": {
"fs": "0.0.1-security", "express": "^4.16.4",
"ytdl-core": "^0.21.1" "moment": "^2.23.0",
"socket.io": "^2.2.0",
"ytdl-core": "^0.28.3"
} }
} }

36
legacy/public/index.html Normal file
View File

@@ -0,0 +1,36 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>Ben's YouTube Downloader</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" type="text/css" media="screen" href="style.css" />
<script src="/socket.io/socket.io.js"></script>
</head>
<body>
<div id="Instructions">
<h1 id="CurrentDirectory">Currently saving videos to: <code id="saveloc">./</code></h1>
Videos to record (seperate different videos by new lines):
</div>
<div id="Input">
<textarea id="VideosToRecord"></textarea>
</div>
<button id="Download">Download</button>
<input type="checkbox" id="AudioOnly">Only Download Audio (.mp3)
<div id="VideoContainer">
<div id="VideoBox"></div>
</div>
<div id="Updater">
<button id="Update">Update</button>
</div>
<script src="index.js"></script>
<div id="copyright">Copyright Benjamin Kyd© <script type="text/javascript">document.write(new Date().getFullYear())</script></div>
</body>
</html>

98
legacy/public/index.js Normal file
View File

@@ -0,0 +1,98 @@
let socket = io();
(() => {
console.log('Starting up');
})();
socket.on('path', async (data) => {
document.getElementById('saveloc').innerText = data;
});
let isDownloading = false;
let VideosToDownload = {};
document.getElementById('VideosToRecord').addEventListener('keyup', (e) => {
if (isDownloading) return;
let current = document.getElementById('VideosToRecord').value;
VideosToDownload = current.split('\n');
let payload = {
ExpectPreview: true,
VideosToDownload: VideosToDownload
};
socket.emit('video-list', payload);
});
let VideoPreview = [];
function renderPreview() {
if (isDownloading) return;
document.getElementById('VideoBox').innerText = '';
for (const [key, value] of Object.entries(VideoPreview)) {
if (document.getElementById(key) == null) {
if (!value.found) {
document.getElementById('VideoBox').innerHTML += `<div id="${key}">${key}: <h>Video not found</h></div>`;
} else {
document.getElementById('VideoBox').innerHTML += `<div id="${key}">${key}: <h>${value.title}</h></div>`;
}
}
}
}
function clearPreview() {
document.getElementById('VideoBox').innerText = '';
}
socket.on('video-preview', async (data) => {
if (isDownloading) return;
if (!data || !data.data || data.contents == false) {
clearPreview();
return;
}
VideoPreview = data.data;
renderPreview();
});
document.getElementById('Download').addEventListener('click', async (event) => {
if (isDownloading) return;
socket.emit('download', {
videos: document.getElementById('VideosToRecord').value.split('\n'),
audioOnly: document.getElementById('AudioOnly').checked
});
document.getElementById('VideoBox').innerText = 'Downloading...';
// document.getElementById('VideosToRecord').value = null;
isDownloading = true;
console.log('Asked server for download...');
});
let downloads = [];
let downloadCount = 0;
let completedDownloads = 0;
function renderDownloads() {
document.getElementById('VideoBox').innerText = '';
for (const [key, value] of Object.entries(downloads)) {
document.getElementById('VideoBox').innerHTML += `<div id="${key}">${value.title}: <h>${value.percent}</h></div>`;
}
}
socket.on('download-count', async (data) => {
downloadCount = data.num;
});
socket.on('download-done', async(data) => {
downloads[data.video] = {title: data.title, percent: 'Complete!'};
renderDownloads();
});
socket.on('download-progress', async (data) => {
downloads[data.video] = data;
renderDownloads();
});
socket.on('queue-concluded', async (data) => {
completedDownloads = 0; downloadCount = 0;
isDownloading = false;
downloads = [];
document.getElementById('VideoBox').innerHTML += "<h>Queue Concluded...</h>";
});

View File

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 29 KiB

13
legacy/src/index.js Normal file
View File

@@ -0,0 +1,13 @@
const server = require('./server');
let config = {
serverPort: 8080,
downloadLocation: './'
};
module.exports.config = config;
module.exports.main = async () => {
await server.init();
await server.listen();
}

49
legacy/src/server.js Normal file
View File

@@ -0,0 +1,49 @@
const logger = require('./logger')
const main = require('./index');
const youtube = require('./youtubehelper');
const express = require('express');
let app;
let http;
let io;
module.exports.init = async () => {
app = require('express')();
http = require('http').Server(app);
io = require('socket.io')(http);
http.listen(main.config.serverPort, () => {
logger.log(`HTTP server listening on port ${main.config.serverPort}`);
logger.log(`WebSocket server listening on ${main.config.serverPort}`);
});
}
module.exports.listen = async () => {
app.use(express.static('public'));
io.on('connection', async (socket) => {
logger.log(`New socket connection from id: ${socket.id}`);
socket.emit('path', main.config.downloadLocation);
socket.on('video-list', async (data) => {
if (data.ExpectPreview) {
logger.log(`Socket id '${socket.id}' is requesting a video preview`);
let response = await youtube.resolveVideos(data.VideosToDownload);
socket.emit('video-preview', response);
logger.log(`Finished preview for socket id '${socket.id}'`);
}
});
socket.on('download', async (data) => {
logger.log(`Socket id '${socket.id}' is requesting a download`);
let toDownload = await youtube.resolveVideos(data.videos);
youtube.setupDownloadQueue(toDownload.data, socket, {path: main.config.downloadLocation, audioOnly: !data.audioOnly});
});
});
}
module.exports.app = app;
module.exports.http = http;
module.exports.io = io;

124
legacy/src/youtubehelper.js Normal file
View File

@@ -0,0 +1,124 @@
const logger = require('./logger');
const fs = require('fs');
const ytdl = require('ytdl-core');
module.exports.resolveVideos = async (arr) => {
let output = {contents: false, data: {}};
for (let video of arr) {
if (video == '' || video == ' ') continue;
try {
if (await ytdl.validateURL(video)) {
const info = await ytdl.getBasicInfo(video);
if (!info) {
output.data[video] = {found: false};
return;
}
output.data[video] = {
found: true,
error: false,
title: info.title,
thumbnail: info.thumbnail_url.replace('default', 'maxresdefault'),
runtime: info.length_seconds
}
output.contents = true;
} else {
output.data[video] = {found: false, error: true};
output.contents = true;
}
} catch (e) {
if (!e.toString().includes('Error: This video contains content')) {
output.data[video] = {found: false};
output.contents = true;
logger.log(`Error resolving video ${video}: ${e}`);
} else {
output.data[video] = {
found: true,
error: true,
title: 'Not found: This video is blocked in your country',
thumbnail: 'ERRORED',
runtime: 'NONE'
}
}
}
}
return output;
}
let downloadQueue = [];
module.exports.setupDownloadQueue = async (arr, socket, options) => {
let numOfDownloads;
for (const [key, value] of Object.entries(arr))
if (value.error) delete arr[key];
downloadQueue[socket.id] = {
count: 0,
videos: { }
}
for (const [key, value] of Object.entries(arr))
if (ytdl.validateURL(key))
socket.emit('download-progress', {video: key, percent: "Queued", title: value.title});
await new Promise(async (resolve, reject) => {
for (const [key, value] of Object.entries(arr)) {
if (ytdl.validateURL(key)) {
await runQueueAsync(socket.id);
await download(key, value.title, socket, options.audioOnly);
}
// if (arr.indexOf(key) == arr.length) {resolve();}
}
resolve();
});
socket.emit('queue-concluded');
}
async function runQueueAsync(socketID) {
// Ready for proper queueuing
}
async function download(video, videoName ,socket, audioOnly, path = './') {
return new Promise(async (resolve, reject) => {
let stream;
try {
if (audioOnly) {
stream = await ytdl(video, {quality: 'highest', filter: (format) => format.container === 'mp4'});
stream.pipe(fs.createWriteStream(`${path}/${(videoName).replace(/\//, '-')}.mp4`));
} else {
stream = await ytdl(video, {quality: 'highest', filter: "audioonly"});
stream.pipe(fs.createWriteStream(`${path}/${(videoName).replace(/\//, '-')}.mp3`));
}
stream.on('response', (res) => {
let totalSize = res.headers['content-length'];
let dataRead = 0;
let lastPercent = 0;
res.on('data', (data) => {
dataRead += data.length;
let percent = Math.floor((dataRead / totalSize) * 100) + '%';
if (percent != lastPercent) {
socket.emit('download-progress', {video: video, percent: percent, title: videoName});
}
lastPercent = percent;
});
res.on('end', () => {
logger.log(`Socket id '${socket.id}' finished downloading ${videoName}`)
socket.emit('download-done', {video: video, title: videoName});
resolve('complete');
});
logger.log(`Socket id '${socket.id}' is downloading ${videoName}`);
});
} catch (e) {
logger.log(`Socket id '${socket.id}' failed to download ${videoName}: ${e}`);
socket.emit('download-done', {video: video, title: videoName});
socket.emit('download-progress', {video: video, percent: "Failed", title: videoName});
resolve('error');
}
});
}

View File

@@ -1,19 +1,25 @@
{ {
"name": "youtube-downloader", "name": "youtube-downloader",
"version": "1.0.0", "version": "1.0.0",
"description": "", "description": "Simple to use youtube downloader, simply run the index javascript file with node and then go to localhost on your browser, by default the video will download to this folder but can be changed in src/index.js",
"main": "main.js", "main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": { "repository": {
"type": "git", "type": "git",
"url": "git+https://github.com/plane000/youtube-downloader.git" "url": "git+https://github.com/plane000/youtube-downloader.git"
}, },
"author": "Ben (plane000)", "author": "Ben (benjaminkyd@gmail.com)",
"license": "MIT", "license": "MIT",
"devDependencies": {}, "bugs": {
"url": "https://github.com/plane000/youtube-downloader/issues"
},
"homepage": "https://github.com/plane000/youtube-downloader#readme",
"dependencies": { "dependencies": {
"express": "^4.16.4", "express": "^4.17.1",
"moment": "^2.23.0", "moment": "^2.26.0",
"socket.io": "^2.2.0", "socket.io": "^2.3.0",
"ytdl-core": "^0.28.3" "ytdl-core": "^2.1.7"
} }
} }

View File

@@ -3,15 +3,13 @@
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>Ben's YouTube Downloader</title> <title>Ben's Amazing YouTube Downloader</title>
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" type="text/css" media="screen" href="style.css" /> <link rel="stylesheet" type="text/css" media="screen" href="style.css" />
<script src="/socket.io/socket.io.js"></script> <script src="/socket.io/socket.io.js"></script>
</head> </head>
<body> <body>
<div id="Instructions"> <div id="Instructions">
<h1 id="CurrentDirectory">Currently saving videos to: <code id="saveloc">./</code></h1>
Videos to record (seperate different videos by new lines): Videos to record (seperate different videos by new lines):
</div> </div>
@@ -19,17 +17,12 @@
<textarea id="VideosToRecord"></textarea> <textarea id="VideosToRecord"></textarea>
</div> </div>
<button id="Download">Download</button> <div id="ResolvedVideoContainer"></div>
<input type="checkbox" id="AudioOnly">Only Download Audio (.mp3)
<div id="VideoContainer"> <!-- <button id="Download">Download</button> -->
<div id="VideoBox"></div> <!-- <input type="checkbox" id="AudioOnly">Only Download Audio (.mp3) -->
</div>
<div id="Updater">
<button id="Update">Update</button>
</div>
<script src="youtubedombuilder.js"></script>
<script src="index.js"></script> <script src="index.js"></script>
<div id="copyright">Copyright Benjamin Kyd© <script type="text/javascript">document.write(new Date().getFullYear())</script></div> <div id="copyright">Copyright Benjamin Kyd© <script type="text/javascript">document.write(new Date().getFullYear())</script></div>
</body> </body>

View File

@@ -1,98 +1,36 @@
let socket = io(); let Socket = io();
(() => { (() =>
console.log('Starting up'); {
})(); console.log('Starting up');
})();
socket.on('path', async (data) => {
document.getElementById('saveloc').innerText = data; function DownloadButtonPressed(format, id)
}); {
alert(`${format} ${id}`);
let isDownloading = false; }
let VideosToDownload = {};
document.getElementById('VideosToRecord').addEventListener('keyup', (e) => { const VideoInput = document.getElementById('VideosToRecord');
if (isDownloading) return;
let current = document.getElementById('VideosToRecord').value; VideoInput.oninput = () =>
VideosToDownload = current.split('\n'); {
const ToSend = {
let payload = { // TODO: add toggle
ExpectPreview: true, ExpectPreview: true,
VideosToDownload: VideosToDownload Content: VideoInput.value.split('\n')
}; };
Socket.emit('VideoListUpdate', ToSend);
socket.emit('video-list', payload); };
});
Socket.on('VideoListResolution', (res) =>
let VideoPreview = []; {
if (res.Error === true) return;
function renderPreview() { ResolvedBasic(res.Content);
if (isDownloading) return; });
document.getElementById('VideoBox').innerText = '';
for (const [key, value] of Object.entries(VideoPreview)) { Socket.on('VideoListResolved', (res) =>
if (document.getElementById(key) == null) { {
if (!value.found) { console.log(res);
document.getElementById('VideoBox').innerHTML += `<div id="${key}">${key}: <h>Video not found</h></div>`; if (res.Error === true) return;
} else { ResolvedVideos(res.Content);
document.getElementById('VideoBox').innerHTML += `<div id="${key}">${key}: <h>${value.title}</h></div>`; });
}
}
}
}
function clearPreview() {
document.getElementById('VideoBox').innerText = '';
}
socket.on('video-preview', async (data) => {
if (isDownloading) return;
if (!data || !data.data || data.contents == false) {
clearPreview();
return;
}
VideoPreview = data.data;
renderPreview();
});
document.getElementById('Download').addEventListener('click', async (event) => {
if (isDownloading) return;
socket.emit('download', {
videos: document.getElementById('VideosToRecord').value.split('\n'),
audioOnly: document.getElementById('AudioOnly').checked
});
document.getElementById('VideoBox').innerText = 'Downloading...';
// document.getElementById('VideosToRecord').value = null;
isDownloading = true;
console.log('Asked server for download...');
});
let downloads = [];
let downloadCount = 0;
let completedDownloads = 0;
function renderDownloads() {
document.getElementById('VideoBox').innerText = '';
for (const [key, value] of Object.entries(downloads)) {
document.getElementById('VideoBox').innerHTML += `<div id="${key}">${value.title}: <h>${value.percent}</h></div>`;
}
}
socket.on('download-count', async (data) => {
downloadCount = data.num;
});
socket.on('download-done', async(data) => {
downloads[data.video] = {title: data.title, percent: 'Complete!'};
renderDownloads();
});
socket.on('download-progress', async (data) => {
downloads[data.video] = data;
renderDownloads();
});
socket.on('queue-concluded', async (data) => {
completedDownloads = 0; downloadCount = 0;
isDownloading = false;
downloads = [];
document.getElementById('VideoBox').innerHTML += "<h>Queue Concluded...</h>";
});

View File

@@ -34,19 +34,42 @@ h {
font-weight: 400; font-weight: 400;
} }
#VideoContainer { .table {
padding: 30px, 0px, 30px, 0px; width: 60%;
} }
#VideoBox { .thumbnail {
padding-top: 10px; max-width: 150%;
font-weight: lighter; height: auto;
} }
#Updater { .videotitle {
position: absolute; font-size: 1rem;
top: 5px; }
right: 10px;}
.channel {
font-size: 0.9rem;
}
.views {
font-size: 0.9rem;
}
.desc {
font-size: 0.8rem;
display: inline-block;
text-overflow: ellipsis;
word-wrap: break-word;
overflow: hidden;
line-height: 1.2em;
max-height: 2.4em;
font-style: italic;
}
.downloadbuttons {
font-size: 0.8rem;
}
#copyright { #copyright {
position: absolute; position: absolute;

103
public/youtubedombuilder.js Normal file
View File

@@ -0,0 +1,103 @@
let VideoList = []; // Array indexed by video ID
const ResolvedVideoContainer = document.getElementById('ResolvedVideoContainer');
function CleanUp()
{
}
function ResolvingElement(id, url, status, error = 'An unknown error occured')
{
console.log(status);
if (status == 'Resolving')
return `<div id="${id}"> URL: ${url} <br>Status: Resolving...</div><br>`
if (status == 'Error')
return `<div id="${id}"> URL: ${url} <br>Status: Error, ${error}</div><br>`
}
function ResolvedBasic(BasicResolution)
{
for (res of BasicResolution)
{
// url is not valid
if (res.id == -1)
{
console.log(`Video: ${res.url} will not be resolved`);
continue;
}
if (VideoList[res.id]) continue;
const action = res.action;
ResolvedVideoContainer.innerHTML += ResolvingElement(res.id, res.url, action)
const htmlElement = document.getElementById(res.id);
VideoList[res.id] = {
id: res.id,
element: htmlElement,
status: res.action
};
}
}
function VideoElement(id, title, desc, views, thumbnail, channel, runtime)
{
// format views so theres commas
// in it ennit - Will is a stupid
// "Fuck you, of course it's a string" - b_boy_ww 2020
views = parseInt(views);
views = views.toLocaleString('en-US');
return `
<table class="table">
<tr>
<td rowspan="4" class="thumbnail"><a href="https://www.youtube.com/watch?v=${id}"><img src="${thumbnail}"></a></td>
<td colspan="4" class="videotitle">${title}</td>
</tr>
<tr>
<td class="channel">${channel}</td>
<td class="views">${views} views</td>
<td></td>
<td></td>
</tr>
<tr>
<td colspan="4" class="desc">${desc}</td>
</tr>
<tr>
<td colspan="2" class="downloadbuttons"><button onclick="DownloadButtonPressed('MP4(MAX)', ${id})">Download Video</button> </td>
<td colspan="2" class="downloadbuttons"><button onclick="DownloadButtonPressed('MP3(MAX)', ${id})">Download Audio</button> </td>
</tr>
</table>
`;
}
function ResolvedVideos(VideoResolution)
{
for (res of VideoResolution)
{
if (!VideoList[res.id])
{
console.log(`Unexpected video, ${res.id} was not in the resolve queue`)
continue;
}
if (VideoList[res.id].status == "Resolved") continue;
const Video = VideoList[res.id];
const htmlElement = Video.element;
if (res.Error == true)
{
// there is no url element ! it is discarded on the server in place for an ID
htmlElement.innerHTML = ResolvingElement(res.id, `https://www.youtube.com/watch?v=${res.id}`, 'Error', res.Errors[0]);
VideoList[res.id].status = "Error";
continue;
}
htmlElement.innerHTML = VideoElement(res.id, res.title, res.desc, res.views, res.thumbnail, res.channel, res.runtime);
VideoList[res.id].status = "Resolved";
}
}

16
src/config.js Normal file
View File

@@ -0,0 +1,16 @@
const Logger = require('./logger');
module.exports.Configuration = {}
module.exports.Load = () =>
{
this.Configuration = {
LogFile: 'logs.log',
ListenPort: 8080,
PublicDirectory: 'public',
StorageDirectory: './tmp/',
CacheCleanInterval: 300000, // 5 mins
CacheTimeToUse: 600000, // 10 mins
}
Logger.Log('Configuration loaded');
}

512
src/formats.js Normal file
View File

@@ -0,0 +1,512 @@
module.exports = {
5: {
mimeType: 'video/flv; codecs="Sorenson H.283, mp3"',
qualityLabel: '240p',
bitrate: 250000,
audioBitrate: 64,
},
6: {
mimeType: 'video/flv; codecs="Sorenson H.263, mp3"',
qualityLabel: '270p',
bitrate: 800000,
audioBitrate: 64,
},
13: {
mimeType: 'video/3gp; codecs="MPEG-4 Visual, aac"',
qualityLabel: null,
bitrate: 500000,
audioBitrate: null,
},
17: {
mimeType: 'video/3gp; codecs="MPEG-4 Visual, aac"',
qualityLabel: '144p',
bitrate: 50000,
audioBitrate: 24,
},
18: {
mimeType: 'video/mp4; codecs="H.264, aac"',
qualityLabel: '360p',
bitrate: 500000,
audioBitrate: 96,
},
22: {
mimeType: 'video/mp4; codecs="H.264, aac"',
qualityLabel: '720p',
bitrate: 2000000,
audioBitrate: 192,
},
34: {
mimeType: 'video/flv; codecs="H.264, aac"',
qualityLabel: '360p',
bitrate: 500000,
audioBitrate: 128,
},
35: {
mimeType: 'video/flv; codecs="H.264, aac"',
qualityLabel: '480p',
bitrate: 800000,
audioBitrate: 128,
},
36: {
mimeType: 'video/3gp; codecs="MPEG-4 Visual, aac"',
qualityLabel: '240p',
bitrate: 175000,
audioBitrate: 32,
},
37: {
mimeType: 'video/mp4; codecs="H.264, aac"',
qualityLabel: '1080p',
bitrate: 3000000,
audioBitrate: 192,
},
38: {
mimeType: 'video/mp4; codecs="H.264, aac"',
qualityLabel: '3072p',
bitrate: 3500000,
audioBitrate: 192,
},
43: {
mimeType: 'video/webm; codecs="VP8, vorbis"',
qualityLabel: '360p',
bitrate: 500000,
audioBitrate: 128,
},
44: {
mimeType: 'video/webm; codecs="VP8, vorbis"',
qualityLabel: '480p',
bitrate: 1000000,
audioBitrate: 128,
},
45: {
mimeType: 'video/webm; codecs="VP8, vorbis"',
qualityLabel: '720p',
bitrate: 2000000,
audioBitrate: 192,
},
46: {
mimeType: 'audio/webm; codecs="vp8, vorbis"',
qualityLabel: '1080p',
bitrate: null,
audioBitrate: 192,
},
82: {
mimeType: 'video/mp4; codecs="H.264, aac"',
qualityLabel: '360p',
bitrate: 500000,
audioBitrate: 96,
},
83: {
mimeType: 'video/mp4; codecs="H.264, aac"',
qualityLabel: '240p',
bitrate: 500000,
audioBitrate: 96,
},
84: {
mimeType: 'video/mp4; codecs="H.264, aac"',
qualityLabel: '720p',
bitrate: 2000000,
audioBitrate: 192,
},
85: {
mimeType: 'video/mp4; codecs="H.264, aac"',
qualityLabel: '1080p',
bitrate: 3000000,
audioBitrate: 192,
},
91: {
mimeType: 'video/ts; codecs="H.264, aac"',
qualityLabel: '144p',
bitrate: 100000,
audioBitrate: 48,
},
92: {
mimeType: 'video/ts; codecs="H.264, aac"',
qualityLabel: '240p',
bitrate: 150000,
audioBitrate: 48,
},
93: {
mimeType: 'video/ts; codecs="H.264, aac"',
qualityLabel: '360p',
bitrate: 500000,
audioBitrate: 128,
},
94: {
mimeType: 'video/ts; codecs="H.264, aac"',
qualityLabel: '480p',
bitrate: 800000,
audioBitrate: 128,
},
95: {
mimeType: 'video/ts; codecs="H.264, aac"',
qualityLabel: '720p',
bitrate: 1500000,
audioBitrate: 256,
},
96: {
mimeType: 'video/ts; codecs="H.264, aac"',
qualityLabel: '1080p',
bitrate: 2500000,
audioBitrate: 256,
},
100: {
mimeType: 'audio/webm; codecs="VP8, vorbis"',
qualityLabel: '360p',
bitrate: null,
audioBitrate: 128,
},
101: {
mimeType: 'audio/webm; codecs="VP8, vorbis"',
qualityLabel: '360p',
bitrate: null,
audioBitrate: 192,
},
102: {
mimeType: 'audio/webm; codecs="VP8, vorbis"',
qualityLabel: '720p',
bitrate: null,
audioBitrate: 192,
},
120: {
mimeType: 'video/flv; codecs="H.264, aac"',
qualityLabel: '720p',
bitrate: 2000000,
audioBitrate: 128,
},
127: {
mimeType: 'audio/ts; codecs="aac"',
qualityLabel: null,
bitrate: null,
audioBitrate: 96,
},
128: {
mimeType: 'audio/ts; codecs="aac"',
qualityLabel: null,
bitrate: null,
audioBitrate: 96,
},
132: {
mimeType: 'video/ts; codecs="H.264, aac"',
qualityLabel: '240p',
bitrate: 150000,
audioBitrate: 48,
},
133: {
mimeType: 'video/mp4; codecs="H.264"',
qualityLabel: '240p',
bitrate: 200000,
audioBitrate: null,
},
134: {
mimeType: 'video/mp4; codecs="H.264"',
qualityLabel: '360p',
bitrate: 300000,
audioBitrate: null,
},
135: {
mimeType: 'video/mp4; codecs="H.264"',
qualityLabel: '480p',
bitrate: 500000,
audioBitrate: null,
},
136: {
mimeType: 'video/mp4; codecs="H.264"',
qualityLabel: '720p',
bitrate: 1000000,
audioBitrate: null,
},
137: {
mimeType: 'video/mp4; codecs="H.264"',
qualityLabel: '1080p',
bitrate: 2500000,
audioBitrate: null,
},
138: {
mimeType: 'video/mp4; codecs="H.264"',
qualityLabel: '4320p',
bitrate: 13500000,
audioBitrate: null,
},
139: {
mimeType: 'audio/mp4; codecs="aac"',
qualityLabel: null,
bitrate: null,
audioBitrate: 48,
},
140: {
mimeType: 'audio/m4a; codecs="aac"',
qualityLabel: null,
bitrate: null,
audioBitrate: 128,
},
141: {
mimeType: 'audio/mp4; codecs="aac"',
qualityLabel: null,
bitrate: null,
audioBitrate: 256,
},
151: {
mimeType: 'video/ts; codecs="H.264, aac"',
qualityLabel: '720p',
bitrate: 50000,
audioBitrate: 24,
},
160: {
mimeType: 'video/mp4; codecs="H.264"',
qualityLabel: '144p',
bitrate: 100000,
audioBitrate: null,
},
171: {
mimeType: 'audio/webm; codecs="vorbis"',
qualityLabel: null,
bitrate: null,
audioBitrate: 128,
},
172: {
mimeType: 'audio/webm; codecs="vorbis"',
qualityLabel: null,
bitrate: null,
audioBitrate: 192,
},
242: {
mimeType: 'video/webm; codecs="VP9"',
qualityLabel: '240p',
bitrate: 100000,
audioBitrate: null,
},
243: {
mimeType: 'video/webm; codecs="VP9"',
qualityLabel: '360p',
bitrate: 250000,
audioBitrate: null,
},
244: {
mimeType: 'video/webm; codecs="VP9"',
qualityLabel: '480p',
bitrate: 500000,
audioBitrate: null,
},
247: {
mimeType: 'video/webm; codecs="VP9"',
qualityLabel: '720p',
bitrate: 700000,
audioBitrate: null,
},
248: {
mimeType: 'video/webm; codecs="VP9"',
qualityLabel: '1080p',
bitrate: 1500000,
audioBitrate: null,
},
249: {
mimeType: 'audio/webm; codecs="opus"',
qualityLabel: null,
bitrate: null,
audioBitrate: 48,
},
250: {
mimeType: 'audio/webm; codecs="opus"',
qualityLabel: null,
bitrate: null,
audioBitrate: 64,
},
251: {
mimeType: 'audio/webm; codecs="opus"',
qualityLabel: null,
bitrate: null,
audioBitrate: 160,
},
264: {
mimeType: 'video/mp4; codecs="H.264"',
qualityLabel: '1440p',
bitrate: 4000000,
audioBitrate: null,
},
266: {
mimeType: 'video/mp4; codecs="H.264"',
qualityLabel: '2160p',
bitrate: 12500000,
audioBitrate: null,
},
271: {
mimeType: 'video/webm; codecs="VP9"',
qualityLabel: '1440p',
bitrate: 9000000,
audioBitrate: null,
},
272: {
mimeType: 'video/webm; codecs="VP9"',
qualityLabel: '4320p',
bitrate: 20000000,
audioBitrate: null,
},
278: {
mimeType: 'video/webm; codecs="VP9"',
qualityLabel: '144p 15fps',
bitrate: 80000,
audioBitrate: null,
},
298: {
mimeType: 'video/mp4; codecs="H.264"',
qualityLabel: '720p',
bitrate: 3000000,
audioBitrate: null,
},
299: {
mimeType: 'video/mp4; codecs="H.264"',
qualityLabel: '1080p',
bitrate: 5500000,
audioBitrate: null,
},
302: {
mimeType: 'video/webm; codecs="VP9"',
qualityLabel: '720p HFR',
bitrate: 2500000,
audioBitrate: null,
},
303: {
mimeType: 'video/webm; codecs="VP9"',
qualityLabel: '1080p HFR',
bitrate: 5000000,
audioBitrate: null,
},
308: {
mimeType: 'video/webm; codecs="VP9"',
qualityLabel: '1440p HFR',
bitrate: 10000000,
audioBitrate: null,
},
313: {
mimeType: 'video/webm; codecs="VP9"',
qualityLabel: '2160p',
bitrate: 13000000,
audioBitrate: null,
},
315: {
mimeType: 'video/webm; codecs="VP9"',
qualityLabel: '2160p HFR',
bitrate: 20000000,
audioBitrate: null,
},
330: {
mimeType: 'video/webm; codecs="VP9"',
qualityLabel: '144p HDR, HFR',
bitrate: 80000,
audioBitrate: null,
},
331: {
mimeType: 'video/webm; codecs="VP9"',
qualityLabel: '240p HDR, HFR',
bitrate: 100000,
audioBitrate: null,
},
332: {
mimeType: 'video/webm; codecs="VP9"',
qualityLabel: '360p HDR, HFR',
bitrate: 250000,
audioBitrate: null,
},
333: {
mimeType: 'video/webm; codecs="VP9"',
qualityLabel: '240p HDR, HFR',
bitrate: 500000,
audioBitrate: null,
},
334: {
mimeType: 'video/webm; codecs="VP9"',
qualityLabel: '720p HDR, HFR',
bitrate: 1000000,
audioBitrate: null,
},
335: {
mimeType: 'video/webm; codecs="VP9"',
qualityLabel: '1080p HDR, HFR',
bitrate: 1500000,
audioBitrate: null,
},
336: {
mimeType: 'video/webm; codecs="VP9"',
qualityLabel: '1440p HDR, HFR',
bitrate: 5000000,
audioBitrate: null,
},
337: {
mimeType: 'video/webm; codecs="VP9"',
qualityLabel: '2160p HDR, HFR',
bitrate: 12000000,
audioBitrate: null,
},
};

View File

@@ -1,13 +1,15 @@
const server = require('./server'); const Logger = require('./logger')
const Config = require('./config');
const Server = require('./server');
let config = { const YoutubeHelper = require('./youtubehelper');
serverPort: 8080,
downloadLocation: './'
};
module.exports.config = config; module.exports.Main = async () =>
{
await Config.Load();
module.exports.main = async () => { await YoutubeHelper.InitResolverCache();
await server.init();
await server.listen(); await Server.Init();
Server.Listen();
} }

View File

@@ -1,7 +1,14 @@
const moment = require('moment'); const Config = require('./config')
const Moment = require('moment');
const fs = require('fs');
const dateFormat = 'DD-MM-YY HH:mm:ss' const dateFormat = 'DD-MM-YY HH:mm:ss'
module.exports.log = function(log) { module.exports.Log = (log) =>
let d = moment().format(dateFormat); {
console.log('[' + d.toLocaleString() + '] ' + log); let d = Moment().format(dateFormat);
log = '[' + d.toLocaleString() + '] ' + log;
console.log(log);
fs.appendFile(Config.Configuration.LogFile, log + '\n', (e) => { if (e) throw e; });
} }

View File

@@ -1,6 +1,8 @@
const logger = require('./logger') const Logger = require('./logger')
const main = require('./index'); const Config = require('./config');
const youtube = require('./youtubehelper');
const YoutubeHelper = require('./youtubehelper');
const YoutubeDownloader = require('./youtubedownloader');
const express = require('express'); const express = require('express');
@@ -8,42 +10,138 @@ let app;
let http; let http;
let io; let io;
module.exports.init = async () => { module.exports.Init = async () =>
{
app = require('express')(); app = require('express')();
http = require('http').Server(app); http = require('http').Server(app);
io = require('socket.io')(http); io = require('socket.io')(http);
http.listen(main.config.serverPort, () => {
logger.log(`HTTP server listening on port ${main.config.serverPort}`);
logger.log(`WebSocket server listening on ${main.config.serverPort}`);
});
} }
module.exports.listen = async () => { module.exports.Listen = async () =>
app.use(express.static('public')); {
http.listen(Config.Configuration.ListenPort, () => {
Logger.Log(`HTTP server listening on port ${Config.Configuration.ListenPort}`);
Logger.Log(`WebSocket server listening on ${Config.Configuration.ListenPort}`);
});
app.use(express.static(Config.Configuration.PublicDirectory));
io.on('connection', async (socket) => { io.on('connection', async (socket) => {
logger.log(`New socket connection from id: ${socket.id}`); Logger.Log(`New socket connection from ip: ${socket.handshake.address}, unique id: ${socket.id}`);
socket.emit('path', main.config.downloadLocation); SocketHandler(socket);
socket.on('video-list', async (data) => {
if (data.ExpectPreview) {
logger.log(`Socket id '${socket.id}' is requesting a video preview`);
let response = await youtube.resolveVideos(data.VideosToDownload);
socket.emit('video-preview', response);
logger.log(`Finished preview for socket id '${socket.id}'`);
}
});
socket.on('download', async (data) => {
logger.log(`Socket id '${socket.id}' is requesting a download`);
let toDownload = await youtube.resolveVideos(data.videos);
youtube.setupDownloadQueue(toDownload.data, socket, {path: main.config.downloadLocation, audioOnly: !data.audioOnly});
});
}); });
} }
module.exports.app = app; function SocketHandler(socket)
module.exports.http = http; {
module.exports.io = io; socket.on('VideoListUpdate', (req) => VideoListUpdate(socket, req) );
}
async function VideoListUpdate(socket, req)
{
// req format
// {
// ExptectPreview: bool,
// Content: [arr]
// }
let Res = {};
// res format
// errors not present if error is false
// content not present if error is true
// {
// Error: bool,
// Errors: [err],
// Content: [{
// id: int,
// url: string,
// valid: bool,
// action: string
// }]
// }
if (!req || !req.Content)
{
Res.Error = true;
Res.Errors = ErorrsBuilder("Content Error", "No content in request");
socket.emit('VideoListResolution', Res);
return;
}
const VideoArray = req.Content;
const YoutubeRegex = /^((?:https?:)?\/\/)?((?:www|m)\.)?((?:youtube\.com|youtu.be))(\/(?:[\w\-]+\?v=|embed\/|v\/)?)([\w\-]+)(\S+)?$/;
let ResolveQueue = [];
Res.Error = false;
Res.Content = [];
for (video of VideoArray)
{
video = video.trim()
if (YoutubeRegex.exec(video))
{
let VideoID = video.match(YoutubeRegex)[5];
// generate ID lol
ResolveQueue.push(VideoID);
Res.Content.push({
id: VideoID,
url: video,
valid: true,
action: 'Resolving'
});
} else
{
Res.Content.push({
id: -1, // -1 means not resolved
url: video, // used as ID on the client
valid: false,
action: 'Error'
});
}
}
socket.emit('VideoListResolution', Res);
if (ResolveQueue.length == 0)
return;
const Resolution = await YoutubeHelper.GetVideoInfoArr(ResolveQueue);
const ResolutionRes = {
Error: false,
Content: Resolution
};
socket.emit('VideoListResolved', ResolutionRes);
}
function ErorrsBuilder(...errors)
{
// errors format
// Errors: [{
// type: string,
// content: string
// }],
let ret = [];
let j = 0;
for (let i = 0; i < errors.length; i += 2)
{
ret[j] = {
type: errors[i],
content: errors[i+1]
};
j++;
}
return ret;
}
module.exports.App = app;
module.exports.Http = http;
module.exports.Io = io;

3
src/youtubedownloader.js Normal file
View File

@@ -0,0 +1,3 @@
const YTDL = require('ytdl-core');

View File

@@ -1,124 +1,128 @@
const logger = require('./logger'); const Config = require('./config');
const Logger = require('./logger');
const fs = require('fs');
const ytdl = require('ytdl-core'); const YTDL = require('ytdl-core');
module.exports.resolveVideos = async (arr) => { // TODO: does the video resolver need a queue?
let output = {contents: false, data: {}};
// cache
for (let video of arr) { let ResolutionCache = [];
if (video == '' || video == ' ') continue;
try { function HitCache(id)
if (await ytdl.validateURL(video)) { {
const info = await ytdl.getBasicInfo(video); if (ResolutionCache[id])
if (!info) { {
output.data[video] = {found: false}; Logger.Log(`Cache hit! ${id}`)
return; ResolutionCache[id].LastUsed = Date.now();
} return ResolutionCache[id].Video;
output.data[video] = { } else
found: true, {
error: false, return false;
title: info.title, }
thumbnail: info.thumbnail_url.replace('default', 'maxresdefault'), }
runtime: info.length_seconds
} function RegisterCache(video)
output.contents = true; {
} else { ResolutionCache[video.player_response.videoDetails.videoId] = {
output.data[video] = {found: false, error: true}; Id: video.player_response.videoDetails.videoId,
output.contents = true; Video: video,
} LastUsed: Date.now(),
} catch (e) { TimeCreated: Date.now()
if (!e.toString().includes('Error: This video contains content')) { }
output.data[video] = {found: false}; }
output.contents = true;
logger.log(`Error resolving video ${video}: ${e}`); function CleanCache()
} else { {
output.data[video] = { // TODO: fix weird bug where a cleaned
found: true, // entry's timeused lingers
error: true, let ExpiredEntrys = [];
title: 'Not found: This video is blocked in your country', for (id in ResolutionCache)
thumbnail: 'ERRORED', {
runtime: 'NONE' entry = ResolutionCache[id];
} // if cache entry has expired
} const LastUsed = Date.now() - entry.LastUsed;
} if (LastUsed > Config.Configuration.CacheTimeToUse)
} {
// remove expired entry
return output; Logger.Log(`Cache entry '${id}' expired`);
} ExpiredEntrys.push(id);
delete entry;
let downloadQueue = []; }
}
module.exports.setupDownloadQueue = async (arr, socket, options) => {
let numOfDownloads; for (expiredEntry of ExpiredEntrys)
{
for (const [key, value] of Object.entries(arr)) delete ResolutionCache[expiredEntry];
if (value.error) delete arr[key]; }
}
downloadQueue[socket.id] = {
count: 0, module.exports.InitResolverCache = async () =>
videos: { } {
} setInterval(CleanCache, Config.Configuration.CacheCleanInterval);
Logger.Log('Video resolver cache settup')
for (const [key, value] of Object.entries(arr)) }
if (ytdl.validateURL(key))
socket.emit('download-progress', {video: key, percent: "Queued", title: value.title}); module.exports.GetVideoInfoArr = async (arr) =>
{
await new Promise(async (resolve, reject) => { let ret = [];
for (const [key, value] of Object.entries(arr)) { // TODO: make async AND retain order
if (ytdl.validateURL(key)) { for (video of arr)
await runQueueAsync(socket.id); {
await download(key, value.title, socket, options.audioOnly); ret.push(await this.GetVideoInfo(video));
} }
// if (arr.indexOf(key) == arr.length) {resolve();} return ret;
} }
resolve();
}); module.exports.GetVideoInfo = async (video) =>
socket.emit('queue-concluded'); {
} try
{
async function runQueueAsync(socketID) { const CacheHit = HitCache(video);
// Ready for proper queueuing
} let Video = {};
async function download(video, videoName ,socket, audioOnly, path = './') { if (CacheHit)
return new Promise(async (resolve, reject) => { {
let stream; Video = CacheHit
try { } else
if (audioOnly) { {
stream = await ytdl(video, {quality: 'highest', filter: (format) => format.container === 'mp4'}); // TODO: is the YouTube API faster for this?
stream.pipe(fs.createWriteStream(`${path}/${(videoName).replace(/\//, '-')}.mp4`)); Logger.Log(`Resolving '${video}'`);
} else { Video = await YTDL.getInfo(video);
stream = await ytdl(video, {quality: 'highest', filter: "audioonly"}); // register the info into the cache
stream.pipe(fs.createWriteStream(`${path}/${(videoName).replace(/\//, '-')}.mp3`)); RegisterCache(Video);
} }
stream.on('response', (res) => {
let totalSize = res.headers['content-length']; let Res = BuildBasicInfoFromInfo(Video);
let dataRead = 0; return Res;
let lastPercent = 0;
res.on('data', (data) => { } catch (e)
dataRead += data.length; {
let percent = Math.floor((dataRead / totalSize) * 100) + '%'; Logger.Log(`Error resolving video '${video}', ${e}`);
if (percent != lastPercent) { return {
socket.emit('download-progress', {video: video, percent: percent, title: videoName}); id: video,
} Error: true,
lastPercent = percent; Errors: [ "Video cannot resolve" ]
}); };
}
res.on('end', () => { }
logger.log(`Socket id '${socket.id}' finished downloading ${videoName}`)
socket.emit('download-done', {video: video, title: videoName}); function BuildBasicInfoFromInfo(info)
resolve('complete'); {
}); let ret = {};
ret.id = info.player_response.videoDetails.videoId;
logger.log(`Socket id '${socket.id}' is downloading ${videoName}`); ret.title = info.player_response.videoDetails.title;
}); ret.desc = info.player_response.videoDetails.shortDescription;
} catch (e) { ret.views = info.player_response.videoDetails.viewCount;
logger.log(`Socket id '${socket.id}' failed to download ${videoName}: ${e}`); if (!info.player_response.videoDetails.thumbnail.thumbnails[0])
socket.emit('download-done', {video: video, title: videoName}); {
socket.emit('download-progress', {video: video, percent: "Failed", title: videoName}); ret.thumbnail = info.player_response.videoDetails.thumbnail.thumbnails[1].url;
resolve('error'); } else
} {
}); ret.thumbnail = info.player_response.videoDetails.thumbnail.thumbnails[0].url;
} }
ret.channel = info.player_response.videoDetails.author;
ret.runtime = info.player_response.videoDetails.lengthSeconds;
return ret;
}