mirror of
https://github.com/Tzahi12345/YoutubeDL-Material.git
synced 2026-03-08 04:20:08 +03:00
Compare commits
157 Commits
remove-arm
...
angular-17
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e2baa30d9a | ||
|
|
a74bca05cc | ||
|
|
7d3458ea41 | ||
|
|
6a7c1c9d0b | ||
|
|
e2e3dd280a | ||
|
|
25bf7a6fdd | ||
|
|
c56987ddd5 | ||
|
|
72399b09e4 | ||
|
|
4258b82040 | ||
|
|
7ac6a50b41 | ||
|
|
fb92975b73 | ||
|
|
b4cf1e39b9 | ||
|
|
026f24a327 | ||
|
|
1bf348f481 | ||
|
|
eb8cd3fd06 | ||
|
|
f96ffab530 | ||
|
|
dcb53691e3 | ||
|
|
2cf21541bb | ||
|
|
13e46397e9 | ||
|
|
7f079c56d0 | ||
|
|
e082919cd0 | ||
|
|
a89378b99f | ||
|
|
4dc899439e | ||
|
|
9b38c56528 | ||
|
|
0644b194d0 | ||
|
|
344d959c05 | ||
|
|
3912655912 | ||
|
|
cdf82abf3f | ||
|
|
84464db0e0 | ||
|
|
4bf03bfd1a | ||
|
|
75cbe4d5d0 | ||
|
|
9556f9c94f | ||
|
|
4a97fa4ef5 | ||
|
|
2c155b74a9 | ||
|
|
25e4c114e8 | ||
|
|
6152df3486 | ||
|
|
7cf5d86fc3 | ||
|
|
f57e0ab187 | ||
|
|
517c9e169d | ||
|
|
69d8751484 | ||
|
|
c3c8f50a92 | ||
|
|
caadf4f9d2 | ||
|
|
d10401cead | ||
|
|
d02d100001 | ||
|
|
6b59446a37 | ||
|
|
4fd25e1e49 | ||
|
|
d30c338189 | ||
|
|
509e996107 | ||
|
|
240e87b453 | ||
|
|
eaefcc5b96 | ||
|
|
85577ac528 | ||
|
|
41050ce923 | ||
|
|
55bc5339f5 | ||
|
|
0e33b2db2b | ||
|
|
1456c25978 | ||
|
|
67c38039b0 | ||
|
|
8f246d905f | ||
|
|
91c2fdc701 | ||
|
|
2c97403027 | ||
|
|
3151200d33 | ||
|
|
c5ed835b09 | ||
|
|
8a588cf858 | ||
|
|
2396c86486 | ||
|
|
2cc2428db2 | ||
|
|
80e83ba817 | ||
|
|
0565cf24a6 | ||
|
|
353c35cd8d | ||
|
|
169a057c37 | ||
|
|
ab6d0f199e | ||
|
|
ae48a4c195 | ||
|
|
241473b99d | ||
|
|
ba98548662 | ||
|
|
72419d7be9 | ||
|
|
50079d2ab7 | ||
|
|
ee21f79fff | ||
|
|
097a3509c1 | ||
|
|
cc0fa03aca | ||
|
|
477cba93cd | ||
|
|
eda3dfcac7 | ||
|
|
188876e383 | ||
|
|
2c70e1367d | ||
|
|
7012524c61 | ||
|
|
cc6dfbf928 | ||
|
|
6ebda81225 | ||
|
|
a50476ac58 | ||
|
|
99c5cf590e | ||
|
|
8ec787c3e3 | ||
|
|
69b5fb50ce | ||
|
|
682c3c98d9 | ||
|
|
5fe2110711 | ||
|
|
3d24b1dc82 | ||
|
|
71086a3bc7 | ||
|
|
9b0cb1a66b | ||
|
|
ace2d83acd | ||
|
|
90f46f0c1c | ||
|
|
609b55754d | ||
|
|
15ca3f27b9 | ||
|
|
3ef8a576b7 | ||
|
|
c807ca2844 | ||
|
|
c823e28a26 | ||
|
|
3170b6aec3 | ||
|
|
57f5d2822a | ||
|
|
9950663326 | ||
|
|
5c8602e1b7 | ||
|
|
d3cb957991 | ||
|
|
098c5a3c25 | ||
|
|
be71a9bd8c | ||
|
|
42c600cea9 | ||
|
|
0427f91cfc | ||
|
|
51552b3092 | ||
|
|
1a7ca0343a | ||
|
|
525e8e04e8 | ||
|
|
5a824cee82 | ||
|
|
13a03a722c | ||
|
|
f7d3835111 | ||
|
|
8212acbac6 | ||
|
|
2a1af69f1f | ||
|
|
1f615a2379 | ||
|
|
f50d3104de | ||
|
|
f23ca61dab | ||
|
|
6eadb37532 | ||
|
|
2c61260e0f | ||
|
|
ba0de7f95c | ||
|
|
3dfdbcb151 | ||
|
|
c207e56855 | ||
|
|
441131e930 | ||
|
|
84c2b2769b | ||
|
|
e145c9c992 | ||
|
|
2adbc0a02c | ||
|
|
fe95f04c18 | ||
|
|
9b3816afce | ||
|
|
07874d9241 | ||
|
|
7447ca038a | ||
|
|
9fa1aab1e5 | ||
|
|
80b41af620 | ||
|
|
ab5d8dc5ca | ||
|
|
4b55c39f39 | ||
|
|
3ca296f195 | ||
|
|
d4fa640f0f | ||
|
|
427eecf214 | ||
|
|
4f54e408a5 | ||
|
|
9e481bbd5f | ||
|
|
78b29a76b8 | ||
|
|
0342d18f76 | ||
|
|
70754c580c | ||
|
|
e58b0b8638 | ||
|
|
df8f8070ca | ||
|
|
0b8ca31594 | ||
|
|
658a76dc1c | ||
|
|
f363ec5db6 | ||
|
|
f36d675abf | ||
|
|
be74377a08 | ||
|
|
26988bd607 | ||
|
|
c5d1b3ffcf | ||
|
|
c64140b605 | ||
|
|
6579f2b59e | ||
|
|
9f5b584593 |
39
.devcontainer/devcontainer.json
Normal file
39
.devcontainer/devcontainer.json
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
// README at: https://github.com/devcontainers/templates/tree/main/src/javascript-node
|
||||||
|
{
|
||||||
|
"name": "Node.js",
|
||||||
|
// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
|
||||||
|
"image": "mcr.microsoft.com/devcontainers/javascript-node:0-18-bullseye",
|
||||||
|
"features": {
|
||||||
|
"ghcr.io/devcontainers-contrib/features/jshint:2": {},
|
||||||
|
"ghcr.io/devcontainers-contrib/features/angular-cli:2": {},
|
||||||
|
"ghcr.io/devcontainers/features/github-cli:1": {}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Use 'forwardPorts' to make a list of ports inside the container available locally.
|
||||||
|
"forwardPorts": [4200, 17442],
|
||||||
|
|
||||||
|
// Use 'postCreateCommand' to run commands after the container is created.
|
||||||
|
"postCreateCommand": "npm install && cd backend && npm install",
|
||||||
|
|
||||||
|
// Configure tool-specific properties.
|
||||||
|
"customizations": {
|
||||||
|
// Configure properties specific to VS Code.
|
||||||
|
"vscode": {
|
||||||
|
// Add the IDs of extensions you want installed when the container is created.
|
||||||
|
"extensions": [
|
||||||
|
"ms-python.python",
|
||||||
|
"Angular.ng-template",
|
||||||
|
"dbaeumer.vscode-eslint",
|
||||||
|
"waderyan.gitblame",
|
||||||
|
"42Crunch.vscode-openapi",
|
||||||
|
"christian-kohler.npm-intellisense",
|
||||||
|
"redhat.vscode-yaml",
|
||||||
|
"hbenl.vscode-mocha-test-adapter",
|
||||||
|
"DavidAnson.vscode-markdownlint"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
|
||||||
|
// "remoteUser": "root"
|
||||||
|
}
|
||||||
8
.github/workflows/build.yml
vendored
8
.github/workflows/build.yml
vendored
@@ -13,11 +13,11 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: checkout code
|
- name: checkout code
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v4
|
||||||
- name: setup node
|
- name: setup node
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: '16'
|
node-version: '18'
|
||||||
cache: 'npm'
|
cache: 'npm'
|
||||||
- name: install dependencies
|
- name: install dependencies
|
||||||
run: |
|
run: |
|
||||||
@@ -65,7 +65,7 @@ jobs:
|
|||||||
if: contains(github.ref, '/tags/v')
|
if: contains(github.ref, '/tags/v')
|
||||||
steps:
|
steps:
|
||||||
- name: checkout code
|
- name: checkout code
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v4
|
||||||
- name: create release
|
- name: create release
|
||||||
id: create_release
|
id: create_release
|
||||||
uses: actions/create-release@v1
|
uses: actions/create-release@v1
|
||||||
@@ -81,7 +81,7 @@ jobs:
|
|||||||
draft: true
|
draft: true
|
||||||
prerelease: false
|
prerelease: false
|
||||||
- name: download build artifact
|
- name: download build artifact
|
||||||
uses: actions/download-artifact@v1
|
uses: actions/download-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: youtubedl-material
|
name: youtubedl-material
|
||||||
path: ${{runner.temp}}/youtubedl-material
|
path: ${{runner.temp}}/youtubedl-material
|
||||||
|
|||||||
8
.github/workflows/codeql-analysis.yml
vendored
8
.github/workflows/codeql-analysis.yml
vendored
@@ -30,7 +30,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
# We must fetch at least the immediate parents so that if this is
|
# We must fetch at least the immediate parents so that if this is
|
||||||
# a pull request then we can checkout the head.
|
# a pull request then we can checkout the head.
|
||||||
@@ -43,7 +43,7 @@ jobs:
|
|||||||
|
|
||||||
# Initializes the CodeQL tools for scanning.
|
# Initializes the CodeQL tools for scanning.
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@v1
|
uses: github/codeql-action/init@v2
|
||||||
with:
|
with:
|
||||||
languages: ${{ matrix.language }}
|
languages: ${{ matrix.language }}
|
||||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||||
@@ -54,7 +54,7 @@ jobs:
|
|||||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
# 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)
|
# If this step fails, then you should remove it and run the build manually (see below)
|
||||||
- name: Autobuild
|
- name: Autobuild
|
||||||
uses: github/codeql-action/autobuild@v1
|
uses: github/codeql-action/autobuild@v2
|
||||||
|
|
||||||
# ℹ️ Command-line programs to run using the OS shell.
|
# ℹ️ Command-line programs to run using the OS shell.
|
||||||
# 📚 https://git.io/JvXDl
|
# 📚 https://git.io/JvXDl
|
||||||
@@ -68,4 +68,4 @@ jobs:
|
|||||||
# make release
|
# make release
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@v1
|
uses: github/codeql-action/analyze@v2
|
||||||
|
|||||||
12
.github/workflows/docker-pr.yml
vendored
12
.github/workflows/docker-pr.yml
vendored
@@ -9,7 +9,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: checkout code
|
- name: checkout code
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v4
|
||||||
- name: Set hash
|
- name: Set hash
|
||||||
id: vars
|
id: vars
|
||||||
run: echo "::set-output name=sha_short::$(git rev-parse --short HEAD)"
|
run: echo "::set-output name=sha_short::$(git rev-parse --short HEAD)"
|
||||||
@@ -24,15 +24,15 @@ jobs:
|
|||||||
json: '{"type": "docker", "tag": "nightly", "commit": "${{ steps.vars.outputs.sha_short }}", "date": "${{ steps.date.outputs.date }}"}'
|
json: '{"type": "docker", "tag": "nightly", "commit": "${{ steps.vars.outputs.sha_short }}", "date": "${{ steps.date.outputs.date }}"}'
|
||||||
dir: 'backend/'
|
dir: 'backend/'
|
||||||
- name: setup platform emulator
|
- name: setup platform emulator
|
||||||
uses: docker/setup-qemu-action@v1
|
uses: docker/setup-qemu-action@v2
|
||||||
- name: setup multi-arch docker build
|
- name: setup multi-arch docker build
|
||||||
uses: docker/setup-buildx-action@v1
|
uses: docker/setup-buildx-action@v2
|
||||||
- name: build & push images
|
- name: build & push images
|
||||||
uses: docker/build-push-action@v2
|
uses: docker/build-push-action@v5
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: ./Dockerfile
|
file: ./Dockerfile
|
||||||
platforms: linux/amd64,linux/arm64/v8
|
platforms: linux/amd64,linux/arm/v7,linux/arm64/v8
|
||||||
#platforms: linux/amd64
|
#platforms: linux/amd64
|
||||||
push: false
|
push: false
|
||||||
tags: tzahi12345/youtubedl-material:nightly-pr
|
tags: tzahi12345/youtubedl-material:nightly-pr
|
||||||
|
|||||||
12
.github/workflows/docker-release.yml
vendored
12
.github/workflows/docker-release.yml
vendored
@@ -15,7 +15,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: checkout code
|
- name: checkout code
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Set hash
|
- name: Set hash
|
||||||
id: vars
|
id: vars
|
||||||
@@ -47,7 +47,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Generate Docker image metadata
|
- name: Generate Docker image metadata
|
||||||
id: docker-meta
|
id: docker-meta
|
||||||
uses: docker/metadata-action@v4
|
uses: docker/metadata-action@v5
|
||||||
with:
|
with:
|
||||||
images: |
|
images: |
|
||||||
${{ secrets.DOCKERHUB_USERNAME }}/${{ secrets.DOCKERHUB_REPO }}
|
${{ secrets.DOCKERHUB_USERNAME }}/${{ secrets.DOCKERHUB_REPO }}
|
||||||
@@ -57,26 +57,26 @@ jobs:
|
|||||||
type=raw,value=latest
|
type=raw,value=latest
|
||||||
|
|
||||||
- name: setup platform emulator
|
- name: setup platform emulator
|
||||||
uses: docker/setup-qemu-action@v1
|
uses: docker/setup-qemu-action@v2
|
||||||
|
|
||||||
- name: setup multi-arch docker build
|
- name: setup multi-arch docker build
|
||||||
uses: docker/setup-buildx-action@v2
|
uses: docker/setup-buildx-action@v2
|
||||||
|
|
||||||
- name: Login to DockerHub
|
- name: Login to DockerHub
|
||||||
uses: docker/login-action@v2
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
uses: docker/login-action@v2
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.repository_owner }}
|
username: ${{ github.repository_owner }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: build & push images
|
- name: build & push images
|
||||||
uses: docker/build-push-action@v2
|
uses: docker/build-push-action@v5
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: ./Dockerfile
|
file: ./Dockerfile
|
||||||
|
|||||||
14
.github/workflows/docker.yml
vendored
14
.github/workflows/docker.yml
vendored
@@ -22,7 +22,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: checkout code
|
- name: checkout code
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Set hash
|
- name: Set hash
|
||||||
id: vars
|
id: vars
|
||||||
@@ -41,14 +41,14 @@ jobs:
|
|||||||
dir: 'backend/'
|
dir: 'backend/'
|
||||||
|
|
||||||
- name: setup platform emulator
|
- name: setup platform emulator
|
||||||
uses: docker/setup-qemu-action@v1
|
uses: docker/setup-qemu-action@v2
|
||||||
|
|
||||||
- name: setup multi-arch docker build
|
- name: setup multi-arch docker build
|
||||||
uses: docker/setup-buildx-action@v2
|
uses: docker/setup-buildx-action@v2
|
||||||
|
|
||||||
- name: Generate Docker image metadata
|
- name: Generate Docker image metadata
|
||||||
id: docker-meta
|
id: docker-meta
|
||||||
uses: docker/metadata-action@v4
|
uses: docker/metadata-action@v5
|
||||||
# Defaults:
|
# Defaults:
|
||||||
# DOCKERHUB_USERNAME : tzahi12345
|
# DOCKERHUB_USERNAME : tzahi12345
|
||||||
# DOCKERHUB_REPO : youtubedl-material
|
# DOCKERHUB_REPO : youtubedl-material
|
||||||
@@ -63,24 +63,24 @@ jobs:
|
|||||||
type=sha,prefix=sha-,format=short
|
type=sha,prefix=sha-,format=short
|
||||||
|
|
||||||
- name: Login to DockerHub
|
- name: Login to DockerHub
|
||||||
uses: docker/login-action@v2
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
uses: docker/login-action@v2
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.repository_owner }}
|
username: ${{ github.repository_owner }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: build & push images
|
- name: build & push images
|
||||||
uses: docker/build-push-action@v2
|
uses: docker/build-push-action@v5
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: ./Dockerfile
|
file: ./Dockerfile
|
||||||
platforms: linux/amd64,linux/arm64/v8
|
platforms: linux/amd64,linux/arm64/v8,linux/arm/v7
|
||||||
push: true
|
push: true
|
||||||
tags: ${{ steps.docker-meta.outputs.tags }}
|
tags: ${{ steps.docker-meta.outputs.tags }}
|
||||||
labels: ${{ steps.docker-meta.outputs.labels }}
|
labels: ${{ steps.docker-meta.outputs.labels }}
|
||||||
|
|||||||
40
.github/workflows/mocha.yml
vendored
Normal file
40
.github/workflows/mocha.yml
vendored
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
name: Tests
|
||||||
|
'on':
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
pull_request:
|
||||||
|
types:
|
||||||
|
- opened
|
||||||
|
- synchronize
|
||||||
|
- reopened
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
name: 'Backend - mocha'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
node:
|
||||||
|
- 18
|
||||||
|
steps:
|
||||||
|
- uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: '${{ matrix.node }}'
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: 'Cache node_modules'
|
||||||
|
uses: actions/cache@v3
|
||||||
|
with:
|
||||||
|
path: ~/.npm
|
||||||
|
key: ${{ runner.os }}-node-v${{ matrix.node }}-${{ hashFiles('**/package-lock.json') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-node-v${{ matrix.node }}-
|
||||||
|
working-directory: ./backend
|
||||||
|
- uses: FedericoCarboni/setup-ffmpeg@v2
|
||||||
|
id: setup-ffmpeg
|
||||||
|
- name: Install Dependencies
|
||||||
|
run: npm install --dev
|
||||||
|
working-directory: ./backend
|
||||||
|
- name: Run All Node.js Tests
|
||||||
|
run: npm run test
|
||||||
|
working-directory: ./backend
|
||||||
2
.vscode/settings.json
vendored
2
.vscode/settings.json
vendored
@@ -3,6 +3,6 @@
|
|||||||
"mochaExplorer.cwd": "backend",
|
"mochaExplorer.cwd": "backend",
|
||||||
"mochaExplorer.globImplementation": "vscode",
|
"mochaExplorer.globImplementation": "vscode",
|
||||||
"mochaExplorer.env": {
|
"mochaExplorer.env": {
|
||||||
"YTDL_MODE": "debug"
|
// "YTDL_MODE": "debug"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
78
Dockerfile
78
Dockerfile
@@ -1,35 +1,46 @@
|
|||||||
# Fetching our ffmpeg
|
# Fetching our utils
|
||||||
FROM ubuntu:22.04 AS ffmpeg
|
FROM ubuntu:23.04 AS utils
|
||||||
ENV DEBIAN_FRONTEND=noninteractive
|
ENV DEBIAN_FRONTEND=noninteractive
|
||||||
# Use script due local build compability
|
# Use script due local build compability
|
||||||
COPY docker-utils/ffmpeg-fetch.sh .
|
COPY docker-utils/*.sh .
|
||||||
RUN chmod +x ffmpeg-fetch.sh
|
RUN chmod +x *.sh
|
||||||
RUN sh ./ffmpeg-fetch.sh
|
RUN sh ./ffmpeg-fetch.sh
|
||||||
|
RUN sh ./fetch-twitchdownloader.sh
|
||||||
|
|
||||||
|
|
||||||
# Create our Ubuntu 22.04 with node 16.14.2 (that specific version is required as per: https://stackoverflow.com/a/72855258/8088021)
|
# Create our Ubuntu 22.04 with node 18.19.0
|
||||||
# Go to 20.04
|
FROM ubuntu:23.04 AS base
|
||||||
FROM ubuntu:20.04 AS base
|
ARG TARGETPLATFORM
|
||||||
ARG DEBIAN_FRONTEND=noninteractive
|
ARG DEBIAN_FRONTEND=noninteractive
|
||||||
ENV UID=1000
|
ENV UID=1001
|
||||||
ENV GID=1000
|
ENV GID=1001
|
||||||
ENV USER=youtube
|
ENV USER=youtube
|
||||||
ENV NO_UPDATE_NOTIFIER=true
|
ENV NO_UPDATE_NOTIFIER=true
|
||||||
ENV PM2_HOME=/app/pm2
|
ENV PM2_HOME=/app/pm2
|
||||||
ENV ALLOW_CONFIG_MUTATIONS=true
|
ENV ALLOW_CONFIG_MUTATIONS=true
|
||||||
|
ENV npm_config_cache=/app/.npm
|
||||||
|
|
||||||
|
# Use NVM to get specific node version
|
||||||
|
ENV NODE_VERSION=18.19.0
|
||||||
RUN groupadd -g $GID $USER && useradd --system -m -g $USER --uid $UID $USER && \
|
RUN groupadd -g $GID $USER && useradd --system -m -g $USER --uid $UID $USER && \
|
||||||
apt update && \
|
apt update && \
|
||||||
apt install -y --no-install-recommends curl ca-certificates tzdata && \
|
apt install -y --no-install-recommends curl ca-certificates tzdata libatomic1 && \
|
||||||
curl -fsSL https://deb.nodesource.com/setup_16.x | bash - && \
|
|
||||||
apt install -y --no-install-recommends nodejs && \
|
|
||||||
npm -g install npm n && \
|
|
||||||
n 16.14.2 && \
|
|
||||||
apt clean && \
|
apt clean && \
|
||||||
rm -rf /var/lib/apt/lists/*
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
RUN mkdir /usr/local/nvm
|
||||||
|
ENV PATH="/usr/local/nvm/versions/node/v${NODE_VERSION}/bin/:${PATH}"
|
||||||
|
ENV NVM_DIR=/usr/local/nvm
|
||||||
|
RUN curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.5/install.sh | bash
|
||||||
|
RUN . "$NVM_DIR/nvm.sh" && nvm install ${NODE_VERSION}
|
||||||
|
RUN . "$NVM_DIR/nvm.sh" && nvm use v${NODE_VERSION}
|
||||||
|
RUN . "$NVM_DIR/nvm.sh" && nvm alias default v${NODE_VERSION}
|
||||||
|
|
||||||
|
RUN npm install -g npm
|
||||||
|
|
||||||
# Build frontend
|
# Build frontend
|
||||||
FROM base as frontend
|
ARG BUILDPLATFORM
|
||||||
|
FROM --platform=${BUILDPLATFORM} node:18 as frontend
|
||||||
RUN npm install -g @angular/cli
|
RUN npm install -g @angular/cli
|
||||||
WORKDIR /build
|
WORKDIR /build
|
||||||
COPY [ "package.json", "package-lock.json", "angular.json", "tsconfig.json", "/build/" ]
|
COPY [ "package.json", "package-lock.json", "angular.json", "tsconfig.json", "/build/" ]
|
||||||
@@ -46,35 +57,40 @@ FROM base as backend
|
|||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY [ "backend/","/app/" ]
|
COPY [ "backend/","/app/" ]
|
||||||
RUN npm config set strict-ssl false && \
|
RUN npm config set strict-ssl false && \
|
||||||
|
npm config set registry https://registry.npm.taobao.org && \
|
||||||
|
npm config set fetch-retry-maxtimeout 60000 && \
|
||||||
npm install --prod && \
|
npm install --prod && \
|
||||||
ls -al
|
ls -al
|
||||||
|
|
||||||
FROM base as python
|
#FROM base as python
|
||||||
WORKDIR /app
|
# armv7 need build from source
|
||||||
COPY docker-utils/GetTwitchDownloader.py .
|
#WORKDIR /app
|
||||||
RUN apt update && \
|
#COPY docker-utils/GetTwitchDownloader.py .
|
||||||
apt install -y --no-install-recommends python3-minimal python-is-python3 python3-pip && \
|
#RUN apt update && \
|
||||||
apt clean && \
|
# apt install -y --no-install-recommends python3-minimal python-is-python3 python3-pip python3-dev build-essential libffi-dev && \
|
||||||
rm -rf /var/lib/apt/lists/*
|
# apt clean && \
|
||||||
RUN pip install PyGithub requests
|
# rm -rf /var/lib/apt/lists/*
|
||||||
RUN python GetTwitchDownloader.py
|
#RUN pip install PyGithub requests
|
||||||
|
#RUN python GetTwitchDownloader.py
|
||||||
|
|
||||||
# Final image
|
# Final image
|
||||||
FROM base
|
FROM base
|
||||||
RUN npm install -g pm2 && \
|
RUN apt update && \
|
||||||
apt update && \
|
curl -sL https://raw.githubusercontent.com/Unitech/pm2/master/packager/setup.deb.sh | bash && \
|
||||||
apt install -y --no-install-recommends gosu python3-minimal python-is-python3 python3-pip atomicparsley build-essential && \
|
apt install -y --no-install-recommends gosu python3-minimal python-is-python3 python3-pip atomicparsley build-essential && \
|
||||||
|
pip install pycryptodomex --break-system-packages && \
|
||||||
|
apt remove -y --purge build-essential && \
|
||||||
|
apt autoremove -y --purge && \
|
||||||
apt clean && \
|
apt clean && \
|
||||||
rm -rf /var/lib/apt/lists/*
|
rm -rf /var/lib/apt/lists/*
|
||||||
RUN pip install pycryptodomex
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
# User 1000 already exist from base image
|
# User 1000 already exist from base image
|
||||||
COPY --chown=$UID:$GID --from=ffmpeg [ "/usr/local/bin/ffmpeg", "/usr/local/bin/ffmpeg" ]
|
COPY --chown=$UID:$GID --from=utils [ "/usr/local/bin/ffmpeg", "/usr/local/bin/ffmpeg" ]
|
||||||
COPY --chown=$UID:$GID --from=ffmpeg [ "/usr/local/bin/ffprobe", "/usr/local/bin/ffprobe" ]
|
COPY --chown=$UID:$GID --from=utils [ "/usr/local/bin/ffprobe", "/usr/local/bin/ffprobe" ]
|
||||||
|
COPY --chown=$UID:$GID --from=utils [ "/usr/local/bin/TwitchDownloaderCLI", "/usr/local/bin/TwitchDownloaderCLI"]
|
||||||
COPY --chown=$UID:$GID --from=backend ["/app/","/app/"]
|
COPY --chown=$UID:$GID --from=backend ["/app/","/app/"]
|
||||||
COPY --chown=$UID:$GID --from=frontend [ "/build/backend/public/", "/app/public/" ]
|
COPY --chown=$UID:$GID --from=frontend [ "/build/backend/public/", "/app/public/" ]
|
||||||
COPY --chown=$UID:$GID --from=python ["/app/TwitchDownloaderCLI","/usr/local/bin/TwitchDownloaderCLI"]
|
#COPY --chown=$UID:$GID --from=python ["/app/TwitchDownloaderCLI","/usr/local/bin/TwitchDownloaderCLI"]
|
||||||
RUN chown $UID:$GID .
|
|
||||||
RUN chmod +x /app/fix-scripts/*.sh
|
RUN chmod +x /app/fix-scripts/*.sh
|
||||||
# Add some persistence data
|
# Add some persistence data
|
||||||
#VOLUME ["/app/appdata"]
|
#VOLUME ["/app/appdata"]
|
||||||
|
|||||||
@@ -293,6 +293,48 @@ paths:
|
|||||||
$ref: '#/components/schemas/UnsubscribeResponse'
|
$ref: '#/components/schemas/UnsubscribeResponse'
|
||||||
security:
|
security:
|
||||||
- Auth query parameter: []
|
- Auth query parameter: []
|
||||||
|
/api/checkSubscription:
|
||||||
|
post:
|
||||||
|
tags:
|
||||||
|
- subscriptions
|
||||||
|
summary: Run a check for videos for a subscription
|
||||||
|
description: Runs a subscription check
|
||||||
|
operationId: post-api-checksubscription
|
||||||
|
requestBody:
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/CheckSubscriptionRequest'
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: OK
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/SuccessObject'
|
||||||
|
security:
|
||||||
|
- Auth query parameter: []
|
||||||
|
/api/cancelCheckSubscription:
|
||||||
|
post:
|
||||||
|
tags:
|
||||||
|
- subscriptions
|
||||||
|
summary: Cancels check for videos for a subscription
|
||||||
|
description: Cancels subscription check
|
||||||
|
operationId: post-api-checksubscription
|
||||||
|
requestBody:
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/CheckSubscriptionRequest'
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: OK
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/SuccessObject'
|
||||||
|
security:
|
||||||
|
- Auth query parameter: []
|
||||||
/api/deleteSubscriptionFile:
|
/api/deleteSubscriptionFile:
|
||||||
post:
|
post:
|
||||||
tags:
|
tags:
|
||||||
@@ -1758,14 +1800,14 @@ components:
|
|||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
task_key:
|
task_key:
|
||||||
type: string
|
$ref: '#/components/schemas/TaskType'
|
||||||
required:
|
required:
|
||||||
- task_key
|
- task_key
|
||||||
UpdateTaskScheduleRequest:
|
UpdateTaskScheduleRequest:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
task_key:
|
task_key:
|
||||||
type: string
|
$ref: '#/components/schemas/TaskType'
|
||||||
new_schedule:
|
new_schedule:
|
||||||
$ref: '#/components/schemas/Schedule'
|
$ref: '#/components/schemas/Schedule'
|
||||||
required:
|
required:
|
||||||
@@ -1775,7 +1817,7 @@ components:
|
|||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
task_key:
|
task_key:
|
||||||
type: string
|
$ref: '#/components/schemas/TaskType'
|
||||||
new_data:
|
new_data:
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
@@ -1785,7 +1827,7 @@ components:
|
|||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
task_key:
|
task_key:
|
||||||
type: string
|
$ref: '#/components/schemas/TaskType'
|
||||||
new_options:
|
new_options:
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
@@ -1981,11 +2023,11 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
UnsubscribeRequest:
|
UnsubscribeRequest:
|
||||||
required:
|
required:
|
||||||
- sub
|
- sub_id
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
sub:
|
sub_id:
|
||||||
$ref: '#/components/schemas/SubscriptionRequestData'
|
type: string
|
||||||
deleteMode:
|
deleteMode:
|
||||||
type: boolean
|
type: boolean
|
||||||
description: Defaults to false
|
description: Defaults to false
|
||||||
@@ -1998,6 +2040,13 @@ components:
|
|||||||
type: boolean
|
type: boolean
|
||||||
error:
|
error:
|
||||||
type: string
|
type: string
|
||||||
|
CheckSubscriptionRequest:
|
||||||
|
required:
|
||||||
|
- sub_id
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
sub_id:
|
||||||
|
type: string
|
||||||
DeleteAllFilesResponse:
|
DeleteAllFilesResponse:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
@@ -2683,6 +2732,8 @@ components:
|
|||||||
type: boolean
|
type: boolean
|
||||||
paused:
|
paused:
|
||||||
type: boolean
|
type: boolean
|
||||||
|
cancelled:
|
||||||
|
type: boolean
|
||||||
finished_step:
|
finished_step:
|
||||||
type: boolean
|
type: boolean
|
||||||
url:
|
url:
|
||||||
@@ -2726,7 +2777,7 @@ components:
|
|||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
key:
|
key:
|
||||||
type: string
|
$ref: '#/components/schemas/TaskType'
|
||||||
title:
|
title:
|
||||||
type: string
|
type: string
|
||||||
last_ran:
|
last_ran:
|
||||||
@@ -2742,9 +2793,20 @@ components:
|
|||||||
error:
|
error:
|
||||||
type: string
|
type: string
|
||||||
schedule:
|
schedule:
|
||||||
type: object
|
$ref: '#/components/schemas/Schedule'
|
||||||
options:
|
options:
|
||||||
type: object
|
type: object
|
||||||
|
TaskType:
|
||||||
|
type: string
|
||||||
|
enum:
|
||||||
|
- backup_local_db
|
||||||
|
- missing_files_check
|
||||||
|
- missing_db_records
|
||||||
|
- duplicate_files_check
|
||||||
|
- youtubedl_update_check
|
||||||
|
- delete_old_files
|
||||||
|
- import_legacy_archives
|
||||||
|
- rebuild_database
|
||||||
Schedule:
|
Schedule:
|
||||||
required:
|
required:
|
||||||
- type
|
- type
|
||||||
@@ -2830,6 +2892,8 @@ components:
|
|||||||
nullable: true
|
nullable: true
|
||||||
isPlaylist:
|
isPlaylist:
|
||||||
type: boolean
|
type: boolean
|
||||||
|
child_process:
|
||||||
|
type: object
|
||||||
archive:
|
archive:
|
||||||
type: string
|
type: string
|
||||||
timerange:
|
timerange:
|
||||||
@@ -2838,6 +2902,10 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
custom_output:
|
custom_output:
|
||||||
type: string
|
type: string
|
||||||
|
downloading:
|
||||||
|
type: boolean
|
||||||
|
paused:
|
||||||
|
type: boolean
|
||||||
videos:
|
videos:
|
||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
@@ -2877,6 +2945,7 @@ components:
|
|||||||
- sharing
|
- sharing
|
||||||
- advanced_download
|
- advanced_download
|
||||||
- downloads_manager
|
- downloads_manager
|
||||||
|
- tasks_manager
|
||||||
YesNo:
|
YesNo:
|
||||||
type: string
|
type: string
|
||||||
enum:
|
enum:
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
[](https://github.com/Tzahi12345/YoutubeDL-Material/issues)
|
[](https://github.com/Tzahi12345/YoutubeDL-Material/issues)
|
||||||
[](https://github.com/Tzahi12345/YoutubeDL-Material/blob/master/LICENSE.md)
|
[](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 15](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 17](https://angular.io/) for the frontend, and [Node.js](https://nodejs.org/) on the backend.
|
||||||
|
|
||||||
Now with [Docker](#Docker) support!
|
Now with [Docker](#Docker) support!
|
||||||
|
|
||||||
@@ -30,7 +30,7 @@ NOTE: If you would like to use Docker, you can skip down to the [Docker](#Docker
|
|||||||
|
|
||||||
Required dependencies:
|
Required dependencies:
|
||||||
|
|
||||||
* Node.js 16
|
* Node.js 18
|
||||||
* Python
|
* Python
|
||||||
|
|
||||||
Optional dependencies:
|
Optional dependencies:
|
||||||
@@ -42,7 +42,7 @@ Optional dependencies:
|
|||||||
<summary>Debian/Ubuntu</summary>
|
<summary>Debian/Ubuntu</summary>
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl -fsSL https://deb.nodesource.com/setup_16.x | sudo -E bash -
|
curl -fsSL https://deb.nodesource.com/setup_18.x | sudo -E bash -
|
||||||
sudo apt-get install nodejs youtube-dl ffmpeg unzip python npm
|
sudo apt-get install nodejs youtube-dl ffmpeg unzip python npm
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -57,7 +57,7 @@ sudo yum localinstall --nogpgcheck https://download1.rpmfusion.org/free/el/rpmfu
|
|||||||
sudo yum install centos-release-scl-rh
|
sudo yum install centos-release-scl-rh
|
||||||
sudo yum install rh-nodejs12
|
sudo yum install rh-nodejs12
|
||||||
scl enable rh-nodejs12 bash
|
scl enable rh-nodejs12 bash
|
||||||
curl -fsSL https://rpm.nodesource.com/setup_16.x | sudo bash -
|
curl -fsSL https://rpm.nodesource.com/setup_18.x | sudo bash -
|
||||||
sudo yum install nodejs youtube-dl ffmpeg ffmpeg-devel
|
sudo yum install nodejs youtube-dl ffmpeg ffmpeg-devel
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
19
angular.json
19
angular.json
@@ -66,6 +66,14 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"codespaces": {
|
||||||
|
"fileReplacements": [
|
||||||
|
{
|
||||||
|
"replace": "src/environments/environment.ts",
|
||||||
|
"with": "src/environments/environment.codespaces.ts"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
"es": {
|
"es": {
|
||||||
"localize": ["es"]
|
"localize": ["es"]
|
||||||
}
|
}
|
||||||
@@ -75,21 +83,24 @@
|
|||||||
"serve": {
|
"serve": {
|
||||||
"builder": "@angular-devkit/build-angular:dev-server",
|
"builder": "@angular-devkit/build-angular:dev-server",
|
||||||
"options": {
|
"options": {
|
||||||
"browserTarget": "youtube-dl-material:build"
|
"buildTarget": "youtube-dl-material:build"
|
||||||
},
|
},
|
||||||
"configurations": {
|
"configurations": {
|
||||||
"production": {
|
"production": {
|
||||||
"browserTarget": "youtube-dl-material:build:production"
|
"buildTarget": "youtube-dl-material:build:production"
|
||||||
},
|
},
|
||||||
"es": {
|
"es": {
|
||||||
"browserTarget": "youtube-dl-material:build:es"
|
"buildTarget": "youtube-dl-material:build:es"
|
||||||
|
},
|
||||||
|
"codespaces": {
|
||||||
|
"buildTarget": "youtube-dl-material:build:codespaces"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"extract-i18n": {
|
"extract-i18n": {
|
||||||
"builder": "@angular-devkit/build-angular:extract-i18n",
|
"builder": "@angular-devkit/build-angular:extract-i18n",
|
||||||
"options": {
|
"options": {
|
||||||
"browserTarget": "youtube-dl-material:build"
|
"buildTarget": "youtube-dl-material:build"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"serve-electron": {
|
"serve-electron": {
|
||||||
|
|||||||
233
backend/app.js
233
backend/app.js
@@ -1,4 +1,4 @@
|
|||||||
const { uuid } = require('uuidv4');
|
const { v4: uuid } = require('uuid');
|
||||||
const fs = require('fs-extra');
|
const fs = require('fs-extra');
|
||||||
const { promisify } = require('util');
|
const { promisify } = require('util');
|
||||||
const auth_api = require('./authentication/auth');
|
const auth_api = require('./authentication/auth');
|
||||||
@@ -20,11 +20,6 @@ const ps = require('ps-node');
|
|||||||
const Feed = require('feed').Feed;
|
const Feed = require('feed').Feed;
|
||||||
const session = require('express-session');
|
const session = require('express-session');
|
||||||
|
|
||||||
// needed if bin/details somehow gets deleted
|
|
||||||
if (!fs.existsSync(CONSTS.DETAILS_BIN_PATH)) fs.writeJSONSync(CONSTS.DETAILS_BIN_PATH, {"version":"2000.06.06","path":"node_modules\\youtube-dl\\bin\\youtube-dl.exe","exec":"youtube-dl.exe","downloader":"youtube-dl"})
|
|
||||||
|
|
||||||
const youtubedl = require('youtube-dl');
|
|
||||||
|
|
||||||
const logger = require('./logger');
|
const logger = require('./logger');
|
||||||
const config_api = require('./config.js');
|
const config_api = require('./config.js');
|
||||||
const downloader_api = require('./downloader');
|
const downloader_api = require('./downloader');
|
||||||
@@ -35,6 +30,7 @@ const twitch_api = require('./twitch');
|
|||||||
const youtubedl_api = require('./youtube-dl');
|
const youtubedl_api = require('./youtube-dl');
|
||||||
const archive_api = require('./archive');
|
const archive_api = require('./archive');
|
||||||
const files_api = require('./files');
|
const files_api = require('./files');
|
||||||
|
const notifications_api = require('./notifications');
|
||||||
|
|
||||||
var app = express();
|
var app = express();
|
||||||
|
|
||||||
@@ -535,15 +531,10 @@ async function loadConfig() {
|
|||||||
if (allowSubscriptions) {
|
if (allowSubscriptions) {
|
||||||
// set downloading to false
|
// set downloading to false
|
||||||
let subscriptions = await subscriptions_api.getAllSubscriptions();
|
let subscriptions = await subscriptions_api.getAllSubscriptions();
|
||||||
subscriptions_api.updateSubscriptionPropertyMultiple(subscriptions, {downloading: false});
|
subscriptions.forEach(async sub => subscriptions_api.writeSubscriptionMetadata(sub));
|
||||||
|
subscriptions_api.updateSubscriptionPropertyMultiple(subscriptions, {downloading: false, child_process: null});
|
||||||
// runs initially, then runs every ${subscriptionCheckInterval} seconds
|
// runs initially, then runs every ${subscriptionCheckInterval} seconds
|
||||||
const watchSubscriptionsInterval = function() {
|
subscriptions_api.watchSubscriptionsInterval();
|
||||||
watchSubscriptions();
|
|
||||||
const subscriptionsCheckInterval = config_api.getConfigItem('ytdl_subscriptions_check_interval');
|
|
||||||
setTimeout(watchSubscriptionsInterval, subscriptionsCheckInterval*1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
watchSubscriptionsInterval();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// start the server here
|
// start the server here
|
||||||
@@ -573,63 +564,8 @@ function loadConfigValues() {
|
|||||||
utils.updateLoggerLevel(logger_level);
|
utils.updateLoggerLevel(logger_level);
|
||||||
}
|
}
|
||||||
|
|
||||||
function calculateSubcriptionRetrievalDelay(subscriptions_amount) {
|
|
||||||
// frequency is once every 5 mins by default
|
|
||||||
const subscriptionsCheckInterval = config_api.getConfigItem('ytdl_subscriptions_check_interval');
|
|
||||||
let interval_in_ms = subscriptionsCheckInterval * 1000;
|
|
||||||
const subinterval_in_ms = interval_in_ms/subscriptions_amount;
|
|
||||||
return subinterval_in_ms;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function watchSubscriptions() {
|
|
||||||
let subscriptions = await subscriptions_api.getAllSubscriptions();
|
|
||||||
|
|
||||||
if (!subscriptions) return;
|
|
||||||
|
|
||||||
// auto pause deprecated streamingOnly mode
|
|
||||||
const streaming_only_subs = subscriptions.filter(sub => sub.streamingOnly);
|
|
||||||
subscriptions_api.updateSubscriptionPropertyMultiple(streaming_only_subs, {paused: true});
|
|
||||||
|
|
||||||
const valid_subscriptions = subscriptions.filter(sub => !sub.paused && !sub.streamingOnly);
|
|
||||||
|
|
||||||
let subscriptions_amount = valid_subscriptions.length;
|
|
||||||
let delay_interval = calculateSubcriptionRetrievalDelay(subscriptions_amount);
|
|
||||||
|
|
||||||
let current_delay = 0;
|
|
||||||
|
|
||||||
const multiUserMode = config_api.getConfigItem('ytdl_multi_user_mode');
|
|
||||||
for (let i = 0; i < valid_subscriptions.length; i++) {
|
|
||||||
let sub = valid_subscriptions[i];
|
|
||||||
|
|
||||||
// don't check the sub if the last check for the same subscription has not completed
|
|
||||||
if (subscription_timeouts[sub.id]) {
|
|
||||||
logger.verbose(`Subscription: skipped checking ${sub.name} as the last check for ${sub.name} has not completed.`);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!sub.name) {
|
|
||||||
logger.verbose(`Subscription: skipped check for subscription with uid ${sub.id} as name has not been retrieved yet.`);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.verbose('Watching ' + sub.name + ' with delay interval of ' + delay_interval);
|
|
||||||
setTimeout(async () => {
|
|
||||||
const multiUserModeChanged = config_api.getConfigItem('ytdl_multi_user_mode') !== multiUserMode;
|
|
||||||
if (multiUserModeChanged) {
|
|
||||||
logger.verbose(`Skipping subscription ${sub.name} due to multi-user mode change.`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await subscriptions_api.getVideosForSub(sub, sub.user_uid);
|
|
||||||
subscription_timeouts[sub.id] = false;
|
|
||||||
}, current_delay);
|
|
||||||
subscription_timeouts[sub.id] = true;
|
|
||||||
current_delay += delay_interval;
|
|
||||||
const subscriptionsCheckInterval = config_api.getConfigItem('ytdl_subscriptions_check_interval');
|
|
||||||
if (current_delay >= subscriptionsCheckInterval * 1000) current_delay = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getOrigin() {
|
function getOrigin() {
|
||||||
|
if (process.env.CODESPACES) return `https://${process.env.CODESPACE_NAME}-4200.${process.env.GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN}`;
|
||||||
return url_domain.origin;
|
return url_domain.origin;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -654,38 +590,11 @@ function generateEnvVarConfigItem(key) {
|
|||||||
return {key: key, value: process['env'][key]};
|
return {key: key, value: process['env'][key]};
|
||||||
}
|
}
|
||||||
|
|
||||||
// currently only works for single urls
|
|
||||||
async function getUrlInfos(url) {
|
|
||||||
let startDate = Date.now();
|
|
||||||
let result = [];
|
|
||||||
return new Promise(resolve => {
|
|
||||||
youtubedl.exec(url, ['--dump-json'], {maxBuffer: Infinity}, (err, output) => {
|
|
||||||
let new_date = Date.now();
|
|
||||||
let difference = (new_date - startDate)/1000;
|
|
||||||
logger.debug(`URL info retrieval delay: ${difference} seconds.`);
|
|
||||||
if (err) {
|
|
||||||
logger.error(`Error during retrieving formats for ${url}: ${err}`);
|
|
||||||
resolve(null);
|
|
||||||
}
|
|
||||||
let try_putput = null;
|
|
||||||
try {
|
|
||||||
try_putput = JSON.parse(output);
|
|
||||||
result = try_putput;
|
|
||||||
} catch(e) {
|
|
||||||
logger.error(`Failed to retrieve available formats for url: ${url}`);
|
|
||||||
}
|
|
||||||
resolve(result);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// youtube-dl functions
|
// youtube-dl functions
|
||||||
|
|
||||||
async function startYoutubeDL() {
|
async function startYoutubeDL() {
|
||||||
// auto update youtube-dl
|
// auto update youtube-dl
|
||||||
youtubedl_api.verifyBinaryExistsLinux();
|
await youtubedl_api.checkForYoutubeDLUpdate();
|
||||||
const update_available = await youtubedl_api.checkForYoutubeDLUpdate();
|
|
||||||
if (update_available) await youtubedl_api.updateYoutubeDL(update_available);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
app.use(function(req, res, next) {
|
app.use(function(req, res, next) {
|
||||||
@@ -705,7 +614,7 @@ app.use(function(req, res, next) {
|
|||||||
next();
|
next();
|
||||||
} else if (req.query.apiKey && config_api.getConfigItem('ytdl_use_api_key') && req.query.apiKey === config_api.getConfigItem('ytdl_api_key')) {
|
} else if (req.query.apiKey && config_api.getConfigItem('ytdl_use_api_key') && req.query.apiKey === config_api.getConfigItem('ytdl_api_key')) {
|
||||||
next();
|
next();
|
||||||
} else if (req.path.includes('/api/stream/') || req.path.includes('/api/thumbnail/') || req.path.includes('/api/rss')) {
|
} else if (req.path.includes('/api/stream/') || req.path.includes('/api/thumbnail/') || req.path.includes('/api/rss') || req.path.includes('/api/telegramRequest')) {
|
||||||
next();
|
next();
|
||||||
} else {
|
} else {
|
||||||
logger.verbose(`Rejecting request - invalid API use for endpoint: ${req.path}. API key received: ${req.query.apiKey}`);
|
logger.verbose(`Rejecting request - invalid API use for endpoint: ${req.path}. API key received: ${req.query.apiKey}`);
|
||||||
@@ -1211,10 +1120,10 @@ app.post('/api/subscribe', optionalJwt, async (req, res) => {
|
|||||||
|
|
||||||
app.post('/api/unsubscribe', optionalJwt, async (req, res) => {
|
app.post('/api/unsubscribe', optionalJwt, async (req, res) => {
|
||||||
let deleteMode = req.body.deleteMode
|
let deleteMode = req.body.deleteMode
|
||||||
let sub = req.body.sub;
|
let sub_id = req.body.sub_id;
|
||||||
let user_uid = req.isAuthenticated() ? req.user.uid : null;
|
let user_uid = req.isAuthenticated() ? req.user.uid : null;
|
||||||
|
|
||||||
let result_obj = subscriptions_api.unsubscribe(sub, deleteMode, user_uid);
|
let result_obj = subscriptions_api.unsubscribe(sub_id, deleteMode, user_uid);
|
||||||
if (result_obj.success) {
|
if (result_obj.success) {
|
||||||
res.send({
|
res.send({
|
||||||
success: result_obj.success
|
success: result_obj.success
|
||||||
@@ -1284,21 +1193,49 @@ app.post('/api/getSubscription', optionalJwt, async (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
app.post('/api/downloadVideosForSubscription', optionalJwt, async (req, res) => {
|
app.post('/api/downloadVideosForSubscription', optionalJwt, async (req, res) => {
|
||||||
let subID = req.body.subID;
|
const subID = req.body.subID;
|
||||||
let user_uid = req.isAuthenticated() ? req.user.uid : null;
|
|
||||||
|
|
||||||
let sub = subscriptions_api.getSubscription(subID, user_uid);
|
const sub = subscriptions_api.getSubscription(subID);
|
||||||
subscriptions_api.getVideosForSub(sub, user_uid);
|
subscriptions_api.getVideosForSub(sub.id);
|
||||||
res.send({
|
res.send({
|
||||||
success: true
|
success: true
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
app.post('/api/updateSubscription', optionalJwt, async (req, res) => {
|
app.post('/api/updateSubscription', optionalJwt, async (req, res) => {
|
||||||
let updated_sub = req.body.subscription;
|
const updated_sub = req.body.subscription;
|
||||||
|
|
||||||
|
const success = subscriptions_api.updateSubscription(updated_sub);
|
||||||
|
res.send({
|
||||||
|
success: success
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/api/checkSubscription', optionalJwt, async (req, res) => {
|
||||||
|
let sub_id = req.body.sub_id;
|
||||||
let user_uid = req.isAuthenticated() ? req.user.uid : null;
|
let user_uid = req.isAuthenticated() ? req.user.uid : null;
|
||||||
|
|
||||||
let success = subscriptions_api.updateSubscription(updated_sub, user_uid);
|
const success = subscriptions_api.getVideosForSub(sub_id, user_uid);
|
||||||
|
res.send({
|
||||||
|
success: success
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/api/cancelCheckSubscription', optionalJwt, async (req, res) => {
|
||||||
|
let sub_id = req.body.sub_id;
|
||||||
|
let user_uid = req.isAuthenticated() ? req.user.uid : null;
|
||||||
|
|
||||||
|
const success = subscriptions_api.cancelCheckSubscription(sub_id, user_uid);
|
||||||
|
res.send({
|
||||||
|
success: success
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/api/cancelSubscriptionCheck', optionalJwt, async (req, res) => {
|
||||||
|
let sub_id = req.body.sub_id;
|
||||||
|
let user_uid = req.isAuthenticated() ? req.user.uid : null;
|
||||||
|
|
||||||
|
const success = subscriptions_api.getVideosForSub(sub_id, user_uid);
|
||||||
res.send({
|
res.send({
|
||||||
success: success
|
success: success
|
||||||
});
|
});
|
||||||
@@ -1642,6 +1579,7 @@ app.get('/api/stream', optionalJwt, async (req, res) => {
|
|||||||
}
|
}
|
||||||
if (!fs.existsSync(file_path)) {
|
if (!fs.existsSync(file_path)) {
|
||||||
logger.error(`File ${file_path} could not be found! UID: ${uid}, ID: ${file_obj && file_obj.id}`);
|
logger.error(`File ${file_path} could not be found! UID: ${uid}, ID: ${file_obj && file_obj.id}`);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
const stat = fs.statSync(file_path);
|
const stat = fs.statSync(file_path);
|
||||||
const fileSize = stat.size;
|
const fileSize = stat.size;
|
||||||
@@ -1776,6 +1714,10 @@ app.post('/api/cancelDownload', optionalJwt, async (req, res) => {
|
|||||||
app.post('/api/getTasks', optionalJwt, async (req, res) => {
|
app.post('/api/getTasks', optionalJwt, async (req, res) => {
|
||||||
const tasks = await db_api.getRecords('tasks');
|
const tasks = await db_api.getRecords('tasks');
|
||||||
for (let task of tasks) {
|
for (let task of tasks) {
|
||||||
|
if (!tasks_api.TASKS[task['key']]) {
|
||||||
|
logger.verbose(`Task ${task['key']} does not exist!`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (task['schedule']) task['next_invocation'] = tasks_api.TASKS[task['key']]['job'].nextInvocation().getTime();
|
if (task['schedule']) task['next_invocation'] = tasks_api.TASKS[task['key']]['job'].nextInvocation().getTime();
|
||||||
}
|
}
|
||||||
res.send({tasks: tasks});
|
res.send({tasks: tasks});
|
||||||
@@ -1918,19 +1860,48 @@ app.post('/api/clearAllLogs', optionalJwt, async function(req, res) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
app.post('/api/getFileFormats', optionalJwt, async (req, res) => {
|
app.post('/api/getFileFormats', optionalJwt, async (req, res) => {
|
||||||
let url = req.body.url;
|
const url = req.body.url;
|
||||||
let result = await getUrlInfos(url);
|
const result = await downloader_api.getVideoInfoByURL(url);
|
||||||
res.send({
|
res.send({
|
||||||
result: result,
|
result: result && result.length === 1 ? result[0] : null,
|
||||||
success: !!result
|
success: result && result.length === 0
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
// user authentication
|
// user authentication
|
||||||
|
|
||||||
app.post('/api/auth/register'
|
app.post('/api/auth/register', optionalJwt, async (req, res) => {
|
||||||
, optionalJwt
|
const userid = req.body.userid;
|
||||||
, auth_api.registerUser);
|
const username = req.body.username;
|
||||||
|
const plaintextPassword = req.body.password;
|
||||||
|
|
||||||
|
if (userid !== 'admin' && !config_api.getConfigItem('ytdl_allow_registration') && !req.isAuthenticated() && (!req.user || !exports.userHasPermission(req.user.uid, 'settings'))) {
|
||||||
|
logger.error(`Registration failed for user ${userid}. Registration is disabled.`);
|
||||||
|
res.sendStatus(409);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (plaintextPassword === "") {
|
||||||
|
logger.error(`Registration failed for user ${userid}. A password must be provided.`);
|
||||||
|
res.sendStatus(409);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!userid || !username) {
|
||||||
|
logger.error(`Registration failed for user ${userid}. Username or userid is invalid.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const new_user = await auth_api.registerUser(userid, username, plaintextPassword);
|
||||||
|
|
||||||
|
if (!new_user) {
|
||||||
|
res.sendStatus(409);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.send({
|
||||||
|
user: new_user
|
||||||
|
});
|
||||||
|
});
|
||||||
app.post('/api/auth/login'
|
app.post('/api/auth/login'
|
||||||
, auth_api.passport.authenticate(['local', 'ldapauth'], {})
|
, auth_api.passport.authenticate(['local', 'ldapauth'], {})
|
||||||
, auth_api.generateJWT
|
, auth_api.generateJWT
|
||||||
@@ -1982,18 +1953,7 @@ app.post('/api/updateUser', optionalJwt, async (req, res) => {
|
|||||||
app.post('/api/deleteUser', optionalJwt, async (req, res) => {
|
app.post('/api/deleteUser', optionalJwt, async (req, res) => {
|
||||||
let uid = req.body.uid;
|
let uid = req.body.uid;
|
||||||
try {
|
try {
|
||||||
let success = false;
|
const success = await auth_api.deleteUser(uid);
|
||||||
let usersFileFolder = config_api.getConfigItem('ytdl_users_base_path');
|
|
||||||
const user_folder = path.join(__dirname, usersFileFolder, uid);
|
|
||||||
const user_db_obj = await db_api.getRecord('users', {uid: uid});
|
|
||||||
if (user_db_obj) {
|
|
||||||
// user exists, let's delete
|
|
||||||
await fs.remove(user_folder);
|
|
||||||
await db_api.removeRecord('users', {uid: uid});
|
|
||||||
success = true;
|
|
||||||
} else {
|
|
||||||
logger.error(`Could not find user with uid ${uid}`);
|
|
||||||
}
|
|
||||||
res.send({success: success});
|
res.send({success: success});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error(err);
|
logger.error(err);
|
||||||
@@ -2066,6 +2026,25 @@ app.post('/api/deleteAllNotifications', optionalJwt, async (req, res) => {
|
|||||||
res.send({success: success});
|
res.send({success: success});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
app.post('/api/telegramRequest', async (req, res) => {
|
||||||
|
if (!req.body.message && !req.body.message.text) {
|
||||||
|
logger.error('Invalid Telegram request received!');
|
||||||
|
res.sendStatus(400);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const text = req.body.message.text;
|
||||||
|
const regex_exp = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)?/gi;
|
||||||
|
const url_regex = new RegExp(regex_exp);
|
||||||
|
if (text.match(url_regex)) {
|
||||||
|
downloader_api.createDownload(text, 'video', {}, req.query.user_uid ? req.query.user_uid : null);
|
||||||
|
res.sendStatus(200);
|
||||||
|
} else {
|
||||||
|
logger.error('Invalid Telegram request received! Make sure you only send a valid URL.');
|
||||||
|
notifications_api.sendTelegramNotification({title: 'Invalid Telegram Request', body: 'Make sure you only send a valid URL.', url: text});
|
||||||
|
res.sendStatus(400);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// rss feed
|
// rss feed
|
||||||
|
|
||||||
app.get('/api/rss', async function (req, res) {
|
app.get('/api/rss', async function (req, res) {
|
||||||
@@ -2133,6 +2112,8 @@ app.use(function(req, res, next) {
|
|||||||
|
|
||||||
let index_path = path.join(__dirname, 'public', 'index.html');
|
let index_path = path.join(__dirname, 'public', 'index.html');
|
||||||
|
|
||||||
|
res.setHeader('Content-Type', 'text/html');
|
||||||
|
|
||||||
fs.createReadStream(index_path).pipe(res);
|
fs.createReadStream(index_path).pipe(res);
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -49,6 +49,7 @@
|
|||||||
"use_telegram_API": false,
|
"use_telegram_API": false,
|
||||||
"telegram_bot_token": "",
|
"telegram_bot_token": "",
|
||||||
"telegram_chat_id": "",
|
"telegram_chat_id": "",
|
||||||
|
"telegram_webhook_proxy": "",
|
||||||
"webhook_URL": "",
|
"webhook_URL": "",
|
||||||
"discord_webhook_URL": "",
|
"discord_webhook_URL": "",
|
||||||
"slack_webhook_URL": ""
|
"slack_webhook_URL": ""
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
const path = require('path');
|
const path = require('path');
|
||||||
const fs = require('fs-extra');
|
const fs = require('fs-extra');
|
||||||
const { uuid } = require('uuidv4');
|
const { v4: uuid } = require('uuid');
|
||||||
|
|
||||||
const db_api = require('./db');
|
const db_api = require('./db');
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
const config_api = require('../config');
|
const config_api = require('../config');
|
||||||
const consts = require('../consts');
|
const CONSTS = require('../consts');
|
||||||
const logger = require('../logger');
|
const logger = require('../logger');
|
||||||
const db_api = require('../db');
|
const db_api = require('../db');
|
||||||
|
|
||||||
const jwt = require('jsonwebtoken');
|
const jwt = require('jsonwebtoken');
|
||||||
const { uuid } = require('uuidv4');
|
const { v4: uuid } = require('uuid');
|
||||||
const bcrypt = require('bcryptjs');
|
const bcrypt = require('bcryptjs');
|
||||||
|
const fs = require('fs-extra');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
var LocalStrategy = require('passport-local').Strategy;
|
var LocalStrategy = require('passport-local').Strategy;
|
||||||
var LdapStrategy = require('passport-ldapauth');
|
var LdapStrategy = require('passport-ldapauth');
|
||||||
@@ -16,7 +18,7 @@ var JwtStrategy = require('passport-jwt').Strategy,
|
|||||||
let SERVER_SECRET = null;
|
let SERVER_SECRET = null;
|
||||||
let JWT_EXPIRATION = null;
|
let JWT_EXPIRATION = null;
|
||||||
let opts = null;
|
let opts = null;
|
||||||
let saltRounds = null;
|
let saltRounds = 10;
|
||||||
|
|
||||||
exports.initialize = function () {
|
exports.initialize = function () {
|
||||||
/*************************
|
/*************************
|
||||||
@@ -31,8 +33,6 @@ exports.initialize = function () {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
saltRounds = 10;
|
|
||||||
|
|
||||||
// Sometimes this value is not properly typed: https://github.com/Tzahi12345/YoutubeDL-Material/issues/813
|
// Sometimes this value is not properly typed: https://github.com/Tzahi12345/YoutubeDL-Material/issues/813
|
||||||
JWT_EXPIRATION = config_api.getConfigItem('ytdl_jwt_expiration');
|
JWT_EXPIRATION = config_api.getConfigItem('ytdl_jwt_expiration');
|
||||||
if (!(+JWT_EXPIRATION)) {
|
if (!(+JWT_EXPIRATION)) {
|
||||||
@@ -68,14 +68,7 @@ exports.initialize = function () {
|
|||||||
const setupRoles = async () => {
|
const setupRoles = async () => {
|
||||||
const required_roles = {
|
const required_roles = {
|
||||||
admin: {
|
admin: {
|
||||||
permissions: [
|
permissions: CONSTS.AVAILABLE_PERMISSIONS
|
||||||
'filemanager',
|
|
||||||
'settings',
|
|
||||||
'subscriptions',
|
|
||||||
'sharing',
|
|
||||||
'advanced_download',
|
|
||||||
'downloads_manager'
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
user: {
|
user: {
|
||||||
permissions: [
|
permissions: [
|
||||||
@@ -113,55 +106,41 @@ exports.passport.deserializeUser(function(user, done) {
|
|||||||
/***************************************
|
/***************************************
|
||||||
* Register user with hashed password
|
* Register user with hashed password
|
||||||
**************************************/
|
**************************************/
|
||||||
exports.registerUser = async function(req, res) {
|
|
||||||
var userid = req.body.userid;
|
|
||||||
var username = req.body.username;
|
|
||||||
var plaintextPassword = req.body.password;
|
|
||||||
|
|
||||||
if (userid !== 'admin' && !config_api.getConfigItem('ytdl_allow_registration') && !req.isAuthenticated() && (!req.user || !exports.userHasPermission(req.user.uid, 'settings'))) {
|
exports.registerUser = async (userid, username, plaintextPassword) => {
|
||||||
res.sendStatus(409);
|
const hash = await bcrypt.hash(plaintextPassword, saltRounds);
|
||||||
logger.error(`Registration failed for user ${userid}. Registration is disabled.`);
|
const new_user = generateUserObject(userid, username, hash);
|
||||||
return;
|
// check if user exists
|
||||||
|
if (await db_api.getRecord('users', {uid: userid})) {
|
||||||
|
// user id is taken!
|
||||||
|
logger.error('Registration failed: UID is already taken!');
|
||||||
|
return null;
|
||||||
|
} else if (await db_api.getRecord('users', {name: username})) {
|
||||||
|
// user name is taken!
|
||||||
|
logger.error('Registration failed: User name is already taken!');
|
||||||
|
return null;
|
||||||
|
} else {
|
||||||
|
// add to db
|
||||||
|
await db_api.insertRecordIntoTable('users', new_user);
|
||||||
|
logger.verbose(`New user created: ${new_user.name}`);
|
||||||
|
return new_user;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (plaintextPassword === "") {
|
exports.deleteUser = async (uid) => {
|
||||||
res.sendStatus(400);
|
let success = false;
|
||||||
logger.error(`Registration failed for user ${userid}. A password must be provided.`);
|
let usersFileFolder = config_api.getConfigItem('ytdl_users_base_path');
|
||||||
return;
|
const user_folder = path.join(__dirname, usersFileFolder, uid);
|
||||||
|
const user_db_obj = await db_api.getRecord('users', {uid: uid});
|
||||||
|
if (user_db_obj) {
|
||||||
|
// user exists, let's delete
|
||||||
|
await fs.remove(user_folder);
|
||||||
|
await db_api.removeRecord('users', {uid: uid});
|
||||||
|
success = true;
|
||||||
|
} else {
|
||||||
|
logger.error(`Could not find user with uid ${uid}`);
|
||||||
}
|
}
|
||||||
|
return success;
|
||||||
bcrypt.hash(plaintextPassword, saltRounds)
|
|
||||||
.then(async function(hash) {
|
|
||||||
let new_user = generateUserObject(userid, username, hash);
|
|
||||||
// check if user exists
|
|
||||||
if (await db_api.getRecord('users', {uid: userid})) {
|
|
||||||
// user id is taken!
|
|
||||||
logger.error('Registration failed: UID is already taken!');
|
|
||||||
res.status(409).send('UID is already taken!');
|
|
||||||
} else if (await db_api.getRecord('users', {name: username})) {
|
|
||||||
// user name is taken!
|
|
||||||
logger.error('Registration failed: User name is already taken!');
|
|
||||||
res.status(409).send('User name is already taken!');
|
|
||||||
} else {
|
|
||||||
// add to db
|
|
||||||
await db_api.insertRecordIntoTable('users', new_user);
|
|
||||||
logger.verbose(`New user created: ${new_user.name}`);
|
|
||||||
res.send({
|
|
||||||
user: new_user
|
|
||||||
});
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.then(function(result) {
|
|
||||||
|
|
||||||
})
|
|
||||||
.catch(function(err) {
|
|
||||||
logger.error(err);
|
|
||||||
if( err.code == 'ER_DUP_ENTRY' ) {
|
|
||||||
res.status(409).send('UserId already taken');
|
|
||||||
} else {
|
|
||||||
res.sendStatus(409);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/***************************************
|
/***************************************
|
||||||
@@ -242,7 +221,7 @@ exports.returnAuthResponse = async function(req, res) {
|
|||||||
user: req.user,
|
user: req.user,
|
||||||
token: req.token,
|
token: req.token,
|
||||||
permissions: await exports.userPermissions(req.user.uid),
|
permissions: await exports.userPermissions(req.user.uid),
|
||||||
available_permissions: consts['AVAILABLE_PERMISSIONS']
|
available_permissions: CONSTS.AVAILABLE_PERMISSIONS
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -326,7 +305,7 @@ exports.getUserVideos = async function(user_uid, type) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
exports.getUserVideo = async function(user_uid, file_uid, requireSharing = false) {
|
exports.getUserVideo = async function(user_uid, file_uid, requireSharing = false) {
|
||||||
let file = await db_api.getRecord('files', {file_uid: file_uid});
|
let file = await db_api.getRecord('files', {uid: file_uid});
|
||||||
|
|
||||||
// prevent unauthorized users from accessing the file info
|
// prevent unauthorized users from accessing the file info
|
||||||
if (file && !file['sharingEnabled'] && requireSharing) file = null;
|
if (file && !file['sharingEnabled'] && requireSharing) file = null;
|
||||||
@@ -413,8 +392,8 @@ exports.userPermissions = async function(user_uid) {
|
|||||||
const role_obj = await db_api.getRecord('roles', {key: role});
|
const role_obj = await db_api.getRecord('roles', {key: role});
|
||||||
const role_permissions = role_obj['permissions'];
|
const role_permissions = role_obj['permissions'];
|
||||||
|
|
||||||
for (let i = 0; i < consts['AVAILABLE_PERMISSIONS'].length; i++) {
|
for (let i = 0; i < CONSTS.AVAILABLE_PERMISSIONS.length; i++) {
|
||||||
let permission = consts['AVAILABLE_PERMISSIONS'][i];
|
let permission = CONSTS.AVAILABLE_PERMISSIONS[i];
|
||||||
|
|
||||||
const user_has_explicit_permission = user_obj['permissions'].includes(permission);
|
const user_has_explicit_permission = user_obj['permissions'].includes(permission);
|
||||||
const permission_in_overrides = user_obj['permission_overrides'].includes(permission);
|
const permission_in_overrides = user_obj['permission_overrides'].includes(permission);
|
||||||
|
|||||||
@@ -32,10 +32,8 @@ async function categorize(file_jsons) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let i = 0; i < file_jsons.length; i++) {
|
for (const file_json of file_jsons) {
|
||||||
const file_json = file_jsons[i];
|
for (const category of categories) {
|
||||||
for (let j = 0; j < categories.length; j++) {
|
|
||||||
const category = categories[j];
|
|
||||||
const rules = category['rules'];
|
const rules = category['rules'];
|
||||||
|
|
||||||
// if rules for current category apply, then that is the selected category
|
// if rules for current category apply, then that is the selected category
|
||||||
|
|||||||
@@ -1,22 +1,26 @@
|
|||||||
const logger = require('./logger');
|
const logger = require('./logger');
|
||||||
|
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
|
const { BehaviorSubject } = require('rxjs');
|
||||||
|
|
||||||
|
exports.CONFIG_ITEMS = require('./consts.js')['CONFIG_ITEMS'];
|
||||||
|
exports.descriptors = {}; // to get rid of file locks when needed, TODO: move to youtube-dl.js
|
||||||
|
|
||||||
let CONFIG_ITEMS = require('./consts.js')['CONFIG_ITEMS'];
|
|
||||||
const debugMode = process.env.YTDL_MODE === 'debug';
|
const debugMode = process.env.YTDL_MODE === 'debug';
|
||||||
|
|
||||||
let configPath = debugMode ? '../src/assets/default.json' : 'appdata/default.json';
|
let configPath = debugMode ? '../src/assets/default.json' : 'appdata/default.json';
|
||||||
|
exports.config_updated = new BehaviorSubject();
|
||||||
|
|
||||||
function initialize() {
|
exports.initialize = () => {
|
||||||
ensureConfigFileExists();
|
ensureConfigFileExists();
|
||||||
ensureConfigItemsExist();
|
ensureConfigItemsExist();
|
||||||
}
|
}
|
||||||
|
|
||||||
function ensureConfigItemsExist() {
|
function ensureConfigItemsExist() {
|
||||||
const config_keys = Object.keys(CONFIG_ITEMS);
|
const config_keys = Object.keys(exports.CONFIG_ITEMS);
|
||||||
for (let i = 0; i < config_keys.length; i++) {
|
for (let i = 0; i < config_keys.length; i++) {
|
||||||
const config_key = config_keys[i];
|
const config_key = config_keys[i];
|
||||||
getConfigItem(config_key);
|
exports.getConfigItem(config_key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,17 +61,17 @@ function getElementNameInConfig(path) {
|
|||||||
/**
|
/**
|
||||||
* Check if config exists. If not, write default config to config path
|
* Check if config exists. If not, write default config to config path
|
||||||
*/
|
*/
|
||||||
function configExistsCheck() {
|
exports.configExistsCheck = () => {
|
||||||
let exists = fs.existsSync(configPath);
|
let exists = fs.existsSync(configPath);
|
||||||
if (!exists) {
|
if (!exists) {
|
||||||
setConfigFile(DEFAULT_CONFIG);
|
exports.setConfigFile(DEFAULT_CONFIG);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Gets config file and returns as a json
|
* Gets config file and returns as a json
|
||||||
*/
|
*/
|
||||||
function getConfigFile() {
|
exports.getConfigFile = () => {
|
||||||
try {
|
try {
|
||||||
let raw_data = fs.readFileSync(configPath);
|
let raw_data = fs.readFileSync(configPath);
|
||||||
let parsed_data = JSON.parse(raw_data);
|
let parsed_data = JSON.parse(raw_data);
|
||||||
@@ -78,35 +82,40 @@ function getConfigFile() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function setConfigFile(config) {
|
exports.setConfigFile = (config) => {
|
||||||
try {
|
try {
|
||||||
|
const old_config = exports.getConfigFile();
|
||||||
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
||||||
|
const changes = exports.findChangedConfigItems(old_config, config);
|
||||||
|
if (changes.length > 0) {
|
||||||
|
for (const change of changes) exports.config_updated.next(change);
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getConfigItem(key) {
|
exports.getConfigItem = (key) => {
|
||||||
let config_json = getConfigFile();
|
let config_json = exports.getConfigFile();
|
||||||
if (!CONFIG_ITEMS[key]) {
|
if (!exports.CONFIG_ITEMS[key]) {
|
||||||
logger.error(`Config item with key '${key}' is not recognized.`);
|
logger.error(`Config item with key '${key}' is not recognized.`);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
let path = CONFIG_ITEMS[key]['path'];
|
let path = exports.CONFIG_ITEMS[key]['path'];
|
||||||
const val = Object.byString(config_json, path);
|
const val = Object.byString(config_json, path);
|
||||||
if (val === undefined && Object.byString(DEFAULT_CONFIG, path) !== undefined) {
|
if (val === undefined && Object.byString(DEFAULT_CONFIG, path) !== undefined) {
|
||||||
logger.warn(`Cannot find config with key '${key}'. Creating one with the default value...`);
|
logger.warn(`Cannot find config with key '${key}'. Creating one with the default value...`);
|
||||||
setConfigItem(key, Object.byString(DEFAULT_CONFIG, path));
|
exports.setConfigItem(key, Object.byString(DEFAULT_CONFIG, path));
|
||||||
return Object.byString(DEFAULT_CONFIG, path);
|
return Object.byString(DEFAULT_CONFIG, path);
|
||||||
}
|
}
|
||||||
return Object.byString(config_json, path);
|
return Object.byString(config_json, path);
|
||||||
}
|
}
|
||||||
|
|
||||||
function setConfigItem(key, value) {
|
exports.setConfigItem = (key, value) => {
|
||||||
let success = false;
|
let success = false;
|
||||||
let config_json = getConfigFile();
|
let config_json = exports.getConfigFile();
|
||||||
let path = CONFIG_ITEMS[key]['path'];
|
let path = exports.CONFIG_ITEMS[key]['path'];
|
||||||
let element_name = getElementNameInConfig(path);
|
let element_name = getElementNameInConfig(path);
|
||||||
let parent_path = getParentPath(path);
|
let parent_path = getParentPath(path);
|
||||||
let parent_object = Object.byString(config_json, parent_path);
|
let parent_object = Object.byString(config_json, parent_path);
|
||||||
@@ -118,20 +127,18 @@ function setConfigItem(key, value) {
|
|||||||
parent_parent_object[parent_parent_single_key] = {};
|
parent_parent_object[parent_parent_single_key] = {};
|
||||||
parent_object = Object.byString(config_json, parent_path);
|
parent_object = Object.byString(config_json, parent_path);
|
||||||
}
|
}
|
||||||
|
if (value === 'false') value = false;
|
||||||
|
if (value === 'true') value = true;
|
||||||
|
parent_object[element_name] = value;
|
||||||
|
|
||||||
if (value === 'false' || value === 'true') {
|
success = exports.setConfigFile(config_json);
|
||||||
parent_object[element_name] = (value === 'true');
|
|
||||||
} else {
|
|
||||||
parent_object[element_name] = value;
|
|
||||||
}
|
|
||||||
success = setConfigFile(config_json);
|
|
||||||
|
|
||||||
return success;
|
return success;
|
||||||
}
|
}
|
||||||
|
|
||||||
function setConfigItems(items) {
|
exports.setConfigItems = (items) => {
|
||||||
let success = false;
|
let success = false;
|
||||||
let config_json = getConfigFile();
|
let config_json = exports.getConfigFile();
|
||||||
for (let i = 0; i < items.length; i++) {
|
for (let i = 0; i < items.length; i++) {
|
||||||
let key = items[i].key;
|
let key = items[i].key;
|
||||||
let value = items[i].value;
|
let value = items[i].value;
|
||||||
@@ -141,7 +148,7 @@ function setConfigItems(items) {
|
|||||||
value = (value === 'true');
|
value = (value === 'true');
|
||||||
}
|
}
|
||||||
|
|
||||||
let item_path = CONFIG_ITEMS[key]['path'];
|
let item_path = exports.CONFIG_ITEMS[key]['path'];
|
||||||
let item_parent_path = getParentPath(item_path);
|
let item_parent_path = getParentPath(item_path);
|
||||||
let item_element_name = getElementNameInConfig(item_path);
|
let item_element_name = getElementNameInConfig(item_path);
|
||||||
|
|
||||||
@@ -149,28 +156,41 @@ function setConfigItems(items) {
|
|||||||
item_parent_object[item_element_name] = value;
|
item_parent_object[item_element_name] = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
success = setConfigFile(config_json);
|
success = exports.setConfigFile(config_json);
|
||||||
return success;
|
return success;
|
||||||
}
|
}
|
||||||
|
|
||||||
function globalArgsRequiresSafeDownload() {
|
exports.globalArgsRequiresSafeDownload = () => {
|
||||||
const globalArgs = getConfigItem('ytdl_custom_args').split(',,');
|
const globalArgs = exports.getConfigItem('ytdl_custom_args').split(',,');
|
||||||
const argsThatRequireSafeDownload = ['--write-sub', '--write-srt', '--proxy'];
|
const argsThatRequireSafeDownload = ['--write-sub', '--write-srt', '--proxy'];
|
||||||
const failedArgs = globalArgs.filter(arg => argsThatRequireSafeDownload.includes(arg));
|
const failedArgs = globalArgs.filter(arg => argsThatRequireSafeDownload.includes(arg));
|
||||||
return failedArgs && failedArgs.length > 0;
|
return failedArgs && failedArgs.length > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
exports.findChangedConfigItems = (old_config, new_config, path = '', changedConfigItems = [], depth = 0) => {
|
||||||
getConfigItem: getConfigItem,
|
if (typeof old_config === 'object' && typeof new_config === 'object' && depth < 3) {
|
||||||
setConfigItem: setConfigItem,
|
for (const key in old_config) {
|
||||||
setConfigItems: setConfigItems,
|
if (Object.prototype.hasOwnProperty.call(new_config, key)) {
|
||||||
getConfigFile: getConfigFile,
|
exports.findChangedConfigItems(old_config[key], new_config[key], `${path}${path ? '.' : ''}${key}`, changedConfigItems, depth + 1);
|
||||||
setConfigFile: setConfigFile,
|
}
|
||||||
configExistsCheck: configExistsCheck,
|
}
|
||||||
CONFIG_ITEMS: CONFIG_ITEMS,
|
} else {
|
||||||
initialize: initialize,
|
if (JSON.stringify(old_config) !== JSON.stringify(new_config)) {
|
||||||
descriptors: {},
|
const key = getConfigItemKeyByPath(path);
|
||||||
globalArgsRequiresSafeDownload: globalArgsRequiresSafeDownload
|
changedConfigItems.push({
|
||||||
|
key: key ? key : path.split('.')[path.split('.').length - 1], // return key in CONFIG_ITEMS or the object key
|
||||||
|
old_value: JSON.parse(JSON.stringify(old_config)),
|
||||||
|
new_value: JSON.parse(JSON.stringify(new_config))
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return changedConfigItems;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getConfigItemKeyByPath(path) {
|
||||||
|
const found_item = Object.values(exports.CONFIG_ITEMS).find(item => item.path === path);
|
||||||
|
if (found_item) return found_item['key'];
|
||||||
|
else return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const DEFAULT_CONFIG = {
|
const DEFAULT_CONFIG = {
|
||||||
@@ -219,6 +239,7 @@ const DEFAULT_CONFIG = {
|
|||||||
"use_telegram_API": false,
|
"use_telegram_API": false,
|
||||||
"telegram_bot_token": "",
|
"telegram_bot_token": "",
|
||||||
"telegram_chat_id": "",
|
"telegram_chat_id": "",
|
||||||
|
"telegram_webhook_proxy": "",
|
||||||
"webhook_URL": "",
|
"webhook_URL": "",
|
||||||
"discord_webhook_URL": "",
|
"discord_webhook_URL": "",
|
||||||
"slack_webhook_URL": "",
|
"slack_webhook_URL": "",
|
||||||
|
|||||||
@@ -154,6 +154,10 @@ exports.CONFIG_ITEMS = {
|
|||||||
'key': 'ytdl_telegram_chat_id',
|
'key': 'ytdl_telegram_chat_id',
|
||||||
'path': 'YoutubeDLMaterial.API.telegram_chat_id'
|
'path': 'YoutubeDLMaterial.API.telegram_chat_id'
|
||||||
},
|
},
|
||||||
|
'ytdl_telegram_webhook_proxy': {
|
||||||
|
'key': 'ytdl_telegram_webhook_proxy',
|
||||||
|
'path': 'YoutubeDLMaterial.API.telegram_webhook_proxy'
|
||||||
|
},
|
||||||
'ytdl_webhook_url': {
|
'ytdl_webhook_url': {
|
||||||
'key': 'ytdl_webhook_url',
|
'key': 'ytdl_webhook_url',
|
||||||
'path': 'YoutubeDLMaterial.API.webhook_URL'
|
'path': 'YoutubeDLMaterial.API.webhook_URL'
|
||||||
@@ -269,7 +273,8 @@ exports.AVAILABLE_PERMISSIONS = [
|
|||||||
'tasks_manager'
|
'tasks_manager'
|
||||||
];
|
];
|
||||||
|
|
||||||
exports.DETAILS_BIN_PATH = 'node_modules/youtube-dl/bin/details'
|
exports.DETAILS_BIN_PATH = 'appdata/youtube-dl.json'
|
||||||
|
exports.OUTDATED_YOUTUBEDL_VERSION = "2020.00.00";
|
||||||
|
|
||||||
// args that have a value after it (e.g. -o <output> or -f <format>)
|
// args that have a value after it (e.g. -o <output> or -f <format>)
|
||||||
const YTDL_ARGS_WITH_VALUES = [
|
const YTDL_ARGS_WITH_VALUES = [
|
||||||
@@ -347,9 +352,11 @@ const YTDL_ARGS_WITH_VALUES = [
|
|||||||
'--convert-subs'
|
'--convert-subs'
|
||||||
];
|
];
|
||||||
|
|
||||||
|
exports.SUBSCRIPTION_BACKUP_PATH = 'subscription_backup.json'
|
||||||
|
|
||||||
// we're using a Set here for performance
|
// we're using a Set here for performance
|
||||||
exports.YTDL_ARGS_WITH_VALUES = new Set(YTDL_ARGS_WITH_VALUES);
|
exports.YTDL_ARGS_WITH_VALUES = new Set(YTDL_ARGS_WITH_VALUES);
|
||||||
|
|
||||||
exports.ICON_URL = 'https://i.imgur.com/IKOlr0N.png';
|
exports.ICON_URL = 'https://i.imgur.com/IKOlr0N.png';
|
||||||
|
|
||||||
exports.CURRENT_VERSION = 'v4.3.1';
|
exports.CURRENT_VERSION = 'v4.3.2';
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
const fs = require('fs-extra')
|
const fs = require('fs-extra')
|
||||||
const path = require('path')
|
const path = require('path')
|
||||||
const { MongoClient } = require("mongodb");
|
const { MongoClient } = require("mongodb");
|
||||||
const { uuid } = require('uuidv4');
|
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
|
|
||||||
const config_api = require('./config');
|
const config_api = require('./config');
|
||||||
@@ -11,9 +10,8 @@ const logger = require('./logger');
|
|||||||
const low = require('lowdb')
|
const low = require('lowdb')
|
||||||
const FileSync = require('lowdb/adapters/FileSync');
|
const FileSync = require('lowdb/adapters/FileSync');
|
||||||
const { BehaviorSubject } = require('rxjs');
|
const { BehaviorSubject } = require('rxjs');
|
||||||
const local_adapter = new FileSync('./appdata/local_db.json');
|
|
||||||
const local_db = low(local_adapter);
|
|
||||||
|
|
||||||
|
let local_db = null;
|
||||||
let database = null;
|
let database = null;
|
||||||
exports.database_initialized = false;
|
exports.database_initialized = false;
|
||||||
exports.database_initialized_bs = new BehaviorSubject(false);
|
exports.database_initialized_bs = new BehaviorSubject(false);
|
||||||
@@ -73,10 +71,6 @@ const tables = {
|
|||||||
|
|
||||||
const tables_list = Object.keys(tables);
|
const tables_list = Object.keys(tables);
|
||||||
|
|
||||||
const local_db_defaults = {}
|
|
||||||
tables_list.forEach(table => {local_db_defaults[table] = []});
|
|
||||||
local_db.defaults(local_db_defaults).write();
|
|
||||||
|
|
||||||
let using_local_db = null;
|
let using_local_db = null;
|
||||||
|
|
||||||
function setDB(input_db, input_users_db) {
|
function setDB(input_db, input_users_db) {
|
||||||
@@ -85,11 +79,18 @@ function setDB(input_db, input_users_db) {
|
|||||||
exports.users_db = input_users_db
|
exports.users_db = input_users_db
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.initialize = (input_db, input_users_db) => {
|
exports.initialize = (input_db, input_users_db, db_name = 'local_db.json') => {
|
||||||
setDB(input_db, input_users_db);
|
setDB(input_db, input_users_db);
|
||||||
|
|
||||||
// must be done here to prevent getConfigItem from being called before init
|
// must be done here to prevent getConfigItem from being called before init
|
||||||
using_local_db = config_api.getConfigItem('ytdl_use_local_db');
|
using_local_db = config_api.getConfigItem('ytdl_use_local_db');
|
||||||
|
|
||||||
|
const local_adapter = new FileSync(`./appdata/${db_name}`);
|
||||||
|
local_db = low(local_adapter);
|
||||||
|
|
||||||
|
const local_db_defaults = {}
|
||||||
|
tables_list.forEach(table => {local_db_defaults[table] = []});
|
||||||
|
local_db.defaults(local_db_defaults).write();
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.connectToDB = async (retries = 5, no_fallback = false, custom_connection_string = null) => {
|
exports.connectToDB = async (retries = 5, no_fallback = false, custom_connection_string = null) => {
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
const fs = require('fs-extra');
|
const fs = require('fs-extra');
|
||||||
const { uuid } = require('uuidv4');
|
const { v4: uuid } = require('uuid');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const NodeID3 = require('node-id3')
|
const NodeID3 = require('node-id3')
|
||||||
const Mutex = require('async-mutex').Mutex;
|
const Mutex = require('async-mutex').Mutex;
|
||||||
|
|
||||||
const youtubedl = require('youtube-dl');
|
|
||||||
|
|
||||||
const logger = require('./logger');
|
const logger = require('./logger');
|
||||||
|
const youtubedl_api = require('./youtube-dl');
|
||||||
const config_api = require('./config');
|
const config_api = require('./config');
|
||||||
const twitch_api = require('./twitch');
|
const twitch_api = require('./twitch');
|
||||||
const { create } = require('xmlbuilder2');
|
const { create } = require('xmlbuilder2');
|
||||||
@@ -20,11 +19,13 @@ const archive_api = require('./archive');
|
|||||||
const mutex = new Mutex();
|
const mutex = new Mutex();
|
||||||
let should_check_downloads = true;
|
let should_check_downloads = true;
|
||||||
|
|
||||||
|
const download_to_child_process = {};
|
||||||
|
|
||||||
if (db_api.database_initialized) {
|
if (db_api.database_initialized) {
|
||||||
setupDownloads();
|
exports.setupDownloads();
|
||||||
} else {
|
} else {
|
||||||
db_api.database_initialized_bs.subscribe(init => {
|
db_api.database_initialized_bs.subscribe(init => {
|
||||||
if (init) setupDownloads();
|
if (init) exports.setupDownloads();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,7 +48,7 @@ We use checkDownloads() to move downloads through the steps and call their respe
|
|||||||
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
exports.createDownload = async (url, type, options, user_uid = null, sub_id = null, sub_name = null, prefetched_info = null) => {
|
exports.createDownload = async (url, type, options, user_uid = null, sub_id = null, sub_name = null, prefetched_info = null, paused = false) => {
|
||||||
return await mutex.runExclusive(async () => {
|
return await mutex.runExclusive(async () => {
|
||||||
const download = {
|
const download = {
|
||||||
url: url,
|
url: url,
|
||||||
@@ -60,7 +61,7 @@ exports.createDownload = async (url, type, options, user_uid = null, sub_id = nu
|
|||||||
options: options,
|
options: options,
|
||||||
uid: uuid(),
|
uid: uuid(),
|
||||||
step_index: 0,
|
step_index: 0,
|
||||||
paused: false,
|
paused: paused,
|
||||||
running: false,
|
running: false,
|
||||||
finished_step: true,
|
finished_step: true,
|
||||||
error: null,
|
error: null,
|
||||||
@@ -83,8 +84,11 @@ exports.pauseDownload = async (download_uid) => {
|
|||||||
} else if (download['finished']) {
|
} else if (download['finished']) {
|
||||||
logger.info(`Download ${download_uid} could not be paused before completing.`);
|
logger.info(`Download ${download_uid} could not be paused before completing.`);
|
||||||
return false;
|
return false;
|
||||||
|
} else {
|
||||||
|
logger.info(`Pausing download ${download_uid}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
killActiveDownload(download);
|
||||||
return await db_api.updateRecord('download_queue', {uid: download_uid}, {paused: true, running: false});
|
return await db_api.updateRecord('download_queue', {uid: download_uid}, {paused: true, running: false});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,21 +123,28 @@ exports.cancelDownload = async (download_uid) => {
|
|||||||
} else if (download['finished']) {
|
} else if (download['finished']) {
|
||||||
logger.info(`Download ${download_uid} could not be cancelled before completing.`);
|
logger.info(`Download ${download_uid} could not be cancelled before completing.`);
|
||||||
return false;
|
return false;
|
||||||
|
} else {
|
||||||
|
logger.info(`Cancelling download ${download_uid}`);
|
||||||
}
|
}
|
||||||
return await db_api.updateRecord('download_queue', {uid: download_uid}, {cancelled: true, running: false});
|
|
||||||
|
killActiveDownload(download);
|
||||||
|
await handleDownloadError(download_uid, 'Cancelled', 'cancelled');
|
||||||
|
return await db_api.updateRecord('download_queue', {uid: download_uid}, {cancelled: true});
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.clearDownload = async (download_uid) => {
|
exports.clearDownload = async (download_uid) => {
|
||||||
return await db_api.removeRecord('download_queue', {uid: download_uid});
|
return await db_api.removeRecord('download_queue', {uid: download_uid});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleDownloadError(download, error_message, error_type = null) {
|
async function handleDownloadError(download_uid, error_message, error_type = null) {
|
||||||
if (!download || !download['uid']) return;
|
if (!download_uid) return;
|
||||||
|
const download = await db_api.getRecord('download_queue', {uid: download_uid});
|
||||||
|
if (!download || download['error']) return;
|
||||||
notifications_api.sendDownloadErrorNotification(download, download['user_uid'], error_message, error_type);
|
notifications_api.sendDownloadErrorNotification(download, download['user_uid'], error_message, error_type);
|
||||||
await db_api.updateRecord('download_queue', {uid: download['uid']}, {error: error_message, finished: true, running: false, error_type: error_type});
|
await db_api.updateRecord('download_queue', {uid: download['uid']}, {error: error_message, finished: true, running: false, error_type: error_type});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function setupDownloads() {
|
exports.setupDownloads = async () => {
|
||||||
await fixDownloadState();
|
await fixDownloadState();
|
||||||
setInterval(checkDownloads, 1000);
|
setInterval(checkDownloads, 1000);
|
||||||
}
|
}
|
||||||
@@ -179,22 +190,30 @@ async function checkDownloads() {
|
|||||||
if (waiting_download['sub_id']) {
|
if (waiting_download['sub_id']) {
|
||||||
const sub_missing = !(await db_api.getRecord('subscriptions', {id: waiting_download['sub_id']}));
|
const sub_missing = !(await db_api.getRecord('subscriptions', {id: waiting_download['sub_id']}));
|
||||||
if (sub_missing) {
|
if (sub_missing) {
|
||||||
handleDownloadError(waiting_download, `Download failed as subscription with id '${waiting_download['sub_id']}' is missing!`, 'sub_id_missing');
|
handleDownloadError(waiting_download['uid'], `Download failed as subscription with id '${waiting_download['sub_id']}' is missing!`, 'sub_id_missing');
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// move to next step
|
// move to next step
|
||||||
running_downloads_count++;
|
running_downloads_count++;
|
||||||
if (waiting_download['step_index'] === 0) {
|
if (waiting_download['step_index'] === 0) {
|
||||||
collectInfo(waiting_download['uid']);
|
exports.collectInfo(waiting_download['uid']);
|
||||||
} else if (waiting_download['step_index'] === 1) {
|
} else if (waiting_download['step_index'] === 1) {
|
||||||
downloadQueuedFile(waiting_download['uid']);
|
exports.downloadQueuedFile(waiting_download['uid']);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function collectInfo(download_uid) {
|
function killActiveDownload(download) {
|
||||||
|
const child_process = download_to_child_process[download['uid']];
|
||||||
|
if (download['step_index'] === 2 && child_process) {
|
||||||
|
youtubedl_api.killYoutubeDLProcess(child_process);
|
||||||
|
delete download_to_child_process[download['uid']];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.collectInfo = async (download_uid) => {
|
||||||
const download = await db_api.getRecord('download_queue', {uid: download_uid});
|
const download = await db_api.getRecord('download_queue', {uid: download_uid});
|
||||||
if (download['paused']) {
|
if (download['paused']) {
|
||||||
return;
|
return;
|
||||||
@@ -217,21 +236,21 @@ async function collectInfo(download_uid) {
|
|||||||
// get video info prior to download
|
// get video info prior to download
|
||||||
let info = download['prefetched_info'] ? download['prefetched_info'] : await exports.getVideoInfoByURL(url, args, download_uid);
|
let info = download['prefetched_info'] ? download['prefetched_info'] : await exports.getVideoInfoByURL(url, args, download_uid);
|
||||||
|
|
||||||
if (!info) {
|
if (!info || info.length === 0) {
|
||||||
// info failed, error presumably already recorded
|
// info failed, error presumably already recorded
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// in subscriptions we don't care if archive mode is enabled, but we already removed archived videos from subs by this point
|
// in subscriptions we don't care if archive mode is enabled, but we already removed archived videos from subs by this point
|
||||||
const useYoutubeDLArchive = config_api.getConfigItem('ytdl_use_youtubedl_archive');
|
const useYoutubeDLArchive = config_api.getConfigItem('ytdl_use_youtubedl_archive');
|
||||||
if (useYoutubeDLArchive && !options.ignoreArchive) {
|
if (useYoutubeDLArchive && !options.ignoreArchive && info.length === 1) {
|
||||||
const exists_in_archive = await archive_api.existsInArchive(info['extractor'], info['id'], type, download['user_uid'], download['sub_id']);
|
const info_obj = info[0];
|
||||||
|
const exists_in_archive = await archive_api.existsInArchive(info['extractor'], info_obj['id'], type, download['user_uid'], download['sub_id']);
|
||||||
if (exists_in_archive) {
|
if (exists_in_archive) {
|
||||||
const error = `File '${info['title']}' already exists in archive! Disable the archive or override to continue downloading.`;
|
const error = `File '${info_obj['title']}' already exists in archive! Disable the archive or override to continue downloading.`;
|
||||||
logger.warn(error);
|
logger.warn(error);
|
||||||
if (download_uid) {
|
if (download_uid) {
|
||||||
const download = await db_api.getRecord('download_queue', {uid: download_uid});
|
await handleDownloadError(download_uid, error, 'exists_in_archive');
|
||||||
await handleDownloadError(download, error, 'exists_in_archive');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -240,7 +259,7 @@ async function collectInfo(download_uid) {
|
|||||||
let category = null;
|
let category = null;
|
||||||
|
|
||||||
// check if it fits into a category. If so, then get info again using new args
|
// check if it fits into a category. If so, then get info again using new args
|
||||||
if (!Array.isArray(info) || config_api.getConfigItem('ytdl_allow_playlist_categorization')) category = await categories_api.categorize(info);
|
if (info.length === 1 || config_api.getConfigItem('ytdl_allow_playlist_categorization')) category = await categories_api.categorize(info);
|
||||||
|
|
||||||
// set custom output if the category has one and re-retrieve info so the download manager has the right file name
|
// set custom output if the category has one and re-retrieve info so the download manager has the right file name
|
||||||
if (category && category['custom_output']) {
|
if (category && category['custom_output']) {
|
||||||
@@ -259,26 +278,22 @@ async function collectInfo(download_uid) {
|
|||||||
const files_to_check_for_progress = [];
|
const files_to_check_for_progress = [];
|
||||||
|
|
||||||
// store info in download for future use
|
// store info in download for future use
|
||||||
if (Array.isArray(info)) {
|
for (let info_obj of info) files_to_check_for_progress.push(utils.removeFileExtension(info_obj['_filename']));
|
||||||
for (let info_obj of info) files_to_check_for_progress.push(utils.removeFileExtension(info_obj['_filename']));
|
|
||||||
} else {
|
|
||||||
files_to_check_for_progress.push(utils.removeFileExtension(info['_filename']));
|
|
||||||
}
|
|
||||||
|
|
||||||
const playlist_title = Array.isArray(info) ? info[0]['playlist_title'] || info[0]['playlist'] : null;
|
const title = info.length > 1 ? info[0]['playlist_title'] || info[0]['playlist'] : info[0]['title'];
|
||||||
await db_api.updateRecord('download_queue', {uid: download_uid}, {args: args,
|
await db_api.updateRecord('download_queue', {uid: download_uid}, {args: args,
|
||||||
finished_step: true,
|
finished_step: true,
|
||||||
running: false,
|
running: false,
|
||||||
options: options,
|
options: options,
|
||||||
files_to_check_for_progress: files_to_check_for_progress,
|
files_to_check_for_progress: files_to_check_for_progress,
|
||||||
expected_file_size: expected_file_size,
|
expected_file_size: expected_file_size,
|
||||||
title: playlist_title ? playlist_title : info['title'],
|
title: title,
|
||||||
category: stripped_category,
|
category: stripped_category,
|
||||||
prefetched_info: null
|
prefetched_info: null
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function downloadQueuedFile(download_uid) {
|
exports.downloadQueuedFile = async(download_uid, customDownloadHandler = null) => {
|
||||||
const download = await db_api.getRecord('download_queue', {uid: download_uid});
|
const download = await db_api.getRecord('download_queue', {uid: download_uid});
|
||||||
if (download['paused']) {
|
if (download['paused']) {
|
||||||
return;
|
return;
|
||||||
@@ -306,121 +321,112 @@ async function downloadQueuedFile(download_uid) {
|
|||||||
const start_time = Date.now();
|
const start_time = Date.now();
|
||||||
|
|
||||||
const download_checker = setInterval(() => checkDownloadPercent(download['uid']), 1000);
|
const download_checker = setInterval(() => checkDownloadPercent(download['uid']), 1000);
|
||||||
|
const file_objs = [];
|
||||||
// download file
|
// download file
|
||||||
youtubedl.exec(url, args, {maxBuffer: Infinity}, async function(err, output) {
|
let {child_process, callback} = await youtubedl_api.runYoutubeDL(url, args, customDownloadHandler);
|
||||||
const file_objs = [];
|
if (child_process) download_to_child_process[download['uid']] = child_process;
|
||||||
let end_time = Date.now();
|
const {parsed_output, err} = await callback;
|
||||||
let difference = (end_time - start_time)/1000;
|
clearInterval(download_checker);
|
||||||
logger.debug(`${type === 'audio' ? 'Audio' : 'Video'} download delay: ${difference} seconds.`);
|
let end_time = Date.now();
|
||||||
clearInterval(download_checker);
|
let difference = (end_time - start_time)/1000;
|
||||||
if (err) {
|
logger.debug(`${type === 'audio' ? 'Audio' : 'Video'} download delay: ${difference} seconds.`);
|
||||||
logger.error(err.stderr);
|
if (!parsed_output) {
|
||||||
await handleDownloadError(download, err.stderr, 'unknown_error');
|
const errored_download = await db_api.getRecord('download_queue', {uid: download_uid});
|
||||||
|
if (errored_download && errored_download['paused']) return;
|
||||||
|
logger.error(err.toString());
|
||||||
|
await handleDownloadError(download_uid, err.toString(), 'unknown_error');
|
||||||
|
resolve(false);
|
||||||
|
return;
|
||||||
|
} else if (parsed_output) {
|
||||||
|
if (parsed_output.length === 0 || parsed_output[0].length === 0) {
|
||||||
|
// ERROR!
|
||||||
|
const error_message = `No output received for video download, check if it exists in your archive.`;
|
||||||
|
await handleDownloadError(download_uid, error_message, 'no_output');
|
||||||
|
logger.warn(error_message);
|
||||||
resolve(false);
|
resolve(false);
|
||||||
return;
|
return;
|
||||||
} else if (output) {
|
|
||||||
if (output.length === 0 || output[0].length === 0) {
|
|
||||||
// ERROR!
|
|
||||||
const error_message = `No output received for video download, check if it exists in your archive.`;
|
|
||||||
await handleDownloadError(download, error_message, 'no_output');
|
|
||||||
logger.warn(error_message);
|
|
||||||
resolve(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = 0; i < output.length; i++) {
|
|
||||||
let output_json = null;
|
|
||||||
try {
|
|
||||||
// we have to do this because sometimes there will be leading characters before the actual json
|
|
||||||
const start_idx = output[i].indexOf('{"');
|
|
||||||
const clean_output = output[i].slice(start_idx, output[i].length);
|
|
||||||
output_json = JSON.parse(clean_output);
|
|
||||||
} catch(e) {
|
|
||||||
output_json = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!output_json) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// get filepath with no extension
|
|
||||||
const filepath_no_extension = utils.removeFileExtension(output_json['_filename']);
|
|
||||||
|
|
||||||
const ext = type === 'audio' ? '.mp3' : '.mp4';
|
|
||||||
var full_file_path = filepath_no_extension + ext;
|
|
||||||
var file_name = filepath_no_extension.substring(fileFolderPath.length, filepath_no_extension.length);
|
|
||||||
|
|
||||||
if (type === 'video' && url.includes('twitch.tv/videos/') && url.split('twitch.tv/videos/').length > 1
|
|
||||||
&& config_api.getConfigItem('ytdl_twitch_auto_download_chat')) {
|
|
||||||
let vodId = url.split('twitch.tv/videos/')[1];
|
|
||||||
vodId = vodId.split('?')[0];
|
|
||||||
twitch_api.downloadTwitchChatByVODID(vodId, file_name, type, download['user_uid']);
|
|
||||||
}
|
|
||||||
|
|
||||||
// renames file if necessary due to bug
|
|
||||||
if (!fs.existsSync(output_json['_filename']) && fs.existsSync(output_json['_filename'] + '.webm')) {
|
|
||||||
try {
|
|
||||||
fs.renameSync(output_json['_filename'] + '.webm', output_json['_filename']);
|
|
||||||
logger.info('Renamed ' + file_name + '.webm to ' + file_name);
|
|
||||||
} catch(e) {
|
|
||||||
logger.error(`Failed to rename file ${output_json['_filename']} to its appropriate extension.`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (type === 'audio') {
|
|
||||||
let tags = {
|
|
||||||
title: output_json['title'],
|
|
||||||
artist: output_json['artist'] ? output_json['artist'] : output_json['uploader']
|
|
||||||
}
|
|
||||||
let success = NodeID3.write(tags, utils.removeFileExtension(output_json['_filename']) + '.mp3');
|
|
||||||
if (!success) logger.error('Failed to apply ID3 tag to audio file ' + output_json['_filename']);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (config_api.getConfigItem('ytdl_generate_nfo_files')) {
|
|
||||||
exports.generateNFOFile(output_json, `${filepath_no_extension}.nfo`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.cropFileSettings) {
|
|
||||||
await utils.cropFile(full_file_path, options.cropFileSettings.cropFileStart, options.cropFileSettings.cropFileEnd, ext);
|
|
||||||
}
|
|
||||||
|
|
||||||
// registers file in DB
|
|
||||||
const file_obj = await files_api.registerFileDB(full_file_path, type, download['user_uid'], category, download['sub_id'] ? download['sub_id'] : null, options.cropFileSettings);
|
|
||||||
|
|
||||||
await archive_api.addToArchive(output_json['extractor'], output_json['id'], type, output_json['title'], download['user_uid'], download['sub_id']);
|
|
||||||
|
|
||||||
notifications_api.sendDownloadNotification(file_obj, download['user_uid']);
|
|
||||||
|
|
||||||
file_objs.push(file_obj);
|
|
||||||
}
|
|
||||||
|
|
||||||
let container = null;
|
|
||||||
|
|
||||||
if (file_objs.length > 1) {
|
|
||||||
// create playlist
|
|
||||||
const playlist_name = file_objs.map(file_obj => file_obj.title).join(', ');
|
|
||||||
container = await files_api.createPlaylist(playlist_name, file_objs.map(file_obj => file_obj.uid), download['user_uid']);
|
|
||||||
} else if (file_objs.length === 1) {
|
|
||||||
container = file_objs[0];
|
|
||||||
} else {
|
|
||||||
const error_message = 'Downloaded file failed to result in metadata object.';
|
|
||||||
logger.error(error_message);
|
|
||||||
await handleDownloadError(download, error_message, 'no_metadata');
|
|
||||||
}
|
|
||||||
|
|
||||||
const file_uids = file_objs.map(file_obj => file_obj.uid);
|
|
||||||
await db_api.updateRecord('download_queue', {uid: download_uid}, {finished_step: true, finished: true, running: false, step_index: 3, percent_complete: 100, file_uids: file_uids, container: container});
|
|
||||||
resolve();
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
for (const output_json of parsed_output) {
|
||||||
|
if (!output_json) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// get filepath with no extension
|
||||||
|
const filepath_no_extension = utils.removeFileExtension(output_json['_filename']);
|
||||||
|
|
||||||
|
const ext = type === 'audio' ? '.mp3' : '.mp4';
|
||||||
|
var full_file_path = filepath_no_extension + ext;
|
||||||
|
var file_name = filepath_no_extension.substring(fileFolderPath.length, filepath_no_extension.length);
|
||||||
|
|
||||||
|
if (type === 'video' && url.includes('twitch.tv/videos/') && url.split('twitch.tv/videos/').length > 1
|
||||||
|
&& config_api.getConfigItem('ytdl_twitch_auto_download_chat')) {
|
||||||
|
let vodId = url.split('twitch.tv/videos/')[1];
|
||||||
|
vodId = vodId.split('?')[0];
|
||||||
|
twitch_api.downloadTwitchChatByVODID(vodId, file_name, type, download['user_uid']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// renames file if necessary due to bug
|
||||||
|
if (!fs.existsSync(output_json['_filename']) && fs.existsSync(output_json['_filename'] + '.webm')) {
|
||||||
|
try {
|
||||||
|
fs.renameSync(output_json['_filename'] + '.webm', output_json['_filename']);
|
||||||
|
logger.info('Renamed ' + file_name + '.webm to ' + file_name);
|
||||||
|
} catch(e) {
|
||||||
|
logger.error(`Failed to rename file ${output_json['_filename']} to its appropriate extension.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === 'audio') {
|
||||||
|
let tags = {
|
||||||
|
title: output_json['title'],
|
||||||
|
artist: output_json['artist'] ? output_json['artist'] : output_json['uploader']
|
||||||
|
}
|
||||||
|
let success = NodeID3.write(tags, utils.removeFileExtension(output_json['_filename']) + '.mp3');
|
||||||
|
if (!success) logger.error('Failed to apply ID3 tag to audio file ' + output_json['_filename']);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config_api.getConfigItem('ytdl_generate_nfo_files')) {
|
||||||
|
exports.generateNFOFile(output_json, `${filepath_no_extension}.nfo`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.cropFileSettings) {
|
||||||
|
await utils.cropFile(full_file_path, options.cropFileSettings.cropFileStart, options.cropFileSettings.cropFileEnd, ext);
|
||||||
|
}
|
||||||
|
|
||||||
|
// registers file in DB
|
||||||
|
const file_obj = await files_api.registerFileDB(full_file_path, type, download['user_uid'], category, download['sub_id'] ? download['sub_id'] : null, options.cropFileSettings);
|
||||||
|
|
||||||
|
await archive_api.addToArchive(output_json['extractor'], output_json['id'], type, output_json['title'], download['user_uid'], download['sub_id']);
|
||||||
|
|
||||||
|
notifications_api.sendDownloadNotification(file_obj, download['user_uid']);
|
||||||
|
|
||||||
|
file_objs.push(file_obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
let container = null;
|
||||||
|
|
||||||
|
if (file_objs.length > 1) {
|
||||||
|
// create playlist
|
||||||
|
container = await files_api.createPlaylist(download['title'], file_objs.map(file_obj => file_obj.uid), download['user_uid']);
|
||||||
|
} else if (file_objs.length === 1) {
|
||||||
|
container = file_objs[0];
|
||||||
|
} else {
|
||||||
|
const error_message = 'Downloaded file failed to result in metadata object.';
|
||||||
|
logger.error(error_message);
|
||||||
|
await handleDownloadError(download_uid, error_message, 'no_metadata');
|
||||||
|
}
|
||||||
|
|
||||||
|
const file_uids = file_objs.map(file_obj => file_obj.uid);
|
||||||
|
await db_api.updateRecord('download_queue', {uid: download_uid}, {finished_step: true, finished: true, running: false, step_index: 3, percent_complete: 100, file_uids: file_uids, container: container});
|
||||||
|
resolve(file_uids);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// helper functions
|
// helper functions
|
||||||
|
|
||||||
exports.generateArgs = async (url, type, options, user_uid = null, simulated = false) => {
|
exports.generateArgs = async (url, type, options, user_uid = null, simulated = false) => {
|
||||||
const default_downloader = utils.getCurrentDownloader() || config_api.getConfigItem('ytdl_default_downloader');
|
const default_downloader = config_api.getConfigItem('ytdl_default_downloader');
|
||||||
|
|
||||||
if (!simulated && (default_downloader === 'youtube-dl' || default_downloader === 'youtube-dlc')) {
|
if (!simulated && (default_downloader === 'youtube-dl' || default_downloader === 'youtube-dlc')) {
|
||||||
logger.warn('It is recommended you use yt-dlp! To prevent failed downloads, change the downloader in your settings menu to yt-dlp and restart your instance.')
|
logger.warn('It is recommended you use yt-dlp! To prevent failed downloads, change the downloader in your settings menu to yt-dlp and restart your instance.')
|
||||||
@@ -515,6 +521,8 @@ exports.generateArgs = async (url, type, options, user_uid = null, simulated = f
|
|||||||
downloadConfig.push('--write-thumbnail');
|
downloadConfig.push('--write-thumbnail');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
downloadConfig.push('-i');
|
||||||
|
|
||||||
if (globalArgs && globalArgs !== '') {
|
if (globalArgs && globalArgs !== '') {
|
||||||
// adds global args
|
// adds global args
|
||||||
if (downloadConfig.indexOf('-o') !== -1 && globalArgs.split(',,').indexOf('-o') !== -1) {
|
if (downloadConfig.indexOf('-o') !== -1 && globalArgs.split(',,').indexOf('-o') !== -1) {
|
||||||
@@ -551,58 +559,30 @@ exports.generateArgs = async (url, type, options, user_uid = null, simulated = f
|
|||||||
}
|
}
|
||||||
|
|
||||||
exports.getVideoInfoByURL = async (url, args = [], download_uid = null) => {
|
exports.getVideoInfoByURL = async (url, args = [], download_uid = null) => {
|
||||||
return new Promise(resolve => {
|
// remove bad args
|
||||||
// remove bad args
|
const temp_args = utils.filterArgs(args, ['--no-simulate']);
|
||||||
const temp_args = utils.filterArgs(args, ['--no-simulate']);
|
const new_args = [...temp_args];
|
||||||
const new_args = [...temp_args];
|
|
||||||
|
|
||||||
const archiveArgIndex = new_args.indexOf('--download-archive');
|
const archiveArgIndex = new_args.indexOf('--download-archive');
|
||||||
if (archiveArgIndex !== -1) {
|
if (archiveArgIndex !== -1) {
|
||||||
new_args.splice(archiveArgIndex, 2);
|
new_args.splice(archiveArgIndex, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
new_args.push('--dump-json');
|
||||||
|
|
||||||
|
let {callback} = await youtubedl_api.runYoutubeDL(url, new_args);
|
||||||
|
const {parsed_output, err} = await callback;
|
||||||
|
if (!parsed_output || parsed_output.length === 0) {
|
||||||
|
let error_message = `Error while retrieving info on video with URL ${url}`;
|
||||||
|
if (err.stderr) error_message += ` with the following message: \n${err.stderr}`;
|
||||||
|
logger.error(error_message);
|
||||||
|
if (download_uid) {
|
||||||
|
await handleDownloadError(download_uid, error_message, 'info_retrieve_failed');
|
||||||
}
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
new_args.push('--dump-json');
|
return parsed_output;
|
||||||
|
|
||||||
youtubedl.exec(url, new_args, {maxBuffer: Infinity}, async (err, output) => {
|
|
||||||
if (output) {
|
|
||||||
let outputs = [];
|
|
||||||
try {
|
|
||||||
for (let i = 0; i < output.length; i++) {
|
|
||||||
let output_json = null;
|
|
||||||
try {
|
|
||||||
output_json = JSON.parse(output[i]);
|
|
||||||
} catch(e) {
|
|
||||||
output_json = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!output_json) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
outputs.push(output_json);
|
|
||||||
}
|
|
||||||
resolve(outputs.length === 1 ? outputs[0] : outputs);
|
|
||||||
} catch(e) {
|
|
||||||
const error = `Error while retrieving info on video with URL ${url} with the following message: output JSON could not be parsed. Output JSON: ${output}`;
|
|
||||||
logger.error(error);
|
|
||||||
if (download_uid) {
|
|
||||||
const download = await db_api.getRecord('download_queue', {uid: download_uid});
|
|
||||||
await handleDownloadError(download, error, 'parse_failed');
|
|
||||||
}
|
|
||||||
resolve(null);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
let error_message = `Error while retrieving info on video with URL ${url} with the following message: ${err}`;
|
|
||||||
if (err.stderr) error_message += `\n\n${err.stderr}`;
|
|
||||||
logger.error(error_message);
|
|
||||||
if (download_uid) {
|
|
||||||
const download = await db_api.getRecord('download_queue', {uid: download_uid});
|
|
||||||
await handleDownloadError(download, error_message, 'info_retrieve_failed');
|
|
||||||
}
|
|
||||||
resolve(null);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function filterArgs(args, isAudio) {
|
function filterArgs(args, isAudio) {
|
||||||
@@ -621,6 +601,7 @@ async function checkDownloadPercent(download_uid) {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
const download = await db_api.getRecord('download_queue', {uid: download_uid});
|
const download = await db_api.getRecord('download_queue', {uid: download_uid});
|
||||||
|
if (!download) return;
|
||||||
const files_to_check_for_progress = download['files_to_check_for_progress'];
|
const files_to_check_for_progress = download['files_to_check_for_progress'];
|
||||||
const resulting_file_size = download['expected_file_size'];
|
const resulting_file_size = download['expected_file_size'];
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +1,7 @@
|
|||||||
#!/bin/sh
|
#!/bin/bash
|
||||||
set -eu
|
set -eu
|
||||||
|
|
||||||
CMD="npm start"
|
|
||||||
|
|
||||||
# if the first arg starts with "-" pass it to program
|
|
||||||
if [ "${1#-}" != "$1" ]; then
|
|
||||||
set -- "$CMD" "$@"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# chown current working directory to current user
|
# chown current working directory to current user
|
||||||
if [ "$*" = "$CMD" ] && [ "$(id -u)" = "0" ]; then
|
echo "[entrypoint] setup permission, this may take a while"
|
||||||
find . \! -user "$UID" -exec chown "$UID:$GID" '{}' + || echo "WARNING! Could not change directory ownership. If you manage permissions externally this is fine, otherwise you may experience issues when downloading or deleting videos."
|
find . \! -user "$UID" -exec chown "$UID:$GID" '{}' + || echo "WARNING! Could not change directory ownership. If you manage permissions externally this is fine, otherwise you may experience issues when downloading or deleting videos."
|
||||||
exec gosu "$UID:$GID" "$0" "$@"
|
exec gosu "$UID:$GID" "$@"
|
||||||
fi
|
|
||||||
|
|
||||||
exec "$@"
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
const fs = require('fs-extra')
|
const fs = require('fs-extra')
|
||||||
const path = require('path')
|
const path = require('path')
|
||||||
const { uuid } = require('uuidv4');
|
const { v4: uuid } = require('uuid');
|
||||||
|
|
||||||
const config_api = require('./config');
|
const config_api = require('./config');
|
||||||
const db_api = require('./db');
|
const db_api = require('./db');
|
||||||
|
|||||||
@@ -4,11 +4,12 @@ const logger = require('./logger');
|
|||||||
const utils = require('./utils');
|
const utils = require('./utils');
|
||||||
const consts = require('./consts');
|
const consts = require('./consts');
|
||||||
|
|
||||||
const { uuid } = require('uuidv4');
|
const { v4: uuid } = require('uuid');
|
||||||
|
|
||||||
const fetch = require('node-fetch');
|
const fetch = require('node-fetch');
|
||||||
const { gotify } = require("gotify");
|
const { gotify } = require("gotify");
|
||||||
const TelegramBot = require('node-telegram-bot-api');
|
const TelegramBotAPI = require('node-telegram-bot-api');
|
||||||
|
let telegram_bot = null;
|
||||||
const REST = require('@discordjs/rest').REST;
|
const REST = require('@discordjs/rest').REST;
|
||||||
const API = require('@discordjs/core').API;
|
const API = require('@discordjs/core').API;
|
||||||
const EmbedBuilder = require('@discordjs/builders').EmbedBuilder;
|
const EmbedBuilder = require('@discordjs/builders').EmbedBuilder;
|
||||||
@@ -56,7 +57,7 @@ exports.sendNotification = async (notification) => {
|
|||||||
sendGotifyNotification(data);
|
sendGotifyNotification(data);
|
||||||
}
|
}
|
||||||
if (config_api.getConfigItem('ytdl_use_telegram_API') && config_api.getConfigItem('ytdl_telegram_bot_token') && config_api.getConfigItem('ytdl_telegram_chat_id')) {
|
if (config_api.getConfigItem('ytdl_use_telegram_API') && config_api.getConfigItem('ytdl_telegram_bot_token') && config_api.getConfigItem('ytdl_telegram_chat_id')) {
|
||||||
sendTelegramNotification(data);
|
exports.sendTelegramNotification(data);
|
||||||
}
|
}
|
||||||
if (config_api.getConfigItem('ytdl_webhook_url')) {
|
if (config_api.getConfigItem('ytdl_webhook_url')) {
|
||||||
sendGenericNotification(data);
|
sendGenericNotification(data);
|
||||||
@@ -113,6 +114,8 @@ function notificationEnabled(type) {
|
|||||||
return config_api.getConfigItem('ytdl_enable_notifications') && (config_api.getConfigItem('ytdl_enable_all_notifications') || config_api.getConfigItem('ytdl_allowed_notification_types').includes(type));
|
return config_api.getConfigItem('ytdl_enable_notifications') && (config_api.getConfigItem('ytdl_enable_all_notifications') || config_api.getConfigItem('ytdl_allowed_notification_types').includes(type));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ntfy
|
||||||
|
|
||||||
function sendNtfyNotification({body, title, type, url, thumbnail}) {
|
function sendNtfyNotification({body, title, type, url, thumbnail}) {
|
||||||
logger.verbose('Sending notification to ntfy');
|
logger.verbose('Sending notification to ntfy');
|
||||||
fetch(config_api.getConfigItem('ytdl_ntfy_topic_url'), {
|
fetch(config_api.getConfigItem('ytdl_ntfy_topic_url'), {
|
||||||
@@ -127,6 +130,8 @@ function sendNtfyNotification({body, title, type, url, thumbnail}) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Gotify
|
||||||
|
|
||||||
async function sendGotifyNotification({body, title, type, url, thumbnail}) {
|
async function sendGotifyNotification({body, title, type, url, thumbnail}) {
|
||||||
logger.verbose('Sending notification to gotify');
|
logger.verbose('Sending notification to gotify');
|
||||||
await gotify({
|
await gotify({
|
||||||
@@ -145,15 +150,50 @@ async function sendGotifyNotification({body, title, type, url, thumbnail}) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function sendTelegramNotification({body, title, type, url, thumbnail}) {
|
// Telegram
|
||||||
logger.verbose('Sending notification to Telegram');
|
|
||||||
|
setupTelegramBot();
|
||||||
|
config_api.config_updated.subscribe(change => {
|
||||||
|
const use_telegram_api = config_api.getConfigItem('ytdl_use_telegram_API');
|
||||||
const bot_token = config_api.getConfigItem('ytdl_telegram_bot_token');
|
const bot_token = config_api.getConfigItem('ytdl_telegram_bot_token');
|
||||||
const chat_id = config_api.getConfigItem('ytdl_telegram_chat_id');
|
if (!use_telegram_api || !bot_token) return;
|
||||||
const bot = new TelegramBot(bot_token);
|
if (!change) return;
|
||||||
if (thumbnail) await bot.sendPhoto(chat_id, thumbnail);
|
if (change['key'] === 'ytdl_use_telegram_API' || change['key'] === 'ytdl_telegram_bot_token' || change['key'] === 'ytdl_telegram_webhook_proxy') {
|
||||||
bot.sendMessage(chat_id, `<b>${title}</b>\n\n${body}\n<a href="${url}">${url}</a>`, {parse_mode: 'HTML'});
|
logger.debug('Telegram bot setting up');
|
||||||
|
setupTelegramBot();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
async function setupTelegramBot() {
|
||||||
|
const use_telegram_api = config_api.getConfigItem('ytdl_use_telegram_API');
|
||||||
|
const bot_token = config_api.getConfigItem('ytdl_telegram_bot_token');
|
||||||
|
if (!use_telegram_api || !bot_token) return;
|
||||||
|
|
||||||
|
telegram_bot = new TelegramBotAPI(bot_token);
|
||||||
|
const webhook_proxy = config_api.getConfigItem('ytdl_telegram_webhook_proxy');
|
||||||
|
const webhook_url = webhook_proxy ? webhook_proxy : `${utils.getBaseURL()}/api/telegramRequest`;
|
||||||
|
telegram_bot.setWebHook(webhook_url);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
exports.sendTelegramNotification = async ({body, title, type, url, thumbnail}) => {
|
||||||
|
if (!telegram_bot){
|
||||||
|
logger.error('Telegram bot not found!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const chat_id = config_api.getConfigItem('ytdl_telegram_chat_id');
|
||||||
|
if (!chat_id){
|
||||||
|
logger.error('Telegram chat ID required!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.verbose('Sending notification to Telegram');
|
||||||
|
if (thumbnail) await telegram_bot.sendPhoto(chat_id, thumbnail);
|
||||||
|
telegram_bot.sendMessage(chat_id, `<b>${title}</b>\n\n${body}\n<a href="${url}">${url}</a>`, {parse_mode: 'HTML'});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Discord
|
||||||
|
|
||||||
async function sendDiscordNotification({body, title, type, url, thumbnail}) {
|
async function sendDiscordNotification({body, title, type, url, thumbnail}) {
|
||||||
const discord_webhook_url = config_api.getConfigItem('ytdl_discord_webhook_url');
|
const discord_webhook_url = config_api.getConfigItem('ytdl_discord_webhook_url');
|
||||||
const url_split = discord_webhook_url.split('webhooks/');
|
const url_split = discord_webhook_url.split('webhooks/');
|
||||||
@@ -177,6 +217,8 @@ async function sendDiscordNotification({body, title, type, url, thumbnail}) {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Slack
|
||||||
|
|
||||||
function sendSlackNotification({body, title, type, url, thumbnail}) {
|
function sendSlackNotification({body, title, type, url, thumbnail}) {
|
||||||
const slack_webhook_url = config_api.getConfigItem('ytdl_slack_webhook_url');
|
const slack_webhook_url = config_api.getConfigItem('ytdl_slack_webhook_url');
|
||||||
logger.verbose(`Sending slack notification to ${slack_webhook_url}`);
|
logger.verbose(`Sending slack notification to ${slack_webhook_url}`);
|
||||||
@@ -236,6 +278,8 @@ function sendSlackNotification({body, title, type, url, thumbnail}) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Generic
|
||||||
|
|
||||||
function sendGenericNotification(data) {
|
function sendGenericNotification(data) {
|
||||||
const webhook_url = config_api.getConfigItem('ytdl_webhook_url');
|
const webhook_url = config_api.getConfigItem('ytdl_webhook_url');
|
||||||
logger.verbose(`Sending generic notification to ${webhook_url}`);
|
logger.verbose(`Sending generic notification to ${webhook_url}`);
|
||||||
|
|||||||
4768
backend/package-lock.json
generated
4768
backend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -4,7 +4,7 @@
|
|||||||
"description": "backend for YoutubeDL-Material",
|
"description": "backend for YoutubeDL-Material",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "echo \"Error: no test specified\" && exit 1",
|
"test": "mocha test --exit -s 1000",
|
||||||
"start": "pm2-runtime --raw pm2.config.js",
|
"start": "pm2-runtime --raw pm2.config.js",
|
||||||
"debug": "set YTDL_MODE=debug && node app.js"
|
"debug": "set YTDL_MODE=debug && node app.js"
|
||||||
},
|
},
|
||||||
@@ -30,8 +30,10 @@
|
|||||||
"async-mutex": "^0.4.0",
|
"async-mutex": "^0.4.0",
|
||||||
"axios": "^0.21.2",
|
"axios": "^0.21.2",
|
||||||
"bcryptjs": "^2.4.0",
|
"bcryptjs": "^2.4.0",
|
||||||
|
"command-exists": "^1.2.9",
|
||||||
"compression": "^1.7.4",
|
"compression": "^1.7.4",
|
||||||
"config": "^3.2.3",
|
"config": "^3.2.3",
|
||||||
|
"execa": "^5.1.1",
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"express-session": "^1.17.3",
|
"express-session": "^1.17.3",
|
||||||
"feed": "^4.2.2",
|
"feed": "^4.2.2",
|
||||||
@@ -42,7 +44,6 @@
|
|||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"lowdb": "^1.0.0",
|
"lowdb": "^1.0.0",
|
||||||
"md5": "^2.2.1",
|
"md5": "^2.2.1",
|
||||||
"mocha": "^9.2.2",
|
|
||||||
"moment": "^2.29.4",
|
"moment": "^2.29.4",
|
||||||
"mongodb": "^3.6.9",
|
"mongodb": "^3.6.9",
|
||||||
"multer": "1.4.5-lts.1",
|
"multer": "1.4.5-lts.1",
|
||||||
@@ -60,10 +61,13 @@
|
|||||||
"read-last-lines": "^1.7.2",
|
"read-last-lines": "^1.7.2",
|
||||||
"rxjs": "^7.3.0",
|
"rxjs": "^7.3.0",
|
||||||
"shortid": "^2.2.15",
|
"shortid": "^2.2.15",
|
||||||
|
"tree-kill": "^1.2.2",
|
||||||
"unzipper": "^0.10.10",
|
"unzipper": "^0.10.10",
|
||||||
"uuidv4": "^6.2.13",
|
"uuid": "^9.0.1",
|
||||||
"winston": "^3.7.2",
|
"winston": "^3.7.2",
|
||||||
"xmlbuilder2": "^3.0.2",
|
"xmlbuilder2": "^3.0.2"
|
||||||
"youtube-dl": "^3.0.2"
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"mocha": "^10.2.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,25 +1,26 @@
|
|||||||
const fs = require('fs-extra');
|
const fs = require('fs-extra');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const youtubedl = require('youtube-dl');
|
|
||||||
|
|
||||||
|
const youtubedl_api = require('./youtube-dl');
|
||||||
const config_api = require('./config');
|
const config_api = require('./config');
|
||||||
const archive_api = require('./archive');
|
const archive_api = require('./archive');
|
||||||
const utils = require('./utils');
|
const utils = require('./utils');
|
||||||
const logger = require('./logger');
|
const logger = require('./logger');
|
||||||
|
const CONSTS = require('./consts');
|
||||||
|
|
||||||
const debugMode = process.env.YTDL_MODE === 'debug';
|
const debugMode = process.env.YTDL_MODE === 'debug';
|
||||||
|
|
||||||
const db_api = require('./db');
|
const db_api = require('./db');
|
||||||
const downloader_api = require('./downloader');
|
const downloader_api = require('./downloader');
|
||||||
|
|
||||||
async function subscribe(sub, user_uid = null) {
|
exports.subscribe = async (sub, user_uid = null, skip_get_info = false) => {
|
||||||
const result_obj = {
|
const result_obj = {
|
||||||
success: false,
|
success: false,
|
||||||
error: ''
|
error: ''
|
||||||
};
|
};
|
||||||
return new Promise(async resolve => {
|
return new Promise(async resolve => {
|
||||||
// sub should just have url and name. here we will get isPlaylist and path
|
// sub should just have url and name. here we will get isPlaylist and path
|
||||||
sub.isPlaylist = sub.url.includes('playlist');
|
sub.isPlaylist = sub.isPlaylist || sub.url.includes('playlist');
|
||||||
sub.videos = [];
|
sub.videos = [];
|
||||||
|
|
||||||
let url_exists = !!(await db_api.getRecord('subscriptions', {url: sub.url, user_uid: user_uid}));
|
let url_exists = !!(await db_api.getRecord('subscriptions', {url: sub.url, user_uid: user_uid}));
|
||||||
@@ -32,12 +33,13 @@ async function subscribe(sub, user_uid = null) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
sub['user_uid'] = user_uid ? user_uid : undefined;
|
sub['user_uid'] = user_uid ? user_uid : undefined;
|
||||||
await db_api.insertRecordIntoTable('subscriptions', sub);
|
await db_api.insertRecordIntoTable('subscriptions', JSON.parse(JSON.stringify(sub)));
|
||||||
|
|
||||||
let success = await getSubscriptionInfo(sub);
|
let success = skip_get_info ? true : await getSubscriptionInfo(sub);
|
||||||
|
exports.writeSubscriptionMetadata(sub);
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
getVideosForSub(sub, user_uid);
|
if (!sub.paused) exports.getVideosForSub(sub.id);
|
||||||
} else {
|
} else {
|
||||||
logger.error('Subscribe: Failed to get subscription info. Subscribe failed.')
|
logger.error('Subscribe: Failed to get subscription info. Subscribe failed.')
|
||||||
}
|
}
|
||||||
@@ -61,55 +63,41 @@ async function getSubscriptionInfo(sub) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Promise(async resolve => {
|
let {callback} = await youtubedl_api.runYoutubeDL(sub.url, downloadConfig);
|
||||||
youtubedl.exec(sub.url, downloadConfig, {maxBuffer: Infinity}, async (err, output) => {
|
const {parsed_output, err} = await callback;
|
||||||
if (debugMode) {
|
if (err) {
|
||||||
logger.info('Subscribe: got info for subscription ' + sub.id);
|
logger.error(err.stderr);
|
||||||
}
|
return false;
|
||||||
if (err) {
|
}
|
||||||
logger.error(err.stderr);
|
logger.verbose('Subscribe: got info for subscription ' + sub.id);
|
||||||
resolve(false);
|
for (const output_json of parsed_output) {
|
||||||
} else if (output) {
|
if (!output_json) {
|
||||||
if (output.length === 0 || (output.length === 1 && output[0] === '')) {
|
continue;
|
||||||
logger.verbose('Could not get info for ' + sub.id);
|
}
|
||||||
resolve(false);
|
|
||||||
}
|
|
||||||
for (let i = 0; i < output.length; i++) {
|
|
||||||
let output_json = null;
|
|
||||||
try {
|
|
||||||
output_json = JSON.parse(output[i]);
|
|
||||||
} catch(e) {
|
|
||||||
output_json = null;
|
|
||||||
}
|
|
||||||
if (!output_json) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (!sub.name) {
|
|
||||||
if (sub.isPlaylist) {
|
|
||||||
sub.name = output_json.playlist_title ? output_json.playlist_title : output_json.playlist;
|
|
||||||
} else {
|
|
||||||
sub.name = output_json.uploader;
|
|
||||||
}
|
|
||||||
// if it's now valid, update
|
|
||||||
if (sub.name) {
|
|
||||||
let sub_name = sub.name;
|
|
||||||
const sub_name_exists = await db_api.getRecord('subscriptions', {name: sub.name, isPlaylist: sub.isPlaylist, user_uid: sub.user_uid});
|
|
||||||
if (sub_name_exists) sub_name += ` - ${sub.id}`;
|
|
||||||
await db_api.updateRecord('subscriptions', {id: sub.id}, {name: sub_name});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: get even more info
|
if (!sub.name) {
|
||||||
|
if (sub.isPlaylist) {
|
||||||
resolve(true);
|
sub.name = output_json.playlist_title ? output_json.playlist_title : output_json.playlist;
|
||||||
}
|
} else {
|
||||||
resolve(false);
|
sub.name = output_json.uploader;
|
||||||
}
|
}
|
||||||
});
|
// if it's now valid, update
|
||||||
});
|
if (sub.name) {
|
||||||
|
let sub_name = sub.name;
|
||||||
|
const sub_name_exists = await db_api.getRecord('subscriptions', {name: sub.name, isPlaylist: sub.isPlaylist, user_uid: sub.user_uid});
|
||||||
|
if (sub_name_exists) sub_name += ` - ${sub.id}`;
|
||||||
|
await db_api.updateRecord('subscriptions', {id: sub.id}, {name: sub_name});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function unsubscribe(sub, deleteMode, user_uid = null) {
|
exports.unsubscribe = async (sub_id, deleteMode, user_uid = null) => {
|
||||||
|
const sub = await exports.getSubscription(sub_id);
|
||||||
let basePath = null;
|
let basePath = null;
|
||||||
if (user_uid)
|
if (user_uid)
|
||||||
basePath = path.join(config_api.getConfigItem('ytdl_users_base_path'), user_uid, 'subscriptions');
|
basePath = path.join(config_api.getConfigItem('ytdl_users_base_path'), user_uid, 'subscriptions');
|
||||||
@@ -132,6 +120,7 @@ async function unsubscribe(sub, deleteMode, user_uid = null) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await killSubDownloads(sub_id, true);
|
||||||
await db_api.removeRecord('subscriptions', {id: id});
|
await db_api.removeRecord('subscriptions', {id: id});
|
||||||
await db_api.removeAllRecords('files', {sub_id: id});
|
await db_api.removeAllRecords('files', {sub_id: id});
|
||||||
|
|
||||||
@@ -148,7 +137,7 @@ async function unsubscribe(sub, deleteMode, user_uid = null) {
|
|||||||
await db_api.removeAllRecords('archives', {sub_id: sub.id});
|
await db_api.removeAllRecords('archives', {sub_id: sub.id});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deleteSubscriptionFile(sub, file, deleteForever, file_uid = null, user_uid = null) {
|
exports.deleteSubscriptionFile = async (sub, file, deleteForever, file_uid = null, user_uid = null) => {
|
||||||
if (typeof sub === 'string') {
|
if (typeof sub === 'string') {
|
||||||
// TODO: fix bad workaround where sub is a sub_id
|
// TODO: fix bad workaround where sub is a sub_id
|
||||||
sub = await db_api.getRecord('subscriptions', {sub_id: sub});
|
sub = await db_api.getRecord('subscriptions', {sub_id: sub});
|
||||||
@@ -216,12 +205,76 @@ async function deleteSubscriptionFile(sub, file, deleteForever, file_uid = null,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getVideosForSub(sub, user_uid = null) {
|
let current_sub_index = 0; // To keep track of the current subscription
|
||||||
const latest_sub_obj = await getSubscription(sub.id);
|
exports.watchSubscriptionsInterval = async () => {
|
||||||
if (!latest_sub_obj || latest_sub_obj['downloading']) {
|
const subscriptions_check_interval = config_api.getConfigItem('ytdl_subscriptions_check_interval');
|
||||||
|
let parent_interval = setInterval(() => watchSubscriptions(), subscriptions_check_interval*1000);
|
||||||
|
watchSubscriptions();
|
||||||
|
config_api.config_updated.subscribe(change => {
|
||||||
|
if (!change) return;
|
||||||
|
if (change['key'] === 'ytdl_subscriptions_check_interval' || change['key'] === 'ytdl_multi_user_mode') {
|
||||||
|
current_sub_index = 0; // TODO: start after the last sub check
|
||||||
|
logger.verbose('Resetting sub check schedule due to config change');
|
||||||
|
clearInterval(parent_interval);
|
||||||
|
const new_interval = config_api.getConfigItem('ytdl_subscriptions_check_interval');
|
||||||
|
parent_interval = setInterval(() => watchSubscriptions(), new_interval*1000);
|
||||||
|
watchSubscriptions();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function watchSubscriptions() {
|
||||||
|
const subscription_ids = await getValidSubscriptionsToCheck();
|
||||||
|
if (subscription_ids.length === 0) {
|
||||||
|
logger.info('Skipping subscription check as no valid subscriptions exist.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
checkSubscription(subscription_ids[current_sub_index]);
|
||||||
|
current_sub_index = (current_sub_index + 1) % subscription_ids.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkSubscription(sub_id) {
|
||||||
|
let sub = await exports.getSubscription(sub_id);
|
||||||
|
|
||||||
|
// don't check the sub if the last check for the same subscription has not completed
|
||||||
|
if (sub.downloading) {
|
||||||
|
logger.verbose(`Subscription: skipped checking ${sub.name} as it's downloading videos.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!sub.name) {
|
||||||
|
logger.verbose(`Subscription: skipped check for subscription with uid ${sub.id} as name has not been retrieved yet.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await exports.getVideosForSub(sub.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getValidSubscriptionsToCheck() {
|
||||||
|
const subscriptions = await exports.getAllSubscriptions();
|
||||||
|
|
||||||
|
if (!subscriptions) return;
|
||||||
|
|
||||||
|
// auto pause deprecated streamingOnly mode
|
||||||
|
const streaming_only_subs = subscriptions.filter(sub => sub.streamingOnly);
|
||||||
|
exports.updateSubscriptionPropertyMultiple(streaming_only_subs, {paused: true});
|
||||||
|
|
||||||
|
const valid_subscription_ids = subscriptions.filter(sub => !sub.paused && !sub.streamingOnly).map(sub => sub.id);
|
||||||
|
return valid_subscription_ids;
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.getVideosForSub = async (sub_id) => {
|
||||||
|
const sub = await exports.getSubscription(sub_id);
|
||||||
|
if (!sub || sub['downloading']) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_getVideosForSub(sub);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function _getVideosForSub(sub) {
|
||||||
|
const user_uid = sub['user_uid'];
|
||||||
updateSubscriptionProperty(sub, {downloading: true}, user_uid);
|
updateSubscriptionProperty(sub, {downloading: true}, user_uid);
|
||||||
|
|
||||||
// get basePath
|
// get basePath
|
||||||
@@ -239,84 +292,52 @@ async function getVideosForSub(sub, user_uid = null) {
|
|||||||
// get videos
|
// get videos
|
||||||
logger.verbose(`Subscription: getting list of videos to download for ${sub.name} with args: ${downloadConfig.join(',')}`);
|
logger.verbose(`Subscription: getting list of videos to download for ${sub.name} with args: ${downloadConfig.join(',')}`);
|
||||||
|
|
||||||
return new Promise(async resolve => {
|
let {child_process, callback} = await youtubedl_api.runYoutubeDL(sub.url, downloadConfig);
|
||||||
youtubedl.exec(sub.url, downloadConfig, {maxBuffer: Infinity}, async function(err, output) {
|
updateSubscriptionProperty(sub, {child_process: child_process}, user_uid);
|
||||||
// cleanup
|
const {parsed_output, err} = await callback;
|
||||||
updateSubscriptionProperty(sub, {downloading: false}, user_uid);
|
updateSubscriptionProperty(sub, {downloading: false, child_process: null}, user_uid);
|
||||||
|
if (!parsed_output) {
|
||||||
|
logger.error('Subscription check failed!');
|
||||||
|
if (err) logger.error(err);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
// remove temporary archive file if it exists
|
// remove temporary archive file if it exists
|
||||||
const archive_path = path.join(appendedBasePath, 'archive.txt');
|
const archive_path = path.join(appendedBasePath, 'archive.txt');
|
||||||
const archive_exists = await fs.pathExists(archive_path);
|
const archive_exists = await fs.pathExists(archive_path);
|
||||||
if (archive_exists) {
|
if (archive_exists) {
|
||||||
await fs.unlink(archive_path);
|
await fs.unlink(archive_path);
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.verbose('Subscription: finished check for ' + sub.name);
|
logger.verbose('Subscription: finished check for ' + sub.name);
|
||||||
if (err && !output) {
|
const files_to_download = await handleOutputJSON(parsed_output, sub, user_uid);
|
||||||
logger.error(err.stderr ? err.stderr : err.message);
|
return files_to_download;
|
||||||
if (err.stderr.includes('This video is unavailable') || err.stderr.includes('Private video')) {
|
|
||||||
logger.info('An error was encountered with at least one video, backup method will be used.')
|
|
||||||
try {
|
|
||||||
const outputs = err.stdout.split(/\r\n|\r|\n/);
|
|
||||||
const files_to_download = await handleOutputJSON(outputs, sub, user_uid);
|
|
||||||
resolve(files_to_download);
|
|
||||||
} catch(e) {
|
|
||||||
logger.error('Backup method failed. See error below:');
|
|
||||||
logger.error(e);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
logger.error('Subscription check failed!');
|
|
||||||
}
|
|
||||||
resolve(false);
|
|
||||||
} else if (output) {
|
|
||||||
const files_to_download = await handleOutputJSON(output, sub, user_uid);
|
|
||||||
resolve(files_to_download);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}, err => {
|
|
||||||
logger.error(err);
|
|
||||||
updateSubscriptionProperty(sub, {downloading: false}, user_uid);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleOutputJSON(output, sub, user_uid) {
|
async function handleOutputJSON(output_jsons, sub, user_uid) {
|
||||||
if (config_api.getConfigItem('ytdl_subscriptions_redownload_fresh_uploads')) {
|
if (config_api.getConfigItem('ytdl_subscriptions_redownload_fresh_uploads')) {
|
||||||
await setFreshUploads(sub, user_uid);
|
await setFreshUploads(sub, user_uid);
|
||||||
checkVideosForFreshUploads(sub, user_uid);
|
checkVideosForFreshUploads(sub, user_uid);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (output.length === 0 || (output.length === 1 && output[0] === '')) {
|
if (output_jsons.length === 0 || (output_jsons.length === 1 && output_jsons[0] === '')) {
|
||||||
logger.verbose('No additional videos to download for ' + sub.name);
|
logger.verbose('No additional videos to download for ' + sub.name);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const output_jsons = [];
|
|
||||||
for (let i = 0; i < output.length; i++) {
|
|
||||||
let output_json = null;
|
|
||||||
try {
|
|
||||||
output_json = JSON.parse(output[i]);
|
|
||||||
output_jsons.push(output_json);
|
|
||||||
} catch(e) {
|
|
||||||
output_json = null;
|
|
||||||
}
|
|
||||||
if (!output_json) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const files_to_download = await getFilesToDownload(sub, output_jsons);
|
const files_to_download = await getFilesToDownload(sub, output_jsons);
|
||||||
const base_download_options = generateOptionsForSubscriptionDownload(sub, user_uid);
|
const base_download_options = exports.generateOptionsForSubscriptionDownload(sub, user_uid);
|
||||||
|
|
||||||
for (let j = 0; j < files_to_download.length; j++) {
|
for (let j = 0; j < files_to_download.length; j++) {
|
||||||
const file_to_download = files_to_download[j];
|
const file_to_download = files_to_download[j];
|
||||||
file_to_download['formats'] = utils.stripPropertiesFromObject(file_to_download['formats'], ['format_id', 'filesize', 'filesize_approx']); // prevent download object from blowing up in size
|
file_to_download['formats'] = utils.stripPropertiesFromObject(file_to_download['formats'], ['format_id', 'filesize', 'filesize_approx']); // prevent download object from blowing up in size
|
||||||
await downloader_api.createDownload(file_to_download['webpage_url'], sub.type || 'video', base_download_options, user_uid, sub.id, sub.name, file_to_download);
|
await downloader_api.createDownload(file_to_download['webpage_url'], sub.type || 'video', base_download_options, user_uid, sub.id, sub.name, [file_to_download]);
|
||||||
}
|
}
|
||||||
|
|
||||||
return files_to_download;
|
return files_to_download;
|
||||||
}
|
}
|
||||||
|
|
||||||
function generateOptionsForSubscriptionDownload(sub, user_uid) {
|
exports.generateOptionsForSubscriptionDownload = (sub, user_uid) => {
|
||||||
let basePath = null;
|
let basePath = null;
|
||||||
if (user_uid)
|
if (user_uid)
|
||||||
basePath = path.join(config_api.getConfigItem('ytdl_users_base_path'), user_uid, 'subscriptions');
|
basePath = path.join(config_api.getConfigItem('ytdl_users_base_path'), user_uid, 'subscriptions');
|
||||||
@@ -371,10 +392,13 @@ async function generateArgsForSubscription(sub, user_uid, redownload = false, de
|
|||||||
|
|
||||||
// skip videos that are in the archive. otherwise sub download can be permanently slow (vs. just the first time)
|
// skip videos that are in the archive. otherwise sub download can be permanently slow (vs. just the first time)
|
||||||
const archive_text = await archive_api.generateArchive(sub.type, sub.user_uid, sub.id);
|
const archive_text = await archive_api.generateArchive(sub.type, sub.user_uid, sub.id);
|
||||||
logger.verbose(`Generating temporary archive file for subscription ${sub.name} with ${archive_text.split('\n').length - 1} entries.`)
|
const archive_count = archive_text.split('\n').length - 1;
|
||||||
const archive_path = path.join(appendedBasePath, 'archive.txt');
|
if (archive_count > 0) {
|
||||||
await fs.writeFile(archive_path, archive_text);
|
logger.verbose(`Generating temporary archive file for subscription ${sub.name} with ${archive_count} entries.`)
|
||||||
downloadConfig.push('--download-archive', archive_path);
|
const archive_path = path.join(appendedBasePath, 'archive.txt');
|
||||||
|
await fs.writeFile(archive_path, archive_text);
|
||||||
|
downloadConfig.push('--download-archive', archive_path);
|
||||||
|
}
|
||||||
|
|
||||||
if (sub.custom_args) {
|
if (sub.custom_args) {
|
||||||
const customArgsArray = sub.custom_args.split(',,');
|
const customArgsArray = sub.custom_args.split(',,');
|
||||||
@@ -408,7 +432,7 @@ async function generateArgsForSubscription(sub, user_uid, redownload = false, de
|
|||||||
downloadConfig.push('-r', rate_limit);
|
downloadConfig.push('-r', rate_limit);
|
||||||
}
|
}
|
||||||
|
|
||||||
const default_downloader = utils.getCurrentDownloader() || config_api.getConfigItem('ytdl_default_downloader');
|
const default_downloader = config_api.getConfigItem('ytdl_default_downloader');
|
||||||
if (default_downloader === 'yt-dlp') {
|
if (default_downloader === 'yt-dlp') {
|
||||||
downloadConfig.push('--no-clean-info-json');
|
downloadConfig.push('--no-clean-info-json');
|
||||||
}
|
}
|
||||||
@@ -439,36 +463,66 @@ async function getFilesToDownload(sub, output_jsons) {
|
|||||||
return files_to_download;
|
return files_to_download;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
exports.cancelCheckSubscription = async (sub_id) => {
|
||||||
|
const sub = await exports.getSubscription(sub_id);
|
||||||
|
if (!sub['downloading'] && !sub['child_process']) {
|
||||||
|
logger.error('Failed to cancel subscription check, verify that it is still running!');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
async function getSubscriptions(user_uid = null) {
|
// if check is ongoing
|
||||||
|
if (sub['child_process']) {
|
||||||
|
const child_process = sub['child_process'];
|
||||||
|
youtubedl_api.killYoutubeDLProcess(child_process);
|
||||||
|
}
|
||||||
|
|
||||||
|
// cancel activate video downloads
|
||||||
|
await killSubDownloads(sub_id);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function killSubDownloads(sub_id, remove_downloads = false) {
|
||||||
|
const sub_downloads = await db_api.getRecords('download_queue', {sub_id: sub_id});
|
||||||
|
for (const sub_download of sub_downloads) {
|
||||||
|
if (sub_download['running'])
|
||||||
|
await downloader_api.cancelDownload(sub_download['uid']);
|
||||||
|
if (remove_downloads)
|
||||||
|
await db_api.removeRecord('download_queue', {uid: sub_download['uid']});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.getSubscriptions = async (user_uid = null) => {
|
||||||
|
// TODO: fix issue where the downloading property may not match getSubscription()
|
||||||
return await db_api.getRecords('subscriptions', {user_uid: user_uid});
|
return await db_api.getRecords('subscriptions', {user_uid: user_uid});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getAllSubscriptions() {
|
exports.getAllSubscriptions = async () => {
|
||||||
const all_subs = await db_api.getRecords('subscriptions');
|
const all_subs = await db_api.getRecords('subscriptions');
|
||||||
const multiUserMode = config_api.getConfigItem('ytdl_multi_user_mode');
|
const multiUserMode = config_api.getConfigItem('ytdl_multi_user_mode');
|
||||||
return all_subs.filter(sub => !!(sub.user_uid) === !!multiUserMode);
|
return all_subs.filter(sub => !!(sub.user_uid) === !!multiUserMode);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getSubscription(subID) {
|
exports.getSubscription = async (subID) => {
|
||||||
// stringify and parse because we may override the 'downloading' property
|
// stringify and parse because we may override the 'downloading' property
|
||||||
const sub = JSON.parse(JSON.stringify(await db_api.getRecord('subscriptions', {id: subID})));
|
const sub = JSON.parse(JSON.stringify(await db_api.getRecord('subscriptions', {id: subID})));
|
||||||
// now with the download_queue, we may need to override 'downloading'
|
// now with the download_queue, we may need to override 'downloading'
|
||||||
const current_downloads = await db_api.getRecords('download_queue', {running: true, sub_id: sub.id}, true);
|
const current_downloads = await db_api.getRecords('download_queue', {running: true, sub_id: subID}, true);
|
||||||
if (!sub['downloading']) sub['downloading'] = current_downloads > 0;
|
if (!sub['downloading']) sub['downloading'] = current_downloads > 0;
|
||||||
return sub;
|
return sub;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getSubscriptionByName(subName, user_uid = null) {
|
exports.getSubscriptionByName = async (subName, user_uid = null) => {
|
||||||
return await db_api.getRecord('subscriptions', {name: subName, user_uid: user_uid});
|
return await db_api.getRecord('subscriptions', {name: subName, user_uid: user_uid});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateSubscription(sub) {
|
exports.updateSubscription = async (sub) => {
|
||||||
await db_api.updateRecord('subscriptions', {id: sub.id}, sub);
|
await db_api.updateRecord('subscriptions', {id: sub.id}, sub);
|
||||||
|
exports.writeSubscriptionMetadata(sub);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateSubscriptionPropertyMultiple(subs, assignment_obj) {
|
exports.updateSubscriptionPropertyMultiple = async (subs, assignment_obj) => {
|
||||||
subs.forEach(async sub => {
|
subs.forEach(async sub => {
|
||||||
await updateSubscriptionProperty(sub, assignment_obj);
|
await updateSubscriptionProperty(sub, assignment_obj);
|
||||||
});
|
});
|
||||||
@@ -480,6 +534,16 @@ async function updateSubscriptionProperty(sub, assignment_obj) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
exports.writeSubscriptionMetadata = (sub) => {
|
||||||
|
let basePath = sub.user_uid ? path.join(config_api.getConfigItem('ytdl_users_base_path'), sub.user_uid, 'subscriptions')
|
||||||
|
: config_api.getConfigItem('ytdl_subscriptions_base_path');
|
||||||
|
const appendedBasePath = getAppendedBasePath(sub, basePath);
|
||||||
|
const metadata_path = path.join(appendedBasePath, CONSTS.SUBSCRIPTION_BACKUP_PATH);
|
||||||
|
|
||||||
|
fs.ensureDirSync(appendedBasePath);
|
||||||
|
fs.writeJSONSync(metadata_path, sub);
|
||||||
|
}
|
||||||
|
|
||||||
async function setFreshUploads(sub) {
|
async function setFreshUploads(sub) {
|
||||||
const sub_files = await db_api.getRecords('files', {sub_id: sub.id});
|
const sub_files = await db_api.getRecords('files', {sub_id: sub.id});
|
||||||
if (!sub_files) return;
|
if (!sub_files) return;
|
||||||
@@ -508,24 +572,22 @@ async function checkVideoIfBetterExists(file_obj, sub, user_uid) {
|
|||||||
const downloadConfig = await generateArgsForSubscription(sub, user_uid, true, new_path);
|
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.`);
|
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
|
// simulate a download to verify that a better version exists
|
||||||
youtubedl.getInfo(file_obj['url'], downloadConfig, async (err, output) => {
|
|
||||||
if (err) {
|
const info = await downloader_api.getVideoInfoByURL(file_obj['url'], downloadConfig);
|
||||||
// video is not available anymore for whatever reason
|
if (info && info.length === 1) {
|
||||||
} else if (output) {
|
const metric_to_compare = sub.type === 'audio' ? 'abr' : 'height';
|
||||||
const metric_to_compare = sub.type === 'audio' ? 'abr' : 'height';
|
if (info[metric_to_compare] > file_obj[metric_to_compare]) {
|
||||||
if (output[metric_to_compare] > file_obj[metric_to_compare]) {
|
// download new video as the simulated one is better
|
||||||
// download new video as the simulated one is better
|
let {callback} = await youtubedl_api.runYoutubeDL(sub.url, downloadConfig);
|
||||||
youtubedl.exec(file_obj['url'], downloadConfig, {maxBuffer: Infinity}, async (err, output) => {
|
const {parsed_output, err} = await callback;
|
||||||
if (err) {
|
if (err) {
|
||||||
logger.verbose(`Failed to download better version of video ${file_obj['id']}`);
|
logger.verbose(`Failed to download better version of video ${file_obj['id']}`);
|
||||||
} else if (output) {
|
} else if (parsed_output) {
|
||||||
logger.verbose(`Successfully upgraded video ${file_obj['id']}'s ${metric_to_compare} from ${file_obj[metric_to_compare]} to ${output[metric_to_compare]}`);
|
logger.verbose(`Successfully upgraded video ${file_obj['id']}'s ${metric_to_compare} from ${file_obj[metric_to_compare]} to ${info[metric_to_compare]}`);
|
||||||
await db_api.setVideoProperty(file_obj['uid'], {[metric_to_compare]: output[metric_to_compare]});
|
await db_api.setVideoProperty(file_obj['uid'], {[metric_to_compare]: info[metric_to_compare]});
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
});
|
|
||||||
await db_api.setVideoProperty(file_obj['uid'], {'fresh_upload': false});
|
await db_api.setVideoProperty(file_obj['uid'], {'fresh_upload': false});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -534,17 +596,3 @@ async function checkVideoIfBetterExists(file_obj, sub, user_uid) {
|
|||||||
function getAppendedBasePath(sub, base_path) {
|
function getAppendedBasePath(sub, base_path) {
|
||||||
return path.join(base_path, (sub.isPlaylist ? 'playlists/' : 'channels/'), sub.name);
|
return path.join(base_path, (sub.isPlaylist ? 'playlists/' : 'channels/'), sub.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
getSubscription : getSubscription,
|
|
||||||
getSubscriptionByName : getSubscriptionByName,
|
|
||||||
getSubscriptions : getSubscriptions,
|
|
||||||
getAllSubscriptions : getAllSubscriptions,
|
|
||||||
updateSubscription : updateSubscription,
|
|
||||||
subscribe : subscribe,
|
|
||||||
unsubscribe : unsubscribe,
|
|
||||||
deleteSubscriptionFile : deleteSubscriptionFile,
|
|
||||||
getVideosForSub : getVideosForSub,
|
|
||||||
updateSubscriptionPropertyMultiple : updateSubscriptionPropertyMultiple,
|
|
||||||
generateOptionsForSubscriptionDownload: generateOptionsForSubscriptionDownload
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -3,9 +3,15 @@ const notifications_api = require('./notifications');
|
|||||||
const youtubedl_api = require('./youtube-dl');
|
const youtubedl_api = require('./youtube-dl');
|
||||||
const archive_api = require('./archive');
|
const archive_api = require('./archive');
|
||||||
const files_api = require('./files');
|
const files_api = require('./files');
|
||||||
|
const subscriptions_api = require('./subscriptions');
|
||||||
|
const config_api = require('./config');
|
||||||
|
const auth_api = require('./authentication/auth');
|
||||||
|
const utils = require('./utils');
|
||||||
|
const logger = require('./logger');
|
||||||
|
const CONSTS = require('./consts');
|
||||||
|
|
||||||
const fs = require('fs-extra');
|
const fs = require('fs-extra');
|
||||||
const logger = require('./logger');
|
const path = require('path');
|
||||||
const scheduler = require('node-schedule');
|
const scheduler = require('node-schedule');
|
||||||
|
|
||||||
const TASKS = {
|
const TASKS = {
|
||||||
@@ -47,6 +53,11 @@ const TASKS = {
|
|||||||
run: archive_api.importArchives,
|
run: archive_api.importArchives,
|
||||||
title: 'Import legacy archives',
|
title: 'Import legacy archives',
|
||||||
job: null
|
job: null
|
||||||
|
},
|
||||||
|
rebuild_database: {
|
||||||
|
run: rebuildDB,
|
||||||
|
title: 'Rebuild database',
|
||||||
|
job: null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -265,4 +276,68 @@ async function autoDeleteFiles(data) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function rebuildDB() {
|
||||||
|
await db_api.backupDB();
|
||||||
|
let subs_to_add = await guessSubscriptions(false);
|
||||||
|
subs_to_add = subs_to_add.concat(await guessSubscriptions(true));
|
||||||
|
const users_to_add = await guessUsers();
|
||||||
|
for (const user_to_add of users_to_add) {
|
||||||
|
const usersFileFolder = config_api.getConfigItem('ytdl_users_base_path');
|
||||||
|
|
||||||
|
const user_exists = await db_api.getRecord('users', {uid: user_to_add});
|
||||||
|
if (!user_exists) {
|
||||||
|
await auth_api.registerUser(user_to_add, user_to_add, 'password');
|
||||||
|
logger.info(`Regenerated user ${user_to_add}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const user_channel_subs = await guessSubscriptions(false, path.join(usersFileFolder, user_to_add), user_to_add);
|
||||||
|
const user_playlist_subs = await guessSubscriptions(true, path.join(usersFileFolder, user_to_add), user_to_add);
|
||||||
|
subs_to_add = subs_to_add.concat(user_channel_subs, user_playlist_subs);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const sub_to_add of subs_to_add) {
|
||||||
|
const sub_exists = !!(await subscriptions_api.getSubscriptionByName(sub_to_add['name'], sub_to_add['user_uid']));
|
||||||
|
// TODO: we shouldn't be creating this here
|
||||||
|
const new_sub = Object.assign({}, sub_to_add, {paused: true});
|
||||||
|
if (!sub_exists) {
|
||||||
|
await subscriptions_api.subscribe(new_sub, sub_to_add['user_uid'], true);
|
||||||
|
logger.info(`Regenerated subscription ${sub_to_add['name']}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(`Importing unregistered files`);
|
||||||
|
await files_api.importUnregisteredFiles();
|
||||||
|
}
|
||||||
|
|
||||||
|
const guessUsers = async () => {
|
||||||
|
const usersFileFolder = config_api.getConfigItem('ytdl_users_base_path');
|
||||||
|
const userPaths = await utils.getDirectoriesInDirectory(usersFileFolder);
|
||||||
|
return userPaths.map(userPath => path.basename(userPath));
|
||||||
|
}
|
||||||
|
|
||||||
|
const guessSubscriptions = async (isPlaylist, basePath = null) => {
|
||||||
|
const guessed_subs = [];
|
||||||
|
const subscriptionsFileFolder = config_api.getConfigItem('ytdl_subscriptions_base_path');
|
||||||
|
|
||||||
|
const subsSubPath = basePath ? path.join(basePath, 'subscriptions') : subscriptionsFileFolder;
|
||||||
|
const subsPath = path.join(subsSubPath, isPlaylist ? 'playlists' : 'channels');
|
||||||
|
|
||||||
|
const subs = await utils.getDirectoriesInDirectory(subsPath);
|
||||||
|
for (const subPath of subs) {
|
||||||
|
const sub_backup_path = path.join(subPath, CONSTS.SUBSCRIPTION_BACKUP_PATH);
|
||||||
|
if (!fs.existsSync(sub_backup_path)) continue;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const sub_backup = fs.readJSONSync(sub_backup_path)
|
||||||
|
delete sub_backup['_id'];
|
||||||
|
guessed_subs.push(sub_backup);
|
||||||
|
} catch(err) {
|
||||||
|
logger.warn(`Failed to reimport subscription in path ${subPath}`)
|
||||||
|
logger.warn(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return guessed_subs;
|
||||||
|
}
|
||||||
|
|
||||||
exports.TASKS = TASKS;
|
exports.TASKS = TASKS;
|
||||||
File diff suppressed because one or more lines are too long
1
backend/test/sample_mp3.info.json
Normal file
1
backend/test/sample_mp3.info.json
Normal file
File diff suppressed because one or more lines are too long
1
backend/test/sample_mp4.info.json
Normal file
1
backend/test/sample_mp4.info.json
Normal file
File diff suppressed because one or more lines are too long
@@ -3,7 +3,11 @@ const assert = require('assert');
|
|||||||
const low = require('lowdb')
|
const low = require('lowdb')
|
||||||
const winston = require('winston');
|
const winston = require('winston');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
const util = require('util');
|
||||||
|
const fs = require('fs-extra');
|
||||||
|
const { v4: uuid } = require('uuid');
|
||||||
|
const NodeID3 = require('node-id3');
|
||||||
|
const exec = util.promisify(require('child_process').exec);
|
||||||
|
|
||||||
const FileSync = require('lowdb/adapters/FileSync');
|
const FileSync = require('lowdb/adapters/FileSync');
|
||||||
|
|
||||||
@@ -41,11 +45,11 @@ const subscriptions_api = require('../subscriptions');
|
|||||||
const archive_api = require('../archive');
|
const archive_api = require('../archive');
|
||||||
const categories_api = require('../categories');
|
const categories_api = require('../categories');
|
||||||
const files_api = require('../files');
|
const files_api = require('../files');
|
||||||
const fs = require('fs-extra');
|
const youtubedl_api = require('../youtube-dl');
|
||||||
const { uuid } = require('uuidv4');
|
const config_api = require('../config');
|
||||||
const NodeID3 = require('node-id3');
|
const CONSTS = require('../consts');
|
||||||
|
|
||||||
db_api.initialize(db, users_db);
|
db_api.initialize(db, users_db, 'local_db_test.json');
|
||||||
|
|
||||||
const sample_video_json = {
|
const sample_video_json = {
|
||||||
id: "Sample Video",
|
id: "Sample Video",
|
||||||
@@ -68,9 +72,9 @@ const sample_video_json = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
describe('Database', async function() {
|
describe('Database', async function() {
|
||||||
describe('Import', async function() {
|
describe.skip('Import', async function() {
|
||||||
// it('Migrate', async function() {
|
// it('Migrate', async function() {
|
||||||
// await db_api.connectToDB();
|
// // await db_api.connectToDB();
|
||||||
// await db_api.removeAllRecords();
|
// await db_api.removeAllRecords();
|
||||||
// const success = await db_api.importJSONToDB(db.value(), users_db.value());
|
// const success = await db_api.importJSONToDB(db.value(), users_db.value());
|
||||||
// assert(success);
|
// assert(success);
|
||||||
@@ -86,7 +90,7 @@ describe('Database', async function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('Transfer to local', async function() {
|
it('Transfer to local', async function() {
|
||||||
await db_api.connectToDB();
|
// await db_api.connectToDB();
|
||||||
await db_api.removeAllRecords('test');
|
await db_api.removeAllRecords('test');
|
||||||
await db_api.insertRecordIntoTable('test', {test: 'test'});
|
await db_api.insertRecordIntoTable('test', {test: 'test'});
|
||||||
|
|
||||||
@@ -114,7 +118,8 @@ describe('Database', async function() {
|
|||||||
|
|
||||||
for (const local_db_mode of local_db_modes) {
|
for (const local_db_mode of local_db_modes) {
|
||||||
let use_local_db = local_db_mode;
|
let use_local_db = local_db_mode;
|
||||||
describe(`Use local DB - ${use_local_db}`, async function() {
|
const describe_skippable = use_local_db ? describe : describe.skip;
|
||||||
|
describe_skippable(`Use local DB - ${use_local_db}`, async function() {
|
||||||
beforeEach(async function() {
|
beforeEach(async function() {
|
||||||
if (!use_local_db) {
|
if (!use_local_db) {
|
||||||
this.timeout(120000);
|
this.timeout(120000);
|
||||||
@@ -167,7 +172,7 @@ describe('Database', async function() {
|
|||||||
];
|
];
|
||||||
await db_api.insertRecordsIntoTable('test', test_duplicates);
|
await db_api.insertRecordsIntoTable('test', test_duplicates);
|
||||||
const duplicates = await db_api.findDuplicatesByKey('test', 'test');
|
const duplicates = await db_api.findDuplicatesByKey('test', 'test');
|
||||||
console.log(duplicates);
|
assert(duplicates && duplicates.length === 2 && duplicates[0]['key'] === '2' && duplicates[1]['key'] === '4')
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Update record', async function() {
|
it('Update record', async function() {
|
||||||
@@ -279,7 +284,7 @@ describe('Database', async function() {
|
|||||||
assert(stats);
|
assert(stats);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Query speed', async function() {
|
it.skip('Query speed', async function() {
|
||||||
this.timeout(120000);
|
this.timeout(120000);
|
||||||
const NUM_RECORDS_TO_ADD = 300004; // max batch ops is 1000
|
const NUM_RECORDS_TO_ADD = 300004; // max batch ops is 1000
|
||||||
const test_records = [];
|
const test_records = [];
|
||||||
@@ -337,16 +342,23 @@ describe('Database', async function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('Multi User', async function() {
|
describe('Multi User', async function() {
|
||||||
let user = null;
|
this.timeout(120000);
|
||||||
const user_to_test = 'admin';
|
const user_to_test = 'test_user';
|
||||||
const sub_to_test = 'dc834388-3454-41bf-a618-e11cb8c7de1c';
|
const user_password = 'test_pass';
|
||||||
const playlist_to_test = 'ysabVZz4x';
|
const sub_to_test = '';
|
||||||
|
const playlist_to_test = '';
|
||||||
beforeEach(async function() {
|
beforeEach(async function() {
|
||||||
await db_api.connectToDB();
|
// await db_api.connectToDB();
|
||||||
user = await auth_api.login('admin', 'pass');
|
await auth_api.deleteUser(user_to_test);
|
||||||
});
|
});
|
||||||
describe('Authentication', function() {
|
describe('Basic', function() {
|
||||||
it('login', async function() {
|
it('Register', async function() {
|
||||||
|
const user = await auth_api.registerUser(user_to_test, user_to_test, user_password);
|
||||||
|
assert(user);
|
||||||
|
});
|
||||||
|
it('Login', async function() {
|
||||||
|
await auth_api.registerUser(user_to_test, user_to_test, user_password);
|
||||||
|
const user = await auth_api.login(user_to_test, user_password);
|
||||||
assert(user);
|
assert(user);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -362,18 +374,18 @@ describe('Multi User', async function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('Video access - disallowed', async function() {
|
it('Video access - disallowed', async function() {
|
||||||
await db_api.setVideoProperty(video_to_test, {sharingEnabled: false}, user_to_test);
|
await db_api.setVideoProperty(video_to_test, {sharingEnabled: false});
|
||||||
const video_obj = auth_api.getUserVideo('admin', video_to_test, true);
|
const video_obj = await auth_api.getUserVideo(user_to_test, video_to_test, true);
|
||||||
assert(!video_obj);
|
assert(!video_obj);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Video access - allowed', async function() {
|
it('Video access - allowed', async function() {
|
||||||
await db_api.setVideoProperty(video_to_test, {sharingEnabled: true}, user_to_test);
|
await db_api.setVideoProperty(video_to_test, {sharingEnabled: true}, user_to_test);
|
||||||
const video_obj = auth_api.getUserVideo('admin', video_to_test, true);
|
const video_obj = await auth_api.getUserVideo(user_to_test, video_to_test, true);
|
||||||
assert(video_obj);
|
assert(video_obj);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('Zip generators', function() {
|
describe.skip('Zip generators', function() {
|
||||||
it('Playlist zip generator', async function() {
|
it('Playlist zip generator', async function() {
|
||||||
const playlist = await files_api.getPlaylist(playlist_to_test, user_to_test);
|
const playlist = await files_api.getPlaylist(playlist_to_test, user_to_test);
|
||||||
assert(playlist);
|
assert(playlist);
|
||||||
@@ -390,7 +402,7 @@ describe('Multi User', async function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('Subscription zip generator', async function() {
|
it('Subscription zip generator', async function() {
|
||||||
const sub = await subscriptions_api.getSubscription(sub_to_test, user_to_test);
|
const sub = await subscriptions_api.getSubscription(sub_to_test.id, user_to_test);
|
||||||
const sub_videos = await db_api.getRecords('files', {sub_id: sub.id});
|
const sub_videos = await db_api.getRecords('files', {sub_id: sub.id});
|
||||||
assert(sub);
|
assert(sub);
|
||||||
const sub_files_to_download = [];
|
const sub_files_to_download = [];
|
||||||
@@ -429,35 +441,100 @@ describe('Multi User', async function() {
|
|||||||
|
|
||||||
describe('Downloader', function() {
|
describe('Downloader', function() {
|
||||||
const downloader_api = require('../downloader');
|
const downloader_api = require('../downloader');
|
||||||
const url = 'https://www.youtube.com/watch?v=dQw4w9WgXcQ';
|
const url = 'https://www.youtube.com/watch?v=hpigjnKl7nI';
|
||||||
|
const playlist_url = 'https://www.youtube.com/playlist?list=PLbZT16X07RLhqK-ZgSkRuUyiz9B_WLdNK';
|
||||||
const sub_id = 'dc834388-3454-41bf-a618-e11cb8c7de1c';
|
const sub_id = 'dc834388-3454-41bf-a618-e11cb8c7de1c';
|
||||||
const options = {
|
const options = {
|
||||||
ui_uid: uuid(),
|
ui_uid: uuid()
|
||||||
user: 'admin'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function createCategory(url) {
|
||||||
|
// get info
|
||||||
|
const args = await downloader_api.generateArgs(url, 'video', options, null, true);
|
||||||
|
const [info] = await downloader_api.getVideoInfoByURL(url, args);
|
||||||
|
|
||||||
|
// create category
|
||||||
|
await db_api.removeAllRecords('categories');
|
||||||
|
const new_category = {
|
||||||
|
name: 'test_category',
|
||||||
|
uid: uuid(),
|
||||||
|
rules: [],
|
||||||
|
custom_output: ''
|
||||||
|
};
|
||||||
|
await db_api.insertRecordIntoTable('categories', new_category);
|
||||||
|
await db_api.pushToRecordsArray('categories', {name: 'test_category'}, 'rules', {
|
||||||
|
preceding_operator: null,
|
||||||
|
comparator: 'includes',
|
||||||
|
property: 'title',
|
||||||
|
value: info['title']
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
before(async function() {
|
||||||
|
const update_available = await youtubedl_api.checkForYoutubeDLUpdate();
|
||||||
|
if (update_available) await youtubedl_api.updateYoutubeDL(update_available);
|
||||||
|
config_api.setConfigItem('ytdl_max_concurrent_downloads', 0);
|
||||||
|
});
|
||||||
|
|
||||||
beforeEach(async function() {
|
beforeEach(async function() {
|
||||||
await db_api.connectToDB();
|
// await db_api.connectToDB();
|
||||||
await db_api.removeAllRecords('download_queue');
|
await db_api.removeAllRecords('download_queue');
|
||||||
|
config_api.setConfigItem('ytdl_allow_playlist_categorization', true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Get file info', async function() {
|
it('Get file info', async function() {
|
||||||
this.timeout(300000);
|
this.timeout(300000);
|
||||||
const info = await downloader_api.getVideoInfoByURL(url);
|
const info = await downloader_api.getVideoInfoByURL(url);
|
||||||
assert(!!info);
|
assert(!!info && info.length > 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Download file', async function() {
|
it('Download file', async function() {
|
||||||
this.timeout(300000);
|
this.timeout(300000);
|
||||||
|
await downloader_api.setupDownloads();
|
||||||
|
const args = await downloader_api.generateArgs(url, 'video', options, null, true);
|
||||||
|
const [info] = await downloader_api.getVideoInfoByURL(url, args);
|
||||||
|
if (fs.existsSync(info['_filename'])) fs.unlinkSync(info['_filename']);
|
||||||
const returned_download = await downloader_api.createDownload(url, 'video', options);
|
const returned_download = await downloader_api.createDownload(url, 'video', options);
|
||||||
console.log(returned_download);
|
assert(returned_download);
|
||||||
await utils.wait(20000);
|
const custom_download_method = async (url, args, options, callback) => {
|
||||||
|
fs.writeJSONSync(utils.getTrueFileName(info['_filename'], 'video', '.info.json'), info);
|
||||||
|
await generateEmptyVideoFile(info['_filename']);
|
||||||
|
return await callback(null, [JSON.stringify(info)]);
|
||||||
|
}
|
||||||
|
const success = await downloader_api.downloadQueuedFile(returned_download['uid'], custom_download_method);
|
||||||
|
assert(success);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Downloader - categorize', async function() {
|
||||||
|
this.timeout(300000);
|
||||||
|
await createCategory(url);
|
||||||
|
// collect info
|
||||||
|
const returned_download = await downloader_api.createDownload(url, 'video', options);
|
||||||
|
await downloader_api.collectInfo(returned_download['uid']);
|
||||||
|
assert(returned_download['category']);
|
||||||
|
assert(returned_download['category']['name'] === 'test_category');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Downloader - categorize playlist', async function() {
|
||||||
|
this.timeout(300000);
|
||||||
|
await createCategory(playlist_url);
|
||||||
|
// collect info
|
||||||
|
const returned_download_pass = await downloader_api.createDownload(playlist_url, 'video', options);
|
||||||
|
await downloader_api.collectInfo(returned_download_pass['uid']);
|
||||||
|
assert(returned_download_pass['category']);
|
||||||
|
assert(returned_download_pass['category']['name'] === 'test_category');
|
||||||
|
|
||||||
|
// test with playlist categorization disabled
|
||||||
|
config_api.setConfigItem('ytdl_allow_playlist_categorization', false);
|
||||||
|
const returned_download_fail = await downloader_api.createDownload(playlist_url, 'video', options);
|
||||||
|
await downloader_api.collectInfo(returned_download_fail['uid']);
|
||||||
|
assert(!returned_download_fail['category']);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Tag file', async function() {
|
it('Tag file', async function() {
|
||||||
const audio_path = './test/sample.mp3';
|
const success = await generateEmptyAudioFile('test/sample_mp3.mp3');
|
||||||
const sample_json = fs.readJSONSync('./test/sample.info.json');
|
const audio_path = './test/sample_mp3.mp3';
|
||||||
|
const sample_json = fs.readJSONSync('./test/sample_mp3.info.json');
|
||||||
const tags = {
|
const tags = {
|
||||||
title: sample_json['title'],
|
title: sample_json['title'],
|
||||||
artist: sample_json['artist'] ? sample_json['artist'] : sample_json['uploader'],
|
artist: sample_json['artist'] ? sample_json['artist'] : sample_json['uploader'],
|
||||||
@@ -465,14 +542,13 @@ describe('Downloader', function() {
|
|||||||
}
|
}
|
||||||
NodeID3.write(tags, audio_path);
|
NodeID3.write(tags, audio_path);
|
||||||
const written_tags = NodeID3.read(audio_path);
|
const written_tags = NodeID3.read(audio_path);
|
||||||
assert(written_tags['raw']['TRCK'] === '27');
|
assert(success && written_tags['raw']['TRCK'] === '27');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Queue file', async function() {
|
it('Queue file', async function() {
|
||||||
this.timeout(300000);
|
this.timeout(300000);
|
||||||
const returned_download = await downloader_api.createDownload(url, 'video', options);
|
const returned_download = await downloader_api.createDownload(url, 'video', options, null, null, null, null, true);
|
||||||
console.log(returned_download);
|
assert(returned_download);
|
||||||
await utils.wait(20000);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Pause file', async function() {
|
it('Pause file', async function() {
|
||||||
@@ -487,7 +563,7 @@ describe('Downloader', function() {
|
|||||||
assert(args.length > 0);
|
assert(args.length > 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Generate args - subscription', async function() {
|
it.skip('Generate args - subscription', async function() {
|
||||||
const sub = await subscriptions_api.getSubscription(sub_id);
|
const sub = await subscriptions_api.getSubscription(sub_id);
|
||||||
const sub_options = subscriptions_api.generateOptionsForSubscriptionDownload(sub, 'admin');
|
const sub_options = subscriptions_api.generateOptionsForSubscriptionDownload(sub, 'admin');
|
||||||
const args_normal = await downloader_api.generateArgs(url, 'video', options);
|
const args_normal = await downloader_api.generateArgs(url, 'video', options);
|
||||||
@@ -500,7 +576,7 @@ describe('Downloader', function() {
|
|||||||
if (fs.existsSync(nfo_file_path)) {
|
if (fs.existsSync(nfo_file_path)) {
|
||||||
fs.unlinkSync(nfo_file_path);
|
fs.unlinkSync(nfo_file_path);
|
||||||
}
|
}
|
||||||
const sample_json = fs.readJSONSync('./test/sample.info.json');
|
const sample_json = fs.readJSONSync('./test/sample_mp4.info.json');
|
||||||
downloader_api.generateNFOFile(sample_json, nfo_file_path);
|
downloader_api.generateNFOFile(sample_json, nfo_file_path);
|
||||||
assert(fs.existsSync(nfo_file_path), true);
|
assert(fs.existsSync(nfo_file_path), true);
|
||||||
fs.unlinkSync(nfo_file_path);
|
fs.unlinkSync(nfo_file_path);
|
||||||
@@ -527,11 +603,19 @@ describe('Downloader', function() {
|
|||||||
});
|
});
|
||||||
describe('Twitch', async function () {
|
describe('Twitch', async function () {
|
||||||
const twitch_api = require('../twitch');
|
const twitch_api = require('../twitch');
|
||||||
const example_vod = '1710641401';
|
const example_vod = '1790315420';
|
||||||
it('Download VOD', async function() {
|
it('Download VOD chat', async function() {
|
||||||
|
this.timeout(300000);
|
||||||
|
if (!fs.existsSync('TwitchDownloaderCLI')) {
|
||||||
|
try {
|
||||||
|
await exec('sh ../docker-utils/fetch-twitchdownloader.sh');
|
||||||
|
fs.copyFileSync('../docker-utils/TwitchDownloaderCLI', 'TwitchDownloaderCLI');
|
||||||
|
} catch (e) {
|
||||||
|
logger.info('TwitchDownloaderCLI fetch failed, file may exist regardless.');
|
||||||
|
}
|
||||||
|
}
|
||||||
const sample_path = path.join('test', 'sample.twitch_chat.json');
|
const sample_path = path.join('test', 'sample.twitch_chat.json');
|
||||||
if (fs.existsSync(sample_path)) fs.unlinkSync(sample_path);
|
if (fs.existsSync(sample_path)) fs.unlinkSync(sample_path);
|
||||||
this.timeout(300000);
|
|
||||||
await twitch_api.downloadTwitchChatByVODID(example_vod, 'sample', null, null, null, './test');
|
await twitch_api.downloadTwitchChatByVODID(example_vod, 'sample', null, null, null, './test');
|
||||||
assert(fs.existsSync(sample_path));
|
assert(fs.existsSync(sample_path));
|
||||||
|
|
||||||
@@ -541,10 +625,109 @@ describe('Downloader', function() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('youtube-dl', async function() {
|
||||||
|
beforeEach(async function () {
|
||||||
|
if (fs.existsSync(CONSTS.DETAILS_BIN_PATH)) fs.unlinkSync(CONSTS.DETAILS_BIN_PATH);
|
||||||
|
await youtubedl_api.checkForYoutubeDLUpdate();
|
||||||
|
});
|
||||||
|
it('Check latest version', async function() {
|
||||||
|
this.timeout(300000);
|
||||||
|
const original_fork = config_api.getConfigItem('ytdl_default_downloader');
|
||||||
|
const latest_version = await youtubedl_api.getLatestUpdateVersion(original_fork);
|
||||||
|
assert(latest_version > CONSTS.OUTDATED_YOUTUBEDL_VERSION);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Update youtube-dl', async function() {
|
||||||
|
this.timeout(300000);
|
||||||
|
const original_fork = config_api.getConfigItem('ytdl_default_downloader');
|
||||||
|
const binary_path = path.join('test', 'test_binary');
|
||||||
|
for (const youtubedl_fork in youtubedl_api.youtubedl_forks) {
|
||||||
|
config_api.setConfigItem('ytdl_default_downloader', youtubedl_fork);
|
||||||
|
const latest_version = await youtubedl_api.checkForYoutubeDLUpdate();
|
||||||
|
await youtubedl_api.updateYoutubeDL(latest_version, binary_path);
|
||||||
|
assert(fs.existsSync(binary_path));
|
||||||
|
if (fs.existsSync(binary_path)) fs.unlinkSync(binary_path);
|
||||||
|
}
|
||||||
|
config_api.setConfigItem('ytdl_default_downloader', original_fork);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Run process', async function() {
|
||||||
|
this.timeout(300000);
|
||||||
|
const downloader_api = require('../downloader');
|
||||||
|
const url = 'https://www.youtube.com/watch?v=hpigjnKl7nI';
|
||||||
|
const args = await downloader_api.generateArgs(url, 'video', {}, null, true);
|
||||||
|
const {child_process} = await youtubedl_api.runYoutubeDL(url, args);
|
||||||
|
assert(child_process);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Subscriptions', function() {
|
||||||
|
const new_sub = {
|
||||||
|
name: 'test_sub',
|
||||||
|
url: 'https://www.youtube.com/channel/UCzofo-P8yMMCOv8rsPfIR-g',
|
||||||
|
maxQuality: null,
|
||||||
|
id: uuid(),
|
||||||
|
user_uid: null,
|
||||||
|
type: 'video',
|
||||||
|
paused: true
|
||||||
|
};
|
||||||
|
beforeEach(async function() {
|
||||||
|
await db_api.removeAllRecords('subscriptions');
|
||||||
|
});
|
||||||
|
it('Subscribe', async function () {
|
||||||
|
const success = await subscriptions_api.subscribe(new_sub, null, true);
|
||||||
|
assert(success);
|
||||||
|
const sub_exists = await db_api.getRecord('subscriptions', {id: new_sub['id']});
|
||||||
|
assert(sub_exists);
|
||||||
|
});
|
||||||
|
it('Unsubscribe', async function () {
|
||||||
|
await subscriptions_api.subscribe(new_sub, null, true);
|
||||||
|
await subscriptions_api.unsubscribe(new_sub);
|
||||||
|
const sub_exists = await db_api.getRecord('subscriptions', {id: new_sub['id']});
|
||||||
|
assert(!sub_exists);
|
||||||
|
});
|
||||||
|
it('Delete subscription file', async function () {
|
||||||
|
|
||||||
|
});
|
||||||
|
it('Get subscription by name', async function () {
|
||||||
|
await subscriptions_api.subscribe(new_sub, null, true);
|
||||||
|
const sub_by_name = await subscriptions_api.getSubscriptionByName('test_sub');
|
||||||
|
assert(sub_by_name);
|
||||||
|
});
|
||||||
|
it('Get subscriptions', async function() {
|
||||||
|
await subscriptions_api.subscribe(new_sub, null, true);
|
||||||
|
const subs = await subscriptions_api.getSubscriptions(null);
|
||||||
|
assert(subs && subs.length === 1);
|
||||||
|
});
|
||||||
|
it('Update subscription', async function () {
|
||||||
|
await subscriptions_api.subscribe(new_sub, null, true);
|
||||||
|
const sub_update = Object.assign({}, new_sub, {name: 'updated_name'});
|
||||||
|
await subscriptions_api.updateSubscription(sub_update);
|
||||||
|
const updated_sub = await db_api.getRecord('subscriptions', {id: new_sub['id']});
|
||||||
|
assert(updated_sub['name'] === 'updated_name');
|
||||||
|
});
|
||||||
|
it('Update subscription property', async function () {
|
||||||
|
await subscriptions_api.subscribe(new_sub, null, true);
|
||||||
|
const sub_update = Object.assign({}, new_sub, {name: 'updated_name'});
|
||||||
|
await subscriptions_api.updateSubscriptionPropertyMultiple([sub_update], {name: 'updated_name'});
|
||||||
|
const updated_sub = await db_api.getRecord('subscriptions', {id: new_sub['id']});
|
||||||
|
assert(updated_sub['name'] === 'updated_name');
|
||||||
|
});
|
||||||
|
it('Write subscription metadata', async function() {
|
||||||
|
const metadata_path = path.join('subscriptions', 'channels', 'test_sub', 'subscription_backup.json');
|
||||||
|
if (fs.existsSync(metadata_path)) fs.unlinkSync(metadata_path);
|
||||||
|
await subscriptions_api.subscribe(new_sub, null, true);
|
||||||
|
assert(fs.existsSync(metadata_path));
|
||||||
|
});
|
||||||
|
it('Fresh uploads', async function() {
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('Tasks', function() {
|
describe('Tasks', function() {
|
||||||
const tasks_api = require('../tasks');
|
const tasks_api = require('../tasks');
|
||||||
beforeEach(async function() {
|
beforeEach(async function() {
|
||||||
await db_api.connectToDB();
|
// await db_api.connectToDB();
|
||||||
await db_api.removeAllRecords('tasks');
|
await db_api.removeAllRecords('tasks');
|
||||||
|
|
||||||
const dummy_task = {
|
const dummy_task = {
|
||||||
@@ -563,7 +746,7 @@ describe('Tasks', function() {
|
|||||||
await tasks_api.executeTask('backup_local_db');
|
await tasks_api.executeTask('backup_local_db');
|
||||||
const backups_new = await utils.recFindByExt('appdata', 'bak');
|
const backups_new = await utils.recFindByExt('appdata', 'bak');
|
||||||
const new_length = backups_new.length;
|
const new_length = backups_new.length;
|
||||||
assert(original_length, new_length-1);
|
assert(original_length === new_length-1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Check for missing files', async function() {
|
it('Check for missing files', async function() {
|
||||||
@@ -573,7 +756,7 @@ describe('Tasks', function() {
|
|||||||
await db_api.insertRecordIntoTable('files', test_missing_file);
|
await db_api.insertRecordIntoTable('files', test_missing_file);
|
||||||
await tasks_api.executeTask('missing_files_check');
|
await tasks_api.executeTask('missing_files_check');
|
||||||
const missing_file_db_record = await db_api.getRecord('files', {uid: 'test'});
|
const missing_file_db_record = await db_api.getRecord('files', {uid: 'test'});
|
||||||
assert(!missing_file_db_record, true);
|
assert(!missing_file_db_record);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Check for duplicate files', async function() {
|
it('Check for duplicate files', async function() {
|
||||||
@@ -593,27 +776,29 @@ describe('Tasks', function() {
|
|||||||
|
|
||||||
await tasks_api.executeTask('duplicate_files_check');
|
await tasks_api.executeTask('duplicate_files_check');
|
||||||
const duplicated_record_count = await db_api.getRecords('files', {path: 'test/missing_file.mp4'}, true);
|
const duplicated_record_count = await db_api.getRecords('files', {path: 'test/missing_file.mp4'}, true);
|
||||||
assert(duplicated_record_count == 1, true);
|
assert(duplicated_record_count === 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Import unregistered files', async function() {
|
it('Import unregistered files', async function() {
|
||||||
this.timeout(300000);
|
this.timeout(300000);
|
||||||
|
|
||||||
|
const success = await generateEmptyVideoFile('test/sample_mp4.mp4');
|
||||||
|
|
||||||
// pre-test cleanup
|
// pre-test cleanup
|
||||||
await db_api.removeAllRecords('files', {title: 'Sample File'});
|
await db_api.removeAllRecords('files', {path: 'test/missing_file.mp4'});
|
||||||
if (fs.existsSync('video/sample.info.json')) fs.unlinkSync('video/sample.info.json');
|
if (fs.existsSync('video/sample_mp4.info.json')) fs.unlinkSync('video/sample_mp4.info.json');
|
||||||
if (fs.existsSync('video/sample.mp4')) fs.unlinkSync('video/sample.mp4');
|
if (fs.existsSync('video/sample_mp4.mp4')) fs.unlinkSync('video/sample_mp4.mp4');
|
||||||
|
|
||||||
// copies in files
|
// copies in files
|
||||||
fs.copyFileSync('test/sample.info.json', 'video/sample.info.json');
|
fs.copyFileSync('test/sample_mp4.info.json', 'video/sample_mp4.info.json');
|
||||||
fs.copyFileSync('test/sample.mp4', 'video/sample.mp4');
|
fs.copyFileSync('test/sample_mp4.mp4', 'video/sample_mp4.mp4');
|
||||||
await tasks_api.executeTask('missing_db_records');
|
await tasks_api.executeTask('missing_db_records');
|
||||||
const imported_file = await db_api.getRecord('files', {title: 'Sample File'});
|
const imported_file = await db_api.getRecord('files', {title: 'Sample File'});
|
||||||
assert(!!imported_file === true);
|
assert(success && !!imported_file);
|
||||||
|
|
||||||
// post-test cleanup
|
// post-test cleanup
|
||||||
if (fs.existsSync('video/sample.info.json')) fs.unlinkSync('video/sample.info.json');
|
if (fs.existsSync('video/sample_mp4.info.json')) fs.unlinkSync('video/sample_mp4.info.json');
|
||||||
if (fs.existsSync('video/sample.mp4')) fs.unlinkSync('video/sample.mp4');
|
if (fs.existsSync('video/sample_mp4.mp4')) fs.unlinkSync('video/sample_mp4.mp4');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Schedule and cancel task', async function() {
|
it('Schedule and cancel task', async function() {
|
||||||
@@ -653,12 +838,12 @@ describe('Tasks', function() {
|
|||||||
|
|
||||||
describe('Archive', async function() {
|
describe('Archive', async function() {
|
||||||
beforeEach(async function() {
|
beforeEach(async function() {
|
||||||
await db_api.connectToDB();
|
// await db_api.connectToDB();
|
||||||
await db_api.removeAllRecords('archives', {user_uid: 'test_user'});
|
await db_api.removeAllRecords('archives');
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(async function() {
|
afterEach(async function() {
|
||||||
await db_api.removeAllRecords('archives', {user_uid: 'test_user'});
|
await db_api.removeAllRecords('archives');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Import archive', async function() {
|
it('Import archive', async function() {
|
||||||
@@ -672,7 +857,6 @@ describe('Archive', async function() {
|
|||||||
const count = await archive_api.importArchiveFile(archive_text, 'video', 'test_user', 'test_sub');
|
const count = await archive_api.importArchiveFile(archive_text, 'video', 'test_user', 'test_sub');
|
||||||
assert(count === 4)
|
assert(count === 4)
|
||||||
const archive_items = await db_api.getRecords('archives', {user_uid: 'test_user', sub_id: 'test_sub'});
|
const archive_items = await db_api.getRecords('archives', {user_uid: 'test_user', sub_id: 'test_sub'});
|
||||||
console.log(archive_items);
|
|
||||||
assert(archive_items.length === 4);
|
assert(archive_items.length === 4);
|
||||||
assert(archive_items.filter(archive_item => archive_item.extractor === 'testextractor2').length === 1);
|
assert(archive_items.filter(archive_item => archive_item.extractor === 'testextractor2').length === 1);
|
||||||
assert(archive_items.filter(archive_item => archive_item.extractor === 'testextractor1').length === 3);
|
assert(archive_items.filter(archive_item => archive_item.extractor === 'testextractor1').length === 3);
|
||||||
@@ -703,9 +887,9 @@ describe('Archive', async function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('Remove from archive', async function() {
|
it('Remove from archive', async function() {
|
||||||
await archive_api.addToArchive('testextractor1', 'testing1', 'video', 'test_user');
|
await archive_api.addToArchive('testextractor1', 'testing1', 'video', 'test_title', 'test_user');
|
||||||
await archive_api.addToArchive('testextractor2', 'testing1', 'video', 'test_user');
|
await archive_api.addToArchive('testextractor2', 'testing1', 'video', 'test_title', 'test_user');
|
||||||
await archive_api.addToArchive('testextractor2', 'testing2', 'video', 'test_user');
|
await archive_api.addToArchive('testextractor2', 'testing2', 'video', 'test_title', 'test_user');
|
||||||
|
|
||||||
const success = await archive_api.removeFromArchive('testextractor2', 'testing1', 'video', 'test_user');
|
const success = await archive_api.removeFromArchive('testextractor2', 'testing1', 'video', 'test_user');
|
||||||
assert(success);
|
assert(success);
|
||||||
@@ -751,14 +935,14 @@ describe('Utils', async function() {
|
|||||||
|
|
||||||
describe('Categories', async function() {
|
describe('Categories', async function() {
|
||||||
beforeEach(async function() {
|
beforeEach(async function() {
|
||||||
await db_api.connectToDB();
|
// await db_api.connectToDB();
|
||||||
const new_category = {
|
const new_category = {
|
||||||
name: 'test_category',
|
name: 'test_category',
|
||||||
uid: uuid(),
|
uid: uuid(),
|
||||||
rules: [],
|
rules: [],
|
||||||
custom_output: ''
|
custom_output: ''
|
||||||
};
|
};
|
||||||
|
await db_api.removeAllRecords('categories', {name: 'test_category'});
|
||||||
await db_api.insertRecordIntoTable('categories', new_category);
|
await db_api.insertRecordIntoTable('categories', new_category);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -799,7 +983,6 @@ describe('Categories', async function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const category = await categories_api.categorize([sample_video_json]);
|
const category = await categories_api.categorize([sample_video_json]);
|
||||||
console.log(category);
|
|
||||||
assert(category && category.name === 'test_category');
|
assert(category && category.name === 'test_category');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -852,4 +1035,74 @@ describe('Categories', async function() {
|
|||||||
const category = await categories_api.categorize([sample_video_json]);
|
const category = await categories_api.categorize([sample_video_json]);
|
||||||
assert(category);
|
assert(category);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Config', async function() {
|
||||||
|
it('findChangedConfigItems', async function() {
|
||||||
|
const old_config = {
|
||||||
|
"YoutubeDLMaterial": {
|
||||||
|
"test_object1": {
|
||||||
|
"test_prop1": true,
|
||||||
|
"test_prop2": false
|
||||||
|
},
|
||||||
|
"test_object2": {
|
||||||
|
"test_prop3": {
|
||||||
|
"test_prop3_1": true,
|
||||||
|
"test_prop3_2": false
|
||||||
|
},
|
||||||
|
"test_prop4": false
|
||||||
|
},
|
||||||
|
"test_object3": {
|
||||||
|
"test_prop5": {
|
||||||
|
"test_prop5_1": true,
|
||||||
|
"test_prop5_2": false
|
||||||
|
},
|
||||||
|
"test_prop6": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const new_config = {
|
||||||
|
"YoutubeDLMaterial": {
|
||||||
|
"test_object1": {
|
||||||
|
"test_prop1": false,
|
||||||
|
"test_prop2": false
|
||||||
|
},
|
||||||
|
"test_object2": {
|
||||||
|
"test_prop3": {
|
||||||
|
"test_prop3_1": false,
|
||||||
|
"test_prop3_2": false
|
||||||
|
},
|
||||||
|
"test_prop4": true
|
||||||
|
},
|
||||||
|
"test_object3": {
|
||||||
|
"test_prop5": {
|
||||||
|
"test_prop5_1": true,
|
||||||
|
"test_prop5_2": false
|
||||||
|
},
|
||||||
|
"test_prop6": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const changes = config_api.findChangedConfigItems(old_config, new_config);
|
||||||
|
assert(changes[0]['key'] === 'test_prop1' && changes[0]['old_value'] === true && changes[0]['new_value'] === false);
|
||||||
|
assert(changes[1]['key'] === 'test_prop3' &&
|
||||||
|
changes[1]['old_value']['test_prop3_1'] === true &&
|
||||||
|
changes[1]['new_value']['test_prop3_1'] === false &&
|
||||||
|
changes[1]['old_value']['test_prop3_2'] === false &&
|
||||||
|
changes[1]['new_value']['test_prop3_2'] === false);
|
||||||
|
assert(changes[2]['key'] === 'test_prop4' && changes[2]['old_value'] === false && changes[2]['new_value'] === true);
|
||||||
|
assert(changes[3]['key'] === 'test_prop6' && changes[3]['old_value'] === false && changes[3]['new_value'] === true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const generateEmptyVideoFile = async (file_path) => {
|
||||||
|
if (fs.existsSync(file_path)) fs.unlinkSync(file_path);
|
||||||
|
return await exec(`ffmpeg -t 1 -f lavfi -i color=c=black:s=640x480 -c:v libx264 -tune stillimage -pix_fmt yuv420p "${file_path}"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const generateEmptyAudioFile = async (file_path) => {
|
||||||
|
if (fs.existsSync(file_path)) fs.unlinkSync(file_path);
|
||||||
|
return await exec(`ffmpeg -f lavfi -i anullsrc=r=44100:cl=mono -t 1 -q:a 9 -acodec libmp3lame ${file_path}`);
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ const fs = require('fs-extra')
|
|||||||
const path = require('path');
|
const path = require('path');
|
||||||
const { promisify } = require('util');
|
const { promisify } = require('util');
|
||||||
const child_process = require('child_process');
|
const child_process = require('child_process');
|
||||||
|
const commandExistsSync = require('command-exists').sync;
|
||||||
|
|
||||||
async function getCommentsForVOD(vodId) {
|
async function getCommentsForVOD(vodId) {
|
||||||
const exec = promisify(child_process.exec);
|
const exec = promisify(child_process.exec);
|
||||||
@@ -20,7 +21,7 @@ async function getCommentsForVOD(vodId) {
|
|||||||
const cliExt = is_windows ? '.exe' : ''
|
const cliExt = is_windows ? '.exe' : ''
|
||||||
const cliPath = `TwitchDownloaderCLI${cliExt}`
|
const cliPath = `TwitchDownloaderCLI${cliExt}`
|
||||||
|
|
||||||
if (!fs.existsSync(cliPath)) {
|
if (!commandExistsSync(cliPath)) {
|
||||||
logger.error(`${cliPath} does not exist. Twitch chat download failed! Get it here: https://github.com/lay295/TwitchDownloader`);
|
logger.error(`${cliPath} does not exist. Twitch chat download failed! Get it here: https://github.com/lay295/TwitchDownloader`);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ const CONSTS = require('./consts');
|
|||||||
const is_windows = process.platform === 'win32';
|
const is_windows = process.platform === 'win32';
|
||||||
|
|
||||||
// replaces .webm with appropriate extension
|
// replaces .webm with appropriate extension
|
||||||
exports.getTrueFileName = (unfixed_path, type) => {
|
exports.getTrueFileName = (unfixed_path, type, force_ext = null) => {
|
||||||
let fixed_path = unfixed_path;
|
let fixed_path = unfixed_path;
|
||||||
|
|
||||||
const new_ext = (type === 'audio' ? 'mp3' : 'mp4');
|
const new_ext = (type === 'audio' ? 'mp3' : 'mp4');
|
||||||
@@ -22,7 +22,7 @@ exports.getTrueFileName = (unfixed_path, type) => {
|
|||||||
|
|
||||||
|
|
||||||
if (old_ext !== new_ext) {
|
if (old_ext !== new_ext) {
|
||||||
unfixed_parts[unfixed_parts.length-1] = new_ext;
|
unfixed_parts[unfixed_parts.length-1] = force_ext || new_ext;
|
||||||
fixed_path = unfixed_parts.join('.');
|
fixed_path = unfixed_parts.join('.');
|
||||||
}
|
}
|
||||||
return fixed_path;
|
return fixed_path;
|
||||||
@@ -241,11 +241,6 @@ exports.addUIDsToCategory = (category, files) => {
|
|||||||
return files_that_match;
|
return files_that_match;
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.getCurrentDownloader = () => {
|
|
||||||
const details_json = fs.readJSONSync(CONSTS.DETAILS_BIN_PATH);
|
|
||||||
return details_json['downloader'];
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.recFindByExt = async (base, ext, files, result, recursive = true) => {
|
exports.recFindByExt = async (base, ext, files, result, recursive = true) => {
|
||||||
files = files || (await fs.readdir(base))
|
files = files || (await fs.readdir(base))
|
||||||
result = result || []
|
result = result || []
|
||||||
@@ -347,7 +342,7 @@ exports.checkExistsWithTimeout = async (filePath, timeout) => {
|
|||||||
if (!err) {
|
if (!err) {
|
||||||
clearTimeout(timer);
|
clearTimeout(timer);
|
||||||
if (watcher) watcher.close();
|
if (watcher) watcher.close();
|
||||||
resolve();
|
resolve(true);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -357,7 +352,7 @@ exports.checkExistsWithTimeout = async (filePath, timeout) => {
|
|||||||
if (eventType === 'rename' && filename === basename) {
|
if (eventType === 'rename' && filename === basename) {
|
||||||
clearTimeout(timer);
|
clearTimeout(timer);
|
||||||
if (watcher) watcher.close();
|
if (watcher) watcher.close();
|
||||||
resolve();
|
resolve(true);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -519,6 +514,53 @@ exports.convertFlatObjectToNestedObject = (obj) => {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
exports.getDirectoriesInDirectory = async (basePath) => {
|
||||||
|
try {
|
||||||
|
const files = await fs.readdir(basePath, { withFileTypes: true });
|
||||||
|
return files
|
||||||
|
.filter((file) => file.isDirectory())
|
||||||
|
.map((file) => path.join(basePath, file.name));
|
||||||
|
} catch (err) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.parseOutputJSON = (output, err) => {
|
||||||
|
let split_output = [];
|
||||||
|
// const output_jsons = [];
|
||||||
|
if (err && !output) {
|
||||||
|
const attempt_backup_errs = ['This video is unavailable', 'Private video', 'unavailable video'];
|
||||||
|
const attempt_backup = err.stderr ? attempt_backup_errs.some(err_msg => err.stderr.includes(err_msg)) : false;
|
||||||
|
if (!attempt_backup) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
logger.info('An error was encountered with at least one video, backup method will be used.')
|
||||||
|
try {
|
||||||
|
split_output = err.stdout.split(/\r\n|\r|\n/);
|
||||||
|
} catch (e) {
|
||||||
|
logger.error('Backup method failed. See error below:');
|
||||||
|
logger.error(e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
} else if (output.length === 0 || (output.length === 1 && output[0].length === 0)) {
|
||||||
|
// output is '' or ['']
|
||||||
|
return [];
|
||||||
|
} else {
|
||||||
|
for (const output_item of output) {
|
||||||
|
// we have to do this because sometimes there will be leading characters before the actual json
|
||||||
|
const start_idx = output_item.indexOf('{"');
|
||||||
|
const clean_output = output_item.slice(start_idx, output_item.length);
|
||||||
|
split_output.push(clean_output);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return split_output.map(split_output_str => JSON.parse(split_output_str));
|
||||||
|
} catch(e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// objects
|
// objects
|
||||||
|
|
||||||
function File(id, title, thumbnailURL, isAudio, duration, url, uploader, size, path, upload_date, description, view_count, height, abr) {
|
function File(id, title, thumbnailURL, isAudio, duration, url, uploader, size, path, upload_date, description, view_count, height, abr) {
|
||||||
|
|||||||
@@ -1,141 +1,167 @@
|
|||||||
const fs = require('fs-extra');
|
const fs = require('fs-extra');
|
||||||
const fetch = require('node-fetch');
|
const fetch = require('node-fetch');
|
||||||
|
const path = require('path');
|
||||||
|
const execa = require('execa');
|
||||||
|
const kill = require('tree-kill');
|
||||||
|
|
||||||
const logger = require('./logger');
|
const logger = require('./logger');
|
||||||
const utils = require('./utils');
|
const utils = require('./utils');
|
||||||
const CONSTS = require('./consts');
|
const CONSTS = require('./consts');
|
||||||
const config_api = require('./config.js');
|
const config_api = require('./config.js');
|
||||||
|
|
||||||
const OUTDATED_VERSION = "2020.00.00";
|
|
||||||
|
|
||||||
const is_windows = process.platform === 'win32';
|
const is_windows = process.platform === 'win32';
|
||||||
|
|
||||||
const download_sources = {
|
exports.youtubedl_forks = {
|
||||||
'youtube-dl': {
|
'youtube-dl': {
|
||||||
'tags_url': 'https://api.github.com/repos/ytdl-org/youtube-dl/tags',
|
'download_url': 'https://github.com/ytdl-org/youtube-dl/releases/latest/download/youtube-dl',
|
||||||
'func': downloadLatestYoutubeDLBinary
|
'tags_url': 'https://api.github.com/repos/ytdl-org/youtube-dl/tags'
|
||||||
},
|
},
|
||||||
'youtube-dlc': {
|
'youtube-dlc': {
|
||||||
'tags_url': 'https://api.github.com/repos/blackjack4494/yt-dlc/tags',
|
'download_url': 'https://github.com/blackjack4494/yt-dlc/releases/latest/download/youtube-dlc',
|
||||||
'func': downloadLatestYoutubeDLCBinary
|
'tags_url': 'https://api.github.com/repos/blackjack4494/yt-dlc/tags'
|
||||||
},
|
},
|
||||||
'yt-dlp': {
|
'yt-dlp': {
|
||||||
'tags_url': 'https://api.github.com/repos/yt-dlp/yt-dlp/tags',
|
'download_url': 'https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp',
|
||||||
'func': downloadLatestYoutubeDLPBinary
|
'tags_url': 'https://api.github.com/repos/yt-dlp/yt-dlp/tags'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.checkForYoutubeDLUpdate = async () => {
|
exports.runYoutubeDL = async (url, args, customDownloadHandler = null) => {
|
||||||
return new Promise(async resolve => {
|
const output_file_path = getYoutubeDLPath();
|
||||||
const default_downloader = config_api.getConfigItem('ytdl_default_downloader');
|
if (!fs.existsSync(output_file_path)) await exports.checkForYoutubeDLUpdate();
|
||||||
const tags_url = download_sources[default_downloader]['tags_url'];
|
let callback = null;
|
||||||
// get current version
|
let child_process = null;
|
||||||
let current_app_details_exists = fs.existsSync(CONSTS.DETAILS_BIN_PATH);
|
if (customDownloadHandler) {
|
||||||
if (!current_app_details_exists) {
|
callback = runYoutubeDLCustom(url, args, customDownloadHandler);
|
||||||
logger.warn(`Failed to get youtube-dl binary details at location '${CONSTS.DETAILS_BIN_PATH}'. Generating file...`);
|
} else {
|
||||||
fs.writeJSONSync(CONSTS.DETAILS_BIN_PATH, {"version": OUTDATED_VERSION, "downloader": default_downloader});
|
({callback, child_process} = await runYoutubeDLProcess(url, args));
|
||||||
}
|
}
|
||||||
let current_app_details = JSON.parse(fs.readFileSync(CONSTS.DETAILS_BIN_PATH));
|
|
||||||
let current_version = current_app_details['version'];
|
|
||||||
let current_downloader = current_app_details['downloader'];
|
|
||||||
let stored_binary_path = current_app_details['path'];
|
|
||||||
if (!stored_binary_path || typeof stored_binary_path !== 'string') {
|
|
||||||
// logger.info(`INFO: Failed to get youtube-dl binary path at location: ${CONSTS.DETAILS_BIN_PATH}, attempting to guess actual path...`);
|
|
||||||
const guessed_base_path = 'node_modules/youtube-dl/bin/';
|
|
||||||
const guessed_file_path = guessed_base_path + 'youtube-dl' + (is_windows ? '.exe' : '');
|
|
||||||
if (fs.existsSync(guessed_file_path)) {
|
|
||||||
stored_binary_path = guessed_file_path;
|
|
||||||
// logger.info('INFO: Guess successful! Update process continuing...')
|
|
||||||
} else {
|
|
||||||
logger.error(`Guess '${guessed_file_path}' is not correct. Cancelling update check. Verify that your youtube-dl binaries exist by running npm install.`);
|
|
||||||
resolve(null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// got version, now let's check the latest version from the youtube-dl API
|
return {child_process, callback};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run youtube-dl directly (not cancellable)
|
||||||
fetch(tags_url, {method: 'Get'})
|
const runYoutubeDLCustom = async (url, args, customDownloadHandler) => {
|
||||||
.then(async res => res.json())
|
const downloadHandler = customDownloadHandler;
|
||||||
.then(async (json) => {
|
return new Promise(resolve => {
|
||||||
// check if the versions are different
|
downloadHandler(url, args, {maxBuffer: Infinity}, async function(err, output) {
|
||||||
if (!json || !json[0]) {
|
const parsed_output = utils.parseOutputJSON(output, err);
|
||||||
logger.error(`Failed to check ${default_downloader} version for an update.`)
|
resolve({parsed_output, err});
|
||||||
resolve(null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const latest_update_version = json[0]['name'];
|
|
||||||
if (current_version !== latest_update_version || default_downloader !== current_downloader) {
|
|
||||||
// versions different or different downloader is being used, download new update
|
|
||||||
resolve(latest_update_version);
|
|
||||||
} else {
|
|
||||||
resolve(null);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
logger.error(`Failed to check ${default_downloader} version for an update.`)
|
|
||||||
logger.error(err);
|
|
||||||
resolve(null);
|
|
||||||
return;
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.updateYoutubeDL = async (latest_update_version) => {
|
// Run youtube-dl in a subprocess (cancellable)
|
||||||
const default_downloader = config_api.getConfigItem('ytdl_default_downloader');
|
const runYoutubeDLProcess = async (url, args, youtubedl_fork = config_api.getConfigItem('ytdl_default_downloader')) => {
|
||||||
await download_sources[default_downloader]['func'](latest_update_version);
|
const youtubedl_path = getYoutubeDLPath(youtubedl_fork);
|
||||||
|
const binary_exists = fs.existsSync(youtubedl_path);
|
||||||
|
if (!binary_exists) {
|
||||||
|
const err = `Could not find path for ${youtubedl_fork} at ${youtubedl_path}`;
|
||||||
|
logger.error(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const child_process = execa(getYoutubeDLPath(youtubedl_fork), [url, ...args], {maxBuffer: Infinity});
|
||||||
|
const callback = new Promise(async resolve => {
|
||||||
|
try {
|
||||||
|
const {stdout, stderr} = await child_process;
|
||||||
|
const parsed_output = utils.parseOutputJSON(stdout.trim().split(/\r?\n/), stderr);
|
||||||
|
resolve({parsed_output, err: stderr});
|
||||||
|
} catch (e) {
|
||||||
|
// Attempt to not fail
|
||||||
|
const parsed_output = utils.parseOutputJSON(e && e.stdout && e.stdout.trim().split(/\r?\n/), e && e.stderr);
|
||||||
|
resolve({parsed_output: parsed_output, err: parsed_output ? null : e});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return {child_process, callback}
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.verifyBinaryExistsLinux = () => {
|
function getYoutubeDLPath(youtubedl_fork = config_api.getConfigItem('ytdl_default_downloader')) {
|
||||||
const details_json = fs.readJSONSync(CONSTS.DETAILS_BIN_PATH);
|
const binary_file_name = youtubedl_fork + (is_windows ? '.exe' : '');
|
||||||
if (!is_windows && details_json && (!details_json['path'] || details_json['path'].includes('.exe'))) {
|
const binary_path = path.join('appdata', 'bin', binary_file_name);
|
||||||
details_json['path'] = 'node_modules/youtube-dl/bin/youtube-dl';
|
return binary_path;
|
||||||
details_json['exec'] = 'youtube-dl';
|
}
|
||||||
details_json['version'] = OUTDATED_VERSION;
|
|
||||||
fs.writeJSONSync(CONSTS.DETAILS_BIN_PATH, details_json);
|
|
||||||
|
|
||||||
utils.restartServer();
|
exports.killYoutubeDLProcess = async (child_process) => {
|
||||||
|
kill(child_process.pid, 'SIGKILL');
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.checkForYoutubeDLUpdate = async () => {
|
||||||
|
const selected_fork = config_api.getConfigItem('ytdl_default_downloader');
|
||||||
|
const output_file_path = getYoutubeDLPath();
|
||||||
|
// get current version
|
||||||
|
let current_app_details_exists = fs.existsSync(CONSTS.DETAILS_BIN_PATH);
|
||||||
|
if (!current_app_details_exists[selected_fork]) {
|
||||||
|
logger.warn(`Failed to get youtube-dl binary details at location '${CONSTS.DETAILS_BIN_PATH}'. Generating file...`);
|
||||||
|
updateDetailsJSON(CONSTS.OUTDATED_YOUTUBEDL_VERSION, selected_fork, output_file_path);
|
||||||
|
}
|
||||||
|
const current_app_details = JSON.parse(fs.readFileSync(CONSTS.DETAILS_BIN_PATH));
|
||||||
|
const current_version = current_app_details[selected_fork]['version'];
|
||||||
|
const current_fork = current_app_details[selected_fork]['downloader'];
|
||||||
|
|
||||||
|
const latest_version = await exports.getLatestUpdateVersion(selected_fork);
|
||||||
|
// if the binary does not exist, or default_downloader doesn't match existing fork, or if the fork has been updated, redownload
|
||||||
|
// TODO: don't redownload if fork already exists
|
||||||
|
if (!fs.existsSync(output_file_path) || current_fork !== selected_fork || !current_version || current_version !== latest_version) {
|
||||||
|
logger.warn(`Updating ${selected_fork} binary to '${output_file_path}', downloading...`);
|
||||||
|
await exports.updateYoutubeDL(latest_version);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function downloadLatestYoutubeDLBinary(new_version) {
|
exports.updateYoutubeDL = async (latest_update_version, custom_output_path = null) => {
|
||||||
const file_ext = is_windows ? '.exe' : '';
|
await fs.ensureDir(path.join('appdata', 'bin'));
|
||||||
|
const default_downloader = config_api.getConfigItem('ytdl_default_downloader');
|
||||||
const download_url = `https://github.com/ytdl-org/youtube-dl/releases/latest/download/youtube-dl${file_ext}`;
|
await downloadLatestYoutubeDLBinaryGeneric(default_downloader, latest_update_version, custom_output_path);
|
||||||
const output_path = `node_modules/youtube-dl/bin/youtube-dl${file_ext}`;
|
|
||||||
|
|
||||||
await utils.fetchFile(download_url, output_path, `youtube-dl ${new_version}`);
|
|
||||||
|
|
||||||
updateDetailsJSON(new_version, 'youtube-dl');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function downloadLatestYoutubeDLCBinary(new_version) {
|
async function downloadLatestYoutubeDLBinaryGeneric(youtubedl_fork, new_version, custom_output_path = null) {
|
||||||
const file_ext = is_windows ? '.exe' : '';
|
const file_ext = is_windows ? '.exe' : '';
|
||||||
|
|
||||||
const download_url = `https://github.com/blackjack4494/yt-dlc/releases/latest/download/youtube-dlc${file_ext}`;
|
// build the URL
|
||||||
const output_path = `node_modules/youtube-dl/bin/youtube-dl${file_ext}`;
|
const download_url = `${exports.youtubedl_forks[youtubedl_fork]['download_url']}${file_ext}`;
|
||||||
|
const output_path = custom_output_path || getYoutubeDLPath(youtubedl_fork);
|
||||||
|
|
||||||
await utils.fetchFile(download_url, output_path, `youtube-dlc ${new_version}`);
|
try {
|
||||||
|
await utils.fetchFile(download_url, output_path, `${youtubedl_fork} ${new_version}`);
|
||||||
|
fs.chmod(output_path, 0o777);
|
||||||
|
|
||||||
updateDetailsJSON(new_version, 'youtube-dlc');
|
updateDetailsJSON(new_version, youtubedl_fork, output_path);
|
||||||
|
} catch (e) {
|
||||||
|
logger.error(`Failed to download new ${youtubedl_fork} version: ${new_version}`);
|
||||||
|
logger.error(e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.getLatestUpdateVersion = async (youtubedl_fork) => {
|
||||||
|
const tags_url = exports.youtubedl_forks[youtubedl_fork]['tags_url'];
|
||||||
|
return new Promise(resolve => {
|
||||||
|
fetch(tags_url, {method: 'Get'})
|
||||||
|
.then(async res => res.json())
|
||||||
|
.then(async (json) => {
|
||||||
|
if (!json || !json[0]) {
|
||||||
|
logger.error(`Failed to check ${youtubedl_fork} version for an update.`)
|
||||||
|
resolve(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const latest_update_version = json[0]['name'];
|
||||||
|
resolve(latest_update_version);
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
logger.error(`Failed to check ${youtubedl_fork} version for an update.`)
|
||||||
|
logger.error(err);
|
||||||
|
resolve(null);
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function downloadLatestYoutubeDLPBinary(new_version) {
|
function updateDetailsJSON(new_version, fork, output_path) {
|
||||||
const file_ext = is_windows ? '.exe' : '';
|
const file_ext = is_windows ? '.exe' : '';
|
||||||
|
const details_json = fs.existsSync(CONSTS.DETAILS_BIN_PATH) ? fs.readJSONSync(CONSTS.DETAILS_BIN_PATH) : {};
|
||||||
const download_url = `https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp${file_ext}`;
|
if (!details_json[fork]) details_json[fork] = {};
|
||||||
const output_path = `node_modules/youtube-dl/bin/youtube-dl${file_ext}`;
|
const fork_json = details_json[fork];
|
||||||
|
fork_json['version'] = new_version;
|
||||||
await utils.fetchFile(download_url, output_path, `yt-dlp ${new_version}`);
|
fork_json['downloader'] = fork;
|
||||||
|
fork_json['path'] = output_path; // unused
|
||||||
updateDetailsJSON(new_version, 'yt-dlp');
|
fork_json['exec'] = fork + file_ext; // unused
|
||||||
}
|
|
||||||
|
|
||||||
function updateDetailsJSON(new_version, downloader) {
|
|
||||||
const details_json = fs.readJSONSync(CONSTS.DETAILS_BIN_PATH);
|
|
||||||
if (new_version) details_json['version'] = new_version;
|
|
||||||
details_json['downloader'] = downloader;
|
|
||||||
fs.writeJSONSync(CONSTS.DETAILS_BIN_PATH, details_json);
|
fs.writeJSONSync(CONSTS.DETAILS_BIN_PATH, details_json);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
apiVersion: v2
|
apiVersion: v2
|
||||||
name: youtubedl-material
|
name: youtubedl-material
|
||||||
description: A Helm chart for Kubernetes
|
description: A Helm chart for https://github.com/Tzahi12345/YoutubeDL-Material
|
||||||
|
|
||||||
# A chart can be either an 'application' or a 'library' chart.
|
# A chart can be either an 'application' or a 'library' chart.
|
||||||
#
|
#
|
||||||
@@ -15,10 +15,10 @@ type: application
|
|||||||
# This is the chart version. This version number should be incremented each time you make changes
|
# This is the chart version. This version number should be incremented each time you make changes
|
||||||
# to the chart and its templates, including the app version.
|
# to the chart and its templates, including the app version.
|
||||||
# Versions are expected to follow Semantic Versioning (https://semver.org/)
|
# Versions are expected to follow Semantic Versioning (https://semver.org/)
|
||||||
version: 0.1.0
|
version: 0.2.0
|
||||||
|
|
||||||
# This is the version number of the application being deployed. This version number should be
|
# This is the version number of the application being deployed. This version number should be
|
||||||
# incremented each time you make changes to the application. Versions are not expected to
|
# incremented each time you make changes to the application. Versions are not expected to
|
||||||
# follow Semantic Versioning. They should reflect the version the application is using.
|
# follow Semantic Versioning. They should reflect the version the application is using.
|
||||||
# It is recommended to use it with quotes.
|
# It is recommended to use it with quotes.
|
||||||
appVersion: "4.3.1"
|
appVersion: "4.3.2"
|
||||||
|
|||||||
@@ -1,7 +1,14 @@
|
|||||||
{{- if .Values.ingress.enabled -}}
|
{{- if .Values.ingress.enabled -}}
|
||||||
{{- $fullName := include "youtubedl-material.fullname" . -}}
|
{{- $fullName := include "youtubedl-material.fullname" . -}}
|
||||||
{{- $svcPort := .Values.service.port -}}
|
{{- $svcPort := .Values.service.port -}}
|
||||||
{{- if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}}
|
{{- if and .Values.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }}
|
||||||
|
{{- if not (hasKey .Values.ingress.annotations "kubernetes.io/ingress.class") }}
|
||||||
|
{{- $_ := set .Values.ingress.annotations "kubernetes.io/ingress.class" .Values.ingress.className}}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}}
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
{{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}}
|
||||||
apiVersion: networking.k8s.io/v1beta1
|
apiVersion: networking.k8s.io/v1beta1
|
||||||
{{- else -}}
|
{{- else -}}
|
||||||
apiVersion: extensions/v1beta1
|
apiVersion: extensions/v1beta1
|
||||||
@@ -16,6 +23,9 @@ metadata:
|
|||||||
{{- toYaml . | nindent 4 }}
|
{{- toYaml . | nindent 4 }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
spec:
|
spec:
|
||||||
|
{{- if and .Values.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }}
|
||||||
|
ingressClassName: {{ .Values.ingress.className }}
|
||||||
|
{{- end }}
|
||||||
{{- if .Values.ingress.tls }}
|
{{- if .Values.ingress.tls }}
|
||||||
tls:
|
tls:
|
||||||
{{- range .Values.ingress.tls }}
|
{{- range .Values.ingress.tls }}
|
||||||
@@ -33,9 +43,19 @@ spec:
|
|||||||
paths:
|
paths:
|
||||||
{{- range .paths }}
|
{{- range .paths }}
|
||||||
- path: {{ .path }}
|
- path: {{ .path }}
|
||||||
|
{{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }}
|
||||||
|
pathType: {{ .pathType }}
|
||||||
|
{{- end }}
|
||||||
backend:
|
backend:
|
||||||
|
{{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }}
|
||||||
|
service:
|
||||||
|
name: {{ $fullName }}
|
||||||
|
port:
|
||||||
|
number: {{ $svcPort }}
|
||||||
|
{{- else }}
|
||||||
serviceName: {{ $fullName }}
|
serviceName: {{ $fullName }}
|
||||||
servicePort: {{ $svcPort }}
|
servicePort: {{ $svcPort }}
|
||||||
|
{{- end }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
|
|||||||
39
docker-utils/fetch-twitchdownloader.sh
Normal file
39
docker-utils/fetch-twitchdownloader.sh
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
# THANK YOU TALULAH (https://github.com/nottalulah) for your help in figuring this out
|
||||||
|
# and also optimizing some code with this commit.
|
||||||
|
# xoxo :D
|
||||||
|
|
||||||
|
case $(uname -m) in
|
||||||
|
x86_64)
|
||||||
|
ARCH=Linux-x64;;
|
||||||
|
aarch64)
|
||||||
|
ARCH=LinuxArm64;;
|
||||||
|
armhf)
|
||||||
|
ARCH=LinuxArm;;
|
||||||
|
armv7)
|
||||||
|
ARCH=LinuxArm;;
|
||||||
|
armv7l)
|
||||||
|
ARCH=LinuxArm;;
|
||||||
|
*)
|
||||||
|
echo "Unsupported architecture: $(uname -m)"
|
||||||
|
exit 1
|
||||||
|
esac
|
||||||
|
|
||||||
|
echo "(INFO) Architecture detected: $ARCH"
|
||||||
|
echo "(1/5) READY - Install unzip"
|
||||||
|
apt-get update && apt-get -y install unzip curl jq
|
||||||
|
VERSION=$(curl --silent "https://api.github.com/repos/lay295/TwitchDownloader/releases" | jq -r --arg arch "$ARCH" '[.[] | select(.assets | length > 0) | select(.assets[].name | contains("CLI") and contains($arch))] | max_by(.published_at) | .tag_name')
|
||||||
|
echo "(2/5) DOWNLOAD - Acquire twitchdownloader"
|
||||||
|
curl -o twitchdownloader.zip \
|
||||||
|
--connect-timeout 5 \
|
||||||
|
--max-time 120 \
|
||||||
|
--retry 5 \
|
||||||
|
--retry-delay 0 \
|
||||||
|
--retry-max-time 40 \
|
||||||
|
-L "https://github.com/lay295/TwitchDownloader/releases/download/$VERSION/TwitchDownloaderCLI-$VERSION-$ARCH.zip"
|
||||||
|
unzip twitchdownloader.zip
|
||||||
|
chmod +x TwitchDownloaderCLI
|
||||||
|
echo "(3/5) Smoke test"
|
||||||
|
./TwitchDownloaderCLI --help
|
||||||
|
cp ./TwitchDownloaderCLI /usr/local/bin/TwitchDownloaderCLI
|
||||||
@@ -30,7 +30,7 @@ curl -o ffmpeg.txz \
|
|||||||
--retry 5 \
|
--retry 5 \
|
||||||
--retry-delay 0 \
|
--retry-delay 0 \
|
||||||
--retry-max-time 40 \
|
--retry-max-time 40 \
|
||||||
"https://johnvansickle.com/ffmpeg/builds/ffmpeg-git-${ARCH}-static.tar.xz"
|
"https://johnvansickle.com/ffmpeg/old-releases/ffmpeg-5.1.1-${ARCH}-static.tar.xz"
|
||||||
mkdir /tmp/ffmpeg
|
mkdir /tmp/ffmpeg
|
||||||
tar xf ffmpeg.txz -C /tmp/ffmpeg
|
tar xf ffmpeg.txz -C /tmp/ffmpeg
|
||||||
echo "(3/5) CLEANUP - Remove temp dependencies from ffmpeg obtain layer"
|
echo "(3/5) CLEANUP - Remove temp dependencies from ffmpeg obtain layer"
|
||||||
|
|||||||
21480
package-lock.json
generated
21480
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
55
package.json
55
package.json
@@ -1,10 +1,11 @@
|
|||||||
{
|
{
|
||||||
"name": "youtube-dl-material",
|
"name": "youtube-dl-material",
|
||||||
"version": "4.3.1",
|
"version": "4.3.2",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"ng": "ng",
|
"ng": "ng",
|
||||||
"start": "ng serve",
|
"start": "ng serve",
|
||||||
|
"codespaces": "ng serve --configuration=codespaces",
|
||||||
"build": "ng build --configuration production",
|
"build": "ng build --configuration production",
|
||||||
"prebuild": "node src/postbuild.mjs",
|
"prebuild": "node src/postbuild.mjs",
|
||||||
"heroku-postbuild": "npm install --prefix backend",
|
"heroku-postbuild": "npm install --prefix backend",
|
||||||
@@ -16,23 +17,23 @@
|
|||||||
"i18n-source": "ng extract-i18n --output-path=src/assets/i18n --out-file=messages.en.xlf"
|
"i18n-source": "ng extract-i18n --output-path=src/assets/i18n --out-file=messages.en.xlf"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "12.3.1",
|
"node": "18.19.0",
|
||||||
"npm": "6.10.3"
|
"npm": "10.2.3"
|
||||||
},
|
},
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@angular-devkit/core": "^15.0.1",
|
"@angular-devkit/core": "^17.0.5",
|
||||||
"@angular/animations": "^15.0.1",
|
"@angular/animations": "^17.0.5",
|
||||||
"@angular/cdk": "^15.0.0",
|
"@angular/cdk": "^17.0.2",
|
||||||
"@angular/common": "^15.0.1",
|
"@angular/common": "^17.0.5",
|
||||||
"@angular/compiler": "^15.0.1",
|
"@angular/compiler": "^17.0.5",
|
||||||
"@angular/core": "^15.0.1",
|
"@angular/core": "^17.0.5",
|
||||||
"@angular/forms": "^15.0.1",
|
"@angular/forms": "^17.0.5",
|
||||||
"@angular/localize": "^15.0.1",
|
"@angular/localize": "^17.0.5",
|
||||||
"@angular/material": "^15.0.0",
|
"@angular/material": "^17.0.2",
|
||||||
"@angular/platform-browser": "^15.0.1",
|
"@angular/platform-browser": "^17.0.5",
|
||||||
"@angular/platform-browser-dynamic": "^15.0.1",
|
"@angular/platform-browser-dynamic": "^17.0.5",
|
||||||
"@angular/router": "^15.0.1",
|
"@angular/router": "^17.0.5",
|
||||||
"@fontsource/material-icons": "^4.5.4",
|
"@fontsource/material-icons": "^4.5.4",
|
||||||
"@ngneat/content-loader": "^7.0.0",
|
"@ngneat/content-loader": "^7.0.0",
|
||||||
"@videogular/ngx-videogular": "^6.0.0",
|
"@videogular/ngx-videogular": "^6.0.0",
|
||||||
@@ -43,20 +44,19 @@
|
|||||||
"fs-extra": "^10.0.0",
|
"fs-extra": "^10.0.0",
|
||||||
"material-icons": "^1.10.8",
|
"material-icons": "^1.10.8",
|
||||||
"nan": "^2.14.1",
|
"nan": "^2.14.1",
|
||||||
"ngx-avatars": "^1.4.1",
|
"ngx-avatars": "^1.10.0",
|
||||||
"ngx-file-drop": "^15.0.0",
|
"ngx-file-drop": "^15.0.0",
|
||||||
"rxjs": "^6.6.3",
|
"rxjs": "^6.6.3",
|
||||||
"rxjs-compat": "^6.6.7",
|
"rxjs-compat": "^6.6.7",
|
||||||
"tslib": "^2.0.0",
|
"tslib": "^2.0.0",
|
||||||
"typescript": "~4.8.4",
|
|
||||||
"xliff-to-json": "^1.0.4",
|
"xliff-to-json": "^1.0.4",
|
||||||
"zone.js": "~0.11.4"
|
"zone.js": "~0.14.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@angular-devkit/build-angular": "^15.0.1",
|
"@angular-devkit/build-angular": "^17.0.5",
|
||||||
"@angular/cli": "^15.0.1",
|
"@angular/cli": "^17.0.5",
|
||||||
"@angular/compiler-cli": "^15.0.1",
|
"@angular/compiler-cli": "^17.0.5",
|
||||||
"@angular/language-service": "^15.0.1",
|
"@angular/language-service": "^17.0.5",
|
||||||
"@types/core-js": "^2.5.2",
|
"@types/core-js": "^2.5.2",
|
||||||
"@types/file-saver": "^2.0.1",
|
"@types/file-saver": "^2.0.1",
|
||||||
"@types/jasmine": "^4.3.1",
|
"@types/jasmine": "^4.3.1",
|
||||||
@@ -66,7 +66,7 @@
|
|||||||
"ajv": "^7.2.4",
|
"ajv": "^7.2.4",
|
||||||
"codelyzer": "^6.0.0",
|
"codelyzer": "^6.0.0",
|
||||||
"eslint": "^7.32.0",
|
"eslint": "^7.32.0",
|
||||||
"jasmine-core": "~3.6.0",
|
"jasmine-core": "~3.8.0",
|
||||||
"jasmine-spec-reporter": "~5.0.0",
|
"jasmine-spec-reporter": "~5.0.0",
|
||||||
"karma": "~6.4.2",
|
"karma": "~6.4.2",
|
||||||
"karma-chrome-launcher": "~3.1.0",
|
"karma-chrome-launcher": "~3.1.0",
|
||||||
@@ -77,6 +77,13 @@
|
|||||||
"openapi-typescript-codegen": "^0.23.0",
|
"openapi-typescript-codegen": "^0.23.0",
|
||||||
"protractor": "~7.0.0",
|
"protractor": "~7.0.0",
|
||||||
"ts-node": "~3.0.4",
|
"ts-node": "~3.0.4",
|
||||||
"tslint": "~6.1.0"
|
"tslint": "~6.1.0",
|
||||||
|
"typescript": "~5.2.0"
|
||||||
|
},
|
||||||
|
"overrides": {
|
||||||
|
"ngx-avatars": {
|
||||||
|
"@angular/common": "^17.0.0",
|
||||||
|
"@angular/core": "^17.0.0"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ export type { ChangeRolePermissionsRequest } from './models/ChangeRolePermission
|
|||||||
export type { ChangeUserPermissionsRequest } from './models/ChangeUserPermissionsRequest';
|
export type { ChangeUserPermissionsRequest } from './models/ChangeUserPermissionsRequest';
|
||||||
export type { CheckConcurrentStreamRequest } from './models/CheckConcurrentStreamRequest';
|
export type { CheckConcurrentStreamRequest } from './models/CheckConcurrentStreamRequest';
|
||||||
export type { CheckConcurrentStreamResponse } from './models/CheckConcurrentStreamResponse';
|
export type { CheckConcurrentStreamResponse } from './models/CheckConcurrentStreamResponse';
|
||||||
|
export type { CheckSubscriptionRequest } from './models/CheckSubscriptionRequest';
|
||||||
export type { ClearDownloadsRequest } from './models/ClearDownloadsRequest';
|
export type { ClearDownloadsRequest } from './models/ClearDownloadsRequest';
|
||||||
export type { ConcurrentStream } from './models/ConcurrentStream';
|
export type { ConcurrentStream } from './models/ConcurrentStream';
|
||||||
export type { Config } from './models/Config';
|
export type { Config } from './models/Config';
|
||||||
@@ -104,6 +105,7 @@ export type { SubscriptionRequestData } from './models/SubscriptionRequestData';
|
|||||||
export type { SuccessObject } from './models/SuccessObject';
|
export type { SuccessObject } from './models/SuccessObject';
|
||||||
export type { TableInfo } from './models/TableInfo';
|
export type { TableInfo } from './models/TableInfo';
|
||||||
export type { Task } from './models/Task';
|
export type { Task } from './models/Task';
|
||||||
|
export { TaskType } from './models/TaskType';
|
||||||
export type { TestConnectionStringRequest } from './models/TestConnectionStringRequest';
|
export type { TestConnectionStringRequest } from './models/TestConnectionStringRequest';
|
||||||
export type { TestConnectionStringResponse } from './models/TestConnectionStringResponse';
|
export type { TestConnectionStringResponse } from './models/TestConnectionStringResponse';
|
||||||
export type { TransferDBRequest } from './models/TransferDBRequest';
|
export type { TransferDBRequest } from './models/TransferDBRequest';
|
||||||
|
|||||||
@@ -5,4 +5,4 @@
|
|||||||
export type AddFileToPlaylistRequest = {
|
export type AddFileToPlaylistRequest = {
|
||||||
file_uid: string;
|
file_uid: string;
|
||||||
playlist_id: string;
|
playlist_id: string;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -13,4 +13,4 @@ export type Archive = {
|
|||||||
sub_id?: string;
|
sub_id?: string;
|
||||||
timestamp: number;
|
timestamp: number;
|
||||||
uid: string;
|
uid: string;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,4 +8,4 @@ import type { YesNo } from './YesNo';
|
|||||||
export type BaseChangePermissionsRequest = {
|
export type BaseChangePermissionsRequest = {
|
||||||
permission: UserPermission;
|
permission: UserPermission;
|
||||||
new_value: YesNo;
|
new_value: YesNo;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -12,4 +12,4 @@ export type Category = {
|
|||||||
* Overrides file output for downloaded files in category
|
* Overrides file output for downloaded files in category
|
||||||
*/
|
*/
|
||||||
custom_output?: string;
|
custom_output?: string;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -22,4 +22,4 @@ export namespace CategoryRule {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,4 +6,4 @@ import type { BaseChangePermissionsRequest } from './BaseChangePermissionsReques
|
|||||||
|
|
||||||
export type ChangeRolePermissionsRequest = (BaseChangePermissionsRequest & {
|
export type ChangeRolePermissionsRequest = (BaseChangePermissionsRequest & {
|
||||||
role: string;
|
role: string;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,4 +6,4 @@ import type { BaseChangePermissionsRequest } from './BaseChangePermissionsReques
|
|||||||
|
|
||||||
export type ChangeUserPermissionsRequest = (BaseChangePermissionsRequest & {
|
export type ChangeUserPermissionsRequest = (BaseChangePermissionsRequest & {
|
||||||
user_uid: string;
|
user_uid: string;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -7,4 +7,4 @@ export type CheckConcurrentStreamRequest = {
|
|||||||
* UID of the concurrent stream
|
* UID of the concurrent stream
|
||||||
*/
|
*/
|
||||||
uid: string;
|
uid: string;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,4 +6,4 @@ import type { ConcurrentStream } from './ConcurrentStream';
|
|||||||
|
|
||||||
export type CheckConcurrentStreamResponse = {
|
export type CheckConcurrentStreamResponse = {
|
||||||
stream: ConcurrentStream;
|
stream: ConcurrentStream;
|
||||||
};
|
};
|
||||||
|
|||||||
7
src/api-types/models/CheckSubscriptionRequest.ts
Normal file
7
src/api-types/models/CheckSubscriptionRequest.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
|
export type CheckSubscriptionRequest = {
|
||||||
|
sub_id: string;
|
||||||
|
};
|
||||||
@@ -6,4 +6,4 @@ export type ClearDownloadsRequest = {
|
|||||||
clear_finished?: boolean;
|
clear_finished?: boolean;
|
||||||
clear_paused?: boolean;
|
clear_paused?: boolean;
|
||||||
clear_errors?: boolean;
|
clear_errors?: boolean;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,4 +6,4 @@ export type ConcurrentStream = {
|
|||||||
playback_timestamp?: number;
|
playback_timestamp?: number;
|
||||||
unix_timestamp?: number;
|
unix_timestamp?: number;
|
||||||
playing?: boolean;
|
playing?: boolean;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,4 +4,4 @@
|
|||||||
|
|
||||||
export type Config = {
|
export type Config = {
|
||||||
YoutubeDLMaterial: any;
|
YoutubeDLMaterial: any;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,4 +7,4 @@ import type { Config } from './Config';
|
|||||||
export type ConfigResponse = {
|
export type ConfigResponse = {
|
||||||
config_file: Config;
|
config_file: Config;
|
||||||
success: boolean;
|
success: boolean;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,4 +4,4 @@
|
|||||||
|
|
||||||
export type CreateCategoryRequest = {
|
export type CreateCategoryRequest = {
|
||||||
name: string;
|
name: string;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,4 +7,4 @@ import type { Category } from './Category';
|
|||||||
export type CreateCategoryResponse = {
|
export type CreateCategoryResponse = {
|
||||||
new_category?: Category;
|
new_category?: Category;
|
||||||
success?: boolean;
|
success?: boolean;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,4 +6,4 @@ export type CreatePlaylistRequest = {
|
|||||||
playlistName: string;
|
playlistName: string;
|
||||||
uids: Array<string>;
|
uids: Array<string>;
|
||||||
thumbnailURL: string;
|
thumbnailURL: string;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,4 +7,4 @@ import type { Playlist } from './Playlist';
|
|||||||
export type CreatePlaylistResponse = {
|
export type CreatePlaylistResponse = {
|
||||||
new_playlist: Playlist;
|
new_playlist: Playlist;
|
||||||
success: boolean;
|
success: boolean;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,4 +5,4 @@
|
|||||||
export type CropFileSettings = {
|
export type CropFileSettings = {
|
||||||
cropFileStart: number;
|
cropFileStart: number;
|
||||||
cropFileEnd: number;
|
cropFileEnd: number;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -17,4 +17,4 @@ export namespace DBBackup {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,4 +16,4 @@ roles?: TableInfo;
|
|||||||
download_queue?: TableInfo;
|
download_queue?: TableInfo;
|
||||||
archives?: TableInfo;
|
archives?: TableInfo;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -42,4 +42,4 @@ export type DatabaseFile = {
|
|||||||
*/
|
*/
|
||||||
abr?: number;
|
abr?: number;
|
||||||
favorite: boolean;
|
favorite: boolean;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -11,4 +11,4 @@ export type DeleteAllFilesResponse = {
|
|||||||
* Number of files removed
|
* Number of files removed
|
||||||
*/
|
*/
|
||||||
delete_count?: number;
|
delete_count?: number;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,4 +6,4 @@ import type { Archive } from './Archive';
|
|||||||
|
|
||||||
export type DeleteArchiveItemsRequest = {
|
export type DeleteArchiveItemsRequest = {
|
||||||
archives: Array<Archive>;
|
archives: Array<Archive>;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,4 +4,4 @@
|
|||||||
|
|
||||||
export type DeleteCategoryRequest = {
|
export type DeleteCategoryRequest = {
|
||||||
category_uid: string;
|
category_uid: string;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,4 +5,4 @@
|
|||||||
export type DeleteMp3Mp4Request = {
|
export type DeleteMp3Mp4Request = {
|
||||||
uid: string;
|
uid: string;
|
||||||
blacklistMode?: boolean;
|
blacklistMode?: boolean;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,4 +4,4 @@
|
|||||||
|
|
||||||
export type DeleteNotificationRequest = {
|
export type DeleteNotificationRequest = {
|
||||||
uid: string;
|
uid: string;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,4 +4,4 @@
|
|||||||
|
|
||||||
export type DeletePlaylistRequest = {
|
export type DeletePlaylistRequest = {
|
||||||
playlist_id: string;
|
playlist_id: string;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,4 +8,4 @@ export type DeleteSubscriptionFileRequest = {
|
|||||||
* If true, does not remove id from archive. Only valid if youtube-dl archive is enabled in settings.
|
* If true, does not remove id from archive. Only valid if youtube-dl archive is enabled in settings.
|
||||||
*/
|
*/
|
||||||
deleteForever?: boolean;
|
deleteForever?: boolean;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,4 +4,4 @@
|
|||||||
|
|
||||||
export type DeleteUserRequest = {
|
export type DeleteUserRequest = {
|
||||||
uid: string;
|
uid: string;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ export type Download = {
|
|||||||
running: boolean;
|
running: boolean;
|
||||||
finished: boolean;
|
finished: boolean;
|
||||||
paused: boolean;
|
paused: boolean;
|
||||||
|
cancelled?: boolean;
|
||||||
finished_step: boolean;
|
finished_step: boolean;
|
||||||
url: string;
|
url: string;
|
||||||
type: string;
|
type: string;
|
||||||
@@ -27,4 +28,4 @@ export type Download = {
|
|||||||
sub_id?: string;
|
sub_id?: string;
|
||||||
sub_name?: string;
|
sub_name?: string;
|
||||||
prefetched_info?: any;
|
prefetched_info?: any;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,4 +7,4 @@ import type { FileType } from './FileType';
|
|||||||
export type DownloadArchiveRequest = {
|
export type DownloadArchiveRequest = {
|
||||||
type?: FileType;
|
type?: FileType;
|
||||||
sub_id?: string;
|
sub_id?: string;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -11,4 +11,4 @@ export type DownloadFileRequest = {
|
|||||||
playlist_id?: string;
|
playlist_id?: string;
|
||||||
url?: string;
|
url?: string;
|
||||||
type?: FileType;
|
type?: FileType;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -49,4 +49,4 @@ export type DownloadRequest = {
|
|||||||
* If using youtube-dl archive, download will ignore it
|
* If using youtube-dl archive, download will ignore it
|
||||||
*/
|
*/
|
||||||
ignoreArchive?: boolean;
|
ignoreArchive?: boolean;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,4 +6,4 @@ import type { Download } from './Download';
|
|||||||
|
|
||||||
export type DownloadResponse = {
|
export type DownloadResponse = {
|
||||||
download?: Download;
|
download?: Download;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -20,4 +20,4 @@ export type DownloadTwitchChatByVODIDRequest = {
|
|||||||
*/
|
*/
|
||||||
uuid?: string;
|
uuid?: string;
|
||||||
sub?: Subscription;
|
sub?: Subscription;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,4 +6,4 @@ import type { TwitchChatMessage } from './TwitchChatMessage';
|
|||||||
|
|
||||||
export type DownloadTwitchChatByVODIDResponse = {
|
export type DownloadTwitchChatByVODIDResponse = {
|
||||||
chat: Array<TwitchChatMessage>;
|
chat: Array<TwitchChatMessage>;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,4 +4,4 @@
|
|||||||
|
|
||||||
export type DownloadVideosForSubscriptionRequest = {
|
export type DownloadVideosForSubscriptionRequest = {
|
||||||
subID: string;
|
subID: string;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,4 +5,4 @@
|
|||||||
export enum FileType {
|
export enum FileType {
|
||||||
AUDIO = 'audio',
|
AUDIO = 'audio',
|
||||||
VIDEO = 'video',
|
VIDEO = 'video',
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,4 +6,4 @@ export enum FileTypeFilter {
|
|||||||
AUDIO_ONLY = 'audio_only',
|
AUDIO_ONLY = 'audio_only',
|
||||||
VIDEO_ONLY = 'video_only',
|
VIDEO_ONLY = 'video_only',
|
||||||
BOTH = 'both',
|
BOTH = 'both',
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,4 +4,4 @@
|
|||||||
|
|
||||||
export type GenerateArgsResponse = {
|
export type GenerateArgsResponse = {
|
||||||
args?: Array<string>;
|
args?: Array<string>;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,4 +4,4 @@
|
|||||||
|
|
||||||
export type GenerateNewApiKeyResponse = {
|
export type GenerateNewApiKeyResponse = {
|
||||||
new_api_key: string;
|
new_api_key: string;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,4 +6,4 @@ import type { Category } from './Category';
|
|||||||
|
|
||||||
export type GetAllCategoriesResponse = {
|
export type GetAllCategoriesResponse = {
|
||||||
categories: Array<Category>;
|
categories: Array<Category>;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,4 +7,4 @@ export type GetAllDownloadsRequest = {
|
|||||||
* Filters downloads with the array
|
* Filters downloads with the array
|
||||||
*/
|
*/
|
||||||
uids?: Array<string> | null;
|
uids?: Array<string> | null;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,4 +6,4 @@ import type { Download } from './Download';
|
|||||||
|
|
||||||
export type GetAllDownloadsResponse = {
|
export type GetAllDownloadsResponse = {
|
||||||
downloads?: Array<Download>;
|
downloads?: Array<Download>;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -21,4 +21,4 @@ export type GetAllFilesRequest = {
|
|||||||
* Include if you want to filter by subscription
|
* Include if you want to filter by subscription
|
||||||
*/
|
*/
|
||||||
sub_id?: string;
|
sub_id?: string;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -11,4 +11,4 @@ export type GetAllFilesResponse = {
|
|||||||
* All video playlists
|
* All video playlists
|
||||||
*/
|
*/
|
||||||
playlists: Array<Playlist>;
|
playlists: Array<Playlist>;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,4 +6,4 @@ import type { Subscription } from './Subscription';
|
|||||||
|
|
||||||
export type GetAllSubscriptionsResponse = {
|
export type GetAllSubscriptionsResponse = {
|
||||||
subscriptions: Array<Subscription>;
|
subscriptions: Array<Subscription>;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,4 +6,4 @@ import type { Task } from './Task';
|
|||||||
|
|
||||||
export type GetAllTasksResponse = {
|
export type GetAllTasksResponse = {
|
||||||
tasks?: Array<Task>;
|
tasks?: Array<Task>;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,4 +7,4 @@ import type { FileType } from './FileType';
|
|||||||
export type GetArchivesRequest = {
|
export type GetArchivesRequest = {
|
||||||
type?: FileType;
|
type?: FileType;
|
||||||
sub_id?: string;
|
sub_id?: string;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,4 +6,4 @@ import type { Archive } from './Archive';
|
|||||||
|
|
||||||
export type GetArchivesResponse = {
|
export type GetArchivesResponse = {
|
||||||
archives: Array<Archive>;
|
archives: Array<Archive>;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,4 +6,4 @@ import type { DBBackup } from './DBBackup';
|
|||||||
|
|
||||||
export type GetDBBackupsResponse = {
|
export type GetDBBackupsResponse = {
|
||||||
tasks?: Array<DBBackup>;
|
tasks?: Array<DBBackup>;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,4 +4,4 @@
|
|||||||
|
|
||||||
export type GetDownloadRequest = {
|
export type GetDownloadRequest = {
|
||||||
download_uid: string;
|
download_uid: string;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,4 +6,4 @@ import type { Download } from './Download';
|
|||||||
|
|
||||||
export type GetDownloadResponse = {
|
export type GetDownloadResponse = {
|
||||||
download?: Download;
|
download?: Download;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,4 +4,4 @@
|
|||||||
|
|
||||||
export type GetFileFormatsRequest = {
|
export type GetFileFormatsRequest = {
|
||||||
url?: string;
|
url?: string;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,4 +7,4 @@ export type GetFileFormatsResponse = {
|
|||||||
result: {
|
result: {
|
||||||
formats?: Array<any>;
|
formats?: Array<any>;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -14,4 +14,4 @@ export type GetFileRequest = {
|
|||||||
* User UID
|
* User UID
|
||||||
*/
|
*/
|
||||||
uuid?: string;
|
uuid?: string;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,4 +7,4 @@ import type { DatabaseFile } from './DatabaseFile';
|
|||||||
export type GetFileResponse = {
|
export type GetFileResponse = {
|
||||||
success: boolean;
|
success: boolean;
|
||||||
file?: DatabaseFile;
|
file?: DatabaseFile;
|
||||||
};
|
};
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user