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
*.mp4
*.mp3
node_modules/
logs.log

View File

@@ -1,6 +1,6 @@
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
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
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
node_modules
package-lock.json
*.mp4
*.mp3

View File

@@ -1,40 +1 @@
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`);
});
require('./src/index').main();

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",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
"main": "main.js",
"repository": {
"type": "git",
"url": "git+https://github.com/plane000/youtube-downloader.git"
},
"author": "Ben (plane000)",
"license": "MIT",
"devDependencies": {},
"dependencies": {
"fs": "0.0.1-security",
"ytdl-core": "^0.21.1"
"express": "^4.16.4",
"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",
"version": "1.0.0",
"description": "",
"main": "main.js",
"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": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
"type": "git",
"url": "git+https://github.com/plane000/youtube-downloader.git"
},
"author": "Ben (plane000)",
"author": "Ben (benjaminkyd@gmail.com)",
"license": "MIT",
"devDependencies": {},
"bugs": {
"url": "https://github.com/plane000/youtube-downloader/issues"
},
"homepage": "https://github.com/plane000/youtube-downloader#readme",
"dependencies": {
"express": "^4.16.4",
"moment": "^2.23.0",
"socket.io": "^2.2.0",
"ytdl-core": "^0.28.3"
"express": "^4.17.1",
"moment": "^2.26.0",
"socket.io": "^2.3.0",
"ytdl-core": "^2.1.7"
}
}

View File

@@ -3,15 +3,13 @@
<head>
<meta charset="utf-8" />
<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">
<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>
<div id="Instructions">
Videos to record (seperate different videos by new lines):
</div>
@@ -19,17 +17,12 @@
<textarea id="VideosToRecord"></textarea>
</div>
<button id="Download">Download</button>
<input type="checkbox" id="AudioOnly">Only Download Audio (.mp3)
<div id="ResolvedVideoContainer"></div>
<div id="VideoContainer">
<div id="VideoBox"></div>
</div>
<div id="Updater">
<button id="Update">Update</button>
</div>
<!-- <button id="Download">Download</button> -->
<!-- <input type="checkbox" id="AudioOnly">Only Download Audio (.mp3) -->
<script src="youtubedombuilder.js"></script>
<script src="index.js"></script>
<div id="copyright">Copyright Benjamin Kyd© <script type="text/javascript">document.write(new Date().getFullYear())</script></div>
</body>

View File

@@ -1,98 +1,36 @@
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>";
});
let Socket = io();
(() =>
{
console.log('Starting up');
})();
function DownloadButtonPressed(format, id)
{
alert(`${format} ${id}`);
}
const VideoInput = document.getElementById('VideosToRecord');
VideoInput.oninput = () =>
{
const ToSend = {
// TODO: add toggle
ExpectPreview: true,
Content: VideoInput.value.split('\n')
};
Socket.emit('VideoListUpdate', ToSend);
};
Socket.on('VideoListResolution', (res) =>
{
if (res.Error === true) return;
ResolvedBasic(res.Content);
});
Socket.on('VideoListResolved', (res) =>
{
console.log(res);
if (res.Error === true) return;
ResolvedVideos(res.Content);
});

View File

@@ -34,19 +34,42 @@ h {
font-weight: 400;
}
#VideoContainer {
padding: 30px, 0px, 30px, 0px;
.table {
width: 60%;
}
#VideoBox {
padding-top: 10px;
font-weight: lighter;
.thumbnail {
max-width: 150%;
height: auto;
}
#Updater {
position: absolute;
top: 5px;
right: 10px;}
.videotitle {
font-size: 1rem;
}
.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 {
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 = {
serverPort: 8080,
downloadLocation: './'
};
const YoutubeHelper = require('./youtubehelper');
module.exports.config = config;
module.exports.Main = async () =>
{
await Config.Load();
module.exports.main = async () => {
await server.init();
await server.listen();
await YoutubeHelper.InitResolverCache();
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'
module.exports.log = function(log) {
let d = moment().format(dateFormat);
console.log('[' + d.toLocaleString() + '] ' + log);
module.exports.Log = (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 main = require('./index');
const youtube = require('./youtubehelper');
const Logger = require('./logger')
const Config = require('./config');
const YoutubeHelper = require('./youtubehelper');
const YoutubeDownloader = require('./youtubedownloader');
const express = require('express');
@@ -8,42 +10,138 @@ let app;
let http;
let io;
module.exports.init = async () => {
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'));
module.exports.Listen = async () =>
{
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) => {
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;
module.exports.http = http;
module.exports.io = io;
function SocketHandler(socket)
{
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 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');
}
});
}
const Config = require('./config');
const Logger = require('./logger');
const YTDL = require('ytdl-core');
// TODO: does the video resolver need a queue?
// cache
let ResolutionCache = [];
function HitCache(id)
{
if (ResolutionCache[id])
{
Logger.Log(`Cache hit! ${id}`)
ResolutionCache[id].LastUsed = Date.now();
return ResolutionCache[id].Video;
} else
{
return false;
}
}
function RegisterCache(video)
{
ResolutionCache[video.player_response.videoDetails.videoId] = {
Id: video.player_response.videoDetails.videoId,
Video: video,
LastUsed: Date.now(),
TimeCreated: Date.now()
}
}
function CleanCache()
{
// TODO: fix weird bug where a cleaned
// entry's timeused lingers
let ExpiredEntrys = [];
for (id in ResolutionCache)
{
entry = ResolutionCache[id];
// if cache entry has expired
const LastUsed = Date.now() - entry.LastUsed;
if (LastUsed > Config.Configuration.CacheTimeToUse)
{
// remove expired entry
Logger.Log(`Cache entry '${id}' expired`);
ExpiredEntrys.push(id);
delete entry;
}
}
for (expiredEntry of ExpiredEntrys)
{
delete ResolutionCache[expiredEntry];
}
}
module.exports.InitResolverCache = async () =>
{
setInterval(CleanCache, Config.Configuration.CacheCleanInterval);
Logger.Log('Video resolver cache settup')
}
module.exports.GetVideoInfoArr = async (arr) =>
{
let ret = [];
// TODO: make async AND retain order
for (video of arr)
{
ret.push(await this.GetVideoInfo(video));
}
return ret;
}
module.exports.GetVideoInfo = async (video) =>
{
try
{
const CacheHit = HitCache(video);
let Video = {};
if (CacheHit)
{
Video = CacheHit
} else
{
// TODO: is the YouTube API faster for this?
Logger.Log(`Resolving '${video}'`);
Video = await YTDL.getInfo(video);
// register the info into the cache
RegisterCache(Video);
}
let Res = BuildBasicInfoFromInfo(Video);
return Res;
} catch (e)
{
Logger.Log(`Error resolving video '${video}', ${e}`);
return {
id: video,
Error: true,
Errors: [ "Video cannot resolve" ]
};
}
}
function BuildBasicInfoFromInfo(info)
{
let ret = {};
ret.id = info.player_response.videoDetails.videoId;
ret.title = info.player_response.videoDetails.title;
ret.desc = info.player_response.videoDetails.shortDescription;
ret.views = info.player_response.videoDetails.viewCount;
if (!info.player_response.videoDetails.thumbnail.thumbnails[0])
{
ret.thumbnail = info.player_response.videoDetails.thumbnail.thumbnails[1].url;
} 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;
}