Compare commits

...

68 Commits

Author SHA1 Message Date
Isaac Grynsztein
a9f197e46d Updated logs viewer component
- now by default last 50 lines are showed
- added copy to clipboard button
- added loading spinner to indicate to users when the logs are loading

app.get('/api/logs') is now app.post to allow for additional parameters (such as lines to retrieve)
2020-07-03 03:46:58 -04:00
Isaac Grynsztein
3732d13562 Implemented greater transparency for login/registration errors on frontend 2020-07-02 17:42:05 -04:00
Isaac Grynsztein
cf14880d21 Empty URL setting will result in the default being applied 2020-07-01 23:20:11 -04:00
Isaac Grynsztein
e81d0cab42 Fixed bug where changing a user's password would change the admin's password 2020-07-01 17:26:32 -04:00
Isaac Grynsztein
7e24180f03 Fixed bug in globalArgsRequiresSafeDownload function 2020-06-30 23:03:07 -04:00
Isaac Grynsztein
053c8db9dd Fixed bug where config api would call itself 2020-06-30 22:55:08 -04:00
Isaac Grynsztein
5537852134 Deleting a file will now delete its downloaded thumbnail as well
Thumbnails will now have their permissions auto updated to align themselves with the other downloaded files
2020-06-30 22:38:01 -04:00
Isaac Grynsztein
efdc471ccf Fixed bug where if multi-user mode was enabled, old subscriptions would keep downloading and vice versa 2020-06-29 23:23:31 -04:00
Tzahi12345
6f1b37d5eb Merge pull request #149 from Tzahi12345/player-improvements
Playlist and player improvements
2020-06-29 20:30:31 -04:00
Isaac Grynsztein
06557673a2 Updated frontend binaries 2020-06-29 20:20:07 -04:00
Isaac Grynsztein
c20d09e902 Merge branch 'master' of https://github.com/Tzahi12345/YoutubeDL-Material into player-improvements 2020-06-29 20:11:20 -04:00
Isaac Grynsztein
a68ecfa730 Modifying playlist in dialog will now update the file manager automatically 2020-06-29 20:06:37 -04:00
Isaac Grynsztein
86c609c1b2 Player component now remembers previously set volume
Updated name of updatePlaylist->updatePlaylistFiles for clarity and added updatePlaylist route

Added smarter safe download override, will auto activate if subtitle args are included.
2020-06-29 19:39:47 -04:00
Isaac Grynsztein
d100e80ccf Added ability to clear all downloads in a session 2020-06-29 19:35:59 -04:00
Isaac Grynsztein
5511c94071 Added modify playlist component 2020-06-29 19:35:34 -04:00
Isaac Grynsztein
b21886d8f8 Rebuilt frontend binaries and included Deutsch translation in public directory 2020-06-29 18:45:40 -04:00
Tzahi12345
e535603103 Merge pull request #145 from UnlimitedCookies/master
Add German Translation
2020-06-29 18:21:06 -04:00
UnlimitedCookies
9415901f17 Revert to 1:1 translation 2020-06-29 18:58:46 +02:00
UnlimitedCookies
92e5716f93 More clarification 2020-06-29 17:03:07 +02:00
UnlimitedCookies
5b5c93f783 Minor changes to increase coherence 2020-06-29 16:42:05 +02:00
UnlimitedCookies
4db6a49df5 Make custom Arg description more clear
Co-authored-by: Sandro <sandro.jaeckel@gmail.com>
2020-06-29 16:12:51 +02:00
Isaac Grynsztein
3abb29eee4 Removed exe binaries from repo 2020-06-29 02:28:00 -04:00
Isaac Grynsztein
e5461de2f7 Ignore youtube-dl cookies and executable files from the repo 2020-06-29 02:26:55 -04:00
UnlimitedCookies
4a69a0d362 modified: src/app/settings/settings.component.ts
new file:   src/assets/i18n/messages.de.json
	new file:   src/assets/i18n/messages.de.xlf
2020-06-29 03:48:29 +02:00
Tzahi12345
97f7f0b462 Merge pull request #141 from web-connect/feature/remove_whitespace
Removing extra white spaces
2020-06-27 12:43:44 -04:00
Tzahi12345
d3cbfa265e Merge pull request #140 from web-connect/feature/readme_update
Updating README regarding output dir
2020-06-27 12:42:30 -04:00
Justin Turner
42bd219ed6 Removing extra white spaces 2020-06-27 01:09:41 -05:00
Justin Turner
f8123cf03b Updating output dir 2020-06-27 00:55:55 -05:00
Isaac Grynsztein
94df98e5d0 Fixed bug that prevented subscription archives from being downloaded if their path was express as a full path 2020-06-23 00:01:23 -04:00
Isaac Grynsztein
2998562655 Added the ability to view logs from the settings menu 2020-06-22 23:15:21 -04:00
Tzahi12345
09d8ce04d7 Merge pull request #128 from web-connect/bug/i127_nan_not_found
Fixes #127 by adding nan to dependencies
2020-06-22 18:33:35 -04:00
Tzahi12345
504c818c2f Merge pull request #136 from Tzahi12345/subscriptions-custom-path
Subscriptions V3
2020-06-22 00:11:12 -04:00
Isaac Grynsztein
ca0e6b993d Re-compiled frontend 2020-06-22 00:03:42 -04:00
Isaac Grynsztein
0346833c3b Merged changes from master 2020-06-21 23:54:18 -04:00
Isaac Grynsztein
32da9dd9dd format in custom args for subscriptions now overrides default format (allows for users to specify custom formats for subs) 2020-06-21 23:49:00 -04:00
Isaac Grynsztein
20f162d794 Added args modifier dialog to custom args input in the subscribe dialog 2020-06-21 23:40:39 -04:00
Isaac Grynsztein
319bb0160b Finished adding support for audio subscriptions, custom args for subscriptions, and custom output for subscription downloads 2020-06-21 23:27:14 -04:00
Tzahi12345
5983a8bd52 Merge pull request #129 from web-connect/feature/i100_UI-typo-for-logger-level
#100 Typo fix for logger
2020-06-13 16:56:02 -04:00
Justin Turner
49b8cd416e Typo fix for logger 2020-06-13 13:39:42 -05:00
Justin Turner
58f71469b5 Fixes #127 by adding nan to dependencies 2020-06-13 01:21:03 -05:00
Tzahi12345
db81120645 Added audioOnlyMode, customArgs, and customFileOutput fields to the subscribe dialog 2020-06-12 17:57:34 -04:00
Tzahi12345
163a88bcfd DB implementation of subs now can properly delete subs 2020-06-10 21:41:05 -04:00
Tzahi12345
2441270d88 Removed redundant redirect when in the login screen
Fixed bug that prevented user registration with a faulty token
2020-06-09 21:19:42 -04:00
Tzahi12345
a518ac680f Fixed bug that prevented new users from accessing the login screen 2020-06-09 18:12:55 -04:00
Tzahi12345
78d3145e0b Deleting a video with an extension in the filename will now work UI-side 2020-06-09 18:02:46 -04:00
Tzahi12345
b8a4e0773f Added new utils.js module to assist backend with shared helper functions
Subscription files are now stored in the database, and will be primarily managed through it
2020-06-09 18:02:25 -04:00
Tzahi12345
f04139634a Merge branch 'master' of https://github.com/Tzahi12345/YoutubeDL-Material into subscriptions-custom-path 2020-06-08 21:24:46 -04:00
Tzahi12345
a074166903 Added catch statement if youtube-dl tags could not be retrieved 2020-06-08 13:04:16 -04:00
Tzahi12345
6893dbd506 Merge pull request #118 from Tzahi12345/Windows-build-fix-for-new-dockerfile
Updated Dockerfile to support Windows builds
2020-06-06 13:08:45 -04:00
Tzahi12345
e8ee4ffb64 Made additional cleanups as per recs by SuperSandro 2020-06-06 13:07:50 -04:00
Tzahi12345
378025bd9d Updated dockerfile to support Windows builds 2020-06-03 20:56:48 -04:00
Tzahi12345
d8e85df6d6 Scaffolding for registering subscription downloads 2020-06-03 19:18:10 -04:00
Tzahi12345
0c864c3d8d Merge pull request #117 from SuperSandro2000/docker-fix
Docker: Fix startup error in entrypoint, ownership of node_modules
2020-06-03 09:46:58 -04:00
Sandro Jäckel
dd8ab9be29 Fix default uid/gid of node_modules 2020-06-03 07:12:14 +02:00
Sandro Jäckel
bab354ce81 Fix variable expansion 2020-06-03 07:11:30 +02:00
Tzahi12345
d3d0f92ea5 Merge branch 'master' of https://github.com/Tzahi12345/YoutubeDL-Material into subscriptions-custom-path 2020-06-02 23:24:54 -04:00
Tzahi12345
b37d912e04 Merge pull request #116 from SuperSandro2000/docker-non-root
Run docker as non root
2020-06-02 21:47:22 -04:00
Tzahi12345
1c93a4f9f2 Updated frontend files to support video sharing with non-users in multi user mode 2020-06-02 20:09:40 -04:00
Tzahi12345
abfe0dad03 Prevents login redirect for shared videos in multi user mode 2020-06-02 20:03:01 -04:00
Sandro Jäckel
5bfecfcefe Run docker as non root, copy package-json.lock 2020-06-03 01:53:06 +02:00
Tzahi12345
ffe3133635 Merge pull request #115 from SuperSandro2000/alpine-12
Docker: Update Alpine to 3.12
2020-06-02 17:43:57 -04:00
Sandro Jäckel
68c67ca7d5 Update alpine to 3.12 2020-06-02 23:34:05 +02:00
Sandro Jäckel
c4d50c9018 Format 2020-06-02 23:32:16 +02:00
Isaac Grynsztein
42b749a101 Updated frontend binaries 2020-05-30 16:39:11 -04:00
Isaac Grynsztein
9c729abfaa Added new safe download override setting to config manager (forgot to do this before) 2020-05-30 16:30:28 -04:00
Isaac Grynsztein
dcc7fbd81c Added new setting to force a safe download (removes features like progress bar) 2020-05-30 16:28:00 -04:00
Isaac Grynsztein
b3c8f9e57a Fixed bug that caused downloads to fail when archiving was enabled
Removed error message on URL input on the home page

Fixed bug that prevented file deletion in multi user mode with archiving enabled
2020-05-30 16:20:03 -04:00
Tzahi12345
9e5ad66a9d Added scaffolding for custom paths in subscriptions 2020-05-10 04:53:49 -04:00
56 changed files with 3456 additions and 336 deletions

2
.gitignore vendored
View File

@@ -53,6 +53,7 @@ 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
@@ -63,3 +64,4 @@ backend/appdata/logs/combined.log
backend/appdata/logs/error.log
backend/appdata/users.json
backend/users/*
backend/appdata/cookies.txt

View File

@@ -87,7 +87,7 @@ If you'd like to install YoutubeDL-Material, go to the Installation section. If
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/dist` folder. Drag those files into the `public` directory in the `backend` 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/backend/public` folder.
The frontend is now complete. The backend is much easier. Just go into the `backend` folder, and type `npm start`.
@@ -101,9 +101,9 @@ If you are looking to setup YoutubeDL-Material with Docker, this section is for
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.
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.
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

View File

@@ -1,18 +1,27 @@
FROM alpine:3.11
FROM alpine:3.12
RUN \
apk add --no-cache npm python ffmpeg && \
apk add --no-cache --repository http://dl-cdn.alpinelinux.org/alpine/edge/testing/ \
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/" ]
COPY package.json /app/
RUN npm install && chown -R $UID:$GID ./
RUN npm install
COPY ./ /app/
COPY --chown=$UID:$GID [ "./", "/app/" ]
EXPOSE 17442
ENTRYPOINT [ "/app/entrypoint.sh" ]
CMD [ "node", "app.js" ]

File diff suppressed because it is too large Load Diff

View File

@@ -13,7 +13,8 @@
"path-audio": "audio/",
"path-video": "video/",
"use_youtubedl_archive": false,
"custom_args": ""
"custom_args": "",
"safe_download_override": false
},
"Extra": {
"title_top": "YoutubeDL-Material",

View File

@@ -13,7 +13,8 @@
"path-audio": "audio/",
"path-video": "video/",
"use_youtubedl_archive": false,
"custom_args": ""
"custom_args": "",
"safe_download_override": false
},
"Extra": {
"title_top": "YoutubeDL-Material",

Binary file not shown.

View File

@@ -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.
*
@@ -331,7 +331,7 @@ exports.addPlaylist = function(user_uid, new_playlist, type) {
return true;
}
exports.updatePlaylist = function(user_uid, playlistID, new_filenames, type) {
exports.updatePlaylistFiles = 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;
}
@@ -392,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}`)
@@ -430,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 audio files. Creating...');
fs.closeSync(fs.openSync(archive_path, 'w'));
logger.info(`Could not find archive file for ${type} files. Creating...`);
fs.ensureFileSync(archive_path);
}
}
}
@@ -454,7 +454,7 @@ exports.changeSharingMode = function(user_uid, file_uid, type, is_playlist, enab
file_db_obj.assign({sharingEnabled: enabled}).write();
}
}
return success;
}
@@ -470,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
@@ -505,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
@@ -537,4 +537,4 @@ function getToken(queryParams) {
} else {
return null;
}
};
};

View File

@@ -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,6 +155,13 @@ 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,
@@ -164,7 +171,8 @@ module.exports = {
configExistsCheck: configExistsCheck,
CONFIG_ITEMS: CONFIG_ITEMS,
initialize: initialize,
descriptors: {}
descriptors: {},
globalArgsRequiresSafeDownload: globalArgsRequiresSafeDownload
}
DEFAULT_CONFIG = {
@@ -182,7 +190,8 @@ DEFAULT_CONFIG = {
"path-audio": "audio/",
"path-video": "video/",
"use_youtubedl_archive": false,
"custom_args": ""
"custom_args": "",
"safe_download_override": false
},
"Extra": {
"title_top": "YoutubeDL-Material",

View File

@@ -40,6 +40,10 @@ 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': {
@@ -171,4 +175,4 @@ module.exports = {
CONFIG_ITEMS: CONFIG_ITEMS,
AVAILABLE_PERMISSIONS: AVAILABLE_PERMISSIONS,
CURRENT_VERSION: 'v4.0'
}
}

119
backend/db.js Normal file
View File

@@ -0,0 +1,119 @@
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});
}
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
}

17
backend/entrypoint.sh Executable file
View File

@@ -0,0 +1,17 @@
#!/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 '{}' +
exec su-exec "$UID:$GID" "$0" "$@"
fi
exec "$@"

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -50,6 +50,7 @@
"passport-jwt": "^4.0.0",
"passport-local": "^1.0.0",
"progress": "^2.0.3",
"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

View File

@@ -0,0 +1,198 @@
{
"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"
}

View File

@@ -14,5 +14,5 @@
<link rel="stylesheet" href="styles.5112d6db78cf21541598.css"></head>
<body>
<app-root></app-root>
<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>
<script src="runtime-es2015.1f02852de81190376ba1.js" type="module"></script><script src="runtime-es5.1f02852de81190376ba1.js" nomodule defer></script><script src="polyfills-es5.7f923c8f5afda210edd3.js" nomodule defer></script><script src="polyfills-es2015.5b408f108bcea938a7e2.js" type="module"></script><script src="main-es2015.0cbc545a4a3bee376826.js" type="module"></script><script src="main-es5.0cbc545a4a3bee376826.js" nomodule defer></script></body>
</html>

View File

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

View File

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

View File

@@ -6,17 +6,20 @@ 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;
function setDB(input_db, input_users_db) { db = input_db; users_db = input_users_db }
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 setLogger(input_logger) { logger = input_logger; }
function initialize(input_db, input_users_db, input_logger) {
setDB(input_db, input_users_db);
function initialize(input_db, input_users_db, input_logger, input_db_api) {
setDB(input_db, input_users_db, input_db_api);
setLogger(input_logger);
}
@@ -28,6 +31,7 @@ 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;
@@ -44,18 +48,28 @@ async function subscribe(sub, user_uid = null) {
}
// add sub to db
if (user_uid)
let sub_db = null;
if (user_uid) {
users_db.get('users').find({uid: user_uid}).get('subscriptions').push(sub).write();
else
sub_db = users_db.get('users').find({uid: user_uid}).get('subscriptions').find({id: sub.id});
} 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) {
@@ -66,7 +80,7 @@ async function getSubscriptionInfo(sub, user_uid = null) {
basePath = config_api.getConfigItem('ytdl_subscriptions_base_path');
return new Promise(resolve => {
// get videos
// get videos
let downloadConfig = ['--dump-json', '--playlist-end', '1']
youtubedl.exec(sub.url, downloadConfig, {}, function(err, output) {
if (debugMode) {
@@ -160,25 +174,33 @@ async function unsubscribe(sub, deleteMode, user_uid = null) {
}
async function deleteSubscriptionFile(sub, file, deleteForever, user_uid = null) {
async function deleteSubscriptionFile(sub, file, deleteForever, file_uid = null, user_uid = null) {
let basePath = null;
if (user_uid)
let sub_db = null;
if (user_uid) {
basePath = path.join(config_api.getConfigItem('ytdl_users_base_path'), user_uid, 'subscriptions');
else
sub_db = users_db.get('users').find({uid: user_uid}).get('subscriptions').find({id: sub.id});
} else {
basePath = config_api.getConfigItem('ytdl_subscriptions_base_path');
sub_db = db.get('subscriptions').find({id: sub.id});
}
const useArchive = config_api.getConfigItem('ytdl_subscriptions_use_youtubedl_archive');
const 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+'.mp4');
var videoFilePath = path.join(__dirname,filePath,name+ext);
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'];
@@ -189,6 +211,10 @@ async function deleteSubscriptionFile(sub, file, deleteForever, user_uid = null)
fs.unlinkSync(imageFilePath);
}
if (altImageFileExists) {
fs.unlinkSync(altImageFilePath);
}
if (videoFileExists) {
fs.unlink(videoFilePath, function(err) {
if (fs.existsSync(jsonPath) || fs.existsSync(videoFilePath)) {
@@ -209,7 +235,7 @@ async function deleteSubscriptionFile(sub, file, deleteForever, user_uid = null)
// TODO: tell user that the file didn't exist
resolve(true);
}
});
}
@@ -237,13 +263,45 @@ async function getVideosForSub(sub, user_uid = null) {
const useArchive = config_api.getConfigItem('ytdl_subscriptions_use_youtubedl_archive');
let appendedBasePath = null
if (sub.name) {
appendedBasePath = getAppendedBasePath(sub, basePath);
} else {
appendedBasePath = path.join(basePath, (sub.isPlaylist ? 'playlists/%(playlist_title)s' : 'channels/%(uploader)s'));
appendedBasePath = getAppendedBasePath(sub, basePath);
let multiUserMode = null;
if (user_uid) {
multiUserMode = {
user: user_uid,
file_path: appendedBasePath
}
}
let downloadConfig = ['-o', appendedBasePath + '/%(title)s.mp4', '-f', 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/mp4', '-ciw', '--write-info-json', '--print-json'];
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');
} else {
qualityPath = ['-f', 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/mp4']
}
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 archive_dir = null;
let archive_path = null;
@@ -274,7 +332,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);
@@ -286,7 +344,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)
handleOutputJSON(sub, sub_db, output, i === 0, multiUserMode)
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']);
@@ -315,18 +373,20 @@ async function getVideosForSub(sub, user_uid = null) {
}
const reset_videos = i === 0;
handleOutputJSON(sub, sub_db, output_json, reset_videos);
handleOutputJSON(sub, sub_db, output_json, multiUserMode, reset_videos);
// TODO: Potentially store downloaded files in db?
}
resolve(true);
}
});
}, err => {
logger.error(err);
});
}
function handleOutputJSON(sub, sub_db, output_json, reset_videos = false) {
function handleOutputJSON(sub, sub_db, output_json, multiUserMode = null, reset_videos = false) {
if (sub.streamingOnly) {
if (reset_videos) {
sub_db.assign({videos: []}).write();
@@ -337,6 +397,9 @@ function handleOutputJSON(sub, sub_db, output_json, reset_videos = false) {
// add to db
sub_db.get('videos').push(output_json).write();
} else {
// TODO: make multiUserMode obj
db_api.registerFileDB(path.basename(output_json['_filename']), sub.type, multiUserMode, sub);
}
}
@@ -350,7 +413,7 @@ 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();
}
@@ -397,7 +460,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;
}
}

97
backend/utils.js Normal file
View File

@@ -0,0 +1,97 @@
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
}

Binary file not shown.

28
package-lock.json generated
View File

@@ -2557,6 +2557,16 @@
"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",
@@ -5230,6 +5240,13 @@
"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",
@@ -7155,6 +7172,8 @@
"dev": true,
"optional": true,
"requires": {
"bindings": "^1.5.0",
"nan": "^2.12.1",
"node-pre-gyp": "*"
},
"dependencies": {
@@ -9077,6 +9096,11 @@
"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",
@@ -14122,6 +14146,8 @@
"dev": true,
"optional": true,
"requires": {
"bindings": "^1.5.0",
"nan": "^2.12.1",
"node-pre-gyp": "*"
},
"dependencies": {
@@ -15200,6 +15226,8 @@
"dev": true,
"optional": true,
"requires": {
"bindings": "^1.5.0",
"nan": "^2.12.1",
"node-pre-gyp": "*"
},
"dependencies": {

View File

@@ -34,6 +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-file-drop": "^9.0.1",
"ngx-videogular": "^9.0.1",

View File

@@ -71,6 +71,8 @@ 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';
registerLocaleData(es, 'es');
@@ -109,7 +111,9 @@ export function isVisible({ event, element, scrollContainer, offset }: IsVisible
AddUserDialogComponent,
ManageUserComponent,
ManageRoleComponent,
CookiesUploaderDialogComponent
CookiesUploaderDialogComponent,
LogsViewerComponent,
ModifyPlaylistComponent
],
imports: [
CommonModule,

View File

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

View File

@@ -43,7 +43,7 @@ export class DownloadsComponent implements OnInit, OnDestroy {
valid_sessions_length = 0;
sort_downloads = (a, b) => {
const result = a.value.timestamp_start < b.value.timestamp_start;
const result = b.value.timestamp_start - a.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,11 +107,32 @@ 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 {

View File

@@ -49,9 +49,19 @@ 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.');
}
});
}
@@ -84,7 +94,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;

View File

@@ -0,0 +1,21 @@
<div style="height: 100%">
<div *ngIf="logs_loading" style="position: absolute; top: 40%; left: 50%">
<mat-spinner [diameter]="32"></mat-spinner>
</div>
<textarea style="height: 275px" matInput readonly [(ngModel)]="logs" placeholder="Logs will appear here" i18n-placeholder="Logs placeholder"></textarea>
<div>
<button style="margin-top: 12px;" [cdkCopyToClipboard]="logs" (click)="copiedLogsToClipboard()" mat-flat-button color="primary"><ng-container i18n="Copy to clipboard button text">Copy to clipboard</ng-container></button>
<div style="float: right">
<ng-container i18n="Label for lines select in logger view">Lines:</ng-container>&nbsp;
<mat-form-field style="width: 75px;">
<mat-select (selectionChange)="getLogs()" [(ngModel)]="requested_lines">
<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>
</div>
</div>

View File

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

View File

@@ -0,0 +1,40 @@
import { Component, OnInit, AfterViewInit } from '@angular/core';
import { PostsService } from '../../posts.services';
@Component({
selector: 'app-logs-viewer',
templateUrl: './logs-viewer.component.html',
styleUrls: ['./logs-viewer.component.scss']
})
export class LogsViewerComponent implements OnInit {
logs: string = null;
requested_lines = 50;
logs_loading = false;
constructor(private postsService: PostsService) { }
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']) {
this.logs = res['logs'];
} 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!');
}
}

View File

@@ -0,0 +1,28 @@
<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>

View File

@@ -0,0 +1,50 @@
.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;
}

View File

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

View File

@@ -0,0 +1,83 @@
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);
}
}

View File

@@ -3,16 +3,20 @@
<mat-dialog-content>
<div class="container-fluid">
<div class="row">
<div class="col-12">
<div class="col-12 mb-4">
<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">
@@ -31,9 +35,33 @@
</div>
<div class="col-12">
<div>
<mat-checkbox [(ngModel)]="streamingOnlyMode"><ng-container i18n="Streaming-only mode">Streaming-only mode</ng-container></mat-checkbox>
<mat-checkbox [(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]="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>

View File

@@ -6,3 +6,8 @@
.mat-spinner {
margin-left: 5%;
}
.args-edit-button {
position: absolute;
margin-left: 10px;
}

View File

@@ -1,7 +1,8 @@
import { Component, OnInit } from '@angular/core';
import { MatDialogRef } from '@angular/material/dialog';
import { MatDialogRef, MatDialog } 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',
@@ -22,6 +23,12 @@ export class SubscribeDialogComponent implements OnInit {
// no videos actually downloaded, just streamed
streamingOnlyMode = false;
// audio only mode
audioOnlyMode = false;
customFileOutput = '';
customArgs = '';
time_units = [
'day',
'week',
@@ -31,6 +38,7 @@ export class SubscribeDialogComponent implements OnInit {
constructor(private postsService: PostsService,
private snackBar: MatSnackBar,
private dialog: MatDialog,
public dialogRef: MatDialogRef<SubscribeDialogComponent>) { }
ngOnInit() {
@@ -49,7 +57,8 @@ 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).subscribe(res => {
this.postsService.createSubscription(this.url, this.name, timerange, this.streamingOnlyMode,
this.audioOnlyMode, this.customArgs, this.customFileOutput).subscribe(res => {
this.subscribing = false;
if (res['new_sub']) {
this.dialogRef.close(res['new_sub']);
@@ -63,6 +72,20 @@ 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,

View File

@@ -2,10 +2,10 @@
<div style="padding:5px">
<div style="height: 52px;">
<div>
<b><a class="file-link" href="javascript:void(0)" (click)="!isPlaylist ? mainComponent.goToFile(name, isAudio, uid) : mainComponent.goToPlaylist(name, type)">{{title}}</a></b>
<b><a class="file-link" href="javascript:void(0)" (click)="!playlist ? 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>&nbsp;{{name}}</span>
<div *ngIf="isPlaylist"><ng-container i18n="Playlist video count">Count:</ng-container>&nbsp;{{count}}</div>
<div *ngIf="playlist"><ng-container i18n="Playlist video count">Count:</ng-container>&nbsp;{{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,11 +14,16 @@
</span>
</div>
</div>
<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>
<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>
<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>

View File

@@ -7,6 +7,7 @@ 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',
@@ -22,7 +23,7 @@ export class FileCardComponent implements OnInit {
@Input() thumbnailURL: string;
@Input() isAudio = true;
@Output() removeFile: EventEmitter<string> = new EventEmitter<string>();
@Input() isPlaylist = false;
@Input() playlist = null;
@Input() count = null;
@Input() use_youtubedl_archive = false;
type;
@@ -46,15 +47,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.isPlaylist ? '?list=' : '?v=')
const index_offset = (this.isPlaylist ? 6 : 3);
const string_id = (this.playlist ? '?list=' : '?v=')
const index_offset = (this.playlist ? 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.isPlaylist) {
if (!this.playlist) {
this.postsService.deleteFile(this.uid, this.isAudio, blacklistMode).subscribe(result => {
if (result) {
this.openSnackBar('Delete success!', 'OK.');
@@ -80,6 +81,24 @@ 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;
}

View File

@@ -11,10 +11,7 @@
<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" [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>
<input style="padding-right: 25px;" matInput (keyup.enter)="downloadClicked()" (ngModelChange)="inputChanged($event)" [(ngModel)]="url" [placeholder]="'URL' + (youtubeSearchEnabled ? ' or search' : '')" type="url" name="url" #urlinput>
</mat-form-field>
<button type="button" class="input-clear-button" mat-icon-button (click)="clearInput()"><mat-icon>clear</mat-icon></button>
</div>
@@ -216,7 +213,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" [isPlaylist]="true" [count]="playlist.fileNames.length" [use_youtubedl_archive]="use_youtubedl_archive"></app-file-card>
[length]="null" [isAudio]="true" [playlist]="playlist" [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,7 +255,7 @@
<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" [isPlaylist]="true" [count]="playlist.fileNames.length" [use_youtubedl_archive]="use_youtubedl_archive"></app-file-card>
[length]="null" [isAudio]="false" [playlist]="playlist" [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>

View File

@@ -439,10 +439,11 @@ export class MainComponent implements OnInit {
public removeFromMp3(name: string) {
for (let i = 0; i < this.mp3s.length; i++) {
if (this.mp3s[i].id === name) {
if (this.mp3s[i].id === name || this.mp3s[i].id + '.mp3' === name) {
this.mp3s.splice(i, 1);
}
}
this.getMp3s();
}
public removePlaylistMp3(playlistID, index) {
@@ -457,10 +458,11 @@ export class MainComponent implements OnInit {
public removeFromMp4(name: string) {
for (let i = 0; i < this.mp4s.length; i++) {
if (this.mp4s[i].id === name) {
if (this.mp4s[i].id === name || this.mp4s[i].id + '.mp4' === name) {
this.mp4s.splice(i, 1);
}
}
this.getMp4s();
}
public removePlaylistMp4(playlistID, index) {

View File

@@ -62,6 +62,8 @@ export class PlayerComponent implements OnInit {
downloading = false;
original_volume = null;
@HostListener('window:resize', ['$event'])
onResize(event) {
this.innerWidth = window.innerWidth;
@@ -127,7 +129,7 @@ export class PlayerComponent implements OnInit {
this.currentItem = this.playlist[0];
this.currentIndex = 0;
this.show_player = true;
} else if (this.type === 'subscription' || this.fileNames) {
} else if (this.subscriptionName || this.fileNames) {
this.show_player = true;
this.parseFileNames();
}
@@ -181,9 +183,6 @@ export class PlayerComponent implements OnInit {
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\'.');
@@ -198,7 +197,7 @@ export class PlayerComponent implements OnInit {
fullLocation = this.baseStreamPath + baseLocation + encodeURIComponent(fileName);
} else {
// default to video but include subscription name param
baseLocation = 'video/';
baseLocation = this.type === 'audio' ? 'audio/' : 'video/';
fullLocation = this.baseStreamPath + baseLocation + encodeURIComponent(fileName) + '?subName=' + this.subscriptionName +
'&subPlaylist=' + this.subPlaylist;
}
@@ -238,6 +237,12 @@ export class PlayerComponent implements OnInit {
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'));
}
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));
@@ -246,6 +251,13 @@ export class PlayerComponent implements OnInit {
}
}
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
@@ -377,7 +389,7 @@ export class PlayerComponent implements OnInit {
updatePlaylist() {
const fileNames = this.getFileNames();
this.playlist_updating = true;
this.postsService.updatePlaylist(this.id, fileNames, this.type).subscribe(res => {
this.postsService.updatePlaylistFiles(this.id, fileNames, this.type).subscribe(res => {
this.playlist_updating = false;
if (res['success']) {
const fileNamesEncoded = fileNames.join('|nvr|');

View File

@@ -73,6 +73,8 @@ 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;
@@ -80,10 +82,12 @@ export class PostsService implements CanActivate {
this.config = result['YoutubeDLMaterial'];
if (this.config['Advanced']['multi_user_mode']) {
// login stuff
if (localStorage.getItem('jwt_token')) {
if (localStorage.getItem('jwt_token') && localStorage.getItem('jwt_token') !== 'null') {
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();
}
@@ -230,6 +234,10 @@ 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);
}
@@ -266,8 +274,12 @@ export class PostsService implements CanActivate {
type: type, uuid: uuid}, this.httpOptions);
}
updatePlaylist(playlistID, fileNames, type) {
return this.http.post(this.path + 'updatePlaylist', {playlistID: playlistID,
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,
fileNames: fileNames,
type: type}, this.httpOptions);
}
@@ -276,17 +288,18 @@ 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) {
return this.http.post(this.path + 'subscribe', {url: url, name: name, timerange: timerange, streamingOnly: streamingOnly},
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);
}
unsubscribe(sub, deleteMode = false) {
return this.http.post(this.path + 'unsubscribe', {sub: sub, deleteMode: deleteMode}, this.httpOptions)
}
deleteSubscriptionFile(sub, file, deleteForever) {
return this.http.post(this.path + 'deleteSubscriptionFile', {sub: sub, file: file, deleteForever: deleteForever}, 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)
}
getSubscription(id) {
@@ -377,6 +390,7 @@ 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']);
@@ -397,17 +411,14 @@ 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() {
this.checkAdminCreationStatus();
if (!this.initialized) {
this.setInitialized();
}
if (this.router.url === '/login') {
return;
}

View File

@@ -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>
@@ -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">
@@ -132,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>
@@ -152,6 +152,10 @@
<mat-checkbox color="accent" [(ngModel)]="new_config['Downloader']['use_youtubedl_archive']"><ng-container i18n="Use youtubedl archive setting">Use youtube-dl archive</ng-container></mat-checkbox>
<p>Note: This setting only applies to downloads on the Home page. If you would like to use youtube-dl archive functionality in subscriptions, head down to the Subscriptions section.</p>
</div>
<div class="col-12 mt-3">
<mat-checkbox color="accent" [(ngModel)]="new_config['Downloader']['safe_download_override']"><ng-container i18n="Safe download override setting">Safe download override</ng-container></mat-checkbox>
</div>
</div>
</div>
</ng-template>
@@ -188,7 +192,7 @@
</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">
@@ -271,7 +275,7 @@
</div>
<div class="col-12 mt-2 mb-1">
<mat-form-field>
<mat-label><ng-container i18n="Logger level select label">Select a downloader</ng-container></mat-label>
<mat-label><ng-container i18n="Logger level select label">Select a logger level</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>
@@ -307,7 +311,14 @@
</div>
<app-modify-users></app-modify-users>
</mat-tab>
</mat-tab-group>
<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-dialog-content>
<mat-dialog-actions>
@@ -319,4 +330,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>

View File

@@ -17,7 +17,7 @@ import { CookiesUploaderDialogComponent } from 'app/dialogs/cookies-uploader-dia
})
export class SettingsComponent implements OnInit {
all_locales = isoLangs;
supported_locales = ['en', 'es'];
supported_locales = ['en', 'es', 'de'];
initialLocale = localStorage.getItem('locale');
initial_config = null;

View File

@@ -71,14 +71,14 @@ export class SubscriptionFileCardComponent implements OnInit {
}
deleteAndRedownload() {
this.postsService.deleteSubscriptionFile(this.sub, this.file.id, false).subscribe(res => {
this.postsService.deleteSubscriptionFile(this.sub, this.file.id, false, this.file.uid).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).subscribe(res => {
this.postsService.deleteSubscriptionFile(this.sub, this.file.id, true, this.file.uid).subscribe(res => {
this.reloadSubscription.emit(true);
this.openSnackBar(`Successfully deleted file: '${this.file.id}'`, 'Dismiss.');
});

View File

@@ -92,8 +92,9 @@ export class SubscriptionComponent implements OnInit {
if (this.subscription.streamingOnly) {
this.router.navigate(['/player', {name: name, url: url}]);
} else {
this.router.navigate(['/player', {fileNames: name, type: 'subscription', subscriptionName: this.subscription.name,
subPlaylist: this.subscription.isPlaylist, uuid: this.postsService.user ? this.postsService.user.uid : null}]);
this.router.navigate(['/player', {fileNames: name,
type: this.subscription.type ? this.subscription.type : 'video', subscriptionName: this.subscription.name,
subPlaylist: this.subscription.isPlaylist, uuid: this.postsService.user ? this.postsService.user.uid : null}]);
}
}

View File

@@ -13,7 +13,8 @@
"path-audio": "audio/",
"path-video": "video/",
"use_youtubedl_archive": false,
"custom_args": ""
"custom_args": "",
"safe_download_override": false
},
"Extra": {
"title_top": "YoutubeDL-Material",
@@ -37,7 +38,7 @@
"Subscriptions": {
"allow_subscriptions": true,
"subscriptions_base_path": "subscriptions/",
"subscriptions_check_interval": "300",
"subscriptions_check_interval": "30",
"subscriptions_use_youtubedl_archive": true
},
"Users": {
@@ -49,7 +50,8 @@
"custom_downloading_agent": "",
"multi_user_mode": true,
"allow_advanced_download": true,
"logger_level": "debug"
"logger_level": "debug",
"use_cookies": true
}
}
}

View File

@@ -0,0 +1,198 @@
{
"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"
}

File diff suppressed because it is too large Load Diff