mirror of
https://github.com/Tzahi12345/YoutubeDL-Material.git
synced 2026-03-24 13:40:57 +03:00
Compare commits
3 Commits
twitch-emo
...
docker-hot
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a4ca1abb7c | ||
|
|
d90434c240 | ||
|
|
7a8e94ee64 |
4
.github/workflows/docker-pr.yml
vendored
4
.github/workflows/docker-pr.yml
vendored
@@ -32,7 +32,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: ./Dockerfile
|
file: ./Dockerfile
|
||||||
platforms: linux/amd64,linux/arm/v7,linux/arm64/v8
|
platforms: linux/amd64,linux/arm,linux/arm64/v8
|
||||||
#platforms: linux/amd64
|
#platforms: linux/amd64
|
||||||
push: false
|
push: false
|
||||||
tags: tzahi12345/youtubedl-material:nightly-pr
|
tags: tzahi12345/youtubedl-material:nightly-pr
|
||||||
4
.github/workflows/docker-release.yml
vendored
4
.github/workflows/docker-release.yml
vendored
@@ -80,7 +80,9 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: ./Dockerfile
|
file: ./Dockerfile
|
||||||
platforms: linux/amd64,linux/arm64/v8
|
platforms: linux/amd64,linux/arm,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 }}
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|||||||
5
.github/workflows/docker.yml
vendored
5
.github/workflows/docker.yml
vendored
@@ -80,7 +80,10 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: ./Dockerfile
|
file: ./Dockerfile
|
||||||
platforms: linux/amd64,linux/arm64/v8,linux/arm/v7
|
platforms: linux/amd64,linux/arm,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 }}
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
|||||||
40
.github/workflows/mocha.yml
vendored
40
.github/workflows/mocha.yml
vendored
@@ -1,40 +0,0 @@
|
|||||||
name: Tests
|
|
||||||
'on':
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- master
|
|
||||||
pull_request:
|
|
||||||
types:
|
|
||||||
- opened
|
|
||||||
- synchronize
|
|
||||||
- reopened
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
test:
|
|
||||||
name: 'Backend - mocha'
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
node:
|
|
||||||
- 16
|
|
||||||
steps:
|
|
||||||
- uses: actions/setup-node@v3
|
|
||||||
with:
|
|
||||||
node-version: '${{ matrix.node }}'
|
|
||||||
- uses: actions/checkout@v2
|
|
||||||
- name: 'Cache node_modules'
|
|
||||||
uses: actions/cache@v2
|
|
||||||
with:
|
|
||||||
path: ~/.npm
|
|
||||||
key: ${{ runner.os }}-node-v${{ matrix.node }}-${{ hashFiles('**/package-lock.json') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-node-v${{ matrix.node }}-
|
|
||||||
working-directory: ./backend
|
|
||||||
- uses: FedericoCarboni/setup-ffmpeg@v2
|
|
||||||
id: setup-ffmpeg
|
|
||||||
- name: Install Dependencies
|
|
||||||
run: npm install
|
|
||||||
working-directory: ./backend
|
|
||||||
- name: Run All Node.js Tests
|
|
||||||
run: npm run test
|
|
||||||
working-directory: ./backend
|
|
||||||
2
.vscode/settings.json
vendored
2
.vscode/settings.json
vendored
@@ -3,6 +3,6 @@
|
|||||||
"mochaExplorer.cwd": "backend",
|
"mochaExplorer.cwd": "backend",
|
||||||
"mochaExplorer.globImplementation": "vscode",
|
"mochaExplorer.globImplementation": "vscode",
|
||||||
"mochaExplorer.env": {
|
"mochaExplorer.env": {
|
||||||
// "YTDL_MODE": "debug"
|
"YTDL_MODE": "debug"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
66
Dockerfile
66
Dockerfile
@@ -1,17 +1,15 @@
|
|||||||
# Fetching our utils
|
# Fetching our ffmpeg
|
||||||
FROM ubuntu:22.04 AS utils
|
FROM ubuntu:22.04 AS ffmpeg
|
||||||
ENV DEBIAN_FRONTEND=noninteractive
|
ENV DEBIAN_FRONTEND=noninteractive
|
||||||
# Use script due local build compability
|
# Use script due local build compability
|
||||||
COPY docker-utils/*.sh .
|
COPY docker-utils/ffmpeg-fetch.sh .
|
||||||
RUN chmod +x *.sh
|
RUN chmod +x ffmpeg-fetch.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:22.04 AS base
|
FROM ubuntu:20.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
|
||||||
@@ -19,30 +17,19 @@ 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 libicu70 && \
|
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 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
|
||||||
ARG BUILDPLATFORM
|
FROM base as frontend
|
||||||
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/" ]
|
||||||
@@ -62,35 +49,32 @@ 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
|
||||||
# armv7 need build from source
|
WORKDIR /app
|
||||||
#WORKDIR /app
|
COPY docker-utils/GetTwitchDownloader.py .
|
||||||
#COPY docker-utils/GetTwitchDownloader.py .
|
RUN apt update && \
|
||||||
#RUN apt update && \
|
apt install -y --no-install-recommends python3-minimal python-is-python3 python3-pip python3.8-dev build-essential && \
|
||||||
# apt install -y --no-install-recommends python3-minimal python-is-python3 python3-pip python3-dev build-essential libffi-dev && \
|
apt clean && \
|
||||||
# apt clean && \
|
rm -rf /var/lib/apt/lists/*
|
||||||
# rm -rf /var/lib/apt/lists/*
|
RUN pip install PyGithub requests
|
||||||
#RUN pip install PyGithub requests
|
RUN python GetTwitchDownloader.py
|
||||||
#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=utils [ "/usr/local/bin/ffmpeg", "/usr/local/bin/ffmpeg" ]
|
COPY --chown=$UID:$GID --from=ffmpeg [ "/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=ffmpeg [ "/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"]
|
||||||
|
|||||||
@@ -1758,14 +1758,14 @@ components:
|
|||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
task_key:
|
task_key:
|
||||||
$ref: '#/components/schemas/TaskType'
|
type: string
|
||||||
required:
|
required:
|
||||||
- task_key
|
- task_key
|
||||||
UpdateTaskScheduleRequest:
|
UpdateTaskScheduleRequest:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
task_key:
|
task_key:
|
||||||
$ref: '#/components/schemas/TaskType'
|
type: string
|
||||||
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:
|
||||||
$ref: '#/components/schemas/TaskType'
|
type: string
|
||||||
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:
|
||||||
$ref: '#/components/schemas/TaskType'
|
type: string
|
||||||
new_options:
|
new_options:
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
@@ -2726,7 +2726,7 @@ components:
|
|||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
key:
|
key:
|
||||||
$ref: '#/components/schemas/TaskType'
|
type: string
|
||||||
title:
|
title:
|
||||||
type: string
|
type: string
|
||||||
last_ran:
|
last_ran:
|
||||||
@@ -2742,20 +2742,9 @@ components:
|
|||||||
error:
|
error:
|
||||||
type: string
|
type: string
|
||||||
schedule:
|
schedule:
|
||||||
$ref: '#/components/schemas/Schedule'
|
type: object
|
||||||
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
|
||||||
@@ -2888,7 +2877,6 @@ components:
|
|||||||
- sharing
|
- sharing
|
||||||
- advanced_download
|
- advanced_download
|
||||||
- downloads_manager
|
- downloads_manager
|
||||||
- tasks_manager
|
|
||||||
YesNo:
|
YesNo:
|
||||||
type: string
|
type: string
|
||||||
enum:
|
enum:
|
||||||
|
|||||||
109
backend/app.js
109
backend/app.js
@@ -535,7 +535,6 @@ 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() {
|
||||||
@@ -706,7 +705,7 @@ app.use(function(req, res, next) {
|
|||||||
next();
|
next();
|
||||||
} else if (req.query.apiKey && config_api.getConfigItem('ytdl_use_api_key') && req.query.apiKey === config_api.getConfigItem('ytdl_api_key')) {
|
} else if (req.query.apiKey && config_api.getConfigItem('ytdl_use_api_key') && req.query.apiKey === config_api.getConfigItem('ytdl_api_key')) {
|
||||||
next();
|
next();
|
||||||
} else if (req.path.includes('/api/stream/') || req.path.includes('/api/thumbnail/') || req.path.includes('/api/emote/') || req.path.includes('/api/rss')) {
|
} else if (req.path.includes('/api/stream/') || req.path.includes('/api/thumbnail/') || req.path.includes('/api/rss')) {
|
||||||
next();
|
next();
|
||||||
} else {
|
} else {
|
||||||
logger.verbose(`Rejecting request - invalid API use for endpoint: ${req.path}. API key received: ${req.query.apiKey}`);
|
logger.verbose(`Rejecting request - invalid API use for endpoint: ${req.path}. API key received: ${req.query.apiKey}`);
|
||||||
@@ -993,11 +992,11 @@ app.post('/api/updateConcurrentStream', optionalJwt, async (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
app.post('/api/getFullTwitchChat', optionalJwt, async (req, res) => {
|
app.post('/api/getFullTwitchChat', optionalJwt, async (req, res) => {
|
||||||
const id = req.body.id;
|
var id = req.body.id;
|
||||||
const type = req.body.type;
|
var type = req.body.type;
|
||||||
const uuid = req.body.uuid;
|
var uuid = req.body.uuid;
|
||||||
const sub = req.body.sub;
|
var sub = req.body.sub;
|
||||||
let user_uid = null;
|
var user_uid = null;
|
||||||
|
|
||||||
if (req.isAuthenticated()) user_uid = req.user.uid;
|
if (req.isAuthenticated()) user_uid = req.user.uid;
|
||||||
|
|
||||||
@@ -1009,12 +1008,12 @@ app.post('/api/getFullTwitchChat', optionalJwt, async (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
app.post('/api/downloadTwitchChatByVODID', optionalJwt, async (req, res) => {
|
app.post('/api/downloadTwitchChatByVODID', optionalJwt, async (req, res) => {
|
||||||
const id = req.body.id;
|
var id = req.body.id;
|
||||||
const type = req.body.type;
|
var type = req.body.type;
|
||||||
const vodId = req.body.vodId;
|
var vodId = req.body.vodId;
|
||||||
const uuid = req.body.uuid;
|
var uuid = req.body.uuid;
|
||||||
const sub = req.body.sub;
|
var sub = req.body.sub;
|
||||||
let user_uid = null;
|
var user_uid = null;
|
||||||
|
|
||||||
if (req.isAuthenticated()) user_uid = req.user.uid;
|
if (req.isAuthenticated()) user_uid = req.user.uid;
|
||||||
|
|
||||||
@@ -1025,47 +1024,17 @@ app.post('/api/downloadTwitchChatByVODID', optionalJwt, async (req, res) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info(`Downloading Twitch chat for ${id}`);
|
|
||||||
const full_chat = await twitch_api.downloadTwitchChatByVODID(vodId, id, type, user_uid, sub);
|
const full_chat = await twitch_api.downloadTwitchChatByVODID(vodId, id, type, user_uid, sub);
|
||||||
logger.info(`Finished downloading Twitch chat for ${id}`);
|
|
||||||
|
|
||||||
res.send({
|
res.send({
|
||||||
chat: full_chat
|
chat: full_chat
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
app.post('/api/getTwitchEmotes', async (req, res) => {
|
|
||||||
const uid = req.body.uid;
|
|
||||||
|
|
||||||
const file = await files_api.getVideo(uid);
|
|
||||||
const channel_name = file['uploader'];
|
|
||||||
|
|
||||||
const emotes_path = path.join('appdata', 'emotes', uid, 'emotes.json')
|
|
||||||
if (!fs.existsSync(emotes_path)) {
|
|
||||||
logger.info(`Downloading Twitch emotes for ${channel_name}`);
|
|
||||||
await twitch_api.downloadTwitchEmotes(channel_name, file.uid);
|
|
||||||
logger.info(`Finished downloading Twitch emotes for ${channel_name}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const emotes = await twitch_api.getTwitchEmotes(file.uid);
|
|
||||||
|
|
||||||
res.send({
|
|
||||||
emotes: emotes
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
app.get('/api/emote/:uid/:id', async (req, res) => {
|
|
||||||
const file_uid = decodeURIComponent(req.params.uid);
|
|
||||||
const emote_id = decodeURIComponent(req.params.id);
|
|
||||||
const emote_path = path.join('appdata', 'emotes', file_uid, emote_id);
|
|
||||||
if (fs.existsSync(emote_path)) path.isAbsolute(emote_path) ? res.sendFile(emote_path) : res.sendFile(path.join(__dirname, emote_path));
|
|
||||||
else res.sendStatus(404);
|
|
||||||
});
|
|
||||||
|
|
||||||
// video sharing
|
// video sharing
|
||||||
app.post('/api/enableSharing', optionalJwt, async (req, res) => {
|
app.post('/api/enableSharing', optionalJwt, async (req, res) => {
|
||||||
const uid = req.body.uid;
|
var uid = req.body.uid;
|
||||||
const is_playlist = req.body.is_playlist;
|
var is_playlist = req.body.is_playlist;
|
||||||
let success = false;
|
let success = false;
|
||||||
// multi-user mode
|
// multi-user mode
|
||||||
if (req.isAuthenticated()) {
|
if (req.isAuthenticated()) {
|
||||||
@@ -1673,8 +1642,6 @@ app.get('/api/stream', optionalJwt, async (req, res) => {
|
|||||||
}
|
}
|
||||||
if (!fs.existsSync(file_path)) {
|
if (!fs.existsSync(file_path)) {
|
||||||
logger.error(`File ${file_path} could not be found! UID: ${uid}, ID: ${file_obj && file_obj.id}`);
|
logger.error(`File ${file_path} could not be found! UID: ${uid}, ID: ${file_obj && file_obj.id}`);
|
||||||
res.sendStatus(404);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
const stat = fs.statSync(file_path);
|
const stat = fs.statSync(file_path);
|
||||||
const fileSize = stat.size;
|
const fileSize = stat.size;
|
||||||
@@ -1961,38 +1928,9 @@ app.post('/api/clearAllLogs', optionalJwt, async function(req, res) {
|
|||||||
|
|
||||||
// user authentication
|
// user authentication
|
||||||
|
|
||||||
app.post('/api/auth/register', optionalJwt, async (req, res) => {
|
app.post('/api/auth/register'
|
||||||
const userid = req.body.userid;
|
, optionalJwt
|
||||||
const username = req.body.username;
|
, auth_api.registerUser);
|
||||||
const plaintextPassword = req.body.password;
|
|
||||||
|
|
||||||
if (userid !== 'admin' && !config_api.getConfigItem('ytdl_allow_registration') && !req.isAuthenticated() && (!req.user || !exports.userHasPermission(req.user.uid, 'settings'))) {
|
|
||||||
logger.error(`Registration failed for user ${userid}. Registration is disabled.`);
|
|
||||||
res.sendStatus(409);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (plaintextPassword === "") {
|
|
||||||
logger.error(`Registration failed for user ${userid}. A password must be provided.`);
|
|
||||||
res.sendStatus(409);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!userid || !username) {
|
|
||||||
logger.error(`Registration failed for user ${userid}. Username or userid is invalid.`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const new_user = await auth_api.registerUser(userid, username, plaintextPassword);
|
|
||||||
|
|
||||||
if (!new_user) {
|
|
||||||
res.sendStatus(409);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
res.send({
|
|
||||||
user: new_user
|
|
||||||
});
|
|
||||||
});
|
|
||||||
app.post('/api/auth/login'
|
app.post('/api/auth/login'
|
||||||
, auth_api.passport.authenticate(['local', 'ldapauth'], {})
|
, auth_api.passport.authenticate(['local', 'ldapauth'], {})
|
||||||
, auth_api.generateJWT
|
, auth_api.generateJWT
|
||||||
@@ -2044,7 +1982,18 @@ 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 {
|
||||||
const success = await auth_api.deleteUser(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}`);
|
||||||
|
}
|
||||||
res.send({success: success});
|
res.send({success: success});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error(err);
|
logger.error(err);
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
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');
|
||||||
@@ -18,7 +16,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 = 10;
|
let saltRounds = null;
|
||||||
|
|
||||||
exports.initialize = function () {
|
exports.initialize = function () {
|
||||||
/*************************
|
/*************************
|
||||||
@@ -33,6 +31,8 @@ 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,7 +68,14 @@ exports.initialize = function () {
|
|||||||
const setupRoles = async () => {
|
const setupRoles = async () => {
|
||||||
const required_roles = {
|
const required_roles = {
|
||||||
admin: {
|
admin: {
|
||||||
permissions: CONSTS.AVAILABLE_PERMISSIONS
|
permissions: [
|
||||||
|
'filemanager',
|
||||||
|
'settings',
|
||||||
|
'subscriptions',
|
||||||
|
'sharing',
|
||||||
|
'advanced_download',
|
||||||
|
'downloads_manager'
|
||||||
|
]
|
||||||
},
|
},
|
||||||
user: {
|
user: {
|
||||||
permissions: [
|
permissions: [
|
||||||
@@ -106,41 +113,55 @@ 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;
|
||||||
|
|
||||||
exports.registerUser = async (userid, username, plaintextPassword) => {
|
if (userid !== 'admin' && !config_api.getConfigItem('ytdl_allow_registration') && !req.isAuthenticated() && (!req.user || !exports.userHasPermission(req.user.uid, 'settings'))) {
|
||||||
const hash = await bcrypt.hash(plaintextPassword, saltRounds);
|
res.sendStatus(409);
|
||||||
const new_user = generateUserObject(userid, username, hash);
|
logger.error(`Registration failed for user ${userid}. Registration is disabled.`);
|
||||||
// check if user exists
|
return;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
exports.deleteUser = async (uid) => {
|
if (plaintextPassword === "") {
|
||||||
let success = false;
|
res.sendStatus(400);
|
||||||
let usersFileFolder = config_api.getConfigItem('ytdl_users_base_path');
|
logger.error(`Registration failed for user ${userid}. A password must be provided.`);
|
||||||
const user_folder = path.join(__dirname, usersFileFolder, uid);
|
return;
|
||||||
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);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/***************************************
|
/***************************************
|
||||||
@@ -221,7 +242,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']
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -305,7 +326,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', {uid: file_uid});
|
let file = await db_api.getRecord('files', {file_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;
|
||||||
@@ -392,8 +413,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);
|
||||||
|
|||||||
@@ -208,9 +208,6 @@ const DEFAULT_CONFIG = {
|
|||||||
"API_key": "",
|
"API_key": "",
|
||||||
"use_youtube_API": false,
|
"use_youtube_API": false,
|
||||||
"youtube_API_key": "",
|
"youtube_API_key": "",
|
||||||
"use_twitch_API": false,
|
|
||||||
"twitch_client_ID": "",
|
|
||||||
"twitch_client_secret": "",
|
|
||||||
"twitch_auto_download_chat": false,
|
"twitch_auto_download_chat": false,
|
||||||
"use_sponsorblock_API": false,
|
"use_sponsorblock_API": false,
|
||||||
"generate_NFO_files": false,
|
"generate_NFO_files": false,
|
||||||
|
|||||||
@@ -110,18 +110,6 @@ exports.CONFIG_ITEMS = {
|
|||||||
'key': 'ytdl_youtube_api_key',
|
'key': 'ytdl_youtube_api_key',
|
||||||
'path': 'YoutubeDLMaterial.API.youtube_API_key'
|
'path': 'YoutubeDLMaterial.API.youtube_API_key'
|
||||||
},
|
},
|
||||||
'ytdl_use_twitch_api': {
|
|
||||||
'key': 'ytdl_use_twitch_api',
|
|
||||||
'path': 'YoutubeDLMaterial.API.use_twitch_API'
|
|
||||||
},
|
|
||||||
'ytdl_twitch_client_id': {
|
|
||||||
'key': 'ytdl_twitch_client_id',
|
|
||||||
'path': 'YoutubeDLMaterial.API.twitch_client_ID'
|
|
||||||
},
|
|
||||||
'ytdl_twitch_client_secret': {
|
|
||||||
'key': 'ytdl_twitch_client_secret',
|
|
||||||
'path': 'YoutubeDLMaterial.API.twitch_client_secret'
|
|
||||||
},
|
|
||||||
'ytdl_twitch_auto_download_chat': {
|
'ytdl_twitch_auto_download_chat': {
|
||||||
'key': 'ytdl_twitch_auto_download_chat',
|
'key': 'ytdl_twitch_auto_download_chat',
|
||||||
'path': 'YoutubeDLMaterial.API.twitch_auto_download_chat'
|
'path': 'YoutubeDLMaterial.API.twitch_auto_download_chat'
|
||||||
@@ -359,11 +347,9 @@ const YTDL_ARGS_WITH_VALUES = [
|
|||||||
'--convert-subs'
|
'--convert-subs'
|
||||||
];
|
];
|
||||||
|
|
||||||
exports.SUBSCRIPTION_BACKUP_PATH = 'subscription_backup.json'
|
|
||||||
|
|
||||||
// we're using a Set here for performance
|
// we're using a Set here for performance
|
||||||
exports.YTDL_ARGS_WITH_VALUES = new Set(YTDL_ARGS_WITH_VALUES);
|
exports.YTDL_ARGS_WITH_VALUES = new Set(YTDL_ARGS_WITH_VALUES);
|
||||||
|
|
||||||
exports.ICON_URL = 'https://i.imgur.com/IKOlr0N.png';
|
exports.ICON_URL = 'https://i.imgur.com/IKOlr0N.png';
|
||||||
|
|
||||||
exports.CURRENT_VERSION = 'v4.3.2';
|
exports.CURRENT_VERSION = 'v4.3.1';
|
||||||
|
|||||||
@@ -11,8 +11,9 @@ const logger = require('./logger');
|
|||||||
const low = require('lowdb')
|
const low = require('lowdb')
|
||||||
const FileSync = require('lowdb/adapters/FileSync');
|
const FileSync = require('lowdb/adapters/FileSync');
|
||||||
const { BehaviorSubject } = require('rxjs');
|
const { BehaviorSubject } = require('rxjs');
|
||||||
|
const local_adapter = new FileSync('./appdata/local_db.json');
|
||||||
|
const local_db = low(local_adapter);
|
||||||
|
|
||||||
let local_db = null;
|
|
||||||
let database = null;
|
let database = null;
|
||||||
exports.database_initialized = false;
|
exports.database_initialized = false;
|
||||||
exports.database_initialized_bs = new BehaviorSubject(false);
|
exports.database_initialized_bs = new BehaviorSubject(false);
|
||||||
@@ -72,6 +73,10 @@ const tables = {
|
|||||||
|
|
||||||
const tables_list = Object.keys(tables);
|
const tables_list = Object.keys(tables);
|
||||||
|
|
||||||
|
const local_db_defaults = {}
|
||||||
|
tables_list.forEach(table => {local_db_defaults[table] = []});
|
||||||
|
local_db.defaults(local_db_defaults).write();
|
||||||
|
|
||||||
let using_local_db = null;
|
let using_local_db = null;
|
||||||
|
|
||||||
function setDB(input_db, input_users_db) {
|
function setDB(input_db, input_users_db) {
|
||||||
@@ -80,18 +85,11 @@ function setDB(input_db, input_users_db) {
|
|||||||
exports.users_db = input_users_db
|
exports.users_db = input_users_db
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.initialize = (input_db, input_users_db, db_name = 'local_db.json') => {
|
exports.initialize = (input_db, input_users_db) => {
|
||||||
setDB(input_db, input_users_db);
|
setDB(input_db, input_users_db);
|
||||||
|
|
||||||
// must be done here to prevent getConfigItem from being called before init
|
// must be done here to prevent getConfigItem from being called before init
|
||||||
using_local_db = config_api.getConfigItem('ytdl_use_local_db');
|
using_local_db = config_api.getConfigItem('ytdl_use_local_db');
|
||||||
|
|
||||||
const local_adapter = new FileSync(`./appdata/${db_name}`);
|
|
||||||
local_db = low(local_adapter);
|
|
||||||
|
|
||||||
const local_db_defaults = {}
|
|
||||||
tables_list.forEach(table => {local_db_defaults[table] = []});
|
|
||||||
local_db.defaults(local_db_defaults).write();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.connectToDB = async (retries = 5, no_fallback = false, custom_connection_string = null) => {
|
exports.connectToDB = async (retries = 5, no_fallback = false, custom_connection_string = null) => {
|
||||||
@@ -815,9 +813,6 @@ exports.applyFilterLocalDB = (db_path, filter_obj, operation) => {
|
|||||||
filtered &= record[filter_prop] === undefined || record[filter_prop] === null;
|
filtered &= record[filter_prop] === undefined || record[filter_prop] === null;
|
||||||
} else {
|
} else {
|
||||||
if (typeof filter_prop_value === 'object') {
|
if (typeof filter_prop_value === 'object') {
|
||||||
if (!record[filter_prop]) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if ('$regex' in filter_prop_value) {
|
if ('$regex' in filter_prop_value) {
|
||||||
filtered &= (record[filter_prop].search(new RegExp(filter_prop_value['$regex'], filter_prop_value['$options'])) !== -1);
|
filtered &= (record[filter_prop].search(new RegExp(filter_prop_value['$regex'], filter_prop_value['$options'])) !== -1);
|
||||||
} else if ('$ne' in filter_prop_value) {
|
} else if ('$ne' in filter_prop_value) {
|
||||||
@@ -833,14 +828,10 @@ exports.applyFilterLocalDB = (db_path, filter_obj, operation) => {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// handle case of nested property check
|
// handle case of nested property check
|
||||||
if (filter_prop.includes('.')) {
|
if (filter_prop.includes('.'))
|
||||||
filtered &= utils.searchObjectByString(record, filter_prop) === filter_prop_value;
|
filtered &= utils.searchObjectByString(record, filter_prop) === filter_prop_value;
|
||||||
} else {
|
else
|
||||||
if (!record[filter_prop]) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
filtered &= record[filter_prop] === filter_prop_value;
|
filtered &= record[filter_prop] === filter_prop_value;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ const Mutex = require('async-mutex').Mutex;
|
|||||||
const youtubedl = require('youtube-dl');
|
const youtubedl = require('youtube-dl');
|
||||||
|
|
||||||
const logger = require('./logger');
|
const logger = require('./logger');
|
||||||
const youtubedl_api = require('./youtube-dl');
|
|
||||||
const config_api = require('./config');
|
const config_api = require('./config');
|
||||||
const twitch_api = require('./twitch');
|
const twitch_api = require('./twitch');
|
||||||
const { create } = require('xmlbuilder2');
|
const { create } = require('xmlbuilder2');
|
||||||
@@ -22,10 +21,10 @@ const mutex = new Mutex();
|
|||||||
let should_check_downloads = true;
|
let should_check_downloads = true;
|
||||||
|
|
||||||
if (db_api.database_initialized) {
|
if (db_api.database_initialized) {
|
||||||
exports.setupDownloads();
|
setupDownloads();
|
||||||
} else {
|
} else {
|
||||||
db_api.database_initialized_bs.subscribe(init => {
|
db_api.database_initialized_bs.subscribe(init => {
|
||||||
if (init) exports.setupDownloads();
|
if (init) setupDownloads();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,7 +47,7 @@ We use checkDownloads() to move downloads through the steps and call their respe
|
|||||||
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
exports.createDownload = async (url, type, options, user_uid = null, sub_id = null, sub_name = null, prefetched_info = null, paused = false) => {
|
exports.createDownload = async (url, type, options, user_uid = null, sub_id = null, sub_name = null, prefetched_info = null) => {
|
||||||
return await mutex.runExclusive(async () => {
|
return await mutex.runExclusive(async () => {
|
||||||
const download = {
|
const download = {
|
||||||
url: url,
|
url: url,
|
||||||
@@ -61,7 +60,7 @@ exports.createDownload = async (url, type, options, user_uid = null, sub_id = nu
|
|||||||
options: options,
|
options: options,
|
||||||
uid: uuid(),
|
uid: uuid(),
|
||||||
step_index: 0,
|
step_index: 0,
|
||||||
paused: paused,
|
paused: false,
|
||||||
running: false,
|
running: false,
|
||||||
finished_step: true,
|
finished_step: true,
|
||||||
error: null,
|
error: null,
|
||||||
@@ -134,7 +133,7 @@ async function handleDownloadError(download, error_message, error_type = null) {
|
|||||||
await db_api.updateRecord('download_queue', {uid: download['uid']}, {error: error_message, finished: true, running: false, error_type: error_type});
|
await db_api.updateRecord('download_queue', {uid: download['uid']}, {error: error_message, finished: true, running: false, error_type: error_type});
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.setupDownloads = async () => {
|
async function setupDownloads() {
|
||||||
await fixDownloadState();
|
await fixDownloadState();
|
||||||
setInterval(checkDownloads, 1000);
|
setInterval(checkDownloads, 1000);
|
||||||
}
|
}
|
||||||
@@ -189,7 +188,7 @@ async function checkDownloads() {
|
|||||||
if (waiting_download['step_index'] === 0) {
|
if (waiting_download['step_index'] === 0) {
|
||||||
collectInfo(waiting_download['uid']);
|
collectInfo(waiting_download['uid']);
|
||||||
} else if (waiting_download['step_index'] === 1) {
|
} else if (waiting_download['step_index'] === 1) {
|
||||||
exports.downloadQueuedFile(waiting_download['uid']);
|
downloadQueuedFile(waiting_download['uid']);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -241,7 +240,7 @@ async function collectInfo(download_uid) {
|
|||||||
let category = null;
|
let category = null;
|
||||||
|
|
||||||
// check if it fits into a category. If so, then get info again using new args
|
// check if it fits into a category. If so, then get info again using new args
|
||||||
if (info.length === 0 || config_api.getConfigItem('ytdl_allow_playlist_categorization')) category = await categories_api.categorize(info);
|
if (!Array.isArray(info) || config_api.getConfigItem('ytdl_allow_playlist_categorization')) category = await categories_api.categorize(info);
|
||||||
|
|
||||||
// set custom output if the category has one and re-retrieve info so the download manager has the right file name
|
// set custom output if the category has one and re-retrieve info so the download manager has the right file name
|
||||||
if (category && category['custom_output']) {
|
if (category && category['custom_output']) {
|
||||||
@@ -260,9 +259,13 @@ async function collectInfo(download_uid) {
|
|||||||
const files_to_check_for_progress = [];
|
const files_to_check_for_progress = [];
|
||||||
|
|
||||||
// store info in download for future use
|
// store info in download for future use
|
||||||
for (let info_obj of info) files_to_check_for_progress.push(utils.removeFileExtension(info_obj['_filename']));
|
if (Array.isArray(info)) {
|
||||||
|
for (let info_obj of info) files_to_check_for_progress.push(utils.removeFileExtension(info_obj['_filename']));
|
||||||
|
} else {
|
||||||
|
files_to_check_for_progress.push(utils.removeFileExtension(info['_filename']));
|
||||||
|
}
|
||||||
|
|
||||||
const playlist_title = info.length > 0 ? info[0]['playlist_title'] || info[0]['playlist'] : null;
|
const playlist_title = Array.isArray(info) ? info[0]['playlist_title'] || info[0]['playlist'] : null;
|
||||||
await db_api.updateRecord('download_queue', {uid: download_uid}, {args: args,
|
await db_api.updateRecord('download_queue', {uid: download_uid}, {args: args,
|
||||||
finished_step: true,
|
finished_step: true,
|
||||||
running: false,
|
running: false,
|
||||||
@@ -275,7 +278,7 @@ async function collectInfo(download_uid) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.downloadQueuedFile = async(download_uid, downloadMethod = youtubedl.exec) => {
|
async function downloadQueuedFile(download_uid) {
|
||||||
const download = await db_api.getRecord('download_queue', {uid: download_uid});
|
const download = await db_api.getRecord('download_queue', {uid: download_uid});
|
||||||
if (download['paused']) {
|
if (download['paused']) {
|
||||||
return;
|
return;
|
||||||
@@ -303,102 +306,114 @@ exports.downloadQueuedFile = async(download_uid, downloadMethod = youtubedl.exec
|
|||||||
const start_time = Date.now();
|
const start_time = Date.now();
|
||||||
|
|
||||||
const download_checker = setInterval(() => checkDownloadPercent(download['uid']), 1000);
|
const download_checker = setInterval(() => checkDownloadPercent(download['uid']), 1000);
|
||||||
const file_objs = [];
|
|
||||||
// download file
|
// download file
|
||||||
const {parsed_output, err} = await youtubedl_api.runYoutubeDL(url, args, downloadMethod);
|
youtubedl.exec(url, args, {maxBuffer: Infinity}, async function(err, output) {
|
||||||
clearInterval(download_checker);
|
const file_objs = [];
|
||||||
let end_time = Date.now();
|
let end_time = Date.now();
|
||||||
let difference = (end_time - start_time)/1000;
|
let difference = (end_time - start_time)/1000;
|
||||||
logger.debug(`${type === 'audio' ? 'Audio' : 'Video'} download delay: ${difference} seconds.`);
|
logger.debug(`${type === 'audio' ? 'Audio' : 'Video'} download delay: ${difference} seconds.`);
|
||||||
if (!parsed_output) {
|
clearInterval(download_checker);
|
||||||
logger.error(err.stderr);
|
if (err) {
|
||||||
await handleDownloadError(download, err.stderr, 'unknown_error');
|
logger.error(err.stderr);
|
||||||
resolve(false);
|
await handleDownloadError(download, err.stderr, 'unknown_error');
|
||||||
return;
|
|
||||||
} else if (parsed_output) {
|
|
||||||
if (parsed_output.length === 0 || parsed_output[0].length === 0) {
|
|
||||||
// ERROR!
|
|
||||||
const error_message = `No output received for video download, check if it exists in your archive.`;
|
|
||||||
await handleDownloadError(download, error_message, 'no_output');
|
|
||||||
logger.warn(error_message);
|
|
||||||
resolve(false);
|
resolve(false);
|
||||||
return;
|
return;
|
||||||
}
|
} else if (output) {
|
||||||
|
if (output.length === 0 || output[0].length === 0) {
|
||||||
for (const output_json of parsed_output) {
|
// ERROR!
|
||||||
if (!output_json) {
|
const error_message = `No output received for video download, check if it exists in your archive.`;
|
||||||
continue;
|
await handleDownloadError(download, error_message, 'no_output');
|
||||||
|
logger.warn(error_message);
|
||||||
|
resolve(false);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// get filepath with no extension
|
for (let i = 0; i < output.length; i++) {
|
||||||
const filepath_no_extension = utils.removeFileExtension(output_json['_filename']);
|
let output_json = null;
|
||||||
|
|
||||||
const ext = type === 'audio' ? '.mp3' : '.mp4';
|
|
||||||
var full_file_path = filepath_no_extension + ext;
|
|
||||||
var file_name = filepath_no_extension.substring(fileFolderPath.length, filepath_no_extension.length);
|
|
||||||
|
|
||||||
if (type === 'video' && url.includes('twitch.tv/videos/') && url.split('twitch.tv/videos/').length > 1
|
|
||||||
&& config_api.getConfigItem('ytdl_twitch_auto_download_chat')) {
|
|
||||||
let vodId = url.split('twitch.tv/videos/')[1];
|
|
||||||
vodId = vodId.split('?')[0];
|
|
||||||
twitch_api.downloadTwitchChatByVODID(vodId, file_name, type, download['user_uid']);
|
|
||||||
}
|
|
||||||
|
|
||||||
// renames file if necessary due to bug
|
|
||||||
if (!fs.existsSync(output_json['_filename']) && fs.existsSync(output_json['_filename'] + '.webm')) {
|
|
||||||
try {
|
try {
|
||||||
fs.renameSync(output_json['_filename'] + '.webm', output_json['_filename']);
|
// we have to do this because sometimes there will be leading characters before the actual json
|
||||||
logger.info('Renamed ' + file_name + '.webm to ' + file_name);
|
const start_idx = output[i].indexOf('{"');
|
||||||
|
const clean_output = output[i].slice(start_idx, output[i].length);
|
||||||
|
output_json = JSON.parse(clean_output);
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
logger.error(`Failed to rename file ${output_json['_filename']} to its appropriate extension.`);
|
output_json = null;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (type === 'audio') {
|
if (!output_json) {
|
||||||
let tags = {
|
continue;
|
||||||
title: output_json['title'],
|
|
||||||
artist: output_json['artist'] ? output_json['artist'] : output_json['uploader']
|
|
||||||
}
|
}
|
||||||
let success = NodeID3.write(tags, utils.removeFileExtension(output_json['_filename']) + '.mp3');
|
|
||||||
if (!success) logger.error('Failed to apply ID3 tag to audio file ' + output_json['_filename']);
|
// get filepath with no extension
|
||||||
|
const filepath_no_extension = utils.removeFileExtension(output_json['_filename']);
|
||||||
|
|
||||||
|
const ext = type === 'audio' ? '.mp3' : '.mp4';
|
||||||
|
var full_file_path = filepath_no_extension + ext;
|
||||||
|
var file_name = filepath_no_extension.substring(fileFolderPath.length, filepath_no_extension.length);
|
||||||
|
|
||||||
|
if (type === 'video' && url.includes('twitch.tv/videos/') && url.split('twitch.tv/videos/').length > 1
|
||||||
|
&& config_api.getConfigItem('ytdl_twitch_auto_download_chat')) {
|
||||||
|
let vodId = url.split('twitch.tv/videos/')[1];
|
||||||
|
vodId = vodId.split('?')[0];
|
||||||
|
twitch_api.downloadTwitchChatByVODID(vodId, file_name, type, download['user_uid']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// renames file if necessary due to bug
|
||||||
|
if (!fs.existsSync(output_json['_filename']) && fs.existsSync(output_json['_filename'] + '.webm')) {
|
||||||
|
try {
|
||||||
|
fs.renameSync(output_json['_filename'] + '.webm', output_json['_filename']);
|
||||||
|
logger.info('Renamed ' + file_name + '.webm to ' + file_name);
|
||||||
|
} catch(e) {
|
||||||
|
logger.error(`Failed to rename file ${output_json['_filename']} to its appropriate extension.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === 'audio') {
|
||||||
|
let tags = {
|
||||||
|
title: output_json['title'],
|
||||||
|
artist: output_json['artist'] ? output_json['artist'] : output_json['uploader']
|
||||||
|
}
|
||||||
|
let success = NodeID3.write(tags, utils.removeFileExtension(output_json['_filename']) + '.mp3');
|
||||||
|
if (!success) logger.error('Failed to apply ID3 tag to audio file ' + output_json['_filename']);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config_api.getConfigItem('ytdl_generate_nfo_files')) {
|
||||||
|
exports.generateNFOFile(output_json, `${filepath_no_extension}.nfo`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.cropFileSettings) {
|
||||||
|
await utils.cropFile(full_file_path, options.cropFileSettings.cropFileStart, options.cropFileSettings.cropFileEnd, ext);
|
||||||
|
}
|
||||||
|
|
||||||
|
// registers file in DB
|
||||||
|
const file_obj = await files_api.registerFileDB(full_file_path, type, download['user_uid'], category, download['sub_id'] ? download['sub_id'] : null, options.cropFileSettings);
|
||||||
|
|
||||||
|
await archive_api.addToArchive(output_json['extractor'], output_json['id'], type, output_json['title'], download['user_uid'], download['sub_id']);
|
||||||
|
|
||||||
|
notifications_api.sendDownloadNotification(file_obj, download['user_uid']);
|
||||||
|
|
||||||
|
file_objs.push(file_obj);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (config_api.getConfigItem('ytdl_generate_nfo_files')) {
|
let container = null;
|
||||||
exports.generateNFOFile(output_json, `${filepath_no_extension}.nfo`);
|
|
||||||
|
if (file_objs.length > 1) {
|
||||||
|
// create playlist
|
||||||
|
const playlist_name = file_objs.map(file_obj => file_obj.title).join(', ');
|
||||||
|
container = await files_api.createPlaylist(playlist_name, file_objs.map(file_obj => file_obj.uid), download['user_uid']);
|
||||||
|
} else if (file_objs.length === 1) {
|
||||||
|
container = file_objs[0];
|
||||||
|
} else {
|
||||||
|
const error_message = 'Downloaded file failed to result in metadata object.';
|
||||||
|
logger.error(error_message);
|
||||||
|
await handleDownloadError(download, error_message, 'no_metadata');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.cropFileSettings) {
|
const file_uids = file_objs.map(file_obj => file_obj.uid);
|
||||||
await utils.cropFile(full_file_path, options.cropFileSettings.cropFileStart, options.cropFileSettings.cropFileEnd, ext);
|
await db_api.updateRecord('download_queue', {uid: download_uid}, {finished_step: true, finished: true, running: false, step_index: 3, percent_complete: 100, file_uids: file_uids, container: container});
|
||||||
}
|
resolve();
|
||||||
|
|
||||||
// registers file in DB
|
|
||||||
const file_obj = await files_api.registerFileDB(full_file_path, type, download['user_uid'], category, download['sub_id'] ? download['sub_id'] : null, options.cropFileSettings);
|
|
||||||
|
|
||||||
await archive_api.addToArchive(output_json['extractor'], output_json['id'], type, output_json['title'], download['user_uid'], download['sub_id']);
|
|
||||||
|
|
||||||
notifications_api.sendDownloadNotification(file_obj, download['user_uid']);
|
|
||||||
|
|
||||||
file_objs.push(file_obj);
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
let container = null;
|
|
||||||
|
|
||||||
if (file_objs.length > 1) {
|
|
||||||
// create playlist
|
|
||||||
const playlist_name = file_objs.map(file_obj => file_obj.title).join(', ');
|
|
||||||
container = await files_api.createPlaylist(playlist_name, file_objs.map(file_obj => file_obj.uid), download['user_uid']);
|
|
||||||
} else if (file_objs.length === 1) {
|
|
||||||
container = file_objs[0];
|
|
||||||
} else {
|
|
||||||
const error_message = 'Downloaded file failed to result in metadata object.';
|
|
||||||
logger.error(error_message);
|
|
||||||
await handleDownloadError(download, error_message, 'no_metadata');
|
|
||||||
}
|
|
||||||
|
|
||||||
const file_uids = file_objs.map(file_obj => file_obj.uid);
|
|
||||||
await db_api.updateRecord('download_queue', {uid: download_uid}, {finished_step: true, finished: true, running: false, step_index: 3, percent_complete: 100, file_uids: file_uids, container: container});
|
|
||||||
resolve(file_uids);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -549,9 +564,33 @@ exports.getVideoInfoByURL = async (url, args = [], download_uid = null) => {
|
|||||||
new_args.push('--dump-json');
|
new_args.push('--dump-json');
|
||||||
|
|
||||||
youtubedl.exec(url, new_args, {maxBuffer: Infinity}, async (err, output) => {
|
youtubedl.exec(url, new_args, {maxBuffer: Infinity}, async (err, output) => {
|
||||||
const parsed_output = utils.parseOutputJSON(output, err);
|
if (output) {
|
||||||
if (parsed_output) {
|
let outputs = [];
|
||||||
resolve(parsed_output);
|
try {
|
||||||
|
for (let i = 0; i < output.length; i++) {
|
||||||
|
let output_json = null;
|
||||||
|
try {
|
||||||
|
output_json = JSON.parse(output[i]);
|
||||||
|
} catch(e) {
|
||||||
|
output_json = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!output_json) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
outputs.push(output_json);
|
||||||
|
}
|
||||||
|
resolve(outputs.length === 1 ? outputs[0] : outputs);
|
||||||
|
} catch(e) {
|
||||||
|
const error = `Error while retrieving info on video with URL ${url} with the following message: output JSON could not be parsed. Output JSON: ${output}`;
|
||||||
|
logger.error(error);
|
||||||
|
if (download_uid) {
|
||||||
|
const download = await db_api.getRecord('download_queue', {uid: download_uid});
|
||||||
|
await handleDownloadError(download, error, 'parse_failed');
|
||||||
|
}
|
||||||
|
resolve(null);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
let error_message = `Error while retrieving info on video with URL ${url} with the following message: ${err}`;
|
let error_message = `Error while retrieving info on video with URL ${url} with the following message: ${err}`;
|
||||||
if (err.stderr) error_message += `\n\n${err.stderr}`;
|
if (err.stderr) error_message += `\n\n${err.stderr}`;
|
||||||
|
|||||||
@@ -1,7 +1,17 @@
|
|||||||
#!/bin/bash
|
#!/bin/sh
|
||||||
set -eu
|
set -eu
|
||||||
|
|
||||||
|
CMD="npm start"
|
||||||
|
|
||||||
|
# if the first arg starts with "-" pass it to program
|
||||||
|
if [ "${1#-}" != "$1" ]; then
|
||||||
|
set -- "$CMD" "$@"
|
||||||
|
fi
|
||||||
|
|
||||||
# chown current working directory to current user
|
# chown current working directory to current user
|
||||||
echo "[entrypoint] setup permission, this may take a while"
|
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" '{}' + || 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" "$@"
|
exec gosu "$UID:$GID" "$0" "$@"
|
||||||
|
fi
|
||||||
|
|
||||||
|
exec "$@"
|
||||||
|
|||||||
6079
backend/package-lock.json
generated
6079
backend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -4,7 +4,7 @@
|
|||||||
"description": "backend for YoutubeDL-Material",
|
"description": "backend for YoutubeDL-Material",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "mocha test --exit -s 1000",
|
"test": "echo \"Error: no test specified\" && exit 1",
|
||||||
"start": "pm2-runtime --raw pm2.config.js",
|
"start": "pm2-runtime --raw pm2.config.js",
|
||||||
"debug": "set YTDL_MODE=debug && node app.js"
|
"debug": "set YTDL_MODE=debug && node app.js"
|
||||||
},
|
},
|
||||||
@@ -25,13 +25,11 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@discordjs/builders": "^1.6.1",
|
"@discordjs/builders": "^1.6.1",
|
||||||
"@discordjs/core": "^0.5.2",
|
"@discordjs/core": "^0.5.2",
|
||||||
"@tzahi12345/twitch-emoticons": "^1.0.9",
|
|
||||||
"archiver": "^5.3.1",
|
"archiver": "^5.3.1",
|
||||||
"async": "^3.2.3",
|
"async": "^3.2.3",
|
||||||
"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",
|
||||||
|
|||||||
@@ -6,21 +6,20 @@ 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');
|
||||||
|
|
||||||
exports.subscribe = async (sub, user_uid = null, skip_get_info = false) => {
|
async function subscribe(sub, user_uid = null) {
|
||||||
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.isPlaylist || sub.url.includes('playlist');
|
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}));
|
||||||
@@ -33,13 +32,12 @@ exports.subscribe = async (sub, user_uid = null, skip_get_info = false) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
sub['user_uid'] = user_uid ? user_uid : undefined;
|
sub['user_uid'] = user_uid ? user_uid : undefined;
|
||||||
await db_api.insertRecordIntoTable('subscriptions', JSON.parse(JSON.stringify(sub)));
|
await db_api.insertRecordIntoTable('subscriptions', sub);
|
||||||
|
|
||||||
let success = skip_get_info ? true : await getSubscriptionInfo(sub);
|
let success = await getSubscriptionInfo(sub);
|
||||||
exports.writeSubscriptionMetadata(sub);
|
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
if (!sub.paused) exports.getVideosForSub(sub, user_uid);
|
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.')
|
||||||
}
|
}
|
||||||
@@ -111,7 +109,7 @@ async function getSubscriptionInfo(sub) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.unsubscribe = async (sub, deleteMode, user_uid = null) => {
|
async function unsubscribe(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');
|
||||||
@@ -150,7 +148,7 @@ exports.unsubscribe = async (sub, deleteMode, user_uid = null) => {
|
|||||||
await db_api.removeAllRecords('archives', {sub_id: sub.id});
|
await db_api.removeAllRecords('archives', {sub_id: sub.id});
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.deleteSubscriptionFile = async (sub, file, deleteForever, file_uid = null, user_uid = null) => {
|
async function deleteSubscriptionFile(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});
|
||||||
@@ -218,8 +216,8 @@ exports.deleteSubscriptionFile = async (sub, file, deleteForever, file_uid = nul
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.getVideosForSub = async (sub, user_uid = null) => {
|
async function getVideosForSub(sub, user_uid = null) {
|
||||||
const latest_sub_obj = await exports.getSubscription(sub.id);
|
const latest_sub_obj = await getSubscription(sub.id);
|
||||||
if (!latest_sub_obj || latest_sub_obj['downloading']) {
|
if (!latest_sub_obj || latest_sub_obj['downloading']) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -254,15 +252,26 @@ exports.getVideosForSub = async (sub, user_uid = null) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
logger.verbose('Subscription: finished check for ' + sub.name);
|
logger.verbose('Subscription: finished check for ' + sub.name);
|
||||||
const parsed_output = utils.parseOutputJSON(output, err);
|
if (err && !output) {
|
||||||
if (!parsed_output) {
|
logger.error(err.stderr ? err.stderr : err.message);
|
||||||
logger.error('Subscription check failed!');
|
if (err.stderr.includes('This video is unavailable') || err.stderr.includes('Private video')) {
|
||||||
resolve(null);
|
logger.info('An error was encountered with at least one video, backup method will be used.')
|
||||||
return;
|
try {
|
||||||
}
|
const outputs = err.stdout.split(/\r\n|\r|\n/);
|
||||||
const files_to_download = await handleOutputJSON(parsed_output, sub, user_uid);
|
const files_to_download = await handleOutputJSON(outputs, sub, user_uid);
|
||||||
resolve(files_to_download);
|
resolve(files_to_download);
|
||||||
return;
|
} catch(e) {
|
||||||
|
logger.error('Backup method failed. See error below:');
|
||||||
|
logger.error(e);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logger.error('Subscription check failed!');
|
||||||
|
}
|
||||||
|
resolve(false);
|
||||||
|
} else if (output) {
|
||||||
|
const files_to_download = await handleOutputJSON(output, sub, user_uid);
|
||||||
|
resolve(files_to_download);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}, err => {
|
}, err => {
|
||||||
logger.error(err);
|
logger.error(err);
|
||||||
@@ -270,30 +279,44 @@ exports.getVideosForSub = async (sub, user_uid = null) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleOutputJSON(output_jsons, sub, user_uid) {
|
async function handleOutputJSON(output, sub, user_uid) {
|
||||||
if (config_api.getConfigItem('ytdl_subscriptions_redownload_fresh_uploads')) {
|
if (config_api.getConfigItem('ytdl_subscriptions_redownload_fresh_uploads')) {
|
||||||
await setFreshUploads(sub, user_uid);
|
await setFreshUploads(sub, user_uid);
|
||||||
checkVideosForFreshUploads(sub, user_uid);
|
checkVideosForFreshUploads(sub, user_uid);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (output_jsons.length === 0 || (output_jsons.length === 1 && output_jsons[0] === '')) {
|
if (output.length === 0 || (output.length === 1 && output[0] === '')) {
|
||||||
logger.verbose('No additional videos to download for ' + sub.name);
|
logger.verbose('No additional videos to download for ' + sub.name);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const output_jsons = [];
|
||||||
|
for (let i = 0; i < output.length; i++) {
|
||||||
|
let output_json = null;
|
||||||
|
try {
|
||||||
|
output_json = JSON.parse(output[i]);
|
||||||
|
output_jsons.push(output_json);
|
||||||
|
} catch(e) {
|
||||||
|
output_json = null;
|
||||||
|
}
|
||||||
|
if (!output_json) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const files_to_download = await getFilesToDownload(sub, output_jsons);
|
const files_to_download = await getFilesToDownload(sub, output_jsons);
|
||||||
const base_download_options = exports.generateOptionsForSubscriptionDownload(sub, user_uid);
|
const base_download_options = generateOptionsForSubscriptionDownload(sub, user_uid);
|
||||||
|
|
||||||
for (let j = 0; j < files_to_download.length; j++) {
|
for (let j = 0; j < files_to_download.length; j++) {
|
||||||
const file_to_download = files_to_download[j];
|
const file_to_download = files_to_download[j];
|
||||||
file_to_download['formats'] = utils.stripPropertiesFromObject(file_to_download['formats'], ['format_id', 'filesize', 'filesize_approx']); // prevent download object from blowing up in size
|
file_to_download['formats'] = utils.stripPropertiesFromObject(file_to_download['formats'], ['format_id', 'filesize', 'filesize_approx']); // prevent download object from blowing up in size
|
||||||
await downloader_api.createDownload(file_to_download['webpage_url'], sub.type || 'video', base_download_options, user_uid, sub.id, sub.name, [file_to_download]);
|
await downloader_api.createDownload(file_to_download['webpage_url'], sub.type || 'video', base_download_options, user_uid, sub.id, sub.name, file_to_download);
|
||||||
}
|
}
|
||||||
|
|
||||||
return files_to_download;
|
return files_to_download;
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.generateOptionsForSubscriptionDownload = (sub, user_uid) => {
|
function 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');
|
||||||
@@ -348,13 +371,10 @@ async function generateArgsForSubscription(sub, user_uid, redownload = false, de
|
|||||||
|
|
||||||
// skip videos that are in the archive. otherwise sub download can be permanently slow (vs. just the first time)
|
// skip videos that are in the archive. otherwise sub download can be permanently slow (vs. just the first time)
|
||||||
const archive_text = await archive_api.generateArchive(sub.type, sub.user_uid, sub.id);
|
const archive_text = await archive_api.generateArchive(sub.type, sub.user_uid, sub.id);
|
||||||
const archive_count = archive_text.split('\n').length - 1;
|
logger.verbose(`Generating temporary archive file for subscription ${sub.name} with ${archive_text.split('\n').length - 1} entries.`)
|
||||||
if (archive_count > 0) {
|
const archive_path = path.join(appendedBasePath, 'archive.txt');
|
||||||
logger.verbose(`Generating temporary archive file for subscription ${sub.name} with ${archive_count} entries.`)
|
await fs.writeFile(archive_path, archive_text);
|
||||||
const archive_path = path.join(appendedBasePath, 'archive.txt');
|
downloadConfig.push('--download-archive', archive_path);
|
||||||
await fs.writeFile(archive_path, archive_text);
|
|
||||||
downloadConfig.push('--download-archive', archive_path);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sub.custom_args) {
|
if (sub.custom_args) {
|
||||||
const customArgsArray = sub.custom_args.split(',,');
|
const customArgsArray = sub.custom_args.split(',,');
|
||||||
@@ -420,36 +440,35 @@ async function getFilesToDownload(sub, output_jsons) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
exports.getSubscriptions = async (user_uid = null) => {
|
async function getSubscriptions(user_uid = null) {
|
||||||
return await db_api.getRecords('subscriptions', {user_uid: user_uid});
|
return await db_api.getRecords('subscriptions', {user_uid: user_uid});
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.getAllSubscriptions = async () => {
|
async function getAllSubscriptions() {
|
||||||
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.getSubscription = async (subID) => {
|
async function getSubscription(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: subID}, true);
|
const current_downloads = await db_api.getRecords('download_queue', {running: true, sub_id: sub.id}, true);
|
||||||
if (!sub['downloading']) sub['downloading'] = current_downloads > 0;
|
if (!sub['downloading']) sub['downloading'] = current_downloads > 0;
|
||||||
return sub;
|
return sub;
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.getSubscriptionByName = async (subName, user_uid = null) => {
|
async function getSubscriptionByName(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});
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.updateSubscription = async (sub) => {
|
async function updateSubscription(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;
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.updateSubscriptionPropertyMultiple = async (subs, assignment_obj) => {
|
async function updateSubscriptionPropertyMultiple(subs, assignment_obj) {
|
||||||
subs.forEach(async sub => {
|
subs.forEach(async sub => {
|
||||||
await updateSubscriptionProperty(sub, assignment_obj);
|
await updateSubscriptionProperty(sub, assignment_obj);
|
||||||
});
|
});
|
||||||
@@ -461,16 +480,6 @@ async function updateSubscriptionProperty(sub, assignment_obj) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.writeSubscriptionMetadata = (sub) => {
|
|
||||||
let basePath = sub.user_uid ? path.join(config_api.getConfigItem('ytdl_users_base_path'), sub.user_uid, 'subscriptions')
|
|
||||||
: config_api.getConfigItem('ytdl_subscriptions_base_path');
|
|
||||||
const appendedBasePath = getAppendedBasePath(sub, basePath);
|
|
||||||
const metadata_path = path.join(appendedBasePath, CONSTS.SUBSCRIPTION_BACKUP_PATH);
|
|
||||||
|
|
||||||
fs.ensureDirSync(appendedBasePath);
|
|
||||||
fs.writeJSONSync(metadata_path, sub);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function setFreshUploads(sub) {
|
async function setFreshUploads(sub) {
|
||||||
const sub_files = await db_api.getRecords('files', {sub_id: sub.id});
|
const sub_files = await db_api.getRecords('files', {sub_id: sub.id});
|
||||||
if (!sub_files) return;
|
if (!sub_files) return;
|
||||||
@@ -525,3 +534,17 @@ async function checkVideoIfBetterExists(file_obj, sub, user_uid) {
|
|||||||
function getAppendedBasePath(sub, base_path) {
|
function getAppendedBasePath(sub, base_path) {
|
||||||
return path.join(base_path, (sub.isPlaylist ? 'playlists/' : 'channels/'), sub.name);
|
return path.join(base_path, (sub.isPlaylist ? 'playlists/' : 'channels/'), sub.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
getSubscription : getSubscription,
|
||||||
|
getSubscriptionByName : getSubscriptionByName,
|
||||||
|
getSubscriptions : getSubscriptions,
|
||||||
|
getAllSubscriptions : getAllSubscriptions,
|
||||||
|
updateSubscription : updateSubscription,
|
||||||
|
subscribe : subscribe,
|
||||||
|
unsubscribe : unsubscribe,
|
||||||
|
deleteSubscriptionFile : deleteSubscriptionFile,
|
||||||
|
getVideosForSub : getVideosForSub,
|
||||||
|
updateSubscriptionPropertyMultiple : updateSubscriptionPropertyMultiple,
|
||||||
|
generateOptionsForSubscriptionDownload: generateOptionsForSubscriptionDownload
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,17 +3,10 @@ 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 path = require('path');
|
const logger = require('./logger');
|
||||||
const scheduler = require('node-schedule');
|
const scheduler = require('node-schedule');
|
||||||
const { uuid } = require('uuidv4');
|
|
||||||
|
|
||||||
const TASKS = {
|
const TASKS = {
|
||||||
backup_local_db: {
|
backup_local_db: {
|
||||||
@@ -54,11 +47,6 @@ 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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -277,68 +265,4 @@ 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;
|
||||||
1
backend/test/sample.info.json
Normal file
1
backend/test/sample.info.json
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -3,8 +3,7 @@ const assert = require('assert');
|
|||||||
const low = require('lowdb')
|
const low = require('lowdb')
|
||||||
const winston = require('winston');
|
const winston = require('winston');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const util = require('util');
|
|
||||||
const exec = util.promisify(require('child_process').exec);
|
|
||||||
|
|
||||||
const FileSync = require('lowdb/adapters/FileSync');
|
const FileSync = require('lowdb/adapters/FileSync');
|
||||||
|
|
||||||
@@ -42,13 +41,11 @@ const subscriptions_api = require('../subscriptions');
|
|||||||
const archive_api = require('../archive');
|
const archive_api = require('../archive');
|
||||||
const categories_api = require('../categories');
|
const categories_api = require('../categories');
|
||||||
const files_api = require('../files');
|
const files_api = require('../files');
|
||||||
const youtubedl_api = require('../youtube-dl');
|
|
||||||
const config_api = require('../config');
|
|
||||||
const fs = require('fs-extra');
|
const fs = require('fs-extra');
|
||||||
const { uuid } = require('uuidv4');
|
const { uuid } = require('uuidv4');
|
||||||
const NodeID3 = require('node-id3');
|
const NodeID3 = require('node-id3');
|
||||||
|
|
||||||
db_api.initialize(db, users_db, 'local_db_test.json');
|
db_api.initialize(db, users_db);
|
||||||
|
|
||||||
const sample_video_json = {
|
const sample_video_json = {
|
||||||
id: "Sample Video",
|
id: "Sample Video",
|
||||||
@@ -71,9 +68,9 @@ const sample_video_json = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
describe('Database', async function() {
|
describe('Database', async function() {
|
||||||
describe.skip('Import', async function() {
|
describe('Import', async function() {
|
||||||
// it('Migrate', async function() {
|
// it('Migrate', async function() {
|
||||||
// // await db_api.connectToDB();
|
// await db_api.connectToDB();
|
||||||
// await db_api.removeAllRecords();
|
// await db_api.removeAllRecords();
|
||||||
// const success = await db_api.importJSONToDB(db.value(), users_db.value());
|
// const success = await db_api.importJSONToDB(db.value(), users_db.value());
|
||||||
// assert(success);
|
// assert(success);
|
||||||
@@ -89,7 +86,7 @@ describe('Database', async function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('Transfer to local', async function() {
|
it('Transfer to local', async function() {
|
||||||
// await db_api.connectToDB();
|
await db_api.connectToDB();
|
||||||
await db_api.removeAllRecords('test');
|
await db_api.removeAllRecords('test');
|
||||||
await db_api.insertRecordIntoTable('test', {test: 'test'});
|
await db_api.insertRecordIntoTable('test', {test: 'test'});
|
||||||
|
|
||||||
@@ -117,8 +114,7 @@ describe('Database', async function() {
|
|||||||
|
|
||||||
for (const local_db_mode of local_db_modes) {
|
for (const local_db_mode of local_db_modes) {
|
||||||
let use_local_db = local_db_mode;
|
let use_local_db = local_db_mode;
|
||||||
const describe_skippable = use_local_db ? describe : describe.skip;
|
describe(`Use local DB - ${use_local_db}`, async function() {
|
||||||
describe_skippable(`Use local DB - ${use_local_db}`, async function() {
|
|
||||||
beforeEach(async function() {
|
beforeEach(async function() {
|
||||||
if (!use_local_db) {
|
if (!use_local_db) {
|
||||||
this.timeout(120000);
|
this.timeout(120000);
|
||||||
@@ -171,7 +167,7 @@ describe('Database', async function() {
|
|||||||
];
|
];
|
||||||
await db_api.insertRecordsIntoTable('test', test_duplicates);
|
await db_api.insertRecordsIntoTable('test', test_duplicates);
|
||||||
const duplicates = await db_api.findDuplicatesByKey('test', 'test');
|
const duplicates = await db_api.findDuplicatesByKey('test', 'test');
|
||||||
assert(duplicates && duplicates.length === 2 && duplicates[0]['key'] === '2' && duplicates[1]['key'] === '4')
|
console.log(duplicates);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Update record', async function() {
|
it('Update record', async function() {
|
||||||
@@ -283,7 +279,7 @@ describe('Database', async function() {
|
|||||||
assert(stats);
|
assert(stats);
|
||||||
});
|
});
|
||||||
|
|
||||||
it.skip('Query speed', async function() {
|
it('Query speed', async function() {
|
||||||
this.timeout(120000);
|
this.timeout(120000);
|
||||||
const NUM_RECORDS_TO_ADD = 300004; // max batch ops is 1000
|
const NUM_RECORDS_TO_ADD = 300004; // max batch ops is 1000
|
||||||
const test_records = [];
|
const test_records = [];
|
||||||
@@ -341,23 +337,16 @@ describe('Database', async function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('Multi User', async function() {
|
describe('Multi User', async function() {
|
||||||
this.timeout(120000);
|
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();
|
||||||
await auth_api.deleteUser(user_to_test);
|
user = await auth_api.login('admin', 'pass');
|
||||||
});
|
});
|
||||||
describe('Basic', function() {
|
describe('Authentication', function() {
|
||||||
it('Register', async function() {
|
it('login', 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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -373,18 +362,18 @@ describe('Multi User', async function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('Video access - disallowed', async function() {
|
it('Video access - disallowed', async function() {
|
||||||
await db_api.setVideoProperty(video_to_test, {sharingEnabled: false});
|
await db_api.setVideoProperty(video_to_test, {sharingEnabled: false}, user_to_test);
|
||||||
const video_obj = await auth_api.getUserVideo(user_to_test, video_to_test, true);
|
const video_obj = auth_api.getUserVideo('admin', 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 = await auth_api.getUserVideo(user_to_test, video_to_test, true);
|
const video_obj = auth_api.getUserVideo('admin', video_to_test, true);
|
||||||
assert(video_obj);
|
assert(video_obj);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe.skip('Zip generators', function() {
|
describe('Zip generators', function() {
|
||||||
it('Playlist zip generator', async function() {
|
it('Playlist zip generator', async function() {
|
||||||
const playlist = await files_api.getPlaylist(playlist_to_test, user_to_test);
|
const playlist = await files_api.getPlaylist(playlist_to_test, user_to_test);
|
||||||
assert(playlist);
|
assert(playlist);
|
||||||
@@ -440,50 +429,35 @@ describe('Multi User', async function() {
|
|||||||
|
|
||||||
describe('Downloader', function() {
|
describe('Downloader', function() {
|
||||||
const downloader_api = require('../downloader');
|
const downloader_api = require('../downloader');
|
||||||
const url = 'https://www.youtube.com/watch?v=hpigjnKl7nI';
|
const url = 'https://www.youtube.com/watch?v=dQw4w9WgXcQ';
|
||||||
const sub_id = 'dc834388-3454-41bf-a618-e11cb8c7de1c';
|
const sub_id = 'dc834388-3454-41bf-a618-e11cb8c7de1c';
|
||||||
const options = {
|
const options = {
|
||||||
ui_uid: uuid()
|
ui_uid: uuid(),
|
||||||
|
user: 'admin'
|
||||||
}
|
}
|
||||||
|
|
||||||
before(async function() {
|
|
||||||
const update_available = await youtubedl_api.checkForYoutubeDLUpdate();
|
|
||||||
if (update_available) await youtubedl_api.updateYoutubeDL(update_available);
|
|
||||||
config_api.setConfigItem('ytdl_max_concurrent_downloads', 0);
|
|
||||||
});
|
|
||||||
|
|
||||||
beforeEach(async function() {
|
beforeEach(async function() {
|
||||||
// await db_api.connectToDB();
|
await db_api.connectToDB();
|
||||||
await db_api.removeAllRecords('download_queue');
|
await db_api.removeAllRecords('download_queue');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Get file info', async function() {
|
it('Get file info', async function() {
|
||||||
this.timeout(300000);
|
this.timeout(300000);
|
||||||
const info = await downloader_api.getVideoInfoByURL(url);
|
const info = await downloader_api.getVideoInfoByURL(url);
|
||||||
assert(!!info && info.length > 0);
|
assert(!!info);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Download file', async function() {
|
it('Download file', async function() {
|
||||||
this.timeout(300000);
|
this.timeout(300000);
|
||||||
await downloader_api.setupDownloads();
|
|
||||||
const args = await downloader_api.generateArgs(url, 'video', options, null, true);
|
|
||||||
const [info] = await downloader_api.getVideoInfoByURL(url, args);
|
|
||||||
if (fs.existsSync(info['_filename'])) fs.unlinkSync(info['_filename']);
|
|
||||||
const returned_download = await downloader_api.createDownload(url, 'video', options);
|
const returned_download = await downloader_api.createDownload(url, 'video', options);
|
||||||
assert(returned_download);
|
console.log(returned_download);
|
||||||
const custom_download_method = async (url, args, options, callback) => {
|
await utils.wait(20000);
|
||||||
fs.writeJSONSync(utils.getTrueFileName(info['_filename'], 'video', '.info.json'), info);
|
|
||||||
await generateEmptyVideoFile(info['_filename']);
|
|
||||||
return await callback(null, [JSON.stringify(info)]);
|
|
||||||
}
|
|
||||||
const success = await downloader_api.downloadQueuedFile(returned_download['uid'], custom_download_method);
|
|
||||||
assert(success);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Tag file', async function() {
|
it('Tag file', async function() {
|
||||||
const success = await generateEmptyAudioFile('test/sample_mp3.mp3');
|
const audio_path = './test/sample.mp3';
|
||||||
const audio_path = './test/sample_mp3.mp3';
|
const sample_json = fs.readJSONSync('./test/sample.info.json');
|
||||||
const sample_json = fs.readJSONSync('./test/sample_mp3.info.json');
|
|
||||||
const tags = {
|
const tags = {
|
||||||
title: sample_json['title'],
|
title: sample_json['title'],
|
||||||
artist: sample_json['artist'] ? sample_json['artist'] : sample_json['uploader'],
|
artist: sample_json['artist'] ? sample_json['artist'] : sample_json['uploader'],
|
||||||
@@ -491,13 +465,14 @@ describe('Downloader', function() {
|
|||||||
}
|
}
|
||||||
NodeID3.write(tags, audio_path);
|
NodeID3.write(tags, audio_path);
|
||||||
const written_tags = NodeID3.read(audio_path);
|
const written_tags = NodeID3.read(audio_path);
|
||||||
assert(success && written_tags['raw']['TRCK'] === '27');
|
assert(written_tags['raw']['TRCK'] === '27');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Queue file', async function() {
|
it('Queue file', async function() {
|
||||||
this.timeout(300000);
|
this.timeout(300000);
|
||||||
const returned_download = await downloader_api.createDownload(url, 'video', options, null, null, null, null, true);
|
const returned_download = await downloader_api.createDownload(url, 'video', options);
|
||||||
assert(returned_download);
|
console.log(returned_download);
|
||||||
|
await utils.wait(20000);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Pause file', async function() {
|
it('Pause file', async function() {
|
||||||
@@ -512,7 +487,7 @@ describe('Downloader', function() {
|
|||||||
assert(args.length > 0);
|
assert(args.length > 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it.skip('Generate args - subscription', async function() {
|
it('Generate args - subscription', async function() {
|
||||||
const sub = await subscriptions_api.getSubscription(sub_id);
|
const sub = await subscriptions_api.getSubscription(sub_id);
|
||||||
const sub_options = subscriptions_api.generateOptionsForSubscriptionDownload(sub, 'admin');
|
const sub_options = subscriptions_api.generateOptionsForSubscriptionDownload(sub, 'admin');
|
||||||
const args_normal = await downloader_api.generateArgs(url, 'video', options);
|
const args_normal = await downloader_api.generateArgs(url, 'video', options);
|
||||||
@@ -525,7 +500,7 @@ describe('Downloader', function() {
|
|||||||
if (fs.existsSync(nfo_file_path)) {
|
if (fs.existsSync(nfo_file_path)) {
|
||||||
fs.unlinkSync(nfo_file_path);
|
fs.unlinkSync(nfo_file_path);
|
||||||
}
|
}
|
||||||
const sample_json = fs.readJSONSync('./test/sample_mp4.info.json');
|
const sample_json = fs.readJSONSync('./test/sample.info.json');
|
||||||
downloader_api.generateNFOFile(sample_json, nfo_file_path);
|
downloader_api.generateNFOFile(sample_json, nfo_file_path);
|
||||||
assert(fs.existsSync(nfo_file_path), true);
|
assert(fs.existsSync(nfo_file_path), true);
|
||||||
fs.unlinkSync(nfo_file_path);
|
fs.unlinkSync(nfo_file_path);
|
||||||
@@ -550,76 +525,26 @@ describe('Downloader', function() {
|
|||||||
const expected_args3 = ['-o', '%(title)s.%(ext)s', '--min-filesize', '1'];
|
const expected_args3 = ['-o', '%(title)s.%(ext)s', '--min-filesize', '1'];
|
||||||
assert(JSON.stringify(updated_args3) === JSON.stringify(expected_args3));
|
assert(JSON.stringify(updated_args3) === JSON.stringify(expected_args3));
|
||||||
});
|
});
|
||||||
});
|
describe('Twitch', async function () {
|
||||||
|
const twitch_api = require('../twitch');
|
||||||
|
const example_vod = '1710641401';
|
||||||
|
it('Download VOD', async function() {
|
||||||
|
const sample_path = path.join('test', 'sample.twitch_chat.json');
|
||||||
|
if (fs.existsSync(sample_path)) fs.unlinkSync(sample_path);
|
||||||
|
this.timeout(300000);
|
||||||
|
await twitch_api.downloadTwitchChatByVODID(example_vod, 'sample', null, null, null, './test');
|
||||||
|
assert(fs.existsSync(sample_path));
|
||||||
|
|
||||||
describe('Twitch', async function () {
|
// cleanup
|
||||||
const twitch_api = require('../twitch');
|
if (fs.existsSync(sample_path)) fs.unlinkSync(sample_path);
|
||||||
const example_vod = '1710641401';
|
});
|
||||||
const example_channel = 'keffals';
|
|
||||||
|
|
||||||
it('Get OAuth Token', async function() {
|
|
||||||
this.timeout(300000);
|
|
||||||
const twitch_client_id = config_api.getConfigItem('ytdl_twitch_client_id');
|
|
||||||
const twitch_client_secret = config_api.getConfigItem('ytdl_twitch_client_secret');
|
|
||||||
if (!twitch_client_id || !twitch_client_secret) {
|
|
||||||
logger.info(`Skipping test 'Get OAuth Token' as Twitch client ID or Twitch client secret is missing.`);
|
|
||||||
assert(true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const token = await twitch_api.getTwitchOAuthToken(twitch_client_id, twitch_client_secret);
|
|
||||||
assert(token);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Get channel ID', async function() {
|
|
||||||
this.timeout(300000);
|
|
||||||
const twitch_client_id = config_api.getConfigItem('ytdl_twitch_client_id');
|
|
||||||
const twitch_client_secret = config_api.getConfigItem('ytdl_twitch_client_secret');
|
|
||||||
if (!twitch_client_id || !twitch_client_secret) {
|
|
||||||
logger.info(`Skipping test 'Get channel ID' as Twitch client ID or Twitch client secret is missing.`);
|
|
||||||
assert(true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const channel_id = await twitch_api.getChannelID(example_channel);
|
|
||||||
assert(channel_id === '494493142');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Download VOD chat', async function() {
|
|
||||||
this.timeout(300000);
|
|
||||||
if (!fs.existsSync('TwitchDownloaderCLI')) {
|
|
||||||
try {
|
|
||||||
await exec('sh ../docker-utils/fetch-twitchdownloader.sh');
|
|
||||||
fs.copyFileSync('../docker-utils/TwitchDownloaderCLI', 'TwitchDownloaderCLI');
|
|
||||||
} catch (e) {
|
|
||||||
logger.info('TwitchDownloaderCLI fetch failed, file may exist regardless.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const sample_path = path.join('test', 'sample.twitch_chat.json');
|
|
||||||
if (fs.existsSync(sample_path)) fs.unlinkSync(sample_path);
|
|
||||||
await twitch_api.downloadTwitchChatByVODID(example_vod, 'sample', null, null, null, './test');
|
|
||||||
assert(fs.existsSync(sample_path));
|
|
||||||
|
|
||||||
// cleanup
|
|
||||||
if (fs.existsSync(sample_path)) fs.unlinkSync(sample_path);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Download Twitch emotes', async function() {
|
|
||||||
this.timeout(300000);
|
|
||||||
const twitch_client_id = config_api.getConfigItem('ytdl_twitch_client_id');
|
|
||||||
const twitch_client_secret = config_api.getConfigItem('ytdl_twitch_client_secret');
|
|
||||||
if (!twitch_client_id || !twitch_client_secret) {
|
|
||||||
logger.info(`Skipping test 'Download Twitch emotes' as Twitch client ID or Twitch client secret is missing.`);
|
|
||||||
assert(true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const emotesJSON = await twitch_api.downloadTwitchEmotes(example_channel, 'test_uid');
|
|
||||||
assert(emotesJSON && emotesJSON.length > 0);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Tasks', function() {
|
describe('Tasks', function() {
|
||||||
const tasks_api = require('../tasks');
|
const tasks_api = require('../tasks');
|
||||||
beforeEach(async function() {
|
beforeEach(async function() {
|
||||||
// await db_api.connectToDB();
|
await db_api.connectToDB();
|
||||||
await db_api.removeAllRecords('tasks');
|
await db_api.removeAllRecords('tasks');
|
||||||
|
|
||||||
const dummy_task = {
|
const dummy_task = {
|
||||||
@@ -638,7 +563,7 @@ describe('Tasks', function() {
|
|||||||
await tasks_api.executeTask('backup_local_db');
|
await tasks_api.executeTask('backup_local_db');
|
||||||
const backups_new = await utils.recFindByExt('appdata', 'bak');
|
const backups_new = await utils.recFindByExt('appdata', 'bak');
|
||||||
const new_length = backups_new.length;
|
const new_length = backups_new.length;
|
||||||
assert(original_length === new_length-1);
|
assert(original_length, new_length-1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Check for missing files', async function() {
|
it('Check for missing files', async function() {
|
||||||
@@ -648,7 +573,7 @@ describe('Tasks', function() {
|
|||||||
await db_api.insertRecordIntoTable('files', test_missing_file);
|
await db_api.insertRecordIntoTable('files', test_missing_file);
|
||||||
await tasks_api.executeTask('missing_files_check');
|
await tasks_api.executeTask('missing_files_check');
|
||||||
const missing_file_db_record = await db_api.getRecord('files', {uid: 'test'});
|
const missing_file_db_record = await db_api.getRecord('files', {uid: 'test'});
|
||||||
assert(!missing_file_db_record);
|
assert(!missing_file_db_record, true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Check for duplicate files', async function() {
|
it('Check for duplicate files', async function() {
|
||||||
@@ -668,29 +593,27 @@ describe('Tasks', function() {
|
|||||||
|
|
||||||
await tasks_api.executeTask('duplicate_files_check');
|
await tasks_api.executeTask('duplicate_files_check');
|
||||||
const duplicated_record_count = await db_api.getRecords('files', {path: 'test/missing_file.mp4'}, true);
|
const duplicated_record_count = await db_api.getRecords('files', {path: 'test/missing_file.mp4'}, true);
|
||||||
assert(duplicated_record_count === 1);
|
assert(duplicated_record_count == 1, true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Import unregistered files', async function() {
|
it('Import unregistered files', async function() {
|
||||||
this.timeout(300000);
|
this.timeout(300000);
|
||||||
|
|
||||||
const success = await generateEmptyVideoFile('test/sample_mp4.mp4');
|
|
||||||
|
|
||||||
// pre-test cleanup
|
// pre-test cleanup
|
||||||
await db_api.removeAllRecords('files', {title: 'Sample File'});
|
await db_api.removeAllRecords('files', {title: 'Sample File'});
|
||||||
if (fs.existsSync('video/sample_mp4.info.json')) fs.unlinkSync('video/sample_mp4.info.json');
|
if (fs.existsSync('video/sample.info.json')) fs.unlinkSync('video/sample.info.json');
|
||||||
if (fs.existsSync('video/sample_mp4.mp4')) fs.unlinkSync('video/sample_mp4.mp4');
|
if (fs.existsSync('video/sample.mp4')) fs.unlinkSync('video/sample.mp4');
|
||||||
|
|
||||||
// copies in files
|
// copies in files
|
||||||
fs.copyFileSync('test/sample_mp4.info.json', 'video/sample_mp4.info.json');
|
fs.copyFileSync('test/sample.info.json', 'video/sample.info.json');
|
||||||
fs.copyFileSync('test/sample_mp4.mp4', 'video/sample_mp4.mp4');
|
fs.copyFileSync('test/sample.mp4', 'video/sample.mp4');
|
||||||
await tasks_api.executeTask('missing_db_records');
|
await tasks_api.executeTask('missing_db_records');
|
||||||
const imported_file = await db_api.getRecord('files', {title: 'Sample File'});
|
const imported_file = await db_api.getRecord('files', {title: 'Sample File'});
|
||||||
assert(success && !!imported_file);
|
assert(!!imported_file === true);
|
||||||
|
|
||||||
// post-test cleanup
|
// post-test cleanup
|
||||||
if (fs.existsSync('video/sample_mp4.info.json')) fs.unlinkSync('video/sample_mp4.info.json');
|
if (fs.existsSync('video/sample.info.json')) fs.unlinkSync('video/sample.info.json');
|
||||||
if (fs.existsSync('video/sample_mp4.mp4')) fs.unlinkSync('video/sample_mp4.mp4');
|
if (fs.existsSync('video/sample.mp4')) fs.unlinkSync('video/sample.mp4');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Schedule and cancel task', async function() {
|
it('Schedule and cancel task', async function() {
|
||||||
@@ -730,12 +653,12 @@ describe('Tasks', function() {
|
|||||||
|
|
||||||
describe('Archive', async function() {
|
describe('Archive', async function() {
|
||||||
beforeEach(async function() {
|
beforeEach(async function() {
|
||||||
// await db_api.connectToDB();
|
await db_api.connectToDB();
|
||||||
await db_api.removeAllRecords('archives');
|
await db_api.removeAllRecords('archives', {user_uid: 'test_user'});
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(async function() {
|
afterEach(async function() {
|
||||||
await db_api.removeAllRecords('archives');
|
await db_api.removeAllRecords('archives', {user_uid: 'test_user'});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Import archive', async function() {
|
it('Import archive', async function() {
|
||||||
@@ -749,6 +672,7 @@ describe('Archive', async function() {
|
|||||||
const count = await archive_api.importArchiveFile(archive_text, 'video', 'test_user', 'test_sub');
|
const count = await archive_api.importArchiveFile(archive_text, 'video', 'test_user', 'test_sub');
|
||||||
assert(count === 4)
|
assert(count === 4)
|
||||||
const archive_items = await db_api.getRecords('archives', {user_uid: 'test_user', sub_id: 'test_sub'});
|
const archive_items = await db_api.getRecords('archives', {user_uid: 'test_user', sub_id: 'test_sub'});
|
||||||
|
console.log(archive_items);
|
||||||
assert(archive_items.length === 4);
|
assert(archive_items.length === 4);
|
||||||
assert(archive_items.filter(archive_item => archive_item.extractor === 'testextractor2').length === 1);
|
assert(archive_items.filter(archive_item => archive_item.extractor === 'testextractor2').length === 1);
|
||||||
assert(archive_items.filter(archive_item => archive_item.extractor === 'testextractor1').length === 3);
|
assert(archive_items.filter(archive_item => archive_item.extractor === 'testextractor1').length === 3);
|
||||||
@@ -779,9 +703,9 @@ describe('Archive', async function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('Remove from archive', async function() {
|
it('Remove from archive', async function() {
|
||||||
await archive_api.addToArchive('testextractor1', 'testing1', 'video', 'test_title', 'test_user');
|
await archive_api.addToArchive('testextractor1', 'testing1', 'video', 'test_user');
|
||||||
await archive_api.addToArchive('testextractor2', 'testing1', 'video', 'test_title', 'test_user');
|
await archive_api.addToArchive('testextractor2', 'testing1', 'video', 'test_user');
|
||||||
await archive_api.addToArchive('testextractor2', 'testing2', 'video', 'test_title', 'test_user');
|
await archive_api.addToArchive('testextractor2', 'testing2', 'video', 'test_user');
|
||||||
|
|
||||||
const success = await archive_api.removeFromArchive('testextractor2', 'testing1', 'video', 'test_user');
|
const success = await archive_api.removeFromArchive('testextractor2', 'testing1', 'video', 'test_user');
|
||||||
assert(success);
|
assert(success);
|
||||||
@@ -827,7 +751,7 @@ describe('Utils', async function() {
|
|||||||
|
|
||||||
describe('Categories', async function() {
|
describe('Categories', async function() {
|
||||||
beforeEach(async function() {
|
beforeEach(async function() {
|
||||||
// await db_api.connectToDB();
|
await db_api.connectToDB();
|
||||||
const new_category = {
|
const new_category = {
|
||||||
name: 'test_category',
|
name: 'test_category',
|
||||||
uid: uuid(),
|
uid: uuid(),
|
||||||
@@ -875,6 +799,7 @@ describe('Categories', async function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const category = await categories_api.categorize([sample_video_json]);
|
const category = await categories_api.categorize([sample_video_json]);
|
||||||
|
console.log(category);
|
||||||
assert(category && category.name === 'test_category');
|
assert(category && category.name === 'test_category');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -927,14 +852,4 @@ describe('Categories', async function() {
|
|||||||
const category = await categories_api.categorize([sample_video_json]);
|
const category = await categories_api.categorize([sample_video_json]);
|
||||||
assert(category);
|
assert(category);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const generateEmptyVideoFile = async (file_path) => {
|
|
||||||
if (fs.existsSync(file_path)) fs.unlinkSync(file_path);
|
|
||||||
return await exec(`ffmpeg -t 1 -f lavfi -i color=c=black:s=640x480 -c:v libx264 -tune stillimage -pix_fmt yuv420p "${file_path}"`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const generateEmptyAudioFile = async (file_path) => {
|
|
||||||
if (fs.existsSync(file_path)) fs.unlinkSync(file_path);
|
|
||||||
return await exec(`ffmpeg -f lavfi -i anullsrc=r=44100:cl=mono -t 1 -q:a 9 -acodec libmp3lame ${file_path}`);
|
|
||||||
}
|
|
||||||
@@ -1,32 +1,13 @@
|
|||||||
const config_api = require('./config');
|
const config_api = require('./config');
|
||||||
const logger = require('./logger');
|
const logger = require('./logger');
|
||||||
const utils = require('./utils');
|
|
||||||
|
|
||||||
const moment = require('moment');
|
const moment = require('moment');
|
||||||
const fs = require('fs-extra')
|
const fs = require('fs-extra')
|
||||||
const axios = require('axios');
|
|
||||||
const { EmoteFetcher } = require('@tzahi12345/twitch-emoticons');
|
|
||||||
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;
|
|
||||||
|
|
||||||
let auth_timeout = null;
|
async function getCommentsForVOD(vodId) {
|
||||||
let cached_oauth = null;
|
|
||||||
|
|
||||||
exports.ensureTwitchAuth = async () => {
|
|
||||||
const TIMEOUT_MARGIN_MS = 60*1000;
|
|
||||||
const twitch_client_id = config_api.getConfigItem('ytdl_twitch_client_id');
|
|
||||||
const twitch_client_secret = config_api.getConfigItem('ytdl_twitch_client_secret');
|
|
||||||
if (cached_oauth && auth_timeout && (Date.now() - TIMEOUT_MARGIN_MS) < auth_timeout) return cached_oauth;
|
|
||||||
|
|
||||||
const {token, expires_in} = await exports.getTwitchOAuthToken(twitch_client_id, twitch_client_secret);
|
|
||||||
cached_oauth = token;
|
|
||||||
auth_timeout = Date.now() + expires_in;
|
|
||||||
return token;
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.getCommentsForVOD = async (vodId) => {
|
|
||||||
const exec = promisify(child_process.exec);
|
const exec = promisify(child_process.exec);
|
||||||
|
|
||||||
// Reject invalid params to prevent command injection attack
|
// Reject invalid params to prevent command injection attack
|
||||||
@@ -39,7 +20,7 @@ exports.getCommentsForVOD = async (vodId) => {
|
|||||||
const cliExt = is_windows ? '.exe' : ''
|
const cliExt = is_windows ? '.exe' : ''
|
||||||
const cliPath = `TwitchDownloaderCLI${cliExt}`
|
const cliPath = `TwitchDownloaderCLI${cliExt}`
|
||||||
|
|
||||||
if (!commandExistsSync(cliPath)) {
|
if (!fs.existsSync(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;
|
||||||
}
|
}
|
||||||
@@ -70,7 +51,7 @@ exports.getCommentsForVOD = async (vodId) => {
|
|||||||
return new_json;
|
return new_json;
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.getTwitchChatByFileID = async (id, type, user_uid, uuid, sub) => {
|
async function getTwitchChatByFileID(id, type, user_uid, uuid, sub) {
|
||||||
const usersFileFolder = config_api.getConfigItem('ytdl_users_base_path');
|
const usersFileFolder = config_api.getConfigItem('ytdl_users_base_path');
|
||||||
const subscriptionsFileFolder = config_api.getConfigItem('ytdl_subscriptions_base_path');
|
const subscriptionsFileFolder = config_api.getConfigItem('ytdl_subscriptions_base_path');
|
||||||
let file_path = null;
|
let file_path = null;
|
||||||
@@ -98,10 +79,10 @@ exports.getTwitchChatByFileID = async (id, type, user_uid, uuid, sub) => {
|
|||||||
return chat_file;
|
return chat_file;
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.downloadTwitchChatByVODID = async (vodId, id, type, user_uid, sub, customFileFolderPath = null) => {
|
async function downloadTwitchChatByVODID(vodId, id, type, user_uid, sub, customFileFolderPath = null) {
|
||||||
const usersFileFolder = config_api.getConfigItem('ytdl_users_base_path');
|
const usersFileFolder = config_api.getConfigItem('ytdl_users_base_path');
|
||||||
const subscriptionsFileFolder = config_api.getConfigItem('ytdl_subscriptions_base_path');
|
const subscriptionsFileFolder = config_api.getConfigItem('ytdl_subscriptions_base_path');
|
||||||
const chat = await exports.getCommentsForVOD(vodId);
|
const chat = await getCommentsForVOD(vodId);
|
||||||
|
|
||||||
// save file if needed params are included
|
// save file if needed params are included
|
||||||
let file_path = null;
|
let file_path = null;
|
||||||
@@ -126,114 +107,6 @@ exports.downloadTwitchChatByVODID = async (vodId, id, type, user_uid, sub, custo
|
|||||||
return chat;
|
return chat;
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.getTwitchEmotes = async (file_uid) => {
|
|
||||||
const emotes_path = path.join('appdata', 'emotes', file_uid, 'emotes.json')
|
|
||||||
if (!fs.existsSync(emotes_path)) return null;
|
|
||||||
const emote_objs = fs.readJSONSync(emotes_path);
|
|
||||||
// inject custom url
|
|
||||||
for (const emote_obj of emote_objs) {
|
|
||||||
emote_obj.custom_url = `${utils.getBaseURL()}/api/emote/${file_uid}/${emote_obj.id}.${emote_obj.ext}`
|
|
||||||
}
|
|
||||||
return emote_objs;
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.downloadTwitchEmotes = async (channel_name, file_uid) => {
|
|
||||||
const twitch_client_id = config_api.getConfigItem('ytdl_twitch_client_id');
|
|
||||||
const twitch_client_secret = config_api.getConfigItem('ytdl_twitch_client_secret');
|
|
||||||
|
|
||||||
const channel_id = await exports.getChannelID(channel_name);
|
|
||||||
|
|
||||||
const fetcher = new EmoteFetcher(twitch_client_id, twitch_client_secret);
|
|
||||||
|
|
||||||
try {
|
|
||||||
await Promise.allSettled([
|
|
||||||
fetcher.fetchTwitchEmotes(),
|
|
||||||
fetcher.fetchTwitchEmotes(channel_id),
|
|
||||||
fetcher.fetchBTTVEmotes(),
|
|
||||||
fetcher.fetchBTTVEmotes(channel_id),
|
|
||||||
fetcher.fetchSevenTVEmotes(),
|
|
||||||
fetcher.fetchSevenTVEmotes(channel_id),
|
|
||||||
fetcher.fetchFFZEmotes(),
|
|
||||||
fetcher.fetchFFZEmotes(channel_id)
|
|
||||||
]);
|
|
||||||
|
|
||||||
const emotes_dir = path.join('appdata', 'emotes', file_uid);
|
|
||||||
const emote_json_path = path.join(emotes_dir, `emotes.json`);
|
|
||||||
fs.ensureDirSync(emotes_dir);
|
|
||||||
|
|
||||||
const emote_objs = [];
|
|
||||||
let failed_emote_count = 0;
|
|
||||||
for (const [, emote] of fetcher.emotes) {
|
|
||||||
const emote_obj = emote.toObject();
|
|
||||||
|
|
||||||
const ext = emote.imageType;
|
|
||||||
const emote_image_path = path.join(emotes_dir, `${emote.id}.${ext}`);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const link = emote.toLink();
|
|
||||||
if (!fs.existsSync(emote_image_path)) await utils.fetchFile(link, emote_image_path);
|
|
||||||
emote_obj['ext'] = ext;
|
|
||||||
emote_objs.push(emote_obj);
|
|
||||||
} catch (err) {
|
|
||||||
failed_emote_count++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (failed_emote_count) logger.warn(`${failed_emote_count} emotes failed to download for channel ${channel_name}`);
|
|
||||||
await fs.writeJSON(emote_json_path, emote_objs);
|
|
||||||
return emote_objs;
|
|
||||||
} catch (err) {
|
|
||||||
logger.error(err);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.getTwitchOAuthToken = async (client_id, client_secret) => {
|
|
||||||
logger.verbose('Generating new Twitch auth token');
|
|
||||||
const url = `https://id.twitch.tv/oauth2/token`;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await axios.post(url, {client_id: client_id, client_secret: client_secret, grant_type: 'client_credentials'});
|
|
||||||
const token = response['data']['access_token'];
|
|
||||||
const expires_in = response['data']['expires_in'];
|
|
||||||
if (token) return {token, expires_in};
|
|
||||||
|
|
||||||
logger.error(`Failed to get token.`);
|
|
||||||
return null;
|
|
||||||
} catch (err) {
|
|
||||||
logger.error(`Failed to get token.`);
|
|
||||||
logger.error(err);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.getChannelID = async (channel_name) => {
|
|
||||||
const twitch_client_id = config_api.getConfigItem('ytdl_twitch_client_id');
|
|
||||||
const token = await exports.ensureTwitchAuth();
|
|
||||||
const url = `https://api.twitch.tv/helix/users?login=${channel_name}`;
|
|
||||||
const headers = {
|
|
||||||
'Client-ID': twitch_client_id,
|
|
||||||
'Authorization': 'Bearer ' + token,
|
|
||||||
// Accept: 'application/vnd.twitchtv.v5+json; charset=UTF-8'
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await axios.get(url, {headers: headers});
|
|
||||||
const data = response.data.data;
|
|
||||||
|
|
||||||
if (data && data.length > 0) {
|
|
||||||
const channelID = data[0].id;
|
|
||||||
return channelID;
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.error(`Failed to get channel ID for user ${channel_name}`);
|
|
||||||
if (data.error) logger.error(data.error);
|
|
||||||
return null; // User not found
|
|
||||||
} catch (err) {
|
|
||||||
logger.error(`Failed to get channel ID for user ${channel_name}`);
|
|
||||||
logger.error(err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const convertTimestamp = (timestamp) => moment.duration(timestamp, 'seconds')
|
const convertTimestamp = (timestamp) => moment.duration(timestamp, 'seconds')
|
||||||
.toISOString()
|
.toISOString()
|
||||||
.replace(/P.*?T(?:(\d+?)H)?(?:(\d+?)M)?(?:(\d+).*?S)?/,
|
.replace(/P.*?T(?:(\d+?)H)?(?:(\d+?)M)?(?:(\d+).*?S)?/,
|
||||||
@@ -241,3 +114,9 @@ const convertTimestamp = (timestamp) => moment.duration(timestamp, 'seconds')
|
|||||||
const seg = v => v ? v.padStart(2, '0') : '00';
|
const seg = v => v ? v.padStart(2, '0') : '00';
|
||||||
return `${seg(ms[0])}:${seg(ms[1])}:${seg(ms[2])}`;
|
return `${seg(ms[0])}:${seg(ms[1])}:${seg(ms[2])}`;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
getCommentsForVOD: getCommentsForVOD,
|
||||||
|
getTwitchChatByFileID: getTwitchChatByFileID,
|
||||||
|
downloadTwitchChatByVODID: downloadTwitchChatByVODID
|
||||||
|
}
|
||||||
@@ -13,7 +13,7 @@ const CONSTS = require('./consts');
|
|||||||
const is_windows = process.platform === 'win32';
|
const is_windows = process.platform === 'win32';
|
||||||
|
|
||||||
// replaces .webm with appropriate extension
|
// replaces .webm with appropriate extension
|
||||||
exports.getTrueFileName = (unfixed_path, type, force_ext = null) => {
|
exports.getTrueFileName = (unfixed_path, type) => {
|
||||||
let fixed_path = unfixed_path;
|
let fixed_path = unfixed_path;
|
||||||
|
|
||||||
const new_ext = (type === 'audio' ? 'mp3' : 'mp4');
|
const new_ext = (type === 'audio' ? 'mp3' : 'mp4');
|
||||||
@@ -22,7 +22,7 @@ exports.getTrueFileName = (unfixed_path, type, force_ext = null) => {
|
|||||||
|
|
||||||
|
|
||||||
if (old_ext !== new_ext) {
|
if (old_ext !== new_ext) {
|
||||||
unfixed_parts[unfixed_parts.length-1] = force_ext || new_ext;
|
unfixed_parts[unfixed_parts.length-1] = new_ext;
|
||||||
fixed_path = unfixed_parts.join('.');
|
fixed_path = unfixed_parts.join('.');
|
||||||
}
|
}
|
||||||
return fixed_path;
|
return fixed_path;
|
||||||
@@ -347,7 +347,7 @@ exports.checkExistsWithTimeout = async (filePath, timeout) => {
|
|||||||
if (!err) {
|
if (!err) {
|
||||||
clearTimeout(timer);
|
clearTimeout(timer);
|
||||||
if (watcher) watcher.close();
|
if (watcher) watcher.close();
|
||||||
resolve(true);
|
resolve();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -357,36 +357,33 @@ exports.checkExistsWithTimeout = async (filePath, timeout) => {
|
|||||||
if (eventType === 'rename' && filename === basename) {
|
if (eventType === 'rename' && filename === basename) {
|
||||||
clearTimeout(timer);
|
clearTimeout(timer);
|
||||||
if (watcher) watcher.close();
|
if (watcher) watcher.close();
|
||||||
resolve(true);
|
resolve();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// helper function to download file using fetch
|
// helper function to download file using fetch
|
||||||
exports.fetchFile = async (url, output_path, file_label = null) => {
|
exports.fetchFile = async (url, path, file_label) => {
|
||||||
var len = null;
|
var len = null;
|
||||||
const res = await fetch(url);
|
const res = await fetch(url);
|
||||||
let bar = null;
|
|
||||||
if (file_label) {
|
|
||||||
len = parseInt(res.headers.get("Content-Length"), 10);
|
|
||||||
|
|
||||||
bar = new ProgressBar(` Downloading ${file_label} [:bar] :percent :etas`, {
|
len = parseInt(res.headers.get("Content-Length"), 10);
|
||||||
complete: '=',
|
|
||||||
incomplete: ' ',
|
|
||||||
width: 20,
|
|
||||||
total: len
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const fileStream = fs.createWriteStream(output_path);
|
var bar = new ProgressBar(` Downloading ${file_label} [:bar] :percent :etas`, {
|
||||||
|
complete: '=',
|
||||||
|
incomplete: ' ',
|
||||||
|
width: 20,
|
||||||
|
total: len
|
||||||
|
});
|
||||||
|
const fileStream = fs.createWriteStream(path);
|
||||||
await new Promise((resolve, reject) => {
|
await new Promise((resolve, reject) => {
|
||||||
res.body.pipe(fileStream);
|
res.body.pipe(fileStream);
|
||||||
res.body.on("error", (err) => {
|
res.body.on("error", (err) => {
|
||||||
reject(err);
|
reject(err);
|
||||||
});
|
});
|
||||||
res.body.on('data', function (chunk) {
|
res.body.on('data', function (chunk) {
|
||||||
if (file_label) bar.tick(chunk.length);
|
bar.tick(chunk.length);
|
||||||
});
|
});
|
||||||
fileStream.on("finish", function() {
|
fileStream.on("finish", function() {
|
||||||
resolve();
|
resolve();
|
||||||
@@ -522,51 +519,6 @@ exports.convertFlatObjectToNestedObject = (obj) => {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.getDirectoriesInDirectory = async (basePath) => {
|
|
||||||
try {
|
|
||||||
const files = await fs.readdir(basePath, { withFileTypes: true });
|
|
||||||
return files
|
|
||||||
.filter((file) => file.isDirectory())
|
|
||||||
.map((file) => path.join(basePath, file.name));
|
|
||||||
} catch (err) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.parseOutputJSON = (output, err) => {
|
|
||||||
let split_output = [];
|
|
||||||
// const output_jsons = [];
|
|
||||||
if (err && !output) {
|
|
||||||
if (!err.stderr.includes('This video is unavailable') && !err.stderr.includes('Private video')) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
logger.info('An error was encountered with at least one video, backup method will be used.')
|
|
||||||
try {
|
|
||||||
split_output = err.stdout.split(/\r\n|\r|\n/);
|
|
||||||
} catch (e) {
|
|
||||||
logger.error('Backup method failed. See error below:');
|
|
||||||
logger.error(e);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
} else if (output.length === 0 || (output.length === 1 && output[0].length === 0)) {
|
|
||||||
// output is '' or ['']
|
|
||||||
return [];
|
|
||||||
} else {
|
|
||||||
for (const output_item of output) {
|
|
||||||
// we have to do this because sometimes there will be leading characters before the actual json
|
|
||||||
const start_idx = output_item.indexOf('{"');
|
|
||||||
const clean_output = output_item.slice(start_idx, output_item.length);
|
|
||||||
split_output.push(clean_output);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
return split_output.map(split_output_str => JSON.parse(split_output_str));
|
|
||||||
} catch(e) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// objects
|
// objects
|
||||||
|
|
||||||
function File(id, title, thumbnailURL, isAudio, duration, url, uploader, size, path, upload_date, description, view_count, height, abr) {
|
function File(id, title, thumbnailURL, isAudio, duration, url, uploader, size, path, upload_date, description, view_count, height, abr) {
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ const logger = require('./logger');
|
|||||||
const utils = require('./utils');
|
const utils = require('./utils');
|
||||||
const CONSTS = require('./consts');
|
const CONSTS = require('./consts');
|
||||||
const config_api = require('./config.js');
|
const config_api = require('./config.js');
|
||||||
const youtubedl = require('youtube-dl');
|
|
||||||
|
|
||||||
const OUTDATED_VERSION = "2020.00.00";
|
const OUTDATED_VERSION = "2020.00.00";
|
||||||
|
|
||||||
@@ -26,15 +25,6 @@ const download_sources = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.runYoutubeDL = async (url, args, downloadMethod = youtubedl.exec) => {
|
|
||||||
return new Promise(resolve => {
|
|
||||||
downloadMethod(url, args, {maxBuffer: Infinity}, async function(err, output) {
|
|
||||||
const parsed_output = utils.parseOutputJSON(output, err);
|
|
||||||
resolve({parsed_output, err});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.checkForYoutubeDLUpdate = async () => {
|
exports.checkForYoutubeDLUpdate = async () => {
|
||||||
return new Promise(async resolve => {
|
return new Promise(async resolve => {
|
||||||
const default_downloader = config_api.getConfigItem('ytdl_default_downloader');
|
const default_downloader = config_api.getConfigItem('ytdl_default_downloader');
|
||||||
|
|||||||
@@ -21,4 +21,4 @@ version: 0.1.0
|
|||||||
# incremented each time you make changes to the application. Versions are not expected to
|
# incremented each time you make changes to the application. Versions are not expected to
|
||||||
# follow Semantic Versioning. They should reflect the version the application is using.
|
# follow Semantic Versioning. They should reflect the version the application is using.
|
||||||
# It is recommended to use it with quotes.
|
# It is recommended to use it with quotes.
|
||||||
appVersion: "4.3.2"
|
appVersion: "4.3.1"
|
||||||
|
|||||||
@@ -26,7 +26,8 @@ def getZipName():
|
|||||||
|
|
||||||
def getLatestFileInRepo(repo, search_string):
|
def getLatestFileInRepo(repo, search_string):
|
||||||
# Create an unauthenticated instance of the Github object
|
# Create an unauthenticated instance of the Github object
|
||||||
g = Github(os.environ.get('GH_TOKEN'))
|
gh_token = os.environ.get('GH_TOKEN')
|
||||||
|
g = Github(gh_token if gh_token else None) # ensure it's none if it's falsy
|
||||||
|
|
||||||
# Replace with the repository owner and name
|
# Replace with the repository owner and name
|
||||||
repo = g.get_repo(repo)
|
repo = g.get_repo(repo)
|
||||||
|
|||||||
@@ -1,39 +0,0 @@
|
|||||||
#!/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 5 \
|
||||||
--retry-delay 0 \
|
--retry-delay 0 \
|
||||||
--retry-max-time 40 \
|
--retry-max-time 40 \
|
||||||
"https://johnvansickle.com/ffmpeg/old-releases/ffmpeg-5.1.1-${ARCH}-static.tar.xz"
|
"https://johnvansickle.com/ffmpeg/builds/ffmpeg-git-${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"
|
||||||
|
|||||||
24081
package-lock.json
generated
24081
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
41
package.json
41
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "youtube-dl-material",
|
"name": "youtube-dl-material",
|
||||||
"version": "4.3.2",
|
"version": "4.3.1",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"ng": "ng",
|
"ng": "ng",
|
||||||
@@ -21,21 +21,20 @@
|
|||||||
},
|
},
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@angular-devkit/core": "^16.0.3",
|
"@angular-devkit/core": "^15.0.1",
|
||||||
"@angular/animations": "^16.0.3",
|
"@angular/animations": "^15.0.1",
|
||||||
"@angular/cdk": "^16.0.2",
|
"@angular/cdk": "^15.0.0",
|
||||||
"@angular/common": "^16.0.3",
|
"@angular/common": "^15.0.1",
|
||||||
"@angular/compiler": "^16.0.3",
|
"@angular/compiler": "^15.0.1",
|
||||||
"@angular/core": "^16.0.3",
|
"@angular/core": "^15.0.1",
|
||||||
"@angular/forms": "^16.0.3",
|
"@angular/forms": "^15.0.1",
|
||||||
"@angular/localize": "^16.0.3",
|
"@angular/localize": "^15.0.1",
|
||||||
"@angular/material": "^16.0.2",
|
"@angular/material": "^15.0.0",
|
||||||
"@angular/platform-browser": "^16.0.3",
|
"@angular/platform-browser": "^15.0.1",
|
||||||
"@angular/platform-browser-dynamic": "^16.0.3",
|
"@angular/platform-browser-dynamic": "^15.0.1",
|
||||||
"@angular/router": "^16.0.3",
|
"@angular/router": "^15.0.1",
|
||||||
"@fontsource/material-icons": "^4.5.4",
|
"@fontsource/material-icons": "^4.5.4",
|
||||||
"@ngneat/content-loader": "^7.0.0",
|
"@ngneat/content-loader": "^7.0.0",
|
||||||
"@tzahi12345/twitch-emoticons": "^1.0.9",
|
|
||||||
"@videogular/ngx-videogular": "^6.0.0",
|
"@videogular/ngx-videogular": "^6.0.0",
|
||||||
"core-js": "^2.4.1",
|
"core-js": "^2.4.1",
|
||||||
"crypto-js": "^4.1.1",
|
"crypto-js": "^4.1.1",
|
||||||
@@ -44,20 +43,20 @@
|
|||||||
"fs-extra": "^10.0.0",
|
"fs-extra": "^10.0.0",
|
||||||
"material-icons": "^1.10.8",
|
"material-icons": "^1.10.8",
|
||||||
"nan": "^2.14.1",
|
"nan": "^2.14.1",
|
||||||
"ngx-avatars": "^1.6.1",
|
"ngx-avatars": "^1.4.1",
|
||||||
"ngx-file-drop": "^15.0.0",
|
"ngx-file-drop": "^15.0.0",
|
||||||
"rxjs": "^6.6.3",
|
"rxjs": "^6.6.3",
|
||||||
"rxjs-compat": "^6.6.7",
|
"rxjs-compat": "^6.6.7",
|
||||||
"tslib": "^2.0.0",
|
"tslib": "^2.0.0",
|
||||||
"typescript": "~5.0.4",
|
"typescript": "~4.8.4",
|
||||||
"xliff-to-json": "^1.0.4",
|
"xliff-to-json": "^1.0.4",
|
||||||
"zone.js": "~0.13.0"
|
"zone.js": "~0.11.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@angular-devkit/build-angular": "^16.0.3",
|
"@angular-devkit/build-angular": "^15.0.1",
|
||||||
"@angular/cli": "^16.0.3",
|
"@angular/cli": "^15.0.1",
|
||||||
"@angular/compiler-cli": "^16.0.3",
|
"@angular/compiler-cli": "^15.0.1",
|
||||||
"@angular/language-service": "^16.0.3",
|
"@angular/language-service": "^15.0.1",
|
||||||
"@types/core-js": "^2.5.2",
|
"@types/core-js": "^2.5.2",
|
||||||
"@types/file-saver": "^2.0.1",
|
"@types/file-saver": "^2.0.1",
|
||||||
"@types/jasmine": "^4.3.1",
|
"@types/jasmine": "^4.3.1",
|
||||||
|
|||||||
@@ -104,7 +104,6 @@ export type { SubscriptionRequestData } from './models/SubscriptionRequestData';
|
|||||||
export type { SuccessObject } from './models/SuccessObject';
|
export type { SuccessObject } from './models/SuccessObject';
|
||||||
export type { TableInfo } from './models/TableInfo';
|
export type { TableInfo } from './models/TableInfo';
|
||||||
export type { Task } from './models/Task';
|
export type { Task } from './models/Task';
|
||||||
export { TaskType } from './models/TaskType';
|
|
||||||
export type { TestConnectionStringRequest } from './models/TestConnectionStringRequest';
|
export type { TestConnectionStringRequest } from './models/TestConnectionStringRequest';
|
||||||
export type { TestConnectionStringResponse } from './models/TestConnectionStringResponse';
|
export type { TestConnectionStringResponse } from './models/TestConnectionStringResponse';
|
||||||
export type { TransferDBRequest } from './models/TransferDBRequest';
|
export type { TransferDBRequest } from './models/TransferDBRequest';
|
||||||
|
|||||||
@@ -5,4 +5,4 @@
|
|||||||
export type AddFileToPlaylistRequest = {
|
export type AddFileToPlaylistRequest = {
|
||||||
file_uid: string;
|
file_uid: string;
|
||||||
playlist_id: string;
|
playlist_id: string;
|
||||||
};
|
};
|
||||||
@@ -13,4 +13,4 @@ export type Archive = {
|
|||||||
sub_id?: string;
|
sub_id?: string;
|
||||||
timestamp: number;
|
timestamp: number;
|
||||||
uid: string;
|
uid: string;
|
||||||
};
|
};
|
||||||
@@ -8,4 +8,4 @@ import type { YesNo } from './YesNo';
|
|||||||
export type BaseChangePermissionsRequest = {
|
export type BaseChangePermissionsRequest = {
|
||||||
permission: UserPermission;
|
permission: UserPermission;
|
||||||
new_value: YesNo;
|
new_value: YesNo;
|
||||||
};
|
};
|
||||||
@@ -12,4 +12,4 @@ export type Category = {
|
|||||||
* Overrides file output for downloaded files in category
|
* Overrides file output for downloaded files in category
|
||||||
*/
|
*/
|
||||||
custom_output?: string;
|
custom_output?: string;
|
||||||
};
|
};
|
||||||
@@ -22,4 +22,4 @@ export namespace CategoryRule {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -6,4 +6,4 @@ import type { BaseChangePermissionsRequest } from './BaseChangePermissionsReques
|
|||||||
|
|
||||||
export type ChangeRolePermissionsRequest = (BaseChangePermissionsRequest & {
|
export type ChangeRolePermissionsRequest = (BaseChangePermissionsRequest & {
|
||||||
role: string;
|
role: string;
|
||||||
});
|
});
|
||||||
@@ -6,4 +6,4 @@ import type { BaseChangePermissionsRequest } from './BaseChangePermissionsReques
|
|||||||
|
|
||||||
export type ChangeUserPermissionsRequest = (BaseChangePermissionsRequest & {
|
export type ChangeUserPermissionsRequest = (BaseChangePermissionsRequest & {
|
||||||
user_uid: string;
|
user_uid: string;
|
||||||
});
|
});
|
||||||
@@ -7,4 +7,4 @@ export type CheckConcurrentStreamRequest = {
|
|||||||
* UID of the concurrent stream
|
* UID of the concurrent stream
|
||||||
*/
|
*/
|
||||||
uid: string;
|
uid: string;
|
||||||
};
|
};
|
||||||
@@ -6,4 +6,4 @@ import type { ConcurrentStream } from './ConcurrentStream';
|
|||||||
|
|
||||||
export type CheckConcurrentStreamResponse = {
|
export type CheckConcurrentStreamResponse = {
|
||||||
stream: ConcurrentStream;
|
stream: ConcurrentStream;
|
||||||
};
|
};
|
||||||
@@ -6,4 +6,4 @@ export type ClearDownloadsRequest = {
|
|||||||
clear_finished?: boolean;
|
clear_finished?: boolean;
|
||||||
clear_paused?: boolean;
|
clear_paused?: boolean;
|
||||||
clear_errors?: boolean;
|
clear_errors?: boolean;
|
||||||
};
|
};
|
||||||
@@ -6,4 +6,4 @@ export type ConcurrentStream = {
|
|||||||
playback_timestamp?: number;
|
playback_timestamp?: number;
|
||||||
unix_timestamp?: number;
|
unix_timestamp?: number;
|
||||||
playing?: boolean;
|
playing?: boolean;
|
||||||
};
|
};
|
||||||
@@ -4,4 +4,4 @@
|
|||||||
|
|
||||||
export type Config = {
|
export type Config = {
|
||||||
YoutubeDLMaterial: any;
|
YoutubeDLMaterial: any;
|
||||||
};
|
};
|
||||||
@@ -7,4 +7,4 @@ import type { Config } from './Config';
|
|||||||
export type ConfigResponse = {
|
export type ConfigResponse = {
|
||||||
config_file: Config;
|
config_file: Config;
|
||||||
success: boolean;
|
success: boolean;
|
||||||
};
|
};
|
||||||
@@ -4,4 +4,4 @@
|
|||||||
|
|
||||||
export type CreateCategoryRequest = {
|
export type CreateCategoryRequest = {
|
||||||
name: string;
|
name: string;
|
||||||
};
|
};
|
||||||
@@ -7,4 +7,4 @@ import type { Category } from './Category';
|
|||||||
export type CreateCategoryResponse = {
|
export type CreateCategoryResponse = {
|
||||||
new_category?: Category;
|
new_category?: Category;
|
||||||
success?: boolean;
|
success?: boolean;
|
||||||
};
|
};
|
||||||
@@ -6,4 +6,4 @@ export type CreatePlaylistRequest = {
|
|||||||
playlistName: string;
|
playlistName: string;
|
||||||
uids: Array<string>;
|
uids: Array<string>;
|
||||||
thumbnailURL: string;
|
thumbnailURL: string;
|
||||||
};
|
};
|
||||||
@@ -7,4 +7,4 @@ import type { Playlist } from './Playlist';
|
|||||||
export type CreatePlaylistResponse = {
|
export type CreatePlaylistResponse = {
|
||||||
new_playlist: Playlist;
|
new_playlist: Playlist;
|
||||||
success: boolean;
|
success: boolean;
|
||||||
};
|
};
|
||||||
@@ -5,4 +5,4 @@
|
|||||||
export type CropFileSettings = {
|
export type CropFileSettings = {
|
||||||
cropFileStart: number;
|
cropFileStart: number;
|
||||||
cropFileEnd: number;
|
cropFileEnd: number;
|
||||||
};
|
};
|
||||||
@@ -17,4 +17,4 @@ export namespace DBBackup {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -16,4 +16,4 @@ roles?: TableInfo;
|
|||||||
download_queue?: TableInfo;
|
download_queue?: TableInfo;
|
||||||
archives?: TableInfo;
|
archives?: TableInfo;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -42,4 +42,4 @@ export type DatabaseFile = {
|
|||||||
*/
|
*/
|
||||||
abr?: number;
|
abr?: number;
|
||||||
favorite: boolean;
|
favorite: boolean;
|
||||||
};
|
};
|
||||||
@@ -11,4 +11,4 @@ export type DeleteAllFilesResponse = {
|
|||||||
* Number of files removed
|
* Number of files removed
|
||||||
*/
|
*/
|
||||||
delete_count?: number;
|
delete_count?: number;
|
||||||
};
|
};
|
||||||
@@ -6,4 +6,4 @@ import type { Archive } from './Archive';
|
|||||||
|
|
||||||
export type DeleteArchiveItemsRequest = {
|
export type DeleteArchiveItemsRequest = {
|
||||||
archives: Array<Archive>;
|
archives: Array<Archive>;
|
||||||
};
|
};
|
||||||
@@ -4,4 +4,4 @@
|
|||||||
|
|
||||||
export type DeleteCategoryRequest = {
|
export type DeleteCategoryRequest = {
|
||||||
category_uid: string;
|
category_uid: string;
|
||||||
};
|
};
|
||||||
@@ -5,4 +5,4 @@
|
|||||||
export type DeleteMp3Mp4Request = {
|
export type DeleteMp3Mp4Request = {
|
||||||
uid: string;
|
uid: string;
|
||||||
blacklistMode?: boolean;
|
blacklistMode?: boolean;
|
||||||
};
|
};
|
||||||
@@ -4,4 +4,4 @@
|
|||||||
|
|
||||||
export type DeleteNotificationRequest = {
|
export type DeleteNotificationRequest = {
|
||||||
uid: string;
|
uid: string;
|
||||||
};
|
};
|
||||||
@@ -4,4 +4,4 @@
|
|||||||
|
|
||||||
export type DeletePlaylistRequest = {
|
export type DeletePlaylistRequest = {
|
||||||
playlist_id: string;
|
playlist_id: string;
|
||||||
};
|
};
|
||||||
@@ -8,4 +8,4 @@ export type DeleteSubscriptionFileRequest = {
|
|||||||
* If true, does not remove id from archive. Only valid if youtube-dl archive is enabled in settings.
|
* If true, does not remove id from archive. Only valid if youtube-dl archive is enabled in settings.
|
||||||
*/
|
*/
|
||||||
deleteForever?: boolean;
|
deleteForever?: boolean;
|
||||||
};
|
};
|
||||||
@@ -4,4 +4,4 @@
|
|||||||
|
|
||||||
export type DeleteUserRequest = {
|
export type DeleteUserRequest = {
|
||||||
uid: string;
|
uid: string;
|
||||||
};
|
};
|
||||||
@@ -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;
|
||||||
};
|
};
|
||||||
@@ -7,4 +7,4 @@ import type { FileType } from './FileType';
|
|||||||
export type DownloadArchiveRequest = {
|
export type DownloadArchiveRequest = {
|
||||||
type?: FileType;
|
type?: FileType;
|
||||||
sub_id?: string;
|
sub_id?: string;
|
||||||
};
|
};
|
||||||
@@ -11,4 +11,4 @@ export type DownloadFileRequest = {
|
|||||||
playlist_id?: string;
|
playlist_id?: string;
|
||||||
url?: string;
|
url?: string;
|
||||||
type?: FileType;
|
type?: FileType;
|
||||||
};
|
};
|
||||||
@@ -49,4 +49,4 @@ export type DownloadRequest = {
|
|||||||
* If using youtube-dl archive, download will ignore it
|
* If using youtube-dl archive, download will ignore it
|
||||||
*/
|
*/
|
||||||
ignoreArchive?: boolean;
|
ignoreArchive?: boolean;
|
||||||
};
|
};
|
||||||
@@ -6,4 +6,4 @@ import type { Download } from './Download';
|
|||||||
|
|
||||||
export type DownloadResponse = {
|
export type DownloadResponse = {
|
||||||
download?: Download;
|
download?: Download;
|
||||||
};
|
};
|
||||||
@@ -20,4 +20,4 @@ export type DownloadTwitchChatByVODIDRequest = {
|
|||||||
*/
|
*/
|
||||||
uuid?: string;
|
uuid?: string;
|
||||||
sub?: Subscription;
|
sub?: Subscription;
|
||||||
};
|
};
|
||||||
@@ -6,4 +6,4 @@ import type { TwitchChatMessage } from './TwitchChatMessage';
|
|||||||
|
|
||||||
export type DownloadTwitchChatByVODIDResponse = {
|
export type DownloadTwitchChatByVODIDResponse = {
|
||||||
chat: Array<TwitchChatMessage>;
|
chat: Array<TwitchChatMessage>;
|
||||||
};
|
};
|
||||||
@@ -4,4 +4,4 @@
|
|||||||
|
|
||||||
export type DownloadVideosForSubscriptionRequest = {
|
export type DownloadVideosForSubscriptionRequest = {
|
||||||
subID: string;
|
subID: string;
|
||||||
};
|
};
|
||||||
@@ -5,4 +5,4 @@
|
|||||||
export enum FileType {
|
export enum FileType {
|
||||||
AUDIO = 'audio',
|
AUDIO = 'audio',
|
||||||
VIDEO = 'video',
|
VIDEO = 'video',
|
||||||
}
|
}
|
||||||
@@ -6,4 +6,4 @@ export enum FileTypeFilter {
|
|||||||
AUDIO_ONLY = 'audio_only',
|
AUDIO_ONLY = 'audio_only',
|
||||||
VIDEO_ONLY = 'video_only',
|
VIDEO_ONLY = 'video_only',
|
||||||
BOTH = 'both',
|
BOTH = 'both',
|
||||||
}
|
}
|
||||||
@@ -4,4 +4,4 @@
|
|||||||
|
|
||||||
export type GenerateArgsResponse = {
|
export type GenerateArgsResponse = {
|
||||||
args?: Array<string>;
|
args?: Array<string>;
|
||||||
};
|
};
|
||||||
@@ -4,4 +4,4 @@
|
|||||||
|
|
||||||
export type GenerateNewApiKeyResponse = {
|
export type GenerateNewApiKeyResponse = {
|
||||||
new_api_key: string;
|
new_api_key: string;
|
||||||
};
|
};
|
||||||
@@ -6,4 +6,4 @@ import type { Category } from './Category';
|
|||||||
|
|
||||||
export type GetAllCategoriesResponse = {
|
export type GetAllCategoriesResponse = {
|
||||||
categories: Array<Category>;
|
categories: Array<Category>;
|
||||||
};
|
};
|
||||||
@@ -7,4 +7,4 @@ export type GetAllDownloadsRequest = {
|
|||||||
* Filters downloads with the array
|
* Filters downloads with the array
|
||||||
*/
|
*/
|
||||||
uids?: Array<string> | null;
|
uids?: Array<string> | null;
|
||||||
};
|
};
|
||||||
@@ -6,4 +6,4 @@ import type { Download } from './Download';
|
|||||||
|
|
||||||
export type GetAllDownloadsResponse = {
|
export type GetAllDownloadsResponse = {
|
||||||
downloads?: Array<Download>;
|
downloads?: Array<Download>;
|
||||||
};
|
};
|
||||||
@@ -21,4 +21,4 @@ export type GetAllFilesRequest = {
|
|||||||
* Include if you want to filter by subscription
|
* Include if you want to filter by subscription
|
||||||
*/
|
*/
|
||||||
sub_id?: string;
|
sub_id?: string;
|
||||||
};
|
};
|
||||||
@@ -11,4 +11,4 @@ export type GetAllFilesResponse = {
|
|||||||
* All video playlists
|
* All video playlists
|
||||||
*/
|
*/
|
||||||
playlists: Array<Playlist>;
|
playlists: Array<Playlist>;
|
||||||
};
|
};
|
||||||
@@ -6,4 +6,4 @@ import type { Subscription } from './Subscription';
|
|||||||
|
|
||||||
export type GetAllSubscriptionsResponse = {
|
export type GetAllSubscriptionsResponse = {
|
||||||
subscriptions: Array<Subscription>;
|
subscriptions: Array<Subscription>;
|
||||||
};
|
};
|
||||||
@@ -6,4 +6,4 @@ import type { Task } from './Task';
|
|||||||
|
|
||||||
export type GetAllTasksResponse = {
|
export type GetAllTasksResponse = {
|
||||||
tasks?: Array<Task>;
|
tasks?: Array<Task>;
|
||||||
};
|
};
|
||||||
@@ -7,4 +7,4 @@ import type { FileType } from './FileType';
|
|||||||
export type GetArchivesRequest = {
|
export type GetArchivesRequest = {
|
||||||
type?: FileType;
|
type?: FileType;
|
||||||
sub_id?: string;
|
sub_id?: string;
|
||||||
};
|
};
|
||||||
@@ -6,4 +6,4 @@ import type { Archive } from './Archive';
|
|||||||
|
|
||||||
export type GetArchivesResponse = {
|
export type GetArchivesResponse = {
|
||||||
archives: Array<Archive>;
|
archives: Array<Archive>;
|
||||||
};
|
};
|
||||||
@@ -6,4 +6,4 @@ import type { DBBackup } from './DBBackup';
|
|||||||
|
|
||||||
export type GetDBBackupsResponse = {
|
export type GetDBBackupsResponse = {
|
||||||
tasks?: Array<DBBackup>;
|
tasks?: Array<DBBackup>;
|
||||||
};
|
};
|
||||||
@@ -4,4 +4,4 @@
|
|||||||
|
|
||||||
export type GetDownloadRequest = {
|
export type GetDownloadRequest = {
|
||||||
download_uid: string;
|
download_uid: string;
|
||||||
};
|
};
|
||||||
@@ -6,4 +6,4 @@ import type { Download } from './Download';
|
|||||||
|
|
||||||
export type GetDownloadResponse = {
|
export type GetDownloadResponse = {
|
||||||
download?: Download;
|
download?: Download;
|
||||||
};
|
};
|
||||||
@@ -4,4 +4,4 @@
|
|||||||
|
|
||||||
export type GetFileFormatsRequest = {
|
export type GetFileFormatsRequest = {
|
||||||
url?: string;
|
url?: string;
|
||||||
};
|
};
|
||||||
@@ -7,4 +7,4 @@ export type GetFileFormatsResponse = {
|
|||||||
result: {
|
result: {
|
||||||
formats?: Array<any>;
|
formats?: Array<any>;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -14,4 +14,4 @@ export type GetFileRequest = {
|
|||||||
* User UID
|
* User UID
|
||||||
*/
|
*/
|
||||||
uuid?: string;
|
uuid?: string;
|
||||||
};
|
};
|
||||||
@@ -7,4 +7,4 @@ import type { DatabaseFile } from './DatabaseFile';
|
|||||||
export type GetFileResponse = {
|
export type GetFileResponse = {
|
||||||
success: boolean;
|
success: boolean;
|
||||||
file?: DatabaseFile;
|
file?: DatabaseFile;
|
||||||
};
|
};
|
||||||
@@ -16,4 +16,4 @@ export type GetFullTwitchChatRequest = {
|
|||||||
*/
|
*/
|
||||||
uuid?: string;
|
uuid?: string;
|
||||||
sub?: Subscription;
|
sub?: Subscription;
|
||||||
};
|
};
|
||||||
@@ -5,4 +5,4 @@
|
|||||||
export type GetFullTwitchChatResponse = {
|
export type GetFullTwitchChatResponse = {
|
||||||
success: boolean;
|
success: boolean;
|
||||||
error?: string;
|
error?: string;
|
||||||
};
|
};
|
||||||
@@ -4,4 +4,4 @@
|
|||||||
|
|
||||||
export type GetLogsRequest = {
|
export type GetLogsRequest = {
|
||||||
lines?: number;
|
lines?: number;
|
||||||
};
|
};
|
||||||
@@ -8,4 +8,4 @@ export type GetLogsResponse = {
|
|||||||
*/
|
*/
|
||||||
logs?: string;
|
logs?: string;
|
||||||
success?: boolean;
|
success?: boolean;
|
||||||
};
|
};
|
||||||
@@ -11,4 +11,4 @@ export type GetMp3sResponse = {
|
|||||||
* All audio playlists
|
* All audio playlists
|
||||||
*/
|
*/
|
||||||
playlists: Array<Playlist>;
|
playlists: Array<Playlist>;
|
||||||
};
|
};
|
||||||
@@ -11,4 +11,4 @@ export type GetMp4sResponse = {
|
|||||||
* All video playlists
|
* All video playlists
|
||||||
*/
|
*/
|
||||||
playlists: Array<Playlist>;
|
playlists: Array<Playlist>;
|
||||||
};
|
};
|
||||||
@@ -6,4 +6,4 @@ import type { Notification } from './Notification';
|
|||||||
|
|
||||||
export type GetNotificationsResponse = {
|
export type GetNotificationsResponse = {
|
||||||
notifications?: Array<Notification>;
|
notifications?: Array<Notification>;
|
||||||
};
|
};
|
||||||
@@ -9,4 +9,4 @@ export type GetPlaylistRequest = {
|
|||||||
type?: FileType;
|
type?: FileType;
|
||||||
uuid?: string;
|
uuid?: string;
|
||||||
include_file_metadata?: boolean;
|
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 objects for every uid in the playlist's uids property, in the same order
|
||||||
*/
|
*/
|
||||||
file_objs?: Array<DatabaseFile>;
|
file_objs?: Array<DatabaseFile>;
|
||||||
};
|
};
|
||||||
@@ -4,4 +4,4 @@
|
|||||||
|
|
||||||
export type GetPlaylistsRequest = {
|
export type GetPlaylistsRequest = {
|
||||||
include_categories?: boolean;
|
include_categories?: boolean;
|
||||||
};
|
};
|
||||||
@@ -6,4 +6,4 @@ import type { Playlist } from './Playlist';
|
|||||||
|
|
||||||
export type GetPlaylistsResponse = {
|
export type GetPlaylistsResponse = {
|
||||||
playlists: Array<Playlist>;
|
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