Compare commits

...

47 Commits

Author SHA1 Message Date
Isaac Abadi
c9c0247480 Tasks actions styling update 2023-05-21 00:12:58 -06:00
Isaac Abadi
2fcf5364d8 Rebuild database task now asks for confirmation before rebuilding
Fixed api types build errors
2023-05-21 00:11:48 -06:00
Isaac Abadi
a38fb0e2e0 Updated api models 2023-05-20 23:49:27 -06:00
Isaac Abadi
e6050969ec Merge branch 'master' of https://github.com/Tzahi12345/YoutubeDL-Material into rebuild-database-task 2023-05-20 23:44:49 -06:00
Isaac Abadi
958300c281 Added typing to task key 2023-05-20 23:44:14 -06:00
Tzahi12345
84c2b2769b Merge pull request #890 from martadinata666/js-generate
docker : reduce build time and size
2023-05-21 00:35:28 -04:00
Isaac Abadi
e145c9c992 Updated fetch-twitchdownloader.sh to get the latest release for the specific arch 2023-05-20 19:32:00 -06:00
Isaac Abadi
078408236c Code cleanup
Default user password after rebuild is now 'password'
2023-05-20 18:27:08 -06:00
Dedy Martadinata S
2adbc0a02c Update fetch-twitchdownloader.sh 2023-05-20 11:35:12 +07:00
Dedy Martadinata S
fe95f04c18 Update Dockerfile 2023-05-20 11:30:24 +07:00
Dedy Martadinata S
9b3816afce Update Dockerfile 2023-05-20 11:25:16 +07:00
Dedy Martadinata S
07874d9241 Revert 142d708ee3
It become fail to set anything
2023-05-20 11:13:09 +07:00
Isaac Abadi
03122b4c81 Subscription metadata is now backed up
Rebuild database now used subscription metadata backup
2023-05-18 23:32:12 -04:00
Isaac Abadi
3deb1e8459 updated subscriptions.js export syntax 2023-05-18 22:23:48 -04:00
Dedy Martadinata S
9fa1aab1e5 Update Dockerfile, Update CI PR, use scripts to download twitchdownloader 2023-05-12 14:46:27 +07:00
Glassed Silver
80b41af620 Merge pull request #910 from Tzahi12345/twitch-chat-fix
Twitch chat downloader fix for Docker
2023-05-12 07:01:12 +02:00
Tzahi12345
ab5d8dc5ca Merge pull request #911 from Tzahi12345/registration-fixes
Registration fixes
2023-05-12 00:03:05 -04:00
Tzahi12345
4b55c39f39 permissions code simplified 2023-05-11 23:14:40 -04:00
Tzahi12345
3ca296f195 Fixed compilation issue 2023-05-11 23:14:25 -04:00
Tzahi12345
d4fa640f0f Added tasks_manager to possible user/role permission in openapi 2023-05-11 02:47:06 -04:00
Tzahi12345
427eecf214 Fixed issue where after resetting the DB, admin would have to be registered twice 2023-05-11 02:44:05 -04:00
Tzahi12345
4f54e408a5 Removed erroneous error after registering admin 2023-05-11 02:43:37 -04:00
Tzahi12345
9e481bbd5f Fixed issue where twitch chat downloader could not be found in docker 2023-05-11 02:27:35 -04:00
Glassed Silver
78b29a76b8 Merge pull request #864 from nardis556/master
Update entrypoint.sh
2023-05-11 03:19:50 +02:00
Glassed Silver
0342d18f76 Merge pull request #906 from weblate/weblate-youtubedl-material-ytdl-material
Translations update from Hosted Weblate
2023-05-11 03:17:40 +02:00
Glassed Silver
70754c580c Merge pull request #908 from Tzahi12345/ffmpeg-force-v5
Force ffmpeg 5.1.1
2023-05-11 03:16:08 +02:00
Tzahi12345
e58b0b8638 Force ffmpeg 5.1.1 2023-05-10 19:04:48 -04:00
YMisterXY
df8f8070ca Translated using Weblate (Polish)
Currently translated at 80.5% (385 of 478 strings)

Translation: YoutubeDL-Material/ytdl-material
Translate-URL: https://hosted.weblate.org/projects/youtubedl-material/ytdl-material/pl/
2023-05-10 13:49:19 +02:00
gallegonovato
0b8ca31594 Translated using Weblate (Spanish)
Currently translated at 100.0% (478 of 478 strings)

Translation: YoutubeDL-Material/ytdl-material
Translate-URL: https://hosted.weblate.org/projects/youtubedl-material/ytdl-material/es/
2023-05-10 13:49:19 +02:00
Tzahi12345
658a76dc1c Added missing admin tasks_manager role 2023-05-08 19:23:07 -04:00
Glassed Silver
f363ec5db6 Merge pull request #901 from weblate/weblate-youtubedl-material-ytdl-material
Translations update from Hosted Weblate
2023-05-08 22:12:01 +02:00
gallegonovato
f36d675abf Translated using Weblate (Spanish)
Currently translated at 100.0% (480 of 480 strings)

Translation: YoutubeDL-Material/ytdl-material
Translate-URL: https://hosted.weblate.org/projects/youtubedl-material/ytdl-material/es/
2023-05-08 17:52:23 +02:00
Tzahi12345
35fcf44e1a Fixed rebuild db log messages
Rebuild db now imports unregistered files
2023-05-08 02:08:01 -04:00
Tzahi12345
bad6080730 Adds task to rebuild database 2023-05-08 02:00:43 -04:00
Tzahi12345
2a7b62272e Merge remote-tracking branch 'origin/testing-improvements' 2023-05-07 23:36:35 -04:00
Tzahi12345
be74377a08 Merge pull request #899 from Tzahi12345/remove-armv7
Remove armv7 support in Docker
2023-05-07 02:13:55 -04:00
Isaac Abadi
808c7e2112 Temporarily remove armv7 support
Revert "Added python3.8-dev/build-essential to dockerfile"

This reverts commit d90434c240.

Revert "Adds token to GH actions for GetTwitchDownloader"

This reverts commit a4ca1abb7c.
2023-05-07 01:23:58 -04:00
Isaac Abadi
d6f39d37b5 Added PR multiarch
Added python3.8-dev/build-essential to dockerfile

Adds token to GH actions for GetTwitchDownloader
2023-05-07 01:23:44 -04:00
Isaac Abadi
0c46b044da Improved tests for multi-user mode 2023-05-06 23:29:20 -04:00
Tzahi12345
e573f34cea Merge pull request #893 from Tzahi12345/readme-update
README update
2023-05-05 00:05:03 -04:00
Tzahi12345
52e32d4f0f Changed tcd to Twitch Downloader 2023-05-04 23:44:12 -04:00
Tzahi12345
adb5f2256e Translations source file update 2023-05-04 22:33:48 -04:00
Tzahi12345
59bf6ff86d Merge pull request #888 from Tzahi12345/dependency-updates
Dependency updates
2023-05-04 22:28:50 -04:00
Tzahi12345
68fbde8907 Merge pull request #800 from Bastians-Bits/master
Development Documentation
2023-05-03 19:32:07 -04:00
Tzahi12345
62bccb3349 Updated DEVELOPMENT.md to reflect dev config file 2023-05-03 17:01:17 -04:00
nardis556
26988bd607 Update entrypoint.sh 2023-04-16 15:03:12 -05:00
bastiansbits
575f7eed4e Added a new read me (DEVELOPMENT.md) as starting point for new develope
Added a new VSC launch configuration to start the backend in the debugger
Update the build instruction in README.md (Issue #728)
2022-12-07 14:43:43 +01:00
158 changed files with 1609 additions and 470 deletions

View File

@@ -23,5 +23,16 @@ jobs:
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": "nightly", "commit": "${{ steps.vars.outputs.sha_short }}", "date": "${{ steps.date.outputs.date }}"}'
dir: 'backend/' dir: 'backend/'
- name: Build docker images - name: setup platform emulator
run: docker build . -t tzahi12345/youtubedl-material:nightly-pr 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

View File

@@ -80,7 +80,7 @@ jobs:
with: with:
context: . context: .
file: ./Dockerfile file: ./Dockerfile
platforms: linux/amd64,linux/arm,linux/arm64/v8 platforms: linux/amd64,linux/arm64/v8
push: true push: true
tags: ${{ steps.docker-meta.outputs.tags }} tags: ${{ steps.docker-meta.outputs.tags }}
labels: ${{ steps.docker-meta.outputs.labels }} labels: ${{ steps.docker-meta.outputs.labels }}

View File

@@ -80,7 +80,7 @@ jobs:
with: with:
context: . context: .
file: ./Dockerfile file: ./Dockerfile
platforms: linux/amd64,linux/arm,linux/arm64/v8 platforms: linux/amd64,linux/arm64/v8
push: true push: true
tags: ${{ steps.docker-meta.outputs.tags }} tags: ${{ steps.docker-meta.outputs.tags }}
labels: ${{ steps.docker-meta.outputs.labels }} labels: ${{ steps.docker-meta.outputs.labels }}

14
.vscode/launch.json vendored
View File

@@ -4,6 +4,20 @@
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0", "version": "0.2.0",
"configurations": [ "configurations": [
{
"name": "Dev: Debug Backend",
"request": "launch",
"runtimeArgs": [
"run-script",
"debug"
],
"runtimeExecutable": "npm",
"skipFiles": [
"<node_internals>/**"
],
"type": "node",
"cwd": "${workspaceFolder}/backend"
},
{ {
"type": "node", "type": "node",
"request": "attach", "request": "attach",

38
DEVELOPMENT.md Normal file
View 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.

View File

@@ -1,15 +1,17 @@
# Fetching our ffmpeg # Fetching our utils
FROM ubuntu:22.04 AS ffmpeg FROM ubuntu:22.04 AS utils
ENV DEBIAN_FRONTEND=noninteractive ENV DEBIAN_FRONTEND=noninteractive
# Use script due local build compability # Use script due local build compability
COPY docker-utils/ffmpeg-fetch.sh . COPY docker-utils/*.sh .
RUN chmod +x ffmpeg-fetch.sh RUN chmod +x *.sh
RUN sh ./ffmpeg-fetch.sh RUN sh ./ffmpeg-fetch.sh
RUN sh ./fetch-twitchdownloader.sh
# Create our Ubuntu 22.04 with node 16.14.2 (that specific version is required as per: https://stackoverflow.com/a/72855258/8088021) # Create our Ubuntu 22.04 with node 16.14.2 (that specific version is required as per: https://stackoverflow.com/a/72855258/8088021)
# Go to 20.04 # Go to 20.04
FROM ubuntu:20.04 AS base FROM ubuntu:22.04 AS base
ARG TARGETPLATFORM
ARG DEBIAN_FRONTEND=noninteractive ARG DEBIAN_FRONTEND=noninteractive
ENV UID=1000 ENV UID=1000
ENV GID=1000 ENV GID=1000
@@ -17,19 +19,30 @@ ENV USER=youtube
ENV NO_UPDATE_NOTIFIER=true ENV NO_UPDATE_NOTIFIER=true
ENV PM2_HOME=/app/pm2 ENV PM2_HOME=/app/pm2
ENV ALLOW_CONFIG_MUTATIONS=true ENV ALLOW_CONFIG_MUTATIONS=true
# 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 && \ RUN groupadd -g $GID $USER && useradd --system -m -g $USER --uid $UID $USER && \
apt update && \ apt update && \
apt install -y --no-install-recommends curl ca-certificates tzdata && \ apt install -y --no-install-recommends curl ca-certificates tzdata libicu70 && \
curl -fsSL https://deb.nodesource.com/setup_16.x | bash - && \
apt install -y --no-install-recommends nodejs && \
npm -g install npm n && \
n 16.14.2 && \
apt clean && \ apt clean && \
rm -rf /var/lib/apt/lists/* rm -rf /var/lib/apt/lists/*
RUN 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 # Build frontend
FROM base as frontend ARG BUILDPLATFORM
FROM --platform=${BUILDPLATFORM} node:16 as frontend
RUN npm install -g @angular/cli RUN npm install -g @angular/cli
WORKDIR /build WORKDIR /build
COPY [ "package.json", "package-lock.json", "angular.json", "tsconfig.json", "/build/" ] COPY [ "package.json", "package-lock.json", "angular.json", "tsconfig.json", "/build/" ]
@@ -49,32 +62,35 @@ RUN npm config set strict-ssl false && \
npm install --prod && \ npm install --prod && \
ls -al ls -al
FROM base as python #FROM base as python
WORKDIR /app # armv7 need build from source
COPY docker-utils/GetTwitchDownloader.py . #WORKDIR /app
RUN apt update && \ #COPY docker-utils/GetTwitchDownloader.py .
apt install -y --no-install-recommends python3-minimal python-is-python3 python3-pip && \ #RUN apt update && \
apt clean && \ # apt install -y --no-install-recommends python3-minimal python-is-python3 python3-pip python3-dev build-essential libffi-dev && \
rm -rf /var/lib/apt/lists/* # apt clean && \
RUN pip install PyGithub requests # rm -rf /var/lib/apt/lists/*
RUN python GetTwitchDownloader.py #RUN pip install PyGithub requests
#RUN python GetTwitchDownloader.py
# Final image # Final image
FROM base FROM base
RUN npm install -g pm2 && \ RUN npm install -g pm2 && \
apt update && \ apt update && \
apt install -y --no-install-recommends gosu python3-minimal python-is-python3 python3-pip atomicparsley build-essential && \ apt install -y --no-install-recommends gosu python3-minimal python-is-python3 python3-pip atomicparsley build-essential && \
pip install pycryptodomex && \
apt remove -y --purge build-essential && \
apt autoremove -y --purge && \
apt clean && \ apt clean && \
rm -rf /var/lib/apt/lists/* rm -rf /var/lib/apt/lists/*
RUN pip install pycryptodomex
WORKDIR /app WORKDIR /app
# User 1000 already exist from base image # User 1000 already exist from base image
COPY --chown=$UID:$GID --from=ffmpeg [ "/usr/local/bin/ffmpeg", "/usr/local/bin/ffmpeg" ] COPY --chown=$UID:$GID --from=utils [ "/usr/local/bin/ffmpeg", "/usr/local/bin/ffmpeg" ]
COPY --chown=$UID:$GID --from=ffmpeg [ "/usr/local/bin/ffprobe", "/usr/local/bin/ffprobe" ] COPY --chown=$UID:$GID --from=utils [ "/usr/local/bin/ffprobe", "/usr/local/bin/ffprobe" ]
COPY --chown=$UID:$GID --from=utils [ "/usr/local/bin/TwitchDownloaderCLI", "/usr/local/bin/TwitchDownloaderCLI"]
COPY --chown=$UID:$GID --from=backend ["/app/","/app/"] COPY --chown=$UID:$GID --from=backend ["/app/","/app/"]
COPY --chown=$UID:$GID --from=frontend [ "/build/backend/public/", "/app/public/" ] COPY --chown=$UID:$GID --from=frontend [ "/build/backend/public/", "/app/public/" ]
COPY --chown=$UID:$GID --from=python ["/app/TwitchDownloaderCLI","/usr/local/bin/TwitchDownloaderCLI"] #COPY --chown=$UID:$GID --from=python ["/app/TwitchDownloaderCLI","/usr/local/bin/TwitchDownloaderCLI"]
RUN chown $UID:$GID .
RUN chmod +x /app/fix-scripts/*.sh RUN chmod +x /app/fix-scripts/*.sh
# Add some persistence data # Add some persistence data
#VOLUME ["/app/appdata"] #VOLUME ["/app/appdata"]

View File

@@ -1758,14 +1758,14 @@ components:
type: object type: object
properties: properties:
task_key: task_key:
type: string $ref: '#/components/schemas/TaskType'
required: required:
- task_key - task_key
UpdateTaskScheduleRequest: UpdateTaskScheduleRequest:
type: object type: object
properties: properties:
task_key: task_key:
type: string $ref: '#/components/schemas/TaskType'
new_schedule: new_schedule:
$ref: '#/components/schemas/Schedule' $ref: '#/components/schemas/Schedule'
required: required:
@@ -1775,7 +1775,7 @@ components:
type: object type: object
properties: properties:
task_key: task_key:
type: string $ref: '#/components/schemas/TaskType'
new_data: new_data:
type: object type: object
required: required:
@@ -1785,7 +1785,7 @@ components:
type: object type: object
properties: properties:
task_key: task_key:
type: string $ref: '#/components/schemas/TaskType'
new_options: new_options:
type: object type: object
required: required:
@@ -2726,7 +2726,7 @@ components:
type: object type: object
properties: properties:
key: key:
type: string $ref: '#/components/schemas/TaskType'
title: title:
type: string type: string
last_ran: last_ran:
@@ -2742,9 +2742,20 @@ components:
error: error:
type: string type: string
schedule: schedule:
type: object $ref: '#/components/schemas/Schedule'
options: options:
type: object type: object
TaskType:
type: string
enum:
- backup_local_db
- missing_files_check
- missing_db_records
- duplicate_files_check
- youtubedl_update_check
- delete_old_files
- import_legacy_archives
- rebuild_database
Schedule: Schedule:
required: required:
- type - type
@@ -2877,6 +2888,7 @@ components:
- sharing - sharing
- advanced_download - advanced_download
- downloads_manager - downloads_manager
- tasks_manager
YesNo: YesNo:
type: string type: string
enum: enum:

View File

@@ -36,8 +36,7 @@ Required dependencies:
Optional dependencies: Optional dependencies:
* AtomicParsley (for embedding thumbnails, package name `atomicparsley`) * AtomicParsley (for embedding thumbnails, package name `atomicparsley`)
* [tcd](https://github.com/PetterKraabol/Twitch-Chat-Downloader) (for downloading Twitch VOD chats) * [Twitch Downloader CLI](https://github.com/lay295/TwitchDownloader) (for downloading Twitch VOD chats)
<details> <details>
<summary>Debian/Ubuntu</summary> <summary>Debian/Ubuntu</summary>
@@ -86,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. 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`. The frontend is now complete. The backend is much easier. Just go into the `backend` folder, and type `npm start`.

View File

@@ -535,6 +535,7 @@ async function loadConfig() {
if (allowSubscriptions) { if (allowSubscriptions) {
// set downloading to false // set downloading to false
let subscriptions = await subscriptions_api.getAllSubscriptions(); let subscriptions = await subscriptions_api.getAllSubscriptions();
subscriptions.forEach(async sub => subscriptions_api.writeSubscriptionMetadata(sub));
subscriptions_api.updateSubscriptionPropertyMultiple(subscriptions, {downloading: false}); subscriptions_api.updateSubscriptionPropertyMultiple(subscriptions, {downloading: false});
// runs initially, then runs every ${subscriptionCheckInterval} seconds // runs initially, then runs every ${subscriptionCheckInterval} seconds
const watchSubscriptionsInterval = function() { const watchSubscriptionsInterval = function() {
@@ -1928,9 +1929,34 @@ app.post('/api/clearAllLogs', optionalJwt, async function(req, res) {
// user authentication // user authentication
app.post('/api/auth/register' app.post('/api/auth/register', optionalJwt, async (req, res) => {
, optionalJwt const userid = req.body.userid;
, auth_api.registerUser); const username = req.body.username;
const plaintextPassword = req.body.password;
if (userid !== 'admin' && !config_api.getConfigItem('ytdl_allow_registration') && !req.isAuthenticated() && (!req.user || !exports.userHasPermission(req.user.uid, 'settings'))) {
logger.error(`Registration failed for user ${userid}. Registration is disabled.`);
res.sendStatus(409);
return;
}
if (plaintextPassword === "") {
logger.error(`Registration failed for user ${userid}. A password must be provided.`);
res.sendStatus(409);
return;
}
const new_user = await auth_api.registerUser(userid, username, plaintextPassword);
if (!new_user) {
res.sendStatus(409);
return;
}
res.send({
user: new_user
});
});
app.post('/api/auth/login' app.post('/api/auth/login'
, auth_api.passport.authenticate(['local', 'ldapauth'], {}) , auth_api.passport.authenticate(['local', 'ldapauth'], {})
, auth_api.generateJWT , auth_api.generateJWT
@@ -1982,18 +2008,7 @@ app.post('/api/updateUser', optionalJwt, async (req, res) => {
app.post('/api/deleteUser', optionalJwt, async (req, res) => { app.post('/api/deleteUser', optionalJwt, async (req, res) => {
let uid = req.body.uid; let uid = req.body.uid;
try { try {
let success = false; const success = await auth_api.deleteUser(uid);
let usersFileFolder = config_api.getConfigItem('ytdl_users_base_path');
const user_folder = path.join(__dirname, usersFileFolder, uid);
const user_db_obj = await db_api.getRecord('users', {uid: uid});
if (user_db_obj) {
// user exists, let's delete
await fs.remove(user_folder);
await db_api.removeRecord('users', {uid: uid});
success = true;
} else {
logger.error(`Could not find user with uid ${uid}`);
}
res.send({success: success}); res.send({success: success});
} catch (err) { } catch (err) {
logger.error(err); logger.error(err);

View File

@@ -1,11 +1,13 @@
const config_api = require('../config'); const config_api = require('../config');
const consts = require('../consts'); const CONSTS = require('../consts');
const logger = require('../logger'); const logger = require('../logger');
const db_api = require('../db'); const db_api = require('../db');
const jwt = require('jsonwebtoken'); const jwt = require('jsonwebtoken');
const { uuid } = require('uuidv4'); const { uuid } = require('uuidv4');
const bcrypt = require('bcryptjs'); const bcrypt = require('bcryptjs');
const fs = require('fs-extra');
const path = require('path');
var LocalStrategy = require('passport-local').Strategy; var LocalStrategy = require('passport-local').Strategy;
var LdapStrategy = require('passport-ldapauth'); var LdapStrategy = require('passport-ldapauth');
@@ -16,7 +18,7 @@ var JwtStrategy = require('passport-jwt').Strategy,
let SERVER_SECRET = null; let SERVER_SECRET = null;
let JWT_EXPIRATION = null; let JWT_EXPIRATION = null;
let opts = null; let opts = null;
let saltRounds = null; let saltRounds = 10;
exports.initialize = function () { exports.initialize = function () {
/************************* /*************************
@@ -31,8 +33,6 @@ exports.initialize = function () {
}); });
} }
saltRounds = 10;
// Sometimes this value is not properly typed: https://github.com/Tzahi12345/YoutubeDL-Material/issues/813 // Sometimes this value is not properly typed: https://github.com/Tzahi12345/YoutubeDL-Material/issues/813
JWT_EXPIRATION = config_api.getConfigItem('ytdl_jwt_expiration'); JWT_EXPIRATION = config_api.getConfigItem('ytdl_jwt_expiration');
if (!(+JWT_EXPIRATION)) { if (!(+JWT_EXPIRATION)) {
@@ -68,14 +68,7 @@ exports.initialize = function () {
const setupRoles = async () => { const setupRoles = async () => {
const required_roles = { const required_roles = {
admin: { admin: {
permissions: [ permissions: CONSTS.AVAILABLE_PERMISSIONS
'filemanager',
'settings',
'subscriptions',
'sharing',
'advanced_download',
'downloads_manager'
]
}, },
user: { user: {
permissions: [ permissions: [
@@ -113,55 +106,41 @@ exports.passport.deserializeUser(function(user, done) {
/*************************************** /***************************************
* Register user with hashed password * Register user with hashed password
**************************************/ **************************************/
exports.registerUser = async function(req, res) {
var userid = req.body.userid;
var username = req.body.username;
var plaintextPassword = req.body.password;
if (userid !== 'admin' && !config_api.getConfigItem('ytdl_allow_registration') && !req.isAuthenticated() && (!req.user || !exports.userHasPermission(req.user.uid, 'settings'))) { exports.registerUser = async (userid, username, plaintextPassword) => {
res.sendStatus(409); const hash = await bcrypt.hash(plaintextPassword, saltRounds);
logger.error(`Registration failed for user ${userid}. Registration is disabled.`); const new_user = generateUserObject(userid, username, hash);
return; // check if user exists
if (await db_api.getRecord('users', {uid: userid})) {
// user id is taken!
logger.error('Registration failed: UID is already taken!');
return null;
} else if (await db_api.getRecord('users', {name: username})) {
// user name is taken!
logger.error('Registration failed: User name is already taken!');
return null;
} else {
// add to db
await db_api.insertRecordIntoTable('users', new_user);
logger.verbose(`New user created: ${new_user.name}`);
return new_user;
} }
}
if (plaintextPassword === "") { exports.deleteUser = async (uid) => {
res.sendStatus(400); let success = false;
logger.error(`Registration failed for user ${userid}. A password must be provided.`); let usersFileFolder = config_api.getConfigItem('ytdl_users_base_path');
return; const user_folder = path.join(__dirname, usersFileFolder, uid);
const user_db_obj = await db_api.getRecord('users', {uid: uid});
if (user_db_obj) {
// user exists, let's delete
await fs.remove(user_folder);
await db_api.removeRecord('users', {uid: uid});
success = true;
} else {
logger.error(`Could not find user with uid ${uid}`);
} }
return success;
bcrypt.hash(plaintextPassword, saltRounds)
.then(async function(hash) {
let new_user = generateUserObject(userid, username, hash);
// check if user exists
if (await db_api.getRecord('users', {uid: userid})) {
// user id is taken!
logger.error('Registration failed: UID is already taken!');
res.status(409).send('UID is already taken!');
} else if (await db_api.getRecord('users', {name: username})) {
// user name is taken!
logger.error('Registration failed: User name is already taken!');
res.status(409).send('User name is already taken!');
} else {
// add to db
await db_api.insertRecordIntoTable('users', new_user);
logger.verbose(`New user created: ${new_user.name}`);
res.send({
user: new_user
});
}
})
.then(function(result) {
})
.catch(function(err) {
logger.error(err);
if( err.code == 'ER_DUP_ENTRY' ) {
res.status(409).send('UserId already taken');
} else {
res.sendStatus(409);
}
});
} }
/*************************************** /***************************************
@@ -242,7 +221,7 @@ exports.returnAuthResponse = async function(req, res) {
user: req.user, user: req.user,
token: req.token, token: req.token,
permissions: await exports.userPermissions(req.user.uid), permissions: await exports.userPermissions(req.user.uid),
available_permissions: consts['AVAILABLE_PERMISSIONS'] available_permissions: CONSTS.AVAILABLE_PERMISSIONS
}); });
} }
@@ -326,7 +305,7 @@ exports.getUserVideos = async function(user_uid, type) {
} }
exports.getUserVideo = async function(user_uid, file_uid, requireSharing = false) { exports.getUserVideo = async function(user_uid, file_uid, requireSharing = false) {
let file = await db_api.getRecord('files', {file_uid: file_uid}); let file = await db_api.getRecord('files', {uid: file_uid});
// prevent unauthorized users from accessing the file info // prevent unauthorized users from accessing the file info
if (file && !file['sharingEnabled'] && requireSharing) file = null; if (file && !file['sharingEnabled'] && requireSharing) file = null;
@@ -413,8 +392,8 @@ exports.userPermissions = async function(user_uid) {
const role_obj = await db_api.getRecord('roles', {key: role}); const role_obj = await db_api.getRecord('roles', {key: role});
const role_permissions = role_obj['permissions']; const role_permissions = role_obj['permissions'];
for (let i = 0; i < consts['AVAILABLE_PERMISSIONS'].length; i++) { for (let i = 0; i < CONSTS.AVAILABLE_PERMISSIONS.length; i++) {
let permission = consts['AVAILABLE_PERMISSIONS'][i]; let permission = CONSTS.AVAILABLE_PERMISSIONS[i];
const user_has_explicit_permission = user_obj['permissions'].includes(permission); const user_has_explicit_permission = user_obj['permissions'].includes(permission);
const permission_in_overrides = user_obj['permission_overrides'].includes(permission); const permission_in_overrides = user_obj['permission_overrides'].includes(permission);

View File

@@ -347,6 +347,8 @@ const YTDL_ARGS_WITH_VALUES = [
'--convert-subs' '--convert-subs'
]; ];
exports.SUBSCRIPTION_BACKUP_PATH = 'subscription_backup.json'
// we're using a Set here for performance // we're using a Set here for performance
exports.YTDL_ARGS_WITH_VALUES = new Set(YTDL_ARGS_WITH_VALUES); exports.YTDL_ARGS_WITH_VALUES = new Set(YTDL_ARGS_WITH_VALUES);

View File

@@ -1,7 +1,7 @@
#!/bin/sh #!/bin/sh
set -eu set -eu
CMD="npm start" CMD="npm start && pm2 start"
# 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
@@ -10,7 +10,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" '{}' + || 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 gosu "$UID:$GID" "$0" "$@" exec gosu "$UID:$GID" "$0" "$@"
fi fi

View File

@@ -769,6 +769,11 @@
"delayed-stream": "~1.0.0" "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": { "compress-commons": {
"version": "4.1.1", "version": "4.1.1",
"resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-4.1.1.tgz", "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-4.1.1.tgz",

View File

@@ -30,6 +30,7 @@
"async-mutex": "^0.4.0", "async-mutex": "^0.4.0",
"axios": "^0.21.2", "axios": "^0.21.2",
"bcryptjs": "^2.4.0", "bcryptjs": "^2.4.0",
"command-exists": "^1.2.9",
"compression": "^1.7.4", "compression": "^1.7.4",
"config": "^3.2.3", "config": "^3.2.3",
"express": "^4.18.2", "express": "^4.18.2",

View File

@@ -6,20 +6,21 @@ const config_api = require('./config');
const archive_api = require('./archive'); const archive_api = require('./archive');
const utils = require('./utils'); const utils = require('./utils');
const logger = require('./logger'); const logger = require('./logger');
const CONSTS = require('./consts');
const debugMode = process.env.YTDL_MODE === 'debug'; const debugMode = process.env.YTDL_MODE === 'debug';
const db_api = require('./db'); const db_api = require('./db');
const downloader_api = require('./downloader'); const downloader_api = require('./downloader');
async function subscribe(sub, user_uid = null) { exports.subscribe = async (sub, user_uid = null, skip_get_info = false) => {
const result_obj = { const result_obj = {
success: false, success: false,
error: '' error: ''
}; };
return new Promise(async resolve => { return new Promise(async resolve => {
// sub should just have url and name. here we will get isPlaylist and path // sub should just have url and name. here we will get isPlaylist and path
sub.isPlaylist = sub.url.includes('playlist'); sub.isPlaylist = sub.isPlaylist || sub.url.includes('playlist');
sub.videos = []; sub.videos = [];
let url_exists = !!(await db_api.getRecord('subscriptions', {url: sub.url, user_uid: user_uid})); let url_exists = !!(await db_api.getRecord('subscriptions', {url: sub.url, user_uid: user_uid}));
@@ -34,10 +35,11 @@ async function subscribe(sub, user_uid = null) {
sub['user_uid'] = user_uid ? user_uid : undefined; sub['user_uid'] = user_uid ? user_uid : undefined;
await db_api.insertRecordIntoTable('subscriptions', sub); await db_api.insertRecordIntoTable('subscriptions', sub);
let success = await getSubscriptionInfo(sub); let success = skip_get_info ? true : await getSubscriptionInfo(sub);
exports.writeSubscriptionMetadata(sub);
if (success) { if (success) {
getVideosForSub(sub, user_uid); if (!sub.paused) exports.getVideosForSub(sub, user_uid);
} else { } else {
logger.error('Subscribe: Failed to get subscription info. Subscribe failed.') 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; let basePath = null;
if (user_uid) if (user_uid)
basePath = path.join(config_api.getConfigItem('ytdl_users_base_path'), user_uid, 'subscriptions'); basePath = path.join(config_api.getConfigItem('ytdl_users_base_path'), user_uid, 'subscriptions');
@@ -148,7 +150,7 @@ async function unsubscribe(sub, deleteMode, user_uid = null) {
await db_api.removeAllRecords('archives', {sub_id: sub.id}); await db_api.removeAllRecords('archives', {sub_id: sub.id});
} }
async function deleteSubscriptionFile(sub, file, deleteForever, file_uid = null, user_uid = null) { exports.deleteSubscriptionFile = async (sub, file, deleteForever, file_uid = null, user_uid = null) => {
if (typeof sub === 'string') { if (typeof sub === 'string') {
// TODO: fix bad workaround where sub is a sub_id // TODO: fix bad workaround where sub is a sub_id
sub = await db_api.getRecord('subscriptions', {sub_id: sub}); sub = await db_api.getRecord('subscriptions', {sub_id: sub});
@@ -216,8 +218,8 @@ async function deleteSubscriptionFile(sub, file, deleteForever, file_uid = null,
} }
} }
async function getVideosForSub(sub, user_uid = null) { exports.getVideosForSub = async (sub, user_uid = null) => {
const latest_sub_obj = await getSubscription(sub.id); const latest_sub_obj = await exports.getSubscription(sub.id);
if (!latest_sub_obj || latest_sub_obj['downloading']) { if (!latest_sub_obj || latest_sub_obj['downloading']) {
return false; return false;
} }
@@ -305,7 +307,7 @@ async function handleOutputJSON(output, sub, user_uid) {
} }
const files_to_download = await getFilesToDownload(sub, output_jsons); const files_to_download = await getFilesToDownload(sub, output_jsons);
const base_download_options = generateOptionsForSubscriptionDownload(sub, user_uid); const base_download_options = exports.generateOptionsForSubscriptionDownload(sub, user_uid);
for (let j = 0; j < files_to_download.length; j++) { for (let j = 0; j < files_to_download.length; j++) {
const file_to_download = files_to_download[j]; const file_to_download = files_to_download[j];
@@ -316,7 +318,7 @@ async function handleOutputJSON(output, sub, user_uid) {
return files_to_download; return files_to_download;
} }
function generateOptionsForSubscriptionDownload(sub, user_uid) { exports.generateOptionsForSubscriptionDownload = (sub, user_uid) => {
let basePath = null; let basePath = null;
if (user_uid) if (user_uid)
basePath = path.join(config_api.getConfigItem('ytdl_users_base_path'), user_uid, 'subscriptions'); basePath = path.join(config_api.getConfigItem('ytdl_users_base_path'), user_uid, 'subscriptions');
@@ -440,35 +442,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}); return await db_api.getRecords('subscriptions', {user_uid: user_uid});
} }
async function getAllSubscriptions() { exports.getAllSubscriptions = async () => {
const all_subs = await db_api.getRecords('subscriptions'); const all_subs = await db_api.getRecords('subscriptions');
const multiUserMode = config_api.getConfigItem('ytdl_multi_user_mode'); const multiUserMode = config_api.getConfigItem('ytdl_multi_user_mode');
return all_subs.filter(sub => !!(sub.user_uid) === !!multiUserMode); return all_subs.filter(sub => !!(sub.user_uid) === !!multiUserMode);
} }
async function getSubscription(subID) { exports.getSubscription = async (subID) => {
// stringify and parse because we may override the 'downloading' property // stringify and parse because we may override the 'downloading' property
const sub = JSON.parse(JSON.stringify(await db_api.getRecord('subscriptions', {id: subID}))); const sub = JSON.parse(JSON.stringify(await db_api.getRecord('subscriptions', {id: subID})));
// now with the download_queue, we may need to override 'downloading' // now with the download_queue, we may need to override 'downloading'
const current_downloads = await db_api.getRecords('download_queue', {running: true, sub_id: sub.id}, true); const current_downloads = await db_api.getRecords('download_queue', {running: true, sub_id: subID}, true);
if (!sub['downloading']) sub['downloading'] = current_downloads > 0; if (!sub['downloading']) sub['downloading'] = current_downloads > 0;
return sub; return sub;
} }
async function getSubscriptionByName(subName, user_uid = null) { exports.getSubscriptionByName = async (subName, user_uid = null) => {
return await db_api.getRecord('subscriptions', {name: subName, user_uid: user_uid}); return await db_api.getRecord('subscriptions', {name: subName, user_uid: user_uid});
} }
async function updateSubscription(sub) { exports.updateSubscription = async (sub) => {
await db_api.updateRecord('subscriptions', {id: sub.id}, sub); await db_api.updateRecord('subscriptions', {id: sub.id}, sub);
exports.writeSubscriptionMetadata(sub);
return true; return true;
} }
async function updateSubscriptionPropertyMultiple(subs, assignment_obj) { exports.updateSubscriptionPropertyMultiple = async (subs, assignment_obj) => {
subs.forEach(async sub => { subs.forEach(async sub => {
await updateSubscriptionProperty(sub, assignment_obj); await updateSubscriptionProperty(sub, assignment_obj);
}); });
@@ -480,6 +483,14 @@ async function updateSubscriptionProperty(sub, assignment_obj) {
return true; return true;
} }
exports.writeSubscriptionMetadata = (sub) => {
let basePath = sub.user_uid ? path.join(config_api.getConfigItem('ytdl_users_base_path'), sub.user_uid, 'subscriptions')
: config_api.getConfigItem('ytdl_subscriptions_base_path');
const appendedBasePath = getAppendedBasePath(sub, basePath);
const metadata_path = path.join(appendedBasePath, CONSTS.SUBSCRIPTION_BACKUP_PATH);
fs.writeJSONSync(metadata_path, sub);
}
async function setFreshUploads(sub) { async function setFreshUploads(sub) {
const sub_files = await db_api.getRecords('files', {sub_id: sub.id}); const sub_files = await db_api.getRecords('files', {sub_id: sub.id});
if (!sub_files) return; if (!sub_files) return;
@@ -534,17 +545,3 @@ async function checkVideoIfBetterExists(file_obj, sub, user_uid) {
function getAppendedBasePath(sub, base_path) { function getAppendedBasePath(sub, base_path) {
return path.join(base_path, (sub.isPlaylist ? 'playlists/' : 'channels/'), sub.name); return path.join(base_path, (sub.isPlaylist ? 'playlists/' : 'channels/'), sub.name);
} }
module.exports = {
getSubscription : getSubscription,
getSubscriptionByName : getSubscriptionByName,
getSubscriptions : getSubscriptions,
getAllSubscriptions : getAllSubscriptions,
updateSubscription : updateSubscription,
subscribe : subscribe,
unsubscribe : unsubscribe,
deleteSubscriptionFile : deleteSubscriptionFile,
getVideosForSub : getVideosForSub,
updateSubscriptionPropertyMultiple : updateSubscriptionPropertyMultiple,
generateOptionsForSubscriptionDownload: generateOptionsForSubscriptionDownload
}

View File

@@ -3,10 +3,17 @@ const notifications_api = require('./notifications');
const youtubedl_api = require('./youtube-dl'); const youtubedl_api = require('./youtube-dl');
const archive_api = require('./archive'); const archive_api = require('./archive');
const files_api = require('./files'); const files_api = require('./files');
const subscriptions_api = require('./subscriptions');
const config_api = require('./config');
const auth_api = require('./authentication/auth');
const utils = require('./utils');
const logger = require('./logger');
const CONSTS = require('./consts');
const fs = require('fs-extra'); const fs = require('fs-extra');
const logger = require('./logger'); const path = require('path');
const scheduler = require('node-schedule'); const scheduler = require('node-schedule');
const { uuid } = require('uuidv4');
const TASKS = { const TASKS = {
backup_local_db: { backup_local_db: {
@@ -47,6 +54,11 @@ const TASKS = {
run: archive_api.importArchives, run: archive_api.importArchives,
title: 'Import legacy archives', title: 'Import legacy archives',
job: null job: null
},
rebuild_database: {
run: rebuildDB,
title: 'Rebuild database',
job: null
} }
} }
@@ -265,4 +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; exports.TASKS = TASKS;

View File

@@ -337,16 +337,22 @@ describe('Database', async function() {
}); });
describe('Multi User', async function() { describe('Multi User', async function() {
let user = null; const user_to_test = 'test_user';
const user_to_test = 'admin'; const user_password = 'test_pass';
const sub_to_test = 'dc834388-3454-41bf-a618-e11cb8c7de1c'; const sub_to_test = '';
const playlist_to_test = 'ysabVZz4x'; const playlist_to_test = '';
beforeEach(async function() { beforeEach(async function() {
await db_api.connectToDB(); await db_api.connectToDB();
user = await auth_api.login('admin', 'pass'); await auth_api.deleteUser(user_to_test);
}); });
describe('Authentication', function() { describe('Basic', function() {
it('login', async function() { it('Register', async function() {
const user = await auth_api.registerUser(user_to_test, user_to_test, user_password);
assert(user);
});
it('Login', async function() {
await auth_api.registerUser(user_to_test, user_to_test, user_password);
const user = await auth_api.login(user_to_test, user_password);
assert(user); assert(user);
}); });
}); });
@@ -362,14 +368,14 @@ describe('Multi User', async function() {
}); });
it('Video access - disallowed', async function() { it('Video access - disallowed', async function() {
await db_api.setVideoProperty(video_to_test, {sharingEnabled: false}, user_to_test); await db_api.setVideoProperty(video_to_test, {sharingEnabled: false});
const video_obj = auth_api.getUserVideo('admin', video_to_test, true); const video_obj = auth_api.getUserVideo(user_to_test, video_to_test, true);
assert(!video_obj); assert(!video_obj);
}); });
it('Video access - allowed', async function() { it('Video access - allowed', async function() {
await db_api.setVideoProperty(video_to_test, {sharingEnabled: true}, user_to_test); await db_api.setVideoProperty(video_to_test, {sharingEnabled: true}, user_to_test);
const video_obj = auth_api.getUserVideo('admin', video_to_test, true); const video_obj = auth_api.getUserVideo(user_to_test, video_to_test, true);
assert(video_obj); assert(video_obj);
}); });
}); });

View File

@@ -6,6 +6,7 @@ const fs = require('fs-extra')
const path = require('path'); const path = require('path');
const { promisify } = require('util'); const { promisify } = require('util');
const child_process = require('child_process'); const child_process = require('child_process');
const commandExistsSync = require('command-exists').sync;
async function getCommentsForVOD(vodId) { async function getCommentsForVOD(vodId) {
const exec = promisify(child_process.exec); const exec = promisify(child_process.exec);
@@ -20,7 +21,7 @@ async function getCommentsForVOD(vodId) {
const cliExt = is_windows ? '.exe' : '' const cliExt = is_windows ? '.exe' : ''
const cliPath = `TwitchDownloaderCLI${cliExt}` const cliPath = `TwitchDownloaderCLI${cliExt}`
if (!fs.existsSync(cliPath)) { if (!commandExistsSync(cliPath)) {
logger.error(`${cliPath} does not exist. Twitch chat download failed! Get it here: https://github.com/lay295/TwitchDownloader`); logger.error(`${cliPath} does not exist. Twitch chat download failed! Get it here: https://github.com/lay295/TwitchDownloader`);
return null; return null;
} }

View File

@@ -519,6 +519,17 @@ exports.convertFlatObjectToNestedObject = (obj) => {
return result; return result;
} }
exports.getDirectoriesInDirectory = async (basePath) => {
try {
const files = await fs.readdir(basePath, { withFileTypes: true });
return files
.filter((file) => file.isDirectory())
.map((file) => path.join(basePath, file.name));
} catch (err) {
return [];
}
}
// 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) {

View 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

View File

@@ -30,7 +30,7 @@ curl -o ffmpeg.txz \
--retry 5 \ --retry 5 \
--retry-delay 0 \ --retry-delay 0 \
--retry-max-time 40 \ --retry-max-time 40 \
"https://johnvansickle.com/ffmpeg/builds/ffmpeg-git-${ARCH}-static.tar.xz" "https://johnvansickle.com/ffmpeg/old-releases/ffmpeg-5.1.1-${ARCH}-static.tar.xz"
mkdir /tmp/ffmpeg mkdir /tmp/ffmpeg
tar xf ffmpeg.txz -C /tmp/ffmpeg tar xf ffmpeg.txz -C /tmp/ffmpeg
echo "(3/5) CLEANUP - Remove temp dependencies from ffmpeg obtain layer" echo "(3/5) CLEANUP - Remove temp dependencies from ffmpeg obtain layer"

View File

@@ -104,6 +104,7 @@ export type { SubscriptionRequestData } from './models/SubscriptionRequestData';
export type { SuccessObject } from './models/SuccessObject'; export type { SuccessObject } from './models/SuccessObject';
export type { TableInfo } from './models/TableInfo'; export type { TableInfo } from './models/TableInfo';
export type { Task } from './models/Task'; export type { Task } from './models/Task';
export { TaskType } from './models/TaskType';
export type { TestConnectionStringRequest } from './models/TestConnectionStringRequest'; export type { TestConnectionStringRequest } from './models/TestConnectionStringRequest';
export type { TestConnectionStringResponse } from './models/TestConnectionStringResponse'; export type { TestConnectionStringResponse } from './models/TestConnectionStringResponse';
export type { TransferDBRequest } from './models/TransferDBRequest'; export type { TransferDBRequest } from './models/TransferDBRequest';

View File

@@ -5,4 +5,4 @@
export type AddFileToPlaylistRequest = { export type AddFileToPlaylistRequest = {
file_uid: string; file_uid: string;
playlist_id: string; playlist_id: string;
}; };

View File

@@ -13,4 +13,4 @@ export type Archive = {
sub_id?: string; sub_id?: string;
timestamp: number; timestamp: number;
uid: string; uid: string;
}; };

View File

@@ -8,4 +8,4 @@ import type { YesNo } from './YesNo';
export type BaseChangePermissionsRequest = { export type BaseChangePermissionsRequest = {
permission: UserPermission; permission: UserPermission;
new_value: YesNo; new_value: YesNo;
}; };

View File

@@ -12,4 +12,4 @@ export type Category = {
* Overrides file output for downloaded files in category * Overrides file output for downloaded files in category
*/ */
custom_output?: string; custom_output?: string;
}; };

View File

@@ -22,4 +22,4 @@ export namespace CategoryRule {
} }
} }

View File

@@ -6,4 +6,4 @@ import type { BaseChangePermissionsRequest } from './BaseChangePermissionsReques
export type ChangeRolePermissionsRequest = (BaseChangePermissionsRequest & { export type ChangeRolePermissionsRequest = (BaseChangePermissionsRequest & {
role: string; role: string;
}); });

View File

@@ -6,4 +6,4 @@ import type { BaseChangePermissionsRequest } from './BaseChangePermissionsReques
export type ChangeUserPermissionsRequest = (BaseChangePermissionsRequest & { export type ChangeUserPermissionsRequest = (BaseChangePermissionsRequest & {
user_uid: string; user_uid: string;
}); });

View File

@@ -7,4 +7,4 @@ export type CheckConcurrentStreamRequest = {
* UID of the concurrent stream * UID of the concurrent stream
*/ */
uid: string; uid: string;
}; };

View File

@@ -6,4 +6,4 @@ import type { ConcurrentStream } from './ConcurrentStream';
export type CheckConcurrentStreamResponse = { export type CheckConcurrentStreamResponse = {
stream: ConcurrentStream; stream: ConcurrentStream;
}; };

View File

@@ -6,4 +6,4 @@ export type ClearDownloadsRequest = {
clear_finished?: boolean; clear_finished?: boolean;
clear_paused?: boolean; clear_paused?: boolean;
clear_errors?: boolean; clear_errors?: boolean;
}; };

View File

@@ -6,4 +6,4 @@ export type ConcurrentStream = {
playback_timestamp?: number; playback_timestamp?: number;
unix_timestamp?: number; unix_timestamp?: number;
playing?: boolean; playing?: boolean;
}; };

View File

@@ -4,4 +4,4 @@
export type Config = { export type Config = {
YoutubeDLMaterial: any; YoutubeDLMaterial: any;
}; };

View File

@@ -7,4 +7,4 @@ import type { Config } from './Config';
export type ConfigResponse = { export type ConfigResponse = {
config_file: Config; config_file: Config;
success: boolean; success: boolean;
}; };

View File

@@ -4,4 +4,4 @@
export type CreateCategoryRequest = { export type CreateCategoryRequest = {
name: string; name: string;
}; };

View File

@@ -7,4 +7,4 @@ import type { Category } from './Category';
export type CreateCategoryResponse = { export type CreateCategoryResponse = {
new_category?: Category; new_category?: Category;
success?: boolean; success?: boolean;
}; };

View File

@@ -6,4 +6,4 @@ export type CreatePlaylistRequest = {
playlistName: string; playlistName: string;
uids: Array<string>; uids: Array<string>;
thumbnailURL: string; thumbnailURL: string;
}; };

View File

@@ -7,4 +7,4 @@ import type { Playlist } from './Playlist';
export type CreatePlaylistResponse = { export type CreatePlaylistResponse = {
new_playlist: Playlist; new_playlist: Playlist;
success: boolean; success: boolean;
}; };

View File

@@ -5,4 +5,4 @@
export type CropFileSettings = { export type CropFileSettings = {
cropFileStart: number; cropFileStart: number;
cropFileEnd: number; cropFileEnd: number;
}; };

View File

@@ -17,4 +17,4 @@ export namespace DBBackup {
} }
} }

View File

@@ -16,4 +16,4 @@ roles?: TableInfo;
download_queue?: TableInfo; download_queue?: TableInfo;
archives?: TableInfo; archives?: TableInfo;
}; };
}; };

View File

@@ -42,4 +42,4 @@ export type DatabaseFile = {
*/ */
abr?: number; abr?: number;
favorite: boolean; favorite: boolean;
}; };

View File

@@ -11,4 +11,4 @@ export type DeleteAllFilesResponse = {
* Number of files removed * Number of files removed
*/ */
delete_count?: number; delete_count?: number;
}; };

View File

@@ -6,4 +6,4 @@ import type { Archive } from './Archive';
export type DeleteArchiveItemsRequest = { export type DeleteArchiveItemsRequest = {
archives: Array<Archive>; archives: Array<Archive>;
}; };

View File

@@ -4,4 +4,4 @@
export type DeleteCategoryRequest = { export type DeleteCategoryRequest = {
category_uid: string; category_uid: string;
}; };

View File

@@ -5,4 +5,4 @@
export type DeleteMp3Mp4Request = { export type DeleteMp3Mp4Request = {
uid: string; uid: string;
blacklistMode?: boolean; blacklistMode?: boolean;
}; };

View File

@@ -4,4 +4,4 @@
export type DeleteNotificationRequest = { export type DeleteNotificationRequest = {
uid: string; uid: string;
}; };

View File

@@ -4,4 +4,4 @@
export type DeletePlaylistRequest = { export type DeletePlaylistRequest = {
playlist_id: string; playlist_id: string;
}; };

View File

@@ -8,4 +8,4 @@ export type DeleteSubscriptionFileRequest = {
* If true, does not remove id from archive. Only valid if youtube-dl archive is enabled in settings. * If true, does not remove id from archive. Only valid if youtube-dl archive is enabled in settings.
*/ */
deleteForever?: boolean; deleteForever?: boolean;
}; };

View File

@@ -4,4 +4,4 @@
export type DeleteUserRequest = { export type DeleteUserRequest = {
uid: string; uid: string;
}; };

View File

@@ -27,4 +27,4 @@ export type Download = {
sub_id?: string; sub_id?: string;
sub_name?: string; sub_name?: string;
prefetched_info?: any; prefetched_info?: any;
}; };

View File

@@ -7,4 +7,4 @@ import type { FileType } from './FileType';
export type DownloadArchiveRequest = { export type DownloadArchiveRequest = {
type?: FileType; type?: FileType;
sub_id?: string; sub_id?: string;
}; };

View File

@@ -11,4 +11,4 @@ export type DownloadFileRequest = {
playlist_id?: string; playlist_id?: string;
url?: string; url?: string;
type?: FileType; type?: FileType;
}; };

View File

@@ -49,4 +49,4 @@ export type DownloadRequest = {
* If using youtube-dl archive, download will ignore it * If using youtube-dl archive, download will ignore it
*/ */
ignoreArchive?: boolean; ignoreArchive?: boolean;
}; };

View File

@@ -6,4 +6,4 @@ import type { Download } from './Download';
export type DownloadResponse = { export type DownloadResponse = {
download?: Download; download?: Download;
}; };

View File

@@ -20,4 +20,4 @@ export type DownloadTwitchChatByVODIDRequest = {
*/ */
uuid?: string; uuid?: string;
sub?: Subscription; sub?: Subscription;
}; };

View File

@@ -6,4 +6,4 @@ import type { TwitchChatMessage } from './TwitchChatMessage';
export type DownloadTwitchChatByVODIDResponse = { export type DownloadTwitchChatByVODIDResponse = {
chat: Array<TwitchChatMessage>; chat: Array<TwitchChatMessage>;
}; };

View File

@@ -4,4 +4,4 @@
export type DownloadVideosForSubscriptionRequest = { export type DownloadVideosForSubscriptionRequest = {
subID: string; subID: string;
}; };

View File

@@ -5,4 +5,4 @@
export enum FileType { export enum FileType {
AUDIO = 'audio', AUDIO = 'audio',
VIDEO = 'video', VIDEO = 'video',
} }

View File

@@ -6,4 +6,4 @@ export enum FileTypeFilter {
AUDIO_ONLY = 'audio_only', AUDIO_ONLY = 'audio_only',
VIDEO_ONLY = 'video_only', VIDEO_ONLY = 'video_only',
BOTH = 'both', BOTH = 'both',
} }

View File

@@ -4,4 +4,4 @@
export type GenerateArgsResponse = { export type GenerateArgsResponse = {
args?: Array<string>; args?: Array<string>;
}; };

View File

@@ -4,4 +4,4 @@
export type GenerateNewApiKeyResponse = { export type GenerateNewApiKeyResponse = {
new_api_key: string; new_api_key: string;
}; };

View File

@@ -6,4 +6,4 @@ import type { Category } from './Category';
export type GetAllCategoriesResponse = { export type GetAllCategoriesResponse = {
categories: Array<Category>; categories: Array<Category>;
}; };

View File

@@ -7,4 +7,4 @@ export type GetAllDownloadsRequest = {
* Filters downloads with the array * Filters downloads with the array
*/ */
uids?: Array<string> | null; uids?: Array<string> | null;
}; };

View File

@@ -6,4 +6,4 @@ import type { Download } from './Download';
export type GetAllDownloadsResponse = { export type GetAllDownloadsResponse = {
downloads?: Array<Download>; downloads?: Array<Download>;
}; };

View File

@@ -21,4 +21,4 @@ export type GetAllFilesRequest = {
* Include if you want to filter by subscription * Include if you want to filter by subscription
*/ */
sub_id?: string; sub_id?: string;
}; };

View File

@@ -11,4 +11,4 @@ export type GetAllFilesResponse = {
* All video playlists * All video playlists
*/ */
playlists: Array<Playlist>; playlists: Array<Playlist>;
}; };

View File

@@ -6,4 +6,4 @@ import type { Subscription } from './Subscription';
export type GetAllSubscriptionsResponse = { export type GetAllSubscriptionsResponse = {
subscriptions: Array<Subscription>; subscriptions: Array<Subscription>;
}; };

View File

@@ -6,4 +6,4 @@ import type { Task } from './Task';
export type GetAllTasksResponse = { export type GetAllTasksResponse = {
tasks?: Array<Task>; tasks?: Array<Task>;
}; };

View File

@@ -7,4 +7,4 @@ import type { FileType } from './FileType';
export type GetArchivesRequest = { export type GetArchivesRequest = {
type?: FileType; type?: FileType;
sub_id?: string; sub_id?: string;
}; };

View File

@@ -6,4 +6,4 @@ import type { Archive } from './Archive';
export type GetArchivesResponse = { export type GetArchivesResponse = {
archives: Array<Archive>; archives: Array<Archive>;
}; };

View File

@@ -6,4 +6,4 @@ import type { DBBackup } from './DBBackup';
export type GetDBBackupsResponse = { export type GetDBBackupsResponse = {
tasks?: Array<DBBackup>; tasks?: Array<DBBackup>;
}; };

View File

@@ -4,4 +4,4 @@
export type GetDownloadRequest = { export type GetDownloadRequest = {
download_uid: string; download_uid: string;
}; };

View File

@@ -6,4 +6,4 @@ import type { Download } from './Download';
export type GetDownloadResponse = { export type GetDownloadResponse = {
download?: Download; download?: Download;
}; };

View File

@@ -4,4 +4,4 @@
export type GetFileFormatsRequest = { export type GetFileFormatsRequest = {
url?: string; url?: string;
}; };

View File

@@ -7,4 +7,4 @@ export type GetFileFormatsResponse = {
result: { result: {
formats?: Array<any>; formats?: Array<any>;
}; };
}; };

View File

@@ -14,4 +14,4 @@ export type GetFileRequest = {
* User UID * User UID
*/ */
uuid?: string; uuid?: string;
}; };

View File

@@ -7,4 +7,4 @@ import type { DatabaseFile } from './DatabaseFile';
export type GetFileResponse = { export type GetFileResponse = {
success: boolean; success: boolean;
file?: DatabaseFile; file?: DatabaseFile;
}; };

View File

@@ -16,4 +16,4 @@ export type GetFullTwitchChatRequest = {
*/ */
uuid?: string; uuid?: string;
sub?: Subscription; sub?: Subscription;
}; };

View File

@@ -5,4 +5,4 @@
export type GetFullTwitchChatResponse = { export type GetFullTwitchChatResponse = {
success: boolean; success: boolean;
error?: string; error?: string;
}; };

View File

@@ -4,4 +4,4 @@
export type GetLogsRequest = { export type GetLogsRequest = {
lines?: number; lines?: number;
}; };

View File

@@ -8,4 +8,4 @@ export type GetLogsResponse = {
*/ */
logs?: string; logs?: string;
success?: boolean; success?: boolean;
}; };

View File

@@ -11,4 +11,4 @@ export type GetMp3sResponse = {
* All audio playlists * All audio playlists
*/ */
playlists: Array<Playlist>; playlists: Array<Playlist>;
}; };

View File

@@ -11,4 +11,4 @@ export type GetMp4sResponse = {
* All video playlists * All video playlists
*/ */
playlists: Array<Playlist>; playlists: Array<Playlist>;
}; };

View File

@@ -6,4 +6,4 @@ import type { Notification } from './Notification';
export type GetNotificationsResponse = { export type GetNotificationsResponse = {
notifications?: Array<Notification>; notifications?: Array<Notification>;
}; };

View File

@@ -9,4 +9,4 @@ export type GetPlaylistRequest = {
type?: FileType; type?: FileType;
uuid?: string; uuid?: string;
include_file_metadata?: boolean; include_file_metadata?: boolean;
}; };

View File

@@ -12,4 +12,4 @@ export type GetPlaylistResponse = {
* File objects for every uid in the playlist's uids property, in the same order * File objects for every uid in the playlist's uids property, in the same order
*/ */
file_objs?: Array<DatabaseFile>; file_objs?: Array<DatabaseFile>;
}; };

View File

@@ -4,4 +4,4 @@
export type GetPlaylistsRequest = { export type GetPlaylistsRequest = {
include_categories?: boolean; include_categories?: boolean;
}; };

View File

@@ -6,4 +6,4 @@ import type { Playlist } from './Playlist';
export type GetPlaylistsResponse = { export type GetPlaylistsResponse = {
playlists: Array<Playlist>; playlists: Array<Playlist>;
}; };

View File

@@ -13,4 +13,4 @@ user?: {
permissions?: Array<UserPermission>; permissions?: Array<UserPermission>;
}; };
}; };
}; };

View File

@@ -11,4 +11,4 @@ export type GetSubscriptionRequest = {
* Subscription name * Subscription name
*/ */
name?: string; name?: string;
}; };

View File

@@ -7,4 +7,4 @@ import type { Subscription } from './Subscription';
export type GetSubscriptionResponse = { export type GetSubscriptionResponse = {
subscription: Subscription; subscription: Subscription;
files: Array<any>; files: Array<any>;
}; };

View File

@@ -2,6 +2,8 @@
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */ /* eslint-disable */
import type { TaskType } from './TaskType';
export type GetTaskRequest = { export type GetTaskRequest = {
task_key: string; task_key: TaskType;
}; };

View File

@@ -6,4 +6,4 @@ import type { Task } from './Task';
export type GetTaskResponse = { export type GetTaskResponse = {
task?: Task; task?: Task;
}; };

View File

@@ -6,4 +6,4 @@ import type { User } from './User';
export type GetUsersResponse = { export type GetUsersResponse = {
users: Array<User>; users: Array<User>;
}; };

View File

@@ -8,4 +8,4 @@ export type ImportArchiveRequest = {
archive: string; archive: string;
type: FileType; type: FileType;
sub_id?: string; sub_id?: string;
}; };

View File

@@ -9,4 +9,4 @@ export type IncrementViewCountRequest = {
* User UID * User UID
*/ */
uuid?: string; uuid?: string;
}; };

View File

@@ -5,4 +5,4 @@
export type LoginRequest = { export type LoginRequest = {
username: string; username: string;
password: string; password: string;
}; };

View File

@@ -10,4 +10,4 @@ export type LoginResponse = {
token?: string; token?: string;
permissions?: Array<UserPermission>; permissions?: Array<UserPermission>;
available_permissions?: Array<UserPermission>; available_permissions?: Array<UserPermission>;
}; };

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