mirror of
https://github.com/Tzahi12345/YoutubeDL-Material.git
synced 2026-03-07 20:10:03 +03:00
Compare commits
37 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
393ed5a210 | ||
|
|
54492b109a | ||
|
|
7eac88a31f | ||
|
|
8fec9639eb | ||
|
|
a15e1f98fa | ||
|
|
6604484765 | ||
|
|
c58f8a4058 | ||
|
|
8545016f1d | ||
|
|
9b1e84821e | ||
|
|
6505fad7bc | ||
|
|
d245904c0d | ||
|
|
0095ea1271 | ||
|
|
b41d10f514 | ||
|
|
8e3d6a0af6 | ||
|
|
1e4995c5ce | ||
|
|
710e3613a8 | ||
|
|
28331c1037 | ||
|
|
202c0718b7 | ||
|
|
a985963661 | ||
|
|
f673b325fd | ||
|
|
5f4a7a1e69 | ||
|
|
d54b2a73c8 | ||
|
|
8e7bb4ba3b | ||
|
|
d595de5786 | ||
|
|
31394fa98c | ||
|
|
af595d3df8 | ||
|
|
81377a2b38 | ||
|
|
35bdd1deeb | ||
|
|
a1ec53edb9 | ||
|
|
aa130d3fc9 | ||
|
|
0f0bf3a401 | ||
|
|
73b9c61080 | ||
|
|
501806909a | ||
|
|
77dd96b3b9 | ||
|
|
505b145bb3 | ||
|
|
ba5592015d | ||
|
|
10c90a01f2 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -45,4 +45,5 @@ node_modules/*
|
||||
backend/node_modules/*
|
||||
YoutubeDL-Material/node_modules/*
|
||||
backend/video/*
|
||||
backend/audio/*
|
||||
backend/audio/*
|
||||
src/assets/default.json
|
||||
|
||||
21
.vscode/launch.json
vendored
Normal file
21
.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"type": "node",
|
||||
"request": "attach",
|
||||
"name": "Attach",
|
||||
"port": 9229
|
||||
},
|
||||
{
|
||||
"type": "chrome",
|
||||
"request": "launch",
|
||||
"name": "Launch chrome against localhost",
|
||||
"url": "http://localhost:4200",
|
||||
"webRoot": "${workspaceFolder}"
|
||||
}
|
||||
]
|
||||
}
|
||||
57
angular.json
57
angular.json
@@ -24,7 +24,7 @@
|
||||
"src/backend"
|
||||
],
|
||||
"styles": [
|
||||
"src/styles.css"
|
||||
"src/styles.scss"
|
||||
],
|
||||
"scripts": []
|
||||
},
|
||||
@@ -65,6 +65,57 @@
|
||||
"browserTarget": "youtube-dl-material:build"
|
||||
}
|
||||
},
|
||||
"serve-electron": {
|
||||
"builder": "@angular-guru/electron-builder:dev-server",
|
||||
"options": {
|
||||
"browserTarget": "youtube-dl-material:build"
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"browserTarget": "youtube-dl-material:build:production"
|
||||
}
|
||||
}
|
||||
},
|
||||
"electron": {
|
||||
"builder": "@angular-guru/electron-builder:build",
|
||||
"options": {
|
||||
"outputPath": "dist",
|
||||
"index": "src/index.html",
|
||||
"main": "main.js",
|
||||
"tsConfig": "src/tsconfig.app.json",
|
||||
"polyfills": "src/polyfills.ts",
|
||||
"assets": [
|
||||
"src/assets",
|
||||
"src/favicon.ico",
|
||||
"src/backend/audio",
|
||||
"src/backend/video",
|
||||
"src/backend"
|
||||
],
|
||||
"styles": [
|
||||
"src/styles.scss"
|
||||
],
|
||||
"scripts": []
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"optimization": true,
|
||||
"outputHashing": "all",
|
||||
"sourceMap": false,
|
||||
"extractCss": true,
|
||||
"namedChunks": false,
|
||||
"aot": true,
|
||||
"extractLicenses": true,
|
||||
"vendorChunk": false,
|
||||
"buildOptimizer": true,
|
||||
"fileReplacements": [
|
||||
{
|
||||
"replace": "src/environments/environment.ts",
|
||||
"with": "src/environments/environment.prod.ts"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"test": {
|
||||
"builder": "@angular-devkit/build-angular:karma",
|
||||
"options": {
|
||||
@@ -74,7 +125,7 @@
|
||||
"tsConfig": "src/tsconfig.spec.json",
|
||||
"scripts": [],
|
||||
"styles": [
|
||||
"src/styles.css"
|
||||
"src/styles.scss"
|
||||
],
|
||||
"assets": [
|
||||
"src/assets",
|
||||
@@ -125,7 +176,7 @@
|
||||
"schematics": {
|
||||
"@schematics/angular:component": {
|
||||
"prefix": "app",
|
||||
"styleext": "css"
|
||||
"styleext": "scss"
|
||||
},
|
||||
"@schematics/angular:directive": {
|
||||
"prefix": "app"
|
||||
|
||||
529
backend/app.js
529
backend/app.js
@@ -6,17 +6,40 @@ var config = require('config');
|
||||
var https = require('https');
|
||||
var express = require("express");
|
||||
var bodyParser = require("body-parser");
|
||||
var archiver = require('archiver');
|
||||
const low = require('lowdb')
|
||||
var URL = require('url').URL;
|
||||
const shortid = require('shortid')
|
||||
|
||||
var app = express();
|
||||
|
||||
var URL = require('url').URL;
|
||||
const FileSync = require('lowdb/adapters/FileSync')
|
||||
const adapter = new FileSync('db.json');
|
||||
const db = low(adapter)
|
||||
|
||||
var frontendUrl = config.get("YoutubeDLMaterial.Host.frontendurl");
|
||||
// Set some defaults
|
||||
db.defaults({ playlists: {
|
||||
audio: [],
|
||||
video: []
|
||||
}}).write();
|
||||
|
||||
|
||||
// check if debug mode
|
||||
let debugMode = process.env.YTDL_MODE === 'debug';
|
||||
|
||||
if (debugMode) console.log('YTDL-Material in debug mode!');
|
||||
|
||||
var frontendUrl = !debugMode ? config.get("YoutubeDLMaterial.Host.frontendurl") : 'http://localhost:4200';
|
||||
var backendUrl = config.get("YoutubeDLMaterial.Host.backendurl")
|
||||
var backendPort = 17442;
|
||||
var usingEncryption = config.get("YoutubeDLMaterial.Encryption.use-encryption");
|
||||
var basePath = config.get("YoutubeDLMaterial.Downloader.path-base");
|
||||
var audioPath = config.get("YoutubeDLMaterial.Downloader.path-audio");
|
||||
var videoPath = config.get("YoutubeDLMaterial.Downloader.path-video");
|
||||
var audioFolderPath = config.get("YoutubeDLMaterial.Downloader.path-audio");
|
||||
var videoFolderPath = config.get("YoutubeDLMaterial.Downloader.path-video");
|
||||
var downloadOnlyMode = config.get("YoutubeDLMaterial.Extra.download_only_mode")
|
||||
|
||||
var descriptors = {};
|
||||
|
||||
|
||||
if (usingEncryption)
|
||||
{
|
||||
@@ -79,7 +102,7 @@ function getThumbnailMp4(name)
|
||||
|
||||
function getFileSizeMp3(name)
|
||||
{
|
||||
var jsonPath = audioPath+name+".mp3.info.json";
|
||||
var jsonPath = audioFolderPath+name+".mp3.info.json";
|
||||
|
||||
if (fs.existsSync(jsonPath))
|
||||
var obj = JSON.parse(fs.readFileSync(jsonPath, 'utf8'));
|
||||
@@ -91,7 +114,7 @@ function getFileSizeMp3(name)
|
||||
|
||||
function getFileSizeMp4(name)
|
||||
{
|
||||
var jsonPath = videoPath+name+".info.json";
|
||||
var jsonPath = videoFolderPath+name+".info.json";
|
||||
var filesize = 0;
|
||||
if (fs.existsSync(jsonPath))
|
||||
{
|
||||
@@ -111,7 +134,7 @@ function getFileSizeMp4(name)
|
||||
|
||||
function getJSONMp3(name)
|
||||
{
|
||||
var jsonPath = audioPath+name+".mp3.info.json";
|
||||
var jsonPath = audioFolderPath+name+".mp3.info.json";
|
||||
if (fs.existsSync(jsonPath))
|
||||
var obj = JSON.parse(fs.readFileSync(jsonPath, 'utf8'));
|
||||
else
|
||||
@@ -122,7 +145,7 @@ function getJSONMp3(name)
|
||||
|
||||
function getJSONMp4(name)
|
||||
{
|
||||
var jsonPath = videoPath+name+".info.json";
|
||||
var jsonPath = videoFolderPath+name+".info.json";
|
||||
if (fs.existsSync(jsonPath))
|
||||
{
|
||||
var obj = JSON.parse(fs.readFileSync(jsonPath, 'utf8'));
|
||||
@@ -133,7 +156,7 @@ function getJSONMp4(name)
|
||||
|
||||
function getAmountDownloadedMp3(name)
|
||||
{
|
||||
var partPath = audioPath+name+".mp3.part";
|
||||
var partPath = audioFolderPath+name+".mp3.part";
|
||||
if (fs.existsSync(partPath))
|
||||
{
|
||||
const stats = fs.statSync(partPath);
|
||||
@@ -149,7 +172,7 @@ function getAmountDownloadedMp3(name)
|
||||
function getAmountDownloadedMp4(name)
|
||||
{
|
||||
var format = getVideoFormatID(name);
|
||||
var partPath = videoPath+name+".f"+format+".mp4.part";
|
||||
var partPath = videoFolderPath+name+".f"+format+".mp4.part";
|
||||
if (fs.existsSync(partPath))
|
||||
{
|
||||
const stats = fs.statSync(partPath);
|
||||
@@ -162,7 +185,7 @@ function getAmountDownloadedMp4(name)
|
||||
|
||||
function getVideoFormatID(name)
|
||||
{
|
||||
var jsonPath = videoPath+name+".info.json";
|
||||
var jsonPath = videoFolderPath+name+".info.json";
|
||||
if (fs.existsSync(jsonPath))
|
||||
{
|
||||
var obj = JSON.parse(fs.readFileSync(jsonPath, 'utf8'));
|
||||
@@ -171,68 +194,317 @@ function getVideoFormatID(name)
|
||||
}
|
||||
}
|
||||
|
||||
async function createPlaylistZipFile(fileNames, type, outputName) {
|
||||
return new Promise(async resolve => {
|
||||
let zipFolderPath = path.join(__dirname, (type === 'audio') ? audioFolderPath : videoFolderPath);
|
||||
// let name = fileNames[0].split(' ')[0] + fileNames[1].split(' ')[0];
|
||||
let ext = (type === 'audio') ? '.mp3' : '.mp4';
|
||||
|
||||
let output = fs.createWriteStream(path.join(zipFolderPath, outputName + '.zip'));
|
||||
|
||||
var archive = archiver('zip', {
|
||||
gzip: true,
|
||||
zlib: { level: 9 } // Sets the compression level.
|
||||
});
|
||||
|
||||
archive.on('error', function(err) {
|
||||
console.log(err);
|
||||
throw err;
|
||||
});
|
||||
|
||||
// pipe archive data to the output file
|
||||
archive.pipe(output);
|
||||
|
||||
for (let i = 0; i < fileNames.length; i++) {
|
||||
let fileName = fileNames[i];
|
||||
archive.file(zipFolderPath + fileName + ext, {name: fileName + ext})
|
||||
}
|
||||
|
||||
await archive.finalize();
|
||||
|
||||
// wait a tiny bit for the zip to reload in fs
|
||||
setTimeout(function() {
|
||||
resolve(path.join(zipFolderPath,outputName + '.zip'));
|
||||
}, 100);
|
||||
|
||||
});
|
||||
|
||||
|
||||
}
|
||||
|
||||
function deleteAudioFile(name) {
|
||||
return new Promise(resolve => {
|
||||
// TODO: split descriptors into audio and video descriptors, as deleting an audio file will close all video file streams
|
||||
var jsonPath = path.join(audioFolderPath,name+'.mp3.info.json');
|
||||
var audioFilePath = path.join(audioFolderPath,name+'.mp3');
|
||||
jsonPath = path.join(__dirname, jsonPath);
|
||||
audioFilePath = path.join(__dirname, audioFilePath);
|
||||
|
||||
let jsonExists = fs.existsSync(jsonPath);
|
||||
let audioFileExists = fs.existsSync(audioFilePath);
|
||||
|
||||
if (descriptors[name]) {
|
||||
try {
|
||||
for (let i = 0; i < descriptors[name].length; i++) {
|
||||
descriptors[name][i].destroy();
|
||||
}
|
||||
} catch(e) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
if (jsonExists) fs.unlinkSync(jsonPath);
|
||||
if (audioFileExists) {
|
||||
fs.unlink(audioFilePath, function(err) {
|
||||
if (fs.existsSync(jsonPath) || fs.existsSync(audioFilePath)) {
|
||||
resolve(false);
|
||||
} else {
|
||||
resolve(true);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// TODO: tell user that the file didn't exist
|
||||
resolve(true);
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
async function deleteVideoFile(name) {
|
||||
return new Promise(resolve => {
|
||||
var jsonPath = path.join(videoFolderPath,name+'.info.json');
|
||||
var videoFilePath = path.join(videoFolderPath,name+'.mp4');
|
||||
jsonPath = path.join(__dirname, jsonPath);
|
||||
videoFilePath = path.join(__dirname, videoFilePath);
|
||||
|
||||
jsonExists = fs.existsSync(jsonPath);
|
||||
videoFileExists = fs.existsSync(videoFilePath);
|
||||
|
||||
if (descriptors[name]) {
|
||||
try {
|
||||
for (let i = 0; i < descriptors[name].length; i++) {
|
||||
descriptors[name][i].destroy();
|
||||
}
|
||||
} catch(e) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
if (jsonExists) fs.unlinkSync(jsonPath);
|
||||
if (videoFileExists) {
|
||||
fs.unlink(videoFilePath, function(err) {
|
||||
if (fs.existsSync(jsonPath) || fs.existsSync(videoFilePath)) {
|
||||
resolve(false);
|
||||
} else {
|
||||
resolve(true);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// TODO: tell user that the file didn't exist
|
||||
resolve(true);
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
function getAudioInfos(fileNames) {
|
||||
let result = [];
|
||||
for (let i = 0; i < fileNames.length; i++) {
|
||||
let fileName = fileNames[i];
|
||||
let fileLocation = audioFolderPath+fileName+'.mp3.info.json';
|
||||
if (fs.existsSync(fileLocation)) {
|
||||
let data = fs.readFileSync(fileLocation);
|
||||
try {
|
||||
result.push(JSON.parse(data));
|
||||
} catch(e) {
|
||||
console.log(`ERROR: Could not find info for file ${fileName}.mp3`);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function getVideoInfos(fileNames) {
|
||||
let result = [];
|
||||
for (let i = 0; i < fileNames.length; i++) {
|
||||
let fileName = fileNames[i];
|
||||
let fileLocation = videoFolderPath+fileName+'.info.json';
|
||||
if (fs.existsSync(fileLocation)) {
|
||||
let data = fs.readFileSync(fileLocation);
|
||||
try {
|
||||
result.push(JSON.parse(data));
|
||||
} catch(e) {
|
||||
console.log(`ERROR: Could not find info for file ${fileName}.mp4`);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// currently only works for single urls
|
||||
async function getUrlInfos(urls) {
|
||||
let result = [];
|
||||
return new Promise(resolve => {
|
||||
youtubedl.exec(urls.join(' '), ['--external-downloader', 'aria2c', '--dump-json'], {}, (err, output) => {
|
||||
if (err) {
|
||||
console.log('Error during parsing:' + err);
|
||||
resolve(null);
|
||||
}
|
||||
let try_putput = null;
|
||||
try {
|
||||
try_putput = JSON.parse(output);
|
||||
result = try_putput;
|
||||
} catch(e) {
|
||||
// probably multiple urls
|
||||
console.log('failed to parse');
|
||||
console.log(output);
|
||||
}
|
||||
resolve(result);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
app.post('/tomp3', function(req, res) {
|
||||
var url = req.body.url;
|
||||
var date = Date.now();
|
||||
var path = audioPath;
|
||||
var audiopath = Date.now();
|
||||
youtubedl.exec(url, ['--external-downloader', 'aria2c', '-o', path + audiopath + ".mp3", '-x', '--audio-format', 'mp3', '--write-info-json'], {}, function(err, output) {
|
||||
var path = audioFolderPath;
|
||||
var audiopath = '%(title)s';
|
||||
|
||||
var customQualityConfiguration = req.body.customQualityConfiguration;
|
||||
var maxBitrate = req.body.maxBitrate;
|
||||
|
||||
let downloadConfig = ['--external-downloader', 'aria2c', '-o', path + audiopath + ".mp3", '-x', '--audio-format', 'mp3', '--write-info-json', '--print-json']
|
||||
let qualityPath = '';
|
||||
|
||||
if (customQualityConfiguration) {
|
||||
qualityPath = `-f ${customQualityConfiguration}`;
|
||||
} else if (maxBitrate) {
|
||||
if (!maxBitrate || maxBitrate === '') maxBitrate = '0';
|
||||
qualityPath = `--audio-quality ${maxBitrate}`
|
||||
}
|
||||
|
||||
if (qualityPath !== '') {
|
||||
downloadConfig.splice(2, 0, qualityPath);
|
||||
}
|
||||
|
||||
youtubedl.exec(url, downloadConfig, {}, function(err, output) {
|
||||
if (debugMode) {
|
||||
let new_date = Date.now();
|
||||
let difference = (new_date - date)/1000;
|
||||
console.log(`Audio download delay: ${difference} seconds.`);
|
||||
}
|
||||
if (err) {
|
||||
audiopath = "-1";
|
||||
res.sendStatus(500);
|
||||
throw err;
|
||||
} else if (output) {
|
||||
var file_names = [];
|
||||
for (let i = 0; i < output.length; i++) {
|
||||
let output_json = null;
|
||||
try {
|
||||
output_json = JSON.parse(output[i]);
|
||||
} catch(e) {
|
||||
output_json = null;
|
||||
}
|
||||
if (!output_json) {
|
||||
// only run on first go
|
||||
return;
|
||||
}
|
||||
var file_name = output_json['_filename'].replace(/^.*[\\\/]/, '');
|
||||
var alternate_file_name = file_name.substring(0, file_name.length-4);
|
||||
if (alternate_file_name) file_names.push(alternate_file_name);
|
||||
}
|
||||
|
||||
let is_playlist = file_names.length > 1;
|
||||
if (!is_playlist) audiopath = file_names[0];
|
||||
|
||||
var audiopathEncoded = encodeURIComponent(file_names[0]);
|
||||
res.send({
|
||||
audiopathEncoded: audiopathEncoded,
|
||||
file_names: is_playlist ? file_names : null
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// write file info
|
||||
|
||||
/*
|
||||
youtubedl.getInfo(url, function(err, info) {
|
||||
if (err) throw err;
|
||||
|
||||
var size = info.size;
|
||||
fs.writeFile("data/"+audiopath, size, function(err) {
|
||||
if(err) {
|
||||
return console.log(err);
|
||||
}
|
||||
|
||||
console.log("The file was saved!");
|
||||
});
|
||||
});
|
||||
*/
|
||||
var completeString = "done";
|
||||
var audiopathEncoded = encodeURIComponent(audiopath);
|
||||
res.send({
|
||||
audiopathEncoded: audiopathEncoded
|
||||
});
|
||||
res.end("yes");
|
||||
});
|
||||
|
||||
app.post('/tomp4', function(req, res) {
|
||||
var url = req.body.url;
|
||||
var date = Date.now();
|
||||
var path = videoPath;
|
||||
var videopath = Date.now();
|
||||
youtubedl.exec(url, ['--external-downloader', 'aria2c', '-o', path + videopath + ".mp4", '-f', 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/mp4', '--write-info-json'], {}, function(err, output) {
|
||||
var path = videoFolderPath;
|
||||
var videopath = '%(title)s';
|
||||
|
||||
var selectedHeight = req.body.selectedHeight;
|
||||
var customQualityConfiguration = req.body.customQualityConfiguration;
|
||||
|
||||
let qualityPath = 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/mp4';
|
||||
|
||||
if (customQualityConfiguration) {
|
||||
qualityPath = customQualityConfiguration;
|
||||
} else if (selectedHeight && selectedHeight !== '') {
|
||||
qualityPath = `bestvideo[height=${selectedHeight}]+bestaudio/best[height=${selectedHeight}]`;
|
||||
}
|
||||
|
||||
youtubedl.exec(url, ['--external-downloader', 'aria2c', '-o', path + videopath + ".mp4", '-f', qualityPath, '--write-info-json', '--print-json'], {}, function(err, output) {
|
||||
if (debugMode) {
|
||||
let new_date = Date.now();
|
||||
let difference = (new_date - date)/1000;
|
||||
console.log(`Video download delay: ${difference} seconds.`);
|
||||
}
|
||||
if (err) {
|
||||
videopath = "-1";
|
||||
res.sendStatus(500);
|
||||
throw err;
|
||||
} else if (output) {
|
||||
var file_names = [];
|
||||
for (let i = 0; i < output.length; i++) {
|
||||
let output_json = null;
|
||||
try {
|
||||
output_json = JSON.parse(output[i]);
|
||||
} catch(e) {
|
||||
output_json = null;
|
||||
}
|
||||
var modified_file_name = output_json ? output_json['title'] : null;
|
||||
if (!output_json) {
|
||||
continue;
|
||||
}
|
||||
var file_name = output_json['_filename'].replace(/^.*[\\\/]/, '');
|
||||
|
||||
// renames file if necessary due to bug
|
||||
if (!fs.existsSync(output_json['_filename'] && fs.existsSync(output_json['_filename'] + '.webm'))) {
|
||||
try {
|
||||
fs.renameSync(output_json['_filename'] + '.webm', output_json['_filename']);
|
||||
console.log('Renamed ' + file_name + '.webm to ' + file_name);
|
||||
} catch(e) {
|
||||
}
|
||||
}
|
||||
var alternate_file_name = file_name.substring(0, file_name.length-4);
|
||||
if (alternate_file_name) file_names.push(alternate_file_name);
|
||||
}
|
||||
|
||||
let is_playlist = file_names.length > 1;
|
||||
if (!is_playlist) audiopath = file_names[0];
|
||||
|
||||
var videopathEncoded = encodeURIComponent(file_names[0]);
|
||||
res.send({
|
||||
videopathEncoded: videopathEncoded,
|
||||
file_names: is_playlist ? file_names : null
|
||||
});
|
||||
res.end("yes");
|
||||
}
|
||||
});
|
||||
var completeString = "done";
|
||||
var videopathEncoded = encodeURIComponent(videopath);
|
||||
res.send({
|
||||
videopathEncoded: videopathEncoded
|
||||
});
|
||||
res.end("yes");
|
||||
});
|
||||
|
||||
// gets the status of the mp3 file that's being downloaded
|
||||
app.post('/fileStatusMp3', function(req, res) {
|
||||
var name = req.body.name + "";
|
||||
var name = decodeURI(req.body.name + "");
|
||||
var exists = "";
|
||||
var fullpath = audioPath + name + ".mp3";
|
||||
var fullpath = audioFolderPath + name + ".mp3";
|
||||
if (fs.existsSync(fullpath)) {
|
||||
exists = [basePath + audioPath + name, getFileSizeMp3(name)];
|
||||
exists = [basePath + audioFolderPath + name, getFileSizeMp3(name)];
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -250,14 +522,12 @@ app.post('/fileStatusMp3', function(req, res) {
|
||||
|
||||
// gets the status of the mp4 file that's being downloaded
|
||||
app.post('/fileStatusMp4', function(req, res) {
|
||||
var name = req.body.name;
|
||||
var name = decodeURI(req.body.name);
|
||||
var exists = "";
|
||||
var fullpath = videoPath + name + ".mp4";
|
||||
var fullpath = videoFolderPath + name + ".mp4";
|
||||
if (fs.existsSync(fullpath)) {
|
||||
exists = [basePath + videoPath + name, getFileSizeMp4(name)];
|
||||
}
|
||||
else
|
||||
{
|
||||
exists = [basePath + videoFolderPath + name, getFileSizeMp4(name)];
|
||||
} else {
|
||||
var percent = 0;
|
||||
var size = getFileSizeMp4(name);
|
||||
var downloaded = getAmountDownloadedMp4(name);
|
||||
@@ -273,8 +543,9 @@ app.post('/fileStatusMp4', function(req, res) {
|
||||
// gets all download mp3s
|
||||
app.post('/getMp3s', function(req, res) {
|
||||
var mp3s = [];
|
||||
var fullpath = audioPath;
|
||||
var files = fs.readdirSync(audioPath);
|
||||
var playlists = db.get('playlists.audio').value();
|
||||
var fullpath = audioFolderPath;
|
||||
var files = fs.readdirSync(audioFolderPath);
|
||||
|
||||
for (var i in files)
|
||||
{
|
||||
@@ -301,7 +572,8 @@ app.post('/getMp3s', function(req, res) {
|
||||
}
|
||||
|
||||
res.send({
|
||||
mp3s: mp3s
|
||||
mp3s: mp3s,
|
||||
playlists: playlists
|
||||
});
|
||||
res.end("yes");
|
||||
});
|
||||
@@ -309,8 +581,9 @@ app.post('/getMp3s', function(req, res) {
|
||||
// gets all download mp4s
|
||||
app.post('/getMp4s', function(req, res) {
|
||||
var mp4s = [];
|
||||
var fullpath = videoPath;
|
||||
var files = fs.readdirSync(videoPath);
|
||||
var playlists = db.get('playlists.video').value();
|
||||
var fullpath = videoFolderPath;
|
||||
var files = fs.readdirSync(videoFolderPath);
|
||||
|
||||
for (var i in files)
|
||||
{
|
||||
@@ -337,21 +610,64 @@ app.post('/getMp4s', function(req, res) {
|
||||
}
|
||||
|
||||
res.send({
|
||||
mp4s: mp4s
|
||||
mp4s: mp4s,
|
||||
playlists: playlists
|
||||
});
|
||||
res.end("yes");
|
||||
});
|
||||
|
||||
app.post('/createPlaylist', async (req, res) => {
|
||||
let playlistName = req.body.playlistName;
|
||||
let fileNames = req.body.fileNames;
|
||||
let type = req.body.type;
|
||||
let thumbnailURL = req.body.thumbnailURL;
|
||||
|
||||
let new_playlist = {
|
||||
'name': playlistName,
|
||||
fileNames: fileNames,
|
||||
id: shortid.generate(),
|
||||
thumbnailURL: thumbnailURL
|
||||
};
|
||||
|
||||
db.get(`playlists.${type}`)
|
||||
.push(new_playlist)
|
||||
.write();
|
||||
|
||||
res.send({
|
||||
new_playlist: new_playlist,
|
||||
success: !!new_playlist // always going to be true
|
||||
})
|
||||
});
|
||||
|
||||
app.post('/deletePlaylist', async (req, res) => {
|
||||
let playlistID = req.body.playlistID;
|
||||
let type = req.body.type;
|
||||
|
||||
let success = null;
|
||||
try {
|
||||
// removes playlist from playlists
|
||||
db.get(`playlists.${type}`)
|
||||
.remove({id: playlistID})
|
||||
.write();
|
||||
|
||||
success = true;
|
||||
} catch(e) {
|
||||
success = false;
|
||||
}
|
||||
|
||||
res.send({
|
||||
success: success
|
||||
})
|
||||
});
|
||||
|
||||
// deletes mp3 file
|
||||
app.post('/deleteMp3', function(req, res) {
|
||||
app.post('/deleteMp3', async (req, res) => {
|
||||
var name = req.body.name;
|
||||
var fullpath = audioPath + name + ".mp3";
|
||||
var fullpath = audioFolderPath + name + ".mp3";
|
||||
var wasDeleted = false;
|
||||
if (fs.existsSync(fullpath))
|
||||
{
|
||||
fs.unlink(fullpath, call => {
|
||||
|
||||
});
|
||||
deleteAudioFile(name);
|
||||
wasDeleted = true;
|
||||
res.send(wasDeleted);
|
||||
res.end("yes");
|
||||
@@ -365,16 +681,14 @@ app.post('/deleteMp3', function(req, res) {
|
||||
});
|
||||
|
||||
// deletes mp4 file
|
||||
app.post('/deleteMp4', function(req, res) {
|
||||
app.post('/deleteMp4', async (req, res) => {
|
||||
var name = req.body.name;
|
||||
var fullpath = videoPath + name + ".mp4";
|
||||
var fullpath = videoFolderPath + name + ".mp4";
|
||||
var wasDeleted = false;
|
||||
if (fs.existsSync(fullpath))
|
||||
{
|
||||
fs.unlink(fullpath, call => {
|
||||
// console.log(call);
|
||||
});
|
||||
wasDeleted = true;
|
||||
wasDeleted = await deleteVideoFile(name);
|
||||
// wasDeleted = true;
|
||||
res.send(wasDeleted);
|
||||
res.end("yes");
|
||||
}
|
||||
@@ -386,10 +700,39 @@ app.post('/deleteMp4', function(req, res) {
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/downloadFile', async (req, res) => {
|
||||
let fileNames = req.body.fileNames;
|
||||
let is_playlist = req.body.is_playlist;
|
||||
let type = req.body.type;
|
||||
let outputName = req.body.outputName;
|
||||
let file = null;
|
||||
if (!is_playlist) {
|
||||
if (type === 'audio') {
|
||||
file = __dirname + '/' + 'audio/' + fileNames + '.mp3';
|
||||
} else if (type === 'video') {
|
||||
file = __dirname + '/' + 'video/' + fileNames + '.mp4';
|
||||
}
|
||||
} else {
|
||||
file = await createPlaylistZipFile(fileNames, type, outputName);
|
||||
}
|
||||
|
||||
res.sendFile(file);
|
||||
});
|
||||
|
||||
app.post('/deleteFile', async (req, res) => {
|
||||
let fileName = req.body.fileName;
|
||||
let type = req.body.type;
|
||||
if (type === 'audio') {
|
||||
deleteAudioFile(fileName);
|
||||
} else if (type === 'video') {
|
||||
deleteVideoFile(fileName);
|
||||
}
|
||||
res.send()
|
||||
});
|
||||
|
||||
app.get('/video/:id', function(req , res){
|
||||
var head;
|
||||
const path = "video/" + req.params.id + ".mp4";
|
||||
const path = "video/" + req.params.id + '.mp4';
|
||||
const stat = fs.statSync(path)
|
||||
const fileSize = stat.size
|
||||
const range = req.headers.range
|
||||
@@ -401,6 +744,13 @@ app.get('/video/:id', function(req , res){
|
||||
: fileSize-1
|
||||
const chunksize = (end-start)+1
|
||||
const file = fs.createReadStream(path, {start, end})
|
||||
if (descriptors[req.params.id]) descriptors[req.params.id].push(file);
|
||||
else descriptors[req.params.id] = [file];
|
||||
file.on('close', function() {
|
||||
let index = descriptors[req.params.id].indexOf(file);
|
||||
descriptors[req.params.id].splice(index, 1);
|
||||
if (debugMode) console.log('Successfully closed stream and removed file reference.');
|
||||
});
|
||||
head = {
|
||||
'Content-Range': `bytes ${start}-${end}/${fileSize}`,
|
||||
'Accept-Ranges': 'bytes',
|
||||
@@ -421,7 +771,8 @@ app.get('/video/:id', function(req , res){
|
||||
|
||||
app.get('/audio/:id', function(req , res){
|
||||
var head;
|
||||
const path = "audio/" + req.params.id + ".mp3";
|
||||
let path = "audio/" + req.params.id + '.mp3';
|
||||
path = path.replace(/\"/g, '\'');
|
||||
const stat = fs.statSync(path)
|
||||
const fileSize = stat.size
|
||||
const range = req.headers.range
|
||||
@@ -432,7 +783,14 @@ app.get('/audio/:id', function(req , res){
|
||||
? parseInt(parts[1], 10)
|
||||
: fileSize-1
|
||||
const chunksize = (end-start)+1
|
||||
const file = fs.createReadStream(path, {start, end})
|
||||
const file = fs.createReadStream(path, {start, end});
|
||||
if (descriptors[req.params.id]) descriptors[req.params.id].push(file);
|
||||
else descriptors[req.params.id] = [file];
|
||||
file.on('close', function() {
|
||||
let index = descriptors[req.params.id].indexOf(file);
|
||||
descriptors[req.params.id].splice(index, 1);
|
||||
if (debugMode) console.log('Successfully closed stream and removed file reference.');
|
||||
});
|
||||
head = {
|
||||
'Content-Range': `bytes ${start}-${end}/${fileSize}`,
|
||||
'Accept-Ranges': 'bytes',
|
||||
@@ -452,6 +810,27 @@ app.get('/audio/:id', function(req , res){
|
||||
});
|
||||
|
||||
|
||||
app.post('/getVideoInfos', async (req, res) => {
|
||||
let fileNames = req.body.fileNames;
|
||||
let urlMode = !!req.body.urlMode;
|
||||
let type = req.body.type;
|
||||
let result = null;
|
||||
if (!urlMode) {
|
||||
if (type === 'audio') {
|
||||
result = getAudioInfos(fileNames)
|
||||
} else if (type === 'video') {
|
||||
result = getVideoInfos(fileNames);
|
||||
}
|
||||
} else {
|
||||
result = await getUrlInfos(fileNames);
|
||||
}
|
||||
res.send({
|
||||
result: result,
|
||||
success: !!result
|
||||
})
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
if (usingEncryption)
|
||||
|
||||
@@ -16,7 +16,17 @@
|
||||
},
|
||||
"Extra": {
|
||||
"title_top": "Youtube Downloader",
|
||||
"file_manager_enabled": true
|
||||
"file_manager_enabled": true,
|
||||
"allow_quality_select": true,
|
||||
"download_only_mode": false
|
||||
},
|
||||
"API": {
|
||||
"use_youtube_API": false,
|
||||
"youtube_API_key": ""
|
||||
},
|
||||
"Themes": {
|
||||
"default_theme": "default",
|
||||
"allow_theme_change": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,17 @@
|
||||
},
|
||||
"Extra": {
|
||||
"title_top": "Youtube Downloader",
|
||||
"file_manager_enabled": true
|
||||
"file_manager_enabled": true,
|
||||
"allow_quality_select": true,
|
||||
"download_only_mode": false
|
||||
},
|
||||
"API": {
|
||||
"use_youtube_API": false,
|
||||
"youtube_API_key": ""
|
||||
},
|
||||
"Themes": {
|
||||
"default_theme": "default",
|
||||
"allow_theme_change": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,26 +1,29 @@
|
||||
{
|
||||
"name": "backend",
|
||||
"version": "1.0.0",
|
||||
"description": "backend for hda",
|
||||
"description": "backend for YoutubeDL-Material",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/Tzahi12345/hda-backend.git"
|
||||
"url": ""
|
||||
},
|
||||
"author": "Isaac Grynsztein",
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/Tzahi12345/hda-backend/issues"
|
||||
"url": ""
|
||||
},
|
||||
"homepage": "https://github.com/Tzahi12345/hda-backend#readme",
|
||||
"homepage": "",
|
||||
"dependencies": {
|
||||
"archiver": "^3.1.1",
|
||||
"async": "^3.1.0",
|
||||
"config": "^3.2.3",
|
||||
"exe": "^1.0.2",
|
||||
"express": "^4.17.1",
|
||||
"youtube-dl": "^2.0.1"
|
||||
"lowdb": "^1.0.0",
|
||||
"shortid": "^2.2.15",
|
||||
"youtube-dl": "^2.3.0"
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
41
main.js
Normal file
41
main.js
Normal file
@@ -0,0 +1,41 @@
|
||||
const { app, BrowserWindow } = require('electron');
|
||||
const path = require('path');
|
||||
const url = require('url');
|
||||
|
||||
let win;
|
||||
|
||||
function createWindow() {
|
||||
win = new BrowserWindow({ width: 800, height: 600 });
|
||||
|
||||
// load the dist folder from Angular
|
||||
win.loadURL(
|
||||
url.format({
|
||||
pathname: path.join(__dirname, `/dist/index.html`),
|
||||
protocol: 'file:',
|
||||
slashes: true
|
||||
})
|
||||
);
|
||||
|
||||
// The following is optional and will open the DevTools:
|
||||
// win.webContents.openDevTools()
|
||||
|
||||
win.on('closed', () => {
|
||||
win = null;
|
||||
});
|
||||
}
|
||||
|
||||
app.on('ready', createWindow);
|
||||
|
||||
// on macOS, closing the window doesn't quit the app
|
||||
app.on('window-all-closed', () => {
|
||||
if (process.platform !== 'darwin') {
|
||||
app.quit();
|
||||
}
|
||||
});
|
||||
|
||||
// initialize the app's main window
|
||||
app.on('activate', () => {
|
||||
if (win === null) {
|
||||
createWindow();
|
||||
}
|
||||
});
|
||||
13
package.json
13
package.json
@@ -8,7 +8,8 @@
|
||||
"build": "ng build",
|
||||
"test": "ng test",
|
||||
"lint": "ng lint",
|
||||
"e2e": "ng e2e"
|
||||
"e2e": "ng e2e",
|
||||
"electron": "ng build --base-href ./ && electron ."
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
@@ -25,20 +26,28 @@
|
||||
"@angular/platform-browser-dynamic": "^8.2.11",
|
||||
"@angular/router": "^8.2.11",
|
||||
"core-js": "^2.4.1",
|
||||
"file-saver": "^2.0.2",
|
||||
"ng-lazyload-image": "^7.0.1",
|
||||
"ng4-configure": "^0.1.7",
|
||||
"ngx-content-loading": "^0.1.3",
|
||||
"rxjs": "^6.5.3",
|
||||
"rxjs-compat": "^6.0.0-rc.0",
|
||||
"tslib": "^1.10.0",
|
||||
"videogular2": "^7.0.1",
|
||||
"web-animations-js": "^2.3.2",
|
||||
"zone.js": "~0.9.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-devkit/build-angular": "~0.803.12",
|
||||
"@angular-devkit/build-angular": "^0.803.24",
|
||||
"@angular/cli": "^8.3.12",
|
||||
"@angular/compiler-cli": "^8.2.11",
|
||||
"@angular/language-service": "^8.2.11",
|
||||
"@types/core-js": "^2.5.2",
|
||||
"@types/file-saver": "^2.0.1",
|
||||
"@types/jasmine": "2.5.45",
|
||||
"@types/node": "~6.0.60",
|
||||
"codelyzer": "^5.0.1",
|
||||
"electron": "^8.0.1",
|
||||
"jasmine-core": "~2.6.2",
|
||||
"jasmine-spec-reporter": "~4.1.0",
|
||||
"karma": "~1.7.0",
|
||||
|
||||
15
src/_palette.scss
Normal file
15
src/_palette.scss
Normal file
@@ -0,0 +1,15 @@
|
||||
/* Coolors Exported Palette - coolors.co/e8aeb7-b8e1ff-a9fff7-94fbab-82aba1 */
|
||||
|
||||
/* HSL */
|
||||
$color1: hsla(351%, 56%, 80%, 1);
|
||||
$softblue: hsla(205%, 100%, 86%, 1);
|
||||
$color3: hsla(174%, 100%, 83%, 1);
|
||||
$color4: hsla(133%, 93%, 78%, 1);
|
||||
$color5: hsla(165%, 20%, 59%, 1);
|
||||
|
||||
/* RGB */
|
||||
$color1: rgba(232, 174, 183, 1);
|
||||
$softblue: rgba(184, 225, 255, 1);
|
||||
$color3: rgba(169, 255, 247, 1);
|
||||
$color4: rgba(148, 251, 171, 1);
|
||||
$color5: rgba(130, 171, 161, 1);
|
||||
15
src/app/app-routing.module.ts
Normal file
15
src/app/app-routing.module.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
import { MainComponent } from './main/main.component';
|
||||
import { PlayerComponent } from './player/player.component';
|
||||
const routes: Routes = [
|
||||
{ path: 'home', component: MainComponent },
|
||||
{ path: 'player', component: PlayerComponent},
|
||||
{ path: '', redirectTo: '/home', pathMatch: 'full' },
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forRoot(routes, { useHash: true })],
|
||||
exports: [RouterModule]
|
||||
})
|
||||
export class AppRoutingModule { }
|
||||
@@ -1,56 +1,13 @@
|
||||
.demo-card {
|
||||
margin: 16px;
|
||||
}
|
||||
|
||||
.demo-basic {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.demo-basic .mat-card-content {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
mat-toolbar.top {
|
||||
height: 60px;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/*::ng-deep .mat-form-field-placeholder{
|
||||
|
||||
transform: scale(.75) translateY(20px) !important;
|
||||
}*/
|
||||
|
||||
.big {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.centered {
|
||||
margin: 0 auto;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
}
|
||||
|
||||
.example-full-width {
|
||||
.flex-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
mat-form-field.mat-form-field {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
position: absolute;
|
||||
display: inline-block;
|
||||
margin-left: -28px;
|
||||
margin-top: -10px;
|
||||
}
|
||||
|
||||
.make-room-for-spinner {
|
||||
padding-right: 40px;
|
||||
}
|
||||
|
||||
.equal-sizes {
|
||||
padding-right: 20px;
|
||||
|
||||
.flex-column {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-basis: 100%;
|
||||
flex: 1;
|
||||
}
|
||||
@@ -1,105 +1,17 @@
|
||||
<mat-toolbar color="primary" class="top">
|
||||
<table width="100%" height="100%">
|
||||
<td class="topbar" style="text-align: left; left:0px; font-size: 15px">
|
||||
</td>
|
||||
<td class="topbar" style="text-align: center">
|
||||
<div style="margin-top: 14px">{{topBarTitle}}</div>
|
||||
</td>
|
||||
<td class="topbar" style="text-align: right">
|
||||
|
||||
</td>
|
||||
</table>
|
||||
</mat-toolbar>
|
||||
|
||||
<br/>
|
||||
|
||||
<div class="big demo-basic">
|
||||
<mat-card id="card" style="margin-right: 20px; margin-left: 20px;">
|
||||
<mat-card-title>
|
||||
Youtube Downloader
|
||||
</mat-card-title>
|
||||
<mat-card-content>
|
||||
<div style="position: relative;">
|
||||
<form class="example-form">
|
||||
<mat-form-field class="example-full-width">
|
||||
<input matInput [(ngModel)]="url" placeholder="URL" type="url" name="url" [formControl]="urlForm" required>
|
||||
<mat-error *ngIf="urlError || urlForm.invalid">Please enter a valid URL!</mat-error>
|
||||
</mat-form-field>
|
||||
</form>
|
||||
<br/>
|
||||
<mat-checkbox [(ngModel)]="audioOnly" style="float: left; margin-top: -12px">Only Audio</mat-checkbox>
|
||||
|
||||
<div [style.background]="postsService.theme ? postsService.theme.background_color : null" style="width: 100%; height: 100%;">
|
||||
<mat-toolbar color="primary" class="top">
|
||||
<div class="flex-row" width="100%" height="100%">
|
||||
<div class="flex-column" style="text-align: left; margin-top: 1px;">
|
||||
<button (click)="goBack()" *ngIf="router.url.split(';')[0] === '/player'" mat-icon-button><mat-icon>arrow_back</mat-icon></button>
|
||||
</div>
|
||||
</mat-card-content>
|
||||
<mat-card-actions>
|
||||
<button style="margin-left: 8px; margin-bottom: 8px" (click)="downloadClicked()" [disabled]="downloadingfile" type="submit" mat-stroked-button
|
||||
color="primary">Download</button>
|
||||
</mat-card-actions>
|
||||
</mat-card>
|
||||
</div>
|
||||
<br/>
|
||||
<div class="centered big" id="bar_div" *ngIf="downloadingfile;else nofile">
|
||||
<div [ngClass]="(determinateProgress && percentDownloaded === 100)?'make-room-for-spinner':'equal-sizes'" style="display: inline-block; width: 100%; padding-left: 20px" *ngIf="determinateProgress;else indeterminateprogress">
|
||||
<mat-progress-bar mode="determinate" value="{{percentDownloaded}}"></mat-progress-bar>
|
||||
<br/>
|
||||
</div>
|
||||
<div *ngIf="determinateProgress && percentDownloaded === 100" class="spinner">
|
||||
<mat-spinner [diameter]="25"></mat-spinner>
|
||||
</div>
|
||||
<ng-template #indeterminateprogress>
|
||||
<mat-progress-bar mode="indeterminate"></mat-progress-bar>
|
||||
</ng-template>
|
||||
<br/>
|
||||
</div>
|
||||
<ng-template #nofile>
|
||||
|
||||
</ng-template>
|
||||
<div style="margin: 20px" *ngIf="fileManagerEnabled">
|
||||
<mat-accordion>
|
||||
<mat-expansion-panel class="big">
|
||||
<mat-expansion-panel-header>
|
||||
<mat-panel-title>
|
||||
Audio
|
||||
</mat-panel-title>
|
||||
<mat-panel-description>
|
||||
Your audio files are here
|
||||
</mat-panel-description>
|
||||
</mat-expansion-panel-header>
|
||||
<div *ngIf="mp3s.length > 0;else nomp3s">
|
||||
<mat-grid-list cols="4" rowHeight="150px">
|
||||
<mat-grid-tile *ngFor="let file of mp3s; index as i;">
|
||||
<app-file-card (removeFile)="removeFromMp3($event)" [title]="file.title" [name]="file.id" [thumbnailURL]="file.thumbnailURL"
|
||||
[length]="file.duration" [isAudio]="true"></app-file-card>
|
||||
</mat-grid-tile>
|
||||
</mat-grid-list>
|
||||
<div class="flex-column" style="text-align: center; margin-top: 5px;">
|
||||
<div>{{topBarTitle}}</div>
|
||||
</div>
|
||||
|
||||
</mat-expansion-panel>
|
||||
<mat-expansion-panel class="big">
|
||||
<mat-expansion-panel-header>
|
||||
<mat-panel-title>
|
||||
Video
|
||||
</mat-panel-title>
|
||||
<mat-panel-description>
|
||||
Your video files are here
|
||||
</mat-panel-description>
|
||||
</mat-expansion-panel-header>
|
||||
<div *ngIf="mp4s.length > 0;else nomp4s">
|
||||
<mat-grid-list cols="4" rowHeight="150px">
|
||||
<mat-grid-tile *ngFor="let file of mp4s; index as i;">
|
||||
<app-file-card (removeFile)="removeFromMp4($event)" [title]="file.title" [name]="file.id" [thumbnailURL]="file.thumbnailURL"
|
||||
[length]="file.duration" [isAudio]="false"></app-file-card>
|
||||
</mat-grid-tile>
|
||||
</mat-grid-list>
|
||||
<div class="flex-column" style="text-align: right; align-items: flex-end;">
|
||||
<button *ngIf="allowThemeChange" mat-icon-button (click)="flipTheme()"><mat-icon>{{(postsService.theme.key === 'default') ? 'brightness_5' : 'brightness_2'}}</mat-icon></button>
|
||||
</div>
|
||||
</mat-expansion-panel>
|
||||
</mat-accordion>
|
||||
</div>
|
||||
</div>
|
||||
</mat-toolbar>
|
||||
|
||||
<ng-template #nomp3s>
|
||||
|
||||
</ng-template>
|
||||
|
||||
<ng-template #nomp4s>
|
||||
|
||||
</ng-template>
|
||||
<router-outlet></router-outlet>
|
||||
</div>
|
||||
@@ -1,213 +1,119 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { Component, OnInit, ElementRef, ViewChild, HostBinding } from '@angular/core';
|
||||
import {PostsService} from './posts.services';
|
||||
import {FileCardComponent} from './file-card/file-card.component';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import {FormControl, Validators} from '@angular/forms';
|
||||
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
|
||||
import {MatSnackBar} from '@angular/material';
|
||||
import { saveAs } from 'file-saver';
|
||||
import 'rxjs/add/observable/of';
|
||||
import 'rxjs/add/operator/mapTo';
|
||||
import 'rxjs/add/operator/toPromise';
|
||||
import 'rxjs/add/observable/fromEvent'
|
||||
import 'rxjs/add/operator/filter'
|
||||
import 'rxjs/add/operator/debounceTime'
|
||||
import 'rxjs/add/operator/do'
|
||||
import 'rxjs/add/operator/switch'
|
||||
import { YoutubeSearchService, Result } from './youtube-search.service';
|
||||
import { Router } from '@angular/router';
|
||||
import { OverlayContainer } from '@angular/cdk/overlay';
|
||||
import { THEMES_CONFIG } from '../themes';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
templateUrl: './app.component.html',
|
||||
styleUrls: ['./app.component.css']
|
||||
})
|
||||
export class AppComponent {
|
||||
determinateProgress = false;
|
||||
downloadingfile = false;
|
||||
audioOnly: boolean;
|
||||
urlError = false;
|
||||
path = '';
|
||||
url = '';
|
||||
exists = '';
|
||||
export class AppComponent implements OnInit {
|
||||
|
||||
@HostBinding('class') componentCssClass;
|
||||
THEMES_CONFIG = THEMES_CONFIG;
|
||||
|
||||
// config items
|
||||
topBarTitle = 'Youtube Downloader';
|
||||
percentDownloaded: number;
|
||||
fileManagerEnabled = false;
|
||||
|
||||
mp3s: any[] = [];
|
||||
mp4s: any[] = [];
|
||||
|
||||
urlForm = new FormControl('', [Validators.required]);
|
||||
|
||||
constructor(private postsService: PostsService, public snackBar: MatSnackBar) {
|
||||
this.audioOnly = false;
|
||||
defaultTheme = null;
|
||||
allowThemeChange = null;
|
||||
|
||||
@ViewChild('urlinput', { read: ElementRef, static: false }) urlInput: ElementRef;
|
||||
|
||||
constructor(public postsService: PostsService, public snackBar: MatSnackBar,
|
||||
public router: Router, public overlayContainer: OverlayContainer) {
|
||||
|
||||
// loading config
|
||||
this.postsService.loadNavItems().subscribe(result => { // loads settings
|
||||
const backendUrl = result['YoutubeDLMaterial']['Host']['backendurl'];
|
||||
this.topBarTitle = result['YoutubeDLMaterial']['Extra']['title_top'];
|
||||
this.fileManagerEnabled = result['YoutubeDLMaterial']['Extra']['file_manager_enabled'];
|
||||
const themingExists = result['YoutubeDLMaterial']['Themes'];
|
||||
this.defaultTheme = themingExists ? result['YoutubeDLMaterial']['Themes']['default_theme'] : 'default';
|
||||
this.allowThemeChange = themingExists ? result['YoutubeDLMaterial']['Themes']['allow_theme_change'] : true;
|
||||
|
||||
this.postsService.path = backendUrl;
|
||||
this.postsService.startPath = backendUrl;
|
||||
this.postsService.startPathSSL = backendUrl;
|
||||
|
||||
if (this.fileManagerEnabled) {
|
||||
this.getMp3s();
|
||||
this.getMp4s();
|
||||
// sets theme to config default if it doesn't exist
|
||||
if (!localStorage.getItem('theme')) {
|
||||
this.setTheme(themingExists ? this.defaultTheme : 'default');
|
||||
}
|
||||
}, error => {
|
||||
console.log(error);
|
||||
});
|
||||
|
||||
}
|
||||
/*
|
||||
|
||||
doHandshake(url: string) {
|
||||
this.postsService.startHandshake(url).subscribe(theurl => {
|
||||
this.postsService.path = theurl;
|
||||
this.postsService.handShakeComplete = true;
|
||||
console.log('Handshake complete!');
|
||||
}, error => {
|
||||
console.log('Initial handshake failed on http.');
|
||||
this.doHandshakeSSL(url);
|
||||
});
|
||||
}
|
||||
// theme stuff
|
||||
|
||||
doHandshakeSSL(url: string) {
|
||||
this.postsService.startHandshakeSSL(url).subscribe(theurl => {
|
||||
this.postsService.path = theurl;
|
||||
this.postsService.handShakeComplete = true;
|
||||
console.log('Handshake complete!');
|
||||
},
|
||||
error => {
|
||||
console.log('Initial handshake failed on https too! Make sure port 17442 is open.');
|
||||
this.postsService.handShakeComplete = false;
|
||||
});
|
||||
}*/
|
||||
|
||||
getMp3s() {
|
||||
this.postsService.getMp3s().subscribe(result => {
|
||||
const mp3s = result['mp3s'];
|
||||
this.mp3s = mp3s;
|
||||
}, error => {
|
||||
console.log(error);
|
||||
});
|
||||
}
|
||||
|
||||
getMp4s() {
|
||||
this.postsService.getMp4s().subscribe(result => {
|
||||
const mp4s = result['mp4s'];
|
||||
this.mp4s = mp4s;
|
||||
},
|
||||
error => {
|
||||
console.log(error);
|
||||
});
|
||||
}
|
||||
|
||||
public goToFile(name, isAudio) {
|
||||
if (isAudio) {
|
||||
this.downloadHelperMp3(name);
|
||||
setTheme(theme) {
|
||||
// theme is registered, so set it to the stored cookie variable
|
||||
let old_theme = null;
|
||||
if (this.THEMES_CONFIG[theme]) {
|
||||
if (localStorage.getItem('theme')) {
|
||||
old_theme = localStorage.getItem('theme');
|
||||
if (!this.THEMES_CONFIG[old_theme]) {
|
||||
console.log('bad theme found, setting to default');
|
||||
if (this.defaultTheme === null) {
|
||||
// means it hasn't loaded yet
|
||||
console.error('No default theme detected');
|
||||
} else {
|
||||
localStorage.setItem('theme', this.defaultTheme);
|
||||
old_theme = localStorage.getItem('theme'); // updates old_theme
|
||||
}
|
||||
}
|
||||
}
|
||||
localStorage.setItem('theme', theme);
|
||||
} else {
|
||||
this.downloadHelperMp4(name);
|
||||
console.error('Invalid theme: ' + theme);
|
||||
return;
|
||||
}
|
||||
|
||||
this.postsService.setTheme(theme);
|
||||
|
||||
this.onSetTheme(this.THEMES_CONFIG[theme]['css_label'], old_theme ? this.THEMES_CONFIG[old_theme]['css_label'] : old_theme);
|
||||
}
|
||||
|
||||
onSetTheme(theme, old_theme) {
|
||||
if (old_theme) {
|
||||
document.body.classList.remove(old_theme);
|
||||
this.overlayContainer.getContainerElement().classList.remove(old_theme);
|
||||
}
|
||||
this.overlayContainer.getContainerElement().classList.add(theme);
|
||||
this.componentCssClass = theme;
|
||||
}
|
||||
|
||||
public removeFromMp3(name: string) {
|
||||
for (let i = 0; i < this.mp3s.length; i++) {
|
||||
if (this.mp3s[i].id === name) {
|
||||
this.mp3s.splice(i, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public removeFromMp4(name: string) {
|
||||
console.log(name);
|
||||
console.log(this.mp4s);
|
||||
for (let i = 0; i < this.mp4s.length; i++) {
|
||||
if (this.mp4s[i].id === name) {
|
||||
this.mp4s.splice(i, 1);
|
||||
}
|
||||
flipTheme() {
|
||||
if (this.postsService.theme.key === 'default') {
|
||||
this.setTheme('dark');
|
||||
} else if (this.postsService.theme.key === 'dark') {
|
||||
this.setTheme('default');
|
||||
}
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
}
|
||||
|
||||
downloadHelperMp3(name: string) {
|
||||
this.postsService.getFileStatusMp3(name).subscribe(fileExists => {
|
||||
const exists = fileExists;
|
||||
this.exists = exists[0];
|
||||
if (exists[0] === 'failed') {
|
||||
const percent = exists[2];
|
||||
console.log(percent);
|
||||
if (percent > 0.30) {
|
||||
this.determinateProgress = true;
|
||||
this.percentDownloaded = percent * 100;
|
||||
}
|
||||
setTimeout(() => this.downloadHelperMp3(name), 500);
|
||||
} else {
|
||||
window.location.href = this.exists;
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
downloadHelperMp4(name: string) {
|
||||
this.postsService.getFileStatusMp4(name).subscribe(fileExists => {
|
||||
const exists = fileExists;
|
||||
this.exists = exists[0];
|
||||
if (exists[0] === 'failed') {
|
||||
const percent = exists[2];
|
||||
if (percent > 0.30) {
|
||||
this.determinateProgress = true;
|
||||
this.percentDownloaded = percent * 100;
|
||||
}
|
||||
setTimeout(() => this.downloadHelperMp4(name), 500);
|
||||
} else {
|
||||
window.location.href = this.exists;
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
downloadClicked() {
|
||||
if (this.ValidURL(this.url)) {
|
||||
this.urlError = false;
|
||||
this.path = '';
|
||||
|
||||
if (this.audioOnly) {
|
||||
this.downloadingfile = true;
|
||||
this.postsService.makeMP3(this.url).subscribe(posts => {
|
||||
this.path = posts['audiopathEncoded'];
|
||||
if (this.path !== '-1') {
|
||||
this.downloadHelperMp3(this.path);
|
||||
}
|
||||
}, error => { // can't access server
|
||||
this.downloadingfile = false;
|
||||
this.openSnackBar('Download failed!', 'OK.');
|
||||
});
|
||||
} else {
|
||||
this.downloadingfile = true;
|
||||
this.postsService.makeMP4(this.url).subscribe(posts => {
|
||||
this.path = posts['videopathEncoded'];
|
||||
if (this.path !== '-1') {
|
||||
this.downloadHelperMp4(this.path);
|
||||
}
|
||||
}, error => { // can't access server
|
||||
this.downloadingfile = false;
|
||||
this.openSnackBar('Download failed!', 'OK.');
|
||||
});
|
||||
}
|
||||
if (localStorage.getItem('theme')) {
|
||||
this.setTheme(localStorage.getItem('theme'));
|
||||
} else {
|
||||
this.urlError = true;
|
||||
//
|
||||
}
|
||||
}
|
||||
|
||||
ValidURL(str) {
|
||||
// tslint:disable-next-line: max-line-length
|
||||
const strRegex = /((([A-Za-z]{3,9}:(?:\/\/)?)(?:[-;:&=\+\$,\w]+@)?[A-Za-z0-9.-]+|(?:www.|[-;:&=\+\$,\w]+@)[A-Za-z0-9.-]+)((?:\/[\+~%\/.\w-_]*)?\??(?:[-\+=&;%@.\w_]*)#?(?:[\w]*))?)/;
|
||||
const re = new RegExp(strRegex);
|
||||
return re.test(str);
|
||||
}
|
||||
|
||||
public openSnackBar(message: string, action: string) {
|
||||
this.snackBar.open(message, action, {
|
||||
duration: 2000,
|
||||
});
|
||||
goBack() {
|
||||
this.router.navigate(['/home']);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,9 @@ import {MatNativeDateModule, MatRadioModule, MatInputModule, MatButtonModule, Ma
|
||||
MatSnackBarModule, MatCardModule, MatSelectModule, MatToolbarModule, MatCheckboxModule, MatGridListModule,
|
||||
MatProgressBarModule, MatExpansionModule,
|
||||
MatGridList,
|
||||
MatProgressSpinnerModule} from '@angular/material';
|
||||
MatProgressSpinnerModule,
|
||||
MatButtonToggleModule,
|
||||
MatDialogModule} from '@angular/material';
|
||||
import {FormsModule, ReactiveFormsModule} from '@angular/forms';
|
||||
import { AppComponent } from './app.component';
|
||||
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
|
||||
@@ -14,11 +16,24 @@ import { PostsService } from 'app/posts.services';
|
||||
import {APP_BASE_HREF} from '@angular/common';
|
||||
import { FileCardComponent } from './file-card/file-card.component';
|
||||
import {RouterModule} from '@angular/router';
|
||||
import { AppRoutingModule } from './app-routing.module';
|
||||
import { MainComponent } from './main/main.component';
|
||||
import { PlayerComponent } from './player/player.component';
|
||||
import {VgCoreModule} from 'videogular2/compiled/core';
|
||||
import {VgControlsModule} from 'videogular2/compiled/controls';
|
||||
import {VgOverlayPlayModule} from 'videogular2/compiled/overlay-play';
|
||||
import {VgBufferingModule} from 'videogular2/compiled/buffering';
|
||||
import { InputDialogComponent } from './input-dialog/input-dialog.component';
|
||||
import { LazyLoadImageModule } from 'ng-lazyload-image';
|
||||
import { NgxContentLoadingModule } from 'ngx-content-loading';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AppComponent,
|
||||
FileCardComponent
|
||||
FileCardComponent,
|
||||
MainComponent,
|
||||
PlayerComponent,
|
||||
InputDialogComponent
|
||||
],
|
||||
imports: [
|
||||
BrowserModule,
|
||||
@@ -43,7 +58,19 @@ import {RouterModule} from '@angular/router';
|
||||
MatExpansionModule,
|
||||
MatProgressBarModule,
|
||||
MatProgressSpinnerModule,
|
||||
RouterModule
|
||||
MatButtonToggleModule,
|
||||
MatDialogModule,
|
||||
VgCoreModule,
|
||||
VgControlsModule,
|
||||
VgOverlayPlayModule,
|
||||
VgBufferingModule,
|
||||
LazyLoadImageModule,
|
||||
NgxContentLoadingModule,
|
||||
RouterModule,
|
||||
AppRoutingModule,
|
||||
],
|
||||
entryComponents: [
|
||||
InputDialogComponent
|
||||
],
|
||||
providers: [PostsService],
|
||||
bootstrap: [AppComponent]
|
||||
|
||||
@@ -30,4 +30,30 @@
|
||||
margin: 0 auto;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
}
|
||||
|
||||
.img-div {
|
||||
max-height: 80px;
|
||||
padding: 0px;
|
||||
margin: 0px 0px 0px -5px;
|
||||
width: calc(100% + 5px + 5px);
|
||||
}
|
||||
|
||||
.max-two-lines {
|
||||
display: -webkit-box;
|
||||
display: -moz-box;
|
||||
max-height: 2.4em;
|
||||
line-height: 1.2em;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
}
|
||||
|
||||
@media (max-width: 576px){
|
||||
|
||||
.example-card {
|
||||
width: 125px !important;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,13 +1,21 @@
|
||||
<mat-card class="example-card">
|
||||
<mat-card class="example-card mat-elevation-z6">
|
||||
<button (click)="deleteFile()" class="deleteButton" mat-icon-button><mat-icon>delete_forever</mat-icon></button>
|
||||
<div style="padding:5px">
|
||||
<b><a href="javascript:void(0)" (click)="appComponent.goToFile(name, isAudio)">{{title}}</a></b>
|
||||
<b><a href="javascript:void(0)" (click)="!isPlaylist ? mainComponent.goToFile(name, isAudio) : mainComponent.goToPlaylist(name, type)">{{title}}</a></b>
|
||||
<br/>
|
||||
ID: {{name}}
|
||||
<span class="max-two-lines">ID: {{name}}</span>
|
||||
<div *ngIf="isPlaylist">Count: {{count}}</div>
|
||||
<div class="img-div">
|
||||
<img class="image" [lazyLoad]="thumbnailURL" (onLoad)="imageLoaded($event)" alt="Thumbnail">
|
||||
<span *ngIf="!image_loaded">
|
||||
<ngx-content-loading [width]="500" [height]="360">
|
||||
<svg:g ngx-rect width="500" height="360" y="0" x="0" rx="4" ry="4"></svg:g>
|
||||
</ngx-content-loading>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="centered example-full-width-height"><img class="image" src="{{thumbnailURL}}" alt="Thumbnail"></div>
|
||||
|
||||
</mat-card>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Component, OnInit, Input, Output } from '@angular/core';
|
||||
import {PostsService} from '../posts.services';
|
||||
import {MatSnackBar} from '@angular/material';
|
||||
import {AppComponent} from '../app.component';
|
||||
import {EventEmitter} from '@angular/core';
|
||||
import { MainComponent } from 'app/main/main.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-file-card',
|
||||
@@ -11,31 +11,41 @@ import {EventEmitter} from '@angular/core';
|
||||
})
|
||||
export class FileCardComponent implements OnInit {
|
||||
|
||||
@Input() title:string;
|
||||
@Input() length:string;
|
||||
@Input() name:string;
|
||||
@Input() title: string;
|
||||
@Input() length: string;
|
||||
@Input() name: string;
|
||||
@Input() thumbnailURL: string;
|
||||
@Input() isAudio: boolean = true;
|
||||
@Input() isAudio = true;
|
||||
@Output() removeFile: EventEmitter<string> = new EventEmitter<string>();
|
||||
@Input() isPlaylist = false;
|
||||
@Input() count = null;
|
||||
type;
|
||||
image_loaded = false;
|
||||
|
||||
constructor(private postsService: PostsService, public snackBar: MatSnackBar, public appComponent: AppComponent) { }
|
||||
constructor(private postsService: PostsService, public snackBar: MatSnackBar, public mainComponent: MainComponent) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.type = this.isAudio ? 'audio' : 'video';
|
||||
}
|
||||
|
||||
deleteFile()
|
||||
{
|
||||
this.postsService.deleteFile(this.name, this.isAudio).subscribe(result => {
|
||||
if (result == true)
|
||||
{
|
||||
this.openSnackBar("Delete success!", "OK.");
|
||||
this.removeFile.emit(this.name);
|
||||
}
|
||||
else
|
||||
{
|
||||
this.openSnackBar("Delete failed!", "OK.");
|
||||
}
|
||||
});
|
||||
deleteFile() {
|
||||
if (!this.isPlaylist) {
|
||||
this.postsService.deleteFile(this.name, this.isAudio).subscribe(result => {
|
||||
if (result === true) {
|
||||
this.openSnackBar('Delete success!', 'OK.');
|
||||
this.removeFile.emit(this.name);
|
||||
} else {
|
||||
this.openSnackBar('Delete failed!', 'OK.');
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.removeFile.emit(this.name);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
imageLoaded(loaded) {
|
||||
this.image_loaded = true;
|
||||
}
|
||||
|
||||
public openSnackBar(message: string, action: string) {
|
||||
|
||||
3
src/app/input-dialog/input-dialog.component.css
Normal file
3
src/app/input-dialog/input-dialog.component.css
Normal file
@@ -0,0 +1,3 @@
|
||||
.mat-spinner {
|
||||
margin-left: 5%;
|
||||
}
|
||||
16
src/app/input-dialog/input-dialog.component.html
Normal file
16
src/app/input-dialog/input-dialog.component.html
Normal file
@@ -0,0 +1,16 @@
|
||||
<h4 mat-dialog-title>{{inputTitle}}</h4>
|
||||
<mat-dialog-content>
|
||||
<div>
|
||||
<mat-form-field color="accent">
|
||||
<input matInput (keyup.enter)="enterPressed()" [(ngModel)]="inputText" [placeholder]="inputPlaceholder">
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</mat-dialog-content>
|
||||
<mat-dialog-actions>
|
||||
<button mat-button mat-dialog-close>Cancel</button>
|
||||
<!-- The mat-dialog-close directive optionally accepts a value as a result for the dialog. -->
|
||||
<button mat-button [disabled]="!inputText" type="submit" (click)="enterPressed()">{{submitText}}</button>
|
||||
<div class="mat-spinner" *ngIf="inputSubmitted">
|
||||
<mat-spinner [diameter]="25"></mat-spinner>
|
||||
</div>
|
||||
</mat-dialog-actions>
|
||||
25
src/app/input-dialog/input-dialog.component.spec.ts
Normal file
25
src/app/input-dialog/input-dialog.component.spec.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { InputDialogComponent } from './input-dialog.component';
|
||||
|
||||
describe('InputDialogComponent', () => {
|
||||
let component: InputDialogComponent;
|
||||
let fixture: ComponentFixture<InputDialogComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ InputDialogComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(InputDialogComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
50
src/app/input-dialog/input-dialog.component.ts
Normal file
50
src/app/input-dialog/input-dialog.component.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { Component, OnInit, Input, Inject, EventEmitter } from '@angular/core';
|
||||
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material';
|
||||
|
||||
@Component({
|
||||
selector: 'app-input-dialog',
|
||||
templateUrl: './input-dialog.component.html',
|
||||
styleUrls: ['./input-dialog.component.css']
|
||||
})
|
||||
export class InputDialogComponent implements OnInit {
|
||||
|
||||
inputTitle: string;
|
||||
inputPlaceholder: string;
|
||||
submitText: string;
|
||||
|
||||
inputText = '';
|
||||
|
||||
inputSubmitted = false;
|
||||
|
||||
doneEmitter: EventEmitter<any> = null;
|
||||
onlyEmitOnDone = false;
|
||||
|
||||
constructor(public dialogRef: MatDialogRef<InputDialogComponent>,
|
||||
@Inject(MAT_DIALOG_DATA) public data: any) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.inputTitle = this.data.inputTitle;
|
||||
this.inputPlaceholder = this.data.inputPlaceholder;
|
||||
this.submitText = this.data.submitText;
|
||||
|
||||
// checks if emitter exists, if so don't autoclose as it should be handled by caller
|
||||
if (this.data.doneEmitter) {
|
||||
this.doneEmitter = this.data.doneEmitter;
|
||||
this.onlyEmitOnDone = true;
|
||||
}
|
||||
}
|
||||
|
||||
enterPressed() {
|
||||
// validates input -- TODO: add custom validator
|
||||
if (this.inputText) {
|
||||
// only emit if emitter is passed
|
||||
if (this.onlyEmitOnDone) {
|
||||
this.doneEmitter.emit(this.inputText);
|
||||
this.inputSubmitted = true;
|
||||
} else {
|
||||
this.dialogRef.close(this.inputText);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
114
src/app/main/main.component.css
Normal file
114
src/app/main/main.component.css
Normal file
@@ -0,0 +1,114 @@
|
||||
.demo-card {
|
||||
margin: 16px;
|
||||
}
|
||||
|
||||
.demo-basic {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.demo-basic .mat-card-content {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
mat-toolbar.top {
|
||||
height: 60px;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/*::ng-deep .mat-form-field-placeholder{
|
||||
|
||||
transform: scale(.75) translateY(20px) !important;
|
||||
}*/
|
||||
|
||||
.big {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.centered {
|
||||
margin: 0 auto;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
}
|
||||
|
||||
.example-full-width {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.example-80-width {
|
||||
width: 80%
|
||||
}
|
||||
|
||||
mat-form-field.mat-form-field {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
position: absolute;
|
||||
display: inline-block;
|
||||
margin-left: -28px;
|
||||
margin-top: -10px;
|
||||
}
|
||||
|
||||
.make-room-for-spinner {
|
||||
padding-right: 40px;
|
||||
}
|
||||
|
||||
.equal-sizes {
|
||||
padding-right: 20px;
|
||||
}
|
||||
|
||||
.search-card-title {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.input-clear-button {
|
||||
position: absolute;
|
||||
right: -10px;
|
||||
top: 5px;
|
||||
}
|
||||
|
||||
.spinner-div {
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
top: 15px;
|
||||
right: -40px;
|
||||
}
|
||||
|
||||
.margined {
|
||||
margin-left: 20px;
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
.results-div {
|
||||
position: relative;
|
||||
top: -15px;
|
||||
}
|
||||
|
||||
.first-result-card {
|
||||
border-radius: 4px 4px 0px 0px !important;
|
||||
}
|
||||
|
||||
.last-result-card {
|
||||
border-radius: 0px 0px 4px 4px !important;
|
||||
}
|
||||
|
||||
.only-result-card {
|
||||
border-radius: 4px !important;
|
||||
}
|
||||
|
||||
.result-card {
|
||||
height: 120px;
|
||||
border-radius: 0px;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
.download-progress-bar {
|
||||
z-index: 999;
|
||||
position: absolute;
|
||||
bottom: 0px;
|
||||
width: 150px;
|
||||
}
|
||||
154
src/app/main/main.component.html
Normal file
154
src/app/main/main.component.html
Normal file
@@ -0,0 +1,154 @@
|
||||
<br/>
|
||||
<div class="big demo-basic">
|
||||
<mat-card id="card" style="margin-right: 20px; margin-left: 20px;">
|
||||
<mat-card-title>
|
||||
Youtube Downloader
|
||||
</mat-card-title>
|
||||
<mat-card-content>
|
||||
<div style="position: relative;">
|
||||
<form class="example-form">
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div [ngClass]="allowQualitySelect ? 'col-sm-9' : null" class="col-12">
|
||||
<mat-form-field color="accent" class="example-full-width">
|
||||
<input style="padding-right: 25px;" matInput (ngModelChange)="inputChanged($event)" [(ngModel)]="url" [placeholder]="'URL' + (youtubeSearchEnabled ? ' or search' : '')" type="url" name="url" [formControl]="urlForm" required #urlinput>
|
||||
<mat-error *ngIf="urlError || urlForm.invalid">Please enter a valid URL!</mat-error>
|
||||
<button class="input-clear-button" mat-icon-button (click)="clearInput()"><mat-icon>clear</mat-icon></button>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div *ngIf="allowQualitySelect" class="col-7 col-sm-3">
|
||||
<mat-form-field color="accent" style="display: inline-block; width: inherit; min-width: 120px;">
|
||||
<mat-label>Quality</mat-label>
|
||||
<mat-select [ngModelOptions]="{standalone: true}" [(ngModel)]="selectedQuality">
|
||||
<ng-container *ngFor="let option of qualityOptions[(audioOnly) ? 'audio' : 'video']">
|
||||
<mat-option *ngIf="option.value === '' || url && cachedAvailableFormats[url] && cachedAvailableFormats[url] && cachedAvailableFormats[url][(audioOnly) ? 'audio' : 'video'][option.value]" [value]="option.value">
|
||||
{{option.label}}
|
||||
</mat-option>
|
||||
</ng-container>
|
||||
</mat-select>
|
||||
<div class="spinner-div" *ngIf="formats_loading && !cachedAvailableFormats[url]">
|
||||
<mat-spinner [diameter]="25"></mat-spinner>
|
||||
</div>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="results-div" *ngIf="results_showing">
|
||||
<span *ngFor="let result of results; let i = index">
|
||||
<mat-card class="result-card mat-elevation-z7" [ngClass]="[(i === 0 && results.length > 1) ? 'first-result-card' : '', ((i === results.length-1) && results.length > 1) ? 'last-result-card' : '', (results.length === 1) ? 'only-result-card' : '']">
|
||||
<div class="search-card-title">
|
||||
{{result.title}}
|
||||
</div>
|
||||
<div style="font-size: 12px; margin-bottom: 10px;">
|
||||
{{result.uploaded}}
|
||||
</div>
|
||||
<button mat-flat-button color="primary" style="float: left;" (click)="useURL(result.videoUrl)">Use URL</button>
|
||||
<button mat-stroked-button color="primary" (click)="visitURL(result.videoUrl)" style="float: right">View</button>
|
||||
</mat-card>
|
||||
</span>
|
||||
</div>
|
||||
</form>
|
||||
<br/>
|
||||
<mat-checkbox (change)="videoModeChanged($event)" [(ngModel)]="audioOnly" style="float: left; margin-top: -12px">Only Audio</mat-checkbox>
|
||||
|
||||
</div>
|
||||
</mat-card-content>
|
||||
<mat-card-actions>
|
||||
<button style="margin-left: 8px; margin-bottom: 8px" (click)="downloadClicked()" [disabled]="downloadingfile" type="submit" mat-stroked-button
|
||||
color="accent">Download</button>
|
||||
</mat-card-actions>
|
||||
</mat-card>
|
||||
</div>
|
||||
<br/>
|
||||
<div class="centered big" id="bar_div" *ngIf="downloadingfile;else nofile">
|
||||
<div class="margined">
|
||||
<div [ngClass]="(determinateProgress && percentDownloaded === 100)?'make-room-for-spinner':'equal-sizes'" style="display: inline-block; width: 100%; padding-left: 20px" *ngIf="determinateProgress;else indeterminateprogress">
|
||||
<mat-progress-bar mode="determinate" value="{{percentDownloaded}}"></mat-progress-bar>
|
||||
<br/>
|
||||
</div>
|
||||
<div *ngIf="determinateProgress && percentDownloaded === 100" class="spinner">
|
||||
<mat-spinner [diameter]="25"></mat-spinner>
|
||||
</div>
|
||||
<ng-template #indeterminateprogress>
|
||||
<mat-progress-bar mode="indeterminate"></mat-progress-bar>
|
||||
</ng-template>
|
||||
</div>
|
||||
<br/>
|
||||
</div>
|
||||
<ng-template #nofile>
|
||||
|
||||
</ng-template>
|
||||
<div style="margin: 20px" *ngIf="fileManagerEnabled">
|
||||
<mat-accordion>
|
||||
<mat-expansion-panel class="big">
|
||||
<mat-expansion-panel-header>
|
||||
<mat-panel-title>
|
||||
Audio
|
||||
</mat-panel-title>
|
||||
<mat-panel-description>
|
||||
Your audio files are here
|
||||
</mat-panel-description>
|
||||
</mat-expansion-panel-header>
|
||||
<div *ngIf="mp3s.length > 0;else nomp3s">
|
||||
<mat-grid-list style="margin-bottom: 15px;" (window:resize)="onResize($event)" [cols]="files_cols" rowHeight="150px">
|
||||
<mat-grid-tile *ngFor="let file of mp3s; index as i;">
|
||||
<app-file-card (removeFile)="removeFromMp3($event)" [title]="file.title" [name]="file.id" [thumbnailURL]="file.thumbnailURL"
|
||||
[length]="file.duration" [isAudio]="true"></app-file-card>
|
||||
<mat-progress-bar *ngIf="downloading_content['audio'][file.id]" class="download-progress-bar" mode="indeterminate"></mat-progress-bar>
|
||||
</mat-grid-tile>
|
||||
</mat-grid-list>
|
||||
<mat-divider *ngIf="playlists.audio.length > 0"></mat-divider>
|
||||
<div style="width: 100%; text-align: center; margin-top: 10px;" *ngIf="playlists.audio.length > 0">
|
||||
<h6>Playlists</h6>
|
||||
</div>
|
||||
<mat-grid-list *ngIf="playlists.audio.length > 0" (window:resize)="onResize($event)" [cols]="files_cols" rowHeight="150px">
|
||||
<mat-grid-tile *ngFor="let playlist of playlists.audio; let i = index;">
|
||||
<app-file-card (removeFile)="removePlaylistMp3(playlist.id, i)" [title]="playlist.name" [name]="playlist.id" [thumbnailURL]="playlist_thumbnails[playlist.id]"
|
||||
[length]="null" [isAudio]="true" [isPlaylist]="true" [count]="playlist.fileNames.length"></app-file-card>
|
||||
<mat-progress-bar *ngIf="downloading_content['audio'][playlist.id]" class="download-progress-bar" mode="indeterminate"></mat-progress-bar>
|
||||
</mat-grid-tile>
|
||||
</mat-grid-list>
|
||||
</div>
|
||||
|
||||
</mat-expansion-panel>
|
||||
<mat-expansion-panel class="big">
|
||||
<mat-expansion-panel-header>
|
||||
<mat-panel-title>
|
||||
Video
|
||||
</mat-panel-title>
|
||||
<mat-panel-description>
|
||||
Your video files are here
|
||||
</mat-panel-description>
|
||||
</mat-expansion-panel-header>
|
||||
<div *ngIf="mp4s.length > 0;else nomp4s">
|
||||
<mat-grid-list style="margin-bottom: 15px;" (window:resize)="onResize($event)" [cols]="files_cols" rowHeight="150px">
|
||||
<mat-grid-tile *ngFor="let file of mp4s; index as i;">
|
||||
<app-file-card (removeFile)="removeFromMp4($event)" [title]="file.title" [name]="file.id" [thumbnailURL]="file.thumbnailURL"
|
||||
[length]="file.duration" [isAudio]="false"></app-file-card>
|
||||
<mat-progress-bar *ngIf="downloading_content['video'][file.id]" class="download-progress-bar" mode="indeterminate"></mat-progress-bar>
|
||||
</mat-grid-tile>
|
||||
</mat-grid-list>
|
||||
<mat-divider *ngIf="playlists.video.length > 0"></mat-divider>
|
||||
|
||||
<div style="width: 100%; text-align: center; margin-top: 10px;" *ngIf="playlists.video.length > 0">
|
||||
<h6>Playlists</h6>
|
||||
</div>
|
||||
<mat-grid-list *ngIf="playlists.video.length > 0" (window:resize)="onResize($event)" [cols]="files_cols" rowHeight="150px">
|
||||
<mat-grid-tile *ngFor="let playlist of playlists.video; let i = index;">
|
||||
<app-file-card (removeFile)="removePlaylistMp4(playlist.id, i)" [title]="playlist.name" [name]="playlist.id" [thumbnailURL]="playlist_thumbnails[playlist.id]"
|
||||
[length]="null" [isAudio]="false" [isPlaylist]="true" [count]="playlist.fileNames.length"></app-file-card>
|
||||
<mat-progress-bar *ngIf="downloading_content['video'][playlist.id]" class="download-progress-bar" mode="indeterminate"></mat-progress-bar>
|
||||
</mat-grid-tile>
|
||||
</mat-grid-list>
|
||||
</div>
|
||||
</mat-expansion-panel>
|
||||
</mat-accordion>
|
||||
</div>
|
||||
|
||||
<ng-template #nomp3s>
|
||||
|
||||
</ng-template>
|
||||
|
||||
<ng-template #nomp4s>
|
||||
|
||||
</ng-template>
|
||||
25
src/app/main/main.component.spec.ts
Normal file
25
src/app/main/main.component.spec.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { MainComponent } from './main.component';
|
||||
|
||||
describe('MainComponent', () => {
|
||||
let component: MainComponent;
|
||||
let fixture: ComponentFixture<MainComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ MainComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(MainComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
657
src/app/main/main.component.ts
Normal file
657
src/app/main/main.component.ts
Normal file
@@ -0,0 +1,657 @@
|
||||
import { Component, OnInit, ElementRef, ViewChild } from '@angular/core';
|
||||
import {PostsService} from '../posts.services';
|
||||
import {FileCardComponent} from '../file-card/file-card.component';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import {FormControl, Validators} from '@angular/forms';
|
||||
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
|
||||
import {MatSnackBar} from '@angular/material';
|
||||
import { saveAs } from 'file-saver';
|
||||
import 'rxjs/add/observable/of';
|
||||
import 'rxjs/add/operator/mapTo';
|
||||
import 'rxjs/add/operator/toPromise';
|
||||
import 'rxjs/add/observable/fromEvent'
|
||||
import 'rxjs/add/operator/filter'
|
||||
import 'rxjs/add/operator/debounceTime'
|
||||
import 'rxjs/add/operator/do'
|
||||
import 'rxjs/add/operator/switch'
|
||||
import { YoutubeSearchService, Result } from '../youtube-search.service';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
templateUrl: './main.component.html',
|
||||
styleUrls: ['./main.component.css']
|
||||
})
|
||||
export class MainComponent implements OnInit {
|
||||
iOS = false;
|
||||
|
||||
determinateProgress = false;
|
||||
downloadingfile = false;
|
||||
audioOnly: boolean;
|
||||
urlError = false;
|
||||
path = '';
|
||||
url = '';
|
||||
exists = '';
|
||||
percentDownloaded: number;
|
||||
|
||||
// settings
|
||||
fileManagerEnabled = false;
|
||||
allowQualitySelect = false;
|
||||
downloadOnlyMode = false;
|
||||
baseStreamPath;
|
||||
audioFolderPath;
|
||||
videoFolderPath;
|
||||
|
||||
cachedAvailableFormats = {};
|
||||
|
||||
// youtube api
|
||||
youtubeSearchEnabled = false;
|
||||
youtubeAPIKey = null;
|
||||
results_loading = false;
|
||||
results_showing = true;
|
||||
results = [];
|
||||
|
||||
mp3s: any[] = [];
|
||||
mp4s: any[] = [];
|
||||
files_cols = (window.innerWidth <= 450) ? 2 : 4;
|
||||
playlists = {'audio': [], 'video': []};
|
||||
playlist_thumbnails = {};
|
||||
downloading_content = {'audio': {}, 'video': {}};
|
||||
|
||||
urlForm = new FormControl('', [Validators.required]);
|
||||
|
||||
qualityOptions = {
|
||||
'video': [
|
||||
{
|
||||
'resolution': null,
|
||||
'value': '',
|
||||
'label': 'Max'
|
||||
},
|
||||
{
|
||||
'resolution': '3840x2160',
|
||||
'value': '2160',
|
||||
'label': '2160p (4K)'
|
||||
},
|
||||
{
|
||||
'resolution': '2560x1440',
|
||||
'value': '1440',
|
||||
'label': '1440p'
|
||||
},
|
||||
{
|
||||
'resolution': '1920x1080',
|
||||
'value': '1080',
|
||||
'label': '1080p'
|
||||
},
|
||||
{
|
||||
'resolution': '1280x720',
|
||||
'value': '720',
|
||||
'label': '720p'
|
||||
},
|
||||
{
|
||||
'resolution': '720x480',
|
||||
'value': '480',
|
||||
'label': '480p'
|
||||
},
|
||||
{
|
||||
'resolution': '480x360',
|
||||
'value': '360',
|
||||
'label': '360p'
|
||||
},
|
||||
{
|
||||
'resolution': '360x240',
|
||||
'value': '240',
|
||||
'label': '240p'
|
||||
},
|
||||
{
|
||||
'resolution': '256x144',
|
||||
'value': '144',
|
||||
'label': '144p'
|
||||
}
|
||||
],
|
||||
'audio': [
|
||||
{
|
||||
'kbitrate': null,
|
||||
'value': '',
|
||||
'label': 'Max'
|
||||
},
|
||||
{
|
||||
'kbitrate': '256',
|
||||
'value': '256K',
|
||||
'label': '256 Kbps'
|
||||
},
|
||||
{
|
||||
'kbitrate': '160',
|
||||
'value': '160K',
|
||||
'label': '160 Kbps'
|
||||
},
|
||||
{
|
||||
'kbitrate': '128',
|
||||
'value': '128K',
|
||||
'label': '128 Kbps'
|
||||
},
|
||||
{
|
||||
'kbitrate': '96',
|
||||
'value': '96K',
|
||||
'label': '96 Kbps'
|
||||
},
|
||||
{
|
||||
'kbitrate': '70',
|
||||
'value': '70K',
|
||||
'label': '70 Kbps'
|
||||
},
|
||||
{
|
||||
'kbitrate': '50',
|
||||
'value': '50K',
|
||||
'label': '50 Kbps'
|
||||
},
|
||||
{
|
||||
'kbitrate': '32',
|
||||
'value': '32K',
|
||||
'label': '32 Kbps'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
selectedQuality = '';
|
||||
formats_loading = false;
|
||||
|
||||
@ViewChild('urlinput', { read: ElementRef, static: false }) urlInput: ElementRef;
|
||||
last_valid_url = '';
|
||||
last_url_check = 0;
|
||||
|
||||
constructor(private postsService: PostsService, private youtubeSearch: YoutubeSearchService, public snackBar: MatSnackBar,
|
||||
private router: Router) {
|
||||
this.audioOnly = false;
|
||||
|
||||
|
||||
// loading config
|
||||
this.postsService.loadNavItems().subscribe(result => { // loads settings
|
||||
const backendUrl = result['YoutubeDLMaterial']['Host']['backendurl'];
|
||||
this.fileManagerEnabled = result['YoutubeDLMaterial']['Extra']['file_manager_enabled'];
|
||||
this.downloadOnlyMode = result['YoutubeDLMaterial']['Extra']['download_only_mode'];
|
||||
this.baseStreamPath = result['YoutubeDLMaterial']['Downloader']['path-base'];
|
||||
this.audioFolderPath = result['YoutubeDLMaterial']['Downloader']['path-audio'];
|
||||
this.videoFolderPath = result['YoutubeDLMaterial']['Downloader']['path-video'];
|
||||
this.youtubeSearchEnabled = result['YoutubeDLMaterial']['API'] && result['YoutubeDLMaterial']['API']['use_youtube_API'] &&
|
||||
result['YoutubeDLMaterial']['API']['youtube_API_key'];
|
||||
this.youtubeAPIKey = this.youtubeSearchEnabled ? result['YoutubeDLMaterial']['API']['youtube_API_key'] : null;
|
||||
this.allowQualitySelect = result['YoutubeDLMaterial']['Extra']['allow_quality_select'];
|
||||
|
||||
this.postsService.path = backendUrl;
|
||||
this.postsService.startPath = backendUrl;
|
||||
this.postsService.startPathSSL = backendUrl;
|
||||
|
||||
if (this.fileManagerEnabled) {
|
||||
this.getMp3s();
|
||||
this.getMp4s();
|
||||
}
|
||||
|
||||
if (this.youtubeSearchEnabled && this.youtubeAPIKey) {
|
||||
this.youtubeSearch.initializeAPI(this.youtubeAPIKey);
|
||||
this.attachToInput();
|
||||
}
|
||||
}, error => {
|
||||
console.log(error);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
// file manager stuff
|
||||
|
||||
getMp3s() {
|
||||
this.postsService.getMp3s().subscribe(result => {
|
||||
const mp3s = result['mp3s'];
|
||||
const playlists = result['playlists'];
|
||||
this.mp3s = mp3s;
|
||||
this.playlists.audio = playlists;
|
||||
|
||||
// get thumbnail url by using first video. this is a temporary hack
|
||||
for (let i = 0; i < this.playlists.audio.length; i++) {
|
||||
const playlist = this.playlists.audio[i];
|
||||
let videoToExtractThumbnail = null;
|
||||
for (let j = 0; j < this.mp3s.length; j++) {
|
||||
if (this.mp3s[j].id === playlist.fileNames[0]) {
|
||||
// found the corresponding file
|
||||
videoToExtractThumbnail = this.mp3s[j];
|
||||
}
|
||||
}
|
||||
|
||||
this.playlist_thumbnails[playlist.id] = videoToExtractThumbnail.thumbnailURL;
|
||||
}
|
||||
}, error => {
|
||||
console.log(error);
|
||||
});
|
||||
}
|
||||
|
||||
getMp4s() {
|
||||
this.postsService.getMp4s().subscribe(result => {
|
||||
const mp4s = result['mp4s'];
|
||||
const playlists = result['playlists'];
|
||||
this.mp4s = mp4s;
|
||||
this.playlists.video = playlists;
|
||||
|
||||
// get thumbnail url by using first video. this is a temporary hack
|
||||
for (let i = 0; i < this.playlists.video.length; i++) {
|
||||
const playlist = this.playlists.video[i];
|
||||
let videoToExtractThumbnail = null;
|
||||
for (let j = 0; j < this.mp4s.length; j++) {
|
||||
if (this.mp4s[j].id === playlist.fileNames[0]) {
|
||||
// found the corresponding file
|
||||
videoToExtractThumbnail = this.mp4s[j];
|
||||
}
|
||||
}
|
||||
|
||||
this.playlist_thumbnails[playlist.id] = videoToExtractThumbnail.thumbnailURL;
|
||||
}
|
||||
},
|
||||
error => {
|
||||
console.log(error);
|
||||
});
|
||||
}
|
||||
|
||||
public goToFile(name, isAudio) {
|
||||
if (isAudio) {
|
||||
this.downloadHelperMp3(name, false, false);
|
||||
} else {
|
||||
this.downloadHelperMp4(name, false, false);
|
||||
}
|
||||
}
|
||||
|
||||
public goToPlaylist(playlistID, type) {
|
||||
const playlist = this.getPlaylistObjectByID(playlistID, type);
|
||||
if (playlist) {
|
||||
if (this.downloadOnlyMode) {
|
||||
this.downloading_content[type][playlistID] = true;
|
||||
this.downloadPlaylist(playlist.fileNames, type, playlist.name, playlistID);
|
||||
} else {
|
||||
const fileNames = playlist.fileNames;
|
||||
this.router.navigate(['/player', {fileNames: fileNames.join('|nvr|'), type: type, id: playlistID}]);
|
||||
}
|
||||
} else {
|
||||
// playlist not found
|
||||
console.error(`Playlist with ID ${playlistID} not found!`);
|
||||
}
|
||||
}
|
||||
|
||||
getPlaylistObjectByID(playlistID, type) {
|
||||
for (let i = 0; i < this.playlists[type].length; i++) {
|
||||
const playlist = this.playlists[type][i];
|
||||
if (playlist.id === playlistID) {
|
||||
return playlist;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public removeFromMp3(name: string) {
|
||||
for (let i = 0; i < this.mp3s.length; i++) {
|
||||
if (this.mp3s[i].id === name) {
|
||||
this.mp3s.splice(i, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public removePlaylistMp3(playlistID, index) {
|
||||
this.postsService.removePlaylist(playlistID, 'audio').subscribe(res => {
|
||||
if (res['success']) {
|
||||
this.playlists.audio.splice(index, 1);
|
||||
this.openSnackBar('Playlist successfully removed.', '');
|
||||
}
|
||||
this.getMp3s();
|
||||
});
|
||||
}
|
||||
|
||||
public removeFromMp4(name: string) {
|
||||
for (let i = 0; i < this.mp4s.length; i++) {
|
||||
if (this.mp4s[i].id === name) {
|
||||
this.mp4s.splice(i, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public removePlaylistMp4(playlistID, index) {
|
||||
this.postsService.removePlaylist(playlistID, 'video').subscribe(res => {
|
||||
if (res['success']) {
|
||||
this.playlists.video.splice(index, 1);
|
||||
this.openSnackBar('Playlist successfully removed.', '');
|
||||
}
|
||||
this.getMp4s();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// app initialization.
|
||||
ngOnInit() {
|
||||
this.iOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window['MSStream'];
|
||||
|
||||
if (localStorage.getItem('audioOnly') !== null) {
|
||||
this.audioOnly = localStorage.getItem('audioOnly') === 'true';
|
||||
}
|
||||
}
|
||||
|
||||
// download helpers
|
||||
|
||||
downloadHelperMp3(name, is_playlist = false, forceView = false) {
|
||||
this.downloadingfile = false;
|
||||
|
||||
// if download only mode, just download the file. no redirect
|
||||
if (forceView === false && this.downloadOnlyMode && !this.iOS) {
|
||||
if (is_playlist) {
|
||||
const zipName = name[0].split(' ')[0] + name[1].split(' ')[0];
|
||||
this.downloadPlaylist(name, 'audio', zipName);
|
||||
} else {
|
||||
this.downloadAudioFile(decodeURI(name));
|
||||
}
|
||||
} else {
|
||||
if (is_playlist) {
|
||||
this.router.navigate(['/player', {fileNames: name.join('|nvr|'), type: 'audio'}]);
|
||||
// window.location.href = this.baseStreamPath + this.audioFolderPath + name[0] + '.mp3';
|
||||
} else {
|
||||
this.router.navigate(['/player', {fileNames: name, type: 'audio'}]);
|
||||
// window.location.href = this.baseStreamPath + this.audioFolderPath + name + '.mp3';
|
||||
}
|
||||
}
|
||||
|
||||
// reloads mp3s
|
||||
if (this.fileManagerEnabled) {
|
||||
this.getMp3s();
|
||||
}
|
||||
}
|
||||
|
||||
downloadHelperMp4(name, is_playlist = false, forceView = false) {
|
||||
this.downloadingfile = false;
|
||||
|
||||
// if download only mode, just download the file. no redirect
|
||||
if (forceView === false && this.downloadOnlyMode) {
|
||||
if (is_playlist) {
|
||||
const zipName = name[0].split(' ')[0] + name[1].split(' ')[0];
|
||||
this.downloadPlaylist(name, 'video', zipName);
|
||||
} else {
|
||||
this.downloadVideoFile(decodeURI(name));
|
||||
}
|
||||
} else {
|
||||
if (is_playlist) {
|
||||
this.router.navigate(['/player', {fileNames: name.join('|nvr|'), type: 'video'}]);
|
||||
// window.location.href = this.baseStreamPath + this.videoFolderPath + name[0] + '.mp4';
|
||||
} else {
|
||||
this.router.navigate(['/player', {fileNames: name, type: 'video'}]);
|
||||
// window.location.href = this.baseStreamPath + this.videoFolderPath + name + '.mp4';
|
||||
}
|
||||
}
|
||||
|
||||
// reloads mp4s
|
||||
if (this.fileManagerEnabled) {
|
||||
this.getMp4s();
|
||||
}
|
||||
}
|
||||
|
||||
// download click handler
|
||||
downloadClicked() {
|
||||
if (this.ValidURL(this.url)) {
|
||||
this.urlError = false;
|
||||
this.path = '';
|
||||
|
||||
if (this.audioOnly) {
|
||||
this.downloadingfile = true;
|
||||
|
||||
let customQualityConfiguration = null;
|
||||
if (this.selectedQuality !== '') {
|
||||
const cachedFormatsExists = this.cachedAvailableFormats[this.url];
|
||||
if (cachedFormatsExists) {
|
||||
const audio_formats = this.cachedAvailableFormats[this.url]['audio'];
|
||||
customQualityConfiguration = audio_formats[this.selectedQuality]['format_id'];
|
||||
}
|
||||
}
|
||||
this.postsService.makeMP3(this.url, (this.selectedQuality === '' ? null : this.selectedQuality),
|
||||
customQualityConfiguration).subscribe(posts => {
|
||||
const is_playlist = !!(posts['file_names']);
|
||||
this.path = is_playlist ? posts['file_names'] : posts['audiopathEncoded'];
|
||||
if (this.path !== '-1') {
|
||||
this.downloadHelperMp3(this.path, is_playlist);
|
||||
}
|
||||
}, error => { // can't access server
|
||||
this.downloadingfile = false;
|
||||
this.openSnackBar('Download failed!', 'OK.');
|
||||
});
|
||||
} else {
|
||||
let customQualityConfiguration = null;
|
||||
const cachedFormatsExists = this.cachedAvailableFormats[this.url];
|
||||
if (cachedFormatsExists) {
|
||||
const video_formats = this.cachedAvailableFormats[this.url]['video'];
|
||||
if (video_formats['best_audio_format'] && this.selectedQuality !== '') {
|
||||
customQualityConfiguration = video_formats[this.selectedQuality]['format_id'] + '+' + video_formats['best_audio_format'];
|
||||
}
|
||||
}
|
||||
|
||||
this.downloadingfile = true;
|
||||
this.postsService.makeMP4(this.url, (this.selectedQuality === '' ? null : this.selectedQuality),
|
||||
customQualityConfiguration).subscribe(posts => {
|
||||
const is_playlist = !!(posts['file_names']);
|
||||
this.path = is_playlist ? posts['file_names'] : posts['videopathEncoded'];
|
||||
if (this.path !== '-1') {
|
||||
this.downloadHelperMp4(this.path, is_playlist);
|
||||
}
|
||||
}, error => { // can't access server
|
||||
this.downloadingfile = false;
|
||||
this.openSnackBar('Download failed!', 'OK.');
|
||||
});
|
||||
}
|
||||
} else {
|
||||
this.urlError = true;
|
||||
}
|
||||
}
|
||||
|
||||
downloadAudioFile(name) {
|
||||
this.downloading_content['audio'][name] = true;
|
||||
this.postsService.downloadFileFromServer(name, 'audio').subscribe(res => {
|
||||
this.downloading_content['audio'][name] = false;
|
||||
const blob: Blob = res;
|
||||
saveAs(blob, name + '.mp3');
|
||||
|
||||
if (!this.fileManagerEnabled) {
|
||||
// tell server to delete the file once downloaded
|
||||
this.postsService.deleteFile(name, true).subscribe(delRes => {
|
||||
// reload mp3s
|
||||
this.getMp3s();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
downloadVideoFile(name) {
|
||||
this.downloading_content['video'][name] = true;
|
||||
this.postsService.downloadFileFromServer(name, 'video').subscribe(res => {
|
||||
this.downloading_content['video'][name] = false;
|
||||
const blob: Blob = res;
|
||||
saveAs(blob, name + '.mp4');
|
||||
|
||||
if (!this.fileManagerEnabled) {
|
||||
// tell server to delete the file once downloaded
|
||||
this.postsService.deleteFile(name, false).subscribe(delRes => {
|
||||
// reload mp4s
|
||||
this.getMp4s();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
downloadPlaylist(fileNames, type, zipName = null, playlistID = null) {
|
||||
this.postsService.downloadFileFromServer(fileNames, type, zipName).subscribe(res => {
|
||||
if (playlistID) { this.downloading_content[type][playlistID] = false };
|
||||
const blob: Blob = res;
|
||||
saveAs(blob, zipName + '.zip');
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
clearInput() {
|
||||
this.url = '';
|
||||
this.results_showing = false;
|
||||
}
|
||||
|
||||
onInputBlur() {
|
||||
this.results_showing = false;
|
||||
}
|
||||
|
||||
visitURL(url) {
|
||||
window.open(url);
|
||||
}
|
||||
|
||||
useURL(url) {
|
||||
this.results_showing = false;
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
inputChanged(new_val) {
|
||||
if (new_val === '' || !new_val) {
|
||||
this.results_showing = false;
|
||||
} else {
|
||||
if (this.ValidURL(new_val)) {
|
||||
this.results_showing = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// checks if url is a valid URL
|
||||
ValidURL(str) {
|
||||
// tslint:disable-next-line: max-line-length
|
||||
const strRegex = /((([A-Za-z]{3,9}:(?:\/\/)?)(?:[-;:&=\+\$,\w]+@)?[A-Za-z0-9.-]+|(?:www.|[-;:&=\+\$,\w]+@)[A-Za-z0-9.-]+)((?:\/[\+~%\/.\w-_]*)?\??(?:[-\+=&;%@.\w_]*)#?(?:[\w]*))?)/;
|
||||
const re = new RegExp(strRegex);
|
||||
const valid = re.test(str);
|
||||
|
||||
if (!valid) { return false; }
|
||||
|
||||
// tslint:disable-next-line: max-line-length
|
||||
const youtubeStrRegex = /(?:http(?:s)?:\/\/)?(?:www\.)?(?:youtu\.be\/|youtube\.com\/(?:(?:watch)?\?(?:.*&)?v(?:i)?=|(?:embed|v|vi|user)\/))([^\?&\"'<> #]+)/;
|
||||
const reYT = new RegExp(youtubeStrRegex);
|
||||
const ytValid = reYT.test(str);
|
||||
if (valid && ytValid && Date.now() - this.last_url_check > 1000) {
|
||||
if (str !== this.last_valid_url && this.allowQualitySelect) {
|
||||
// get info
|
||||
this.getURLInfo(str);
|
||||
}
|
||||
this.last_valid_url = str;
|
||||
}
|
||||
return valid;
|
||||
}
|
||||
|
||||
// snackbar helper
|
||||
public openSnackBar(message: string, action: string) {
|
||||
this.snackBar.open(message, action, {
|
||||
duration: 2000,
|
||||
});
|
||||
}
|
||||
|
||||
getURLInfo(url) {
|
||||
if (!(this.cachedAvailableFormats[url])) {
|
||||
this.formats_loading = true;
|
||||
this.postsService.getFileInfo([url], 'irrelevant', true).subscribe(res => {
|
||||
if (url === this.url) { this.formats_loading = false; }
|
||||
const infos = res['result'];
|
||||
const parsed_infos = this.getAudioAndVideoFormats(infos.formats);
|
||||
const available_formats = {audio: parsed_infos[0], video: parsed_infos[1]};
|
||||
this.cachedAvailableFormats[url] = available_formats;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
attachToInput() {
|
||||
Observable.fromEvent(this.urlInput.nativeElement, 'keyup')
|
||||
.map((e: any) => e.target.value) // extract the value of input
|
||||
.filter((text: string) => text.length > 1) // filter out if empty
|
||||
.debounceTime(250) // only once every 250ms
|
||||
.do(() => this.results_loading = true) // enable loading
|
||||
.map((query: string) => this.youtubeSearch.search(query))
|
||||
.switch() // act on the return of the search
|
||||
.subscribe(
|
||||
(results: Result[]) => {
|
||||
this.results_loading = false;
|
||||
if (this.url !== '' && results && results.length > 0) {
|
||||
this.results = results;
|
||||
this.results_showing = true;
|
||||
} else {
|
||||
this.results_showing = false;
|
||||
}
|
||||
},
|
||||
(err: any) => {
|
||||
console.log(err)
|
||||
this.results_loading = false;
|
||||
this.results_showing = false;
|
||||
},
|
||||
() => { // on completion
|
||||
this.results_loading = false;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
onResize(event) {
|
||||
this.files_cols = (event.target.innerWidth <= 450) ? 2 : 4;
|
||||
}
|
||||
|
||||
videoModeChanged(new_val) {
|
||||
this.selectedQuality = '';
|
||||
localStorage.setItem('audioOnly', new_val.checked.toString());
|
||||
}
|
||||
|
||||
getAudioAndVideoFormats(formats): any[] {
|
||||
const audio_formats = {};
|
||||
const video_formats = {};
|
||||
|
||||
for (let i = 0; i < formats.length; i++) {
|
||||
const format_obj = {type: null};
|
||||
|
||||
const format = formats[i];
|
||||
const format_type = (format.vcodec === 'none') ? 'audio' : 'video';
|
||||
|
||||
format_obj.type = format_type;
|
||||
if (format_obj.type === 'audio' && format.abr) {
|
||||
const key = format.abr.toString() + 'K';
|
||||
format_obj['bitrate'] = format.abr;
|
||||
format_obj['format_id'] = format.format_id;
|
||||
format_obj['ext'] = format.ext;
|
||||
// don't overwrite if not m4a
|
||||
if (audio_formats[key]) {
|
||||
if (format.ext === 'm4a') {
|
||||
audio_formats[key] = format_obj;
|
||||
}
|
||||
} else {
|
||||
audio_formats[key] = format_obj;
|
||||
}
|
||||
} else if (format_obj.type === 'video') {
|
||||
// check if video format is mp4
|
||||
const key = format.height.toString();
|
||||
if (format.ext === 'mp4') {
|
||||
format_obj['height'] = format.height;
|
||||
format_obj['acodec'] = format.acodec;
|
||||
format_obj['format_id'] = format.format_id;
|
||||
|
||||
// no acodec means no overwrite
|
||||
if (!(video_formats[key]) || format_obj['acodec'] !== 'none') {
|
||||
video_formats[key] = format_obj;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
video_formats['best_audio_format'] = this.getBestAudioFormatForMp4(audio_formats);
|
||||
|
||||
return [audio_formats, video_formats]
|
||||
}
|
||||
|
||||
getBestAudioFormatForMp4(audio_formats) {
|
||||
let best_audio_format_for_mp4 = null;
|
||||
let best_audio_format_bitrate = 0;
|
||||
const available_audio_format_keys = Object.keys(audio_formats);
|
||||
for (let i = 0; i < available_audio_format_keys.length; i++) {
|
||||
const audio_format_key = available_audio_format_keys[i];
|
||||
const audio_format = audio_formats[audio_format_key];
|
||||
const is_m4a = audio_format.ext === 'm4a';
|
||||
if (is_m4a && audio_format.bitrate > best_audio_format_bitrate) {
|
||||
best_audio_format_for_mp4 = audio_format.format_id;
|
||||
best_audio_format_bitrate = audio_format.bitrate;
|
||||
}
|
||||
}
|
||||
return best_audio_format_for_mp4;
|
||||
}
|
||||
}
|
||||
|
||||
61
src/app/player/player.component.css
Normal file
61
src/app/player/player.component.css
Normal file
@@ -0,0 +1,61 @@
|
||||
.video-player {
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.audio-styles {
|
||||
height: 50px;
|
||||
background-color: transparent;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.video-styles {
|
||||
width: 80%;
|
||||
}
|
||||
|
||||
::ng-deep .mat-button-toggle-label-content {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.container-video {
|
||||
max-width: 100%;
|
||||
padding-left: 0px;
|
||||
padding-right: 0px;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
position: absolute;
|
||||
left: 0px;
|
||||
bottom: -1px;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
bottom: 3px;
|
||||
left: 3px;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.save-button {
|
||||
right: 25px;
|
||||
position: absolute;
|
||||
bottom: 25px;
|
||||
}
|
||||
|
||||
.favorite-button {
|
||||
left: 25px;
|
||||
position: absolute;
|
||||
bottom: 25px;
|
||||
}
|
||||
|
||||
.video-col {
|
||||
padding-right: 0px;
|
||||
padding-left: 0.01px;
|
||||
}
|
||||
|
||||
.save-icon {
|
||||
bottom: 1px;
|
||||
position: relative;
|
||||
}
|
||||
25
src/app/player/player.component.html
Normal file
25
src/app/player/player.component.html
Normal file
@@ -0,0 +1,25 @@
|
||||
<div *ngIf="playlist.length > 0">
|
||||
<div [ngClass]="(type === 'audio') ? null : 'container-video'" class="container">
|
||||
<div style="max-width: 100%; margin-left: 0px;" class="row">
|
||||
<div [ngClass]="(type === 'audio') ? 'my-2 px-1' : 'video-col'" class="col">
|
||||
<vg-player (onPlayerReady)="onPlayerReady($event)" [style.background-color]="(type === 'audio') ? 'transparent' : 'black'">
|
||||
<video [ngClass]="(type === 'audio') ? 'audio-styles' : 'video-styles'" #media class="video-player" [vgMedia]="media" [src]="currentItem.src" id="singleVideo" preload="auto" controls>
|
||||
</video>
|
||||
</vg-player>
|
||||
</div>
|
||||
<div class="col-12 my-2">
|
||||
<mat-button-toggle-group style="width: 80%; left: 9%" vertical name="videoSelect" aria-label="Video Select" #group="matButtonToggleGroup">
|
||||
<mat-button-toggle *ngFor="let name of fileNames; let i = index" [checked]="currentItem.title === name" (click)="onClickPlaylistItem(playlist[i], i)" class="toggle-button" [value]="name">{{decodeURI(name)}}</mat-button-toggle>
|
||||
</mat-button-toggle-group>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div *ngIf="playlist.length > 1">
|
||||
<button class="save-button" color="primary" (click)="downloadContent()" [disabled]="downloading" mat-fab><mat-icon class="save-icon">save</mat-icon><mat-spinner *ngIf="downloading" class="spinner" [diameter]="50"></mat-spinner></button>
|
||||
<button *ngIf="!id" color="accent" class="favorite-button" color="primary" (click)="namePlaylistDialog()" mat-fab><mat-icon class="save-icon">favorite</mat-icon></button>
|
||||
</div>
|
||||
<div *ngIf="playlist.length === 1">
|
||||
<button class="save-button" color="primary" (click)="downloadFile()" [disabled]="downloading" mat-fab><mat-icon class="save-icon">save</mat-icon><mat-spinner *ngIf="downloading" class="spinner" [diameter]="50"></mat-spinner></button>
|
||||
</div>
|
||||
</div>
|
||||
25
src/app/player/player.component.spec.ts
Normal file
25
src/app/player/player.component.spec.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { PlayerComponent } from './player.component';
|
||||
|
||||
describe('PlayerComponent', () => {
|
||||
let component: PlayerComponent;
|
||||
let fixture: ComponentFixture<PlayerComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ PlayerComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(PlayerComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
218
src/app/player/player.component.ts
Normal file
218
src/app/player/player.component.ts
Normal file
@@ -0,0 +1,218 @@
|
||||
import { Component, OnInit, HostListener, EventEmitter } from '@angular/core';
|
||||
import { VgAPI } from 'videogular2/compiled/core';
|
||||
import { PostsService } from 'app/posts.services';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { MatDialog, MatSnackBar } from '@angular/material';
|
||||
import { InputDialogComponent } from 'app/input-dialog/input-dialog.component';
|
||||
|
||||
export interface IMedia {
|
||||
title: string;
|
||||
src: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-player',
|
||||
templateUrl: './player.component.html',
|
||||
styleUrls: ['./player.component.css']
|
||||
})
|
||||
export class PlayerComponent implements OnInit {
|
||||
|
||||
playlist: Array<IMedia> = [];
|
||||
|
||||
currentIndex = 0;
|
||||
currentItem: IMedia = null;
|
||||
api: VgAPI;
|
||||
|
||||
// params
|
||||
fileNames: string[];
|
||||
type: string;
|
||||
|
||||
baseStreamPath = null;
|
||||
audioFolderPath = null;
|
||||
videoFolderPath = null;
|
||||
innerWidth: number;
|
||||
|
||||
downloading = false;
|
||||
|
||||
id = null;
|
||||
|
||||
@HostListener('window:resize', ['$event'])
|
||||
onResize(event) {
|
||||
this.innerWidth = window.innerWidth;
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.innerWidth = window.innerWidth;
|
||||
|
||||
this.fileNames = this.route.snapshot.paramMap.get('fileNames').split('|nvr|');
|
||||
this.type = this.route.snapshot.paramMap.get('type');
|
||||
this.id = this.route.snapshot.paramMap.get('id');
|
||||
|
||||
// loading config
|
||||
this.postsService.loadNavItems().subscribe(result => { // loads settings
|
||||
this.baseStreamPath = result['YoutubeDLMaterial']['Downloader']['path-base'];
|
||||
this.audioFolderPath = result['YoutubeDLMaterial']['Downloader']['path-audio'];
|
||||
this.videoFolderPath = result['YoutubeDLMaterial']['Downloader']['path-video'];
|
||||
const backendUrl = result['YoutubeDLMaterial']['Host']['backendurl'];
|
||||
|
||||
this.postsService.path = backendUrl;
|
||||
this.postsService.startPath = backendUrl;
|
||||
this.postsService.startPathSSL = backendUrl;
|
||||
|
||||
let fileType = null;
|
||||
if (this.type === 'audio') {
|
||||
fileType = 'audio/mp3';
|
||||
} else if (this.type === 'video') {
|
||||
fileType = 'video/mp4';
|
||||
} else {
|
||||
// error
|
||||
console.error('Must have valid file type! Use \'audio\' or \video\'');
|
||||
}
|
||||
|
||||
for (let i = 0; i < this.fileNames.length; i++) {
|
||||
const fileName = this.fileNames[i];
|
||||
const baseLocation = (this.type === 'audio') ? this.audioFolderPath : this.videoFolderPath;
|
||||
const fullLocation = this.baseStreamPath + baseLocation + fileName; // + (this.type === 'audio' ? '.mp3' : '.mp4');
|
||||
const mediaObject: IMedia = {
|
||||
title: fileName,
|
||||
src: fullLocation,
|
||||
type: fileType
|
||||
}
|
||||
this.playlist.push(mediaObject);
|
||||
}
|
||||
this.currentItem = this.playlist[this.currentIndex];
|
||||
});
|
||||
|
||||
// this.getFileInfos();
|
||||
|
||||
}
|
||||
|
||||
constructor(private postsService: PostsService, private route: ActivatedRoute, private dialog: MatDialog, private router: Router,
|
||||
public snackBar: MatSnackBar) {
|
||||
|
||||
}
|
||||
|
||||
onPlayerReady(api: VgAPI) {
|
||||
this.api = api;
|
||||
|
||||
this.api.getDefaultMedia().subscriptions.loadedMetadata.subscribe(this.playVideo.bind(this));
|
||||
this.api.getDefaultMedia().subscriptions.ended.subscribe(this.nextVideo.bind(this));
|
||||
}
|
||||
|
||||
nextVideo() {
|
||||
if (this.currentIndex === this.playlist.length - 1) {
|
||||
// dont continue playing
|
||||
// this.currentIndex = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
this.currentIndex++;
|
||||
this.currentItem = this.playlist[ this.currentIndex ];
|
||||
}
|
||||
|
||||
playVideo() {
|
||||
this.api.play();
|
||||
}
|
||||
|
||||
onClickPlaylistItem(item: IMedia, index: number) {
|
||||
// console.log('new current item is ' + item.title + ' at index ' + index);
|
||||
this.currentIndex = index;
|
||||
this.currentItem = item;
|
||||
}
|
||||
|
||||
getFileInfos() {
|
||||
this.postsService.getFileInfo(this.fileNames, this.type, false).subscribe(res => {
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
decodeURI(string) {
|
||||
return decodeURI(string);
|
||||
}
|
||||
|
||||
downloadContent() {
|
||||
const fileNames = [];
|
||||
for (let i = 0; i < this.playlist.length; i++) {
|
||||
fileNames.push(this.playlist[i].title);
|
||||
}
|
||||
|
||||
const zipName = fileNames[0].split(' ')[0] + fileNames[1].split(' ')[0];
|
||||
this.downloading = true;
|
||||
this.postsService.downloadFileFromServer(fileNames, this.type, zipName).subscribe(res => {
|
||||
this.downloading = false;
|
||||
const blob: Blob = res;
|
||||
saveAs(blob, zipName + '.zip');
|
||||
}, err => {
|
||||
console.log(err);
|
||||
this.downloading = false;
|
||||
});
|
||||
}
|
||||
|
||||
downloadFile() {
|
||||
const ext = (this.type === 'audio') ? '.mp3' : '.mp4';
|
||||
const filename = this.playlist[0].title;
|
||||
this.downloading = true;
|
||||
this.postsService.downloadFileFromServer(filename, this.type).subscribe(res => {
|
||||
this.downloading = false;
|
||||
const blob: Blob = res;
|
||||
saveAs(blob, filename + ext);
|
||||
}, err => {
|
||||
console.log(err);
|
||||
this.downloading = false;
|
||||
});
|
||||
}
|
||||
|
||||
namePlaylistDialog() {
|
||||
const done = new EventEmitter<any>();
|
||||
const dialogRef = this.dialog.open(InputDialogComponent, {
|
||||
width: '300px',
|
||||
data: {
|
||||
inputTitle: 'Name the playlist',
|
||||
inputPlaceholder: 'Name',
|
||||
submitText: 'Favorite',
|
||||
doneEmitter: done
|
||||
}
|
||||
});
|
||||
|
||||
done.subscribe(name => {
|
||||
|
||||
// Eventually do additional checks on name
|
||||
if (name) {
|
||||
this.postsService.createPlaylist(name, this.fileNames, this.type, null).subscribe(res => {
|
||||
if (res['success']) {
|
||||
dialogRef.close();
|
||||
const new_playlist = res['new_playlist'];
|
||||
this.openSnackBar('Playlist \'' + name + '\' successfully created!', '')
|
||||
this.playlistPostCreationHandler(new_playlist.id);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/*
|
||||
createPlaylist(name) {
|
||||
this.postsService.createPlaylist(name, this.fileNames, this.type, null).subscribe(res => {
|
||||
if (res['success']) {
|
||||
console.log('Success!');
|
||||
}
|
||||
});
|
||||
}
|
||||
*/
|
||||
|
||||
playlistPostCreationHandler(playlistID) {
|
||||
// changes the route without moving from the current view or
|
||||
// triggering a navigation event
|
||||
this.id = playlistID;
|
||||
this.router.navigateByUrl(this.router.url + ';id=' + playlistID);
|
||||
}
|
||||
|
||||
// snackbar helper
|
||||
public openSnackBar(message: string, action: string) {
|
||||
this.snackBar.open(message, action, {
|
||||
duration: 2000,
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,11 +1,12 @@
|
||||
import {Injectable, isDevMode} from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { HttpClient, HttpHeaders, HttpRequest, HttpResponseBase } from '@angular/common/http';
|
||||
import config from '../assets/default.json';
|
||||
import 'rxjs/add/operator/map';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import 'rxjs/add/operator/map';
|
||||
import 'rxjs/add/operator/catch';
|
||||
import 'rxjs/add/observable/throw';
|
||||
import { THEMES_CONFIG } from '../themes';
|
||||
|
||||
@Injectable()
|
||||
export class PostsService {
|
||||
@@ -15,11 +16,17 @@ export class PostsService {
|
||||
startPath = 'http://localhost:17442/';
|
||||
startPathSSL = 'https://localhost:17442/'
|
||||
handShakeComplete = false;
|
||||
THEMES_CONFIG = THEMES_CONFIG;
|
||||
theme;
|
||||
|
||||
constructor(private http: HttpClient) {
|
||||
console.log('PostsService Initialized...');
|
||||
}
|
||||
|
||||
setTheme(theme) {
|
||||
this.theme = this.THEMES_CONFIG[theme];
|
||||
}
|
||||
|
||||
startHandshake(url: string) {
|
||||
return this.http.get(url + 'geturl');
|
||||
}
|
||||
@@ -36,12 +43,16 @@ export class PostsService {
|
||||
return this.http.get(this.startPath + 'audiofolder');
|
||||
}
|
||||
|
||||
makeMP3(url: string) {
|
||||
return this.http.post(this.path + 'tomp3', {url: url});
|
||||
makeMP3(url: string, selectedQuality: string, customQualityConfiguration: string) {
|
||||
return this.http.post(this.path + 'tomp3', {url: url,
|
||||
maxBitrate: selectedQuality,
|
||||
customQualityConfiguration: customQualityConfiguration});
|
||||
}
|
||||
|
||||
makeMP4(url: string) {
|
||||
return this.http.post(this.path + 'tomp4', {url: url});
|
||||
makeMP4(url: string, selectedQuality: string, customQualityConfiguration: string) {
|
||||
return this.http.post(this.path + 'tomp4', {url: url,
|
||||
selectedHeight: selectedQuality,
|
||||
customQualityConfiguration: customQualityConfiguration});
|
||||
}
|
||||
|
||||
getFileStatusMp3(name: string) {
|
||||
@@ -56,8 +67,9 @@ export class PostsService {
|
||||
if (isDevMode()) {
|
||||
return this.http.get('./assets/default.json');
|
||||
}
|
||||
console.log('Config location: ' + window.location.href + 'backend/config/default.json');
|
||||
return this.http.get(window.location.href + 'backend/config/default.json');
|
||||
const locations = window.location.href.split('#');
|
||||
const current_location = locations[0];
|
||||
return this.http.get(current_location + 'backend/config/default.json');
|
||||
}
|
||||
|
||||
deleteFile(name: string, isAudio: boolean) {
|
||||
@@ -75,6 +87,29 @@ export class PostsService {
|
||||
getMp4s() {
|
||||
return this.http.post(this.path + 'getMp4s', {});
|
||||
}
|
||||
|
||||
downloadFileFromServer(fileName, type, outputName = null) {
|
||||
return this.http.post(this.path + 'downloadFile', {fileNames: fileName,
|
||||
type: type,
|
||||
is_playlist: Array.isArray(fileName),
|
||||
outputName: outputName},
|
||||
{responseType: 'blob'});
|
||||
}
|
||||
|
||||
getFileInfo(fileNames, type, urlMode) {
|
||||
return this.http.post(this.path + 'getVideoInfos', {fileNames: fileNames, type: type, urlMode: urlMode});
|
||||
}
|
||||
|
||||
createPlaylist(playlistName, fileNames, type, thumbnailURL) {
|
||||
return this.http.post(this.path + 'createPlaylist', {playlistName: playlistName,
|
||||
fileNames: fileNames,
|
||||
type: type,
|
||||
thumbnailURL: thumbnailURL});
|
||||
}
|
||||
|
||||
removePlaylist(playlistID, type) {
|
||||
return this.http.post(this.path + 'deletePlaylist', {playlistID: playlistID, type: type});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
12
src/app/youtube-search.service.spec.ts
Normal file
12
src/app/youtube-search.service.spec.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
|
||||
import { YoutubeSearchService } from './youtube-search.service';
|
||||
|
||||
describe('YoutubeSearchService', () => {
|
||||
beforeEach(() => TestBed.configureTestingModule({}));
|
||||
|
||||
it('should be created', () => {
|
||||
const service: YoutubeSearchService = TestBed.get(YoutubeSearchService);
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
});
|
||||
101
src/app/youtube-search.service.ts
Normal file
101
src/app/youtube-search.service.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
export class Result {
|
||||
id: string
|
||||
title: string
|
||||
desc: string
|
||||
thumbnailUrl: string
|
||||
videoUrl: string
|
||||
uploaded: any;
|
||||
|
||||
constructor(obj?: any) {
|
||||
this.id = obj && obj.id || null
|
||||
this.title = obj && obj.title || null
|
||||
this.desc = obj && obj.desc || null
|
||||
this.thumbnailUrl = obj && obj.thumbnailUrl || null
|
||||
this.uploaded = obj && obj.uploaded || null
|
||||
this.videoUrl = obj && obj.videoUrl || `https://www.youtube.com/watch?v=${this.id}`
|
||||
|
||||
this.uploaded = formatDate(Date.parse(this.uploaded));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class YoutubeSearchService {
|
||||
|
||||
url = 'https://www.googleapis.com/youtube/v3/search';
|
||||
key = null;
|
||||
|
||||
constructor(private http: HttpClient) { }
|
||||
|
||||
initializeAPI(key) {
|
||||
this.key = key;
|
||||
}
|
||||
|
||||
search(query: string): Observable<Result[]> {
|
||||
if (this.ValidURL(query)) {
|
||||
return new Observable<Result[]>();
|
||||
}
|
||||
const params: string = [
|
||||
`q=${query}`,
|
||||
`key=${this.key}`,
|
||||
`part=snippet`,
|
||||
`type=video`,
|
||||
`maxResults=5`
|
||||
].join('&')
|
||||
const queryUrl = `${this.url}?${params}`
|
||||
return this.http.get(queryUrl).map(response => {
|
||||
return <any>response['items'].map(item => {
|
||||
return new Result({
|
||||
id: item.id.videoId,
|
||||
title: item.snippet.title,
|
||||
desc: item.snippet.description,
|
||||
thumbnailUrl: item.snippet.thumbnails.high.url,
|
||||
uploaded: item.snippet.publishedAt
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// checks if url is a valid URL
|
||||
ValidURL(str) {
|
||||
// tslint:disable-next-line: max-line-length
|
||||
const strRegex = /((([A-Za-z]{3,9}:(?:\/\/)?)(?:[-;:&=\+\$,\w]+@)?[A-Za-z0-9.-]+|(?:www.|[-;:&=\+\$,\w]+@)[A-Za-z0-9.-]+)((?:\/[\+~%\/.\w-_]*)?\??(?:[-\+=&;%@.\w_]*)#?(?:[\w]*))?)/;
|
||||
const re = new RegExp(strRegex);
|
||||
return re.test(str);
|
||||
}
|
||||
}
|
||||
|
||||
function formatDate(dateVal) {
|
||||
const newDate = new Date(dateVal);
|
||||
|
||||
const sMonth = padValue(newDate.getMonth() + 1);
|
||||
const sDay = padValue(newDate.getDate());
|
||||
const sYear = newDate.getFullYear();
|
||||
let sHour: any;
|
||||
sHour = newDate.getHours();
|
||||
const sMinute = padValue(newDate.getMinutes());
|
||||
let sAMPM = 'AM';
|
||||
|
||||
const iHourCheck = parseInt(sHour, 10);
|
||||
|
||||
if (iHourCheck > 12) {
|
||||
sAMPM = 'PM';
|
||||
sHour = iHourCheck - 12;
|
||||
} else if (iHourCheck === 0) {
|
||||
sHour = '12';
|
||||
}
|
||||
|
||||
sHour = padValue(sHour);
|
||||
|
||||
return sMonth + '-' + sDay + '-' + sYear + ' ' + sHour + ':' + sMinute + ' ' + sAMPM;
|
||||
}
|
||||
|
||||
function padValue(value) {
|
||||
return (value < 10) ? '0' + value : value;
|
||||
}
|
||||
@@ -16,7 +16,17 @@
|
||||
},
|
||||
"Extra": {
|
||||
"title_top": "Youtube Downloader",
|
||||
"file_manager_enabled": true
|
||||
"file_manager_enabled": true,
|
||||
"allow_quality_select": true,
|
||||
"download_only_mode": false
|
||||
},
|
||||
"API": {
|
||||
"use_youtube_API": false,
|
||||
"youtube_API_key": ""
|
||||
},
|
||||
"Themes": {
|
||||
"default_theme": "default",
|
||||
"allow_theme_change": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,10 +7,9 @@
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
||||
<link href="https://fonts.googleapis.com/css?family=Roboto" rel="stylesheet">
|
||||
<link href="https://fonts.googleapis.com/css?family=Roboto&display=swap" rel="stylesheet">
|
||||
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
||||
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css">
|
||||
<link href="https://unpkg.com/@angular/material/prebuilt-themes/indigo-pink.css" rel="stylesheet"> <script src="systemjs.config.js"></script>
|
||||
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
|
||||
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -42,11 +42,11 @@
|
||||
|
||||
|
||||
/** Evergreen browsers require these. **/
|
||||
import 'core-js/es6/reflect';
|
||||
// import 'core-js/es6/reflect';
|
||||
|
||||
|
||||
/** ALL Firefox browsers require the following to support `@angular/animation`. **/
|
||||
// import 'web-animations-js'; // Run `npm install --save web-animations-js`.
|
||||
import 'web-animations-js'; // Run `npm install --save web-animations-js`.
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
/* You can add global styles to this file, and also import other style files */
|
||||
70
src/styles.scss
Normal file
70
src/styles.scss
Normal file
@@ -0,0 +1,70 @@
|
||||
/* You can add global styles to this file, and also import other style files */
|
||||
|
||||
@import '@angular/material/prebuilt-themes/indigo-pink.css';
|
||||
|
||||
//@import './app-theme';
|
||||
/* You can add global styles to this file, and also import other style files */
|
||||
// @import "../node_modules/@angular/material/prebuilt-themes/purple-green.css";
|
||||
@import "palette.scss";
|
||||
|
||||
html, body { height: 100%; }
|
||||
body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; }
|
||||
|
||||
@import '~@angular/material/theming';
|
||||
// Plus imports for other components in your app.
|
||||
|
||||
/*// Typography
|
||||
$custom-typography: mat-typography-config(
|
||||
$font-family: Raleway,
|
||||
$headline: mat-typography-level(24px, 48px, 400),
|
||||
$body-1: mat-typography-level(16px, 24px, 400)
|
||||
);
|
||||
@include angular-material-typography($custom-typography);
|
||||
*/
|
||||
// Default colors
|
||||
$my-app-primary: mat-palette($mat-light-blue, 700, 100, 800);
|
||||
$my-app-accent: mat-palette($mat-blue, 700, 100, 800);
|
||||
$my-app-warn: mat-palette($mat-red, 700, 100, 800);
|
||||
|
||||
$my-app-theme: mat-light-theme($my-app-primary, $my-app-accent, $my-app-warn);
|
||||
@include angular-material-theme($my-app-theme);
|
||||
|
||||
// Dark theme
|
||||
$dark-primary: mat-palette($mat-indigo);
|
||||
$dark-accent: mat-palette($mat-blue);
|
||||
$dark-warn: mat-palette($mat-deep-orange);
|
||||
|
||||
$dark-theme: mat-dark-theme($dark-primary, $dark-accent, $dark-warn);
|
||||
|
||||
.dark-theme {
|
||||
@include angular-material-theme($dark-theme);
|
||||
}
|
||||
|
||||
// Light theme
|
||||
$light-primary: mat-palette($mat-grey, 200, 500, 300);
|
||||
$light-accent: mat-palette($mat-brown, 200);
|
||||
$light-warn: mat-palette($mat-deep-orange, 200);
|
||||
|
||||
$light-theme: mat-light-theme($light-primary, $light-accent, $light-warn);
|
||||
|
||||
.light-theme {
|
||||
@include angular-material-theme($light-theme)
|
||||
}
|
||||
|
||||
.no-outline {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
|
||||
// Include the common styles for Angular Material. We include this here so that you only
|
||||
// have to load a single css file for Angular Material in your app.
|
||||
// Be sure that you only ever include this mixin once!
|
||||
@include mat-core();
|
||||
|
||||
// @import '../node_modules/@angular/material/theming';
|
||||
|
||||
.centered {
|
||||
margin: 0 auto;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
}
|
||||
22
src/themes.ts
Normal file
22
src/themes.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
const THEMES_CONFIG = {
|
||||
'default': {
|
||||
'key': 'default',
|
||||
'background_color': 'ghostwhite',
|
||||
'css_label': 'default-theme',
|
||||
'social_theme': 'material-light'
|
||||
},
|
||||
'dark': {
|
||||
'key': 'dark',
|
||||
'background_color': '#757575',
|
||||
'css_label': 'dark-theme',
|
||||
'social_theme': 'material-dark'
|
||||
},
|
||||
'light': {
|
||||
'key': 'light',
|
||||
'background_color': 'white',
|
||||
'css_label': 'light-theme',
|
||||
'social_theme': 'material-light'
|
||||
}
|
||||
};
|
||||
|
||||
export {THEMES_CONFIG};
|
||||
Reference in New Issue
Block a user