Compare commits

...

51 Commits

Author SHA1 Message Date
Isaac Abadi
dc0ae1dbcc Moved hooks to proper location 2020-08-08 16:18:24 -04:00
Isaac Abadi
d5955f6a4c Updated arm dockerfile 2020-08-08 16:10:21 -04:00
Isaac Abadi
8d8ccc66dd Merge branch 'master' of https://github.com/Tzahi12345/YoutubeDL-Material into dockertest 2020-08-08 16:08:54 -04:00
Isaac Abadi
5088ce0291 Potentially fixed autobuild issue for ARM images 2020-08-08 16:02:41 -04:00
Isaac Abadi
576e2109d7 Merge branch 'master' of https://github.com/Tzahi12345/YoutubeDL-Material 2020-08-08 16:00:09 -04:00
Tzahi12345
ee169cd7ce If chown fails during container setup, then a warning will be shown 2020-08-05 21:29:03 -04:00
Tzahi12345
835790e69c Chown that fails will not crash startup anymore for Docker 2020-08-03 20:07:02 -04:00
Tzahi12345
d0aed8f144 Merge pull request #184 from Tzahi12345/edit-subscriptions
Adds ability to edit subscriptions
2020-08-01 21:31:46 -04:00
Isaac Abadi
d17b68d76e Updated frontend binaries 2020-08-01 21:28:49 -04:00
Isaac Abadi
a481869166 Slightly update subscribe dialog styling 2020-08-01 21:28:22 -04:00
Isaac Abadi
eb4ed32fcb Added edit button to subscription 2020-08-01 21:27:35 -04:00
Isaac Abadi
9aee6e91cd Added API to update subscription
Edit subscription component now works
2020-08-01 21:26:47 -04:00
Isaac Abadi
37d3e9326c Merge branch 'master' of https://github.com/Tzahi12345/YoutubeDL-Material into edit-subscriptions 2020-08-01 20:10:47 -04:00
Isaac Abadi
dbf8f9ebfd Updated package-locks 2020-08-01 20:09:29 -04:00
Tzahi12345
7858e26b15 Update README.md
Updated Docker section in README to show how to use a custom UID/GID
2020-08-01 14:06:47 -04:00
Isaac Abadi
057ad67672 Fixed bug where subscribing to a private playlist failed 2020-08-01 06:55:25 -04:00
Isaac Abadi
3ebc903ce9 Fixed bug where non-admins in multi user mode could change the settings if they could open the dialog 2020-07-27 22:09:15 -04:00
Isaac Abadi
21c5795f1c Rebuilt frontend binaries 2020-07-27 22:02:23 -04:00
Isaac Abadi
f12ea017bc Re-added parameter to checkAdminCreationStatus 2020-07-27 22:01:47 -04:00
Isaac Abadi
cd18bce509 Fixed bug where settings menu could be accessed from the login menu in multi user mode 2020-07-27 21:55:01 -04:00
Isaac Abadi
5ef4388d73 Fixed bug where checking admin creation status would not run 2020-07-27 21:51:31 -04:00
Isaac Abadi
a78c0cb56c Updated frontend binaries 2020-07-25 16:13:09 -04:00
Isaac Abadi
ae9e4e6857 Updated styling in settings and about page for a cleaner look 2020-07-25 16:02:36 -04:00
Isaac Abadi
333556c305 Removed erroneous code and added the ability to kill all downlaods 2020-07-25 15:57:10 -04:00
Isaac Abadi
c5b0a7a697 Updated confirm dialog to support async requests with loading spinner 2020-07-25 15:55:52 -04:00
Isaac Abadi
d7631360cc Erroneous quality args are no longer added when using then normal downloading method 2020-07-23 23:29:05 -04:00
Isaac Abadi
c800308b9d Removed all instances of res.end() as it caused errors on debian-based systems and is a redundant call 2020-07-22 21:58:35 -04:00
Isaac Abadi
c1c57135ba Fixed bug where deleting an audio file would result in an error 2020-07-22 21:57:09 -04:00
Isaac Grynsztein
f5215baa55 Updated arm dockerfile 2020-07-18 22:35:06 -04:00
Isaac Grynsztein
834ac00694 Merge branch 'master' of https://github.com/Tzahi12345/YoutubeDL-Material 2020-07-15 23:48:34 -04:00
Isaac Grynsztein
935ae3452c Repo cleanup 2020-07-15 23:46:25 -04:00
Isaac Grynsztein
335b588c3a Added edit subscription dialog (WIP) 2020-07-15 22:58:17 -04:00
Tzahi12345
493abc3a4c Merge pull request #170 from Tzahi12345/dependabot/npm_and_yarn/backend/lodash-4.17.19
Bump lodash from 4.17.15 to 4.17.19 in /backend
2020-07-15 17:37:18 -04:00
dependabot[bot]
238abc1686 Bump lodash from 4.17.15 to 4.17.19 in /backend
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.15 to 4.17.19.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.15...4.17.19)

Signed-off-by: dependabot[bot] <support@github.com>
2020-07-15 21:35:09 +00:00
Tzahi12345
fbfad6c3e2 Merge pull request #168 from fluxtendu/master
Switch from bcrypt to bcryptjs to keep compatibility with Raspberry Pi and a Dockerfile-armhf bonus
2020-07-15 17:34:34 -04:00
fluxtendu
a6ff65f004 Merge branch 'master' of github.com:fluxtendu/YoutubeDL-Material 2020-07-15 00:02:06 +02:00
fluxtendu
89cd969fcb Adapted for Raspberry Pi:
- Added Dockerfile-armhf (arm32v7 alpine)
 - Switched from bcrypt to bcrypt.js

new file : Dockerfile-armhf
modified : authentication/auth.js
modified : package.json
2020-07-14 23:00:08 +02:00
Tzahi12345
e7181b57c7 Update README.md
Updated contributing section to include the Contributing wiki page
2020-07-11 22:03:13 -04:00
Tzahi12345
38fa39d765 Merge pull request #159 from UnlimitedCookies/patch-1
Make more items translatable
2020-07-11 19:26:16 -04:00
UnlimitedCookies
9e5de88675 Even more internationalization improvements :) 2020-07-10 15:00:54 +02:00
Isaac Grynsztein
9cf4949c30 Updated frontend binaries 2020-07-09 22:37:24 -04:00
Isaac Grynsztein
dd80c51f16 Removed pin setting functionality
- Simplifies security options: use multi user mode if you want to restrict access to the settings menu
2020-07-09 22:33:07 -04:00
UnlimitedCookies
84d83f228e Make close button in About Dialog translatable 2020-07-10 01:31:59 +02:00
Isaac Grynsztein
14bd82c508 Fixed bug where navigating home would reset the cached volume setting 2020-07-07 15:27:15 -04:00
Isaac Grynsztein
0b6606cafb Updated spanish translations 2020-07-05 22:18:08 -04:00
Isaac Grynsztein
e97e9ec717 Logs viewer will now color-code logs based on type (error, warning, info, etc.)
You can also clear logs from the logs viewer as well
2020-07-05 22:17:54 -04:00
Isaac Grynsztein
990b3d4037 Added confirm dialog component to help with confirming actions 2020-07-05 22:12:07 -04:00
Isaac Grynsztein
2e7b1c2d53 Fixed wording for youtube-dl archive setting in the downloader tab 2020-07-04 21:34:47 -04:00
Isaac Grynsztein
a4eb7fb745 Updated README to reflect new changes to Docker installation process and removed config items due to clutter and out-of-dateness 2020-07-04 12:17:16 -04:00
Isaac Grynsztein
2442067ca0 Manually updated spanish translations 2020-07-04 12:07:22 -04:00
Isaac Grynsztein
41d4dfeba1 Updated version to 4.1 2020-07-03 17:57:47 -04:00
64 changed files with 1025 additions and 847 deletions

View File

@@ -53,34 +53,6 @@ NOTE: If you are intending to use a [reverse proxy](https://github.com/Tzahi1234
If you experience problems, know that it's usually caused by a configuration problem. The first thing you should do is check the console. To get there, right click anywhere on the page and click "Inspect element." Then on the menu that pops up, click console. Look at the error there, and try to investigate.
### Configuration
NOTE: If you are using YoutubeDL-Material v3.2 or lower, click [here](https://github.com/Tzahi12345/YoutubeDL-Material/blob/b87a9f1e2fd896b8e3b2f12429b7ffb15ea4521b/README.md#configuration) for the old README
Here is an explanation for the configuration entries. Check out the [default config](https://github.com/Tzahi12345/YoutubeDL-Material/blob/master/backend/config/default.json) for more context.
| Config item | Description | Default |
| ------------- | ------------- | ------------- |
| url | URL to the server hosting YoutubeDL-Material | "http://example.com" |
| port | Desired port for YoutubeDL-Material | "17442" |
| use-encryption | true if you intend to use SSL encryption (https) | false |
| cert-file-path | Cert file path - required if using encryption | "/etc/letsencrypt/live/example.com/fullchain.pem" |
| key-file-path | Private key file path - required if using encryption | "/etc/letsencrypt/live/example.com/privkey.pem" |
| path-audio | Path to audio folder for saved mp3s | "audio/" |
| path-video | Path to video folder for saved mp4s | "video/" |
| title_top | Title shown on the top toolbar | "Youtube Downloader" |
| file_manager_enabled | true if you want to use the file manager | true |
| allow_quality_select | true if you want to select a videos quality level before downloading | true |
| download_only_mode | true if you want files to directly download to the client with no media player | false |
| allow_multi_download_mode | true if you want the ability to download multiple videos at the same time | true |
| use_youtube_API | true if you want to use the Youtube API which is used for YT searches | false |
| youtube_API_key | Youtube API key. Required if use_youtube_API is enabled | "" |
| default_theme | Default theme to use. Options are "default" and "dark" | "default" |
| allow_theme_change | true if you want the icon in the top toolbar that toggles dark mode | true |
| use_default_downloading_agent | true if you want to use youtube-dl's default downloader | true |
| custom_downloading_agent | If not using the default downloader, this is the downloader you want to use | "" |
| allow_advanced_download | true if you want to use the Advanced download options | false |
## Build it yourself
If you'd like to install YoutubeDL-Material, go to the Installation section. If you want to build it yourself and/or develop the repository, then this section is for you.
@@ -97,14 +69,24 @@ 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/youtubedl-material-docker.zip -o youtubedl-material-docker.zip` to download the latest Docker zip release, or go to the [releases](https://github.com/Tzahi12345/YoutubeDL-Material/releases/) page to grab the version you'd like.
2. Unzip the `youtubedl-material-docker.zip` and navigate into the root folder.
3. Modify the config items in the `appdata` folder to your liking. The default options will work, however, and point to `http://localhost:8998`. You can find an explanation of these configuration items in [Configuration](#Configuration) section.
4. Run `docker-compose pull`. This will download the official YoutubeDL-Material docker image.
5. Run `docker-compose up` to start it up. If successful, it should say "HTTP(S): Started on port 8998" or something similar.
6. Make sure you can connect to the specified URL + port, and if so, you are done!
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.
2. Run `docker-compose pull`. This will download the official YoutubeDL-Material docker image.
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
@@ -116,12 +98,20 @@ Once you have enabled the API and have the key, you can start sending requests b
## Contributing
Feel free to submit a pull request! I have no guidelines as of yet, so no need to worry about that.
If you're interested in contributing, first: awesome! Second, please refer to the guidelines/setup information located in the [Contributing](https://github.com/Tzahi12345/YoutubeDL-Material/wiki/Contributing) wiki page, it's a helpful way to get you on your feet and coding away.
Pull requests are always appreciated! If you're a bit rusty with coding, that's no problem: we can always help you learn. And if that's too scary, that's OK too! You can create issues for features you'd like to see or bugs you encounter, it all helps this project grow.
If you're interested in translating the app into a new language, check out the [Translate](https://github.com/Tzahi12345/YoutubeDL-Material/wiki/Translate) wiki page.
## Authors
* **Isaac Grynsztein** (me!) - *Initial work*
Official translators:
* Spanish - tzahi12345
* German - UnlimitedCookies
See also the list of [contributors](https://github.com/your/project/contributors) who participated in this project.
## License

29
backend/Dockerfile-armhf Normal file
View File

@@ -0,0 +1,29 @@
FROM arm32v7/alpine:3.12
COPY qemu-arm-static /usr/bin
ENV UID=1000 \
GID=1000 \
USER=youtube
RUN addgroup -S $USER -g $GID && adduser -D -S $USER -G $USER -u $UID
RUN apk add --no-cache \
ffmpeg \
npm \
python2 \
su-exec \
&& apk add --no-cache --repository http://dl-cdn.alpinelinux.org/alpine/edge/testing/ \
atomicparsley
WORKDIR /app
COPY --chown=$UID:$GID [ "package.json", "package-lock.json", "/app/" ]
RUN npm install && chown -R $UID:$GID ./
COPY --chown=$UID:$GID [ "./", "/app/" ]
EXPOSE 17442
ENTRYPOINT [ "/app/entrypoint.sh" ]
CMD [ "node", "app.js" ]

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';
@@ -96,7 +97,6 @@ db.defaults(
configWriteFlag: false,
downloads: {},
subscriptions: [],
pin_md5: '',
files_to_db_migration_complete: false
}).write();
@@ -504,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());
@@ -881,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);
@@ -928,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;
@@ -964,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);
@@ -1005,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;
@@ -1194,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
@@ -1329,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
@@ -1469,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) {
@@ -1495,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');
@@ -1849,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);
@@ -1868,23 +1911,6 @@ app.get('/api/using-encryption', function(req, res) {
res.send(usingEncryption);
});
app.post('/api/logs', async function(req, res) {
let logs = null;
let lines = req.body.lines;
logs_path = path.join('appdata', 'logs', 'combined.log')
if (fs.existsSync(logs_path)) {
if (lines) logs = await read_last_lines.read(logs_path, lines);
else logs = fs.readFileSync(logs_path, 'utf8');
}
else
logger.error(`Failed to find logs file at the expected location: ${logs_path}`)
res.send({
logs: logs,
success: !!logs
});
});
app.post('/api/tomp3', optionalJwt, async function(req, res) {
var url = req.body.url;
var options = {
@@ -1912,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) {
@@ -1943,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
@@ -2325,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;
@@ -2473,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;
@@ -2509,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;
@@ -2517,7 +2510,6 @@ app.post('/api/deleteMp4', optionalJwt, async (req, res) => {
} else {
wasDeleted = false;
res.send(wasDeleted);
res.end("yes");
}
});
@@ -2640,49 +2632,6 @@ app.post('/api/updateServer', async (req, res) => {
});
// Pin API calls
app.post('/api/isPinSet', async (req, res) => {
let stored_pin = db.get('pin_md5').value();
let is_set = false;
if (!stored_pin || stored_pin.length === 0) {
} else {
is_set = true;
}
res.send({
is_set: is_set
});
});
app.post('/api/setPin', async (req, res) => {
let unhashed_pin = req.body.pin;
let hashed_pin = md5(unhashed_pin);
db.set('pin_md5', hashed_pin).write();
res.send({
success: true
});
});
app.post('/api/checkPin', async (req, res) => {
let input_pin = req.body.input_pin;
let input_pin_md5 = md5(input_pin);
let stored_pin = db.get('pin_md5').value();
let successful = false;
if (input_pin_md5 === stored_pin) {
successful = true;
}
res.send({
success: successful
});
});
// API Key API calls
app.post('/api/generateNewAPIKey', function (req, res) {
@@ -2868,6 +2817,42 @@ app.get('/api/audio/:id', optionalJwt, function(req , res){
res.send({success: success, downloads: downloads});
});
// logs management
app.post('/api/logs', async function(req, res) {
let logs = null;
let lines = req.body.lines;
logs_path = path.join('appdata', 'logs', 'combined.log')
if (fs.existsSync(logs_path)) {
if (lines) logs = await read_last_lines.read(logs_path, lines);
else logs = fs.readFileSync(logs_path, 'utf8');
}
else
logger.error(`Failed to find logs file at the expected location: ${logs_path}`)
res.send({
logs: logs,
success: !!logs
});
});
app.post('/api/clearAllLogs', async function(req, res) {
logs_path = path.join('appdata', 'logs', 'combined.log');
logs_err_path = path.join('appdata', 'logs', 'error.log');
let success = false;
try {
fs.writeFileSync(logs_path, '');
fs.writeFileSync(logs_err_path, '');
success = true;
} catch(e) {
logger.error(e);
}
res.send({
success: success
});
});
app.post('/api/getVideoInfos', async (req, res) => {
let fileNames = req.body.fileNames;
let urlMode = !!req.body.urlMode;

View File

@@ -22,7 +22,6 @@
"allow_quality_select": true,
"download_only_mode": false,
"allow_multi_download_mode": true,
"settings_pin_required": false,
"enable_downloads_manager": true
},
"API": {

View File

@@ -22,7 +22,6 @@
"allow_quality_select": true,
"download_only_mode": false,
"allow_multi_download_mode": true,
"settings_pin_required": false,
"enable_downloads_manager": true
},
"API": {

View File

@@ -5,7 +5,7 @@ var subscriptions_api = require('../subscriptions')
const fs = require('fs-extra');
var jwt = require('jsonwebtoken');
const { uuid } = require('uuidv4');
var bcrypt = require('bcrypt');
var bcrypt = require('bcryptjs');
var LocalStrategy = require('passport-local').Strategy;

View File

@@ -199,7 +199,6 @@ DEFAULT_CONFIG = {
"allow_quality_select": true,
"download_only_mode": false,
"allow_multi_download_mode": true,
"settings_pin_required": false,
"enable_downloads_manager": true
},
"API": {

View File

@@ -66,10 +66,6 @@ let CONFIG_ITEMS = {
'key': 'ytdl_allow_multi_download_mode',
'path': 'YoutubeDLMaterial.Extra.allow_multi_download_mode'
},
'ytdl_settings_pin_required': {
'key': 'ytdl_settings_pin_required',
'path': 'YoutubeDLMaterial.Extra.settings_pin_required'
},
'ytdl_enable_downloads_manager': {
'key': 'ytdl_enable_downloads_manager',
'path': 'YoutubeDLMaterial.Extra.enable_downloads_manager'
@@ -174,5 +170,5 @@ AVAILABLE_PERMISSIONS = [
module.exports = {
CONFIG_ITEMS: CONFIG_ITEMS,
AVAILABLE_PERMISSIONS: AVAILABLE_PERMISSIONS,
CURRENT_VERSION: 'v4.0'
CURRENT_VERSION: 'v4.1'
}

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

@@ -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
backend/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

View File

@@ -50,6 +50,11 @@
"color-convert": "^1.9.0"
}
},
"any-promise": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
"integrity": "sha1-q8av7tzqUugJzcA3au0845Y10X8="
},
"anymatch": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz",
@@ -64,11 +69,6 @@
"resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz",
"integrity": "sha1-HjRA6RXwsSA9I3SOeO3XubW0PlY="
},
"aproba": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz",
"integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw=="
},
"archiver": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/archiver/-/archiver-3.1.1.tgz",
@@ -126,31 +126,6 @@
}
}
},
"are-we-there-yet": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz",
"integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==",
"requires": {
"delegates": "^1.0.0",
"readable-stream": "^2.0.6"
},
"dependencies": {
"readable-stream": {
"version": "2.3.7",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz",
"integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==",
"requires": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
"isarray": "~1.0.0",
"process-nextick-args": "~2.0.0",
"safe-buffer": "~5.1.1",
"string_decoder": "~1.1.1",
"util-deprecate": "~1.0.1"
}
}
}
},
"array-flatten": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
@@ -204,15 +179,6 @@
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz",
"integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g=="
},
"bcrypt": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-4.0.1.tgz",
"integrity": "sha512-hSIZHkUxIDS5zA2o00Kf2O5RfVbQ888n54xQoF/eIaquU4uaLxK8vhhBdktd0B3n2MjkcAWzv4mnhogykBKOUQ==",
"requires": {
"node-addon-api": "^2.0.0",
"node-pre-gyp": "0.14.0"
}
},
"bcrypt-pbkdf": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz",
@@ -221,6 +187,11 @@
"tweetnacl": "^0.14.3"
}
},
"bcryptjs": {
"version": "2.4.3",
"resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz",
"integrity": "sha1-mrVie5PmBiH/fNrF2pczAn3x0Ms="
},
"big-integer": {
"version": "1.6.48",
"resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.48.tgz",
@@ -434,11 +405,6 @@
"readdirp": "~3.3.0"
}
},
"chownr": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
"integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="
},
"ci-info": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/ci-info/-/ci-info-1.6.0.tgz",
@@ -449,11 +415,6 @@
"resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-1.0.0.tgz",
"integrity": "sha1-T6kXw+WclKAEzWH47lCdplFocUM="
},
"code-point-at": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz",
"integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c="
},
"color": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/color/-/color-3.0.0.tgz",
@@ -614,10 +575,10 @@
"xdg-basedir": "^3.0.0"
}
},
"console-control-strings": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz",
"integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4="
"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",
@@ -718,11 +679,6 @@
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk="
},
"delegates": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
"integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o="
},
"depd": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
@@ -733,11 +689,6 @@
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz",
"integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA="
},
"detect-libc": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz",
"integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups="
},
"diagnostics": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/diagnostics/-/diagnostics-1.1.1.tgz",
@@ -1051,14 +1002,6 @@
"universalify": "^1.0.0"
}
},
"fs-minipass": {
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.7.tgz",
"integrity": "sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA==",
"requires": {
"minipass": "^2.6.0"
}
},
"fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
@@ -1081,54 +1024,6 @@
"rimraf": "2"
}
},
"gauge": {
"version": "2.7.4",
"resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz",
"integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=",
"requires": {
"aproba": "^1.0.3",
"console-control-strings": "^1.0.0",
"has-unicode": "^2.0.0",
"object-assign": "^4.1.0",
"signal-exit": "^3.0.0",
"string-width": "^1.0.1",
"strip-ansi": "^3.0.1",
"wide-align": "^1.1.0"
},
"dependencies": {
"ansi-regex": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
"integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8="
},
"is-fullwidth-code-point": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz",
"integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=",
"requires": {
"number-is-nan": "^1.0.0"
}
},
"string-width": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz",
"integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=",
"requires": {
"code-point-at": "^1.0.0",
"is-fullwidth-code-point": "^1.0.0",
"strip-ansi": "^3.0.0"
}
},
"strip-ansi": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
"integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
"requires": {
"ansi-regex": "^2.0.0"
}
}
}
},
"get-stream": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.1.0.tgz",
@@ -1228,11 +1123,6 @@
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
"integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0="
},
"has-unicode": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz",
"integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk="
},
"hashish": {
"version": "0.0.4",
"resolved": "https://registry.npmjs.org/hashish/-/hashish-0.0.4.tgz",
@@ -1301,14 +1191,6 @@
"resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz",
"integrity": "sha1-SMptcvbGo68Aqa1K5odr44ieKwk="
},
"ignore-walk": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.3.tgz",
"integrity": "sha512-m7o6xuOaT1aqheYHKf8W6J5pYH85ZI9w077erOzLje3JsB1gkafkAhHHY19dqjulgIZHFm32Cp5uNZgcQqdJKw==",
"requires": {
"minimatch": "^3.0.4"
}
},
"import-lazy": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-2.1.0.tgz",
@@ -1596,9 +1478,9 @@
"integrity": "sha1-hMinKrWcRyUyFIDJdeZQg0LnCTc="
},
"lodash": {
"version": "4.17.15",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
"integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A=="
"version": "4.17.19",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz",
"integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ=="
},
"lodash.defaults": {
"version": "4.2.0",
@@ -1782,30 +1664,6 @@
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw=="
},
"minipass": {
"version": "2.9.0",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-2.9.0.tgz",
"integrity": "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==",
"requires": {
"safe-buffer": "^5.1.2",
"yallist": "^3.0.0"
},
"dependencies": {
"yallist": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="
}
}
},
"minizlib": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.3.3.tgz",
"integrity": "sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q==",
"requires": {
"minipass": "^2.9.0"
}
},
"mkdirp": {
"version": "0.5.4",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.4.tgz",
@@ -1859,46 +1717,26 @@
}
}
},
"mz": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",
"integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==",
"requires": {
"any-promise": "^1.0.0",
"object-assign": "^4.0.1",
"thenify-all": "^1.0.0"
}
},
"nanoid": {
"version": "2.1.11",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-2.1.11.tgz",
"integrity": "sha512-s/snB+WGm6uwi0WjsZdaVcuf3KJXlfGl2LcxgwkEwJF0D/BWzVWAZW/XY4bFaiR7s0Jk3FPvlnepg1H1b1UwlA=="
},
"needle": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/needle/-/needle-2.4.1.tgz",
"integrity": "sha512-x/gi6ijr4B7fwl6WYL9FwlCvRQKGlUNvnceho8wxkwXqN8jvVmmmATTmZPRRG7b/yC1eode26C2HO9jl78Du9g==",
"requires": {
"debug": "^3.2.6",
"iconv-lite": "^0.4.4",
"sax": "^1.2.4"
},
"dependencies": {
"debug": {
"version": "3.2.6",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
"integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
"requires": {
"ms": "^2.1.1"
}
},
"ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
}
}
},
"negotiator": {
"version": "0.6.2",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz",
"integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw=="
},
"node-addon-api": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-2.0.0.tgz",
"integrity": "sha512-ASCL5U13as7HhOExbT6OlWJJUV/lLzL2voOSP1UVehpRD8FbSrSDjfScK/KwAvVTI5AS6r4VwbOMlIqtvRidnA=="
},
"node-fetch": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.0.tgz",
@@ -1912,34 +1750,6 @@
"iconv-lite": "^0.4.15"
}
},
"node-pre-gyp": {
"version": "0.14.0",
"resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.14.0.tgz",
"integrity": "sha512-+CvDC7ZttU/sSt9rFjix/P05iS43qHCOOGzcr3Ry99bXG7VX953+vFyEuph/tfqoYu8dttBkE86JSKBO2OzcxA==",
"requires": {
"detect-libc": "^1.0.2",
"mkdirp": "^0.5.1",
"needle": "^2.2.1",
"nopt": "^4.0.1",
"npm-packlist": "^1.1.6",
"npmlog": "^4.0.2",
"rc": "^1.2.7",
"rimraf": "^2.6.1",
"semver": "^5.3.0",
"tar": "^4.4.2"
},
"dependencies": {
"nopt": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.3.tgz",
"integrity": "sha512-CvaGwVMztSMJLOeXPrez7fyfObdZqNUK1cPAEzLHrTybIua9pMdmmPR5YwtfNftIOMv3DPUhFaxsZMNTQO20Kg==",
"requires": {
"abbrev": "1",
"osenv": "^0.1.4"
}
}
}
},
"nodemon": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.2.tgz",
@@ -1985,29 +1795,6 @@
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="
},
"npm-bundled": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.1.1.tgz",
"integrity": "sha512-gqkfgGePhTpAEgUsGEgcq1rqPXA+tv/aVBlgEzfXwA1yiUJF7xtEt3CtVwOjNYQOVknDk0F20w58Fnm3EtG0fA==",
"requires": {
"npm-normalize-package-bin": "^1.0.1"
}
},
"npm-normalize-package-bin": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-1.0.1.tgz",
"integrity": "sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA=="
},
"npm-packlist": {
"version": "1.4.8",
"resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.4.8.tgz",
"integrity": "sha512-5+AZgwru5IevF5ZdnFglB5wNlHG1AOOuw28WhUq8/8emhBmLv6jX5by4WJCh7lW0uSYZYS6DXqIsyZVIXRZU9A==",
"requires": {
"ignore-walk": "^3.0.1",
"npm-bundled": "^1.0.1",
"npm-normalize-package-bin": "^1.0.1"
}
},
"npm-run-path": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz",
@@ -2016,22 +1803,6 @@
"path-key": "^3.0.0"
}
},
"npmlog": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz",
"integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==",
"requires": {
"are-we-there-yet": "~1.1.2",
"console-control-strings": "~1.1.0",
"gauge": "~2.7.3",
"set-blocking": "~2.0.0"
}
},
"number-is-nan": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz",
"integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0="
},
"oauth-sign": {
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz",
@@ -2076,25 +1847,6 @@
"mimic-fn": "^2.1.0"
}
},
"os-homedir": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz",
"integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M="
},
"os-tmpdir": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
"integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ="
},
"osenv": {
"version": "0.1.5",
"resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz",
"integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==",
"requires": {
"os-homedir": "^1.0.0",
"os-tmpdir": "^1.0.0"
}
},
"p-finally": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/p-finally/-/p-finally-2.0.1.tgz",
@@ -2243,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",
@@ -2311,6 +2071,14 @@
"strip-json-comments": "~2.0.1"
}
},
"read-last-lines": {
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/read-last-lines/-/read-last-lines-1.7.2.tgz",
"integrity": "sha512-K0yUvTYAYn6qpyLJufaJ7yC6BeL23qpgZ8SKM7/fA1R1rHotCDxB/zDp9i1I2JHvexWBW6/35jkt07iiIKKp4g==",
"requires": {
"mz": "^2.7.0"
}
},
"readable-stream": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
@@ -2403,11 +2171,6 @@
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
},
"sax": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
"integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw=="
},
"semver": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
@@ -2459,11 +2222,6 @@
"send": "0.17.1"
}
},
"set-blocking": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
"integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc="
},
"setimmediate": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
@@ -2603,25 +2361,12 @@
"has-flag": "^3.0.0"
}
},
"tar": {
"version": "4.4.13",
"resolved": "https://registry.npmjs.org/tar/-/tar-4.4.13.tgz",
"integrity": "sha512-w2VwSrBoHa5BsSyH+KxEqeQBAllHhccyMFVHtGtdMpF4W7IRWfZjFiQceJPChOeTsSDVUpER2T8FA93pr0L+QA==",
"table-parser": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/table-parser/-/table-parser-0.1.3.tgz",
"integrity": "sha1-BEHPzhallIFoTCfRtaZ/8VpDx7A=",
"requires": {
"chownr": "^1.1.1",
"fs-minipass": "^1.2.5",
"minipass": "^2.8.6",
"minizlib": "^1.2.1",
"mkdirp": "^0.5.0",
"safe-buffer": "^5.1.2",
"yallist": "^3.0.3"
},
"dependencies": {
"yallist": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="
}
"connected-domain": "^1.0.0"
}
},
"tar-stream": {
@@ -2724,6 +2469,22 @@
"resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz",
"integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg=="
},
"thenify": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
"integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==",
"requires": {
"any-promise": "^1.0.0"
}
},
"thenify-all": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz",
"integrity": "sha1-GhkY1ALY/D+Y+/I02wvMjMEOlyY=",
"requires": {
"thenify": ">= 3.1.0 < 4"
}
},
"timed-out": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/timed-out/-/timed-out-4.0.1.tgz",
@@ -2939,14 +2700,6 @@
"isexe": "^2.0.0"
}
},
"wide-align": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz",
"integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==",
"requires": {
"string-width": "^1.0.2 || 2"
}
},
"widest-line": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/widest-line/-/widest-line-2.0.1.tgz",

View File

@@ -30,7 +30,7 @@
"dependencies": {
"archiver": "^3.1.1",
"async": "^3.1.0",
"bcrypt": "^4.0.1",
"bcryptjs": "^2.4.0",
"compression": "^1.7.4",
"config": "^3.2.3",
"exe": "^1.0.2",
@@ -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

View File

@@ -183,7 +183,7 @@
"b7ff2e2b909c53abe088fe60b9f4b6ac7757247f": "Nutzer registrieren",
"024886ca34a6f309e3e51c2ed849320592c3faaa": "Benutzername",
"2bd201aea09e43fbfd3cd15ec0499b6755302329": "Benutzer verwalten",
"29c97c8e76763bb15b6d515648fa5bd1eb0f7510": "Benutzer-UID",
"29c97c8e76763bb15b6d515648fa5bd1eb0f7510": "Benutzer-UID:",
"e70e209561583f360b1e9cefd2cbb1fe434b6229": "Neues Passwort",
"6498fa1b8f563988f769654a75411bb8060134b9": "Neues Passwort festlegen",
"40da072004086c9ec00d125165da91eaade7f541": "Standard verwenden",

View File

@@ -196,5 +196,20 @@
"52c1447c1ec9570a2a3025c7e566557b8d19ed92": " Rol ",
"59a8c38db3091a63ac1cb9590188dc3a972acfb3": " Acciones ",
"4d92a0395dd66778a931460118626c5794a3fc7a": "Agregar Usuarios",
"b0d7dd8a1b0349622d6e0c6e643e24a9ea0efa1d": "Editar Rol"
"b0d7dd8a1b0349622d6e0c6e643e24a9ea0efa1d": "Editar Rol",
"4f389e41e4592f7f9bb76abdd8af4afdfb13f4f1": "Modify playlist",
"28f86ffd419b869711aa13f5e5ff54be6d70731c": "Editar",
"ebadf946ae90f13ecd0c70f09edbc0f983af8a0f": "Sube nuevas cookies",
"98a8a42e5efffe17ab786636ed0139b4c7032d0e": "Arrastrar y soltar",
"85e0725c870b28458fd3bbba905397d890f00a69": "NOTA: Cargar nuevas cookies anulará sus cookies anteriores. También tenga en cuenta que las cookies son de toda la instancia, no por usuario.",
"d01715b75228878a773ae6d059acc639d4898a03": "Anulación de descarga segura",
"00e274c496b094a019f0679c3fab3945793f3335": "Seleccione un nivel de registrador",
"431e5f3a0dde88768d1074baedd65266412b3f02": "Utilizar Cookies",
"80651a7ad1229ea6613557d3559f702cfa5aecf5": "Establecer Cookies",
"eb3d5aefff38a814b76da74371cbf02c0789a1ef": "Registros",
"c76a955642714b8949ff3e4b4990864a2e2cac95": "Solo audio",
"f432e1a8d6adb12e612127978ce2e0ced933959c": "Estos se agregan después de los argumentos estándar.",
"98b6ec9ec138186d663e64770267b67334353d63": "Salida de archivo personalizada",
"fd59fb984749fcdb5e386ae85faec82f8e5ac098": "Los registros aparecerán aquí",
"5009630cdf32ab4f1c78737b9617b8773512c05a": "Líneas:"
}

View File

@@ -14,5 +14,5 @@
<link rel="stylesheet" href="styles.5112d6db78cf21541598.css"></head>
<body>
<app-root></app-root>
<script src="runtime-es2015.1f02852de81190376ba1.js" type="module"></script><script src="runtime-es5.1f02852de81190376ba1.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:"6bc1f7cd24dfb6add92c"}[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:"6bc1f7cd24dfb6add92c"}[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,

View File

@@ -1,6 +0,0 @@
{
"playlists": {
"audio": [],
"video": []
}
}

View File

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

@@ -1,6 +1,6 @@
{
"name": "youtube-dl-material",
"version": "4.0.0",
"version": "4.1.0",
"license": "MIT",
"scripts": {
"ng": "ng",

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

@@ -21,7 +21,6 @@ import { Router, NavigationStart, NavigationEnd } from '@angular/router';
import { OverlayContainer } from '@angular/cdk/overlay';
import { THEMES_CONFIG } from '../themes';
import { SettingsComponent } from './settings/settings.component';
import { CheckOrSetPinDialogComponent } from './dialogs/check-or-set-pin-dialog/check-or-set-pin-dialog.component';
import { AboutDialogComponent } from './dialogs/about-dialog/about-dialog.component';
import { UserProfileDialogComponent } from './dialogs/user-profile-dialog/user-profile-dialog.component';
import { SetDefaultAdminDialogComponent } from './dialogs/set-default-admin-dialog/set-default-admin-dialog.component';
@@ -42,8 +41,6 @@ export class AppComponent implements OnInit {
allowThemeChange = null;
allowSubscriptions = false;
enableDownloadsManager = false;
// defaults to true to prevent attack
settingsPinRequired = true;
@ViewChild('sidenav') sidenav: MatSidenav;
@ViewChild('hamburgerMenu', { read: ElementRef }) hamburgerMenuButton: ElementRef;
@@ -79,7 +76,6 @@ export class AppComponent implements OnInit {
loadConfig() {
// loading config
this.topBarTitle = this.postsService.config['Extra']['title_top'];
this.settingsPinRequired = this.postsService.config['Extra']['settings_pin_required'];
const themingExists = this.postsService.config['Themes'];
this.defaultTheme = themingExists ? this.postsService.config['Themes']['default_theme'] : 'default';
this.allowThemeChange = themingExists ? this.postsService.config['Themes']['allow_theme_change'] : true;
@@ -175,31 +171,11 @@ onSetTheme(theme, old_theme) {
}
openSettingsDialog() {
if (this.settingsPinRequired) {
this.openPinDialog();
} else {
this.actuallyOpenSettingsDialog();
}
}
actuallyOpenSettingsDialog() {
const dialogRef = this.dialog.open(SettingsComponent, {
width: '80vw'
});
}
openPinDialog() {
const dialogRef = this.dialog.open(CheckOrSetPinDialogComponent, {
});
dialogRef.afterClosed().subscribe(res => {
if (res) {
this.actuallyOpenSettingsDialog();
}
});
}
openAboutDialog() {
const dialogRef = this.dialog.open(AboutDialogComponent, {
width: '80vw'

View File

@@ -51,7 +51,6 @@ import { SubscriptionComponent } from './subscription//subscription/subscription
import { SubscriptionFileCardComponent } from './subscription/subscription-file-card/subscription-file-card.component';
import { SubscriptionInfoDialogComponent } from './dialogs/subscription-info-dialog/subscription-info-dialog.component';
import { SettingsComponent } from './settings/settings.component';
import { CheckOrSetPinDialogComponent } from './dialogs/check-or-set-pin-dialog/check-or-set-pin-dialog.component';
import { MatChipsModule } from '@angular/material/chips';
import { NgxFileDropModule } from 'ngx-file-drop';
@@ -73,6 +72,8 @@ import { ManageRoleComponent } from './components/manage-role/manage-role.compon
import { CookiesUploaderDialogComponent } from './dialogs/cookies-uploader-dialog/cookies-uploader-dialog.component';
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');
@@ -95,7 +96,6 @@ export function isVisible({ event, element, scrollContainer, offset }: IsVisible
SubscriptionFileCardComponent,
SubscriptionInfoDialogComponent,
SettingsComponent,
CheckOrSetPinDialogComponent,
AboutDialogComponent,
VideoInfoDialogComponent,
ArgModifierDialogComponent,
@@ -113,7 +113,9 @@ export function isVisible({ event, element, scrollContainer, offset }: IsVisible
ManageRoleComponent,
CookiesUploaderDialogComponent,
LogsViewerComponent,
ModifyPlaylistComponent
ModifyPlaylistComponent,
ConfirmDialogComponent,
EditSubscriptionDialogComponent
],
imports: [
CommonModule,
@@ -167,8 +169,7 @@ export function isVisible({ event, element, scrollContainer, offset }: IsVisible
CreatePlaylistComponent,
SubscribeDialogComponent,
SubscriptionInfoDialogComponent,
SettingsComponent,
CheckOrSetPinDialogComponent
SettingsComponent
],
providers: [
PostsService

View File

@@ -15,7 +15,7 @@
</div>
</div>
<div>
<button style="top: 15px;" (click)="clearDownloads(session_downloads.key)" mat-stroked-button color="warn">Clear all downloads</button>
<button style="top: 15px;" (click)="clearDownloads(session_downloads.key)" mat-stroked-button color="warn"><ng-container i18n="clear all downloads action button">Clear all downloads</ng-container></button>
</div>
</mat-card>
</ng-container>

View File

@@ -1,11 +1,24 @@
<div style="height: 100%">
<div *ngIf="logs_loading" style="position: absolute; top: 40%; left: 50%">
<div style="height: 275px;">
<div *ngIf="logs_loading" style="z-index: 999; position: absolute; top: 40%; left: 50%">
<mat-spinner [diameter]="32"></mat-spinner>
</div>
<textarea style="height: 275px" matInput readonly [(ngModel)]="logs" placeholder="Logs will appear here" i18n-placeholder="Logs placeholder"></textarea>
<!-- Virtual mode (fast, select text buggy) -->
<!--<cdk-virtual-scroll-viewport style="height: 274px;" itemSize="50" class="example-viewport">
<div *cdkVirtualFor="let log of logs; let i = index" class="example-item">
<span [ngStyle]="{'color':log.color}">{{log.text}}</span>
</div>
</cdk-virtual-scroll-viewport>-->
<!-- Non-virtual mode (slow, bug-free) -->
<div style="height: 274px; overflow-y: auto">
<div *ngFor="let log of logs; let i = index" class="example-item">
<span [ngStyle]="{'color':log.color}">{{log.text}}</span>
</div>
</div>
<div>
<button style="margin-top: 12px;" [cdkCopyToClipboard]="logs" (click)="copiedLogsToClipboard()" mat-flat-button color="primary"><ng-container i18n="Copy to clipboard button text">Copy to clipboard</ng-container></button>
<div style="float: right">
<button style="position: absolute; right: 0px; top: 12px;" [cdkCopyToClipboard]="logs_text" (click)="copiedLogsToClipboard()" mat-mini-fab color="primary"><mat-icon style="font-size: 22px !important;">content_copy</mat-icon></button>
<div style="display: inline-block;">
<ng-container i18n="Label for lines select in logger view">Lines:</ng-container>&nbsp;
<mat-form-field style="width: 75px;">
<mat-select (selectionChange)="getLogs()" [(ngModel)]="requested_lines">
@@ -17,5 +30,8 @@
</mat-select>
</mat-form-field>
</div>
<span class="spacer"></span>
<button style="float: right; margin-top: 12px;" (click)="clearLogs()" mat-stroked-button color="warn"><ng-container i18n="Clear logs button">Clear logs</ng-container></button>
</div>
</div>

View File

@@ -0,0 +1 @@
.spacer {flex: 1 1 auto;}

View File

@@ -1,5 +1,7 @@
import { Component, OnInit, AfterViewInit } from '@angular/core';
import { Component, OnInit } from '@angular/core';
import { PostsService } from '../../posts.services';
import { MatDialog } from '@angular/material/dialog';
import { ConfirmDialogComponent } from 'app/dialogs/confirm-dialog/confirm-dialog.component';
@Component({
selector: 'app-logs-viewer',
@@ -8,10 +10,11 @@ import { PostsService } from '../../posts.services';
})
export class LogsViewerComponent implements OnInit {
logs: string = null;
logs: any = null;
logs_text: string = null;
requested_lines = 50;
logs_loading = false;
constructor(private postsService: PostsService) { }
constructor(private postsService: PostsService, private dialog: MatDialog) { }
ngOnInit(): void {
this.getLogs();
@@ -21,8 +24,24 @@ export class LogsViewerComponent implements OnInit {
if (!this.logs) { this.logs_loading = true; } // only show loading spinner at the first load
this.postsService.getLogs(this.requested_lines !== 0 ? this.requested_lines : null).subscribe(res => {
this.logs_loading = false;
if (res['logs']) {
this.logs = res['logs'];
if (res['logs'] !== null || res['logs'] !== undefined) {
this.logs_text = res['logs'];
this.logs = [];
const logs_arr = res['logs'].split('\n');
logs_arr.forEach(log_line => {
let color = 'inherit'
if (log_line.includes('ERROR')) {
color = 'red';
} else if (log_line.includes('WARN')) {
color = 'yellow';
} else if (log_line.includes('VERBOSE')) {
color = 'gray';
}
this.logs.push({
text: log_line,
color: color
})
});
} else {
this.postsService.openSnackBar('Failed to retrieve logs!');
}
@@ -37,4 +56,30 @@ export class LogsViewerComponent implements OnInit {
this.postsService.openSnackBar('Logs copied to clipboard!');
}
clearLogs() {
const dialogRef = this.dialog.open(ConfirmDialogComponent, {
data: {
dialogTitle: 'Clear logs',
dialogText: 'Would you like to clear your logs? This will delete all your current logs, permanently.',
submitText: 'Clear'
}
});
dialogRef.afterClosed().subscribe(confirmed => {
if (confirmed) {
this.postsService.clearAllLogs().subscribe(res => {
if (res['success']) {
this.logs = [];
this.logs_text = '';
this.getLogs();
this.postsService.openSnackBar('Logs successfully cleared!');
} else {
this.postsService.openSnackBar('Failed to clear logs!');
}
}, err => {
this.postsService.openSnackBar('Failed to clear logs!');
});
}
});
}
}

View File

@@ -16,7 +16,7 @@
<h3 matLine>{{permissionToLabel[permission] ? permissionToLabel[permission] : permission}}</h3>
<span matLine>
<mat-radio-group [disabled]="permission === 'settings' && postsService.user.uid === user.uid" (change)="changeUserPermissions($event, permission)" [(ngModel)]="permissions[permission]" [attr.aria-label]="'Give user permission for ' + permission">
<mat-radio-button value="default"><ng-container i18n="Use default">Use default</ng-container></mat-radio-button>
<mat-radio-button value="default"><ng-container i18n="Use role default">Use role default</ng-container></mat-radio-button>
<mat-radio-button value="yes"><ng-container i18n="Yes">Yes</ng-container></mat-radio-button>
<mat-radio-button value="no"><ng-container i18n="No">No</ng-container></mat-radio-button>
</mat-radio-group>
@@ -27,5 +27,5 @@
</mat-dialog-content>
<mat-dialog-actions>
<button mat-button mat-dialog-close><ng-container i18n="Close">Close</ng-container></button>
<button style="margin-bottom: 5px;" mat-stroked-button mat-dialog-close><ng-container i18n="Close">Close</ng-container></button>
</mat-dialog-actions>

View File

@@ -4,7 +4,7 @@
<div class="table table-responsive px-5 pb-4 pt-2">
<div class="example-header">
<mat-form-field>
<input matInput (keyup)="applyFilter($event.target.value)" placeholder="Search">
<input matInput (keyup)="applyFilter($event.target.value)" placeholder="Search" i18n-placeholder="search field description">
</mat-form-field>
</div>
@@ -55,22 +55,22 @@
<mat-header-cell *matHeaderCellDef mat-sort-header><ng-container i18n="Actions users table header"> Actions </ng-container></mat-header-cell>
<mat-cell *matCellDef="let row">
<span *ngIf="editObject && editObject.uid === row.uid; else notediting">
<button mat-icon-button color="primary" (click)="finishEditing(row.uid)" matTooltip="Finish editing user">
<button mat-icon-button color="primary" (click)="finishEditing(row.uid)" matTooltip="Save" i18n-matTooltip="save user edit action button tooltip">
<mat-icon>done</mat-icon>
</button>
<button mat-icon-button (click)="disableEditMode()" matTooltip="Cancel editing user">
<button mat-icon-button (click)="disableEditMode()" matTooltip="Cancel" i18n-matTooltip="cancel user edit action button tooltip">
<mat-icon>cancel</mat-icon>
</button>
</span>
<ng-template #notediting>
<button mat-icon-button (click)="enableEditMode(row.uid)" matTooltip="Edit user">
<button mat-icon-button (click)="enableEditMode(row.uid)" matTooltip="Edit user" i18n-matTooltip="edit user action button tooltip">
<mat-icon>edit</mat-icon>
</button>
</ng-template>
<button (click)="manageUser(row.uid)" mat-icon-button [disabled]="editObject && editObject.uid === row.uid" matTooltip="Manage user">
<button (click)="manageUser(row.uid)" mat-icon-button [disabled]="editObject && editObject.uid === row.uid" matTooltip="Manage user" i18n-matTooltip="manage user action button tooltip">
<mat-icon>settings</mat-icon>
</button>
<button mat-icon-button [disabled]="editObject && editObject.uid === row.uid || row.uid === postsService.user.uid" (click)="removeUser(row.uid)" matTooltip="Delete user">
<button mat-icon-button [disabled]="editObject && editObject.uid === row.uid || row.uid === postsService.user.uid" (click)="removeUser(row.uid)" matTooltip="Delete user" i18n-matTooltip="delete user action button tooltip">
<mat-icon>delete</mat-icon>
</button>
</mat-cell>

View File

@@ -1 +1 @@
export const CURRENT_VERSION = 'v4.0';
export const CURRENT_VERSION = 'v4.1';

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>
@@ -28,5 +28,5 @@
</mat-dialog-content>
<mat-dialog-actions>
<button style="margin-bottom: 5px;" mat-stroked-button mat-dialog-close>Close</button>
</mat-dialog-actions>
<button style="margin-bottom: 5px;" mat-stroked-button mat-dialog-close><ng-container i18n="Close">Close</ng-container></button>
</mat-dialog-actions>

View File

@@ -1,18 +0,0 @@
<h4 *ngIf="pinSetChecked" mat-dialog-title>{{dialog_title}}</h4>
<mat-dialog-content>
<div style="position: relative">
<div *ngIf="pinSetChecked">
<mat-form-field color="accent">
<input type="password" (keyup.enter)="doAction()" matInput [(ngModel)]="input" [placeholder]="input_placeholder">
</mat-form-field>
</div>
<div class="spinner-div" *ngIf="!pinSetChecked">
<mat-spinner [diameter]="25"></mat-spinner>
</div>
</div>
</mat-dialog-content>
<mat-dialog-actions>
<button [disabled]="input.length === 0" color="accent" style="margin-bottom: 12px;" (click)="doAction()" mat-raised-button>{{button_label}}</button>
</mat-dialog-actions>

View File

@@ -1,6 +0,0 @@
.spinner-div {
position: absolute;
margin: 0 auto;
top: 30%;
left: 42%;
}

View File

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

View File

@@ -1,96 +0,0 @@
import { Component, OnInit, Inject } from '@angular/core';
import { PostsService } from 'app/posts.services';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { MatSnackBar } from '@angular/material/snack-bar';
@Component({
selector: 'app-check-or-set-pin-dialog',
templateUrl: './check-or-set-pin-dialog.component.html',
styleUrls: ['./check-or-set-pin-dialog.component.scss']
})
export class CheckOrSetPinDialogComponent implements OnInit {
pinSetChecked = false;
pinSet = true;
resetMode = false;
dialog_title = '';
input_placeholder = null;
input = '';
button_label = '';
constructor(private postsService: PostsService, @Inject(MAT_DIALOG_DATA) public data: any,
public dialogRef: MatDialogRef<CheckOrSetPinDialogComponent>, private snackBar: MatSnackBar) { }
ngOnInit() {
if (this.data) {
this.resetMode = this.data.resetMode;
}
if (this.resetMode) {
this.pinSetChecked = true;
this.notSetLogic();
} else {
this.isPinSet();
}
}
isPinSet() {
this.postsService.isPinSet().subscribe(res => {
this.pinSetChecked = true;
if (res['is_set']) {
this.isSetLogic();
} else {
this.notSetLogic();
}
});
}
isSetLogic() {
this.pinSet = true;
this.dialog_title = 'Pin Required';
this.input_placeholder = 'Pin';
this.button_label = 'Submit'
}
notSetLogic() {
this.pinSet = false;
this.dialog_title = 'Set your pin';
this.input_placeholder = 'New pin';
this.button_label = 'Set Pin'
}
doAction() {
// pin set must have been checked, and input must not be empty
if (!this.pinSetChecked || this.input.length === 0) {
return;
}
if (this.pinSet) {
this.postsService.checkPin(this.input).subscribe(res => {
if (res['success']) {
this.dialogRef.close(true);
} else {
this.dialogRef.close(false);
this.openSnackBar('Pin is incorrect!');
}
});
} else {
this.postsService.setPin(this.input).subscribe(res => {
if (res['success']) {
this.dialogRef.close(true);
this.openSnackBar('Pin successfully set!');
} else {
this.dialogRef.close(false);
this.openSnackBar('Failed to set pin!');
}
});
}
}
public openSnackBar(message: string, action: string = '') {
this.snackBar.open(message, action, {
duration: 2000,
});
}
}

View File

@@ -0,0 +1,15 @@
<h4 mat-dialog-title>{{dialogTitle}}</h4>
<mat-dialog-content>
<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" (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

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

View File

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

View File

@@ -0,0 +1,44 @@
import { Component, OnInit, Inject, EventEmitter } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
@Component({
selector: 'app-confirm-dialog',
templateUrl: './confirm-dialog.component.html',
styleUrls: ['./confirm-dialog.component.scss']
})
export class ConfirmDialogComponent implements OnInit {
dialogTitle = 'Confirm';
dialogText = 'Would you like to confirm?';
submitText = 'Yes'
submitClicked = false;
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

@@ -5,8 +5,8 @@
</mat-grid-tile>
<mat-grid-tile [colspan]="13">
<mat-progress-bar [value]="(download.complete || download.error) ? 100 : download.percent_complete" [mode]="(!download.complete && download.percent_complete === 0 && !download.error) ? 'indeterminate' : 'determinate'"></mat-progress-bar>
<mat-icon *ngIf="download.complete" style="margin-left: 25px; cursor: default" matTooltip="The download is complete" matTooltip-i18n>done</mat-icon>
<mat-icon *ngIf="download.error" style="margin-left: 25px; cursor: default" matTooltip="An error has occurred" matTooltip-i18n>error</mat-icon>
<mat-icon *ngIf="download.complete" style="margin-left: 25px; cursor: default" matTooltip="The download was successful" i18n-matTooltip="download successful tooltip">done</mat-icon>
<mat-icon *ngIf="download.error" style="margin-left: 25px; cursor: default" matTooltip="An error has occurred" i18n-matTooltip="download error tooltip">error</mat-icon>
</mat-grid-tile>
<mat-grid-tile [colspan]="4">
<button style="margin-bottom: 2px;" (click)="cancelTheDownload()" mat-icon-button color="warn"><mat-icon fontSet="material-icons-outlined">cancel</mat-icon></button>

View File

@@ -1,4 +1,4 @@
import { Component, OnInit, HostListener, EventEmitter } from '@angular/core';
import { Component, OnInit, HostListener, EventEmitter, OnDestroy } from '@angular/core';
import { VgAPI } from 'ngx-videogular';
import { PostsService } from 'app/posts.services';
import { ActivatedRoute, Router } from '@angular/router';
@@ -20,7 +20,7 @@ export interface IMedia {
templateUrl: './player.component.html',
styleUrls: ['./player.component.css']
})
export class PlayerComponent implements OnInit {
export class PlayerComponent implements OnInit, OnDestroy {
playlist: Array<IMedia> = [];
original_playlist: string = null;
@@ -62,6 +62,7 @@ export class PlayerComponent implements OnInit {
downloading = false;
save_volume_timer = null;
original_volume = null;
@HostListener('window:resize', ['$event'])
@@ -94,6 +95,11 @@ export class PlayerComponent implements OnInit {
}
}
ngOnDestroy() {
// prevents volume save feature from running in the background
clearInterval(this.save_volume_timer);
}
constructor(public postsService: PostsService, private route: ActivatedRoute, private dialog: MatDialog, private router: Router,
public snackBar: MatSnackBar) {
@@ -241,7 +247,8 @@ export class PlayerComponent implements OnInit {
if (localStorage.getItem('player_volume')) {
this.api.volume = parseFloat(localStorage.getItem('player_volume'));
}
setInterval(() => this.saveVolume(this.api), 2000)
this.save_volume_timer = setInterval(() => this.saveVolume(this.api), 2000)
this.api.getDefaultMedia().subscriptions.loadedMetadata.subscribe(this.playVideo.bind(this));
this.api.getDefaultMedia().subscriptions.ended.subscribe(this.nextVideo.bind(this));

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() {
@@ -238,16 +235,8 @@ export class PostsService implements CanActivate {
return this.http.post(this.path + 'logs', {lines: lines}, this.httpOptions);
}
isPinSet() {
return this.http.post(this.path + 'isPinSet', {}, this.httpOptions);
}
setPin(unhashed_pin) {
return this.http.post(this.path + 'setPin', {pin: unhashed_pin}, this.httpOptions);
}
checkPin(unhashed_pin) {
return this.http.post(this.path + 'checkPin', {input_pin: unhashed_pin}, this.httpOptions);
clearAllLogs() {
return this.http.post(this.path + 'clearAllLogs', {}, this.httpOptions);
}
generateNewAPIKey() {
@@ -293,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)
}
@@ -415,7 +408,6 @@ export class PostsService implements CanActivate {
}
sendToLogin() {
this.checkAdminCreationStatus();
if (!this.initialized) {
this.setInitialized();
}
@@ -445,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>Note: This setting only applies to downloads on the Home page. If you would like to use youtube-dl archive functionality in subscriptions, head down to the Subscriptions section.</p>
<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>
@@ -186,10 +190,6 @@
<div class="col-12">
<mat-checkbox color="accent" [(ngModel)]="new_config['Extra']['allow_multi_download_mode']"><ng-container i18n="Allow multi-download mode setting">Allow multi-download mode</ng-container></mat-checkbox>
</div>
<div class="col-12">
<mat-checkbox [disabled]="new_config['Advanced']['multi_user_mode']" color="accent" [(ngModel)]="new_config['Extra']['settings_pin_required']"><ng-container i18n="Require pin for settings setting">Require pin for settings</ng-container></mat-checkbox>
<button style="margin-left: 15px; margin-bottom: 10px;" mat-stroked-button (click)="setNewPin()" [disabled]="!new_config['Extra']['settings_pin_required']"><ng-container i18n="Set new pin button">Set New Pin</ng-container></button>
</div>
</div>
</div>
<mat-divider></mat-divider>
@@ -206,7 +206,7 @@
</mat-form-field>
</div>
<div class="api-key-div">
<button matTooltip-i18n matTooltip="This will delete your old API key!" mat-stroked-button (click)="generateAPIKey()"><ng-container i18n="Generate key button">Generate</ng-container></button>
<button matTooltip="This will delete your old API key!" i18n-matTooltip="delete api key tooltip" mat-stroked-button (click)="generateAPIKey()"><ng-container i18n="Generate key button">Generate</ng-container></button>
</div>
</div>
</div>

View File

@@ -1,6 +1,5 @@
import { Component, OnInit } from '@angular/core';
import { Component, OnInit, EventEmitter } from '@angular/core';
import { PostsService } from 'app/posts.services';
import { CheckOrSetPinDialogComponent } from 'app/dialogs/check-or-set-pin-dialog/check-or-set-pin-dialog.component';
import { isoLangs } from './locales_list';
import { MatSnackBar } from '@angular/material/snack-bar';
import {DomSanitizer} from '@angular/platform-browser';
@@ -9,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',
@@ -77,14 +77,6 @@ export class SettingsComponent implements OnInit {
})
}
setNewPin() {
const dialogRef = this.dialog.open(CheckOrSetPinDialogComponent, {
data: {
resetMode: true
}
});
}
generateAPIKey() {
this.postsService.generateNewAPIKey().subscribe(res => {
if (res['new_api_key']) {
@@ -163,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
}
});
}
}

View File

@@ -183,7 +183,7 @@
"b7ff2e2b909c53abe088fe60b9f4b6ac7757247f": "Nutzer registrieren",
"024886ca34a6f309e3e51c2ed849320592c3faaa": "Benutzername",
"2bd201aea09e43fbfd3cd15ec0499b6755302329": "Benutzer verwalten",
"29c97c8e76763bb15b6d515648fa5bd1eb0f7510": "Benutzer-UID",
"29c97c8e76763bb15b6d515648fa5bd1eb0f7510": "Benutzer-UID:",
"e70e209561583f360b1e9cefd2cbb1fe434b6229": "Neues Passwort",
"6498fa1b8f563988f769654a75411bb8060134b9": "Neues Passwort festlegen",
"40da072004086c9ec00d125165da91eaade7f541": "Standard verwenden",

View File

@@ -1772,7 +1772,7 @@
</trans-unit>
<trans-unit id="29c97c8e76763bb15b6d515648fa5bd1eb0f7510" datatype="html">
<source>User UID:</source>
<target xml:lang="de-DE">Benutzer-UID</target>
<target xml:lang="de-DE">Benutzer-UID:</target>
<note from="description" priority="1">User UID</note><context-group purpose="location">
<context context-type="sourcefile">app/components/manage-user/manage-user.component.html</context>
<context context-type="linenumber">4</context>

View File

@@ -16,6 +16,10 @@
<context context-type="sourcefile">app/create-playlist/create-playlist.component.html</context>
<context context-type="linenumber">5</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">app/dialogs/modify-playlist/modify-playlist.component.html</context>
<context context-type="linenumber">7</context>
</context-group>
<note priority="1" from="description">Playlist name placeholder</note>
</trans-unit>
<trans-unit id="f47e2d56dd8a145b2e9599da9730c049d52962a2" datatype="html">
@@ -102,7 +106,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">app/dialogs/subscribe-dialog/subscribe-dialog.component.html</context>
<context context-type="linenumber">42</context>
<context context-type="linenumber">70</context>
</context-group>
<note priority="1" from="description">Arg modifier cancel button</note>
</trans-unit>
@@ -122,21 +126,13 @@
</context-group>
<note priority="1" from="description">Youtube downloader home page label</note>
</trans-unit>
<trans-unit id="6d2ec8898344c8955542b0542c942038ef28bb80" datatype="html">
<source>Please enter a valid URL!</source>
<context-group purpose="location">
<context context-type="sourcefile">app/main/main.component.html</context>
<context context-type="linenumber">16</context>
</context-group>
<note priority="1" from="description">Enter valid URL error</note>
</trans-unit>
<trans-unit id="a38ae1082fec79ba1f379978337385a539a28e73" datatype="html">
<source>
Quality
</source>
<context-group purpose="location">
<context context-type="sourcefile">app/main/main.component.html</context>
<context context-type="linenumber">24</context>
<context context-type="linenumber">21</context>
</context-group>
<note priority="1" from="description">Quality select label</note>
</trans-unit>
@@ -144,7 +140,7 @@
<source>Use URL</source>
<context-group purpose="location">
<context context-type="sourcefile">app/main/main.component.html</context>
<context context-type="linenumber">52</context>
<context context-type="linenumber">49</context>
</context-group>
<note priority="1" from="description">YT search Use URL button for searched video</note>
</trans-unit>
@@ -154,7 +150,7 @@
</source>
<context-group purpose="location">
<context context-type="sourcefile">app/main/main.component.html</context>
<context context-type="linenumber">55</context>
<context context-type="linenumber">52</context>
</context-group>
<note priority="1" from="description">YT search View button for searched video</note>
</trans-unit>
@@ -164,7 +160,7 @@
</source>
<context-group purpose="location">
<context context-type="sourcefile">app/main/main.component.html</context>
<context context-type="linenumber">65</context>
<context context-type="linenumber">62</context>
</context-group>
<note priority="1" from="description">Only Audio checkbox</note>
</trans-unit>
@@ -174,7 +170,7 @@
</source>
<context-group purpose="location">
<context context-type="sourcefile">app/main/main.component.html</context>
<context context-type="linenumber">70</context>
<context context-type="linenumber">67</context>
</context-group>
<note priority="1" from="description">Multi-download Mode checkbox</note>
</trans-unit>
@@ -184,7 +180,7 @@
</source>
<context-group purpose="location">
<context context-type="sourcefile">app/main/main.component.html</context>
<context context-type="linenumber">79</context>
<context context-type="linenumber">76</context>
</context-group>
<note priority="1" from="description">Main download button</note>
</trans-unit>
@@ -194,7 +190,7 @@
</source>
<context-group purpose="location">
<context context-type="sourcefile">app/main/main.component.html</context>
<context context-type="linenumber">84</context>
<context context-type="linenumber">81</context>
</context-group>
<note priority="1" from="description">Cancel download button</note>
</trans-unit>
@@ -204,7 +200,7 @@
</source>
<context-group purpose="location">
<context context-type="sourcefile">app/main/main.component.html</context>
<context context-type="linenumber">96</context>
<context context-type="linenumber">93</context>
</context-group>
<note priority="1" from="description">Advanced download mode panel</note>
</trans-unit>
@@ -214,7 +210,7 @@
</source>
<context-group purpose="location">
<context context-type="sourcefile">app/main/main.component.html</context>
<context context-type="linenumber">102</context>
<context context-type="linenumber">99</context>
</context-group>
<note priority="1" from="description">Simulated command label</note>
</trans-unit>
@@ -224,7 +220,7 @@
</source>
<context-group purpose="location">
<context context-type="sourcefile">app/main/main.component.html</context>
<context context-type="linenumber">110</context>
<context context-type="linenumber">107</context>
</context-group>
<note priority="1" from="description">Use custom args checkbox</note>
</trans-unit>
@@ -232,12 +228,16 @@
<source>Custom args</source>
<context-group purpose="location">
<context context-type="sourcefile">app/main/main.component.html</context>
<context context-type="linenumber">116</context>
<context context-type="linenumber">113</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">app/settings/settings.component.html</context>
<context context-type="linenumber">145</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">app/dialogs/subscribe-dialog/subscribe-dialog.component.html</context>
<context context-type="linenumber">48</context>
</context-group>
<note priority="1" from="description">Custom args placeholder</note>
</trans-unit>
<trans-unit id="a6911c2157f1b775284bbe9654ce5eb30cf45d7f" datatype="html">
@@ -246,7 +246,7 @@
</source>
<context-group purpose="location">
<context context-type="sourcefile">app/main/main.component.html</context>
<context context-type="linenumber">118</context>
<context context-type="linenumber">115</context>
</context-group>
<note priority="1" from="description">Custom Args input hint</note>
</trans-unit>
@@ -256,7 +256,7 @@
</source>
<context-group purpose="location">
<context context-type="sourcefile">app/main/main.component.html</context>
<context context-type="linenumber">126</context>
<context context-type="linenumber">123</context>
</context-group>
<note priority="1" from="description">Use custom output checkbox</note>
</trans-unit>
@@ -264,7 +264,7 @@
<source>Custom output</source>
<context-group purpose="location">
<context context-type="sourcefile">app/main/main.component.html</context>
<context context-type="linenumber">131</context>
<context context-type="linenumber">128</context>
</context-group>
<note priority="1" from="description">Custom output placeholder</note>
</trans-unit>
@@ -272,7 +272,11 @@
<source>Documentation</source>
<context-group purpose="location">
<context context-type="sourcefile">app/main/main.component.html</context>
<context context-type="linenumber">133</context>
<context context-type="linenumber">130</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">app/dialogs/subscribe-dialog/subscribe-dialog.component.html</context>
<context context-type="linenumber">60</context>
</context-group>
<note priority="1" from="description">Youtube-dl output template documentation link</note>
</trans-unit>
@@ -280,7 +284,11 @@
<source>Path is relative to the config download path. Don&apos;t include extension.</source>
<context-group purpose="location">
<context context-type="sourcefile">app/main/main.component.html</context>
<context context-type="linenumber">134</context>
<context context-type="linenumber">131</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">app/dialogs/subscribe-dialog/subscribe-dialog.component.html</context>
<context context-type="linenumber">61</context>
</context-group>
<note priority="1" from="description">Custom Output input hint</note>
</trans-unit>
@@ -290,7 +298,7 @@
</source>
<context-group purpose="location">
<context context-type="sourcefile">app/main/main.component.html</context>
<context context-type="linenumber">140</context>
<context context-type="linenumber">137</context>
</context-group>
<note priority="1" from="description">Use authentication checkbox</note>
</trans-unit>
@@ -298,7 +306,7 @@
<source>Username</source>
<context-group purpose="location">
<context context-type="sourcefile">app/main/main.component.html</context>
<context context-type="linenumber">145</context>
<context context-type="linenumber">142</context>
</context-group>
<note priority="1" from="description">YT Username placeholder</note>
</trans-unit>
@@ -306,7 +314,7 @@
<source>Password</source>
<context-group purpose="location">
<context context-type="sourcefile">app/main/main.component.html</context>
<context context-type="linenumber">150</context>
<context context-type="linenumber">147</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">app/dialogs/set-default-admin-dialog/set-default-admin-dialog.component.html</context>
@@ -324,7 +332,7 @@
</source>
<context-group purpose="location">
<context context-type="sourcefile">app/main/main.component.html</context>
<context context-type="linenumber">194</context>
<context context-type="linenumber">191</context>
</context-group>
<note priority="1" from="description">Audio files title</note>
</trans-unit>
@@ -334,7 +342,7 @@
</source>
<context-group purpose="location">
<context context-type="sourcefile">app/main/main.component.html</context>
<context context-type="linenumber">199</context>
<context context-type="linenumber">196</context>
</context-group>
<note priority="1" from="description">Audio files description</note>
</trans-unit>
@@ -342,11 +350,11 @@
<source>Playlists</source>
<context-group purpose="location">
<context context-type="sourcefile">app/main/main.component.html</context>
<context context-type="linenumber">214</context>
<context context-type="linenumber">211</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">app/main/main.component.html</context>
<context context-type="linenumber">256</context>
<context context-type="linenumber">253</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">app/subscriptions/subscriptions.component.html</context>
@@ -360,7 +368,7 @@
</source>
<context-group purpose="location">
<context context-type="sourcefile">app/main/main.component.html</context>
<context context-type="linenumber">225</context>
<context context-type="linenumber">222</context>
</context-group>
<note priority="1" from="description">No video playlists available text</note>
</trans-unit>
@@ -370,7 +378,7 @@
</source>
<context-group purpose="location">
<context context-type="sourcefile">app/main/main.component.html</context>
<context context-type="linenumber">235</context>
<context context-type="linenumber">232</context>
</context-group>
<note priority="1" from="description">Video files title</note>
</trans-unit>
@@ -380,7 +388,7 @@
</source>
<context-group purpose="location">
<context context-type="sourcefile">app/main/main.component.html</context>
<context context-type="linenumber">240</context>
<context context-type="linenumber">237</context>
</context-group>
<note priority="1" from="description">Video files description</note>
</trans-unit>
@@ -390,7 +398,7 @@
</source>
<context-group purpose="location">
<context context-type="sourcefile">app/main/main.component.html</context>
<context context-type="linenumber">269</context>
<context context-type="linenumber">266</context>
</context-group>
<note priority="1" from="description">No video playlists available text</note>
</trans-unit>
@@ -456,6 +464,10 @@
<context context-type="sourcefile">app/dialogs/video-info-dialog/video-info-dialog.component.html</context>
<context context-type="linenumber">31</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">app/dialogs/cookies-uploader-dialog/cookies-uploader-dialog.component.html</context>
<context context-type="linenumber">40</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">app/dialogs/user-profile-dialog/user-profile-dialog.component.html</context>
<context context-type="linenumber">27</context>
@@ -486,6 +498,14 @@
</context-group>
<note priority="1" from="description">Close subscription info button</note>
</trans-unit>
<trans-unit id="4f389e41e4592f7f9bb76abdd8af4afdfb13f4f1" datatype="html">
<source>Modify playlist</source>
<context-group purpose="location">
<context context-type="sourcefile">app/dialogs/modify-playlist/modify-playlist.component.html</context>
<context context-type="linenumber">1</context>
</context-group>
<note priority="1" from="description">Modify playlist dialog title</note>
</trans-unit>
<trans-unit id="ca3dbbc7f3e011bffe32a10a3ea45cc84f30ecf1" datatype="html">
<source>ID:</source>
<context-group purpose="location">
@@ -510,11 +530,31 @@
</context-group>
<note priority="1" from="description">Playlist video count</note>
</trans-unit>
<trans-unit id="28f86ffd419b869711aa13f5e5ff54be6d70731c" datatype="html">
<source>Edit</source>
<context-group purpose="location">
<context context-type="sourcefile">app/file-card/file-card.component.html</context>
<context context-type="linenumber">19</context>
</context-group>
<note priority="1" from="description">Playlist edit button</note>
</trans-unit>
<trans-unit id="826b25211922a1b46436589233cb6f1a163d89b7" datatype="html">
<source>Delete</source>
<context-group purpose="location">
<context context-type="sourcefile">app/file-card/file-card.component.html</context>
<context context-type="linenumber">20</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">app/file-card/file-card.component.html</context>
<context context-type="linenumber">25</context>
</context-group>
<note priority="1" from="description">Delete playlist</note>
</trans-unit>
<trans-unit id="321e4419a943044e674beb55b8039f42a9761ca5" datatype="html">
<source>Info</source>
<context-group purpose="location">
<context context-type="sourcefile">app/file-card/file-card.component.html</context>
<context context-type="linenumber">20</context>
<context context-type="linenumber">24</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">app/subscription/subscription-file-card/subscription-file-card.component.html</context>
@@ -522,22 +562,38 @@
</context-group>
<note priority="1" from="description">Video info button</note>
</trans-unit>
<trans-unit id="826b25211922a1b46436589233cb6f1a163d89b7" datatype="html">
<source>Delete</source>
<context-group purpose="location">
<context context-type="sourcefile">app/file-card/file-card.component.html</context>
<context context-type="linenumber">21</context>
</context-group>
<note priority="1" from="description">Delete video button</note>
</trans-unit>
<trans-unit id="34504b488c24c27e68089be549f0eeae6ebaf30b" datatype="html">
<source>Delete and blacklist</source>
<context-group purpose="location">
<context context-type="sourcefile">app/file-card/file-card.component.html</context>
<context context-type="linenumber">22</context>
<context context-type="linenumber">26</context>
</context-group>
<note priority="1" from="description">Delete and blacklist video button</note>
</trans-unit>
<trans-unit id="ebadf946ae90f13ecd0c70f09edbc0f983af8a0f" datatype="html">
<source>Upload new cookies</source>
<context-group purpose="location">
<context context-type="sourcefile">app/dialogs/cookies-uploader-dialog/cookies-uploader-dialog.component.html</context>
<context context-type="linenumber">1</context>
</context-group>
<note priority="1" from="description">Cookies uploader dialog title</note>
</trans-unit>
<trans-unit id="98a8a42e5efffe17ab786636ed0139b4c7032d0e" datatype="html">
<source>Drag and Drop</source>
<context-group purpose="location">
<context context-type="sourcefile">app/dialogs/cookies-uploader-dialog/cookies-uploader-dialog.component.html</context>
<context context-type="linenumber">11</context>
</context-group>
<note priority="1" from="description">Drag and Drop</note>
</trans-unit>
<trans-unit id="85e0725c870b28458fd3bbba905397d890f00a69" datatype="html">
<source>NOTE: Uploading new cookies will overrride your previous cookies. Also note that cookies are instance-wide, not per-user.</source>
<context-group purpose="location">
<context context-type="sourcefile">app/dialogs/cookies-uploader-dialog/cookies-uploader-dialog.component.html</context>
<context context-type="linenumber">20</context>
</context-group>
<note priority="1" from="description">Cookies upload warning</note>
</trans-unit>
<trans-unit id="121cc5391cd2a5115bc2b3160379ee5b36cd7716" datatype="html">
<source>Settings</source>
<context-group purpose="location">
@@ -802,6 +858,14 @@
</context-group>
<note priority="1" from="description">Custom args setting input hint</note>
</trans-unit>
<trans-unit id="d01715b75228878a773ae6d059acc639d4898a03" datatype="html">
<source>Safe download override</source>
<context-group purpose="location">
<context context-type="sourcefile">app/settings/settings.component.html</context>
<context context-type="linenumber">157</context>
</context-group>
<note priority="1" from="description">Safe download override setting</note>
</trans-unit>
<trans-unit id="0ba25ad86a240576c4f20a2fada4722ebba77b1e" datatype="html">
<source>Downloader</source>
<context-group purpose="location">
@@ -814,7 +878,7 @@
<source>Top title</source>
<context-group purpose="location">
<context context-type="sourcefile">app/settings/settings.component.html</context>
<context context-type="linenumber">166</context>
<context context-type="linenumber">170</context>
</context-group>
<note priority="1" from="description">Top title input placeholder</note>
</trans-unit>
@@ -822,7 +886,7 @@
<source>File manager enabled</source>
<context-group purpose="location">
<context context-type="sourcefile">app/settings/settings.component.html</context>
<context context-type="linenumber">171</context>
<context context-type="linenumber">175</context>
</context-group>
<note priority="1" from="description">File manager enabled setting</note>
</trans-unit>
@@ -830,7 +894,7 @@
<source>Downloads manager enabled</source>
<context-group purpose="location">
<context context-type="sourcefile">app/settings/settings.component.html</context>
<context context-type="linenumber">174</context>
<context context-type="linenumber">178</context>
</context-group>
<note priority="1" from="description">Downloads manager enabled setting</note>
</trans-unit>
@@ -838,7 +902,7 @@
<source>Allow quality select</source>
<context-group purpose="location">
<context context-type="sourcefile">app/settings/settings.component.html</context>
<context context-type="linenumber">177</context>
<context context-type="linenumber">181</context>
</context-group>
<note priority="1" from="description">Allow quality seelct setting</note>
</trans-unit>
@@ -846,7 +910,7 @@
<source>Download only mode</source>
<context-group purpose="location">
<context context-type="sourcefile">app/settings/settings.component.html</context>
<context context-type="linenumber">180</context>
<context context-type="linenumber">184</context>
</context-group>
<note priority="1" from="description">Download only mode setting</note>
</trans-unit>
@@ -854,7 +918,7 @@
<source>Allow multi-download mode</source>
<context-group purpose="location">
<context context-type="sourcefile">app/settings/settings.component.html</context>
<context context-type="linenumber">183</context>
<context context-type="linenumber">187</context>
</context-group>
<note priority="1" from="description">Allow multi-download mode setting</note>
</trans-unit>
@@ -862,7 +926,7 @@
<source>Require pin for settings</source>
<context-group purpose="location">
<context context-type="sourcefile">app/settings/settings.component.html</context>
<context context-type="linenumber">186</context>
<context context-type="linenumber">190</context>
</context-group>
<note priority="1" from="description">Require pin for settings setting</note>
</trans-unit>
@@ -870,7 +934,7 @@
<source>Set New Pin</source>
<context-group purpose="location">
<context context-type="sourcefile">app/settings/settings.component.html</context>
<context context-type="linenumber">187</context>
<context context-type="linenumber">191</context>
</context-group>
<note priority="1" from="description">Set new pin button</note>
</trans-unit>
@@ -878,7 +942,7 @@
<source>Enable Public API</source>
<context-group purpose="location">
<context context-type="sourcefile">app/settings/settings.component.html</context>
<context context-type="linenumber">195</context>
<context context-type="linenumber">199</context>
</context-group>
<note priority="1" from="description">Enable Public API key setting</note>
</trans-unit>
@@ -886,7 +950,7 @@
<source>Public API Key</source>
<context-group purpose="location">
<context context-type="sourcefile">app/settings/settings.component.html</context>
<context context-type="linenumber">200</context>
<context context-type="linenumber">204</context>
</context-group>
<note priority="1" from="description">Public API Key setting placeholder</note>
</trans-unit>
@@ -894,7 +958,7 @@
<source>View documentation</source>
<context-group purpose="location">
<context context-type="sourcefile">app/settings/settings.component.html</context>
<context context-type="linenumber">201</context>
<context context-type="linenumber">205</context>
</context-group>
<note priority="1" from="description">View API docs setting hint</note>
</trans-unit>
@@ -902,7 +966,7 @@
<source>Generate</source>
<context-group purpose="location">
<context context-type="sourcefile">app/settings/settings.component.html</context>
<context context-type="linenumber">205</context>
<context context-type="linenumber">209</context>
</context-group>
<note priority="1" from="description">Generate key button</note>
</trans-unit>
@@ -910,7 +974,7 @@
<source>Use YouTube API</source>
<context-group purpose="location">
<context context-type="sourcefile">app/settings/settings.component.html</context>
<context context-type="linenumber">214</context>
<context context-type="linenumber">218</context>
</context-group>
<note priority="1" from="description">Use YouTube API setting</note>
</trans-unit>
@@ -918,7 +982,7 @@
<source>Youtube API Key</source>
<context-group purpose="location">
<context context-type="sourcefile">app/settings/settings.component.html</context>
<context context-type="linenumber">218</context>
<context context-type="linenumber">222</context>
</context-group>
<note priority="1" from="description">Youtube API Key setting placeholder</note>
</trans-unit>
@@ -926,7 +990,7 @@
<source>Generating a key is easy!</source>
<context-group purpose="location">
<context context-type="sourcefile">app/settings/settings.component.html</context>
<context context-type="linenumber">219</context>
<context context-type="linenumber">223</context>
</context-group>
<note priority="1" from="description">Youtube API Key setting hint</note>
</trans-unit>
@@ -934,11 +998,11 @@
<source>Click here</source>
<context-group purpose="location">
<context context-type="sourcefile">app/settings/settings.component.html</context>
<context context-type="linenumber">229</context>
<context context-type="linenumber">233</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">app/settings/settings.component.html</context>
<context context-type="linenumber">235</context>
<context context-type="linenumber">239</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">app/dialogs/about-dialog/about-dialog.component.html</context>
@@ -950,7 +1014,7 @@
<source>to download the official YoutubeDL-Material Chrome extension manually.</source>
<context-group purpose="location">
<context context-type="sourcefile">app/settings/settings.component.html</context>
<context context-type="linenumber">229</context>
<context context-type="linenumber">233</context>
</context-group>
<note priority="1" from="description">Chrome click here suffix</note>
</trans-unit>
@@ -958,7 +1022,7 @@
<source>You must manually load the extension and modify the extension&apos;s settings to set the frontend URL.</source>
<context-group purpose="location">
<context context-type="sourcefile">app/settings/settings.component.html</context>
<context context-type="linenumber">230</context>
<context context-type="linenumber">234</context>
</context-group>
<note priority="1" from="description">Chrome setup suffix</note>
</trans-unit>
@@ -966,7 +1030,7 @@
<source>to install the official YoutubeDL-Material Firefox extension right off the Firefox extensions page.</source>
<context-group purpose="location">
<context context-type="sourcefile">app/settings/settings.component.html</context>
<context context-type="linenumber">235</context>
<context context-type="linenumber">239</context>
</context-group>
<note priority="1" from="description">Firefox click here suffix</note>
</trans-unit>
@@ -974,7 +1038,7 @@
<source>Detailed setup instructions.</source>
<context-group purpose="location">
<context context-type="sourcefile">app/settings/settings.component.html</context>
<context context-type="linenumber">236</context>
<context context-type="linenumber">240</context>
</context-group>
<note priority="1" from="description">Firefox setup prefix link</note>
</trans-unit>
@@ -982,7 +1046,7 @@
<source>Not much is required other than changing the extension&apos;s settings to set the frontend URL.</source>
<context-group purpose="location">
<context context-type="sourcefile">app/settings/settings.component.html</context>
<context context-type="linenumber">236</context>
<context context-type="linenumber">240</context>
</context-group>
<note priority="1" from="description">Firefox setup suffix</note>
</trans-unit>
@@ -990,7 +1054,7 @@
<source>Drag the link below to your bookmarks, and you&apos;re good to go! Just navigate to the YouTube video you&apos;d like to download, and click the bookmark.</source>
<context-group purpose="location">
<context context-type="sourcefile">app/settings/settings.component.html</context>
<context context-type="linenumber">241</context>
<context context-type="linenumber">245</context>
</context-group>
<note priority="1" from="description">Bookmarklet instructions</note>
</trans-unit>
@@ -998,7 +1062,7 @@
<source>Generate &apos;audio only&apos; bookmarklet</source>
<context-group purpose="location">
<context context-type="sourcefile">app/settings/settings.component.html</context>
<context context-type="linenumber">242</context>
<context context-type="linenumber">246</context>
</context-group>
<note priority="1" from="description">Generate audio only bookmarklet checkbox</note>
</trans-unit>
@@ -1006,7 +1070,7 @@
<source>Extra</source>
<context-group purpose="location">
<context context-type="sourcefile">app/settings/settings.component.html</context>
<context context-type="linenumber">160</context>
<context context-type="linenumber">164</context>
</context-group>
<note priority="1" from="description">Extra settings label</note>
</trans-unit>
@@ -1014,7 +1078,7 @@
<source>Use default downloading agent</source>
<context-group purpose="location">
<context context-type="sourcefile">app/settings/settings.component.html</context>
<context context-type="linenumber">256</context>
<context context-type="linenumber">260</context>
</context-group>
<note priority="1" from="description">Use default downloading agent setting</note>
</trans-unit>
@@ -1022,27 +1086,47 @@
<source>Select a downloader</source>
<context-group purpose="location">
<context context-type="sourcefile">app/settings/settings.component.html</context>
<context context-type="linenumber">260</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">app/settings/settings.component.html</context>
<context context-type="linenumber">274</context>
<context context-type="linenumber">264</context>
</context-group>
<note priority="1" from="description">Custom downloader select label</note>
</trans-unit>
<trans-unit id="00e274c496b094a019f0679c3fab3945793f3335" datatype="html">
<source>Select a logger level</source>
<context-group purpose="location">
<context context-type="sourcefile">app/settings/settings.component.html</context>
<context context-type="linenumber">278</context>
</context-group>
<note priority="1" from="description">Logger level select label</note>
</trans-unit>
<trans-unit id="dc3d990391c944d1fbfc7cfb402f7b5e112fb3a8" datatype="html">
<source>Allow advanced download</source>
<context-group purpose="location">
<context context-type="sourcefile">app/settings/settings.component.html</context>
<context context-type="linenumber">285</context>
<context context-type="linenumber">289</context>
</context-group>
<note priority="1" from="description">Allow advanced downloading setting</note>
</trans-unit>
<trans-unit id="431e5f3a0dde88768d1074baedd65266412b3f02" datatype="html">
<source>Use Cookies</source>
<context-group purpose="location">
<context context-type="sourcefile">app/settings/settings.component.html</context>
<context context-type="linenumber">297</context>
</context-group>
<note priority="1" from="description">Use cookies setting</note>
</trans-unit>
<trans-unit id="80651a7ad1229ea6613557d3559f702cfa5aecf5" datatype="html">
<source>Set Cookies</source>
<context-group purpose="location">
<context context-type="sourcefile">app/settings/settings.component.html</context>
<context context-type="linenumber">298</context>
</context-group>
<note priority="1" from="description">Set cookies button</note>
</trans-unit>
<trans-unit id="bc2e854e111ecf2bd7db170da5e3c2ed08181d88" datatype="html">
<source>Advanced</source>
<context-group purpose="location">
<context context-type="sourcefile">app/settings/settings.component.html</context>
<context context-type="linenumber">251</context>
<context context-type="linenumber">255</context>
</context-group>
<note priority="1" from="description">Host settings label</note>
</trans-unit>
@@ -1050,7 +1134,7 @@
<source>Allow user registration</source>
<context-group purpose="location">
<context context-type="sourcefile">app/settings/settings.component.html</context>
<context context-type="linenumber">297</context>
<context context-type="linenumber">310</context>
</context-group>
<note priority="1" from="description">Allow registration setting</note>
</trans-unit>
@@ -1058,15 +1142,23 @@
<source>Users</source>
<context-group purpose="location">
<context context-type="sourcefile">app/settings/settings.component.html</context>
<context context-type="linenumber">295</context>
<context context-type="linenumber">308</context>
</context-group>
<note priority="1" from="description">Users settings label</note>
</trans-unit>
<trans-unit id="eb3d5aefff38a814b76da74371cbf02c0789a1ef" datatype="html">
<source>Logs</source>
<context-group purpose="location">
<context context-type="sourcefile">app/settings/settings.component.html</context>
<context context-type="linenumber">314</context>
</context-group>
<note priority="1" from="description">Logs settings label</note>
</trans-unit>
<trans-unit id="52c9a103b812f258bcddc3d90a6e3f46871d25fe" datatype="html">
<source>Save</source>
<context-group purpose="location">
<context context-type="sourcefile">app/settings/settings.component.html</context>
<context context-type="linenumber">307</context>
<context context-type="linenumber">327</context>
</context-group>
<note priority="1" from="description">Settings save button</note>
</trans-unit>
@@ -1074,7 +1166,7 @@
<source>{VAR_SELECT, select, true {Close} false {Cancel} other {otha} }</source>
<context-group purpose="location">
<context context-type="sourcefile">app/settings/settings.component.html</context>
<context context-type="linenumber">310</context>
<context context-type="linenumber">330</context>
</context-group>
<note priority="1" from="description">Settings cancel and close button</note>
</trans-unit>
@@ -1188,6 +1280,10 @@
<context context-type="sourcefile">app/dialogs/user-profile-dialog/user-profile-dialog.component.html</context>
<context context-type="linenumber">20</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">app/app.component.html</context>
<context context-type="linenumber">44</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">app/components/login/login.component.html</context>
<context context-type="linenumber">15</context>
@@ -1254,7 +1350,7 @@
<source>Subscriptions</source>
<context-group purpose="location">
<context context-type="sourcefile">app/app.component.html</context>
<context context-type="linenumber">44</context>
<context context-type="linenumber">45</context>
</context-group>
<note priority="1" from="description">Navigation menu Subscriptions Page title</note>
</trans-unit>
@@ -1262,7 +1358,7 @@
<source>Downloads</source>
<context-group purpose="location">
<context context-type="sourcefile">app/app.component.html</context>
<context context-type="linenumber">45</context>
<context context-type="linenumber">46</context>
</context-group>
<note priority="1" from="description">Navigation menu Downloads Page title</note>
</trans-unit>
@@ -1320,6 +1416,10 @@
<context context-type="sourcefile">app/dialogs/share-media-dialog/share-media-dialog.component.html</context>
<context context-type="linenumber">24</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">app/components/logs-viewer/logs-viewer.component.html</context>
<context context-type="linenumber">7</context>
</context-group>
<note priority="1" from="description">Copy to clipboard button</note>
</trans-unit>
<trans-unit id="5b3075e8dc3f3921ec316b0bd83b6d14a06c1a4f" datatype="html">
@@ -1390,23 +1490,15 @@
<source>Custom name</source>
<context-group purpose="location">
<context context-type="sourcefile">app/dialogs/subscribe-dialog/subscribe-dialog.component.html</context>
<context context-type="linenumber">14</context>
<context context-type="linenumber">19</context>
</context-group>
<note priority="1" from="description">Subscription custom name placeholder</note>
</trans-unit>
<trans-unit id="f3f62aa84d59f3a8b900cc9a7eec3ef279a7b4e7" datatype="html">
<source>This is optional</source>
<context-group purpose="location">
<context context-type="sourcefile">app/dialogs/subscribe-dialog/subscribe-dialog.component.html</context>
<context context-type="linenumber">15</context>
</context-group>
<note priority="1" from="description">Custom name input hint</note>
</trans-unit>
<trans-unit id="ea30873bd3f0d5e4fb2378eec3f0a1db77634a28" datatype="html">
<source>Download all uploads</source>
<context-group purpose="location">
<context context-type="sourcefile">app/dialogs/subscribe-dialog/subscribe-dialog.component.html</context>
<context context-type="linenumber">19</context>
<context context-type="linenumber">23</context>
</context-group>
<note priority="1" from="description">Download all uploads subscription setting</note>
</trans-unit>
@@ -1414,23 +1506,47 @@
<source>Download videos uploaded in the last</source>
<context-group purpose="location">
<context context-type="sourcefile">app/dialogs/subscribe-dialog/subscribe-dialog.component.html</context>
<context context-type="linenumber">22</context>
<context context-type="linenumber">26</context>
</context-group>
<note priority="1" from="description">Download time range prefix</note>
</trans-unit>
<trans-unit id="c76a955642714b8949ff3e4b4990864a2e2cac95" datatype="html">
<source>Audio-only mode</source>
<context-group purpose="location">
<context context-type="sourcefile">app/dialogs/subscribe-dialog/subscribe-dialog.component.html</context>
<context context-type="linenumber">38</context>
</context-group>
<note priority="1" from="description">Streaming-only mode</note>
</trans-unit>
<trans-unit id="408ca4911457e84a348cecf214f02c69289aa8f1" datatype="html">
<source>Streaming-only mode</source>
<context-group purpose="location">
<context context-type="sourcefile">app/dialogs/subscribe-dialog/subscribe-dialog.component.html</context>
<context context-type="linenumber">34</context>
<context context-type="linenumber">43</context>
</context-group>
<note priority="1" from="description">Streaming-only mode</note>
</trans-unit>
<trans-unit id="f432e1a8d6adb12e612127978ce2e0ced933959c" datatype="html">
<source>These are added after the standard args.</source>
<context-group purpose="location">
<context context-type="sourcefile">app/dialogs/subscribe-dialog/subscribe-dialog.component.html</context>
<context context-type="linenumber">51</context>
</context-group>
<note priority="1" from="description">Custom args hint</note>
</trans-unit>
<trans-unit id="98b6ec9ec138186d663e64770267b67334353d63" datatype="html">
<source>Custom file output</source>
<context-group purpose="location">
<context context-type="sourcefile">app/dialogs/subscribe-dialog/subscribe-dialog.component.html</context>
<context context-type="linenumber">57</context>
</context-group>
<note priority="1" from="description">Subscription custom file output placeholder</note>
</trans-unit>
<trans-unit id="d0336848b0c375a1c25ba369b3481ee383217a4f" datatype="html">
<source>Subscribe</source>
<context-group purpose="location">
<context context-type="sourcefile">app/dialogs/subscribe-dialog/subscribe-dialog.component.html</context>
<context context-type="linenumber">44</context>
<context context-type="linenumber">72</context>
</context-group>
<note priority="1" from="description">Subscribe button</note>
</trans-unit>
@@ -1594,7 +1710,7 @@
<source>No downloads available!</source>
<context-group purpose="location">
<context context-type="sourcefile">app/components/downloads/downloads.component.html</context>
<context context-type="linenumber">22</context>
<context context-type="linenumber">25</context>
</context-group>
<note priority="1" from="description">No downloads label</note>
</trans-unit>
@@ -1726,6 +1842,22 @@
</context-group>
<note priority="1" from="description">Edit role</note>
</trans-unit>
<trans-unit id="fd59fb984749fcdb5e386ae85faec82f8e5ac098" datatype="html">
<source>Logs will appear here</source>
<context-group purpose="location">
<context context-type="sourcefile">app/components/logs-viewer/logs-viewer.component.html</context>
<context context-type="linenumber">5</context>
</context-group>
<note priority="1" from="description">Logs placeholder</note>
</trans-unit>
<trans-unit id="5009630cdf32ab4f1c78737b9617b8773512c05a" datatype="html">
<source>Lines:</source>
<context-group purpose="location">
<context context-type="sourcefile">app/components/logs-viewer/logs-viewer.component.html</context>
<context context-type="linenumber">9</context>
</context-group>
<note priority="1" from="description">Label for lines select in logger view</note>
</trans-unit>
</body>
</file>
</xliff>

View File

@@ -196,5 +196,20 @@
"52c1447c1ec9570a2a3025c7e566557b8d19ed92": " Rol ",
"59a8c38db3091a63ac1cb9590188dc3a972acfb3": " Acciones ",
"4d92a0395dd66778a931460118626c5794a3fc7a": "Agregar Usuarios",
"b0d7dd8a1b0349622d6e0c6e643e24a9ea0efa1d": "Editar Rol"
"b0d7dd8a1b0349622d6e0c6e643e24a9ea0efa1d": "Editar Rol",
"4f389e41e4592f7f9bb76abdd8af4afdfb13f4f1": "Modify playlist",
"28f86ffd419b869711aa13f5e5ff54be6d70731c": "Editar",
"ebadf946ae90f13ecd0c70f09edbc0f983af8a0f": "Sube nuevas cookies",
"98a8a42e5efffe17ab786636ed0139b4c7032d0e": "Arrastrar y soltar",
"85e0725c870b28458fd3bbba905397d890f00a69": "NOTA: Cargar nuevas cookies anulará sus cookies anteriores. También tenga en cuenta que las cookies son de toda la instancia, no por usuario.",
"d01715b75228878a773ae6d059acc639d4898a03": "Anulación de descarga segura",
"00e274c496b094a019f0679c3fab3945793f3335": "Seleccione un nivel de registrador",
"431e5f3a0dde88768d1074baedd65266412b3f02": "Utilizar Cookies",
"80651a7ad1229ea6613557d3559f702cfa5aecf5": "Establecer Cookies",
"eb3d5aefff38a814b76da74371cbf02c0789a1ef": "Registros",
"c76a955642714b8949ff3e4b4990864a2e2cac95": "Solo audio",
"f432e1a8d6adb12e612127978ce2e0ced933959c": "Estos se agregan después de los argumentos estándar.",
"98b6ec9ec138186d663e64770267b67334353d63": "Salida de archivo personalizada",
"fd59fb984749fcdb5e386ae85faec82f8e5ac098": "Los registros aparecerán aquí",
"5009630cdf32ab4f1c78737b9617b8773512c05a": "Líneas:"
}