Compare commits

..

6 Commits

Author SHA1 Message Date
Isaac Abadi
ab5cd409bb Attempt to reduce docker image size 2022-05-05 03:04:30 -04:00
Isaac Abadi
e0509e8091 Merge branch 'master' of https://github.com/Tzahi12345/YoutubeDL-Material into docker-ubuntu 2022-05-05 03:03:44 -04:00
Isaac Abadi
0c1568b38d Renamed postbuild.mjs to postbuild.js 2022-04-30 23:30:56 -04:00
Isaac Abadi
f8a0d14968 Updated Dockerfile to support ubuntu 2022-04-30 17:17:07 -04:00
Isaac Abadi
f5894e6bc0 apk add -> apt-get 2022-04-30 13:50:41 -04:00
Isaac Abadi
f205f8e58e Switched from alpine to ubuntu 2022-04-30 13:38:26 -04:00
225 changed files with 4744 additions and 16524 deletions

View File

@@ -1,18 +0,0 @@
version: 2
updates:
- package-ecosystem: "docker"
directory: "/"
schedule:
interval: "daily"
- package-ecosystem: "github-actions"
directory: "/.github/workflows"
schedule:
interval: "daily"
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "daily"
- package-ecosystem: "npm"
directory: "/backend/"
schedule:
interval: "daily"

View File

@@ -1,38 +0,0 @@
name: No Response
# Both `issue_comment` and `scheduled` event types are required for this Action
# to work properly.
on:
issue_comment:
types: [created]
schedule:
# Schedule for five minutes after the hour, every hour
- cron: '5 * * * *'
# By specifying the access of one of the scopes, all of those that are not
# specified are set to 'none'.
permissions:
issues: write
jobs:
noResponse:
runs-on: ubuntu-latest
if: ${{ github.repository == 'Tzahi12345/YoutubeDL-Material' }}
steps:
- uses: lee-dohm/no-response@v0.5.0
with:
token: ${{ github.token }}
# Comment to post when closing an Issue for lack of response. Set to `false` to disable
closeComment: >
This issue has been automatically closed because there has been no response
to our request for more information from the original author. With only the
information that is currently in the issue, we don't have enough information
to take action. Please reach out if you have or find the answers we need so
that we can investigate further. We will re-open this issue if you provide us
with the requested information with a comment under this issue.
Thank you for your understanding and for trying to help make this application
a better one!
# Number of days of inactivity before an issue is closed for lack of response.
daysUntilClose: 21
# Label requiring a response.
responseRequiredLabel: "💬 response-needed"

View File

@@ -6,25 +6,19 @@ on:
tags: tags:
description: 'Docker tags' description: 'Docker tags'
required: true required: true
release:
types: [published]
jobs: jobs:
build-and-push: build-and-push:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: checkout code - name: checkout code
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: Set hash - name: Set hash
id: vars id: vars
run: echo "::set-output name=sha_short::$(git rev-parse --short HEAD)" run: echo "::set-output name=sha_short::$(git rev-parse --short HEAD)"
- name: Get current date - name: Get current date
id: date id: date
run: echo "::set-output name=date::$(date +'%Y-%m-%d')" run: echo "::set-output name=date::$(date +'%Y-%m-%d')"
- name: create-json - name: create-json
id: create-json id: create-json
uses: jsdaniell/create-json@1.1.2 uses: jsdaniell/create-json@1.1.2
@@ -32,49 +26,15 @@ jobs:
name: "version.json" name: "version.json"
json: '{"type": "docker", "tag": "latest", "commit": "${{ steps.vars.outputs.sha_short }}", "date": "${{ steps.date.outputs.date }}"}' json: '{"type": "docker", "tag": "latest", "commit": "${{ steps.vars.outputs.sha_short }}", "date": "${{ steps.date.outputs.date }}"}'
dir: 'backend/' dir: 'backend/'
- name: Set image tag
id: tags
run: |
if [ "${{ github.event.inputs.tags }}" != "" ]; then
echo "::set-output name=tags::${{ github.event.inputs.tags }}"
elif [ ${{ github.event.action }} == "release" ]; then
echo "::set-output name=tags::${{ github.event.release.tag_name }}"
else
echo "Unknown workflow trigger: ${{ github.event.action }}! Cannot determine default tag."
exit 1
fi
- name: Generate Docker image metadata
id: docker-meta
uses: docker/metadata-action@v4
with:
images: |
${{ secrets.DOCKERHUB_USERNAME }}/${{ secrets.DOCKERHUB_REPO }}
ghcr.io/${{ github.repository_owner }}/${{ secrets.DOCKERHUB_REPO }}
tags: |
type=raw,value=${{ steps.tags.outputs.tags }}
type=raw,value=latest
- name: setup platform emulator - name: setup platform emulator
uses: docker/setup-qemu-action@v1 uses: docker/setup-qemu-action@v1
- name: setup multi-arch docker build - name: setup multi-arch docker build
uses: docker/setup-buildx-action@v1 uses: docker/setup-buildx-action@v1
- name: Login to DockerHub - name: Login to DockerHub
uses: docker/login-action@v1 uses: docker/login-action@v1
with: with:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GitHub Container Registry
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: build & push images - name: build & push images
uses: docker/build-push-action@v2 uses: docker/build-push-action@v2
with: with:
@@ -82,5 +42,4 @@ jobs:
file: ./Dockerfile file: ./Dockerfile
platforms: linux/amd64,linux/arm,linux/arm64/v8 platforms: linux/amd64,linux/arm,linux/arm64/v8
push: true push: true
tags: ${{ steps.docker-meta.outputs.tags }} tags: ${{ github.event.inputs.tags }}
labels: ${{ steps.docker-meta.outputs.labels }}

View File

@@ -13,9 +13,6 @@ on:
- '**.pem' - '**.pem'
- '.dockerignore' - '.dockerignore'
- '.gitignore' - '.gitignore'
schedule:
- cron: '34 4 * * 2'
workflow_dispatch:
jobs: jobs:
build-and-push: build-and-push:
@@ -23,15 +20,12 @@ jobs:
steps: steps:
- name: checkout code - name: checkout code
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: Set hash - name: Set hash
id: vars id: vars
run: echo "::set-output name=sha_short::$(git rev-parse --short HEAD)" run: echo "::set-output name=sha_short::$(git rev-parse --short HEAD)"
- name: Get current date - name: Get current date
id: date id: date
run: echo "::set-output name=date::$(date +'%Y-%m-%d')" run: echo "::set-output name=date::$(date +'%Y-%m-%d')"
- name: create-json - name: create-json
id: create-json id: create-json
uses: jsdaniell/create-json@1.1.2 uses: jsdaniell/create-json@1.1.2
@@ -39,42 +33,15 @@ jobs:
name: "version.json" name: "version.json"
json: '{"type": "docker", "tag": "${{secrets.DOCKERHUB_MASTER_TAG}}", "commit": "${{ steps.vars.outputs.sha_short }}", "date": "${{ steps.date.outputs.date }}"}' json: '{"type": "docker", "tag": "${{secrets.DOCKERHUB_MASTER_TAG}}", "commit": "${{ steps.vars.outputs.sha_short }}", "date": "${{ steps.date.outputs.date }}"}'
dir: 'backend/' dir: 'backend/'
- name: setup platform emulator - name: setup platform emulator
uses: docker/setup-qemu-action@v1 uses: docker/setup-qemu-action@v1
- name: setup multi-arch docker build - name: setup multi-arch docker build
uses: docker/setup-buildx-action@v1 uses: docker/setup-buildx-action@v1
- name: Generate Docker image metadata
id: docker-meta
uses: docker/metadata-action@v4
# Defaults:
# DOCKERHUB_USERNAME : tzahi12345
# DOCKERHUB_REPO : youtubedl-material
# DOCKERHUB_MASTER_TAG: nightly
with:
images: |
${{ secrets.DOCKERHUB_USERNAME }}/${{ secrets.DOCKERHUB_REPO }}
ghcr.io/${{ github.repository_owner }}/${{ secrets.DOCKERHUB_REPO }}
tags: |
type=raw,${{secrets.DOCKERHUB_MASTER_TAG}}-{{ date 'YYYY-MM-DD' }}
type=raw,${{secrets.DOCKERHUB_MASTER_TAG}}
type=sha,prefix=sha-,format=short
- name: Login to DockerHub - name: Login to DockerHub
uses: docker/login-action@v1 uses: docker/login-action@v1
with: with:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GitHub Container Registry
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: build & push images - name: build & push images
uses: docker/build-push-action@v2 uses: docker/build-push-action@v2
with: with:
@@ -82,5 +49,8 @@ jobs:
file: ./Dockerfile file: ./Dockerfile
platforms: linux/amd64,linux/arm,linux/arm64/v8 platforms: linux/amd64,linux/arm,linux/arm64/v8
push: true push: true
tags: ${{ steps.docker-meta.outputs.tags }} # Defaults:
labels: ${{ steps.docker-meta.outputs.labels }} # DOCKERHUB_USERNAME : tzahi12345
# DOCKERHUB_REPO : youtubedl-material
# DOCKERHUB_MASTER_TAG: nightly
tags: ${{ secrets.DOCKERHUB_USERNAME }}/${{ secrets.DOCKERHUB_REPO }}:${{secrets.DOCKERHUB_MASTER_TAG}}

View File

@@ -1,69 +1,77 @@
# Fetching our ffmpeg
FROM ubuntu:22.04 AS ffmpeg FROM ubuntu:22.04 AS ffmpeg
ENV DEBIAN_FRONTEND=noninteractive ENV DEBIAN_FRONTEND=noninteractive
# Use script due local build compability
COPY ffmpeg-fetch.sh .
RUN sh ./ffmpeg-fetch.sh
RUN apt-get update && apt-get install -y software-properties-common
RUN add-apt-repository ppa:savoury1/ffmpeg4
RUN add-apt-repository ppa:savoury1/ffmpeg5 && apt-get update && apt-get install -y ffmpeg
# Create our Ubuntu 22.04 with node 16 #--------------# Stage 2
# Go to 20.04
FROM ubuntu:20.04 AS base
ARG DEBIAN_FRONTEND=noninteractive
ENV UID=1000
ENV GID=1000
ENV USER=youtube
ENV NO_UPDATE_NOTIFIER=true
ENV PM2_HOME=/app/pm2
ENV ALLOW_CONFIG_MUTATIONS=true
RUN groupadd -g $GID $USER && useradd --system -m -g $USER --uid $UID $USER && \
apt update && \
apt install -y --no-install-recommends curl ca-certificates tzdata && \
curl -fsSL https://deb.nodesource.com/setup_16.x | bash - && \
apt install -y --no-install-recommends nodejs && \
npm -g install npm && \
apt clean && \
rm -rf /var/lib/apt/lists/*
FROM ubuntu:22.04 as frontend
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get -y install \
curl \
gnupg \
# Ubuntu 22.04 ships Node.JS 12 by default :)
nodejs \
# needed on 21.10 and before, maybe not on 22.04 YARN: brings along npm, solves dependency conflicts,
# spares us this spaghetti approach: https://stackoverflow.com/a/60547197
npm && \
apt-get install -f && \
npm config set strict-ssl false && \
npm install -g @angular/cli
# Build frontend
FROM base as frontend
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", "/build/" ]
RUN npm install
COPY [ "angular.json", "tsconfig.json", "/build/" ]
COPY [ "src/", "/build/src/" ] COPY [ "src/", "/build/src/" ]
RUN npm install && \ RUN npm run build
npm run build && \
ls -al /build/backend/public
#--------------# Final Stage
FROM ubuntu:22.04
ENV UID=1000 \
GID=1000 \
USER=youtube \
NO_UPDATE_NOTIFIER=true
ENV DEBIAN_FRONTEND=noninteractive
RUN groupadd -g $GID $USER && useradd --system -g $USER --uid $UID $USER
RUN apt-get update && apt-get -y install \
npm \
python2 \
python3 \
gosu \
atomicparsley \
--no-install-recommends && \
apt-get install -f && \
apt-get autoremove --purge && \
apt-get autoremove && \
apt-get clean && \
rm -rf /var/lib/apt
# Install backend deps
FROM base as backend
WORKDIR /app WORKDIR /app
COPY [ "backend/","/app/" ]
RUN npm config set strict-ssl false && \
npm install --prod && \
ls -al
# Final image
FROM base
RUN npm install -g pm2 && \
apt update && \
apt install -y --no-install-recommends gosu python3-minimal python-is-python3 python3-pip atomicparsley && \
apt clean && \
rm -rf /var/lib/apt/lists/*
RUN pip install tcd
WORKDIR /app
# User 1000 already exist from base image
COPY --chown=$UID:$GID --from=ffmpeg [ "/usr/local/bin/ffmpeg", "/usr/local/bin/ffmpeg" ] COPY --chown=$UID:$GID --from=ffmpeg [ "/usr/local/bin/ffmpeg", "/usr/local/bin/ffmpeg" ]
COPY --chown=$UID:$GID --from=ffmpeg [ "/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=backend ["/app/","/app/"] COPY --chown=$UID:$GID [ "backend/package.json", "backend/package-lock.json", "/app/" ]
ENV PM2_HOME=/app/pm2
RUN npm config set strict-ssl false && \
npm install pm2 -g && \
npm install && chown -R $UID:$GID ./
# needed for ubuntu, see #596
RUN ln -s /usr/bin/python3 /usr/bin/python
COPY --chown=$UID:$GID --from=frontend [ "/build/backend/public/", "/app/public/" ] COPY --chown=$UID:$GID --from=frontend [ "/build/backend/public/", "/app/public/" ]
RUN chmod +x /app/fix-scripts/*.sh COPY --chown=$UID:$GID [ "/backend/", "/app/" ]
# Add some persistence data
#VOLUME ["/app/appdata"]
EXPOSE 17442 EXPOSE 17442
ENTRYPOINT [ "/app/entrypoint.sh" ] # ENTRYPOINT [ "/app/entrypoint.sh" ]
CMD [ "npm","start" ] CMD [ "pm2-runtime", "pm2.config.js" ]

View File

@@ -1,2 +1,2 @@
FROM tzahi12345/youtubedl-material:latest FROM tzahi12345/youtubedl-material:nightly
CMD [ "npm", "start" ] CMD [ "pm2-runtime", "pm2.config.js" ]

View File

@@ -97,11 +97,6 @@ paths:
summary: Get all files summary: Get all files
description: Gets all files and playlists stored in the db description: Gets all files and playlists stored in the db
operationId: get-getAllFiles operationId: get-getAllFiles
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/GetAllFilesRequest'
responses: responses:
'200': '200':
description: OK description: OK
@@ -134,27 +129,6 @@ paths:
description: User is not authorized to view the file. description: User is not authorized to view the file.
security: security:
- Auth query parameter: [] - Auth query parameter: []
/api/updateFile:
post:
tags:
- files
summary: Updates file database object
description: Updates a file db object using its uid and a change object.
operationId: post-updateFile
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/UpdateFileRequest'
responses:
'200':
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/SuccessObject'
security:
- Auth query parameter: []
/api/enableSharing: /api/enableSharing:
post: post:
tags: tags:
@@ -867,10 +841,17 @@ paths:
- Auth query parameter: [] - Auth query parameter: []
tags: tags:
- downloader - downloader
/api/clearDownloads: /api/clearFinishedDownloads:
post: post:
summary: Clear multiple downloads tags:
operationId: post-api-clear-downloads - downloader
summary: Clear finished downloads
operationId: post-api-clear-finished-downloads
requestBody:
content:
application/json:
schema:
type: object
responses: responses:
'200': '200':
description: OK description: OK
@@ -878,17 +859,8 @@ paths:
application/json: application/json:
schema: schema:
$ref: '#/components/schemas/SuccessObject' $ref: '#/components/schemas/SuccessObject'
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/ClearDownloadsRequest'
description: ''
description: "Clears multiple downloads based on a given filter."
security: security:
- Auth query parameter: [] - Auth query parameter: []
tags:
- downloader
/api/getTask: /api/getTask:
post: post:
summary: Get info for one task summary: Get info for one task
@@ -1535,8 +1507,6 @@ components:
properties: properties:
success: success:
type: boolean type: boolean
error:
type: string
FileType: FileType:
type: string type: string
enum: enum:
@@ -1637,15 +1607,6 @@ components:
type: array type: array
items: items:
$ref: '#/components/schemas/Download' $ref: '#/components/schemas/Download'
ClearDownloadsRequest:
type: object
properties:
clear_finished:
type: boolean
clear_paused:
type: boolean
clear_errors:
type: boolean
GetTaskRequest: GetTaskRequest:
type: object type: object
properties: properties:
@@ -1729,41 +1690,6 @@ components:
description: All video playlists description: All video playlists
items: items:
$ref: '#/components/schemas/Playlist' $ref: '#/components/schemas/Playlist'
GetAllFilesRequest:
type: object
properties:
sort:
$ref: '#/components/schemas/Sort'
range:
type: array
items:
type: number
description: Two elements allowed, start index and end index
minItems: 2
maxItems: 2
text_search:
type: string
description: Filter files by title
file_type_filter:
$ref: '#/components/schemas/FileTypeFilter'
sub_id:
type: string
description: Include if you want to filter by subscription
Sort:
type: object
properties:
by:
type: string
description: Property to sort by
order:
type: number
description: 1 for ascending, -1 for descending
FileTypeFilter:
type: string
enum:
- audio_only
- video_only
- both
GetAllFilesResponse: GetAllFilesResponse:
required: required:
- files - files
@@ -1801,18 +1727,6 @@ components:
type: boolean type: boolean
file: file:
$ref: '#/components/schemas/DatabaseFile' $ref: '#/components/schemas/DatabaseFile'
UpdateFileRequest:
required:
- uid
- change_obj
type: object
properties:
uid:
type: string
description: Video UID
change_obj:
type: object
description: Object with fields to update as keys and their new values
SharingToggle: SharingToggle:
required: required:
- uid - uid
@@ -1826,6 +1740,7 @@ components:
required: required:
- name - name
- url - url
- streamingOnly
type: object type: object
properties: properties:
name: name:
@@ -1938,6 +1853,7 @@ components:
- uids - uids
- playlistName - playlistName
- thumbnailURL - thumbnailURL
- type
type: object type: object
properties: properties:
playlistName: playlistName:
@@ -1946,6 +1862,8 @@ components:
type: array type: array
items: items:
type: string type: string
type:
$ref: '#/components/schemas/FileType'
thumbnailURL: thumbnailURL:
type: string type: string
CreatePlaylistResponse: CreatePlaylistResponse:
@@ -1975,17 +1893,15 @@ components:
required: required:
- playlist - playlist
- success - success
- type
type: object type: object
properties: properties:
playlist: playlist:
$ref: '#/components/schemas/Playlist' $ref: '#/components/schemas/Playlist'
type:
$ref: '#/components/schemas/FileType'
success: success:
type: boolean type: boolean
file_objs:
type: array
description: File objects for every uid in the playlist's uids property, in the same order
items:
$ref: '#/components/schemas/DatabaseFile'
GetPlaylistsRequest: GetPlaylistsRequest:
type: object type: object
properties: properties:
@@ -2010,10 +1926,13 @@ components:
DeletePlaylistRequest: DeletePlaylistRequest:
required: required:
- playlist_id - playlist_id
- type
type: object type: object
properties: properties:
playlist_id: playlist_id:
type: string type: string
type:
$ref: '#/components/schemas/FileType'
DownloadFileRequest: DownloadFileRequest:
type: object type: object
properties: properties:
@@ -2234,6 +2153,7 @@ components:
type: boolean type: boolean
result: result:
allOf: allOf:
- $ref: '#/components/schemas/file'
- type: object - type: object
properties: properties:
formats: formats:
@@ -2391,9 +2311,6 @@ components:
type: string type: string
thumbnailURL: thumbnailURL:
type: string type: string
description: Backup if thumbnailPath is not defined
thumbnailPath:
type: string
isAudio: isAudio:
type: boolean type: boolean
duration: duration:
@@ -2405,7 +2322,6 @@ components:
type: string type: string
size: size:
type: number type: number
description: In bytes
path: path:
type: string type: string
upload_date: upload_date:
@@ -2414,22 +2330,6 @@ components:
type: string type: string
sharingEnabled: sharingEnabled:
type: boolean type: boolean
category:
$ref: '#/components/schemas/Category'
view_count:
type: number
local_view_count:
type: number
sub_id:
type: string
registered:
type: number
height:
type: number
description: In pixels, only for videos
abr:
type: number
description: In Kbps
Playlist: Playlist:
required: required:
- uids - uids
@@ -2459,8 +2359,6 @@ components:
type: number type: number
user_uid: user_uid:
type: string type: string
auto:
type: boolean
Download: Download:
required: required:
- url - url
@@ -2511,8 +2409,6 @@ components:
type: string type: string
sub_name: sub_name:
type: string type: string
prefetched_info:
type: object
Task: Task:
required: required:
- key - key
@@ -2527,8 +2423,6 @@ components:
properties: properties:
key: key:
type: string type: string
title:
type: string
last_ran: last_ran:
type: number type: number
last_confirmed: last_confirmed:
@@ -2609,6 +2503,7 @@ components:
- url - url
- type - type
- user_uid - user_uid
- streamingOnly
- isPlaylist - isPlaylist
- videos - videos
type: object type: object
@@ -2624,6 +2519,8 @@ components:
user_uid: user_uid:
type: string type: string
nullable: true nullable: true
streamingOnly:
type: boolean
isPlaylist: isPlaylist:
type: boolean type: boolean
archive: archive:
@@ -2648,6 +2545,28 @@ components:
type: string type: string
passhash: passhash:
type: string type: string
files:
type: object
properties:
audio:
type: array
items:
$ref: '#/components/schemas/file'
video:
type: array
items:
$ref: '#/components/schemas/file'
playlists:
type: object
properties:
audio:
type: array
items:
$ref: '#/components/schemas/file'
video:
type: array
items:
$ref: '#/components/schemas/file'
subscriptions: subscriptions:
type: array type: array
items: items:

View File

@@ -12,6 +12,16 @@ Now with [Docker](#Docker) support!
<hr> <hr>
### USAGE OF THE NIGHTLY BUILDS IS HIGHLY RECOMMENDED.
For much better scaling with large datasets please run your YTDL-M instance with a MongoDB backend rather than the json file-based default.
It will fix a lot of performance problems (especially with datasets in the tens of thousands videos/audios)!
The (closed) issues as well as the project's Wiki will give you good starting points for your journey!
For MongoDB specifically there is [this little guide](https://github.com/Tzahi12345/YoutubeDL-Material/wiki/Setting-a-MongoDB-backend-to-use-as-database-provider-for-YTDL-M).
<hr>
## Getting Started ## Getting Started
Check out the prerequisites, and go to the installation section. Easy as pie! Check out the prerequisites, and go to the installation section. Easy as pie!
@@ -48,7 +58,6 @@ sudo yum install nodejs youtube-dl ffmpeg ffmpeg-devel
Optional dependencies: Optional dependencies:
* AtomicParsley (for embedding thumbnails, package name `atomicparsley`) * AtomicParsley (for embedding thumbnails, package name `atomicparsley`)
* [tcd](https://github.com/PetterKraabol/Twitch-Chat-Downloader) (for downloading Twitch VOD chats)
### Installing ### Installing
@@ -82,7 +91,7 @@ Alternatively, you can port forward the port specified in the config (defaults t
### Host-specific instructions ### Host-specific instructions
If you're on a Synology NAS, unRAID, Raspberry Pi 4 or any other possible special case you can check if there's known issues or instructions both in the issue tracker and in the [Wiki!](https://github.com/Tzahi12345/YoutubeDL-Material/wiki#environment-specific-guideshelp) If you're on a Synology NAS, unRAID or any other possible special case you can check if there's known issues or instructions both in the issue tracker and in the [Wiki!](https://github.com/Tzahi12345/YoutubeDL-Material/wiki#environment-specific-guideshelp)
### Setup ### Setup
@@ -93,6 +102,8 @@ If you are looking to setup YoutubeDL-Material with Docker, this section is for
3. Run `docker-compose up` to start it up. If successful, it should say "HTTP(S): Started on port 17443" or something similar. This tells you the *container-internal* port of the application. Please check your `docker-compose.yml` file for the *external* port. If you downloaded the file as described above, it defaults to **8998**. 3. Run `docker-compose up` to start it up. If successful, it should say "HTTP(S): Started on port 17443" or something similar. This tells you the *container-internal* port of the application. Please check your `docker-compose.yml` file for the *external* port. If you downloaded the file as described above, it defaults to **8998**.
4. Make sure you can connect to the specified URL + *external* port, and if so, you are done! 4. Make sure you can connect to the specified URL + *external* port, and if so, you are done!
NOTE: It is currently recommended that you use the `nightly` tag on Docker. To do so, simply update the docker-compose.yml `image` field so that it points to `tzahi12345/youtubedl-material:nightly`.
### Custom UID/GID ### Custom UID/GID
By default, the Docker container runs as non-root with UID=1000 and GID=1000. To set this to your own UID/GID, simply update the `environment` section in your `docker-compose.yml` like so: By default, the Docker container runs as non-root with UID=1000 and GID=1000. To set this to your own UID/GID, simply update the `environment` section in your `docker-compose.yml` like so:
@@ -103,12 +114,6 @@ environment:
GID: YOUR_GID GID: YOUR_GID
``` ```
## MongoDB
For much better scaling with large datasets please run your YoutubeDL-Material instance with MongoDB backend rather than the json file-based default. It will fix a lot of performance problems (especially with datasets in the tens of thousands videos/audios)!
[Tutorial](https://github.com/Tzahi12345/YoutubeDL-Material/wiki/Setting-a-MongoDB-backend-to-use-as-database-provider-for-YTDL-M).
## API ## API
[API Docs](https://youtubedl-material.stoplight.io/docs/youtubedl-material/Public%20API%20v1.yaml) [API Docs](https://youtubedl-material.stoplight.io/docs/youtubedl-material/Public%20API%20v1.yaml)

View File

@@ -2,14 +2,14 @@
## Supported Versions ## Supported Versions
If you would like to see the latest updates, use the `nightly` tag on Docker. Currently all work on this project goes into the nightly builds.
4.2's RELEASE build is now quite old and should be considered legacy.
If you'd like to stick with more stable releases, use the `latest` tag on Docker or download the [latest release here](https://github.com/Tzahi12345/YoutubeDL-Material/releases/latest). We urge users to use the nightly releases, because the project
constantly sees fixes.
| Version | Supported | | Version | Supported |
| -------------------- | ------------------ | | ------------- | ------------------ |
| 4.3 Docker Nightlies | :white_check_mark: | | 4.2 Nightlies | :white_check_mark: |
| 4.3 Release | :white_check_mark: |
| 4.2 Release | :x: | | 4.2 Release | :x: |
| < 4.2 | :x: | | < 4.2 | :x: |

View File

@@ -30,8 +30,7 @@
"src/backend" "src/backend"
], ],
"styles": [ "styles": [
"src/styles.scss", "src/styles.scss"
"src/bootstrap.min.css"
], ],
"scripts": [], "scripts": [],
"vendorChunk": true, "vendorChunk": true,
@@ -119,8 +118,7 @@
"src/backend" "src/backend"
], ],
"styles": [ "styles": [
"src/styles.scss", "src/styles.scss"
"src/bootstrap.min.css"
], ],
"scripts": [] "scripts": []
}, },
@@ -153,8 +151,7 @@
"tsConfig": "src/tsconfig.spec.json", "tsConfig": "src/tsconfig.spec.json",
"scripts": [], "scripts": [],
"styles": [ "styles": [
"src/styles.scss", "src/styles.scss"
"src/bootstrap.min.css"
], ],
"assets": [ "assets": [
"src/assets", "src/assets",
@@ -182,6 +179,7 @@
} }
} }
}, },
"defaultProject": "youtube-dl-material",
"schematics": { "schematics": {
"@schematics/angular:component": { "@schematics/angular:component": {
"prefix": "app", "prefix": "app",

View File

@@ -68,8 +68,7 @@ db.defaults(
configWriteFlag: false, configWriteFlag: false,
downloads: {}, downloads: {},
subscriptions: [], subscriptions: [],
files_to_db_migration_complete: false, files_to_db_migration_complete: false
tasks_manager_role_migration_complete: false
}).write(); }).write();
users_db.defaults( users_db.defaults(
@@ -102,6 +101,7 @@ let backendPort = null;
let useDefaultDownloadingAgent = null; let useDefaultDownloadingAgent = null;
let customDownloadingAgent = null; let customDownloadingAgent = null;
let allowSubscriptions = null; let allowSubscriptions = null;
let archivePath = path.join(__dirname, 'appdata', 'archives');
// other needed values // other needed values
let url_domain = null; let url_domain = null;
@@ -148,11 +148,16 @@ if (fs.existsSync('version.json')) {
// don't overwrite config if it already happened.. NOT // don't overwrite config if it already happened.. NOT
// let alreadyWritten = db.get('configWriteFlag').value(); // let alreadyWritten = db.get('configWriteFlag').value();
let writeConfigMode = process.env.write_ytdl_config;
// checks if config exists, if not, a config is auto generated // checks if config exists, if not, a config is auto generated
config_api.configExistsCheck(); config_api.configExistsCheck();
if (writeConfigMode) {
setAndLoadConfig(); setAndLoadConfig();
} else {
loadConfig();
}
app.use(bodyParser.urlencoded({ extended: false })); app.use(bodyParser.urlencoded({ extended: false }));
app.use(bodyParser.json()); app.use(bodyParser.json());
@@ -183,22 +188,13 @@ async function checkMigrations() {
if (!new_db_system_migration_complete) { if (!new_db_system_migration_complete) {
logger.info('Beginning migration: 4.2->4.3+') logger.info('Beginning migration: 4.2->4.3+')
let success = await db_api.importJSONToDB(db.value(), users_db.value()); let success = await db_api.importJSONToDB(db.value(), users_db.value());
await tasks_api.setupTasks(); // necessary as tasks were not properly initialized at first
// sets migration to complete // sets migration to complete
db.set('new_db_system_migration_complete', true).write(); db.set('new_db_system_migration_complete', true).write();
if (success) { logger.info('4.2->4.3+ migration complete!'); } if (success) { logger.info('4.2->4.3+ migration complete!'); }
else { logger.error('Migration failed: 4.2->4.3+'); } else { logger.error('Migration failed: 4.2->4.3+'); }
} }
const tasks_manager_role_migration_complete = db.get('tasks_manager_role_migration_complete').value();
if (!tasks_manager_role_migration_complete) {
logger.info('Checking if tasks manager role permissions exist for admin user...');
const success = await auth_api.changeRolePermissions('admin', 'tasks_manager', 'yes');
if (success) logger.info('Task manager permissions check complete!');
else logger.error('Failed to auto add tasks manager permissions to admin role!');
db.set('tasks_manager_role_migration_complete', true).write();
}
return true; return true;
} }
@@ -488,9 +484,8 @@ async function setAndLoadConfig() {
} }
async function setConfigFromEnv() { async function setConfigFromEnv() {
const config_items = getEnvConfigItems(); let config_items = getEnvConfigItems();
if (!config_items || config_items.length === 0) return true; let success = config_api.setConfigItems(config_items);
const success = config_api.setConfigItems(config_items);
if (success) { if (success) {
logger.info('Config items set using ENV variables.'); logger.info('Config items set using ENV variables.');
await utils.wait(100); await utils.wait(100);
@@ -505,13 +500,12 @@ async function loadConfig() {
loadConfigValues(); loadConfigValues();
// connect to DB // connect to DB
if (!config_api.getConfigItem('ytdl_use_local_db'))
await db_api.connectToDB(); await db_api.connectToDB();
db_api.database_initialized = true; db_api.database_initialized = true;
db_api.database_initialized_bs.next(true); db_api.database_initialized_bs.next(true);
// creates archive path if missing // creates archive path if missing
await fs.ensureDir(utils.getArchiveFolder()); await fs.ensureDir(archivePath);
// check migrations // check migrations
await checkMigrations(); await checkMigrations();
@@ -581,11 +575,7 @@ async function watchSubscriptions() {
if (!subscriptions) return; if (!subscriptions) return;
// auto pause deprecated streamingOnly mode const valid_subscriptions = subscriptions.filter(sub => !sub.paused);
const streaming_only_subs = subscriptions.filter(sub => sub.streamingOnly);
subscriptions_api.updateSubscriptionPropertyMultiple(streaming_only_subs, {paused: true});
const valid_subscriptions = subscriptions.filter(sub => !sub.paused && !sub.streamingOnly);
let subscriptions_amount = valid_subscriptions.length; let subscriptions_amount = valid_subscriptions.length;
let delay_interval = calculateSubcriptionRetrievalDelay(subscriptions_amount); let delay_interval = calculateSubcriptionRetrievalDelay(subscriptions_amount);
@@ -922,11 +912,11 @@ app.post('/api/getFile', optionalJwt, async function (req, res) {
app.post('/api/getAllFiles', optionalJwt, async function (req, res) { app.post('/api/getAllFiles', optionalJwt, async function (req, res) {
// these are returned // these are returned
let files = null; let files = null;
const sort = req.body.sort; let playlists = null;
const range = req.body.range; let sort = req.body.sort;
const text_search = req.body.text_search; let range = req.body.range;
const file_type_filter = req.body.file_type_filter; let text_search = req.body.text_search;
const sub_id = req.body.sub_id; let file_type_filter = req.body.file_type_filter;
const uuid = req.isAuthenticated() ? req.user.uid : null; const uuid = req.isAuthenticated() ? req.user.uid : null;
const filter_obj = {user_uid: uuid}; const filter_obj = {user_uid: uuid};
@@ -939,42 +929,27 @@ app.post('/api/getAllFiles', optionalJwt, async function (req, res) {
} }
} }
if (sub_id) {
filter_obj['sub_id'] = sub_id;
}
if (file_type_filter === 'audio_only') filter_obj['isAudio'] = true; if (file_type_filter === 'audio_only') filter_obj['isAudio'] = true;
else if (file_type_filter === 'video_only') filter_obj['isAudio'] = false; else if (file_type_filter === 'video_only') filter_obj['isAudio'] = false;
files = await db_api.getRecords('files', filter_obj, false, sort, range, text_search); files = await db_api.getRecords('files', filter_obj, false, sort, range, text_search);
const file_count = await db_api.getRecords('files', filter_obj, true); let file_count = await db_api.getRecords('files', filter_obj, true);
playlists = await db_api.getRecords('playlists', {user_uid: uuid});
const categories = await categories_api.getCategoriesAsPlaylists(files);
if (categories) {
playlists = playlists.concat(categories);
}
files = JSON.parse(JSON.stringify(files)); files = JSON.parse(JSON.stringify(files));
res.send({ res.send({
files: files, files: files,
file_count: file_count, file_count: file_count,
playlists: playlists
}); });
}); });
app.post('/api/updateFile', optionalJwt, async function (req, res) {
const uid = req.body.uid;
const change_obj = req.body.change_obj;
const file = await db_api.updateRecord('files', {uid: uid}, change_obj);
if (!file) {
res.send({
success: false,
error: 'File could not be found'
});
} else {
res.send({
success: true
});
}
});
app.post('/api/checkConcurrentStream', async (req, res) => { app.post('/api/checkConcurrentStream', async (req, res) => {
const uid = req.body.uid; const uid = req.body.uid;
@@ -1282,7 +1257,7 @@ app.post('/api/getSubscription', optionalJwt, async (req, res) => {
subscription = JSON.parse(JSON.stringify(subscription)); subscription = JSON.parse(JSON.stringify(subscription));
// get sub videos // get sub videos
if (subscription.name) { if (subscription.name && !subscription.streamingOnly) {
var parsed_files = await db_api.getRecords('files', {sub_id: subscription.id}); // subscription.videos; var parsed_files = await db_api.getRecords('files', {sub_id: subscription.id}); // subscription.videos;
subscription['videos'] = parsed_files; subscription['videos'] = parsed_files;
// loop through files for extra processing // loop through files for extra processing
@@ -1292,6 +1267,19 @@ app.post('/api/getSubscription', optionalJwt, async (req, res) => {
if (file && file['url'].includes('twitch.tv')) file['chat_exists'] = fs.existsSync(file['path'].substring(0, file['path'].length - 4) + '.twitch_chat.json'); if (file && file['url'].includes('twitch.tv')) file['chat_exists'] = fs.existsSync(file['path'].substring(0, file['path'].length - 4) + '.twitch_chat.json');
} }
res.send({
subscription: subscription,
files: parsed_files
});
} else if (subscription.name && subscription.streamingOnly) {
// return list of videos
let parsed_files = [];
if (subscription.videos) {
for (let i = 0; i < subscription.videos.length; i++) {
const video = subscription.videos[i];
parsed_files.push(new utils.File(video.title, video.title, video.thumbnail, false, video.duration, video.url, video.uploader, video.size, null, null, video.upload_date, video.view_count, video.height, video.abr));
}
}
res.send({ res.send({
subscription: subscription, subscription: subscription,
files: parsed_files files: parsed_files
@@ -1336,8 +1324,9 @@ app.post('/api/getSubscriptions', optionalJwt, async (req, res) => {
app.post('/api/createPlaylist', optionalJwt, async (req, res) => { app.post('/api/createPlaylist', optionalJwt, async (req, res) => {
let playlistName = req.body.playlistName; let playlistName = req.body.playlistName;
let uids = req.body.uids; let uids = req.body.uids;
let type = req.body.type;
const new_playlist = await db_api.createPlaylist(playlistName, uids, req.isAuthenticated() ? req.user.uid : null); const new_playlist = await db_api.createPlaylist(playlistName, uids, type, req.isAuthenticated() ? req.user.uid : null);
res.send({ res.send({
new_playlist: new_playlist, new_playlist: new_playlist,
@@ -1365,6 +1354,7 @@ app.post('/api/getPlaylist', optionalJwt, async (req, res) => {
res.send({ res.send({
playlist: playlist, playlist: playlist,
file_objs: file_objs, file_objs: file_objs,
type: playlist && playlist.type,
success: !!playlist success: !!playlist
}); });
}); });
@@ -1375,7 +1365,7 @@ app.post('/api/getPlaylists', optionalJwt, async (req, res) => {
let playlists = await db_api.getRecords('playlists', {user_uid: uuid}); let playlists = await db_api.getRecords('playlists', {user_uid: uuid});
if (include_categories) { if (include_categories) {
const categories = await categories_api.getCategoriesAsPlaylists(); const categories = await categories_api.getCategoriesAsPlaylists(files);
if (categories) { if (categories) {
playlists = playlists.concat(categories); playlists = playlists.concat(categories);
} }
@@ -1679,15 +1669,9 @@ app.post('/api/download', optionalJwt, async (req, res) => {
} }
}); });
app.post('/api/clearDownloads', optionalJwt, async (req, res) => { app.post('/api/clearFinishedDownloads', optionalJwt, async (req, res) => {
const user_uid = req.isAuthenticated() ? req.user.uid : null; const user_uid = req.isAuthenticated() ? req.user.uid : null;
const clear_finished = req.body.clear_finished; const success = db_api.removeAllRecords('download_queue', {finished: true, user_uid: user_uid});
const clear_paused = req.body.clear_paused;
const clear_errors = req.body.clear_errors;
let success = true;
if (clear_finished) success &= await db_api.removeAllRecords('download_queue', {finished: true, user_uid: user_uid});
if (clear_paused) success &= await db_api.removeAllRecords('download_queue', {paused: true, user_uid: user_uid});
if (clear_errors) success &= await db_api.removeAllRecords('download_queue', {error: {$ne: null}, user_uid: user_uid});
res.send({success: success}); res.send({success: success});
}); });

View File

@@ -31,8 +31,7 @@
"use_youtube_API": false, "use_youtube_API": false,
"youtube_API_key": "", "youtube_API_key": "",
"use_twitch_API": false, "use_twitch_API": false,
"twitch_client_ID": "", "twitch_API_key": "",
"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
@@ -64,7 +63,7 @@
"mongodb_connection_string": "mongodb://127.0.0.1:27017/?compressors=zlib" "mongodb_connection_string": "mongodb://127.0.0.1:27017/?compressors=zlib"
}, },
"Advanced": { "Advanced": {
"default_downloader": "yt-dlp", "default_downloader": "youtube-dl",
"use_default_downloading_agent": true, "use_default_downloading_agent": true,
"custom_downloading_agent": "", "custom_downloading_agent": "",
"multi_user_mode": false, "multi_user_mode": false,

View File

@@ -171,12 +171,8 @@ exports.registerUser = async function(req, res) {
exports.login = async (username, password) => { exports.login = async (username, password) => {
// even if we're using LDAP, we still want users to be able to login using internal credentials
const user = await db_api.getRecord('users', {name: username}); const user = await db_api.getRecord('users', {name: username});
if (!user) { if (!user) { logger.error(`User ${username} not found`); return false }
if (config_api.getConfigItem('ytdl_auth_method') === 'internal') logger.error(`User ${username} not found`);
return false;
}
if (user.auth_method && user.auth_method !== 'internal') { return false } if (user.auth_method && user.auth_method !== 'internal') { return false }
return await bcrypt.compare(password, user.passhash) ? user : false; return await bcrypt.compare(password, user.passhash) ? user : false;
} }
@@ -361,6 +357,7 @@ exports.userHasPermission = async function(user_uid, permission) {
logger.error('Invalid role ' + role); logger.error('Invalid role ' + role);
return false; return false;
} }
const role_permissions = (await db_api.getRecords('roles'))['permissions'];
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);
@@ -375,8 +372,7 @@ exports.userHasPermission = async function(user_uid, permission) {
} }
// no overrides, let's check if the role has the permission // no overrides, let's check if the role has the permission
const role_has_permission = await exports.roleHasPermissions(role, permission); if (role_permissions.includes(permission)) {
if (role_has_permission) {
return true; return true;
} else { } else {
logger.verbose(`User ${user_uid} failed to get permission ${permission}`); logger.verbose(`User ${user_uid} failed to get permission ${permission}`);
@@ -384,16 +380,6 @@ exports.userHasPermission = async function(user_uid, permission) {
} }
} }
exports.roleHasPermissions = async function(role, permission) {
const role_obj = await db_api.getRecord('roles', {key: role})
if (!role) {
logger.error(`Role ${role} does not exist!`);
}
const role_permissions = role_obj['permissions'];
if (role_permissions && role_permissions.includes(permission)) return true;
else return false;
}
exports.userPermissions = async function(user_uid) { exports.userPermissions = async function(user_uid) {
let user_permissions = []; let user_permissions = [];
const user_obj = await db_api.getRecord('users', ({uid: user_uid})); const user_obj = await db_api.getRecord('users', ({uid: user_uid}));

View File

@@ -55,18 +55,17 @@ async function getCategories() {
return categories ? categories : null; return categories ? categories : null;
} }
async function getCategoriesAsPlaylists() { async function getCategoriesAsPlaylists(files = null) {
const categories_as_playlists = []; const categories_as_playlists = [];
const available_categories = await getCategories(); const available_categories = await getCategories();
if (available_categories) { if (available_categories && files) {
for (let category of available_categories) { for (let category of available_categories) {
const files_that_match = await db_api.getRecords('files', {'category.uid': category['uid']}); const files_that_match = utils.addUIDsToCategory(category, files);
if (files_that_match && files_that_match.length > 0) { if (files_that_match && files_that_match.length > 0) {
category['thumbnailURL'] = files_that_match[0].thumbnailURL; category['thumbnailURL'] = files_that_match[0].thumbnailURL;
category['thumbnailPath'] = files_that_match[0].thumbnailPath; category['thumbnailPath'] = files_that_match[0].thumbnailPath;
category['duration'] = files_that_match.reduce((a, b) => a + utils.durationStringToNumber(b.duration), 0); category['duration'] = files_that_match.reduce((a, b) => a + utils.durationStringToNumber(b.duration), 0);
category['id'] = category['uid']; category['id'] = category['uid'];
category['auto'] = true;
categories_as_playlists.push(category); categories_as_playlists.push(category);
} }
} }

View File

@@ -127,7 +127,7 @@ function setConfigItem(key, value) {
success = setConfigFile(config_json); success = setConfigFile(config_json);
return success; return success;
} };
function setConfigItems(items) { function setConfigItems(items) {
let success = false; let success = false;
@@ -206,8 +206,7 @@ const DEFAULT_CONFIG = {
"use_youtube_API": false, "use_youtube_API": false,
"youtube_API_key": "", "youtube_API_key": "",
"use_twitch_API": false, "use_twitch_API": false,
"twitch_client_ID": "", "twitch_API_key": "",
"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
@@ -239,7 +238,7 @@ const DEFAULT_CONFIG = {
"mongodb_connection_string": "mongodb://127.0.0.1:27017/?compressors=zlib" "mongodb_connection_string": "mongodb://127.0.0.1:27017/?compressors=zlib"
}, },
"Advanced": { "Advanced": {
"default_downloader": "yt-dlp", "default_downloader": "youtube-dl",
"use_default_downloading_agent": true, "use_default_downloading_agent": true,
"custom_downloading_agent": "", "custom_downloading_agent": "",
"multi_user_mode": false, "multi_user_mode": false,

View File

@@ -102,13 +102,9 @@ exports.CONFIG_ITEMS = {
'key': 'ytdl_use_twitch_api', 'key': 'ytdl_use_twitch_api',
'path': 'YoutubeDLMaterial.API.use_twitch_API' 'path': 'YoutubeDLMaterial.API.use_twitch_API'
}, },
'ytdl_twitch_client_id': { 'ytdl_twitch_api_key': {
'key': 'ytdl_twitch_client_id', 'key': 'ytdl_twitch_api_key',
'path': 'YoutubeDLMaterial.API.twitch_client_ID' 'path': 'YoutubeDLMaterial.API.twitch_API_key'
},
'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',
@@ -221,8 +217,7 @@ exports.AVAILABLE_PERMISSIONS = [
'subscriptions', 'subscriptions',
'sharing', 'sharing',
'advanced_download', 'advanced_download',
'downloads_manager', 'downloads_manager'
'tasks_manager'
]; ];
exports.DETAILS_BIN_PATH = 'node_modules/youtube-dl/bin/details' exports.DETAILS_BIN_PATH = 'node_modules/youtube-dl/bin/details'
@@ -306,4 +301,4 @@ const YTDL_ARGS_WITH_VALUES = [
// 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.CURRENT_VERSION = 'v4.3'; exports.CURRENT_VERSION = 'v4.2';

View File

@@ -198,7 +198,7 @@ async function registerFileDBManual(file_object) {
path_object = path.parse(file_object['path']); path_object = path.parse(file_object['path']);
file_object['path'] = path.format(path_object); file_object['path'] = path.format(path_object);
await exports.insertRecordIntoTable('files', file_object, {path: file_object['path']}) exports.insertRecordIntoTable('files', file_object, {path: file_object['path']})
return file_object; return file_object;
} }
@@ -357,7 +357,7 @@ exports.addMetadataPropertyToDB = async (property_key) => {
} }
} }
exports.createPlaylist = async (playlist_name, uids, user_uid = null) => { exports.createPlaylist = async (playlist_name, uids, type, user_uid = null) => {
const first_video = await exports.getVideo(uids[0]); const first_video = await exports.getVideo(uids[0]);
const thumbnailToUse = first_video['thumbnailURL']; const thumbnailToUse = first_video['thumbnailURL'];
@@ -366,6 +366,7 @@ exports.createPlaylist = async (playlist_name, uids, user_uid = null) => {
uids: uids, uids: uids,
id: uuid(), id: uuid(),
thumbnailURL: thumbnailToUse, thumbnailURL: thumbnailToUse,
type: type,
registered: Date.now(), registered: Date.now(),
randomize_order: false randomize_order: false
}; };
@@ -386,9 +387,9 @@ exports.getPlaylist = async (playlist_id, user_uid = null, require_sharing = fal
if (!playlist) { if (!playlist) {
playlist = await exports.getRecord('categories', {uid: playlist_id}); playlist = await exports.getRecord('categories', {uid: playlist_id});
if (playlist) { if (playlist) {
const uids = (await exports.getRecords('files', {'category.uid': playlist_id})).map(file => file.uid); // category found
playlist['uids'] = uids; const files = await exports.getFiles(user_uid);
playlist['auto'] = true; utils.addUIDsToCategory(playlist, files);
} }
} }
@@ -494,7 +495,8 @@ exports.deleteFile = async (uid, uuid = null, blacklistMode = false) => {
let useYoutubeDLArchive = config_api.getConfigItem('ytdl_use_youtubedl_archive'); let useYoutubeDLArchive = config_api.getConfigItem('ytdl_use_youtubedl_archive');
if (useYoutubeDLArchive) { if (useYoutubeDLArchive) {
const archive_path = utils.getArchiveFolder(type, uuid); const usersFileFolder = config_api.getConfigItem('ytdl_users_base_path');
const archive_path = uuid ? path.join(usersFileFolder, uuid, 'archives', `archive_${type}.txt`) : path.join('appdata', 'archives', `archive_${type}.txt`);
// get ID from JSON // get ID from JSON
@@ -502,8 +504,14 @@ exports.deleteFile = async (uid, uuid = null, blacklistMode = false) => {
let id = null; let id = null;
if (jsonobj) id = jsonobj.id; if (jsonobj) id = jsonobj.id;
// Remove file ID from the archive file, and write it to the blacklist (if enabled) // use subscriptions API to remove video from the archive file, and write it to the blacklist
await utils.deleteFileFromArchive(uid, type, archive_path, id, blacklistMode); if (await fs.pathExists(archive_path)) {
const line = id ? await utils.removeIDFromArchive(archive_path, id) : null;
if (blacklistMode && line) await writeToBlacklist(type, line);
} else {
logger.info('Could not find archive file for audio files. Creating...');
await fs.close(await fs.open(archive_path, 'w'));
}
} }
if (jsonExists) await fs.unlink(jsonPath); if (jsonExists) await fs.unlink(jsonPath);
@@ -621,7 +629,7 @@ exports.bulkInsertRecordsIntoTable = async (table, docs) => {
exports.getRecord = async (table, filter_obj) => { exports.getRecord = async (table, filter_obj) => {
// local db override // local db override
if (using_local_db) { if (using_local_db) {
return exports.applyFilterLocalDB(local_db.get(table), filter_obj, 'find').value(); return applyFilterLocalDB(local_db.get(table), filter_obj, 'find').value();
} }
return await database.collection(table).findOne(filter_obj); return await database.collection(table).findOne(filter_obj);
@@ -630,7 +638,7 @@ exports.getRecord = async (table, filter_obj) => {
exports.getRecords = async (table, filter_obj = null, return_count = false, sort = null, range = null) => { exports.getRecords = async (table, filter_obj = null, return_count = false, sort = null, range = null) => {
// local db override // local db override
if (using_local_db) { if (using_local_db) {
let cursor = filter_obj ? exports.applyFilterLocalDB(local_db.get(table), filter_obj, 'filter').value() : local_db.get(table).value(); let cursor = filter_obj ? applyFilterLocalDB(local_db.get(table), filter_obj, 'filter').value() : local_db.get(table).value();
if (sort) { if (sort) {
cursor = cursor.sort((a, b) => (a[sort['by']] > b[sort['by']] ? sort['order'] : sort['order']*-1)); cursor = cursor.sort((a, b) => (a[sort['by']] > b[sort['by']] ? sort['order'] : sort['order']*-1));
} }
@@ -656,7 +664,7 @@ exports.getRecords = async (table, filter_obj = null, return_count = false, sort
exports.updateRecord = async (table, filter_obj, update_obj) => { exports.updateRecord = async (table, filter_obj, update_obj) => {
// local db override // local db override
if (using_local_db) { if (using_local_db) {
exports.applyFilterLocalDB(local_db.get(table), filter_obj, 'find').assign(update_obj).write(); applyFilterLocalDB(local_db.get(table), filter_obj, 'find').assign(update_obj).write();
return true; return true;
} }
@@ -669,7 +677,7 @@ exports.updateRecord = async (table, filter_obj, update_obj) => {
exports.updateRecords = async (table, filter_obj, update_obj) => { exports.updateRecords = async (table, filter_obj, update_obj) => {
// local db override // local db override
if (using_local_db) { if (using_local_db) {
exports.applyFilterLocalDB(local_db.get(table), filter_obj, 'filter').assign(update_obj).write(); applyFilterLocalDB(local_db.get(table), filter_obj, 'filter').assign(update_obj).write();
return true; return true;
} }
@@ -714,7 +722,7 @@ exports.bulkUpdateRecords = async (table, key_label, update_obj) => {
exports.pushToRecordsArray = async (table, filter_obj, key, value) => { exports.pushToRecordsArray = async (table, filter_obj, key, value) => {
// local db override // local db override
if (using_local_db) { if (using_local_db) {
exports.applyFilterLocalDB(local_db.get(table), filter_obj, 'find').get(key).push(value).write(); applyFilterLocalDB(local_db.get(table), filter_obj, 'find').get(key).push(value).write();
return true; return true;
} }
@@ -725,7 +733,7 @@ exports.pushToRecordsArray = async (table, filter_obj, key, value) => {
exports.pullFromRecordsArray = async (table, filter_obj, key, value) => { exports.pullFromRecordsArray = async (table, filter_obj, key, value) => {
// local db override // local db override
if (using_local_db) { if (using_local_db) {
exports.applyFilterLocalDB(local_db.get(table), filter_obj, 'find').get(key).pull(value).write(); applyFilterLocalDB(local_db.get(table), filter_obj, 'find').get(key).pull(value).write();
return true; return true;
} }
@@ -738,7 +746,7 @@ exports.pullFromRecordsArray = async (table, filter_obj, key, value) => {
exports.removeRecord = async (table, filter_obj) => { exports.removeRecord = async (table, filter_obj) => {
// local db override // local db override
if (using_local_db) { if (using_local_db) {
exports.applyFilterLocalDB(local_db.get(table), filter_obj, 'remove').write(); applyFilterLocalDB(local_db.get(table), filter_obj, 'remove').write();
return true; return true;
} }
@@ -749,7 +757,7 @@ exports.removeRecord = async (table, filter_obj) => {
// exports.removeRecordsByUIDBulk = async (table, uids) => { // exports.removeRecordsByUIDBulk = async (table, uids) => {
// // local db override // // local db override
// if (using_local_db) { // if (using_local_db) {
// exports.applyFilterLocalDB(local_db.get(table), filter_obj, 'remove').write(); // applyFilterLocalDB(local_db.get(table), filter_obj, 'remove').write();
// return true; // return true;
// } // }
@@ -813,7 +821,7 @@ exports.removeAllRecords = async (table = null, filter_obj = null) => {
if (using_local_db) { if (using_local_db) {
for (let i = 0; i < tables_to_remove.length; i++) { for (let i = 0; i < tables_to_remove.length; i++) {
const table_to_remove = tables_to_remove[i]; const table_to_remove = tables_to_remove[i];
if (filter_obj) exports.applyFilterLocalDB(local_db.get(table), filter_obj, 'remove').write(); if (filter_obj) applyFilterLocalDB(local_db.get(table), filter_obj, 'remove').write();
else local_db.assign({[table_to_remove]: []}).write(); else local_db.assign({[table_to_remove]: []}).write();
logger.debug(`Successfully removed records from ${table_to_remove}`); logger.debug(`Successfully removed records from ${table_to_remove}`);
} }
@@ -925,7 +933,6 @@ exports.importJSONToDB = async (db_json, users_json) => {
const createFilesRecords = (files, subscriptions) => { const createFilesRecords = (files, subscriptions) => {
for (let i = 0; i < subscriptions.length; i++) { for (let i = 0; i < subscriptions.length; i++) {
const subscription = subscriptions[i]; const subscription = subscriptions[i];
if (!subscription['videos']) continue;
subscription['videos'] = subscription['videos'].map(file => ({ ...file, sub_id: subscription['id'], user_uid: subscription['user_uid'] ? subscription['user_uid'] : undefined})); subscription['videos'] = subscription['videos'].map(file => ({ ...file, sub_id: subscription['id'], user_uid: subscription['user_uid'] ? subscription['user_uid'] : undefined}));
files = files.concat(subscriptions[i]['videos']); files = files.concat(subscriptions[i]['videos']);
} }
@@ -986,7 +993,7 @@ exports.backupDB = async () => {
const backup_file_name = `${using_local_db ? 'local' : 'remote'}_db.json.${Date.now()/1000}.bak`; const backup_file_name = `${using_local_db ? 'local' : 'remote'}_db.json.${Date.now()/1000}.bak`;
const path_to_backups = path.join(backup_dir, backup_file_name); const path_to_backups = path.join(backup_dir, backup_file_name);
logger.info(`Backing up ${using_local_db ? 'local' : 'remote'} DB to ${path_to_backups}`); logger.verbose(`Backing up ${using_local_db ? 'local' : 'remote'} DB to ${path_to_backups}`);
const table_to_records = {}; const table_to_records = {};
for (let i = 0; i < tables_list.length; i++) { for (let i = 0; i < tables_list.length; i++) {
@@ -1033,11 +1040,10 @@ exports.transferDB = async (local_to_remote) => {
table_to_records[table] = await exports.getRecords(table); table_to_records[table] = await exports.getRecords(table);
} }
logger.info('Backup up DB...');
await exports.backupDB(); // should backup always
using_local_db = !local_to_remote; using_local_db = !local_to_remote;
if (local_to_remote) { if (local_to_remote) {
logger.debug('Backup up DB...');
await exports.backupDB();
const db_connected = await exports.connectToDB(5, true); const db_connected = await exports.connectToDB(5, true);
if (!db_connected) { if (!db_connected) {
logger.error('Failed to transfer database - could not connect to MongoDB. Verify that your connection URL is valid.'); logger.error('Failed to transfer database - could not connect to MongoDB. Verify that your connection URL is valid.');
@@ -1069,13 +1075,8 @@ exports.transferDB = async (local_to_remote) => {
This function is necessary to emulate mongodb's ability to search for null or missing values. This function is necessary to emulate mongodb's ability to search for null or missing values.
A filter of null or undefined for a property will find docs that have that property missing, or have it A filter of null or undefined for a property will find docs that have that property missing, or have it
null or undefined. We want that same functionality for the local DB as well null or undefined. We want that same functionality for the local DB as well
error: {$ne: null}
^ ^
| |
filter_prop filter_prop_value
*/ */
exports.applyFilterLocalDB = (db_path, filter_obj, operation) => { const applyFilterLocalDB = (db_path, filter_obj, operation) => {
const filter_props = Object.keys(filter_obj); const filter_props = Object.keys(filter_obj);
const return_val = db_path[operation](record => { const return_val = db_path[operation](record => {
if (!filter_props) return true; if (!filter_props) return true;
@@ -1084,19 +1085,13 @@ exports.applyFilterLocalDB = (db_path, filter_obj, operation) => {
const filter_prop = filter_props[i]; const filter_prop = filter_props[i];
const filter_prop_value = filter_obj[filter_prop]; const filter_prop_value = filter_obj[filter_prop];
if (filter_prop_value === undefined || filter_prop_value === null) { if (filter_prop_value === undefined || filter_prop_value === null) {
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 ('$regex' in filter_prop_value) { if (filter_prop_value['$regex']) {
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) {
filtered &= filter_prop in record && record[filter_prop] !== filter_prop_value['$ne'];
} }
} else { } else {
// handle case of nested property check
if (filter_prop.includes('.'))
filtered &= utils.searchObjectByString(record, filter_prop) === filter_prop_value;
else
filtered &= record[filter_prop] === filter_prop_value; filtered &= record[filter_prop] === filter_prop_value;
} }
} }
@@ -1105,3 +1100,15 @@ exports.applyFilterLocalDB = (db_path, filter_obj, operation) => {
}); });
return return_val; return return_val;
} }
// archive helper functions
async function writeToBlacklist(type, line) {
const archivePath = path.join(__dirname, 'appdata', 'archives');
let blacklistPath = path.join(archivePath, (type === 'audio') ? 'blacklist_audio.txt' : 'blacklist_video.txt');
// adds newline to the beginning of the line
line.replace('\n', '');
line.replace('\r', '');
line = '\n' + line;
await fs.appendFile(blacklistPath, line);
}

View File

@@ -18,6 +18,8 @@ const db_api = require('./db');
const mutex = new Mutex(); const mutex = new Mutex();
let should_check_downloads = true; let should_check_downloads = true;
const archivePath = path.join(__dirname, 'appdata', 'archives');
if (db_api.database_initialized) { if (db_api.database_initialized) {
setupDownloads(); setupDownloads();
} else { } else {
@@ -26,7 +28,7 @@ if (db_api.database_initialized) {
}); });
} }
exports.createDownload = async (url, type, options, user_uid = null, sub_id = null, sub_name = null, prefetched_info = null) => { exports.createDownload = async (url, type, options, user_uid = null, sub_id = null, sub_name = null) => {
return await mutex.runExclusive(async () => { return await mutex.runExclusive(async () => {
const download = { const download = {
url: url, url: url,
@@ -35,7 +37,6 @@ exports.createDownload = async (url, type, options, user_uid = null, sub_id = nu
user_uid: user_uid, user_uid: user_uid,
sub_id: sub_id, sub_id: sub_id,
sub_name: sub_name, sub_name: sub_name,
prefetched_info: prefetched_info,
options: options, options: options,
uid: uuid(), uid: uuid(),
step_index: 0, step_index: 0,
@@ -107,7 +108,6 @@ exports.clearDownload = async (download_uid) => {
} }
async function handleDownloadError(download_uid, error_message) { async function handleDownloadError(download_uid, error_message) {
if (!download_uid) return;
await db_api.updateRecord('download_queue', {uid: download_uid}, {error: error_message, finished: true, running: false}); await db_api.updateRecord('download_queue', {uid: download_uid}, {error: error_message, finished: true, running: false});
} }
@@ -186,7 +186,7 @@ async function collectInfo(download_uid) {
let args = await exports.generateArgs(url, type, options, download['user_uid']); let args = await exports.generateArgs(url, type, options, download['user_uid']);
// get video info prior to download // get video info prior to download
let info = download['prefetched_info'] ? download['prefetched_info'] : await exports.getVideoInfoByURL(url, args, download_uid); let info = await getVideoInfoByURL(url, args, download_uid);
if (!info) { if (!info) {
// info failed, error presumably already recorded // info failed, error presumably already recorded
@@ -203,12 +203,9 @@ async function collectInfo(download_uid) {
options.customOutput = category['custom_output']; options.customOutput = category['custom_output'];
options.noRelativePath = true; options.noRelativePath = true;
args = await exports.generateArgs(url, type, options, download['user_uid']); args = await exports.generateArgs(url, type, options, download['user_uid']);
args = utils.filterArgs(args, ['--no-simulate']); info = await getVideoInfoByURL(url, args, download_uid);
info = await exports.getVideoInfoByURL(url, args, download_uid);
} }
download['category'] = category;
// setup info required to calculate download progress // setup info required to calculate download progress
const expected_file_size = utils.getExpectedFileSize(info); const expected_file_size = utils.getExpectedFileSize(info);
@@ -229,8 +226,7 @@ async function collectInfo(download_uid) {
options: options, options: options,
files_to_check_for_progress: files_to_check_for_progress, files_to_check_for_progress: files_to_check_for_progress,
expected_file_size: expected_file_size, expected_file_size: expected_file_size,
title: playlist_title ? playlist_title : info['title'], title: playlist_title ? playlist_title : info['title']
prefetched_info: null
}); });
} }
@@ -243,7 +239,6 @@ async function downloadQueuedFile(download_uid) {
return new Promise(async resolve => { return new Promise(async resolve => {
const audioFolderPath = config_api.getConfigItem('ytdl_audio_folder_path'); const audioFolderPath = config_api.getConfigItem('ytdl_audio_folder_path');
const videoFolderPath = config_api.getConfigItem('ytdl_video_folder_path'); const videoFolderPath = config_api.getConfigItem('ytdl_video_folder_path');
const usersFolderPath = config_api.getConfigItem('ytdl_users_base_path');
await db_api.updateRecord('download_queue', {uid: download_uid}, {step_index: 2, finished_step: false, running: true}); await db_api.updateRecord('download_queue', {uid: download_uid}, {step_index: 2, finished_step: false, running: true});
const url = download['url']; const url = download['url'];
@@ -251,11 +246,9 @@ async function downloadQueuedFile(download_uid) {
const options = download['options']; const options = download['options'];
const args = download['args']; const args = download['args'];
const category = download['category']; const category = download['category'];
let fileFolderPath = type === 'audio' ? audioFolderPath : videoFolderPath; let fileFolderPath = type === 'audio' ? audioFolderPath : videoFolderPath; // TODO: fix
if (options.customFileFolderPath) { if (options.customFileFolderPath) {
fileFolderPath = options.customFileFolderPath; fileFolderPath = options.customFileFolderPath;
} else if (download['user_uid']) {
fileFolderPath = path.join(usersFolderPath, download['user_uid'], type);
} }
fs.ensureDirSync(fileFolderPath); fs.ensureDirSync(fileFolderPath);
@@ -357,7 +350,7 @@ async function downloadQueuedFile(download_uid) {
if (file_objs.length > 1) { if (file_objs.length > 1) {
// create playlist // create playlist
const playlist_name = file_objs.map(file_obj => file_obj.title).join(', '); const playlist_name = file_objs.map(file_obj => file_obj.title).join(', ');
container = await db_api.createPlaylist(playlist_name, file_objs.map(file_obj => file_obj.uid), download['user_uid']); container = await db_api.createPlaylist(playlist_name, file_objs.map(file_obj => file_obj.uid), type, download['user_uid']);
} else if (file_objs.length === 1) { } else if (file_objs.length === 1) {
container = file_objs[0]; container = file_objs[0];
} else { } else {
@@ -377,23 +370,15 @@ async function downloadQueuedFile(download_uid) {
// helper functions // helper functions
exports.generateArgs = async (url, type, options, user_uid = null, simulated = false) => { exports.generateArgs = async (url, type, options, user_uid = null, simulated = false) => {
const default_downloader = utils.getCurrentDownloader() || config_api.getConfigItem('ytdl_default_downloader');
const audioFolderPath = config_api.getConfigItem('ytdl_audio_folder_path'); const audioFolderPath = config_api.getConfigItem('ytdl_audio_folder_path');
const videoFolderPath = config_api.getConfigItem('ytdl_video_folder_path'); const videoFolderPath = config_api.getConfigItem('ytdl_video_folder_path');
const usersFolderPath = config_api.getConfigItem('ytdl_users_base_path');
const videopath = config_api.getConfigItem('ytdl_default_file_output') ? config_api.getConfigItem('ytdl_default_file_output') : '%(title)s'; const videopath = config_api.getConfigItem('ytdl_default_file_output') ? config_api.getConfigItem('ytdl_default_file_output') : '%(title)s';
const globalArgs = config_api.getConfigItem('ytdl_custom_args'); const globalArgs = config_api.getConfigItem('ytdl_custom_args');
const useCookies = config_api.getConfigItem('ytdl_use_cookies'); const useCookies = config_api.getConfigItem('ytdl_use_cookies');
const is_audio = type === 'audio'; const is_audio = type === 'audio';
let fileFolderPath = type === 'audio' ? audioFolderPath : videoFolderPath; // TODO: fix let fileFolderPath = is_audio ? audioFolderPath : videoFolderPath;
if (options.customFileFolderPath) {
fileFolderPath = options.customFileFolderPath;
} else if (user_uid) {
fileFolderPath = path.join(usersFolderPath, user_uid, fileFolderPath);
}
if (options.customFileFolderPath) fileFolderPath = options.customFileFolderPath; if (options.customFileFolderPath) fileFolderPath = options.customFileFolderPath;
@@ -403,8 +388,6 @@ exports.generateArgs = async (url, type, options, user_uid = null, simulated = f
// video-specific args // video-specific args
const selectedHeight = options.selectedHeight; const selectedHeight = options.selectedHeight;
const maxHeight = options.maxHeight;
const heightParam = selectedHeight || maxHeight;
// audio-specific args // audio-specific args
const maxBitrate = options.maxBitrate; const maxBitrate = options.maxBitrate;
@@ -418,6 +401,8 @@ exports.generateArgs = async (url, type, options, user_uid = null, simulated = f
if (!is_audio && !is_youtube) { if (!is_audio && !is_youtube) {
// tiktok videos fail when using the default format // tiktok videos fail when using the default format
qualityPath = null; qualityPath = null;
} else if (!is_audio && !is_youtube && (url.includes('reddit') || url.includes('pornhub'))) {
qualityPath = ['-f', 'bestvideo+bestaudio']
} }
if (customArgs) { if (customArgs) {
@@ -425,8 +410,8 @@ exports.generateArgs = async (url, type, options, user_uid = null, simulated = f
} else { } else {
if (customQualityConfiguration) { if (customQualityConfiguration) {
qualityPath = ['-f', customQualityConfiguration, '--merge-output-format', 'mp4']; qualityPath = ['-f', customQualityConfiguration, '--merge-output-format', 'mp4'];
} else if (heightParam && heightParam !== '' && !is_audio) { } else if (selectedHeight && selectedHeight !== '' && !is_audio) {
qualityPath = ['-f', `'(mp4)[height${maxHeight ? '<' : ''}=${heightParam}]`]; qualityPath = ['-f', `'(mp4)[height=${selectedHeight}'`];
} else if (is_audio) { } else if (is_audio) {
qualityPath = ['--audio-quality', maxBitrate ? maxBitrate : '0'] qualityPath = ['--audio-quality', maxBitrate ? maxBitrate : '0']
} }
@@ -508,11 +493,9 @@ exports.generateArgs = async (url, type, options, user_uid = null, simulated = f
downloadConfig.push('-r', rate_limit); downloadConfig.push('-r', rate_limit);
} }
const default_downloader = utils.getCurrentDownloader() || config_api.getConfigItem('ytdl_default_downloader');
if (default_downloader === 'yt-dlp') { if (default_downloader === 'yt-dlp') {
downloadConfig = utils.filterArgs(downloadConfig, ['--print-json']); downloadConfig.push('--no-clean-infojson');
// in yt-dlp -j --no-simulate is preferable
downloadConfig.push('--no-clean-info-json', '-j', '--no-simulate');
} }
} }
@@ -520,11 +503,11 @@ exports.generateArgs = async (url, type, options, user_uid = null, simulated = f
// filter out incompatible args // filter out incompatible args
downloadConfig = filterArgs(downloadConfig, is_audio); downloadConfig = filterArgs(downloadConfig, is_audio);
if (!simulated) logger.verbose(`${default_downloader} args being used: ${downloadConfig.join(',')}`); if (!simulated) logger.verbose(`youtube-dl args being used: ${downloadConfig.join(',')}`);
return downloadConfig; return downloadConfig;
} }
exports.getVideoInfoByURL = async (url, args = [], download_uid = null) => { async function getVideoInfoByURL(url, args = [], download_uid = null) {
return new Promise(resolve => { return new Promise(resolve => {
// remove bad args // remove bad args
const new_args = [...args]; const new_args = [...args];
@@ -579,7 +562,8 @@ exports.getVideoInfoByURL = async (url, args = [], download_uid = null) => {
function filterArgs(args, isAudio) { function filterArgs(args, isAudio) {
const video_only_args = ['--add-metadata', '--embed-subs', '--xattrs']; const video_only_args = ['--add-metadata', '--embed-subs', '--xattrs'];
const audio_only_args = ['-x', '--extract-audio', '--embed-thumbnail']; const audio_only_args = ['-x', '--extract-audio', '--embed-thumbnail'];
return utils.filterArgs(args, isAudio ? video_only_args : audio_only_args); const args_to_remove = isAudio ? video_only_args : audio_only_args;
return args.filter(x => !args_to_remove.includes(x));
} }
async function checkDownloadPercent(download_uid) { async function checkDownloadPercent(download_uid) {
@@ -641,6 +625,6 @@ function getArchiveFolder(fileFolderPath, options, user_uid) {
} else if (user_uid) { } else if (user_uid) {
return path.join(fileFolderPath, 'archives'); return path.join(fileFolderPath, 'archives');
} else { } else {
return path.join('appdata', 'archives'); return path.join(archivePath);
} }
} }

View File

@@ -1,7 +1,7 @@
#!/bin/sh #!/bin/sh
set -eu set -eu
CMD="npm start" CMD="pm2-runtime pm2.config.js"
# if the first arg starts with "-" pass it to program # if the first arg starts with "-" pass it to program
if [ "${1#-}" != "$1" ]; then if [ "${1#-}" != "$1" ]; then

View File

@@ -1,4 +1,4 @@
#!/bin/bash #!/bin/sh
# INTERACTIVE PERMISSIONS FIX SCRIPT FOR YTDL-M # INTERACTIVE PERMISSIONS FIX SCRIPT FOR YTDL-M
# Date: 2022-05-03 # Date: 2022-05-03
@@ -6,7 +6,8 @@
# If you want to run this script on a bare-metal installation instead of within Docker # If you want to run this script on a bare-metal installation instead of within Docker
# make sure that the paths configured below match your paths! (it's wise to use the full paths) # make sure that the paths configured below match your paths! (it's wise to use the full paths)
# USAGE: within your container's bash shell: # USAGE: within your container's bash shell:
# ./fix-scripts/<name of fix-script> # chmod -R +x ./fix-scripts/
# ./fix-scripts/001-fix_download_permissions.sh
# User defines / Docker env defaults # User defines / Docker env defaults
PATH_SUBS=/app/subscriptions PATH_SUBS=/app/subscriptions

View File

@@ -1,142 +0,0 @@
#!/bin/bash
# INTERACTIVE ARCHIVE-DUPE-ENTRY FIX SCRIPT FOR YTDL-M
# Date: 2022-05-09
# If you want to run this script on a bare-metal installation instead of within Docker
# make sure that the paths configured below match your paths! (it's wise to use the full paths)
# USAGE: within your container's bash shell:
# ./fix-scripts/<name of fix-script>
# User defines (NO TRAILING SLASHES) / Docker env defaults
PATH_SUBSARCHIVE=/app/subscriptions/archives
PATH_ONEOFFARCHIVE=/app/appdata/archives
# Backup paths (substitute with your personal preference if you like)
PATH_SUBSARCHIVEBKP=$PATH_SUBSARCHIVE-BKP-$(date +%Y%m%d%H%M%S)
PATH_ONEOFFARCHIVEBKP=$PATH_ONEOFFARCHIVE-BKP-$(date +%Y%m%d%H%M%S)
# Define Colors for TUI
yellow=$(tput setaf 3)
normal=$(tput sgr0)
tput civis # hide the cursor
clear -x
printf "\n"
printf '%*s\n' "${COLUMNS:-$(tput cols)}" '' | tr ' ' - # horizontal line
printf "Welcome to the INTERACTIVE ARCHIVE-DUPE-ENTRY FIX SCRIPT FOR YTDL-M."
printf "\nThis script will cycle through the archive files in the folders mentioned"
printf "\nbelow and remove within each archive the dupe entries. (compact them)"
printf "\nDuring some older builds of YTDL-M the archives could receive dupe"
printf "\nentries and blow up in size, sometimes causing conflicts with download management."
printf '\n%*s' "${COLUMNS:-$(tput cols)}" '' | tr ' ' - # horizontal line
printf "\n"
# check whether dirs exist
i=0
[ -d $PATH_SUBSARCHIVE ] && i=$((i+1)) && printf "\n✔ (${i}/2) Found Subscriptions archive directory at ${PATH_SUBSARCHIVE}"
[ -d $PATH_ONEOFFARCHIVE ] && i=$((i+1)) && printf "\n✔ (${i}/2) Found one-off archive directory at ${PATH_ONEOFFARCHIVE}"
# Ask to proceed or cancel, exit on missing paths
case $i in
0)
printf "\n\n Couldn't find any archive location path! \n\nPlease edit this script to configure!"
tput cnorm
exit 2;;
2)
printf "\n\n Found all archive locations. \n\nProceed? (Y/N)";;
*)
printf "\n\n Only found ${i} out of 2 archive locations! Something about this script's config must be wrong. \n\nProceed anyways? (Y/N)";;
esac
old_stty_cfg=$(stty -g)
stty raw -echo ; answer=$(head -c 1) ; stty $old_stty_cfg # Careful playing with stty
if echo "$answer" | grep -iq "^y" ;then
printf "\n\nRunning jobs now... (this may take a while)\n"
printf "\nBacking up directories...\n"
chars="⣾⣽⣻⢿⡿⣟⣯⣷"
cp -R $PATH_SUBSARCHIVE $PATH_SUBSARCHIVEBKP &
PID=$!
i=1
echo -n ' '
while [ -d /proc/$PID ]
do
printf "${yellow}\b${chars:i++%${#chars}:1}${normal}"
sleep 0.15
done
[ -d $PATH_SUBSARCHIVEBKP ] && printf "\r✔ Backed up ${PATH_SUBSARCHIVE} to ${PATH_SUBSARCHIVEBKP} ($(du -sh $PATH_SUBSARCHIVEBKP | cut -f1))\n"
cp -R $PATH_ONEOFFARCHIVE $PATH_ONEOFFARCHIVEBKP &
PID2=$!
i=1
echo -n ' '
while [ -d /proc/$PID2 ]
do
printf "${yellow}\b${chars:i++%${#chars}:1}${normal}"
sleep 0.1
done
[ -d $PATH_ONEOFFARCHIVEBKP ] && printf "\r✔ Backed up ${PATH_ONEOFFARCHIVE} to ${PATH_ONEOFFARCHIVEBKP} ($(du -sh $PATH_ONEOFFARCHIVEBKP | cut -f1))\n"
printf "\nCompacting files...\n"
tmpfile=$(mktemp) &&
[ -d $PATH_SUBSARCHIVE ] &&
find $PATH_SUBSARCHIVE -name '*.txt' -print0 | while read -d $'\0' file # Set delimiter to null because we want to catch all possible filenames (WE CANNOT CHANGE IFS HERE) - https://stackoverflow.com/a/15931055
do
cp "$file" "$tmpfile"
{ awk '!x[$0]++' "$tmpfile" > "$file"; } & # https://unix.stackexchange.com/questions/159695/how-does-awk-a0-work
PID3=$!
i=1
echo -n ''
while [ -d /proc/$PID3 ]
do
printf "${yellow}\b${chars:i++%${#chars}:1}${normal}"
sleep 0.1
done
BEFORE=$(wc -l < $tmpfile)
AFTER=$(wc -l < $file)
if [[ "$AFTER" -ne "$BEFORE" ]]; then
printf "\b✔ Compacted down to ${AFTER} lines from ${BEFORE}: ${file}\n"
else
printf "\b No action needed for file: ${file}\n"
fi
done
[ -d $PATH_ONEOFFARCHIVE ] &&
find $PATH_ONEOFFARCHIVE -name '*.txt' -print0 | while read -d $'\0' file
do
cp "$file" "$tmpfile" &
awk '!x[$0]++' "$tmpfile" > "$file" &
PID4=$!
i=1
echo -n ''
while [ -d /proc/$PID4 ]
do
printf "${yellow}\b${chars:i++%${#chars}:1}${normal}"
sleep 0.1
done
BEFORE=$(wc -l < $tmpfile)
AFTER=$(wc -l < $file)
if [ "$BEFORE" -ne "$AFTER" ]; then
printf "\b✔ Compacted down to ${AFTER} lines from ${BEFORE}: ${file}\n"
else
printf "\b No action ran for file: ${file}\n"
fi
done
tput cnorm # show the cursor
rm "$tmpfile"
printf "\n\n✔ Done."
printf "\n Please keep in mind that you may still want to"
printf "\n run corruption checks against your archives!\n\n"
exit
else
tput cnorm
printf "\nOkay, bye.\n\n"
exit
fi

1825
backend/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -5,9 +5,20 @@
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
"test": "echo \"Error: no test specified\" && exit 1", "test": "echo \"Error: no test specified\" && exit 1",
"start": "pm2-runtime --raw pm2.config.js", "start": "nodemon app.js",
"debug": "set YTDL_MODE=debug && node app.js" "debug": "set YTDL_MODE=debug && node app.js"
}, },
"nodemonConfig": {
"ignore": [
"*.js",
"appdata/*",
"public/*"
],
"watch": [
"restart_update.json",
"restart_general.json"
]
},
"repository": { "repository": {
"type": "git", "type": "git",
"url": "" "url": ""
@@ -36,10 +47,11 @@
"mocha": "^9.2.2", "mocha": "^9.2.2",
"moment": "^2.29.2", "moment": "^2.29.2",
"mongodb": "^3.6.9", "mongodb": "^3.6.9",
"multer": "1.4.5-lts.1", "multer": "^1.4.2",
"node-fetch": "^2.6.7", "node-fetch": "^2.6.7",
"node-id3": "^0.1.14", "node-id3": "^0.1.14",
"node-schedule": "^2.1.0", "node-schedule": "^2.1.0",
"nodemon": "^2.0.7",
"passport": "^0.4.1", "passport": "^0.4.1",
"passport-http": "^0.3.0", "passport-http": "^0.3.0",
"passport-jwt": "^4.0.0", "passport-jwt": "^4.0.0",

View File

@@ -178,7 +178,7 @@ async function deleteSubscriptionFile(sub, file, deleteForever, file_uid = null,
]); ]);
if (jsonExists) { if (jsonExists) {
retrievedID = fs.readJSONSync(jsonPath)['id']; retrievedID = JSON.parse(await fs.readFile(jsonPath, 'utf8'))['id'];
await fs.unlink(jsonPath); await fs.unlink(jsonPath);
} }
@@ -196,11 +196,12 @@ async function deleteSubscriptionFile(sub, file, deleteForever, file_uid = null,
return false; return false;
} else { } else {
// check if the user wants the video to be redownloaded (deleteForever === false) // check if the user wants the video to be redownloaded (deleteForever === false)
if (useArchive && retrievedID) { if (!deleteForever && useArchive && sub.archive && retrievedID) {
const archive_path = utils.getArchiveFolder(sub.type, user_uid, sub); const archive_path = path.join(sub.archive, 'archive.txt')
// if archive exists, remove line with video ID
// Remove file ID from the archive file, and write it to the blacklist (if enabled) if (await fs.pathExists(archive_path)) {
await utils.deleteFileFromArchive(file_uid, sub.type, archive_path, retrievedID, deleteForever); utils.removeIDFromArchive(archive_path, retrievedID);
}
} }
return true; return true;
} }
@@ -241,32 +242,31 @@ async function getVideosForSub(sub, user_uid = null) {
logger.verbose('Subscription: finished check for ' + sub.name); logger.verbose('Subscription: finished check for ' + sub.name);
if (err && !output) { if (err && !output) {
logger.error(err.stderr ? err.stderr : err.message); logger.error(err.stderr ? err.stderr : err.message);
if (err.stderr.includes('This video is unavailable') || err.stderr.includes('Private video')) { if (err.stderr.includes('This video is unavailable')) {
logger.info('An error was encountered with at least one video, backup method will be used.') logger.info('An error was encountered with at least one video, backup method will be used.')
try { try {
const outputs = err.stdout.split(/\r\n|\r|\n/); // TODO: reimplement
const files_to_download = await handleOutputJSON(outputs, sub, user_uid);
resolve(files_to_download); // const outputs = err.stdout.split(/\r\n|\r|\n/);
// for (let i = 0; i < outputs.length; i++) {
// const output = JSON.parse(outputs[i]);
// await handleOutputJSON(sub, output, i === 0, multiUserMode)
// if (err.stderr.includes(output['id']) && archive_path) {
// // we found a video that errored! add it to the archive to prevent future errors
// if (sub.archive) {
// archive_dir = sub.archive;
// archive_path = path.join(archive_dir, 'archive.txt')
// fs.appendFileSync(archive_path, output['id']);
// }
// }
// }
} catch(e) { } catch(e) {
logger.error('Backup method failed. See error below:'); logger.error('Backup method failed. See error below:');
logger.error(e); logger.error(e);
} }
} else {
logger.error('Subscription check failed!');
} }
resolve(false); resolve(false);
} else if (output) { } else if (output) {
const files_to_download = await handleOutputJSON(output, sub, user_uid);
resolve(files_to_download);
}
});
}, err => {
logger.error(err);
updateSubscriptionProperty(sub, {downloading: false}, user_uid);
});
}
async function handleOutputJSON(output, sub, user_uid) {
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);
@@ -274,7 +274,8 @@ async function handleOutputJSON(output, sub, user_uid) {
if (output.length === 0 || (output.length === 1 && output[0] === '')) { if (output.length === 0 || (output.length === 1 && output[0] === '')) {
logger.verbose('No additional videos to download for ' + sub.name); logger.verbose('No additional videos to download for ' + sub.name);
return []; resolve(true);
return;
} }
const output_jsons = []; const output_jsons = [];
@@ -296,11 +297,16 @@ async function handleOutputJSON(output, 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 await downloader_api.createDownload(file_to_download['webpage_url'], sub.type || 'video', base_download_options, user_uid, sub.id, sub.name);
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; resolve(files_to_download);
}
});
}, err => {
logger.error(err);
updateSubscriptionProperty(sub, {downloading: false}, user_uid);
});
} }
function generateOptionsForSubscriptionDownload(sub, user_uid) { function generateOptionsForSubscriptionDownload(sub, user_uid) {
@@ -313,10 +319,10 @@ function generateOptionsForSubscriptionDownload(sub, user_uid) {
let default_output = config_api.getConfigItem('ytdl_default_file_output') ? config_api.getConfigItem('ytdl_default_file_output') : '%(title)s'; let default_output = config_api.getConfigItem('ytdl_default_file_output') ? config_api.getConfigItem('ytdl_default_file_output') : '%(title)s';
const base_download_options = { const base_download_options = {
maxHeight: sub.maxQuality && sub.maxQuality !== 'best' ? sub.maxQuality : null, selectedHeight: sub.maxQuality && sub.maxQuality !== 'best' ? sub.maxQuality : null,
customFileFolderPath: getAppendedBasePath(sub, basePath), customFileFolderPath: getAppendedBasePath(sub, basePath),
customOutput: sub.custom_output ? `${sub.custom_output}` : `${default_output}`, customOutput: sub.custom_output ? `${sub.custom_output}` : `${default_output}`,
customArchivePath: path.join(basePath, 'archives', sub.name), customArchivePath: path.join(__dirname, basePath, 'archives', sub.name),
additionalArgs: sub.custom_args additionalArgs: sub.custom_args
} }
@@ -383,6 +389,11 @@ async function generateArgsForSubscription(sub, user_uid, redownload = false, de
downloadConfig.push('--download-archive', archive_path); downloadConfig.push('--download-archive', archive_path);
} }
// if streaming only mode, just get the list of videos
if (sub.streamingOnly) {
downloadConfig = ['-f', 'best', '--dump-json'];
}
if (sub.timerange && !redownload) { if (sub.timerange && !redownload) {
downloadConfig.push('--dateafter', sub.timerange); downloadConfig.push('--dateafter', sub.timerange);
} }
@@ -407,11 +418,9 @@ async function generateArgsForSubscription(sub, user_uid, redownload = false, de
const default_downloader = utils.getCurrentDownloader() || config_api.getConfigItem('ytdl_default_downloader'); const default_downloader = utils.getCurrentDownloader() || config_api.getConfigItem('ytdl_default_downloader');
if (default_downloader === 'yt-dlp') { if (default_downloader === 'yt-dlp') {
downloadConfig.push('--no-clean-info-json'); downloadConfig.push('--no-clean-infojson');
} }
downloadConfig = utils.filterArgs(downloadConfig, ['--write-comments']);
return downloadConfig; return downloadConfig;
} }
@@ -458,7 +467,7 @@ async function updateSubscription(sub) {
async function updateSubscriptionPropertyMultiple(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, sub.user_uid);
}); });
} }
@@ -470,7 +479,6 @@ async function updateSubscriptionProperty(sub, assignment_obj) {
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;
const current_date = new Date().toISOString().split('T')[0].replace(/-/g, ''); const current_date = new Date().toISOString().split('T')[0].replace(/-/g, '');
sub_files.forEach(async file => { sub_files.forEach(async file => {
if (current_date === file['upload_date'].replace(/-/g, '')) { if (current_date === file['upload_date'].replace(/-/g, '')) {

View File

@@ -148,7 +148,6 @@ exports.updateTaskSchedule = async (task_key, schedule) => {
await db_api.updateRecord('tasks', {key: task_key}, {schedule: schedule}); await db_api.updateRecord('tasks', {key: task_key}, {schedule: schedule});
if (TASKS[task_key]['job']) { if (TASKS[task_key]['job']) {
TASKS[task_key]['job'].cancel(); TASKS[task_key]['job'].cancel();
TASKS[task_key]['job'] = null;
} }
if (schedule) { if (schedule) {
TASKS[task_key]['job'] = scheduleJob(task_key, schedule); TASKS[task_key]['job'] = scheduleJob(task_key, schedule);

View File

@@ -1,7 +1,6 @@
const assert = require('assert'); var assert = require('assert');
const low = require('lowdb') const low = require('lowdb')
const winston = require('winston'); var winston = require('winston');
const path = require('path');
process.chdir('./backend') process.chdir('./backend')
@@ -40,29 +39,9 @@ const utils = require('../utils');
const subscriptions_api = require('../subscriptions'); const subscriptions_api = require('../subscriptions');
const fs = require('fs-extra'); const fs = require('fs-extra');
const { uuid } = require('uuidv4'); const { uuid } = require('uuidv4');
const NodeID3 = require('node-id3');
db_api.initialize(db, users_db); db_api.initialize(db, users_db);
const sample_video_json = {
id: "Sample Video",
title: "Sample Video",
thumbnailURL: "https://sampleurl.jpg",
isAudio: false,
duration: 177.413,
url: "sampleurl.com",
uploader: "Sample Uploader",
size: 2838445,
path: "users\\admin\\video\\Sample Video.mp4",
upload_date: "2017-07-28",
description: null,
view_count: 230,
abr: 128,
thumbnailPath: null,
user_uid: "admin",
uid: "1ada04ab-2773-4dd4-bbdd-3e2d40761c50",
registered: 1628469039377
}
describe('Database', async function() { describe('Database', async function() {
describe('Import', async function() { describe('Import', async function() {
@@ -235,7 +214,7 @@ describe('Database', async function() {
for (let i = 0; i < NUM_RECORDS_TO_ADD; i++) { for (let i = 0; i < NUM_RECORDS_TO_ADD; i++) {
const uid = uuid(); const uid = uuid();
if (i === NUM_RECORDS_TO_ADD/2) random_uid = uid; if (i === NUM_RECORDS_TO_ADD/2) random_uid = uid;
test_records.push({"id":"RandomTextRandomText","title":"RandomTextRandomTextRandomTextRandomTextRandomTextRandomTextRandomTextRandomText","thumbnailURL":"https://i.ytimg.com/vi/randomurl/maxresdefault.jpg","isAudio":true,"duration":312,"url":"https://www.youtube.com/watch?v=randomvideo","uploader":"randomUploader","size":5060157,"path":"audio\\RandomTextRandomText.mp3","upload_date":"2016-05-11","description":"RandomTextRandomTextRandomTextRandomTextRandomTextRandomTextRandomTextRandomTextRandomTextRandomTextRandomTextRandomText","view_count":118689353,"height":null,"abr":160,"uid": uid,"registered":1626672120632}); test_records.push({"id":"A$AP Mob - Yamborghini High (Official Music Video) ft. Juicy J","title":"A$AP Mob - Yamborghini High (Official Music Video) ft. Juicy J","thumbnailURL":"https://i.ytimg.com/vi/tt7gP_IW-1w/maxresdefault.jpg","isAudio":true,"duration":312,"url":"https://www.youtube.com/watch?v=tt7gP_IW-1w","uploader":"asapmobVEVO","size":5060157,"path":"audio\\A$AP Mob - Yamborghini High (Official Music Video) ft. Juicy J.mp3","upload_date":"2016-05-11","description":"A$AP Mob ft. Juicy J - \"Yamborghini High\" Get it now on:\niTunes: http://smarturl.it/iYAMH?IQid=yt\nListen on Spotify: http://smarturl.it/sYAMH?IQid=yt\nGoogle Play: http://smarturl.it/gYAMH?IQid=yt\nAmazon: http://smarturl.it/aYAMH?IQid=yt\n\nFollow A$AP Mob:\nhttps://www.facebook.com/asapmobofficial\nhttps://twitter.com/ASAPMOB\nhttp://instagram.com/asapmob \nhttp://www.asapmob.com/\n\n#AsapMob #YamborghiniHigh #Vevo #HipHop #OfficialMusicVideo #JuicyJ","view_count":118689353,"height":null,"abr":160,"uid": uid,"registered":1626672120632});
} }
const insert_start = Date.now(); const insert_start = Date.now();
let success = await db_api.bulkInsertRecordsIntoTable('test', test_records); let success = await db_api.bulkInsertRecordsIntoTable('test', test_records);
@@ -256,30 +235,6 @@ describe('Database', async function() {
assert(success); assert(success);
}); });
}); });
describe('Local DB Filters', async function() {
it('Basic', async function() {
const result = db_api.applyFilterLocalDB([{test: 'test'}, {test: 'test1'}], {test: 'test'}, 'find');
assert(result && result['test'] === 'test');
});
it('Regex', async function() {
const filter = {$regex: `\\w+\\d`, $options: 'i'};
const result = db_api.applyFilterLocalDB([{test: 'test'}, {test: 'test1'}], {test: filter}, 'find');
assert(result && result['test'] === 'test1');
});
it('Not equals', async function() {
const filter = {$ne: 'test'};
const result = db_api.applyFilterLocalDB([{test: 'test'}, {test: 'test1'}], {test: filter}, 'find');
assert(result && result['test'] === 'test1');
});
it('Nested', async function() {
const result = db_api.applyFilterLocalDB([{test1: {test2: 'test3'}}, {test4: 'test5'}], {'test1.test2': 'test3'}, 'find');
assert(result && result['test1']['test2'] === 'test3');
});
})
}); });
describe('Multi User', async function() { describe('Multi User', async function() {
@@ -298,12 +253,10 @@ describe('Multi User', async function() {
assert(user); assert(user);
}); });
}); });
describe('Video player - normal', async function() { describe('Video player - normal', function() {
await db_api.removeRecord('files', {uid: sample_video_json['uid']}); const video_to_test = 'ebbcfffb-d6f1-4510-ad25-d1ec82e0477e';
await db_api.insertRecordIntoTable('files', sample_video_json);
const video_to_test = sample_video_json['uid'];
it('Get video', async function() { it('Get video', async function() {
const video_obj = await db_api.getVideo(video_to_test); const video_obj = db_api.getVideo(video_to_test, 'admin');
assert(video_obj); assert(video_obj);
}); });
@@ -388,9 +341,7 @@ describe('Downloader', function() {
}); });
it('Get file info', async function() { it('Get file info', async function() {
this.timeout(300000);
const info = await downloader_api.getVideoInfoByURL(url);
assert(!!info);
}); });
it('Download file', async function() { it('Download file', async function() {
@@ -401,19 +352,6 @@ describe('Downloader', function() {
}); });
it('Tag file', async function() {
const audio_path = './test/sample.mp3';
const sample_json = fs.readJSONSync('./test/sample.info.json');
const tags = {
title: sample_json['title'],
artist: sample_json['artist'] ? sample_json['artist'] : sample_json['uploader'],
TRCK: '27'
}
NodeID3.write(tags, audio_path);
const written_tags = NodeID3.read(audio_path);
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); const returned_download = await downloader_api.createDownload(url, 'video', options);
@@ -422,23 +360,20 @@ describe('Downloader', function() {
}); });
it('Pause file', async function() { it('Pause file', async function() {
const returned_download = await downloader_api.createDownload(url, 'video', options);
await downloader_api.pauseDownload(returned_download['uid']);
const updated_download = await db_api.getRecord('download_queue', {uid: returned_download['uid']});
assert(updated_download['paused'] && !updated_download['running']);
}); });
it('Generate args', async function() { it('Generate args', async function() {
const args = await downloader_api.generateArgs(url, 'video', options); const args = await downloader_api.generateArgs(url, 'video', options);
assert(args.length > 0); console.log(args);
}); });
it('Generate args - subscription', async function() { it('Generate args - subscription', async function() {
subscriptions_api.initialize(db_api, logger);
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 = await downloader_api.generateArgs(url, 'video', sub_options, 'admin');
const args_sub = await downloader_api.generateArgs(url, 'video', sub_options, 'admin'); console.log(args);
console.log(JSON.stringify(args_normal) !== JSON.stringify(args_sub));
}); });
it('Generate kodi NFO file', async function() { it('Generate kodi NFO file', async function() {
@@ -466,20 +401,6 @@ describe('Downloader', function() {
console.log(updated_args2); console.log(updated_args2);
assert(JSON.stringify(updated_args2), JSON.stringify(expected_args2)); assert(JSON.stringify(updated_args2), JSON.stringify(expected_args2));
}); });
describe('Twitch', async function () {
const twitch_api = require('../twitch');
const example_vod = '1493770675';
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));
// cleanup
if (fs.existsSync(sample_path)) fs.unlinkSync(sample_path);
});
});
}); });
describe('Tasks', function() { describe('Tasks', function() {
@@ -496,7 +417,7 @@ describe('Tasks', function() {
}; };
tasks_api.TASKS['dummy_task'] = dummy_task; tasks_api.TASKS['dummy_task'] = dummy_task;
await tasks_api.setupTasks(); await tasks_api.initialize();
}); });
it('Backup db', async function() { it('Backup db', async function() {
const backups_original = await utils.recFindByExt('appdata', 'bak'); const backups_original = await utils.recFindByExt('appdata', 'bak');
@@ -508,13 +429,12 @@ describe('Tasks', function() {
}); });
it('Check for missing files', async function() { it('Check for missing files', async function() {
this.timeout(300000);
await db_api.removeAllRecords('files', {uid: 'test'}); await db_api.removeAllRecords('files', {uid: 'test'});
const test_missing_file = {uid: 'test', path: 'test/missing_file.mp4'}; const test_missing_file = {uid: 'test', path: 'test/missing_file.mp4'};
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 task_obj = await db_api.getRecord('tasks', {key: 'missing_files_check'});
assert(!missing_file_db_record, true); assert(task_obj['data'] && task_obj['data']['uids'] && task_obj['data']['uids'].length >= 1, true);
}); });
it('Check for duplicate files', async function() { it('Check for duplicate files', async function() {
@@ -527,13 +447,10 @@ describe('Tasks', function() {
await db_api.insertRecordIntoTable('files', test_duplicate_file1); await db_api.insertRecordIntoTable('files', test_duplicate_file1);
await db_api.insertRecordIntoTable('files', test_duplicate_file2); await db_api.insertRecordIntoTable('files', test_duplicate_file2);
await db_api.insertRecordIntoTable('files', test_duplicate_file3); await db_api.insertRecordIntoTable('files', test_duplicate_file3);
await tasks_api.executeRun('duplicate_files_check');
const task_obj = await db_api.getRecord('tasks', {key: 'duplicate_files_check'});
assert(task_obj['data'] && task_obj['data']['uids'] && task_obj['data']['uids'].length >= 1, true);
await tasks_api.executeTask('duplicate_files_check'); await tasks_api.executeTask('duplicate_files_check');
const task_obj = await db_api.getRecord('tasks', {key: 'duplicate_files_check'});
const duplicated_record_count = await db_api.getRecords('files', {path: 'test/missing_file.mp4'}, true); const duplicated_record_count = await db_api.getRecords('files', {path: 'test/missing_file.mp4'}, true);
assert(task_obj['data'] && task_obj['data']['uids'] && task_obj['data']['uids'].length >= 1, true);
assert(duplicated_record_count == 1, true); assert(duplicated_record_count == 1, true);
}); });
@@ -558,72 +475,22 @@ describe('Tasks', function() {
}); });
it('Schedule and cancel task', async function() { it('Schedule and cancel task', async function() {
this.timeout(5000); const today_4_hours = new Date();
const today_one_year = new Date(); today_4_hours.setHours(today_4_hours.getHours() + 4);
today_one_year.setFullYear(today_one_year.getFullYear() + 1); await tasks_api.updateTaskSchedule('dummy_task', today_4_hours);
const schedule_obj = { assert(!!tasks_api.TASKS['dummy_task']['job'], true);
type: 'timestamp',
data: { timestamp: today_one_year.getTime() }
}
await tasks_api.updateTaskSchedule('dummy_task', schedule_obj);
const dummy_task = await db_api.getRecord('tasks', {key: 'dummy_task'});
assert(!!tasks_api.TASKS['dummy_task']['job']);
assert(!!dummy_task['schedule']);
await tasks_api.updateTaskSchedule('dummy_task', null); await tasks_api.updateTaskSchedule('dummy_task', null);
const dummy_task_updated = await db_api.getRecord('tasks', {key: 'dummy_task'}); assert(!!tasks_api.TASKS['dummy_task']['job'], false);
assert(!tasks_api.TASKS['dummy_task']['job']);
assert(!dummy_task_updated['schedule']);
}); });
it('Schedule and run task', async function() { it('Schedule and run task', async function() {
this.timeout(5000); this.timeout(5000);
const today_1_second = new Date(); const today_1_second = new Date();
today_1_second.setSeconds(today_1_second.getSeconds() + 1); today_1_second.setSeconds(today_1_second.getSeconds() + 1);
const schedule_obj = { await tasks_api.updateTaskSchedule('dummy_task', today_1_second);
type: 'timestamp', assert(!!tasks_api.TASKS['dummy_task']['job'], true);
data: { timestamp: today_1_second.getTime() }
}
await tasks_api.updateTaskSchedule('dummy_task', schedule_obj);
assert(!!tasks_api.TASKS['dummy_task']['job']);
await utils.wait(2000); await utils.wait(2000);
const dummy_task_obj = await db_api.getRecord('tasks', {key: 'dummy_task'}); const dummy_task_obj = await db_api.getRecord('tasks', {key: 'dummy_task'});
assert(dummy_task_obj['data']); assert(dummy_task_obj['data'], true);
});
});
describe('Archive', async function() {
const archive_path = path.join('test', 'archives');
fs.ensureDirSync(archive_path);
const archive_file_path = path.join(archive_path, 'archive_video.txt');
const blacklist_file_path = path.join(archive_path, 'blacklist_video.txt');
beforeEach(async function() {
if (fs.existsSync(archive_file_path)) fs.unlinkSync(archive_file_path);
fs.writeFileSync(archive_file_path, 'youtube testing1\nyoutube testing2\nyoutube testing3\n');
if (fs.existsSync(blacklist_file_path)) fs.unlinkSync(blacklist_file_path);
fs.writeFileSync(blacklist_file_path, '');
});
it('Delete from archive', async function() {
await utils.deleteFileFromArchive('N/A', 'video', archive_path, 'testing2', false);
const new_archive = fs.readFileSync(archive_file_path);
assert(!new_archive.includes('testing2'));
});
it('Delete from archive - blacklist', async function() {
await utils.deleteFileFromArchive('N/A', 'video', archive_path, 'testing2', true);
const new_archive = fs.readFileSync(archive_file_path);
const new_blacklist = fs.readFileSync(blacklist_file_path);
assert(!new_archive.includes('testing2'));
assert(new_blacklist.includes('testing2'));
});
});
describe('Utils', async function() {
it('Strip properties', async function() {
const test_obj = {test1: 'test1', test2: 'test2', test3: 'test3'};
const stripped_obj = utils.stripPropertiesFromObject(test_obj, ['test1', 'test3']);
assert(!stripped_obj['test1'] && stripped_obj['test2'] && !stripped_obj['test3'])
}); });
}); });

View File

@@ -1,64 +1,90 @@
var moment = require('moment');
var Axios = require('axios');
var fs = require('fs-extra')
var path = require('path');
const config_api = require('./config'); const config_api = require('./config');
const logger = require('./logger');
const moment = require('moment'); async function getCommentsForVOD(clientID, vodId) {
const fs = require('fs-extra') let url = `https://api.twitch.tv/v5/videos/${vodId}/comments?content_offset_seconds=0`,
const path = require('path'); batch,
cursor;
async function getCommentsForVOD(clientID, clientSecret, vodId) { let comments = null;
const { promisify } = require('util');
const child_process = require('child_process');
const exec = promisify(child_process.exec);
// Reject invalid params to prevent command injection attack try {
if (!clientID.match(/^[0-9a-z]+$/) || !clientSecret.match(/^[0-9a-z]+$/) || !vodId.match(/^[0-9a-z]+$/)) { do {
logger.error('Client ID, client secret, and VOD ID must be purely alphanumeric. Twitch chat download failed!'); batch = (await Axios.get(url, {
return null; headers: {
'Client-ID': clientID,
Accept: 'application/vnd.twitchtv.v5+json; charset=UTF-8',
'Content-Type': 'application/json; charset=UTF-8',
} }
})).data;
const result = await exec(`tcd --video ${vodId} --client-id ${clientID} --client-secret ${clientSecret} --format json -o appdata`, {stdio:[0,1,2]}); const str = batch.comments.map(c => {
let {
if (result['stderr']) { created_at: msgCreated,
logger.error(`Failed to download twitch comments for ${vodId}`); content_offset_seconds: timestamp,
logger.error(result['stderr']); commenter: {
return null; name,
_id,
created_at: acctCreated
},
message: {
body: msg,
user_color: user_color
} }
} = c;
const temp_chat_path = path.join('appdata', `${vodId}.json`); const timestamp_str = moment.duration(timestamp, 'seconds')
.toISOString()
const raw_json = fs.readJSONSync(temp_chat_path); .replace(/P.*?T(?:(\d+?)H)?(?:(\d+?)M)?(?:(\d+).*?S)?/,
const new_json = raw_json.comments.map(comment_obj => { (_, ...ms) => {
return { const seg = v => v ? v.padStart(2, '0') : '00';
timestamp: comment_obj.content_offset_seconds, return `${seg(ms[0])}:${seg(ms[1])}:${seg(ms[2])}`;
timestamp_str: convertTimestamp(comment_obj.content_offset_seconds),
name: comment_obj.commenter.name,
message: comment_obj.message.body,
user_color: comment_obj.message.user_color
}
}); });
fs.unlinkSync(temp_chat_path); acctCreated = moment(acctCreated).utc();
msgCreated = moment(msgCreated).utc();
return new_json; if (!comments) comments = [];
comments.push({
timestamp: timestamp,
timestamp_str: timestamp_str,
name: name,
message: msg,
user_color: user_color
});
// let line = `${timestamp},${msgCreated.format(tsFormat)},${name},${_id},"${msg.replace(/"/g, '""')}",${acctCreated.format(tsFormat)}`;
// return line;
}).join('\n');
cursor = batch._next;
url = `https://api.twitch.tv/v5/videos/${vodId}/comments?cursor=${cursor}`;
await new Promise(res => setTimeout(res, 300));
} while (cursor);
} catch (err) {
console.error(err);
}
return comments;
} }
async function getTwitchChatByFileID(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 subscriptionsFileFolder = config_api.getConfigItem('ytdl_subscriptions_base_path');
let file_path = null; let file_path = null;
if (user_uid) { if (user_uid) {
if (sub) { if (sub) {
file_path = path.join(usersFileFolder, user_uid, 'subscriptions', sub.isPlaylist ? 'playlists' : 'channels', sub.name, `${id}.twitch_chat.json`); file_path = path.join('users', user_uid, 'subscriptions', sub.isPlaylist ? 'playlists' : 'channels', sub.name, id + '.twitch_chat.json');
} else { } else {
file_path = path.join(usersFileFolder, user_uid, type, `${id}.twitch_chat.json`); file_path = path.join('users', user_uid, type, id + '.twitch_chat.json');
} }
} else { } else {
if (sub) { if (sub) {
file_path = path.join(subscriptionsFileFolder, sub.isPlaylist ? 'playlists' : 'channels', sub.name, `${id}.twitch_chat.json`); file_path = path.join('subscriptions', sub.isPlaylist ? 'playlists' : 'channels', sub.name, id + '.twitch_chat.json');
} else { } else {
const typeFolder = config_api.getConfigItem(`ytdl_${type}_folder_path`); file_path = path.join(type, id + '.twitch_chat.json');
file_path = path.join(typeFolder, `${id}.twitch_chat.json`);
} }
} }
@@ -70,28 +96,23 @@ async function getTwitchChatByFileID(id, type, user_uid, uuid, sub) {
return chat_file; return chat_file;
} }
async function downloadTwitchChatByVODID(vodId, id, type, user_uid, sub, customFileFolderPath = null) { async function downloadTwitchChatByVODID(vodId, id, type, user_uid, sub) {
const usersFileFolder = config_api.getConfigItem('ytdl_users_base_path'); const twitch_api_key = config_api.getConfigItem('ytdl_twitch_api_key');
const subscriptionsFileFolder = config_api.getConfigItem('ytdl_subscriptions_base_path'); const chat = await getCommentsForVOD(twitch_api_key, vodId);
const twitch_client_id = config_api.getConfigItem('ytdl_twitch_client_id');
const twitch_client_secret = config_api.getConfigItem('ytdl_twitch_client_secret');
const chat = await getCommentsForVOD(twitch_client_id, twitch_client_secret, vodId);
// save file if needed params are included // save file if needed params are included
let file_path = null; let file_path = null;
if (customFileFolderPath) { if (user_uid) {
file_path = path.join(customFileFolderPath, `${id}.twitch_chat.json`)
} else if (user_uid) {
if (sub) { if (sub) {
file_path = path.join(usersFileFolder, user_uid, 'subscriptions', sub.isPlaylist ? 'playlists' : 'channels', sub.name, `${id}.twitch_chat.json`); file_path = path.join('users', user_uid, 'subscriptions', sub.isPlaylist ? 'playlists' : 'channels', sub.name, id + '.twitch_chat.json');
} else { } else {
file_path = path.join(usersFileFolder, user_uid, type, `${id}.twitch_chat.json`); file_path = path.join('users', user_uid, type, id + '.twitch_chat.json');
} }
} else { } else {
if (sub) { if (sub) {
file_path = path.join(subscriptionsFileFolder, sub.isPlaylist ? 'playlists' : 'channels', sub.name, `${id}.twitch_chat.json`); file_path = path.join('subscriptions', sub.isPlaylist ? 'playlists' : 'channels', sub.name, id + '.twitch_chat.json');
} else { } else {
file_path = path.join(type, `${id}.twitch_chat.json`); file_path = path.join(type, id + '.twitch_chat.json');
} }
} }
@@ -100,14 +121,6 @@ async function downloadTwitchChatByVODID(vodId, id, type, user_uid, sub, customF
return chat; return chat;
} }
const convertTimestamp = (timestamp) => moment.duration(timestamp, 'seconds')
.toISOString()
.replace(/P.*?T(?:(\d+?)H)?(?:(\d+?)M)?(?:(\d+).*?S)?/,
(_, ...ms) => {
const seg = v => v ? v.padStart(2, '0') : '00';
return `${seg(ms[0])}:${seg(ms[1])}:${seg(ms[2])}`;
});
module.exports = { module.exports = {
getCommentsForVOD: getCommentsForVOD, getCommentsForVOD: getCommentsForVOD,
getTwitchChatByFileID: getTwitchChatByFileID, getTwitchChatByFileID: getTwitchChatByFileID,

View File

@@ -172,13 +172,11 @@ function getExpectedFileSize(input_info_jsons) {
const formats = info_json['format_id'].split('+'); const formats = info_json['format_id'].split('+');
let individual_expected_filesize = 0; let individual_expected_filesize = 0;
formats.forEach(format_id => { formats.forEach(format_id => {
if (info_json.formats !== undefined) {
info_json.formats.forEach(available_format => { info_json.formats.forEach(available_format => {
if (available_format.format_id === format_id && (available_format.filesize || available_format.filesize_approx)) { if (available_format.format_id === format_id && available_format.filesize) {
individual_expected_filesize += (available_format.filesize ? available_format.filesize : available_format.filesize_approx); individual_expected_filesize += available_format.filesize;
} }
}); });
}
}); });
expected_filesize += individual_expected_filesize; expected_filesize += individual_expected_filesize;
}); });
@@ -220,11 +218,8 @@ function deleteJSONFile(file_path, type) {
if (fs.existsSync(alternate_json_path)) fs.unlinkSync(alternate_json_path); if (fs.existsSync(alternate_json_path)) fs.unlinkSync(alternate_json_path);
} }
// archive helper functions async function removeIDFromArchive(archive_path, id) {
let data = await fs.readFile(archive_path, {encoding: 'utf-8'});
async function removeIDFromArchive(archive_path, type, id) {
const archive_file = path.join(archive_path, `archive_${type}.txt`);
const data = await fs.readFile(archive_file, {encoding: 'utf-8'});
if (!data) { if (!data) {
logger.error('Archive could not be found.'); logger.error('Archive could not be found.');
return; return;
@@ -241,34 +236,12 @@ async function removeIDFromArchive(archive_path, type, id) {
} }
} }
if (lastIndex === -1) return null;
const line = dataArray.splice(lastIndex, 1); // remove the keyword id from the data Array const line = dataArray.splice(lastIndex, 1); // remove the keyword id from the data Array
// UPDATE FILE WITH NEW DATA // UPDATE FILE WITH NEW DATA
const updatedData = dataArray.join('\n'); const updatedData = dataArray.join('\n');
await fs.writeFile(archive_file, updatedData); await fs.writeFile(archive_path, updatedData);
if (line) return Array.isArray(line) && line.length === 1 ? line[0] : line; if (line) return line;
}
async function writeToBlacklist(archive_folder, type, line) {
let blacklistPath = path.join(archive_folder, (type === 'audio') ? 'blacklist_audio.txt' : 'blacklist_video.txt');
// adds newline to the beginning of the line
line.replace('\n', '');
line.replace('\r', '');
line = '\n' + line;
await fs.appendFile(blacklistPath, line);
}
async function deleteFileFromArchive(uid, type, archive_path, id, blacklistMode) {
const archive_file = path.join(archive_path, `archive_${type}.txt`);
if (await fs.pathExists(archive_path)) {
const line = id ? await removeIDFromArchive(archive_path, type, id) : null;
if (blacklistMode && line) await writeToBlacklist(archive_path, type, line);
} else {
logger.info(`Could not find archive file for file ${uid}. Creating...`);
await fs.close(await fs.open(archive_file, 'w'));
}
} }
function durationStringToNumber(dur_str) { function durationStringToNumber(dur_str) {
@@ -445,7 +418,7 @@ async function fetchFile(url, path, file_label) {
async function restartServer(is_update = false) { async function restartServer(is_update = false) {
logger.info(`${is_update ? 'Update complete! ' : ''}Restarting server...`); logger.info(`${is_update ? 'Update complete! ' : ''}Restarting server...`);
// the following line restarts the server through pm2 // the following line restarts the server through nodemon
fs.writeFileSync(`restart${is_update ? '_update' : '_general'}.json`, 'internal use only'); fs.writeFileSync(`restart${is_update ? '_update' : '_general'}.json`, 'internal use only');
process.exit(1); process.exit(1);
} }
@@ -483,60 +456,6 @@ function injectArgs(original_args, new_args) {
return updated_args; return updated_args;
} }
function filterArgs(args, args_to_remove) {
return args.filter(x => !args_to_remove.includes(x));
}
const searchObjectByString = function(o, s) {
s = s.replace(/\[(\w+)\]/g, '.$1'); // convert indexes to properties
s = s.replace(/^\./, ''); // strip a leading dot
var a = s.split('.');
for (var i = 0, n = a.length; i < n; ++i) {
var k = a[i];
if (k in o) {
o = o[k];
} else {
return;
}
}
return o;
}
function stripPropertiesFromObject(obj, properties, whitelist = false) {
if (!whitelist) {
const new_obj = JSON.parse(JSON.stringify(obj));
for (let field of properties) {
delete new_obj[field];
}
return new_obj;
}
const new_obj = {};
for (let field of properties) {
new_obj[field] = obj[field];
}
return new_obj;
}
function getArchiveFolder(type, user_uid = null, sub = null) {
const usersFolderPath = config_api.getConfigItem('ytdl_users_base_path');
const subsFolderPath = config_api.getConfigItem('ytdl_subscriptions_base_path');
if (user_uid) {
if (sub) {
return path.join(usersFolderPath, user_uid, 'subscriptions', 'archives', sub.name);
} else {
return path.join(usersFolderPath, user_uid, type, 'archives');
}
} else {
if (sub) {
return path.join(subsFolderPath, 'archives', sub.name);
} else {
return path.join('appdata', 'archives');
}
}
}
// 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) {
@@ -566,12 +485,11 @@ module.exports = {
fixVideoMetadataPerms: fixVideoMetadataPerms, fixVideoMetadataPerms: fixVideoMetadataPerms,
deleteJSONFile: deleteJSONFile, deleteJSONFile: deleteJSONFile,
removeIDFromArchive: removeIDFromArchive, removeIDFromArchive: removeIDFromArchive,
writeToBlacklist: writeToBlacklist,
deleteFileFromArchive: deleteFileFromArchive,
getDownloadedFilesByType: getDownloadedFilesByType, getDownloadedFilesByType: getDownloadedFilesByType,
createContainerZipFile: createContainerZipFile, createContainerZipFile: createContainerZipFile,
durationStringToNumber: durationStringToNumber, durationStringToNumber: durationStringToNumber,
getMatchingCategoryFiles: getMatchingCategoryFiles, getMatchingCategoryFiles: getMatchingCategoryFiles,
addUIDsToCategory: addUIDsToCategory,
getCurrentDownloader: getCurrentDownloader, getCurrentDownloader: getCurrentDownloader,
recFindByExt: recFindByExt, recFindByExt: recFindByExt,
removeFileExtension: removeFileExtension, removeFileExtension: removeFileExtension,
@@ -583,9 +501,5 @@ module.exports = {
fetchFile: fetchFile, fetchFile: fetchFile,
restartServer: restartServer, restartServer: restartServer,
injectArgs: injectArgs, injectArgs: injectArgs,
filterArgs: filterArgs,
searchObjectByString: searchObjectByString,
stripPropertiesFromObject: stripPropertiesFromObject,
getArchiveFolder: getArchiveFolder,
File: File File: File
} }

View File

@@ -90,7 +90,7 @@ exports.updateYoutubeDL = async (latest_update_version) => {
exports.verifyBinaryExistsLinux = () => { exports.verifyBinaryExistsLinux = () => {
const details_json = fs.readJSONSync(CONSTS.DETAILS_BIN_PATH); const details_json = fs.readJSONSync(CONSTS.DETAILS_BIN_PATH);
if (!is_windows && details_json && (!details_json['path'] || details_json['path'].includes('.exe'))) { if (!is_windows && details_json && details_json['path'].includes('.exe')) {
details_json['path'] = 'node_modules/youtube-dl/bin/youtube-dl'; details_json['path'] = 'node_modules/youtube-dl/bin/youtube-dl';
details_json['exec'] = 'youtube-dl'; details_json['exec'] = 'youtube-dl';
details_json['version'] = OUTDATED_VERSION; details_json['version'] = OUTDATED_VERSION;

View File

@@ -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" appVersion: "4.2"

View File

@@ -2,6 +2,7 @@ version: "2"
services: services:
ytdl_material: ytdl_material:
environment: environment:
ALLOW_CONFIG_MUTATIONS: 'true'
ytdl_mongodb_connection_string: 'mongodb://ytdl-mongo-db:27017' ytdl_mongodb_connection_string: 'mongodb://ytdl-mongo-db:27017'
ytdl_use_local_db: 'false' ytdl_use_local_db: 'false'
write_ytdl_config: 'true' write_ytdl_config: 'true'
@@ -16,9 +17,11 @@ services:
- ./users:/app/users - ./users:/app/users
ports: ports:
- "8998:17442" - "8998:17442"
image: tzahi12345/youtubedl-material:latest image: tzahi12345/youtubedl-material:nightly
ytdl-mongo-db: ytdl-mongo-db:
image: mongo image: mongo
ports:
- "27017:27017"
logging: logging:
driver: "none" driver: "none"
container_name: mongo-db container_name: mongo-db

3791
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,19 +1,19 @@
{ {
"name": "youtube-dl-material", "name": "youtube-dl-material",
"version": "4.3.0", "version": "4.2.0",
"license": "MIT", "license": "MIT",
"scripts": { "scripts": {
"ng": "ng", "ng": "ng",
"start": "ng serve", "start": "ng serve",
"build": "ng build --configuration production", "build": "ng build --configuration production",
"prebuild": "node src/postbuild.mjs", "prebuild": "node src/postbuild.js",
"heroku-postbuild": "npm install --prefix backend", "heroku-postbuild": "npm install --prefix backend",
"test": "ng test", "test": "ng test",
"lint": "ng lint", "lint": "ng lint",
"e2e": "ng e2e", "e2e": "ng e2e",
"electron": "ng build --base-href ./ && electron .", "electron": "ng build --base-href ./ && electron .",
"generate": "openapi --input ./\"Public API v1.yaml\" --output ./src/api-types --exportCore false --exportServices false --exportModels true", "generate": "openapi --input ./\"Public API v1.yaml\" --output ./src/api-types --exportCore false --exportServices false --exportModels true",
"i18n-source": "ng extract-i18n --output-path=src/assets/i18n --out-file=messages.en.xlf" "i18n-source": "ng extract-i18n --output-path=src/assets/i18n"
}, },
"engines": { "engines": {
"node": "12.3.1", "node": "12.3.1",
@@ -21,18 +21,18 @@
}, },
"private": true, "private": true,
"dependencies": { "dependencies": {
"@angular-devkit/core": "^14.0.4", "@angular-devkit/core": "^13.3.3",
"@angular/animations": "^14.0.4", "@angular/animations": "^13.3.4",
"@angular/cdk": "^14.0.4", "@angular/cdk": "^13.3.4",
"@angular/common": "^14.0.4", "@angular/common": "^13.3.4",
"@angular/compiler": "^14.0.4", "@angular/compiler": "^13.3.4",
"@angular/core": "^14.0.4", "@angular/core": "^13.3.4",
"@angular/forms": "^14.0.4", "@angular/forms": "^13.3.4",
"@angular/localize": "^14.0.4", "@angular/localize": "^13.3.4",
"@angular/material": "^14.0.4", "@angular/material": "^13.3.4",
"@angular/platform-browser": "^14.0.4", "@angular/platform-browser": "^13.3.4",
"@angular/platform-browser-dynamic": "^14.0.4", "@angular/platform-browser-dynamic": "^13.3.4",
"@angular/router": "^14.0.4", "@angular/router": "^13.3.4",
"@fontsource/material-icons": "^4.5.4", "@fontsource/material-icons": "^4.5.4",
"@ngneat/content-loader": "^5.0.0", "@ngneat/content-loader": "^5.0.0",
"@videogular/ngx-videogular": "^5.0.1", "@videogular/ngx-videogular": "^5.0.1",
@@ -55,10 +55,10 @@
"zone.js": "~0.11.4" "zone.js": "~0.11.4"
}, },
"devDependencies": { "devDependencies": {
"@angular-devkit/build-angular": "^14.0.4", "@angular-devkit/build-angular": "^13.3.3",
"@angular/cli": "^14.0.4", "@angular/cli": "^13.3.3",
"@angular/compiler-cli": "^14.0.4", "@angular/compiler-cli": "^13.3.4",
"@angular/language-service": "^14.0.4", "@angular/language-service": "^13.3.4",
"@types/core-js": "^2.5.2", "@types/core-js": "^2.5.2",
"@types/file-saver": "^2.0.1", "@types/file-saver": "^2.0.1",
"@types/jasmine": "~3.6.0", "@types/jasmine": "~3.6.0",
@@ -66,7 +66,7 @@
"@typescript-eslint/eslint-plugin": "^4.29.0", "@typescript-eslint/eslint-plugin": "^4.29.0",
"@typescript-eslint/parser": "^4.29.0", "@typescript-eslint/parser": "^4.29.0",
"codelyzer": "^6.0.0", "codelyzer": "^6.0.0",
"electron": "^19.0.6", "electron": "^13.6.6",
"eslint": "^7.32.0", "eslint": "^7.32.0",
"jasmine-core": "~3.6.0", "jasmine-core": "~3.6.0",
"jasmine-spec-reporter": "~5.0.0", "jasmine-spec-reporter": "~5.0.0",

Binary file not shown.

View File

@@ -4,7 +4,6 @@
export type { AddFileToPlaylistRequest } from './models/AddFileToPlaylistRequest'; export type { AddFileToPlaylistRequest } from './models/AddFileToPlaylistRequest';
export type { BaseChangePermissionsRequest } from './models/BaseChangePermissionsRequest'; export type { BaseChangePermissionsRequest } from './models/BaseChangePermissionsRequest';
export type { binary } from './models/binary';
export type { body_19 } from './models/body_19'; export type { body_19 } from './models/body_19';
export type { body_20 } from './models/body_20'; export type { body_20 } from './models/body_20';
export type { Category } from './models/Category'; export type { Category } from './models/Category';
@@ -13,7 +12,6 @@ export type { ChangeRolePermissionsRequest } from './models/ChangeRolePermission
export type { ChangeUserPermissionsRequest } from './models/ChangeUserPermissionsRequest'; export type { ChangeUserPermissionsRequest } from './models/ChangeUserPermissionsRequest';
export type { CheckConcurrentStreamRequest } from './models/CheckConcurrentStreamRequest'; export type { CheckConcurrentStreamRequest } from './models/CheckConcurrentStreamRequest';
export type { CheckConcurrentStreamResponse } from './models/CheckConcurrentStreamResponse'; export type { CheckConcurrentStreamResponse } from './models/CheckConcurrentStreamResponse';
export type { ClearDownloadsRequest } from './models/ClearDownloadsRequest';
export type { ConcurrentStream } from './models/ConcurrentStream'; export type { ConcurrentStream } from './models/ConcurrentStream';
export type { Config } from './models/Config'; export type { Config } from './models/Config';
export type { ConfigResponse } from './models/ConfigResponse'; export type { ConfigResponse } from './models/ConfigResponse';
@@ -25,7 +23,6 @@ export type { CropFileSettings } from './models/CropFileSettings';
export type { DatabaseFile } from './models/DatabaseFile'; export type { DatabaseFile } from './models/DatabaseFile';
export { DBBackup } from './models/DBBackup'; export { DBBackup } from './models/DBBackup';
export type { DBInfoResponse } from './models/DBInfoResponse'; export type { DBInfoResponse } from './models/DBInfoResponse';
export type { DeleteAllFilesResponse } from './models/DeleteAllFilesResponse';
export type { DeleteCategoryRequest } from './models/DeleteCategoryRequest'; export type { DeleteCategoryRequest } from './models/DeleteCategoryRequest';
export type { DeleteMp3Mp4Request } from './models/DeleteMp3Mp4Request'; export type { DeleteMp3Mp4Request } from './models/DeleteMp3Mp4Request';
export type { DeletePlaylistRequest } from './models/DeletePlaylistRequest'; export type { DeletePlaylistRequest } from './models/DeletePlaylistRequest';
@@ -39,14 +36,13 @@ export type { DownloadResponse } from './models/DownloadResponse';
export type { DownloadTwitchChatByVODIDRequest } from './models/DownloadTwitchChatByVODIDRequest'; export type { DownloadTwitchChatByVODIDRequest } from './models/DownloadTwitchChatByVODIDRequest';
export type { DownloadTwitchChatByVODIDResponse } from './models/DownloadTwitchChatByVODIDResponse'; export type { DownloadTwitchChatByVODIDResponse } from './models/DownloadTwitchChatByVODIDResponse';
export type { DownloadVideosForSubscriptionRequest } from './models/DownloadVideosForSubscriptionRequest'; export type { DownloadVideosForSubscriptionRequest } from './models/DownloadVideosForSubscriptionRequest';
export type { File } from './models/File';
export { FileType } from './models/FileType'; export { FileType } from './models/FileType';
export { FileTypeFilter } from './models/FileTypeFilter';
export type { GenerateArgsResponse } from './models/GenerateArgsResponse'; export type { GenerateArgsResponse } from './models/GenerateArgsResponse';
export type { GenerateNewApiKeyResponse } from './models/GenerateNewApiKeyResponse'; export type { GenerateNewApiKeyResponse } from './models/GenerateNewApiKeyResponse';
export type { GetAllCategoriesResponse } from './models/GetAllCategoriesResponse'; export type { GetAllCategoriesResponse } from './models/GetAllCategoriesResponse';
export type { GetAllDownloadsRequest } from './models/GetAllDownloadsRequest'; export type { GetAllDownloadsRequest } from './models/GetAllDownloadsRequest';
export type { GetAllDownloadsResponse } from './models/GetAllDownloadsResponse'; export type { GetAllDownloadsResponse } from './models/GetAllDownloadsResponse';
export type { GetAllFilesRequest } from './models/GetAllFilesRequest';
export type { GetAllFilesResponse } from './models/GetAllFilesResponse'; export type { GetAllFilesResponse } from './models/GetAllFilesResponse';
export type { GetAllSubscriptionsResponse } from './models/GetAllSubscriptionsResponse'; export type { GetAllSubscriptionsResponse } from './models/GetAllSubscriptionsResponse';
export type { GetAllTasksResponse } from './models/GetAllTasksResponse'; export type { GetAllTasksResponse } from './models/GetAllTasksResponse';
@@ -84,7 +80,6 @@ export type { RestoreDBBackupRequest } from './models/RestoreDBBackupRequest';
export { Schedule } from './models/Schedule'; export { Schedule } from './models/Schedule';
export type { SetConfigRequest } from './models/SetConfigRequest'; export type { SetConfigRequest } from './models/SetConfigRequest';
export type { SharingToggle } from './models/SharingToggle'; export type { SharingToggle } from './models/SharingToggle';
export type { Sort } from './models/Sort';
export type { SubscribeRequest } from './models/SubscribeRequest'; export type { SubscribeRequest } from './models/SubscribeRequest';
export type { SubscribeResponse } from './models/SubscribeResponse'; export type { SubscribeResponse } from './models/SubscribeResponse';
export type { Subscription } from './models/Subscription'; export type { Subscription } from './models/Subscription';
@@ -103,7 +98,6 @@ export type { UpdateCategoriesRequest } from './models/UpdateCategoriesRequest';
export type { UpdateCategoryRequest } from './models/UpdateCategoryRequest'; export type { UpdateCategoryRequest } from './models/UpdateCategoryRequest';
export type { UpdateConcurrentStreamRequest } from './models/UpdateConcurrentStreamRequest'; export type { UpdateConcurrentStreamRequest } from './models/UpdateConcurrentStreamRequest';
export type { UpdateConcurrentStreamResponse } from './models/UpdateConcurrentStreamResponse'; export type { UpdateConcurrentStreamResponse } from './models/UpdateConcurrentStreamResponse';
export type { UpdateFileRequest } from './models/UpdateFileRequest';
export type { UpdatePlaylistRequest } from './models/UpdatePlaylistRequest'; export type { UpdatePlaylistRequest } from './models/UpdatePlaylistRequest';
export type { UpdaterStatus } from './models/UpdaterStatus'; export type { UpdaterStatus } from './models/UpdaterStatus';
export type { UpdateServerRequest } from './models/UpdateServerRequest'; export type { UpdateServerRequest } from './models/UpdateServerRequest';

View File

@@ -2,7 +2,8 @@
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */ /* eslint-disable */
export type AddFileToPlaylistRequest = {
export interface AddFileToPlaylistRequest {
file_uid: string; file_uid: string;
playlist_id: string; playlist_id: string;
}; }

View File

@@ -2,10 +2,10 @@
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */ /* eslint-disable */
import type { UserPermission } from './UserPermission'; import { UserPermission } from './UserPermission';
import type { YesNo } from './YesNo'; import { YesNo } from './YesNo';
export type BaseChangePermissionsRequest = { export interface BaseChangePermissionsRequest {
permission: UserPermission; permission: UserPermission;
new_value: YesNo; new_value: YesNo;
}; }

View File

@@ -2,9 +2,9 @@
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */ /* eslint-disable */
import type { CategoryRule } from './CategoryRule'; import { CategoryRule } from './CategoryRule';
export type Category = { export interface Category {
name?: string; name?: string;
uid?: string; uid?: string;
rules?: Array<CategoryRule>; rules?: Array<CategoryRule>;
@@ -12,4 +12,4 @@ export type Category = {
* Overrides file output for downloaded files in category * Overrides file output for downloaded files in category
*/ */
custom_output?: string; custom_output?: string;
}; }

View File

@@ -2,10 +2,11 @@
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */ /* eslint-disable */
export type CategoryRule = {
export interface CategoryRule {
preceding_operator?: CategoryRule.preceding_operator; preceding_operator?: CategoryRule.preceding_operator;
comparator?: CategoryRule.comparator; comparator?: CategoryRule.comparator;
}; }
export namespace CategoryRule { export namespace CategoryRule {

View File

@@ -2,8 +2,8 @@
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */ /* eslint-disable */
import type { BaseChangePermissionsRequest } from './BaseChangePermissionsRequest'; import { BaseChangePermissionsRequest } from './BaseChangePermissionsRequest';
export type ChangeRolePermissionsRequest = (BaseChangePermissionsRequest & { export interface ChangeRolePermissionsRequest extends BaseChangePermissionsRequest {
role: string; role: string;
}); }

View File

@@ -2,8 +2,8 @@
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */ /* eslint-disable */
import type { BaseChangePermissionsRequest } from './BaseChangePermissionsRequest'; import { BaseChangePermissionsRequest } from './BaseChangePermissionsRequest';
export type ChangeUserPermissionsRequest = (BaseChangePermissionsRequest & { export interface ChangeUserPermissionsRequest extends BaseChangePermissionsRequest {
user_uid: string; user_uid: string;
}); }

View File

@@ -2,9 +2,10 @@
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */ /* eslint-disable */
export type CheckConcurrentStreamRequest = {
export interface CheckConcurrentStreamRequest {
/** /**
* UID of the concurrent stream * UID of the concurrent stream
*/ */
uid: string; uid: string;
}; }

View File

@@ -2,8 +2,8 @@
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */ /* eslint-disable */
import type { ConcurrentStream } from './ConcurrentStream'; import { ConcurrentStream } from './ConcurrentStream';
export type CheckConcurrentStreamResponse = { export interface CheckConcurrentStreamResponse {
stream: ConcurrentStream; stream: ConcurrentStream;
}; }

View File

@@ -1,9 +0,0 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type ClearDownloadsRequest = {
clear_finished?: boolean;
clear_paused?: boolean;
clear_errors?: boolean;
};

View File

@@ -2,8 +2,9 @@
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */ /* eslint-disable */
export type ConcurrentStream = {
export interface ConcurrentStream {
playback_timestamp?: number; playback_timestamp?: number;
unix_timestamp?: number; unix_timestamp?: number;
playing?: boolean; playing?: boolean;
}; }

View File

@@ -2,6 +2,7 @@
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */ /* eslint-disable */
export type Config = {
export interface Config {
YoutubeDLMaterial: any; YoutubeDLMaterial: any;
}; }

View File

@@ -2,9 +2,9 @@
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */ /* eslint-disable */
import type { Config } from './Config'; import { Config } from './Config';
export type ConfigResponse = { export interface ConfigResponse {
config_file: Config; config_file: Config;
success: boolean; success: boolean;
}; }

View File

@@ -2,6 +2,7 @@
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */ /* eslint-disable */
export type CreateCategoryRequest = {
export interface CreateCategoryRequest {
name: string; name: string;
}; }

View File

@@ -2,9 +2,9 @@
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */ /* eslint-disable */
import type { Category } from './Category'; import { Category } from './Category';
export type CreateCategoryResponse = { export interface CreateCategoryResponse {
new_category?: Category; new_category?: Category;
success?: boolean; success?: boolean;
}; }

View File

@@ -2,8 +2,11 @@
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */ /* eslint-disable */
export type CreatePlaylistRequest = { import { FileType } from './FileType';
export interface CreatePlaylistRequest {
playlistName: string; playlistName: string;
uids: Array<string>; uids: Array<string>;
type: FileType;
thumbnailURL: string; thumbnailURL: string;
}; }

View File

@@ -2,9 +2,9 @@
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */ /* eslint-disable */
import type { Playlist } from './Playlist'; import { Playlist } from './Playlist';
export type CreatePlaylistResponse = { export interface CreatePlaylistResponse {
new_playlist: Playlist; new_playlist: Playlist;
success: boolean; success: boolean;
}; }

View File

@@ -2,7 +2,8 @@
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */ /* eslint-disable */
export type CropFileSettings = {
export interface CropFileSettings {
cropFileStart: number; cropFileStart: number;
cropFileEnd: number; cropFileEnd: number;
}; }

View File

@@ -2,12 +2,13 @@
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */ /* eslint-disable */
export type DBBackup = {
export interface DBBackup {
name: string; name: string;
timestamp: number; timestamp: number;
size: number; size: number;
source: DBBackup.source; source: DBBackup.source;
}; }
export namespace DBBackup { export namespace DBBackup {

View File

@@ -2,17 +2,17 @@
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */ /* eslint-disable */
import type { TableInfo } from './TableInfo'; import { TableInfo } from './TableInfo';
export type DBInfoResponse = { export interface DBInfoResponse {
using_local_db?: boolean; using_local_db?: boolean;
stats_by_table?: { stats_by_table?: {
files?: TableInfo; files?: TableInfo,
playlists?: TableInfo; playlists?: TableInfo,
categories?: TableInfo; categories?: TableInfo,
subscriptions?: TableInfo; subscriptions?: TableInfo,
users?: TableInfo; users?: TableInfo,
roles?: TableInfo; roles?: TableInfo,
download_queue?: TableInfo; download_queue?: TableInfo,
};
}; };
}

View File

@@ -2,16 +2,11 @@
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */ /* eslint-disable */
import type { Category } from './Category';
export type DatabaseFile = { export interface DatabaseFile {
id: string; id: string;
title: string; title: string;
/**
* Backup if thumbnailPath is not defined
*/
thumbnailURL: string; thumbnailURL: string;
thumbnailPath?: string;
isAudio: boolean; isAudio: boolean;
/** /**
* In seconds * In seconds
@@ -19,25 +14,9 @@ export type DatabaseFile = {
duration: number; duration: number;
url: string; url: string;
uploader: string; uploader: string;
/**
* In bytes
*/
size: number; size: number;
path: string; path: string;
upload_date: string; upload_date: string;
uid: string; uid: string;
sharingEnabled?: boolean; sharingEnabled?: boolean;
category?: Category; }
view_count?: number;
local_view_count?: number;
sub_id?: string;
registered?: number;
/**
* In pixels, only for videos
*/
height?: number;
/**
* In Kbps
*/
abr?: number;
};

View File

@@ -1,14 +0,0 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type DeleteAllFilesResponse = {
/**
* Number of files found matching search parameters
*/
file_count?: number;
/**
* Number of files removed
*/
delete_count?: number;
};

View File

@@ -2,6 +2,7 @@
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */ /* eslint-disable */
export type DeleteCategoryRequest = {
export interface DeleteCategoryRequest {
category_uid: string; category_uid: string;
}; }

View File

@@ -2,7 +2,8 @@
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */ /* eslint-disable */
export type DeleteMp3Mp4Request = {
export interface DeleteMp3Mp4Request {
uid: string; uid: string;
blacklistMode?: boolean; blacklistMode?: boolean;
}; }

View File

@@ -2,6 +2,9 @@
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */ /* eslint-disable */
export type DeletePlaylistRequest = { import { FileType } from './FileType';
export interface DeletePlaylistRequest {
playlist_id: string; playlist_id: string;
}; type: FileType;
}

View File

@@ -2,9 +2,9 @@
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */ /* eslint-disable */
import type { SubscriptionRequestData } from './SubscriptionRequestData'; import { SubscriptionRequestData } from './SubscriptionRequestData';
export type DeleteSubscriptionFileRequest = { export interface DeleteSubscriptionFileRequest {
file: string; file: string;
file_uid?: string; file_uid?: string;
sub: SubscriptionRequestData; sub: SubscriptionRequestData;
@@ -12,4 +12,4 @@ export type DeleteSubscriptionFileRequest = {
* If true, does not remove id from archive. Only valid if youtube-dl archive is enabled in settings. * If true, does not remove id from archive. Only valid if youtube-dl archive is enabled in settings.
*/ */
deleteForever?: boolean; deleteForever?: boolean;
}; }

View File

@@ -2,6 +2,7 @@
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */ /* eslint-disable */
export type DeleteUserRequest = {
export interface DeleteUserRequest {
uid: string; uid: string;
}; }

View File

@@ -0,0 +1,7 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type Dictionary<T> = {
[key: string]: T;
}

View File

@@ -2,7 +2,8 @@
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */ /* eslint-disable */
export type Download = {
export interface Download {
uid: string; uid: string;
ui_uid?: string; ui_uid?: string;
running: boolean; running: boolean;
@@ -22,5 +23,4 @@ export type Download = {
user_uid?: string; user_uid?: string;
sub_id?: string; sub_id?: string;
sub_name?: string; sub_name?: string;
prefetched_info?: any; }
};

View File

@@ -2,8 +2,9 @@
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */ /* eslint-disable */
export type DownloadArchiveRequest = {
export interface DownloadArchiveRequest {
sub: { sub: {
archive_dir: string; archive_dir: string,
};
}; };
}

View File

@@ -2,13 +2,13 @@
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */ /* eslint-disable */
import type { FileType } from './FileType'; import { FileType } from './FileType';
export type DownloadFileRequest = { export interface DownloadFileRequest {
uid?: string; uid?: string;
uuid?: string; uuid?: string;
sub_id?: string; sub_id?: string;
playlist_id?: string; playlist_id?: string;
url?: string; url?: string;
type?: FileType; type?: FileType;
}; }

View File

@@ -2,10 +2,10 @@
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */ /* eslint-disable */
import type { CropFileSettings } from './CropFileSettings'; import { CropFileSettings } from './CropFileSettings';
import type { FileType } from './FileType'; import { FileType } from './FileType';
export type DownloadRequest = { export interface DownloadRequest {
url: string; url: string;
/** /**
* Video format code. Overrides other quality options. * Video format code. Overrides other quality options.
@@ -41,4 +41,4 @@ export type DownloadRequest = {
maxBitrate?: string; maxBitrate?: string;
type?: FileType; type?: FileType;
cropFileSettings?: CropFileSettings; cropFileSettings?: CropFileSettings;
}; }

View File

@@ -2,8 +2,8 @@
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */ /* eslint-disable */
import type { Download } from './Download'; import { Download } from './Download';
export type DownloadResponse = { export interface DownloadResponse {
download?: Download; download?: Download;
}; }

View File

@@ -2,10 +2,10 @@
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */ /* eslint-disable */
import type { FileType } from './FileType'; import { FileType } from './FileType';
import type { Subscription } from './Subscription'; import { Subscription } from './Subscription';
export type DownloadTwitchChatByVODIDRequest = { export interface DownloadTwitchChatByVODIDRequest {
/** /**
* File ID * File ID
*/ */
@@ -20,4 +20,4 @@ export type DownloadTwitchChatByVODIDRequest = {
*/ */
uuid?: string; uuid?: string;
sub?: Subscription; sub?: Subscription;
}; }

View File

@@ -2,8 +2,8 @@
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */ /* eslint-disable */
import type { TwitchChatMessage } from './TwitchChatMessage'; import { TwitchChatMessage } from './TwitchChatMessage';
export type DownloadTwitchChatByVODIDResponse = { export interface DownloadTwitchChatByVODIDResponse {
chat: Array<TwitchChatMessage>; chat: Array<TwitchChatMessage>;
}; }

View File

@@ -2,6 +2,7 @@
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */ /* eslint-disable */
export type DownloadVideosForSubscriptionRequest = {
export interface DownloadVideosForSubscriptionRequest {
subID: string; subID: string;
}; }

View File

@@ -2,6 +2,7 @@
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */ /* eslint-disable */
export type binary = {
export interface File {
id?: string; id?: string;
}; }

View File

@@ -2,6 +2,7 @@
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */ /* eslint-disable */
export enum FileType { export enum FileType {
AUDIO = 'audio', AUDIO = 'audio',
VIDEO = 'video', VIDEO = 'video',

View File

@@ -1,9 +0,0 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export enum FileTypeFilter {
AUDIO_ONLY = 'audio_only',
VIDEO_ONLY = 'video_only',
BOTH = 'both',
}

View File

@@ -2,6 +2,7 @@
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */ /* eslint-disable */
export type GenerateArgsResponse = {
export interface GenerateArgsResponse {
args?: Array<string>; args?: Array<string>;
}; }

View File

@@ -2,6 +2,7 @@
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */ /* eslint-disable */
export type GenerateNewApiKeyResponse = {
export interface GenerateNewApiKeyResponse {
new_api_key: string; new_api_key: string;
}; }

View File

@@ -2,8 +2,8 @@
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */ /* eslint-disable */
import type { Category } from './Category'; import { Category } from './Category';
export type GetAllCategoriesResponse = { export interface GetAllCategoriesResponse {
categories: Array<Category>; categories: Array<Category>;
}; }

View File

@@ -2,9 +2,10 @@
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */ /* eslint-disable */
export type GetAllDownloadsRequest = {
export interface GetAllDownloadsRequest {
/** /**
* Filters downloads with the array * Filters downloads with the array
*/ */
uids?: Array<string> | null; uids?: Array<string> | null;
}; }

View File

@@ -2,8 +2,8 @@
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */ /* eslint-disable */
import type { Download } from './Download'; import { Download } from './Download';
export type GetAllDownloadsResponse = { export interface GetAllDownloadsResponse {
downloads?: Array<Download>; downloads?: Array<Download>;
}; }

View File

@@ -1,20 +0,0 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { FileTypeFilter } from './FileTypeFilter';
import type { Sort } from './Sort';
export type GetAllFilesRequest = {
sort?: Sort;
range?: Array<number>;
/**
* Filter files by title
*/
text_search?: string;
file_type_filter?: FileTypeFilter;
/**
* Include if you want to filter by subscription
*/
sub_id?: string;
};

View File

@@ -2,13 +2,13 @@
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */ /* eslint-disable */
import type { DatabaseFile } from './DatabaseFile'; import { DatabaseFile } from './DatabaseFile';
import type { Playlist } from './Playlist'; import { Playlist } from './Playlist';
export type GetAllFilesResponse = { export interface GetAllFilesResponse {
files: Array<DatabaseFile>; files: Array<DatabaseFile>;
/** /**
* All video playlists * All video playlists
*/ */
playlists: Array<Playlist>; playlists: Array<Playlist>;
}; }

View File

@@ -2,8 +2,8 @@
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */ /* eslint-disable */
import type { Subscription } from './Subscription'; import { Subscription } from './Subscription';
export type GetAllSubscriptionsResponse = { export interface GetAllSubscriptionsResponse {
subscriptions: Array<Subscription>; subscriptions: Array<Subscription>;
}; }

View File

@@ -2,8 +2,8 @@
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */ /* eslint-disable */
import type { Task } from './Task'; import { Task } from './Task';
export type GetAllTasksResponse = { export interface GetAllTasksResponse {
tasks?: Array<Task>; tasks?: Array<Task>;
}; }

View File

@@ -2,8 +2,8 @@
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */ /* eslint-disable */
import type { DBBackup } from './DBBackup'; import { DBBackup } from './DBBackup';
export type GetDBBackupsResponse = { export interface GetDBBackupsResponse {
tasks?: Array<DBBackup>; tasks?: Array<DBBackup>;
}; }

View File

@@ -2,6 +2,7 @@
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */ /* eslint-disable */
export type GetDownloadRequest = {
export interface GetDownloadRequest {
download_uid: string; download_uid: string;
}; }

View File

@@ -2,8 +2,8 @@
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */ /* eslint-disable */
import type { Download } from './Download'; import { Download } from './Download';
export type GetDownloadResponse = { export interface GetDownloadResponse {
download?: Download; download?: Download;
}; }

View File

@@ -2,6 +2,7 @@
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */ /* eslint-disable */
export type GetFileFormatsRequest = {
export interface GetFileFormatsRequest {
url?: string; url?: string;
}; }

View File

@@ -2,9 +2,11 @@
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */ /* eslint-disable */
export type GetFileFormatsResponse = { import { File } from './File';
export interface GetFileFormatsResponse {
success: boolean; success: boolean;
result: { result: {
formats?: Array<any>; formats?: Array<any>,
};
}; };
}

View File

@@ -2,9 +2,9 @@
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */ /* eslint-disable */
import type { FileType } from './FileType'; import { FileType } from './FileType';
export type GetFileRequest = { export interface GetFileRequest {
/** /**
* Video UID * Video UID
*/ */
@@ -14,4 +14,4 @@ export type GetFileRequest = {
* User UID * User UID
*/ */
uuid?: string; uuid?: string;
}; }

View File

@@ -2,9 +2,9 @@
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */ /* eslint-disable */
import type { DatabaseFile } from './DatabaseFile'; import { DatabaseFile } from './DatabaseFile';
export type GetFileResponse = { export interface GetFileResponse {
success: boolean; success: boolean;
file?: DatabaseFile; file?: DatabaseFile;
}; }

View File

@@ -2,10 +2,10 @@
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */ /* eslint-disable */
import type { FileType } from './FileType'; import { FileType } from './FileType';
import type { Subscription } from './Subscription'; import { Subscription } from './Subscription';
export type GetFullTwitchChatRequest = { export interface GetFullTwitchChatRequest {
/** /**
* File ID * File ID
*/ */
@@ -16,4 +16,4 @@ export type GetFullTwitchChatRequest = {
*/ */
uuid?: string; uuid?: string;
sub?: Subscription; sub?: Subscription;
}; }

View File

@@ -2,7 +2,8 @@
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */ /* eslint-disable */
export type GetFullTwitchChatResponse = {
export interface GetFullTwitchChatResponse {
success: boolean; success: boolean;
error?: string; error?: string;
}; }

View File

@@ -2,6 +2,7 @@
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */ /* eslint-disable */
export type GetLogsRequest = {
export interface GetLogsRequest {
lines?: number; lines?: number;
}; }

View File

@@ -2,10 +2,11 @@
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */ /* eslint-disable */
export type GetLogsResponse = {
export interface GetLogsResponse {
/** /**
* Number of lines to retrieve from the bottom * Number of lines to retrieve from the bottom
*/ */
logs?: string; logs?: string;
success?: boolean; success?: boolean;
}; }

View File

@@ -2,13 +2,13 @@
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */ /* eslint-disable */
import type { DatabaseFile } from './DatabaseFile'; import { DatabaseFile } from './DatabaseFile';
import type { Playlist } from './Playlist'; import { Playlist } from './Playlist';
export type GetMp3sResponse = { export interface GetMp3sResponse {
mp3s: Array<DatabaseFile>; mp3s: Array<DatabaseFile>;
/** /**
* All audio playlists * All audio playlists
*/ */
playlists: Array<Playlist>; playlists: Array<Playlist>;
}; }

View File

@@ -2,13 +2,13 @@
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */ /* eslint-disable */
import type { DatabaseFile } from './DatabaseFile'; import { DatabaseFile } from './DatabaseFile';
import type { Playlist } from './Playlist'; import { Playlist } from './Playlist';
export type GetMp4sResponse = { export interface GetMp4sResponse {
mp4s: Array<DatabaseFile>; mp4s: Array<DatabaseFile>;
/** /**
* All video playlists * All video playlists
*/ */
playlists: Array<Playlist>; playlists: Array<Playlist>;
}; }

View File

@@ -2,11 +2,11 @@
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */ /* eslint-disable */
import type { FileType } from './FileType'; import { FileType } from './FileType';
export type GetPlaylistRequest = { export interface GetPlaylistRequest {
playlist_id: string; playlist_id: string;
type?: FileType; type?: FileType;
uuid?: string; uuid?: string;
include_file_metadata?: boolean; include_file_metadata?: boolean;
}; }

View File

@@ -2,14 +2,11 @@
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */ /* eslint-disable */
import type { DatabaseFile } from './DatabaseFile'; import { FileType } from './FileType';
import type { Playlist } from './Playlist'; import { Playlist } from './Playlist';
export type GetPlaylistResponse = { export interface GetPlaylistResponse {
playlist: Playlist; playlist: Playlist;
type: FileType;
success: boolean; success: boolean;
/** }
* File objects for every uid in the playlist's uids property, in the same order
*/
file_objs?: Array<DatabaseFile>;
};

View File

@@ -2,6 +2,7 @@
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */ /* eslint-disable */
export type GetPlaylistsRequest = {
export interface GetPlaylistsRequest {
include_categories?: boolean; include_categories?: boolean;
}; }

View File

@@ -2,8 +2,8 @@
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */ /* eslint-disable */
import type { Playlist } from './Playlist'; import { Playlist } from './Playlist';
export type GetPlaylistsResponse = { export interface GetPlaylistsResponse {
playlists: Array<Playlist>; playlists: Array<Playlist>;
}; }

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