mirror of
https://github.com/Tzahi12345/YoutubeDL-Material.git
synced 2026-03-07 20:10:03 +03:00
Compare commits
138 Commits
v4.1
...
fix-playli
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
004a234b02 | ||
|
|
daca715d1b | ||
|
|
8fdc231f08 | ||
|
|
ae8f7a2a33 | ||
|
|
d0782bb444 | ||
|
|
49210abb49 | ||
|
|
851bfb81ba | ||
|
|
35d0d439fa | ||
|
|
ded3ad6dfc | ||
|
|
61daf26641 | ||
|
|
95e53b9549 | ||
|
|
46ed0fe992 | ||
|
|
082252ab1e | ||
|
|
5eccaa13e5 | ||
|
|
71633950b2 | ||
|
|
f31dad0215 | ||
|
|
7efbe40bb2 | ||
|
|
5b768b5bda | ||
|
|
365cbc3ffa | ||
|
|
44647f3306 | ||
|
|
8a7409478a | ||
|
|
70159813e5 | ||
|
|
babba9aa30 | ||
|
|
d292275956 | ||
|
|
ba2acedb94 | ||
|
|
aa0558b770 | ||
|
|
d7f04fc90a | ||
|
|
087c9f1bb1 | ||
|
|
f874617965 | ||
|
|
8fb8543829 | ||
|
|
70d89d310c | ||
|
|
c48aaaf13c | ||
|
|
6cf7ea193a | ||
|
|
c9016f446d | ||
|
|
919e2a649a | ||
|
|
dc6dd5f5a2 | ||
|
|
2e4ef3b224 | ||
|
|
0e35b2ca1b | ||
|
|
9937bc9bb6 | ||
|
|
178a61c381 | ||
|
|
b455d8a900 | ||
|
|
72fa439569 | ||
|
|
33b1affa73 | ||
|
|
da73e47f08 | ||
|
|
3faf715b88 | ||
|
|
eda75e9a19 | ||
|
|
560aaadca1 | ||
|
|
eb7fdb649d | ||
|
|
533dd49ed0 | ||
|
|
001d907c3a | ||
|
|
4302625858 | ||
|
|
e2cec9321e | ||
|
|
2add31af6d | ||
|
|
5de37f6fbf | ||
|
|
7aace85ef4 | ||
|
|
736c3f5cab | ||
|
|
c3c7667c17 | ||
|
|
52bee8b280 | ||
|
|
945ba268fb | ||
|
|
d49a67dfd0 | ||
|
|
96c52f2d5b | ||
|
|
61c03b6681 | ||
|
|
7c349163b6 | ||
|
|
a0acdd1d86 | ||
|
|
25bfb9e518 | ||
|
|
c9b615c659 | ||
|
|
c4f21dc1cc | ||
|
|
e678f4b476 | ||
|
|
356f31343e | ||
|
|
38c46b5be5 | ||
|
|
12dcdfcb3c | ||
|
|
47c19c0cdc | ||
|
|
d0eff42f2a | ||
|
|
4472aae3e9 | ||
|
|
c55d3de9a0 | ||
|
|
1cdc1640ac | ||
|
|
fcaf8b5a62 | ||
|
|
8c7b2dfc79 | ||
|
|
59ad74ed79 | ||
|
|
690871f6b2 | ||
|
|
5088ce0291 | ||
|
|
576e2109d7 | ||
|
|
c6c80579df | ||
|
|
0ab6535fec | ||
|
|
d7aa39599d | ||
|
|
ee169cd7ce | ||
|
|
835790e69c | ||
|
|
68037613d8 | ||
|
|
3df384de22 | ||
|
|
f0c4ed4590 | ||
|
|
fd35153721 | ||
|
|
d0aed8f144 | ||
|
|
d17b68d76e | ||
|
|
a481869166 | ||
|
|
eb4ed32fcb | ||
|
|
9aee6e91cd | ||
|
|
37d3e9326c | ||
|
|
dbf8f9ebfd | ||
|
|
7858e26b15 | ||
|
|
057ad67672 | ||
|
|
3ebc903ce9 | ||
|
|
21c5795f1c | ||
|
|
f12ea017bc | ||
|
|
cd18bce509 | ||
|
|
5ef4388d73 | ||
|
|
a78c0cb56c | ||
|
|
ae9e4e6857 | ||
|
|
333556c305 | ||
|
|
c5b0a7a697 | ||
|
|
d7631360cc | ||
|
|
c800308b9d | ||
|
|
c1c57135ba | ||
|
|
8384b73c4c | ||
|
|
834ac00694 | ||
|
|
935ae3452c | ||
|
|
cc189a3abd | ||
|
|
335b588c3a | ||
|
|
493abc3a4c | ||
|
|
238abc1686 | ||
|
|
fbfad6c3e2 | ||
|
|
a6ff65f004 | ||
|
|
89cd969fcb | ||
|
|
4ebb2d4297 | ||
|
|
d371ccf094 | ||
|
|
e7181b57c7 | ||
|
|
38fa39d765 | ||
|
|
9e5de88675 | ||
|
|
9cf4949c30 | ||
|
|
dd80c51f16 | ||
|
|
84d83f228e | ||
|
|
14bd82c508 | ||
|
|
0b6606cafb | ||
|
|
e97e9ec717 | ||
|
|
990b3d4037 | ||
|
|
2e7b1c2d53 | ||
|
|
a4eb7fb745 | ||
|
|
2442067ca0 | ||
|
|
41d4dfeba1 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -65,3 +65,4 @@ backend/appdata/logs/error.log
|
||||
backend/appdata/users.json
|
||||
backend/users/*
|
||||
backend/appdata/cookies.txt
|
||||
backend/public
|
||||
43
Dockerfile
Normal file
43
Dockerfile
Normal file
@@ -0,0 +1,43 @@
|
||||
FROM alpine:3.12 as frontend
|
||||
|
||||
RUN apk add --no-cache \
|
||||
npm
|
||||
|
||||
RUN npm install -g @angular/cli
|
||||
|
||||
WORKDIR /build
|
||||
COPY [ "package.json", "package-lock.json", "/build/" ]
|
||||
RUN npm install
|
||||
|
||||
COPY [ "angular.json", "tsconfig.json", "/build/" ]
|
||||
COPY [ "src/", "/build/src/" ]
|
||||
RUN ng build --prod
|
||||
|
||||
#--------------#
|
||||
|
||||
FROM alpine:3.12
|
||||
|
||||
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 [ "backend/package.json", "backend/package-lock.json", "/app/" ]
|
||||
RUN npm install && chown -R $UID:$GID ./
|
||||
|
||||
COPY --chown=$UID:$GID --from=frontend [ "/build/backend/public/", "/app/public/" ]
|
||||
COPY --chown=$UID:$GID [ "/backend/", "/app/" ]
|
||||
|
||||
EXPOSE 17442
|
||||
ENTRYPOINT [ "/app/entrypoint.sh" ]
|
||||
CMD [ "node", "app.js" ]
|
||||
64
README.md
64
README.md
@@ -43,7 +43,7 @@ Optional dependencies:
|
||||
|
||||
1. First, download the [latest release](https://github.com/Tzahi12345/YoutubeDL-Material/releases/latest)!
|
||||
|
||||
2. Drag the `youtubedl-material` directory to an easily accessible directory. Navigate to the `appdata` folder and edit the `default.json` file. If you're using SSL encryption, look at the `encrypted.json` file for a template.
|
||||
2. Drag the `youtubedl-material` directory to an easily accessible directory. Navigate to the `appdata` folder and edit the `default.json` file.
|
||||
|
||||
NOTE: If you are intending to use a [reverse proxy](https://github.com/Tzahi12345/YoutubeDL-Material/wiki/Reverse-Proxy-Setup), this next step is not necessary
|
||||
|
||||
@@ -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,18 +69,28 @@ 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
|
||||
|
||||
[API Docs](https://stoplight.io/p/docs/gh/tzahi12345/youtubedl-material?group=master&utm_campaign=publish_dialog&utm_source=studio)
|
||||
[API Docs](https://youtubedl-material.stoplight.io/docs/youtubedl-material/Public%20API%20v1.yaml)
|
||||
|
||||
To get started, go to the settings menu and enable the public API from the *Extra* tab. You can generate an API key if one is missing.
|
||||
|
||||
@@ -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
|
||||
|
||||
49
armhf.Dockerfile
Normal file
49
armhf.Dockerfile
Normal file
@@ -0,0 +1,49 @@
|
||||
FROM alpine:3.12 as frontend
|
||||
|
||||
RUN apk add --no-cache \
|
||||
npm \
|
||||
curl
|
||||
|
||||
RUN npm install -g @angular/cli
|
||||
|
||||
WORKDIR /build
|
||||
|
||||
RUN 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 .
|
||||
|
||||
COPY [ "package.json", "package-lock.json", "/build/" ]
|
||||
RUN npm install
|
||||
|
||||
COPY [ "angular.json", "tsconfig.json", "/build/" ]
|
||||
COPY [ "src/", "/build/src/" ]
|
||||
RUN ng build --prod
|
||||
|
||||
#--------------#
|
||||
|
||||
FROM arm32v7/alpine:3.12
|
||||
|
||||
COPY --from=frontend /build/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 [ "backend/package.json", "backend/package-lock.json", "/app/" ]
|
||||
RUN npm install && chown -R $UID:$GID ./
|
||||
|
||||
COPY --chown=$UID:$GID --from=frontend [ "/build/backend/public/", "/app/public/" ]
|
||||
COPY --chown=$UID:$GID [ "/backend/", "/app/" ]
|
||||
|
||||
EXPOSE 17442
|
||||
ENTRYPOINT [ "/app/entrypoint.sh" ]
|
||||
CMD [ "node", "app.js" ]
|
||||
@@ -1,27 +0,0 @@
|
||||
FROM alpine:3.12
|
||||
|
||||
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" ]
|
||||
577
backend/app.js
577
backend/app.js
@@ -7,7 +7,7 @@ var path = require('path');
|
||||
var youtubedl = require('youtube-dl');
|
||||
var ffmpeg = require('fluent-ffmpeg');
|
||||
var compression = require('compression');
|
||||
var https = require('https');
|
||||
var glob = require("glob")
|
||||
var multer = require('multer');
|
||||
var express = require("express");
|
||||
var bodyParser = require("body-parser");
|
||||
@@ -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();
|
||||
|
||||
@@ -128,7 +128,6 @@ users_db.defaults(
|
||||
var frontendUrl = null;
|
||||
var backendUrl = null;
|
||||
var backendPort = null;
|
||||
var usingEncryption = null;
|
||||
var basePath = null;
|
||||
var audioFolderPath = null;
|
||||
var videoFolderPath = null;
|
||||
@@ -140,7 +139,6 @@ var subscriptionsCheckInterval = null;
|
||||
var archivePath = path.join(__dirname, 'appdata', 'archives');
|
||||
|
||||
// other needed values
|
||||
var options = null; // encryption options
|
||||
var url_domain = null;
|
||||
var updaterStatus = null;
|
||||
|
||||
@@ -256,18 +254,10 @@ async function startServer() {
|
||||
// set config to port
|
||||
await setPortItemFromENV();
|
||||
}
|
||||
if (usingEncryption)
|
||||
{
|
||||
https.createServer(options, app).listen(backendPort, function() {
|
||||
logger.info(`YoutubeDL-Material ${CONSTS['CURRENT_VERSION']} started on port ${backendPort} - using SSL`);
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
app.listen(backendPort,function(){
|
||||
logger.info(`YoutubeDL-Material ${CONSTS['CURRENT_VERSION']} started on PORT ${backendPort}`);
|
||||
});
|
||||
}
|
||||
|
||||
app.listen(backendPort,function(){
|
||||
logger.info(`YoutubeDL-Material ${CONSTS['CURRENT_VERSION']} started on PORT ${backendPort}`);
|
||||
});
|
||||
}
|
||||
|
||||
async function restartServer() {
|
||||
@@ -504,6 +494,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());
|
||||
@@ -548,6 +575,8 @@ async function loadConfig() {
|
||||
}, subscriptionsCheckInterval * 1000);
|
||||
}
|
||||
|
||||
db_api.importUnregisteredFiles();
|
||||
|
||||
// check migrations
|
||||
await checkMigrations();
|
||||
|
||||
@@ -565,7 +594,6 @@ async function loadConfig() {
|
||||
function loadConfigValues() {
|
||||
url = !debugMode ? config_api.getConfigItem('ytdl_url') : 'http://localhost:4200';
|
||||
backendPort = config_api.getConfigItem('ytdl_port');
|
||||
usingEncryption = config_api.getConfigItem('ytdl_use_encryption');
|
||||
audioFolderPath = config_api.getConfigItem('ytdl_audio_folder_path');
|
||||
videoFolderPath = config_api.getConfigItem('ytdl_video_folder_path');
|
||||
downloadOnlyMode = config_api.getConfigItem('ytdl_download_only_mode');
|
||||
@@ -580,20 +608,6 @@ function loadConfigValues() {
|
||||
customDownloadingAgent = null;
|
||||
}
|
||||
|
||||
if (usingEncryption)
|
||||
{
|
||||
var certFilePath = path.resolve(config_api.getConfigItem('ytdl_cert_file_path'));
|
||||
var keyFilePath = path.resolve(config_api.getConfigItem('ytdl_key_file_path'));
|
||||
|
||||
var certKeyFile = fs.readFileSync(keyFilePath);
|
||||
var certFile = fs.readFileSync(certFilePath);
|
||||
|
||||
options = {
|
||||
key: certKeyFile,
|
||||
cert: certFile
|
||||
};
|
||||
}
|
||||
|
||||
// empty url defaults to default URL
|
||||
if (!url || url === '') url = 'http://example.com'
|
||||
url_domain = new URL(url);
|
||||
@@ -693,7 +707,7 @@ function generateEnvVarConfigItem(key) {
|
||||
|
||||
function getMp3s() {
|
||||
let mp3s = [];
|
||||
var files = recFindByExt(audioFolderPath, 'mp3'); // fs.readdirSync(audioFolderPath);
|
||||
var files = utils.recFindByExt(audioFolderPath, 'mp3'); // fs.readdirSync(audioFolderPath);
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
let file = files[i];
|
||||
var file_path = file.substring(audioFolderPath.length, file.length);
|
||||
@@ -722,7 +736,7 @@ function getMp3s() {
|
||||
|
||||
function getMp4s(relative_path = true) {
|
||||
let mp4s = [];
|
||||
var files = recFindByExt(videoFolderPath, 'mp4');
|
||||
var files = utils.recFindByExt(videoFolderPath, 'mp4');
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
let file = files[i];
|
||||
var file_path = file.substring(videoFolderPath.length, file.length);
|
||||
@@ -881,14 +895,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 +944,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 +980,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 +1023,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;
|
||||
|
||||
@@ -1037,88 +1055,6 @@ async function deleteVideoFile(name, customPath = null, blacklistMode = false) {
|
||||
});
|
||||
}
|
||||
|
||||
function recFindByExt(base,ext,files,result)
|
||||
{
|
||||
files = files || fs.readdirSync(base)
|
||||
result = result || []
|
||||
|
||||
files.forEach(
|
||||
function (file) {
|
||||
var newbase = path.join(base,file)
|
||||
if ( fs.statSync(newbase).isDirectory() )
|
||||
{
|
||||
result = recFindByExt(newbase,ext,fs.readdirSync(newbase),result)
|
||||
}
|
||||
else
|
||||
{
|
||||
if ( file.substr(-1*(ext.length+1)) == '.' + ext )
|
||||
{
|
||||
result.push(newbase)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
return result
|
||||
}
|
||||
/*
|
||||
function registerFileDB(file_path, type, multiUserMode = null) {
|
||||
const file_id = file_path.substring(0, file_path.length-4);
|
||||
const file_object = generateFileObject(file_id, type, multiUserMode && multiUserMode.file_path);
|
||||
if (!file_object) {
|
||||
logger.error(`Could not find associated JSON file for ${type} file ${file_id}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// add additional info
|
||||
file_object['uid'] = uuid();
|
||||
file_object['registered'] = Date.now();
|
||||
path_object = path.parse(file_object['path']);
|
||||
file_object['path'] = path.format(path_object);
|
||||
|
||||
if (multiUserMode) {
|
||||
auth_api.registerUserFile(multiUserMode.user, file_object, type);
|
||||
} else if (type === 'audio' || type === 'video') {
|
||||
// remove existing video if overwriting
|
||||
db.get(`files.${type}`)
|
||||
.remove({
|
||||
path: file_object['path']
|
||||
}).write();
|
||||
|
||||
db.get(`files.${type}`)
|
||||
.push(file_object)
|
||||
.write();
|
||||
} else if (type == 'subscription') {
|
||||
|
||||
}
|
||||
|
||||
return file_object['uid'];
|
||||
}
|
||||
|
||||
function generateFileObject(id, type, customPath = null) {
|
||||
var jsonobj = (type === 'audio') ? utils.getJSONMp3(id, customPath, true) : utils.getJSONMp4(id, customPath, true);
|
||||
if (!jsonobj) {
|
||||
return null;
|
||||
}
|
||||
const ext = (type === 'audio') ? '.mp3' : '.mp4'
|
||||
const file_path = getTrueFileName(jsonobj['_filename'], type); // path.join(type === 'audio' ? audioFolderPath : videoFolderPath, id + ext);
|
||||
// console.
|
||||
var stats = fs.statSync(path.join(__dirname, file_path));
|
||||
|
||||
var title = jsonobj.title;
|
||||
var url = jsonobj.webpage_url;
|
||||
var uploader = jsonobj.uploader;
|
||||
var upload_date = jsonobj.upload_date;
|
||||
upload_date = upload_date ? `${upload_date.substring(0, 4)}-${upload_date.substring(4, 6)}-${upload_date.substring(6, 8)}` : 'N/A';
|
||||
|
||||
var size = stats.size;
|
||||
|
||||
var thumbnail = jsonobj.thumbnail;
|
||||
var duration = jsonobj.duration;
|
||||
var isaudio = type === 'audio';
|
||||
var file_obj = new utils.File(id, title, thumbnail, isaudio, duration, url, uploader, size, file_path, upload_date);
|
||||
return file_obj;
|
||||
}
|
||||
*/
|
||||
// replaces .webm with appropriate extension
|
||||
function getTrueFileName(unfixed_path, type) {
|
||||
let fixed_path = unfixed_path;
|
||||
@@ -1194,6 +1130,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
|
||||
@@ -1209,12 +1146,34 @@ async function downloadFileByURL_exec(url, type, options, sessionID = null) {
|
||||
type: type,
|
||||
percent_complete: 0,
|
||||
is_playlist: url.includes('playlist'),
|
||||
timestamp_start: Date.now()
|
||||
timestamp_start: Date.now(),
|
||||
filesize: null
|
||||
};
|
||||
const download = downloads[session][download_uid];
|
||||
updateDownloads();
|
||||
|
||||
// get video info prior to download
|
||||
const info = await getVideoInfoByURL(url, downloadConfig, download);
|
||||
if (!info) {
|
||||
resolve(false);
|
||||
return;
|
||||
} else {
|
||||
// store info in download for future use
|
||||
if (Array.isArray(info)) {
|
||||
download['fileNames'] = [];
|
||||
for (let info_obj of info) download['fileNames'].push(info_obj['_filename']);
|
||||
} else {
|
||||
download['_filename'] = info['_filename'];
|
||||
}
|
||||
download['filesize'] = utils.getExpectedFileSize(info);
|
||||
}
|
||||
|
||||
const download_checker = setInterval(() => checkDownloadPercent(download), 1000);
|
||||
|
||||
// download file
|
||||
youtubedl.exec(url, downloadConfig, {}, function(err, output) {
|
||||
clearInterval(download_checker); // stops the download checker from running as the download finished (or errored)
|
||||
|
||||
download['downloading'] = false;
|
||||
download['timestamp_end'] = Date.now();
|
||||
var file_uid = null;
|
||||
@@ -1227,7 +1186,7 @@ async function downloadFileByURL_exec(url, type, options, sessionID = null) {
|
||||
download['error'] = err.stderr;
|
||||
updateDownloads();
|
||||
resolve(false);
|
||||
throw err;
|
||||
return;
|
||||
} else if (output) {
|
||||
if (output.length === 0 || output[0].length === 0) {
|
||||
download['error'] = 'No output. Check if video already exists in your archive.';
|
||||
@@ -1329,6 +1288,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 +1429,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', 'bestvideo+bestaudio', '--merge-output-format', 'mp4'];
|
||||
const is_youtube = url.includes('youtu');
|
||||
if (!is_audio && !is_youtube) {
|
||||
// tiktok videos fail when using the default format
|
||||
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 +1455,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');
|
||||
@@ -1547,6 +1507,10 @@ async function generateArgs(url, type, options) {
|
||||
downloadConfig.push('--download-archive', merged_path);
|
||||
}
|
||||
|
||||
if (config_api.getConfigItem('ytdl_include_thumbnail')) {
|
||||
downloadConfig.push('--write-thumbnail');
|
||||
}
|
||||
|
||||
if (globalArgs && globalArgs !== '') {
|
||||
// adds global args
|
||||
if (downloadConfig.indexOf('-o') !== -1 && globalArgs.split(',,').indexOf('-o') !== -1) {
|
||||
@@ -1559,11 +1523,36 @@ async function generateArgs(url, type, options) {
|
||||
|
||||
}
|
||||
logger.verbose(`youtube-dl args being used: ${downloadConfig.join(',')}`);
|
||||
// downloadConfig.map((arg) => `"${arg}"`);
|
||||
resolve(downloadConfig);
|
||||
});
|
||||
}
|
||||
|
||||
async function getVideoInfoByURL(url, args = [], download = null) {
|
||||
return new Promise(resolve => {
|
||||
// remove bad args
|
||||
const new_args = [...args];
|
||||
|
||||
const archiveArgIndex = new_args.indexOf('--download-archive');
|
||||
if (archiveArgIndex !== -1) {
|
||||
new_args.splice(archiveArgIndex, 2);
|
||||
}
|
||||
|
||||
// actually get info
|
||||
youtubedl.getInfo(url, new_args, (err, output) => {
|
||||
if (output) {
|
||||
resolve(output);
|
||||
} else {
|
||||
logger.error(`Error while retrieving info on video with URL ${url} with the following message: ${err}`);
|
||||
if (download) {
|
||||
download['error'] = `Failed pre-check for video info: ${err}`;
|
||||
updateDownloads();
|
||||
}
|
||||
resolve(null);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// currently only works for single urls
|
||||
async function getUrlInfos(urls) {
|
||||
let startDate = Date.now();
|
||||
@@ -1621,47 +1610,36 @@ function updateDownloads() {
|
||||
db.assign({downloads: downloads}).write();
|
||||
}
|
||||
|
||||
/*
|
||||
function checkDownloads() {
|
||||
for (let [session_id, session_downloads] of Object.entries(downloads)) {
|
||||
for (let [download_uid, download_obj] of Object.entries(session_downloads)) {
|
||||
if (download_obj && !download_obj['complete'] && !download_obj['error']
|
||||
&& download_obj.timestamp_start > timestamp_server_start) {
|
||||
// download is still running (presumably)
|
||||
download_obj.percent_complete = getDownloadPercent(download_obj);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
function checkDownloadPercent(download) {
|
||||
/*
|
||||
This is more of an art than a science, we're just selecting files that start with the file name,
|
||||
thus capturing the parts being downloaded in files named like so: '<video title>.<format>.<ext>.part'.
|
||||
|
||||
function getDownloadPercent(download_obj) {
|
||||
if (!download_obj.final_size) {
|
||||
if (fs.existsSync(download_obj.expected_json_path)) {
|
||||
const file_json = JSON.parse(fs.readFileSync(download_obj.expected_json_path, 'utf8'));
|
||||
let calculated_filesize = null;
|
||||
if (file_json['format_id']) {
|
||||
calculated_filesize = 0;
|
||||
const formats_used = file_json['format_id'].split('+');
|
||||
for (let i = 0; i < file_json['formats'].length; i++) {
|
||||
if (formats_used.includes(file_json['formats'][i]['format_id'])) {
|
||||
calculated_filesize += file_json['formats'][i]['filesize'];
|
||||
}
|
||||
Any file that starts with <video title> will be counted as part of the "bytes downloaded", which will
|
||||
be divided by the "total expected bytes."
|
||||
*/
|
||||
// assume it's a playlist for logic reasons
|
||||
const fileNames = Array.isArray(download['fileNames']) ? download['fileNames']
|
||||
: [path.format(path.parse(download['_filename'].substring(0, download['_filename'].length-4)))];
|
||||
|
||||
|
||||
const resulting_file_size = download['filesize'];
|
||||
let sum_size = 0;
|
||||
let glob_str = '';
|
||||
glob(`{${fileNames.join(',')}, }*`, (err, files) => {
|
||||
files.forEach(file => {
|
||||
try {
|
||||
const file_stats = fs.statSync(file);
|
||||
if (file_stats && file_stats.size) {
|
||||
sum_size += file_stats.size;
|
||||
}
|
||||
} catch (e) {
|
||||
|
||||
}
|
||||
download_obj.final_size = calculated_filesize;
|
||||
} else {
|
||||
console.log('could not find json file');
|
||||
}
|
||||
}
|
||||
if (fs.existsSync(download_obj.expected_path)) {
|
||||
const stats = fs.statSync(download_obj.expected_path);
|
||||
const size = stats.size;
|
||||
return (size / download_obj.final_size)*100;
|
||||
} else {
|
||||
console.log('could not find file');
|
||||
return 0;
|
||||
}
|
||||
});
|
||||
download['percent_complete'] = (sum_size/resulting_file_size * 100).toFixed(2);
|
||||
updateDownloads();
|
||||
});
|
||||
}
|
||||
|
||||
// youtube-dl functions
|
||||
@@ -1823,7 +1801,8 @@ const optionalJwt = function (req, res, next) {
|
||||
const uuid = using_body ? req.body.uuid : req.query.uuid;
|
||||
const uid = using_body ? req.body.uid : req.query.uid;
|
||||
const type = using_body ? req.body.type : req.query.type;
|
||||
const is_shared = !req.query.id ? auth_api.getUserVideo(uuid, uid, type, true) : auth_api.getUserPlaylist(uuid, req.query.id, null, true);
|
||||
const file = !req.query.id ? auth_api.getUserVideo(uuid, uid, type, true, req.body) : auth_api.getUserPlaylist(uuid, req.query.id, null, true);
|
||||
const is_shared = file ? file['sharingEnabled'] : false;
|
||||
if (is_shared) {
|
||||
req.can_watch = true;
|
||||
return next();
|
||||
@@ -1849,7 +1828,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);
|
||||
@@ -1864,27 +1843,6 @@ app.post('/api/setConfig', function(req, res) {
|
||||
|
||||
});
|
||||
|
||||
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 = {
|
||||
@@ -1903,7 +1861,7 @@ app.post('/api/tomp3', optionalJwt, async function(req, res) {
|
||||
const is_playlist = url.includes('playlist');
|
||||
|
||||
let result_obj = null;
|
||||
if (safeDownloadOverride || is_playlist || options.customQualityConfiguration || options.customArgs || options.maxBitrate)
|
||||
if (true || safeDownloadOverride || is_playlist || options.customQualityConfiguration || options.customArgs || options.maxBitrate)
|
||||
result_obj = await downloadFileByURL_exec(url, 'audio', options, req.query.sessionID);
|
||||
else
|
||||
result_obj = await downloadFileByURL_normal(url, 'audio', options, req.query.sessionID);
|
||||
@@ -1912,11 +1870,10 @@ 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) {
|
||||
req.setTimeout(0); // remove timeout in case of long videos
|
||||
var url = req.body.url;
|
||||
var options = {
|
||||
customArgs: req.body.customArgs,
|
||||
@@ -1934,7 +1891,7 @@ app.post('/api/tomp4', optionalJwt, async function(req, res) {
|
||||
const is_playlist = url.includes('playlist');
|
||||
|
||||
let result_obj = null;
|
||||
if (safeDownloadOverride || is_playlist || options.customQualityConfiguration || options.customArgs || options.selectedHeight || !url.includes('youtu'))
|
||||
if (true || safeDownloadOverride || is_playlist || options.customQualityConfiguration || options.customArgs || options.selectedHeight || !url.includes('youtu'))
|
||||
result_obj = await downloadFileByURL_exec(url, 'video', options, req.query.sessionID);
|
||||
else
|
||||
result_obj = await downloadFileByURL_normal(url, 'video', options, req.query.sessionID);
|
||||
@@ -1943,50 +1900,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
|
||||
@@ -2001,6 +1919,14 @@ app.get('/api/getMp3s', optionalJwt, function(req, res) {
|
||||
playlists = auth_api.getUserPlaylists(req.user.uid, 'audio');
|
||||
}
|
||||
|
||||
mp3s = JSON.parse(JSON.stringify(mp3s));
|
||||
|
||||
// add thumbnails if present
|
||||
mp3s.forEach(mp3 => {
|
||||
if (mp3['thumbnailPath'] && fs.existsSync(mp3['thumbnailPath']))
|
||||
mp3['thumbnailBlob'] = fs.readFileSync(mp3['thumbnailPath']);
|
||||
});
|
||||
|
||||
res.send({
|
||||
mp3s: mp3s,
|
||||
playlists: playlists
|
||||
@@ -2020,6 +1946,14 @@ app.get('/api/getMp4s', optionalJwt, function(req, res) {
|
||||
playlists = auth_api.getUserPlaylists(req.user.uid, 'video');
|
||||
}
|
||||
|
||||
mp4s = JSON.parse(JSON.stringify(mp4s));
|
||||
|
||||
// add thumbnails if present
|
||||
mp4s.forEach(mp4 => {
|
||||
if (mp4['thumbnailPath'] && fs.existsSync(mp4['thumbnailPath']))
|
||||
mp4['thumbnailBlob'] = fs.readFileSync(mp4['thumbnailPath']);
|
||||
});
|
||||
|
||||
res.send({
|
||||
mp4s: mp4s,
|
||||
playlists: playlists
|
||||
@@ -2064,6 +1998,60 @@ app.post('/api/getFile', optionalJwt, function (req, res) {
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/getAllFiles', optionalJwt, function (req, res) {
|
||||
// these are returned
|
||||
let files = [];
|
||||
let playlists = [];
|
||||
let subscription_files = [];
|
||||
|
||||
let videos = null;
|
||||
let audios = null;
|
||||
let audio_playlists = null;
|
||||
let video_playlists = null;
|
||||
let subscriptions = subscriptions_api.getAllSubscriptions(req.isAuthenticated() ? req.user.uid : null);
|
||||
|
||||
// get basic info depending on multi-user mode being enabled
|
||||
if (req.isAuthenticated()) {
|
||||
videos = auth_api.getUserVideos(req.user.uid, 'video');
|
||||
audios = auth_api.getUserVideos(req.user.uid, 'audio');
|
||||
audio_playlists = auth_api.getUserPlaylists(req.user.uid, 'audio');
|
||||
video_playlists = auth_api.getUserPlaylists(req.user.uid, 'video');
|
||||
} else {
|
||||
videos = db.get('files.audio').value();
|
||||
audios = db.get('files.video').value();
|
||||
audio_playlists = db.get('playlists.audio').value();
|
||||
video_playlists = db.get('playlists.video').value();
|
||||
}
|
||||
|
||||
files = videos.concat(audios);
|
||||
playlists = video_playlists.concat(audio_playlists);
|
||||
|
||||
// loop through subscriptions and add videos
|
||||
for (let i = 0; i < subscriptions.length; i++) {
|
||||
sub = subscriptions[i];
|
||||
if (!sub.videos) continue;
|
||||
// add sub id for UI
|
||||
for (let j = 0; j < sub.videos.length; j++) {
|
||||
sub.videos[j].sub_id = sub.id;
|
||||
}
|
||||
|
||||
files = files.concat(sub.videos);
|
||||
}
|
||||
|
||||
files = JSON.parse(JSON.stringify(files));
|
||||
|
||||
// add thumbnails if present
|
||||
files.forEach(file => {
|
||||
if (file['thumbnailPath'] && fs.existsSync(file['thumbnailPath']))
|
||||
file['thumbnailBlob'] = fs.readFileSync(file['thumbnailPath']);
|
||||
});
|
||||
|
||||
res.send({
|
||||
files: files,
|
||||
playlists: playlists
|
||||
});
|
||||
});
|
||||
|
||||
// video sharing
|
||||
app.post('/api/enableSharing', optionalJwt, function(req, res) {
|
||||
var type = req.body.type;
|
||||
@@ -2260,7 +2248,7 @@ app.post('/api/getSubscription', optionalJwt, async (req, res) => {
|
||||
let appended_base_path = path.join(base_path, (subscription.isPlaylist ? 'playlists' : 'channels'), subscription.name, '/');
|
||||
let files;
|
||||
try {
|
||||
files = recFindByExt(appended_base_path, 'mp4');
|
||||
files = utils.recFindByExt(appended_base_path, 'mp4');
|
||||
} catch(e) {
|
||||
files = null;
|
||||
logger.info('Failed to get folder for subscription: ' + subscription.name + ' at path ' + appended_base_path);
|
||||
@@ -2325,6 +2313,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;
|
||||
|
||||
@@ -2341,13 +2339,16 @@ app.post('/api/createPlaylist', optionalJwt, async (req, res) => {
|
||||
let fileNames = req.body.fileNames;
|
||||
let type = req.body.type;
|
||||
let thumbnailURL = req.body.thumbnailURL;
|
||||
let duration = req.body.duration;
|
||||
|
||||
let new_playlist = {
|
||||
'name': playlistName,
|
||||
name: playlistName,
|
||||
fileNames: fileNames,
|
||||
id: shortid.generate(),
|
||||
thumbnailURL: thumbnailURL,
|
||||
type: type
|
||||
type: type,
|
||||
registered: Date.now(),
|
||||
duration: duration
|
||||
};
|
||||
|
||||
if (req.isAuthenticated()) {
|
||||
@@ -2473,11 +2474,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 +2509,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 +2516,6 @@ app.post('/api/deleteMp4', optionalJwt, async (req, res) => {
|
||||
} else {
|
||||
wasDeleted = false;
|
||||
res.send(wasDeleted);
|
||||
res.end("yes");
|
||||
}
|
||||
});
|
||||
|
||||
@@ -2538,9 +2536,10 @@ app.post('/api/downloadFile', optionalJwt, async (req, res) => {
|
||||
|
||||
let base_path = fileFolderPath;
|
||||
let usersFileFolder = null;
|
||||
if (req.isAuthenticated()) {
|
||||
const multiUserMode = config_api.getConfigItem('ytdl_multi_user_mode');
|
||||
if (multiUserMode && req.body.uuid) {
|
||||
usersFileFolder = config_api.getConfigItem('ytdl_users_base_path');
|
||||
base_path = path.join(usersFileFolder, req.user.uid, type);
|
||||
base_path = path.join(usersFileFolder, req.body.uuid, type);
|
||||
}
|
||||
if (!subscriptionName) {
|
||||
file = path.join(__dirname, base_path, fileNames + ext);
|
||||
@@ -2551,7 +2550,7 @@ app.post('/api/downloadFile', optionalJwt, async (req, res) => {
|
||||
else
|
||||
basePath = config_api.getConfigItem('ytdl_subscriptions_base_path');
|
||||
|
||||
file = path.join(__dirname, basePath, (subscriptionPlaylist === 'true' ? 'playlists' : 'channels'), subscriptionName, fileNames + ext);
|
||||
file = path.join(__dirname, basePath, (subscriptionPlaylist === true || subscriptionPlaylist === 'true' ? 'playlists' : 'channels'), subscriptionName, fileNames + ext);
|
||||
}
|
||||
} else {
|
||||
for (let i = 0; i < fileNames.length; i++) {
|
||||
@@ -2640,49 +2639,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 +2824,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;
|
||||
@@ -2894,8 +2886,7 @@ app.post('/api/auth/register'
|
||||
, optionalJwt
|
||||
, auth_api.registerUser);
|
||||
app.post('/api/auth/login'
|
||||
, auth_api.passport.authenticate('local', {})
|
||||
, auth_api.passport.authorize('local')
|
||||
, auth_api.passport.authenticate(['local', 'ldapauth'], {})
|
||||
, auth_api.generateJWT
|
||||
, auth_api.returnAuthResponse
|
||||
);
|
||||
|
||||
@@ -4,17 +4,14 @@
|
||||
"url": "http://example.com",
|
||||
"port": "17442"
|
||||
},
|
||||
"Encryption": {
|
||||
"use-encryption": false,
|
||||
"cert-file-path": "/etc/letsencrypt/live/example.com/fullchain.pem",
|
||||
"key-file-path": "/etc/letsencrypt/live/example.com/privkey.pem"
|
||||
},
|
||||
"Downloader": {
|
||||
"path-audio": "audio/",
|
||||
"path-video": "video/",
|
||||
"use_youtubedl_archive": false,
|
||||
"custom_args": "",
|
||||
"safe_download_override": false
|
||||
"safe_download_override": false,
|
||||
"include_thumbnail": true,
|
||||
"include_metadata": true
|
||||
},
|
||||
"Extra": {
|
||||
"title_top": "YoutubeDL-Material",
|
||||
@@ -22,7 +19,6 @@
|
||||
"allow_quality_select": true,
|
||||
"download_only_mode": false,
|
||||
"allow_multi_download_mode": true,
|
||||
"settings_pin_required": false,
|
||||
"enable_downloads_manager": true
|
||||
},
|
||||
"API": {
|
||||
@@ -38,12 +34,19 @@
|
||||
"Subscriptions": {
|
||||
"allow_subscriptions": true,
|
||||
"subscriptions_base_path": "subscriptions/",
|
||||
"subscriptions_check_interval": "300",
|
||||
"subscriptions_use_youtubedl_archive": true
|
||||
"subscriptions_check_interval": "300"
|
||||
},
|
||||
"Users": {
|
||||
"base_path": "users/",
|
||||
"allow_registration": true
|
||||
"allow_registration": true,
|
||||
"auth_method": "internal",
|
||||
"ldap_config": {
|
||||
"url": "ldap://localhost:389",
|
||||
"bindDN": "cn=root",
|
||||
"bindCredentials": "secret",
|
||||
"searchBase": "ou=passport-ldapauth",
|
||||
"searchFilter": "(uid={{username}})"
|
||||
}
|
||||
},
|
||||
"Advanced": {
|
||||
"use_default_downloading_agent": true,
|
||||
@@ -51,6 +54,7 @@
|
||||
"multi_user_mode": false,
|
||||
"allow_advanced_download": false,
|
||||
"use_cookies": false,
|
||||
"jwt_expiration": 86400,
|
||||
"logger_level": "info"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
{
|
||||
"YoutubeDLMaterial": {
|
||||
"Host": {
|
||||
"url": "https://example.com",
|
||||
"port": "17442"
|
||||
},
|
||||
"Encryption": {
|
||||
"use-encryption": true,
|
||||
"cert-file-path": "/etc/letsencrypt/live/example.com/fullchain.pem",
|
||||
"key-file-path": "/etc/letsencrypt/live/example.com/privkey.pem"
|
||||
},
|
||||
"Downloader": {
|
||||
"path-audio": "audio/",
|
||||
"path-video": "video/",
|
||||
"use_youtubedl_archive": false,
|
||||
"custom_args": "",
|
||||
"safe_download_override": false
|
||||
},
|
||||
"Extra": {
|
||||
"title_top": "YoutubeDL-Material",
|
||||
"file_manager_enabled": true,
|
||||
"allow_quality_select": true,
|
||||
"download_only_mode": false,
|
||||
"allow_multi_download_mode": true,
|
||||
"settings_pin_required": false,
|
||||
"enable_downloads_manager": true
|
||||
},
|
||||
"API": {
|
||||
"use_API_key": false,
|
||||
"API_key": "",
|
||||
"use_youtube_API": false,
|
||||
"youtube_API_key": ""
|
||||
},
|
||||
"Themes": {
|
||||
"default_theme": "default",
|
||||
"allow_theme_change": true
|
||||
},
|
||||
"Subscriptions": {
|
||||
"allow_subscriptions": true,
|
||||
"subscriptions_base_path": "subscriptions/",
|
||||
"subscriptions_check_interval": "300",
|
||||
"subscriptions_use_youtubedl_archive": true
|
||||
},
|
||||
"Users": {
|
||||
"base_path": "users/",
|
||||
"allow_registration": true
|
||||
},
|
||||
"Advanced": {
|
||||
"use_default_downloading_agent": true,
|
||||
"custom_downloading_agent": "",
|
||||
"multi_user_mode": false,
|
||||
"allow_advanced_download": false,
|
||||
"use_cookies": false,
|
||||
"logger_level": "info"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,10 +5,11 @@ 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;
|
||||
var LdapStrategy = require('passport-ldapauth');
|
||||
var JwtStrategy = require('passport-jwt').Strategy,
|
||||
ExtractJwt = require('passport-jwt').ExtractJwt;
|
||||
|
||||
@@ -29,7 +30,7 @@ exports.initialize = function(input_users_db, input_logger) {
|
||||
************************/
|
||||
saltRounds = 10;
|
||||
|
||||
JWT_EXPIRATION = (60 * 60); // one hour
|
||||
JWT_EXPIRATION = config_api.getConfigItem('ytdl_jwt_expiration');
|
||||
|
||||
SERVER_SECRET = null;
|
||||
if (users_db.get('jwt_secret').value()) {
|
||||
@@ -90,24 +91,7 @@ exports.registerUser = function(req, res) {
|
||||
|
||||
bcrypt.hash(plaintextPassword, saltRounds)
|
||||
.then(function(hash) {
|
||||
let new_user = {
|
||||
name: username,
|
||||
uid: userid,
|
||||
passhash: hash,
|
||||
files: {
|
||||
audio: [],
|
||||
video: []
|
||||
},
|
||||
playlists: {
|
||||
audio: [],
|
||||
video: []
|
||||
},
|
||||
subscriptions: [],
|
||||
created: Date.now(),
|
||||
role: userid === 'admin' ? 'admin' : 'user',
|
||||
permissions: [],
|
||||
permission_overrides: []
|
||||
};
|
||||
let new_user = generateUserObject(userid, username, hash);
|
||||
// check if user exists
|
||||
if (users_db.get('users').find({uid: userid}).value()) {
|
||||
// user id is taken!
|
||||
@@ -153,52 +137,43 @@ exports.registerUser = function(req, res) {
|
||||
|
||||
|
||||
exports.passport.use(new LocalStrategy({
|
||||
usernameField: 'userid',
|
||||
usernameField: 'username',
|
||||
passwordField: 'password'},
|
||||
function(username, password, done) {
|
||||
const user = users_db.get('users').find({name: username}).value();
|
||||
if (!user) { logger.error(`User ${username} not found`); return done(null, false); }
|
||||
if (user.auth_method && user.auth_method !== 'internal') { return done(null, false); }
|
||||
if (user) {
|
||||
return done(null, bcrypt.compareSync(password, user.passhash) ? user : false);
|
||||
}
|
||||
}
|
||||
));
|
||||
|
||||
/*passport.use(new BasicStrategy(
|
||||
function(userid, plainTextPassword, done) {
|
||||
const user = users_db.get('users').find({name: userid}).value();
|
||||
if (user) {
|
||||
var hashedPwd = user.passhash;
|
||||
return bcrypt.compare(plainTextPassword, hashedPwd);
|
||||
} else {
|
||||
return false;
|
||||
var getLDAPConfiguration = function(req, callback) {
|
||||
const ldap_config = config_api.getConfigItem('ytdl_ldap_config');
|
||||
const opts = {server: ldap_config};
|
||||
callback(null, opts);
|
||||
};
|
||||
|
||||
exports.passport.use(new LdapStrategy(getLDAPConfiguration,
|
||||
function(user, done) {
|
||||
// check if ldap auth is enabled
|
||||
const ldap_enabled = config_api.getConfigItem('ytdl_auth_method') === 'ldap';
|
||||
if (!ldap_enabled) return done(null, false);
|
||||
|
||||
const user_uid = user.uid;
|
||||
let db_user = users_db.get('users').find({uid: user_uid}).value();
|
||||
if (!db_user) {
|
||||
// generate DB user
|
||||
let new_user = generateUserObject(user_uid, user_uid, null, 'ldap');
|
||||
users_db.get('users').push(new_user).write();
|
||||
db_user = new_user;
|
||||
logger.verbose(`Generated new user ${user_uid} using LDAP`);
|
||||
}
|
||||
return done(null, db_user);
|
||||
}
|
||||
));
|
||||
*/
|
||||
|
||||
/*************************************************************
|
||||
* This is a wrapper for auth.passport.authenticate().
|
||||
* We use this to change WWW-Authenticate header so
|
||||
* the browser doesn't pop-up challenge dialog box by default.
|
||||
* Browser's will pop-up up dialog when status is 401 and
|
||||
* "WWW-Authenticate:Basic..."
|
||||
*************************************************************/
|
||||
/*
|
||||
exports.authenticateViaPassport = function(req, res, next) {
|
||||
exports.passport.authenticate('basic',{session:false},
|
||||
function(err, user, info) {
|
||||
if(!user){
|
||||
res.set('WWW-Authenticate', 'x'+info); // change to xBasic
|
||||
res.status(401).send('Invalid Authentication');
|
||||
} else {
|
||||
req.user = user;
|
||||
next();
|
||||
}
|
||||
}
|
||||
)(req, res, next);
|
||||
};
|
||||
*/
|
||||
|
||||
/**********************************
|
||||
* Generating/Signing a JWT token
|
||||
@@ -538,3 +513,26 @@ function getToken(queryParams) {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
function generateUserObject(userid, username, hash, auth_method = 'internal') {
|
||||
let new_user = {
|
||||
name: username,
|
||||
uid: userid,
|
||||
passhash: auth_method === 'internal' ? hash : null,
|
||||
files: {
|
||||
audio: [],
|
||||
video: []
|
||||
},
|
||||
playlists: {
|
||||
audio: [],
|
||||
video: []
|
||||
},
|
||||
subscriptions: [],
|
||||
created: Date.now(),
|
||||
role: userid === 'admin' && auth_method === 'internal' ? 'admin' : 'user',
|
||||
permissions: [],
|
||||
permission_overrides: [],
|
||||
auth_method: auth_method
|
||||
};
|
||||
return new_user;
|
||||
}
|
||||
|
||||
@@ -157,7 +157,7 @@ function setConfigItems(items) {
|
||||
|
||||
function globalArgsRequiresSafeDownload() {
|
||||
const globalArgs = getConfigItem('ytdl_custom_args').split(',,');
|
||||
const argsThatRequireSafeDownload = ['--write-sub', '--write-srt'];
|
||||
const argsThatRequireSafeDownload = ['--write-sub', '--write-srt', '--proxy'];
|
||||
const failedArgs = globalArgs.filter(arg => argsThatRequireSafeDownload.includes(arg));
|
||||
return failedArgs && failedArgs.length > 0;
|
||||
}
|
||||
@@ -181,17 +181,14 @@ DEFAULT_CONFIG = {
|
||||
"url": "http://example.com",
|
||||
"port": "17442"
|
||||
},
|
||||
"Encryption": {
|
||||
"use-encryption": false,
|
||||
"cert-file-path": "/etc/letsencrypt/live/example.com/fullchain.pem",
|
||||
"key-file-path": "/etc/letsencrypt/live/example.com/privkey.pem"
|
||||
},
|
||||
"Downloader": {
|
||||
"path-audio": "audio/",
|
||||
"path-video": "video/",
|
||||
"use_youtubedl_archive": false,
|
||||
"custom_args": "",
|
||||
"safe_download_override": false
|
||||
"safe_download_override": false,
|
||||
"include_thumbnail": true,
|
||||
"include_metadata": true
|
||||
},
|
||||
"Extra": {
|
||||
"title_top": "YoutubeDL-Material",
|
||||
@@ -199,7 +196,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": {
|
||||
@@ -215,12 +211,19 @@ DEFAULT_CONFIG = {
|
||||
"Subscriptions": {
|
||||
"allow_subscriptions": true,
|
||||
"subscriptions_base_path": "subscriptions/",
|
||||
"subscriptions_check_interval": "300",
|
||||
"subscriptions_use_youtubedl_archive": true
|
||||
"subscriptions_check_interval": "300"
|
||||
},
|
||||
"Users": {
|
||||
"base_path": "users/",
|
||||
"allow_registration": true
|
||||
"allow_registration": true,
|
||||
"auth_method": "internal",
|
||||
"ldap_config": {
|
||||
"url": "ldap://localhost:389",
|
||||
"bindDN": "cn=root",
|
||||
"bindCredentials": "secret",
|
||||
"searchBase": "ou=passport-ldapauth",
|
||||
"searchFilter": "(uid={{username}})"
|
||||
}
|
||||
},
|
||||
"Advanced": {
|
||||
"use_default_downloading_agent": true,
|
||||
@@ -228,6 +231,7 @@ DEFAULT_CONFIG = {
|
||||
"multi_user_mode": false,
|
||||
"allow_advanced_download": false,
|
||||
"use_cookies": false,
|
||||
"jwt_expiration": 86400,
|
||||
"logger_level": "info"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,20 +9,6 @@ let CONFIG_ITEMS = {
|
||||
'path': 'YoutubeDLMaterial.Host.port'
|
||||
},
|
||||
|
||||
// Encryption
|
||||
'ytdl_use_encryption': {
|
||||
'key': 'ytdl_use_encryption',
|
||||
'path': 'YoutubeDLMaterial.Encryption.use-encryption'
|
||||
},
|
||||
'ytdl_cert_file_path': {
|
||||
'key': 'ytdl_cert_file_path',
|
||||
'path': 'YoutubeDLMaterial.Encryption.cert-file-path'
|
||||
},
|
||||
'ytdl_key_file_path': {
|
||||
'key': 'ytdl_key_file_path',
|
||||
'path': 'YoutubeDLMaterial.Encryption.key-file-path'
|
||||
},
|
||||
|
||||
// Downloader
|
||||
'ytdl_audio_folder_path': {
|
||||
'key': 'ytdl_audio_folder_path',
|
||||
@@ -44,6 +30,14 @@ let CONFIG_ITEMS = {
|
||||
'key': 'ytdl_safe_download_override',
|
||||
'path': 'YoutubeDLMaterial.Downloader.safe_download_override'
|
||||
},
|
||||
'ytdl_include_thumbnail': {
|
||||
'key': 'ytdl_include_thumbnail',
|
||||
'path': 'YoutubeDLMaterial.Downloader.include_thumbnail'
|
||||
},
|
||||
'ytdl_include_metadata': {
|
||||
'key': 'ytdl_include_metadata',
|
||||
'path': 'YoutubeDLMaterial.Downloader.include_metadata'
|
||||
},
|
||||
|
||||
// Extra
|
||||
'ytdl_title_top': {
|
||||
@@ -66,10 +60,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'
|
||||
@@ -120,10 +110,6 @@ let CONFIG_ITEMS = {
|
||||
'key': 'ytdl_subscriptions_check_interval',
|
||||
'path': 'YoutubeDLMaterial.Subscriptions.subscriptions_check_interval'
|
||||
},
|
||||
'ytdl_subscriptions_use_youtubedl_archive': {
|
||||
'key': 'ytdl_use_youtubedl_archive',
|
||||
'path': 'YoutubeDLMaterial.Subscriptions.subscriptions_use_youtubedl_archive'
|
||||
},
|
||||
|
||||
// Users
|
||||
'ytdl_users_base_path': {
|
||||
@@ -134,6 +120,14 @@ let CONFIG_ITEMS = {
|
||||
'key': 'ytdl_allow_registration',
|
||||
'path': 'YoutubeDLMaterial.Users.allow_registration'
|
||||
},
|
||||
'ytdl_auth_method': {
|
||||
'key': 'ytdl_auth_method',
|
||||
'path': 'YoutubeDLMaterial.Users.auth_method'
|
||||
},
|
||||
'ytdl_ldap_config': {
|
||||
'key': 'ytdl_ldap_config',
|
||||
'path': 'YoutubeDLMaterial.Users.ldap_config'
|
||||
},
|
||||
|
||||
// Advanced
|
||||
'ytdl_use_default_downloading_agent': {
|
||||
@@ -156,6 +150,10 @@ let CONFIG_ITEMS = {
|
||||
'key': 'ytdl_use_cookies',
|
||||
'path': 'YoutubeDLMaterial.Advanced.use_cookies'
|
||||
},
|
||||
'ytdl_jwt_expiration': {
|
||||
'key': 'ytdl_jwt_expiration',
|
||||
'path': 'YoutubeDLMaterial.Advanced.jwt_expiration'
|
||||
},
|
||||
'ytdl_logger_level': {
|
||||
'key': 'ytdl_logger_level',
|
||||
'path': 'YoutubeDLMaterial.Advanced.logger_level'
|
||||
@@ -174,5 +172,5 @@ AVAILABLE_PERMISSIONS = [
|
||||
module.exports = {
|
||||
CONFIG_ITEMS: CONFIG_ITEMS,
|
||||
AVAILABLE_PERMISSIONS: AVAILABLE_PERMISSIONS,
|
||||
CURRENT_VERSION: 'v4.0'
|
||||
CURRENT_VERSION: 'v4.1'
|
||||
}
|
||||
|
||||
154
backend/db.js
154
backend/db.js
@@ -16,6 +16,7 @@ function initialize(input_db, input_users_db, input_logger) {
|
||||
}
|
||||
|
||||
function registerFileDB(file_path, type, multiUserMode = null, sub = null) {
|
||||
let db_path = null;
|
||||
const file_id = file_path.substring(0, file_path.length-4);
|
||||
const file_object = generateFileObject(file_id, type, multiUserMode && multiUserMode.file_path, sub);
|
||||
if (!file_object) {
|
||||
@@ -25,45 +26,47 @@ function registerFileDB(file_path, type, multiUserMode = null, sub = null) {
|
||||
|
||||
utils.fixVideoMetadataPerms(file_id, type, multiUserMode && multiUserMode.file_path);
|
||||
|
||||
// add thumbnail path
|
||||
file_object['thumbnailPath'] = utils.getDownloadedThumbnail(file_id, type, multiUserMode && multiUserMode.file_path);
|
||||
|
||||
if (!sub) {
|
||||
if (multiUserMode) {
|
||||
const user_uid = multiUserMode.user;
|
||||
db_path = users_db.get('users').find({uid: user_uid}).get(`files.${type}`);
|
||||
} else {
|
||||
db_path = db.get(`files.${type}`)
|
||||
}
|
||||
} else {
|
||||
if (multiUserMode) {
|
||||
const user_uid = multiUserMode.user;
|
||||
db_path = users_db.get('users').find({uid: user_uid}).get('subscriptions').find({id: sub.id}).get('videos');
|
||||
} else {
|
||||
db_path = db.get('subscriptions').find({id: sub.id}).get('videos');
|
||||
}
|
||||
}
|
||||
|
||||
const file_uid = registerFileDBManual(db_path, file_object);
|
||||
|
||||
// remove metadata JSON if needed
|
||||
if (!config_api.getConfigItem('ytdl_include_metadata')) {
|
||||
utils.deleteJSONFile(file_id, type, multiUserMode && multiUserMode.file_path)
|
||||
}
|
||||
|
||||
return file_uid;
|
||||
}
|
||||
|
||||
function registerFileDBManual(db_path, file_object) {
|
||||
// add additional info
|
||||
file_object['uid'] = uuid();
|
||||
file_object['registered'] = Date.now();
|
||||
path_object = path.parse(file_object['path']);
|
||||
file_object['path'] = path.format(path_object);
|
||||
|
||||
if (!sub) {
|
||||
if (multiUserMode) {
|
||||
const user_uid = multiUserMode.user;
|
||||
users_db.get('users').find({uid: user_uid}).get(`files.${type}`)
|
||||
.remove({
|
||||
path: file_object['path']
|
||||
}).write();
|
||||
|
||||
users_db.get('users').find({uid: user_uid}).get(`files.${type}`)
|
||||
.push(file_object)
|
||||
.write();
|
||||
} else {
|
||||
// remove existing video if overwriting
|
||||
db.get(`files.${type}`)
|
||||
.remove({
|
||||
path: file_object['path']
|
||||
}).write();
|
||||
|
||||
db.get(`files.${type}`)
|
||||
.push(file_object)
|
||||
.write();
|
||||
}
|
||||
} else {
|
||||
sub_db = null;
|
||||
if (multiUserMode) {
|
||||
const user_uid = multiUserMode.user;
|
||||
sub_db = users_db.get('users').find({uid: user_uid}).get('subscriptions').find({id: sub.id});
|
||||
} else {
|
||||
sub_db = db.get('subscriptions').find({id: sub.id});
|
||||
}
|
||||
sub_db.get('videos').push(file_object).write();
|
||||
}
|
||||
// remove duplicate(s)
|
||||
db_path.remove({path: file_object['path']}).write();
|
||||
|
||||
// add new file to db
|
||||
db_path.push(file_object).write();
|
||||
return file_object['uid'];
|
||||
}
|
||||
|
||||
@@ -112,8 +115,93 @@ function getAppendedBasePathSub(sub, base_path) {
|
||||
return path.join(base_path, (sub.isPlaylist ? 'playlists/' : 'channels/'), sub.name);
|
||||
}
|
||||
|
||||
async function importUnregisteredFiles() {
|
||||
let dirs_to_check = [];
|
||||
let subscriptions_to_check = [];
|
||||
const subscriptions_base_path = config_api.getConfigItem('ytdl_subscriptions_base_path'); // only for single-user mode
|
||||
const multi_user_mode = config_api.getConfigItem('ytdl_multi_user_mode');
|
||||
const usersFileFolder = config_api.getConfigItem('ytdl_users_base_path');
|
||||
const subscriptions_enabled = config_api.getConfigItem('ytdl_allow_subscriptions');
|
||||
if (multi_user_mode) {
|
||||
let users = users_db.get('users').value();
|
||||
for (let i = 0; i < users.length; i++) {
|
||||
const user = users[i];
|
||||
|
||||
if (subscriptions_enabled) subscriptions_to_check = subscriptions_to_check.concat(users[i]['subscriptions']);
|
||||
|
||||
// add user's audio dir to check list
|
||||
dirs_to_check.push({
|
||||
basePath: path.join(usersFileFolder, user.uid, 'audio'),
|
||||
dbPath: users_db.get('users').find({uid: user.uid}).get('files.audio'),
|
||||
type: 'audio'
|
||||
});
|
||||
|
||||
// add user's video dir to check list
|
||||
dirs_to_check.push({
|
||||
basePath: path.join(usersFileFolder, user.uid, 'video'),
|
||||
dbPath: users_db.get('users').find({uid: user.uid}).get('files.video'),
|
||||
type: 'video'
|
||||
});
|
||||
}
|
||||
} else {
|
||||
const audioFolderPath = config_api.getConfigItem('ytdl_audio_folder_path');
|
||||
const videoFolderPath = config_api.getConfigItem('ytdl_video_folder_path');
|
||||
const subscriptions = db.get('subscriptions').value();
|
||||
|
||||
if (subscriptions_enabled && subscriptions) subscriptions_to_check = subscriptions_to_check.concat(subscriptions);
|
||||
|
||||
// add audio dir to check list
|
||||
dirs_to_check.push({
|
||||
basePath: audioFolderPath,
|
||||
dbPath: db.get('files.audio'),
|
||||
type: 'audio'
|
||||
});
|
||||
|
||||
// add video dir to check list
|
||||
dirs_to_check.push({
|
||||
basePath: videoFolderPath,
|
||||
dbPath: db.get('files.video'),
|
||||
type: 'video'
|
||||
});
|
||||
}
|
||||
|
||||
// add subscriptions to check list
|
||||
for (let i = 0; i < subscriptions_to_check.length; i++) {
|
||||
let subscription_to_check = subscriptions_to_check[i];
|
||||
if (!subscription_to_check.name) {
|
||||
// TODO: Remove subscription as it'll never complete
|
||||
continue;
|
||||
}
|
||||
dirs_to_check.push({
|
||||
basePath: multi_user_mode ? path.join(usersFileFolder, subscription_to_check.user_uid, 'subscriptions', subscription_to_check.isPlaylist ? 'playlists/' : 'channels/', subscription_to_check.name)
|
||||
: path.join(subscriptions_base_path, subscription_to_check.isPlaylist ? 'playlists/' : 'channels/', subscription_to_check.name),
|
||||
dbPath: multi_user_mode ? users_db.get('users').find({uid: subscription_to_check.user_uid}).get('subscriptions').find({id: subscription_to_check.id}).get('videos')
|
||||
: db.get('subscriptions').find({id: subscription_to_check.id}).get('videos'),
|
||||
type: subscription_to_check.type
|
||||
});
|
||||
}
|
||||
|
||||
// run through check list and check each file to see if it's missing from the db
|
||||
dirs_to_check.forEach(dir_to_check => {
|
||||
// recursively get all files in dir's path
|
||||
const files = utils.getDownloadedFilesByType(dir_to_check.basePath, dir_to_check.type);
|
||||
|
||||
files.forEach(file => {
|
||||
// check if file exists in db, if not add it
|
||||
const file_is_registered = !!(dir_to_check.dbPath.find({id: file.id}).value())
|
||||
if (!file_is_registered) {
|
||||
// add additional info
|
||||
registerFileDBManual(dir_to_check.dbPath, file);
|
||||
logger.verbose(`Added discovered file to the database: ${file.id}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
initialize: initialize,
|
||||
registerFileDB: registerFileDB,
|
||||
updatePlaylist: updatePlaylist
|
||||
updatePlaylist: updatePlaylist,
|
||||
importUnregisteredFiles: importUnregisteredFiles
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
654
backend/package-lock.json
generated
654
backend/package-lock.json
generated
@@ -4,6 +4,89 @@
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
"@types/body-parser": {
|
||||
"version": "1.19.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.0.tgz",
|
||||
"integrity": "sha512-W98JrE0j2K78swW4ukqMleo8R7h/pFETjM2DQ90MF6XK2i4LO4W3gQ71Lt4w3bfm2EvVSyWHplECvB5sK22yFQ==",
|
||||
"requires": {
|
||||
"@types/connect": "*",
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"@types/connect": {
|
||||
"version": "3.4.33",
|
||||
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.33.tgz",
|
||||
"integrity": "sha512-2+FrkXY4zllzTNfJth7jOqEHC+enpLeGslEhpnTAkg21GkRrWV4SsAtqchtT4YS9/nODBU2/ZfsBY2X4J/dX7A==",
|
||||
"requires": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"@types/express": {
|
||||
"version": "4.17.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.7.tgz",
|
||||
"integrity": "sha512-dCOT5lcmV/uC2J9k0rPafATeeyz+99xTt54ReX11/LObZgfzJqZNcW27zGhYyX+9iSEGXGt5qLPwRSvBZcLvtQ==",
|
||||
"requires": {
|
||||
"@types/body-parser": "*",
|
||||
"@types/express-serve-static-core": "*",
|
||||
"@types/qs": "*",
|
||||
"@types/serve-static": "*"
|
||||
}
|
||||
},
|
||||
"@types/express-serve-static-core": {
|
||||
"version": "4.17.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.9.tgz",
|
||||
"integrity": "sha512-DG0BYg6yO+ePW+XoDENYz8zhNGC3jDDEpComMYn7WJc4mY1Us8Rw9ax2YhJXxpyk2SF47PQAoQ0YyVT1a0bEkA==",
|
||||
"requires": {
|
||||
"@types/node": "*",
|
||||
"@types/qs": "*",
|
||||
"@types/range-parser": "*"
|
||||
}
|
||||
},
|
||||
"@types/ldapjs": {
|
||||
"version": "1.0.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/ldapjs/-/ldapjs-1.0.9.tgz",
|
||||
"integrity": "sha512-3PvY7Drp1zoLbcGlothCAkoc5o6Jp9KvUPwHadlHyKp3yPvyeIh7w2zQc9UXMzgDRkoeGXUEODtbEs5XCh9ZyA==",
|
||||
"requires": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"@types/mime": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-2.0.3.tgz",
|
||||
"integrity": "sha512-Jus9s4CDbqwocc5pOAnh8ShfrnMcPHuJYzVcSUU7lrh8Ni5HuIqX3oilL86p3dlTrk0LzHRCgA/GQ7uNCw6l2Q=="
|
||||
},
|
||||
"@types/node": {
|
||||
"version": "14.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.6.0.tgz",
|
||||
"integrity": "sha512-mikldZQitV94akrc4sCcSjtJfsTKt4p+e/s0AGscVA6XArQ9kFclP+ZiYUMnq987rc6QlYxXv/EivqlfSLxpKA=="
|
||||
},
|
||||
"@types/passport": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/passport/-/passport-1.0.4.tgz",
|
||||
"integrity": "sha512-h5OfAbfBBYSzjeU0GTuuqYEk9McTgWeGQql9g3gUw2/NNCfD7VgExVRYJVVeU13Twn202Mvk9BT0bUrl30sEgA==",
|
||||
"requires": {
|
||||
"@types/express": "*"
|
||||
}
|
||||
},
|
||||
"@types/qs": {
|
||||
"version": "6.9.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.4.tgz",
|
||||
"integrity": "sha512-+wYo+L6ZF6BMoEjtf8zB2esQsqdV6WsjRK/GP9WOgLPrq87PbNWgIxS76dS5uvl/QXtHGakZmwTznIfcPXcKlQ=="
|
||||
},
|
||||
"@types/range-parser": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.3.tgz",
|
||||
"integrity": "sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA=="
|
||||
},
|
||||
"@types/serve-static": {
|
||||
"version": "1.13.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.5.tgz",
|
||||
"integrity": "sha512-6M64P58N+OXjU432WoLLBQxbA0LRGBCRm7aAGQJ+SMC1IMl0dgRVi9EFfoDcS2a7Xogygk/eGN94CfwU9UF7UQ==",
|
||||
"requires": {
|
||||
"@types/express-serve-static-core": "*",
|
||||
"@types/mime": "*"
|
||||
}
|
||||
},
|
||||
"abbrev": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
|
||||
@@ -50,6 +133,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 +152,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 +209,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",
|
||||
@@ -194,6 +252,14 @@
|
||||
"resolved": "https://registry.npmjs.org/aws4/-/aws4-1.9.1.tgz",
|
||||
"integrity": "sha512-wMHVg2EOHaMRxbzgFJ9gtjOOCrI80OHLG14rxi28XwOW8ux6IiEbRCGGGqCtdAIg4FQCbW20k9RsT4y3gJlFug=="
|
||||
},
|
||||
"backoff": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/backoff/-/backoff-2.5.0.tgz",
|
||||
"integrity": "sha1-9hbtqdPktmuMp/ynn2lXIsX44m8=",
|
||||
"requires": {
|
||||
"precond": "0.2"
|
||||
}
|
||||
},
|
||||
"balanced-match": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
|
||||
@@ -204,15 +270,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 +278,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",
|
||||
@@ -344,6 +406,17 @@
|
||||
"resolved": "https://registry.npmjs.org/buffers/-/buffers-0.1.1.tgz",
|
||||
"integrity": "sha1-skV5w77U1tOWru5tmorn9Ugqt7s="
|
||||
},
|
||||
"bunyan": {
|
||||
"version": "1.8.14",
|
||||
"resolved": "https://registry.npmjs.org/bunyan/-/bunyan-1.8.14.tgz",
|
||||
"integrity": "sha512-LlahJUxXzZLuw/hetUQJmRgZ1LF6+cr5TPpRj6jf327AsiIq2jhYEH4oqUUkVKTor+9w2BT3oxVwhzE5lw9tcg==",
|
||||
"requires": {
|
||||
"dtrace-provider": "~0.8",
|
||||
"moment": "^2.19.3",
|
||||
"mv": "~2",
|
||||
"safe-json-stringify": "~1"
|
||||
}
|
||||
},
|
||||
"busboy": {
|
||||
"version": "0.2.14",
|
||||
"resolved": "https://registry.npmjs.org/busboy/-/busboy-0.2.14.tgz",
|
||||
@@ -434,11 +507,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 +517,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 +677,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 +781,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 +791,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",
|
||||
@@ -788,6 +841,15 @@
|
||||
"is-obj": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"dtrace-provider": {
|
||||
"version": "0.8.8",
|
||||
"resolved": "https://registry.npmjs.org/dtrace-provider/-/dtrace-provider-0.8.8.tgz",
|
||||
"integrity": "sha512-b7Z7cNtHPhH9EJhNNbbeqTcXB8LGFFZhq1PGgEvpeHlzd36bhbdTWoE/Ba/YguqpBSlAPKnARWhVlhunCMwfxg==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"nan": "^2.14.0"
|
||||
}
|
||||
},
|
||||
"duplexer2": {
|
||||
"version": "0.1.4",
|
||||
"resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz",
|
||||
@@ -1051,14 +1113,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 +1135,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 +1234,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 +1302,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",
|
||||
@@ -1590,15 +1583,81 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"ldap-filter": {
|
||||
"version": "0.2.2",
|
||||
"resolved": "https://registry.npmjs.org/ldap-filter/-/ldap-filter-0.2.2.tgz",
|
||||
"integrity": "sha1-8rhCvguG2jNSeYUFsx68rlkNd9A=",
|
||||
"requires": {
|
||||
"assert-plus": "0.1.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"assert-plus": {
|
||||
"version": "0.1.5",
|
||||
"resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.1.5.tgz",
|
||||
"integrity": "sha1-7nQAlBMALYTOxyGcasgRgS5yMWA="
|
||||
}
|
||||
}
|
||||
},
|
||||
"ldapauth-fork": {
|
||||
"version": "4.3.3",
|
||||
"resolved": "https://registry.npmjs.org/ldapauth-fork/-/ldapauth-fork-4.3.3.tgz",
|
||||
"integrity": "sha512-x76VpQ5ZqkwAJmqwcD6KIwDiNEbgIGIPGwC/eA17e1dxWhlTx36w0DlLOFwjTuZ2iuaLTsZsUprlVqvSlwc/1Q==",
|
||||
"requires": {
|
||||
"@types/ldapjs": "^1.0.0",
|
||||
"@types/node": "*",
|
||||
"bcryptjs": "^2.4.0",
|
||||
"ldapjs": "^1.0.2",
|
||||
"lru-cache": "^5.1.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"lru-cache": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
|
||||
"integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
|
||||
"requires": {
|
||||
"yallist": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"yallist": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
|
||||
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"ldapjs": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/ldapjs/-/ldapjs-1.0.2.tgz",
|
||||
"integrity": "sha1-VE/3Ayt7g8aPBwEyjZKXqmlDQPk=",
|
||||
"requires": {
|
||||
"asn1": "0.2.3",
|
||||
"assert-plus": "^1.0.0",
|
||||
"backoff": "^2.5.0",
|
||||
"bunyan": "^1.8.3",
|
||||
"dashdash": "^1.14.0",
|
||||
"dtrace-provider": "~0.8",
|
||||
"ldap-filter": "0.2.2",
|
||||
"once": "^1.4.0",
|
||||
"vasync": "^1.6.4",
|
||||
"verror": "^1.8.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"asn1": {
|
||||
"version": "0.2.3",
|
||||
"resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz",
|
||||
"integrity": "sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y="
|
||||
}
|
||||
}
|
||||
},
|
||||
"listenercount": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/listenercount/-/listenercount-1.0.1.tgz",
|
||||
"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 +1841,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",
|
||||
@@ -1814,6 +1849,12 @@
|
||||
"minimist": "^1.2.5"
|
||||
}
|
||||
},
|
||||
"moment": {
|
||||
"version": "2.27.0",
|
||||
"resolved": "https://registry.npmjs.org/moment/-/moment-2.27.0.tgz",
|
||||
"integrity": "sha512-al0MUK7cpIcglMv3YF13qSgdAIqxHTO7brRtaz3DlSULbqfazqkc5kEjNrLDOM7fsjshoFIihnU8snrP7zUvhQ==",
|
||||
"optional": true
|
||||
},
|
||||
"ms": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||
@@ -1859,50 +1900,77 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"mv": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/mv/-/mv-2.1.1.tgz",
|
||||
"integrity": "sha1-rmzg1vbV4KT32JN5jQPB6pVZtqI=",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"mkdirp": "~0.5.1",
|
||||
"ncp": "~2.0.0",
|
||||
"rimraf": "~2.4.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"glob": {
|
||||
"version": "6.0.4",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz",
|
||||
"integrity": "sha1-DwiGD2oVUSey+t1PnOJLGqtuTSI=",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"inflight": "^1.0.4",
|
||||
"inherits": "2",
|
||||
"minimatch": "2 || 3",
|
||||
"once": "^1.3.0",
|
||||
"path-is-absolute": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"rimraf": {
|
||||
"version": "2.4.5",
|
||||
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.4.5.tgz",
|
||||
"integrity": "sha1-7nEM5dk6j9uFb7Xqj/Di11k0sto=",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"glob": "^6.0.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"mz": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",
|
||||
"integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==",
|
||||
"requires": {
|
||||
"any-promise": "^1.0.0",
|
||||
"object-assign": "^4.0.1",
|
||||
"thenify-all": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"nan": {
|
||||
"version": "2.14.1",
|
||||
"resolved": "https://registry.npmjs.org/nan/-/nan-2.14.1.tgz",
|
||||
"integrity": "sha512-isWHgVjnFjh2x2yuJ/tj3JbwoHu3UC2dX5G/88Cm24yB6YopVgxvBObDY7n5xW6ExmFhJpSEQqFPvq9zaXc8Jw==",
|
||||
"optional": true
|
||||
},
|
||||
"nanoid": {
|
||||
"version": "2.1.11",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-2.1.11.tgz",
|
||||
"integrity": "sha512-s/snB+WGm6uwi0WjsZdaVcuf3KJXlfGl2LcxgwkEwJF0D/BWzVWAZW/XY4bFaiR7s0Jk3FPvlnepg1H1b1UwlA=="
|
||||
},
|
||||
"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=="
|
||||
}
|
||||
}
|
||||
"ncp": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz",
|
||||
"integrity": "sha1-GVoh1sRuNh0vsSgbo4uR6d9727M=",
|
||||
"optional": true
|
||||
},
|
||||
"negotiator": {
|
||||
"version": "0.6.2",
|
||||
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz",
|
||||
"integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw=="
|
||||
},
|
||||
"node-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",
|
||||
"integrity": "sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA=="
|
||||
"version": "2.6.1",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz",
|
||||
"integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw=="
|
||||
},
|
||||
"node-id3": {
|
||||
"version": "0.1.16",
|
||||
@@ -1912,34 +1980,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 +2025,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 +2033,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 +2077,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",
|
||||
@@ -2166,6 +2148,17 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"passport-ldapauth": {
|
||||
"version": "2.1.4",
|
||||
"resolved": "https://registry.npmjs.org/passport-ldapauth/-/passport-ldapauth-2.1.4.tgz",
|
||||
"integrity": "sha512-VeVL69ZK+cpJe0DKMSGuwcf7k+V4dr0U0Y7ZhXL785pcRb5gRA6qYZfIH+XTsAzwqTK9l0Dn3Ds4weOZ1jKkLQ==",
|
||||
"requires": {
|
||||
"@types/node": "*",
|
||||
"@types/passport": "^1.0.0",
|
||||
"ldapauth-fork": "^4.3.2",
|
||||
"passport-strategy": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"passport-local": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/passport-local/-/passport-local-1.0.0.tgz",
|
||||
@@ -2219,6 +2212,11 @@
|
||||
"resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz",
|
||||
"integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY="
|
||||
},
|
||||
"precond": {
|
||||
"version": "0.2.3",
|
||||
"resolved": "https://registry.npmjs.org/precond/-/precond-0.2.3.tgz",
|
||||
"integrity": "sha1-qpWRvKokkj8eD0hJ0kD0fvwQdaw="
|
||||
},
|
||||
"prepend-http": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-1.0.4.tgz",
|
||||
@@ -2243,6 +2241,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 +2317,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",
|
||||
@@ -2398,16 +2412,17 @@
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
||||
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
|
||||
},
|
||||
"safe-json-stringify": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/safe-json-stringify/-/safe-json-stringify-1.2.0.tgz",
|
||||
"integrity": "sha512-gH8eh2nZudPQO6TytOvbxnuhYBOvDBBLW52tz5q6X58lJcd/tkmqFR+5Z9adS8aJtURSXWThWy/xJtJwixErvg==",
|
||||
"optional": true
|
||||
},
|
||||
"safer-buffer": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||
"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 +2474,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 +2613,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 +2721,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",
|
||||
@@ -2921,6 +2934,29 @@
|
||||
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
||||
"integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw="
|
||||
},
|
||||
"vasync": {
|
||||
"version": "1.6.4",
|
||||
"resolved": "https://registry.npmjs.org/vasync/-/vasync-1.6.4.tgz",
|
||||
"integrity": "sha1-3+k2Fq0OeugBszKp2Iv8XNyOHR8=",
|
||||
"requires": {
|
||||
"verror": "1.6.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"extsprintf": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.2.0.tgz",
|
||||
"integrity": "sha1-WtlGwi9bMrp/jNdCZxHG6KP8JSk="
|
||||
},
|
||||
"verror": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/verror/-/verror-1.6.0.tgz",
|
||||
"integrity": "sha1-fROyex+swuLakEBetepuW90lLqU=",
|
||||
"requires": {
|
||||
"extsprintf": "1.2.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"verror": {
|
||||
"version": "1.10.0",
|
||||
"resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz",
|
||||
@@ -2939,14 +2975,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",
|
||||
|
||||
@@ -30,26 +30,29 @@
|
||||
"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",
|
||||
"express": "^4.17.1",
|
||||
"fluent-ffmpeg": "^2.1.2",
|
||||
"fs-extra": "^9.0.0",
|
||||
"glob": "^7.1.6",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"lowdb": "^1.0.0",
|
||||
"md5": "^2.2.1",
|
||||
"merge-files": "^0.1.2",
|
||||
"multer": "^1.4.2",
|
||||
"node-fetch": "^2.6.0",
|
||||
"node-fetch": "^2.6.1",
|
||||
"node-id3": "^0.1.14",
|
||||
"nodemon": "^2.0.2",
|
||||
"passport": "^0.4.1",
|
||||
"passport-http": "^0.3.0",
|
||||
"passport-jwt": "^4.0.0",
|
||||
"passport-ldapauth": "^2.1.4",
|
||||
"passport-local": "^1.0.0",
|
||||
"progress": "^2.0.3",
|
||||
"ps-node": "^0.1.6",
|
||||
"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
@@ -1,794 +0,0 @@
|
||||
@angular-devkit/build-angular
|
||||
MIT
|
||||
The MIT License
|
||||
|
||||
Copyright (c) 2017 Google, Inc.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
|
||||
@angular/animations
|
||||
MIT
|
||||
|
||||
@angular/cdk
|
||||
MIT
|
||||
The MIT License
|
||||
|
||||
Copyright (c) 2020 Google LLC.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
|
||||
@angular/common
|
||||
MIT
|
||||
|
||||
@angular/compiler
|
||||
MIT
|
||||
|
||||
@angular/core
|
||||
MIT
|
||||
|
||||
@angular/forms
|
||||
MIT
|
||||
|
||||
@angular/localize
|
||||
MIT
|
||||
|
||||
@angular/material
|
||||
MIT
|
||||
The MIT License
|
||||
|
||||
Copyright (c) 2020 Google LLC.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
|
||||
@angular/platform-browser
|
||||
MIT
|
||||
|
||||
@angular/platform-browser-dynamic
|
||||
MIT
|
||||
|
||||
@angular/router
|
||||
MIT
|
||||
|
||||
core-js
|
||||
MIT
|
||||
Copyright (c) 2014-2020 Denis Pushkarev
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
|
||||
file-saver
|
||||
MIT
|
||||
The MIT License
|
||||
|
||||
Copyright © 2016 [Eli Grey][1].
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
[1]: http://eligrey.com
|
||||
|
||||
|
||||
filesize
|
||||
BSD-3-Clause
|
||||
Copyright (c) 2020, Jason Mulligan
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
* Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
* Neither the name of filesize nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
|
||||
|
||||
fingerprintjs2
|
||||
MIT
|
||||
Fingerprintjs2 – Modern & flexible browser fingerprint library v2
|
||||
https://github.com/Valve/fingerprintjs2
|
||||
Copyright (c) 2018– Jonas Haag (jonas@lophus.org)
|
||||
Copyright (c) 2015– Valentin Vasilyev (valentin.vasilyev@outlook.com)
|
||||
Licensed under the MIT (http://www.opensource.org/licenses/mit-license.php) license.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL VALENTIN VASILYEV BE LIABLE FOR ANY
|
||||
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
|
||||
THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
|
||||
ng-lazyload-image
|
||||
MIT
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2016 Oskar Karlsson
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
|
||||
ngx-file-drop
|
||||
MIT
|
||||
|
||||
ngx-videogular
|
||||
MIT
|
||||
|
||||
regenerator-runtime
|
||||
MIT
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2014-present, Facebook, Inc.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
|
||||
rxjs
|
||||
Apache-2.0
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright (c) 2015-2018 Google, Inc., Netflix, Inc., Microsoft Corp. and contributors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
|
||||
|
||||
rxjs-compat
|
||||
Apache-2.0
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright (c) 2015-2018 Google, Inc., Netflix, Inc., Microsoft Corp. and contributors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
|
||||
|
||||
tslib
|
||||
Apache-2.0
|
||||
Apache License
|
||||
|
||||
Version 2.0, January 2004
|
||||
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions:
|
||||
|
||||
You must give any other recipients of the Work or Derivative Works a copy of this License; and
|
||||
|
||||
You must cause any modified files to carry prominent notices stating that You changed the files; and
|
||||
|
||||
You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and
|
||||
|
||||
If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
|
||||
uuid
|
||||
MIT
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2010-2016 Robert Kieffer and other contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
|
||||
web-animations-js
|
||||
Apache-2.0
|
||||
|
||||
webpack
|
||||
MIT
|
||||
Copyright JS Foundation and other contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
'Software'), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
|
||||
zone.js
|
||||
MIT
|
||||
The MIT License
|
||||
|
||||
Copyright (c) 2010-2020 Google LLC. http://angular.io/license
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
@@ -1,198 +0,0 @@
|
||||
{
|
||||
"17f0ea5d2d7a262b0e875acc70475f102aee84e6": "Playlist erstellen",
|
||||
"cff1428d10d59d14e45edec3c735a27b5482db59": "Name",
|
||||
"f47e2d56dd8a145b2e9599da9730c049d52962a2": "Audiodateien",
|
||||
"a52dae09be10ca3a65da918533ced3d3f4992238": "Videos",
|
||||
"d9e83ac17026e70ef6e9c0f3240a3b2450367f40": "Youtube-dl Argumente ändern",
|
||||
"7fc1946abe2b40f60059c6cd19975d677095fd19": "Simulierte neue Argumente",
|
||||
"0b71824ae71972f236039bed43f8d2323e8fd570": "Argument hinzufügen",
|
||||
"c8b0e59eb491f2ac7505f0fbab747062e6b32b23": "Nach Kategorie filtern",
|
||||
"9eeb91caef5a50256dd87e1c4b7b3e8216479377": "Argument-Wert verwenden",
|
||||
"25d8ad5eba2ec24e68295a27d6a4bb9b49e3dacd": "Argument-Wert",
|
||||
"7de2451ed3fb8d8b847979bd3f0c740b970f167b": "Argument hinzufügen",
|
||||
"d7b35c384aecd25a516200d6921836374613dfe7": "Abbrechen",
|
||||
"b2623aee44b70c9a4ba1fce16c8a593b0a4c7974": "Ändern",
|
||||
"038ebcb2a89155d90c24fa1c17bfe83dbadc3c20": "YouTube Downloader",
|
||||
"6d2ec8898344c8955542b0542c942038ef28bb80": "Bitte geben Sie eine gültige URL ein.",
|
||||
"a38ae1082fec79ba1f379978337385a539a28e73": "Qualität",
|
||||
"4be966a9dcfbc9b54dfcc604b831c0289f847fa4": "URL verwenden",
|
||||
"d3f02f845e62cebd75fde451ab8479d2a8ad784d": "Ansehen",
|
||||
"4a9889d36910edc8323d7bab60858ab3da6d91df": "Nur Audio",
|
||||
"96a01fafe135afc58b0f8071a4ab00234495ce18": "Multi-Download Modus",
|
||||
"6a21ba5fb0ac804a525bf9ab168038c3ee88e661": "Download",
|
||||
"6a3777f913cf3f288664f0632b9f24794fdcc24e": "Abbrechen",
|
||||
"322ed150e02666fe2259c5b4614eac7066f4ffa0": "Erweitert",
|
||||
"b7ffe7c6586d6f3f18a9246806a7c7d5538ab43e": "Simulierter Befehl:",
|
||||
"4e4c721129466be9c3862294dc40241b64045998": "Benutzerdefinierte Argumente verwenden",
|
||||
"ad2f8ac8b7de7945b80c8e424484da94e597125f": "Benutzerdefinierte Argumente",
|
||||
"a6911c2157f1b775284bbe9654ce5eb30cf45d7f": "Die URL muss nicht angegeben werden, sondern nur der Teil danach. Argumente werden mit zwei Kommata getrennt: ,,",
|
||||
"3a92a3443c65a52f37ca7efb8f453b35dbefbf29": "Benutzerdefinierte Ausgabe verwenden",
|
||||
"d9c02face477f2f9cdaae318ccee5f89856851fb": "Benutzerdefinierte Ausgabe",
|
||||
"fcfd4675b4c90f08d18d3abede9a9a4dff4cfdc7": "Dokumentation",
|
||||
"19d1ae64d94d28a29b2c57ae8671aace906b5401": "Der Pfad ist relativ zum Konfigurations-Download-Pfad. Dateiendung auslassen.",
|
||||
"8fad10737d3e3735a6699a4d89cbf6c20f6bb55f": "Authentifizierung verwenden",
|
||||
"08c74dc9762957593b91f6eb5d65efdfc975bf48": "Benutzername",
|
||||
"c32ef07f8803a223a83ed17024b38e8d82292407": "Passwort",
|
||||
"4a0dada6e841a425de3e5006e6a04df26c644fa5": "Audio",
|
||||
"9779715ac05308973d8f1c8658b29b986e92450f": "Ihre Audiodateien befinden sich hier",
|
||||
"47546e45bbb476baaaad38244db444c427ddc502": "Playlisten",
|
||||
"78bd81adb4609b68cfa4c589222bdc233ba1faaa": "Keine Wiedergabelisten verfügbar. Erstellen Sie eine aus Ihren heruntergeladenen Audiodateien, indem Sie auf das blaue Pluszeichen klicken.",
|
||||
"9d2b62bb0b91e2e17fb4177a7e3d6756a2e6ee33": "Video",
|
||||
"960582a8b9d7942716866ecfb7718309728f2916": "Ihre Videodateien befinden sich hier",
|
||||
"0f59c46ca29e9725898093c9ea6b586730d0624e": "Keine Playlisten verfügbar. Erstellen Sie eine aus heruntergeladenen Audiodateien, indem Sie auf das blaue Pluszeichen klicken.",
|
||||
"616e206cb4f25bd5885fc35925365e43cf5fb929": "Name:",
|
||||
"c52db455cca9109ee47e1a612c3f4117c09eb71b": "URL:",
|
||||
"c6eb45d085384903e53ab001a3513d1de6a1dbac": "Kanal:",
|
||||
"109c6f4a5e46efb933612ededfaf52a13178b7e0": "Dateigröße:",
|
||||
"bd630d8669b16e5f264ec4649d9b469fe03e5ff4": "Pfad:",
|
||||
"a67e7d843cef735c79d5ef1c8ba4af3e758912bb": "Hochgeladen am:",
|
||||
"f4e529ae5ffd73001d1ff4bbdeeb0a72e342e5c8": "Schließen",
|
||||
"ca3dbbc7f3e011bffe32a10a3ea45cc84f30ecf1": "ID:",
|
||||
"e684046d73bcee88e82f7ff01e2852789a05fc32": "Anzahl:",
|
||||
"321e4419a943044e674beb55b8039f42a9761ca5": "Info",
|
||||
"826b25211922a1b46436589233cb6f1a163d89b7": "Löschen",
|
||||
"34504b488c24c27e68089be549f0eeae6ebaf30b": "Löschen und zur Blacklist hinzufügen",
|
||||
"121cc5391cd2a5115bc2b3160379ee5b36cd7716": "Einstellungen",
|
||||
"801b98c6f02fe3b32f6afa3ee854c99ed83474e6": "URL",
|
||||
"54c512cca1923ab72faf1a0bd98d3d172469629a": "URL, über die auf diese Applikation zugegriffen wird, ohne Port.",
|
||||
"cb2741a46e3560f6bc6dfd99d385e86b08b26d72": "Port",
|
||||
"22e8f1d0423a3b784fe40fab187b92c06541b577": "Der gewünschte Port. Standard ist 17442.",
|
||||
"d4477669a560750d2064051a510ef4d7679e2f3e": "Multi-User Modus",
|
||||
"2eb03565fcdce7a7a67abc277a936a32fcf51557": "Benutzer Basispfad",
|
||||
"a64505c41150663968e277ec9b3ddaa5f4838798": "Basispfad für Benutzer und deren heruntergeladene Videos.",
|
||||
"cbe16a57be414e84b6a68309d08fad894df797d6": "Verschlüsselung verwenden",
|
||||
"0c1875a79b7ecc792cc1bebca3e063e40b5764f9": "Dateipfad zum Zertifikat",
|
||||
"736551b93461d2de64b118cf4043eee1d1c2cb2c": "Dateipfad zum Zertifikatsschlüssel",
|
||||
"4e3120311801c4acd18de7146add2ee4a4417773": "Abonnements erlauben",
|
||||
"4bee2a4bef2d26d37c9b353c278e24e5cd309ce3": "Abonnements Basispfad",
|
||||
"bc9892814ee2d119ae94378c905ea440a249b84a": "Basispfad für Videos von abonnierten Kanälen und Wiedergabelisten. Dieser ist relativ zum Stammordner von YTDL-Material.",
|
||||
"5bef4b25ba680da7fff06b86a91b1fc7e6a926e3": "Prüfintervall",
|
||||
"0f56a7449b77630c114615395bbda4cab398efd8": "Einheit ist Sekunden, nur Zahlen sind erlaubt.",
|
||||
"78e49b7339b4fa7184dd21bcaae107ce9b7076f6": "Youtube-DL Archiv verwenden",
|
||||
"fa9fe4255231dd1cc6b29d3d254a25cb7c764f0f": "Mit der Archivfunktion",
|
||||
"09006404cccc24b7a8f8d1ce0b39f2761ab841d8": "werden Informationen über Videos, welche durch ein Abonnement heruntergeladen wurden, in einem Textdokument festgehalten. Diese befinden sich in dem Archiv Unterverzeichnis vom Abonnementsordner.",
|
||||
"29ed79a98fc01e7f9537777598e31dbde3aa7981": "Dadurch können Videos permanent gelöscht werden, ohne das Abonnement beenden zu müssen. Außerdem kann dadurch aufgezeichnet werden, welche Videos heruntergeladen wurden. Z. B. im Falle eines Datenverlusts.",
|
||||
"27a56aad79d8b61269ed303f11664cc78bcc2522": "Design",
|
||||
"ff7cee38a2259526c519f878e71b964f41db4348": "Standard",
|
||||
"adb4562d2dbd3584370e44496969d58c511ecb63": "Dunkel",
|
||||
"7a6bacee4c31cb5c0ac2d24274fb4610d8858602": "Designänderung erlauben",
|
||||
"fe46ccaae902ce974e2441abe752399288298619": "Sprache",
|
||||
"82421c3e46a0453a70c42900eab51d58d79e6599": "Allgemein",
|
||||
"ab2756805742e84ad0cc0468f4be2d8aa9f855a5": "Audio Basispfad",
|
||||
"c2c89cdf45d46ea64d2ed2f9ac15dfa4d77e26ca": "Dateipfad für Audio-Downloads. Dieser ist relativ zum Stammordner von YTDL-Material.",
|
||||
"46826331da1949bd6fb74624447057099c9d20cd": "Video Basispfad",
|
||||
"17c92e6d47a213fa95b5aa344b3f258147123f93": "Dateipfad für Video-Downloads. Dieser ist relativ zum Stammordner von YTDL-Material.",
|
||||
"6b995e7130b4d667eaab6c5f61b362ace486d26d": "Globale benutzerdefinierte Argumente für Downloads auf der Startseite. Argumente werden durch zwei Kommata voneinander getrennt: ,,",
|
||||
"0ba25ad86a240576c4f20a2fada4722ebba77b1e": "Downloader",
|
||||
"61f8fd90b5f8cb20c70371feb2ee5e1fac5a9095": "Titel der Kopfzeile",
|
||||
"78d3531417c0d4ba4c90f0d4ae741edc261ec8df": "Dateimanager aktivieren",
|
||||
"a5a1be0a5df07de9eec57f5d2a86ed0204b2e75a": "Download-Manager aktivieren",
|
||||
"c33bd5392b39dbed36b8e5a1145163a15d45835f": "Qualitätsauswahl erlauben",
|
||||
"bda5508e24e0d77debb28bcd9194d8fefb1cfb92": "Nur Download Modus",
|
||||
"09d31c803a7252658694e1e3176b97f5655a3fe3": "Multi-Download Modus erlauben",
|
||||
"d8b47221b5af9e9e4cd5cb434d76fc0c91611409": "Einstellungen durch PIN schützen",
|
||||
"f5ec7b2cdf87d41154f4fcbc86e856314409dcb9": "Neuen PIN festlegen",
|
||||
"1c4dbce56d96b8974aac24a02f7ab2ee81415014": "Öffentliche API aktivieren",
|
||||
"23bd81dcc30b74d06279a26d7a42e8901c1b124e": "Öffentlicher API-Schlüssel",
|
||||
"41016a73d8ad85e6cb26dffa0a8fab9fe8f60d8e": "Dokumentation ansehen",
|
||||
"1b258b258b4cc475ceb2871305b61756b0134f4a": "Generieren",
|
||||
"d5d7c61349f3b0859336066e6d453fc35d334fe5": "YouTube API verwenden",
|
||||
"ce10d31febb3d9d60c160750570310f303a22c22": "Youtube API-Schlüssel",
|
||||
"8602e313cdfa7c4cc475ccbe86459fce3c3fd986": "Schlüsselgeneration ist einfach!",
|
||||
"9b3cedfa83c6d7acb3210953289d1be4aab115c7": "Hier klicken",
|
||||
"7f09776373995003161235c0c8d02b7f91dbc4df": "um die offizielle YoutubeDL-Material Chrome-Erweiterung manuell herunterzuladen.",
|
||||
"5b5296423906ab3371fdb2b5a5aaa83acaa2ee52": "Die Erweiterung muss manuell installiert werden und in den Einstellungen der Erweiterung muss die Frontend-URL eingetragen werden.",
|
||||
"9a2ec6da48771128384887525bdcac992632c863": "um die offizielle YoutubeDL-Material Firefox-Erweiterung direkt aus dem Firefox-Addon-Store zu installieren.",
|
||||
"eb81be6b49e195e5307811d1d08a19259d411f37": "Detaillierte Anleitung.",
|
||||
"cb17ff8fe3961cf90f44bee97c88a3f3347a7e55": "Die Frontend-URL muss in den Einstellungen der Erweiterung eingetragen werden.",
|
||||
"61b81b11aad0b9d970ece2fce18405f07eac69c2": "Der untenstehende Link muss nur in die Lesezeichenleiste gezogen werden. Auf einer unterstützten Webseite können Sie danach einfach auf das Lesezeichen klicken, um das Video herunterzuladen.",
|
||||
"c505d6c5de63cc700f0aaf8a4b31fae9e18024e5": "'Nur Audio' Lesezeichen generieren",
|
||||
"d5f69691f9f05711633128b5a3db696783266b58": "Extra",
|
||||
"5fab47f146b0a4b809dcebf3db9da94df6299ea1": "Standard Download-Agent verwenden",
|
||||
"ec71e08aee647ea4a71fd6b7510c54d84a797ca6": "Downloader auswählen",
|
||||
"dc3d990391c944d1fbfc7cfb402f7b5e112fb3a8": "Erweiterte Download-Optionen aktivieren",
|
||||
"bc2e854e111ecf2bd7db170da5e3c2ed08181d88": "Erweitert",
|
||||
"37224420db54d4bc7696f157b779a7225f03ca9d": "Benutzerregistrierung zulassen",
|
||||
"4d13a9cd5ed3dcee0eab22cb25198d43886942be": "Benutzer",
|
||||
"52c9a103b812f258bcddc3d90a6e3f46871d25fe": "Speichern",
|
||||
"fe8fd36dbf5deee1d56564965787a782a66eba44": "{VAR_SELECT, select, true {Schließen} false {Abbrechen} other {Andere}}",
|
||||
"cec82c0a545f37420d55a9b6c45c20546e82f94e": "Über YoutubeDL-Material",
|
||||
"199c17e5d6a419313af3c325f06dcbb9645ca618": "ist ein Open-Source YouTube-Downloader, der nach den Material-Design-Richtlinien von Google erstellt wurde. Sie können Ihre Lieblingsvideos reibungslos als Video- oder Audiodateien herunterladen und sogar Ihre Lieblingskanäle und Wiedergabelisten abonnieren, um auf dem Laufenden zu bleiben.",
|
||||
"bc0ad0ee6630acb7fcb7802ec79f5a0ee943c1a7": "beinhaltet viele tolle Funktionen! API, Docker und Lokalisierung werden unter anderem unterstützt. Informieren Sie sich über alle unterstützten Funktionen auf Github.",
|
||||
"a45e3b05f0529dc5246d70ef62304c94426d4c81": "Installierte Version:",
|
||||
"e22f3a5351944f3a1a10cfc7da6f65dfbe0037fe": "Suche nach Updates ...",
|
||||
"a16e92385b4fd9677bb830a4b796b8b79c113290": "Aktualisierung verfügbar",
|
||||
"189b28aaa19b3c51c6111ad039c4fd5e2a22e370": "Sie können über das Einstellungsmenü aktualisieren.",
|
||||
"b33536f59b94ec935a16bd6869d836895dc5300c": "Haben Sie einen Fehler gefunden oder einen Vorschlag?",
|
||||
"e1f398f38ff1534303d4bb80bd6cece245f24016": "um ein Ticket zu öffnen.",
|
||||
"42ff677ec14f111e88bd6cdd30145378e994d1bf": "Ihr Profil",
|
||||
"ac9d09de42edca1296371e4d801349c9096ac8de": "UID:",
|
||||
"a5ed099ffc9e96f6970df843289ade8a7d20ab9f": "Erstellt:",
|
||||
"fa96f2137af0a24e6d6d54c598c0af7d5d5ad344": "Sie sind nicht angemeldet.",
|
||||
"6765b4c916060f6bc42d9bb69e80377dbcb5e4e9": "Anmelden",
|
||||
"bb694b49d408265c91c62799c2b3a7e3151c824d": "Ausloggen",
|
||||
"a1dbca87b9f36d2b06a5cbcffb5814c4ae9b798a": "Admin-Konto erstellen",
|
||||
"2d2adf3ca26a676bca2269295b7455a26fd26980": "Es wurde kein Standard-Administratorkonto erkannt. Ein Administratorkonto mit dem Benutzernamen \"admin\" wird erstellt und ein Passwort wird festgelegt.",
|
||||
"70a67e04629f6d412db0a12d51820b480788d795": "Erstellen",
|
||||
"994363f08f9fbfa3b3994ff7b35c6904fdff18d8": "Profil",
|
||||
"004b222ff9ef9dd4771b777950ca1d0e4cd4348a": "Über",
|
||||
"92eee6be6de0b11c924e3ab27db30257159c0a7c": "Startseite",
|
||||
"357064ca9d9ac859eb618e28e8126fa32be049e2": "Abonnements",
|
||||
"822fab38216f64e8166d368b59fe756ca39d301b": "Downloads",
|
||||
"a249a5ae13e0835383885aaf697d2890cc3e53e9": "Playlist teilen",
|
||||
"15da89490e04496ca9ea1e1b3d44fb5efd4a75d9": "Video teilen",
|
||||
"1d540dcd271b316545d070f9d182c372d923aadd": "Audio teilen",
|
||||
"1f6d14a780a37a97899dc611881e6bc971268285": "Freigabe aktivieren",
|
||||
"6580b6a950d952df847cb3d8e7176720a740adc8": "Zeitstempel verwenden",
|
||||
"4f2ed9e71a7c981db3e50ae2fedb28aff2ec4e6c": "Sekunden",
|
||||
"3a6e5a6aa78ca864f6542410c5dafb6334538106": "In die Zwischenablage kopieren",
|
||||
"5b3075e8dc3f3921ec316b0bd83b6d14a06c1a4f": "Änderungen speichern",
|
||||
"4f8b2bb476981727ab34ed40fde1218361f92c45": "Details",
|
||||
"e9aff8e6df2e2bf6299ea27bb2894c70bc48bd4d": "Ein Fehler ist aufgetreten:",
|
||||
"77b0c73840665945b25bd128709aa64c8f017e1c": "Download Start:",
|
||||
"08ff9375ec078065bcdd7637b7ea65fce2979266": "Download Ende:",
|
||||
"ad127117f9471612f47d01eae09709da444a36a4": "Dateipfad(e):",
|
||||
"a9806cf78ce00eb2613eeca11354a97e033377b8": "Abonnieren Sie eine Playlist oder einen Kanal",
|
||||
"93efc99ae087fc116de708ecd3ace86ca237cf30": "Playlist oder Kanal URL",
|
||||
"08f5d0ef937ae17feb1b04aff15ad88911e87baf": "Benutzerdefinierter Name",
|
||||
"f3f62aa84d59f3a8b900cc9a7eec3ef279a7b4e7": "Dies ist optional",
|
||||
"ea30873bd3f0d5e4fb2378eec3f0a1db77634a28": "Alle Uploads herunterladen",
|
||||
"28a678e9cabf86e44c32594c43fa0e890135c20f": "Videos herunterladen aus den letzten",
|
||||
"408ca4911457e84a348cecf214f02c69289aa8f1": "Nur Streaming Modus",
|
||||
"d0336848b0c375a1c25ba369b3481ee383217a4f": "Abonnieren",
|
||||
"e78c0d60ac39787f62c9159646fe0b3c1ed55a1d": "Typ:",
|
||||
"a44d86aa1e6c20ced07aca3a7c081d8db9ded1c6": "Archiv:",
|
||||
"8efc77bf327659c0fec1f518cf48a98cdcd9dddf": "Archiv exportieren",
|
||||
"3042bd3ad8dffcfeca5fd1ae6159fd1047434e95": "Deabonnieren",
|
||||
"e2319dec5b4ccfb6ed9f55ccabd63650a8fdf547": "Ihre Abonnements",
|
||||
"807cf11e6ac1cde912496f764c176bdfdd6b7e19": "Kanäle",
|
||||
"29b89f751593e1b347eef103891b7a1ff36ec03f": "Name nicht verfügbar. Kanal wird abgerufen...",
|
||||
"4636cd4a1379c50d471e98786098c4d39e1e82ad": "Sie haben keine Kanäle abonniert.",
|
||||
"2e0a410652cb07d069f576b61eab32586a18320d": "Name nicht verfügbar. Playlist wird abgerufen...",
|
||||
"587b57ced54965d8874c3fd0e9dfedb987e5df04": "Sie haben keine Playlisten abonniert.",
|
||||
"7e892ba15f2c6c17e83510e273b3e10fc32ea016": "Suchen",
|
||||
"2054791b822475aeaea95c0119113de3200f5e1c": "Länge:",
|
||||
"94e01842dcee90531caa52e4147f70679bac87fe": "Löschen und erneut herunterladen",
|
||||
"2031adb51e07a41844e8ba7704b054e98345c9c1": "Permanent löschen",
|
||||
"91ecce65f1d23f9419d1c953cd6b7bc7f91c110e": "Updater",
|
||||
"1372e61c5bd06100844bd43b98b016aabc468f62": "Wählen Sie eine Version:",
|
||||
"cfc2f436ec2beffb042e7511a73c89c372e86a6c": "Registrieren",
|
||||
"a1ad8b1be9be43b5183bd2c3186d4e19496f2a0b": "Sitzungs-ID:",
|
||||
"eb98135e35af26a9a326ee69bd8ff104d36dd8ec": "(aktuell)",
|
||||
"7117fc42f860e86d983bfccfcf2654e5750f3406": "Zurzeit sind keine Downloads verfügbar.",
|
||||
"b7ff2e2b909c53abe088fe60b9f4b6ac7757247f": "Nutzer registrieren",
|
||||
"024886ca34a6f309e3e51c2ed849320592c3faaa": "Benutzername",
|
||||
"2bd201aea09e43fbfd3cd15ec0499b6755302329": "Benutzer verwalten",
|
||||
"29c97c8e76763bb15b6d515648fa5bd1eb0f7510": "Benutzer-UID",
|
||||
"e70e209561583f360b1e9cefd2cbb1fe434b6229": "Neues Passwort",
|
||||
"6498fa1b8f563988f769654a75411bb8060134b9": "Neues Passwort festlegen",
|
||||
"40da072004086c9ec00d125165da91eaade7f541": "Standard verwenden",
|
||||
"4f20f2d5a6882190892e58b85f6ccbedfa737952": "Ja",
|
||||
"3d3ae7deebc5949b0c1c78b9847886a94321d9fd": "Nein",
|
||||
"57c6c05d8ebf4ef1180c2705033c044f655bb2c4": "Rolle verwalten",
|
||||
"746f64ddd9001ac456327cd9a3d5152203a4b93c": "Benutzername",
|
||||
"52c1447c1ec9570a2a3025c7e566557b8d19ed92": "Rolle",
|
||||
"59a8c38db3091a63ac1cb9590188dc3a972acfb3": "Aktionen",
|
||||
"4d92a0395dd66778a931460118626c5794a3fc7a": "Benutzer hinzufügen",
|
||||
"b0d7dd8a1b0349622d6e0c6e643e24a9ea0efa1d": "Rolle bearbeiten"
|
||||
}
|
||||
@@ -1,200 +0,0 @@
|
||||
{
|
||||
"ccc7e92cbdd35e901acf9ad80941abee07bd8f60": "No es necesario incluir URL, solo todo después ",
|
||||
"f41145afc02fd47ef0576ac79acd2c47ebbf4901": "Argumentos personalizados globales para descargas en la página de inicio.",
|
||||
"17f0ea5d2d7a262b0e875acc70475f102aee84e6": "Crea una lista de reproducción",
|
||||
"cff1428d10d59d14e45edec3c735a27b5482db59": "Nombre",
|
||||
"f47e2d56dd8a145b2e9599da9730c049d52962a2": "Archivos de sonido",
|
||||
"a52dae09be10ca3a65da918533ced3d3f4992238": "Archivos de video",
|
||||
"d9e83ac17026e70ef6e9c0f3240a3b2450367f40": "Modificar args de youtube-dl",
|
||||
"7fc1946abe2b40f60059c6cd19975d677095fd19": "Args nuevos simulados",
|
||||
"0b71824ae71972f236039bed43f8d2323e8fd570": "Añadir un arg",
|
||||
"c8b0e59eb491f2ac7505f0fbab747062e6b32b23": "Busqueda por categoria",
|
||||
"9eeb91caef5a50256dd87e1c4b7b3e8216479377": "Usar valor de arg",
|
||||
"25d8ad5eba2ec24e68295a27d6a4bb9b49e3dacd": "Valor de arg",
|
||||
"7de2451ed3fb8d8b847979bd3f0c740b970f167b": "Añadir arg",
|
||||
"d7b35c384aecd25a516200d6921836374613dfe7": "Cancelar",
|
||||
"b2623aee44b70c9a4ba1fce16c8a593b0a4c7974": "Modificar",
|
||||
"038ebcb2a89155d90c24fa1c17bfe83dbadc3c20": "Descargador de Youtube",
|
||||
"6d2ec8898344c8955542b0542c942038ef28bb80": "Por favor entre una URL válida",
|
||||
"a38ae1082fec79ba1f379978337385a539a28e73": " Calidad ",
|
||||
"4be966a9dcfbc9b54dfcc604b831c0289f847fa4": "Usa URL",
|
||||
"d3f02f845e62cebd75fde451ab8479d2a8ad784d": " Ver ",
|
||||
"4a9889d36910edc8323d7bab60858ab3da6d91df": "Solo audio",
|
||||
"96a01fafe135afc58b0f8071a4ab00234495ce18": "Descarga múltiple",
|
||||
"6a21ba5fb0ac804a525bf9ab168038c3ee88e661": "Descarga",
|
||||
"6a3777f913cf3f288664f0632b9f24794fdcc24e": "Cancelar",
|
||||
"322ed150e02666fe2259c5b4614eac7066f4ffa0": "Avanzado",
|
||||
"b7ffe7c6586d6f3f18a9246806a7c7d5538ab43e": "Commando simulado:",
|
||||
"4e4c721129466be9c3862294dc40241b64045998": "Usar argumentos personalizados",
|
||||
"ad2f8ac8b7de7945b80c8e424484da94e597125f": "Argumentos personalizados",
|
||||
"a6911c2157f1b775284bbe9654ce5eb30cf45d7f": "No es necesario incluir URL, solo todo después. Los argumentos se delimitan usando dos comas así: ,,",
|
||||
"3a92a3443c65a52f37ca7efb8f453b35dbefbf29": "Usar salida personalizada",
|
||||
"d9c02face477f2f9cdaae318ccee5f89856851fb": "Salida personalizada",
|
||||
"fcfd4675b4c90f08d18d3abede9a9a4dff4cfdc7": "Documentación",
|
||||
"19d1ae64d94d28a29b2c57ae8671aace906b5401": "La ruta es relativa a la ruta de descarga de la config. No incluya el extensión.",
|
||||
"8fad10737d3e3735a6699a4d89cbf6c20f6bb55f": "Usa autenticación",
|
||||
"08c74dc9762957593b91f6eb5d65efdfc975bf48": "Nombre de usuario",
|
||||
"c32ef07f8803a223a83ed17024b38e8d82292407": "Contraseña",
|
||||
"4a0dada6e841a425de3e5006e6a04df26c644fa5": "Audio",
|
||||
"9779715ac05308973d8f1c8658b29b986e92450f": "Tus archivos de audio están aquí",
|
||||
"47546e45bbb476baaaad38244db444c427ddc502": "Listas de reproducción",
|
||||
"78bd81adb4609b68cfa4c589222bdc233ba1faaa": "No hay listas de reproducción disponibles. Cree uno de tus archivos de audio haciendo clic en el botón azul más.",
|
||||
"9d2b62bb0b91e2e17fb4177a7e3d6756a2e6ee33": "Vídeo",
|
||||
"960582a8b9d7942716866ecfb7718309728f2916": "Tus archivos de video son aquí",
|
||||
"0f59c46ca29e9725898093c9ea6b586730d0624e": "No hay listas de reproducción disponibles. Cree uno de tus archivos de video haciendo clic en el botón azul más.",
|
||||
"616e206cb4f25bd5885fc35925365e43cf5fb929": "Nombre:",
|
||||
"c52db455cca9109ee47e1a612c3f4117c09eb71b": "URL:",
|
||||
"c6eb45d085384903e53ab001a3513d1de6a1dbac": "Cargador:",
|
||||
"109c6f4a5e46efb933612ededfaf52a13178b7e0": "Tamaño del archivo:",
|
||||
"bd630d8669b16e5f264ec4649d9b469fe03e5ff4": "Ruta:",
|
||||
"a67e7d843cef735c79d5ef1c8ba4af3e758912bb": "Subido:",
|
||||
"f4e529ae5ffd73001d1ff4bbdeeb0a72e342e5c8": "Cerca",
|
||||
"ca3dbbc7f3e011bffe32a10a3ea45cc84f30ecf1": "ID:",
|
||||
"e684046d73bcee88e82f7ff01e2852789a05fc32": "Cuenta:",
|
||||
"321e4419a943044e674beb55b8039f42a9761ca5": "Información",
|
||||
"826b25211922a1b46436589233cb6f1a163d89b7": "Eliminar",
|
||||
"34504b488c24c27e68089be549f0eeae6ebaf30b": "Eliminar y pones en la lista negra",
|
||||
"121cc5391cd2a5115bc2b3160379ee5b36cd7716": "Configuraciones",
|
||||
"801b98c6f02fe3b32f6afa3ee854c99ed83474e6": "URL",
|
||||
"54c512cca1923ab72faf1a0bd98d3d172469629a": "URL desde la que se accederá a esta aplicación, sin el puerto.",
|
||||
"cb2741a46e3560f6bc6dfd99d385e86b08b26d72": "Puerto",
|
||||
"22e8f1d0423a3b784fe40fab187b92c06541b577": "Puerto deseado. El valor predeterminado es 17442.",
|
||||
"d4477669a560750d2064051a510ef4d7679e2f3e": "Modo multiusuario",
|
||||
"2eb03565fcdce7a7a67abc277a936a32fcf51557": "Ruta base de usuarios",
|
||||
"a64505c41150663968e277ec9b3ddaa5f4838798": "Ruta base para los usuarios y sus videos descargados.",
|
||||
"cbe16a57be414e84b6a68309d08fad894df797d6": "Usa cifrado",
|
||||
"0c1875a79b7ecc792cc1bebca3e063e40b5764f9": "Ruta del archivo de certificado",
|
||||
"736551b93461d2de64b118cf4043eee1d1c2cb2c": "Ruta de archivo de clave",
|
||||
"4e3120311801c4acd18de7146add2ee4a4417773": "Permitir suscripciones",
|
||||
"4bee2a4bef2d26d37c9b353c278e24e5cd309ce3": "Ruta base de suscripciones",
|
||||
"bc9892814ee2d119ae94378c905ea440a249b84a": "Ruta base para videos de sus canales y listas de reproducción suscritos. Es relativo a la carpeta raíz de YTDL-Material.",
|
||||
"5bef4b25ba680da7fff06b86a91b1fc7e6a926e3": "Intervalo de comprobación",
|
||||
"0f56a7449b77630c114615395bbda4cab398efd8": "La unidad es segundos, solo incluye números.",
|
||||
"78e49b7339b4fa7184dd21bcaae107ce9b7076f6": "Usa el archivo de youtube-dl",
|
||||
"fa9fe4255231dd1cc6b29d3d254a25cb7c764f0f": "Con la función de archivo de youtube-dl,",
|
||||
"09006404cccc24b7a8f8d1ce0b39f2761ab841d8": "los videos descargados de sus suscripciones se graban en un archivo de texto en el subdirectorio del archivo de suscripciones.",
|
||||
"29ed79a98fc01e7f9537777598e31dbde3aa7981": "Esto permite eliminar videos de sus suscripciones de forma permanente sin darse de baja y le permite grabar los videos que descargó en caso de pérdida de datos.",
|
||||
"27a56aad79d8b61269ed303f11664cc78bcc2522": "Tema",
|
||||
"ff7cee38a2259526c519f878e71b964f41db4348": "Defecto",
|
||||
"adb4562d2dbd3584370e44496969d58c511ecb63": "Oscura",
|
||||
"7a6bacee4c31cb5c0ac2d24274fb4610d8858602": "Permitir cambio de tema",
|
||||
"fe46ccaae902ce974e2441abe752399288298619": "Idioma",
|
||||
"82421c3e46a0453a70c42900eab51d58d79e6599": "Principal",
|
||||
"ab2756805742e84ad0cc0468f4be2d8aa9f855a5": "Ruta de la carpeta de audio",
|
||||
"c2c89cdf45d46ea64d2ed2f9ac15dfa4d77e26ca": "Ruta para descargas de solo audio. Es relativo a la carpeta raíz de YTDL-Material.",
|
||||
"46826331da1949bd6fb74624447057099c9d20cd": "Ruta de la carpeta de video",
|
||||
"17c92e6d47a213fa95b5aa344b3f258147123f93": "Ruta de descarga de videos. Es relativo a la carpeta raíz de YTDL-Material.",
|
||||
"6b995e7130b4d667eaab6c5f61b362ace486d26d": "Argumentos personalizados globales para descargas en la página de inicio. Los argumentos se delimitan usando dos comas así: ,,",
|
||||
"0ba25ad86a240576c4f20a2fada4722ebba77b1e": "Descargador",
|
||||
"61f8fd90b5f8cb20c70371feb2ee5e1fac5a9095": "Título superior",
|
||||
"78d3531417c0d4ba4c90f0d4ae741edc261ec8df": "Administrador de archivos habilitado",
|
||||
"a5a1be0a5df07de9eec57f5d2a86ed0204b2e75a": "Administrador de descargas habilitado",
|
||||
"c33bd5392b39dbed36b8e5a1145163a15d45835f": "Permitir selección de calidad",
|
||||
"bda5508e24e0d77debb28bcd9194d8fefb1cfb92": "Modo de solo descarga",
|
||||
"09d31c803a7252658694e1e3176b97f5655a3fe3": "Permitir el modo de descarga múltiple",
|
||||
"d8b47221b5af9e9e4cd5cb434d76fc0c91611409": "Requiere pin para la configuración",
|
||||
"f5ec7b2cdf87d41154f4fcbc86e856314409dcb9": "Establecer nuevo pin",
|
||||
"1c4dbce56d96b8974aac24a02f7ab2ee81415014": "Habilitar API pública",
|
||||
"23bd81dcc30b74d06279a26d7a42e8901c1b124e": "Clave API pública",
|
||||
"41016a73d8ad85e6cb26dffa0a8fab9fe8f60d8e": "Ver documentación",
|
||||
"1b258b258b4cc475ceb2871305b61756b0134f4a": "Generar",
|
||||
"d5d7c61349f3b0859336066e6d453fc35d334fe5": "Utilizar la API de YouTube",
|
||||
"ce10d31febb3d9d60c160750570310f303a22c22": "Clave API de YouTube",
|
||||
"8602e313cdfa7c4cc475ccbe86459fce3c3fd986": "¡Generar una clave es fácil!",
|
||||
"9b3cedfa83c6d7acb3210953289d1be4aab115c7": "¡Haga clic aquí",
|
||||
"7f09776373995003161235c0c8d02b7f91dbc4df": "para descargar la extensión Chrome oficial de YoutubeDL-Material manualmente.",
|
||||
"5b5296423906ab3371fdb2b5a5aaa83acaa2ee52": "Debe cargar manualmente la extensión y modificar la configuración de la extensión para establecer la URL de la interfaz.",
|
||||
"9a2ec6da48771128384887525bdcac992632c863": "para instalar la extensión Firefox oficial de YoutubeDL-Material directamente desde la página de extensiones de Firefox.",
|
||||
"eb81be6b49e195e5307811d1d08a19259d411f37": "Instrucciones detalladas de configuración.",
|
||||
"cb17ff8fe3961cf90f44bee97c88a3f3347a7e55": "No se requiere mucho más que cambiar la configuración de la extensión para establecer la URL de la interfaz.",
|
||||
"61b81b11aad0b9d970ece2fce18405f07eac69c2": "Arrastra el enlace de abajo a tus marcadores, ¡y listo! Simplemente navegue hasta el video de YouTube que desea descargar y haga clic en el marcador.",
|
||||
"c505d6c5de63cc700f0aaf8a4b31fae9e18024e5": "Generar bookmarklet solo de audio",
|
||||
"d5f69691f9f05711633128b5a3db696783266b58": "Extra",
|
||||
"5fab47f146b0a4b809dcebf3db9da94df6299ea1": "Usar agente de descarga predeterminado",
|
||||
"ec71e08aee647ea4a71fd6b7510c54d84a797ca6": "Seleccione un descargador",
|
||||
"dc3d990391c944d1fbfc7cfb402f7b5e112fb3a8": "Permitir descarga avanzada",
|
||||
"bc2e854e111ecf2bd7db170da5e3c2ed08181d88": "Avanzado",
|
||||
"37224420db54d4bc7696f157b779a7225f03ca9d": "Permitir registro de usuario",
|
||||
"4d13a9cd5ed3dcee0eab22cb25198d43886942be": "Usuarios",
|
||||
"52c9a103b812f258bcddc3d90a6e3f46871d25fe": "Salvar",
|
||||
"fe8fd36dbf5deee1d56564965787a782a66eba44": "{VAR_SELECT, select, true {Cerrar} false {Cancelar} other {Otro} }",
|
||||
"cec82c0a545f37420d55a9b6c45c20546e82f94e": "Sobre YoutubeDL-Material",
|
||||
"199c17e5d6a419313af3c325f06dcbb9645ca618": "es un descargador de código abierto de YouTube creado bajo las especificaciones de \"Material Design\" de Google. Puede descargar sin problemas sus videos favoritos como archivos de video o audio, e incluso suscribirse a sus canales favoritos y listas de reproducción para mantenerse actualizado con sus nuevos videos.",
|
||||
"bc0ad0ee6630acb7fcb7802ec79f5a0ee943c1a7": "tiene algunas características increíbles incluidas! Una amplia API, soporte de Docker y soporte de localización (traducción). Lea todas las funciones compatibles haciendo clic en el icono de GitHub que se encuentra arriba.",
|
||||
"a45e3b05f0529dc5246d70ef62304c94426d4c81": "Versión instalada:",
|
||||
"e22f3a5351944f3a1a10cfc7da6f65dfbe0037fe": "Comprobando actualizaciones...",
|
||||
"a16e92385b4fd9677bb830a4b796b8b79c113290": "Actualización disponible",
|
||||
"189b28aaa19b3c51c6111ad039c4fd5e2a22e370": "Puede actualizar desde el menú de configuración.",
|
||||
"b33536f59b94ec935a16bd6869d836895dc5300c": "¿Encontró un error o tiene una sugerencia?",
|
||||
"e1f398f38ff1534303d4bb80bd6cece245f24016": "para crear una cuestión!",
|
||||
"42ff677ec14f111e88bd6cdd30145378e994d1bf": "Tu perfil",
|
||||
"ac9d09de42edca1296371e4d801349c9096ac8de": "UID:",
|
||||
"a5ed099ffc9e96f6970df843289ade8a7d20ab9f": "Creado:",
|
||||
"fa96f2137af0a24e6d6d54c598c0af7d5d5ad344": "Usted no se ha identificado.",
|
||||
"6765b4c916060f6bc42d9bb69e80377dbcb5e4e9": "Identificarse",
|
||||
"bb694b49d408265c91c62799c2b3a7e3151c824d": "Salir",
|
||||
"a1dbca87b9f36d2b06a5cbcffb5814c4ae9b798a": "Crear cuenta de administrador",
|
||||
"2d2adf3ca26a676bca2269295b7455a26fd26980": "No se detectó una cuenta de administrador predeterminada. Esto creará y establecerá la contraseña para una cuenta de administrador con el nombre de usuario como 'admin'.",
|
||||
"70a67e04629f6d412db0a12d51820b480788d795": "Crear",
|
||||
"994363f08f9fbfa3b3994ff7b35c6904fdff18d8": "Perfil",
|
||||
"004b222ff9ef9dd4771b777950ca1d0e4cd4348a": "Sobre",
|
||||
"92eee6be6de0b11c924e3ab27db30257159c0a7c": "Inicio",
|
||||
"357064ca9d9ac859eb618e28e8126fa32be049e2": "Suscripciones",
|
||||
"822fab38216f64e8166d368b59fe756ca39d301b": "Descargas",
|
||||
"a249a5ae13e0835383885aaf697d2890cc3e53e9": "Compartir lista de reproducción",
|
||||
"15da89490e04496ca9ea1e1b3d44fb5efd4a75d9": "Compartir vídeo",
|
||||
"1d540dcd271b316545d070f9d182c372d923aadd": "Compartir audio",
|
||||
"1f6d14a780a37a97899dc611881e6bc971268285": "Habilitar compartir",
|
||||
"6580b6a950d952df847cb3d8e7176720a740adc8": "Usar marca de tiempo",
|
||||
"4f2ed9e71a7c981db3e50ae2fedb28aff2ec4e6c": "Segundos",
|
||||
"3a6e5a6aa78ca864f6542410c5dafb6334538106": "Copiar al Portapapeles",
|
||||
"5b3075e8dc3f3921ec316b0bd83b6d14a06c1a4f": "Guardar cambios",
|
||||
"4f8b2bb476981727ab34ed40fde1218361f92c45": "Detalles",
|
||||
"383000ab16bf415d5a1d61d7eb7b5959c72a9515": "Se ha producido un error:",
|
||||
"77b0c73840665945b25bd128709aa64c8f017e1c": "Inicio de descarga:",
|
||||
"08ff9375ec078065bcdd7637b7ea65fce2979266": "Fin de descarga:",
|
||||
"ad127117f9471612f47d01eae09709da444a36a4": "Ruta(s) del archivo:",
|
||||
"a9806cf78ce00eb2613eeca11354a97e033377b8": "Suscríbase a la lista de reproducción o al canal",
|
||||
"93efc99ae087fc116de708ecd3ace86ca237cf30": "La lista de reproducción o la URL del canal",
|
||||
"08f5d0ef937ae17feb1b04aff15ad88911e87baf": "Nombre personalizado",
|
||||
"f3f62aa84d59f3a8b900cc9a7eec3ef279a7b4e7": "Esto es opcional",
|
||||
"ea30873bd3f0d5e4fb2378eec3f0a1db77634a28": "Descargar todas las cargas",
|
||||
"28a678e9cabf86e44c32594c43fa0e890135c20f": "Descargar videos subidos en el último",
|
||||
"408ca4911457e84a348cecf214f02c69289aa8f1": "Modo de solo transmisión",
|
||||
"d0336848b0c375a1c25ba369b3481ee383217a4f": "Subscribe",
|
||||
"e78c0d60ac39787f62c9159646fe0b3c1ed55a1d": "Tipo:",
|
||||
"a44d86aa1e6c20ced07aca3a7c081d8db9ded1c6": "Archivo:",
|
||||
"8efc77bf327659c0fec1f518cf48a98cdcd9dddf": "Exportar el archivo",
|
||||
"3042bd3ad8dffcfeca5fd1ae6159fd1047434e95": "Darse de baja",
|
||||
"e2319dec5b4ccfb6ed9f55ccabd63650a8fdf547": "Sus suscripciones",
|
||||
"807cf11e6ac1cde912496f764c176bdfdd6b7e19": "Canales",
|
||||
"29b89f751593e1b347eef103891b7a1ff36ec03f": "Nombre no disponible. Recuperación de canales en progreso.",
|
||||
"4636cd4a1379c50d471e98786098c4d39e1e82ad": "No tienes suscripciones de canal.",
|
||||
"2e0a410652cb07d069f576b61eab32586a18320d": "Nombre no disponible. Recuperación de listas de reproducción en progreso.",
|
||||
"587b57ced54965d8874c3fd0e9dfedb987e5df04": "No tienes suscripciones a listas de reproducción.",
|
||||
"7e892ba15f2c6c17e83510e273b3e10fc32ea016": "Buscar",
|
||||
"2054791b822475aeaea95c0119113de3200f5e1c": "Duración:",
|
||||
"94e01842dcee90531caa52e4147f70679bac87fe": "Eliminar y volver a descargar",
|
||||
"2031adb51e07a41844e8ba7704b054e98345c9c1": "Borrar para siempre",
|
||||
"91ecce65f1d23f9419d1c953cd6b7bc7f91c110e": "Updater",
|
||||
"1372e61c5bd06100844bd43b98b016aabc468f62": "Seleccione una versión:",
|
||||
"cfc2f436ec2beffb042e7511a73c89c372e86a6c": "Registrarse",
|
||||
"a1ad8b1be9be43b5183bd2c3186d4e19496f2a0b": "ID de sesión:",
|
||||
"eb98135e35af26a9a326ee69bd8ff104d36dd8ec": "(actual)",
|
||||
"7117fc42f860e86d983bfccfcf2654e5750f3406": "¡No hay descargas disponibles!",
|
||||
"b7ff2e2b909c53abe088fe60b9f4b6ac7757247f": "Registrar un usuario",
|
||||
"024886ca34a6f309e3e51c2ed849320592c3faaa": "Nombre de usuario",
|
||||
"2bd201aea09e43fbfd3cd15ec0499b6755302329": "Administrar usuario",
|
||||
"29c97c8e76763bb15b6d515648fa5bd1eb0f7510": "UID de usuario:",
|
||||
"e70e209561583f360b1e9cefd2cbb1fe434b6229": "Nueva contraseña",
|
||||
"6498fa1b8f563988f769654a75411bb8060134b9": "Establecer nueva contraseña",
|
||||
"40da072004086c9ec00d125165da91eaade7f541": "Uso por defecto",
|
||||
"4f20f2d5a6882190892e58b85f6ccbedfa737952": "Si",
|
||||
"3d3ae7deebc5949b0c1c78b9847886a94321d9fd": "No",
|
||||
"57c6c05d8ebf4ef1180c2705033c044f655bb2c4": "Gestionar rol",
|
||||
"746f64ddd9001ac456327cd9a3d5152203a4b93c": " Nombre de usuario ",
|
||||
"52c1447c1ec9570a2a3025c7e566557b8d19ed92": " Rol ",
|
||||
"59a8c38db3091a63ac1cb9590188dc3a972acfb3": " Acciones ",
|
||||
"4d92a0395dd66778a931460118626c5794a3fc7a": "Agregar Usuarios",
|
||||
"b0d7dd8a1b0349622d6e0c6e643e24a9ea0efa1d": "Editar Rol"
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 2.6 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 6.1 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 2.3 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 19 KiB |
@@ -1,18 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>YoutubeDLMaterial</title>
|
||||
<base href="./">
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
||||
<link href="https://fonts.googleapis.com/css?family=Roboto&display=swap" rel="stylesheet">
|
||||
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
||||
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
|
||||
<link href="https://fonts.googleapis.com/icon?family=Material+Icons|Material+Icons+Outlined" rel="stylesheet"/>
|
||||
<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>
|
||||
</html>
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +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()}([]);
|
||||
@@ -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()}([]);
|
||||
File diff suppressed because one or more lines are too long
@@ -40,9 +40,9 @@ async function subscribe(sub, user_uid = null) {
|
||||
else
|
||||
url_exists = !!db.get('subscriptions').find({url: sub.url}).value();
|
||||
|
||||
if (url_exists) {
|
||||
logger.info('Sub already exists');
|
||||
result_obj.error = 'Subcription with URL ' + sub.url + ' already exists!';
|
||||
if (!sub.name && url_exists) {
|
||||
logger.error(`Sub with the same URL "${sub.url}" already exists -- please provide a custom name for this new subscription.`);
|
||||
result_obj.error = 'Subcription with URL ' + sub.url + ' already exists! Custom name is required.';
|
||||
resolve(result_obj);
|
||||
return;
|
||||
}
|
||||
@@ -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);
|
||||
@@ -115,7 +123,7 @@ async function getSubscriptionInfo(sub, user_uid = null) {
|
||||
}
|
||||
}
|
||||
|
||||
const useArchive = config_api.getConfigItem('ytdl_subscriptions_use_youtubedl_archive');
|
||||
const useArchive = config_api.getConfigItem('ytdl_use_youtubedl_archive');
|
||||
if (useArchive && !sub.archive) {
|
||||
// must create the archive
|
||||
const archive_dir = path.join(__dirname, basePath, 'archives', sub.name);
|
||||
@@ -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)) {
|
||||
@@ -184,7 +197,7 @@ async function deleteSubscriptionFile(sub, file, deleteForever, file_uid = null,
|
||||
basePath = config_api.getConfigItem('ytdl_subscriptions_base_path');
|
||||
sub_db = db.get('subscriptions').find({id: sub.id});
|
||||
}
|
||||
const useArchive = config_api.getConfigItem('ytdl_subscriptions_use_youtubedl_archive');
|
||||
const useArchive = config_api.getConfigItem('ytdl_use_youtubedl_archive');
|
||||
const appendedBasePath = getAppendedBasePath(sub, basePath);
|
||||
const name = file;
|
||||
let retrievedID = null;
|
||||
@@ -260,7 +273,7 @@ async function getVideosForSub(sub, user_uid = null) {
|
||||
else
|
||||
basePath = config_api.getConfigItem('ytdl_subscriptions_base_path');
|
||||
|
||||
const useArchive = config_api.getConfigItem('ytdl_subscriptions_use_youtubedl_archive');
|
||||
const useArchive = config_api.getConfigItem('ytdl_use_youtubedl_archive');
|
||||
|
||||
let appendedBasePath = null
|
||||
appendedBasePath = getAppendedBasePath(sub, basePath);
|
||||
@@ -332,6 +345,10 @@ async function getVideosForSub(sub, user_uid = null) {
|
||||
}
|
||||
}
|
||||
|
||||
if (config_api.getConfigItem('ytdl_include_thumbnail')) {
|
||||
downloadConfig.push('--write-thumbnail');
|
||||
}
|
||||
|
||||
// get videos
|
||||
logger.verbose('Subscription: getting videos for subscription ' + sub.name);
|
||||
youtubedl.exec(sub.url, downloadConfig, {}, function(err, output) {
|
||||
@@ -398,7 +415,6 @@ function handleOutputJSON(sub, sub_db, output_json, multiUserMode = null, reset_
|
||||
// add to db
|
||||
sub_db.get('videos').push(output_json).write();
|
||||
} else {
|
||||
// TODO: make multiUserMode obj
|
||||
db_api.registerFileDB(path.basename(output_json['_filename']), sub.type, multiUserMode, sub);
|
||||
}
|
||||
}
|
||||
@@ -417,6 +433,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 +501,7 @@ function removeIDFromArchive(archive_path, id) {
|
||||
module.exports = {
|
||||
getSubscription : getSubscription,
|
||||
getAllSubscriptions : getAllSubscriptions,
|
||||
updateSubscription : updateSubscription,
|
||||
subscribe : subscribe,
|
||||
unsubscribe : unsubscribe,
|
||||
deleteSubscriptionFile : deleteSubscriptionFile,
|
||||
|
||||
122
backend/utils.js
122
backend/utils.js
@@ -19,6 +19,39 @@ function getTrueFileName(unfixed_path, type) {
|
||||
return fixed_path;
|
||||
}
|
||||
|
||||
function getDownloadedFilesByType(basePath, type) {
|
||||
// return empty array if the path doesn't exist
|
||||
if (!fs.existsSync(basePath)) return [];
|
||||
|
||||
let files = [];
|
||||
const ext = type === 'audio' ? 'mp3' : 'mp4';
|
||||
var located_files = recFindByExt(basePath, ext);
|
||||
for (let i = 0; i < located_files.length; i++) {
|
||||
let file = located_files[i];
|
||||
var file_path = file.substring(basePath.includes('\\') ? basePath.length+1 : basePath.length, file.length);
|
||||
|
||||
var stats = fs.statSync(file);
|
||||
|
||||
var id = file_path.substring(0, file_path.length-4);
|
||||
var jsonobj = getJSONByType(type, id, basePath);
|
||||
if (!jsonobj) continue;
|
||||
var title = jsonobj.title;
|
||||
var url = jsonobj.webpage_url;
|
||||
var uploader = jsonobj.uploader;
|
||||
var upload_date = jsonobj.upload_date;
|
||||
upload_date = upload_date ? `${upload_date.substring(0, 4)}-${upload_date.substring(4, 6)}-${upload_date.substring(6, 8)}` : null;
|
||||
var thumbnail = jsonobj.thumbnail;
|
||||
var duration = jsonobj.duration;
|
||||
|
||||
var size = stats.size;
|
||||
|
||||
var isaudio = type === 'audio';
|
||||
var file_obj = new File(id, title, thumbnail, isaudio, duration, url, uploader, size, file, upload_date);
|
||||
files.push(file_obj);
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
function getJSONMp4(name, customPath, openReadPerms = false) {
|
||||
var obj = null; // output
|
||||
if (!customPath) customPath = config_api.getConfigItem('ytdl_video_folder_path');
|
||||
@@ -51,6 +84,53 @@ function getJSONMp3(name, customPath, openReadPerms = false) {
|
||||
return obj;
|
||||
}
|
||||
|
||||
function getJSONByType(type, name, customPath, openReadPerms = false) {
|
||||
return type === 'audio' ? getJSONMp3(name, customPath, openReadPerms) : getJSONMp4(name, customPath, openReadPerms)
|
||||
}
|
||||
|
||||
function getDownloadedThumbnail(name, type, customPath = null) {
|
||||
if (!customPath) customPath = type === 'audio' ? config_api.getConfigItem('ytdl_audio_folder_path') : config_api.getConfigItem('ytdl_video_folder_path');
|
||||
|
||||
let jpgPath = path.join(customPath, name + '.jpg');
|
||||
let webpPath = path.join(customPath, name + '.webp');
|
||||
let pngPath = path.join(customPath, name + '.png');
|
||||
|
||||
if (fs.existsSync(jpgPath))
|
||||
return jpgPath;
|
||||
else if (fs.existsSync(webpPath))
|
||||
return webpPath;
|
||||
else if (fs.existsSync(pngPath))
|
||||
return pngPath;
|
||||
else
|
||||
return null;
|
||||
}
|
||||
|
||||
function getExpectedFileSize(input_info_jsons) {
|
||||
// treat single videos as arrays to have the file sizes checked/added to. makes the code cleaner
|
||||
const info_jsons = Array.isArray(input_info_jsons) ? input_info_jsons : [input_info_jsons];
|
||||
|
||||
let expected_filesize = 0;
|
||||
|
||||
info_jsons.forEach(info_json => {
|
||||
if (info_json['filesize']) {
|
||||
expected_filesize += info_json['filesize'];
|
||||
return;
|
||||
}
|
||||
const formats = info_json['format_id'].split('+');
|
||||
let individual_expected_filesize = 0;
|
||||
formats.forEach(format_id => {
|
||||
info_json.formats.forEach(available_format => {
|
||||
if (available_format.format_id === format_id && available_format.filesize) {
|
||||
individual_expected_filesize += available_format.filesize;
|
||||
}
|
||||
});
|
||||
});
|
||||
expected_filesize += individual_expected_filesize;
|
||||
});
|
||||
|
||||
return expected_filesize;
|
||||
}
|
||||
|
||||
function fixVideoMetadataPerms(name, type, customPath = null) {
|
||||
if (is_windows) return;
|
||||
if (!customPath) customPath = type === 'audio' ? config_api.getConfigItem('ytdl_audio_folder_path')
|
||||
@@ -73,6 +153,43 @@ function fixVideoMetadataPerms(name, type, customPath = null) {
|
||||
}
|
||||
}
|
||||
|
||||
function deleteJSONFile(name, type, customPath = null) {
|
||||
if (!customPath) customPath = type === 'audio' ? config_api.getConfigItem('ytdl_audio_folder_path')
|
||||
: config_api.getConfigItem('ytdl_video_folder_path');
|
||||
|
||||
const ext = type === 'audio' ? '.mp3' : '.mp4';
|
||||
let json_path = path.join(customPath, name + '.info.json');
|
||||
let alternate_json_path = path.join(customPath, name + ext + '.info.json');
|
||||
|
||||
if (fs.existsSync(json_path)) fs.unlinkSync(json_path);
|
||||
if (fs.existsSync(alternate_json_path)) fs.unlinkSync(alternate_json_path);
|
||||
}
|
||||
|
||||
|
||||
function recFindByExt(base,ext,files,result)
|
||||
{
|
||||
files = files || fs.readdirSync(base)
|
||||
result = result || []
|
||||
|
||||
files.forEach(
|
||||
function (file) {
|
||||
var newbase = path.join(base,file)
|
||||
if ( fs.statSync(newbase).isDirectory() )
|
||||
{
|
||||
result = recFindByExt(newbase,ext,fs.readdirSync(newbase),result)
|
||||
}
|
||||
else
|
||||
{
|
||||
if ( file.substr(-1*(ext.length+1)) == '.' + ext )
|
||||
{
|
||||
result.push(newbase)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
return result
|
||||
}
|
||||
|
||||
// objects
|
||||
|
||||
function File(id, title, thumbnailURL, isAudio, duration, url, uploader, size, path, upload_date) {
|
||||
@@ -92,6 +209,11 @@ module.exports = {
|
||||
getJSONMp3: getJSONMp3,
|
||||
getJSONMp4: getJSONMp4,
|
||||
getTrueFileName: getTrueFileName,
|
||||
getDownloadedThumbnail: getDownloadedThumbnail,
|
||||
getExpectedFileSize: getExpectedFileSize,
|
||||
fixVideoMetadataPerms: fixVideoMetadataPerms,
|
||||
deleteJSONFile: deleteJSONFile,
|
||||
getDownloadedFilesByType: getDownloadedFilesByType,
|
||||
recFindByExt: recFindByExt,
|
||||
File: File
|
||||
}
|
||||
|
||||
36
package-lock.json
generated
36
package-lock.json
generated
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "youtube-dl-material",
|
||||
"version": "4.0.0",
|
||||
"version": "4.1.0",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
@@ -1404,6 +1404,21 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"@ngneat/content-loader": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@ngneat/content-loader/-/content-loader-5.0.0.tgz",
|
||||
"integrity": "sha512-XrS53rsiJoQIOy2BwmOHkvgPv0a5cTzXbbM+/3IpgezPFdNqDunpZW+AciWQffVOfgbmL+7cYp1DVb6WM15LhQ==",
|
||||
"requires": {
|
||||
"tslib": "^2.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"tslib": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.1.tgz",
|
||||
"integrity": "sha512-SgIkNheinmEBgx1IUNirK0TUD4X9yjjBRTqqjggWCU3pUEqIk3/Uwl3yRixYKT6WjQuGiwDv4NomL3wqRCj+CQ=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"@ngtools/webpack": {
|
||||
"version": "9.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-9.1.0.tgz",
|
||||
@@ -6588,6 +6603,11 @@
|
||||
"integrity": "sha512-qgDYXFSR5WvEfuS5dMj6oTMEbrrSaM0CrFk2Yiq/gXnBvD9pMa2jGXxyhGLfvhZpuMZe18CJpFxAt3CRs42NMg==",
|
||||
"dev": true
|
||||
},
|
||||
"is-retina": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/is-retina/-/is-retina-1.0.3.tgz",
|
||||
"integrity": "sha1-10AbKGvqKuN/Ykd1iN5QTQuGR+M="
|
||||
},
|
||||
"is-stream": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz",
|
||||
@@ -9137,6 +9157,15 @@
|
||||
"resolved": "https://registry.npmjs.org/ng-lazyload-image/-/ng-lazyload-image-7.1.0.tgz",
|
||||
"integrity": "sha512-1fip2FdPBDRnjGyBokI/DupBxOnrKh2lbtT8X8N1oPbE3KBZXXl82VIKcK2Sx+XQD67/+VtFzlISmrgsatzYuw=="
|
||||
},
|
||||
"ngx-avatar": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ngx-avatar/-/ngx-avatar-4.0.0.tgz",
|
||||
"integrity": "sha512-Uk40UXl26RvDy1ori9NDsGFB+f84AaxMnsIwZA6JPJK0pLcbo3F4vZTmzLZeOusOw1Qtgk5IzF630jo06keXwQ==",
|
||||
"requires": {
|
||||
"is-retina": "^1.0.3",
|
||||
"ts-md5": "^1.2.4"
|
||||
}
|
||||
},
|
||||
"ngx-file-drop": {
|
||||
"version": "9.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ngx-file-drop/-/ngx-file-drop-9.0.1.tgz",
|
||||
@@ -13457,6 +13486,11 @@
|
||||
"utf8-byte-length": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"ts-md5": {
|
||||
"version": "1.2.7",
|
||||
"resolved": "https://registry.npmjs.org/ts-md5/-/ts-md5-1.2.7.tgz",
|
||||
"integrity": "sha512-emODogvKGWi1KO1l9c6YxLMBn6CEH3VrH5mVPIyOtxBG52BvV4jP3GWz6bOZCz61nLgBc3ffQYE4+EHfCD+V7w=="
|
||||
},
|
||||
"ts-node": {
|
||||
"version": "3.0.6",
|
||||
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-3.0.6.tgz",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "youtube-dl-material",
|
||||
"version": "4.0.0",
|
||||
"version": "4.1.0",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"ng": "ng",
|
||||
@@ -30,12 +30,14 @@
|
||||
"@angular/platform-browser": "^9.1.0",
|
||||
"@angular/platform-browser-dynamic": "^9.1.0",
|
||||
"@angular/router": "^9.1.0",
|
||||
"@ngneat/content-loader": "^5.0.0",
|
||||
"core-js": "^2.4.1",
|
||||
"file-saver": "^2.0.2",
|
||||
"filesize": "^6.1.0",
|
||||
"fingerprintjs2": "^2.1.0",
|
||||
"nan": "^2.14.1",
|
||||
"ng-lazyload-image": "^7.0.1",
|
||||
"ngx-avatar": "^4.0.0",
|
||||
"ngx-file-drop": "^9.0.1",
|
||||
"ngx-videogular": "^9.0.1",
|
||||
"rxjs": "^6.5.3",
|
||||
|
||||
@@ -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>
|
||||
@@ -38,12 +38,16 @@
|
||||
</div>
|
||||
<div class="sidenav-container" style="height: calc(100% - 64px)">
|
||||
<mat-sidenav-container style="height: 100%">
|
||||
<mat-sidenav #sidenav>
|
||||
<mat-sidenav [opened]="postsService.sidepanel_mode === 'side' && !window.location.href.includes('/player')" [mode]="postsService.sidepanel_mode" #sidenav>
|
||||
<mat-nav-list>
|
||||
<a *ngIf="postsService.config && (!postsService.config.Advanced.multi_user_mode || postsService.isLoggedIn)" mat-list-item (click)="sidenav.close()" routerLink='/home'><ng-container i18n="Navigation menu Home Page title">Home</ng-container></a>
|
||||
<a *ngIf="postsService.config && (!postsService.config.Advanced.multi_user_mode || postsService.isLoggedIn)" mat-list-item (click)="postsService.sidepanel_mode === 'over' ? sidenav.close() : null" routerLink='/home'><ng-container i18n="Navigation menu Home Page title">Home</ng-container></a>
|
||||
<a *ngIf="postsService.config && postsService.config.Advanced.multi_user_mode && !postsService.isLoggedIn" mat-list-item (click)="sidenav.close()" routerLink='/login'><ng-container i18n="Navigation menu Login Page title">Login</ng-container></a>
|
||||
<a *ngIf="postsService.config && allowSubscriptions && (!postsService.config.Advanced.multi_user_mode || (postsService.isLoggedIn && postsService.permissions.includes('subscriptions')))" mat-list-item (click)="sidenav.close()" routerLink='/subscriptions'><ng-container i18n="Navigation menu Subscriptions Page title">Subscriptions</ng-container></a>
|
||||
<a *ngIf="postsService.config && enableDownloadsManager && (!postsService.config.Advanced.multi_user_mode || (postsService.isLoggedIn && postsService.permissions.includes('downloads_manager')))" mat-list-item (click)="sidenav.close()" routerLink='/downloads'><ng-container i18n="Navigation menu Downloads Page title">Downloads</ng-container></a>
|
||||
<a *ngIf="postsService.config && allowSubscriptions && (!postsService.config.Advanced.multi_user_mode || (postsService.isLoggedIn && postsService.permissions.includes('subscriptions')))" mat-list-item (click)="postsService.sidepanel_mode === 'over' ? sidenav.close() : null" routerLink='/subscriptions'><ng-container i18n="Navigation menu Subscriptions Page title">Subscriptions</ng-container></a>
|
||||
<a *ngIf="postsService.config && enableDownloadsManager && (!postsService.config.Advanced.multi_user_mode || (postsService.isLoggedIn && postsService.permissions.includes('downloads_manager')))" mat-list-item (click)="postsService.sidepanel_mode === 'over' ? sidenav.close() : null" routerLink='/downloads'><ng-container i18n="Navigation menu Downloads Page title">Downloads</ng-container></a>
|
||||
<ng-container *ngIf="postsService.config && allowSubscriptions && postsService.subscriptions && (!postsService.config.Advanced.multi_user_mode || (postsService.isLoggedIn && postsService.permissions.includes('subscriptions')))">
|
||||
<mat-divider></mat-divider>
|
||||
<a *ngFor="let subscription of postsService.subscriptions" mat-list-item (click)="postsService.sidepanel_mode === 'over' ? sidenav.close() : null" [routerLink]="['/subscription', { id: subscription.id }]"><ngx-avatar [style.margin-right]="'10px'" size="32" [name]="subscription.name"></ngx-avatar><ng-container i18n="Navigation menu Downloads Page title">{{subscription.name}}</ng-container></a>
|
||||
</ng-container>
|
||||
</mat-nav-list>
|
||||
</mat-sidenav>
|
||||
<mat-sidenav-content [style.background]="postsService.theme ? postsService.theme.background_color : null">
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Component, OnInit, ElementRef, ViewChild, HostBinding } from '@angular/core';
|
||||
import { Component, OnInit, ElementRef, ViewChild, HostBinding, AfterViewInit } from '@angular/core';
|
||||
import {PostsService} from './posts.services';
|
||||
import {FileCardComponent} from './file-card/file-card.component';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
@@ -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';
|
||||
@@ -31,19 +30,19 @@ import { SetDefaultAdminDialogComponent } from './dialogs/set-default-admin-dial
|
||||
templateUrl: './app.component.html',
|
||||
styleUrls: ['./app.component.css']
|
||||
})
|
||||
export class AppComponent implements OnInit {
|
||||
export class AppComponent implements OnInit, AfterViewInit {
|
||||
|
||||
@HostBinding('class') componentCssClass;
|
||||
THEMES_CONFIG = THEMES_CONFIG;
|
||||
|
||||
window = window;
|
||||
|
||||
// config items
|
||||
topBarTitle = 'Youtube Downloader';
|
||||
defaultTheme = null;
|
||||
allowThemeChange = null;
|
||||
allowSubscriptions = false;
|
||||
enableDownloadsManager = false;
|
||||
// defaults to true to prevent attack
|
||||
settingsPinRequired = true;
|
||||
|
||||
@ViewChild('sidenav') sidenav: MatSidenav;
|
||||
@ViewChild('hamburgerMenu', { read: ElementRef }) hamburgerMenuButton: ElementRef;
|
||||
@@ -72,6 +71,29 @@ export class AppComponent implements OnInit {
|
||||
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
if (localStorage.getItem('theme')) {
|
||||
this.setTheme(localStorage.getItem('theme'));
|
||||
}
|
||||
|
||||
this.postsService.open_create_default_admin_dialog.subscribe(open => {
|
||||
if (open) {
|
||||
const dialogRef = this.dialog.open(SetDefaultAdminDialogComponent);
|
||||
dialogRef.afterClosed().subscribe(success => {
|
||||
if (success) {
|
||||
if (this.router.url !== '/login') { this.router.navigate(['/login']); }
|
||||
} else {
|
||||
console.error('Failed to create default admin account. See logs for details.');
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ngAfterViewInit() {
|
||||
this.postsService.sidenav = this.sidenav;
|
||||
}
|
||||
|
||||
toggleSidenav() {
|
||||
this.sidenav.toggle();
|
||||
}
|
||||
@@ -79,7 +101,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;
|
||||
@@ -90,6 +111,11 @@ export class AppComponent implements OnInit {
|
||||
if (!localStorage.getItem('theme')) {
|
||||
this.setTheme(themingExists ? this.defaultTheme : 'default');
|
||||
}
|
||||
|
||||
// gets the subscriptions
|
||||
if (this.allowSubscriptions) {
|
||||
this.postsService.reloadSubscriptions();
|
||||
}
|
||||
}
|
||||
|
||||
// theme stuff
|
||||
@@ -121,9 +147,9 @@ export class AppComponent implements OnInit {
|
||||
this.postsService.setTheme(theme);
|
||||
|
||||
this.onSetTheme(this.THEMES_CONFIG[theme]['css_label'], old_theme ? this.THEMES_CONFIG[old_theme]['css_label'] : old_theme);
|
||||
}
|
||||
}
|
||||
|
||||
onSetTheme(theme, old_theme) {
|
||||
onSetTheme(theme, old_theme) {
|
||||
if (old_theme) {
|
||||
document.body.classList.remove(old_theme);
|
||||
this.overlayContainer.getContainerElement().classList.remove(old_theme);
|
||||
@@ -145,24 +171,8 @@ onSetTheme(theme, old_theme) {
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
if (localStorage.getItem('theme')) {
|
||||
this.setTheme(localStorage.getItem('theme'));
|
||||
} else {
|
||||
//
|
||||
}
|
||||
this.postsService.open_create_default_admin_dialog.subscribe(open => {
|
||||
if (open) {
|
||||
const dialogRef = this.dialog.open(SetDefaultAdminDialogComponent);
|
||||
dialogRef.afterClosed().subscribe(success => {
|
||||
if (success) {
|
||||
if (this.router.url !== '/login') { this.router.navigate(['/login']); }
|
||||
} else {
|
||||
console.error('Failed to create default admin account. See logs for details.');
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
getSubscriptions() {
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -175,31 +185,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'
|
||||
|
||||
@@ -51,9 +51,10 @@ 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';
|
||||
import { AvatarModule } from 'ngx-avatar';
|
||||
import { ContentLoaderModule } from '@ngneat/content-loader';
|
||||
|
||||
import es from '@angular/common/locales/es';
|
||||
import { AboutDialogComponent } from './dialogs/about-dialog/about-dialog.component';
|
||||
@@ -73,6 +74,11 @@ 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 { UnifiedFileCardComponent } from './components/unified-file-card/unified-file-card.component';
|
||||
import { RecentVideosComponent } from './components/recent-videos/recent-videos.component';
|
||||
import { EditSubscriptionDialogComponent } from './dialogs/edit-subscription-dialog/edit-subscription-dialog.component';
|
||||
import { CustomPlaylistsComponent } from './components/custom-playlists/custom-playlists.component';
|
||||
|
||||
registerLocaleData(es, 'es');
|
||||
|
||||
@@ -95,7 +101,6 @@ export function isVisible({ event, element, scrollContainer, offset }: IsVisible
|
||||
SubscriptionFileCardComponent,
|
||||
SubscriptionInfoDialogComponent,
|
||||
SettingsComponent,
|
||||
CheckOrSetPinDialogComponent,
|
||||
AboutDialogComponent,
|
||||
VideoInfoDialogComponent,
|
||||
ArgModifierDialogComponent,
|
||||
@@ -113,7 +118,12 @@ export function isVisible({ event, element, scrollContainer, offset }: IsVisible
|
||||
ManageRoleComponent,
|
||||
CookiesUploaderDialogComponent,
|
||||
LogsViewerComponent,
|
||||
ModifyPlaylistComponent
|
||||
ModifyPlaylistComponent,
|
||||
ConfirmDialogComponent,
|
||||
UnifiedFileCardComponent,
|
||||
RecentVideosComponent,
|
||||
EditSubscriptionDialogComponent,
|
||||
CustomPlaylistsComponent
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
@@ -143,7 +153,6 @@ export function isVisible({ event, element, scrollContainer, offset }: IsVisible
|
||||
MatMenuModule,
|
||||
MatDialogModule,
|
||||
MatSlideToggleModule,
|
||||
MatMenuModule,
|
||||
MatAutocompleteModule,
|
||||
MatTabsModule,
|
||||
MatTooltipModule,
|
||||
@@ -154,6 +163,8 @@ export function isVisible({ event, element, scrollContainer, offset }: IsVisible
|
||||
DragDropModule,
|
||||
ClipboardModule,
|
||||
NgxFileDropModule,
|
||||
AvatarModule,
|
||||
ContentLoaderModule,
|
||||
VgCoreModule,
|
||||
VgControlsModule,
|
||||
VgOverlayPlayModule,
|
||||
@@ -167,8 +178,7 @@ export function isVisible({ event, element, scrollContainer, offset }: IsVisible
|
||||
CreatePlaylistComponent,
|
||||
SubscribeDialogComponent,
|
||||
SubscriptionInfoDialogComponent,
|
||||
SettingsComponent,
|
||||
CheckOrSetPinDialogComponent
|
||||
SettingsComponent
|
||||
],
|
||||
providers: [
|
||||
PostsService
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
<div *ngIf="playlists && playlists.length > 0">
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div *ngFor="let playlist of playlists; let i = index" class="mb-2 mt-2" [ngClass]="[ postsService.card_size === 'small' ? 'col-2 small-col' : '', postsService.card_size === 'medium' ? 'col-6 col-lg-4 medium-col' : '', postsService.card_size === 'large' ? 'col-12 large-col' : '' ]">
|
||||
<app-unified-file-card [index]="i" [card_size]="postsService.card_size" (goToFile)="goToPlaylist($event)" [file_obj]="playlist" [is_playlist]="true" (editPlaylist)="editPlaylistDialog($event)" (deleteFile)="deletePlaylist($event)" [loading]="false"></app-unified-file-card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div *ngIf="playlists && playlists.length === 0" style="text-align: center;">
|
||||
No playlists available. Create one from your downloading files by clicking the blue plus button.
|
||||
</div>
|
||||
<div class="add-playlist-button"><button (click)="openCreatePlaylistDialog()" mat-fab><mat-icon>add</mat-icon></button></div>
|
||||
@@ -0,0 +1,18 @@
|
||||
.add-playlist-button {
|
||||
float: right;
|
||||
position: relative;
|
||||
bottom: 15px;
|
||||
right: 15px;
|
||||
}
|
||||
|
||||
.large-col {
|
||||
max-width: 320px;
|
||||
}
|
||||
|
||||
.medium-col {
|
||||
max-width: 240px;
|
||||
}
|
||||
|
||||
.small-col {
|
||||
max-width: 240px;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { CustomPlaylistsComponent } from './custom-playlists.component';
|
||||
|
||||
describe('CustomPlaylistsComponent', () => {
|
||||
let component: CustomPlaylistsComponent;
|
||||
let fixture: ComponentFixture<CustomPlaylistsComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ CustomPlaylistsComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(CustomPlaylistsComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,113 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { PostsService } from 'app/posts.services';
|
||||
import { Router } from '@angular/router';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { CreatePlaylistComponent } from 'app/create-playlist/create-playlist.component';
|
||||
import { ModifyPlaylistComponent } from 'app/dialogs/modify-playlist/modify-playlist.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-custom-playlists',
|
||||
templateUrl: './custom-playlists.component.html',
|
||||
styleUrls: ['./custom-playlists.component.scss']
|
||||
})
|
||||
export class CustomPlaylistsComponent implements OnInit {
|
||||
|
||||
playlists = null;
|
||||
playlists_received = false;
|
||||
downloading_content = {'video': {}, 'audio': {}};
|
||||
|
||||
constructor(public postsService: PostsService, private router: Router, private dialog: MatDialog) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.postsService.service_initialized.subscribe(init => {
|
||||
if (init) {
|
||||
this.getAllPlaylists();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getAllPlaylists() {
|
||||
this.playlists_received = false;
|
||||
this.postsService.getAllFiles().subscribe(res => {
|
||||
this.playlists = res['playlists'];
|
||||
this.playlists_received = true;
|
||||
});
|
||||
}
|
||||
|
||||
// creating a playlist
|
||||
openCreatePlaylistDialog() {
|
||||
const dialogRef = this.dialog.open(CreatePlaylistComponent, {
|
||||
data: {
|
||||
}
|
||||
});
|
||||
dialogRef.afterClosed().subscribe(result => {
|
||||
if (result) {
|
||||
this.getAllPlaylists();
|
||||
this.postsService.openSnackBar('Successfully created playlist!', '');
|
||||
} else if (result === false) {
|
||||
this.postsService.openSnackBar('ERROR: failed to create playlist!', '');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
goToPlaylist(info_obj) {
|
||||
const playlist = info_obj.file;
|
||||
const playlistID = playlist.id;
|
||||
const type = playlist.type;
|
||||
|
||||
if (playlist) {
|
||||
if (this.postsService.config['Extra']['download_only_mode']) {
|
||||
this.downloading_content[type][playlistID] = true;
|
||||
this.downloadPlaylist(playlist.fileNames, type, playlist.name, playlistID);
|
||||
} else {
|
||||
localStorage.setItem('player_navigator', this.router.url);
|
||||
const fileNames = playlist.fileNames;
|
||||
this.router.navigate(['/player', {fileNames: fileNames.join('|nvr|'), type: type, id: playlistID, uid: playlistID}]);
|
||||
}
|
||||
} else {
|
||||
// playlist not found
|
||||
console.error(`Playlist with ID ${playlistID} not found!`);
|
||||
}
|
||||
}
|
||||
|
||||
downloadPlaylist(fileNames, type, zipName = null, playlistID = null) {
|
||||
this.postsService.downloadFileFromServer(fileNames, type, zipName).subscribe(res => {
|
||||
if (playlistID) { this.downloading_content[type][playlistID] = false };
|
||||
const blob: Blob = res;
|
||||
saveAs(blob, zipName + '.zip');
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
deletePlaylist(args) {
|
||||
const playlist = args.file;
|
||||
const index = args.index;
|
||||
const playlistID = playlist.id;
|
||||
this.postsService.removePlaylist(playlistID, 'audio').subscribe(res => {
|
||||
if (res['success']) {
|
||||
this.playlists.splice(index, 1);
|
||||
this.postsService.openSnackBar('Playlist successfully removed.', '');
|
||||
}
|
||||
this.getAllPlaylists();
|
||||
});
|
||||
}
|
||||
|
||||
editPlaylistDialog(args) {
|
||||
const playlist = args.playlist;
|
||||
const index = args.index;
|
||||
const dialogRef = this.dialog.open(ModifyPlaylistComponent, {
|
||||
data: {
|
||||
playlist: playlist,
|
||||
width: '65vw'
|
||||
}
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe(res => {
|
||||
// updates playlist in file manager if it changed
|
||||
if (dialogRef.componentInstance.playlist_updated) {
|
||||
this.playlists[index] = dialogRef.componentInstance.original_playlist;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
<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>
|
||||
@@ -0,0 +1 @@
|
||||
.spacer {flex: 1 1 auto;}
|
||||
|
||||
@@ -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!');
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
<div class="container-fluid" style="max-width: 941px;">
|
||||
<div class="row">
|
||||
<div class="col-12 order-2 col-sm-4 order-sm-1 d-flex justify-content-center">
|
||||
<div>
|
||||
<div style="display: inline-block;">
|
||||
<mat-form-field style="width: 132px;">
|
||||
<mat-select [(ngModel)]="this.filterProperty" (selectionChange)="filterOptionChanged($event.value)">
|
||||
<mat-option *ngFor="let filterOption of filterProperties | keyvalue" [value]="filterOption.value">
|
||||
{{filterOption['value']['label']}}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="sort-dir-div">
|
||||
<button (click)="toggleModeChange()" mat-icon-button><mat-icon>{{descendingMode ? 'arrow_downward' : 'arrow_upward'}}</mat-icon></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 order-1 col-sm-4 order-sm-2 d-flex justify-content-center">
|
||||
<h4 class="my-videos-title" i18n="My videos title">My videos</h4>
|
||||
</div>
|
||||
<div class="col-12 order-3 col-sm-4 order-sm-3 d-flex justify-content-center">
|
||||
<mat-form-field [ngClass]="searchIsFocused ? 'search-bar-focused' : 'search-bar-unfocused'" class="search-bar" color="accent">
|
||||
<input (focus)="searchIsFocused = true" (blur)="searchIsFocused = false" class="search-input" type="text" placeholder="Search" i18n-placeholder="Files search placeholder" [(ngModel)]="search_text" (ngModelChange)="onSearchInputChanged($event)" matInput>
|
||||
<mat-icon matSuffix>search</mat-icon>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<ng-container *ngIf="normal_files_received">
|
||||
<div *ngFor="let file of filtered_files; let i = index" class="mb-2 mt-2 d-flex justify-content-center" [ngClass]="[ postsService.card_size === 'small' ? 'col-2 small-col' : '', postsService.card_size === 'medium' ? 'col-6 col-lg-4 medium-col' : '', postsService.card_size === 'large' ? 'col-12 large-col' : '' ]">
|
||||
<app-unified-file-card [index]="i" [card_size]="postsService.card_size" (goToFile)="goToFile($event)" (goToSubscription)="goToSubscription($event)" [file_obj]="file" [use_youtubedl_archive]="postsService.config['Downloader']['use_youtubedl_archive']" [loading]="false" (deleteFile)="deleteFile($event)"></app-unified-file-card>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="!normal_files_received && loading_files && loading_files.length > 0">
|
||||
<div *ngFor="let file of loading_files; let i = index" class="mb-2 mt-2 d-flex justify-content-center" [ngClass]="[ postsService.card_size === 'small' ? 'col-2 small-col' : '', postsService.card_size === 'medium' ? 'col-6 col-lg-4 medium-col' : '', postsService.card_size === 'large' ? 'col-12 large-col' : '' ]">
|
||||
<app-unified-file-card [index]="i" [card_size]="postsService.card_size" [loading]="true" [theme]="postsService.theme"></app-unified-file-card>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,60 @@
|
||||
.large-col {
|
||||
max-width: 320px;
|
||||
}
|
||||
|
||||
.medium-col {
|
||||
max-width: 240px;
|
||||
}
|
||||
|
||||
.small-col {
|
||||
max-width: 240px;
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
transition: all .5s ease;
|
||||
position: relative;
|
||||
float: right;
|
||||
}
|
||||
|
||||
.search-bar-unfocused {
|
||||
width: 132px;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
transition: all .5s ease;
|
||||
}
|
||||
|
||||
.search-bar-focused {
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
.flex-grid {
|
||||
width: 100%;
|
||||
display: block;
|
||||
position: relative;
|
||||
padding-left: 12px;
|
||||
padding-right: 12px;
|
||||
}
|
||||
|
||||
.column {
|
||||
width: 33%;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.sort-dir-div {
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
}
|
||||
|
||||
.my-videos-title {
|
||||
text-align: center;
|
||||
position: relative;
|
||||
top: 12px;
|
||||
}
|
||||
|
||||
@media (max-width: 576px) {
|
||||
.my-videos-title {
|
||||
top: 0px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { RecentVideosComponent } from './recent-videos.component';
|
||||
|
||||
describe('RecentVideosComponent', () => {
|
||||
let component: RecentVideosComponent;
|
||||
let fixture: ComponentFixture<RecentVideosComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ RecentVideosComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(RecentVideosComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
288
src/app/components/recent-videos/recent-videos.component.ts
Normal file
288
src/app/components/recent-videos/recent-videos.component.ts
Normal file
@@ -0,0 +1,288 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { PostsService } from 'app/posts.services';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
@Component({
|
||||
selector: 'app-recent-videos',
|
||||
templateUrl: './recent-videos.component.html',
|
||||
styleUrls: ['./recent-videos.component.scss']
|
||||
})
|
||||
export class RecentVideosComponent implements OnInit {
|
||||
|
||||
cached_file_count = 0;
|
||||
loading_files = null;
|
||||
|
||||
normal_files_received = false;
|
||||
subscription_files_received = false;
|
||||
files: any[] = null;
|
||||
filtered_files: any[] = null;
|
||||
downloading_content = {'video': {}, 'audio': {}};
|
||||
search_mode = false;
|
||||
search_text = '';
|
||||
searchIsFocused = false;
|
||||
descendingMode = true;
|
||||
filterProperties = {
|
||||
'registered': {
|
||||
'key': 'registered',
|
||||
'label': 'Download Date',
|
||||
'property': 'registered'
|
||||
},
|
||||
'upload_date': {
|
||||
'key': 'upload_date',
|
||||
'label': 'Upload Date',
|
||||
'property': 'upload_date'
|
||||
},
|
||||
'name': {
|
||||
'key': 'name',
|
||||
'label': 'Name',
|
||||
'property': 'title'
|
||||
},
|
||||
'file_size': {
|
||||
'key': 'file_size',
|
||||
'label': 'File Size',
|
||||
'property': 'size'
|
||||
},
|
||||
'duration': {
|
||||
'key': 'duration',
|
||||
'label': 'Duration',
|
||||
'property': 'duration'
|
||||
}
|
||||
};
|
||||
filterProperty = this.filterProperties['upload_date'];
|
||||
|
||||
constructor(public postsService: PostsService, private router: Router) {
|
||||
// get cached file count
|
||||
if (localStorage.getItem('cached_file_count')) {
|
||||
this.cached_file_count = +localStorage.getItem('cached_file_count');
|
||||
this.loading_files = Array(this.cached_file_count).fill(0);
|
||||
}
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
if (this.postsService.initialized) {
|
||||
this.getAllFiles();
|
||||
}
|
||||
|
||||
this.postsService.service_initialized.subscribe(init => {
|
||||
if (init) {
|
||||
this.getAllFiles();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// set filter property to cached
|
||||
const cached_filter_property = localStorage.getItem('filter_property');
|
||||
if (cached_filter_property && this.filterProperties[cached_filter_property]) {
|
||||
this.filterProperty = this.filterProperties[cached_filter_property];
|
||||
}
|
||||
}
|
||||
|
||||
// search
|
||||
|
||||
onSearchInputChanged(newvalue) {
|
||||
if (newvalue.length > 0) {
|
||||
this.search_mode = true;
|
||||
this.filterFiles(newvalue);
|
||||
} else {
|
||||
this.search_mode = false;
|
||||
this.filtered_files = this.files;
|
||||
}
|
||||
}
|
||||
|
||||
private filterFiles(value: string) {
|
||||
const filterValue = value.toLowerCase();
|
||||
this.filtered_files = this.files.filter(option => option.id.toLowerCase().includes(filterValue));
|
||||
}
|
||||
|
||||
filterByProperty(prop) {
|
||||
if (this.descendingMode) {
|
||||
this.filtered_files = this.filtered_files.sort((a, b) => (a[prop] > b[prop] ? -1 : 1));
|
||||
} else {
|
||||
this.filtered_files = this.filtered_files.sort((a, b) => (a[prop] > b[prop] ? 1 : -1));
|
||||
}
|
||||
}
|
||||
|
||||
filterOptionChanged(value) {
|
||||
this.filterByProperty(value['property']);
|
||||
localStorage.setItem('filter_property', value['key']);
|
||||
}
|
||||
|
||||
toggleModeChange() {
|
||||
this.descendingMode = !this.descendingMode;
|
||||
this.filterByProperty(this.filterProperty['property']);
|
||||
}
|
||||
|
||||
// get files
|
||||
|
||||
getAllFiles() {
|
||||
this.normal_files_received = false;
|
||||
this.postsService.getAllFiles().subscribe(res => {
|
||||
this.files = res['files'];
|
||||
this.files.forEach(file => {
|
||||
file.duration = typeof file.duration !== 'string' ? file.duration : this.durationStringToNumber(file.duration);
|
||||
});
|
||||
this.files.sort(this.sortFiles);
|
||||
if (this.search_mode) {
|
||||
this.filterFiles(this.search_text);
|
||||
} else {
|
||||
this.filtered_files = this.files;
|
||||
}
|
||||
this.filterByProperty(this.filterProperty['property']);
|
||||
|
||||
// set cached file count for future use, note that we convert the amount of files to a string
|
||||
localStorage.setItem('cached_file_count', '' + this.files.length);
|
||||
|
||||
this.normal_files_received = true;
|
||||
});
|
||||
}
|
||||
|
||||
// navigation
|
||||
|
||||
goToFile(info_obj) {
|
||||
const file = info_obj['file'];
|
||||
const event = info_obj['event'];
|
||||
if (this.postsService.config['Extra']['download_only_mode']) {
|
||||
this.downloadFile(file);
|
||||
} else {
|
||||
this.navigateToFile(file, event.ctrlKey);
|
||||
}
|
||||
}
|
||||
|
||||
navigateToFile(file, new_tab) {
|
||||
localStorage.setItem('player_navigator', this.router.url);
|
||||
if (file.sub_id) {
|
||||
const sub = this.postsService.getSubscriptionByID(file.sub_id);
|
||||
if (sub.streamingOnly) {
|
||||
// streaming only mode subscriptions
|
||||
!new_tab ? this.router.navigate(['/player', {name: file.id,
|
||||
url: file.requested_formats ? file.requested_formats[0].url : file.url}])
|
||||
: window.open(`/#/player;name=${file.id};url=${file.requested_formats ? file.requested_formats[0].url : file.url}`);
|
||||
} else {
|
||||
// normal subscriptions
|
||||
!new_tab ? this.router.navigate(['/player', {fileNames: file.id,
|
||||
type: file.isAudio ? 'audio' : 'video', subscriptionName: sub.name,
|
||||
subPlaylist: sub.isPlaylist}])
|
||||
: window.open(`/#/player;fileNames=${file.id};type=${file.isAudio ? 'audio' : 'video'};subscriptionName=${sub.name};subPlaylist=${sub.isPlaylist}`);
|
||||
}
|
||||
} else {
|
||||
// normal files
|
||||
!new_tab ? this.router.navigate(['/player', {type: file.isAudio ? 'audio' : 'video', uid: file.uid}])
|
||||
: window.open(`/#/player;type=${file.isAudio ? 'audio' : 'video'};uid=${file.uid}`);
|
||||
}
|
||||
}
|
||||
|
||||
goToSubscription(file) {
|
||||
this.router.navigate(['/subscription', {id: file.sub_id}]);
|
||||
}
|
||||
|
||||
// downloading
|
||||
|
||||
downloadFile(file) {
|
||||
if (file.sub_id) {
|
||||
this.downloadSubscriptionFile(file);
|
||||
} else {
|
||||
this.downloadNormalFile(file);
|
||||
}
|
||||
}
|
||||
|
||||
downloadSubscriptionFile(file) {
|
||||
const type = file.isAudio ? 'audio' : 'video';
|
||||
const ext = type === 'audio' ? '.mp3' : '.mp4'
|
||||
const sub = this.postsService.getSubscriptionByID(file.sub_id);
|
||||
this.postsService.downloadFileFromServer(file.id, type, null, null, sub.name, sub.isPlaylist,
|
||||
this.postsService.user ? this.postsService.user.uid : null, null).subscribe(res => {
|
||||
const blob: Blob = res;
|
||||
saveAs(blob, file.id + ext);
|
||||
}, err => {
|
||||
console.log(err);
|
||||
});
|
||||
}
|
||||
|
||||
downloadNormalFile(file) {
|
||||
const type = file.isAudio ? 'audio' : 'video';
|
||||
const ext = type === 'audio' ? '.mp3' : '.mp4'
|
||||
const name = file.id;
|
||||
this.downloading_content[type][name] = true;
|
||||
this.postsService.downloadFileFromServer(name, type).subscribe(res => {
|
||||
this.downloading_content[type][name] = false;
|
||||
const blob: Blob = res;
|
||||
saveAs(blob, decodeURIComponent(name) + ext);
|
||||
|
||||
if (!this.postsService.config.Extra.file_manager_enabled) {
|
||||
// tell server to delete the file once downloaded
|
||||
this.postsService.deleteFile(name, false).subscribe(delRes => {
|
||||
// reload mp4s
|
||||
this.getAllFiles();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// deleting
|
||||
|
||||
deleteFile(args) {
|
||||
const file = args.file;
|
||||
const index = args.index;
|
||||
const blacklistMode = args.blacklistMode;
|
||||
|
||||
if (file.sub_id) {
|
||||
this.deleteSubscriptionFile(file, index, blacklistMode);
|
||||
} else {
|
||||
this.deleteNormalFile(file, index, blacklistMode);
|
||||
}
|
||||
}
|
||||
|
||||
deleteNormalFile(file, index, blacklistMode = false) {
|
||||
this.postsService.deleteFile(file.uid, file.isAudio, blacklistMode).subscribe(result => {
|
||||
if (result) {
|
||||
this.postsService.openSnackBar('Delete success!', 'OK.');
|
||||
this.files.splice(index, 1);
|
||||
} else {
|
||||
this.postsService.openSnackBar('Delete failed!', 'OK.');
|
||||
}
|
||||
}, err => {
|
||||
this.postsService.openSnackBar('Delete failed!', 'OK.');
|
||||
});
|
||||
}
|
||||
|
||||
deleteSubscriptionFile(file, index, blacklistMode = false) {
|
||||
if (blacklistMode) {
|
||||
this.deleteForever(file, index);
|
||||
} else {
|
||||
this.deleteAndRedownload(file, index);
|
||||
}
|
||||
}
|
||||
|
||||
deleteAndRedownload(file, index) {
|
||||
const sub = this.postsService.getSubscriptionByID(file.sub_id);
|
||||
this.postsService.deleteSubscriptionFile(sub, file.id, false, file.uid).subscribe(res => {
|
||||
this.postsService.openSnackBar(`Successfully deleted file: '${file.id}'`);
|
||||
this.files.splice(index, 1);
|
||||
});
|
||||
}
|
||||
|
||||
deleteForever(file, index) {
|
||||
const sub = this.postsService.getSubscriptionByID(file.sub_id);
|
||||
this.postsService.deleteSubscriptionFile(sub, file.id, true, file.uid).subscribe(res => {
|
||||
this.postsService.openSnackBar(`Successfully deleted file: '${file.id}'`);
|
||||
this.files.splice(index, 1);
|
||||
});
|
||||
}
|
||||
|
||||
// sorting and filtering
|
||||
|
||||
sortFiles(a, b) {
|
||||
// uses the 'registered' flag as the timestamp
|
||||
const result = b.registered - a.registered;
|
||||
return result;
|
||||
}
|
||||
|
||||
durationStringToNumber(dur_str) {
|
||||
let num_sum = 0;
|
||||
const dur_str_parts = dur_str.split(':');
|
||||
for (let i = dur_str_parts.length-1; i >= 0; i--) {
|
||||
num_sum += parseInt(dur_str_parts[i])*(60**(dur_str_parts.length-1-i));
|
||||
}
|
||||
return num_sum;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
<div (mouseover)="elevated=true" (mouseout)="elevated=false" style="position: relative; width: fit-content;">
|
||||
<div *ngIf="!loading" class="download-time"><mat-icon class="audio-video-icon">{{(file_obj.type === 'audio' || file_obj.isAudio) ? 'audiotrack' : 'movie'}}</mat-icon> {{file_obj.registered | date:'shortDate'}}</div>
|
||||
<div *ngIf="loading" class="download-time" style="width: 75%; margin-top: 5px;"><content-loader [primaryColor]="theme.ghost_primary" [secondaryColor]="theme.ghost_secondary" width="250" height="30"><svg:rect x="0" y="0" rx="3" ry="3" width="250" height="30" /></content-loader></div>
|
||||
<button [disabled]="loading" [matMenuTriggerFor]="action_menu" class="menuButton" mat-icon-button><mat-icon>more_vert</mat-icon></button>
|
||||
<mat-menu #action_menu="matMenu">
|
||||
<ng-container *ngIf="!is_playlist && !loading">
|
||||
<button (click)="openFileInfoDialog()" mat-menu-item><mat-icon>info</mat-icon><ng-container i18n="Video info button">Info</ng-container></button>
|
||||
<button (click)="navigateToSubscription()" mat-menu-item *ngIf="file_obj.sub_id"><mat-icon>{{file_obj.isAudio ? 'library_music' : 'video_library'}}</mat-icon> <ng-container i18n="Go to subscription menu item">Go to subscription</ng-container></button>
|
||||
<mat-divider></mat-divider>
|
||||
<button *ngIf="file_obj.sub_id" (click)="emitDeleteFile()" mat-menu-item>
|
||||
<mat-icon>restore</mat-icon><ng-container i18n="Delete and redownload subscription video button">Delete and redownload</ng-container>
|
||||
</button>
|
||||
<button *ngIf="file_obj.sub_id && use_youtubedl_archive" (click)="emitDeleteFile(true)" mat-menu-item>
|
||||
<mat-icon>delete_forever</mat-icon><ng-container i18n="Delete forever subscription video button">Delete forever</ng-container>
|
||||
</button>
|
||||
<button *ngIf="!file_obj.sub_id" (click)="emitDeleteFile()" mat-menu-item><mat-icon>delete</mat-icon><ng-container i18n="Delete video button">Delete</ng-container></button>
|
||||
<button *ngIf="!file_obj.sub_id && use_youtubedl_archive" (click)="emitDeleteFile(true)" mat-menu-item><mat-icon>delete_forever</mat-icon><ng-container i18n="Delete and blacklist video button">Delete and blacklist</ng-container></button>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="is_playlist && !loading">
|
||||
<button (click)="emitEditPlaylist()" mat-menu-item><mat-icon>edit</mat-icon><ng-container i18n="Playlist edit button">Edit</ng-container></button>
|
||||
<mat-divider></mat-divider>
|
||||
<button (click)="emitDeleteFile()" mat-menu-item><mat-icon>delete_forever</mat-icon><ng-container i18n="Delete playlist">Delete</ng-container></button>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="loading">
|
||||
<button mat-menu-item>Placeholder</button>
|
||||
</ng-container>
|
||||
</mat-menu>
|
||||
<mat-card [matTooltip]="null" (click)="navigateToFile($event)" matRipple class="file-mat-card" [ngClass]="{'small-mat-card': card_size === 'small', 'file-mat-card': card_size === 'medium', 'large-mat-card': card_size === 'large', 'mat-elevation-z4': !elevated, 'mat-elevation-z8': elevated}">
|
||||
<div style="padding:5px">
|
||||
<div *ngIf="!loading && file_obj.thumbnailURL" class="img-div">
|
||||
<div style="position: relative">
|
||||
<img [ngClass]="{'image-small': card_size === 'small', 'image': card_size === 'medium', 'image-large': card_size === 'large'}" [src]="file_obj.thumbnailBlob ? thumbnailBlobURL : file_obj.thumbnailURL" alt="Thumbnail">
|
||||
<div class="duration-time">
|
||||
{{file_length}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div *ngIf="loading" class="img-div">
|
||||
<content-loader [primaryColor]="theme.ghost_primary" [secondaryColor]="theme.ghost_secondary" width="100" height="55"><svg:rect x="0" y="0" rx="0" ry="0" width="100" height="55" /></content-loader>
|
||||
</div>
|
||||
|
||||
<span *ngIf="!loading" [ngClass]="{'max-two-lines': card_size !== 'small', 'max-one-line': card_size === 'small' }">{{card_size === 'large' && file_obj.uploader ? file_obj.uploader + ' - ' : ''}}<strong>{{!is_playlist ? file_obj.title : file_obj.name}}</strong></span>
|
||||
<span *ngIf="loading" class="title-loading"><content-loader [primaryColor]="theme.ghost_primary" [secondaryColor]="theme.ghost_secondary" width="250" height="30"><svg:rect x="0" y="0" rx="3" ry="3" width="250" height="30" /></content-loader></span>
|
||||
</div>
|
||||
</mat-card>
|
||||
</div>
|
||||
@@ -0,0 +1,137 @@
|
||||
.large-mat-card {
|
||||
width: 300px;
|
||||
height: 250px;
|
||||
padding: 0px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.file-mat-card {
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
padding: 0px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.small-mat-card {
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
padding: 0px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.menuButton {
|
||||
right: 0px;
|
||||
top: -1px;
|
||||
position: absolute;
|
||||
z-index: 999;
|
||||
|
||||
}
|
||||
|
||||
/* Coerce the <span> icon container away from display:inline */
|
||||
.mat-icon-button .mat-button-wrapper {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.image-large {
|
||||
width: 300px;
|
||||
height: 167.5px;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.image {
|
||||
width: 200px;
|
||||
height: 112.5px;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.image-small {
|
||||
width: 150px;
|
||||
height: 84.5px;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.example-full-width-height {
|
||||
width: 100%;
|
||||
height: 100%
|
||||
}
|
||||
|
||||
.centered {
|
||||
margin: 0 auto;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
}
|
||||
|
||||
.img-div {
|
||||
max-height: 80px;
|
||||
padding: 0px;
|
||||
margin: 32px 0px 0px -5px;
|
||||
width: calc(100% + 5px + 5px);
|
||||
}
|
||||
|
||||
.max-two-lines {
|
||||
display: -webkit-box;
|
||||
display: -moz-box;
|
||||
max-height: 2.4em;
|
||||
line-height: 1.2em;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
bottom: 5px;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.max-one-line {
|
||||
display: -webkit-box;
|
||||
display: -moz-box;
|
||||
max-height: 1.2em;
|
||||
line-height: 1.2em;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
bottom: 5px;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.duration-time {
|
||||
position: absolute;
|
||||
bottom: 5px;
|
||||
right: 5px;
|
||||
z-index: 99999;
|
||||
background: rgba(255,255,255,0.6);
|
||||
padding-left: 5px;
|
||||
padding-right: 5px;
|
||||
color: black;
|
||||
}
|
||||
|
||||
.download-time {
|
||||
position: absolute;
|
||||
top: 1px;
|
||||
left: 5px;
|
||||
z-index: 99999;
|
||||
}
|
||||
|
||||
.audio-video-icon {
|
||||
position: relative;
|
||||
top: 6px;
|
||||
}
|
||||
|
||||
.title-loading {
|
||||
width: 93%;
|
||||
position: absolute;
|
||||
bottom: 1px;
|
||||
}
|
||||
|
||||
@media (max-width: 576px){
|
||||
|
||||
// .example-card {
|
||||
// width: 175px !important;
|
||||
// }
|
||||
|
||||
// .image {
|
||||
// width: 175px;
|
||||
// }
|
||||
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { UnifiedFileCardComponent } from './unified-file-card.component';
|
||||
|
||||
describe('UnifiedFileCardComponent', () => {
|
||||
let component: UnifiedFileCardComponent;
|
||||
let fixture: ComponentFixture<UnifiedFileCardComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ UnifiedFileCardComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(UnifiedFileCardComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,124 @@
|
||||
import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { VideoInfoDialogComponent } from 'app/dialogs/video-info-dialog/video-info-dialog.component';
|
||||
import { DomSanitizer } from '@angular/platform-browser';
|
||||
|
||||
@Component({
|
||||
selector: 'app-unified-file-card',
|
||||
templateUrl: './unified-file-card.component.html',
|
||||
styleUrls: ['./unified-file-card.component.scss']
|
||||
})
|
||||
export class UnifiedFileCardComponent implements OnInit {
|
||||
|
||||
// required info
|
||||
file_title = '';
|
||||
file_length = '';
|
||||
file_thumbnail = '';
|
||||
type = null;
|
||||
elevated = false;
|
||||
|
||||
// optional vars
|
||||
thumbnailBlobURL = null;
|
||||
|
||||
// input/output
|
||||
@Input() loading = true;
|
||||
@Input() theme = null;
|
||||
@Input() file_obj = null;
|
||||
@Input() card_size = 'medium';
|
||||
@Input() use_youtubedl_archive = false;
|
||||
@Input() is_playlist = false;
|
||||
@Input() index: number;
|
||||
@Output() goToFile = new EventEmitter<any>();
|
||||
@Output() goToSubscription = new EventEmitter<any>();
|
||||
@Output() deleteFile = new EventEmitter<any>();
|
||||
@Output() editPlaylist = new EventEmitter<any>();
|
||||
|
||||
/*
|
||||
Planned sizes:
|
||||
small: 150x175
|
||||
medium: 200x200
|
||||
big: 250x200
|
||||
*/
|
||||
|
||||
constructor(private dialog: MatDialog, private sanitizer: DomSanitizer) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
if (!this.loading) {
|
||||
this.file_length = fancyTimeFormat(this.file_obj.duration);
|
||||
}
|
||||
|
||||
if (this.file_obj && this.file_obj.thumbnailBlob) {
|
||||
const mime = getMimeByFilename(this.file_obj.thumbnailPath);
|
||||
const blob = new Blob([new Uint8Array(this.file_obj.thumbnailBlob.data)], {type: mime});
|
||||
const bloburl = URL.createObjectURL(blob);
|
||||
this.thumbnailBlobURL = this.sanitizer.bypassSecurityTrustUrl(bloburl);
|
||||
}
|
||||
}
|
||||
|
||||
emitDeleteFile(blacklistMode = false) {
|
||||
this.deleteFile.emit({
|
||||
file: this.file_obj,
|
||||
index: this.index,
|
||||
blacklistMode: blacklistMode
|
||||
});
|
||||
}
|
||||
|
||||
navigateToFile(event) {
|
||||
this.goToFile.emit({file: this.file_obj, event: event});
|
||||
}
|
||||
|
||||
navigateToSubscription() {
|
||||
this.goToSubscription.emit(this.file_obj);
|
||||
}
|
||||
|
||||
openFileInfoDialog() {
|
||||
this.dialog.open(VideoInfoDialogComponent, {
|
||||
data: {
|
||||
file: this.file_obj,
|
||||
},
|
||||
minWidth: '50vw'
|
||||
})
|
||||
}
|
||||
|
||||
emitEditPlaylist() {
|
||||
this.editPlaylist.emit({
|
||||
playlist: this.file_obj,
|
||||
index: this.index
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function fancyTimeFormat(time) {
|
||||
if (typeof time === 'string') {
|
||||
return time;
|
||||
}
|
||||
// Hours, minutes and seconds
|
||||
const hrs = ~~(time / 3600);
|
||||
const mins = ~~((time % 3600) / 60);
|
||||
const secs = ~~time % 60;
|
||||
|
||||
// Output like "1:01" or "4:03:59" or "123:03:59"
|
||||
let ret = '';
|
||||
|
||||
if (hrs > 0) {
|
||||
ret += '' + hrs + ':' + (mins < 10 ? '0' : '');
|
||||
}
|
||||
|
||||
ret += '' + mins + ':' + (secs < 10 ? '0' : '');
|
||||
ret += '' + secs;
|
||||
return ret;
|
||||
}
|
||||
|
||||
function getMimeByFilename(name) {
|
||||
switch (name.substring(name.length-4, name.length)) {
|
||||
case '.jpg':
|
||||
return 'image/jpeg';
|
||||
case '.png':
|
||||
return 'image/png';
|
||||
case 'webp':
|
||||
return 'image/webp';
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1 +1 @@
|
||||
export const CURRENT_VERSION = 'v4.0';
|
||||
export const CURRENT_VERSION = 'v4.1';
|
||||
|
||||
@@ -1,18 +1,34 @@
|
||||
<h4 mat-dialog-title i18n="Create a playlist dialog title">Create a playlist</h4>
|
||||
<form>
|
||||
<div>
|
||||
<mat-form-field color="accent">
|
||||
<input [(ngModel)]="name" matInput placeholder="Name" i18n-placeholder="Playlist name placeholder" type="text" required aria-required [ngModelOptions]="{standalone: true}">
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div>
|
||||
<mat-form-field color="accent">
|
||||
<mat-label *ngIf="type === 'audio'"><ng-container i18n="Audio files title">Audio files</ng-container></mat-label>
|
||||
<mat-label *ngIf="type === 'video'"><ng-container i18n="Videos title">Videos</ng-container></mat-label>
|
||||
<mat-select [formControl]="filesSelect" multiple required aria-required>
|
||||
<mat-option *ngFor="let file of filesToSelectFrom" [value]="file.id">{{file.id}}</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<div *ngIf="filesToSelectFrom || (audiosToSelectFrom && videosToSelectFrom)">
|
||||
<div>
|
||||
<mat-form-field color="accent">
|
||||
<input [(ngModel)]="name" matInput placeholder="Name" i18n-placeholder="Playlist name placeholder" type="text" required aria-required [ngModelOptions]="{standalone: true}">
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div *ngIf="!filesToSelectFrom">
|
||||
<mat-form-field color="accent">
|
||||
<mat-select placeholder="Type" i18n-placeholder="Type select" [(ngModel)]="type" [ngModelOptions]="{standalone: true}">
|
||||
<mat-option value="audio"><ng-container i18n="Audio">Audio</ng-container></mat-option>
|
||||
<mat-option value="video"><ng-container i18n="Video">Video</ng-container></mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div>
|
||||
<mat-form-field *ngIf="type && ((filesToSelectFrom && filesToSelectFrom.length > 0) || (type === 'audio' && audiosToSelectFrom && audiosToSelectFrom.length > 0) || (type === 'video' && videosToSelectFrom && videosToSelectFrom.length > 0))" color="accent">
|
||||
<mat-label *ngIf="type === 'audio'"><ng-container i18n="Audio files title">Audio files</ng-container></mat-label>
|
||||
<mat-label *ngIf="type === 'video'"><ng-container i18n="Videos title">Videos</ng-container></mat-label>
|
||||
<mat-select [formControl]="filesSelect" multiple required aria-required>
|
||||
<ng-container *ngIf="filesToSelectFrom"><mat-option *ngFor="let file of filesToSelectFrom" [value]="file.id">{{file.id}}</mat-option></ng-container>
|
||||
<ng-container *ngIf="audiosToSelectFrom && type === 'audio'"><mat-option *ngFor="let file of audiosToSelectFrom" [value]="file.id">{{file.id}}</mat-option></ng-container>
|
||||
<ng-container *ngIf="videosToSelectFrom && type === 'video'"><mat-option *ngFor="let file of videosToSelectFrom" [value]="file.id">{{file.id}}</mat-option></ng-container>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<!-- No videos available -->
|
||||
<div style="margin-bottom: 15px;" *ngIf="type && ((filesToSelectFrom && filesToSelectFrom.length === 0) || (type === 'audio' && audiosToSelectFrom && audiosToSelectFrom.length === 0) || (type === 'video' && videosToSelectFrom && videosToSelectFrom.length === 0))">
|
||||
No files available.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
|
||||
@@ -14,6 +14,8 @@ export class CreatePlaylistComponent implements OnInit {
|
||||
filesToSelectFrom = null;
|
||||
type = null;
|
||||
filesSelect = new FormControl();
|
||||
audiosToSelectFrom = null;
|
||||
videosToSelectFrom = null;
|
||||
name = '';
|
||||
|
||||
create_in_progress = false;
|
||||
@@ -28,12 +30,30 @@ export class CreatePlaylistComponent implements OnInit {
|
||||
this.filesToSelectFrom = this.data.filesToSelectFrom;
|
||||
this.type = this.data.type;
|
||||
}
|
||||
|
||||
if (!this.filesToSelectFrom) {
|
||||
this.getMp3s();
|
||||
this.getMp4s();
|
||||
}
|
||||
}
|
||||
|
||||
getMp3s() {
|
||||
this.postsService.getMp3s().subscribe(result => {
|
||||
this.audiosToSelectFrom = result['mp3s'];
|
||||
});
|
||||
}
|
||||
|
||||
getMp4s() {
|
||||
this.postsService.getMp4s().subscribe(result => {
|
||||
this.videosToSelectFrom = result['mp4s'];
|
||||
});
|
||||
}
|
||||
|
||||
createPlaylist() {
|
||||
const thumbnailURL = this.getThumbnailURL();
|
||||
const duration = this.calculateDuration();
|
||||
this.create_in_progress = true;
|
||||
this.postsService.createPlaylist(this.name, this.filesSelect.value, this.type, thumbnailURL).subscribe(res => {
|
||||
this.postsService.createPlaylist(this.name, this.filesSelect.value, this.type, thumbnailURL, duration).subscribe(res => {
|
||||
this.create_in_progress = false;
|
||||
if (res['success']) {
|
||||
this.dialogRef.close(true);
|
||||
@@ -44,8 +64,12 @@ export class CreatePlaylistComponent implements OnInit {
|
||||
}
|
||||
|
||||
getThumbnailURL() {
|
||||
for (let i = 0; i < this.filesToSelectFrom.length; i++) {
|
||||
const file = this.filesToSelectFrom[i];
|
||||
let properFilesToSelectFrom = this.filesToSelectFrom;
|
||||
if (!this.filesToSelectFrom) {
|
||||
properFilesToSelectFrom = this.type === 'audio' ? this.audiosToSelectFrom : this.videosToSelectFrom;
|
||||
}
|
||||
for (let i = 0; i < properFilesToSelectFrom.length; i++) {
|
||||
const file = properFilesToSelectFrom[i];
|
||||
if (file.id === this.filesSelect.value[0]) {
|
||||
// different services store the thumbnail in different places
|
||||
if (file.thumbnailURL) { return file.thumbnailURL };
|
||||
@@ -55,4 +79,35 @@ export class CreatePlaylistComponent implements OnInit {
|
||||
return null;
|
||||
}
|
||||
|
||||
getDuration(file_id) {
|
||||
let properFilesToSelectFrom = this.filesToSelectFrom;
|
||||
if (!this.filesToSelectFrom) {
|
||||
properFilesToSelectFrom = this.type === 'audio' ? this.audiosToSelectFrom : this.videosToSelectFrom;
|
||||
}
|
||||
for (let i = 0; i < properFilesToSelectFrom.length; i++) {
|
||||
const file = properFilesToSelectFrom[i];
|
||||
if (file.id === file_id) {
|
||||
return file.duration;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
calculateDuration() {
|
||||
let sum = 0;
|
||||
for (let i = 0; i < this.filesSelect.value.length; i++) {
|
||||
const duration_val = this.getDuration(this.filesSelect.value[i]);
|
||||
sum += typeof duration_val === 'string' ? this.durationStringToNumber(duration_val) : duration_val;
|
||||
}
|
||||
return sum;
|
||||
}
|
||||
|
||||
durationStringToNumber(dur_str) {
|
||||
let num_sum = 0;
|
||||
const dur_str_parts = dur_str.split(':');
|
||||
for (let i = dur_str_parts.length-1; i >= 0; i--) {
|
||||
num_sum += parseInt(dur_str_parts[i])*(60**(dur_str_parts.length-1-i));
|
||||
}
|
||||
return num_sum;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,15 +18,43 @@
|
||||
<h5 style="margin-top: 10px;">Installation details:</h5>
|
||||
<p>
|
||||
<ng-container i18n="Version label">Installed version:</ng-container> {{current_version_tag}} - <span style="display: inline-block" *ngIf="checking_for_updates"><mat-spinner class="version-spinner" [diameter]="22"></mat-spinner> <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> <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> <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>
|
||||
<ng-container i18n="About bug prefix">Found a bug or have a suggestion?</ng-container> <a [href]="issuesLink" target="_blank"><ng-container i18n="About bug click here">Click here</ng-container></a> <ng-container i18n="About bug suffix">to create an issue!</ng-container>
|
||||
</p>
|
||||
<mat-divider></mat-divider>
|
||||
<div style="margin-top: 10px;">
|
||||
<h5>Personal settings:</h5>
|
||||
<mat-form-field placeholder="Sidepanel mode">
|
||||
<mat-select [(ngModel)]="sidepanel_mode" (selectionChange)="sidePanelModeChanged($event.value)">
|
||||
<mat-option value="over">
|
||||
Over
|
||||
</mat-option>
|
||||
<mat-option value="side">
|
||||
Side
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<br/>
|
||||
<mat-form-field placeholder="Card size">
|
||||
<mat-select [(ngModel)]="card_size" (selectionChange)="cardSizeOptionChanged($event.value)">
|
||||
<mat-option value="large">
|
||||
Large
|
||||
</mat-option>
|
||||
<mat-option value="medium">
|
||||
Medium
|
||||
</mat-option>
|
||||
<mat-option value="small">
|
||||
Small
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
</mat-dialog-content>
|
||||
|
||||
<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>
|
||||
|
||||
@@ -16,11 +16,13 @@ export class AboutDialogComponent implements OnInit {
|
||||
checking_for_updates = true;
|
||||
|
||||
current_version_tag = CURRENT_VERSION;
|
||||
sidepanel_mode = this.postsService.sidepanel_mode;
|
||||
card_size = this.postsService.card_size;
|
||||
|
||||
constructor(private postsService: PostsService) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.getLatestGithubRelease()
|
||||
this.getLatestGithubRelease();
|
||||
}
|
||||
|
||||
getLatestGithubRelease() {
|
||||
@@ -30,4 +32,14 @@ export class AboutDialogComponent implements OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
sidePanelModeChanged(new_mode) {
|
||||
localStorage.setItem('sidepanel_mode', new_mode);
|
||||
this.postsService.sidepanel_mode = new_mode;
|
||||
}
|
||||
|
||||
cardSizeOptionChanged(new_size) {
|
||||
localStorage.setItem('card_size', new_size);
|
||||
this.postsService.card_size = new_size;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,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>
|
||||
@@ -1,6 +0,0 @@
|
||||
.spinner-div {
|
||||
position: absolute;
|
||||
margin: 0 auto;
|
||||
top: 30%;
|
||||
left: 42%;
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
15
src/app/dialogs/confirm-dialog/confirm-dialog.component.html
Normal file
15
src/app/dialogs/confirm-dialog/confirm-dialog.component.html
Normal 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>
|
||||
@@ -0,0 +1,5 @@
|
||||
.spacer {flex: 1 1 auto;}
|
||||
|
||||
.mat-spinner {
|
||||
margin-left: 8px;
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
44
src/app/dialogs/confirm-dialog/confirm-dialog.component.ts
Normal file
44
src/app/dialogs/confirm-dialog/confirm-dialog.component.ts
Normal 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 {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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>
|
||||
@@ -0,0 +1,9 @@
|
||||
.args-edit-button {
|
||||
position: absolute;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.unit-select {
|
||||
width: 75px;
|
||||
margin-left: 20px;
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
<br/>
|
||||
<div class="big demo-basic">
|
||||
<mat-card id="card" style="margin-right: 20px; margin-left: 20px;" [ngClass]="(allowAdvancedDownload) ? 'no-border-radius-bottom' : null">
|
||||
<mat-card-title>
|
||||
<ng-container i18n="Youtube downloader home page label">Youtube Downloader</ng-container>
|
||||
</mat-card-title>
|
||||
<mat-card-content>
|
||||
<div style="position: relative;">
|
||||
<mat-card-content style="padding: 0px 8px 0px 8px;">
|
||||
<div style="position: relative; margin-right: 15px;">
|
||||
<form class="example-form">
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
@@ -167,7 +164,7 @@
|
||||
<br/>
|
||||
<div class="centered big" id="bar_div" *ngIf="current_download && current_download.downloading; else nofile">
|
||||
<div class="margined">
|
||||
<div [ngClass]="(+percentDownloaded > 99)?'make-room-for-spinner':'equal-sizes'" style="display: inline-block; width: 100%; padding-left: 20px" *ngIf="current_download.percent_complete && current_download.percent_complete > 15;else indeterminateprogress">
|
||||
<div [ngClass]="(+percentDownloaded > 99)?'make-room-for-spinner':'equal-sizes'" style="display: inline-block; width: 100%; padding-left: 20px" *ngIf="current_download.percent_complete && current_download.percent_complete > 1;else indeterminateprogress">
|
||||
<mat-progress-bar style="border-radius: 5px;" mode="determinate" value="{{percentDownloaded}}"></mat-progress-bar>
|
||||
<br/>
|
||||
</div>
|
||||
@@ -183,7 +180,15 @@
|
||||
<ng-template #nofile>
|
||||
|
||||
</ng-template>
|
||||
<div style="margin: 20px" *ngIf="fileManagerEnabled && (!postsService.isLoggedIn || postsService.permissions.includes('filemanager'))">
|
||||
|
||||
<ng-container *ngIf="cachedFileManagerEnabled || fileManagerEnabled">
|
||||
<app-recent-videos></app-recent-videos>
|
||||
<br/>
|
||||
<h4 style="text-align: center">Custom playlists</h4>
|
||||
<app-custom-playlists></app-custom-playlists>
|
||||
</ng-container>
|
||||
|
||||
<!--<div style="margin: 20px" *ngIf="fileManagerEnabled && (!postsService.isLoggedIn || postsService.permissions.includes('filemanager'))">
|
||||
<mat-accordion>
|
||||
<mat-expansion-panel (opened)="accordionOpened('audio')" (closed)="accordionClosed('audio')" (mouseleave)="accordionLeft('audio')" (mouseenter)="accordionEntered('audio')" class="big">
|
||||
<mat-expansion-panel-header>
|
||||
@@ -260,7 +265,7 @@
|
||||
</mat-grid-tile>
|
||||
</mat-grid-list>
|
||||
|
||||
<!-- Add video playlist button -->
|
||||
<!-- Add video playlist button --<
|
||||
<div class="add-playlist-button"><button (click)="openCreatePlaylistDialog('video')" mat-fab><mat-icon>add</mat-icon></button></div>
|
||||
<div *ngIf="playlists.video.length === 0">
|
||||
<ng-container i18n="No video playlists available text">
|
||||
@@ -270,12 +275,4 @@
|
||||
</div>
|
||||
</mat-expansion-panel>
|
||||
</mat-accordion>
|
||||
</div>
|
||||
|
||||
<ng-template #nomp3s>
|
||||
|
||||
</ng-template>
|
||||
|
||||
<ng-template #nomp4s>
|
||||
|
||||
</ng-template>
|
||||
</div>-->
|
||||
@@ -82,8 +82,9 @@ export class MainComponent implements OnInit {
|
||||
useDefaultDownloadingAgent = true;
|
||||
customDownloadingAgent = null;
|
||||
|
||||
// formats cache
|
||||
// cache
|
||||
cachedAvailableFormats = {};
|
||||
cachedFileManagerEnabled = localStorage.getItem('cached_filemanager_enabled') === 'true';
|
||||
|
||||
// youtube api
|
||||
youtubeSearchEnabled = false;
|
||||
@@ -232,7 +233,8 @@ export class MainComponent implements OnInit {
|
||||
|
||||
async loadConfig() {
|
||||
// loading config
|
||||
this.fileManagerEnabled = this.postsService.config['Extra']['file_manager_enabled'];
|
||||
this.fileManagerEnabled = this.postsService.config['Extra']['file_manager_enabled']
|
||||
&& (!this.postsService.isLoggedIn || this.postsService.permissions.includes('filemanager'));
|
||||
this.downloadOnlyMode = this.postsService.config['Extra']['download_only_mode'];
|
||||
this.allowMultiDownloadMode = this.postsService.config['Extra']['allow_multi_download_mode'];
|
||||
this.audioFolderPath = this.postsService.config['Downloader']['path-audio'];
|
||||
@@ -261,6 +263,10 @@ export class MainComponent implements OnInit {
|
||||
}
|
||||
|
||||
// set final cache items
|
||||
|
||||
localStorage.setItem('cached_filemanager_enabled', this.fileManagerEnabled.toString());
|
||||
this.cachedFileManagerEnabled = this.fileManagerEnabled;
|
||||
|
||||
if (this.allowAdvancedDownload) {
|
||||
if (localStorage.getItem('customArgsEnabled') !== null) {
|
||||
this.customArgsEnabled = localStorage.getItem('customArgsEnabled') === 'true';
|
||||
@@ -1033,8 +1039,8 @@ export class MainComponent implements OnInit {
|
||||
}
|
||||
} else if (format_obj.type === 'video') {
|
||||
// check if video format is mp4
|
||||
const key = format.height.toString();
|
||||
if (format.ext === 'mp4') {
|
||||
const key = format.format_note.replace('p', '');
|
||||
if (format.ext === 'mp4' || format.ext === 'mkv' || format.ext === 'webm') {
|
||||
format_obj['height'] = format.height;
|
||||
format_obj['acodec'] = format.acodec;
|
||||
format_obj['format_id'] = format.format_id;
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
|
||||
.video-styles {
|
||||
width: 100%;
|
||||
height: 75vh;
|
||||
}
|
||||
|
||||
::ng-deep .mat-button-toggle-label-content {
|
||||
@@ -65,6 +66,13 @@
|
||||
padding-right: 0px;
|
||||
padding-left: 0.01px;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.audio-col {
|
||||
margin: 0 auto;
|
||||
margin-top: 10px;
|
||||
width: 95%;
|
||||
}
|
||||
|
||||
.save-icon {
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
<div *ngIf="playlist.length > 0 && show_player">
|
||||
<div [ngClass]="(type === 'audio') ? null : 'container-video'" class="container">
|
||||
<div style="max-width: 100%; margin-left: 0px; height: 70vh" class="row">
|
||||
<div [ngClass]="(type === 'audio') ? 'my-2 px-1' : 'video-col'" class="col">
|
||||
<vg-player (onPlayerReady)="onPlayerReady($event)" [style.background-color]="(type === 'audio') ? 'transparent' : 'black'">
|
||||
<div [ngClass]="(type === 'audio') ? null : 'container-video'">
|
||||
<div style="max-width: 100%; margin-left: 0px; height: 70vh">
|
||||
<div style="height: fit-content" [ngClass]="(type === 'audio') ? 'audio-col' : 'video-col'">
|
||||
<vg-player style="height: fit-content; max-height: 75vh" (onPlayerReady)="onPlayerReady($event)" [style.background-color]="(type === 'audio') ? 'transparent' : 'black'">
|
||||
<video [ngClass]="(type === 'audio') ? 'audio-styles' : 'video-styles'" #media class="video-player" [vgMedia]="media" [src]="currentItem.src" id="singleVideo" preload="auto" controls>
|
||||
</video>
|
||||
</vg-player>
|
||||
</div>
|
||||
<div class="col-12 my-2">
|
||||
<div style="height: fit-content; width: 100%; margin-top: 10px;">
|
||||
<mat-button-toggle-group cdkDropList [cdkDropListSortingDisabled]="!id" (cdkDropListDropped)="drop($event)" style="width: 80%; left: 9%" vertical name="videoSelect" aria-label="Video Select" #group="matButtonToggleGroup">
|
||||
<mat-button-toggle cdkDrag *ngFor="let playlist_item of playlist; let i = index" [checked]="currentItem.title === playlist_item.title" (click)="onClickPlaylistItem(playlist_item, i)" class="toggle-button" [value]="playlist_item.title">{{playlist_item.label}}</mat-button-toggle>
|
||||
</mat-button-toggle-group>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Component, OnInit, HostListener, EventEmitter } from '@angular/core';
|
||||
import { Component, OnInit, HostListener, EventEmitter, OnDestroy, AfterViewInit } from '@angular/core';
|
||||
import { VgAPI } from 'ngx-videogular';
|
||||
import { PostsService } from 'app/posts.services';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
@@ -20,7 +20,7 @@ export interface IMedia {
|
||||
templateUrl: './player.component.html',
|
||||
styleUrls: ['./player.component.css']
|
||||
})
|
||||
export class PlayerComponent implements OnInit {
|
||||
export class PlayerComponent implements OnInit, AfterViewInit, 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,15 @@ export class PlayerComponent implements OnInit {
|
||||
}
|
||||
}
|
||||
|
||||
ngAfterViewInit() {
|
||||
this.postsService.sidenav.close();
|
||||
}
|
||||
|
||||
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 +251,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));
|
||||
|
||||
@@ -15,14 +15,14 @@ import * as Fingerprint2 from 'fingerprintjs2';
|
||||
@Injectable()
|
||||
export class PostsService implements CanActivate {
|
||||
path = '';
|
||||
audioFolder = '';
|
||||
videoFolder = '';
|
||||
startPath = null; // 'http://localhost:17442/';
|
||||
startPathSSL = null; // 'https://localhost:17442/'
|
||||
handShakeComplete = false;
|
||||
|
||||
// local settings
|
||||
THEMES_CONFIG = THEMES_CONFIG;
|
||||
theme;
|
||||
settings_changed = new BehaviorSubject<boolean>(false);
|
||||
card_size = 'medium';
|
||||
sidepanel_mode = 'over';
|
||||
|
||||
// auth
|
||||
auth_token = '4241b401-7236-493e-92b5-b72696b9d853';
|
||||
session_id = null;
|
||||
httpOptions = null;
|
||||
@@ -39,19 +39,24 @@ export class PostsService implements CanActivate {
|
||||
|
||||
available_permissions = null;
|
||||
|
||||
// behavior subjects
|
||||
reload_config = new BehaviorSubject<boolean>(false);
|
||||
config_reloaded = new BehaviorSubject<boolean>(false);
|
||||
service_initialized = new BehaviorSubject<boolean>(false);
|
||||
initialized = false;
|
||||
|
||||
settings_changed = new BehaviorSubject<boolean>(false);
|
||||
open_create_default_admin_dialog = new BehaviorSubject<boolean>(false);
|
||||
|
||||
// app status
|
||||
initialized = false;
|
||||
|
||||
// global vars
|
||||
config = null;
|
||||
subscriptions = null;
|
||||
sidenav = null;
|
||||
|
||||
constructor(private http: HttpClient, private router: Router, @Inject(DOCUMENT) private document: Document,
|
||||
public snackBar: MatSnackBar) {
|
||||
console.log('PostsService Initialized...');
|
||||
// this.startPath = window.location.href + '/api/';
|
||||
// this.startPathSSL = window.location.href + '/api/';
|
||||
this.path = this.document.location.origin + '/api/';
|
||||
|
||||
if (isDevMode()) {
|
||||
@@ -81,6 +86,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');
|
||||
@@ -100,6 +106,14 @@ export class PostsService implements CanActivate {
|
||||
this.reload_config.subscribe(yes_reload => {
|
||||
if (yes_reload) { this.reloadConfig(); }
|
||||
});
|
||||
|
||||
if (localStorage.getItem('sidepanel_mode')) {
|
||||
this.sidepanel_mode = localStorage.getItem('sidepanel_mode');
|
||||
}
|
||||
|
||||
if (localStorage.getItem('card_size')) {
|
||||
this.card_size = localStorage.getItem('card_size');
|
||||
}
|
||||
}
|
||||
canActivate(route, state): Promise<boolean> {
|
||||
return new Promise(resolve => {
|
||||
@@ -113,6 +127,15 @@ export class PostsService implements CanActivate {
|
||||
this.theme = this.THEMES_CONFIG[theme];
|
||||
}
|
||||
|
||||
getSubscriptionByID(sub_id) {
|
||||
for (let i = 0; i < this.subscriptions.length; i++) {
|
||||
if (this.subscriptions[i]['id'] === sub_id) {
|
||||
return this.subscriptions[i];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
startHandshake(url: string) {
|
||||
return this.http.get(url + 'geturl');
|
||||
}
|
||||
@@ -131,14 +154,6 @@ export class PostsService implements CanActivate {
|
||||
});
|
||||
}
|
||||
|
||||
getVideoFolder() {
|
||||
return this.http.get(this.startPath + 'videofolder');
|
||||
}
|
||||
|
||||
getAudioFolder() {
|
||||
return this.http.get(this.startPath + 'audiofolder');
|
||||
}
|
||||
|
||||
// tslint:disable-next-line: max-line-length
|
||||
makeMP3(url: string, selectedQuality: string, customQualityConfiguration: string, customArgs: string = null, customOutput: string = null, youtubeUsername: string = null, youtubePassword: string = null, ui_uid = null) {
|
||||
return this.http.post(this.path + 'tomp3', {url: url,
|
||||
@@ -163,12 +178,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() {
|
||||
@@ -207,6 +218,10 @@ export class PostsService implements CanActivate {
|
||||
return this.http.post(this.path + 'getFile', {uid: uid, type: type, uuid: uuid}, this.httpOptions);
|
||||
}
|
||||
|
||||
getAllFiles() {
|
||||
return this.http.post(this.path + 'getAllFiles', {}, this.httpOptions);
|
||||
}
|
||||
|
||||
downloadFileFromServer(fileName, type, outputName = null, fullPathProvided = null, subscriptionName = null, subPlaylist = null,
|
||||
uid = null, uuid = null) {
|
||||
return this.http.post(this.path + 'downloadFile', {fileNames: fileName,
|
||||
@@ -238,16 +253,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() {
|
||||
@@ -262,11 +269,12 @@ export class PostsService implements CanActivate {
|
||||
return this.http.post(this.path + 'disableSharing', {uid: uid, type: type, is_playlist: is_playlist}, this.httpOptions);
|
||||
}
|
||||
|
||||
createPlaylist(playlistName, fileNames, type, thumbnailURL) {
|
||||
createPlaylist(playlistName, fileNames, type, thumbnailURL, duration = null) {
|
||||
return this.http.post(this.path + 'createPlaylist', {playlistName: playlistName,
|
||||
fileNames: fileNames,
|
||||
type: type,
|
||||
thumbnailURL: thumbnailURL}, this.httpOptions);
|
||||
thumbnailURL: thumbnailURL,
|
||||
duration: duration}, this.httpOptions);
|
||||
}
|
||||
|
||||
getPlaylist(playlistID, type, uuid = null) {
|
||||
@@ -293,6 +301,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)
|
||||
}
|
||||
@@ -366,7 +378,7 @@ export class PostsService implements CanActivate {
|
||||
|
||||
// user methods
|
||||
login(username, password) {
|
||||
const call = this.http.post(this.path + 'auth/login', {userid: username, password: password}, this.httpOptions);
|
||||
const call = this.http.post(this.path + 'auth/login', {username: username, password: password}, this.httpOptions);
|
||||
return call;
|
||||
}
|
||||
|
||||
@@ -380,6 +392,8 @@ export class PostsService implements CanActivate {
|
||||
}, err => {
|
||||
if (err.status === 401) {
|
||||
this.sendToLogin();
|
||||
this.token = null;
|
||||
this.resetHttpParams();
|
||||
}
|
||||
console.log(err);
|
||||
});
|
||||
@@ -396,14 +410,7 @@ export class PostsService implements CanActivate {
|
||||
this.router.navigate(['/login']);
|
||||
}
|
||||
|
||||
// resets http params
|
||||
this.http_params = `apiKey=${this.auth_token}&sessionID=${this.session_id}`
|
||||
|
||||
this.httpOptions = {
|
||||
params: new HttpParams({
|
||||
fromString: this.http_params
|
||||
}),
|
||||
};
|
||||
this.resetHttpParams();
|
||||
}
|
||||
|
||||
// user methods
|
||||
@@ -415,7 +422,6 @@ export class PostsService implements CanActivate {
|
||||
}
|
||||
|
||||
sendToLogin() {
|
||||
this.checkAdminCreationStatus();
|
||||
if (!this.initialized) {
|
||||
this.setInitialized();
|
||||
}
|
||||
@@ -429,12 +435,29 @@ export class PostsService implements CanActivate {
|
||||
this.openSnackBar('You must log in to access this page!');
|
||||
}
|
||||
|
||||
resetHttpParams() {
|
||||
// resets http params
|
||||
this.http_params = `apiKey=${this.auth_token}&sessionID=${this.session_id}`
|
||||
|
||||
this.httpOptions = {
|
||||
params: new HttpParams({
|
||||
fromString: this.http_params
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
setInitialized() {
|
||||
this.service_initialized.next(true);
|
||||
this.initialized = true;
|
||||
this.config_reloaded.next(true);
|
||||
}
|
||||
|
||||
reloadSubscriptions() {
|
||||
this.getAllSubscriptions().subscribe(res => {
|
||||
this.subscriptions = res['subscriptions'];
|
||||
});
|
||||
}
|
||||
|
||||
adminExists() {
|
||||
return this.http.post(this.path + 'auth/adminExists', {}, this.httpOptions);
|
||||
}
|
||||
@@ -445,8 +468,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 => {
|
||||
|
||||
@@ -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>
|
||||
@@ -42,26 +42,6 @@
|
||||
</div>
|
||||
</div>
|
||||
<mat-divider></mat-divider>
|
||||
<div *ngIf="new_config" class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-12 mt-3">
|
||||
<mat-checkbox color="accent" [(ngModel)]="new_config['Encryption']['use-encryption']"><ng-container i18n="Use encryption setting">Use encryption</ng-container></mat-checkbox>
|
||||
</div>
|
||||
|
||||
<div class="col-12">
|
||||
<mat-form-field class="text-field" color="accent">
|
||||
<input [disabled]="!new_config['Encryption']['use-encryption']" [(ngModel)]="new_config['Encryption']['cert-file-path']" matInput placeholder="Cert file path" i18n-placeholder="Cert file path input placeholder">
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div class="col-12">
|
||||
<mat-form-field class="text-field" color="accent">
|
||||
<input [disabled]="!new_config['Encryption']['use-encryption']" [(ngModel)]="new_config['Encryption']['key-file-path']" matInput placeholder="Key file path" i18n-placeholder="Key file path input placeholder">
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<mat-divider></mat-divider>
|
||||
<div *ngIf="new_config" class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-12 mt-3">
|
||||
@@ -73,17 +53,12 @@
|
||||
<mat-hint><ng-container i18n="Subscriptions base path setting input hint">Base path for videos from your subscribed channels and playlists. It is relative to YTDL-Material's root folder.</ng-container></mat-hint>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="col-12 mt-5">
|
||||
<div class="col-12 mt-5 mb-3">
|
||||
<mat-form-field class="text-field" color="accent">
|
||||
<input [disabled]="!new_config['Subscriptions']['allow_subscriptions']" [(ngModel)]="new_config['Subscriptions']['subscriptions_check_interval']" matInput placeholder="Check interval" i18n-placeholder="Check interval input setting placeholder">
|
||||
<mat-hint><ng-container i18n="Check interval setting input hint">Unit is seconds, only include numbers.</ng-container></mat-hint>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="col-12 mt-4">
|
||||
<mat-checkbox color="accent" [disabled]="!new_config['Subscriptions']['allow_subscriptions']" [(ngModel)]="new_config['Subscriptions']['subscriptions_use_youtubedl_archive']"><ng-container i18n="Use youtube-dl archive setting">Use youtube-dl archive</ng-container></mat-checkbox>
|
||||
<p><a target="_blank" href="https://github.com/ytdl-org/youtube-dl/blob/master/README.md#how-do-i-download-only-new-videos-from-a-playlist"><ng-container i18n="youtube-dl archive explanation prefix link">With youtube-dl's archive</ng-container></a> <ng-container i18n="youtube-dl archive explanation middle">feature, downloaded videos from your subscriptions get recorded in a text file in the subscriptions archive sub-directory.</ng-container></p>
|
||||
<p><ng-container i18n="youtube-dl archive explanation suffix">This enables the ability to permanently delete videos from your subscriptions without unsubscribing, and allows you to record which videos you downloaded in case of data loss.</ng-container></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<mat-divider></mat-divider>
|
||||
@@ -140,7 +115,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,13 +123,20 @@
|
||||
</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>
|
||||
</div>
|
||||
|
||||
<div class="col-12 mt-3">
|
||||
<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 class="col-12 mt-2">
|
||||
<mat-checkbox color="accent" [(ngModel)]="new_config['Downloader']['include_thumbnail']"><ng-container i18n="Include thumbnail setting">Include thumbnail</ng-container></mat-checkbox>
|
||||
</div>
|
||||
|
||||
<div class="col-12 mt-2">
|
||||
<mat-checkbox color="accent" [(ngModel)]="new_config['Downloader']['include_metadata']"><ng-container i18n="Include metadata setting">Include metadata</ng-container></mat-checkbox>
|
||||
</div>
|
||||
|
||||
<div class="col-12 mt-2">
|
||||
<button (click)="killAllDownloads()" mat-stroked-button color="warn"><ng-container i18n="Kill all downloads button">Kill all downloads</ng-container></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -186,10 +168,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 +184,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>
|
||||
@@ -285,6 +263,18 @@
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="col-12 mt-2 mb-1">
|
||||
<mat-form-field>
|
||||
<mat-label><ng-container i18n="Login expiration select label">Login expiration</ng-container></mat-label>
|
||||
<mat-select color="accent" [(ngModel)]="new_config['Advanced']['jwt_expiration']">
|
||||
<mat-option [value]="3600">1 Hour</mat-option>
|
||||
<mat-option [value]="86400">1 Day</mat-option>
|
||||
<mat-option [value]="604800">1 Week</mat-option>
|
||||
<mat-option [value]="2592000">1 Month</mat-option>
|
||||
<mat-option [value]="31536000">1 Year</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<mat-checkbox color="accent" [(ngModel)]="new_config['Advanced']['allow_advanced_download']"><ng-container i18n="Allow advanced downloading setting">Allow advanced download</ng-container></mat-checkbox>
|
||||
</div>
|
||||
@@ -306,8 +296,50 @@
|
||||
</ng-template>
|
||||
</mat-tab>
|
||||
<mat-tab *ngIf="postsService.config && postsService.config.Advanced.multi_user_mode" label="Users" i18n-label="Users settings label">
|
||||
<div style="margin-left: 48px; margin-top: 24px;">
|
||||
<mat-checkbox color="accent" [(ngModel)]="new_config['Users']['allow_registration']"><ng-container i18n="Allow registration setting">Allow user registration</ng-container></mat-checkbox>
|
||||
|
||||
<div style="margin-left: 48px; margin-top: 24px; margin-bottom: -25px;">
|
||||
<div>
|
||||
<mat-checkbox color="accent" [(ngModel)]="new_config['Users']['allow_registration']"><ng-container i18n="Allow registration setting">Allow user registration</ng-container></mat-checkbox>
|
||||
</div>
|
||||
<mat-divider></mat-divider>
|
||||
<mat-form-field style="margin-top: 15px;">
|
||||
<mat-select [(ngModel)]="new_config['Users']['auth_method']" placeholder="Auth method" i18n-placeholder="Auth method select">
|
||||
<mat-option value="internal">
|
||||
<ng-container i18n="Internal auth method">Internal</ng-container>
|
||||
</mat-option>
|
||||
<mat-option value="ldap">
|
||||
<ng-container i18n="LDAP auth method">LDAP</ng-container>
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<div *ngIf="new_config['Users']['auth_method'] === 'ldap'">
|
||||
<div>
|
||||
<mat-form-field>
|
||||
<input matInput i18n-placeholder="LDAP URL" placeholder="LDAP URL" [(ngModel)]="new_config['Users']['ldap_config']['url']">
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div>
|
||||
<mat-form-field>
|
||||
<input matInput i18n-placeholder="Bind DN" placeholder="Bind DN" [(ngModel)]="new_config['Users']['ldap_config']['bindDN']">
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div>
|
||||
<mat-form-field>
|
||||
<input matInput i18n-placeholder="Bind Credentials" placeholder="Bind Credentials" [(ngModel)]="new_config['Users']['ldap_config']['bindCredentials']">
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div>
|
||||
<mat-form-field>
|
||||
<input matInput i18n-placeholder="Search Base" placeholder="Search Base" [(ngModel)]="new_config['Users']['ldap_config']['searchBase']">
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div>
|
||||
<mat-form-field>
|
||||
<input matInput i18n-placeholder="Search Filter" placeholder="Search Filter" [(ngModel)]="new_config['Users']['ldap_config']['searchFilter']">
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
<mat-divider></mat-divider>
|
||||
</div>
|
||||
<app-modify-users></app-modify-users>
|
||||
</mat-tab>
|
||||
|
||||
@@ -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, {
|
||||
|
||||
@@ -108,4 +108,4 @@ function fancyTimeFormat(time) {
|
||||
ret += '' + mins + ':' + (secs < 10 ? '0' : '');
|
||||
ret += '' + secs;
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -54,7 +54,13 @@
|
||||
|
||||
.save-button {
|
||||
right: 25px;
|
||||
position: absolute;
|
||||
position: fixed;
|
||||
bottom: 25px;
|
||||
}
|
||||
|
||||
.edit-button {
|
||||
left: 25px;
|
||||
position: fixed;
|
||||
bottom: 25px;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { PostsService } from 'app/posts.services';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { ActivatedRoute, Router, ParamMap } from '@angular/router';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { EditSubscriptionDialogComponent } from 'app/dialogs/edit-subscription-dialog/edit-subscription-dialog.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-subscription',
|
||||
@@ -43,9 +45,21 @@ export class SubscriptionComponent implements OnInit {
|
||||
filterProperty = this.filterProperties['upload_date'];
|
||||
downloading = false;
|
||||
|
||||
constructor(private postsService: PostsService, private route: ActivatedRoute, private router: Router) { }
|
||||
initialized = false;
|
||||
|
||||
constructor(private postsService: PostsService, private route: ActivatedRoute, private router: Router, private dialog: MatDialog) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.route.paramMap.subscribe((params: ParamMap) => {
|
||||
this.id = params.get('id');
|
||||
this.postsService.service_initialized.subscribe(init => {
|
||||
if (init) {
|
||||
this.initialized = true;
|
||||
this.getConfig();
|
||||
this.getSubscription();
|
||||
}
|
||||
});
|
||||
});
|
||||
if (this.route.snapshot.paramMap.get('id')) {
|
||||
this.id = this.route.snapshot.paramMap.get('id');
|
||||
|
||||
@@ -82,7 +96,7 @@ export class SubscriptionComponent implements OnInit {
|
||||
}
|
||||
|
||||
getConfig() {
|
||||
this.use_youtubedl_archive = this.postsService.config['Subscriptions']['subscriptions_use_youtubedl_archive'];
|
||||
this.use_youtubedl_archive = this.postsService.config['Downloader']['use_youtubedl_archive'];
|
||||
}
|
||||
|
||||
goToFile(emit_obj) {
|
||||
@@ -121,7 +135,6 @@ export class SubscriptionComponent implements OnInit {
|
||||
}
|
||||
|
||||
filterOptionChanged(value) {
|
||||
// this.filterProperty = value;
|
||||
this.filterByProperty(value['property']);
|
||||
localStorage.setItem('filter_property', value['key']);
|
||||
}
|
||||
@@ -148,4 +161,12 @@ export class SubscriptionComponent implements OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
editSubscription() {
|
||||
this.dialog.open(EditSubscriptionDialogComponent, {
|
||||
data: {
|
||||
sub: this.subscription
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -80,6 +80,7 @@ export class SubscriptionsComponent implements OnInit {
|
||||
} else {
|
||||
this.channel_subscriptions.push(result);
|
||||
}
|
||||
this.postsService.reloadSubscriptions();
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -96,6 +97,7 @@ export class SubscriptionsComponent implements OnInit {
|
||||
if (success) {
|
||||
this.openSnackBar(`${sub.name} successfully deleted!`)
|
||||
this.getSubscriptions();
|
||||
this.postsService.reloadSubscriptions();
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -4,15 +4,10 @@
|
||||
"url": "http://localhost",
|
||||
"port": "17442"
|
||||
},
|
||||
"Encryption": {
|
||||
"use-encryption": false,
|
||||
"cert-file-path": "/etc/letsencrypt/live/example.com/fullchain.pem",
|
||||
"key-file-path": "/etc/letsencrypt/live/example.com/privkey.pem"
|
||||
},
|
||||
"Downloader": {
|
||||
"path-audio": "audio/",
|
||||
"path-video": "video/",
|
||||
"use_youtubedl_archive": false,
|
||||
"use_youtubedl_archive": true,
|
||||
"custom_args": "",
|
||||
"safe_download_override": false
|
||||
},
|
||||
@@ -50,6 +45,7 @@
|
||||
"custom_downloading_agent": "",
|
||||
"multi_user_mode": true,
|
||||
"allow_advanced_download": true,
|
||||
"jwt_expiration": 86400,
|
||||
"logger_level": "debug",
|
||||
"use_cookies": true
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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'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'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'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're good to go! Just navigate to the YouTube video you'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 'audio only' 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>
|
||||
|
||||
@@ -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:"
|
||||
}
|
||||
@@ -3,6 +3,8 @@ const THEMES_CONFIG = {
|
||||
'key': 'default',
|
||||
'background_color': 'ghostwhite',
|
||||
'alternate_color': 'gray',
|
||||
'ghost_primary': '#f9f9f9',
|
||||
'ghost_secondary': '#ecebeb',
|
||||
'css_label': 'default-theme',
|
||||
'social_theme': 'material-light'
|
||||
},
|
||||
@@ -10,12 +12,16 @@ const THEMES_CONFIG = {
|
||||
'key': 'dark',
|
||||
'background_color': '#141414',
|
||||
'alternate_color': '#695959',
|
||||
'ghost_primary': '#444444',
|
||||
'ghost_secondary': '#141414',
|
||||
'css_label': 'dark-theme',
|
||||
'social_theme': 'material-dark'
|
||||
},
|
||||
'light': {
|
||||
'key': 'light',
|
||||
'background_color': 'white',
|
||||
'ghost_primary': '#f9f9f9',
|
||||
'ghost_secondary': '#ecebeb',
|
||||
'css_label': 'light-theme',
|
||||
'social_theme': 'material-light'
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user