mirror of
https://github.com/Tzahi12345/YoutubeDL-Material.git
synced 2026-03-07 20:10:03 +03:00
Merge pull request #236 from Tzahi12345/categories
Adds rule-based categories
This commit is contained in:
263
backend/app.js
263
backend/app.js
@@ -26,6 +26,7 @@ const shortid = require('shortid')
|
||||
const url_api = require('url');
|
||||
var config_api = require('./config.js');
|
||||
var subscriptions_api = require('./subscriptions')
|
||||
var categories_api = require('./categories');
|
||||
const CONSTS = require('./consts')
|
||||
const { spawn } = require('child_process')
|
||||
const read_last_lines = require('read-last-lines');
|
||||
@@ -36,7 +37,7 @@ const is_windows = process.platform === 'win32';
|
||||
var app = express();
|
||||
|
||||
// database setup
|
||||
const FileSync = require('lowdb/adapters/FileSync')
|
||||
const FileSync = require('lowdb/adapters/FileSync');
|
||||
|
||||
const adapter = new FileSync('./appdata/db.json');
|
||||
const db = low(adapter)
|
||||
@@ -79,8 +80,7 @@ config_api.initialize(logger);
|
||||
auth_api.initialize(users_db, logger);
|
||||
db_api.initialize(db, users_db, logger);
|
||||
subscriptions_api.initialize(db, users_db, logger, db_api);
|
||||
|
||||
// var GithubContent = require('github-content');
|
||||
categories_api.initialize(db, users_db, logger, db_api);
|
||||
|
||||
// Set some defaults
|
||||
db.defaults(
|
||||
@@ -173,7 +173,6 @@ const subscription_timeouts = {};
|
||||
// don't overwrite config if it already happened.. NOT
|
||||
// let alreadyWritten = db.get('configWriteFlag').value();
|
||||
let writeConfigMode = process.env.write_ytdl_config;
|
||||
var config = null;
|
||||
|
||||
// checks if config exists, if not, a config is auto generated
|
||||
config_api.configExistsCheck();
|
||||
@@ -1077,6 +1076,7 @@ async function downloadFileByURL_exec(url, type, options, sessionID = null) {
|
||||
var is_audio = type === 'audio';
|
||||
var ext = is_audio ? '.mp3' : '.mp4';
|
||||
var fileFolderPath = type === 'audio' ? audioFolderPath : videoFolderPath;
|
||||
let category = null;
|
||||
|
||||
// prepend with user if needed
|
||||
let multiUserMode = null;
|
||||
@@ -1093,7 +1093,7 @@ async function downloadFileByURL_exec(url, type, options, sessionID = null) {
|
||||
}
|
||||
|
||||
options.downloading_method = 'exec';
|
||||
const downloadConfig = await generateArgs(url, type, options);
|
||||
let downloadConfig = await generateArgs(url, type, options);
|
||||
|
||||
// adds download to download helper
|
||||
const download_uid = uuid();
|
||||
@@ -1115,11 +1115,22 @@ async function downloadFileByURL_exec(url, type, options, sessionID = null) {
|
||||
updateDownloads();
|
||||
|
||||
// get video info prior to download
|
||||
const info = await getVideoInfoByURL(url, downloadConfig, download);
|
||||
let info = await getVideoInfoByURL(url, downloadConfig, download);
|
||||
if (!info) {
|
||||
resolve(false);
|
||||
return;
|
||||
} else {
|
||||
// check if it fits into a category. If so, then get info again using new downloadConfig
|
||||
category = await categories_api.categorize(info);
|
||||
|
||||
// set custom output if the category has one and re-retrieve info so the download manager has the right file name
|
||||
if (category && category['custom_output']) {
|
||||
options.customOutput = category['custom_output'];
|
||||
options.noRelativePath = true;
|
||||
downloadConfig = await generateArgs(url, type, options);
|
||||
info = await getVideoInfoByURL(url, downloadConfig, download);
|
||||
}
|
||||
|
||||
// store info in download for future use
|
||||
download['_filename'] = info['_filename'];
|
||||
download['filesize'] = utils.getExpectedFileSize(info);
|
||||
@@ -1161,7 +1172,7 @@ async function downloadFileByURL_exec(url, type, options, sessionID = null) {
|
||||
} catch(e) {
|
||||
output_json = null;
|
||||
}
|
||||
var modified_file_name = output_json ? output_json['title'] : null;
|
||||
|
||||
if (!output_json) {
|
||||
continue;
|
||||
}
|
||||
@@ -1190,8 +1201,11 @@ async function downloadFileByURL_exec(url, type, options, sessionID = null) {
|
||||
if (!success) logger.error('Failed to apply ID3 tag to audio file ' + output_json['_filename']);
|
||||
}
|
||||
|
||||
const file_path = options.noRelativePath ? path.basename(full_file_path) : full_file_path.substring(fileFolderPath.length, full_file_path.length);
|
||||
const customPath = options.noRelativePath ? path.dirname(full_file_path).split(path.sep).pop() : null;
|
||||
|
||||
// registers file in DB
|
||||
file_uid = db_api.registerFileDB(full_file_path.substring(fileFolderPath.length, full_file_path.length), type, multiUserMode);
|
||||
file_uid = db_api.registerFileDB(file_path, type, multiUserMode, null, customPath);
|
||||
|
||||
if (file_name) file_names.push(file_name);
|
||||
}
|
||||
@@ -1406,7 +1420,8 @@ async function generateArgs(url, type, options) {
|
||||
}
|
||||
|
||||
if (customOutput) {
|
||||
downloadConfig = ['-o', path.join(fileFolderPath, customOutput) + ".%(ext)s", '--write-info-json', '--print-json'];
|
||||
customOutput = options.noRelativePath ? customOutput : path.join(fileFolderPath, customOutput);
|
||||
downloadConfig = ['-o', `${customOutput}.%(ext)s`, '--write-info-json', '--print-json'];
|
||||
} else {
|
||||
downloadConfig = ['-o', path.join(fileFolderPath, videopath + (is_audio ? '.%(ext)s' : '.mp4')), '--write-info-json', '--print-json'];
|
||||
}
|
||||
@@ -1715,13 +1730,9 @@ app.use(function(req, res, next) {
|
||||
next();
|
||||
} else if (req.query.apiKey === admin_token) {
|
||||
next();
|
||||
} else if (req.query.apiKey && config_api.getConfigItem('ytdl_use_api_key')) {
|
||||
if (req.query.apiKey === config_api.getConfigItem('ytdl_api_key')) {
|
||||
next();
|
||||
} else {
|
||||
res.status(401).send('Invalid API key');
|
||||
}
|
||||
} else if (req.path.includes('/api/video/') || req.path.includes('/api/audio/')) {
|
||||
} else if (req.query.apiKey && config_api.getConfigItem('ytdl_use_api_key') && req.query.apiKey === config_api.getConfigItem('ytdl_api_key')) {
|
||||
next();
|
||||
} else if (req.path.includes('/api/stream/')) {
|
||||
next();
|
||||
} else {
|
||||
logger.verbose(`Rejecting request - invalid API use for endpoint: ${req.path}. API key received: ${req.query.apiKey}`);
|
||||
@@ -1734,8 +1745,7 @@ app.use(compression());
|
||||
const optionalJwt = function (req, res, next) {
|
||||
const multiUserMode = config_api.getConfigItem('ytdl_multi_user_mode');
|
||||
if (multiUserMode && ((req.body && req.body.uuid) || (req.query && req.query.uuid)) && (req.path.includes('/api/getFile') ||
|
||||
req.path.includes('/api/audio') ||
|
||||
req.path.includes('/api/video') ||
|
||||
req.path.includes('/api/stream') ||
|
||||
req.path.includes('/api/downloadFile'))) {
|
||||
// check if shared video
|
||||
const using_body = req.body && req.body.uuid;
|
||||
@@ -1875,8 +1885,11 @@ app.get('/api/getMp3s', optionalJwt, async function(req, res) {
|
||||
|
||||
mp3s = JSON.parse(JSON.stringify(mp3s));
|
||||
|
||||
// add thumbnails if present
|
||||
await addThumbnails(mp3s);
|
||||
if (config_api.getConfigItem('ytdl_include_thumbnail')) {
|
||||
// add thumbnails if present
|
||||
await addThumbnails(mp3s);
|
||||
}
|
||||
|
||||
|
||||
res.send({
|
||||
mp3s: mp3s,
|
||||
@@ -1899,8 +1912,10 @@ app.get('/api/getMp4s', optionalJwt, async function(req, res) {
|
||||
|
||||
mp4s = JSON.parse(JSON.stringify(mp4s));
|
||||
|
||||
// add thumbnails if present
|
||||
await addThumbnails(mp4s);
|
||||
if (config_api.getConfigItem('ytdl_include_thumbnail')) {
|
||||
// add thumbnails if present
|
||||
await addThumbnails(mp4s);
|
||||
}
|
||||
|
||||
res.send({
|
||||
mp4s: mp4s,
|
||||
@@ -1988,8 +2003,10 @@ app.post('/api/getAllFiles', optionalJwt, async function (req, res) {
|
||||
|
||||
files = JSON.parse(JSON.stringify(files));
|
||||
|
||||
// add thumbnails if present
|
||||
await addThumbnails(files);
|
||||
if (config_api.getConfigItem('ytdl_include_thumbnail')) {
|
||||
// add thumbnails if present
|
||||
await addThumbnails(files);
|
||||
}
|
||||
|
||||
res.send({
|
||||
files: files,
|
||||
@@ -2084,6 +2101,54 @@ app.post('/api/disableSharing', optionalJwt, function(req, res) {
|
||||
});
|
||||
});
|
||||
|
||||
// categories
|
||||
|
||||
app.post('/api/getAllCategories', optionalJwt, async (req, res) => {
|
||||
const categories = db.get('categories').value();
|
||||
res.send({categories: categories});
|
||||
});
|
||||
|
||||
app.post('/api/createCategory', optionalJwt, async (req, res) => {
|
||||
const name = req.body.name;
|
||||
const new_category = {
|
||||
name: name,
|
||||
uid: uuid(),
|
||||
rules: [],
|
||||
custom_putput: ''
|
||||
};
|
||||
|
||||
db.get('categories').push(new_category).write();
|
||||
|
||||
res.send({
|
||||
new_category: new_category,
|
||||
success: !!new_category
|
||||
});
|
||||
});
|
||||
|
||||
app.post('/api/deleteCategory', optionalJwt, async (req, res) => {
|
||||
const category_uid = req.body.category_uid;
|
||||
|
||||
db.get('categories').remove({uid: category_uid}).write();
|
||||
|
||||
res.send({
|
||||
success: true
|
||||
});
|
||||
});
|
||||
|
||||
app.post('/api/updateCategory', optionalJwt, async (req, res) => {
|
||||
const category = req.body.category;
|
||||
db.get('categories').find({uid: category.uid}).assign(category).write();
|
||||
res.send({success: true});
|
||||
});
|
||||
|
||||
app.post('/api/updateCategories', optionalJwt, async (req, res) => {
|
||||
const categories = req.body.categories;
|
||||
db.get('categories').assign(categories).write();
|
||||
res.send({success: true});
|
||||
});
|
||||
|
||||
// subscriptions
|
||||
|
||||
app.post('/api/subscribe', optionalJwt, async (req, res) => {
|
||||
let name = req.body.name;
|
||||
let url = req.body.url;
|
||||
@@ -2168,10 +2233,17 @@ app.post('/api/deleteSubscriptionFile', optionalJwt, async (req, res) => {
|
||||
|
||||
app.post('/api/getSubscription', optionalJwt, async (req, res) => {
|
||||
let subID = req.body.id;
|
||||
let subName = req.body.name; // if included, subID is optional
|
||||
|
||||
let user_uid = req.isAuthenticated() ? req.user.uid : null;
|
||||
|
||||
// get sub from db
|
||||
let subscription = subscriptions_api.getSubscription(subID, user_uid);
|
||||
let subscription = null;
|
||||
if (subID) {
|
||||
subscription = subscriptions_api.getSubscription(subID, user_uid)
|
||||
} else if (subName) {
|
||||
subscription = subscriptions_api.getSubscriptionByName(subName, user_uid)
|
||||
}
|
||||
|
||||
if (!subscription) {
|
||||
// failed to get subscription from db, send 400 error
|
||||
@@ -2401,56 +2473,25 @@ app.post('/api/deletePlaylist', optionalJwt, async (req, res) => {
|
||||
})
|
||||
});
|
||||
|
||||
// deletes mp3 file
|
||||
app.post('/api/deleteMp3', optionalJwt, async (req, res) => {
|
||||
// var name = req.body.name;
|
||||
// deletes non-subscription files
|
||||
app.post('/api/deleteFile', optionalJwt, async (req, res) => {
|
||||
var uid = req.body.uid;
|
||||
var type = req.body.type;
|
||||
var blacklistMode = req.body.blacklistMode;
|
||||
|
||||
if (req.isAuthenticated()) {
|
||||
let success = await auth_api.deleteUserFile(req.user.uid, uid, 'audio', blacklistMode);
|
||||
let success = auth_api.deleteUserFile(req.user.uid, uid, type, blacklistMode);
|
||||
res.send(success);
|
||||
return;
|
||||
}
|
||||
|
||||
var audio_obj = db.get('files.audio').find({uid: uid}).value();
|
||||
var name = audio_obj.id;
|
||||
var fullpath = audioFolderPath + name + ".mp3";
|
||||
var file_obj = db.get(`files.${type}`).find({uid: uid}).value();
|
||||
var name = file_obj.id;
|
||||
var fullpath = file_obj ? file_obj.path : null;
|
||||
var wasDeleted = false;
|
||||
if (await fs.pathExists(fullpath))
|
||||
{
|
||||
deleteAudioFile(name, null, blacklistMode);
|
||||
db.get('files.audio').remove({uid: uid}).write();
|
||||
wasDeleted = true;
|
||||
res.send(wasDeleted);
|
||||
} else if (audio_obj) {
|
||||
db.get('files.audio').remove({uid: uid}).write();
|
||||
wasDeleted = true;
|
||||
res.send(wasDeleted);
|
||||
} else {
|
||||
wasDeleted = false;
|
||||
res.send(wasDeleted);
|
||||
}
|
||||
});
|
||||
|
||||
// deletes mp4 file
|
||||
app.post('/api/deleteMp4', optionalJwt, async (req, res) => {
|
||||
var uid = req.body.uid;
|
||||
var blacklistMode = req.body.blacklistMode;
|
||||
|
||||
if (req.isAuthenticated()) {
|
||||
let success = await auth_api.deleteUserFile(req.user.uid, uid, 'video', blacklistMode);
|
||||
res.send(success);
|
||||
return;
|
||||
}
|
||||
|
||||
var video_obj = db.get('files.video').find({uid: uid}).value();
|
||||
var name = video_obj.id;
|
||||
var fullpath = videoFolderPath + name + ".mp4";
|
||||
var wasDeleted = false;
|
||||
if (await fs.pathExists(fullpath))
|
||||
{
|
||||
wasDeleted = await deleteVideoFile(name, null, blacklistMode);
|
||||
wasDeleted = type === 'audio' ? await deleteAudioFile(name, path.basename(fullpath), blacklistMode) : await deleteVideoFile(name, path.basename(fullpath), blacklistMode);
|
||||
db.get('files.video').remove({uid: uid}).write();
|
||||
// wasDeleted = true;
|
||||
res.send(wasDeleted);
|
||||
@@ -2517,17 +2558,6 @@ app.post('/api/downloadFile', optionalJwt, async (req, res) => {
|
||||
});
|
||||
});
|
||||
|
||||
app.post('/api/deleteFile', async (req, res) => {
|
||||
let fileName = req.body.fileName;
|
||||
let type = req.body.type;
|
||||
if (type === 'audio') {
|
||||
deleteAudioFile(fileName);
|
||||
} else if (type === 'video') {
|
||||
deleteVideoFile(fileName);
|
||||
}
|
||||
res.send({});
|
||||
});
|
||||
|
||||
app.post('/api/downloadArchive', async (req, res) => {
|
||||
let sub = req.body.sub;
|
||||
let archive_dir = sub.archive;
|
||||
@@ -2595,25 +2625,33 @@ app.post('/api/generateNewAPIKey', function (req, res) {
|
||||
|
||||
// Streaming API calls
|
||||
|
||||
app.get('/api/video/:id', optionalJwt, function(req , res){
|
||||
app.get('/api/stream/:id', optionalJwt, (req, res) => {
|
||||
const type = req.query.type;
|
||||
const ext = type === 'audio' ? '.mp3' : '.mp4';
|
||||
const mimetype = type === 'audio' ? 'audio/mp3' : 'video/mp4';
|
||||
var head;
|
||||
let optionalParams = url_api.parse(req.url,true).query;
|
||||
let id = decodeURIComponent(req.params.id);
|
||||
let file_path = videoFolderPath + id + '.mp4';
|
||||
if (req.isAuthenticated() || req.can_watch) {
|
||||
let file_path = req.query.file_path ? decodeURIComponent(req.query.file_path) : null;
|
||||
if (!file_path && (req.isAuthenticated() || req.can_watch)) {
|
||||
let usersFileFolder = config_api.getConfigItem('ytdl_users_base_path');
|
||||
if (optionalParams['subName']) {
|
||||
const isPlaylist = optionalParams['subPlaylist'];
|
||||
file_path = path.join(usersFileFolder, req.user.uid, 'subscriptions', (isPlaylist === 'true' ? 'playlists/' : 'channels/'),optionalParams['subName'], id + '.mp4')
|
||||
file_path = path.join(usersFileFolder, req.user.uid, 'subscriptions', (isPlaylist === 'true' ? 'playlists/' : 'channels/'),optionalParams['subName'], id + ext)
|
||||
} else {
|
||||
file_path = path.join(usersFileFolder, req.query.uuid ? req.query.uuid : req.user.uid, 'video', id + '.mp4');
|
||||
file_path = path.join(usersFileFolder, req.query.uuid ? req.query.uuid : req.user.uid, type, id + ext);
|
||||
}
|
||||
} else if (optionalParams['subName']) {
|
||||
} else if (!file_path && optionalParams['subName']) {
|
||||
let basePath = config_api.getConfigItem('ytdl_subscriptions_base_path');
|
||||
const isPlaylist = optionalParams['subPlaylist'];
|
||||
basePath += (isPlaylist === 'true' ? 'playlists/' : 'channels/');
|
||||
file_path = basePath + optionalParams['subName'] + '/' + id + '.mp4';
|
||||
file_path = basePath + optionalParams['subName'] + '/' + id + ext;
|
||||
}
|
||||
|
||||
if (!file_path) {
|
||||
file_path = path.join(videoFolderPath, id + ext);
|
||||
}
|
||||
|
||||
const stat = fs.statSync(file_path)
|
||||
const fileSize = stat.size
|
||||
const range = req.headers.range
|
||||
@@ -2636,77 +2674,20 @@ app.get('/api/video/:id', optionalJwt, function(req , res){
|
||||
'Content-Range': `bytes ${start}-${end}/${fileSize}`,
|
||||
'Accept-Ranges': 'bytes',
|
||||
'Content-Length': chunksize,
|
||||
'Content-Type': 'video/mp4',
|
||||
'Content-Type': mimetype,
|
||||
}
|
||||
res.writeHead(206, head);
|
||||
file.pipe(res);
|
||||
} else {
|
||||
head = {
|
||||
'Content-Length': fileSize,
|
||||
'Content-Type': 'video/mp4',
|
||||
'Content-Type': mimetype,
|
||||
}
|
||||
res.writeHead(200, head)
|
||||
fs.createReadStream(file_path).pipe(res)
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/audio/:id', optionalJwt, function(req , res){
|
||||
var head;
|
||||
let id = decodeURIComponent(req.params.id);
|
||||
let file_path = "audio/" + id + '.mp3';
|
||||
let usersFileFolder = config_api.getConfigItem('ytdl_users_base_path');
|
||||
let optionalParams = url_api.parse(req.url,true).query;
|
||||
if (req.isAuthenticated()) {
|
||||
if (optionalParams['subName']) {
|
||||
const isPlaylist = optionalParams['subPlaylist'];
|
||||
file_path = path.join(usersFileFolder, req.user.uid, 'subscriptions', (isPlaylist === 'true' ? 'playlists/' : 'channels/'),optionalParams['subName'], id + '.mp3')
|
||||
} else {
|
||||
let usersFileFolder = config_api.getConfigItem('ytdl_users_base_path');
|
||||
file_path = path.join(usersFileFolder, req.user.uid, 'audio', id + '.mp3');
|
||||
}
|
||||
} else if (optionalParams['subName']) {
|
||||
let basePath = config_api.getConfigItem('ytdl_subscriptions_base_path');
|
||||
const isPlaylist = optionalParams['subPlaylist'];
|
||||
basePath += (isPlaylist === 'true' ? 'playlists/' : 'channels/');
|
||||
file_path = basePath + optionalParams['subName'] + '/' + id + '.mp3';
|
||||
}
|
||||
file_path = file_path.replace(/\"/g, '\'');
|
||||
const stat = fs.statSync(file_path)
|
||||
const fileSize = stat.size
|
||||
const range = req.headers.range
|
||||
if (range) {
|
||||
const parts = range.replace(/bytes=/, "").split("-")
|
||||
const start = parseInt(parts[0], 10)
|
||||
const end = parts[1]
|
||||
? parseInt(parts[1], 10)
|
||||
: fileSize-1
|
||||
const chunksize = (end-start)+1
|
||||
const file = fs.createReadStream(file_path, {start, end});
|
||||
if (config_api.descriptors[id]) config_api.descriptors[id].push(file);
|
||||
else config_api.descriptors[id] = [file];
|
||||
file.on('close', function() {
|
||||
let index = config_api.descriptors[id].indexOf(file);
|
||||
config_api.descriptors[id].splice(index, 1);
|
||||
logger.debug('Successfully closed stream and removed file reference.');
|
||||
});
|
||||
head = {
|
||||
'Content-Range': `bytes ${start}-${end}/${fileSize}`,
|
||||
'Accept-Ranges': 'bytes',
|
||||
'Content-Length': chunksize,
|
||||
'Content-Type': 'audio/mp3',
|
||||
}
|
||||
res.writeHead(206, head);
|
||||
file.pipe(res);
|
||||
} else {
|
||||
head = {
|
||||
'Content-Length': fileSize,
|
||||
'Content-Type': 'audio/mp3',
|
||||
}
|
||||
res.writeHead(200, head)
|
||||
fs.createReadStream(file_path).pipe(res)
|
||||
}
|
||||
});
|
||||
|
||||
// Downloads management
|
||||
|
||||
app.get('/api/downloads', async (req, res) => {
|
||||
|
||||
123
backend/categories.js
Normal file
123
backend/categories.js
Normal file
@@ -0,0 +1,123 @@
|
||||
const config_api = require('./config');
|
||||
|
||||
var logger = null;
|
||||
var db = null;
|
||||
var users_db = null;
|
||||
var db_api = null;
|
||||
|
||||
function setDB(input_db, input_users_db, input_db_api) { db = input_db; users_db = input_users_db; db_api = input_db_api }
|
||||
function setLogger(input_logger) { logger = input_logger; }
|
||||
|
||||
function initialize(input_db, input_users_db, input_logger, input_db_api) {
|
||||
setDB(input_db, input_users_db, input_db_api);
|
||||
setLogger(input_logger);
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
Categories:
|
||||
|
||||
Categories are a way to organize videos based on dynamic rules set by the user. Categories are universal (so not per-user).
|
||||
|
||||
Categories, besides rules, have an optional custom output. This custom output can help users create their
|
||||
desired directory structure.
|
||||
|
||||
Rules:
|
||||
A category rule consists of a property, a comparison, and a value. For example, "uploader includes 'VEVO'"
|
||||
|
||||
Rules are stored as an object with the above fields. In addition to those fields, it also has a preceding_operator, which
|
||||
is either OR or AND, and signifies whether the rule should be ANDed with the previous rules, or just ORed. For the first
|
||||
rule, this field is null.
|
||||
|
||||
Ex. (title includes 'Rihanna' OR title includes 'Beyonce' AND uploader includes 'VEVO')
|
||||
|
||||
*/
|
||||
|
||||
async function categorize(file_json) {
|
||||
let selected_category = null;
|
||||
const categories = getCategories();
|
||||
if (!categories) {
|
||||
logger.warn('Categories could not be found. Initializing categories...');
|
||||
db.assign({categories: []}).write();
|
||||
return null;
|
||||
return;
|
||||
}
|
||||
|
||||
for (let i = 0; i < categories.length; i++) {
|
||||
const category = categories[i];
|
||||
const rules = category['rules'];
|
||||
|
||||
// if rules for current category apply, then that is the selected category
|
||||
if (applyCategoryRules(file_json, rules, category['name'])) {
|
||||
selected_category = category;
|
||||
logger.verbose(`Selected category ${category['name']} for ${file_json['webpage_url']}`);
|
||||
return selected_category;
|
||||
}
|
||||
}
|
||||
return selected_category;
|
||||
}
|
||||
|
||||
function getCategories() {
|
||||
const categories = db.get('categories').value();
|
||||
return categories ? categories : null;
|
||||
}
|
||||
|
||||
function applyCategoryRules(file_json, rules, category_name) {
|
||||
let rules_apply = false;
|
||||
for (let i = 0; i < rules.length; i++) {
|
||||
const rule = rules[i];
|
||||
let rule_applies = null;
|
||||
|
||||
let preceding_operator = rule['preceding_operator'];
|
||||
|
||||
switch (rule['comparator']) {
|
||||
case 'includes':
|
||||
rule_applies = file_json[rule['property']].includes(rule['value']);
|
||||
break;
|
||||
case 'not_includes':
|
||||
rule_applies = !(file_json[rule['property']].includes(rule['value']));
|
||||
break;
|
||||
case 'equals':
|
||||
rule_applies = file_json[rule['property']] === rule['value'];
|
||||
break;
|
||||
case 'not_equals':
|
||||
rule_applies = file_json[rule['property']] !== rule['value'];
|
||||
break;
|
||||
default:
|
||||
logger.warn(`Invalid comparison used for category ${category_name}`)
|
||||
break;
|
||||
}
|
||||
|
||||
// OR the first rule with rules_apply, which will be initially false
|
||||
if (i === 0) preceding_operator = 'or';
|
||||
|
||||
// update rules_apply based on current rule
|
||||
if (preceding_operator === 'or')
|
||||
rules_apply = rules_apply || rule_applies;
|
||||
else
|
||||
rules_apply = rules_apply && rule_applies;
|
||||
}
|
||||
|
||||
return rules_apply;
|
||||
}
|
||||
|
||||
async function addTagToVideo(tag, video, user_uid) {
|
||||
// TODO: Implement
|
||||
}
|
||||
|
||||
async function removeTagFromVideo(tag, video, user_uid) {
|
||||
// TODO: Implement
|
||||
}
|
||||
|
||||
// adds tag to list of existing tags (used for tag suggestions)
|
||||
async function addTagToExistingTags(tag) {
|
||||
const existing_tags = db.get('tags').value();
|
||||
if (!existing_tags.includes(tag)) {
|
||||
db.get('tags').push(tag).write();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
initialize: initialize,
|
||||
categorize: categorize,
|
||||
}
|
||||
@@ -15,10 +15,10 @@ function initialize(input_db, input_users_db, input_logger) {
|
||||
setLogger(input_logger);
|
||||
}
|
||||
|
||||
function registerFileDB(file_path, type, multiUserMode = null, sub = null) {
|
||||
function registerFileDB(file_path, type, multiUserMode = null, sub = null, customPath = null) {
|
||||
let db_path = null;
|
||||
const file_id = file_path.substring(0, file_path.length-4);
|
||||
const file_object = generateFileObject(file_id, type, multiUserMode && multiUserMode.file_path, sub);
|
||||
const file_object = generateFileObject(file_id, type, customPath || multiUserMode && multiUserMode.file_path, sub);
|
||||
if (!file_object) {
|
||||
logger.error(`Could not find associated JSON file for ${type} file ${file_id}`);
|
||||
return false;
|
||||
@@ -27,7 +27,7 @@ function registerFileDB(file_path, type, multiUserMode = null, sub = null) {
|
||||
utils.fixVideoMetadataPerms(file_id, type, multiUserMode && multiUserMode.file_path);
|
||||
|
||||
// add thumbnail path
|
||||
file_object['thumbnailPath'] = utils.getDownloadedThumbnail(file_id, type, multiUserMode && multiUserMode.file_path);
|
||||
file_object['thumbnailPath'] = utils.getDownloadedThumbnail(file_id, type, customPath || multiUserMode && multiUserMode.file_path);
|
||||
|
||||
if (!sub) {
|
||||
if (multiUserMode) {
|
||||
|
||||
@@ -430,6 +430,13 @@ function getSubscription(subID, user_uid = null) {
|
||||
return db.get('subscriptions').find({id: subID}).value();
|
||||
}
|
||||
|
||||
function getSubscriptionByName(subName, user_uid = null) {
|
||||
if (user_uid)
|
||||
return users_db.get('users').find({uid: user_uid}).get('subscriptions').find({name: subName}).value();
|
||||
else
|
||||
return db.get('subscriptions').find({name: subName}).value();
|
||||
}
|
||||
|
||||
function updateSubscription(sub, user_uid = null) {
|
||||
if (user_uid) {
|
||||
users_db.get('users').find({uid: user_uid}).get('subscriptions').find({id: sub.id}).assign(sub).write();
|
||||
@@ -482,6 +489,7 @@ async function removeIDFromArchive(archive_path, id) {
|
||||
|
||||
module.exports = {
|
||||
getSubscription : getSubscription,
|
||||
getSubscriptionByName : getSubscriptionByName,
|
||||
getAllSubscriptions : getAllSubscriptions,
|
||||
updateSubscription : updateSubscription,
|
||||
subscribe : subscribe,
|
||||
|
||||
@@ -116,6 +116,8 @@ export class AppComponent implements OnInit, AfterViewInit {
|
||||
if (this.allowSubscriptions) {
|
||||
this.postsService.reloadSubscriptions();
|
||||
}
|
||||
|
||||
this.postsService.reloadCategories();
|
||||
}
|
||||
|
||||
// theme stuff
|
||||
|
||||
@@ -79,6 +79,7 @@ import { UnifiedFileCardComponent } from './components/unified-file-card/unified
|
||||
import { RecentVideosComponent } from './components/recent-videos/recent-videos.component';
|
||||
import { EditSubscriptionDialogComponent } from './dialogs/edit-subscription-dialog/edit-subscription-dialog.component';
|
||||
import { CustomPlaylistsComponent } from './components/custom-playlists/custom-playlists.component';
|
||||
import { EditCategoryDialogComponent } from './dialogs/edit-category-dialog/edit-category-dialog.component';
|
||||
|
||||
registerLocaleData(es, 'es');
|
||||
|
||||
@@ -123,7 +124,8 @@ export function isVisible({ event, element, scrollContainer, offset }: IsVisible
|
||||
UnifiedFileCardComponent,
|
||||
RecentVideosComponent,
|
||||
EditSubscriptionDialogComponent,
|
||||
CustomPlaylistsComponent
|
||||
CustomPlaylistsComponent,
|
||||
EditCategoryDialogComponent
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
|
||||
@@ -61,7 +61,8 @@ export class LogsViewerComponent implements OnInit {
|
||||
data: {
|
||||
dialogTitle: 'Clear logs',
|
||||
dialogText: 'Would you like to clear your logs? This will delete all your current logs, permanently.',
|
||||
submitText: 'Clear'
|
||||
submitText: 'Clear',
|
||||
warnSubmitColor: true
|
||||
}
|
||||
});
|
||||
dialogRef.afterClosed().subscribe(confirmed => {
|
||||
|
||||
@@ -210,7 +210,7 @@ export class RecentVideosComponent implements OnInit {
|
||||
|
||||
if (!this.postsService.config.Extra.file_manager_enabled) {
|
||||
// tell server to delete the file once downloaded
|
||||
this.postsService.deleteFile(name, false).subscribe(delRes => {
|
||||
this.postsService.deleteFile(name, type).subscribe(delRes => {
|
||||
// reload mp4s
|
||||
this.getAllFiles();
|
||||
});
|
||||
@@ -233,7 +233,7 @@ export class RecentVideosComponent implements OnInit {
|
||||
}
|
||||
|
||||
deleteNormalFile(file, index, blacklistMode = false) {
|
||||
this.postsService.deleteFile(file.uid, file.isAudio, blacklistMode).subscribe(result => {
|
||||
this.postsService.deleteFile(file.uid, file.isAudio ? 'audio' : 'video', blacklistMode).subscribe(result => {
|
||||
if (result) {
|
||||
this.postsService.openSnackBar('Delete success!', 'OK.');
|
||||
this.files.splice(index, 1);
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
</mat-dialog-content>
|
||||
<mat-dialog-actions>
|
||||
<!-- The mat-dialog-close directive optionally accepts a value as a result for the dialog. -->
|
||||
<button color="primary" mat-flat-button type="submit" (click)="confirmClicked()">{{submitText}}</button>
|
||||
<button [color]="warnSubmitColor ? 'warn' : 'primary'" mat-flat-button type="submit" (click)="confirmClicked()">{{submitText}}</button>
|
||||
<div class="mat-spinner" *ngIf="submitClicked">
|
||||
<mat-spinner [diameter]="25"></mat-spinner>
|
||||
</div>
|
||||
|
||||
@@ -15,12 +15,14 @@ export class ConfirmDialogComponent implements OnInit {
|
||||
|
||||
doneEmitter: EventEmitter<any> = null;
|
||||
onlyEmitOnDone = false;
|
||||
|
||||
|
||||
warnSubmitColor = false;
|
||||
|
||||
constructor(@Inject(MAT_DIALOG_DATA) public data: any, public dialogRef: MatDialogRef<ConfirmDialogComponent>) {
|
||||
if (this.data.dialogTitle) { this.dialogTitle = this.data.dialogTitle };
|
||||
if (this.data.dialogText) { this.dialogText = this.data.dialogText };
|
||||
if (this.data.submitText) { this.submitText = this.data.submitText };
|
||||
if (this.data.warnSubmitColor) { this.warnSubmitColor = this.data.warnSubmitColor };
|
||||
|
||||
// checks if emitter exists, if so don't autoclose as it should be handled by caller
|
||||
if (this.data.doneEmitter) {
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
<h4 mat-dialog-title><ng-container i18n="Editing category dialog title">Editing category</ng-container> {{category['name']}}</h4>
|
||||
|
||||
<mat-dialog-content style="max-height: 50vh">
|
||||
<mat-form-field style="width: 250px; margin-bottom: 5px;">
|
||||
<input matInput placeholder="Name" i18n-placeholder="Name" [(ngModel)]="category['name']" required>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-divider></mat-divider>
|
||||
|
||||
<h6 style="margin-top: 20px;" i18n="Rules">Rules</h6>
|
||||
|
||||
<mat-list>
|
||||
<mat-list-item *ngFor="let rule of category['rules']; let i = index">
|
||||
<mat-form-field [style.visibility]="i === 0 ? 'hidden' : null" class="operator-select">
|
||||
<mat-select [disabled]="i === 0" [(ngModel)]="rule['preceding_operator']">
|
||||
<mat-option value="or">OR</mat-option>
|
||||
<mat-option value="and">AND</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<mat-form-field class="property-select">
|
||||
<mat-select [(ngModel)]="rule['property']">
|
||||
<mat-option *ngFor="let propertyOption of propertyOptions" [value]="propertyOption.value">{{propertyOption.label}}</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<mat-form-field class="comparator-select">
|
||||
<mat-select [(ngModel)]="rule['comparator']">
|
||||
<mat-option *ngFor="let comparatorOption of comparatorOptions" [value]="comparatorOption.value">{{comparatorOption.label}}</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<mat-form-field class="value-input">
|
||||
<input matInput [(ngModel)]="rule['value']">
|
||||
</mat-form-field>
|
||||
<button [disabled]="i === category['rules'].length-1" (click)="swapRules(i, i+1)" mat-icon-button><mat-icon>arrow_downward</mat-icon></button>
|
||||
<button [disabled]="i === 0" (click)="swapRules(i, i-1)" mat-icon-button><mat-icon>arrow_upward</mat-icon></button>
|
||||
<button (click)="removeRule(i)" mat-icon-button><mat-icon>cancel</mat-icon></button>
|
||||
</mat-list-item>
|
||||
</mat-list>
|
||||
|
||||
<button style="margin-bottom: 8px;" mat-icon-button (click)="addNewRule()" matTooltip="Add new rule" i18n-matTooltip="Add new rule tooltip"><mat-icon>add</mat-icon></button>
|
||||
|
||||
<mat-divider></mat-divider>
|
||||
|
||||
<mat-form-field style="width: 250px; margin-top: 10px;">
|
||||
<input matInput [(ngModel)]="category['custom_output']" placeholder="Custom file output" i18n-placeholder="Category custom file output placeholder">
|
||||
<mat-hint>
|
||||
<a target="_blank" href="https://github.com/ytdl-org/youtube-dl/blob/master/README.md#output-template">
|
||||
<ng-container i18n="Custom output template documentation link">Documentation</ng-container></a>.
|
||||
<ng-container i18n="Custom Output input hint">Path is relative to the config download path. Don't include extension.</ng-container>
|
||||
</mat-hint>
|
||||
</mat-form-field>
|
||||
</mat-dialog-content>
|
||||
|
||||
<mat-dialog-actions>
|
||||
<button mat-button mat-dialog-close><ng-container i18n="Cancel">Cancel</ng-container></button>
|
||||
|
||||
<button mat-button [disabled]="categoryChanged()" type="submit" (click)="saveClicked()"><ng-container i18n="Save button">Save</ng-container></button>
|
||||
<div class="mat-spinner" *ngIf="updating">
|
||||
<mat-spinner [diameter]="25"></mat-spinner>
|
||||
</div>
|
||||
</mat-dialog-actions>
|
||||
@@ -0,0 +1,16 @@
|
||||
.operator-select {
|
||||
width: 55px;
|
||||
}
|
||||
|
||||
.property-select {
|
||||
margin-left: 10px;
|
||||
width: 110px;
|
||||
}
|
||||
|
||||
.comparator-select {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.value-input {
|
||||
margin-left: 10px;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { EditCategoryDialogComponent } from './edit-category-dialog.component';
|
||||
|
||||
describe('EditCategoryDialogComponent', () => {
|
||||
let component: EditCategoryDialogComponent;
|
||||
let fixture: ComponentFixture<EditCategoryDialogComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ EditCategoryDialogComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(EditCategoryDialogComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,111 @@
|
||||
import { Component, OnInit, Inject } from '@angular/core';
|
||||
import { MAT_DIALOG_DATA } from '@angular/material/dialog';
|
||||
import { PostsService } from 'app/posts.services';
|
||||
|
||||
@Component({
|
||||
selector: 'app-edit-category-dialog',
|
||||
templateUrl: './edit-category-dialog.component.html',
|
||||
styleUrls: ['./edit-category-dialog.component.scss']
|
||||
})
|
||||
export class EditCategoryDialogComponent implements OnInit {
|
||||
|
||||
updating = false;
|
||||
original_category = null;
|
||||
category = null;
|
||||
|
||||
propertyOptions = [
|
||||
{
|
||||
value: 'fulltitle',
|
||||
label: 'Title'
|
||||
},
|
||||
{
|
||||
value: 'id',
|
||||
label: 'ID'
|
||||
},
|
||||
{
|
||||
value: 'webpage_url',
|
||||
label: 'URL'
|
||||
},
|
||||
{
|
||||
value: 'view_count',
|
||||
label: 'Views'
|
||||
},
|
||||
{
|
||||
value: 'uploader',
|
||||
label: 'Uploader'
|
||||
},
|
||||
{
|
||||
value: '_filename',
|
||||
label: 'File Name'
|
||||
},
|
||||
{
|
||||
value: 'tags',
|
||||
label: 'Tags'
|
||||
}
|
||||
];
|
||||
|
||||
comparatorOptions = [
|
||||
{
|
||||
value: 'includes',
|
||||
label: 'includes'
|
||||
},
|
||||
{
|
||||
value: 'not_includes',
|
||||
label: 'not includes'
|
||||
},
|
||||
{
|
||||
value: 'equals',
|
||||
label: 'equals'
|
||||
},
|
||||
{
|
||||
value: 'not_equals',
|
||||
label: 'not equals'
|
||||
},
|
||||
|
||||
];
|
||||
|
||||
constructor(@Inject(MAT_DIALOG_DATA) public data: any, private postsService: PostsService) {
|
||||
if (this.data) {
|
||||
this.original_category = this.data.category;
|
||||
this.category = JSON.parse(JSON.stringify(this.original_category));
|
||||
}
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
}
|
||||
|
||||
addNewRule() {
|
||||
this.category['rules'].push({
|
||||
preceding_operator: 'or',
|
||||
property: 'fulltitle',
|
||||
comparator: 'includes',
|
||||
value: ''
|
||||
});
|
||||
}
|
||||
|
||||
saveClicked() {
|
||||
this.updating = true;
|
||||
this.postsService.updateCategory(this.category).subscribe(res => {
|
||||
this.updating = false;
|
||||
this.original_category = JSON.parse(JSON.stringify(this.category));
|
||||
this.postsService.reloadCategories();
|
||||
}, err => {
|
||||
this.updating = false;
|
||||
console.error(err);
|
||||
});
|
||||
}
|
||||
|
||||
categoryChanged() {
|
||||
return JSON.stringify(this.category) === JSON.stringify(this.original_category);
|
||||
}
|
||||
|
||||
swapRules(original_index, new_index) {
|
||||
[this.category.rules[original_index], this.category.rules[new_index]] = [this.category.rules[new_index],
|
||||
this.category.rules[original_index]];
|
||||
}
|
||||
|
||||
removeRule(index) {
|
||||
this.category['rules'].splice(index, 1);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -56,7 +56,7 @@ export class FileCardComponent implements OnInit {
|
||||
|
||||
deleteFile(blacklistMode = false) {
|
||||
if (!this.playlist) {
|
||||
this.postsService.deleteFile(this.uid, this.isAudio, blacklistMode).subscribe(result => {
|
||||
this.postsService.deleteFile(this.uid, this.isAudio ? 'audio' : 'video', blacklistMode).subscribe(result => {
|
||||
if (result) {
|
||||
this.openSnackBar('Delete success!', 'OK.');
|
||||
this.removeFile.emit(this.name);
|
||||
|
||||
@@ -746,7 +746,7 @@ export class MainComponent implements OnInit {
|
||||
|
||||
if (!this.fileManagerEnabled) {
|
||||
// tell server to delete the file once downloaded
|
||||
this.postsService.deleteFile(name, true).subscribe(delRes => {
|
||||
this.postsService.deleteFile(name, 'video').subscribe(delRes => {
|
||||
// reload mp3s
|
||||
this.getMp3s();
|
||||
});
|
||||
@@ -763,7 +763,7 @@ export class MainComponent implements OnInit {
|
||||
|
||||
if (!this.fileManagerEnabled) {
|
||||
// tell server to delete the file once downloaded
|
||||
this.postsService.deleteFile(name, false).subscribe(delRes => {
|
||||
this.postsService.deleteFile(name, 'audio').subscribe(delRes => {
|
||||
// reload mp4s
|
||||
this.getMp4s();
|
||||
});
|
||||
|
||||
@@ -124,6 +124,8 @@ export class PlayerComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
this.getFile();
|
||||
} else if (this.id) {
|
||||
this.getPlaylistFiles();
|
||||
} else if (this.subscriptionName) {
|
||||
this.getSubscription();
|
||||
}
|
||||
|
||||
if (this.url) {
|
||||
@@ -139,7 +141,7 @@ export class PlayerComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
this.currentItem = this.playlist[0];
|
||||
this.currentIndex = 0;
|
||||
this.show_player = true;
|
||||
} else if (this.subscriptionName || this.fileNames) {
|
||||
} else if (this.fileNames && !this.subscriptionName) {
|
||||
this.show_player = true;
|
||||
this.parseFileNames();
|
||||
}
|
||||
@@ -171,6 +173,25 @@ export class PlayerComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
});
|
||||
}
|
||||
|
||||
getSubscription() {
|
||||
this.postsService.getSubscription(null, this.subscriptionName).subscribe(res => {
|
||||
const subscription = res['subscription'];
|
||||
if (this.fileNames) {
|
||||
subscription.videos.forEach(video => {
|
||||
if (video['id'] === this.fileNames[0]) {
|
||||
this.db_file = video;
|
||||
this.show_player = true;
|
||||
this.parseFileNames();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
console.log('no file name specified');
|
||||
}
|
||||
}, err => {
|
||||
this.openSnackBar(`Failed to find subscription ${this.subscriptionName}`, 'Dismiss');
|
||||
});
|
||||
}
|
||||
|
||||
getPlaylistFiles() {
|
||||
this.postsService.getPlaylist(this.id, null, this.uuid).subscribe(res => {
|
||||
if (res['playlist']) {
|
||||
@@ -202,23 +223,26 @@ export class PlayerComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
const fileName = this.fileNames[i];
|
||||
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 = this.type === 'audio' ? 'audio/' : 'video/';
|
||||
fullLocation = this.baseStreamPath + baseLocation + encodeURIComponent(fileName) + '?subName=' + this.subscriptionName +
|
||||
'&subPlaylist=' + this.subPlaylist;
|
||||
}
|
||||
|
||||
// adds user token if in multi-user-mode
|
||||
const uuid_str = this.uuid ? `&uuid=${this.uuid}` : '';
|
||||
const uid_str = (this.id || !this.db_file) ? '' : `&uid=${this.db_file.uid}`;
|
||||
const type_str = (this.id || !this.db_file || !this.db_file.type) ? '' : `&type=${this.db_file.type}`
|
||||
const type_str = (this.type || !this.db_file) ? `&type=${this.type}` : `&type=${this.db_file.type}`
|
||||
const id_str = this.id ? `&id=${this.id}` : '';
|
||||
const file_path_str = (!this.db_file) ? '' : `&file_path=${encodeURIComponent(this.db_file.path)}`;
|
||||
|
||||
if (!this.subscriptionName) {
|
||||
baseLocation = 'stream/';
|
||||
fullLocation = this.baseStreamPath + baseLocation + encodeURIComponent(fileName) + `?test=test${type_str}${file_path_str}`;
|
||||
} else {
|
||||
// default to video but include subscription name param
|
||||
baseLocation = 'stream/';
|
||||
fullLocation = this.baseStreamPath + baseLocation + encodeURIComponent(fileName) + '?subName=' + this.subscriptionName +
|
||||
'&subPlaylist=' + this.subPlaylist + `${file_path_str}${type_str}`;
|
||||
}
|
||||
|
||||
if (this.postsService.isLoggedIn) {
|
||||
fullLocation += (this.subscriptionName ? '&' : '?') + `jwt=${this.postsService.token}`;
|
||||
fullLocation += (this.subscriptionName ? '&' : '&') + `jwt=${this.postsService.token}`;
|
||||
if (this.is_shared) { fullLocation += `${uuid_str}${uid_str}${type_str}${id_str}`; }
|
||||
} else if (this.is_shared) {
|
||||
fullLocation += (this.subscriptionName ? '&' : '?') + `test=test${uuid_str}${uid_str}${type_str}${id_str}`;
|
||||
|
||||
@@ -53,6 +53,7 @@ export class PostsService implements CanActivate {
|
||||
// global vars
|
||||
config = null;
|
||||
subscriptions = null;
|
||||
categories = null;
|
||||
sidenav = null;
|
||||
locale = isoLangs['en'];
|
||||
|
||||
@@ -211,12 +212,8 @@ export class PostsService implements CanActivate {
|
||||
return this.http.post(this.path + 'setConfig', {new_config_file: config}, this.httpOptions);
|
||||
}
|
||||
|
||||
deleteFile(uid: string, isAudio: boolean, blacklistMode = false) {
|
||||
if (isAudio) {
|
||||
return this.http.post(this.path + 'deleteMp3', {uid: uid, blacklistMode: blacklistMode}, this.httpOptions);
|
||||
} else {
|
||||
return this.http.post(this.path + 'deleteMp4', {uid: uid, blacklistMode: blacklistMode}, this.httpOptions);
|
||||
}
|
||||
deleteFile(uid: string, type: string, blacklistMode = false) {
|
||||
return this.http.post(this.path + 'deleteFile', {uid: uid, type: type, blacklistMode: blacklistMode}, this.httpOptions);
|
||||
}
|
||||
|
||||
getMp3s() {
|
||||
@@ -310,6 +307,34 @@ export class PostsService implements CanActivate {
|
||||
return this.http.post(this.path + 'deletePlaylist', {playlistID: playlistID, type: type}, this.httpOptions);
|
||||
}
|
||||
|
||||
// categories
|
||||
|
||||
getAllCategories() {
|
||||
return this.http.post(this.path + 'getAllCategories', {}, this.httpOptions);
|
||||
}
|
||||
|
||||
createCategory(name) {
|
||||
return this.http.post(this.path + 'createCategory', {name: name}, this.httpOptions);
|
||||
}
|
||||
|
||||
deleteCategory(category_uid) {
|
||||
return this.http.post(this.path + 'deleteCategory', {category_uid: category_uid}, this.httpOptions);
|
||||
}
|
||||
|
||||
updateCategory(category) {
|
||||
return this.http.post(this.path + 'updateCategory', {category: category}, this.httpOptions);
|
||||
}
|
||||
|
||||
updateCategories(categories) {
|
||||
return this.http.post(this.path + 'updateCategories', {categories: categories}, this.httpOptions);
|
||||
}
|
||||
|
||||
reloadCategories() {
|
||||
this.getAllCategories().subscribe(res => {
|
||||
this.categories = res['categories'];
|
||||
});
|
||||
}
|
||||
|
||||
createSubscription(url, name, timerange = null, streamingOnly = false, audioOnly = false, customArgs = null, customFileOutput = null) {
|
||||
return this.http.post(this.path + 'subscribe', {url: url, name: name, timerange: timerange, streamingOnly: streamingOnly,
|
||||
audioOnly: audioOnly, customArgs: customArgs, customFileOutput: customFileOutput}, this.httpOptions);
|
||||
@@ -328,8 +353,8 @@ export class PostsService implements CanActivate {
|
||||
file_uid: file_uid}, this.httpOptions)
|
||||
}
|
||||
|
||||
getSubscription(id) {
|
||||
return this.http.post(this.path + 'getSubscription', {id: id}, this.httpOptions);
|
||||
getSubscription(id, name = null) {
|
||||
return this.http.post(this.path + 'getSubscription', {id: id, name: name}, this.httpOptions);
|
||||
}
|
||||
|
||||
getAllSubscriptions() {
|
||||
|
||||
@@ -115,15 +115,38 @@
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div class="col-12 mt-4">
|
||||
<div class="col-12 mt-4 mb-5">
|
||||
<mat-form-field class="text-field" style="margin-right: 12px;" color="accent">
|
||||
<textarea matInput [(ngModel)]="new_config['Downloader']['custom_args']" placeholder="Custom args" i18n-placeholder="Custom args input placeholder"></textarea>
|
||||
<mat-hint><ng-container i18n="Custom args setting input hint">Global custom args for downloads on the home page. Args are delimited using two commas like so: ,,</ng-container></mat-hint>
|
||||
<button class="args-edit-button" (click)="openArgsModifierDialog()" mat-icon-button><mat-icon>edit</mat-icon></button>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div class="col-12 mt-5">
|
||||
</div>
|
||||
</div>
|
||||
<mat-divider></mat-divider>
|
||||
<div *ngIf="new_config" class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-12 mt-3 mb-2">
|
||||
<h6 i18n="Categories">Categories</h6>
|
||||
<div cdkDropList class="category-list" (cdkDropListDropped)="dropCategory($event)">
|
||||
<div class="category-box" *ngFor="let category of postsService.categories" cdkDrag>
|
||||
<div class="category-custom-placeholder" *cdkDragPlaceholder></div>
|
||||
{{category['name']}}
|
||||
<span style="float: right">
|
||||
<button mat-icon-button (click)="openEditCategoryDialog(category)"><mat-icon>edit</mat-icon></button>
|
||||
<button mat-icon-button (click)="deleteCategory(category)"><mat-icon>cancel</mat-icon></button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<button style="margin-top: 10px;" mat-mini-fab (click)="openAddCategoryDialog()"><mat-icon>add</mat-icon></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<mat-divider></mat-divider>
|
||||
<div *ngIf="new_config" class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-12 mt-3">
|
||||
<mat-checkbox color="accent" [(ngModel)]="new_config['Downloader']['use_youtubedl_archive']"><ng-container i18n="Use youtubedl archive setting">Use youtube-dl archive</ng-container></mat-checkbox>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -30,4 +30,55 @@
|
||||
margin-left: 15px;
|
||||
margin-bottom: 12px;
|
||||
bottom: 4px;
|
||||
}
|
||||
|
||||
.category-list {
|
||||
width: 500px;
|
||||
max-width: 100%;
|
||||
border: solid 1px #ccc;
|
||||
min-height: 60px;
|
||||
display: block;
|
||||
// background: white;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.category-box {
|
||||
padding: 20px 10px;
|
||||
border-bottom: solid 1px #ccc;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
box-sizing: border-box;
|
||||
cursor: move;
|
||||
// background: white;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.cdk-drag-preview {
|
||||
box-sizing: border-box;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 5px 5px -3px rgba(0, 0, 0, 0.2),
|
||||
0 8px 10px 1px rgba(0, 0, 0, 0.14),
|
||||
0 3px 14px 2px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
.cdk-drag-animating {
|
||||
transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.category-box:last-child {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.category-list.cdk-drop-list-dragging .category-box:not(.cdk-drag-placeholder) {
|
||||
transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.category-custom-placeholder {
|
||||
background: #ccc;
|
||||
border: dotted 3px #999;
|
||||
min-height: 60px;
|
||||
transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
|
||||
}
|
||||
@@ -9,6 +9,9 @@ import { CURRENT_VERSION } from 'app/consts';
|
||||
import { MatCheckboxChange } from '@angular/material/checkbox';
|
||||
import { CookiesUploaderDialogComponent } from 'app/dialogs/cookies-uploader-dialog/cookies-uploader-dialog.component';
|
||||
import { ConfirmDialogComponent } from 'app/dialogs/confirm-dialog/confirm-dialog.component';
|
||||
import { moveItemInArray, CdkDragDrop } from '@angular/cdk/drag-drop';
|
||||
import { InputDialogComponent } from 'app/input-dialog/input-dialog.component';
|
||||
import { EditCategoryDialogComponent } from 'app/dialogs/edit-category-dialog/edit-category-dialog.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-settings',
|
||||
@@ -77,6 +80,74 @@ export class SettingsComponent implements OnInit {
|
||||
})
|
||||
}
|
||||
|
||||
dropCategory(event: CdkDragDrop<string[]>) {
|
||||
moveItemInArray(this.postsService.categories, event.previousIndex, event.currentIndex);
|
||||
this.postsService.updateCategories(this.postsService.categories).subscribe(res => {
|
||||
|
||||
}, err => {
|
||||
this.postsService.openSnackBar('Failed to update categories!');
|
||||
});
|
||||
}
|
||||
|
||||
openAddCategoryDialog() {
|
||||
const done = new EventEmitter<any>();
|
||||
const dialogRef = this.dialog.open(InputDialogComponent, {
|
||||
width: '300px',
|
||||
data: {
|
||||
inputTitle: 'Name the category',
|
||||
inputPlaceholder: 'Name',
|
||||
submitText: 'Add',
|
||||
doneEmitter: done
|
||||
}
|
||||
});
|
||||
|
||||
done.subscribe(name => {
|
||||
|
||||
// Eventually do additional checks on name
|
||||
if (name) {
|
||||
this.postsService.createCategory(name).subscribe(res => {
|
||||
if (res['success']) {
|
||||
this.postsService.reloadCategories();
|
||||
dialogRef.close();
|
||||
const new_category = res['new_category'];
|
||||
this.openEditCategoryDialog(new_category);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
deleteCategory(category) {
|
||||
const dialogRef = this.dialog.open(ConfirmDialogComponent, {
|
||||
data: {
|
||||
dialogTitle: 'Delete category',
|
||||
dialogText: `Would you like to delete ${category['name']}?`,
|
||||
submitText: 'Delete',
|
||||
warnSubmitColor: true
|
||||
}
|
||||
});
|
||||
dialogRef.afterClosed().subscribe(confirmed => {
|
||||
if (confirmed) {
|
||||
this.postsService.deleteCategory(category['uid']).subscribe(res => {
|
||||
if (res['success']) {
|
||||
this.postsService.openSnackBar(`Successfully deleted ${category['name']}!`);
|
||||
this.postsService.reloadCategories();
|
||||
}
|
||||
}, err => {
|
||||
this.postsService.openSnackBar(`Failed to delete ${category['name']}!`);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
openEditCategoryDialog(category) {
|
||||
this.dialog.open(EditCategoryDialogComponent, {
|
||||
data: {
|
||||
category: category
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
generateAPIKey() {
|
||||
this.postsService.generateNewAPIKey().subscribe(res => {
|
||||
if (res['new_api_key']) {
|
||||
@@ -162,7 +233,8 @@ export class SettingsComponent implements OnInit {
|
||||
dialogTitle: 'Kill downloads',
|
||||
dialogText: 'Are you sure you want to kill all downloads? Any subscription and non-subscription downloads will end immediately, though this operation may take a minute or so to complete.',
|
||||
submitText: 'Kill all downloads',
|
||||
doneEmitter: done
|
||||
doneEmitter: done,
|
||||
warnSubmitColor: true
|
||||
}
|
||||
});
|
||||
done.subscribe(confirmed => {
|
||||
|
||||
@@ -7,9 +7,11 @@
|
||||
"Downloader": {
|
||||
"path-audio": "audio/",
|
||||
"path-video": "video/",
|
||||
"use_youtubedl_archive": true,
|
||||
"use_youtubedl_archive": false,
|
||||
"custom_args": "",
|
||||
"safe_download_override": false
|
||||
"safe_download_override": false,
|
||||
"include_thumbnail": false,
|
||||
"include_metadata": true
|
||||
},
|
||||
"Extra": {
|
||||
"title_top": "YoutubeDL-Material",
|
||||
@@ -33,12 +35,20 @@
|
||||
"Subscriptions": {
|
||||
"allow_subscriptions": true,
|
||||
"subscriptions_base_path": "subscriptions/",
|
||||
"subscriptions_check_interval": "30",
|
||||
"subscriptions_check_interval": "300",
|
||||
"subscriptions_use_youtubedl_archive": true
|
||||
},
|
||||
"Users": {
|
||||
"base_path": "users/",
|
||||
"allow_registration": true
|
||||
"allow_registration": true,
|
||||
"auth_method": "internal",
|
||||
"ldap_config": {
|
||||
"url": "ldap://localhost:389",
|
||||
"bindDN": "cn=root",
|
||||
"bindCredentials": "secret",
|
||||
"searchBase": "ou=passport-ldapauth",
|
||||
"searchFilter": "(uid={{username}})"
|
||||
}
|
||||
},
|
||||
"Advanced": {
|
||||
"use_default_downloading_agent": true,
|
||||
@@ -47,7 +57,7 @@
|
||||
"allow_advanced_download": true,
|
||||
"jwt_expiration": 86400,
|
||||
"logger_level": "debug",
|
||||
"use_cookies": true
|
||||
"use_cookies": false
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user