mirror of
https://github.com/Tzahi12345/YoutubeDL-Material.git
synced 2026-03-07 20:10:03 +03:00
Compare commits
141 Commits
blacklist-
...
categories
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6481102e01 | ||
|
|
d7d861ef0e | ||
|
|
1d5490c0ff | ||
|
|
28ee77cee0 | ||
|
|
133d848729 | ||
|
|
a78f4e99d0 | ||
|
|
539bc5094a | ||
|
|
f0f2faa398 | ||
|
|
7835185fe0 | ||
|
|
95bb69f16b | ||
|
|
a93aa080b3 | ||
|
|
ed1375d40b | ||
|
|
db78e4ad5e | ||
|
|
6ef0082563 | ||
|
|
b978007472 | ||
|
|
c09dd7a03b | ||
|
|
b6c09324d9 | ||
|
|
1a900399d8 | ||
|
|
ea959547fd | ||
|
|
085849c7ee | ||
|
|
cf1dd43d36 | ||
|
|
250f150587 | ||
|
|
dbf08e1276 | ||
|
|
f74ce4b865 | ||
|
|
8e4e0c7908 | ||
|
|
b0cb09309d | ||
|
|
75c1c9e9b7 | ||
|
|
c19e0bb881 | ||
|
|
a1af5496c7 | ||
|
|
3c206c31d5 | ||
|
|
3ffcfac28b | ||
|
|
0e7bc1979f | ||
|
|
33fc74b7e7 | ||
|
|
c08993e20b | ||
|
|
4835093606 | ||
|
|
c63a64ebef | ||
|
|
9a57080bb3 | ||
|
|
1cc4df2829 | ||
|
|
6eb6ffa5e4 | ||
|
|
2656147570 | ||
|
|
88a1c31090 | ||
|
|
3f1532b4c6 | ||
|
|
afb5e3800c | ||
|
|
2971580f91 | ||
|
|
4cbfab20e0 | ||
|
|
7e06d30205 | ||
|
|
e75b56ad3f | ||
|
|
441a470990 | ||
|
|
eb7661c14a | ||
|
|
59c38321fd | ||
|
|
9847577431 | ||
|
|
0fec9d71a0 | ||
|
|
5f13205017 | ||
|
|
cd93313cfc | ||
|
|
8058b743eb | ||
|
|
e3374c573a | ||
|
|
29b8dc227c | ||
|
|
c30350205f | ||
|
|
ff8886d2e0 | ||
|
|
43b0c2fb9e | ||
|
|
e39e8f3dba | ||
|
|
da3bd2600f | ||
|
|
6ad590497b | ||
|
|
4f693d4eda | ||
|
|
9de403245b | ||
|
|
ff1bb8dee1 | ||
|
|
3f10986cdf | ||
|
|
c6fc5352c5 | ||
|
|
f425b9842f | ||
|
|
8c916d8fe4 | ||
|
|
bb18e1427e | ||
|
|
1542436e96 | ||
|
|
b0acb63123 | ||
|
|
0713eda7e2 | ||
|
|
d08fee1223 | ||
|
|
8938844ffa | ||
|
|
9895d77e01 | ||
|
|
27437a615f | ||
|
|
b730bc5adc | ||
|
|
d15d262b87 | ||
|
|
1aade1202d | ||
|
|
2f541a49df | ||
|
|
d93481640c | ||
|
|
71814cbdc9 | ||
|
|
09832ad15b | ||
|
|
cc78091403 | ||
|
|
cb88c7bc7c | ||
|
|
98f4828db4 | ||
|
|
8f0739c0f9 | ||
|
|
ab355d62a0 | ||
|
|
4d2d9a6b10 | ||
|
|
89dfac1249 | ||
|
|
d4f81eb0ab | ||
|
|
6b7d0681d2 | ||
|
|
b32fdb2445 | ||
|
|
b059c7ed5e | ||
|
|
8d87cbb08d | ||
|
|
1bb2f54eba | ||
|
|
7392338d6e | ||
|
|
82df92a72d | ||
|
|
9e4b328f91 | ||
|
|
3a049a99ac | ||
|
|
b323b548ca | ||
|
|
568463487f | ||
|
|
3318ac364d | ||
|
|
1ce85813fb | ||
|
|
6ea4176d63 | ||
|
|
3aa08e1817 | ||
|
|
727b047c39 | ||
|
|
d659a7614f | ||
|
|
6ad9d5ea8e | ||
|
|
edc22cc47b | ||
|
|
0189d292a8 | ||
|
|
deac54e8d6 | ||
|
|
d4e5082039 | ||
|
|
6f089491a5 | ||
|
|
0a38b01971 | ||
|
|
fe7303a191 | ||
|
|
dff4b141b0 | ||
|
|
fed0a54145 | ||
|
|
8366089444 | ||
|
|
44445f0b67 | ||
|
|
7dcc38c26d | ||
|
|
79b4b993f8 | ||
|
|
37a19eabe6 | ||
|
|
91b892b21a | ||
|
|
fb72dee26f | ||
|
|
3e4e7edd90 | ||
|
|
b8280e8646 | ||
|
|
70ee071e57 | ||
|
|
e26ac82c66 | ||
|
|
cdd2f78998 | ||
|
|
21eafeab22 | ||
|
|
f535d18cb9 | ||
|
|
2c43ce3c47 | ||
|
|
3d2d4efb31 | ||
|
|
10922fedd7 | ||
|
|
96cf1b87d1 | ||
|
|
6bed5851ed | ||
|
|
6717a59422 | ||
|
|
8595864118 |
95
.github/workflows/build.yml
vendored
Normal file
95
.github/workflows/build.yml
vendored
Normal file
@@ -0,0 +1,95 @@
|
||||
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
|
||||
71
.github/workflows/codeql-analysis.yml
vendored
Normal file
71
.github/workflows/codeql-analysis.yml
vendored
Normal file
@@ -0,0 +1,71 @@
|
||||
# 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
|
||||
32
.github/workflows/docker-release.yml
vendored
Normal file
32
.github/workflows/docker-release.yml
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
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 }}
|
||||
29
.github/workflows/docker.yml
vendored
Normal file
29
.github/workflows/docker.yml
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
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
|
||||
@@ -261,12 +261,12 @@ paths:
|
||||
$ref: '#/components/schemas/inline_response_200_10'
|
||||
security:
|
||||
- Auth query parameter: []
|
||||
/api/getAllSubscriptions:
|
||||
/api/getSubscriptions:
|
||||
post:
|
||||
tags:
|
||||
- subscriptions
|
||||
summary: Get all subscriptions
|
||||
operationId: post-api-getAllSubscriptions
|
||||
operationId: post-api-getSubscriptions
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
|
||||
46
README.md
46
README.md
@@ -1,12 +1,12 @@
|
||||
# YoutubeDL-Material
|
||||
[](https://hub.docker.com/r/tzahi12345/youtubedl-material)
|
||||
[](https://hub.docker.com/r/tzahi12345/youtubedl-material)
|
||||
[](https://heroku.com/deploy?template=https://github.com/Tzahi12345/YoutubeDL-Material)
|
||||
[](https://github.com/Tzahi12345/YoutubeDL-Material/issues)
|
||||
[](https://github.com/Tzahi12345/YoutubeDL-Material/blob/master/LICENSE.md)
|
||||
|
||||
[](https://hub.docker.com/r/tzahi12345/youtubedl-material)
|
||||
[](https://hub.docker.com/r/tzahi12345/youtubedl-material)
|
||||
[](https://heroku.com/deploy?template=https://github.com/Tzahi12345/YoutubeDL-Material)
|
||||
[](https://github.com/Tzahi12345/YoutubeDL-Material/issues)
|
||||
[](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 9](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 11](https://angular.io/) for the frontend, and [Node.js](https://nodejs.org/) on the backend.
|
||||
|
||||
Now with [Docker](#Docker) support!
|
||||
|
||||
@@ -16,27 +16,35 @@ 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:
|
||||
|
||||

|
||||
|
||||
With optional file management enabled (default):
|
||||
|
||||

|
||||
<img src="https://i.imgur.com/C6vFGbL.png" width="800">
|
||||
|
||||
Dark mode:
|
||||
|
||||

|
||||
<img src="https://i.imgur.com/vOtvH5w.png" width="800">
|
||||
|
||||
### Prerequisites
|
||||
|
||||
NOTE: If you would like to use Docker, you can skip down to the [Docker](#Docker) section for a setup guide.
|
||||
|
||||
Make sure you have these dependencies installed on your system: nodejs and youtube-dl. If you don't, run this command:
|
||||
Debian/Ubuntu:
|
||||
|
||||
```bash
|
||||
sudo apt-get install nodejs youtube-dl ffmpeg
|
||||
```
|
||||
sudo apt-get install nodejs youtube-dl
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
Optional dependencies:
|
||||
|
||||
* AtomicParsley (for embedding thumbnails, package name `atomicparsley`)
|
||||
|
||||
### Installing
|
||||
@@ -75,14 +83,16 @@ 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 8998" or something similar.
|
||||
4. Make sure you can connect to the specified URL + port, and if so, you are done!
|
||||
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`.
|
||||
|
||||
### 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
|
||||
@@ -109,8 +119,10 @@ 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.
|
||||
|
||||
|
||||
@@ -45,8 +45,6 @@
|
||||
],
|
||||
"optimization": true,
|
||||
"outputHashing": "all",
|
||||
"sourceMap": false,
|
||||
"extractCss": true,
|
||||
"namedChunks": false,
|
||||
"aot": true,
|
||||
"extractLicenses": true,
|
||||
|
||||
1777
backend/app.js
1777
backend/app.js
File diff suppressed because it is too large
Load Diff
@@ -7,6 +7,7 @@
|
||||
"Downloader": {
|
||||
"path-audio": "audio/",
|
||||
"path-video": "video/",
|
||||
"default_file_output": "",
|
||||
"use_youtubedl_archive": false,
|
||||
"custom_args": "",
|
||||
"safe_download_override": false,
|
||||
@@ -19,13 +20,17 @@
|
||||
"allow_quality_select": true,
|
||||
"download_only_mode": false,
|
||||
"allow_multi_download_mode": true,
|
||||
"enable_downloads_manager": true
|
||||
"enable_downloads_manager": true,
|
||||
"allow_playlist_categorization": true
|
||||
},
|
||||
"API": {
|
||||
"use_API_key": false,
|
||||
"API_key": "",
|
||||
"use_youtube_API": false,
|
||||
"youtube_API_key": ""
|
||||
"youtube_API_key": "",
|
||||
"use_twitch_API": false,
|
||||
"twitch_API_key": "",
|
||||
"twitch_auto_download_chat": false
|
||||
},
|
||||
"Themes": {
|
||||
"default_theme": "default",
|
||||
@@ -34,7 +39,8 @@
|
||||
"Subscriptions": {
|
||||
"allow_subscriptions": true,
|
||||
"subscriptions_base_path": "subscriptions/",
|
||||
"subscriptions_check_interval": "300"
|
||||
"subscriptions_check_interval": "300",
|
||||
"redownload_fresh_uploads": false
|
||||
},
|
||||
"Users": {
|
||||
"base_path": "users/",
|
||||
@@ -49,6 +55,7 @@
|
||||
}
|
||||
},
|
||||
"Advanced": {
|
||||
"default_downloader": "youtube-dl",
|
||||
"use_default_downloading_agent": true,
|
||||
"custom_downloading_agent": "",
|
||||
"multi_user_mode": false,
|
||||
|
||||
@@ -15,15 +15,16 @@ var JwtStrategy = require('passport-jwt').Strategy,
|
||||
|
||||
// other required vars
|
||||
let logger = null;
|
||||
var users_db = null;
|
||||
let db = null;
|
||||
let users_db = null;
|
||||
let SERVER_SECRET = null;
|
||||
let JWT_EXPIRATION = null;
|
||||
let opts = null;
|
||||
let saltRounds = null;
|
||||
|
||||
exports.initialize = function(input_users_db, input_logger) {
|
||||
exports.initialize = function(input_db, input_users_db, input_logger) {
|
||||
setLogger(input_logger)
|
||||
setDB(input_users_db);
|
||||
setDB(input_db, input_users_db);
|
||||
|
||||
/*************************
|
||||
* Authentication module
|
||||
@@ -61,7 +62,8 @@ function setLogger(input_logger) {
|
||||
logger = input_logger;
|
||||
}
|
||||
|
||||
function setDB(input_users_db) {
|
||||
function setDB(input_db, input_users_db) {
|
||||
db = input_db;
|
||||
users_db = input_users_db;
|
||||
}
|
||||
|
||||
@@ -89,6 +91,12 @@ 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);
|
||||
@@ -139,12 +147,12 @@ exports.registerUser = function(req, res) {
|
||||
exports.passport.use(new LocalStrategy({
|
||||
usernameField: 'username',
|
||||
passwordField: 'password'},
|
||||
function(username, password, done) {
|
||||
async function(username, password, done) {
|
||||
const user = users_db.get('users').find({name: username}).value();
|
||||
if (!user) { logger.error(`User ${username} not found`); return done(null, false); }
|
||||
if (user.auth_method && user.auth_method !== 'internal') { return done(null, false); }
|
||||
if (user) {
|
||||
return done(null, bcrypt.compareSync(password, user.passhash) ? user : false);
|
||||
return done(null, (await bcrypt.compare(password, user.passhash)) ? user : false);
|
||||
}
|
||||
}
|
||||
));
|
||||
@@ -160,7 +168,7 @@ exports.passport.use(new LdapStrategy(getLDAPConfiguration,
|
||||
// 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) {
|
||||
@@ -226,15 +234,13 @@ exports.ensureAuthenticatedElseError = function(req, res, next) {
|
||||
|
||||
// change password
|
||||
exports.changeUserPassword = async function(user_uid, new_pass) {
|
||||
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);
|
||||
});
|
||||
});
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// change user permissions
|
||||
@@ -279,60 +285,70 @@ exports.adminExists = function() {
|
||||
|
||||
exports.getUserVideos = function(user_uid, type) {
|
||||
const user = users_db.get('users').find({uid: user_uid}).value();
|
||||
return user['files'][type];
|
||||
return type ? user['files'].filter(file => file.isAudio === (type === 'audio')) : user['files'];
|
||||
}
|
||||
|
||||
exports.getUserVideo = function(user_uid, file_uid, type, requireSharing = false) {
|
||||
if (!type) {
|
||||
file = users_db.get('users').find({uid: user_uid}).get(`files.audio`).find({uid: file_uid}).value();
|
||||
if (!file) {
|
||||
file = users_db.get('users').find({uid: user_uid}).get(`files.video`).find({uid: file_uid}).value();
|
||||
if (file) type = 'video';
|
||||
} else {
|
||||
type = 'audio';
|
||||
}
|
||||
}
|
||||
|
||||
if (!file && type) file = users_db.get('users').find({uid: user_uid}).get(`files.${type}`).find({uid: file_uid}).value();
|
||||
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();
|
||||
|
||||
// prevent unauthorized users from accessing the file info
|
||||
if (requireSharing && !file['sharingEnabled']) file = null;
|
||||
if (file && !file['sharingEnabled'] && requireSharing) file = null;
|
||||
|
||||
return file;
|
||||
}
|
||||
|
||||
exports.addPlaylist = function(user_uid, new_playlist, type) {
|
||||
users_db.get('users').find({uid: user_uid}).get(`playlists.${type}`).push(new_playlist).write();
|
||||
exports.addPlaylist = function(user_uid, new_playlist) {
|
||||
users_db.get('users').find({uid: user_uid}).get(`playlists`).push(new_playlist).write();
|
||||
return true;
|
||||
}
|
||||
|
||||
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});
|
||||
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});
|
||||
return true;
|
||||
}
|
||||
|
||||
exports.removePlaylist = function(user_uid, playlistID, type) {
|
||||
users_db.get('users').find({uid: user_uid}).get(`playlists.${type}`).remove({id: playlistID}).write();
|
||||
exports.removePlaylist = function(user_uid, playlistID) {
|
||||
users_db.get('users').find({uid: user_uid}).get(`playlists`).remove({id: playlistID}).write();
|
||||
return true;
|
||||
}
|
||||
|
||||
exports.getUserPlaylists = function(user_uid, type) {
|
||||
exports.getUserPlaylists = function(user_uid, user_files = null) {
|
||||
const user = users_db.get('users').find({uid: user_uid}).value();
|
||||
return user['playlists'][type];
|
||||
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;
|
||||
}
|
||||
|
||||
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();
|
||||
exports.getUserPlaylist = function(user_uid, playlistID, requireSharing = false) {
|
||||
let playlist = users_db.get('users').find({uid: user_uid}).get(`playlists`).find({id: playlistID}).value();
|
||||
|
||||
// prevent unauthorized users from accessing the file info
|
||||
if (requireSharing && !playlist['sharingEnabled']) playlist = null;
|
||||
@@ -340,21 +356,22 @@ exports.getUserPlaylist = function(user_uid, playlistID, type, requireSharing =
|
||||
return playlist;
|
||||
}
|
||||
|
||||
exports.registerUserFile = function(user_uid, file_object, type) {
|
||||
users_db.get('users').find({uid: user_uid}).get(`files.${type}`)
|
||||
exports.registerUserFile = function(user_uid, file_object) {
|
||||
users_db.get('users').find({uid: user_uid}).get(`files`)
|
||||
.remove({
|
||||
path: file_object['path']
|
||||
}).write();
|
||||
|
||||
users_db.get('users').find({uid: user_uid}).get(`files.${type}`)
|
||||
users_db.get('users').find({uid: user_uid}).get(`files`)
|
||||
.push(file_object)
|
||||
.write();
|
||||
}
|
||||
|
||||
exports.deleteUserFile = function(user_uid, file_uid, type, blacklistMode = false) {
|
||||
exports.deleteUserFile = async function(user_uid, file_uid, blacklistMode = false) {
|
||||
let success = false;
|
||||
const file_obj = users_db.get('users').find({uid: user_uid}).get(`files.${type}`).find({uid: file_uid}).value();
|
||||
const file_obj = users_db.get('users').find({uid: user_uid}).get(`files`).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';
|
||||
|
||||
@@ -370,24 +387,24 @@ exports.deleteUserFile = function(user_uid, file_uid, type, 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.${type}`)
|
||||
users_db.get('users').find({uid: user_uid}).get(`files`)
|
||||
.remove({
|
||||
uid: file_uid
|
||||
}).write();
|
||||
if (fs.existsSync(full_path)) {
|
||||
if (await fs.pathExists(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 (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);
|
||||
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);
|
||||
}
|
||||
|
||||
fs.unlinkSync(full_path);
|
||||
await fs.unlink(full_path);
|
||||
|
||||
// do archive stuff
|
||||
|
||||
@@ -396,17 +413,17 @@ exports.deleteUserFile = function(user_uid, file_uid, type, 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 (fs.existsSync(archive_path)) {
|
||||
let line = youtube_id ? subscriptions_api.removeIDFromArchive(archive_path, youtube_id) : null;
|
||||
if (await fs.pathExists(archive_path)) {
|
||||
const line = youtube_id ? await 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;
|
||||
fs.appendFileSync(blacklistPath, line);
|
||||
await fs.appendFile(blacklistPath, line);
|
||||
}
|
||||
} else {
|
||||
logger.info(`Could not find archive file for ${type} files. Creating...`);
|
||||
fs.ensureFileSync(archive_path);
|
||||
await fs.ensureFile(archive_path);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -419,11 +436,11 @@ exports.deleteUserFile = function(user_uid, file_uid, type, blacklistMode = fals
|
||||
return success;
|
||||
}
|
||||
|
||||
exports.changeSharingMode = function(user_uid, file_uid, type, is_playlist, enabled) {
|
||||
exports.changeSharingMode = function(user_uid, file_uid, 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.${type}`).find({id: file_uid}) : user_db_obj.get(`files.${type}`).find({uid: file_uid});
|
||||
const file_db_obj = is_playlist ? user_db_obj.get(`playlists`).find({id: file_uid}) : user_db_obj.get(`files`).find({uid: file_uid});
|
||||
if (file_db_obj.value()) {
|
||||
success = true;
|
||||
file_db_obj.assign({sharingEnabled: enabled}).write();
|
||||
@@ -519,20 +536,14 @@ function generateUserObject(userid, username, hash, auth_method = 'internal') {
|
||||
name: username,
|
||||
uid: userid,
|
||||
passhash: auth_method === 'internal' ? hash : null,
|
||||
files: {
|
||||
audio: [],
|
||||
video: []
|
||||
},
|
||||
playlists: {
|
||||
audio: [],
|
||||
video: []
|
||||
},
|
||||
files: [],
|
||||
playlists: [],
|
||||
subscriptions: [],
|
||||
created: Date.now(),
|
||||
role: userid === 'admin' && auth_method === 'internal' ? 'admin' : 'user',
|
||||
permissions: [],
|
||||
permission_overrides: [],
|
||||
auth_method: auth_method
|
||||
auth_method: auth_method
|
||||
};
|
||||
return new_user;
|
||||
}
|
||||
|
||||
129
backend/categories.js
Normal file
129
backend/categories.js
Normal file
@@ -0,0 +1,129 @@
|
||||
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,
|
||||
}
|
||||
@@ -184,6 +184,7 @@ DEFAULT_CONFIG = {
|
||||
"Downloader": {
|
||||
"path-audio": "audio/",
|
||||
"path-video": "video/",
|
||||
"default_file_output": "",
|
||||
"use_youtubedl_archive": false,
|
||||
"custom_args": "",
|
||||
"safe_download_override": false,
|
||||
@@ -196,13 +197,17 @@ DEFAULT_CONFIG = {
|
||||
"allow_quality_select": true,
|
||||
"download_only_mode": false,
|
||||
"allow_multi_download_mode": true,
|
||||
"enable_downloads_manager": true
|
||||
"enable_downloads_manager": true,
|
||||
"allow_playlist_categorization": true
|
||||
},
|
||||
"API": {
|
||||
"use_API_key": false,
|
||||
"API_key": "",
|
||||
"use_youtube_API": false,
|
||||
"youtube_API_key": ""
|
||||
"youtube_API_key": "",
|
||||
"use_twitch_API": false,
|
||||
"twitch_API_key": "",
|
||||
"twitch_auto_download_chat": false
|
||||
},
|
||||
"Themes": {
|
||||
"default_theme": "default",
|
||||
@@ -211,7 +216,8 @@ DEFAULT_CONFIG = {
|
||||
"Subscriptions": {
|
||||
"allow_subscriptions": true,
|
||||
"subscriptions_base_path": "subscriptions/",
|
||||
"subscriptions_check_interval": "300"
|
||||
"subscriptions_check_interval": "300",
|
||||
"redownload_fresh_uploads": false
|
||||
},
|
||||
"Users": {
|
||||
"base_path": "users/",
|
||||
@@ -226,6 +232,7 @@ DEFAULT_CONFIG = {
|
||||
}
|
||||
},
|
||||
"Advanced": {
|
||||
"default_downloader": "youtube-dl",
|
||||
"use_default_downloading_agent": true,
|
||||
"custom_downloading_agent": "",
|
||||
"multi_user_mode": false,
|
||||
|
||||
@@ -18,6 +18,10 @@ 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'
|
||||
@@ -64,6 +68,10 @@ 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': {
|
||||
@@ -82,6 +90,18 @@ 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': {
|
||||
@@ -110,6 +130,10 @@ 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': {
|
||||
@@ -130,6 +154,10 @@ let CONFIG_ITEMS = {
|
||||
},
|
||||
|
||||
// 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'
|
||||
@@ -172,5 +200,5 @@ AVAILABLE_PERMISSIONS = [
|
||||
module.exports = {
|
||||
CONFIG_ITEMS: CONFIG_ITEMS,
|
||||
AVAILABLE_PERMISSIONS: AVAILABLE_PERMISSIONS,
|
||||
CURRENT_VERSION: 'v4.1'
|
||||
CURRENT_VERSION: 'v4.2'
|
||||
}
|
||||
|
||||
@@ -15,10 +15,10 @@ function initialize(input_db, input_users_db, input_logger) {
|
||||
setLogger(input_logger);
|
||||
}
|
||||
|
||||
function registerFileDB(file_path, type, multiUserMode = null, sub = null) {
|
||||
function registerFileDB(file_path, type, multiUserMode = null, sub = null, customPath = null, category = null) {
|
||||
let db_path = null;
|
||||
const file_id = file_path.substring(0, file_path.length-4);
|
||||
const file_object = generateFileObject(file_id, type, multiUserMode && multiUserMode.file_path, sub);
|
||||
const file_id = utils.removeFileExtension(file_path);
|
||||
const file_object = generateFileObject(file_id, type, customPath || multiUserMode && multiUserMode.file_path, sub);
|
||||
if (!file_object) {
|
||||
logger.error(`Could not find associated JSON file for ${type} file ${file_id}`);
|
||||
return false;
|
||||
@@ -27,14 +27,17 @@ function registerFileDB(file_path, type, multiUserMode = null, sub = null) {
|
||||
utils.fixVideoMetadataPerms(file_id, type, multiUserMode && multiUserMode.file_path);
|
||||
|
||||
// add thumbnail path
|
||||
file_object['thumbnailPath'] = utils.getDownloadedThumbnail(file_id, type, multiUserMode && multiUserMode.file_path);
|
||||
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.${type}`);
|
||||
db_path = users_db.get('users').find({uid: user_uid}).get(`files`);
|
||||
} else {
|
||||
db_path = db.get(`files.${type}`)
|
||||
db_path = db.get(`files`);
|
||||
}
|
||||
} else {
|
||||
if (multiUserMode) {
|
||||
@@ -94,18 +97,18 @@ function generateFileObject(id, type, customPath = null, sub = null) {
|
||||
var thumbnail = jsonobj.thumbnail;
|
||||
var duration = jsonobj.duration;
|
||||
var isaudio = type === 'audio';
|
||||
var file_obj = new utils.File(id, title, thumbnail, isaudio, duration, url, uploader, size, file_path, upload_date);
|
||||
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);
|
||||
return file_obj;
|
||||
}
|
||||
|
||||
function updatePlaylist(playlist, user_uid) {
|
||||
let playlistID = playlist.id;
|
||||
let type = playlist.type;
|
||||
let db_loc = null;
|
||||
if (user_uid) {
|
||||
db_loc = users_db.get('users').find({uid: user_uid}).get(`playlists.${type}`).find({id: playlistID});
|
||||
db_loc = users_db.get('users').find({uid: user_uid}).get(`playlists`).find({id: playlistID});
|
||||
} else {
|
||||
db_loc = db.get(`playlists.${type}`).find({id: playlistID});
|
||||
db_loc = db.get(`playlists`).find({id: playlistID});
|
||||
}
|
||||
db_loc.assign(playlist).write();
|
||||
return true;
|
||||
@@ -115,7 +118,7 @@ function getAppendedBasePathSub(sub, base_path) {
|
||||
return path.join(base_path, (sub.isPlaylist ? 'playlists/' : 'channels/'), sub.name);
|
||||
}
|
||||
|
||||
async function importUnregisteredFiles() {
|
||||
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
|
||||
@@ -132,14 +135,14 @@ async function importUnregisteredFiles() {
|
||||
// add user's audio dir to check list
|
||||
dirs_to_check.push({
|
||||
basePath: path.join(usersFileFolder, user.uid, 'audio'),
|
||||
dbPath: users_db.get('users').find({uid: user.uid}).get('files.audio'),
|
||||
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.video'),
|
||||
dbPath: users_db.get('users').find({uid: user.uid}).get('files'),
|
||||
type: 'video'
|
||||
});
|
||||
}
|
||||
@@ -153,14 +156,14 @@ async function importUnregisteredFiles() {
|
||||
// add audio dir to check list
|
||||
dirs_to_check.push({
|
||||
basePath: audioFolderPath,
|
||||
dbPath: db.get('files.audio'),
|
||||
dbPath: db.get('files'),
|
||||
type: 'audio'
|
||||
});
|
||||
|
||||
// add video dir to check list
|
||||
dirs_to_check.push({
|
||||
basePath: videoFolderPath,
|
||||
dbPath: db.get('files.video'),
|
||||
dbPath: db.get('files'),
|
||||
type: 'video'
|
||||
});
|
||||
}
|
||||
@@ -181,10 +184,16 @@ async function importUnregisteredFiles() {
|
||||
});
|
||||
}
|
||||
|
||||
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
|
||||
dirs_to_check.forEach(dir_to_check => {
|
||||
for (const dir_to_check of dirs_to_check) {
|
||||
// recursively get all files in dir's path
|
||||
const files = utils.getDownloadedFilesByType(dir_to_check.basePath, dir_to_check.type);
|
||||
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
|
||||
@@ -195,13 +204,32 @@ async function importUnregisteredFiles() {
|
||||
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,
|
||||
importUnregisteredFiles: importUnregisteredFiles
|
||||
getFileDirectoriesAndDBs: getFileDirectoriesAndDBs,
|
||||
importUnregisteredFiles: importUnregisteredFiles,
|
||||
getVideo: getVideo,
|
||||
setVideoProperty: setVideoProperty
|
||||
}
|
||||
|
||||
20
backend/package-lock.json
generated
20
backend/package-lock.json
generated
@@ -252,6 +252,14 @@
|
||||
"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",
|
||||
@@ -1072,6 +1080,11 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"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",
|
||||
@@ -1850,10 +1863,9 @@
|
||||
}
|
||||
},
|
||||
"moment": {
|
||||
"version": "2.27.0",
|
||||
"resolved": "https://registry.npmjs.org/moment/-/moment-2.27.0.tgz",
|
||||
"integrity": "sha512-al0MUK7cpIcglMv3YF13qSgdAIqxHTO7brRtaz3DlSULbqfazqkc5kEjNrLDOM7fsjshoFIihnU8snrP7zUvhQ==",
|
||||
"optional": true
|
||||
"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",
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
"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",
|
||||
@@ -42,6 +43,7 @@
|
||||
"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-id3": "^0.1.14",
|
||||
|
||||
@@ -6,7 +6,8 @@ var path = require('path');
|
||||
|
||||
var youtubedl = require('youtube-dl');
|
||||
const config_api = require('./config');
|
||||
var utils = require('./utils')
|
||||
const twitch_api = require('./twitch');
|
||||
var utils = require('./utils');
|
||||
|
||||
const debugMode = process.env.YTDL_MODE === 'debug';
|
||||
|
||||
@@ -79,17 +80,18 @@ async function getSubscriptionInfo(sub, user_uid = null) {
|
||||
else
|
||||
basePath = config_api.getConfigItem('ytdl_subscriptions_base_path');
|
||||
|
||||
return new Promise(resolve => {
|
||||
// get videos
|
||||
let downloadConfig = ['--dump-json', '--playlist-end', '1'];
|
||||
let useCookies = config_api.getConfigItem('ytdl_use_cookies');
|
||||
if (useCookies) {
|
||||
if (fs.existsSync(path.join(__dirname, 'appdata', 'cookies.txt'))) {
|
||||
downloadConfig.push('--cookies', path.join('appdata', 'cookies.txt'));
|
||||
} else {
|
||||
logger.warn('Cookies file could not be found. You can either upload one, or disable \'use cookies\' in the Advanced tab in the settings.');
|
||||
}
|
||||
// get videos
|
||||
let downloadConfig = ['--dump-json', '--playlist-end', '1'];
|
||||
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 => {
|
||||
youtubedl.exec(sub.url, downloadConfig, {}, function(err, output) {
|
||||
if (debugMode) {
|
||||
logger.info('Subscribe: got info for subscription ' + sub.id);
|
||||
@@ -113,7 +115,11 @@ async function getSubscriptionInfo(sub, user_uid = null) {
|
||||
continue;
|
||||
}
|
||||
if (!sub.name) {
|
||||
sub.name = sub.isPlaylist ? output_json.playlist_title : output_json.uploader;
|
||||
if (sub.isPlaylist) {
|
||||
sub.name = output_json.playlist_title ? output_json.playlist_title : output_json.playlist;
|
||||
} else {
|
||||
sub.name = output_json.uploader;
|
||||
}
|
||||
// if it's now valid, update
|
||||
if (sub.name) {
|
||||
if (user_uid)
|
||||
@@ -152,39 +158,36 @@ async function getSubscriptionInfo(sub, user_uid = null) {
|
||||
}
|
||||
|
||||
async function unsubscribe(sub, deleteMode, user_uid = null) {
|
||||
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 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;
|
||||
}
|
||||
// failed subs have no name, on unsubscribe they shouldn't error
|
||||
if (!sub.name) {
|
||||
return;
|
||||
}
|
||||
|
||||
const appendedBasePath = getAppendedBasePath(sub, basePath);
|
||||
if (deleteMode && fs.existsSync(appendedBasePath)) {
|
||||
if (sub.archive && fs.existsSync(sub.archive)) {
|
||||
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);
|
||||
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);
|
||||
}
|
||||
deleteFolderRecursive(appendedBasePath);
|
||||
await fs.rmdir(sub.archive);
|
||||
}
|
||||
});
|
||||
|
||||
await fs.remove(appendedBasePath);
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteSubscriptionFile(sub, file, deleteForever, file_uid = null, user_uid = null) {
|
||||
@@ -202,159 +205,98 @@ 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');
|
||||
|
||||
jsonExists = fs.existsSync(jsonPath);
|
||||
videoFileExists = fs.existsSync(videoFilePath);
|
||||
imageFileExists = fs.existsSync(imageFilePath);
|
||||
altImageFileExists = fs.existsSync(altImageFilePath);
|
||||
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');
|
||||
|
||||
if (jsonExists) {
|
||||
retrievedID = JSON.parse(fs.readFileSync(jsonPath, 'utf8'))['id'];
|
||||
fs.unlinkSync(jsonPath);
|
||||
}
|
||||
const [jsonExists, videoFileExists, imageFileExists, altImageFileExists] = await Promise.all([
|
||||
fs.pathExists(jsonPath),
|
||||
fs.pathExists(videoFilePath),
|
||||
fs.pathExists(imageFilePath),
|
||||
fs.pathExists(altImageFilePath),
|
||||
]);
|
||||
|
||||
if (imageFileExists) {
|
||||
fs.unlinkSync(imageFilePath);
|
||||
}
|
||||
if (jsonExists) {
|
||||
retrievedID = JSON.parse(await fs.readFile(jsonPath, 'utf8'))['id'];
|
||||
await fs.unlink(jsonPath);
|
||||
}
|
||||
|
||||
if (altImageFileExists) {
|
||||
fs.unlinkSync(altImageFilePath);
|
||||
}
|
||||
if (imageFileExists) {
|
||||
await fs.unlink(imageFilePath);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
if (altImageFileExists) {
|
||||
await fs.unlink(altImageFilePath);
|
||||
}
|
||||
|
||||
if (videoFileExists) {
|
||||
await fs.unlink(videoFilePath);
|
||||
if ((await fs.pathExists(jsonPath)) || (await fs.pathExists(videoFilePath))) {
|
||||
return false;
|
||||
} else {
|
||||
// TODO: tell user that the file didn't exist
|
||||
resolve(true);
|
||||
// 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;
|
||||
}
|
||||
|
||||
});
|
||||
} else {
|
||||
// TODO: tell user that the file didn't exist
|
||||
return 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 => {
|
||||
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.');
|
||||
}
|
||||
}
|
||||
|
||||
if (config_api.getConfigItem('ytdl_include_thumbnail')) {
|
||||
downloadConfig.push('--write-thumbnail');
|
||||
}
|
||||
|
||||
// get videos
|
||||
logger.verbose('Subscription: getting videos for subscription ' + sub.name);
|
||||
youtubedl.exec(sub.url, downloadConfig, {}, function(err, output) {
|
||||
youtubedl.exec(sub.url, downloadConfig, {}, async function(err, output) {
|
||||
updateSubscriptionProperty(sub, {downloading: false}, user_uid);
|
||||
logger.verbose('Subscription: finished check for ' + sub.name);
|
||||
if (err && !output) {
|
||||
logger.error(err.stderr);
|
||||
logger.error(err.stderr ? err.stderr : err.message);
|
||||
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 {
|
||||
@@ -364,7 +306,11 @@ 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
|
||||
fs.appendFileSync(archive_path, output['id']);
|
||||
if (sub.archive) {
|
||||
archive_dir = sub.archive;
|
||||
archive_path = path.join(archive_dir, 'archive.txt')
|
||||
fs.appendFileSync(archive_path, output['id']);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch(e) {
|
||||
@@ -377,6 +323,7 @@ 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;
|
||||
@@ -391,18 +338,101 @@ async function getVideosForSub(sub, user_uid = null) {
|
||||
|
||||
const reset_videos = i === 0;
|
||||
handleOutputJSON(sub, sub_db, output_json, multiUserMode, reset_videos);
|
||||
|
||||
// TODO: Potentially store downloaded files in db?
|
||||
|
||||
}
|
||||
|
||||
if (config_api.getConfigItem('ytdl_subscriptions_redownload_fresh_uploads')) {
|
||||
await setFreshUploads(sub, user_uid);
|
||||
checkVideosForFreshUploads(sub, user_uid);
|
||||
}
|
||||
|
||||
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) {
|
||||
@@ -415,17 +445,49 @@ 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 getAllSubscriptions(user_uid = null) {
|
||||
function getSubscriptions(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();
|
||||
@@ -433,6 +495,13 @@ 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();
|
||||
@@ -442,6 +511,21 @@ 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();
|
||||
@@ -449,6 +533,52 @@ 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) {
|
||||
@@ -456,23 +586,8 @@ function getAppendedBasePath(sub, base_path) {
|
||||
return path.join(base_path, (sub.isPlaylist ? 'playlists/' : 'channels/'), sub.name);
|
||||
}
|
||||
|
||||
// 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'});
|
||||
async function removeIDFromArchive(archive_path, id) {
|
||||
let data = await fs.readFile(archive_path, {encoding: 'utf-8'});
|
||||
if (!data) {
|
||||
logger.error('Archive could not be found.');
|
||||
return;
|
||||
@@ -493,13 +608,15 @@ function removeIDFromArchive(archive_path, id) {
|
||||
|
||||
// UPDATE FILE WITH NEW DATA
|
||||
const updatedData = dataArray.join('\n');
|
||||
fs.writeFileSync(archive_path, updatedData);
|
||||
await fs.writeFile(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,
|
||||
@@ -508,5 +625,6 @@ module.exports = {
|
||||
getVideosForSub : getVideosForSub,
|
||||
removeIDFromArchive : removeIDFromArchive,
|
||||
setLogger : setLogger,
|
||||
initialize : initialize
|
||||
initialize : initialize,
|
||||
updateSubscriptionPropertyMultiple : updateSubscriptionPropertyMultiple
|
||||
}
|
||||
|
||||
128
backend/twitch.js
Normal file
128
backend/twitch.js
Normal file
@@ -0,0 +1,128 @@
|
||||
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
|
||||
}
|
||||
@@ -4,6 +4,7 @@ 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;
|
||||
|
||||
@@ -19,34 +20,33 @@ function getTrueFileName(unfixed_path, type) {
|
||||
return fixed_path;
|
||||
}
|
||||
|
||||
function getDownloadedFilesByType(basePath, type) {
|
||||
async function getDownloadedFilesByType(basePath, type, full_metadata = false) {
|
||||
// return empty array if the path doesn't exist
|
||||
if (!fs.existsSync(basePath)) return [];
|
||||
if (!(await fs.pathExists(basePath))) return [];
|
||||
|
||||
let files = [];
|
||||
const ext = type === 'audio' ? 'mp3' : 'mp4';
|
||||
var located_files = recFindByExt(basePath, ext);
|
||||
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 = fs.statSync(file);
|
||||
var stats = await fs.stat(file);
|
||||
|
||||
var id = file_path.substring(0, file_path.length-4);
|
||||
var jsonobj = getJSONByType(type, id, basePath);
|
||||
var jsonobj = await getJSONByType(type, id, basePath);
|
||||
if (!jsonobj) continue;
|
||||
var title = jsonobj.title;
|
||||
var url = jsonobj.webpage_url;
|
||||
var uploader = jsonobj.uploader;
|
||||
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 thumbnail = jsonobj.thumbnail;
|
||||
var duration = jsonobj.duration;
|
||||
|
||||
var size = stats.size;
|
||||
|
||||
var isaudio = type === 'audio';
|
||||
var file_obj = new File(id, title, thumbnail, isaudio, duration, url, uploader, size, file, upload_date);
|
||||
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;
|
||||
@@ -105,19 +105,26 @@ function getDownloadedThumbnail(name, type, customPath = null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
function getExpectedFileSize(info_json) {
|
||||
if (info_json['filesize']) {
|
||||
return info_json['filesize'];
|
||||
}
|
||||
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];
|
||||
|
||||
const formats = info_json['format_id'].split('+');
|
||||
let expected_filesize = 0;
|
||||
formats.forEach(format_id => {
|
||||
info_json.formats.forEach(available_format => {
|
||||
if (available_format.format_id === format_id && available_format.filesize) {
|
||||
expected_filesize += available_format.filesize;
|
||||
}
|
||||
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;
|
||||
@@ -129,7 +136,7 @@ function fixVideoMetadataPerms(name, type, customPath = null) {
|
||||
: config_api.getConfigItem('ytdl_video_folder_path');
|
||||
|
||||
const ext = type === 'audio' ? '.mp3' : '.mp4';
|
||||
|
||||
|
||||
const files_to_fix = [
|
||||
// JSONs
|
||||
path.join(customPath, name + '.info.json'),
|
||||
@@ -158,33 +165,37 @@ function deleteJSONFile(name, type, customPath = null) {
|
||||
}
|
||||
|
||||
|
||||
function recFindByExt(base,ext,files,result)
|
||||
async function recFindByExt(base,ext,files,result)
|
||||
{
|
||||
files = files || fs.readdirSync(base)
|
||||
files = files || (await fs.readdir(base))
|
||||
result = result || []
|
||||
|
||||
files.forEach(
|
||||
function (file) {
|
||||
var newbase = path.join(base,file)
|
||||
if ( fs.statSync(newbase).isDirectory() )
|
||||
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 = recFindByExt(newbase,ext,fs.readdirSync(newbase),result)
|
||||
}
|
||||
else
|
||||
{
|
||||
if ( file.substr(-1*(ext.length+1)) == '.' + ext )
|
||||
{
|
||||
result.push(newbase)
|
||||
}
|
||||
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) {
|
||||
function File(id, title, thumbnailURL, isAudio, duration, url, uploader, size, path, upload_date, description, view_count, height, abr) {
|
||||
this.id = id;
|
||||
this.title = title;
|
||||
this.thumbnailURL = thumbnailURL;
|
||||
@@ -195,6 +206,10 @@ 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 = {
|
||||
@@ -207,5 +222,6 @@ module.exports = {
|
||||
deleteJSONFile: deleteJSONFile,
|
||||
getDownloadedFilesByType: getDownloadedFilesByType,
|
||||
recFindByExt: recFindByExt,
|
||||
removeFileExtension: removeFileExtension,
|
||||
File: File
|
||||
}
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
// 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 });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
@@ -1,17 +1,17 @@
|
||||
{
|
||||
"manifest_version": 2,
|
||||
"name": "YoutubeDL-Material",
|
||||
"version": "0.3",
|
||||
"version": "0.4",
|
||||
"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_icon": "favicon.png",
|
||||
"default_popup": "popup.html",
|
||||
"default_title": "YoutubeDL-Material"
|
||||
},
|
||||
"permissions": [
|
||||
"tabs",
|
||||
"storage"
|
||||
"storage",
|
||||
"contextMenus"
|
||||
],
|
||||
"options_ui": {
|
||||
"page": "options.html",
|
||||
|
||||
35
chrome-extension/popup.html
Normal file
35
chrome-extension/popup.html
Normal file
@@ -0,0 +1,35 @@
|
||||
<!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>
|
||||
50
chrome-extension/popup.js
Normal file
50
chrome-extension/popup.js
Normal file
@@ -0,0 +1,50 @@
|
||||
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);
|
||||
Binary file not shown.
Binary file not shown.
13012
package-lock.json
generated
13012
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
64
package.json
64
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "youtube-dl-material",
|
||||
"version": "4.1.0",
|
||||
"version": "4.2.0",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"ng": "ng",
|
||||
@@ -18,19 +18,20 @@
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@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",
|
||||
"@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",
|
||||
"core-js": "^2.4.1",
|
||||
"file-saver": "^2.0.2",
|
||||
"filesize": "^6.1.0",
|
||||
@@ -39,35 +40,34 @@
|
||||
"ng-lazyload-image": "^7.0.1",
|
||||
"ngx-avatar": "^4.0.0",
|
||||
"ngx-file-drop": "^9.0.1",
|
||||
"ngx-videogular": "^9.0.1",
|
||||
"rxjs": "^6.5.3",
|
||||
"rxjs": "^6.6.3",
|
||||
"rxjs-compat": "^6.0.0-rc.0",
|
||||
"tslib": "^1.10.0",
|
||||
"typescript": "~3.7.5",
|
||||
"tslib": "^2.0.0",
|
||||
"typescript": "~4.0.5",
|
||||
"web-animations-js": "^2.3.2",
|
||||
"zone.js": "~0.10.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-devkit/build-angular": "^0.901.0",
|
||||
"@angular/cli": "^9.0.7",
|
||||
"@angular/compiler-cli": "^9.0.7",
|
||||
"@angular/language-service": "^9.0.7",
|
||||
"@angular-devkit/build-angular": "^0.1100.4",
|
||||
"@angular/cli": "^11.0.4",
|
||||
"@angular/compiler-cli": "^11.0.4",
|
||||
"@angular/language-service": "^11.0.4",
|
||||
"@types/core-js": "^2.5.2",
|
||||
"@types/file-saver": "^2.0.1",
|
||||
"@types/jasmine": "2.5.45",
|
||||
"@types/jasmine": "~3.6.0",
|
||||
"@types/node": "^12.11.1",
|
||||
"codelyzer": "^5.1.2",
|
||||
"codelyzer": "^6.0.0",
|
||||
"electron": "^8.0.1",
|
||||
"jasmine-core": "~2.6.2",
|
||||
"jasmine-spec-reporter": "~4.1.0",
|
||||
"karma": "~1.7.0",
|
||||
"karma-chrome-launcher": "~2.1.1",
|
||||
"jasmine-core": "~3.6.0",
|
||||
"jasmine-spec-reporter": "~5.0.0",
|
||||
"karma": "~5.0.0",
|
||||
"karma-chrome-launcher": "~3.1.0",
|
||||
"karma-cli": "~1.0.1",
|
||||
"karma-coverage-istanbul-reporter": "^1.2.1",
|
||||
"karma-jasmine": "~1.1.0",
|
||||
"karma-jasmine-html-reporter": "^0.2.2",
|
||||
"protractor": "~5.1.2",
|
||||
"karma-coverage-istanbul-reporter": "~3.0.2",
|
||||
"karma-jasmine": "~4.0.0",
|
||||
"karma-jasmine-html-reporter": "^1.5.0",
|
||||
"protractor": "~7.0.0",
|
||||
"ts-node": "~3.0.4",
|
||||
"tslint": "~5.3.2"
|
||||
"tslint": "~6.1.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ const routes: Routes = [
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forRoot(routes, { useHash: true })],
|
||||
imports: [RouterModule.forRoot(routes, { useHash: true, relativeLinkResolution: 'legacy' })],
|
||||
exports: [RouterModule]
|
||||
})
|
||||
export class AppRoutingModule { }
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
<a *ngIf="postsService.config && enableDownloadsManager && (!postsService.config.Advanced.multi_user_mode || (postsService.isLoggedIn && postsService.permissions.includes('downloads_manager')))" mat-list-item (click)="postsService.sidepanel_mode === 'over' ? sidenav.close() : null" routerLink='/downloads'><ng-container i18n="Navigation menu Downloads Page title">Downloads</ng-container></a>
|
||||
<ng-container *ngIf="postsService.config && allowSubscriptions && postsService.subscriptions && (!postsService.config.Advanced.multi_user_mode || (postsService.isLoggedIn && postsService.permissions.includes('subscriptions')))">
|
||||
<mat-divider></mat-divider>
|
||||
<a *ngFor="let subscription of postsService.subscriptions" mat-list-item (click)="postsService.sidepanel_mode === 'over' ? sidenav.close() : null" [routerLink]="['/subscription', { id: subscription.id }]"><ngx-avatar [style.margin-right]="'10px'" size="32" [name]="subscription.name"></ngx-avatar><ng-container i18n="Navigation menu Downloads Page title">{{subscription.name}}</ng-container></a>
|
||||
<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>
|
||||
</ng-container>
|
||||
</mat-nav-list>
|
||||
</mat-sidenav>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { TestBed, async } from '@angular/core/testing';
|
||||
import { TestBed, waitForAsync } from '@angular/core/testing';
|
||||
|
||||
import { AppComponent } from './app.component';
|
||||
|
||||
describe('AppComponent', () => {
|
||||
beforeEach(async(() => {
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [
|
||||
AppComponent
|
||||
@@ -11,19 +11,19 @@ describe('AppComponent', () => {
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
it('should create the app', async(() => {
|
||||
it('should create the app', waitForAsync(() => {
|
||||
const fixture = TestBed.createComponent(AppComponent);
|
||||
const app = fixture.debugElement.componentInstance;
|
||||
expect(app).toBeTruthy();
|
||||
}));
|
||||
|
||||
it(`should have as title 'app'`, async(() => {
|
||||
it(`should have as title 'app'`, waitForAsync(() => {
|
||||
const fixture = TestBed.createComponent(AppComponent);
|
||||
const app = fixture.debugElement.componentInstance;
|
||||
expect(app.title).toEqual('app');
|
||||
}));
|
||||
|
||||
it('should render title in a h1 tag', async(() => {
|
||||
it('should render title in a h1 tag', waitForAsync(() => {
|
||||
const fixture = TestBed.createComponent(AppComponent);
|
||||
fixture.detectChanges();
|
||||
const compiled = fixture.debugElement.nativeElement;
|
||||
|
||||
@@ -116,6 +116,8 @@ export class AppComponent implements OnInit, AfterViewInit {
|
||||
if (this.allowSubscriptions) {
|
||||
this.postsService.reloadSubscriptions();
|
||||
}
|
||||
|
||||
this.postsService.reloadCategories();
|
||||
}
|
||||
|
||||
// theme stuff
|
||||
|
||||
@@ -32,14 +32,17 @@ 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, HttpClient } from '@angular/common/http';
|
||||
import { HttpClientModule, HTTP_INTERCEPTORS } 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 { VgCoreModule, VgControlsModule, VgOverlayPlayModule, VgBufferingModule } from 'ngx-videogular';
|
||||
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 { InputDialogComponent } from './input-dialog/input-dialog.component';
|
||||
import { LazyLoadImageModule, IsVisibleProps } from 'ng-lazyload-image';
|
||||
import { audioFilesMouseHovering, videoFilesMouseHovering, audioFilesOpened, videoFilesOpened } from './main/main.component';
|
||||
@@ -79,6 +82,10 @@ 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');
|
||||
|
||||
@@ -105,6 +112,7 @@ export function isVisible({ event, element, scrollContainer, offset }: IsVisible
|
||||
VideoInfoDialogComponent,
|
||||
ArgModifierDialogComponent,
|
||||
HighlightPipe,
|
||||
LinkifyPipe,
|
||||
UpdaterComponent,
|
||||
UpdateProgressDialogComponent,
|
||||
ShareMediaDialogComponent,
|
||||
@@ -123,7 +131,10 @@ export function isVisible({ event, element, scrollContainer, offset }: IsVisible
|
||||
UnifiedFileCardComponent,
|
||||
RecentVideosComponent,
|
||||
EditSubscriptionDialogComponent,
|
||||
CustomPlaylistsComponent
|
||||
CustomPlaylistsComponent,
|
||||
EditCategoryDialogComponent,
|
||||
TwitchChatComponent,
|
||||
SeeMoreComponent
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
@@ -181,10 +192,12 @@ export function isVisible({ event, element, scrollContainer, offset }: IsVisible
|
||||
SettingsComponent
|
||||
],
|
||||
providers: [
|
||||
PostsService
|
||||
PostsService,
|
||||
{ provide: HTTP_INTERCEPTORS, useClass: H401Interceptor, multi: true }
|
||||
],
|
||||
exports: [
|
||||
HighlightPipe
|
||||
HighlightPipe,
|
||||
LinkifyPipe
|
||||
],
|
||||
bootstrap: [AppComponent]
|
||||
})
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div *ngFor="let playlist of playlists; let i = index" class="mb-2 mt-2" [ngClass]="[ postsService.card_size === 'small' ? 'col-2 small-col' : '', postsService.card_size === 'medium' ? 'col-6 col-lg-4 medium-col' : '', postsService.card_size === 'large' ? 'col-12 large-col' : '' ]">
|
||||
<app-unified-file-card [index]="i" [card_size]="postsService.card_size" (goToFile)="goToPlaylist($event)" [file_obj]="playlist" [is_playlist]="true" (editPlaylist)="editPlaylistDialog($event)" (deleteFile)="deletePlaylist($event)" [loading]="false"></app-unified-file-card>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
|
||||
import { CustomPlaylistsComponent } from './custom-playlists.component';
|
||||
|
||||
@@ -6,7 +6,7 @@ describe('CustomPlaylistsComponent', () => {
|
||||
let component: CustomPlaylistsComponent;
|
||||
let fixture: ComponentFixture<CustomPlaylistsComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ CustomPlaylistsComponent ]
|
||||
})
|
||||
|
||||
@@ -62,7 +62,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}]);
|
||||
this.router.navigate(['/player', {fileNames: fileNames.join('|nvr|'), type: type, id: playlistID, uid: playlistID, auto: playlist.auto}]);
|
||||
}
|
||||
} else {
|
||||
// playlist not found
|
||||
@@ -83,7 +83,7 @@ export class CustomPlaylistsComponent implements OnInit {
|
||||
const playlist = args.file;
|
||||
const index = args.index;
|
||||
const playlistID = playlist.id;
|
||||
this.postsService.removePlaylist(playlistID, 'audio').subscribe(res => {
|
||||
this.postsService.removePlaylist(playlistID, playlist.type).subscribe(res => {
|
||||
if (res['success']) {
|
||||
this.playlists.splice(index, 1);
|
||||
this.postsService.openSnackBar('Playlist successfully removed.', '');
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
|
||||
import { DownloadsComponent } from './downloads.component';
|
||||
|
||||
@@ -6,7 +6,7 @@ describe('DownloadsComponent', () => {
|
||||
let component: DownloadsComponent;
|
||||
let fixture: ComponentFixture<DownloadsComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ DownloadsComponent ]
|
||||
})
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
|
||||
import { LoginComponent } from './login.component';
|
||||
|
||||
@@ -6,7 +6,7 @@ describe('LoginComponent', () => {
|
||||
let component: LoginComponent;
|
||||
let fixture: ComponentFixture<LoginComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ LoginComponent ]
|
||||
})
|
||||
|
||||
@@ -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) {
|
||||
if (this.postsService.isLoggedIn && localStorage.getItem('jwt_token') !== 'null') {
|
||||
this.router.navigate(['/home']);
|
||||
}
|
||||
this.postsService.service_initialized.subscribe(init => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
|
||||
import { LogsViewerComponent } from './logs-viewer.component';
|
||||
|
||||
@@ -6,7 +6,7 @@ describe('LogsViewerComponent', () => {
|
||||
let component: LogsViewerComponent;
|
||||
let fixture: ComponentFixture<LogsViewerComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ LogsViewerComponent ]
|
||||
})
|
||||
|
||||
@@ -61,7 +61,8 @@ 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'
|
||||
submitText: 'Clear',
|
||||
warnSubmitColor: true
|
||||
}
|
||||
});
|
||||
dialogRef.afterClosed().subscribe(confirmed => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
|
||||
import { ManageRoleComponent } from './manage-role.component';
|
||||
|
||||
@@ -6,7 +6,7 @@ describe('ManageRoleComponent', () => {
|
||||
let component: ManageRoleComponent;
|
||||
let fixture: ComponentFixture<ManageRoleComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ ManageRoleComponent ]
|
||||
})
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
|
||||
import { ManageUserComponent } from './manage-user.component';
|
||||
|
||||
@@ -6,7 +6,7 @@ describe('ManageUserComponent', () => {
|
||||
let component: ManageUserComponent;
|
||||
let fixture: ComponentFixture<ManageUserComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ ManageUserComponent ]
|
||||
})
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
|
||||
import { ModifyUsersComponent } from './modify-users.component';
|
||||
|
||||
@@ -6,7 +6,7 @@ describe('ModifyUsersComponent', () => {
|
||||
let component: ModifyUsersComponent;
|
||||
let fixture: ComponentFixture<ModifyUsersComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ ModifyUsersComponent ]
|
||||
})
|
||||
|
||||
@@ -30,16 +30,24 @@
|
||||
<div>
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<ng-container *ngIf="normal_files_received">
|
||||
<div *ngFor="let file of filtered_files; let i = index" class="mb-2 mt-2 d-flex justify-content-center" [ngClass]="[ postsService.card_size === 'small' ? 'col-2 small-col' : '', postsService.card_size === 'medium' ? 'col-6 col-lg-4 medium-col' : '', postsService.card_size === 'large' ? 'col-12 large-col' : '' ]">
|
||||
<app-unified-file-card [index]="i" [card_size]="postsService.card_size" (goToFile)="goToFile($event)" (goToSubscription)="goToSubscription($event)" [file_obj]="file" [use_youtubedl_archive]="postsService.config['Downloader']['use_youtubedl_archive']" [loading]="false" (deleteFile)="deleteFile($event)"></app-unified-file-card>
|
||||
<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" [loading]="true" [theme]="postsService.theme"></app-unified-file-card>
|
||||
<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>
|
||||
</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>
|
||||
@@ -47,6 +47,10 @@
|
||||
top: 10px;
|
||||
}
|
||||
|
||||
.paginator {
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.my-videos-title {
|
||||
text-align: center;
|
||||
position: relative;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
|
||||
import { RecentVideosComponent } from './recent-videos.component';
|
||||
|
||||
@@ -6,7 +6,7 @@ describe('RecentVideosComponent', () => {
|
||||
let component: RecentVideosComponent;
|
||||
let fixture: ComponentFixture<RecentVideosComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ RecentVideosComponent ]
|
||||
})
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { Component, OnInit, ViewChild } 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',
|
||||
@@ -50,10 +51,16 @@ 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');
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -91,7 +98,8 @@ 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));
|
||||
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});
|
||||
}
|
||||
|
||||
filterByProperty(prop) {
|
||||
@@ -100,6 +108,7 @@ 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) {
|
||||
@@ -118,10 +127,11 @@ export class RecentVideosComponent implements OnInit {
|
||||
this.normal_files_received = false;
|
||||
this.postsService.getAllFiles().subscribe(res => {
|
||||
this.files = res['files'];
|
||||
this.files.forEach(file => {
|
||||
file.duration = typeof file.duration !== 'string' ? file.duration : this.durationStringToNumber(file.duration);
|
||||
});
|
||||
this.files.sort(this.sortFiles);
|
||||
for (let i = 0; i < this.files.length; i++) {
|
||||
const file = this.files[i];
|
||||
file.duration = typeof file.duration !== 'string' ? file.duration : this.durationStringToNumber(file.duration);
|
||||
}
|
||||
if (this.search_mode) {
|
||||
this.filterFiles(this.search_text);
|
||||
} else {
|
||||
@@ -133,6 +143,8 @@ export class RecentVideosComponent implements OnInit {
|
||||
localStorage.setItem('cached_file_count', '' + this.files.length);
|
||||
|
||||
this.normal_files_received = true;
|
||||
|
||||
this.paged_data = this.filtered_files.slice(0, 10);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -210,7 +222,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, false).subscribe(delRes => {
|
||||
this.postsService.deleteFile(name, type).subscribe(delRes => {
|
||||
// reload mp4s
|
||||
this.getAllFiles();
|
||||
});
|
||||
@@ -226,17 +238,17 @@ export class RecentVideosComponent implements OnInit {
|
||||
const blacklistMode = args.blacklistMode;
|
||||
|
||||
if (file.sub_id) {
|
||||
this.deleteSubscriptionFile(file, index, blacklistMode);
|
||||
this.deleteSubscriptionFile(file, blacklistMode);
|
||||
} else {
|
||||
this.deleteNormalFile(file, index, blacklistMode);
|
||||
this.deleteNormalFile(file, blacklistMode);
|
||||
}
|
||||
}
|
||||
|
||||
deleteNormalFile(file, index, blacklistMode = false) {
|
||||
this.postsService.deleteFile(file.uid, file.isAudio, blacklistMode).subscribe(result => {
|
||||
deleteNormalFile(file, blacklistMode = false) {
|
||||
this.postsService.deleteFile(file.uid, file.isAudio ? 'audio' : 'video', blacklistMode).subscribe(result => {
|
||||
if (result) {
|
||||
this.postsService.openSnackBar('Delete success!', 'OK.');
|
||||
this.files.splice(index, 1);
|
||||
this.removeFileCard(file);
|
||||
} else {
|
||||
this.postsService.openSnackBar('Delete failed!', 'OK.');
|
||||
}
|
||||
@@ -245,30 +257,39 @@ export class RecentVideosComponent implements OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
deleteSubscriptionFile(file, index, blacklistMode = false) {
|
||||
deleteSubscriptionFile(file, blacklistMode = false) {
|
||||
if (blacklistMode) {
|
||||
this.deleteForever(file, index);
|
||||
this.deleteForever(file);
|
||||
} else {
|
||||
this.deleteAndRedownload(file, index);
|
||||
this.deleteAndRedownload(file);
|
||||
}
|
||||
}
|
||||
|
||||
deleteAndRedownload(file, index) {
|
||||
deleteAndRedownload(file) {
|
||||
const sub = this.postsService.getSubscriptionByID(file.sub_id);
|
||||
this.postsService.deleteSubscriptionFile(sub, file.id, false, file.uid).subscribe(res => {
|
||||
this.postsService.openSnackBar(`Successfully deleted file: '${file.id}'`);
|
||||
this.files.splice(index, 1);
|
||||
this.removeFileCard(file);
|
||||
});
|
||||
}
|
||||
|
||||
deleteForever(file, index) {
|
||||
deleteForever(file) {
|
||||
const sub = this.postsService.getSubscriptionByID(file.sub_id);
|
||||
this.postsService.deleteSubscriptionFile(sub, file.id, true, file.uid).subscribe(res => {
|
||||
this.postsService.openSnackBar(`Successfully deleted file: '${file.id}'`);
|
||||
this.files.splice(index, 1);
|
||||
this.removeFileCard(file);
|
||||
});
|
||||
}
|
||||
|
||||
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) {
|
||||
@@ -276,13 +297,18 @@ 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);
|
||||
}
|
||||
}
|
||||
|
||||
11
src/app/components/see-more/see-more.component.html
Normal file
11
src/app/components/see-more/see-more.component.html
Normal file
@@ -0,0 +1,11 @@
|
||||
<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>
|
||||
7
src/app/components/see-more/see-more.component.scss
Normal file
7
src/app/components/see-more/see-more.component.scss
Normal file
@@ -0,0 +1,7 @@
|
||||
.text {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
25
src/app/components/see-more/see-more.component.spec.ts
Normal file
25
src/app/components/see-more/see-more.component.spec.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
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();
|
||||
});
|
||||
});
|
||||
60
src/app/components/see-more/see-more.component.ts
Normal file
60
src/app/components/see-more/see-more.component.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
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>")
|
||||
}
|
||||
|
||||
}
|
||||
12
src/app/components/twitch-chat/twitch-chat.component.html
Normal file
12
src/app/components/twitch-chat/twitch-chat.component.html
Normal file
@@ -0,0 +1,12 @@
|
||||
<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>
|
||||
13
src/app/components/twitch-chat/twitch-chat.component.scss
Normal file
13
src/app/components/twitch-chat/twitch-chat.component.scss
Normal file
@@ -0,0 +1,13 @@
|
||||
.chat-container {
|
||||
height: 100%;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
.download-button {
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
.downloading-spinner {
|
||||
top: 50%;
|
||||
left: 80px;
|
||||
}
|
||||
25
src/app/components/twitch-chat/twitch-chat.component.spec.ts
Normal file
25
src/app/components/twitch-chat/twitch-chat.component.spec.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
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();
|
||||
});
|
||||
});
|
||||
138
src/app/components/twitch-chat/twitch-chat.component.ts
Normal file
138
src/app/components/twitch-chat/twitch-chat.component.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
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;
|
||||
}
|
||||
@@ -1,7 +1,24 @@
|
||||
<div (mouseover)="elevated=true" (mouseout)="elevated=false" style="position: relative; width: fit-content;">
|
||||
<div *ngIf="!loading" class="download-time"><mat-icon class="audio-video-icon">{{(file_obj.type === 'audio' || file_obj.isAudio) ? 'audiotrack' : 'movie'}}</mat-icon> {{file_obj.registered | date:'shortDate'}}</div>
|
||||
<div (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>
|
||||
|
||||
<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>
|
||||
<button [disabled]="loading" [matMenuTriggerFor]="action_menu" class="menuButton" mat-icon-button><mat-icon>more_vert</mat-icon></button>
|
||||
<!-- 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>
|
||||
<mat-menu #action_menu="matMenu">
|
||||
<ng-container *ngIf="!is_playlist && !loading">
|
||||
<button (click)="openFileInfoDialog()" mat-menu-item><mat-icon>info</mat-icon><ng-container i18n="Video info button">Info</ng-container></button>
|
||||
@@ -29,7 +46,7 @@
|
||||
<div style="padding:5px">
|
||||
<div *ngIf="!loading && file_obj.thumbnailURL" class="img-div">
|
||||
<div style="position: relative">
|
||||
<img [ngClass]="{'image-small': card_size === 'small', 'image': card_size === 'medium', 'image-large': card_size === 'large'}" [src]="file_obj.thumbnailBlob ? thumbnailBlobURL : file_obj.thumbnailURL" alt="Thumbnail">
|
||||
<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">
|
||||
<div class="duration-time">
|
||||
{{file_length}}
|
||||
</div>
|
||||
|
||||
@@ -110,7 +110,12 @@
|
||||
position: absolute;
|
||||
top: 1px;
|
||||
left: 5px;
|
||||
z-index: 99999;
|
||||
z-index: 999;
|
||||
width: calc(100% - 8px);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
display: block;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.audio-video-icon {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { ComponentFixture, TestBed, waitForAsync } 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(async(() => {
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ UnifiedFileCardComponent ]
|
||||
})
|
||||
|
||||
@@ -1,7 +1,22 @@
|
||||
import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core';
|
||||
import { Component, OnInit, Input, Output, EventEmitter, ViewChild } 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',
|
||||
@@ -28,11 +43,18 @@ export class UnifiedFileCardComponent implements OnInit {
|
||||
@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
|
||||
@@ -47,11 +69,12 @@ export class UnifiedFileCardComponent implements OnInit {
|
||||
this.file_length = fancyTimeFormat(this.file_obj.duration);
|
||||
}
|
||||
|
||||
if (this.file_obj && this.file_obj.thumbnailBlob) {
|
||||
const mime = getMimeByFilename(this.file_obj.thumbnailPath);
|
||||
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.thumbnailBlobURL = this.sanitizer.bypassSecurityTrustUrl(bloburl);*/
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,6 +110,15 @@ 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) {
|
||||
|
||||
@@ -1 +1 @@
|
||||
export const CURRENT_VERSION = 'v4.1';
|
||||
export const CURRENT_VERSION = 'v4.2';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
|
||||
import { CreatePlaylistComponent } from './create-playlist.component';
|
||||
|
||||
@@ -6,7 +6,7 @@ describe('CreatePlaylistComponent', () => {
|
||||
let component: CreatePlaylistComponent;
|
||||
let fixture: ComponentFixture<CreatePlaylistComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ CreatePlaylistComponent ]
|
||||
})
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
|
||||
import { AboutDialogComponent } from './about-dialog.component';
|
||||
|
||||
@@ -6,7 +6,7 @@ describe('AboutDialogComponent', () => {
|
||||
let component: AboutDialogComponent;
|
||||
let fixture: ComponentFixture<AboutDialogComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ AboutDialogComponent ]
|
||||
})
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { ComponentFixture, TestBed, waitForAsync } 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(async(() => {
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ AddUserDialogComponent ]
|
||||
})
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { ComponentFixture, TestBed, waitForAsync } 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(async(() => {
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ ArgModifierDialogComponent ]
|
||||
})
|
||||
|
||||
@@ -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="primary" mat-flat-button type="submit" (click)="confirmClicked()">{{submitText}}</button>
|
||||
<button [color]="warnSubmitColor ? 'warn' : 'primary'" mat-flat-button type="submit" (click)="confirmClicked()">{{submitText}}</button>
|
||||
<div class="mat-spinner" *ngIf="submitClicked">
|
||||
<mat-spinner [diameter]="25"></mat-spinner>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
|
||||
import { ConfirmDialogComponent } from './confirm-dialog.component';
|
||||
|
||||
@@ -6,7 +6,7 @@ describe('ConfirmDialogComponent', () => {
|
||||
let component: ConfirmDialogComponent;
|
||||
let fixture: ComponentFixture<ConfirmDialogComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ ConfirmDialogComponent ]
|
||||
})
|
||||
|
||||
@@ -15,12 +15,14 @@ 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) {
|
||||
|
||||
@@ -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 overrride 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 override your previous cookies. Also note that cookies are instance-wide, not per-user.</p>
|
||||
</div>
|
||||
<div style="margin-top: 10px;">
|
||||
<table class="table">
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { ComponentFixture, TestBed, waitForAsync } 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(async(() => {
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ CookiesUploaderDialogComponent ]
|
||||
})
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
<h4 mat-dialog-title><ng-container i18n="Editing category dialog title">Editing category</ng-container> {{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>
|
||||
@@ -0,0 +1,16 @@
|
||||
.operator-select {
|
||||
width: 55px;
|
||||
}
|
||||
|
||||
.property-select {
|
||||
margin-left: 10px;
|
||||
width: 110px;
|
||||
}
|
||||
|
||||
.comparator-select {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.value-input {
|
||||
margin-left: 10px;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
|
||||
import { EditCategoryDialogComponent } from './edit-category-dialog.component';
|
||||
|
||||
describe('EditCategoryDialogComponent', () => {
|
||||
let component: EditCategoryDialogComponent;
|
||||
let fixture: ComponentFixture<EditCategoryDialogComponent>;
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ EditCategoryDialogComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(EditCategoryDialogComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,111 @@
|
||||
import { Component, OnInit, Inject } from '@angular/core';
|
||||
import { MAT_DIALOG_DATA } from '@angular/material/dialog';
|
||||
import { PostsService } from 'app/posts.services';
|
||||
|
||||
@Component({
|
||||
selector: 'app-edit-category-dialog',
|
||||
templateUrl: './edit-category-dialog.component.html',
|
||||
styleUrls: ['./edit-category-dialog.component.scss']
|
||||
})
|
||||
export class EditCategoryDialogComponent implements OnInit {
|
||||
|
||||
updating = false;
|
||||
original_category = null;
|
||||
category = null;
|
||||
|
||||
propertyOptions = [
|
||||
{
|
||||
value: 'fulltitle',
|
||||
label: 'Title'
|
||||
},
|
||||
{
|
||||
value: 'id',
|
||||
label: 'ID'
|
||||
},
|
||||
{
|
||||
value: 'webpage_url',
|
||||
label: 'URL'
|
||||
},
|
||||
{
|
||||
value: 'view_count',
|
||||
label: 'Views'
|
||||
},
|
||||
{
|
||||
value: 'uploader',
|
||||
label: 'Uploader'
|
||||
},
|
||||
{
|
||||
value: '_filename',
|
||||
label: 'File Name'
|
||||
},
|
||||
{
|
||||
value: 'tags',
|
||||
label: 'Tags'
|
||||
}
|
||||
];
|
||||
|
||||
comparatorOptions = [
|
||||
{
|
||||
value: 'includes',
|
||||
label: 'includes'
|
||||
},
|
||||
{
|
||||
value: 'not_includes',
|
||||
label: 'not includes'
|
||||
},
|
||||
{
|
||||
value: 'equals',
|
||||
label: 'equals'
|
||||
},
|
||||
{
|
||||
value: 'not_equals',
|
||||
label: 'not equals'
|
||||
},
|
||||
|
||||
];
|
||||
|
||||
constructor(@Inject(MAT_DIALOG_DATA) public data: any, private postsService: PostsService) {
|
||||
if (this.data) {
|
||||
this.original_category = this.data.category;
|
||||
this.category = JSON.parse(JSON.stringify(this.original_category));
|
||||
}
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
}
|
||||
|
||||
addNewRule() {
|
||||
this.category['rules'].push({
|
||||
preceding_operator: 'or',
|
||||
property: 'fulltitle',
|
||||
comparator: 'includes',
|
||||
value: ''
|
||||
});
|
||||
}
|
||||
|
||||
saveClicked() {
|
||||
this.updating = true;
|
||||
this.postsService.updateCategory(this.category).subscribe(res => {
|
||||
this.updating = false;
|
||||
this.original_category = JSON.parse(JSON.stringify(this.category));
|
||||
this.postsService.reloadCategories();
|
||||
}, err => {
|
||||
this.updating = false;
|
||||
console.error(err);
|
||||
});
|
||||
}
|
||||
|
||||
categoryChanged() {
|
||||
return JSON.stringify(this.category) === JSON.stringify(this.original_category);
|
||||
}
|
||||
|
||||
swapRules(original_index, new_index) {
|
||||
[this.category.rules[original_index], this.category.rules[new_index]] = [this.category.rules[new_index],
|
||||
this.category.rules[original_index]];
|
||||
}
|
||||
|
||||
removeRule(index) {
|
||||
this.category['rules'].splice(index, 1);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,8 +1,11 @@
|
||||
<h4 mat-dialog-title i18n="Edit subscription dialog title">Editing {{sub.name}}</h4>
|
||||
<h4 mat-dialog-title><ng-container i18n="Edit subscription dialog title prefix">Editing</ng-container> {{sub.name}} <ng-container *ngIf="sub.paused" i18n="Paused suffix">(Paused)</ng-container></h4>
|
||||
|
||||
<mat-dialog-content>
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-12 mt-3">
|
||||
<mat-checkbox [(ngModel)]="new_sub.paused"><ng-container i18n="Paused subscription setting">Paused</ng-container></mat-checkbox>
|
||||
</div>
|
||||
<div class="col-12 mt-3">
|
||||
<mat-checkbox (change)="downloadAllToggled()" [(ngModel)]="download_all"><ng-container i18n="Download all uploads subscription setting">Download all uploads</ng-container></mat-checkbox>
|
||||
</div>
|
||||
@@ -24,7 +27,14 @@
|
||||
<mat-checkbox [disabled]="true" [(ngModel)]="audioOnlyMode"><ng-container i18n="Streaming-only mode">Audio-only mode</ng-container></mat-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<div class="col-12 mt-2">
|
||||
<mat-form-field>
|
||||
<mat-select placeholder="Max quality" i18n-placeholder="Max quality placeholder" [disabled]="new_sub.type === 'audio'" [(ngModel)]="new_sub.maxQuality">
|
||||
<mat-option *ngFor="let available_quality of available_qualities" [value]="available_quality['value']">{{available_quality['label']}}</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="col-12 mt-1">
|
||||
<div>
|
||||
<mat-checkbox [disabled]="new_sub.type === 'audio'" [(ngModel)]="new_sub.streamingOnly"><ng-container i18n="Streaming-only mode">Streaming-only mode</ng-container></mat-checkbox>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
|
||||
import { EditSubscriptionDialogComponent } from './edit-subscription-dialog.component';
|
||||
|
||||
@@ -6,7 +6,7 @@ describe('EditSubscriptionDialogComponent', () => {
|
||||
let component: EditSubscriptionDialogComponent;
|
||||
let fixture: ComponentFixture<EditSubscriptionDialogComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ EditSubscriptionDialogComponent ]
|
||||
})
|
||||
|
||||
@@ -22,6 +22,36 @@ export class EditSubscriptionDialogComponent implements OnInit {
|
||||
audioOnlyMode = null;
|
||||
download_all = null;
|
||||
|
||||
available_qualities = [
|
||||
{
|
||||
'label': 'Best',
|
||||
'value': 'best'
|
||||
},
|
||||
{
|
||||
'label': '4K',
|
||||
'value': '2160'
|
||||
},
|
||||
{
|
||||
'label': '1440p',
|
||||
'value': '1440'
|
||||
},
|
||||
{
|
||||
'label': '1080p',
|
||||
'value': '1080'
|
||||
},
|
||||
{
|
||||
'label': '720p',
|
||||
'value': '720'
|
||||
},
|
||||
{
|
||||
'label': '480p',
|
||||
'value': '480'
|
||||
},
|
||||
{
|
||||
'label': '360p',
|
||||
'value': '360'
|
||||
}
|
||||
];
|
||||
|
||||
time_units = [
|
||||
'day',
|
||||
@@ -31,24 +61,24 @@ export class EditSubscriptionDialogComponent implements OnInit {
|
||||
];
|
||||
|
||||
constructor(@Inject(MAT_DIALOG_DATA) public data: any, private dialog: MatDialog, private postsService: PostsService) {
|
||||
this.sub = this.data.sub;
|
||||
this.sub = JSON.parse(JSON.stringify(this.data.sub));
|
||||
this.new_sub = JSON.parse(JSON.stringify(this.sub));
|
||||
|
||||
// ignore videos to keep requests small
|
||||
delete this.sub['videos'];
|
||||
delete this.new_sub['videos'];
|
||||
|
||||
this.audioOnlyMode = this.sub.type === 'audio';
|
||||
this.download_all = !this.sub.timerange;
|
||||
|
||||
if (this.sub.timerange) {
|
||||
const timerange_str = this.sub.timerange.split('-')[1];
|
||||
console.log(timerange_str);
|
||||
const number = timerange_str.replace(/\D/g,'');
|
||||
let units = timerange_str.replace(/[0-9]/g, '');
|
||||
|
||||
console.log(units);
|
||||
|
||||
// // remove plural on units
|
||||
// if (units[units.length-1] === 's') {
|
||||
// units = units.substring(0, units.length-1);
|
||||
// }
|
||||
if (+number === 1) {
|
||||
units = units.replace('s', '');
|
||||
}
|
||||
|
||||
this.timerange_amount = parseInt(number);
|
||||
this.timerange_unit = units;
|
||||
@@ -71,9 +101,10 @@ export class EditSubscriptionDialogComponent implements OnInit {
|
||||
}
|
||||
|
||||
saveSubscription() {
|
||||
this.postsService.updateSubscription(this.sub).subscribe(res => {
|
||||
this.postsService.updateSubscription(this.new_sub).subscribe(res => {
|
||||
this.sub = this.new_sub;
|
||||
this.new_sub = JSON.parse(JSON.stringify(this.sub));
|
||||
this.postsService.reloadSubscriptions();
|
||||
})
|
||||
}
|
||||
|
||||
@@ -85,12 +116,16 @@ export class EditSubscriptionDialogComponent implements OnInit {
|
||||
}
|
||||
|
||||
timerangeChanged(value, select_changed) {
|
||||
console.log(this.timerange_amount);
|
||||
console.log(this.timerange_unit);
|
||||
if (+this.timerange_amount === 1) {
|
||||
this.timerange_unit = this.timerange_unit.replace('s', '');
|
||||
} else {
|
||||
if (!this.timerange_unit.includes('s')) {
|
||||
this.timerange_unit += 's';
|
||||
}
|
||||
}
|
||||
|
||||
if (this.timerange_amount && this.timerange_unit && !this.download_all) {
|
||||
this.new_sub.timerange = 'now-' + this.timerange_amount.toString() + this.timerange_unit;
|
||||
console.log(this.new_sub.timerange);
|
||||
} else {
|
||||
this.new_sub.timerange = null;
|
||||
}
|
||||
|
||||
@@ -8,14 +8,24 @@
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 10px; height: 40px;">
|
||||
<div style="float: left">
|
||||
<span *ngIf="reverse_order === false" i18n="Normal order">Normal order </span>
|
||||
<span *ngIf="reverse_order === true" i18n="Reverse order">Reverse order </span>
|
||||
<button (click)="togglePlaylistOrder()" mat-icon-button><mat-icon>{{!reverse_order ? 'arrow_downward' : 'arrow_upward'}}</mat-icon></button>
|
||||
</div>
|
||||
|
||||
<div style="float: right">
|
||||
<button [disabled]="available_files.length === 0" mat-stroked-button [matMenuTriggerFor]="menu"><ng-container i18n="Add content">Add content</ng-container></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Playlist order -->
|
||||
<mat-button-toggle-group class="media-list" cdkDropList (cdkDropListDropped)="drop($event)" style="width: 80%; left: 9%" vertical name="videoSelect" aria-label="Video Select" #group="matButtonToggleGroup">
|
||||
<mat-button-toggle class="media-box" cdkDrag *ngFor="let playlist_item of playlist.fileNames; let i = index" [checked]="false"><div><div class="playlist-item-text">{{playlist_item}}</div> <button (click)="removeContent(i)" class="remove-item-button" mat-icon-button><mat-icon>cancel</mat-icon></button></div></mat-button-toggle>
|
||||
<!-- The following for loop can be optimized but it requires a pipe https://stackoverflow.com/a/35703364/8088021 -->
|
||||
<mat-button-toggle class="media-box" cdkDrag *ngFor="let playlist_item of (reverse_order ? playlist.fileNames.slice().reverse() : playlist.fileNames); let i = index" [checked]="false"><div><div class="playlist-item-text">{{playlist_item}}</div> <button (click)="removeContent(i)" class="remove-item-button" mat-icon-button><mat-icon>cancel</mat-icon></button></div></mat-button-toggle>
|
||||
</mat-button-toggle-group>
|
||||
|
||||
<div class="add-content-button">
|
||||
<button [disabled]="available_files.length === 0" mat-stroked-button [matMenuTriggerFor]="menu">Add more content</button>
|
||||
</div>
|
||||
|
||||
<mat-menu #menu="matMenu">
|
||||
<button *ngFor="let file of available_files" (click)="addContent(file)" mat-menu-item>{{file}}</button>
|
||||
</mat-menu>
|
||||
@@ -24,5 +34,5 @@
|
||||
|
||||
<mat-dialog-actions>
|
||||
<!-- Save -->
|
||||
<button [disabled]="!playlistChanged()" (click)="updatePlaylist()" [disabled]="playlistChanged()" mat-raised-button color="accent">Save</button>
|
||||
<button [disabled]="!playlistChanged()" (click)="updatePlaylist()" [disabled]="playlistChanged()" mat-raised-button color="accent"><ng-container i18n="Save">Save</ng-container></button>
|
||||
</mat-dialog-actions>
|
||||
@@ -30,11 +30,6 @@ border: none;
|
||||
transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.add-content-button {
|
||||
margin-top: 15px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.remove-item-button {
|
||||
right: 10px;
|
||||
position: absolute;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
|
||||
import { ModifyPlaylistComponent } from './modify-playlist.component';
|
||||
|
||||
@@ -6,7 +6,7 @@ describe('ModifyPlaylistComponent', () => {
|
||||
let component: ModifyPlaylistComponent;
|
||||
let fixture: ComponentFixture<ModifyPlaylistComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ ModifyPlaylistComponent ]
|
||||
})
|
||||
|
||||
@@ -15,6 +15,7 @@ export class ModifyPlaylistComponent implements OnInit {
|
||||
available_files = [];
|
||||
all_files = [];
|
||||
playlist_updated = false;
|
||||
reverse_order = false;
|
||||
|
||||
constructor(@Inject(MAT_DIALOG_DATA) public data: any,
|
||||
private postsService: PostsService,
|
||||
@@ -26,6 +27,8 @@ export class ModifyPlaylistComponent implements OnInit {
|
||||
this.original_playlist = JSON.parse(JSON.stringify(this.data.playlist));
|
||||
this.getFiles();
|
||||
}
|
||||
|
||||
this.reverse_order = localStorage.getItem('default_playlist_order_reversed') === 'true';
|
||||
}
|
||||
|
||||
getFiles() {
|
||||
@@ -72,11 +75,23 @@ export class ModifyPlaylistComponent implements OnInit {
|
||||
}
|
||||
|
||||
removeContent(index) {
|
||||
if (this.reverse_order) {
|
||||
index = this.playlist.fileNames.length - 1 - index;
|
||||
}
|
||||
this.playlist.fileNames.splice(index, 1);
|
||||
this.processFiles();
|
||||
}
|
||||
|
||||
togglePlaylistOrder() {
|
||||
this.reverse_order = !this.reverse_order;
|
||||
localStorage.setItem('default_playlist_order_reversed', '' + this.reverse_order);
|
||||
}
|
||||
|
||||
drop(event: CdkDragDrop<string[]>) {
|
||||
if (this.reverse_order) {
|
||||
event.previousIndex = this.playlist.fileNames.length - 1 - event.previousIndex;
|
||||
event.currentIndex = this.playlist.fileNames.length - 1 - event.currentIndex;
|
||||
}
|
||||
moveItemInArray(this.playlist.fileNames, event.previousIndex, event.currentIndex);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
|
||||
import { SetDefaultAdminDialogComponent } from './set-default-admin-dialog.component';
|
||||
|
||||
@@ -6,7 +6,7 @@ describe('SetDefaultAdminDialogComponent', () => {
|
||||
let component: SetDefaultAdminDialogComponent;
|
||||
let fixture: ComponentFixture<SetDefaultAdminDialogComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ SetDefaultAdminDialogComponent ]
|
||||
})
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
|
||||
import { ShareMediaDialogComponent } from './share-media-dialog.component';
|
||||
|
||||
@@ -6,7 +6,7 @@ describe('ShareMediaDialogComponent', () => {
|
||||
let component: ShareMediaDialogComponent;
|
||||
let fixture: ComponentFixture<ShareMediaDialogComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ ShareMediaDialogComponent ]
|
||||
})
|
||||
|
||||
@@ -35,6 +35,13 @@
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="col-12 mt-2">
|
||||
<mat-form-field>
|
||||
<mat-select placeholder="Max quality" i18n-placeholder="Max quality placeholder" [disabled]="audioOnlyMode" [(ngModel)]="maxQuality">
|
||||
<mat-option *ngFor="let available_quality of available_qualities" [value]="available_quality['value']">{{available_quality['label']}}</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<div>
|
||||
<mat-checkbox [(ngModel)]="audioOnlyMode"><ng-container i18n="Streaming-only mode">Audio-only mode</ng-container></mat-checkbox>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
|
||||
import { SubscribeDialogComponent } from './subscribe-dialog.component';
|
||||
|
||||
@@ -6,7 +6,7 @@ describe('SubscribeDialogComponent', () => {
|
||||
let component: SubscribeDialogComponent;
|
||||
let fixture: ComponentFixture<SubscribeDialogComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ SubscribeDialogComponent ]
|
||||
})
|
||||
|
||||
@@ -17,6 +17,8 @@ export class SubscribeDialogComponent implements OnInit {
|
||||
url = null;
|
||||
name = null;
|
||||
|
||||
maxQuality = 'best';
|
||||
|
||||
// state
|
||||
subscribing = false;
|
||||
|
||||
@@ -29,12 +31,43 @@ export class SubscribeDialogComponent implements OnInit {
|
||||
customFileOutput = '';
|
||||
customArgs = '';
|
||||
|
||||
available_qualities = [
|
||||
{
|
||||
'label': 'Best',
|
||||
'value': 'best'
|
||||
},
|
||||
{
|
||||
'label': '4K',
|
||||
'value': '2160'
|
||||
},
|
||||
{
|
||||
'label': '1440p',
|
||||
'value': '1440'
|
||||
},
|
||||
{
|
||||
'label': '1080p',
|
||||
'value': '1080'
|
||||
},
|
||||
{
|
||||
'label': '720p',
|
||||
'value': '720'
|
||||
},
|
||||
{
|
||||
'label': '480p',
|
||||
'value': '480'
|
||||
},
|
||||
{
|
||||
'label': '360p',
|
||||
'value': '360'
|
||||
}
|
||||
];
|
||||
|
||||
time_units = [
|
||||
'day',
|
||||
'week',
|
||||
'month',
|
||||
'year'
|
||||
]
|
||||
];
|
||||
|
||||
constructor(private postsService: PostsService,
|
||||
private snackBar: MatSnackBar,
|
||||
@@ -57,7 +90,7 @@ export class SubscribeDialogComponent implements OnInit {
|
||||
if (!this.download_all) {
|
||||
timerange = 'now-' + this.timerange_amount.toString() + this.timerange_unit;
|
||||
}
|
||||
this.postsService.createSubscription(this.url, this.name, timerange, this.streamingOnlyMode,
|
||||
this.postsService.createSubscription(this.url, this.name, timerange, this.streamingOnlyMode, this.maxQuality,
|
||||
this.audioOnlyMode, this.customArgs, this.customFileOutput).subscribe(res => {
|
||||
this.subscribing = false;
|
||||
if (res['new_sub']) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<h4 mat-dialog-title>{{sub.name}}</h4>
|
||||
<h4 mat-dialog-title>{{sub.name}} <ng-container *ngIf="sub.paused" i18n="Paused suffix">(Paused)</ng-container></h4>
|
||||
|
||||
<mat-dialog-content>
|
||||
<div class="info-item">
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
|
||||
import { SubscriptionInfoDialogComponent } from './subscription-info-dialog.component';
|
||||
|
||||
@@ -6,7 +6,7 @@ describe('SubscriptionInfoDialogComponent', () => {
|
||||
let component: SubscriptionInfoDialogComponent;
|
||||
let fixture: ComponentFixture<SubscriptionInfoDialogComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ SubscriptionInfoDialogComponent ]
|
||||
})
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
|
||||
import { UpdateProgressDialogComponent } from './update-progress-dialog.component';
|
||||
|
||||
@@ -6,7 +6,7 @@ describe('UpdateProgressDialogComponent', () => {
|
||||
let component: UpdateProgressDialogComponent;
|
||||
let fixture: ComponentFixture<UpdateProgressDialogComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ UpdateProgressDialogComponent ]
|
||||
})
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
|
||||
import { UserProfileDialogComponent } from './user-profile-dialog.component';
|
||||
|
||||
@@ -6,7 +6,7 @@ describe('UserProfileDialogComponent', () => {
|
||||
let component: UserProfileDialogComponent;
|
||||
let fixture: ComponentFixture<UserProfileDialogComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ UserProfileDialogComponent ]
|
||||
})
|
||||
|
||||
@@ -25,6 +25,10 @@
|
||||
<div class="info-item-label"><strong><ng-container i18n="Video upload date property">Upload Date:</ng-container> </strong></div>
|
||||
<div class="info-item-value">{{file.upload_date ? file.upload_date : 'N/A'}}</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="info-item-label"><strong><ng-container i18n="Category property">Category:</ng-container> </strong></div>
|
||||
<div class="info-item-value"><ng-container *ngIf="file.category"><mat-chip-list><mat-chip>{{file.category.name}}</mat-chip></mat-chip-list></ng-container><ng-container *ngIf="!file.category">N/A</ng-container></div>
|
||||
</div>
|
||||
</mat-dialog-content>
|
||||
|
||||
<mat-dialog-actions>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
|
||||
import { VideoInfoDialogComponent } from './video-info-dialog.component';
|
||||
|
||||
@@ -6,7 +6,7 @@ describe('VideoInfoDialogComponent', () => {
|
||||
let component: VideoInfoDialogComponent;
|
||||
let fixture: ComponentFixture<VideoInfoDialogComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ VideoInfoDialogComponent ]
|
||||
})
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
|
||||
import { DownloadItemComponent } from './download-item.component';
|
||||
|
||||
@@ -6,7 +6,7 @@ describe('DownloadItemComponent', () => {
|
||||
let component: DownloadItemComponent;
|
||||
let fixture: ComponentFixture<DownloadItemComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ DownloadItemComponent ]
|
||||
})
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
|
||||
import { FileCardComponent } from './file-card.component';
|
||||
|
||||
@@ -6,7 +6,7 @@ describe('FileCardComponent', () => {
|
||||
let component: FileCardComponent;
|
||||
let fixture: ComponentFixture<FileCardComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ FileCardComponent ]
|
||||
})
|
||||
|
||||
@@ -56,7 +56,7 @@ export class FileCardComponent implements OnInit {
|
||||
|
||||
deleteFile(blacklistMode = false) {
|
||||
if (!this.playlist) {
|
||||
this.postsService.deleteFile(this.uid, this.isAudio, blacklistMode).subscribe(result => {
|
||||
this.postsService.deleteFile(this.uid, this.isAudio ? 'audio' : 'video', blacklistMode).subscribe(result => {
|
||||
if (result) {
|
||||
this.openSnackBar('Delete success!', 'OK.');
|
||||
this.removeFile.emit(this.name);
|
||||
|
||||
34
src/app/http.interceptor.ts
Normal file
34
src/app/http.interceptor.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { Router } from '@angular/router';
|
||||
import { Observable, throwError } from 'rxjs';
|
||||
import { catchError } from 'rxjs/operators';
|
||||
|
||||
@Injectable()
|
||||
export class H401Interceptor implements HttpInterceptor {
|
||||
|
||||
constructor(private router: Router, private snackBar: MatSnackBar) { }
|
||||
|
||||
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
|
||||
return next.handle(request).pipe(catchError(err => {
|
||||
if (err.status === 401) {
|
||||
localStorage.setItem('jwt_token', null);
|
||||
if (this.router.url !== '/login') {
|
||||
this.router.navigate(['/login']).then(() => {
|
||||
this.openSnackBar('Login expired, please login again.');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const error = err.error.message || err.statusText;
|
||||
return throwError(error);
|
||||
}));
|
||||
}
|
||||
|
||||
public openSnackBar(message: string, action: string = '') {
|
||||
this.snackBar.open(message, action, {
|
||||
duration: 2000,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
|
||||
import { InputDialogComponent } from './input-dialog.component';
|
||||
|
||||
@@ -6,7 +6,7 @@ describe('InputDialogComponent', () => {
|
||||
let component: InputDialogComponent;
|
||||
let fixture: ComponentFixture<InputDialogComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ InputDialogComponent ]
|
||||
})
|
||||
|
||||
@@ -182,97 +182,8 @@
|
||||
</ng-template>
|
||||
|
||||
<ng-container *ngIf="cachedFileManagerEnabled || fileManagerEnabled">
|
||||
<app-recent-videos></app-recent-videos>
|
||||
<app-recent-videos #recentVideos></app-recent-videos>
|
||||
<br/>
|
||||
<h4 style="text-align: center">Custom playlists</h4>
|
||||
<app-custom-playlists></app-custom-playlists>
|
||||
</ng-container>
|
||||
|
||||
<!--<div style="margin: 20px" *ngIf="fileManagerEnabled && (!postsService.isLoggedIn || postsService.permissions.includes('filemanager'))">
|
||||
<mat-accordion>
|
||||
<mat-expansion-panel (opened)="accordionOpened('audio')" (closed)="accordionClosed('audio')" (mouseleave)="accordionLeft('audio')" (mouseenter)="accordionEntered('audio')" class="big">
|
||||
<mat-expansion-panel-header>
|
||||
<mat-panel-title>
|
||||
<ng-container i18n="Audio files title">
|
||||
Audio
|
||||
</ng-container>
|
||||
</mat-panel-title>
|
||||
<mat-panel-description>
|
||||
<ng-container i18n="Audio files description">
|
||||
Your audio files are here
|
||||
</ng-container>
|
||||
</mat-panel-description>
|
||||
</mat-expansion-panel-header>
|
||||
<div *ngIf="mp3s.length > 0;else nomp3s">
|
||||
<mat-grid-list style="margin-bottom: 15px;" (window:resize)="onResize($event)" [cols]="files_cols" rowHeight="150px">
|
||||
<mat-grid-tile *ngFor="let file of mp3s; index as i;">
|
||||
<app-file-card #audiofilecard (removeFile)="removeFromMp3($event)" [file]="file" [title]="file.title" [name]="file.id" [uid]="file.uid" [thumbnailURL]="file.thumbnailURL"
|
||||
[length]="file.duration" [isAudio]="true" [use_youtubedl_archive]="use_youtubedl_archive"></app-file-card>
|
||||
<mat-progress-bar *ngIf="downloading_content['audio'][file.id]" class="download-progress-bar" mode="indeterminate"></mat-progress-bar>
|
||||
</mat-grid-tile>
|
||||
</mat-grid-list>
|
||||
<mat-divider></mat-divider>
|
||||
<div style="width: 100%; text-align: center; margin-top: 10px;">
|
||||
<h6 i18n="Playlists title">Playlists</h6>
|
||||
</div>
|
||||
<mat-grid-list *ngIf="playlists.audio.length > 0" (window:resize)="onResize($event)" [cols]="files_cols" rowHeight="150px">
|
||||
<mat-grid-tile *ngFor="let playlist of playlists.audio; let i = index;">
|
||||
<app-file-card #audiofilecard (removeFile)="removePlaylistMp3(playlist.id, i)" [title]="playlist.name" [name]="playlist.id" [thumbnailURL]="playlist_thumbnails[playlist.id]"
|
||||
[length]="null" [isAudio]="true" [playlist]="playlist" [count]="playlist.fileNames.length" [use_youtubedl_archive]="use_youtubedl_archive"></app-file-card>
|
||||
<mat-progress-bar *ngIf="downloading_content['audio'][playlist.id]" class="download-progress-bar" mode="indeterminate"></mat-progress-bar>
|
||||
</mat-grid-tile>
|
||||
</mat-grid-list>
|
||||
<div class="add-playlist-button"><button (click)="openCreatePlaylistDialog('audio')" mat-fab><mat-icon>add</mat-icon></button></div>
|
||||
<div *ngIf="playlists.audio.length === 0">
|
||||
<ng-container i18n="No video playlists available text">
|
||||
No playlists available. Create one from your downloading audio files by clicking the blue plus button.
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</mat-expansion-panel>
|
||||
<mat-expansion-panel (opened)="accordionOpened('video')" (closed)="accordionClosed('video')" (mouseleave)="accordionLeft('video')" (mouseenter)="accordionEntered('video')" class="big">
|
||||
<mat-expansion-panel-header>
|
||||
<mat-panel-title>
|
||||
<ng-container i18n="Video files title">
|
||||
Video
|
||||
</ng-container>
|
||||
</mat-panel-title>
|
||||
<mat-panel-description>
|
||||
<ng-container i18n="Video files description">
|
||||
Your video files are here
|
||||
</ng-container>
|
||||
</mat-panel-description>
|
||||
</mat-expansion-panel-header>
|
||||
<div *ngIf="mp4s.length > 0;else nomp4s">
|
||||
<mat-grid-list style="margin-bottom: 15px;" (window:resize)="onResize($event)" [cols]="files_cols" rowHeight="150px">
|
||||
<mat-grid-tile *ngFor="let file of mp4s; index as i;">
|
||||
<app-file-card #videofilecard (removeFile)="removeFromMp4($event)" [file]="file" [title]="file.title" [name]="file.id" [uid]="file.uid" [thumbnailURL]="file.thumbnailURL"
|
||||
[length]="file.duration" [isAudio]="false" [use_youtubedl_archive]="use_youtubedl_archive"></app-file-card>
|
||||
<mat-progress-bar *ngIf="downloading_content['video'][file.id]" class="download-progress-bar" mode="indeterminate"></mat-progress-bar>
|
||||
</mat-grid-tile>
|
||||
</mat-grid-list>
|
||||
<mat-divider></mat-divider>
|
||||
|
||||
<div style="width: 100%; text-align: center; margin-top: 10px;">
|
||||
<h6 i18n="Playlists title">Playlists</h6>
|
||||
</div>
|
||||
<mat-grid-list *ngIf="playlists.video.length > 0" (window:resize)="onResize($event)" [cols]="files_cols" rowHeight="150px">
|
||||
<mat-grid-tile *ngFor="let playlist of playlists.video; let i = index;">
|
||||
<app-file-card #videofilecard (removeFile)="removePlaylistMp4(playlist.id, i)" [title]="playlist.name" [name]="playlist.id" [thumbnailURL]="playlist_thumbnails[playlist.id]"
|
||||
[length]="null" [isAudio]="false" [playlist]="playlist" [count]="playlist.fileNames.length" [use_youtubedl_archive]="use_youtubedl_archive"></app-file-card>
|
||||
<mat-progress-bar *ngIf="downloading_content['video'][playlist.id]" class="download-progress-bar" mode="indeterminate"></mat-progress-bar>
|
||||
</mat-grid-tile>
|
||||
</mat-grid-list>
|
||||
|
||||
<!-- Add video playlist button --<
|
||||
<div class="add-playlist-button"><button (click)="openCreatePlaylistDialog('video')" mat-fab><mat-icon>add</mat-icon></button></div>
|
||||
<div *ngIf="playlists.video.length === 0">
|
||||
<ng-container i18n="No video playlists available text">
|
||||
No playlists available. Create one from your downloading video files by clicking the blue plus button.
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
</mat-expansion-panel>
|
||||
</mat-accordion>
|
||||
</div>-->
|
||||
@@ -1,4 +1,4 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
|
||||
import { MainComponent } from './main.component';
|
||||
|
||||
@@ -6,7 +6,7 @@ describe('MainComponent', () => {
|
||||
let component: MainComponent;
|
||||
let fixture: ComponentFixture<MainComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ MainComponent ]
|
||||
})
|
||||
|
||||
@@ -1,25 +1,17 @@
|
||||
import { Component, OnInit, ElementRef, ViewChild, ViewChildren, QueryList } from '@angular/core';
|
||||
import {PostsService} from '../posts.services';
|
||||
import {FileCardComponent} from '../file-card/file-card.component';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import { Observable } from 'rxjs';
|
||||
import {FormControl, Validators} from '@angular/forms';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { saveAs } from 'file-saver';
|
||||
import 'rxjs/add/observable/of';
|
||||
import 'rxjs/add/operator/mapTo';
|
||||
import 'rxjs/add/operator/toPromise';
|
||||
import 'rxjs/add/observable/fromEvent'
|
||||
import 'rxjs/add/operator/filter'
|
||||
import 'rxjs/add/operator/debounceTime'
|
||||
import 'rxjs/add/operator/do'
|
||||
import 'rxjs/add/operator/switch'
|
||||
import { YoutubeSearchService, Result } from '../youtube-search.service';
|
||||
import { Router, ActivatedRoute } from '@angular/router';
|
||||
import { CreatePlaylistComponent } from 'app/create-playlist/create-playlist.component';
|
||||
import { Platform } from '@angular/cdk/platform';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { ArgModifierDialogComponent } from 'app/dialogs/arg-modifier-dialog/arg-modifier-dialog.component';
|
||||
import { RecentVideosComponent } from 'app/components/recent-videos/recent-videos.component';
|
||||
|
||||
export let audioFilesMouseHovering = false;
|
||||
export let videoFilesMouseHovering = false;
|
||||
@@ -200,6 +192,7 @@ export class MainComponent implements OnInit {
|
||||
formats_loading = false;
|
||||
|
||||
@ViewChild('urlinput', { read: ElementRef }) urlInput: ElementRef;
|
||||
@ViewChild('recentVideos') recentVideos: RecentVideosComponent;
|
||||
@ViewChildren('audiofilecard') audioFileCards: QueryList<FileCardComponent>;
|
||||
@ViewChildren('videofilecard') videoFileCards: QueryList<FileCardComponent>;
|
||||
last_valid_url = '';
|
||||
@@ -250,13 +243,6 @@ export class MainComponent implements OnInit {
|
||||
this.useDefaultDownloadingAgent = this.postsService.config['Advanced']['use_default_downloading_agent'];
|
||||
this.customDownloadingAgent = this.postsService.config['Advanced']['custom_downloading_agent'];
|
||||
|
||||
|
||||
|
||||
if (this.fileManagerEnabled) {
|
||||
this.getMp3s();
|
||||
this.getMp4s();
|
||||
}
|
||||
|
||||
if (this.youtubeSearchEnabled && this.youtubeAPIKey) {
|
||||
this.youtubeSearch.initializeAPI(this.youtubeAPIKey);
|
||||
this.attachToInput();
|
||||
@@ -341,61 +327,6 @@ export class MainComponent implements OnInit {
|
||||
this.setCols();
|
||||
}
|
||||
|
||||
// file manager stuff
|
||||
|
||||
getMp3s() {
|
||||
this.postsService.getMp3s().subscribe(result => {
|
||||
const mp3s = result['mp3s'];
|
||||
const playlists = result['playlists'];
|
||||
// if they are different
|
||||
if (JSON.stringify(this.mp3s) !== JSON.stringify(mp3s)) { this.mp3s = mp3s };
|
||||
this.playlists.audio = playlists;
|
||||
|
||||
// get thumbnail url by using first video. this is a temporary hack
|
||||
for (let i = 0; i < this.playlists.audio.length; i++) {
|
||||
const playlist = this.playlists.audio[i];
|
||||
let videoToExtractThumbnail = null;
|
||||
for (let j = 0; j < this.mp3s.length; j++) {
|
||||
if (this.mp3s[j].id === playlist.fileNames[0]) {
|
||||
// found the corresponding file
|
||||
videoToExtractThumbnail = this.mp3s[j];
|
||||
}
|
||||
}
|
||||
|
||||
if (videoToExtractThumbnail) { this.playlist_thumbnails[playlist.id] = videoToExtractThumbnail.thumbnailURL; }
|
||||
}
|
||||
}, error => {
|
||||
console.log(error);
|
||||
});
|
||||
}
|
||||
|
||||
getMp4s() {
|
||||
this.postsService.getMp4s().subscribe(result => {
|
||||
const mp4s = result['mp4s'];
|
||||
const playlists = result['playlists'];
|
||||
// if they are different
|
||||
if (JSON.stringify(this.mp4s) !== JSON.stringify(mp4s)) { this.mp4s = mp4s };
|
||||
this.playlists.video = playlists;
|
||||
|
||||
// get thumbnail url by using first video. this is a temporary hack
|
||||
for (let i = 0; i < this.playlists.video.length; i++) {
|
||||
const playlist = this.playlists.video[i];
|
||||
let videoToExtractThumbnail = null;
|
||||
for (let j = 0; j < this.mp4s.length; j++) {
|
||||
if (this.mp4s[j].id === playlist.fileNames[0]) {
|
||||
// found the corresponding file
|
||||
videoToExtractThumbnail = this.mp4s[j];
|
||||
}
|
||||
}
|
||||
|
||||
if (videoToExtractThumbnail) { this.playlist_thumbnails[playlist.id] = videoToExtractThumbnail.thumbnailURL; }
|
||||
}
|
||||
},
|
||||
error => {
|
||||
console.log(error);
|
||||
});
|
||||
}
|
||||
|
||||
public setCols() {
|
||||
if (window.innerWidth <= 350) {
|
||||
this.files_cols = 1;
|
||||
@@ -443,50 +374,13 @@ export class MainComponent implements OnInit {
|
||||
return null;
|
||||
}
|
||||
|
||||
public removeFromMp3(name: string) {
|
||||
for (let i = 0; i < this.mp3s.length; i++) {
|
||||
if (this.mp3s[i].id === name || this.mp3s[i].id + '.mp3' === name) {
|
||||
this.mp3s.splice(i, 1);
|
||||
}
|
||||
}
|
||||
this.getMp3s();
|
||||
}
|
||||
|
||||
public removePlaylistMp3(playlistID, index) {
|
||||
this.postsService.removePlaylist(playlistID, 'audio').subscribe(res => {
|
||||
if (res['success']) {
|
||||
this.playlists.audio.splice(index, 1);
|
||||
this.openSnackBar('Playlist successfully removed.', '');
|
||||
}
|
||||
this.getMp3s();
|
||||
});
|
||||
}
|
||||
|
||||
public removeFromMp4(name: string) {
|
||||
for (let i = 0; i < this.mp4s.length; i++) {
|
||||
if (this.mp4s[i].id === name || this.mp4s[i].id + '.mp4' === name) {
|
||||
this.mp4s.splice(i, 1);
|
||||
}
|
||||
}
|
||||
this.getMp4s();
|
||||
}
|
||||
|
||||
public removePlaylistMp4(playlistID, index) {
|
||||
this.postsService.removePlaylist(playlistID, 'video').subscribe(res => {
|
||||
if (res['success']) {
|
||||
this.playlists.video.splice(index, 1);
|
||||
this.openSnackBar('Playlist successfully removed.', '');
|
||||
}
|
||||
this.getMp4s();
|
||||
});
|
||||
}
|
||||
|
||||
// download helpers
|
||||
|
||||
downloadHelperMp3(name, uid, is_playlist = false, forceView = false, new_download = null, navigate_mode = false) {
|
||||
this.downloadingfile = false;
|
||||
if (this.multiDownloadMode && !this.downloadOnlyMode && !navigate_mode) {
|
||||
// do nothing
|
||||
this.reloadRecentVideos();
|
||||
} else {
|
||||
// if download only mode, just download the file. no redirect
|
||||
if (forceView === false && this.downloadOnlyMode && !this.iOS) {
|
||||
@@ -496,6 +390,7 @@ export class MainComponent implements OnInit {
|
||||
} else {
|
||||
this.downloadAudioFile(decodeURI(name));
|
||||
}
|
||||
this.reloadRecentVideos();
|
||||
} else {
|
||||
localStorage.setItem('player_navigator', this.router.url.split(';')[0]);
|
||||
if (is_playlist) {
|
||||
@@ -508,22 +403,13 @@ export class MainComponent implements OnInit {
|
||||
|
||||
// remove download from current downloads
|
||||
this.removeDownloadFromCurrentDownloads(new_download);
|
||||
|
||||
// reloads mp3s
|
||||
if (this.fileManagerEnabled) {
|
||||
this.getMp3s();
|
||||
setTimeout(() => {
|
||||
this.audioFileCards.forEach(filecard => {
|
||||
filecard.onHoverResponse();
|
||||
});
|
||||
}, 200);
|
||||
}
|
||||
}
|
||||
|
||||
downloadHelperMp4(name, uid, is_playlist = false, forceView = false, new_download = null, navigate_mode = false) {
|
||||
this.downloadingfile = false;
|
||||
if (this.multiDownloadMode && !this.downloadOnlyMode && !navigate_mode) {
|
||||
// do nothing
|
||||
this.reloadRecentVideos();
|
||||
} else {
|
||||
// if download only mode, just download the file. no redirect
|
||||
if (forceView === false && this.downloadOnlyMode) {
|
||||
@@ -533,6 +419,7 @@ export class MainComponent implements OnInit {
|
||||
} else {
|
||||
this.downloadVideoFile(decodeURI(name));
|
||||
}
|
||||
this.reloadRecentVideos();
|
||||
} else {
|
||||
localStorage.setItem('player_navigator', this.router.url.split(';')[0]);
|
||||
if (is_playlist) {
|
||||
@@ -545,16 +432,6 @@ export class MainComponent implements OnInit {
|
||||
|
||||
// remove download from current downloads
|
||||
this.removeDownloadFromCurrentDownloads(new_download);
|
||||
|
||||
// reloads mp4s
|
||||
if (this.fileManagerEnabled) {
|
||||
this.getMp4s();
|
||||
setTimeout(() => {
|
||||
this.videoFileCards.forEach(filecard => {
|
||||
filecard.onHoverResponse();
|
||||
});
|
||||
}, 200);
|
||||
}
|
||||
}
|
||||
|
||||
// download click handler
|
||||
@@ -746,9 +623,7 @@ export class MainComponent implements OnInit {
|
||||
|
||||
if (!this.fileManagerEnabled) {
|
||||
// tell server to delete the file once downloaded
|
||||
this.postsService.deleteFile(name, true).subscribe(delRes => {
|
||||
// reload mp3s
|
||||
this.getMp3s();
|
||||
this.postsService.deleteFile(name, 'video').subscribe(delRes => {
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -763,9 +638,7 @@ export class MainComponent implements OnInit {
|
||||
|
||||
if (!this.fileManagerEnabled) {
|
||||
// tell server to delete the file once downloaded
|
||||
this.postsService.deleteFile(name, false).subscribe(delRes => {
|
||||
// reload mp4s
|
||||
this.getMp4s();
|
||||
this.postsService.deleteFile(name, 'audio').subscribe(delRes => {
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -1112,25 +985,6 @@ export class MainComponent implements OnInit {
|
||||
}
|
||||
}
|
||||
|
||||
// creating a playlist
|
||||
openCreatePlaylistDialog(type) {
|
||||
const dialogRef = this.dialog.open(CreatePlaylistComponent, {
|
||||
data: {
|
||||
filesToSelectFrom: (type === 'audio') ? this.mp3s : this.mp4s,
|
||||
type: type
|
||||
}
|
||||
});
|
||||
dialogRef.afterClosed().subscribe(result => {
|
||||
if (result) {
|
||||
if (type === 'audio') { this.getMp3s() };
|
||||
if (type === 'video') { this.getMp4s() };
|
||||
this.openSnackBar('Successfully created playlist!', '');
|
||||
} else if (result === false) {
|
||||
this.openSnackBar('ERROR: failed to create playlist!', '');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// modify custom args
|
||||
openArgsModifierDialog() {
|
||||
const dialogRef = this.dialog.open(ArgModifierDialogComponent, {
|
||||
@@ -1161,4 +1015,10 @@ export class MainComponent implements OnInit {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
reloadRecentVideos() {
|
||||
if (this.recentVideos) {
|
||||
this.recentVideos.getAllFiles();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user