mirror of
https://github.com/Tzahi12345/YoutubeDL-Material.git
synced 2026-03-08 04:20:08 +03:00
Compare commits
33 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 |
6
.gitignore
vendored
6
.gitignore
vendored
@@ -48,5 +48,11 @@ 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
|
||||
|
||||
22
README.md
22
README.md
@@ -34,7 +34,7 @@ sudo apt-get install nodejs youtube-dl
|
||||
|
||||
1. First, download the [latest release](https://github.com/Tzahi12345/YoutubeDL-Material/releases/latest)!
|
||||
|
||||
2. Drag all the files in `youtubedl-material` to an easily accessible directory. Navigate to the `config` folder and edit the `default.json` file. If you're using SSL encryption, look at the `encrypted.json` file for a template.
|
||||
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.
|
||||
|
||||
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`.
|
||||
@@ -69,7 +69,7 @@ Here is an explanation for the configuration entries. Check out the [default con
|
||||
| allow_theme_change | true if you want the icon in the top toolbar that toggles dark mode | true |
|
||||
| use_default_downloading_agent | true if you want to use youtube-dl's default downloader | true |
|
||||
| custom_downloading_agent | If not using the default downloader, this is the downloader you want to use | "" |
|
||||
| allow_advanced_download | true if you want to use the Advanced download options - NOT FULLY IMPLEMENTED | false |
|
||||
| allow_advanced_download | true if you want to use the Advanced download options | false |
|
||||
|
||||
## Deployment
|
||||
|
||||
@@ -77,9 +77,9 @@ 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 the `public` directory in the `backend` folder.
|
||||
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 `backend` folder, and type `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 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.
|
||||
|
||||
@@ -93,6 +93,20 @@ If you are looking to setup YoutubeDL-Material with Docker, this section is for
|
||||
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
|
||||
|
||||
Feel free to submit a pull request! I have no guidelines as of yet, so no need to worry about that.
|
||||
|
||||
284
backend/app.js
284
backend/app.js
@@ -1,4 +1,5 @@
|
||||
var async = require('async');
|
||||
const { uuid } = require('uuidv4');
|
||||
var fs = require('fs');
|
||||
var path = require('path');
|
||||
var youtubedl = require('youtube-dl');
|
||||
@@ -10,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();
|
||||
|
||||
@@ -25,7 +28,8 @@ db.defaults(
|
||||
audio: [],
|
||||
video: []
|
||||
},
|
||||
configWriteFlag: false
|
||||
configWriteFlag: false,
|
||||
subscriptions: []
|
||||
}).write();
|
||||
|
||||
// config values
|
||||
@@ -39,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
|
||||
@@ -129,6 +135,9 @@ async function loadConfig() {
|
||||
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}\'`)
|
||||
}
|
||||
@@ -149,6 +158,15 @@ async function loadConfig() {
|
||||
|
||||
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();
|
||||
|
||||
@@ -157,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;
|
||||
}
|
||||
@@ -239,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'));
|
||||
@@ -368,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);
|
||||
|
||||
@@ -510,6 +562,20 @@ app.get('/api/config', function(req, res) {
|
||||
});
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
@@ -521,6 +587,7 @@ app.post('/api/tomp3', function(req, res) {
|
||||
|
||||
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;
|
||||
@@ -533,12 +600,6 @@ app.post('/api/tomp3', function(req, res) {
|
||||
if (customArgs) {
|
||||
downloadConfig = customArgs.split(' ');
|
||||
} else {
|
||||
if (customOutput) {
|
||||
downloadConfig = ['-o', audioFolderPath + customOutput + '.mp3', '-x', '--audio-format', 'mp3', '--write-info-json', '--print-json'];
|
||||
} else {
|
||||
downloadConfig = ['-o', audioFolderPath + audiopath + ".mp3", '-x', '--audio-format', 'mp3', '--write-info-json', '--print-json'];
|
||||
}
|
||||
|
||||
if (customQualityConfiguration) {
|
||||
qualityPath = `-f ${customQualityConfiguration}`;
|
||||
} else if (maxBitrate) {
|
||||
@@ -546,6 +607,12 @@ app.post('/api/tomp3', function(req, res) {
|
||||
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 (youtubeUsername && youtubePassword) {
|
||||
downloadConfig.push('--username', youtubeUsername, '--password', youtubePassword);
|
||||
}
|
||||
@@ -557,6 +624,11 @@ app.post('/api/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) {
|
||||
@@ -607,6 +679,7 @@ app.post('/api/tomp4', function(req, res) {
|
||||
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;
|
||||
|
||||
@@ -621,18 +694,18 @@ app.post('/api/tomp4', function(req, res) {
|
||||
if (customArgs) {
|
||||
downloadConfig = customArgs.split(' ');
|
||||
} else {
|
||||
if (customOutput) {
|
||||
downloadConfig = ['-o', path + customOutput + ".mp4", '-f', qualityPath, '--write-info-json', '--print-json'];
|
||||
} else {
|
||||
downloadConfig = ['-o', path + videopath + ".mp4", '-f', qualityPath, '--write-info-json', '--print-json'];
|
||||
}
|
||||
|
||||
if (customQualityConfiguration) {
|
||||
qualityPath = customQualityConfiguration;
|
||||
} else if (selectedHeight && selectedHeight !== '') {
|
||||
qualityPath = `bestvideo[height=${selectedHeight}]+bestaudio/best[height=${selectedHeight}]`;
|
||||
}
|
||||
|
||||
if (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 (youtubeUsername && youtubePassword) {
|
||||
downloadConfig.push('--username', youtubeUsername, '--password', youtubePassword);
|
||||
}
|
||||
@@ -640,6 +713,11 @@ app.post('/api/tomp4', 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) {
|
||||
@@ -697,7 +775,7 @@ app.post('/api/tomp4', function(req, res) {
|
||||
|
||||
// gets the status of the mp3 file that's being downloaded
|
||||
app.post('/api/fileStatusMp3', function(req, res) {
|
||||
var name = decodeURI(req.body.name + "");
|
||||
var name = decodeURIComponent(req.body.name + "");
|
||||
var exists = "";
|
||||
var fullpath = audioFolderPath + name + ".mp3";
|
||||
if (fs.existsSync(fullpath)) {
|
||||
@@ -719,7 +797,7 @@ app.post('/api/fileStatusMp3', function(req, res) {
|
||||
|
||||
// gets the status of the mp4 file that's being downloaded
|
||||
app.post('/api/fileStatusMp4', function(req, res) {
|
||||
var name = decodeURI(req.body.name);
|
||||
var name = decodeURIComponent(req.body.name);
|
||||
var exists = "";
|
||||
var fullpath = videoFolderPath + name + ".mp4";
|
||||
if (fs.existsSync(fullpath)) {
|
||||
@@ -802,6 +880,141 @@ app.post('/api/getMp4s', function(req, res) {
|
||||
res.end("yes");
|
||||
});
|
||||
|
||||
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;
|
||||
@@ -919,7 +1132,7 @@ app.post('/api/downloadFile', async (req, res) => {
|
||||
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') {
|
||||
@@ -927,7 +1140,7 @@ app.post('/api/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);
|
||||
}
|
||||
@@ -946,10 +1159,31 @@ app.post('/api/deleteFile', async (req, res) => {
|
||||
res.send()
|
||||
});
|
||||
|
||||
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
|
||||
@@ -988,7 +1222,7 @@ app.get('/api/video/: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)
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
@@ -109,5 +113,6 @@ module.exports = {
|
||||
setConfigItem: setConfigItem,
|
||||
setConfigItems: setConfigItems,
|
||||
getConfigFile: getConfigFile,
|
||||
setConfigFile: setConfigFile,
|
||||
CONFIG_ITEMS: CONFIG_ITEMS
|
||||
}
|
||||
@@ -11,7 +11,8 @@
|
||||
},
|
||||
"Downloader": {
|
||||
"path-audio": "audio/",
|
||||
"path-video": "video/"
|
||||
"path-video": "video/",
|
||||
"custom_args": ""
|
||||
},
|
||||
"Extra": {
|
||||
"title_top": "Youtube Downloader",
|
||||
@@ -28,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": "",
|
||||
|
||||
@@ -11,7 +11,8 @@
|
||||
},
|
||||
"Downloader": {
|
||||
"path-audio": "audio/",
|
||||
"path-video": "video/"
|
||||
"path-video": "video/",
|
||||
"custom_args": ""
|
||||
},
|
||||
"Extra": {
|
||||
"title_top": "Youtube Downloader",
|
||||
@@ -28,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": "",
|
||||
|
||||
@@ -34,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': {
|
||||
@@ -56,7 +60,6 @@ let CONFIG_ITEMS = {
|
||||
'key': 'ytdl_allow_multi_download_mode',
|
||||
'path': 'YoutubeDLMaterial.Extra.allow_multi_download_mode'
|
||||
},
|
||||
|
||||
|
||||
// API
|
||||
'ytdl_use_youtube_api': {
|
||||
@@ -78,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',
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
"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.
Binary file not shown.
BIN
chrome-extension/youtubedl-material-chrome-extension.zip
Normal file
BIN
chrome-extension/youtubedl-material-chrome-extension.zip
Normal file
Binary file not shown.
@@ -12,6 +12,7 @@ services:
|
||||
ytdl_key_file_path: /etc/letsencrypt/live/example.com/privkey.pem
|
||||
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'
|
||||
@@ -21,6 +22,10 @@ services:
|
||||
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,4 +35,4 @@ services:
|
||||
restart: always
|
||||
ports:
|
||||
- "8998:17442"
|
||||
image: tzahi12345/youtubedl-material:3.3
|
||||
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,12 +34,27 @@ 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(res => { // loads settings
|
||||
const result = !this.postsService.debugMode ? res['config_file'] : res;
|
||||
@@ -46,6 +62,7 @@ export class AppComponent implements OnInit {
|
||||
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')) {
|
||||
@@ -57,6 +74,10 @@ export class AppComponent implements OnInit {
|
||||
|
||||
}
|
||||
|
||||
toggleSidenav() {
|
||||
this.sidenav.toggle();
|
||||
}
|
||||
|
||||
// theme stuff
|
||||
|
||||
setTheme(theme) {
|
||||
@@ -105,6 +126,11 @@ onSetTheme(theme, old_theme) {
|
||||
}
|
||||
}
|
||||
|
||||
themeMenuItemClicked(event) {
|
||||
this.flipTheme();
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
if (localStorage.getItem('theme')) {
|
||||
this.setTheme(localStorage.getItem('theme'));
|
||||
@@ -115,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');
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -69,6 +69,7 @@
|
||||
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-12 col-sm-6">
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Component, OnInit, ElementRef, ViewChild, ViewChildren, QueryList, isDevMode } from '@angular/core';
|
||||
import { Component, OnInit, ElementRef, ViewChild, ViewChildren, QueryList } from '@angular/core';
|
||||
import {PostsService} from '../posts.services';
|
||||
import {FileCardComponent} from '../file-card/file-card.component';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
@@ -45,6 +45,7 @@ export class MainComponent implements OnInit {
|
||||
|
||||
iOS = false;
|
||||
|
||||
// local settings
|
||||
determinateProgress = false;
|
||||
downloadingfile = false;
|
||||
audioOnly: boolean;
|
||||
@@ -63,15 +64,19 @@ export class MainComponent implements OnInit {
|
||||
percentDownloaded: number;
|
||||
autoStartDownload = false;
|
||||
|
||||
// settings
|
||||
// global settings
|
||||
fileManagerEnabled = false;
|
||||
allowQualitySelect = false;
|
||||
downloadOnlyMode = false;
|
||||
allowMultiDownloadMode = false;
|
||||
audioFolderPath;
|
||||
videoFolderPath;
|
||||
globalCustomArgs = null;
|
||||
allowAdvancedDownload = false;
|
||||
useDefaultDownloadingAgent = true;
|
||||
customDownloadingAgent = null;
|
||||
|
||||
// formats cache
|
||||
cachedAvailableFormats = {};
|
||||
|
||||
// youtube api
|
||||
@@ -202,6 +207,8 @@ 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 route: ActivatedRoute) {
|
||||
this.audioOnly = false;
|
||||
@@ -214,11 +221,15 @@ export class MainComponent implements OnInit {
|
||||
this.allowMultiDownloadMode = result['YoutubeDLMaterial']['Extra']['allow_multi_download_mode'];
|
||||
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'];
|
||||
|
||||
|
||||
|
||||
if (this.fileManagerEnabled) {
|
||||
@@ -259,6 +270,8 @@ export class MainComponent implements OnInit {
|
||||
this.downloadClicked();
|
||||
}
|
||||
|
||||
setInterval(() => this.getSimulatedOutput(), 1000);
|
||||
|
||||
}, error => {
|
||||
console.log(error);
|
||||
});
|
||||
@@ -372,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}]);
|
||||
}
|
||||
@@ -444,6 +458,7 @@ 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'}]);
|
||||
} else {
|
||||
@@ -481,6 +496,7 @@ 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'}]);
|
||||
} else {
|
||||
@@ -544,11 +560,7 @@ 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();
|
||||
}
|
||||
|
||||
this.postsService.makeMP3(this.url, (this.selectedQuality === '' ? null : this.selectedQuality),
|
||||
@@ -581,14 +593,7 @@ 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 customQualityConfiguration = this.getSelectedVideoFormat();
|
||||
|
||||
this.postsService.makeMP4(this.url, (this.selectedQuality === '' ? null : this.selectedQuality),
|
||||
customQualityConfiguration, customArgs, customOutput, youtubeUsername, youtubePassword).subscribe(posts => {
|
||||
@@ -629,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) {
|
||||
@@ -771,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);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
.video-player {
|
||||
margin: 0 auto;
|
||||
min-width: 300px;
|
||||
}
|
||||
|
||||
.video-player:focus {
|
||||
|
||||
@@ -31,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;
|
||||
@@ -52,6 +55,8 @@ 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(res => { // loads settings
|
||||
@@ -59,6 +64,7 @@ export class PlayerComponent implements OnInit {
|
||||
this.baseStreamPath = this.postsService.path;
|
||||
this.audioFolderPath = result['YoutubeDLMaterial']['Downloader']['path-audio'];
|
||||
this.videoFolderPath = result['YoutubeDLMaterial']['Downloader']['path-video'];
|
||||
this.subscriptionFolderPath = result['YoutubeDLMaterial']['Subscriptions']['subscriptions_base_path'];
|
||||
|
||||
|
||||
let fileType = null;
|
||||
@@ -66,15 +72,27 @@ export class PlayerComponent implements OnInit {
|
||||
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);
|
||||
|
||||
@@ -92,6 +92,10 @@ export class PostsService {
|
||||
}
|
||||
}
|
||||
|
||||
setConfig(config) {
|
||||
return this.http.post(this.path + 'setConfig', {new_config_file: config});
|
||||
}
|
||||
|
||||
deleteFile(name: string, isAudio: boolean) {
|
||||
if (isAudio) {
|
||||
return this.http.post(this.path + 'deleteMp3', {name: name});
|
||||
@@ -116,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});
|
||||
}
|
||||
@@ -136,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,
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -11,7 +11,8 @@
|
||||
},
|
||||
"Downloader": {
|
||||
"path-audio": "audio/",
|
||||
"path-video": "video/"
|
||||
"path-video": "video/",
|
||||
"custom_args": ""
|
||||
},
|
||||
"Extra": {
|
||||
"title_top": "Youtube Downloader",
|
||||
@@ -28,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