mirror of
https://github.com/Tzahi12345/YoutubeDL-Material.git
synced 2026-03-07 20:10:03 +03:00
Compare commits
4 Commits
experiment
...
downloadin
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cc5fd7e997 | ||
|
|
b7b77995b9 | ||
|
|
d7eb3f35ac | ||
|
|
0ea826487d |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -53,7 +53,6 @@ backend/public/assets/default.json
|
||||
backend/subscriptions/channels/*
|
||||
backend/subscriptions/playlists/*
|
||||
backend/subscriptions/archives/*
|
||||
backend/*.exe
|
||||
src/assets/default.json
|
||||
backend/appdata/db.json
|
||||
backend/appdata/archives/archive_audio.txt
|
||||
@@ -64,4 +63,3 @@ backend/appdata/logs/combined.log
|
||||
backend/appdata/logs/error.log
|
||||
backend/appdata/users.json
|
||||
backend/users/*
|
||||
backend/appdata/cookies.txt
|
||||
|
||||
62
README.md
62
README.md
@@ -53,13 +53,41 @@ 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.
|
||||
|
||||
To deploy, simply clone the repository, and go into the `youtubedl-material` directory. Type `npm install` and all the dependencies will install. Then type `cd backend` and again type `npm install` to install the dependencies for the backend.
|
||||
|
||||
Once you do that, you're almost up and running. All you need to do is edit the configuration in `youtubedl-material/appdata`, go back into the `youtubedl-material` directory, and type `ng build --prod`. This will build the app, and put the output files in the `youtubedl-material/backend/public` folder.
|
||||
Once you do that, you're almost up and running. All you need to do is edit the configuration in `youtubedl-material/appdata`, go back into the `youtubedl-material` directory, and type `ng build --prod`. This will build the app, and put the output files in the `youtubedl-material/dist` folder. Drag those files into the `public` directory in the `backend` folder.
|
||||
|
||||
The frontend is now complete. The backend is much easier. Just go into the `backend` folder, and type `npm start`.
|
||||
|
||||
@@ -69,24 +97,14 @@ Alternatively, you can port forward the port specified in the config (defaults t
|
||||
|
||||
## Docker
|
||||
|
||||
### Setup
|
||||
|
||||
If you are looking to setup YoutubeDL-Material with Docker, this section is for you. And you're in luck! Docker setup is quite simple.
|
||||
|
||||
1. Run `curl -L https://github.com/Tzahi12345/YoutubeDL-Material/releases/latest/download/docker-compose.yml -o docker-compose.yml` to download the latest Docker Compose, or go to the [releases](https://github.com/Tzahi12345/YoutubeDL-Material/releases/) page to grab the version you'd like.
|
||||
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
|
||||
```
|
||||
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!
|
||||
|
||||
## API
|
||||
|
||||
@@ -98,20 +116,12 @@ Once you have enabled the API and have the key, you can start sending requests b
|
||||
|
||||
## Contributing
|
||||
|
||||
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.
|
||||
Feel free to submit a pull request! I have no guidelines as of yet, so no need to worry about that.
|
||||
|
||||
## 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
|
||||
|
||||
@@ -1,27 +1,18 @@
|
||||
FROM alpine:3.12
|
||||
FROM alpine:3.11
|
||||
|
||||
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/ \
|
||||
RUN \
|
||||
apk add --no-cache npm python ffmpeg && \
|
||||
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 package.json /app/
|
||||
|
||||
COPY --chown=$UID:$GID [ "./", "/app/" ]
|
||||
RUN npm install
|
||||
|
||||
COPY ./ /app/
|
||||
|
||||
EXPOSE 17442
|
||||
|
||||
ENTRYPOINT [ "/app/entrypoint.sh" ]
|
||||
CMD [ "node", "app.js" ]
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
FROM arm32v7/alpine:3.12
|
||||
|
||||
COPY qemu-arm-static /usr/bin
|
||||
|
||||
ENV UID=1000 \
|
||||
GID=1000 \
|
||||
USER=youtube
|
||||
|
||||
RUN addgroup -S $USER -g $GID && adduser -D -S $USER -G $USER -u $UID
|
||||
|
||||
RUN apk add --no-cache \
|
||||
ffmpeg \
|
||||
npm \
|
||||
python2 \
|
||||
su-exec \
|
||||
&& apk add --no-cache --repository http://dl-cdn.alpinelinux.org/alpine/edge/testing/ \
|
||||
atomicparsley
|
||||
|
||||
WORKDIR /app
|
||||
COPY --chown=$UID:$GID [ "package.json", "package-lock.json", "/app/" ]
|
||||
|
||||
RUN npm install && chown -R $UID:$GID ./
|
||||
|
||||
COPY --chown=$UID:$GID [ "./", "/app/" ]
|
||||
|
||||
EXPOSE 17442
|
||||
|
||||
ENTRYPOINT [ "/app/entrypoint.sh" ]
|
||||
CMD [ "node", "app.js" ]
|
||||
756
backend/app.js
756
backend/app.js
File diff suppressed because it is too large
Load Diff
@@ -13,8 +13,7 @@
|
||||
"path-audio": "audio/",
|
||||
"path-video": "video/",
|
||||
"use_youtubedl_archive": false,
|
||||
"custom_args": "",
|
||||
"safe_download_override": false
|
||||
"custom_args": ""
|
||||
},
|
||||
"Extra": {
|
||||
"title_top": "YoutubeDL-Material",
|
||||
@@ -22,6 +21,7 @@
|
||||
"allow_quality_select": true,
|
||||
"download_only_mode": false,
|
||||
"allow_multi_download_mode": true,
|
||||
"settings_pin_required": false,
|
||||
"enable_downloads_manager": true
|
||||
},
|
||||
"API": {
|
||||
@@ -37,7 +37,8 @@
|
||||
"Subscriptions": {
|
||||
"allow_subscriptions": true,
|
||||
"subscriptions_base_path": "subscriptions/",
|
||||
"subscriptions_check_interval": "300"
|
||||
"subscriptions_check_interval": "300",
|
||||
"subscriptions_use_youtubedl_archive": true
|
||||
},
|
||||
"Users": {
|
||||
"base_path": "users/",
|
||||
@@ -49,7 +50,6 @@
|
||||
"multi_user_mode": false,
|
||||
"allow_advanced_download": false,
|
||||
"use_cookies": false,
|
||||
"jwt_expiration": 86400,
|
||||
"logger_level": "info"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,8 +13,7 @@
|
||||
"path-audio": "audio/",
|
||||
"path-video": "video/",
|
||||
"use_youtubedl_archive": false,
|
||||
"custom_args": "",
|
||||
"safe_download_override": false
|
||||
"custom_args": ""
|
||||
},
|
||||
"Extra": {
|
||||
"title_top": "YoutubeDL-Material",
|
||||
@@ -22,6 +21,7 @@
|
||||
"allow_quality_select": true,
|
||||
"download_only_mode": false,
|
||||
"allow_multi_download_mode": true,
|
||||
"settings_pin_required": false,
|
||||
"enable_downloads_manager": true
|
||||
},
|
||||
"API": {
|
||||
@@ -37,7 +37,8 @@
|
||||
"Subscriptions": {
|
||||
"allow_subscriptions": true,
|
||||
"subscriptions_base_path": "subscriptions/",
|
||||
"subscriptions_check_interval": "300"
|
||||
"subscriptions_check_interval": "300",
|
||||
"subscriptions_use_youtubedl_archive": true
|
||||
},
|
||||
"Users": {
|
||||
"base_path": "users/",
|
||||
@@ -49,7 +50,6 @@
|
||||
"multi_user_mode": false,
|
||||
"allow_advanced_download": false,
|
||||
"use_cookies": false,
|
||||
"jwt_expiration": 86400,
|
||||
"logger_level": "info"
|
||||
}
|
||||
}
|
||||
|
||||
BIN
backend/aria2c.exe
Normal file
BIN
backend/aria2c.exe
Normal file
Binary file not shown.
@@ -5,7 +5,7 @@ var subscriptions_api = require('../subscriptions')
|
||||
const fs = require('fs-extra');
|
||||
var jwt = require('jsonwebtoken');
|
||||
const { uuid } = require('uuidv4');
|
||||
var bcrypt = require('bcryptjs');
|
||||
var bcrypt = require('bcrypt');
|
||||
|
||||
|
||||
var LocalStrategy = require('passport-local').Strategy;
|
||||
@@ -29,7 +29,7 @@ exports.initialize = function(input_users_db, input_logger) {
|
||||
************************/
|
||||
saltRounds = 10;
|
||||
|
||||
JWT_EXPIRATION = config_api.getConfigItem('ytdl_jwt_expiration');
|
||||
JWT_EXPIRATION = (60 * 60); // one hour
|
||||
|
||||
SERVER_SECRET = null;
|
||||
if (users_db.get('jwt_secret').value()) {
|
||||
@@ -69,7 +69,7 @@ exports.passport = require('passport');
|
||||
exports.passport.serializeUser(function(user, done) {
|
||||
done(null, user);
|
||||
});
|
||||
|
||||
|
||||
exports.passport.deserializeUser(function(user, done) {
|
||||
done(null, user);
|
||||
});
|
||||
@@ -87,7 +87,7 @@ exports.registerUser = function(req, res) {
|
||||
logger.error(`Registration failed for user ${userid}. Registration is disabled.`);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
bcrypt.hash(plaintextPassword, saltRounds)
|
||||
.then(function(hash) {
|
||||
let new_user = {
|
||||
@@ -127,7 +127,7 @@ exports.registerUser = function(req, res) {
|
||||
}
|
||||
})
|
||||
.then(function(result) {
|
||||
|
||||
|
||||
})
|
||||
.catch(function(err) {
|
||||
logger.error(err);
|
||||
@@ -146,7 +146,7 @@ exports.registerUser = function(req, res) {
|
||||
/*************************************************
|
||||
* This gets called when passport.authenticate()
|
||||
* gets called.
|
||||
*
|
||||
*
|
||||
* This checks that the credentials are valid.
|
||||
* If so, passes the user info to the next middleware.
|
||||
************************************************/
|
||||
@@ -181,12 +181,12 @@ exports.passport.use(new LocalStrategy({
|
||||
* 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
|
||||
* 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},
|
||||
exports.passport.authenticate('basic',{session:false},
|
||||
function(err, user, info) {
|
||||
if(!user){
|
||||
res.set('WWW-Authenticate', 'x'+info); // change to xBasic
|
||||
@@ -212,7 +212,7 @@ exports.generateJWT = function(req, res, next) {
|
||||
, user: req.user.uid
|
||||
};
|
||||
req.token = jwt.sign(payload, SERVER_SECRET);
|
||||
next();
|
||||
next();
|
||||
}
|
||||
|
||||
exports.returnAuthResponse = function(req, res) {
|
||||
@@ -221,11 +221,11 @@ exports.returnAuthResponse = function(req, res) {
|
||||
token: req.token,
|
||||
permissions: exports.userPermissions(req.user.uid),
|
||||
available_permissions: consts['AVAILABLE_PERMISSIONS']
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/***************************************
|
||||
* Authorization: middleware that checks the
|
||||
* Authorization: middleware that checks the
|
||||
* JWT token for validity before allowing
|
||||
* the user to access anything.
|
||||
*
|
||||
@@ -307,27 +307,21 @@ exports.getUserVideos = function(user_uid, type) {
|
||||
return user['files'][type];
|
||||
}
|
||||
|
||||
exports.getUserVideo = function(user_uid, file_uid, type, requireSharing = false, reqBody = null) {
|
||||
// check if it's a subscription first
|
||||
let file = null;
|
||||
if (reqBody && reqBody.subscriptionName) {
|
||||
file = users_db.get('users').find({uid: user_uid}).get('subscriptions').find({name: reqBody.subscriptionName}).get('videos').find({id: reqBody.fileNames}).value();
|
||||
} else {
|
||||
if (!type) {
|
||||
file = users_db.get('users').find({uid: user_uid}).get(`files.audio`).find({uid: file_uid}).value();
|
||||
if (!file) {
|
||||
file = users_db.get('users').find({uid: user_uid}).get(`files.video`).find({uid: file_uid}).value();
|
||||
if (file) type = 'video';
|
||||
} else {
|
||||
type = 'audio';
|
||||
}
|
||||
exports.getUserVideo = function(user_uid, file_uid, type, requireSharing = false) {
|
||||
if (!type) {
|
||||
file = users_db.get('users').find({uid: user_uid}).get(`files.audio`).find({uid: file_uid}).value();
|
||||
if (!file) {
|
||||
file = users_db.get('users').find({uid: user_uid}).get(`files.video`).find({uid: file_uid}).value();
|
||||
if (file) type = 'video';
|
||||
} else {
|
||||
type = 'audio';
|
||||
}
|
||||
|
||||
if (!file && type) file = users_db.get('users').find({uid: user_uid}).get(`files.${type}`).find({uid: file_uid}).value();
|
||||
}
|
||||
|
||||
if (!file && type) file = users_db.get('users').find({uid: user_uid}).get(`files.${type}`).find({uid: file_uid}).value();
|
||||
|
||||
// prevent unauthorized users from accessing the file info
|
||||
if (file && requireSharing && !file['sharingEnabled']) file = null;
|
||||
if (requireSharing && !file['sharingEnabled']) file = null;
|
||||
|
||||
return file;
|
||||
}
|
||||
@@ -337,7 +331,7 @@ exports.addPlaylist = function(user_uid, new_playlist, type) {
|
||||
return true;
|
||||
}
|
||||
|
||||
exports.updatePlaylistFiles = function(user_uid, playlistID, new_filenames, type) {
|
||||
exports.updatePlaylist = function(user_uid, playlistID, new_filenames, type) {
|
||||
users_db.get('users').find({uid: user_uid}).get(`playlists.${type}`).find({id: playlistID}).assign({fileNames: new_filenames});
|
||||
return true;
|
||||
}
|
||||
@@ -398,7 +392,7 @@ exports.deleteUserFile = function(user_uid, file_uid, type, blacklistMode = fals
|
||||
} catch(e) {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const full_path = path.join(usersFileFolder, user_uid, type, file_obj.id + ext);
|
||||
users_db.get('users').find({uid: user_uid}).get(`files.${type}`)
|
||||
@@ -436,8 +430,8 @@ exports.deleteUserFile = function(user_uid, file_uid, type, blacklistMode = fals
|
||||
fs.appendFileSync(blacklistPath, line);
|
||||
}
|
||||
} else {
|
||||
logger.info(`Could not find archive file for ${type} files. Creating...`);
|
||||
fs.ensureFileSync(archive_path);
|
||||
logger.info('Could not find archive file for audio files. Creating...');
|
||||
fs.closeSync(fs.openSync(archive_path, 'w'));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -460,7 +454,7 @@ exports.changeSharingMode = function(user_uid, file_uid, type, is_playlist, enab
|
||||
file_db_obj.assign({sharingEnabled: enabled}).write();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
@@ -476,7 +470,7 @@ exports.userHasPermission = function(user_uid, permission) {
|
||||
|
||||
const user_has_explicit_permission = user_obj['permissions'].includes(permission);
|
||||
const permission_in_overrides = user_obj['permission_overrides'].includes(permission);
|
||||
|
||||
|
||||
// check if user has a negative/positive override
|
||||
if (user_has_explicit_permission && permission_in_overrides) {
|
||||
// positive override
|
||||
@@ -511,7 +505,7 @@ exports.userPermissions = function(user_uid) {
|
||||
|
||||
const user_has_explicit_permission = user_obj['permissions'].includes(permission);
|
||||
const permission_in_overrides = user_obj['permission_overrides'].includes(permission);
|
||||
|
||||
|
||||
// check if user has a negative/positive override
|
||||
if (user_has_explicit_permission && permission_in_overrides) {
|
||||
// positive override
|
||||
@@ -543,4 +537,4 @@ function getToken(queryParams) {
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
};
|
||||
@@ -119,8 +119,8 @@ function setConfigItem(key, value) {
|
||||
let parent_parent_single_key = parent_path_arr[parent_path_arr.length-1];
|
||||
parent_parent_object[parent_parent_single_key] = {};
|
||||
parent_object = Object.byString(config_json, parent_path);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (value === 'false' || value === 'true') {
|
||||
parent_object[element_name] = (value === 'true');
|
||||
} else {
|
||||
@@ -146,7 +146,7 @@ function setConfigItems(items) {
|
||||
let item_path = CONFIG_ITEMS[key]['path'];
|
||||
let item_parent_path = getParentPath(item_path);
|
||||
let item_element_name = getElementNameInConfig(item_path);
|
||||
|
||||
|
||||
let item_parent_object = Object.byString(config_json, item_parent_path);
|
||||
item_parent_object[item_element_name] = value;
|
||||
}
|
||||
@@ -155,13 +155,6 @@ function setConfigItems(items) {
|
||||
return success;
|
||||
}
|
||||
|
||||
function globalArgsRequiresSafeDownload() {
|
||||
const globalArgs = getConfigItem('ytdl_custom_args').split(',,');
|
||||
const argsThatRequireSafeDownload = ['--write-sub', '--write-srt'];
|
||||
const failedArgs = globalArgs.filter(arg => argsThatRequireSafeDownload.includes(arg));
|
||||
return failedArgs && failedArgs.length > 0;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getConfigItem: getConfigItem,
|
||||
setConfigItem: setConfigItem,
|
||||
@@ -171,8 +164,7 @@ module.exports = {
|
||||
configExistsCheck: configExistsCheck,
|
||||
CONFIG_ITEMS: CONFIG_ITEMS,
|
||||
initialize: initialize,
|
||||
descriptors: {},
|
||||
globalArgsRequiresSafeDownload: globalArgsRequiresSafeDownload
|
||||
descriptors: {}
|
||||
}
|
||||
|
||||
DEFAULT_CONFIG = {
|
||||
@@ -190,8 +182,7 @@ DEFAULT_CONFIG = {
|
||||
"path-audio": "audio/",
|
||||
"path-video": "video/",
|
||||
"use_youtubedl_archive": false,
|
||||
"custom_args": "",
|
||||
"safe_download_override": false
|
||||
"custom_args": ""
|
||||
},
|
||||
"Extra": {
|
||||
"title_top": "YoutubeDL-Material",
|
||||
@@ -199,6 +190,7 @@ DEFAULT_CONFIG = {
|
||||
"allow_quality_select": true,
|
||||
"download_only_mode": false,
|
||||
"allow_multi_download_mode": true,
|
||||
"settings_pin_required": false,
|
||||
"enable_downloads_manager": true
|
||||
},
|
||||
"API": {
|
||||
@@ -214,7 +206,8 @@ DEFAULT_CONFIG = {
|
||||
"Subscriptions": {
|
||||
"allow_subscriptions": true,
|
||||
"subscriptions_base_path": "subscriptions/",
|
||||
"subscriptions_check_interval": "300"
|
||||
"subscriptions_check_interval": "300",
|
||||
"subscriptions_use_youtubedl_archive": true
|
||||
},
|
||||
"Users": {
|
||||
"base_path": "users/",
|
||||
@@ -226,7 +219,6 @@ DEFAULT_CONFIG = {
|
||||
"multi_user_mode": false,
|
||||
"allow_advanced_download": false,
|
||||
"use_cookies": false,
|
||||
"jwt_expiration": 86400,
|
||||
"logger_level": "info"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,10 +40,6 @@ let CONFIG_ITEMS = {
|
||||
'key': 'ytdl_custom_args',
|
||||
'path': 'YoutubeDLMaterial.Downloader.custom_args'
|
||||
},
|
||||
'ytdl_safe_download_override': {
|
||||
'key': 'ytdl_safe_download_override',
|
||||
'path': 'YoutubeDLMaterial.Downloader.safe_download_override'
|
||||
},
|
||||
|
||||
// Extra
|
||||
'ytdl_title_top': {
|
||||
@@ -66,6 +62,10 @@ 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'
|
||||
@@ -116,6 +116,10 @@ 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': {
|
||||
@@ -148,10 +152,6 @@ 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'
|
||||
@@ -170,5 +170,5 @@ AVAILABLE_PERMISSIONS = [
|
||||
module.exports = {
|
||||
CONFIG_ITEMS: CONFIG_ITEMS,
|
||||
AVAILABLE_PERMISSIONS: AVAILABLE_PERMISSIONS,
|
||||
CURRENT_VERSION: 'v4.1'
|
||||
}
|
||||
CURRENT_VERSION: 'v4.0'
|
||||
}
|
||||
123
backend/db.js
123
backend/db.js
@@ -1,123 +0,0 @@
|
||||
var fs = require('fs-extra')
|
||||
var path = require('path')
|
||||
var utils = require('./utils')
|
||||
const { uuid } = require('uuidv4');
|
||||
const config_api = require('./config');
|
||||
|
||||
var logger = null;
|
||||
var db = null;
|
||||
var users_db = null;
|
||||
function setDB(input_db, input_users_db) { db = input_db; users_db = input_users_db }
|
||||
function setLogger(input_logger) { logger = input_logger; }
|
||||
|
||||
function initialize(input_db, input_users_db, input_logger) {
|
||||
setDB(input_db, input_users_db);
|
||||
setLogger(input_logger);
|
||||
}
|
||||
|
||||
function registerFileDB(file_path, type, multiUserMode = null, sub = 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) {
|
||||
logger.error(`Could not find associated JSON file for ${type} file ${file_id}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
utils.fixVideoMetadataPerms(file_id, type, multiUserMode && multiUserMode.file_path);
|
||||
|
||||
// add additional info
|
||||
file_object['uid'] = uuid();
|
||||
file_object['registered'] = Date.now();
|
||||
path_object = path.parse(file_object['path']);
|
||||
file_object['path'] = path.format(path_object);
|
||||
|
||||
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});
|
||||
}
|
||||
if (sub_db.get('videos').find({id: file_object.id}).value()) {
|
||||
logger.verbose(`Subscription video ${file_object.id} already exists, skipping DB registration.`);
|
||||
return null;
|
||||
}
|
||||
sub_db.get('videos').push(file_object).write();
|
||||
}
|
||||
|
||||
return file_object['uid'];
|
||||
}
|
||||
|
||||
function generateFileObject(id, type, customPath = null, sub = null) {
|
||||
if (!customPath && sub) {
|
||||
customPath = getAppendedBasePathSub(sub, config_api.getConfigItem('ytdl_subscriptions_base_path'));
|
||||
}
|
||||
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 = utils.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;
|
||||
}
|
||||
|
||||
function updatePlaylist(playlist, user_uid) {
|
||||
let playlistID = playlist.id;
|
||||
let type = playlist.type;
|
||||
let db_loc = null;
|
||||
if (user_uid) {
|
||||
db_loc = users_db.get('users').find({uid: user_uid}).get(`playlists.${type}`).find({id: playlistID});
|
||||
} else {
|
||||
db_loc = db.get(`playlists.${type}`).find({id: playlistID});
|
||||
}
|
||||
db_loc.assign(playlist).write();
|
||||
return true;
|
||||
}
|
||||
|
||||
function getAppendedBasePathSub(sub, base_path) {
|
||||
return path.join(base_path, (sub.isPlaylist ? 'playlists/' : 'channels/'), sub.name);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
initialize: initialize,
|
||||
registerFileDB: registerFileDB,
|
||||
updatePlaylist: updatePlaylist
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
CMD="node app.js"
|
||||
|
||||
# if the first arg starts with "-" pass it to program
|
||||
if [ "${1#-}" != "$1" ]; then
|
||||
set -- "$CMD" "$@"
|
||||
fi
|
||||
|
||||
# chown current working directory to current user
|
||||
if [ "$*" = "$CMD" ] && [ "$(id -u)" = "0" ]; then
|
||||
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
|
||||
|
||||
exec "$@"
|
||||
BIN
backend/ffmpeg.exe
Normal file
BIN
backend/ffmpeg.exe
Normal file
Binary file not shown.
BIN
backend/ffplay.exe
Normal file
BIN
backend/ffplay.exe
Normal file
Binary file not shown.
BIN
backend/ffprobe.exe
Normal file
BIN
backend/ffprobe.exe
Normal file
Binary file not shown.
@@ -1,3 +0,0 @@
|
||||
#!/bin/bash
|
||||
# downloads a local copy of qemu on docker-hub build machines
|
||||
curl -L https://github.com/balena-io/qemu/releases/download/v3.0.0%2Bresin/qemu-3.0.0+resin-arm.tar.gz | tar zxvf - -C . && mv qemu-3.0.0+resin-arm/qemu-arm-static .
|
||||
@@ -1,4 +0,0 @@
|
||||
#!/bin/bash
|
||||
# Register qemu-*-static for all supported processors except the
|
||||
# current one, but also remove all registered binfmt_misc before
|
||||
docker run --rm --privileged multiarch/qemu-user-static:register --reset
|
||||
375
backend/package-lock.json
generated
375
backend/package-lock.json
generated
@@ -50,11 +50,6 @@
|
||||
"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",
|
||||
@@ -69,6 +64,11 @@
|
||||
"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,6 +126,31 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"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",
|
||||
@@ -179,6 +204,15 @@
|
||||
"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",
|
||||
@@ -187,11 +221,6 @@
|
||||
"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",
|
||||
@@ -405,6 +434,11 @@
|
||||
"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",
|
||||
@@ -415,6 +449,11 @@
|
||||
"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",
|
||||
@@ -575,10 +614,10 @@
|
||||
"xdg-basedir": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"connected-domain": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/connected-domain/-/connected-domain-1.0.0.tgz",
|
||||
"integrity": "sha1-v+dyOMdL5FOnnwy2BY3utPI1jpM="
|
||||
"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="
|
||||
},
|
||||
"content-disposition": {
|
||||
"version": "0.5.3",
|
||||
@@ -679,6 +718,11 @@
|
||||
"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",
|
||||
@@ -689,6 +733,11 @@
|
||||
"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",
|
||||
@@ -1002,6 +1051,14 @@
|
||||
"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",
|
||||
@@ -1024,6 +1081,54 @@
|
||||
"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",
|
||||
@@ -1123,6 +1228,11 @@
|
||||
"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",
|
||||
@@ -1191,6 +1301,14 @@
|
||||
"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",
|
||||
@@ -1478,9 +1596,9 @@
|
||||
"integrity": "sha1-hMinKrWcRyUyFIDJdeZQg0LnCTc="
|
||||
},
|
||||
"lodash": {
|
||||
"version": "4.17.19",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz",
|
||||
"integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ=="
|
||||
"version": "4.17.15",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
|
||||
"integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A=="
|
||||
},
|
||||
"lodash.defaults": {
|
||||
"version": "4.2.0",
|
||||
@@ -1664,6 +1782,30 @@
|
||||
"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",
|
||||
@@ -1717,26 +1859,46 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"mz": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",
|
||||
"integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==",
|
||||
"requires": {
|
||||
"any-promise": "^1.0.0",
|
||||
"object-assign": "^4.0.1",
|
||||
"thenify-all": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"nanoid": {
|
||||
"version": "2.1.11",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-2.1.11.tgz",
|
||||
"integrity": "sha512-s/snB+WGm6uwi0WjsZdaVcuf3KJXlfGl2LcxgwkEwJF0D/BWzVWAZW/XY4bFaiR7s0Jk3FPvlnepg1H1b1UwlA=="
|
||||
},
|
||||
"needle": {
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmjs.org/needle/-/needle-2.4.1.tgz",
|
||||
"integrity": "sha512-x/gi6ijr4B7fwl6WYL9FwlCvRQKGlUNvnceho8wxkwXqN8jvVmmmATTmZPRRG7b/yC1eode26C2HO9jl78Du9g==",
|
||||
"requires": {
|
||||
"debug": "^3.2.6",
|
||||
"iconv-lite": "^0.4.4",
|
||||
"sax": "^1.2.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"debug": {
|
||||
"version": "3.2.6",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
|
||||
"integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
|
||||
"requires": {
|
||||
"ms": "^2.1.1"
|
||||
}
|
||||
},
|
||||
"ms": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"negotiator": {
|
||||
"version": "0.6.2",
|
||||
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz",
|
||||
"integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw=="
|
||||
},
|
||||
"node-addon-api": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-2.0.0.tgz",
|
||||
"integrity": "sha512-ASCL5U13as7HhOExbT6OlWJJUV/lLzL2voOSP1UVehpRD8FbSrSDjfScK/KwAvVTI5AS6r4VwbOMlIqtvRidnA=="
|
||||
},
|
||||
"node-fetch": {
|
||||
"version": "2.6.0",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.0.tgz",
|
||||
@@ -1750,6 +1912,34 @@
|
||||
"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",
|
||||
@@ -1795,6 +1985,29 @@
|
||||
"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",
|
||||
@@ -1803,6 +2016,22 @@
|
||||
"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",
|
||||
@@ -1847,6 +2076,25 @@
|
||||
"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",
|
||||
@@ -1995,14 +2243,6 @@
|
||||
"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",
|
||||
@@ -2071,14 +2311,6 @@
|
||||
"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",
|
||||
@@ -2171,6 +2403,11 @@
|
||||
"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",
|
||||
@@ -2222,6 +2459,11 @@
|
||||
"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",
|
||||
@@ -2361,12 +2603,25 @@
|
||||
"has-flag": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"table-parser": {
|
||||
"version": "0.1.3",
|
||||
"resolved": "https://registry.npmjs.org/table-parser/-/table-parser-0.1.3.tgz",
|
||||
"integrity": "sha1-BEHPzhallIFoTCfRtaZ/8VpDx7A=",
|
||||
"tar": {
|
||||
"version": "4.4.13",
|
||||
"resolved": "https://registry.npmjs.org/tar/-/tar-4.4.13.tgz",
|
||||
"integrity": "sha512-w2VwSrBoHa5BsSyH+KxEqeQBAllHhccyMFVHtGtdMpF4W7IRWfZjFiQceJPChOeTsSDVUpER2T8FA93pr0L+QA==",
|
||||
"requires": {
|
||||
"connected-domain": "^1.0.0"
|
||||
"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=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"tar-stream": {
|
||||
@@ -2469,22 +2724,6 @@
|
||||
"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",
|
||||
@@ -2700,6 +2939,14 @@
|
||||
"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,7 +30,7 @@
|
||||
"dependencies": {
|
||||
"archiver": "^3.1.1",
|
||||
"async": "^3.1.0",
|
||||
"bcryptjs": "^2.4.0",
|
||||
"bcrypt": "^4.0.1",
|
||||
"compression": "^1.7.4",
|
||||
"config": "^3.2.3",
|
||||
"exe": "^1.0.2",
|
||||
@@ -50,8 +50,6 @@
|
||||
"passport-jwt": "^4.0.0",
|
||||
"passport-local": "^1.0.0",
|
||||
"progress": "^2.0.3",
|
||||
"ps-node": "^0.1.6",
|
||||
"read-last-lines": "^1.7.2",
|
||||
"shortid": "^2.2.15",
|
||||
"unzipper": "^0.10.10",
|
||||
"uuidv4": "^6.0.6",
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
backend/public/1-es5.cc1ef452b2945b55327a.js
Normal file
1
backend/public/1-es5.cc1ef452b2945b55327a.js
Normal file
File diff suppressed because one or more lines are too long
@@ -190,32 +190,6 @@ ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
|
||||
is-retina
|
||||
MIT
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2014 Kyle Mathews
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
|
||||
|
||||
ng-lazyload-image
|
||||
MIT
|
||||
The MIT License (MIT)
|
||||
@@ -241,30 +215,6 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
|
||||
ngx-avatar
|
||||
MIT
|
||||
The MIT License
|
||||
|
||||
Copyright (c) 2020 Haithem Mosbahi, ngx-avatar
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
ngx-file-drop
|
||||
MIT
|
||||
|
||||
@@ -708,9 +658,6 @@ Apache-2.0
|
||||
|
||||
|
||||
|
||||
ts-md5
|
||||
MIT
|
||||
|
||||
tslib
|
||||
Apache-2.0
|
||||
Apache License
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -196,20 +196,5 @@
|
||||
"52c1447c1ec9570a2a3025c7e566557b8d19ed92": " Rol ",
|
||||
"59a8c38db3091a63ac1cb9590188dc3a972acfb3": " Acciones ",
|
||||
"4d92a0395dd66778a931460118626c5794a3fc7a": "Agregar Usuarios",
|
||||
"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:"
|
||||
"b0d7dd8a1b0349622d6e0c6e643e24a9ea0efa1d": "Editar Rol"
|
||||
}
|
||||
@@ -14,5 +14,5 @@
|
||||
<link rel="stylesheet" href="styles.5112d6db78cf21541598.css"></head>
|
||||
<body>
|
||||
<app-root></app-root>
|
||||
<script src="runtime-es2015.49084170e55eb5da181d.js" type="module"></script><script src="runtime-es5.49084170e55eb5da181d.js" nomodule defer></script><script src="polyfills-es5.7f923c8f5afda210edd3.js" nomodule defer></script><script src="polyfills-es2015.5b408f108bcea938a7e2.js" type="module"></script><script src="main-es2015.63a61681b752f95b7352.js" type="module"></script><script src="main-es5.63a61681b752f95b7352.js" nomodule defer></script></body>
|
||||
<script src="runtime-es2015.06b6262a0d981fd4885e.js" type="module"></script><script src="runtime-es5.06b6262a0d981fd4885e.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>
|
||||
|
||||
1
backend/public/main-es2015.0cbc545a4a3bee376826.js
Normal file
1
backend/public/main-es2015.0cbc545a4a3bee376826.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
backend/public/main-es5.0cbc545a4a3bee376826.js
Normal file
1
backend/public/main-es5.0cbc545a4a3bee376826.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
||||
!function(e){function r(r){for(var n,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:"058e3b6a3de95b8ddbf8"}[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()}([]);
|
||||
!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:"cc1ef452b2945b55327a"}[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 +1 @@
|
||||
!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:"058e3b6a3de95b8ddbf8"}[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()}([]);
|
||||
!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:"cc1ef452b2945b55327a"}[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()}([]);
|
||||
@@ -6,20 +6,17 @@ var path = require('path');
|
||||
|
||||
var youtubedl = require('youtube-dl');
|
||||
const config_api = require('./config');
|
||||
var utils = require('./utils')
|
||||
|
||||
const debugMode = process.env.YTDL_MODE === 'debug';
|
||||
|
||||
var logger = null;
|
||||
var db = null;
|
||||
var users_db = null;
|
||||
var db_api = null;
|
||||
|
||||
function setDB(input_db, input_users_db, input_db_api) { db = input_db; users_db = input_users_db; db_api = input_db_api }
|
||||
function setDB(input_db, input_users_db) { db = input_db; users_db = input_users_db }
|
||||
function setLogger(input_logger) { logger = input_logger; }
|
||||
|
||||
function initialize(input_db, input_users_db, input_logger, input_db_api) {
|
||||
setDB(input_db, input_users_db, input_db_api);
|
||||
function initialize(input_db, input_users_db, input_logger) {
|
||||
setDB(input_db, input_users_db);
|
||||
setLogger(input_logger);
|
||||
}
|
||||
|
||||
@@ -31,7 +28,6 @@ async function subscribe(sub, user_uid = null) {
|
||||
return new Promise(async resolve => {
|
||||
// sub should just have url and name. here we will get isPlaylist and path
|
||||
sub.isPlaylist = sub.url.includes('playlist');
|
||||
sub.videos = [];
|
||||
|
||||
let url_exists = false;
|
||||
|
||||
@@ -48,28 +44,18 @@ async function subscribe(sub, user_uid = null) {
|
||||
}
|
||||
|
||||
// add sub to db
|
||||
let sub_db = null;
|
||||
if (user_uid) {
|
||||
if (user_uid)
|
||||
users_db.get('users').find({uid: user_uid}).get('subscriptions').push(sub).write();
|
||||
sub_db = users_db.get('users').find({uid: user_uid}).get('subscriptions').find({id: sub.id});
|
||||
} else {
|
||||
else
|
||||
db.get('subscriptions').push(sub).write();
|
||||
sub_db = db.get('subscriptions').find({id: sub.id});
|
||||
}
|
||||
|
||||
let success = await getSubscriptionInfo(sub, user_uid);
|
||||
|
||||
if (success) {
|
||||
sub = sub_db.value();
|
||||
getVideosForSub(sub, user_uid);
|
||||
} else {
|
||||
logger.error('Subscribe: Failed to get subscription info. Subscribe failed.')
|
||||
};
|
||||
|
||||
result_obj.success = success;
|
||||
result_obj.sub = sub;
|
||||
getVideosForSub(sub, user_uid);
|
||||
resolve(result_obj);
|
||||
});
|
||||
|
||||
|
||||
}
|
||||
|
||||
async function getSubscriptionInfo(sub, user_uid = null) {
|
||||
@@ -80,16 +66,8 @@ async function getSubscriptionInfo(sub, user_uid = null) {
|
||||
basePath = config_api.getConfigItem('ytdl_subscriptions_base_path');
|
||||
|
||||
return new Promise(resolve => {
|
||||
// get videos
|
||||
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.');
|
||||
}
|
||||
}
|
||||
// get videos
|
||||
let downloadConfig = ['--dump-json', '--playlist-end', '1']
|
||||
youtubedl.exec(sub.url, downloadConfig, {}, function(err, output) {
|
||||
if (debugMode) {
|
||||
logger.info('Subscribe: got info for subscription ' + sub.id);
|
||||
@@ -123,7 +101,7 @@ async function getSubscriptionInfo(sub, user_uid = null) {
|
||||
}
|
||||
}
|
||||
|
||||
const useArchive = config_api.getConfigItem('ytdl_use_youtubedl_archive');
|
||||
const useArchive = config_api.getConfigItem('ytdl_subscriptions_use_youtubedl_archive');
|
||||
if (useArchive && !sub.archive) {
|
||||
// must create the archive
|
||||
const archive_dir = path.join(__dirname, basePath, 'archives', sub.name);
|
||||
@@ -166,11 +144,6 @@ 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)) {
|
||||
@@ -187,33 +160,25 @@ async function unsubscribe(sub, deleteMode, user_uid = null) {
|
||||
|
||||
}
|
||||
|
||||
async function deleteSubscriptionFile(sub, file, deleteForever, file_uid = null, user_uid = null) {
|
||||
async function deleteSubscriptionFile(sub, file, deleteForever, user_uid = null) {
|
||||
let basePath = null;
|
||||
let sub_db = null;
|
||||
if (user_uid) {
|
||||
if (user_uid)
|
||||
basePath = path.join(config_api.getConfigItem('ytdl_users_base_path'), user_uid, 'subscriptions');
|
||||
sub_db = users_db.get('users').find({uid: user_uid}).get('subscriptions').find({id: sub.id});
|
||||
} else {
|
||||
else
|
||||
basePath = config_api.getConfigItem('ytdl_subscriptions_base_path');
|
||||
sub_db = db.get('subscriptions').find({id: sub.id});
|
||||
}
|
||||
const useArchive = config_api.getConfigItem('ytdl_use_youtubedl_archive');
|
||||
const useArchive = config_api.getConfigItem('ytdl_subscriptions_use_youtubedl_archive');
|
||||
const appendedBasePath = getAppendedBasePath(sub, basePath);
|
||||
const name = file;
|
||||
let retrievedID = null;
|
||||
sub_db.get('videos').remove({uid: file_uid}).write();
|
||||
return new Promise(resolve => {
|
||||
let filePath = appendedBasePath;
|
||||
const ext = (sub.type && sub.type === 'audio') ? '.mp3' : '.mp4'
|
||||
var jsonPath = path.join(__dirname,filePath,name+'.info.json');
|
||||
var videoFilePath = path.join(__dirname,filePath,name+ext);
|
||||
var videoFilePath = path.join(__dirname,filePath,name+'.mp4');
|
||||
var imageFilePath = path.join(__dirname,filePath,name+'.jpg');
|
||||
var altImageFilePath = path.join(__dirname,filePath,name+'.jpg');
|
||||
|
||||
jsonExists = fs.existsSync(jsonPath);
|
||||
videoFileExists = fs.existsSync(videoFilePath);
|
||||
imageFileExists = fs.existsSync(imageFilePath);
|
||||
altImageFileExists = fs.existsSync(altImageFilePath);
|
||||
|
||||
if (jsonExists) {
|
||||
retrievedID = JSON.parse(fs.readFileSync(jsonPath, 'utf8'))['id'];
|
||||
@@ -224,10 +189,6 @@ async function deleteSubscriptionFile(sub, file, deleteForever, file_uid = null,
|
||||
fs.unlinkSync(imageFilePath);
|
||||
}
|
||||
|
||||
if (altImageFileExists) {
|
||||
fs.unlinkSync(altImageFilePath);
|
||||
}
|
||||
|
||||
if (videoFileExists) {
|
||||
fs.unlink(videoFilePath, function(err) {
|
||||
if (fs.existsSync(jsonPath) || fs.existsSync(videoFilePath)) {
|
||||
@@ -248,7 +209,7 @@ async function deleteSubscriptionFile(sub, file, deleteForever, file_uid = null,
|
||||
// TODO: tell user that the file didn't exist
|
||||
resolve(true);
|
||||
}
|
||||
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
@@ -273,48 +234,16 @@ async function getVideosForSub(sub, user_uid = null) {
|
||||
else
|
||||
basePath = config_api.getConfigItem('ytdl_subscriptions_base_path');
|
||||
|
||||
const useArchive = config_api.getConfigItem('ytdl_use_youtubedl_archive');
|
||||
const useArchive = config_api.getConfigItem('ytdl_subscriptions_use_youtubedl_archive');
|
||||
|
||||
let appendedBasePath = null
|
||||
appendedBasePath = getAppendedBasePath(sub, basePath);
|
||||
|
||||
let multiUserMode = null;
|
||||
if (user_uid) {
|
||||
multiUserMode = {
|
||||
user: user_uid,
|
||||
file_path: appendedBasePath
|
||||
}
|
||||
}
|
||||
|
||||
const ext = (sub.type && sub.type === 'audio') ? '.mp3' : '.mp4'
|
||||
|
||||
let fullOutput = appendedBasePath + '/%(title)s' + ext;
|
||||
if (sub.custom_output) {
|
||||
fullOutput = appendedBasePath + '/' + sub.custom_output + ext;
|
||||
}
|
||||
|
||||
let downloadConfig = ['-o', fullOutput, '-ciw', '--write-info-json', '--print-json'];
|
||||
|
||||
let qualityPath = null;
|
||||
if (sub.type && sub.type === 'audio') {
|
||||
qualityPath = ['-f', 'bestaudio']
|
||||
qualityPath.push('-x');
|
||||
qualityPath.push('--audio-format', 'mp3');
|
||||
if (sub.name) {
|
||||
appendedBasePath = getAppendedBasePath(sub, basePath);
|
||||
} else {
|
||||
qualityPath = ['-f', 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/mp4']
|
||||
appendedBasePath = path.join(basePath, (sub.isPlaylist ? 'playlists/%(playlist_title)s' : 'channels/%(uploader)s'));
|
||||
}
|
||||
|
||||
downloadConfig.push(...qualityPath)
|
||||
|
||||
if (sub.custom_args) {
|
||||
customArgsArray = sub.custom_args.split(',,');
|
||||
if (customArgsArray.indexOf('-f') !== -1) {
|
||||
// if custom args has a custom quality, replce the original quality with that of custom args
|
||||
const original_output_index = downloadConfig.indexOf('-f');
|
||||
downloadConfig.splice(original_output_index, 2);
|
||||
}
|
||||
downloadConfig.push(...customArgsArray);
|
||||
}
|
||||
let downloadConfig = ['-o', appendedBasePath + '/%(title)s.mp4', '-f', 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/mp4', '-ciw', '--write-info-json', '--print-json'];
|
||||
|
||||
let archive_dir = null;
|
||||
let archive_path = null;
|
||||
@@ -345,7 +274,7 @@ async function getVideosForSub(sub, user_uid = null) {
|
||||
}
|
||||
}
|
||||
|
||||
// get videos
|
||||
// get videos
|
||||
logger.verbose('Subscription: getting videos for subscription ' + sub.name);
|
||||
youtubedl.exec(sub.url, downloadConfig, {}, function(err, output) {
|
||||
logger.verbose('Subscription: finished check for ' + sub.name);
|
||||
@@ -357,7 +286,7 @@ async function getVideosForSub(sub, user_uid = null) {
|
||||
const outputs = err.stdout.split(/\r\n|\r|\n/);
|
||||
for (let i = 0; i < outputs.length; i++) {
|
||||
const output = JSON.parse(outputs[i]);
|
||||
handleOutputJSON(sub, sub_db, output, i === 0, multiUserMode)
|
||||
handleOutputJSON(sub, sub_db, output, i === 0)
|
||||
if (err.stderr.includes(output['id']) && archive_path) {
|
||||
// we found a video that errored! add it to the archive to prevent future errors
|
||||
fs.appendFileSync(archive_path, output['id']);
|
||||
@@ -386,20 +315,18 @@ async function getVideosForSub(sub, user_uid = null) {
|
||||
}
|
||||
|
||||
const reset_videos = i === 0;
|
||||
handleOutputJSON(sub, sub_db, output_json, multiUserMode, reset_videos);
|
||||
handleOutputJSON(sub, sub_db, output_json, reset_videos);
|
||||
|
||||
// TODO: Potentially store downloaded files in db?
|
||||
|
||||
|
||||
}
|
||||
resolve(true);
|
||||
}
|
||||
});
|
||||
}, err => {
|
||||
logger.error(err);
|
||||
});
|
||||
}
|
||||
|
||||
function handleOutputJSON(sub, sub_db, output_json, multiUserMode = null, reset_videos = false) {
|
||||
function handleOutputJSON(sub, sub_db, output_json, reset_videos = false) {
|
||||
if (sub.streamingOnly) {
|
||||
if (reset_videos) {
|
||||
sub_db.assign({videos: []}).write();
|
||||
@@ -410,8 +337,6 @@ function handleOutputJSON(sub, sub_db, output_json, multiUserMode = null, reset_
|
||||
|
||||
// add to db
|
||||
sub_db.get('videos').push(output_json).write();
|
||||
} else {
|
||||
db_api.registerFileDB(path.basename(output_json['_filename']), sub.type, multiUserMode, sub);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -425,19 +350,10 @@ function getAllSubscriptions(user_uid = null) {
|
||||
function getSubscription(subID, user_uid = null) {
|
||||
if (user_uid)
|
||||
return users_db.get('users').find({uid: user_uid}).get('subscriptions').find({id: subID}).value();
|
||||
else
|
||||
else
|
||||
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();
|
||||
@@ -481,7 +397,7 @@ function removeIDFromArchive(archive_path, id) {
|
||||
for (let index=0; index<dataArray.length; index++) {
|
||||
if (dataArray[index].includes(searchKeyword)) { // check if a line contains the id keyword
|
||||
lastIndex = index; // found a line includes a id keyword
|
||||
break;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -497,7 +413,6 @@ function removeIDFromArchive(archive_path, id) {
|
||||
module.exports = {
|
||||
getSubscription : getSubscription,
|
||||
getAllSubscriptions : getAllSubscriptions,
|
||||
updateSubscription : updateSubscription,
|
||||
subscribe : subscribe,
|
||||
unsubscribe : unsubscribe,
|
||||
deleteSubscriptionFile : deleteSubscriptionFile,
|
||||
|
||||
@@ -1,97 +0,0 @@
|
||||
var fs = require('fs-extra')
|
||||
var path = require('path')
|
||||
const config_api = require('./config');
|
||||
|
||||
const is_windows = process.platform === 'win32';
|
||||
|
||||
function getTrueFileName(unfixed_path, type) {
|
||||
let fixed_path = unfixed_path;
|
||||
|
||||
const new_ext = (type === 'audio' ? 'mp3' : 'mp4');
|
||||
let unfixed_parts = unfixed_path.split('.');
|
||||
const old_ext = unfixed_parts[unfixed_parts.length-1];
|
||||
|
||||
|
||||
if (old_ext !== new_ext) {
|
||||
unfixed_parts[unfixed_parts.length-1] = new_ext;
|
||||
fixed_path = unfixed_parts.join('.');
|
||||
}
|
||||
return fixed_path;
|
||||
}
|
||||
|
||||
function getJSONMp4(name, customPath, openReadPerms = false) {
|
||||
var obj = null; // output
|
||||
if (!customPath) customPath = config_api.getConfigItem('ytdl_video_folder_path');
|
||||
var jsonPath = path.join(customPath, name + ".info.json");
|
||||
var alternateJsonPath = path.join(customPath, name + ".mp4.info.json");
|
||||
if (fs.existsSync(jsonPath))
|
||||
{
|
||||
obj = JSON.parse(fs.readFileSync(jsonPath, 'utf8'));
|
||||
} else if (fs.existsSync(alternateJsonPath)) {
|
||||
obj = JSON.parse(fs.readFileSync(alternateJsonPath, 'utf8'));
|
||||
}
|
||||
else obj = 0;
|
||||
return obj;
|
||||
}
|
||||
|
||||
function getJSONMp3(name, customPath, openReadPerms = false) {
|
||||
var obj = null;
|
||||
if (!customPath) customPath = config_api.getConfigItem('ytdl_audio_folder_path');
|
||||
var jsonPath = path.join(customPath, name + ".info.json");
|
||||
var alternateJsonPath = path.join(customPath, name + ".mp3.info.json");
|
||||
if (fs.existsSync(jsonPath)) {
|
||||
obj = JSON.parse(fs.readFileSync(jsonPath, 'utf8'));
|
||||
}
|
||||
else if (fs.existsSync(alternateJsonPath)) {
|
||||
obj = JSON.parse(fs.readFileSync(alternateJsonPath, 'utf8'));
|
||||
}
|
||||
else
|
||||
obj = 0;
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
function fixVideoMetadataPerms(name, type, customPath = null) {
|
||||
if (is_windows) return;
|
||||
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';
|
||||
|
||||
const files_to_fix = [
|
||||
// JSONs
|
||||
path.join(customPath, name + '.info.json'),
|
||||
path.join(customPath, name + ext + '.info.json'),
|
||||
// Thumbnails
|
||||
path.join(customPath, name + '.webp'),
|
||||
path.join(customPath, name + '.jpg')
|
||||
];
|
||||
|
||||
for (const file of files_to_fix) {
|
||||
if (!fs.existsSync(file)) continue;
|
||||
fs.chmodSync(file, 0o644);
|
||||
}
|
||||
}
|
||||
|
||||
// objects
|
||||
|
||||
function File(id, title, thumbnailURL, isAudio, duration, url, uploader, size, path, upload_date) {
|
||||
this.id = id;
|
||||
this.title = title;
|
||||
this.thumbnailURL = thumbnailURL;
|
||||
this.isAudio = isAudio;
|
||||
this.duration = duration;
|
||||
this.url = url;
|
||||
this.uploader = uploader;
|
||||
this.size = size;
|
||||
this.path = path;
|
||||
this.upload_date = upload_date;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getJSONMp3: getJSONMp3,
|
||||
getJSONMp4: getJSONMp4,
|
||||
getTrueFileName: getTrueFileName,
|
||||
fixVideoMetadataPerms: fixVideoMetadataPerms,
|
||||
File: File
|
||||
}
|
||||
BIN
backend/youtube-dl.exe
Normal file
BIN
backend/youtube-dl.exe
Normal file
Binary file not shown.
0
installer.py
Normal file
0
installer.py
Normal file
49
package-lock.json
generated
49
package-lock.json
generated
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "youtube-dl-material",
|
||||
"version": "4.1.0",
|
||||
"version": "4.0.0",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
@@ -2557,16 +2557,6 @@
|
||||
"integrity": "sha512-Phlt0plgpIIBOGTT/ehfFnbNlfsDEiqmzE2KRXoX1bLIlir4X/MR+zSyBEkL05ffWgnRSf/DXv+WrUAVr93/ow==",
|
||||
"dev": true
|
||||
},
|
||||
"bindings": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
|
||||
"integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"file-uri-to-path": "1.0.0"
|
||||
}
|
||||
},
|
||||
"blob": {
|
||||
"version": "0.0.4",
|
||||
"resolved": "https://registry.npmjs.org/blob/-/blob-0.0.4.tgz",
|
||||
@@ -5240,13 +5230,6 @@
|
||||
"resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.2.tgz",
|
||||
"integrity": "sha512-Wz3c3XQ5xroCxd1G8b7yL0Ehkf0TC9oYC6buPFkNnU9EnaPlifeAFCyCh+iewXTyFRcg0a6j3J7FmJsIhlhBdw=="
|
||||
},
|
||||
"file-uri-to-path": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
|
||||
"integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"filename-regex": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/filename-regex/-/filename-regex-2.0.1.tgz",
|
||||
@@ -6588,11 +6571,6 @@
|
||||
"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",
|
||||
@@ -7177,8 +7155,6 @@
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"bindings": "^1.5.0",
|
||||
"nan": "^2.12.1",
|
||||
"node-pre-gyp": "*"
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -9101,11 +9077,6 @@
|
||||
"integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==",
|
||||
"dev": true
|
||||
},
|
||||
"nan": {
|
||||
"version": "2.14.1",
|
||||
"resolved": "https://registry.npmjs.org/nan/-/nan-2.14.1.tgz",
|
||||
"integrity": "sha512-isWHgVjnFjh2x2yuJ/tj3JbwoHu3UC2dX5G/88Cm24yB6YopVgxvBObDY7n5xW6ExmFhJpSEQqFPvq9zaXc8Jw=="
|
||||
},
|
||||
"nanomatch": {
|
||||
"version": "1.2.13",
|
||||
"resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz",
|
||||
@@ -9142,15 +9113,6 @@
|
||||
"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",
|
||||
@@ -13471,11 +13433,6 @@
|
||||
"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",
|
||||
@@ -14165,8 +14122,6 @@
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"bindings": "^1.5.0",
|
||||
"nan": "^2.12.1",
|
||||
"node-pre-gyp": "*"
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -15245,8 +15200,6 @@
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"bindings": "^1.5.0",
|
||||
"nan": "^2.12.1",
|
||||
"node-pre-gyp": "*"
|
||||
},
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "youtube-dl-material",
|
||||
"version": "4.1.0",
|
||||
"version": "4.0.0",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"ng": "ng",
|
||||
@@ -34,9 +34,7 @@
|
||||
"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.config && (!postsService.config.Advanced.multi_user_mode || (postsService.isLoggedIn && postsService.permissions.includes('settings')))" (click)="openSettingsDialog()" mat-menu-item>
|
||||
<button *ngIf="!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,16 +38,12 @@
|
||||
</div>
|
||||
<div class="sidenav-container" style="height: calc(100% - 64px)">
|
||||
<mat-sidenav-container style="height: 100%">
|
||||
<mat-sidenav [opened]="postsService.sidepanel_mode === 'side' && router.url === '/home'" [mode]="postsService.sidepanel_mode" #sidenav>
|
||||
<mat-sidenav #sidenav>
|
||||
<mat-nav-list>
|
||||
<a *ngIf="postsService.config && (!postsService.config.Advanced.multi_user_mode || postsService.isLoggedIn)" mat-list-item (click)="sidenav.close()" routerLink='/home'><ng-container i18n="Navigation menu Home Page title">Home</ng-container></a>
|
||||
<a *ngIf="postsService.config && postsService.config.Advanced.multi_user_mode && !postsService.isLoggedIn" mat-list-item (click)="sidenav.close()" routerLink='/login'><ng-container i18n="Navigation menu Login Page title">Login</ng-container></a>
|
||||
<a *ngIf="postsService.config && allowSubscriptions && (!postsService.config.Advanced.multi_user_mode || (postsService.isLoggedIn && postsService.permissions.includes('subscriptions')))" mat-list-item (click)="sidenav.close()" routerLink='/subscriptions'><ng-container i18n="Navigation menu Subscriptions Page title">Subscriptions</ng-container></a>
|
||||
<a *ngIf="postsService.config && enableDownloadsManager && (!postsService.config.Advanced.multi_user_mode || (postsService.isLoggedIn && postsService.permissions.includes('downloads_manager')))" mat-list-item (click)="sidenav.close()" routerLink='/downloads'><ng-container i18n="Navigation menu Downloads Page title">Downloads</ng-container></a>
|
||||
<ng-container *ngIf="postsService.config && allowSubscriptions && postsService.subscriptions && (!postsService.config.Advanced.multi_user_mode || (postsService.isLoggedIn && postsService.permissions.includes('subscriptions')))">
|
||||
<mat-divider></mat-divider>
|
||||
<a *ngFor="let subscription of postsService.subscriptions" mat-list-item (click)="sidenav.close()" [routerLink]="['/subscription', { id: subscription.id }]"><ngx-avatar [style.margin-right]="'10px'" size="32" [name]="subscription.name"></ngx-avatar><ng-container i18n="Navigation menu Downloads Page title">{{subscription.name}}</ng-container></a>
|
||||
</ng-container>
|
||||
</mat-nav-list>
|
||||
</mat-sidenav>
|
||||
<mat-sidenav-content [style.background]="postsService.theme ? postsService.theme.background_color : null">
|
||||
|
||||
@@ -21,6 +21,7 @@ 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';
|
||||
@@ -41,6 +42,8 @@ export class AppComponent implements OnInit {
|
||||
allowThemeChange = null;
|
||||
allowSubscriptions = false;
|
||||
enableDownloadsManager = false;
|
||||
// defaults to true to prevent attack
|
||||
settingsPinRequired = true;
|
||||
|
||||
@ViewChild('sidenav') sidenav: MatSidenav;
|
||||
@ViewChild('hamburgerMenu', { read: ElementRef }) hamburgerMenuButton: ElementRef;
|
||||
@@ -76,6 +79,7 @@ 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;
|
||||
@@ -86,13 +90,6 @@ export class AppComponent implements OnInit {
|
||||
if (!localStorage.getItem('theme')) {
|
||||
this.setTheme(themingExists ? this.defaultTheme : 'default');
|
||||
}
|
||||
|
||||
// gets the subscriptions
|
||||
if (this.allowSubscriptions) {
|
||||
this.postsService.getAllSubscriptions().subscribe(res => {
|
||||
this.postsService.subscriptions = res['subscriptions'];
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// theme stuff
|
||||
@@ -169,11 +166,6 @@ onSetTheme(theme, old_theme) {
|
||||
}
|
||||
|
||||
|
||||
getSubscriptions() {
|
||||
|
||||
}
|
||||
|
||||
|
||||
goBack() {
|
||||
if (!this.navigator) {
|
||||
this.router.navigate(['/home']);
|
||||
@@ -183,11 +175,31 @@ 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,9 @@ 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 es from '@angular/common/locales/es';
|
||||
import { AboutDialogComponent } from './dialogs/about-dialog/about-dialog.component';
|
||||
@@ -71,13 +71,6 @@ import { AddUserDialogComponent } from './dialogs/add-user-dialog/add-user-dialo
|
||||
import { ManageUserComponent } from './components/manage-user/manage-user.component';
|
||||
import { ManageRoleComponent } from './components/manage-role/manage-role.component';
|
||||
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');
|
||||
|
||||
@@ -100,6 +93,7 @@ export function isVisible({ event, element, scrollContainer, offset }: IsVisible
|
||||
SubscriptionFileCardComponent,
|
||||
SubscriptionInfoDialogComponent,
|
||||
SettingsComponent,
|
||||
CheckOrSetPinDialogComponent,
|
||||
AboutDialogComponent,
|
||||
VideoInfoDialogComponent,
|
||||
ArgModifierDialogComponent,
|
||||
@@ -115,14 +109,7 @@ export function isVisible({ event, element, scrollContainer, offset }: IsVisible
|
||||
AddUserDialogComponent,
|
||||
ManageUserComponent,
|
||||
ManageRoleComponent,
|
||||
CookiesUploaderDialogComponent,
|
||||
LogsViewerComponent,
|
||||
ModifyPlaylistComponent,
|
||||
ConfirmDialogComponent,
|
||||
UnifiedFileCardComponent,
|
||||
RecentVideosComponent,
|
||||
EditSubscriptionDialogComponent,
|
||||
CustomPlaylistsComponent
|
||||
CookiesUploaderDialogComponent
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
@@ -163,7 +150,6 @@ export function isVisible({ event, element, scrollContainer, offset }: IsVisible
|
||||
DragDropModule,
|
||||
ClipboardModule,
|
||||
NgxFileDropModule,
|
||||
AvatarModule,
|
||||
VgCoreModule,
|
||||
VgControlsModule,
|
||||
VgOverlayPlayModule,
|
||||
@@ -177,7 +163,8 @@ export function isVisible({ event, element, scrollContainer, offset }: IsVisible
|
||||
CreatePlaylistComponent,
|
||||
SubscribeDialogComponent,
|
||||
SubscriptionInfoDialogComponent,
|
||||
SettingsComponent
|
||||
SettingsComponent,
|
||||
CheckOrSetPinDialogComponent
|
||||
],
|
||||
providers: [
|
||||
PostsService
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
<div *ngIf="playlists && playlists.length > 0">
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div *ngFor="let playlist of playlists; let i = index" [ngClass]="[ postsService.card_size === 'small' ? 'col-2 mb-2 mt-2 file-col' : '', postsService.card_size === 'medium' ? 'col-6 col-lg-4 mb-2 mt-2 file-col' : '' ]">
|
||||
<app-unified-file-card [index]="i" [card_size]="postsService.card_size" (goToFile)="goToPlaylist($event)" [file_obj]="playlist" [is_playlist]="true" (editPlaylist)="editPlaylistDialog($event)" (deleteFile)="deletePlaylist($event)"></app-unified-file-card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div *ngIf="playlists && playlists.length === 0" style="text-align: center;">
|
||||
No playlists available. Create one from your downloading files by clicking the blue plus button.
|
||||
</div>
|
||||
<div class="add-playlist-button"><button (click)="openCreatePlaylistDialog()" mat-fab><mat-icon>add</mat-icon></button></div>
|
||||
@@ -1,10 +0,0 @@
|
||||
.add-playlist-button {
|
||||
float: right;
|
||||
position: relative;
|
||||
bottom: 15px;
|
||||
right: 15px;
|
||||
}
|
||||
|
||||
.file-col {
|
||||
max-width: 240px;
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
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();
|
||||
});
|
||||
});
|
||||
@@ -1,112 +0,0 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { PostsService } from 'app/posts.services';
|
||||
import { Router } from '@angular/router';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { CreatePlaylistComponent } from 'app/create-playlist/create-playlist.component';
|
||||
import { ModifyPlaylistComponent } from 'app/dialogs/modify-playlist/modify-playlist.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-custom-playlists',
|
||||
templateUrl: './custom-playlists.component.html',
|
||||
styleUrls: ['./custom-playlists.component.scss']
|
||||
})
|
||||
export class CustomPlaylistsComponent implements OnInit {
|
||||
|
||||
playlists = null;
|
||||
playlists_received = false;
|
||||
downloading_content = {'video': {}, 'audio': {}};
|
||||
|
||||
constructor(public postsService: PostsService, private router: Router, private dialog: MatDialog) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.postsService.service_initialized.subscribe(init => {
|
||||
if (init) {
|
||||
this.getAllPlaylists();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getAllPlaylists() {
|
||||
this.playlists_received = false;
|
||||
this.postsService.getAllFiles().subscribe(res => {
|
||||
this.playlists = res['playlists'];
|
||||
this.playlists_received = true;
|
||||
});
|
||||
}
|
||||
|
||||
// creating a playlist
|
||||
openCreatePlaylistDialog() {
|
||||
const dialogRef = this.dialog.open(CreatePlaylistComponent, {
|
||||
data: {
|
||||
}
|
||||
});
|
||||
dialogRef.afterClosed().subscribe(result => {
|
||||
if (result) {
|
||||
this.getAllPlaylists();
|
||||
this.postsService.openSnackBar('Successfully created playlist!', '');
|
||||
} else if (result === false) {
|
||||
this.postsService.openSnackBar('ERROR: failed to create playlist!', '');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
goToPlaylist(playlist) {
|
||||
const playlistID = playlist.id;
|
||||
const type = playlist.type;
|
||||
|
||||
if (playlist) {
|
||||
if (this.postsService.config['Extra']['download_only_mode']) {
|
||||
this.downloading_content[type][playlistID] = true;
|
||||
this.downloadPlaylist(playlist.fileNames, type, playlist.name, playlistID);
|
||||
} else {
|
||||
localStorage.setItem('player_navigator', this.router.url);
|
||||
const fileNames = playlist.fileNames;
|
||||
this.router.navigate(['/player', {fileNames: fileNames.join('|nvr|'), type: type, id: playlistID, uid: playlistID}]);
|
||||
}
|
||||
} else {
|
||||
// playlist not found
|
||||
console.error(`Playlist with ID ${playlistID} not found!`);
|
||||
}
|
||||
}
|
||||
|
||||
downloadPlaylist(fileNames, type, zipName = null, playlistID = null) {
|
||||
this.postsService.downloadFileFromServer(fileNames, type, zipName).subscribe(res => {
|
||||
if (playlistID) { this.downloading_content[type][playlistID] = false };
|
||||
const blob: Blob = res;
|
||||
saveAs(blob, zipName + '.zip');
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
deletePlaylist(args) {
|
||||
const playlist = args.file;
|
||||
const index = args.index;
|
||||
const playlistID = playlist.id;
|
||||
this.postsService.removePlaylist(playlistID, 'audio').subscribe(res => {
|
||||
if (res['success']) {
|
||||
this.playlists.splice(index, 1);
|
||||
this.postsService.openSnackBar('Playlist successfully removed.', '');
|
||||
}
|
||||
this.getAllPlaylists();
|
||||
});
|
||||
}
|
||||
|
||||
editPlaylistDialog(args) {
|
||||
const playlist = args.playlist;
|
||||
const index = args.index;
|
||||
const dialogRef = this.dialog.open(ModifyPlaylistComponent, {
|
||||
data: {
|
||||
playlist: playlist,
|
||||
width: '65vw'
|
||||
}
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe(res => {
|
||||
// updates playlist in file manager if it changed
|
||||
if (dialogRef.componentInstance.playlist_updated) {
|
||||
this.playlists[index] = dialogRef.componentInstance.original_playlist;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -14,9 +14,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
@@ -43,7 +43,7 @@ export class DownloadsComponent implements OnInit, OnDestroy {
|
||||
valid_sessions_length = 0;
|
||||
|
||||
sort_downloads = (a, b) => {
|
||||
const result = b.value.timestamp_start - a.value.timestamp_start;
|
||||
const result = a.value.timestamp_start < b.value.timestamp_start;
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -81,7 +81,7 @@ export class DownloadsComponent implements OnInit, OnDestroy {
|
||||
clearDownload(session_id, download_uid) {
|
||||
this.postsService.clearDownloads(false, session_id, download_uid).subscribe(res => {
|
||||
if (res['success']) {
|
||||
// this.downloads = res['downloads'];
|
||||
this.downloads = res['downloads'];
|
||||
} else {
|
||||
}
|
||||
});
|
||||
@@ -107,32 +107,11 @@ export class DownloadsComponent implements OnInit, OnDestroy {
|
||||
|
||||
assignNewValues(new_downloads_by_session) {
|
||||
const session_keys = Object.keys(new_downloads_by_session);
|
||||
|
||||
// remove missing session IDs
|
||||
const current_session_ids = Object.keys(this.downloads);
|
||||
const missing_session_ids = current_session_ids.filter(session => session_keys.indexOf(session) === -1)
|
||||
|
||||
for (const missing_session_id of missing_session_ids) {
|
||||
delete this.downloads[missing_session_id];
|
||||
}
|
||||
|
||||
// loop through sessions
|
||||
for (let i = 0; i < session_keys.length; i++) {
|
||||
const session_id = session_keys[i];
|
||||
const session_downloads_by_id = new_downloads_by_session[session_id];
|
||||
const session_download_ids = Object.keys(session_downloads_by_id);
|
||||
|
||||
if (this.downloads[session_id]) {
|
||||
// remove missing download IDs
|
||||
const current_download_ids = Object.keys(this.downloads[session_id]);
|
||||
const missing_download_ids = current_download_ids.filter(download => session_download_ids.indexOf(download) === -1)
|
||||
|
||||
for (const missing_download_id of missing_download_ids) {
|
||||
console.log('removing missing download id');
|
||||
delete this.downloads[session_id][missing_download_id];
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.downloads[session_id]) {
|
||||
this.downloads[session_id] = session_downloads_by_id;
|
||||
} else {
|
||||
|
||||
@@ -49,19 +49,9 @@ export class LoginComponent implements OnInit {
|
||||
this.loggingIn = false;
|
||||
if (res['token']) {
|
||||
this.postsService.afterLogin(res['user'], res['token'], res['permissions'], res['available_permissions']);
|
||||
} else {
|
||||
this.openSnackBar('Login failed, unknown error.');
|
||||
}
|
||||
}, err => {
|
||||
this.loggingIn = false;
|
||||
const error_code = err.status;
|
||||
if (error_code === 401) {
|
||||
this.openSnackBar('User name or password is incorrect!');
|
||||
} else if (error_code === 404) {
|
||||
this.openSnackBar('Login failed, cannot connect to the server.');
|
||||
} else {
|
||||
this.openSnackBar('Login failed, unknown error.');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -94,7 +84,7 @@ export class LoginComponent implements OnInit {
|
||||
this.loginUsernameInput = res['user']['name'];
|
||||
this.selectedTabIndex = 0;
|
||||
} else {
|
||||
this.openSnackBar('Failed to register user, unknown error.');
|
||||
|
||||
}
|
||||
}, err => {
|
||||
this.registering = false;
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
<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>
|
||||
<!-- 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="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">
|
||||
<mat-option [value]="10">10</mat-option>
|
||||
<mat-option [value]="25">25</mat-option>
|
||||
<mat-option [value]="50">50</mat-option>
|
||||
<mat-option [value]="100">100</mat-option>
|
||||
<mat-option [value]="0">All</mat-option>
|
||||
</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>
|
||||
@@ -1 +0,0 @@
|
||||
.spacer {flex: 1 1 auto;}
|
||||
@@ -1,25 +0,0 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { LogsViewerComponent } from './logs-viewer.component';
|
||||
|
||||
describe('LogsViewerComponent', () => {
|
||||
let component: LogsViewerComponent;
|
||||
let fixture: ComponentFixture<LogsViewerComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ LogsViewerComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(LogsViewerComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -1,85 +0,0 @@
|
||||
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',
|
||||
templateUrl: './logs-viewer.component.html',
|
||||
styleUrls: ['./logs-viewer.component.scss']
|
||||
})
|
||||
export class LogsViewerComponent implements OnInit {
|
||||
|
||||
logs: any = null;
|
||||
logs_text: string = null;
|
||||
requested_lines = 50;
|
||||
logs_loading = false;
|
||||
constructor(private postsService: PostsService, private dialog: MatDialog) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.getLogs();
|
||||
}
|
||||
|
||||
getLogs() {
|
||||
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'] !== 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!');
|
||||
}
|
||||
}, err => {
|
||||
this.logs_loading = false;
|
||||
console.error(err);
|
||||
this.postsService.openSnackBar('Failed to retrieve logs!');
|
||||
});
|
||||
}
|
||||
|
||||
copiedLogsToClipboard() {
|
||||
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 role default">Use role default</ng-container></mat-radio-button>
|
||||
<mat-radio-button value="default"><ng-container i18n="Use default">Use 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 style="margin-bottom: 5px;" mat-stroked-button mat-dialog-close><ng-container i18n="Close">Close</ng-container></button>
|
||||
<button mat-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" i18n-placeholder="search field description">
|
||||
<input matInput (keyup)="applyFilter($event.target.value)" placeholder="Search">
|
||||
</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="Save" i18n-matTooltip="save user edit action button tooltip">
|
||||
<button mat-icon-button color="primary" (click)="finishEditing(row.uid)" matTooltip="Finish editing user">
|
||||
<mat-icon>done</mat-icon>
|
||||
</button>
|
||||
<button mat-icon-button (click)="disableEditMode()" matTooltip="Cancel" i18n-matTooltip="cancel user edit action button tooltip">
|
||||
<button mat-icon-button (click)="disableEditMode()" matTooltip="Cancel editing user">
|
||||
<mat-icon>cancel</mat-icon>
|
||||
</button>
|
||||
</span>
|
||||
<ng-template #notediting>
|
||||
<button mat-icon-button (click)="enableEditMode(row.uid)" matTooltip="Edit user" i18n-matTooltip="edit user action button tooltip">
|
||||
<button mat-icon-button (click)="enableEditMode(row.uid)" matTooltip="Edit user">
|
||||
<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" i18n-matTooltip="manage user action button tooltip">
|
||||
<button (click)="manageUser(row.uid)" mat-icon-button [disabled]="editObject && editObject.uid === row.uid" matTooltip="Manage user">
|
||||
<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" i18n-matTooltip="delete user action button tooltip">
|
||||
<button mat-icon-button [disabled]="editObject && editObject.uid === row.uid || row.uid === postsService.user.uid" (click)="removeUser(row.uid)" matTooltip="Delete user">
|
||||
<mat-icon>delete</mat-icon>
|
||||
</button>
|
||||
</mat-cell>
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
<div class="flex-grid">
|
||||
<div class="col">
|
||||
<div style="display: inline-block;">
|
||||
<mat-form-field style="width: 132px;">
|
||||
<mat-select [(ngModel)]="this.filterProperty" (selectionChange)="filterOptionChanged($event.value)">
|
||||
<mat-option *ngFor="let filterOption of filterProperties | keyvalue" [value]="filterOption.value">
|
||||
{{filterOption['value']['label']}}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div style="display: inline-block;">
|
||||
<button (click)="toggleModeChange()" mat-icon-button><mat-icon>{{descendingMode ? 'arrow_downward' : 'arrow_upward'}}</mat-icon></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col">
|
||||
<h4 style="text-align: center">My videos</h4>
|
||||
</div>
|
||||
<div class="col" style="top: 25px;">
|
||||
<mat-form-field [ngClass]="searchIsFocused ? 'search-bar-focused' : 'search-bar-unfocused'" class="search-bar" color="accent">
|
||||
<input (focus)="searchIsFocused = true" (blur)="searchIsFocused = false" class="search-input" type="text" placeholder="Search" i18n-placeholder="Files search placeholder" [(ngModel)]="search_text" (ngModelChange)="onSearchInputChanged($event)" matInput>
|
||||
<mat-icon matSuffix>search</mat-icon>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div *ngFor="let file of filtered_files; let i = index" [ngClass]="[ postsService.card_size === 'small' ? 'col-2 mb-2 mt-2 file-col' : '', postsService.card_size === 'medium' ? 'col-6 col-lg-4 mb-2 mt-2 file-col' : '' ]">
|
||||
<app-unified-file-card [index]="i" [card_size]="postsService.card_size" (goToFile)="goToFile($event)" (goToSubscription)="goToSubscription($event)" [file_obj]="file" [use_youtubedl_archive]="postsService.config['Downloader']['use_youtubedl_archive']" (deleteFile)="deleteFile($event)"></app-unified-file-card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,34 +0,0 @@
|
||||
.file-col {
|
||||
max-width: 240px;
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
transition: all .5s ease;
|
||||
position: relative;
|
||||
float: right;
|
||||
}
|
||||
|
||||
.search-bar-unfocused {
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
transition: all .5s ease;
|
||||
}
|
||||
|
||||
.search-bar-focused {
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
.flex-grid {
|
||||
width: 100%;
|
||||
display: block;
|
||||
position: relative;
|
||||
padding-left: 12px;
|
||||
padding-right: 12px;
|
||||
}
|
||||
|
||||
.col {
|
||||
width: 33%;
|
||||
display: inline-block;
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
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();
|
||||
});
|
||||
});
|
||||
@@ -1,262 +0,0 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { PostsService } from 'app/posts.services';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
@Component({
|
||||
selector: 'app-recent-videos',
|
||||
templateUrl: './recent-videos.component.html',
|
||||
styleUrls: ['./recent-videos.component.scss']
|
||||
})
|
||||
export class RecentVideosComponent implements OnInit {
|
||||
|
||||
normal_files_received = false;
|
||||
subscription_files_received = false;
|
||||
files: any[] = null;
|
||||
filtered_files: any[] = null;
|
||||
downloading_content = {'video': {}, 'audio': {}};
|
||||
search_mode = false;
|
||||
search_text = '';
|
||||
searchIsFocused = false;
|
||||
descendingMode = true;
|
||||
filterProperties = {
|
||||
'registered': {
|
||||
'key': 'registered',
|
||||
'label': 'Download Date',
|
||||
'property': 'registered'
|
||||
},
|
||||
'upload_date': {
|
||||
'key': 'upload_date',
|
||||
'label': 'Upload Date',
|
||||
'property': 'upload_date'
|
||||
},
|
||||
'name': {
|
||||
'key': 'name',
|
||||
'label': 'Name',
|
||||
'property': 'title'
|
||||
},
|
||||
'file_size': {
|
||||
'key': 'file_size',
|
||||
'label': 'File Size',
|
||||
'property': 'size'
|
||||
},
|
||||
'duration': {
|
||||
'key': 'duration',
|
||||
'label': 'Duration',
|
||||
'property': 'duration'
|
||||
}
|
||||
};
|
||||
filterProperty = this.filterProperties['upload_date'];
|
||||
|
||||
constructor(public postsService: PostsService, private router: Router) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.postsService.service_initialized.subscribe(init => {
|
||||
if (init) {
|
||||
this.getAllFiles();
|
||||
}
|
||||
});
|
||||
|
||||
// set filter property to cached
|
||||
const cached_filter_property = localStorage.getItem('filter_property');
|
||||
if (cached_filter_property && this.filterProperties[cached_filter_property]) {
|
||||
this.filterProperty = this.filterProperties[cached_filter_property];
|
||||
}
|
||||
}
|
||||
|
||||
// search
|
||||
|
||||
onSearchInputChanged(newvalue) {
|
||||
if (newvalue.length > 0) {
|
||||
this.search_mode = true;
|
||||
this.filterFiles(newvalue);
|
||||
} else {
|
||||
this.search_mode = false;
|
||||
this.filtered_files = this.files;
|
||||
}
|
||||
}
|
||||
|
||||
private filterFiles(value: string) {
|
||||
const filterValue = value.toLowerCase();
|
||||
this.filtered_files = this.files.filter(option => option.id.toLowerCase().includes(filterValue));
|
||||
}
|
||||
|
||||
filterByProperty(prop) {
|
||||
if (this.descendingMode) {
|
||||
this.filtered_files = this.filtered_files.sort((a, b) => (a[prop] > b[prop] ? -1 : 1));
|
||||
} else {
|
||||
this.filtered_files = this.filtered_files.sort((a, b) => (a[prop] > b[prop] ? 1 : -1));
|
||||
}
|
||||
}
|
||||
|
||||
filterOptionChanged(value) {
|
||||
this.filterByProperty(value['property']);
|
||||
localStorage.setItem('filter_property', value['key']);
|
||||
}
|
||||
|
||||
toggleModeChange() {
|
||||
this.descendingMode = !this.descendingMode;
|
||||
this.filterByProperty(this.filterProperty['property']);
|
||||
}
|
||||
|
||||
// get files
|
||||
|
||||
getAllFiles() {
|
||||
this.normal_files_received = false;
|
||||
this.postsService.getAllFiles().subscribe(res => {
|
||||
this.files = res['files'];
|
||||
this.files.forEach(file => {
|
||||
file.duration = typeof file.duration !== 'string' ? file.duration : this.durationStringToNumber(file.duration);
|
||||
});
|
||||
this.files.sort(this.sortFiles);
|
||||
if (this.search_mode) {
|
||||
this.filterFiles(this.search_text);
|
||||
} else {
|
||||
this.filtered_files = this.files;
|
||||
}
|
||||
this.filterByProperty(this.filterProperty['property']);
|
||||
});
|
||||
}
|
||||
|
||||
// navigation
|
||||
|
||||
goToFile(file) {
|
||||
if (this.postsService.config['Extra']['download_only_mode']) {
|
||||
this.downloadFile(file);
|
||||
} else {
|
||||
this.navigateToFile(file);
|
||||
}
|
||||
}
|
||||
|
||||
navigateToFile(file) {
|
||||
localStorage.setItem('player_navigator', this.router.url);
|
||||
if (file.sub_id) {
|
||||
const sub = this.postsService.getSubscriptionByID(file.sub_id)
|
||||
if (sub.streamingOnly) {
|
||||
this.router.navigate(['/player', {name: file.id,
|
||||
url: file.requested_formats ? file.requested_formats[0].url : file.url}]);
|
||||
} else {
|
||||
this.router.navigate(['/player', {fileNames: file.id,
|
||||
type: file.isAudio ? 'audio' : 'video', subscriptionName: sub.name,
|
||||
subPlaylist: sub.isPlaylist}]);
|
||||
}
|
||||
} else {
|
||||
this.router.navigate(['/player', {type: file.isAudio ? 'audio' : 'video', uid: file.uid}]);
|
||||
}
|
||||
}
|
||||
|
||||
goToSubscription(file) {
|
||||
this.router.navigate(['/subscription', {id: file.sub_id}]);
|
||||
}
|
||||
|
||||
// downloading
|
||||
|
||||
downloadFile(file) {
|
||||
if (file.sub_id) {
|
||||
this.downloadSubscriptionFile(file);
|
||||
} else {
|
||||
this.downloadNormalFile(file);
|
||||
}
|
||||
}
|
||||
|
||||
downloadSubscriptionFile(file) {
|
||||
const type = file.isAudio ? 'audio' : 'video';
|
||||
const ext = type === 'audio' ? '.mp3' : '.mp4'
|
||||
const sub = this.postsService.getSubscriptionByID(file.sub_id);
|
||||
console.log(sub.isPlaylist)
|
||||
this.postsService.downloadFileFromServer(file.id, type, null, null, sub.name, sub.isPlaylist,
|
||||
this.postsService.user ? this.postsService.user.uid : null, null).subscribe(res => {
|
||||
const blob: Blob = res;
|
||||
saveAs(blob, file.id + ext);
|
||||
}, err => {
|
||||
console.log(err);
|
||||
});
|
||||
}
|
||||
|
||||
downloadNormalFile(file) {
|
||||
const type = file.isAudio ? 'audio' : 'video';
|
||||
const ext = type === 'audio' ? '.mp3' : '.mp4'
|
||||
const name = file.id;
|
||||
this.downloading_content[type][name] = true;
|
||||
this.postsService.downloadFileFromServer(name, type).subscribe(res => {
|
||||
this.downloading_content[type][name] = false;
|
||||
const blob: Blob = res;
|
||||
saveAs(blob, decodeURIComponent(name) + ext);
|
||||
|
||||
if (!this.postsService.config.Extra.file_manager_enabled) {
|
||||
// tell server to delete the file once downloaded
|
||||
this.postsService.deleteFile(name, false).subscribe(delRes => {
|
||||
// reload mp4s
|
||||
this.getAllFiles();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// deleting
|
||||
|
||||
deleteFile(args) {
|
||||
const file = args.file;
|
||||
const index = args.index;
|
||||
const blacklistMode = args.blacklistMode;
|
||||
|
||||
if (file.sub_id) {
|
||||
this.deleteSubscriptionFile(file, index, blacklistMode);
|
||||
} else {
|
||||
this.deleteNormalFile(file, index, blacklistMode);
|
||||
}
|
||||
}
|
||||
|
||||
deleteNormalFile(file, index, blacklistMode = false) {
|
||||
this.postsService.deleteFile(file.uid, file.isAudio, blacklistMode).subscribe(result => {
|
||||
if (result) {
|
||||
this.postsService.openSnackBar('Delete success!', 'OK.');
|
||||
this.files.splice(index, 1);
|
||||
} else {
|
||||
this.postsService.openSnackBar('Delete failed!', 'OK.');
|
||||
}
|
||||
}, err => {
|
||||
this.postsService.openSnackBar('Delete failed!', 'OK.');
|
||||
});
|
||||
}
|
||||
|
||||
deleteSubscriptionFile(file, index, blacklistMode = false) {
|
||||
if (blacklistMode) {
|
||||
this.deleteForever(file, index);
|
||||
} else {
|
||||
this.deleteAndRedownload(file, index);
|
||||
}
|
||||
}
|
||||
|
||||
deleteAndRedownload(file, index) {
|
||||
const sub = this.postsService.getSubscriptionByID(file.sub_id);
|
||||
this.postsService.deleteSubscriptionFile(sub, file.id, false, file.uid).subscribe(res => {
|
||||
this.postsService.openSnackBar(`Successfully deleted file: '${file.id}'`);
|
||||
this.files.splice(index, 1);
|
||||
});
|
||||
}
|
||||
|
||||
deleteForever(file, index) {
|
||||
const sub = this.postsService.getSubscriptionByID(file.sub_id);
|
||||
this.postsService.deleteSubscriptionFile(sub, file.id, true, file.uid).subscribe(res => {
|
||||
this.postsService.openSnackBar(`Successfully deleted file: '${file.id}'`);
|
||||
this.files.splice(index, 1);
|
||||
});
|
||||
}
|
||||
|
||||
// sorting and filtering
|
||||
|
||||
sortFiles(a, b) {
|
||||
// uses the 'registered' flag as the timestamp
|
||||
const result = b.registered - a.registered;
|
||||
return result;
|
||||
}
|
||||
|
||||
durationStringToNumber(dur_str) {
|
||||
let num_sum = 0;
|
||||
const dur_str_parts = dur_str.split(':');
|
||||
for (let i = dur_str_parts.length-1; i >= 0; i--) {
|
||||
num_sum += parseInt(dur_str_parts[i])*(60**(dur_str_parts.length-1-i));
|
||||
}
|
||||
return num_sum;
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
<div (mouseover)="elevated=true" (mouseout)="elevated=false" style="position: relative; width: fit-content;">
|
||||
<div class="download-time"><mat-icon class="audio-video-icon">{{(file_obj.type === 'audio' || file_obj.isAudio) ? 'audiotrack' : 'movie'}}</mat-icon> {{file_obj.registered | date:'shortDate'}}</div>
|
||||
<button [matMenuTriggerFor]="action_menu" class="menuButton" mat-icon-button><mat-icon>more_vert</mat-icon></button>
|
||||
<mat-menu #action_menu="matMenu">
|
||||
<ng-container *ngIf="!is_playlist">
|
||||
<button (click)="openFileInfoDialog()" mat-menu-item><mat-icon>info</mat-icon><ng-container i18n="Video info button">Info</ng-container></button>
|
||||
<button (click)="navigateToSubscription()" mat-menu-item *ngIf="file_obj.sub_id"><mat-icon>{{file_obj.isAudio ? 'library_music' : 'video_library'}}</mat-icon> <ng-container i18n="Go to subscription menu item">Go to subscription</ng-container></button>
|
||||
<mat-divider></mat-divider>
|
||||
<button *ngIf="file_obj.sub_id" (click)="emitDeleteFile()" mat-menu-item>
|
||||
<mat-icon>restore</mat-icon><ng-container i18n="Delete and redownload subscription video button">Delete and redownload</ng-container>
|
||||
</button>
|
||||
<button *ngIf="file_obj.sub_id && use_youtubedl_archive" (click)="emitDeleteFile(true)" mat-menu-item>
|
||||
<mat-icon>delete_forever</mat-icon><ng-container i18n="Delete forever subscription video button">Delete forever</ng-container>
|
||||
</button>
|
||||
<button *ngIf="!file_obj.sub_id" (click)="emitDeleteFile()" mat-menu-item><mat-icon>delete</mat-icon><ng-container i18n="Delete video button">Delete</ng-container></button>
|
||||
<button *ngIf="!file_obj.sub_id && use_youtubedl_archive" (click)="emitDeleteFile(true)" mat-menu-item><mat-icon>delete_forever</mat-icon><ng-container i18n="Delete and blacklist video button">Delete and blacklist</ng-container></button>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="is_playlist">
|
||||
<button (click)="emitEditPlaylist()" mat-menu-item><mat-icon>edit</mat-icon><ng-container i18n="Playlist edit button">Edit</ng-container></button>
|
||||
<mat-divider></mat-divider>
|
||||
<button (click)="emitDeleteFile()" mat-menu-item><mat-icon>delete_forever</mat-icon><ng-container i18n="Delete playlist">Delete</ng-container></button>
|
||||
</ng-container>
|
||||
</mat-menu>
|
||||
<mat-card [matTooltip]="null" (click)="navigateToFile()" matRipple class="file-mat-card" [ngClass]="{'small-mat-card': card_size === 'small', 'file-mat-card': card_size === 'medium', 'mat-elevation-z4': !elevated, 'mat-elevation-z8': elevated}">
|
||||
<div style="padding:5px">
|
||||
<div *ngIf="file_obj.thumbnailURL" class="img-div">
|
||||
<div style="position: relative">
|
||||
<img [ngClass]="{'image-small': card_size === 'small', 'image': card_size === 'medium'}" [src]="file_obj.thumbnailURL" alt="Thumbnail">
|
||||
<div class="duration-time">
|
||||
{{file_length}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<span [ngClass]="{'max-two-lines': card_size !== 'small', 'max-one-line': card_size === 'small' }"><strong>{{!is_playlist ? file_obj.title : file_obj.name}}</strong></span>
|
||||
</div>
|
||||
</mat-card>
|
||||
</div>
|
||||
@@ -1,117 +0,0 @@
|
||||
.file-mat-card {
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
padding: 0px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.small-mat-card {
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
padding: 0px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.menuButton {
|
||||
right: 0px;
|
||||
top: -1px;
|
||||
position: absolute;
|
||||
z-index: 999;
|
||||
|
||||
}
|
||||
|
||||
/* Coerce the <span> icon container away from display:inline */
|
||||
.mat-icon-button .mat-button-wrapper {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.image {
|
||||
width: 200px;
|
||||
height: 112.5px;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.image-small {
|
||||
width: 150px;
|
||||
height: 84.5px;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.example-full-width-height {
|
||||
width: 100%;
|
||||
height: 100%
|
||||
}
|
||||
|
||||
.centered {
|
||||
margin: 0 auto;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
}
|
||||
|
||||
.img-div {
|
||||
max-height: 80px;
|
||||
padding: 0px;
|
||||
margin: 32px 0px 0px -5px;
|
||||
width: calc(100% + 5px + 5px);
|
||||
}
|
||||
|
||||
.max-two-lines {
|
||||
display: -webkit-box;
|
||||
display: -moz-box;
|
||||
max-height: 2.4em;
|
||||
line-height: 1.2em;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
bottom: 5px;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.max-one-line {
|
||||
display: -webkit-box;
|
||||
display: -moz-box;
|
||||
max-height: 1.2em;
|
||||
line-height: 1.2em;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
bottom: 5px;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.duration-time {
|
||||
position: absolute;
|
||||
bottom: 5px;
|
||||
right: 5px;
|
||||
z-index: 99999;
|
||||
background: rgba(255,255,255,0.6);
|
||||
padding-left: 5px;
|
||||
padding-right: 5px;
|
||||
}
|
||||
|
||||
.download-time {
|
||||
position: absolute;
|
||||
top: 1px;
|
||||
left: 5px;
|
||||
z-index: 99999;
|
||||
}
|
||||
|
||||
.audio-video-icon {
|
||||
position: relative;
|
||||
top: 6px;
|
||||
}
|
||||
|
||||
@media (max-width: 576px){
|
||||
|
||||
// .example-card {
|
||||
// width: 175px !important;
|
||||
// }
|
||||
|
||||
// .image {
|
||||
// width: 175px;
|
||||
// }
|
||||
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
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();
|
||||
});
|
||||
});
|
||||
@@ -1,95 +0,0 @@
|
||||
import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { VideoInfoDialogComponent } from 'app/dialogs/video-info-dialog/video-info-dialog.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-unified-file-card',
|
||||
templateUrl: './unified-file-card.component.html',
|
||||
styleUrls: ['./unified-file-card.component.scss']
|
||||
})
|
||||
export class UnifiedFileCardComponent implements OnInit {
|
||||
|
||||
// required info
|
||||
file_title = '';
|
||||
file_length = '';
|
||||
file_thumbnail = '';
|
||||
type = null;
|
||||
elevated = false;
|
||||
|
||||
@Input() file_obj = null;
|
||||
@Input() card_size = 'medium';
|
||||
@Input() use_youtubedl_archive = false;
|
||||
@Input() is_playlist = false;
|
||||
@Input() index: number;
|
||||
@Output() goToFile = new EventEmitter<any>();
|
||||
@Output() goToSubscription = new EventEmitter<any>();
|
||||
@Output() deleteFile = new EventEmitter<any>();
|
||||
@Output() editPlaylist = new EventEmitter<any>();
|
||||
|
||||
/*
|
||||
Planned sizes:
|
||||
small: 150x175
|
||||
medium: 200x200
|
||||
big: 250x200
|
||||
*/
|
||||
|
||||
constructor(private dialog: MatDialog) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.file_length = fancyTimeFormat(this.file_obj.duration);
|
||||
}
|
||||
|
||||
emitDeleteFile(blacklistMode = false) {
|
||||
this.deleteFile.emit({
|
||||
file: this.file_obj,
|
||||
index: this.index,
|
||||
blacklistMode: blacklistMode
|
||||
});
|
||||
}
|
||||
|
||||
navigateToFile() {
|
||||
this.goToFile.emit(this.file_obj);
|
||||
}
|
||||
|
||||
navigateToSubscription() {
|
||||
this.goToSubscription.emit(this.file_obj);
|
||||
}
|
||||
|
||||
openFileInfoDialog() {
|
||||
this.dialog.open(VideoInfoDialogComponent, {
|
||||
data: {
|
||||
file: this.file_obj,
|
||||
},
|
||||
minWidth: '50vw'
|
||||
})
|
||||
}
|
||||
|
||||
emitEditPlaylist() {
|
||||
this.editPlaylist.emit({
|
||||
playlist: this.file_obj,
|
||||
index: this.index
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function fancyTimeFormat(time) {
|
||||
if (typeof time === 'string') {
|
||||
return time;
|
||||
}
|
||||
// Hours, minutes and seconds
|
||||
const hrs = ~~(time / 3600);
|
||||
const mins = ~~((time % 3600) / 60);
|
||||
const secs = ~~time % 60;
|
||||
|
||||
// Output like "1:01" or "4:03:59" or "123:03:59"
|
||||
let ret = '';
|
||||
|
||||
if (hrs > 0) {
|
||||
ret += '' + hrs + ':' + (mins < 10 ? '0' : '');
|
||||
}
|
||||
|
||||
ret += '' + mins + ':' + (secs < 10 ? '0' : '');
|
||||
ret += '' + secs;
|
||||
return ret;
|
||||
}
|
||||
@@ -1 +1 @@
|
||||
export const CURRENT_VERSION = 'v4.1';
|
||||
export const CURRENT_VERSION = 'v4.0';
|
||||
|
||||
@@ -1,34 +1,18 @@
|
||||
<h4 mat-dialog-title i18n="Create a playlist dialog title">Create a playlist</h4>
|
||||
<form>
|
||||
<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>
|
||||
<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>
|
||||
</form>
|
||||
|
||||
|
||||
@@ -14,8 +14,6 @@ export class CreatePlaylistComponent implements OnInit {
|
||||
filesToSelectFrom = null;
|
||||
type = null;
|
||||
filesSelect = new FormControl();
|
||||
audiosToSelectFrom = null;
|
||||
videosToSelectFrom = null;
|
||||
name = '';
|
||||
|
||||
create_in_progress = false;
|
||||
@@ -30,30 +28,12 @@ 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, duration).subscribe(res => {
|
||||
this.postsService.createPlaylist(this.name, this.filesSelect.value, this.type, thumbnailURL).subscribe(res => {
|
||||
this.create_in_progress = false;
|
||||
if (res['success']) {
|
||||
this.dialogRef.close(true);
|
||||
@@ -64,12 +44,8 @@ export class CreatePlaylistComponent implements OnInit {
|
||||
}
|
||||
|
||||
getThumbnailURL() {
|
||||
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];
|
||||
for (let i = 0; i < this.filesToSelectFrom.length; i++) {
|
||||
const file = this.filesToSelectFrom[i];
|
||||
if (file.id === this.filesSelect.value[0]) {
|
||||
// different services store the thumbnail in different places
|
||||
if (file.thumbnailURL) { return file.thumbnailURL };
|
||||
@@ -79,35 +55,4 @@ 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,43 +18,15 @@
|
||||
<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> <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>
|
||||
<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>
|
||||
<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" [disabled]="true">
|
||||
Large
|
||||
</mat-option>
|
||||
<mat-option value="medium">
|
||||
Medium
|
||||
</mat-option>
|
||||
<mat-option value="small">
|
||||
Small
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
</mat-dialog-content>
|
||||
|
||||
<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>
|
||||
<button style="margin-bottom: 5px;" mat-stroked-button mat-dialog-close>Close</button>
|
||||
</mat-dialog-actions>
|
||||
@@ -16,13 +16,11 @@ 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() {
|
||||
@@ -32,14 +30,4 @@ 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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
<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>
|
||||
@@ -0,0 +1,6 @@
|
||||
.spinner-div {
|
||||
position: absolute;
|
||||
margin: 0 auto;
|
||||
top: 30%;
|
||||
left: 42%;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
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();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,96 @@
|
||||
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,
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
<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>
|
||||
@@ -1,5 +0,0 @@
|
||||
.spacer {flex: 1 1 auto;}
|
||||
|
||||
.mat-spinner {
|
||||
margin-left: 8px;
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
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();
|
||||
});
|
||||
});
|
||||
@@ -1,44 +0,0 @@
|
||||
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 {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
<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>
|
||||
@@ -1,9 +0,0 @@
|
||||
.args-edit-button {
|
||||
position: absolute;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.unit-select {
|
||||
width: 75px;
|
||||
margin-left: 20px;
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
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();
|
||||
});
|
||||
});
|
||||
@@ -1,124 +0,0 @@
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
<h4 mat-dialog-title><ng-container i18n="Modify playlist dialog title">Modify playlist</ng-container></h4>
|
||||
|
||||
<mat-dialog-content>
|
||||
<!-- Playlist info -->
|
||||
<div>
|
||||
<mat-form-field color="accent">
|
||||
<input matInput placeholder="Name" i18n-placeholder="Name" [(ngModel)]="playlist.name">
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<!-- Playlist order -->
|
||||
<mat-button-toggle-group class="media-list" cdkDropList (cdkDropListDropped)="drop($event)" style="width: 80%; left: 9%" vertical name="videoSelect" aria-label="Video Select" #group="matButtonToggleGroup">
|
||||
<mat-button-toggle class="media-box" cdkDrag *ngFor="let playlist_item of playlist.fileNames; let i = index" [checked]="false"><div><div class="playlist-item-text">{{playlist_item}}</div> <button (click)="removeContent(i)" class="remove-item-button" mat-icon-button><mat-icon>cancel</mat-icon></button></div></mat-button-toggle>
|
||||
</mat-button-toggle-group>
|
||||
|
||||
<div class="add-content-button">
|
||||
<button [disabled]="available_files.length === 0" mat-stroked-button [matMenuTriggerFor]="menu">Add more content</button>
|
||||
</div>
|
||||
<mat-menu #menu="matMenu">
|
||||
<button *ngFor="let file of available_files" (click)="addContent(file)" mat-menu-item>{{file}}</button>
|
||||
</mat-menu>
|
||||
|
||||
</mat-dialog-content>
|
||||
|
||||
<mat-dialog-actions>
|
||||
<!-- Save -->
|
||||
<button [disabled]="!playlistChanged()" (click)="updatePlaylist()" [disabled]="playlistChanged()" mat-raised-button color="accent">Save</button>
|
||||
</mat-dialog-actions>
|
||||
@@ -1,50 +0,0 @@
|
||||
.media-list {
|
||||
|
||||
}
|
||||
|
||||
.media-box {
|
||||
|
||||
}
|
||||
|
||||
.cdk-drag-preview {
|
||||
box-sizing: border-box;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 5px 5px -3px rgba(0, 0, 0, 0.2),
|
||||
0 8px 10px 1px rgba(0, 0, 0, 0.14),
|
||||
0 3px 14px 2px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
.cdk-drag-placeholder {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.cdk-drag-animating {
|
||||
transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.media-box:last-child {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.media-list.cdk-drop-list-dragging .media-box:not(.cdk-drag-placeholder) {
|
||||
transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.add-content-button {
|
||||
margin-top: 15px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.remove-item-button {
|
||||
right: 10px;
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
}
|
||||
|
||||
.playlist-item-text {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 70%;
|
||||
margin: 0 auto;
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ModifyPlaylistComponent } from './modify-playlist.component';
|
||||
|
||||
describe('ModifyPlaylistComponent', () => {
|
||||
let component: ModifyPlaylistComponent;
|
||||
let fixture: ComponentFixture<ModifyPlaylistComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ ModifyPlaylistComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(ModifyPlaylistComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -1,83 +0,0 @@
|
||||
import { Component, OnInit, Inject } from '@angular/core';
|
||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||
import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop';
|
||||
import { PostsService } from 'app/posts.services';
|
||||
|
||||
@Component({
|
||||
selector: 'app-modify-playlist',
|
||||
templateUrl: './modify-playlist.component.html',
|
||||
styleUrls: ['./modify-playlist.component.scss']
|
||||
})
|
||||
export class ModifyPlaylistComponent implements OnInit {
|
||||
|
||||
original_playlist = null;
|
||||
playlist = null;
|
||||
available_files = [];
|
||||
all_files = [];
|
||||
playlist_updated = false;
|
||||
|
||||
constructor(@Inject(MAT_DIALOG_DATA) public data: any,
|
||||
private postsService: PostsService,
|
||||
public dialogRef: MatDialogRef<ModifyPlaylistComponent>) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
if (this.data) {
|
||||
this.playlist = JSON.parse(JSON.stringify(this.data.playlist));
|
||||
this.original_playlist = JSON.parse(JSON.stringify(this.data.playlist));
|
||||
this.getFiles();
|
||||
}
|
||||
}
|
||||
|
||||
getFiles() {
|
||||
if (this.playlist.type === 'audio') {
|
||||
this.postsService.getMp3s().subscribe(res => {
|
||||
this.processFiles(res['mp3s']);
|
||||
});
|
||||
} else {
|
||||
this.postsService.getMp4s().subscribe(res => {
|
||||
this.processFiles(res['mp4s']);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
processFiles(new_files = null) {
|
||||
if (new_files) { this.all_files = new_files.map(file => file.id); }
|
||||
this.available_files = this.all_files.filter(e => !this.playlist.fileNames.includes(e))
|
||||
}
|
||||
|
||||
updatePlaylist() {
|
||||
this.postsService.updatePlaylist(this.playlist).subscribe(res => {
|
||||
this.playlist_updated = true;
|
||||
this.postsService.openSnackBar('Playlist updated successfully.');
|
||||
this.getPlaylist();
|
||||
});
|
||||
}
|
||||
|
||||
playlistChanged() {
|
||||
return JSON.stringify(this.playlist) === JSON.stringify(this.original_playlist);
|
||||
}
|
||||
|
||||
getPlaylist() {
|
||||
this.postsService.getPlaylist(this.playlist.id, this.playlist.type, null).subscribe(res => {
|
||||
if (res['playlist']) {
|
||||
this.playlist = res['playlist'];
|
||||
this.original_playlist = JSON.parse(JSON.stringify(this.playlist));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
addContent(file) {
|
||||
this.playlist.fileNames.push(file);
|
||||
this.processFiles();
|
||||
}
|
||||
|
||||
removeContent(index) {
|
||||
this.playlist.fileNames.splice(index, 1);
|
||||
this.processFiles();
|
||||
}
|
||||
|
||||
drop(event: CdkDragDrop<string[]>) {
|
||||
moveItemInArray(this.playlist.fileNames, event.previousIndex, event.currentIndex);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -3,20 +3,16 @@
|
||||
<mat-dialog-content>
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-12 mb-4">
|
||||
<div class="col-12">
|
||||
<mat-form-field color="accent">
|
||||
<input [(ngModel)]="url" matInput placeholder="URL" i18n-placeholder="Subscription URL input placeholder" required aria-required="true">
|
||||
<mat-hint><ng-container i18n="Subscription URL input hint">The playlist or channel URL</ng-container></mat-hint>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<mat-divider></mat-divider>
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<mat-form-field color="accent">
|
||||
<input [(ngModel)]="name" matInput placeholder="Custom name" i18n-placeholder="Subscription custom name placeholder">
|
||||
<mat-hint><ng-container i18n="Custom name input hint">This is optional</ng-container></mat-hint>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="col-12 mt-3">
|
||||
@@ -24,46 +20,20 @@
|
||||
</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; margin-left: 10px;">
|
||||
<mat-form-field color="accent" style="width: 50px; text-align: center">
|
||||
<input type="number" matInput [(ngModel)]="timerange_amount">
|
||||
</mat-form-field>
|
||||
<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>
|
||||
<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>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<div>
|
||||
<mat-checkbox [(ngModel)]="audioOnlyMode"><ng-container i18n="Streaming-only mode">Audio-only mode</ng-container></mat-checkbox>
|
||||
<mat-checkbox [(ngModel)]="streamingOnlyMode"><ng-container i18n="Streaming-only mode">Streaming-only mode</ng-container></mat-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<div>
|
||||
<mat-checkbox [disabled]="audioOnlyMode" [(ngModel)]="streamingOnlyMode"><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)]="customArgs" 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)]="customFileOutput" 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>
|
||||
|
||||
@@ -6,8 +6,3 @@
|
||||
.mat-spinner {
|
||||
margin-left: 5%;
|
||||
}
|
||||
|
||||
.args-edit-button {
|
||||
position: absolute;
|
||||
margin-left: 10px;
|
||||
}
|
||||
@@ -1,8 +1,7 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { MatDialogRef, MatDialog } from '@angular/material/dialog';
|
||||
import { MatDialogRef } from '@angular/material/dialog';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { PostsService } from 'app/posts.services';
|
||||
import { ArgModifierDialogComponent } from '../arg-modifier-dialog/arg-modifier-dialog.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-subscribe-dialog',
|
||||
@@ -23,12 +22,6 @@ export class SubscribeDialogComponent implements OnInit {
|
||||
// no videos actually downloaded, just streamed
|
||||
streamingOnlyMode = false;
|
||||
|
||||
// audio only mode
|
||||
audioOnlyMode = false;
|
||||
|
||||
customFileOutput = '';
|
||||
customArgs = '';
|
||||
|
||||
time_units = [
|
||||
'day',
|
||||
'week',
|
||||
@@ -38,7 +31,6 @@ export class SubscribeDialogComponent implements OnInit {
|
||||
|
||||
constructor(private postsService: PostsService,
|
||||
private snackBar: MatSnackBar,
|
||||
private dialog: MatDialog,
|
||||
public dialogRef: MatDialogRef<SubscribeDialogComponent>) { }
|
||||
|
||||
ngOnInit() {
|
||||
@@ -57,8 +49,7 @@ export class SubscribeDialogComponent implements OnInit {
|
||||
if (!this.download_all) {
|
||||
timerange = 'now-' + this.timerange_amount.toString() + this.timerange_unit;
|
||||
}
|
||||
this.postsService.createSubscription(this.url, this.name, timerange, this.streamingOnlyMode,
|
||||
this.audioOnlyMode, this.customArgs, this.customFileOutput).subscribe(res => {
|
||||
this.postsService.createSubscription(this.url, this.name, timerange, this.streamingOnlyMode).subscribe(res => {
|
||||
this.subscribing = false;
|
||||
if (res['new_sub']) {
|
||||
this.dialogRef.close(res['new_sub']);
|
||||
@@ -72,20 +63,6 @@ export class SubscribeDialogComponent implements OnInit {
|
||||
}
|
||||
}
|
||||
|
||||
// modify custom args
|
||||
openArgsModifierDialog() {
|
||||
const dialogRef = this.dialog.open(ArgModifierDialogComponent, {
|
||||
data: {
|
||||
initial_args: this.customArgs
|
||||
}
|
||||
});
|
||||
dialogRef.afterClosed().subscribe(new_args => {
|
||||
if (new_args !== null && new_args !== undefined) {
|
||||
this.customArgs = new_args;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public openSnackBar(message: string, action = '') {
|
||||
this.snackBar.open(message, action, {
|
||||
duration: 2000,
|
||||
|
||||
@@ -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 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-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-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>
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
<div style="padding:5px">
|
||||
<div style="height: 52px;">
|
||||
<div>
|
||||
<b><a class="file-link" href="javascript:void(0)" (click)="!playlist ? mainComponent.goToFile(name, isAudio, uid) : mainComponent.goToPlaylist(name, type)">{{title}}</a></b>
|
||||
<b><a class="file-link" href="javascript:void(0)" (click)="!isPlaylist ? mainComponent.goToFile(name, isAudio, uid) : mainComponent.goToPlaylist(name, type)">{{title}}</a></b>
|
||||
</div>
|
||||
<span class="max-two-lines"><ng-container i18n="File or playlist ID">ID:</ng-container> {{name}}</span>
|
||||
<div *ngIf="playlist"><ng-container i18n="Playlist video count">Count:</ng-container> {{count}}</div>
|
||||
<div *ngIf="isPlaylist"><ng-container i18n="Playlist video count">Count:</ng-container> {{count}}</div>
|
||||
</div>
|
||||
<div *ngIf="!image_errored && thumbnailURL" class="img-div">
|
||||
<img class="image" (error) ="onImgError($event)" [id]="type" [lazyLoad]="thumbnailURL" [customObservable]="scrollAndLoad" (onLoad)="imageLoaded($event)" alt="Thumbnail">
|
||||
@@ -14,16 +14,11 @@
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<button [matMenuTriggerFor]="playlist_menu" *ngIf="playlist" class="deleteButton" mat-icon-button><mat-icon>more_vert</mat-icon></button>
|
||||
<mat-menu #playlist_menu="matMenu">
|
||||
<button (click)="editPlaylistDialog()" mat-menu-item><mat-icon>edit</mat-icon><ng-container i18n="Playlist edit button">Edit</ng-container></button>
|
||||
<button (click)="deleteFile()" mat-menu-item><mat-icon>delete_forever</mat-icon><ng-container i18n="Delete playlist">Delete</ng-container></button>
|
||||
</mat-menu>
|
||||
<button [matMenuTriggerFor]="action_menu" *ngIf="!playlist" class="deleteButton" mat-icon-button><mat-icon>more_vert</mat-icon></button>
|
||||
<button *ngIf="isPlaylist" (click)="deleteFile()" class="deleteButton" mat-icon-button><mat-icon>delete_forever</mat-icon></button>
|
||||
<button [matMenuTriggerFor]="action_menu" *ngIf="!isPlaylist" class="deleteButton" mat-icon-button><mat-icon>more_vert</mat-icon></button>
|
||||
<mat-menu #action_menu="matMenu">
|
||||
<button (click)="openVideoInfoDialog()" mat-menu-item><mat-icon>info</mat-icon><ng-container i18n="Video info button">Info</ng-container></button>
|
||||
<button (click)="deleteFile()" mat-menu-item><mat-icon>delete</mat-icon><ng-container i18n="Delete video button">Delete</ng-container></button>
|
||||
<button *ngIf="use_youtubedl_archive" (click)="deleteFile(true)" mat-menu-item><mat-icon>delete_forever</mat-icon><ng-container i18n="Delete and blacklist video button">Delete and blacklist</ng-container></button>
|
||||
</mat-menu>
|
||||
|
||||
</mat-card>
|
||||
|
||||
@@ -7,7 +7,6 @@ import { Subject, Observable } from 'rxjs';
|
||||
import 'rxjs/add/observable/merge';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { VideoInfoDialogComponent } from 'app/dialogs/video-info-dialog/video-info-dialog.component';
|
||||
import { ModifyPlaylistComponent } from '../dialogs/modify-playlist/modify-playlist.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-file-card',
|
||||
@@ -23,7 +22,7 @@ export class FileCardComponent implements OnInit {
|
||||
@Input() thumbnailURL: string;
|
||||
@Input() isAudio = true;
|
||||
@Output() removeFile: EventEmitter<string> = new EventEmitter<string>();
|
||||
@Input() playlist = null;
|
||||
@Input() isPlaylist = false;
|
||||
@Input() count = null;
|
||||
@Input() use_youtubedl_archive = false;
|
||||
type;
|
||||
@@ -47,15 +46,15 @@ export class FileCardComponent implements OnInit {
|
||||
this.type = this.isAudio ? 'audio' : 'video';
|
||||
|
||||
if (this.file && this.file.url && this.file.url.includes('youtu')) {
|
||||
const string_id = (this.playlist ? '?list=' : '?v=')
|
||||
const index_offset = (this.playlist ? 6 : 3);
|
||||
const string_id = (this.isPlaylist ? '?list=' : '?v=')
|
||||
const index_offset = (this.isPlaylist ? 6 : 3);
|
||||
const end_index = this.file.url.indexOf(string_id) + index_offset;
|
||||
this.name = this.file.url.substring(end_index, this.file.url.length);
|
||||
}
|
||||
}
|
||||
|
||||
deleteFile(blacklistMode = false) {
|
||||
if (!this.playlist) {
|
||||
if (!this.isPlaylist) {
|
||||
this.postsService.deleteFile(this.uid, this.isAudio, blacklistMode).subscribe(result => {
|
||||
if (result) {
|
||||
this.openSnackBar('Delete success!', 'OK.');
|
||||
@@ -81,24 +80,6 @@ export class FileCardComponent implements OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
editPlaylistDialog() {
|
||||
const dialogRef = this.dialog.open(ModifyPlaylistComponent, {
|
||||
data: {
|
||||
playlist: this.playlist,
|
||||
width: '65vw'
|
||||
}
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe(res => {
|
||||
// updates playlist in file manager if it changed
|
||||
if (dialogRef.componentInstance.playlist_updated) {
|
||||
this.playlist = dialogRef.componentInstance.original_playlist;
|
||||
this.title = this.playlist.name;
|
||||
this.count = this.playlist.fileNames.length;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onImgError(event) {
|
||||
this.image_errored = true;
|
||||
}
|
||||
|
||||
@@ -1,14 +1,20 @@
|
||||
<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-content style="padding: 0px 8px 0px 8px;">
|
||||
<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;">
|
||||
<form class="example-form">
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div [ngClass]="allowQualitySelect ? 'col-sm-9' : null" class="col-12">
|
||||
<mat-form-field color="accent" class="example-full-width">
|
||||
<input style="padding-right: 25px;" matInput (keyup.enter)="downloadClicked()" (ngModelChange)="inputChanged($event)" [(ngModel)]="url" [placeholder]="'URL' + (youtubeSearchEnabled ? ' or search' : '')" type="url" name="url" #urlinput>
|
||||
<input style="padding-right: 25px;" matInput (keyup.enter)="downloadClicked()" (ngModelChange)="inputChanged($event)" [(ngModel)]="url" [placeholder]="'URL' + (youtubeSearchEnabled ? ' or search' : '')" type="url" name="url" [formControl]="urlForm" #urlinput>
|
||||
<mat-error *ngIf="urlError || urlForm.invalid">
|
||||
<ng-container i18n="Enter valid URL error">Please enter a valid URL!</ng-container>
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
<button type="button" class="input-clear-button" mat-icon-button (click)="clearInput()"><mat-icon>clear</mat-icon></button>
|
||||
</div>
|
||||
@@ -21,7 +27,7 @@
|
||||
</mat-label>
|
||||
<mat-select [ngModelOptions]="{standalone: true}" [(ngModel)]="selectedQuality">
|
||||
<ng-container *ngFor="let option of qualityOptions[(audioOnly) ? 'audio' : 'video']">
|
||||
<mat-option *ngIf="option.value === '' || url && cachedAvailableFormats[url] && cachedAvailableFormats[url]['formats'] && cachedAvailableFormats[url]['formats'][(audioOnly) ? 'audio' : 'video'][option.value]" [value]="option.value">
|
||||
<mat-option *ngIf="option.value === '' || url && cachedAvailableFormats[url] && cachedAvailableFormats[url]['formats'] && cachedAvailableFormats[url]['formats'][(audioOnly) ? 'audio' : 'video'][option.note]" [value]="option.note">
|
||||
{{option.label}}
|
||||
</mat-option>
|
||||
</ng-container>
|
||||
@@ -180,13 +186,7 @@
|
||||
<ng-template #nofile>
|
||||
|
||||
</ng-template>
|
||||
|
||||
<app-recent-videos></app-recent-videos>
|
||||
<br/>
|
||||
<h4 style="text-align: center">Custom playlists</h4>
|
||||
<app-custom-playlists></app-custom-playlists>
|
||||
|
||||
<!--<div style="margin: 20px" *ngIf="fileManagerEnabled && (!postsService.isLoggedIn || postsService.permissions.includes('filemanager'))">
|
||||
<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>
|
||||
@@ -216,7 +216,7 @@
|
||||
<mat-grid-list *ngIf="playlists.audio.length > 0" (window:resize)="onResize($event)" [cols]="files_cols" rowHeight="150px">
|
||||
<mat-grid-tile *ngFor="let playlist of playlists.audio; let i = index;">
|
||||
<app-file-card #audiofilecard (removeFile)="removePlaylistMp3(playlist.id, i)" [title]="playlist.name" [name]="playlist.id" [thumbnailURL]="playlist_thumbnails[playlist.id]"
|
||||
[length]="null" [isAudio]="true" [playlist]="playlist" [count]="playlist.fileNames.length" [use_youtubedl_archive]="use_youtubedl_archive"></app-file-card>
|
||||
[length]="null" [isAudio]="true" [isPlaylist]="true" [count]="playlist.fileNames.length" [use_youtubedl_archive]="use_youtubedl_archive"></app-file-card>
|
||||
<mat-progress-bar *ngIf="downloading_content['audio'][playlist.id]" class="download-progress-bar" mode="indeterminate"></mat-progress-bar>
|
||||
</mat-grid-tile>
|
||||
</mat-grid-list>
|
||||
@@ -258,12 +258,12 @@
|
||||
<mat-grid-list *ngIf="playlists.video.length > 0" (window:resize)="onResize($event)" [cols]="files_cols" rowHeight="150px">
|
||||
<mat-grid-tile *ngFor="let playlist of playlists.video; let i = index;">
|
||||
<app-file-card #videofilecard (removeFile)="removePlaylistMp4(playlist.id, i)" [title]="playlist.name" [name]="playlist.id" [thumbnailURL]="playlist_thumbnails[playlist.id]"
|
||||
[length]="null" [isAudio]="false" [playlist]="playlist" [count]="playlist.fileNames.length" [use_youtubedl_archive]="use_youtubedl_archive"></app-file-card>
|
||||
[length]="null" [isAudio]="false" [isPlaylist]="true" [count]="playlist.fileNames.length" [use_youtubedl_archive]="use_youtubedl_archive"></app-file-card>
|
||||
<mat-progress-bar *ngIf="downloading_content['video'][playlist.id]" class="download-progress-bar" mode="indeterminate"></mat-progress-bar>
|
||||
</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">
|
||||
@@ -273,4 +273,12 @@
|
||||
</div>
|
||||
</mat-expansion-panel>
|
||||
</mat-accordion>
|
||||
</div>-->
|
||||
</div>
|
||||
|
||||
<ng-template #nomp3s>
|
||||
|
||||
</ng-template>
|
||||
|
||||
<ng-template #nomp4s>
|
||||
|
||||
</ng-template>
|
||||
@@ -108,47 +108,56 @@ export class MainComponent implements OnInit {
|
||||
{
|
||||
'resolution': null,
|
||||
'value': '',
|
||||
'label': 'Max'
|
||||
'label': 'Max',
|
||||
'note': ''
|
||||
},
|
||||
{
|
||||
'resolution': '3840x2160',
|
||||
'value': '2160',
|
||||
'label': '2160p (4K)'
|
||||
'label': '2160p (4K)',
|
||||
'note': '2160p'
|
||||
},
|
||||
{
|
||||
'resolution': '2560x1440',
|
||||
'value': '1440',
|
||||
'label': '1440p'
|
||||
'label': '1440p',
|
||||
'note': '1440p'
|
||||
},
|
||||
{
|
||||
'resolution': '1920x1080',
|
||||
'value': '1080',
|
||||
'label': '1080p'
|
||||
'label': '1080p',
|
||||
'note': '1080p'
|
||||
},
|
||||
{
|
||||
'resolution': '1280x720',
|
||||
'value': '720',
|
||||
'label': '720p'
|
||||
'label': '720p',
|
||||
'note': '720p'
|
||||
},
|
||||
{
|
||||
'resolution': '720x480',
|
||||
'value': '480',
|
||||
'label': '480p'
|
||||
'label': '480p',
|
||||
'note': '480p'
|
||||
},
|
||||
{
|
||||
'resolution': '480x360',
|
||||
'value': '360',
|
||||
'label': '360p'
|
||||
'label': '360p',
|
||||
'note': '360p'
|
||||
},
|
||||
{
|
||||
'resolution': '360x240',
|
||||
'value': '240',
|
||||
'label': '240p'
|
||||
'label': '240p',
|
||||
'note': '240p'
|
||||
},
|
||||
{
|
||||
'resolution': '256x144',
|
||||
'value': '144',
|
||||
'label': '144p'
|
||||
'label': '144p',
|
||||
'note': '144p'
|
||||
}
|
||||
],
|
||||
'audio': [
|
||||
@@ -439,11 +448,10 @@ export class MainComponent implements OnInit {
|
||||
|
||||
public removeFromMp3(name: string) {
|
||||
for (let i = 0; i < this.mp3s.length; i++) {
|
||||
if (this.mp3s[i].id === name || this.mp3s[i].id + '.mp3' === name) {
|
||||
if (this.mp3s[i].id === name) {
|
||||
this.mp3s.splice(i, 1);
|
||||
}
|
||||
}
|
||||
this.getMp3s();
|
||||
}
|
||||
|
||||
public removePlaylistMp3(playlistID, index) {
|
||||
@@ -458,11 +466,10 @@ export class MainComponent implements OnInit {
|
||||
|
||||
public removeFromMp4(name: string) {
|
||||
for (let i = 0; i < this.mp4s.length; i++) {
|
||||
if (this.mp4s[i].id === name || this.mp4s[i].id + '.mp4' === name) {
|
||||
if (this.mp4s[i].id === name) {
|
||||
this.mp4s.splice(i, 1);
|
||||
}
|
||||
}
|
||||
this.getMp4s();
|
||||
}
|
||||
|
||||
public removePlaylistMp4(playlistID, index) {
|
||||
@@ -1034,14 +1041,16 @@ 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') {
|
||||
if (true) {
|
||||
format_obj['height'] = format.height;
|
||||
format_obj['acodec'] = format.acodec;
|
||||
format_obj['format_id'] = format.format_id;
|
||||
format_obj['ext'] = format.ext;
|
||||
format_obj['note'] = format.format_note;
|
||||
|
||||
// no acodec means no overwrite
|
||||
if (!(video_formats[key]) || format_obj['acodec'] !== 'none') {
|
||||
video_formats[key] = format_obj;
|
||||
video_formats[format_obj['note']] = format_obj;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
|
||||
.video-styles {
|
||||
width: 100%;
|
||||
height: 75vh;
|
||||
}
|
||||
|
||||
::ng-deep .mat-button-toggle-label-content {
|
||||
@@ -66,13 +65,6 @@
|
||||
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'">
|
||||
<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'">
|
||||
<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'">
|
||||
<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 style="height: fit-content; width: 100%; margin-top: 10px;">
|
||||
<div class="col-12 my-2">
|
||||
<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, OnDestroy } from '@angular/core';
|
||||
import { Component, OnInit, HostListener, EventEmitter } from '@angular/core';
|
||||
import { VgAPI } from 'ngx-videogular';
|
||||
import { PostsService } from 'app/posts.services';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
@@ -20,7 +20,7 @@ export interface IMedia {
|
||||
templateUrl: './player.component.html',
|
||||
styleUrls: ['./player.component.css']
|
||||
})
|
||||
export class PlayerComponent implements OnInit, OnDestroy {
|
||||
export class PlayerComponent implements OnInit {
|
||||
|
||||
playlist: Array<IMedia> = [];
|
||||
original_playlist: string = null;
|
||||
@@ -62,9 +62,6 @@ export class PlayerComponent implements OnInit, OnDestroy {
|
||||
|
||||
downloading = false;
|
||||
|
||||
save_volume_timer = null;
|
||||
original_volume = null;
|
||||
|
||||
@HostListener('window:resize', ['$event'])
|
||||
onResize(event) {
|
||||
this.innerWidth = window.innerWidth;
|
||||
@@ -95,11 +92,6 @@ export class PlayerComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
|
||||
@@ -135,7 +127,7 @@ export class PlayerComponent implements OnInit, OnDestroy {
|
||||
this.currentItem = this.playlist[0];
|
||||
this.currentIndex = 0;
|
||||
this.show_player = true;
|
||||
} else if (this.subscriptionName || this.fileNames) {
|
||||
} else if (this.type === 'subscription' || this.fileNames) {
|
||||
this.show_player = true;
|
||||
this.parseFileNames();
|
||||
}
|
||||
@@ -189,6 +181,9 @@ export class PlayerComponent implements OnInit, OnDestroy {
|
||||
fileType = 'audio/mp3';
|
||||
} else if (this.type === 'video') {
|
||||
fileType = 'video/mp4';
|
||||
} else if (this.type === 'subscription') {
|
||||
// only supports mp4 for now
|
||||
fileType = 'video/mp4';
|
||||
} else {
|
||||
// error
|
||||
console.error('Must have valid file type! Use \'audio\', \'video\', or \'subscription\'.');
|
||||
@@ -203,7 +198,7 @@ export class PlayerComponent implements OnInit, OnDestroy {
|
||||
fullLocation = this.baseStreamPath + baseLocation + encodeURIComponent(fileName);
|
||||
} else {
|
||||
// default to video but include subscription name param
|
||||
baseLocation = this.type === 'audio' ? 'audio/' : 'video/';
|
||||
baseLocation = 'video/';
|
||||
fullLocation = this.baseStreamPath + baseLocation + encodeURIComponent(fileName) + '?subName=' + this.subscriptionName +
|
||||
'&subPlaylist=' + this.subPlaylist;
|
||||
}
|
||||
@@ -243,13 +238,6 @@ export class PlayerComponent implements OnInit, OnDestroy {
|
||||
onPlayerReady(api: VgAPI) {
|
||||
this.api = api;
|
||||
|
||||
// checks if volume has been previously set. if so, use that as default
|
||||
if (localStorage.getItem('player_volume')) {
|
||||
this.api.volume = parseFloat(localStorage.getItem('player_volume'));
|
||||
}
|
||||
|
||||
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));
|
||||
|
||||
@@ -258,13 +246,6 @@ export class PlayerComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
saveVolume(api) {
|
||||
if (this.original_volume !== api.volume) {
|
||||
localStorage.setItem('player_volume', api.volume)
|
||||
this.original_volume = api.volume;
|
||||
}
|
||||
}
|
||||
|
||||
nextVideo() {
|
||||
if (this.currentIndex === this.playlist.length - 1) {
|
||||
// dont continue playing
|
||||
@@ -330,7 +311,7 @@ export class PlayerComponent implements OnInit, OnDestroy {
|
||||
this.postsService.downloadFileFromServer(filename, this.type, null, null, this.subscriptionName, this.subPlaylist,
|
||||
this.is_shared ? this.db_file['uid'] : null, this.uuid).subscribe(res => {
|
||||
this.downloading = false;
|
||||
const blob = new Blob([res], {type: this.type === 'audio' ? 'audio/mp3' : 'video/mp4'})
|
||||
const blob: Blob = res;
|
||||
saveAs(blob, filename + ext);
|
||||
}, err => {
|
||||
console.log(err);
|
||||
@@ -396,7 +377,7 @@ export class PlayerComponent implements OnInit, OnDestroy {
|
||||
updatePlaylist() {
|
||||
const fileNames = this.getFileNames();
|
||||
this.playlist_updating = true;
|
||||
this.postsService.updatePlaylistFiles(this.id, fileNames, this.type).subscribe(res => {
|
||||
this.postsService.updatePlaylist(this.id, fileNames, this.type).subscribe(res => {
|
||||
this.playlist_updating = false;
|
||||
if (res['success']) {
|
||||
const fileNamesEncoded = fileNames.join('|nvr|');
|
||||
|
||||
@@ -22,8 +22,6 @@ export class PostsService implements CanActivate {
|
||||
handShakeComplete = false;
|
||||
THEMES_CONFIG = THEMES_CONFIG;
|
||||
theme;
|
||||
card_size = 'medium';
|
||||
sidepanel_mode = 'over';
|
||||
settings_changed = new BehaviorSubject<boolean>(false);
|
||||
auth_token = '4241b401-7236-493e-92b5-b72696b9d853';
|
||||
session_id = null;
|
||||
@@ -49,7 +47,6 @@ export class PostsService implements CanActivate {
|
||||
open_create_default_admin_dialog = new BehaviorSubject<boolean>(false);
|
||||
|
||||
config = null;
|
||||
subscriptions = null;
|
||||
constructor(private http: HttpClient, private router: Router, @Inject(DOCUMENT) private document: Document,
|
||||
public snackBar: MatSnackBar) {
|
||||
console.log('PostsService Initialized...');
|
||||
@@ -76,22 +73,17 @@ export class PostsService implements CanActivate {
|
||||
this.httpOptions.params = this.httpOptions.params.set('sessionID', this.session_id);
|
||||
});
|
||||
|
||||
const redirect_not_required = window.location.href.includes('/player') || window.location.href.includes('/login');
|
||||
|
||||
// get config
|
||||
this.loadNavItems().subscribe(res => {
|
||||
const result = !this.debugMode ? res['config_file'] : res;
|
||||
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') {
|
||||
if (localStorage.getItem('jwt_token')) {
|
||||
this.token = localStorage.getItem('jwt_token');
|
||||
this.httpOptions.params = this.httpOptions.params.set('jwt', this.token);
|
||||
this.jwtAuth();
|
||||
} else if (redirect_not_required) {
|
||||
this.setInitialized();
|
||||
} else {
|
||||
this.sendToLogin();
|
||||
}
|
||||
@@ -104,14 +96,6 @@ 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 => {
|
||||
@@ -125,15 +109,6 @@ 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');
|
||||
}
|
||||
@@ -184,8 +159,12 @@ export class PostsService implements CanActivate {
|
||||
ui_uid: ui_uid}, this.httpOptions);
|
||||
}
|
||||
|
||||
killAllDownloads() {
|
||||
return this.http.post(this.path + 'killAllDownloads', {}, 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);
|
||||
}
|
||||
|
||||
loadNavItems() {
|
||||
@@ -224,10 +203,6 @@ 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,
|
||||
@@ -255,12 +230,16 @@ export class PostsService implements CanActivate {
|
||||
return this.http.post(this.path + 'getVideoInfos', {fileNames: fileNames, type: type, urlMode: urlMode}, this.httpOptions);
|
||||
}
|
||||
|
||||
getLogs(lines = 50) {
|
||||
return this.http.post(this.path + 'logs', {lines: lines}, this.httpOptions);
|
||||
isPinSet() {
|
||||
return this.http.post(this.path + 'isPinSet', {}, this.httpOptions);
|
||||
}
|
||||
|
||||
clearAllLogs() {
|
||||
return this.http.post(this.path + 'clearAllLogs', {}, 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);
|
||||
}
|
||||
|
||||
generateNewAPIKey() {
|
||||
@@ -275,12 +254,11 @@ 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, duration = null) {
|
||||
createPlaylist(playlistName, fileNames, type, thumbnailURL) {
|
||||
return this.http.post(this.path + 'createPlaylist', {playlistName: playlistName,
|
||||
fileNames: fileNames,
|
||||
type: type,
|
||||
thumbnailURL: thumbnailURL,
|
||||
duration: duration}, this.httpOptions);
|
||||
thumbnailURL: thumbnailURL}, this.httpOptions);
|
||||
}
|
||||
|
||||
getPlaylist(playlistID, type, uuid = null) {
|
||||
@@ -288,12 +266,8 @@ export class PostsService implements CanActivate {
|
||||
type: type, uuid: uuid}, this.httpOptions);
|
||||
}
|
||||
|
||||
updatePlaylist(playlist) {
|
||||
return this.http.post(this.path + 'updatePlaylist', {playlist: playlist}, this.httpOptions);
|
||||
}
|
||||
|
||||
updatePlaylistFiles(playlistID, fileNames, type) {
|
||||
return this.http.post(this.path + 'updatePlaylistFiles', {playlistID: playlistID,
|
||||
updatePlaylist(playlistID, fileNames, type) {
|
||||
return this.http.post(this.path + 'updatePlaylist', {playlistID: playlistID,
|
||||
fileNames: fileNames,
|
||||
type: type}, this.httpOptions);
|
||||
}
|
||||
@@ -302,22 +276,17 @@ export class PostsService implements CanActivate {
|
||||
return this.http.post(this.path + 'deletePlaylist', {playlistID: playlistID, type: type}, this.httpOptions);
|
||||
}
|
||||
|
||||
createSubscription(url, name, timerange = null, streamingOnly = false, audioOnly = false, customArgs = null, customFileOutput = null) {
|
||||
return this.http.post(this.path + 'subscribe', {url: url, name: name, timerange: timerange, streamingOnly: streamingOnly,
|
||||
audioOnly: audioOnly, customArgs: customArgs, customFileOutput: customFileOutput}, this.httpOptions);
|
||||
}
|
||||
|
||||
updateSubscription(subscription) {
|
||||
return this.http.post(this.path + 'updateSubscription', {subscription: subscription}, this.httpOptions);
|
||||
createSubscription(url, name, timerange = null, streamingOnly = false) {
|
||||
return this.http.post(this.path + 'subscribe', {url: url, name: name, timerange: timerange, streamingOnly: streamingOnly},
|
||||
this.httpOptions);
|
||||
}
|
||||
|
||||
unsubscribe(sub, deleteMode = false) {
|
||||
return this.http.post(this.path + 'unsubscribe', {sub: sub, deleteMode: deleteMode}, this.httpOptions)
|
||||
}
|
||||
|
||||
deleteSubscriptionFile(sub, file, deleteForever, file_uid) {
|
||||
return this.http.post(this.path + 'deleteSubscriptionFile', {sub: sub, file: file, deleteForever: deleteForever,
|
||||
file_uid: file_uid}, this.httpOptions)
|
||||
deleteSubscriptionFile(sub, file, deleteForever) {
|
||||
return this.http.post(this.path + 'deleteSubscriptionFile', {sub: sub, file: file, deleteForever: deleteForever}, this.httpOptions)
|
||||
}
|
||||
|
||||
getSubscription(id) {
|
||||
@@ -408,7 +377,6 @@ export class PostsService implements CanActivate {
|
||||
this.user = null;
|
||||
this.permissions = null;
|
||||
this.isLoggedIn = false;
|
||||
this.token = null;
|
||||
localStorage.setItem('jwt_token', null);
|
||||
if (this.router.url !== '/login') {
|
||||
this.router.navigate(['/login']);
|
||||
@@ -429,13 +397,17 @@ export class PostsService implements CanActivate {
|
||||
const call = this.http.post(this.path + 'auth/register', {userid: username,
|
||||
username: username,
|
||||
password: password}, this.httpOptions);
|
||||
/*call.subscribe(res => {
|
||||
console.log(res['user']);
|
||||
if (res['user']) {
|
||||
// this.afterRegistration(res['user']);
|
||||
}
|
||||
});*/
|
||||
return call;
|
||||
}
|
||||
|
||||
sendToLogin() {
|
||||
if (!this.initialized) {
|
||||
this.setInitialized();
|
||||
}
|
||||
this.checkAdminCreationStatus();
|
||||
if (this.router.url === '/login') {
|
||||
return;
|
||||
}
|
||||
@@ -462,8 +434,8 @@ export class PostsService implements CanActivate {
|
||||
password: password}, this.httpOptions);
|
||||
}
|
||||
|
||||
checkAdminCreationStatus(force_show = false) {
|
||||
if (!force_show && !this.config['Advanced']['multi_user_mode']) {
|
||||
checkAdminCreationStatus(skip_check = false) {
|
||||
if (!skip_check && !this.config['Advanced']['multi_user_mode']) {
|
||||
return;
|
||||
}
|
||||
this.adminExists().subscribe(res => {
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
<!-- <ng-container i18n="Allow subscriptions setting"></ng-container> -->
|
||||
<mat-dialog-content>
|
||||
|
||||
<!-- Language
|
||||
<!-- Language
|
||||
<div style="margin-bottom: 10px;">
|
||||
|
||||
|
||||
</div> -->
|
||||
|
||||
<mat-tab-group>
|
||||
@@ -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 mt-3">
|
||||
<div class="col-12 mb-4">
|
||||
<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>
|
||||
@@ -47,13 +47,13 @@
|
||||
<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">
|
||||
@@ -79,6 +79,11 @@
|
||||
<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>
|
||||
@@ -127,7 +132,7 @@
|
||||
<mat-hint><ng-container i18n="Aduio path setting input hint">Path for audio only downloads. It is relative to YTDL-Material's root folder.</ng-container></mat-hint>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="col-12 mt-5">
|
||||
<mat-form-field class="text-field" color="accent">
|
||||
<input matInput [(ngModel)]="new_config['Downloader']['path-video']" placeholder="Video folder path" i18n-placeholder="Video folder path input placeholder" required>
|
||||
@@ -135,7 +140,7 @@
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div class="col-12 mt-4">
|
||||
<div class="col-12 mt-5">
|
||||
<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>
|
||||
@@ -143,16 +148,9 @@
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div class="col-12 mt-5">
|
||||
<div class="col-12 mt-4">
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<div class="col-12 mt-2">
|
||||
<mat-checkbox color="accent" [(ngModel)]="new_config['Downloader']['safe_download_override']"><ng-container i18n="Safe download override setting">Safe download override</ng-container></mat-checkbox>
|
||||
</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>
|
||||
<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>
|
||||
</div>
|
||||
@@ -184,9 +182,13 @@
|
||||
<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>
|
||||
<mat-divider></mat-divider>
|
||||
<div *ngIf="new_config" class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-12 mt-3">
|
||||
@@ -200,7 +202,7 @@
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="api-key-div">
|
||||
<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>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -269,7 +271,7 @@
|
||||
</div>
|
||||
<div class="col-12 mt-2 mb-1">
|
||||
<mat-form-field>
|
||||
<mat-label><ng-container i18n="Logger level select label">Select a logger level</ng-container></mat-label>
|
||||
<mat-label><ng-container i18n="Logger level select label">Select a downloader</ng-container></mat-label>
|
||||
<mat-select color="accent" [(ngModel)]="new_config['Advanced']['logger_level']">
|
||||
<mat-option value="debug">Debug</mat-option>
|
||||
<mat-option value="verbose">Verbose</mat-option>
|
||||
@@ -279,18 +281,6 @@
|
||||
</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>
|
||||
@@ -317,14 +307,7 @@
|
||||
</div>
|
||||
<app-modify-users></app-modify-users>
|
||||
</mat-tab>
|
||||
<mat-tab *ngIf="postsService.config" label="Logs" i18n-label="Logs settings label">
|
||||
<ng-template matTabContent>
|
||||
<div style="margin-left: 48px; margin-top: 24px; height: 340px">
|
||||
<app-logs-viewer></app-logs-viewer>
|
||||
</div>
|
||||
</ng-template>
|
||||
</mat-tab>
|
||||
</mat-tab-group>
|
||||
</mat-tab-group>
|
||||
</mat-dialog-content>
|
||||
|
||||
<mat-dialog-actions>
|
||||
@@ -336,4 +319,4 @@
|
||||
<span i18n="Settings cancel and close button">{settingsAreTheSame + "", select, true {Close} false {Cancel} other {otha}}</span>
|
||||
</button>
|
||||
</div>
|
||||
</mat-dialog-actions>
|
||||
</mat-dialog-actions>
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Component, OnInit, EventEmitter } from '@angular/core';
|
||||
import { Component, OnInit } 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';
|
||||
@@ -8,7 +9,6 @@ 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',
|
||||
@@ -17,7 +17,7 @@ import { ConfirmDialogComponent } from 'app/dialogs/confirm-dialog/confirm-dialo
|
||||
})
|
||||
export class SettingsComponent implements OnInit {
|
||||
all_locales = isoLangs;
|
||||
supported_locales = ['en', 'es', 'de'];
|
||||
supported_locales = ['en', 'es'];
|
||||
initialLocale = localStorage.getItem('locale');
|
||||
|
||||
initial_config = null;
|
||||
@@ -77,6 +77,14 @@ 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']) {
|
||||
@@ -155,34 +163,6 @@ 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, {
|
||||
|
||||
@@ -71,14 +71,14 @@ export class SubscriptionFileCardComponent implements OnInit {
|
||||
}
|
||||
|
||||
deleteAndRedownload() {
|
||||
this.postsService.deleteSubscriptionFile(this.sub, this.file.id, false, this.file.uid).subscribe(res => {
|
||||
this.postsService.deleteSubscriptionFile(this.sub, this.file.id, false).subscribe(res => {
|
||||
this.reloadSubscription.emit(true);
|
||||
this.openSnackBar(`Successfully deleted file: '${this.file.id}'`, 'Dismiss.');
|
||||
});
|
||||
}
|
||||
|
||||
deleteForever() {
|
||||
this.postsService.deleteSubscriptionFile(this.sub, this.file.id, true, this.file.uid).subscribe(res => {
|
||||
this.postsService.deleteSubscriptionFile(this.sub, this.file.id, true).subscribe(res => {
|
||||
this.reloadSubscription.emit(true);
|
||||
this.openSnackBar(`Successfully deleted file: '${this.file.id}'`, 'Dismiss.');
|
||||
});
|
||||
@@ -108,4 +108,4 @@ function fancyTimeFormat(time) {
|
||||
ret += '' + mins + ':' + (secs < 10 ? '0' : '');
|
||||
ret += '' + secs;
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user