Merge branch 'master' of https://github.com/Tzahi12345/YoutubeDL-Material into dockertest

This commit is contained in:
Isaac Abadi
2020-08-08 16:08:54 -04:00
36 changed files with 480 additions and 103 deletions

View File

@@ -69,6 +69,8 @@ Alternatively, you can port forward the port specified in the config (defaults t
## Docker
### Setup
If you are looking to setup YoutubeDL-Material with Docker, this section is for you. And you're in luck! Docker setup is quite simple.
1. Run `curl -L https://github.com/Tzahi12345/YoutubeDL-Material/releases/latest/download/docker-compose.yml -o docker-compose.yml` to download the latest Docker Compose, or go to the [releases](https://github.com/Tzahi12345/YoutubeDL-Material/releases/) page to grab the version you'd like.
@@ -76,6 +78,16 @@ If you are looking to setup YoutubeDL-Material with Docker, this section is for
3. Run `docker-compose up` to start it up. If successful, it should say "HTTP(S): Started on port 8998" or something similar.
4. Make sure you can connect to the specified URL + port, and if so, you are done!
### Custom UID/GID
By default, the Docker container runs as non-root with UID=1000 and GID=1000. To set this to your own UID/GID, simply update the `environment` section in your `docker-compose.yml` like so:
```
environment:
UID: YOUR_UID
GID: YOUR_GID
```
## API
[API Docs](https://stoplight.io/p/docs/gh/tzahi12345/youtubedl-material?group=master&utm_campaign=publish_dialog&utm_source=studio)

View File

@@ -1,5 +1,7 @@
FROM arm32v7/alpine:3.12
COPY qemu-arm-static /usr/bin
ENV UID=1000 \
GID=1000 \
USER=youtube

View File

@@ -30,6 +30,7 @@ var subscriptions_api = require('./subscriptions')
const CONSTS = require('./consts')
const { spawn } = require('child_process')
const read_last_lines = require('read-last-lines');
var ps = require('ps-node');
const is_windows = process.platform === 'win32';
@@ -503,6 +504,43 @@ async function getLatestVersion() {
});
}
async function killAllDownloads() {
return new Promise(resolve => {
ps.lookup({
command: 'youtube-dl',
}, function(err, resultList ) {
if (err) {
// failed to get list of processes
logger.error('Failed to get a list of running youtube-dl processes.');
logger.error(err);
resolve({
details: err,
success: false
});
}
// processes that contain the string 'youtube-dl' in the name will be looped
resultList.forEach(function( process ){
if (process) {
ps.kill(process.pid, 'SIGKILL', function( err ) {
if (err) {
// failed to kill, process may have ended on its own
logger.warn(`Failed to kill process with PID ${process.pid}`);
logger.warn(err);
}
else {
logger.verbose(`Process ${process.pid} has been killed!`);
}
});
}
});
resolve({
success: true
});
});
});
}
async function setPortItemFromENV() {
return new Promise(resolve => {
config_api.setConfigItem('ytdl_port', backendPort.toString());
@@ -880,14 +918,16 @@ async function createPlaylistZipFile(fileNames, type, outputName, fullPathProvid
}
async function deleteAudioFile(name, blacklistMode = false) {
async function deleteAudioFile(name, customPath = null, blacklistMode = false) {
return new Promise(resolve => {
// TODO: split descriptors into audio and video descriptors, as deleting an audio file will close all video file streams
var jsonPath = path.join(audioFolderPath,name+'.mp3.info.json');
var altJSONPath = path.join(audioFolderPath,name+'.info.json');
var audioFilePath = path.join(audioFolderPath,name+'.mp3');
let filePath = customPath ? customPath : audioFolderPath;
var jsonPath = path.join(filePath,name+'.mp3.info.json');
var altJSONPath = path.join(filePath,name+'.info.json');
var audioFilePath = path.join(filePath,name+'.mp3');
var thumbnailPath = path.join(filePath,name+'.webp');
var altThumbnailPath = path.join(filePath,name+'.jpg');
jsonPath = path.join(__dirname, jsonPath);
altJSONPath = path.join(__dirname, altJSONPath);
audioFilePath = path.join(__dirname, audioFilePath);
@@ -927,7 +967,7 @@ async function deleteAudioFile(name, blacklistMode = false) {
// get ID from JSON
var jsonobj = utils.getJSONMp3(name, audioFolderPath);
var jsonobj = utils.getJSONMp3(name, filePath);
let id = null;
if (jsonobj) id = jsonobj.id;
@@ -963,10 +1003,12 @@ async function deleteVideoFile(name, customPath = null, blacklistMode = false) {
return new Promise(resolve => {
let filePath = customPath ? customPath : videoFolderPath;
var jsonPath = path.join(filePath,name+'.info.json');
var altJSONPath = path.join(filePath,name+'.mp4.info.json');
var videoFilePath = path.join(filePath,name+'.mp4');
var thumbnailPath = path.join(filePath,name+'.webp');
var altThumbnailPath = path.join(filePath,name+'.jpg');
jsonPath = path.join(__dirname, jsonPath);
videoFilePath = path.join(__dirname, videoFilePath);
@@ -1004,7 +1046,7 @@ async function deleteVideoFile(name, customPath = null, blacklistMode = false) {
// get ID from JSON
var jsonobj = utils.getJSONMp4(name, videoFolderPath);
var jsonobj = utils.getJSONMp4(name, filePath);
let id = null;
if (jsonobj) id = jsonobj.id;
@@ -1193,6 +1235,7 @@ async function downloadFileByURL_exec(url, type, options, sessionID = null) {
options.customFileFolderPath = fileFolderPath;
}
options.downloading_method = 'exec';
const downloadConfig = await generateArgs(url, type, options);
// adds download to download helper
@@ -1328,6 +1371,7 @@ async function downloadFileByURL_normal(url, type, options, sessionID = null) {
options.customFileFolderPath = fileFolderPath;
}
options.downloading_method = 'normal';
const downloadConfig = await generateArgs(url, type, options);
// adds download to download helper
@@ -1468,24 +1512,24 @@ 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', 'best[ext=mp4]'];
const is_youtube = url.includes('youtu');
if (!is_audio && !is_youtube) {
// tiktok videos fail when using the default format
qualityPath = null;
} else if (!is_audio && !is_youtube && (url.includes('reddit') || url.includes('pornhub'))) {
qualityPath = '-f bestvideo+bestaudio'
qualityPath = ['-f', 'bestvideo+bestaudio']
}
if (customArgs) {
downloadConfig = customArgs.split(',,');
} else {
if (customQualityConfiguration) {
qualityPath = `-f ${customQualityConfiguration}`;
qualityPath = ['-f', customQualityConfiguration];
} else if (selectedHeight && selectedHeight !== '' && !is_audio) {
qualityPath = `-f '(mp4)[height=${selectedHeight}]'`;
qualityPath = ['-f', `'(mp4)[height=${selectedHeight}'`];
} else if (maxBitrate && is_audio) {
qualityPath = `--audio-quality ${maxBitrate}`
qualityPath = ['--audio-quality', maxBitrate]
}
if (customOutput) {
@@ -1494,7 +1538,7 @@ async function generateArgs(url, type, options) {
downloadConfig = ['-o', path.join(fileFolderPath, videopath + (is_audio ? '.%(ext)s' : '.mp4')), '--write-info-json', '--print-json'];
}
if (qualityPath) downloadConfig.push(qualityPath);
if (qualityPath && options.downloading_method === 'exec') downloadConfig.push(...qualityPath);
if (is_audio && !options.skip_audio_args) {
downloadConfig.push('-x');
@@ -1848,7 +1892,7 @@ app.get('/api/config', function(req, res) {
});
});
app.post('/api/setConfig', function(req, res) {
app.post('/api/setConfig', optionalJwt, function(req, res) {
let new_config_file = req.body.new_config_file;
if (new_config_file && new_config_file['YoutubeDLMaterial']) {
let success = config_api.setConfigFile(new_config_file);
@@ -1894,8 +1938,6 @@ app.post('/api/tomp3', optionalJwt, async function(req, res) {
} else {
res.sendStatus(500);
}
res.end("yes");
});
app.post('/api/tomp4', optionalJwt, async function(req, res) {
@@ -1925,50 +1967,11 @@ app.post('/api/tomp4', optionalJwt, async function(req, res) {
} else {
res.sendStatus(500);
}
res.end("yes");
});
// gets the status of the mp3 file that's being downloaded
app.post('/api/fileStatusMp3', function(req, res) {
var name = decodeURIComponent(req.body.name + "");
var exists = "";
var fullpath = audioFolderPath + name + ".mp3";
if (fs.existsSync(fullpath)) {
exists = [basePath + audioFolderPath + name, getFileSizeMp3(name)];
}
else
{
var percent = 0;
var size = getFileSizeMp3(name);
var downloaded = getAmountDownloadedMp3(name);
if (size > 0)
percent = downloaded/size;
exists = ["failed", getFileSizeMp3(name), percent];
}
//logger.info(exists + " " + name);
res.send(exists);
res.end("yes");
});
// gets the status of the mp4 file that's being downloaded
app.post('/api/fileStatusMp4', function(req, res) {
var name = decodeURIComponent(req.body.name);
var exists = "";
var fullpath = videoFolderPath + name + ".mp4";
if (fs.existsSync(fullpath)) {
exists = [basePath + videoFolderPath + name, getFileSizeMp4(name)];
} else {
var percent = 0;
var size = getFileSizeMp4(name);
var downloaded = getAmountDownloadedMp4(name);
if (size > 0)
percent = downloaded/size;
exists = ["failed", getFileSizeMp4(name), percent];
}
//logger.info(exists + " " + name);
res.send(exists);
res.end("yes");
app.post('/api/killAllDownloads', optionalJwt, async function(req, res) {
const result_obj = await killAllDownloads();
res.send(result_obj);
});
// gets all download mp3s
@@ -2307,6 +2310,16 @@ app.post('/api/downloadVideosForSubscription', optionalJwt, async (req, res) =>
});
});
app.post('/api/updateSubscription', optionalJwt, async (req, res) => {
let updated_sub = req.body.subscription;
let user_uid = req.isAuthenticated() ? req.user.uid : null;
let success = subscriptions_api.updateSubscription(updated_sub, user_uid);
res.send({
success: success
});
});
app.post('/api/getAllSubscriptions', optionalJwt, async (req, res) => {
let user_uid = req.isAuthenticated() ? req.user.uid : null;
@@ -2455,11 +2468,10 @@ app.post('/api/deleteMp3', optionalJwt, async (req, res) => {
var wasDeleted = false;
if (fs.existsSync(fullpath))
{
deleteAudioFile(name, blacklistMode);
deleteAudioFile(name, null, blacklistMode);
db.get('files.audio').remove({uid: uid}).write();
wasDeleted = true;
res.send(wasDeleted);
res.end("yes");
} else if (audio_obj) {
db.get('files.audio').remove({uid: uid}).write();
wasDeleted = true;
@@ -2491,7 +2503,6 @@ app.post('/api/deleteMp4', optionalJwt, async (req, res) => {
db.get('files.video').remove({uid: uid}).write();
// wasDeleted = true;
res.send(wasDeleted);
res.end("yes");
} else if (video_obj) {
db.get('files.video').remove({uid: uid}).write();
wasDeleted = true;
@@ -2499,7 +2510,6 @@ app.post('/api/deleteMp4', optionalJwt, async (req, res) => {
} else {
wasDeleted = false;
res.send(wasDeleted);
res.end("yes");
}
});

View File

@@ -10,7 +10,7 @@ fi
# chown current working directory to current user
if [ "$*" = "$CMD" ] && [ "$(id -u)" = "0" ]; then
find . \! -user "$UID" -exec chown "$UID:$GID" -R '{}' +
find . \! -user "$UID" -exec chown "$UID:$GID" -R '{}' + || echo "WARNING! Could not change directory ownership. If you manage permissions externally this is fine, otherwise you may experience issues when downloading or deleting videos."
exec su-exec "$UID:$GID" "$0" "$@"
fi

View File

@@ -575,6 +575,11 @@
"xdg-basedir": "^3.0.0"
}
},
"connected-domain": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/connected-domain/-/connected-domain-1.0.0.tgz",
"integrity": "sha1-v+dyOMdL5FOnnwy2BY3utPI1jpM="
},
"content-disposition": {
"version": "0.5.3",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz",
@@ -1990,6 +1995,14 @@
"ipaddr.js": "1.9.1"
}
},
"ps-node": {
"version": "0.1.6",
"resolved": "https://registry.npmjs.org/ps-node/-/ps-node-0.1.6.tgz",
"integrity": "sha1-mvZ6mdex0BMuUaUDCZ04qNKs4sM=",
"requires": {
"table-parser": "^0.1.3"
}
},
"pseudomap": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz",
@@ -2348,6 +2361,14 @@
"has-flag": "^3.0.0"
}
},
"table-parser": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/table-parser/-/table-parser-0.1.3.tgz",
"integrity": "sha1-BEHPzhallIFoTCfRtaZ/8VpDx7A=",
"requires": {
"connected-domain": "^1.0.0"
}
},
"tar-stream": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.1.2.tgz",

View File

@@ -50,6 +50,7 @@
"passport-jwt": "^4.0.0",
"passport-local": "^1.0.0",
"progress": "^2.0.3",
"ps-node": "^0.1.6",
"read-last-lines": "^1.7.2",
"shortid": "^2.2.15",
"unzipper": "^0.10.10",

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

View File

@@ -14,5 +14,5 @@
<link rel="stylesheet" href="styles.5112d6db78cf21541598.css"></head>
<body>
<app-root></app-root>
<script src="runtime-es2015.0785764e1e4f6ab5497e.js" type="module"></script><script src="runtime-es5.0785764e1e4f6ab5497e.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.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>
</html>

View File

@@ -1 +0,0 @@
!function(e){function r(r){for(var n,i,a=r[0],c=r[1],l=r[2],p=0,s=[];p<a.length;p++)i=a[p],Object.prototype.hasOwnProperty.call(o,i)&&o[i]&&s.push(o[i][0]),o[i]=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,a=1;a<t.length;a++)0!==o[t[a]]&&(n=!1);n&&(u.splice(r--,1),e=i(i.s=t[0]))}return e}var n={},o={0:0},u=[];function i(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,i),t.l=!0,t.exports}i.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,a=document.createElement("script");a.charset="utf-8",a.timeout=120,i.nc&&a.setAttribute("nonce",i.nc),a.src=function(e){return i.p+""+({}[e]||e)+"-es2015."+{1:"74313ded392a393618d4"}[e]+".js"}(e);var c=new Error;u=function(r){a.onerror=a.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:a})}),12e4);a.onerror=a.onload=u,document.head.appendChild(a)}return Promise.all(r)},i.m=e,i.c=n,i.d=function(e,r,t){i.o(e,r)||Object.defineProperty(e,r,{enumerable:!0,get:t})},i.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},i.t=function(e,r){if(1&r&&(e=i(e)),8&r)return e;if(4&r&&"object"==typeof e&&e&&e.__esModule)return e;var t=Object.create(null);if(i.r(t),Object.defineProperty(t,"default",{enumerable:!0,value:e}),2&r&&"string"!=typeof e)for(var n in e)i.d(t,n,(function(r){return e[r]}).bind(null,n));return t},i.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return i.d(r,"a",r),r},i.o=function(e,r){return Object.prototype.hasOwnProperty.call(e,r)},i.p="",i.oe=function(e){throw console.error(e),e};var a=window.webpackJsonp=window.webpackJsonp||[],c=a.push.bind(a);a.push=r,a=a.slice();for(var l=0;l<a.length;l++)r(a[l]);var f=c;t()}([]);

View File

@@ -0,0 +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()}([]);

View File

@@ -1 +0,0 @@
!function(e){function r(r){for(var n,i,a=r[0],c=r[1],l=r[2],p=0,s=[];p<a.length;p++)i=a[p],Object.prototype.hasOwnProperty.call(o,i)&&o[i]&&s.push(o[i][0]),o[i]=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,a=1;a<t.length;a++)0!==o[t[a]]&&(n=!1);n&&(u.splice(r--,1),e=i(i.s=t[0]))}return e}var n={},o={0:0},u=[];function i(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,i),t.l=!0,t.exports}i.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,a=document.createElement("script");a.charset="utf-8",a.timeout=120,i.nc&&a.setAttribute("nonce",i.nc),a.src=function(e){return i.p+""+({}[e]||e)+"-es5."+{1:"74313ded392a393618d4"}[e]+".js"}(e);var c=new Error;u=function(r){a.onerror=a.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:a})}),12e4);a.onerror=a.onload=u,document.head.appendChild(a)}return Promise.all(r)},i.m=e,i.c=n,i.d=function(e,r,t){i.o(e,r)||Object.defineProperty(e,r,{enumerable:!0,get:t})},i.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},i.t=function(e,r){if(1&r&&(e=i(e)),8&r)return e;if(4&r&&"object"==typeof e&&e&&e.__esModule)return e;var t=Object.create(null);if(i.r(t),Object.defineProperty(t,"default",{enumerable:!0,value:e}),2&r&&"string"!=typeof e)for(var n in e)i.d(t,n,(function(r){return e[r]}).bind(null,n));return t},i.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return i.d(r,"a",r),r},i.o=function(e,r){return Object.prototype.hasOwnProperty.call(e,r)},i.p="",i.oe=function(e){throw console.error(e),e};var a=window.webpackJsonp=window.webpackJsonp||[],c=a.push.bind(a);a.push=r,a=a.slice();for(var l=0;l<a.length;l++)r(a[l]);var f=c;t()}([]);

View File

@@ -0,0 +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()}([]);

View File

@@ -81,7 +81,15 @@ async function getSubscriptionInfo(sub, user_uid = null) {
return new Promise(resolve => {
// get videos
let downloadConfig = ['--dump-json', '--playlist-end', '1']
let downloadConfig = ['--dump-json', '--playlist-end', '1'];
let useCookies = config_api.getConfigItem('ytdl_use_cookies');
if (useCookies) {
if (fs.existsSync(path.join(__dirname, 'appdata', 'cookies.txt'))) {
downloadConfig.push('--cookies', path.join('appdata', 'cookies.txt'));
} else {
logger.warn('Cookies file could not be found. You can either upload one, or disable \'use cookies\' in the Advanced tab in the settings.');
}
}
youtubedl.exec(sub.url, downloadConfig, {}, function(err, output) {
if (debugMode) {
logger.info('Subscribe: got info for subscription ' + sub.id);
@@ -158,6 +166,11 @@ async function unsubscribe(sub, deleteMode, user_uid = null) {
else
db.get('subscriptions').remove({id: id}).write();
// failed subs have no name, on unsubscribe they shouldn't error
if (!sub.name) {
return;
}
const appendedBasePath = getAppendedBasePath(sub, basePath);
if (deleteMode && fs.existsSync(appendedBasePath)) {
if (sub.archive && fs.existsSync(sub.archive)) {
@@ -417,6 +430,15 @@ function getSubscription(subID, user_uid = null) {
return db.get('subscriptions').find({id: subID}).value();
}
function updateSubscription(sub, user_uid = null) {
if (user_uid) {
users_db.get('users').find({uid: user_uid}).get('subscriptions').find({id: sub.id}).assign(sub).write();
} else {
db.get('subscriptions').find({id: sub.id}).assign(sub).write();
}
return true;
}
function subExists(subID, user_uid = null) {
if (user_uid)
return !!users_db.get('users').find({uid: user_uid}).get('subscriptions').find({id: subID}).value();
@@ -476,6 +498,7 @@ function removeIDFromArchive(archive_path, id) {
module.exports = {
getSubscription : getSubscription,
getAllSubscriptions : getAllSubscriptions,
updateSubscription : updateSubscription,
subscribe : subscribe,
unsubscribe : unsubscribe,
deleteSubscriptionFile : deleteSubscriptionFile,

3
hooks/post_checkout Normal file
View File

@@ -0,0 +1,3 @@
#!/bin/bash
# downloads a local copy of qemu on docker-hub build machines
curl -L https://github.com/balena-io/qemu/releases/download/v3.0.0%2Bresin/qemu-3.0.0+resin-arm.tar.gz | tar zxvf - -C . && mv qemu-3.0.0+resin-arm/qemu-arm-static .

4
hooks/pre_build Normal file
View File

@@ -0,0 +1,4 @@
#!/bin/bash
# Register qemu-*-static for all supported processors except the
# current one, but also remove all registered binfmt_misc before
docker run --rm --privileged multiarch/qemu-user-static:register --reset

2
package-lock.json generated
View File

@@ -1,6 +1,6 @@
{
"name": "youtube-dl-material",
"version": "4.0.0",
"version": "4.1.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {

View File

@@ -23,7 +23,7 @@
<span i18n="Dark mode toggle label">Dark</span>
<mat-slide-toggle class="theme-slide-toggle" [checked]="postsService.theme.key === 'dark'"></mat-slide-toggle>
</button>
<button *ngIf="!postsService.isLoggedIn || postsService.permissions.includes('settings')" (click)="openSettingsDialog()" mat-menu-item>
<button *ngIf="postsService.config && (!postsService.config.Advanced.multi_user_mode || (postsService.isLoggedIn && postsService.permissions.includes('settings')))" (click)="openSettingsDialog()" mat-menu-item>
<mat-icon>settings</mat-icon>
<span i18n="Settings menu label">Settings</span>
</button>

View File

@@ -73,6 +73,7 @@ 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 { EditSubscriptionDialogComponent } from './dialogs/edit-subscription-dialog/edit-subscription-dialog.component';
registerLocaleData(es, 'es');
@@ -113,7 +114,8 @@ export function isVisible({ event, element, scrollContainer, offset }: IsVisible
CookiesUploaderDialogComponent,
LogsViewerComponent,
ModifyPlaylistComponent,
ConfirmDialogComponent
ConfirmDialogComponent,
EditSubscriptionDialogComponent
],
imports: [
CommonModule,

View File

@@ -18,7 +18,7 @@
<h5 style="margin-top: 10px;">Installation details:</h5>
<p>
<ng-container i18n="Version label">Installed version:</ng-container>&nbsp;{{current_version_tag}} - <span style="display: inline-block" *ngIf="checking_for_updates"><mat-spinner class="version-spinner" [diameter]="22"></mat-spinner>&nbsp;<ng-container i18n="Checking for updates text">Checking for updates...</ng-container></span>
<mat-icon *ngIf="!checking_for_updates" class="version-checked-icon">done</mat-icon>&nbsp;&nbsp;<a *ngIf="!checking_for_updates && latestGithubRelease['tag_name'] !== current_version_tag" [href]="latestUpdateLink" target="_blank"><ng-container i18n="View latest update">Update available</ng-container> - {{latestGithubRelease['tag_name']}}</a>. <ng-container i18n="Update through settings menu hint">You can update from the settings menu.</ng-container>
<mat-icon *ngIf="!checking_for_updates" class="version-checked-icon">done</mat-icon>&nbsp;&nbsp;<ng-container *ngIf="!checking_for_updates && latestGithubRelease['tag_name'] !== current_version_tag"><a [href]="latestUpdateLink" target="_blank"><ng-container i18n="View latest update">Update available</ng-container> - {{latestGithubRelease['tag_name']}}</a>. <ng-container i18n="Update through settings menu hint">You can update from the settings menu.</ng-container></ng-container>
<span *ngIf="!checking_for_updates && latestGithubRelease['tag_name'] === current_version_tag">You are up to date.</span>
</p>
<p>

View File

@@ -1,12 +1,15 @@
<h4 mat-dialog-title>{{dialogTitle}}</h4>
<mat-dialog-content>
<div>
<div style="margin-bottom: 10px;">
{{dialogText}}
</div>
</mat-dialog-content>
<mat-dialog-actions>
<!-- The mat-dialog-close directive optionally accepts a value as a result for the dialog. -->
<button color="primary" mat-flat-button type="submit" [mat-dialog-close]="true">{{submitText}}</button>
<button color="primary" mat-flat-button type="submit" (click)="confirmClicked()">{{submitText}}</button>
<div class="mat-spinner" *ngIf="submitClicked">
<mat-spinner [diameter]="25"></mat-spinner>
</div>
<span class="spacer"></span>
<button style="float: right;" mat-stroked-button mat-dialog-close>Cancel</button>
</mat-dialog-actions>

View File

@@ -1 +1,5 @@
.spacer {flex: 1 1 auto;}
.spacer {flex: 1 1 auto;}
.mat-spinner {
margin-left: 8px;
}

View File

@@ -1,5 +1,5 @@
import { Component, OnInit, Inject } from '@angular/core';
import { MAT_DIALOG_DATA } from '@angular/material/dialog';
import { Component, OnInit, Inject, EventEmitter } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
@Component({
selector: 'app-confirm-dialog',
@@ -8,14 +8,34 @@ import { MAT_DIALOG_DATA } from '@angular/material/dialog';
})
export class ConfirmDialogComponent implements OnInit {
dialogTitle: 'Confirm';
dialogText: 'Would you like to confirm?';
submitText: 'Yes'
dialogTitle = 'Confirm';
dialogText = 'Would you like to confirm?';
submitText = 'Yes'
submitClicked = false;
constructor(@Inject(MAT_DIALOG_DATA) public data: any) {
doneEmitter: EventEmitter<any> = null;
onlyEmitOnDone = false;
constructor(@Inject(MAT_DIALOG_DATA) public data: any, public dialogRef: MatDialogRef<ConfirmDialogComponent>) {
if (this.data.dialogTitle) { this.dialogTitle = this.data.dialogTitle };
if (this.data.dialogText) { this.dialogText = this.data.dialogText };
if (this.data.submitText) { this.submitText = this.data.submitText };
// checks if emitter exists, if so don't autoclose as it should be handled by caller
if (this.data.doneEmitter) {
this.doneEmitter = this.data.doneEmitter;
this.onlyEmitOnDone = true;
}
}
confirmClicked() {
if (this.onlyEmitOnDone) {
this.doneEmitter.emit(true);
this.submitClicked = true;
} else {
this.dialogRef.close(true);
}
}
ngOnInit(): void {

View File

@@ -0,0 +1,62 @@
<h4 mat-dialog-title i18n="Edit subscription dialog title">Editing {{sub.name}}</h4>
<mat-dialog-content>
<div class="container-fluid">
<div class="row">
<div class="col-12 mt-3">
<mat-checkbox (change)="downloadAllToggled()" [(ngModel)]="download_all"><ng-container i18n="Download all uploads subscription setting">Download all uploads</ng-container></mat-checkbox>
</div>
<div class="col-12" *ngIf="!download_all && editor_initialized">
<ng-container i18n="Download time range prefix">Download videos uploaded in the last</ng-container>
<mat-form-field color="accent" style="width: 50px; text-align: center; margin-left: 10px;">
<input type="number" matInput [(ngModel)]="timerange_amount" (ngModelChange)="timerangeChanged($event, false)">
</mat-form-field>
<mat-form-field class="unit-select">
<mat-select color="accent" [(ngModel)]="timerange_unit" (ngModelChange)="timerangeChanged($event, true)">
<mat-option *ngFor="let time_unit of time_units" [value]="time_unit + (timerange_amount === 1 ? '' : 's')">
{{time_unit + (timerange_amount === 1 ? '' : 's')}}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<div class="col-12">
<div>
<mat-checkbox [disabled]="true" [(ngModel)]="audioOnlyMode"><ng-container i18n="Streaming-only mode">Audio-only mode</ng-container></mat-checkbox>
</div>
</div>
<div class="col-12">
<div>
<mat-checkbox [disabled]="new_sub.type === 'audio'" [(ngModel)]="new_sub.streamingOnly"><ng-container i18n="Streaming-only mode">Streaming-only mode</ng-container></mat-checkbox>
</div>
</div>
<div class="col-12 mb-3">
<mat-form-field color="accent">
<input [(ngModel)]="new_sub.custom_args" matInput placeholder="Custom args" i18n-placeholder="Subscription custom args placeholder">
<button class="args-edit-button" (click)="openArgsModifierDialog()" mat-icon-button><mat-icon>edit</mat-icon></button>
<mat-hint>
<ng-container i18n="Custom args hint">These are added after the standard args.</ng-container>
</mat-hint>
</mat-form-field>
</div>
<div class="col-12">
<mat-form-field color="accent">
<input [(ngModel)]="new_sub.custom_output" matInput placeholder="Custom file output" i18n-placeholder="Subscription custom file output placeholder">
<mat-hint>
<a target="_blank" href="https://github.com/ytdl-org/youtube-dl/blob/master/README.md#output-template">
<ng-container i18n="Custom output template documentation link">Documentation</ng-container></a>.
<ng-container i18n="Custom Output input hint">Path is relative to the config download path. Don't include extension.</ng-container>
</mat-hint>
</mat-form-field>
</div>
</div>
</div>
</mat-dialog-content>
<mat-dialog-actions>
<button mat-button mat-dialog-close><ng-container i18n="Subscribe cancel button">Cancel</ng-container></button>
<!-- The mat-dialog-close directive optionally accepts a value as a result for the dialog. -->
<button mat-button [disabled]="updating || !subChanged()" type="submit" (click)="saveClicked()"><ng-container i18n="Save button">Save</ng-container></button>
<div class="mat-spinner" *ngIf="updating">
<mat-spinner [diameter]="25"></mat-spinner>
</div>
</mat-dialog-actions>

View File

@@ -0,0 +1,9 @@
.args-edit-button {
position: absolute;
margin-left: 10px;
}
.unit-select {
width: 75px;
margin-left: 20px;
}

View File

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

View File

@@ -0,0 +1,124 @@
import { Component, OnInit, Inject, ChangeDetectorRef } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialog } from '@angular/material/dialog';
import { PostsService } from 'app/posts.services';
import { ArgModifierDialogComponent } from '../arg-modifier-dialog/arg-modifier-dialog.component';
@Component({
selector: 'app-edit-subscription-dialog',
templateUrl: './edit-subscription-dialog.component.html',
styleUrls: ['./edit-subscription-dialog.component.scss']
})
export class EditSubscriptionDialogComponent implements OnInit {
updating = false;
sub = null;
new_sub = null;
editor_initialized = false;
timerange_amount: number;
timerange_unit = 'days';
audioOnlyMode = null;
download_all = null;
time_units = [
'day',
'week',
'month',
'year'
];
constructor(@Inject(MAT_DIALOG_DATA) public data: any, private dialog: MatDialog, private postsService: PostsService) {
this.sub = this.data.sub;
this.new_sub = JSON.parse(JSON.stringify(this.sub));
this.audioOnlyMode = this.sub.type === 'audio';
this.download_all = !this.sub.timerange;
if (this.sub.timerange) {
const timerange_str = this.sub.timerange.split('-')[1];
console.log(timerange_str);
const number = timerange_str.replace(/\D/g,'');
let units = timerange_str.replace(/[0-9]/g, '');
console.log(units);
// // remove plural on units
// if (units[units.length-1] === 's') {
// units = units.substring(0, units.length-1);
// }
this.timerange_amount = parseInt(number);
this.timerange_unit = units;
this.editor_initialized = true;
} else {
this.editor_initialized = true
}
}
ngOnInit(): void {
}
downloadAllToggled() {
if (this.download_all) {
this.new_sub.timerange = null;
} else {
console.log('checking');
this.timerangeChanged(null, null);
}
}
saveSubscription() {
this.postsService.updateSubscription(this.sub).subscribe(res => {
this.sub = this.new_sub;
this.new_sub = JSON.parse(JSON.stringify(this.sub));
})
}
getSubscription() {
this.postsService.getSubscription(this.sub.id).subscribe(res => {
this.sub = res['subscription'];
this.new_sub = JSON.parse(JSON.stringify(this.sub));
});
}
timerangeChanged(value, select_changed) {
console.log(this.timerange_amount);
console.log(this.timerange_unit);
if (this.timerange_amount && this.timerange_unit && !this.download_all) {
this.new_sub.timerange = 'now-' + this.timerange_amount.toString() + this.timerange_unit;
console.log(this.new_sub.timerange);
} else {
this.new_sub.timerange = null;
}
}
saveClicked() {
this.saveSubscription();
}
// modify custom args
openArgsModifierDialog() {
if (!this.new_sub.custom_args) {
this.new_sub.custom_args = '';
}
const dialogRef = this.dialog.open(ArgModifierDialogComponent, {
data: {
initial_args: this.new_sub.custom_args
}
});
dialogRef.afterClosed().subscribe(new_args => {
if (new_args !== null && new_args !== undefined) {
this.new_sub.custom_args = new_args;
}
});
}
subChanged() {
return JSON.stringify(this.new_sub) !== JSON.stringify(this.sub);
}
}

View File

@@ -24,14 +24,16 @@
</div>
<div class="col-12" *ngIf="!download_all">
<ng-container i18n="Download time range prefix">Download videos uploaded in the last</ng-container>
<mat-form-field color="accent" style="width: 50px; text-align: center">
<mat-form-field color="accent" style="width: 50px; text-align: center; margin-left: 10px;">
<input type="number" matInput [(ngModel)]="timerange_amount">
</mat-form-field>
<mat-select color="accent" class="unit-select" [(ngModel)]="timerange_unit">
<mat-option *ngFor="let time_unit of time_units" [value]="time_unit + (timerange_amount === 1 ? '' : 's')">
{{time_unit + (timerange_amount === 1 ? '' : 's')}}
</mat-option>
</mat-select>
<mat-form-field class="unit-select">
<mat-select color="accent" [(ngModel)]="timerange_unit">
<mat-option *ngFor="let time_unit of time_units" [value]="time_unit + (timerange_amount === 1 ? '' : 's')">
{{time_unit + (timerange_amount === 1 ? '' : 's')}}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<div class="col-12">
<div>

View File

@@ -81,6 +81,7 @@ export class PostsService implements CanActivate {
if (result) {
this.config = result['YoutubeDLMaterial'];
if (this.config['Advanced']['multi_user_mode']) {
this.checkAdminCreationStatus();
// login stuff
if (localStorage.getItem('jwt_token') && localStorage.getItem('jwt_token') !== 'null') {
this.token = localStorage.getItem('jwt_token');
@@ -163,12 +164,8 @@ export class PostsService implements CanActivate {
ui_uid: ui_uid}, this.httpOptions);
}
getFileStatusMp3(name: string) {
return this.http.post(this.path + 'fileStatusMp3', {name: name}, this.httpOptions);
}
getFileStatusMp4(name: string) {
return this.http.post(this.path + 'fileStatusMp4', {name: name}, this.httpOptions);
killAllDownloads() {
return this.http.post(this.path + 'killAllDownloads', {}, this.httpOptions);
}
loadNavItems() {
@@ -285,6 +282,10 @@ export class PostsService implements CanActivate {
audioOnly: audioOnly, customArgs: customArgs, customFileOutput: customFileOutput}, this.httpOptions);
}
updateSubscription(subscription) {
return this.http.post(this.path + 'updateSubscription', {subscription: subscription}, this.httpOptions);
}
unsubscribe(sub, deleteMode = false) {
return this.http.post(this.path + 'unsubscribe', {sub: sub, deleteMode: deleteMode}, this.httpOptions)
}
@@ -407,7 +408,6 @@ export class PostsService implements CanActivate {
}
sendToLogin() {
this.checkAdminCreationStatus();
if (!this.initialized) {
this.setInitialized();
}
@@ -437,8 +437,8 @@ export class PostsService implements CanActivate {
password: password}, this.httpOptions);
}
checkAdminCreationStatus(skip_check = false) {
if (!skip_check && !this.config['Advanced']['multi_user_mode']) {
checkAdminCreationStatus(force_show = false) {
if (!force_show && !this.config['Advanced']['multi_user_mode']) {
return;
}
this.adminExists().subscribe(res => {

View File

@@ -19,7 +19,7 @@
<mat-hint><ng-container i18n="URL setting input hint">URL this app will be accessed from, without the port.</ng-container></mat-hint>
</mat-form-field>
</div>
<div class="col-12 mb-4">
<div class="col-12 mb-4 mt-3">
<mat-form-field class="text-field" color="accent">
<input [(ngModel)]="new_config['Host']['port']" matInput placeholder="Port" i18n-placeholder="Port input placeholder" required>
<mat-hint><ng-container i18n="Port setting input hint">The desired port. Default is 17442.</ng-container></mat-hint>
@@ -140,7 +140,7 @@
</mat-form-field>
</div>
<div class="col-12 mt-5">
<div class="col-12 mt-4">
<mat-form-field class="text-field" style="margin-right: 12px;" color="accent">
<textarea matInput [(ngModel)]="new_config['Downloader']['custom_args']" placeholder="Custom args" i18n-placeholder="Custom args input placeholder"></textarea>
<mat-hint><ng-container i18n="Custom args setting input hint">Global custom args for downloads on the home page. Args are delimited using two commas like so: ,,</ng-container></mat-hint>
@@ -148,14 +148,18 @@
</mat-form-field>
</div>
<div class="col-12 mt-4">
<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-3">
<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>
</div>
<div class="col-12 mt-2">
<button (click)="killAllDownloads()" mat-stroked-button color="warn"><ng-container i18n="Kill all downloads button">Kill all downloads</ng-container></button>
</div>
</div>
</div>
</ng-template>

View File

@@ -1,4 +1,4 @@
import { Component, OnInit } from '@angular/core';
import { Component, OnInit, EventEmitter } from '@angular/core';
import { PostsService } from 'app/posts.services';
import { isoLangs } from './locales_list';
import { MatSnackBar } from '@angular/material/snack-bar';
@@ -8,6 +8,7 @@ import { ArgModifierDialogComponent } from 'app/dialogs/arg-modifier-dialog/arg-
import { CURRENT_VERSION } from 'app/consts';
import { MatCheckboxChange } from '@angular/material/checkbox';
import { CookiesUploaderDialogComponent } from 'app/dialogs/cookies-uploader-dialog/cookies-uploader-dialog.component';
import { ConfirmDialogComponent } from 'app/dialogs/confirm-dialog/confirm-dialog.component';
@Component({
selector: 'app-settings',
@@ -154,6 +155,34 @@ export class SettingsComponent implements OnInit {
});
}
killAllDownloads() {
const done = new EventEmitter<any>();
const dialogRef = this.dialog.open(ConfirmDialogComponent, {
data: {
dialogTitle: 'Kill downloads',
dialogText: 'Are you sure you want to kill all downloads? Any subscription and non-subscription downloads will end immediately, though this operation may take a minute or so to complete.',
submitText: 'Kill all downloads',
doneEmitter: done
}
});
done.subscribe(confirmed => {
if (confirmed) {
this.postsService.killAllDownloads().subscribe(res => {
if (res['success']) {
dialogRef.close();
this.postsService.openSnackBar('Successfully killed all downloads!');
} else {
dialogRef.close();
this.postsService.openSnackBar('Failed to kill all downloads! Check logs for details.');
}
}, err => {
dialogRef.close();
this.postsService.openSnackBar('Failed to kill all downloads! Check logs for details.');
});
}
});
}
// snackbar helper
public openSnackBar(message: string, action: string = '') {
this.snackBar.open(message, action, {

View File

@@ -42,5 +42,6 @@
</div>
</div>
</div>
<button class="edit-button" color="primary" (click)="editSubscription()" [disabled]="downloading" mat-fab><mat-icon class="save-icon">edit</mat-icon></button>
<button class="save-button" color="primary" (click)="downloadContent()" [disabled]="downloading" mat-fab><mat-icon class="save-icon">save</mat-icon><mat-spinner *ngIf="downloading" class="spinner" [diameter]="50"></mat-spinner></button>
</div>

View File

@@ -58,6 +58,12 @@
bottom: 25px;
}
.edit-button {
left: 25px;
position: absolute;
bottom: 25px;
}
.save-icon {
bottom: 1px;
position: relative;

View File

@@ -1,6 +1,8 @@
import { Component, OnInit } from '@angular/core';
import { PostsService } from 'app/posts.services';
import { ActivatedRoute, Router } from '@angular/router';
import { MatDialog } from '@angular/material/dialog';
import { EditSubscriptionDialogComponent } from 'app/dialogs/edit-subscription-dialog/edit-subscription-dialog.component';
@Component({
selector: 'app-subscription',
@@ -43,7 +45,7 @@ export class SubscriptionComponent implements OnInit {
filterProperty = this.filterProperties['upload_date'];
downloading = false;
constructor(private postsService: PostsService, private route: ActivatedRoute, private router: Router) { }
constructor(private postsService: PostsService, private route: ActivatedRoute, private router: Router, private dialog: MatDialog) { }
ngOnInit() {
if (this.route.snapshot.paramMap.get('id')) {
@@ -148,4 +150,12 @@ export class SubscriptionComponent implements OnInit {
});
}
editSubscription() {
this.dialog.open(EditSubscriptionDialogComponent, {
data: {
sub: this.subscription
}
});
}
}