mirror of
https://github.com/Tzahi12345/YoutubeDL-Material.git
synced 2026-03-07 20:10:03 +03:00
Merge branch 'master' of https://github.com/Tzahi12345/YoutubeDL-Material into heroku-fixes-and-repo-cleanup
This commit is contained in:
7
.dockerignore
Normal file
7
.dockerignore
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
.git
|
||||||
|
db
|
||||||
|
appdata
|
||||||
|
audio
|
||||||
|
video
|
||||||
|
subscriptions
|
||||||
|
users
|
||||||
7
.github/ISSUE_TEMPLATE/bug_report.md
vendored
7
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -27,5 +27,12 @@ If applicable, add screenshots to help explain your problem.
|
|||||||
- YoutubeDL-Material version
|
- YoutubeDL-Material version
|
||||||
- Docker tag: <tag> (optional)
|
- Docker tag: <tag> (optional)
|
||||||
|
|
||||||
|
Ideally you'd copy the info as presented on the "About" dialogue
|
||||||
|
in YoutubeDL-Material.
|
||||||
|
(for that, click on the three dots on the top right and then
|
||||||
|
check "installation details". On later versions of YoutubeDL-
|
||||||
|
Material you will find pretty much all the crucial information
|
||||||
|
here that we need in most cases!)
|
||||||
|
|
||||||
**Additional context**
|
**Additional context**
|
||||||
Add any other context about the problem here. For example, a YouTube link.
|
Add any other context about the problem here. For example, a YouTube link.
|
||||||
|
|||||||
6
.github/workflows/build.yml
vendored
6
.github/workflows/build.yml
vendored
@@ -25,10 +25,6 @@ jobs:
|
|||||||
cd backend
|
cd backend
|
||||||
npm install
|
npm install
|
||||||
sudo npm install -g @angular/cli
|
sudo npm install -g @angular/cli
|
||||||
- name: prepare localization
|
|
||||||
run: |
|
|
||||||
sudo npm install -g xliff-to-json
|
|
||||||
xliff-to-json ./src/assets/i18n
|
|
||||||
- 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)"
|
||||||
@@ -43,7 +39,7 @@ jobs:
|
|||||||
json: '{"type": "autobuild", "tag": "N/A", "commit": "${{ steps.vars.outputs.sha_short }}", "date": "${{ steps.date.outputs.date }}"}'
|
json: '{"type": "autobuild", "tag": "N/A", "commit": "${{ steps.vars.outputs.sha_short }}", "date": "${{ steps.date.outputs.date }}"}'
|
||||||
dir: 'backend/'
|
dir: 'backend/'
|
||||||
- name: build
|
- name: build
|
||||||
run: ng build --prod
|
run: npm run build
|
||||||
- name: prepare artifact upload
|
- name: prepare artifact upload
|
||||||
shell: pwsh
|
shell: pwsh
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
27
.github/workflows/docker-pr.yml
vendored
Normal file
27
.github/workflows/docker-pr.yml
vendored
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
name: docker-pr
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches: [master]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-push:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: checkout code
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
- name: Set hash
|
||||||
|
id: vars
|
||||||
|
run: echo "::set-output name=sha_short::$(git rev-parse --short HEAD)"
|
||||||
|
- name: Get current date
|
||||||
|
id: date
|
||||||
|
run: echo "::set-output name=date::$(date +'%Y-%m-%d')"
|
||||||
|
- name: create-json
|
||||||
|
id: create-json
|
||||||
|
uses: jsdaniell/create-json@1.1.2
|
||||||
|
with:
|
||||||
|
name: "version.json"
|
||||||
|
json: '{"type": "docker", "tag": "nightly", "commit": "${{ steps.vars.outputs.sha_short }}", "date": "${{ steps.date.outputs.date }}"}'
|
||||||
|
dir: 'backend/'
|
||||||
|
- name: Build docker images
|
||||||
|
run: docker build . -t tzahi12345/youtubedl-material:nightly-pr
|
||||||
4
.github/workflows/docker-release.yml
vendored
4
.github/workflows/docker-release.yml
vendored
@@ -13,10 +13,6 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: checkout code
|
- name: checkout code
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
- name: prepare localization
|
|
||||||
run: |
|
|
||||||
sudo npm install -g xliff-to-json
|
|
||||||
xliff-to-json ./src/assets/i18n
|
|
||||||
- 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)"
|
||||||
|
|||||||
22
.github/workflows/docker.yml
vendored
22
.github/workflows/docker.yml
vendored
@@ -3,6 +3,16 @@ name: docker
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [master]
|
branches: [master]
|
||||||
|
paths-ignore:
|
||||||
|
- '.github/**'
|
||||||
|
- '.vscode/**'
|
||||||
|
- 'chrome-extension/**'
|
||||||
|
- 'releases/**'
|
||||||
|
- '**/**.md'
|
||||||
|
- '**.crx'
|
||||||
|
- '**.pem'
|
||||||
|
- '.dockerignore'
|
||||||
|
- '.gitignore'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-and-push:
|
build-and-push:
|
||||||
@@ -10,10 +20,6 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: checkout code
|
- name: checkout code
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
- name: prepare localization
|
|
||||||
run: |
|
|
||||||
sudo npm install -g xliff-to-json
|
|
||||||
xliff-to-json ./src/assets/i18n
|
|
||||||
- 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)"
|
||||||
@@ -25,7 +31,7 @@ jobs:
|
|||||||
uses: jsdaniell/create-json@1.1.2
|
uses: jsdaniell/create-json@1.1.2
|
||||||
with:
|
with:
|
||||||
name: "version.json"
|
name: "version.json"
|
||||||
json: '{"type": "docker", "tag": "nightly", "commit": "${{ steps.vars.outputs.sha_short }}", "date": "${{ steps.date.outputs.date }}"}'
|
json: '{"type": "docker", "tag": "${{secrets.DOCKERHUB_MASTER_TAG}}", "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@v1
|
||||||
@@ -43,4 +49,8 @@ jobs:
|
|||||||
file: ./Dockerfile
|
file: ./Dockerfile
|
||||||
platforms: linux/amd64,linux/arm,linux/arm64/v8
|
platforms: linux/amd64,linux/arm,linux/arm64/v8
|
||||||
push: true
|
push: true
|
||||||
tags: tzahi12345/youtubedl-material:nightly
|
# Defaults:
|
||||||
|
# DOCKERHUB_USERNAME : tzahi12345
|
||||||
|
# DOCKERHUB_REPO : youtubedl-material
|
||||||
|
# DOCKERHUB_MASTER_TAG: nightly
|
||||||
|
tags: ${{ secrets.DOCKERHUB_USERNAME }}/${{ secrets.DOCKERHUB_REPO }}:${{secrets.DOCKERHUB_MASTER_TAG}}
|
||||||
|
|||||||
10
.gitignore
vendored
10
.gitignore
vendored
@@ -25,6 +25,7 @@
|
|||||||
!.vscode/extensions.json
|
!.vscode/extensions.json
|
||||||
|
|
||||||
# misc
|
# misc
|
||||||
|
/.angular/cache
|
||||||
/.sass-cache
|
/.sass-cache
|
||||||
/connect.lock
|
/connect.lock
|
||||||
/coverage
|
/coverage
|
||||||
@@ -66,3 +67,12 @@ backend/appdata/users.json
|
|||||||
backend/users/*
|
backend/users/*
|
||||||
backend/appdata/cookies.txt
|
backend/appdata/cookies.txt
|
||||||
backend/public
|
backend/public
|
||||||
|
src/assets/i18n/*.json
|
||||||
|
|
||||||
|
# User Files
|
||||||
|
db/
|
||||||
|
appdata/
|
||||||
|
audio/
|
||||||
|
video/
|
||||||
|
subscriptions/
|
||||||
|
users/
|
||||||
62
Dockerfile
62
Dockerfile
@@ -1,9 +1,26 @@
|
|||||||
FROM alpine:latest as frontend
|
FROM ubuntu:22.04 AS ffmpeg
|
||||||
|
|
||||||
RUN apk add --no-cache \
|
ENV DEBIAN_FRONTEND=noninteractive
|
||||||
npm
|
|
||||||
|
|
||||||
RUN npm install -g @angular/cli
|
COPY docker-build.sh .
|
||||||
|
RUN sh ./docker-build.sh
|
||||||
|
|
||||||
|
#--------------# Stage 2
|
||||||
|
|
||||||
|
FROM ubuntu:22.04 as frontend
|
||||||
|
|
||||||
|
ENV DEBIAN_FRONTEND=noninteractive
|
||||||
|
RUN apt-get update && apt-get -y install \
|
||||||
|
curl \
|
||||||
|
gnupg \
|
||||||
|
# Ubuntu 22.04 ships Node.JS 12 by default :)
|
||||||
|
nodejs \
|
||||||
|
# needed on 21.10 and before, maybe not on 22.04 YARN: brings along npm, solves dependency conflicts,
|
||||||
|
# spares us this spaghetti approach: https://stackoverflow.com/a/60547197
|
||||||
|
npm && \
|
||||||
|
apt-get install -f && \
|
||||||
|
npm config set strict-ssl false && \
|
||||||
|
npm install -g @angular/cli
|
||||||
|
|
||||||
WORKDIR /build
|
WORKDIR /build
|
||||||
COPY [ "package.json", "package-lock.json", "/build/" ]
|
COPY [ "package.json", "package-lock.json", "/build/" ]
|
||||||
@@ -11,38 +28,45 @@ RUN npm install
|
|||||||
|
|
||||||
COPY [ "angular.json", "tsconfig.json", "/build/" ]
|
COPY [ "angular.json", "tsconfig.json", "/build/" ]
|
||||||
COPY [ "src/", "/build/src/" ]
|
COPY [ "src/", "/build/src/" ]
|
||||||
RUN ng build --prod
|
RUN npm run build
|
||||||
|
|
||||||
#--------------#
|
#--------------# Final Stage
|
||||||
|
|
||||||
FROM alpine:latest
|
FROM ubuntu:22.04
|
||||||
|
|
||||||
ENV UID=1000 \
|
ENV UID=1000 \
|
||||||
GID=1000 \
|
GID=1000 \
|
||||||
USER=youtube
|
USER=youtube \
|
||||||
|
NO_UPDATE_NOTIFIER=true
|
||||||
|
|
||||||
ENV NO_UPDATE_NOTIFIER=true
|
ENV DEBIAN_FRONTEND=noninteractive
|
||||||
ENV FOREVER_ROOT=/app/.forever
|
|
||||||
|
|
||||||
RUN addgroup -S $USER -g $GID && adduser -D -S $USER -G $USER -u $UID
|
RUN groupadd -g $GID $USER && useradd --system -g $USER --uid $UID $USER
|
||||||
|
|
||||||
RUN apk add --no-cache \
|
RUN apt-get update && apt-get -y install \
|
||||||
ffmpeg \
|
|
||||||
npm \
|
npm \
|
||||||
python2 \
|
python2 \
|
||||||
python3 \
|
python3 \
|
||||||
su-exec \
|
gosu \
|
||||||
&& apk add --no-cache --repository http://dl-cdn.alpinelinux.org/alpine/edge/testing/ \
|
atomicparsley && \
|
||||||
atomicparsley
|
apt-get install -f && \
|
||||||
|
apt-get autoremove --purge && \
|
||||||
|
apt-get autoremove && \
|
||||||
|
apt-get clean && \
|
||||||
|
rm -rf /var/lib/apt
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
COPY --chown=$UID:$GID --from=ffmpeg [ "/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 [ "backend/package.json", "backend/package-lock.json", "/app/" ]
|
COPY --chown=$UID:$GID [ "backend/package.json", "backend/package-lock.json", "/app/" ]
|
||||||
RUN npm install forever -g
|
ENV PM2_HOME=/app/pm2
|
||||||
RUN npm install && chown -R $UID:$GID ./
|
RUN npm config set strict-ssl false && \
|
||||||
|
npm install pm2 -g && \
|
||||||
|
npm install && chown -R $UID:$GID ./
|
||||||
|
|
||||||
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 [ "/backend/", "/app/" ]
|
COPY --chown=$UID:$GID [ "/backend/", "/app/" ]
|
||||||
|
|
||||||
EXPOSE 17442
|
EXPOSE 17442
|
||||||
ENTRYPOINT [ "/app/entrypoint.sh" ]
|
ENTRYPOINT [ "/app/entrypoint.sh" ]
|
||||||
CMD [ "forever", "app.js" ]
|
CMD [ "pm2-runtime", "pm2.config.js" ]
|
||||||
|
|||||||
2402
Public API v1.yaml
2402
Public API v1.yaml
File diff suppressed because it is too large
Load Diff
26
README.md
26
README.md
@@ -6,10 +6,22 @@
|
|||||||
[](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 11](https://angular.io/) for the frontend, and [Node.js](https://nodejs.org/) on the backend.
|
YoutubeDL-Material is a Material Design frontend for [youtube-dl](https://rg3.github.io/youtube-dl/). It's coded using [Angular 13](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!
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
### USAGE OF THE NIGHTLY BUILDS IS HIGHLY RECOMMENDED.
|
||||||
|
|
||||||
|
For much better scaling with large datasets please run your YTDL-M instance with a MongoDB backend rather than the json file-based default.
|
||||||
|
It will fix a lot of performance problems (especially with datasets in the tens of thousands videos/audios)!
|
||||||
|
The (closed) issues as well as the project's Wiki will give you good starting points for your journey!
|
||||||
|
|
||||||
|
For MongoDB specifically there is [this little guide](https://github.com/Tzahi12345/YoutubeDL-Material/wiki/Setting-a-MongoDB-backend-to-use-as-database-provider-for-YTDL-M).
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
## Getting Started
|
## Getting Started
|
||||||
|
|
||||||
Check out the prerequisites, and go to the installation section. Easy as pie!
|
Check out the prerequisites, and go to the installation section. Easy as pie!
|
||||||
@@ -67,7 +79,7 @@ If you'd like to install YoutubeDL-Material, go to the Installation section. If
|
|||||||
|
|
||||||
To deploy, simply clone the repository, and go into the `youtubedl-material` directory. Type `npm install` and all the dependencies will install. Then type `cd backend` and again type `npm install` to install the dependencies for the backend.
|
To deploy, simply clone the repository, and go into the `youtubedl-material` directory. Type `npm install` and all the dependencies will install. Then type `cd backend` and again type `npm install` to install the dependencies for the backend.
|
||||||
|
|
||||||
Once you do that, you're almost up and running. All you need to do is edit the configuration in `youtubedl-material/appdata`, go back into the `youtubedl-material` directory, and type `ng build --prod`. This will build the app, and put the output files in the `youtubedl-material/backend/public` folder.
|
Once you do that, you're almost up and running. All you need to do is edit the configuration in `youtubedl-material/appdata`, go back into the `youtubedl-material` directory, and type `npm build`. This will build the app, and put the output files in the `youtubedl-material/backend/public` folder.
|
||||||
|
|
||||||
The frontend is now complete. The backend is much easier. Just go into the `backend` folder, and type `npm start`.
|
The frontend is now complete. The backend is much easier. Just go into the `backend` folder, and type `npm start`.
|
||||||
|
|
||||||
@@ -110,6 +122,12 @@ To get started, go to the settings menu and enable the public API from the *Extr
|
|||||||
|
|
||||||
Once you have enabled the API and have the key, you can start sending requests by adding the query param `apiKey=API_KEY`. Replace `API_KEY` with your actual API key, and you should be good to go! Nearly all of the backend should be at your disposal. View available endpoints in the link above.
|
Once you have enabled the API and have the key, you can start sending requests by adding the query param `apiKey=API_KEY`. Replace `API_KEY` with your actual API key, and you should be good to go! Nearly all of the backend should be at your disposal. View available endpoints in the link above.
|
||||||
|
|
||||||
|
## iOS Shortcut
|
||||||
|
|
||||||
|
If you are using iOS, try YoutubeDL-Material more conveniently with a Shortcut. With this Shorcut, you can easily start downloading YouTube video with just two taps! (Or maybe three?)
|
||||||
|
|
||||||
|
You can download Shortcut [here.](https://routinehub.co/shortcut/10283/)
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
If you're interested in contributing, first: awesome! Second, please refer to the guidelines/setup information located in the [Contributing](https://github.com/Tzahi12345/YoutubeDL-Material/wiki/Contributing) wiki page, it's a helpful way to get you on your feet and coding away.
|
If you're interested in contributing, first: awesome! Second, please refer to the guidelines/setup information located in the [Contributing](https://github.com/Tzahi12345/YoutubeDL-Material/wiki/Contributing) wiki page, it's a helpful way to get you on your feet and coding away.
|
||||||
@@ -134,6 +152,10 @@ See also the list of [contributors](https://github.com/Tzahi12345/YoutubeDL-Mate
|
|||||||
|
|
||||||
This project is licensed under the MIT License - see the [LICENSE.md](LICENSE.md) file for details
|
This project is licensed under the MIT License - see the [LICENSE.md](LICENSE.md) file for details
|
||||||
|
|
||||||
|
## Legal Disclaimer
|
||||||
|
|
||||||
|
This project is in no way affiliated with Google LLC, Alphabet Inc. or YouTube (or their subsidiaries) nor endorsed by them.
|
||||||
|
|
||||||
## Acknowledgments
|
## Acknowledgments
|
||||||
|
|
||||||
* youtube-dl
|
* youtube-dl
|
||||||
|
|||||||
21
SECURITY.md
Normal file
21
SECURITY.md
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
# Security Policy
|
||||||
|
|
||||||
|
## Supported Versions
|
||||||
|
|
||||||
|
Currently all work on this project goes into the nightly builds.
|
||||||
|
4.2's RELEASE build is now quite old and should be considered legacy.
|
||||||
|
We urge users to use the nightly releases, because the project
|
||||||
|
constantly sees fixes.
|
||||||
|
|
||||||
|
| Version | Supported |
|
||||||
|
| ------------- | ------------------ |
|
||||||
|
| 4.2 Nightlies | :white_check_mark: |
|
||||||
|
| 4.2 Release | :x: |
|
||||||
|
| < 4.2 | :x: |
|
||||||
|
|
||||||
|
## Reporting a Vulnerability
|
||||||
|
|
||||||
|
Please file an issue in our GitHub's repo, because this app
|
||||||
|
isn't meant to be safe to run as public instance yet, but rather as a LAN facing app.
|
||||||
|
|
||||||
|
We welcome PRs and help in general in making YTDL-M more secure, but it's not a priority as of now.
|
||||||
36
angular.json
36
angular.json
@@ -17,7 +17,6 @@
|
|||||||
"build": {
|
"build": {
|
||||||
"builder": "@angular-devkit/build-angular:browser",
|
"builder": "@angular-devkit/build-angular:browser",
|
||||||
"options": {
|
"options": {
|
||||||
"aot": true,
|
|
||||||
"outputPath": "backend/public",
|
"outputPath": "backend/public",
|
||||||
"index": "src/index.html",
|
"index": "src/index.html",
|
||||||
"main": "src/main.ts",
|
"main": "src/main.ts",
|
||||||
@@ -33,7 +32,17 @@
|
|||||||
"styles": [
|
"styles": [
|
||||||
"src/styles.scss"
|
"src/styles.scss"
|
||||||
],
|
],
|
||||||
"scripts": []
|
"scripts": [],
|
||||||
|
"vendorChunk": true,
|
||||||
|
"extractLicenses": false,
|
||||||
|
"buildOptimizer": false,
|
||||||
|
"sourceMap": true,
|
||||||
|
"optimization": false,
|
||||||
|
"namedChunks": true,
|
||||||
|
"allowedCommonJsDependencies": [
|
||||||
|
"rxjs",
|
||||||
|
"crypto-js"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"configurations": {
|
"configurations": {
|
||||||
"production": {
|
"production": {
|
||||||
@@ -46,7 +55,6 @@
|
|||||||
"optimization": true,
|
"optimization": true,
|
||||||
"outputHashing": "all",
|
"outputHashing": "all",
|
||||||
"namedChunks": false,
|
"namedChunks": false,
|
||||||
"aot": true,
|
|
||||||
"extractLicenses": true,
|
"extractLicenses": true,
|
||||||
"vendorChunk": false,
|
"vendorChunk": false,
|
||||||
"buildOptimizer": true,
|
"buildOptimizer": true,
|
||||||
@@ -60,7 +68,8 @@
|
|||||||
"es": {
|
"es": {
|
||||||
"localize": ["es"]
|
"localize": ["es"]
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"defaultConfiguration": ""
|
||||||
},
|
},
|
||||||
"serve": {
|
"serve": {
|
||||||
"builder": "@angular-devkit/build-angular:dev-server",
|
"builder": "@angular-devkit/build-angular:dev-server",
|
||||||
@@ -152,16 +161,6 @@
|
|||||||
"src/backend"
|
"src/backend"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"lint": {
|
|
||||||
"builder": "@angular-devkit/build-angular:tslint",
|
|
||||||
"options": {
|
|
||||||
"tsConfig": [
|
|
||||||
"src/tsconfig.app.json",
|
|
||||||
"src/tsconfig.spec.json"
|
|
||||||
],
|
|
||||||
"exclude": []
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -176,15 +175,6 @@
|
|||||||
"protractorConfig": "./protractor.conf.js",
|
"protractorConfig": "./protractor.conf.js",
|
||||||
"devServerTarget": "youtube-dl-material:serve"
|
"devServerTarget": "youtube-dl-material:serve"
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"lint": {
|
|
||||||
"builder": "@angular-devkit/build-angular:tslint",
|
|
||||||
"options": {
|
|
||||||
"tsConfig": [
|
|
||||||
"e2e/tsconfig.e2e.json"
|
|
||||||
],
|
|
||||||
"exclude": []
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
375
backend/app.js
375
backend/app.js
@@ -13,7 +13,6 @@ const unzipper = require('unzipper');
|
|||||||
const db_api = require('./db');
|
const db_api = require('./db');
|
||||||
const utils = require('./utils')
|
const utils = require('./utils')
|
||||||
const low = require('lowdb')
|
const low = require('lowdb')
|
||||||
const ProgressBar = require('progress');
|
|
||||||
const fetch = require('node-fetch');
|
const fetch = require('node-fetch');
|
||||||
const URL = require('url').URL;
|
const URL = require('url').URL;
|
||||||
const CONSTS = require('./consts')
|
const CONSTS = require('./consts')
|
||||||
@@ -28,11 +27,11 @@ 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');
|
||||||
|
const tasks_api = require('./tasks');
|
||||||
const subscriptions_api = require('./subscriptions');
|
const subscriptions_api = require('./subscriptions');
|
||||||
const categories_api = require('./categories');
|
const categories_api = require('./categories');
|
||||||
const twitch_api = require('./twitch');
|
const twitch_api = require('./twitch');
|
||||||
|
const youtubedl_api = require('./youtube-dl');
|
||||||
const is_windows = process.platform === 'win32';
|
|
||||||
|
|
||||||
var app = express();
|
var app = express();
|
||||||
|
|
||||||
@@ -60,9 +59,6 @@ const admin_token = '4241b401-7236-493e-92b5-b72696b9d853';
|
|||||||
config_api.initialize();
|
config_api.initialize();
|
||||||
db_api.initialize(db, users_db);
|
db_api.initialize(db, users_db);
|
||||||
auth_api.initialize(db_api);
|
auth_api.initialize(db_api);
|
||||||
downloader_api.initialize(db_api);
|
|
||||||
subscriptions_api.initialize(db_api, downloader_api);
|
|
||||||
categories_api.initialize(db_api);
|
|
||||||
|
|
||||||
// Set some defaults
|
// Set some defaults
|
||||||
db.defaults(
|
db.defaults(
|
||||||
@@ -359,34 +355,6 @@ async function downloadReleaseFiles(tag) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// helper function to download file using fetch
|
|
||||||
async function fetchFile(url, path, file_label) {
|
|
||||||
var len = null;
|
|
||||||
const res = await fetch(url);
|
|
||||||
|
|
||||||
len = parseInt(res.headers.get("Content-Length"), 10);
|
|
||||||
|
|
||||||
var bar = new ProgressBar(` Downloading ${file_label} [:bar] :percent :etas`, {
|
|
||||||
complete: '=',
|
|
||||||
incomplete: ' ',
|
|
||||||
width: 20,
|
|
||||||
total: len
|
|
||||||
});
|
|
||||||
const fileStream = fs.createWriteStream(path);
|
|
||||||
await new Promise((resolve, reject) => {
|
|
||||||
res.body.pipe(fileStream);
|
|
||||||
res.body.on("error", (err) => {
|
|
||||||
reject(err);
|
|
||||||
});
|
|
||||||
res.body.on('data', function (chunk) {
|
|
||||||
bar.tick(chunk.length);
|
|
||||||
});
|
|
||||||
fileStream.on("finish", function() {
|
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function downloadReleaseZip(tag) {
|
async function downloadReleaseZip(tag) {
|
||||||
return new Promise(async resolve => {
|
return new Promise(async resolve => {
|
||||||
// get name of zip file, which depends on the version
|
// get name of zip file, which depends on the version
|
||||||
@@ -397,7 +365,7 @@ async function downloadReleaseZip(tag) {
|
|||||||
let output_path = path.join(__dirname, `youtubedl-material-release-${tag}.zip`);
|
let output_path = path.join(__dirname, `youtubedl-material-release-${tag}.zip`);
|
||||||
|
|
||||||
// download zip from release
|
// download zip from release
|
||||||
await fetchFile(latest_zip_link, output_path, 'update ' + tag);
|
await utils.fetchFile(latest_zip_link, output_path, 'update ' + tag);
|
||||||
resolve(true);
|
resolve(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -568,8 +536,6 @@ async function loadConfig() {
|
|||||||
watchSubscriptionsInterval();
|
watchSubscriptionsInterval();
|
||||||
}
|
}
|
||||||
|
|
||||||
db_api.importUnregisteredFiles();
|
|
||||||
|
|
||||||
// start the server here
|
// start the server here
|
||||||
startServer();
|
startServer();
|
||||||
|
|
||||||
@@ -710,156 +676,8 @@ async function getUrlInfos(url) {
|
|||||||
|
|
||||||
async function startYoutubeDL() {
|
async function startYoutubeDL() {
|
||||||
// auto update youtube-dl
|
// auto update youtube-dl
|
||||||
await autoUpdateYoutubeDL();
|
const update_available = await youtubedl_api.checkForYoutubeDLUpdate();
|
||||||
}
|
if (update_available) await youtubedl_api.updateYoutubeDL(update_available);
|
||||||
|
|
||||||
// auto updates the underlying youtube-dl binary, not YoutubeDL-Material
|
|
||||||
async function autoUpdateYoutubeDL() {
|
|
||||||
const download_sources = {
|
|
||||||
'youtube-dl': {
|
|
||||||
'tags_url': 'https://api.github.com/repos/ytdl-org/youtube-dl/tags',
|
|
||||||
'func': downloadLatestYoutubeDLBinary
|
|
||||||
},
|
|
||||||
'youtube-dlc': {
|
|
||||||
'tags_url': 'https://api.github.com/repos/blackjack4494/yt-dlc/tags',
|
|
||||||
'func': downloadLatestYoutubeDLCBinary
|
|
||||||
},
|
|
||||||
'yt-dlp': {
|
|
||||||
'tags_url': 'https://api.github.com/repos/yt-dlp/yt-dlp/tags',
|
|
||||||
'func': downloadLatestYoutubeDLPBinary
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return new Promise(async resolve => {
|
|
||||||
const default_downloader = config_api.getConfigItem('ytdl_default_downloader');
|
|
||||||
const tags_url = download_sources[default_downloader]['tags_url'];
|
|
||||||
// get current version
|
|
||||||
let current_app_details_exists = fs.existsSync(CONSTS.DETAILS_BIN_PATH);
|
|
||||||
if (!current_app_details_exists) {
|
|
||||||
logger.warn(`Failed to get youtube-dl binary details at location '${CONSTS.DETAILS_BIN_PATH}'. Generating file...`);
|
|
||||||
fs.writeJSONSync(CONSTS.DETAILS_BIN_PATH, {"version":"2020.00.00", "downloader": default_downloader});
|
|
||||||
}
|
|
||||||
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(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// got version, now let's check the latest version from the youtube-dl API
|
|
||||||
|
|
||||||
|
|
||||||
fetch(tags_url, {method: 'Get'})
|
|
||||||
.then(async res => res.json())
|
|
||||||
.then(async (json) => {
|
|
||||||
// check if the versions are different
|
|
||||||
if (!json || !json[0]) {
|
|
||||||
logger.error(`Failed to check ${default_downloader} version for an update.`)
|
|
||||||
resolve(false);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
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
|
|
||||||
logger.info(`Found new update for ${default_downloader}. Updating binary...`);
|
|
||||||
try {
|
|
||||||
await checkExistsWithTimeout(stored_binary_path, 10000);
|
|
||||||
} catch(e) {
|
|
||||||
logger.error(`Failed to update ${default_downloader} - ${e}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
await download_sources[default_downloader]['func'](latest_update_version);
|
|
||||||
|
|
||||||
resolve(true);
|
|
||||||
} else {
|
|
||||||
resolve(false);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
logger.error(`Failed to check ${default_downloader} version for an update.`)
|
|
||||||
logger.error(err);
|
|
||||||
resolve(false);
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function downloadLatestYoutubeDLBinary(new_version) {
|
|
||||||
const file_ext = is_windows ? '.exe' : '';
|
|
||||||
|
|
||||||
const download_url = `https://github.com/ytdl-org/youtube-dl/releases/latest/download/youtube-dl${file_ext}`;
|
|
||||||
const output_path = `node_modules/youtube-dl/bin/youtube-dl${file_ext}`;
|
|
||||||
|
|
||||||
await fetchFile(download_url, output_path, `youtube-dl ${new_version}`);
|
|
||||||
|
|
||||||
updateDetailsJSON(new_version, 'youtube-dl');
|
|
||||||
}
|
|
||||||
|
|
||||||
async function downloadLatestYoutubeDLCBinary(new_version) {
|
|
||||||
const file_ext = is_windows ? '.exe' : '';
|
|
||||||
|
|
||||||
const download_url = `https://github.com/blackjack4494/yt-dlc/releases/latest/download/youtube-dlc${file_ext}`;
|
|
||||||
const output_path = `node_modules/youtube-dl/bin/youtube-dl${file_ext}`;
|
|
||||||
|
|
||||||
await fetchFile(download_url, output_path, `youtube-dlc ${new_version}`);
|
|
||||||
|
|
||||||
updateDetailsJSON(new_version, 'youtube-dlc');
|
|
||||||
}
|
|
||||||
|
|
||||||
async function downloadLatestYoutubeDLPBinary(new_version) {
|
|
||||||
const file_ext = is_windows ? '.exe' : '';
|
|
||||||
|
|
||||||
const download_url = `https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp${file_ext}`;
|
|
||||||
const output_path = `node_modules/youtube-dl/bin/youtube-dl${file_ext}`;
|
|
||||||
|
|
||||||
await fetchFile(download_url, output_path, `yt-dlp ${new_version}`);
|
|
||||||
|
|
||||||
updateDetailsJSON(new_version, 'yt-dlp');
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function checkExistsWithTimeout(filePath, timeout) {
|
|
||||||
return new Promise(function (resolve, reject) {
|
|
||||||
|
|
||||||
var timer = setTimeout(function () {
|
|
||||||
if (watcher) watcher.close();
|
|
||||||
reject(new Error('File did not exists and was not created during the timeout.'));
|
|
||||||
}, timeout);
|
|
||||||
|
|
||||||
fs.access(filePath, fs.constants.R_OK, function (err) {
|
|
||||||
if (!err) {
|
|
||||||
clearTimeout(timer);
|
|
||||||
if (watcher) watcher.close();
|
|
||||||
resolve();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
var dir = path.dirname(filePath);
|
|
||||||
var basename = path.basename(filePath);
|
|
||||||
var watcher = fs.watch(dir, function (eventType, filename) {
|
|
||||||
if (eventType === 'rename' && filename === basename) {
|
|
||||||
clearTimeout(timer);
|
|
||||||
if (watcher) watcher.close();
|
|
||||||
resolve();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
app.use(function(req, res, next) {
|
app.use(function(req, res, next) {
|
||||||
@@ -950,7 +768,7 @@ app.post('/api/restartServer', optionalJwt, (req, res) => {
|
|||||||
res.send({success: true});
|
res.send({success: true});
|
||||||
});
|
});
|
||||||
|
|
||||||
app.post('/api/getDBInfo', optionalJwt, async (req, res) => {
|
app.get('/api/getDBInfo', optionalJwt, async (req, res) => {
|
||||||
const db_info = await db_api.getDBStats();
|
const db_info = await db_api.getDBStats();
|
||||||
res.send({db_info: db_info});
|
res.send({db_info: db_info});
|
||||||
});
|
});
|
||||||
@@ -985,10 +803,11 @@ app.post('/api/testConnectionString', optionalJwt, async (req, res) => {
|
|||||||
app.post('/api/downloadFile', optionalJwt, async function(req, res) {
|
app.post('/api/downloadFile', optionalJwt, async function(req, res) {
|
||||||
req.setTimeout(0); // remove timeout in case of long videos
|
req.setTimeout(0); // remove timeout in case of long videos
|
||||||
const url = req.body.url;
|
const url = req.body.url;
|
||||||
const type = req.body.type;
|
const type = req.body.type ? req.body.type : 'video';
|
||||||
const user_uid = req.isAuthenticated() ? req.user.uid : null;
|
const user_uid = req.isAuthenticated() ? req.user.uid : null;
|
||||||
var options = {
|
const options = {
|
||||||
customArgs: req.body.customArgs,
|
customArgs: req.body.customArgs,
|
||||||
|
additionalArgs: req.body.additionalArgs,
|
||||||
customOutput: req.body.customOutput,
|
customOutput: req.body.customOutput,
|
||||||
selectedHeight: req.body.selectedHeight,
|
selectedHeight: req.body.selectedHeight,
|
||||||
customQualityConfiguration: req.body.customQualityConfiguration,
|
customQualityConfiguration: req.body.customQualityConfiguration,
|
||||||
@@ -996,7 +815,7 @@ app.post('/api/downloadFile', optionalJwt, async function(req, res) {
|
|||||||
youtubePassword: req.body.youtubePassword,
|
youtubePassword: req.body.youtubePassword,
|
||||||
ui_uid: req.body.ui_uid,
|
ui_uid: req.body.ui_uid,
|
||||||
cropFileSettings: req.body.cropFileSettings
|
cropFileSettings: req.body.cropFileSettings
|
||||||
}
|
};
|
||||||
|
|
||||||
const download = await downloader_api.createDownload(url, type, options, user_uid);
|
const download = await downloader_api.createDownload(url, type, options, user_uid);
|
||||||
|
|
||||||
@@ -1012,6 +831,26 @@ app.post('/api/killAllDownloads', optionalJwt, async function(req, res) {
|
|||||||
res.send(result_obj);
|
res.send(result_obj);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
app.post('/api/generateArgs', optionalJwt, async function(req, res) {
|
||||||
|
const url = req.body.url;
|
||||||
|
const type = req.body.type;
|
||||||
|
const user_uid = req.isAuthenticated() ? req.user.uid : null;
|
||||||
|
const options = {
|
||||||
|
customArgs: req.body.customArgs,
|
||||||
|
additionalArgs: req.body.additionalArgs,
|
||||||
|
customOutput: req.body.customOutput,
|
||||||
|
selectedHeight: req.body.selectedHeight,
|
||||||
|
customQualityConfiguration: req.body.customQualityConfiguration,
|
||||||
|
youtubeUsername: req.body.youtubeUsername,
|
||||||
|
youtubePassword: req.body.youtubePassword,
|
||||||
|
ui_uid: req.body.ui_uid,
|
||||||
|
cropFileSettings: req.body.cropFileSettings
|
||||||
|
};
|
||||||
|
|
||||||
|
const args = await downloader_api.generateArgs(url, type, options, user_uid, true);
|
||||||
|
res.send({args: args});
|
||||||
|
});
|
||||||
|
|
||||||
// gets all download mp3s
|
// gets all download mp3s
|
||||||
app.get('/api/getMp3s', optionalJwt, async function(req, res) {
|
app.get('/api/getMp3s', optionalJwt, async function(req, res) {
|
||||||
// TODO: simplify
|
// TODO: simplify
|
||||||
@@ -1327,7 +1166,6 @@ app.post('/api/subscribe', optionalJwt, async (req, res) => {
|
|||||||
let url = req.body.url;
|
let url = req.body.url;
|
||||||
let maxQuality = req.body.maxQuality;
|
let maxQuality = req.body.maxQuality;
|
||||||
let timerange = req.body.timerange;
|
let timerange = req.body.timerange;
|
||||||
let streamingOnly = req.body.streamingOnly;
|
|
||||||
let audioOnly = req.body.audioOnly;
|
let audioOnly = req.body.audioOnly;
|
||||||
let customArgs = req.body.customArgs;
|
let customArgs = req.body.customArgs;
|
||||||
let customOutput = req.body.customFileOutput;
|
let customOutput = req.body.customFileOutput;
|
||||||
@@ -1337,7 +1175,6 @@ app.post('/api/subscribe', optionalJwt, async (req, res) => {
|
|||||||
url: url,
|
url: url,
|
||||||
maxQuality: maxQuality,
|
maxQuality: maxQuality,
|
||||||
id: uuid(),
|
id: uuid(),
|
||||||
streamingOnly: streamingOnly,
|
|
||||||
user_uid: user_uid,
|
user_uid: user_uid,
|
||||||
type: audioOnly ? 'audio' : 'video'
|
type: audioOnly ? 'audio' : 'video'
|
||||||
};
|
};
|
||||||
@@ -1533,7 +1370,7 @@ app.post('/api/getPlaylists', optionalJwt, async (req, res) => {
|
|||||||
const uuid = req.isAuthenticated() ? req.user.uid : null;
|
const uuid = req.isAuthenticated() ? req.user.uid : null;
|
||||||
const include_categories = req.body.include_categories;
|
const include_categories = req.body.include_categories;
|
||||||
|
|
||||||
const playlists = await db_api.getRecords('playlists', {user_uid: uuid});
|
let playlists = await db_api.getRecords('playlists', {user_uid: uuid});
|
||||||
if (include_categories) {
|
if (include_categories) {
|
||||||
const categories = await categories_api.getCategoriesAsPlaylists(files);
|
const categories = await categories_api.getCategoriesAsPlaylists(files);
|
||||||
if (categories) {
|
if (categories) {
|
||||||
@@ -1597,6 +1434,46 @@ app.post('/api/deleteFile', optionalJwt, async (req, res) => {
|
|||||||
res.send(wasDeleted);
|
res.send(wasDeleted);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
app.post('/api/deleteAllFiles', optionalJwt, async (req, res) => {
|
||||||
|
const blacklistMode = false;
|
||||||
|
const uuid = req.isAuthenticated() ? req.user.uid : null;
|
||||||
|
|
||||||
|
let files = null;
|
||||||
|
let text_search = req.body.text_search;
|
||||||
|
let file_type_filter = req.body.file_type_filter;
|
||||||
|
|
||||||
|
const filter_obj = {user_uid: uuid};
|
||||||
|
const regex = true;
|
||||||
|
if (text_search) {
|
||||||
|
if (regex) {
|
||||||
|
filter_obj['title'] = {$regex: `.*${text_search}.*`, $options: 'i'};
|
||||||
|
} else {
|
||||||
|
filter_obj['$text'] = { $search: utils.createEdgeNGrams(text_search) };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (file_type_filter === 'audio_only') filter_obj['isAudio'] = true;
|
||||||
|
else if (file_type_filter === 'video_only') filter_obj['isAudio'] = false;
|
||||||
|
|
||||||
|
files = await db_api.getRecords('files', filter_obj);
|
||||||
|
|
||||||
|
let file_count = await db_api.getRecords('files', filter_obj, true);
|
||||||
|
let delete_count = 0;
|
||||||
|
|
||||||
|
for (let i = 0; i < files.length; i++) {
|
||||||
|
let wasDeleted = false;
|
||||||
|
wasDeleted = await db_api.deleteFile(files[i].uid, uuid, blacklistMode);
|
||||||
|
if (wasDeleted) {
|
||||||
|
delete_count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
res.send({
|
||||||
|
file_count: file_count,
|
||||||
|
delete_count: delete_count
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
app.post('/api/downloadFileFromServer', optionalJwt, async (req, res) => {
|
app.post('/api/downloadFileFromServer', optionalJwt, async (req, res) => {
|
||||||
let uid = req.body.uid;
|
let uid = req.body.uid;
|
||||||
let uuid = req.body.uuid;
|
let uuid = req.body.uuid;
|
||||||
@@ -1619,18 +1496,14 @@ app.post('/api/downloadFileFromServer', optionalJwt, async (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// generate zip
|
// generate zip
|
||||||
file_path_to_download = await utils.createContainerZipFile(playlist, playlist_files_to_download);
|
file_path_to_download = await utils.createContainerZipFile(playlist['name'], playlist_files_to_download);
|
||||||
} else if (sub_id && !uid) {
|
} else if (sub_id && !uid) {
|
||||||
zip_file_generated = true;
|
zip_file_generated = true;
|
||||||
const sub_files_to_download = [];
|
const sub = await db_api.getRecord('subscriptions', {id: sub_id});
|
||||||
const sub = subscriptions_api.getSubscription(sub_id, uuid);
|
const sub_files_to_download = await db_api.getRecords('files', {sub_id: sub_id});
|
||||||
for (let i = 0; i < sub['videos'].length; i++) {
|
|
||||||
const sub_file = sub['videos'][i];
|
|
||||||
sub_files_to_download.push(sub_file);
|
|
||||||
}
|
|
||||||
|
|
||||||
// generate zip
|
// generate zip
|
||||||
file_path_to_download = await utils.createContainerZipFile(sub, sub_files_to_download);
|
file_path_to_download = await utils.createContainerZipFile(sub['name'], sub_files_to_download);
|
||||||
} else {
|
} else {
|
||||||
const file_obj = await db_api.getVideo(uid, uuid, sub_id)
|
const file_obj = await db_api.getVideo(uid, uuid, sub_id)
|
||||||
file_path_to_download = file_obj.path;
|
file_path_to_download = file_obj.path;
|
||||||
@@ -1859,6 +1732,104 @@ app.post('/api/cancelDownload', optionalJwt, async (req, res) => {
|
|||||||
res.send({success: success});
|
res.send({success: success});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// tasks
|
||||||
|
|
||||||
|
app.post('/api/getTasks', optionalJwt, async (req, res) => {
|
||||||
|
const tasks = await db_api.getRecords('tasks');
|
||||||
|
for (let task of tasks) {
|
||||||
|
if (task['schedule']) task['next_invocation'] = tasks_api.TASKS[task['key']]['job'].nextInvocation().getTime();
|
||||||
|
}
|
||||||
|
res.send({tasks: tasks});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/api/resetTasks', optionalJwt, async (req, res) => {
|
||||||
|
const tasks_keys = Object.keys(tasks_api.TASKS);
|
||||||
|
for (let i = 0; i < tasks_keys.length; i++) {
|
||||||
|
const task_key = tasks_keys[i];
|
||||||
|
tasks_api.TASKS[task_key]['job'] = null;
|
||||||
|
}
|
||||||
|
await db_api.removeAllRecords('tasks');
|
||||||
|
await tasks_api.setupTasks();
|
||||||
|
res.send({success: true});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/api/getTask', optionalJwt, async (req, res) => {
|
||||||
|
const task_key = req.body.task_key;
|
||||||
|
const task = await db_api.getRecord('tasks', {key: task_key});
|
||||||
|
if (task['schedule']) task['next_invocation'] = tasks_api.TASKS[task_key]['job'].nextInvocation().getTime();
|
||||||
|
res.send({task: task});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/api/runTask', optionalJwt, async (req, res) => {
|
||||||
|
const task_key = req.body.task_key;
|
||||||
|
const task = await db_api.getRecord('tasks', {key: task_key});
|
||||||
|
|
||||||
|
let success = true;
|
||||||
|
if (task['running'] || task['confirming']) success = false;
|
||||||
|
else await tasks_api.executeRun(task_key);
|
||||||
|
|
||||||
|
res.send({success: success});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/api/confirmTask', optionalJwt, async (req, res) => {
|
||||||
|
const task_key = req.body.task_key;
|
||||||
|
const task = await db_api.getRecord('tasks', {key: task_key});
|
||||||
|
|
||||||
|
let success = true;
|
||||||
|
if (task['running'] || task['confirming'] || !task['data']) success = false;
|
||||||
|
else await tasks_api.executeConfirm(task_key);
|
||||||
|
|
||||||
|
res.send({success: success});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/api/updateTaskSchedule', optionalJwt, async (req, res) => {
|
||||||
|
const task_key = req.body.task_key;
|
||||||
|
const new_schedule = req.body.new_schedule;
|
||||||
|
|
||||||
|
await tasks_api.updateTaskSchedule(task_key, new_schedule);
|
||||||
|
|
||||||
|
res.send({success: true});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/api/updateTaskData', optionalJwt, async (req, res) => {
|
||||||
|
const task_key = req.body.task_key;
|
||||||
|
const new_data = req.body.new_data;
|
||||||
|
|
||||||
|
const success = await db_api.updateRecord('tasks', {key: task_key}, {data: new_data});
|
||||||
|
|
||||||
|
res.send({success: success});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/api/getDBBackups', optionalJwt, async (req, res) => {
|
||||||
|
const backup_dir = path.join('appdata', 'db_backup');
|
||||||
|
const db_backups = [];
|
||||||
|
|
||||||
|
const candidate_backups = await utils.recFindByExt(backup_dir, 'bak', null, [], false);
|
||||||
|
for (let i = 0; i < candidate_backups.length; i++) {
|
||||||
|
const candidate_backup = candidate_backups[i];
|
||||||
|
|
||||||
|
// must have specific format
|
||||||
|
if (candidate_backup.split('.').length - 1 !== 4) continue;
|
||||||
|
|
||||||
|
const candidate_backup_path = candidate_backup;
|
||||||
|
const stats = fs.statSync(candidate_backup_path);
|
||||||
|
|
||||||
|
db_backups.push({ name: path.basename(candidate_backup), timestamp: parseInt(candidate_backup.split('.')[2]), size: stats.size, source: candidate_backup.includes('local') ? 'local' : 'remote' });
|
||||||
|
}
|
||||||
|
|
||||||
|
db_backups.sort((a,b) => b.timestamp - a.timestamp);
|
||||||
|
|
||||||
|
res.send({db_backups: db_backups});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/api/restoreDBBackup', optionalJwt, async (req, res) => {
|
||||||
|
const file_name = req.body.file_name;
|
||||||
|
|
||||||
|
const success = await db_api.restoreDB(file_name);
|
||||||
|
|
||||||
|
res.send({success: success});
|
||||||
|
});
|
||||||
|
|
||||||
// logs management
|
// logs management
|
||||||
|
|
||||||
app.post('/api/logs', optionalJwt, async function(req, res) {
|
app.post('/api/logs', optionalJwt, async function(req, res) {
|
||||||
|
|||||||
@@ -33,7 +33,8 @@
|
|||||||
"use_twitch_API": false,
|
"use_twitch_API": false,
|
||||||
"twitch_API_key": "",
|
"twitch_API_key": "",
|
||||||
"twitch_auto_download_chat": false,
|
"twitch_auto_download_chat": false,
|
||||||
"use_sponsorblock_API": false
|
"use_sponsorblock_API": false,
|
||||||
|
"generate_NFO_files": false
|
||||||
},
|
},
|
||||||
"Themes": {
|
"Themes": {
|
||||||
"default_theme": "default",
|
"default_theme": "default",
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
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 jwt = require('jsonwebtoken');
|
const jwt = require('jsonwebtoken');
|
||||||
const { uuid } = require('uuidv4');
|
const { uuid } = require('uuidv4');
|
||||||
@@ -12,18 +13,24 @@ var JwtStrategy = require('passport-jwt').Strategy,
|
|||||||
ExtractJwt = require('passport-jwt').ExtractJwt;
|
ExtractJwt = require('passport-jwt').ExtractJwt;
|
||||||
|
|
||||||
// other required vars
|
// other required vars
|
||||||
let db_api = null;
|
|
||||||
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 = null;
|
||||||
|
|
||||||
exports.initialize = function(db_api) {
|
exports.initialize = function () {
|
||||||
setDB(db_api);
|
|
||||||
|
|
||||||
/*************************
|
/*************************
|
||||||
* Authentication module
|
* Authentication module
|
||||||
************************/
|
************************/
|
||||||
|
|
||||||
|
if (db_api.database_initialized) {
|
||||||
|
setupRoles();
|
||||||
|
} else {
|
||||||
|
db_api.database_initialized_bs.subscribe(init => {
|
||||||
|
if (init) setupRoles();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
saltRounds = 10;
|
saltRounds = 10;
|
||||||
|
|
||||||
JWT_EXPIRATION = config_api.getConfigItem('ytdl_jwt_expiration');
|
JWT_EXPIRATION = config_api.getConfigItem('ytdl_jwt_expiration');
|
||||||
@@ -51,8 +58,39 @@ exports.initialize = function(db_api) {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
function setDB(input_db_api) {
|
const setupRoles = async () => {
|
||||||
db_api = input_db_api;
|
const required_roles = {
|
||||||
|
admin: {
|
||||||
|
permissions: [
|
||||||
|
'filemanager',
|
||||||
|
'settings',
|
||||||
|
'subscriptions',
|
||||||
|
'sharing',
|
||||||
|
'advanced_download',
|
||||||
|
'downloads_manager'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
user: {
|
||||||
|
permissions: [
|
||||||
|
'filemanager',
|
||||||
|
'subscriptions',
|
||||||
|
'sharing'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const role_keys = Object.keys(required_roles);
|
||||||
|
for (let i = 0; i < role_keys.length; i++) {
|
||||||
|
const role_key = role_keys[i];
|
||||||
|
const role_in_db = await db_api.getRecord('roles', {key: role_key});
|
||||||
|
if (!role_in_db) {
|
||||||
|
// insert task metadata into table if missing
|
||||||
|
await db_api.insertRecordIntoTable('roles', {
|
||||||
|
key: role_key,
|
||||||
|
permissions: required_roles[role_key]['permissions']
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.passport = require('passport');
|
exports.passport = require('passport');
|
||||||
|
|||||||
@@ -1,14 +1,6 @@
|
|||||||
const utils = require('./utils');
|
const utils = require('./utils');
|
||||||
const logger = require('./logger');
|
const logger = require('./logger');
|
||||||
|
const db_api = require('./db');
|
||||||
var db_api = null;
|
|
||||||
|
|
||||||
function setDB(input_db_api) { db_api = input_db_api }
|
|
||||||
|
|
||||||
function initialize(input_db_api) {
|
|
||||||
setDB(input_db_api);
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
||||||
Categories:
|
Categories:
|
||||||
@@ -137,7 +129,6 @@ function applyCategoryRules(file_json, rules, category_name) {
|
|||||||
// }
|
// }
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
initialize: initialize,
|
|
||||||
categorize: categorize,
|
categorize: categorize,
|
||||||
getCategories: getCategories,
|
getCategories: getCategories,
|
||||||
getCategoriesAsPlaylists: getCategoriesAsPlaylists
|
getCategoriesAsPlaylists: getCategoriesAsPlaylists
|
||||||
|
|||||||
@@ -208,7 +208,8 @@ const DEFAULT_CONFIG = {
|
|||||||
"use_twitch_API": false,
|
"use_twitch_API": false,
|
||||||
"twitch_API_key": "",
|
"twitch_API_key": "",
|
||||||
"twitch_auto_download_chat": false,
|
"twitch_auto_download_chat": false,
|
||||||
"use_sponsorblock_API": false
|
"use_sponsorblock_API": false,
|
||||||
|
"generate_NFO_files": false
|
||||||
},
|
},
|
||||||
"Themes": {
|
"Themes": {
|
||||||
"default_theme": "default",
|
"default_theme": "default",
|
||||||
|
|||||||
@@ -114,6 +114,11 @@ exports.CONFIG_ITEMS = {
|
|||||||
'key': 'ytdl_use_sponsorblock_api',
|
'key': 'ytdl_use_sponsorblock_api',
|
||||||
'path': 'YoutubeDLMaterial.API.use_sponsorblock_API'
|
'path': 'YoutubeDLMaterial.API.use_sponsorblock_API'
|
||||||
},
|
},
|
||||||
|
'ytdl_generate_nfo_files': {
|
||||||
|
'key': 'ytdl_generate_nfo_files',
|
||||||
|
'path': 'YoutubeDLMaterial.API.generate_NFO_files'
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
// Themes
|
// Themes
|
||||||
'ytdl_default_theme': {
|
'ytdl_default_theme': {
|
||||||
|
|||||||
132
backend/db.js
132
backend/db.js
@@ -54,6 +54,10 @@ const tables = {
|
|||||||
name: 'download_queue',
|
name: 'download_queue',
|
||||||
primary_key: 'uid'
|
primary_key: 'uid'
|
||||||
},
|
},
|
||||||
|
tasks: {
|
||||||
|
name: 'tasks',
|
||||||
|
primary_key: 'key'
|
||||||
|
},
|
||||||
test: {
|
test: {
|
||||||
name: 'test'
|
name: 'test'
|
||||||
}
|
}
|
||||||
@@ -81,6 +85,7 @@ exports.initialize = (input_db, input_users_db) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
exports.connectToDB = async (retries = 5, no_fallback = false, custom_connection_string = null) => {
|
exports.connectToDB = async (retries = 5, no_fallback = false, custom_connection_string = null) => {
|
||||||
|
using_local_db = config_api.getConfigItem('ytdl_use_local_db'); // verify
|
||||||
if (using_local_db && !custom_connection_string) return;
|
if (using_local_db && !custom_connection_string) return;
|
||||||
const success = await exports._connectToDB(custom_connection_string);
|
const success = await exports._connectToDB(custom_connection_string);
|
||||||
if (success) return true;
|
if (success) return true;
|
||||||
@@ -217,8 +222,7 @@ function generateFileObject(file_path, type) {
|
|||||||
var title = jsonobj.title;
|
var title = jsonobj.title;
|
||||||
var url = jsonobj.webpage_url;
|
var url = jsonobj.webpage_url;
|
||||||
var uploader = jsonobj.uploader;
|
var uploader = jsonobj.uploader;
|
||||||
var upload_date = jsonobj.upload_date;
|
var upload_date = utils.formatDateString(jsonobj.upload_date);
|
||||||
upload_date = upload_date ? `${upload_date.substring(0, 4)}-${upload_date.substring(4, 6)}-${upload_date.substring(6, 8)}` : 'N/A';
|
|
||||||
|
|
||||||
var size = stats.size;
|
var size = stats.size;
|
||||||
|
|
||||||
@@ -301,6 +305,7 @@ exports.getFileDirectoriesAndDBs = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
exports.importUnregisteredFiles = async () => {
|
exports.importUnregisteredFiles = async () => {
|
||||||
|
const imported_files = [];
|
||||||
const dirs_to_check = await exports.getFileDirectoriesAndDBs();
|
const dirs_to_check = await exports.getFileDirectoriesAndDBs();
|
||||||
|
|
||||||
// run through check list and check each file to see if it's missing from the db
|
// run through check list and check each file to see if it's missing from the db
|
||||||
@@ -317,12 +322,17 @@ exports.importUnregisteredFiles = async () => {
|
|||||||
const file_is_registered = !!(files_with_same_url.find(file_with_same_url => path.resolve(file_with_same_url.path) === path.resolve(file.path)));
|
const file_is_registered = !!(files_with_same_url.find(file_with_same_url => path.resolve(file_with_same_url.path) === path.resolve(file.path)));
|
||||||
if (!file_is_registered) {
|
if (!file_is_registered) {
|
||||||
// add additional info
|
// add additional info
|
||||||
await exports.registerFileDB(file['path'], dir_to_check.type, dir_to_check.user_uid, null, dir_to_check.sub_id, null);
|
const file_obj = await exports.registerFileDB(file['path'], dir_to_check.type, dir_to_check.user_uid, null, dir_to_check.sub_id, null);
|
||||||
logger.verbose(`Added discovered file to the database: ${file.id}`);
|
if (file_obj) {
|
||||||
|
imported_files.push(file_obj['uid']);
|
||||||
|
logger.verbose(`Added discovered file to the database: ${file.id}`);
|
||||||
|
} else {
|
||||||
|
logger.error(`Failed to import ${file['path']} automatically.`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return imported_files;
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.addMetadataPropertyToDB = async (property_key) => {
|
exports.addMetadataPropertyToDB = async (property_key) => {
|
||||||
@@ -487,6 +497,7 @@ exports.deleteFile = async (uid, uuid = null, blacklistMode = false) => {
|
|||||||
|
|
||||||
let useYoutubeDLArchive = config_api.getConfigItem('ytdl_use_youtubedl_archive');
|
let useYoutubeDLArchive = config_api.getConfigItem('ytdl_use_youtubedl_archive');
|
||||||
if (useYoutubeDLArchive) {
|
if (useYoutubeDLArchive) {
|
||||||
|
const usersFileFolder = config_api.getConfigItem('ytdl_users_base_path');
|
||||||
const archive_path = uuid ? path.join(usersFileFolder, uuid, 'archives', `archive_${type}.txt`) : path.join('appdata', 'archives', `archive_${type}.txt`);
|
const archive_path = uuid ? path.join(usersFileFolder, uuid, 'archives', `archive_${type}.txt`) : path.join('appdata', 'archives', `archive_${type}.txt`);
|
||||||
|
|
||||||
// get ID from JSON
|
// get ID from JSON
|
||||||
@@ -745,6 +756,66 @@ exports.removeRecord = async (table, filter_obj) => {
|
|||||||
return !!(output['result']['ok']);
|
return !!(output['result']['ok']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// exports.removeRecordsByUIDBulk = async (table, uids) => {
|
||||||
|
// // local db override
|
||||||
|
// if (using_local_db) {
|
||||||
|
// applyFilterLocalDB(local_db.get(table), filter_obj, 'remove').write();
|
||||||
|
// return true;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// const table_collection = database.collection(table);
|
||||||
|
|
||||||
|
// let bulk = table_collection.initializeOrderedBulkOp(); // Initialize the Ordered Batch
|
||||||
|
|
||||||
|
// const item_ids_to_remove =
|
||||||
|
|
||||||
|
// for (let i = 0; i < item_ids_to_update.length; i++) {
|
||||||
|
// const item_id_to_update = item_ids_to_update[i];
|
||||||
|
// bulk.find({[key_label]: item_id_to_update }).updateOne({
|
||||||
|
// "$set": update_obj[item_id_to_update]
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
|
||||||
|
// const output = await bulk.execute();
|
||||||
|
// return !!(output['result']['ok']);
|
||||||
|
// }
|
||||||
|
|
||||||
|
|
||||||
|
exports.findDuplicatesByKey = async (table, key) => {
|
||||||
|
let duplicates = [];
|
||||||
|
if (using_local_db) {
|
||||||
|
// this can probably be optimized
|
||||||
|
const all_records = await exports.getRecords(table);
|
||||||
|
const existing_records = {};
|
||||||
|
for (let i = 0; i < all_records.length; i++) {
|
||||||
|
const record = all_records[i];
|
||||||
|
const value = record[key];
|
||||||
|
|
||||||
|
if (existing_records[value]) {
|
||||||
|
duplicates.push(record);
|
||||||
|
}
|
||||||
|
|
||||||
|
existing_records[value] = true;
|
||||||
|
}
|
||||||
|
return duplicates;
|
||||||
|
}
|
||||||
|
|
||||||
|
const duplicated_values = await database.collection(table).aggregate([
|
||||||
|
{"$group" : { "_id": `$${key}`, "count": { "$sum": 1 } } },
|
||||||
|
{"$match": {"_id" :{ "$ne" : null } , "count" : {"$gt": 1} } },
|
||||||
|
{"$project": {[key] : "$_id", "_id" : 0} }
|
||||||
|
]).toArray();
|
||||||
|
|
||||||
|
for (let i = 0; i < duplicated_values.length; i++) {
|
||||||
|
const duplicated_value = duplicated_values[i];
|
||||||
|
const duplicated_records = await exports.getRecords(table, duplicated_value, false);
|
||||||
|
if (duplicated_records.length > 1) {
|
||||||
|
duplicates = duplicates.concat(duplicated_records.slice(1, duplicated_records.length));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return duplicates;
|
||||||
|
}
|
||||||
|
|
||||||
exports.removeAllRecords = async (table = null, filter_obj = null) => {
|
exports.removeAllRecords = async (table = null, filter_obj = null) => {
|
||||||
// local db override
|
// local db override
|
||||||
const tables_to_remove = table ? [table] : tables_list;
|
const tables_to_remove = table ? [table] : tables_list;
|
||||||
@@ -918,6 +989,52 @@ const createDownloadsRecords = (downloads) => {
|
|||||||
return new_downloads;
|
return new_downloads;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
exports.backupDB = async () => {
|
||||||
|
const backup_dir = path.join('appdata', 'db_backup');
|
||||||
|
fs.ensureDirSync(backup_dir);
|
||||||
|
const backup_file_name = `${using_local_db ? 'local' : 'remote'}_db.json.${Date.now()/1000}.bak`;
|
||||||
|
const path_to_backups = path.join(backup_dir, backup_file_name);
|
||||||
|
|
||||||
|
logger.verbose(`Backing up ${using_local_db ? 'local' : 'remote'} DB to ${path_to_backups}`);
|
||||||
|
|
||||||
|
const table_to_records = {};
|
||||||
|
for (let i = 0; i < tables_list.length; i++) {
|
||||||
|
const table = tables_list[i];
|
||||||
|
table_to_records[table] = await exports.getRecords(table);
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.writeJsonSync(path_to_backups, table_to_records);
|
||||||
|
|
||||||
|
return backup_file_name;
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.restoreDB = async (file_name) => {
|
||||||
|
const path_to_backup = path.join('appdata', 'db_backup', file_name);
|
||||||
|
|
||||||
|
logger.debug('Reading database backup file.');
|
||||||
|
const table_to_records = fs.readJSONSync(path_to_backup);
|
||||||
|
|
||||||
|
if (!table_to_records) {
|
||||||
|
logger.error(`Failed to restore DB! Backup file '${path_to_backup}' could not be read.`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug('Clearing database.');
|
||||||
|
await exports.removeAllRecords();
|
||||||
|
|
||||||
|
logger.debug('Database cleared! Beginning restore.');
|
||||||
|
let success = true;
|
||||||
|
for (let i = 0; i < tables_list.length; i++) {
|
||||||
|
const table = tables_list[i];
|
||||||
|
if (!table_to_records[table] || table_to_records[table].length === 0) continue;
|
||||||
|
success &= await exports.bulkInsertRecordsIntoTable(table, table_to_records[table]);
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug('Restore finished!');
|
||||||
|
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
exports.transferDB = async (local_to_remote) => {
|
exports.transferDB = async (local_to_remote) => {
|
||||||
const table_to_records = {};
|
const table_to_records = {};
|
||||||
for (let i = 0; i < tables_list.length; i++) {
|
for (let i = 0; i < tables_list.length; i++) {
|
||||||
@@ -927,9 +1044,8 @@ exports.transferDB = async (local_to_remote) => {
|
|||||||
|
|
||||||
using_local_db = !local_to_remote;
|
using_local_db = !local_to_remote;
|
||||||
if (local_to_remote) {
|
if (local_to_remote) {
|
||||||
// backup local DB
|
logger.debug('Backup up DB...');
|
||||||
logger.debug('Backup up Local DB...');
|
await exports.backupDB();
|
||||||
await fs.copyFile('appdata/local_db.json', `appdata/local_db.json.${Date.now()/1000}.bak`);
|
|
||||||
const db_connected = await exports.connectToDB(5, true);
|
const db_connected = await exports.connectToDB(5, true);
|
||||||
if (!db_connected) {
|
if (!db_connected) {
|
||||||
logger.error('Failed to transfer database - could not connect to MongoDB. Verify that your connection URL is valid.');
|
logger.error('Failed to transfer database - could not connect to MongoDB. Verify that your connection URL is valid.');
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ const { uuid } = require('uuidv4');
|
|||||||
const path = require('path');
|
const path = require('path');
|
||||||
const mergeFiles = require('merge-files');
|
const mergeFiles = require('merge-files');
|
||||||
const NodeID3 = require('node-id3')
|
const NodeID3 = require('node-id3')
|
||||||
const glob = require('glob')
|
|
||||||
const Mutex = require('async-mutex').Mutex;
|
const Mutex = require('async-mutex').Mutex;
|
||||||
|
|
||||||
const youtubedl = require('youtube-dl');
|
const youtubedl = require('youtube-dl');
|
||||||
@@ -11,28 +10,22 @@ const youtubedl = require('youtube-dl');
|
|||||||
const logger = require('./logger');
|
const logger = require('./logger');
|
||||||
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 categories_api = require('./categories');
|
const categories_api = require('./categories');
|
||||||
const utils = require('./utils');
|
const utils = require('./utils');
|
||||||
|
const db_api = require('./db');
|
||||||
let db_api = null;
|
|
||||||
|
|
||||||
const mutex = new Mutex();
|
const mutex = new Mutex();
|
||||||
let should_check_downloads = true;
|
let should_check_downloads = true;
|
||||||
|
|
||||||
const archivePath = path.join(__dirname, 'appdata', 'archives');
|
const archivePath = path.join(__dirname, 'appdata', 'archives');
|
||||||
|
|
||||||
function setDB(input_db_api) { db_api = input_db_api }
|
if (db_api.database_initialized) {
|
||||||
|
setupDownloads();
|
||||||
exports.initialize = (input_db_api) => {
|
} else {
|
||||||
setDB(input_db_api);
|
db_api.database_initialized_bs.subscribe(init => {
|
||||||
categories_api.initialize(db_api);
|
if (init) setupDownloads();
|
||||||
if (db_api.database_initialized) {
|
});
|
||||||
setupDownloads();
|
|
||||||
} else {
|
|
||||||
db_api.database_initialized_bs.subscribe(init => {
|
|
||||||
if (init) setupDownloads();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.createDownload = async (url, type, options, user_uid = null, sub_id = null, sub_name = null) => {
|
exports.createDownload = async (url, type, options, user_uid = null, sub_id = null, sub_name = null) => {
|
||||||
@@ -278,7 +271,9 @@ async function downloadQueuedFile(download_uid) {
|
|||||||
} else if (output) {
|
} else if (output) {
|
||||||
if (output.length === 0 || output[0].length === 0) {
|
if (output.length === 0 || output[0].length === 0) {
|
||||||
// ERROR!
|
// ERROR!
|
||||||
logger.warn(`No output received for video download, check if it exists in your archive.`)
|
const error_message = `No output received for video download, check if it exists in your archive.`;
|
||||||
|
await handleDownloadError(download_uid, error_message);
|
||||||
|
logger.warn(error_message);
|
||||||
resolve(false);
|
resolve(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -328,6 +323,10 @@ async function downloadQueuedFile(download_uid) {
|
|||||||
if (!success) logger.error('Failed to apply ID3 tag to audio file ' + output_json['_filename']);
|
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) {
|
if (options.cropFileSettings) {
|
||||||
await utils.cropFile(full_file_path, options.cropFileSettings.cropFileStart, options.cropFileSettings.cropFileEnd, ext);
|
await utils.cropFile(full_file_path, options.cropFileSettings.cropFileStart, options.cropFileSettings.cropFileEnd, ext);
|
||||||
}
|
}
|
||||||
@@ -339,9 +338,10 @@ async function downloadQueuedFile(download_uid) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (options.merged_string !== null && options.merged_string !== undefined) {
|
if (options.merged_string !== null && options.merged_string !== undefined) {
|
||||||
let current_merged_archive = fs.readFileSync(path.join(fileFolderPath, `merged_${type}.txt`), 'utf8');
|
const archive_folder = getArchiveFolder(fileFolderPath, options, download['user_uid']);
|
||||||
let diff = current_merged_archive.replace(options.merged_string, '');
|
const current_merged_archive = fs.readFileSync(path.join(archive_folder, `merged_${type}.txt`), 'utf8');
|
||||||
const archive_path = download['user_uid'] ? path.join(fileFolderPath, 'archives', `archive_${type}.txt`) : path.join(archivePath, `archive_${type}.txt`);
|
const diff = current_merged_archive.replace(options.merged_string, '');
|
||||||
|
const archive_path = path.join(archive_folder, `archive_${type}.txt`);
|
||||||
fs.appendFileSync(archive_path, diff);
|
fs.appendFileSync(archive_path, diff);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -369,7 +369,7 @@ async function downloadQueuedFile(download_uid) {
|
|||||||
|
|
||||||
// helper functions
|
// helper functions
|
||||||
|
|
||||||
exports.generateArgs = async (url, type, options, user_uid = null) => {
|
exports.generateArgs = async (url, type, options, user_uid = null, simulated = false) => {
|
||||||
const audioFolderPath = config_api.getConfigItem('ytdl_audio_folder_path');
|
const audioFolderPath = config_api.getConfigItem('ytdl_audio_folder_path');
|
||||||
const videoFolderPath = config_api.getConfigItem('ytdl_video_folder_path');
|
const videoFolderPath = config_api.getConfigItem('ytdl_video_folder_path');
|
||||||
|
|
||||||
@@ -409,7 +409,7 @@ exports.generateArgs = async (url, type, options, user_uid = null) => {
|
|||||||
downloadConfig = customArgs.split(',,');
|
downloadConfig = customArgs.split(',,');
|
||||||
} else {
|
} else {
|
||||||
if (customQualityConfiguration) {
|
if (customQualityConfiguration) {
|
||||||
qualityPath = ['-f', customQualityConfiguration];
|
qualityPath = ['-f', customQualityConfiguration, '--merge-output-format', 'mp4'];
|
||||||
} else if (selectedHeight && selectedHeight !== '' && !is_audio) {
|
} else if (selectedHeight && selectedHeight !== '' && !is_audio) {
|
||||||
qualityPath = ['-f', `'(mp4)[height=${selectedHeight}'`];
|
qualityPath = ['-f', `'(mp4)[height=${selectedHeight}'`];
|
||||||
} else if (is_audio) {
|
} else if (is_audio) {
|
||||||
@@ -450,23 +450,16 @@ exports.generateArgs = async (url, type, options, user_uid = null) => {
|
|||||||
|
|
||||||
let useYoutubeDLArchive = config_api.getConfigItem('ytdl_use_youtubedl_archive');
|
let useYoutubeDLArchive = config_api.getConfigItem('ytdl_use_youtubedl_archive');
|
||||||
if (useYoutubeDLArchive) {
|
if (useYoutubeDLArchive) {
|
||||||
let archive_folder = null;
|
const archive_folder = getArchiveFolder(fileFolderPath, options, user_uid);
|
||||||
if (options.customArchivePath) {
|
|
||||||
archive_folder = path.join(options.customArchivePath);
|
|
||||||
} else if (user_uid) {
|
|
||||||
archive_folder = path.join(fileFolderPath, 'archives');
|
|
||||||
} else {
|
|
||||||
archive_folder = path.join(archivePath);
|
|
||||||
}
|
|
||||||
const archive_path = path.join(archive_folder, `archive_${type}.txt`);
|
const archive_path = path.join(archive_folder, `archive_${type}.txt`);
|
||||||
|
|
||||||
await fs.ensureDir(archive_folder);
|
await fs.ensureDir(archive_folder);
|
||||||
await fs.ensureFile(archive_path);
|
await fs.ensureFile(archive_path);
|
||||||
|
|
||||||
let blacklist_path = path.join(archive_folder, `blacklist_${type}.txt`);
|
const blacklist_path = path.join(archive_folder, `blacklist_${type}.txt`);
|
||||||
await fs.ensureFile(blacklist_path);
|
await fs.ensureFile(blacklist_path);
|
||||||
|
|
||||||
let merged_path = path.join(fileFolderPath, `merged_${type}.txt`);
|
const merged_path = path.join(archive_folder, `merged_${type}.txt`);
|
||||||
await fs.ensureFile(merged_path);
|
await fs.ensureFile(merged_path);
|
||||||
// merges blacklist and regular archive
|
// merges blacklist and regular archive
|
||||||
let inputPathList = [archive_path, blacklist_path];
|
let inputPathList = [archive_path, blacklist_path];
|
||||||
@@ -510,7 +503,7 @@ exports.generateArgs = async (url, type, options, user_uid = null) => {
|
|||||||
// filter out incompatible args
|
// filter out incompatible args
|
||||||
downloadConfig = filterArgs(downloadConfig, is_audio);
|
downloadConfig = filterArgs(downloadConfig, is_audio);
|
||||||
|
|
||||||
logger.verbose(`youtube-dl args being used: ${downloadConfig.join(',')}`);
|
if (!simulated) logger.verbose(`youtube-dl args being used: ${downloadConfig.join(',')}`);
|
||||||
return downloadConfig;
|
return downloadConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -589,18 +582,49 @@ async function checkDownloadPercent(download_uid) {
|
|||||||
if (!resulting_file_size) return;
|
if (!resulting_file_size) return;
|
||||||
|
|
||||||
let sum_size = 0;
|
let sum_size = 0;
|
||||||
glob(`{${files_to_check_for_progress.join(',')}, }*`, async (err, files) => {
|
for (let i = 0; i < files_to_check_for_progress.length; i++) {
|
||||||
files.forEach(async file => {
|
const file_to_check_for_progress = files_to_check_for_progress[i];
|
||||||
try {
|
const dir = path.dirname(file_to_check_for_progress);
|
||||||
const file_stats = fs.statSync(file);
|
if (!fs.existsSync(dir)) continue;
|
||||||
if (file_stats && file_stats.size) {
|
fs.readdir(dir, async (err, files) => {
|
||||||
sum_size += file_stats.size;
|
for (let j = 0; j < files.length; j++) {
|
||||||
}
|
const file = files[j];
|
||||||
} catch (e) {
|
if (!file.includes(path.basename(file_to_check_for_progress))) continue;
|
||||||
|
try {
|
||||||
|
const file_stats = fs.statSync(path.join(dir, file));
|
||||||
|
if (file_stats && file_stats.size) {
|
||||||
|
sum_size += file_stats.size;
|
||||||
|
}
|
||||||
|
} catch (e) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const percent_complete = (sum_size/resulting_file_size * 100).toFixed(2);
|
||||||
|
await db_api.updateRecord('download_queue', {uid: download_uid}, {percent_complete: percent_complete});
|
||||||
});
|
});
|
||||||
const percent_complete = (sum_size/resulting_file_size * 100).toFixed(2);
|
}
|
||||||
await db_api.updateRecord('download_queue', {uid: download_uid}, {percent_complete: percent_complete});
|
}
|
||||||
});
|
|
||||||
|
exports.generateNFOFile = (info, output_path) => {
|
||||||
|
const nfo_obj = {
|
||||||
|
episodedetails: {
|
||||||
|
title: info['fulltitle'],
|
||||||
|
episode: info['playlist_index'] ? info['playlist_index'] : undefined,
|
||||||
|
premiered: utils.formatDateString(info['upload_date']),
|
||||||
|
plot: `${info['uploader_url']}\n${info['description']}\n${info['playlist_title'] ? info['playlist_title'] : ''}`,
|
||||||
|
director: info['artist'] ? info['artist'] : info['uploader']
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const doc = create(nfo_obj);
|
||||||
|
const xml = doc.end({ prettyPrint: true });
|
||||||
|
fs.writeFileSync(output_path, xml);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getArchiveFolder(fileFolderPath, options, user_uid) {
|
||||||
|
if (options.customArchivePath) {
|
||||||
|
return path.join(options.customArchivePath);
|
||||||
|
} else if (user_uid) {
|
||||||
|
return path.join(fileFolderPath, 'archives');
|
||||||
|
} else {
|
||||||
|
return path.join(archivePath);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
8
backend/ecosystem.config.js
Normal file
8
backend/ecosystem.config.js
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
module.exports = {
|
||||||
|
apps : [{
|
||||||
|
name : "YoutubeDL-Material",
|
||||||
|
script : "./app.js",
|
||||||
|
watch : "placeholder",
|
||||||
|
watch_delay: 5000
|
||||||
|
}]
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
set -eu
|
set -eu
|
||||||
|
|
||||||
CMD="forever app.js"
|
CMD="pm2-runtime pm2.config.js"
|
||||||
|
|
||||||
# if the first arg starts with "-" pass it to program
|
# if the first arg starts with "-" pass it to program
|
||||||
if [ "${1#-}" != "$1" ]; then
|
if [ "${1#-}" != "$1" ]; then
|
||||||
@@ -11,7 +11,7 @@ fi
|
|||||||
# chown current working directory to current user
|
# chown current working directory to current user
|
||||||
if [ "$*" = "$CMD" ] && [ "$(id -u)" = "0" ]; then
|
if [ "$*" = "$CMD" ] && [ "$(id -u)" = "0" ]; then
|
||||||
find . \! -user "$UID" -exec chown "$UID:$GID" -R '{}' + || echo "WARNING! Could not change directory ownership. If you manage permissions externally this is fine, otherwise you may experience issues when downloading or deleting videos."
|
find . \! -user "$UID" -exec chown "$UID:$GID" -R '{}' + || echo "WARNING! Could not change directory ownership. If you manage permissions externally this is fine, otherwise you may experience issues when downloading or deleting videos."
|
||||||
exec su-exec "$UID:$GID" "$0" "$@"
|
exec gosu "$UID:$GID" "$0" "$@"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
exec "$@"
|
exec "$@"
|
||||||
|
|||||||
58
backend/fix-scripts/001-fix_download_permissions.sh
Normal file
58
backend/fix-scripts/001-fix_download_permissions.sh
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
# INTERACTIVE PERMISSIONS FIX SCRIPT FOR YTDL-M
|
||||||
|
# Date: 2022-05-03
|
||||||
|
|
||||||
|
# If you want to run this script on a bare-metal installation instead of within Docker
|
||||||
|
# make sure that the paths configured below match your paths! (it's wise to use the full paths)
|
||||||
|
# USAGE: within your container's bash shell:
|
||||||
|
# chmod -R +x ./fix-scripts/
|
||||||
|
# ./fix-scripts/001-fix_download_permissions.sh
|
||||||
|
|
||||||
|
# User defines / Docker env defaults
|
||||||
|
PATH_SUBS=/app/subscriptions
|
||||||
|
PATH_AUDIO=/app/audio
|
||||||
|
PATH_VIDS=/app/video
|
||||||
|
|
||||||
|
clear -x
|
||||||
|
echo "\n"
|
||||||
|
printf '%*s\n' "${COLUMNS:-$(tput cols)}" '' | tr ' ' - # horizontal line
|
||||||
|
echo "Welcome to the INTERACTIVE PERMISSIONS FIX SCRIPT FOR YTDL-M."
|
||||||
|
echo "This script will set YTDL-M's download paths' owner to ${USER} (${UID}:${GID})"
|
||||||
|
echo "and permissions to the default of 644."
|
||||||
|
printf '%*s\n' "${COLUMNS:-$(tput cols)}" '' | tr ' ' - # horizontal line
|
||||||
|
echo "\n"
|
||||||
|
|
||||||
|
# check whether dirs exist
|
||||||
|
i=0
|
||||||
|
[ -d $PATH_SUBS ] && i=$((i+1)) && echo "✔ (${i}/3) Found Subscriptions directory at ${PATH_SUBS}"
|
||||||
|
[ -d $PATH_AUDIO ] && i=$((i+1)) && echo "✔ (${i}/3) Found Audio directory at ${PATH_AUDIO}"
|
||||||
|
[ -d $PATH_VIDS ] && i=$((i+1)) && echo "✔ (${i}/3) Found Video directory at ${PATH_VIDS}"
|
||||||
|
|
||||||
|
# Ask to proceed or cancel, exit on missing paths
|
||||||
|
case $i in
|
||||||
|
0)
|
||||||
|
echo "\nCouldn't find any download path to fix permissions for! \nPlease edit this script to configure!"
|
||||||
|
exit 2;;
|
||||||
|
3)
|
||||||
|
echo "\nFound all download paths to fix permissions for. \nProceed? (Y/N)";;
|
||||||
|
*)
|
||||||
|
echo "\nOnly found ${i} out of 3 download paths! Something about this script's config must be wrong. \nProceed anyways? (Y/N)";;
|
||||||
|
esac
|
||||||
|
old_stty_cfg=$(stty -g)
|
||||||
|
stty raw -echo ; answer=$(head -c 1) ; stty $old_stty_cfg # Careful playing with stty
|
||||||
|
if echo "$answer" | grep -iq "^y" ;then
|
||||||
|
echo "\n Running jobs now... (this may take a while)\n"
|
||||||
|
[ -d $PATH_SUBS ] && chown "$UID:$GID" -R $PATH_SUBS && echo "✔ Set owner of ${PATH_SUBS} to ${USER}."
|
||||||
|
[ -d $PATH_SUBS ] && chmod 644 -R $PATH_SUBS && echo "✔ Set permissions of ${PATH_SUBS} to 644."
|
||||||
|
[ -d $PATH_AUDIO ] && chown "$UID:$GID" -R $PATH_AUDIO && echo "✔ Set owner of ${PATH_AUDIO} to ${USER}."
|
||||||
|
[ -d $PATH_AUDIO ] && chmod 644 -R $PATH_AUDIO && echo "✔ Set permissions of ${PATH_AUDIO} to 644."
|
||||||
|
[ -d $PATH_VIDS ] && chown "$UID:$GID" -R $PATH_VIDS && echo "✔ Set owner of ${PATH_VIDS} to ${USER}."
|
||||||
|
[ -d $PATH_VIDS ] && chmod 644 -R $PATH_VIDS && echo "✔ Set permissions of ${PATH_VIDS} to 644."
|
||||||
|
echo "\n✔ Done."
|
||||||
|
echo "\n If you noticed file access errors those MAY be due to currently running downloads."
|
||||||
|
echo " Feel free to re-run this script, however download parts should have correct file permissions anyhow. :)"
|
||||||
|
exit
|
||||||
|
else
|
||||||
|
echo "\nOkay, bye."
|
||||||
|
fi
|
||||||
1282
backend/package-lock.json
generated
1282
backend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -30,33 +30,32 @@
|
|||||||
},
|
},
|
||||||
"homepage": "",
|
"homepage": "",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"archiver": "^3.1.1",
|
"archiver": "^5.3.1",
|
||||||
"async": "^3.1.0",
|
"async": "^3.2.3",
|
||||||
"async-mutex": "^0.3.1",
|
"async-mutex": "^0.3.1",
|
||||||
"axios": "^0.21.1",
|
"axios": "^0.21.2",
|
||||||
"bcryptjs": "^2.4.0",
|
"bcryptjs": "^2.4.0",
|
||||||
"compression": "^1.7.4",
|
"compression": "^1.7.4",
|
||||||
"config": "^3.2.3",
|
"config": "^3.2.3",
|
||||||
"exe": "^1.0.2",
|
"express": "^4.17.3",
|
||||||
"express": "^4.17.1",
|
|
||||||
"fluent-ffmpeg": "^2.1.2",
|
"fluent-ffmpeg": "^2.1.2",
|
||||||
"fs-extra": "^9.0.0",
|
"fs-extra": "^9.0.0",
|
||||||
"glob": "^7.1.6",
|
|
||||||
"jsonwebtoken": "^8.5.1",
|
"jsonwebtoken": "^8.5.1",
|
||||||
"lowdb": "^1.0.0",
|
"lowdb": "^1.0.0",
|
||||||
"md5": "^2.2.1",
|
"md5": "^2.2.1",
|
||||||
"merge-files": "^0.1.2",
|
"merge-files": "^0.1.2",
|
||||||
"mocha": "^8.4.0",
|
"mocha": "^9.2.2",
|
||||||
"moment": "^2.29.1",
|
"moment": "^2.29.2",
|
||||||
"mongodb": "^3.6.9",
|
"mongodb": "^3.6.9",
|
||||||
"multer": "^1.4.2",
|
"multer": "^1.4.2",
|
||||||
"node-fetch": "^2.6.1",
|
"node-fetch": "^2.6.7",
|
||||||
"node-id3": "^0.1.14",
|
"node-id3": "^0.1.14",
|
||||||
|
"node-schedule": "^2.1.0",
|
||||||
"nodemon": "^2.0.7",
|
"nodemon": "^2.0.7",
|
||||||
"passport": "^0.4.1",
|
"passport": "^0.4.1",
|
||||||
"passport-http": "^0.3.0",
|
"passport-http": "^0.3.0",
|
||||||
"passport-jwt": "^4.0.0",
|
"passport-jwt": "^4.0.0",
|
||||||
"passport-ldapauth": "^2.1.4",
|
"passport-ldapauth": "^3.0.1",
|
||||||
"passport-local": "^1.0.0",
|
"passport-local": "^1.0.0",
|
||||||
"progress": "^2.0.3",
|
"progress": "^2.0.3",
|
||||||
"ps-node": "^0.1.6",
|
"ps-node": "^0.1.6",
|
||||||
@@ -65,7 +64,8 @@
|
|||||||
"shortid": "^2.2.15",
|
"shortid": "^2.2.15",
|
||||||
"unzipper": "^0.10.10",
|
"unzipper": "^0.10.10",
|
||||||
"uuidv4": "^6.0.6",
|
"uuidv4": "^6.0.6",
|
||||||
"winston": "^3.2.1",
|
"winston": "^3.7.2",
|
||||||
|
"xmlbuilder2": "^3.0.2",
|
||||||
"youtube-dl": "^3.0.2"
|
"youtube-dl": "^3.0.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
9
backend/pm2.config.js
Normal file
9
backend/pm2.config.js
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
module.exports = {
|
||||||
|
apps : [{
|
||||||
|
name : "YoutubeDL-Material",
|
||||||
|
script : "./app.js",
|
||||||
|
watch : "placeholder",
|
||||||
|
out_file: "/dev/null",
|
||||||
|
error_file: "/dev/null"
|
||||||
|
}]
|
||||||
|
}
|
||||||
@@ -8,15 +8,8 @@ const logger = require('./logger');
|
|||||||
|
|
||||||
const debugMode = process.env.YTDL_MODE === 'debug';
|
const debugMode = process.env.YTDL_MODE === 'debug';
|
||||||
|
|
||||||
let db_api = null;
|
const db_api = require('./db');
|
||||||
let downloader_api = null;
|
const downloader_api = require('./downloader');
|
||||||
|
|
||||||
function setDB(input_db_api) { db_api = input_db_api }
|
|
||||||
|
|
||||||
function initialize(input_db_api, input_downloader_api) {
|
|
||||||
setDB(input_db_api);
|
|
||||||
downloader_api = input_downloader_api;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function subscribe(sub, user_uid = null) {
|
async function subscribe(sub, user_uid = null) {
|
||||||
const result_obj = {
|
const result_obj = {
|
||||||
@@ -148,6 +141,7 @@ async function unsubscribe(sub, deleteMode, user_uid = null) {
|
|||||||
if (sub.archive && (await fs.pathExists(sub.archive))) {
|
if (sub.archive && (await fs.pathExists(sub.archive))) {
|
||||||
const archive_file_path = path.join(sub.archive, 'archive.txt');
|
const archive_file_path = path.join(sub.archive, 'archive.txt');
|
||||||
// deletes archive if it exists
|
// deletes archive if it exists
|
||||||
|
// TODO: Keep entries in blacklist_video.txt by moving them to a global blacklist
|
||||||
if (await fs.pathExists(archive_file_path)) {
|
if (await fs.pathExists(archive_file_path)) {
|
||||||
await fs.unlink(archive_file_path);
|
await fs.unlink(archive_file_path);
|
||||||
}
|
}
|
||||||
@@ -273,11 +267,17 @@ async function getVideosForSub(sub, user_uid = null) {
|
|||||||
}
|
}
|
||||||
resolve(false);
|
resolve(false);
|
||||||
} else if (output) {
|
} else if (output) {
|
||||||
|
if (config_api.getConfigItem('ytdl_subscriptions_redownload_fresh_uploads')) {
|
||||||
|
await setFreshUploads(sub, user_uid);
|
||||||
|
checkVideosForFreshUploads(sub, user_uid);
|
||||||
|
}
|
||||||
|
|
||||||
if (output.length === 0 || (output.length === 1 && output[0] === '')) {
|
if (output.length === 0 || (output.length === 1 && output[0] === '')) {
|
||||||
logger.verbose('No additional videos to download for ' + sub.name);
|
logger.verbose('No additional videos to download for ' + sub.name);
|
||||||
resolve(true);
|
resolve(true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const output_jsons = [];
|
const output_jsons = [];
|
||||||
for (let i = 0; i < output.length; i++) {
|
for (let i = 0; i < output.length; i++) {
|
||||||
let output_json = null;
|
let output_json = null;
|
||||||
@@ -301,14 +301,7 @@ async function getVideosForSub(sub, user_uid = null) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
resolve(files_to_download);
|
resolve(files_to_download);
|
||||||
|
}
|
||||||
if (config_api.getConfigItem('ytdl_subscriptions_redownload_fresh_uploads')) {
|
|
||||||
await setFreshUploads(sub, user_uid);
|
|
||||||
checkVideosForFreshUploads(sub, user_uid);
|
|
||||||
}
|
|
||||||
|
|
||||||
resolve(true);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}, err => {
|
}, err => {
|
||||||
logger.error(err);
|
logger.error(err);
|
||||||
@@ -350,11 +343,11 @@ async function generateArgsForSubscription(sub, user_uid, redownload = false, de
|
|||||||
|
|
||||||
const file_output = config_api.getConfigItem('ytdl_default_file_output') ? config_api.getConfigItem('ytdl_default_file_output') : '%(title)s';
|
const file_output = config_api.getConfigItem('ytdl_default_file_output') ? config_api.getConfigItem('ytdl_default_file_output') : '%(title)s';
|
||||||
|
|
||||||
let fullOutput = `${appendedBasePath}/${file_output}.%(ext)s`;
|
let fullOutput = `"${appendedBasePath}/${file_output}.%(ext)s"`;
|
||||||
if (desired_path) {
|
if (desired_path) {
|
||||||
fullOutput = `${desired_path}.%(ext)s`;
|
fullOutput = `"${desired_path}.%(ext)s"`;
|
||||||
} else if (sub.custom_output) {
|
} else if (sub.custom_output) {
|
||||||
fullOutput = `${appendedBasePath}/${sub.custom_output}.%(ext)s`;
|
fullOutput = `"${appendedBasePath}/${sub.custom_output}.%(ext)s"`;
|
||||||
}
|
}
|
||||||
|
|
||||||
let downloadConfig = ['--dump-json', '-o', fullOutput, !redownload ? '-ciw' : '-ci', '--write-info-json', '--print-json'];
|
let downloadConfig = ['--dump-json', '-o', fullOutput, !redownload ? '-ciw' : '-ci', '--write-info-json', '--print-json'];
|
||||||
@@ -387,7 +380,11 @@ async function generateArgsForSubscription(sub, user_uid, redownload = false, de
|
|||||||
if (useArchive && !redownload) {
|
if (useArchive && !redownload) {
|
||||||
if (sub.archive) {
|
if (sub.archive) {
|
||||||
archive_dir = sub.archive;
|
archive_dir = sub.archive;
|
||||||
archive_path = path.join(archive_dir, 'archive.txt')
|
if (sub.type && sub.type === 'audio') {
|
||||||
|
archive_path = path.join(archive_dir, 'merged_audio.txt');
|
||||||
|
} else {
|
||||||
|
archive_path = path.join(archive_dir, 'merged_video.txt');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
downloadConfig.push('--download-archive', archive_path);
|
downloadConfig.push('--download-archive', archive_path);
|
||||||
}
|
}
|
||||||
@@ -431,7 +428,7 @@ async function getFilesToDownload(sub, output_jsons) {
|
|||||||
const files_to_download = [];
|
const files_to_download = [];
|
||||||
for (let i = 0; i < output_jsons.length; i++) {
|
for (let i = 0; i < output_jsons.length; i++) {
|
||||||
const output_json = output_jsons[i];
|
const output_json = output_jsons[i];
|
||||||
const file_missing = !(await db_api.getRecord('files', {sub_id: sub.id, url: output_json['webpage_url']})) && !(await db_api.getRecord('download_queue', {sub_id: sub.id, url: output_json['webpage_url'], error: null}));
|
const file_missing = !(await db_api.getRecord('files', {sub_id: sub.id, url: output_json['webpage_url']})) && !(await db_api.getRecord('download_queue', {sub_id: sub.id, url: output_json['webpage_url'], error: null, finished: false}));
|
||||||
if (file_missing) {
|
if (file_missing) {
|
||||||
const file_with_path_exists = await db_api.getRecord('files', {sub_id: sub.id, path: output_json['_filename']});
|
const file_with_path_exists = await db_api.getRecord('files', {sub_id: sub.id, path: output_json['_filename']});
|
||||||
if (file_with_path_exists) {
|
if (file_with_path_exists) {
|
||||||
@@ -480,22 +477,24 @@ async function updateSubscriptionProperty(sub, assignment_obj) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function setFreshUploads(sub, user_uid) {
|
async function setFreshUploads(sub) {
|
||||||
|
const sub_files = await db_api.getRecords('files', {sub_id: sub.id});
|
||||||
const current_date = new Date().toISOString().split('T')[0].replace(/-/g, '');
|
const current_date = new Date().toISOString().split('T')[0].replace(/-/g, '');
|
||||||
sub.videos.forEach(async video => {
|
sub_files.forEach(async file => {
|
||||||
if (current_date === video['upload_date'].replace(/-/g, '')) {
|
if (current_date === file['upload_date'].replace(/-/g, '')) {
|
||||||
// set upload as fresh
|
// set upload as fresh
|
||||||
const video_uid = video['uid'];
|
const file_uid = file['uid'];
|
||||||
await db_api.setVideoProperty(video_uid, {'fresh_upload': true}, user_uid, sub['id']);
|
await db_api.setVideoProperty(file_uid, {'fresh_upload': true});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function checkVideosForFreshUploads(sub, user_uid) {
|
async function checkVideosForFreshUploads(sub, user_uid) {
|
||||||
|
const sub_files = await db_api.getRecords('files', {sub_id: sub.id});
|
||||||
const current_date = new Date().toISOString().split('T')[0].replace(/-/g, '');
|
const current_date = new Date().toISOString().split('T')[0].replace(/-/g, '');
|
||||||
sub.videos.forEach(async video => {
|
sub_files.forEach(async file => {
|
||||||
if (video['fresh_upload'] && current_date > video['upload_date'].replace(/-/g, '')) {
|
if (file['fresh_upload'] && current_date > file['upload_date'].replace(/-/g, '')) {
|
||||||
await checkVideoIfBetterExists(video, sub, user_uid)
|
await checkVideoIfBetterExists(file, sub, user_uid)
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -517,13 +516,13 @@ async function checkVideoIfBetterExists(file_obj, sub, user_uid) {
|
|||||||
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 (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 ${output[metric_to_compare]}`);
|
||||||
await db_api.setVideoProperty(file_obj['uid'], {[metric_to_compare]: output[metric_to_compare]}, user_uid, sub['id']);
|
await db_api.setVideoProperty(file_obj['uid'], {[metric_to_compare]: output[metric_to_compare]});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
await db_api.setVideoProperty(file_obj['uid'], {'fresh_upload': false}, user_uid, sub['id']);
|
await db_api.setVideoProperty(file_obj['uid'], {'fresh_upload': false});
|
||||||
}
|
}
|
||||||
|
|
||||||
// helper functions
|
// helper functions
|
||||||
@@ -542,7 +541,6 @@ module.exports = {
|
|||||||
unsubscribe : unsubscribe,
|
unsubscribe : unsubscribe,
|
||||||
deleteSubscriptionFile : deleteSubscriptionFile,
|
deleteSubscriptionFile : deleteSubscriptionFile,
|
||||||
getVideosForSub : getVideosForSub,
|
getVideosForSub : getVideosForSub,
|
||||||
initialize : initialize,
|
|
||||||
updateSubscriptionPropertyMultiple : updateSubscriptionPropertyMultiple,
|
updateSubscriptionPropertyMultiple : updateSubscriptionPropertyMultiple,
|
||||||
generateOptionsForSubscriptionDownload: generateOptionsForSubscriptionDownload
|
generateOptionsForSubscriptionDownload: generateOptionsForSubscriptionDownload
|
||||||
}
|
}
|
||||||
|
|||||||
195
backend/tasks.js
Normal file
195
backend/tasks.js
Normal file
@@ -0,0 +1,195 @@
|
|||||||
|
const db_api = require('./db');
|
||||||
|
const youtubedl_api = require('./youtube-dl');
|
||||||
|
|
||||||
|
const fs = require('fs-extra');
|
||||||
|
const logger = require('./logger');
|
||||||
|
const scheduler = require('node-schedule');
|
||||||
|
|
||||||
|
const TASKS = {
|
||||||
|
backup_local_db: {
|
||||||
|
run: db_api.backupDB,
|
||||||
|
title: 'Backup DB',
|
||||||
|
job: null
|
||||||
|
},
|
||||||
|
missing_files_check: {
|
||||||
|
run: checkForMissingFiles,
|
||||||
|
confirm: deleteMissingFiles,
|
||||||
|
title: 'Missing files check',
|
||||||
|
job: null
|
||||||
|
},
|
||||||
|
missing_db_records: {
|
||||||
|
run: db_api.importUnregisteredFiles,
|
||||||
|
title: 'Import missing DB records',
|
||||||
|
job: null
|
||||||
|
},
|
||||||
|
duplicate_files_check: {
|
||||||
|
run: checkForDuplicateFiles,
|
||||||
|
confirm: removeDuplicates,
|
||||||
|
title: 'Find duplicate files in DB',
|
||||||
|
job: null
|
||||||
|
},
|
||||||
|
youtubedl_update_check: {
|
||||||
|
run: youtubedl_api.checkForYoutubeDLUpdate,
|
||||||
|
confirm: youtubedl_api.updateYoutubeDL,
|
||||||
|
title: 'Update youtube-dl',
|
||||||
|
job: null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function scheduleJob(task_key, schedule) {
|
||||||
|
// schedule has to be converted from our format to one node-schedule can consume
|
||||||
|
let converted_schedule = null;
|
||||||
|
if (schedule['type'] === 'timestamp') {
|
||||||
|
converted_schedule = new Date(schedule['data']['timestamp']);
|
||||||
|
} else if (schedule['type'] === 'recurring') {
|
||||||
|
const dayOfWeek = schedule['data']['dayOfWeek'] != null ? schedule['data']['dayOfWeek'] : null;
|
||||||
|
const hour = schedule['data']['hour'] != null ? schedule['data']['hour'] : null;
|
||||||
|
const minute = schedule['data']['minute'] != null ? schedule['data']['minute'] : null;
|
||||||
|
converted_schedule = new scheduler.RecurrenceRule(null, null, null, dayOfWeek, hour, minute);
|
||||||
|
} else {
|
||||||
|
logger.error(`Failed to schedule job '${task_key}' as the type '${schedule['type']}' is invalid.`)
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return scheduler.scheduleJob(converted_schedule, async () => {
|
||||||
|
const task_state = await db_api.getRecord('tasks', {key: task_key});
|
||||||
|
if (task_state['running'] || task_state['confirming']) {
|
||||||
|
logger.verbose(`Skipping running task ${task_state['key']} as it is already in progress.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove schedule if it's a one-time task
|
||||||
|
if (task_state['schedule']['type'] !== 'recurring') await db_api.updateRecord('tasks', {key: task_key}, {schedule: null});
|
||||||
|
// we're just "running" the task, any confirmation should be user-initiated
|
||||||
|
exports.executeRun(task_key);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (db_api.database_initialized) {
|
||||||
|
exports.setupTasks();
|
||||||
|
} else {
|
||||||
|
db_api.database_initialized_bs.subscribe(init => {
|
||||||
|
if (init) exports.setupTasks();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.setupTasks = async () => {
|
||||||
|
const tasks_keys = Object.keys(TASKS);
|
||||||
|
for (let i = 0; i < tasks_keys.length; i++) {
|
||||||
|
const task_key = tasks_keys[i];
|
||||||
|
const task_in_db = await db_api.getRecord('tasks', {key: task_key});
|
||||||
|
if (!task_in_db) {
|
||||||
|
// insert task metadata into table if missing
|
||||||
|
await db_api.insertRecordIntoTable('tasks', {
|
||||||
|
key: task_key,
|
||||||
|
title: TASKS[task_key]['title'],
|
||||||
|
last_ran: null,
|
||||||
|
last_confirmed: null,
|
||||||
|
running: false,
|
||||||
|
confirming: false,
|
||||||
|
data: null,
|
||||||
|
error: null,
|
||||||
|
schedule: null,
|
||||||
|
options: {}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// reset task if necessary
|
||||||
|
await db_api.updateRecord('tasks', {key: task_key}, {running: false, confirming: false});
|
||||||
|
|
||||||
|
// schedule task and save job
|
||||||
|
if (task_in_db['schedule']) {
|
||||||
|
// prevent timestamp schedules from being set to the past
|
||||||
|
if (task_in_db['schedule']['type'] === 'timestamp' && task_in_db['schedule']['data']['timestamp'] < Date.now()) {
|
||||||
|
await db_api.updateRecord('tasks', {key: task_key}, {schedule: null});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
TASKS[task_key]['job'] = scheduleJob(task_key, task_in_db['schedule']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.executeTask = async (task_key) => {
|
||||||
|
if (!TASKS[task_key]) {
|
||||||
|
logger.error(`Task ${task_key} does not exist!`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
logger.verbose(`Executing task ${task_key}`);
|
||||||
|
await exports.executeRun(task_key);
|
||||||
|
if (!TASKS[task_key]['confirm']) return;
|
||||||
|
await exports.executeConfirm(task_key);
|
||||||
|
logger.verbose(`Finished executing ${task_key}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.executeRun = async (task_key) => {
|
||||||
|
logger.verbose(`Running task ${task_key}`);
|
||||||
|
// don't set running to true when backup up DB as it will be stick "running" if restored
|
||||||
|
if (task_key !== 'backup_local_db') await db_api.updateRecord('tasks', {key: task_key}, {running: true});
|
||||||
|
const data = await TASKS[task_key].run();
|
||||||
|
await db_api.updateRecord('tasks', {key: task_key}, {data: TASKS[task_key]['confirm'] ? data : null, last_ran: Date.now()/1000, running: false});
|
||||||
|
logger.verbose(`Finished running task ${task_key}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.executeConfirm = async (task_key) => {
|
||||||
|
logger.verbose(`Confirming task ${task_key}`);
|
||||||
|
if (!TASKS[task_key]['confirm']) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
await db_api.updateRecord('tasks', {key: task_key}, {confirming: true});
|
||||||
|
const task_obj = await db_api.getRecord('tasks', {key: task_key});
|
||||||
|
const data = task_obj['data'];
|
||||||
|
await TASKS[task_key].confirm(data);
|
||||||
|
await db_api.updateRecord('tasks', {key: task_key}, {confirming: false, last_confirmed: Date.now()/1000, data: null});
|
||||||
|
logger.verbose(`Finished confirming task ${task_key}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.updateTaskSchedule = async (task_key, schedule) => {
|
||||||
|
logger.verbose(`Updating schedule for task ${task_key}`);
|
||||||
|
await db_api.updateRecord('tasks', {key: task_key}, {schedule: schedule});
|
||||||
|
if (TASKS[task_key]['job']) {
|
||||||
|
TASKS[task_key]['job'].cancel();
|
||||||
|
}
|
||||||
|
if (schedule) {
|
||||||
|
TASKS[task_key]['job'] = scheduleJob(task_key, schedule);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// missing files check
|
||||||
|
|
||||||
|
async function checkForMissingFiles() {
|
||||||
|
const missing_files = [];
|
||||||
|
const all_files = await db_api.getRecords('files');
|
||||||
|
for (let i = 0; i < all_files.length; i++) {
|
||||||
|
const file_to_check = all_files[i];
|
||||||
|
const file_exists = fs.existsSync(file_to_check['path']);
|
||||||
|
if (!file_exists) missing_files.push(file_to_check['uid']);
|
||||||
|
}
|
||||||
|
return {uids: missing_files};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteMissingFiles(data) {
|
||||||
|
const uids = data['uids'];
|
||||||
|
for (let i = 0; i < uids.length; i++) {
|
||||||
|
const uid = uids[i];
|
||||||
|
await db_api.removeRecord('files', {uid: uid});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// duplicate files check
|
||||||
|
|
||||||
|
async function checkForDuplicateFiles() {
|
||||||
|
const duplicate_files = await db_api.findDuplicatesByKey('files', 'path');
|
||||||
|
const duplicate_uids = duplicate_files.map(duplicate_file => duplicate_file['uid']);
|
||||||
|
if (duplicate_uids && duplicate_uids.length > 0) {
|
||||||
|
return {uids: duplicate_uids};
|
||||||
|
}
|
||||||
|
return {uids: []};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function removeDuplicates(data) {
|
||||||
|
for (let i = 0; i < data['uids'].length; i++) {
|
||||||
|
await db_api.removeRecord('files', {uid: data['uids'][i]});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.TASKS = TASKS;
|
||||||
1
backend/test/sample.info.json
Normal file
1
backend/test/sample.info.json
Normal file
File diff suppressed because one or more lines are too long
@@ -70,6 +70,17 @@ describe('Database', async function() {
|
|||||||
const success = await db_api.getRecord('test', {test: 'test'});
|
const success = await db_api.getRecord('test', {test: 'test'});
|
||||||
assert(success);
|
assert(success);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('Restore db', async function() {
|
||||||
|
const db_stats = await db_api.getDBStats();
|
||||||
|
|
||||||
|
const file_name = await db_api.backupDB();
|
||||||
|
await db_api.restoreDB(file_name);
|
||||||
|
|
||||||
|
const new_db_stats = await db_api.getDBStats();
|
||||||
|
|
||||||
|
assert(JSON.stringify(db_stats), JSON.stringify(new_db_stats));
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Export', function() {
|
describe('Export', function() {
|
||||||
@@ -83,12 +94,37 @@ describe('Database', async function() {
|
|||||||
await db_api.removeAllRecords('test');
|
await db_api.removeAllRecords('test');
|
||||||
});
|
});
|
||||||
it('Add and read record', async function() {
|
it('Add and read record', async function() {
|
||||||
|
this.timeout(120000);
|
||||||
await db_api.insertRecordIntoTable('test', {test_add: 'test', test_undefined: undefined, test_null: undefined});
|
await db_api.insertRecordIntoTable('test', {test_add: 'test', test_undefined: undefined, test_null: undefined});
|
||||||
const added_record = await db_api.getRecord('test', {test_add: 'test', test_undefined: undefined, test_null: null});
|
const added_record = await db_api.getRecord('test', {test_add: 'test', test_undefined: undefined, test_null: null});
|
||||||
assert(added_record['test_add'] === 'test');
|
assert(added_record['test_add'] === 'test');
|
||||||
await db_api.removeRecord('test', {test_add: 'test'});
|
await db_api.removeRecord('test', {test_add: 'test'});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('Find duplicates by key', async function() {
|
||||||
|
const test_duplicates = [
|
||||||
|
{
|
||||||
|
test: 'testing',
|
||||||
|
key: '1'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: 'testing',
|
||||||
|
key: '2'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: 'testing_missing',
|
||||||
|
key: '3'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: 'testing',
|
||||||
|
key: '4'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
await db_api.insertRecordsIntoTable('test', test_duplicates);
|
||||||
|
const duplicates = await db_api.findDuplicatesByKey('test', 'test');
|
||||||
|
console.log(duplicates);
|
||||||
|
});
|
||||||
|
|
||||||
it('Update record', async function() {
|
it('Update record', async function() {
|
||||||
await db_api.insertRecordIntoTable('test', {test_update: 'test'});
|
await db_api.insertRecordIntoTable('test', {test_update: 'test'});
|
||||||
await db_api.updateRecord('test', {test_update: 'test'}, {added_field: true});
|
await db_api.updateRecord('test', {test_update: 'test'}, {added_field: true});
|
||||||
@@ -122,6 +158,7 @@ describe('Database', async function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('Bulk add', async function() {
|
it('Bulk add', async function() {
|
||||||
|
this.timeout(120000);
|
||||||
const NUM_RECORDS_TO_ADD = 2002; // max batch ops is 1000
|
const NUM_RECORDS_TO_ADD = 2002; // max batch ops is 1000
|
||||||
const test_records = [];
|
const test_records = [];
|
||||||
for (let i = 0; i < NUM_RECORDS_TO_ADD; i++) {
|
for (let i = 0; i < NUM_RECORDS_TO_ADD; i++) {
|
||||||
@@ -291,7 +328,6 @@ describe('Multi User', async function() {
|
|||||||
|
|
||||||
describe('Downloader', function() {
|
describe('Downloader', function() {
|
||||||
const downloader_api = require('../downloader');
|
const downloader_api = require('../downloader');
|
||||||
downloader_api.initialize(db_api);
|
|
||||||
const url = 'https://www.youtube.com/watch?v=dQw4w9WgXcQ';
|
const url = 'https://www.youtube.com/watch?v=dQw4w9WgXcQ';
|
||||||
const sub_id = 'dc834388-3454-41bf-a618-e11cb8c7de1c';
|
const sub_id = 'dc834388-3454-41bf-a618-e11cb8c7de1c';
|
||||||
const options = {
|
const options = {
|
||||||
@@ -339,4 +375,107 @@ describe('Downloader', function() {
|
|||||||
const args = await downloader_api.generateArgs(url, 'video', sub_options, 'admin');
|
const args = await downloader_api.generateArgs(url, 'video', sub_options, 'admin');
|
||||||
console.log(args);
|
console.log(args);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('Generate kodi NFO file', async function() {
|
||||||
|
const nfo_file_path = './test/sample.nfo';
|
||||||
|
if (fs.existsSync(nfo_file_path)) {
|
||||||
|
fs.unlinkSync(nfo_file_path);
|
||||||
|
}
|
||||||
|
const sample_json = fs.readJSONSync('./test/sample.info.json');
|
||||||
|
downloader_api.generateNFOFile(sample_json, nfo_file_path);
|
||||||
|
assert(fs.existsSync(nfo_file_path), true);
|
||||||
|
fs.unlinkSync(nfo_file_path);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Tasks', function() {
|
||||||
|
const tasks_api = require('../tasks');
|
||||||
|
beforeEach(async function() {
|
||||||
|
await db_api.connectToDB();
|
||||||
|
await db_api.removeAllRecords('tasks');
|
||||||
|
|
||||||
|
const dummy_task = {
|
||||||
|
run: async () => { await utils.wait(500); return true; },
|
||||||
|
confirm: async () => { await utils.wait(500); return true; },
|
||||||
|
title: 'Dummy task',
|
||||||
|
job: null
|
||||||
|
};
|
||||||
|
tasks_api.TASKS['dummy_task'] = dummy_task;
|
||||||
|
|
||||||
|
await tasks_api.initialize();
|
||||||
|
});
|
||||||
|
it('Backup db', async function() {
|
||||||
|
const backups_original = await utils.recFindByExt('appdata', 'bak');
|
||||||
|
const original_length = backups_original.length;
|
||||||
|
await tasks_api.executeTask('backup_local_db');
|
||||||
|
const backups_new = await utils.recFindByExt('appdata', 'bak');
|
||||||
|
const new_length = backups_new.length;
|
||||||
|
assert(original_length, new_length-1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Check for missing files', async function() {
|
||||||
|
await db_api.removeAllRecords('files', {uid: 'test'});
|
||||||
|
const test_missing_file = {uid: 'test', path: 'test/missing_file.mp4'};
|
||||||
|
await db_api.insertRecordIntoTable('files', test_missing_file);
|
||||||
|
await tasks_api.executeTask('missing_files_check');
|
||||||
|
const task_obj = await db_api.getRecord('tasks', {key: 'missing_files_check'});
|
||||||
|
assert(task_obj['data'] && task_obj['data']['uids'] && task_obj['data']['uids'].length >= 1, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Check for duplicate files', async function() {
|
||||||
|
this.timeout(300000);
|
||||||
|
await db_api.removeAllRecords('files', {uid: 'test1'});
|
||||||
|
await db_api.removeAllRecords('files', {uid: 'test2'});
|
||||||
|
const test_duplicate_file1 = {uid: 'test1', path: 'test/missing_file.mp4'};
|
||||||
|
const test_duplicate_file2 = {uid: 'test2', path: 'test/missing_file.mp4'};
|
||||||
|
const test_duplicate_file3 = {uid: 'test3', path: 'test/missing_file.mp4'};
|
||||||
|
await db_api.insertRecordIntoTable('files', test_duplicate_file1);
|
||||||
|
await db_api.insertRecordIntoTable('files', test_duplicate_file2);
|
||||||
|
await db_api.insertRecordIntoTable('files', test_duplicate_file3);
|
||||||
|
await tasks_api.executeTask('duplicate_files_check');
|
||||||
|
const task_obj = await db_api.getRecord('tasks', {key: 'duplicate_files_check'});
|
||||||
|
const duplicated_record_count = await db_api.getRecords('files', {path: 'test/missing_file.mp4'}, true);
|
||||||
|
assert(task_obj['data'] && task_obj['data']['uids'] && task_obj['data']['uids'].length >= 1, true);
|
||||||
|
assert(duplicated_record_count == 1, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Import unregistered files', async function() {
|
||||||
|
this.timeout(300000);
|
||||||
|
|
||||||
|
// pre-test cleanup
|
||||||
|
await db_api.removeAllRecords('files', {title: 'Sample File'});
|
||||||
|
if (fs.existsSync('video/sample.info.json')) fs.unlinkSync('video/sample.info.json');
|
||||||
|
if (fs.existsSync('video/sample.mp4')) fs.unlinkSync('video/sample.mp4');
|
||||||
|
|
||||||
|
// copies in files
|
||||||
|
fs.copyFileSync('test/sample.info.json', 'video/sample.info.json');
|
||||||
|
fs.copyFileSync('test/sample.mp4', 'video/sample.mp4');
|
||||||
|
await tasks_api.executeTask('missing_db_records');
|
||||||
|
const imported_file = await db_api.getRecord('files', {title: 'Sample File'});
|
||||||
|
assert(!!imported_file, true);
|
||||||
|
|
||||||
|
// post-test cleanup
|
||||||
|
if (fs.existsSync('video/sample.info.json')) fs.unlinkSync('video/sample.info.json');
|
||||||
|
if (fs.existsSync('video/sample.mp4')) fs.unlinkSync('video/sample.mp4');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Schedule and cancel task', async function() {
|
||||||
|
const today_4_hours = new Date();
|
||||||
|
today_4_hours.setHours(today_4_hours.getHours() + 4);
|
||||||
|
await tasks_api.updateTaskSchedule('dummy_task', today_4_hours);
|
||||||
|
assert(!!tasks_api.TASKS['dummy_task']['job'], true);
|
||||||
|
await tasks_api.updateTaskSchedule('dummy_task', null);
|
||||||
|
assert(!!tasks_api.TASKS['dummy_task']['job'], false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Schedule and run task', async function() {
|
||||||
|
this.timeout(5000);
|
||||||
|
const today_1_second = new Date();
|
||||||
|
today_1_second.setSeconds(today_1_second.getSeconds() + 1);
|
||||||
|
await tasks_api.updateTaskSchedule('dummy_task', today_1_second);
|
||||||
|
assert(!!tasks_api.TASKS['dummy_task']['job'], true);
|
||||||
|
await utils.wait(2000);
|
||||||
|
const dummy_task_obj = await db_api.getRecord('tasks', {key: 'dummy_task'});
|
||||||
|
assert(dummy_task_obj['data'], true);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
@@ -1,10 +1,13 @@
|
|||||||
const fs = require('fs-extra')
|
const fs = require('fs-extra');
|
||||||
const path = require('path')
|
const path = require('path');
|
||||||
const ffmpeg = require('fluent-ffmpeg');
|
const ffmpeg = require('fluent-ffmpeg');
|
||||||
|
const archiver = require('archiver');
|
||||||
|
const fetch = require('node-fetch');
|
||||||
|
const ProgressBar = require('progress');
|
||||||
|
|
||||||
const config_api = require('./config');
|
const config_api = require('./config');
|
||||||
const logger = require('./logger');
|
const logger = require('./logger');
|
||||||
const CONSTS = require('./consts')
|
const CONSTS = require('./consts');
|
||||||
const archiver = require('archiver');
|
|
||||||
|
|
||||||
const is_windows = process.platform === 'win32';
|
const is_windows = process.platform === 'win32';
|
||||||
|
|
||||||
@@ -45,8 +48,7 @@ async function getDownloadedFilesByType(basePath, type, full_metadata = false) {
|
|||||||
files.push(jsonobj);
|
files.push(jsonobj);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
var upload_date = jsonobj.upload_date;
|
var upload_date = formatDateString(jsonobj.upload_date);
|
||||||
upload_date = upload_date ? `${upload_date.substring(0, 4)}-${upload_date.substring(4, 6)}-${upload_date.substring(6, 8)}` : null;
|
|
||||||
|
|
||||||
var isaudio = type === 'audio';
|
var isaudio = type === 'audio';
|
||||||
var file_obj = new File(id, jsonobj.title, jsonobj.thumbnail, isaudio, jsonobj.duration, jsonobj.webpage_url, jsonobj.uploader,
|
var file_obj = new File(id, jsonobj.title, jsonobj.thumbnail, isaudio, jsonobj.duration, jsonobj.webpage_url, jsonobj.uploader,
|
||||||
@@ -56,13 +58,13 @@ async function getDownloadedFilesByType(basePath, type, full_metadata = false) {
|
|||||||
return files;
|
return files;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createContainerZipFile(container_obj, container_file_objs) {
|
async function createContainerZipFile(file_name, container_file_objs) {
|
||||||
const container_files_to_download = [];
|
const container_files_to_download = [];
|
||||||
for (let i = 0; i < container_file_objs.length; i++) {
|
for (let i = 0; i < container_file_objs.length; i++) {
|
||||||
const container_file_obj = container_file_objs[i];
|
const container_file_obj = container_file_objs[i];
|
||||||
container_files_to_download.push(container_file_obj.path);
|
container_files_to_download.push(container_file_obj.path);
|
||||||
}
|
}
|
||||||
return await createZipFile(path.join('appdata', container_obj.name + '.zip'), container_files_to_download);
|
return await createZipFile(path.join('appdata', file_name + '.zip'), container_files_to_download);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createZipFile(zip_file_path, file_paths) {
|
async function createZipFile(zip_file_path, file_paths) {
|
||||||
@@ -267,7 +269,7 @@ function getCurrentDownloader() {
|
|||||||
return details_json['downloader'];
|
return details_json['downloader'];
|
||||||
}
|
}
|
||||||
|
|
||||||
async function recFindByExt(base,ext,files,result)
|
async function recFindByExt(base, ext, files, result, recursive = true)
|
||||||
{
|
{
|
||||||
files = files || (await fs.readdir(base))
|
files = files || (await fs.readdir(base))
|
||||||
result = result || []
|
result = result || []
|
||||||
@@ -276,6 +278,7 @@ async function recFindByExt(base,ext,files,result)
|
|||||||
var newbase = path.join(base,file)
|
var newbase = path.join(base,file)
|
||||||
if ( (await fs.stat(newbase)).isDirectory() )
|
if ( (await fs.stat(newbase)).isDirectory() )
|
||||||
{
|
{
|
||||||
|
if (!recursive) continue;
|
||||||
result = await recFindByExt(newbase,ext,await fs.readdir(newbase),result)
|
result = await recFindByExt(newbase,ext,await fs.readdir(newbase),result)
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -295,6 +298,10 @@ function removeFileExtension(filename) {
|
|||||||
return filename_parts.join('.');
|
return filename_parts.join('.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatDateString(date_string) {
|
||||||
|
return date_string ? `${date_string.substring(0, 4)}-${date_string.substring(4, 6)}-${date_string.substring(6, 8)}` : 'N/A';
|
||||||
|
}
|
||||||
|
|
||||||
function createEdgeNGrams(str) {
|
function createEdgeNGrams(str) {
|
||||||
if (str && str.length > 3) {
|
if (str && str.length > 3) {
|
||||||
const minGram = 3
|
const minGram = 3
|
||||||
@@ -352,6 +359,62 @@ async function cropFile(file_path, start, end, ext) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function checkExistsWithTimeout(filePath, timeout) {
|
||||||
|
return new Promise(function (resolve, reject) {
|
||||||
|
|
||||||
|
var timer = setTimeout(function () {
|
||||||
|
if (watcher) watcher.close();
|
||||||
|
reject(new Error('File did not exists and was not created during the timeout.'));
|
||||||
|
}, timeout);
|
||||||
|
|
||||||
|
fs.access(filePath, fs.constants.R_OK, function (err) {
|
||||||
|
if (!err) {
|
||||||
|
clearTimeout(timer);
|
||||||
|
if (watcher) watcher.close();
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var dir = path.dirname(filePath);
|
||||||
|
var basename = path.basename(filePath);
|
||||||
|
var watcher = fs.watch(dir, function (eventType, filename) {
|
||||||
|
if (eventType === 'rename' && filename === basename) {
|
||||||
|
clearTimeout(timer);
|
||||||
|
if (watcher) watcher.close();
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// helper function to download file using fetch
|
||||||
|
async function fetchFile(url, path, file_label) {
|
||||||
|
var len = null;
|
||||||
|
const res = await fetch(url);
|
||||||
|
|
||||||
|
len = parseInt(res.headers.get("Content-Length"), 10);
|
||||||
|
|
||||||
|
var bar = new ProgressBar(` Downloading ${file_label} [:bar] :percent :etas`, {
|
||||||
|
complete: '=',
|
||||||
|
incomplete: ' ',
|
||||||
|
width: 20,
|
||||||
|
total: len
|
||||||
|
});
|
||||||
|
const fileStream = fs.createWriteStream(path);
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
res.body.pipe(fileStream);
|
||||||
|
res.body.on("error", (err) => {
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
res.body.on('data', function (chunk) {
|
||||||
|
bar.tick(chunk.length);
|
||||||
|
});
|
||||||
|
fileStream.on("finish", function() {
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// 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) {
|
||||||
@@ -389,8 +452,11 @@ module.exports = {
|
|||||||
getCurrentDownloader: getCurrentDownloader,
|
getCurrentDownloader: getCurrentDownloader,
|
||||||
recFindByExt: recFindByExt,
|
recFindByExt: recFindByExt,
|
||||||
removeFileExtension: removeFileExtension,
|
removeFileExtension: removeFileExtension,
|
||||||
|
formatDateString: formatDateString,
|
||||||
cropFile: cropFile,
|
cropFile: cropFile,
|
||||||
createEdgeNGrams: createEdgeNGrams,
|
createEdgeNGrams: createEdgeNGrams,
|
||||||
wait: wait,
|
wait: wait,
|
||||||
|
checkExistsWithTimeout: checkExistsWithTimeout,
|
||||||
|
fetchFile: fetchFile,
|
||||||
File: File
|
File: File
|
||||||
}
|
}
|
||||||
|
|||||||
127
backend/youtube-dl.js
Normal file
127
backend/youtube-dl.js
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
const fs = require('fs-extra');
|
||||||
|
const fetch = require('node-fetch');
|
||||||
|
|
||||||
|
const logger = require('./logger');
|
||||||
|
const utils = require('./utils');
|
||||||
|
const CONSTS = require('./consts');
|
||||||
|
const config_api = require('./config.js');
|
||||||
|
|
||||||
|
const is_windows = process.platform === 'win32';
|
||||||
|
|
||||||
|
const download_sources = {
|
||||||
|
'youtube-dl': {
|
||||||
|
'tags_url': 'https://api.github.com/repos/ytdl-org/youtube-dl/tags',
|
||||||
|
'func': downloadLatestYoutubeDLBinary
|
||||||
|
},
|
||||||
|
'youtube-dlc': {
|
||||||
|
'tags_url': 'https://api.github.com/repos/blackjack4494/yt-dlc/tags',
|
||||||
|
'func': downloadLatestYoutubeDLCBinary
|
||||||
|
},
|
||||||
|
'yt-dlp': {
|
||||||
|
'tags_url': 'https://api.github.com/repos/yt-dlp/yt-dlp/tags',
|
||||||
|
'func': downloadLatestYoutubeDLPBinary
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.checkForYoutubeDLUpdate = async () => {
|
||||||
|
return new Promise(async resolve => {
|
||||||
|
const default_downloader = config_api.getConfigItem('ytdl_default_downloader');
|
||||||
|
const tags_url = download_sources[default_downloader]['tags_url'];
|
||||||
|
// get current version
|
||||||
|
let current_app_details_exists = fs.existsSync(CONSTS.DETAILS_BIN_PATH);
|
||||||
|
if (!current_app_details_exists) {
|
||||||
|
logger.warn(`Failed to get youtube-dl binary details at location '${CONSTS.DETAILS_BIN_PATH}'. Generating file...`);
|
||||||
|
fs.writeJSONSync(CONSTS.DETAILS_BIN_PATH, {"version":"2020.00.00", "downloader": default_downloader});
|
||||||
|
}
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
fetch(tags_url, {method: 'Get'})
|
||||||
|
.then(async res => res.json())
|
||||||
|
.then(async (json) => {
|
||||||
|
// check if the versions are different
|
||||||
|
if (!json || !json[0]) {
|
||||||
|
logger.error(`Failed to check ${default_downloader} version for an update.`)
|
||||||
|
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) => {
|
||||||
|
const default_downloader = config_api.getConfigItem('ytdl_default_downloader');
|
||||||
|
await download_sources[default_downloader]['func'](latest_update_version);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function downloadLatestYoutubeDLBinary(new_version) {
|
||||||
|
const file_ext = is_windows ? '.exe' : '';
|
||||||
|
|
||||||
|
const download_url = `https://github.com/ytdl-org/youtube-dl/releases/latest/download/youtube-dl${file_ext}`;
|
||||||
|
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) {
|
||||||
|
const file_ext = is_windows ? '.exe' : '';
|
||||||
|
|
||||||
|
const download_url = `https://github.com/blackjack4494/yt-dlc/releases/latest/download/youtube-dlc${file_ext}`;
|
||||||
|
const output_path = `node_modules/youtube-dl/bin/youtube-dl${file_ext}`;
|
||||||
|
|
||||||
|
await utils.fetchFile(download_url, output_path, `youtube-dlc ${new_version}`);
|
||||||
|
|
||||||
|
updateDetailsJSON(new_version, 'youtube-dlc');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function downloadLatestYoutubeDLPBinary(new_version) {
|
||||||
|
const file_ext = is_windows ? '.exe' : '';
|
||||||
|
|
||||||
|
const download_url = `https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp${file_ext}`;
|
||||||
|
const output_path = `node_modules/youtube-dl/bin/youtube-dl${file_ext}`;
|
||||||
|
|
||||||
|
await utils.fetchFile(download_url, output_path, `yt-dlp ${new_version}`);
|
||||||
|
|
||||||
|
updateDetailsJSON(new_version, 'yt-dlp');
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
43
docker-build.sh
Normal file
43
docker-build.sh
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
#!/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=amd64;;
|
||||||
|
aarch64)
|
||||||
|
ARCH=arm64;;
|
||||||
|
armhf)
|
||||||
|
ARCH=armhf;;
|
||||||
|
armv7)
|
||||||
|
ARCH=armel;;
|
||||||
|
armv7l)
|
||||||
|
ARCH=armel;;
|
||||||
|
*)
|
||||||
|
echo "Unsupported architecture: $(uname -m)"
|
||||||
|
exit 1
|
||||||
|
esac
|
||||||
|
|
||||||
|
echo "(INFO) Architecture detected: $ARCH"
|
||||||
|
echo "(1/5) READY - Acquire temp dependencies in ffmpeg obtain layer"
|
||||||
|
apt-get update && apt-get -y install curl xz-utils
|
||||||
|
echo "(2/5) DOWNLOAD - Acquire latest ffmpeg and ffprobe from John van Sickle's master-sourced builds in ffmpeg obtain layer"
|
||||||
|
curl -o ffmpeg.txz \
|
||||||
|
--connect-timeout 5 \
|
||||||
|
--max-time 10 \
|
||||||
|
--retry 5 \
|
||||||
|
--retry-delay 0 \
|
||||||
|
--retry-max-time 40 \
|
||||||
|
"https://johnvansickle.com/ffmpeg/builds/ffmpeg-git-${ARCH}-static.tar.xz"
|
||||||
|
mkdir /tmp/ffmpeg
|
||||||
|
tar xf ffmpeg.txz -C /tmp/ffmpeg
|
||||||
|
echo "(3/5) CLEANUP - Remove temp dependencies from ffmpeg obtain layer"
|
||||||
|
apt-get -y remove curl xz-utils
|
||||||
|
apt-get -y autoremove
|
||||||
|
echo "(4/5) PROVISION - Provide ffmpeg and ffprobe from ffmpeg obtain layer"
|
||||||
|
cp /tmp/ffmpeg/*/ffmpeg /usr/local/bin/ffmpeg
|
||||||
|
cp /tmp/ffmpeg/*/ffprobe /usr/local/bin/ffprobe
|
||||||
|
echo "(5/5) CLEANUP - Remove temporary downloads from ffmpeg obtain layer"
|
||||||
|
rm -rf /tmp/ffmpeg ffmpeg.txz
|
||||||
@@ -7,6 +7,8 @@ services:
|
|||||||
ytdl_use_local_db: 'false'
|
ytdl_use_local_db: 'false'
|
||||||
write_ytdl_config: 'true'
|
write_ytdl_config: 'true'
|
||||||
restart: always
|
restart: always
|
||||||
|
depends_on:
|
||||||
|
- ytdl-mongo-db
|
||||||
volumes:
|
volumes:
|
||||||
- ./appdata:/app/appdata
|
- ./appdata:/app/appdata
|
||||||
- ./audio:/app/audio
|
- ./audio:/app/audio
|
||||||
@@ -23,5 +25,6 @@ services:
|
|||||||
logging:
|
logging:
|
||||||
driver: "none"
|
driver: "none"
|
||||||
container_name: mongo-db
|
container_name: mongo-db
|
||||||
|
restart: always
|
||||||
volumes:
|
volumes:
|
||||||
- ./db/:/data/db
|
- ./db/:/data/db
|
||||||
12580
package-lock.json
generated
12580
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
59
package.json
59
package.json
@@ -5,12 +5,14 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"ng": "ng",
|
"ng": "ng",
|
||||||
"start": "ng serve",
|
"start": "ng serve",
|
||||||
"build": "ng build",
|
"build": "ng build --configuration production",
|
||||||
|
"prebuild": "node src/postbuild.mjs",
|
||||||
"heroku-postbuild": "npm install --prefix backend",
|
"heroku-postbuild": "npm install --prefix backend",
|
||||||
"test": "ng test",
|
"test": "ng test",
|
||||||
"lint": "ng lint",
|
"lint": "ng lint",
|
||||||
"e2e": "ng e2e",
|
"e2e": "ng e2e",
|
||||||
"electron": "ng build --base-href ./ && electron ."
|
"electron": "ng build --base-href ./ && electron .",
|
||||||
|
"generate": "openapi --input ./\"Public API v1.yaml\" --output ./src/api-types --exportCore false --exportServices false --exportModels true"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "12.3.1",
|
"node": "12.3.1",
|
||||||
@@ -18,42 +20,44 @@
|
|||||||
},
|
},
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@angular-devkit/core": "^11.0.4",
|
"@angular-devkit/core": "^13.3.3",
|
||||||
"@angular/animations": "^11.0.4",
|
"@angular/animations": "^13.3.4",
|
||||||
"@angular/cdk": "^11.0.2",
|
"@angular/cdk": "^13.3.4",
|
||||||
"@angular/common": "^11.0.4",
|
"@angular/common": "^13.3.4",
|
||||||
"@angular/compiler": "^11.0.4",
|
"@angular/compiler": "^13.3.4",
|
||||||
"@angular/core": "^11.0.4",
|
"@angular/core": "^13.3.4",
|
||||||
"@angular/forms": "^11.0.4",
|
"@angular/forms": "^13.3.4",
|
||||||
"@angular/localize": "^11.0.4",
|
"@angular/localize": "^13.3.4",
|
||||||
"@angular/material": "^11.0.2",
|
"@angular/material": "^13.3.4",
|
||||||
"@angular/platform-browser": "^11.0.4",
|
"@angular/platform-browser": "^13.3.4",
|
||||||
"@angular/platform-browser-dynamic": "^11.0.4",
|
"@angular/platform-browser-dynamic": "^13.3.4",
|
||||||
"@angular/router": "^11.0.4",
|
"@angular/router": "^13.3.4",
|
||||||
|
"@fontsource/material-icons": "^4.5.4",
|
||||||
"@ngneat/content-loader": "^5.0.0",
|
"@ngneat/content-loader": "^5.0.0",
|
||||||
"@videogular/ngx-videogular": "^2.1.0",
|
"@videogular/ngx-videogular": "^5.0.1",
|
||||||
"core-js": "^2.4.1",
|
"core-js": "^2.4.1",
|
||||||
"crypto-js": "^4.1.1",
|
"crypto-js": "^4.1.1",
|
||||||
"file-saver": "^2.0.2",
|
"file-saver": "^2.0.2",
|
||||||
"filesize": "^6.1.0",
|
"filesize": "^6.1.0",
|
||||||
"fingerprintjs2": "^2.1.0",
|
"fingerprintjs2": "^2.1.0",
|
||||||
"material-icons": "^0.5.4",
|
"fs-extra": "^10.0.0",
|
||||||
|
"material-icons": "^1.10.8",
|
||||||
"nan": "^2.14.1",
|
"nan": "^2.14.1",
|
||||||
"ng-lazyload-image": "^7.0.1",
|
"ng-lazyload-image": "^7.0.1",
|
||||||
"ngx-avatar": "^4.0.0",
|
"ngx-avatars": "^1.3.1",
|
||||||
"ngx-file-drop": "^9.0.1",
|
"ngx-file-drop": "^13.0.0",
|
||||||
"rxjs": "^6.6.3",
|
"rxjs": "^6.6.3",
|
||||||
"rxjs-compat": "^6.0.0-rc.0",
|
"rxjs-compat": "^6.0.0-rc.0",
|
||||||
"tslib": "^2.0.0",
|
"tslib": "^2.0.0",
|
||||||
"typescript": "~4.0.5",
|
"typescript": "~4.6.3",
|
||||||
"web-animations-js": "^2.3.2",
|
"xliff-to-json": "^1.0.4",
|
||||||
"zone.js": "~0.10.2"
|
"zone.js": "~0.11.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@angular-devkit/build-angular": "^0.1100.4",
|
"@angular-devkit/build-angular": "^13.3.3",
|
||||||
"@angular/cli": "^11.0.4",
|
"@angular/cli": "^13.3.3",
|
||||||
"@angular/compiler-cli": "^11.0.4",
|
"@angular/compiler-cli": "^13.3.4",
|
||||||
"@angular/language-service": "^11.0.4",
|
"@angular/language-service": "^13.3.4",
|
||||||
"@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": "~3.6.0",
|
"@types/jasmine": "~3.6.0",
|
||||||
@@ -61,16 +65,17 @@
|
|||||||
"@typescript-eslint/eslint-plugin": "^4.29.0",
|
"@typescript-eslint/eslint-plugin": "^4.29.0",
|
||||||
"@typescript-eslint/parser": "^4.29.0",
|
"@typescript-eslint/parser": "^4.29.0",
|
||||||
"codelyzer": "^6.0.0",
|
"codelyzer": "^6.0.0",
|
||||||
"electron": "^8.0.1",
|
"electron": "^13.6.6",
|
||||||
"eslint": "^7.32.0",
|
"eslint": "^7.32.0",
|
||||||
"jasmine-core": "~3.6.0",
|
"jasmine-core": "~3.6.0",
|
||||||
"jasmine-spec-reporter": "~5.0.0",
|
"jasmine-spec-reporter": "~5.0.0",
|
||||||
"karma": "~5.0.0",
|
"karma": "~6.3.16",
|
||||||
"karma-chrome-launcher": "~3.1.0",
|
"karma-chrome-launcher": "~3.1.0",
|
||||||
"karma-cli": "~1.0.1",
|
"karma-cli": "~1.0.1",
|
||||||
"karma-coverage-istanbul-reporter": "~3.0.2",
|
"karma-coverage-istanbul-reporter": "~3.0.2",
|
||||||
"karma-jasmine": "~4.0.0",
|
"karma-jasmine": "~4.0.0",
|
||||||
"karma-jasmine-html-reporter": "^1.5.0",
|
"karma-jasmine-html-reporter": "^1.5.0",
|
||||||
|
"openapi-typescript-codegen": "^0.21.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"
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
/* Coolors Exported Palette - coolors.co/e8aeb7-b8e1ff-a9fff7-94fbab-82aba1 */
|
/* Coolors Exported Palette - coolors.co/e8aeb7-b8e1ff-a9fff7-94fbab-82aba1 */
|
||||||
|
|
||||||
/* HSL */
|
/* HSL */
|
||||||
$color1: hsla(351%, 56%, 80%, 1);
|
$color1: hsla(351, 56%, 80%, 1);
|
||||||
$softblue: hsla(205%, 100%, 86%, 1);
|
$softblue: hsla(205, 100%, 86%, 1);
|
||||||
$color3: hsla(174%, 100%, 83%, 1);
|
$color3: hsla(174, 100%, 83%, 1);
|
||||||
$color4: hsla(133%, 93%, 78%, 1);
|
$color4: hsla(133, 93%, 78%, 1);
|
||||||
$color5: hsla(165%, 20%, 59%, 1);
|
$color5: hsla(165, 20%, 59%, 1);
|
||||||
|
|
||||||
/* RGB */
|
/* RGB */
|
||||||
$color1: rgba(232, 174, 183, 1);
|
$color1: rgba(232, 174, 183, 1);
|
||||||
|
|||||||
111
src/api-types/index.ts
Normal file
111
src/api-types/index.ts
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
|
export type { AddFileToPlaylistRequest } from './models/AddFileToPlaylistRequest';
|
||||||
|
export type { BaseChangePermissionsRequest } from './models/BaseChangePermissionsRequest';
|
||||||
|
export type { body_19 } from './models/body_19';
|
||||||
|
export type { body_20 } from './models/body_20';
|
||||||
|
export type { Category } from './models/Category';
|
||||||
|
export { CategoryRule } from './models/CategoryRule';
|
||||||
|
export type { ChangeRolePermissionsRequest } from './models/ChangeRolePermissionsRequest';
|
||||||
|
export type { ChangeUserPermissionsRequest } from './models/ChangeUserPermissionsRequest';
|
||||||
|
export type { CheckConcurrentStreamRequest } from './models/CheckConcurrentStreamRequest';
|
||||||
|
export type { CheckConcurrentStreamResponse } from './models/CheckConcurrentStreamResponse';
|
||||||
|
export type { ConcurrentStream } from './models/ConcurrentStream';
|
||||||
|
export type { Config } from './models/Config';
|
||||||
|
export type { ConfigResponse } from './models/ConfigResponse';
|
||||||
|
export type { CreateCategoryRequest } from './models/CreateCategoryRequest';
|
||||||
|
export type { CreateCategoryResponse } from './models/CreateCategoryResponse';
|
||||||
|
export type { CreatePlaylistRequest } from './models/CreatePlaylistRequest';
|
||||||
|
export type { CreatePlaylistResponse } from './models/CreatePlaylistResponse';
|
||||||
|
export type { CropFileSettings } from './models/CropFileSettings';
|
||||||
|
export type { DatabaseFile } from './models/DatabaseFile';
|
||||||
|
export { DBBackup } from './models/DBBackup';
|
||||||
|
export type { DBInfoResponse } from './models/DBInfoResponse';
|
||||||
|
export type { DeleteCategoryRequest } from './models/DeleteCategoryRequest';
|
||||||
|
export type { DeleteMp3Mp4Request } from './models/DeleteMp3Mp4Request';
|
||||||
|
export type { DeletePlaylistRequest } from './models/DeletePlaylistRequest';
|
||||||
|
export type { DeleteSubscriptionFileRequest } from './models/DeleteSubscriptionFileRequest';
|
||||||
|
export type { DeleteUserRequest } from './models/DeleteUserRequest';
|
||||||
|
export type { Download } from './models/Download';
|
||||||
|
export type { DownloadArchiveRequest } from './models/DownloadArchiveRequest';
|
||||||
|
export type { DownloadFileRequest } from './models/DownloadFileRequest';
|
||||||
|
export type { DownloadRequest } from './models/DownloadRequest';
|
||||||
|
export type { DownloadResponse } from './models/DownloadResponse';
|
||||||
|
export type { DownloadTwitchChatByVODIDRequest } from './models/DownloadTwitchChatByVODIDRequest';
|
||||||
|
export type { DownloadTwitchChatByVODIDResponse } from './models/DownloadTwitchChatByVODIDResponse';
|
||||||
|
export type { DownloadVideosForSubscriptionRequest } from './models/DownloadVideosForSubscriptionRequest';
|
||||||
|
export type { File } from './models/File';
|
||||||
|
export { FileType } from './models/FileType';
|
||||||
|
export type { GenerateArgsResponse } from './models/GenerateArgsResponse';
|
||||||
|
export type { GenerateNewApiKeyResponse } from './models/GenerateNewApiKeyResponse';
|
||||||
|
export type { GetAllCategoriesResponse } from './models/GetAllCategoriesResponse';
|
||||||
|
export type { GetAllDownloadsRequest } from './models/GetAllDownloadsRequest';
|
||||||
|
export type { GetAllDownloadsResponse } from './models/GetAllDownloadsResponse';
|
||||||
|
export type { GetAllFilesResponse } from './models/GetAllFilesResponse';
|
||||||
|
export type { GetAllSubscriptionsResponse } from './models/GetAllSubscriptionsResponse';
|
||||||
|
export type { GetAllTasksResponse } from './models/GetAllTasksResponse';
|
||||||
|
export type { GetDBBackupsResponse } from './models/GetDBBackupsResponse';
|
||||||
|
export type { GetDownloadRequest } from './models/GetDownloadRequest';
|
||||||
|
export type { GetDownloadResponse } from './models/GetDownloadResponse';
|
||||||
|
export type { GetFileFormatsRequest } from './models/GetFileFormatsRequest';
|
||||||
|
export type { GetFileFormatsResponse } from './models/GetFileFormatsResponse';
|
||||||
|
export type { GetFileRequest } from './models/GetFileRequest';
|
||||||
|
export type { GetFileResponse } from './models/GetFileResponse';
|
||||||
|
export type { GetFullTwitchChatRequest } from './models/GetFullTwitchChatRequest';
|
||||||
|
export type { GetFullTwitchChatResponse } from './models/GetFullTwitchChatResponse';
|
||||||
|
export type { GetLogsRequest } from './models/GetLogsRequest';
|
||||||
|
export type { GetLogsResponse } from './models/GetLogsResponse';
|
||||||
|
export type { GetMp3sResponse } from './models/GetMp3sResponse';
|
||||||
|
export type { GetMp4sResponse } from './models/GetMp4sResponse';
|
||||||
|
export type { GetPlaylistRequest } from './models/GetPlaylistRequest';
|
||||||
|
export type { GetPlaylistResponse } from './models/GetPlaylistResponse';
|
||||||
|
export type { GetPlaylistsRequest } from './models/GetPlaylistsRequest';
|
||||||
|
export type { GetPlaylistsResponse } from './models/GetPlaylistsResponse';
|
||||||
|
export type { GetRolesResponse } from './models/GetRolesResponse';
|
||||||
|
export type { GetSubscriptionRequest } from './models/GetSubscriptionRequest';
|
||||||
|
export type { GetSubscriptionResponse } from './models/GetSubscriptionResponse';
|
||||||
|
export type { GetTaskRequest } from './models/GetTaskRequest';
|
||||||
|
export type { GetTaskResponse } from './models/GetTaskResponse';
|
||||||
|
export type { GetUsersResponse } from './models/GetUsersResponse';
|
||||||
|
export type { IncrementViewCountRequest } from './models/IncrementViewCountRequest';
|
||||||
|
export type { inline_response_200_15 } from './models/inline_response_200_15';
|
||||||
|
export type { LoginRequest } from './models/LoginRequest';
|
||||||
|
export type { LoginResponse } from './models/LoginResponse';
|
||||||
|
export type { Playlist } from './models/Playlist';
|
||||||
|
export type { RegisterRequest } from './models/RegisterRequest';
|
||||||
|
export type { RegisterResponse } from './models/RegisterResponse';
|
||||||
|
export type { RestoreDBBackupRequest } from './models/RestoreDBBackupRequest';
|
||||||
|
export { Schedule } from './models/Schedule';
|
||||||
|
export type { SetConfigRequest } from './models/SetConfigRequest';
|
||||||
|
export type { SharingToggle } from './models/SharingToggle';
|
||||||
|
export type { SubscribeRequest } from './models/SubscribeRequest';
|
||||||
|
export type { SubscribeResponse } from './models/SubscribeResponse';
|
||||||
|
export type { Subscription } from './models/Subscription';
|
||||||
|
export type { SubscriptionRequestData } from './models/SubscriptionRequestData';
|
||||||
|
export type { SuccessObject } from './models/SuccessObject';
|
||||||
|
export type { TableInfo } from './models/TableInfo';
|
||||||
|
export type { Task } from './models/Task';
|
||||||
|
export type { TestConnectionStringRequest } from './models/TestConnectionStringRequest';
|
||||||
|
export type { TestConnectionStringResponse } from './models/TestConnectionStringResponse';
|
||||||
|
export type { TransferDBRequest } from './models/TransferDBRequest';
|
||||||
|
export type { TransferDBResponse } from './models/TransferDBResponse';
|
||||||
|
export type { TwitchChatMessage } from './models/TwitchChatMessage';
|
||||||
|
export type { UnsubscribeRequest } from './models/UnsubscribeRequest';
|
||||||
|
export type { UnsubscribeResponse } from './models/UnsubscribeResponse';
|
||||||
|
export type { UpdateCategoriesRequest } from './models/UpdateCategoriesRequest';
|
||||||
|
export type { UpdateCategoryRequest } from './models/UpdateCategoryRequest';
|
||||||
|
export type { UpdateConcurrentStreamRequest } from './models/UpdateConcurrentStreamRequest';
|
||||||
|
export type { UpdateConcurrentStreamResponse } from './models/UpdateConcurrentStreamResponse';
|
||||||
|
export type { UpdatePlaylistRequest } from './models/UpdatePlaylistRequest';
|
||||||
|
export type { UpdaterStatus } from './models/UpdaterStatus';
|
||||||
|
export type { UpdateServerRequest } from './models/UpdateServerRequest';
|
||||||
|
export type { UpdateTaskDataRequest } from './models/UpdateTaskDataRequest';
|
||||||
|
export type { UpdateTaskScheduleRequest } from './models/UpdateTaskScheduleRequest';
|
||||||
|
export type { UpdateUserRequest } from './models/UpdateUserRequest';
|
||||||
|
export type { User } from './models/User';
|
||||||
|
export { UserPermission } from './models/UserPermission';
|
||||||
|
export type { Version } from './models/Version';
|
||||||
|
export type { VersionInfoResponse } from './models/VersionInfoResponse';
|
||||||
|
export { YesNo } from './models/YesNo';
|
||||||
9
src/api-types/models/AddFileToPlaylistRequest.ts
Normal file
9
src/api-types/models/AddFileToPlaylistRequest.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
|
|
||||||
|
export interface AddFileToPlaylistRequest {
|
||||||
|
file_uid: string;
|
||||||
|
playlist_id: string;
|
||||||
|
}
|
||||||
11
src/api-types/models/BaseChangePermissionsRequest.ts
Normal file
11
src/api-types/models/BaseChangePermissionsRequest.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
|
import { UserPermission } from './UserPermission';
|
||||||
|
import { YesNo } from './YesNo';
|
||||||
|
|
||||||
|
export interface BaseChangePermissionsRequest {
|
||||||
|
permission: UserPermission;
|
||||||
|
new_value: YesNo;
|
||||||
|
}
|
||||||
15
src/api-types/models/Category.ts
Normal file
15
src/api-types/models/Category.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
|
import { CategoryRule } from './CategoryRule';
|
||||||
|
|
||||||
|
export interface Category {
|
||||||
|
name?: string;
|
||||||
|
uid?: string;
|
||||||
|
rules?: Array<CategoryRule>;
|
||||||
|
/**
|
||||||
|
* Overrides file output for downloaded files in category
|
||||||
|
*/
|
||||||
|
custom_output?: string;
|
||||||
|
}
|
||||||
26
src/api-types/models/CategoryRule.ts
Normal file
26
src/api-types/models/CategoryRule.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
|
|
||||||
|
export interface CategoryRule {
|
||||||
|
preceding_operator?: CategoryRule.preceding_operator;
|
||||||
|
comparator?: CategoryRule.comparator;
|
||||||
|
}
|
||||||
|
|
||||||
|
export namespace CategoryRule {
|
||||||
|
|
||||||
|
export enum preceding_operator {
|
||||||
|
OR = 'or',
|
||||||
|
AND = 'and',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum comparator {
|
||||||
|
INCLUDES = 'includes',
|
||||||
|
NOT_INCLUDES = 'not_includes',
|
||||||
|
EQUALS = 'equals',
|
||||||
|
NOT_EQUALS = 'not_equals',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
9
src/api-types/models/ChangeRolePermissionsRequest.ts
Normal file
9
src/api-types/models/ChangeRolePermissionsRequest.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
|
import { BaseChangePermissionsRequest } from './BaseChangePermissionsRequest';
|
||||||
|
|
||||||
|
export interface ChangeRolePermissionsRequest extends BaseChangePermissionsRequest {
|
||||||
|
role: string;
|
||||||
|
}
|
||||||
9
src/api-types/models/ChangeUserPermissionsRequest.ts
Normal file
9
src/api-types/models/ChangeUserPermissionsRequest.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
|
import { BaseChangePermissionsRequest } from './BaseChangePermissionsRequest';
|
||||||
|
|
||||||
|
export interface ChangeUserPermissionsRequest extends BaseChangePermissionsRequest {
|
||||||
|
user_uid: string;
|
||||||
|
}
|
||||||
11
src/api-types/models/CheckConcurrentStreamRequest.ts
Normal file
11
src/api-types/models/CheckConcurrentStreamRequest.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
|
|
||||||
|
export interface CheckConcurrentStreamRequest {
|
||||||
|
/**
|
||||||
|
* UID of the concurrent stream
|
||||||
|
*/
|
||||||
|
uid: string;
|
||||||
|
}
|
||||||
9
src/api-types/models/CheckConcurrentStreamResponse.ts
Normal file
9
src/api-types/models/CheckConcurrentStreamResponse.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
|
import { ConcurrentStream } from './ConcurrentStream';
|
||||||
|
|
||||||
|
export interface CheckConcurrentStreamResponse {
|
||||||
|
stream: ConcurrentStream;
|
||||||
|
}
|
||||||
10
src/api-types/models/ConcurrentStream.ts
Normal file
10
src/api-types/models/ConcurrentStream.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
|
|
||||||
|
export interface ConcurrentStream {
|
||||||
|
playback_timestamp?: number;
|
||||||
|
unix_timestamp?: number;
|
||||||
|
playing?: boolean;
|
||||||
|
}
|
||||||
8
src/api-types/models/Config.ts
Normal file
8
src/api-types/models/Config.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
|
|
||||||
|
export interface Config {
|
||||||
|
YoutubeDLMaterial: any;
|
||||||
|
}
|
||||||
10
src/api-types/models/ConfigResponse.ts
Normal file
10
src/api-types/models/ConfigResponse.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
|
import { Config } from './Config';
|
||||||
|
|
||||||
|
export interface ConfigResponse {
|
||||||
|
config_file: Config;
|
||||||
|
success: boolean;
|
||||||
|
}
|
||||||
8
src/api-types/models/CreateCategoryRequest.ts
Normal file
8
src/api-types/models/CreateCategoryRequest.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
|
|
||||||
|
export interface CreateCategoryRequest {
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
10
src/api-types/models/CreateCategoryResponse.ts
Normal file
10
src/api-types/models/CreateCategoryResponse.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
|
import { Category } from './Category';
|
||||||
|
|
||||||
|
export interface CreateCategoryResponse {
|
||||||
|
new_category?: Category;
|
||||||
|
success?: boolean;
|
||||||
|
}
|
||||||
12
src/api-types/models/CreatePlaylistRequest.ts
Normal file
12
src/api-types/models/CreatePlaylistRequest.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
|
import { FileType } from './FileType';
|
||||||
|
|
||||||
|
export interface CreatePlaylistRequest {
|
||||||
|
playlistName: string;
|
||||||
|
uids: Array<string>;
|
||||||
|
type: FileType;
|
||||||
|
thumbnailURL: string;
|
||||||
|
}
|
||||||
10
src/api-types/models/CreatePlaylistResponse.ts
Normal file
10
src/api-types/models/CreatePlaylistResponse.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
|
import { Playlist } from './Playlist';
|
||||||
|
|
||||||
|
export interface CreatePlaylistResponse {
|
||||||
|
new_playlist: Playlist;
|
||||||
|
success: boolean;
|
||||||
|
}
|
||||||
9
src/api-types/models/CropFileSettings.ts
Normal file
9
src/api-types/models/CropFileSettings.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
|
|
||||||
|
export interface CropFileSettings {
|
||||||
|
cropFileStart: number;
|
||||||
|
cropFileEnd: number;
|
||||||
|
}
|
||||||
21
src/api-types/models/DBBackup.ts
Normal file
21
src/api-types/models/DBBackup.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
|
|
||||||
|
export interface DBBackup {
|
||||||
|
name: string;
|
||||||
|
timestamp: number;
|
||||||
|
size: number;
|
||||||
|
source: DBBackup.source;
|
||||||
|
}
|
||||||
|
|
||||||
|
export namespace DBBackup {
|
||||||
|
|
||||||
|
export enum source {
|
||||||
|
LOCAL = 'local',
|
||||||
|
REMOTE = 'remote',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
18
src/api-types/models/DBInfoResponse.ts
Normal file
18
src/api-types/models/DBInfoResponse.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
|
import { TableInfo } from './TableInfo';
|
||||||
|
|
||||||
|
export interface DBInfoResponse {
|
||||||
|
using_local_db?: boolean;
|
||||||
|
stats_by_table?: {
|
||||||
|
files?: TableInfo,
|
||||||
|
playlists?: TableInfo,
|
||||||
|
categories?: TableInfo,
|
||||||
|
subscriptions?: TableInfo,
|
||||||
|
users?: TableInfo,
|
||||||
|
roles?: TableInfo,
|
||||||
|
download_queue?: TableInfo,
|
||||||
|
};
|
||||||
|
}
|
||||||
22
src/api-types/models/DatabaseFile.ts
Normal file
22
src/api-types/models/DatabaseFile.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
|
|
||||||
|
export interface DatabaseFile {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
thumbnailURL: string;
|
||||||
|
isAudio: boolean;
|
||||||
|
/**
|
||||||
|
* In seconds
|
||||||
|
*/
|
||||||
|
duration: number;
|
||||||
|
url: string;
|
||||||
|
uploader: string;
|
||||||
|
size: number;
|
||||||
|
path: string;
|
||||||
|
upload_date: string;
|
||||||
|
uid: string;
|
||||||
|
sharingEnabled?: boolean;
|
||||||
|
}
|
||||||
8
src/api-types/models/DeleteCategoryRequest.ts
Normal file
8
src/api-types/models/DeleteCategoryRequest.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
|
|
||||||
|
export interface DeleteCategoryRequest {
|
||||||
|
category_uid: string;
|
||||||
|
}
|
||||||
9
src/api-types/models/DeleteMp3Mp4Request.ts
Normal file
9
src/api-types/models/DeleteMp3Mp4Request.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
|
|
||||||
|
export interface DeleteMp3Mp4Request {
|
||||||
|
uid: string;
|
||||||
|
blacklistMode?: boolean;
|
||||||
|
}
|
||||||
10
src/api-types/models/DeletePlaylistRequest.ts
Normal file
10
src/api-types/models/DeletePlaylistRequest.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
|
import { FileType } from './FileType';
|
||||||
|
|
||||||
|
export interface DeletePlaylistRequest {
|
||||||
|
playlist_id: string;
|
||||||
|
type: FileType;
|
||||||
|
}
|
||||||
15
src/api-types/models/DeleteSubscriptionFileRequest.ts
Normal file
15
src/api-types/models/DeleteSubscriptionFileRequest.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
|
import { SubscriptionRequestData } from './SubscriptionRequestData';
|
||||||
|
|
||||||
|
export interface DeleteSubscriptionFileRequest {
|
||||||
|
file: string;
|
||||||
|
file_uid?: string;
|
||||||
|
sub: SubscriptionRequestData;
|
||||||
|
/**
|
||||||
|
* If true, does not remove id from archive. Only valid if youtube-dl archive is enabled in settings.
|
||||||
|
*/
|
||||||
|
deleteForever?: boolean;
|
||||||
|
}
|
||||||
8
src/api-types/models/DeleteUserRequest.ts
Normal file
8
src/api-types/models/DeleteUserRequest.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
|
|
||||||
|
export interface DeleteUserRequest {
|
||||||
|
uid: string;
|
||||||
|
}
|
||||||
7
src/api-types/models/Dictionary.ts
Normal file
7
src/api-types/models/Dictionary.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
|
export type Dictionary<T> = {
|
||||||
|
[key: string]: T;
|
||||||
|
}
|
||||||
26
src/api-types/models/Download.ts
Normal file
26
src/api-types/models/Download.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
|
|
||||||
|
export interface Download {
|
||||||
|
uid: string;
|
||||||
|
ui_uid?: string;
|
||||||
|
running: boolean;
|
||||||
|
finished: boolean;
|
||||||
|
paused: boolean;
|
||||||
|
finished_step: boolean;
|
||||||
|
url: string;
|
||||||
|
type: string;
|
||||||
|
title: string;
|
||||||
|
step_index: number;
|
||||||
|
percent_complete: number;
|
||||||
|
timestamp_start: number;
|
||||||
|
/**
|
||||||
|
* Error text, set if download fails.
|
||||||
|
*/
|
||||||
|
error?: string | null;
|
||||||
|
user_uid?: string;
|
||||||
|
sub_id?: string;
|
||||||
|
sub_name?: string;
|
||||||
|
}
|
||||||
10
src/api-types/models/DownloadArchiveRequest.ts
Normal file
10
src/api-types/models/DownloadArchiveRequest.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
|
|
||||||
|
export interface DownloadArchiveRequest {
|
||||||
|
sub: {
|
||||||
|
archive_dir: string,
|
||||||
|
};
|
||||||
|
}
|
||||||
14
src/api-types/models/DownloadFileRequest.ts
Normal file
14
src/api-types/models/DownloadFileRequest.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
|
import { FileType } from './FileType';
|
||||||
|
|
||||||
|
export interface DownloadFileRequest {
|
||||||
|
uid?: string;
|
||||||
|
uuid?: string;
|
||||||
|
sub_id?: string;
|
||||||
|
playlist_id?: string;
|
||||||
|
url?: string;
|
||||||
|
type?: FileType;
|
||||||
|
}
|
||||||
44
src/api-types/models/DownloadRequest.ts
Normal file
44
src/api-types/models/DownloadRequest.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
|
import { CropFileSettings } from './CropFileSettings';
|
||||||
|
import { FileType } from './FileType';
|
||||||
|
|
||||||
|
export interface DownloadRequest {
|
||||||
|
url: string;
|
||||||
|
/**
|
||||||
|
* Video format code. Overrides other quality options.
|
||||||
|
*/
|
||||||
|
customQualityConfiguration?: string;
|
||||||
|
/**
|
||||||
|
* Custom command-line arguments for youtube-dl. Overrides all other options, except url.
|
||||||
|
*/
|
||||||
|
customArgs?: string;
|
||||||
|
/**
|
||||||
|
* Additional command-line arguments for youtube-dl. Added to whatever args would normally be used.
|
||||||
|
*/
|
||||||
|
additionalArgs?: string;
|
||||||
|
/**
|
||||||
|
* Custom output filename template.
|
||||||
|
*/
|
||||||
|
customOutput?: string;
|
||||||
|
/**
|
||||||
|
* Login with this account ID
|
||||||
|
*/
|
||||||
|
youtubeUsername?: string;
|
||||||
|
/**
|
||||||
|
* Account password
|
||||||
|
*/
|
||||||
|
youtubePassword?: string;
|
||||||
|
/**
|
||||||
|
* Height of the video, if known
|
||||||
|
*/
|
||||||
|
selectedHeight?: string;
|
||||||
|
/**
|
||||||
|
* Specify ffmpeg/avconv audio quality
|
||||||
|
*/
|
||||||
|
maxBitrate?: string;
|
||||||
|
type?: FileType;
|
||||||
|
cropFileSettings?: CropFileSettings;
|
||||||
|
}
|
||||||
9
src/api-types/models/DownloadResponse.ts
Normal file
9
src/api-types/models/DownloadResponse.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
|
import { Download } from './Download';
|
||||||
|
|
||||||
|
export interface DownloadResponse {
|
||||||
|
download?: Download;
|
||||||
|
}
|
||||||
23
src/api-types/models/DownloadTwitchChatByVODIDRequest.ts
Normal file
23
src/api-types/models/DownloadTwitchChatByVODIDRequest.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
|
import { FileType } from './FileType';
|
||||||
|
import { Subscription } from './Subscription';
|
||||||
|
|
||||||
|
export interface DownloadTwitchChatByVODIDRequest {
|
||||||
|
/**
|
||||||
|
* File ID
|
||||||
|
*/
|
||||||
|
id: string;
|
||||||
|
/**
|
||||||
|
* ID of the VOD
|
||||||
|
*/
|
||||||
|
vodId: string;
|
||||||
|
type: FileType;
|
||||||
|
/**
|
||||||
|
* User UID
|
||||||
|
*/
|
||||||
|
uuid?: string;
|
||||||
|
sub?: Subscription;
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
|
import { TwitchChatMessage } from './TwitchChatMessage';
|
||||||
|
|
||||||
|
export interface DownloadTwitchChatByVODIDResponse {
|
||||||
|
chat: Array<TwitchChatMessage>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
|
|
||||||
|
export interface DownloadVideosForSubscriptionRequest {
|
||||||
|
subID: string;
|
||||||
|
}
|
||||||
8
src/api-types/models/File.ts
Normal file
8
src/api-types/models/File.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
|
|
||||||
|
export interface File {
|
||||||
|
id?: string;
|
||||||
|
}
|
||||||
9
src/api-types/models/FileType.ts
Normal file
9
src/api-types/models/FileType.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
|
|
||||||
|
export enum FileType {
|
||||||
|
AUDIO = 'audio',
|
||||||
|
VIDEO = 'video',
|
||||||
|
}
|
||||||
8
src/api-types/models/GenerateArgsResponse.ts
Normal file
8
src/api-types/models/GenerateArgsResponse.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
|
|
||||||
|
export interface GenerateArgsResponse {
|
||||||
|
args?: Array<string>;
|
||||||
|
}
|
||||||
8
src/api-types/models/GenerateNewApiKeyResponse.ts
Normal file
8
src/api-types/models/GenerateNewApiKeyResponse.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
|
|
||||||
|
export interface GenerateNewApiKeyResponse {
|
||||||
|
new_api_key: string;
|
||||||
|
}
|
||||||
9
src/api-types/models/GetAllCategoriesResponse.ts
Normal file
9
src/api-types/models/GetAllCategoriesResponse.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
|
import { Category } from './Category';
|
||||||
|
|
||||||
|
export interface GetAllCategoriesResponse {
|
||||||
|
categories: Array<Category>;
|
||||||
|
}
|
||||||
11
src/api-types/models/GetAllDownloadsRequest.ts
Normal file
11
src/api-types/models/GetAllDownloadsRequest.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
|
|
||||||
|
export interface GetAllDownloadsRequest {
|
||||||
|
/**
|
||||||
|
* Filters downloads with the array
|
||||||
|
*/
|
||||||
|
uids?: Array<string> | null;
|
||||||
|
}
|
||||||
9
src/api-types/models/GetAllDownloadsResponse.ts
Normal file
9
src/api-types/models/GetAllDownloadsResponse.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
|
import { Download } from './Download';
|
||||||
|
|
||||||
|
export interface GetAllDownloadsResponse {
|
||||||
|
downloads?: Array<Download>;
|
||||||
|
}
|
||||||
14
src/api-types/models/GetAllFilesResponse.ts
Normal file
14
src/api-types/models/GetAllFilesResponse.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
|
import { DatabaseFile } from './DatabaseFile';
|
||||||
|
import { Playlist } from './Playlist';
|
||||||
|
|
||||||
|
export interface GetAllFilesResponse {
|
||||||
|
files: Array<DatabaseFile>;
|
||||||
|
/**
|
||||||
|
* All video playlists
|
||||||
|
*/
|
||||||
|
playlists: Array<Playlist>;
|
||||||
|
}
|
||||||
9
src/api-types/models/GetAllSubscriptionsResponse.ts
Normal file
9
src/api-types/models/GetAllSubscriptionsResponse.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
|
import { Subscription } from './Subscription';
|
||||||
|
|
||||||
|
export interface GetAllSubscriptionsResponse {
|
||||||
|
subscriptions: Array<Subscription>;
|
||||||
|
}
|
||||||
9
src/api-types/models/GetAllTasksResponse.ts
Normal file
9
src/api-types/models/GetAllTasksResponse.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
|
import { Task } from './Task';
|
||||||
|
|
||||||
|
export interface GetAllTasksResponse {
|
||||||
|
tasks?: Array<Task>;
|
||||||
|
}
|
||||||
9
src/api-types/models/GetDBBackupsResponse.ts
Normal file
9
src/api-types/models/GetDBBackupsResponse.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
|
import { DBBackup } from './DBBackup';
|
||||||
|
|
||||||
|
export interface GetDBBackupsResponse {
|
||||||
|
tasks?: Array<DBBackup>;
|
||||||
|
}
|
||||||
8
src/api-types/models/GetDownloadRequest.ts
Normal file
8
src/api-types/models/GetDownloadRequest.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
|
|
||||||
|
export interface GetDownloadRequest {
|
||||||
|
download_uid: string;
|
||||||
|
}
|
||||||
9
src/api-types/models/GetDownloadResponse.ts
Normal file
9
src/api-types/models/GetDownloadResponse.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
|
import { Download } from './Download';
|
||||||
|
|
||||||
|
export interface GetDownloadResponse {
|
||||||
|
download?: Download;
|
||||||
|
}
|
||||||
8
src/api-types/models/GetFileFormatsRequest.ts
Normal file
8
src/api-types/models/GetFileFormatsRequest.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
|
|
||||||
|
export interface GetFileFormatsRequest {
|
||||||
|
url?: string;
|
||||||
|
}
|
||||||
12
src/api-types/models/GetFileFormatsResponse.ts
Normal file
12
src/api-types/models/GetFileFormatsResponse.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
|
import { File } from './File';
|
||||||
|
|
||||||
|
export interface GetFileFormatsResponse {
|
||||||
|
success: boolean;
|
||||||
|
result: {
|
||||||
|
formats?: Array<any>,
|
||||||
|
};
|
||||||
|
}
|
||||||
17
src/api-types/models/GetFileRequest.ts
Normal file
17
src/api-types/models/GetFileRequest.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
|
import { FileType } from './FileType';
|
||||||
|
|
||||||
|
export interface GetFileRequest {
|
||||||
|
/**
|
||||||
|
* Video UID
|
||||||
|
*/
|
||||||
|
uid: string;
|
||||||
|
type?: FileType;
|
||||||
|
/**
|
||||||
|
* User UID
|
||||||
|
*/
|
||||||
|
uuid?: string;
|
||||||
|
}
|
||||||
10
src/api-types/models/GetFileResponse.ts
Normal file
10
src/api-types/models/GetFileResponse.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
|
import { DatabaseFile } from './DatabaseFile';
|
||||||
|
|
||||||
|
export interface GetFileResponse {
|
||||||
|
success: boolean;
|
||||||
|
file?: DatabaseFile;
|
||||||
|
}
|
||||||
19
src/api-types/models/GetFullTwitchChatRequest.ts
Normal file
19
src/api-types/models/GetFullTwitchChatRequest.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
|
import { FileType } from './FileType';
|
||||||
|
import { Subscription } from './Subscription';
|
||||||
|
|
||||||
|
export interface GetFullTwitchChatRequest {
|
||||||
|
/**
|
||||||
|
* File ID
|
||||||
|
*/
|
||||||
|
id: string;
|
||||||
|
type: FileType;
|
||||||
|
/**
|
||||||
|
* User UID
|
||||||
|
*/
|
||||||
|
uuid?: string;
|
||||||
|
sub?: Subscription;
|
||||||
|
}
|
||||||
9
src/api-types/models/GetFullTwitchChatResponse.ts
Normal file
9
src/api-types/models/GetFullTwitchChatResponse.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
|
|
||||||
|
export interface GetFullTwitchChatResponse {
|
||||||
|
success: boolean;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
8
src/api-types/models/GetLogsRequest.ts
Normal file
8
src/api-types/models/GetLogsRequest.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
|
|
||||||
|
export interface GetLogsRequest {
|
||||||
|
lines?: number;
|
||||||
|
}
|
||||||
12
src/api-types/models/GetLogsResponse.ts
Normal file
12
src/api-types/models/GetLogsResponse.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
|
|
||||||
|
export interface GetLogsResponse {
|
||||||
|
/**
|
||||||
|
* Number of lines to retrieve from the bottom
|
||||||
|
*/
|
||||||
|
logs?: string;
|
||||||
|
success?: boolean;
|
||||||
|
}
|
||||||
14
src/api-types/models/GetMp3sResponse.ts
Normal file
14
src/api-types/models/GetMp3sResponse.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
|
import { DatabaseFile } from './DatabaseFile';
|
||||||
|
import { Playlist } from './Playlist';
|
||||||
|
|
||||||
|
export interface GetMp3sResponse {
|
||||||
|
mp3s: Array<DatabaseFile>;
|
||||||
|
/**
|
||||||
|
* All audio playlists
|
||||||
|
*/
|
||||||
|
playlists: Array<Playlist>;
|
||||||
|
}
|
||||||
14
src/api-types/models/GetMp4sResponse.ts
Normal file
14
src/api-types/models/GetMp4sResponse.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
|
import { DatabaseFile } from './DatabaseFile';
|
||||||
|
import { Playlist } from './Playlist';
|
||||||
|
|
||||||
|
export interface GetMp4sResponse {
|
||||||
|
mp4s: Array<DatabaseFile>;
|
||||||
|
/**
|
||||||
|
* All video playlists
|
||||||
|
*/
|
||||||
|
playlists: Array<Playlist>;
|
||||||
|
}
|
||||||
12
src/api-types/models/GetPlaylistRequest.ts
Normal file
12
src/api-types/models/GetPlaylistRequest.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
|
import { FileType } from './FileType';
|
||||||
|
|
||||||
|
export interface GetPlaylistRequest {
|
||||||
|
playlist_id: string;
|
||||||
|
type?: FileType;
|
||||||
|
uuid?: string;
|
||||||
|
include_file_metadata?: boolean;
|
||||||
|
}
|
||||||
12
src/api-types/models/GetPlaylistResponse.ts
Normal file
12
src/api-types/models/GetPlaylistResponse.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
|
import { FileType } from './FileType';
|
||||||
|
import { Playlist } from './Playlist';
|
||||||
|
|
||||||
|
export interface GetPlaylistResponse {
|
||||||
|
playlist: Playlist;
|
||||||
|
type: FileType;
|
||||||
|
success: boolean;
|
||||||
|
}
|
||||||
8
src/api-types/models/GetPlaylistsRequest.ts
Normal file
8
src/api-types/models/GetPlaylistsRequest.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
|
|
||||||
|
export interface GetPlaylistsRequest {
|
||||||
|
include_categories?: boolean;
|
||||||
|
}
|
||||||
9
src/api-types/models/GetPlaylistsResponse.ts
Normal file
9
src/api-types/models/GetPlaylistsResponse.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
|
import { Playlist } from './Playlist';
|
||||||
|
|
||||||
|
export interface GetPlaylistsResponse {
|
||||||
|
playlists: Array<Playlist>;
|
||||||
|
}
|
||||||
16
src/api-types/models/GetRolesResponse.ts
Normal file
16
src/api-types/models/GetRolesResponse.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
|
import { UserPermission } from './UserPermission';
|
||||||
|
|
||||||
|
export interface GetRolesResponse {
|
||||||
|
roles: {
|
||||||
|
admin?: {
|
||||||
|
permissions?: Array<UserPermission>,
|
||||||
|
},
|
||||||
|
user?: {
|
||||||
|
permissions?: Array<UserPermission>,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
15
src/api-types/models/GetSubscriptionRequest.ts
Normal file
15
src/api-types/models/GetSubscriptionRequest.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
|
|
||||||
|
export interface GetSubscriptionRequest {
|
||||||
|
/**
|
||||||
|
* Subscription ID
|
||||||
|
*/
|
||||||
|
id: string;
|
||||||
|
/**
|
||||||
|
* Subscription name
|
||||||
|
*/
|
||||||
|
name?: string;
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user