mirror of
https://github.com/Tzahi12345/YoutubeDL-Material.git
synced 2026-03-07 20:10:03 +03:00
Compare commits
58 Commits
v3.0-alpha
...
v3.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
12c227badb | ||
|
|
181a9f842c | ||
|
|
b79d801c0f | ||
|
|
fc3691336d | ||
|
|
bcd879ebc8 | ||
|
|
b646db4828 | ||
|
|
426d52e359 | ||
|
|
17199dd9c0 | ||
|
|
c680c2827b | ||
|
|
2dbf8d31f7 | ||
|
|
a3753e557c | ||
|
|
ec80abdc8e | ||
|
|
1ef7d24c22 | ||
|
|
4b6f6996ae | ||
|
|
c930ee94c5 | ||
|
|
e88edbef5a | ||
|
|
ac13ed3359 | ||
|
|
faae0d44e6 | ||
|
|
7d8ec04ad6 | ||
|
|
8629e6ae9e | ||
|
|
6e311d46a6 | ||
|
|
006e983c14 | ||
|
|
5db3e06a81 | ||
|
|
2ced7b7f91 | ||
|
|
042baa418b | ||
|
|
deb928da12 | ||
|
|
a7f5cc01d3 | ||
|
|
414b6a26d9 | ||
|
|
c069672e62 | ||
|
|
167d9dafa2 | ||
|
|
9302084f60 | ||
|
|
ac0199f596 | ||
|
|
8e8ab7ac6c | ||
|
|
f06c9ba44a | ||
|
|
6e593472d9 | ||
|
|
0bddbda36d | ||
|
|
23feb05fab | ||
|
|
a0eff4d96d | ||
|
|
b87b49d77b | ||
|
|
c05026aa15 | ||
|
|
883df63d2f | ||
|
|
da1d49b541 | ||
|
|
393ed5a210 | ||
|
|
54492b109a | ||
|
|
7eac88a31f | ||
|
|
8fec9639eb | ||
|
|
a15e1f98fa | ||
|
|
6604484765 | ||
|
|
c58f8a4058 | ||
|
|
8545016f1d | ||
|
|
9b1e84821e | ||
|
|
6505fad7bc | ||
|
|
d245904c0d | ||
|
|
0095ea1271 | ||
|
|
b41d10f514 | ||
|
|
8e3d6a0af6 | ||
|
|
1e4995c5ce | ||
|
|
710e3613a8 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -46,4 +46,5 @@ backend/node_modules/*
|
||||
YoutubeDL-Material/node_modules/*
|
||||
backend/video/*
|
||||
backend/audio/*
|
||||
backend/db.json
|
||||
src/assets/default.json
|
||||
|
||||
28
Dockerfile
Normal file
28
Dockerfile
Normal file
@@ -0,0 +1,28 @@
|
||||
FROM ubuntu:18.04
|
||||
|
||||
RUN apt-get update && apt-get install -y \
|
||||
nodejs \
|
||||
apache2 \
|
||||
npm \
|
||||
youtube-dl
|
||||
|
||||
# Change directory so that our commands run inside this new directory
|
||||
WORKDIR /var/www/html
|
||||
|
||||
# Copy dependency definitions
|
||||
COPY ./ /var/www/html/
|
||||
|
||||
# Change directory to backend
|
||||
WORKDIR /var/www/html/backend
|
||||
|
||||
# Install dependencies on backend
|
||||
RUN npm install
|
||||
|
||||
# Change back to original directory
|
||||
WORKDIR /var/www/html
|
||||
|
||||
# Expose the port the app runs in
|
||||
EXPOSE 80
|
||||
|
||||
# Run the specified command within the container.
|
||||
CMD ./docker_wrapper.sh
|
||||
53
README.md
53
README.md
@@ -1,6 +1,8 @@
|
||||
# YoutubeDL-Material
|
||||
|
||||
YoutubeDL-Material is a material design frontend for [youtube-dl](https://rg3.github.io/youtube-dl/). It's coded using [Angular 5](https://angular.io/) for the frontend, and [Nodejs](https://nodejs.org/) on the backend.
|
||||
YoutubeDL-Material is a Material Design frontend for [youtube-dl](https://rg3.github.io/youtube-dl/). It's coded using [Angular 8](https://angular.io/) for the frontend, and [Node.js](https://nodejs.org/) on the backend.
|
||||
|
||||
Now with [Docker](#Docker) support!
|
||||
|
||||
## Getting Started
|
||||
|
||||
@@ -8,18 +10,24 @@ Check out the prerequisites, and go to the installation section. Easy as pie!
|
||||
|
||||
Here's an image of what it'll look like once you're done:
|
||||
|
||||

|
||||

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

|
||||

|
||||
|
||||
Dark mode:
|
||||
|
||||

|
||||
|
||||
### 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:
|
||||
NOTE: If you would like to use Docker, you can go down to the [Docker](#Docker) section for a setup guide.
|
||||
|
||||
You need to have a functioning web server for this to work. Also make sure you have these dependencies installed on your system: nodejs and youtube-dl. If you don't, run this command:
|
||||
|
||||
```
|
||||
sudo apt-get install ffmpeg nodejs python
|
||||
sudo apt-get install nodejs youtube-dl
|
||||
```
|
||||
|
||||
### Installing
|
||||
@@ -30,10 +38,33 @@ Drag all the files in `youtubedl-material` to a location accessible to a web ser
|
||||
|
||||
Port forward `17442` if you're going to access YoutubeDL-Material from out of your network. This is an important step. Make sure the configuration reflects this appropriately.
|
||||
|
||||
Once the configuration is done, type `sudo nodejs app.js`. This will run the backend server. On your browser, navigate to your installation folder. Try putting in a youtube link to see if it works. If it does, viola! YoutubeDL-Material is now up and running.
|
||||
Once the configuration is done, run `npm install` to install all the backend dependencies. Once that is finished, type `nodejs app.js`. This will run the backend server. On your browser, navigate to your installation folder. Try putting in a youtube link to see if it works. If it does, viola! YoutubeDL-Material is now up and running.
|
||||
|
||||
If you experience problems, know that it's usually caused by a configuration problem. The first thing you should do is check the console. To get there, right click anywhere on the page and click "Inspect element." Then on the menu that pops up, click console. Look at the error there, and try to investigate.
|
||||
|
||||
### Configuration
|
||||
|
||||
Here is an explanation for the configuration entries. Check out the [default config](https://github.com/Tzahi12345/YoutubeDL-Material/blob/master/backend/config/default.json) for more context.
|
||||
|
||||
| Config item | Description | Default |
|
||||
| ------------- | ------------- | ------------- |
|
||||
| frontendurl | URL to the webserver hosting YTDL-Material | "http://example.com" |
|
||||
| backendurl | URL to the YTDL-Material's backend, should include port 17442 | "http://example.com:17442/" |
|
||||
| use-encryption | true if you intend to use SSL encryption (https) | false |
|
||||
| cert-file-path | Cert file path - required if using encryption | "/etc/letsencrypt/live/example.com/fullchain.pem" |
|
||||
| key-file-path | Private key file path - required if using encryption | "/etc/letsencrypt/live/example.com/privkey.pem" |
|
||||
| path-base | Audio/video stream URL. Usually the same as backendurl | "http://example.com:17442/" |
|
||||
| path-audio | Path to audio folder for saved mp3s | "audio/" |
|
||||
| path-video | Path to video folder for saved mp4s | "video/" |
|
||||
| title_top | Title shown on the top toolbar | "Youtube Downloader" |
|
||||
| file_manager_enabled | true if you want to use the file manager | true |
|
||||
| allow_quality_select | true if you want to select a videos quality level before downloading | true |
|
||||
| download_only_mode | true if you want files to directly download to the client with no media player | false |
|
||||
| use_youtube_API | true if you want to use the Youtube API which is used for YT searches | false |
|
||||
| youtube_API_key | Youtube API key. Required if use_youtube_API is enabled | "" |
|
||||
| default_theme | Default theme to use. Options are "default" and "dark" | "default" |
|
||||
| allow_theme_change | true if you want the icon in the top toolbar that toggles dark mode | true |
|
||||
|
||||
## Deployment
|
||||
|
||||
If you'd like to install YoutubeDL-Material, go to the Installation section. If you want to build it yourself and/or develop the repository, then this section is for you.
|
||||
@@ -46,6 +77,16 @@ The frontend is now complete. The backend is much easier. Just go into the `yout
|
||||
|
||||
Finally, port forward the port `17442` and point it to the server's IP address. Make sure the port is also allowed through the firewall.
|
||||
|
||||
## Docker
|
||||
|
||||
If you are looking to setup YoutubeDL-Material with Docker, this section is for you. And you're in luck! Docker setup is quite simple.
|
||||
|
||||
1. Run `curl https://github.com/Tzahi12345/YoutubeDL-Material/releases/latest/download/docker-compose.yml -o docker-compose.yml` to download the latest release of `docker-compose.yml`, or go to the [releases](https://github.com/Tzahi12345/YoutubeDL-Material/releases/) page to grab the version you'd like.
|
||||
2. Modify the config items in the `environment` section of `docker-compose.yml` to your liking. Otherwise, the default options will work and point to `http://localhost:8998`. You can find an explanation of these configuration items in [Configuration](#Configuration) section.
|
||||
3. Make sure the port in the `frontend_url` environment variable lines up with the port in the `ports` section.
|
||||
4. Run `docker-compose pull`. This will download the official YoutubeDL-Material docker image.
|
||||
5. Run `docker-compose up` to start it up. If successful, it should say "HTTP(S): Started on port 17442" or something similar. Make sure you can connect to the frontend, and if so, you are done!
|
||||
|
||||
## Contributing
|
||||
|
||||
Feel free to submit a pull request! I have no guidelines as of yet, so no need to worry about that.
|
||||
|
||||
59
angular.json
59
angular.json
@@ -24,8 +24,7 @@
|
||||
"src/backend"
|
||||
],
|
||||
"styles": [
|
||||
"src/styles.css",
|
||||
"../node_modules/videogular2/fonts/videogular.css"
|
||||
"src/styles.scss"
|
||||
],
|
||||
"scripts": []
|
||||
},
|
||||
@@ -66,6 +65,57 @@
|
||||
"browserTarget": "youtube-dl-material:build"
|
||||
}
|
||||
},
|
||||
"serve-electron": {
|
||||
"builder": "@angular-guru/electron-builder:dev-server",
|
||||
"options": {
|
||||
"browserTarget": "youtube-dl-material:build"
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"browserTarget": "youtube-dl-material:build:production"
|
||||
}
|
||||
}
|
||||
},
|
||||
"electron": {
|
||||
"builder": "@angular-guru/electron-builder:build",
|
||||
"options": {
|
||||
"outputPath": "dist",
|
||||
"index": "src/index.html",
|
||||
"main": "main.js",
|
||||
"tsConfig": "src/tsconfig.app.json",
|
||||
"polyfills": "src/polyfills.ts",
|
||||
"assets": [
|
||||
"src/assets",
|
||||
"src/favicon.ico",
|
||||
"src/backend/audio",
|
||||
"src/backend/video",
|
||||
"src/backend"
|
||||
],
|
||||
"styles": [
|
||||
"src/styles.scss"
|
||||
],
|
||||
"scripts": []
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"optimization": true,
|
||||
"outputHashing": "all",
|
||||
"sourceMap": false,
|
||||
"extractCss": true,
|
||||
"namedChunks": false,
|
||||
"aot": true,
|
||||
"extractLicenses": true,
|
||||
"vendorChunk": false,
|
||||
"buildOptimizer": true,
|
||||
"fileReplacements": [
|
||||
{
|
||||
"replace": "src/environments/environment.ts",
|
||||
"with": "src/environments/environment.prod.ts"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"test": {
|
||||
"builder": "@angular-devkit/build-angular:karma",
|
||||
"options": {
|
||||
@@ -75,8 +125,7 @@
|
||||
"tsConfig": "src/tsconfig.spec.json",
|
||||
"scripts": [],
|
||||
"styles": [
|
||||
"src/styles.css",
|
||||
"../node_modules/videogular2/fonts/videogular.css"
|
||||
"src/styles.scss"
|
||||
],
|
||||
"assets": [
|
||||
"src/assets",
|
||||
@@ -127,7 +176,7 @@
|
||||
"schematics": {
|
||||
"@schematics/angular:component": {
|
||||
"prefix": "app",
|
||||
"styleext": "css"
|
||||
"styleext": "scss"
|
||||
},
|
||||
"@schematics/angular:directive": {
|
||||
"prefix": "app"
|
||||
|
||||
660
backend/app.js
660
backend/app.js
@@ -2,59 +2,72 @@ var async = require('async');
|
||||
var fs = require('fs');
|
||||
var path = require('path');
|
||||
var youtubedl = require('youtube-dl');
|
||||
var config = require('config');
|
||||
var https = require('https');
|
||||
var express = require("express");
|
||||
var bodyParser = require("body-parser");
|
||||
var archiver = require('archiver');
|
||||
const low = require('lowdb')
|
||||
var URL = require('url').URL;
|
||||
const shortid = require('shortid')
|
||||
var config_api = require('./config.js');
|
||||
|
||||
var app = express();
|
||||
|
||||
var URL = require('url').URL;
|
||||
const FileSync = require('lowdb/adapters/FileSync')
|
||||
const adapter = new FileSync('db.json');
|
||||
const db = low(adapter)
|
||||
|
||||
// Set some defaults
|
||||
db.defaults(
|
||||
{
|
||||
playlists: {
|
||||
audio: [],
|
||||
video: []
|
||||
},
|
||||
configWriteFlag: false
|
||||
}).write();
|
||||
|
||||
// config values
|
||||
var frontendUrl = null;
|
||||
var backendUrl = null;
|
||||
var backendPort = 17442;
|
||||
var usingEncryption = null;
|
||||
var basePath = null;
|
||||
var audioFolderPath = null;
|
||||
var videoFolderPath = null;
|
||||
var downloadOnlyMode = null;
|
||||
var useDefaultDownloadingAgent = null;
|
||||
var customDownloadingAgent = null;
|
||||
|
||||
// other needed values
|
||||
var options = null; // encryption options
|
||||
var url_domain = null;
|
||||
|
||||
// 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 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 validDownloadingAgents = [
|
||||
'aria2c'
|
||||
]
|
||||
|
||||
// don't overwrite config if it already happened.. NOT
|
||||
// let alreadyWritten = db.get('configWriteFlag').value();
|
||||
let writeConfigMode = process.env.write_ytdl_config;
|
||||
var config = null;
|
||||
|
||||
if (writeConfigMode) {
|
||||
setAndLoadConfig();
|
||||
} else {
|
||||
loadConfig();
|
||||
}
|
||||
|
||||
var descriptors = {};
|
||||
|
||||
|
||||
if (usingEncryption)
|
||||
{
|
||||
|
||||
var certFilePath = path.resolve(config.get("YoutubeDLMaterial.Encryption.cert-file-path"));
|
||||
var keyFilePath = path.resolve(config.get("YoutubeDLMaterial.Encryption.key-file-path"));
|
||||
|
||||
var certKeyFile = fs.readFileSync(keyFilePath);
|
||||
var certFile = fs.readFileSync(certFilePath);
|
||||
|
||||
var options = {
|
||||
key: certKeyFile,
|
||||
cert: certFile
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
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", url_domain.origin);
|
||||
res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
|
||||
next();
|
||||
});
|
||||
|
||||
app.get('/using-encryption', function(req, res) {
|
||||
res.send(usingEncryption);
|
||||
res.end("yes");
|
||||
@@ -72,6 +85,109 @@ function File(id, title, thumbnailURL, isAudio, duration) {
|
||||
|
||||
// actual functions
|
||||
|
||||
function startServer() {
|
||||
if (usingEncryption)
|
||||
{
|
||||
https.createServer(options, app).listen(backendPort, function() {
|
||||
console.log('HTTPS: Anchor set on 17442');
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
app.listen(backendPort,function(){
|
||||
console.log("HTTP: Started on PORT " + backendPort);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function setAndLoadConfig() {
|
||||
await setConfigFromEnv();
|
||||
await loadConfig();
|
||||
// console.log(backendUrl);
|
||||
}
|
||||
|
||||
async function setConfigFromEnv() {
|
||||
return new Promise(resolve => {
|
||||
let config_items = getEnvConfigItems();
|
||||
let success = config_api.setConfigItems(config_items);
|
||||
if (success) {
|
||||
console.log('Config items set using ENV variables.');
|
||||
setTimeout(() => resolve(true), 100);
|
||||
} else {
|
||||
console.log('ERROR: Failed to set config items using ENV variables.');
|
||||
resolve(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function loadConfig() {
|
||||
return new Promise(resolve => {
|
||||
// get config library
|
||||
// config = require('config');
|
||||
|
||||
frontendUrl = !debugMode ? config_api.getConfigItem('ytdl_frontend_url') : 'http://localhost:4200';
|
||||
backendUrl = config_api.getConfigItem('ytdl_backend_url')
|
||||
backendPort = 17442;
|
||||
usingEncryption = config_api.getConfigItem('ytdl_use_encryption');
|
||||
basePath = config_api.getConfigItem('ytdl_base_path');
|
||||
audioFolderPath = config_api.getConfigItem('ytdl_audio_folder_path');
|
||||
videoFolderPath = config_api.getConfigItem('ytdl_video_folder_path');
|
||||
downloadOnlyMode = config_api.getConfigItem('ytdl_download_only_mode');
|
||||
useDefaultDownloadingAgent = config_api.getConfigItem('ytdl_use_default_downloading_agent');
|
||||
customDownloadingAgent = config_api.getConfigItem('ytdl_custom_downloading_agent');
|
||||
if (!useDefaultDownloadingAgent && validDownloadingAgents.indexOf(customDownloadingAgent) !== -1 ) {
|
||||
console.log(`INFO: Using non-default downloading agent \'${customDownloadingAgent}\'`)
|
||||
}
|
||||
|
||||
if (usingEncryption)
|
||||
{
|
||||
var certFilePath = path.resolve(config_api.getConfigItem('ytdl_cert_file_path'));
|
||||
var keyFilePath = path.resolve(config_api.getConfigItem('ytdl_key_file_path'));
|
||||
|
||||
var certKeyFile = fs.readFileSync(keyFilePath);
|
||||
var certFile = fs.readFileSync(certFilePath);
|
||||
|
||||
options = {
|
||||
key: certKeyFile,
|
||||
cert: certFile
|
||||
};
|
||||
}
|
||||
|
||||
url_domain = new URL(frontendUrl);
|
||||
|
||||
// start the server here
|
||||
startServer();
|
||||
|
||||
resolve(true);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
function getOrigin() {
|
||||
return url_domain.origin;
|
||||
}
|
||||
|
||||
// gets a list of config items that are stored as an environment variable
|
||||
function getEnvConfigItems() {
|
||||
let config_items = [];
|
||||
|
||||
let config_item_keys = Object.keys(config_api.CONFIG_ITEMS);
|
||||
for (let i = 0; i < config_item_keys.length; i++) {
|
||||
let key = config_item_keys[i];
|
||||
if (process['env'][key]) {
|
||||
const config_item = generateEnvVarConfigItem(key);
|
||||
config_items.push(config_item);
|
||||
}
|
||||
}
|
||||
|
||||
return config_items;
|
||||
}
|
||||
|
||||
// gets value of a config item and stores it in an object
|
||||
function generateEnvVarConfigItem(key) {
|
||||
return {key: key, value: process['env'][key]};
|
||||
}
|
||||
|
||||
function getThumbnailMp3(name)
|
||||
{
|
||||
var obj = getJSONMp3(name);
|
||||
@@ -180,6 +296,44 @@ function getVideoFormatID(name)
|
||||
}
|
||||
}
|
||||
|
||||
async function createPlaylistZipFile(fileNames, type, outputName) {
|
||||
return new Promise(async resolve => {
|
||||
let zipFolderPath = path.join(__dirname, (type === 'audio') ? audioFolderPath : videoFolderPath);
|
||||
// let name = fileNames[0].split(' ')[0] + fileNames[1].split(' ')[0];
|
||||
let ext = (type === 'audio') ? '.mp3' : '.mp4';
|
||||
|
||||
let output = fs.createWriteStream(path.join(zipFolderPath, outputName + '.zip'));
|
||||
|
||||
var archive = archiver('zip', {
|
||||
gzip: true,
|
||||
zlib: { level: 9 } // Sets the compression level.
|
||||
});
|
||||
|
||||
archive.on('error', function(err) {
|
||||
console.log(err);
|
||||
throw err;
|
||||
});
|
||||
|
||||
// pipe archive data to the output file
|
||||
archive.pipe(output);
|
||||
|
||||
for (let i = 0; i < fileNames.length; i++) {
|
||||
let fileName = fileNames[i];
|
||||
archive.file(zipFolderPath + fileName + ext, {name: fileName + ext})
|
||||
}
|
||||
|
||||
await archive.finalize();
|
||||
|
||||
// wait a tiny bit for the zip to reload in fs
|
||||
setTimeout(function() {
|
||||
resolve(path.join(zipFolderPath,outputName + '.zip'));
|
||||
}, 100);
|
||||
|
||||
});
|
||||
|
||||
|
||||
}
|
||||
|
||||
function deleteAudioFile(name) {
|
||||
return new Promise(resolve => {
|
||||
// TODO: split descriptors into audio and video descriptors, as deleting an audio file will close all video file streams
|
||||
@@ -196,7 +350,7 @@ function deleteAudioFile(name) {
|
||||
for (let i = 0; i < descriptors[name].length; i++) {
|
||||
descriptors[name][i].destroy();
|
||||
}
|
||||
} catch {
|
||||
} catch(e) {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -235,7 +389,7 @@ async function deleteVideoFile(name) {
|
||||
for (let i = 0; i < descriptors[name].length; i++) {
|
||||
descriptors[name][i].destroy();
|
||||
}
|
||||
} catch {
|
||||
} catch(e) {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -259,6 +413,30 @@ async function deleteVideoFile(name) {
|
||||
});
|
||||
}
|
||||
|
||||
function recFindByExt(base,ext,files,result)
|
||||
{
|
||||
files = files || fs.readdirSync(base)
|
||||
result = result || []
|
||||
|
||||
files.forEach(
|
||||
function (file) {
|
||||
var newbase = path.join(base,file)
|
||||
if ( fs.statSync(newbase).isDirectory() )
|
||||
{
|
||||
result = recFindByExt(newbase,ext,fs.readdirSync(newbase),result)
|
||||
}
|
||||
else
|
||||
{
|
||||
if ( file.substr(-1*(ext.length+1)) == '.' + ext )
|
||||
{
|
||||
result.push(newbase)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
return result
|
||||
}
|
||||
|
||||
function getAudioInfos(fileNames) {
|
||||
let result = [];
|
||||
for (let i = 0; i < fileNames.length; i++) {
|
||||
@@ -268,7 +446,7 @@ function getAudioInfos(fileNames) {
|
||||
let data = fs.readFileSync(fileLocation);
|
||||
try {
|
||||
result.push(JSON.parse(data));
|
||||
} catch {
|
||||
} catch(e) {
|
||||
console.log(`ERROR: Could not find info for file ${fileName}.mp3`);
|
||||
}
|
||||
}
|
||||
@@ -285,7 +463,7 @@ function getVideoInfos(fileNames) {
|
||||
let data = fs.readFileSync(fileLocation);
|
||||
try {
|
||||
result.push(JSON.parse(data));
|
||||
} catch {
|
||||
} catch(e) {
|
||||
console.log(`ERROR: Could not find info for file ${fileName}.mp4`);
|
||||
}
|
||||
}
|
||||
@@ -295,9 +473,15 @@ function getVideoInfos(fileNames) {
|
||||
|
||||
// currently only works for single urls
|
||||
async function getUrlInfos(urls) {
|
||||
let startDate = Date.now();
|
||||
let result = [];
|
||||
return new Promise(resolve => {
|
||||
youtubedl.exec(urls.join(' '), ['--external-downloader', 'aria2c', '--dump-json'], {}, (err, output) => {
|
||||
youtubedl.exec(urls.join(' '), ['--dump-json'], {}, (err, output) => {
|
||||
if (debugMode) {
|
||||
let new_date = Date.now();
|
||||
let difference = (new_date - startDate)/1000;
|
||||
console.log(`URL info retrieval delay: ${difference} seconds.`);
|
||||
}
|
||||
if (err) {
|
||||
console.log('Error during parsing:' + err);
|
||||
resolve(null);
|
||||
@@ -306,38 +490,59 @@ async function getUrlInfos(urls) {
|
||||
try {
|
||||
try_putput = JSON.parse(output);
|
||||
result = try_putput;
|
||||
}
|
||||
catch {
|
||||
} catch(e) {
|
||||
// probably multiple urls
|
||||
console.log('failed to parse');
|
||||
console.log(output);
|
||||
console.log('failed to parse for urls starting with ' + urls[0]);
|
||||
// console.log(output);
|
||||
}
|
||||
resolve(result);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
app.use(function(req, res, next) {
|
||||
res.header("Access-Control-Allow-Origin", getOrigin());
|
||||
res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
|
||||
next();
|
||||
});
|
||||
|
||||
app.post('/tomp3', function(req, res) {
|
||||
var url = req.body.url;
|
||||
var date = Date.now();
|
||||
var path = audioFolderPath;
|
||||
var audiopath = '%(title)s';
|
||||
|
||||
var customQualityConfiguration = req.body.customQualityConfiguration;
|
||||
var maxBitrate = req.body.maxBitrate;
|
||||
var customArgs = req.body.customArgs;
|
||||
var customOutput = req.body.customOutput;
|
||||
|
||||
let downloadConfig = ['--external-downloader', 'aria2c', '-o', path + audiopath + ".mp3", '-x', '--audio-format', 'mp3', '--write-info-json', '--print-json']
|
||||
|
||||
let downloadConfig = null;
|
||||
let qualityPath = '';
|
||||
|
||||
if (customQualityConfiguration) {
|
||||
qualityPath = `-f ${customQualityConfiguration}`;
|
||||
} else if (maxBitrate) {
|
||||
if (!maxBitrate || maxBitrate === '') maxBitrate = '0';
|
||||
qualityPath = `--audio-quality ${maxBitrate}`
|
||||
}
|
||||
if (customArgs) {
|
||||
downloadConfig = [customArgs];
|
||||
} else {
|
||||
if (customOutput) {
|
||||
downloadConfig = ['-o', audioFolderPath + customOutput + '.mp3', '-x', '--audio-format', 'mp3', '--write-info-json', '--print-json'];
|
||||
} else {
|
||||
downloadConfig = ['-o', audioFolderPath + audiopath + ".mp3", '-x', '--audio-format', 'mp3', '--write-info-json', '--print-json'];
|
||||
}
|
||||
|
||||
if (qualityPath !== '') {
|
||||
downloadConfig.splice(2, 0, 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);
|
||||
}
|
||||
|
||||
if (!useDefaultDownloadingAgent && customDownloadingAgent === 'aria2c') {
|
||||
downloadConfig.splice(0, 0, '--external-downloader', 'aria2c');
|
||||
}
|
||||
}
|
||||
|
||||
youtubedl.exec(url, downloadConfig, {}, function(err, output) {
|
||||
@@ -348,6 +553,7 @@ app.post('/tomp3', function(req, res) {
|
||||
}
|
||||
if (err) {
|
||||
audiopath = "-1";
|
||||
console.log(err.stderr);
|
||||
res.sendStatus(500);
|
||||
throw err;
|
||||
} else if (output) {
|
||||
@@ -356,22 +562,22 @@ app.post('/tomp3', function(req, res) {
|
||||
let output_json = null;
|
||||
try {
|
||||
output_json = JSON.parse(output[i]);
|
||||
} catch {
|
||||
} catch(e) {
|
||||
output_json = null;
|
||||
}
|
||||
if (!output_json) {
|
||||
// only run on first go
|
||||
return;
|
||||
// if invalid, continue onto the next
|
||||
continue;
|
||||
}
|
||||
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);
|
||||
var file_name = output_json['_filename'].replace(/^.*[\\\/]/, '');
|
||||
var file_path = output_json['_filename'].substring(audioFolderPath.length, output_json['_filename'].length);
|
||||
var alternate_file_path = file_path.substring(0, file_path.length-4);
|
||||
var alternate_file_name = file_name.substring(0, file_name.length-4);
|
||||
if (alternate_file_path) file_names.push(alternate_file_path);
|
||||
}
|
||||
|
||||
let is_playlist = file_names.length > 1;
|
||||
if (!is_playlist) audiopath = file_names[0];
|
||||
// if (!is_playlist) audiopath = file_names[0];
|
||||
|
||||
var audiopathEncoded = encodeURIComponent(file_names[0]);
|
||||
res.send({
|
||||
@@ -387,21 +593,36 @@ app.post('/tomp4', function(req, res) {
|
||||
var date = Date.now();
|
||||
var path = videoFolderPath;
|
||||
var videopath = '%(title)s';
|
||||
var customArgs = req.body.customArgs;
|
||||
var customOutput = req.body.customOutput;
|
||||
|
||||
var selectedHeight = req.body.selectedHeight;
|
||||
var customQualityConfiguration = req.body.customQualityConfiguration;
|
||||
|
||||
// console.log(selectedHeight);
|
||||
|
||||
let downloadConfig = null;
|
||||
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}]`;
|
||||
if (customArgs) {
|
||||
downloadConfig = [customArgs];
|
||||
} else {
|
||||
if (customOutput) {
|
||||
downloadConfig = ['-o', path + customOutput + ".mp4", '-f', qualityPath, '--write-info-json', '--print-json'];
|
||||
} else {
|
||||
downloadConfig = ['-o', path + videopath + ".mp4", '-f', qualityPath, '--write-info-json', '--print-json'];
|
||||
}
|
||||
|
||||
if (customQualityConfiguration) {
|
||||
qualityPath = customQualityConfiguration;
|
||||
} else if (selectedHeight && selectedHeight !== '') {
|
||||
qualityPath = `bestvideo[height=${selectedHeight}]+bestaudio/best[height=${selectedHeight}]`;
|
||||
}
|
||||
|
||||
if (!useDefaultDownloadingAgent && customDownloadingAgent === 'aria2c') {
|
||||
downloadConfig.splice(0, 0, '--external-downloader', 'aria2c');
|
||||
}
|
||||
}
|
||||
|
||||
youtubedl.exec(url, ['--external-downloader', 'aria2c', '-o', path + videopath + ".mp4", '-f', qualityPath, '--write-info-json', '--print-json'], {}, function(err, output) {
|
||||
youtubedl.exec(url, downloadConfig, {}, function(err, output) {
|
||||
if (debugMode) {
|
||||
let new_date = Date.now();
|
||||
let difference = (new_date - date)/1000;
|
||||
@@ -409,6 +630,7 @@ app.post('/tomp4', function(req, res) {
|
||||
}
|
||||
if (err) {
|
||||
videopath = "-1";
|
||||
console.log(err.stderr);
|
||||
res.sendStatus(500);
|
||||
throw err;
|
||||
} else if (output) {
|
||||
@@ -417,30 +639,27 @@ app.post('/tomp4', function(req, res) {
|
||||
let output_json = null;
|
||||
try {
|
||||
output_json = JSON.parse(output[i]);
|
||||
} catch {
|
||||
} catch(e) {
|
||||
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('\\');
|
||||
var file_name = output_json['_filename'].replace(/^.*[\\\/]/, '');
|
||||
|
||||
// renames file if necessary due to bug
|
||||
if (!fs.existsSync(output_json['_filename'] && fs.existsSync(output_json['_filename'] + '.webm'))) {
|
||||
try {
|
||||
fs.renameSync(output_json['_filename'] + '.webm', output_json['_filename']);
|
||||
console.log('Renamed ' + file_path + '.webm to ' + file_path);
|
||||
} catch {
|
||||
console.log('Renamed ' + file_name + '.webm to ' + file_name);
|
||||
} catch(e) {
|
||||
}
|
||||
}
|
||||
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);
|
||||
var alternate_file_name = file_name.substring(0, file_name.length-4);
|
||||
var file_path = output_json['_filename'].substring(audioFolderPath.length, output_json['_filename'].length);
|
||||
var alternate_file_path = file_path.substring(0, file_path.length-4);
|
||||
if (alternate_file_name) file_names.push(alternate_file_path);
|
||||
}
|
||||
|
||||
let is_playlist = file_names.length > 1;
|
||||
@@ -501,35 +720,31 @@ app.post('/fileStatusMp4', function(req, res) {
|
||||
// gets all download mp3s
|
||||
app.post('/getMp3s', function(req, res) {
|
||||
var mp3s = [];
|
||||
var fullpath = audioFolderPath;
|
||||
var files = fs.readdirSync(audioFolderPath);
|
||||
|
||||
for (var i in files)
|
||||
{
|
||||
var nameLength = path.basename(files[i]).length;
|
||||
var ext = path.basename(files[i]).substring(nameLength-4, nameLength);
|
||||
if (ext == ".mp3")
|
||||
var playlists = db.get('playlists.audio').value();
|
||||
var files = recFindByExt(audioFolderPath, 'mp3'); // fs.readdirSync(audioFolderPath);
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
let file = files[i];
|
||||
var file_path = file.substring(audioFolderPath.length, file.length);
|
||||
var id = file_path.substring(0, file_path.length-4);
|
||||
var jsonobj = getJSONMp3(id);
|
||||
if (!jsonobj) continue;
|
||||
var title = jsonobj.title;
|
||||
|
||||
if (title.length > 14) // edits title if it's too long
|
||||
{
|
||||
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;
|
||||
|
||||
if (title.length > 14) // edits title if it's too long
|
||||
{
|
||||
title = title.substring(0,12) + "...";
|
||||
}
|
||||
|
||||
var thumbnail = jsonobj.thumbnail;
|
||||
var duration = jsonobj.duration;
|
||||
var isaudio = true;
|
||||
var file = new File(id, title, thumbnail, isaudio, duration);
|
||||
mp3s.push(file);
|
||||
title = title.substring(0,12) + "...";
|
||||
}
|
||||
|
||||
var thumbnail = jsonobj.thumbnail;
|
||||
var duration = jsonobj.duration;
|
||||
var isaudio = true;
|
||||
var file_obj = new File(id, title, thumbnail, isaudio, duration);
|
||||
mp3s.push(file_obj);
|
||||
}
|
||||
|
||||
res.send({
|
||||
mp3s: mp3s
|
||||
mp3s: mp3s,
|
||||
playlists: playlists
|
||||
});
|
||||
res.end("yes");
|
||||
});
|
||||
@@ -537,39 +752,106 @@ app.post('/getMp3s', function(req, res) {
|
||||
// gets all download mp4s
|
||||
app.post('/getMp4s', function(req, res) {
|
||||
var mp4s = [];
|
||||
var playlists = db.get('playlists.video').value();
|
||||
var fullpath = videoFolderPath;
|
||||
var files = fs.readdirSync(videoFolderPath);
|
||||
|
||||
for (var i in files)
|
||||
{
|
||||
var nameLength = path.basename(files[i]).length;
|
||||
var ext = path.basename(files[i]).substring(nameLength-4, nameLength);
|
||||
if (ext == ".mp4")
|
||||
var files = recFindByExt(videoFolderPath, 'mp4');
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
let file = files[i];
|
||||
var file_path = file.substring(videoFolderPath.length, file.length);
|
||||
var id = file_path.substring(0, file_path.length-4);
|
||||
var jsonobj = getJSONMp4(id);
|
||||
if (!jsonobj) continue;
|
||||
var title = jsonobj.title;
|
||||
|
||||
if (title.length > 14) // edits title if it's too long
|
||||
{
|
||||
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;
|
||||
|
||||
if (title.length > 14) // edits title if it's too long
|
||||
{
|
||||
title = title.substring(0,12) + "...";
|
||||
}
|
||||
|
||||
var thumbnail = jsonobj.thumbnail;
|
||||
var duration = jsonobj.duration;
|
||||
var isaudio = false;
|
||||
var file = new File(id, title, thumbnail, isaudio, duration);
|
||||
mp4s.push(file);
|
||||
title = title.substring(0,12) + "...";
|
||||
}
|
||||
|
||||
var thumbnail = jsonobj.thumbnail;
|
||||
var duration = jsonobj.duration;
|
||||
var isaudio = false;
|
||||
var file_obj = new File(id, title, thumbnail, isaudio, duration);
|
||||
mp4s.push(file_obj);
|
||||
}
|
||||
|
||||
res.send({
|
||||
mp4s: mp4s
|
||||
mp4s: mp4s,
|
||||
playlists: playlists
|
||||
});
|
||||
res.end("yes");
|
||||
});
|
||||
|
||||
app.post('/createPlaylist', async (req, res) => {
|
||||
let playlistName = req.body.playlistName;
|
||||
let fileNames = req.body.fileNames;
|
||||
let type = req.body.type;
|
||||
let thumbnailURL = req.body.thumbnailURL;
|
||||
|
||||
let new_playlist = {
|
||||
'name': playlistName,
|
||||
fileNames: fileNames,
|
||||
id: shortid.generate(),
|
||||
thumbnailURL: thumbnailURL
|
||||
};
|
||||
|
||||
db.get(`playlists.${type}`)
|
||||
.push(new_playlist)
|
||||
.write();
|
||||
|
||||
res.send({
|
||||
new_playlist: new_playlist,
|
||||
success: !!new_playlist // always going to be true
|
||||
})
|
||||
});
|
||||
|
||||
app.post('/updatePlaylist', async (req, res) => {
|
||||
let playlistID = req.body.playlistID;
|
||||
let fileNames = req.body.fileNames;
|
||||
let type = req.body.type;
|
||||
|
||||
let success = false;
|
||||
try {
|
||||
db.get(`playlists.${type}`)
|
||||
.find({id: playlistID})
|
||||
.assign({fileNames: fileNames})
|
||||
.write();
|
||||
/*console.log('success!');
|
||||
let new_val = db.get(`playlists.${type}`)
|
||||
.find({id: playlistID})
|
||||
.value();
|
||||
console.log(new_val);*/
|
||||
success = true;
|
||||
} catch(e) {
|
||||
console.error(`Failed to find playlist with ID ${playlistID}`);
|
||||
}
|
||||
|
||||
res.send({
|
||||
success: success
|
||||
})
|
||||
});
|
||||
|
||||
app.post('/deletePlaylist', async (req, res) => {
|
||||
let playlistID = req.body.playlistID;
|
||||
let type = req.body.type;
|
||||
|
||||
let success = null;
|
||||
try {
|
||||
// removes playlist from playlists
|
||||
db.get(`playlists.${type}`)
|
||||
.remove({id: playlistID})
|
||||
.write();
|
||||
|
||||
success = true;
|
||||
} catch(e) {
|
||||
success = false;
|
||||
}
|
||||
|
||||
res.send({
|
||||
success: success
|
||||
})
|
||||
});
|
||||
|
||||
// deletes mp3 file
|
||||
app.post('/deleteMp3', async (req, res) => {
|
||||
var name = req.body.name;
|
||||
@@ -610,15 +892,24 @@ app.post('/deleteMp4', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/downloadFile', function(req, res) {
|
||||
let fileName = req.body.fileName;
|
||||
app.post('/downloadFile', async (req, res) => {
|
||||
let fileNames = req.body.fileNames;
|
||||
let is_playlist = req.body.is_playlist;
|
||||
let type = req.body.type;
|
||||
let outputName = req.body.outputName;
|
||||
let file = null;
|
||||
if (type === 'audio') {
|
||||
file = __dirname + '/' + 'audio/' + fileName + '.mp3';
|
||||
} else if (type === 'video') {
|
||||
file = __dirname + '/' + 'video/' + fileName + '.mp4';
|
||||
if (!is_playlist) {
|
||||
fileNames = decodeURI(fileNames);
|
||||
if (type === 'audio') {
|
||||
file = __dirname + '/' + audioFolderPath + fileNames + '.mp3';
|
||||
} else if (type === 'video') {
|
||||
file = __dirname + '/' + videoFolderPath + fileNames + '.mp4';
|
||||
}
|
||||
} else {
|
||||
for (let i = 0; i < fileNames.length; i++) {
|
||||
fileNames[i] = decodeURI(fileNames[i]);
|
||||
}
|
||||
file = await createPlaylistZipFile(fileNames, type, outputName);
|
||||
}
|
||||
|
||||
res.sendFile(file);
|
||||
@@ -637,46 +928,48 @@ app.post('/deleteFile', async (req, res) => {
|
||||
|
||||
app.get('/video/:id', function(req , res){
|
||||
var head;
|
||||
const path = "video/" + req.params.id + '.mp4';
|
||||
const stat = fs.statSync(path)
|
||||
const fileSize = stat.size
|
||||
const range = req.headers.range
|
||||
if (range) {
|
||||
const parts = range.replace(/bytes=/, "").split("-")
|
||||
const start = parseInt(parts[0], 10)
|
||||
const end = parts[1]
|
||||
? parseInt(parts[1], 10)
|
||||
: 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',
|
||||
'Content-Length': chunksize,
|
||||
'Content-Type': 'video/mp4',
|
||||
let id = decodeURI(req.params.id);
|
||||
const path = "video/" + id + '.mp4';
|
||||
const stat = fs.statSync(path)
|
||||
const fileSize = stat.size
|
||||
const range = req.headers.range
|
||||
if (range) {
|
||||
const parts = range.replace(/bytes=/, "").split("-")
|
||||
const start = parseInt(parts[0], 10)
|
||||
const end = parts[1]
|
||||
? parseInt(parts[1], 10)
|
||||
: fileSize-1
|
||||
const chunksize = (end-start)+1
|
||||
const file = fs.createReadStream(path, {start, end})
|
||||
if (descriptors[id]) descriptors[id].push(file);
|
||||
else descriptors[id] = [file];
|
||||
file.on('close', function() {
|
||||
let index = descriptors[id].indexOf(file);
|
||||
descriptors[id].splice(index, 1);
|
||||
if (debugMode) console.log('Successfully closed stream and removed file reference.');
|
||||
});
|
||||
head = {
|
||||
'Content-Range': `bytes ${start}-${end}/${fileSize}`,
|
||||
'Accept-Ranges': 'bytes',
|
||||
'Content-Length': chunksize,
|
||||
'Content-Type': 'video/mp4',
|
||||
}
|
||||
res.writeHead(206, head);
|
||||
file.pipe(res);
|
||||
} else {
|
||||
head = {
|
||||
'Content-Length': fileSize,
|
||||
'Content-Type': 'video/mp4',
|
||||
}
|
||||
res.writeHead(200, head)
|
||||
fs.createReadStream(path).pipe(res)
|
||||
}
|
||||
res.writeHead(206, head);
|
||||
file.pipe(res);
|
||||
} else {
|
||||
head = {
|
||||
'Content-Length': fileSize,
|
||||
'Content-Type': 'video/mp4',
|
||||
}
|
||||
res.writeHead(200, head)
|
||||
fs.createReadStream(path).pipe(res)
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/audio/:id', function(req , res){
|
||||
var head;
|
||||
let path = "audio/" + req.params.id + '.mp3';
|
||||
let id = decodeURI(req.params.id);
|
||||
let path = "audio/" + id + '.mp3';
|
||||
path = path.replace(/\"/g, '\'');
|
||||
const stat = fs.statSync(path)
|
||||
const fileSize = stat.size
|
||||
@@ -689,12 +982,12 @@ app.get('/audio/: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];
|
||||
if (descriptors[id]) descriptors[id].push(file);
|
||||
else descriptors[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.');
|
||||
let index = descriptors[id].indexOf(file);
|
||||
descriptors[id].splice(index, 1);
|
||||
if (debugMode) console.log('Successfully closed stream and removed file reference.');
|
||||
});
|
||||
head = {
|
||||
'Content-Range': `bytes ${start}-${end}/${fileSize}`,
|
||||
@@ -720,7 +1013,6 @@ app.get('/audio/:id', function(req , res){
|
||||
let urlMode = !!req.body.urlMode;
|
||||
let type = req.body.type;
|
||||
let result = null;
|
||||
// console.log(urlMode);
|
||||
if (!urlMode) {
|
||||
if (type === 'audio') {
|
||||
result = getAudioInfos(fileNames)
|
||||
@@ -735,19 +1027,3 @@ app.get('/audio/:id', function(req , res){
|
||||
success: !!result
|
||||
})
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
if (usingEncryption)
|
||||
{
|
||||
https.createServer(options, app).listen(backendPort, function() {
|
||||
console.log('HTTPS: Anchor set on 17442');
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
app.listen(backendPort,function(){
|
||||
console.log("HTTP: Started on PORT " + backendPort);
|
||||
});
|
||||
}
|
||||
112
backend/config.js
Normal file
112
backend/config.js
Normal file
@@ -0,0 +1,112 @@
|
||||
const fs = require('fs');
|
||||
|
||||
let CONFIG_ITEMS = require('./consts.js')['CONFIG_ITEMS'];
|
||||
|
||||
let configPath = 'config/default.json';
|
||||
|
||||
// https://stackoverflow.com/questions/6491463/accessing-nested-javascript-objects-with-string-key
|
||||
Object.byString = function(o, s) {
|
||||
s = s.replace(/\[(\w+)\]/g, '.$1'); // convert indexes to properties
|
||||
s = s.replace(/^\./, ''); // strip a leading dot
|
||||
var a = s.split('.');
|
||||
for (var i = 0, n = a.length; i < n; ++i) {
|
||||
var k = a[i];
|
||||
if (k in o) {
|
||||
o = o[k];
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
return o;
|
||||
}
|
||||
|
||||
function getParentPath(path) {
|
||||
let elements = path.split('.');
|
||||
elements.splice(elements.length - 1, 1);
|
||||
return elements.join('.');
|
||||
}
|
||||
|
||||
function getElementNameInConfig(path) {
|
||||
let elements = path.split('.');
|
||||
return elements[elements.length - 1];
|
||||
}
|
||||
|
||||
/*
|
||||
* Gets config file and returns as a json
|
||||
*/
|
||||
function getConfigFile() {
|
||||
let raw_data = fs.readFileSync(configPath);
|
||||
try {
|
||||
let parsed_data = JSON.parse(raw_data);
|
||||
return parsed_data;
|
||||
} catch(e) {
|
||||
console.log('ERROR: Failed to get config file');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function setConfigFile(config) {
|
||||
try {
|
||||
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
||||
return true;
|
||||
} catch(e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function getConfigItem(key) {
|
||||
let config_json = getConfigFile();
|
||||
if (!CONFIG_ITEMS[key]) console.log('cannot find config with key ' + key);
|
||||
let path = CONFIG_ITEMS[key]['path'];
|
||||
return Object.byString(config_json, path);
|
||||
};
|
||||
|
||||
function setConfigItem(key, value) {
|
||||
let success = false;
|
||||
let config_json = getConfigFile();
|
||||
let path = CONFIG_ITEMS[key]['path'];
|
||||
let parent_path = getParentPath(path);
|
||||
let element_name = getElementNameInConfig(path);
|
||||
|
||||
let parent_object = Object.byString(config_json, parent_path);
|
||||
if (value === 'false' || value === 'true') {
|
||||
parent_object[element_name] = (value === 'true');
|
||||
} else {
|
||||
parent_object[element_name] = value;
|
||||
}
|
||||
|
||||
success = setConfigFile(config_json);
|
||||
|
||||
return success;
|
||||
};
|
||||
|
||||
function setConfigItems(items) {
|
||||
let success = false;
|
||||
let config_json = getConfigFile();
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
let key = items[i].key;
|
||||
let value = items[i].value;
|
||||
|
||||
// if boolean strings, set to booleans again
|
||||
if (value === 'false' || value === 'true') {
|
||||
value = (value === 'true');
|
||||
}
|
||||
|
||||
let item_path = CONFIG_ITEMS[key]['path'];
|
||||
let item_parent_path = getParentPath(item_path);
|
||||
let item_element_name = getElementNameInConfig(item_path);
|
||||
|
||||
let item_parent_object = Object.byString(config_json, item_parent_path);
|
||||
item_parent_object[item_element_name] = value;
|
||||
}
|
||||
|
||||
success = setConfigFile(config_json);
|
||||
return success;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getConfigItem: getConfigItem,
|
||||
setConfigItem: setConfigItem,
|
||||
setConfigItems: setConfigItems,
|
||||
CONFIG_ITEMS: CONFIG_ITEMS
|
||||
}
|
||||
@@ -16,12 +16,23 @@
|
||||
},
|
||||
"Extra": {
|
||||
"title_top": "Youtube Downloader",
|
||||
"file_manager_enabled": true,
|
||||
"allow_quality_select": true,
|
||||
"download_only_mode": false,
|
||||
"file_manager_enabled": true
|
||||
"allow_multi_download_mode": true
|
||||
},
|
||||
"API": {
|
||||
"use_youtube_API": false,
|
||||
"youtube_API_key": ""
|
||||
},
|
||||
"Themes": {
|
||||
"default_theme": "default",
|
||||
"allow_theme_change": true
|
||||
},
|
||||
"Advanced": {
|
||||
"use_default_downloading_agent": true,
|
||||
"custom_downloading_agent": "",
|
||||
"allow_advanced_download": false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,12 +16,23 @@
|
||||
},
|
||||
"Extra": {
|
||||
"title_top": "Youtube Downloader",
|
||||
"file_manager_enabled": true,
|
||||
"allow_quality_select": true,
|
||||
"download_only_mode": false,
|
||||
"file_manager_enabled": true
|
||||
"allow_multi_download_mode": true
|
||||
},
|
||||
"API": {
|
||||
"use_youtube_API": false,
|
||||
"youtube_API_key": ""
|
||||
},
|
||||
"Themes": {
|
||||
"default_theme": "default",
|
||||
"allow_theme_change": true
|
||||
},
|
||||
"Advanced": {
|
||||
"use_default_downloading_agent": true,
|
||||
"custom_downloading_agent": "",
|
||||
"allow_advanced_download": false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
100
backend/consts.js
Normal file
100
backend/consts.js
Normal file
@@ -0,0 +1,100 @@
|
||||
var config = require('config');
|
||||
|
||||
let CONFIG_ITEMS = {
|
||||
// Host
|
||||
'ytdl_frontend_url': {
|
||||
'key': 'ytdl_frontend_url',
|
||||
'path': 'YoutubeDLMaterial.Host.frontendurl'
|
||||
},
|
||||
'ytdl_backend_url': {
|
||||
'key': 'ytdl_backend_url',
|
||||
'path': 'YoutubeDLMaterial.Host.backendurl'
|
||||
},
|
||||
|
||||
// Encryption
|
||||
'ytdl_use_encryption': {
|
||||
'key': 'ytdl_use_encryption',
|
||||
'path': 'YoutubeDLMaterial.Encryption.use-encryption'
|
||||
},
|
||||
'ytdl_cert_file_path': {
|
||||
'key': 'ytdl_cert_file_path',
|
||||
'path': 'YoutubeDLMaterial.Encryption.cert-file-path'
|
||||
},
|
||||
'ytdl_key_file_path': {
|
||||
'key': 'ytdl_key_file_path',
|
||||
'path': 'YoutubeDLMaterial.Encryption.key-file-path'
|
||||
},
|
||||
|
||||
// Downloader
|
||||
'ytdl_base_path': {
|
||||
'key': 'ytdl_base_path',
|
||||
'path': 'YoutubeDLMaterial.Downloader.path-base'
|
||||
},
|
||||
'ytdl_audio_folder_path': {
|
||||
'key': 'ytdl_audio_folder_path',
|
||||
'path': 'YoutubeDLMaterial.Downloader.path-audio'
|
||||
},
|
||||
'ytdl_video_folder_path': {
|
||||
'key': 'ytdl_video_folder_path',
|
||||
'path': 'YoutubeDLMaterial.Downloader.path-video'
|
||||
},
|
||||
|
||||
// Extra
|
||||
'ytdl_title_top': {
|
||||
'key': 'ytdl_title_top',
|
||||
'path': 'YoutubeDLMaterial.Extra.title_top'
|
||||
},
|
||||
'ytdl_file_manager_enabled': {
|
||||
'key': 'ytdl_file_manager_enabled',
|
||||
'path': 'YoutubeDLMaterial.Extra.file_manager_enabled'
|
||||
},
|
||||
'ytdl_allow_quality_select': {
|
||||
'key': 'ytdl_allow_quality_select',
|
||||
'path': 'YoutubeDLMaterial.Extra.allow_quality_select'
|
||||
},
|
||||
'ytdl_download_only_mode': {
|
||||
'key': 'ytdl_download_only_mode',
|
||||
'path': 'YoutubeDLMaterial.Extra.download_only_mode'
|
||||
},
|
||||
'ytdl_allow_multi_download_mode': {
|
||||
'key': 'ytdl_allow_multi_download_mode',
|
||||
'path': 'YoutubeDLMaterial.Extra.allow_multi_download_mode'
|
||||
},
|
||||
|
||||
|
||||
// API
|
||||
'ytdl_use_youtube_api': {
|
||||
'key': 'ytdl_use_youtube_api',
|
||||
'path': 'YoutubeDLMaterial.API.use_youtube_API'
|
||||
},
|
||||
'ytdl_youtube_api_key': {
|
||||
'key': 'ytdl_youtube_api_key',
|
||||
'path': 'YoutubeDLMaterial.API.youtube_API_key'
|
||||
},
|
||||
|
||||
// Themes
|
||||
'ytdl_default_theme': {
|
||||
'key': 'ytdl_default_theme',
|
||||
'path': 'YoutubeDLMaterial.Themes.default_theme'
|
||||
},
|
||||
'ytdl_allow_theme_change': {
|
||||
'key': 'ytdl_allow_theme_change',
|
||||
'path': 'YoutubeDLMaterial.Themes.allow_theme_change'
|
||||
},
|
||||
|
||||
// Advanced
|
||||
'ytdl_use_default_downloading_agent': {
|
||||
'key': 'ytdl_use_default_downloading_agent',
|
||||
'path': 'YoutubeDLMaterial.Advanced.use_default_downloading_agent'
|
||||
},
|
||||
'ytdl_custom_downloading_agent': {
|
||||
'key': 'ytdl_custom_downloading_agent',
|
||||
'path': 'YoutubeDLMaterial.Advanced.custom_downloading_agent'
|
||||
},
|
||||
'ytdl_allow_advanced_download': {
|
||||
'key': 'ytdl_allow_advanced_download',
|
||||
'path': 'YoutubeDLMaterial.Advanced.allow_advanced_download'
|
||||
},
|
||||
};
|
||||
|
||||
module.exports.CONFIG_ITEMS = CONFIG_ITEMS;
|
||||
1345
backend/package-lock.json
generated
Normal file
1345
backend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,26 +1,30 @@
|
||||
{
|
||||
"name": "backend",
|
||||
"version": "1.0.0",
|
||||
"description": "backend for hda",
|
||||
"description": "backend for YoutubeDL-Material",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"start": "node app.js"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/Tzahi12345/hda-backend.git"
|
||||
"url": ""
|
||||
},
|
||||
"author": "Isaac Grynsztein",
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/Tzahi12345/hda-backend/issues"
|
||||
"url": ""
|
||||
},
|
||||
"homepage": "https://github.com/Tzahi12345/hda-backend#readme",
|
||||
"homepage": "",
|
||||
"dependencies": {
|
||||
"archiver": "^3.1.1",
|
||||
"async": "^3.1.0",
|
||||
"config": "^3.2.3",
|
||||
"exe": "^1.0.2",
|
||||
"express": "^4.17.1",
|
||||
"youtube-dl": "^2.3.0"
|
||||
"lowdb": "^1.0.0",
|
||||
"shortid": "^2.2.15",
|
||||
"youtube-dl": "^3.0.2"
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
35
docker-compose.yml
Normal file
35
docker-compose.yml
Normal file
@@ -0,0 +1,35 @@
|
||||
|
||||
version: "2"
|
||||
services:
|
||||
ytdl_material:
|
||||
build: .
|
||||
environment:
|
||||
# config items
|
||||
ytdl_frontend_url: http://localhost:8998
|
||||
ytdl_backend_url: http://localhost:17442/
|
||||
ytdl_use_encryption: 'false'
|
||||
ytdl_cert_file_path: /etc/letsencrypt/live/example.com/fullchain.pem
|
||||
ytdl_key_file_path: /etc/letsencrypt/live/example.com/privkey.pem
|
||||
ytdl_base_path: http://localhost:17442/
|
||||
ytdl_audio_folder_path: audio/
|
||||
ytdl_video_folder_path: video/
|
||||
ytdl_title_top: Youtube Downloader
|
||||
ytdl_file_manager_enabled: 'true'
|
||||
ytdl_allow_quality_select: 'true'
|
||||
ytdl_download_only_mode: 'false'
|
||||
ytdl_allow_multi_download_mode: true
|
||||
ytdl_use_youtube_api: 'false'
|
||||
ytdl_youtube_api_key: 'false'
|
||||
ytdl_default_theme: default
|
||||
ytdl_allow_theme_change: 'true'
|
||||
ytdl_use_default_downloading_agent: 'true'
|
||||
ytdl_custom_downloading_agent: 'false'
|
||||
ytdl_allow_advanced_download: 'false'
|
||||
# do not touch this
|
||||
write_ytdl_config: 'true'
|
||||
ALLOW_CONFIG_MUTATIONS: 'true'
|
||||
restart: always
|
||||
ports:
|
||||
- "17442:17442"
|
||||
- "8998:80"
|
||||
image: tzahi12345/youtubedl-material:3.1
|
||||
39
docker_wrapper.sh
Normal file
39
docker_wrapper.sh
Normal file
@@ -0,0 +1,39 @@
|
||||
#!/bin/bash
|
||||
|
||||
cd backend
|
||||
|
||||
# Start the first process
|
||||
node app.js &
|
||||
status=$?
|
||||
if [ $status -ne 0 ]; then
|
||||
echo "Failed to start my_first_process: $status"
|
||||
exit $status
|
||||
fi
|
||||
|
||||
# Start the second process
|
||||
apachectl -DFOREGROUND
|
||||
status=$?
|
||||
if [ $status -ne 0 ]; then
|
||||
echo "Failed to start my_second_process: $status"
|
||||
exit $status
|
||||
fi
|
||||
|
||||
# Naive check runs checks once a minute to see if either of the processes exited.
|
||||
# This illustrates part of the heavy lifting you need to do if you want to run
|
||||
# more than one service in a container. The container will exit with an error
|
||||
# if it detects that either of the processes has exited.
|
||||
# Otherwise it will loop forever, waking up every 60 seconds
|
||||
|
||||
while /bin/true; do
|
||||
ps aux |grep node\ app.js # |grep -q -v grep
|
||||
PROCESS_1_STATUS=$?
|
||||
ps aux |grep apache2 # |grep -q -v grep
|
||||
PROCESS_2_STATUS=$?
|
||||
# If the greps above find anything, they will exit with 0 status
|
||||
# If they are not both 0, then something is wrong
|
||||
if [ $PROCESS_1_STATUS -ne 0 -o $PROCESS_2_STATUS -ne 0 ]; then
|
||||
echo "One of the processes has already exited."
|
||||
exit -1
|
||||
fi
|
||||
sleep 60
|
||||
done
|
||||
0
installer.py
Normal file
0
installer.py
Normal file
41
main.js
Normal file
41
main.js
Normal file
@@ -0,0 +1,41 @@
|
||||
const { app, BrowserWindow } = require('electron');
|
||||
const path = require('path');
|
||||
const url = require('url');
|
||||
|
||||
let win;
|
||||
|
||||
function createWindow() {
|
||||
win = new BrowserWindow({ width: 800, height: 600 });
|
||||
|
||||
// load the dist folder from Angular
|
||||
win.loadURL(
|
||||
url.format({
|
||||
pathname: path.join(__dirname, `/dist/index.html`),
|
||||
protocol: 'file:',
|
||||
slashes: true
|
||||
})
|
||||
);
|
||||
|
||||
// The following is optional and will open the DevTools:
|
||||
// win.webContents.openDevTools()
|
||||
|
||||
win.on('closed', () => {
|
||||
win = null;
|
||||
});
|
||||
}
|
||||
|
||||
app.on('ready', createWindow);
|
||||
|
||||
// on macOS, closing the window doesn't quit the app
|
||||
app.on('window-all-closed', () => {
|
||||
if (process.platform !== 'darwin') {
|
||||
app.quit();
|
||||
}
|
||||
});
|
||||
|
||||
// initialize the app's main window
|
||||
app.on('activate', () => {
|
||||
if (win === null) {
|
||||
createWindow();
|
||||
}
|
||||
});
|
||||
12768
package-lock.json
generated
Normal file
12768
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
14
package.json
14
package.json
@@ -8,7 +8,12 @@
|
||||
"build": "ng build",
|
||||
"test": "ng test",
|
||||
"lint": "ng lint",
|
||||
"e2e": "ng e2e"
|
||||
"e2e": "ng e2e",
|
||||
"electron": "ng build --base-href ./ && electron ."
|
||||
},
|
||||
"engines": {
|
||||
"node": "12.3.1",
|
||||
"npm": "6.10.3"
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
@@ -26,12 +31,16 @@
|
||||
"@angular/router": "^8.2.11",
|
||||
"core-js": "^2.4.1",
|
||||
"file-saver": "^2.0.2",
|
||||
"ng-lazyload-image": "^7.0.1",
|
||||
"ng4-configure": "^0.1.7",
|
||||
"ngx-content-loading": "^0.1.3",
|
||||
"rxjs": "^6.5.3",
|
||||
"rxjs-compat": "^6.0.0-rc.0",
|
||||
"tslib": "^1.10.0",
|
||||
"videogular2": "^7.0.1",
|
||||
"zone.js": "~0.9.1"
|
||||
"web-animations-js": "^2.3.2",
|
||||
"zone.js": "~0.9.1",
|
||||
"typescript": "~3.5.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-devkit/build-angular": "^0.803.24",
|
||||
@@ -43,6 +52,7 @@
|
||||
"@types/jasmine": "2.5.45",
|
||||
"@types/node": "~6.0.60",
|
||||
"codelyzer": "^5.0.1",
|
||||
"electron": "^8.0.1",
|
||||
"jasmine-core": "~2.6.2",
|
||||
"jasmine-spec-reporter": "~4.1.0",
|
||||
"karma": "~1.7.0",
|
||||
|
||||
15
src/_palette.scss
Normal file
15
src/_palette.scss
Normal file
@@ -0,0 +1,15 @@
|
||||
/* Coolors Exported Palette - coolors.co/e8aeb7-b8e1ff-a9fff7-94fbab-82aba1 */
|
||||
|
||||
/* HSL */
|
||||
$color1: hsla(351%, 56%, 80%, 1);
|
||||
$softblue: hsla(205%, 100%, 86%, 1);
|
||||
$color3: hsla(174%, 100%, 83%, 1);
|
||||
$color4: hsla(133%, 93%, 78%, 1);
|
||||
$color5: hsla(165%, 20%, 59%, 1);
|
||||
|
||||
/* RGB */
|
||||
$color1: rgba(232, 174, 183, 1);
|
||||
$softblue: rgba(184, 225, 255, 1);
|
||||
$color3: rgba(169, 255, 247, 1);
|
||||
$color4: rgba(148, 251, 171, 1);
|
||||
$color5: rgba(130, 171, 161, 1);
|
||||
@@ -1,15 +1,17 @@
|
||||
<mat-toolbar color="primary" class="top">
|
||||
<div class="flex-row" width="100%" height="100%">
|
||||
<div class="flex-column" style="text-align: left; margin-top: 1px;">
|
||||
<button (click)="goBack()" *ngIf="router.url.split(';')[0] === '/player'" mat-icon-button><mat-icon>arrow_back</mat-icon></button>
|
||||
<div [style.background]="postsService.theme ? postsService.theme.background_color : null" style="width: 100%; min-height: 100%;">
|
||||
<mat-toolbar color="primary" class="top">
|
||||
<div class="flex-row" width="100%" height="100%">
|
||||
<div class="flex-column" style="text-align: left; margin-top: 1px;">
|
||||
<button (click)="goBack()" *ngIf="router.url.split(';')[0] === '/player'" mat-icon-button><mat-icon>arrow_back</mat-icon></button>
|
||||
</div>
|
||||
<div class="flex-column" style="text-align: center; margin-top: 5px;">
|
||||
<div>{{topBarTitle}}</div>
|
||||
</div>
|
||||
<div class="flex-column" style="text-align: right; align-items: flex-end;">
|
||||
<button *ngIf="allowThemeChange" mat-icon-button (click)="flipTheme()"><mat-icon>{{(postsService.theme.key === 'default') ? 'brightness_5' : 'brightness_2'}}</mat-icon></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-column" style="text-align: center; margin-top: 5px;">
|
||||
<div>{{topBarTitle}}</div>
|
||||
</div>
|
||||
<div class="flex-column" style="text-align: right">
|
||||
</mat-toolbar>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</mat-toolbar>
|
||||
|
||||
<router-outlet></router-outlet>
|
||||
<router-outlet></router-outlet>
|
||||
</div>
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Component, OnInit, ElementRef, ViewChild } from '@angular/core';
|
||||
import { Component, OnInit, ElementRef, ViewChild, HostBinding } from '@angular/core';
|
||||
import {PostsService} from './posts.services';
|
||||
import {FileCardComponent} from './file-card/file-card.component';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
@@ -16,6 +16,8 @@ import 'rxjs/add/operator/do'
|
||||
import 'rxjs/add/operator/switch'
|
||||
import { YoutubeSearchService, Result } from './youtube-search.service';
|
||||
import { Router } from '@angular/router';
|
||||
import { OverlayContainer } from '@angular/cdk/overlay';
|
||||
import { THEMES_CONFIG } from '../themes';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
@@ -23,56 +25,94 @@ import { Router } from '@angular/router';
|
||||
styleUrls: ['./app.component.css']
|
||||
})
|
||||
export class AppComponent implements OnInit {
|
||||
iOS = false;
|
||||
|
||||
determinateProgress = false;
|
||||
downloadingfile = false;
|
||||
audioOnly: boolean;
|
||||
urlError = false;
|
||||
path = '';
|
||||
url = '';
|
||||
exists = '';
|
||||
@HostBinding('class') componentCssClass;
|
||||
THEMES_CONFIG = THEMES_CONFIG;
|
||||
|
||||
// config items
|
||||
topBarTitle = 'Youtube Downloader';
|
||||
percentDownloaded: number;
|
||||
fileManagerEnabled = false;
|
||||
downloadOnlyMode = false;
|
||||
baseStreamPath;
|
||||
audioFolderPath;
|
||||
videoFolderPath;
|
||||
|
||||
// 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]);
|
||||
defaultTheme = null;
|
||||
allowThemeChange = null;
|
||||
|
||||
@ViewChild('urlinput', { read: ElementRef, static: false }) urlInput: ElementRef;
|
||||
|
||||
constructor(private postsService: PostsService, private youtubeSearch: YoutubeSearchService, public snackBar: MatSnackBar,
|
||||
public router: Router) {
|
||||
this.audioOnly = false;
|
||||
|
||||
constructor(public postsService: PostsService, public snackBar: MatSnackBar,
|
||||
public router: Router, public overlayContainer: OverlayContainer, private elementRef: ElementRef) {
|
||||
|
||||
// loading config
|
||||
this.postsService.loadNavItems().subscribe(result => { // loads settings
|
||||
this.topBarTitle = result['YoutubeDLMaterial']['Extra']['title_top'];
|
||||
const themingExists = result['YoutubeDLMaterial']['Themes'];
|
||||
this.defaultTheme = themingExists ? result['YoutubeDLMaterial']['Themes']['default_theme'] : 'default';
|
||||
this.allowThemeChange = themingExists ? result['YoutubeDLMaterial']['Themes']['allow_theme_change'] : true;
|
||||
|
||||
// sets theme to config default if it doesn't exist
|
||||
if (!localStorage.getItem('theme')) {
|
||||
this.setTheme(themingExists ? this.defaultTheme : 'default');
|
||||
}
|
||||
}, error => {
|
||||
console.log(error);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
|
||||
// theme stuff
|
||||
|
||||
setTheme(theme) {
|
||||
// theme is registered, so set it to the stored cookie variable
|
||||
let old_theme = null;
|
||||
if (this.THEMES_CONFIG[theme]) {
|
||||
if (localStorage.getItem('theme')) {
|
||||
old_theme = localStorage.getItem('theme');
|
||||
if (!this.THEMES_CONFIG[old_theme]) {
|
||||
console.log('bad theme found, setting to default');
|
||||
if (this.defaultTheme === null) {
|
||||
// means it hasn't loaded yet
|
||||
console.error('No default theme detected');
|
||||
} else {
|
||||
localStorage.setItem('theme', this.defaultTheme);
|
||||
old_theme = localStorage.getItem('theme'); // updates old_theme
|
||||
}
|
||||
}
|
||||
}
|
||||
localStorage.setItem('theme', theme);
|
||||
this.elementRef.nativeElement.ownerDocument.body.style.backgroundColor = this.THEMES_CONFIG[theme]['background_color'];
|
||||
} else {
|
||||
console.error('Invalid theme: ' + theme);
|
||||
return;
|
||||
}
|
||||
|
||||
this.postsService.setTheme(theme);
|
||||
|
||||
this.onSetTheme(this.THEMES_CONFIG[theme]['css_label'], old_theme ? this.THEMES_CONFIG[old_theme]['css_label'] : old_theme);
|
||||
}
|
||||
|
||||
onSetTheme(theme, old_theme) {
|
||||
if (old_theme) {
|
||||
document.body.classList.remove(old_theme);
|
||||
this.overlayContainer.getContainerElement().classList.remove(old_theme);
|
||||
}
|
||||
this.overlayContainer.getContainerElement().classList.add(theme);
|
||||
this.componentCssClass = theme;
|
||||
}
|
||||
|
||||
flipTheme() {
|
||||
if (this.postsService.theme.key === 'default') {
|
||||
this.setTheme('dark');
|
||||
} else if (this.postsService.theme.key === 'dark') {
|
||||
this.setTheme('default');
|
||||
}
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
if (localStorage.getItem('theme')) {
|
||||
this.setTheme(localStorage.getItem('theme'));
|
||||
} else {
|
||||
//
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
goBack() {
|
||||
this.router.navigate(['/home']);
|
||||
}
|
||||
|
||||
@@ -3,16 +3,16 @@ import { NgModule } from '@angular/core';
|
||||
import {MatNativeDateModule, MatRadioModule, MatInputModule, MatButtonModule, MatSidenavModule, MatIconModule, MatListModule,
|
||||
MatSnackBarModule, MatCardModule, MatSelectModule, MatToolbarModule, MatCheckboxModule, MatGridListModule,
|
||||
MatProgressBarModule, MatExpansionModule,
|
||||
MatGridList,
|
||||
MatProgressSpinnerModule,
|
||||
MatButtonToggleModule} from '@angular/material';
|
||||
MatButtonToggleModule,
|
||||
MatDialogModule} from '@angular/material';
|
||||
import {DragDropModule} from '@angular/cdk/drag-drop';
|
||||
import {FormsModule, ReactiveFormsModule} from '@angular/forms';
|
||||
import { AppComponent } from './app.component';
|
||||
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
|
||||
import { HttpModule } from '@angular/http';
|
||||
import { HttpClientModule, HttpClient } from '@angular/common/http';
|
||||
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';
|
||||
@@ -22,13 +22,26 @@ import {VgCoreModule} from 'videogular2/compiled/core';
|
||||
import {VgControlsModule} from 'videogular2/compiled/controls';
|
||||
import {VgOverlayPlayModule} from 'videogular2/compiled/overlay-play';
|
||||
import {VgBufferingModule} from 'videogular2/compiled/buffering';
|
||||
import { InputDialogComponent } from './input-dialog/input-dialog.component';
|
||||
import { LazyLoadImageModule, IsVisibleProps } from 'ng-lazyload-image';
|
||||
import { NgxContentLoadingModule } from 'ngx-content-loading';
|
||||
import { audioFilesMouseHovering, videoFilesMouseHovering, audioFilesOpened, videoFilesOpened } from './main/main.component';
|
||||
import { CreatePlaylistComponent } from './create-playlist/create-playlist.component';
|
||||
import { DownloadItemComponent } from './download-item/download-item.component';
|
||||
|
||||
export function isVisible({ event, element, scrollContainer, offset }: IsVisibleProps<any>) {
|
||||
return (element.id === 'video' ? videoFilesMouseHovering || videoFilesOpened : audioFilesMouseHovering || audioFilesOpened);
|
||||
}
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AppComponent,
|
||||
FileCardComponent,
|
||||
MainComponent,
|
||||
PlayerComponent
|
||||
PlayerComponent,
|
||||
InputDialogComponent,
|
||||
CreatePlaylistComponent,
|
||||
DownloadItemComponent
|
||||
],
|
||||
imports: [
|
||||
BrowserModule,
|
||||
@@ -54,12 +67,20 @@ import {VgBufferingModule} from 'videogular2/compiled/buffering';
|
||||
MatProgressBarModule,
|
||||
MatProgressSpinnerModule,
|
||||
MatButtonToggleModule,
|
||||
MatDialogModule,
|
||||
DragDropModule,
|
||||
VgCoreModule,
|
||||
VgControlsModule,
|
||||
VgOverlayPlayModule,
|
||||
VgBufferingModule,
|
||||
LazyLoadImageModule.forRoot({ isVisible }),
|
||||
NgxContentLoadingModule,
|
||||
RouterModule,
|
||||
AppRoutingModule
|
||||
AppRoutingModule,
|
||||
],
|
||||
entryComponents: [
|
||||
InputDialogComponent,
|
||||
CreatePlaylistComponent
|
||||
],
|
||||
providers: [PostsService],
|
||||
bootstrap: [AppComponent]
|
||||
|
||||
19
src/app/create-playlist/create-playlist.component.html
Normal file
19
src/app/create-playlist/create-playlist.component.html
Normal file
@@ -0,0 +1,19 @@
|
||||
<h4 mat-dialog-title>Create a playlist</h4>
|
||||
<form>
|
||||
<div>
|
||||
<mat-form-field color="accent">
|
||||
<input [(ngModel)]="name" matInput placeholder="Name" type="text" required aria-required [ngModelOptions]="{standalone: true}">
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div>
|
||||
<mat-form-field color="accent">
|
||||
<mat-label>{{(type === 'audio') ? 'Audio files' : 'Videos'}}</mat-label>
|
||||
<mat-select [formControl]="filesSelect" multiple required aria-required>
|
||||
<mat-option *ngFor="let file of filesToSelectFrom" [value]="file.id">{{file.id}}</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div *ngIf="create_in_progress" style="float: left"><mat-spinner [diameter]="25"></mat-spinner></div>
|
||||
<button (click)="createPlaylist()" [disabled]="!name || !filesSelect.value || filesSelect.value.length === 0" color="primary" style="float: right" mat-flat-button>Create</button>
|
||||
25
src/app/create-playlist/create-playlist.component.spec.ts
Normal file
25
src/app/create-playlist/create-playlist.component.spec.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { CreatePlaylistComponent } from './create-playlist.component';
|
||||
|
||||
describe('CreatePlaylistComponent', () => {
|
||||
let component: CreatePlaylistComponent;
|
||||
let fixture: ComponentFixture<CreatePlaylistComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ CreatePlaylistComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(CreatePlaylistComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
58
src/app/create-playlist/create-playlist.component.ts
Normal file
58
src/app/create-playlist/create-playlist.component.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { Component, OnInit, Inject } from '@angular/core';
|
||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material';
|
||||
import { FormControl } from '@angular/forms';
|
||||
import { PostsService } from 'app/posts.services';
|
||||
|
||||
@Component({
|
||||
selector: 'app-create-playlist',
|
||||
templateUrl: './create-playlist.component.html',
|
||||
styleUrls: ['./create-playlist.component.scss']
|
||||
})
|
||||
export class CreatePlaylistComponent implements OnInit {
|
||||
// really "createPlaylistDialogComponent"
|
||||
|
||||
filesToSelectFrom = null;
|
||||
type = null;
|
||||
filesSelect = new FormControl();
|
||||
name = '';
|
||||
|
||||
create_in_progress = false;
|
||||
|
||||
constructor(@Inject(MAT_DIALOG_DATA) public data: any,
|
||||
private postsService: PostsService,
|
||||
public dialogRef: MatDialogRef<CreatePlaylistComponent>) { }
|
||||
|
||||
|
||||
ngOnInit() {
|
||||
if (this.data) {
|
||||
this.filesToSelectFrom = this.data.filesToSelectFrom;
|
||||
this.type = this.data.type;
|
||||
}
|
||||
}
|
||||
|
||||
createPlaylist() {
|
||||
const thumbnailURL = this.getThumbnailURL();
|
||||
this.create_in_progress = true;
|
||||
this.postsService.createPlaylist(this.name, this.filesSelect.value, this.type, thumbnailURL).subscribe(res => {
|
||||
this.create_in_progress = false;
|
||||
if (res['success']) {
|
||||
this.dialogRef.close(true);
|
||||
} else {
|
||||
this.dialogRef.close(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getThumbnailURL() {
|
||||
for (let i = 0; i < this.filesToSelectFrom.length; i++) {
|
||||
const file = this.filesToSelectFrom[i];
|
||||
if (file.id === this.filesSelect.value[0]) {
|
||||
// different services store the thumbnail in different places
|
||||
if (file.thumbnailURL) { return file.thumbnailURL };
|
||||
if (file.thumbnail) { return file.thumbnail };
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
16
src/app/download-item/download-item.component.html
Normal file
16
src/app/download-item/download-item.component.html
Normal file
@@ -0,0 +1,16 @@
|
||||
<div>
|
||||
<mat-grid-list [rowHeight]="50" [cols]="24">
|
||||
<mat-grid-tile [colspan]="2">
|
||||
<h5 style="display: inline-block; margin-right: 5px; position: relative; top: 5px;">{{queueNumber}}.</h5>
|
||||
</mat-grid-tile>
|
||||
<mat-grid-tile [colspan]="6">
|
||||
<div style="display: inline-block; text-align: center;">ID: {{url_id}}</div>
|
||||
</mat-grid-tile>
|
||||
<mat-grid-tile [colspan]="13">
|
||||
<mat-progress-bar style="width: 80%" [value]="download.percent_complete" [mode]="(download.percent_complete === 0) ? 'indeterminate' : 'determinate'"></mat-progress-bar>
|
||||
</mat-grid-tile>
|
||||
<mat-grid-tile [colspan]="3">
|
||||
<button (click)="cancelTheDownload()" mat-icon-button color="warn"><mat-icon fontSet="material-icons-outlined">cancel</mat-icon></button>
|
||||
</mat-grid-tile>
|
||||
</mat-grid-list>
|
||||
</div>
|
||||
0
src/app/download-item/download-item.component.scss
Normal file
0
src/app/download-item/download-item.component.scss
Normal file
25
src/app/download-item/download-item.component.spec.ts
Normal file
25
src/app/download-item/download-item.component.spec.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { DownloadItemComponent } from './download-item.component';
|
||||
|
||||
describe('DownloadItemComponent', () => {
|
||||
let component: DownloadItemComponent;
|
||||
let fixture: ComponentFixture<DownloadItemComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ DownloadItemComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(DownloadItemComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
41
src/app/download-item/download-item.component.ts
Normal file
41
src/app/download-item/download-item.component.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { Component, OnInit, Output, EventEmitter, Input } from '@angular/core';
|
||||
import { Download } from 'app/main/main.component';
|
||||
|
||||
|
||||
@Component({
|
||||
selector: 'app-download-item',
|
||||
templateUrl: './download-item.component.html',
|
||||
styleUrls: ['./download-item.component.scss']
|
||||
})
|
||||
export class DownloadItemComponent implements OnInit {
|
||||
|
||||
@Input() download: Download = {
|
||||
uid: null,
|
||||
type: 'audio',
|
||||
percent_complete: 0,
|
||||
url: 'http://youtube.com/watch?v=17848rufj',
|
||||
downloading: true,
|
||||
is_playlist: false
|
||||
};
|
||||
@Output() cancelDownload = new EventEmitter<Download>();
|
||||
|
||||
@Input() queueNumber = null;
|
||||
|
||||
url_id = null;
|
||||
|
||||
constructor() { }
|
||||
|
||||
ngOnInit() {
|
||||
if (this.download && this.download.url && this.download.url.includes('youtube')) {
|
||||
const string_id = (this.download.is_playlist ? '?list=' : '?v=')
|
||||
const index_offset = (this.download.is_playlist ? 6 : 3);
|
||||
const end_index = this.download.url.indexOf(string_id) + index_offset;
|
||||
this.url_id = this.download.url.substring(end_index, this.download.url.length);
|
||||
}
|
||||
}
|
||||
|
||||
cancelTheDownload() {
|
||||
this.cancelDownload.emit(this.download);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -30,4 +30,30 @@
|
||||
margin: 0 auto;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
}
|
||||
|
||||
.img-div {
|
||||
max-height: 80px;
|
||||
padding: 0px;
|
||||
margin: 0px 0px 0px -5px;
|
||||
width: calc(100% + 5px + 5px);
|
||||
}
|
||||
|
||||
.max-two-lines {
|
||||
display: -webkit-box;
|
||||
display: -moz-box;
|
||||
max-height: 2.4em;
|
||||
line-height: 1.2em;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
}
|
||||
|
||||
@media (max-width: 576px){
|
||||
|
||||
.example-card {
|
||||
width: 125px !important;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,13 +1,17 @@
|
||||
<mat-card class="example-card">
|
||||
<button (click)="deleteFile()" class="deleteButton" mat-icon-button><mat-icon>delete_forever</mat-icon></button>
|
||||
<mat-card class="example-card mat-elevation-z6">
|
||||
<div style="padding:5px">
|
||||
<b><a href="javascript:void(0)" (click)="mainComponent.goToFile(name, isAudio)">{{title}}</a></b>
|
||||
<b><a href="javascript:void(0)" (click)="!isPlaylist ? mainComponent.goToFile(name, isAudio) : mainComponent.goToPlaylist(name, type)">{{title}}</a></b>
|
||||
<br/>
|
||||
ID: {{name}}
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="centered example-full-width-height"><img class="image" src="{{thumbnailURL}}" alt="Thumbnail"></div>
|
||||
|
||||
<span class="max-two-lines">ID: {{name}}</span>
|
||||
<div *ngIf="isPlaylist">Count: {{count}}</div>
|
||||
<div *ngIf="!image_errored && thumbnailURL" class="img-div">
|
||||
<img class="image" (error) ="onImgError($event)" [id]="type" [lazyLoad]="thumbnailURL" [customObservable]="scrollAndLoad" (onLoad)="imageLoaded($event)" alt="Thumbnail">
|
||||
<span *ngIf="!image_loaded">
|
||||
<ngx-content-loading [width]="500" [height]="360">
|
||||
<svg:g ngx-rect width="500" height="360" y="0" x="0" rx="4" ry="4"></svg:g>
|
||||
</ngx-content-loading>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<button (click)="deleteFile()" class="deleteButton" mat-icon-button><mat-icon>delete_forever</mat-icon></button>
|
||||
</mat-card>
|
||||
|
||||
@@ -3,6 +3,8 @@ import {PostsService} from '../posts.services';
|
||||
import {MatSnackBar} from '@angular/material';
|
||||
import {EventEmitter} from '@angular/core';
|
||||
import { MainComponent } from 'app/main/main.component';
|
||||
import { Subject, Observable } from 'rxjs';
|
||||
import 'rxjs/add/observable/merge';
|
||||
|
||||
@Component({
|
||||
selector: 'app-file-card',
|
||||
@@ -17,21 +19,53 @@ export class FileCardComponent implements OnInit {
|
||||
@Input() thumbnailURL: string;
|
||||
@Input() isAudio = true;
|
||||
@Output() removeFile: EventEmitter<string> = new EventEmitter<string>();
|
||||
@Input() isPlaylist = false;
|
||||
@Input() count = null;
|
||||
type;
|
||||
image_loaded = false;
|
||||
image_errored = false;
|
||||
|
||||
constructor(private postsService: PostsService, public snackBar: MatSnackBar, public mainComponent: MainComponent) { }
|
||||
scrollSubject;
|
||||
scrollAndLoad;
|
||||
|
||||
constructor(private postsService: PostsService, public snackBar: MatSnackBar, public mainComponent: MainComponent) {
|
||||
this.scrollSubject = new Subject();
|
||||
this.scrollAndLoad = Observable.merge(
|
||||
Observable.fromEvent(window, 'scroll'),
|
||||
this.scrollSubject
|
||||
);
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.type = this.isAudio ? 'audio' : 'video';
|
||||
}
|
||||
|
||||
deleteFile() {
|
||||
this.postsService.deleteFile(this.name, this.isAudio).subscribe(result => {
|
||||
if (result === true) {
|
||||
this.openSnackBar('Delete success!', 'OK.');
|
||||
this.removeFile.emit(this.name);
|
||||
} else {
|
||||
this.openSnackBar('Delete failed!', 'OK.');
|
||||
}
|
||||
});
|
||||
if (!this.isPlaylist) {
|
||||
this.postsService.deleteFile(this.name, this.isAudio).subscribe(result => {
|
||||
if (result === true) {
|
||||
this.openSnackBar('Delete success!', 'OK.');
|
||||
this.removeFile.emit(this.name);
|
||||
} else {
|
||||
this.openSnackBar('Delete failed!', 'OK.');
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.removeFile.emit(this.name);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
onImgError(event) {
|
||||
this.image_errored = true;
|
||||
}
|
||||
|
||||
onHoverResponse() {
|
||||
this.scrollSubject.next();
|
||||
}
|
||||
|
||||
imageLoaded(loaded) {
|
||||
this.image_loaded = true;
|
||||
}
|
||||
|
||||
public openSnackBar(message: string, action: string) {
|
||||
|
||||
3
src/app/input-dialog/input-dialog.component.css
Normal file
3
src/app/input-dialog/input-dialog.component.css
Normal file
@@ -0,0 +1,3 @@
|
||||
.mat-spinner {
|
||||
margin-left: 5%;
|
||||
}
|
||||
16
src/app/input-dialog/input-dialog.component.html
Normal file
16
src/app/input-dialog/input-dialog.component.html
Normal file
@@ -0,0 +1,16 @@
|
||||
<h4 mat-dialog-title>{{inputTitle}}</h4>
|
||||
<mat-dialog-content>
|
||||
<div>
|
||||
<mat-form-field color="accent">
|
||||
<input matInput (keyup.enter)="enterPressed()" [(ngModel)]="inputText" [placeholder]="inputPlaceholder">
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</mat-dialog-content>
|
||||
<mat-dialog-actions>
|
||||
<button mat-button mat-dialog-close>Cancel</button>
|
||||
<!-- The mat-dialog-close directive optionally accepts a value as a result for the dialog. -->
|
||||
<button mat-button [disabled]="!inputText" type="submit" (click)="enterPressed()">{{submitText}}</button>
|
||||
<div class="mat-spinner" *ngIf="inputSubmitted">
|
||||
<mat-spinner [diameter]="25"></mat-spinner>
|
||||
</div>
|
||||
</mat-dialog-actions>
|
||||
25
src/app/input-dialog/input-dialog.component.spec.ts
Normal file
25
src/app/input-dialog/input-dialog.component.spec.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { InputDialogComponent } from './input-dialog.component';
|
||||
|
||||
describe('InputDialogComponent', () => {
|
||||
let component: InputDialogComponent;
|
||||
let fixture: ComponentFixture<InputDialogComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ InputDialogComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(InputDialogComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
50
src/app/input-dialog/input-dialog.component.ts
Normal file
50
src/app/input-dialog/input-dialog.component.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { Component, OnInit, Input, Inject, EventEmitter } from '@angular/core';
|
||||
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material';
|
||||
|
||||
@Component({
|
||||
selector: 'app-input-dialog',
|
||||
templateUrl: './input-dialog.component.html',
|
||||
styleUrls: ['./input-dialog.component.css']
|
||||
})
|
||||
export class InputDialogComponent implements OnInit {
|
||||
|
||||
inputTitle: string;
|
||||
inputPlaceholder: string;
|
||||
submitText: string;
|
||||
|
||||
inputText = '';
|
||||
|
||||
inputSubmitted = false;
|
||||
|
||||
doneEmitter: EventEmitter<any> = null;
|
||||
onlyEmitOnDone = false;
|
||||
|
||||
constructor(public dialogRef: MatDialogRef<InputDialogComponent>,
|
||||
@Inject(MAT_DIALOG_DATA) public data: any) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.inputTitle = this.data.inputTitle;
|
||||
this.inputPlaceholder = this.data.inputPlaceholder;
|
||||
this.submitText = this.data.submitText;
|
||||
|
||||
// checks if emitter exists, if so don't autoclose as it should be handled by caller
|
||||
if (this.data.doneEmitter) {
|
||||
this.doneEmitter = this.data.doneEmitter;
|
||||
this.onlyEmitOnDone = true;
|
||||
}
|
||||
}
|
||||
|
||||
enterPressed() {
|
||||
// validates input -- TODO: add custom validator
|
||||
if (this.inputText) {
|
||||
// only emit if emitter is passed
|
||||
if (this.onlyEmitOnDone) {
|
||||
this.doneEmitter.emit(this.inputText);
|
||||
this.inputSubmitted = true;
|
||||
} else {
|
||||
this.dialogRef.close(this.inputText);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -81,4 +81,44 @@ mat-form-field.mat-form-field {
|
||||
.margined {
|
||||
margin-left: 20px;
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
.results-div {
|
||||
position: relative;
|
||||
top: -15px;
|
||||
}
|
||||
|
||||
.first-result-card {
|
||||
border-radius: 4px 4px 0px 0px !important;
|
||||
}
|
||||
|
||||
.last-result-card {
|
||||
border-radius: 0px 0px 4px 4px !important;
|
||||
}
|
||||
|
||||
.only-result-card {
|
||||
border-radius: 4px !important;
|
||||
}
|
||||
|
||||
.result-card {
|
||||
height: 120px;
|
||||
border-radius: 0px;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
.download-progress-bar {
|
||||
z-index: 999;
|
||||
position: absolute;
|
||||
bottom: 0px;
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
.add-playlist-button {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.advanced-input {
|
||||
width: 100%;
|
||||
margin-top: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
@@ -9,59 +9,101 @@
|
||||
<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>
|
||||
<div [ngClass]="allowQualitySelect ? 'col-sm-9' : null" class="col-12">
|
||||
<mat-form-field color="accent" class="example-full-width">
|
||||
<input style="padding-right: 25px;" matInput (ngModelChange)="inputChanged($event)" [(ngModel)]="url" [placeholder]="'URL' + (youtubeSearchEnabled ? ' or search' : '')" type="url" name="url" [formControl]="urlForm" required #urlinput>
|
||||
<mat-error *ngIf="urlError || urlForm.invalid">Please enter a valid URL!</mat-error>
|
||||
<button class="input-clear-button" mat-icon-button (click)="clearInput()"><mat-icon>clear</mat-icon></button>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="col-7 col-sm-3">
|
||||
<mat-form-field style="display: inline-block; width: inherit; min-width: 120px;">
|
||||
<div *ngIf="allowQualitySelect" class="col-7 col-sm-3">
|
||||
<mat-form-field color="accent" style="display: inline-block; width: inherit; min-width: 120px;">
|
||||
<mat-label>Quality</mat-label>
|
||||
<mat-select [ngModelOptions]="{standalone: true}" [(ngModel)]="selectedQuality">
|
||||
<ng-container *ngFor="let option of qualityOptions[(audioOnly) ? 'audio' : 'video']">
|
||||
<mat-option *ngIf="option.value === '' || url && cachedAvailableFormats[url] && cachedAvailableFormats[url] && cachedAvailableFormats[url][(audioOnly) ? 'audio' : 'video'][option.value]" [value]="option.value">
|
||||
<mat-option *ngIf="option.value === '' || url && cachedAvailableFormats[url] && cachedAvailableFormats[url]['formats'] && cachedAvailableFormats[url]['formats'][(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]">
|
||||
<div class="spinner-div" *ngIf="url !== '' && cachedAvailableFormats[url] && cachedAvailableFormats[url]['formats_loading']">
|
||||
<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="results-div" *ngIf="results_showing">
|
||||
<span *ngFor="let result of results; let i = index">
|
||||
<mat-card class="result-card mat-elevation-z7" [ngClass]="[(i === 0 && results.length > 1) ? 'first-result-card' : '', ((i === results.length-1) && results.length > 1) ? 'last-result-card' : '', (results.length === 1) ? 'only-result-card' : '']">
|
||||
<div class="search-card-title">
|
||||
{{result.title}}
|
||||
</div>
|
||||
<div style="font-size: 12px">
|
||||
<div style="font-size: 12px; margin-bottom: 10px;">
|
||||
{{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>
|
||||
</div>
|
||||
</form>
|
||||
<br/>
|
||||
<mat-checkbox (change)="videoModeChanged($event)" [(ngModel)]="audioOnly" style="float: left; margin-top: -12px">Only Audio</mat-checkbox>
|
||||
<mat-checkbox [disabled]="current_download" (change)="videoModeChanged($event)" [(ngModel)]="audioOnly" style="float: left; margin-top: -12px">Only Audio</mat-checkbox>
|
||||
<mat-checkbox *ngIf="allowMultiDownloadMode" [disabled]="current_download" (change)="multiDownloadModeChanged($event)" [(ngModel)]="multiDownloadMode" style="float: right; margin-top: -12px">Multi-download mode</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>
|
||||
color="accent">Download</button>
|
||||
<button (click)="cancelDownload()" style="float: right" *ngIf="!!current_download" mat-stroked-button color="warn">Cancel</button>
|
||||
</mat-card-actions>
|
||||
</mat-card>
|
||||
</div>
|
||||
<div *ngIf="false && allowAdvancedDownload" class="big demo-basic">
|
||||
<form style="margin-left: 20px; margin-right: 20px;">
|
||||
<mat-expansion-panel class="big">
|
||||
<mat-expansion-panel-header>
|
||||
<mat-panel-title>
|
||||
Advanced
|
||||
</mat-panel-title>
|
||||
</mat-expansion-panel-header>
|
||||
<div class="container" style="padding-bottom: 20px;">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<mat-checkbox color="accent" [disabled]="current_download" (change)="customArgsEnabledChanged($event)" [(ngModel)]="customArgsEnabled" style="position: absolute; z-index: 999" [ngModelOptions]="{standalone: true}">Use custom args</mat-checkbox>
|
||||
<mat-form-field color="accent" class="advanced-input">
|
||||
<input [(ngModel)]="customArgs" [ngModelOptions]="{standalone: true}" [disabled]="!customArgsEnabled" matInput placeholder="Custom args">
|
||||
<mat-hint>No need to include URL, just everything after.</mat-hint>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="col">
|
||||
<mat-checkbox color="accent" [disabled]="current_download" (change)="customOutputEnabledChanged($event)" [(ngModel)]="customOutputEnabled" style="position: absolute; z-index: 999" [ngModelOptions]="{standalone: true}">Use custom output</mat-checkbox>
|
||||
<mat-form-field color="accent" class="advanced-input">
|
||||
<input [(ngModel)]="customOutput" [ngModelOptions]="{standalone: true}" [disabled]="!customOutputEnabled" matInput placeholder="Custom output">
|
||||
<mat-hint><a target="_blank" href="https://github.com/ytdl-org/youtube-dl/blob/master/README.md#output-template">This link</a> will be helpful. Path is relative to the config download path.</mat-hint>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</mat-expansion-panel>
|
||||
</form>
|
||||
</div>
|
||||
<div *ngIf="multiDownloadMode && downloads.length > 0 && !current_download" style="margin-top: 15px;" class="big demo-basic">
|
||||
<mat-card id="card" style="margin-right: 20px; margin-left: 20px;">
|
||||
<div class="container">
|
||||
<div *ngFor="let download of downloads; let i = index;" class="row">
|
||||
<ng-container *ngIf="current_download !== download">
|
||||
<app-download-item style="width: 100%" [download]="download" [queueNumber]="i+1" (cancelDownload)="cancelDownload($event)"></app-download-item>
|
||||
<mat-divider style="position: relative" *ngIf="i !== downloads.length - 1"></mat-divider>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
</mat-card>
|
||||
</div>
|
||||
<br/>
|
||||
<div class="centered big" id="bar_div" *ngIf="downloadingfile;else nofile">
|
||||
<div class="centered big" id="bar_div" *ngIf="current_download && current_download.downloading; 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>
|
||||
@@ -81,7 +123,7 @@
|
||||
</ng-template>
|
||||
<div style="margin: 20px" *ngIf="fileManagerEnabled">
|
||||
<mat-accordion>
|
||||
<mat-expansion-panel class="big">
|
||||
<mat-expansion-panel (opened)="accordionOpened('audio')" (closed)="accordionClosed('audio')" (mouseleave)="accordionLeft('audio')" (mouseenter)="accordionEntered('audio')" class="big">
|
||||
<mat-expansion-panel-header>
|
||||
<mat-panel-title>
|
||||
Audio
|
||||
@@ -91,16 +133,32 @@
|
||||
</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-list style="margin-bottom: 15px;" (window:resize)="onResize($event)" [cols]="files_cols" rowHeight="150px">
|
||||
<mat-grid-tile *ngFor="let file of mp3s; index as i;">
|
||||
<app-file-card (removeFile)="removeFromMp3($event)" [title]="file.title" [name]="file.id" [thumbnailURL]="file.thumbnailURL"
|
||||
<app-file-card #audiofilecard (removeFile)="removeFromMp3($event)" [title]="file.title" [name]="file.id" [thumbnailURL]="file.thumbnailURL"
|
||||
[length]="file.duration" [isAudio]="true"></app-file-card>
|
||||
<mat-progress-bar *ngIf="downloading_content['audio'][file.id]" class="download-progress-bar" mode="indeterminate"></mat-progress-bar>
|
||||
</mat-grid-tile>
|
||||
</mat-grid-list>
|
||||
<mat-divider></mat-divider>
|
||||
<div style="width: 100%; text-align: center; margin-top: 10px;">
|
||||
<h6>Playlists</h6>
|
||||
</div>
|
||||
<mat-grid-list *ngIf="playlists.audio.length > 0" (window:resize)="onResize($event)" [cols]="files_cols" rowHeight="150px">
|
||||
<mat-grid-tile *ngFor="let playlist of playlists.audio; let i = index;">
|
||||
<app-file-card #audiofilecard (removeFile)="removePlaylistMp3(playlist.id, i)" [title]="playlist.name" [name]="playlist.id" [thumbnailURL]="playlist_thumbnails[playlist.id]"
|
||||
[length]="null" [isAudio]="true" [isPlaylist]="true" [count]="playlist.fileNames.length"></app-file-card>
|
||||
<mat-progress-bar *ngIf="downloading_content['audio'][playlist.id]" class="download-progress-bar" mode="indeterminate"></mat-progress-bar>
|
||||
</mat-grid-tile>
|
||||
</mat-grid-list>
|
||||
<div class="add-playlist-button"><button (click)="openCreatePlaylistDialog('audio')" mat-fab><mat-icon>add</mat-icon></button></div>
|
||||
<div *ngIf="playlists.audio.length === 0">
|
||||
No playlists available. Create one from your downloading audio files by clicking the blue plus button.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</mat-expansion-panel>
|
||||
<mat-expansion-panel class="big">
|
||||
<mat-expansion-panel (opened)="accordionOpened('video')" (closed)="accordionClosed('video')" (mouseleave)="accordionLeft('video')" (mouseenter)="accordionEntered('video')" class="big">
|
||||
<mat-expansion-panel-header>
|
||||
<mat-panel-title>
|
||||
Video
|
||||
@@ -110,12 +168,31 @@
|
||||
</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-list style="margin-bottom: 15px;" (window:resize)="onResize($event)" [cols]="files_cols" rowHeight="150px">
|
||||
<mat-grid-tile *ngFor="let file of mp4s; index as i;">
|
||||
<app-file-card (removeFile)="removeFromMp4($event)" [title]="file.title" [name]="file.id" [thumbnailURL]="file.thumbnailURL"
|
||||
<app-file-card #videofilecard (removeFile)="removeFromMp4($event)" [title]="file.title" [name]="file.id" [thumbnailURL]="file.thumbnailURL"
|
||||
[length]="file.duration" [isAudio]="false"></app-file-card>
|
||||
<mat-progress-bar *ngIf="downloading_content['video'][file.id]" class="download-progress-bar" mode="indeterminate"></mat-progress-bar>
|
||||
</mat-grid-tile>
|
||||
</mat-grid-list>
|
||||
<mat-divider></mat-divider>
|
||||
|
||||
<div style="width: 100%; text-align: center; margin-top: 10px;">
|
||||
<h6>Playlists</h6>
|
||||
</div>
|
||||
<mat-grid-list *ngIf="playlists.video.length > 0" (window:resize)="onResize($event)" [cols]="files_cols" rowHeight="150px">
|
||||
<mat-grid-tile *ngFor="let playlist of playlists.video; let i = index;">
|
||||
<app-file-card #videofilecard (removeFile)="removePlaylistMp4(playlist.id, i)" [title]="playlist.name" [name]="playlist.id" [thumbnailURL]="playlist_thumbnails[playlist.id]"
|
||||
[length]="null" [isAudio]="false" [isPlaylist]="true" [count]="playlist.fileNames.length"></app-file-card>
|
||||
<mat-progress-bar *ngIf="downloading_content['video'][playlist.id]" class="download-progress-bar" mode="indeterminate"></mat-progress-bar>
|
||||
</mat-grid-tile>
|
||||
</mat-grid-list>
|
||||
|
||||
<!-- Add video playlist button -->
|
||||
<div class="add-playlist-button"><button (click)="openCreatePlaylistDialog('video')" mat-fab><mat-icon>add</mat-icon></button></div>
|
||||
<div *ngIf="playlists.video.length === 0">
|
||||
No playlists available. Create one from your downloading video files by clicking the blue plus button.
|
||||
</div>
|
||||
</div>
|
||||
</mat-expansion-panel>
|
||||
</mat-accordion>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { Component, OnInit, ElementRef, ViewChild } from '@angular/core';
|
||||
import { Component, OnInit, ElementRef, ViewChild, ViewChildren, QueryList } 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 {MatSnackBar, MatDialog} from '@angular/material';
|
||||
import { saveAs } from 'file-saver';
|
||||
import 'rxjs/add/observable/of';
|
||||
import 'rxjs/add/operator/mapTo';
|
||||
@@ -16,6 +16,24 @@ import 'rxjs/add/operator/do'
|
||||
import 'rxjs/add/operator/switch'
|
||||
import { YoutubeSearchService, Result } from '../youtube-search.service';
|
||||
import { Router } from '@angular/router';
|
||||
import { CreatePlaylistComponent } from 'app/create-playlist/create-playlist.component';
|
||||
import { Platform } from '@angular/cdk/platform';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
export let audioFilesMouseHovering = false;
|
||||
export let videoFilesMouseHovering = false;
|
||||
export let audioFilesOpened = false;
|
||||
export let videoFilesOpened = false;
|
||||
|
||||
export interface Download {
|
||||
uid: string;
|
||||
type: string;
|
||||
url: string;
|
||||
percent_complete: number;
|
||||
downloading: boolean;
|
||||
is_playlist: boolean;
|
||||
fileNames?: string[];
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
@@ -28,16 +46,26 @@ export class MainComponent implements OnInit {
|
||||
determinateProgress = false;
|
||||
downloadingfile = false;
|
||||
audioOnly: boolean;
|
||||
multiDownloadMode = false;
|
||||
customArgsEnabled = false;
|
||||
customArgs = null;
|
||||
customOutputEnabled = false;
|
||||
customOutput = null;
|
||||
urlError = false;
|
||||
path = '';
|
||||
url = '';
|
||||
exists = '';
|
||||
percentDownloaded: number;
|
||||
|
||||
// settings
|
||||
fileManagerEnabled = false;
|
||||
allowQualitySelect = false;
|
||||
downloadOnlyMode = false;
|
||||
allowMultiDownloadMode = false;
|
||||
baseStreamPath;
|
||||
audioFolderPath;
|
||||
videoFolderPath;
|
||||
allowAdvancedDownload = false;
|
||||
|
||||
cachedAvailableFormats = {};
|
||||
|
||||
@@ -51,6 +79,11 @@ export class MainComponent implements OnInit {
|
||||
mp3s: any[] = [];
|
||||
mp4s: any[] = [];
|
||||
files_cols = (window.innerWidth <= 450) ? 2 : 4;
|
||||
playlists = {'audio': [], 'video': []};
|
||||
playlist_thumbnails = {};
|
||||
downloading_content = {'audio': {}, 'video': {}};
|
||||
downloads: Download[] = [];
|
||||
current_download: Download = null;
|
||||
|
||||
urlForm = new FormControl('', [Validators.required]);
|
||||
|
||||
@@ -150,25 +183,38 @@ export class MainComponent implements OnInit {
|
||||
formats_loading = false;
|
||||
|
||||
@ViewChild('urlinput', { read: ElementRef, static: false }) urlInput: ElementRef;
|
||||
@ViewChildren('audiofilecard') audioFileCards: QueryList<FileCardComponent>;
|
||||
@ViewChildren('videofilecard') videoFileCards: QueryList<FileCardComponent>;
|
||||
last_valid_url = '';
|
||||
last_url_check = 0;
|
||||
|
||||
constructor(private postsService: PostsService, private youtubeSearch: YoutubeSearchService, public snackBar: MatSnackBar,
|
||||
private router: Router) {
|
||||
this.audioOnly = false;
|
||||
test_download: Download = {
|
||||
uid: null,
|
||||
type: 'audio',
|
||||
percent_complete: 0,
|
||||
url: 'http://youtube.com/watch?v=17848rufj',
|
||||
downloading: true,
|
||||
is_playlist: false
|
||||
};
|
||||
|
||||
constructor(private postsService: PostsService, private youtubeSearch: YoutubeSearchService, public snackBar: MatSnackBar,
|
||||
private router: Router, public dialog: MatDialog, private platform: Platform) {
|
||||
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.allowMultiDownloadMode = result['YoutubeDLMaterial']['Extra']['allow_multi_download_mode'];
|
||||
this.baseStreamPath = result['YoutubeDLMaterial']['Downloader']['path-base'];
|
||||
this.audioFolderPath = result['YoutubeDLMaterial']['Downloader']['path-audio'];
|
||||
this.videoFolderPath = result['YoutubeDLMaterial']['Downloader']['path-video'];
|
||||
this.youtubeSearchEnabled = result['YoutubeDLMaterial']['API'] && result['YoutubeDLMaterial']['API']['use_youtube_API'] &&
|
||||
result['YoutubeDLMaterial']['API']['youtube_API_key'];
|
||||
this.youtubeAPIKey = this.youtubeSearchEnabled ? result['YoutubeDLMaterial']['API']['youtube_API_key'] : null;
|
||||
this.allowQualitySelect = result['YoutubeDLMaterial']['Extra']['allow_quality_select'];
|
||||
this.allowAdvancedDownload = result['YoutubeDLMaterial']['Advanced']['allow_advanced_download'];
|
||||
|
||||
this.postsService.path = backendUrl;
|
||||
this.postsService.startPath = backendUrl;
|
||||
@@ -183,6 +229,18 @@ export class MainComponent implements OnInit {
|
||||
this.youtubeSearch.initializeAPI(this.youtubeAPIKey);
|
||||
this.attachToInput();
|
||||
}
|
||||
|
||||
// set final cache items
|
||||
if (this.allowAdvancedDownload) {
|
||||
if (localStorage.getItem('customArgsEnabled') !== null) {
|
||||
this.customArgsEnabled = localStorage.getItem('customArgsEnabled') === 'true';
|
||||
}
|
||||
|
||||
if (localStorage.getItem('customOutputEnabled') !== null) {
|
||||
this.customOutputEnabled = localStorage.getItem('customOutputEnabled') === 'true';
|
||||
}
|
||||
}
|
||||
|
||||
}, error => {
|
||||
console.log(error);
|
||||
});
|
||||
@@ -194,7 +252,24 @@ export class MainComponent implements OnInit {
|
||||
getMp3s() {
|
||||
this.postsService.getMp3s().subscribe(result => {
|
||||
const mp3s = result['mp3s'];
|
||||
this.mp3s = mp3s;
|
||||
const playlists = result['playlists'];
|
||||
// if they are different
|
||||
if (JSON.stringify(this.mp3s) !== JSON.stringify(mp3s)) { this.mp3s = mp3s };
|
||||
this.playlists.audio = playlists;
|
||||
|
||||
// get thumbnail url by using first video. this is a temporary hack
|
||||
for (let i = 0; i < this.playlists.audio.length; i++) {
|
||||
const playlist = this.playlists.audio[i];
|
||||
let videoToExtractThumbnail = null;
|
||||
for (let j = 0; j < this.mp3s.length; j++) {
|
||||
if (this.mp3s[j].id === playlist.fileNames[0]) {
|
||||
// found the corresponding file
|
||||
videoToExtractThumbnail = this.mp3s[j];
|
||||
}
|
||||
}
|
||||
|
||||
if (videoToExtractThumbnail) { this.playlist_thumbnails[playlist.id] = videoToExtractThumbnail.thumbnailURL; }
|
||||
}
|
||||
}, error => {
|
||||
console.log(error);
|
||||
});
|
||||
@@ -203,7 +278,24 @@ export class MainComponent implements OnInit {
|
||||
getMp4s() {
|
||||
this.postsService.getMp4s().subscribe(result => {
|
||||
const mp4s = result['mp4s'];
|
||||
this.mp4s = mp4s;
|
||||
const playlists = result['playlists'];
|
||||
// if they are different
|
||||
if (JSON.stringify(this.mp4s) !== JSON.stringify(mp4s)) { this.mp4s = mp4s };
|
||||
this.playlists.video = playlists;
|
||||
|
||||
// get thumbnail url by using first video. this is a temporary hack
|
||||
for (let i = 0; i < this.playlists.video.length; i++) {
|
||||
const playlist = this.playlists.video[i];
|
||||
let videoToExtractThumbnail = null;
|
||||
for (let j = 0; j < this.mp4s.length; j++) {
|
||||
if (this.mp4s[j].id === playlist.fileNames[0]) {
|
||||
// found the corresponding file
|
||||
videoToExtractThumbnail = this.mp4s[j];
|
||||
}
|
||||
}
|
||||
|
||||
if (videoToExtractThumbnail) { this.playlist_thumbnails[playlist.id] = videoToExtractThumbnail.thumbnailURL; }
|
||||
}
|
||||
},
|
||||
error => {
|
||||
console.log(error);
|
||||
@@ -212,12 +304,38 @@ export class MainComponent implements OnInit {
|
||||
|
||||
public goToFile(name, isAudio) {
|
||||
if (isAudio) {
|
||||
this.downloadHelperMp3(name, false, true);
|
||||
this.downloadHelperMp3(name, false, false);
|
||||
} else {
|
||||
this.downloadHelperMp4(name, false, true);
|
||||
this.downloadHelperMp4(name, false, false);
|
||||
}
|
||||
}
|
||||
|
||||
public goToPlaylist(playlistID, type) {
|
||||
const playlist = this.getPlaylistObjectByID(playlistID, type);
|
||||
if (playlist) {
|
||||
if (this.downloadOnlyMode) {
|
||||
this.downloading_content[type][playlistID] = true;
|
||||
this.downloadPlaylist(playlist.fileNames, type, playlist.name, playlistID);
|
||||
} else {
|
||||
const fileNames = playlist.fileNames;
|
||||
this.router.navigate(['/player', {fileNames: fileNames.join('|nvr|'), type: type, id: playlistID}]);
|
||||
}
|
||||
} else {
|
||||
// playlist not found
|
||||
console.error(`Playlist with ID ${playlistID} not found!`);
|
||||
}
|
||||
}
|
||||
|
||||
getPlaylistObjectByID(playlistID, type) {
|
||||
for (let i = 0; i < this.playlists[type].length; i++) {
|
||||
const playlist = this.playlists[type][i];
|
||||
if (playlist.id === playlistID) {
|
||||
return playlist;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public removeFromMp3(name: string) {
|
||||
for (let i = 0; i < this.mp3s.length; i++) {
|
||||
if (this.mp3s[i].id === name) {
|
||||
@@ -226,9 +344,17 @@ export class MainComponent implements OnInit {
|
||||
}
|
||||
}
|
||||
|
||||
public removePlaylistMp3(playlistID, index) {
|
||||
this.postsService.removePlaylist(playlistID, 'audio').subscribe(res => {
|
||||
if (res['success']) {
|
||||
this.playlists.audio.splice(index, 1);
|
||||
this.openSnackBar('Playlist successfully removed.', '');
|
||||
}
|
||||
this.getMp3s();
|
||||
});
|
||||
}
|
||||
|
||||
public removeFromMp4(name: string) {
|
||||
// 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);
|
||||
@@ -236,66 +362,108 @@ export class MainComponent implements OnInit {
|
||||
}
|
||||
}
|
||||
|
||||
public removePlaylistMp4(playlistID, index) {
|
||||
this.postsService.removePlaylist(playlistID, 'video').subscribe(res => {
|
||||
if (res['success']) {
|
||||
this.playlists.video.splice(index, 1);
|
||||
this.openSnackBar('Playlist successfully removed.', '');
|
||||
}
|
||||
this.getMp4s();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// app initialization.
|
||||
ngOnInit() {
|
||||
this.iOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window['MSStream'];
|
||||
// this.iOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window['MSStream'];
|
||||
this.iOS = this.platform.IOS;
|
||||
|
||||
if (localStorage.getItem('audioOnly') !== null) {
|
||||
this.audioOnly = localStorage.getItem('audioOnly') === 'true';
|
||||
}
|
||||
|
||||
if (localStorage.getItem('multiDownloadMode') !== null) {
|
||||
this.multiDownloadMode = localStorage.getItem('multiDownloadMode') === 'true';
|
||||
}
|
||||
}
|
||||
|
||||
// download helpers
|
||||
|
||||
downloadHelperMp3(name, is_playlist = false, forceView = false) {
|
||||
downloadHelperMp3(name, is_playlist = false, forceView = false, new_download = null) {
|
||||
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]));
|
||||
if (new_download && this.current_download !== new_download) {
|
||||
// console.log('mismatched downloads');
|
||||
} else if (!this.multiDownloadMode) {
|
||||
// if download only mode, just download the file. no redirect
|
||||
if (forceView === false && this.downloadOnlyMode && !this.iOS) {
|
||||
if (is_playlist) {
|
||||
const zipName = name[0].split(' ')[0] + name[1].split(' ')[0];
|
||||
this.downloadPlaylist(name, 'audio', zipName);
|
||||
} else {
|
||||
this.downloadAudioFile(decodeURI(name));
|
||||
}
|
||||
} else {
|
||||
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';
|
||||
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';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// remove download from current downloads
|
||||
this.removeDownloadFromCurrentDownloads(new_download);
|
||||
|
||||
// reloads mp3s
|
||||
if (this.fileManagerEnabled) {
|
||||
this.getMp3s();
|
||||
setTimeout(() => {
|
||||
this.audioFileCards.forEach(filecard => {
|
||||
filecard.onHoverResponse();
|
||||
});
|
||||
}, 200);
|
||||
}
|
||||
}
|
||||
|
||||
downloadHelperMp4(name, is_playlist = false, forceView = false) {
|
||||
downloadHelperMp4(name, is_playlist = false, forceView = false, new_download = null) {
|
||||
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]));
|
||||
if (new_download && this.current_download !== new_download) {
|
||||
// console.log('mismatched downloads');
|
||||
} else if (!this.multiDownloadMode) {
|
||||
// if download only mode, just download the file. no redirect
|
||||
if (forceView === false && this.downloadOnlyMode) {
|
||||
if (is_playlist) {
|
||||
const zipName = name[0].split(' ')[0] + name[1].split(' ')[0];
|
||||
this.downloadPlaylist(name, 'video', zipName);
|
||||
} else {
|
||||
this.downloadVideoFile(decodeURI(name));
|
||||
}
|
||||
} else {
|
||||
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';
|
||||
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';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// remove download from current downloads
|
||||
this.removeDownloadFromCurrentDownloads(new_download);
|
||||
|
||||
// reloads mp4s
|
||||
if (this.fileManagerEnabled) {
|
||||
this.getMp4s();
|
||||
setTimeout(() => {
|
||||
this.videoFileCards.forEach(filecard => {
|
||||
filecard.onHoverResponse();
|
||||
});
|
||||
}, 200);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -306,59 +474,135 @@ export class MainComponent implements OnInit {
|
||||
this.path = '';
|
||||
|
||||
if (this.audioOnly) {
|
||||
// create download object
|
||||
const new_download: Download = {
|
||||
uid: uuid(),
|
||||
type: 'audio',
|
||||
percent_complete: 0,
|
||||
url: this.url,
|
||||
downloading: true,
|
||||
is_playlist: this.url.includes('playlist')
|
||||
};
|
||||
this.downloads.push(new_download);
|
||||
if (!this.current_download && !this.multiDownloadMode) { this.current_download = new_download };
|
||||
this.downloadingfile = true;
|
||||
|
||||
let customQualityConfiguration = null;
|
||||
if (this.selectedQuality !== '') {
|
||||
const cachedFormatsExists = this.cachedAvailableFormats[this.url];
|
||||
const cachedFormatsExists = this.cachedAvailableFormats[this.url] && this.cachedAvailableFormats[this.url]['formats'];
|
||||
if (cachedFormatsExists) {
|
||||
const audio_formats = this.cachedAvailableFormats[this.url]['audio'];
|
||||
const audio_formats = this.cachedAvailableFormats[this.url]['formats']['audio'];
|
||||
customQualityConfiguration = audio_formats[this.selectedQuality]['format_id'];
|
||||
}
|
||||
}
|
||||
|
||||
const customArgs = (this.customArgsEnabled ? this.customArgs : null);
|
||||
const customOutput = (this.customOutputEnabled ? this.customOutput : null);
|
||||
|
||||
this.postsService.makeMP3(this.url, (this.selectedQuality === '' ? null : this.selectedQuality),
|
||||
customQualityConfiguration).subscribe(posts => {
|
||||
customQualityConfiguration, customArgs, customOutput).subscribe(posts => {
|
||||
// update download object
|
||||
new_download.downloading = false;
|
||||
new_download.percent_complete = 100;
|
||||
|
||||
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);
|
||||
this.downloadHelperMp3(this.path, is_playlist, false, new_download);
|
||||
}
|
||||
}, error => { // can't access server
|
||||
this.downloadingfile = false;
|
||||
this.openSnackBar('Download failed!', 'OK.');
|
||||
});
|
||||
} else {
|
||||
// create download object
|
||||
const new_download: Download = {
|
||||
uid: uuid(),
|
||||
type: 'video',
|
||||
percent_complete: 0,
|
||||
url: this.url,
|
||||
downloading: true,
|
||||
is_playlist: this.url.includes('playlist')
|
||||
};
|
||||
this.downloads.push(new_download);
|
||||
if (!this.current_download && !this.multiDownloadMode) { this.current_download = new_download };
|
||||
this.downloadingfile = true;
|
||||
|
||||
let customQualityConfiguration = null;
|
||||
const cachedFormatsExists = this.cachedAvailableFormats[this.url];
|
||||
const cachedFormatsExists = this.cachedAvailableFormats[this.url] && this.cachedAvailableFormats[this.url]['formats'];
|
||||
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);
|
||||
const video_formats = this.cachedAvailableFormats[this.url]['formats']['video'];
|
||||
if (video_formats['best_audio_format'] && this.selectedQuality !== '') {
|
||||
customQualityConfiguration = video_formats[this.selectedQuality]['format_id'] + '+' + video_formats['best_audio_format'];
|
||||
}
|
||||
}
|
||||
|
||||
this.downloadingfile = true;
|
||||
const customArgs = (this.customArgsEnabled ? this.customArgs : null);
|
||||
const customOutput = (this.customOutputEnabled ? this.customOutput : null);
|
||||
|
||||
this.postsService.makeMP4(this.url, (this.selectedQuality === '' ? null : this.selectedQuality),
|
||||
customQualityConfiguration).subscribe(posts => {
|
||||
customQualityConfiguration, customArgs, customOutput).subscribe(posts => {
|
||||
// update download object
|
||||
new_download.downloading = false;
|
||||
new_download.percent_complete = 100;
|
||||
|
||||
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);
|
||||
this.downloadHelperMp4(this.path, is_playlist, false, new_download);
|
||||
}
|
||||
}, error => { // can't access server
|
||||
this.downloadingfile = false;
|
||||
this.openSnackBar('Download failed!', 'OK.');
|
||||
});
|
||||
}
|
||||
|
||||
if (this.multiDownloadMode) {
|
||||
this.url = '';
|
||||
this.downloadingfile = false;
|
||||
}
|
||||
} else {
|
||||
this.urlError = true;
|
||||
}
|
||||
}
|
||||
|
||||
// download canceled handler
|
||||
cancelDownload(download_to_cancel = null) {
|
||||
// if one is provided, cancel that one. otherwise, remove the current one
|
||||
if (download_to_cancel) {
|
||||
this.removeDownloadFromCurrentDownloads(download_to_cancel)
|
||||
return;
|
||||
}
|
||||
this.downloadingfile = false;
|
||||
this.current_download.downloading = false;
|
||||
this.current_download = null;
|
||||
}
|
||||
|
||||
getDownloadByUID(uid) {
|
||||
const index = this.downloads.findIndex(download => download.uid === uid);
|
||||
if (index !== -1) {
|
||||
return this.downloads[index];
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
removeDownloadFromCurrentDownloads(download_to_remove) {
|
||||
const index = this.downloads.indexOf(download_to_remove);
|
||||
if (index !== -1) {
|
||||
this.downloads.splice(index, 1);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
downloadAudioFile(name) {
|
||||
this.downloading_content['audio'][name] = true;
|
||||
this.postsService.downloadFileFromServer(name, 'audio').subscribe(res => {
|
||||
this.downloading_content['audio'][name] = false;
|
||||
const blob: Blob = res;
|
||||
saveAs(blob, name + '.mp3');
|
||||
|
||||
@@ -373,7 +617,9 @@ export class MainComponent implements OnInit {
|
||||
}
|
||||
|
||||
downloadVideoFile(name) {
|
||||
this.downloading_content['video'][name] = true;
|
||||
this.postsService.downloadFileFromServer(name, 'video').subscribe(res => {
|
||||
this.downloading_content['video'][name] = false;
|
||||
const blob: Blob = res;
|
||||
saveAs(blob, name + '.mp4');
|
||||
|
||||
@@ -387,6 +633,15 @@ export class MainComponent implements OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
downloadPlaylist(fileNames, type, zipName = null, playlistID = null) {
|
||||
this.postsService.downloadFileFromServer(fileNames, type, zipName).subscribe(res => {
|
||||
if (playlistID) { this.downloading_content[type][playlistID] = false };
|
||||
const blob: Blob = res;
|
||||
saveAs(blob, zipName + '.zip');
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
clearInput() {
|
||||
this.url = '';
|
||||
this.results_showing = false;
|
||||
@@ -406,7 +661,7 @@ export class MainComponent implements OnInit {
|
||||
}
|
||||
|
||||
inputChanged(new_val) {
|
||||
if (new_val === '') {
|
||||
if (new_val === '' || !new_val) {
|
||||
this.results_showing = false;
|
||||
} else {
|
||||
if (this.ValidURL(new_val)) {
|
||||
@@ -427,9 +682,9 @@ export class MainComponent implements OnInit {
|
||||
// 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);
|
||||
const ytValid = true || reYT.test(str);
|
||||
if (valid && ytValid && Date.now() - this.last_url_check > 1000) {
|
||||
if (str !== this.last_valid_url) {
|
||||
if (str !== this.last_valid_url && this.allowQualitySelect) {
|
||||
// get info
|
||||
this.getURLInfo(str);
|
||||
}
|
||||
@@ -446,21 +701,32 @@ export class MainComponent implements OnInit {
|
||||
}
|
||||
|
||||
getURLInfo(url) {
|
||||
console.log(this.cachedAvailableFormats[url]);
|
||||
if (!(this.cachedAvailableFormats[url])) {
|
||||
this.formats_loading = true;
|
||||
console.log('has no cached formats available');
|
||||
if (!this.cachedAvailableFormats[url]) {
|
||||
this.cachedAvailableFormats[url] = {};
|
||||
}
|
||||
if (!(this.cachedAvailableFormats[url] && this.cachedAvailableFormats[url]['formats'])) {
|
||||
this.cachedAvailableFormats[url]['formats_loading'] = true;
|
||||
this.postsService.getFileInfo([url], 'irrelevant', true).subscribe(res => {
|
||||
if (url === this.url) { this.formats_loading = false; }
|
||||
this.cachedAvailableFormats[url]['formats_loading'] = false;
|
||||
const infos = res['result'];
|
||||
if (!infos || !infos.formats) {
|
||||
this.errorFormats(url);
|
||||
return;
|
||||
}
|
||||
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;
|
||||
this.cachedAvailableFormats[url]['formats'] = available_formats;
|
||||
}, err => {
|
||||
this.errorFormats(url);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
errorFormats(url) {
|
||||
this.cachedAvailableFormats[url]['formats_loading'] = false;
|
||||
console.error('Could not load formats for url ' + url);
|
||||
}
|
||||
|
||||
attachToInput() {
|
||||
Observable.fromEvent(this.urlInput.nativeElement, 'keyup')
|
||||
.map((e: any) => e.target.value) // extract the value of input
|
||||
@@ -471,9 +737,8 @@ export class MainComponent implements OnInit {
|
||||
.switch() // act on the return of the search
|
||||
.subscribe(
|
||||
(results: Result[]) => {
|
||||
// console.log(results);
|
||||
this.results_loading = false;
|
||||
if (results && results.length > 0) {
|
||||
if (this.url !== '' && results && results.length > 0) {
|
||||
this.results = results;
|
||||
this.results_showing = true;
|
||||
} else {
|
||||
@@ -497,6 +762,25 @@ export class MainComponent implements OnInit {
|
||||
|
||||
videoModeChanged(new_val) {
|
||||
this.selectedQuality = '';
|
||||
localStorage.setItem('audioOnly', new_val.checked.toString());
|
||||
}
|
||||
|
||||
multiDownloadModeChanged(new_val) {
|
||||
localStorage.setItem('multiDownloadMode', new_val.checked.toString());
|
||||
}
|
||||
|
||||
customArgsEnabledChanged(new_val) {
|
||||
localStorage.setItem('customArgsEnabled', new_val.checked.toString());
|
||||
if (new_val.checked === true && this.customOutputEnabled) {
|
||||
this.customOutputEnabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
customOutputEnabledChanged(new_val) {
|
||||
localStorage.setItem('customOutputEnabled', new_val.checked.toString());
|
||||
if (new_val.checked === true && this.customArgsEnabled) {
|
||||
this.customArgsEnabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
getAudioAndVideoFormats(formats): any[] {
|
||||
@@ -510,7 +794,6 @@ export class MainComponent implements OnInit {
|
||||
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;
|
||||
@@ -560,5 +843,61 @@ export class MainComponent implements OnInit {
|
||||
}
|
||||
return best_audio_format_for_mp4;
|
||||
}
|
||||
}
|
||||
|
||||
accordionEntered(type) {
|
||||
if (type === 'audio') {
|
||||
audioFilesMouseHovering = true;
|
||||
this.audioFileCards.forEach(filecard => {
|
||||
filecard.onHoverResponse();
|
||||
});
|
||||
} else if (type === 'video') {
|
||||
videoFilesMouseHovering = true;
|
||||
this.videoFileCards.forEach(filecard => {
|
||||
filecard.onHoverResponse();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
accordionLeft(type) {
|
||||
if (type === 'audio') {
|
||||
audioFilesMouseHovering = false;
|
||||
} else if (type === 'video') {
|
||||
videoFilesMouseHovering = false;
|
||||
}
|
||||
}
|
||||
|
||||
accordionOpened(type) {
|
||||
if (type === 'audio') {
|
||||
audioFilesOpened = true;
|
||||
} else if (type === 'video') {
|
||||
videoFilesOpened = true;
|
||||
}
|
||||
}
|
||||
|
||||
accordionClosed(type) {
|
||||
if (type === 'audio') {
|
||||
audioFilesOpened = false;
|
||||
} else if (type === 'video') {
|
||||
videoFilesOpened = false;
|
||||
}
|
||||
}
|
||||
|
||||
// creating a playlist
|
||||
openCreatePlaylistDialog(type) {
|
||||
const dialogRef = this.dialog.open(CreatePlaylistComponent, {
|
||||
data: {
|
||||
filesToSelectFrom: (type === 'audio') ? this.mp3s : this.mp4s,
|
||||
type: type
|
||||
}
|
||||
});
|
||||
dialogRef.afterClosed().subscribe(result => {
|
||||
if (result) {
|
||||
if (type === 'audio') { this.getMp3s() };
|
||||
if (type === 'video') { this.getMp4s() };
|
||||
this.openSnackBar('Successfully created playlist!', '');
|
||||
} else if (result === false) {
|
||||
this.openSnackBar('ERROR: failed to create playlist!', '');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.video-player:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.audio-styles {
|
||||
height: 50px;
|
||||
background-color: transparent;
|
||||
@@ -22,4 +26,54 @@
|
||||
max-width: 100%;
|
||||
padding-left: 0px;
|
||||
padding-right: 0px;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
position: absolute;
|
||||
left: 0px;
|
||||
bottom: -1px;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
bottom: 3px;
|
||||
left: 3px;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.save-button {
|
||||
right: 25px;
|
||||
position: absolute;
|
||||
bottom: 25px;
|
||||
}
|
||||
|
||||
.favorite-button {
|
||||
left: 25px;
|
||||
position: absolute;
|
||||
bottom: 25px;
|
||||
}
|
||||
|
||||
.video-col {
|
||||
padding-right: 0px;
|
||||
padding-left: 0.01px;
|
||||
}
|
||||
|
||||
.save-icon {
|
||||
bottom: 1px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.update-playlist-button-div {
|
||||
float: right;
|
||||
margin-right: 30px;
|
||||
margin-top: 25px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.spinner-div {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
margin-right: 12px;
|
||||
top: 8px;
|
||||
}
|
||||
@@ -1,17 +1,33 @@
|
||||
<div *ngIf="playlist.length > 0; else loading">
|
||||
<div *ngIf="playlist.length > 0">
|
||||
<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">
|
||||
<div style="max-width: 100%; margin-left: 0px;" class="row">
|
||||
<div [ngClass]="(type === 'audio') ? 'my-2 px-1' : 'video-col'" class="col">
|
||||
<vg-player (onPlayerReady)="onPlayerReady($event)" [style.background-color]="(type === 'audio') ? 'transparent' : 'black'">
|
||||
<video [ngClass]="(type === 'audio') ? 'audio-styles' : 'video-styles'" #media class="video-player" [vgMedia]="media" [src]="currentItem.src" id="singleVideo" preload="auto" controls>
|
||||
</video>
|
||||
</vg-player>
|
||||
</div>
|
||||
<div class="col-12 my-2">
|
||||
<mat-button-toggle-group style="width: 80%; left: 9%" vertical name="videoSelect" aria-label="Video Select" #group="matButtonToggleGroup">
|
||||
<mat-button-toggle *ngFor="let name of fileNames; let i = index" [checked]="currentItem.title === name" (click)="onClickPlaylistItem(playlist[i], i)" class="toggle-button" [value]="name">{{decodeURI(name)}}</mat-button-toggle>
|
||||
<mat-button-toggle-group cdkDropList [cdkDropListSortingDisabled]="!id" (cdkDropListDropped)="drop($event)" style="width: 80%; left: 9%" vertical name="videoSelect" aria-label="Video Select" #group="matButtonToggleGroup">
|
||||
<mat-button-toggle cdkDrag *ngFor="let playlist_item of playlist; let i = index" [checked]="currentItem.title === playlist_item.title" (click)="onClickPlaylistItem(playlist_item, i)" class="toggle-button" [value]="playlist_item.title">{{decodeURI(playlist_item.title)}}</mat-button-toggle>
|
||||
</mat-button-toggle-group>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="update-playlist-button-div" *ngIf="id && playlistChanged()">
|
||||
<div class="spinner-div">
|
||||
<mat-spinner *ngIf="playlist_updating" [diameter]="25"></mat-spinner>
|
||||
</div>
|
||||
<button color="primary" [disabled]="playlist_updating" (click)="updatePlaylist()" mat-raised-button>Save changes <mat-icon>update</mat-icon></button>
|
||||
|
||||
</div>
|
||||
|
||||
<div *ngIf="playlist.length > 1">
|
||||
<button class="save-button" color="primary" (click)="downloadContent()" [disabled]="downloading" mat-fab><mat-icon class="save-icon">save</mat-icon><mat-spinner *ngIf="downloading" class="spinner" [diameter]="50"></mat-spinner></button>
|
||||
<button *ngIf="!id" color="accent" class="favorite-button" color="primary" (click)="namePlaylistDialog()" mat-fab><mat-icon class="save-icon">favorite</mat-icon></button>
|
||||
</div>
|
||||
<div *ngIf="playlist.length === 1">
|
||||
<button class="save-button" color="primary" (click)="downloadFile()" [disabled]="downloading" mat-fab><mat-icon class="save-icon">save</mat-icon><mat-spinner *ngIf="downloading" class="spinner" [diameter]="50"></mat-spinner></button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,7 +1,10 @@
|
||||
import { Component, OnInit, HostListener } from '@angular/core';
|
||||
import { Component, OnInit, HostListener, EventEmitter } from '@angular/core';
|
||||
import { VgAPI } from 'videogular2/compiled/core';
|
||||
import { PostsService } from 'app/posts.services';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { MatDialog, MatSnackBar } from '@angular/material';
|
||||
import { InputDialogComponent } from 'app/input-dialog/input-dialog.component';
|
||||
import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop';
|
||||
|
||||
export interface IMedia {
|
||||
title: string;
|
||||
@@ -17,6 +20,8 @@ export interface IMedia {
|
||||
export class PlayerComponent implements OnInit {
|
||||
|
||||
playlist: Array<IMedia> = [];
|
||||
original_playlist: string = null;
|
||||
playlist_updating = false;
|
||||
|
||||
currentIndex = 0;
|
||||
currentItem: IMedia = null;
|
||||
@@ -31,6 +36,10 @@ export class PlayerComponent implements OnInit {
|
||||
videoFolderPath = null;
|
||||
innerWidth: number;
|
||||
|
||||
downloading = false;
|
||||
|
||||
id = null;
|
||||
|
||||
@HostListener('window:resize', ['$event'])
|
||||
onResize(event) {
|
||||
this.innerWidth = window.innerWidth;
|
||||
@@ -41,12 +50,19 @@ export class PlayerComponent implements OnInit {
|
||||
|
||||
this.fileNames = this.route.snapshot.paramMap.get('fileNames').split('|nvr|');
|
||||
this.type = this.route.snapshot.paramMap.get('type');
|
||||
this.id = this.route.snapshot.paramMap.get('id');
|
||||
|
||||
// loading config
|
||||
this.postsService.loadNavItems().subscribe(result => { // loads settings
|
||||
this.baseStreamPath = result['YoutubeDLMaterial']['Downloader']['path-base'];
|
||||
this.audioFolderPath = result['YoutubeDLMaterial']['Downloader']['path-audio'];
|
||||
this.videoFolderPath = result['YoutubeDLMaterial']['Downloader']['path-video'];
|
||||
const backendUrl = result['YoutubeDLMaterial']['Host']['backendurl'];
|
||||
|
||||
this.postsService.path = backendUrl;
|
||||
this.postsService.startPath = backendUrl;
|
||||
this.postsService.startPathSSL = backendUrl;
|
||||
|
||||
let fileType = null;
|
||||
if (this.type === 'audio') {
|
||||
fileType = 'audio/mp3';
|
||||
@@ -60,23 +76,24 @@ export class PlayerComponent implements OnInit {
|
||||
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 fullLocation = this.baseStreamPath + baseLocation + encodeURI(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.original_playlist = JSON.stringify(this.playlist);
|
||||
});
|
||||
|
||||
// this.getFileInfos();
|
||||
|
||||
}
|
||||
|
||||
constructor(private postsService: PostsService, private route: ActivatedRoute) {
|
||||
constructor(private postsService: PostsService, private route: ActivatedRoute, private dialog: MatDialog, private router: Router,
|
||||
public snackBar: MatSnackBar) {
|
||||
|
||||
}
|
||||
|
||||
@@ -103,19 +120,137 @@ export class PlayerComponent implements OnInit {
|
||||
}
|
||||
|
||||
onClickPlaylistItem(item: IMedia, index: number) {
|
||||
console.log('new current item is ' + item.title + ' at index ' + index);
|
||||
// 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 => {
|
||||
const fileNames = this.getFileNames();
|
||||
this.postsService.getFileInfo(fileNames, this.type, false).subscribe(res => {
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
getFileNames() {
|
||||
const fileNames = [];
|
||||
for (let i = 0; i < this.playlist.length; i++) {
|
||||
fileNames.push(this.playlist[i].title);
|
||||
}
|
||||
return fileNames;
|
||||
}
|
||||
|
||||
decodeURI(string) {
|
||||
return decodeURI(string);
|
||||
}
|
||||
|
||||
downloadContent() {
|
||||
const fileNames = [];
|
||||
for (let i = 0; i < this.playlist.length; i++) {
|
||||
fileNames.push(this.playlist[i].title);
|
||||
}
|
||||
|
||||
const zipName = fileNames[0].split(' ')[0] + fileNames[1].split(' ')[0];
|
||||
this.downloading = true;
|
||||
this.postsService.downloadFileFromServer(fileNames, this.type, zipName).subscribe(res => {
|
||||
this.downloading = false;
|
||||
const blob: Blob = res;
|
||||
saveAs(blob, zipName + '.zip');
|
||||
}, err => {
|
||||
console.log(err);
|
||||
this.downloading = false;
|
||||
});
|
||||
}
|
||||
|
||||
downloadFile() {
|
||||
const ext = (this.type === 'audio') ? '.mp3' : '.mp4';
|
||||
const filename = this.playlist[0].title;
|
||||
this.downloading = true;
|
||||
this.postsService.downloadFileFromServer(filename, this.type).subscribe(res => {
|
||||
this.downloading = false;
|
||||
const blob: Blob = res;
|
||||
saveAs(blob, filename + ext);
|
||||
}, err => {
|
||||
console.log(err);
|
||||
this.downloading = false;
|
||||
});
|
||||
}
|
||||
|
||||
namePlaylistDialog() {
|
||||
const done = new EventEmitter<any>();
|
||||
const dialogRef = this.dialog.open(InputDialogComponent, {
|
||||
width: '300px',
|
||||
data: {
|
||||
inputTitle: 'Name the playlist',
|
||||
inputPlaceholder: 'Name',
|
||||
submitText: 'Favorite',
|
||||
doneEmitter: done
|
||||
}
|
||||
});
|
||||
|
||||
done.subscribe(name => {
|
||||
|
||||
// Eventually do additional checks on name
|
||||
if (name) {
|
||||
const fileNames = this.getFileNames();
|
||||
this.postsService.createPlaylist(name, fileNames, this.type, null).subscribe(res => {
|
||||
if (res['success']) {
|
||||
dialogRef.close();
|
||||
const new_playlist = res['new_playlist'];
|
||||
this.openSnackBar('Playlist \'' + name + '\' successfully created!', '')
|
||||
this.playlistPostCreationHandler(new_playlist.id);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/*
|
||||
createPlaylist(name) {
|
||||
this.postsService.createPlaylist(name, this.fileNames, this.type, null).subscribe(res => {
|
||||
if (res['success']) {
|
||||
console.log('Success!');
|
||||
}
|
||||
});
|
||||
}
|
||||
*/
|
||||
|
||||
playlistPostCreationHandler(playlistID) {
|
||||
// changes the route without moving from the current view or
|
||||
// triggering a navigation event
|
||||
this.id = playlistID;
|
||||
this.router.navigateByUrl(this.router.url + ';id=' + playlistID);
|
||||
}
|
||||
|
||||
drop(event: CdkDragDrop<string[]>) {
|
||||
moveItemInArray(this.playlist, event.previousIndex, event.currentIndex);
|
||||
}
|
||||
|
||||
playlistChanged() {
|
||||
return JSON.stringify(this.playlist) !== this.original_playlist;
|
||||
}
|
||||
|
||||
updatePlaylist() {
|
||||
const fileNames = this.getFileNames();
|
||||
this.playlist_updating = true;
|
||||
this.postsService.updatePlaylist(this.id, fileNames, this.type).subscribe(res => {
|
||||
this.playlist_updating = false;
|
||||
if (res['success']) {
|
||||
const fileNamesEncoded = fileNames.join('|nvr|');
|
||||
this.router.navigate(['/player', {fileNames: fileNamesEncoded, type: 'video', id: this.id}]);
|
||||
this.openSnackBar('Successfully updated playlist.', '');
|
||||
this.original_playlist = JSON.stringify(this.playlist);
|
||||
} else {
|
||||
this.openSnackBar('ERROR: Failed to update playlist.', '');
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// snackbar helper
|
||||
public openSnackBar(message: string, action: string) {
|
||||
this.snackBar.open(message, action, {
|
||||
duration: 2000,
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { Observable } from 'rxjs/Observable';
|
||||
import 'rxjs/add/operator/map';
|
||||
import 'rxjs/add/operator/catch';
|
||||
import 'rxjs/add/observable/throw';
|
||||
import { THEMES_CONFIG } from '../themes';
|
||||
|
||||
@Injectable()
|
||||
export class PostsService {
|
||||
@@ -15,11 +16,17 @@ export class PostsService {
|
||||
startPath = 'http://localhost:17442/';
|
||||
startPathSSL = 'https://localhost:17442/'
|
||||
handShakeComplete = false;
|
||||
THEMES_CONFIG = THEMES_CONFIG;
|
||||
theme;
|
||||
|
||||
constructor(private http: HttpClient) {
|
||||
console.log('PostsService Initialized...');
|
||||
}
|
||||
|
||||
setTheme(theme) {
|
||||
this.theme = this.THEMES_CONFIG[theme];
|
||||
}
|
||||
|
||||
startHandshake(url: string) {
|
||||
return this.http.get(url + 'geturl');
|
||||
}
|
||||
@@ -36,16 +43,22 @@ export class PostsService {
|
||||
return this.http.get(this.startPath + 'audiofolder');
|
||||
}
|
||||
|
||||
makeMP3(url: string, selectedQuality: string, customQualityConfiguration: string) {
|
||||
// tslint:disable-next-line: max-line-length
|
||||
makeMP3(url: string, selectedQuality: string, customQualityConfiguration: string, customArgs: string = null, customOutput: string = null) {
|
||||
return this.http.post(this.path + 'tomp3', {url: url,
|
||||
maxBitrate: selectedQuality,
|
||||
customQualityConfiguration: customQualityConfiguration});
|
||||
customQualityConfiguration: customQualityConfiguration,
|
||||
customArgs: customArgs,
|
||||
customOutput: customOutput});
|
||||
}
|
||||
|
||||
makeMP4(url: string, selectedQuality: string, customQualityConfiguration: string) {
|
||||
// tslint:disable-next-line: max-line-length
|
||||
makeMP4(url: string, selectedQuality: string, customQualityConfiguration: string, customArgs: string = null, customOutput: string = null) {
|
||||
return this.http.post(this.path + 'tomp4', {url: url,
|
||||
selectedHeight: selectedQuality,
|
||||
customQualityConfiguration: customQualityConfiguration});
|
||||
customQualityConfiguration: customQualityConfiguration,
|
||||
customArgs: customArgs,
|
||||
customOutput: customOutput});
|
||||
}
|
||||
|
||||
getFileStatusMp3(name: string) {
|
||||
@@ -60,8 +73,9 @@ export class PostsService {
|
||||
if (isDevMode()) {
|
||||
return this.http.get('./assets/default.json');
|
||||
}
|
||||
console.log('Config location: ' + window.location.href + 'backend/config/default.json');
|
||||
return this.http.get(window.location.href + 'backend/config/default.json');
|
||||
const locations = window.location.href.split('#');
|
||||
const current_location = locations[0];
|
||||
return this.http.get(current_location + 'backend/config/default.json');
|
||||
}
|
||||
|
||||
deleteFile(name: string, isAudio: boolean) {
|
||||
@@ -80,13 +94,34 @@ export class PostsService {
|
||||
return this.http.post(this.path + 'getMp4s', {});
|
||||
}
|
||||
|
||||
downloadFileFromServer(fileName, type) {
|
||||
return this.http.post(this.path + 'downloadFile', {fileName: fileName, type: type}, {responseType: 'blob'});
|
||||
downloadFileFromServer(fileName, type, outputName = null) {
|
||||
return this.http.post(this.path + 'downloadFile', {fileNames: fileName,
|
||||
type: type,
|
||||
is_playlist: Array.isArray(fileName),
|
||||
outputName: outputName},
|
||||
{responseType: 'blob'});
|
||||
}
|
||||
|
||||
getFileInfo(fileNames, type, urlMode) {
|
||||
return this.http.post(this.path + 'getVideoInfos', {fileNames: fileNames, type: type, urlMode: urlMode});
|
||||
}
|
||||
|
||||
createPlaylist(playlistName, fileNames, type, thumbnailURL) {
|
||||
return this.http.post(this.path + 'createPlaylist', {playlistName: playlistName,
|
||||
fileNames: fileNames,
|
||||
type: type,
|
||||
thumbnailURL: thumbnailURL});
|
||||
}
|
||||
|
||||
updatePlaylist(playlistID, fileNames, type) {
|
||||
return this.http.post(this.path + 'updatePlaylist', {playlistID: playlistID,
|
||||
fileNames: fileNames,
|
||||
type: type});
|
||||
}
|
||||
|
||||
removePlaylist(playlistID, type) {
|
||||
return this.http.post(this.path + 'deletePlaylist', {playlistID: playlistID, type: type});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -17,11 +17,22 @@
|
||||
"Extra": {
|
||||
"title_top": "Youtube Downloader",
|
||||
"file_manager_enabled": true,
|
||||
"download_only_mode": false
|
||||
"allow_quality_select": true,
|
||||
"download_only_mode": false,
|
||||
"allow_multi_download_mode": true
|
||||
},
|
||||
"API": {
|
||||
"use_youtube_API": false,
|
||||
"youtube_API_key": ""
|
||||
},
|
||||
"Themes": {
|
||||
"default_theme": "default",
|
||||
"allow_theme_change": true
|
||||
},
|
||||
"Advanced": {
|
||||
"use_default_downloading_agent": true,
|
||||
"custom_downloading_agent": "",
|
||||
"allow_advanced_download": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BIN
src/favicon.ico
BIN
src/favicon.ico
Binary file not shown.
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 21 KiB |
@@ -7,11 +7,10 @@
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
||||
<link href="https://fonts.googleapis.com/css?family=Roboto" rel="stylesheet">
|
||||
<link href="https://fonts.googleapis.com/css?family=Roboto&display=swap" rel="stylesheet">
|
||||
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
||||
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
|
||||
<link href="https://unpkg.com/@angular/material/prebuilt-themes/indigo-pink.css" rel="stylesheet"> <script src="systemjs.config.js"></script>
|
||||
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
|
||||
<link href="https://fonts.googleapis.com/icon?family=Material+Icons|Material+Icons+Outlined" rel="stylesheet"/>
|
||||
</head>
|
||||
<body>
|
||||
<app-root></app-root>
|
||||
|
||||
@@ -42,11 +42,11 @@
|
||||
|
||||
|
||||
/** Evergreen browsers require these. **/
|
||||
import 'core-js/es6/reflect';
|
||||
// import 'core-js/es6/reflect';
|
||||
|
||||
|
||||
/** ALL Firefox browsers require the following to support `@angular/animation`. **/
|
||||
// import 'web-animations-js'; // Run `npm install --save web-animations-js`.
|
||||
import 'web-animations-js'; // Run `npm install --save web-animations-js`.
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
/* You can add global styles to this file, and also import other style files */
|
||||
70
src/styles.scss
Normal file
70
src/styles.scss
Normal file
@@ -0,0 +1,70 @@
|
||||
/* You can add global styles to this file, and also import other style files */
|
||||
|
||||
@import '@angular/material/prebuilt-themes/indigo-pink.css';
|
||||
|
||||
//@import './app-theme';
|
||||
/* You can add global styles to this file, and also import other style files */
|
||||
// @import "../node_modules/@angular/material/prebuilt-themes/purple-green.css";
|
||||
@import "palette.scss";
|
||||
|
||||
html, body { height: 100%; }
|
||||
body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; }
|
||||
|
||||
@import '~@angular/material/theming';
|
||||
// Plus imports for other components in your app.
|
||||
|
||||
/*// Typography
|
||||
$custom-typography: mat-typography-config(
|
||||
$font-family: Raleway,
|
||||
$headline: mat-typography-level(24px, 48px, 400),
|
||||
$body-1: mat-typography-level(16px, 24px, 400)
|
||||
);
|
||||
@include angular-material-typography($custom-typography);
|
||||
*/
|
||||
// Default colors
|
||||
$my-app-primary: mat-palette($mat-light-blue, 700, 100, 800);
|
||||
$my-app-accent: mat-palette($mat-blue, 700, 100, 800);
|
||||
$my-app-warn: mat-palette($mat-red, 700, 100, 800);
|
||||
|
||||
$my-app-theme: mat-light-theme($my-app-primary, $my-app-accent, $my-app-warn);
|
||||
@include angular-material-theme($my-app-theme);
|
||||
|
||||
// Dark theme
|
||||
$dark-primary: mat-palette($mat-indigo);
|
||||
$dark-accent: mat-palette($mat-blue);
|
||||
$dark-warn: mat-palette($mat-deep-orange);
|
||||
|
||||
$dark-theme: mat-dark-theme($dark-primary, $dark-accent, $dark-warn);
|
||||
|
||||
.dark-theme {
|
||||
@include angular-material-theme($dark-theme);
|
||||
}
|
||||
|
||||
// Light theme
|
||||
$light-primary: mat-palette($mat-grey, 200, 500, 300);
|
||||
$light-accent: mat-palette($mat-brown, 200);
|
||||
$light-warn: mat-palette($mat-deep-orange, 200);
|
||||
|
||||
$light-theme: mat-light-theme($light-primary, $light-accent, $light-warn);
|
||||
|
||||
.light-theme {
|
||||
@include angular-material-theme($light-theme)
|
||||
}
|
||||
|
||||
.no-outline {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
|
||||
// Include the common styles for Angular Material. We include this here so that you only
|
||||
// have to load a single css file for Angular Material in your app.
|
||||
// Be sure that you only ever include this mixin once!
|
||||
@include mat-core();
|
||||
|
||||
// @import '../node_modules/@angular/material/theming';
|
||||
|
||||
.centered {
|
||||
margin: 0 auto;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
}
|
||||
22
src/themes.ts
Normal file
22
src/themes.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
const THEMES_CONFIG = {
|
||||
'default': {
|
||||
'key': 'default',
|
||||
'background_color': 'ghostwhite',
|
||||
'css_label': 'default-theme',
|
||||
'social_theme': 'material-light'
|
||||
},
|
||||
'dark': {
|
||||
'key': 'dark',
|
||||
'background_color': '#757575',
|
||||
'css_label': 'dark-theme',
|
||||
'social_theme': 'material-dark'
|
||||
},
|
||||
'light': {
|
||||
'key': 'light',
|
||||
'background_color': 'white',
|
||||
'css_label': 'light-theme',
|
||||
'social_theme': 'material-light'
|
||||
}
|
||||
};
|
||||
|
||||
export {THEMES_CONFIG};
|
||||
Reference in New Issue
Block a user