mirror of
https://github.com/Tzahi12345/YoutubeDL-Material.git
synced 2026-03-19 11:10:57 +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 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -46,3 +46,4 @@ backend/node_modules/*
|
|||||||
YoutubeDL-Material/node_modules/*
|
YoutubeDL-Material/node_modules/*
|
||||||
backend/video/*
|
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"
|
"src/backend"
|
||||||
],
|
],
|
||||||
"styles": [
|
"styles": [
|
||||||
"src/styles.css"
|
"src/styles.scss"
|
||||||
],
|
],
|
||||||
"scripts": []
|
"scripts": []
|
||||||
},
|
},
|
||||||
@@ -65,6 +65,57 @@
|
|||||||
"browserTarget": "youtube-dl-material:build"
|
"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": {
|
"test": {
|
||||||
"builder": "@angular-devkit/build-angular:karma",
|
"builder": "@angular-devkit/build-angular:karma",
|
||||||
"options": {
|
"options": {
|
||||||
@@ -74,7 +125,7 @@
|
|||||||
"tsConfig": "src/tsconfig.spec.json",
|
"tsConfig": "src/tsconfig.spec.json",
|
||||||
"scripts": [],
|
"scripts": [],
|
||||||
"styles": [
|
"styles": [
|
||||||
"src/styles.css"
|
"src/styles.scss"
|
||||||
],
|
],
|
||||||
"assets": [
|
"assets": [
|
||||||
"src/assets",
|
"src/assets",
|
||||||
@@ -125,7 +176,7 @@
|
|||||||
"schematics": {
|
"schematics": {
|
||||||
"@schematics/angular:component": {
|
"@schematics/angular:component": {
|
||||||
"prefix": "app",
|
"prefix": "app",
|
||||||
"styleext": "css"
|
"styleext": "scss"
|
||||||
},
|
},
|
||||||
"@schematics/angular:directive": {
|
"@schematics/angular:directive": {
|
||||||
"prefix": "app"
|
"prefix": "app"
|
||||||
|
|||||||
527
backend/app.js
527
backend/app.js
@@ -6,17 +6,40 @@ var config = require('config');
|
|||||||
var https = require('https');
|
var https = require('https');
|
||||||
var express = require("express");
|
var express = require("express");
|
||||||
var bodyParser = require("body-parser");
|
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 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 backendUrl = config.get("YoutubeDLMaterial.Host.backendurl")
|
||||||
var backendPort = 17442;
|
var backendPort = 17442;
|
||||||
var usingEncryption = config.get("YoutubeDLMaterial.Encryption.use-encryption");
|
var usingEncryption = config.get("YoutubeDLMaterial.Encryption.use-encryption");
|
||||||
var basePath = config.get("YoutubeDLMaterial.Downloader.path-base");
|
var basePath = config.get("YoutubeDLMaterial.Downloader.path-base");
|
||||||
var audioPath = config.get("YoutubeDLMaterial.Downloader.path-audio");
|
var audioFolderPath = config.get("YoutubeDLMaterial.Downloader.path-audio");
|
||||||
var videoPath = config.get("YoutubeDLMaterial.Downloader.path-video");
|
var videoFolderPath = config.get("YoutubeDLMaterial.Downloader.path-video");
|
||||||
|
var downloadOnlyMode = config.get("YoutubeDLMaterial.Extra.download_only_mode")
|
||||||
|
|
||||||
|
var descriptors = {};
|
||||||
|
|
||||||
|
|
||||||
if (usingEncryption)
|
if (usingEncryption)
|
||||||
{
|
{
|
||||||
@@ -79,7 +102,7 @@ function getThumbnailMp4(name)
|
|||||||
|
|
||||||
function getFileSizeMp3(name)
|
function getFileSizeMp3(name)
|
||||||
{
|
{
|
||||||
var jsonPath = audioPath+name+".mp3.info.json";
|
var jsonPath = audioFolderPath+name+".mp3.info.json";
|
||||||
|
|
||||||
if (fs.existsSync(jsonPath))
|
if (fs.existsSync(jsonPath))
|
||||||
var obj = JSON.parse(fs.readFileSync(jsonPath, 'utf8'));
|
var obj = JSON.parse(fs.readFileSync(jsonPath, 'utf8'));
|
||||||
@@ -91,7 +114,7 @@ function getFileSizeMp3(name)
|
|||||||
|
|
||||||
function getFileSizeMp4(name)
|
function getFileSizeMp4(name)
|
||||||
{
|
{
|
||||||
var jsonPath = videoPath+name+".info.json";
|
var jsonPath = videoFolderPath+name+".info.json";
|
||||||
var filesize = 0;
|
var filesize = 0;
|
||||||
if (fs.existsSync(jsonPath))
|
if (fs.existsSync(jsonPath))
|
||||||
{
|
{
|
||||||
@@ -111,7 +134,7 @@ function getFileSizeMp4(name)
|
|||||||
|
|
||||||
function getJSONMp3(name)
|
function getJSONMp3(name)
|
||||||
{
|
{
|
||||||
var jsonPath = audioPath+name+".mp3.info.json";
|
var jsonPath = audioFolderPath+name+".mp3.info.json";
|
||||||
if (fs.existsSync(jsonPath))
|
if (fs.existsSync(jsonPath))
|
||||||
var obj = JSON.parse(fs.readFileSync(jsonPath, 'utf8'));
|
var obj = JSON.parse(fs.readFileSync(jsonPath, 'utf8'));
|
||||||
else
|
else
|
||||||
@@ -122,7 +145,7 @@ function getJSONMp3(name)
|
|||||||
|
|
||||||
function getJSONMp4(name)
|
function getJSONMp4(name)
|
||||||
{
|
{
|
||||||
var jsonPath = videoPath+name+".info.json";
|
var jsonPath = videoFolderPath+name+".info.json";
|
||||||
if (fs.existsSync(jsonPath))
|
if (fs.existsSync(jsonPath))
|
||||||
{
|
{
|
||||||
var obj = JSON.parse(fs.readFileSync(jsonPath, 'utf8'));
|
var obj = JSON.parse(fs.readFileSync(jsonPath, 'utf8'));
|
||||||
@@ -133,7 +156,7 @@ function getJSONMp4(name)
|
|||||||
|
|
||||||
function getAmountDownloadedMp3(name)
|
function getAmountDownloadedMp3(name)
|
||||||
{
|
{
|
||||||
var partPath = audioPath+name+".mp3.part";
|
var partPath = audioFolderPath+name+".mp3.part";
|
||||||
if (fs.existsSync(partPath))
|
if (fs.existsSync(partPath))
|
||||||
{
|
{
|
||||||
const stats = fs.statSync(partPath);
|
const stats = fs.statSync(partPath);
|
||||||
@@ -149,7 +172,7 @@ function getAmountDownloadedMp3(name)
|
|||||||
function getAmountDownloadedMp4(name)
|
function getAmountDownloadedMp4(name)
|
||||||
{
|
{
|
||||||
var format = getVideoFormatID(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))
|
if (fs.existsSync(partPath))
|
||||||
{
|
{
|
||||||
const stats = fs.statSync(partPath);
|
const stats = fs.statSync(partPath);
|
||||||
@@ -162,7 +185,7 @@ function getAmountDownloadedMp4(name)
|
|||||||
|
|
||||||
function getVideoFormatID(name)
|
function getVideoFormatID(name)
|
||||||
{
|
{
|
||||||
var jsonPath = videoPath+name+".info.json";
|
var jsonPath = videoFolderPath+name+".info.json";
|
||||||
if (fs.existsSync(jsonPath))
|
if (fs.existsSync(jsonPath))
|
||||||
{
|
{
|
||||||
var obj = JSON.parse(fs.readFileSync(jsonPath, 'utf8'));
|
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) {
|
app.post('/tomp3', function(req, res) {
|
||||||
var url = req.body.url;
|
var url = req.body.url;
|
||||||
var date = Date.now();
|
var date = Date.now();
|
||||||
var path = audioPath;
|
var path = audioFolderPath;
|
||||||
var audiopath = Date.now();
|
var audiopath = '%(title)s';
|
||||||
youtubedl.exec(url, ['--external-downloader', 'aria2c', '-o', path + audiopath + ".mp3", '-x', '--audio-format', 'mp3', '--write-info-json'], {}, function(err, output) {
|
|
||||||
|
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) {
|
if (err) {
|
||||||
audiopath = "-1";
|
audiopath = "-1";
|
||||||
|
res.sendStatus(500);
|
||||||
throw err;
|
throw err;
|
||||||
}
|
} else if (output) {
|
||||||
});
|
var file_names = [];
|
||||||
|
for (let i = 0; i < output.length; i++) {
|
||||||
// write file info
|
let output_json = null;
|
||||||
|
try {
|
||||||
/*
|
output_json = JSON.parse(output[i]);
|
||||||
youtubedl.getInfo(url, function(err, info) {
|
} catch(e) {
|
||||||
if (err) throw err;
|
output_json = null;
|
||||||
|
}
|
||||||
var size = info.size;
|
if (!output_json) {
|
||||||
fs.writeFile("data/"+audiopath, size, function(err) {
|
// only run on first go
|
||||||
if(err) {
|
return;
|
||||||
return console.log(err);
|
}
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("The file was saved!");
|
let is_playlist = file_names.length > 1;
|
||||||
});
|
if (!is_playlist) audiopath = file_names[0];
|
||||||
});
|
|
||||||
*/
|
var audiopathEncoded = encodeURIComponent(file_names[0]);
|
||||||
var completeString = "done";
|
res.send({
|
||||||
var audiopathEncoded = encodeURIComponent(audiopath);
|
audiopathEncoded: audiopathEncoded,
|
||||||
res.send({
|
file_names: is_playlist ? file_names : null
|
||||||
audiopathEncoded: audiopathEncoded
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
res.end("yes");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
app.post('/tomp4', function(req, res) {
|
app.post('/tomp4', function(req, res) {
|
||||||
var url = req.body.url;
|
var url = req.body.url;
|
||||||
var date = Date.now();
|
var date = Date.now();
|
||||||
var path = videoPath;
|
var path = videoFolderPath;
|
||||||
var videopath = Date.now();
|
var videopath = '%(title)s';
|
||||||
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 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) {
|
if (err) {
|
||||||
videopath = "-1";
|
videopath = "-1";
|
||||||
|
res.sendStatus(500);
|
||||||
throw err;
|
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
|
// gets the status of the mp3 file that's being downloaded
|
||||||
app.post('/fileStatusMp3', function(req, res) {
|
app.post('/fileStatusMp3', function(req, res) {
|
||||||
var name = req.body.name + "";
|
var name = decodeURI(req.body.name + "");
|
||||||
var exists = "";
|
var exists = "";
|
||||||
var fullpath = audioPath + name + ".mp3";
|
var fullpath = audioFolderPath + name + ".mp3";
|
||||||
if (fs.existsSync(fullpath)) {
|
if (fs.existsSync(fullpath)) {
|
||||||
exists = [basePath + audioPath + name, getFileSizeMp3(name)];
|
exists = [basePath + audioFolderPath + name, getFileSizeMp3(name)];
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -250,14 +522,12 @@ app.post('/fileStatusMp3', function(req, res) {
|
|||||||
|
|
||||||
// gets the status of the mp4 file that's being downloaded
|
// gets the status of the mp4 file that's being downloaded
|
||||||
app.post('/fileStatusMp4', function(req, res) {
|
app.post('/fileStatusMp4', function(req, res) {
|
||||||
var name = req.body.name;
|
var name = decodeURI(req.body.name);
|
||||||
var exists = "";
|
var exists = "";
|
||||||
var fullpath = videoPath + name + ".mp4";
|
var fullpath = videoFolderPath + name + ".mp4";
|
||||||
if (fs.existsSync(fullpath)) {
|
if (fs.existsSync(fullpath)) {
|
||||||
exists = [basePath + videoPath + name, getFileSizeMp4(name)];
|
exists = [basePath + videoFolderPath + name, getFileSizeMp4(name)];
|
||||||
}
|
} else {
|
||||||
else
|
|
||||||
{
|
|
||||||
var percent = 0;
|
var percent = 0;
|
||||||
var size = getFileSizeMp4(name);
|
var size = getFileSizeMp4(name);
|
||||||
var downloaded = getAmountDownloadedMp4(name);
|
var downloaded = getAmountDownloadedMp4(name);
|
||||||
@@ -273,8 +543,9 @@ app.post('/fileStatusMp4', function(req, res) {
|
|||||||
// gets all download mp3s
|
// gets all download mp3s
|
||||||
app.post('/getMp3s', function(req, res) {
|
app.post('/getMp3s', function(req, res) {
|
||||||
var mp3s = [];
|
var mp3s = [];
|
||||||
var fullpath = audioPath;
|
var playlists = db.get('playlists.audio').value();
|
||||||
var files = fs.readdirSync(audioPath);
|
var fullpath = audioFolderPath;
|
||||||
|
var files = fs.readdirSync(audioFolderPath);
|
||||||
|
|
||||||
for (var i in files)
|
for (var i in files)
|
||||||
{
|
{
|
||||||
@@ -301,7 +572,8 @@ app.post('/getMp3s', function(req, res) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
res.send({
|
res.send({
|
||||||
mp3s: mp3s
|
mp3s: mp3s,
|
||||||
|
playlists: playlists
|
||||||
});
|
});
|
||||||
res.end("yes");
|
res.end("yes");
|
||||||
});
|
});
|
||||||
@@ -309,8 +581,9 @@ app.post('/getMp3s', function(req, res) {
|
|||||||
// gets all download mp4s
|
// gets all download mp4s
|
||||||
app.post('/getMp4s', function(req, res) {
|
app.post('/getMp4s', function(req, res) {
|
||||||
var mp4s = [];
|
var mp4s = [];
|
||||||
var fullpath = videoPath;
|
var playlists = db.get('playlists.video').value();
|
||||||
var files = fs.readdirSync(videoPath);
|
var fullpath = videoFolderPath;
|
||||||
|
var files = fs.readdirSync(videoFolderPath);
|
||||||
|
|
||||||
for (var i in files)
|
for (var i in files)
|
||||||
{
|
{
|
||||||
@@ -337,21 +610,64 @@ app.post('/getMp4s', function(req, res) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
res.send({
|
res.send({
|
||||||
mp4s: mp4s
|
mp4s: mp4s,
|
||||||
|
playlists: playlists
|
||||||
});
|
});
|
||||||
res.end("yes");
|
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
|
// deletes mp3 file
|
||||||
app.post('/deleteMp3', function(req, res) {
|
app.post('/deleteMp3', async (req, res) => {
|
||||||
var name = req.body.name;
|
var name = req.body.name;
|
||||||
var fullpath = audioPath + name + ".mp3";
|
var fullpath = audioFolderPath + name + ".mp3";
|
||||||
var wasDeleted = false;
|
var wasDeleted = false;
|
||||||
if (fs.existsSync(fullpath))
|
if (fs.existsSync(fullpath))
|
||||||
{
|
{
|
||||||
fs.unlink(fullpath, call => {
|
deleteAudioFile(name);
|
||||||
|
|
||||||
});
|
|
||||||
wasDeleted = true;
|
wasDeleted = true;
|
||||||
res.send(wasDeleted);
|
res.send(wasDeleted);
|
||||||
res.end("yes");
|
res.end("yes");
|
||||||
@@ -365,16 +681,14 @@ app.post('/deleteMp3', function(req, res) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// deletes mp4 file
|
// deletes mp4 file
|
||||||
app.post('/deleteMp4', function(req, res) {
|
app.post('/deleteMp4', async (req, res) => {
|
||||||
var name = req.body.name;
|
var name = req.body.name;
|
||||||
var fullpath = videoPath + name + ".mp4";
|
var fullpath = videoFolderPath + name + ".mp4";
|
||||||
var wasDeleted = false;
|
var wasDeleted = false;
|
||||||
if (fs.existsSync(fullpath))
|
if (fs.existsSync(fullpath))
|
||||||
{
|
{
|
||||||
fs.unlink(fullpath, call => {
|
wasDeleted = await deleteVideoFile(name);
|
||||||
// console.log(call);
|
// wasDeleted = true;
|
||||||
});
|
|
||||||
wasDeleted = true;
|
|
||||||
res.send(wasDeleted);
|
res.send(wasDeleted);
|
||||||
res.end("yes");
|
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){
|
app.get('/video/:id', function(req , res){
|
||||||
var head;
|
var head;
|
||||||
const path = "video/" + req.params.id + ".mp4";
|
const path = "video/" + req.params.id + '.mp4';
|
||||||
const stat = fs.statSync(path)
|
const stat = fs.statSync(path)
|
||||||
const fileSize = stat.size
|
const fileSize = stat.size
|
||||||
const range = req.headers.range
|
const range = req.headers.range
|
||||||
@@ -401,6 +744,13 @@ app.get('/video/:id', function(req , res){
|
|||||||
: fileSize-1
|
: fileSize-1
|
||||||
const chunksize = (end-start)+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 = {
|
head = {
|
||||||
'Content-Range': `bytes ${start}-${end}/${fileSize}`,
|
'Content-Range': `bytes ${start}-${end}/${fileSize}`,
|
||||||
'Accept-Ranges': 'bytes',
|
'Accept-Ranges': 'bytes',
|
||||||
@@ -421,7 +771,8 @@ app.get('/video/:id', function(req , res){
|
|||||||
|
|
||||||
app.get('/audio/:id', function(req , res){
|
app.get('/audio/:id', function(req , res){
|
||||||
var head;
|
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 stat = fs.statSync(path)
|
||||||
const fileSize = stat.size
|
const fileSize = stat.size
|
||||||
const range = req.headers.range
|
const range = req.headers.range
|
||||||
@@ -432,7 +783,14 @@ app.get('/audio/:id', function(req , res){
|
|||||||
? parseInt(parts[1], 10)
|
? parseInt(parts[1], 10)
|
||||||
: fileSize-1
|
: fileSize-1
|
||||||
const chunksize = (end-start)+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 = {
|
head = {
|
||||||
'Content-Range': `bytes ${start}-${end}/${fileSize}`,
|
'Content-Range': `bytes ${start}-${end}/${fileSize}`,
|
||||||
'Accept-Ranges': 'bytes',
|
'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)
|
if (usingEncryption)
|
||||||
|
|||||||
@@ -16,7 +16,17 @@
|
|||||||
},
|
},
|
||||||
"Extra": {
|
"Extra": {
|
||||||
"title_top": "Youtube Downloader",
|
"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": {
|
"Extra": {
|
||||||
"title_top": "Youtube Downloader",
|
"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",
|
"name": "backend",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "backend for hda",
|
"description": "backend for YoutubeDL-Material",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "echo \"Error: no test specified\" && exit 1"
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git+https://github.com/Tzahi12345/hda-backend.git"
|
"url": ""
|
||||||
},
|
},
|
||||||
"author": "Isaac Grynsztein",
|
"author": "Isaac Grynsztein",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"bugs": {
|
"bugs": {
|
||||||
"url": "https://github.com/Tzahi12345/hda-backend/issues"
|
"url": ""
|
||||||
},
|
},
|
||||||
"homepage": "https://github.com/Tzahi12345/hda-backend#readme",
|
"homepage": "",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"archiver": "^3.1.1",
|
||||||
"async": "^3.1.0",
|
"async": "^3.1.0",
|
||||||
"config": "^3.2.3",
|
"config": "^3.2.3",
|
||||||
"exe": "^1.0.2",
|
"exe": "^1.0.2",
|
||||||
"express": "^4.17.1",
|
"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",
|
"build": "ng build",
|
||||||
"test": "ng test",
|
"test": "ng test",
|
||||||
"lint": "ng lint",
|
"lint": "ng lint",
|
||||||
"e2e": "ng e2e"
|
"e2e": "ng e2e",
|
||||||
|
"electron": "ng build --base-href ./ && electron ."
|
||||||
},
|
},
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -25,20 +26,28 @@
|
|||||||
"@angular/platform-browser-dynamic": "^8.2.11",
|
"@angular/platform-browser-dynamic": "^8.2.11",
|
||||||
"@angular/router": "^8.2.11",
|
"@angular/router": "^8.2.11",
|
||||||
"core-js": "^2.4.1",
|
"core-js": "^2.4.1",
|
||||||
|
"file-saver": "^2.0.2",
|
||||||
|
"ng-lazyload-image": "^7.0.1",
|
||||||
"ng4-configure": "^0.1.7",
|
"ng4-configure": "^0.1.7",
|
||||||
|
"ngx-content-loading": "^0.1.3",
|
||||||
"rxjs": "^6.5.3",
|
"rxjs": "^6.5.3",
|
||||||
"rxjs-compat": "^6.0.0-rc.0",
|
"rxjs-compat": "^6.0.0-rc.0",
|
||||||
"tslib": "^1.10.0",
|
"tslib": "^1.10.0",
|
||||||
|
"videogular2": "^7.0.1",
|
||||||
|
"web-animations-js": "^2.3.2",
|
||||||
"zone.js": "~0.9.1"
|
"zone.js": "~0.9.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@angular-devkit/build-angular": "~0.803.12",
|
"@angular-devkit/build-angular": "^0.803.24",
|
||||||
"@angular/cli": "^8.3.12",
|
"@angular/cli": "^8.3.12",
|
||||||
"@angular/compiler-cli": "^8.2.11",
|
"@angular/compiler-cli": "^8.2.11",
|
||||||
"@angular/language-service": "^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/jasmine": "2.5.45",
|
||||||
"@types/node": "~6.0.60",
|
"@types/node": "~6.0.60",
|
||||||
"codelyzer": "^5.0.1",
|
"codelyzer": "^5.0.1",
|
||||||
|
"electron": "^8.0.1",
|
||||||
"jasmine-core": "~2.6.2",
|
"jasmine-core": "~2.6.2",
|
||||||
"jasmine-spec-reporter": "~4.1.0",
|
"jasmine-spec-reporter": "~4.1.0",
|
||||||
"karma": "~1.7.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 {
|
.flex-row {
|
||||||
margin: 16px;
|
display: flex;
|
||||||
}
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
.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%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
mat-form-field.mat-form-field {
|
.flex-column {
|
||||||
font-size: 24px;
|
display: flex;
|
||||||
}
|
flex-direction: column;
|
||||||
|
flex-basis: 100%;
|
||||||
.spinner {
|
flex: 1;
|
||||||
position: absolute;
|
|
||||||
display: inline-block;
|
|
||||||
margin-left: -28px;
|
|
||||||
margin-top: -10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.make-room-for-spinner {
|
|
||||||
padding-right: 40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.equal-sizes {
|
|
||||||
padding-right: 20px;
|
|
||||||
}
|
}
|
||||||
@@ -1,105 +1,17 @@
|
|||||||
<mat-toolbar color="primary" class="top">
|
<div [style.background]="postsService.theme ? postsService.theme.background_color : null" style="width: 100%; height: 100%;">
|
||||||
<table width="100%" height="100%">
|
<mat-toolbar color="primary" class="top">
|
||||||
<td class="topbar" style="text-align: left; left:0px; font-size: 15px">
|
<div class="flex-row" width="100%" height="100%">
|
||||||
</td>
|
<div class="flex-column" style="text-align: left; margin-top: 1px;">
|
||||||
<td class="topbar" style="text-align: center">
|
<button (click)="goBack()" *ngIf="router.url.split(';')[0] === '/player'" mat-icon-button><mat-icon>arrow_back</mat-icon></button>
|
||||||
<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>
|
</div>
|
||||||
</mat-card-content>
|
<div class="flex-column" style="text-align: center; margin-top: 5px;">
|
||||||
<mat-card-actions>
|
<div>{{topBarTitle}}</div>
|
||||||
<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>
|
</div>
|
||||||
|
<div class="flex-column" style="text-align: right; align-items: flex-end;">
|
||||||
</mat-expansion-panel>
|
<button *ngIf="allowThemeChange" mat-icon-button (click)="flipTheme()"><mat-icon>{{(postsService.theme.key === 'default') ? 'brightness_5' : 'brightness_2'}}</mat-icon></button>
|
||||||
<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>
|
</div>
|
||||||
</mat-expansion-panel>
|
</div>
|
||||||
</mat-accordion>
|
</mat-toolbar>
|
||||||
|
|
||||||
|
<router-outlet></router-outlet>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ng-template #nomp3s>
|
|
||||||
|
|
||||||
</ng-template>
|
|
||||||
|
|
||||||
<ng-template #nomp4s>
|
|
||||||
|
|
||||||
</ng-template>
|
|
||||||
@@ -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 {PostsService} from './posts.services';
|
||||||
import {FileCardComponent} from './file-card/file-card.component';
|
import {FileCardComponent} from './file-card/file-card.component';
|
||||||
import { Observable } from 'rxjs/Observable';
|
import { Observable } from 'rxjs/Observable';
|
||||||
import {FormControl, Validators} from '@angular/forms';
|
import {FormControl, Validators} from '@angular/forms';
|
||||||
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
|
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
|
||||||
import {MatSnackBar} from '@angular/material';
|
import {MatSnackBar} from '@angular/material';
|
||||||
|
import { saveAs } from 'file-saver';
|
||||||
import 'rxjs/add/observable/of';
|
import 'rxjs/add/observable/of';
|
||||||
import 'rxjs/add/operator/mapTo';
|
import 'rxjs/add/operator/mapTo';
|
||||||
import 'rxjs/add/operator/toPromise';
|
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({
|
@Component({
|
||||||
selector: 'app-root',
|
selector: 'app-root',
|
||||||
templateUrl: './app.component.html',
|
templateUrl: './app.component.html',
|
||||||
styleUrls: ['./app.component.css']
|
styleUrls: ['./app.component.css']
|
||||||
})
|
})
|
||||||
export class AppComponent {
|
export class AppComponent implements OnInit {
|
||||||
determinateProgress = false;
|
|
||||||
downloadingfile = false;
|
@HostBinding('class') componentCssClass;
|
||||||
audioOnly: boolean;
|
THEMES_CONFIG = THEMES_CONFIG;
|
||||||
urlError = false;
|
|
||||||
path = '';
|
// config items
|
||||||
url = '';
|
|
||||||
exists = '';
|
|
||||||
topBarTitle = 'Youtube Downloader';
|
topBarTitle = 'Youtube Downloader';
|
||||||
percentDownloaded: number;
|
defaultTheme = null;
|
||||||
fileManagerEnabled = false;
|
allowThemeChange = null;
|
||||||
|
|
||||||
mp3s: any[] = [];
|
|
||||||
mp4s: any[] = [];
|
|
||||||
|
|
||||||
urlForm = new FormControl('', [Validators.required]);
|
|
||||||
|
|
||||||
constructor(private postsService: PostsService, public snackBar: MatSnackBar) {
|
|
||||||
this.audioOnly = false;
|
|
||||||
|
|
||||||
|
@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
|
this.postsService.loadNavItems().subscribe(result => { // loads settings
|
||||||
const backendUrl = result['YoutubeDLMaterial']['Host']['backendurl'];
|
|
||||||
this.topBarTitle = result['YoutubeDLMaterial']['Extra']['title_top'];
|
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;
|
// sets theme to config default if it doesn't exist
|
||||||
this.postsService.startPath = backendUrl;
|
if (!localStorage.getItem('theme')) {
|
||||||
this.postsService.startPathSSL = backendUrl;
|
this.setTheme(themingExists ? this.defaultTheme : 'default');
|
||||||
|
|
||||||
if (this.fileManagerEnabled) {
|
|
||||||
this.getMp3s();
|
|
||||||
this.getMp4s();
|
|
||||||
}
|
}
|
||||||
}, error => {
|
}, error => {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
/*
|
|
||||||
|
|
||||||
doHandshake(url: string) {
|
// theme stuff
|
||||||
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);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
doHandshakeSSL(url: string) {
|
setTheme(theme) {
|
||||||
this.postsService.startHandshakeSSL(url).subscribe(theurl => {
|
// theme is registered, so set it to the stored cookie variable
|
||||||
this.postsService.path = theurl;
|
let old_theme = null;
|
||||||
this.postsService.handShakeComplete = true;
|
if (this.THEMES_CONFIG[theme]) {
|
||||||
console.log('Handshake complete!');
|
if (localStorage.getItem('theme')) {
|
||||||
},
|
old_theme = localStorage.getItem('theme');
|
||||||
error => {
|
if (!this.THEMES_CONFIG[old_theme]) {
|
||||||
console.log('Initial handshake failed on https too! Make sure port 17442 is open.');
|
console.log('bad theme found, setting to default');
|
||||||
this.postsService.handShakeComplete = false;
|
if (this.defaultTheme === null) {
|
||||||
});
|
// means it hasn't loaded yet
|
||||||
}*/
|
console.error('No default theme detected');
|
||||||
|
} else {
|
||||||
getMp3s() {
|
localStorage.setItem('theme', this.defaultTheme);
|
||||||
this.postsService.getMp3s().subscribe(result => {
|
old_theme = localStorage.getItem('theme'); // updates old_theme
|
||||||
const mp3s = result['mp3s'];
|
}
|
||||||
this.mp3s = mp3s;
|
}
|
||||||
}, error => {
|
}
|
||||||
console.log(error);
|
localStorage.setItem('theme', theme);
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
} else {
|
} 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) {
|
flipTheme() {
|
||||||
for (let i = 0; i < this.mp3s.length; i++) {
|
if (this.postsService.theme.key === 'default') {
|
||||||
if (this.mp3s[i].id === name) {
|
this.setTheme('dark');
|
||||||
this.mp3s.splice(i, 1);
|
} else if (this.postsService.theme.key === 'dark') {
|
||||||
}
|
this.setTheme('default');
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
}
|
if (localStorage.getItem('theme')) {
|
||||||
|
this.setTheme(localStorage.getItem('theme'));
|
||||||
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.');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
} 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) {
|
goBack() {
|
||||||
this.snackBar.open(message, action, {
|
this.router.navigate(['/home']);
|
||||||
duration: 2000,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,9 @@ import {MatNativeDateModule, MatRadioModule, MatInputModule, MatButtonModule, Ma
|
|||||||
MatSnackBarModule, MatCardModule, MatSelectModule, MatToolbarModule, MatCheckboxModule, MatGridListModule,
|
MatSnackBarModule, MatCardModule, MatSelectModule, MatToolbarModule, MatCheckboxModule, MatGridListModule,
|
||||||
MatProgressBarModule, MatExpansionModule,
|
MatProgressBarModule, MatExpansionModule,
|
||||||
MatGridList,
|
MatGridList,
|
||||||
MatProgressSpinnerModule} from '@angular/material';
|
MatProgressSpinnerModule,
|
||||||
|
MatButtonToggleModule,
|
||||||
|
MatDialogModule} from '@angular/material';
|
||||||
import {FormsModule, ReactiveFormsModule} from '@angular/forms';
|
import {FormsModule, ReactiveFormsModule} from '@angular/forms';
|
||||||
import { AppComponent } from './app.component';
|
import { AppComponent } from './app.component';
|
||||||
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
|
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 {APP_BASE_HREF} from '@angular/common';
|
||||||
import { FileCardComponent } from './file-card/file-card.component';
|
import { FileCardComponent } from './file-card/file-card.component';
|
||||||
import {RouterModule} from '@angular/router';
|
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({
|
@NgModule({
|
||||||
declarations: [
|
declarations: [
|
||||||
AppComponent,
|
AppComponent,
|
||||||
FileCardComponent
|
FileCardComponent,
|
||||||
|
MainComponent,
|
||||||
|
PlayerComponent,
|
||||||
|
InputDialogComponent
|
||||||
],
|
],
|
||||||
imports: [
|
imports: [
|
||||||
BrowserModule,
|
BrowserModule,
|
||||||
@@ -43,7 +58,19 @@ import {RouterModule} from '@angular/router';
|
|||||||
MatExpansionModule,
|
MatExpansionModule,
|
||||||
MatProgressBarModule,
|
MatProgressBarModule,
|
||||||
MatProgressSpinnerModule,
|
MatProgressSpinnerModule,
|
||||||
RouterModule
|
MatButtonToggleModule,
|
||||||
|
MatDialogModule,
|
||||||
|
VgCoreModule,
|
||||||
|
VgControlsModule,
|
||||||
|
VgOverlayPlayModule,
|
||||||
|
VgBufferingModule,
|
||||||
|
LazyLoadImageModule,
|
||||||
|
NgxContentLoadingModule,
|
||||||
|
RouterModule,
|
||||||
|
AppRoutingModule,
|
||||||
|
],
|
||||||
|
entryComponents: [
|
||||||
|
InputDialogComponent
|
||||||
],
|
],
|
||||||
providers: [PostsService],
|
providers: [PostsService],
|
||||||
bootstrap: [AppComponent]
|
bootstrap: [AppComponent]
|
||||||
|
|||||||
@@ -31,3 +31,29 @@
|
|||||||
top: 50%;
|
top: 50%;
|
||||||
left: 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>
|
<button (click)="deleteFile()" class="deleteButton" mat-icon-button><mat-icon>delete_forever</mat-icon></button>
|
||||||
<div style="padding:5px">
|
<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/>
|
<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>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<div class="centered example-full-width-height"><img class="image" src="{{thumbnailURL}}" alt="Thumbnail"></div>
|
|
||||||
|
|
||||||
</mat-card>
|
</mat-card>
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { Component, OnInit, Input, Output } from '@angular/core';
|
import { Component, OnInit, Input, Output } from '@angular/core';
|
||||||
import {PostsService} from '../posts.services';
|
import {PostsService} from '../posts.services';
|
||||||
import {MatSnackBar} from '@angular/material';
|
import {MatSnackBar} from '@angular/material';
|
||||||
import {AppComponent} from '../app.component';
|
|
||||||
import {EventEmitter} from '@angular/core';
|
import {EventEmitter} from '@angular/core';
|
||||||
|
import { MainComponent } from 'app/main/main.component';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-file-card',
|
selector: 'app-file-card',
|
||||||
@@ -11,31 +11,41 @@ import {EventEmitter} from '@angular/core';
|
|||||||
})
|
})
|
||||||
export class FileCardComponent implements OnInit {
|
export class FileCardComponent implements OnInit {
|
||||||
|
|
||||||
@Input() title:string;
|
@Input() title: string;
|
||||||
@Input() length:string;
|
@Input() length: string;
|
||||||
@Input() name:string;
|
@Input() name: string;
|
||||||
@Input() thumbnailURL: string;
|
@Input() thumbnailURL: string;
|
||||||
@Input() isAudio: boolean = true;
|
@Input() isAudio = true;
|
||||||
@Output() removeFile: EventEmitter<string> = new EventEmitter<string>();
|
@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() {
|
ngOnInit() {
|
||||||
|
this.type = this.isAudio ? 'audio' : 'video';
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteFile()
|
deleteFile() {
|
||||||
{
|
if (!this.isPlaylist) {
|
||||||
this.postsService.deleteFile(this.name, this.isAudio).subscribe(result => {
|
this.postsService.deleteFile(this.name, this.isAudio).subscribe(result => {
|
||||||
if (result == true)
|
if (result === true) {
|
||||||
{
|
this.openSnackBar('Delete success!', 'OK.');
|
||||||
this.openSnackBar("Delete success!", "OK.");
|
this.removeFile.emit(this.name);
|
||||||
this.removeFile.emit(this.name);
|
} else {
|
||||||
}
|
this.openSnackBar('Delete failed!', 'OK.');
|
||||||
else
|
}
|
||||||
{
|
});
|
||||||
this.openSnackBar("Delete failed!", "OK.");
|
} else {
|
||||||
}
|
this.removeFile.emit(this.name);
|
||||||
});
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
imageLoaded(loaded) {
|
||||||
|
this.image_loaded = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public openSnackBar(message: string, action: string) {
|
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 {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 config from '../assets/default.json';
|
||||||
import 'rxjs/add/operator/map';
|
import 'rxjs/add/operator/map';
|
||||||
import { Observable } from 'rxjs/Observable';
|
import { Observable } from 'rxjs/Observable';
|
||||||
import 'rxjs/add/operator/map';
|
import 'rxjs/add/operator/map';
|
||||||
import 'rxjs/add/operator/catch';
|
import 'rxjs/add/operator/catch';
|
||||||
import 'rxjs/add/observable/throw';
|
import 'rxjs/add/observable/throw';
|
||||||
|
import { THEMES_CONFIG } from '../themes';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class PostsService {
|
export class PostsService {
|
||||||
@@ -15,11 +16,17 @@ export class PostsService {
|
|||||||
startPath = 'http://localhost:17442/';
|
startPath = 'http://localhost:17442/';
|
||||||
startPathSSL = 'https://localhost:17442/'
|
startPathSSL = 'https://localhost:17442/'
|
||||||
handShakeComplete = false;
|
handShakeComplete = false;
|
||||||
|
THEMES_CONFIG = THEMES_CONFIG;
|
||||||
|
theme;
|
||||||
|
|
||||||
constructor(private http: HttpClient) {
|
constructor(private http: HttpClient) {
|
||||||
console.log('PostsService Initialized...');
|
console.log('PostsService Initialized...');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setTheme(theme) {
|
||||||
|
this.theme = this.THEMES_CONFIG[theme];
|
||||||
|
}
|
||||||
|
|
||||||
startHandshake(url: string) {
|
startHandshake(url: string) {
|
||||||
return this.http.get(url + 'geturl');
|
return this.http.get(url + 'geturl');
|
||||||
}
|
}
|
||||||
@@ -36,12 +43,16 @@ export class PostsService {
|
|||||||
return this.http.get(this.startPath + 'audiofolder');
|
return this.http.get(this.startPath + 'audiofolder');
|
||||||
}
|
}
|
||||||
|
|
||||||
makeMP3(url: string) {
|
makeMP3(url: string, selectedQuality: string, customQualityConfiguration: string) {
|
||||||
return this.http.post(this.path + 'tomp3', {url: url});
|
return this.http.post(this.path + 'tomp3', {url: url,
|
||||||
|
maxBitrate: selectedQuality,
|
||||||
|
customQualityConfiguration: customQualityConfiguration});
|
||||||
}
|
}
|
||||||
|
|
||||||
makeMP4(url: string) {
|
makeMP4(url: string, selectedQuality: string, customQualityConfiguration: string) {
|
||||||
return this.http.post(this.path + 'tomp4', {url: url});
|
return this.http.post(this.path + 'tomp4', {url: url,
|
||||||
|
selectedHeight: selectedQuality,
|
||||||
|
customQualityConfiguration: customQualityConfiguration});
|
||||||
}
|
}
|
||||||
|
|
||||||
getFileStatusMp3(name: string) {
|
getFileStatusMp3(name: string) {
|
||||||
@@ -56,8 +67,9 @@ export class PostsService {
|
|||||||
if (isDevMode()) {
|
if (isDevMode()) {
|
||||||
return this.http.get('./assets/default.json');
|
return this.http.get('./assets/default.json');
|
||||||
}
|
}
|
||||||
console.log('Config location: ' + window.location.href + 'backend/config/default.json');
|
const locations = window.location.href.split('#');
|
||||||
return this.http.get(window.location.href + 'backend/config/default.json');
|
const current_location = locations[0];
|
||||||
|
return this.http.get(current_location + 'backend/config/default.json');
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteFile(name: string, isAudio: boolean) {
|
deleteFile(name: string, isAudio: boolean) {
|
||||||
@@ -75,6 +87,29 @@ export class PostsService {
|
|||||||
getMp4s() {
|
getMp4s() {
|
||||||
return this.http.post(this.path + '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": {
|
"Extra": {
|
||||||
"title_top": "Youtube Downloader",
|
"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">
|
<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="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 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://unpkg.com/@angular/material/prebuilt-themes/indigo-pink.css" rel="stylesheet"> <script src="systemjs.config.js"></script>
|
|
||||||
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
|
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -42,11 +42,11 @@
|
|||||||
|
|
||||||
|
|
||||||
/** Evergreen browsers require these. **/
|
/** Evergreen browsers require these. **/
|
||||||
import 'core-js/es6/reflect';
|
// import 'core-js/es6/reflect';
|
||||||
|
|
||||||
|
|
||||||
/** ALL Firefox browsers require the following to support `@angular/animation`. **/
|
/** 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