mirror of
https://github.com/Tzahi12345/YoutubeDL-Material.git
synced 2026-03-22 20:51:00 +03:00
Compare commits
63 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a8d2e1d890 | ||
|
|
0511996b26 | ||
|
|
f29a29bf2f | ||
|
|
24d4107311 | ||
|
|
a46f9c37c6 | ||
|
|
2b1c68bad0 | ||
|
|
e2d23404ce | ||
|
|
db208ed55e | ||
|
|
b87a9f1e2f | ||
|
|
a1ac1e450d | ||
|
|
6764ea6c3b | ||
|
|
8a52020186 | ||
|
|
695b836852 | ||
|
|
71d7c30032 | ||
|
|
5ca4f036c7 | ||
|
|
1ffe61f01f | ||
|
|
5e331b9ffa | ||
|
|
09bdae90e2 | ||
|
|
37cc8f4fe1 | ||
|
|
925e083b8d | ||
|
|
6dc42278c4 | ||
|
|
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 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -43,7 +43,10 @@ Thumbs.db
|
|||||||
|
|
||||||
node_modules/*
|
node_modules/*
|
||||||
backend/node_modules/*
|
backend/node_modules/*
|
||||||
|
backend/public/*
|
||||||
YoutubeDL-Material/node_modules/*
|
YoutubeDL-Material/node_modules/*
|
||||||
backend/video/*
|
backend/video/*
|
||||||
backend/audio/*
|
backend/audio/*
|
||||||
|
backend/public/*
|
||||||
|
backend/db.json
|
||||||
src/assets/default.json
|
src/assets/default.json
|
||||||
|
|||||||
21
Dockerfile
Normal file
21
Dockerfile
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
FROM alpine:3.11
|
||||||
|
|
||||||
|
RUN apk add --update npm python ffmpeg
|
||||||
|
|
||||||
|
# Change directory so that our commands run inside this new directory
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy dependency definitions
|
||||||
|
COPY ./ /app/
|
||||||
|
|
||||||
|
# Change directory to backend
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Install dependencies on backend
|
||||||
|
RUN npm install
|
||||||
|
|
||||||
|
# Expose the port the app runs in
|
||||||
|
EXPOSE 17442
|
||||||
|
|
||||||
|
# Run the specified command within the container.
|
||||||
|
CMD [ "node", "app.js" ]
|
||||||
71
README.md
71
README.md
@@ -1,6 +1,8 @@
|
|||||||
# YoutubeDL-Material
|
# 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
|
## Getting Started
|
||||||
|
|
||||||
@@ -8,43 +10,88 @@ 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:
|
Here's an image of what it'll look like once you're done:
|
||||||
|
|
||||||

|

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

|

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

|
||||||
|
|
||||||
### Prerequisites
|
### 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 skip 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
|
### Installing
|
||||||
|
|
||||||
First, download the [latest release](https://github.com/Tzahi12345/YoutubeDL-Material/releases/latest)!
|
1. First, download the [latest release](https://github.com/Tzahi12345/YoutubeDL-Material/releases/latest)!
|
||||||
|
|
||||||
Drag all the files in `youtubedl-material` to a location accessible to a web server. It works best if it's the root (usually right inside `public_html`. Once that's done, navigate to `backend` and edit the `default.json` file. If you're using SSL encryption, look at the `encrypted.json` file for a template.
|
2. Drag all the files in `youtubedl-material` to an easily accessible directory. Navigate to the `config` folder and edit the `default.json` file. If you're using SSL encryption, look at the `encrypted.json` file for a template.
|
||||||
|
|
||||||
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.
|
NOTE: If you are intending to use a reverse proxy, this next step is not necessary
|
||||||
|
3. Port forward the port listed in `default.json`, which defaults to `17442`.
|
||||||
|
|
||||||
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.
|
4. 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, which serves the frontend as well. On your browser, navigate to to the server (url with the specified port). 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.
|
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
|
||||||
|
|
||||||
|
NOTE: If you are using YoutubeDL-Material v3.2 or lower, click [here](https://github.com/Tzahi12345/YoutubeDL-Material/blob/b87a9f1e2fd896b8e3b2f12429b7ffb15ea4521b/README.md#configuration) for the old README
|
||||||
|
|
||||||
|
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 |
|
||||||
|
| ------------- | ------------- | ------------- |
|
||||||
|
| url | URL to the server hosting YoutubeDL-Material | "http://example.com" |
|
||||||
|
| port | Desired port for YoutubeDL-Material | "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-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 |
|
||||||
|
| allow_multi_download_mode | true if you want the ability to download multiple videos at the same time | true |
|
||||||
|
| 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 |
|
||||||
|
| use_default_downloading_agent | true if you want to use youtube-dl's default downloader | true |
|
||||||
|
| custom_downloading_agent | If not using the default downloader, this is the downloader you want to use | "" |
|
||||||
|
| allow_advanced_download | true if you want to use the Advanced download options - NOT FULLY IMPLEMENTED | false |
|
||||||
|
|
||||||
## Deployment
|
## 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.
|
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.
|
||||||
|
|
||||||
To deploy, simply clone the repository, and go into the `youtubedl-material` directory. Type `npm install` and all the dependencies will install. Then type `cd backend` and again type `npm install` to install the dependencies for the backend.
|
To deploy, simply clone the repository, and go into the `youtubedl-material` directory. Type `npm install` and all the dependencies will install. Then type `cd backend` and again type `npm install` to install the dependencies for the backend.
|
||||||
|
|
||||||
Once you do that, you're almost up and running. All you need to do is edit the configuration in `youtubedl-material/backend/config`, go back into the `youtubedl-material` directory, and type `ng build --prod`. This will build the app, and put the output files in the `youtubedl-material/dist` folder. Drag those files into a web server, and drag the `backend` directory into the same folder. This folder should have `index.html` in it as well. If it does **not**, you're in the wrong directory.
|
Once you do that, you're almost up and running. All you need to do is edit the configuration in `youtubedl-material/backend/config`, go back into the `youtubedl-material` directory, and type `ng build --prod`. This will build the app, and put the output files in the `youtubedl-material/dist` folder. Drag those files into the `public` directory in the `backend` folder.
|
||||||
|
|
||||||
The frontend is now complete. The backend is much easier. Just go into the `youtubedl-material/backend` folder, and type `sudo nodejs app.js`.
|
The frontend is now complete. The backend is much easier. Just go into the `backend` folder, and type `nodejs app.js`.
|
||||||
|
|
||||||
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.
|
Finally, port forward the port specified in the config (defaults to `17442`) and point it to the server's IP address. Make sure the port is also allowed through the server's 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 -L 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. The default options will work, however, and point to `http://localhost:8998`. You can find an explanation of these configuration items in [Configuration](#Configuration) section.
|
||||||
|
3. Run `docker-compose pull`. This will download the official YoutubeDL-Material docker image.
|
||||||
|
4. Run `docker-compose up` to start it up. If successful, it should say "HTTP(S): Started on port 8998" or something similar.
|
||||||
|
5. Make sure you can connect to the specified URL + port, and if so, you are done!
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
|
|||||||
581
backend/app.js
581
backend/app.js
@@ -2,7 +2,7 @@ var async = require('async');
|
|||||||
var fs = require('fs');
|
var fs = require('fs');
|
||||||
var path = require('path');
|
var path = require('path');
|
||||||
var youtubedl = require('youtube-dl');
|
var youtubedl = require('youtube-dl');
|
||||||
var config = require('config');
|
var compression = require('compression');
|
||||||
var https = require('https');
|
var https = require('https');
|
||||||
var express = require("express");
|
var express = require("express");
|
||||||
var bodyParser = require("body-parser");
|
var bodyParser = require("body-parser");
|
||||||
@@ -10,6 +10,7 @@ var archiver = require('archiver');
|
|||||||
const low = require('lowdb')
|
const low = require('lowdb')
|
||||||
var URL = require('url').URL;
|
var URL = require('url').URL;
|
||||||
const shortid = require('shortid')
|
const shortid = require('shortid')
|
||||||
|
var config_api = require('./config.js');
|
||||||
|
|
||||||
var app = express();
|
var app = express();
|
||||||
|
|
||||||
@@ -18,62 +19,56 @@ const adapter = new FileSync('db.json');
|
|||||||
const db = low(adapter)
|
const db = low(adapter)
|
||||||
|
|
||||||
// Set some defaults
|
// Set some defaults
|
||||||
db.defaults({ playlists: {
|
db.defaults(
|
||||||
audio: [],
|
{
|
||||||
video: []
|
playlists: {
|
||||||
}}).write();
|
audio: [],
|
||||||
|
video: []
|
||||||
|
},
|
||||||
|
configWriteFlag: false
|
||||||
|
}).write();
|
||||||
|
|
||||||
|
// config values
|
||||||
|
var frontendUrl = null;
|
||||||
|
var backendUrl = null;
|
||||||
|
var backendPort = null;
|
||||||
|
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
|
// check if debug mode
|
||||||
let debugMode = process.env.YTDL_MODE === 'debug';
|
let debugMode = process.env.YTDL_MODE === 'debug';
|
||||||
|
|
||||||
if (debugMode) console.log('YTDL-Material in debug mode!');
|
if (debugMode) console.log('YTDL-Material in debug mode!');
|
||||||
|
|
||||||
var frontendUrl = !debugMode ? config.get("YoutubeDLMaterial.Host.frontendurl") : 'http://localhost:4200';
|
var validDownloadingAgents = [
|
||||||
var backendUrl = config.get("YoutubeDLMaterial.Host.backendurl")
|
'aria2c'
|
||||||
var backendPort = 17442;
|
]
|
||||||
var usingEncryption = config.get("YoutubeDLMaterial.Encryption.use-encryption");
|
|
||||||
var basePath = config.get("YoutubeDLMaterial.Downloader.path-base");
|
// don't overwrite config if it already happened.. NOT
|
||||||
var audioFolderPath = config.get("YoutubeDLMaterial.Downloader.path-audio");
|
// let alreadyWritten = db.get('configWriteFlag').value();
|
||||||
var videoFolderPath = config.get("YoutubeDLMaterial.Downloader.path-video");
|
let writeConfigMode = process.env.write_ytdl_config;
|
||||||
var downloadOnlyMode = config.get("YoutubeDLMaterial.Extra.download_only_mode")
|
var config = null;
|
||||||
|
|
||||||
|
if (writeConfigMode) {
|
||||||
|
setAndLoadConfig();
|
||||||
|
} else {
|
||||||
|
loadConfig();
|
||||||
|
}
|
||||||
|
|
||||||
var descriptors = {};
|
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.urlencoded({ extended: false }));
|
||||||
app.use(bodyParser.json());
|
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");
|
|
||||||
});
|
|
||||||
|
|
||||||
// objects
|
// objects
|
||||||
|
|
||||||
function File(id, title, thumbnailURL, isAudio, duration) {
|
function File(id, title, thumbnailURL, isAudio, duration) {
|
||||||
@@ -86,6 +81,107 @@ function File(id, title, thumbnailURL, isAudio, duration) {
|
|||||||
|
|
||||||
// actual functions
|
// actual functions
|
||||||
|
|
||||||
|
function startServer() {
|
||||||
|
if (usingEncryption)
|
||||||
|
{
|
||||||
|
https.createServer(options, app).listen(backendPort, function() {
|
||||||
|
console.log('HTTPS: Started on PORT ' + backendPort);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
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');
|
||||||
|
|
||||||
|
url = !debugMode ? config_api.getConfigItem('ytdl_url') : 'http://localhost:4200';
|
||||||
|
backendPort = config_api.getConfigItem('ytdl_port');
|
||||||
|
usingEncryption = config_api.getConfigItem('ytdl_use_encryption');
|
||||||
|
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(url);
|
||||||
|
|
||||||
|
// 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)
|
function getThumbnailMp3(name)
|
||||||
{
|
{
|
||||||
var obj = getJSONMp3(name);
|
var obj = getJSONMp3(name);
|
||||||
@@ -311,6 +407,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) {
|
function getAudioInfos(fileNames) {
|
||||||
let result = [];
|
let result = [];
|
||||||
for (let i = 0; i < fileNames.length; i++) {
|
for (let i = 0; i < fileNames.length; i++) {
|
||||||
@@ -347,9 +467,15 @@ function getVideoInfos(fileNames) {
|
|||||||
|
|
||||||
// currently only works for single urls
|
// currently only works for single urls
|
||||||
async function getUrlInfos(urls) {
|
async function getUrlInfos(urls) {
|
||||||
|
let startDate = Date.now();
|
||||||
let result = [];
|
let result = [];
|
||||||
return new Promise(resolve => {
|
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) {
|
if (err) {
|
||||||
console.log('Error during parsing:' + err);
|
console.log('Error during parsing:' + err);
|
||||||
resolve(null);
|
resolve(null);
|
||||||
@@ -360,35 +486,77 @@ async function getUrlInfos(urls) {
|
|||||||
result = try_putput;
|
result = try_putput;
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
// probably multiple urls
|
// probably multiple urls
|
||||||
console.log('failed to parse');
|
console.log('failed to parse for urls starting with ' + urls[0]);
|
||||||
console.log(output);
|
// console.log(output);
|
||||||
}
|
}
|
||||||
resolve(result);
|
resolve(result);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
app.post('/tomp3', function(req, res) {
|
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.use(compression());
|
||||||
|
|
||||||
|
app.get('/api/config', function(req, res) {
|
||||||
|
let config_file = config_api.getConfigFile();
|
||||||
|
res.send({
|
||||||
|
config_file: config_file,
|
||||||
|
success: !!config_file
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get('/api/using-encryption', function(req, res) {
|
||||||
|
res.send(usingEncryption);
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/api/tomp3', function(req, res) {
|
||||||
var url = req.body.url;
|
var url = req.body.url;
|
||||||
var date = Date.now();
|
var date = Date.now();
|
||||||
var path = audioFolderPath;
|
|
||||||
var audiopath = '%(title)s';
|
var audiopath = '%(title)s';
|
||||||
|
|
||||||
var customQualityConfiguration = req.body.customQualityConfiguration;
|
var customQualityConfiguration = req.body.customQualityConfiguration;
|
||||||
var maxBitrate = req.body.maxBitrate;
|
var maxBitrate = req.body.maxBitrate;
|
||||||
|
var customArgs = req.body.customArgs;
|
||||||
|
var customOutput = req.body.customOutput;
|
||||||
|
var youtubeUsername = req.body.youtubeUsername;
|
||||||
|
var youtubePassword = req.body.youtubePassword;
|
||||||
|
|
||||||
let downloadConfig = ['--external-downloader', 'aria2c', '-o', path + audiopath + ".mp3", '-x', '--audio-format', 'mp3', '--write-info-json', '--print-json']
|
|
||||||
|
let downloadConfig = null;
|
||||||
let qualityPath = '';
|
let qualityPath = '';
|
||||||
|
|
||||||
if (customQualityConfiguration) {
|
if (customArgs) {
|
||||||
qualityPath = `-f ${customQualityConfiguration}`;
|
downloadConfig = customArgs.split(' ');
|
||||||
} else if (maxBitrate) {
|
} else {
|
||||||
if (!maxBitrate || maxBitrate === '') maxBitrate = '0';
|
if (customOutput) {
|
||||||
qualityPath = `--audio-quality ${maxBitrate}`
|
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 !== '') {
|
if (customQualityConfiguration) {
|
||||||
downloadConfig.splice(2, 0, qualityPath);
|
qualityPath = `-f ${customQualityConfiguration}`;
|
||||||
|
} else if (maxBitrate) {
|
||||||
|
if (!maxBitrate || maxBitrate === '') maxBitrate = '0';
|
||||||
|
qualityPath = `--audio-quality ${maxBitrate}`
|
||||||
|
}
|
||||||
|
|
||||||
|
if (youtubeUsername && youtubePassword) {
|
||||||
|
downloadConfig.push('--username', youtubeUsername, '--password', youtubePassword);
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
youtubedl.exec(url, downloadConfig, {}, function(err, output) {
|
||||||
@@ -399,6 +567,7 @@ app.post('/tomp3', function(req, res) {
|
|||||||
}
|
}
|
||||||
if (err) {
|
if (err) {
|
||||||
audiopath = "-1";
|
audiopath = "-1";
|
||||||
|
console.log(err.stderr);
|
||||||
res.sendStatus(500);
|
res.sendStatus(500);
|
||||||
throw err;
|
throw err;
|
||||||
} else if (output) {
|
} else if (output) {
|
||||||
@@ -411,16 +580,18 @@ app.post('/tomp3', function(req, res) {
|
|||||||
output_json = null;
|
output_json = null;
|
||||||
}
|
}
|
||||||
if (!output_json) {
|
if (!output_json) {
|
||||||
// only run on first go
|
// if invalid, continue onto the next
|
||||||
return;
|
continue;
|
||||||
}
|
}
|
||||||
var file_name = output_json['_filename'].replace(/^.*[\\\/]/, '');
|
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);
|
var alternate_file_name = file_name.substring(0, file_name.length-4);
|
||||||
if (alternate_file_name) file_names.push(alternate_file_name);
|
if (alternate_file_path) file_names.push(alternate_file_path);
|
||||||
}
|
}
|
||||||
|
|
||||||
let is_playlist = file_names.length > 1;
|
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]);
|
var audiopathEncoded = encodeURIComponent(file_names[0]);
|
||||||
res.send({
|
res.send({
|
||||||
@@ -431,24 +602,47 @@ app.post('/tomp3', function(req, res) {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
app.post('/tomp4', function(req, res) {
|
app.post('/api/tomp4', function(req, res) {
|
||||||
var url = req.body.url;
|
var url = req.body.url;
|
||||||
var date = Date.now();
|
var date = Date.now();
|
||||||
var path = videoFolderPath;
|
var path = videoFolderPath;
|
||||||
var videopath = '%(title)s';
|
var videopath = '%(title)s';
|
||||||
|
var customArgs = req.body.customArgs;
|
||||||
|
var customOutput = req.body.customOutput;
|
||||||
|
|
||||||
var selectedHeight = req.body.selectedHeight;
|
var selectedHeight = req.body.selectedHeight;
|
||||||
var customQualityConfiguration = req.body.customQualityConfiguration;
|
var customQualityConfiguration = req.body.customQualityConfiguration;
|
||||||
|
var youtubeUsername = req.body.youtubeUsername;
|
||||||
|
var youtubePassword = req.body.youtubePassword;
|
||||||
|
|
||||||
|
let downloadConfig = null;
|
||||||
let qualityPath = 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/mp4';
|
let qualityPath = 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/mp4';
|
||||||
|
|
||||||
if (customQualityConfiguration) {
|
if (customArgs) {
|
||||||
qualityPath = customQualityConfiguration;
|
downloadConfig = customArgs.split(' ');
|
||||||
} else if (selectedHeight && selectedHeight !== '') {
|
} else {
|
||||||
qualityPath = `bestvideo[height=${selectedHeight}]+bestaudio/best[height=${selectedHeight}]`;
|
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 (youtubeUsername && youtubePassword) {
|
||||||
|
downloadConfig.push('--username', youtubeUsername, '--password', youtubePassword);
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
if (debugMode) {
|
||||||
let new_date = Date.now();
|
let new_date = Date.now();
|
||||||
let difference = (new_date - date)/1000;
|
let difference = (new_date - date)/1000;
|
||||||
@@ -456,6 +650,7 @@ app.post('/tomp4', function(req, res) {
|
|||||||
}
|
}
|
||||||
if (err) {
|
if (err) {
|
||||||
videopath = "-1";
|
videopath = "-1";
|
||||||
|
console.log(err.stderr);
|
||||||
res.sendStatus(500);
|
res.sendStatus(500);
|
||||||
throw err;
|
throw err;
|
||||||
} else if (output) {
|
} else if (output) {
|
||||||
@@ -482,7 +677,9 @@ app.post('/tomp4', function(req, res) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
var alternate_file_name = file_name.substring(0, file_name.length-4);
|
var alternate_file_name = file_name.substring(0, file_name.length-4);
|
||||||
if (alternate_file_name) file_names.push(alternate_file_name);
|
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;
|
let is_playlist = file_names.length > 1;
|
||||||
@@ -499,7 +696,7 @@ app.post('/tomp4', function(req, res) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// gets the status of the mp3 file that's being downloaded
|
// gets the status of the mp3 file that's being downloaded
|
||||||
app.post('/fileStatusMp3', function(req, res) {
|
app.post('/api/fileStatusMp3', function(req, res) {
|
||||||
var name = decodeURI(req.body.name + "");
|
var name = decodeURI(req.body.name + "");
|
||||||
var exists = "";
|
var exists = "";
|
||||||
var fullpath = audioFolderPath + name + ".mp3";
|
var fullpath = audioFolderPath + name + ".mp3";
|
||||||
@@ -521,7 +718,7 @@ app.post('/fileStatusMp3', function(req, res) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// gets the status of the mp4 file that's being downloaded
|
// gets the status of the mp4 file that's being downloaded
|
||||||
app.post('/fileStatusMp4', function(req, res) {
|
app.post('/api/fileStatusMp4', function(req, res) {
|
||||||
var name = decodeURI(req.body.name);
|
var name = decodeURI(req.body.name);
|
||||||
var exists = "";
|
var exists = "";
|
||||||
var fullpath = videoFolderPath + name + ".mp4";
|
var fullpath = videoFolderPath + name + ".mp4";
|
||||||
@@ -541,34 +738,28 @@ app.post('/fileStatusMp4', function(req, res) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// gets all download mp3s
|
// gets all download mp3s
|
||||||
app.post('/getMp3s', function(req, res) {
|
app.post('/api/getMp3s', function(req, res) {
|
||||||
var mp3s = [];
|
var mp3s = [];
|
||||||
var playlists = db.get('playlists.audio').value();
|
var playlists = db.get('playlists.audio').value();
|
||||||
var fullpath = audioFolderPath;
|
var files = recFindByExt(audioFolderPath, 'mp3'); // fs.readdirSync(audioFolderPath);
|
||||||
var files = fs.readdirSync(audioFolderPath);
|
for (let i = 0; i < files.length; i++) {
|
||||||
|
let file = files[i];
|
||||||
for (var i in files)
|
var file_path = file.substring(audioFolderPath.length, file.length);
|
||||||
{
|
var id = file_path.substring(0, file_path.length-4);
|
||||||
var nameLength = path.basename(files[i]).length;
|
var jsonobj = getJSONMp3(id);
|
||||||
var ext = path.basename(files[i]).substring(nameLength-4, nameLength);
|
if (!jsonobj) continue;
|
||||||
if (ext == ".mp3")
|
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));
|
title = title.substring(0,12) + "...";
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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({
|
res.send({
|
||||||
@@ -579,34 +770,29 @@ app.post('/getMp3s', function(req, res) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// gets all download mp4s
|
// gets all download mp4s
|
||||||
app.post('/getMp4s', function(req, res) {
|
app.post('/api/getMp4s', function(req, res) {
|
||||||
var mp4s = [];
|
var mp4s = [];
|
||||||
var playlists = db.get('playlists.video').value();
|
var playlists = db.get('playlists.video').value();
|
||||||
var fullpath = videoFolderPath;
|
var fullpath = videoFolderPath;
|
||||||
var files = fs.readdirSync(videoFolderPath);
|
var files = recFindByExt(videoFolderPath, 'mp4');
|
||||||
|
for (let i = 0; i < files.length; i++) {
|
||||||
for (var i in files)
|
let file = files[i];
|
||||||
{
|
var file_path = file.substring(videoFolderPath.length, file.length);
|
||||||
var nameLength = path.basename(files[i]).length;
|
var id = file_path.substring(0, file_path.length-4);
|
||||||
var ext = path.basename(files[i]).substring(nameLength-4, nameLength);
|
var jsonobj = getJSONMp4(id);
|
||||||
if (ext == ".mp4")
|
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));
|
title = title.substring(0,12) + "...";
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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({
|
res.send({
|
||||||
@@ -616,7 +802,7 @@ app.post('/getMp4s', function(req, res) {
|
|||||||
res.end("yes");
|
res.end("yes");
|
||||||
});
|
});
|
||||||
|
|
||||||
app.post('/createPlaylist', async (req, res) => {
|
app.post('/api/createPlaylist', async (req, res) => {
|
||||||
let playlistName = req.body.playlistName;
|
let playlistName = req.body.playlistName;
|
||||||
let fileNames = req.body.fileNames;
|
let fileNames = req.body.fileNames;
|
||||||
let type = req.body.type;
|
let type = req.body.type;
|
||||||
@@ -639,7 +825,33 @@ app.post('/createPlaylist', async (req, res) => {
|
|||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
app.post('/deletePlaylist', async (req, res) => {
|
app.post('/api/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('/api/deletePlaylist', async (req, res) => {
|
||||||
let playlistID = req.body.playlistID;
|
let playlistID = req.body.playlistID;
|
||||||
let type = req.body.type;
|
let type = req.body.type;
|
||||||
|
|
||||||
@@ -661,7 +873,7 @@ app.post('/deletePlaylist', async (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// deletes mp3 file
|
// deletes mp3 file
|
||||||
app.post('/deleteMp3', async (req, res) => {
|
app.post('/api/deleteMp3', async (req, res) => {
|
||||||
var name = req.body.name;
|
var name = req.body.name;
|
||||||
var fullpath = audioFolderPath + name + ".mp3";
|
var fullpath = audioFolderPath + name + ".mp3";
|
||||||
var wasDeleted = false;
|
var wasDeleted = false;
|
||||||
@@ -681,7 +893,7 @@ app.post('/deleteMp3', async (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// deletes mp4 file
|
// deletes mp4 file
|
||||||
app.post('/deleteMp4', async (req, res) => {
|
app.post('/api/deleteMp4', async (req, res) => {
|
||||||
var name = req.body.name;
|
var name = req.body.name;
|
||||||
var fullpath = videoFolderPath + name + ".mp4";
|
var fullpath = videoFolderPath + name + ".mp4";
|
||||||
var wasDeleted = false;
|
var wasDeleted = false;
|
||||||
@@ -700,26 +912,30 @@ app.post('/deleteMp4', async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
app.post('/downloadFile', async (req, res) => {
|
app.post('/api/downloadFile', async (req, res) => {
|
||||||
let fileNames = req.body.fileNames;
|
let fileNames = req.body.fileNames;
|
||||||
let is_playlist = req.body.is_playlist;
|
let is_playlist = req.body.is_playlist;
|
||||||
let type = req.body.type;
|
let type = req.body.type;
|
||||||
let outputName = req.body.outputName;
|
let outputName = req.body.outputName;
|
||||||
let file = null;
|
let file = null;
|
||||||
if (!is_playlist) {
|
if (!is_playlist) {
|
||||||
|
fileNames = decodeURI(fileNames);
|
||||||
if (type === 'audio') {
|
if (type === 'audio') {
|
||||||
file = __dirname + '/' + 'audio/' + fileNames + '.mp3';
|
file = __dirname + '/' + audioFolderPath + fileNames + '.mp3';
|
||||||
} else if (type === 'video') {
|
} else if (type === 'video') {
|
||||||
file = __dirname + '/' + 'video/' + fileNames + '.mp4';
|
file = __dirname + '/' + videoFolderPath + fileNames + '.mp4';
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
for (let i = 0; i < fileNames.length; i++) {
|
||||||
|
fileNames[i] = decodeURI(fileNames[i]);
|
||||||
|
}
|
||||||
file = await createPlaylistZipFile(fileNames, type, outputName);
|
file = await createPlaylistZipFile(fileNames, type, outputName);
|
||||||
}
|
}
|
||||||
|
|
||||||
res.sendFile(file);
|
res.sendFile(file);
|
||||||
});
|
});
|
||||||
|
|
||||||
app.post('/deleteFile', async (req, res) => {
|
app.post('/api/deleteFile', async (req, res) => {
|
||||||
let fileName = req.body.fileName;
|
let fileName = req.body.fileName;
|
||||||
let type = req.body.type;
|
let type = req.body.type;
|
||||||
if (type === 'audio') {
|
if (type === 'audio') {
|
||||||
@@ -730,48 +946,50 @@ app.post('/deleteFile', async (req, res) => {
|
|||||||
res.send()
|
res.send()
|
||||||
});
|
});
|
||||||
|
|
||||||
app.get('/video/:id', function(req , res){
|
app.get('/api/video/:id', function(req , res){
|
||||||
var head;
|
var head;
|
||||||
const path = "video/" + req.params.id + '.mp4';
|
let id = decodeURI(req.params.id);
|
||||||
const stat = fs.statSync(path)
|
const path = "video/" + id + '.mp4';
|
||||||
const fileSize = stat.size
|
const stat = fs.statSync(path)
|
||||||
const range = req.headers.range
|
const fileSize = stat.size
|
||||||
if (range) {
|
const range = req.headers.range
|
||||||
const parts = range.replace(/bytes=/, "").split("-")
|
if (range) {
|
||||||
const start = parseInt(parts[0], 10)
|
const parts = range.replace(/bytes=/, "").split("-")
|
||||||
const end = parts[1]
|
const start = parseInt(parts[0], 10)
|
||||||
? parseInt(parts[1], 10)
|
const end = parts[1]
|
||||||
: fileSize-1
|
? parseInt(parts[1], 10)
|
||||||
const chunksize = (end-start)+1
|
: fileSize-1
|
||||||
const file = fs.createReadStream(path, {start, end})
|
const chunksize = (end-start)+1
|
||||||
if (descriptors[req.params.id]) descriptors[req.params.id].push(file);
|
const file = fs.createReadStream(path, {start, end})
|
||||||
else descriptors[req.params.id] = [file];
|
if (descriptors[id]) descriptors[id].push(file);
|
||||||
file.on('close', function() {
|
else descriptors[id] = [file];
|
||||||
let index = descriptors[req.params.id].indexOf(file);
|
file.on('close', function() {
|
||||||
descriptors[req.params.id].splice(index, 1);
|
let index = descriptors[id].indexOf(file);
|
||||||
if (debugMode) console.log('Successfully closed stream and removed file reference.');
|
descriptors[id].splice(index, 1);
|
||||||
});
|
if (debugMode) console.log('Successfully closed stream and removed file reference.');
|
||||||
head = {
|
});
|
||||||
'Content-Range': `bytes ${start}-${end}/${fileSize}`,
|
head = {
|
||||||
'Accept-Ranges': 'bytes',
|
'Content-Range': `bytes ${start}-${end}/${fileSize}`,
|
||||||
'Content-Length': chunksize,
|
'Accept-Ranges': 'bytes',
|
||||||
'Content-Type': 'video/mp4',
|
'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){
|
app.get('/api/audio/:id', function(req , res){
|
||||||
var head;
|
var head;
|
||||||
let path = "audio/" + req.params.id + '.mp3';
|
let id = decodeURI(req.params.id);
|
||||||
|
let path = "audio/" + id + '.mp3';
|
||||||
path = path.replace(/\"/g, '\'');
|
path = path.replace(/\"/g, '\'');
|
||||||
const stat = fs.statSync(path)
|
const stat = fs.statSync(path)
|
||||||
const fileSize = stat.size
|
const fileSize = stat.size
|
||||||
@@ -784,11 +1002,11 @@ app.get('/audio/:id', function(req , res){
|
|||||||
: fileSize-1
|
: fileSize-1
|
||||||
const chunksize = (end-start)+1
|
const chunksize = (end-start)+1
|
||||||
const file = fs.createReadStream(path, {start, end});
|
const file = fs.createReadStream(path, {start, end});
|
||||||
if (descriptors[req.params.id]) descriptors[req.params.id].push(file);
|
if (descriptors[id]) descriptors[id].push(file);
|
||||||
else descriptors[req.params.id] = [file];
|
else descriptors[id] = [file];
|
||||||
file.on('close', function() {
|
file.on('close', function() {
|
||||||
let index = descriptors[req.params.id].indexOf(file);
|
let index = descriptors[id].indexOf(file);
|
||||||
descriptors[req.params.id].splice(index, 1);
|
descriptors[id].splice(index, 1);
|
||||||
if (debugMode) console.log('Successfully closed stream and removed file reference.');
|
if (debugMode) console.log('Successfully closed stream and removed file reference.');
|
||||||
});
|
});
|
||||||
head = {
|
head = {
|
||||||
@@ -810,7 +1028,7 @@ app.get('/audio/:id', function(req , res){
|
|||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
app.post('/getVideoInfos', async (req, res) => {
|
app.post('/api/getVideoInfos', async (req, res) => {
|
||||||
let fileNames = req.body.fileNames;
|
let fileNames = req.body.fileNames;
|
||||||
let urlMode = !!req.body.urlMode;
|
let urlMode = !!req.body.urlMode;
|
||||||
let type = req.body.type;
|
let type = req.body.type;
|
||||||
@@ -830,18 +1048,21 @@ app.get('/audio/:id', function(req , res){
|
|||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
|
app.use(function(req, res, next) {
|
||||||
|
//if the request is not html then move along
|
||||||
|
var accept = req.accepts('html', 'json', 'xml');
|
||||||
|
if (accept !== 'html') {
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
|
// if the request has a '.' assume that it's for a file, move along
|
||||||
|
var ext = path.extname(req.path);
|
||||||
|
if (ext !== '') {
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.createReadStream('./public/index.html').pipe(res);
|
||||||
|
|
||||||
if (usingEncryption)
|
});
|
||||||
{
|
|
||||||
https.createServer(options, app).listen(backendPort, function() {
|
app.use(express.static('./public'));
|
||||||
console.log('HTTPS: Anchor set on 17442');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
app.listen(backendPort,function(){
|
|
||||||
console.log("HTTP: Started on PORT " + backendPort);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|||||||
113
backend/config.js
Normal file
113
backend/config.js
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
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,
|
||||||
|
getConfigFile: getConfigFile,
|
||||||
|
CONFIG_ITEMS: CONFIG_ITEMS
|
||||||
|
}
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"YoutubeDLMaterial": {
|
"YoutubeDLMaterial": {
|
||||||
"Host": {
|
"Host": {
|
||||||
"frontendurl": "http://example.com",
|
"url": "http://example.com",
|
||||||
"backendurl": "http://example.com:17442/"
|
"port": "17442"
|
||||||
},
|
},
|
||||||
"Encryption": {
|
"Encryption": {
|
||||||
"use-encryption": false,
|
"use-encryption": false,
|
||||||
@@ -10,7 +10,6 @@
|
|||||||
"key-file-path": "/etc/letsencrypt/live/example.com/privkey.pem"
|
"key-file-path": "/etc/letsencrypt/live/example.com/privkey.pem"
|
||||||
},
|
},
|
||||||
"Downloader": {
|
"Downloader": {
|
||||||
"path-base": "http://example.com:17442/",
|
|
||||||
"path-audio": "audio/",
|
"path-audio": "audio/",
|
||||||
"path-video": "video/"
|
"path-video": "video/"
|
||||||
},
|
},
|
||||||
@@ -18,7 +17,8 @@
|
|||||||
"title_top": "Youtube Downloader",
|
"title_top": "Youtube Downloader",
|
||||||
"file_manager_enabled": true,
|
"file_manager_enabled": true,
|
||||||
"allow_quality_select": true,
|
"allow_quality_select": true,
|
||||||
"download_only_mode": false
|
"download_only_mode": false,
|
||||||
|
"allow_multi_download_mode": true
|
||||||
},
|
},
|
||||||
"API": {
|
"API": {
|
||||||
"use_youtube_API": false,
|
"use_youtube_API": false,
|
||||||
@@ -27,6 +27,11 @@
|
|||||||
"Themes": {
|
"Themes": {
|
||||||
"default_theme": "default",
|
"default_theme": "default",
|
||||||
"allow_theme_change": true
|
"allow_theme_change": true
|
||||||
|
},
|
||||||
|
"Advanced": {
|
||||||
|
"use_default_downloading_agent": true,
|
||||||
|
"custom_downloading_agent": "",
|
||||||
|
"allow_advanced_download": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,15 @@
|
|||||||
{
|
{
|
||||||
"YoutubeDLMaterial": {
|
"YoutubeDLMaterial": {
|
||||||
"Host": {
|
"Host": {
|
||||||
"frontendurl": "https://example.com",
|
"url": "https://example.com",
|
||||||
"backendurl": "https://example.com:17442/"
|
"port": "17442"
|
||||||
},
|
},
|
||||||
"Encryption": {
|
"Encryption": {
|
||||||
"use-encryption": true,
|
"use-encryption": true,
|
||||||
"cert-file-path": "/etc/letsencrypt/live/example.com/fullchain.pem",
|
"cert-file-path": "/etc/letsencrypt/live/example.com/fullchain.pem",
|
||||||
"key-file-path": "/etc/letsencrypt/live/example.com/privkey.pem"
|
"key-file-path": "/etc/letsencrypt/live/example.com/privkey.pem"
|
||||||
},
|
},
|
||||||
"Downloader": {
|
"Downloader": {
|
||||||
"path-base": "https://example.com:17442/",
|
|
||||||
"path-audio": "audio/",
|
"path-audio": "audio/",
|
||||||
"path-video": "video/"
|
"path-video": "video/"
|
||||||
},
|
},
|
||||||
@@ -18,7 +17,8 @@
|
|||||||
"title_top": "Youtube Downloader",
|
"title_top": "Youtube Downloader",
|
||||||
"file_manager_enabled": true,
|
"file_manager_enabled": true,
|
||||||
"allow_quality_select": true,
|
"allow_quality_select": true,
|
||||||
"download_only_mode": false
|
"download_only_mode": false,
|
||||||
|
"allow_multi_download_mode": true
|
||||||
},
|
},
|
||||||
"API": {
|
"API": {
|
||||||
"use_youtube_API": false,
|
"use_youtube_API": false,
|
||||||
@@ -27,6 +27,11 @@
|
|||||||
"Themes": {
|
"Themes": {
|
||||||
"default_theme": "default",
|
"default_theme": "default",
|
||||||
"allow_theme_change": true
|
"allow_theme_change": true
|
||||||
|
},
|
||||||
|
"Advanced": {
|
||||||
|
"use_default_downloading_agent": true,
|
||||||
|
"custom_downloading_agent": "",
|
||||||
|
"allow_advanced_download": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
96
backend/consts.js
Normal file
96
backend/consts.js
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
var config = require('config');
|
||||||
|
|
||||||
|
let CONFIG_ITEMS = {
|
||||||
|
// Host
|
||||||
|
'ytdl_url': {
|
||||||
|
'key': 'ytdl_url',
|
||||||
|
'path': 'YoutubeDLMaterial.Host.url'
|
||||||
|
},
|
||||||
|
'ytdl_port': {
|
||||||
|
'key': 'ytdl_port',
|
||||||
|
'path': 'YoutubeDLMaterial.Host.port'
|
||||||
|
},
|
||||||
|
|
||||||
|
// 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_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
@@ -4,7 +4,8 @@
|
|||||||
"description": "backend for YoutubeDL-Material",
|
"description": "backend for YoutubeDL-Material",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "echo \"Error: no test specified\" && exit 1"
|
"test": "echo \"Error: no test specified\" && exit 1",
|
||||||
|
"start": "node app.js"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@@ -19,11 +20,12 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"archiver": "^3.1.1",
|
"archiver": "^3.1.1",
|
||||||
"async": "^3.1.0",
|
"async": "^3.1.0",
|
||||||
|
"compression": "^1.7.4",
|
||||||
"config": "^3.2.3",
|
"config": "^3.2.3",
|
||||||
"exe": "^1.0.2",
|
"exe": "^1.0.2",
|
||||||
"express": "^4.17.1",
|
"express": "^4.17.1",
|
||||||
"lowdb": "^1.0.0",
|
"lowdb": "^1.0.0",
|
||||||
"shortid": "^2.2.15",
|
"shortid": "^2.2.15",
|
||||||
"youtube-dl": "^2.3.0"
|
"youtube-dl": "^3.0.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
BIN
chrome-extension.crx
Normal file
BIN
chrome-extension.crx
Normal file
Binary file not shown.
28
chrome-extension.pem
Normal file
28
chrome-extension.pem
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
-----BEGIN PRIVATE KEY-----
|
||||||
|
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDMX9Wk5SM5cIfY
|
||||||
|
6ReKX3ybY1rsbNbOzG8ceN7yyeXB0mor8pVsX1MOna2HewOyBuaaYNJRO4tJBxic
|
||||||
|
7a8zQErfgHL/i/QrVvVCpfJ7xKvq6zij5NYoqd/FBUwawqjeH5/voIcAp9z5Vmsr
|
||||||
|
kL0sxJUKy6b4IWNp3noU7Nvq2RwxnXQbKDhz8FrX6oQAnDC6gsG5a2OSPsaE4oqw
|
||||||
|
6nmonORJypmpP5hqyHY8ffXBT2lAxjHT7OnYbaCBe2TQP8+rH6rDBOhjVNtUJ089
|
||||||
|
ocTQL6LtQEPkcF4yKJmtcOwHl8OPGZs5l9i8xb4j9RuSPkm2lbzZX8sOsdGGoqJZ
|
||||||
|
q68nYhsHAgMBAAECggEAXmtKEzfPObq88B/kAcgSk+FngMHZzcmR7bgD3GwdSxnQ
|
||||||
|
dkRI9zvk7eQ35tcUwntAr4Lat6/ILjFqlBmVLxrdXHuF5Xz9jcZLYgKzz61xdYM9
|
||||||
|
dC6FKF0u5eGIIvbauGAo7jaeGFX1F3Zu5b4lP9kEOGwU1B7sxF0FzsQM5+dtCJgv
|
||||||
|
We/hWQeF+9gtoVnkCSS/Mq2p0UomXXHW0Bz4+HuHlTR9aiYbviYnotABiLUhZyzt
|
||||||
|
v5yUaktb9qniBfdLpRlq8cp06xYlTEA9gJpa4Pnok8OWUsbAiW6EiXUSaZ/cchVa
|
||||||
|
AnO8WWYvVOnnt6WHI3+QdFTnqVjE5TBX4N/7bVhHGQKBgQD0dtbFqp7vZK/jVYvE
|
||||||
|
z0WPdySOg2ZDmoSfk5ZlR1+Y9zWToHv0qu8zqoOjL8Ubxrh9fGlOow+cCVdkEuaa
|
||||||
|
jWC2AWetuRvW0Z5A3XMXr0/N/1fgOkTqtp3WNrUPjVJahEg3lN+90opgFoT8swSi
|
||||||
|
s1oxW0oLcVIlrjhGBXAPCfsAuQKBgQDWBLRhHsRAvGcK5wGuVnxVApTIyBOermsW
|
||||||
|
3bJt+7+UI+4sYrBAwkWdQG93IG0cQtn48TEPBgmR2fjRF5IFT9M4/u+QOeeByT7I
|
||||||
|
we7nVtHgSY5ByC9N0mjWbcmSg8fktz/LonjldNC4kWdOFb75fxGf8kOGS5rUaMA4
|
||||||
|
zHucfB6ZvwKBgQCPHJrysMXGY21MaqIeHzEboaX3ABl37hdBzAa5V6UxSVdGCydF
|
||||||
|
vmO2HVZey/JaJmWOoKyNaowSzq0oWqBBTg6VvhDR9JHFmoVId9uOvAS+FYN+Mt5x
|
||||||
|
gWK5KuGoLxVNBC+6yh6JY526TrSfsrU+Aj0Es+qO9FIg2PL8muZVB4S3kQKBgH/5
|
||||||
|
CDMaxpc/EQ5/2413wZjDllwI51J3USm3Hz6Mzp2ybnSz/lh60k2Zfg1polTH1Lb6
|
||||||
|
4i7tmUNRZ2sAARyUAuWN64n+VeRRhe1dqZFDZPQMh7fmEAMk0fOGaoXlrt2ghdEq
|
||||||
|
Mchi9Xun1nHmpu9hgBR4NNBU3RwuFuLfwvprbZDZAoGAWa62QJChE86xQGP1MrL2
|
||||||
|
SbIzw3cfeP5xdQ3MKldJiy5IkbMR7Z13WZ7FwvPTy0g/onLHD1rqlm1kUMsGRHpD
|
||||||
|
5vH06PNpKXQ6x8BYaRGtE6P39jLycO/X+WK/lYTrWo1bR+mGCebDh4B5XrwT3gI6
|
||||||
|
x4Gvz134pZCTyQCf5JCwbQs=
|
||||||
|
-----END PRIVATE KEY-----
|
||||||
20
chrome-extension/background.js
Normal file
20
chrome-extension/background.js
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
// background.js
|
||||||
|
|
||||||
|
// Called when the user clicks on the browser action.
|
||||||
|
chrome.browserAction.onClicked.addListener(function(tab) {
|
||||||
|
// get the frontend_url
|
||||||
|
chrome.storage.sync.get({
|
||||||
|
frontend_url: 'http://localhost',
|
||||||
|
audio_only: false
|
||||||
|
}, function(items) {
|
||||||
|
chrome.tabs.query({active: true, currentWindow: true}, function(tabs) {
|
||||||
|
var activeTab = tabs[0];
|
||||||
|
var url = activeTab.url;
|
||||||
|
if (url.includes('youtube.com')) {
|
||||||
|
var new_url = items.frontend_url + '/#/home;url=' + encodeURIComponent(url) + ';audioOnly=' + items.audio_only;
|
||||||
|
chrome.tabs.create({ url: new_url });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
BIN
chrome-extension/favicon.png
Normal file
BIN
chrome-extension/favicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.5 KiB |
20
chrome-extension/manifest.json
Normal file
20
chrome-extension/manifest.json
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"manifest_version": 2,
|
||||||
|
"name": "YoutubeDL-Material",
|
||||||
|
"version": "0.1",
|
||||||
|
"description": "The official chrome extension of YoutubeDL-Material, an open-source and self-hosted YouTube downloader.",
|
||||||
|
"background": {
|
||||||
|
"scripts": ["background.js"]
|
||||||
|
},
|
||||||
|
"browser_action": {
|
||||||
|
"default_icon": "favicon.png"
|
||||||
|
},
|
||||||
|
"permissions": [
|
||||||
|
"tabs",
|
||||||
|
"storage"
|
||||||
|
],
|
||||||
|
"options_ui": {
|
||||||
|
"page": "options.html",
|
||||||
|
"open_in_tab": false
|
||||||
|
}
|
||||||
|
}
|
||||||
29
chrome-extension/options.html
Normal file
29
chrome-extension/options.html
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head><title>YoutubeDL-Material Extension Options</title></head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<h2>Settings</h2>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h4>Frontend URL</h4>
|
||||||
|
<input placeholder="Frontend URL" type="text" id="frontend_url">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" id="audio_only">
|
||||||
|
Audio only
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
<div id="status"></div>
|
||||||
|
<button id="save">Save</button>
|
||||||
|
|
||||||
|
<script src="options.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
31
chrome-extension/options.js
Normal file
31
chrome-extension/options.js
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
// Saves options to chrome.storage
|
||||||
|
function save_options() {
|
||||||
|
var frontend_url = document.getElementById('frontend_url').value;
|
||||||
|
var audio_only = document.getElementById('audio_only').checked;
|
||||||
|
chrome.storage.sync.set({
|
||||||
|
frontend_url: frontend_url,
|
||||||
|
audio_only: audio_only
|
||||||
|
}, function() {
|
||||||
|
// Update status to let user know options were saved.
|
||||||
|
var status = document.getElementById('status');
|
||||||
|
status.textContent = 'Options saved.';
|
||||||
|
setTimeout(function() {
|
||||||
|
status.textContent = '';
|
||||||
|
}, 750);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restores select box and checkbox state using the preferences
|
||||||
|
// stored in chrome.storage.
|
||||||
|
function restore_options() {
|
||||||
|
chrome.storage.sync.get({
|
||||||
|
frontend_url: 'http://localhost',
|
||||||
|
audio_only: false
|
||||||
|
}, function(items) {
|
||||||
|
document.getElementById('frontend_url').value = items.frontend_url;
|
||||||
|
document.getElementById('audio_only').checked = items.audio_only;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
document.addEventListener('DOMContentLoaded', restore_options);
|
||||||
|
document.getElementById('save').addEventListener('click',
|
||||||
|
save_options);
|
||||||
33
docker-compose.yml
Normal file
33
docker-compose.yml
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
|
||||||
|
version: "2"
|
||||||
|
services:
|
||||||
|
ytdl_material:
|
||||||
|
build: .
|
||||||
|
environment:
|
||||||
|
# config items
|
||||||
|
ytdl_url: http://localhost:8998
|
||||||
|
ytdl_port: '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_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:
|
||||||
|
- "8998:17442"
|
||||||
|
image: tzahi12345/youtubedl-material:3.3
|
||||||
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
12768
package-lock.json
generated
Normal file
12768
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -11,6 +11,10 @@
|
|||||||
"e2e": "ng e2e",
|
"e2e": "ng e2e",
|
||||||
"electron": "ng build --base-href ./ && electron ."
|
"electron": "ng build --base-href ./ && electron ."
|
||||||
},
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "12.3.1",
|
||||||
|
"npm": "6.10.3"
|
||||||
|
},
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@angular-devkit/core": "^8.3.12",
|
"@angular-devkit/core": "^8.3.12",
|
||||||
@@ -35,7 +39,8 @@
|
|||||||
"tslib": "^1.10.0",
|
"tslib": "^1.10.0",
|
||||||
"videogular2": "^7.0.1",
|
"videogular2": "^7.0.1",
|
||||||
"web-animations-js": "^2.3.2",
|
"web-animations-js": "^2.3.2",
|
||||||
"zone.js": "~0.9.1"
|
"zone.js": "~0.9.1",
|
||||||
|
"typescript": "~3.5.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@angular-devkit/build-angular": "^0.803.24",
|
"@angular-devkit/build-angular": "^0.803.24",
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<div [style.background]="postsService.theme ? postsService.theme.background_color : null" style="width: 100%; height: 100%;">
|
<div [style.background]="postsService.theme ? postsService.theme.background_color : null" style="width: 100%; min-height: 100%;">
|
||||||
<mat-toolbar color="primary" class="top">
|
<mat-toolbar color="primary" class="top">
|
||||||
<div class="flex-row" width="100%" height="100%">
|
<div class="flex-row" width="100%" height="100%">
|
||||||
<div class="flex-column" style="text-align: left; margin-top: 1px;">
|
<div class="flex-column" style="text-align: left; margin-top: 1px;">
|
||||||
|
|||||||
@@ -37,10 +37,11 @@ export class AppComponent implements OnInit {
|
|||||||
@ViewChild('urlinput', { read: ElementRef, static: false }) urlInput: ElementRef;
|
@ViewChild('urlinput', { read: ElementRef, static: false }) urlInput: ElementRef;
|
||||||
|
|
||||||
constructor(public postsService: PostsService, public snackBar: MatSnackBar,
|
constructor(public postsService: PostsService, public snackBar: MatSnackBar,
|
||||||
public router: Router, public overlayContainer: OverlayContainer) {
|
public router: Router, public overlayContainer: OverlayContainer, private elementRef: ElementRef) {
|
||||||
|
|
||||||
// loading config
|
// loading config
|
||||||
this.postsService.loadNavItems().subscribe(result => { // loads settings
|
this.postsService.loadNavItems().subscribe(res => { // loads settings
|
||||||
|
const result = !this.postsService.debugMode ? res['config_file'] : res;
|
||||||
this.topBarTitle = result['YoutubeDLMaterial']['Extra']['title_top'];
|
this.topBarTitle = result['YoutubeDLMaterial']['Extra']['title_top'];
|
||||||
const themingExists = result['YoutubeDLMaterial']['Themes'];
|
const themingExists = result['YoutubeDLMaterial']['Themes'];
|
||||||
this.defaultTheme = themingExists ? result['YoutubeDLMaterial']['Themes']['default_theme'] : 'default';
|
this.defaultTheme = themingExists ? result['YoutubeDLMaterial']['Themes']['default_theme'] : 'default';
|
||||||
@@ -76,6 +77,7 @@ export class AppComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
localStorage.setItem('theme', theme);
|
localStorage.setItem('theme', theme);
|
||||||
|
this.elementRef.nativeElement.ownerDocument.body.style.backgroundColor = this.THEMES_CONFIG[theme]['background_color'];
|
||||||
} else {
|
} else {
|
||||||
console.error('Invalid theme: ' + theme);
|
console.error('Invalid theme: ' + theme);
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -3,17 +3,16 @@ import { NgModule } from '@angular/core';
|
|||||||
import {MatNativeDateModule, MatRadioModule, MatInputModule, MatButtonModule, MatSidenavModule, MatIconModule, MatListModule,
|
import {MatNativeDateModule, MatRadioModule, MatInputModule, MatButtonModule, MatSidenavModule, MatIconModule, MatListModule,
|
||||||
MatSnackBarModule, MatCardModule, MatSelectModule, MatToolbarModule, MatCheckboxModule, MatGridListModule,
|
MatSnackBarModule, MatCardModule, MatSelectModule, MatToolbarModule, MatCheckboxModule, MatGridListModule,
|
||||||
MatProgressBarModule, MatExpansionModule,
|
MatProgressBarModule, MatExpansionModule,
|
||||||
MatGridList,
|
|
||||||
MatProgressSpinnerModule,
|
MatProgressSpinnerModule,
|
||||||
MatButtonToggleModule,
|
MatButtonToggleModule,
|
||||||
MatDialogModule} from '@angular/material';
|
MatDialogModule} from '@angular/material';
|
||||||
|
import {DragDropModule} from '@angular/cdk/drag-drop';
|
||||||
import {FormsModule, ReactiveFormsModule} from '@angular/forms';
|
import {FormsModule, ReactiveFormsModule} from '@angular/forms';
|
||||||
import { AppComponent } from './app.component';
|
import { AppComponent } from './app.component';
|
||||||
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
|
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
|
||||||
import { HttpModule } from '@angular/http';
|
import { HttpModule } from '@angular/http';
|
||||||
import { HttpClientModule, HttpClient } from '@angular/common/http';
|
import { HttpClientModule, HttpClient } from '@angular/common/http';
|
||||||
import { PostsService } from 'app/posts.services';
|
import { PostsService } from 'app/posts.services';
|
||||||
import {APP_BASE_HREF} from '@angular/common';
|
|
||||||
import { FileCardComponent } from './file-card/file-card.component';
|
import { FileCardComponent } from './file-card/file-card.component';
|
||||||
import {RouterModule} from '@angular/router';
|
import {RouterModule} from '@angular/router';
|
||||||
import { AppRoutingModule } from './app-routing.module';
|
import { AppRoutingModule } from './app-routing.module';
|
||||||
@@ -24,8 +23,15 @@ import {VgControlsModule} from 'videogular2/compiled/controls';
|
|||||||
import {VgOverlayPlayModule} from 'videogular2/compiled/overlay-play';
|
import {VgOverlayPlayModule} from 'videogular2/compiled/overlay-play';
|
||||||
import {VgBufferingModule} from 'videogular2/compiled/buffering';
|
import {VgBufferingModule} from 'videogular2/compiled/buffering';
|
||||||
import { InputDialogComponent } from './input-dialog/input-dialog.component';
|
import { InputDialogComponent } from './input-dialog/input-dialog.component';
|
||||||
import { LazyLoadImageModule } from 'ng-lazyload-image';
|
import { LazyLoadImageModule, IsVisibleProps } from 'ng-lazyload-image';
|
||||||
import { NgxContentLoadingModule } from 'ngx-content-loading';
|
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({
|
@NgModule({
|
||||||
declarations: [
|
declarations: [
|
||||||
@@ -33,7 +39,9 @@ import { NgxContentLoadingModule } from 'ngx-content-loading';
|
|||||||
FileCardComponent,
|
FileCardComponent,
|
||||||
MainComponent,
|
MainComponent,
|
||||||
PlayerComponent,
|
PlayerComponent,
|
||||||
InputDialogComponent
|
InputDialogComponent,
|
||||||
|
CreatePlaylistComponent,
|
||||||
|
DownloadItemComponent
|
||||||
],
|
],
|
||||||
imports: [
|
imports: [
|
||||||
BrowserModule,
|
BrowserModule,
|
||||||
@@ -60,17 +68,19 @@ import { NgxContentLoadingModule } from 'ngx-content-loading';
|
|||||||
MatProgressSpinnerModule,
|
MatProgressSpinnerModule,
|
||||||
MatButtonToggleModule,
|
MatButtonToggleModule,
|
||||||
MatDialogModule,
|
MatDialogModule,
|
||||||
|
DragDropModule,
|
||||||
VgCoreModule,
|
VgCoreModule,
|
||||||
VgControlsModule,
|
VgControlsModule,
|
||||||
VgOverlayPlayModule,
|
VgOverlayPlayModule,
|
||||||
VgBufferingModule,
|
VgBufferingModule,
|
||||||
LazyLoadImageModule,
|
LazyLoadImageModule.forRoot({ isVisible }),
|
||||||
NgxContentLoadingModule,
|
NgxContentLoadingModule,
|
||||||
RouterModule,
|
RouterModule,
|
||||||
AppRoutingModule,
|
AppRoutingModule,
|
||||||
],
|
],
|
||||||
entryComponents: [
|
entryComponents: [
|
||||||
InputDialogComponent
|
InputDialogComponent,
|
||||||
|
CreatePlaylistComponent
|
||||||
],
|
],
|
||||||
providers: [PostsService],
|
providers: [PostsService],
|
||||||
bootstrap: [AppComponent]
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,21 +1,17 @@
|
|||||||
<mat-card class="example-card mat-elevation-z6">
|
<mat-card class="example-card mat-elevation-z6">
|
||||||
<button (click)="deleteFile()" class="deleteButton" mat-icon-button><mat-icon>delete_forever</mat-icon></button>
|
|
||||||
<div style="padding:5px">
|
<div style="padding:5px">
|
||||||
<b><a href="javascript:void(0)" (click)="!isPlaylist ? mainComponent.goToFile(name, isAudio) : mainComponent.goToPlaylist(name, type)">{{title}}</a></b>
|
<b><a href="javascript:void(0)" (click)="!isPlaylist ? mainComponent.goToFile(name, isAudio) : mainComponent.goToPlaylist(name, type)">{{title}}</a></b>
|
||||||
<br/>
|
<br/>
|
||||||
<span class="max-two-lines">ID: {{name}}</span>
|
<span class="max-two-lines">ID: {{name}}</span>
|
||||||
<div *ngIf="isPlaylist">Count: {{count}}</div>
|
<div *ngIf="isPlaylist">Count: {{count}}</div>
|
||||||
<div class="img-div">
|
<div *ngIf="!image_errored && thumbnailURL" class="img-div">
|
||||||
<img class="image" [lazyLoad]="thumbnailURL" (onLoad)="imageLoaded($event)" alt="Thumbnail">
|
<img class="image" (error) ="onImgError($event)" [id]="type" [lazyLoad]="thumbnailURL" [customObservable]="scrollAndLoad" (onLoad)="imageLoaded($event)" alt="Thumbnail">
|
||||||
<span *ngIf="!image_loaded">
|
<span *ngIf="!image_loaded">
|
||||||
<ngx-content-loading [width]="500" [height]="360">
|
<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>
|
<svg:g ngx-rect width="500" height="360" y="0" x="0" rx="4" ry="4"></svg:g>
|
||||||
</ngx-content-loading>
|
</ngx-content-loading>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<button (click)="deleteFile()" class="deleteButton" mat-icon-button><mat-icon>delete_forever</mat-icon></button>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</mat-card>
|
</mat-card>
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import {PostsService} from '../posts.services';
|
|||||||
import {MatSnackBar} from '@angular/material';
|
import {MatSnackBar} from '@angular/material';
|
||||||
import {EventEmitter} from '@angular/core';
|
import {EventEmitter} from '@angular/core';
|
||||||
import { MainComponent } from 'app/main/main.component';
|
import { MainComponent } from 'app/main/main.component';
|
||||||
|
import { Subject, Observable } from 'rxjs';
|
||||||
|
import 'rxjs/add/observable/merge';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-file-card',
|
selector: 'app-file-card',
|
||||||
@@ -21,8 +23,18 @@ export class FileCardComponent implements OnInit {
|
|||||||
@Input() count = null;
|
@Input() count = null;
|
||||||
type;
|
type;
|
||||||
image_loaded = false;
|
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() {
|
ngOnInit() {
|
||||||
this.type = this.isAudio ? 'audio' : 'video';
|
this.type = this.isAudio ? 'audio' : 'video';
|
||||||
@@ -44,6 +56,14 @@ export class FileCardComponent implements OnInit {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onImgError(event) {
|
||||||
|
this.image_errored = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
onHoverResponse() {
|
||||||
|
this.scrollSubject.next();
|
||||||
|
}
|
||||||
|
|
||||||
imageLoaded(loaded) {
|
imageLoaded(loaded) {
|
||||||
this.image_loaded = true;
|
this.image_loaded = true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -111,4 +111,12 @@ mat-form-field.mat-form-field {
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 0px;
|
bottom: 0px;
|
||||||
width: 150px;
|
width: 150px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-playlist-button {
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.advanced-input {
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<div [ngClass]="allowQualitySelect ? 'col-sm-9' : null" class="col-12">
|
<div [ngClass]="allowQualitySelect ? 'col-sm-9' : null" class="col-12">
|
||||||
<mat-form-field color="accent" class="example-full-width">
|
<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>
|
<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>
|
<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>
|
<button class="input-clear-button" mat-icon-button (click)="clearInput()"><mat-icon>clear</mat-icon></button>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
@@ -21,12 +21,12 @@
|
|||||||
<mat-label>Quality</mat-label>
|
<mat-label>Quality</mat-label>
|
||||||
<mat-select [ngModelOptions]="{standalone: true}" [(ngModel)]="selectedQuality">
|
<mat-select [ngModelOptions]="{standalone: true}" [(ngModel)]="selectedQuality">
|
||||||
<ng-container *ngFor="let option of qualityOptions[(audioOnly) ? 'audio' : 'video']">
|
<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}}
|
{{option.label}}
|
||||||
</mat-option>
|
</mat-option>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</mat-select>
|
</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>
|
<mat-spinner [diameter]="25"></mat-spinner>
|
||||||
</div>
|
</div>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
@@ -49,18 +49,72 @@
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
<br/>
|
<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>
|
</div>
|
||||||
</mat-card-content>
|
</mat-card-content>
|
||||||
<mat-card-actions>
|
<mat-card-actions>
|
||||||
<button style="margin-left: 8px; margin-bottom: 8px" (click)="downloadClicked()" [disabled]="downloadingfile" type="submit" mat-stroked-button
|
<button style="margin-left: 8px; margin-bottom: 8px" (click)="downloadClicked()" [disabled]="downloadingfile" type="submit" mat-stroked-button
|
||||||
color="accent">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-actions>
|
||||||
</mat-card>
|
</mat-card>
|
||||||
</div>
|
</div>
|
||||||
|
<div *ngIf="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-12 col-sm-6">
|
||||||
|
<mat-checkbox color="accent" [disabled]="current_download" (change)="customArgsEnabledChanged($event)" [(ngModel)]="customArgsEnabled" style="z-index: 999" [ngModelOptions]="{standalone: true}">Use custom args</mat-checkbox>
|
||||||
|
<mat-form-field color="accent" style="margin-bottom: 42px;" 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-12 col-sm-6">
|
||||||
|
<mat-checkbox color="accent" [disabled]="current_download" (change)="customOutputEnabledChanged($event)" [(ngModel)]="customOutputEnabled" style="z-index: 999" [ngModelOptions]="{standalone: true}">Use custom output</mat-checkbox>
|
||||||
|
<mat-form-field style="margin-bottom: 42px;" 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">Documentation</a>. Path is relative to the config download path. Don't include extension.</mat-hint>
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
<div *ngIf="!youtubeAuthDisabledOverride" class="col-12 col-sm-6 mt-2">
|
||||||
|
<mat-checkbox color="accent" [disabled]="current_download" (change)="youtubeAuthEnabledChanged($event)" [(ngModel)]="youtubeAuthEnabled" style="z-index: 999" [ngModelOptions]="{standalone: true}">Use authentication</mat-checkbox>
|
||||||
|
<mat-form-field color="accent" class="advanced-input">
|
||||||
|
<input [(ngModel)]="youtubeUsername" [ngModelOptions]="{standalone: true}" [disabled]="!youtubeAuthEnabled" matInput placeholder="Username">
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
<div *ngIf="!youtubeAuthDisabledOverride" class="col-12 col-sm-6 mt-2">
|
||||||
|
<mat-form-field style="margin-top: 31px;" color="accent" class="advanced-input">
|
||||||
|
<input [(ngModel)]="youtubePassword" type="password" [ngModelOptions]="{standalone: true}" [disabled]="!youtubeAuthEnabled" matInput placeholder="Password">
|
||||||
|
</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/>
|
<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 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">
|
<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>
|
<mat-progress-bar mode="determinate" value="{{percentDownloaded}}"></mat-progress-bar>
|
||||||
@@ -80,7 +134,7 @@
|
|||||||
</ng-template>
|
</ng-template>
|
||||||
<div style="margin: 20px" *ngIf="fileManagerEnabled">
|
<div style="margin: 20px" *ngIf="fileManagerEnabled">
|
||||||
<mat-accordion>
|
<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-expansion-panel-header>
|
||||||
<mat-panel-title>
|
<mat-panel-title>
|
||||||
Audio
|
Audio
|
||||||
@@ -92,26 +146,30 @@
|
|||||||
<div *ngIf="mp3s.length > 0;else nomp3s">
|
<div *ngIf="mp3s.length > 0;else nomp3s">
|
||||||
<mat-grid-list style="margin-bottom: 15px;" (window:resize)="onResize($event)" [cols]="files_cols" rowHeight="150px">
|
<mat-grid-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;">
|
<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>
|
[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-progress-bar *ngIf="downloading_content['audio'][file.id]" class="download-progress-bar" mode="indeterminate"></mat-progress-bar>
|
||||||
</mat-grid-tile>
|
</mat-grid-tile>
|
||||||
</mat-grid-list>
|
</mat-grid-list>
|
||||||
<mat-divider *ngIf="playlists.audio.length > 0"></mat-divider>
|
<mat-divider></mat-divider>
|
||||||
<div style="width: 100%; text-align: center; margin-top: 10px;" *ngIf="playlists.audio.length > 0">
|
<div style="width: 100%; text-align: center; margin-top: 10px;">
|
||||||
<h6>Playlists</h6>
|
<h6>Playlists</h6>
|
||||||
</div>
|
</div>
|
||||||
<mat-grid-list *ngIf="playlists.audio.length > 0" (window:resize)="onResize($event)" [cols]="files_cols" rowHeight="150px">
|
<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;">
|
<mat-grid-tile *ngFor="let playlist of playlists.audio; let i = index;">
|
||||||
<app-file-card (removeFile)="removePlaylistMp3(playlist.id, i)" [title]="playlist.name" [name]="playlist.id" [thumbnailURL]="playlist_thumbnails[playlist.id]"
|
<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>
|
[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-progress-bar *ngIf="downloading_content['audio'][playlist.id]" class="download-progress-bar" mode="indeterminate"></mat-progress-bar>
|
||||||
</mat-grid-tile>
|
</mat-grid-tile>
|
||||||
</mat-grid-list>
|
</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>
|
</div>
|
||||||
|
|
||||||
</mat-expansion-panel>
|
</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-expansion-panel-header>
|
||||||
<mat-panel-title>
|
<mat-panel-title>
|
||||||
Video
|
Video
|
||||||
@@ -123,23 +181,29 @@
|
|||||||
<div *ngIf="mp4s.length > 0;else nomp4s">
|
<div *ngIf="mp4s.length > 0;else nomp4s">
|
||||||
<mat-grid-list style="margin-bottom: 15px;" (window:resize)="onResize($event)" [cols]="files_cols" rowHeight="150px">
|
<mat-grid-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;">
|
<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>
|
[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-progress-bar *ngIf="downloading_content['video'][file.id]" class="download-progress-bar" mode="indeterminate"></mat-progress-bar>
|
||||||
</mat-grid-tile>
|
</mat-grid-tile>
|
||||||
</mat-grid-list>
|
</mat-grid-list>
|
||||||
<mat-divider *ngIf="playlists.video.length > 0"></mat-divider>
|
<mat-divider></mat-divider>
|
||||||
|
|
||||||
<div style="width: 100%; text-align: center; margin-top: 10px;" *ngIf="playlists.video.length > 0">
|
<div style="width: 100%; text-align: center; margin-top: 10px;">
|
||||||
<h6>Playlists</h6>
|
<h6>Playlists</h6>
|
||||||
</div>
|
</div>
|
||||||
<mat-grid-list *ngIf="playlists.video.length > 0" (window:resize)="onResize($event)" [cols]="files_cols" rowHeight="150px">
|
<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;">
|
<mat-grid-tile *ngFor="let playlist of playlists.video; let i = index;">
|
||||||
<app-file-card (removeFile)="removePlaylistMp4(playlist.id, i)" [title]="playlist.name" [name]="playlist.id" [thumbnailURL]="playlist_thumbnails[playlist.id]"
|
<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>
|
[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-progress-bar *ngIf="downloading_content['video'][playlist.id]" class="download-progress-bar" mode="indeterminate"></mat-progress-bar>
|
||||||
</mat-grid-tile>
|
</mat-grid-tile>
|
||||||
</mat-grid-list>
|
</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>
|
</div>
|
||||||
</mat-expansion-panel>
|
</mat-expansion-panel>
|
||||||
</mat-accordion>
|
</mat-accordion>
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { Component, OnInit, ElementRef, ViewChild } from '@angular/core';
|
import { Component, OnInit, ElementRef, ViewChild, ViewChildren, QueryList, isDevMode } from '@angular/core';
|
||||||
import {PostsService} from '../posts.services';
|
import {PostsService} from '../posts.services';
|
||||||
import {FileCardComponent} from '../file-card/file-card.component';
|
import {FileCardComponent} from '../file-card/file-card.component';
|
||||||
import { Observable } from 'rxjs/Observable';
|
import { Observable } from 'rxjs/Observable';
|
||||||
import {FormControl, Validators} from '@angular/forms';
|
import {FormControl, Validators} from '@angular/forms';
|
||||||
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
|
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
|
||||||
import {MatSnackBar} from '@angular/material';
|
import {MatSnackBar, MatDialog} from '@angular/material';
|
||||||
import { saveAs } from 'file-saver';
|
import { saveAs } from 'file-saver';
|
||||||
import 'rxjs/add/observable/of';
|
import 'rxjs/add/observable/of';
|
||||||
import 'rxjs/add/operator/mapTo';
|
import 'rxjs/add/operator/mapTo';
|
||||||
@@ -15,7 +15,25 @@ import 'rxjs/add/operator/debounceTime'
|
|||||||
import 'rxjs/add/operator/do'
|
import 'rxjs/add/operator/do'
|
||||||
import 'rxjs/add/operator/switch'
|
import 'rxjs/add/operator/switch'
|
||||||
import { YoutubeSearchService, Result } from '../youtube-search.service';
|
import { YoutubeSearchService, Result } from '../youtube-search.service';
|
||||||
import { Router } from '@angular/router';
|
import { Router, ActivatedRoute } 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({
|
@Component({
|
||||||
selector: 'app-root',
|
selector: 'app-root',
|
||||||
@@ -23,24 +41,36 @@ import { Router } from '@angular/router';
|
|||||||
styleUrls: ['./main.component.css']
|
styleUrls: ['./main.component.css']
|
||||||
})
|
})
|
||||||
export class MainComponent implements OnInit {
|
export class MainComponent implements OnInit {
|
||||||
|
youtubeAuthDisabledOverride = true;
|
||||||
|
|
||||||
iOS = false;
|
iOS = false;
|
||||||
|
|
||||||
determinateProgress = false;
|
determinateProgress = false;
|
||||||
downloadingfile = false;
|
downloadingfile = false;
|
||||||
audioOnly: boolean;
|
audioOnly: boolean;
|
||||||
|
multiDownloadMode = false;
|
||||||
|
customArgsEnabled = false;
|
||||||
|
customArgs = null;
|
||||||
|
customOutputEnabled = false;
|
||||||
|
customOutput = null;
|
||||||
|
youtubeAuthEnabled = false;
|
||||||
|
youtubeUsername = null;
|
||||||
|
youtubePassword = null;
|
||||||
urlError = false;
|
urlError = false;
|
||||||
path = '';
|
path = '';
|
||||||
url = '';
|
url = '';
|
||||||
exists = '';
|
exists = '';
|
||||||
percentDownloaded: number;
|
percentDownloaded: number;
|
||||||
|
autoStartDownload = false;
|
||||||
|
|
||||||
// settings
|
// settings
|
||||||
fileManagerEnabled = false;
|
fileManagerEnabled = false;
|
||||||
allowQualitySelect = false;
|
allowQualitySelect = false;
|
||||||
downloadOnlyMode = false;
|
downloadOnlyMode = false;
|
||||||
baseStreamPath;
|
allowMultiDownloadMode = false;
|
||||||
audioFolderPath;
|
audioFolderPath;
|
||||||
videoFolderPath;
|
videoFolderPath;
|
||||||
|
allowAdvancedDownload = false;
|
||||||
|
|
||||||
cachedAvailableFormats = {};
|
cachedAvailableFormats = {};
|
||||||
|
|
||||||
@@ -53,10 +83,12 @@ export class MainComponent implements OnInit {
|
|||||||
|
|
||||||
mp3s: any[] = [];
|
mp3s: any[] = [];
|
||||||
mp4s: any[] = [];
|
mp4s: any[] = [];
|
||||||
files_cols = (window.innerWidth <= 450) ? 2 : 4;
|
files_cols = null;
|
||||||
playlists = {'audio': [], 'video': []};
|
playlists = {'audio': [], 'video': []};
|
||||||
playlist_thumbnails = {};
|
playlist_thumbnails = {};
|
||||||
downloading_content = {'audio': {}, 'video': {}};
|
downloading_content = {'audio': {}, 'video': {}};
|
||||||
|
downloads: Download[] = [];
|
||||||
|
current_download: Download = null;
|
||||||
|
|
||||||
urlForm = new FormControl('', [Validators.required]);
|
urlForm = new FormControl('', [Validators.required]);
|
||||||
|
|
||||||
@@ -156,30 +188,38 @@ export class MainComponent implements OnInit {
|
|||||||
formats_loading = false;
|
formats_loading = false;
|
||||||
|
|
||||||
@ViewChild('urlinput', { read: ElementRef, static: false }) urlInput: ElementRef;
|
@ViewChild('urlinput', { read: ElementRef, static: false }) urlInput: ElementRef;
|
||||||
|
@ViewChildren('audiofilecard') audioFileCards: QueryList<FileCardComponent>;
|
||||||
|
@ViewChildren('videofilecard') videoFileCards: QueryList<FileCardComponent>;
|
||||||
last_valid_url = '';
|
last_valid_url = '';
|
||||||
last_url_check = 0;
|
last_url_check = 0;
|
||||||
|
|
||||||
|
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,
|
constructor(private postsService: PostsService, private youtubeSearch: YoutubeSearchService, public snackBar: MatSnackBar,
|
||||||
private router: Router) {
|
private router: Router, public dialog: MatDialog, private platform: Platform, private route: ActivatedRoute) {
|
||||||
this.audioOnly = false;
|
this.audioOnly = false;
|
||||||
|
|
||||||
|
|
||||||
// loading config
|
// loading config
|
||||||
this.postsService.loadNavItems().subscribe(result => { // loads settings
|
this.postsService.loadNavItems().subscribe(res => { // loads settings
|
||||||
const backendUrl = result['YoutubeDLMaterial']['Host']['backendurl'];
|
const result = !this.postsService.debugMode ? res['config_file'] : res;
|
||||||
this.fileManagerEnabled = result['YoutubeDLMaterial']['Extra']['file_manager_enabled'];
|
this.fileManagerEnabled = result['YoutubeDLMaterial']['Extra']['file_manager_enabled'];
|
||||||
this.downloadOnlyMode = result['YoutubeDLMaterial']['Extra']['download_only_mode'];
|
this.downloadOnlyMode = result['YoutubeDLMaterial']['Extra']['download_only_mode'];
|
||||||
this.baseStreamPath = result['YoutubeDLMaterial']['Downloader']['path-base'];
|
this.allowMultiDownloadMode = result['YoutubeDLMaterial']['Extra']['allow_multi_download_mode'];
|
||||||
this.audioFolderPath = result['YoutubeDLMaterial']['Downloader']['path-audio'];
|
this.audioFolderPath = result['YoutubeDLMaterial']['Downloader']['path-audio'];
|
||||||
this.videoFolderPath = result['YoutubeDLMaterial']['Downloader']['path-video'];
|
this.videoFolderPath = result['YoutubeDLMaterial']['Downloader']['path-video'];
|
||||||
this.youtubeSearchEnabled = result['YoutubeDLMaterial']['API'] && result['YoutubeDLMaterial']['API']['use_youtube_API'] &&
|
this.youtubeSearchEnabled = result['YoutubeDLMaterial']['API'] && result['YoutubeDLMaterial']['API']['use_youtube_API'] &&
|
||||||
result['YoutubeDLMaterial']['API']['youtube_API_key'];
|
result['YoutubeDLMaterial']['API']['youtube_API_key'];
|
||||||
this.youtubeAPIKey = this.youtubeSearchEnabled ? result['YoutubeDLMaterial']['API']['youtube_API_key'] : null;
|
this.youtubeAPIKey = this.youtubeSearchEnabled ? result['YoutubeDLMaterial']['API']['youtube_API_key'] : null;
|
||||||
this.allowQualitySelect = result['YoutubeDLMaterial']['Extra']['allow_quality_select'];
|
this.allowQualitySelect = result['YoutubeDLMaterial']['Extra']['allow_quality_select'];
|
||||||
|
this.allowAdvancedDownload = result['YoutubeDLMaterial']['Advanced']['allow_advanced_download'];
|
||||||
|
|
||||||
this.postsService.path = backendUrl;
|
|
||||||
this.postsService.startPath = backendUrl;
|
|
||||||
this.postsService.startPathSSL = backendUrl;
|
|
||||||
|
|
||||||
if (this.fileManagerEnabled) {
|
if (this.fileManagerEnabled) {
|
||||||
this.getMp3s();
|
this.getMp3s();
|
||||||
@@ -190,19 +230,74 @@ export class MainComponent implements OnInit {
|
|||||||
this.youtubeSearch.initializeAPI(this.youtubeAPIKey);
|
this.youtubeSearch.initializeAPI(this.youtubeAPIKey);
|
||||||
this.attachToInput();
|
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';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (localStorage.getItem('youtubeAuthEnabled') !== null) {
|
||||||
|
this.youtubeAuthEnabled = localStorage.getItem('youtubeAuthEnabled') === 'true';
|
||||||
|
}
|
||||||
|
|
||||||
|
// set advanced inputs
|
||||||
|
const customArgs = localStorage.getItem('customArgs');
|
||||||
|
const customOutput = localStorage.getItem('customOutput');
|
||||||
|
const youtubeUsername = localStorage.getItem('youtubeUsername');
|
||||||
|
|
||||||
|
if (customArgs && customArgs !== 'null') { this.customArgs = customArgs };
|
||||||
|
if (customOutput && customOutput !== 'null') { this.customOutput = customOutput };
|
||||||
|
if (youtubeUsername && youtubeUsername !== 'null') { this.youtubeUsername = youtubeUsername };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.autoStartDownload) {
|
||||||
|
this.downloadClicked();
|
||||||
|
}
|
||||||
|
|
||||||
}, error => {
|
}, error => {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// app initialization.
|
||||||
|
ngOnInit() {
|
||||||
|
this.iOS = this.platform.IOS;
|
||||||
|
|
||||||
|
// get checkboxes
|
||||||
|
if (localStorage.getItem('audioOnly') !== null) {
|
||||||
|
this.audioOnly = localStorage.getItem('audioOnly') === 'true';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (localStorage.getItem('multiDownloadMode') !== null) {
|
||||||
|
this.multiDownloadMode = localStorage.getItem('multiDownloadMode') === 'true';
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if params exist
|
||||||
|
if (this.route.snapshot.paramMap.get('url')) {
|
||||||
|
this.url = decodeURIComponent(this.route.snapshot.paramMap.get('url'));
|
||||||
|
this.audioOnly = this.route.snapshot.paramMap.get('audioOnly') === 'true';
|
||||||
|
|
||||||
|
// set auto start flag to true
|
||||||
|
this.autoStartDownload = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setCols();
|
||||||
|
}
|
||||||
|
|
||||||
// file manager stuff
|
// file manager stuff
|
||||||
|
|
||||||
getMp3s() {
|
getMp3s() {
|
||||||
this.postsService.getMp3s().subscribe(result => {
|
this.postsService.getMp3s().subscribe(result => {
|
||||||
const mp3s = result['mp3s'];
|
const mp3s = result['mp3s'];
|
||||||
const playlists = result['playlists'];
|
const playlists = result['playlists'];
|
||||||
this.mp3s = mp3s;
|
// if they are different
|
||||||
|
if (JSON.stringify(this.mp3s) !== JSON.stringify(mp3s)) { this.mp3s = mp3s };
|
||||||
this.playlists.audio = playlists;
|
this.playlists.audio = playlists;
|
||||||
|
|
||||||
// get thumbnail url by using first video. this is a temporary hack
|
// get thumbnail url by using first video. this is a temporary hack
|
||||||
@@ -216,7 +311,7 @@ export class MainComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.playlist_thumbnails[playlist.id] = videoToExtractThumbnail.thumbnailURL;
|
if (videoToExtractThumbnail) { this.playlist_thumbnails[playlist.id] = videoToExtractThumbnail.thumbnailURL; }
|
||||||
}
|
}
|
||||||
}, error => {
|
}, error => {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
@@ -227,7 +322,8 @@ export class MainComponent implements OnInit {
|
|||||||
this.postsService.getMp4s().subscribe(result => {
|
this.postsService.getMp4s().subscribe(result => {
|
||||||
const mp4s = result['mp4s'];
|
const mp4s = result['mp4s'];
|
||||||
const playlists = result['playlists'];
|
const playlists = result['playlists'];
|
||||||
this.mp4s = mp4s;
|
// if they are different
|
||||||
|
if (JSON.stringify(this.mp4s) !== JSON.stringify(mp4s)) { this.mp4s = mp4s };
|
||||||
this.playlists.video = playlists;
|
this.playlists.video = playlists;
|
||||||
|
|
||||||
// get thumbnail url by using first video. this is a temporary hack
|
// get thumbnail url by using first video. this is a temporary hack
|
||||||
@@ -241,7 +337,7 @@ export class MainComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.playlist_thumbnails[playlist.id] = videoToExtractThumbnail.thumbnailURL;
|
if (videoToExtractThumbnail) { this.playlist_thumbnails[playlist.id] = videoToExtractThumbnail.thumbnailURL; }
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
error => {
|
error => {
|
||||||
@@ -249,6 +345,18 @@ export class MainComponent implements OnInit {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public setCols() {
|
||||||
|
if (window.innerWidth <= 350) {
|
||||||
|
this.files_cols = 1;
|
||||||
|
} else if (window.innerWidth <= 500) {
|
||||||
|
this.files_cols = 2;
|
||||||
|
} else if (window.innerWidth <= 750) {
|
||||||
|
this.files_cols = 3
|
||||||
|
} else {
|
||||||
|
this.files_cols = 4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public goToFile(name, isAudio) {
|
public goToFile(name, isAudio) {
|
||||||
if (isAudio) {
|
if (isAudio) {
|
||||||
this.downloadHelperMp3(name, false, false);
|
this.downloadHelperMp3(name, false, false);
|
||||||
@@ -319,69 +427,79 @@ export class MainComponent implements OnInit {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// app initialization.
|
|
||||||
ngOnInit() {
|
|
||||||
this.iOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window['MSStream'];
|
|
||||||
|
|
||||||
if (localStorage.getItem('audioOnly') !== null) {
|
|
||||||
this.audioOnly = localStorage.getItem('audioOnly') === 'true';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// download helpers
|
// download helpers
|
||||||
|
|
||||||
downloadHelperMp3(name, is_playlist = false, forceView = false) {
|
downloadHelperMp3(name, is_playlist = false, forceView = false, new_download = null) {
|
||||||
this.downloadingfile = false;
|
this.downloadingfile = false;
|
||||||
|
|
||||||
// if download only mode, just download the file. no redirect
|
if (new_download && this.current_download !== new_download) {
|
||||||
if (forceView === false && this.downloadOnlyMode && !this.iOS) {
|
// console.log('mismatched downloads');
|
||||||
if (is_playlist) {
|
} else if (!this.multiDownloadMode || !new_download) {
|
||||||
const zipName = name[0].split(' ')[0] + name[1].split(' ')[0];
|
// if download only mode, just download the file. no redirect
|
||||||
this.downloadPlaylist(name, 'audio', zipName);
|
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 {
|
} else {
|
||||||
this.downloadAudioFile(decodeURI(name));
|
if (is_playlist) {
|
||||||
}
|
this.router.navigate(['/player', {fileNames: name.join('|nvr|'), type: 'audio'}]);
|
||||||
} else {
|
} else {
|
||||||
if (is_playlist) {
|
this.router.navigate(['/player', {fileNames: name, type: 'audio'}]);
|
||||||
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
|
// reloads mp3s
|
||||||
if (this.fileManagerEnabled) {
|
if (this.fileManagerEnabled) {
|
||||||
this.getMp3s();
|
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;
|
this.downloadingfile = false;
|
||||||
|
|
||||||
// if download only mode, just download the file. no redirect
|
if (new_download && this.current_download !== new_download) {
|
||||||
if (forceView === false && this.downloadOnlyMode) {
|
// console.log('mismatched downloads');
|
||||||
if (is_playlist) {
|
} else if (!this.multiDownloadMode || !new_download) {
|
||||||
const zipName = name[0].split(' ')[0] + name[1].split(' ')[0];
|
// if download only mode, just download the file. no redirect
|
||||||
this.downloadPlaylist(name, 'video', zipName);
|
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 {
|
} else {
|
||||||
this.downloadVideoFile(decodeURI(name));
|
if (is_playlist) {
|
||||||
}
|
this.router.navigate(['/player', {fileNames: name.join('|nvr|'), type: 'video'}]);
|
||||||
} else {
|
} else {
|
||||||
if (is_playlist) {
|
this.router.navigate(['/player', {fileNames: name, type: 'video'}]);
|
||||||
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
|
// reloads mp4s
|
||||||
if (this.fileManagerEnabled) {
|
if (this.fileManagerEnabled) {
|
||||||
this.getMp4s();
|
this.getMp4s();
|
||||||
|
setTimeout(() => {
|
||||||
|
this.videoFileCards.forEach(filecard => {
|
||||||
|
filecard.onHoverResponse();
|
||||||
|
});
|
||||||
|
}, 200);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -391,56 +509,145 @@ export class MainComponent implements OnInit {
|
|||||||
this.urlError = false;
|
this.urlError = false;
|
||||||
this.path = '';
|
this.path = '';
|
||||||
|
|
||||||
|
// get common args
|
||||||
|
const customArgs = (this.customArgsEnabled ? this.customArgs : null);
|
||||||
|
const customOutput = (this.customOutputEnabled ? this.customOutput : null);
|
||||||
|
const youtubeUsername = (this.youtubeAuthEnabled && this.youtubeUsername ? this.youtubeUsername : null);
|
||||||
|
const youtubePassword = (this.youtubeAuthEnabled && this.youtubePassword ? this.youtubePassword : null);
|
||||||
|
|
||||||
|
// set advanced inputs
|
||||||
|
if (this.allowAdvancedDownload) {
|
||||||
|
if (customArgs) {
|
||||||
|
localStorage.setItem('customArgs', customArgs);
|
||||||
|
}
|
||||||
|
if (customOutput) {
|
||||||
|
localStorage.setItem('customOutput', customOutput);
|
||||||
|
}
|
||||||
|
if (youtubeUsername) {
|
||||||
|
localStorage.setItem('youtubeUsername', youtubeUsername);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (this.audioOnly) {
|
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;
|
this.downloadingfile = true;
|
||||||
|
|
||||||
let customQualityConfiguration = null;
|
let customQualityConfiguration = null;
|
||||||
if (this.selectedQuality !== '') {
|
if (this.selectedQuality !== '') {
|
||||||
const cachedFormatsExists = this.cachedAvailableFormats[this.url];
|
const cachedFormatsExists = this.cachedAvailableFormats[this.url] && this.cachedAvailableFormats[this.url]['formats'];
|
||||||
if (cachedFormatsExists) {
|
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'];
|
customQualityConfiguration = audio_formats[this.selectedQuality]['format_id'];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.postsService.makeMP3(this.url, (this.selectedQuality === '' ? null : this.selectedQuality),
|
this.postsService.makeMP3(this.url, (this.selectedQuality === '' ? null : this.selectedQuality),
|
||||||
customQualityConfiguration).subscribe(posts => {
|
customQualityConfiguration, customArgs, customOutput, youtubeUsername, youtubePassword).subscribe(posts => {
|
||||||
|
// update download object
|
||||||
|
new_download.downloading = false;
|
||||||
|
new_download.percent_complete = 100;
|
||||||
|
|
||||||
const is_playlist = !!(posts['file_names']);
|
const is_playlist = !!(posts['file_names']);
|
||||||
this.path = is_playlist ? posts['file_names'] : posts['audiopathEncoded'];
|
this.path = is_playlist ? posts['file_names'] : posts['audiopathEncoded'];
|
||||||
|
|
||||||
if (this.path !== '-1') {
|
if (this.path !== '-1') {
|
||||||
this.downloadHelperMp3(this.path, is_playlist);
|
this.downloadHelperMp3(this.path, is_playlist, false, new_download);
|
||||||
}
|
}
|
||||||
}, error => { // can't access server
|
}, error => { // can't access server
|
||||||
this.downloadingfile = false;
|
this.downloadingfile = false;
|
||||||
this.openSnackBar('Download failed!', 'OK.');
|
this.openSnackBar('Download failed!', 'OK.');
|
||||||
});
|
});
|
||||||
} else {
|
} 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;
|
let customQualityConfiguration = null;
|
||||||
const cachedFormatsExists = this.cachedAvailableFormats[this.url];
|
const cachedFormatsExists = this.cachedAvailableFormats[this.url] && this.cachedAvailableFormats[this.url]['formats'];
|
||||||
if (cachedFormatsExists) {
|
if (cachedFormatsExists) {
|
||||||
const video_formats = this.cachedAvailableFormats[this.url]['video'];
|
const video_formats = this.cachedAvailableFormats[this.url]['formats']['video'];
|
||||||
if (video_formats['best_audio_format'] && this.selectedQuality !== '') {
|
if (video_formats['best_audio_format'] && this.selectedQuality !== '') {
|
||||||
customQualityConfiguration = video_formats[this.selectedQuality]['format_id'] + '+' + video_formats['best_audio_format'];
|
customQualityConfiguration = video_formats[this.selectedQuality]['format_id'] + '+' + video_formats['best_audio_format'];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.downloadingfile = true;
|
|
||||||
this.postsService.makeMP4(this.url, (this.selectedQuality === '' ? null : this.selectedQuality),
|
this.postsService.makeMP4(this.url, (this.selectedQuality === '' ? null : this.selectedQuality),
|
||||||
customQualityConfiguration).subscribe(posts => {
|
customQualityConfiguration, customArgs, customOutput, youtubeUsername, youtubePassword).subscribe(posts => {
|
||||||
|
// update download object
|
||||||
|
new_download.downloading = false;
|
||||||
|
new_download.percent_complete = 100;
|
||||||
|
|
||||||
const is_playlist = !!(posts['file_names']);
|
const is_playlist = !!(posts['file_names']);
|
||||||
this.path = is_playlist ? posts['file_names'] : posts['videopathEncoded'];
|
this.path = is_playlist ? posts['file_names'] : posts['videopathEncoded'];
|
||||||
|
|
||||||
if (this.path !== '-1') {
|
if (this.path !== '-1') {
|
||||||
this.downloadHelperMp4(this.path, is_playlist);
|
this.downloadHelperMp4(this.path, is_playlist, false, new_download);
|
||||||
}
|
}
|
||||||
}, error => { // can't access server
|
}, error => { // can't access server
|
||||||
this.downloadingfile = false;
|
this.downloadingfile = false;
|
||||||
this.openSnackBar('Download failed!', 'OK.');
|
this.openSnackBar('Download failed!', 'OK.');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.multiDownloadMode) {
|
||||||
|
this.url = '';
|
||||||
|
this.downloadingfile = false;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
this.urlError = true;
|
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) {
|
downloadAudioFile(name) {
|
||||||
this.downloading_content['audio'][name] = true;
|
this.downloading_content['audio'][name] = true;
|
||||||
this.postsService.downloadFileFromServer(name, 'audio').subscribe(res => {
|
this.postsService.downloadFileFromServer(name, 'audio').subscribe(res => {
|
||||||
@@ -524,7 +731,7 @@ export class MainComponent implements OnInit {
|
|||||||
// tslint:disable-next-line: max-line-length
|
// tslint:disable-next-line: max-line-length
|
||||||
const youtubeStrRegex = /(?:http(?:s)?:\/\/)?(?:www\.)?(?:youtu\.be\/|youtube\.com\/(?:(?:watch)?\?(?:.*&)?v(?:i)?=|(?:embed|v|vi|user)\/))([^\?&\"'<> #]+)/;
|
const youtubeStrRegex = /(?:http(?:s)?:\/\/)?(?:www\.)?(?:youtu\.be\/|youtube\.com\/(?:(?:watch)?\?(?:.*&)?v(?:i)?=|(?:embed|v|vi|user)\/))([^\?&\"'<> #]+)/;
|
||||||
const reYT = new RegExp(youtubeStrRegex);
|
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 (valid && ytValid && Date.now() - this.last_url_check > 1000) {
|
||||||
if (str !== this.last_valid_url && this.allowQualitySelect) {
|
if (str !== this.last_valid_url && this.allowQualitySelect) {
|
||||||
// get info
|
// get info
|
||||||
@@ -543,18 +750,32 @@ export class MainComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getURLInfo(url) {
|
getURLInfo(url) {
|
||||||
if (!(this.cachedAvailableFormats[url])) {
|
if (!this.cachedAvailableFormats[url]) {
|
||||||
this.formats_loading = true;
|
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 => {
|
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'];
|
const infos = res['result'];
|
||||||
|
if (!infos || !infos.formats) {
|
||||||
|
this.errorFormats(url);
|
||||||
|
return;
|
||||||
|
}
|
||||||
const parsed_infos = this.getAudioAndVideoFormats(infos.formats);
|
const parsed_infos = this.getAudioAndVideoFormats(infos.formats);
|
||||||
const available_formats = {audio: parsed_infos[0], video: parsed_infos[1]};
|
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() {
|
attachToInput() {
|
||||||
Observable.fromEvent(this.urlInput.nativeElement, 'keyup')
|
Observable.fromEvent(this.urlInput.nativeElement, 'keyup')
|
||||||
.map((e: any) => e.target.value) // extract the value of input
|
.map((e: any) => e.target.value) // extract the value of input
|
||||||
@@ -585,7 +806,7 @@ export class MainComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onResize(event) {
|
onResize(event) {
|
||||||
this.files_cols = (event.target.innerWidth <= 450) ? 2 : 4;
|
this.setCols();
|
||||||
}
|
}
|
||||||
|
|
||||||
videoModeChanged(new_val) {
|
videoModeChanged(new_val) {
|
||||||
@@ -593,6 +814,37 @@ export class MainComponent implements OnInit {
|
|||||||
localStorage.setItem('audioOnly', new_val.checked.toString());
|
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;
|
||||||
|
localStorage.setItem('customOutputEnabled', 'false');
|
||||||
|
|
||||||
|
this.youtubeAuthEnabled = false;
|
||||||
|
localStorage.setItem('youtubeAuthEnabled', 'false');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customOutputEnabledChanged(new_val) {
|
||||||
|
localStorage.setItem('customOutputEnabled', new_val.checked.toString());
|
||||||
|
if (new_val.checked === true && this.customArgsEnabled) {
|
||||||
|
this.customArgsEnabled = false;
|
||||||
|
localStorage.setItem('customArgsEnabled', 'false');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
youtubeAuthEnabledChanged(new_val) {
|
||||||
|
localStorage.setItem('youtubeAuthEnabled', new_val.checked.toString());
|
||||||
|
if (new_val.checked === true && this.customArgsEnabled) {
|
||||||
|
this.customArgsEnabled = false;
|
||||||
|
localStorage.setItem('customArgsEnabled', 'false');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
getAudioAndVideoFormats(formats): any[] {
|
getAudioAndVideoFormats(formats): any[] {
|
||||||
const audio_formats = {};
|
const audio_formats = {};
|
||||||
const video_formats = {};
|
const video_formats = {};
|
||||||
@@ -653,5 +905,61 @@ export class MainComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
return best_audio_format_for_mp4;
|
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;
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.video-player:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
.audio-styles {
|
.audio-styles {
|
||||||
height: 50px;
|
height: 50px;
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
@@ -58,4 +62,18 @@
|
|||||||
.save-icon {
|
.save-icon {
|
||||||
bottom: 1px;
|
bottom: 1px;
|
||||||
position: relative;
|
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;
|
||||||
}
|
}
|
||||||
@@ -8,13 +8,21 @@
|
|||||||
</vg-player>
|
</vg-player>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12 my-2">
|
<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-group cdkDropList [cdkDropListSortingDisabled]="!id" (cdkDropListDropped)="drop($event)" 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 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">{{playlist_item.label}}</mat-button-toggle>
|
||||||
</mat-button-toggle-group>
|
</mat-button-toggle-group>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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">
|
<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 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>
|
<button *ngIf="!id" color="accent" class="favorite-button" color="primary" (click)="namePlaylistDialog()" mat-fab><mat-icon class="save-icon">favorite</mat-icon></button>
|
||||||
|
|||||||
@@ -4,11 +4,13 @@ import { PostsService } from 'app/posts.services';
|
|||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
import { MatDialog, MatSnackBar } from '@angular/material';
|
import { MatDialog, MatSnackBar } from '@angular/material';
|
||||||
import { InputDialogComponent } from 'app/input-dialog/input-dialog.component';
|
import { InputDialogComponent } from 'app/input-dialog/input-dialog.component';
|
||||||
|
import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop';
|
||||||
|
|
||||||
export interface IMedia {
|
export interface IMedia {
|
||||||
title: string;
|
title: string;
|
||||||
src: string;
|
src: string;
|
||||||
type: string;
|
type: string;
|
||||||
|
label: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -19,6 +21,8 @@ export interface IMedia {
|
|||||||
export class PlayerComponent implements OnInit {
|
export class PlayerComponent implements OnInit {
|
||||||
|
|
||||||
playlist: Array<IMedia> = [];
|
playlist: Array<IMedia> = [];
|
||||||
|
original_playlist: string = null;
|
||||||
|
playlist_updating = false;
|
||||||
|
|
||||||
currentIndex = 0;
|
currentIndex = 0;
|
||||||
currentItem: IMedia = null;
|
currentItem: IMedia = null;
|
||||||
@@ -50,15 +54,12 @@ export class PlayerComponent implements OnInit {
|
|||||||
this.id = this.route.snapshot.paramMap.get('id');
|
this.id = this.route.snapshot.paramMap.get('id');
|
||||||
|
|
||||||
// loading config
|
// loading config
|
||||||
this.postsService.loadNavItems().subscribe(result => { // loads settings
|
this.postsService.loadNavItems().subscribe(res => { // loads settings
|
||||||
this.baseStreamPath = result['YoutubeDLMaterial']['Downloader']['path-base'];
|
const result = !this.postsService.debugMode ? res['config_file'] : res;
|
||||||
|
this.baseStreamPath = this.postsService.path;
|
||||||
this.audioFolderPath = result['YoutubeDLMaterial']['Downloader']['path-audio'];
|
this.audioFolderPath = result['YoutubeDLMaterial']['Downloader']['path-audio'];
|
||||||
this.videoFolderPath = result['YoutubeDLMaterial']['Downloader']['path-video'];
|
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;
|
let fileType = null;
|
||||||
if (this.type === 'audio') {
|
if (this.type === 'audio') {
|
||||||
@@ -73,15 +74,26 @@ export class PlayerComponent implements OnInit {
|
|||||||
for (let i = 0; i < this.fileNames.length; i++) {
|
for (let i = 0; i < this.fileNames.length; i++) {
|
||||||
const fileName = this.fileNames[i];
|
const fileName = this.fileNames[i];
|
||||||
const baseLocation = (this.type === 'audio') ? this.audioFolderPath : this.videoFolderPath;
|
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');
|
||||||
|
// if it has a slash (meaning it's in a directory), only get the file name for the label
|
||||||
|
let label = null;
|
||||||
|
const decodedName = decodeURIComponent(fileName);
|
||||||
|
const hasSlash = decodedName.includes('/') || decodedName.includes('\\');
|
||||||
|
if (hasSlash) {
|
||||||
|
label = decodedName.replace(/^.*[\\\/]/, '');
|
||||||
|
} else {
|
||||||
|
label = decodedName;
|
||||||
|
}
|
||||||
const mediaObject: IMedia = {
|
const mediaObject: IMedia = {
|
||||||
title: fileName,
|
title: fileName,
|
||||||
src: fullLocation,
|
src: fullLocation,
|
||||||
type: fileType
|
type: fileType,
|
||||||
|
label: label
|
||||||
}
|
}
|
||||||
this.playlist.push(mediaObject);
|
this.playlist.push(mediaObject);
|
||||||
}
|
}
|
||||||
this.currentItem = this.playlist[this.currentIndex];
|
this.currentItem = this.playlist[this.currentIndex];
|
||||||
|
this.original_playlist = JSON.stringify(this.playlist);
|
||||||
});
|
});
|
||||||
|
|
||||||
// this.getFileInfos();
|
// this.getFileInfos();
|
||||||
@@ -122,11 +134,20 @@ export class PlayerComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getFileInfos() {
|
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) {
|
decodeURI(string) {
|
||||||
return decodeURI(string);
|
return decodeURI(string);
|
||||||
}
|
}
|
||||||
@@ -179,7 +200,8 @@ export class PlayerComponent implements OnInit {
|
|||||||
|
|
||||||
// Eventually do additional checks on name
|
// Eventually do additional checks on name
|
||||||
if (name) {
|
if (name) {
|
||||||
this.postsService.createPlaylist(name, this.fileNames, this.type, null).subscribe(res => {
|
const fileNames = this.getFileNames();
|
||||||
|
this.postsService.createPlaylist(name, fileNames, this.type, null).subscribe(res => {
|
||||||
if (res['success']) {
|
if (res['success']) {
|
||||||
dialogRef.close();
|
dialogRef.close();
|
||||||
const new_playlist = res['new_playlist'];
|
const new_playlist = res['new_playlist'];
|
||||||
@@ -208,6 +230,30 @@ export class PlayerComponent implements OnInit {
|
|||||||
this.router.navigateByUrl(this.router.url + ';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
|
// snackbar helper
|
||||||
public openSnackBar(message: string, action: string) {
|
public openSnackBar(message: string, action: string) {
|
||||||
this.snackBar.open(message, action, {
|
this.snackBar.open(message, action, {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import {Injectable, isDevMode} from '@angular/core';
|
import {Injectable, isDevMode, Inject} from '@angular/core';
|
||||||
import { HttpClient, HttpHeaders, HttpRequest, HttpResponseBase } from '@angular/common/http';
|
import { HttpClient, HttpHeaders, HttpRequest, HttpResponseBase } from '@angular/common/http';
|
||||||
import config from '../assets/default.json';
|
import config from '../assets/default.json';
|
||||||
import 'rxjs/add/operator/map';
|
import 'rxjs/add/operator/map';
|
||||||
@@ -7,20 +7,31 @@ import 'rxjs/add/operator/map';
|
|||||||
import 'rxjs/add/operator/catch';
|
import 'rxjs/add/operator/catch';
|
||||||
import 'rxjs/add/observable/throw';
|
import 'rxjs/add/observable/throw';
|
||||||
import { THEMES_CONFIG } from '../themes';
|
import { THEMES_CONFIG } from '../themes';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
import { DOCUMENT } from '@angular/common';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class PostsService {
|
export class PostsService {
|
||||||
path = '';
|
path = '';
|
||||||
audioFolder = '';
|
audioFolder = '';
|
||||||
videoFolder = '';
|
videoFolder = '';
|
||||||
startPath = 'http://localhost:17442/';
|
startPath = null; // 'http://localhost:17442/';
|
||||||
startPathSSL = 'https://localhost:17442/'
|
startPathSSL = null; // 'https://localhost:17442/'
|
||||||
handShakeComplete = false;
|
handShakeComplete = false;
|
||||||
THEMES_CONFIG = THEMES_CONFIG;
|
THEMES_CONFIG = THEMES_CONFIG;
|
||||||
theme;
|
theme;
|
||||||
|
|
||||||
constructor(private http: HttpClient) {
|
debugMode = false;
|
||||||
|
constructor(private http: HttpClient, private router: Router, @Inject(DOCUMENT) private document: Document) {
|
||||||
console.log('PostsService Initialized...');
|
console.log('PostsService Initialized...');
|
||||||
|
// this.startPath = window.location.href + '/api/';
|
||||||
|
// this.startPathSSL = window.location.href + '/api/';
|
||||||
|
this.path = this.document.location.origin + '/api/';
|
||||||
|
|
||||||
|
if (isDevMode()) {
|
||||||
|
this.debugMode = true;
|
||||||
|
this.path = 'http://localhost:17442/api/';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setTheme(theme) {
|
setTheme(theme) {
|
||||||
@@ -43,16 +54,26 @@ export class PostsService {
|
|||||||
return this.http.get(this.startPath + 'audiofolder');
|
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, youtubeUsername: string = null, youtubePassword: string = null) {
|
||||||
return this.http.post(this.path + 'tomp3', {url: url,
|
return this.http.post(this.path + 'tomp3', {url: url,
|
||||||
maxBitrate: selectedQuality,
|
maxBitrate: selectedQuality,
|
||||||
customQualityConfiguration: customQualityConfiguration});
|
customQualityConfiguration: customQualityConfiguration,
|
||||||
|
customArgs: customArgs,
|
||||||
|
customOutput: customOutput,
|
||||||
|
youtubeUsername: youtubeUsername,
|
||||||
|
youtubePassword: youtubePassword});
|
||||||
}
|
}
|
||||||
|
|
||||||
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, youtubeUsername: string = null, youtubePassword: string = null) {
|
||||||
return this.http.post(this.path + 'tomp4', {url: url,
|
return this.http.post(this.path + 'tomp4', {url: url,
|
||||||
selectedHeight: selectedQuality,
|
selectedHeight: selectedQuality,
|
||||||
customQualityConfiguration: customQualityConfiguration});
|
customQualityConfiguration: customQualityConfiguration,
|
||||||
|
customArgs: customArgs,
|
||||||
|
customOutput: customOutput,
|
||||||
|
youtubeUsername: youtubeUsername,
|
||||||
|
youtubePassword: youtubePassword});
|
||||||
}
|
}
|
||||||
|
|
||||||
getFileStatusMp3(name: string) {
|
getFileStatusMp3(name: string) {
|
||||||
@@ -66,10 +87,9 @@ export class PostsService {
|
|||||||
loadNavItems() {
|
loadNavItems() {
|
||||||
if (isDevMode()) {
|
if (isDevMode()) {
|
||||||
return this.http.get('./assets/default.json');
|
return this.http.get('./assets/default.json');
|
||||||
|
} else {
|
||||||
|
return this.http.get(this.path + 'config');
|
||||||
}
|
}
|
||||||
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) {
|
deleteFile(name: string, isAudio: boolean) {
|
||||||
@@ -107,6 +127,12 @@ export class PostsService {
|
|||||||
thumbnailURL: thumbnailURL});
|
thumbnailURL: thumbnailURL});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updatePlaylist(playlistID, fileNames, type) {
|
||||||
|
return this.http.post(this.path + 'updatePlaylist', {playlistID: playlistID,
|
||||||
|
fileNames: fileNames,
|
||||||
|
type: type});
|
||||||
|
}
|
||||||
|
|
||||||
removePlaylist(playlistID, type) {
|
removePlaylist(playlistID, type) {
|
||||||
return this.http.post(this.path + 'deletePlaylist', {playlistID: playlistID, type: type});
|
return this.http.post(this.path + 'deletePlaylist', {playlistID: playlistID, type: type});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,15 @@
|
|||||||
{
|
{
|
||||||
"YoutubeDLMaterial": {
|
"YoutubeDLMaterial": {
|
||||||
"Host": {
|
"Host": {
|
||||||
"frontendurl": "http://localhost:4200",
|
"url": "http://localhost",
|
||||||
"backendurl": "http://localhost:17442/"
|
"port": "17442"
|
||||||
},
|
},
|
||||||
"Encryption": {
|
"Encryption": {
|
||||||
"use-encryption": false,
|
"use-encryption": false,
|
||||||
"cert-file-path": "/etc/letsencrypt/live/example.com/fullchain.pem",
|
"cert-file-path": "/etc/letsencrypt/live/example.com/fullchain.pem",
|
||||||
"key-file-path": "/etc/letsencrypt/live/example.com/privkey.pem"
|
"key-file-path": "/etc/letsencrypt/live/example.com/privkey.pem"
|
||||||
},
|
},
|
||||||
"Downloader": {
|
"Downloader": {
|
||||||
"path-base": "http://localhost:17442/",
|
|
||||||
"path-audio": "audio/",
|
"path-audio": "audio/",
|
||||||
"path-video": "video/"
|
"path-video": "video/"
|
||||||
},
|
},
|
||||||
@@ -18,7 +17,8 @@
|
|||||||
"title_top": "Youtube Downloader",
|
"title_top": "Youtube Downloader",
|
||||||
"file_manager_enabled": true,
|
"file_manager_enabled": true,
|
||||||
"allow_quality_select": true,
|
"allow_quality_select": true,
|
||||||
"download_only_mode": false
|
"download_only_mode": false,
|
||||||
|
"allow_multi_download_mode": true
|
||||||
},
|
},
|
||||||
"API": {
|
"API": {
|
||||||
"use_youtube_API": false,
|
"use_youtube_API": false,
|
||||||
@@ -27,6 +27,11 @@
|
|||||||
"Themes": {
|
"Themes": {
|
||||||
"default_theme": "default",
|
"default_theme": "default",
|
||||||
"allow_theme_change": true
|
"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 |
@@ -10,7 +10,7 @@
|
|||||||
<link href="https://fonts.googleapis.com/css?family=Roboto&display=swap" rel="stylesheet">
|
<link href="https://fonts.googleapis.com/css?family=Roboto&display=swap" rel="stylesheet">
|
||||||
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
||||||
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
|
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
|
||||||
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
|
<link href="https://fonts.googleapis.com/icon?family=Material+Icons|Material+Icons+Outlined" rel="stylesheet"/>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<app-root></app-root>
|
<app-root></app-root>
|
||||||
|
|||||||
Reference in New Issue
Block a user