Compare commits

..

2 Commits

Author SHA1 Message Date
Isaac Abadi
3ea47af7a1 Scaffolding for subscription sharing 2020-08-12 16:30:38 -04:00
Isaac Abadi
cf01a0d8ab Potentially fixed bug where iOS downloads failed 2020-08-12 00:32:03 -04:00
162 changed files with 13482 additions and 19416 deletions

View File

@@ -1,95 +0,0 @@
name: continuous integration
on:
push:
branches: [master, feat/*]
tags:
- v*
pull_request:
branches: [master]
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: checkout code
uses: actions/checkout@v2
- name: setup node
uses: actions/setup-node@v1
- name: install dependencies
run: |
npm install
cd backend
npm install
sudo npm install -g @angular/cli
- name: build
run: ng build --prod
- name: prepare artifact upload
shell: pwsh
run: |
New-Item -Name build -ItemType Directory
New-Item -Path build -Name youtubedl-material -ItemType Directory
Copy-Item -Path ./backend/appdata -Recurse -Destination ./build/youtubedl-material
Copy-Item -Path ./backend/audio -Recurse -Destination ./build/youtubedl-material
Copy-Item -Path ./backend/authentication -Recurse -Destination ./build/youtubedl-material
Copy-Item -Path ./backend/public -Recurse -Destination ./build/youtubedl-material
Copy-Item -Path ./backend/subscriptions -Recurse -Destination ./build/youtubedl-material
Copy-Item -Path ./backend/video -Recurse -Destination ./build/youtubedl-material
New-Item -Path ./build/youtubedl-material -Name users
Copy-Item -Path ./backend/*.js -Destination ./build/youtubedl-material
Copy-Item -Path ./backend/*.json -Destination ./build/youtubedl-material
- name: upload build artifact
uses: actions/upload-artifact@v1
with:
name: youtubedl-material
path: build
release:
runs-on: ubuntu-latest
needs: build
if: contains(github.ref, '/tags/v')
steps:
- name: checkout code
uses: actions/checkout@v2
- name: create release
id: create_release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ github.ref }}
release_name: YoutubeDL-Material ${{ github.ref }}
body: |
# New features
# Minor additions
# Bug fixes
draft: true
prerelease: false
- name: download build artifact
uses: actions/download-artifact@v1
with:
name: youtubedl-material
path: ${{runner.temp}}/youtubedl-material
- name: extract tag name
id: tag_name
run: echo ::set-output name=tag_name::${GITHUB_REF#refs/tags/}
- name: prepare release asset
shell: pwsh
run: Compress-Archive -Path ${{runner.temp}}/youtubedl-material -DestinationPath youtubedl-material-${{ steps.tag_name.outputs.tag_name }}.zip
- name: upload release asset
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./youtubedl-material-${{ steps.tag_name.outputs.tag_name }}.zip
asset_name: youtubedl-material-${{ steps.tag_name.outputs.tag_name }}.zip
asset_content_type: application/zip
- name: upload docker-compose asset
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./docker-compose.yml
asset_name: docker-compose.yml
asset_content_type: text/plain

View File

@@ -1,71 +0,0 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
name: "CodeQL"
on:
push:
branches: [master]
pull_request:
# The branches below must be a subset of the branches above
branches: [master]
schedule:
- cron: '0 12 * * 6'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
# Override automatic language detection by changing the below list
# Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python']
language: ['javascript']
# Learn more...
# https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection
steps:
- name: Checkout repository
uses: actions/checkout@v2
with:
# We must fetch at least the immediate parents so that if this is
# a pull request then we can checkout the head.
fetch-depth: 2
# If this run was triggered by a pull request event, then checkout
# the head of the pull request instead of the merge commit.
- run: git checkout HEAD^2
if: ${{ github.event_name == 'pull_request' }}
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# queries: ./path/to/local/query, your-org/your-repo/queries@main
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v1
# Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
# and modify them (or add more) to build your code if your project
# uses a compiled language
#- run: |
# make bootstrap
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1

View File

@@ -1,32 +0,0 @@
name: docker-release
on:
workflow_dispatch:
inputs:
tags:
description: 'Docker tags'
required: true
jobs:
build-and-push:
runs-on: ubuntu-latest
steps:
- name: checkout code
uses: actions/checkout@v2
- name: setup platform emulator
uses: docker/setup-qemu-action@v1
- name: setup multi-arch docker build
uses: docker/setup-buildx-action@v1
- name: Login to DockerHub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: build & push images
uses: docker/build-push-action@v2
with:
context: .
file: ./Dockerfile
platforms: linux/amd64,linux/arm,linux/arm64/v8
push: true
tags: ${{ github.event.inputs.tags }}

View File

@@ -1,29 +0,0 @@
name: docker
on:
push:
branches: [master]
jobs:
build-and-push:
runs-on: ubuntu-latest
steps:
- name: checkout code
uses: actions/checkout@v2
- name: setup platform emulator
uses: docker/setup-qemu-action@v1
- name: setup multi-arch docker build
uses: docker/setup-buildx-action@v1
- name: Login to DockerHub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: build & push images
uses: docker/build-push-action@v2
with:
context: .
file: ./Dockerfile
platforms: linux/amd64,linux/arm,linux/arm64/v8
push: true
tags: tzahi12345/youtubedl-material:nightly

1
.gitignore vendored
View File

@@ -65,4 +65,3 @@ backend/appdata/logs/error.log
backend/appdata/users.json
backend/users/*
backend/appdata/cookies.txt
backend/public

View File

@@ -1,43 +0,0 @@
FROM alpine:3.12 as frontend
RUN apk add --no-cache \
npm
RUN npm install -g @angular/cli
WORKDIR /build
COPY [ "package.json", "package-lock.json", "/build/" ]
RUN npm install
COPY [ "angular.json", "tsconfig.json", "/build/" ]
COPY [ "src/", "/build/src/" ]
RUN ng build --prod
#--------------#
FROM alpine:3.12
ENV UID=1000 \
GID=1000 \
USER=youtube
RUN addgroup -S $USER -g $GID && adduser -D -S $USER -G $USER -u $UID
RUN apk add --no-cache \
ffmpeg \
npm \
python2 \
su-exec \
&& apk add --no-cache --repository http://dl-cdn.alpinelinux.org/alpine/edge/testing/ \
atomicparsley
WORKDIR /app
COPY --chown=$UID:$GID [ "backend/package.json", "backend/package-lock.json", "/app/" ]
RUN npm install && chown -R $UID:$GID ./
COPY --chown=$UID:$GID --from=frontend [ "/build/backend/public/", "/app/public/" ]
COPY --chown=$UID:$GID [ "/backend/", "/app/" ]
EXPOSE 17442
ENTRYPOINT [ "/app/entrypoint.sh" ]
CMD [ "node", "app.js" ]

View File

@@ -261,12 +261,12 @@ paths:
$ref: '#/components/schemas/inline_response_200_10'
security:
- Auth query parameter: []
/api/getSubscriptions:
/api/getAllSubscriptions:
post:
tags:
- subscriptions
summary: Get all subscriptions
operationId: post-api-getSubscriptions
operationId: post-api-getAllSubscriptions
requestBody:
content:
application/json:

View File

@@ -1,12 +1,12 @@
# YoutubeDL-Material
[![](https://img.shields.io/docker/pulls/tzahi12345/youtubedl-material.svg)](https://hub.docker.com/r/tzahi12345/youtubedl-material)
[![](https://img.shields.io/docker/image-size/tzahi12345/youtubedl-material?sort=date)](https://hub.docker.com/r/tzahi12345/youtubedl-material)
[![](https://img.shields.io/badge/%E2%86%91_Deploy_to-Heroku-7056bf.svg)](https://heroku.com/deploy?template=https://github.com/Tzahi12345/YoutubeDL-Material)
[![](https://img.shields.io/github/issues/Tzahi12345/YoutubeDL-Material)](https://github.com/Tzahi12345/YoutubeDL-Material/issues)
[![](https://img.shields.io/github/license/Tzahi12345/YoutubeDL-Material)](https://github.com/Tzahi12345/YoutubeDL-Material/blob/master/LICENSE.md)
[![Docker pulls badge](https://img.shields.io/docker/pulls/tzahi12345/youtubedl-material.svg)](https://hub.docker.com/r/tzahi12345/youtubedl-material)
[![Docker image size badge](https://img.shields.io/docker/image-size/tzahi12345/youtubedl-material?sort=date)](https://hub.docker.com/r/tzahi12345/youtubedl-material)
[![Heroku deploy badge](https://img.shields.io/badge/%E2%86%91_Deploy_to-Heroku-7056bf.svg)](https://heroku.com/deploy?template=https://github.com/Tzahi12345/YoutubeDL-Material)
[![GitHub issues badge](https://img.shields.io/github/issues/Tzahi12345/YoutubeDL-Material)](https://github.com/Tzahi12345/YoutubeDL-Material/issues)
[![License badge](https://img.shields.io/github/license/Tzahi12345/YoutubeDL-Material)](https://github.com/Tzahi12345/YoutubeDL-Material/blob/master/LICENSE.md)
YoutubeDL-Material is a Material Design frontend for [youtube-dl](https://rg3.github.io/youtube-dl/). It's coded using [Angular 11](https://angular.io/) for the frontend, and [Node.js](https://nodejs.org/) on the backend.
YoutubeDL-Material is a Material Design frontend for [youtube-dl](https://rg3.github.io/youtube-dl/). It's coded using [Angular 9](https://angular.io/) for the frontend, and [Node.js](https://nodejs.org/) on the backend.
Now with [Docker](#Docker) support!
@@ -16,42 +16,34 @@ Check out the prerequisites, and go to the installation section. Easy as pie!
Here's an image of what it'll look like once you're done:
<img src="https://i.imgur.com/C6vFGbL.png" width="800">
![frontpage](https://i.imgur.com/w8iofbb.png)
With optional file management enabled (default):
![frontpage_with_files](https://i.imgur.com/FTATqBM.png)
Dark mode:
<img src="https://i.imgur.com/vOtvH5w.png" width="800">
![dark_mode](https://i.imgur.com/r5ZtBqd.png)
### Prerequisites
NOTE: If you would like to use Docker, you can skip down to the [Docker](#Docker) section for a setup guide.
Debian/Ubuntu:
Make sure you have these dependencies installed on your system: nodejs and youtube-dl. If you don't, run this command:
```bash
sudo apt-get install nodejs youtube-dl ffmpeg
```
CentOS 7:
```bash
sudo yum install epel-release
sudo yum localinstall --nogpgcheck https://download1.rpmfusion.org/free/el/rpmfusion-free-release-7.noarch.rpm
sudo yum install centos-release-scl-rh
sudo yum install rh-nodejs12
scl enable rh-nodejs12 bash
sudo yum install nodejs youtube-dl ffmpeg ffmpeg-devel
sudo apt-get install nodejs youtube-dl
```
Optional dependencies:
* AtomicParsley (for embedding thumbnails, package name `atomicparsley`)
### Installing
1. First, download the [latest release](https://github.com/Tzahi12345/YoutubeDL-Material/releases/latest)!
2. Drag the `youtubedl-material` directory to an easily accessible directory. Navigate to the `appdata` folder and edit the `default.json` file.
2. Drag the `youtubedl-material` directory to an easily accessible directory. Navigate to the `appdata` folder and edit the `default.json` file. If you're using SSL encryption, look at the `encrypted.json` file for a template.
NOTE: If you are intending to use a [reverse proxy](https://github.com/Tzahi12345/YoutubeDL-Material/wiki/Reverse-Proxy-Setup), this next step is not necessary
@@ -83,16 +75,14 @@ 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/docker-compose.yml -o docker-compose.yml` to download the latest Docker Compose, or go to the [releases](https://github.com/Tzahi12345/YoutubeDL-Material/releases/) page to grab the version you'd like.
2. Run `docker-compose pull`. This will download the official YoutubeDL-Material docker image.
3. Run `docker-compose up` to start it up. If successful, it should say "HTTP(S): Started on port 17443" or something similar. This tells you the *container-internal* port of the application. Please check your `docker-compose.yml` file for the *external* port. If you downloaded the file as described above, it defaults to **8998**.
4. Make sure you can connect to the specified URL + *external* port, and if so, you are done!
NOTE: It is currently recommended that you use the `nightly` tag on Docker. To do so, simply update the docker-compose.yml `image` field so that it points to `tzahi12345/youtubedl-material:nightly`.
3. Run `docker-compose up` to start it up. If successful, it should say "HTTP(S): Started on port 8998" or something similar.
4. Make sure you can connect to the specified URL + port, and if so, you are done!
### Custom UID/GID
By default, the Docker container runs as non-root with UID=1000 and GID=1000. To set this to your own UID/GID, simply update the `environment` section in your `docker-compose.yml` like so:
```yml
```
environment:
UID: YOUR_UID
GID: YOUR_GID
@@ -100,7 +90,7 @@ environment:
## API
[API Docs](https://youtubedl-material.stoplight.io/docs/youtubedl-material/Public%20API%20v1.yaml)
[API Docs](https://stoplight.io/p/docs/gh/tzahi12345/youtubedl-material?group=master&utm_campaign=publish_dialog&utm_source=studio)
To get started, go to the settings menu and enable the public API from the *Extra* tab. You can generate an API key if one is missing.
@@ -119,10 +109,8 @@ If you're interested in translating the app into a new language, check out the [
* **Isaac Grynsztein** (me!) - *Initial work*
Official translators:
* Spanish - tzahi12345
* German - UnlimitedCookies
* Chinese - TyRoyal
See also the list of [contributors](https://github.com/your/project/contributors) who participated in this project.

View File

@@ -45,6 +45,8 @@
],
"optimization": true,
"outputHashing": "all",
"sourceMap": false,
"extractCss": true,
"namedChunks": false,
"aot": true,
"extractLicenses": true,

View File

@@ -1,49 +0,0 @@
FROM alpine:3.12 as frontend
RUN apk add --no-cache \
npm \
curl
RUN npm install -g @angular/cli
WORKDIR /build
RUN curl -L https://github.com/balena-io/qemu/releases/download/v3.0.0%2Bresin/qemu-3.0.0+resin-arm.tar.gz | tar zxvf - -C . && mv qemu-3.0.0+resin-arm/qemu-arm-static .
COPY [ "package.json", "package-lock.json", "/build/" ]
RUN npm install
COPY [ "angular.json", "tsconfig.json", "/build/" ]
COPY [ "src/", "/build/src/" ]
RUN ng build --prod
#--------------#
FROM arm32v7/alpine:3.12
COPY --from=frontend /build/qemu-arm-static /usr/bin
ENV UID=1000 \
GID=1000 \
USER=youtube
RUN addgroup -S $USER -g $GID && adduser -D -S $USER -G $USER -u $UID
RUN apk add --no-cache \
ffmpeg \
npm \
python2 \
su-exec \
&& apk add --no-cache --repository http://dl-cdn.alpinelinux.org/alpine/edge/testing/ \
atomicparsley
WORKDIR /app
COPY --chown=$UID:$GID [ "backend/package.json", "backend/package-lock.json", "/app/" ]
RUN npm install && chown -R $UID:$GID ./
COPY --chown=$UID:$GID --from=frontend [ "/build/backend/public/", "/app/public/" ]
COPY --chown=$UID:$GID [ "/backend/", "/app/" ]
EXPOSE 17442
ENTRYPOINT [ "/app/entrypoint.sh" ]
CMD [ "node", "app.js" ]

27
backend/Dockerfile Normal file
View File

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

29
backend/Dockerfile-armhf Normal file
View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -4,15 +4,17 @@
"url": "http://example.com",
"port": "17442"
},
"Encryption": {
"use-encryption": false,
"cert-file-path": "/etc/letsencrypt/live/example.com/fullchain.pem",
"key-file-path": "/etc/letsencrypt/live/example.com/privkey.pem"
},
"Downloader": {
"path-audio": "audio/",
"path-video": "video/",
"default_file_output": "",
"use_youtubedl_archive": false,
"custom_args": "",
"safe_download_override": false,
"include_thumbnail": true,
"include_metadata": true
"safe_download_override": false
},
"Extra": {
"title_top": "YoutubeDL-Material",
@@ -20,17 +22,13 @@
"allow_quality_select": true,
"download_only_mode": false,
"allow_multi_download_mode": true,
"enable_downloads_manager": true,
"allow_playlist_categorization": true
"enable_downloads_manager": true
},
"API": {
"use_API_key": false,
"API_key": "",
"use_youtube_API": false,
"youtube_API_key": "",
"use_twitch_API": false,
"twitch_API_key": "",
"twitch_auto_download_chat": false
"youtube_API_key": ""
},
"Themes": {
"default_theme": "default",
@@ -39,23 +37,13 @@
"Subscriptions": {
"allow_subscriptions": true,
"subscriptions_base_path": "subscriptions/",
"subscriptions_check_interval": "300",
"redownload_fresh_uploads": false
"subscriptions_check_interval": "300"
},
"Users": {
"base_path": "users/",
"allow_registration": true,
"auth_method": "internal",
"ldap_config": {
"url": "ldap://localhost:389",
"bindDN": "cn=root",
"bindCredentials": "secret",
"searchBase": "ou=passport-ldapauth",
"searchFilter": "(uid={{username}})"
}
"allow_registration": true
},
"Advanced": {
"default_downloader": "youtube-dl",
"use_default_downloading_agent": true,
"custom_downloading_agent": "",
"multi_user_mode": false,

View File

@@ -0,0 +1,56 @@
{
"YoutubeDLMaterial": {
"Host": {
"url": "https://example.com",
"port": "17442"
},
"Encryption": {
"use-encryption": true,
"cert-file-path": "/etc/letsencrypt/live/example.com/fullchain.pem",
"key-file-path": "/etc/letsencrypt/live/example.com/privkey.pem"
},
"Downloader": {
"path-audio": "audio/",
"path-video": "video/",
"use_youtubedl_archive": false,
"custom_args": "",
"safe_download_override": false
},
"Extra": {
"title_top": "YoutubeDL-Material",
"file_manager_enabled": true,
"allow_quality_select": true,
"download_only_mode": false,
"allow_multi_download_mode": true,
"enable_downloads_manager": true
},
"API": {
"use_API_key": false,
"API_key": "",
"use_youtube_API": false,
"youtube_API_key": ""
},
"Themes": {
"default_theme": "default",
"allow_theme_change": true
},
"Subscriptions": {
"allow_subscriptions": true,
"subscriptions_base_path": "subscriptions/",
"subscriptions_check_interval": "300"
},
"Users": {
"base_path": "users/",
"allow_registration": true
},
"Advanced": {
"use_default_downloading_agent": true,
"custom_downloading_agent": "",
"multi_user_mode": false,
"allow_advanced_download": false,
"use_cookies": false,
"jwt_expiration": 86400,
"logger_level": "info"
}
}
}

View File

@@ -9,22 +9,20 @@ var bcrypt = require('bcryptjs');
var LocalStrategy = require('passport-local').Strategy;
var LdapStrategy = require('passport-ldapauth');
var JwtStrategy = require('passport-jwt').Strategy,
ExtractJwt = require('passport-jwt').ExtractJwt;
// other required vars
let logger = null;
let db = null;
let users_db = null;
var users_db = null;
let SERVER_SECRET = null;
let JWT_EXPIRATION = null;
let opts = null;
let saltRounds = null;
exports.initialize = function(input_db, input_users_db, input_logger) {
exports.initialize = function(input_users_db, input_logger) {
setLogger(input_logger)
setDB(input_db, input_users_db);
setDB(input_users_db);
/*************************
* Authentication module
@@ -62,8 +60,7 @@ function setLogger(input_logger) {
logger = input_logger;
}
function setDB(input_db, input_users_db) {
db = input_db;
function setDB(input_users_db) {
users_db = input_users_db;
}
@@ -91,15 +88,26 @@ exports.registerUser = function(req, res) {
return;
}
if (plaintextPassword === "") {
res.sendStatus(400);
logger.error(`Registration failed for user ${userid}. A password must be provided.`);
return;
}
bcrypt.hash(plaintextPassword, saltRounds)
.then(function(hash) {
let new_user = generateUserObject(userid, username, hash);
let new_user = {
name: username,
uid: userid,
passhash: hash,
files: {
audio: [],
video: []
},
playlists: {
audio: [],
video: []
},
subscriptions: [],
created: Date.now(),
role: userid === 'admin' ? 'admin' : 'user',
permissions: [],
permission_overrides: []
};
// check if user exists
if (users_db.get('users').find({uid: userid}).value()) {
// user id is taken!
@@ -145,43 +153,52 @@ exports.registerUser = function(req, res) {
exports.passport.use(new LocalStrategy({
usernameField: 'username',
usernameField: 'userid',
passwordField: 'password'},
async function(username, password, done) {
function(username, password, done) {
const user = users_db.get('users').find({name: username}).value();
if (!user) { logger.error(`User ${username} not found`); return done(null, false); }
if (user.auth_method && user.auth_method !== 'internal') { return done(null, false); }
if (user) {
return done(null, (await bcrypt.compare(password, user.passhash)) ? user : false);
return done(null, bcrypt.compareSync(password, user.passhash) ? user : false);
}
}
));
var getLDAPConfiguration = function(req, callback) {
const ldap_config = config_api.getConfigItem('ytdl_ldap_config');
const opts = {server: ldap_config};
callback(null, opts);
};
exports.passport.use(new LdapStrategy(getLDAPConfiguration,
function(user, done) {
// check if ldap auth is enabled
const ldap_enabled = config_api.getConfigItem('ytdl_auth_method') === 'ldap';
if (!ldap_enabled) return done(null, false);
const user_uid = user.uid;
let db_user = users_db.get('users').find({uid: user_uid}).value();
if (!db_user) {
// generate DB user
let new_user = generateUserObject(user_uid, user_uid, null, 'ldap');
users_db.get('users').push(new_user).write();
db_user = new_user;
logger.verbose(`Generated new user ${user_uid} using LDAP`);
/*passport.use(new BasicStrategy(
function(userid, plainTextPassword, done) {
const user = users_db.get('users').find({name: userid}).value();
if (user) {
var hashedPwd = user.passhash;
return bcrypt.compare(plainTextPassword, hashedPwd);
} else {
return false;
}
return done(null, db_user);
}
));
*/
/*************************************************************
* This is a wrapper for auth.passport.authenticate().
* We use this to change WWW-Authenticate header so
* the browser doesn't pop-up challenge dialog box by default.
* Browser's will pop-up up dialog when status is 401 and
* "WWW-Authenticate:Basic..."
*************************************************************/
/*
exports.authenticateViaPassport = function(req, res, next) {
exports.passport.authenticate('basic',{session:false},
function(err, user, info) {
if(!user){
res.set('WWW-Authenticate', 'x'+info); // change to xBasic
res.status(401).send('Invalid Authentication');
} else {
req.user = user;
next();
}
}
)(req, res, next);
};
*/
/**********************************
* Generating/Signing a JWT token
@@ -234,13 +251,15 @@ exports.ensureAuthenticatedElseError = function(req, res, next) {
// change password
exports.changeUserPassword = async function(user_uid, new_pass) {
try {
const hash = await bcrypt.hash(new_pass, saltRounds);
users_db.get('users').find({uid: user_uid}).assign({passhash: hash}).write();
return true;
} catch (err) {
return false;
}
return new Promise(resolve => {
bcrypt.hash(new_pass, saltRounds)
.then(function(hash) {
users_db.get('users').find({uid: user_uid}).assign({passhash: hash}).write();
resolve(true);
}).catch(err => {
resolve(false);
});
});
}
// change user permissions
@@ -285,70 +304,66 @@ exports.adminExists = function() {
exports.getUserVideos = function(user_uid, type) {
const user = users_db.get('users').find({uid: user_uid}).value();
return type ? user['files'].filter(file => file.isAudio === (type === 'audio')) : user['files'];
return user['files'][type];
}
exports.getUserVideo = function(user_uid, file_uid, requireSharing = false) {
let file = users_db.get('users').find({uid: user_uid}).get(`files`).find({uid: file_uid}).value();
exports.getUserVideo = function(user_uid, file_uid, type, requireSharing = false, reqBody = null) {
// check if it's a subscription first
let file = null;
if (reqBody && reqBody.subscriptionName) {
file = users_db.get('users').find({uid: user_uid}).get('subscriptions').find({name: reqBody.subscriptionName}).get('videos').find({id: reqBody.fileNames}).value();
} else {
if (!type) {
file = users_db.get('users').find({uid: user_uid}).get(`files.audio`).find({uid: file_uid}).value();
if (!file) {
file = users_db.get('users').find({uid: user_uid}).get(`files.video`).find({uid: file_uid}).value();
if (file) type = 'video';
} else {
type = 'audio';
}
}
if (!file && type) file = users_db.get('users').find({uid: user_uid}).get(`files.${type}`).find({uid: file_uid}).value();
}
// prevent unauthorized users from accessing the file info
if (file && !file['sharingEnabled'] && requireSharing) file = null;
if (file && requireSharing && !file['sharingEnabled']) file = null;
return file;
}
exports.addPlaylist = function(user_uid, new_playlist) {
users_db.get('users').find({uid: user_uid}).get(`playlists`).push(new_playlist).write();
exports.addPlaylist = function(user_uid, new_playlist, type) {
users_db.get('users').find({uid: user_uid}).get(`playlists.${type}`).push(new_playlist).write();
return true;
}
exports.updatePlaylistFiles = function(user_uid, playlistID, new_filenames) {
users_db.get('users').find({uid: user_uid}).get(`playlists`).find({id: playlistID}).assign({fileNames: new_filenames});
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;
}
exports.removePlaylist = function(user_uid, playlistID) {
users_db.get('users').find({uid: user_uid}).get(`playlists`).remove({id: playlistID}).write();
exports.removePlaylist = function(user_uid, playlistID, type) {
users_db.get('users').find({uid: user_uid}).get(`playlists.${type}`).remove({id: playlistID}).write();
return true;
}
exports.getUserPlaylists = function(user_uid, user_files = null) {
exports.getUserPlaylists = function(user_uid, type) {
const user = users_db.get('users').find({uid: user_uid}).value();
const playlists = JSON.parse(JSON.stringify(user['playlists']));
const categories = db.get('categories').value();
if (categories && user_files) {
categories.forEach(category => {
const audio_files = user_files && user_files.filter(file => file.category && file.category.uid === category.uid && file.isAudio);
const video_files = user_files && user_files.filter(file => file.category && file.category.uid === category.uid && !file.isAudio);
if (audio_files && audio_files.length > 0) {
playlists.push({
name: category['name'],
thumbnailURL: audio_files[0].thumbnailURL,
thumbnailPath: audio_files[0].thumbnailPath,
fileNames: audio_files.map(file => file.id),
type: 'audio',
uid: user_uid,
auto: true
});
}
if (video_files && video_files.length > 0) {
playlists.push({
name: category['name'],
thumbnailURL: video_files[0].thumbnailURL,
thumbnailPath: video_files[0].thumbnailPath,
fileNames: video_files.map(file => file.id),
type: 'video',
uid: user_uid,
auto: true
});
}
});
}
return playlists;
return user['playlists'][type];
}
exports.getUserPlaylist = function(user_uid, playlistID, requireSharing = false) {
let playlist = users_db.get('users').find({uid: user_uid}).get(`playlists`).find({id: playlistID}).value();
exports.getUserPlaylist = function(user_uid, playlistID, type, requireSharing = false) {
let playlist = null;
if (!type) {
playlist = users_db.get('users').find({uid: user_uid}).get(`playlists.audio`).find({id: playlistID}).value();
if (!playlist) {
playlist = users_db.get('users').find({uid: user_uid}).get(`playlists.video`).find({id: playlistID}).value();
if (playlist) type = 'video';
} else {
type = 'audio';
}
}
if (!playlist) playlist = users_db.get('users').find({uid: user_uid}).get(`playlists.${type}`).find({id: playlistID}).value();
// prevent unauthorized users from accessing the file info
if (requireSharing && !playlist['sharingEnabled']) playlist = null;
@@ -356,22 +371,21 @@ exports.getUserPlaylist = function(user_uid, playlistID, requireSharing = false)
return playlist;
}
exports.registerUserFile = function(user_uid, file_object) {
users_db.get('users').find({uid: user_uid}).get(`files`)
exports.registerUserFile = function(user_uid, file_object, type) {
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`)
users_db.get('users').find({uid: user_uid}).get(`files.${type}`)
.push(file_object)
.write();
}
exports.deleteUserFile = async function(user_uid, file_uid, blacklistMode = false) {
exports.deleteUserFile = function(user_uid, file_uid, type, blacklistMode = false) {
let success = false;
const file_obj = users_db.get('users').find({uid: user_uid}).get(`files`).find({uid: file_uid}).value();
const file_obj = users_db.get('users').find({uid: user_uid}).get(`files.${type}`).find({uid: file_uid}).value();
if (file_obj) {
const type = file_obj.isAudio ? 'audio' : 'video';
const usersFileFolder = config_api.getConfigItem('ytdl_users_base_path');
const ext = type === 'audio' ? '.mp3' : '.mp4';
@@ -387,24 +401,24 @@ exports.deleteUserFile = async function(user_uid, file_uid, blacklistMode = fals
}
const full_path = path.join(usersFileFolder, user_uid, type, file_obj.id + ext);
users_db.get('users').find({uid: user_uid}).get(`files`)
users_db.get('users').find({uid: user_uid}).get(`files.${type}`)
.remove({
uid: file_uid
}).write();
if (await fs.pathExists(full_path)) {
if (fs.existsSync(full_path)) {
// remove json and file
const json_path = path.join(usersFileFolder, user_uid, type, file_obj.id + '.info.json');
const alternate_json_path = path.join(usersFileFolder, user_uid, type, file_obj.id + ext + '.info.json');
let youtube_id = null;
if (await fs.pathExists(json_path)) {
youtube_id = await fs.readJSON(json_path).id;
await fs.unlink(json_path);
} else if (await fs.pathExists(alternate_json_path)) {
youtube_id = await fs.readJSON(alternate_json_path).id;
await fs.unlink(alternate_json_path);
if (fs.existsSync(json_path)) {
youtube_id = fs.readJSONSync(json_path).id;
fs.unlinkSync(json_path);
} else if (fs.existsSync(alternate_json_path)) {
youtube_id = fs.readJSONSync(alternate_json_path).id;
fs.unlinkSync(alternate_json_path);
}
await fs.unlink(full_path);
fs.unlinkSync(full_path);
// do archive stuff
@@ -413,17 +427,17 @@ exports.deleteUserFile = async function(user_uid, file_uid, blacklistMode = fals
const archive_path = path.join(usersFileFolder, user_uid, 'archives', `archive_${type}.txt`);
// use subscriptions API to remove video from the archive file, and write it to the blacklist
if (await fs.pathExists(archive_path)) {
const line = youtube_id ? await subscriptions_api.removeIDFromArchive(archive_path, youtube_id) : null;
if (fs.existsSync(archive_path)) {
const line = youtube_id ? subscriptions_api.removeIDFromArchive(archive_path, youtube_id) : null;
if (blacklistMode && line) {
let blacklistPath = path.join(usersFileFolder, user_uid, 'archives', `blacklist_${type}.txt`);
// adds newline to the beginning of the line
line = '\n' + line;
await fs.appendFile(blacklistPath, line);
fs.appendFileSync(blacklistPath, line);
}
} else {
logger.info(`Could not find archive file for ${type} files. Creating...`);
await fs.ensureFile(archive_path);
fs.ensureFileSync(archive_path);
}
}
}
@@ -436,11 +450,11 @@ exports.deleteUserFile = async function(user_uid, file_uid, blacklistMode = fals
return success;
}
exports.changeSharingMode = function(user_uid, file_uid, is_playlist, enabled) {
exports.changeSharingMode = function(user_uid, file_uid, type, is_playlist, enabled) {
let success = false;
const user_db_obj = users_db.get('users').find({uid: user_uid});
if (user_db_obj.value()) {
const file_db_obj = is_playlist ? user_db_obj.get(`playlists`).find({id: file_uid}) : user_db_obj.get(`files`).find({uid: file_uid});
const file_db_obj = is_playlist ? user_db_obj.get(`playlists.${type}`).find({id: file_uid}) : user_db_obj.get(`files.${type}`).find({uid: file_uid});
if (file_db_obj.value()) {
success = true;
file_db_obj.assign({sharingEnabled: enabled}).write();
@@ -530,20 +544,3 @@ function getToken(queryParams) {
return null;
}
};
function generateUserObject(userid, username, hash, auth_method = 'internal') {
let new_user = {
name: username,
uid: userid,
passhash: auth_method === 'internal' ? hash : null,
files: [],
playlists: [],
subscriptions: [],
created: Date.now(),
role: userid === 'admin' && auth_method === 'internal' ? 'admin' : 'user',
permissions: [],
permission_overrides: [],
auth_method: auth_method
};
return new_user;
}

View File

@@ -1,129 +0,0 @@
const config_api = require('./config');
var logger = null;
var db = null;
var users_db = null;
var db_api = null;
function setDB(input_db, input_users_db, input_db_api) { db = input_db; users_db = input_users_db; db_api = input_db_api }
function setLogger(input_logger) { logger = input_logger; }
function initialize(input_db, input_users_db, input_logger, input_db_api) {
setDB(input_db, input_users_db, input_db_api);
setLogger(input_logger);
}
/*
Categories:
Categories are a way to organize videos based on dynamic rules set by the user. Categories are universal (so not per-user).
Categories, besides rules, have an optional custom output. This custom output can help users create their
desired directory structure.
Rules:
A category rule consists of a property, a comparison, and a value. For example, "uploader includes 'VEVO'"
Rules are stored as an object with the above fields. In addition to those fields, it also has a preceding_operator, which
is either OR or AND, and signifies whether the rule should be ANDed with the previous rules, or just ORed. For the first
rule, this field is null.
Ex. (title includes 'Rihanna' OR title includes 'Beyonce' AND uploader includes 'VEVO')
*/
async function categorize(file_jsons) {
// to make the logic easier, let's assume the file metadata is an array
if (!Array.isArray(file_jsons)) file_jsons = [file_jsons];
let selected_category = null;
const categories = getCategories();
if (!categories) {
logger.warn('Categories could not be found. Initializing categories...');
db.assign({categories: []}).write();
return null;
}
for (let i = 0; i < file_jsons.length; i++) {
const file_json = file_jsons[i];
for (let j = 0; j < categories.length; j++) {
const category = categories[i];
const rules = category['rules'];
// if rules for current category apply, then that is the selected category
if (applyCategoryRules(file_json, rules, category['name'])) {
selected_category = category;
logger.verbose(`Selected category ${category['name']} for ${file_json['webpage_url']}`);
return selected_category;
}
}
}
return selected_category;
}
function getCategories() {
const categories = db.get('categories').value();
return categories ? categories : null;
}
function applyCategoryRules(file_json, rules, category_name) {
let rules_apply = false;
for (let i = 0; i < rules.length; i++) {
const rule = rules[i];
let rule_applies = null;
let preceding_operator = rule['preceding_operator'];
switch (rule['comparator']) {
case 'includes':
rule_applies = file_json[rule['property']].includes(rule['value']);
break;
case 'not_includes':
rule_applies = !(file_json[rule['property']].includes(rule['value']));
break;
case 'equals':
rule_applies = file_json[rule['property']] === rule['value'];
break;
case 'not_equals':
rule_applies = file_json[rule['property']] !== rule['value'];
break;
default:
logger.warn(`Invalid comparison used for category ${category_name}`)
break;
}
// OR the first rule with rules_apply, which will be initially false
if (i === 0) preceding_operator = 'or';
// update rules_apply based on current rule
if (preceding_operator === 'or')
rules_apply = rules_apply || rule_applies;
else
rules_apply = rules_apply && rule_applies;
}
return rules_apply;
}
async function addTagToVideo(tag, video, user_uid) {
// TODO: Implement
}
async function removeTagFromVideo(tag, video, user_uid) {
// TODO: Implement
}
// adds tag to list of existing tags (used for tag suggestions)
async function addTagToExistingTags(tag) {
const existing_tags = db.get('tags').value();
if (!existing_tags.includes(tag)) {
db.get('tags').push(tag).write();
}
}
module.exports = {
initialize: initialize,
categorize: categorize,
}

View File

@@ -157,7 +157,7 @@ function setConfigItems(items) {
function globalArgsRequiresSafeDownload() {
const globalArgs = getConfigItem('ytdl_custom_args').split(',,');
const argsThatRequireSafeDownload = ['--write-sub', '--write-srt', '--proxy'];
const argsThatRequireSafeDownload = ['--write-sub', '--write-srt'];
const failedArgs = globalArgs.filter(arg => argsThatRequireSafeDownload.includes(arg));
return failedArgs && failedArgs.length > 0;
}
@@ -181,15 +181,17 @@ DEFAULT_CONFIG = {
"url": "http://example.com",
"port": "17442"
},
"Encryption": {
"use-encryption": false,
"cert-file-path": "/etc/letsencrypt/live/example.com/fullchain.pem",
"key-file-path": "/etc/letsencrypt/live/example.com/privkey.pem"
},
"Downloader": {
"path-audio": "audio/",
"path-video": "video/",
"default_file_output": "",
"use_youtubedl_archive": false,
"custom_args": "",
"safe_download_override": false,
"include_thumbnail": true,
"include_metadata": true
"safe_download_override": false
},
"Extra": {
"title_top": "YoutubeDL-Material",
@@ -197,17 +199,13 @@ DEFAULT_CONFIG = {
"allow_quality_select": true,
"download_only_mode": false,
"allow_multi_download_mode": true,
"enable_downloads_manager": true,
"allow_playlist_categorization": true
"enable_downloads_manager": true
},
"API": {
"use_API_key": false,
"API_key": "",
"use_youtube_API": false,
"youtube_API_key": "",
"use_twitch_API": false,
"twitch_API_key": "",
"twitch_auto_download_chat": false
"youtube_API_key": ""
},
"Themes": {
"default_theme": "default",
@@ -216,23 +214,13 @@ DEFAULT_CONFIG = {
"Subscriptions": {
"allow_subscriptions": true,
"subscriptions_base_path": "subscriptions/",
"subscriptions_check_interval": "300",
"redownload_fresh_uploads": false
"subscriptions_check_interval": "300"
},
"Users": {
"base_path": "users/",
"allow_registration": true,
"auth_method": "internal",
"ldap_config": {
"url": "ldap://localhost:389",
"bindDN": "cn=root",
"bindCredentials": "secret",
"searchBase": "ou=passport-ldapauth",
"searchFilter": "(uid={{username}})"
}
"allow_registration": true
},
"Advanced": {
"default_downloader": "youtube-dl",
"use_default_downloading_agent": true,
"custom_downloading_agent": "",
"multi_user_mode": false,

View File

@@ -9,6 +9,20 @@ let CONFIG_ITEMS = {
'path': 'YoutubeDLMaterial.Host.port'
},
// Encryption
'ytdl_use_encryption': {
'key': 'ytdl_use_encryption',
'path': 'YoutubeDLMaterial.Encryption.use-encryption'
},
'ytdl_cert_file_path': {
'key': 'ytdl_cert_file_path',
'path': 'YoutubeDLMaterial.Encryption.cert-file-path'
},
'ytdl_key_file_path': {
'key': 'ytdl_key_file_path',
'path': 'YoutubeDLMaterial.Encryption.key-file-path'
},
// Downloader
'ytdl_audio_folder_path': {
'key': 'ytdl_audio_folder_path',
@@ -18,10 +32,6 @@ let CONFIG_ITEMS = {
'key': 'ytdl_video_folder_path',
'path': 'YoutubeDLMaterial.Downloader.path-video'
},
'ytdl_default_file_output': {
'key': 'ytdl_default_file_output',
'path': 'YoutubeDLMaterial.Downloader.default_file_output'
},
'ytdl_use_youtubedl_archive': {
'key': 'ytdl_use_youtubedl_archive',
'path': 'YoutubeDLMaterial.Downloader.use_youtubedl_archive'
@@ -34,14 +44,6 @@ let CONFIG_ITEMS = {
'key': 'ytdl_safe_download_override',
'path': 'YoutubeDLMaterial.Downloader.safe_download_override'
},
'ytdl_include_thumbnail': {
'key': 'ytdl_include_thumbnail',
'path': 'YoutubeDLMaterial.Downloader.include_thumbnail'
},
'ytdl_include_metadata': {
'key': 'ytdl_include_metadata',
'path': 'YoutubeDLMaterial.Downloader.include_metadata'
},
// Extra
'ytdl_title_top': {
@@ -68,10 +70,6 @@ let CONFIG_ITEMS = {
'key': 'ytdl_enable_downloads_manager',
'path': 'YoutubeDLMaterial.Extra.enable_downloads_manager'
},
'ytdl_allow_playlist_categorization': {
'key': 'ytdl_allow_playlist_categorization',
'path': 'YoutubeDLMaterial.Extra.allow_playlist_categorization'
},
// API
'ytdl_use_api_key': {
@@ -90,18 +88,6 @@ let CONFIG_ITEMS = {
'key': 'ytdl_youtube_api_key',
'path': 'YoutubeDLMaterial.API.youtube_API_key'
},
'ytdl_use_twitch_api': {
'key': 'ytdl_use_twitch_api',
'path': 'YoutubeDLMaterial.API.use_twitch_API'
},
'ytdl_twitch_api_key': {
'key': 'ytdl_twitch_api_key',
'path': 'YoutubeDLMaterial.API.twitch_API_key'
},
'ytdl_twitch_auto_download_chat': {
'key': 'ytdl_twitch_auto_download_chat',
'path': 'YoutubeDLMaterial.API.twitch_auto_download_chat'
},
// Themes
'ytdl_default_theme': {
@@ -130,10 +116,6 @@ let CONFIG_ITEMS = {
'key': 'ytdl_subscriptions_check_interval',
'path': 'YoutubeDLMaterial.Subscriptions.subscriptions_check_interval'
},
'ytdl_subscriptions_redownload_fresh_uploads': {
'key': 'ytdl_subscriptions_redownload_fresh_uploads',
'path': 'YoutubeDLMaterial.Subscriptions.redownload_fresh_uploads'
},
// Users
'ytdl_users_base_path': {
@@ -144,20 +126,8 @@ let CONFIG_ITEMS = {
'key': 'ytdl_allow_registration',
'path': 'YoutubeDLMaterial.Users.allow_registration'
},
'ytdl_auth_method': {
'key': 'ytdl_auth_method',
'path': 'YoutubeDLMaterial.Users.auth_method'
},
'ytdl_ldap_config': {
'key': 'ytdl_ldap_config',
'path': 'YoutubeDLMaterial.Users.ldap_config'
},
// Advanced
'ytdl_default_downloader': {
'key': 'ytdl_default_downloader',
'path': 'YoutubeDLMaterial.Advanced.default_downloader'
},
'ytdl_use_default_downloading_agent': {
'key': 'ytdl_use_default_downloading_agent',
'path': 'YoutubeDLMaterial.Advanced.use_default_downloading_agent'
@@ -200,5 +170,5 @@ AVAILABLE_PERMISSIONS = [
module.exports = {
CONFIG_ITEMS: CONFIG_ITEMS,
AVAILABLE_PERMISSIONS: AVAILABLE_PERMISSIONS,
CURRENT_VERSION: 'v4.2'
CURRENT_VERSION: 'v4.1'
}

View File

@@ -15,10 +15,9 @@ function initialize(input_db, input_users_db, input_logger) {
setLogger(input_logger);
}
function registerFileDB(file_path, type, multiUserMode = null, sub = null, customPath = null, category = null) {
let db_path = null;
const file_id = utils.removeFileExtension(file_path);
const file_object = generateFileObject(file_id, type, customPath || multiUserMode && multiUserMode.file_path, sub);
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;
@@ -26,50 +25,49 @@ function registerFileDB(file_path, type, multiUserMode = null, sub = null, custo
utils.fixVideoMetadataPerms(file_id, type, multiUserMode && multiUserMode.file_path);
// add thumbnail path
file_object['thumbnailPath'] = utils.getDownloadedThumbnail(file_id, type, customPath || multiUserMode && multiUserMode.file_path);
// if category exists, only include essential info
if (category) file_object['category'] = {name: category['name'], uid: category['uid']};
if (!sub) {
if (multiUserMode) {
const user_uid = multiUserMode.user;
db_path = users_db.get('users').find({uid: user_uid}).get(`files`);
} else {
db_path = db.get(`files`);
}
} else {
if (multiUserMode) {
const user_uid = multiUserMode.user;
db_path = users_db.get('users').find({uid: user_uid}).get('subscriptions').find({id: sub.id}).get('videos');
} else {
db_path = db.get('subscriptions').find({id: sub.id}).get('videos');
}
}
const file_uid = registerFileDBManual(db_path, file_object);
// remove metadata JSON if needed
if (!config_api.getConfigItem('ytdl_include_metadata')) {
utils.deleteJSONFile(file_id, type, multiUserMode && multiUserMode.file_path)
}
return file_uid;
}
function registerFileDBManual(db_path, file_object) {
// add additional info
file_object['uid'] = uuid();
file_object['registered'] = Date.now();
path_object = path.parse(file_object['path']);
file_object['path'] = path.format(path_object);
// remove duplicate(s)
db_path.remove({path: file_object['path']}).write();
if (!sub) {
if (multiUserMode) {
const user_uid = multiUserMode.user;
users_db.get('users').find({uid: user_uid}).get(`files.${type}`)
.remove({
path: file_object['path']
}).write();
users_db.get('users').find({uid: user_uid}).get(`files.${type}`)
.push(file_object)
.write();
} else {
// remove existing video if overwriting
db.get(`files.${type}`)
.remove({
path: file_object['path']
}).write();
db.get(`files.${type}`)
.push(file_object)
.write();
}
} else {
sub_db = null;
if (multiUserMode) {
const user_uid = multiUserMode.user;
sub_db = users_db.get('users').find({uid: user_uid}).get('subscriptions').find({id: sub.id});
} else {
sub_db = db.get('subscriptions').find({id: sub.id});
}
if (sub_db.get('videos').find({id: file_object.id}).value()) {
logger.verbose(`Subscription video ${file_object.id} already exists, skipping DB registration.`);
return null;
}
sub_db.get('videos').push(file_object).write();
}
// add new file to db
db_path.push(file_object).write();
return file_object['uid'];
}
@@ -97,18 +95,18 @@ function generateFileObject(id, type, customPath = null, sub = null) {
var thumbnail = jsonobj.thumbnail;
var duration = jsonobj.duration;
var isaudio = type === 'audio';
var description = jsonobj.description;
var file_obj = new utils.File(id, title, thumbnail, isaudio, duration, url, uploader, size, file_path, upload_date, description, jsonobj.view_count, jsonobj.height, jsonobj.abr);
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`).find({id: playlistID});
db_loc = users_db.get('users').find({uid: user_uid}).get(`playlists.${type}`).find({id: playlistID});
} else {
db_loc = db.get(`playlists`).find({id: playlistID});
db_loc = db.get(`playlists.${type}`).find({id: playlistID});
}
db_loc.assign(playlist).write();
return true;
@@ -118,118 +116,8 @@ function getAppendedBasePathSub(sub, base_path) {
return path.join(base_path, (sub.isPlaylist ? 'playlists/' : 'channels/'), sub.name);
}
function getFileDirectoriesAndDBs() {
let dirs_to_check = [];
let subscriptions_to_check = [];
const subscriptions_base_path = config_api.getConfigItem('ytdl_subscriptions_base_path'); // only for single-user mode
const multi_user_mode = config_api.getConfigItem('ytdl_multi_user_mode');
const usersFileFolder = config_api.getConfigItem('ytdl_users_base_path');
const subscriptions_enabled = config_api.getConfigItem('ytdl_allow_subscriptions');
if (multi_user_mode) {
let users = users_db.get('users').value();
for (let i = 0; i < users.length; i++) {
const user = users[i];
if (subscriptions_enabled) subscriptions_to_check = subscriptions_to_check.concat(users[i]['subscriptions']);
// add user's audio dir to check list
dirs_to_check.push({
basePath: path.join(usersFileFolder, user.uid, 'audio'),
dbPath: users_db.get('users').find({uid: user.uid}).get('files'),
type: 'audio'
});
// add user's video dir to check list
dirs_to_check.push({
basePath: path.join(usersFileFolder, user.uid, 'video'),
dbPath: users_db.get('users').find({uid: user.uid}).get('files'),
type: 'video'
});
}
} else {
const audioFolderPath = config_api.getConfigItem('ytdl_audio_folder_path');
const videoFolderPath = config_api.getConfigItem('ytdl_video_folder_path');
const subscriptions = db.get('subscriptions').value();
if (subscriptions_enabled && subscriptions) subscriptions_to_check = subscriptions_to_check.concat(subscriptions);
// add audio dir to check list
dirs_to_check.push({
basePath: audioFolderPath,
dbPath: db.get('files'),
type: 'audio'
});
// add video dir to check list
dirs_to_check.push({
basePath: videoFolderPath,
dbPath: db.get('files'),
type: 'video'
});
}
// add subscriptions to check list
for (let i = 0; i < subscriptions_to_check.length; i++) {
let subscription_to_check = subscriptions_to_check[i];
if (!subscription_to_check.name) {
// TODO: Remove subscription as it'll never complete
continue;
}
dirs_to_check.push({
basePath: multi_user_mode ? path.join(usersFileFolder, subscription_to_check.user_uid, 'subscriptions', subscription_to_check.isPlaylist ? 'playlists/' : 'channels/', subscription_to_check.name)
: path.join(subscriptions_base_path, subscription_to_check.isPlaylist ? 'playlists/' : 'channels/', subscription_to_check.name),
dbPath: multi_user_mode ? users_db.get('users').find({uid: subscription_to_check.user_uid}).get('subscriptions').find({id: subscription_to_check.id}).get('videos')
: db.get('subscriptions').find({id: subscription_to_check.id}).get('videos'),
type: subscription_to_check.type
});
}
return dirs_to_check;
}
async function importUnregisteredFiles() {
const dirs_to_check = getFileDirectoriesAndDBs();
// run through check list and check each file to see if it's missing from the db
for (const dir_to_check of dirs_to_check) {
// recursively get all files in dir's path
const files = await utils.getDownloadedFilesByType(dir_to_check.basePath, dir_to_check.type);
files.forEach(file => {
// check if file exists in db, if not add it
const file_is_registered = !!(dir_to_check.dbPath.find({id: file.id}).value())
if (!file_is_registered) {
// add additional info
registerFileDBManual(dir_to_check.dbPath, file);
logger.verbose(`Added discovered file to the database: ${file.id}`);
}
});
}
}
async function getVideo(file_uid, uuid, sub_id) {
const base_db_path = uuid ? users_db.get('users').find({uid: uuid}) : db;
const sub_db_path = sub_id ? base_db_path.get('subscriptions').find({id: sub_id}).get('videos') : base_db_path.get('files');
return sub_db_path.find({uid: file_uid}).value();
}
async function setVideoProperty(file_uid, assignment_obj, uuid, sub_id) {
const base_db_path = uuid ? users_db.get('users').find({uid: uuid}) : db;
const sub_db_path = sub_id ? base_db_path.get('subscriptions').find({id: sub_id}).get('videos') : base_db_path.get('files');
const file_db_path = sub_db_path.find({uid: file_uid});
if (!(file_db_path.value())) {
logger.error(`Failed to find file with uid ${file_uid}`);
}
sub_db_path.find({uid: file_uid}).assign(assignment_obj).write();
}
module.exports = {
initialize: initialize,
registerFileDB: registerFileDB,
updatePlaylist: updatePlaylist,
getFileDirectoriesAndDBs: getFileDirectoriesAndDBs,
importUnregisteredFiles: importUnregisteredFiles,
getVideo: getVideo,
setVideoProperty: setVideoProperty
updatePlaylist: updatePlaylist
}

View File

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

4
backend/hooks/pre_build Normal file
View File

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

View File

@@ -4,89 +4,6 @@
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"@types/body-parser": {
"version": "1.19.0",
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.0.tgz",
"integrity": "sha512-W98JrE0j2K78swW4ukqMleo8R7h/pFETjM2DQ90MF6XK2i4LO4W3gQ71Lt4w3bfm2EvVSyWHplECvB5sK22yFQ==",
"requires": {
"@types/connect": "*",
"@types/node": "*"
}
},
"@types/connect": {
"version": "3.4.33",
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.33.tgz",
"integrity": "sha512-2+FrkXY4zllzTNfJth7jOqEHC+enpLeGslEhpnTAkg21GkRrWV4SsAtqchtT4YS9/nODBU2/ZfsBY2X4J/dX7A==",
"requires": {
"@types/node": "*"
}
},
"@types/express": {
"version": "4.17.7",
"resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.7.tgz",
"integrity": "sha512-dCOT5lcmV/uC2J9k0rPafATeeyz+99xTt54ReX11/LObZgfzJqZNcW27zGhYyX+9iSEGXGt5qLPwRSvBZcLvtQ==",
"requires": {
"@types/body-parser": "*",
"@types/express-serve-static-core": "*",
"@types/qs": "*",
"@types/serve-static": "*"
}
},
"@types/express-serve-static-core": {
"version": "4.17.9",
"resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.9.tgz",
"integrity": "sha512-DG0BYg6yO+ePW+XoDENYz8zhNGC3jDDEpComMYn7WJc4mY1Us8Rw9ax2YhJXxpyk2SF47PQAoQ0YyVT1a0bEkA==",
"requires": {
"@types/node": "*",
"@types/qs": "*",
"@types/range-parser": "*"
}
},
"@types/ldapjs": {
"version": "1.0.9",
"resolved": "https://registry.npmjs.org/@types/ldapjs/-/ldapjs-1.0.9.tgz",
"integrity": "sha512-3PvY7Drp1zoLbcGlothCAkoc5o6Jp9KvUPwHadlHyKp3yPvyeIh7w2zQc9UXMzgDRkoeGXUEODtbEs5XCh9ZyA==",
"requires": {
"@types/node": "*"
}
},
"@types/mime": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-2.0.3.tgz",
"integrity": "sha512-Jus9s4CDbqwocc5pOAnh8ShfrnMcPHuJYzVcSUU7lrh8Ni5HuIqX3oilL86p3dlTrk0LzHRCgA/GQ7uNCw6l2Q=="
},
"@types/node": {
"version": "14.6.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.6.0.tgz",
"integrity": "sha512-mikldZQitV94akrc4sCcSjtJfsTKt4p+e/s0AGscVA6XArQ9kFclP+ZiYUMnq987rc6QlYxXv/EivqlfSLxpKA=="
},
"@types/passport": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@types/passport/-/passport-1.0.4.tgz",
"integrity": "sha512-h5OfAbfBBYSzjeU0GTuuqYEk9McTgWeGQql9g3gUw2/NNCfD7VgExVRYJVVeU13Twn202Mvk9BT0bUrl30sEgA==",
"requires": {
"@types/express": "*"
}
},
"@types/qs": {
"version": "6.9.4",
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.4.tgz",
"integrity": "sha512-+wYo+L6ZF6BMoEjtf8zB2esQsqdV6WsjRK/GP9WOgLPrq87PbNWgIxS76dS5uvl/QXtHGakZmwTznIfcPXcKlQ=="
},
"@types/range-parser": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.3.tgz",
"integrity": "sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA=="
},
"@types/serve-static": {
"version": "1.13.5",
"resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.5.tgz",
"integrity": "sha512-6M64P58N+OXjU432WoLLBQxbA0LRGBCRm7aAGQJ+SMC1IMl0dgRVi9EFfoDcS2a7Xogygk/eGN94CfwU9UF7UQ==",
"requires": {
"@types/express-serve-static-core": "*",
"@types/mime": "*"
}
},
"abbrev": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
@@ -252,22 +169,6 @@
"resolved": "https://registry.npmjs.org/aws4/-/aws4-1.9.1.tgz",
"integrity": "sha512-wMHVg2EOHaMRxbzgFJ9gtjOOCrI80OHLG14rxi28XwOW8ux6IiEbRCGGGqCtdAIg4FQCbW20k9RsT4y3gJlFug=="
},
"axios": {
"version": "0.21.1",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.21.1.tgz",
"integrity": "sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==",
"requires": {
"follow-redirects": "^1.10.0"
}
},
"backoff": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/backoff/-/backoff-2.5.0.tgz",
"integrity": "sha1-9hbtqdPktmuMp/ynn2lXIsX44m8=",
"requires": {
"precond": "0.2"
}
},
"balanced-match": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
@@ -414,17 +315,6 @@
"resolved": "https://registry.npmjs.org/buffers/-/buffers-0.1.1.tgz",
"integrity": "sha1-skV5w77U1tOWru5tmorn9Ugqt7s="
},
"bunyan": {
"version": "1.8.14",
"resolved": "https://registry.npmjs.org/bunyan/-/bunyan-1.8.14.tgz",
"integrity": "sha512-LlahJUxXzZLuw/hetUQJmRgZ1LF6+cr5TPpRj6jf327AsiIq2jhYEH4oqUUkVKTor+9w2BT3oxVwhzE5lw9tcg==",
"requires": {
"dtrace-provider": "~0.8",
"moment": "^2.19.3",
"mv": "~2",
"safe-json-stringify": "~1"
}
},
"busboy": {
"version": "0.2.14",
"resolved": "https://registry.npmjs.org/busboy/-/busboy-0.2.14.tgz",
@@ -849,15 +739,6 @@
"is-obj": "^1.0.0"
}
},
"dtrace-provider": {
"version": "0.8.8",
"resolved": "https://registry.npmjs.org/dtrace-provider/-/dtrace-provider-0.8.8.tgz",
"integrity": "sha512-b7Z7cNtHPhH9EJhNNbbeqTcXB8LGFFZhq1PGgEvpeHlzd36bhbdTWoE/Ba/YguqpBSlAPKnARWhVlhunCMwfxg==",
"optional": true,
"requires": {
"nan": "^2.14.0"
}
},
"duplexer2": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz",
@@ -1080,11 +961,6 @@
}
}
},
"follow-redirects": {
"version": "1.13.1",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.13.1.tgz",
"integrity": "sha512-SSG5xmZh1mkPGyKzjZP8zLjltIfpW32Y5QpdNJyjcfGxK3qo3NDDkZOZSFiGn1A6SclQxY9GzEwAHQ3dmYRWpg=="
},
"forever-agent": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz",
@@ -1596,72 +1472,6 @@
}
}
},
"ldap-filter": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/ldap-filter/-/ldap-filter-0.2.2.tgz",
"integrity": "sha1-8rhCvguG2jNSeYUFsx68rlkNd9A=",
"requires": {
"assert-plus": "0.1.5"
},
"dependencies": {
"assert-plus": {
"version": "0.1.5",
"resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.1.5.tgz",
"integrity": "sha1-7nQAlBMALYTOxyGcasgRgS5yMWA="
}
}
},
"ldapauth-fork": {
"version": "4.3.3",
"resolved": "https://registry.npmjs.org/ldapauth-fork/-/ldapauth-fork-4.3.3.tgz",
"integrity": "sha512-x76VpQ5ZqkwAJmqwcD6KIwDiNEbgIGIPGwC/eA17e1dxWhlTx36w0DlLOFwjTuZ2iuaLTsZsUprlVqvSlwc/1Q==",
"requires": {
"@types/ldapjs": "^1.0.0",
"@types/node": "*",
"bcryptjs": "^2.4.0",
"ldapjs": "^1.0.2",
"lru-cache": "^5.1.1"
},
"dependencies": {
"lru-cache": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
"integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
"requires": {
"yallist": "^3.0.2"
}
},
"yallist": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="
}
}
},
"ldapjs": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/ldapjs/-/ldapjs-1.0.2.tgz",
"integrity": "sha1-VE/3Ayt7g8aPBwEyjZKXqmlDQPk=",
"requires": {
"asn1": "0.2.3",
"assert-plus": "^1.0.0",
"backoff": "^2.5.0",
"bunyan": "^1.8.3",
"dashdash": "^1.14.0",
"dtrace-provider": "~0.8",
"ldap-filter": "0.2.2",
"once": "^1.4.0",
"vasync": "^1.6.4",
"verror": "^1.8.1"
},
"dependencies": {
"asn1": {
"version": "0.2.3",
"resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz",
"integrity": "sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y="
}
}
},
"listenercount": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/listenercount/-/listenercount-1.0.1.tgz",
@@ -1862,11 +1672,6 @@
"minimist": "^1.2.5"
}
},
"moment": {
"version": "2.29.1",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz",
"integrity": "sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ=="
},
"ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
@@ -1912,41 +1717,6 @@
}
}
},
"mv": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/mv/-/mv-2.1.1.tgz",
"integrity": "sha1-rmzg1vbV4KT32JN5jQPB6pVZtqI=",
"optional": true,
"requires": {
"mkdirp": "~0.5.1",
"ncp": "~2.0.0",
"rimraf": "~2.4.0"
},
"dependencies": {
"glob": {
"version": "6.0.4",
"resolved": "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz",
"integrity": "sha1-DwiGD2oVUSey+t1PnOJLGqtuTSI=",
"optional": true,
"requires": {
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "2 || 3",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
}
},
"rimraf": {
"version": "2.4.5",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.4.5.tgz",
"integrity": "sha1-7nEM5dk6j9uFb7Xqj/Di11k0sto=",
"optional": true,
"requires": {
"glob": "^6.0.1"
}
}
}
},
"mz": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",
@@ -1957,32 +1727,20 @@
"thenify-all": "^1.0.0"
}
},
"nan": {
"version": "2.14.1",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.14.1.tgz",
"integrity": "sha512-isWHgVjnFjh2x2yuJ/tj3JbwoHu3UC2dX5G/88Cm24yB6YopVgxvBObDY7n5xW6ExmFhJpSEQqFPvq9zaXc8Jw==",
"optional": true
},
"nanoid": {
"version": "2.1.11",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-2.1.11.tgz",
"integrity": "sha512-s/snB+WGm6uwi0WjsZdaVcuf3KJXlfGl2LcxgwkEwJF0D/BWzVWAZW/XY4bFaiR7s0Jk3FPvlnepg1H1b1UwlA=="
},
"ncp": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz",
"integrity": "sha1-GVoh1sRuNh0vsSgbo4uR6d9727M=",
"optional": true
},
"negotiator": {
"version": "0.6.2",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz",
"integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw=="
},
"node-fetch": {
"version": "2.6.1",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz",
"integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw=="
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.0.tgz",
"integrity": "sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA=="
},
"node-id3": {
"version": "0.1.16",
@@ -2160,17 +1918,6 @@
}
}
},
"passport-ldapauth": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/passport-ldapauth/-/passport-ldapauth-2.1.4.tgz",
"integrity": "sha512-VeVL69ZK+cpJe0DKMSGuwcf7k+V4dr0U0Y7ZhXL785pcRb5gRA6qYZfIH+XTsAzwqTK9l0Dn3Ds4weOZ1jKkLQ==",
"requires": {
"@types/node": "*",
"@types/passport": "^1.0.0",
"ldapauth-fork": "^4.3.2",
"passport-strategy": "^1.0.0"
}
},
"passport-local": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/passport-local/-/passport-local-1.0.0.tgz",
@@ -2224,11 +1971,6 @@
"resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz",
"integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY="
},
"precond": {
"version": "0.2.3",
"resolved": "https://registry.npmjs.org/precond/-/precond-0.2.3.tgz",
"integrity": "sha1-qpWRvKokkj8eD0hJ0kD0fvwQdaw="
},
"prepend-http": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-1.0.4.tgz",
@@ -2424,12 +2166,6 @@
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
},
"safe-json-stringify": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/safe-json-stringify/-/safe-json-stringify-1.2.0.tgz",
"integrity": "sha512-gH8eh2nZudPQO6TytOvbxnuhYBOvDBBLW52tz5q6X58lJcd/tkmqFR+5Z9adS8aJtURSXWThWy/xJtJwixErvg==",
"optional": true
},
"safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
@@ -2946,29 +2682,6 @@
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
"integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw="
},
"vasync": {
"version": "1.6.4",
"resolved": "https://registry.npmjs.org/vasync/-/vasync-1.6.4.tgz",
"integrity": "sha1-3+k2Fq0OeugBszKp2Iv8XNyOHR8=",
"requires": {
"verror": "1.6.0"
},
"dependencies": {
"extsprintf": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.2.0.tgz",
"integrity": "sha1-WtlGwi9bMrp/jNdCZxHG6KP8JSk="
},
"verror": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/verror/-/verror-1.6.0.tgz",
"integrity": "sha1-fROyex+swuLakEBetepuW90lLqU=",
"requires": {
"extsprintf": "1.2.0"
}
}
}
},
"verror": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz",

View File

@@ -30,7 +30,6 @@
"dependencies": {
"archiver": "^3.1.1",
"async": "^3.1.0",
"axios": "^0.21.1",
"bcryptjs": "^2.4.0",
"compression": "^1.7.4",
"config": "^3.2.3",
@@ -38,20 +37,17 @@
"express": "^4.17.1",
"fluent-ffmpeg": "^2.1.2",
"fs-extra": "^9.0.0",
"glob": "^7.1.6",
"jsonwebtoken": "^8.5.1",
"lowdb": "^1.0.0",
"md5": "^2.2.1",
"merge-files": "^0.1.2",
"moment": "^2.29.1",
"multer": "^1.4.2",
"node-fetch": "^2.6.1",
"node-fetch": "^2.6.0",
"node-id3": "^0.1.14",
"nodemon": "^2.0.2",
"passport": "^0.4.1",
"passport-http": "^0.3.0",
"passport-jwt": "^4.0.0",
"passport-ldapauth": "^2.1.4",
"passport-local": "^1.0.0",
"progress": "^2.0.3",
"ps-node": "^0.1.6",

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,847 @@
@angular-devkit/build-angular
MIT
The MIT License
Copyright (c) 2017 Google, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
@angular/animations
MIT
@angular/cdk
MIT
The MIT License
Copyright (c) 2020 Google LLC.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
@angular/common
MIT
@angular/compiler
MIT
@angular/core
MIT
@angular/forms
MIT
@angular/localize
MIT
@angular/material
MIT
The MIT License
Copyright (c) 2020 Google LLC.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
@angular/platform-browser
MIT
@angular/platform-browser-dynamic
MIT
@angular/router
MIT
core-js
MIT
Copyright (c) 2014-2020 Denis Pushkarev
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
file-saver
MIT
The MIT License
Copyright © 2016 [Eli Grey][1].
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
[1]: http://eligrey.com
filesize
BSD-3-Clause
Copyright (c) 2020, Jason Mulligan
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of filesize nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
fingerprintjs2
MIT
Fingerprintjs2 Modern & flexible browser fingerprint library v2
https://github.com/Valve/fingerprintjs2
Copyright (c) 2018 Jonas Haag (jonas@lophus.org)
Copyright (c) 2015 Valentin Vasilyev (valentin.vasilyev@outlook.com)
Licensed under the MIT (http://www.opensource.org/licenses/mit-license.php) license.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL VALENTIN VASILYEV BE LIABLE FOR ANY
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
is-retina
MIT
The MIT License (MIT)
Copyright (c) 2014 Kyle Mathews
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
ng-lazyload-image
MIT
The MIT License (MIT)
Copyright (c) 2016 Oskar Karlsson
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
ngx-avatar
MIT
The MIT License
Copyright (c) 2020 Haithem Mosbahi, ngx-avatar
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
ngx-file-drop
MIT
ngx-videogular
MIT
regenerator-runtime
MIT
MIT License
Copyright (c) 2014-present, Facebook, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
rxjs
Apache-2.0
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright (c) 2015-2018 Google, Inc., Netflix, Inc., Microsoft Corp. and contributors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
rxjs-compat
Apache-2.0
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright (c) 2015-2018 Google, Inc., Netflix, Inc., Microsoft Corp. and contributors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
ts-md5
MIT
tslib
Apache-2.0
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files.
"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions:
You must give any other recipients of the Work or Derivative Works a copy of this License; and
You must cause any modified files to carry prominent notices stating that You changed the files; and
You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and
If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
uuid
MIT
The MIT License (MIT)
Copyright (c) 2010-2016 Robert Kieffer and other contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
web-animations-js
Apache-2.0
webpack
MIT
Copyright JS Foundation and other contributors
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
'Software'), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
zone.js
MIT
The MIT License
Copyright (c) 2010-2020 Google LLC. http://angular.io/license
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

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

@@ -0,0 +1,215 @@
{
"ccc7e92cbdd35e901acf9ad80941abee07bd8f60": "No es necesario incluir URL, solo todo después ",
"f41145afc02fd47ef0576ac79acd2c47ebbf4901": "Argumentos personalizados globales para descargas en la página de inicio.",
"17f0ea5d2d7a262b0e875acc70475f102aee84e6": "Crea una lista de reproducción",
"cff1428d10d59d14e45edec3c735a27b5482db59": "Nombre",
"f47e2d56dd8a145b2e9599da9730c049d52962a2": "Archivos de sonido",
"a52dae09be10ca3a65da918533ced3d3f4992238": "Archivos de video",
"d9e83ac17026e70ef6e9c0f3240a3b2450367f40": "Modificar args de youtube-dl",
"7fc1946abe2b40f60059c6cd19975d677095fd19": "Args nuevos simulados",
"0b71824ae71972f236039bed43f8d2323e8fd570": "Añadir un arg",
"c8b0e59eb491f2ac7505f0fbab747062e6b32b23": "Busqueda por categoria",
"9eeb91caef5a50256dd87e1c4b7b3e8216479377": "Usar valor de arg",
"25d8ad5eba2ec24e68295a27d6a4bb9b49e3dacd": "Valor de arg",
"7de2451ed3fb8d8b847979bd3f0c740b970f167b": "Añadir arg",
"d7b35c384aecd25a516200d6921836374613dfe7": "Cancelar",
"b2623aee44b70c9a4ba1fce16c8a593b0a4c7974": "Modificar",
"038ebcb2a89155d90c24fa1c17bfe83dbadc3c20": "Descargador de Youtube",
"6d2ec8898344c8955542b0542c942038ef28bb80": "Por favor entre una URL válida",
"a38ae1082fec79ba1f379978337385a539a28e73": " Calidad ",
"4be966a9dcfbc9b54dfcc604b831c0289f847fa4": "Usa URL",
"d3f02f845e62cebd75fde451ab8479d2a8ad784d": " Ver ",
"4a9889d36910edc8323d7bab60858ab3da6d91df": "Solo audio",
"96a01fafe135afc58b0f8071a4ab00234495ce18": "Descarga múltiple",
"6a21ba5fb0ac804a525bf9ab168038c3ee88e661": "Descarga",
"6a3777f913cf3f288664f0632b9f24794fdcc24e": "Cancelar",
"322ed150e02666fe2259c5b4614eac7066f4ffa0": "Avanzado",
"b7ffe7c6586d6f3f18a9246806a7c7d5538ab43e": "Commando simulado:",
"4e4c721129466be9c3862294dc40241b64045998": "Usar argumentos personalizados",
"ad2f8ac8b7de7945b80c8e424484da94e597125f": "Argumentos personalizados",
"a6911c2157f1b775284bbe9654ce5eb30cf45d7f": "No es necesario incluir URL, solo todo después. Los argumentos se delimitan usando dos comas así: ,,",
"3a92a3443c65a52f37ca7efb8f453b35dbefbf29": "Usar salida personalizada",
"d9c02face477f2f9cdaae318ccee5f89856851fb": "Salida personalizada",
"fcfd4675b4c90f08d18d3abede9a9a4dff4cfdc7": "Documentación",
"19d1ae64d94d28a29b2c57ae8671aace906b5401": "La ruta es relativa a la ruta de descarga de la config. No incluya el extensión.",
"8fad10737d3e3735a6699a4d89cbf6c20f6bb55f": "Usa autenticación",
"08c74dc9762957593b91f6eb5d65efdfc975bf48": "Nombre de usuario",
"c32ef07f8803a223a83ed17024b38e8d82292407": "Contraseña",
"4a0dada6e841a425de3e5006e6a04df26c644fa5": "Audio",
"9779715ac05308973d8f1c8658b29b986e92450f": "Tus archivos de audio están aquí",
"47546e45bbb476baaaad38244db444c427ddc502": "Listas de reproducción",
"78bd81adb4609b68cfa4c589222bdc233ba1faaa": "No hay listas de reproducción disponibles. Cree uno de tus archivos de audio haciendo clic en el botón azul más.",
"9d2b62bb0b91e2e17fb4177a7e3d6756a2e6ee33": "Vídeo",
"960582a8b9d7942716866ecfb7718309728f2916": "Tus archivos de video son aquí",
"0f59c46ca29e9725898093c9ea6b586730d0624e": "No hay listas de reproducción disponibles. Cree uno de tus archivos de video haciendo clic en el botón azul más.",
"616e206cb4f25bd5885fc35925365e43cf5fb929": "Nombre:",
"c52db455cca9109ee47e1a612c3f4117c09eb71b": "URL:",
"c6eb45d085384903e53ab001a3513d1de6a1dbac": "Cargador:",
"109c6f4a5e46efb933612ededfaf52a13178b7e0": "Tamaño del archivo:",
"bd630d8669b16e5f264ec4649d9b469fe03e5ff4": "Ruta:",
"a67e7d843cef735c79d5ef1c8ba4af3e758912bb": "Subido:",
"f4e529ae5ffd73001d1ff4bbdeeb0a72e342e5c8": "Cerca",
"ca3dbbc7f3e011bffe32a10a3ea45cc84f30ecf1": "ID:",
"e684046d73bcee88e82f7ff01e2852789a05fc32": "Cuenta:",
"321e4419a943044e674beb55b8039f42a9761ca5": "Información",
"826b25211922a1b46436589233cb6f1a163d89b7": "Eliminar",
"34504b488c24c27e68089be549f0eeae6ebaf30b": "Eliminar y pones en la lista negra",
"121cc5391cd2a5115bc2b3160379ee5b36cd7716": "Configuraciones",
"801b98c6f02fe3b32f6afa3ee854c99ed83474e6": "URL",
"54c512cca1923ab72faf1a0bd98d3d172469629a": "URL desde la que se accederá a esta aplicación, sin el puerto.",
"cb2741a46e3560f6bc6dfd99d385e86b08b26d72": "Puerto",
"22e8f1d0423a3b784fe40fab187b92c06541b577": "Puerto deseado. El valor predeterminado es 17442.",
"d4477669a560750d2064051a510ef4d7679e2f3e": "Modo multiusuario",
"2eb03565fcdce7a7a67abc277a936a32fcf51557": "Ruta base de usuarios",
"a64505c41150663968e277ec9b3ddaa5f4838798": "Ruta base para los usuarios y sus videos descargados.",
"cbe16a57be414e84b6a68309d08fad894df797d6": "Usa cifrado",
"0c1875a79b7ecc792cc1bebca3e063e40b5764f9": "Ruta del archivo de certificado",
"736551b93461d2de64b118cf4043eee1d1c2cb2c": "Ruta de archivo de clave",
"4e3120311801c4acd18de7146add2ee4a4417773": "Permitir suscripciones",
"4bee2a4bef2d26d37c9b353c278e24e5cd309ce3": "Ruta base de suscripciones",
"bc9892814ee2d119ae94378c905ea440a249b84a": "Ruta base para videos de sus canales y listas de reproducción suscritos. Es relativo a la carpeta raíz de YTDL-Material.",
"5bef4b25ba680da7fff06b86a91b1fc7e6a926e3": "Intervalo de comprobación",
"0f56a7449b77630c114615395bbda4cab398efd8": "La unidad es segundos, solo incluye números.",
"78e49b7339b4fa7184dd21bcaae107ce9b7076f6": "Usa el archivo de youtube-dl",
"fa9fe4255231dd1cc6b29d3d254a25cb7c764f0f": "Con la función de archivo de youtube-dl,",
"09006404cccc24b7a8f8d1ce0b39f2761ab841d8": "los videos descargados de sus suscripciones se graban en un archivo de texto en el subdirectorio del archivo de suscripciones.",
"29ed79a98fc01e7f9537777598e31dbde3aa7981": "Esto permite eliminar videos de sus suscripciones de forma permanente sin darse de baja y le permite grabar los videos que descargó en caso de pérdida de datos.",
"27a56aad79d8b61269ed303f11664cc78bcc2522": "Tema",
"ff7cee38a2259526c519f878e71b964f41db4348": "Defecto",
"adb4562d2dbd3584370e44496969d58c511ecb63": "Oscura",
"7a6bacee4c31cb5c0ac2d24274fb4610d8858602": "Permitir cambio de tema",
"fe46ccaae902ce974e2441abe752399288298619": "Idioma",
"82421c3e46a0453a70c42900eab51d58d79e6599": "Principal",
"ab2756805742e84ad0cc0468f4be2d8aa9f855a5": "Ruta de la carpeta de audio",
"c2c89cdf45d46ea64d2ed2f9ac15dfa4d77e26ca": "Ruta para descargas de solo audio. Es relativo a la carpeta raíz de YTDL-Material.",
"46826331da1949bd6fb74624447057099c9d20cd": "Ruta de la carpeta de video",
"17c92e6d47a213fa95b5aa344b3f258147123f93": "Ruta de descarga de videos. Es relativo a la carpeta raíz de YTDL-Material.",
"6b995e7130b4d667eaab6c5f61b362ace486d26d": "Argumentos personalizados globales para descargas en la página de inicio. Los argumentos se delimitan usando dos comas así: ,,",
"0ba25ad86a240576c4f20a2fada4722ebba77b1e": "Descargador",
"61f8fd90b5f8cb20c70371feb2ee5e1fac5a9095": "Título superior",
"78d3531417c0d4ba4c90f0d4ae741edc261ec8df": "Administrador de archivos habilitado",
"a5a1be0a5df07de9eec57f5d2a86ed0204b2e75a": "Administrador de descargas habilitado",
"c33bd5392b39dbed36b8e5a1145163a15d45835f": "Permitir selección de calidad",
"bda5508e24e0d77debb28bcd9194d8fefb1cfb92": "Modo de solo descarga",
"09d31c803a7252658694e1e3176b97f5655a3fe3": "Permitir el modo de descarga múltiple",
"d8b47221b5af9e9e4cd5cb434d76fc0c91611409": "Requiere pin para la configuración",
"f5ec7b2cdf87d41154f4fcbc86e856314409dcb9": "Establecer nuevo pin",
"1c4dbce56d96b8974aac24a02f7ab2ee81415014": "Habilitar API pública",
"23bd81dcc30b74d06279a26d7a42e8901c1b124e": "Clave API pública",
"41016a73d8ad85e6cb26dffa0a8fab9fe8f60d8e": "Ver documentación",
"1b258b258b4cc475ceb2871305b61756b0134f4a": "Generar",
"d5d7c61349f3b0859336066e6d453fc35d334fe5": "Utilizar la API de YouTube",
"ce10d31febb3d9d60c160750570310f303a22c22": "Clave API de YouTube",
"8602e313cdfa7c4cc475ccbe86459fce3c3fd986": "¡Generar una clave es fácil!",
"9b3cedfa83c6d7acb3210953289d1be4aab115c7": "¡Haga clic aquí",
"7f09776373995003161235c0c8d02b7f91dbc4df": "para descargar la extensión Chrome oficial de YoutubeDL-Material manualmente.",
"5b5296423906ab3371fdb2b5a5aaa83acaa2ee52": "Debe cargar manualmente la extensión y modificar la configuración de la extensión para establecer la URL de la interfaz.",
"9a2ec6da48771128384887525bdcac992632c863": "para instalar la extensión Firefox oficial de YoutubeDL-Material directamente desde la página de extensiones de Firefox.",
"eb81be6b49e195e5307811d1d08a19259d411f37": "Instrucciones detalladas de configuración.",
"cb17ff8fe3961cf90f44bee97c88a3f3347a7e55": "No se requiere mucho más que cambiar la configuración de la extensión para establecer la URL de la interfaz.",
"61b81b11aad0b9d970ece2fce18405f07eac69c2": "Arrastra el enlace de abajo a tus marcadores, ¡y listo! Simplemente navegue hasta el video de YouTube que desea descargar y haga clic en el marcador.",
"c505d6c5de63cc700f0aaf8a4b31fae9e18024e5": "Generar bookmarklet solo de audio",
"d5f69691f9f05711633128b5a3db696783266b58": "Extra",
"5fab47f146b0a4b809dcebf3db9da94df6299ea1": "Usar agente de descarga predeterminado",
"ec71e08aee647ea4a71fd6b7510c54d84a797ca6": "Seleccione un descargador",
"dc3d990391c944d1fbfc7cfb402f7b5e112fb3a8": "Permitir descarga avanzada",
"bc2e854e111ecf2bd7db170da5e3c2ed08181d88": "Avanzado",
"37224420db54d4bc7696f157b779a7225f03ca9d": "Permitir registro de usuario",
"4d13a9cd5ed3dcee0eab22cb25198d43886942be": "Usuarios",
"52c9a103b812f258bcddc3d90a6e3f46871d25fe": "Salvar",
"fe8fd36dbf5deee1d56564965787a782a66eba44": "{VAR_SELECT, select, true {Cerrar} false {Cancelar} other {Otro} }",
"cec82c0a545f37420d55a9b6c45c20546e82f94e": "Sobre YoutubeDL-Material",
"199c17e5d6a419313af3c325f06dcbb9645ca618": "es un descargador de código abierto de YouTube creado bajo las especificaciones de \"Material Design\" de Google. Puede descargar sin problemas sus videos favoritos como archivos de video o audio, e incluso suscribirse a sus canales favoritos y listas de reproducción para mantenerse actualizado con sus nuevos videos.",
"bc0ad0ee6630acb7fcb7802ec79f5a0ee943c1a7": "tiene algunas características increíbles incluidas! Una amplia API, soporte de Docker y soporte de localización (traducción). Lea todas las funciones compatibles haciendo clic en el icono de GitHub que se encuentra arriba.",
"a45e3b05f0529dc5246d70ef62304c94426d4c81": "Versión instalada:",
"e22f3a5351944f3a1a10cfc7da6f65dfbe0037fe": "Comprobando actualizaciones...",
"a16e92385b4fd9677bb830a4b796b8b79c113290": "Actualización disponible",
"189b28aaa19b3c51c6111ad039c4fd5e2a22e370": "Puede actualizar desde el menú de configuración.",
"b33536f59b94ec935a16bd6869d836895dc5300c": "¿Encontró un error o tiene una sugerencia?",
"e1f398f38ff1534303d4bb80bd6cece245f24016": "para crear una cuestión!",
"42ff677ec14f111e88bd6cdd30145378e994d1bf": "Tu perfil",
"ac9d09de42edca1296371e4d801349c9096ac8de": "UID:",
"a5ed099ffc9e96f6970df843289ade8a7d20ab9f": "Creado:",
"fa96f2137af0a24e6d6d54c598c0af7d5d5ad344": "Usted no se ha identificado.",
"6765b4c916060f6bc42d9bb69e80377dbcb5e4e9": "Identificarse",
"bb694b49d408265c91c62799c2b3a7e3151c824d": "Salir",
"a1dbca87b9f36d2b06a5cbcffb5814c4ae9b798a": "Crear cuenta de administrador",
"2d2adf3ca26a676bca2269295b7455a26fd26980": "No se detectó una cuenta de administrador predeterminada. Esto creará y establecerá la contraseña para una cuenta de administrador con el nombre de usuario como 'admin'.",
"70a67e04629f6d412db0a12d51820b480788d795": "Crear",
"994363f08f9fbfa3b3994ff7b35c6904fdff18d8": "Perfil",
"004b222ff9ef9dd4771b777950ca1d0e4cd4348a": "Sobre",
"92eee6be6de0b11c924e3ab27db30257159c0a7c": "Inicio",
"357064ca9d9ac859eb618e28e8126fa32be049e2": "Suscripciones",
"822fab38216f64e8166d368b59fe756ca39d301b": "Descargas",
"a249a5ae13e0835383885aaf697d2890cc3e53e9": "Compartir lista de reproducción",
"15da89490e04496ca9ea1e1b3d44fb5efd4a75d9": "Compartir vídeo",
"1d540dcd271b316545d070f9d182c372d923aadd": "Compartir audio",
"1f6d14a780a37a97899dc611881e6bc971268285": "Habilitar compartir",
"6580b6a950d952df847cb3d8e7176720a740adc8": "Usar marca de tiempo",
"4f2ed9e71a7c981db3e50ae2fedb28aff2ec4e6c": "Segundos",
"3a6e5a6aa78ca864f6542410c5dafb6334538106": "Copiar al Portapapeles",
"5b3075e8dc3f3921ec316b0bd83b6d14a06c1a4f": "Guardar cambios",
"4f8b2bb476981727ab34ed40fde1218361f92c45": "Detalles",
"383000ab16bf415d5a1d61d7eb7b5959c72a9515": "Se ha producido un error:",
"77b0c73840665945b25bd128709aa64c8f017e1c": "Inicio de descarga:",
"08ff9375ec078065bcdd7637b7ea65fce2979266": "Fin de descarga:",
"ad127117f9471612f47d01eae09709da444a36a4": "Ruta(s) del archivo:",
"a9806cf78ce00eb2613eeca11354a97e033377b8": "Suscríbase a la lista de reproducción o al canal",
"93efc99ae087fc116de708ecd3ace86ca237cf30": "La lista de reproducción o la URL del canal",
"08f5d0ef937ae17feb1b04aff15ad88911e87baf": "Nombre personalizado",
"f3f62aa84d59f3a8b900cc9a7eec3ef279a7b4e7": "Esto es opcional",
"ea30873bd3f0d5e4fb2378eec3f0a1db77634a28": "Descargar todas las cargas",
"28a678e9cabf86e44c32594c43fa0e890135c20f": "Descargar videos subidos en el último",
"408ca4911457e84a348cecf214f02c69289aa8f1": "Modo de solo transmisión",
"d0336848b0c375a1c25ba369b3481ee383217a4f": "Subscribe",
"e78c0d60ac39787f62c9159646fe0b3c1ed55a1d": "Tipo:",
"a44d86aa1e6c20ced07aca3a7c081d8db9ded1c6": "Archivo:",
"8efc77bf327659c0fec1f518cf48a98cdcd9dddf": "Exportar el archivo",
"3042bd3ad8dffcfeca5fd1ae6159fd1047434e95": "Darse de baja",
"e2319dec5b4ccfb6ed9f55ccabd63650a8fdf547": "Sus suscripciones",
"807cf11e6ac1cde912496f764c176bdfdd6b7e19": "Canales",
"29b89f751593e1b347eef103891b7a1ff36ec03f": "Nombre no disponible. Recuperación de canales en progreso.",
"4636cd4a1379c50d471e98786098c4d39e1e82ad": "No tienes suscripciones de canal.",
"2e0a410652cb07d069f576b61eab32586a18320d": "Nombre no disponible. Recuperación de listas de reproducción en progreso.",
"587b57ced54965d8874c3fd0e9dfedb987e5df04": "No tienes suscripciones a listas de reproducción.",
"7e892ba15f2c6c17e83510e273b3e10fc32ea016": "Buscar",
"2054791b822475aeaea95c0119113de3200f5e1c": "Duración:",
"94e01842dcee90531caa52e4147f70679bac87fe": "Eliminar y volver a descargar",
"2031adb51e07a41844e8ba7704b054e98345c9c1": "Borrar para siempre",
"91ecce65f1d23f9419d1c953cd6b7bc7f91c110e": "Updater",
"1372e61c5bd06100844bd43b98b016aabc468f62": "Seleccione una versión:",
"cfc2f436ec2beffb042e7511a73c89c372e86a6c": "Registrarse",
"a1ad8b1be9be43b5183bd2c3186d4e19496f2a0b": "ID de sesión:",
"eb98135e35af26a9a326ee69bd8ff104d36dd8ec": "(actual)",
"7117fc42f860e86d983bfccfcf2654e5750f3406": "¡No hay descargas disponibles!",
"b7ff2e2b909c53abe088fe60b9f4b6ac7757247f": "Registrar un usuario",
"024886ca34a6f309e3e51c2ed849320592c3faaa": "Nombre de usuario",
"2bd201aea09e43fbfd3cd15ec0499b6755302329": "Administrar usuario",
"29c97c8e76763bb15b6d515648fa5bd1eb0f7510": "UID de usuario:",
"e70e209561583f360b1e9cefd2cbb1fe434b6229": "Nueva contraseña",
"6498fa1b8f563988f769654a75411bb8060134b9": "Establecer nueva contraseña",
"40da072004086c9ec00d125165da91eaade7f541": "Uso por defecto",
"4f20f2d5a6882190892e58b85f6ccbedfa737952": "Si",
"3d3ae7deebc5949b0c1c78b9847886a94321d9fd": "No",
"57c6c05d8ebf4ef1180c2705033c044f655bb2c4": "Gestionar rol",
"746f64ddd9001ac456327cd9a3d5152203a4b93c": " Nombre de usuario ",
"52c1447c1ec9570a2a3025c7e566557b8d19ed92": " Rol ",
"59a8c38db3091a63ac1cb9590188dc3a972acfb3": " Acciones ",
"4d92a0395dd66778a931460118626c5794a3fc7a": "Agregar Usuarios",
"b0d7dd8a1b0349622d6e0c6e643e24a9ea0efa1d": "Editar Rol",
"4f389e41e4592f7f9bb76abdd8af4afdfb13f4f1": "Modify playlist",
"28f86ffd419b869711aa13f5e5ff54be6d70731c": "Editar",
"ebadf946ae90f13ecd0c70f09edbc0f983af8a0f": "Sube nuevas cookies",
"98a8a42e5efffe17ab786636ed0139b4c7032d0e": "Arrastrar y soltar",
"85e0725c870b28458fd3bbba905397d890f00a69": "NOTA: Cargar nuevas cookies anulará sus cookies anteriores. También tenga en cuenta que las cookies son de toda la instancia, no por usuario.",
"d01715b75228878a773ae6d059acc639d4898a03": "Anulación de descarga segura",
"00e274c496b094a019f0679c3fab3945793f3335": "Seleccione un nivel de registrador",
"431e5f3a0dde88768d1074baedd65266412b3f02": "Utilizar Cookies",
"80651a7ad1229ea6613557d3559f702cfa5aecf5": "Establecer Cookies",
"eb3d5aefff38a814b76da74371cbf02c0789a1ef": "Registros",
"c76a955642714b8949ff3e4b4990864a2e2cac95": "Solo audio",
"f432e1a8d6adb12e612127978ce2e0ced933959c": "Estos se agregan después de los argumentos estándar.",
"98b6ec9ec138186d663e64770267b67334353d63": "Salida de archivo personalizada",
"fd59fb984749fcdb5e386ae85faec82f8e5ac098": "Los registros aparecerán aquí",
"5009630cdf32ab4f1c78737b9617b8773512c05a": "Líneas:"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

BIN
backend/public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

18
backend/public/index.html Normal file
View File

@@ -0,0 +1,18 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>YoutubeDLMaterial</title>
<base href="./">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="https://fonts.googleapis.com/css?family=Roboto&display=swap" rel="stylesheet">
<link rel="icon" type="image/x-icon" href="favicon.ico">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons|Material+Icons+Outlined" rel="stylesheet"/>
<link rel="stylesheet" href="styles.5112d6db78cf21541598.css"></head>
<body>
<app-root></app-root>
<script src="runtime-es2015.49084170e55eb5da181d.js" type="module"></script><script src="runtime-es5.49084170e55eb5da181d.js" nomodule defer></script><script src="polyfills-es5.7f923c8f5afda210edd3.js" nomodule defer></script><script src="polyfills-es2015.5b408f108bcea938a7e2.js" type="module"></script><script src="main-es2015.63a61681b752f95b7352.js" type="module"></script><script src="main-es5.63a61681b752f95b7352.js" nomodule defer></script></body>
</html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

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

View File

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

File diff suppressed because one or more lines are too long

View File

@@ -6,8 +6,7 @@ var path = require('path');
var youtubedl = require('youtube-dl');
const config_api = require('./config');
const twitch_api = require('./twitch');
var utils = require('./utils');
var utils = require('./utils')
const debugMode = process.env.YTDL_MODE === 'debug';
@@ -41,9 +40,9 @@ async function subscribe(sub, user_uid = null) {
else
url_exists = !!db.get('subscriptions').find({url: sub.url}).value();
if (!sub.name && url_exists) {
logger.error(`Sub with the same URL "${sub.url}" already exists -- please provide a custom name for this new subscription.`);
result_obj.error = 'Subcription with URL ' + sub.url + ' already exists! Custom name is required.';
if (url_exists) {
logger.info('Sub already exists');
result_obj.error = 'Subcription with URL ' + sub.url + ' already exists!';
resolve(result_obj);
return;
}
@@ -80,18 +79,17 @@ async function getSubscriptionInfo(sub, user_uid = null) {
else
basePath = config_api.getConfigItem('ytdl_subscriptions_base_path');
// get videos
let downloadConfig = ['--dump-json', '--playlist-end', '1'];
let useCookies = config_api.getConfigItem('ytdl_use_cookies');
if (useCookies) {
if (await fs.pathExists(path.join(__dirname, 'appdata', 'cookies.txt'))) {
downloadConfig.push('--cookies', path.join('appdata', 'cookies.txt'));
} else {
logger.warn('Cookies file could not be found. You can either upload one, or disable \'use cookies\' in the Advanced tab in the settings.');
}
}
return new Promise(resolve => {
// get videos
let downloadConfig = ['--dump-json', '--playlist-end', '1'];
let useCookies = config_api.getConfigItem('ytdl_use_cookies');
if (useCookies) {
if (fs.existsSync(path.join(__dirname, 'appdata', 'cookies.txt'))) {
downloadConfig.push('--cookies', path.join('appdata', 'cookies.txt'));
} else {
logger.warn('Cookies file could not be found. You can either upload one, or disable \'use cookies\' in the Advanced tab in the settings.');
}
}
youtubedl.exec(sub.url, downloadConfig, {}, function(err, output) {
if (debugMode) {
logger.info('Subscribe: got info for subscription ' + sub.id);
@@ -115,11 +113,7 @@ async function getSubscriptionInfo(sub, user_uid = null) {
continue;
}
if (!sub.name) {
if (sub.isPlaylist) {
sub.name = output_json.playlist_title ? output_json.playlist_title : output_json.playlist;
} else {
sub.name = output_json.uploader;
}
sub.name = sub.isPlaylist ? output_json.playlist_title : output_json.uploader;
// if it's now valid, update
if (sub.name) {
if (user_uid)
@@ -158,36 +152,39 @@ async function getSubscriptionInfo(sub, user_uid = null) {
}
async function unsubscribe(sub, deleteMode, user_uid = null) {
let basePath = null;
if (user_uid)
basePath = path.join(config_api.getConfigItem('ytdl_users_base_path'), user_uid, 'subscriptions');
else
basePath = config_api.getConfigItem('ytdl_subscriptions_base_path');
let result_obj = { success: false, error: '' };
return new Promise(async resolve => {
let basePath = null;
if (user_uid)
basePath = path.join(config_api.getConfigItem('ytdl_users_base_path'), user_uid, 'subscriptions');
else
basePath = config_api.getConfigItem('ytdl_subscriptions_base_path');
let result_obj = { success: false, error: '' };
let id = sub.id;
if (user_uid)
users_db.get('users').find({uid: user_uid}).get('subscriptions').remove({id: id}).write();
else
db.get('subscriptions').remove({id: id}).write();
let id = sub.id;
if (user_uid)
users_db.get('users').find({uid: user_uid}).get('subscriptions').remove({id: id}).write();
else
db.get('subscriptions').remove({id: id}).write();
// failed subs have no name, on unsubscribe they shouldn't error
if (!sub.name) {
return;
}
const appendedBasePath = getAppendedBasePath(sub, basePath);
if (deleteMode && (await fs.pathExists(appendedBasePath))) {
if (sub.archive && (await fs.pathExists(sub.archive))) {
const archive_file_path = path.join(sub.archive, 'archive.txt');
// deletes archive if it exists
if (await fs.pathExists(archive_file_path)) {
await fs.unlink(archive_file_path);
}
await fs.rmdir(sub.archive);
// failed subs have no name, on unsubscribe they shouldn't error
if (!sub.name) {
return;
}
await fs.remove(appendedBasePath);
}
const appendedBasePath = getAppendedBasePath(sub, basePath);
if (deleteMode && fs.existsSync(appendedBasePath)) {
if (sub.archive && fs.existsSync(sub.archive)) {
const archive_file_path = path.join(sub.archive, 'archive.txt');
// deletes archive if it exists
if (fs.existsSync(archive_file_path)) {
fs.unlinkSync(archive_file_path);
}
fs.rmdirSync(sub.archive);
}
deleteFolderRecursive(appendedBasePath);
}
});
}
async function deleteSubscriptionFile(sub, file, deleteForever, file_uid = null, user_uid = null) {
@@ -205,98 +202,155 @@ async function deleteSubscriptionFile(sub, file, deleteForever, file_uid = null,
const name = file;
let retrievedID = null;
sub_db.get('videos').remove({uid: file_uid}).write();
return new Promise(resolve => {
let filePath = appendedBasePath;
const ext = (sub.type && sub.type === 'audio') ? '.mp3' : '.mp4'
var jsonPath = path.join(__dirname,filePath,name+'.info.json');
var videoFilePath = path.join(__dirname,filePath,name+ext);
var imageFilePath = path.join(__dirname,filePath,name+'.jpg');
var altImageFilePath = path.join(__dirname,filePath,name+'.jpg');
let filePath = appendedBasePath;
const ext = (sub.type && sub.type === 'audio') ? '.mp3' : '.mp4'
var jsonPath = path.join(__dirname,filePath,name+'.info.json');
var videoFilePath = path.join(__dirname,filePath,name+ext);
var imageFilePath = path.join(__dirname,filePath,name+'.jpg');
var altImageFilePath = path.join(__dirname,filePath,name+'.webp');
jsonExists = fs.existsSync(jsonPath);
videoFileExists = fs.existsSync(videoFilePath);
imageFileExists = fs.existsSync(imageFilePath);
altImageFileExists = fs.existsSync(altImageFilePath);
const [jsonExists, videoFileExists, imageFileExists, altImageFileExists] = await Promise.all([
fs.pathExists(jsonPath),
fs.pathExists(videoFilePath),
fs.pathExists(imageFilePath),
fs.pathExists(altImageFilePath),
]);
if (jsonExists) {
retrievedID = JSON.parse(await fs.readFile(jsonPath, 'utf8'))['id'];
await fs.unlink(jsonPath);
}
if (imageFileExists) {
await fs.unlink(imageFilePath);
}
if (altImageFileExists) {
await fs.unlink(altImageFilePath);
}
if (videoFileExists) {
await fs.unlink(videoFilePath);
if ((await fs.pathExists(jsonPath)) || (await fs.pathExists(videoFilePath))) {
return false;
} else {
// check if the user wants the video to be redownloaded (deleteForever === false)
if (!deleteForever && useArchive && sub.archive && retrievedID) {
const archive_path = path.join(sub.archive, 'archive.txt')
// if archive exists, remove line with video ID
if (await fs.pathExists(archive_path)) {
await removeIDFromArchive(archive_path, retrievedID);
}
}
return true;
if (jsonExists) {
retrievedID = JSON.parse(fs.readFileSync(jsonPath, 'utf8'))['id'];
fs.unlinkSync(jsonPath);
}
} else {
// TODO: tell user that the file didn't exist
return true;
}
if (imageFileExists) {
fs.unlinkSync(imageFilePath);
}
if (altImageFileExists) {
fs.unlinkSync(altImageFilePath);
}
if (videoFileExists) {
fs.unlink(videoFilePath, function(err) {
if (fs.existsSync(jsonPath) || fs.existsSync(videoFilePath)) {
resolve(false);
} else {
// check if the user wants the video to be redownloaded (deleteForever === false)
if (!deleteForever && useArchive && sub.archive && retrievedID) {
const archive_path = path.join(sub.archive, 'archive.txt')
// if archive exists, remove line with video ID
if (fs.existsSync(archive_path)) {
removeIDFromArchive(archive_path, retrievedID);
}
}
resolve(true);
}
});
} else {
// TODO: tell user that the file didn't exist
resolve(true);
}
});
}
async function getVideosForSub(sub, user_uid = null) {
// get sub_db
let sub_db = null;
if (user_uid)
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});
const latest_sub_obj = sub_db.value();
if (!latest_sub_obj || latest_sub_obj['downloading']) {
return false;
}
updateSubscriptionProperty(sub, {downloading: true}, user_uid);
// get basePath
let basePath = null;
if (user_uid)
basePath = path.join(config_api.getConfigItem('ytdl_users_base_path'), user_uid, 'subscriptions');
else
basePath = config_api.getConfigItem('ytdl_subscriptions_base_path');
let appendedBasePath = getAppendedBasePath(sub, basePath);
let multiUserMode = null;
if (user_uid) {
multiUserMode = {
user: user_uid,
file_path: appendedBasePath
}
}
const downloadConfig = await generateArgsForSubscription(sub, user_uid);
// get videos
logger.verbose('Subscription: getting videos for subscription ' + sub.name);
return new Promise(resolve => {
youtubedl.exec(sub.url, downloadConfig, {}, async function(err, output) {
updateSubscriptionProperty(sub, {downloading: false}, user_uid);
if (!subExists(sub.id, user_uid)) {
resolve(false);
return;
}
// get sub_db
let sub_db = null;
if (user_uid)
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});
// get basePath
let basePath = null;
if (user_uid)
basePath = path.join(config_api.getConfigItem('ytdl_users_base_path'), user_uid, 'subscriptions');
else
basePath = config_api.getConfigItem('ytdl_subscriptions_base_path');
const useArchive = config_api.getConfigItem('ytdl_use_youtubedl_archive');
let appendedBasePath = null
appendedBasePath = getAppendedBasePath(sub, basePath);
let multiUserMode = null;
if (user_uid) {
multiUserMode = {
user: user_uid,
file_path: appendedBasePath
}
}
const ext = (sub.type && sub.type === 'audio') ? '.mp3' : '.mp4'
let fullOutput = appendedBasePath + '/%(title)s' + ext;
if (sub.custom_output) {
fullOutput = appendedBasePath + '/' + sub.custom_output + ext;
}
let downloadConfig = ['-o', fullOutput, '-ciw', '--write-info-json', '--print-json'];
let qualityPath = null;
if (sub.type && sub.type === 'audio') {
qualityPath = ['-f', 'bestaudio']
qualityPath.push('-x');
qualityPath.push('--audio-format', 'mp3');
} 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;
if (useArchive) {
if (sub.archive) {
archive_dir = sub.archive;
archive_path = path.join(archive_dir, 'archive.txt')
}
downloadConfig.push('--download-archive', archive_path);
}
// if streaming only mode, just get the list of videos
if (sub.streamingOnly) {
downloadConfig = ['-f', 'best', '--dump-json'];
}
if (sub.timerange) {
downloadConfig.push('--dateafter', sub.timerange);
}
let useCookies = config_api.getConfigItem('ytdl_use_cookies');
if (useCookies) {
if (fs.existsSync(path.join(__dirname, 'appdata', 'cookies.txt'))) {
downloadConfig.push('--cookies', path.join('appdata', 'cookies.txt'));
} else {
logger.warn('Cookies file could not be found. You can either upload one, or disable \'use cookies\' in the Advanced tab in the settings.');
}
}
// get videos
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);
if (err && !output) {
logger.error(err.stderr ? err.stderr : err.message);
logger.error(err.stderr);
if (err.stderr.includes('This video is unavailable')) {
logger.info('An error was encountered with at least one video, backup method will be used.')
try {
@@ -306,11 +360,7 @@ async function getVideosForSub(sub, user_uid = null) {
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
if (sub.archive) {
archive_dir = sub.archive;
archive_path = path.join(archive_dir, 'archive.txt')
fs.appendFileSync(archive_path, output['id']);
}
fs.appendFileSync(archive_path, output['id']);
}
}
} catch(e) {
@@ -323,7 +373,6 @@ async function getVideosForSub(sub, user_uid = null) {
if (output.length === 0 || (output.length === 1 && output[0] === '')) {
logger.verbose('No additional videos to download for ' + sub.name);
resolve(true);
return;
}
for (let i = 0; i < output.length; i++) {
let output_json = null;
@@ -338,101 +387,18 @@ async function getVideosForSub(sub, user_uid = null) {
const reset_videos = i === 0;
handleOutputJSON(sub, sub_db, output_json, multiUserMode, reset_videos);
}
if (config_api.getConfigItem('ytdl_subscriptions_redownload_fresh_uploads')) {
await setFreshUploads(sub, user_uid);
checkVideosForFreshUploads(sub, user_uid);
}
// TODO: Potentially store downloaded files in db?
}
resolve(true);
}
});
}, err => {
logger.error(err);
updateSubscriptionProperty(sub, {downloading: false}, user_uid);
});
}
async function generateArgsForSubscription(sub, user_uid, redownload = false, desired_path = null) {
// get basePath
let basePath = null;
if (user_uid)
basePath = path.join(config_api.getConfigItem('ytdl_users_base_path'), user_uid, 'subscriptions');
else
basePath = config_api.getConfigItem('ytdl_subscriptions_base_path');
const useArchive = config_api.getConfigItem('ytdl_use_youtubedl_archive');
let appendedBasePath = getAppendedBasePath(sub, basePath);
let fullOutput = `${appendedBasePath}/%(title)s.%(ext)s`;
if (desired_path) {
fullOutput = `${desired_path}.%(ext)s`;
} else if (sub.custom_output) {
fullOutput = `${appendedBasePath}/${sub.custom_output}.%(ext)s`;
}
let downloadConfig = ['-o', fullOutput, !redownload ? '-ciw' : '-ci', '--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 {
if (!sub.maxQuality || sub.maxQuality === 'best') qualityPath = ['-f', 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/mp4'];
else qualityPath = ['-f', `bestvideo[height<=${sub.maxQuality}]+bestaudio/best[height<=${sub.maxQuality}]`, '--merge-output-format', '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;
if (useArchive && !redownload) {
if (sub.archive) {
archive_dir = sub.archive;
archive_path = path.join(archive_dir, 'archive.txt')
}
downloadConfig.push('--download-archive', archive_path);
}
// if streaming only mode, just get the list of videos
if (sub.streamingOnly) {
downloadConfig = ['-f', 'best', '--dump-json'];
}
if (sub.timerange && !redownload) {
downloadConfig.push('--dateafter', sub.timerange);
}
let useCookies = config_api.getConfigItem('ytdl_use_cookies');
if (useCookies) {
if (await fs.pathExists(path.join(__dirname, 'appdata', 'cookies.txt'))) {
downloadConfig.push('--cookies', path.join('appdata', 'cookies.txt'));
} else {
logger.warn('Cookies file could not be found. You can either upload one, or disable \'use cookies\' in the Advanced tab in the settings.');
}
}
if (config_api.getConfigItem('ytdl_include_thumbnail')) {
downloadConfig.push('--write-thumbnail');
}
return downloadConfig;
}
function handleOutputJSON(sub, sub_db, output_json, multiUserMode = null, reset_videos = false) {
if (sub.streamingOnly) {
if (reset_videos) {
@@ -445,49 +411,17 @@ function handleOutputJSON(sub, sub_db, output_json, multiUserMode = null, reset_
// add to db
sub_db.get('videos').push(output_json).write();
} else {
path_object = path.parse(output_json['_filename']);
const path_string = path.format(path_object);
if (sub_db.get('videos').find({path: path_string}).value()) {
// file already exists in DB, return early to avoid reseting the download date
return;
}
db_api.registerFileDB(path.basename(output_json['_filename']), sub.type, multiUserMode, sub);
const url = output_json['webpage_url'];
if (sub.type === 'video' && url.includes('twitch.tv/videos/') && url.split('twitch.tv/videos/').length > 1
&& config_api.getConfigItem('ytdl_use_twitch_api') && config_api.getConfigItem('ytdl_twitch_auto_download_chat')) {
const file_name = path.basename(output_json['_filename']);
const id = file_name.substring(0, file_name.length-4);
let vodId = url.split('twitch.tv/videos/')[1];
vodId = vodId.split('?')[0];
twitch_api.downloadTwitchChatByVODID(vodId, id, sub.type, multiUserMode.user, sub);
}
}
}
function getSubscriptions(user_uid = null) {
function getAllSubscriptions(user_uid = null) {
if (user_uid)
return users_db.get('users').find({uid: user_uid}).get('subscriptions').value();
else
return db.get('subscriptions').value();
}
function getAllSubscriptions() {
let subscriptions = null;
const multiUserMode = config_api.getConfigItem('ytdl_multi_user_mode');
if (multiUserMode) {
subscriptions = [];
let users = users_db.get('users').value();
for (let i = 0; i < users.length; i++) {
if (users[i]['subscriptions']) subscriptions = subscriptions.concat(users[i]['subscriptions']);
}
} else {
subscriptions = getSubscriptions();
}
return subscriptions;
}
function getSubscription(subID, user_uid = null) {
if (user_uid)
return users_db.get('users').find({uid: user_uid}).get('subscriptions').find({id: subID}).value();
@@ -495,13 +429,6 @@ function getSubscription(subID, user_uid = null) {
return db.get('subscriptions').find({id: subID}).value();
}
function getSubscriptionByName(subName, user_uid = null) {
if (user_uid)
return users_db.get('users').find({uid: user_uid}).get('subscriptions').find({name: subName}).value();
else
return db.get('subscriptions').find({name: subName}).value();
}
function updateSubscription(sub, user_uid = null) {
if (user_uid) {
users_db.get('users').find({uid: user_uid}).get('subscriptions').find({id: sub.id}).assign(sub).write();
@@ -511,21 +438,6 @@ function updateSubscription(sub, user_uid = null) {
return true;
}
function updateSubscriptionPropertyMultiple(subs, assignment_obj) {
subs.forEach(sub => {
updateSubscriptionProperty(sub, assignment_obj, sub.user_uid);
});
}
function updateSubscriptionProperty(sub, assignment_obj, user_uid = null) {
if (user_uid) {
users_db.get('users').find({uid: user_uid}).get('subscriptions').find({id: sub.id}).assign(assignment_obj).write();
} else {
db.get('subscriptions').find({id: sub.id}).assign(assignment_obj).write();
}
return true;
}
function subExists(subID, user_uid = null) {
if (user_uid)
return !!users_db.get('users').find({uid: user_uid}).get('subscriptions').find({id: subID}).value();
@@ -533,52 +445,6 @@ function subExists(subID, user_uid = null) {
return !!db.get('subscriptions').find({id: subID}).value();
}
async function setFreshUploads(sub, user_uid) {
const current_date = new Date().toISOString().split('T')[0].replace(/-/g, '');
sub.videos.forEach(async video => {
if (current_date === video['upload_date'].replace(/-/g, '')) {
// set upload as fresh
const video_uid = video['uid'];
await db_api.setVideoProperty(video_uid, {'fresh_upload': true}, user_uid, sub['id']);
}
});
}
async function checkVideosForFreshUploads(sub, user_uid) {
const current_date = new Date().toISOString().split('T')[0].replace(/-/g, '');
sub.videos.forEach(async video => {
if (video['fresh_upload'] && current_date > video['upload_date'].replace(/-/g, '')) {
checkVideoIfBetterExists(video, sub, user_uid)
}
});
}
async function checkVideoIfBetterExists(file_obj, sub, user_uid) {
const new_path = file_obj['path'].substring(0, file_obj['path'].length - 4);
const downloadConfig = await generateArgsForSubscription(sub, user_uid, true, new_path);
logger.verbose(`Checking if a better version of the fresh upload ${file_obj['id']} exists.`);
// simulate a download to verify that a better version exists
youtubedl.getInfo(file_obj['url'], downloadConfig, (err, output) => {
if (err) {
// video is not available anymore for whatever reason
} else if (output) {
const metric_to_compare = sub.type === 'audio' ? 'abr' : 'height';
if (output[metric_to_compare] > file_obj[metric_to_compare]) {
// download new video as the simulated one is better
youtubedl.exec(file_obj['url'], downloadConfig, async (err, output) => {
if (err) {
logger.verbose(`Failed to download better version of video ${file_obj['id']}`);
} else if (output) {
logger.verbose(`Successfully upgraded video ${file_obj['id']}'s ${metric_to_compare} from ${file_obj[metric_to_compare]} to ${output[metric_to_compare]}`);
await db_api.setVideoProperty(file_obj['uid'], {[metric_to_compare]: output[metric_to_compare]}, user_uid, sub['id']);
}
});
}
}
});
await db_api.setVideoProperty(file_obj['uid'], {'fresh_upload': false}, user_uid, sub['id']);
}
// helper functions
function getAppendedBasePath(sub, base_path) {
@@ -586,8 +452,23 @@ function getAppendedBasePath(sub, base_path) {
return path.join(base_path, (sub.isPlaylist ? 'playlists/' : 'channels/'), sub.name);
}
async function removeIDFromArchive(archive_path, id) {
let data = await fs.readFile(archive_path, {encoding: 'utf-8'});
// https://stackoverflow.com/a/32197381/8088021
const deleteFolderRecursive = function(folder_to_delete) {
if (fs.existsSync(folder_to_delete)) {
fs.readdirSync(folder_to_delete).forEach((file, index) => {
const curPath = path.join(folder_to_delete, file);
if (fs.lstatSync(curPath).isDirectory()) { // recurse
deleteFolderRecursive(curPath);
} else { // delete file
fs.unlinkSync(curPath);
}
});
fs.rmdirSync(folder_to_delete);
}
};
function removeIDFromArchive(archive_path, id) {
let data = fs.readFileSync(archive_path, {encoding: 'utf-8'});
if (!data) {
logger.error('Archive could not be found.');
return;
@@ -608,15 +489,13 @@ async function removeIDFromArchive(archive_path, id) {
// UPDATE FILE WITH NEW DATA
const updatedData = dataArray.join('\n');
await fs.writeFile(archive_path, updatedData);
fs.writeFileSync(archive_path, updatedData);
if (line) return line;
if (err) throw err;
}
module.exports = {
getSubscription : getSubscription,
getSubscriptionByName : getSubscriptionByName,
getSubscriptions : getSubscriptions,
getAllSubscriptions : getAllSubscriptions,
updateSubscription : updateSubscription,
subscribe : subscribe,
@@ -625,6 +504,5 @@ module.exports = {
getVideosForSub : getVideosForSub,
removeIDFromArchive : removeIDFromArchive,
setLogger : setLogger,
initialize : initialize,
updateSubscriptionPropertyMultiple : updateSubscriptionPropertyMultiple
initialize : initialize
}

View File

@@ -1,128 +0,0 @@
var moment = require('moment');
var Axios = require('axios');
var fs = require('fs-extra')
var path = require('path');
const config_api = require('./config');
async function getCommentsForVOD(clientID, vodId) {
let url = `https://api.twitch.tv/v5/videos/${vodId}/comments?content_offset_seconds=0`,
batch,
cursor;
let comments = null;
try {
do {
batch = (await Axios.get(url, {
headers: {
'Client-ID': clientID,
Accept: 'application/vnd.twitchtv.v5+json; charset=UTF-8',
'Content-Type': 'application/json; charset=UTF-8',
}
})).data;
const str = batch.comments.map(c => {
let {
created_at: msgCreated,
content_offset_seconds: timestamp,
commenter: {
name,
_id,
created_at: acctCreated
},
message: {
body: msg,
user_color: user_color
}
} = c;
const timestamp_str = moment.duration(timestamp, 'seconds')
.toISOString()
.replace(/P.*?T(?:(\d+?)H)?(?:(\d+?)M)?(?:(\d+).*?S)?/,
(_, ...ms) => {
const seg = v => v ? v.padStart(2, '0') : '00';
return `${seg(ms[0])}:${seg(ms[1])}:${seg(ms[2])}`;
});
acctCreated = moment(acctCreated).utc();
msgCreated = moment(msgCreated).utc();
if (!comments) comments = [];
comments.push({
timestamp: timestamp,
timestamp_str: timestamp_str,
name: name,
message: msg,
user_color: user_color
});
// let line = `${timestamp},${msgCreated.format(tsFormat)},${name},${_id},"${msg.replace(/"/g, '""')}",${acctCreated.format(tsFormat)}`;
// return line;
}).join('\n');
cursor = batch._next;
url = `https://api.twitch.tv/v5/videos/${vodId}/comments?cursor=${cursor}`;
await new Promise(res => setTimeout(res, 300));
} while (cursor);
} catch (err) {
console.error(err);
}
return comments;
}
async function getTwitchChatByFileID(id, type, user_uid, uuid, sub) {
let file_path = null;
if (user_uid) {
if (sub) {
file_path = path.join('users', user_uid, 'subscriptions', sub.isPlaylist ? 'playlists' : 'channels', sub.name, id + '.twitch_chat.json');
} else {
file_path = path.join('users', user_uid, type, id + '.twitch_chat.json');
}
} else {
if (sub) {
file_path = path.join('subscriptions', sub.isPlaylist ? 'playlists' : 'channels', sub.name, id + '.twitch_chat.json');
} else {
file_path = path.join(type, id + '.twitch_chat.json');
}
}
var chat_file = null;
if (fs.existsSync(file_path)) {
chat_file = fs.readJSONSync(file_path);
}
return chat_file;
}
async function downloadTwitchChatByVODID(vodId, id, type, user_uid, sub) {
const twitch_api_key = config_api.getConfigItem('ytdl_twitch_api_key');
const chat = await getCommentsForVOD(twitch_api_key, vodId);
// save file if needed params are included
let file_path = null;
if (user_uid) {
if (sub) {
file_path = path.join('users', user_uid, 'subscriptions', sub.isPlaylist ? 'playlists' : 'channels', sub.name, id + '.twitch_chat.json');
} else {
file_path = path.join('users', user_uid, type, id + '.twitch_chat.json');
}
} else {
if (sub) {
file_path = path.join('subscriptions', sub.isPlaylist ? 'playlists' : 'channels', sub.name, id + '.twitch_chat.json');
} else {
file_path = path.join(type, id + '.twitch_chat.json');
}
}
if (chat) fs.writeJSONSync(file_path, chat);
return chat;
}
module.exports = {
getCommentsForVOD: getCommentsForVOD,
getTwitchChatByFileID: getTwitchChatByFileID,
downloadTwitchChatByVODID: downloadTwitchChatByVODID
}

View File

@@ -4,7 +4,6 @@ const config_api = require('./config');
const is_windows = process.platform === 'win32';
// replaces .webm with appropriate extension
function getTrueFileName(unfixed_path, type) {
let fixed_path = unfixed_path;
@@ -20,38 +19,6 @@ function getTrueFileName(unfixed_path, type) {
return fixed_path;
}
async function getDownloadedFilesByType(basePath, type, full_metadata = false) {
// return empty array if the path doesn't exist
if (!(await fs.pathExists(basePath))) return [];
let files = [];
const ext = type === 'audio' ? 'mp3' : 'mp4';
var located_files = await recFindByExt(basePath, ext);
for (let i = 0; i < located_files.length; i++) {
let file = located_files[i];
var file_path = file.substring(basePath.includes('\\') ? basePath.length+1 : basePath.length, file.length);
var stats = await fs.stat(file);
var id = file_path.substring(0, file_path.length-4);
var jsonobj = await getJSONByType(type, id, basePath);
if (!jsonobj) continue;
if (full_metadata) {
jsonobj['id'] = id;
files.push(jsonobj);
continue;
}
var upload_date = jsonobj.upload_date;
upload_date = upload_date ? `${upload_date.substring(0, 4)}-${upload_date.substring(4, 6)}-${upload_date.substring(6, 8)}` : null;
var isaudio = type === 'audio';
var file_obj = new File(id, jsonobj.title, jsonobj.thumbnail, isaudio, jsonobj.duration, jsonobj.webpage_url, jsonobj.uploader,
stats.size, file, upload_date, jsonobj.description, jsonobj.view_count, jsonobj.height, jsonobj.abr);
files.push(file_obj);
}
return files;
}
function getJSONMp4(name, customPath, openReadPerms = false) {
var obj = null; // output
if (!customPath) customPath = config_api.getConfigItem('ytdl_video_folder_path');
@@ -84,59 +51,13 @@ function getJSONMp3(name, customPath, openReadPerms = false) {
return obj;
}
function getJSONByType(type, name, customPath, openReadPerms = false) {
return type === 'audio' ? getJSONMp3(name, customPath, openReadPerms) : getJSONMp4(name, customPath, openReadPerms)
}
function getDownloadedThumbnail(name, type, customPath = null) {
if (!customPath) customPath = type === 'audio' ? config_api.getConfigItem('ytdl_audio_folder_path') : config_api.getConfigItem('ytdl_video_folder_path');
let jpgPath = path.join(customPath, name + '.jpg');
let webpPath = path.join(customPath, name + '.webp');
let pngPath = path.join(customPath, name + '.png');
if (fs.existsSync(jpgPath))
return jpgPath;
else if (fs.existsSync(webpPath))
return webpPath;
else if (fs.existsSync(pngPath))
return pngPath;
else
return null;
}
function getExpectedFileSize(input_info_jsons) {
// treat single videos as arrays to have the file sizes checked/added to. makes the code cleaner
const info_jsons = Array.isArray(input_info_jsons) ? input_info_jsons : [input_info_jsons];
let expected_filesize = 0;
info_jsons.forEach(info_json => {
if (info_json['filesize']) {
expected_filesize += info_json['filesize'];
return;
}
const formats = info_json['format_id'].split('+');
let individual_expected_filesize = 0;
formats.forEach(format_id => {
info_json.formats.forEach(available_format => {
if (available_format.format_id === format_id && available_format.filesize) {
individual_expected_filesize += available_format.filesize;
}
});
});
expected_filesize += individual_expected_filesize;
});
return expected_filesize;
}
function fixVideoMetadataPerms(name, type, customPath = null) {
if (is_windows) return;
if (!customPath) customPath = type === 'audio' ? config_api.getConfigItem('ytdl_audio_folder_path')
: config_api.getConfigItem('ytdl_video_folder_path');
const ext = type === 'audio' ? '.mp3' : '.mp4';
const files_to_fix = [
// JSONs
path.join(customPath, name + '.info.json'),
@@ -152,50 +73,9 @@ function fixVideoMetadataPerms(name, type, customPath = null) {
}
}
function deleteJSONFile(name, type, customPath = null) {
if (!customPath) customPath = type === 'audio' ? config_api.getConfigItem('ytdl_audio_folder_path')
: config_api.getConfigItem('ytdl_video_folder_path');
const ext = type === 'audio' ? '.mp3' : '.mp4';
let json_path = path.join(customPath, name + '.info.json');
let alternate_json_path = path.join(customPath, name + ext + '.info.json');
if (fs.existsSync(json_path)) fs.unlinkSync(json_path);
if (fs.existsSync(alternate_json_path)) fs.unlinkSync(alternate_json_path);
}
async function recFindByExt(base,ext,files,result)
{
files = files || (await fs.readdir(base))
result = result || []
for (const file of files) {
var newbase = path.join(base,file)
if ( (await fs.stat(newbase)).isDirectory() )
{
result = await recFindByExt(newbase,ext,await fs.readdir(newbase),result)
}
else
{
if ( file.substr(-1*(ext.length+1)) == '.' + ext )
{
result.push(newbase)
}
}
}
return result
}
function removeFileExtension(filename) {
const filename_parts = filename.split('.');
filename_parts.splice(filename_parts.length - 1);
return filename_parts.join('.');
}
// objects
function File(id, title, thumbnailURL, isAudio, duration, url, uploader, size, path, upload_date, description, view_count, height, abr) {
function File(id, title, thumbnailURL, isAudio, duration, url, uploader, size, path, upload_date) {
this.id = id;
this.title = title;
this.thumbnailURL = thumbnailURL;
@@ -206,22 +86,12 @@ function File(id, title, thumbnailURL, isAudio, duration, url, uploader, size, p
this.size = size;
this.path = path;
this.upload_date = upload_date;
this.description = description;
this.view_count = view_count;
this.height = height;
this.abr = abr;
}
module.exports = {
getJSONMp3: getJSONMp3,
getJSONMp4: getJSONMp4,
getTrueFileName: getTrueFileName,
getDownloadedThumbnail: getDownloadedThumbnail,
getExpectedFileSize: getExpectedFileSize,
fixVideoMetadataPerms: fixVideoMetadataPerms,
deleteJSONFile: deleteJSONFile,
getDownloadedFilesByType: getDownloadedFilesByType,
recFindByExt: recFindByExt,
removeFileExtension: removeFileExtension,
File: File
}

View File

@@ -0,0 +1,20 @@
// background.js
// Called when the user clicks on the browser action.
chrome.browserAction.onClicked.addListener(function(tab) {
// get the frontend_url
chrome.storage.sync.get({
frontend_url: 'http://localhost',
audio_only: false
}, function(items) {
chrome.tabs.query({active: true, currentWindow: true}, function(tabs) {
var activeTab = tabs[0];
var url = activeTab.url;
if (url.includes('youtube.com')) {
var new_url = items.frontend_url + '/#/home;url=' + encodeURIComponent(url) + ';audioOnly=' + items.audio_only;
chrome.tabs.create({ url: new_url });
}
});
});
});

View File

@@ -1,17 +1,17 @@
{
"manifest_version": 2,
"name": "YoutubeDL-Material",
"version": "0.4",
"version": "0.3",
"description": "The Official Firefox & Chrome Extension of YoutubeDL-Material, an open-source and self-hosted YouTube downloader.",
"background": {
"scripts": ["background.js"]
},
"browser_action": {
"default_icon": "favicon.png",
"default_popup": "popup.html",
"default_title": "YoutubeDL-Material"
"default_icon": "favicon.png"
},
"permissions": [
"tabs",
"storage",
"contextMenus"
"storage"
],
"options_ui": {
"page": "options.html",

View File

@@ -1,35 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<!-- Scripts -->
<script src="js/jquery-3.4.1.min.js"></script>
<script src="js/popper.min.js"></script>
<script src="js/bootstrap.min.js"></script>
<!-- Cascading Style Sheets -->
<link href="css/bootstrap.min.css" rel="stylesheet" media="screen">
</head>
<body>
<div style="width: 400px; margin: 0 auto;">
<div style="margin: 10px;">
<div class="checkbox">
<label>
<input type="checkbox" id="audio_only">
Audio only
</label>
</div>
<div class="input-group mb-3">
<input id="url_input" type="text" class="form-control" placeholder="URL" aria-label="URL" aria-describedby="basic-addon2">
<div class="input-group-append">
<button class="btn btn-outline-secondary" type="button" id="download">Download</button>
</div>
</div>
</div>
</div>
<script src="popup.js"></script>
</body>
</html>

View File

@@ -1,50 +0,0 @@
function audioOnlyClicked() {
console.log('audio only clicked');
var audio_only = document.getElementById("audio_only").checked;
// save state
chrome.storage.sync.set({
audio_only: audio_only
}, function() {});
}
function downloadVideo() {
var input_url = document.getElementById("url_input").value
// get the frontend_url
chrome.storage.sync.get({
frontend_url: 'http://localhost',
audio_only: false
}, function(items) {
var download_url = items.frontend_url + '/#/home;url=' + encodeURIComponent(input_url) + ';audioOnly=' + items.audio_only;
chrome.tabs.create({ url: download_url });
});
}
function loadInputs() {
// load audio-only input
chrome.storage.sync.get({
frontend_url: 'http://localhost',
audio_only: false
}, function(items) {
document.getElementById("audio_only").checked = items.audio_only;
});
// load url input
chrome.tabs.query({active: true, currentWindow: true}, function(tabs) {
var activeTab = tabs[0];
var current_url = activeTab.url;
console.log(current_url);
if (current_url && current_url.includes('youtube.com')) {
document.getElementById("url_input").value = current_url;
}
});
}
document.getElementById('download').addEventListener('click',
downloadVideo);
document.getElementById('audio_only').addEventListener('click',
audioOnlyClicked);
document.addEventListener('DOMContentLoaded', loadInputs);

12961
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "youtube-dl-material",
"version": "4.2.0",
"version": "4.1.0",
"license": "MIT",
"scripts": {
"ng": "ng",
@@ -18,20 +18,18 @@
},
"private": true,
"dependencies": {
"@angular-devkit/core": "^11.0.4",
"@angular/animations": "^11.0.4",
"@angular/cdk": "^11.0.2",
"@angular/common": "^11.0.4",
"@angular/compiler": "^11.0.4",
"@angular/core": "^11.0.4",
"@angular/forms": "^11.0.4",
"@angular/localize": "^11.0.4",
"@angular/material": "^11.0.2",
"@angular/platform-browser": "^11.0.4",
"@angular/platform-browser-dynamic": "^11.0.4",
"@angular/router": "^11.0.4",
"@ngneat/content-loader": "^5.0.0",
"@videogular/ngx-videogular": "^2.1.0",
"@angular-devkit/core": "^9.0.6",
"@angular/animations": "^9.1.0",
"@angular/cdk": "^9.2.0",
"@angular/common": "^9.1.0",
"@angular/compiler": "^9.1.0",
"@angular/core": "^9.0.7",
"@angular/forms": "^9.1.0",
"@angular/localize": "^9.1.0",
"@angular/material": "^9.2.0",
"@angular/platform-browser": "^9.1.0",
"@angular/platform-browser-dynamic": "^9.1.0",
"@angular/router": "^9.1.0",
"core-js": "^2.4.1",
"file-saver": "^2.0.2",
"filesize": "^6.1.0",
@@ -40,34 +38,35 @@
"ng-lazyload-image": "^7.0.1",
"ngx-avatar": "^4.0.0",
"ngx-file-drop": "^9.0.1",
"rxjs": "^6.6.3",
"ngx-videogular": "^9.0.1",
"rxjs": "^6.5.3",
"rxjs-compat": "^6.0.0-rc.0",
"tslib": "^2.0.0",
"typescript": "~4.0.5",
"tslib": "^1.10.0",
"typescript": "~3.7.5",
"web-animations-js": "^2.3.2",
"zone.js": "~0.10.2"
},
"devDependencies": {
"@angular-devkit/build-angular": "^0.1100.4",
"@angular/cli": "^11.0.4",
"@angular/compiler-cli": "^11.0.4",
"@angular/language-service": "^11.0.4",
"@angular-devkit/build-angular": "^0.901.0",
"@angular/cli": "^9.0.7",
"@angular/compiler-cli": "^9.0.7",
"@angular/language-service": "^9.0.7",
"@types/core-js": "^2.5.2",
"@types/file-saver": "^2.0.1",
"@types/jasmine": "~3.6.0",
"@types/jasmine": "2.5.45",
"@types/node": "^12.11.1",
"codelyzer": "^6.0.0",
"codelyzer": "^5.1.2",
"electron": "^8.0.1",
"jasmine-core": "~3.6.0",
"jasmine-spec-reporter": "~5.0.0",
"karma": "~5.0.0",
"karma-chrome-launcher": "~3.1.0",
"jasmine-core": "~2.6.2",
"jasmine-spec-reporter": "~4.1.0",
"karma": "~1.7.0",
"karma-chrome-launcher": "~2.1.1",
"karma-cli": "~1.0.1",
"karma-coverage-istanbul-reporter": "~3.0.2",
"karma-jasmine": "~4.0.0",
"karma-jasmine-html-reporter": "^1.5.0",
"protractor": "~7.0.0",
"karma-coverage-istanbul-reporter": "^1.2.1",
"karma-jasmine": "~1.1.0",
"karma-jasmine-html-reporter": "^0.2.2",
"protractor": "~5.1.2",
"ts-node": "~3.0.4",
"tslint": "~6.1.0"
"tslint": "~5.3.2"
}
}

View File

@@ -19,7 +19,7 @@ const routes: Routes = [
];
@NgModule({
imports: [RouterModule.forRoot(routes, { useHash: true, relativeLinkResolution: 'legacy' })],
imports: [RouterModule.forRoot(routes, { useHash: true })],
exports: [RouterModule]
})
export class AppRoutingModule { }

View File

@@ -38,15 +38,15 @@
</div>
<div class="sidenav-container" style="height: calc(100% - 64px)">
<mat-sidenav-container style="height: 100%">
<mat-sidenav [opened]="postsService.sidepanel_mode === 'side' && !window.location.href.includes('/player')" [mode]="postsService.sidepanel_mode" #sidenav>
<mat-sidenav [opened]="postsService.sidepanel_mode === 'side' && router.url === '/home'" [mode]="postsService.sidepanel_mode" #sidenav>
<mat-nav-list>
<a *ngIf="postsService.config && (!postsService.config.Advanced.multi_user_mode || postsService.isLoggedIn)" mat-list-item (click)="postsService.sidepanel_mode === 'over' ? sidenav.close() : null" routerLink='/home'><ng-container i18n="Navigation menu Home Page title">Home</ng-container></a>
<a *ngIf="postsService.config && (!postsService.config.Advanced.multi_user_mode || postsService.isLoggedIn)" mat-list-item (click)="sidenav.close()" routerLink='/home'><ng-container i18n="Navigation menu Home Page title">Home</ng-container></a>
<a *ngIf="postsService.config && postsService.config.Advanced.multi_user_mode && !postsService.isLoggedIn" mat-list-item (click)="sidenav.close()" routerLink='/login'><ng-container i18n="Navigation menu Login Page title">Login</ng-container></a>
<a *ngIf="postsService.config && allowSubscriptions && (!postsService.config.Advanced.multi_user_mode || (postsService.isLoggedIn && postsService.permissions.includes('subscriptions')))" mat-list-item (click)="postsService.sidepanel_mode === 'over' ? sidenav.close() : null" routerLink='/subscriptions'><ng-container i18n="Navigation menu Subscriptions Page title">Subscriptions</ng-container></a>
<a *ngIf="postsService.config && enableDownloadsManager && (!postsService.config.Advanced.multi_user_mode || (postsService.isLoggedIn && postsService.permissions.includes('downloads_manager')))" mat-list-item (click)="postsService.sidepanel_mode === 'over' ? sidenav.close() : null" routerLink='/downloads'><ng-container i18n="Navigation menu Downloads Page title">Downloads</ng-container></a>
<a *ngIf="postsService.config && allowSubscriptions && (!postsService.config.Advanced.multi_user_mode || (postsService.isLoggedIn && postsService.permissions.includes('subscriptions')))" mat-list-item (click)="sidenav.close()" routerLink='/subscriptions'><ng-container i18n="Navigation menu Subscriptions Page title">Subscriptions</ng-container></a>
<a *ngIf="postsService.config && enableDownloadsManager && (!postsService.config.Advanced.multi_user_mode || (postsService.isLoggedIn && postsService.permissions.includes('downloads_manager')))" mat-list-item (click)="sidenav.close()" routerLink='/downloads'><ng-container i18n="Navigation menu Downloads Page title">Downloads</ng-container></a>
<ng-container *ngIf="postsService.config && allowSubscriptions && postsService.subscriptions && (!postsService.config.Advanced.multi_user_mode || (postsService.isLoggedIn && postsService.permissions.includes('subscriptions')))">
<mat-divider></mat-divider>
<a *ngFor="let subscription of postsService.subscriptions" mat-list-item (click)="postsService.sidepanel_mode === 'over' ? sidenav.close() : null" [routerLink]="['/subscription', { id: subscription.id }]"><ngx-avatar [style.margin-right]="'10px'" size="32" [name]="subscription.name"></ngx-avatar>{{subscription.name}}</a>
<a *ngFor="let subscription of postsService.subscriptions" mat-list-item (click)="sidenav.close()" [routerLink]="['/subscription', { id: subscription.id }]"><ngx-avatar [style.margin-right]="'10px'" size="32" [name]="subscription.name"></ngx-avatar><ng-container i18n="Navigation menu Downloads Page title">{{subscription.name}}</ng-container></a>
</ng-container>
</mat-nav-list>
</mat-sidenav>

View File

@@ -1,9 +1,9 @@
import { TestBed, waitForAsync } from '@angular/core/testing';
import { TestBed, async } from '@angular/core/testing';
import { AppComponent } from './app.component';
describe('AppComponent', () => {
beforeEach(waitForAsync(() => {
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [
AppComponent
@@ -11,19 +11,19 @@ describe('AppComponent', () => {
}).compileComponents();
}));
it('should create the app', waitForAsync(() => {
it('should create the app', async(() => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.debugElement.componentInstance;
expect(app).toBeTruthy();
}));
it(`should have as title 'app'`, waitForAsync(() => {
it(`should have as title 'app'`, async(() => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.debugElement.componentInstance;
expect(app.title).toEqual('app');
}));
it('should render title in a h1 tag', waitForAsync(() => {
it('should render title in a h1 tag', async(() => {
const fixture = TestBed.createComponent(AppComponent);
fixture.detectChanges();
const compiled = fixture.debugElement.nativeElement;

View File

@@ -1,4 +1,4 @@
import { Component, OnInit, ElementRef, ViewChild, HostBinding, AfterViewInit } from '@angular/core';
import { Component, OnInit, ElementRef, ViewChild, HostBinding } from '@angular/core';
import {PostsService} from './posts.services';
import {FileCardComponent} from './file-card/file-card.component';
import { Observable } from 'rxjs/Observable';
@@ -30,13 +30,11 @@ import { SetDefaultAdminDialogComponent } from './dialogs/set-default-admin-dial
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit, AfterViewInit {
export class AppComponent implements OnInit {
@HostBinding('class') componentCssClass;
THEMES_CONFIG = THEMES_CONFIG;
window = window;
// config items
topBarTitle = 'Youtube Downloader';
defaultTheme = null;
@@ -71,29 +69,6 @@ export class AppComponent implements OnInit, AfterViewInit {
}
ngOnInit() {
if (localStorage.getItem('theme')) {
this.setTheme(localStorage.getItem('theme'));
}
this.postsService.open_create_default_admin_dialog.subscribe(open => {
if (open) {
const dialogRef = this.dialog.open(SetDefaultAdminDialogComponent);
dialogRef.afterClosed().subscribe(success => {
if (success) {
if (this.router.url !== '/login') { this.router.navigate(['/login']); }
} else {
console.error('Failed to create default admin account. See logs for details.');
}
});
}
});
}
ngAfterViewInit() {
this.postsService.sidenav = this.sidenav;
}
toggleSidenav() {
this.sidenav.toggle();
}
@@ -114,10 +89,10 @@ export class AppComponent implements OnInit, AfterViewInit {
// gets the subscriptions
if (this.allowSubscriptions) {
this.postsService.reloadSubscriptions();
this.postsService.getAllSubscriptions().subscribe(res => {
this.postsService.subscriptions = res['subscriptions'];
})
}
this.postsService.reloadCategories();
}
// theme stuff
@@ -149,9 +124,9 @@ export class AppComponent implements OnInit, AfterViewInit {
this.postsService.setTheme(theme);
this.onSetTheme(this.THEMES_CONFIG[theme]['css_label'], old_theme ? this.THEMES_CONFIG[old_theme]['css_label'] : old_theme);
}
}
onSetTheme(theme, old_theme) {
onSetTheme(theme, old_theme) {
if (old_theme) {
document.body.classList.remove(old_theme);
this.overlayContainer.getContainerElement().classList.remove(old_theme);
@@ -173,6 +148,27 @@ export class AppComponent implements OnInit, AfterViewInit {
event.stopPropagation();
}
ngOnInit() {
if (localStorage.getItem('theme')) {
this.setTheme(localStorage.getItem('theme'));
} else {
//
}
this.postsService.open_create_default_admin_dialog.subscribe(open => {
if (open) {
const dialogRef = this.dialog.open(SetDefaultAdminDialogComponent);
dialogRef.afterClosed().subscribe(success => {
if (success) {
if (this.router.url !== '/login') { this.router.navigate(['/login']); }
} else {
console.error('Failed to create default admin account. See logs for details.');
}
});
}
});
}
getSubscriptions() {
}

View File

@@ -32,17 +32,14 @@ import { DragDropModule } from '@angular/cdk/drag-drop';
import { ClipboardModule } from '@angular/cdk/clipboard';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { AppComponent } from './app.component';
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
import { HttpClientModule, HttpClient } from '@angular/common/http';
import { PostsService } from 'app/posts.services';
import { FileCardComponent } from './file-card/file-card.component';
import { RouterModule } from '@angular/router';
import { AppRoutingModule } from './app-routing.module';
import { MainComponent } from './main/main.component';
import { PlayerComponent } from './player/player.component';
import { VgControlsModule } from '@videogular/ngx-videogular/controls';
import { VgBufferingModule } from '@videogular/ngx-videogular/buffering';
import { VgOverlayPlayModule } from '@videogular/ngx-videogular/overlay-play';
import { VgCoreModule } from '@videogular/ngx-videogular/core';
import { VgCoreModule, VgControlsModule, VgOverlayPlayModule, VgBufferingModule } from 'ngx-videogular';
import { InputDialogComponent } from './input-dialog/input-dialog.component';
import { LazyLoadImageModule, IsVisibleProps } from 'ng-lazyload-image';
import { audioFilesMouseHovering, videoFilesMouseHovering, audioFilesOpened, videoFilesOpened } from './main/main.component';
@@ -57,7 +54,6 @@ import { SettingsComponent } from './settings/settings.component';
import { MatChipsModule } from '@angular/material/chips';
import { NgxFileDropModule } from 'ngx-file-drop';
import { AvatarModule } from 'ngx-avatar';
import { ContentLoaderModule } from '@ngneat/content-loader';
import es from '@angular/common/locales/es';
import { AboutDialogComponent } from './dialogs/about-dialog/about-dialog.component';
@@ -82,10 +78,6 @@ import { UnifiedFileCardComponent } from './components/unified-file-card/unified
import { RecentVideosComponent } from './components/recent-videos/recent-videos.component';
import { EditSubscriptionDialogComponent } from './dialogs/edit-subscription-dialog/edit-subscription-dialog.component';
import { CustomPlaylistsComponent } from './components/custom-playlists/custom-playlists.component';
import { EditCategoryDialogComponent } from './dialogs/edit-category-dialog/edit-category-dialog.component';
import { TwitchChatComponent } from './components/twitch-chat/twitch-chat.component';
import { LinkifyPipe, SeeMoreComponent } from './components/see-more/see-more.component';
import { H401Interceptor } from './http.interceptor';
registerLocaleData(es, 'es');
@@ -112,7 +104,6 @@ export function isVisible({ event, element, scrollContainer, offset }: IsVisible
VideoInfoDialogComponent,
ArgModifierDialogComponent,
HighlightPipe,
LinkifyPipe,
UpdaterComponent,
UpdateProgressDialogComponent,
ShareMediaDialogComponent,
@@ -131,10 +122,7 @@ export function isVisible({ event, element, scrollContainer, offset }: IsVisible
UnifiedFileCardComponent,
RecentVideosComponent,
EditSubscriptionDialogComponent,
CustomPlaylistsComponent,
EditCategoryDialogComponent,
TwitchChatComponent,
SeeMoreComponent
CustomPlaylistsComponent
],
imports: [
CommonModule,
@@ -164,6 +152,7 @@ export function isVisible({ event, element, scrollContainer, offset }: IsVisible
MatMenuModule,
MatDialogModule,
MatSlideToggleModule,
MatMenuModule,
MatAutocompleteModule,
MatTabsModule,
MatTooltipModule,
@@ -175,7 +164,6 @@ export function isVisible({ event, element, scrollContainer, offset }: IsVisible
ClipboardModule,
NgxFileDropModule,
AvatarModule,
ContentLoaderModule,
VgCoreModule,
VgControlsModule,
VgOverlayPlayModule,
@@ -192,12 +180,10 @@ export function isVisible({ event, element, scrollContainer, offset }: IsVisible
SettingsComponent
],
providers: [
PostsService,
{ provide: HTTP_INTERCEPTORS, useClass: H401Interceptor, multi: true }
PostsService
],
exports: [
HighlightPipe,
LinkifyPipe
HighlightPipe
],
bootstrap: [AppComponent]
})

View File

@@ -1,8 +1,8 @@
<div *ngIf="playlists && playlists.length > 0">
<div class="container">
<div class="row justify-content-center">
<div *ngFor="let playlist of playlists; let i = index" class="mb-2 mt-2" [ngClass]="[ postsService.card_size === 'small' ? 'col-2 small-col' : '', postsService.card_size === 'medium' ? 'col-6 col-lg-4 medium-col' : '', postsService.card_size === 'large' ? 'col-12 large-col' : '' ]">
<app-unified-file-card [index]="i" [card_size]="postsService.card_size" [locale]="postsService.locale" (goToFile)="goToPlaylist($event)" [file_obj]="playlist" [is_playlist]="true" (editPlaylist)="editPlaylistDialog($event)" (deleteFile)="deletePlaylist($event)" [loading]="false"></app-unified-file-card>
<div *ngFor="let playlist of playlists; let i = index" [ngClass]="[ postsService.card_size === 'small' ? 'col-2 mb-2 mt-2 file-col' : '', postsService.card_size === 'medium' ? 'col-6 col-lg-4 mb-2 mt-2 file-col' : '' ]">
<app-unified-file-card [index]="i" [card_size]="postsService.card_size" (goToFile)="goToPlaylist($event)" [file_obj]="playlist" [is_playlist]="true" (editPlaylist)="editPlaylistDialog($event)" (deleteFile)="deletePlaylist($event)"></app-unified-file-card>
</div>
</div>
</div>

View File

@@ -5,14 +5,6 @@
right: 15px;
}
.large-col {
max-width: 320px;
}
.medium-col {
max-width: 240px;
}
.small-col {
.file-col {
max-width: 240px;
}

View File

@@ -1,4 +1,4 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { CustomPlaylistsComponent } from './custom-playlists.component';
@@ -6,7 +6,7 @@ describe('CustomPlaylistsComponent', () => {
let component: CustomPlaylistsComponent;
let fixture: ComponentFixture<CustomPlaylistsComponent>;
beforeEach(waitForAsync(() => {
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ CustomPlaylistsComponent ]
})

View File

@@ -50,8 +50,7 @@ export class CustomPlaylistsComponent implements OnInit {
});
}
goToPlaylist(info_obj) {
const playlist = info_obj.file;
goToPlaylist(playlist) {
const playlistID = playlist.id;
const type = playlist.type;
@@ -62,7 +61,7 @@ export class CustomPlaylistsComponent implements OnInit {
} else {
localStorage.setItem('player_navigator', this.router.url);
const fileNames = playlist.fileNames;
this.router.navigate(['/player', {fileNames: fileNames.join('|nvr|'), type: type, id: playlistID, uid: playlistID, auto: playlist.auto}]);
this.router.navigate(['/player', {fileNames: fileNames.join('|nvr|'), type: type, id: playlistID, uid: playlistID}]);
}
} else {
// playlist not found
@@ -83,7 +82,7 @@ export class CustomPlaylistsComponent implements OnInit {
const playlist = args.file;
const index = args.index;
const playlistID = playlist.id;
this.postsService.removePlaylist(playlistID, playlist.type).subscribe(res => {
this.postsService.removePlaylist(playlistID, 'audio').subscribe(res => {
if (res['success']) {
this.playlists.splice(index, 1);
this.postsService.openSnackBar('Playlist successfully removed.', '');

View File

@@ -1,4 +1,4 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { DownloadsComponent } from './downloads.component';
@@ -6,7 +6,7 @@ describe('DownloadsComponent', () => {
let component: DownloadsComponent;
let fixture: ComponentFixture<DownloadsComponent>;
beforeEach(waitForAsync(() => {
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ DownloadsComponent ]
})

View File

@@ -1,4 +1,4 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { LoginComponent } from './login.component';
@@ -6,7 +6,7 @@ describe('LoginComponent', () => {
let component: LoginComponent;
let fixture: ComponentFixture<LoginComponent>;
beforeEach(waitForAsync(() => {
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ LoginComponent ]
})

View File

@@ -27,7 +27,7 @@ export class LoginComponent implements OnInit {
constructor(private postsService: PostsService, private snackBar: MatSnackBar, private router: Router) { }
ngOnInit(): void {
if (this.postsService.isLoggedIn && localStorage.getItem('jwt_token') !== 'null') {
if (this.postsService.isLoggedIn) {
this.router.navigate(['/home']);
}
this.postsService.service_initialized.subscribe(init => {

View File

@@ -1,4 +1,4 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { LogsViewerComponent } from './logs-viewer.component';
@@ -6,7 +6,7 @@ describe('LogsViewerComponent', () => {
let component: LogsViewerComponent;
let fixture: ComponentFixture<LogsViewerComponent>;
beforeEach(waitForAsync(() => {
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ LogsViewerComponent ]
})

View File

@@ -61,8 +61,7 @@ export class LogsViewerComponent implements OnInit {
data: {
dialogTitle: 'Clear logs',
dialogText: 'Would you like to clear your logs? This will delete all your current logs, permanently.',
submitText: 'Clear',
warnSubmitColor: true
submitText: 'Clear'
}
});
dialogRef.afterClosed().subscribe(confirmed => {

View File

@@ -1,4 +1,4 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ManageRoleComponent } from './manage-role.component';
@@ -6,7 +6,7 @@ describe('ManageRoleComponent', () => {
let component: ManageRoleComponent;
let fixture: ComponentFixture<ManageRoleComponent>;
beforeEach(waitForAsync(() => {
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ ManageRoleComponent ]
})

View File

@@ -1,4 +1,4 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ManageUserComponent } from './manage-user.component';
@@ -6,7 +6,7 @@ describe('ManageUserComponent', () => {
let component: ManageUserComponent;
let fixture: ComponentFixture<ManageUserComponent>;
beforeEach(waitForAsync(() => {
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ ManageUserComponent ]
})

View File

@@ -1,4 +1,4 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ModifyUsersComponent } from './modify-users.component';
@@ -6,7 +6,7 @@ describe('ModifyUsersComponent', () => {
let component: ModifyUsersComponent;
let fixture: ComponentFixture<ModifyUsersComponent>;
beforeEach(waitForAsync(() => {
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ ModifyUsersComponent ]
})

View File

@@ -1,53 +1,34 @@
<div class="container-fluid" style="max-width: 941px;">
<div class="row">
<div class="col-12 order-2 col-sm-4 order-sm-1 d-flex justify-content-center">
<div>
<div style="display: inline-block;">
<mat-form-field style="width: 132px;">
<mat-select [(ngModel)]="this.filterProperty" (selectionChange)="filterOptionChanged($event.value)">
<mat-option *ngFor="let filterOption of filterProperties | keyvalue" [value]="filterOption.value">
{{filterOption['value']['label']}}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<div class="sort-dir-div">
<button (click)="toggleModeChange()" mat-icon-button><mat-icon>{{descendingMode ? 'arrow_downward' : 'arrow_upward'}}</mat-icon></button>
</div>
</div>
</div>
<div class="col-12 order-1 col-sm-4 order-sm-2 d-flex justify-content-center">
<h4 class="my-videos-title" i18n="My videos title">My videos</h4>
</div>
<div class="col-12 order-3 col-sm-4 order-sm-3 d-flex justify-content-center">
<mat-form-field [ngClass]="searchIsFocused ? 'search-bar-focused' : 'search-bar-unfocused'" class="search-bar" color="accent">
<input (focus)="searchIsFocused = true" (blur)="searchIsFocused = false" class="search-input" type="text" placeholder="Search" i18n-placeholder="Files search placeholder" [(ngModel)]="search_text" (ngModelChange)="onSearchInputChanged($event)" matInput>
<mat-icon matSuffix>search</mat-icon>
<div class="flex-grid">
<div class="col">
<div style="display: inline-block;">
<mat-form-field style="width: 132px;">
<mat-select [(ngModel)]="this.filterProperty" (selectionChange)="filterOptionChanged($event.value)">
<mat-option *ngFor="let filterOption of filterProperties | keyvalue" [value]="filterOption.value">
{{filterOption['value']['label']}}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<div style="display: inline-block;">
<button (click)="toggleModeChange()" mat-icon-button><mat-icon>{{descendingMode ? 'arrow_downward' : 'arrow_upward'}}</mat-icon></button>
</div>
</div>
<div class="col">
<h4 style="text-align: center">My videos</h4>
</div>
<div class="col" style="top: 25px;">
<mat-form-field [ngClass]="searchIsFocused ? 'search-bar-focused' : 'search-bar-unfocused'" class="search-bar" color="accent">
<input (focus)="searchIsFocused = true" (blur)="searchIsFocused = false" class="search-input" type="text" placeholder="Search" i18n-placeholder="Files search placeholder" [(ngModel)]="search_text" (ngModelChange)="onSearchInputChanged($event)" matInput>
<mat-icon matSuffix>search</mat-icon>
</mat-form-field>
</div>
</div>
<div>
<div class="container">
<div class="row justify-content-center">
<ng-container *ngIf="normal_files_received && paged_data">
<div *ngFor="let file of paged_data; let i = index" class="mb-2 mt-2 d-flex justify-content-center" [ngClass]="[ postsService.card_size === 'small' ? 'col-2 small-col' : '', postsService.card_size === 'medium' ? 'col-6 col-lg-4 medium-col' : '', postsService.card_size === 'large' ? 'col-12 large-col' : '' ]">
<app-unified-file-card [index]="i" [card_size]="postsService.card_size" [locale]="postsService.locale" (goToFile)="goToFile($event)" (goToSubscription)="goToSubscription($event)" [file_obj]="file" [use_youtubedl_archive]="postsService.config['Downloader']['use_youtubedl_archive']" [loading]="false" (deleteFile)="deleteFile($event)" [baseStreamPath]="postsService.path" [jwtString]="postsService.isLoggedIn ? '?jwt=' + this.postsService.token : ''"></app-unified-file-card>
</div>
<div *ngIf="filtered_files.length === 0">
<ng-container i18n="No videos found">No videos found.</ng-container>
</div>
</ng-container>
<ng-container *ngIf="!normal_files_received && loading_files && loading_files.length > 0">
<div *ngFor="let file of loading_files; let i = index" class="mb-2 mt-2 d-flex justify-content-center" [ngClass]="[ postsService.card_size === 'small' ? 'col-2 small-col' : '', postsService.card_size === 'medium' ? 'col-6 col-lg-4 medium-col' : '', postsService.card_size === 'large' ? 'col-12 large-col' : '' ]">
<app-unified-file-card [index]="i" [card_size]="postsService.card_size" [locale]="postsService.locale" [loading]="true" [theme]="postsService.theme"></app-unified-file-card>
</div>
</ng-container>
<div *ngFor="let file of filtered_files; let i = index" [ngClass]="[ postsService.card_size === 'small' ? 'col-2 mb-2 mt-2 file-col' : '', postsService.card_size === 'medium' ? 'col-6 col-lg-4 mb-2 mt-2 file-col' : '' ]">
<app-unified-file-card [index]="i" [card_size]="postsService.card_size" (goToFile)="goToFile($event)" (goToSubscription)="goToSubscription($event)" [file_obj]="file" [use_youtubedl_archive]="postsService.config['Downloader']['use_youtubedl_archive']" (deleteFile)="deleteFile($event)"></app-unified-file-card>
</div>
</div>
</div>
<mat-paginator class="paginator" #paginator *ngIf="filtered_files && filtered_files.length > 0" (page)="pageChangeEvent($event)" [length]="filtered_files.length"
[pageSize]="pageSize"
[pageSizeOptions]="[5, 10, 25, 100, this.paged_data && this.paged_data.length > 100 ? this.paged_data.length : 250]">
</mat-paginator>
</div>

View File

@@ -1,12 +1,4 @@
.large-col {
max-width: 320px;
}
.medium-col {
max-width: 240px;
}
.small-col {
.file-col {
max-width: 240px;
}
@@ -17,7 +9,7 @@
}
.search-bar-unfocused {
width: 132px;
width: 100px;
}
.search-input {
@@ -36,29 +28,7 @@
padding-right: 12px;
}
.column {
.col {
width: 33%;
display: inline-block;
}
.sort-dir-div {
display: inline-block;
position: absolute;
top: 10px;
}
.paginator {
margin-top: 5px;
}
.my-videos-title {
text-align: center;
position: relative;
top: 12px;
}
@media (max-width: 576px) {
.my-videos-title {
top: 0px;
}
}

View File

@@ -1,4 +1,4 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { RecentVideosComponent } from './recent-videos.component';
@@ -6,7 +6,7 @@ describe('RecentVideosComponent', () => {
let component: RecentVideosComponent;
let fixture: ComponentFixture<RecentVideosComponent>;
beforeEach(waitForAsync(() => {
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ RecentVideosComponent ]
})

View File

@@ -1,7 +1,6 @@
import { Component, OnInit, ViewChild } from '@angular/core';
import { Component, OnInit } from '@angular/core';
import { PostsService } from 'app/posts.services';
import { Router } from '@angular/router';
import { MatPaginator } from '@angular/material/paginator';
@Component({
selector: 'app-recent-videos',
@@ -10,9 +9,6 @@ import { MatPaginator } from '@angular/material/paginator';
})
export class RecentVideosComponent implements OnInit {
cached_file_count = 0;
loading_files = null;
normal_files_received = false;
subscription_files_received = false;
files: any[] = null;
@@ -51,32 +47,15 @@ export class RecentVideosComponent implements OnInit {
};
filterProperty = this.filterProperties['upload_date'];
pageSize = 10;
paged_data = null;
@ViewChild('paginator') paginator: MatPaginator
constructor(public postsService: PostsService, private router: Router) {
// get cached file count
if (localStorage.getItem('cached_file_count')) {
this.cached_file_count = +localStorage.getItem('cached_file_count') <= 10 ? +localStorage.getItem('cached_file_count') : 10;
this.loading_files = Array(this.cached_file_count).fill(0);
}
}
constructor(public postsService: PostsService, private router: Router) { }
ngOnInit(): void {
if (this.postsService.initialized) {
this.getAllFiles();
}
this.postsService.service_initialized.subscribe(init => {
if (init) {
this.getAllFiles();
}
});
// set filter property to cached
const cached_filter_property = localStorage.getItem('filter_property');
if (cached_filter_property && this.filterProperties[cached_filter_property]) {
@@ -98,8 +77,7 @@ export class RecentVideosComponent implements OnInit {
private filterFiles(value: string) {
const filterValue = value.toLowerCase();
this.filtered_files = this.files.filter(option => option.id.toLowerCase().includes(filterValue) || option.category?.name?.toLowerCase().includes(filterValue));
this.pageChangeEvent({pageSize: this.pageSize, pageIndex: this.paginator.pageIndex});
this.filtered_files = this.files.filter(option => option.id.toLowerCase().includes(filterValue));
}
filterByProperty(prop) {
@@ -108,7 +86,6 @@ export class RecentVideosComponent implements OnInit {
} else {
this.filtered_files = this.filtered_files.sort((a, b) => (a[prop] > b[prop] ? 1 : -1));
}
if (this.paginator) { this.pageChangeEvent({pageSize: this.pageSize, pageIndex: this.paginator.pageIndex}) };
}
filterOptionChanged(value) {
@@ -127,59 +104,43 @@ export class RecentVideosComponent implements OnInit {
this.normal_files_received = false;
this.postsService.getAllFiles().subscribe(res => {
this.files = res['files'];
this.files.sort(this.sortFiles);
for (let i = 0; i < this.files.length; i++) {
const file = this.files[i];
this.files.forEach(file => {
file.duration = typeof file.duration !== 'string' ? file.duration : this.durationStringToNumber(file.duration);
}
});
this.files.sort(this.sortFiles);
if (this.search_mode) {
this.filterFiles(this.search_text);
} else {
this.filtered_files = this.files;
}
this.filterByProperty(this.filterProperty['property']);
// set cached file count for future use, note that we convert the amount of files to a string
localStorage.setItem('cached_file_count', '' + this.files.length);
this.normal_files_received = true;
this.paged_data = this.filtered_files.slice(0, 10);
});
}
// navigation
goToFile(info_obj) {
const file = info_obj['file'];
const event = info_obj['event'];
goToFile(file) {
if (this.postsService.config['Extra']['download_only_mode']) {
this.downloadFile(file);
} else {
this.navigateToFile(file, event.ctrlKey);
this.navigateToFile(file);
}
}
navigateToFile(file, new_tab) {
navigateToFile(file) {
localStorage.setItem('player_navigator', this.router.url);
if (file.sub_id) {
const sub = this.postsService.getSubscriptionByID(file.sub_id);
const sub = this.postsService.getSubscriptionByID(file.sub_id)
if (sub.streamingOnly) {
// streaming only mode subscriptions
!new_tab ? this.router.navigate(['/player', {name: file.id,
url: file.requested_formats ? file.requested_formats[0].url : file.url}])
: window.open(`/#/player;name=${file.id};url=${file.requested_formats ? file.requested_formats[0].url : file.url}`);
this.router.navigate(['/player', {name: file.id,
url: file.requested_formats ? file.requested_formats[0].url : file.url}]);
} else {
// normal subscriptions
!new_tab ? this.router.navigate(['/player', {fileNames: file.id,
type: file.isAudio ? 'audio' : 'video', subscriptionName: sub.name,
subPlaylist: sub.isPlaylist}])
: window.open(`/#/player;fileNames=${file.id};type=${file.isAudio ? 'audio' : 'video'};subscriptionName=${sub.name};subPlaylist=${sub.isPlaylist}`);
this.router.navigate(['/player', {fileNames: file.id,
type: file.isAudio ? 'audio' : 'video', subscriptionName: sub.name,
subPlaylist: sub.isPlaylist}]);
}
} else {
// normal files
!new_tab ? this.router.navigate(['/player', {type: file.isAudio ? 'audio' : 'video', uid: file.uid}])
: window.open(`/#/player;type=${file.isAudio ? 'audio' : 'video'};uid=${file.uid}`);
this.router.navigate(['/player', {type: file.isAudio ? 'audio' : 'video', uid: file.uid}]);
}
}
@@ -201,6 +162,7 @@ export class RecentVideosComponent implements OnInit {
const type = file.isAudio ? 'audio' : 'video';
const ext = type === 'audio' ? '.mp3' : '.mp4'
const sub = this.postsService.getSubscriptionByID(file.sub_id);
console.log(sub.isPlaylist)
this.postsService.downloadFileFromServer(file.id, type, null, null, sub.name, sub.isPlaylist,
this.postsService.user ? this.postsService.user.uid : null, null).subscribe(res => {
const blob: Blob = res;
@@ -222,7 +184,7 @@ export class RecentVideosComponent implements OnInit {
if (!this.postsService.config.Extra.file_manager_enabled) {
// tell server to delete the file once downloaded
this.postsService.deleteFile(name, type).subscribe(delRes => {
this.postsService.deleteFile(name, false).subscribe(delRes => {
// reload mp4s
this.getAllFiles();
});
@@ -238,17 +200,17 @@ export class RecentVideosComponent implements OnInit {
const blacklistMode = args.blacklistMode;
if (file.sub_id) {
this.deleteSubscriptionFile(file, blacklistMode);
this.deleteSubscriptionFile(file, index, blacklistMode);
} else {
this.deleteNormalFile(file, blacklistMode);
this.deleteNormalFile(file, index, blacklistMode);
}
}
deleteNormalFile(file, blacklistMode = false) {
this.postsService.deleteFile(file.uid, file.isAudio ? 'audio' : 'video', blacklistMode).subscribe(result => {
deleteNormalFile(file, index, blacklistMode = false) {
this.postsService.deleteFile(file.uid, file.isAudio, blacklistMode).subscribe(result => {
if (result) {
this.postsService.openSnackBar('Delete success!', 'OK.');
this.removeFileCard(file);
this.files.splice(index, 1);
} else {
this.postsService.openSnackBar('Delete failed!', 'OK.');
}
@@ -257,39 +219,30 @@ export class RecentVideosComponent implements OnInit {
});
}
deleteSubscriptionFile(file, blacklistMode = false) {
deleteSubscriptionFile(file, index, blacklistMode = false) {
if (blacklistMode) {
this.deleteForever(file);
this.deleteForever(file, index);
} else {
this.deleteAndRedownload(file);
this.deleteAndRedownload(file, index);
}
}
deleteAndRedownload(file) {
deleteAndRedownload(file, index) {
const sub = this.postsService.getSubscriptionByID(file.sub_id);
this.postsService.deleteSubscriptionFile(sub, file.id, false, file.uid).subscribe(res => {
this.postsService.openSnackBar(`Successfully deleted file: '${file.id}'`);
this.removeFileCard(file);
this.files.splice(index, 1);
});
}
deleteForever(file) {
deleteForever(file, index) {
const sub = this.postsService.getSubscriptionByID(file.sub_id);
this.postsService.deleteSubscriptionFile(sub, file.id, true, file.uid).subscribe(res => {
this.postsService.openSnackBar(`Successfully deleted file: '${file.id}'`);
this.removeFileCard(file);
this.files.splice(index, 1);
});
}
removeFileCard(file_to_remove) {
const index = this.files.map(e => e.uid).indexOf(file_to_remove.uid);
this.files.splice(index, 1);
if (this.search_mode) {
this.filterFiles(this.search_text);
}
this.filterByProperty(this.filterProperty['property']);
}
// sorting and filtering
sortFiles(a, b) {
@@ -297,18 +250,13 @@ export class RecentVideosComponent implements OnInit {
const result = b.registered - a.registered;
return result;
}
durationStringToNumber(dur_str) {
let num_sum = 0;
const dur_str_parts = dur_str.split(':');
for (let i = dur_str_parts.length - 1; i >= 0; i--) {
num_sum += parseInt(dur_str_parts[i]) * (60 ** (dur_str_parts.length - 1 - i));
for (let i = dur_str_parts.length-1; i >= 0; i--) {
num_sum += parseInt(dur_str_parts[i])*(60**(dur_str_parts.length-1-i));
}
return num_sum;
}
pageChangeEvent(event) {
const offset = ((event.pageIndex + 1) - 1) * event.pageSize;
this.paged_data = this.filtered_files.slice(offset).slice(0, event.pageSize);
}
}

View File

@@ -1,11 +0,0 @@
<span class="text" [ngStyle]="{'-webkit-line-clamp': !see_more_active ? line_limit : null}" [innerHTML]="text | linkify"></span>
<span>
<a [routerLink]="" (click)="toggleSeeMore()">
<ng-container *ngIf="!see_more_active" i18n="See more">
See more.
</ng-container>
<ng-container *ngIf="see_more_active" i18n="See less">
See less.
</ng-container>
</a>
</span>

View File

@@ -1,7 +0,0 @@
.text {
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-box-orient: vertical;
word-wrap: break-word;
}

View File

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

View File

@@ -1,60 +0,0 @@
import { Component, Input, OnInit, Pipe, PipeTransform } from '@angular/core';
import { DomSanitizer } from '@angular/platform-browser';
@Pipe({ name: 'linkify' })
export class LinkifyPipe implements PipeTransform {
constructor(private _domSanitizer: DomSanitizer) {}
transform(value: any, args?: any): any {
return this._domSanitizer.bypassSecurityTrustHtml(this.stylize(value));
}
// Modify this method according to your custom logic
private stylize(text: string): string {
let stylizedText: string = '';
if (text && text.length > 0) {
for (let line of text.split("\n")) {
for (let t of line.split(" ")) {
if (t.startsWith("http") && t.length>7) {
stylizedText += `<a target="_blank" href="${t}">${t}</a> `;
}
else
stylizedText += t + " ";
}
stylizedText += '<br>';
}
return stylizedText;
}
else return text;
}
}
@Component({
selector: 'app-see-more',
templateUrl: './see-more.component.html',
providers: [LinkifyPipe],
styleUrls: ['./see-more.component.scss']
})
export class SeeMoreComponent implements OnInit {
see_more_active = false;
@Input() text = '';
@Input() line_limit = 2;
constructor() { }
ngOnInit(): void {
}
toggleSeeMore() {
this.see_more_active = !this.see_more_active;
}
parseText() {
return this.text.replace(/(http.*?\s)/, "<a href=\"$1\">$1</a>")
}
}

View File

@@ -1,12 +0,0 @@
<div class="chat-container" #scrollContainer *ngIf="visible_chat">
<div style="width: 250px; text-align: center;"><strong>Twitch Chat</strong></div>
<div #chat style="max-width: 250px" *ngFor="let chat of visible_chat; let last = last">
{{chat.timestamp_str}} - <strong [style.color]="chat.user_color ? chat.user_color : null">{{chat.name}}</strong>: {{chat.message}}
{{last ? scrollToBottom() : ''}}
</div>
</div>
<ng-container *ngIf="chat_response_received && !full_chat">
<button [disabled]="downloading_chat" (click)="downloadTwitchChat()" class="download-button" mat-raised-button color="accent"><ng-container i18n="Download Twitch Chat button">Download Twitch Chat</ng-container></button>
<mat-spinner *ngIf="downloading_chat" class="downloading-spinner" [diameter]="30"></mat-spinner>
</ng-container>

View File

@@ -1,13 +0,0 @@
.chat-container {
height: 100%;
overflow-y: scroll;
}
.download-button {
margin: 10px;
}
.downloading-spinner {
top: 50%;
left: 80px;
}

View File

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

View File

@@ -1,138 +0,0 @@
import { AfterViewInit, Component, ElementRef, Input, OnInit, QueryList, ViewChild, ViewChildren } from '@angular/core';
import { PostsService } from 'app/posts.services';
@Component({
selector: 'app-twitch-chat',
templateUrl: './twitch-chat.component.html',
styleUrls: ['./twitch-chat.component.scss']
})
export class TwitchChatComponent implements OnInit, AfterViewInit {
full_chat = null;
visible_chat = null;
chat_response_received = false;
downloading_chat = false;
current_chat_index = null;
CHAT_CHECK_INTERVAL_MS = 200;
chat_check_interval_obj = null;
scrollContainer = null;
@Input() db_file = null;
@Input() sub = null;
@Input() current_timestamp = null;
@ViewChild('scrollContainer') scrollRef: ElementRef;
@ViewChildren('chat') chat: QueryList<any>;
constructor(private postsService: PostsService) { }
ngOnInit(): void {
this.getFullChat();
}
ngAfterViewInit() {
}
private isUserNearBottom(): boolean {
const threshold = 150;
const position = this.scrollContainer.scrollTop + this.scrollContainer.offsetHeight;
const height = this.scrollContainer.scrollHeight;
return position > height - threshold;
}
scrollToBottom = (force_scroll) => {
if (force_scroll || this.isUserNearBottom()) {
this.scrollContainer.scrollTop = this.scrollContainer.scrollHeight;
}
}
addNewChatMessages() {
const next_chat_index = this.getIndexOfNextChat();
if (!this.scrollContainer) {
this.scrollContainer = this.scrollRef.nativeElement;
}
if (this.current_chat_index === null) {
this.current_chat_index = next_chat_index;
}
if (Math.abs(next_chat_index - this.current_chat_index) > 25) {
this.visible_chat = [];
this.current_chat_index = next_chat_index - 25;
setTimeout(() => this.scrollToBottom(true), 100);
}
const latest_chat_timestamp = this.visible_chat.length ? this.visible_chat[this.visible_chat.length - 1]['timestamp'] : 0;
for (let i = this.current_chat_index + 1; i < this.full_chat.length; i++) {
if (this.full_chat[i]['timestamp'] >= latest_chat_timestamp && this.full_chat[i]['timestamp'] <= this.current_timestamp) {
this.visible_chat.push(this.full_chat[i]);
this.current_chat_index = i;
} else if (this.full_chat[i]['timestamp'] > this.current_timestamp) {
break;
}
}
}
getIndexOfNextChat() {
const index = binarySearch(this.full_chat, 'timestamp', this.current_timestamp);
return index;
}
getFullChat() {
this.postsService.getFullTwitchChat(this.db_file.id, this.db_file.isAudio ? 'audio' : 'video', null, this.sub).subscribe(res => {
this.chat_response_received = true;
if (res['chat']) {
this.initializeChatCheck(res['chat']);
}
});
}
downloadTwitchChat() {
this.downloading_chat = true;
let vodId = this.db_file.url.split('videos/').length > 1 && this.db_file.url.split('videos/')[1];
vodId = vodId.split('?')[0];
if (!vodId) {
this.postsService.openSnackBar('VOD url for this video is not supported. VOD ID must be after "twitch.tv/videos/"');
}
this.postsService.downloadTwitchChat(this.db_file.id, this.db_file.isAudio ? 'audio' : 'video', vodId, null, this.sub).subscribe(res => {
if (res['chat']) {
this.initializeChatCheck(res['chat']);
} else {
this.downloading_chat = false;
this.postsService.openSnackBar('Download failed.')
}
}, err => {
this.downloading_chat = false;
this.postsService.openSnackBar('Chat could not be downloaded.')
});
}
initializeChatCheck(full_chat) {
this.full_chat = full_chat;
this.visible_chat = [];
this.chat_check_interval_obj = setInterval(() => this.addNewChatMessages(), this.CHAT_CHECK_INTERVAL_MS);
}
}
function binarySearch(arr, key, n) {
let min = 0;
let max = arr.length - 1;
let mid;
while (min <= max) {
// tslint:disable-next-line: no-bitwise
mid = (min + max) >>> 1;
if (arr[mid][key] === n) {
return mid;
} else if (arr[mid][key] < n) {
min = mid + 1;
} else {
max = mid - 1;
}
}
return min;
}

View File

@@ -1,26 +1,8 @@
<div (mouseover)="elevated=true" (mouseout)="elevated=false" (contextmenu)="onRightClick($event)" style="position: relative; width: fit-content;">
<div *ngIf="!loading" class="download-time">
<mat-icon class="audio-video-icon">{{(file_obj.type === 'audio' || file_obj.isAudio) ? 'audiotrack' : 'movie'}}</mat-icon>
&nbsp;&nbsp;
<ng-container i18n="Auto-generated label" *ngIf="file_obj.auto">Auto-generated</ng-container>
<ng-container *ngIf="!file_obj.auto">{{file_obj.registered | date:'shortDate' : undefined : locale.ngID}}</ng-container>
</div>
<div *ngIf="loading" class="download-time" style="width: 75%; margin-top: 5px;"><content-loader [primaryColor]="theme.ghost_primary" [secondaryColor]="theme.ghost_secondary" width="250" height="30"><svg:rect x="0" y="0" rx="3" ry="3" width="250" height="30" /></content-loader></div>
<!-- The context menu trigger must be kept above the "more info" menu -->
<div style="visibility: hidden; position: fixed"
[style.left]="contextMenuPosition.x"
[style.top]="contextMenuPosition.y"
[matMenuTriggerFor]="context_menu">
</div>
<button *ngIf="!file_obj || !file_obj.auto" [disabled]="loading" [matMenuTriggerFor]="action_menu" class="menuButton" mat-icon-button><mat-icon>more_vert</mat-icon></button>
<mat-menu #context_menu>
<ng-container *ngIf="!loading">
<button (click)="navigateToFile($event)" mat-menu-item><mat-icon>open_in_browser</mat-icon><ng-container i18n="Open file button">Open file</ng-container></button>
<button (click)="navigateToFile({ctrlKey: true})" mat-menu-item><mat-icon>open_in_new</mat-icon><ng-container i18n="Open file in new tab">Open file in new tab</ng-container></button>
</ng-container>
</mat-menu>
<div (mouseover)="elevated=true" (mouseout)="elevated=false" style="position: relative; width: fit-content;">
<div class="download-time"><mat-icon class="audio-video-icon">{{(file_obj.type === 'audio' || file_obj.isAudio) ? 'audiotrack' : 'movie'}}</mat-icon>&nbsp;&nbsp;{{file_obj.registered | date:'shortDate'}}</div>
<button [matMenuTriggerFor]="action_menu" class="menuButton" mat-icon-button><mat-icon>more_vert</mat-icon></button>
<mat-menu #action_menu="matMenu">
<ng-container *ngIf="!is_playlist && !loading">
<ng-container *ngIf="!is_playlist">
<button (click)="openFileInfoDialog()" mat-menu-item><mat-icon>info</mat-icon><ng-container i18n="Video info button">Info</ng-container></button>
<button (click)="navigateToSubscription()" mat-menu-item *ngIf="file_obj.sub_id"><mat-icon>{{file_obj.isAudio ? 'library_music' : 'video_library'}}</mat-icon>&nbsp;<ng-container i18n="Go to subscription menu item">Go to subscription</ng-container></button>
<mat-divider></mat-divider>
@@ -33,20 +15,17 @@
<button *ngIf="!file_obj.sub_id" (click)="emitDeleteFile()" mat-menu-item><mat-icon>delete</mat-icon><ng-container i18n="Delete video button">Delete</ng-container></button>
<button *ngIf="!file_obj.sub_id && use_youtubedl_archive" (click)="emitDeleteFile(true)" mat-menu-item><mat-icon>delete_forever</mat-icon><ng-container i18n="Delete and blacklist video button">Delete and blacklist</ng-container></button>
</ng-container>
<ng-container *ngIf="is_playlist && !loading">
<ng-container *ngIf="is_playlist">
<button (click)="emitEditPlaylist()" mat-menu-item><mat-icon>edit</mat-icon><ng-container i18n="Playlist edit button">Edit</ng-container></button>
<mat-divider></mat-divider>
<button (click)="emitDeleteFile()" mat-menu-item><mat-icon>delete_forever</mat-icon><ng-container i18n="Delete playlist">Delete</ng-container></button>
</ng-container>
<ng-container *ngIf="loading">
<button mat-menu-item>Placeholder</button>
</ng-container>
</mat-menu>
<mat-card [matTooltip]="null" (click)="navigateToFile($event)" matRipple class="file-mat-card" [ngClass]="{'small-mat-card': card_size === 'small', 'file-mat-card': card_size === 'medium', 'large-mat-card': card_size === 'large', 'mat-elevation-z4': !elevated, 'mat-elevation-z8': elevated}">
<mat-card [matTooltip]="null" (click)="navigateToFile()" matRipple class="file-mat-card" [ngClass]="{'small-mat-card': card_size === 'small', 'file-mat-card': card_size === 'medium', 'mat-elevation-z4': !elevated, 'mat-elevation-z8': elevated}">
<div style="padding:5px">
<div *ngIf="!loading && file_obj.thumbnailURL" class="img-div">
<div *ngIf="file_obj.thumbnailURL" class="img-div">
<div style="position: relative">
<img [ngClass]="{'image-small': card_size === 'small', 'image': card_size === 'medium', 'image-large': card_size === 'large'}" [src]="file_obj.thumbnailPath ? thumbnailBlobURL : file_obj.thumbnailURL" alt="Thumbnail">
<img [ngClass]="{'image-small': card_size === 'small', 'image': card_size === 'medium'}" [src]="file_obj.thumbnailURL" alt="Thumbnail">
<div class="duration-time">
{{file_length}}
</div>
@@ -54,12 +33,7 @@
</div>
<div *ngIf="loading" class="img-div">
<content-loader [primaryColor]="theme.ghost_primary" [secondaryColor]="theme.ghost_secondary" width="100" height="55"><svg:rect x="0" y="0" rx="0" ry="0" width="100" height="55" /></content-loader>
</div>
<span *ngIf="!loading" [ngClass]="{'max-two-lines': card_size !== 'small', 'max-one-line': card_size === 'small' }">{{card_size === 'large' && file_obj.uploader ? file_obj.uploader + ' - ' : ''}}<strong>{{!is_playlist ? file_obj.title : file_obj.name}}</strong></span>
<span *ngIf="loading" class="title-loading"><content-loader [primaryColor]="theme.ghost_primary" [secondaryColor]="theme.ghost_secondary" width="250" height="30"><svg:rect x="0" y="0" rx="3" ry="3" width="250" height="30" /></content-loader></span>
<span [ngClass]="{'max-two-lines': card_size !== 'small', 'max-one-line': card_size === 'small' }"><strong>{{!is_playlist ? file_obj.title : file_obj.name}}</strong></span>
</div>
</mat-card>
</div>

View File

@@ -1,10 +1,3 @@
.large-mat-card {
width: 300px;
height: 250px;
padding: 0px;
cursor: pointer;
}
.file-mat-card {
width: 200px;
height: 200px;
@@ -33,12 +26,6 @@
justify-content: center;
}
.image-large {
width: 300px;
height: 167.5px;
object-fit: cover;
}
.image {
width: 200px;
height: 112.5px;
@@ -103,19 +90,13 @@
background: rgba(255,255,255,0.6);
padding-left: 5px;
padding-right: 5px;
color: black;
}
.download-time {
position: absolute;
top: 1px;
left: 5px;
z-index: 999;
width: calc(100% - 8px);
white-space: nowrap;
overflow: hidden;
display: block;
text-overflow: ellipsis;
z-index: 99999;
}
.audio-video-icon {
@@ -123,12 +104,6 @@
top: 6px;
}
.title-loading {
width: 93%;
position: absolute;
bottom: 1px;
}
@media (max-width: 576px){
// .example-card {

View File

@@ -1,4 +1,4 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { UnifiedFileCardComponent } from './unified-file-card.component';
@@ -6,7 +6,7 @@ describe('UnifiedFileCardComponent', () => {
let component: UnifiedFileCardComponent;
let fixture: ComponentFixture<UnifiedFileCardComponent>;
beforeEach(waitForAsync(() => {
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ UnifiedFileCardComponent ]
})

View File

@@ -1,22 +1,6 @@
import { Component, OnInit, Input, Output, EventEmitter, ViewChild } from '@angular/core';
import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { VideoInfoDialogComponent } from 'app/dialogs/video-info-dialog/video-info-dialog.component';
import { DomSanitizer } from '@angular/platform-browser';
import { MatMenuTrigger } from '@angular/material/menu';
import { registerLocaleData } from '@angular/common';
import localeGB from '@angular/common/locales/en-GB';
import localeFR from '@angular/common/locales/fr';
import localeES from '@angular/common/locales/es';
import localeDE from '@angular/common/locales/de';
import localeZH from '@angular/common/locales/zh';
import localeNB from '@angular/common/locales/nb';
registerLocaleData(localeGB);
registerLocaleData(localeFR);
registerLocaleData(localeES);
registerLocaleData(localeDE);
registerLocaleData(localeZH);
registerLocaleData(localeNB);
@Component({
selector: 'app-unified-file-card',
@@ -32,29 +16,16 @@ export class UnifiedFileCardComponent implements OnInit {
type = null;
elevated = false;
// optional vars
thumbnailBlobURL = null;
// input/output
@Input() loading = true;
@Input() theme = null;
@Input() file_obj = null;
@Input() card_size = 'medium';
@Input() use_youtubedl_archive = false;
@Input() is_playlist = false;
@Input() index: number;
@Input() locale = null;
@Input() baseStreamPath = null;
@Input() jwtString = null;
@Output() goToFile = new EventEmitter<any>();
@Output() goToSubscription = new EventEmitter<any>();
@Output() deleteFile = new EventEmitter<any>();
@Output() editPlaylist = new EventEmitter<any>();
@ViewChild(MatMenuTrigger) contextMenu: MatMenuTrigger;
contextMenuPosition = { x: '0px', y: '0px' };
/*
Planned sizes:
small: 150x175
@@ -62,20 +33,10 @@ export class UnifiedFileCardComponent implements OnInit {
big: 250x200
*/
constructor(private dialog: MatDialog, private sanitizer: DomSanitizer) { }
constructor(private dialog: MatDialog) { }
ngOnInit(): void {
if (!this.loading) {
this.file_length = fancyTimeFormat(this.file_obj.duration);
}
if (this.file_obj && this.file_obj.thumbnailPath) {
this.thumbnailBlobURL = `${this.baseStreamPath}thumbnail/${encodeURIComponent(this.file_obj.thumbnailPath)}${this.jwtString}`;
/*const mime = getMimeByFilename(this.file_obj.thumbnailPath);
const blob = new Blob([new Uint8Array(this.file_obj.thumbnailBlob.data)], {type: mime});
const bloburl = URL.createObjectURL(blob);
this.thumbnailBlobURL = this.sanitizer.bypassSecurityTrustUrl(bloburl);*/
}
this.file_length = fancyTimeFormat(this.file_obj.duration);
}
emitDeleteFile(blacklistMode = false) {
@@ -86,8 +47,8 @@ export class UnifiedFileCardComponent implements OnInit {
});
}
navigateToFile(event) {
this.goToFile.emit({file: this.file_obj, event: event});
navigateToFile() {
this.goToFile.emit(this.file_obj);
}
navigateToSubscription() {
@@ -110,15 +71,6 @@ export class UnifiedFileCardComponent implements OnInit {
});
}
onRightClick(event) {
event.preventDefault();
this.contextMenuPosition.x = event.clientX + 'px';
this.contextMenuPosition.y = event.clientY + 'px';
this.contextMenu.menuData = { 'item': {id: 1, name: 'hi'} };
this.contextMenu.menu.focusFirstItem('mouse');
this.contextMenu.openMenu();
}
}
function fancyTimeFormat(time) {
@@ -141,16 +93,3 @@ function fancyTimeFormat(time) {
ret += '' + secs;
return ret;
}
function getMimeByFilename(name) {
switch (name.substring(name.length-4, name.length)) {
case '.jpg':
return 'image/jpeg';
case '.png':
return 'image/png';
case 'webp':
return 'image/webp';
default:
return null;
}
}

View File

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

View File

@@ -1,4 +1,4 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { CreatePlaylistComponent } from './create-playlist.component';
@@ -6,7 +6,7 @@ describe('CreatePlaylistComponent', () => {
let component: CreatePlaylistComponent;
let fixture: ComponentFixture<CreatePlaylistComponent>;
beforeEach(waitForAsync(() => {
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ CreatePlaylistComponent ]
})

View File

@@ -40,7 +40,7 @@
<br/>
<mat-form-field placeholder="Card size">
<mat-select [(ngModel)]="card_size" (selectionChange)="cardSizeOptionChanged($event.value)">
<mat-option value="large">
<mat-option value="large" [disabled]="true">
Large
</mat-option>
<mat-option value="medium">

View File

@@ -1,4 +1,4 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { AboutDialogComponent } from './about-dialog.component';
@@ -6,7 +6,7 @@ describe('AboutDialogComponent', () => {
let component: AboutDialogComponent;
let fixture: ComponentFixture<AboutDialogComponent>;
beforeEach(waitForAsync(() => {
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ AboutDialogComponent ]
})

View File

@@ -1,4 +1,4 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { AddUserDialogComponent } from './add-user-dialog.component';
@@ -6,7 +6,7 @@ describe('AddUserDialogComponent', () => {
let component: AddUserDialogComponent;
let fixture: ComponentFixture<AddUserDialogComponent>;
beforeEach(waitForAsync(() => {
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ AddUserDialogComponent ]
})

View File

@@ -1,4 +1,4 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ArgModifierDialogComponent } from './arg-modifier-dialog.component';
@@ -6,7 +6,7 @@ describe('ArgModifierDialogComponent', () => {
let component: ArgModifierDialogComponent;
let fixture: ComponentFixture<ArgModifierDialogComponent>;
beforeEach(waitForAsync(() => {
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ ArgModifierDialogComponent ]
})

View File

@@ -6,7 +6,7 @@
</mat-dialog-content>
<mat-dialog-actions>
<!-- The mat-dialog-close directive optionally accepts a value as a result for the dialog. -->
<button [color]="warnSubmitColor ? 'warn' : 'primary'" mat-flat-button type="submit" (click)="confirmClicked()">{{submitText}}</button>
<button color="primary" mat-flat-button type="submit" (click)="confirmClicked()">{{submitText}}</button>
<div class="mat-spinner" *ngIf="submitClicked">
<mat-spinner [diameter]="25"></mat-spinner>
</div>

View File

@@ -1,4 +1,4 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ConfirmDialogComponent } from './confirm-dialog.component';
@@ -6,7 +6,7 @@ describe('ConfirmDialogComponent', () => {
let component: ConfirmDialogComponent;
let fixture: ComponentFixture<ConfirmDialogComponent>;
beforeEach(waitForAsync(() => {
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ ConfirmDialogComponent ]
})

View File

@@ -15,14 +15,12 @@ export class ConfirmDialogComponent implements OnInit {
doneEmitter: EventEmitter<any> = null;
onlyEmitOnDone = false;
warnSubmitColor = false;
constructor(@Inject(MAT_DIALOG_DATA) public data: any, public dialogRef: MatDialogRef<ConfirmDialogComponent>) {
if (this.data.dialogTitle) { this.dialogTitle = this.data.dialogTitle };
if (this.data.dialogText) { this.dialogText = this.data.dialogText };
if (this.data.submitText) { this.submitText = this.data.submitText };
if (this.data.warnSubmitColor) { this.warnSubmitColor = this.data.warnSubmitColor };
// checks if emitter exists, if so don't autoclose as it should be handled by caller
if (this.data.doneEmitter) {

View File

@@ -17,7 +17,7 @@
</ng-template>
</ngx-file-drop>
<div style="margin-top: 15px;">
<p style="font-size: 14px;" i18n="Cookies upload warning">NOTE: Uploading new cookies will override your previous cookies. Also note that cookies are instance-wide, not per-user.</p>
<p style="font-size: 14px;" i18n="Cookies upload warning">NOTE: Uploading new cookies will overrride your previous cookies. Also note that cookies are instance-wide, not per-user.</p>
</div>
<div style="margin-top: 10px;">
<table class="table">

View File

@@ -1,4 +1,4 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { CookiesUploaderDialogComponent } from './cookies-uploader-dialog.component';
@@ -6,7 +6,7 @@ describe('CookiesUploaderDialogComponent', () => {
let component: CookiesUploaderDialogComponent;
let fixture: ComponentFixture<CookiesUploaderDialogComponent>;
beforeEach(waitForAsync(() => {
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ CookiesUploaderDialogComponent ]
})

View File

@@ -1,60 +0,0 @@
<h4 mat-dialog-title><ng-container i18n="Editing category dialog title">Editing category</ng-container>&nbsp;{{category['name']}}</h4>
<mat-dialog-content style="max-height: 50vh">
<mat-form-field style="width: 250px; margin-bottom: 5px;">
<input matInput placeholder="Name" i18n-placeholder="Name" [(ngModel)]="category['name']" required>
</mat-form-field>
<mat-divider></mat-divider>
<h6 style="margin-top: 20px;" i18n="Rules">Rules</h6>
<mat-list>
<mat-list-item *ngFor="let rule of category['rules']; let i = index">
<mat-form-field [style.visibility]="i === 0 ? 'hidden' : null" class="operator-select">
<mat-select [disabled]="i === 0" [(ngModel)]="rule['preceding_operator']">
<mat-option value="or">OR</mat-option>
<mat-option value="and">AND</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field class="property-select">
<mat-select [(ngModel)]="rule['property']">
<mat-option *ngFor="let propertyOption of propertyOptions" [value]="propertyOption.value">{{propertyOption.label}}</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field class="comparator-select">
<mat-select [(ngModel)]="rule['comparator']">
<mat-option *ngFor="let comparatorOption of comparatorOptions" [value]="comparatorOption.value">{{comparatorOption.label}}</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field class="value-input">
<input matInput [(ngModel)]="rule['value']">
</mat-form-field>
<button [disabled]="i === category['rules'].length-1" (click)="swapRules(i, i+1)" mat-icon-button><mat-icon>arrow_downward</mat-icon></button>
<button [disabled]="i === 0" (click)="swapRules(i, i-1)" mat-icon-button><mat-icon>arrow_upward</mat-icon></button>
<button (click)="removeRule(i)" mat-icon-button><mat-icon>cancel</mat-icon></button>
</mat-list-item>
</mat-list>
<button style="margin-bottom: 8px;" mat-icon-button (click)="addNewRule()" matTooltip="Add new rule" i18n-matTooltip="Add new rule tooltip"><mat-icon>add</mat-icon></button>
<mat-divider></mat-divider>
<mat-form-field style="width: 250px; margin-top: 10px;">
<input matInput [(ngModel)]="category['custom_output']" placeholder="Custom file output" i18n-placeholder="Category 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>
</mat-dialog-content>
<mat-dialog-actions>
<button mat-button mat-dialog-close><ng-container i18n="Cancel">Cancel</ng-container></button>
<button mat-button [disabled]="categoryChanged()" type="submit" (click)="saveClicked()"><ng-container i18n="Save button">Save</ng-container></button>
<div class="mat-spinner" *ngIf="updating">
<mat-spinner [diameter]="25"></mat-spinner>
</div>
</mat-dialog-actions>

View File

@@ -1,16 +0,0 @@
.operator-select {
width: 55px;
}
.property-select {
margin-left: 10px;
width: 110px;
}
.comparator-select {
margin-left: 10px;
}
.value-input {
margin-left: 10px;
}

Some files were not shown because too many files have changed in this diff Show More