mirror of
https://github.com/Tzahi12345/YoutubeDL-Material.git
synced 2026-03-08 04:20:08 +03:00
Compare commits
66 Commits
archive-im
...
4.3.2-rele
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c7b017498f | ||
|
|
eb6584a4d6 | ||
|
|
2c61260e0f | ||
|
|
ba0de7f95c | ||
|
|
3dfdbcb151 | ||
|
|
c207e56855 | ||
|
|
441131e930 | ||
|
|
84c2b2769b | ||
|
|
e145c9c992 | ||
|
|
2adbc0a02c | ||
|
|
fe95f04c18 | ||
|
|
9b3816afce | ||
|
|
07874d9241 | ||
|
|
7447ca038a | ||
|
|
9fa1aab1e5 | ||
|
|
80b41af620 | ||
|
|
ab5d8dc5ca | ||
|
|
4b55c39f39 | ||
|
|
3ca296f195 | ||
|
|
d4fa640f0f | ||
|
|
427eecf214 | ||
|
|
4f54e408a5 | ||
|
|
9e481bbd5f | ||
|
|
78b29a76b8 | ||
|
|
0342d18f76 | ||
|
|
70754c580c | ||
|
|
e58b0b8638 | ||
|
|
df8f8070ca | ||
|
|
0b8ca31594 | ||
|
|
658a76dc1c | ||
|
|
f363ec5db6 | ||
|
|
f36d675abf | ||
|
|
be74377a08 | ||
|
|
808c7e2112 | ||
|
|
d6f39d37b5 | ||
|
|
e573f34cea | ||
|
|
52e32d4f0f | ||
|
|
adb5f2256e | ||
|
|
59bf6ff86d | ||
|
|
5ce2e2a35d | ||
|
|
68fbde8907 | ||
|
|
62bccb3349 | ||
|
|
90d9ac025a | ||
|
|
07903131f9 | ||
|
|
ec3bb3e738 | ||
|
|
18fcf4eb61 | ||
|
|
19f35d6af4 | ||
|
|
3a918b7059 | ||
|
|
7e7da6c0bc | ||
|
|
f9f7204deb | ||
|
|
8827d9f3de | ||
|
|
42bc255d6c | ||
|
|
2df3b9cbfd | ||
|
|
b859d08d86 | ||
|
|
e7325b2dc2 | ||
|
|
21463762ce | ||
|
|
b06f6a81bb | ||
|
|
82c8146032 | ||
|
|
6f13eab550 | ||
|
|
9d2d70b194 | ||
|
|
4e04ceae16 | ||
|
|
5eec5ac082 | ||
|
|
5253ce8793 | ||
|
|
9a08fc6140 | ||
|
|
26988bd607 | ||
|
|
575f7eed4e |
8
.github/workflows/build.yml
vendored
8
.github/workflows/build.yml
vendored
@@ -15,9 +15,9 @@ jobs:
|
||||
- name: checkout code
|
||||
uses: actions/checkout@v2
|
||||
- name: setup node
|
||||
uses: actions/setup-node@v2
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '14'
|
||||
node-version: '16'
|
||||
cache: 'npm'
|
||||
- name: install dependencies
|
||||
run: |
|
||||
@@ -33,7 +33,7 @@ jobs:
|
||||
run: echo "::set-output name=date::$(date +'%Y-%m-%d')"
|
||||
- name: create-json
|
||||
id: create-json
|
||||
uses: jsdaniell/create-json@1.1.2
|
||||
uses: jsdaniell/create-json@v1.2.2
|
||||
with:
|
||||
name: "version.json"
|
||||
json: '{"type": "autobuild", "tag": "N/A", "commit": "${{ steps.vars.outputs.sha_short }}", "date": "${{ steps.date.outputs.date }}"}'
|
||||
@@ -55,7 +55,7 @@ jobs:
|
||||
Copy-Item -Path ./backend/*.js -Destination ./build/youtubedl-material
|
||||
Copy-Item -Path ./backend/*.json -Destination ./build/youtubedl-material
|
||||
- name: upload build artifact
|
||||
uses: actions/upload-artifact@v1
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: youtubedl-material
|
||||
path: build
|
||||
|
||||
17
.github/workflows/docker-pr.yml
vendored
17
.github/workflows/docker-pr.yml
vendored
@@ -18,10 +18,21 @@ jobs:
|
||||
run: echo "::set-output name=date::$(date +'%Y-%m-%d')"
|
||||
- name: create-json
|
||||
id: create-json
|
||||
uses: jsdaniell/create-json@1.1.2
|
||||
uses: jsdaniell/create-json@v1.2.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
|
||||
- name: setup platform emulator
|
||||
uses: docker/setup-qemu-action@v1
|
||||
- name: setup multi-arch docker build
|
||||
uses: docker/setup-buildx-action@v1
|
||||
- name: build & push images
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
platforms: linux/amd64,linux/arm/v7,linux/arm64/v8
|
||||
#platforms: linux/amd64
|
||||
push: false
|
||||
tags: tzahi12345/youtubedl-material:nightly-pr
|
||||
|
||||
8
.github/workflows/docker-release.yml
vendored
8
.github/workflows/docker-release.yml
vendored
@@ -27,7 +27,7 @@ jobs:
|
||||
|
||||
- name: create-json
|
||||
id: create-json
|
||||
uses: jsdaniell/create-json@1.1.2
|
||||
uses: jsdaniell/create-json@v1.2.2
|
||||
with:
|
||||
name: "version.json"
|
||||
json: '{"type": "docker", "tag": "latest", "commit": "${{ steps.vars.outputs.sha_short }}", "date": "${{ steps.date.outputs.date }}"}'
|
||||
@@ -60,10 +60,10 @@ jobs:
|
||||
uses: docker/setup-qemu-action@v1
|
||||
|
||||
- name: setup multi-arch docker build
|
||||
uses: docker/setup-buildx-action@v1
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v1
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
@@ -80,7 +80,7 @@ jobs:
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
platforms: linux/amd64,linux/arm,linux/arm64/v8
|
||||
platforms: linux/amd64,linux/arm64/v8
|
||||
push: true
|
||||
tags: ${{ steps.docker-meta.outputs.tags }}
|
||||
labels: ${{ steps.docker-meta.outputs.labels }}
|
||||
|
||||
8
.github/workflows/docker.yml
vendored
8
.github/workflows/docker.yml
vendored
@@ -34,7 +34,7 @@ jobs:
|
||||
|
||||
- name: create-json
|
||||
id: create-json
|
||||
uses: jsdaniell/create-json@1.1.2
|
||||
uses: jsdaniell/create-json@v1.2.2
|
||||
with:
|
||||
name: "version.json"
|
||||
json: '{"type": "docker", "tag": "${{secrets.DOCKERHUB_MASTER_TAG}}", "commit": "${{ steps.vars.outputs.sha_short }}", "date": "${{ steps.date.outputs.date }}"}'
|
||||
@@ -44,7 +44,7 @@ jobs:
|
||||
uses: docker/setup-qemu-action@v1
|
||||
|
||||
- name: setup multi-arch docker build
|
||||
uses: docker/setup-buildx-action@v1
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
- name: Generate Docker image metadata
|
||||
id: docker-meta
|
||||
@@ -63,7 +63,7 @@ jobs:
|
||||
type=sha,prefix=sha-,format=short
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v1
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
@@ -80,7 +80,7 @@ jobs:
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
platforms: linux/amd64,linux/arm,linux/arm64/v8
|
||||
platforms: linux/amd64,linux/arm64/v8,linux/arm/v7
|
||||
push: true
|
||||
tags: ${{ steps.docker-meta.outputs.tags }}
|
||||
labels: ${{ steps.docker-meta.outputs.labels }}
|
||||
|
||||
14
.vscode/launch.json
vendored
14
.vscode/launch.json
vendored
@@ -4,6 +4,20 @@
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Dev: Debug Backend",
|
||||
"request": "launch",
|
||||
"runtimeArgs": [
|
||||
"run-script",
|
||||
"debug"
|
||||
],
|
||||
"runtimeExecutable": "npm",
|
||||
"skipFiles": [
|
||||
"<node_internals>/**"
|
||||
],
|
||||
"type": "node",
|
||||
"cwd": "${workspaceFolder}/backend"
|
||||
},
|
||||
{
|
||||
"type": "node",
|
||||
"request": "attach",
|
||||
|
||||
38
DEVELOPMENT.md
Normal file
38
DEVELOPMENT.md
Normal file
@@ -0,0 +1,38 @@
|
||||
<h1>Development</h1>
|
||||
|
||||
- [First time...](#first-time)
|
||||
- [Setup](#setup)
|
||||
- [Startup](#startup)
|
||||
- [Debugging the backend (VSC)](#debugging-the-backend-vsc)
|
||||
- [Deploy changes](#deploy-changes)
|
||||
- [Frontend](#frontend)
|
||||
- [Backend](#backend)
|
||||
|
||||
# First time...
|
||||
|
||||
## Setup
|
||||
Checkout the repository and navigate to the `youtubedl-material` directory.
|
||||
```bash
|
||||
vim ./src/assets/default.json # Edit settings for your local environment. This config file is just the dev config file, if YTDL_MODE is not set to "debug", then ./backend/appdata/default.json will be used
|
||||
npm -g install pm2 # Install pm2
|
||||
npm install # Install dependencies for the frontend
|
||||
cd ./backend
|
||||
npm install # Install dependencies for the backend
|
||||
cd ..
|
||||
npm run build # Build the frontend
|
||||
```
|
||||
This step have to be done only once.
|
||||
|
||||
## Startup
|
||||
Navigate to the `youtubedl-material/backend` directory and run `npm start`.
|
||||
|
||||
# Debugging the backend (VSC)
|
||||
Open the `youtubedl-material` directory in Visual Studio Code and run the launch configuration `Dev: Debug Backend`.
|
||||
|
||||
# Deploy changes
|
||||
|
||||
## Frontend
|
||||
Navigate to the `youtubedl-material` directory and run `npm run build`. Restart the backend.
|
||||
|
||||
## Backend
|
||||
Simply restart the backend.
|
||||
67
Dockerfile
67
Dockerfile
@@ -1,15 +1,17 @@
|
||||
# Fetching our ffmpeg
|
||||
FROM ubuntu:22.04 AS ffmpeg
|
||||
# Fetching our utils
|
||||
FROM ubuntu:22.04 AS utils
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
# Use script due local build compability
|
||||
COPY docker-utils/ffmpeg-fetch.sh .
|
||||
RUN chmod +x ffmpeg-fetch.sh
|
||||
COPY docker-utils/*.sh .
|
||||
RUN chmod +x *.sh
|
||||
RUN sh ./ffmpeg-fetch.sh
|
||||
RUN sh ./fetch-twitchdownloader.sh
|
||||
|
||||
|
||||
# Create our Ubuntu 22.04 with node 16.14.2 (that specific version is required as per: https://stackoverflow.com/a/72855258/8088021)
|
||||
# Go to 20.04
|
||||
FROM ubuntu:20.04 AS base
|
||||
FROM ubuntu:22.04 AS base
|
||||
ARG TARGETPLATFORM
|
||||
ARG DEBIAN_FRONTEND=noninteractive
|
||||
ENV UID=1000
|
||||
ENV GID=1000
|
||||
@@ -17,19 +19,30 @@ ENV USER=youtube
|
||||
ENV NO_UPDATE_NOTIFIER=true
|
||||
ENV PM2_HOME=/app/pm2
|
||||
ENV ALLOW_CONFIG_MUTATIONS=true
|
||||
# Directy fetch specific version
|
||||
## https://deb.nodesource.com/node_16.x/pool/main/n/nodejs/nodejs_16.14.2-deb-1nodesource1_amd64.deb
|
||||
RUN groupadd -g $GID $USER && useradd --system -m -g $USER --uid $UID $USER && \
|
||||
apt update && \
|
||||
apt install -y --no-install-recommends curl ca-certificates tzdata && \
|
||||
curl -fsSL https://deb.nodesource.com/setup_16.x | bash - && \
|
||||
apt install -y --no-install-recommends nodejs && \
|
||||
npm -g install npm n && \
|
||||
n 16.14.2 && \
|
||||
apt install -y --no-install-recommends curl ca-certificates tzdata libicu70 && \
|
||||
apt clean && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
RUN case ${TARGETPLATFORM} in \
|
||||
"linux/amd64") NODE_ARCH=amd64 ;; \
|
||||
"linux/arm") NODE_ARCH=armhf ;; \
|
||||
"linux/arm/v7") NODE_ARCH=armhf ;; \
|
||||
"linux/arm64") NODE_ARCH=arm64 ;; \
|
||||
esac \
|
||||
&& curl -L https://deb.nodesource.com/node_16.x/pool/main/n/nodejs/nodejs_16.14.2-deb-1nodesource1_$NODE_ARCH.deb -o ./nodejs.deb && \
|
||||
apt update && \
|
||||
apt install -y ./nodejs.deb && \
|
||||
apt clean && \
|
||||
rm -rf /var/lib/apt/lists/* &&\
|
||||
rm nodejs.deb;
|
||||
|
||||
|
||||
# Build frontend
|
||||
FROM base as frontend
|
||||
ARG BUILDPLATFORM
|
||||
FROM --platform=${BUILDPLATFORM} node:16 as frontend
|
||||
RUN npm install -g @angular/cli
|
||||
WORKDIR /build
|
||||
COPY [ "package.json", "package-lock.json", "angular.json", "tsconfig.json", "/build/" ]
|
||||
@@ -37,6 +50,8 @@ COPY [ "src/", "/build/src/" ]
|
||||
RUN npm install && \
|
||||
npm run build && \
|
||||
ls -al /build/backend/public
|
||||
RUN npm uninstall -g @angular/cli
|
||||
RUN rm -rf node_modules
|
||||
|
||||
|
||||
# Install backend deps
|
||||
@@ -47,31 +62,35 @@ RUN npm config set strict-ssl false && \
|
||||
npm install --prod && \
|
||||
ls -al
|
||||
|
||||
FROM base as python
|
||||
WORKDIR /app
|
||||
COPY docker-utils/GetTwitchDownloader.py .
|
||||
RUN apt update && \
|
||||
apt install -y --no-install-recommends python3-minimal python-is-python3 python3-pip && \
|
||||
apt clean && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
RUN pip install PyGithub requests
|
||||
RUN python GetTwitchDownloader.py
|
||||
#FROM base as python
|
||||
# armv7 need build from source
|
||||
#WORKDIR /app
|
||||
#COPY docker-utils/GetTwitchDownloader.py .
|
||||
#RUN apt update && \
|
||||
# apt install -y --no-install-recommends python3-minimal python-is-python3 python3-pip python3-dev build-essential libffi-dev && \
|
||||
# apt clean && \
|
||||
# rm -rf /var/lib/apt/lists/*
|
||||
#RUN pip install PyGithub requests
|
||||
#RUN python GetTwitchDownloader.py
|
||||
|
||||
# Final image
|
||||
FROM base
|
||||
RUN npm install -g pm2 && \
|
||||
apt update && \
|
||||
apt install -y --no-install-recommends gosu python3-minimal python-is-python3 python3-pip atomicparsley build-essential && \
|
||||
pip install pycryptodomex && \
|
||||
apt remove -y --purge build-essential && \
|
||||
apt autoremove -y --purge && \
|
||||
apt clean && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
RUN pip install pycryptodomex
|
||||
WORKDIR /app
|
||||
# User 1000 already exist from base image
|
||||
COPY --chown=$UID:$GID --from=ffmpeg [ "/usr/local/bin/ffmpeg", "/usr/local/bin/ffmpeg" ]
|
||||
COPY --chown=$UID:$GID --from=ffmpeg [ "/usr/local/bin/ffprobe", "/usr/local/bin/ffprobe" ]
|
||||
COPY --chown=$UID:$GID --from=utils [ "/usr/local/bin/ffmpeg", "/usr/local/bin/ffmpeg" ]
|
||||
COPY --chown=$UID:$GID --from=utils [ "/usr/local/bin/ffprobe", "/usr/local/bin/ffprobe" ]
|
||||
COPY --chown=$UID:$GID --from=utils [ "/usr/local/bin/TwitchDownloaderCLI", "/usr/local/bin/TwitchDownloaderCLI"]
|
||||
COPY --chown=$UID:$GID --from=backend ["/app/","/app/"]
|
||||
COPY --chown=$UID:$GID --from=frontend [ "/build/backend/public/", "/app/public/" ]
|
||||
RUN chown $UID:$GID .
|
||||
#COPY --chown=$UID:$GID --from=python ["/app/TwitchDownloaderCLI","/usr/local/bin/TwitchDownloaderCLI"]
|
||||
RUN chmod +x /app/fix-scripts/*.sh
|
||||
# Add some persistence data
|
||||
#VOLUME ["/app/appdata"]
|
||||
|
||||
@@ -1758,14 +1758,14 @@ components:
|
||||
type: object
|
||||
properties:
|
||||
task_key:
|
||||
type: string
|
||||
$ref: '#/components/schemas/TaskType'
|
||||
required:
|
||||
- task_key
|
||||
UpdateTaskScheduleRequest:
|
||||
type: object
|
||||
properties:
|
||||
task_key:
|
||||
type: string
|
||||
$ref: '#/components/schemas/TaskType'
|
||||
new_schedule:
|
||||
$ref: '#/components/schemas/Schedule'
|
||||
required:
|
||||
@@ -1775,7 +1775,7 @@ components:
|
||||
type: object
|
||||
properties:
|
||||
task_key:
|
||||
type: string
|
||||
$ref: '#/components/schemas/TaskType'
|
||||
new_data:
|
||||
type: object
|
||||
required:
|
||||
@@ -1785,7 +1785,7 @@ components:
|
||||
type: object
|
||||
properties:
|
||||
task_key:
|
||||
type: string
|
||||
$ref: '#/components/schemas/TaskType'
|
||||
new_options:
|
||||
type: object
|
||||
required:
|
||||
@@ -2726,7 +2726,7 @@ components:
|
||||
type: object
|
||||
properties:
|
||||
key:
|
||||
type: string
|
||||
$ref: '#/components/schemas/TaskType'
|
||||
title:
|
||||
type: string
|
||||
last_ran:
|
||||
@@ -2742,9 +2742,20 @@ components:
|
||||
error:
|
||||
type: string
|
||||
schedule:
|
||||
type: object
|
||||
$ref: '#/components/schemas/Schedule'
|
||||
options:
|
||||
type: object
|
||||
TaskType:
|
||||
type: string
|
||||
enum:
|
||||
- backup_local_db
|
||||
- missing_files_check
|
||||
- missing_db_records
|
||||
- duplicate_files_check
|
||||
- youtubedl_update_check
|
||||
- delete_old_files
|
||||
- import_legacy_archives
|
||||
- rebuild_database
|
||||
Schedule:
|
||||
required:
|
||||
- type
|
||||
@@ -2877,6 +2888,7 @@ components:
|
||||
- sharing
|
||||
- advanced_download
|
||||
- downloads_manager
|
||||
- tasks_manager
|
||||
YesNo:
|
||||
type: string
|
||||
enum:
|
||||
|
||||
29
README.md
29
README.md
@@ -28,13 +28,28 @@ Dark mode:
|
||||
|
||||
NOTE: If you would like to use Docker, you can skip down to the [Docker](#Docker) section for a setup guide.
|
||||
|
||||
Debian/Ubuntu:
|
||||
Required dependencies:
|
||||
|
||||
* Node.js 16
|
||||
* Python
|
||||
|
||||
Optional dependencies:
|
||||
|
||||
* AtomicParsley (for embedding thumbnails, package name `atomicparsley`)
|
||||
* [Twitch Downloader CLI](https://github.com/lay295/TwitchDownloader) (for downloading Twitch VOD chats)
|
||||
|
||||
<details>
|
||||
<summary>Debian/Ubuntu</summary>
|
||||
|
||||
```bash
|
||||
curl -fsSL https://deb.nodesource.com/setup_16.x | sudo -E bash -
|
||||
sudo apt-get install nodejs youtube-dl ffmpeg unzip python npm
|
||||
```
|
||||
|
||||
CentOS 7:
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>CentOS 7</summary>
|
||||
|
||||
```bash
|
||||
sudo yum install epel-release
|
||||
@@ -42,13 +57,11 @@ sudo yum localinstall --nogpgcheck https://download1.rpmfusion.org/free/el/rpmfu
|
||||
sudo yum install centos-release-scl-rh
|
||||
sudo yum install rh-nodejs12
|
||||
scl enable rh-nodejs12 bash
|
||||
curl -fsSL https://rpm.nodesource.com/setup_16.x | sudo bash -
|
||||
sudo yum install nodejs youtube-dl ffmpeg ffmpeg-devel
|
||||
```
|
||||
|
||||
Optional dependencies:
|
||||
|
||||
* AtomicParsley (for embedding thumbnails, package name `atomicparsley`)
|
||||
* [tcd](https://github.com/PetterKraabol/Twitch-Chat-Downloader) (for downloading Twitch VOD chats)
|
||||
</details>
|
||||
|
||||
### Installing
|
||||
|
||||
@@ -72,7 +85,9 @@ If you'd like to install YoutubeDL-Material, go to the Installation section. If
|
||||
|
||||
To deploy, simply clone the repository, and go into the `youtubedl-material` directory. Type `npm install` and all the dependencies will install. Then type `cd backend` and again type `npm install` to install the dependencies for the backend.
|
||||
|
||||
Once you do that, you're almost up and running. All you need to do is edit the configuration in `youtubedl-material/appdata`, go back into the `youtubedl-material` directory, and type `npm build`. 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 run build`. This will build the app, and put the output files in the `youtubedl-material/backend/public` folder.
|
||||
|
||||
Lastly, type `npm -g install pm2` to install pm2 globally.
|
||||
|
||||
The frontend is now complete. The backend is much easier. Just go into the `backend` folder, and type `npm start`.
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ const { uuid } = require('uuidv4');
|
||||
const fs = require('fs-extra');
|
||||
const { promisify } = require('util');
|
||||
const auth_api = require('./authentication/auth');
|
||||
const winston = require('winston');
|
||||
const path = require('path');
|
||||
const compression = require('compression');
|
||||
const multer = require('multer');
|
||||
@@ -19,6 +18,7 @@ const CONSTS = require('./consts')
|
||||
const read_last_lines = require('read-last-lines');
|
||||
const ps = require('ps-node');
|
||||
const Feed = require('feed').Feed;
|
||||
const session = require('express-session');
|
||||
|
||||
// needed if bin/details somehow gets deleted
|
||||
if (!fs.existsSync(CONSTS.DETAILS_BIN_PATH)) fs.writeJSONSync(CONSTS.DETAILS_BIN_PATH, {"version":"2000.06.06","path":"node_modules\\youtube-dl\\bin\\youtube-dl.exe","exec":"youtube-dl.exe","downloader":"youtube-dl"})
|
||||
@@ -163,6 +163,7 @@ app.use(bodyParser.json());
|
||||
|
||||
// use passport
|
||||
app.use(auth_api.passport.initialize());
|
||||
app.use(session({ secret: uuid(), resave: true, saveUninitialized: true }))
|
||||
app.use(auth_api.passport.session());
|
||||
|
||||
// actual functions
|
||||
@@ -534,6 +535,7 @@ async function loadConfig() {
|
||||
if (allowSubscriptions) {
|
||||
// set downloading to false
|
||||
let subscriptions = await subscriptions_api.getAllSubscriptions();
|
||||
subscriptions.forEach(async sub => subscriptions_api.writeSubscriptionMetadata(sub));
|
||||
subscriptions_api.updateSubscriptionPropertyMultiple(subscriptions, {downloading: false});
|
||||
// runs initially, then runs every ${subscriptionCheckInterval} seconds
|
||||
const watchSubscriptionsInterval = function() {
|
||||
@@ -1927,9 +1929,38 @@ app.post('/api/clearAllLogs', optionalJwt, async function(req, res) {
|
||||
|
||||
// user authentication
|
||||
|
||||
app.post('/api/auth/register'
|
||||
, optionalJwt
|
||||
, auth_api.registerUser);
|
||||
app.post('/api/auth/register', optionalJwt, async (req, res) => {
|
||||
const userid = req.body.userid;
|
||||
const username = req.body.username;
|
||||
const plaintextPassword = req.body.password;
|
||||
|
||||
if (userid !== 'admin' && !config_api.getConfigItem('ytdl_allow_registration') && !req.isAuthenticated() && (!req.user || !exports.userHasPermission(req.user.uid, 'settings'))) {
|
||||
logger.error(`Registration failed for user ${userid}. Registration is disabled.`);
|
||||
res.sendStatus(409);
|
||||
return;
|
||||
}
|
||||
|
||||
if (plaintextPassword === "") {
|
||||
logger.error(`Registration failed for user ${userid}. A password must be provided.`);
|
||||
res.sendStatus(409);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!userid || !username) {
|
||||
logger.error(`Registration failed for user ${userid}. Username or userid is invalid.`);
|
||||
}
|
||||
|
||||
const new_user = await auth_api.registerUser(userid, username, plaintextPassword);
|
||||
|
||||
if (!new_user) {
|
||||
res.sendStatus(409);
|
||||
return;
|
||||
}
|
||||
|
||||
res.send({
|
||||
user: new_user
|
||||
});
|
||||
});
|
||||
app.post('/api/auth/login'
|
||||
, auth_api.passport.authenticate(['local', 'ldapauth'], {})
|
||||
, auth_api.generateJWT
|
||||
@@ -1981,18 +2012,7 @@ app.post('/api/updateUser', optionalJwt, async (req, res) => {
|
||||
app.post('/api/deleteUser', optionalJwt, async (req, res) => {
|
||||
let uid = req.body.uid;
|
||||
try {
|
||||
let success = false;
|
||||
let usersFileFolder = config_api.getConfigItem('ytdl_users_base_path');
|
||||
const user_folder = path.join(__dirname, usersFileFolder, uid);
|
||||
const user_db_obj = await db_api.getRecord('users', {uid: uid});
|
||||
if (user_db_obj) {
|
||||
// user exists, let's delete
|
||||
await fs.remove(user_folder);
|
||||
await db_api.removeRecord('users', {uid: uid});
|
||||
success = true;
|
||||
} else {
|
||||
logger.error(`Could not find user with uid ${uid}`);
|
||||
}
|
||||
const success = await auth_api.deleteUser(uid);
|
||||
res.send({success: success});
|
||||
} catch (err) {
|
||||
logger.error(err);
|
||||
|
||||
@@ -50,7 +50,8 @@
|
||||
"telegram_bot_token": "",
|
||||
"telegram_chat_id": "",
|
||||
"webhook_URL": "",
|
||||
"discord_webhook_URL": ""
|
||||
"discord_webhook_URL": "",
|
||||
"slack_webhook_URL": ""
|
||||
},
|
||||
"Themes": {
|
||||
"default_theme": "default",
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
const config_api = require('../config');
|
||||
const consts = require('../consts');
|
||||
const CONSTS = require('../consts');
|
||||
const logger = require('../logger');
|
||||
const db_api = require('../db');
|
||||
|
||||
const jwt = require('jsonwebtoken');
|
||||
const { uuid } = require('uuidv4');
|
||||
const bcrypt = require('bcryptjs');
|
||||
const fs = require('fs-extra');
|
||||
const path = require('path');
|
||||
|
||||
var LocalStrategy = require('passport-local').Strategy;
|
||||
var LdapStrategy = require('passport-ldapauth');
|
||||
@@ -16,7 +18,7 @@ var JwtStrategy = require('passport-jwt').Strategy,
|
||||
let SERVER_SECRET = null;
|
||||
let JWT_EXPIRATION = null;
|
||||
let opts = null;
|
||||
let saltRounds = null;
|
||||
let saltRounds = 10;
|
||||
|
||||
exports.initialize = function () {
|
||||
/*************************
|
||||
@@ -31,8 +33,6 @@ exports.initialize = function () {
|
||||
});
|
||||
}
|
||||
|
||||
saltRounds = 10;
|
||||
|
||||
// Sometimes this value is not properly typed: https://github.com/Tzahi12345/YoutubeDL-Material/issues/813
|
||||
JWT_EXPIRATION = config_api.getConfigItem('ytdl_jwt_expiration');
|
||||
if (!(+JWT_EXPIRATION)) {
|
||||
@@ -68,14 +68,7 @@ exports.initialize = function () {
|
||||
const setupRoles = async () => {
|
||||
const required_roles = {
|
||||
admin: {
|
||||
permissions: [
|
||||
'filemanager',
|
||||
'settings',
|
||||
'subscriptions',
|
||||
'sharing',
|
||||
'advanced_download',
|
||||
'downloads_manager'
|
||||
]
|
||||
permissions: CONSTS.AVAILABLE_PERMISSIONS
|
||||
},
|
||||
user: {
|
||||
permissions: [
|
||||
@@ -113,55 +106,41 @@ exports.passport.deserializeUser(function(user, done) {
|
||||
/***************************************
|
||||
* Register user with hashed password
|
||||
**************************************/
|
||||
exports.registerUser = async function(req, res) {
|
||||
var userid = req.body.userid;
|
||||
var username = req.body.username;
|
||||
var plaintextPassword = req.body.password;
|
||||
|
||||
if (userid !== 'admin' && !config_api.getConfigItem('ytdl_allow_registration') && !req.isAuthenticated() && (!req.user || !exports.userHasPermission(req.user.uid, 'settings'))) {
|
||||
res.sendStatus(409);
|
||||
logger.error(`Registration failed for user ${userid}. Registration is disabled.`);
|
||||
return;
|
||||
exports.registerUser = async (userid, username, plaintextPassword) => {
|
||||
const hash = await bcrypt.hash(plaintextPassword, saltRounds);
|
||||
const new_user = generateUserObject(userid, username, hash);
|
||||
// check if user exists
|
||||
if (await db_api.getRecord('users', {uid: userid})) {
|
||||
// user id is taken!
|
||||
logger.error('Registration failed: UID is already taken!');
|
||||
return null;
|
||||
} else if (await db_api.getRecord('users', {name: username})) {
|
||||
// user name is taken!
|
||||
logger.error('Registration failed: User name is already taken!');
|
||||
return null;
|
||||
} else {
|
||||
// add to db
|
||||
await db_api.insertRecordIntoTable('users', new_user);
|
||||
logger.verbose(`New user created: ${new_user.name}`);
|
||||
return new_user;
|
||||
}
|
||||
}
|
||||
|
||||
if (plaintextPassword === "") {
|
||||
res.sendStatus(400);
|
||||
logger.error(`Registration failed for user ${userid}. A password must be provided.`);
|
||||
return;
|
||||
exports.deleteUser = async (uid) => {
|
||||
let success = false;
|
||||
let usersFileFolder = config_api.getConfigItem('ytdl_users_base_path');
|
||||
const user_folder = path.join(__dirname, usersFileFolder, uid);
|
||||
const user_db_obj = await db_api.getRecord('users', {uid: uid});
|
||||
if (user_db_obj) {
|
||||
// user exists, let's delete
|
||||
await fs.remove(user_folder);
|
||||
await db_api.removeRecord('users', {uid: uid});
|
||||
success = true;
|
||||
} else {
|
||||
logger.error(`Could not find user with uid ${uid}`);
|
||||
}
|
||||
|
||||
bcrypt.hash(plaintextPassword, saltRounds)
|
||||
.then(async function(hash) {
|
||||
let new_user = generateUserObject(userid, username, hash);
|
||||
// check if user exists
|
||||
if (await db_api.getRecord('users', {uid: userid})) {
|
||||
// user id is taken!
|
||||
logger.error('Registration failed: UID is already taken!');
|
||||
res.status(409).send('UID is already taken!');
|
||||
} else if (await db_api.getRecord('users', {name: username})) {
|
||||
// user name is taken!
|
||||
logger.error('Registration failed: User name is already taken!');
|
||||
res.status(409).send('User name is already taken!');
|
||||
} else {
|
||||
// add to db
|
||||
await db_api.insertRecordIntoTable('users', new_user);
|
||||
logger.verbose(`New user created: ${new_user.name}`);
|
||||
res.send({
|
||||
user: new_user
|
||||
});
|
||||
}
|
||||
})
|
||||
.then(function(result) {
|
||||
|
||||
})
|
||||
.catch(function(err) {
|
||||
logger.error(err);
|
||||
if( err.code == 'ER_DUP_ENTRY' ) {
|
||||
res.status(409).send('UserId already taken');
|
||||
} else {
|
||||
res.sendStatus(409);
|
||||
}
|
||||
});
|
||||
return success;
|
||||
}
|
||||
|
||||
/***************************************
|
||||
@@ -242,7 +221,7 @@ exports.returnAuthResponse = async function(req, res) {
|
||||
user: req.user,
|
||||
token: req.token,
|
||||
permissions: await exports.userPermissions(req.user.uid),
|
||||
available_permissions: consts['AVAILABLE_PERMISSIONS']
|
||||
available_permissions: CONSTS.AVAILABLE_PERMISSIONS
|
||||
});
|
||||
}
|
||||
|
||||
@@ -326,7 +305,7 @@ exports.getUserVideos = async function(user_uid, type) {
|
||||
}
|
||||
|
||||
exports.getUserVideo = async function(user_uid, file_uid, requireSharing = false) {
|
||||
let file = await db_api.getRecord('files', {file_uid: file_uid});
|
||||
let file = await db_api.getRecord('files', {uid: file_uid});
|
||||
|
||||
// prevent unauthorized users from accessing the file info
|
||||
if (file && !file['sharingEnabled'] && requireSharing) file = null;
|
||||
@@ -413,8 +392,8 @@ exports.userPermissions = async function(user_uid) {
|
||||
const role_obj = await db_api.getRecord('roles', {key: role});
|
||||
const role_permissions = role_obj['permissions'];
|
||||
|
||||
for (let i = 0; i < consts['AVAILABLE_PERMISSIONS'].length; i++) {
|
||||
let permission = consts['AVAILABLE_PERMISSIONS'][i];
|
||||
for (let i = 0; i < CONSTS.AVAILABLE_PERMISSIONS.length; i++) {
|
||||
let permission = CONSTS.AVAILABLE_PERMISSIONS[i];
|
||||
|
||||
const user_has_explicit_permission = user_obj['permissions'].includes(permission);
|
||||
const permission_in_overrides = user_obj['permission_overrides'].includes(permission);
|
||||
|
||||
@@ -220,7 +220,8 @@ const DEFAULT_CONFIG = {
|
||||
"telegram_bot_token": "",
|
||||
"telegram_chat_id": "",
|
||||
"webhook_URL": "",
|
||||
"discord_webhook_URL": ""
|
||||
"discord_webhook_URL": "",
|
||||
"slack_webhook_URL": "",
|
||||
},
|
||||
"Themes": {
|
||||
"default_theme": "default",
|
||||
|
||||
@@ -162,6 +162,10 @@ exports.CONFIG_ITEMS = {
|
||||
'key': 'ytdl_discord_webhook_url',
|
||||
'path': 'YoutubeDLMaterial.API.discord_webhook_URL'
|
||||
},
|
||||
'ytdl_slack_webhook_url': {
|
||||
'key': 'ytdl_slack_webhook_url',
|
||||
'path': 'YoutubeDLMaterial.API.slack_webhook_URL'
|
||||
},
|
||||
|
||||
|
||||
// Themes
|
||||
@@ -343,6 +347,8 @@ const YTDL_ARGS_WITH_VALUES = [
|
||||
'--convert-subs'
|
||||
];
|
||||
|
||||
exports.SUBSCRIPTION_BACKUP_PATH = 'subscription_backup.json'
|
||||
|
||||
// we're using a Set here for performance
|
||||
exports.YTDL_ARGS_WITH_VALUES = new Set(YTDL_ARGS_WITH_VALUES);
|
||||
|
||||
|
||||
@@ -169,7 +169,7 @@ exports._connectToDB = async (custom_connection_string = null) => {
|
||||
|
||||
exports.setVideoProperty = async (file_uid, assignment_obj) => {
|
||||
// TODO: check if video exists, throw error if not
|
||||
await db_api.updateRecord('files', {uid: file_uid}, assignment_obj);
|
||||
await exports.updateRecord('files', {uid: file_uid}, assignment_obj);
|
||||
}
|
||||
|
||||
exports.getFileDirectoriesAndDBs = async () => {
|
||||
|
||||
@@ -1,17 +1,7 @@
|
||||
#!/bin/sh
|
||||
#!/bin/bash
|
||||
set -eu
|
||||
|
||||
CMD="npm start"
|
||||
|
||||
# if the first arg starts with "-" pass it to program
|
||||
if [ "${1#-}" != "$1" ]; then
|
||||
set -- "$CMD" "$@"
|
||||
fi
|
||||
|
||||
# chown current working directory to current user
|
||||
if [ "$*" = "$CMD" ] && [ "$(id -u)" = "0" ]; then
|
||||
find . \! -user "$UID" -exec chown "$UID:$GID" '{}' + || echo "WARNING! Could not change directory ownership. If you manage permissions externally this is fine, otherwise you may experience issues when downloading or deleting videos."
|
||||
exec gosu "$UID:$GID" "$0" "$@"
|
||||
fi
|
||||
|
||||
exec "$@"
|
||||
echo "[entrypoint] setup permission, this may take a while"
|
||||
find . \! -user "$UID" -exec chown "$UID:$GID" '{}' + || echo "WARNING! Could not change directory ownership. If you manage permissions externally this is fine, otherwise you may experience issues when downloading or deleting videos."
|
||||
exec gosu "$UID:$GID" "$@"
|
||||
|
||||
@@ -64,6 +64,9 @@ exports.sendNotification = async (notification) => {
|
||||
if (config_api.getConfigItem('ytdl_discord_webhook_url')) {
|
||||
sendDiscordNotification(data);
|
||||
}
|
||||
if (config_api.getConfigItem('ytdl_slack_webhook_url')) {
|
||||
sendSlackNotification(data);
|
||||
}
|
||||
|
||||
await db_api.insertRecordIntoTable('notifications', notification);
|
||||
return notification;
|
||||
@@ -174,6 +177,65 @@ async function sendDiscordNotification({body, title, type, url, thumbnail}) {
|
||||
return result;
|
||||
}
|
||||
|
||||
function sendSlackNotification({body, title, type, url, thumbnail}) {
|
||||
const slack_webhook_url = config_api.getConfigItem('ytdl_slack_webhook_url');
|
||||
logger.verbose(`Sending slack notification to ${slack_webhook_url}`);
|
||||
const data = {
|
||||
blocks: [
|
||||
{
|
||||
type: "section",
|
||||
text: {
|
||||
type: "mrkdwn",
|
||||
text: `*${title}*`
|
||||
}
|
||||
},
|
||||
{
|
||||
type: "section",
|
||||
text: {
|
||||
type: "plain_text",
|
||||
text: body
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
// add thumbnail if exists
|
||||
if (thumbnail) {
|
||||
data['blocks'].push({
|
||||
type: "image",
|
||||
image_url: thumbnail,
|
||||
alt_text: "notification_thumbnail"
|
||||
});
|
||||
}
|
||||
|
||||
data['blocks'].push(
|
||||
{
|
||||
type: "section",
|
||||
text: {
|
||||
type: "mrkdwn",
|
||||
text: `<${url}|${url}>`
|
||||
}
|
||||
},
|
||||
{
|
||||
type: "context",
|
||||
elements: [
|
||||
{
|
||||
type: "mrkdwn",
|
||||
text: `*ID:* ${type}`
|
||||
}
|
||||
]
|
||||
}
|
||||
);
|
||||
|
||||
fetch(slack_webhook_url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
}
|
||||
|
||||
function sendGenericNotification(data) {
|
||||
const webhook_url = config_api.getConfigItem('ytdl_webhook_url');
|
||||
logger.verbose(`Sending generic notification to ${webhook_url}`);
|
||||
|
||||
93
backend/package-lock.json
generated
93
backend/package-lock.json
generated
@@ -769,6 +769,11 @@
|
||||
"delayed-stream": "~1.0.0"
|
||||
}
|
||||
},
|
||||
"command-exists": {
|
||||
"version": "1.2.9",
|
||||
"resolved": "https://registry.npmjs.org/command-exists/-/command-exists-1.2.9.tgz",
|
||||
"integrity": "sha512-LTQ/SGc+s0Xc0Fu5WaKnR0YiygZkm9eKFvyS+fRsU7/ZWFF8ykFM6Pc9aCVf1+xasOOZpO3BAVgVrKvsqKHV7w=="
|
||||
},
|
||||
"compress-commons": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-4.1.1.tgz",
|
||||
@@ -2036,26 +2041,20 @@
|
||||
}
|
||||
},
|
||||
"jsonwebtoken": {
|
||||
"version": "8.5.1",
|
||||
"resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz",
|
||||
"integrity": "sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w==",
|
||||
"version": "9.0.0",
|
||||
"resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.0.tgz",
|
||||
"integrity": "sha512-tuGfYXxkQGDPnLJ7SibiQgVgeDgfbPq2k2ICcbgqW8WxWLBAxKQM/ZCu/IT8SOSwmaYl4dpTFCW5xZv7YbbWUw==",
|
||||
"requires": {
|
||||
"jws": "^3.2.2",
|
||||
"lodash.includes": "^4.3.0",
|
||||
"lodash.isboolean": "^3.0.3",
|
||||
"lodash.isinteger": "^4.0.4",
|
||||
"lodash.isnumber": "^3.0.3",
|
||||
"lodash.isplainobject": "^4.0.6",
|
||||
"lodash.isstring": "^4.0.1",
|
||||
"lodash.once": "^4.0.0",
|
||||
"lodash": "^4.17.21",
|
||||
"ms": "^2.1.1",
|
||||
"semver": "^5.6.0"
|
||||
"semver": "^7.3.8"
|
||||
},
|
||||
"dependencies": {
|
||||
"ms": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -2200,41 +2199,11 @@
|
||||
"resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz",
|
||||
"integrity": "sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8="
|
||||
},
|
||||
"lodash.includes": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
|
||||
"integrity": "sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8="
|
||||
},
|
||||
"lodash.isboolean": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
|
||||
"integrity": "sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY="
|
||||
},
|
||||
"lodash.isinteger": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
|
||||
"integrity": "sha1-YZwK89A/iwTDH1iChAt3sRzWg0M="
|
||||
},
|
||||
"lodash.isnumber": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
|
||||
"integrity": "sha1-POdoEMWSjQM1IwGsKHMX8RwLH/w="
|
||||
},
|
||||
"lodash.isplainobject": {
|
||||
"version": "4.0.6",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
|
||||
"integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs="
|
||||
},
|
||||
"lodash.isstring": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
|
||||
"integrity": "sha1-1SfftUVuynzJu5XV2ur4i6VKVFE="
|
||||
},
|
||||
"lodash.once": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
|
||||
"integrity": "sha1-DdOXEhPHxW34gJd9UEyI+0cal6w="
|
||||
},
|
||||
"lodash.union": {
|
||||
"version": "4.6.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz",
|
||||
@@ -2633,11 +2602,21 @@
|
||||
}
|
||||
},
|
||||
"node-id3": {
|
||||
"version": "0.1.16",
|
||||
"resolved": "https://registry.npmjs.org/node-id3/-/node-id3-0.1.16.tgz",
|
||||
"integrity": "sha512-neWBJZxwrWnnebqy0b6gOGpnOPu1l1ASlusVCJUlrgr55ksftcz3lPbP/h4KaFXN+WQX7hh+kmNwkj5DMAa7KA==",
|
||||
"version": "0.2.6",
|
||||
"resolved": "https://registry.npmjs.org/node-id3/-/node-id3-0.2.6.tgz",
|
||||
"integrity": "sha512-w8GuKXLlPpDjTxLowCt/uYMhRQzED3cg2GdSG1i6RSGKeDzPvxlXeLQuQInKljahPZ0aDnmyX7FX8BbJOM7REg==",
|
||||
"requires": {
|
||||
"iconv-lite": "^0.4.15"
|
||||
"iconv-lite": "0.6.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"iconv-lite": {
|
||||
"version": "0.6.2",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.2.tgz",
|
||||
"integrity": "sha512-2y91h5OpQlolefMPmUlivelittSWy0rP+oYVpn6A7GwVHNE8AWzoYOBNmlwks3LobaJxgHCYZAnyNo2GgpNRNQ==",
|
||||
"requires": {
|
||||
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"node-schedule": {
|
||||
@@ -2862,12 +2841,13 @@
|
||||
"integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="
|
||||
},
|
||||
"passport": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/passport/-/passport-0.4.1.tgz",
|
||||
"integrity": "sha512-IxXgZZs8d7uFSt3eqNjM9NQ3g3uQCW5avD8mRNoXV99Yig50vjuaez6dQK2qC0kVWPRTujxY0dWgGfT09adjYg==",
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/passport/-/passport-0.6.0.tgz",
|
||||
"integrity": "sha512-0fe+p3ZnrWRW74fe8+SvCyf4a3Pb2/h7gFkQ8yTJpAO50gDzlfjZUZTO1k5Eg9kUct22OxHLqDZoKUWRHOh9ug==",
|
||||
"requires": {
|
||||
"passport-strategy": "1.x.x",
|
||||
"pause": "0.0.1"
|
||||
"pause": "0.0.1",
|
||||
"utils-merge": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"passport-http": {
|
||||
@@ -3288,9 +3268,12 @@
|
||||
"integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw=="
|
||||
},
|
||||
"semver": {
|
||||
"version": "5.7.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
|
||||
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ=="
|
||||
"version": "7.5.0",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.0.tgz",
|
||||
"integrity": "sha512-+XC0AD/R7Q2mPSRuy2Id0+CGTZ98+8f+KvwirxOKIEyid+XSx6HbC63p+O4IndTHuX5Z+JxQ0TghCkO5Cg/2HA==",
|
||||
"requires": {
|
||||
"lru-cache": "^6.0.0"
|
||||
}
|
||||
},
|
||||
"send": {
|
||||
"version": "0.18.0",
|
||||
|
||||
@@ -17,6 +17,10 @@
|
||||
"bugs": {
|
||||
"url": ""
|
||||
},
|
||||
"engines": {
|
||||
"node": "^16",
|
||||
"npm": "6.14.4"
|
||||
},
|
||||
"homepage": "",
|
||||
"dependencies": {
|
||||
"@discordjs/builders": "^1.6.1",
|
||||
@@ -26,6 +30,7 @@
|
||||
"async-mutex": "^0.4.0",
|
||||
"axios": "^0.21.2",
|
||||
"bcryptjs": "^2.4.0",
|
||||
"command-exists": "^1.2.9",
|
||||
"compression": "^1.7.4",
|
||||
"config": "^3.2.3",
|
||||
"express": "^4.18.2",
|
||||
@@ -34,7 +39,7 @@
|
||||
"fluent-ffmpeg": "^2.1.2",
|
||||
"fs-extra": "^9.0.0",
|
||||
"gotify": "^1.1.0",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"jsonwebtoken": "^9.0.0",
|
||||
"lodash": "^4.17.21",
|
||||
"lowdb": "^1.0.0",
|
||||
"md5": "^2.2.1",
|
||||
@@ -43,10 +48,10 @@
|
||||
"mongodb": "^3.6.9",
|
||||
"multer": "1.4.5-lts.1",
|
||||
"node-fetch": "^2.6.7",
|
||||
"node-id3": "^0.1.14",
|
||||
"node-id3": "^0.2.6",
|
||||
"node-schedule": "^2.1.0",
|
||||
"node-telegram-bot-api": "^0.61.0",
|
||||
"passport": "^0.4.1",
|
||||
"passport": "^0.6.0",
|
||||
"passport-http": "^0.3.0",
|
||||
"passport-jwt": "^4.0.1",
|
||||
"passport-ldapauth": "^3.0.1",
|
||||
|
||||
@@ -6,20 +6,21 @@ const config_api = require('./config');
|
||||
const archive_api = require('./archive');
|
||||
const utils = require('./utils');
|
||||
const logger = require('./logger');
|
||||
const CONSTS = require('./consts');
|
||||
|
||||
const debugMode = process.env.YTDL_MODE === 'debug';
|
||||
|
||||
const db_api = require('./db');
|
||||
const downloader_api = require('./downloader');
|
||||
|
||||
async function subscribe(sub, user_uid = null) {
|
||||
exports.subscribe = async (sub, user_uid = null, skip_get_info = false) => {
|
||||
const result_obj = {
|
||||
success: false,
|
||||
error: ''
|
||||
};
|
||||
return new Promise(async resolve => {
|
||||
// sub should just have url and name. here we will get isPlaylist and path
|
||||
sub.isPlaylist = sub.url.includes('playlist');
|
||||
sub.isPlaylist = sub.isPlaylist || sub.url.includes('playlist');
|
||||
sub.videos = [];
|
||||
|
||||
let url_exists = !!(await db_api.getRecord('subscriptions', {url: sub.url, user_uid: user_uid}));
|
||||
@@ -34,10 +35,11 @@ async function subscribe(sub, user_uid = null) {
|
||||
sub['user_uid'] = user_uid ? user_uid : undefined;
|
||||
await db_api.insertRecordIntoTable('subscriptions', sub);
|
||||
|
||||
let success = await getSubscriptionInfo(sub);
|
||||
let success = skip_get_info ? true : await getSubscriptionInfo(sub);
|
||||
exports.writeSubscriptionMetadata(sub);
|
||||
|
||||
if (success) {
|
||||
getVideosForSub(sub, user_uid);
|
||||
if (!sub.paused) exports.getVideosForSub(sub, user_uid);
|
||||
} else {
|
||||
logger.error('Subscribe: Failed to get subscription info. Subscribe failed.')
|
||||
}
|
||||
@@ -109,7 +111,7 @@ async function getSubscriptionInfo(sub) {
|
||||
});
|
||||
}
|
||||
|
||||
async function unsubscribe(sub, deleteMode, user_uid = null) {
|
||||
exports.unsubscribe = async (sub, deleteMode, user_uid = null) => {
|
||||
let basePath = null;
|
||||
if (user_uid)
|
||||
basePath = path.join(config_api.getConfigItem('ytdl_users_base_path'), user_uid, 'subscriptions');
|
||||
@@ -148,7 +150,7 @@ async function unsubscribe(sub, deleteMode, user_uid = null) {
|
||||
await db_api.removeAllRecords('archives', {sub_id: sub.id});
|
||||
}
|
||||
|
||||
async function deleteSubscriptionFile(sub, file, deleteForever, file_uid = null, user_uid = null) {
|
||||
exports.deleteSubscriptionFile = async (sub, file, deleteForever, file_uid = null, user_uid = null) => {
|
||||
if (typeof sub === 'string') {
|
||||
// TODO: fix bad workaround where sub is a sub_id
|
||||
sub = await db_api.getRecord('subscriptions', {sub_id: sub});
|
||||
@@ -216,8 +218,8 @@ async function deleteSubscriptionFile(sub, file, deleteForever, file_uid = null,
|
||||
}
|
||||
}
|
||||
|
||||
async function getVideosForSub(sub, user_uid = null) {
|
||||
const latest_sub_obj = await getSubscription(sub.id);
|
||||
exports.getVideosForSub = async (sub, user_uid = null) => {
|
||||
const latest_sub_obj = await exports.getSubscription(sub.id);
|
||||
if (!latest_sub_obj || latest_sub_obj['downloading']) {
|
||||
return false;
|
||||
}
|
||||
@@ -305,7 +307,7 @@ async function handleOutputJSON(output, sub, user_uid) {
|
||||
}
|
||||
|
||||
const files_to_download = await getFilesToDownload(sub, output_jsons);
|
||||
const base_download_options = generateOptionsForSubscriptionDownload(sub, user_uid);
|
||||
const base_download_options = exports.generateOptionsForSubscriptionDownload(sub, user_uid);
|
||||
|
||||
for (let j = 0; j < files_to_download.length; j++) {
|
||||
const file_to_download = files_to_download[j];
|
||||
@@ -316,7 +318,7 @@ async function handleOutputJSON(output, sub, user_uid) {
|
||||
return files_to_download;
|
||||
}
|
||||
|
||||
function generateOptionsForSubscriptionDownload(sub, user_uid) {
|
||||
exports.generateOptionsForSubscriptionDownload = (sub, user_uid) => {
|
||||
let basePath = null;
|
||||
if (user_uid)
|
||||
basePath = path.join(config_api.getConfigItem('ytdl_users_base_path'), user_uid, 'subscriptions');
|
||||
@@ -371,10 +373,13 @@ async function generateArgsForSubscription(sub, user_uid, redownload = false, de
|
||||
|
||||
// skip videos that are in the archive. otherwise sub download can be permanently slow (vs. just the first time)
|
||||
const archive_text = await archive_api.generateArchive(sub.type, sub.user_uid, sub.id);
|
||||
logger.verbose(`Generating temporary archive file for subscription ${sub.name} with ${archive_text.split('\n').length - 1} entries.`)
|
||||
const archive_path = path.join(appendedBasePath, 'archive.txt');
|
||||
await fs.writeFile(archive_path, archive_text);
|
||||
downloadConfig.push('--download-archive', archive_path);
|
||||
const archive_count = archive_text.split('\n').length - 1;
|
||||
if (archive_count > 0) {
|
||||
logger.verbose(`Generating temporary archive file for subscription ${sub.name} with ${archive_count} entries.`)
|
||||
const archive_path = path.join(appendedBasePath, 'archive.txt');
|
||||
await fs.writeFile(archive_path, archive_text);
|
||||
downloadConfig.push('--download-archive', archive_path);
|
||||
}
|
||||
|
||||
if (sub.custom_args) {
|
||||
const customArgsArray = sub.custom_args.split(',,');
|
||||
@@ -440,35 +445,36 @@ async function getFilesToDownload(sub, output_jsons) {
|
||||
}
|
||||
|
||||
|
||||
async function getSubscriptions(user_uid = null) {
|
||||
exports.getSubscriptions = async (user_uid = null) => {
|
||||
return await db_api.getRecords('subscriptions', {user_uid: user_uid});
|
||||
}
|
||||
|
||||
async function getAllSubscriptions() {
|
||||
exports.getAllSubscriptions = async () => {
|
||||
const all_subs = await db_api.getRecords('subscriptions');
|
||||
const multiUserMode = config_api.getConfigItem('ytdl_multi_user_mode');
|
||||
return all_subs.filter(sub => !!(sub.user_uid) === !!multiUserMode);
|
||||
}
|
||||
|
||||
async function getSubscription(subID) {
|
||||
exports.getSubscription = async (subID) => {
|
||||
// stringify and parse because we may override the 'downloading' property
|
||||
const sub = JSON.parse(JSON.stringify(await db_api.getRecord('subscriptions', {id: subID})));
|
||||
// now with the download_queue, we may need to override 'downloading'
|
||||
const current_downloads = await db_api.getRecords('download_queue', {running: true, sub_id: sub.id}, true);
|
||||
const current_downloads = await db_api.getRecords('download_queue', {running: true, sub_id: subID}, true);
|
||||
if (!sub['downloading']) sub['downloading'] = current_downloads > 0;
|
||||
return sub;
|
||||
}
|
||||
|
||||
async function getSubscriptionByName(subName, user_uid = null) {
|
||||
exports.getSubscriptionByName = async (subName, user_uid = null) => {
|
||||
return await db_api.getRecord('subscriptions', {name: subName, user_uid: user_uid});
|
||||
}
|
||||
|
||||
async function updateSubscription(sub) {
|
||||
exports.updateSubscription = async (sub) => {
|
||||
await db_api.updateRecord('subscriptions', {id: sub.id}, sub);
|
||||
exports.writeSubscriptionMetadata(sub);
|
||||
return true;
|
||||
}
|
||||
|
||||
async function updateSubscriptionPropertyMultiple(subs, assignment_obj) {
|
||||
exports.updateSubscriptionPropertyMultiple = async (subs, assignment_obj) => {
|
||||
subs.forEach(async sub => {
|
||||
await updateSubscriptionProperty(sub, assignment_obj);
|
||||
});
|
||||
@@ -480,6 +486,14 @@ async function updateSubscriptionProperty(sub, assignment_obj) {
|
||||
return true;
|
||||
}
|
||||
|
||||
exports.writeSubscriptionMetadata = (sub) => {
|
||||
let basePath = sub.user_uid ? path.join(config_api.getConfigItem('ytdl_users_base_path'), sub.user_uid, 'subscriptions')
|
||||
: config_api.getConfigItem('ytdl_subscriptions_base_path');
|
||||
const appendedBasePath = getAppendedBasePath(sub, basePath);
|
||||
const metadata_path = path.join(appendedBasePath, CONSTS.SUBSCRIPTION_BACKUP_PATH);
|
||||
fs.writeJSONSync(metadata_path, sub);
|
||||
}
|
||||
|
||||
async function setFreshUploads(sub) {
|
||||
const sub_files = await db_api.getRecords('files', {sub_id: sub.id});
|
||||
if (!sub_files) return;
|
||||
@@ -534,17 +548,3 @@ async function checkVideoIfBetterExists(file_obj, sub, user_uid) {
|
||||
function getAppendedBasePath(sub, base_path) {
|
||||
return path.join(base_path, (sub.isPlaylist ? 'playlists/' : 'channels/'), sub.name);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getSubscription : getSubscription,
|
||||
getSubscriptionByName : getSubscriptionByName,
|
||||
getSubscriptions : getSubscriptions,
|
||||
getAllSubscriptions : getAllSubscriptions,
|
||||
updateSubscription : updateSubscription,
|
||||
subscribe : subscribe,
|
||||
unsubscribe : unsubscribe,
|
||||
deleteSubscriptionFile : deleteSubscriptionFile,
|
||||
getVideosForSub : getVideosForSub,
|
||||
updateSubscriptionPropertyMultiple : updateSubscriptionPropertyMultiple,
|
||||
generateOptionsForSubscriptionDownload: generateOptionsForSubscriptionDownload
|
||||
}
|
||||
|
||||
@@ -3,10 +3,17 @@ const notifications_api = require('./notifications');
|
||||
const youtubedl_api = require('./youtube-dl');
|
||||
const archive_api = require('./archive');
|
||||
const files_api = require('./files');
|
||||
const subscriptions_api = require('./subscriptions');
|
||||
const config_api = require('./config');
|
||||
const auth_api = require('./authentication/auth');
|
||||
const utils = require('./utils');
|
||||
const logger = require('./logger');
|
||||
const CONSTS = require('./consts');
|
||||
|
||||
const fs = require('fs-extra');
|
||||
const logger = require('./logger');
|
||||
const path = require('path');
|
||||
const scheduler = require('node-schedule');
|
||||
const { uuid } = require('uuidv4');
|
||||
|
||||
const TASKS = {
|
||||
backup_local_db: {
|
||||
@@ -47,6 +54,11 @@ const TASKS = {
|
||||
run: archive_api.importArchives,
|
||||
title: 'Import legacy archives',
|
||||
job: null
|
||||
},
|
||||
rebuild_database: {
|
||||
run: rebuildDB,
|
||||
title: 'Rebuild database',
|
||||
job: null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -265,4 +277,68 @@ async function autoDeleteFiles(data) {
|
||||
}
|
||||
}
|
||||
|
||||
async function rebuildDB() {
|
||||
await db_api.backupDB();
|
||||
let subs_to_add = await guessSubscriptions(false);
|
||||
subs_to_add = subs_to_add.concat(await guessSubscriptions(true));
|
||||
const users_to_add = await guessUsers();
|
||||
for (const user_to_add of users_to_add) {
|
||||
const usersFileFolder = config_api.getConfigItem('ytdl_users_base_path');
|
||||
|
||||
const user_exists = await db_api.getRecord('users', {uid: user_to_add});
|
||||
if (!user_exists) {
|
||||
await auth_api.registerUser(user_to_add, user_to_add, 'password');
|
||||
logger.info(`Regenerated user ${user_to_add}`);
|
||||
}
|
||||
|
||||
const user_channel_subs = await guessSubscriptions(false, path.join(usersFileFolder, user_to_add), user_to_add);
|
||||
const user_playlist_subs = await guessSubscriptions(true, path.join(usersFileFolder, user_to_add), user_to_add);
|
||||
subs_to_add = subs_to_add.concat(user_channel_subs, user_playlist_subs);
|
||||
}
|
||||
|
||||
for (const sub_to_add of subs_to_add) {
|
||||
const sub_exists = !!(await subscriptions_api.getSubscriptionByName(sub_to_add['name'], sub_to_add['user_uid']));
|
||||
// TODO: we shouldn't be creating this here
|
||||
const new_sub = Object.assign({}, sub_to_add, {paused: true});
|
||||
if (!sub_exists) {
|
||||
await subscriptions_api.subscribe(new_sub, sub_to_add['user_uid'], true);
|
||||
logger.info(`Regenerated subscription ${sub_to_add['name']}`);
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(`Importing unregistered files`);
|
||||
await files_api.importUnregisteredFiles();
|
||||
}
|
||||
|
||||
const guessUsers = async () => {
|
||||
const usersFileFolder = config_api.getConfigItem('ytdl_users_base_path');
|
||||
const userPaths = await utils.getDirectoriesInDirectory(usersFileFolder);
|
||||
return userPaths.map(userPath => path.basename(userPath));
|
||||
}
|
||||
|
||||
const guessSubscriptions = async (isPlaylist, basePath = null) => {
|
||||
const guessed_subs = [];
|
||||
const subscriptionsFileFolder = config_api.getConfigItem('ytdl_subscriptions_base_path');
|
||||
|
||||
const subsSubPath = basePath ? path.join(basePath, 'subscriptions') : subscriptionsFileFolder;
|
||||
const subsPath = path.join(subsSubPath, isPlaylist ? 'playlists' : 'channels');
|
||||
|
||||
const subs = await utils.getDirectoriesInDirectory(subsPath);
|
||||
for (const subPath of subs) {
|
||||
const sub_backup_path = path.join(subPath, CONSTS.SUBSCRIPTION_BACKUP_PATH);
|
||||
if (!fs.existsSync(sub_backup_path)) continue;
|
||||
|
||||
try {
|
||||
const sub_backup = fs.readJSONSync(sub_backup_path)
|
||||
delete sub_backup['_id'];
|
||||
guessed_subs.push(sub_backup);
|
||||
} catch(err) {
|
||||
logger.warn(`Failed to reimport subscription in path ${subPath}`)
|
||||
logger.warn(err);
|
||||
}
|
||||
}
|
||||
|
||||
return guessed_subs;
|
||||
}
|
||||
|
||||
exports.TASKS = TASKS;
|
||||
@@ -337,16 +337,22 @@ describe('Database', async function() {
|
||||
});
|
||||
|
||||
describe('Multi User', async function() {
|
||||
let user = null;
|
||||
const user_to_test = 'admin';
|
||||
const sub_to_test = 'dc834388-3454-41bf-a618-e11cb8c7de1c';
|
||||
const playlist_to_test = 'ysabVZz4x';
|
||||
const user_to_test = 'test_user';
|
||||
const user_password = 'test_pass';
|
||||
const sub_to_test = '';
|
||||
const playlist_to_test = '';
|
||||
beforeEach(async function() {
|
||||
await db_api.connectToDB();
|
||||
user = await auth_api.login('admin', 'pass');
|
||||
await auth_api.deleteUser(user_to_test);
|
||||
});
|
||||
describe('Authentication', function() {
|
||||
it('login', async function() {
|
||||
describe('Basic', function() {
|
||||
it('Register', async function() {
|
||||
const user = await auth_api.registerUser(user_to_test, user_to_test, user_password);
|
||||
assert(user);
|
||||
});
|
||||
it('Login', async function() {
|
||||
await auth_api.registerUser(user_to_test, user_to_test, user_password);
|
||||
const user = await auth_api.login(user_to_test, user_password);
|
||||
assert(user);
|
||||
});
|
||||
});
|
||||
@@ -362,14 +368,14 @@ describe('Multi User', async function() {
|
||||
});
|
||||
|
||||
it('Video access - disallowed', async function() {
|
||||
await db_api.setVideoProperty(video_to_test, {sharingEnabled: false}, user_to_test);
|
||||
const video_obj = auth_api.getUserVideo('admin', video_to_test, true);
|
||||
await db_api.setVideoProperty(video_to_test, {sharingEnabled: false});
|
||||
const video_obj = auth_api.getUserVideo(user_to_test, video_to_test, true);
|
||||
assert(!video_obj);
|
||||
});
|
||||
|
||||
it('Video access - allowed', async function() {
|
||||
await db_api.setVideoProperty(video_to_test, {sharingEnabled: true}, user_to_test);
|
||||
const video_obj = auth_api.getUserVideo('admin', video_to_test, true);
|
||||
const video_obj = auth_api.getUserVideo(user_to_test, video_to_test, true);
|
||||
assert(video_obj);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,6 +6,7 @@ const fs = require('fs-extra')
|
||||
const path = require('path');
|
||||
const { promisify } = require('util');
|
||||
const child_process = require('child_process');
|
||||
const commandExistsSync = require('command-exists').sync;
|
||||
|
||||
async function getCommentsForVOD(vodId) {
|
||||
const exec = promisify(child_process.exec);
|
||||
@@ -20,12 +21,12 @@ async function getCommentsForVOD(vodId) {
|
||||
const cliExt = is_windows ? '.exe' : ''
|
||||
const cliPath = `TwitchDownloaderCLI${cliExt}`
|
||||
|
||||
if (!fs.existsSync(cliPath)) {
|
||||
if (!commandExistsSync(cliPath)) {
|
||||
logger.error(`${cliPath} does not exist. Twitch chat download failed! Get it here: https://github.com/lay295/TwitchDownloader`);
|
||||
return null;
|
||||
}
|
||||
|
||||
const result = await exec(`TwitchDownloaderCLI chatdownload -u ${vodId} -o appdata/${vodId}.json`, {stdio:[0,1,2]});
|
||||
const result = await exec(`${cliPath} chatdownload -u ${vodId} -o appdata/${vodId}.json`, {stdio:[0,1,2]});
|
||||
|
||||
if (result['stderr']) {
|
||||
logger.error(`Failed to download twitch comments for ${vodId}`);
|
||||
|
||||
@@ -519,6 +519,17 @@ exports.convertFlatObjectToNestedObject = (obj) => {
|
||||
return result;
|
||||
}
|
||||
|
||||
exports.getDirectoriesInDirectory = async (basePath) => {
|
||||
try {
|
||||
const files = await fs.readdir(basePath, { withFileTypes: true });
|
||||
return files
|
||||
.filter((file) => file.isDirectory())
|
||||
.map((file) => path.join(basePath, file.name));
|
||||
} catch (err) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// objects
|
||||
|
||||
function File(id, title, thumbnailURL, isAudio, duration, url, uploader, size, path, upload_date, description, view_count, height, abr) {
|
||||
|
||||
@@ -21,4 +21,4 @@ version: 0.1.0
|
||||
# incremented each time you make changes to the application. Versions are not expected to
|
||||
# follow Semantic Versioning. They should reflect the version the application is using.
|
||||
# It is recommended to use it with quotes.
|
||||
appVersion: "4.3.1"
|
||||
appVersion: "4.3.2"
|
||||
|
||||
@@ -18,6 +18,7 @@ services:
|
||||
- "8998:17442"
|
||||
image: tzahi12345/youtubedl-material:latest
|
||||
ytdl-mongo-db:
|
||||
# If you are using a Raspberry Pi, use mongo:4.4.18
|
||||
image: mongo:4
|
||||
logging:
|
||||
driver: "none"
|
||||
|
||||
@@ -3,13 +3,26 @@ import requests
|
||||
import shutil
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from collections import OrderedDict
|
||||
|
||||
from github import Github
|
||||
|
||||
machine = platform.machine()
|
||||
|
||||
def isARM():
|
||||
return True if machine.startswith('arm') else False
|
||||
# https://stackoverflow.com/questions/45125516/possible-values-for-uname-m
|
||||
MACHINES_TO_ZIP = OrderedDict([
|
||||
("x86_64", "Linux-x64"),
|
||||
("aarch64", "LinuxArm64"),
|
||||
("armv8", "LinuxArm64"),
|
||||
("arm", "LinuxArm"),
|
||||
("AMD64", "Windows-x64")
|
||||
])
|
||||
|
||||
def getZipName():
|
||||
for possibleMachine, possibleZipName in MACHINES_TO_ZIP.items():
|
||||
if possibleMachine in machine:
|
||||
return possibleZipName
|
||||
|
||||
def getLatestFileInRepo(repo, search_string):
|
||||
# Create an unauthenticated instance of the Github object
|
||||
@@ -46,8 +59,11 @@ def getLatestFileInRepo(repo, search_string):
|
||||
print(f'No release found with {search_string}')
|
||||
|
||||
def getLatestCLIRelease():
|
||||
isArm = isARM()
|
||||
searchString = r'.*CLI.*' + "LinuxArm.zip" if isArm else "Linux-x64.zip"
|
||||
zipName = getZipName()
|
||||
if not zipName:
|
||||
print(f"GetTwitchDownloader.py could not get valid path for '{machine}'. Exiting...")
|
||||
sys.exit(1)
|
||||
searchString = r'.*CLI.*' + zipName
|
||||
getLatestFileInRepo("lay295/TwitchDownloader", searchString)
|
||||
|
||||
getLatestCLIRelease()
|
||||
|
||||
39
docker-utils/fetch-twitchdownloader.sh
Normal file
39
docker-utils/fetch-twitchdownloader.sh
Normal file
@@ -0,0 +1,39 @@
|
||||
#!/bin/sh
|
||||
|
||||
# THANK YOU TALULAH (https://github.com/nottalulah) for your help in figuring this out
|
||||
# and also optimizing some code with this commit.
|
||||
# xoxo :D
|
||||
|
||||
case $(uname -m) in
|
||||
x86_64)
|
||||
ARCH=Linux-x64;;
|
||||
aarch64)
|
||||
ARCH=LinuxArm64;;
|
||||
armhf)
|
||||
ARCH=LinuxArm;;
|
||||
armv7)
|
||||
ARCH=LinuxArm;;
|
||||
armv7l)
|
||||
ARCH=LinuxArm;;
|
||||
*)
|
||||
echo "Unsupported architecture: $(uname -m)"
|
||||
exit 1
|
||||
esac
|
||||
|
||||
echo "(INFO) Architecture detected: $ARCH"
|
||||
echo "(1/5) READY - Install unzip"
|
||||
apt-get update && apt-get -y install unzip curl jq libicu70
|
||||
VERSION=$(curl --silent "https://api.github.com/repos/lay295/TwitchDownloader/releases" | jq -r --arg arch "$ARCH" '[.[] | select(.assets | length > 0) | select(.assets[].name | contains("CLI") and contains($arch))] | max_by(.published_at) | .tag_name')
|
||||
echo "(2/5) DOWNLOAD - Acquire twitchdownloader"
|
||||
curl -o twitchdownloader.zip \
|
||||
--connect-timeout 5 \
|
||||
--max-time 120 \
|
||||
--retry 5 \
|
||||
--retry-delay 0 \
|
||||
--retry-max-time 40 \
|
||||
-L "https://github.com/lay295/TwitchDownloader/releases/download/$VERSION/TwitchDownloaderCLI-$VERSION-$ARCH.zip"
|
||||
unzip twitchdownloader.zip
|
||||
chmod +x TwitchDownloaderCLI
|
||||
echo "(3/5) Smoke test"
|
||||
./TwitchDownloaderCLI --help
|
||||
cp ./TwitchDownloaderCLI /usr/local/bin/TwitchDownloaderCLI
|
||||
@@ -30,7 +30,7 @@ curl -o ffmpeg.txz \
|
||||
--retry 5 \
|
||||
--retry-delay 0 \
|
||||
--retry-max-time 40 \
|
||||
"https://johnvansickle.com/ffmpeg/builds/ffmpeg-git-${ARCH}-static.tar.xz"
|
||||
"https://johnvansickle.com/ffmpeg/old-releases/ffmpeg-5.1.1-${ARCH}-static.tar.xz"
|
||||
mkdir /tmp/ffmpeg
|
||||
tar xf ffmpeg.txz -C /tmp/ffmpeg
|
||||
echo "(3/5) CLEANUP - Remove temp dependencies from ffmpeg obtain layer"
|
||||
|
||||
821
package-lock.json
generated
821
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "youtube-dl-material",
|
||||
"version": "4.3.1",
|
||||
"version": "4.3.2",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"ng": "ng",
|
||||
@@ -34,18 +34,17 @@
|
||||
"@angular/platform-browser-dynamic": "^15.0.1",
|
||||
"@angular/router": "^15.0.1",
|
||||
"@fontsource/material-icons": "^4.5.4",
|
||||
"@ngneat/content-loader": "^5.0.0",
|
||||
"@ngneat/content-loader": "^7.0.0",
|
||||
"@videogular/ngx-videogular": "^6.0.0",
|
||||
"core-js": "^2.4.1",
|
||||
"crypto-js": "^4.1.1",
|
||||
"file-saver": "^2.0.2",
|
||||
"filesize": "^10.0.7",
|
||||
"fingerprintjs2": "^2.1.0",
|
||||
"fs-extra": "^10.0.0",
|
||||
"material-icons": "^1.10.8",
|
||||
"nan": "^2.14.1",
|
||||
"ngx-avatars": "^1.4.1",
|
||||
"ngx-file-drop": "^13.0.0",
|
||||
"ngx-file-drop": "^15.0.0",
|
||||
"rxjs": "^6.6.3",
|
||||
"rxjs-compat": "^6.6.7",
|
||||
"tslib": "^2.0.0",
|
||||
@@ -66,7 +65,6 @@
|
||||
"@typescript-eslint/parser": "^4.29.0",
|
||||
"ajv": "^7.2.4",
|
||||
"codelyzer": "^6.0.0",
|
||||
"electron": "^19.1.9",
|
||||
"eslint": "^7.32.0",
|
||||
"jasmine-core": "~3.6.0",
|
||||
"jasmine-spec-reporter": "~5.0.0",
|
||||
|
||||
@@ -104,6 +104,7 @@ 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 { TaskType } from './models/TaskType';
|
||||
export type { TestConnectionStringRequest } from './models/TestConnectionStringRequest';
|
||||
export type { TestConnectionStringResponse } from './models/TestConnectionStringResponse';
|
||||
export type { TransferDBRequest } from './models/TransferDBRequest';
|
||||
|
||||
@@ -5,4 +5,4 @@
|
||||
export type AddFileToPlaylistRequest = {
|
||||
file_uid: string;
|
||||
playlist_id: string;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -13,4 +13,4 @@ export type Archive = {
|
||||
sub_id?: string;
|
||||
timestamp: number;
|
||||
uid: string;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -8,4 +8,4 @@ import type { YesNo } from './YesNo';
|
||||
export type BaseChangePermissionsRequest = {
|
||||
permission: UserPermission;
|
||||
new_value: YesNo;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -12,4 +12,4 @@ export type Category = {
|
||||
* Overrides file output for downloaded files in category
|
||||
*/
|
||||
custom_output?: string;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -22,4 +22,4 @@ export namespace CategoryRule {
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,4 +6,4 @@ import type { BaseChangePermissionsRequest } from './BaseChangePermissionsReques
|
||||
|
||||
export type ChangeRolePermissionsRequest = (BaseChangePermissionsRequest & {
|
||||
role: string;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,4 +6,4 @@ import type { BaseChangePermissionsRequest } from './BaseChangePermissionsReques
|
||||
|
||||
export type ChangeUserPermissionsRequest = (BaseChangePermissionsRequest & {
|
||||
user_uid: string;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,4 +7,4 @@ export type CheckConcurrentStreamRequest = {
|
||||
* UID of the concurrent stream
|
||||
*/
|
||||
uid: string;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -6,4 +6,4 @@ import type { ConcurrentStream } from './ConcurrentStream';
|
||||
|
||||
export type CheckConcurrentStreamResponse = {
|
||||
stream: ConcurrentStream;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -6,4 +6,4 @@ export type ClearDownloadsRequest = {
|
||||
clear_finished?: boolean;
|
||||
clear_paused?: boolean;
|
||||
clear_errors?: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -6,4 +6,4 @@ export type ConcurrentStream = {
|
||||
playback_timestamp?: number;
|
||||
unix_timestamp?: number;
|
||||
playing?: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -4,4 +4,4 @@
|
||||
|
||||
export type Config = {
|
||||
YoutubeDLMaterial: any;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -7,4 +7,4 @@ import type { Config } from './Config';
|
||||
export type ConfigResponse = {
|
||||
config_file: Config;
|
||||
success: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -4,4 +4,4 @@
|
||||
|
||||
export type CreateCategoryRequest = {
|
||||
name: string;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -7,4 +7,4 @@ import type { Category } from './Category';
|
||||
export type CreateCategoryResponse = {
|
||||
new_category?: Category;
|
||||
success?: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -6,4 +6,4 @@ export type CreatePlaylistRequest = {
|
||||
playlistName: string;
|
||||
uids: Array<string>;
|
||||
thumbnailURL: string;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -7,4 +7,4 @@ import type { Playlist } from './Playlist';
|
||||
export type CreatePlaylistResponse = {
|
||||
new_playlist: Playlist;
|
||||
success: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -5,4 +5,4 @@
|
||||
export type CropFileSettings = {
|
||||
cropFileStart: number;
|
||||
cropFileEnd: number;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -17,4 +17,4 @@ export namespace DBBackup {
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,4 +16,4 @@ roles?: TableInfo;
|
||||
download_queue?: TableInfo;
|
||||
archives?: TableInfo;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
@@ -42,4 +42,4 @@ export type DatabaseFile = {
|
||||
*/
|
||||
abr?: number;
|
||||
favorite: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -11,4 +11,4 @@ export type DeleteAllFilesResponse = {
|
||||
* Number of files removed
|
||||
*/
|
||||
delete_count?: number;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -6,4 +6,4 @@ import type { Archive } from './Archive';
|
||||
|
||||
export type DeleteArchiveItemsRequest = {
|
||||
archives: Array<Archive>;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -4,4 +4,4 @@
|
||||
|
||||
export type DeleteCategoryRequest = {
|
||||
category_uid: string;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -5,4 +5,4 @@
|
||||
export type DeleteMp3Mp4Request = {
|
||||
uid: string;
|
||||
blacklistMode?: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -4,4 +4,4 @@
|
||||
|
||||
export type DeleteNotificationRequest = {
|
||||
uid: string;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -4,4 +4,4 @@
|
||||
|
||||
export type DeletePlaylistRequest = {
|
||||
playlist_id: string;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -8,4 +8,4 @@ export type DeleteSubscriptionFileRequest = {
|
||||
* If true, does not remove id from archive. Only valid if youtube-dl archive is enabled in settings.
|
||||
*/
|
||||
deleteForever?: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -4,4 +4,4 @@
|
||||
|
||||
export type DeleteUserRequest = {
|
||||
uid: string;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -27,4 +27,4 @@ export type Download = {
|
||||
sub_id?: string;
|
||||
sub_name?: string;
|
||||
prefetched_info?: any;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -7,4 +7,4 @@ import type { FileType } from './FileType';
|
||||
export type DownloadArchiveRequest = {
|
||||
type?: FileType;
|
||||
sub_id?: string;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -11,4 +11,4 @@ export type DownloadFileRequest = {
|
||||
playlist_id?: string;
|
||||
url?: string;
|
||||
type?: FileType;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -49,4 +49,4 @@ export type DownloadRequest = {
|
||||
* If using youtube-dl archive, download will ignore it
|
||||
*/
|
||||
ignoreArchive?: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -6,4 +6,4 @@ import type { Download } from './Download';
|
||||
|
||||
export type DownloadResponse = {
|
||||
download?: Download;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -20,4 +20,4 @@ export type DownloadTwitchChatByVODIDRequest = {
|
||||
*/
|
||||
uuid?: string;
|
||||
sub?: Subscription;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -6,4 +6,4 @@ import type { TwitchChatMessage } from './TwitchChatMessage';
|
||||
|
||||
export type DownloadTwitchChatByVODIDResponse = {
|
||||
chat: Array<TwitchChatMessage>;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -4,4 +4,4 @@
|
||||
|
||||
export type DownloadVideosForSubscriptionRequest = {
|
||||
subID: string;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -5,4 +5,4 @@
|
||||
export enum FileType {
|
||||
AUDIO = 'audio',
|
||||
VIDEO = 'video',
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,4 +6,4 @@ export enum FileTypeFilter {
|
||||
AUDIO_ONLY = 'audio_only',
|
||||
VIDEO_ONLY = 'video_only',
|
||||
BOTH = 'both',
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,4 +4,4 @@
|
||||
|
||||
export type GenerateArgsResponse = {
|
||||
args?: Array<string>;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -4,4 +4,4 @@
|
||||
|
||||
export type GenerateNewApiKeyResponse = {
|
||||
new_api_key: string;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -6,4 +6,4 @@ import type { Category } from './Category';
|
||||
|
||||
export type GetAllCategoriesResponse = {
|
||||
categories: Array<Category>;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -7,4 +7,4 @@ export type GetAllDownloadsRequest = {
|
||||
* Filters downloads with the array
|
||||
*/
|
||||
uids?: Array<string> | null;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -6,4 +6,4 @@ import type { Download } from './Download';
|
||||
|
||||
export type GetAllDownloadsResponse = {
|
||||
downloads?: Array<Download>;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -21,4 +21,4 @@ export type GetAllFilesRequest = {
|
||||
* Include if you want to filter by subscription
|
||||
*/
|
||||
sub_id?: string;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -11,4 +11,4 @@ export type GetAllFilesResponse = {
|
||||
* All video playlists
|
||||
*/
|
||||
playlists: Array<Playlist>;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -6,4 +6,4 @@ import type { Subscription } from './Subscription';
|
||||
|
||||
export type GetAllSubscriptionsResponse = {
|
||||
subscriptions: Array<Subscription>;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -6,4 +6,4 @@ import type { Task } from './Task';
|
||||
|
||||
export type GetAllTasksResponse = {
|
||||
tasks?: Array<Task>;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -7,4 +7,4 @@ import type { FileType } from './FileType';
|
||||
export type GetArchivesRequest = {
|
||||
type?: FileType;
|
||||
sub_id?: string;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -6,4 +6,4 @@ import type { Archive } from './Archive';
|
||||
|
||||
export type GetArchivesResponse = {
|
||||
archives: Array<Archive>;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -6,4 +6,4 @@ import type { DBBackup } from './DBBackup';
|
||||
|
||||
export type GetDBBackupsResponse = {
|
||||
tasks?: Array<DBBackup>;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -4,4 +4,4 @@
|
||||
|
||||
export type GetDownloadRequest = {
|
||||
download_uid: string;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -6,4 +6,4 @@ import type { Download } from './Download';
|
||||
|
||||
export type GetDownloadResponse = {
|
||||
download?: Download;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -4,4 +4,4 @@
|
||||
|
||||
export type GetFileFormatsRequest = {
|
||||
url?: string;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -7,4 +7,4 @@ export type GetFileFormatsResponse = {
|
||||
result: {
|
||||
formats?: Array<any>;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
@@ -14,4 +14,4 @@ export type GetFileRequest = {
|
||||
* User UID
|
||||
*/
|
||||
uuid?: string;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -7,4 +7,4 @@ import type { DatabaseFile } from './DatabaseFile';
|
||||
export type GetFileResponse = {
|
||||
success: boolean;
|
||||
file?: DatabaseFile;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -16,4 +16,4 @@ export type GetFullTwitchChatRequest = {
|
||||
*/
|
||||
uuid?: string;
|
||||
sub?: Subscription;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -5,4 +5,4 @@
|
||||
export type GetFullTwitchChatResponse = {
|
||||
success: boolean;
|
||||
error?: string;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -4,4 +4,4 @@
|
||||
|
||||
export type GetLogsRequest = {
|
||||
lines?: number;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -8,4 +8,4 @@ export type GetLogsResponse = {
|
||||
*/
|
||||
logs?: string;
|
||||
success?: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -11,4 +11,4 @@ export type GetMp3sResponse = {
|
||||
* All audio playlists
|
||||
*/
|
||||
playlists: Array<Playlist>;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -11,4 +11,4 @@ export type GetMp4sResponse = {
|
||||
* All video playlists
|
||||
*/
|
||||
playlists: Array<Playlist>;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -6,4 +6,4 @@ import type { Notification } from './Notification';
|
||||
|
||||
export type GetNotificationsResponse = {
|
||||
notifications?: Array<Notification>;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -9,4 +9,4 @@ export type GetPlaylistRequest = {
|
||||
type?: FileType;
|
||||
uuid?: string;
|
||||
include_file_metadata?: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -12,4 +12,4 @@ export type GetPlaylistResponse = {
|
||||
* File objects for every uid in the playlist's uids property, in the same order
|
||||
*/
|
||||
file_objs?: Array<DatabaseFile>;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -4,4 +4,4 @@
|
||||
|
||||
export type GetPlaylistsRequest = {
|
||||
include_categories?: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -6,4 +6,4 @@ import type { Playlist } from './Playlist';
|
||||
|
||||
export type GetPlaylistsResponse = {
|
||||
playlists: Array<Playlist>;
|
||||
};
|
||||
};
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user