mirror of
https://github.com/Tzahi12345/YoutubeDL-Material.git
synced 2026-03-07 20:10:03 +03:00
Compare commits
31 Commits
v1.1
...
v3.0-alpha
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
28331c1037 | ||
|
|
202c0718b7 | ||
|
|
a985963661 | ||
|
|
f673b325fd | ||
|
|
5f4a7a1e69 | ||
|
|
d54b2a73c8 | ||
|
|
8e7bb4ba3b | ||
|
|
d595de5786 | ||
|
|
31394fa98c | ||
|
|
af595d3df8 | ||
|
|
81377a2b38 | ||
|
|
35bdd1deeb | ||
|
|
a1ec53edb9 | ||
|
|
aa130d3fc9 | ||
|
|
0f0bf3a401 | ||
|
|
73b9c61080 | ||
|
|
501806909a | ||
|
|
77dd96b3b9 | ||
|
|
505b145bb3 | ||
|
|
ba5592015d | ||
|
|
10c90a01f2 | ||
|
|
abaa799628 | ||
|
|
9e148d1464 | ||
|
|
9fa646132d | ||
|
|
f98ba00551 | ||
|
|
bc1e0ea542 | ||
|
|
9ebb684d5c | ||
|
|
91713f1140 | ||
|
|
377676cda4 | ||
|
|
8e445bb80d | ||
|
|
f0bde1efc4 |
@@ -1,61 +0,0 @@
|
||||
{
|
||||
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
|
||||
"project": {
|
||||
"name": "youtube-dl-material"
|
||||
},
|
||||
"apps": [
|
||||
{
|
||||
"root": "src",
|
||||
"outDir": "dist",
|
||||
"assets": [
|
||||
"assets",
|
||||
"favicon.ico",
|
||||
"backend/audio",
|
||||
"backend/video",
|
||||
"backend",
|
||||
{ "glob": "default.json", "input": "./", "output": "../backend/config/", "allowOutsideOutDir": true }
|
||||
],
|
||||
"index": "index.html",
|
||||
"main": "main.ts",
|
||||
"polyfills": "polyfills.ts",
|
||||
"test": "test.ts",
|
||||
"tsconfig": "tsconfig.app.json",
|
||||
"testTsconfig": "tsconfig.spec.json",
|
||||
"prefix": "app",
|
||||
"styles": [
|
||||
"styles.css"
|
||||
],
|
||||
"scripts": [],
|
||||
"environmentSource": "environments/environment.ts",
|
||||
"environments": {
|
||||
"dev": "environments/environment.ts",
|
||||
"prod": "environments/environment.prod.ts"
|
||||
}
|
||||
}
|
||||
],
|
||||
"e2e": {
|
||||
"protractor": {
|
||||
"config": "./protractor.conf.js"
|
||||
}
|
||||
},
|
||||
"lint": [
|
||||
{
|
||||
"project": "src/tsconfig.app.json"
|
||||
},
|
||||
{
|
||||
"project": "src/tsconfig.spec.json"
|
||||
},
|
||||
{
|
||||
"project": "e2e/tsconfig.e2e.json"
|
||||
}
|
||||
],
|
||||
"test": {
|
||||
"karma": {
|
||||
"config": "./karma.conf.js"
|
||||
}
|
||||
},
|
||||
"defaults": {
|
||||
"styleExt": "css",
|
||||
"component": {}
|
||||
}
|
||||
}
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -45,4 +45,5 @@ node_modules/*
|
||||
backend/node_modules/*
|
||||
YoutubeDL-Material/node_modules/*
|
||||
backend/video/*
|
||||
backend/audio/*
|
||||
backend/audio/*
|
||||
src/assets/default.json
|
||||
|
||||
21
.vscode/launch.json
vendored
Normal file
21
.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"type": "node",
|
||||
"request": "attach",
|
||||
"name": "Attach",
|
||||
"port": 9229
|
||||
},
|
||||
{
|
||||
"type": "chrome",
|
||||
"request": "launch",
|
||||
"name": "Launch chrome against localhost",
|
||||
"url": "http://localhost:4200",
|
||||
"webRoot": "${workspaceFolder}"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -10,6 +10,10 @@ Here's an image of what it'll look like once you're done:
|
||||
|
||||

|
||||
|
||||
With optional file management enabled (default):
|
||||
|
||||

|
||||
|
||||
### Prerequisites
|
||||
|
||||
You need to have a functioning web server for this to work. Also make sure you have these dependencies installed on your system: ffmpeg, nodejs, python. If you don't, run this command:
|
||||
|
||||
136
angular.json
Normal file
136
angular.json
Normal file
@@ -0,0 +1,136 @@
|
||||
{
|
||||
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
|
||||
"version": 1,
|
||||
"newProjectRoot": "projects",
|
||||
"projects": {
|
||||
"youtube-dl-material": {
|
||||
"root": "",
|
||||
"sourceRoot": "src",
|
||||
"projectType": "application",
|
||||
"architect": {
|
||||
"build": {
|
||||
"builder": "@angular-devkit/build-angular:browser",
|
||||
"options": {
|
||||
"outputPath": "dist",
|
||||
"index": "src/index.html",
|
||||
"main": "src/main.ts",
|
||||
"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.css",
|
||||
"../node_modules/videogular2/fonts/videogular.css"
|
||||
],
|
||||
"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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"serve": {
|
||||
"builder": "@angular-devkit/build-angular:dev-server",
|
||||
"options": {
|
||||
"browserTarget": "youtube-dl-material:build"
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"browserTarget": "youtube-dl-material:build:production"
|
||||
}
|
||||
}
|
||||
},
|
||||
"extract-i18n": {
|
||||
"builder": "@angular-devkit/build-angular:extract-i18n",
|
||||
"options": {
|
||||
"browserTarget": "youtube-dl-material:build"
|
||||
}
|
||||
},
|
||||
"test": {
|
||||
"builder": "@angular-devkit/build-angular:karma",
|
||||
"options": {
|
||||
"main": "src/test.ts",
|
||||
"karmaConfig": "./karma.conf.js",
|
||||
"polyfills": "src/polyfills.ts",
|
||||
"tsConfig": "src/tsconfig.spec.json",
|
||||
"scripts": [],
|
||||
"styles": [
|
||||
"src/styles.css",
|
||||
"../node_modules/videogular2/fonts/videogular.css"
|
||||
],
|
||||
"assets": [
|
||||
"src/assets",
|
||||
"src/favicon.ico",
|
||||
"src/backend/audio",
|
||||
"src/backend/video",
|
||||
"src/backend"
|
||||
]
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
"builder": "@angular-devkit/build-angular:tslint",
|
||||
"options": {
|
||||
"tsConfig": [
|
||||
"src/tsconfig.app.json",
|
||||
"src/tsconfig.spec.json"
|
||||
],
|
||||
"exclude": []
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"youtube-dl-material-e2e": {
|
||||
"root": "e2e",
|
||||
"sourceRoot": "e2e",
|
||||
"projectType": "application",
|
||||
"architect": {
|
||||
"e2e": {
|
||||
"builder": "@angular-devkit/build-angular:protractor",
|
||||
"options": {
|
||||
"protractorConfig": "./protractor.conf.js",
|
||||
"devServerTarget": "youtube-dl-material:serve"
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
"builder": "@angular-devkit/build-angular:tslint",
|
||||
"options": {
|
||||
"tsConfig": [
|
||||
"e2e/tsconfig.e2e.json"
|
||||
],
|
||||
"exclude": []
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"defaultProject": "youtube-dl-material",
|
||||
"schematics": {
|
||||
"@schematics/angular:component": {
|
||||
"prefix": "app",
|
||||
"styleext": "css"
|
||||
},
|
||||
"@schematics/angular:directive": {
|
||||
"prefix": "app"
|
||||
}
|
||||
}
|
||||
}
|
||||
436
backend/app.js
436
backend/app.js
@@ -6,16 +6,26 @@ var config = require('config');
|
||||
var https = require('https');
|
||||
var express = require("express");
|
||||
var bodyParser = require("body-parser");
|
||||
var pem = require('https-pem');
|
||||
var app = express();
|
||||
|
||||
var frontendUrl = config.get("YoutubeDLMaterial.Host.frontendurl");
|
||||
var URL = require('url').URL;
|
||||
|
||||
// check if debug mode
|
||||
let debugMode = process.env.YTDL_MODE === 'debug';
|
||||
|
||||
if (debugMode) console.log('YTDL-Material in debug mode!');
|
||||
|
||||
var frontendUrl = !debugMode ? config.get("YoutubeDLMaterial.Host.frontendurl") : 'http://localhost:4200';
|
||||
var backendUrl = config.get("YoutubeDLMaterial.Host.backendurl")
|
||||
var backendPort = 17442;
|
||||
var usingEncryption = config.get("YoutubeDLMaterial.Encryption.use-encryption");
|
||||
var basePath = config.get("YoutubeDLMaterial.Downloader.path-base");
|
||||
var audioPath = config.get("YoutubeDLMaterial.Downloader.path-audio");
|
||||
var videoPath = config.get("YoutubeDLMaterial.Downloader.path-video");
|
||||
var audioFolderPath = config.get("YoutubeDLMaterial.Downloader.path-audio");
|
||||
var videoFolderPath = config.get("YoutubeDLMaterial.Downloader.path-video");
|
||||
var downloadOnlyMode = config.get("YoutubeDLMaterial.Extra.download_only_mode")
|
||||
|
||||
var descriptors = {};
|
||||
|
||||
|
||||
if (usingEncryption)
|
||||
{
|
||||
@@ -37,8 +47,10 @@ if (usingEncryption)
|
||||
app.use(bodyParser.urlencoded({ extended: false }));
|
||||
app.use(bodyParser.json());
|
||||
|
||||
var url_domain = new URL(frontendUrl);
|
||||
|
||||
app.use(function(req, res, next) {
|
||||
res.header("Access-Control-Allow-Origin", frontendUrl);
|
||||
res.header("Access-Control-Allow-Origin", url_domain.origin);
|
||||
res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
|
||||
next();
|
||||
});
|
||||
@@ -76,7 +88,7 @@ function getThumbnailMp4(name)
|
||||
|
||||
function getFileSizeMp3(name)
|
||||
{
|
||||
var jsonPath = audioPath+name+".mp3.info.json";
|
||||
var jsonPath = audioFolderPath+name+".mp3.info.json";
|
||||
|
||||
if (fs.existsSync(jsonPath))
|
||||
var obj = JSON.parse(fs.readFileSync(jsonPath, 'utf8'));
|
||||
@@ -88,7 +100,7 @@ function getFileSizeMp3(name)
|
||||
|
||||
function getFileSizeMp4(name)
|
||||
{
|
||||
var jsonPath = videoPath+name+".info.json";
|
||||
var jsonPath = videoFolderPath+name+".info.json";
|
||||
var filesize = 0;
|
||||
if (fs.existsSync(jsonPath))
|
||||
{
|
||||
@@ -108,7 +120,7 @@ function getFileSizeMp4(name)
|
||||
|
||||
function getJSONMp3(name)
|
||||
{
|
||||
var jsonPath = audioPath+name+".mp3.info.json";
|
||||
var jsonPath = audioFolderPath+name+".mp3.info.json";
|
||||
if (fs.existsSync(jsonPath))
|
||||
var obj = JSON.parse(fs.readFileSync(jsonPath, 'utf8'));
|
||||
else
|
||||
@@ -119,7 +131,7 @@ function getJSONMp3(name)
|
||||
|
||||
function getJSONMp4(name)
|
||||
{
|
||||
var jsonPath = videoPath+name+".info.json";
|
||||
var jsonPath = videoFolderPath+name+".info.json";
|
||||
if (fs.existsSync(jsonPath))
|
||||
{
|
||||
var obj = JSON.parse(fs.readFileSync(jsonPath, 'utf8'));
|
||||
@@ -130,7 +142,7 @@ function getJSONMp4(name)
|
||||
|
||||
function getAmountDownloadedMp3(name)
|
||||
{
|
||||
var partPath = audioPath+name+".mp3.part";
|
||||
var partPath = audioFolderPath+name+".mp3.part";
|
||||
if (fs.existsSync(partPath))
|
||||
{
|
||||
const stats = fs.statSync(partPath);
|
||||
@@ -146,7 +158,7 @@ function getAmountDownloadedMp3(name)
|
||||
function getAmountDownloadedMp4(name)
|
||||
{
|
||||
var format = getVideoFormatID(name);
|
||||
var partPath = videoPath+name+".f"+format+".mp4.part";
|
||||
var partPath = videoFolderPath+name+".f"+format+".mp4.part";
|
||||
if (fs.existsSync(partPath))
|
||||
{
|
||||
const stats = fs.statSync(partPath);
|
||||
@@ -159,7 +171,7 @@ function getAmountDownloadedMp4(name)
|
||||
|
||||
function getVideoFormatID(name)
|
||||
{
|
||||
var jsonPath = videoPath+name+".info.json";
|
||||
var jsonPath = videoFolderPath+name+".info.json";
|
||||
if (fs.existsSync(jsonPath))
|
||||
{
|
||||
var obj = JSON.parse(fs.readFileSync(jsonPath, 'utf8'));
|
||||
@@ -168,62 +180,289 @@ function getVideoFormatID(name)
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
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 {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
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 {
|
||||
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 {
|
||||
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 {
|
||||
// probably multiple urls
|
||||
console.log('failed to parse');
|
||||
console.log(output);
|
||||
}
|
||||
resolve(result);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
app.post('/tomp3', function(req, res) {
|
||||
var url = req.body.url;
|
||||
var date = Date.now();
|
||||
var path = audioPath;
|
||||
var audiopath = Date.now();
|
||||
youtubedl.exec(url, ['-o', path + audiopath + ".mp3", '-x', '--audio-format', 'mp3', '--write-info-json'], {}, function(err, output) {
|
||||
var path = audioFolderPath;
|
||||
var audiopath = '%(title)s';
|
||||
|
||||
var customQualityConfiguration = req.body.customQualityConfiguration;
|
||||
var maxBitrate = req.body.maxBitrate;
|
||||
|
||||
let downloadConfig = ['--external-downloader', 'aria2c', '-o', path + audiopath + ".mp3", '-x', '--audio-format', 'mp3', '--write-info-json', '--print-json']
|
||||
let qualityPath = '';
|
||||
|
||||
if (customQualityConfiguration) {
|
||||
qualityPath = `-f ${customQualityConfiguration}`;
|
||||
} else if (maxBitrate) {
|
||||
if (!maxBitrate || maxBitrate === '') maxBitrate = '0';
|
||||
qualityPath = `--audio-quality ${maxBitrate}`
|
||||
}
|
||||
|
||||
if (qualityPath !== '') {
|
||||
downloadConfig.splice(2, 0, qualityPath);
|
||||
}
|
||||
|
||||
youtubedl.exec(url, downloadConfig, {}, function(err, output) {
|
||||
if (debugMode) {
|
||||
let new_date = Date.now();
|
||||
let difference = (new_date - date)/1000;
|
||||
console.log(`Audio download delay: ${difference} seconds.`);
|
||||
}
|
||||
if (err) {
|
||||
audiopath = "-1";
|
||||
res.sendStatus(500);
|
||||
throw err;
|
||||
} else if (output) {
|
||||
var file_names = [];
|
||||
for (let i = 0; i < output.length; i++) {
|
||||
let output_json = null;
|
||||
try {
|
||||
output_json = JSON.parse(output[i]);
|
||||
} catch {
|
||||
output_json = null;
|
||||
}
|
||||
if (!output_json) {
|
||||
// only run on first go
|
||||
return;
|
||||
}
|
||||
var modified_file_name = output_json ? output_json['title'] : null;
|
||||
var file_path = output_json['_filename'].split('\\');
|
||||
var alternate_file_name = file_path[file_path.length - 1];
|
||||
alternate_file_name = alternate_file_name.substring(0, alternate_file_name.length-4);
|
||||
if (alternate_file_name) file_names.push(alternate_file_name);
|
||||
}
|
||||
|
||||
let is_playlist = file_names.length > 1;
|
||||
if (!is_playlist) audiopath = file_names[0];
|
||||
|
||||
var audiopathEncoded = encodeURIComponent(file_names[0]);
|
||||
res.send({
|
||||
audiopathEncoded: audiopathEncoded,
|
||||
file_names: is_playlist ? file_names : null
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// write file info
|
||||
|
||||
youtubedl.getInfo(url, function(err, info) {
|
||||
if (err) throw err;
|
||||
|
||||
var size = info.size;
|
||||
fs.writeFile("data/"+audiopath, size, function(err) {
|
||||
if(err) {
|
||||
return console.log(err);
|
||||
}
|
||||
|
||||
console.log("The file was saved!");
|
||||
});
|
||||
});
|
||||
var completeString = "done";
|
||||
var audiopathEncoded = encodeURIComponent(audiopath);
|
||||
res.send(audiopathEncoded);
|
||||
res.end("yes");
|
||||
});
|
||||
|
||||
app.post('/tomp4', function(req, res) {
|
||||
var url = req.body.url;
|
||||
var date = Date.now();
|
||||
var path = videoPath;
|
||||
var videopath = Date.now();
|
||||
youtubedl.exec(url, ['-o', path + videopath + ".mp4", '-f', 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/mp4', '--write-info-json'], {}, function(err, output) {
|
||||
var path = videoFolderPath;
|
||||
var videopath = '%(title)s';
|
||||
|
||||
var selectedHeight = req.body.selectedHeight;
|
||||
var customQualityConfiguration = req.body.customQualityConfiguration;
|
||||
|
||||
// console.log(selectedHeight);
|
||||
|
||||
let qualityPath = 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/mp4';
|
||||
|
||||
if (customQualityConfiguration) {
|
||||
qualityPath = customQualityConfiguration;
|
||||
} else if (selectedHeight && selectedHeight !== '') {
|
||||
qualityPath = `bestvideo[height=${selectedHeight}]+bestaudio/best[height=${selectedHeight}]`;
|
||||
}
|
||||
|
||||
youtubedl.exec(url, ['--external-downloader', 'aria2c', '-o', path + videopath + ".mp4", '-f', qualityPath, '--write-info-json', '--print-json'], {}, function(err, output) {
|
||||
if (debugMode) {
|
||||
let new_date = Date.now();
|
||||
let difference = (new_date - date)/1000;
|
||||
console.log(`Video download delay: ${difference} seconds.`);
|
||||
}
|
||||
if (err) {
|
||||
videopath = "-1";
|
||||
res.sendStatus(500);
|
||||
throw err;
|
||||
} else if (output) {
|
||||
var file_names = [];
|
||||
for (let i = 0; i < output.length; i++) {
|
||||
let output_json = null;
|
||||
try {
|
||||
output_json = JSON.parse(output[i]);
|
||||
} catch {
|
||||
output_json = null;
|
||||
}
|
||||
var modified_file_name = output_json ? output_json['title'] : null;
|
||||
if (!output_json) {
|
||||
// only get the first result
|
||||
// console.log(output_json);
|
||||
// console.log(output);
|
||||
continue;
|
||||
res.sendStatus(500);
|
||||
}
|
||||
var file_path = output_json['_filename'].split('\\');
|
||||
|
||||
// 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_path + '.webm to ' + file_path);
|
||||
} catch {
|
||||
}
|
||||
}
|
||||
var alternate_file_name = file_path[file_path.length - 1];
|
||||
alternate_file_name = alternate_file_name.substring(0, alternate_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);
|
||||
res.end("yes");
|
||||
});
|
||||
|
||||
// gets the status of the mp3 file that's being downloaded
|
||||
app.post('/fileStatusMp3', function(req, res) {
|
||||
var name = req.body.name + "";
|
||||
var name = decodeURI(req.body.name + "");
|
||||
var exists = "";
|
||||
var fullpath = audioPath + name + ".mp3";
|
||||
var fullpath = audioFolderPath + name + ".mp3";
|
||||
if (fs.existsSync(fullpath)) {
|
||||
exists = [basePath + audioPath + name, getFileSizeMp3(name)];
|
||||
exists = [basePath + audioFolderPath + name, getFileSizeMp3(name)];
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -241,14 +480,12 @@ app.post('/fileStatusMp3', function(req, res) {
|
||||
|
||||
// gets the status of the mp4 file that's being downloaded
|
||||
app.post('/fileStatusMp4', function(req, res) {
|
||||
var name = req.body.name;
|
||||
var name = decodeURI(req.body.name);
|
||||
var exists = "";
|
||||
var fullpath = videoPath + name + ".mp4";
|
||||
var fullpath = videoFolderPath + name + ".mp4";
|
||||
if (fs.existsSync(fullpath)) {
|
||||
exists = [basePath + videoPath + name, getFileSizeMp4(name)];
|
||||
}
|
||||
else
|
||||
{
|
||||
exists = [basePath + videoFolderPath + name, getFileSizeMp4(name)];
|
||||
} else {
|
||||
var percent = 0;
|
||||
var size = getFileSizeMp4(name);
|
||||
var downloaded = getAmountDownloadedMp4(name);
|
||||
@@ -264,8 +501,8 @@ app.post('/fileStatusMp4', function(req, res) {
|
||||
// gets all download mp3s
|
||||
app.post('/getMp3s', function(req, res) {
|
||||
var mp3s = [];
|
||||
var fullpath = audioPath;
|
||||
var files = fs.readdirSync(audioPath);
|
||||
var fullpath = audioFolderPath;
|
||||
var files = fs.readdirSync(audioFolderPath);
|
||||
|
||||
for (var i in files)
|
||||
{
|
||||
@@ -274,6 +511,7 @@ app.post('/getMp3s', function(req, res) {
|
||||
if (ext == ".mp3")
|
||||
{
|
||||
var jsonobj = getJSONMp3(path.basename(files[i]).substring(0, path.basename(files[i]).length-4));
|
||||
if (!jsonobj) continue;
|
||||
var id = path.basename(files[i]).substring(0, path.basename(files[i]).length-4);
|
||||
var title = jsonobj.title;
|
||||
|
||||
@@ -290,15 +528,17 @@ app.post('/getMp3s', function(req, res) {
|
||||
}
|
||||
}
|
||||
|
||||
res.send(mp3s);
|
||||
res.send({
|
||||
mp3s: mp3s
|
||||
});
|
||||
res.end("yes");
|
||||
});
|
||||
|
||||
// gets all download mp4s
|
||||
app.post('/getMp4s', function(req, res) {
|
||||
var mp4s = [];
|
||||
var fullpath = videoPath;
|
||||
var files = fs.readdirSync(videoPath);
|
||||
var fullpath = videoFolderPath;
|
||||
var files = fs.readdirSync(videoFolderPath);
|
||||
|
||||
for (var i in files)
|
||||
{
|
||||
@@ -307,6 +547,7 @@ app.post('/getMp4s', function(req, res) {
|
||||
if (ext == ".mp4")
|
||||
{
|
||||
var jsonobj = getJSONMp4(path.basename(files[i]).substring(0, path.basename(files[i]).length-4));
|
||||
if (!jsonobj) continue;
|
||||
var id = path.basename(files[i]).substring(0, path.basename(files[i]).length-4);
|
||||
var title = jsonobj.title;
|
||||
|
||||
@@ -323,18 +564,20 @@ app.post('/getMp4s', function(req, res) {
|
||||
}
|
||||
}
|
||||
|
||||
res.send(mp4s);
|
||||
res.send({
|
||||
mp4s: mp4s
|
||||
});
|
||||
res.end("yes");
|
||||
});
|
||||
|
||||
// deletes mp3 file
|
||||
app.post('/deleteMp3', function(req, res) {
|
||||
app.post('/deleteMp3', async (req, res) => {
|
||||
var name = req.body.name;
|
||||
var fullpath = audioPath + name + ".mp3";
|
||||
var fullpath = audioFolderPath + name + ".mp3";
|
||||
var wasDeleted = false;
|
||||
if (fs.existsSync(fullpath))
|
||||
{
|
||||
fs.unlink(fullpath);
|
||||
deleteAudioFile(name);
|
||||
wasDeleted = true;
|
||||
res.send(wasDeleted);
|
||||
res.end("yes");
|
||||
@@ -348,14 +591,14 @@ app.post('/deleteMp3', function(req, res) {
|
||||
});
|
||||
|
||||
// deletes mp4 file
|
||||
app.post('/deleteMp4', function(req, res) {
|
||||
app.post('/deleteMp4', async (req, res) => {
|
||||
var name = req.body.name;
|
||||
var fullpath = videoPath + name + ".mp4";
|
||||
var fullpath = videoFolderPath + name + ".mp4";
|
||||
var wasDeleted = false;
|
||||
if (fs.existsSync(fullpath))
|
||||
{
|
||||
fs.unlink(fullpath);
|
||||
wasDeleted = true;
|
||||
wasDeleted = await deleteVideoFile(name);
|
||||
// wasDeleted = true;
|
||||
res.send(wasDeleted);
|
||||
res.end("yes");
|
||||
}
|
||||
@@ -367,10 +610,34 @@ app.post('/deleteMp4', function(req, res) {
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/downloadFile', function(req, res) {
|
||||
let fileName = req.body.fileName;
|
||||
let is_playlist = req.body.is_playlist;
|
||||
let type = req.body.type;
|
||||
let file = null;
|
||||
if (type === 'audio') {
|
||||
file = __dirname + '/' + 'audio/' + fileName + '.mp3';
|
||||
} else if (type === 'video') {
|
||||
file = __dirname + '/' + 'video/' + fileName + '.mp4';
|
||||
}
|
||||
|
||||
res.sendFile(file);
|
||||
});
|
||||
|
||||
app.post('/deleteFile', async (req, res) => {
|
||||
let fileName = req.body.fileName;
|
||||
let type = req.body.type;
|
||||
if (type === 'audio') {
|
||||
deleteAudioFile(fileName);
|
||||
} else if (type === 'video') {
|
||||
deleteVideoFile(fileName);
|
||||
}
|
||||
res.send()
|
||||
});
|
||||
|
||||
app.get('/video/:id', function(req , res){
|
||||
var head;
|
||||
const path = "video/" + req.params.id + ".mp4";
|
||||
const path = "video/" + req.params.id + '.mp4';
|
||||
const stat = fs.statSync(path)
|
||||
const fileSize = stat.size
|
||||
const range = req.headers.range
|
||||
@@ -382,6 +649,13 @@ app.get('/video/:id', function(req , res){
|
||||
: fileSize-1
|
||||
const chunksize = (end-start)+1
|
||||
const file = fs.createReadStream(path, {start, end})
|
||||
if (descriptors[req.params.id]) descriptors[req.params.id].push(file);
|
||||
else descriptors[req.params.id] = [file];
|
||||
file.on('close', function() {
|
||||
let index = descriptors[req.params.id].indexOf(file);
|
||||
descriptors[req.params.id].splice(index, 1);
|
||||
console.log('Successfully closed stream and removed file reference.');
|
||||
});
|
||||
head = {
|
||||
'Content-Range': `bytes ${start}-${end}/${fileSize}`,
|
||||
'Accept-Ranges': 'bytes',
|
||||
@@ -402,7 +676,8 @@ app.get('/video/:id', function(req , res){
|
||||
|
||||
app.get('/audio/:id', function(req , res){
|
||||
var head;
|
||||
const path = "audio/" + req.params.id + ".mp3";
|
||||
let path = "audio/" + req.params.id + '.mp3';
|
||||
path = path.replace(/\"/g, '\'');
|
||||
const stat = fs.statSync(path)
|
||||
const fileSize = stat.size
|
||||
const range = req.headers.range
|
||||
@@ -413,7 +688,14 @@ app.get('/audio/:id', function(req , res){
|
||||
? parseInt(parts[1], 10)
|
||||
: fileSize-1
|
||||
const chunksize = (end-start)+1
|
||||
const file = fs.createReadStream(path, {start, end})
|
||||
const file = fs.createReadStream(path, {start, end});
|
||||
if (descriptors[req.params.id]) descriptors[req.params.id].push(file);
|
||||
else descriptors[req.params.id] = [file];
|
||||
file.on('close', function() {
|
||||
let index = descriptors[req.params.id].indexOf(file);
|
||||
descriptors[req.params.id].splice(index, 1);
|
||||
console.log('Successfully closed stream and removed file reference.');
|
||||
});
|
||||
head = {
|
||||
'Content-Range': `bytes ${start}-${end}/${fileSize}`,
|
||||
'Accept-Ranges': 'bytes',
|
||||
@@ -433,6 +715,28 @@ 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;
|
||||
// console.log(urlMode);
|
||||
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)
|
||||
|
||||
BIN
backend/aria2c.exe
Normal file
BIN
backend/aria2c.exe
Normal file
Binary file not shown.
@@ -16,7 +16,12 @@
|
||||
},
|
||||
"Extra": {
|
||||
"title_top": "Youtube Downloader",
|
||||
"file_manager_enabled": true
|
||||
"download_only_mode": false,
|
||||
"file_manager_enabled": true
|
||||
},
|
||||
"API": {
|
||||
"use_youtube_API": false,
|
||||
"youtube_API_key": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,12 @@
|
||||
},
|
||||
"Extra": {
|
||||
"title_top": "Youtube Downloader",
|
||||
"file_manager_enabled": true
|
||||
"download_only_mode": false,
|
||||
"file_manager_enabled": true
|
||||
},
|
||||
"API": {
|
||||
"use_youtube_API": false,
|
||||
"youtube_API_key": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,10 @@
|
||||
},
|
||||
"homepage": "https://github.com/Tzahi12345/hda-backend#readme",
|
||||
"dependencies": {
|
||||
"async": "^3.1.0",
|
||||
"config": "^3.2.3",
|
||||
"exe": "^1.0.2",
|
||||
"youtube-dl": "1.11.1"
|
||||
"express": "^4.17.1",
|
||||
"youtube-dl": "^2.3.0"
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
12
browserslist
Normal file
12
browserslist
Normal file
@@ -0,0 +1,12 @@
|
||||
# This file is used by the build system to adjust CSS and JS output to support the specified browsers below.
|
||||
# For additional information regarding the format and rule options, please see:
|
||||
# https://github.com/browserslist/browserslist#queries
|
||||
|
||||
# You can see what browsers were selected by your queries by running:
|
||||
# npx browserslist
|
||||
|
||||
> 0.5%
|
||||
last 2 versions
|
||||
Firefox ESR
|
||||
not dead
|
||||
not IE 9-11 # For IE 9-11 support, remove 'not'.
|
||||
@@ -4,24 +4,22 @@
|
||||
module.exports = function (config) {
|
||||
config.set({
|
||||
basePath: '',
|
||||
frameworks: ['jasmine', '@angular/cli'],
|
||||
frameworks: ['jasmine', '@angular-devkit/build-angular'],
|
||||
plugins: [
|
||||
require('karma-jasmine'),
|
||||
require('karma-chrome-launcher'),
|
||||
require('karma-jasmine-html-reporter'),
|
||||
require('karma-coverage-istanbul-reporter'),
|
||||
require('@angular/cli/plugins/karma')
|
||||
require('@angular-devkit/build-angular/plugins/karma')
|
||||
],
|
||||
client:{
|
||||
clearContext: false // leave Jasmine Spec Runner output visible in browser
|
||||
},
|
||||
coverageIstanbulReporter: {
|
||||
reports: [ 'html', 'lcovonly' ],
|
||||
dir: require('path').join(__dirname, 'coverage'), reports: [ 'html', 'lcovonly' ],
|
||||
fixWebpackSourcePaths: true
|
||||
},
|
||||
angularCli: {
|
||||
environment: 'dev'
|
||||
},
|
||||
|
||||
reporters: ['progress', 'kjhtml'],
|
||||
port: 9876,
|
||||
colors: true,
|
||||
|
||||
46
package.json
46
package.json
@@ -12,40 +12,48 @@
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular/animations": "^5.2.0",
|
||||
"@angular/cdk": "^5.0.4",
|
||||
"@angular/common": "^5.2.0",
|
||||
"@angular/compiler": "^5.2.0",
|
||||
"@angular/core": "^5.2.0",
|
||||
"@angular/forms": "^5.0.0",
|
||||
"@angular/http": "^5.0.0",
|
||||
"@angular/material": "^5.0.4",
|
||||
"@angular/platform-browser": "^5.0.0",
|
||||
"@angular/platform-browser-dynamic": "^5.0.0",
|
||||
"@angular/router": "^5.0.0",
|
||||
"@angular-devkit/core": "^8.3.12",
|
||||
"@angular/animations": "^8.2.11",
|
||||
"@angular/cdk": "^8.2.3",
|
||||
"@angular/common": "^8.2.11",
|
||||
"@angular/compiler": "^8.2.11",
|
||||
"@angular/core": "^8.2.11",
|
||||
"@angular/forms": "^8.2.11",
|
||||
"@angular/http": "^7.2.15",
|
||||
"@angular/material": "^8.2.3",
|
||||
"@angular/platform-browser": "^8.2.11",
|
||||
"@angular/platform-browser-dynamic": "^8.2.11",
|
||||
"@angular/router": "^8.2.11",
|
||||
"core-js": "^2.4.1",
|
||||
"file-saver": "^2.0.2",
|
||||
"ng4-configure": "^0.1.7",
|
||||
"rxjs": "^5.5.3",
|
||||
"zone.js": "^0.8.4"
|
||||
"rxjs": "^6.5.3",
|
||||
"rxjs-compat": "^6.0.0-rc.0",
|
||||
"tslib": "^1.10.0",
|
||||
"videogular2": "^7.0.1",
|
||||
"zone.js": "~0.9.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular/cli": "1.5.5",
|
||||
"@angular/compiler-cli": "^5.0.0",
|
||||
"@angular/language-service": "^4.0.0",
|
||||
"@angular-devkit/build-angular": "^0.803.24",
|
||||
"@angular/cli": "^8.3.12",
|
||||
"@angular/compiler-cli": "^8.2.11",
|
||||
"@angular/language-service": "^8.2.11",
|
||||
"@types/core-js": "^2.5.2",
|
||||
"@types/file-saver": "^2.0.1",
|
||||
"@types/jasmine": "2.5.45",
|
||||
"@types/node": "~6.0.60",
|
||||
"codelyzer": "~3.0.1",
|
||||
"codelyzer": "^5.0.1",
|
||||
"jasmine-core": "~2.6.2",
|
||||
"jasmine-spec-reporter": "~4.1.0",
|
||||
"karma": "~1.7.0",
|
||||
"karma-chrome-launcher": "~2.1.1",
|
||||
"karma-cli": "~1.0.1",
|
||||
"karma-coverage-istanbul-reporter": "^1.2.1",
|
||||
"karma-jasmine": "~1.1.0",
|
||||
"karma-jasmine-html-reporter": "^0.2.2",
|
||||
"karma-coverage-istanbul-reporter": "^1.2.1",
|
||||
"protractor": "~5.1.2",
|
||||
"ts-node": "~3.0.4",
|
||||
"tslint": "~5.3.2",
|
||||
"typescript": "~2.5.3"
|
||||
"typescript": "~3.5.3"
|
||||
}
|
||||
}
|
||||
|
||||
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,41 +1,13 @@
|
||||
.demo-card {
|
||||
margin: 16px;
|
||||
}
|
||||
|
||||
.demo-basic {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.demo-basic .mat-card-content {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
mat-toolbar.top {
|
||||
height: 60px;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/*::ng-deep .mat-form-field-placeholder{
|
||||
|
||||
transform: scale(.75) translateY(20px) !important;
|
||||
}*/
|
||||
|
||||
.big {
|
||||
width: 60%;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.centered {
|
||||
margin: 0 auto;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
}
|
||||
|
||||
.example-full-width {
|
||||
.flex-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
mat-form-field.mat-form-field {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.flex-column {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-basis: 100%;
|
||||
flex: 1;
|
||||
}
|
||||
@@ -1,96 +1,15 @@
|
||||
<mat-toolbar color="primary" class="top">
|
||||
<table width="100%" height="100%">
|
||||
<td class="topbar" style="text-align: left; left:0px; font-size: 15px">
|
||||
</td>
|
||||
<td class="topbar" style="text-align: center">
|
||||
<div style="margin-top: 14px">{{topBarTitle}}</div>
|
||||
</td>
|
||||
<td class="topbar" style="text-align: right">
|
||||
<div class="flex-row" width="100%" height="100%">
|
||||
<div class="flex-column" style="text-align: left; margin-top: 1px;">
|
||||
<button (click)="goBack()" *ngIf="router.url.split(';')[0] === '/player'" mat-icon-button><mat-icon>arrow_back</mat-icon></button>
|
||||
</div>
|
||||
<div class="flex-column" style="text-align: center; margin-top: 5px;">
|
||||
<div>{{topBarTitle}}</div>
|
||||
</div>
|
||||
<div class="flex-column" style="text-align: right">
|
||||
|
||||
</td>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</mat-toolbar>
|
||||
|
||||
<br/>
|
||||
|
||||
<mat-card id="card" class="big demo-basic">
|
||||
<mat-card-title>
|
||||
<mat-toolbar color="primary">Youtube Downloader</mat-toolbar>
|
||||
</mat-card-title>
|
||||
<div style="width: 100%; height: 100%; padding-left: 24px; padding-right: 24px; position: relative">
|
||||
<mat-card-content>
|
||||
<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>
|
||||
<button style="float: right; margin-top: -16px" (click)="downloadClicked()" [disabled]="downloadingfile" type="submit" mat-raised-button
|
||||
color="primary">Download</button>
|
||||
</mat-card-content>
|
||||
</div>
|
||||
</mat-card>
|
||||
<br/>
|
||||
<div class="centered big" id="bar_div" *ngIf="downloadingfile;else nofile">
|
||||
<div *ngIf="determinateProgress;else indeterminateprogress">
|
||||
<mat-progress-bar mode="determinate" value="{{percentDownloaded}}"></mat-progress-bar>
|
||||
</div>
|
||||
<ng-template #indeterminateprogress>
|
||||
<mat-progress-bar mode="indeterminate"></mat-progress-bar>
|
||||
</ng-template>
|
||||
<br/>
|
||||
</div>
|
||||
<ng-template #nofile>
|
||||
|
||||
</ng-template>
|
||||
<div *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>
|
||||
|
||||
</mat-expansion-panel>
|
||||
<mat-expansion-panel class="big">
|
||||
<mat-expansion-panel-header>
|
||||
<mat-panel-title>
|
||||
Video
|
||||
</mat-panel-title>
|
||||
<mat-panel-description>
|
||||
Your video files are here
|
||||
</mat-panel-description>
|
||||
</mat-expansion-panel-header>
|
||||
<div *ngIf="mp4s.length > 0;else nomp4s">
|
||||
<mat-grid-list cols="4" rowHeight="150px">
|
||||
<mat-grid-tile *ngFor="let file of mp4s; index as i;">
|
||||
<app-file-card (removeFile)="removeFromMp4($event)" [title]="file.title" [name]="file.id" [thumbnailURL]="file.thumbnailURL"
|
||||
[length]="file.duration" [isAudio]="false"></app-file-card>
|
||||
</mat-grid-tile>
|
||||
</mat-grid-list>
|
||||
</div>
|
||||
</mat-expansion-panel>
|
||||
</mat-accordion>
|
||||
</div>
|
||||
|
||||
<ng-template #nomp3s>
|
||||
|
||||
</ng-template>
|
||||
|
||||
<ng-template #nomp4s>
|
||||
|
||||
</ng-template>
|
||||
<router-outlet></router-outlet>
|
||||
@@ -1,246 +1,80 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
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: './app.component.html',
|
||||
styleUrls: ['./app.component.css']
|
||||
})
|
||||
export class AppComponent {
|
||||
determinateProgress: boolean = false;
|
||||
downloadingfile: boolean = false;
|
||||
export class AppComponent implements OnInit {
|
||||
iOS = false;
|
||||
|
||||
determinateProgress = false;
|
||||
downloadingfile = false;
|
||||
audioOnly: boolean;
|
||||
urlError: boolean = false;
|
||||
path: string = '';
|
||||
url: string = '';
|
||||
exists: string = "";
|
||||
topBarTitle: string = "Youtube Downloader";
|
||||
urlError = false;
|
||||
path = '';
|
||||
url = '';
|
||||
exists = '';
|
||||
topBarTitle = 'Youtube Downloader';
|
||||
percentDownloaded: number;
|
||||
fileManagerEnabled: boolean = false;
|
||||
fileManagerEnabled = false;
|
||||
downloadOnlyMode = false;
|
||||
baseStreamPath;
|
||||
audioFolderPath;
|
||||
videoFolderPath;
|
||||
|
||||
// youtube api
|
||||
youtubeSearchEnabled = false;
|
||||
youtubeAPIKey = null;
|
||||
results_loading = false;
|
||||
results_showing = true;
|
||||
results = [];
|
||||
|
||||
mp3s: any[] = [];
|
||||
mp4s: any[] = [];
|
||||
|
||||
constructor(private postsService: PostsService, public snackBar: MatSnackBar) {
|
||||
this.audioOnly = true;
|
||||
|
||||
|
||||
|
||||
this.postsService.loadNavItems().subscribe(result => { // loads settings
|
||||
var backendUrl = result.YoutubeDLMaterial.Host.backendurl;
|
||||
this.topBarTitle = result.YoutubeDLMaterial.Extra.title_top;
|
||||
this.fileManagerEnabled = result.YoutubeDLMaterial.Extra.file_manager_enabled;
|
||||
|
||||
this.postsService.path = backendUrl;
|
||||
this.postsService.startPath = backendUrl;
|
||||
this.postsService.startPathSSL = backendUrl;
|
||||
|
||||
if (this.fileManagerEnabled)
|
||||
{
|
||||
this.getMp3s();
|
||||
this.getMp4s();
|
||||
}
|
||||
},
|
||||
error => {
|
||||
console.log(error);
|
||||
});
|
||||
|
||||
}
|
||||
files_cols = (window.innerWidth <= 450) ? 2 : 4;
|
||||
|
||||
urlForm = new FormControl('', [Validators.required]);
|
||||
|
||||
doHandshake(url: string) {
|
||||
this.postsService.startHandshake(url).subscribe(theurl => {
|
||||
this.postsService.path = theurl;
|
||||
this.postsService.handShakeComplete = true;
|
||||
console.log("Handshake complete!");
|
||||
},
|
||||
error => {
|
||||
console.log("Initial handshake failed on http.");
|
||||
this.doHandshakeSSL(url);
|
||||
});
|
||||
}
|
||||
@ViewChild('urlinput', { read: ElementRef, static: false }) urlInput: ElementRef;
|
||||
|
||||
doHandshakeSSL(url: string) {
|
||||
this.postsService.startHandshakeSSL(url).subscribe(theurl => {
|
||||
this.postsService.path = theurl;
|
||||
this.postsService.handShakeComplete = true;
|
||||
console.log("Handshake complete!");
|
||||
},
|
||||
error => {
|
||||
console.log("Initial handshake failed on https too! Make sure port 17442 is open.");
|
||||
this.postsService.handShakeComplete = false;
|
||||
});
|
||||
}
|
||||
constructor(private postsService: PostsService, private youtubeSearch: YoutubeSearchService, public snackBar: MatSnackBar,
|
||||
public router: Router) {
|
||||
this.audioOnly = false;
|
||||
|
||||
getMp3s() {
|
||||
this.postsService.getMp3s().subscribe(result => {
|
||||
var mp3s = result;
|
||||
this.mp3s = mp3s;
|
||||
},
|
||||
error => {
|
||||
|
||||
// loading config
|
||||
this.postsService.loadNavItems().subscribe(result => { // loads settings
|
||||
this.topBarTitle = result['YoutubeDLMaterial']['Extra']['title_top'];
|
||||
}, error => {
|
||||
console.log(error);
|
||||
});
|
||||
}
|
||||
|
||||
getMp4s() {
|
||||
this.postsService.getMp4s().subscribe(result => {
|
||||
var mp4s = result;
|
||||
this.mp4s = mp4s;
|
||||
},
|
||||
error => {
|
||||
console.log(error);
|
||||
});
|
||||
}
|
||||
|
||||
public goToFile(name, isAudio)
|
||||
{
|
||||
if (isAudio)
|
||||
{
|
||||
this.downloadHelperMp3(name);
|
||||
}
|
||||
else
|
||||
{
|
||||
this.downloadHelperMp4(name);
|
||||
}
|
||||
}
|
||||
|
||||
public removeFromMp3(name: string)
|
||||
{
|
||||
for (var i = 0; i < this.mp3s.length; i++)
|
||||
{
|
||||
if (this.mp3s[i].id == name)
|
||||
{
|
||||
this.mp3s.splice(i,1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public removeFromMp4(name: string)
|
||||
{
|
||||
console.log(name);
|
||||
console.log(this.mp4s);
|
||||
for (var i = 0; i < this.mp4s.length; i++)
|
||||
{
|
||||
if (this.mp4s[i].id == name)
|
||||
{
|
||||
this.mp4s.splice(i,1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
}
|
||||
|
||||
downloadHelperMp3(name: string)
|
||||
{
|
||||
this.postsService.getFileStatusMp3(name).subscribe(fileExists => {
|
||||
var exists = fileExists;
|
||||
this.exists = exists[0];
|
||||
if (exists[0] == "failed")
|
||||
{
|
||||
var 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 => {
|
||||
var exists = fileExists;
|
||||
this.exists = exists[0];
|
||||
if (exists[0] == "failed")
|
||||
{
|
||||
var 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;
|
||||
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;
|
||||
if (this.path != "-1")
|
||||
{
|
||||
this.downloadHelperMp4(this.path);
|
||||
}
|
||||
},
|
||||
error => { // can't access server
|
||||
this.downloadingfile = false;
|
||||
this.openSnackBar("Download failed!", "OK.");
|
||||
});
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
this.urlError = true;
|
||||
}
|
||||
}
|
||||
|
||||
ValidURL(str) {
|
||||
var strRegex = /((([A-Za-z]{3,9}:(?:\/\/)?)(?:[-;:&=\+\$,\w]+@)?[A-Za-z0-9.-]+|(?:www.|[-;:&=\+\$,\w]+@)[A-Za-z0-9.-]+)((?:\/[\+~%\/.\w-_]*)?\??(?:[-\+=&;%@.\w_]*)#?(?:[\w]*))?)/;
|
||||
var re=new RegExp(strRegex);
|
||||
return re.test(str);
|
||||
}
|
||||
|
||||
public openSnackBar(message: string, action: string) {
|
||||
this.snackBar.open(message, action, {
|
||||
duration: 2000,
|
||||
});
|
||||
goBack() {
|
||||
this.router.navigate(['/home']);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,9 @@ import { NgModule } from '@angular/core';
|
||||
import {MatNativeDateModule, MatRadioModule, MatInputModule, MatButtonModule, MatSidenavModule, MatIconModule, MatListModule,
|
||||
MatSnackBarModule, MatCardModule, MatSelectModule, MatToolbarModule, MatCheckboxModule, MatGridListModule,
|
||||
MatProgressBarModule, MatExpansionModule,
|
||||
MatGridList} from '@angular/material';
|
||||
MatGridList,
|
||||
MatProgressSpinnerModule,
|
||||
MatButtonToggleModule} from '@angular/material';
|
||||
import {FormsModule, ReactiveFormsModule} from '@angular/forms';
|
||||
import { AppComponent } from './app.component';
|
||||
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
|
||||
@@ -13,11 +15,20 @@ import { PostsService } from 'app/posts.services';
|
||||
import {APP_BASE_HREF} from '@angular/common';
|
||||
import { FileCardComponent } from './file-card/file-card.component';
|
||||
import {RouterModule} from '@angular/router';
|
||||
import { AppRoutingModule } from './app-routing.module';
|
||||
import { MainComponent } from './main/main.component';
|
||||
import { PlayerComponent } from './player/player.component';
|
||||
import {VgCoreModule} from 'videogular2/compiled/core';
|
||||
import {VgControlsModule} from 'videogular2/compiled/controls';
|
||||
import {VgOverlayPlayModule} from 'videogular2/compiled/overlay-play';
|
||||
import {VgBufferingModule} from 'videogular2/compiled/buffering';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AppComponent,
|
||||
FileCardComponent
|
||||
FileCardComponent,
|
||||
MainComponent,
|
||||
PlayerComponent
|
||||
],
|
||||
imports: [
|
||||
BrowserModule,
|
||||
@@ -41,7 +52,14 @@ import {RouterModule} from '@angular/router';
|
||||
MatGridListModule,
|
||||
MatExpansionModule,
|
||||
MatProgressBarModule,
|
||||
RouterModule
|
||||
MatProgressSpinnerModule,
|
||||
MatButtonToggleModule,
|
||||
VgCoreModule,
|
||||
VgControlsModule,
|
||||
VgOverlayPlayModule,
|
||||
VgBufferingModule,
|
||||
RouterModule,
|
||||
AppRoutingModule
|
||||
],
|
||||
providers: [PostsService],
|
||||
bootstrap: [AppComponent]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<mat-card class="example-card">
|
||||
<button (click)="deleteFile()" class="deleteButton" mat-icon-button><mat-icon>delete_forever</mat-icon></button>
|
||||
<div style="padding:5px">
|
||||
<b><a href="javascript:void(0)" (click)="appComponent.goToFile(name, isAudio)">{{title}}</a></b>
|
||||
<b><a href="javascript:void(0)" (click)="mainComponent.goToFile(name, isAudio)">{{title}}</a></b>
|
||||
<br/>
|
||||
ID: {{name}}
|
||||
</div>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Component, OnInit, Input, Output } from '@angular/core';
|
||||
import {PostsService} from '../posts.services';
|
||||
import {MatSnackBar} from '@angular/material';
|
||||
import {AppComponent} from '../app.component';
|
||||
import {EventEmitter} from '@angular/core';
|
||||
import { MainComponent } from 'app/main/main.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-file-card',
|
||||
@@ -11,29 +11,25 @@ import {EventEmitter} from '@angular/core';
|
||||
})
|
||||
export class FileCardComponent implements OnInit {
|
||||
|
||||
@Input() title:string;
|
||||
@Input() length:string;
|
||||
@Input() name:string;
|
||||
@Input() title: string;
|
||||
@Input() length: string;
|
||||
@Input() name: string;
|
||||
@Input() thumbnailURL: string;
|
||||
@Input() isAudio: boolean = true;
|
||||
@Input() isAudio = true;
|
||||
@Output() removeFile: EventEmitter<string> = new EventEmitter<string>();
|
||||
|
||||
constructor(private postsService: PostsService, public snackBar: MatSnackBar, public appComponent: AppComponent) { }
|
||||
constructor(private postsService: PostsService, public snackBar: MatSnackBar, public mainComponent: MainComponent) { }
|
||||
|
||||
ngOnInit() {
|
||||
}
|
||||
|
||||
deleteFile()
|
||||
{
|
||||
deleteFile() {
|
||||
this.postsService.deleteFile(this.name, this.isAudio).subscribe(result => {
|
||||
if (result == true)
|
||||
{
|
||||
this.openSnackBar("Delete success!", "OK.");
|
||||
if (result === true) {
|
||||
this.openSnackBar('Delete success!', 'OK.');
|
||||
this.removeFile.emit(this.name);
|
||||
}
|
||||
else
|
||||
{
|
||||
this.openSnackBar("Delete failed!", "OK.");
|
||||
} else {
|
||||
this.openSnackBar('Delete failed!', 'OK.');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
84
src/app/main/main.component.css
Normal file
84
src/app/main/main.component.css
Normal file
@@ -0,0 +1,84 @@
|
||||
.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;
|
||||
}
|
||||
130
src/app/main/main.component.html
Normal file
130
src/app/main/main.component.html
Normal file
@@ -0,0 +1,130 @@
|
||||
<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 class="col-12 col-sm-9">
|
||||
<mat-form-field 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 class="col-7 col-sm-3">
|
||||
<mat-form-field 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>
|
||||
<span *ngIf="results_showing">
|
||||
<span *ngFor="let result of results">
|
||||
<mat-card style="height: 120px; border-radius: 0px">
|
||||
<div class="search-card-title">
|
||||
{{result.title}}
|
||||
</div>
|
||||
<div style="font-size: 12px">
|
||||
{{result.uploaded}}
|
||||
</div>
|
||||
<br/>
|
||||
<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>
|
||||
</span>
|
||||
</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="primary">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 (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-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 (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-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();
|
||||
});
|
||||
});
|
||||
564
src/app/main/main.component.ts
Normal file
564
src/app/main/main.component.ts
Normal file
@@ -0,0 +1,564 @@
|
||||
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;
|
||||
fileManagerEnabled = 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;
|
||||
|
||||
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.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'];
|
||||
this.mp3s = mp3s;
|
||||
}, error => {
|
||||
console.log(error);
|
||||
});
|
||||
}
|
||||
|
||||
getMp4s() {
|
||||
this.postsService.getMp4s().subscribe(result => {
|
||||
const mp4s = result['mp4s'];
|
||||
this.mp4s = mp4s;
|
||||
},
|
||||
error => {
|
||||
console.log(error);
|
||||
});
|
||||
}
|
||||
|
||||
public goToFile(name, isAudio) {
|
||||
if (isAudio) {
|
||||
this.downloadHelperMp3(name, false, true);
|
||||
} else {
|
||||
this.downloadHelperMp4(name, false, true);
|
||||
}
|
||||
}
|
||||
|
||||
public removeFromMp3(name: string) {
|
||||
for (let i = 0; i < this.mp3s.length; i++) {
|
||||
if (this.mp3s[i].id === name) {
|
||||
this.mp3s.splice(i, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public removeFromMp4(name: string) {
|
||||
// console.log(name);
|
||||
// console.log(this.mp4s);
|
||||
for (let i = 0; i < this.mp4s.length; i++) {
|
||||
if (this.mp4s[i].id === name) {
|
||||
this.mp4s.splice(i, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// app initialization.
|
||||
ngOnInit() {
|
||||
this.iOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window['MSStream'];
|
||||
}
|
||||
|
||||
// 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) {
|
||||
for (let i = 0; i < name.length; i++) {
|
||||
this.downloadAudioFile(decodeURI(name[i]));
|
||||
}
|
||||
} 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) {
|
||||
for (let i = 0; i < name.length; i++) {
|
||||
this.downloadVideoFile(decodeURI(name[i]));
|
||||
}
|
||||
} 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 !== ''/* &&
|
||||
video_formats[this.selectedQuality]['acodec'] === 'none'*/) {
|
||||
console.log(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.postsService.downloadFileFromServer(name, 'audio').subscribe(res => {
|
||||
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.postsService.downloadFileFromServer(name, 'video').subscribe(res => {
|
||||
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();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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 === '') {
|
||||
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) {
|
||||
// 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) {
|
||||
console.log(this.cachedAvailableFormats[url]);
|
||||
if (!(this.cachedAvailableFormats[url])) {
|
||||
this.formats_loading = true;
|
||||
console.log('has no cached formats available');
|
||||
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);
|
||||
console.log('got formats for ' + url);
|
||||
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[]) => {
|
||||
// console.log(results);
|
||||
this.results_loading = false;
|
||||
if (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 = '';
|
||||
}
|
||||
|
||||
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;
|
||||
// console.log(format);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
25
src/app/player/player.component.css
Normal file
25
src/app/player/player.component.css
Normal file
@@ -0,0 +1,25 @@
|
||||
.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;
|
||||
}
|
||||
17
src/app/player/player.component.html
Normal file
17
src/app/player/player.component.html
Normal file
@@ -0,0 +1,17 @@
|
||||
<div *ngIf="playlist.length > 0; else loading">
|
||||
<div [ngClass]="(type === 'audio') ? null : 'container-video'" class="container">
|
||||
<div class="row">
|
||||
<div [ngClass]="(type === 'audio') ? 'my-2 px-1' : null" class="col px-1">
|
||||
<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>
|
||||
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();
|
||||
});
|
||||
});
|
||||
121
src/app/player/player.component.ts
Normal file
121
src/app/player/player.component.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
import { Component, OnInit, HostListener } from '@angular/core';
|
||||
import { VgAPI } from 'videogular2/compiled/core';
|
||||
import { PostsService } from 'app/posts.services';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
|
||||
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;
|
||||
|
||||
@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');
|
||||
|
||||
// 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'];
|
||||
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
|
||||
}
|
||||
console.log(mediaObject);
|
||||
this.playlist.push(mediaObject);
|
||||
}
|
||||
this.currentItem = this.playlist[this.currentIndex];
|
||||
});
|
||||
|
||||
// this.getFileInfos();
|
||||
|
||||
}
|
||||
|
||||
constructor(private postsService: PostsService, private route: ActivatedRoute) {
|
||||
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import {Injectable} from '@angular/core';
|
||||
import {Http} from '@angular/http';
|
||||
import {Injectable, isDevMode} from '@angular/core';
|
||||
import { HttpClient, HttpHeaders, HttpRequest, HttpResponseBase } from '@angular/common/http';
|
||||
import config from '../assets/default.json';
|
||||
import 'rxjs/add/operator/map';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import 'rxjs/add/operator/map';
|
||||
@@ -8,93 +9,83 @@ import 'rxjs/add/observable/throw';
|
||||
|
||||
@Injectable()
|
||||
export class PostsService {
|
||||
path: string = "";
|
||||
audioFolder: string = "";
|
||||
videoFolder: string = "";
|
||||
startPath: string = "http://localhost:17442/";
|
||||
startPathSSL: string = "https://localhost:17442/"
|
||||
handShakeComplete: boolean = false;
|
||||
path = '';
|
||||
audioFolder = '';
|
||||
videoFolder = '';
|
||||
startPath = 'http://localhost:17442/';
|
||||
startPathSSL = 'https://localhost:17442/'
|
||||
handShakeComplete = false;
|
||||
|
||||
constructor(private http: Http){
|
||||
constructor(private http: HttpClient) {
|
||||
console.log('PostsService Initialized...');
|
||||
}
|
||||
|
||||
startHandshake(url: string): Observable<string>
|
||||
{
|
||||
return this.http.get(url + "geturl")
|
||||
.map(res => res.json());
|
||||
startHandshake(url: string) {
|
||||
return this.http.get(url + 'geturl');
|
||||
}
|
||||
|
||||
startHandshakeSSL(url: string): Observable<string>
|
||||
{
|
||||
return this.http.get(url + "geturl")
|
||||
.map(res => res.json());
|
||||
startHandshakeSSL(url: string) {
|
||||
return this.http.get(url + 'geturl');
|
||||
}
|
||||
|
||||
getVideoFolder(): Observable<string>
|
||||
{
|
||||
return this.http.get(this.startPath + "videofolder")
|
||||
.map(res => res.json());
|
||||
getVideoFolder() {
|
||||
return this.http.get(this.startPath + 'videofolder');
|
||||
}
|
||||
|
||||
getAudioFolder(): Observable<string>
|
||||
{
|
||||
return this.http.get(this.startPath + "audiofolder")
|
||||
.map(res => res.json());
|
||||
getAudioFolder() {
|
||||
return this.http.get(this.startPath + 'audiofolder');
|
||||
}
|
||||
|
||||
makeMP3(url: string): Observable<string>
|
||||
{
|
||||
return this.http.post(this.path + "tomp3",{url: url})
|
||||
.map(res => res.json());
|
||||
makeMP3(url: string, selectedQuality: string, customQualityConfiguration: string) {
|
||||
return this.http.post(this.path + 'tomp3', {url: url,
|
||||
maxBitrate: selectedQuality,
|
||||
customQualityConfiguration: customQualityConfiguration});
|
||||
}
|
||||
|
||||
makeMP4(url: string): Observable<string>
|
||||
{
|
||||
return this.http.post(this.path + "tomp4",{url: url})
|
||||
.map(res => res.json());
|
||||
makeMP4(url: string, selectedQuality: string, customQualityConfiguration: string) {
|
||||
return this.http.post(this.path + 'tomp4', {url: url,
|
||||
selectedHeight: selectedQuality,
|
||||
customQualityConfiguration: customQualityConfiguration});
|
||||
}
|
||||
|
||||
getFileStatusMp3(name: string): Observable<any> {
|
||||
return this.http.post(this.path + "fileStatusMp3",{name: name})
|
||||
.map(res => res.json());
|
||||
getFileStatusMp3(name: string) {
|
||||
return this.http.post(this.path + 'fileStatusMp3', {name: name});
|
||||
}
|
||||
|
||||
getFileStatusMp4(name: string): Observable<any> {
|
||||
return this.http.post(this.path + "fileStatusMp4",{name: name})
|
||||
.map(res => res.json());
|
||||
getFileStatusMp4(name: string) {
|
||||
return this.http.post(this.path + 'fileStatusMp4', {name: name});
|
||||
}
|
||||
|
||||
loadNavItems() {
|
||||
console.log("Config location: " + window.location.href + "backend/config/default.json");
|
||||
return this.http.get(window.location.href + "backend/config/default.json")
|
||||
.map(res => res.json());
|
||||
if (isDevMode()) {
|
||||
return this.http.get('./assets/default.json');
|
||||
}
|
||||
console.log('Config location: ' + window.location.href + 'backend/config/default.json');
|
||||
return this.http.get(window.location.href + 'backend/config/default.json');
|
||||
}
|
||||
|
||||
deleteFile(name: string, isAudio: boolean)
|
||||
{
|
||||
if (isAudio)
|
||||
{
|
||||
return this.http.post(this.path + "deleteMp3",{name: name})
|
||||
.map(res => res.json());
|
||||
}
|
||||
else
|
||||
{
|
||||
return this.http.post(this.path + "deleteMp4",{name: name})
|
||||
.map(res => res.json());
|
||||
deleteFile(name: string, isAudio: boolean) {
|
||||
if (isAudio) {
|
||||
return this.http.post(this.path + 'deleteMp3', {name: name});
|
||||
} else {
|
||||
return this.http.post(this.path + 'deleteMp4', {name: name});
|
||||
}
|
||||
}
|
||||
|
||||
getMp3s()
|
||||
{
|
||||
return this.http.post(this.path + "getMp3s", {})
|
||||
.map(res => res.json());
|
||||
getMp3s() {
|
||||
return this.http.post(this.path + 'getMp3s', {});
|
||||
}
|
||||
|
||||
getMp4s()
|
||||
{
|
||||
return this.http.post(this.path + "getMp4s", {})
|
||||
.map(res => res.json());
|
||||
getMp4s() {
|
||||
return this.http.post(this.path + 'getMp4s', {});
|
||||
}
|
||||
|
||||
downloadFileFromServer(fileName, type) {
|
||||
return this.http.post(this.path + 'downloadFile', {fileName: fileName, type: type}, {responseType: 'blob'});
|
||||
}
|
||||
|
||||
getFileInfo(fileNames, type, urlMode) {
|
||||
return this.http.post(this.path + 'getVideoInfos', {fileNames: fileNames, type: type, urlMode: urlMode});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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;
|
||||
}
|
||||
27
src/assets/default.json
Normal file
27
src/assets/default.json
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"YoutubeDLMaterial": {
|
||||
"Host": {
|
||||
"frontendurl": "http://localhost:4200",
|
||||
"backendurl": "http://localhost:17442/"
|
||||
},
|
||||
"Encryption": {
|
||||
"use-encryption": false,
|
||||
"cert-file-path": "/etc/letsencrypt/live/example.com/fullchain.pem",
|
||||
"key-file-path": "/etc/letsencrypt/live/example.com/privkey.pem"
|
||||
},
|
||||
"Downloader": {
|
||||
"path-base": "http://localhost:17442/",
|
||||
"path-audio": "audio/",
|
||||
"path-video": "video/"
|
||||
},
|
||||
"Extra": {
|
||||
"title_top": "Youtube Downloader",
|
||||
"file_manager_enabled": true,
|
||||
"download_only_mode": false
|
||||
},
|
||||
"API": {
|
||||
"use_youtube_API": false,
|
||||
"youtube_API_key": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
<link href="https://fonts.googleapis.com/css?family=Roboto" rel="stylesheet">
|
||||
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
||||
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css">
|
||||
<link 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">
|
||||
</head>
|
||||
|
||||
@@ -43,7 +43,6 @@
|
||||
|
||||
/** Evergreen browsers require these. **/
|
||||
import 'core-js/es6/reflect';
|
||||
import 'core-js/es7/reflect';
|
||||
|
||||
|
||||
/** ALL Firefox browsers require the following to support `@angular/animation`. **/
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
"extends": "../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../out-tsc/app",
|
||||
"module": "es2015",
|
||||
"baseUrl": "",
|
||||
"types": []
|
||||
},
|
||||
|
||||
@@ -1,20 +1,19 @@
|
||||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../out-tsc/spec",
|
||||
"module": "commonjs",
|
||||
"target": "es5",
|
||||
"baseUrl": "",
|
||||
"types": [
|
||||
"jasmine",
|
||||
"node"
|
||||
]
|
||||
},
|
||||
"files": [
|
||||
"test.ts"
|
||||
],
|
||||
"include": [
|
||||
"**/*.spec.ts",
|
||||
"**/*.d.ts"
|
||||
]
|
||||
}
|
||||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../out-tsc/spec",
|
||||
"baseUrl": "",
|
||||
"types": [
|
||||
"jasmine",
|
||||
"node"
|
||||
]
|
||||
},
|
||||
"files": [
|
||||
"test.ts",
|
||||
"polyfills.ts"
|
||||
|
||||
],
|
||||
"include": [
|
||||
"**/*.spec.ts",
|
||||
"**/*.d.ts"
|
||||
]
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
{
|
||||
"compileOnSave": false,
|
||||
"compilerOptions": {
|
||||
"downlevelIteration": true,
|
||||
"importHelpers": true,
|
||||
"outDir": "./dist/out-tsc",
|
||||
"baseUrl": "src",
|
||||
"sourceMap": true,
|
||||
@@ -8,13 +10,16 @@
|
||||
"moduleResolution": "node",
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"target": "es5",
|
||||
"target": "es2015",
|
||||
"resolveJsonModule": true,
|
||||
"esModuleInterop": true,
|
||||
"typeRoots": [
|
||||
"node_modules/@types"
|
||||
],
|
||||
"lib": [
|
||||
"es2016",
|
||||
"dom"
|
||||
]
|
||||
],
|
||||
"module": "esnext"
|
||||
}
|
||||
}
|
||||
}
|
||||
11
tslint.json
11
tslint.json
@@ -14,8 +14,7 @@
|
||||
"eofline": true,
|
||||
"forin": true,
|
||||
"import-blacklist": [
|
||||
true,
|
||||
"rxjs"
|
||||
true
|
||||
],
|
||||
"import-spacing": true,
|
||||
"indent": [
|
||||
@@ -119,12 +118,12 @@
|
||||
"app",
|
||||
"kebab-case"
|
||||
],
|
||||
"use-input-property-decorator": true,
|
||||
"use-output-property-decorator": true,
|
||||
"use-host-property-decorator": true,
|
||||
"no-inputs-metadata-property": true,
|
||||
"no-outputs-metadata-property": true,
|
||||
"no-host-metadata-property": true,
|
||||
"no-input-rename": true,
|
||||
"no-output-rename": true,
|
||||
"use-life-cycle-interface": true,
|
||||
"use-lifecycle-interface": true,
|
||||
"use-pipe-transform-interface": true,
|
||||
"component-class-suffix": true,
|
||||
"directive-class-suffix": true,
|
||||
|
||||
Reference in New Issue
Block a user