Compare commits

..

30 Commits

Author SHA1 Message Date
Isaac Abadi
004a234b02 Downloads are now properly assigned a filename 2020-09-21 00:27:49 -04:00
Isaac Abadi
daca715d1b Fixes bug where playlists could not have download progress tracked
- downloads are now treated as playlists for cleaner logic
2020-09-20 23:01:43 -04:00
Isaac Abadi
8fdc231f08 Updated new home page UI to support file manager disabling and permissions
- file manager enabled state is now cached for faster loading
2020-09-18 11:22:45 -04:00
Isaac Abadi
ae8f7a2a33 Fixed bug that prevented playlists from being navigated to 2020-09-18 11:05:13 -04:00
Tzahi12345
d0782bb444 Update README.md
Updated API docs

Fixes #213
2020-09-18 00:46:42 -04:00
Isaac Abadi
49210abb49 Merge branch 'master' of https://github.com/Tzahi12345/YoutubeDL-Material 2020-09-17 03:15:29 -04:00
Isaac Abadi
851bfb81ba File cards are now properly centered 2020-09-17 03:12:09 -04:00
Isaac Abadi
35d0d439fa Control-clicking file cards will now open the player in a new tab 2020-09-17 03:11:52 -04:00
Tzahi12345
ded3ad6dfc Merge pull request #212 from Tzahi12345/dependabot/npm_and_yarn/backend/node-fetch-2.6.1
Bump node-fetch from 2.6.0 to 2.6.1 in /backend
2020-09-12 17:07:53 -04:00
dependabot[bot]
61daf26641 Bump node-fetch from 2.6.0 to 2.6.1 in /backend
Bumps [node-fetch](https://github.com/bitinn/node-fetch) from 2.6.0 to 2.6.1.
- [Release notes](https://github.com/bitinn/node-fetch/releases)
- [Changelog](https://github.com/node-fetch/node-fetch/blob/master/docs/CHANGELOG.md)
- [Commits](https://github.com/bitinn/node-fetch/compare/v2.6.0...v2.6.1)

Signed-off-by: dependabot[bot] <support@github.com>
2020-09-12 20:55:44 +00:00
Tzahi12345
95e53b9549 Fixed bug where unix paths would improperly parsed while importing unregistered files 2020-09-07 16:06:25 -04:00
Tzahi12345
46ed0fe992 Fixed bug in import unregistered logic where files in subfolders could not be found 2020-09-07 00:39:27 -04:00
Isaac Abadi
082252ab1e Updated sidenav logic for "side" mode, where it will now autoclose in the player, be open everywhere else 2020-08-31 15:21:58 -04:00
Tzahi12345
5eccaa13e5 Merge pull request #206 from Tzahi12345/downloader-improvements
Downloader improvements - updated system and bug fixes
2020-08-31 15:17:40 -04:00
Isaac Abadi
71633950b2 Comments cleanup 2020-08-31 15:03:04 -04:00
Isaac Abadi
f31dad0215 JSON metadata files are no longer kept if the associated setting is not enabled 2020-08-30 05:56:25 -04:00
Isaac Abadi
7efbe40bb2 Added setting for including metadata/thumbnails in the UI 2020-08-30 05:55:50 -04:00
Isaac Abadi
5b768b5bda JSON blobs were accidentally inserted into DB, stringifying then parsing the video file object fixes this 2020-08-30 05:42:52 -04:00
Isaac Abadi
365cbc3ffa Mkv/webm formats are now included for quality select (will get merged into mp4 at the end) 2020-08-29 23:08:23 -04:00
Isaac Abadi
44647f3306 Download progress is now shown when downloads are 1% complete or more (it was 15% before) 2020-08-29 23:06:40 -04:00
Isaac Abadi
8a7409478a Added the ability to download videos at higher resolutions than the highest mp4 (fixes #76)
Deprecates normal downloading method. The "safe" method is now always used, and download progress is now estimated using the predicted end file size

Thumbnails are now auto downloaded along with the other metadata
2020-08-29 23:05:37 -04:00
Tzahi12345
70159813e5 Merge pull request #205 from Tzahi12345/add-ldap-auth
Added ability to register/login through LDAP
2020-08-26 04:30:43 -04:00
Isaac Abadi
babba9aa30 Added ability to register/login through LDAP
- Added ability to edit LDAP settings and whether to use LDAP or not in the users tab in the settings
2020-08-26 04:18:29 -04:00
Tzahi12345
d292275956 Unfinished subscriptions will no longer cause an error during server startup 2020-08-24 05:13:27 -04:00
Tzahi12345
ba2acedb94 Files are now reloaded when you navigate back home 2020-08-24 05:13:01 -04:00
Tzahi12345
aa0558b770 Subscriptions are now reloaded on subscribe/unsubscribe in PostsService 2020-08-24 05:11:56 -04:00
Tzahi12345
d7f04fc90a Text for file duration in the unified file card component is now always black 2020-08-24 05:11:04 -04:00
Tzahi12345
087c9f1bb1 Added public directory to the gitignore 2020-08-24 02:44:52 -04:00
Tzahi12345
f874617965 Fixes bug where cached JWT token could prevent default admin creation 2020-08-24 02:44:39 -04:00
Tzahi12345
8fb8543829 Merge pull request #203 from Tzahi12345/arm-autobuild-test
Fix ARM autobuild
2020-08-24 02:20:19 -04:00
26 changed files with 759 additions and 207 deletions

1
.gitignore vendored
View File

@@ -65,3 +65,4 @@ backend/appdata/logs/error.log
backend/appdata/users.json
backend/users/*
backend/appdata/cookies.txt
backend/public

View File

@@ -90,7 +90,7 @@ environment:
## API
[API Docs](https://stoplight.io/p/docs/gh/tzahi12345/youtubedl-material?group=master&utm_campaign=publish_dialog&utm_source=studio)
[API Docs](https://youtubedl-material.stoplight.io/docs/youtubedl-material/Public%20API%20v1.yaml)
To get started, go to the settings menu and enable the public API from the *Extra* tab. You can generate an API key if one is missing.

View File

@@ -7,7 +7,7 @@ var path = require('path');
var youtubedl = require('youtube-dl');
var ffmpeg = require('fluent-ffmpeg');
var compression = require('compression');
var https = require('https');
var glob = require("glob")
var multer = require('multer');
var express = require("express");
var bodyParser = require("body-parser");
@@ -1146,12 +1146,34 @@ async function downloadFileByURL_exec(url, type, options, sessionID = null) {
type: type,
percent_complete: 0,
is_playlist: url.includes('playlist'),
timestamp_start: Date.now()
timestamp_start: Date.now(),
filesize: null
};
const download = downloads[session][download_uid];
updateDownloads();
// get video info prior to download
const info = await getVideoInfoByURL(url, downloadConfig, download);
if (!info) {
resolve(false);
return;
} else {
// store info in download for future use
if (Array.isArray(info)) {
download['fileNames'] = [];
for (let info_obj of info) download['fileNames'].push(info_obj['_filename']);
} else {
download['_filename'] = info['_filename'];
}
download['filesize'] = utils.getExpectedFileSize(info);
}
const download_checker = setInterval(() => checkDownloadPercent(download), 1000);
// download file
youtubedl.exec(url, downloadConfig, {}, function(err, output) {
clearInterval(download_checker); // stops the download checker from running as the download finished (or errored)
download['downloading'] = false;
download['timestamp_end'] = Date.now();
var file_uid = null;
@@ -1164,7 +1186,7 @@ async function downloadFileByURL_exec(url, type, options, sessionID = null) {
download['error'] = err.stderr;
updateDownloads();
resolve(false);
throw err;
return;
} else if (output) {
if (output.length === 0 || output[0].length === 0) {
download['error'] = 'No output. Check if video already exists in your archive.';
@@ -1407,7 +1429,7 @@ async function generateArgs(url, type, options) {
var youtubePassword = options.youtubePassword;
let downloadConfig = null;
let qualityPath = (is_audio && !options.skip_audio_args) ? ['-f', 'bestaudio'] : ['-f', 'best[ext=mp4]'];
let qualityPath = (is_audio && !options.skip_audio_args) ? ['-f', 'bestaudio'] : ['-f', 'bestvideo+bestaudio', '--merge-output-format', 'mp4'];
const is_youtube = url.includes('youtu');
if (!is_audio && !is_youtube) {
// tiktok videos fail when using the default format
@@ -1485,6 +1507,10 @@ async function generateArgs(url, type, options) {
downloadConfig.push('--download-archive', merged_path);
}
if (config_api.getConfigItem('ytdl_include_thumbnail')) {
downloadConfig.push('--write-thumbnail');
}
if (globalArgs && globalArgs !== '') {
// adds global args
if (downloadConfig.indexOf('-o') !== -1 && globalArgs.split(',,').indexOf('-o') !== -1) {
@@ -1497,11 +1523,36 @@ async function generateArgs(url, type, options) {
}
logger.verbose(`youtube-dl args being used: ${downloadConfig.join(',')}`);
// downloadConfig.map((arg) => `"${arg}"`);
resolve(downloadConfig);
});
}
async function getVideoInfoByURL(url, args = [], download = null) {
return new Promise(resolve => {
// remove bad args
const new_args = [...args];
const archiveArgIndex = new_args.indexOf('--download-archive');
if (archiveArgIndex !== -1) {
new_args.splice(archiveArgIndex, 2);
}
// actually get info
youtubedl.getInfo(url, new_args, (err, output) => {
if (output) {
resolve(output);
} else {
logger.error(`Error while retrieving info on video with URL ${url} with the following message: ${err}`);
if (download) {
download['error'] = `Failed pre-check for video info: ${err}`;
updateDownloads();
}
resolve(null);
}
});
});
}
// currently only works for single urls
async function getUrlInfos(urls) {
let startDate = Date.now();
@@ -1559,47 +1610,36 @@ function updateDownloads() {
db.assign({downloads: downloads}).write();
}
/*
function checkDownloads() {
for (let [session_id, session_downloads] of Object.entries(downloads)) {
for (let [download_uid, download_obj] of Object.entries(session_downloads)) {
if (download_obj && !download_obj['complete'] && !download_obj['error']
&& download_obj.timestamp_start > timestamp_server_start) {
// download is still running (presumably)
download_obj.percent_complete = getDownloadPercent(download_obj);
}
}
}
}
*/
function checkDownloadPercent(download) {
/*
This is more of an art than a science, we're just selecting files that start with the file name,
thus capturing the parts being downloaded in files named like so: '<video title>.<format>.<ext>.part'.
function getDownloadPercent(download_obj) {
if (!download_obj.final_size) {
if (fs.existsSync(download_obj.expected_json_path)) {
const file_json = JSON.parse(fs.readFileSync(download_obj.expected_json_path, 'utf8'));
let calculated_filesize = null;
if (file_json['format_id']) {
calculated_filesize = 0;
const formats_used = file_json['format_id'].split('+');
for (let i = 0; i < file_json['formats'].length; i++) {
if (formats_used.includes(file_json['formats'][i]['format_id'])) {
calculated_filesize += file_json['formats'][i]['filesize'];
}
Any file that starts with <video title> will be counted as part of the "bytes downloaded", which will
be divided by the "total expected bytes."
*/
// assume it's a playlist for logic reasons
const fileNames = Array.isArray(download['fileNames']) ? download['fileNames']
: [path.format(path.parse(download['_filename'].substring(0, download['_filename'].length-4)))];
const resulting_file_size = download['filesize'];
let sum_size = 0;
let glob_str = '';
glob(`{${fileNames.join(',')}, }*`, (err, files) => {
files.forEach(file => {
try {
const file_stats = fs.statSync(file);
if (file_stats && file_stats.size) {
sum_size += file_stats.size;
}
} catch (e) {
}
download_obj.final_size = calculated_filesize;
} else {
console.log('could not find json file');
}
}
if (fs.existsSync(download_obj.expected_path)) {
const stats = fs.statSync(download_obj.expected_path);
const size = stats.size;
return (size / download_obj.final_size)*100;
} else {
console.log('could not find file');
return 0;
}
});
download['percent_complete'] = (sum_size/resulting_file_size * 100).toFixed(2);
updateDownloads();
});
}
// youtube-dl functions
@@ -1821,7 +1861,7 @@ app.post('/api/tomp3', optionalJwt, async function(req, res) {
const is_playlist = url.includes('playlist');
let result_obj = null;
if (safeDownloadOverride || is_playlist || options.customQualityConfiguration || options.customArgs || options.maxBitrate)
if (true || safeDownloadOverride || is_playlist || options.customQualityConfiguration || options.customArgs || options.maxBitrate)
result_obj = await downloadFileByURL_exec(url, 'audio', options, req.query.sessionID);
else
result_obj = await downloadFileByURL_normal(url, 'audio', options, req.query.sessionID);
@@ -1833,6 +1873,7 @@ app.post('/api/tomp3', optionalJwt, async function(req, res) {
});
app.post('/api/tomp4', optionalJwt, async function(req, res) {
req.setTimeout(0); // remove timeout in case of long videos
var url = req.body.url;
var options = {
customArgs: req.body.customArgs,
@@ -1850,7 +1891,7 @@ app.post('/api/tomp4', optionalJwt, async function(req, res) {
const is_playlist = url.includes('playlist');
let result_obj = null;
if (safeDownloadOverride || is_playlist || options.customQualityConfiguration || options.customArgs || options.selectedHeight || !url.includes('youtu'))
if (true || safeDownloadOverride || is_playlist || options.customQualityConfiguration || options.customArgs || options.selectedHeight || !url.includes('youtu'))
result_obj = await downloadFileByURL_exec(url, 'video', options, req.query.sessionID);
else
result_obj = await downloadFileByURL_normal(url, 'video', options, req.query.sessionID);
@@ -1878,6 +1919,14 @@ app.get('/api/getMp3s', optionalJwt, function(req, res) {
playlists = auth_api.getUserPlaylists(req.user.uid, 'audio');
}
mp3s = JSON.parse(JSON.stringify(mp3s));
// add thumbnails if present
mp3s.forEach(mp3 => {
if (mp3['thumbnailPath'] && fs.existsSync(mp3['thumbnailPath']))
mp3['thumbnailBlob'] = fs.readFileSync(mp3['thumbnailPath']);
});
res.send({
mp3s: mp3s,
playlists: playlists
@@ -1897,6 +1946,14 @@ app.get('/api/getMp4s', optionalJwt, function(req, res) {
playlists = auth_api.getUserPlaylists(req.user.uid, 'video');
}
mp4s = JSON.parse(JSON.stringify(mp4s));
// add thumbnails if present
mp4s.forEach(mp4 => {
if (mp4['thumbnailPath'] && fs.existsSync(mp4['thumbnailPath']))
mp4['thumbnailBlob'] = fs.readFileSync(mp4['thumbnailPath']);
});
res.send({
mp4s: mp4s,
playlists: playlists
@@ -1981,6 +2038,14 @@ app.post('/api/getAllFiles', optionalJwt, function (req, res) {
files = files.concat(sub.videos);
}
files = JSON.parse(JSON.stringify(files));
// add thumbnails if present
files.forEach(file => {
if (file['thumbnailPath'] && fs.existsSync(file['thumbnailPath']))
file['thumbnailBlob'] = fs.readFileSync(file['thumbnailPath']);
});
res.send({
files: files,
playlists: playlists
@@ -2821,8 +2886,7 @@ app.post('/api/auth/register'
, optionalJwt
, auth_api.registerUser);
app.post('/api/auth/login'
, auth_api.passport.authenticate('local', {})
, auth_api.passport.authorize('local')
, auth_api.passport.authenticate(['local', 'ldapauth'], {})
, auth_api.generateJWT
, auth_api.returnAuthResponse
);

View File

@@ -9,7 +9,9 @@
"path-video": "video/",
"use_youtubedl_archive": false,
"custom_args": "",
"safe_download_override": false
"safe_download_override": false,
"include_thumbnail": true,
"include_metadata": true
},
"Extra": {
"title_top": "YoutubeDL-Material",
@@ -36,7 +38,15 @@
},
"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,

View File

@@ -9,6 +9,7 @@ var bcrypt = require('bcryptjs');
var LocalStrategy = require('passport-local').Strategy;
var LdapStrategy = require('passport-ldapauth');
var JwtStrategy = require('passport-jwt').Strategy,
ExtractJwt = require('passport-jwt').ExtractJwt;
@@ -90,24 +91,7 @@ exports.registerUser = function(req, res) {
bcrypt.hash(plaintextPassword, saltRounds)
.then(function(hash) {
let new_user = {
name: username,
uid: userid,
passhash: hash,
files: {
audio: [],
video: []
},
playlists: {
audio: [],
video: []
},
subscriptions: [],
created: Date.now(),
role: userid === 'admin' ? 'admin' : 'user',
permissions: [],
permission_overrides: []
};
let new_user = generateUserObject(userid, username, hash);
// check if user exists
if (users_db.get('users').find({uid: userid}).value()) {
// user id is taken!
@@ -153,52 +137,43 @@ exports.registerUser = function(req, res) {
exports.passport.use(new LocalStrategy({
usernameField: 'userid',
usernameField: 'username',
passwordField: 'password'},
function(username, password, done) {
const user = users_db.get('users').find({name: username}).value();
if (!user) { logger.error(`User ${username} not found`); return done(null, false); }
if (user.auth_method && user.auth_method !== 'internal') { return done(null, false); }
if (user) {
return done(null, bcrypt.compareSync(password, user.passhash) ? user : false);
}
}
));
/*passport.use(new BasicStrategy(
function(userid, plainTextPassword, done) {
const user = users_db.get('users').find({name: userid}).value();
if (user) {
var hashedPwd = user.passhash;
return bcrypt.compare(plainTextPassword, hashedPwd);
} else {
return false;
var getLDAPConfiguration = function(req, callback) {
const ldap_config = config_api.getConfigItem('ytdl_ldap_config');
const opts = {server: ldap_config};
callback(null, opts);
};
exports.passport.use(new LdapStrategy(getLDAPConfiguration,
function(user, done) {
// check if ldap auth is enabled
const ldap_enabled = config_api.getConfigItem('ytdl_auth_method') === 'ldap';
if (!ldap_enabled) return done(null, false);
const user_uid = user.uid;
let db_user = users_db.get('users').find({uid: user_uid}).value();
if (!db_user) {
// generate DB user
let new_user = generateUserObject(user_uid, user_uid, null, 'ldap');
users_db.get('users').push(new_user).write();
db_user = new_user;
logger.verbose(`Generated new user ${user_uid} using LDAP`);
}
return done(null, db_user);
}
));
*/
/*************************************************************
* This is a wrapper for auth.passport.authenticate().
* We use this to change WWW-Authenticate header so
* the browser doesn't pop-up challenge dialog box by default.
* Browser's will pop-up up dialog when status is 401 and
* "WWW-Authenticate:Basic..."
*************************************************************/
/*
exports.authenticateViaPassport = function(req, res, next) {
exports.passport.authenticate('basic',{session:false},
function(err, user, info) {
if(!user){
res.set('WWW-Authenticate', 'x'+info); // change to xBasic
res.status(401).send('Invalid Authentication');
} else {
req.user = user;
next();
}
}
)(req, res, next);
};
*/
/**********************************
* Generating/Signing a JWT token
@@ -538,3 +513,26 @@ function getToken(queryParams) {
return null;
}
};
function generateUserObject(userid, username, hash, auth_method = 'internal') {
let new_user = {
name: username,
uid: userid,
passhash: auth_method === 'internal' ? hash : null,
files: {
audio: [],
video: []
},
playlists: {
audio: [],
video: []
},
subscriptions: [],
created: Date.now(),
role: userid === 'admin' && auth_method === 'internal' ? 'admin' : 'user',
permissions: [],
permission_overrides: [],
auth_method: auth_method
};
return new_user;
}

View File

@@ -186,7 +186,9 @@ DEFAULT_CONFIG = {
"path-video": "video/",
"use_youtubedl_archive": false,
"custom_args": "",
"safe_download_override": false
"safe_download_override": false,
"include_thumbnail": true,
"include_metadata": true
},
"Extra": {
"title_top": "YoutubeDL-Material",
@@ -213,7 +215,15 @@ DEFAULT_CONFIG = {
},
"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,

View File

@@ -30,6 +30,14 @@ let CONFIG_ITEMS = {
'key': 'ytdl_safe_download_override',
'path': 'YoutubeDLMaterial.Downloader.safe_download_override'
},
'ytdl_include_thumbnail': {
'key': 'ytdl_include_thumbnail',
'path': 'YoutubeDLMaterial.Downloader.include_thumbnail'
},
'ytdl_include_metadata': {
'key': 'ytdl_include_metadata',
'path': 'YoutubeDLMaterial.Downloader.include_metadata'
},
// Extra
'ytdl_title_top': {
@@ -112,6 +120,14 @@ let CONFIG_ITEMS = {
'key': 'ytdl_allow_registration',
'path': 'YoutubeDLMaterial.Users.allow_registration'
},
'ytdl_auth_method': {
'key': 'ytdl_auth_method',
'path': 'YoutubeDLMaterial.Users.auth_method'
},
'ytdl_ldap_config': {
'key': 'ytdl_ldap_config',
'path': 'YoutubeDLMaterial.Users.ldap_config'
},
// Advanced
'ytdl_use_default_downloading_agent': {

View File

@@ -26,11 +26,8 @@ function registerFileDB(file_path, type, multiUserMode = null, sub = null) {
utils.fixVideoMetadataPerms(file_id, type, multiUserMode && multiUserMode.file_path);
// add additional info
file_object['uid'] = uuid();
file_object['registered'] = Date.now();
path_object = path.parse(file_object['path']);
file_object['path'] = path.format(path_object);
// add thumbnail path
file_object['thumbnailPath'] = utils.getDownloadedThumbnail(file_id, type, multiUserMode && multiUserMode.file_path);
if (!sub) {
if (multiUserMode) {
@@ -48,7 +45,13 @@ function registerFileDB(file_path, type, multiUserMode = null, sub = null) {
}
}
const file_uid = registerFileDBManual(db_path, file_object)
const file_uid = registerFileDBManual(db_path, file_object);
// remove metadata JSON if needed
if (!config_api.getConfigItem('ytdl_include_metadata')) {
utils.deleteJSONFile(file_id, type, multiUserMode && multiUserMode.file_path)
}
return file_uid;
}
@@ -165,6 +168,10 @@ async function importUnregisteredFiles() {
// add subscriptions to check list
for (let i = 0; i < subscriptions_to_check.length; i++) {
let subscription_to_check = subscriptions_to_check[i];
if (!subscription_to_check.name) {
// TODO: Remove subscription as it'll never complete
continue;
}
dirs_to_check.push({
basePath: multi_user_mode ? path.join(usersFileFolder, subscription_to_check.user_uid, 'subscriptions', subscription_to_check.isPlaylist ? 'playlists/' : 'channels/', subscription_to_check.name)
: path.join(subscriptions_base_path, subscription_to_check.isPlaylist ? 'playlists/' : 'channels/', subscription_to_check.name),

View File

@@ -4,6 +4,89 @@
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"@types/body-parser": {
"version": "1.19.0",
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.0.tgz",
"integrity": "sha512-W98JrE0j2K78swW4ukqMleo8R7h/pFETjM2DQ90MF6XK2i4LO4W3gQ71Lt4w3bfm2EvVSyWHplECvB5sK22yFQ==",
"requires": {
"@types/connect": "*",
"@types/node": "*"
}
},
"@types/connect": {
"version": "3.4.33",
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.33.tgz",
"integrity": "sha512-2+FrkXY4zllzTNfJth7jOqEHC+enpLeGslEhpnTAkg21GkRrWV4SsAtqchtT4YS9/nODBU2/ZfsBY2X4J/dX7A==",
"requires": {
"@types/node": "*"
}
},
"@types/express": {
"version": "4.17.7",
"resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.7.tgz",
"integrity": "sha512-dCOT5lcmV/uC2J9k0rPafATeeyz+99xTt54ReX11/LObZgfzJqZNcW27zGhYyX+9iSEGXGt5qLPwRSvBZcLvtQ==",
"requires": {
"@types/body-parser": "*",
"@types/express-serve-static-core": "*",
"@types/qs": "*",
"@types/serve-static": "*"
}
},
"@types/express-serve-static-core": {
"version": "4.17.9",
"resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.9.tgz",
"integrity": "sha512-DG0BYg6yO+ePW+XoDENYz8zhNGC3jDDEpComMYn7WJc4mY1Us8Rw9ax2YhJXxpyk2SF47PQAoQ0YyVT1a0bEkA==",
"requires": {
"@types/node": "*",
"@types/qs": "*",
"@types/range-parser": "*"
}
},
"@types/ldapjs": {
"version": "1.0.9",
"resolved": "https://registry.npmjs.org/@types/ldapjs/-/ldapjs-1.0.9.tgz",
"integrity": "sha512-3PvY7Drp1zoLbcGlothCAkoc5o6Jp9KvUPwHadlHyKp3yPvyeIh7w2zQc9UXMzgDRkoeGXUEODtbEs5XCh9ZyA==",
"requires": {
"@types/node": "*"
}
},
"@types/mime": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-2.0.3.tgz",
"integrity": "sha512-Jus9s4CDbqwocc5pOAnh8ShfrnMcPHuJYzVcSUU7lrh8Ni5HuIqX3oilL86p3dlTrk0LzHRCgA/GQ7uNCw6l2Q=="
},
"@types/node": {
"version": "14.6.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.6.0.tgz",
"integrity": "sha512-mikldZQitV94akrc4sCcSjtJfsTKt4p+e/s0AGscVA6XArQ9kFclP+ZiYUMnq987rc6QlYxXv/EivqlfSLxpKA=="
},
"@types/passport": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@types/passport/-/passport-1.0.4.tgz",
"integrity": "sha512-h5OfAbfBBYSzjeU0GTuuqYEk9McTgWeGQql9g3gUw2/NNCfD7VgExVRYJVVeU13Twn202Mvk9BT0bUrl30sEgA==",
"requires": {
"@types/express": "*"
}
},
"@types/qs": {
"version": "6.9.4",
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.4.tgz",
"integrity": "sha512-+wYo+L6ZF6BMoEjtf8zB2esQsqdV6WsjRK/GP9WOgLPrq87PbNWgIxS76dS5uvl/QXtHGakZmwTznIfcPXcKlQ=="
},
"@types/range-parser": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.3.tgz",
"integrity": "sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA=="
},
"@types/serve-static": {
"version": "1.13.5",
"resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.5.tgz",
"integrity": "sha512-6M64P58N+OXjU432WoLLBQxbA0LRGBCRm7aAGQJ+SMC1IMl0dgRVi9EFfoDcS2a7Xogygk/eGN94CfwU9UF7UQ==",
"requires": {
"@types/express-serve-static-core": "*",
"@types/mime": "*"
}
},
"abbrev": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
@@ -169,6 +252,14 @@
"resolved": "https://registry.npmjs.org/aws4/-/aws4-1.9.1.tgz",
"integrity": "sha512-wMHVg2EOHaMRxbzgFJ9gtjOOCrI80OHLG14rxi28XwOW8ux6IiEbRCGGGqCtdAIg4FQCbW20k9RsT4y3gJlFug=="
},
"backoff": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/backoff/-/backoff-2.5.0.tgz",
"integrity": "sha1-9hbtqdPktmuMp/ynn2lXIsX44m8=",
"requires": {
"precond": "0.2"
}
},
"balanced-match": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
@@ -315,6 +406,17 @@
"resolved": "https://registry.npmjs.org/buffers/-/buffers-0.1.1.tgz",
"integrity": "sha1-skV5w77U1tOWru5tmorn9Ugqt7s="
},
"bunyan": {
"version": "1.8.14",
"resolved": "https://registry.npmjs.org/bunyan/-/bunyan-1.8.14.tgz",
"integrity": "sha512-LlahJUxXzZLuw/hetUQJmRgZ1LF6+cr5TPpRj6jf327AsiIq2jhYEH4oqUUkVKTor+9w2BT3oxVwhzE5lw9tcg==",
"requires": {
"dtrace-provider": "~0.8",
"moment": "^2.19.3",
"mv": "~2",
"safe-json-stringify": "~1"
}
},
"busboy": {
"version": "0.2.14",
"resolved": "https://registry.npmjs.org/busboy/-/busboy-0.2.14.tgz",
@@ -739,6 +841,15 @@
"is-obj": "^1.0.0"
}
},
"dtrace-provider": {
"version": "0.8.8",
"resolved": "https://registry.npmjs.org/dtrace-provider/-/dtrace-provider-0.8.8.tgz",
"integrity": "sha512-b7Z7cNtHPhH9EJhNNbbeqTcXB8LGFFZhq1PGgEvpeHlzd36bhbdTWoE/Ba/YguqpBSlAPKnARWhVlhunCMwfxg==",
"optional": true,
"requires": {
"nan": "^2.14.0"
}
},
"duplexer2": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz",
@@ -1472,6 +1583,72 @@
}
}
},
"ldap-filter": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/ldap-filter/-/ldap-filter-0.2.2.tgz",
"integrity": "sha1-8rhCvguG2jNSeYUFsx68rlkNd9A=",
"requires": {
"assert-plus": "0.1.5"
},
"dependencies": {
"assert-plus": {
"version": "0.1.5",
"resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.1.5.tgz",
"integrity": "sha1-7nQAlBMALYTOxyGcasgRgS5yMWA="
}
}
},
"ldapauth-fork": {
"version": "4.3.3",
"resolved": "https://registry.npmjs.org/ldapauth-fork/-/ldapauth-fork-4.3.3.tgz",
"integrity": "sha512-x76VpQ5ZqkwAJmqwcD6KIwDiNEbgIGIPGwC/eA17e1dxWhlTx36w0DlLOFwjTuZ2iuaLTsZsUprlVqvSlwc/1Q==",
"requires": {
"@types/ldapjs": "^1.0.0",
"@types/node": "*",
"bcryptjs": "^2.4.0",
"ldapjs": "^1.0.2",
"lru-cache": "^5.1.1"
},
"dependencies": {
"lru-cache": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
"integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
"requires": {
"yallist": "^3.0.2"
}
},
"yallist": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="
}
}
},
"ldapjs": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/ldapjs/-/ldapjs-1.0.2.tgz",
"integrity": "sha1-VE/3Ayt7g8aPBwEyjZKXqmlDQPk=",
"requires": {
"asn1": "0.2.3",
"assert-plus": "^1.0.0",
"backoff": "^2.5.0",
"bunyan": "^1.8.3",
"dashdash": "^1.14.0",
"dtrace-provider": "~0.8",
"ldap-filter": "0.2.2",
"once": "^1.4.0",
"vasync": "^1.6.4",
"verror": "^1.8.1"
},
"dependencies": {
"asn1": {
"version": "0.2.3",
"resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz",
"integrity": "sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y="
}
}
},
"listenercount": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/listenercount/-/listenercount-1.0.1.tgz",
@@ -1672,6 +1849,12 @@
"minimist": "^1.2.5"
}
},
"moment": {
"version": "2.27.0",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.27.0.tgz",
"integrity": "sha512-al0MUK7cpIcglMv3YF13qSgdAIqxHTO7brRtaz3DlSULbqfazqkc5kEjNrLDOM7fsjshoFIihnU8snrP7zUvhQ==",
"optional": true
},
"ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
@@ -1717,6 +1900,41 @@
}
}
},
"mv": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/mv/-/mv-2.1.1.tgz",
"integrity": "sha1-rmzg1vbV4KT32JN5jQPB6pVZtqI=",
"optional": true,
"requires": {
"mkdirp": "~0.5.1",
"ncp": "~2.0.0",
"rimraf": "~2.4.0"
},
"dependencies": {
"glob": {
"version": "6.0.4",
"resolved": "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz",
"integrity": "sha1-DwiGD2oVUSey+t1PnOJLGqtuTSI=",
"optional": true,
"requires": {
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "2 || 3",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
}
},
"rimraf": {
"version": "2.4.5",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.4.5.tgz",
"integrity": "sha1-7nEM5dk6j9uFb7Xqj/Di11k0sto=",
"optional": true,
"requires": {
"glob": "^6.0.1"
}
}
}
},
"mz": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",
@@ -1727,20 +1945,32 @@
"thenify-all": "^1.0.0"
}
},
"nan": {
"version": "2.14.1",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.14.1.tgz",
"integrity": "sha512-isWHgVjnFjh2x2yuJ/tj3JbwoHu3UC2dX5G/88Cm24yB6YopVgxvBObDY7n5xW6ExmFhJpSEQqFPvq9zaXc8Jw==",
"optional": true
},
"nanoid": {
"version": "2.1.11",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-2.1.11.tgz",
"integrity": "sha512-s/snB+WGm6uwi0WjsZdaVcuf3KJXlfGl2LcxgwkEwJF0D/BWzVWAZW/XY4bFaiR7s0Jk3FPvlnepg1H1b1UwlA=="
},
"ncp": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz",
"integrity": "sha1-GVoh1sRuNh0vsSgbo4uR6d9727M=",
"optional": true
},
"negotiator": {
"version": "0.6.2",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz",
"integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw=="
},
"node-fetch": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.0.tgz",
"integrity": "sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA=="
"version": "2.6.1",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz",
"integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw=="
},
"node-id3": {
"version": "0.1.16",
@@ -1918,6 +2148,17 @@
}
}
},
"passport-ldapauth": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/passport-ldapauth/-/passport-ldapauth-2.1.4.tgz",
"integrity": "sha512-VeVL69ZK+cpJe0DKMSGuwcf7k+V4dr0U0Y7ZhXL785pcRb5gRA6qYZfIH+XTsAzwqTK9l0Dn3Ds4weOZ1jKkLQ==",
"requires": {
"@types/node": "*",
"@types/passport": "^1.0.0",
"ldapauth-fork": "^4.3.2",
"passport-strategy": "^1.0.0"
}
},
"passport-local": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/passport-local/-/passport-local-1.0.0.tgz",
@@ -1971,6 +2212,11 @@
"resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz",
"integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY="
},
"precond": {
"version": "0.2.3",
"resolved": "https://registry.npmjs.org/precond/-/precond-0.2.3.tgz",
"integrity": "sha1-qpWRvKokkj8eD0hJ0kD0fvwQdaw="
},
"prepend-http": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-1.0.4.tgz",
@@ -2166,6 +2412,12 @@
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
},
"safe-json-stringify": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/safe-json-stringify/-/safe-json-stringify-1.2.0.tgz",
"integrity": "sha512-gH8eh2nZudPQO6TytOvbxnuhYBOvDBBLW52tz5q6X58lJcd/tkmqFR+5Z9adS8aJtURSXWThWy/xJtJwixErvg==",
"optional": true
},
"safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
@@ -2682,6 +2934,29 @@
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
"integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw="
},
"vasync": {
"version": "1.6.4",
"resolved": "https://registry.npmjs.org/vasync/-/vasync-1.6.4.tgz",
"integrity": "sha1-3+k2Fq0OeugBszKp2Iv8XNyOHR8=",
"requires": {
"verror": "1.6.0"
},
"dependencies": {
"extsprintf": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.2.0.tgz",
"integrity": "sha1-WtlGwi9bMrp/jNdCZxHG6KP8JSk="
},
"verror": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/verror/-/verror-1.6.0.tgz",
"integrity": "sha1-fROyex+swuLakEBetepuW90lLqU=",
"requires": {
"extsprintf": "1.2.0"
}
}
}
},
"verror": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz",

View File

@@ -37,17 +37,19 @@
"express": "^4.17.1",
"fluent-ffmpeg": "^2.1.2",
"fs-extra": "^9.0.0",
"glob": "^7.1.6",
"jsonwebtoken": "^8.5.1",
"lowdb": "^1.0.0",
"md5": "^2.2.1",
"merge-files": "^0.1.2",
"multer": "^1.4.2",
"node-fetch": "^2.6.0",
"node-fetch": "^2.6.1",
"node-id3": "^0.1.14",
"nodemon": "^2.0.2",
"passport": "^0.4.1",
"passport-http": "^0.3.0",
"passport-jwt": "^4.0.0",
"passport-ldapauth": "^2.1.4",
"passport-local": "^1.0.0",
"progress": "^2.0.3",
"ps-node": "^0.1.6",

View File

@@ -345,6 +345,10 @@ async function getVideosForSub(sub, user_uid = null) {
}
}
if (config_api.getConfigItem('ytdl_include_thumbnail')) {
downloadConfig.push('--write-thumbnail');
}
// get videos
logger.verbose('Subscription: getting videos for subscription ' + sub.name);
youtubedl.exec(sub.url, downloadConfig, {}, function(err, output) {

View File

@@ -28,7 +28,7 @@ function getDownloadedFilesByType(basePath, type) {
var located_files = recFindByExt(basePath, ext);
for (let i = 0; i < located_files.length; i++) {
let file = located_files[i];
var file_path = path.basename(file);
var file_path = file.substring(basePath.includes('\\') ? basePath.length+1 : basePath.length, file.length);
var stats = fs.statSync(file);
@@ -88,6 +88,49 @@ function getJSONByType(type, name, customPath, openReadPerms = false) {
return type === 'audio' ? getJSONMp3(name, customPath, openReadPerms) : getJSONMp4(name, customPath, openReadPerms)
}
function getDownloadedThumbnail(name, type, customPath = null) {
if (!customPath) customPath = type === 'audio' ? config_api.getConfigItem('ytdl_audio_folder_path') : config_api.getConfigItem('ytdl_video_folder_path');
let jpgPath = path.join(customPath, name + '.jpg');
let webpPath = path.join(customPath, name + '.webp');
let pngPath = path.join(customPath, name + '.png');
if (fs.existsSync(jpgPath))
return jpgPath;
else if (fs.existsSync(webpPath))
return webpPath;
else if (fs.existsSync(pngPath))
return pngPath;
else
return null;
}
function getExpectedFileSize(input_info_jsons) {
// treat single videos as arrays to have the file sizes checked/added to. makes the code cleaner
const info_jsons = Array.isArray(input_info_jsons) ? input_info_jsons : [input_info_jsons];
let expected_filesize = 0;
info_jsons.forEach(info_json => {
if (info_json['filesize']) {
expected_filesize += info_json['filesize'];
return;
}
const formats = info_json['format_id'].split('+');
let individual_expected_filesize = 0;
formats.forEach(format_id => {
info_json.formats.forEach(available_format => {
if (available_format.format_id === format_id && available_format.filesize) {
individual_expected_filesize += available_format.filesize;
}
});
});
expected_filesize += individual_expected_filesize;
});
return expected_filesize;
}
function fixVideoMetadataPerms(name, type, customPath = null) {
if (is_windows) return;
if (!customPath) customPath = type === 'audio' ? config_api.getConfigItem('ytdl_audio_folder_path')
@@ -110,6 +153,19 @@ function fixVideoMetadataPerms(name, type, customPath = null) {
}
}
function deleteJSONFile(name, type, customPath = null) {
if (!customPath) customPath = type === 'audio' ? config_api.getConfigItem('ytdl_audio_folder_path')
: config_api.getConfigItem('ytdl_video_folder_path');
const ext = type === 'audio' ? '.mp3' : '.mp4';
let json_path = path.join(customPath, name + '.info.json');
let alternate_json_path = path.join(customPath, name + ext + '.info.json');
if (fs.existsSync(json_path)) fs.unlinkSync(json_path);
if (fs.existsSync(alternate_json_path)) fs.unlinkSync(alternate_json_path);
}
function recFindByExt(base,ext,files,result)
{
files = files || fs.readdirSync(base)
@@ -153,7 +209,10 @@ module.exports = {
getJSONMp3: getJSONMp3,
getJSONMp4: getJSONMp4,
getTrueFileName: getTrueFileName,
getDownloadedThumbnail: getDownloadedThumbnail,
getExpectedFileSize: getExpectedFileSize,
fixVideoMetadataPerms: fixVideoMetadataPerms,
deleteJSONFile: deleteJSONFile,
getDownloadedFilesByType: getDownloadedFilesByType,
recFindByExt: recFindByExt,
File: File

View File

@@ -38,15 +38,15 @@
</div>
<div class="sidenav-container" style="height: calc(100% - 64px)">
<mat-sidenav-container style="height: 100%">
<mat-sidenav [opened]="postsService.sidepanel_mode === 'side' && router.url === '/home'" [mode]="postsService.sidepanel_mode" #sidenav>
<mat-sidenav [opened]="postsService.sidepanel_mode === 'side' && !window.location.href.includes('/player')" [mode]="postsService.sidepanel_mode" #sidenav>
<mat-nav-list>
<a *ngIf="postsService.config && (!postsService.config.Advanced.multi_user_mode || postsService.isLoggedIn)" mat-list-item (click)="sidenav.close()" routerLink='/home'><ng-container i18n="Navigation menu Home Page title">Home</ng-container></a>
<a *ngIf="postsService.config && (!postsService.config.Advanced.multi_user_mode || postsService.isLoggedIn)" mat-list-item (click)="postsService.sidepanel_mode === 'over' ? sidenav.close() : null" routerLink='/home'><ng-container i18n="Navigation menu Home Page title">Home</ng-container></a>
<a *ngIf="postsService.config && postsService.config.Advanced.multi_user_mode && !postsService.isLoggedIn" mat-list-item (click)="sidenav.close()" routerLink='/login'><ng-container i18n="Navigation menu Login Page title">Login</ng-container></a>
<a *ngIf="postsService.config && allowSubscriptions && (!postsService.config.Advanced.multi_user_mode || (postsService.isLoggedIn && postsService.permissions.includes('subscriptions')))" mat-list-item (click)="sidenav.close()" routerLink='/subscriptions'><ng-container i18n="Navigation menu Subscriptions Page title">Subscriptions</ng-container></a>
<a *ngIf="postsService.config && enableDownloadsManager && (!postsService.config.Advanced.multi_user_mode || (postsService.isLoggedIn && postsService.permissions.includes('downloads_manager')))" mat-list-item (click)="sidenav.close()" routerLink='/downloads'><ng-container i18n="Navigation menu Downloads Page title">Downloads</ng-container></a>
<a *ngIf="postsService.config && allowSubscriptions && (!postsService.config.Advanced.multi_user_mode || (postsService.isLoggedIn && postsService.permissions.includes('subscriptions')))" mat-list-item (click)="postsService.sidepanel_mode === 'over' ? sidenav.close() : null" routerLink='/subscriptions'><ng-container i18n="Navigation menu Subscriptions Page title">Subscriptions</ng-container></a>
<a *ngIf="postsService.config && enableDownloadsManager && (!postsService.config.Advanced.multi_user_mode || (postsService.isLoggedIn && postsService.permissions.includes('downloads_manager')))" mat-list-item (click)="postsService.sidepanel_mode === 'over' ? sidenav.close() : null" routerLink='/downloads'><ng-container i18n="Navigation menu Downloads Page title">Downloads</ng-container></a>
<ng-container *ngIf="postsService.config && allowSubscriptions && postsService.subscriptions && (!postsService.config.Advanced.multi_user_mode || (postsService.isLoggedIn && postsService.permissions.includes('subscriptions')))">
<mat-divider></mat-divider>
<a *ngFor="let subscription of postsService.subscriptions" mat-list-item (click)="sidenav.close()" [routerLink]="['/subscription', { id: subscription.id }]"><ngx-avatar [style.margin-right]="'10px'" size="32" [name]="subscription.name"></ngx-avatar><ng-container i18n="Navigation menu Downloads Page title">{{subscription.name}}</ng-container></a>
<a *ngFor="let subscription of postsService.subscriptions" mat-list-item (click)="postsService.sidepanel_mode === 'over' ? sidenav.close() : null" [routerLink]="['/subscription', { id: subscription.id }]"><ngx-avatar [style.margin-right]="'10px'" size="32" [name]="subscription.name"></ngx-avatar><ng-container i18n="Navigation menu Downloads Page title">{{subscription.name}}</ng-container></a>
</ng-container>
</mat-nav-list>
</mat-sidenav>

View File

@@ -1,4 +1,4 @@
import { Component, OnInit, ElementRef, ViewChild, HostBinding } from '@angular/core';
import { Component, OnInit, ElementRef, ViewChild, HostBinding, AfterViewInit } from '@angular/core';
import {PostsService} from './posts.services';
import {FileCardComponent} from './file-card/file-card.component';
import { Observable } from 'rxjs/Observable';
@@ -30,11 +30,13 @@ import { SetDefaultAdminDialogComponent } from './dialogs/set-default-admin-dial
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
export class AppComponent implements OnInit, AfterViewInit {
@HostBinding('class') componentCssClass;
THEMES_CONFIG = THEMES_CONFIG;
window = window;
// config items
topBarTitle = 'Youtube Downloader';
defaultTheme = null;
@@ -69,6 +71,29 @@ export class AppComponent implements OnInit {
}
ngOnInit() {
if (localStorage.getItem('theme')) {
this.setTheme(localStorage.getItem('theme'));
}
this.postsService.open_create_default_admin_dialog.subscribe(open => {
if (open) {
const dialogRef = this.dialog.open(SetDefaultAdminDialogComponent);
dialogRef.afterClosed().subscribe(success => {
if (success) {
if (this.router.url !== '/login') { this.router.navigate(['/login']); }
} else {
console.error('Failed to create default admin account. See logs for details.');
}
});
}
});
}
ngAfterViewInit() {
this.postsService.sidenav = this.sidenav;
}
toggleSidenav() {
this.sidenav.toggle();
}
@@ -89,9 +114,7 @@ export class AppComponent implements OnInit {
// gets the subscriptions
if (this.allowSubscriptions) {
this.postsService.getAllSubscriptions().subscribe(res => {
this.postsService.subscriptions = res['subscriptions'];
})
this.postsService.reloadSubscriptions();
}
}
@@ -124,9 +147,9 @@ export class AppComponent implements OnInit {
this.postsService.setTheme(theme);
this.onSetTheme(this.THEMES_CONFIG[theme]['css_label'], old_theme ? this.THEMES_CONFIG[old_theme]['css_label'] : old_theme);
}
}
onSetTheme(theme, old_theme) {
onSetTheme(theme, old_theme) {
if (old_theme) {
document.body.classList.remove(old_theme);
this.overlayContainer.getContainerElement().classList.remove(old_theme);
@@ -148,27 +171,6 @@ onSetTheme(theme, old_theme) {
event.stopPropagation();
}
ngOnInit() {
if (localStorage.getItem('theme')) {
this.setTheme(localStorage.getItem('theme'));
} else {
//
}
this.postsService.open_create_default_admin_dialog.subscribe(open => {
if (open) {
const dialogRef = this.dialog.open(SetDefaultAdminDialogComponent);
dialogRef.afterClosed().subscribe(success => {
if (success) {
if (this.router.url !== '/login') { this.router.navigate(['/login']); }
} else {
console.error('Failed to create default admin account. See logs for details.');
}
});
}
});
}
getSubscriptions() {
}

View File

@@ -50,7 +50,8 @@ export class CustomPlaylistsComponent implements OnInit {
});
}
goToPlaylist(playlist) {
goToPlaylist(info_obj) {
const playlist = info_obj.file;
const playlistID = playlist.id;
const type = playlist.type;

View File

@@ -31,12 +31,12 @@
<div class="container">
<div class="row justify-content-center">
<ng-container *ngIf="normal_files_received">
<div *ngFor="let file of filtered_files; let i = index" class="mb-2 mt-2" [ngClass]="[ postsService.card_size === 'small' ? 'col-2 small-col' : '', postsService.card_size === 'medium' ? 'col-6 col-lg-4 medium-col' : '', postsService.card_size === 'large' ? 'col-12 large-col' : '' ]">
<div *ngFor="let file of filtered_files; let i = index" class="mb-2 mt-2 d-flex justify-content-center" [ngClass]="[ postsService.card_size === 'small' ? 'col-2 small-col' : '', postsService.card_size === 'medium' ? 'col-6 col-lg-4 medium-col' : '', postsService.card_size === 'large' ? 'col-12 large-col' : '' ]">
<app-unified-file-card [index]="i" [card_size]="postsService.card_size" (goToFile)="goToFile($event)" (goToSubscription)="goToSubscription($event)" [file_obj]="file" [use_youtubedl_archive]="postsService.config['Downloader']['use_youtubedl_archive']" [loading]="false" (deleteFile)="deleteFile($event)"></app-unified-file-card>
</div>
</ng-container>
<ng-container *ngIf="!normal_files_received && loading_files && loading_files.length > 0">
<div *ngFor="let file of loading_files; let i = index" class="mb-2 mt-2" [ngClass]="[ postsService.card_size === 'small' ? 'col-2 small-col' : '', postsService.card_size === 'medium' ? 'col-6 col-lg-4 medium-col' : '', postsService.card_size === 'large' ? 'col-12 large-col' : '' ]">
<div *ngFor="let file of loading_files; let i = index" class="mb-2 mt-2 d-flex justify-content-center" [ngClass]="[ postsService.card_size === 'small' ? 'col-2 small-col' : '', postsService.card_size === 'medium' ? 'col-6 col-lg-4 medium-col' : '', postsService.card_size === 'large' ? 'col-12 large-col' : '' ]">
<app-unified-file-card [index]="i" [card_size]="postsService.card_size" [loading]="true" [theme]="postsService.theme"></app-unified-file-card>
</div>
</ng-container>

View File

@@ -55,17 +55,21 @@ export class RecentVideosComponent implements OnInit {
if (localStorage.getItem('cached_file_count')) {
this.cached_file_count = +localStorage.getItem('cached_file_count');
this.loading_files = Array(this.cached_file_count).fill(0);
console.log(this.loading_files);
}
}
ngOnInit(): void {
if (this.postsService.initialized) {
this.getAllFiles();
}
this.postsService.service_initialized.subscribe(init => {
if (init) {
this.getAllFiles();
}
});
// set filter property to cached
const cached_filter_property = localStorage.getItem('filter_property');
if (cached_filter_property && this.filterProperties[cached_filter_property]) {
@@ -134,28 +138,36 @@ export class RecentVideosComponent implements OnInit {
// navigation
goToFile(file) {
goToFile(info_obj) {
const file = info_obj['file'];
const event = info_obj['event'];
if (this.postsService.config['Extra']['download_only_mode']) {
this.downloadFile(file);
} else {
this.navigateToFile(file);
this.navigateToFile(file, event.ctrlKey);
}
}
navigateToFile(file) {
navigateToFile(file, new_tab) {
localStorage.setItem('player_navigator', this.router.url);
if (file.sub_id) {
const sub = this.postsService.getSubscriptionByID(file.sub_id)
const sub = this.postsService.getSubscriptionByID(file.sub_id);
if (sub.streamingOnly) {
this.router.navigate(['/player', {name: file.id,
url: file.requested_formats ? file.requested_formats[0].url : file.url}]);
// streaming only mode subscriptions
!new_tab ? this.router.navigate(['/player', {name: file.id,
url: file.requested_formats ? file.requested_formats[0].url : file.url}])
: window.open(`/#/player;name=${file.id};url=${file.requested_formats ? file.requested_formats[0].url : file.url}`);
} else {
this.router.navigate(['/player', {fileNames: file.id,
type: file.isAudio ? 'audio' : 'video', subscriptionName: sub.name,
subPlaylist: sub.isPlaylist}]);
// normal subscriptions
!new_tab ? this.router.navigate(['/player', {fileNames: file.id,
type: file.isAudio ? 'audio' : 'video', subscriptionName: sub.name,
subPlaylist: sub.isPlaylist}])
: window.open(`/#/player;fileNames=${file.id};type=${file.isAudio ? 'audio' : 'video'};subscriptionName=${sub.name};subPlaylist=${sub.isPlaylist}`);
}
} else {
this.router.navigate(['/player', {type: file.isAudio ? 'audio' : 'video', uid: file.uid}]);
// normal files
!new_tab ? this.router.navigate(['/player', {type: file.isAudio ? 'audio' : 'video', uid: file.uid}])
: window.open(`/#/player;type=${file.isAudio ? 'audio' : 'video'};uid=${file.uid}`);
}
}
@@ -177,7 +189,6 @@ export class RecentVideosComponent implements OnInit {
const type = file.isAudio ? 'audio' : 'video';
const ext = type === 'audio' ? '.mp3' : '.mp4'
const sub = this.postsService.getSubscriptionByID(file.sub_id);
console.log(sub.isPlaylist)
this.postsService.downloadFileFromServer(file.id, type, null, null, sub.name, sub.isPlaylist,
this.postsService.user ? this.postsService.user.uid : null, null).subscribe(res => {
const blob: Blob = res;

View File

@@ -25,11 +25,11 @@
<button mat-menu-item>Placeholder</button>
</ng-container>
</mat-menu>
<mat-card [matTooltip]="null" (click)="navigateToFile()" matRipple class="file-mat-card" [ngClass]="{'small-mat-card': card_size === 'small', 'file-mat-card': card_size === 'medium', 'large-mat-card': card_size === 'large', 'mat-elevation-z4': !elevated, 'mat-elevation-z8': elevated}">
<mat-card [matTooltip]="null" (click)="navigateToFile($event)" matRipple class="file-mat-card" [ngClass]="{'small-mat-card': card_size === 'small', 'file-mat-card': card_size === 'medium', 'large-mat-card': card_size === 'large', 'mat-elevation-z4': !elevated, 'mat-elevation-z8': elevated}">
<div style="padding:5px">
<div *ngIf="!loading && file_obj.thumbnailURL" class="img-div">
<div style="position: relative">
<img [ngClass]="{'image-small': card_size === 'small', 'image': card_size === 'medium', 'image-large': card_size === 'large'}" [src]="file_obj.thumbnailURL" alt="Thumbnail">
<img [ngClass]="{'image-small': card_size === 'small', 'image': card_size === 'medium', 'image-large': card_size === 'large'}" [src]="file_obj.thumbnailBlob ? thumbnailBlobURL : file_obj.thumbnailURL" alt="Thumbnail">
<div class="duration-time">
{{file_length}}
</div>

View File

@@ -103,6 +103,7 @@
background: rgba(255,255,255,0.6);
padding-left: 5px;
padding-right: 5px;
color: black;
}
.download-time {

View File

@@ -1,6 +1,7 @@
import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { VideoInfoDialogComponent } from 'app/dialogs/video-info-dialog/video-info-dialog.component';
import { DomSanitizer } from '@angular/platform-browser';
@Component({
selector: 'app-unified-file-card',
@@ -16,6 +17,10 @@ export class UnifiedFileCardComponent implements OnInit {
type = null;
elevated = false;
// optional vars
thumbnailBlobURL = null;
// input/output
@Input() loading = true;
@Input() theme = null;
@Input() file_obj = null;
@@ -35,12 +40,19 @@ export class UnifiedFileCardComponent implements OnInit {
big: 250x200
*/
constructor(private dialog: MatDialog) { }
constructor(private dialog: MatDialog, private sanitizer: DomSanitizer) { }
ngOnInit(): void {
if (!this.loading) {
this.file_length = fancyTimeFormat(this.file_obj.duration);
}
if (this.file_obj && this.file_obj.thumbnailBlob) {
const mime = getMimeByFilename(this.file_obj.thumbnailPath);
const blob = new Blob([new Uint8Array(this.file_obj.thumbnailBlob.data)], {type: mime});
const bloburl = URL.createObjectURL(blob);
this.thumbnailBlobURL = this.sanitizer.bypassSecurityTrustUrl(bloburl);
}
}
emitDeleteFile(blacklistMode = false) {
@@ -51,8 +63,8 @@ export class UnifiedFileCardComponent implements OnInit {
});
}
navigateToFile() {
this.goToFile.emit(this.file_obj);
navigateToFile(event) {
this.goToFile.emit({file: this.file_obj, event: event});
}
navigateToSubscription() {
@@ -97,3 +109,16 @@ function fancyTimeFormat(time) {
ret += '' + secs;
return ret;
}
function getMimeByFilename(name) {
switch (name.substring(name.length-4, name.length)) {
case '.jpg':
return 'image/jpeg';
case '.png':
return 'image/png';
case 'webp':
return 'image/webp';
default:
return null;
}
}

View File

@@ -164,7 +164,7 @@
<br/>
<div class="centered big" id="bar_div" *ngIf="current_download && current_download.downloading; else nofile">
<div class="margined">
<div [ngClass]="(+percentDownloaded > 99)?'make-room-for-spinner':'equal-sizes'" style="display: inline-block; width: 100%; padding-left: 20px" *ngIf="current_download.percent_complete && current_download.percent_complete > 15;else indeterminateprogress">
<div [ngClass]="(+percentDownloaded > 99)?'make-room-for-spinner':'equal-sizes'" style="display: inline-block; width: 100%; padding-left: 20px" *ngIf="current_download.percent_complete && current_download.percent_complete > 1;else indeterminateprogress">
<mat-progress-bar style="border-radius: 5px;" mode="determinate" value="{{percentDownloaded}}"></mat-progress-bar>
<br/>
</div>
@@ -181,10 +181,12 @@
</ng-template>
<app-recent-videos></app-recent-videos>
<br/>
<h4 style="text-align: center">Custom playlists</h4>
<app-custom-playlists></app-custom-playlists>
<ng-container *ngIf="cachedFileManagerEnabled || fileManagerEnabled">
<app-recent-videos></app-recent-videos>
<br/>
<h4 style="text-align: center">Custom playlists</h4>
<app-custom-playlists></app-custom-playlists>
</ng-container>
<!--<div style="margin: 20px" *ngIf="fileManagerEnabled && (!postsService.isLoggedIn || postsService.permissions.includes('filemanager'))">
<mat-accordion>

View File

@@ -82,8 +82,9 @@ export class MainComponent implements OnInit {
useDefaultDownloadingAgent = true;
customDownloadingAgent = null;
// formats cache
// cache
cachedAvailableFormats = {};
cachedFileManagerEnabled = localStorage.getItem('cached_filemanager_enabled') === 'true';
// youtube api
youtubeSearchEnabled = false;
@@ -232,7 +233,8 @@ export class MainComponent implements OnInit {
async loadConfig() {
// loading config
this.fileManagerEnabled = this.postsService.config['Extra']['file_manager_enabled'];
this.fileManagerEnabled = this.postsService.config['Extra']['file_manager_enabled']
&& (!this.postsService.isLoggedIn || this.postsService.permissions.includes('filemanager'));
this.downloadOnlyMode = this.postsService.config['Extra']['download_only_mode'];
this.allowMultiDownloadMode = this.postsService.config['Extra']['allow_multi_download_mode'];
this.audioFolderPath = this.postsService.config['Downloader']['path-audio'];
@@ -261,6 +263,10 @@ export class MainComponent implements OnInit {
}
// set final cache items
localStorage.setItem('cached_filemanager_enabled', this.fileManagerEnabled.toString());
this.cachedFileManagerEnabled = this.fileManagerEnabled;
if (this.allowAdvancedDownload) {
if (localStorage.getItem('customArgsEnabled') !== null) {
this.customArgsEnabled = localStorage.getItem('customArgsEnabled') === 'true';
@@ -1033,8 +1039,8 @@ export class MainComponent implements OnInit {
}
} else if (format_obj.type === 'video') {
// check if video format is mp4
const key = format.height.toString();
if (format.ext === 'mp4') {
const key = format.format_note.replace('p', '');
if (format.ext === 'mp4' || format.ext === 'mkv' || format.ext === 'webm') {
format_obj['height'] = format.height;
format_obj['acodec'] = format.acodec;
format_obj['format_id'] = format.format_id;

View File

@@ -1,4 +1,4 @@
import { Component, OnInit, HostListener, EventEmitter, OnDestroy } from '@angular/core';
import { Component, OnInit, HostListener, EventEmitter, OnDestroy, AfterViewInit } from '@angular/core';
import { VgAPI } from 'ngx-videogular';
import { PostsService } from 'app/posts.services';
import { ActivatedRoute, Router } from '@angular/router';
@@ -20,7 +20,7 @@ export interface IMedia {
templateUrl: './player.component.html',
styleUrls: ['./player.component.css']
})
export class PlayerComponent implements OnInit, OnDestroy {
export class PlayerComponent implements OnInit, AfterViewInit, OnDestroy {
playlist: Array<IMedia> = [];
original_playlist: string = null;
@@ -95,6 +95,10 @@ export class PlayerComponent implements OnInit, OnDestroy {
}
}
ngAfterViewInit() {
this.postsService.sidenav.close();
}
ngOnDestroy() {
// prevents volume save feature from running in the background
clearInterval(this.save_volume_timer);

View File

@@ -15,16 +15,14 @@ import * as Fingerprint2 from 'fingerprintjs2';
@Injectable()
export class PostsService implements CanActivate {
path = '';
audioFolder = '';
videoFolder = '';
startPath = null; // 'http://localhost:17442/';
startPathSSL = null; // 'https://localhost:17442/'
handShakeComplete = false;
// local settings
THEMES_CONFIG = THEMES_CONFIG;
theme;
card_size = 'medium';
sidepanel_mode = 'over';
settings_changed = new BehaviorSubject<boolean>(false);
// auth
auth_token = '4241b401-7236-493e-92b5-b72696b9d853';
session_id = null;
httpOptions = null;
@@ -41,20 +39,24 @@ export class PostsService implements CanActivate {
available_permissions = null;
// behavior subjects
reload_config = new BehaviorSubject<boolean>(false);
config_reloaded = new BehaviorSubject<boolean>(false);
service_initialized = new BehaviorSubject<boolean>(false);
initialized = false;
settings_changed = new BehaviorSubject<boolean>(false);
open_create_default_admin_dialog = new BehaviorSubject<boolean>(false);
// app status
initialized = false;
// global vars
config = null;
subscriptions = null;
sidenav = null;
constructor(private http: HttpClient, private router: Router, @Inject(DOCUMENT) private document: Document,
public snackBar: MatSnackBar) {
console.log('PostsService Initialized...');
// this.startPath = window.location.href + '/api/';
// this.startPathSSL = window.location.href + '/api/';
this.path = this.document.location.origin + '/api/';
if (isDevMode()) {
@@ -152,14 +154,6 @@ export class PostsService implements CanActivate {
});
}
getVideoFolder() {
return this.http.get(this.startPath + 'videofolder');
}
getAudioFolder() {
return this.http.get(this.startPath + 'audiofolder');
}
// tslint:disable-next-line: max-line-length
makeMP3(url: string, selectedQuality: string, customQualityConfiguration: string, customArgs: string = null, customOutput: string = null, youtubeUsername: string = null, youtubePassword: string = null, ui_uid = null) {
return this.http.post(this.path + 'tomp3', {url: url,
@@ -384,7 +378,7 @@ export class PostsService implements CanActivate {
// user methods
login(username, password) {
const call = this.http.post(this.path + 'auth/login', {userid: username, password: password}, this.httpOptions);
const call = this.http.post(this.path + 'auth/login', {username: username, password: password}, this.httpOptions);
return call;
}
@@ -398,6 +392,8 @@ export class PostsService implements CanActivate {
}, err => {
if (err.status === 401) {
this.sendToLogin();
this.token = null;
this.resetHttpParams();
}
console.log(err);
});
@@ -414,14 +410,7 @@ export class PostsService implements CanActivate {
this.router.navigate(['/login']);
}
// resets http params
this.http_params = `apiKey=${this.auth_token}&sessionID=${this.session_id}`
this.httpOptions = {
params: new HttpParams({
fromString: this.http_params
}),
};
this.resetHttpParams();
}
// user methods
@@ -446,12 +435,29 @@ export class PostsService implements CanActivate {
this.openSnackBar('You must log in to access this page!');
}
resetHttpParams() {
// resets http params
this.http_params = `apiKey=${this.auth_token}&sessionID=${this.session_id}`
this.httpOptions = {
params: new HttpParams({
fromString: this.http_params
}),
};
}
setInitialized() {
this.service_initialized.next(true);
this.initialized = true;
this.config_reloaded.next(true);
}
reloadSubscriptions() {
this.getAllSubscriptions().subscribe(res => {
this.subscriptions = res['subscriptions'];
});
}
adminExists() {
return this.http.post(this.path + 'auth/adminExists', {}, this.httpOptions);
}

View File

@@ -128,7 +128,11 @@
</div>
<div class="col-12 mt-2">
<mat-checkbox color="accent" [(ngModel)]="new_config['Downloader']['safe_download_override']"><ng-container i18n="Safe download override setting">Safe download override</ng-container></mat-checkbox>
<mat-checkbox color="accent" [(ngModel)]="new_config['Downloader']['include_thumbnail']"><ng-container i18n="Include thumbnail setting">Include thumbnail</ng-container></mat-checkbox>
</div>
<div class="col-12 mt-2">
<mat-checkbox color="accent" [(ngModel)]="new_config['Downloader']['include_metadata']"><ng-container i18n="Include metadata setting">Include metadata</ng-container></mat-checkbox>
</div>
<div class="col-12 mt-2">
@@ -292,8 +296,50 @@
</ng-template>
</mat-tab>
<mat-tab *ngIf="postsService.config && postsService.config.Advanced.multi_user_mode" label="Users" i18n-label="Users settings label">
<div style="margin-left: 48px; margin-top: 24px;">
<mat-checkbox color="accent" [(ngModel)]="new_config['Users']['allow_registration']"><ng-container i18n="Allow registration setting">Allow user registration</ng-container></mat-checkbox>
<div style="margin-left: 48px; margin-top: 24px; margin-bottom: -25px;">
<div>
<mat-checkbox color="accent" [(ngModel)]="new_config['Users']['allow_registration']"><ng-container i18n="Allow registration setting">Allow user registration</ng-container></mat-checkbox>
</div>
<mat-divider></mat-divider>
<mat-form-field style="margin-top: 15px;">
<mat-select [(ngModel)]="new_config['Users']['auth_method']" placeholder="Auth method" i18n-placeholder="Auth method select">
<mat-option value="internal">
<ng-container i18n="Internal auth method">Internal</ng-container>
</mat-option>
<mat-option value="ldap">
<ng-container i18n="LDAP auth method">LDAP</ng-container>
</mat-option>
</mat-select>
</mat-form-field>
<div *ngIf="new_config['Users']['auth_method'] === 'ldap'">
<div>
<mat-form-field>
<input matInput i18n-placeholder="LDAP URL" placeholder="LDAP URL" [(ngModel)]="new_config['Users']['ldap_config']['url']">
</mat-form-field>
</div>
<div>
<mat-form-field>
<input matInput i18n-placeholder="Bind DN" placeholder="Bind DN" [(ngModel)]="new_config['Users']['ldap_config']['bindDN']">
</mat-form-field>
</div>
<div>
<mat-form-field>
<input matInput i18n-placeholder="Bind Credentials" placeholder="Bind Credentials" [(ngModel)]="new_config['Users']['ldap_config']['bindCredentials']">
</mat-form-field>
</div>
<div>
<mat-form-field>
<input matInput i18n-placeholder="Search Base" placeholder="Search Base" [(ngModel)]="new_config['Users']['ldap_config']['searchBase']">
</mat-form-field>
</div>
<div>
<mat-form-field>
<input matInput i18n-placeholder="Search Filter" placeholder="Search Filter" [(ngModel)]="new_config['Users']['ldap_config']['searchFilter']">
</mat-form-field>
</div>
</div>
<mat-divider></mat-divider>
</div>
<app-modify-users></app-modify-users>
</mat-tab>

View File

@@ -80,6 +80,7 @@ export class SubscriptionsComponent implements OnInit {
} else {
this.channel_subscriptions.push(result);
}
this.postsService.reloadSubscriptions();
}
});
}
@@ -96,6 +97,7 @@ export class SubscriptionsComponent implements OnInit {
if (success) {
this.openSnackBar(`${sub.name} successfully deleted!`)
this.getSubscriptions();
this.postsService.reloadSubscriptions();
}
})
}