Isaac Abadi 5 years ago
commit 25bfb9e518

@ -2049,6 +2049,52 @@ app.post('/api/getFile', optionalJwt, function (req, res) {
}
});
app.post('/api/getAllFiles', optionalJwt, function (req, res) {
// these are returned
let files = [];
let playlists = [];
let subscription_files = [];
let videos = null;
let audios = null;
let audio_playlists = null;
let video_playlists = null;
let subscriptions = subscriptions_api.getAllSubscriptions(req.isAuthenticated() ? req.user.uid : null);
// get basic info depending on multi-user mode being enabled
if (req.isAuthenticated()) {
videos = auth_api.getUserVideos(req.user.uid, 'video');
audios = auth_api.getUserVideos(req.user.uid, 'audio');
audio_playlists = auth_api.getUserPlaylists(req.user.uid, 'audio');
video_playlists = auth_api.getUserPlaylists(req.user.uid, 'video');
} else {
videos = db.get('files.audio').value();
audios = db.get('files.video').value();
audio_playlists = db.get('playlists.audio').value();
video_playlists = db.get('playlists.video').value();
}
files = videos.concat(audios);
playlists = video_playlists.concat(audio_playlists);
// loop through subscriptions and add videos
for (let i = 0; i < subscriptions.length; i++) {
sub = subscriptions[i];
if (!sub.videos) continue;
// add sub id for UI
for (let j = 0; j < sub.videos.length; j++) {
sub.videos[j].sub_id = sub.id;
}
files = files.concat(sub.videos);
}
res.send({
files: files,
playlists: playlists
});
});
// video sharing
app.post('/api/enableSharing', optionalJwt, function(req, res) {
var type = req.body.type;
@ -2336,13 +2382,16 @@ app.post('/api/createPlaylist', optionalJwt, async (req, res) => {
let fileNames = req.body.fileNames;
let type = req.body.type;
let thumbnailURL = req.body.thumbnailURL;
let duration = req.body.duration;
let new_playlist = {
'name': playlistName,
name: playlistName,
fileNames: fileNames,
id: shortid.generate(),
thumbnailURL: thumbnailURL,
type: type
type: type,
registered: Date.now(),
duration: duration
};
if (req.isAuthenticated()) {
@ -2543,7 +2592,7 @@ app.post('/api/downloadFile', optionalJwt, async (req, res) => {
else
basePath = config_api.getConfigItem('ytdl_subscriptions_base_path');
file = path.join(__dirname, basePath, (subscriptionPlaylist === 'true' ? 'playlists' : 'channels'), subscriptionName, fileNames + ext);
file = path.join(__dirname, basePath, (subscriptionPlaylist === true || subscriptionPlaylist === 'true' ? 'playlists' : 'channels'), subscriptionName, fileNames + ext);
}
} else {
for (let i = 0; i < fileNames.length; i++) {

@ -37,8 +37,7 @@
"Subscriptions": {
"allow_subscriptions": true,
"subscriptions_base_path": "subscriptions/",
"subscriptions_check_interval": "300",
"subscriptions_use_youtubedl_archive": true
"subscriptions_check_interval": "300"
},
"Users": {
"base_path": "users/",

@ -37,8 +37,7 @@
"Subscriptions": {
"allow_subscriptions": true,
"subscriptions_base_path": "subscriptions/",
"subscriptions_check_interval": "300",
"subscriptions_use_youtubedl_archive": true
"subscriptions_check_interval": "300"
},
"Users": {
"base_path": "users/",

@ -214,8 +214,7 @@ DEFAULT_CONFIG = {
"Subscriptions": {
"allow_subscriptions": true,
"subscriptions_base_path": "subscriptions/",
"subscriptions_check_interval": "300",
"subscriptions_use_youtubedl_archive": true
"subscriptions_check_interval": "300"
},
"Users": {
"base_path": "users/",

@ -116,10 +116,6 @@ let CONFIG_ITEMS = {
'key': 'ytdl_subscriptions_check_interval',
'path': 'YoutubeDLMaterial.Subscriptions.subscriptions_check_interval'
},
'ytdl_subscriptions_use_youtubedl_archive': {
'key': 'ytdl_use_youtubedl_archive',
'path': 'YoutubeDLMaterial.Subscriptions.subscriptions_use_youtubedl_archive'
},
// Users
'ytdl_users_base_path': {

@ -61,6 +61,10 @@ function registerFileDB(file_path, type, multiUserMode = null, sub = null) {
} else {
sub_db = db.get('subscriptions').find({id: sub.id});
}
if (sub_db.get('videos').find({id: file_object.id}).value()) {
logger.verbose(`Subscription video ${file_object.id} already exists, skipping DB registration.`);
return null;
}
sub_db.get('videos').push(file_object).write();
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -190,6 +190,32 @@ ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
is-retina
MIT
The MIT License (MIT)
Copyright (c) 2014 Kyle Mathews
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
ng-lazyload-image
MIT
The MIT License (MIT)
@ -215,6 +241,30 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
ngx-avatar
MIT
The MIT License
Copyright (c) 2020 Haithem Mosbahi, ngx-avatar
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
ngx-file-drop
MIT
@ -658,6 +708,9 @@ Apache-2.0
ts-md5
MIT
tslib
Apache-2.0
Apache License

@ -14,5 +14,5 @@
<link rel="stylesheet" href="styles.5112d6db78cf21541598.css"></head>
<body>
<app-root></app-root>
<script src="runtime-es2015.42092efdfb84b81949da.js" type="module"></script><script src="runtime-es5.42092efdfb84b81949da.js" nomodule defer></script><script src="polyfills-es5.7f923c8f5afda210edd3.js" nomodule defer></script><script src="polyfills-es2015.5b408f108bcea938a7e2.js" type="module"></script><script src="main-es2015.0cbc545a4a3bee376826.js" type="module"></script><script src="main-es5.0cbc545a4a3bee376826.js" nomodule defer></script></body>
<script src="runtime-es2015.30745161154a1499dbf2.js" type="module"></script><script src="runtime-es5.30745161154a1499dbf2.js" nomodule defer></script><script src="polyfills-es5.7f923c8f5afda210edd3.js" nomodule defer></script><script src="polyfills-es2015.5b408f108bcea938a7e2.js" type="module"></script><script src="main-es2015.63a61681b752f95b7352.js" type="module"></script><script src="main-es5.63a61681b752f95b7352.js" nomodule defer></script></body>
</html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -1 +1 @@
!function(e){function r(r){for(var n,a,i=r[0],c=r[1],l=r[2],p=0,s=[];p<i.length;p++)a=i[p],Object.prototype.hasOwnProperty.call(o,a)&&o[a]&&s.push(o[a][0]),o[a]=0;for(n in c)Object.prototype.hasOwnProperty.call(c,n)&&(e[n]=c[n]);for(f&&f(r);s.length;)s.shift()();return u.push.apply(u,l||[]),t()}function t(){for(var e,r=0;r<u.length;r++){for(var t=u[r],n=!0,i=1;i<t.length;i++)0!==o[t[i]]&&(n=!1);n&&(u.splice(r--,1),e=a(a.s=t[0]))}return e}var n={},o={0:0},u=[];function a(r){if(n[r])return n[r].exports;var t=n[r]={i:r,l:!1,exports:{}};return e[r].call(t.exports,t,t.exports,a),t.l=!0,t.exports}a.e=function(e){var r=[],t=o[e];if(0!==t)if(t)r.push(t[2]);else{var n=new Promise((function(r,n){t=o[e]=[r,n]}));r.push(t[2]=n);var u,i=document.createElement("script");i.charset="utf-8",i.timeout=120,a.nc&&i.setAttribute("nonce",a.nc),i.src=function(e){return a.p+""+({}[e]||e)+"-es2015."+{1:"c401a556fe28cac6abab"}[e]+".js"}(e);var c=new Error;u=function(r){i.onerror=i.onload=null,clearTimeout(l);var t=o[e];if(0!==t){if(t){var n=r&&("load"===r.type?"missing":r.type),u=r&&r.target&&r.target.src;c.message="Loading chunk "+e+" failed.\n("+n+": "+u+")",c.name="ChunkLoadError",c.type=n,c.request=u,t[1](c)}o[e]=void 0}};var l=setTimeout((function(){u({type:"timeout",target:i})}),12e4);i.onerror=i.onload=u,document.head.appendChild(i)}return Promise.all(r)},a.m=e,a.c=n,a.d=function(e,r,t){a.o(e,r)||Object.defineProperty(e,r,{enumerable:!0,get:t})},a.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},a.t=function(e,r){if(1&r&&(e=a(e)),8&r)return e;if(4&r&&"object"==typeof e&&e&&e.__esModule)return e;var t=Object.create(null);if(a.r(t),Object.defineProperty(t,"default",{enumerable:!0,value:e}),2&r&&"string"!=typeof e)for(var n in e)a.d(t,n,(function(r){return e[r]}).bind(null,n));return t},a.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return a.d(r,"a",r),r},a.o=function(e,r){return Object.prototype.hasOwnProperty.call(e,r)},a.p="",a.oe=function(e){throw console.error(e),e};var i=window.webpackJsonp=window.webpackJsonp||[],c=i.push.bind(i);i.push=r,i=i.slice();for(var l=0;l<i.length;l++)r(i[l]);var f=c;t()}([]);
!function(e){function r(r){for(var n,a,i=r[0],c=r[1],l=r[2],p=0,s=[];p<i.length;p++)a=i[p],Object.prototype.hasOwnProperty.call(o,a)&&o[a]&&s.push(o[a][0]),o[a]=0;for(n in c)Object.prototype.hasOwnProperty.call(c,n)&&(e[n]=c[n]);for(f&&f(r);s.length;)s.shift()();return u.push.apply(u,l||[]),t()}function t(){for(var e,r=0;r<u.length;r++){for(var t=u[r],n=!0,i=1;i<t.length;i++)0!==o[t[i]]&&(n=!1);n&&(u.splice(r--,1),e=a(a.s=t[0]))}return e}var n={},o={0:0},u=[];function a(r){if(n[r])return n[r].exports;var t=n[r]={i:r,l:!1,exports:{}};return e[r].call(t.exports,t,t.exports,a),t.l=!0,t.exports}a.e=function(e){var r=[],t=o[e];if(0!==t)if(t)r.push(t[2]);else{var n=new Promise((function(r,n){t=o[e]=[r,n]}));r.push(t[2]=n);var u,i=document.createElement("script");i.charset="utf-8",i.timeout=120,a.nc&&i.setAttribute("nonce",a.nc),i.src=function(e){return a.p+""+({}[e]||e)+"-es2015."+{1:"95e37a140299b9d6887a"}[e]+".js"}(e);var c=new Error;u=function(r){i.onerror=i.onload=null,clearTimeout(l);var t=o[e];if(0!==t){if(t){var n=r&&("load"===r.type?"missing":r.type),u=r&&r.target&&r.target.src;c.message="Loading chunk "+e+" failed.\n("+n+": "+u+")",c.name="ChunkLoadError",c.type=n,c.request=u,t[1](c)}o[e]=void 0}};var l=setTimeout((function(){u({type:"timeout",target:i})}),12e4);i.onerror=i.onload=u,document.head.appendChild(i)}return Promise.all(r)},a.m=e,a.c=n,a.d=function(e,r,t){a.o(e,r)||Object.defineProperty(e,r,{enumerable:!0,get:t})},a.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},a.t=function(e,r){if(1&r&&(e=a(e)),8&r)return e;if(4&r&&"object"==typeof e&&e&&e.__esModule)return e;var t=Object.create(null);if(a.r(t),Object.defineProperty(t,"default",{enumerable:!0,value:e}),2&r&&"string"!=typeof e)for(var n in e)a.d(t,n,(function(r){return e[r]}).bind(null,n));return t},a.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return a.d(r,"a",r),r},a.o=function(e,r){return Object.prototype.hasOwnProperty.call(e,r)},a.p="",a.oe=function(e){throw console.error(e),e};var i=window.webpackJsonp=window.webpackJsonp||[],c=i.push.bind(i);i.push=r,i=i.slice();for(var l=0;l<i.length;l++)r(i[l]);var f=c;t()}([]);

@ -1 +1 @@
!function(e){function r(r){for(var n,a,i=r[0],c=r[1],l=r[2],p=0,s=[];p<i.length;p++)a=i[p],Object.prototype.hasOwnProperty.call(o,a)&&o[a]&&s.push(o[a][0]),o[a]=0;for(n in c)Object.prototype.hasOwnProperty.call(c,n)&&(e[n]=c[n]);for(f&&f(r);s.length;)s.shift()();return u.push.apply(u,l||[]),t()}function t(){for(var e,r=0;r<u.length;r++){for(var t=u[r],n=!0,i=1;i<t.length;i++)0!==o[t[i]]&&(n=!1);n&&(u.splice(r--,1),e=a(a.s=t[0]))}return e}var n={},o={0:0},u=[];function a(r){if(n[r])return n[r].exports;var t=n[r]={i:r,l:!1,exports:{}};return e[r].call(t.exports,t,t.exports,a),t.l=!0,t.exports}a.e=function(e){var r=[],t=o[e];if(0!==t)if(t)r.push(t[2]);else{var n=new Promise((function(r,n){t=o[e]=[r,n]}));r.push(t[2]=n);var u,i=document.createElement("script");i.charset="utf-8",i.timeout=120,a.nc&&i.setAttribute("nonce",a.nc),i.src=function(e){return a.p+""+({}[e]||e)+"-es5."+{1:"c401a556fe28cac6abab"}[e]+".js"}(e);var c=new Error;u=function(r){i.onerror=i.onload=null,clearTimeout(l);var t=o[e];if(0!==t){if(t){var n=r&&("load"===r.type?"missing":r.type),u=r&&r.target&&r.target.src;c.message="Loading chunk "+e+" failed.\n("+n+": "+u+")",c.name="ChunkLoadError",c.type=n,c.request=u,t[1](c)}o[e]=void 0}};var l=setTimeout((function(){u({type:"timeout",target:i})}),12e4);i.onerror=i.onload=u,document.head.appendChild(i)}return Promise.all(r)},a.m=e,a.c=n,a.d=function(e,r,t){a.o(e,r)||Object.defineProperty(e,r,{enumerable:!0,get:t})},a.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},a.t=function(e,r){if(1&r&&(e=a(e)),8&r)return e;if(4&r&&"object"==typeof e&&e&&e.__esModule)return e;var t=Object.create(null);if(a.r(t),Object.defineProperty(t,"default",{enumerable:!0,value:e}),2&r&&"string"!=typeof e)for(var n in e)a.d(t,n,(function(r){return e[r]}).bind(null,n));return t},a.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return a.d(r,"a",r),r},a.o=function(e,r){return Object.prototype.hasOwnProperty.call(e,r)},a.p="",a.oe=function(e){throw console.error(e),e};var i=window.webpackJsonp=window.webpackJsonp||[],c=i.push.bind(i);i.push=r,i=i.slice();for(var l=0;l<i.length;l++)r(i[l]);var f=c;t()}([]);
!function(e){function r(r){for(var n,a,i=r[0],c=r[1],l=r[2],p=0,s=[];p<i.length;p++)a=i[p],Object.prototype.hasOwnProperty.call(o,a)&&o[a]&&s.push(o[a][0]),o[a]=0;for(n in c)Object.prototype.hasOwnProperty.call(c,n)&&(e[n]=c[n]);for(f&&f(r);s.length;)s.shift()();return u.push.apply(u,l||[]),t()}function t(){for(var e,r=0;r<u.length;r++){for(var t=u[r],n=!0,i=1;i<t.length;i++)0!==o[t[i]]&&(n=!1);n&&(u.splice(r--,1),e=a(a.s=t[0]))}return e}var n={},o={0:0},u=[];function a(r){if(n[r])return n[r].exports;var t=n[r]={i:r,l:!1,exports:{}};return e[r].call(t.exports,t,t.exports,a),t.l=!0,t.exports}a.e=function(e){var r=[],t=o[e];if(0!==t)if(t)r.push(t[2]);else{var n=new Promise((function(r,n){t=o[e]=[r,n]}));r.push(t[2]=n);var u,i=document.createElement("script");i.charset="utf-8",i.timeout=120,a.nc&&i.setAttribute("nonce",a.nc),i.src=function(e){return a.p+""+({}[e]||e)+"-es5."+{1:"95e37a140299b9d6887a"}[e]+".js"}(e);var c=new Error;u=function(r){i.onerror=i.onload=null,clearTimeout(l);var t=o[e];if(0!==t){if(t){var n=r&&("load"===r.type?"missing":r.type),u=r&&r.target&&r.target.src;c.message="Loading chunk "+e+" failed.\n("+n+": "+u+")",c.name="ChunkLoadError",c.type=n,c.request=u,t[1](c)}o[e]=void 0}};var l=setTimeout((function(){u({type:"timeout",target:i})}),12e4);i.onerror=i.onload=u,document.head.appendChild(i)}return Promise.all(r)},a.m=e,a.c=n,a.d=function(e,r,t){a.o(e,r)||Object.defineProperty(e,r,{enumerable:!0,get:t})},a.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},a.t=function(e,r){if(1&r&&(e=a(e)),8&r)return e;if(4&r&&"object"==typeof e&&e&&e.__esModule)return e;var t=Object.create(null);if(a.r(t),Object.defineProperty(t,"default",{enumerable:!0,value:e}),2&r&&"string"!=typeof e)for(var n in e)a.d(t,n,(function(r){return e[r]}).bind(null,n));return t},a.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return a.d(r,"a",r),r},a.o=function(e,r){return Object.prototype.hasOwnProperty.call(e,r)},a.p="",a.oe=function(e){throw console.error(e),e};var i=window.webpackJsonp=window.webpackJsonp||[],c=i.push.bind(i);i.push=r,i=i.slice();for(var l=0;l<i.length;l++)r(i[l]);var f=c;t()}([]);

@ -123,7 +123,7 @@ async function getSubscriptionInfo(sub, user_uid = null) {
}
}
const useArchive = config_api.getConfigItem('ytdl_subscriptions_use_youtubedl_archive');
const useArchive = config_api.getConfigItem('ytdl_use_youtubedl_archive');
if (useArchive && !sub.archive) {
// must create the archive
const archive_dir = path.join(__dirname, basePath, 'archives', sub.name);
@ -197,7 +197,7 @@ async function deleteSubscriptionFile(sub, file, deleteForever, file_uid = null,
basePath = config_api.getConfigItem('ytdl_subscriptions_base_path');
sub_db = db.get('subscriptions').find({id: sub.id});
}
const useArchive = config_api.getConfigItem('ytdl_subscriptions_use_youtubedl_archive');
const useArchive = config_api.getConfigItem('ytdl_use_youtubedl_archive');
const appendedBasePath = getAppendedBasePath(sub, basePath);
const name = file;
let retrievedID = null;
@ -273,7 +273,7 @@ async function getVideosForSub(sub, user_uid = null) {
else
basePath = config_api.getConfigItem('ytdl_subscriptions_base_path');
const useArchive = config_api.getConfigItem('ytdl_subscriptions_use_youtubedl_archive');
const useArchive = config_api.getConfigItem('ytdl_use_youtubedl_archive');
let appendedBasePath = null
appendedBasePath = getAppendedBasePath(sub, basePath);
@ -411,7 +411,6 @@ function handleOutputJSON(sub, sub_db, output_json, multiUserMode = null, reset_
// add to db
sub_db.get('videos').push(output_json).write();
} else {
// TODO: make multiUserMode obj
db_api.registerFileDB(path.basename(output_json['_filename']), sub.type, multiUserMode, sub);
}
}

19
package-lock.json generated

@ -6588,6 +6588,11 @@
"integrity": "sha512-qgDYXFSR5WvEfuS5dMj6oTMEbrrSaM0CrFk2Yiq/gXnBvD9pMa2jGXxyhGLfvhZpuMZe18CJpFxAt3CRs42NMg==",
"dev": true
},
"is-retina": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/is-retina/-/is-retina-1.0.3.tgz",
"integrity": "sha1-10AbKGvqKuN/Ykd1iN5QTQuGR+M="
},
"is-stream": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz",
@ -9137,6 +9142,15 @@
"resolved": "https://registry.npmjs.org/ng-lazyload-image/-/ng-lazyload-image-7.1.0.tgz",
"integrity": "sha512-1fip2FdPBDRnjGyBokI/DupBxOnrKh2lbtT8X8N1oPbE3KBZXXl82VIKcK2Sx+XQD67/+VtFzlISmrgsatzYuw=="
},
"ngx-avatar": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/ngx-avatar/-/ngx-avatar-4.0.0.tgz",
"integrity": "sha512-Uk40UXl26RvDy1ori9NDsGFB+f84AaxMnsIwZA6JPJK0pLcbo3F4vZTmzLZeOusOw1Qtgk5IzF630jo06keXwQ==",
"requires": {
"is-retina": "^1.0.3",
"ts-md5": "^1.2.4"
}
},
"ngx-file-drop": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/ngx-file-drop/-/ngx-file-drop-9.0.1.tgz",
@ -13457,6 +13471,11 @@
"utf8-byte-length": "^1.0.1"
}
},
"ts-md5": {
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/ts-md5/-/ts-md5-1.2.7.tgz",
"integrity": "sha512-emODogvKGWi1KO1l9c6YxLMBn6CEH3VrH5mVPIyOtxBG52BvV4jP3GWz6bOZCz61nLgBc3ffQYE4+EHfCD+V7w=="
},
"ts-node": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-3.0.6.tgz",

@ -36,6 +36,7 @@
"fingerprintjs2": "^2.1.0",
"nan": "^2.14.1",
"ng-lazyload-image": "^7.0.1",
"ngx-avatar": "^4.0.0",
"ngx-file-drop": "^9.0.1",
"ngx-videogular": "^9.0.1",
"rxjs": "^6.5.3",

@ -38,12 +38,16 @@
</div>
<div class="sidenav-container" style="height: calc(100% - 64px)">
<mat-sidenav-container style="height: 100%">
<mat-sidenav #sidenav>
<mat-sidenav [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)="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>
<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>
</ng-container>
</mat-nav-list>
</mat-sidenav>
<mat-sidenav-content [style.background]="postsService.theme ? postsService.theme.background_color : null">

@ -86,6 +86,13 @@ export class AppComponent implements OnInit {
if (!localStorage.getItem('theme')) {
this.setTheme(themingExists ? this.defaultTheme : 'default');
}
// gets the subscriptions
if (this.allowSubscriptions) {
this.postsService.getAllSubscriptions().subscribe(res => {
this.postsService.subscriptions = res['subscriptions'];
})
}
}
// theme stuff
@ -162,6 +169,11 @@ onSetTheme(theme, old_theme) {
}
getSubscriptions() {
}
goBack() {
if (!this.navigator) {
this.router.navigate(['/home']);

@ -53,6 +53,7 @@ import { SubscriptionInfoDialogComponent } from './dialogs/subscription-info-dia
import { SettingsComponent } from './settings/settings.component';
import { MatChipsModule } from '@angular/material/chips';
import { NgxFileDropModule } from 'ngx-file-drop';
import { AvatarModule } from 'ngx-avatar';
import es from '@angular/common/locales/es';
import { AboutDialogComponent } from './dialogs/about-dialog/about-dialog.component';
@ -73,7 +74,10 @@ import { CookiesUploaderDialogComponent } from './dialogs/cookies-uploader-dialo
import { LogsViewerComponent } from './components/logs-viewer/logs-viewer.component';
import { ModifyPlaylistComponent } from './dialogs/modify-playlist/modify-playlist.component';
import { ConfirmDialogComponent } from './dialogs/confirm-dialog/confirm-dialog.component';
import { UnifiedFileCardComponent } from './components/unified-file-card/unified-file-card.component';
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';
registerLocaleData(es, 'es');
@ -115,7 +119,10 @@ export function isVisible({ event, element, scrollContainer, offset }: IsVisible
LogsViewerComponent,
ModifyPlaylistComponent,
ConfirmDialogComponent,
EditSubscriptionDialogComponent
UnifiedFileCardComponent,
RecentVideosComponent,
EditSubscriptionDialogComponent,
CustomPlaylistsComponent
],
imports: [
CommonModule,
@ -156,6 +163,7 @@ export function isVisible({ event, element, scrollContainer, offset }: IsVisible
DragDropModule,
ClipboardModule,
NgxFileDropModule,
AvatarModule,
VgCoreModule,
VgControlsModule,
VgOverlayPlayModule,

@ -0,0 +1,13 @@
<div *ngIf="playlists && playlists.length > 0">
<div class="container">
<div class="row justify-content-center">
<div *ngFor="let playlist of playlists; let i = index" [ngClass]="[ postsService.card_size === 'small' ? 'col-2 mb-2 mt-2 file-col' : '', postsService.card_size === 'medium' ? 'col-6 col-lg-4 mb-2 mt-2 file-col' : '' ]">
<app-unified-file-card [index]="i" [card_size]="postsService.card_size" (goToFile)="goToPlaylist($event)" [file_obj]="playlist" [is_playlist]="true" (editPlaylist)="editPlaylistDialog($event)" (deleteFile)="deletePlaylist($event)"></app-unified-file-card>
</div>
</div>
</div>
</div>
<div *ngIf="playlists && playlists.length === 0" style="text-align: center;">
No playlists available. Create one from your downloading files by clicking the blue plus button.
</div>
<div class="add-playlist-button"><button (click)="openCreatePlaylistDialog()" mat-fab><mat-icon>add</mat-icon></button></div>

@ -0,0 +1,10 @@
.add-playlist-button {
float: right;
position: relative;
bottom: 15px;
right: 15px;
}
.file-col {
max-width: 240px;
}

@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { CustomPlaylistsComponent } from './custom-playlists.component';
describe('CustomPlaylistsComponent', () => {
let component: CustomPlaylistsComponent;
let fixture: ComponentFixture<CustomPlaylistsComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ CustomPlaylistsComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(CustomPlaylistsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

@ -0,0 +1,112 @@
import { Component, OnInit } from '@angular/core';
import { PostsService } from 'app/posts.services';
import { Router } from '@angular/router';
import { MatDialog } from '@angular/material/dialog';
import { CreatePlaylistComponent } from 'app/create-playlist/create-playlist.component';
import { ModifyPlaylistComponent } from 'app/dialogs/modify-playlist/modify-playlist.component';
@Component({
selector: 'app-custom-playlists',
templateUrl: './custom-playlists.component.html',
styleUrls: ['./custom-playlists.component.scss']
})
export class CustomPlaylistsComponent implements OnInit {
playlists = null;
playlists_received = false;
downloading_content = {'video': {}, 'audio': {}};
constructor(public postsService: PostsService, private router: Router, private dialog: MatDialog) { }
ngOnInit(): void {
this.postsService.service_initialized.subscribe(init => {
if (init) {
this.getAllPlaylists();
}
});
}
getAllPlaylists() {
this.playlists_received = false;
this.postsService.getAllFiles().subscribe(res => {
this.playlists = res['playlists'];
this.playlists_received = true;
});
}
// creating a playlist
openCreatePlaylistDialog() {
const dialogRef = this.dialog.open(CreatePlaylistComponent, {
data: {
}
});
dialogRef.afterClosed().subscribe(result => {
if (result) {
this.getAllPlaylists();
this.postsService.openSnackBar('Successfully created playlist!', '');
} else if (result === false) {
this.postsService.openSnackBar('ERROR: failed to create playlist!', '');
}
});
}
goToPlaylist(playlist) {
const playlistID = playlist.id;
const type = playlist.type;
if (playlist) {
if (this.postsService.config['Extra']['download_only_mode']) {
this.downloading_content[type][playlistID] = true;
this.downloadPlaylist(playlist.fileNames, type, playlist.name, playlistID);
} else {
localStorage.setItem('player_navigator', this.router.url);
const fileNames = playlist.fileNames;
this.router.navigate(['/player', {fileNames: fileNames.join('|nvr|'), type: type, id: playlistID, uid: playlistID}]);
}
} else {
// playlist not found
console.error(`Playlist with ID ${playlistID} not found!`);
}
}
downloadPlaylist(fileNames, type, zipName = null, playlistID = null) {
this.postsService.downloadFileFromServer(fileNames, type, zipName).subscribe(res => {
if (playlistID) { this.downloading_content[type][playlistID] = false };
const blob: Blob = res;
saveAs(blob, zipName + '.zip');
});
}
deletePlaylist(args) {
const playlist = args.file;
const index = args.index;
const playlistID = playlist.id;
this.postsService.removePlaylist(playlistID, 'audio').subscribe(res => {
if (res['success']) {
this.playlists.splice(index, 1);
this.postsService.openSnackBar('Playlist successfully removed.', '');
}
this.getAllPlaylists();
});
}
editPlaylistDialog(args) {
const playlist = args.playlist;
const index = args.index;
const dialogRef = this.dialog.open(ModifyPlaylistComponent, {
data: {
playlist: playlist,
width: '65vw'
}
});
dialogRef.afterClosed().subscribe(res => {
// updates playlist in file manager if it changed
if (dialogRef.componentInstance.playlist_updated) {
this.playlists[index] = dialogRef.componentInstance.original_playlist;
}
});
}
}

@ -0,0 +1,34 @@
<div class="flex-grid">
<div class="col">
<div style="display: inline-block;">
<mat-form-field style="width: 132px;">
<mat-select [(ngModel)]="this.filterProperty" (selectionChange)="filterOptionChanged($event.value)">
<mat-option *ngFor="let filterOption of filterProperties | keyvalue" [value]="filterOption.value">
{{filterOption['value']['label']}}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<div style="display: inline-block;">
<button (click)="toggleModeChange()" mat-icon-button><mat-icon>{{descendingMode ? 'arrow_downward' : 'arrow_upward'}}</mat-icon></button>
</div>
</div>
<div class="col">
<h4 style="text-align: center">My videos</h4>
</div>
<div class="col" style="top: 25px;">
<mat-form-field [ngClass]="searchIsFocused ? 'search-bar-focused' : 'search-bar-unfocused'" class="search-bar" color="accent">
<input (focus)="searchIsFocused = true" (blur)="searchIsFocused = false" class="search-input" type="text" placeholder="Search" i18n-placeholder="Files search placeholder" [(ngModel)]="search_text" (ngModelChange)="onSearchInputChanged($event)" matInput>
<mat-icon matSuffix>search</mat-icon>
</mat-form-field>
</div>
</div>
<div>
<div class="container">
<div class="row justify-content-center">
<div *ngFor="let file of filtered_files; let i = index" [ngClass]="[ postsService.card_size === 'small' ? 'col-2 mb-2 mt-2 file-col' : '', postsService.card_size === 'medium' ? 'col-6 col-lg-4 mb-2 mt-2 file-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']" (deleteFile)="deleteFile($event)"></app-unified-file-card>
</div>
</div>
</div>
</div>

@ -0,0 +1,34 @@
.file-col {
max-width: 240px;
}
.search-bar {
transition: all .5s ease;
position: relative;
float: right;
}
.search-bar-unfocused {
width: 100px;
}
.search-input {
transition: all .5s ease;
}
.search-bar-focused {
width: 200px;
}
.flex-grid {
width: 100%;
display: block;
position: relative;
padding-left: 12px;
padding-right: 12px;
}
.col {
width: 33%;
display: inline-block;
}

@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { RecentVideosComponent } from './recent-videos.component';
describe('RecentVideosComponent', () => {
let component: RecentVideosComponent;
let fixture: ComponentFixture<RecentVideosComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ RecentVideosComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(RecentVideosComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

@ -0,0 +1,262 @@
import { Component, OnInit } from '@angular/core';
import { PostsService } from 'app/posts.services';
import { Router } from '@angular/router';
@Component({
selector: 'app-recent-videos',
templateUrl: './recent-videos.component.html',
styleUrls: ['./recent-videos.component.scss']
})
export class RecentVideosComponent implements OnInit {
normal_files_received = false;
subscription_files_received = false;
files: any[] = null;
filtered_files: any[] = null;
downloading_content = {'video': {}, 'audio': {}};
search_mode = false;
search_text = '';
searchIsFocused = false;
descendingMode = true;
filterProperties = {
'registered': {
'key': 'registered',
'label': 'Download Date',
'property': 'registered'
},
'upload_date': {
'key': 'upload_date',
'label': 'Upload Date',
'property': 'upload_date'
},
'name': {
'key': 'name',
'label': 'Name',
'property': 'title'
},
'file_size': {
'key': 'file_size',
'label': 'File Size',
'property': 'size'
},
'duration': {
'key': 'duration',
'label': 'Duration',
'property': 'duration'
}
};
filterProperty = this.filterProperties['upload_date'];
constructor(public postsService: PostsService, private router: Router) { }
ngOnInit(): void {
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]) {
this.filterProperty = this.filterProperties[cached_filter_property];
}
}
// search
onSearchInputChanged(newvalue) {
if (newvalue.length > 0) {
this.search_mode = true;
this.filterFiles(newvalue);
} else {
this.search_mode = false;
this.filtered_files = this.files;
}
}
private filterFiles(value: string) {
const filterValue = value.toLowerCase();
this.filtered_files = this.files.filter(option => option.id.toLowerCase().includes(filterValue));
}
filterByProperty(prop) {
if (this.descendingMode) {
this.filtered_files = this.filtered_files.sort((a, b) => (a[prop] > b[prop] ? -1 : 1));
} else {
this.filtered_files = this.filtered_files.sort((a, b) => (a[prop] > b[prop] ? 1 : -1));
}
}
filterOptionChanged(value) {
this.filterByProperty(value['property']);
localStorage.setItem('filter_property', value['key']);
}
toggleModeChange() {
this.descendingMode = !this.descendingMode;
this.filterByProperty(this.filterProperty['property']);
}
// get files
getAllFiles() {
this.normal_files_received = false;
this.postsService.getAllFiles().subscribe(res => {
this.files = res['files'];
this.files.forEach(file => {
file.duration = typeof file.duration !== 'string' ? file.duration : this.durationStringToNumber(file.duration);
});
this.files.sort(this.sortFiles);
if (this.search_mode) {
this.filterFiles(this.search_text);
} else {
this.filtered_files = this.files;
}
this.filterByProperty(this.filterProperty['property']);
});
}
// navigation
goToFile(file) {
if (this.postsService.config['Extra']['download_only_mode']) {
this.downloadFile(file);
} else {
this.navigateToFile(file);
}
}
navigateToFile(file) {
localStorage.setItem('player_navigator', this.router.url);
if (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}]);
} else {
this.router.navigate(['/player', {fileNames: file.id,
type: file.isAudio ? 'audio' : 'video', subscriptionName: sub.name,
subPlaylist: sub.isPlaylist, uuid: this.postsService.user ? this.postsService.user.uid : null}]);
}
} else {
this.router.navigate(['/player', {type: file.isAudio ? 'audio' : 'video', uid: file.uid}]);
}
}
goToSubscription(file) {
this.router.navigate(['/subscription', {id: file.sub_id}]);
}
// downloading
downloadFile(file) {
if (file.sub_id) {
this.downloadSubscriptionFile(file);
} else {
this.downloadNormalFile(file);
}
}
downloadSubscriptionFile(file) {
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;
saveAs(blob, file.id + ext);
}, err => {
console.log(err);
});
}
downloadNormalFile(file) {
const type = file.isAudio ? 'audio' : 'video';
const ext = type === 'audio' ? '.mp3' : '.mp4'
const name = file.id;
this.downloading_content[type][name] = true;
this.postsService.downloadFileFromServer(name, type).subscribe(res => {
this.downloading_content[type][name] = false;
const blob: Blob = res;
saveAs(blob, decodeURIComponent(name) + ext);
if (!this.postsService.config.Extra.file_manager_enabled) {
// tell server to delete the file once downloaded
this.postsService.deleteFile(name, false).subscribe(delRes => {
// reload mp4s
this.getAllFiles();
});
}
});
}
// deleting
deleteFile(args) {
const file = args.file;
const index = args.index;
const blacklistMode = args.blacklistMode;
if (file.sub_id) {
this.deleteSubscriptionFile(file, index, blacklistMode);
} else {
this.deleteNormalFile(file, index, blacklistMode);
}
}
deleteNormalFile(file, index, blacklistMode = false) {
this.postsService.deleteFile(file.uid, file.isAudio, blacklistMode).subscribe(result => {
if (result) {
this.postsService.openSnackBar('Delete success!', 'OK.');
this.files.splice(index, 1);
} else {
this.postsService.openSnackBar('Delete failed!', 'OK.');
}
}, err => {
this.postsService.openSnackBar('Delete failed!', 'OK.');
});
}
deleteSubscriptionFile(file, index, blacklistMode = false) {
if (blacklistMode) {
this.deleteForever(file, index);
} else {
this.deleteAndRedownload(file, index);
}
}
deleteAndRedownload(file, index) {
const sub = this.postsService.getSubscriptionByID(file.sub_id);
this.postsService.deleteSubscriptionFile(sub, file.id, false, file.uid).subscribe(res => {
this.postsService.openSnackBar(`Successfully deleted file: '${file.id}'`);
this.files.splice(index, 1);
});
}
deleteForever(file, index) {
const sub = this.postsService.getSubscriptionByID(file.sub_id);
this.postsService.deleteSubscriptionFile(sub, file.id, true, file.uid).subscribe(res => {
this.postsService.openSnackBar(`Successfully deleted file: '${file.id}'`);
this.files.splice(index, 1);
});
}
// sorting and filtering
sortFiles(a, b) {
// uses the 'registered' flag as the timestamp
const result = b.registered - a.registered;
return result;
}
durationStringToNumber(dur_str) {
let num_sum = 0;
const dur_str_parts = dur_str.split(':');
for (let i = dur_str_parts.length-1; i >= 0; i--) {
num_sum += parseInt(dur_str_parts[i])*(60**(dur_str_parts.length-1-i));
}
return num_sum;
}
}

@ -0,0 +1,39 @@
<div (mouseover)="elevated=true" (mouseout)="elevated=false" style="position: relative; width: fit-content;">
<div class="download-time"><mat-icon class="audio-video-icon">{{(file_obj.type === 'audio' || file_obj.isAudio) ? 'audiotrack' : 'movie'}}</mat-icon>&nbsp;&nbsp;{{file_obj.registered | date:'shortDate'}}</div>
<button [matMenuTriggerFor]="action_menu" class="menuButton" mat-icon-button><mat-icon>more_vert</mat-icon></button>
<mat-menu #action_menu="matMenu">
<ng-container *ngIf="!is_playlist">
<button (click)="openFileInfoDialog()" mat-menu-item><mat-icon>info</mat-icon><ng-container i18n="Video info button">Info</ng-container></button>
<button (click)="navigateToSubscription()" mat-menu-item *ngIf="file_obj.sub_id"><mat-icon>{{file_obj.isAudio ? 'library_music' : 'video_library'}}</mat-icon>&nbsp;<ng-container i18n="Go to subscription menu item">Go to subscription</ng-container></button>
<mat-divider></mat-divider>
<button *ngIf="file_obj.sub_id" (click)="emitDeleteFile()" mat-menu-item>
<mat-icon>restore</mat-icon><ng-container i18n="Delete and redownload subscription video button">Delete and redownload</ng-container>
</button>
<button *ngIf="file_obj.sub_id && use_youtubedl_archive" (click)="emitDeleteFile(true)" mat-menu-item>
<mat-icon>delete_forever</mat-icon><ng-container i18n="Delete forever subscription video button">Delete forever</ng-container>
</button>
<button *ngIf="!file_obj.sub_id" (click)="emitDeleteFile()" mat-menu-item><mat-icon>delete</mat-icon><ng-container i18n="Delete video button">Delete</ng-container></button>
<button *ngIf="!file_obj.sub_id && use_youtubedl_archive" (click)="emitDeleteFile(true)" mat-menu-item><mat-icon>delete_forever</mat-icon><ng-container i18n="Delete and blacklist video button">Delete and blacklist</ng-container></button>
</ng-container>
<ng-container *ngIf="is_playlist">
<button (click)="emitEditPlaylist()" mat-menu-item><mat-icon>edit</mat-icon><ng-container i18n="Playlist edit button">Edit</ng-container></button>
<mat-divider></mat-divider>
<button (click)="emitDeleteFile()" mat-menu-item><mat-icon>delete_forever</mat-icon><ng-container i18n="Delete playlist">Delete</ng-container></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', 'mat-elevation-z4': !elevated, 'mat-elevation-z8': elevated}">
<div style="padding:5px">
<div *ngIf="file_obj.thumbnailURL" class="img-div">
<div style="position: relative">
<img [ngClass]="{'image-small': card_size === 'small', 'image': card_size === 'medium'}" [src]="file_obj.thumbnailURL" alt="Thumbnail">
<div class="duration-time">
{{file_length}}
</div>
</div>
</div>
<span [ngClass]="{'max-two-lines': card_size !== 'small', 'max-one-line': card_size === 'small' }"><strong>{{!is_playlist ? file_obj.title : file_obj.name}}</strong></span>
</div>
</mat-card>
</div>

@ -0,0 +1,117 @@
.file-mat-card {
width: 200px;
height: 200px;
padding: 0px;
cursor: pointer;
}
.small-mat-card {
width: 150px;
height: 150px;
padding: 0px;
cursor: pointer;
}
.menuButton {
right: 0px;
top: -1px;
position: absolute;
z-index: 999;
}
/* Coerce the <span> icon container away from display:inline */
.mat-icon-button .mat-button-wrapper {
display: flex;
justify-content: center;
}
.image {
width: 200px;
height: 112.5px;
object-fit: cover;
}
.image-small {
width: 150px;
height: 84.5px;
object-fit: cover;
}
.example-full-width-height {
width: 100%;
height: 100%
}
.centered {
margin: 0 auto;
top: 50%;
left: 50%;
}
.img-div {
max-height: 80px;
padding: 0px;
margin: 32px 0px 0px -5px;
width: calc(100% + 5px + 5px);
}
.max-two-lines {
display: -webkit-box;
display: -moz-box;
max-height: 2.4em;
line-height: 1.2em;
overflow: hidden;
text-overflow: ellipsis;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
bottom: 5px;
position: absolute;
}
.max-one-line {
display: -webkit-box;
display: -moz-box;
max-height: 1.2em;
line-height: 1.2em;
overflow: hidden;
text-overflow: ellipsis;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
bottom: 5px;
position: absolute;
}
.duration-time {
position: absolute;
bottom: 5px;
right: 5px;
z-index: 99999;
background: rgba(255,255,255,0.6);
padding-left: 5px;
padding-right: 5px;
}
.download-time {
position: absolute;
top: 1px;
left: 5px;
z-index: 99999;
}
.audio-video-icon {
position: relative;
top: 6px;
}
@media (max-width: 576px){
// .example-card {
// width: 175px !important;
// }
// .image {
// width: 175px;
// }
}

@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { UnifiedFileCardComponent } from './unified-file-card.component';
describe('UnifiedFileCardComponent', () => {
let component: UnifiedFileCardComponent;
let fixture: ComponentFixture<UnifiedFileCardComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ UnifiedFileCardComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(UnifiedFileCardComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

@ -0,0 +1,95 @@
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';
@Component({
selector: 'app-unified-file-card',
templateUrl: './unified-file-card.component.html',
styleUrls: ['./unified-file-card.component.scss']
})
export class UnifiedFileCardComponent implements OnInit {
// required info
file_title = '';
file_length = '';
file_thumbnail = '';
type = null;
elevated = false;
@Input() file_obj = null;
@Input() card_size = 'medium';
@Input() use_youtubedl_archive = false;
@Input() is_playlist = false;
@Input() index: number;
@Output() goToFile = new EventEmitter<any>();
@Output() goToSubscription = new EventEmitter<any>();
@Output() deleteFile = new EventEmitter<any>();
@Output() editPlaylist = new EventEmitter<any>();
/*
Planned sizes:
small: 150x175
medium: 200x200
big: 250x200
*/
constructor(private dialog: MatDialog) { }
ngOnInit(): void {
this.file_length = fancyTimeFormat(this.file_obj.duration);
}
emitDeleteFile(blacklistMode = false) {
this.deleteFile.emit({
file: this.file_obj,
index: this.index,
blacklistMode: blacklistMode
});
}
navigateToFile() {
this.goToFile.emit(this.file_obj);
}
navigateToSubscription() {
this.goToSubscription.emit(this.file_obj);
}
openFileInfoDialog() {
this.dialog.open(VideoInfoDialogComponent, {
data: {
file: this.file_obj,
},
minWidth: '50vw'
})
}
emitEditPlaylist() {
this.editPlaylist.emit({
playlist: this.file_obj,
index: this.index
});
}
}
function fancyTimeFormat(time) {
if (typeof time === 'string') {
return time;
}
// Hours, minutes and seconds
const hrs = ~~(time / 3600);
const mins = ~~((time % 3600) / 60);
const secs = ~~time % 60;
// Output like "1:01" or "4:03:59" or "123:03:59"
let ret = '';
if (hrs > 0) {
ret += '' + hrs + ':' + (mins < 10 ? '0' : '');
}
ret += '' + mins + ':' + (secs < 10 ? '0' : '');
ret += '' + secs;
return ret;
}

@ -1,18 +1,34 @@
<h4 mat-dialog-title i18n="Create a playlist dialog title">Create a playlist</h4>
<form>
<div>
<mat-form-field color="accent">
<input [(ngModel)]="name" matInput placeholder="Name" i18n-placeholder="Playlist name placeholder" type="text" required aria-required [ngModelOptions]="{standalone: true}">
</mat-form-field>
</div>
<div>
<mat-form-field color="accent">
<mat-label *ngIf="type === 'audio'"><ng-container i18n="Audio files title">Audio files</ng-container></mat-label>
<mat-label *ngIf="type === 'video'"><ng-container i18n="Videos title">Videos</ng-container></mat-label>
<mat-select [formControl]="filesSelect" multiple required aria-required>
<mat-option *ngFor="let file of filesToSelectFrom" [value]="file.id">{{file.id}}</mat-option>
</mat-select>
</mat-form-field>
<div *ngIf="filesToSelectFrom || (audiosToSelectFrom && videosToSelectFrom)">
<div>
<mat-form-field color="accent">
<input [(ngModel)]="name" matInput placeholder="Name" i18n-placeholder="Playlist name placeholder" type="text" required aria-required [ngModelOptions]="{standalone: true}">
</mat-form-field>
</div>
<div *ngIf="!filesToSelectFrom">
<mat-form-field color="accent">
<mat-select placeholder="Type" i18n-placeholder="Type select" [(ngModel)]="type" [ngModelOptions]="{standalone: true}">
<mat-option value="audio"><ng-container i18n="Audio">Audio</ng-container></mat-option>
<mat-option value="video"><ng-container i18n="Video">Video</ng-container></mat-option>
</mat-select>
</mat-form-field>
</div>
<div>
<mat-form-field *ngIf="type && ((filesToSelectFrom && filesToSelectFrom.length > 0) || (type === 'audio' && audiosToSelectFrom && audiosToSelectFrom.length > 0) || (type === 'video' && videosToSelectFrom && videosToSelectFrom.length > 0))" color="accent">
<mat-label *ngIf="type === 'audio'"><ng-container i18n="Audio files title">Audio files</ng-container></mat-label>
<mat-label *ngIf="type === 'video'"><ng-container i18n="Videos title">Videos</ng-container></mat-label>
<mat-select [formControl]="filesSelect" multiple required aria-required>
<ng-container *ngIf="filesToSelectFrom"><mat-option *ngFor="let file of filesToSelectFrom" [value]="file.id">{{file.id}}</mat-option></ng-container>
<ng-container *ngIf="audiosToSelectFrom && type === 'audio'"><mat-option *ngFor="let file of audiosToSelectFrom" [value]="file.id">{{file.id}}</mat-option></ng-container>
<ng-container *ngIf="videosToSelectFrom && type === 'video'"><mat-option *ngFor="let file of videosToSelectFrom" [value]="file.id">{{file.id}}</mat-option></ng-container>
</mat-select>
</mat-form-field>
<!-- No videos available -->
<div style="margin-bottom: 15px;" *ngIf="type && ((filesToSelectFrom && filesToSelectFrom.length === 0) || (type === 'audio' && audiosToSelectFrom && audiosToSelectFrom.length === 0) || (type === 'video' && videosToSelectFrom && videosToSelectFrom.length === 0))">
No files available.
</div>
</div>
</div>
</form>

@ -14,6 +14,8 @@ export class CreatePlaylistComponent implements OnInit {
filesToSelectFrom = null;
type = null;
filesSelect = new FormControl();
audiosToSelectFrom = null;
videosToSelectFrom = null;
name = '';
create_in_progress = false;
@ -28,12 +30,30 @@ export class CreatePlaylistComponent implements OnInit {
this.filesToSelectFrom = this.data.filesToSelectFrom;
this.type = this.data.type;
}
if (!this.filesToSelectFrom) {
this.getMp3s();
this.getMp4s();
}
}
getMp3s() {
this.postsService.getMp3s().subscribe(result => {
this.audiosToSelectFrom = result['mp3s'];
});
}
getMp4s() {
this.postsService.getMp4s().subscribe(result => {
this.videosToSelectFrom = result['mp4s'];
});
}
createPlaylist() {
const thumbnailURL = this.getThumbnailURL();
const duration = this.calculateDuration();
this.create_in_progress = true;
this.postsService.createPlaylist(this.name, this.filesSelect.value, this.type, thumbnailURL).subscribe(res => {
this.postsService.createPlaylist(this.name, this.filesSelect.value, this.type, thumbnailURL, duration).subscribe(res => {
this.create_in_progress = false;
if (res['success']) {
this.dialogRef.close(true);
@ -44,8 +64,12 @@ export class CreatePlaylistComponent implements OnInit {
}
getThumbnailURL() {
for (let i = 0; i < this.filesToSelectFrom.length; i++) {
const file = this.filesToSelectFrom[i];
let properFilesToSelectFrom = this.filesToSelectFrom;
if (!this.filesToSelectFrom) {
properFilesToSelectFrom = this.type === 'audio' ? this.audiosToSelectFrom : this.videosToSelectFrom;
}
for (let i = 0; i < properFilesToSelectFrom.length; i++) {
const file = properFilesToSelectFrom[i];
if (file.id === this.filesSelect.value[0]) {
// different services store the thumbnail in different places
if (file.thumbnailURL) { return file.thumbnailURL };
@ -55,4 +79,35 @@ export class CreatePlaylistComponent implements OnInit {
return null;
}
getDuration(file_id) {
let properFilesToSelectFrom = this.filesToSelectFrom;
if (!this.filesToSelectFrom) {
properFilesToSelectFrom = this.type === 'audio' ? this.audiosToSelectFrom : this.videosToSelectFrom;
}
for (let i = 0; i < properFilesToSelectFrom.length; i++) {
const file = properFilesToSelectFrom[i];
if (file.id === file_id) {
return file.duration;
}
}
return null;
}
calculateDuration() {
let sum = 0;
for (let i = 0; i < this.filesSelect.value.length; i++) {
const duration_val = this.getDuration(this.filesSelect.value[i]);
sum += typeof duration_val === 'string' ? this.durationStringToNumber(duration_val) : duration_val;
}
return sum;
}
durationStringToNumber(dur_str) {
let num_sum = 0;
const dur_str_parts = dur_str.split(':');
for (let i = dur_str_parts.length-1; i >= 0; i--) {
num_sum += parseInt(dur_str_parts[i])*(60**(dur_str_parts.length-1-i));
}
return num_sum;
}
}

@ -24,6 +24,34 @@
<p>
<ng-container i18n="About bug prefix">Found a bug or have a suggestion?</ng-container>&nbsp;<a [href]="issuesLink" target="_blank"><ng-container i18n="About bug click here">Click here</ng-container></a>&nbsp;<ng-container i18n="About bug suffix">to create an issue!</ng-container>
</p>
<mat-divider></mat-divider>
<div style="margin-top: 10px;">
<h5>Personal settings:</h5>
<mat-form-field placeholder="Sidepanel mode">
<mat-select [(ngModel)]="sidepanel_mode" (selectionChange)="sidePanelModeChanged($event.value)">
<mat-option value="over">
Over
</mat-option>
<mat-option value="side">
Side
</mat-option>
</mat-select>
</mat-form-field>
<br/>
<mat-form-field placeholder="Card size">
<mat-select [(ngModel)]="card_size" (selectionChange)="cardSizeOptionChanged($event.value)">
<mat-option value="large" [disabled]="true">
Large
</mat-option>
<mat-option value="medium">
Medium
</mat-option>
<mat-option value="small">
Small
</mat-option>
</mat-select>
</mat-form-field>
</div>
</div>
</mat-dialog-content>

@ -16,11 +16,13 @@ export class AboutDialogComponent implements OnInit {
checking_for_updates = true;
current_version_tag = CURRENT_VERSION;
sidepanel_mode = this.postsService.sidepanel_mode;
card_size = this.postsService.card_size;
constructor(private postsService: PostsService) { }
ngOnInit(): void {
this.getLatestGithubRelease()
this.getLatestGithubRelease();
}
getLatestGithubRelease() {
@ -30,4 +32,14 @@ export class AboutDialogComponent implements OnInit {
});
}
sidePanelModeChanged(new_mode) {
localStorage.setItem('sidepanel_mode', new_mode);
this.postsService.sidepanel_mode = new_mode;
}
cardSizeOptionChanged(new_size) {
localStorage.setItem('card_size', new_size);
this.postsService.card_size = new_size;
}
}

@ -1,10 +1,7 @@
<br/>
<div class="big demo-basic">
<mat-card id="card" style="margin-right: 20px; margin-left: 20px;" [ngClass]="(allowAdvancedDownload) ? 'no-border-radius-bottom' : null">
<mat-card-title>
<ng-container i18n="Youtube downloader home page label">Youtube Downloader</ng-container>
</mat-card-title>
<mat-card-content>
<mat-card-content style="padding: 0px 8px 0px 8px;">
<div style="position: relative;">
<form class="example-form">
<div class="container-fluid">
@ -183,7 +180,13 @@
<ng-template #nofile>
</ng-template>
<div style="margin: 20px" *ngIf="fileManagerEnabled && (!postsService.isLoggedIn || postsService.permissions.includes('filemanager'))">
<app-recent-videos></app-recent-videos>
<br/>
<h4 style="text-align: center">Custom playlists</h4>
<app-custom-playlists></app-custom-playlists>
<!--<div style="margin: 20px" *ngIf="fileManagerEnabled && (!postsService.isLoggedIn || postsService.permissions.includes('filemanager'))">
<mat-accordion>
<mat-expansion-panel (opened)="accordionOpened('audio')" (closed)="accordionClosed('audio')" (mouseleave)="accordionLeft('audio')" (mouseenter)="accordionEntered('audio')" class="big">
<mat-expansion-panel-header>
@ -260,7 +263,7 @@
</mat-grid-tile>
</mat-grid-list>
<!-- Add video playlist button -->
<!-- Add video playlist button --<
<div class="add-playlist-button"><button (click)="openCreatePlaylistDialog('video')" mat-fab><mat-icon>add</mat-icon></button></div>
<div *ngIf="playlists.video.length === 0">
<ng-container i18n="No video playlists available text">
@ -270,12 +273,4 @@
</div>
</mat-expansion-panel>
</mat-accordion>
</div>
<ng-template #nomp3s>
</ng-template>
<ng-template #nomp4s>
</ng-template>
</div>-->

@ -65,6 +65,13 @@
padding-right: 0px;
padding-left: 0.01px;
height: 100%;
width: 100%;
}
.audio-col {
margin: 0 auto;
margin-top: 10px;
width: 95%;
}
.save-icon {

@ -1,13 +1,13 @@
<div *ngIf="playlist.length > 0 && show_player">
<div [ngClass]="(type === 'audio') ? null : 'container-video'" class="container">
<div style="max-width: 100%; margin-left: 0px; height: 70vh" class="row">
<div [ngClass]="(type === 'audio') ? 'my-2 px-1' : 'video-col'" class="col">
<vg-player (onPlayerReady)="onPlayerReady($event)" [style.background-color]="(type === 'audio') ? 'transparent' : 'black'">
<div [ngClass]="(type === 'audio') ? null : 'container-video'">
<div style="max-width: 100%; margin-left: 0px; height: 70vh">
<div style="height: fit-content" [ngClass]="(type === 'audio') ? 'audio-col' : 'video-col'">
<vg-player style="height: fit-content" (onPlayerReady)="onPlayerReady($event)" [style.background-color]="(type === 'audio') ? 'transparent' : 'black'">
<video [ngClass]="(type === 'audio') ? 'audio-styles' : 'video-styles'" #media class="video-player" [vgMedia]="media" [src]="currentItem.src" id="singleVideo" preload="auto" controls>
</video>
</vg-player>
</div>
<div class="col-12 my-2">
<div style="height: fit-content; width: 100%; margin-top: 10px;">
<mat-button-toggle-group cdkDropList [cdkDropListSortingDisabled]="!id" (cdkDropListDropped)="drop($event)" style="width: 80%; left: 9%" vertical name="videoSelect" aria-label="Video Select" #group="matButtonToggleGroup">
<mat-button-toggle cdkDrag *ngFor="let playlist_item of playlist; let i = index" [checked]="currentItem.title === playlist_item.title" (click)="onClickPlaylistItem(playlist_item, i)" class="toggle-button" [value]="playlist_item.title">{{playlist_item.label}}</mat-button-toggle>
</mat-button-toggle-group>

@ -22,6 +22,8 @@ export class PostsService implements CanActivate {
handShakeComplete = false;
THEMES_CONFIG = THEMES_CONFIG;
theme;
card_size = 'medium';
sidepanel_mode = 'over';
settings_changed = new BehaviorSubject<boolean>(false);
auth_token = '4241b401-7236-493e-92b5-b72696b9d853';
session_id = null;
@ -47,6 +49,7 @@ export class PostsService implements CanActivate {
open_create_default_admin_dialog = new BehaviorSubject<boolean>(false);
config = null;
subscriptions = null;
constructor(private http: HttpClient, private router: Router, @Inject(DOCUMENT) private document: Document,
public snackBar: MatSnackBar) {
console.log('PostsService Initialized...');
@ -101,6 +104,14 @@ export class PostsService implements CanActivate {
this.reload_config.subscribe(yes_reload => {
if (yes_reload) { this.reloadConfig(); }
});
if (localStorage.getItem('sidepanel_mode')) {
this.sidepanel_mode = localStorage.getItem('sidepanel_mode');
}
if (localStorage.getItem('card_size')) {
this.card_size = localStorage.getItem('card_size');
}
}
canActivate(route, state): Promise<boolean> {
return new Promise(resolve => {
@ -114,6 +125,15 @@ export class PostsService implements CanActivate {
this.theme = this.THEMES_CONFIG[theme];
}
getSubscriptionByID(sub_id) {
for (let i = 0; i < this.subscriptions.length; i++) {
if (this.subscriptions[i]['id'] === sub_id) {
return this.subscriptions[i];
}
}
return null;
}
startHandshake(url: string) {
return this.http.get(url + 'geturl');
}
@ -204,6 +224,10 @@ export class PostsService implements CanActivate {
return this.http.post(this.path + 'getFile', {uid: uid, type: type, uuid: uuid}, this.httpOptions);
}
getAllFiles() {
return this.http.post(this.path + 'getAllFiles', {}, this.httpOptions);
}
downloadFileFromServer(fileName, type, outputName = null, fullPathProvided = null, subscriptionName = null, subPlaylist = null,
uid = null, uuid = null) {
return this.http.post(this.path + 'downloadFile', {fileNames: fileName,
@ -251,11 +275,12 @@ export class PostsService implements CanActivate {
return this.http.post(this.path + 'disableSharing', {uid: uid, type: type, is_playlist: is_playlist}, this.httpOptions);
}
createPlaylist(playlistName, fileNames, type, thumbnailURL) {
createPlaylist(playlistName, fileNames, type, thumbnailURL, duration = null) {
return this.http.post(this.path + 'createPlaylist', {playlistName: playlistName,
fileNames: fileNames,
type: type,
thumbnailURL: thumbnailURL}, this.httpOptions);
thumbnailURL: thumbnailURL,
duration: duration}, this.httpOptions);
}
getPlaylist(playlistID, type, uuid = null) {

@ -79,11 +79,6 @@
<mat-hint><ng-container i18n="Check interval setting input hint">Unit is seconds, only include numbers.</ng-container></mat-hint>
</mat-form-field>
</div>
<div class="col-12 mt-4">
<mat-checkbox color="accent" [disabled]="!new_config['Subscriptions']['allow_subscriptions']" [(ngModel)]="new_config['Subscriptions']['subscriptions_use_youtubedl_archive']"><ng-container i18n="Use youtube-dl archive setting">Use youtube-dl archive</ng-container></mat-checkbox>
<p><a target="_blank" href="https://github.com/ytdl-org/youtube-dl/blob/master/README.md#how-do-i-download-only-new-videos-from-a-playlist"><ng-container i18n="youtube-dl archive explanation prefix link">With youtube-dl's archive</ng-container></a>&nbsp;<ng-container i18n="youtube-dl archive explanation middle">feature, downloaded videos from your subscriptions get recorded in a text file in the subscriptions archive sub-directory.</ng-container></p>
<p><ng-container i18n="youtube-dl archive explanation suffix">This enables the ability to permanently delete videos from your subscriptions without unsubscribing, and allows you to record which videos you downloaded in case of data loss.</ng-container></p>
</div>
</div>
</div>
<mat-divider></mat-divider>
@ -150,7 +145,6 @@
<div class="col-12 mt-5">
<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>
<p><ng-container i18n="youtubedl archive setting Note">Note: This setting only applies to downloads on the Home page. If you would like to use youtube-dl archive functionality in subscriptions, head to the Main tab and activate this option there.</ng-container></p>
</div>
<div class="col-12 mt-2">

@ -1,6 +1,6 @@
import { Component, OnInit } from '@angular/core';
import { PostsService } from 'app/posts.services';
import { ActivatedRoute, Router } from '@angular/router';
import { ActivatedRoute, Router, ParamMap } from '@angular/router';
import { MatDialog } from '@angular/material/dialog';
import { EditSubscriptionDialogComponent } from 'app/dialogs/edit-subscription-dialog/edit-subscription-dialog.component';
@ -45,9 +45,21 @@ export class SubscriptionComponent implements OnInit {
filterProperty = this.filterProperties['upload_date'];
downloading = false;
initialized = false;
constructor(private postsService: PostsService, private route: ActivatedRoute, private router: Router, private dialog: MatDialog) { }
ngOnInit() {
this.route.paramMap.subscribe((params: ParamMap) => {
this.id = params.get('id');
this.postsService.service_initialized.subscribe(init => {
if (init) {
this.initialized = true;
this.getConfig();
this.getSubscription();
}
});
});
if (this.route.snapshot.paramMap.get('id')) {
this.id = this.route.snapshot.paramMap.get('id');
@ -84,7 +96,7 @@ export class SubscriptionComponent implements OnInit {
}
getConfig() {
this.use_youtubedl_archive = this.postsService.config['Subscriptions']['subscriptions_use_youtubedl_archive'];
this.use_youtubedl_archive = this.postsService.config['Downloader']['use_youtubedl_archive'];
}
goToFile(emit_obj) {
@ -123,7 +135,6 @@ export class SubscriptionComponent implements OnInit {
}
filterOptionChanged(value) {
// this.filterProperty = value;
this.filterByProperty(value['property']);
localStorage.setItem('filter_property', value['key']);
}

@ -12,7 +12,7 @@
"Downloader": {
"path-audio": "audio/",
"path-video": "video/",
"use_youtubedl_archive": false,
"use_youtubedl_archive": true,
"custom_args": "",
"safe_download_override": false
},

Loading…
Cancel
Save