Compare commits

..

45 Commits
v3.1 ... v3.3

Author SHA1 Message Date
Tzahi12345
a8d2e1d890 Merge pull request #12 from Tzahi12345/serve-nodejs
Serve frontend app through nodejs
2020-03-01 00:50:20 -05:00
Isaac Grynsztein
0511996b26 fixed margins on advanced mode UI and temporarily disabled youtube auth until youtube-dl fixes it
advanced mode inputs now get saved in cookies

fixed bug in UI where delete button was missing by making it more mobile-friendly
2020-03-01 00:48:22 -05:00
Isaac Grynsztein
f29a29bf2f fixed bug that prevented custom args from working 2020-03-01 00:46:42 -05:00
Isaac Grynsztein
24d4107311 Clarified old config in README 2020-02-29 04:31:23 -05:00
Isaac Grynsztein
a46f9c37c6 fixed bug where old config item was fetched 2020-02-29 04:31:06 -05:00
Isaac Grynsztein
2b1c68bad0 update docker-compose and dockerfile 2020-02-29 04:30:56 -05:00
Isaac Grynsztein
e2d23404ce removed unused variable 2020-02-29 04:30:34 -05:00
Isaac Grynsztein
db208ed55e Updated README to reflect changes in the config 2020-02-29 03:30:21 -05:00
Isaac Grynsztein
b87a9f1e2f fixed bug where playlist titles included their relative path 2020-02-29 03:08:02 -05:00
Tzahi12345
a1ac1e450d Merge pull request #11 from Tzahi12345/auth-params
Add the ability to use youtube authentication
2020-02-28 22:12:29 -05:00
Tzahi12345
6764ea6c3b Merge pull request #10 from Tzahi12345/url_params
Add URL params home page
2020-02-28 22:07:53 -05:00
Isaac Grynsztein
8a52020186 resized favicon 2020-02-28 22:05:16 -05:00
Isaac Grynsztein
695b836852 added url params on home page to auto download content
created chrome extension to facilitate this feature
2020-02-28 21:21:17 -05:00
Isaac Grynsztein
71d7c30032 updated backend to support youtube auth
frontend now support youtube auth as well
2020-02-28 20:09:59 -05:00
Isaac Grynsztein
5ca4f036c7 fixed bug where if multi mode was enabled, click on file card URLs didn't work 2020-02-28 00:32:33 -05:00
Isaac Grynsztein
1ffe61f01f removed path-base and updated docker-compose.yml & README 2020-02-28 00:20:08 -05:00
Isaac Grynsztein
5e331b9ffa config settings now just have url and port
fixed bug where multi download mode would not allow file card link clicking
2020-02-28 00:14:46 -05:00
Isaac Grynsztein
09bdae90e2 refactored code so node.js serves the angular app, and all the backend routes are prepended with /api/
nodejs now compressed requests
2020-02-27 22:52:50 -05:00
Isaac Grynsztein
37cc8f4fe1 fixed error in docker compose 2020-02-27 04:15:19 -05:00
Isaac Grynsztein
925e083b8d added new config settings to README 2020-02-27 04:12:27 -05:00
Isaac Grynsztein
6dc42278c4 updated docker-compose to new youtubedl-material version number 2020-02-27 03:57:41 -05:00
Isaac Grynsztein
12c227badb temporarily disabled advanced mode until further testing 2020-02-27 03:47:11 -05:00
Isaac Grynsztein
181a9f842c fixed bug where downloading files failed if the name had to be encoded 2020-02-27 03:46:57 -05:00
Isaac Grynsztein
b79d801c0f Added support for custom arguments and custom output patch 2020-02-27 03:27:57 -05:00
Isaac Grynsztein
fc3691336d added allow multi download mode setting frontend implementation 2020-02-27 01:10:23 -05:00
Isaac Grynsztein
bcd879ebc8 added multiple download support
lazy loaded images now reload after a new download
2020-02-27 01:06:32 -05:00
Isaac Grynsztein
b646db4828 Added the ability to cancel downloads
Audio only checkbox now disabled when downloading

Laid the groundwork for multiple simulataneous downloads
2020-02-26 19:04:02 -05:00
Isaac Grynsztein
426d52e359 fixed tabindex ordering of file cards (delete came before url) 2020-02-26 19:03:13 -05:00
Isaac Grynsztein
17199dd9c0 Updated readme 2020-02-26 18:11:37 -05:00
Isaac Grynsztein
c680c2827b reworded docker steps 2020-02-26 03:18:19 -05:00
Isaac Grynsztein
2dbf8d31f7 Updated docker info in readme 2020-02-26 03:17:29 -05:00
Isaac Grynsztein
a3753e557c Updated docker instructions 2020-02-26 03:16:42 -05:00
Isaac Grynsztein
ec80abdc8e Updated readme.md to include docker steps 2020-02-26 03:07:22 -05:00
Isaac Grynsztein
1ef7d24c22 downgraded required docker-compose.yml to 2 2020-02-26 01:35:21 -05:00
Isaac Grynsztein
4b6f6996ae fixed bug in docker during building 2020-02-26 01:28:46 -05:00
Isaac Grynsztein
c930ee94c5 added docker support
reworked backend to allow for containerization. config items can now be overwritten by environment vars

fixed bug during building

updated youtube-dl version in backend
2020-02-26 00:34:13 -05:00
Isaac Grynsztein
e88edbef5a Updated README to fix missing step 2020-02-24 16:22:54 -05:00
Isaac Grynsztein
ac13ed3359 updated package.json to remove bug during building 2020-02-24 16:18:09 -05:00
Isaac Grynsztein
faae0d44e6 make it better 2020-02-24 07:47:36 -05:00
Isaac Grynsztein
7d8ec04ad6 make it better 2020-02-24 07:39:00 -05:00
Isaac Grynsztein
8629e6ae9e make it better 2020-02-24 07:28:11 -05:00
Isaac Grynsztein
6e311d46a6 make it better 2020-02-24 06:41:13 -05:00
Isaac Grynsztein
006e983c14 make it better 2020-02-24 06:13:56 -05:00
Isaac Grynsztein
5db3e06a81 make it better 2020-02-24 06:05:24 -05:00
Isaac Grynsztein
2ced7b7f91 researching heroku support 2020-02-24 05:58:25 -05:00
39 changed files with 15479 additions and 315 deletions

2
.gitignore vendored
View File

@@ -43,8 +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 backend/db.json
src/assets/default.json src/assets/default.json

21
Dockerfile Normal file
View 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" ]

1
Procfile Normal file
View File

@@ -0,0 +1 @@
web: cd backend && node app.js

View File

@@ -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 8](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
@@ -20,46 +22,54 @@ 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 ### 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. 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 | | Config item | Description | Default |
| ------------- | ------------- | ------------- | | ------------- | ------------- | ------------- |
| frontendurl | URL to the webserver hosting YTDL-Material | "http://example.com" | | url | URL to the server hosting YoutubeDL-Material | "http://example.com" |
| backendurl | URL to the YTDL-Material's backend, should include port 17442 | "http://example.com:17442/" | | port | Desired port for YoutubeDL-Material | "17442" |
| use-encryption | true if you intend to use SSL encryption (https) | false | | 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" | | 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" | | key-file-path | Private key file path - required if using encryption | "/etc/letsencrypt/live/example.com/privkey.pem" |
| path-base | Audio/video stream URL. Usually the same as backendurl | "http://example.com:17442/" |
| path-audio | Path to audio folder for saved mp3s | "audio/" | | path-audio | Path to audio folder for saved mp3s | "audio/" |
| path-video | Path to video folder for saved mp4s | "video/" | | path-video | Path to video folder for saved mp4s | "video/" |
| title_top | Title shown on the top toolbar | "Youtube Downloader" | | title_top | Title shown on the top toolbar | "Youtube Downloader" |
| file_manager_enabled | true if you want to use the file manager | true | | 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 | | 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 | | 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 | | 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 | "" | | 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" | | 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 | | 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
@@ -67,11 +77,21 @@ If you'd like to install YoutubeDL-Material, go to the Installation section. If
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

View File

@@ -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,70 +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 backendUrl = config.get("YoutubeDLMaterial.Host.backendurl")
var backendPort = 17442;
var usingEncryption = config.get("YoutubeDLMaterial.Encryption.use-encryption");
var basePath = config.get("YoutubeDLMaterial.Downloader.path-base");
var audioFolderPath = config.get("YoutubeDLMaterial.Downloader.path-audio");
var videoFolderPath = config.get("YoutubeDLMaterial.Downloader.path-video");
var downloadOnlyMode = config.get("YoutubeDLMaterial.Extra.download_only_mode")
var useDefaultDownloadingAgent = config.get("YoutubeDLMaterial.Advanced.use_default_downloading_agent");
var customDownloadingAgent = config.get("YoutubeDLMaterial.Advanced.custom_downloading_agent");
var validDownloadingAgents = [ var validDownloadingAgents = [
'aria2c' 'aria2c'
] ]
if (!useDefaultDownloadingAgent && validDownloadingAgents.indexOf(customDownloadingAgent) !== -1 ) {
console.log(`INFO: Using non-default downloading agent \'${customDownloadingAgent}\'`) // don't overwrite config if it already happened.. NOT
// let alreadyWritten = db.get('configWriteFlag').value();
let writeConfigMode = process.env.write_ytdl_config;
var config = null;
if (writeConfigMode) {
setAndLoadConfig();
} else {
loadConfig();
} }
var descriptors = {}; 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) {
@@ -94,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);
@@ -319,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++) {
@@ -382,31 +494,69 @@ async function getUrlInfos(urls) {
}); });
} }
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 = ['-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 (!useDefaultDownloadingAgent && customDownloadingAgent === 'aria2c') { if (youtubeUsername && youtubePassword) {
downloadConfig.splice(0, 0, '--external-downloader', 'aria2c'); 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) {
@@ -434,12 +584,14 @@ app.post('/tomp3', function(req, res) {
continue; 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({
@@ -450,27 +602,46 @@ 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');
}
} }
let downloadConfig = ['-o', path + videopath + ".mp4", '-f', qualityPath, '--write-info-json', '--print-json']
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) {
if (debugMode) { if (debugMode) {
let new_date = Date.now(); let new_date = Date.now();
@@ -506,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;
@@ -523,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";
@@ -545,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";
@@ -565,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({
@@ -603,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({
@@ -640,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;
@@ -663,7 +825,7 @@ app.post('/createPlaylist', async (req, res) => {
}) })
}); });
app.post('/updatePlaylist', async (req, res) => { app.post('/api/updatePlaylist', async (req, res) => {
let playlistID = req.body.playlistID; let playlistID = req.body.playlistID;
let fileNames = req.body.fileNames; let fileNames = req.body.fileNames;
let type = req.body.type; let type = req.body.type;
@@ -689,7 +851,7 @@ app.post('/updatePlaylist', async (req, res) => {
}) })
}); });
app.post('/deletePlaylist', async (req, res) => { 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;
@@ -711,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;
@@ -731,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;
@@ -750,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') {
@@ -780,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
@@ -834,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 = {
@@ -860,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;
@@ -880,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
View 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
}

View File

@@ -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,
@@ -30,7 +30,8 @@
}, },
"Advanced": { "Advanced": {
"use_default_downloading_agent": true, "use_default_downloading_agent": true,
"custom_downloading_agent": "" "custom_downloading_agent": "",
"allow_advanced_download": false
} }
} }
} }

View File

@@ -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,
@@ -30,7 +30,8 @@
}, },
"Advanced": { "Advanced": {
"use_default_downloading_agent": true, "use_default_downloading_agent": true,
"custom_downloading_agent": "" "custom_downloading_agent": "",
"allow_advanced_download": false
} }
} }
} }

96
backend/consts.js Normal file
View 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

File diff suppressed because it is too large Load Diff

View File

@@ -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"
} }
} }

BIN
chrome-extension.crx Normal file

Binary file not shown.

28
chrome-extension.pem Normal file
View 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-----

View 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 });
}
});
});
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

View 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
}
}

View 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>

View 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);

6
db.json Normal file
View File

@@ -0,0 +1,6 @@
{
"playlists": {
"audio": [],
"video": []
}
}

33
docker-compose.yml Normal file
View 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
View 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
View File

12768
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -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",

View File

@@ -40,7 +40,8 @@ export class AppComponent implements OnInit {
public router: Router, public overlayContainer: OverlayContainer, private elementRef: ElementRef) { 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';

View File

@@ -25,11 +25,12 @@ import {VgBufferingModule} from 'videogular2/compiled/buffering';
import { InputDialogComponent } from './input-dialog/input-dialog.component'; import { InputDialogComponent } from './input-dialog/input-dialog.component';
import { LazyLoadImageModule, IsVisibleProps } 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 } from './main/main.component'; import { audioFilesMouseHovering, videoFilesMouseHovering, audioFilesOpened, videoFilesOpened } from './main/main.component';
import { CreatePlaylistComponent } from './create-playlist/create-playlist.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>) { export function isVisible({ event, element, scrollContainer, offset }: IsVisibleProps<any>) {
return (element.id === 'video' ? videoFilesMouseHovering : audioFilesMouseHovering); return (element.id === 'video' ? videoFilesMouseHovering || videoFilesOpened : audioFilesMouseHovering || audioFilesOpened);
} }
@NgModule({ @NgModule({
@@ -39,7 +40,8 @@ export function isVisible({ event, element, scrollContainer, offset }: IsVisible
MainComponent, MainComponent,
PlayerComponent, PlayerComponent,
InputDialogComponent, InputDialogComponent,
CreatePlaylistComponent CreatePlaylistComponent,
DownloadItemComponent
], ],
imports: [ imports: [
BrowserModule, BrowserModule,

View 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>

View 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();
});
});

View 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);
}
}

View File

@@ -1,5 +1,4 @@
<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/>
@@ -13,9 +12,6 @@
</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>

View File

@@ -115,4 +115,8 @@ mat-form-field.mat-form-field {
.add-playlist-button { .add-playlist-button {
float: right; float: right;
}
.advanced-input {
width: 100%;
} }

View File

@@ -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 (mouseleave)="accordionLeft('audio')" (mouseenter)="accordionEntered('audio')" 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
@@ -115,7 +169,7 @@
</div> </div>
</mat-expansion-panel> </mat-expansion-panel>
<mat-expansion-panel (mouseleave)="accordionLeft('video')" (mouseenter)="accordionEntered('video')" 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

View File

@@ -1,4 +1,4 @@
import { Component, OnInit, ElementRef, ViewChild, ViewChildren, QueryList } 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';
@@ -15,12 +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 { CreatePlaylistComponent } from 'app/create-playlist/create-playlist.component';
import { Platform } from '@angular/cdk/platform'; import { Platform } from '@angular/cdk/platform';
import { v4 as uuid } from 'uuid';
export let audioFilesMouseHovering = false; export let audioFilesMouseHovering = false;
export let videoFilesMouseHovering = 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',
@@ -28,24 +41,36 @@ export let videoFilesMouseHovering = false;
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 = {};
@@ -58,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]);
@@ -166,27 +193,33 @@ export class MainComponent implements OnInit {
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, public dialog: MatDialog, private platform: Platform) { 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();
@@ -197,12 +230,66 @@ 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() {
@@ -258,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);
@@ -328,70 +427,79 @@ export class MainComponent implements OnInit {
}); });
} }
// app initialization.
ngOnInit() {
// this.iOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window['MSStream'];
this.iOS = this.platform.IOS;
if (localStorage.getItem('audioOnly') !== null) {
this.audioOnly = localStorage.getItem('audioOnly') === 'true';
}
}
// 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);
} }
} }
@@ -401,7 +509,37 @@ 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;
@@ -412,18 +550,37 @@ export class MainComponent implements OnInit {
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] && this.cachedAvailableFormats[this.url]['formats']; const cachedFormatsExists = this.cachedAvailableFormats[this.url] && this.cachedAvailableFormats[this.url]['formats'];
if (cachedFormatsExists) { if (cachedFormatsExists) {
@@ -433,24 +590,64 @@ export class MainComponent implements OnInit {
} }
} }
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 => {
@@ -609,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) {
@@ -617,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 = {};
@@ -700,6 +928,22 @@ export class MainComponent implements OnInit {
} }
} }
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 // creating a playlist
openCreatePlaylistDialog(type) { openCreatePlaylistDialog(type) {
const dialogRef = this.dialog.open(CreatePlaylistComponent, { const dialogRef = this.dialog.open(CreatePlaylistComponent, {

View File

@@ -9,7 +9,7 @@
</div> </div>
<div class="col-12 my-2"> <div class="col-12 my-2">
<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-group cdkDropList [cdkDropListSortingDisabled]="!id" (cdkDropListDropped)="drop($event)" style="width: 80%; left: 9%" vertical name="videoSelect" aria-label="Video Select" #group="matButtonToggleGroup">
<mat-button-toggle cdkDrag *ngFor="let playlist_item of playlist; let i = index" [checked]="currentItem.title === playlist_item.title" (click)="onClickPlaylistItem(playlist_item, i)" class="toggle-button" [value]="playlist_item.title">{{decodeURI(playlist_item.title)}}</mat-button-toggle> <mat-button-toggle 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>

View File

@@ -10,6 +10,7 @@ export interface IMedia {
title: string; title: string;
src: string; src: string;
type: string; type: string;
label: string;
} }
@Component({ @Component({
@@ -53,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') {
@@ -76,11 +74,21 @@ 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);
} }

View File

@@ -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) {

View File

@@ -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,
@@ -30,7 +30,8 @@
}, },
"Advanced": { "Advanced": {
"use_default_downloading_agent": true, "use_default_downloading_agent": true,
"custom_downloading_agent": "" "custom_downloading_agent": "",
"allow_advanced_download": true
} }
} }
} }

View File

@@ -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>