mirror of
https://github.com/Tzahi12345/YoutubeDL-Material.git
synced 2026-03-07 20:10:03 +03:00
Compare commits
54 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f92a5549b5 | ||
|
|
69bf4e1ad5 | ||
|
|
9d1aaf95ed | ||
|
|
bb925ac0c8 | ||
|
|
74cda25c63 | ||
|
|
54dcbe452e | ||
|
|
946abd2e92 | ||
|
|
73d4cca615 | ||
|
|
846dd7e250 | ||
|
|
6f3e94cf24 | ||
|
|
3cbb517d64 | ||
|
|
5d945d729b | ||
|
|
cda842a6c7 | ||
|
|
480ed7d000 | ||
|
|
881a103051 | ||
|
|
4172b0b355 | ||
|
|
f6b7c41666 | ||
|
|
3d1874c69b | ||
|
|
ccfe7901c9 | ||
|
|
17e85196ae | ||
|
|
ae605d5f70 | ||
|
|
e57839e8de | ||
|
|
09bcac1c14 | ||
|
|
f5073b83ed | ||
|
|
41bfc80c4e | ||
|
|
3d2e138f50 | ||
|
|
717f024c42 | ||
|
|
a70abb3945 | ||
|
|
a755b0b281 | ||
|
|
434f6751d0 | ||
|
|
dfecf3645b | ||
|
|
62a000b631 | ||
|
|
2673f4ee98 | ||
|
|
a8d2e1d890 | ||
|
|
0511996b26 | ||
|
|
f29a29bf2f | ||
|
|
24d4107311 | ||
|
|
a46f9c37c6 | ||
|
|
2b1c68bad0 | ||
|
|
e2d23404ce | ||
|
|
db208ed55e | ||
|
|
b87a9f1e2f | ||
|
|
a1ac1e450d | ||
|
|
6764ea6c3b | ||
|
|
8a52020186 | ||
|
|
695b836852 | ||
|
|
71d7c30032 | ||
|
|
5ca4f036c7 | ||
|
|
1ffe61f01f | ||
|
|
5e331b9ffa | ||
|
|
09bdae90e2 | ||
|
|
37cc8f4fe1 | ||
|
|
925e083b8d | ||
|
|
6dc42278c4 |
8
.gitignore
vendored
8
.gitignore
vendored
@@ -43,8 +43,16 @@ Thumbs.db
|
||||
|
||||
node_modules/*
|
||||
backend/node_modules/*
|
||||
backend/public/*
|
||||
YoutubeDL-Material/node_modules/*
|
||||
backend/video/*
|
||||
backend/audio/*
|
||||
backend/public/*
|
||||
backend/subscriptions/archives/*
|
||||
backend/subscriptions/playlists/*
|
||||
backend/subscriptions/channels/*
|
||||
backend/db.json
|
||||
backend/subscriptions/channels/*
|
||||
backend/subscriptions/playlists/*
|
||||
backend/subscriptions/archives/*
|
||||
src/assets/default.json
|
||||
|
||||
21
Dockerfile
21
Dockerfile
@@ -1,28 +1,21 @@
|
||||
FROM ubuntu:18.04
|
||||
FROM alpine:3.11
|
||||
|
||||
RUN apt-get update && apt-get install -y \
|
||||
nodejs \
|
||||
apache2 \
|
||||
npm \
|
||||
youtube-dl
|
||||
RUN apk add --update npm python ffmpeg
|
||||
|
||||
# Change directory so that our commands run inside this new directory
|
||||
WORKDIR /var/www/html
|
||||
WORKDIR /app
|
||||
|
||||
# Copy dependency definitions
|
||||
COPY ./ /var/www/html/
|
||||
COPY ./ /app/
|
||||
|
||||
# Change directory to backend
|
||||
WORKDIR /var/www/html/backend
|
||||
WORKDIR /app
|
||||
|
||||
# Install dependencies on backend
|
||||
RUN npm install
|
||||
|
||||
# Change back to original directory
|
||||
WORKDIR /var/www/html
|
||||
|
||||
# Expose the port the app runs in
|
||||
EXPOSE 80
|
||||
EXPOSE 17442
|
||||
|
||||
# Run the specified command within the container.
|
||||
CMD ./docker_wrapper.sh
|
||||
CMD [ "node", "app.js" ]
|
||||
52
README.md
52
README.md
@@ -22,7 +22,7 @@ Dark mode:
|
||||
|
||||
### Prerequisites
|
||||
|
||||
NOTE: If you would like to use Docker, you can go down to the [Docker](#Docker) section for a setup guide.
|
||||
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:
|
||||
|
||||
@@ -32,38 +32,44 @@ sudo apt-get install nodejs youtube-dl
|
||||
|
||||
### 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 the `youtubedl-material` directory 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, run `npm install` to install all the backend dependencies. Once that is finished, type `nodejs app.js`. This will run the backend server. On your browser, navigate to your installation folder. Try putting in a youtube link to see if it works. If it does, viola! YoutubeDL-Material is now up and running.
|
||||
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.
|
||||
|
||||
### Configuration
|
||||
|
||||
NOTE: If you are using YoutubeDL-Material v3.2 or lower, click [here](https://github.com/Tzahi12345/YoutubeDL-Material/blob/b87a9f1e2fd896b8e3b2f12429b7ffb15ea4521b/README.md#configuration) for the old README
|
||||
|
||||
Here is an explanation for the configuration entries. Check out the [default config](https://github.com/Tzahi12345/YoutubeDL-Material/blob/master/backend/config/default.json) for more context.
|
||||
|
||||
| Config item | Description | Default |
|
||||
| ------------- | ------------- | ------------- |
|
||||
| frontendurl | URL to the webserver hosting YTDL-Material | "http://example.com" |
|
||||
| backendurl | URL to the YTDL-Material's backend, should include port 17442 | "http://example.com:17442/" |
|
||||
| url | URL to the server hosting YoutubeDL-Material | "http://example.com" |
|
||||
| port | Desired port for YoutubeDL-Material | "17442" |
|
||||
| use-encryption | true if you intend to use SSL encryption (https) | false |
|
||||
| cert-file-path | Cert file path - required if using encryption | "/etc/letsencrypt/live/example.com/fullchain.pem" |
|
||||
| key-file-path | Private key file path - required if using encryption | "/etc/letsencrypt/live/example.com/privkey.pem" |
|
||||
| path-base | Audio/video stream URL. Usually the same as backendurl | "http://example.com:17442/" |
|
||||
| path-audio | Path to audio folder for saved mp3s | "audio/" |
|
||||
| path-video | Path to video folder for saved mp4s | "video/" |
|
||||
| title_top | Title shown on the top toolbar | "Youtube Downloader" |
|
||||
| file_manager_enabled | true if you want to use the file manager | true |
|
||||
| allow_quality_select | true if you want to select a videos quality level before downloading | true |
|
||||
| download_only_mode | true if you want files to directly download to the client with no media player | false |
|
||||
| allow_multi_download_mode | true if you want the ability to download multiple videos at the same time | true |
|
||||
| use_youtube_API | true if you want to use the Youtube API which is used for YT searches | false |
|
||||
| youtube_API_key | Youtube API key. Required if use_youtube_API is enabled | "" |
|
||||
| default_theme | Default theme to use. Options are "default" and "dark" | "default" |
|
||||
| allow_theme_change | true if you want the icon in the top toolbar that toggles dark mode | true |
|
||||
| use_default_downloading_agent | true if you want to use youtube-dl's default downloader | true |
|
||||
| custom_downloading_agent | If not using the default downloader, this is the downloader you want to use | "" |
|
||||
| allow_advanced_download | true if you want to use the Advanced download options | false |
|
||||
|
||||
## Deployment
|
||||
|
||||
@@ -71,21 +77,35 @@ 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.
|
||||
|
||||
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/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 `youtubedl-material` 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 https://github.com/Tzahi12345/YoutubeDL-Material/releases/latest/download/docker-compose.yml -o docker-compose.yml` to download the latest release of `docker-compose.yml`, or go to the [releases](https://github.com/Tzahi12345/YoutubeDL-Material/releases/) page to grab the version you'd like.
|
||||
2. Modify the config items in the `environment` section of `docker-compose.yml` to your liking. Otherwise, the default options will work and point to `http://localhost:8998`. You can find an explanation of these configuration items in [Configuration](#Configuration) section.
|
||||
3. Make sure the port in the `frontend_url` environment variable lines up with the port in the `ports` section.
|
||||
4. Run `docker-compose pull`. This will download the official YoutubeDL-Material docker image.
|
||||
5. Run `docker-compose up` to start it up. If successful, it should say "HTTP(S): Started on port 17442" or something similar. Make sure you can connect to the frontend, and if so, you are done!
|
||||
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!
|
||||
|
||||
## API
|
||||
|
||||
You can use the internal API on your server to run downloads on your instance without using the frontend. All of the available endpoints can be seen over [here](https://github.com/Tzahi12345/YoutubeDL-Material/blob/master/backend/app.js) -- search for '/api/' on the page to find all the endpoints. I will expand on the available endpoints in the future, but for now I'd like to highlight the two most useful ones:
|
||||
|
||||
#### Downloading audio files
|
||||
`curl -XPOST -H "Content-type: application/json" -d '{"url": "<your youtube url>"}' 'http://localhost:17442/api/tomp3'`
|
||||
|
||||
Remember to replace `<your video url>` with the actual URL.
|
||||
|
||||
#### Downloading video files
|
||||
`curl -XPOST -H "Content-type: application/json" -d '{"url": "<your youtube url>"}' 'http://localhost:17442/api/tomp4'`
|
||||
|
||||
Remember to replace `<your video url>` with the actual URL.
|
||||
|
||||
## Contributing
|
||||
|
||||
|
||||
377
backend/app.js
377
backend/app.js
@@ -1,7 +1,9 @@
|
||||
var async = require('async');
|
||||
const { uuid } = require('uuidv4');
|
||||
var fs = require('fs');
|
||||
var path = require('path');
|
||||
var youtubedl = require('youtube-dl');
|
||||
var compression = require('compression');
|
||||
var https = require('https');
|
||||
var express = require("express");
|
||||
var bodyParser = require("body-parser");
|
||||
@@ -9,7 +11,9 @@ var archiver = require('archiver');
|
||||
const low = require('lowdb')
|
||||
var URL = require('url').URL;
|
||||
const shortid = require('shortid')
|
||||
const url_api = require('url');
|
||||
var config_api = require('./config.js');
|
||||
var subscriptions_api = require('./subscriptions')
|
||||
|
||||
var app = express();
|
||||
|
||||
@@ -24,13 +28,14 @@ db.defaults(
|
||||
audio: [],
|
||||
video: []
|
||||
},
|
||||
configWriteFlag: false
|
||||
configWriteFlag: false,
|
||||
subscriptions: []
|
||||
}).write();
|
||||
|
||||
// config values
|
||||
var frontendUrl = null;
|
||||
var backendUrl = null;
|
||||
var backendPort = 17442;
|
||||
var backendPort = null;
|
||||
var usingEncryption = null;
|
||||
var basePath = null;
|
||||
var audioFolderPath = null;
|
||||
@@ -38,6 +43,8 @@ var videoFolderPath = null;
|
||||
var downloadOnlyMode = null;
|
||||
var useDefaultDownloadingAgent = null;
|
||||
var customDownloadingAgent = null;
|
||||
var allowSubscriptions = null;
|
||||
var subscriptionsCheckInterval = null;
|
||||
|
||||
// other needed values
|
||||
var options = null; // encryption options
|
||||
@@ -68,11 +75,6 @@ var descriptors = {};
|
||||
app.use(bodyParser.urlencoded({ extended: false }));
|
||||
app.use(bodyParser.json());
|
||||
|
||||
app.get('/using-encryption', function(req, res) {
|
||||
res.send(usingEncryption);
|
||||
res.end("yes");
|
||||
});
|
||||
|
||||
// objects
|
||||
|
||||
function File(id, title, thumbnailURL, isAudio, duration) {
|
||||
@@ -89,7 +91,7 @@ function startServer() {
|
||||
if (usingEncryption)
|
||||
{
|
||||
https.createServer(options, app).listen(backendPort, function() {
|
||||
console.log('HTTPS: Anchor set on 17442');
|
||||
console.log('HTTPS: Started on PORT ' + backendPort);
|
||||
});
|
||||
}
|
||||
else
|
||||
@@ -125,16 +127,17 @@ async function loadConfig() {
|
||||
// get config library
|
||||
// config = require('config');
|
||||
|
||||
frontendUrl = !debugMode ? config_api.getConfigItem('ytdl_frontend_url') : 'http://localhost:4200';
|
||||
backendUrl = config_api.getConfigItem('ytdl_backend_url')
|
||||
backendPort = 17442;
|
||||
url = !debugMode ? config_api.getConfigItem('ytdl_url') : 'http://localhost:4200';
|
||||
backendPort = config_api.getConfigItem('ytdl_port');
|
||||
usingEncryption = config_api.getConfigItem('ytdl_use_encryption');
|
||||
basePath = config_api.getConfigItem('ytdl_base_path');
|
||||
audioFolderPath = config_api.getConfigItem('ytdl_audio_folder_path');
|
||||
videoFolderPath = config_api.getConfigItem('ytdl_video_folder_path');
|
||||
downloadOnlyMode = config_api.getConfigItem('ytdl_download_only_mode');
|
||||
useDefaultDownloadingAgent = config_api.getConfigItem('ytdl_use_default_downloading_agent');
|
||||
customDownloadingAgent = config_api.getConfigItem('ytdl_custom_downloading_agent');
|
||||
allowSubscriptions = config_api.getConfigItem('ytdl_allow_subscriptions');
|
||||
subscriptionsCheckInterval = config_api.getConfigItem('ytdl_subscriptions_check_interval');
|
||||
|
||||
if (!useDefaultDownloadingAgent && validDownloadingAgents.indexOf(customDownloadingAgent) !== -1 ) {
|
||||
console.log(`INFO: Using non-default downloading agent \'${customDownloadingAgent}\'`)
|
||||
}
|
||||
@@ -153,7 +156,16 @@ async function loadConfig() {
|
||||
};
|
||||
}
|
||||
|
||||
url_domain = new URL(frontendUrl);
|
||||
url_domain = new URL(url);
|
||||
|
||||
// get subscriptions
|
||||
if (allowSubscriptions) {
|
||||
// runs initially, then runs every ${subscriptionCheckInterval} seconds
|
||||
watchSubscriptions();
|
||||
setInterval(() => {
|
||||
watchSubscriptions();
|
||||
}, subscriptionsCheckInterval * 1000);
|
||||
}
|
||||
|
||||
// start the server here
|
||||
startServer();
|
||||
@@ -163,6 +175,34 @@ async function loadConfig() {
|
||||
|
||||
}
|
||||
|
||||
function calculateSubcriptionRetrievalDelay(amount) {
|
||||
// frequency is 5 mins
|
||||
let frequency_in_ms = subscriptionsCheckInterval * 1000;
|
||||
let minimum_frequency = 60 * 1000;
|
||||
const first_frequency = frequency_in_ms/amount;
|
||||
return (first_frequency < minimum_frequency) ? minimum_frequency : first_frequency;
|
||||
}
|
||||
|
||||
function watchSubscriptions() {
|
||||
let subscriptions = subscriptions_api.getAllSubscriptions();
|
||||
|
||||
if (!subscriptions) return;
|
||||
|
||||
let subscriptions_amount = subscriptions.length;
|
||||
let delay_interval = calculateSubcriptionRetrievalDelay(subscriptions_amount);
|
||||
|
||||
let current_delay = 0;
|
||||
for (let i = 0; i < subscriptions.length; i++) {
|
||||
let sub = subscriptions[i];
|
||||
if (debugMode) console.log('watching ' + sub.name + ' with delay interval of ' + delay_interval);
|
||||
setTimeout(() => {
|
||||
subscriptions_api.getVideosForSub(sub);
|
||||
}, current_delay);
|
||||
current_delay += delay_interval;
|
||||
if (current_delay >= subscriptionsCheckInterval * 1000) current_delay = 0;
|
||||
}
|
||||
}
|
||||
|
||||
function getOrigin() {
|
||||
return url_domain.origin;
|
||||
}
|
||||
@@ -245,9 +285,14 @@ function getJSONMp3(name)
|
||||
return obj;
|
||||
}
|
||||
|
||||
function getJSONMp4(name)
|
||||
function getJSONMp4(name, customPath = null)
|
||||
{
|
||||
var jsonPath = videoFolderPath+name+".info.json";
|
||||
let jsonPath = null;
|
||||
if (!customPath) {
|
||||
jsonPath = videoFolderPath+name+".info.json";
|
||||
} else {
|
||||
jsonPath = customPath + name + ".info.json";
|
||||
}
|
||||
if (fs.existsSync(jsonPath))
|
||||
{
|
||||
var obj = JSON.parse(fs.readFileSync(jsonPath, 'utf8'));
|
||||
@@ -374,10 +419,11 @@ function deleteAudioFile(name) {
|
||||
});
|
||||
}
|
||||
|
||||
async function deleteVideoFile(name) {
|
||||
async function deleteVideoFile(name, customPath = null) {
|
||||
return new Promise(resolve => {
|
||||
var jsonPath = path.join(videoFolderPath,name+'.info.json');
|
||||
var videoFilePath = path.join(videoFolderPath,name+'.mp4');
|
||||
let filePath = customPath ? customPath : videoFolderPath;
|
||||
var jsonPath = path.join(filePath,name+'.info.json');
|
||||
var videoFilePath = path.join(filePath,name+'.mp4');
|
||||
jsonPath = path.join(__dirname, jsonPath);
|
||||
videoFilePath = path.join(__dirname, videoFilePath);
|
||||
|
||||
@@ -506,34 +552,69 @@ app.use(function(req, res, next) {
|
||||
next();
|
||||
});
|
||||
|
||||
app.post('/tomp3', function(req, res) {
|
||||
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.post('/api/setConfig', function(req, res) {
|
||||
let new_config_file = req.body.new_config_file;
|
||||
if (new_config_file && new_config_file['YoutubeDLMaterial']) {
|
||||
let success = config_api.setConfigFile(new_config_file);
|
||||
res.send({
|
||||
success: success
|
||||
});
|
||||
} else {
|
||||
console.log('ERROR: Tried to save invalid config file!')
|
||||
res.sendStatus(400);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
app.get('/api/using-encryption', function(req, res) {
|
||||
res.send(usingEncryption);
|
||||
});
|
||||
|
||||
app.post('/api/tomp3', function(req, res) {
|
||||
var url = req.body.url;
|
||||
var date = Date.now();
|
||||
var audiopath = '%(title)s';
|
||||
|
||||
var customQualityConfiguration = req.body.customQualityConfiguration;
|
||||
var maxBitrate = req.body.maxBitrate;
|
||||
var globalArgs = config_api.getConfigItem('ytdl_custom_args');
|
||||
var customArgs = req.body.customArgs;
|
||||
var customOutput = req.body.customOutput;
|
||||
var youtubeUsername = req.body.youtubeUsername;
|
||||
var youtubePassword = req.body.youtubePassword;
|
||||
|
||||
|
||||
let downloadConfig = null;
|
||||
let qualityPath = '';
|
||||
|
||||
if (customArgs) {
|
||||
downloadConfig = [customArgs];
|
||||
downloadConfig = customArgs.split(' ');
|
||||
} else {
|
||||
if (customQualityConfiguration) {
|
||||
qualityPath = `-f ${customQualityConfiguration}`;
|
||||
} else if (maxBitrate) {
|
||||
if (!maxBitrate || maxBitrate === '') maxBitrate = '0';
|
||||
qualityPath = `--audio-quality ${maxBitrate}`
|
||||
}
|
||||
|
||||
if (customOutput) {
|
||||
downloadConfig = ['-o', audioFolderPath + customOutput + '.mp3', '-x', '--audio-format', 'mp3', '--write-info-json', '--print-json'];
|
||||
} else {
|
||||
downloadConfig = ['-o', audioFolderPath + audiopath + ".mp3", '-x', '--audio-format', 'mp3', '--write-info-json', '--print-json'];
|
||||
}
|
||||
|
||||
if (customQualityConfiguration) {
|
||||
qualityPath = `-f ${customQualityConfiguration}`;
|
||||
} else if (maxBitrate) {
|
||||
if (!maxBitrate || maxBitrate === '') maxBitrate = '0';
|
||||
qualityPath = `--audio-quality ${maxBitrate}`
|
||||
if (youtubeUsername && youtubePassword) {
|
||||
downloadConfig.push('--username', youtubeUsername, '--password', youtubePassword);
|
||||
}
|
||||
|
||||
if (qualityPath !== '') {
|
||||
@@ -543,6 +624,11 @@ app.post('/tomp3', function(req, res) {
|
||||
if (!useDefaultDownloadingAgent && customDownloadingAgent === 'aria2c') {
|
||||
downloadConfig.splice(0, 0, '--external-downloader', 'aria2c');
|
||||
}
|
||||
|
||||
if (globalArgs && globalArgs !== '') {
|
||||
// adds global args
|
||||
downloadConfig = downloadConfig.concat(globalArgs.split(' '));
|
||||
}
|
||||
}
|
||||
|
||||
youtubedl.exec(url, downloadConfig, {}, function(err, output) {
|
||||
@@ -588,38 +674,50 @@ 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 date = Date.now();
|
||||
var path = videoFolderPath;
|
||||
var videopath = '%(title)s';
|
||||
var globalArgs = config_api.getConfigItem('ytdl_custom_args');
|
||||
var customArgs = req.body.customArgs;
|
||||
var customOutput = req.body.customOutput;
|
||||
|
||||
var selectedHeight = req.body.selectedHeight;
|
||||
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';
|
||||
|
||||
if (customArgs) {
|
||||
downloadConfig = [customArgs];
|
||||
downloadConfig = customArgs.split(' ');
|
||||
} else {
|
||||
if (customQualityConfiguration) {
|
||||
qualityPath = customQualityConfiguration;
|
||||
} else if (selectedHeight && selectedHeight !== '') {
|
||||
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');
|
||||
}
|
||||
|
||||
if (globalArgs && globalArgs !== '') {
|
||||
// adds global args
|
||||
downloadConfig = downloadConfig.concat(globalArgs.split(' '));
|
||||
}
|
||||
}
|
||||
|
||||
youtubedl.exec(url, downloadConfig, {}, function(err, output) {
|
||||
@@ -676,8 +774,8 @@ app.post('/tomp4', function(req, res) {
|
||||
});
|
||||
|
||||
// gets the status of the mp3 file that's being downloaded
|
||||
app.post('/fileStatusMp3', function(req, res) {
|
||||
var name = decodeURI(req.body.name + "");
|
||||
app.post('/api/fileStatusMp3', function(req, res) {
|
||||
var name = decodeURIComponent(req.body.name + "");
|
||||
var exists = "";
|
||||
var fullpath = audioFolderPath + name + ".mp3";
|
||||
if (fs.existsSync(fullpath)) {
|
||||
@@ -698,8 +796,8 @@ app.post('/fileStatusMp3', function(req, res) {
|
||||
});
|
||||
|
||||
// gets the status of the mp4 file that's being downloaded
|
||||
app.post('/fileStatusMp4', function(req, res) {
|
||||
var name = decodeURI(req.body.name);
|
||||
app.post('/api/fileStatusMp4', function(req, res) {
|
||||
var name = decodeURIComponent(req.body.name);
|
||||
var exists = "";
|
||||
var fullpath = videoFolderPath + name + ".mp4";
|
||||
if (fs.existsSync(fullpath)) {
|
||||
@@ -718,7 +816,7 @@ app.post('/fileStatusMp4', function(req, res) {
|
||||
});
|
||||
|
||||
// gets all download mp3s
|
||||
app.post('/getMp3s', function(req, res) {
|
||||
app.post('/api/getMp3s', function(req, res) {
|
||||
var mp3s = [];
|
||||
var playlists = db.get('playlists.audio').value();
|
||||
var files = recFindByExt(audioFolderPath, 'mp3'); // fs.readdirSync(audioFolderPath);
|
||||
@@ -750,7 +848,7 @@ app.post('/getMp3s', function(req, res) {
|
||||
});
|
||||
|
||||
// gets all download mp4s
|
||||
app.post('/getMp4s', function(req, res) {
|
||||
app.post('/api/getMp4s', function(req, res) {
|
||||
var mp4s = [];
|
||||
var playlists = db.get('playlists.video').value();
|
||||
var fullpath = videoFolderPath;
|
||||
@@ -782,7 +880,142 @@ app.post('/getMp4s', function(req, res) {
|
||||
res.end("yes");
|
||||
});
|
||||
|
||||
app.post('/createPlaylist', async (req, res) => {
|
||||
app.post('/api/subscribe', async (req, res) => {
|
||||
let name = req.body.name;
|
||||
let url = req.body.url;
|
||||
let timerange = req.body.timerange;
|
||||
|
||||
const new_sub = {
|
||||
name: name,
|
||||
url: url,
|
||||
id: uuid()
|
||||
};
|
||||
|
||||
// adds timerange if it exists, otherwise all videos will be downloaded
|
||||
if (timerange) {
|
||||
new_sub.timerange = timerange;
|
||||
}
|
||||
|
||||
const result_obj = await subscriptions_api.subscribe(new_sub);
|
||||
|
||||
if (result_obj.success) {
|
||||
res.send({
|
||||
new_sub: new_sub
|
||||
});
|
||||
} else {
|
||||
res.send({
|
||||
new_sub: null,
|
||||
error: result_obj.error
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/unsubscribe', async (req, res) => {
|
||||
let deleteMode = req.body.deleteMode
|
||||
let sub = req.body.sub;
|
||||
|
||||
let result_obj = subscriptions_api.unsubscribe(sub, deleteMode);
|
||||
if (result_obj.success) {
|
||||
res.send({
|
||||
success: result_obj.success
|
||||
});
|
||||
} else {
|
||||
res.send({
|
||||
success: false,
|
||||
error: result_obj.error
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/deleteSubscriptionFile', async (req, res) => {
|
||||
let deleteForever = req.body.deleteForever;
|
||||
let file = req.body.file;
|
||||
let sub = req.body.sub;
|
||||
|
||||
let success = await subscriptions_api.deleteSubscriptionFile(sub, file, deleteForever);
|
||||
|
||||
if (success) {
|
||||
res.send({
|
||||
success: success
|
||||
});
|
||||
} else {
|
||||
res.sendStatus(500);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
app.post('/api/getSubscription', async (req, res) => {
|
||||
let subID = req.body.id;
|
||||
|
||||
// get sub from db
|
||||
let subscription = subscriptions_api.getSubscription(subID);
|
||||
|
||||
if (!subscription) {
|
||||
// failed to get subscription from db, send 400 error
|
||||
res.sendStatus(400);
|
||||
return;
|
||||
}
|
||||
|
||||
// get sub videos
|
||||
if (subscription.name) {
|
||||
let base_path = config_api.getConfigItem('ytdl_subscriptions_base_path');
|
||||
let appended_base_path = path.join(base_path, subscription.isPlaylist ? 'playlists' : 'channels', subscription.name, '/');
|
||||
let files;
|
||||
try {
|
||||
files = recFindByExt(appended_base_path, 'mp4');
|
||||
} catch(e) {
|
||||
files = null;
|
||||
console.log('Failed to get folder for subscription: ' + subscription.name + ' at path ' + appended_base_path);
|
||||
res.sendStatus(500);
|
||||
return;
|
||||
}
|
||||
var parsed_files = [];
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
let file = files[i];
|
||||
var file_path = file.substring(appended_base_path.length, file.length);
|
||||
var id = file_path.substring(0, file_path.length-4);
|
||||
var jsonobj = getJSONMp4(id, appended_base_path);
|
||||
if (!jsonobj) continue;
|
||||
var title = jsonobj.title;
|
||||
|
||||
var thumbnail = jsonobj.thumbnail;
|
||||
var duration = jsonobj.duration;
|
||||
var isaudio = false;
|
||||
var file_obj = new File(id, title, thumbnail, isaudio, duration);
|
||||
parsed_files.push(file_obj);
|
||||
}
|
||||
|
||||
res.send({
|
||||
subscription: subscription,
|
||||
files: parsed_files
|
||||
});
|
||||
} else {
|
||||
res.sendStatus(500);
|
||||
}
|
||||
|
||||
|
||||
|
||||
});
|
||||
|
||||
app.post('/api/downloadVideosForSubscription', async (req, res) => {
|
||||
let subID = req.body.subID;
|
||||
let sub = subscriptions_api.getSubscription(subID);
|
||||
subscriptions_api.getVideosForSub(sub);
|
||||
res.send({
|
||||
success: true
|
||||
});
|
||||
});
|
||||
|
||||
app.post('/api/getAllSubscriptions', async (req, res) => {
|
||||
// get subs from api
|
||||
let subscriptions = subscriptions_api.getAllSubscriptions();
|
||||
|
||||
res.send({
|
||||
subscriptions: subscriptions
|
||||
});
|
||||
});
|
||||
|
||||
app.post('/api/createPlaylist', async (req, res) => {
|
||||
let playlistName = req.body.playlistName;
|
||||
let fileNames = req.body.fileNames;
|
||||
let type = req.body.type;
|
||||
@@ -805,7 +1038,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 fileNames = req.body.fileNames;
|
||||
let type = req.body.type;
|
||||
@@ -831,7 +1064,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 type = req.body.type;
|
||||
|
||||
@@ -853,7 +1086,7 @@ app.post('/deletePlaylist', async (req, res) => {
|
||||
});
|
||||
|
||||
// deletes mp3 file
|
||||
app.post('/deleteMp3', async (req, res) => {
|
||||
app.post('/api/deleteMp3', async (req, res) => {
|
||||
var name = req.body.name;
|
||||
var fullpath = audioFolderPath + name + ".mp3";
|
||||
var wasDeleted = false;
|
||||
@@ -873,7 +1106,7 @@ app.post('/deleteMp3', async (req, res) => {
|
||||
});
|
||||
|
||||
// deletes mp4 file
|
||||
app.post('/deleteMp4', async (req, res) => {
|
||||
app.post('/api/deleteMp4', async (req, res) => {
|
||||
var name = req.body.name;
|
||||
var fullpath = videoFolderPath + name + ".mp4";
|
||||
var wasDeleted = false;
|
||||
@@ -892,14 +1125,14 @@ 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 is_playlist = req.body.is_playlist;
|
||||
let type = req.body.type;
|
||||
let outputName = req.body.outputName;
|
||||
let file = null;
|
||||
if (!is_playlist) {
|
||||
fileNames = decodeURI(fileNames);
|
||||
fileNames = decodeURIComponent(fileNames);
|
||||
if (type === 'audio') {
|
||||
file = __dirname + '/' + audioFolderPath + fileNames + '.mp3';
|
||||
} else if (type === 'video') {
|
||||
@@ -907,7 +1140,7 @@ app.post('/downloadFile', async (req, res) => {
|
||||
}
|
||||
} else {
|
||||
for (let i = 0; i < fileNames.length; i++) {
|
||||
fileNames[i] = decodeURI(fileNames[i]);
|
||||
fileNames[i] = decodeURIComponent(fileNames[i]);
|
||||
}
|
||||
file = await createPlaylistZipFile(fileNames, type, outputName);
|
||||
}
|
||||
@@ -915,7 +1148,7 @@ app.post('/downloadFile', async (req, res) => {
|
||||
res.sendFile(file);
|
||||
});
|
||||
|
||||
app.post('/deleteFile', async (req, res) => {
|
||||
app.post('/api/deleteFile', async (req, res) => {
|
||||
let fileName = req.body.fileName;
|
||||
let type = req.body.type;
|
||||
if (type === 'audio') {
|
||||
@@ -926,10 +1159,31 @@ app.post('/deleteFile', async (req, res) => {
|
||||
res.send()
|
||||
});
|
||||
|
||||
app.get('/video/:id', function(req , res){
|
||||
app.post('/api/downloadArchive', async (req, res) => {
|
||||
let sub = req.body.sub;
|
||||
let archive_dir = sub.archive;
|
||||
|
||||
let full_archive_path = path.join(__dirname, archive_dir, 'archive.txt');
|
||||
|
||||
if (fs.existsSync(full_archive_path)) {
|
||||
res.sendFile(full_archive_path);
|
||||
} else {
|
||||
res.sendStatus(404);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
app.get('/api/video/:id', function(req , res){
|
||||
var head;
|
||||
let id = decodeURI(req.params.id);
|
||||
const path = "video/" + id + '.mp4';
|
||||
let optionalParams = url_api.parse(req.url,true).query;
|
||||
let id = decodeURIComponent(req.params.id);
|
||||
let path = "video/" + id + '.mp4';
|
||||
if (optionalParams['subName']) {
|
||||
let basePath = config_api.getConfigItem('ytdl_subscriptions_base_path');
|
||||
const isPlaylist = optionalParams['subPlaylist'];
|
||||
basePath += (isPlaylist === 'true' ? 'playlists/' : 'channels/');
|
||||
path = basePath + optionalParams['subName'] + '/' + id + '.mp4';
|
||||
}
|
||||
const stat = fs.statSync(path)
|
||||
const fileSize = stat.size
|
||||
const range = req.headers.range
|
||||
@@ -966,9 +1220,9 @@ app.get('/video/:id', function(req , res){
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/audio/:id', function(req , res){
|
||||
app.get('/api/audio/:id', function(req , res){
|
||||
var head;
|
||||
let id = decodeURI(req.params.id);
|
||||
let id = decodeURIComponent(req.params.id);
|
||||
let path = "audio/" + id + '.mp3';
|
||||
path = path.replace(/\"/g, '\'');
|
||||
const stat = fs.statSync(path)
|
||||
@@ -1008,7 +1262,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 urlMode = !!req.body.urlMode;
|
||||
let type = req.body.type;
|
||||
@@ -1027,3 +1281,22 @@ app.get('/audio/:id', function(req , res){
|
||||
success: !!result
|
||||
})
|
||||
});
|
||||
|
||||
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);
|
||||
|
||||
});
|
||||
|
||||
app.use(express.static('./public'));
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
const fs = require('fs');
|
||||
|
||||
let CONFIG_ITEMS = require('./consts.js')['CONFIG_ITEMS'];
|
||||
const debugMode = process.env.YTDL_MODE === 'debug';
|
||||
|
||||
let configPath = 'config/default.json';
|
||||
let configPath = debugMode ? '../src/assets/default.json' : 'config/default.json';
|
||||
|
||||
// https://stackoverflow.com/questions/6491463/accessing-nested-javascript-objects-with-string-key
|
||||
Object.byString = function(o, s) {
|
||||
@@ -56,7 +57,10 @@ function setConfigFile(config) {
|
||||
|
||||
function getConfigItem(key) {
|
||||
let config_json = getConfigFile();
|
||||
if (!CONFIG_ITEMS[key]) console.log('cannot find config with key ' + key);
|
||||
if (!CONFIG_ITEMS[key]) {
|
||||
console.log('cannot find config with key ' + key);
|
||||
return null;
|
||||
}
|
||||
let path = CONFIG_ITEMS[key]['path'];
|
||||
return Object.byString(config_json, path);
|
||||
};
|
||||
@@ -108,5 +112,7 @@ module.exports = {
|
||||
getConfigItem: getConfigItem,
|
||||
setConfigItem: setConfigItem,
|
||||
setConfigItems: setConfigItems,
|
||||
getConfigFile: getConfigFile,
|
||||
setConfigFile: setConfigFile,
|
||||
CONFIG_ITEMS: CONFIG_ITEMS
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"YoutubeDLMaterial": {
|
||||
"Host": {
|
||||
"frontendurl": "http://example.com",
|
||||
"backendurl": "http://example.com:17442/"
|
||||
"url": "http://example.com",
|
||||
"port": "17442"
|
||||
},
|
||||
"Encryption": {
|
||||
"use-encryption": false,
|
||||
@@ -10,9 +10,9 @@
|
||||
"key-file-path": "/etc/letsencrypt/live/example.com/privkey.pem"
|
||||
},
|
||||
"Downloader": {
|
||||
"path-base": "http://example.com:17442/",
|
||||
"path-audio": "audio/",
|
||||
"path-video": "video/"
|
||||
"path-video": "video/",
|
||||
"custom_args": ""
|
||||
},
|
||||
"Extra": {
|
||||
"title_top": "Youtube Downloader",
|
||||
@@ -29,6 +29,12 @@
|
||||
"default_theme": "default",
|
||||
"allow_theme_change": true
|
||||
},
|
||||
"Subscriptions": {
|
||||
"allow_subscriptions": true,
|
||||
"subscriptions_base_path": "subscriptions/",
|
||||
"subscriptions_check_interval": "300",
|
||||
"subscriptions_use_youtubedl_archive": true
|
||||
},
|
||||
"Advanced": {
|
||||
"use_default_downloading_agent": true,
|
||||
"custom_downloading_agent": "",
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
{
|
||||
"YoutubeDLMaterial": {
|
||||
"Host": {
|
||||
"frontendurl": "https://example.com",
|
||||
"backendurl": "https://example.com:17442/"
|
||||
},
|
||||
"Host": {
|
||||
"url": "https://example.com",
|
||||
"port": "17442"
|
||||
},
|
||||
"Encryption": {
|
||||
"use-encryption": true,
|
||||
"cert-file-path": "/etc/letsencrypt/live/example.com/fullchain.pem",
|
||||
"key-file-path": "/etc/letsencrypt/live/example.com/privkey.pem"
|
||||
},
|
||||
"Downloader": {
|
||||
"path-base": "https://example.com:17442/",
|
||||
"path-audio": "audio/",
|
||||
"path-video": "video/"
|
||||
"path-video": "video/",
|
||||
"custom_args": ""
|
||||
},
|
||||
"Extra": {
|
||||
"title_top": "Youtube Downloader",
|
||||
@@ -29,6 +29,12 @@
|
||||
"default_theme": "default",
|
||||
"allow_theme_change": true
|
||||
},
|
||||
"Subscriptions": {
|
||||
"allow_subscriptions": true,
|
||||
"subscriptions_base_path": "subscriptions/",
|
||||
"subscriptions_check_interval": "300",
|
||||
"subscriptions_use_youtubedl_archive": true
|
||||
},
|
||||
"Advanced": {
|
||||
"use_default_downloading_agent": true,
|
||||
"custom_downloading_agent": "",
|
||||
|
||||
@@ -2,13 +2,13 @@ var config = require('config');
|
||||
|
||||
let CONFIG_ITEMS = {
|
||||
// Host
|
||||
'ytdl_frontend_url': {
|
||||
'key': 'ytdl_frontend_url',
|
||||
'path': 'YoutubeDLMaterial.Host.frontendurl'
|
||||
'ytdl_url': {
|
||||
'key': 'ytdl_url',
|
||||
'path': 'YoutubeDLMaterial.Host.url'
|
||||
},
|
||||
'ytdl_backend_url': {
|
||||
'key': 'ytdl_backend_url',
|
||||
'path': 'YoutubeDLMaterial.Host.backendurl'
|
||||
'ytdl_port': {
|
||||
'key': 'ytdl_port',
|
||||
'path': 'YoutubeDLMaterial.Host.port'
|
||||
},
|
||||
|
||||
// Encryption
|
||||
@@ -26,10 +26,6 @@ let CONFIG_ITEMS = {
|
||||
},
|
||||
|
||||
// Downloader
|
||||
'ytdl_base_path': {
|
||||
'key': 'ytdl_base_path',
|
||||
'path': 'YoutubeDLMaterial.Downloader.path-base'
|
||||
},
|
||||
'ytdl_audio_folder_path': {
|
||||
'key': 'ytdl_audio_folder_path',
|
||||
'path': 'YoutubeDLMaterial.Downloader.path-audio'
|
||||
@@ -38,6 +34,10 @@ let CONFIG_ITEMS = {
|
||||
'key': 'ytdl_video_folder_path',
|
||||
'path': 'YoutubeDLMaterial.Downloader.path-video'
|
||||
},
|
||||
'ytdl_custom_args': {
|
||||
'key': 'ytdl_custom_args',
|
||||
'path': 'YoutubeDLMaterial.Downloader.custom_args'
|
||||
},
|
||||
|
||||
// Extra
|
||||
'ytdl_title_top': {
|
||||
@@ -60,7 +60,6 @@ let CONFIG_ITEMS = {
|
||||
'key': 'ytdl_allow_multi_download_mode',
|
||||
'path': 'YoutubeDLMaterial.Extra.allow_multi_download_mode'
|
||||
},
|
||||
|
||||
|
||||
// API
|
||||
'ytdl_use_youtube_api': {
|
||||
@@ -82,6 +81,28 @@ let CONFIG_ITEMS = {
|
||||
'path': 'YoutubeDLMaterial.Themes.allow_theme_change'
|
||||
},
|
||||
|
||||
// Subscriptions
|
||||
'ytdl_allow_subscriptions': {
|
||||
'key': 'ytdl_allow_subscriptions',
|
||||
'path': 'YoutubeDLMaterial.Subscriptions.allow_subscriptions'
|
||||
},
|
||||
'ytdl_subscriptions_base_path': {
|
||||
'key': 'ytdl_subscriptions_base_path',
|
||||
'path': 'YoutubeDLMaterial.Subscriptions.subscriptions_base_path'
|
||||
},
|
||||
'ytdl_subscriptions_check_interval': {
|
||||
'key': 'ytdl_subscriptions_check_interval',
|
||||
'path': 'YoutubeDLMaterial.Subscriptions.subscriptions_check_interval'
|
||||
},
|
||||
'ytdl_subscriptions_check_interval': {
|
||||
'key': 'ytdl_subscriptions_check_interval',
|
||||
'path': 'YoutubeDLMaterial.Subscriptions.subscriptions_check_interval'
|
||||
},
|
||||
'ytdl_subscriptions_use_youtubedl_archive': {
|
||||
'key': 'ytdl_use_youtubedl_archive',
|
||||
'path': 'YoutubeDLMaterial.Subscriptions.subscriptions_use_youtubedl_archive'
|
||||
},
|
||||
|
||||
// Advanced
|
||||
'ytdl_use_default_downloading_agent': {
|
||||
'key': 'ytdl_use_default_downloading_agent',
|
||||
|
||||
@@ -20,11 +20,13 @@
|
||||
"dependencies": {
|
||||
"archiver": "^3.1.1",
|
||||
"async": "^3.1.0",
|
||||
"compression": "^1.7.4",
|
||||
"config": "^3.2.3",
|
||||
"exe": "^1.0.2",
|
||||
"express": "^4.17.1",
|
||||
"lowdb": "^1.0.0",
|
||||
"shortid": "^2.2.15",
|
||||
"uuidv4": "^6.0.6",
|
||||
"youtube-dl": "^3.0.2"
|
||||
}
|
||||
}
|
||||
|
||||
315
backend/subscriptions.js
Normal file
315
backend/subscriptions.js
Normal file
@@ -0,0 +1,315 @@
|
||||
const low = require('lowdb')
|
||||
const FileSync = require('lowdb/adapters/FileSync')
|
||||
|
||||
var fs = require('fs');
|
||||
const { uuid } = require('uuidv4');
|
||||
var path = require('path');
|
||||
|
||||
var youtubedl = require('youtube-dl');
|
||||
const config_api = require('./config');
|
||||
|
||||
const adapter = new FileSync('db.json');
|
||||
const db = low(adapter)
|
||||
|
||||
const debugMode = process.env.YTDL_MODE === 'debug';
|
||||
|
||||
async function subscribe(sub) {
|
||||
const result_obj = {
|
||||
success: false,
|
||||
error: ''
|
||||
};
|
||||
return new Promise(async resolve => {
|
||||
// sub should just have url and name. here we will get isPlaylist and path
|
||||
sub.isPlaylist = sub.url.includes('playlist');
|
||||
|
||||
if (db.get('subscriptions').find({url: sub.url}).value()) {
|
||||
console.log('Sub already exists');
|
||||
result_obj.error = 'Subcription with URL ' + sub.url + ' already exists!';
|
||||
resolve(result_obj);
|
||||
return;
|
||||
}
|
||||
|
||||
// add sub to db
|
||||
db.get('subscriptions').push(sub).write();
|
||||
|
||||
let success = await getSubscriptionInfo(sub);
|
||||
result_obj.success = success;
|
||||
result_obj.sub = sub;
|
||||
getVideosForSub(sub);
|
||||
resolve(result_obj);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
async function getSubscriptionInfo(sub) {
|
||||
const basePath = config_api.getConfigItem('ytdl_subscriptions_base_path');
|
||||
return new Promise(resolve => {
|
||||
// get videos
|
||||
let downloadConfig = ['--dump-json', '--playlist-end', '1']
|
||||
youtubedl.exec(sub.url, downloadConfig, {}, function(err, output) {
|
||||
if (debugMode) {
|
||||
console.log('Subscribe: got info for subscription ' + sub.id);
|
||||
}
|
||||
if (err) {
|
||||
console.log(err.stderr);
|
||||
resolve(false);
|
||||
} else if (output) {
|
||||
if (output.length === 0 || (output.length === 1 && output[0] === '')) {
|
||||
if (debugMode) console.log('Could not get info for ' + sub.id);
|
||||
resolve(false);
|
||||
}
|
||||
for (let i = 0; i < output.length; i++) {
|
||||
let output_json = null;
|
||||
try {
|
||||
output_json = JSON.parse(output[i]);
|
||||
} catch(e) {
|
||||
output_json = null;
|
||||
}
|
||||
if (!output_json) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!sub.name) {
|
||||
sub.name = sub.isPlaylist ? output_json.playlist_title : output_json.uploader;
|
||||
// if it's now valid, update
|
||||
if (sub.name) {
|
||||
db.get('subscriptions').find({id: sub.id}).assign({name: sub.name}).write();
|
||||
}
|
||||
}
|
||||
|
||||
if (!sub.archive) {
|
||||
// must create the archive
|
||||
const archive_dir = basePath + 'archives/' + sub.name;
|
||||
const archive_path = path.join(archive_dir, 'archive.txt');
|
||||
|
||||
// creates archive directory and text file if it doesn't exist
|
||||
if (!fs.existsSync(archive_dir)) {
|
||||
fs.mkdirSync(archive_dir);
|
||||
fs.closeSync(fs.openSync(archive_path, 'w'));
|
||||
} else if (!fs.existsSync(archive_path)) {
|
||||
fs.closeSync(fs.openSync(archive_path, 'w'));
|
||||
}
|
||||
|
||||
// updates subscription
|
||||
sub.archive = archive_dir;
|
||||
db.get('subscriptions').find({id: sub.id}).assign({archive: archive_dir}).write();
|
||||
}
|
||||
|
||||
// TODO: get even more info
|
||||
|
||||
resolve(true);
|
||||
}
|
||||
resolve(false);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function unsubscribe(sub, deleteMode) {
|
||||
return new Promise(async resolve => {
|
||||
const basePath = config_api.getConfigItem('ytdl_subscriptions_base_path');
|
||||
let result_obj = { success: false, error: '' };
|
||||
|
||||
let id = sub.id;
|
||||
db.get('subscriptions').remove({id: id}).write();
|
||||
|
||||
const appendedBasePath = getAppendedBasePath(sub, basePath);
|
||||
if (deleteMode && fs.existsSync(appendedBasePath)) {
|
||||
if (sub.archive && fs.existsSync(sub.archive)) {
|
||||
const archive_file_path = path.join(sub.archive, 'archive.txt');
|
||||
// deletes archive if it exists
|
||||
if (fs.existsSync(archive_file_path)) {
|
||||
fs.unlinkSync(archive_file_path);
|
||||
}
|
||||
fs.rmdirSync(sub.archive);
|
||||
}
|
||||
deleteFolderRecursive(appendedBasePath);
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
async function deleteSubscriptionFile(sub, file, deleteForever) {
|
||||
const basePath = config_api.getConfigItem('ytdl_subscriptions_base_path');
|
||||
const useArchive = config_api.getConfigItem('ytdl_subscriptions_use_youtubedl_archive');
|
||||
const appendedBasePath = getAppendedBasePath(sub, basePath);
|
||||
const name = file;
|
||||
let retrievedID = null;
|
||||
return new Promise(resolve => {
|
||||
let filePath = appendedBasePath;
|
||||
var jsonPath = path.join(__dirname,filePath,name+'.info.json');
|
||||
var videoFilePath = path.join(__dirname,filePath,name+'.mp4');
|
||||
var imageFilePath = path.join(__dirname,filePath,name+'.jpg');
|
||||
|
||||
jsonExists = fs.existsSync(jsonPath);
|
||||
videoFileExists = fs.existsSync(videoFilePath);
|
||||
imageFileExists = fs.existsSync(imageFilePath);
|
||||
|
||||
if (jsonExists) {
|
||||
retrievedID = JSON.parse(fs.readFileSync(jsonPath, 'utf8'))['id'];
|
||||
fs.unlinkSync(jsonPath);
|
||||
}
|
||||
|
||||
if (imageFileExists) {
|
||||
fs.unlinkSync(imageFilePath);
|
||||
}
|
||||
|
||||
if (videoFileExists) {
|
||||
fs.unlink(videoFilePath, function(err) {
|
||||
if (fs.existsSync(jsonPath) || fs.existsSync(videoFilePath)) {
|
||||
resolve(false);
|
||||
} else {
|
||||
// check if the user wants the video to be redownloaded (deleteForever === false)
|
||||
if (!deleteForever && useArchive && sub.archive && retrievedID) {
|
||||
const archive_path = path.join(sub.archive, 'archive.txt')
|
||||
// if archive exists, remove line with video ID
|
||||
if (fs.existsSync(archive_path)) {
|
||||
removeIDFromArchive(archive_path, retrievedID);
|
||||
}
|
||||
}
|
||||
resolve(true);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// TODO: tell user that the file didn't exist
|
||||
resolve(true);
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
async function getVideosForSub(sub) {
|
||||
return new Promise(resolve => {
|
||||
if (!subExists(sub.id)) {
|
||||
resolve(false);
|
||||
return;
|
||||
}
|
||||
const basePath = config_api.getConfigItem('ytdl_subscriptions_base_path');
|
||||
const useArchive = config_api.getConfigItem('ytdl_subscriptions_use_youtubedl_archive');
|
||||
|
||||
let appendedBasePath = null
|
||||
if (sub.name) {
|
||||
appendedBasePath = getAppendedBasePath(sub, basePath);
|
||||
} else {
|
||||
appendedBasePath = basePath + (sub.isPlaylist ? 'playlists/%(playlist_title)s' : 'channels/%(uploader)s');
|
||||
}
|
||||
|
||||
let downloadConfig = ['-o', appendedBasePath + '/%(title)s.mp4', '-f', 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/mp4', '-ciw', '--write-annotations', '--write-thumbnail', '--write-info-json', '--print-json'];
|
||||
|
||||
if (sub.timerange) {
|
||||
downloadConfig.push('--dateafter', sub.timerange);
|
||||
}
|
||||
|
||||
let archive_dir = null;
|
||||
let archive_path = null;
|
||||
|
||||
if (useArchive) {
|
||||
if (sub.archive) {
|
||||
archive_dir = sub.archive;
|
||||
archive_path = path.join(archive_dir, 'archive.txt')
|
||||
}
|
||||
downloadConfig.push('--download-archive', archive_path);
|
||||
}
|
||||
|
||||
// get videos
|
||||
youtubedl.exec(sub.url, downloadConfig, {}, function(err, output) {
|
||||
if (debugMode) {
|
||||
console.log('Subscribe: got videos for subscription ' + sub.name);
|
||||
}
|
||||
if (err) {
|
||||
console.log(err.stderr);
|
||||
resolve(false);
|
||||
} else if (output) {
|
||||
if (output.length === 0 || (output.length === 1 && output[0] === '')) {
|
||||
if (debugMode) console.log('No additional videos to download for ' + sub.name);
|
||||
resolve(true);
|
||||
}
|
||||
for (let i = 0; i < output.length; i++) {
|
||||
let output_json = null;
|
||||
try {
|
||||
output_json = JSON.parse(output[i]);
|
||||
} catch(e) {
|
||||
output_json = null;
|
||||
}
|
||||
if (!output_json) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// TODO: Potentially store downloaded files in db?
|
||||
|
||||
}
|
||||
resolve(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getAllSubscriptions() {
|
||||
const subscriptions = db.get('subscriptions').value();
|
||||
return subscriptions;
|
||||
}
|
||||
|
||||
function getSubscription(subID) {
|
||||
return db.get('subscriptions').find({id: subID}).value();
|
||||
}
|
||||
|
||||
function subExists(subID) {
|
||||
return !!db.get('subscriptions').find({id: subID}).value();
|
||||
}
|
||||
|
||||
// helper functions
|
||||
|
||||
function getAppendedBasePath(sub, base_path) {
|
||||
return base_path + (sub.isPlaylist ? 'playlists/' : 'channels/') + sub.name;
|
||||
}
|
||||
|
||||
// https://stackoverflow.com/a/32197381/8088021
|
||||
const deleteFolderRecursive = function(folder_to_delete) {
|
||||
if (fs.existsSync(folder_to_delete)) {
|
||||
fs.readdirSync(folder_to_delete).forEach((file, index) => {
|
||||
const curPath = path.join(folder_to_delete, file);
|
||||
if (fs.lstatSync(curPath).isDirectory()) { // recurse
|
||||
deleteFolderRecursive(curPath);
|
||||
} else { // delete file
|
||||
fs.unlinkSync(curPath);
|
||||
}
|
||||
});
|
||||
fs.rmdirSync(folder_to_delete);
|
||||
}
|
||||
};
|
||||
|
||||
function removeIDFromArchive(archive_path, id) {
|
||||
fs.readFile(archive_path, {encoding: 'utf-8'}, function(err, data) {
|
||||
if (err) throw error;
|
||||
|
||||
let dataArray = data.split('\n'); // convert file data in an array
|
||||
const searchKeyword = id; // we are looking for a line, contains, key word id in the file
|
||||
let lastIndex = -1; // let say, we have not found the keyword
|
||||
|
||||
for (let index=0; index<dataArray.length; index++) {
|
||||
if (dataArray[index].includes(searchKeyword)) { // check if a line contains the id keyword
|
||||
lastIndex = index; // found a line includes a id keyword
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
dataArray.splice(lastIndex, 1); // remove the keyword id from the data Array
|
||||
|
||||
// UPDATE FILE WITH NEW DATA
|
||||
const updatedData = dataArray.join('\n');
|
||||
fs.writeFile(archive_path, updatedData, (err) => {
|
||||
if (err) throw err;
|
||||
// console.log ('Successfully updated the file data');
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getSubscription : getSubscription,
|
||||
getAllSubscriptions : getAllSubscriptions,
|
||||
subscribe : subscribe,
|
||||
unsubscribe : unsubscribe,
|
||||
deleteSubscriptionFile : deleteSubscriptionFile,
|
||||
getVideosForSub : getVideosForSub
|
||||
}
|
||||
Binary file not shown.
28
chrome-extension.pem
Normal file
28
chrome-extension.pem
Normal file
@@ -0,0 +1,28 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDMX9Wk5SM5cIfY
|
||||
6ReKX3ybY1rsbNbOzG8ceN7yyeXB0mor8pVsX1MOna2HewOyBuaaYNJRO4tJBxic
|
||||
7a8zQErfgHL/i/QrVvVCpfJ7xKvq6zij5NYoqd/FBUwawqjeH5/voIcAp9z5Vmsr
|
||||
kL0sxJUKy6b4IWNp3noU7Nvq2RwxnXQbKDhz8FrX6oQAnDC6gsG5a2OSPsaE4oqw
|
||||
6nmonORJypmpP5hqyHY8ffXBT2lAxjHT7OnYbaCBe2TQP8+rH6rDBOhjVNtUJ089
|
||||
ocTQL6LtQEPkcF4yKJmtcOwHl8OPGZs5l9i8xb4j9RuSPkm2lbzZX8sOsdGGoqJZ
|
||||
q68nYhsHAgMBAAECggEAXmtKEzfPObq88B/kAcgSk+FngMHZzcmR7bgD3GwdSxnQ
|
||||
dkRI9zvk7eQ35tcUwntAr4Lat6/ILjFqlBmVLxrdXHuF5Xz9jcZLYgKzz61xdYM9
|
||||
dC6FKF0u5eGIIvbauGAo7jaeGFX1F3Zu5b4lP9kEOGwU1B7sxF0FzsQM5+dtCJgv
|
||||
We/hWQeF+9gtoVnkCSS/Mq2p0UomXXHW0Bz4+HuHlTR9aiYbviYnotABiLUhZyzt
|
||||
v5yUaktb9qniBfdLpRlq8cp06xYlTEA9gJpa4Pnok8OWUsbAiW6EiXUSaZ/cchVa
|
||||
AnO8WWYvVOnnt6WHI3+QdFTnqVjE5TBX4N/7bVhHGQKBgQD0dtbFqp7vZK/jVYvE
|
||||
z0WPdySOg2ZDmoSfk5ZlR1+Y9zWToHv0qu8zqoOjL8Ubxrh9fGlOow+cCVdkEuaa
|
||||
jWC2AWetuRvW0Z5A3XMXr0/N/1fgOkTqtp3WNrUPjVJahEg3lN+90opgFoT8swSi
|
||||
s1oxW0oLcVIlrjhGBXAPCfsAuQKBgQDWBLRhHsRAvGcK5wGuVnxVApTIyBOermsW
|
||||
3bJt+7+UI+4sYrBAwkWdQG93IG0cQtn48TEPBgmR2fjRF5IFT9M4/u+QOeeByT7I
|
||||
we7nVtHgSY5ByC9N0mjWbcmSg8fktz/LonjldNC4kWdOFb75fxGf8kOGS5rUaMA4
|
||||
zHucfB6ZvwKBgQCPHJrysMXGY21MaqIeHzEboaX3ABl37hdBzAa5V6UxSVdGCydF
|
||||
vmO2HVZey/JaJmWOoKyNaowSzq0oWqBBTg6VvhDR9JHFmoVId9uOvAS+FYN+Mt5x
|
||||
gWK5KuGoLxVNBC+6yh6JY526TrSfsrU+Aj0Es+qO9FIg2PL8muZVB4S3kQKBgH/5
|
||||
CDMaxpc/EQ5/2413wZjDllwI51J3USm3Hz6Mzp2ybnSz/lh60k2Zfg1polTH1Lb6
|
||||
4i7tmUNRZ2sAARyUAuWN64n+VeRRhe1dqZFDZPQMh7fmEAMk0fOGaoXlrt2ghdEq
|
||||
Mchi9Xun1nHmpu9hgBR4NNBU3RwuFuLfwvprbZDZAoGAWa62QJChE86xQGP1MrL2
|
||||
SbIzw3cfeP5xdQ3MKldJiy5IkbMR7Z13WZ7FwvPTy0g/onLHD1rqlm1kUMsGRHpD
|
||||
5vH06PNpKXQ6x8BYaRGtE6P39jLycO/X+WK/lYTrWo1bR+mGCebDh4B5XrwT3gI6
|
||||
x4Gvz134pZCTyQCf5JCwbQs=
|
||||
-----END PRIVATE KEY-----
|
||||
20
chrome-extension/background.js
Normal file
20
chrome-extension/background.js
Normal file
@@ -0,0 +1,20 @@
|
||||
// background.js
|
||||
|
||||
// Called when the user clicks on the browser action.
|
||||
chrome.browserAction.onClicked.addListener(function(tab) {
|
||||
// get the frontend_url
|
||||
chrome.storage.sync.get({
|
||||
frontend_url: 'http://localhost',
|
||||
audio_only: false
|
||||
}, function(items) {
|
||||
chrome.tabs.query({active: true, currentWindow: true}, function(tabs) {
|
||||
var activeTab = tabs[0];
|
||||
var url = activeTab.url;
|
||||
if (url.includes('youtube.com')) {
|
||||
var new_url = items.frontend_url + '/#/home;url=' + encodeURIComponent(url) + ';audioOnly=' + items.audio_only;
|
||||
chrome.tabs.create({ url: new_url });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
BIN
chrome-extension/favicon.png
Normal file
BIN
chrome-extension/favicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.5 KiB |
20
chrome-extension/manifest.json
Normal file
20
chrome-extension/manifest.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"manifest_version": 2,
|
||||
"name": "YoutubeDL-Material",
|
||||
"version": "0.1",
|
||||
"description": "The official chrome extension of YoutubeDL-Material, an open-source and self-hosted YouTube downloader.",
|
||||
"background": {
|
||||
"scripts": ["background.js"]
|
||||
},
|
||||
"browser_action": {
|
||||
"default_icon": "favicon.png"
|
||||
},
|
||||
"permissions": [
|
||||
"tabs",
|
||||
"storage"
|
||||
],
|
||||
"options_ui": {
|
||||
"page": "options.html",
|
||||
"open_in_tab": false
|
||||
}
|
||||
}
|
||||
29
chrome-extension/options.html
Normal file
29
chrome-extension/options.html
Normal file
@@ -0,0 +1,29 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head><title>YoutubeDL-Material Extension Options</title></head>
|
||||
<body>
|
||||
|
||||
<h2>Settings</h2>
|
||||
|
||||
<div>
|
||||
<h4>Frontend URL</h4>
|
||||
<input placeholder="Frontend URL" type="text" id="frontend_url">
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
|
||||
<div>
|
||||
<label>
|
||||
<input type="checkbox" id="audio_only">
|
||||
Audio only
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
|
||||
<div id="status"></div>
|
||||
<button id="save">Save</button>
|
||||
|
||||
<script src="options.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
31
chrome-extension/options.js
Normal file
31
chrome-extension/options.js
Normal file
@@ -0,0 +1,31 @@
|
||||
// Saves options to chrome.storage
|
||||
function save_options() {
|
||||
var frontend_url = document.getElementById('frontend_url').value;
|
||||
var audio_only = document.getElementById('audio_only').checked;
|
||||
chrome.storage.sync.set({
|
||||
frontend_url: frontend_url,
|
||||
audio_only: audio_only
|
||||
}, function() {
|
||||
// Update status to let user know options were saved.
|
||||
var status = document.getElementById('status');
|
||||
status.textContent = 'Options saved.';
|
||||
setTimeout(function() {
|
||||
status.textContent = '';
|
||||
}, 750);
|
||||
});
|
||||
}
|
||||
|
||||
// Restores select box and checkbox state using the preferences
|
||||
// stored in chrome.storage.
|
||||
function restore_options() {
|
||||
chrome.storage.sync.get({
|
||||
frontend_url: 'http://localhost',
|
||||
audio_only: false
|
||||
}, function(items) {
|
||||
document.getElementById('frontend_url').value = items.frontend_url;
|
||||
document.getElementById('audio_only').checked = items.audio_only;
|
||||
});
|
||||
}
|
||||
document.addEventListener('DOMContentLoaded', restore_options);
|
||||
document.getElementById('save').addEventListener('click',
|
||||
save_options);
|
||||
BIN
chrome-extension/youtubedl-material-chrome-extension.zip
Normal file
BIN
chrome-extension/youtubedl-material-chrome-extension.zip
Normal file
Binary file not shown.
@@ -5,23 +5,27 @@ services:
|
||||
build: .
|
||||
environment:
|
||||
# config items
|
||||
ytdl_frontend_url: http://localhost:8998
|
||||
ytdl_backend_url: http://localhost:17442/
|
||||
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_base_path: http://localhost:17442/
|
||||
ytdl_audio_folder_path: audio/
|
||||
ytdl_video_folder_path: video/
|
||||
ytdl_custom_args: ''
|
||||
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_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_allow_subscriptions: 'true'
|
||||
ytdl_subscriptions_base_path: subscriptions/
|
||||
ytdl_subscriptions_check_interval: '300'
|
||||
ytdl_subscriptions_use_youtubedl_archive: 'true'
|
||||
ytdl_use_default_downloading_agent: 'true'
|
||||
ytdl_custom_downloading_agent: 'false'
|
||||
ytdl_allow_advanced_download: 'false'
|
||||
@@ -30,6 +34,5 @@ services:
|
||||
ALLOW_CONFIG_MUTATIONS: 'true'
|
||||
restart: always
|
||||
ports:
|
||||
- "17442:17442"
|
||||
- "8998:80"
|
||||
image: tzahi12345/youtubedl-material:3.1
|
||||
- "8998:17442"
|
||||
image: tzahi12345/youtubedl-material:3.4
|
||||
@@ -1,39 +0,0 @@
|
||||
#!/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
|
||||
@@ -31,16 +31,17 @@
|
||||
"@angular/router": "^8.2.11",
|
||||
"core-js": "^2.4.1",
|
||||
"file-saver": "^2.0.2",
|
||||
"hammerjs": "^2.0.8",
|
||||
"ng-lazyload-image": "^7.0.1",
|
||||
"ng4-configure": "^0.1.7",
|
||||
"ngx-content-loading": "^0.1.3",
|
||||
"rxjs": "^6.5.3",
|
||||
"rxjs-compat": "^6.0.0-rc.0",
|
||||
"tslib": "^1.10.0",
|
||||
"typescript": "~3.5.3",
|
||||
"videogular2": "^7.0.1",
|
||||
"web-animations-js": "^2.3.2",
|
||||
"zone.js": "~0.9.1",
|
||||
"typescript": "~3.5.3"
|
||||
"zone.js": "~0.9.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-devkit/build-angular": "^0.803.24",
|
||||
|
||||
BIN
releases/3.3/youtubedl-material-3.3-docker.zip
Normal file
BIN
releases/3.3/youtubedl-material-3.3-docker.zip
Normal file
Binary file not shown.
BIN
releases/3.3/youtubedl-material-3.3.zip
Normal file
BIN
releases/3.3/youtubedl-material-3.3.zip
Normal file
Binary file not shown.
@@ -2,9 +2,13 @@ import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
import { MainComponent } from './main/main.component';
|
||||
import { PlayerComponent } from './player/player.component';
|
||||
import { SubscriptionsComponent } from './subscriptions/subscriptions.component';
|
||||
import { SubscriptionComponent } from './subscription/subscription/subscription.component';
|
||||
const routes: Routes = [
|
||||
{ path: 'home', component: MainComponent },
|
||||
{ path: 'player', component: PlayerComponent},
|
||||
{ path: 'subscriptions', component: SubscriptionsComponent },
|
||||
{ path: 'subscription', component: SubscriptionComponent },
|
||||
{ path: '', redirectTo: '/home', pathMatch: 'full' },
|
||||
];
|
||||
|
||||
|
||||
@@ -10,4 +10,10 @@
|
||||
flex-direction: column;
|
||||
flex-basis: 100%;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.theme-slide-toggle {
|
||||
top: 2px;
|
||||
left: 10px;
|
||||
position: relative;
|
||||
}
|
||||
@@ -1,17 +1,42 @@
|
||||
<div [style.background]="postsService.theme ? postsService.theme.background_color : null" style="width: 100%; min-height: 100%;">
|
||||
<mat-toolbar color="primary" class="top">
|
||||
<div class="flex-row" width="100%" height="100%">
|
||||
<div class="flex-column" style="text-align: left; margin-top: 1px;">
|
||||
<button (click)="goBack()" *ngIf="router.url.split(';')[0] === '/player'" mat-icon-button><mat-icon>arrow_back</mat-icon></button>
|
||||
<div [style.background]="postsService.theme ? postsService.theme.background_color : null" style="width: 100%; height: 100%;">
|
||||
<div>
|
||||
<mat-toolbar color="primary" class="top">
|
||||
<div class="flex-row" width="100%" height="100%">
|
||||
<div class="flex-column" style="text-align: left; margin-top: 1px;">
|
||||
<button #hamburgerMenu style="outline: none" *ngIf="router.url.split(';')[0] !== '/player' && allowSubscriptions" mat-icon-button aria-label="Toggle side navigation" (click)="toggleSidenav()"><mat-icon>menu</mat-icon></button>
|
||||
<button (click)="goBack()" *ngIf="router.url.split(';')[0] === '/player'" mat-icon-button><mat-icon>arrow_back</mat-icon></button>
|
||||
</div>
|
||||
<div class="flex-column" style="text-align: center; margin-top: 5px;">
|
||||
<div>{{topBarTitle}}</div>
|
||||
</div>
|
||||
<div class="flex-column" style="text-align: right; align-items: flex-end;">
|
||||
<button [matMenuTriggerFor]="menuSettings" mat-icon-button><mat-icon>more_vert</mat-icon></button>
|
||||
<mat-menu #menuSettings="matMenu">
|
||||
<button (click)="themeMenuItemClicked($event)" *ngIf="allowThemeChange" mat-menu-item>
|
||||
<mat-icon>{{(postsService.theme.key === 'default') ? 'brightness_5' : 'brightness_2'}}</mat-icon>
|
||||
<span>Dark</span>
|
||||
<mat-slide-toggle class="theme-slide-toggle" [checked]="postsService.theme.key === 'dark'"></mat-slide-toggle>
|
||||
</button>
|
||||
<button (click)="openSettingsDialog()" mat-menu-item>
|
||||
<mat-icon>settings</mat-icon>
|
||||
<span>Settings</span>
|
||||
</button>
|
||||
</mat-menu>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-column" style="text-align: center; margin-top: 5px;">
|
||||
<div>{{topBarTitle}}</div>
|
||||
</div>
|
||||
<div class="flex-column" style="text-align: right; align-items: flex-end;">
|
||||
<button *ngIf="allowThemeChange" mat-icon-button (click)="flipTheme()"><mat-icon>{{(postsService.theme.key === 'default') ? 'brightness_5' : 'brightness_2'}}</mat-icon></button>
|
||||
</div>
|
||||
</div>
|
||||
</mat-toolbar>
|
||||
|
||||
<router-outlet></router-outlet>
|
||||
</mat-toolbar>
|
||||
</div>
|
||||
<div style="height: calc(100% - 64px)">
|
||||
<mat-sidenav-container style="height: 100%">
|
||||
<mat-sidenav #sidenav>
|
||||
<mat-nav-list>
|
||||
<a mat-list-item (click)="sidenav.close()" routerLink='/home'>Home</a>
|
||||
<a mat-list-item (click)="sidenav.close()" routerLink='/subscriptions'>Subscriptions</a>
|
||||
</mat-nav-list>
|
||||
</mat-sidenav>
|
||||
<mat-sidenav-content [style.background]="postsService.theme ? postsService.theme.background_color : null">
|
||||
<router-outlet></router-outlet>
|
||||
</mat-sidenav-content>
|
||||
</mat-sidenav-container>
|
||||
</div>
|
||||
</div>
|
||||
@@ -4,7 +4,7 @@ import {FileCardComponent} from './file-card/file-card.component';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import {FormControl, Validators} from '@angular/forms';
|
||||
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
|
||||
import {MatSnackBar} from '@angular/material';
|
||||
import {MatSnackBar, MatDialog, MatSidenav} from '@angular/material';
|
||||
import { saveAs } from 'file-saver';
|
||||
import 'rxjs/add/observable/of';
|
||||
import 'rxjs/add/operator/mapTo';
|
||||
@@ -15,9 +15,10 @@ import 'rxjs/add/operator/debounceTime'
|
||||
import 'rxjs/add/operator/do'
|
||||
import 'rxjs/add/operator/switch'
|
||||
import { YoutubeSearchService, Result } from './youtube-search.service';
|
||||
import { Router } from '@angular/router';
|
||||
import { Router, NavigationStart, NavigationEnd } from '@angular/router';
|
||||
import { OverlayContainer } from '@angular/cdk/overlay';
|
||||
import { THEMES_CONFIG } from '../themes';
|
||||
import { SettingsComponent } from './settings/settings.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
@@ -33,18 +34,35 @@ export class AppComponent implements OnInit {
|
||||
topBarTitle = 'Youtube Downloader';
|
||||
defaultTheme = null;
|
||||
allowThemeChange = null;
|
||||
allowSubscriptions = false;
|
||||
|
||||
@ViewChild('urlinput', { read: ElementRef, static: false }) urlInput: ElementRef;
|
||||
@ViewChild('sidenav', {static: false}) sidenav: MatSidenav;
|
||||
@ViewChild('hamburgerMenu', {static: false, read: ElementRef}) hamburgerMenuButton: ElementRef;
|
||||
navigator: string = null;
|
||||
|
||||
constructor(public postsService: PostsService, public snackBar: MatSnackBar,
|
||||
constructor(public postsService: PostsService, public snackBar: MatSnackBar, private dialog: MatDialog,
|
||||
public router: Router, public overlayContainer: OverlayContainer, private elementRef: ElementRef) {
|
||||
|
||||
this.navigator = localStorage.getItem('player_navigator');
|
||||
// runs on navigate, captures the route that navigated to the player (if needed)
|
||||
this.router.events.subscribe((e) => { if (e instanceof NavigationStart) {
|
||||
this.navigator = localStorage.getItem('player_navigator');
|
||||
} else if (e instanceof NavigationEnd) {
|
||||
// blurs hamburger menu if it exists, as the sidenav likes to focus on it after closing
|
||||
if (this.hamburgerMenuButton && this.hamburgerMenuButton.nativeElement) {
|
||||
this.hamburgerMenuButton.nativeElement.blur();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 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'];
|
||||
const themingExists = result['YoutubeDLMaterial']['Themes'];
|
||||
this.defaultTheme = themingExists ? result['YoutubeDLMaterial']['Themes']['default_theme'] : 'default';
|
||||
this.allowThemeChange = themingExists ? result['YoutubeDLMaterial']['Themes']['allow_theme_change'] : true;
|
||||
this.allowSubscriptions = result['YoutubeDLMaterial']['Subscriptions']['allow_subscriptions'];
|
||||
|
||||
// sets theme to config default if it doesn't exist
|
||||
if (!localStorage.getItem('theme')) {
|
||||
@@ -56,6 +74,10 @@ export class AppComponent implements OnInit {
|
||||
|
||||
}
|
||||
|
||||
toggleSidenav() {
|
||||
this.sidenav.toggle();
|
||||
}
|
||||
|
||||
// theme stuff
|
||||
|
||||
setTheme(theme) {
|
||||
@@ -104,6 +126,11 @@ onSetTheme(theme, old_theme) {
|
||||
}
|
||||
}
|
||||
|
||||
themeMenuItemClicked(event) {
|
||||
this.flipTheme();
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
if (localStorage.getItem('theme')) {
|
||||
this.setTheme(localStorage.getItem('theme'));
|
||||
@@ -114,7 +141,18 @@ onSetTheme(theme, old_theme) {
|
||||
|
||||
|
||||
goBack() {
|
||||
this.router.navigate(['/home']);
|
||||
if (!this.navigator) {
|
||||
this.router.navigate(['/home']);
|
||||
} else {
|
||||
this.router.navigateByUrl(this.navigator);
|
||||
}
|
||||
}
|
||||
|
||||
openSettingsDialog() {
|
||||
const dialogRef = this.dialog.open(SettingsComponent, {
|
||||
width: '80vw'
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,10 @@ import {MatNativeDateModule, MatRadioModule, MatInputModule, MatButtonModule, Ma
|
||||
MatProgressBarModule, MatExpansionModule,
|
||||
MatProgressSpinnerModule,
|
||||
MatButtonToggleModule,
|
||||
MatDialogModule} from '@angular/material';
|
||||
MatDialogModule,
|
||||
MatRippleModule,
|
||||
MatSlideToggleModule,
|
||||
MatMenuModule} from '@angular/material';
|
||||
import {DragDropModule} from '@angular/cdk/drag-drop';
|
||||
import {FormsModule, ReactiveFormsModule} from '@angular/forms';
|
||||
import { AppComponent } from './app.component';
|
||||
@@ -28,6 +31,12 @@ import { NgxContentLoadingModule } from 'ngx-content-loading';
|
||||
import { audioFilesMouseHovering, videoFilesMouseHovering, audioFilesOpened, videoFilesOpened } from './main/main.component';
|
||||
import { CreatePlaylistComponent } from './create-playlist/create-playlist.component';
|
||||
import { DownloadItemComponent } from './download-item/download-item.component';
|
||||
import { SubscriptionsComponent } from './subscriptions/subscriptions.component';
|
||||
import { SubscribeDialogComponent } from './dialogs/subscribe-dialog/subscribe-dialog.component';
|
||||
import { SubscriptionComponent } from './subscription//subscription/subscription.component';
|
||||
import { SubscriptionFileCardComponent } from './subscription/subscription-file-card/subscription-file-card.component';
|
||||
import { SubscriptionInfoDialogComponent } from './dialogs/subscription-info-dialog/subscription-info-dialog.component';
|
||||
import { SettingsComponent } from './settings/settings.component';
|
||||
|
||||
export function isVisible({ event, element, scrollContainer, offset }: IsVisibleProps<any>) {
|
||||
return (element.id === 'video' ? videoFilesMouseHovering || videoFilesOpened : audioFilesMouseHovering || audioFilesOpened);
|
||||
@@ -41,7 +50,13 @@ export function isVisible({ event, element, scrollContainer, offset }: IsVisible
|
||||
PlayerComponent,
|
||||
InputDialogComponent,
|
||||
CreatePlaylistComponent,
|
||||
DownloadItemComponent
|
||||
DownloadItemComponent,
|
||||
SubscriptionsComponent,
|
||||
SubscribeDialogComponent,
|
||||
SubscriptionComponent,
|
||||
SubscriptionFileCardComponent,
|
||||
SubscriptionInfoDialogComponent,
|
||||
SettingsComponent
|
||||
],
|
||||
imports: [
|
||||
BrowserModule,
|
||||
@@ -67,7 +82,11 @@ export function isVisible({ event, element, scrollContainer, offset }: IsVisible
|
||||
MatProgressBarModule,
|
||||
MatProgressSpinnerModule,
|
||||
MatButtonToggleModule,
|
||||
MatRippleModule,
|
||||
MatMenuModule,
|
||||
MatDialogModule,
|
||||
MatSlideToggleModule,
|
||||
MatMenuModule,
|
||||
DragDropModule,
|
||||
VgCoreModule,
|
||||
VgControlsModule,
|
||||
@@ -80,7 +99,10 @@ export function isVisible({ event, element, scrollContainer, offset }: IsVisible
|
||||
],
|
||||
entryComponents: [
|
||||
InputDialogComponent,
|
||||
CreatePlaylistComponent
|
||||
CreatePlaylistComponent,
|
||||
SubscribeDialogComponent,
|
||||
SubscriptionInfoDialogComponent,
|
||||
SettingsComponent
|
||||
],
|
||||
providers: [PostsService],
|
||||
bootstrap: [AppComponent]
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
<h4 mat-dialog-title>Subscribe to playlist or channel</h4>
|
||||
|
||||
<mat-dialog-content>
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<mat-form-field color="accent">
|
||||
<input [(ngModel)]="url" matInput placeholder="URL" required aria-required="true">
|
||||
<mat-hint>The playlist or channel URL</mat-hint>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<mat-form-field color="accent">
|
||||
<input [(ngModel)]="name" matInput placeholder="Custom name">
|
||||
<mat-hint>This is optional</mat-hint>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="col-12 mt-3">
|
||||
<mat-checkbox [(ngModel)]="download_all">Download all uploads</mat-checkbox>
|
||||
</div>
|
||||
<div class="col-12" *ngIf="!download_all">
|
||||
Download videos uploaded in the last
|
||||
<mat-form-field color="accent" style="width: 50px; text-align: center">
|
||||
<input type="number" matInput [(ngModel)]="timerange_amount">
|
||||
</mat-form-field>
|
||||
<mat-select color="accent" class="unit-select" [(ngModel)]="timerange_unit">
|
||||
<mat-option *ngFor="let time_unit of time_units" [value]="time_unit + (timerange_amount === 1 ? '' : 's')">
|
||||
{{time_unit + (timerange_amount === 1 ? '' : 's')}}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</mat-dialog-content>
|
||||
|
||||
<mat-dialog-actions>
|
||||
<button mat-button mat-dialog-close>Cancel</button>
|
||||
<!-- The mat-dialog-close directive optionally accepts a value as a result for the dialog. -->
|
||||
<button mat-button [disabled]="!url" type="submit" (click)="subscribeClicked()">Subscribe</button>
|
||||
<div class="mat-spinner" *ngIf="subscribing">
|
||||
<mat-spinner [diameter]="25"></mat-spinner>
|
||||
</div>
|
||||
</mat-dialog-actions>
|
||||
@@ -0,0 +1,8 @@
|
||||
.unit-select {
|
||||
width: 75px;
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
.mat-spinner {
|
||||
margin-left: 5%;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { SubscribeDialogComponent } from './subscribe-dialog.component';
|
||||
|
||||
describe('SubscribeDialogComponent', () => {
|
||||
let component: SubscribeDialogComponent;
|
||||
let fixture: ComponentFixture<SubscribeDialogComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ SubscribeDialogComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(SubscribeDialogComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,69 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { MatSnackBar, MatDialogRef } from '@angular/material';
|
||||
import { PostsService } from 'app/posts.services';
|
||||
|
||||
@Component({
|
||||
selector: 'app-subscribe-dialog',
|
||||
templateUrl: './subscribe-dialog.component.html',
|
||||
styleUrls: ['./subscribe-dialog.component.scss']
|
||||
})
|
||||
export class SubscribeDialogComponent implements OnInit {
|
||||
// inputs
|
||||
timerange_amount;
|
||||
timerange_unit = 'days';
|
||||
download_all = true;
|
||||
url = null;
|
||||
name = null;
|
||||
|
||||
// state
|
||||
subscribing = false;
|
||||
|
||||
time_units = [
|
||||
'day',
|
||||
'week',
|
||||
'month',
|
||||
'year'
|
||||
]
|
||||
|
||||
constructor(private postsService: PostsService,
|
||||
private snackBar: MatSnackBar,
|
||||
public dialogRef: MatDialogRef<SubscribeDialogComponent>) { }
|
||||
|
||||
ngOnInit() {
|
||||
}
|
||||
|
||||
subscribeClicked() {
|
||||
if (this.url && this.url !== '') {
|
||||
// timerange must be specified if download_all is false
|
||||
if (!this.download_all && !this.timerange_amount) {
|
||||
this.openSnackBar('You must specify an amount of time');
|
||||
return;
|
||||
}
|
||||
this.subscribing = true;
|
||||
|
||||
let timerange = null;
|
||||
if (!this.download_all) {
|
||||
timerange = 'now-' + this.timerange_amount.toString() + this.timerange_unit;
|
||||
}
|
||||
|
||||
this.postsService.createSubscription(this.url, this.name, timerange).subscribe(res => {
|
||||
this.subscribing = false;
|
||||
if (res['new_sub']) {
|
||||
this.dialogRef.close(res['new_sub']);
|
||||
} else {
|
||||
if (res['error']) {
|
||||
this.openSnackBar('ERROR: ' + res['error']);
|
||||
}
|
||||
this.dialogRef.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public openSnackBar(message: string, action = '') {
|
||||
this.snackBar.open(message, action, {
|
||||
duration: 2000,
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<h4 mat-dialog-title>{{sub.name}}</h4>
|
||||
|
||||
<mat-dialog-content>
|
||||
<div class="info-item">
|
||||
<strong>Type: </strong>
|
||||
<span class="info-item-value">{{(sub.isPlaylist ? 'Playlist' : 'Channel')}}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<strong>URL: </strong>
|
||||
<span class="info-item-value">{{sub.url}}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<strong>ID: </strong>
|
||||
<span class="info-item-value">{{sub.id}}</span>
|
||||
</div>
|
||||
<div class="info-item" *ngIf="sub.archive">
|
||||
<strong>Archive: </strong>
|
||||
<span class="info-item-value">{{sub.archive}}</span>
|
||||
</div>
|
||||
</mat-dialog-content>
|
||||
|
||||
<mat-dialog-actions>
|
||||
<button mat-button mat-dialog-close>Close</button>
|
||||
<button mat-stroked-button (click)="downloadArchive()" color="accent">Export Archive</button>
|
||||
<span class="spacer"></span>
|
||||
<button mat-button (click)="unsubscribe()" color="warn">Unsubscribe</button>
|
||||
</mat-dialog-actions>
|
||||
@@ -0,0 +1,9 @@
|
||||
.info-item {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.info-item-value {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.spacer {flex: 1 1 auto;}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { SubscriptionInfoDialogComponent } from './subscription-info-dialog.component';
|
||||
|
||||
describe('SubscriptionInfoDialogComponent', () => {
|
||||
let component: SubscriptionInfoDialogComponent;
|
||||
let fixture: ComponentFixture<SubscriptionInfoDialogComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ SubscriptionInfoDialogComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(SubscriptionInfoDialogComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,39 @@
|
||||
import { Component, OnInit, Inject } from '@angular/core';
|
||||
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material';
|
||||
import { PostsService } from 'app/posts.services';
|
||||
|
||||
@Component({
|
||||
selector: 'app-subscription-info-dialog',
|
||||
templateUrl: './subscription-info-dialog.component.html',
|
||||
styleUrls: ['./subscription-info-dialog.component.scss']
|
||||
})
|
||||
export class SubscriptionInfoDialogComponent implements OnInit {
|
||||
|
||||
sub = null;
|
||||
unsubbedEmitter = null;
|
||||
|
||||
constructor(public dialogRef: MatDialogRef<SubscriptionInfoDialogComponent>,
|
||||
@Inject(MAT_DIALOG_DATA) public data: any, private postsService: PostsService) { }
|
||||
|
||||
ngOnInit() {
|
||||
if (this.data) {
|
||||
this.sub = this.data.sub;
|
||||
this.unsubbedEmitter = this.data.unsubbedEmitter;
|
||||
}
|
||||
}
|
||||
|
||||
unsubscribe() {
|
||||
this.postsService.unsubscribe(this.sub, true).subscribe(res => {
|
||||
this.unsubbedEmitter.emit(true);
|
||||
this.dialogRef.close();
|
||||
});
|
||||
}
|
||||
|
||||
downloadArchive() {
|
||||
this.postsService.downloadArchive(this.sub).subscribe(res => {
|
||||
const blob: Blob = res;
|
||||
saveAs(blob, 'archive.txt');
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -119,6 +119,4 @@ mat-form-field.mat-form-field {
|
||||
|
||||
.advanced-input {
|
||||
width: 100%;
|
||||
margin-top: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
@@ -61,7 +61,7 @@
|
||||
</mat-card-actions>
|
||||
</mat-card>
|
||||
</div>
|
||||
<div *ngIf="false && allowAdvancedDownload" class="big demo-basic">
|
||||
<div *ngIf="allowAdvancedDownload" class="big demo-basic">
|
||||
<form style="margin-left: 20px; margin-right: 20px;">
|
||||
<mat-expansion-panel class="big">
|
||||
<mat-expansion-panel-header>
|
||||
@@ -69,20 +69,32 @@
|
||||
Advanced
|
||||
</mat-panel-title>
|
||||
</mat-expansion-panel-header>
|
||||
<p *ngIf="this.simulatedOutput">Simulated command: <i>{{this.simulatedOutput}}</i></p>
|
||||
<div class="container" style="padding-bottom: 20px;">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<mat-checkbox color="accent" [disabled]="current_download" (change)="customArgsEnabledChanged($event)" [(ngModel)]="customArgsEnabled" style="position: absolute; z-index: 999" [ngModelOptions]="{standalone: true}">Use custom args</mat-checkbox>
|
||||
<mat-form-field color="accent" class="advanced-input">
|
||||
<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">
|
||||
<mat-checkbox color="accent" [disabled]="current_download" (change)="customOutputEnabledChanged($event)" [(ngModel)]="customOutputEnabled" style="position: absolute; z-index: 999" [ngModelOptions]="{standalone: true}">Use custom output</mat-checkbox>
|
||||
<mat-form-field color="accent" class="advanced-input">
|
||||
<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">This link</a> will be helpful. Path is relative to the config download path.</mat-hint>
|
||||
<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>
|
||||
|
||||
@@ -15,7 +15,7 @@ import 'rxjs/add/operator/debounceTime'
|
||||
import 'rxjs/add/operator/do'
|
||||
import 'rxjs/add/operator/switch'
|
||||
import { YoutubeSearchService, Result } from '../youtube-search.service';
|
||||
import { Router } from '@angular/router';
|
||||
import { Router, ActivatedRoute } from '@angular/router';
|
||||
import { CreatePlaylistComponent } from 'app/create-playlist/create-playlist.component';
|
||||
import { Platform } from '@angular/cdk/platform';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
@@ -41,8 +41,11 @@ export interface Download {
|
||||
styleUrls: ['./main.component.css']
|
||||
})
|
||||
export class MainComponent implements OnInit {
|
||||
youtubeAuthDisabledOverride = true;
|
||||
|
||||
iOS = false;
|
||||
|
||||
// local settings
|
||||
determinateProgress = false;
|
||||
downloadingfile = false;
|
||||
audioOnly: boolean;
|
||||
@@ -51,22 +54,29 @@ export class MainComponent implements OnInit {
|
||||
customArgs = null;
|
||||
customOutputEnabled = false;
|
||||
customOutput = null;
|
||||
youtubeAuthEnabled = false;
|
||||
youtubeUsername = null;
|
||||
youtubePassword = null;
|
||||
urlError = false;
|
||||
path = '';
|
||||
url = '';
|
||||
exists = '';
|
||||
percentDownloaded: number;
|
||||
autoStartDownload = false;
|
||||
|
||||
// settings
|
||||
// global settings
|
||||
fileManagerEnabled = false;
|
||||
allowQualitySelect = false;
|
||||
downloadOnlyMode = false;
|
||||
allowMultiDownloadMode = false;
|
||||
baseStreamPath;
|
||||
audioFolderPath;
|
||||
videoFolderPath;
|
||||
globalCustomArgs = null;
|
||||
allowAdvancedDownload = false;
|
||||
useDefaultDownloadingAgent = true;
|
||||
customDownloadingAgent = null;
|
||||
|
||||
// formats cache
|
||||
cachedAvailableFormats = {};
|
||||
|
||||
// youtube api
|
||||
@@ -78,7 +88,7 @@ export class MainComponent implements OnInit {
|
||||
|
||||
mp3s: any[] = [];
|
||||
mp4s: any[] = [];
|
||||
files_cols = (window.innerWidth <= 450) ? 2 : 4;
|
||||
files_cols = null;
|
||||
playlists = {'audio': [], 'video': []};
|
||||
playlist_thumbnails = {};
|
||||
downloading_content = {'audio': {}, 'video': {}};
|
||||
@@ -197,28 +207,30 @@ export class MainComponent implements OnInit {
|
||||
is_playlist: false
|
||||
};
|
||||
|
||||
simulatedOutput = '';
|
||||
|
||||
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;
|
||||
|
||||
// loading config
|
||||
this.postsService.loadNavItems().subscribe(result => { // loads settings
|
||||
const backendUrl = result['YoutubeDLMaterial']['Host']['backendurl'];
|
||||
this.postsService.loadNavItems().subscribe(res => { // loads settings
|
||||
const result = !this.postsService.debugMode ? res['config_file'] : res;
|
||||
this.fileManagerEnabled = result['YoutubeDLMaterial']['Extra']['file_manager_enabled'];
|
||||
this.downloadOnlyMode = result['YoutubeDLMaterial']['Extra']['download_only_mode'];
|
||||
this.allowMultiDownloadMode = result['YoutubeDLMaterial']['Extra']['allow_multi_download_mode'];
|
||||
this.baseStreamPath = result['YoutubeDLMaterial']['Downloader']['path-base'];
|
||||
this.audioFolderPath = result['YoutubeDLMaterial']['Downloader']['path-audio'];
|
||||
this.videoFolderPath = result['YoutubeDLMaterial']['Downloader']['path-video'];
|
||||
this.globalCustomArgs = result['YoutubeDLMaterial']['Downloader']['custom_args'];
|
||||
this.youtubeSearchEnabled = result['YoutubeDLMaterial']['API'] && result['YoutubeDLMaterial']['API']['use_youtube_API'] &&
|
||||
result['YoutubeDLMaterial']['API']['youtube_API_key'];
|
||||
this.youtubeAPIKey = this.youtubeSearchEnabled ? result['YoutubeDLMaterial']['API']['youtube_API_key'] : null;
|
||||
this.allowQualitySelect = result['YoutubeDLMaterial']['Extra']['allow_quality_select'];
|
||||
this.allowAdvancedDownload = result['YoutubeDLMaterial']['Advanced']['allow_advanced_download'];
|
||||
this.useDefaultDownloadingAgent = result['YoutubeDLMaterial']['Advanced']['use_default_downloading_agent'];
|
||||
this.customDownloadingAgent = result['YoutubeDLMaterial']['Advanced']['custom_downloading_agent'];
|
||||
|
||||
|
||||
this.postsService.path = backendUrl;
|
||||
this.postsService.startPath = backendUrl;
|
||||
this.postsService.startPathSSL = backendUrl;
|
||||
|
||||
if (this.fileManagerEnabled) {
|
||||
this.getMp3s();
|
||||
@@ -239,14 +251,58 @@ export class MainComponent implements OnInit {
|
||||
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();
|
||||
}
|
||||
|
||||
setInterval(() => this.getSimulatedOutput(), 1000);
|
||||
|
||||
}, 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
|
||||
|
||||
getMp3s() {
|
||||
@@ -302,6 +358,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) {
|
||||
if (isAudio) {
|
||||
this.downloadHelperMp3(name, false, false);
|
||||
@@ -317,6 +385,7 @@ export class MainComponent implements OnInit {
|
||||
this.downloading_content[type][playlistID] = true;
|
||||
this.downloadPlaylist(playlist.fileNames, type, playlist.name, playlistID);
|
||||
} else {
|
||||
localStorage.setItem('player_navigator', this.router.url);
|
||||
const fileNames = playlist.fileNames;
|
||||
this.router.navigate(['/player', {fileNames: fileNames.join('|nvr|'), type: type, id: playlistID}]);
|
||||
}
|
||||
@@ -372,21 +441,6 @@ 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';
|
||||
}
|
||||
|
||||
if (localStorage.getItem('multiDownloadMode') !== null) {
|
||||
this.multiDownloadMode = localStorage.getItem('multiDownloadMode') === 'true';
|
||||
}
|
||||
}
|
||||
|
||||
// download helpers
|
||||
|
||||
downloadHelperMp3(name, is_playlist = false, forceView = false, new_download = null) {
|
||||
@@ -394,7 +448,7 @@ export class MainComponent implements OnInit {
|
||||
|
||||
if (new_download && this.current_download !== new_download) {
|
||||
// console.log('mismatched downloads');
|
||||
} else if (!this.multiDownloadMode) {
|
||||
} else if (!this.multiDownloadMode || !new_download) {
|
||||
// if download only mode, just download the file. no redirect
|
||||
if (forceView === false && this.downloadOnlyMode && !this.iOS) {
|
||||
if (is_playlist) {
|
||||
@@ -404,12 +458,11 @@ export class MainComponent implements OnInit {
|
||||
this.downloadAudioFile(decodeURI(name));
|
||||
}
|
||||
} else {
|
||||
localStorage.setItem('player_navigator', this.router.url);
|
||||
if (is_playlist) {
|
||||
this.router.navigate(['/player', {fileNames: name.join('|nvr|'), type: 'audio'}]);
|
||||
// window.location.href = this.baseStreamPath + this.audioFolderPath + name[0] + '.mp3';
|
||||
} else {
|
||||
this.router.navigate(['/player', {fileNames: name, type: 'audio'}]);
|
||||
// window.location.href = this.baseStreamPath + this.audioFolderPath + name + '.mp3';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -433,7 +486,7 @@ export class MainComponent implements OnInit {
|
||||
|
||||
if (new_download && this.current_download !== new_download) {
|
||||
// console.log('mismatched downloads');
|
||||
} else if (!this.multiDownloadMode) {
|
||||
} else if (!this.multiDownloadMode || !new_download) {
|
||||
// if download only mode, just download the file. no redirect
|
||||
if (forceView === false && this.downloadOnlyMode) {
|
||||
if (is_playlist) {
|
||||
@@ -443,12 +496,11 @@ export class MainComponent implements OnInit {
|
||||
this.downloadVideoFile(decodeURI(name));
|
||||
}
|
||||
} else {
|
||||
localStorage.setItem('player_navigator', this.router.url);
|
||||
if (is_playlist) {
|
||||
this.router.navigate(['/player', {fileNames: name.join('|nvr|'), type: 'video'}]);
|
||||
// window.location.href = this.baseStreamPath + this.videoFolderPath + name[0] + '.mp4';
|
||||
} else {
|
||||
this.router.navigate(['/player', {fileNames: name, type: 'video'}]);
|
||||
// window.location.href = this.baseStreamPath + this.videoFolderPath + name + '.mp4';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -473,6 +525,25 @@ export class MainComponent implements OnInit {
|
||||
this.urlError = false;
|
||||
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) {
|
||||
// create download object
|
||||
const new_download: Download = {
|
||||
@@ -489,18 +560,11 @@ export class MainComponent implements OnInit {
|
||||
|
||||
let customQualityConfiguration = null;
|
||||
if (this.selectedQuality !== '') {
|
||||
const cachedFormatsExists = this.cachedAvailableFormats[this.url] && this.cachedAvailableFormats[this.url]['formats'];
|
||||
if (cachedFormatsExists) {
|
||||
const audio_formats = this.cachedAvailableFormats[this.url]['formats']['audio'];
|
||||
customQualityConfiguration = audio_formats[this.selectedQuality]['format_id'];
|
||||
}
|
||||
customQualityConfiguration = this.getSelectedAudioFormat();
|
||||
}
|
||||
|
||||
const customArgs = (this.customArgsEnabled ? this.customArgs : null);
|
||||
const customOutput = (this.customOutputEnabled ? this.customOutput : null);
|
||||
|
||||
this.postsService.makeMP3(this.url, (this.selectedQuality === '' ? null : this.selectedQuality),
|
||||
customQualityConfiguration, customArgs, customOutput).subscribe(posts => {
|
||||
customQualityConfiguration, customArgs, customOutput, youtubeUsername, youtubePassword).subscribe(posts => {
|
||||
// update download object
|
||||
new_download.downloading = false;
|
||||
new_download.percent_complete = 100;
|
||||
@@ -529,20 +593,10 @@ export class MainComponent implements OnInit {
|
||||
if (!this.current_download && !this.multiDownloadMode) { this.current_download = new_download };
|
||||
this.downloadingfile = true;
|
||||
|
||||
let customQualityConfiguration = null;
|
||||
const cachedFormatsExists = this.cachedAvailableFormats[this.url] && this.cachedAvailableFormats[this.url]['formats'];
|
||||
if (cachedFormatsExists) {
|
||||
const video_formats = this.cachedAvailableFormats[this.url]['formats']['video'];
|
||||
if (video_formats['best_audio_format'] && this.selectedQuality !== '') {
|
||||
customQualityConfiguration = video_formats[this.selectedQuality]['format_id'] + '+' + video_formats['best_audio_format'];
|
||||
}
|
||||
}
|
||||
|
||||
const customArgs = (this.customArgsEnabled ? this.customArgs : null);
|
||||
const customOutput = (this.customOutputEnabled ? this.customOutput : null);
|
||||
const customQualityConfiguration = this.getSelectedVideoFormat();
|
||||
|
||||
this.postsService.makeMP4(this.url, (this.selectedQuality === '' ? null : this.selectedQuality),
|
||||
customQualityConfiguration, customArgs, customOutput).subscribe(posts => {
|
||||
customQualityConfiguration, customArgs, customOutput, youtubeUsername, youtubePassword).subscribe(posts => {
|
||||
// update download object
|
||||
new_download.downloading = false;
|
||||
new_download.percent_complete = 100;
|
||||
@@ -580,6 +634,29 @@ export class MainComponent implements OnInit {
|
||||
this.current_download = null;
|
||||
}
|
||||
|
||||
getSelectedAudioFormat() {
|
||||
if (this.selectedQuality === '') { return null };
|
||||
const cachedFormatsExists = this.cachedAvailableFormats[this.url] && this.cachedAvailableFormats[this.url]['formats'];
|
||||
if (cachedFormatsExists) {
|
||||
const audio_formats = this.cachedAvailableFormats[this.url]['formats']['audio'];
|
||||
return audio_formats[this.selectedQuality]['format_id'];
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
getSelectedVideoFormat() {
|
||||
if (this.selectedQuality === '') { return null };
|
||||
const cachedFormatsExists = this.cachedAvailableFormats[this.url] && this.cachedAvailableFormats[this.url]['formats'];
|
||||
if (cachedFormatsExists) {
|
||||
const video_formats = this.cachedAvailableFormats[this.url]['formats']['video'];
|
||||
if (video_formats['best_audio_format'] && this.selectedQuality !== '') {
|
||||
return video_formats[this.selectedQuality]['format_id'] + '+' + video_formats['best_audio_format'];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
getDownloadByUID(uid) {
|
||||
const index = this.downloads.findIndex(download => download.uid === uid);
|
||||
if (index !== -1) {
|
||||
@@ -722,6 +799,77 @@ export class MainComponent implements OnInit {
|
||||
}
|
||||
}
|
||||
|
||||
getSimulatedOutput() {
|
||||
const customArgsExists = this.customArgsEnabled && this.customArgs;
|
||||
const globalArgsExists = this.globalCustomArgs && this.globalCustomArgs !== '';
|
||||
|
||||
let full_string_array: string[] = [];
|
||||
const base_string_array = ['youtube-dl', this.url];
|
||||
|
||||
if (customArgsExists) {
|
||||
this.simulatedOutput = base_string_array.join(' ') + ' ' + this.customArgs;
|
||||
return this.simulatedOutput;
|
||||
}
|
||||
|
||||
full_string_array.push(...base_string_array);
|
||||
|
||||
const base_path = this.audioOnly ? this.audioFolderPath : this.videoFolderPath;
|
||||
const ext = this.audioOnly ? '.mp3' : '.mp4';
|
||||
// gets output
|
||||
let output_string_array = ['-o', base_path + '%(title)s' + ext];
|
||||
if (this.customOutputEnabled && this.customOutput) {
|
||||
output_string_array = ['-o', this.customOutput + ext];
|
||||
}
|
||||
// before pushing output, should check if using an external downloader
|
||||
if (!this.useDefaultDownloadingAgent && this.customDownloadingAgent === 'aria2c') {
|
||||
full_string_array.push('--external-downloader', 'aria2c');
|
||||
}
|
||||
// pushes output
|
||||
full_string_array.push(...output_string_array);
|
||||
|
||||
// logic splits into audio and video modes
|
||||
if (this.audioOnly) {
|
||||
// adds base audio string
|
||||
const format_array = [];
|
||||
const audio_format = this.getSelectedAudioFormat();
|
||||
if (audio_format) {
|
||||
format_array.push('-f', audio_format);
|
||||
} else if (this.selectedQuality) {
|
||||
format_array.push('--audio-quality', this.selectedQuality);
|
||||
}
|
||||
|
||||
// pushes formats
|
||||
full_string_array.splice(2, 0, ...format_array);
|
||||
|
||||
const additional_params = ['-x', '--audio-format', 'mp3', '--write-info-json', '--print-json'];
|
||||
|
||||
full_string_array.push(...additional_params);
|
||||
} else {
|
||||
// adds base video string
|
||||
let format_array = ['-f', 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/mp4'];
|
||||
const video_format = this.getSelectedVideoFormat();
|
||||
if (video_format) {
|
||||
format_array = ['-f', video_format];
|
||||
} else if (this.selectedQuality) {
|
||||
format_array = [`bestvideo[height=${this.selectedQuality}]+bestaudio/best[height=${this.selectedQuality}]`];
|
||||
}
|
||||
|
||||
// pushes formats
|
||||
full_string_array.splice(2, 0, ...format_array);
|
||||
|
||||
const additional_params = ['--write-info-json', '--print-json'];
|
||||
|
||||
full_string_array.push(...additional_params);
|
||||
}
|
||||
|
||||
if (globalArgsExists) {
|
||||
full_string_array = full_string_array.concat(this.globalCustomArgs.split(' '));
|
||||
}
|
||||
|
||||
this.simulatedOutput = full_string_array.join(' ');
|
||||
return this.simulatedOutput;
|
||||
}
|
||||
|
||||
errorFormats(url) {
|
||||
this.cachedAvailableFormats[url]['formats_loading'] = false;
|
||||
console.error('Could not load formats for url ' + url);
|
||||
@@ -757,7 +905,7 @@ export class MainComponent implements OnInit {
|
||||
}
|
||||
|
||||
onResize(event) {
|
||||
this.files_cols = (event.target.innerWidth <= 450) ? 2 : 4;
|
||||
this.setCols();
|
||||
}
|
||||
|
||||
videoModeChanged(new_val) {
|
||||
@@ -773,6 +921,10 @@ export class MainComponent implements OnInit {
|
||||
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');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -780,6 +932,15 @@ export class MainComponent implements OnInit {
|
||||
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');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
.video-player {
|
||||
margin: 0 auto;
|
||||
min-width: 300px;
|
||||
}
|
||||
|
||||
.video-player:focus {
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
</div>
|
||||
<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 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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -10,6 +10,7 @@ export interface IMedia {
|
||||
title: string;
|
||||
src: string;
|
||||
type: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
@@ -30,16 +31,19 @@ export class PlayerComponent implements OnInit {
|
||||
// params
|
||||
fileNames: string[];
|
||||
type: string;
|
||||
id = null; // used for playlists (not subscription)
|
||||
subscriptionName = null;
|
||||
subPlaylist = null;
|
||||
|
||||
baseStreamPath = null;
|
||||
audioFolderPath = null;
|
||||
videoFolderPath = null;
|
||||
subscriptionFolderPath = null;
|
||||
|
||||
innerWidth: number;
|
||||
|
||||
downloading = false;
|
||||
|
||||
id = null;
|
||||
|
||||
@HostListener('window:resize', ['$event'])
|
||||
onResize(event) {
|
||||
this.innerWidth = window.innerWidth;
|
||||
@@ -51,36 +55,58 @@ export class PlayerComponent implements OnInit {
|
||||
this.fileNames = this.route.snapshot.paramMap.get('fileNames').split('|nvr|');
|
||||
this.type = this.route.snapshot.paramMap.get('type');
|
||||
this.id = this.route.snapshot.paramMap.get('id');
|
||||
this.subscriptionName = this.route.snapshot.paramMap.get('subscriptionName');
|
||||
this.subPlaylist = this.route.snapshot.paramMap.get('subPlaylist');
|
||||
|
||||
// loading config
|
||||
this.postsService.loadNavItems().subscribe(result => { // loads settings
|
||||
this.baseStreamPath = result['YoutubeDLMaterial']['Downloader']['path-base'];
|
||||
this.postsService.loadNavItems().subscribe(res => { // loads settings
|
||||
const result = !this.postsService.debugMode ? res['config_file'] : res;
|
||||
this.baseStreamPath = this.postsService.path;
|
||||
this.audioFolderPath = result['YoutubeDLMaterial']['Downloader']['path-audio'];
|
||||
this.videoFolderPath = result['YoutubeDLMaterial']['Downloader']['path-video'];
|
||||
const backendUrl = result['YoutubeDLMaterial']['Host']['backendurl'];
|
||||
this.subscriptionFolderPath = result['YoutubeDLMaterial']['Subscriptions']['subscriptions_base_path'];
|
||||
|
||||
this.postsService.path = backendUrl;
|
||||
this.postsService.startPath = backendUrl;
|
||||
this.postsService.startPathSSL = backendUrl;
|
||||
|
||||
let fileType = null;
|
||||
if (this.type === 'audio') {
|
||||
fileType = 'audio/mp3';
|
||||
} else if (this.type === 'video') {
|
||||
fileType = 'video/mp4';
|
||||
} else if (this.type === 'subscription') {
|
||||
// only supports mp4 for now
|
||||
fileType = 'video/mp4';
|
||||
} else {
|
||||
// error
|
||||
console.error('Must have valid file type! Use \'audio\' or \video\'');
|
||||
console.error('Must have valid file type! Use \'audio\', \'video\', or \'subscription\'.');
|
||||
}
|
||||
|
||||
for (let i = 0; i < this.fileNames.length; i++) {
|
||||
const fileName = this.fileNames[i];
|
||||
const baseLocation = (this.type === 'audio') ? this.audioFolderPath : this.videoFolderPath;
|
||||
const fullLocation = this.baseStreamPath + baseLocation + encodeURI(fileName); // + (this.type === 'audio' ? '.mp3' : '.mp4');
|
||||
let baseLocation = null;
|
||||
let fullLocation = null;
|
||||
if (!this.subscriptionName) {
|
||||
baseLocation = this.type + '/';
|
||||
fullLocation = this.baseStreamPath + baseLocation + encodeURIComponent(fileName);
|
||||
} else {
|
||||
// default to video but include subscription name param
|
||||
baseLocation = 'video/';
|
||||
fullLocation = this.baseStreamPath + baseLocation + encodeURIComponent(fileName) + '?subName=' + this.subscriptionName +
|
||||
'&subPlaylist=' + this.subPlaylist;
|
||||
}
|
||||
// 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 = {
|
||||
title: fileName,
|
||||
src: fullLocation,
|
||||
type: fileType
|
||||
type: fileType,
|
||||
label: label
|
||||
}
|
||||
this.playlist.push(mediaObject);
|
||||
}
|
||||
|
||||
@@ -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 config from '../assets/default.json';
|
||||
import 'rxjs/add/operator/map';
|
||||
@@ -7,20 +7,31 @@ import 'rxjs/add/operator/map';
|
||||
import 'rxjs/add/operator/catch';
|
||||
import 'rxjs/add/observable/throw';
|
||||
import { THEMES_CONFIG } from '../themes';
|
||||
import { Router } from '@angular/router';
|
||||
import { DOCUMENT } from '@angular/common';
|
||||
|
||||
@Injectable()
|
||||
export class PostsService {
|
||||
path = '';
|
||||
audioFolder = '';
|
||||
videoFolder = '';
|
||||
startPath = 'http://localhost:17442/';
|
||||
startPathSSL = 'https://localhost:17442/'
|
||||
startPath = null; // 'http://localhost:17442/';
|
||||
startPathSSL = null; // 'https://localhost:17442/'
|
||||
handShakeComplete = false;
|
||||
THEMES_CONFIG = THEMES_CONFIG;
|
||||
theme;
|
||||
|
||||
constructor(private http: HttpClient) {
|
||||
debugMode = false;
|
||||
constructor(private http: HttpClient, private router: Router, @Inject(DOCUMENT) private document: Document) {
|
||||
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) {
|
||||
@@ -44,21 +55,25 @@ export class PostsService {
|
||||
}
|
||||
|
||||
// tslint:disable-next-line: max-line-length
|
||||
makeMP3(url: string, selectedQuality: string, customQualityConfiguration: string, customArgs: string = null, customOutput: string = null) {
|
||||
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,
|
||||
maxBitrate: selectedQuality,
|
||||
customQualityConfiguration: customQualityConfiguration,
|
||||
customArgs: customArgs,
|
||||
customOutput: customOutput});
|
||||
customOutput: customOutput,
|
||||
youtubeUsername: youtubeUsername,
|
||||
youtubePassword: youtubePassword});
|
||||
}
|
||||
|
||||
// tslint:disable-next-line: max-line-length
|
||||
makeMP4(url: string, selectedQuality: string, customQualityConfiguration: string, customArgs: string = null, customOutput: string = null) {
|
||||
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,
|
||||
selectedHeight: selectedQuality,
|
||||
customQualityConfiguration: customQualityConfiguration,
|
||||
customArgs: customArgs,
|
||||
customOutput: customOutput});
|
||||
customOutput: customOutput,
|
||||
youtubeUsername: youtubeUsername,
|
||||
youtubePassword: youtubePassword});
|
||||
}
|
||||
|
||||
getFileStatusMp3(name: string) {
|
||||
@@ -72,10 +87,13 @@ export class PostsService {
|
||||
loadNavItems() {
|
||||
if (isDevMode()) {
|
||||
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');
|
||||
}
|
||||
|
||||
setConfig(config) {
|
||||
return this.http.post(this.path + 'setConfig', {new_config_file: config});
|
||||
}
|
||||
|
||||
deleteFile(name: string, isAudio: boolean) {
|
||||
@@ -102,6 +120,10 @@ export class PostsService {
|
||||
{responseType: 'blob'});
|
||||
}
|
||||
|
||||
downloadArchive(sub) {
|
||||
return this.http.post(this.path + 'downloadArchive', {sub: sub}, {responseType: 'blob'});
|
||||
}
|
||||
|
||||
getFileInfo(fileNames, type, urlMode) {
|
||||
return this.http.post(this.path + 'getVideoInfos', {fileNames: fileNames, type: type, urlMode: urlMode});
|
||||
}
|
||||
@@ -122,6 +144,26 @@ export class PostsService {
|
||||
removePlaylist(playlistID, type) {
|
||||
return this.http.post(this.path + 'deletePlaylist', {playlistID: playlistID, type: type});
|
||||
}
|
||||
|
||||
createSubscription(url, name, timerange = null) {
|
||||
return this.http.post(this.path + 'subscribe', {url: url, name: name, timerange: timerange})
|
||||
}
|
||||
|
||||
unsubscribe(sub, deleteMode = false) {
|
||||
return this.http.post(this.path + 'unsubscribe', {sub: sub, deleteMode: deleteMode})
|
||||
}
|
||||
|
||||
deleteSubscriptionFile(sub, file, deleteForever) {
|
||||
return this.http.post(this.path + 'deleteSubscriptionFile', {sub: sub, file: file, deleteForever: deleteForever})
|
||||
}
|
||||
|
||||
getSubscription(id) {
|
||||
return this.http.post(this.path + 'getSubscription', {id: id});
|
||||
}
|
||||
|
||||
getAllSubscriptions() {
|
||||
return this.http.post(this.path + 'getAllSubscriptions', {});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
231
src/app/settings/settings.component.html
Normal file
231
src/app/settings/settings.component.html
Normal file
@@ -0,0 +1,231 @@
|
||||
<h4 mat-dialog-title>Settings</h4>
|
||||
|
||||
<mat-dialog-content>
|
||||
<!-- Host -->
|
||||
<mat-expansion-panel class="settings-expansion-panel">
|
||||
<mat-expansion-panel-header>
|
||||
<mat-panel-title>
|
||||
Host
|
||||
</mat-panel-title>
|
||||
</mat-expansion-panel-header>
|
||||
<div *ngIf="new_config" class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<mat-form-field color="accent">
|
||||
<input [(ngModel)]="new_config['Host']['url']" matInput placeholder="URL" required>
|
||||
<mat-hint>Base URL this app will be accessed from, without the port.</mat-hint>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="col-12 mt-4">
|
||||
<mat-form-field color="accent">
|
||||
<input [(ngModel)]="new_config['Host']['port']" matInput placeholder="Port" required>
|
||||
<mat-hint>The desired port. Default is 17442.</mat-hint>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</mat-expansion-panel>
|
||||
|
||||
<!-- Encryption -->
|
||||
<mat-expansion-panel class="settings-expansion-panel">
|
||||
<mat-expansion-panel-header>
|
||||
<mat-panel-title>
|
||||
Encryption
|
||||
</mat-panel-title>
|
||||
</mat-expansion-panel-header>
|
||||
<div *ngIf="new_config" class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<mat-checkbox color="accent" [(ngModel)]="new_config['Encryption']['use-encryption']">Use encryption</mat-checkbox>
|
||||
</div>
|
||||
|
||||
<div class="col-12">
|
||||
<mat-form-field color="accent">
|
||||
<input [disabled]="!new_config['Encryption']['use-encryption']" [(ngModel)]="new_config['Encryption']['cert-file-path']" matInput placeholder="Cert file path">
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div class="col-12">
|
||||
<mat-form-field color="accent">
|
||||
<input [disabled]="!new_config['Encryption']['use-encryption']" [(ngModel)]="new_config['Encryption']['key-file-path']" matInput placeholder="Key file path">
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</mat-expansion-panel>
|
||||
|
||||
<!-- Downloader -->
|
||||
<mat-expansion-panel class="settings-expansion-panel">
|
||||
<mat-expansion-panel-header>
|
||||
<mat-panel-title>
|
||||
Downloader
|
||||
</mat-panel-title>
|
||||
</mat-expansion-panel-header>
|
||||
<div *ngIf="new_config" class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<mat-form-field color="accent">
|
||||
<input matInput [(ngModel)]="new_config['Downloader']['path-audio']" placeholder="Audio folder path" required>
|
||||
<mat-hint>Path for audio only downloads. It is relative to YTDL-Material's root folder.</mat-hint>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div class="col-12 mt-4">
|
||||
<mat-form-field color="accent">
|
||||
<input matInput [(ngModel)]="new_config['Downloader']['path-video']" placeholder="Video folder path" required>
|
||||
<mat-hint>Path for video downloads. It is relative to YTDL-Material's root folder.</mat-hint>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div class="col-12 mt-4">
|
||||
<mat-form-field color="accent">
|
||||
<textarea matInput [(ngModel)]="new_config['Downloader']['custom_args']" placeholder="Custom args"></textarea>
|
||||
<mat-hint>Global custom args for downloads on the home page.</mat-hint>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</mat-expansion-panel>
|
||||
|
||||
<!-- Extra -->
|
||||
<mat-expansion-panel class="settings-expansion-panel">
|
||||
<mat-expansion-panel-header>
|
||||
<mat-panel-title>
|
||||
Extra
|
||||
</mat-panel-title>
|
||||
</mat-expansion-panel-header>
|
||||
<div *ngIf="new_config" class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<mat-form-field color="accent">
|
||||
<input [(ngModel)]="new_config['Extra']['title_top']" matInput placeholder="Top title" required>
|
||||
<mat-hint></mat-hint>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<mat-checkbox color="accent" [(ngModel)]="new_config['Extra']['file_manager_enabled']">File manager enabled</mat-checkbox>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<mat-checkbox color="accent" [(ngModel)]="new_config['Extra']['allow_quality_select']">Allow quality select</mat-checkbox>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<mat-checkbox color="accent" [(ngModel)]="new_config['Extra']['download_only_mode']">Download only mode</mat-checkbox>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<mat-checkbox color="accent" [(ngModel)]="new_config['Extra']['allow_multi_download_mode']">Allow multi-download mode</mat-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</mat-expansion-panel>
|
||||
|
||||
<!-- API -->
|
||||
<mat-expansion-panel class="settings-expansion-panel">
|
||||
<mat-expansion-panel-header>
|
||||
<mat-panel-title>
|
||||
API
|
||||
</mat-panel-title>
|
||||
</mat-expansion-panel-header>
|
||||
<div *ngIf="new_config" class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<mat-checkbox color="accent" [(ngModel)]="new_config['API']['use_youtube_api']">Use YouTube API</mat-checkbox>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<mat-form-field color="accent">
|
||||
<input [disabled]="!new_config['API']['use_youtube_api']" [(ngModel)]="new_config['API']['youtube_API_key']" matInput placeholder="Youtube API Key" required>
|
||||
<mat-hint><a target="_blank" href="https://developers.google.com/youtube/v3/getting-started">Generating a key</a> is easy!</mat-hint>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</mat-expansion-panel>
|
||||
|
||||
<!-- Themes -->
|
||||
<mat-expansion-panel class="settings-expansion-panel">
|
||||
<mat-expansion-panel-header>
|
||||
<mat-panel-title>
|
||||
Themes
|
||||
</mat-panel-title>
|
||||
</mat-expansion-panel-header>
|
||||
<div *ngIf="new_config" class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<mat-select color="accent" style="width: 100px" [(ngModel)]="new_config['Themes']['default_theme']">
|
||||
<mat-option value="default">Default</mat-option>
|
||||
<mat-option value="dark">Dark</mat-option>
|
||||
</mat-select>
|
||||
</div>
|
||||
|
||||
<div class="col-12 mt-4">
|
||||
<mat-checkbox color="accent" [(ngModel)]="new_config['Themes']['allow_theme_change']">Allow theme change</mat-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</mat-expansion-panel>
|
||||
|
||||
<!-- Subscriptions -->
|
||||
<mat-expansion-panel class="settings-expansion-panel">
|
||||
<mat-expansion-panel-header>
|
||||
<mat-panel-title>
|
||||
Subscriptions
|
||||
</mat-panel-title>
|
||||
</mat-expansion-panel-header>
|
||||
<div *ngIf="new_config" class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<mat-checkbox color="accent" [(ngModel)]="new_config['Subscriptions']['allow_subscriptions']">Allow subscriptions</mat-checkbox>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<mat-form-field color="accent">
|
||||
<input [disabled]="!new_config['Subscriptions']['allow_subscriptions']" [(ngModel)]="new_config['Subscriptions']['subscriptions_base_path']" matInput placeholder="Subscriptions base path">
|
||||
<mat-hint>Base path for videos from your subscribed channels and playlists. It is relative to YTDL-Material's root folder.</mat-hint>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="col-12 mt-5">
|
||||
<mat-form-field color="accent">
|
||||
<input [disabled]="!new_config['Subscriptions']['allow_subscriptions']" [(ngModel)]="new_config['Subscriptions']['subscriptions_check_interval']" matInput placeholder="Check interval">
|
||||
<mat-hint>Unit is seconds, only include numbers.</mat-hint>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="col-12 mt-4">
|
||||
<mat-checkbox color="accent" [disabled]="!new_config['Subscriptions']['allow_subscriptions']" [(ngModel)]="new_config['Subscriptions']['subscriptions_use_youtubedl_archive']">Use youtube-dl archive</mat-checkbox>
|
||||
<p>With youtube-dl's <a target="_blank" href="https://github.com/ytdl-org/youtube-dl/blob/master/README.md#how-do-i-download-only-new-videos-from-a-playlist">archive</a> feature, downloaded videos from your subscriptions get recorded in a text file in the subscriptions <i>archive</i> sub-directory.</p>
|
||||
<p>This enables the ability to permanently delete videos from your subscriptions without unsubscribing, and allows you to record which videos you downloaded in case of data loss.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</mat-expansion-panel>
|
||||
|
||||
<!-- Advanced -->
|
||||
<mat-expansion-panel class="settings-expansion-panel">
|
||||
<mat-expansion-panel-header>
|
||||
<mat-panel-title>
|
||||
Advanced
|
||||
</mat-panel-title>
|
||||
</mat-expansion-panel-header>
|
||||
<div *ngIf="new_config" class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<mat-checkbox color="accent" [(ngModel)]="new_config['Advanced']['use_default_downloading_agent']">Use default downloading agent</mat-checkbox>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<mat-form-field color="accent">
|
||||
<input [disabled]="new_config['Advanced']['use_default_downloading_agent']" [(ngModel)]="new_config['Advanced']['custom_downloading_agent']" matInput placeholder="Custom agent" required>
|
||||
<mat-hint></mat-hint>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<mat-checkbox color="accent" [(ngModel)]="new_config['Advanced']['allow_advanced_download']">Allow advanced download</mat-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</mat-expansion-panel>
|
||||
</mat-dialog-content>
|
||||
|
||||
<mat-dialog-actions>
|
||||
<div style="margin-bottom: 10px;">
|
||||
<button color="accent" (click)="saveSettings()" [disabled]="settingsSame()" mat-raised-button><mat-icon>done</mat-icon> Save</button>
|
||||
<button mat-flat-button [mat-dialog-close]="false"><mat-icon>cancel</mat-icon> Cancel</button>
|
||||
</div>
|
||||
</mat-dialog-actions>
|
||||
3
src/app/settings/settings.component.scss
Normal file
3
src/app/settings/settings.component.scss
Normal file
@@ -0,0 +1,3 @@
|
||||
.settings-expansion-panel {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
25
src/app/settings/settings.component.spec.ts
Normal file
25
src/app/settings/settings.component.spec.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { SettingsComponent } from './settings.component';
|
||||
|
||||
describe('SettingsComponent', () => {
|
||||
let component: SettingsComponent;
|
||||
let fixture: ComponentFixture<SettingsComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ SettingsComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(SettingsComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
48
src/app/settings/settings.component.ts
Normal file
48
src/app/settings/settings.component.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { PostsService } from 'app/posts.services';
|
||||
|
||||
@Component({
|
||||
selector: 'app-settings',
|
||||
templateUrl: './settings.component.html',
|
||||
styleUrls: ['./settings.component.scss']
|
||||
})
|
||||
export class SettingsComponent implements OnInit {
|
||||
|
||||
initial_config = null;
|
||||
new_config = null
|
||||
loading_config = false;
|
||||
|
||||
constructor(private postsService: PostsService) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.getConfig();
|
||||
}
|
||||
|
||||
getConfig() {
|
||||
this.loading_config = true;
|
||||
this.postsService.loadNavItems().subscribe(res => {
|
||||
this.loading_config = false;
|
||||
// successfully loaded config
|
||||
|
||||
this.initial_config = !this.postsService.debugMode ? res['config_file']['YoutubeDLMaterial'] : res['YoutubeDLMaterial'];
|
||||
this.new_config = JSON.parse(JSON.stringify(this.initial_config));
|
||||
});
|
||||
}
|
||||
|
||||
settingsSame() {
|
||||
return JSON.stringify(this.new_config) === JSON.stringify(this.initial_config);
|
||||
}
|
||||
|
||||
saveSettings() {
|
||||
const settingsToSave = {'YoutubeDLMaterial': this.new_config};
|
||||
this.postsService.setConfig(settingsToSave).subscribe(res => {
|
||||
if (res['success']) {
|
||||
// sets new config as old config
|
||||
this.initial_config = JSON.parse(JSON.stringify(this.new_config));
|
||||
}
|
||||
}, err => {
|
||||
console.error('Failed to save config!');
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<div style="position: relative; width: fit-content;">
|
||||
<div class="duration-time">
|
||||
Length: {{formattedDuration}}
|
||||
</div>
|
||||
<button [matMenuTriggerFor]="action_menu" class="menuButton" mat-icon-button><mat-icon>more_vert</mat-icon></button>
|
||||
<mat-menu #action_menu="matMenu">
|
||||
<button (click)="deleteAndRedownload()" mat-menu-item><mat-icon>restore</mat-icon>Delete and redownload</button>
|
||||
<button (click)="deleteForever()" mat-menu-item *ngIf="sub.archive && use_youtubedl_archive"><mat-icon>delete_forever</mat-icon>Delete forever</button>
|
||||
</mat-menu>
|
||||
<mat-card (click)="goToFile()" matRipple class="example-card mat-elevation-z6">
|
||||
<div style="padding:5px">
|
||||
<div *ngIf="!image_errored && file.thumbnailURL" class="img-div">
|
||||
<img class="image" (error)="onImgError($event)" [src]="file.thumbnailURL" alt="Thumbnail">
|
||||
</div>
|
||||
|
||||
<span class="max-two-lines"><strong>{{file.title}}</strong></span>
|
||||
</div>
|
||||
</mat-card>
|
||||
</div>
|
||||
@@ -0,0 +1,76 @@
|
||||
.example-card {
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
padding: 0px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.menuButton {
|
||||
right: 0px;
|
||||
top: -1px;
|
||||
position: absolute;
|
||||
z-index: 999;
|
||||
|
||||
}
|
||||
|
||||
/* Coerce the <span> icon container away from display:inline */
|
||||
.mat-icon-button .mat-button-wrapper {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.image {
|
||||
width: 200px;
|
||||
height: 112.5px;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.example-full-width-height {
|
||||
width: 100%;
|
||||
height: 100%
|
||||
}
|
||||
|
||||
.centered {
|
||||
margin: 0 auto;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
}
|
||||
|
||||
.img-div {
|
||||
max-height: 80px;
|
||||
padding: 0px;
|
||||
margin: 32px 0px 0px -5px;
|
||||
width: calc(100% + 5px + 5px);
|
||||
}
|
||||
|
||||
.max-two-lines {
|
||||
display: -webkit-box;
|
||||
display: -moz-box;
|
||||
max-height: 2.4em;
|
||||
line-height: 1.2em;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
bottom: 5px;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.duration-time {
|
||||
position: absolute;
|
||||
left: 5px;
|
||||
top: 5px;
|
||||
z-index: 99999;
|
||||
}
|
||||
|
||||
@media (max-width: 576px){
|
||||
|
||||
.example-card {
|
||||
width: 175px !important;
|
||||
}
|
||||
|
||||
.image {
|
||||
width: 175px;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { SubscriptionFileCardComponent } from './subscription-file-card.component';
|
||||
|
||||
describe('SubscriptionFileCardComponent', () => {
|
||||
let component: SubscriptionFileCardComponent;
|
||||
let fixture: ComponentFixture<SubscriptionFileCardComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ SubscriptionFileCardComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(SubscriptionFileCardComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,97 @@
|
||||
import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core';
|
||||
import { Observable, Subject } from 'rxjs';
|
||||
import { MatSnackBar } from '@angular/material';
|
||||
import { Router } from '@angular/router';
|
||||
import { PostsService } from 'app/posts.services';
|
||||
|
||||
@Component({
|
||||
selector: 'app-subscription-file-card',
|
||||
templateUrl: './subscription-file-card.component.html',
|
||||
styleUrls: ['./subscription-file-card.component.scss']
|
||||
})
|
||||
export class SubscriptionFileCardComponent implements OnInit {
|
||||
image_errored = false;
|
||||
image_loaded = false;
|
||||
|
||||
scrollSubject;
|
||||
scrollAndLoad;
|
||||
|
||||
formattedDuration = null;
|
||||
|
||||
@Input() file;
|
||||
@Input() sub;
|
||||
@Input() use_youtubedl_archive = false;
|
||||
|
||||
@Output() goToFileEmit = new EventEmitter<any>();
|
||||
@Output() reloadSubscription = new EventEmitter<boolean>();
|
||||
|
||||
constructor(private snackBar: MatSnackBar, private postsService: PostsService) {
|
||||
this.scrollSubject = new Subject();
|
||||
this.scrollAndLoad = Observable.merge(
|
||||
Observable.fromEvent(window, 'scroll'),
|
||||
this.scrollSubject
|
||||
);
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
if (this.file.duration) {
|
||||
this.formattedDuration = fancyTimeFormat(this.file.duration);
|
||||
}
|
||||
}
|
||||
|
||||
onImgError(event) {
|
||||
this.image_errored = true;
|
||||
}
|
||||
|
||||
onHoverResponse() {
|
||||
this.scrollSubject.next();
|
||||
}
|
||||
|
||||
imageLoaded(loaded) {
|
||||
this.image_loaded = true;
|
||||
}
|
||||
|
||||
goToFile() {
|
||||
this.goToFileEmit.emit(this.file.id);
|
||||
}
|
||||
|
||||
deleteAndRedownload() {
|
||||
this.postsService.deleteSubscriptionFile(this.sub, this.file.id, false).subscribe(res => {
|
||||
this.reloadSubscription.emit(true);
|
||||
this.openSnackBar(`Successfully deleted file: '${this.file.id}'`, 'Dismiss.');
|
||||
});
|
||||
}
|
||||
|
||||
deleteForever() {
|
||||
this.postsService.deleteSubscriptionFile(this.sub, this.file.id, true).subscribe(res => {
|
||||
this.reloadSubscription.emit(true);
|
||||
this.openSnackBar(`Successfully deleted file: '${this.file.id}'`, 'Dismiss.');
|
||||
});
|
||||
}
|
||||
|
||||
public openSnackBar(message: string, action: string) {
|
||||
this.snackBar.open(message, action, {
|
||||
duration: 2000,
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function fancyTimeFormat(time)
|
||||
{
|
||||
// Hours, minutes and seconds
|
||||
const hrs = ~~(time / 3600);
|
||||
const mins = ~~((time % 3600) / 60);
|
||||
const secs = ~~time % 60;
|
||||
|
||||
// Output like "1:01" or "4:03:59" or "123:03:59"
|
||||
let ret = '';
|
||||
|
||||
if (hrs > 0) {
|
||||
ret += '' + hrs + ':' + (mins < 10 ? '0' : '');
|
||||
}
|
||||
|
||||
ret += '' + mins + ':' + (secs < 10 ? '0' : '');
|
||||
ret += '' + secs;
|
||||
return ret;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<br/>
|
||||
<button class="back-button" (click)="goBack()" mat-icon-button><mat-icon>arrow_back</mat-icon></button>
|
||||
<div style="margin-bottom: 15px;">
|
||||
<h2 style="text-align: center;" *ngIf="subscription">
|
||||
{{subscription.name}}
|
||||
</h2>
|
||||
</div>
|
||||
<mat-divider style="width: 80%; margin: 0 auto"></mat-divider>
|
||||
<br/>
|
||||
|
||||
<div *ngIf="subscription">
|
||||
<div class="flex-grid">
|
||||
<div class="col"></div>
|
||||
<div class="col">
|
||||
<h4 style="text-align: center; margin-bottom: 20px;">Videos</h4>
|
||||
</div>
|
||||
<div class="col">
|
||||
<mat-form-field [ngClass]="searchIsFocused ? 'search-bar-focused' : 'search-bar-unfocused'" class="search-bar" color="accent">
|
||||
<input (focus)="searchIsFocused = true" (blur)="searchIsFocused = false" class="search-input" type="text" placeholder="Search" [(ngModel)]="search_text" (ngModelChange)="onSearchInputChanged($event)" matInput>
|
||||
<mat-icon matSuffix>search</mat-icon>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div *ngFor="let file of filtered_files" class="col-6 col-lg-4 mb-2 mt-2 sub-file-col">
|
||||
<app-subscription-file-card (reloadSubscription)="getSubscription()" (goToFileEmit)="goToFile($event)" [file]="file" [sub]="subscription" [use_youtubedl_archive]="use_youtubedl_archive"></app-subscription-file-card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,36 @@
|
||||
.sub-file-col {
|
||||
max-width: 240px;
|
||||
}
|
||||
|
||||
.back-button {
|
||||
float: left;
|
||||
position: absolute;
|
||||
left: 15px;
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
transition: all .5s ease;
|
||||
position: relative;
|
||||
float: right;
|
||||
}
|
||||
|
||||
.search-bar-unfocused {
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
transition: all .5s ease;
|
||||
}
|
||||
|
||||
.search-bar-focused {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.flex-grid {
|
||||
width: 100%;
|
||||
display: block;
|
||||
}
|
||||
.col {
|
||||
width: 33%;
|
||||
display: inline-block;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { SubscriptionComponent } from './subscription.component';
|
||||
|
||||
describe('SubscriptionComponent', () => {
|
||||
let component: SubscriptionComponent;
|
||||
let fixture: ComponentFixture<SubscriptionComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ SubscriptionComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(SubscriptionComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
75
src/app/subscription/subscription/subscription.component.ts
Normal file
75
src/app/subscription/subscription/subscription.component.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { PostsService } from 'app/posts.services';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
|
||||
@Component({
|
||||
selector: 'app-subscription',
|
||||
templateUrl: './subscription.component.html',
|
||||
styleUrls: ['./subscription.component.scss']
|
||||
})
|
||||
export class SubscriptionComponent implements OnInit {
|
||||
|
||||
id = null;
|
||||
subscription = null;
|
||||
files: any[] = null;
|
||||
filtered_files: any[] = null;
|
||||
use_youtubedl_archive = false;
|
||||
search_mode = false;
|
||||
search_text = '';
|
||||
searchIsFocused = false;
|
||||
|
||||
constructor(private postsService: PostsService, private route: ActivatedRoute, private router: Router) { }
|
||||
|
||||
ngOnInit() {
|
||||
if (this.route.snapshot.paramMap.get('id')) {
|
||||
this.id = this.route.snapshot.paramMap.get('id');
|
||||
|
||||
this.getSubscription();
|
||||
this.getConfig();
|
||||
}
|
||||
}
|
||||
|
||||
goBack() {
|
||||
this.router.navigate(['/subscriptions']);
|
||||
}
|
||||
|
||||
getSubscription() {
|
||||
this.postsService.getSubscription(this.id).subscribe(res => {
|
||||
this.subscription = res['subscription'];
|
||||
this.files = res['files'];
|
||||
if (this.search_mode) {
|
||||
this.filterFiles(this.search_text);
|
||||
} else {
|
||||
this.filtered_files = this.files;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getConfig() {
|
||||
this.postsService.loadNavItems().subscribe(res => {
|
||||
const result = !this.postsService.debugMode ? res['config_file'] : res;
|
||||
this.use_youtubedl_archive = result['YoutubeDLMaterial']['Subscriptions']['subscriptions_use_youtubedl_archive'];
|
||||
});
|
||||
}
|
||||
|
||||
goToFile(name) {
|
||||
localStorage.setItem('player_navigator', this.router.url);
|
||||
this.router.navigate(['/player', {fileNames: name, type: 'subscription', subscriptionName: this.subscription.name,
|
||||
subPlaylist: this.subscription.isPlaylist}]);
|
||||
}
|
||||
|
||||
onSearchInputChanged(newvalue) {
|
||||
if (newvalue.length > 0) {
|
||||
this.search_mode = true;
|
||||
this.filterFiles(newvalue);
|
||||
} else {
|
||||
this.search_mode = false;
|
||||
}
|
||||
}
|
||||
|
||||
private filterFiles(value: string) {
|
||||
const filterValue = value.toLowerCase();
|
||||
this.filtered_files = this.files.filter(option => option.id.toLowerCase().includes(filterValue));
|
||||
}
|
||||
|
||||
}
|
||||
56
src/app/subscriptions/subscriptions.component.html
Normal file
56
src/app/subscriptions/subscriptions.component.html
Normal file
@@ -0,0 +1,56 @@
|
||||
<br/>
|
||||
|
||||
<h2 style="text-align: center; margin-bottom: 15px;">Your subscriptions</h2>
|
||||
|
||||
<mat-divider style="width: 80%; margin: 0 auto"></mat-divider>
|
||||
<br/>
|
||||
|
||||
<h4 style="text-align: center;">Channels</h4>
|
||||
<mat-nav-list class="sub-nav-list">
|
||||
<mat-list-item *ngFor="let sub of channel_subscriptions">
|
||||
<a class="a-list-item" matLine (click)="goToSubscription(sub)" href="javascript:void(0)">
|
||||
<strong *ngIf="sub.name">{{ sub.name }}</strong>
|
||||
<div *ngIf="!sub.name">
|
||||
Name not available. Channel retrieval in progress.
|
||||
<ngx-content-loading *ngIf="false" [width]="200" [height]="20">
|
||||
<svg:g ngx-rect width="200" height="20" y="0" x="0" rx="4" ry="4"></svg:g>
|
||||
</ngx-content-loading>
|
||||
</div>
|
||||
</a>
|
||||
<button mat-icon-button (click)="showSubInfo(sub)">
|
||||
<mat-icon>info</mat-icon>
|
||||
</button>
|
||||
</mat-list-item>
|
||||
</mat-nav-list>
|
||||
|
||||
<div style="width: 80%; margin: 0 auto; padding-left: 15px;" *ngIf="channel_subscriptions.length === 0 && subscriptions">
|
||||
<p>You have no channel subscriptions.</p>
|
||||
</div>
|
||||
|
||||
<h4 style="text-align: center; margin-top: 10px;">Playlists</h4>
|
||||
<mat-nav-list class="sub-nav-list">
|
||||
<mat-list-item *ngFor="let sub of playlist_subscriptions">
|
||||
<a class="a-list-item" matLine (click)="goToSubscription(sub)" href="javascript:void(0)">
|
||||
<strong>{{ sub.name }}</strong>
|
||||
<div class="content-loading-div" *ngIf="!sub.name">
|
||||
Name not available. Playlist retrieval in progress.
|
||||
<ngx-content-loading *ngIf="false" [primaryColor]="postsService.theme.background_color" [secondaryColor]="postsService.theme.alternate_color" [width]="200" [height]="20">
|
||||
<svg:g ngx-rect width="200" height="20" y="0" x="0" rx="4" ry="4"></svg:g>
|
||||
</ngx-content-loading>
|
||||
</div>
|
||||
</a>
|
||||
<button mat-icon-button (click)="showSubInfo(sub)">
|
||||
<mat-icon>info</mat-icon>
|
||||
</button>
|
||||
</mat-list-item>
|
||||
</mat-nav-list>
|
||||
|
||||
<div style="width: 80%; margin: 0 auto; padding-left: 15px;" *ngIf="playlist_subscriptions.length === 0 && subscriptions">
|
||||
<p>You have no playlist subscriptions.</p>
|
||||
</div>
|
||||
|
||||
<div style="margin: 0 auto; width: 80%" *ngIf="subscriptions_loading">
|
||||
<mat-progress-bar mode="indeterminate"></mat-progress-bar>
|
||||
</div>
|
||||
|
||||
<button class="add-subscription-button" (click)="openSubscribeDialog()" mat-fab><mat-icon>add</mat-icon></button>
|
||||
27
src/app/subscriptions/subscriptions.component.scss
Normal file
27
src/app/subscriptions/subscriptions.component.scss
Normal file
@@ -0,0 +1,27 @@
|
||||
.add-subscription-button {
|
||||
position: fixed;
|
||||
bottom: 30px;
|
||||
right: 30px;
|
||||
}
|
||||
|
||||
.subscription-card {
|
||||
height: 200px;
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
.content-loading-div {
|
||||
position: absolute;
|
||||
width: 200px;
|
||||
height: 50px;
|
||||
bottom: -18px;
|
||||
}
|
||||
|
||||
.a-list-item {
|
||||
height: 48px;
|
||||
padding-top: 12px !important;
|
||||
}
|
||||
|
||||
.sub-nav-list {
|
||||
margin: 0 auto;
|
||||
width: 80%;
|
||||
}
|
||||
25
src/app/subscriptions/subscriptions.component.spec.ts
Normal file
25
src/app/subscriptions/subscriptions.component.spec.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { SubscriptionsComponent } from './subscriptions.component';
|
||||
|
||||
describe('SubscriptionsComponent', () => {
|
||||
let component: SubscriptionsComponent;
|
||||
let fixture: ComponentFixture<SubscriptionsComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ SubscriptionsComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(SubscriptionsComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
97
src/app/subscriptions/subscriptions.component.ts
Normal file
97
src/app/subscriptions/subscriptions.component.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
import { Component, OnInit, EventEmitter } from '@angular/core';
|
||||
import { MatDialog, MatSnackBar } from '@angular/material';
|
||||
import { SubscribeDialogComponent } from 'app/dialogs/subscribe-dialog/subscribe-dialog.component';
|
||||
import { PostsService } from 'app/posts.services';
|
||||
import { Router } from '@angular/router';
|
||||
import { SubscriptionInfoDialogComponent } from 'app/dialogs/subscription-info-dialog/subscription-info-dialog.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-subscriptions',
|
||||
templateUrl: './subscriptions.component.html',
|
||||
styleUrls: ['./subscriptions.component.scss']
|
||||
})
|
||||
export class SubscriptionsComponent implements OnInit {
|
||||
|
||||
playlist_subscriptions = [];
|
||||
channel_subscriptions = [];
|
||||
subscriptions = null;
|
||||
|
||||
subscriptions_loading = false;
|
||||
|
||||
constructor(private dialog: MatDialog, private postsService: PostsService, private router: Router, private snackBar: MatSnackBar) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.getSubscriptions();
|
||||
}
|
||||
|
||||
getSubscriptions() {
|
||||
this.subscriptions_loading = true;
|
||||
this.subscriptions = null;
|
||||
this.channel_subscriptions = [];
|
||||
this.playlist_subscriptions = [];
|
||||
this.postsService.getAllSubscriptions().subscribe(res => {
|
||||
this.subscriptions_loading = false;
|
||||
this.subscriptions = res['subscriptions'];
|
||||
|
||||
for (let i = 0; i < this.subscriptions.length; i++) {
|
||||
const sub = this.subscriptions[i];
|
||||
|
||||
// parse subscriptions into channels and playlists
|
||||
if (sub.isPlaylist) {
|
||||
this.playlist_subscriptions.push(sub);
|
||||
} else {
|
||||
this.channel_subscriptions.push(sub);
|
||||
}
|
||||
}
|
||||
}, err => {
|
||||
this.subscriptions_loading = false;
|
||||
console.error('Failed to get subscriptions');
|
||||
this.openSnackBar('ERROR: Failed to get subscriptions!', 'OK.');
|
||||
});
|
||||
}
|
||||
|
||||
goToSubscription(sub) {
|
||||
this.router.navigate(['/subscription', {id: sub.id}]);
|
||||
}
|
||||
|
||||
openSubscribeDialog() {
|
||||
const dialogRef = this.dialog.open(SubscribeDialogComponent, {
|
||||
maxWidth: 500,
|
||||
width: '80vw'
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe(result => {
|
||||
if (result) {
|
||||
if (result.isPlaylist) {
|
||||
this.playlist_subscriptions.push(result);
|
||||
} else {
|
||||
this.channel_subscriptions.push(result);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
showSubInfo(sub) {
|
||||
const unsubbedEmitter = new EventEmitter<any>();
|
||||
const dialogRef = this.dialog.open(SubscriptionInfoDialogComponent, {
|
||||
data: {
|
||||
sub: sub,
|
||||
unsubbedEmitter: unsubbedEmitter
|
||||
}
|
||||
});
|
||||
unsubbedEmitter.subscribe(success => {
|
||||
if (success) {
|
||||
this.openSnackBar(`${sub.name} successfully deleted!`)
|
||||
this.getSubscriptions();
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// snackbar helper
|
||||
public openSnackBar(message: string, action = '') {
|
||||
this.snackBar.open(message, action, {
|
||||
duration: 2000,
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,18 +1,18 @@
|
||||
{
|
||||
"YoutubeDLMaterial": {
|
||||
"Host": {
|
||||
"frontendurl": "http://localhost:4200",
|
||||
"backendurl": "http://localhost:17442/"
|
||||
},
|
||||
"Host": {
|
||||
"url": "http://localhost",
|
||||
"port": "17442"
|
||||
},
|
||||
"Encryption": {
|
||||
"use-encryption": false,
|
||||
"cert-file-path": "/etc/letsencrypt/live/example.com/fullchain.pem",
|
||||
"key-file-path": "/etc/letsencrypt/live/example.com/privkey.pem"
|
||||
},
|
||||
"Downloader": {
|
||||
"path-base": "http://localhost:17442/",
|
||||
"path-audio": "audio/",
|
||||
"path-video": "video/"
|
||||
"path-video": "video/",
|
||||
"custom_args": ""
|
||||
},
|
||||
"Extra": {
|
||||
"title_top": "Youtube Downloader",
|
||||
@@ -29,6 +29,12 @@
|
||||
"default_theme": "default",
|
||||
"allow_theme_change": true
|
||||
},
|
||||
"Subscriptions": {
|
||||
"allow_subscriptions": true,
|
||||
"subscriptions_base_path": "subscriptions/",
|
||||
"subscriptions_check_interval": "300",
|
||||
"subscriptions_use_youtubedl_archive": true
|
||||
},
|
||||
"Advanced": {
|
||||
"use_default_downloading_agent": true,
|
||||
"custom_downloading_agent": "",
|
||||
|
||||
BIN
src/favicon.ico
BIN
src/favicon.ico
Binary file not shown.
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 19 KiB |
@@ -1,5 +1,6 @@
|
||||
import { enableProdMode } from '@angular/core';
|
||||
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
|
||||
import 'hammerjs';
|
||||
|
||||
import { AppModule } from './app/app.module';
|
||||
import { environment } from './environments/environment';
|
||||
|
||||
@@ -2,12 +2,14 @@ const THEMES_CONFIG = {
|
||||
'default': {
|
||||
'key': 'default',
|
||||
'background_color': 'ghostwhite',
|
||||
'alternate_color': 'gray',
|
||||
'css_label': 'default-theme',
|
||||
'social_theme': 'material-light'
|
||||
},
|
||||
'dark': {
|
||||
'key': 'dark',
|
||||
'background_color': '#757575',
|
||||
'alternate_color': '#695959',
|
||||
'css_label': 'dark-theme',
|
||||
'social_theme': 'material-dark'
|
||||
},
|
||||
|
||||
BIN
youtubedl-material-chrome-extension.crx
Normal file
BIN
youtubedl-material-chrome-extension.crx
Normal file
Binary file not shown.
Reference in New Issue
Block a user