mirror of
https://github.com/Tzahi12345/YoutubeDL-Material.git
synced 2026-03-08 04:20:08 +03:00
Compare commits
421 Commits
downloadin
...
python3-do
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ccb4819a94 | ||
|
|
ce8f90ca1d | ||
|
|
8469ae10ad | ||
|
|
f0e73c1708 | ||
|
|
aa1e36ae35 | ||
|
|
a1841e84ca | ||
|
|
05909877f4 | ||
|
|
90af895552 | ||
|
|
9f908aa3fc | ||
|
|
b56b371ece | ||
|
|
84e54cb4d5 | ||
|
|
42aaecc13a | ||
|
|
aac11b2105 | ||
|
|
bbf94ef982 | ||
|
|
2876cf55db | ||
|
|
375d3b4f38 | ||
|
|
160cffc737 | ||
|
|
7aad7b7d24 | ||
|
|
380475b33e | ||
|
|
384d365cf9 | ||
|
|
d6a43c76a4 | ||
|
|
407333a314 | ||
|
|
0fb01469c4 | ||
|
|
d10eb4f2eb | ||
|
|
148ed9aa65 | ||
|
|
1125de43d7 | ||
|
|
00b591a9a4 | ||
|
|
06d9793d1a | ||
|
|
0a2529330d | ||
|
|
19317dbddb | ||
|
|
3b74a2b5da | ||
|
|
a810628f15 | ||
|
|
a7d349a71a | ||
|
|
f8c4653ae0 | ||
|
|
bb6503e86d | ||
|
|
dbbfc041a4 | ||
|
|
342dafd52a | ||
|
|
984e990103 | ||
|
|
4ea239170e | ||
|
|
e2c31319cf | ||
|
|
b933af03e2 | ||
|
|
419fe3c3c6 | ||
|
|
07b48a4da1 | ||
|
|
a11445b80d | ||
|
|
297a4a3f34 | ||
|
|
1d2ab0dc41 | ||
|
|
46f8579439 | ||
|
|
b3744e616d | ||
|
|
de154a9c3e | ||
|
|
9e71b1ff12 | ||
|
|
6d318234b6 | ||
|
|
49925848ff | ||
|
|
356a807cad | ||
|
|
4e07440ed2 | ||
|
|
59c9237be5 | ||
|
|
4ba4710741 | ||
|
|
addd54fefd | ||
|
|
aefdde5401 | ||
|
|
4c1f975eae | ||
|
|
4c06bc750c | ||
|
|
4643efbae0 | ||
|
|
d11f77a6c9 | ||
|
|
1f0153b17e | ||
|
|
f32b394715 | ||
|
|
9d09eeffe3 | ||
|
|
c660c28422 | ||
|
|
669c87dd1b | ||
|
|
023df9c29d | ||
|
|
433d08e9df | ||
|
|
e34aa4d9d6 | ||
|
|
3f9314a0c3 | ||
|
|
00a0ab460b | ||
|
|
a1b32e2851 | ||
|
|
b8cab673ae | ||
|
|
6481102e01 | ||
|
|
af58854f0e | ||
|
|
d7d861ef0e | ||
|
|
1d5490c0ff | ||
|
|
28ee77cee0 | ||
|
|
133d848729 | ||
|
|
a78f4e99d0 | ||
|
|
539bc5094a | ||
|
|
f0f2faa398 | ||
|
|
7835185fe0 | ||
|
|
95bb69f16b | ||
|
|
a93aa080b3 | ||
|
|
ed1375d40b | ||
|
|
db78e4ad5e | ||
|
|
6ef0082563 | ||
|
|
b978007472 | ||
|
|
c09dd7a03b | ||
|
|
b6c09324d9 | ||
|
|
1a900399d8 | ||
|
|
ea959547fd | ||
|
|
085849c7ee | ||
|
|
cf1dd43d36 | ||
|
|
250f150587 | ||
|
|
dbf08e1276 | ||
|
|
f74ce4b865 | ||
|
|
8e4e0c7908 | ||
|
|
b0cb09309d | ||
|
|
75c1c9e9b7 | ||
|
|
c19e0bb881 | ||
|
|
a1af5496c7 | ||
|
|
3c206c31d5 | ||
|
|
3ffcfac28b | ||
|
|
0e7bc1979f | ||
|
|
33fc74b7e7 | ||
|
|
c08993e20b | ||
|
|
4835093606 | ||
|
|
c63a64ebef | ||
|
|
9a57080bb3 | ||
|
|
1cc4df2829 | ||
|
|
6eb6ffa5e4 | ||
|
|
2656147570 | ||
|
|
88a1c31090 | ||
|
|
3f1532b4c6 | ||
|
|
afb5e3800c | ||
|
|
2971580f91 | ||
|
|
4cbfab20e0 | ||
|
|
7e06d30205 | ||
|
|
e75b56ad3f | ||
|
|
441a470990 | ||
|
|
eb7661c14a | ||
|
|
59c38321fd | ||
|
|
9847577431 | ||
|
|
0fec9d71a0 | ||
|
|
5f13205017 | ||
|
|
cd93313cfc | ||
|
|
8058b743eb | ||
|
|
e3374c573a | ||
|
|
29b8dc227c | ||
|
|
c30350205f | ||
|
|
ff8886d2e0 | ||
|
|
43b0c2fb9e | ||
|
|
e39e8f3dba | ||
|
|
da3bd2600f | ||
|
|
6ad590497b | ||
|
|
4f693d4eda | ||
|
|
9de403245b | ||
|
|
ff1bb8dee1 | ||
|
|
3f10986cdf | ||
|
|
c6fc5352c5 | ||
|
|
f425b9842f | ||
|
|
8c916d8fe4 | ||
|
|
bb18e1427e | ||
|
|
1542436e96 | ||
|
|
b0acb63123 | ||
|
|
0713eda7e2 | ||
|
|
d08fee1223 | ||
|
|
8938844ffa | ||
|
|
9895d77e01 | ||
|
|
27437a615f | ||
|
|
b730bc5adc | ||
|
|
d15d262b87 | ||
|
|
1aade1202d | ||
|
|
2f541a49df | ||
|
|
d93481640c | ||
|
|
71814cbdc9 | ||
|
|
09832ad15b | ||
|
|
cc78091403 | ||
|
|
cb88c7bc7c | ||
|
|
98f4828db4 | ||
|
|
8f0739c0f9 | ||
|
|
ab355d62a0 | ||
|
|
4d2d9a6b10 | ||
|
|
89dfac1249 | ||
|
|
d4f81eb0ab | ||
|
|
6b7d0681d2 | ||
|
|
b32fdb2445 | ||
|
|
b059c7ed5e | ||
|
|
8d87cbb08d | ||
|
|
1bb2f54eba | ||
|
|
7392338d6e | ||
|
|
82df92a72d | ||
|
|
9e4b328f91 | ||
|
|
3a049a99ac | ||
|
|
b323b548ca | ||
|
|
568463487f | ||
|
|
3318ac364d | ||
|
|
1ce85813fb | ||
|
|
6ea4176d63 | ||
|
|
3aa08e1817 | ||
|
|
727b047c39 | ||
|
|
d659a7614f | ||
|
|
6ad9d5ea8e | ||
|
|
edc22cc47b | ||
|
|
0189d292a8 | ||
|
|
deac54e8d6 | ||
|
|
d4e5082039 | ||
|
|
6f089491a5 | ||
|
|
0a38b01971 | ||
|
|
fe7303a191 | ||
|
|
dff4b141b0 | ||
|
|
fed0a54145 | ||
|
|
8366089444 | ||
|
|
44445f0b67 | ||
|
|
7dcc38c26d | ||
|
|
79b4b993f8 | ||
|
|
37a19eabe6 | ||
|
|
91b892b21a | ||
|
|
fb72dee26f | ||
|
|
3e4e7edd90 | ||
|
|
b8280e8646 | ||
|
|
70ee071e57 | ||
|
|
e26ac82c66 | ||
|
|
cdd2f78998 | ||
|
|
21eafeab22 | ||
|
|
f535d18cb9 | ||
|
|
2c43ce3c47 | ||
|
|
3d2d4efb31 | ||
|
|
10922fedd7 | ||
|
|
96cf1b87d1 | ||
|
|
6bed5851ed | ||
|
|
6717a59422 | ||
|
|
899633e124 | ||
|
|
8fdc231f08 | ||
|
|
ae8f7a2a33 | ||
|
|
d0782bb444 | ||
|
|
49210abb49 | ||
|
|
8595864118 | ||
|
|
851bfb81ba | ||
|
|
35d0d439fa | ||
|
|
ded3ad6dfc | ||
|
|
61daf26641 | ||
|
|
95e53b9549 | ||
|
|
46ed0fe992 | ||
|
|
082252ab1e | ||
|
|
5eccaa13e5 | ||
|
|
71633950b2 | ||
|
|
f31dad0215 | ||
|
|
7efbe40bb2 | ||
|
|
5b768b5bda | ||
|
|
365cbc3ffa | ||
|
|
44647f3306 | ||
|
|
8a7409478a | ||
|
|
70159813e5 | ||
|
|
babba9aa30 | ||
|
|
d292275956 | ||
|
|
ba2acedb94 | ||
|
|
aa0558b770 | ||
|
|
d7f04fc90a | ||
|
|
087c9f1bb1 | ||
|
|
f874617965 | ||
|
|
8fb8543829 | ||
|
|
70d89d310c | ||
|
|
c48aaaf13c | ||
|
|
6cf7ea193a | ||
|
|
c9016f446d | ||
|
|
919e2a649a | ||
|
|
dc6dd5f5a2 | ||
|
|
2e4ef3b224 | ||
|
|
0e35b2ca1b | ||
|
|
9937bc9bb6 | ||
|
|
178a61c381 | ||
|
|
b455d8a900 | ||
|
|
72fa439569 | ||
|
|
33b1affa73 | ||
|
|
da73e47f08 | ||
|
|
3faf715b88 | ||
|
|
eda75e9a19 | ||
|
|
560aaadca1 | ||
|
|
eb7fdb649d | ||
|
|
533dd49ed0 | ||
|
|
001d907c3a | ||
|
|
4302625858 | ||
|
|
e2cec9321e | ||
|
|
2add31af6d | ||
|
|
5de37f6fbf | ||
|
|
7aace85ef4 | ||
|
|
736c3f5cab | ||
|
|
c3c7667c17 | ||
|
|
52bee8b280 | ||
|
|
945ba268fb | ||
|
|
d49a67dfd0 | ||
|
|
96c52f2d5b | ||
|
|
61c03b6681 | ||
|
|
7c349163b6 | ||
|
|
a0acdd1d86 | ||
|
|
25bfb9e518 | ||
|
|
c9b615c659 | ||
|
|
c4f21dc1cc | ||
|
|
e678f4b476 | ||
|
|
356f31343e | ||
|
|
38c46b5be5 | ||
|
|
12dcdfcb3c | ||
|
|
47c19c0cdc | ||
|
|
d0eff42f2a | ||
|
|
4472aae3e9 | ||
|
|
c55d3de9a0 | ||
|
|
1cdc1640ac | ||
|
|
fcaf8b5a62 | ||
|
|
8c7b2dfc79 | ||
|
|
59ad74ed79 | ||
|
|
690871f6b2 | ||
|
|
5088ce0291 | ||
|
|
576e2109d7 | ||
|
|
c6c80579df | ||
|
|
0ab6535fec | ||
|
|
d7aa39599d | ||
|
|
ee169cd7ce | ||
|
|
835790e69c | ||
|
|
68037613d8 | ||
|
|
3df384de22 | ||
|
|
f0c4ed4590 | ||
|
|
fd35153721 | ||
|
|
d0aed8f144 | ||
|
|
d17b68d76e | ||
|
|
a481869166 | ||
|
|
eb4ed32fcb | ||
|
|
9aee6e91cd | ||
|
|
37d3e9326c | ||
|
|
dbf8f9ebfd | ||
|
|
7858e26b15 | ||
|
|
057ad67672 | ||
|
|
3ebc903ce9 | ||
|
|
21c5795f1c | ||
|
|
f12ea017bc | ||
|
|
cd18bce509 | ||
|
|
5ef4388d73 | ||
|
|
a78c0cb56c | ||
|
|
ae9e4e6857 | ||
|
|
333556c305 | ||
|
|
c5b0a7a697 | ||
|
|
d7631360cc | ||
|
|
c800308b9d | ||
|
|
c1c57135ba | ||
|
|
8384b73c4c | ||
|
|
834ac00694 | ||
|
|
935ae3452c | ||
|
|
cc189a3abd | ||
|
|
335b588c3a | ||
|
|
493abc3a4c | ||
|
|
238abc1686 | ||
|
|
fbfad6c3e2 | ||
|
|
a6ff65f004 | ||
|
|
89cd969fcb | ||
|
|
4ebb2d4297 | ||
|
|
d371ccf094 | ||
|
|
e7181b57c7 | ||
|
|
38fa39d765 | ||
|
|
9e5de88675 | ||
|
|
9cf4949c30 | ||
|
|
dd80c51f16 | ||
|
|
84d83f228e | ||
|
|
14bd82c508 | ||
|
|
0b6606cafb | ||
|
|
e97e9ec717 | ||
|
|
990b3d4037 | ||
|
|
2e7b1c2d53 | ||
|
|
a4eb7fb745 | ||
|
|
2442067ca0 | ||
|
|
41d4dfeba1 | ||
|
|
a9f197e46d | ||
|
|
3732d13562 | ||
|
|
cf14880d21 | ||
|
|
e81d0cab42 | ||
|
|
7e24180f03 | ||
|
|
053c8db9dd | ||
|
|
5537852134 | ||
|
|
efdc471ccf | ||
|
|
6f1b37d5eb | ||
|
|
06557673a2 | ||
|
|
c20d09e902 | ||
|
|
a68ecfa730 | ||
|
|
86c609c1b2 | ||
|
|
d100e80ccf | ||
|
|
5511c94071 | ||
|
|
b21886d8f8 | ||
|
|
e535603103 | ||
|
|
9415901f17 | ||
|
|
92e5716f93 | ||
|
|
5b5c93f783 | ||
|
|
4db6a49df5 | ||
|
|
3abb29eee4 | ||
|
|
e5461de2f7 | ||
|
|
4a69a0d362 | ||
|
|
97f7f0b462 | ||
|
|
d3cbfa265e | ||
|
|
42bd219ed6 | ||
|
|
f8123cf03b | ||
|
|
94df98e5d0 | ||
|
|
2998562655 | ||
|
|
09d8ce04d7 | ||
|
|
504c818c2f | ||
|
|
ca0e6b993d | ||
|
|
0346833c3b | ||
|
|
32da9dd9dd | ||
|
|
20f162d794 | ||
|
|
319bb0160b | ||
|
|
5983a8bd52 | ||
|
|
49b8cd416e | ||
|
|
58f71469b5 | ||
|
|
db81120645 | ||
|
|
163a88bcfd | ||
|
|
2441270d88 | ||
|
|
a518ac680f | ||
|
|
78d3145e0b | ||
|
|
b8a4e0773f | ||
|
|
f04139634a | ||
|
|
a074166903 | ||
|
|
6893dbd506 | ||
|
|
e8ee4ffb64 | ||
|
|
378025bd9d | ||
|
|
d8e85df6d6 | ||
|
|
0c864c3d8d | ||
|
|
dd8ab9be29 | ||
|
|
bab354ce81 | ||
|
|
d3d0f92ea5 | ||
|
|
b37d912e04 | ||
|
|
1c93a4f9f2 | ||
|
|
abfe0dad03 | ||
|
|
5bfecfcefe | ||
|
|
ffe3133635 | ||
|
|
68c67ca7d5 | ||
|
|
c4d50c9018 | ||
|
|
42b749a101 | ||
|
|
9c729abfaa | ||
|
|
dcc7fbd81c | ||
|
|
b3c8f9e57a | ||
|
|
9e5ad66a9d |
98
.github/workflows/build.yml
vendored
Normal file
98
.github/workflows/build.yml
vendored
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
name: continuous integration
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [master, feat/*]
|
||||||
|
tags:
|
||||||
|
- v*
|
||||||
|
pull_request:
|
||||||
|
branches: [master]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: checkout code
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
- name: setup node
|
||||||
|
uses: actions/setup-node@v2
|
||||||
|
with:
|
||||||
|
node-version: '12'
|
||||||
|
cache: 'npm'
|
||||||
|
- name: install dependencies
|
||||||
|
run: |
|
||||||
|
npm install
|
||||||
|
cd backend
|
||||||
|
npm install
|
||||||
|
sudo npm install -g @angular/cli
|
||||||
|
- name: build
|
||||||
|
run: ng build --prod
|
||||||
|
- name: prepare artifact upload
|
||||||
|
shell: pwsh
|
||||||
|
run: |
|
||||||
|
New-Item -Name build -ItemType Directory
|
||||||
|
New-Item -Path build -Name youtubedl-material -ItemType Directory
|
||||||
|
Copy-Item -Path ./backend/appdata -Recurse -Destination ./build/youtubedl-material
|
||||||
|
Copy-Item -Path ./backend/audio -Recurse -Destination ./build/youtubedl-material
|
||||||
|
Copy-Item -Path ./backend/authentication -Recurse -Destination ./build/youtubedl-material
|
||||||
|
Copy-Item -Path ./backend/public -Recurse -Destination ./build/youtubedl-material
|
||||||
|
Copy-Item -Path ./backend/subscriptions -Recurse -Destination ./build/youtubedl-material
|
||||||
|
Copy-Item -Path ./backend/video -Recurse -Destination ./build/youtubedl-material
|
||||||
|
New-Item -Path ./build/youtubedl-material -Name users
|
||||||
|
Copy-Item -Path ./backend/*.js -Destination ./build/youtubedl-material
|
||||||
|
Copy-Item -Path ./backend/*.json -Destination ./build/youtubedl-material
|
||||||
|
- name: upload build artifact
|
||||||
|
uses: actions/upload-artifact@v1
|
||||||
|
with:
|
||||||
|
name: youtubedl-material
|
||||||
|
path: build
|
||||||
|
release:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: build
|
||||||
|
if: contains(github.ref, '/tags/v')
|
||||||
|
steps:
|
||||||
|
- name: checkout code
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
- name: create release
|
||||||
|
id: create_release
|
||||||
|
uses: actions/create-release@v1
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
with:
|
||||||
|
tag_name: ${{ github.ref }}
|
||||||
|
release_name: YoutubeDL-Material ${{ github.ref }}
|
||||||
|
body: |
|
||||||
|
# New features
|
||||||
|
# Minor additions
|
||||||
|
# Bug fixes
|
||||||
|
draft: true
|
||||||
|
prerelease: false
|
||||||
|
- name: download build artifact
|
||||||
|
uses: actions/download-artifact@v1
|
||||||
|
with:
|
||||||
|
name: youtubedl-material
|
||||||
|
path: ${{runner.temp}}/youtubedl-material
|
||||||
|
- name: extract tag name
|
||||||
|
id: tag_name
|
||||||
|
run: echo ::set-output name=tag_name::${GITHUB_REF#refs/tags/}
|
||||||
|
- name: prepare release asset
|
||||||
|
shell: pwsh
|
||||||
|
run: Compress-Archive -Path ${{runner.temp}}/youtubedl-material -DestinationPath youtubedl-material-${{ steps.tag_name.outputs.tag_name }}.zip
|
||||||
|
- name: upload release asset
|
||||||
|
uses: actions/upload-release-asset@v1
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
with:
|
||||||
|
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||||
|
asset_path: ./youtubedl-material-${{ steps.tag_name.outputs.tag_name }}.zip
|
||||||
|
asset_name: youtubedl-material-${{ steps.tag_name.outputs.tag_name }}.zip
|
||||||
|
asset_content_type: application/zip
|
||||||
|
- name: upload docker-compose asset
|
||||||
|
uses: actions/upload-release-asset@v1
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
with:
|
||||||
|
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||||
|
asset_path: ./docker-compose.yml
|
||||||
|
asset_name: docker-compose.yml
|
||||||
|
asset_content_type: text/plain
|
||||||
71
.github/workflows/codeql-analysis.yml
vendored
Normal file
71
.github/workflows/codeql-analysis.yml
vendored
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
# For most projects, this workflow file will not need changing; you simply need
|
||||||
|
# to commit it to your repository.
|
||||||
|
#
|
||||||
|
# You may wish to alter this file to override the set of languages analyzed,
|
||||||
|
# or to provide custom queries or build logic.
|
||||||
|
name: "CodeQL"
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [master]
|
||||||
|
pull_request:
|
||||||
|
# The branches below must be a subset of the branches above
|
||||||
|
branches: [master]
|
||||||
|
schedule:
|
||||||
|
- cron: '0 12 * * 6'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
analyze:
|
||||||
|
name: Analyze
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
# Override automatic language detection by changing the below list
|
||||||
|
# Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python']
|
||||||
|
language: ['javascript']
|
||||||
|
# Learn more...
|
||||||
|
# https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
with:
|
||||||
|
# We must fetch at least the immediate parents so that if this is
|
||||||
|
# a pull request then we can checkout the head.
|
||||||
|
fetch-depth: 2
|
||||||
|
|
||||||
|
# If this run was triggered by a pull request event, then checkout
|
||||||
|
# the head of the pull request instead of the merge commit.
|
||||||
|
- run: git checkout HEAD^2
|
||||||
|
if: ${{ github.event_name == 'pull_request' }}
|
||||||
|
|
||||||
|
# Initializes the CodeQL tools for scanning.
|
||||||
|
- name: Initialize CodeQL
|
||||||
|
uses: github/codeql-action/init@v1
|
||||||
|
with:
|
||||||
|
languages: ${{ matrix.language }}
|
||||||
|
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||||
|
# By default, queries listed here will override any specified in a config file.
|
||||||
|
# Prefix the list here with "+" to use these queries and those in the config file.
|
||||||
|
# queries: ./path/to/local/query, your-org/your-repo/queries@main
|
||||||
|
|
||||||
|
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||||
|
# If this step fails, then you should remove it and run the build manually (see below)
|
||||||
|
- name: Autobuild
|
||||||
|
uses: github/codeql-action/autobuild@v1
|
||||||
|
|
||||||
|
# ℹ️ Command-line programs to run using the OS shell.
|
||||||
|
# 📚 https://git.io/JvXDl
|
||||||
|
|
||||||
|
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
|
||||||
|
# and modify them (or add more) to build your code if your project
|
||||||
|
# uses a compiled language
|
||||||
|
|
||||||
|
#- run: |
|
||||||
|
# make bootstrap
|
||||||
|
# make release
|
||||||
|
|
||||||
|
- name: Perform CodeQL Analysis
|
||||||
|
uses: github/codeql-action/analyze@v1
|
||||||
32
.github/workflows/docker-release.yml
vendored
Normal file
32
.github/workflows/docker-release.yml
vendored
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
name: docker-release
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
tags:
|
||||||
|
description: 'Docker tags'
|
||||||
|
required: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-push:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: checkout code
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
- name: setup platform emulator
|
||||||
|
uses: docker/setup-qemu-action@v1
|
||||||
|
- name: setup multi-arch docker build
|
||||||
|
uses: docker/setup-buildx-action@v1
|
||||||
|
- name: Login to DockerHub
|
||||||
|
uses: docker/login-action@v1
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
- name: build & push images
|
||||||
|
uses: docker/build-push-action@v2
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: ./Dockerfile
|
||||||
|
platforms: linux/amd64,linux/arm,linux/arm64/v8
|
||||||
|
push: true
|
||||||
|
tags: ${{ github.event.inputs.tags }}
|
||||||
29
.github/workflows/docker.yml
vendored
Normal file
29
.github/workflows/docker.yml
vendored
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
name: docker
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [master]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-push:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: checkout code
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
- name: setup platform emulator
|
||||||
|
uses: docker/setup-qemu-action@v1
|
||||||
|
- name: setup multi-arch docker build
|
||||||
|
uses: docker/setup-buildx-action@v1
|
||||||
|
- name: Login to DockerHub
|
||||||
|
uses: docker/login-action@v1
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
- name: build & push images
|
||||||
|
uses: docker/build-push-action@v2
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: ./Dockerfile
|
||||||
|
platforms: linux/amd64,linux/arm,linux/arm64/v8
|
||||||
|
push: true
|
||||||
|
tags: tzahi12345/youtubedl-material:nightly
|
||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -53,6 +53,7 @@ backend/public/assets/default.json
|
|||||||
backend/subscriptions/channels/*
|
backend/subscriptions/channels/*
|
||||||
backend/subscriptions/playlists/*
|
backend/subscriptions/playlists/*
|
||||||
backend/subscriptions/archives/*
|
backend/subscriptions/archives/*
|
||||||
|
backend/*.exe
|
||||||
src/assets/default.json
|
src/assets/default.json
|
||||||
backend/appdata/db.json
|
backend/appdata/db.json
|
||||||
backend/appdata/archives/archive_audio.txt
|
backend/appdata/archives/archive_audio.txt
|
||||||
@@ -63,3 +64,5 @@ backend/appdata/logs/combined.log
|
|||||||
backend/appdata/logs/error.log
|
backend/appdata/logs/error.log
|
||||||
backend/appdata/users.json
|
backend/appdata/users.json
|
||||||
backend/users/*
|
backend/users/*
|
||||||
|
backend/appdata/cookies.txt
|
||||||
|
backend/public
|
||||||
25
.vscode/tasks.json
vendored
Normal file
25
.vscode/tasks.json
vendored
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"version": "2.0.0",
|
||||||
|
"tasks": [
|
||||||
|
{
|
||||||
|
"type": "npm",
|
||||||
|
"script": "start",
|
||||||
|
"problemMatcher": [],
|
||||||
|
"label": "Dev: start frontend",
|
||||||
|
"detail": "ng serve"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Dev: start backend",
|
||||||
|
"type": "shell",
|
||||||
|
"command": "set YTDL_MODE=debug && node app.js",
|
||||||
|
"options": {
|
||||||
|
"cwd": "./backend"
|
||||||
|
},
|
||||||
|
"presentation": {
|
||||||
|
"reveal": "always",
|
||||||
|
"panel": "new"
|
||||||
|
},
|
||||||
|
"problemMatcher": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
48
Dockerfile
Normal file
48
Dockerfile
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
FROM alpine:3.12 as frontend
|
||||||
|
|
||||||
|
RUN apk add --no-cache \
|
||||||
|
npm
|
||||||
|
|
||||||
|
RUN npm install -g @angular/cli
|
||||||
|
|
||||||
|
WORKDIR /build
|
||||||
|
COPY [ "package.json", "package-lock.json", "/build/" ]
|
||||||
|
RUN npm install
|
||||||
|
|
||||||
|
COPY [ "angular.json", "tsconfig.json", "/build/" ]
|
||||||
|
COPY [ "src/", "/build/src/" ]
|
||||||
|
RUN ng build --prod
|
||||||
|
|
||||||
|
#--------------#
|
||||||
|
|
||||||
|
FROM alpine:3.12
|
||||||
|
|
||||||
|
ENV UID=1000 \
|
||||||
|
GID=1000 \
|
||||||
|
USER=youtube
|
||||||
|
|
||||||
|
ENV NO_UPDATE_NOTIFIER=true
|
||||||
|
ENV FOREVER_ROOT=/app/.forever
|
||||||
|
|
||||||
|
RUN addgroup -S $USER -g $GID && adduser -D -S $USER -G $USER -u $UID
|
||||||
|
|
||||||
|
RUN apk add --no-cache \
|
||||||
|
ffmpeg \
|
||||||
|
npm \
|
||||||
|
python2 \
|
||||||
|
python3 \
|
||||||
|
su-exec \
|
||||||
|
&& apk add --no-cache --repository http://dl-cdn.alpinelinux.org/alpine/edge/testing/ \
|
||||||
|
atomicparsley
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
COPY --chown=$UID:$GID [ "backend/package.json", "backend/package-lock.json", "/app/" ]
|
||||||
|
RUN npm install forever -g
|
||||||
|
RUN npm install && chown -R $UID:$GID ./
|
||||||
|
|
||||||
|
COPY --chown=$UID:$GID --from=frontend [ "/build/backend/public/", "/app/public/" ]
|
||||||
|
COPY --chown=$UID:$GID [ "/backend/", "/app/" ]
|
||||||
|
|
||||||
|
EXPOSE 17442
|
||||||
|
ENTRYPOINT [ "/app/entrypoint.sh" ]
|
||||||
|
CMD [ "forever", "app.js" ]
|
||||||
@@ -261,12 +261,12 @@ paths:
|
|||||||
$ref: '#/components/schemas/inline_response_200_10'
|
$ref: '#/components/schemas/inline_response_200_10'
|
||||||
security:
|
security:
|
||||||
- Auth query parameter: []
|
- Auth query parameter: []
|
||||||
/api/getAllSubscriptions:
|
/api/getSubscriptions:
|
||||||
post:
|
post:
|
||||||
tags:
|
tags:
|
||||||
- subscriptions
|
- subscriptions
|
||||||
summary: Get all subscriptions
|
summary: Get all subscriptions
|
||||||
operationId: post-api-getAllSubscriptions
|
operationId: post-api-getSubscriptions
|
||||||
requestBody:
|
requestBody:
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
|
|||||||
106
README.md
106
README.md
@@ -1,12 +1,12 @@
|
|||||||
# YoutubeDL-Material
|
# YoutubeDL-Material
|
||||||
[](https://hub.docker.com/r/tzahi12345/youtubedl-material)
|
|
||||||
[](https://hub.docker.com/r/tzahi12345/youtubedl-material)
|
|
||||||
[](https://heroku.com/deploy?template=https://github.com/Tzahi12345/YoutubeDL-Material)
|
|
||||||
[](https://github.com/Tzahi12345/YoutubeDL-Material/issues)
|
|
||||||
[](https://github.com/Tzahi12345/YoutubeDL-Material/blob/master/LICENSE.md)
|
|
||||||
|
|
||||||
|
[](https://hub.docker.com/r/tzahi12345/youtubedl-material)
|
||||||
|
[](https://hub.docker.com/r/tzahi12345/youtubedl-material)
|
||||||
|
[](https://heroku.com/deploy?template=https://github.com/Tzahi12345/YoutubeDL-Material)
|
||||||
|
[](https://github.com/Tzahi12345/YoutubeDL-Material/issues)
|
||||||
|
[](https://github.com/Tzahi12345/YoutubeDL-Material/blob/master/LICENSE.md)
|
||||||
|
|
||||||
YoutubeDL-Material is a Material Design frontend for [youtube-dl](https://rg3.github.io/youtube-dl/). It's coded using [Angular 9](https://angular.io/) for the frontend, and [Node.js](https://nodejs.org/) on the backend.
|
YoutubeDL-Material is a Material Design frontend for [youtube-dl](https://rg3.github.io/youtube-dl/). It's coded using [Angular 11](https://angular.io/) for the frontend, and [Node.js](https://nodejs.org/) on the backend.
|
||||||
|
|
||||||
Now with [Docker](#Docker) support!
|
Now with [Docker](#Docker) support!
|
||||||
|
|
||||||
@@ -16,34 +16,42 @@ Check out the prerequisites, and go to the installation section. Easy as pie!
|
|||||||
|
|
||||||
Here's an image of what it'll look like once you're done:
|
Here's an image of what it'll look like once you're done:
|
||||||
|
|
||||||

|
<img src="https://i.imgur.com/C6vFGbL.png" width="800">
|
||||||
|
|
||||||
With optional file management enabled (default):
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
Dark mode:
|
Dark mode:
|
||||||
|
|
||||||

|
<img src="https://i.imgur.com/vOtvH5w.png" width="800">
|
||||||
|
|
||||||
### Prerequisites
|
### Prerequisites
|
||||||
|
|
||||||
NOTE: If you would like to use Docker, you can skip down to the [Docker](#Docker) section for a setup guide.
|
NOTE: If you would like to use Docker, you can skip down to the [Docker](#Docker) section for a setup guide.
|
||||||
|
|
||||||
Make sure you have these dependencies installed on your system: nodejs and youtube-dl. If you don't, run this command:
|
Debian/Ubuntu:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo apt-get install nodejs youtube-dl ffmpeg unzip python npm
|
||||||
```
|
```
|
||||||
sudo apt-get install nodejs youtube-dl
|
|
||||||
|
CentOS 7:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo yum install epel-release
|
||||||
|
sudo yum localinstall --nogpgcheck https://download1.rpmfusion.org/free/el/rpmfusion-free-release-7.noarch.rpm
|
||||||
|
sudo yum install centos-release-scl-rh
|
||||||
|
sudo yum install rh-nodejs12
|
||||||
|
scl enable rh-nodejs12 bash
|
||||||
|
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`)
|
||||||
|
|
||||||
### Installing
|
### Installing
|
||||||
|
|
||||||
1. First, download the [latest release](https://github.com/Tzahi12345/YoutubeDL-Material/releases/latest)!
|
1. First, download the [latest release](https://github.com/Tzahi12345/YoutubeDL-Material/releases/latest)!
|
||||||
|
|
||||||
2. Drag the `youtubedl-material` directory to an easily accessible directory. Navigate to the `appdata` folder and edit the `default.json` file. If you're using SSL encryption, look at the `encrypted.json` file for a template.
|
2. Drag the `youtubedl-material` directory to an easily accessible directory. Navigate to the `appdata` folder and edit the `default.json` file.
|
||||||
|
|
||||||
NOTE: If you are intending to use a [reverse proxy](https://github.com/Tzahi12345/YoutubeDL-Material/wiki/Reverse-Proxy-Setup), this next step is not necessary
|
NOTE: If you are intending to use a [reverse proxy](https://github.com/Tzahi12345/YoutubeDL-Material/wiki/Reverse-Proxy-Setup), this next step is not necessary
|
||||||
|
|
||||||
@@ -53,41 +61,13 @@ NOTE: If you are intending to use a [reverse proxy](https://github.com/Tzahi1234
|
|||||||
|
|
||||||
If you experience problems, know that it's usually caused by a configuration problem. The first thing you should do is check the console. To get there, right click anywhere on the page and click "Inspect element." Then on the menu that pops up, click console. Look at the error there, and try to investigate.
|
If you experience problems, know that it's usually caused by a configuration problem. The first thing you should do is check the console. To get there, right click anywhere on the page and click "Inspect element." Then on the menu that pops up, click console. Look at the error there, and try to investigate.
|
||||||
|
|
||||||
### Configuration
|
|
||||||
|
|
||||||
NOTE: If you are using YoutubeDL-Material v3.2 or lower, click [here](https://github.com/Tzahi12345/YoutubeDL-Material/blob/b87a9f1e2fd896b8e3b2f12429b7ffb15ea4521b/README.md#configuration) for the old README
|
|
||||||
|
|
||||||
Here is an explanation for the configuration entries. Check out the [default config](https://github.com/Tzahi12345/YoutubeDL-Material/blob/master/backend/config/default.json) for more context.
|
|
||||||
|
|
||||||
| Config item | Description | Default |
|
|
||||||
| ------------- | ------------- | ------------- |
|
|
||||||
| url | URL to the server hosting YoutubeDL-Material | "http://example.com" |
|
|
||||||
| port | Desired port for YoutubeDL-Material | "17442" |
|
|
||||||
| use-encryption | true if you intend to use SSL encryption (https) | false |
|
|
||||||
| cert-file-path | Cert file path - required if using encryption | "/etc/letsencrypt/live/example.com/fullchain.pem" |
|
|
||||||
| key-file-path | Private key file path - required if using encryption | "/etc/letsencrypt/live/example.com/privkey.pem" |
|
|
||||||
| path-audio | Path to audio folder for saved mp3s | "audio/" |
|
|
||||||
| path-video | Path to video folder for saved mp4s | "video/" |
|
|
||||||
| title_top | Title shown on the top toolbar | "Youtube Downloader" |
|
|
||||||
| file_manager_enabled | true if you want to use the file manager | true |
|
|
||||||
| allow_quality_select | true if you want to select a videos quality level before downloading | true |
|
|
||||||
| download_only_mode | true if you want files to directly download to the client with no media player | false |
|
|
||||||
| allow_multi_download_mode | true if you want the ability to download multiple videos at the same time | true |
|
|
||||||
| use_youtube_API | true if you want to use the Youtube API which is used for YT searches | false |
|
|
||||||
| youtube_API_key | Youtube API key. Required if use_youtube_API is enabled | "" |
|
|
||||||
| default_theme | Default theme to use. Options are "default" and "dark" | "default" |
|
|
||||||
| allow_theme_change | true if you want the icon in the top toolbar that toggles dark mode | true |
|
|
||||||
| use_default_downloading_agent | true if you want to use youtube-dl's default downloader | true |
|
|
||||||
| custom_downloading_agent | If not using the default downloader, this is the downloader you want to use | "" |
|
|
||||||
| allow_advanced_download | true if you want to use the Advanced download options | false |
|
|
||||||
|
|
||||||
## Build it yourself
|
## Build it yourself
|
||||||
|
|
||||||
If you'd like to install YoutubeDL-Material, go to the Installation section. If you want to build it yourself and/or develop the repository, then this section is for you.
|
If you'd like to install YoutubeDL-Material, go to the Installation section. If you want to build it yourself and/or develop the repository, then this section is for you.
|
||||||
|
|
||||||
To deploy, simply clone the repository, and go into the `youtubedl-material` directory. Type `npm install` and all the dependencies will install. Then type `cd backend` and again type `npm install` to install the dependencies for the backend.
|
To deploy, simply clone the repository, and go into the `youtubedl-material` directory. Type `npm install` and all the dependencies will install. Then type `cd backend` and again type `npm install` to install the dependencies for the backend.
|
||||||
|
|
||||||
Once you do that, you're almost up and running. All you need to do is edit the configuration in `youtubedl-material/appdata`, go back into the `youtubedl-material` directory, and type `ng build --prod`. This will build the app, and put the output files in the `youtubedl-material/dist` folder. Drag those files into the `public` directory in the `backend` folder.
|
Once you do that, you're almost up and running. All you need to do is edit the configuration in `youtubedl-material/appdata`, go back into the `youtubedl-material` directory, and type `ng build --prod`. This will build the app, and put the output files in the `youtubedl-material/backend/public` folder.
|
||||||
|
|
||||||
The frontend is now complete. The backend is much easier. Just go into the `backend` folder, and type `npm start`.
|
The frontend is now complete. The backend is much easier. Just go into the `backend` folder, and type `npm start`.
|
||||||
|
|
||||||
@@ -97,18 +77,30 @@ Alternatively, you can port forward the port specified in the config (defaults t
|
|||||||
|
|
||||||
## Docker
|
## Docker
|
||||||
|
|
||||||
|
### Setup
|
||||||
|
|
||||||
If you are looking to setup YoutubeDL-Material with Docker, this section is for you. And you're in luck! Docker setup is quite simple.
|
If you are looking to setup YoutubeDL-Material with Docker, this section is for you. And you're in luck! Docker setup is quite simple.
|
||||||
|
|
||||||
1. Run `curl -L https://github.com/Tzahi12345/YoutubeDL-Material/releases/latest/download/youtubedl-material-docker.zip -o youtubedl-material-docker.zip` to download the latest Docker zip release, or go to the [releases](https://github.com/Tzahi12345/YoutubeDL-Material/releases/) page to grab the version you'd like.
|
1. Run `curl -L https://github.com/Tzahi12345/YoutubeDL-Material/releases/latest/download/docker-compose.yml -o docker-compose.yml` to download the latest Docker Compose, or go to the [releases](https://github.com/Tzahi12345/YoutubeDL-Material/releases/) page to grab the version you'd like.
|
||||||
2. Unzip the `youtubedl-material-docker.zip` and navigate into the root folder.
|
2. Run `docker-compose pull`. This will download the official YoutubeDL-Material docker image.
|
||||||
3. Modify the config items in the `appdata` folder to your liking. The default options will work, however, and point to `http://localhost:8998`. You can find an explanation of these configuration items in [Configuration](#Configuration) section.
|
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. Run `docker-compose pull`. This will download the official YoutubeDL-Material docker image.
|
4. Make sure you can connect to the specified URL + *external* port, and if so, you are done!
|
||||||
5. Run `docker-compose up` to start it up. If successful, it should say "HTTP(S): Started on port 8998" or something similar.
|
|
||||||
6. Make sure you can connect to the specified URL + 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
|
||||||
|
|
||||||
|
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:
|
||||||
|
|
||||||
|
```yml
|
||||||
|
environment:
|
||||||
|
UID: YOUR_UID
|
||||||
|
GID: YOUR_GID
|
||||||
|
```
|
||||||
|
|
||||||
## API
|
## API
|
||||||
|
|
||||||
[API Docs](https://stoplight.io/p/docs/gh/tzahi12345/youtubedl-material?group=master&utm_campaign=publish_dialog&utm_source=studio)
|
[API Docs](https://youtubedl-material.stoplight.io/docs/youtubedl-material/Public%20API%20v1.yaml)
|
||||||
|
|
||||||
To get started, go to the settings menu and enable the public API from the *Extra* tab. You can generate an API key if one is missing.
|
To get started, go to the settings menu and enable the public API from the *Extra* tab. You can generate an API key if one is missing.
|
||||||
|
|
||||||
@@ -116,12 +108,22 @@ Once you have enabled the API and have the key, you can start sending requests b
|
|||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
Feel free to submit a pull request! I have no guidelines as of yet, so no need to worry about that.
|
If you're interested in contributing, first: awesome! Second, please refer to the guidelines/setup information located in the [Contributing](https://github.com/Tzahi12345/YoutubeDL-Material/wiki/Contributing) wiki page, it's a helpful way to get you on your feet and coding away.
|
||||||
|
|
||||||
|
Pull requests are always appreciated! If you're a bit rusty with coding, that's no problem: we can always help you learn. And if that's too scary, that's OK too! You can create issues for features you'd like to see or bugs you encounter, it all helps this project grow.
|
||||||
|
|
||||||
|
If you're interested in translating the app into a new language, check out the [Translate](https://github.com/Tzahi12345/YoutubeDL-Material/wiki/Translate) wiki page.
|
||||||
|
|
||||||
## Authors
|
## Authors
|
||||||
|
|
||||||
* **Isaac Grynsztein** (me!) - *Initial work*
|
* **Isaac Grynsztein** (me!) - *Initial work*
|
||||||
|
|
||||||
|
Official translators:
|
||||||
|
|
||||||
|
* Spanish - tzahi12345
|
||||||
|
* German - UnlimitedCookies
|
||||||
|
* Chinese - TyRoyal
|
||||||
|
|
||||||
See also the list of [contributors](https://github.com/your/project/contributors) who participated in this project.
|
See also the list of [contributors](https://github.com/your/project/contributors) who participated in this project.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|||||||
@@ -45,8 +45,6 @@
|
|||||||
],
|
],
|
||||||
"optimization": true,
|
"optimization": true,
|
||||||
"outputHashing": "all",
|
"outputHashing": "all",
|
||||||
"sourceMap": false,
|
|
||||||
"extractCss": true,
|
|
||||||
"namedChunks": false,
|
"namedChunks": false,
|
||||||
"aot": true,
|
"aot": true,
|
||||||
"extractLicenses": true,
|
"extractLicenses": true,
|
||||||
|
|||||||
49
armhf.Dockerfile
Normal file
49
armhf.Dockerfile
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
FROM alpine:3.12 as frontend
|
||||||
|
|
||||||
|
RUN apk add --no-cache \
|
||||||
|
npm \
|
||||||
|
curl
|
||||||
|
|
||||||
|
RUN npm install -g @angular/cli
|
||||||
|
|
||||||
|
WORKDIR /build
|
||||||
|
|
||||||
|
RUN curl -L https://github.com/balena-io/qemu/releases/download/v3.0.0%2Bresin/qemu-3.0.0+resin-arm.tar.gz | tar zxvf - -C . && mv qemu-3.0.0+resin-arm/qemu-arm-static .
|
||||||
|
|
||||||
|
COPY [ "package.json", "package-lock.json", "/build/" ]
|
||||||
|
RUN npm install
|
||||||
|
|
||||||
|
COPY [ "angular.json", "tsconfig.json", "/build/" ]
|
||||||
|
COPY [ "src/", "/build/src/" ]
|
||||||
|
RUN ng build --prod
|
||||||
|
|
||||||
|
#--------------#
|
||||||
|
|
||||||
|
FROM arm32v7/alpine:3.12
|
||||||
|
|
||||||
|
COPY --from=frontend /build/qemu-arm-static /usr/bin
|
||||||
|
|
||||||
|
ENV UID=1000 \
|
||||||
|
GID=1000 \
|
||||||
|
USER=youtube
|
||||||
|
|
||||||
|
RUN addgroup -S $USER -g $GID && adduser -D -S $USER -G $USER -u $UID
|
||||||
|
|
||||||
|
RUN apk add --no-cache \
|
||||||
|
ffmpeg \
|
||||||
|
npm \
|
||||||
|
python2 \
|
||||||
|
su-exec \
|
||||||
|
&& apk add --no-cache --repository http://dl-cdn.alpinelinux.org/alpine/edge/testing/ \
|
||||||
|
atomicparsley
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
COPY --chown=$UID:$GID [ "backend/package.json", "backend/package-lock.json", "/app/" ]
|
||||||
|
RUN npm install && chown -R $UID:$GID ./
|
||||||
|
|
||||||
|
COPY --chown=$UID:$GID --from=frontend [ "/build/backend/public/", "/app/public/" ]
|
||||||
|
COPY --chown=$UID:$GID [ "/backend/", "/app/" ]
|
||||||
|
|
||||||
|
EXPOSE 17442
|
||||||
|
ENTRYPOINT [ "/app/entrypoint.sh" ]
|
||||||
|
CMD [ "node", "app.js" ]
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
FROM alpine:3.11
|
|
||||||
|
|
||||||
RUN \
|
|
||||||
apk add --no-cache npm python ffmpeg && \
|
|
||||||
apk add --no-cache --repository http://dl-cdn.alpinelinux.org/alpine/edge/testing/ \
|
|
||||||
atomicparsley
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
COPY package.json /app/
|
|
||||||
|
|
||||||
RUN npm install
|
|
||||||
|
|
||||||
COPY ./ /app/
|
|
||||||
|
|
||||||
EXPOSE 17442
|
|
||||||
|
|
||||||
CMD [ "node", "app.js" ]
|
|
||||||
2724
backend/app.js
2724
backend/app.js
File diff suppressed because it is too large
Load Diff
@@ -4,16 +4,15 @@
|
|||||||
"url": "http://example.com",
|
"url": "http://example.com",
|
||||||
"port": "17442"
|
"port": "17442"
|
||||||
},
|
},
|
||||||
"Encryption": {
|
|
||||||
"use-encryption": false,
|
|
||||||
"cert-file-path": "/etc/letsencrypt/live/example.com/fullchain.pem",
|
|
||||||
"key-file-path": "/etc/letsencrypt/live/example.com/privkey.pem"
|
|
||||||
},
|
|
||||||
"Downloader": {
|
"Downloader": {
|
||||||
"path-audio": "audio/",
|
"path-audio": "audio/",
|
||||||
"path-video": "video/",
|
"path-video": "video/",
|
||||||
|
"default_file_output": "",
|
||||||
"use_youtubedl_archive": false,
|
"use_youtubedl_archive": false,
|
||||||
"custom_args": ""
|
"custom_args": "",
|
||||||
|
"safe_download_override": false,
|
||||||
|
"include_thumbnail": true,
|
||||||
|
"include_metadata": true
|
||||||
},
|
},
|
||||||
"Extra": {
|
"Extra": {
|
||||||
"title_top": "YoutubeDL-Material",
|
"title_top": "YoutubeDL-Material",
|
||||||
@@ -21,14 +20,17 @@
|
|||||||
"allow_quality_select": true,
|
"allow_quality_select": true,
|
||||||
"download_only_mode": false,
|
"download_only_mode": false,
|
||||||
"allow_multi_download_mode": true,
|
"allow_multi_download_mode": true,
|
||||||
"settings_pin_required": false,
|
"enable_downloads_manager": true,
|
||||||
"enable_downloads_manager": true
|
"allow_playlist_categorization": true
|
||||||
},
|
},
|
||||||
"API": {
|
"API": {
|
||||||
"use_API_key": false,
|
"use_API_key": false,
|
||||||
"API_key": "",
|
"API_key": "",
|
||||||
"use_youtube_API": false,
|
"use_youtube_API": false,
|
||||||
"youtube_API_key": ""
|
"youtube_API_key": "",
|
||||||
|
"use_twitch_API": false,
|
||||||
|
"twitch_API_key": "",
|
||||||
|
"twitch_auto_download_chat": false
|
||||||
},
|
},
|
||||||
"Themes": {
|
"Themes": {
|
||||||
"default_theme": "default",
|
"default_theme": "default",
|
||||||
@@ -38,18 +40,32 @@
|
|||||||
"allow_subscriptions": true,
|
"allow_subscriptions": true,
|
||||||
"subscriptions_base_path": "subscriptions/",
|
"subscriptions_base_path": "subscriptions/",
|
||||||
"subscriptions_check_interval": "300",
|
"subscriptions_check_interval": "300",
|
||||||
"subscriptions_use_youtubedl_archive": true
|
"redownload_fresh_uploads": false
|
||||||
},
|
},
|
||||||
"Users": {
|
"Users": {
|
||||||
"base_path": "users/",
|
"base_path": "users/",
|
||||||
"allow_registration": true
|
"allow_registration": true,
|
||||||
|
"auth_method": "internal",
|
||||||
|
"ldap_config": {
|
||||||
|
"url": "ldap://localhost:389",
|
||||||
|
"bindDN": "cn=root",
|
||||||
|
"bindCredentials": "secret",
|
||||||
|
"searchBase": "ou=passport-ldapauth",
|
||||||
|
"searchFilter": "(uid={{username}})"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Database": {
|
||||||
|
"use_local_db": false,
|
||||||
|
"mongodb_connection_string": "mongodb://127.0.0.1:27017/?compressors=zlib"
|
||||||
},
|
},
|
||||||
"Advanced": {
|
"Advanced": {
|
||||||
|
"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,
|
||||||
"allow_advanced_download": false,
|
"allow_advanced_download": false,
|
||||||
"use_cookies": false,
|
"use_cookies": false,
|
||||||
|
"jwt_expiration": 86400,
|
||||||
"logger_level": "info"
|
"logger_level": "info"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,56 +0,0 @@
|
|||||||
{
|
|
||||||
"YoutubeDLMaterial": {
|
|
||||||
"Host": {
|
|
||||||
"url": "https://example.com",
|
|
||||||
"port": "17442"
|
|
||||||
},
|
|
||||||
"Encryption": {
|
|
||||||
"use-encryption": true,
|
|
||||||
"cert-file-path": "/etc/letsencrypt/live/example.com/fullchain.pem",
|
|
||||||
"key-file-path": "/etc/letsencrypt/live/example.com/privkey.pem"
|
|
||||||
},
|
|
||||||
"Downloader": {
|
|
||||||
"path-audio": "audio/",
|
|
||||||
"path-video": "video/",
|
|
||||||
"use_youtubedl_archive": false,
|
|
||||||
"custom_args": ""
|
|
||||||
},
|
|
||||||
"Extra": {
|
|
||||||
"title_top": "YoutubeDL-Material",
|
|
||||||
"file_manager_enabled": true,
|
|
||||||
"allow_quality_select": true,
|
|
||||||
"download_only_mode": false,
|
|
||||||
"allow_multi_download_mode": true,
|
|
||||||
"settings_pin_required": false,
|
|
||||||
"enable_downloads_manager": true
|
|
||||||
},
|
|
||||||
"API": {
|
|
||||||
"use_API_key": false,
|
|
||||||
"API_key": "",
|
|
||||||
"use_youtube_API": false,
|
|
||||||
"youtube_API_key": ""
|
|
||||||
},
|
|
||||||
"Themes": {
|
|
||||||
"default_theme": "default",
|
|
||||||
"allow_theme_change": true
|
|
||||||
},
|
|
||||||
"Subscriptions": {
|
|
||||||
"allow_subscriptions": true,
|
|
||||||
"subscriptions_base_path": "subscriptions/",
|
|
||||||
"subscriptions_check_interval": "300",
|
|
||||||
"subscriptions_use_youtubedl_archive": true
|
|
||||||
},
|
|
||||||
"Users": {
|
|
||||||
"base_path": "users/",
|
|
||||||
"allow_registration": true
|
|
||||||
},
|
|
||||||
"Advanced": {
|
|
||||||
"use_default_downloading_agent": true,
|
|
||||||
"custom_downloading_agent": "",
|
|
||||||
"multi_user_mode": false,
|
|
||||||
"allow_advanced_download": false,
|
|
||||||
"use_cookies": false,
|
|
||||||
"logger_level": "info"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Binary file not shown.
@@ -1,52 +1,49 @@
|
|||||||
const path = require('path');
|
const path = require('path');
|
||||||
const config_api = require('../config');
|
const config_api = require('../config');
|
||||||
const consts = require('../consts');
|
const consts = require('../consts');
|
||||||
var subscriptions_api = require('../subscriptions')
|
|
||||||
const fs = require('fs-extra');
|
const fs = require('fs-extra');
|
||||||
var jwt = require('jsonwebtoken');
|
const jwt = require('jsonwebtoken');
|
||||||
const { uuid } = require('uuidv4');
|
const { uuid } = require('uuidv4');
|
||||||
var bcrypt = require('bcrypt');
|
const bcrypt = require('bcryptjs');
|
||||||
|
|
||||||
|
|
||||||
var LocalStrategy = require('passport-local').Strategy;
|
var LocalStrategy = require('passport-local').Strategy;
|
||||||
|
var LdapStrategy = require('passport-ldapauth');
|
||||||
var JwtStrategy = require('passport-jwt').Strategy,
|
var JwtStrategy = require('passport-jwt').Strategy,
|
||||||
ExtractJwt = require('passport-jwt').ExtractJwt;
|
ExtractJwt = require('passport-jwt').ExtractJwt;
|
||||||
|
|
||||||
// other required vars
|
// other required vars
|
||||||
let logger = null;
|
let logger = null;
|
||||||
var users_db = null;
|
let db_api = null;
|
||||||
let SERVER_SECRET = null;
|
let SERVER_SECRET = null;
|
||||||
let JWT_EXPIRATION = null;
|
let JWT_EXPIRATION = null;
|
||||||
let opts = null;
|
let opts = null;
|
||||||
let saltRounds = null;
|
let saltRounds = null;
|
||||||
|
|
||||||
exports.initialize = function(input_users_db, input_logger) {
|
exports.initialize = function(db_api, input_logger) {
|
||||||
setLogger(input_logger)
|
setLogger(input_logger)
|
||||||
setDB(input_users_db);
|
setDB(db_api);
|
||||||
|
|
||||||
/*************************
|
/*************************
|
||||||
* Authentication module
|
* Authentication module
|
||||||
************************/
|
************************/
|
||||||
saltRounds = 10;
|
saltRounds = 10;
|
||||||
|
|
||||||
JWT_EXPIRATION = (60 * 60); // one hour
|
JWT_EXPIRATION = config_api.getConfigItem('ytdl_jwt_expiration');
|
||||||
|
|
||||||
SERVER_SECRET = null;
|
SERVER_SECRET = null;
|
||||||
if (users_db.get('jwt_secret').value()) {
|
if (db_api.users_db.get('jwt_secret').value()) {
|
||||||
SERVER_SECRET = users_db.get('jwt_secret').value();
|
SERVER_SECRET = db_api.users_db.get('jwt_secret').value();
|
||||||
} else {
|
} else {
|
||||||
SERVER_SECRET = uuid();
|
SERVER_SECRET = uuid();
|
||||||
users_db.set('jwt_secret', SERVER_SECRET).write();
|
db_api.users_db.set('jwt_secret', SERVER_SECRET).write();
|
||||||
}
|
}
|
||||||
|
|
||||||
opts = {}
|
opts = {}
|
||||||
opts.jwtFromRequest = ExtractJwt.fromUrlQueryParameter('jwt');
|
opts.jwtFromRequest = ExtractJwt.fromUrlQueryParameter('jwt');
|
||||||
opts.secretOrKey = SERVER_SECRET;
|
opts.secretOrKey = SERVER_SECRET;
|
||||||
/*opts.issuer = 'example.com';
|
|
||||||
opts.audience = 'example.com';*/
|
|
||||||
|
|
||||||
exports.passport.use(new JwtStrategy(opts, function(jwt_payload, done) {
|
exports.passport.use(new JwtStrategy(opts, async function(jwt_payload, done) {
|
||||||
const user = users_db.get('users').find({uid: jwt_payload.user}).value();
|
const user = await db_api.getRecord('users', {uid: jwt_payload.user});
|
||||||
if (user) {
|
if (user) {
|
||||||
return done(null, user);
|
return done(null, user);
|
||||||
} else {
|
} else {
|
||||||
@@ -60,8 +57,8 @@ function setLogger(input_logger) {
|
|||||||
logger = input_logger;
|
logger = input_logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
function setDB(input_users_db) {
|
function setDB(input_db_api) {
|
||||||
users_db = input_users_db;
|
db_api = input_db_api;
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.passport = require('passport');
|
exports.passport = require('passport');
|
||||||
@@ -69,7 +66,7 @@ exports.passport = require('passport');
|
|||||||
exports.passport.serializeUser(function(user, done) {
|
exports.passport.serializeUser(function(user, done) {
|
||||||
done(null, user);
|
done(null, user);
|
||||||
});
|
});
|
||||||
|
|
||||||
exports.passport.deserializeUser(function(user, done) {
|
exports.passport.deserializeUser(function(user, done) {
|
||||||
done(null, user);
|
done(null, user);
|
||||||
});
|
});
|
||||||
@@ -77,7 +74,7 @@ exports.passport.deserializeUser(function(user, done) {
|
|||||||
/***************************************
|
/***************************************
|
||||||
* Register user with hashed password
|
* Register user with hashed password
|
||||||
**************************************/
|
**************************************/
|
||||||
exports.registerUser = function(req, res) {
|
exports.registerUser = async function(req, res) {
|
||||||
var userid = req.body.userid;
|
var userid = req.body.userid;
|
||||||
var username = req.body.username;
|
var username = req.body.username;
|
||||||
var plaintextPassword = req.body.password;
|
var plaintextPassword = req.body.password;
|
||||||
@@ -87,39 +84,28 @@ exports.registerUser = function(req, res) {
|
|||||||
logger.error(`Registration failed for user ${userid}. Registration is disabled.`);
|
logger.error(`Registration failed for user ${userid}. Registration is disabled.`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (plaintextPassword === "") {
|
||||||
|
res.sendStatus(400);
|
||||||
|
logger.error(`Registration failed for user ${userid}. A password must be provided.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
bcrypt.hash(plaintextPassword, saltRounds)
|
bcrypt.hash(plaintextPassword, saltRounds)
|
||||||
.then(function(hash) {
|
.then(async function(hash) {
|
||||||
let new_user = {
|
let new_user = generateUserObject(userid, username, hash);
|
||||||
name: username,
|
|
||||||
uid: userid,
|
|
||||||
passhash: hash,
|
|
||||||
files: {
|
|
||||||
audio: [],
|
|
||||||
video: []
|
|
||||||
},
|
|
||||||
playlists: {
|
|
||||||
audio: [],
|
|
||||||
video: []
|
|
||||||
},
|
|
||||||
subscriptions: [],
|
|
||||||
created: Date.now(),
|
|
||||||
role: userid === 'admin' ? 'admin' : 'user',
|
|
||||||
permissions: [],
|
|
||||||
permission_overrides: []
|
|
||||||
};
|
|
||||||
// check if user exists
|
// check if user exists
|
||||||
if (users_db.get('users').find({uid: userid}).value()) {
|
if (await db_api.getRecord('users', {uid: userid})) {
|
||||||
// user id is taken!
|
// user id is taken!
|
||||||
logger.error('Registration failed: UID is already taken!');
|
logger.error('Registration failed: UID is already taken!');
|
||||||
res.status(409).send('UID is already taken!');
|
res.status(409).send('UID is already taken!');
|
||||||
} else if (users_db.get('users').find({name: username}).value()) {
|
} else if (await db_api.getRecord('users', {name: username})) {
|
||||||
// user name is taken!
|
// user name is taken!
|
||||||
logger.error('Registration failed: User name is already taken!');
|
logger.error('Registration failed: User name is already taken!');
|
||||||
res.status(409).send('User name is already taken!');
|
res.status(409).send('User name is already taken!');
|
||||||
} else {
|
} else {
|
||||||
// add to db
|
// add to db
|
||||||
users_db.get('users').push(new_user).write();
|
await db_api.insertRecordIntoTable('users', new_user);
|
||||||
logger.verbose(`New user created: ${new_user.name}`);
|
logger.verbose(`New user created: ${new_user.name}`);
|
||||||
res.send({
|
res.send({
|
||||||
user: new_user
|
user: new_user
|
||||||
@@ -127,7 +113,7 @@ exports.registerUser = function(req, res) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.then(function(result) {
|
.then(function(result) {
|
||||||
|
|
||||||
})
|
})
|
||||||
.catch(function(err) {
|
.catch(function(err) {
|
||||||
logger.error(err);
|
logger.error(err);
|
||||||
@@ -146,59 +132,52 @@ exports.registerUser = function(req, res) {
|
|||||||
/*************************************************
|
/*************************************************
|
||||||
* This gets called when passport.authenticate()
|
* This gets called when passport.authenticate()
|
||||||
* gets called.
|
* gets called.
|
||||||
*
|
*
|
||||||
* This checks that the credentials are valid.
|
* This checks that the credentials are valid.
|
||||||
* If so, passes the user info to the next middleware.
|
* If so, passes the user info to the next middleware.
|
||||||
************************************************/
|
************************************************/
|
||||||
|
|
||||||
|
|
||||||
|
exports.login = async (username, password) => {
|
||||||
|
const user = await db_api.getRecord('users', {name: username});
|
||||||
|
if (!user) { logger.error(`User ${username} not found`); false }
|
||||||
|
if (user.auth_method && user.auth_method !== 'internal') { return false }
|
||||||
|
return await bcrypt.compare(password, user.passhash) ? user : false;
|
||||||
|
}
|
||||||
|
|
||||||
exports.passport.use(new LocalStrategy({
|
exports.passport.use(new LocalStrategy({
|
||||||
usernameField: 'userid',
|
usernameField: 'username',
|
||||||
passwordField: 'password'},
|
passwordField: 'password'},
|
||||||
function(username, password, done) {
|
async function(username, password, done) {
|
||||||
const user = users_db.get('users').find({name: username}).value();
|
return done(null, await exports.login(username, password));
|
||||||
if (!user) { logger.error(`User ${username} not found`); return done(null, false); }
|
|
||||||
if (user) {
|
|
||||||
return done(null, bcrypt.compareSync(password, user.passhash) ? user : false);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
));
|
));
|
||||||
|
|
||||||
/*passport.use(new BasicStrategy(
|
var getLDAPConfiguration = function(req, callback) {
|
||||||
function(userid, plainTextPassword, done) {
|
const ldap_config = config_api.getConfigItem('ytdl_ldap_config');
|
||||||
const user = users_db.get('users').find({name: userid}).value();
|
const opts = {server: ldap_config};
|
||||||
if (user) {
|
callback(null, opts);
|
||||||
var hashedPwd = user.passhash;
|
};
|
||||||
return bcrypt.compare(plainTextPassword, hashedPwd);
|
|
||||||
} else {
|
exports.passport.use(new LdapStrategy(getLDAPConfiguration,
|
||||||
return false;
|
async function(user, done) {
|
||||||
|
// check if ldap auth is enabled
|
||||||
|
const ldap_enabled = config_api.getConfigItem('ytdl_auth_method') === 'ldap';
|
||||||
|
if (!ldap_enabled) return done(null, false);
|
||||||
|
|
||||||
|
const user_uid = user.uid;
|
||||||
|
let db_user = await db_api.getRecord('users', {uid: user_uid});
|
||||||
|
if (!db_user) {
|
||||||
|
// generate DB user
|
||||||
|
let new_user = generateUserObject(user_uid, user_uid, null, 'ldap');
|
||||||
|
await db_api.insertRecordIntoTable('users', new_user);
|
||||||
|
db_user = new_user;
|
||||||
|
logger.verbose(`Generated new user ${user_uid} using LDAP`);
|
||||||
}
|
}
|
||||||
|
return done(null, db_user);
|
||||||
}
|
}
|
||||||
));
|
));
|
||||||
*/
|
|
||||||
|
|
||||||
/*************************************************************
|
|
||||||
* This is a wrapper for auth.passport.authenticate().
|
|
||||||
* We use this to change WWW-Authenticate header so
|
|
||||||
* the browser doesn't pop-up challenge dialog box by default.
|
|
||||||
* Browser's will pop-up up dialog when status is 401 and
|
|
||||||
* "WWW-Authenticate:Basic..."
|
|
||||||
*************************************************************/
|
|
||||||
/*
|
|
||||||
exports.authenticateViaPassport = function(req, res, next) {
|
|
||||||
exports.passport.authenticate('basic',{session:false},
|
|
||||||
function(err, user, info) {
|
|
||||||
if(!user){
|
|
||||||
res.set('WWW-Authenticate', 'x'+info); // change to xBasic
|
|
||||||
res.status(401).send('Invalid Authentication');
|
|
||||||
} else {
|
|
||||||
req.user = user;
|
|
||||||
next();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)(req, res, next);
|
|
||||||
};
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**********************************
|
/**********************************
|
||||||
* Generating/Signing a JWT token
|
* Generating/Signing a JWT token
|
||||||
@@ -212,27 +191,27 @@ exports.generateJWT = function(req, res, next) {
|
|||||||
, user: req.user.uid
|
, user: req.user.uid
|
||||||
};
|
};
|
||||||
req.token = jwt.sign(payload, SERVER_SECRET);
|
req.token = jwt.sign(payload, SERVER_SECRET);
|
||||||
next();
|
next();
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.returnAuthResponse = function(req, res) {
|
exports.returnAuthResponse = async function(req, res) {
|
||||||
res.status(200).json({
|
res.status(200).json({
|
||||||
user: req.user,
|
user: req.user,
|
||||||
token: req.token,
|
token: req.token,
|
||||||
permissions: exports.userPermissions(req.user.uid),
|
permissions: await exports.userPermissions(req.user.uid),
|
||||||
available_permissions: consts['AVAILABLE_PERMISSIONS']
|
available_permissions: consts['AVAILABLE_PERMISSIONS']
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/***************************************
|
/***************************************
|
||||||
* Authorization: middleware that checks the
|
* Authorization: middleware that checks the
|
||||||
* JWT token for validity before allowing
|
* JWT token for validity before allowing
|
||||||
* the user to access anything.
|
* the user to access anything.
|
||||||
*
|
*
|
||||||
* It also passes the user object to the next
|
* It also passes the user object to the next
|
||||||
* middleware through res.locals
|
* middleware through res.locals
|
||||||
**************************************/
|
**************************************/
|
||||||
exports.ensureAuthenticatedElseError = function(req, res, next) {
|
exports.ensureAuthenticatedElseError = (req, res, next) => {
|
||||||
var token = getToken(req.query);
|
var token = getToken(req.query);
|
||||||
if( token ) {
|
if( token ) {
|
||||||
try {
|
try {
|
||||||
@@ -250,29 +229,26 @@ exports.ensureAuthenticatedElseError = function(req, res, next) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// change password
|
// change password
|
||||||
exports.changeUserPassword = async function(user_uid, new_pass) {
|
exports.changeUserPassword = async (user_uid, new_pass) => {
|
||||||
return new Promise(resolve => {
|
try {
|
||||||
bcrypt.hash(new_pass, saltRounds)
|
const hash = await bcrypt.hash(new_pass, saltRounds);
|
||||||
.then(function(hash) {
|
await db_api.updateRecord('users', {uid: user_uid}, {passhash: hash});
|
||||||
users_db.get('users').find({uid: user_uid}).assign({passhash: hash}).write();
|
return true;
|
||||||
resolve(true);
|
} catch (err) {
|
||||||
}).catch(err => {
|
return false;
|
||||||
resolve(false);
|
}
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// change user permissions
|
// change user permissions
|
||||||
exports.changeUserPermissions = function(user_uid, permission, new_value) {
|
exports.changeUserPermissions = async (user_uid, permission, new_value) => {
|
||||||
try {
|
try {
|
||||||
const user_db_obj = users_db.get('users').find({uid: user_uid});
|
await db_api.pullFromRecordsArray('users', {uid: user_uid}, 'permissions', permission);
|
||||||
user_db_obj.get('permissions').pull(permission).write();
|
await db_api.pullFromRecordsArray('users', {uid: user_uid}, 'permission_overrides', permission);
|
||||||
user_db_obj.get('permission_overrides').pull(permission).write();
|
|
||||||
if (new_value === 'yes') {
|
if (new_value === 'yes') {
|
||||||
user_db_obj.get('permissions').push(permission).write();
|
await db_api.pushToRecordsArray('users', {uid: user_uid}, 'permissions', permission);
|
||||||
user_db_obj.get('permission_overrides').push(permission).write();
|
await db_api.pushToRecordsArray('users', {uid: user_uid}, 'permission_overrides', permission);
|
||||||
} else if (new_value === 'no') {
|
} else if (new_value === 'no') {
|
||||||
user_db_obj.get('permission_overrides').push(permission).write();
|
await db_api.pushToRecordsArray('users', {uid: user_uid}, 'permission_overrides', permission);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -282,12 +258,11 @@ exports.changeUserPermissions = function(user_uid, permission, new_value) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// change role permissions
|
// change role permissions
|
||||||
exports.changeRolePermissions = function(role, permission, new_value) {
|
exports.changeRolePermissions = async (role, permission, new_value) => {
|
||||||
try {
|
try {
|
||||||
const role_db_obj = users_db.get('roles').get(role);
|
await db_api.pullFromRecordsArray('roles', {key: role}, 'permissions', permission);
|
||||||
role_db_obj.get('permissions').pull(permission).write();
|
|
||||||
if (new_value === 'yes') {
|
if (new_value === 'yes') {
|
||||||
role_db_obj.get('permissions').push(permission).write();
|
await db_api.pushToRecordsArray('roles', {key: role}, 'permissions', permission);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -296,68 +271,42 @@ exports.changeRolePermissions = function(role, permission, new_value) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.adminExists = function() {
|
exports.adminExists = async function() {
|
||||||
return !!users_db.get('users').find({uid: 'admin'}).value();
|
return !!(await db_api.getRecord('users', {uid: 'admin'}));
|
||||||
}
|
}
|
||||||
|
|
||||||
// video stuff
|
// video stuff
|
||||||
|
|
||||||
exports.getUserVideos = function(user_uid, type) {
|
exports.getUserVideos = async function(user_uid, type) {
|
||||||
const user = users_db.get('users').find({uid: user_uid}).value();
|
const files = await db_api.getRecords('files', {user_uid: user_uid});
|
||||||
return user['files'][type];
|
return type ? files.filter(file => file.isAudio === (type === 'audio')) : files;
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.getUserVideo = function(user_uid, file_uid, type, requireSharing = false) {
|
exports.getUserVideo = async function(user_uid, file_uid, requireSharing = false) {
|
||||||
if (!type) {
|
let file = await db_api.getRecord('files', {file_uid: file_uid});
|
||||||
file = users_db.get('users').find({uid: user_uid}).get(`files.audio`).find({uid: file_uid}).value();
|
|
||||||
if (!file) {
|
|
||||||
file = users_db.get('users').find({uid: user_uid}).get(`files.video`).find({uid: file_uid}).value();
|
|
||||||
if (file) type = 'video';
|
|
||||||
} else {
|
|
||||||
type = 'audio';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!file && type) file = users_db.get('users').find({uid: user_uid}).get(`files.${type}`).find({uid: file_uid}).value();
|
|
||||||
|
|
||||||
// prevent unauthorized users from accessing the file info
|
// prevent unauthorized users from accessing the file info
|
||||||
if (requireSharing && !file['sharingEnabled']) file = null;
|
if (file && !file['sharingEnabled'] && requireSharing) file = null;
|
||||||
|
|
||||||
return file;
|
return file;
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.addPlaylist = function(user_uid, new_playlist, type) {
|
exports.updatePlaylistFiles = function(user_uid, playlistID, new_filenames) {
|
||||||
users_db.get('users').find({uid: user_uid}).get(`playlists.${type}`).push(new_playlist).write();
|
users_db.get('users').find({uid: user_uid}).get(`playlists`).find({id: playlistID}).assign({fileNames: new_filenames});
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.updatePlaylist = function(user_uid, playlistID, new_filenames, type) {
|
exports.removePlaylist = async function(user_uid, playlistID) {
|
||||||
users_db.get('users').find({uid: user_uid}).get(`playlists.${type}`).find({id: playlistID}).assign({fileNames: new_filenames});
|
await db_api.removeRecord('playlist', {playlistID: playlistID});
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.removePlaylist = function(user_uid, playlistID, type) {
|
exports.getUserPlaylists = async function(user_uid, user_files = null) {
|
||||||
users_db.get('users').find({uid: user_uid}).get(`playlists.${type}`).remove({id: playlistID}).write();
|
return await db_api.getRecords('playlists', {user_uid: user_uid});
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.getUserPlaylists = function(user_uid, type) {
|
exports.getUserPlaylist = async function(user_uid, playlistID, requireSharing = false) {
|
||||||
const user = users_db.get('users').find({uid: user_uid}).value();
|
let playlist = await db_api.getRecord('playlists', {id: playlistID});
|
||||||
return user['playlists'][type];
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.getUserPlaylist = function(user_uid, playlistID, type, requireSharing = false) {
|
|
||||||
let playlist = null;
|
|
||||||
if (!type) {
|
|
||||||
playlist = users_db.get('users').find({uid: user_uid}).get(`playlists.audio`).find({id: playlistID}).value();
|
|
||||||
if (!playlist) {
|
|
||||||
playlist = users_db.get('users').find({uid: user_uid}).get(`playlists.video`).find({id: playlistID}).value();
|
|
||||||
if (playlist) type = 'video';
|
|
||||||
} else {
|
|
||||||
type = 'audio';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!playlist) playlist = users_db.get('users').find({uid: user_uid}).get(`playlists.${type}`).find({id: playlistID}).value();
|
|
||||||
|
|
||||||
// prevent unauthorized users from accessing the file info
|
// prevent unauthorized users from accessing the file info
|
||||||
if (requireSharing && !playlist['sharingEnabled']) playlist = null;
|
if (requireSharing && !playlist['sharingEnabled']) playlist = null;
|
||||||
@@ -365,112 +314,27 @@ exports.getUserPlaylist = function(user_uid, playlistID, type, requireSharing =
|
|||||||
return playlist;
|
return playlist;
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.registerUserFile = function(user_uid, file_object, type) {
|
exports.changeSharingMode = async function(user_uid, file_uid, is_playlist, enabled) {
|
||||||
users_db.get('users').find({uid: user_uid}).get(`files.${type}`)
|
|
||||||
.remove({
|
|
||||||
path: file_object['path']
|
|
||||||
}).write();
|
|
||||||
|
|
||||||
users_db.get('users').find({uid: user_uid}).get(`files.${type}`)
|
|
||||||
.push(file_object)
|
|
||||||
.write();
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.deleteUserFile = function(user_uid, file_uid, type, blacklistMode = false) {
|
|
||||||
let success = false;
|
let success = false;
|
||||||
const file_obj = users_db.get('users').find({uid: user_uid}).get(`files.${type}`).find({uid: file_uid}).value();
|
is_playlist ? await db_api.updateRecord(`playlists`, {id: file_uid}, {sharingEnabled: enabled}) : await db_api.updateRecord(`files`, {uid: file_uid}, {sharingEnabled: enabled});
|
||||||
if (file_obj) {
|
success = true;
|
||||||
const usersFileFolder = config_api.getConfigItem('ytdl_users_base_path');
|
|
||||||
const ext = type === 'audio' ? '.mp3' : '.mp4';
|
|
||||||
|
|
||||||
// close descriptors
|
|
||||||
if (config_api.descriptors[file_obj.id]) {
|
|
||||||
try {
|
|
||||||
for (let i = 0; i < config_api.descriptors[file_obj.id].length; i++) {
|
|
||||||
config_api.descriptors[file_obj.id][i].destroy();
|
|
||||||
}
|
|
||||||
} catch(e) {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const full_path = path.join(usersFileFolder, user_uid, type, file_obj.id + ext);
|
|
||||||
users_db.get('users').find({uid: user_uid}).get(`files.${type}`)
|
|
||||||
.remove({
|
|
||||||
uid: file_uid
|
|
||||||
}).write();
|
|
||||||
if (fs.existsSync(full_path)) {
|
|
||||||
// remove json and file
|
|
||||||
const json_path = path.join(usersFileFolder, user_uid, type, file_obj.id + '.info.json');
|
|
||||||
const alternate_json_path = path.join(usersFileFolder, user_uid, type, file_obj.id + ext + '.info.json');
|
|
||||||
let youtube_id = null;
|
|
||||||
if (fs.existsSync(json_path)) {
|
|
||||||
youtube_id = fs.readJSONSync(json_path).id;
|
|
||||||
fs.unlinkSync(json_path);
|
|
||||||
} else if (fs.existsSync(alternate_json_path)) {
|
|
||||||
youtube_id = fs.readJSONSync(alternate_json_path).id;
|
|
||||||
fs.unlinkSync(alternate_json_path);
|
|
||||||
}
|
|
||||||
|
|
||||||
fs.unlinkSync(full_path);
|
|
||||||
|
|
||||||
// do archive stuff
|
|
||||||
|
|
||||||
let useYoutubeDLArchive = config_api.getConfigItem('ytdl_use_youtubedl_archive');
|
|
||||||
if (useYoutubeDLArchive) {
|
|
||||||
const archive_path = path.join(usersFileFolder, user_uid, 'archives', `archive_${type}.txt`);
|
|
||||||
|
|
||||||
// use subscriptions API to remove video from the archive file, and write it to the blacklist
|
|
||||||
if (fs.existsSync(archive_path)) {
|
|
||||||
const line = youtube_id ? subscriptions_api.removeIDFromArchive(archive_path, youtube_id) : null;
|
|
||||||
if (blacklistMode && line) {
|
|
||||||
let blacklistPath = path.join(usersFileFolder, user_uid, 'archives', `blacklist_${type}.txt`);
|
|
||||||
// adds newline to the beginning of the line
|
|
||||||
line = '\n' + line;
|
|
||||||
fs.appendFileSync(blacklistPath, line);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
logger.info('Could not find archive file for audio files. Creating...');
|
|
||||||
fs.closeSync(fs.openSync(archive_path, 'w'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
success = true;
|
|
||||||
} else {
|
|
||||||
success = false;
|
|
||||||
logger.warn(`User file ${file_uid} does not exist!`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return success;
|
return success;
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.changeSharingMode = function(user_uid, file_uid, type, is_playlist, enabled) {
|
exports.userHasPermission = async function(user_uid, permission) {
|
||||||
let success = false;
|
|
||||||
const user_db_obj = users_db.get('users').find({uid: user_uid});
|
|
||||||
if (user_db_obj.value()) {
|
|
||||||
const file_db_obj = is_playlist ? user_db_obj.get(`playlists.${type}`).find({id: file_uid}) : user_db_obj.get(`files.${type}`).find({uid: file_uid});
|
|
||||||
if (file_db_obj.value()) {
|
|
||||||
success = true;
|
|
||||||
file_db_obj.assign({sharingEnabled: enabled}).write();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return success;
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.userHasPermission = function(user_uid, permission) {
|
const user_obj = await db_api.getRecord('users', ({uid: user_uid}));
|
||||||
const user_obj = users_db.get('users').find({uid: user_uid}).value();
|
|
||||||
const role = user_obj['role'];
|
const role = user_obj['role'];
|
||||||
if (!role) {
|
if (!role) {
|
||||||
// role doesn't exist
|
// role doesn't exist
|
||||||
logger.error('Invalid role ' + role);
|
logger.error('Invalid role ' + role);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const role_permissions = (users_db.get('roles').value())['permissions'];
|
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);
|
||||||
|
|
||||||
// check if user has a negative/positive override
|
// check if user has a negative/positive override
|
||||||
if (user_has_explicit_permission && permission_in_overrides) {
|
if (user_has_explicit_permission && permission_in_overrides) {
|
||||||
// positive override
|
// positive override
|
||||||
@@ -489,23 +353,24 @@ exports.userHasPermission = function(user_uid, permission) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.userPermissions = function(user_uid) {
|
exports.userPermissions = async function(user_uid) {
|
||||||
let user_permissions = [];
|
let user_permissions = [];
|
||||||
const user_obj = users_db.get('users').find({uid: user_uid}).value();
|
const user_obj = await db_api.getRecord('users', ({uid: user_uid}));
|
||||||
const role = user_obj['role'];
|
const role = user_obj['role'];
|
||||||
if (!role) {
|
if (!role) {
|
||||||
// role doesn't exist
|
// role doesn't exist
|
||||||
logger.error('Invalid role ' + role);
|
logger.error('Invalid role ' + role);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const role_permissions = users_db.get('roles').get(role).get('permissions').value()
|
const role_obj = await db_api.getRecord('roles', {key: role});
|
||||||
|
const role_permissions = role_obj['permissions'];
|
||||||
|
|
||||||
for (let i = 0; i < consts['AVAILABLE_PERMISSIONS'].length; i++) {
|
for (let i = 0; i < consts['AVAILABLE_PERMISSIONS'].length; i++) {
|
||||||
let permission = consts['AVAILABLE_PERMISSIONS'][i];
|
let permission = consts['AVAILABLE_PERMISSIONS'][i];
|
||||||
|
|
||||||
const user_has_explicit_permission = user_obj['permissions'].includes(permission);
|
const user_has_explicit_permission = user_obj['permissions'].includes(permission);
|
||||||
const permission_in_overrides = user_obj['permission_overrides'].includes(permission);
|
const permission_in_overrides = user_obj['permission_overrides'].includes(permission);
|
||||||
|
|
||||||
// check if user has a negative/positive override
|
// check if user has a negative/positive override
|
||||||
if (user_has_explicit_permission && permission_in_overrides) {
|
if (user_has_explicit_permission && permission_in_overrides) {
|
||||||
// positive override
|
// positive override
|
||||||
@@ -537,4 +402,21 @@ function getToken(queryParams) {
|
|||||||
} else {
|
} else {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function generateUserObject(userid, username, hash, auth_method = 'internal') {
|
||||||
|
let new_user = {
|
||||||
|
name: username,
|
||||||
|
uid: userid,
|
||||||
|
passhash: auth_method === 'internal' ? hash : null,
|
||||||
|
files: [],
|
||||||
|
playlists: [],
|
||||||
|
subscriptions: [],
|
||||||
|
created: Date.now(),
|
||||||
|
role: userid === 'admin' && auth_method === 'internal' ? 'admin' : 'user',
|
||||||
|
permissions: [],
|
||||||
|
permission_overrides: [],
|
||||||
|
auth_method: auth_method
|
||||||
|
};
|
||||||
|
return new_user;
|
||||||
|
}
|
||||||
|
|||||||
149
backend/categories.js
Normal file
149
backend/categories.js
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
const config_api = require('./config');
|
||||||
|
const utils = require('./utils');
|
||||||
|
|
||||||
|
var logger = null;
|
||||||
|
var db = null;
|
||||||
|
var users_db = null;
|
||||||
|
var db_api = null;
|
||||||
|
|
||||||
|
function setDB(input_db, input_users_db, input_db_api) { db = input_db; users_db = input_users_db; db_api = input_db_api }
|
||||||
|
function setLogger(input_logger) { logger = input_logger; }
|
||||||
|
|
||||||
|
function initialize(input_db, input_users_db, input_logger, input_db_api) {
|
||||||
|
setDB(input_db, input_users_db, input_db_api);
|
||||||
|
setLogger(input_logger);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
Categories:
|
||||||
|
|
||||||
|
Categories are a way to organize videos based on dynamic rules set by the user. Categories are universal (so not per-user).
|
||||||
|
|
||||||
|
Categories, besides rules, have an optional custom output. This custom output can help users create their
|
||||||
|
desired directory structure.
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
A category rule consists of a property, a comparison, and a value. For example, "uploader includes 'VEVO'"
|
||||||
|
|
||||||
|
Rules are stored as an object with the above fields. In addition to those fields, it also has a preceding_operator, which
|
||||||
|
is either OR or AND, and signifies whether the rule should be ANDed with the previous rules, or just ORed. For the first
|
||||||
|
rule, this field is null.
|
||||||
|
|
||||||
|
Ex. (title includes 'Rihanna' OR title includes 'Beyonce' AND uploader includes 'VEVO')
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
async function categorize(file_jsons) {
|
||||||
|
// to make the logic easier, let's assume the file metadata is an array
|
||||||
|
if (!Array.isArray(file_jsons)) file_jsons = [file_jsons];
|
||||||
|
|
||||||
|
let selected_category = null;
|
||||||
|
const categories = await getCategories();
|
||||||
|
if (!categories) {
|
||||||
|
logger.warn('Categories could not be found.');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < file_jsons.length; i++) {
|
||||||
|
const file_json = file_jsons[i];
|
||||||
|
for (let j = 0; j < categories.length; j++) {
|
||||||
|
const category = categories[j];
|
||||||
|
const rules = category['rules'];
|
||||||
|
|
||||||
|
// if rules for current category apply, then that is the selected category
|
||||||
|
if (applyCategoryRules(file_json, rules, category['name'])) {
|
||||||
|
selected_category = category;
|
||||||
|
logger.verbose(`Selected category ${category['name']} for ${file_json['webpage_url']}`);
|
||||||
|
return selected_category;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return selected_category;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getCategories() {
|
||||||
|
const categories = await db_api.getRecords('categories');
|
||||||
|
return categories ? categories : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getCategoriesAsPlaylists(files = null) {
|
||||||
|
const categories_as_playlists = [];
|
||||||
|
const available_categories = await getCategories();
|
||||||
|
if (available_categories && files) {
|
||||||
|
for (category of available_categories) {
|
||||||
|
const files_that_match = utils.addUIDsToCategory(category, files);
|
||||||
|
if (files_that_match && files_that_match.length > 0) {
|
||||||
|
category['thumbnailURL'] = files_that_match[0].thumbnailURL;
|
||||||
|
category['thumbnailPath'] = files_that_match[0].thumbnailPath;
|
||||||
|
category['duration'] = files_that_match.reduce((a, b) => a + utils.durationStringToNumber(b.duration), 0);
|
||||||
|
category['id'] = category['uid'];
|
||||||
|
categories_as_playlists.push(category);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return categories_as_playlists;
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyCategoryRules(file_json, rules, category_name) {
|
||||||
|
let rules_apply = false;
|
||||||
|
for (let i = 0; i < rules.length; i++) {
|
||||||
|
const rule = rules[i];
|
||||||
|
let rule_applies = null;
|
||||||
|
|
||||||
|
let preceding_operator = rule['preceding_operator'];
|
||||||
|
|
||||||
|
switch (rule['comparator']) {
|
||||||
|
case 'includes':
|
||||||
|
rule_applies = file_json[rule['property']].toLowerCase().includes(rule['value'].toLowerCase());
|
||||||
|
break;
|
||||||
|
case 'not_includes':
|
||||||
|
rule_applies = !(file_json[rule['property']].toLowerCase().includes(rule['value'].toLowerCase()));
|
||||||
|
break;
|
||||||
|
case 'equals':
|
||||||
|
rule_applies = file_json[rule['property']] === rule['value'];
|
||||||
|
break;
|
||||||
|
case 'not_equals':
|
||||||
|
rule_applies = file_json[rule['property']] !== rule['value'];
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
logger.warn(`Invalid comparison used for category ${category_name}`)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// OR the first rule with rules_apply, which will be initially false
|
||||||
|
if (i === 0) preceding_operator = 'or';
|
||||||
|
|
||||||
|
// update rules_apply based on current rule
|
||||||
|
if (preceding_operator === 'or')
|
||||||
|
rules_apply = rules_apply || rule_applies;
|
||||||
|
else
|
||||||
|
rules_apply = rules_apply && rule_applies;
|
||||||
|
}
|
||||||
|
|
||||||
|
return rules_apply;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function addTagToVideo(tag, video, user_uid) {
|
||||||
|
// TODO: Implement
|
||||||
|
}
|
||||||
|
|
||||||
|
async function removeTagFromVideo(tag, video, user_uid) {
|
||||||
|
// TODO: Implement
|
||||||
|
}
|
||||||
|
|
||||||
|
// adds tag to list of existing tags (used for tag suggestions)
|
||||||
|
async function addTagToExistingTags(tag) {
|
||||||
|
const existing_tags = db.get('tags').value();
|
||||||
|
if (!existing_tags.includes(tag)) {
|
||||||
|
db.get('tags').push(tag).write();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
initialize: initialize,
|
||||||
|
categorize: categorize,
|
||||||
|
getCategories: getCategories,
|
||||||
|
getCategoriesAsPlaylists: getCategoriesAsPlaylists
|
||||||
|
}
|
||||||
@@ -119,8 +119,8 @@ function setConfigItem(key, value) {
|
|||||||
let parent_parent_single_key = parent_path_arr[parent_path_arr.length-1];
|
let parent_parent_single_key = parent_path_arr[parent_path_arr.length-1];
|
||||||
parent_parent_object[parent_parent_single_key] = {};
|
parent_parent_object[parent_parent_single_key] = {};
|
||||||
parent_object = Object.byString(config_json, parent_path);
|
parent_object = Object.byString(config_json, parent_path);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (value === 'false' || value === 'true') {
|
if (value === 'false' || value === 'true') {
|
||||||
parent_object[element_name] = (value === 'true');
|
parent_object[element_name] = (value === 'true');
|
||||||
} else {
|
} else {
|
||||||
@@ -146,7 +146,7 @@ function setConfigItems(items) {
|
|||||||
let item_path = CONFIG_ITEMS[key]['path'];
|
let item_path = CONFIG_ITEMS[key]['path'];
|
||||||
let item_parent_path = getParentPath(item_path);
|
let item_parent_path = getParentPath(item_path);
|
||||||
let item_element_name = getElementNameInConfig(item_path);
|
let item_element_name = getElementNameInConfig(item_path);
|
||||||
|
|
||||||
let item_parent_object = Object.byString(config_json, item_parent_path);
|
let item_parent_object = Object.byString(config_json, item_parent_path);
|
||||||
item_parent_object[item_element_name] = value;
|
item_parent_object[item_element_name] = value;
|
||||||
}
|
}
|
||||||
@@ -155,6 +155,13 @@ function setConfigItems(items) {
|
|||||||
return success;
|
return success;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function globalArgsRequiresSafeDownload() {
|
||||||
|
const globalArgs = getConfigItem('ytdl_custom_args').split(',,');
|
||||||
|
const argsThatRequireSafeDownload = ['--write-sub', '--write-srt', '--proxy'];
|
||||||
|
const failedArgs = globalArgs.filter(arg => argsThatRequireSafeDownload.includes(arg));
|
||||||
|
return failedArgs && failedArgs.length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
getConfigItem: getConfigItem,
|
getConfigItem: getConfigItem,
|
||||||
setConfigItem: setConfigItem,
|
setConfigItem: setConfigItem,
|
||||||
@@ -164,7 +171,8 @@ module.exports = {
|
|||||||
configExistsCheck: configExistsCheck,
|
configExistsCheck: configExistsCheck,
|
||||||
CONFIG_ITEMS: CONFIG_ITEMS,
|
CONFIG_ITEMS: CONFIG_ITEMS,
|
||||||
initialize: initialize,
|
initialize: initialize,
|
||||||
descriptors: {}
|
descriptors: {},
|
||||||
|
globalArgsRequiresSafeDownload: globalArgsRequiresSafeDownload
|
||||||
}
|
}
|
||||||
|
|
||||||
DEFAULT_CONFIG = {
|
DEFAULT_CONFIG = {
|
||||||
@@ -173,16 +181,15 @@ DEFAULT_CONFIG = {
|
|||||||
"url": "http://example.com",
|
"url": "http://example.com",
|
||||||
"port": "17442"
|
"port": "17442"
|
||||||
},
|
},
|
||||||
"Encryption": {
|
|
||||||
"use-encryption": false,
|
|
||||||
"cert-file-path": "/etc/letsencrypt/live/example.com/fullchain.pem",
|
|
||||||
"key-file-path": "/etc/letsencrypt/live/example.com/privkey.pem"
|
|
||||||
},
|
|
||||||
"Downloader": {
|
"Downloader": {
|
||||||
"path-audio": "audio/",
|
"path-audio": "audio/",
|
||||||
"path-video": "video/",
|
"path-video": "video/",
|
||||||
|
"default_file_output": "",
|
||||||
"use_youtubedl_archive": false,
|
"use_youtubedl_archive": false,
|
||||||
"custom_args": ""
|
"custom_args": "",
|
||||||
|
"safe_download_override": false,
|
||||||
|
"include_thumbnail": true,
|
||||||
|
"include_metadata": true
|
||||||
},
|
},
|
||||||
"Extra": {
|
"Extra": {
|
||||||
"title_top": "YoutubeDL-Material",
|
"title_top": "YoutubeDL-Material",
|
||||||
@@ -190,14 +197,17 @@ DEFAULT_CONFIG = {
|
|||||||
"allow_quality_select": true,
|
"allow_quality_select": true,
|
||||||
"download_only_mode": false,
|
"download_only_mode": false,
|
||||||
"allow_multi_download_mode": true,
|
"allow_multi_download_mode": true,
|
||||||
"settings_pin_required": false,
|
"enable_downloads_manager": true,
|
||||||
"enable_downloads_manager": true
|
"allow_playlist_categorization": true
|
||||||
},
|
},
|
||||||
"API": {
|
"API": {
|
||||||
"use_API_key": false,
|
"use_API_key": false,
|
||||||
"API_key": "",
|
"API_key": "",
|
||||||
"use_youtube_API": false,
|
"use_youtube_API": false,
|
||||||
"youtube_API_key": ""
|
"youtube_API_key": "",
|
||||||
|
"use_twitch_API": false,
|
||||||
|
"twitch_API_key": "",
|
||||||
|
"twitch_auto_download_chat": false
|
||||||
},
|
},
|
||||||
"Themes": {
|
"Themes": {
|
||||||
"default_theme": "default",
|
"default_theme": "default",
|
||||||
@@ -207,18 +217,32 @@ DEFAULT_CONFIG = {
|
|||||||
"allow_subscriptions": true,
|
"allow_subscriptions": true,
|
||||||
"subscriptions_base_path": "subscriptions/",
|
"subscriptions_base_path": "subscriptions/",
|
||||||
"subscriptions_check_interval": "300",
|
"subscriptions_check_interval": "300",
|
||||||
"subscriptions_use_youtubedl_archive": true
|
"redownload_fresh_uploads": false
|
||||||
},
|
},
|
||||||
"Users": {
|
"Users": {
|
||||||
"base_path": "users/",
|
"base_path": "users/",
|
||||||
"allow_registration": true
|
"allow_registration": true,
|
||||||
|
"auth_method": "internal",
|
||||||
|
"ldap_config": {
|
||||||
|
"url": "ldap://localhost:389",
|
||||||
|
"bindDN": "cn=root",
|
||||||
|
"bindCredentials": "secret",
|
||||||
|
"searchBase": "ou=passport-ldapauth",
|
||||||
|
"searchFilter": "(uid={{username}})"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Database": {
|
||||||
|
"use_local_db": false,
|
||||||
|
"mongodb_connection_string": "mongodb://127.0.0.1:27017/?compressors=zlib"
|
||||||
},
|
},
|
||||||
"Advanced": {
|
"Advanced": {
|
||||||
|
"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,
|
||||||
"allow_advanced_download": false,
|
"allow_advanced_download": false,
|
||||||
"use_cookies": false,
|
"use_cookies": false,
|
||||||
|
"jwt_expiration": 86400,
|
||||||
"logger_level": "info"
|
"logger_level": "info"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,20 +9,6 @@ let CONFIG_ITEMS = {
|
|||||||
'path': 'YoutubeDLMaterial.Host.port'
|
'path': 'YoutubeDLMaterial.Host.port'
|
||||||
},
|
},
|
||||||
|
|
||||||
// Encryption
|
|
||||||
'ytdl_use_encryption': {
|
|
||||||
'key': 'ytdl_use_encryption',
|
|
||||||
'path': 'YoutubeDLMaterial.Encryption.use-encryption'
|
|
||||||
},
|
|
||||||
'ytdl_cert_file_path': {
|
|
||||||
'key': 'ytdl_cert_file_path',
|
|
||||||
'path': 'YoutubeDLMaterial.Encryption.cert-file-path'
|
|
||||||
},
|
|
||||||
'ytdl_key_file_path': {
|
|
||||||
'key': 'ytdl_key_file_path',
|
|
||||||
'path': 'YoutubeDLMaterial.Encryption.key-file-path'
|
|
||||||
},
|
|
||||||
|
|
||||||
// Downloader
|
// Downloader
|
||||||
'ytdl_audio_folder_path': {
|
'ytdl_audio_folder_path': {
|
||||||
'key': 'ytdl_audio_folder_path',
|
'key': 'ytdl_audio_folder_path',
|
||||||
@@ -32,6 +18,10 @@ let CONFIG_ITEMS = {
|
|||||||
'key': 'ytdl_video_folder_path',
|
'key': 'ytdl_video_folder_path',
|
||||||
'path': 'YoutubeDLMaterial.Downloader.path-video'
|
'path': 'YoutubeDLMaterial.Downloader.path-video'
|
||||||
},
|
},
|
||||||
|
'ytdl_default_file_output': {
|
||||||
|
'key': 'ytdl_default_file_output',
|
||||||
|
'path': 'YoutubeDLMaterial.Downloader.default_file_output'
|
||||||
|
},
|
||||||
'ytdl_use_youtubedl_archive': {
|
'ytdl_use_youtubedl_archive': {
|
||||||
'key': 'ytdl_use_youtubedl_archive',
|
'key': 'ytdl_use_youtubedl_archive',
|
||||||
'path': 'YoutubeDLMaterial.Downloader.use_youtubedl_archive'
|
'path': 'YoutubeDLMaterial.Downloader.use_youtubedl_archive'
|
||||||
@@ -40,6 +30,18 @@ let CONFIG_ITEMS = {
|
|||||||
'key': 'ytdl_custom_args',
|
'key': 'ytdl_custom_args',
|
||||||
'path': 'YoutubeDLMaterial.Downloader.custom_args'
|
'path': 'YoutubeDLMaterial.Downloader.custom_args'
|
||||||
},
|
},
|
||||||
|
'ytdl_safe_download_override': {
|
||||||
|
'key': 'ytdl_safe_download_override',
|
||||||
|
'path': 'YoutubeDLMaterial.Downloader.safe_download_override'
|
||||||
|
},
|
||||||
|
'ytdl_include_thumbnail': {
|
||||||
|
'key': 'ytdl_include_thumbnail',
|
||||||
|
'path': 'YoutubeDLMaterial.Downloader.include_thumbnail'
|
||||||
|
},
|
||||||
|
'ytdl_include_metadata': {
|
||||||
|
'key': 'ytdl_include_metadata',
|
||||||
|
'path': 'YoutubeDLMaterial.Downloader.include_metadata'
|
||||||
|
},
|
||||||
|
|
||||||
// Extra
|
// Extra
|
||||||
'ytdl_title_top': {
|
'ytdl_title_top': {
|
||||||
@@ -62,14 +64,14 @@ let CONFIG_ITEMS = {
|
|||||||
'key': 'ytdl_allow_multi_download_mode',
|
'key': 'ytdl_allow_multi_download_mode',
|
||||||
'path': 'YoutubeDLMaterial.Extra.allow_multi_download_mode'
|
'path': 'YoutubeDLMaterial.Extra.allow_multi_download_mode'
|
||||||
},
|
},
|
||||||
'ytdl_settings_pin_required': {
|
|
||||||
'key': 'ytdl_settings_pin_required',
|
|
||||||
'path': 'YoutubeDLMaterial.Extra.settings_pin_required'
|
|
||||||
},
|
|
||||||
'ytdl_enable_downloads_manager': {
|
'ytdl_enable_downloads_manager': {
|
||||||
'key': 'ytdl_enable_downloads_manager',
|
'key': 'ytdl_enable_downloads_manager',
|
||||||
'path': 'YoutubeDLMaterial.Extra.enable_downloads_manager'
|
'path': 'YoutubeDLMaterial.Extra.enable_downloads_manager'
|
||||||
},
|
},
|
||||||
|
'ytdl_allow_playlist_categorization': {
|
||||||
|
'key': 'ytdl_allow_playlist_categorization',
|
||||||
|
'path': 'YoutubeDLMaterial.Extra.allow_playlist_categorization'
|
||||||
|
},
|
||||||
|
|
||||||
// API
|
// API
|
||||||
'ytdl_use_api_key': {
|
'ytdl_use_api_key': {
|
||||||
@@ -88,6 +90,18 @@ let CONFIG_ITEMS = {
|
|||||||
'key': 'ytdl_youtube_api_key',
|
'key': 'ytdl_youtube_api_key',
|
||||||
'path': 'YoutubeDLMaterial.API.youtube_API_key'
|
'path': 'YoutubeDLMaterial.API.youtube_API_key'
|
||||||
},
|
},
|
||||||
|
'ytdl_use_twitch_api': {
|
||||||
|
'key': 'ytdl_use_twitch_api',
|
||||||
|
'path': 'YoutubeDLMaterial.API.use_twitch_API'
|
||||||
|
},
|
||||||
|
'ytdl_twitch_api_key': {
|
||||||
|
'key': 'ytdl_twitch_api_key',
|
||||||
|
'path': 'YoutubeDLMaterial.API.twitch_API_key'
|
||||||
|
},
|
||||||
|
'ytdl_twitch_auto_download_chat': {
|
||||||
|
'key': 'ytdl_twitch_auto_download_chat',
|
||||||
|
'path': 'YoutubeDLMaterial.API.twitch_auto_download_chat'
|
||||||
|
},
|
||||||
|
|
||||||
// Themes
|
// Themes
|
||||||
'ytdl_default_theme': {
|
'ytdl_default_theme': {
|
||||||
@@ -116,9 +130,9 @@ let CONFIG_ITEMS = {
|
|||||||
'key': 'ytdl_subscriptions_check_interval',
|
'key': 'ytdl_subscriptions_check_interval',
|
||||||
'path': 'YoutubeDLMaterial.Subscriptions.subscriptions_check_interval'
|
'path': 'YoutubeDLMaterial.Subscriptions.subscriptions_check_interval'
|
||||||
},
|
},
|
||||||
'ytdl_subscriptions_use_youtubedl_archive': {
|
'ytdl_subscriptions_redownload_fresh_uploads': {
|
||||||
'key': 'ytdl_use_youtubedl_archive',
|
'key': 'ytdl_subscriptions_redownload_fresh_uploads',
|
||||||
'path': 'YoutubeDLMaterial.Subscriptions.subscriptions_use_youtubedl_archive'
|
'path': 'YoutubeDLMaterial.Subscriptions.redownload_fresh_uploads'
|
||||||
},
|
},
|
||||||
|
|
||||||
// Users
|
// Users
|
||||||
@@ -130,8 +144,30 @@ let CONFIG_ITEMS = {
|
|||||||
'key': 'ytdl_allow_registration',
|
'key': 'ytdl_allow_registration',
|
||||||
'path': 'YoutubeDLMaterial.Users.allow_registration'
|
'path': 'YoutubeDLMaterial.Users.allow_registration'
|
||||||
},
|
},
|
||||||
|
'ytdl_auth_method': {
|
||||||
|
'key': 'ytdl_auth_method',
|
||||||
|
'path': 'YoutubeDLMaterial.Users.auth_method'
|
||||||
|
},
|
||||||
|
'ytdl_ldap_config': {
|
||||||
|
'key': 'ytdl_ldap_config',
|
||||||
|
'path': 'YoutubeDLMaterial.Users.ldap_config'
|
||||||
|
},
|
||||||
|
|
||||||
|
// Database
|
||||||
|
'ytdl_use_local_db': {
|
||||||
|
'key': 'ytdl_use_local_db',
|
||||||
|
'path': 'YoutubeDLMaterial.Database.use_local_db'
|
||||||
|
},
|
||||||
|
'ytdl_mongodb_connection_string': {
|
||||||
|
'key': 'ytdl_mongodb_connection_string',
|
||||||
|
'path': 'YoutubeDLMaterial.Database.mongodb_connection_string'
|
||||||
|
},
|
||||||
|
|
||||||
// Advanced
|
// Advanced
|
||||||
|
'ytdl_default_downloader': {
|
||||||
|
'key': 'ytdl_default_downloader',
|
||||||
|
'path': 'YoutubeDLMaterial.Advanced.default_downloader'
|
||||||
|
},
|
||||||
'ytdl_use_default_downloading_agent': {
|
'ytdl_use_default_downloading_agent': {
|
||||||
'key': 'ytdl_use_default_downloading_agent',
|
'key': 'ytdl_use_default_downloading_agent',
|
||||||
'path': 'YoutubeDLMaterial.Advanced.use_default_downloading_agent'
|
'path': 'YoutubeDLMaterial.Advanced.use_default_downloading_agent'
|
||||||
@@ -152,6 +188,10 @@ let CONFIG_ITEMS = {
|
|||||||
'key': 'ytdl_use_cookies',
|
'key': 'ytdl_use_cookies',
|
||||||
'path': 'YoutubeDLMaterial.Advanced.use_cookies'
|
'path': 'YoutubeDLMaterial.Advanced.use_cookies'
|
||||||
},
|
},
|
||||||
|
'ytdl_jwt_expiration': {
|
||||||
|
'key': 'ytdl_jwt_expiration',
|
||||||
|
'path': 'YoutubeDLMaterial.Advanced.jwt_expiration'
|
||||||
|
},
|
||||||
'ytdl_logger_level': {
|
'ytdl_logger_level': {
|
||||||
'key': 'ytdl_logger_level',
|
'key': 'ytdl_logger_level',
|
||||||
'path': 'YoutubeDLMaterial.Advanced.logger_level'
|
'path': 'YoutubeDLMaterial.Advanced.logger_level'
|
||||||
@@ -170,5 +210,5 @@ AVAILABLE_PERMISSIONS = [
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
CONFIG_ITEMS: CONFIG_ITEMS,
|
CONFIG_ITEMS: CONFIG_ITEMS,
|
||||||
AVAILABLE_PERMISSIONS: AVAILABLE_PERMISSIONS,
|
AVAILABLE_PERMISSIONS: AVAILABLE_PERMISSIONS,
|
||||||
CURRENT_VERSION: 'v4.0'
|
CURRENT_VERSION: 'v4.2'
|
||||||
}
|
}
|
||||||
|
|||||||
1006
backend/db.js
Normal file
1006
backend/db.js
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,15 +0,0 @@
|
|||||||
version: "2"
|
|
||||||
services:
|
|
||||||
ytdl_material:
|
|
||||||
environment:
|
|
||||||
ALLOW_CONFIG_MUTATIONS: 'true'
|
|
||||||
restart: always
|
|
||||||
volumes:
|
|
||||||
- ./appdata:/app/appdata
|
|
||||||
- ./audio:/app/audio
|
|
||||||
- ./video:/app/video
|
|
||||||
- ./subscriptions:/app/subscriptions
|
|
||||||
- ./users:/app/users
|
|
||||||
ports:
|
|
||||||
- "8998:17442"
|
|
||||||
image: tzahi12345/youtubedl-material:latest
|
|
||||||
17
backend/entrypoint.sh
Executable file
17
backend/entrypoint.sh
Executable file
@@ -0,0 +1,17 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
CMD="forever app.js"
|
||||||
|
|
||||||
|
# if the first arg starts with "-" pass it to program
|
||||||
|
if [ "${1#-}" != "$1" ]; then
|
||||||
|
set -- "$CMD" "$@"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# chown current working directory to current user
|
||||||
|
if [ "$*" = "$CMD" ] && [ "$(id -u)" = "0" ]; then
|
||||||
|
find . \! -user "$UID" -exec chown "$UID:$GID" -R '{}' + || echo "WARNING! Could not change directory ownership. If you manage permissions externally this is fine, otherwise you may experience issues when downloading or deleting videos."
|
||||||
|
exec su-exec "$UID:$GID" "$0" "$@"
|
||||||
|
fi
|
||||||
|
|
||||||
|
exec "$@"
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
1888
backend/package-lock.json
generated
1888
backend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -5,7 +5,8 @@
|
|||||||
"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": "nodemon -q app.js"
|
"start": "nodemon app.js",
|
||||||
|
"debug": "set YTDL_MODE=debug && node app.js"
|
||||||
},
|
},
|
||||||
"nodemonConfig": {
|
"nodemonConfig": {
|
||||||
"ignore": [
|
"ignore": [
|
||||||
@@ -14,7 +15,8 @@
|
|||||||
"public/*"
|
"public/*"
|
||||||
],
|
],
|
||||||
"watch": [
|
"watch": [
|
||||||
"restart.json"
|
"restart_update.json",
|
||||||
|
"restart_general.json"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
@@ -30,26 +32,34 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"archiver": "^3.1.1",
|
"archiver": "^3.1.1",
|
||||||
"async": "^3.1.0",
|
"async": "^3.1.0",
|
||||||
"bcrypt": "^4.0.1",
|
"axios": "^0.21.1",
|
||||||
|
"bcryptjs": "^2.4.0",
|
||||||
"compression": "^1.7.4",
|
"compression": "^1.7.4",
|
||||||
"config": "^3.2.3",
|
"config": "^3.2.3",
|
||||||
"exe": "^1.0.2",
|
"exe": "^1.0.2",
|
||||||
"express": "^4.17.1",
|
"express": "^4.17.1",
|
||||||
"fluent-ffmpeg": "^2.1.2",
|
"fluent-ffmpeg": "^2.1.2",
|
||||||
"fs-extra": "^9.0.0",
|
"fs-extra": "^9.0.0",
|
||||||
|
"glob": "^7.1.6",
|
||||||
"jsonwebtoken": "^8.5.1",
|
"jsonwebtoken": "^8.5.1",
|
||||||
"lowdb": "^1.0.0",
|
"lowdb": "^1.0.0",
|
||||||
"md5": "^2.2.1",
|
"md5": "^2.2.1",
|
||||||
"merge-files": "^0.1.2",
|
"merge-files": "^0.1.2",
|
||||||
|
"mocha": "^8.4.0",
|
||||||
|
"moment": "^2.29.1",
|
||||||
|
"mongodb": "^3.6.9",
|
||||||
"multer": "^1.4.2",
|
"multer": "^1.4.2",
|
||||||
"node-fetch": "^2.6.0",
|
"node-fetch": "^2.6.1",
|
||||||
"node-id3": "^0.1.14",
|
"node-id3": "^0.1.14",
|
||||||
"nodemon": "^2.0.2",
|
"nodemon": "^2.0.7",
|
||||||
"passport": "^0.4.1",
|
"passport": "^0.4.1",
|
||||||
"passport-http": "^0.3.0",
|
"passport-http": "^0.3.0",
|
||||||
"passport-jwt": "^4.0.0",
|
"passport-jwt": "^4.0.0",
|
||||||
|
"passport-ldapauth": "^2.1.4",
|
||||||
"passport-local": "^1.0.0",
|
"passport-local": "^1.0.0",
|
||||||
"progress": "^2.0.3",
|
"progress": "^2.0.3",
|
||||||
|
"ps-node": "^0.1.6",
|
||||||
|
"read-last-lines": "^1.7.2",
|
||||||
"shortid": "^2.2.15",
|
"shortid": "^2.2.15",
|
||||||
"unzipper": "^0.10.10",
|
"unzipper": "^0.10.10",
|
||||||
"uuidv4": "^6.0.6",
|
"uuidv4": "^6.0.6",
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,794 +0,0 @@
|
|||||||
@angular-devkit/build-angular
|
|
||||||
MIT
|
|
||||||
The MIT License
|
|
||||||
|
|
||||||
Copyright (c) 2017 Google, Inc.
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
||||||
|
|
||||||
|
|
||||||
@angular/animations
|
|
||||||
MIT
|
|
||||||
|
|
||||||
@angular/cdk
|
|
||||||
MIT
|
|
||||||
The MIT License
|
|
||||||
|
|
||||||
Copyright (c) 2020 Google LLC.
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in
|
|
||||||
all copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
||||||
THE SOFTWARE.
|
|
||||||
|
|
||||||
|
|
||||||
@angular/common
|
|
||||||
MIT
|
|
||||||
|
|
||||||
@angular/compiler
|
|
||||||
MIT
|
|
||||||
|
|
||||||
@angular/core
|
|
||||||
MIT
|
|
||||||
|
|
||||||
@angular/forms
|
|
||||||
MIT
|
|
||||||
|
|
||||||
@angular/localize
|
|
||||||
MIT
|
|
||||||
|
|
||||||
@angular/material
|
|
||||||
MIT
|
|
||||||
The MIT License
|
|
||||||
|
|
||||||
Copyright (c) 2020 Google LLC.
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in
|
|
||||||
all copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
||||||
THE SOFTWARE.
|
|
||||||
|
|
||||||
|
|
||||||
@angular/platform-browser
|
|
||||||
MIT
|
|
||||||
|
|
||||||
@angular/platform-browser-dynamic
|
|
||||||
MIT
|
|
||||||
|
|
||||||
@angular/router
|
|
||||||
MIT
|
|
||||||
|
|
||||||
core-js
|
|
||||||
MIT
|
|
||||||
Copyright (c) 2014-2020 Denis Pushkarev
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in
|
|
||||||
all copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
||||||
THE SOFTWARE.
|
|
||||||
|
|
||||||
|
|
||||||
file-saver
|
|
||||||
MIT
|
|
||||||
The MIT License
|
|
||||||
|
|
||||||
Copyright © 2016 [Eli Grey][1].
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
||||||
|
|
||||||
[1]: http://eligrey.com
|
|
||||||
|
|
||||||
|
|
||||||
filesize
|
|
||||||
BSD-3-Clause
|
|
||||||
Copyright (c) 2020, Jason Mulligan
|
|
||||||
All rights reserved.
|
|
||||||
|
|
||||||
Redistribution and use in source and binary forms, with or without
|
|
||||||
modification, are permitted provided that the following conditions are met:
|
|
||||||
|
|
||||||
* Redistributions of source code must retain the above copyright notice, this
|
|
||||||
list of conditions and the following disclaimer.
|
|
||||||
|
|
||||||
* Redistributions in binary form must reproduce the above copyright notice,
|
|
||||||
this list of conditions and the following disclaimer in the documentation
|
|
||||||
and/or other materials provided with the distribution.
|
|
||||||
|
|
||||||
* Neither the name of filesize nor the names of its
|
|
||||||
contributors may be used to endorse or promote products derived from
|
|
||||||
this software without specific prior written permission.
|
|
||||||
|
|
||||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
||||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
||||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
||||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
||||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
||||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
||||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
||||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
||||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
||||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
fingerprintjs2
|
|
||||||
MIT
|
|
||||||
Fingerprintjs2 – Modern & flexible browser fingerprint library v2
|
|
||||||
https://github.com/Valve/fingerprintjs2
|
|
||||||
Copyright (c) 2018– Jonas Haag (jonas@lophus.org)
|
|
||||||
Copyright (c) 2015– Valentin Vasilyev (valentin.vasilyev@outlook.com)
|
|
||||||
Licensed under the MIT (http://www.opensource.org/licenses/mit-license.php) license.
|
|
||||||
|
|
||||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
||||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
||||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
|
||||||
ARE DISCLAIMED. IN NO EVENT SHALL VALENTIN VASILYEV BE LIABLE FOR ANY
|
|
||||||
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
|
||||||
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
|
||||||
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
|
||||||
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
||||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
|
|
||||||
THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
||||||
|
|
||||||
|
|
||||||
ng-lazyload-image
|
|
||||||
MIT
|
|
||||||
The MIT License (MIT)
|
|
||||||
|
|
||||||
Copyright (c) 2016 Oskar Karlsson
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
||||||
|
|
||||||
|
|
||||||
ngx-file-drop
|
|
||||||
MIT
|
|
||||||
|
|
||||||
ngx-videogular
|
|
||||||
MIT
|
|
||||||
|
|
||||||
regenerator-runtime
|
|
||||||
MIT
|
|
||||||
MIT License
|
|
||||||
|
|
||||||
Copyright (c) 2014-present, Facebook, Inc.
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
||||||
|
|
||||||
|
|
||||||
rxjs
|
|
||||||
Apache-2.0
|
|
||||||
Apache License
|
|
||||||
Version 2.0, January 2004
|
|
||||||
http://www.apache.org/licenses/
|
|
||||||
|
|
||||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
||||||
|
|
||||||
1. Definitions.
|
|
||||||
|
|
||||||
"License" shall mean the terms and conditions for use, reproduction,
|
|
||||||
and distribution as defined by Sections 1 through 9 of this document.
|
|
||||||
|
|
||||||
"Licensor" shall mean the copyright owner or entity authorized by
|
|
||||||
the copyright owner that is granting the License.
|
|
||||||
|
|
||||||
"Legal Entity" shall mean the union of the acting entity and all
|
|
||||||
other entities that control, are controlled by, or are under common
|
|
||||||
control with that entity. For the purposes of this definition,
|
|
||||||
"control" means (i) the power, direct or indirect, to cause the
|
|
||||||
direction or management of such entity, whether by contract or
|
|
||||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
||||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
||||||
|
|
||||||
"You" (or "Your") shall mean an individual or Legal Entity
|
|
||||||
exercising permissions granted by this License.
|
|
||||||
|
|
||||||
"Source" form shall mean the preferred form for making modifications,
|
|
||||||
including but not limited to software source code, documentation
|
|
||||||
source, and configuration files.
|
|
||||||
|
|
||||||
"Object" form shall mean any form resulting from mechanical
|
|
||||||
transformation or translation of a Source form, including but
|
|
||||||
not limited to compiled object code, generated documentation,
|
|
||||||
and conversions to other media types.
|
|
||||||
|
|
||||||
"Work" shall mean the work of authorship, whether in Source or
|
|
||||||
Object form, made available under the License, as indicated by a
|
|
||||||
copyright notice that is included in or attached to the work
|
|
||||||
(an example is provided in the Appendix below).
|
|
||||||
|
|
||||||
"Derivative Works" shall mean any work, whether in Source or Object
|
|
||||||
form, that is based on (or derived from) the Work and for which the
|
|
||||||
editorial revisions, annotations, elaborations, or other modifications
|
|
||||||
represent, as a whole, an original work of authorship. For the purposes
|
|
||||||
of this License, Derivative Works shall not include works that remain
|
|
||||||
separable from, or merely link (or bind by name) to the interfaces of,
|
|
||||||
the Work and Derivative Works thereof.
|
|
||||||
|
|
||||||
"Contribution" shall mean any work of authorship, including
|
|
||||||
the original version of the Work and any modifications or additions
|
|
||||||
to that Work or Derivative Works thereof, that is intentionally
|
|
||||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
||||||
or by an individual or Legal Entity authorized to submit on behalf of
|
|
||||||
the copyright owner. For the purposes of this definition, "submitted"
|
|
||||||
means any form of electronic, verbal, or written communication sent
|
|
||||||
to the Licensor or its representatives, including but not limited to
|
|
||||||
communication on electronic mailing lists, source code control systems,
|
|
||||||
and issue tracking systems that are managed by, or on behalf of, the
|
|
||||||
Licensor for the purpose of discussing and improving the Work, but
|
|
||||||
excluding communication that is conspicuously marked or otherwise
|
|
||||||
designated in writing by the copyright owner as "Not a Contribution."
|
|
||||||
|
|
||||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
||||||
on behalf of whom a Contribution has been received by Licensor and
|
|
||||||
subsequently incorporated within the Work.
|
|
||||||
|
|
||||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
||||||
this License, each Contributor hereby grants to You a perpetual,
|
|
||||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
||||||
copyright license to reproduce, prepare Derivative Works of,
|
|
||||||
publicly display, publicly perform, sublicense, and distribute the
|
|
||||||
Work and such Derivative Works in Source or Object form.
|
|
||||||
|
|
||||||
3. Grant of Patent License. Subject to the terms and conditions of
|
|
||||||
this License, each Contributor hereby grants to You a perpetual,
|
|
||||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
||||||
(except as stated in this section) patent license to make, have made,
|
|
||||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
||||||
where such license applies only to those patent claims licensable
|
|
||||||
by such Contributor that are necessarily infringed by their
|
|
||||||
Contribution(s) alone or by combination of their Contribution(s)
|
|
||||||
with the Work to which such Contribution(s) was submitted. If You
|
|
||||||
institute patent litigation against any entity (including a
|
|
||||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
||||||
or a Contribution incorporated within the Work constitutes direct
|
|
||||||
or contributory patent infringement, then any patent licenses
|
|
||||||
granted to You under this License for that Work shall terminate
|
|
||||||
as of the date such litigation is filed.
|
|
||||||
|
|
||||||
4. Redistribution. You may reproduce and distribute copies of the
|
|
||||||
Work or Derivative Works thereof in any medium, with or without
|
|
||||||
modifications, and in Source or Object form, provided that You
|
|
||||||
meet the following conditions:
|
|
||||||
|
|
||||||
(a) You must give any other recipients of the Work or
|
|
||||||
Derivative Works a copy of this License; and
|
|
||||||
|
|
||||||
(b) You must cause any modified files to carry prominent notices
|
|
||||||
stating that You changed the files; and
|
|
||||||
|
|
||||||
(c) You must retain, in the Source form of any Derivative Works
|
|
||||||
that You distribute, all copyright, patent, trademark, and
|
|
||||||
attribution notices from the Source form of the Work,
|
|
||||||
excluding those notices that do not pertain to any part of
|
|
||||||
the Derivative Works; and
|
|
||||||
|
|
||||||
(d) If the Work includes a "NOTICE" text file as part of its
|
|
||||||
distribution, then any Derivative Works that You distribute must
|
|
||||||
include a readable copy of the attribution notices contained
|
|
||||||
within such NOTICE file, excluding those notices that do not
|
|
||||||
pertain to any part of the Derivative Works, in at least one
|
|
||||||
of the following places: within a NOTICE text file distributed
|
|
||||||
as part of the Derivative Works; within the Source form or
|
|
||||||
documentation, if provided along with the Derivative Works; or,
|
|
||||||
within a display generated by the Derivative Works, if and
|
|
||||||
wherever such third-party notices normally appear. The contents
|
|
||||||
of the NOTICE file are for informational purposes only and
|
|
||||||
do not modify the License. You may add Your own attribution
|
|
||||||
notices within Derivative Works that You distribute, alongside
|
|
||||||
or as an addendum to the NOTICE text from the Work, provided
|
|
||||||
that such additional attribution notices cannot be construed
|
|
||||||
as modifying the License.
|
|
||||||
|
|
||||||
You may add Your own copyright statement to Your modifications and
|
|
||||||
may provide additional or different license terms and conditions
|
|
||||||
for use, reproduction, or distribution of Your modifications, or
|
|
||||||
for any such Derivative Works as a whole, provided Your use,
|
|
||||||
reproduction, and distribution of the Work otherwise complies with
|
|
||||||
the conditions stated in this License.
|
|
||||||
|
|
||||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
||||||
any Contribution intentionally submitted for inclusion in the Work
|
|
||||||
by You to the Licensor shall be under the terms and conditions of
|
|
||||||
this License, without any additional terms or conditions.
|
|
||||||
Notwithstanding the above, nothing herein shall supersede or modify
|
|
||||||
the terms of any separate license agreement you may have executed
|
|
||||||
with Licensor regarding such Contributions.
|
|
||||||
|
|
||||||
6. Trademarks. This License does not grant permission to use the trade
|
|
||||||
names, trademarks, service marks, or product names of the Licensor,
|
|
||||||
except as required for reasonable and customary use in describing the
|
|
||||||
origin of the Work and reproducing the content of the NOTICE file.
|
|
||||||
|
|
||||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
||||||
agreed to in writing, Licensor provides the Work (and each
|
|
||||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
||||||
implied, including, without limitation, any warranties or conditions
|
|
||||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
||||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
||||||
appropriateness of using or redistributing the Work and assume any
|
|
||||||
risks associated with Your exercise of permissions under this License.
|
|
||||||
|
|
||||||
8. Limitation of Liability. In no event and under no legal theory,
|
|
||||||
whether in tort (including negligence), contract, or otherwise,
|
|
||||||
unless required by applicable law (such as deliberate and grossly
|
|
||||||
negligent acts) or agreed to in writing, shall any Contributor be
|
|
||||||
liable to You for damages, including any direct, indirect, special,
|
|
||||||
incidental, or consequential damages of any character arising as a
|
|
||||||
result of this License or out of the use or inability to use the
|
|
||||||
Work (including but not limited to damages for loss of goodwill,
|
|
||||||
work stoppage, computer failure or malfunction, or any and all
|
|
||||||
other commercial damages or losses), even if such Contributor
|
|
||||||
has been advised of the possibility of such damages.
|
|
||||||
|
|
||||||
9. Accepting Warranty or Additional Liability. While redistributing
|
|
||||||
the Work or Derivative Works thereof, You may choose to offer,
|
|
||||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
||||||
or other liability obligations and/or rights consistent with this
|
|
||||||
License. However, in accepting such obligations, You may act only
|
|
||||||
on Your own behalf and on Your sole responsibility, not on behalf
|
|
||||||
of any other Contributor, and only if You agree to indemnify,
|
|
||||||
defend, and hold each Contributor harmless for any liability
|
|
||||||
incurred by, or claims asserted against, such Contributor by reason
|
|
||||||
of your accepting any such warranty or additional liability.
|
|
||||||
|
|
||||||
END OF TERMS AND CONDITIONS
|
|
||||||
|
|
||||||
APPENDIX: How to apply the Apache License to your work.
|
|
||||||
|
|
||||||
To apply the Apache License to your work, attach the following
|
|
||||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
||||||
replaced with your own identifying information. (Don't include
|
|
||||||
the brackets!) The text should be enclosed in the appropriate
|
|
||||||
comment syntax for the file format. We also recommend that a
|
|
||||||
file or class name and description of purpose be included on the
|
|
||||||
same "printed page" as the copyright notice for easier
|
|
||||||
identification within third-party archives.
|
|
||||||
|
|
||||||
Copyright (c) 2015-2018 Google, Inc., Netflix, Inc., Microsoft Corp. and contributors
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
rxjs-compat
|
|
||||||
Apache-2.0
|
|
||||||
Apache License
|
|
||||||
Version 2.0, January 2004
|
|
||||||
http://www.apache.org/licenses/
|
|
||||||
|
|
||||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
||||||
|
|
||||||
1. Definitions.
|
|
||||||
|
|
||||||
"License" shall mean the terms and conditions for use, reproduction,
|
|
||||||
and distribution as defined by Sections 1 through 9 of this document.
|
|
||||||
|
|
||||||
"Licensor" shall mean the copyright owner or entity authorized by
|
|
||||||
the copyright owner that is granting the License.
|
|
||||||
|
|
||||||
"Legal Entity" shall mean the union of the acting entity and all
|
|
||||||
other entities that control, are controlled by, or are under common
|
|
||||||
control with that entity. For the purposes of this definition,
|
|
||||||
"control" means (i) the power, direct or indirect, to cause the
|
|
||||||
direction or management of such entity, whether by contract or
|
|
||||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
||||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
||||||
|
|
||||||
"You" (or "Your") shall mean an individual or Legal Entity
|
|
||||||
exercising permissions granted by this License.
|
|
||||||
|
|
||||||
"Source" form shall mean the preferred form for making modifications,
|
|
||||||
including but not limited to software source code, documentation
|
|
||||||
source, and configuration files.
|
|
||||||
|
|
||||||
"Object" form shall mean any form resulting from mechanical
|
|
||||||
transformation or translation of a Source form, including but
|
|
||||||
not limited to compiled object code, generated documentation,
|
|
||||||
and conversions to other media types.
|
|
||||||
|
|
||||||
"Work" shall mean the work of authorship, whether in Source or
|
|
||||||
Object form, made available under the License, as indicated by a
|
|
||||||
copyright notice that is included in or attached to the work
|
|
||||||
(an example is provided in the Appendix below).
|
|
||||||
|
|
||||||
"Derivative Works" shall mean any work, whether in Source or Object
|
|
||||||
form, that is based on (or derived from) the Work and for which the
|
|
||||||
editorial revisions, annotations, elaborations, or other modifications
|
|
||||||
represent, as a whole, an original work of authorship. For the purposes
|
|
||||||
of this License, Derivative Works shall not include works that remain
|
|
||||||
separable from, or merely link (or bind by name) to the interfaces of,
|
|
||||||
the Work and Derivative Works thereof.
|
|
||||||
|
|
||||||
"Contribution" shall mean any work of authorship, including
|
|
||||||
the original version of the Work and any modifications or additions
|
|
||||||
to that Work or Derivative Works thereof, that is intentionally
|
|
||||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
||||||
or by an individual or Legal Entity authorized to submit on behalf of
|
|
||||||
the copyright owner. For the purposes of this definition, "submitted"
|
|
||||||
means any form of electronic, verbal, or written communication sent
|
|
||||||
to the Licensor or its representatives, including but not limited to
|
|
||||||
communication on electronic mailing lists, source code control systems,
|
|
||||||
and issue tracking systems that are managed by, or on behalf of, the
|
|
||||||
Licensor for the purpose of discussing and improving the Work, but
|
|
||||||
excluding communication that is conspicuously marked or otherwise
|
|
||||||
designated in writing by the copyright owner as "Not a Contribution."
|
|
||||||
|
|
||||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
||||||
on behalf of whom a Contribution has been received by Licensor and
|
|
||||||
subsequently incorporated within the Work.
|
|
||||||
|
|
||||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
||||||
this License, each Contributor hereby grants to You a perpetual,
|
|
||||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
||||||
copyright license to reproduce, prepare Derivative Works of,
|
|
||||||
publicly display, publicly perform, sublicense, and distribute the
|
|
||||||
Work and such Derivative Works in Source or Object form.
|
|
||||||
|
|
||||||
3. Grant of Patent License. Subject to the terms and conditions of
|
|
||||||
this License, each Contributor hereby grants to You a perpetual,
|
|
||||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
||||||
(except as stated in this section) patent license to make, have made,
|
|
||||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
||||||
where such license applies only to those patent claims licensable
|
|
||||||
by such Contributor that are necessarily infringed by their
|
|
||||||
Contribution(s) alone or by combination of their Contribution(s)
|
|
||||||
with the Work to which such Contribution(s) was submitted. If You
|
|
||||||
institute patent litigation against any entity (including a
|
|
||||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
||||||
or a Contribution incorporated within the Work constitutes direct
|
|
||||||
or contributory patent infringement, then any patent licenses
|
|
||||||
granted to You under this License for that Work shall terminate
|
|
||||||
as of the date such litigation is filed.
|
|
||||||
|
|
||||||
4. Redistribution. You may reproduce and distribute copies of the
|
|
||||||
Work or Derivative Works thereof in any medium, with or without
|
|
||||||
modifications, and in Source or Object form, provided that You
|
|
||||||
meet the following conditions:
|
|
||||||
|
|
||||||
(a) You must give any other recipients of the Work or
|
|
||||||
Derivative Works a copy of this License; and
|
|
||||||
|
|
||||||
(b) You must cause any modified files to carry prominent notices
|
|
||||||
stating that You changed the files; and
|
|
||||||
|
|
||||||
(c) You must retain, in the Source form of any Derivative Works
|
|
||||||
that You distribute, all copyright, patent, trademark, and
|
|
||||||
attribution notices from the Source form of the Work,
|
|
||||||
excluding those notices that do not pertain to any part of
|
|
||||||
the Derivative Works; and
|
|
||||||
|
|
||||||
(d) If the Work includes a "NOTICE" text file as part of its
|
|
||||||
distribution, then any Derivative Works that You distribute must
|
|
||||||
include a readable copy of the attribution notices contained
|
|
||||||
within such NOTICE file, excluding those notices that do not
|
|
||||||
pertain to any part of the Derivative Works, in at least one
|
|
||||||
of the following places: within a NOTICE text file distributed
|
|
||||||
as part of the Derivative Works; within the Source form or
|
|
||||||
documentation, if provided along with the Derivative Works; or,
|
|
||||||
within a display generated by the Derivative Works, if and
|
|
||||||
wherever such third-party notices normally appear. The contents
|
|
||||||
of the NOTICE file are for informational purposes only and
|
|
||||||
do not modify the License. You may add Your own attribution
|
|
||||||
notices within Derivative Works that You distribute, alongside
|
|
||||||
or as an addendum to the NOTICE text from the Work, provided
|
|
||||||
that such additional attribution notices cannot be construed
|
|
||||||
as modifying the License.
|
|
||||||
|
|
||||||
You may add Your own copyright statement to Your modifications and
|
|
||||||
may provide additional or different license terms and conditions
|
|
||||||
for use, reproduction, or distribution of Your modifications, or
|
|
||||||
for any such Derivative Works as a whole, provided Your use,
|
|
||||||
reproduction, and distribution of the Work otherwise complies with
|
|
||||||
the conditions stated in this License.
|
|
||||||
|
|
||||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
||||||
any Contribution intentionally submitted for inclusion in the Work
|
|
||||||
by You to the Licensor shall be under the terms and conditions of
|
|
||||||
this License, without any additional terms or conditions.
|
|
||||||
Notwithstanding the above, nothing herein shall supersede or modify
|
|
||||||
the terms of any separate license agreement you may have executed
|
|
||||||
with Licensor regarding such Contributions.
|
|
||||||
|
|
||||||
6. Trademarks. This License does not grant permission to use the trade
|
|
||||||
names, trademarks, service marks, or product names of the Licensor,
|
|
||||||
except as required for reasonable and customary use in describing the
|
|
||||||
origin of the Work and reproducing the content of the NOTICE file.
|
|
||||||
|
|
||||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
||||||
agreed to in writing, Licensor provides the Work (and each
|
|
||||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
||||||
implied, including, without limitation, any warranties or conditions
|
|
||||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
||||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
||||||
appropriateness of using or redistributing the Work and assume any
|
|
||||||
risks associated with Your exercise of permissions under this License.
|
|
||||||
|
|
||||||
8. Limitation of Liability. In no event and under no legal theory,
|
|
||||||
whether in tort (including negligence), contract, or otherwise,
|
|
||||||
unless required by applicable law (such as deliberate and grossly
|
|
||||||
negligent acts) or agreed to in writing, shall any Contributor be
|
|
||||||
liable to You for damages, including any direct, indirect, special,
|
|
||||||
incidental, or consequential damages of any character arising as a
|
|
||||||
result of this License or out of the use or inability to use the
|
|
||||||
Work (including but not limited to damages for loss of goodwill,
|
|
||||||
work stoppage, computer failure or malfunction, or any and all
|
|
||||||
other commercial damages or losses), even if such Contributor
|
|
||||||
has been advised of the possibility of such damages.
|
|
||||||
|
|
||||||
9. Accepting Warranty or Additional Liability. While redistributing
|
|
||||||
the Work or Derivative Works thereof, You may choose to offer,
|
|
||||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
||||||
or other liability obligations and/or rights consistent with this
|
|
||||||
License. However, in accepting such obligations, You may act only
|
|
||||||
on Your own behalf and on Your sole responsibility, not on behalf
|
|
||||||
of any other Contributor, and only if You agree to indemnify,
|
|
||||||
defend, and hold each Contributor harmless for any liability
|
|
||||||
incurred by, or claims asserted against, such Contributor by reason
|
|
||||||
of your accepting any such warranty or additional liability.
|
|
||||||
|
|
||||||
END OF TERMS AND CONDITIONS
|
|
||||||
|
|
||||||
APPENDIX: How to apply the Apache License to your work.
|
|
||||||
|
|
||||||
To apply the Apache License to your work, attach the following
|
|
||||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
||||||
replaced with your own identifying information. (Don't include
|
|
||||||
the brackets!) The text should be enclosed in the appropriate
|
|
||||||
comment syntax for the file format. We also recommend that a
|
|
||||||
file or class name and description of purpose be included on the
|
|
||||||
same "printed page" as the copyright notice for easier
|
|
||||||
identification within third-party archives.
|
|
||||||
|
|
||||||
Copyright (c) 2015-2018 Google, Inc., Netflix, Inc., Microsoft Corp. and contributors
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
tslib
|
|
||||||
Apache-2.0
|
|
||||||
Apache License
|
|
||||||
|
|
||||||
Version 2.0, January 2004
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/
|
|
||||||
|
|
||||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
||||||
|
|
||||||
1. Definitions.
|
|
||||||
|
|
||||||
"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document.
|
|
||||||
|
|
||||||
"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License.
|
|
||||||
|
|
||||||
"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
|
|
||||||
|
|
||||||
"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License.
|
|
||||||
|
|
||||||
"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files.
|
|
||||||
|
|
||||||
"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types.
|
|
||||||
|
|
||||||
"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below).
|
|
||||||
|
|
||||||
"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof.
|
|
||||||
|
|
||||||
"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution."
|
|
||||||
|
|
||||||
"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work.
|
|
||||||
|
|
||||||
2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form.
|
|
||||||
|
|
||||||
3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed.
|
|
||||||
|
|
||||||
4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions:
|
|
||||||
|
|
||||||
You must give any other recipients of the Work or Derivative Works a copy of this License; and
|
|
||||||
|
|
||||||
You must cause any modified files to carry prominent notices stating that You changed the files; and
|
|
||||||
|
|
||||||
You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and
|
|
||||||
|
|
||||||
If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License.
|
|
||||||
|
|
||||||
5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions.
|
|
||||||
|
|
||||||
6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file.
|
|
||||||
|
|
||||||
7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License.
|
|
||||||
|
|
||||||
8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages.
|
|
||||||
|
|
||||||
9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability.
|
|
||||||
|
|
||||||
END OF TERMS AND CONDITIONS
|
|
||||||
|
|
||||||
|
|
||||||
uuid
|
|
||||||
MIT
|
|
||||||
The MIT License (MIT)
|
|
||||||
|
|
||||||
Copyright (c) 2010-2016 Robert Kieffer and other contributors
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
||||||
|
|
||||||
|
|
||||||
web-animations-js
|
|
||||||
Apache-2.0
|
|
||||||
|
|
||||||
webpack
|
|
||||||
MIT
|
|
||||||
Copyright JS Foundation and other contributors
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining
|
|
||||||
a copy of this software and associated documentation files (the
|
|
||||||
'Software'), to deal in the Software without restriction, including
|
|
||||||
without limitation the rights to use, copy, modify, merge, publish,
|
|
||||||
distribute, sublicense, and/or sell copies of the Software, and to
|
|
||||||
permit persons to whom the Software is furnished to do so, subject to
|
|
||||||
the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be
|
|
||||||
included in all copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
|
||||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
||||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
|
||||||
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
|
||||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
|
||||||
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
|
||||||
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
||||||
|
|
||||||
|
|
||||||
zone.js
|
|
||||||
MIT
|
|
||||||
The MIT License
|
|
||||||
|
|
||||||
Copyright (c) 2010-2020 Google LLC. http://angular.io/license
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in
|
|
||||||
all copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
||||||
THE SOFTWARE.
|
|
||||||
@@ -1,200 +0,0 @@
|
|||||||
{
|
|
||||||
"ccc7e92cbdd35e901acf9ad80941abee07bd8f60": "No es necesario incluir URL, solo todo después ",
|
|
||||||
"f41145afc02fd47ef0576ac79acd2c47ebbf4901": "Argumentos personalizados globales para descargas en la página de inicio.",
|
|
||||||
"17f0ea5d2d7a262b0e875acc70475f102aee84e6": "Crea una lista de reproducción",
|
|
||||||
"cff1428d10d59d14e45edec3c735a27b5482db59": "Nombre",
|
|
||||||
"f47e2d56dd8a145b2e9599da9730c049d52962a2": "Archivos de sonido",
|
|
||||||
"a52dae09be10ca3a65da918533ced3d3f4992238": "Archivos de video",
|
|
||||||
"d9e83ac17026e70ef6e9c0f3240a3b2450367f40": "Modificar args de youtube-dl",
|
|
||||||
"7fc1946abe2b40f60059c6cd19975d677095fd19": "Args nuevos simulados",
|
|
||||||
"0b71824ae71972f236039bed43f8d2323e8fd570": "Añadir un arg",
|
|
||||||
"c8b0e59eb491f2ac7505f0fbab747062e6b32b23": "Busqueda por categoria",
|
|
||||||
"9eeb91caef5a50256dd87e1c4b7b3e8216479377": "Usar valor de arg",
|
|
||||||
"25d8ad5eba2ec24e68295a27d6a4bb9b49e3dacd": "Valor de arg",
|
|
||||||
"7de2451ed3fb8d8b847979bd3f0c740b970f167b": "Añadir arg",
|
|
||||||
"d7b35c384aecd25a516200d6921836374613dfe7": "Cancelar",
|
|
||||||
"b2623aee44b70c9a4ba1fce16c8a593b0a4c7974": "Modificar",
|
|
||||||
"038ebcb2a89155d90c24fa1c17bfe83dbadc3c20": "Descargador de Youtube",
|
|
||||||
"6d2ec8898344c8955542b0542c942038ef28bb80": "Por favor entre una URL válida",
|
|
||||||
"a38ae1082fec79ba1f379978337385a539a28e73": " Calidad ",
|
|
||||||
"4be966a9dcfbc9b54dfcc604b831c0289f847fa4": "Usa URL",
|
|
||||||
"d3f02f845e62cebd75fde451ab8479d2a8ad784d": " Ver ",
|
|
||||||
"4a9889d36910edc8323d7bab60858ab3da6d91df": "Solo audio",
|
|
||||||
"96a01fafe135afc58b0f8071a4ab00234495ce18": "Descarga múltiple",
|
|
||||||
"6a21ba5fb0ac804a525bf9ab168038c3ee88e661": "Descarga",
|
|
||||||
"6a3777f913cf3f288664f0632b9f24794fdcc24e": "Cancelar",
|
|
||||||
"322ed150e02666fe2259c5b4614eac7066f4ffa0": "Avanzado",
|
|
||||||
"b7ffe7c6586d6f3f18a9246806a7c7d5538ab43e": "Commando simulado:",
|
|
||||||
"4e4c721129466be9c3862294dc40241b64045998": "Usar argumentos personalizados",
|
|
||||||
"ad2f8ac8b7de7945b80c8e424484da94e597125f": "Argumentos personalizados",
|
|
||||||
"a6911c2157f1b775284bbe9654ce5eb30cf45d7f": "No es necesario incluir URL, solo todo después. Los argumentos se delimitan usando dos comas así: ,,",
|
|
||||||
"3a92a3443c65a52f37ca7efb8f453b35dbefbf29": "Usar salida personalizada",
|
|
||||||
"d9c02face477f2f9cdaae318ccee5f89856851fb": "Salida personalizada",
|
|
||||||
"fcfd4675b4c90f08d18d3abede9a9a4dff4cfdc7": "Documentación",
|
|
||||||
"19d1ae64d94d28a29b2c57ae8671aace906b5401": "La ruta es relativa a la ruta de descarga de la config. No incluya el extensión.",
|
|
||||||
"8fad10737d3e3735a6699a4d89cbf6c20f6bb55f": "Usa autenticación",
|
|
||||||
"08c74dc9762957593b91f6eb5d65efdfc975bf48": "Nombre de usuario",
|
|
||||||
"c32ef07f8803a223a83ed17024b38e8d82292407": "Contraseña",
|
|
||||||
"4a0dada6e841a425de3e5006e6a04df26c644fa5": "Audio",
|
|
||||||
"9779715ac05308973d8f1c8658b29b986e92450f": "Tus archivos de audio están aquí",
|
|
||||||
"47546e45bbb476baaaad38244db444c427ddc502": "Listas de reproducción",
|
|
||||||
"78bd81adb4609b68cfa4c589222bdc233ba1faaa": "No hay listas de reproducción disponibles. Cree uno de tus archivos de audio haciendo clic en el botón azul más.",
|
|
||||||
"9d2b62bb0b91e2e17fb4177a7e3d6756a2e6ee33": "Vídeo",
|
|
||||||
"960582a8b9d7942716866ecfb7718309728f2916": "Tus archivos de video son aquí",
|
|
||||||
"0f59c46ca29e9725898093c9ea6b586730d0624e": "No hay listas de reproducción disponibles. Cree uno de tus archivos de video haciendo clic en el botón azul más.",
|
|
||||||
"616e206cb4f25bd5885fc35925365e43cf5fb929": "Nombre:",
|
|
||||||
"c52db455cca9109ee47e1a612c3f4117c09eb71b": "URL:",
|
|
||||||
"c6eb45d085384903e53ab001a3513d1de6a1dbac": "Cargador:",
|
|
||||||
"109c6f4a5e46efb933612ededfaf52a13178b7e0": "Tamaño del archivo:",
|
|
||||||
"bd630d8669b16e5f264ec4649d9b469fe03e5ff4": "Ruta:",
|
|
||||||
"a67e7d843cef735c79d5ef1c8ba4af3e758912bb": "Subido:",
|
|
||||||
"f4e529ae5ffd73001d1ff4bbdeeb0a72e342e5c8": "Cerca",
|
|
||||||
"ca3dbbc7f3e011bffe32a10a3ea45cc84f30ecf1": "ID:",
|
|
||||||
"e684046d73bcee88e82f7ff01e2852789a05fc32": "Cuenta:",
|
|
||||||
"321e4419a943044e674beb55b8039f42a9761ca5": "Información",
|
|
||||||
"826b25211922a1b46436589233cb6f1a163d89b7": "Eliminar",
|
|
||||||
"34504b488c24c27e68089be549f0eeae6ebaf30b": "Eliminar y pones en la lista negra",
|
|
||||||
"121cc5391cd2a5115bc2b3160379ee5b36cd7716": "Configuraciones",
|
|
||||||
"801b98c6f02fe3b32f6afa3ee854c99ed83474e6": "URL",
|
|
||||||
"54c512cca1923ab72faf1a0bd98d3d172469629a": "URL desde la que se accederá a esta aplicación, sin el puerto.",
|
|
||||||
"cb2741a46e3560f6bc6dfd99d385e86b08b26d72": "Puerto",
|
|
||||||
"22e8f1d0423a3b784fe40fab187b92c06541b577": "Puerto deseado. El valor predeterminado es 17442.",
|
|
||||||
"d4477669a560750d2064051a510ef4d7679e2f3e": "Modo multiusuario",
|
|
||||||
"2eb03565fcdce7a7a67abc277a936a32fcf51557": "Ruta base de usuarios",
|
|
||||||
"a64505c41150663968e277ec9b3ddaa5f4838798": "Ruta base para los usuarios y sus videos descargados.",
|
|
||||||
"cbe16a57be414e84b6a68309d08fad894df797d6": "Usa cifrado",
|
|
||||||
"0c1875a79b7ecc792cc1bebca3e063e40b5764f9": "Ruta del archivo de certificado",
|
|
||||||
"736551b93461d2de64b118cf4043eee1d1c2cb2c": "Ruta de archivo de clave",
|
|
||||||
"4e3120311801c4acd18de7146add2ee4a4417773": "Permitir suscripciones",
|
|
||||||
"4bee2a4bef2d26d37c9b353c278e24e5cd309ce3": "Ruta base de suscripciones",
|
|
||||||
"bc9892814ee2d119ae94378c905ea440a249b84a": "Ruta base para videos de sus canales y listas de reproducción suscritos. Es relativo a la carpeta raíz de YTDL-Material.",
|
|
||||||
"5bef4b25ba680da7fff06b86a91b1fc7e6a926e3": "Intervalo de comprobación",
|
|
||||||
"0f56a7449b77630c114615395bbda4cab398efd8": "La unidad es segundos, solo incluye números.",
|
|
||||||
"78e49b7339b4fa7184dd21bcaae107ce9b7076f6": "Usa el archivo de youtube-dl",
|
|
||||||
"fa9fe4255231dd1cc6b29d3d254a25cb7c764f0f": "Con la función de archivo de youtube-dl,",
|
|
||||||
"09006404cccc24b7a8f8d1ce0b39f2761ab841d8": "los videos descargados de sus suscripciones se graban en un archivo de texto en el subdirectorio del archivo de suscripciones.",
|
|
||||||
"29ed79a98fc01e7f9537777598e31dbde3aa7981": "Esto permite eliminar videos de sus suscripciones de forma permanente sin darse de baja y le permite grabar los videos que descargó en caso de pérdida de datos.",
|
|
||||||
"27a56aad79d8b61269ed303f11664cc78bcc2522": "Tema",
|
|
||||||
"ff7cee38a2259526c519f878e71b964f41db4348": "Defecto",
|
|
||||||
"adb4562d2dbd3584370e44496969d58c511ecb63": "Oscura",
|
|
||||||
"7a6bacee4c31cb5c0ac2d24274fb4610d8858602": "Permitir cambio de tema",
|
|
||||||
"fe46ccaae902ce974e2441abe752399288298619": "Idioma",
|
|
||||||
"82421c3e46a0453a70c42900eab51d58d79e6599": "Principal",
|
|
||||||
"ab2756805742e84ad0cc0468f4be2d8aa9f855a5": "Ruta de la carpeta de audio",
|
|
||||||
"c2c89cdf45d46ea64d2ed2f9ac15dfa4d77e26ca": "Ruta para descargas de solo audio. Es relativo a la carpeta raíz de YTDL-Material.",
|
|
||||||
"46826331da1949bd6fb74624447057099c9d20cd": "Ruta de la carpeta de video",
|
|
||||||
"17c92e6d47a213fa95b5aa344b3f258147123f93": "Ruta de descarga de videos. Es relativo a la carpeta raíz de YTDL-Material.",
|
|
||||||
"6b995e7130b4d667eaab6c5f61b362ace486d26d": "Argumentos personalizados globales para descargas en la página de inicio. Los argumentos se delimitan usando dos comas así: ,,",
|
|
||||||
"0ba25ad86a240576c4f20a2fada4722ebba77b1e": "Descargador",
|
|
||||||
"61f8fd90b5f8cb20c70371feb2ee5e1fac5a9095": "Título superior",
|
|
||||||
"78d3531417c0d4ba4c90f0d4ae741edc261ec8df": "Administrador de archivos habilitado",
|
|
||||||
"a5a1be0a5df07de9eec57f5d2a86ed0204b2e75a": "Administrador de descargas habilitado",
|
|
||||||
"c33bd5392b39dbed36b8e5a1145163a15d45835f": "Permitir selección de calidad",
|
|
||||||
"bda5508e24e0d77debb28bcd9194d8fefb1cfb92": "Modo de solo descarga",
|
|
||||||
"09d31c803a7252658694e1e3176b97f5655a3fe3": "Permitir el modo de descarga múltiple",
|
|
||||||
"d8b47221b5af9e9e4cd5cb434d76fc0c91611409": "Requiere pin para la configuración",
|
|
||||||
"f5ec7b2cdf87d41154f4fcbc86e856314409dcb9": "Establecer nuevo pin",
|
|
||||||
"1c4dbce56d96b8974aac24a02f7ab2ee81415014": "Habilitar API pública",
|
|
||||||
"23bd81dcc30b74d06279a26d7a42e8901c1b124e": "Clave API pública",
|
|
||||||
"41016a73d8ad85e6cb26dffa0a8fab9fe8f60d8e": "Ver documentación",
|
|
||||||
"1b258b258b4cc475ceb2871305b61756b0134f4a": "Generar",
|
|
||||||
"d5d7c61349f3b0859336066e6d453fc35d334fe5": "Utilizar la API de YouTube",
|
|
||||||
"ce10d31febb3d9d60c160750570310f303a22c22": "Clave API de YouTube",
|
|
||||||
"8602e313cdfa7c4cc475ccbe86459fce3c3fd986": "¡Generar una clave es fácil!",
|
|
||||||
"9b3cedfa83c6d7acb3210953289d1be4aab115c7": "¡Haga clic aquí",
|
|
||||||
"7f09776373995003161235c0c8d02b7f91dbc4df": "para descargar la extensión Chrome oficial de YoutubeDL-Material manualmente.",
|
|
||||||
"5b5296423906ab3371fdb2b5a5aaa83acaa2ee52": "Debe cargar manualmente la extensión y modificar la configuración de la extensión para establecer la URL de la interfaz.",
|
|
||||||
"9a2ec6da48771128384887525bdcac992632c863": "para instalar la extensión Firefox oficial de YoutubeDL-Material directamente desde la página de extensiones de Firefox.",
|
|
||||||
"eb81be6b49e195e5307811d1d08a19259d411f37": "Instrucciones detalladas de configuración.",
|
|
||||||
"cb17ff8fe3961cf90f44bee97c88a3f3347a7e55": "No se requiere mucho más que cambiar la configuración de la extensión para establecer la URL de la interfaz.",
|
|
||||||
"61b81b11aad0b9d970ece2fce18405f07eac69c2": "Arrastra el enlace de abajo a tus marcadores, ¡y listo! Simplemente navegue hasta el video de YouTube que desea descargar y haga clic en el marcador.",
|
|
||||||
"c505d6c5de63cc700f0aaf8a4b31fae9e18024e5": "Generar bookmarklet solo de audio",
|
|
||||||
"d5f69691f9f05711633128b5a3db696783266b58": "Extra",
|
|
||||||
"5fab47f146b0a4b809dcebf3db9da94df6299ea1": "Usar agente de descarga predeterminado",
|
|
||||||
"ec71e08aee647ea4a71fd6b7510c54d84a797ca6": "Seleccione un descargador",
|
|
||||||
"dc3d990391c944d1fbfc7cfb402f7b5e112fb3a8": "Permitir descarga avanzada",
|
|
||||||
"bc2e854e111ecf2bd7db170da5e3c2ed08181d88": "Avanzado",
|
|
||||||
"37224420db54d4bc7696f157b779a7225f03ca9d": "Permitir registro de usuario",
|
|
||||||
"4d13a9cd5ed3dcee0eab22cb25198d43886942be": "Usuarios",
|
|
||||||
"52c9a103b812f258bcddc3d90a6e3f46871d25fe": "Salvar",
|
|
||||||
"fe8fd36dbf5deee1d56564965787a782a66eba44": "{VAR_SELECT, select, true {Cerrar} false {Cancelar} other {Otro} }",
|
|
||||||
"cec82c0a545f37420d55a9b6c45c20546e82f94e": "Sobre YoutubeDL-Material",
|
|
||||||
"199c17e5d6a419313af3c325f06dcbb9645ca618": "es un descargador de código abierto de YouTube creado bajo las especificaciones de \"Material Design\" de Google. Puede descargar sin problemas sus videos favoritos como archivos de video o audio, e incluso suscribirse a sus canales favoritos y listas de reproducción para mantenerse actualizado con sus nuevos videos.",
|
|
||||||
"bc0ad0ee6630acb7fcb7802ec79f5a0ee943c1a7": "tiene algunas características increíbles incluidas! Una amplia API, soporte de Docker y soporte de localización (traducción). Lea todas las funciones compatibles haciendo clic en el icono de GitHub que se encuentra arriba.",
|
|
||||||
"a45e3b05f0529dc5246d70ef62304c94426d4c81": "Versión instalada:",
|
|
||||||
"e22f3a5351944f3a1a10cfc7da6f65dfbe0037fe": "Comprobando actualizaciones...",
|
|
||||||
"a16e92385b4fd9677bb830a4b796b8b79c113290": "Actualización disponible",
|
|
||||||
"189b28aaa19b3c51c6111ad039c4fd5e2a22e370": "Puede actualizar desde el menú de configuración.",
|
|
||||||
"b33536f59b94ec935a16bd6869d836895dc5300c": "¿Encontró un error o tiene una sugerencia?",
|
|
||||||
"e1f398f38ff1534303d4bb80bd6cece245f24016": "para crear una cuestión!",
|
|
||||||
"42ff677ec14f111e88bd6cdd30145378e994d1bf": "Tu perfil",
|
|
||||||
"ac9d09de42edca1296371e4d801349c9096ac8de": "UID:",
|
|
||||||
"a5ed099ffc9e96f6970df843289ade8a7d20ab9f": "Creado:",
|
|
||||||
"fa96f2137af0a24e6d6d54c598c0af7d5d5ad344": "Usted no se ha identificado.",
|
|
||||||
"6765b4c916060f6bc42d9bb69e80377dbcb5e4e9": "Identificarse",
|
|
||||||
"bb694b49d408265c91c62799c2b3a7e3151c824d": "Salir",
|
|
||||||
"a1dbca87b9f36d2b06a5cbcffb5814c4ae9b798a": "Crear cuenta de administrador",
|
|
||||||
"2d2adf3ca26a676bca2269295b7455a26fd26980": "No se detectó una cuenta de administrador predeterminada. Esto creará y establecerá la contraseña para una cuenta de administrador con el nombre de usuario como 'admin'.",
|
|
||||||
"70a67e04629f6d412db0a12d51820b480788d795": "Crear",
|
|
||||||
"994363f08f9fbfa3b3994ff7b35c6904fdff18d8": "Perfil",
|
|
||||||
"004b222ff9ef9dd4771b777950ca1d0e4cd4348a": "Sobre",
|
|
||||||
"92eee6be6de0b11c924e3ab27db30257159c0a7c": "Inicio",
|
|
||||||
"357064ca9d9ac859eb618e28e8126fa32be049e2": "Suscripciones",
|
|
||||||
"822fab38216f64e8166d368b59fe756ca39d301b": "Descargas",
|
|
||||||
"a249a5ae13e0835383885aaf697d2890cc3e53e9": "Compartir lista de reproducción",
|
|
||||||
"15da89490e04496ca9ea1e1b3d44fb5efd4a75d9": "Compartir vídeo",
|
|
||||||
"1d540dcd271b316545d070f9d182c372d923aadd": "Compartir audio",
|
|
||||||
"1f6d14a780a37a97899dc611881e6bc971268285": "Habilitar compartir",
|
|
||||||
"6580b6a950d952df847cb3d8e7176720a740adc8": "Usar marca de tiempo",
|
|
||||||
"4f2ed9e71a7c981db3e50ae2fedb28aff2ec4e6c": "Segundos",
|
|
||||||
"3a6e5a6aa78ca864f6542410c5dafb6334538106": "Copiar al Portapapeles",
|
|
||||||
"5b3075e8dc3f3921ec316b0bd83b6d14a06c1a4f": "Guardar cambios",
|
|
||||||
"4f8b2bb476981727ab34ed40fde1218361f92c45": "Detalles",
|
|
||||||
"383000ab16bf415d5a1d61d7eb7b5959c72a9515": "Se ha producido un error:",
|
|
||||||
"77b0c73840665945b25bd128709aa64c8f017e1c": "Inicio de descarga:",
|
|
||||||
"08ff9375ec078065bcdd7637b7ea65fce2979266": "Fin de descarga:",
|
|
||||||
"ad127117f9471612f47d01eae09709da444a36a4": "Ruta(s) del archivo:",
|
|
||||||
"a9806cf78ce00eb2613eeca11354a97e033377b8": "Suscríbase a la lista de reproducción o al canal",
|
|
||||||
"93efc99ae087fc116de708ecd3ace86ca237cf30": "La lista de reproducción o la URL del canal",
|
|
||||||
"08f5d0ef937ae17feb1b04aff15ad88911e87baf": "Nombre personalizado",
|
|
||||||
"f3f62aa84d59f3a8b900cc9a7eec3ef279a7b4e7": "Esto es opcional",
|
|
||||||
"ea30873bd3f0d5e4fb2378eec3f0a1db77634a28": "Descargar todas las cargas",
|
|
||||||
"28a678e9cabf86e44c32594c43fa0e890135c20f": "Descargar videos subidos en el último",
|
|
||||||
"408ca4911457e84a348cecf214f02c69289aa8f1": "Modo de solo transmisión",
|
|
||||||
"d0336848b0c375a1c25ba369b3481ee383217a4f": "Subscribe",
|
|
||||||
"e78c0d60ac39787f62c9159646fe0b3c1ed55a1d": "Tipo:",
|
|
||||||
"a44d86aa1e6c20ced07aca3a7c081d8db9ded1c6": "Archivo:",
|
|
||||||
"8efc77bf327659c0fec1f518cf48a98cdcd9dddf": "Exportar el archivo",
|
|
||||||
"3042bd3ad8dffcfeca5fd1ae6159fd1047434e95": "Darse de baja",
|
|
||||||
"e2319dec5b4ccfb6ed9f55ccabd63650a8fdf547": "Sus suscripciones",
|
|
||||||
"807cf11e6ac1cde912496f764c176bdfdd6b7e19": "Canales",
|
|
||||||
"29b89f751593e1b347eef103891b7a1ff36ec03f": "Nombre no disponible. Recuperación de canales en progreso.",
|
|
||||||
"4636cd4a1379c50d471e98786098c4d39e1e82ad": "No tienes suscripciones de canal.",
|
|
||||||
"2e0a410652cb07d069f576b61eab32586a18320d": "Nombre no disponible. Recuperación de listas de reproducción en progreso.",
|
|
||||||
"587b57ced54965d8874c3fd0e9dfedb987e5df04": "No tienes suscripciones a listas de reproducción.",
|
|
||||||
"7e892ba15f2c6c17e83510e273b3e10fc32ea016": "Buscar",
|
|
||||||
"2054791b822475aeaea95c0119113de3200f5e1c": "Duración:",
|
|
||||||
"94e01842dcee90531caa52e4147f70679bac87fe": "Eliminar y volver a descargar",
|
|
||||||
"2031adb51e07a41844e8ba7704b054e98345c9c1": "Borrar para siempre",
|
|
||||||
"91ecce65f1d23f9419d1c953cd6b7bc7f91c110e": "Updater",
|
|
||||||
"1372e61c5bd06100844bd43b98b016aabc468f62": "Seleccione una versión:",
|
|
||||||
"cfc2f436ec2beffb042e7511a73c89c372e86a6c": "Registrarse",
|
|
||||||
"a1ad8b1be9be43b5183bd2c3186d4e19496f2a0b": "ID de sesión:",
|
|
||||||
"eb98135e35af26a9a326ee69bd8ff104d36dd8ec": "(actual)",
|
|
||||||
"7117fc42f860e86d983bfccfcf2654e5750f3406": "¡No hay descargas disponibles!",
|
|
||||||
"b7ff2e2b909c53abe088fe60b9f4b6ac7757247f": "Registrar un usuario",
|
|
||||||
"024886ca34a6f309e3e51c2ed849320592c3faaa": "Nombre de usuario",
|
|
||||||
"2bd201aea09e43fbfd3cd15ec0499b6755302329": "Administrar usuario",
|
|
||||||
"29c97c8e76763bb15b6d515648fa5bd1eb0f7510": "UID de usuario:",
|
|
||||||
"e70e209561583f360b1e9cefd2cbb1fe434b6229": "Nueva contraseña",
|
|
||||||
"6498fa1b8f563988f769654a75411bb8060134b9": "Establecer nueva contraseña",
|
|
||||||
"40da072004086c9ec00d125165da91eaade7f541": "Uso por defecto",
|
|
||||||
"4f20f2d5a6882190892e58b85f6ccbedfa737952": "Si",
|
|
||||||
"3d3ae7deebc5949b0c1c78b9847886a94321d9fd": "No",
|
|
||||||
"57c6c05d8ebf4ef1180c2705033c044f655bb2c4": "Gestionar rol",
|
|
||||||
"746f64ddd9001ac456327cd9a3d5152203a4b93c": " Nombre de usuario ",
|
|
||||||
"52c1447c1ec9570a2a3025c7e566557b8d19ed92": " Rol ",
|
|
||||||
"59a8c38db3091a63ac1cb9590188dc3a972acfb3": " Acciones ",
|
|
||||||
"4d92a0395dd66778a931460118626c5794a3fc7a": "Agregar Usuarios",
|
|
||||||
"b0d7dd8a1b0349622d6e0c6e643e24a9ea0efa1d": "Editar Rol"
|
|
||||||
}
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 2.6 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 6.1 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 2.3 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 19 KiB |
@@ -1,18 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<title>YoutubeDLMaterial</title>
|
|
||||||
<base href="./">
|
|
||||||
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
||||||
|
|
||||||
<link href="https://fonts.googleapis.com/css?family=Roboto&display=swap" rel="stylesheet">
|
|
||||||
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
|
||||||
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
|
|
||||||
<link href="https://fonts.googleapis.com/icon?family=Material+Icons|Material+Icons+Outlined" rel="stylesheet"/>
|
|
||||||
<link rel="stylesheet" href="styles.5112d6db78cf21541598.css"></head>
|
|
||||||
<body>
|
|
||||||
<app-root></app-root>
|
|
||||||
<script src="runtime-es2015.06b6262a0d981fd4885e.js" type="module"></script><script src="runtime-es5.06b6262a0d981fd4885e.js" nomodule defer></script><script src="polyfills-es5.7f923c8f5afda210edd3.js" nomodule defer></script><script src="polyfills-es2015.5b408f108bcea938a7e2.js" type="module"></script><script src="main-es2015.0cbc545a4a3bee376826.js" type="module"></script><script src="main-es5.0cbc545a4a3bee376826.js" nomodule defer></script></body>
|
|
||||||
</html>
|
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
|
|||||||
!function(e){function r(r){for(var n,i,a=r[0],c=r[1],l=r[2],p=0,s=[];p<a.length;p++)i=a[p],Object.prototype.hasOwnProperty.call(o,i)&&o[i]&&s.push(o[i][0]),o[i]=0;for(n in c)Object.prototype.hasOwnProperty.call(c,n)&&(e[n]=c[n]);for(f&&f(r);s.length;)s.shift()();return u.push.apply(u,l||[]),t()}function t(){for(var e,r=0;r<u.length;r++){for(var t=u[r],n=!0,a=1;a<t.length;a++)0!==o[t[a]]&&(n=!1);n&&(u.splice(r--,1),e=i(i.s=t[0]))}return e}var n={},o={0:0},u=[];function i(r){if(n[r])return n[r].exports;var t=n[r]={i:r,l:!1,exports:{}};return e[r].call(t.exports,t,t.exports,i),t.l=!0,t.exports}i.e=function(e){var r=[],t=o[e];if(0!==t)if(t)r.push(t[2]);else{var n=new Promise((function(r,n){t=o[e]=[r,n]}));r.push(t[2]=n);var u,a=document.createElement("script");a.charset="utf-8",a.timeout=120,i.nc&&a.setAttribute("nonce",i.nc),a.src=function(e){return i.p+""+({}[e]||e)+"-es2015."+{1:"cc1ef452b2945b55327a"}[e]+".js"}(e);var c=new Error;u=function(r){a.onerror=a.onload=null,clearTimeout(l);var t=o[e];if(0!==t){if(t){var n=r&&("load"===r.type?"missing":r.type),u=r&&r.target&&r.target.src;c.message="Loading chunk "+e+" failed.\n("+n+": "+u+")",c.name="ChunkLoadError",c.type=n,c.request=u,t[1](c)}o[e]=void 0}};var l=setTimeout((function(){u({type:"timeout",target:a})}),12e4);a.onerror=a.onload=u,document.head.appendChild(a)}return Promise.all(r)},i.m=e,i.c=n,i.d=function(e,r,t){i.o(e,r)||Object.defineProperty(e,r,{enumerable:!0,get:t})},i.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},i.t=function(e,r){if(1&r&&(e=i(e)),8&r)return e;if(4&r&&"object"==typeof e&&e&&e.__esModule)return e;var t=Object.create(null);if(i.r(t),Object.defineProperty(t,"default",{enumerable:!0,value:e}),2&r&&"string"!=typeof e)for(var n in e)i.d(t,n,(function(r){return e[r]}).bind(null,n));return t},i.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return i.d(r,"a",r),r},i.o=function(e,r){return Object.prototype.hasOwnProperty.call(e,r)},i.p="",i.oe=function(e){throw console.error(e),e};var a=window.webpackJsonp=window.webpackJsonp||[],c=a.push.bind(a);a.push=r,a=a.slice();for(var l=0;l<a.length;l++)r(a[l]);var f=c;t()}([]);
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
!function(e){function r(r){for(var n,i,a=r[0],c=r[1],l=r[2],p=0,s=[];p<a.length;p++)i=a[p],Object.prototype.hasOwnProperty.call(o,i)&&o[i]&&s.push(o[i][0]),o[i]=0;for(n in c)Object.prototype.hasOwnProperty.call(c,n)&&(e[n]=c[n]);for(f&&f(r);s.length;)s.shift()();return u.push.apply(u,l||[]),t()}function t(){for(var e,r=0;r<u.length;r++){for(var t=u[r],n=!0,a=1;a<t.length;a++)0!==o[t[a]]&&(n=!1);n&&(u.splice(r--,1),e=i(i.s=t[0]))}return e}var n={},o={0:0},u=[];function i(r){if(n[r])return n[r].exports;var t=n[r]={i:r,l:!1,exports:{}};return e[r].call(t.exports,t,t.exports,i),t.l=!0,t.exports}i.e=function(e){var r=[],t=o[e];if(0!==t)if(t)r.push(t[2]);else{var n=new Promise((function(r,n){t=o[e]=[r,n]}));r.push(t[2]=n);var u,a=document.createElement("script");a.charset="utf-8",a.timeout=120,i.nc&&a.setAttribute("nonce",i.nc),a.src=function(e){return i.p+""+({}[e]||e)+"-es5."+{1:"cc1ef452b2945b55327a"}[e]+".js"}(e);var c=new Error;u=function(r){a.onerror=a.onload=null,clearTimeout(l);var t=o[e];if(0!==t){if(t){var n=r&&("load"===r.type?"missing":r.type),u=r&&r.target&&r.target.src;c.message="Loading chunk "+e+" failed.\n("+n+": "+u+")",c.name="ChunkLoadError",c.type=n,c.request=u,t[1](c)}o[e]=void 0}};var l=setTimeout((function(){u({type:"timeout",target:a})}),12e4);a.onerror=a.onload=u,document.head.appendChild(a)}return Promise.all(r)},i.m=e,i.c=n,i.d=function(e,r,t){i.o(e,r)||Object.defineProperty(e,r,{enumerable:!0,get:t})},i.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},i.t=function(e,r){if(1&r&&(e=i(e)),8&r)return e;if(4&r&&"object"==typeof e&&e&&e.__esModule)return e;var t=Object.create(null);if(i.r(t),Object.defineProperty(t,"default",{enumerable:!0,value:e}),2&r&&"string"!=typeof e)for(var n in e)i.d(t,n,(function(r){return e[r]}).bind(null,n));return t},i.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return i.d(r,"a",r),r},i.o=function(e,r){return Object.prototype.hasOwnProperty.call(e,r)},i.p="",i.oe=function(e){throw console.error(e),e};var a=window.webpackJsonp=window.webpackJsonp||[],c=a.push.bind(a);a.push=r,a=a.slice();for(var l=0;l<a.length;l++)r(a[l]);var f=c;t()}([]);
|
|
||||||
File diff suppressed because one or more lines are too long
@@ -6,17 +6,21 @@ var path = require('path');
|
|||||||
|
|
||||||
var youtubedl = require('youtube-dl');
|
var youtubedl = require('youtube-dl');
|
||||||
const config_api = require('./config');
|
const config_api = require('./config');
|
||||||
|
const twitch_api = require('./twitch');
|
||||||
|
var utils = require('./utils');
|
||||||
|
|
||||||
const debugMode = process.env.YTDL_MODE === 'debug';
|
const debugMode = process.env.YTDL_MODE === 'debug';
|
||||||
|
|
||||||
var logger = null;
|
var logger = null;
|
||||||
var db = null;
|
var db = null;
|
||||||
var users_db = null;
|
var users_db = null;
|
||||||
function setDB(input_db, input_users_db) { db = input_db; users_db = input_users_db }
|
let db_api = null;
|
||||||
|
|
||||||
|
function setDB(input_db_api) { db_api = input_db_api }
|
||||||
function setLogger(input_logger) { logger = input_logger; }
|
function setLogger(input_logger) { logger = input_logger; }
|
||||||
|
|
||||||
function initialize(input_db, input_users_db, input_logger) {
|
function initialize(input_db_api, input_logger) {
|
||||||
setDB(input_db, input_users_db);
|
setDB(input_db_api);
|
||||||
setLogger(input_logger);
|
setLogger(input_logger);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -28,34 +32,33 @@ async function subscribe(sub, user_uid = null) {
|
|||||||
return new Promise(async resolve => {
|
return new Promise(async resolve => {
|
||||||
// sub should just have url and name. here we will get isPlaylist and path
|
// sub should just have url and name. here we will get isPlaylist and path
|
||||||
sub.isPlaylist = sub.url.includes('playlist');
|
sub.isPlaylist = sub.url.includes('playlist');
|
||||||
|
sub.videos = [];
|
||||||
|
|
||||||
let url_exists = false;
|
let url_exists = !!(await db_api.getRecord('subscriptions', {url: sub.url, user_uid: user_uid}));
|
||||||
|
|
||||||
if (user_uid)
|
if (!sub.name && url_exists) {
|
||||||
url_exists = !!users_db.get('users').find({uid: user_uid}).get('subscriptions').find({url: sub.url}).value()
|
logger.error(`Sub with the same URL "${sub.url}" already exists -- please provide a custom name for this new subscription.`);
|
||||||
else
|
result_obj.error = 'Subcription with URL ' + sub.url + ' already exists! Custom name is required.';
|
||||||
url_exists = !!db.get('subscriptions').find({url: sub.url}).value();
|
|
||||||
|
|
||||||
if (url_exists) {
|
|
||||||
logger.info('Sub already exists');
|
|
||||||
result_obj.error = 'Subcription with URL ' + sub.url + ' already exists!';
|
|
||||||
resolve(result_obj);
|
resolve(result_obj);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// add sub to db
|
sub['user_uid'] = user_uid ? user_uid : undefined;
|
||||||
if (user_uid)
|
await db_api.insertRecordIntoTable('subscriptions', sub);
|
||||||
users_db.get('users').find({uid: user_uid}).get('subscriptions').push(sub).write();
|
|
||||||
else
|
|
||||||
db.get('subscriptions').push(sub).write();
|
|
||||||
|
|
||||||
let success = await getSubscriptionInfo(sub, user_uid);
|
let success = await getSubscriptionInfo(sub, user_uid);
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
getVideosForSub(sub, user_uid);
|
||||||
|
} else {
|
||||||
|
logger.error('Subscribe: Failed to get subscription info. Subscribe failed.')
|
||||||
|
};
|
||||||
|
|
||||||
result_obj.success = success;
|
result_obj.success = success;
|
||||||
result_obj.sub = sub;
|
result_obj.sub = sub;
|
||||||
getVideosForSub(sub, user_uid);
|
|
||||||
resolve(result_obj);
|
resolve(result_obj);
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getSubscriptionInfo(sub, user_uid = null) {
|
async function getSubscriptionInfo(sub, user_uid = null) {
|
||||||
@@ -65,10 +68,19 @@ async function getSubscriptionInfo(sub, user_uid = null) {
|
|||||||
else
|
else
|
||||||
basePath = config_api.getConfigItem('ytdl_subscriptions_base_path');
|
basePath = config_api.getConfigItem('ytdl_subscriptions_base_path');
|
||||||
|
|
||||||
return new Promise(resolve => {
|
// get videos
|
||||||
// get videos
|
let downloadConfig = ['--dump-json', '--playlist-end', '1'];
|
||||||
let downloadConfig = ['--dump-json', '--playlist-end', '1']
|
let useCookies = config_api.getConfigItem('ytdl_use_cookies');
|
||||||
youtubedl.exec(sub.url, downloadConfig, {}, function(err, output) {
|
if (useCookies) {
|
||||||
|
if (await fs.pathExists(path.join(__dirname, 'appdata', 'cookies.txt'))) {
|
||||||
|
downloadConfig.push('--cookies', path.join('appdata', 'cookies.txt'));
|
||||||
|
} else {
|
||||||
|
logger.warn('Cookies file could not be found. You can either upload one, or disable \'use cookies\' in the Advanced tab in the settings.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise(async resolve => {
|
||||||
|
youtubedl.exec(sub.url, downloadConfig, {maxBuffer: Infinity}, async (err, output) => {
|
||||||
if (debugMode) {
|
if (debugMode) {
|
||||||
logger.info('Subscribe: got info for subscription ' + sub.id);
|
logger.info('Subscribe: got info for subscription ' + sub.id);
|
||||||
}
|
}
|
||||||
@@ -91,17 +103,18 @@ async function getSubscriptionInfo(sub, user_uid = null) {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (!sub.name) {
|
if (!sub.name) {
|
||||||
sub.name = sub.isPlaylist ? output_json.playlist_title : output_json.uploader;
|
if (sub.isPlaylist) {
|
||||||
|
sub.name = output_json.playlist_title ? output_json.playlist_title : output_json.playlist;
|
||||||
|
} else {
|
||||||
|
sub.name = output_json.uploader;
|
||||||
|
}
|
||||||
// if it's now valid, update
|
// if it's now valid, update
|
||||||
if (sub.name) {
|
if (sub.name) {
|
||||||
if (user_uid)
|
await db_api.updateRecord('subscriptions', {id: sub.id}, {name: sub.name});
|
||||||
users_db.get('users').find({uid: user_uid}).get('subscriptions').find({id: sub.id}).assign({name: sub.name}).write();
|
|
||||||
else
|
|
||||||
db.get('subscriptions').find({id: sub.id}).assign({name: sub.name}).write();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const useArchive = config_api.getConfigItem('ytdl_subscriptions_use_youtubedl_archive');
|
const useArchive = config_api.getConfigItem('ytdl_use_youtubedl_archive');
|
||||||
if (useArchive && !sub.archive) {
|
if (useArchive && !sub.archive) {
|
||||||
// must create the archive
|
// must create the archive
|
||||||
const archive_dir = path.join(__dirname, basePath, 'archives', sub.name);
|
const archive_dir = path.join(__dirname, basePath, 'archives', sub.name);
|
||||||
@@ -113,10 +126,8 @@ async function getSubscriptionInfo(sub, user_uid = null) {
|
|||||||
|
|
||||||
// updates subscription
|
// updates subscription
|
||||||
sub.archive = archive_dir;
|
sub.archive = archive_dir;
|
||||||
if (user_uid)
|
|
||||||
users_db.get('users').find({uid: user_uid}).get('subscriptions').find({id: sub.id}).assign({archive: archive_dir}).write();
|
await db_api.updateRecord('subscriptions', {id: sub.id}, {archive: archive_dir});
|
||||||
else
|
|
||||||
db.get('subscriptions').find({id: sub.id}).assign({archive: archive_dir}).write();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: get even more info
|
// TODO: get even more info
|
||||||
@@ -130,166 +141,156 @@ async function getSubscriptionInfo(sub, user_uid = null) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function unsubscribe(sub, deleteMode, user_uid = null) {
|
async function unsubscribe(sub, deleteMode, user_uid = null) {
|
||||||
return new Promise(async resolve => {
|
|
||||||
let basePath = null;
|
|
||||||
if (user_uid)
|
|
||||||
basePath = path.join(config_api.getConfigItem('ytdl_users_base_path'), user_uid, 'subscriptions');
|
|
||||||
else
|
|
||||||
basePath = config_api.getConfigItem('ytdl_subscriptions_base_path');
|
|
||||||
let result_obj = { success: false, error: '' };
|
|
||||||
|
|
||||||
let id = sub.id;
|
|
||||||
if (user_uid)
|
|
||||||
users_db.get('users').find({uid: user_uid}).get('subscriptions').remove({id: id}).write();
|
|
||||||
else
|
|
||||||
db.get('subscriptions').remove({id: id}).write();
|
|
||||||
|
|
||||||
const appendedBasePath = getAppendedBasePath(sub, basePath);
|
|
||||||
if (deleteMode && fs.existsSync(appendedBasePath)) {
|
|
||||||
if (sub.archive && fs.existsSync(sub.archive)) {
|
|
||||||
const archive_file_path = path.join(sub.archive, 'archive.txt');
|
|
||||||
// deletes archive if it exists
|
|
||||||
if (fs.existsSync(archive_file_path)) {
|
|
||||||
fs.unlinkSync(archive_file_path);
|
|
||||||
}
|
|
||||||
fs.rmdirSync(sub.archive);
|
|
||||||
}
|
|
||||||
deleteFolderRecursive(appendedBasePath);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
async function deleteSubscriptionFile(sub, file, deleteForever, user_uid = null) {
|
|
||||||
let basePath = null;
|
let basePath = null;
|
||||||
if (user_uid)
|
if (user_uid)
|
||||||
basePath = path.join(config_api.getConfigItem('ytdl_users_base_path'), user_uid, 'subscriptions');
|
basePath = path.join(config_api.getConfigItem('ytdl_users_base_path'), user_uid, 'subscriptions');
|
||||||
else
|
else
|
||||||
basePath = config_api.getConfigItem('ytdl_subscriptions_base_path');
|
basePath = config_api.getConfigItem('ytdl_subscriptions_base_path');
|
||||||
const useArchive = config_api.getConfigItem('ytdl_subscriptions_use_youtubedl_archive');
|
let result_obj = { success: false, error: '' };
|
||||||
|
|
||||||
|
let id = sub.id;
|
||||||
|
await db_api.removeRecord('subscriptions', {id: id});
|
||||||
|
await db_api.removeAllRecords('files', {sub_id: id});
|
||||||
|
|
||||||
|
// failed subs have no name, on unsubscribe they shouldn't error
|
||||||
|
if (!sub.name) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const appendedBasePath = getAppendedBasePath(sub, basePath);
|
||||||
|
if (deleteMode && (await fs.pathExists(appendedBasePath))) {
|
||||||
|
if (sub.archive && (await fs.pathExists(sub.archive))) {
|
||||||
|
const archive_file_path = path.join(sub.archive, 'archive.txt');
|
||||||
|
// deletes archive if it exists
|
||||||
|
if (await fs.pathExists(archive_file_path)) {
|
||||||
|
await fs.unlink(archive_file_path);
|
||||||
|
}
|
||||||
|
await fs.rmdir(sub.archive);
|
||||||
|
}
|
||||||
|
await fs.remove(appendedBasePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteSubscriptionFile(sub, file, deleteForever, file_uid = null, user_uid = null) {
|
||||||
|
// TODO: combine this with deletefile
|
||||||
|
let basePath = null;
|
||||||
|
basePath = user_uid ? path.join(config_api.getConfigItem('ytdl_users_base_path'), user_uid, 'subscriptions')
|
||||||
|
: config_api.getConfigItem('ytdl_subscriptions_base_path');
|
||||||
|
const useArchive = config_api.getConfigItem('ytdl_use_youtubedl_archive');
|
||||||
const appendedBasePath = getAppendedBasePath(sub, basePath);
|
const appendedBasePath = getAppendedBasePath(sub, basePath);
|
||||||
const name = file;
|
const name = file;
|
||||||
let retrievedID = null;
|
let retrievedID = null;
|
||||||
return new Promise(resolve => {
|
|
||||||
let filePath = appendedBasePath;
|
|
||||||
var jsonPath = path.join(__dirname,filePath,name+'.info.json');
|
|
||||||
var videoFilePath = path.join(__dirname,filePath,name+'.mp4');
|
|
||||||
var imageFilePath = path.join(__dirname,filePath,name+'.jpg');
|
|
||||||
|
|
||||||
jsonExists = fs.existsSync(jsonPath);
|
await db_api.removeRecord('files', {uid: file_uid});
|
||||||
videoFileExists = fs.existsSync(videoFilePath);
|
|
||||||
imageFileExists = fs.existsSync(imageFilePath);
|
|
||||||
|
|
||||||
if (jsonExists) {
|
let filePath = appendedBasePath;
|
||||||
retrievedID = JSON.parse(fs.readFileSync(jsonPath, 'utf8'))['id'];
|
const ext = (sub.type && sub.type === 'audio') ? '.mp3' : '.mp4'
|
||||||
fs.unlinkSync(jsonPath);
|
var jsonPath = path.join(__dirname,filePath,name+'.info.json');
|
||||||
}
|
var videoFilePath = path.join(__dirname,filePath,name+ext);
|
||||||
|
var imageFilePath = path.join(__dirname,filePath,name+'.jpg');
|
||||||
|
var altImageFilePath = path.join(__dirname,filePath,name+'.webp');
|
||||||
|
|
||||||
if (imageFileExists) {
|
const [jsonExists, videoFileExists, imageFileExists, altImageFileExists] = await Promise.all([
|
||||||
fs.unlinkSync(imageFilePath);
|
fs.pathExists(jsonPath),
|
||||||
}
|
fs.pathExists(videoFilePath),
|
||||||
|
fs.pathExists(imageFilePath),
|
||||||
|
fs.pathExists(altImageFilePath),
|
||||||
|
]);
|
||||||
|
|
||||||
if (videoFileExists) {
|
if (jsonExists) {
|
||||||
fs.unlink(videoFilePath, function(err) {
|
retrievedID = JSON.parse(await fs.readFile(jsonPath, 'utf8'))['id'];
|
||||||
if (fs.existsSync(jsonPath) || fs.existsSync(videoFilePath)) {
|
await fs.unlink(jsonPath);
|
||||||
resolve(false);
|
}
|
||||||
} else {
|
|
||||||
// check if the user wants the video to be redownloaded (deleteForever === false)
|
if (imageFileExists) {
|
||||||
if (!deleteForever && useArchive && sub.archive && retrievedID) {
|
await fs.unlink(imageFilePath);
|
||||||
const archive_path = path.join(sub.archive, 'archive.txt')
|
}
|
||||||
// if archive exists, remove line with video ID
|
|
||||||
if (fs.existsSync(archive_path)) {
|
if (altImageFileExists) {
|
||||||
removeIDFromArchive(archive_path, retrievedID);
|
await fs.unlink(altImageFilePath);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
resolve(true);
|
if (videoFileExists) {
|
||||||
}
|
await fs.unlink(videoFilePath);
|
||||||
});
|
if ((await fs.pathExists(jsonPath)) || (await fs.pathExists(videoFilePath))) {
|
||||||
|
return false;
|
||||||
} else {
|
} else {
|
||||||
// TODO: tell user that the file didn't exist
|
// check if the user wants the video to be redownloaded (deleteForever === false)
|
||||||
resolve(true);
|
if (!deleteForever && useArchive && sub.archive && retrievedID) {
|
||||||
|
const archive_path = path.join(sub.archive, 'archive.txt')
|
||||||
|
// if archive exists, remove line with video ID
|
||||||
|
if (await fs.pathExists(archive_path)) {
|
||||||
|
utils.removeIDFromArchive(archive_path, retrievedID);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
});
|
// TODO: tell user that the file didn't exist
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getVideosForSub(sub, user_uid = null) {
|
async function getVideosForSub(sub, user_uid = null) {
|
||||||
return new Promise(resolve => {
|
const latest_sub_obj = await getSubscription(sub.id);
|
||||||
if (!subExists(sub.id, user_uid)) {
|
if (!latest_sub_obj || latest_sub_obj['downloading']) {
|
||||||
resolve(false);
|
return false;
|
||||||
return;
|
}
|
||||||
|
|
||||||
|
updateSubscriptionProperty(sub, {downloading: true}, user_uid);
|
||||||
|
|
||||||
|
// get basePath
|
||||||
|
let basePath = null;
|
||||||
|
if (user_uid)
|
||||||
|
basePath = path.join(config_api.getConfigItem('ytdl_users_base_path'), user_uid, 'subscriptions');
|
||||||
|
else
|
||||||
|
basePath = config_api.getConfigItem('ytdl_subscriptions_base_path');
|
||||||
|
|
||||||
|
let appendedBasePath = getAppendedBasePath(sub, basePath);
|
||||||
|
fs.ensureDirSync(appendedBasePath);
|
||||||
|
|
||||||
|
let multiUserMode = null;
|
||||||
|
if (user_uid) {
|
||||||
|
multiUserMode = {
|
||||||
|
user: user_uid,
|
||||||
|
file_path: appendedBasePath
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// get sub_db
|
const downloadConfig = await generateArgsForSubscription(sub, user_uid);
|
||||||
let sub_db = null;
|
|
||||||
if (user_uid)
|
|
||||||
sub_db = users_db.get('users').find({uid: user_uid}).get('subscriptions').find({id: sub.id});
|
|
||||||
else
|
|
||||||
sub_db = db.get('subscriptions').find({id: sub.id});
|
|
||||||
|
|
||||||
// get basePath
|
// get videos
|
||||||
let basePath = null;
|
logger.verbose('Subscription: getting videos for subscription ' + sub.name);
|
||||||
if (user_uid)
|
|
||||||
basePath = path.join(config_api.getConfigItem('ytdl_users_base_path'), user_uid, 'subscriptions');
|
|
||||||
else
|
|
||||||
basePath = config_api.getConfigItem('ytdl_subscriptions_base_path');
|
|
||||||
|
|
||||||
const useArchive = config_api.getConfigItem('ytdl_subscriptions_use_youtubedl_archive');
|
return new Promise(async resolve => {
|
||||||
|
const preimported_file_paths = [];
|
||||||
|
const PREIMPORT_INTERVAL = 5000;
|
||||||
|
const preregister_check = setInterval(async () => {
|
||||||
|
if (sub.streamingOnly) return;
|
||||||
|
await db_api.preimportUnregisteredSubscriptionFile(sub, appendedBasePath);
|
||||||
|
}, PREIMPORT_INTERVAL);
|
||||||
|
youtubedl.exec(sub.url, downloadConfig, {maxBuffer: Infinity}, async function(err, output) {
|
||||||
|
// cleanup
|
||||||
|
updateSubscriptionProperty(sub, {downloading: false}, user_uid);
|
||||||
|
clearInterval(preregister_check);
|
||||||
|
|
||||||
let appendedBasePath = null
|
|
||||||
if (sub.name) {
|
|
||||||
appendedBasePath = getAppendedBasePath(sub, basePath);
|
|
||||||
} else {
|
|
||||||
appendedBasePath = path.join(basePath, (sub.isPlaylist ? 'playlists/%(playlist_title)s' : 'channels/%(uploader)s'));
|
|
||||||
}
|
|
||||||
|
|
||||||
let downloadConfig = ['-o', appendedBasePath + '/%(title)s.mp4', '-f', 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/mp4', '-ciw', '--write-info-json', '--print-json'];
|
|
||||||
|
|
||||||
let archive_dir = null;
|
|
||||||
let archive_path = null;
|
|
||||||
|
|
||||||
if (useArchive) {
|
|
||||||
if (sub.archive) {
|
|
||||||
archive_dir = sub.archive;
|
|
||||||
archive_path = path.join(archive_dir, 'archive.txt')
|
|
||||||
}
|
|
||||||
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) {
|
|
||||||
downloadConfig.push('--dateafter', sub.timerange);
|
|
||||||
}
|
|
||||||
|
|
||||||
let useCookies = config_api.getConfigItem('ytdl_use_cookies');
|
|
||||||
if (useCookies) {
|
|
||||||
if (fs.existsSync(path.join(__dirname, 'appdata', 'cookies.txt'))) {
|
|
||||||
downloadConfig.push('--cookies', path.join('appdata', 'cookies.txt'));
|
|
||||||
} else {
|
|
||||||
logger.warn('Cookies file could not be found. You can either upload one, or disable \'use cookies\' in the Advanced tab in the settings.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// get videos
|
|
||||||
logger.verbose('Subscription: getting videos for subscription ' + sub.name);
|
|
||||||
youtubedl.exec(sub.url, downloadConfig, {}, function(err, output) {
|
|
||||||
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);
|
logger.error(err.stderr ? err.stderr : err.message);
|
||||||
if (err.stderr.includes('This video is unavailable')) {
|
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/);
|
const outputs = err.stdout.split(/\r\n|\r|\n/);
|
||||||
for (let i = 0; i < outputs.length; i++) {
|
for (let i = 0; i < outputs.length; i++) {
|
||||||
const output = JSON.parse(outputs[i]);
|
const output = JSON.parse(outputs[i]);
|
||||||
handleOutputJSON(sub, sub_db, output, i === 0)
|
await handleOutputJSON(sub, output, i === 0, multiUserMode)
|
||||||
if (err.stderr.includes(output['id']) && archive_path) {
|
if (err.stderr.includes(output['id']) && archive_path) {
|
||||||
// we found a video that errored! add it to the archive to prevent future errors
|
// we found a video that errored! add it to the archive to prevent future errors
|
||||||
fs.appendFileSync(archive_path, output['id']);
|
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) {
|
||||||
@@ -302,6 +303,7 @@ async function getVideosForSub(sub, user_uid = null) {
|
|||||||
if (output.length === 0 || (output.length === 1 && output[0] === '')) {
|
if (output.length === 0 || (output.length === 1 && output[0] === '')) {
|
||||||
logger.verbose('No additional videos to download for ' + sub.name);
|
logger.verbose('No additional videos to download for ' + sub.name);
|
||||||
resolve(true);
|
resolve(true);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
for (let i = 0; i < output.length; i++) {
|
for (let i = 0; i < output.length; i++) {
|
||||||
let output_json = null;
|
let output_json = null;
|
||||||
@@ -315,19 +317,106 @@ async function getVideosForSub(sub, user_uid = null) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const reset_videos = i === 0;
|
const reset_videos = i === 0;
|
||||||
handleOutputJSON(sub, sub_db, output_json, reset_videos);
|
await handleOutputJSON(sub, output_json, multiUserMode, preimported_file_paths, reset_videos);
|
||||||
|
|
||||||
// TODO: Potentially store downloaded files in db?
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (config_api.getConfigItem('ytdl_subscriptions_redownload_fresh_uploads')) {
|
||||||
|
await setFreshUploads(sub, user_uid);
|
||||||
|
checkVideosForFreshUploads(sub, user_uid);
|
||||||
|
}
|
||||||
|
|
||||||
resolve(true);
|
resolve(true);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
}, err => {
|
||||||
|
logger.error(err);
|
||||||
|
updateSubscriptionProperty(sub, {downloading: false}, user_uid);
|
||||||
|
clearInterval(preregister_check);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleOutputJSON(sub, sub_db, output_json, reset_videos = false) {
|
async function generateArgsForSubscription(sub, user_uid, redownload = false, desired_path = null) {
|
||||||
|
// get basePath
|
||||||
|
let basePath = null;
|
||||||
|
if (user_uid)
|
||||||
|
basePath = path.join(config_api.getConfigItem('ytdl_users_base_path'), user_uid, 'subscriptions');
|
||||||
|
else
|
||||||
|
basePath = config_api.getConfigItem('ytdl_subscriptions_base_path');
|
||||||
|
|
||||||
|
const useArchive = config_api.getConfigItem('ytdl_use_youtubedl_archive');
|
||||||
|
|
||||||
|
let appendedBasePath = getAppendedBasePath(sub, basePath);
|
||||||
|
|
||||||
|
let fullOutput = `${appendedBasePath}/%(title)s.%(ext)s`;
|
||||||
|
if (desired_path) {
|
||||||
|
fullOutput = `${desired_path}.%(ext)s`;
|
||||||
|
} else if (sub.custom_output) {
|
||||||
|
fullOutput = `${appendedBasePath}/${sub.custom_output}.%(ext)s`;
|
||||||
|
}
|
||||||
|
|
||||||
|
let downloadConfig = ['-o', fullOutput, !redownload ? '-ciw' : '-ci', '--write-info-json', '--print-json'];
|
||||||
|
|
||||||
|
let qualityPath = null;
|
||||||
|
if (sub.type && sub.type === 'audio') {
|
||||||
|
qualityPath = ['-f', 'bestaudio']
|
||||||
|
qualityPath.push('-x');
|
||||||
|
qualityPath.push('--audio-format', 'mp3');
|
||||||
|
} else {
|
||||||
|
if (!sub.maxQuality || sub.maxQuality === 'best') qualityPath = ['-f', 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/mp4'];
|
||||||
|
else qualityPath = ['-f', `bestvideo[height<=${sub.maxQuality}]+bestaudio/best[height<=${sub.maxQuality}]`, '--merge-output-format', 'mp4'];
|
||||||
|
}
|
||||||
|
|
||||||
|
downloadConfig.push(...qualityPath)
|
||||||
|
|
||||||
|
if (sub.custom_args) {
|
||||||
|
customArgsArray = sub.custom_args.split(',,');
|
||||||
|
if (customArgsArray.indexOf('-f') !== -1) {
|
||||||
|
// if custom args has a custom quality, replce the original quality with that of custom args
|
||||||
|
const original_output_index = downloadConfig.indexOf('-f');
|
||||||
|
downloadConfig.splice(original_output_index, 2);
|
||||||
|
}
|
||||||
|
downloadConfig.push(...customArgsArray);
|
||||||
|
}
|
||||||
|
|
||||||
|
let archive_dir = null;
|
||||||
|
let archive_path = null;
|
||||||
|
|
||||||
|
if (useArchive && !redownload) {
|
||||||
|
if (sub.archive) {
|
||||||
|
archive_dir = sub.archive;
|
||||||
|
archive_path = path.join(archive_dir, 'archive.txt')
|
||||||
|
}
|
||||||
|
downloadConfig.push('--download-archive', archive_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
// if streaming only mode, just get the list of videos
|
||||||
if (sub.streamingOnly) {
|
if (sub.streamingOnly) {
|
||||||
|
downloadConfig = ['-f', 'best', '--dump-json'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sub.timerange && !redownload) {
|
||||||
|
downloadConfig.push('--dateafter', sub.timerange);
|
||||||
|
}
|
||||||
|
|
||||||
|
let useCookies = config_api.getConfigItem('ytdl_use_cookies');
|
||||||
|
if (useCookies) {
|
||||||
|
if (await fs.pathExists(path.join(__dirname, 'appdata', 'cookies.txt'))) {
|
||||||
|
downloadConfig.push('--cookies', path.join('appdata', 'cookies.txt'));
|
||||||
|
} else {
|
||||||
|
logger.warn('Cookies file could not be found. You can either upload one, or disable \'use cookies\' in the Advanced tab in the settings.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config_api.getConfigItem('ytdl_include_thumbnail')) {
|
||||||
|
downloadConfig.push('--write-thumbnail');
|
||||||
|
}
|
||||||
|
|
||||||
|
return downloadConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleOutputJSON(sub, output_json, multiUserMode = null, reset_videos = false) {
|
||||||
|
// TODO: remove streaming only mode
|
||||||
|
if (false && sub.streamingOnly) {
|
||||||
if (reset_videos) {
|
if (reset_videos) {
|
||||||
sub_db.assign({videos: []}).write();
|
sub_db.assign({videos: []}).write();
|
||||||
}
|
}
|
||||||
@@ -337,28 +426,110 @@ function handleOutputJSON(sub, sub_db, output_json, reset_videos = false) {
|
|||||||
|
|
||||||
// add to db
|
// add to db
|
||||||
sub_db.get('videos').push(output_json).write();
|
sub_db.get('videos').push(output_json).write();
|
||||||
|
} else {
|
||||||
|
path_object = path.parse(output_json['_filename']);
|
||||||
|
const path_string = path.format(path_object);
|
||||||
|
|
||||||
|
const file_exists = await db_api.getRecord('files', {path: path_string, sub_id: sub.id});
|
||||||
|
if (file_exists) {
|
||||||
|
// TODO: fix issue where files of different paths due to custom path get downloaded multiple times
|
||||||
|
// file already exists in DB, return early to avoid reseting the download date
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await db_api.registerFileDB2(output_json['_filename'], sub.type, sub.user_uid, null, sub.id);
|
||||||
|
|
||||||
|
const url = output_json['webpage_url'];
|
||||||
|
if (sub.type === 'video' && url.includes('twitch.tv/videos/') && url.split('twitch.tv/videos/').length > 1
|
||||||
|
&& config_api.getConfigItem('ytdl_use_twitch_api') && config_api.getConfigItem('ytdl_twitch_auto_download_chat')) {
|
||||||
|
const file_name = path.basename(output_json['_filename']);
|
||||||
|
const id = file_name.substring(0, file_name.length-4);
|
||||||
|
let vodId = url.split('twitch.tv/videos/')[1];
|
||||||
|
vodId = vodId.split('?')[0];
|
||||||
|
twitch_api.downloadTwitchChatByVODID(vodId, id, sub.type, multiUserMode.user, sub);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getAllSubscriptions(user_uid = null) {
|
async function getSubscriptions(user_uid = null) {
|
||||||
if (user_uid)
|
return await db_api.getRecords('subscriptions', {user_uid: user_uid});
|
||||||
return users_db.get('users').find({uid: user_uid}).get('subscriptions').value();
|
|
||||||
else
|
|
||||||
return db.get('subscriptions').value();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSubscription(subID, user_uid = null) {
|
async function getAllSubscriptions() {
|
||||||
if (user_uid)
|
const all_subs = await db_api.getRecords('subscriptions');
|
||||||
return users_db.get('users').find({uid: user_uid}).get('subscriptions').find({id: subID}).value();
|
const multiUserMode = config_api.getConfigItem('ytdl_multi_user_mode');
|
||||||
else
|
return all_subs.filter(sub => !!(sub.user_uid) === multiUserMode);
|
||||||
return db.get('subscriptions').find({id: subID}).value();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function subExists(subID, user_uid = null) {
|
async function getSubscription(subID) {
|
||||||
if (user_uid)
|
return await db_api.getRecord('subscriptions', {id: subID});
|
||||||
return !!users_db.get('users').find({uid: user_uid}).get('subscriptions').find({id: subID}).value();
|
}
|
||||||
else
|
|
||||||
return !!db.get('subscriptions').find({id: subID}).value();
|
async function getSubscriptionByName(subName, user_uid = null) {
|
||||||
|
return await db_api.getRecord('subscriptions', {name: subName, user_uid: user_uid});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateSubscription(sub, user_uid = null) {
|
||||||
|
await db_api.updateRecord('subscriptions', {id: sub.id}, sub);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateSubscriptionPropertyMultiple(subs, assignment_obj) {
|
||||||
|
subs.forEach(async sub => {
|
||||||
|
await updateSubscriptionProperty(sub, assignment_obj, sub.user_uid);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateSubscriptionProperty(sub, assignment_obj, user_uid = null) {
|
||||||
|
// TODO: combine with updateSubscription
|
||||||
|
await db_api.updateRecord('subscriptions', {id: sub.id}, assignment_obj);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function setFreshUploads(sub, user_uid) {
|
||||||
|
const current_date = new Date().toISOString().split('T')[0].replace(/-/g, '');
|
||||||
|
sub.videos.forEach(async video => {
|
||||||
|
if (current_date === video['upload_date'].replace(/-/g, '')) {
|
||||||
|
// set upload as fresh
|
||||||
|
const video_uid = video['uid'];
|
||||||
|
await db_api.setVideoProperty(video_uid, {'fresh_upload': true}, user_uid, sub['id']);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkVideosForFreshUploads(sub, user_uid) {
|
||||||
|
const current_date = new Date().toISOString().split('T')[0].replace(/-/g, '');
|
||||||
|
sub.videos.forEach(async video => {
|
||||||
|
if (video['fresh_upload'] && current_date > video['upload_date'].replace(/-/g, '')) {
|
||||||
|
await checkVideoIfBetterExists(video, sub, user_uid)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkVideoIfBetterExists(file_obj, sub, user_uid) {
|
||||||
|
const new_path = file_obj['path'].substring(0, file_obj['path'].length - 4);
|
||||||
|
const downloadConfig = await generateArgsForSubscription(sub, user_uid, true, new_path);
|
||||||
|
logger.verbose(`Checking if a better version of the fresh upload ${file_obj['id']} exists.`);
|
||||||
|
// simulate a download to verify that a better version exists
|
||||||
|
youtubedl.getInfo(file_obj['url'], downloadConfig, async (err, output) => {
|
||||||
|
if (err) {
|
||||||
|
// video is not available anymore for whatever reason
|
||||||
|
} else if (output) {
|
||||||
|
const metric_to_compare = sub.type === 'audio' ? 'abr' : 'height';
|
||||||
|
if (output[metric_to_compare] > file_obj[metric_to_compare]) {
|
||||||
|
// download new video as the simulated one is better
|
||||||
|
youtubedl.exec(file_obj['url'], downloadConfig, {maxBuffer: Infinity}, async (err, output) => {
|
||||||
|
if (err) {
|
||||||
|
logger.verbose(`Failed to download better version of video ${file_obj['id']}`);
|
||||||
|
} else if (output) {
|
||||||
|
logger.verbose(`Successfully upgraded video ${file_obj['id']}'s ${metric_to_compare} from ${file_obj[metric_to_compare]} to ${output[metric_to_compare]}`);
|
||||||
|
await db_api.setVideoProperty(file_obj['uid'], {[metric_to_compare]: output[metric_to_compare]}, user_uid, sub['id']);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
await db_api.setVideoProperty(file_obj['uid'], {'fresh_upload': false}, user_uid, sub['id']);
|
||||||
}
|
}
|
||||||
|
|
||||||
// helper functions
|
// helper functions
|
||||||
@@ -368,56 +539,17 @@ function getAppendedBasePath(sub, base_path) {
|
|||||||
return path.join(base_path, (sub.isPlaylist ? 'playlists/' : 'channels/'), sub.name);
|
return path.join(base_path, (sub.isPlaylist ? 'playlists/' : 'channels/'), sub.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
// https://stackoverflow.com/a/32197381/8088021
|
|
||||||
const deleteFolderRecursive = function(folder_to_delete) {
|
|
||||||
if (fs.existsSync(folder_to_delete)) {
|
|
||||||
fs.readdirSync(folder_to_delete).forEach((file, index) => {
|
|
||||||
const curPath = path.join(folder_to_delete, file);
|
|
||||||
if (fs.lstatSync(curPath).isDirectory()) { // recurse
|
|
||||||
deleteFolderRecursive(curPath);
|
|
||||||
} else { // delete file
|
|
||||||
fs.unlinkSync(curPath);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
fs.rmdirSync(folder_to_delete);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
function removeIDFromArchive(archive_path, id) {
|
|
||||||
let data = fs.readFileSync(archive_path, {encoding: 'utf-8'});
|
|
||||||
if (!data) {
|
|
||||||
logger.error('Archive could not be found.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let dataArray = data.split('\n'); // convert file data in an array
|
|
||||||
const searchKeyword = id; // we are looking for a line, contains, key word id in the file
|
|
||||||
let lastIndex = -1; // let say, we have not found the keyword
|
|
||||||
|
|
||||||
for (let index=0; index<dataArray.length; index++) {
|
|
||||||
if (dataArray[index].includes(searchKeyword)) { // check if a line contains the id keyword
|
|
||||||
lastIndex = index; // found a line includes a id keyword
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const line = dataArray.splice(lastIndex, 1); // remove the keyword id from the data Array
|
|
||||||
|
|
||||||
// UPDATE FILE WITH NEW DATA
|
|
||||||
const updatedData = dataArray.join('\n');
|
|
||||||
fs.writeFileSync(archive_path, updatedData);
|
|
||||||
if (line) return line;
|
|
||||||
if (err) throw err;
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
getSubscription : getSubscription,
|
getSubscription : getSubscription,
|
||||||
|
getSubscriptionByName : getSubscriptionByName,
|
||||||
|
getSubscriptions : getSubscriptions,
|
||||||
getAllSubscriptions : getAllSubscriptions,
|
getAllSubscriptions : getAllSubscriptions,
|
||||||
|
updateSubscription : updateSubscription,
|
||||||
subscribe : subscribe,
|
subscribe : subscribe,
|
||||||
unsubscribe : unsubscribe,
|
unsubscribe : unsubscribe,
|
||||||
deleteSubscriptionFile : deleteSubscriptionFile,
|
deleteSubscriptionFile : deleteSubscriptionFile,
|
||||||
getVideosForSub : getVideosForSub,
|
getVideosForSub : getVideosForSub,
|
||||||
removeIDFromArchive : removeIDFromArchive,
|
|
||||||
setLogger : setLogger,
|
setLogger : setLogger,
|
||||||
initialize : initialize
|
initialize : initialize,
|
||||||
|
updateSubscriptionPropertyMultiple : updateSubscriptionPropertyMultiple
|
||||||
}
|
}
|
||||||
|
|||||||
290
backend/test/tests.js
Normal file
290
backend/test/tests.js
Normal file
@@ -0,0 +1,290 @@
|
|||||||
|
var assert = require('assert');
|
||||||
|
const low = require('lowdb')
|
||||||
|
var winston = require('winston');
|
||||||
|
|
||||||
|
process.chdir('./backend')
|
||||||
|
|
||||||
|
const FileSync = require('lowdb/adapters/FileSync');
|
||||||
|
|
||||||
|
const adapter = new FileSync('./appdata/db.json');
|
||||||
|
const db = low(adapter)
|
||||||
|
|
||||||
|
const users_adapter = new FileSync('./appdata/users.json');
|
||||||
|
const users_db = low(users_adapter);
|
||||||
|
|
||||||
|
const defaultFormat = winston.format.printf(({ level, message, label, timestamp }) => {
|
||||||
|
return `${timestamp} ${level.toUpperCase()}: ${message}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
let debugMode = process.env.YTDL_MODE === 'debug';
|
||||||
|
|
||||||
|
const logger = winston.createLogger({
|
||||||
|
level: 'info',
|
||||||
|
format: winston.format.combine(winston.format.timestamp(), defaultFormat),
|
||||||
|
defaultMeta: {},
|
||||||
|
transports: [
|
||||||
|
//
|
||||||
|
// - Write to all logs with level `info` and below to `combined.log`
|
||||||
|
// - Write all logs error (and below) to `error.log`.
|
||||||
|
//
|
||||||
|
new winston.transports.File({ filename: 'appdata/logs/error.log', level: 'error' }),
|
||||||
|
new winston.transports.File({ filename: 'appdata/logs/combined.log' }),
|
||||||
|
new winston.transports.Console({level: 'debug', name: 'console'})
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
var auth_api = require('../authentication/auth');
|
||||||
|
var db_api = require('../db');
|
||||||
|
const utils = require('../utils');
|
||||||
|
const subscriptions_api = require('../subscriptions');
|
||||||
|
const fs = require('fs-extra');
|
||||||
|
const { uuid } = require('uuidv4');
|
||||||
|
|
||||||
|
db_api.initialize(db, users_db, logger);
|
||||||
|
|
||||||
|
|
||||||
|
describe('Database', async function() {
|
||||||
|
describe('Import', async function() {
|
||||||
|
it('Migrate', async function() {
|
||||||
|
await db_api.connectToDB();
|
||||||
|
await db_api.removeAllRecords();
|
||||||
|
const success = await db_api.importJSONToDB(db.value(), users_db.value());
|
||||||
|
assert(success);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Transfer to remote', async function() {
|
||||||
|
await db_api.removeAllRecords('test');
|
||||||
|
await db_api.insertRecordIntoTable('test', {test: 'test'});
|
||||||
|
|
||||||
|
await db_api.transferDB(true);
|
||||||
|
const success = await db_api.getRecord('test', {test: 'test'});
|
||||||
|
assert(success);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Transfer to local', async function() {
|
||||||
|
await db_api.connectToDB();
|
||||||
|
await db_api.removeAllRecords('test');
|
||||||
|
await db_api.insertRecordIntoTable('test', {test: 'test'});
|
||||||
|
|
||||||
|
await db_api.transferDB(false);
|
||||||
|
const success = await db_api.getRecord('test', {test: 'test'});
|
||||||
|
assert(success);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Export', function() {
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
describe('Basic functions', async function() {
|
||||||
|
beforeEach(async function() {
|
||||||
|
await db_api.connectToDB();
|
||||||
|
await db_api.removeAllRecords('test');
|
||||||
|
});
|
||||||
|
it('Add and read record', async function() {
|
||||||
|
await db_api.insertRecordIntoTable('test', {test_add: 'test', test_undefined: undefined, test_null: undefined});
|
||||||
|
const added_record = await db_api.getRecord('test', {test_add: 'test', test_undefined: undefined, test_null: null});
|
||||||
|
assert(added_record['test_add'] === 'test');
|
||||||
|
await db_api.removeRecord('test', {test_add: 'test'});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Update record', async function() {
|
||||||
|
await db_api.insertRecordIntoTable('test', {test_update: 'test'});
|
||||||
|
await db_api.updateRecord('test', {test_update: 'test'}, {added_field: true});
|
||||||
|
const updated_record = await db_api.getRecord('test', {test_update: 'test'});
|
||||||
|
assert(updated_record['added_field']);
|
||||||
|
await db_api.removeRecord('test', {test_update: 'test'});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Remove record', async function() {
|
||||||
|
await db_api.insertRecordIntoTable('test', {test_remove: 'test'});
|
||||||
|
const delete_succeeded = await db_api.removeRecord('test', {test_remove: 'test'});
|
||||||
|
assert(delete_succeeded);
|
||||||
|
const deleted_record = await db_api.getRecord('test', {test_remove: 'test'});
|
||||||
|
assert(!deleted_record);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Push to record array', async function() {
|
||||||
|
await db_api.insertRecordIntoTable('test', {test: 'test', test_array: []});
|
||||||
|
await db_api.pushToRecordsArray('test', {test: 'test'}, 'test_array', 'test_item');
|
||||||
|
const record = await db_api.getRecord('test', {test: 'test'});
|
||||||
|
assert(record);
|
||||||
|
assert(record['test_array'].length === 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Pull from record array', async function() {
|
||||||
|
await db_api.insertRecordIntoTable('test', {test: 'test', test_array: ['test_item']});
|
||||||
|
await db_api.pullFromRecordsArray('test', {test: 'test'}, 'test_array', 'test_item');
|
||||||
|
const record = await db_api.getRecord('test', {test: 'test'});
|
||||||
|
assert(record);
|
||||||
|
assert(record['test_array'].length === 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Bulk add', async function() {
|
||||||
|
const NUM_RECORDS_TO_ADD = 2002; // max batch ops is 1000
|
||||||
|
const test_records = [];
|
||||||
|
for (let i = 0; i < NUM_RECORDS_TO_ADD; i++) {
|
||||||
|
test_records.push({
|
||||||
|
uid: uuid()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const succcess = await db_api.bulkInsertRecordsIntoTable('test', test_records);
|
||||||
|
|
||||||
|
const received_records = await db_api.getRecords('test');
|
||||||
|
assert(succcess && received_records && received_records.length === NUM_RECORDS_TO_ADD);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Bulk update', async function() {
|
||||||
|
// bulk add records
|
||||||
|
const NUM_RECORDS_TO_ADD = 100; // max batch ops is 1000
|
||||||
|
const test_records = [];
|
||||||
|
const update_obj = {};
|
||||||
|
for (let i = 0; i < NUM_RECORDS_TO_ADD; i++) {
|
||||||
|
const test_uid = uuid();
|
||||||
|
test_records.push({
|
||||||
|
uid: test_uid
|
||||||
|
});
|
||||||
|
update_obj[test_uid] = {added_field: true};
|
||||||
|
}
|
||||||
|
let success = await db_api.bulkInsertRecordsIntoTable('test', test_records);
|
||||||
|
assert(success);
|
||||||
|
|
||||||
|
// makes sure they are added
|
||||||
|
const received_records = await db_api.getRecords('test');
|
||||||
|
assert(received_records && received_records.length === NUM_RECORDS_TO_ADD);
|
||||||
|
|
||||||
|
success = await db_api.bulkUpdateRecords('test', 'uid', update_obj);
|
||||||
|
assert(success);
|
||||||
|
|
||||||
|
const received_updated_records = await db_api.getRecords('test');
|
||||||
|
for (let i = 0; i < received_updated_records.length; i++) {
|
||||||
|
success &= received_updated_records[i]['added_field'];
|
||||||
|
}
|
||||||
|
assert(success);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Stats', async function() {
|
||||||
|
const stats = await db_api.getDBStats();
|
||||||
|
assert(stats);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Query speed', async function() {
|
||||||
|
this.timeout(120000);
|
||||||
|
const NUM_RECORDS_TO_ADD = 300004; // max batch ops is 1000
|
||||||
|
const test_records = [];
|
||||||
|
let random_uid = '06241f83-d1b8-4465-812c-618dfa7f2943';
|
||||||
|
for (let i = 0; i < NUM_RECORDS_TO_ADD; i++) {
|
||||||
|
const uid = uuid();
|
||||||
|
if (i === NUM_RECORDS_TO_ADD/2) random_uid = uid;
|
||||||
|
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();
|
||||||
|
let success = await db_api.bulkInsertRecordsIntoTable('test', test_records);
|
||||||
|
const insert_end = Date.now();
|
||||||
|
|
||||||
|
console.log(`Insert time: ${(insert_end - insert_start)/1000}s`);
|
||||||
|
|
||||||
|
const query_start = Date.now();
|
||||||
|
const random_record = await db_api.getRecord('test', {uid: random_uid});
|
||||||
|
const query_end = Date.now();
|
||||||
|
|
||||||
|
console.log(random_record)
|
||||||
|
|
||||||
|
console.log(`Query time: ${(query_end - query_start)/1000}s`);
|
||||||
|
|
||||||
|
success = !!random_record;
|
||||||
|
|
||||||
|
assert(success);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Multi User', async function() {
|
||||||
|
let user = null;
|
||||||
|
const user_to_test = 'admin';
|
||||||
|
const sub_to_test = 'dc834388-3454-41bf-a618-e11cb8c7de1c';
|
||||||
|
const playlist_to_test = 'ysabVZz4x';
|
||||||
|
beforeEach(async function() {
|
||||||
|
await db_api.connectToDB();
|
||||||
|
auth_api.initialize(db_api, logger);
|
||||||
|
subscriptions_api.initialize(db_api, logger);
|
||||||
|
user = await auth_api.login('admin', 'pass');
|
||||||
|
});
|
||||||
|
describe('Authentication', function() {
|
||||||
|
it('login', async function() {
|
||||||
|
assert(user);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('Video player - normal', function() {
|
||||||
|
const video_to_test = 'ebbcfffb-d6f1-4510-ad25-d1ec82e0477e';
|
||||||
|
it('Get video', async function() {
|
||||||
|
const video_obj = db_api.getVideo(video_to_test, 'admin');
|
||||||
|
assert(video_obj);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Video access - disallowed', async function() {
|
||||||
|
await db_api.setVideoProperty(video_to_test, {sharingEnabled: false}, user_to_test);
|
||||||
|
const video_obj = auth_api.getUserVideo('admin', video_to_test, true);
|
||||||
|
assert(!video_obj);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Video access - allowed', async function() {
|
||||||
|
await db_api.setVideoProperty(video_to_test, {sharingEnabled: true}, user_to_test);
|
||||||
|
const video_obj = auth_api.getUserVideo('admin', video_to_test, true);
|
||||||
|
assert(video_obj);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('Zip generators', function() {
|
||||||
|
it('Playlist zip generator', async function() {
|
||||||
|
const playlist = await db_api.getPlaylist(playlist_to_test, user_to_test);
|
||||||
|
assert(playlist);
|
||||||
|
const playlist_files_to_download = [];
|
||||||
|
for (let i = 0; i < playlist['uids'].length; i++) {
|
||||||
|
const uid = playlist['uids'][i];
|
||||||
|
const playlist_file = await db_api.getVideo(uid, user_to_test);
|
||||||
|
playlist_files_to_download.push(playlist_file);
|
||||||
|
}
|
||||||
|
const zip_path = await utils.createContainerZipFile(playlist, playlist_files_to_download);
|
||||||
|
const zip_exists = fs.pathExistsSync(zip_path);
|
||||||
|
assert(zip_exists);
|
||||||
|
if (zip_exists) fs.unlinkSync(zip_path);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Subscription zip generator', async function() {
|
||||||
|
const sub = await subscriptions_api.getSubscription(sub_to_test, user_to_test);
|
||||||
|
const sub_videos = await db_api.getRecords('files', {sub_id: sub.id});
|
||||||
|
assert(sub);
|
||||||
|
const sub_files_to_download = [];
|
||||||
|
for (let i = 0; i < sub_videos.length; i++) {
|
||||||
|
const sub_file = sub_videos[i];
|
||||||
|
sub_files_to_download.push(sub_file);
|
||||||
|
}
|
||||||
|
const zip_path = await utils.createContainerZipFile(sub, sub_files_to_download);
|
||||||
|
const zip_exists = fs.pathExistsSync(zip_path);
|
||||||
|
assert(zip_exists);
|
||||||
|
if (zip_exists) fs.unlinkSync(zip_path);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
// describe('Video player - subscription', function() {
|
||||||
|
// const sub_to_test = '';
|
||||||
|
// const video_to_test = 'ebbcfffb-d6f1-4510-ad25-d1ec82e0477e';
|
||||||
|
// it('Get video', async function() {
|
||||||
|
// const video_obj = db_api.getVideo(video_to_test, 'admin', );
|
||||||
|
// assert(video_obj);
|
||||||
|
// });
|
||||||
|
|
||||||
|
// it('Video access - disallowed', async function() {
|
||||||
|
// await db_api.setVideoProperty(video_to_test, {sharingEnabled: false}, user_to_test, sub_to_test);
|
||||||
|
// const video_obj = auth_api.getUserVideo('admin', video_to_test, true);
|
||||||
|
// assert(!video_obj);
|
||||||
|
// });
|
||||||
|
|
||||||
|
// it('Video access - allowed', async function() {
|
||||||
|
// await db_api.setVideoProperty(video_to_test, {sharingEnabled: true}, user_to_test, sub_to_test);
|
||||||
|
// const video_obj = auth_api.getUserVideo('admin', video_to_test, true);
|
||||||
|
// assert(video_obj);
|
||||||
|
// });
|
||||||
|
// });
|
||||||
|
|
||||||
|
});
|
||||||
128
backend/twitch.js
Normal file
128
backend/twitch.js
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
var moment = require('moment');
|
||||||
|
var Axios = require('axios');
|
||||||
|
var fs = require('fs-extra')
|
||||||
|
var path = require('path');
|
||||||
|
const config_api = require('./config');
|
||||||
|
|
||||||
|
async function getCommentsForVOD(clientID, vodId) {
|
||||||
|
let url = `https://api.twitch.tv/v5/videos/${vodId}/comments?content_offset_seconds=0`,
|
||||||
|
batch,
|
||||||
|
cursor;
|
||||||
|
|
||||||
|
let comments = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
do {
|
||||||
|
batch = (await Axios.get(url, {
|
||||||
|
headers: {
|
||||||
|
'Client-ID': clientID,
|
||||||
|
Accept: 'application/vnd.twitchtv.v5+json; charset=UTF-8',
|
||||||
|
'Content-Type': 'application/json; charset=UTF-8',
|
||||||
|
}
|
||||||
|
})).data;
|
||||||
|
|
||||||
|
const str = batch.comments.map(c => {
|
||||||
|
let {
|
||||||
|
created_at: msgCreated,
|
||||||
|
content_offset_seconds: timestamp,
|
||||||
|
commenter: {
|
||||||
|
name,
|
||||||
|
_id,
|
||||||
|
created_at: acctCreated
|
||||||
|
},
|
||||||
|
message: {
|
||||||
|
body: msg,
|
||||||
|
user_color: user_color
|
||||||
|
}
|
||||||
|
} = c;
|
||||||
|
|
||||||
|
const timestamp_str = 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])}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
acctCreated = moment(acctCreated).utc();
|
||||||
|
msgCreated = moment(msgCreated).utc();
|
||||||
|
|
||||||
|
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) {
|
||||||
|
let file_path = null;
|
||||||
|
|
||||||
|
if (user_uid) {
|
||||||
|
if (sub) {
|
||||||
|
file_path = path.join('users', user_uid, 'subscriptions', sub.isPlaylist ? 'playlists' : 'channels', sub.name, id + '.twitch_chat.json');
|
||||||
|
} else {
|
||||||
|
file_path = path.join('users', user_uid, type, id + '.twitch_chat.json');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (sub) {
|
||||||
|
file_path = path.join('subscriptions', sub.isPlaylist ? 'playlists' : 'channels', sub.name, id + '.twitch_chat.json');
|
||||||
|
} else {
|
||||||
|
file_path = path.join(type, id + '.twitch_chat.json');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var chat_file = null;
|
||||||
|
if (fs.existsSync(file_path)) {
|
||||||
|
chat_file = fs.readJSONSync(file_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
return chat_file;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function downloadTwitchChatByVODID(vodId, id, type, user_uid, sub) {
|
||||||
|
const twitch_api_key = config_api.getConfigItem('ytdl_twitch_api_key');
|
||||||
|
const chat = await getCommentsForVOD(twitch_api_key, vodId);
|
||||||
|
|
||||||
|
// save file if needed params are included
|
||||||
|
let file_path = null;
|
||||||
|
if (user_uid) {
|
||||||
|
if (sub) {
|
||||||
|
file_path = path.join('users', user_uid, 'subscriptions', sub.isPlaylist ? 'playlists' : 'channels', sub.name, id + '.twitch_chat.json');
|
||||||
|
} else {
|
||||||
|
file_path = path.join('users', user_uid, type, id + '.twitch_chat.json');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (sub) {
|
||||||
|
file_path = path.join('subscriptions', sub.isPlaylist ? 'playlists' : 'channels', sub.name, id + '.twitch_chat.json');
|
||||||
|
} else {
|
||||||
|
file_path = path.join(type, id + '.twitch_chat.json');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (chat) fs.writeJSONSync(file_path, chat);
|
||||||
|
|
||||||
|
return chat;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
getCommentsForVOD: getCommentsForVOD,
|
||||||
|
getTwitchChatByFileID: getTwitchChatByFileID,
|
||||||
|
downloadTwitchChatByVODID: downloadTwitchChatByVODID
|
||||||
|
}
|
||||||
397
backend/utils.js
Normal file
397
backend/utils.js
Normal file
@@ -0,0 +1,397 @@
|
|||||||
|
const fs = require('fs-extra')
|
||||||
|
const path = require('path')
|
||||||
|
const config_api = require('./config');
|
||||||
|
const archiver = require('archiver');
|
||||||
|
|
||||||
|
const is_windows = process.platform === 'win32';
|
||||||
|
|
||||||
|
// replaces .webm with appropriate extension
|
||||||
|
function getTrueFileName(unfixed_path, type) {
|
||||||
|
let fixed_path = unfixed_path;
|
||||||
|
|
||||||
|
const new_ext = (type === 'audio' ? 'mp3' : 'mp4');
|
||||||
|
let unfixed_parts = unfixed_path.split('.');
|
||||||
|
const old_ext = unfixed_parts[unfixed_parts.length-1];
|
||||||
|
|
||||||
|
|
||||||
|
if (old_ext !== new_ext) {
|
||||||
|
unfixed_parts[unfixed_parts.length-1] = new_ext;
|
||||||
|
fixed_path = unfixed_parts.join('.');
|
||||||
|
}
|
||||||
|
return fixed_path;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getDownloadedFilesByType(basePath, type, full_metadata = false) {
|
||||||
|
// return empty array if the path doesn't exist
|
||||||
|
if (!(await fs.pathExists(basePath))) return [];
|
||||||
|
|
||||||
|
let files = [];
|
||||||
|
const ext = type === 'audio' ? 'mp3' : 'mp4';
|
||||||
|
var located_files = await recFindByExt(basePath, ext);
|
||||||
|
for (let i = 0; i < located_files.length; i++) {
|
||||||
|
let file = located_files[i];
|
||||||
|
var file_path = file.substring(basePath.includes('\\') ? basePath.length+1 : basePath.length, file.length);
|
||||||
|
|
||||||
|
var stats = await fs.stat(file);
|
||||||
|
|
||||||
|
var id = file_path.substring(0, file_path.length-4);
|
||||||
|
var jsonobj = await getJSONByType(type, id, basePath);
|
||||||
|
if (!jsonobj) continue;
|
||||||
|
if (full_metadata) {
|
||||||
|
jsonobj['id'] = id;
|
||||||
|
files.push(jsonobj);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
var upload_date = jsonobj.upload_date;
|
||||||
|
upload_date = upload_date ? `${upload_date.substring(0, 4)}-${upload_date.substring(4, 6)}-${upload_date.substring(6, 8)}` : null;
|
||||||
|
|
||||||
|
var isaudio = type === 'audio';
|
||||||
|
var file_obj = new File(id, jsonobj.title, jsonobj.thumbnail, isaudio, jsonobj.duration, jsonobj.webpage_url, jsonobj.uploader,
|
||||||
|
stats.size, file, upload_date, jsonobj.description, jsonobj.view_count, jsonobj.height, jsonobj.abr);
|
||||||
|
files.push(file_obj);
|
||||||
|
}
|
||||||
|
return files;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createContainerZipFile(container_obj, container_file_objs) {
|
||||||
|
const container_files_to_download = [];
|
||||||
|
for (let i = 0; i < container_file_objs.length; i++) {
|
||||||
|
const container_file_obj = container_file_objs[i];
|
||||||
|
container_files_to_download.push(container_file_obj.path);
|
||||||
|
}
|
||||||
|
return await createZipFile(path.join('appdata', container_obj.name + '.zip'), container_files_to_download);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createZipFile(zip_file_path, file_paths) {
|
||||||
|
let output = fs.createWriteStream(zip_file_path);
|
||||||
|
|
||||||
|
var archive = archiver('zip', {
|
||||||
|
gzip: true,
|
||||||
|
zlib: { level: 9 } // Sets the compression level.
|
||||||
|
});
|
||||||
|
|
||||||
|
archive.on('error', function(err) {
|
||||||
|
logger.error(err);
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
|
|
||||||
|
// pipe archive data to the output file
|
||||||
|
archive.pipe(output);
|
||||||
|
|
||||||
|
for (let file_path of file_paths) {
|
||||||
|
const file_name = path.parse(file_path).base;
|
||||||
|
archive.file(file_path, {name: file_name})
|
||||||
|
}
|
||||||
|
|
||||||
|
await archive.finalize();
|
||||||
|
|
||||||
|
// wait a tiny bit for the zip to reload in fs
|
||||||
|
await wait(100);
|
||||||
|
return zip_file_path;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getJSONMp4(name, customPath, openReadPerms = false) {
|
||||||
|
var obj = null; // output
|
||||||
|
if (!customPath) customPath = config_api.getConfigItem('ytdl_video_folder_path');
|
||||||
|
var jsonPath = path.join(customPath, name + ".info.json");
|
||||||
|
var alternateJsonPath = path.join(customPath, name + ".mp4.info.json");
|
||||||
|
if (fs.existsSync(jsonPath))
|
||||||
|
{
|
||||||
|
obj = JSON.parse(fs.readFileSync(jsonPath, 'utf8'));
|
||||||
|
} else if (fs.existsSync(alternateJsonPath)) {
|
||||||
|
obj = JSON.parse(fs.readFileSync(alternateJsonPath, 'utf8'));
|
||||||
|
}
|
||||||
|
else obj = 0;
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getJSONMp3(name, customPath, openReadPerms = false) {
|
||||||
|
var obj = null;
|
||||||
|
if (!customPath) customPath = config_api.getConfigItem('ytdl_audio_folder_path');
|
||||||
|
var jsonPath = path.join(customPath, name + ".info.json");
|
||||||
|
var alternateJsonPath = path.join(customPath, name + ".mp3.info.json");
|
||||||
|
if (fs.existsSync(jsonPath)) {
|
||||||
|
obj = JSON.parse(fs.readFileSync(jsonPath, 'utf8'));
|
||||||
|
}
|
||||||
|
else if (fs.existsSync(alternateJsonPath)) {
|
||||||
|
obj = JSON.parse(fs.readFileSync(alternateJsonPath, 'utf8'));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
obj = 0;
|
||||||
|
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getJSON(file_path, type) {
|
||||||
|
const ext = type === 'audio' ? '.mp3' : '.mp4';
|
||||||
|
let obj = null;
|
||||||
|
var jsonPath = removeFileExtension(file_path) + '.info.json';
|
||||||
|
var alternateJsonPath = removeFileExtension(file_path) + `${ext}.info.json`;
|
||||||
|
if (fs.existsSync(jsonPath))
|
||||||
|
{
|
||||||
|
obj = JSON.parse(fs.readFileSync(jsonPath, 'utf8'));
|
||||||
|
} else if (fs.existsSync(alternateJsonPath)) {
|
||||||
|
obj = JSON.parse(fs.readFileSync(alternateJsonPath, 'utf8'));
|
||||||
|
}
|
||||||
|
else obj = 0;
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getJSONByType(type, name, customPath, openReadPerms = false) {
|
||||||
|
return type === 'audio' ? getJSONMp3(name, customPath, openReadPerms) : getJSONMp4(name, customPath, openReadPerms)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDownloadedThumbnail(name, type, customPath = null) {
|
||||||
|
if (!customPath) customPath = type === 'audio' ? config_api.getConfigItem('ytdl_audio_folder_path') : config_api.getConfigItem('ytdl_video_folder_path');
|
||||||
|
|
||||||
|
let jpgPath = path.join(customPath, name + '.jpg');
|
||||||
|
let webpPath = path.join(customPath, name + '.webp');
|
||||||
|
let pngPath = path.join(customPath, name + '.png');
|
||||||
|
|
||||||
|
if (fs.existsSync(jpgPath))
|
||||||
|
return jpgPath;
|
||||||
|
else if (fs.existsSync(webpPath))
|
||||||
|
return webpPath;
|
||||||
|
else if (fs.existsSync(pngPath))
|
||||||
|
return pngPath;
|
||||||
|
else
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDownloadedThumbnail2(file_path, type) {
|
||||||
|
const file_path_no_extension = removeFileExtension(file_path);
|
||||||
|
|
||||||
|
let jpgPath = file_path_no_extension + '.jpg';
|
||||||
|
let webpPath = file_path_no_extension + '.webp';
|
||||||
|
let pngPath = file_path_no_extension + '.png';
|
||||||
|
|
||||||
|
if (fs.existsSync(jpgPath))
|
||||||
|
return jpgPath;
|
||||||
|
else if (fs.existsSync(webpPath))
|
||||||
|
return webpPath;
|
||||||
|
else if (fs.existsSync(pngPath))
|
||||||
|
return pngPath;
|
||||||
|
else
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getExpectedFileSize(input_info_jsons) {
|
||||||
|
// treat single videos as arrays to have the file sizes checked/added to. makes the code cleaner
|
||||||
|
const info_jsons = Array.isArray(input_info_jsons) ? input_info_jsons : [input_info_jsons];
|
||||||
|
|
||||||
|
let expected_filesize = 0;
|
||||||
|
info_jsons.forEach(info_json => {
|
||||||
|
if (info_json['filesize']) {
|
||||||
|
expected_filesize += info_json['filesize'];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const formats = info_json['format_id'].split('+');
|
||||||
|
let individual_expected_filesize = 0;
|
||||||
|
formats.forEach(format_id => {
|
||||||
|
info_json.formats.forEach(available_format => {
|
||||||
|
if (available_format.format_id === format_id && available_format.filesize) {
|
||||||
|
individual_expected_filesize += available_format.filesize;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
expected_filesize += individual_expected_filesize;
|
||||||
|
});
|
||||||
|
|
||||||
|
return expected_filesize;
|
||||||
|
}
|
||||||
|
|
||||||
|
function fixVideoMetadataPerms(name, type, customPath = null) {
|
||||||
|
if (is_windows) return;
|
||||||
|
if (!customPath) customPath = type === 'audio' ? config_api.getConfigItem('ytdl_audio_folder_path')
|
||||||
|
: config_api.getConfigItem('ytdl_video_folder_path');
|
||||||
|
|
||||||
|
const ext = type === 'audio' ? '.mp3' : '.mp4';
|
||||||
|
|
||||||
|
const files_to_fix = [
|
||||||
|
// JSONs
|
||||||
|
path.join(customPath, name + '.info.json'),
|
||||||
|
path.join(customPath, name + ext + '.info.json'),
|
||||||
|
// Thumbnails
|
||||||
|
path.join(customPath, name + '.webp'),
|
||||||
|
path.join(customPath, name + '.jpg')
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const file of files_to_fix) {
|
||||||
|
if (!fs.existsSync(file)) continue;
|
||||||
|
fs.chmodSync(file, 0o644);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function fixVideoMetadataPerms2(file_path, type) {
|
||||||
|
if (is_windows) return;
|
||||||
|
|
||||||
|
const ext = type === 'audio' ? '.mp3' : '.mp4';
|
||||||
|
|
||||||
|
const file_path_no_extension = removeFileExtension(file_path);
|
||||||
|
|
||||||
|
const files_to_fix = [
|
||||||
|
// JSONs
|
||||||
|
file_path_no_extension + '.info.json',
|
||||||
|
file_path_no_extension + ext + '.info.json',
|
||||||
|
// Thumbnails
|
||||||
|
file_path_no_extension + '.webp',
|
||||||
|
file_path_no_extension + '.jpg'
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const file of files_to_fix) {
|
||||||
|
if (!fs.existsSync(file)) continue;
|
||||||
|
fs.chmodSync(file, 0o644);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteJSONFile(name, type, customPath = null) {
|
||||||
|
if (!customPath) customPath = type === 'audio' ? config_api.getConfigItem('ytdl_audio_folder_path')
|
||||||
|
: config_api.getConfigItem('ytdl_video_folder_path');
|
||||||
|
|
||||||
|
const ext = type === 'audio' ? '.mp3' : '.mp4';
|
||||||
|
let json_path = path.join(customPath, name + '.info.json');
|
||||||
|
let alternate_json_path = path.join(customPath, name + ext + '.info.json');
|
||||||
|
|
||||||
|
if (fs.existsSync(json_path)) fs.unlinkSync(json_path);
|
||||||
|
if (fs.existsSync(alternate_json_path)) fs.unlinkSync(alternate_json_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteJSONFile2(file_path, type) {
|
||||||
|
const ext = type === 'audio' ? '.mp3' : '.mp4';
|
||||||
|
|
||||||
|
const file_path_no_extension = removeFileExtension(file_path);
|
||||||
|
|
||||||
|
let json_path = file_path_no_extension + '.info.json';
|
||||||
|
let alternate_json_path = file_path_no_extension + ext + '.info.json';
|
||||||
|
|
||||||
|
if (fs.existsSync(json_path)) fs.unlinkSync(json_path);
|
||||||
|
if (fs.existsSync(alternate_json_path)) fs.unlinkSync(alternate_json_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function removeIDFromArchive(archive_path, id) {
|
||||||
|
let data = await fs.readFile(archive_path, {encoding: 'utf-8'});
|
||||||
|
if (!data) {
|
||||||
|
logger.error('Archive could not be found.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let dataArray = data.split('\n'); // convert file data in an array
|
||||||
|
const searchKeyword = id; // we are looking for a line, contains, key word id in the file
|
||||||
|
let lastIndex = -1; // let say, we have not found the keyword
|
||||||
|
|
||||||
|
for (let index=0; index<dataArray.length; index++) {
|
||||||
|
if (dataArray[index].includes(searchKeyword)) { // check if a line contains the id keyword
|
||||||
|
lastIndex = index; // found a line includes a id keyword
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const line = dataArray.splice(lastIndex, 1); // remove the keyword id from the data Array
|
||||||
|
|
||||||
|
// UPDATE FILE WITH NEW DATA
|
||||||
|
const updatedData = dataArray.join('\n');
|
||||||
|
await fs.writeFile(archive_path, updatedData);
|
||||||
|
if (line) return line;
|
||||||
|
if (err) throw err;
|
||||||
|
}
|
||||||
|
|
||||||
|
function durationStringToNumber(dur_str) {
|
||||||
|
if (typeof dur_str === 'number') return dur_str;
|
||||||
|
let num_sum = 0;
|
||||||
|
const dur_str_parts = dur_str.split(':');
|
||||||
|
for (let i = dur_str_parts.length-1; i >= 0; i--) {
|
||||||
|
num_sum += parseInt(dur_str_parts[i])*(60**(dur_str_parts.length-1-i));
|
||||||
|
}
|
||||||
|
return num_sum;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMatchingCategoryFiles(category, files) {
|
||||||
|
return files && files.filter(file => file.category && file.category.uid === category.uid);
|
||||||
|
}
|
||||||
|
|
||||||
|
function addUIDsToCategory(category, files) {
|
||||||
|
const files_that_match = getMatchingCategoryFiles(category, files);
|
||||||
|
category['uids'] = files_that_match.map(file => file.uid);
|
||||||
|
return files_that_match;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function recFindByExt(base,ext,files,result)
|
||||||
|
{
|
||||||
|
files = files || (await fs.readdir(base))
|
||||||
|
result = result || []
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
var newbase = path.join(base,file)
|
||||||
|
if ( (await fs.stat(newbase)).isDirectory() )
|
||||||
|
{
|
||||||
|
result = await recFindByExt(newbase,ext,await fs.readdir(newbase),result)
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if ( file.substr(-1*(ext.length+1)) == '.' + ext )
|
||||||
|
{
|
||||||
|
result.push(newbase)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeFileExtension(filename) {
|
||||||
|
const filename_parts = filename.split('.');
|
||||||
|
filename_parts.splice(filename_parts.length - 1);
|
||||||
|
return filename_parts.join('.');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* setTimeout, but its a promise.
|
||||||
|
* @param {number} ms
|
||||||
|
*/
|
||||||
|
async function wait(ms) {
|
||||||
|
await new Promise(resolve => {
|
||||||
|
setTimeout(resolve, ms);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// objects
|
||||||
|
|
||||||
|
function File(id, title, thumbnailURL, isAudio, duration, url, uploader, size, path, upload_date, description, view_count, height, abr) {
|
||||||
|
this.id = id;
|
||||||
|
this.title = title;
|
||||||
|
this.thumbnailURL = thumbnailURL;
|
||||||
|
this.isAudio = isAudio;
|
||||||
|
this.duration = duration;
|
||||||
|
this.url = url;
|
||||||
|
this.uploader = uploader;
|
||||||
|
this.size = size;
|
||||||
|
this.path = path;
|
||||||
|
this.upload_date = upload_date;
|
||||||
|
this.description = description;
|
||||||
|
this.view_count = view_count;
|
||||||
|
this.height = height;
|
||||||
|
this.abr = abr;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
getJSONMp3: getJSONMp3,
|
||||||
|
getJSONMp4: getJSONMp4,
|
||||||
|
getJSON: getJSON,
|
||||||
|
getTrueFileName: getTrueFileName,
|
||||||
|
getDownloadedThumbnail: getDownloadedThumbnail,
|
||||||
|
getDownloadedThumbnail2: getDownloadedThumbnail2,
|
||||||
|
getExpectedFileSize: getExpectedFileSize,
|
||||||
|
fixVideoMetadataPerms: fixVideoMetadataPerms,
|
||||||
|
fixVideoMetadataPerms2: fixVideoMetadataPerms2,
|
||||||
|
deleteJSONFile: deleteJSONFile,
|
||||||
|
deleteJSONFile2: deleteJSONFile2,
|
||||||
|
removeIDFromArchive, removeIDFromArchive,
|
||||||
|
getDownloadedFilesByType: getDownloadedFilesByType,
|
||||||
|
createContainerZipFile: createContainerZipFile,
|
||||||
|
durationStringToNumber: durationStringToNumber,
|
||||||
|
getMatchingCategoryFiles: getMatchingCategoryFiles,
|
||||||
|
addUIDsToCategory: addUIDsToCategory,
|
||||||
|
recFindByExt: recFindByExt,
|
||||||
|
removeFileExtension: removeFileExtension,
|
||||||
|
wait: wait,
|
||||||
|
File: File
|
||||||
|
}
|
||||||
Binary file not shown.
23
chart/.helmignore
Normal file
23
chart/.helmignore
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
# Patterns to ignore when building packages.
|
||||||
|
# This supports shell glob matching, relative path matching, and
|
||||||
|
# negation (prefixed with !). Only one pattern per line.
|
||||||
|
.DS_Store
|
||||||
|
# Common VCS dirs
|
||||||
|
.git/
|
||||||
|
.gitignore
|
||||||
|
.bzr/
|
||||||
|
.bzrignore
|
||||||
|
.hg/
|
||||||
|
.hgignore
|
||||||
|
.svn/
|
||||||
|
# Common backup files
|
||||||
|
*.swp
|
||||||
|
*.bak
|
||||||
|
*.tmp
|
||||||
|
*.orig
|
||||||
|
*~
|
||||||
|
# Various IDEs
|
||||||
|
.project
|
||||||
|
.idea/
|
||||||
|
*.tmproj
|
||||||
|
.vscode/
|
||||||
24
chart/Chart.yaml
Normal file
24
chart/Chart.yaml
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
apiVersion: v2
|
||||||
|
name: youtubedl-material
|
||||||
|
description: A Helm chart for Kubernetes
|
||||||
|
|
||||||
|
# A chart can be either an 'application' or a 'library' chart.
|
||||||
|
#
|
||||||
|
# Application charts are a collection of templates that can be packaged into versioned archives
|
||||||
|
# to be deployed.
|
||||||
|
#
|
||||||
|
# Library charts provide useful utilities or functions for the chart developer. They're included as
|
||||||
|
# a dependency of application charts to inject those utilities and functions into the rendering
|
||||||
|
# pipeline. Library charts do not define any templates and therefore cannot be deployed.
|
||||||
|
type: application
|
||||||
|
|
||||||
|
# This is the chart version. This version number should be incremented each time you make changes
|
||||||
|
# to the chart and its templates, including the app version.
|
||||||
|
# Versions are expected to follow Semantic Versioning (https://semver.org/)
|
||||||
|
version: 0.1.0
|
||||||
|
|
||||||
|
# This is the version number of the application being deployed. This version number should be
|
||||||
|
# incremented each time you make changes to the application. Versions are not expected to
|
||||||
|
# follow Semantic Versioning. They should reflect the version the application is using.
|
||||||
|
# It is recommended to use it with quotes.
|
||||||
|
appVersion: "4.2"
|
||||||
22
chart/templates/NOTES.txt
Normal file
22
chart/templates/NOTES.txt
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
1. Get the application URL by running these commands:
|
||||||
|
{{- if .Values.ingress.enabled }}
|
||||||
|
{{- range $host := .Values.ingress.hosts }}
|
||||||
|
{{- range .paths }}
|
||||||
|
http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
{{- else if contains "NodePort" .Values.service.type }}
|
||||||
|
export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "youtubedl-material.fullname" . }})
|
||||||
|
export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}")
|
||||||
|
echo http://$NODE_IP:$NODE_PORT
|
||||||
|
{{- else if contains "LoadBalancer" .Values.service.type }}
|
||||||
|
NOTE: It may take a few minutes for the LoadBalancer IP to be available.
|
||||||
|
You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "youtubedl-material.fullname" . }}'
|
||||||
|
export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "youtubedl-material.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}")
|
||||||
|
echo http://$SERVICE_IP:{{ .Values.service.port }}
|
||||||
|
{{- else if contains "ClusterIP" .Values.service.type }}
|
||||||
|
export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "youtubedl-material.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}")
|
||||||
|
export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}")
|
||||||
|
echo "Visit http://127.0.0.1:8080 to use your application"
|
||||||
|
kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT
|
||||||
|
{{- end }}
|
||||||
62
chart/templates/_helpers.tpl
Normal file
62
chart/templates/_helpers.tpl
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
{{/*
|
||||||
|
Expand the name of the chart.
|
||||||
|
*/}}
|
||||||
|
{{- define "youtubedl-material.name" -}}
|
||||||
|
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
{{/*
|
||||||
|
Create a default fully qualified app name.
|
||||||
|
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
|
||||||
|
If release name contains chart name it will be used as a full name.
|
||||||
|
*/}}
|
||||||
|
{{- define "youtubedl-material.fullname" -}}
|
||||||
|
{{- if .Values.fullnameOverride }}
|
||||||
|
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
|
||||||
|
{{- else }}
|
||||||
|
{{- $name := default .Chart.Name .Values.nameOverride }}
|
||||||
|
{{- if contains $name .Release.Name }}
|
||||||
|
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
|
||||||
|
{{- else }}
|
||||||
|
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
{{/*
|
||||||
|
Create chart name and version as used by the chart label.
|
||||||
|
*/}}
|
||||||
|
{{- define "youtubedl-material.chart" -}}
|
||||||
|
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
{{/*
|
||||||
|
Common labels
|
||||||
|
*/}}
|
||||||
|
{{- define "youtubedl-material.labels" -}}
|
||||||
|
helm.sh/chart: {{ include "youtubedl-material.chart" . }}
|
||||||
|
{{ include "youtubedl-material.selectorLabels" . }}
|
||||||
|
{{- if .Chart.AppVersion }}
|
||||||
|
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
|
||||||
|
{{- end }}
|
||||||
|
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
{{/*
|
||||||
|
Selector labels
|
||||||
|
*/}}
|
||||||
|
{{- define "youtubedl-material.selectorLabels" -}}
|
||||||
|
app.kubernetes.io/name: {{ include "youtubedl-material.name" . }}
|
||||||
|
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
{{/*
|
||||||
|
Create the name of the service account to use
|
||||||
|
*/}}
|
||||||
|
{{- define "youtubedl-material.serviceAccountName" -}}
|
||||||
|
{{- if .Values.serviceAccount.create }}
|
||||||
|
{{- default (include "youtubedl-material.fullname" .) .Values.serviceAccount.name }}
|
||||||
|
{{- else }}
|
||||||
|
{{- default "default" .Values.serviceAccount.name }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
21
chart/templates/appdata-pvc.yaml
Normal file
21
chart/templates/appdata-pvc.yaml
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
{{- if and .Values.persistence.appdata.enabled (not .Values.persistence.appdata.existingClaim) }}
|
||||||
|
kind: PersistentVolumeClaim
|
||||||
|
apiVersion: v1
|
||||||
|
metadata:
|
||||||
|
name: {{ template "youtubedl-material.fullname" . }}-appdata
|
||||||
|
labels:
|
||||||
|
{{- include "youtubedl-material.labels" . | nindent 4 }}
|
||||||
|
spec:
|
||||||
|
accessModes:
|
||||||
|
- {{ .Values.persistence.appdata.accessMode | quote }}
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
storage: {{ .Values.persistence.appdata.size | quote }}
|
||||||
|
{{- if .Values.persistence.appdata.storageClass }}
|
||||||
|
{{- if (eq "-" .Values.persistence.appdata.storageClass) }}
|
||||||
|
storageClassName: ""
|
||||||
|
{{- else }}
|
||||||
|
storageClassName: "{{ .Values.persistence.appdata.storageClass }}"
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end -}}
|
||||||
21
chart/templates/audio-pvc.yaml
Normal file
21
chart/templates/audio-pvc.yaml
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
{{- if and .Values.persistence.audio.enabled (not .Values.persistence.audio.existingClaim) }}
|
||||||
|
kind: PersistentVolumeClaim
|
||||||
|
apiVersion: v1
|
||||||
|
metadata:
|
||||||
|
name: {{ template "youtubedl-material.fullname" . }}-audio
|
||||||
|
labels:
|
||||||
|
{{- include "youtubedl-material.labels" . | nindent 4 }}
|
||||||
|
spec:
|
||||||
|
accessModes:
|
||||||
|
- {{ .Values.persistence.audio.accessMode | quote }}
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
storage: {{ .Values.persistence.audio.size | quote }}
|
||||||
|
{{- if .Values.persistence.audio.storageClass }}
|
||||||
|
{{- if (eq "-" .Values.persistence.audio.storageClass) }}
|
||||||
|
storageClassName: ""
|
||||||
|
{{- else }}
|
||||||
|
storageClassName: "{{ .Values.persistence.audio.storageClass }}"
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end -}}
|
||||||
121
chart/templates/deployment.yaml
Normal file
121
chart/templates/deployment.yaml
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: {{ include "youtubedl-material.fullname" . }}
|
||||||
|
labels:
|
||||||
|
{{- include "youtubedl-material.labels" . | nindent 4 }}
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
{{- include "youtubedl-material.selectorLabels" . | nindent 6 }}
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
{{- with .Values.podAnnotations }}
|
||||||
|
annotations:
|
||||||
|
{{- toYaml . | nindent 8 }}
|
||||||
|
{{- end }}
|
||||||
|
labels:
|
||||||
|
{{- include "youtubedl-material.selectorLabels" . | nindent 8 }}
|
||||||
|
spec:
|
||||||
|
{{- with .Values.imagePullSecrets }}
|
||||||
|
imagePullSecrets:
|
||||||
|
{{- toYaml . | nindent 8 }}
|
||||||
|
{{- end }}
|
||||||
|
serviceAccountName: {{ include "youtubedl-material.serviceAccountName" . }}
|
||||||
|
securityContext:
|
||||||
|
{{- toYaml .Values.podSecurityContext | nindent 8 }}
|
||||||
|
containers:
|
||||||
|
- name: {{ .Chart.Name }}
|
||||||
|
securityContext:
|
||||||
|
{{- toYaml .Values.securityContext | nindent 12 }}
|
||||||
|
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
|
||||||
|
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||||
|
ports:
|
||||||
|
- name: http
|
||||||
|
containerPort: 17442
|
||||||
|
protocol: TCP
|
||||||
|
livenessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /
|
||||||
|
port: http
|
||||||
|
readinessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /
|
||||||
|
port: http
|
||||||
|
resources:
|
||||||
|
{{- toYaml .Values.resources | nindent 12 }}
|
||||||
|
volumeMounts:
|
||||||
|
- mountPath: /app/appdata
|
||||||
|
name: appdata
|
||||||
|
{{- if .Values.persistence.appdata.subPath }}
|
||||||
|
subPath: {{ .Values.persistence.appdata.subPath }}
|
||||||
|
{{- end }}
|
||||||
|
- mountPath: /app/audio
|
||||||
|
name: audio
|
||||||
|
{{- if .Values.persistence.audio.subPath }}
|
||||||
|
subPath: {{ .Values.persistence.audio.subPath }}
|
||||||
|
{{- end }}
|
||||||
|
- mountPath: /app/video
|
||||||
|
name: video
|
||||||
|
{{- if .Values.persistence.video.subPath }}
|
||||||
|
subPath: {{ .Values.persistence.video.subPath }}
|
||||||
|
{{- end }}
|
||||||
|
- mountPath: /app/subscriptions
|
||||||
|
name: subscriptions
|
||||||
|
{{- if .Values.persistence.subscriptions.subPath }}
|
||||||
|
subPath: {{ .Values.persistence.subscriptions.subPath }}
|
||||||
|
{{- end }}
|
||||||
|
- mountPath: /app/users
|
||||||
|
name: users
|
||||||
|
{{- if .Values.persistence.users.subPath }}
|
||||||
|
subPath: {{ .Values.persistence.users.subPath }}
|
||||||
|
{{- end }}
|
||||||
|
volumes:
|
||||||
|
- name: appdata
|
||||||
|
{{- if .Values.persistence.appdata.enabled}}
|
||||||
|
persistentVolumeClaim:
|
||||||
|
claimName: {{ if .Values.persistence.appdata.existingClaim }}{{ .Values.persistence.appdata.existingClaim }}{{- else }}{{ template "youtubedl-material.fullname" . }}-appdata{{- end }}
|
||||||
|
{{- else }}
|
||||||
|
emptyDir: {}
|
||||||
|
{{- end }}
|
||||||
|
- name: audio
|
||||||
|
{{- if .Values.persistence.audio.enabled}}
|
||||||
|
persistentVolumeClaim:
|
||||||
|
claimName: {{ if .Values.persistence.audio.existingClaim }}{{ .Values.persistence.audio.existingClaim }}{{- else }}{{ template "youtubedl-material.fullname" . }}-audio{{- end }}
|
||||||
|
{{- else }}
|
||||||
|
emptyDir: {}
|
||||||
|
{{- end }}
|
||||||
|
- name: subscriptions
|
||||||
|
{{- if .Values.persistence.subscriptions.enabled}}
|
||||||
|
persistentVolumeClaim:
|
||||||
|
claimName: {{ if .Values.persistence.subscriptions.existingClaim }}{{ .Values.persistence.subscriptions.existingClaim }}{{- else }}{{ template "youtubedl-material.fullname" . }}-subscriptions{{- end }}
|
||||||
|
{{- else }}
|
||||||
|
emptyDir: {}
|
||||||
|
{{- end }}
|
||||||
|
- name: users
|
||||||
|
{{- if .Values.persistence.users.enabled}}
|
||||||
|
persistentVolumeClaim:
|
||||||
|
claimName: {{ if .Values.persistence.users.existingClaim }}{{ .Values.persistence.users.existingClaim }}{{- else }}{{ template "youtubedl-material.fullname" . }}-users{{- end }}
|
||||||
|
{{- else }}
|
||||||
|
emptyDir: {}
|
||||||
|
{{- end }}
|
||||||
|
- name: video
|
||||||
|
{{- if .Values.persistence.video.enabled}}
|
||||||
|
persistentVolumeClaim:
|
||||||
|
claimName: {{ if .Values.persistence.video.existingClaim }}{{ .Values.persistence.video.existingClaim }}{{- else }}{{ template "youtubedl-material.fullname" . }}-video{{- end }}
|
||||||
|
{{- else }}
|
||||||
|
emptyDir: {}
|
||||||
|
{{- end }}
|
||||||
|
{{- with .Values.nodeSelector }}
|
||||||
|
nodeSelector:
|
||||||
|
{{- toYaml . | nindent 8 }}
|
||||||
|
{{- end }}
|
||||||
|
{{- with .Values.affinity }}
|
||||||
|
affinity:
|
||||||
|
{{- toYaml . | nindent 8 }}
|
||||||
|
{{- end }}
|
||||||
|
{{- with .Values.tolerations }}
|
||||||
|
tolerations:
|
||||||
|
{{- toYaml . | nindent 8 }}
|
||||||
|
{{- end }}
|
||||||
41
chart/templates/ingress.yaml
Normal file
41
chart/templates/ingress.yaml
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
{{- if .Values.ingress.enabled -}}
|
||||||
|
{{- $fullName := include "youtubedl-material.fullname" . -}}
|
||||||
|
{{- $svcPort := .Values.service.port -}}
|
||||||
|
{{- if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}}
|
||||||
|
apiVersion: networking.k8s.io/v1beta1
|
||||||
|
{{- else -}}
|
||||||
|
apiVersion: extensions/v1beta1
|
||||||
|
{{- end }}
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
name: {{ $fullName }}
|
||||||
|
labels:
|
||||||
|
{{- include "youtubedl-material.labels" . | nindent 4 }}
|
||||||
|
{{- with .Values.ingress.annotations }}
|
||||||
|
annotations:
|
||||||
|
{{- toYaml . | nindent 4 }}
|
||||||
|
{{- end }}
|
||||||
|
spec:
|
||||||
|
{{- if .Values.ingress.tls }}
|
||||||
|
tls:
|
||||||
|
{{- range .Values.ingress.tls }}
|
||||||
|
- hosts:
|
||||||
|
{{- range .hosts }}
|
||||||
|
- {{ . | quote }}
|
||||||
|
{{- end }}
|
||||||
|
secretName: {{ .secretName }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
rules:
|
||||||
|
{{- range .Values.ingress.hosts }}
|
||||||
|
- host: {{ .host | quote }}
|
||||||
|
http:
|
||||||
|
paths:
|
||||||
|
{{- range .paths }}
|
||||||
|
- path: {{ .path }}
|
||||||
|
backend:
|
||||||
|
serviceName: {{ $fullName }}
|
||||||
|
servicePort: {{ $svcPort }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
15
chart/templates/service.yaml
Normal file
15
chart/templates/service.yaml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: {{ include "youtubedl-material.fullname" . }}
|
||||||
|
labels:
|
||||||
|
{{- include "youtubedl-material.labels" . | nindent 4 }}
|
||||||
|
spec:
|
||||||
|
type: {{ .Values.service.type }}
|
||||||
|
ports:
|
||||||
|
- port: {{ .Values.service.port }}
|
||||||
|
targetPort: http
|
||||||
|
protocol: TCP
|
||||||
|
name: http
|
||||||
|
selector:
|
||||||
|
{{- include "youtubedl-material.selectorLabels" . | nindent 4 }}
|
||||||
12
chart/templates/serviceaccount.yaml
Normal file
12
chart/templates/serviceaccount.yaml
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{{- if .Values.serviceAccount.create -}}
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ServiceAccount
|
||||||
|
metadata:
|
||||||
|
name: {{ include "youtubedl-material.serviceAccountName" . }}
|
||||||
|
labels:
|
||||||
|
{{- include "youtubedl-material.labels" . | nindent 4 }}
|
||||||
|
{{- with .Values.serviceAccount.annotations }}
|
||||||
|
annotations:
|
||||||
|
{{- toYaml . | nindent 4 }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
21
chart/templates/subscriptions-pvc.yaml
Normal file
21
chart/templates/subscriptions-pvc.yaml
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
{{- if and .Values.persistence.subscriptions.enabled (not .Values.persistence.subscriptions.existingClaim) }}
|
||||||
|
kind: PersistentVolumeClaim
|
||||||
|
apiVersion: v1
|
||||||
|
metadata:
|
||||||
|
name: {{ template "youtubedl-material.fullname" . }}-subscriptions
|
||||||
|
labels:
|
||||||
|
{{- include "youtubedl-material.labels" . | nindent 4 }}
|
||||||
|
spec:
|
||||||
|
accessModes:
|
||||||
|
- {{ .Values.persistence.subscriptions.accessMode | quote }}
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
storage: {{ .Values.persistence.subscriptions.size | quote }}
|
||||||
|
{{- if .Values.persistence.subscriptions.storageClass }}
|
||||||
|
{{- if (eq "-" .Values.persistence.subscriptions.storageClass) }}
|
||||||
|
storageClassName: ""
|
||||||
|
{{- else }}
|
||||||
|
storageClassName: "{{ .Values.persistence.subscriptions.storageClass }}"
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end -}}
|
||||||
15
chart/templates/tests/test-connection.yaml
Normal file
15
chart/templates/tests/test-connection.yaml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Pod
|
||||||
|
metadata:
|
||||||
|
name: "{{ include "youtubedl-material.fullname" . }}-test-connection"
|
||||||
|
labels:
|
||||||
|
{{- include "youtubedl-material.labels" . | nindent 4 }}
|
||||||
|
annotations:
|
||||||
|
"helm.sh/hook": test
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: wget
|
||||||
|
image: busybox
|
||||||
|
command: ['wget']
|
||||||
|
args: ['{{ include "youtubedl-material.fullname" . }}:{{ .Values.service.port }}']
|
||||||
|
restartPolicy: Never
|
||||||
21
chart/templates/users-pvc.yaml
Normal file
21
chart/templates/users-pvc.yaml
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
{{- if and .Values.persistence.users.enabled (not .Values.persistence.users.existingClaim) }}
|
||||||
|
kind: PersistentVolumeClaim
|
||||||
|
apiVersion: v1
|
||||||
|
metadata:
|
||||||
|
name: {{ template "youtubedl-material.fullname" . }}-users
|
||||||
|
labels:
|
||||||
|
{{- include "youtubedl-material.labels" . | nindent 4 }}
|
||||||
|
spec:
|
||||||
|
accessModes:
|
||||||
|
- {{ .Values.persistence.users.accessMode | quote }}
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
storage: {{ .Values.persistence.users.size | quote }}
|
||||||
|
{{- if .Values.persistence.users.storageClass }}
|
||||||
|
{{- if (eq "-" .Values.persistence.users.storageClass) }}
|
||||||
|
storageClassName: ""
|
||||||
|
{{- else }}
|
||||||
|
storageClassName: "{{ .Values.persistence.users.storageClass }}"
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end -}}
|
||||||
21
chart/templates/video-pvc.yaml
Normal file
21
chart/templates/video-pvc.yaml
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
{{- if and .Values.persistence.video.enabled (not .Values.persistence.video.existingClaim) }}
|
||||||
|
kind: PersistentVolumeClaim
|
||||||
|
apiVersion: v1
|
||||||
|
metadata:
|
||||||
|
name: {{ template "youtubedl-material.fullname" . }}-video
|
||||||
|
labels:
|
||||||
|
{{- include "youtubedl-material.labels" . | nindent 4 }}
|
||||||
|
spec:
|
||||||
|
accessModes:
|
||||||
|
- {{ .Values.persistence.video.accessMode | quote }}
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
storage: {{ .Values.persistence.video.size | quote }}
|
||||||
|
{{- if .Values.persistence.video.storageClass }}
|
||||||
|
{{- if (eq "-" .Values.persistence.video.storageClass) }}
|
||||||
|
storageClassName: ""
|
||||||
|
{{- else }}
|
||||||
|
storageClassName: "{{ .Values.persistence.video.storageClass }}"
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end -}}
|
||||||
153
chart/values.yaml
Normal file
153
chart/values.yaml
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
# Default values for youtubedl-material.
|
||||||
|
# This is a YAML-formatted file.
|
||||||
|
# Declare variables to be passed into your templates.
|
||||||
|
|
||||||
|
replicaCount: 1
|
||||||
|
|
||||||
|
image:
|
||||||
|
repository: tzahi12345/youtubedl-material
|
||||||
|
pullPolicy: IfNotPresent
|
||||||
|
# Overrides the image tag whose default is the chart appVersion.
|
||||||
|
tag: ""
|
||||||
|
|
||||||
|
imagePullSecrets: []
|
||||||
|
nameOverride: ""
|
||||||
|
fullnameOverride: ""
|
||||||
|
|
||||||
|
serviceAccount:
|
||||||
|
# Specifies whether a service account should be created
|
||||||
|
create: true
|
||||||
|
# Annotations to add to the service account
|
||||||
|
annotations: {}
|
||||||
|
# The name of the service account to use.
|
||||||
|
# If not set and create is true, a name is generated using the fullname template
|
||||||
|
name: ""
|
||||||
|
|
||||||
|
podAnnotations: {}
|
||||||
|
|
||||||
|
podSecurityContext: {}
|
||||||
|
# fsGroup: 2000
|
||||||
|
|
||||||
|
securityContext: {}
|
||||||
|
# capabilities:
|
||||||
|
# drop:
|
||||||
|
# - ALL
|
||||||
|
# readOnlyRootFilesystem: true
|
||||||
|
# runAsNonRoot: true
|
||||||
|
# runAsUser: 1000
|
||||||
|
|
||||||
|
service:
|
||||||
|
type: ClusterIP
|
||||||
|
port: 17442
|
||||||
|
|
||||||
|
ingress:
|
||||||
|
enabled: false
|
||||||
|
annotations: {}
|
||||||
|
# kubernetes.io/ingress.class: nginx
|
||||||
|
# kubernetes.io/tls-acme: "true"
|
||||||
|
hosts:
|
||||||
|
- host: chart-example.local
|
||||||
|
paths: []
|
||||||
|
tls: []
|
||||||
|
# - secretName: chart-example-tls
|
||||||
|
# hosts:
|
||||||
|
# - chart-example.local
|
||||||
|
|
||||||
|
resources: {}
|
||||||
|
# We usually recommend not to specify default resources and to leave this as a conscious
|
||||||
|
# choice for the user. This also increases chances charts run on environments with little
|
||||||
|
# resources, such as Minikube. If you do want to specify resources, uncomment the following
|
||||||
|
# lines, adjust them as necessary, and remove the curly braces after 'resources:'.
|
||||||
|
# limits:
|
||||||
|
# cpu: 100m
|
||||||
|
# memory: 128Mi
|
||||||
|
# requests:
|
||||||
|
# cpu: 100m
|
||||||
|
# memory: 128Mi
|
||||||
|
|
||||||
|
persistence:
|
||||||
|
appdata:
|
||||||
|
enabled: true
|
||||||
|
## If defined, storageClassName: <storageClass>
|
||||||
|
## If set to "-", storageClassName: "", which disables dynamic provisioning
|
||||||
|
## If undefined (the default) or set to null, no storageClassName spec is
|
||||||
|
## set, choosing the default provisioner. (gp2 on AWS, standard on
|
||||||
|
## GKE, AWS & OpenStack)
|
||||||
|
##
|
||||||
|
# storageClass: "-"
|
||||||
|
## If you want to reuse an existing claim, you can pass the name of the PVC using
|
||||||
|
## the existingClaim variable
|
||||||
|
# existingClaim: your-claim
|
||||||
|
# subPath: some-subpath
|
||||||
|
accessMode: ReadWriteOnce
|
||||||
|
size: 1Gi
|
||||||
|
audio:
|
||||||
|
enabled: true
|
||||||
|
## If defined, storageClassName: <storageClass>
|
||||||
|
## If set to "-", storageClassName: "", which disables dynamic provisioning
|
||||||
|
## If undefined (the default) or set to null, no storageClassName spec is
|
||||||
|
## set, choosing the default provisioner. (gp2 on AWS, standard on
|
||||||
|
## GKE, AWS & OpenStack)
|
||||||
|
##
|
||||||
|
# storageClass: "-"
|
||||||
|
##
|
||||||
|
## If you want to reuse an existing claim, you can pass the name of the PVC using
|
||||||
|
## the existingClaim variable
|
||||||
|
# existingClaim: your-claim
|
||||||
|
# subPath: some-subpath
|
||||||
|
accessMode: ReadWriteOnce
|
||||||
|
size: 50Gi
|
||||||
|
video:
|
||||||
|
enabled: true
|
||||||
|
## If defined, storageClassName: <storageClass>
|
||||||
|
## If set to "-", storageClassName: "", which disables dynamic provisioning
|
||||||
|
## If undefined (the default) or set to null, no storageClassName spec is
|
||||||
|
## set, choosing the default provisioner. (gp2 on AWS, standard on
|
||||||
|
## GKE, AWS & OpenStack)
|
||||||
|
##
|
||||||
|
# storageClass: "-"
|
||||||
|
##
|
||||||
|
## If you want to reuse an existing claim, you can pass the name of the PVC using
|
||||||
|
## the existingClaim variable
|
||||||
|
# existingClaim: your-claim
|
||||||
|
# subPath: some-subpath
|
||||||
|
accessMode: ReadWriteOnce
|
||||||
|
size: 50Gi
|
||||||
|
subscriptions:
|
||||||
|
enabled: true
|
||||||
|
## If defined, storageClassName: <storageClass>
|
||||||
|
## If set to "-", storageClassName: "", which disables dynamic provisioning
|
||||||
|
## If undefined (the default) or set to null, no storageClassName spec is
|
||||||
|
## set, choosing the default provisioner. (gp2 on AWS, standard on
|
||||||
|
## GKE, AWS & OpenStack)
|
||||||
|
##
|
||||||
|
# storageClass: "-"
|
||||||
|
##
|
||||||
|
## If you want to reuse an existing claim, you can pass the name of the PVC using
|
||||||
|
## the existingClaim variable
|
||||||
|
# existingClaim: your-claim
|
||||||
|
# subPath: some-subpath
|
||||||
|
accessMode: ReadWriteOnce
|
||||||
|
size: 50Gi
|
||||||
|
users:
|
||||||
|
enabled: true
|
||||||
|
## If defined, storageClassName: <storageClass>
|
||||||
|
## If set to "-", storageClassName: "", which disables dynamic provisioning
|
||||||
|
## If undefined (the default) or set to null, no storageClassName spec is
|
||||||
|
## set, choosing the default provisioner. (gp2 on AWS, standard on
|
||||||
|
## GKE, AWS & OpenStack)
|
||||||
|
##
|
||||||
|
# storageClass: "-"
|
||||||
|
##
|
||||||
|
## If you want to reuse an existing claim, you can pass the name of the PVC using
|
||||||
|
## the existingClaim variable
|
||||||
|
# existingClaim: your-claim
|
||||||
|
# subPath: some-subpath
|
||||||
|
accessMode: ReadWriteOnce
|
||||||
|
size: 50Gi
|
||||||
|
|
||||||
|
nodeSelector: {}
|
||||||
|
|
||||||
|
tolerations: []
|
||||||
|
|
||||||
|
affinity: {}
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
// background.js
|
|
||||||
|
|
||||||
// Called when the user clicks on the browser action.
|
|
||||||
chrome.browserAction.onClicked.addListener(function(tab) {
|
|
||||||
// get the frontend_url
|
|
||||||
chrome.storage.sync.get({
|
|
||||||
frontend_url: 'http://localhost',
|
|
||||||
audio_only: false
|
|
||||||
}, function(items) {
|
|
||||||
chrome.tabs.query({active: true, currentWindow: true}, function(tabs) {
|
|
||||||
var activeTab = tabs[0];
|
|
||||||
var url = activeTab.url;
|
|
||||||
if (url.includes('youtube.com')) {
|
|
||||||
var new_url = items.frontend_url + '/#/home;url=' + encodeURIComponent(url) + ';audioOnly=' + items.audio_only;
|
|
||||||
chrome.tabs.create({ url: new_url });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
@@ -1,17 +1,17 @@
|
|||||||
{
|
{
|
||||||
"manifest_version": 2,
|
"manifest_version": 2,
|
||||||
"name": "YoutubeDL-Material",
|
"name": "YoutubeDL-Material",
|
||||||
"version": "0.3",
|
"version": "0.4",
|
||||||
"description": "The Official Firefox & Chrome Extension of YoutubeDL-Material, an open-source and self-hosted YouTube downloader.",
|
"description": "The Official Firefox & Chrome Extension of YoutubeDL-Material, an open-source and self-hosted YouTube downloader.",
|
||||||
"background": {
|
|
||||||
"scripts": ["background.js"]
|
|
||||||
},
|
|
||||||
"browser_action": {
|
"browser_action": {
|
||||||
"default_icon": "favicon.png"
|
"default_icon": "favicon.png",
|
||||||
|
"default_popup": "popup.html",
|
||||||
|
"default_title": "YoutubeDL-Material"
|
||||||
},
|
},
|
||||||
"permissions": [
|
"permissions": [
|
||||||
"tabs",
|
"tabs",
|
||||||
"storage"
|
"storage",
|
||||||
|
"contextMenus"
|
||||||
],
|
],
|
||||||
"options_ui": {
|
"options_ui": {
|
||||||
"page": "options.html",
|
"page": "options.html",
|
||||||
|
|||||||
35
chrome-extension/popup.html
Normal file
35
chrome-extension/popup.html
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<!-- Scripts -->
|
||||||
|
<script src="js/jquery-3.4.1.min.js"></script>
|
||||||
|
<script src="js/popper.min.js"></script>
|
||||||
|
<script src="js/bootstrap.min.js"></script>
|
||||||
|
|
||||||
|
<!-- Cascading Style Sheets -->
|
||||||
|
<link href="css/bootstrap.min.css" rel="stylesheet" media="screen">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div style="width: 400px; margin: 0 auto;">
|
||||||
|
<div style="margin: 10px;">
|
||||||
|
<div class="checkbox">
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" id="audio_only">
|
||||||
|
Audio only
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="input-group mb-3">
|
||||||
|
<input id="url_input" type="text" class="form-control" placeholder="URL" aria-label="URL" aria-describedby="basic-addon2">
|
||||||
|
<div class="input-group-append">
|
||||||
|
<button class="btn btn-outline-secondary" type="button" id="download">Download</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="popup.js"></script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
50
chrome-extension/popup.js
Normal file
50
chrome-extension/popup.js
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
function audioOnlyClicked() {
|
||||||
|
console.log('audio only clicked');
|
||||||
|
var audio_only = document.getElementById("audio_only").checked;
|
||||||
|
|
||||||
|
// save state
|
||||||
|
|
||||||
|
chrome.storage.sync.set({
|
||||||
|
audio_only: audio_only
|
||||||
|
}, function() {});
|
||||||
|
}
|
||||||
|
|
||||||
|
function downloadVideo() {
|
||||||
|
var input_url = document.getElementById("url_input").value
|
||||||
|
// get the frontend_url
|
||||||
|
chrome.storage.sync.get({
|
||||||
|
frontend_url: 'http://localhost',
|
||||||
|
audio_only: false
|
||||||
|
}, function(items) {
|
||||||
|
var download_url = items.frontend_url + '/#/home;url=' + encodeURIComponent(input_url) + ';audioOnly=' + items.audio_only;
|
||||||
|
chrome.tabs.create({ url: download_url });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadInputs() {
|
||||||
|
// load audio-only input
|
||||||
|
chrome.storage.sync.get({
|
||||||
|
frontend_url: 'http://localhost',
|
||||||
|
audio_only: false
|
||||||
|
}, function(items) {
|
||||||
|
document.getElementById("audio_only").checked = items.audio_only;
|
||||||
|
});
|
||||||
|
|
||||||
|
// load url input
|
||||||
|
chrome.tabs.query({active: true, currentWindow: true}, function(tabs) {
|
||||||
|
var activeTab = tabs[0];
|
||||||
|
var current_url = activeTab.url;
|
||||||
|
console.log(current_url);
|
||||||
|
if (current_url && current_url.includes('youtube.com')) {
|
||||||
|
document.getElementById("url_input").value = current_url;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('download').addEventListener('click',
|
||||||
|
downloadVideo);
|
||||||
|
|
||||||
|
document.getElementById('audio_only').addEventListener('click',
|
||||||
|
audioOnlyClicked);
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', loadInputs);
|
||||||
Binary file not shown.
Binary file not shown.
27
docker-compose.yml
Normal file
27
docker-compose.yml
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
version: "2"
|
||||||
|
services:
|
||||||
|
ytdl_material:
|
||||||
|
environment:
|
||||||
|
ALLOW_CONFIG_MUTATIONS: 'true'
|
||||||
|
ytdl_mongodb_connection_string: 'mongodb://ytdl-mongo-db:27017'
|
||||||
|
ytdl_use_local_db: 'false'
|
||||||
|
write_ytdl_config: 'true'
|
||||||
|
restart: always
|
||||||
|
volumes:
|
||||||
|
- ./appdata:/app/appdata
|
||||||
|
- ./audio:/app/audio
|
||||||
|
- ./video:/app/video
|
||||||
|
- ./subscriptions:/app/subscriptions
|
||||||
|
- ./users:/app/users
|
||||||
|
ports:
|
||||||
|
- "8998:17442"
|
||||||
|
image: tzahi12345/youtubedl-material:latest
|
||||||
|
ytdl-mongo-db:
|
||||||
|
image: mongo
|
||||||
|
ports:
|
||||||
|
- "27017:27017"
|
||||||
|
logging:
|
||||||
|
driver: "none"
|
||||||
|
container_name: mongo-db
|
||||||
|
volumes:
|
||||||
|
- ./db/:/data/db
|
||||||
13085
package-lock.json
generated
13085
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
68
package.json
68
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "youtube-dl-material",
|
"name": "youtube-dl-material",
|
||||||
"version": "4.0.0",
|
"version": "4.2.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"ng": "ng",
|
"ng": "ng",
|
||||||
@@ -18,53 +18,57 @@
|
|||||||
},
|
},
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@angular-devkit/core": "^9.0.6",
|
"@angular-devkit/core": "^11.0.4",
|
||||||
"@angular/animations": "^9.1.0",
|
"@angular/animations": "^11.0.4",
|
||||||
"@angular/cdk": "^9.2.0",
|
"@angular/cdk": "^11.0.2",
|
||||||
"@angular/common": "^9.1.0",
|
"@angular/common": "^11.0.4",
|
||||||
"@angular/compiler": "^9.1.0",
|
"@angular/compiler": "^11.0.4",
|
||||||
"@angular/core": "^9.0.7",
|
"@angular/core": "^11.0.4",
|
||||||
"@angular/forms": "^9.1.0",
|
"@angular/forms": "^11.0.4",
|
||||||
"@angular/localize": "^9.1.0",
|
"@angular/localize": "^11.0.4",
|
||||||
"@angular/material": "^9.2.0",
|
"@angular/material": "^11.0.2",
|
||||||
"@angular/platform-browser": "^9.1.0",
|
"@angular/platform-browser": "^11.0.4",
|
||||||
"@angular/platform-browser-dynamic": "^9.1.0",
|
"@angular/platform-browser-dynamic": "^11.0.4",
|
||||||
"@angular/router": "^9.1.0",
|
"@angular/router": "^11.0.4",
|
||||||
|
"@ngneat/content-loader": "^5.0.0",
|
||||||
|
"@videogular/ngx-videogular": "^2.1.0",
|
||||||
"core-js": "^2.4.1",
|
"core-js": "^2.4.1",
|
||||||
"file-saver": "^2.0.2",
|
"file-saver": "^2.0.2",
|
||||||
"filesize": "^6.1.0",
|
"filesize": "^6.1.0",
|
||||||
"fingerprintjs2": "^2.1.0",
|
"fingerprintjs2": "^2.1.0",
|
||||||
|
"material-icons": "^0.5.4",
|
||||||
|
"nan": "^2.14.1",
|
||||||
"ng-lazyload-image": "^7.0.1",
|
"ng-lazyload-image": "^7.0.1",
|
||||||
|
"ngx-avatar": "^4.0.0",
|
||||||
"ngx-file-drop": "^9.0.1",
|
"ngx-file-drop": "^9.0.1",
|
||||||
"ngx-videogular": "^9.0.1",
|
"rxjs": "^6.6.3",
|
||||||
"rxjs": "^6.5.3",
|
|
||||||
"rxjs-compat": "^6.0.0-rc.0",
|
"rxjs-compat": "^6.0.0-rc.0",
|
||||||
"tslib": "^1.10.0",
|
"tslib": "^2.0.0",
|
||||||
"typescript": "~3.7.5",
|
"typescript": "~4.0.5",
|
||||||
"web-animations-js": "^2.3.2",
|
"web-animations-js": "^2.3.2",
|
||||||
"zone.js": "~0.10.2"
|
"zone.js": "~0.10.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@angular-devkit/build-angular": "^0.901.0",
|
"@angular-devkit/build-angular": "^0.1100.4",
|
||||||
"@angular/cli": "^9.0.7",
|
"@angular/cli": "^11.0.4",
|
||||||
"@angular/compiler-cli": "^9.0.7",
|
"@angular/compiler-cli": "^11.0.4",
|
||||||
"@angular/language-service": "^9.0.7",
|
"@angular/language-service": "^11.0.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": "2.5.45",
|
"@types/jasmine": "~3.6.0",
|
||||||
"@types/node": "^12.11.1",
|
"@types/node": "^12.11.1",
|
||||||
"codelyzer": "^5.1.2",
|
"codelyzer": "^6.0.0",
|
||||||
"electron": "^8.0.1",
|
"electron": "^8.0.1",
|
||||||
"jasmine-core": "~2.6.2",
|
"jasmine-core": "~3.6.0",
|
||||||
"jasmine-spec-reporter": "~4.1.0",
|
"jasmine-spec-reporter": "~5.0.0",
|
||||||
"karma": "~1.7.0",
|
"karma": "~5.0.0",
|
||||||
"karma-chrome-launcher": "~2.1.1",
|
"karma-chrome-launcher": "~3.1.0",
|
||||||
"karma-cli": "~1.0.1",
|
"karma-cli": "~1.0.1",
|
||||||
"karma-coverage-istanbul-reporter": "^1.2.1",
|
"karma-coverage-istanbul-reporter": "~3.0.2",
|
||||||
"karma-jasmine": "~1.1.0",
|
"karma-jasmine": "~4.0.0",
|
||||||
"karma-jasmine-html-reporter": "^0.2.2",
|
"karma-jasmine-html-reporter": "^1.5.0",
|
||||||
"protractor": "~5.1.2",
|
"protractor": "~7.0.0",
|
||||||
"ts-node": "~3.0.4",
|
"ts-node": "~3.0.4",
|
||||||
"tslint": "~5.3.2"
|
"tslint": "~6.1.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ const routes: Routes = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [RouterModule.forRoot(routes, { useHash: true })],
|
imports: [RouterModule.forRoot(routes, { useHash: true, relativeLinkResolution: 'legacy' })],
|
||||||
exports: [RouterModule]
|
exports: [RouterModule]
|
||||||
})
|
})
|
||||||
export class AppRoutingModule { }
|
export class AppRoutingModule { }
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
<span i18n="Dark mode toggle label">Dark</span>
|
<span i18n="Dark mode toggle label">Dark</span>
|
||||||
<mat-slide-toggle class="theme-slide-toggle" [checked]="postsService.theme.key === 'dark'"></mat-slide-toggle>
|
<mat-slide-toggle class="theme-slide-toggle" [checked]="postsService.theme.key === 'dark'"></mat-slide-toggle>
|
||||||
</button>
|
</button>
|
||||||
<button *ngIf="!postsService.isLoggedIn || postsService.permissions.includes('settings')" (click)="openSettingsDialog()" mat-menu-item>
|
<button *ngIf="postsService.config && (!postsService.config.Advanced.multi_user_mode || (postsService.isLoggedIn && postsService.permissions.includes('settings')))" (click)="openSettingsDialog()" mat-menu-item>
|
||||||
<mat-icon>settings</mat-icon>
|
<mat-icon>settings</mat-icon>
|
||||||
<span i18n="Settings menu label">Settings</span>
|
<span i18n="Settings menu label">Settings</span>
|
||||||
</button>
|
</button>
|
||||||
@@ -38,12 +38,16 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="sidenav-container" style="height: calc(100% - 64px)">
|
<div class="sidenav-container" style="height: calc(100% - 64px)">
|
||||||
<mat-sidenav-container style="height: 100%">
|
<mat-sidenav-container style="height: 100%">
|
||||||
<mat-sidenav #sidenav>
|
<mat-sidenav [opened]="postsService.sidepanel_mode === 'side' && !window.location.href.includes('/player')" [mode]="postsService.sidepanel_mode" #sidenav>
|
||||||
<mat-nav-list>
|
<mat-nav-list>
|
||||||
<a *ngIf="postsService.config && (!postsService.config.Advanced.multi_user_mode || postsService.isLoggedIn)" mat-list-item (click)="sidenav.close()" routerLink='/home'><ng-container i18n="Navigation menu Home Page title">Home</ng-container></a>
|
<a *ngIf="postsService.config && (!postsService.config.Advanced.multi_user_mode || postsService.isLoggedIn)" mat-list-item (click)="postsService.sidepanel_mode === 'over' ? sidenav.close() : null" routerLink='/home'><ng-container i18n="Navigation menu Home Page title">Home</ng-container></a>
|
||||||
<a *ngIf="postsService.config && postsService.config.Advanced.multi_user_mode && !postsService.isLoggedIn" mat-list-item (click)="sidenav.close()" routerLink='/login'><ng-container i18n="Navigation menu Login Page title">Login</ng-container></a>
|
<a *ngIf="postsService.config && postsService.config.Advanced.multi_user_mode && !postsService.isLoggedIn" mat-list-item (click)="sidenav.close()" routerLink='/login'><ng-container i18n="Navigation menu Login Page title">Login</ng-container></a>
|
||||||
<a *ngIf="postsService.config && allowSubscriptions && (!postsService.config.Advanced.multi_user_mode || (postsService.isLoggedIn && postsService.permissions.includes('subscriptions')))" mat-list-item (click)="sidenav.close()" routerLink='/subscriptions'><ng-container i18n="Navigation menu Subscriptions Page title">Subscriptions</ng-container></a>
|
<a *ngIf="postsService.config && allowSubscriptions && (!postsService.config.Advanced.multi_user_mode || (postsService.isLoggedIn && postsService.permissions.includes('subscriptions')))" mat-list-item (click)="postsService.sidepanel_mode === 'over' ? sidenav.close() : null" routerLink='/subscriptions'><ng-container i18n="Navigation menu Subscriptions Page title">Subscriptions</ng-container></a>
|
||||||
<a *ngIf="postsService.config && enableDownloadsManager && (!postsService.config.Advanced.multi_user_mode || (postsService.isLoggedIn && postsService.permissions.includes('downloads_manager')))" mat-list-item (click)="sidenav.close()" routerLink='/downloads'><ng-container i18n="Navigation menu Downloads Page title">Downloads</ng-container></a>
|
<a *ngIf="postsService.config && enableDownloadsManager && (!postsService.config.Advanced.multi_user_mode || (postsService.isLoggedIn && postsService.permissions.includes('downloads_manager')))" mat-list-item (click)="postsService.sidepanel_mode === 'over' ? sidenav.close() : null" routerLink='/downloads'><ng-container i18n="Navigation menu Downloads Page title">Downloads</ng-container></a>
|
||||||
|
<ng-container *ngIf="postsService.config && allowSubscriptions && postsService.subscriptions && (!postsService.config.Advanced.multi_user_mode || (postsService.isLoggedIn && postsService.permissions.includes('subscriptions')))">
|
||||||
|
<mat-divider></mat-divider>
|
||||||
|
<a *ngFor="let subscription of postsService.subscriptions" mat-list-item (click)="postsService.sidepanel_mode === 'over' ? sidenav.close() : null" [routerLink]="['/subscription', { id: subscription.id }]"><ngx-avatar [style.margin-right]="'10px'" size="32" [name]="subscription.name"></ngx-avatar>{{subscription.name}}</a>
|
||||||
|
</ng-container>
|
||||||
</mat-nav-list>
|
</mat-nav-list>
|
||||||
</mat-sidenav>
|
</mat-sidenav>
|
||||||
<mat-sidenav-content [style.background]="postsService.theme ? postsService.theme.background_color : null">
|
<mat-sidenav-content [style.background]="postsService.theme ? postsService.theme.background_color : null">
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { TestBed, async } from '@angular/core/testing';
|
import { TestBed, waitForAsync } from '@angular/core/testing';
|
||||||
|
|
||||||
import { AppComponent } from './app.component';
|
import { AppComponent } from './app.component';
|
||||||
|
|
||||||
describe('AppComponent', () => {
|
describe('AppComponent', () => {
|
||||||
beforeEach(async(() => {
|
beforeEach(waitForAsync(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
declarations: [
|
declarations: [
|
||||||
AppComponent
|
AppComponent
|
||||||
@@ -11,19 +11,19 @@ describe('AppComponent', () => {
|
|||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
it('should create the app', async(() => {
|
it('should create the app', waitForAsync(() => {
|
||||||
const fixture = TestBed.createComponent(AppComponent);
|
const fixture = TestBed.createComponent(AppComponent);
|
||||||
const app = fixture.debugElement.componentInstance;
|
const app = fixture.debugElement.componentInstance;
|
||||||
expect(app).toBeTruthy();
|
expect(app).toBeTruthy();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
it(`should have as title 'app'`, async(() => {
|
it(`should have as title 'app'`, waitForAsync(() => {
|
||||||
const fixture = TestBed.createComponent(AppComponent);
|
const fixture = TestBed.createComponent(AppComponent);
|
||||||
const app = fixture.debugElement.componentInstance;
|
const app = fixture.debugElement.componentInstance;
|
||||||
expect(app.title).toEqual('app');
|
expect(app.title).toEqual('app');
|
||||||
}));
|
}));
|
||||||
|
|
||||||
it('should render title in a h1 tag', async(() => {
|
it('should render title in a h1 tag', waitForAsync(() => {
|
||||||
const fixture = TestBed.createComponent(AppComponent);
|
const fixture = TestBed.createComponent(AppComponent);
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
const compiled = fixture.debugElement.nativeElement;
|
const compiled = fixture.debugElement.nativeElement;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Component, OnInit, ElementRef, ViewChild, HostBinding } from '@angular/core';
|
import { Component, OnInit, ElementRef, ViewChild, HostBinding, AfterViewInit } from '@angular/core';
|
||||||
import {PostsService} from './posts.services';
|
import {PostsService} from './posts.services';
|
||||||
import {FileCardComponent} from './file-card/file-card.component';
|
import {FileCardComponent} from './file-card/file-card.component';
|
||||||
import { Observable } from 'rxjs/Observable';
|
import { Observable } from 'rxjs/Observable';
|
||||||
@@ -21,7 +21,6 @@ import { Router, NavigationStart, NavigationEnd } from '@angular/router';
|
|||||||
import { OverlayContainer } from '@angular/cdk/overlay';
|
import { OverlayContainer } from '@angular/cdk/overlay';
|
||||||
import { THEMES_CONFIG } from '../themes';
|
import { THEMES_CONFIG } from '../themes';
|
||||||
import { SettingsComponent } from './settings/settings.component';
|
import { SettingsComponent } from './settings/settings.component';
|
||||||
import { CheckOrSetPinDialogComponent } from './dialogs/check-or-set-pin-dialog/check-or-set-pin-dialog.component';
|
|
||||||
import { AboutDialogComponent } from './dialogs/about-dialog/about-dialog.component';
|
import { AboutDialogComponent } from './dialogs/about-dialog/about-dialog.component';
|
||||||
import { UserProfileDialogComponent } from './dialogs/user-profile-dialog/user-profile-dialog.component';
|
import { UserProfileDialogComponent } from './dialogs/user-profile-dialog/user-profile-dialog.component';
|
||||||
import { SetDefaultAdminDialogComponent } from './dialogs/set-default-admin-dialog/set-default-admin-dialog.component';
|
import { SetDefaultAdminDialogComponent } from './dialogs/set-default-admin-dialog/set-default-admin-dialog.component';
|
||||||
@@ -31,19 +30,19 @@ import { SetDefaultAdminDialogComponent } from './dialogs/set-default-admin-dial
|
|||||||
templateUrl: './app.component.html',
|
templateUrl: './app.component.html',
|
||||||
styleUrls: ['./app.component.css']
|
styleUrls: ['./app.component.css']
|
||||||
})
|
})
|
||||||
export class AppComponent implements OnInit {
|
export class AppComponent implements OnInit, AfterViewInit {
|
||||||
|
|
||||||
@HostBinding('class') componentCssClass;
|
@HostBinding('class') componentCssClass;
|
||||||
THEMES_CONFIG = THEMES_CONFIG;
|
THEMES_CONFIG = THEMES_CONFIG;
|
||||||
|
|
||||||
|
window = window;
|
||||||
|
|
||||||
// config items
|
// config items
|
||||||
topBarTitle = 'Youtube Downloader';
|
topBarTitle = 'Youtube Downloader';
|
||||||
defaultTheme = null;
|
defaultTheme = null;
|
||||||
allowThemeChange = null;
|
allowThemeChange = null;
|
||||||
allowSubscriptions = false;
|
allowSubscriptions = false;
|
||||||
enableDownloadsManager = false;
|
enableDownloadsManager = false;
|
||||||
// defaults to true to prevent attack
|
|
||||||
settingsPinRequired = true;
|
|
||||||
|
|
||||||
@ViewChild('sidenav') sidenav: MatSidenav;
|
@ViewChild('sidenav') sidenav: MatSidenav;
|
||||||
@ViewChild('hamburgerMenu', { read: ElementRef }) hamburgerMenuButton: ElementRef;
|
@ViewChild('hamburgerMenu', { read: ElementRef }) hamburgerMenuButton: ElementRef;
|
||||||
@@ -72,6 +71,29 @@ export class AppComponent implements OnInit {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
if (localStorage.getItem('theme')) {
|
||||||
|
this.setTheme(localStorage.getItem('theme'));
|
||||||
|
}
|
||||||
|
|
||||||
|
this.postsService.open_create_default_admin_dialog.subscribe(open => {
|
||||||
|
if (open) {
|
||||||
|
const dialogRef = this.dialog.open(SetDefaultAdminDialogComponent);
|
||||||
|
dialogRef.afterClosed().subscribe(success => {
|
||||||
|
if (success) {
|
||||||
|
if (this.router.url !== '/login') { this.router.navigate(['/login']); }
|
||||||
|
} else {
|
||||||
|
console.error('Failed to create default admin account. See logs for details.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ngAfterViewInit() {
|
||||||
|
this.postsService.sidenav = this.sidenav;
|
||||||
|
}
|
||||||
|
|
||||||
toggleSidenav() {
|
toggleSidenav() {
|
||||||
this.sidenav.toggle();
|
this.sidenav.toggle();
|
||||||
}
|
}
|
||||||
@@ -79,7 +101,6 @@ export class AppComponent implements OnInit {
|
|||||||
loadConfig() {
|
loadConfig() {
|
||||||
// loading config
|
// loading config
|
||||||
this.topBarTitle = this.postsService.config['Extra']['title_top'];
|
this.topBarTitle = this.postsService.config['Extra']['title_top'];
|
||||||
this.settingsPinRequired = this.postsService.config['Extra']['settings_pin_required'];
|
|
||||||
const themingExists = this.postsService.config['Themes'];
|
const themingExists = this.postsService.config['Themes'];
|
||||||
this.defaultTheme = themingExists ? this.postsService.config['Themes']['default_theme'] : 'default';
|
this.defaultTheme = themingExists ? this.postsService.config['Themes']['default_theme'] : 'default';
|
||||||
this.allowThemeChange = themingExists ? this.postsService.config['Themes']['allow_theme_change'] : true;
|
this.allowThemeChange = themingExists ? this.postsService.config['Themes']['allow_theme_change'] : true;
|
||||||
@@ -90,6 +111,13 @@ export class AppComponent implements OnInit {
|
|||||||
if (!localStorage.getItem('theme')) {
|
if (!localStorage.getItem('theme')) {
|
||||||
this.setTheme(themingExists ? this.defaultTheme : 'default');
|
this.setTheme(themingExists ? this.defaultTheme : 'default');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// gets the subscriptions
|
||||||
|
if (this.allowSubscriptions) {
|
||||||
|
this.postsService.reloadSubscriptions();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.postsService.reloadCategories();
|
||||||
}
|
}
|
||||||
|
|
||||||
// theme stuff
|
// theme stuff
|
||||||
@@ -121,9 +149,9 @@ export class AppComponent implements OnInit {
|
|||||||
this.postsService.setTheme(theme);
|
this.postsService.setTheme(theme);
|
||||||
|
|
||||||
this.onSetTheme(this.THEMES_CONFIG[theme]['css_label'], old_theme ? this.THEMES_CONFIG[old_theme]['css_label'] : old_theme);
|
this.onSetTheme(this.THEMES_CONFIG[theme]['css_label'], old_theme ? this.THEMES_CONFIG[old_theme]['css_label'] : old_theme);
|
||||||
}
|
}
|
||||||
|
|
||||||
onSetTheme(theme, old_theme) {
|
onSetTheme(theme, old_theme) {
|
||||||
if (old_theme) {
|
if (old_theme) {
|
||||||
document.body.classList.remove(old_theme);
|
document.body.classList.remove(old_theme);
|
||||||
this.overlayContainer.getContainerElement().classList.remove(old_theme);
|
this.overlayContainer.getContainerElement().classList.remove(old_theme);
|
||||||
@@ -145,24 +173,8 @@ onSetTheme(theme, old_theme) {
|
|||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit() {
|
getSubscriptions() {
|
||||||
if (localStorage.getItem('theme')) {
|
|
||||||
this.setTheme(localStorage.getItem('theme'));
|
|
||||||
} else {
|
|
||||||
//
|
|
||||||
}
|
|
||||||
this.postsService.open_create_default_admin_dialog.subscribe(open => {
|
|
||||||
if (open) {
|
|
||||||
const dialogRef = this.dialog.open(SetDefaultAdminDialogComponent);
|
|
||||||
dialogRef.afterClosed().subscribe(success => {
|
|
||||||
if (success) {
|
|
||||||
if (this.router.url !== '/login') { this.router.navigate(['/login']); }
|
|
||||||
} else {
|
|
||||||
console.error('Failed to create default admin account. See logs for details.');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -175,31 +187,11 @@ onSetTheme(theme, old_theme) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
openSettingsDialog() {
|
openSettingsDialog() {
|
||||||
if (this.settingsPinRequired) {
|
|
||||||
this.openPinDialog();
|
|
||||||
} else {
|
|
||||||
this.actuallyOpenSettingsDialog();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
actuallyOpenSettingsDialog() {
|
|
||||||
const dialogRef = this.dialog.open(SettingsComponent, {
|
const dialogRef = this.dialog.open(SettingsComponent, {
|
||||||
width: '80vw'
|
width: '80vw'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
openPinDialog() {
|
|
||||||
const dialogRef = this.dialog.open(CheckOrSetPinDialogComponent, {
|
|
||||||
});
|
|
||||||
|
|
||||||
dialogRef.afterClosed().subscribe(res => {
|
|
||||||
if (res) {
|
|
||||||
this.actuallyOpenSettingsDialog();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
openAboutDialog() {
|
openAboutDialog() {
|
||||||
const dialogRef = this.dialog.open(AboutDialogComponent, {
|
const dialogRef = this.dialog.open(AboutDialogComponent, {
|
||||||
width: '80vw'
|
width: '80vw'
|
||||||
|
|||||||
@@ -32,14 +32,17 @@ import { DragDropModule } from '@angular/cdk/drag-drop';
|
|||||||
import { ClipboardModule } from '@angular/cdk/clipboard';
|
import { ClipboardModule } from '@angular/cdk/clipboard';
|
||||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||||
import { AppComponent } from './app.component';
|
import { AppComponent } from './app.component';
|
||||||
import { HttpClientModule, HttpClient } from '@angular/common/http';
|
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
|
||||||
import { PostsService } from 'app/posts.services';
|
import { PostsService } from 'app/posts.services';
|
||||||
import { FileCardComponent } from './file-card/file-card.component';
|
import { FileCardComponent } from './file-card/file-card.component';
|
||||||
import { RouterModule } from '@angular/router';
|
import { RouterModule } from '@angular/router';
|
||||||
import { AppRoutingModule } from './app-routing.module';
|
import { AppRoutingModule } from './app-routing.module';
|
||||||
import { MainComponent } from './main/main.component';
|
import { MainComponent } from './main/main.component';
|
||||||
import { PlayerComponent } from './player/player.component';
|
import { PlayerComponent } from './player/player.component';
|
||||||
import { VgCoreModule, VgControlsModule, VgOverlayPlayModule, VgBufferingModule } from 'ngx-videogular';
|
import { VgControlsModule } from '@videogular/ngx-videogular/controls';
|
||||||
|
import { VgBufferingModule } from '@videogular/ngx-videogular/buffering';
|
||||||
|
import { VgOverlayPlayModule } from '@videogular/ngx-videogular/overlay-play';
|
||||||
|
import { VgCoreModule } from '@videogular/ngx-videogular/core';
|
||||||
import { InputDialogComponent } from './input-dialog/input-dialog.component';
|
import { InputDialogComponent } from './input-dialog/input-dialog.component';
|
||||||
import { LazyLoadImageModule, IsVisibleProps } from 'ng-lazyload-image';
|
import { LazyLoadImageModule, IsVisibleProps } from 'ng-lazyload-image';
|
||||||
import { audioFilesMouseHovering, videoFilesMouseHovering, audioFilesOpened, videoFilesOpened } from './main/main.component';
|
import { audioFilesMouseHovering, videoFilesMouseHovering, audioFilesOpened, videoFilesOpened } from './main/main.component';
|
||||||
@@ -51,9 +54,10 @@ import { SubscriptionComponent } from './subscription//subscription/subscription
|
|||||||
import { SubscriptionFileCardComponent } from './subscription/subscription-file-card/subscription-file-card.component';
|
import { SubscriptionFileCardComponent } from './subscription/subscription-file-card/subscription-file-card.component';
|
||||||
import { SubscriptionInfoDialogComponent } from './dialogs/subscription-info-dialog/subscription-info-dialog.component';
|
import { SubscriptionInfoDialogComponent } from './dialogs/subscription-info-dialog/subscription-info-dialog.component';
|
||||||
import { SettingsComponent } from './settings/settings.component';
|
import { SettingsComponent } from './settings/settings.component';
|
||||||
import { CheckOrSetPinDialogComponent } from './dialogs/check-or-set-pin-dialog/check-or-set-pin-dialog.component';
|
|
||||||
import { MatChipsModule } from '@angular/material/chips';
|
import { MatChipsModule } from '@angular/material/chips';
|
||||||
import { NgxFileDropModule } from 'ngx-file-drop';
|
import { NgxFileDropModule } from 'ngx-file-drop';
|
||||||
|
import { AvatarModule } from 'ngx-avatar';
|
||||||
|
import { ContentLoaderModule } from '@ngneat/content-loader';
|
||||||
|
|
||||||
import es from '@angular/common/locales/es';
|
import es from '@angular/common/locales/es';
|
||||||
import { AboutDialogComponent } from './dialogs/about-dialog/about-dialog.component';
|
import { AboutDialogComponent } from './dialogs/about-dialog/about-dialog.component';
|
||||||
@@ -71,6 +75,18 @@ import { AddUserDialogComponent } from './dialogs/add-user-dialog/add-user-dialo
|
|||||||
import { ManageUserComponent } from './components/manage-user/manage-user.component';
|
import { ManageUserComponent } from './components/manage-user/manage-user.component';
|
||||||
import { ManageRoleComponent } from './components/manage-role/manage-role.component';
|
import { ManageRoleComponent } from './components/manage-role/manage-role.component';
|
||||||
import { CookiesUploaderDialogComponent } from './dialogs/cookies-uploader-dialog/cookies-uploader-dialog.component';
|
import { CookiesUploaderDialogComponent } from './dialogs/cookies-uploader-dialog/cookies-uploader-dialog.component';
|
||||||
|
import { LogsViewerComponent } from './components/logs-viewer/logs-viewer.component';
|
||||||
|
import { ModifyPlaylistComponent } from './dialogs/modify-playlist/modify-playlist.component';
|
||||||
|
import { ConfirmDialogComponent } from './dialogs/confirm-dialog/confirm-dialog.component';
|
||||||
|
import { UnifiedFileCardComponent } from './components/unified-file-card/unified-file-card.component';
|
||||||
|
import { RecentVideosComponent } from './components/recent-videos/recent-videos.component';
|
||||||
|
import { EditSubscriptionDialogComponent } from './dialogs/edit-subscription-dialog/edit-subscription-dialog.component';
|
||||||
|
import { CustomPlaylistsComponent } from './components/custom-playlists/custom-playlists.component';
|
||||||
|
import { EditCategoryDialogComponent } from './dialogs/edit-category-dialog/edit-category-dialog.component';
|
||||||
|
import { TwitchChatComponent } from './components/twitch-chat/twitch-chat.component';
|
||||||
|
import { LinkifyPipe, SeeMoreComponent } from './components/see-more/see-more.component';
|
||||||
|
import { H401Interceptor } from './http.interceptor';
|
||||||
|
import { ConcurrentStreamComponent } from './components/concurrent-stream/concurrent-stream.component';
|
||||||
|
|
||||||
registerLocaleData(es, 'es');
|
registerLocaleData(es, 'es');
|
||||||
|
|
||||||
@@ -93,11 +109,11 @@ export function isVisible({ event, element, scrollContainer, offset }: IsVisible
|
|||||||
SubscriptionFileCardComponent,
|
SubscriptionFileCardComponent,
|
||||||
SubscriptionInfoDialogComponent,
|
SubscriptionInfoDialogComponent,
|
||||||
SettingsComponent,
|
SettingsComponent,
|
||||||
CheckOrSetPinDialogComponent,
|
|
||||||
AboutDialogComponent,
|
AboutDialogComponent,
|
||||||
VideoInfoDialogComponent,
|
VideoInfoDialogComponent,
|
||||||
ArgModifierDialogComponent,
|
ArgModifierDialogComponent,
|
||||||
HighlightPipe,
|
HighlightPipe,
|
||||||
|
LinkifyPipe,
|
||||||
UpdaterComponent,
|
UpdaterComponent,
|
||||||
UpdateProgressDialogComponent,
|
UpdateProgressDialogComponent,
|
||||||
ShareMediaDialogComponent,
|
ShareMediaDialogComponent,
|
||||||
@@ -109,7 +125,18 @@ export function isVisible({ event, element, scrollContainer, offset }: IsVisible
|
|||||||
AddUserDialogComponent,
|
AddUserDialogComponent,
|
||||||
ManageUserComponent,
|
ManageUserComponent,
|
||||||
ManageRoleComponent,
|
ManageRoleComponent,
|
||||||
CookiesUploaderDialogComponent
|
CookiesUploaderDialogComponent,
|
||||||
|
LogsViewerComponent,
|
||||||
|
ModifyPlaylistComponent,
|
||||||
|
ConfirmDialogComponent,
|
||||||
|
UnifiedFileCardComponent,
|
||||||
|
RecentVideosComponent,
|
||||||
|
EditSubscriptionDialogComponent,
|
||||||
|
CustomPlaylistsComponent,
|
||||||
|
EditCategoryDialogComponent,
|
||||||
|
TwitchChatComponent,
|
||||||
|
SeeMoreComponent,
|
||||||
|
ConcurrentStreamComponent
|
||||||
],
|
],
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
@@ -139,7 +166,6 @@ export function isVisible({ event, element, scrollContainer, offset }: IsVisible
|
|||||||
MatMenuModule,
|
MatMenuModule,
|
||||||
MatDialogModule,
|
MatDialogModule,
|
||||||
MatSlideToggleModule,
|
MatSlideToggleModule,
|
||||||
MatMenuModule,
|
|
||||||
MatAutocompleteModule,
|
MatAutocompleteModule,
|
||||||
MatTabsModule,
|
MatTabsModule,
|
||||||
MatTooltipModule,
|
MatTooltipModule,
|
||||||
@@ -150,6 +176,8 @@ export function isVisible({ event, element, scrollContainer, offset }: IsVisible
|
|||||||
DragDropModule,
|
DragDropModule,
|
||||||
ClipboardModule,
|
ClipboardModule,
|
||||||
NgxFileDropModule,
|
NgxFileDropModule,
|
||||||
|
AvatarModule,
|
||||||
|
ContentLoaderModule,
|
||||||
VgCoreModule,
|
VgCoreModule,
|
||||||
VgControlsModule,
|
VgControlsModule,
|
||||||
VgOverlayPlayModule,
|
VgOverlayPlayModule,
|
||||||
@@ -163,14 +191,15 @@ export function isVisible({ event, element, scrollContainer, offset }: IsVisible
|
|||||||
CreatePlaylistComponent,
|
CreatePlaylistComponent,
|
||||||
SubscribeDialogComponent,
|
SubscribeDialogComponent,
|
||||||
SubscriptionInfoDialogComponent,
|
SubscriptionInfoDialogComponent,
|
||||||
SettingsComponent,
|
SettingsComponent
|
||||||
CheckOrSetPinDialogComponent
|
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
PostsService
|
PostsService,
|
||||||
|
{ provide: HTTP_INTERCEPTORS, useClass: H401Interceptor, multi: true }
|
||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
HighlightPipe
|
HighlightPipe,
|
||||||
|
LinkifyPipe
|
||||||
],
|
],
|
||||||
bootstrap: [AppComponent]
|
bootstrap: [AppComponent]
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<div class="buttons-container">
|
||||||
|
<button (click)="startWatching()" *ngIf="!watch_together_clicked" mat-flat-button>Watch together</button>
|
||||||
|
<button (click)="startServer()" *ngIf="watch_together_clicked && !started && server_mode && server_already_exists === false" mat-flat-button>Start stream</button>
|
||||||
|
<button (click)="startClient()" *ngIf="watch_together_clicked && !started && server_already_exists === true" mat-flat-button>Join stream</button>
|
||||||
|
<button style="margin-left: 10px;" (click)="stop()" *ngIf="watch_together_clicked" mat-flat-button>Stop</button>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
.buttons-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: 15px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { ConcurrentStreamComponent } from './concurrent-stream.component';
|
||||||
|
|
||||||
|
describe('ConcurrentStreamComponent', () => {
|
||||||
|
let component: ConcurrentStreamComponent;
|
||||||
|
let fixture: ComponentFixture<ConcurrentStreamComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
declarations: [ ConcurrentStreamComponent ]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(ConcurrentStreamComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
|
||||||
|
import { PostsService } from 'app/posts.services';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-concurrent-stream',
|
||||||
|
templateUrl: './concurrent-stream.component.html',
|
||||||
|
styleUrls: ['./concurrent-stream.component.scss']
|
||||||
|
})
|
||||||
|
export class ConcurrentStreamComponent implements OnInit {
|
||||||
|
|
||||||
|
@Input() server_mode = false;
|
||||||
|
@Input() playback_timestamp;
|
||||||
|
@Input() playing;
|
||||||
|
@Input() uid;
|
||||||
|
|
||||||
|
@Output() setPlaybackTimestamp = new EventEmitter<any>();
|
||||||
|
@Output() togglePlayback = new EventEmitter<boolean>();
|
||||||
|
@Output() setPlaybackRate = new EventEmitter<number>();
|
||||||
|
|
||||||
|
started = false;
|
||||||
|
server_started = false;
|
||||||
|
watch_together_clicked = false;
|
||||||
|
|
||||||
|
server_already_exists = null;
|
||||||
|
|
||||||
|
check_timeout: any;
|
||||||
|
update_timeout: any;
|
||||||
|
|
||||||
|
PLAYBACK_TIMESTAMP_DIFFERENCE_THRESHOLD_PLAYBACK_MODIFICATION = 0.5;
|
||||||
|
PLAYBACK_TIMESTAMP_DIFFERENCE_THRESHOLD_SKIP = 2;
|
||||||
|
|
||||||
|
PLAYBACK_MODIFIER = 0.1;
|
||||||
|
|
||||||
|
playback_rate_modified = false;
|
||||||
|
|
||||||
|
constructor(private postsService: PostsService) { }
|
||||||
|
|
||||||
|
// flow: click start watching -> check for available stream to enable join button and if user, display "start stream"
|
||||||
|
// users who join a stream will send continuous requests for info on playback
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
startServer() {
|
||||||
|
this.started = true;
|
||||||
|
this.server_started = true;
|
||||||
|
this.update_timeout = setInterval(() => {
|
||||||
|
this.updateStream();
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateStream() {
|
||||||
|
this.postsService.updateConcurrentStream(this.uid, this.playback_timestamp, Date.now()/1000, this.playing).subscribe(res => {
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
startClient() {
|
||||||
|
this.started = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
checkStream() {
|
||||||
|
if (this.server_started) { return; }
|
||||||
|
const current_playback_timestamp = this.playback_timestamp;
|
||||||
|
const current_unix_timestamp = Date.now()/1000;
|
||||||
|
this.postsService.checkConcurrentStream(this.uid).subscribe(res => {
|
||||||
|
const stream = res['stream'];
|
||||||
|
|
||||||
|
if (!stream) {
|
||||||
|
this.server_already_exists = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.server_already_exists = true;
|
||||||
|
|
||||||
|
// check whether client has joined the stream
|
||||||
|
if (!this.started) { return; }
|
||||||
|
|
||||||
|
if (!stream['playing'] && this.playing) {
|
||||||
|
// tell client to pause and set the timestamp to sync
|
||||||
|
this.togglePlayback.emit(false);
|
||||||
|
this.setPlaybackTimestamp.emit(stream['playback_timestamp']);
|
||||||
|
} else if (stream['playing']) {
|
||||||
|
// sync unpause state
|
||||||
|
if (!this.playing) { this.togglePlayback.emit(true); }
|
||||||
|
|
||||||
|
// sync time
|
||||||
|
const zeroed_local_unix_timestamp = current_unix_timestamp - current_playback_timestamp;
|
||||||
|
const zeroed_server_unix_timestamp = stream['unix_timestamp'] - stream['playback_timestamp'];
|
||||||
|
|
||||||
|
const seconds_behind_locally = zeroed_local_unix_timestamp - zeroed_server_unix_timestamp;
|
||||||
|
|
||||||
|
if (Math.abs(seconds_behind_locally) > this.PLAYBACK_TIMESTAMP_DIFFERENCE_THRESHOLD_SKIP) {
|
||||||
|
// skip to playback timestamp because the difference is too high
|
||||||
|
this.setPlaybackTimestamp.emit(this.playback_timestamp + seconds_behind_locally + 0.3);
|
||||||
|
this.playback_rate_modified = false;
|
||||||
|
} else if (!this.playback_rate_modified && Math.abs(seconds_behind_locally) > this.PLAYBACK_TIMESTAMP_DIFFERENCE_THRESHOLD_PLAYBACK_MODIFICATION) {
|
||||||
|
// increase playback speed to avoid skipping
|
||||||
|
let seconds_to_wait = (Math.abs(seconds_behind_locally)/this.PLAYBACK_MODIFIER);
|
||||||
|
seconds_to_wait += 0.3/this.PLAYBACK_MODIFIER;
|
||||||
|
|
||||||
|
this.playback_rate_modified = true;
|
||||||
|
|
||||||
|
if (seconds_behind_locally > 0) {
|
||||||
|
// increase speed
|
||||||
|
this.setPlaybackRate.emit(1 + this.PLAYBACK_MODIFIER);
|
||||||
|
setTimeout(() => {
|
||||||
|
this.setPlaybackRate.emit(1);
|
||||||
|
this.playback_rate_modified = false;
|
||||||
|
}, seconds_to_wait * 1000);
|
||||||
|
} else {
|
||||||
|
// decrease speed
|
||||||
|
this.setPlaybackRate.emit(1 - this.PLAYBACK_MODIFIER);
|
||||||
|
setTimeout(() => {
|
||||||
|
this.setPlaybackRate.emit(1);
|
||||||
|
this.playback_rate_modified = false;
|
||||||
|
}, seconds_to_wait * 1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
startWatching() {
|
||||||
|
this.watch_together_clicked = true;
|
||||||
|
this.check_timeout = setInterval(() => {
|
||||||
|
this.checkStream();
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
stop() {
|
||||||
|
if (this.check_timeout) { clearInterval(this.check_timeout); }
|
||||||
|
if (this.update_timeout) { clearInterval(this.update_timeout); }
|
||||||
|
this.started = false;
|
||||||
|
this.server_started = false;
|
||||||
|
this.watch_together_clicked = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<div *ngIf="playlists && playlists.length > 0">
|
||||||
|
<div class="container">
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div *ngFor="let playlist of playlists; let i = index" class="mb-2 mt-2" [ngClass]="[ postsService.card_size === 'small' ? 'col-2 small-col' : '', postsService.card_size === 'medium' ? 'col-6 col-lg-4 medium-col' : '', postsService.card_size === 'large' ? 'col-12 large-col' : '' ]">
|
||||||
|
<app-unified-file-card [index]="i" [card_size]="postsService.card_size" [locale]="postsService.locale" (goToFile)="goToPlaylist($event)" [file_obj]="playlist" [is_playlist]="true" (editPlaylist)="editPlaylistDialog($event)" (deleteFile)="deletePlaylist($event)" [loading]="false"></app-unified-file-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div *ngIf="playlists && playlists.length === 0" style="text-align: center;">
|
||||||
|
No playlists available. Create one from your downloading files by clicking the blue plus button.
|
||||||
|
</div>
|
||||||
|
<div class="add-playlist-button"><button (click)="openCreatePlaylistDialog()" mat-fab><mat-icon>add</mat-icon></button></div>
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
.add-playlist-button {
|
||||||
|
float: right;
|
||||||
|
position: relative;
|
||||||
|
bottom: 15px;
|
||||||
|
right: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.large-col {
|
||||||
|
max-width: 320px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.medium-col {
|
||||||
|
max-width: 240px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.small-col {
|
||||||
|
max-width: 240px;
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { CustomPlaylistsComponent } from './custom-playlists.component';
|
||||||
|
|
||||||
|
describe('CustomPlaylistsComponent', () => {
|
||||||
|
let component: CustomPlaylistsComponent;
|
||||||
|
let fixture: ComponentFixture<CustomPlaylistsComponent>;
|
||||||
|
|
||||||
|
beforeEach(waitForAsync(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [ CustomPlaylistsComponent ]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(CustomPlaylistsComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
import { PostsService } from 'app/posts.services';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
import { MatDialog } from '@angular/material/dialog';
|
||||||
|
import { CreatePlaylistComponent } from 'app/create-playlist/create-playlist.component';
|
||||||
|
import { ModifyPlaylistComponent } from 'app/dialogs/modify-playlist/modify-playlist.component';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-custom-playlists',
|
||||||
|
templateUrl: './custom-playlists.component.html',
|
||||||
|
styleUrls: ['./custom-playlists.component.scss']
|
||||||
|
})
|
||||||
|
export class CustomPlaylistsComponent implements OnInit {
|
||||||
|
|
||||||
|
playlists = null;
|
||||||
|
playlists_received = false;
|
||||||
|
downloading_content = {'video': {}, 'audio': {}};
|
||||||
|
|
||||||
|
constructor(public postsService: PostsService, private router: Router, private dialog: MatDialog) { }
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.postsService.service_initialized.subscribe(init => {
|
||||||
|
if (init) {
|
||||||
|
this.getAllPlaylists();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getAllPlaylists() {
|
||||||
|
this.playlists_received = false;
|
||||||
|
this.postsService.getAllFiles().subscribe(res => {
|
||||||
|
this.playlists = res['playlists'];
|
||||||
|
this.playlists_received = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// creating a playlist
|
||||||
|
openCreatePlaylistDialog() {
|
||||||
|
const dialogRef = this.dialog.open(CreatePlaylistComponent, {
|
||||||
|
data: {
|
||||||
|
}
|
||||||
|
});
|
||||||
|
dialogRef.afterClosed().subscribe(result => {
|
||||||
|
if (result) {
|
||||||
|
this.getAllPlaylists();
|
||||||
|
this.postsService.openSnackBar('Successfully created playlist!', '');
|
||||||
|
} else if (result === false) {
|
||||||
|
this.postsService.openSnackBar('ERROR: failed to create playlist!', '');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
goToPlaylist(info_obj) {
|
||||||
|
const playlist = info_obj.file;
|
||||||
|
const playlistID = playlist.id;
|
||||||
|
|
||||||
|
if (playlist) {
|
||||||
|
if (this.postsService.config['Extra']['download_only_mode']) {
|
||||||
|
this.downloadPlaylist(playlist.id, playlist.name);
|
||||||
|
} else {
|
||||||
|
localStorage.setItem('player_navigator', this.router.url);
|
||||||
|
const routeParams = {playlist_id: playlistID};
|
||||||
|
if (playlist.auto) { routeParams['auto'] = playlist.auto; }
|
||||||
|
this.router.navigate(['/player', routeParams]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// playlist not found
|
||||||
|
console.error(`Playlist with ID ${playlistID} not found!`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
downloadPlaylist(playlist_id, playlist_name) {
|
||||||
|
this.downloading_content[playlist_id] = true;
|
||||||
|
this.postsService.downloadPlaylistFromServer(playlist_id).subscribe(res => {
|
||||||
|
this.downloading_content[playlist_id] = false;
|
||||||
|
const blob: any = res;
|
||||||
|
saveAs(blob, playlist_name + '.zip');
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
deletePlaylist(args) {
|
||||||
|
const playlist = args.file;
|
||||||
|
const index = args.index;
|
||||||
|
const playlistID = playlist.id;
|
||||||
|
this.postsService.removePlaylist(playlistID, playlist.type).subscribe(res => {
|
||||||
|
if (res['success']) {
|
||||||
|
this.playlists.splice(index, 1);
|
||||||
|
this.postsService.openSnackBar('Playlist successfully removed.', '');
|
||||||
|
}
|
||||||
|
this.getAllPlaylists();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
editPlaylistDialog(args) {
|
||||||
|
const playlist = args.playlist;
|
||||||
|
const index = args.index;
|
||||||
|
const dialogRef = this.dialog.open(ModifyPlaylistComponent, {
|
||||||
|
data: {
|
||||||
|
playlist_id: playlist.id,
|
||||||
|
width: '65vw'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
dialogRef.afterClosed().subscribe(res => {
|
||||||
|
// updates playlist in file manager if it changed
|
||||||
|
if (dialogRef.componentInstance.playlist_updated) {
|
||||||
|
this.playlists[index] = dialogRef.componentInstance.original_playlist;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,19 +1,22 @@
|
|||||||
<div style="padding: 20px;">
|
<div style="padding: 20px;">
|
||||||
<div *ngFor="let session_downloads of downloads | keyvalue">
|
<div *ngFor="let session_downloads of downloads">
|
||||||
<ng-container *ngIf="keys(session_downloads.value).length > 0">
|
<ng-container *ngIf="keys(session_downloads).length > 2">
|
||||||
<mat-card style="padding-bottom: 30px; margin-bottom: 15px;">
|
<mat-card style="padding-bottom: 30px; margin-bottom: 15px;">
|
||||||
<h4 style="text-align: center;"><ng-container i18n="Session ID">Session ID:</ng-container> {{session_downloads.key}}
|
<h4 style="text-align: center;"><ng-container i18n="Session ID">Session ID:</ng-container> {{session_downloads['session_id']}}
|
||||||
<span *ngIf="session_downloads.key === postsService.session_id"> <ng-container i18n="Current session">(current)</ng-container></span>
|
<span *ngIf="session_downloads['session_id'] === postsService.session_id"> <ng-container i18n="Current session">(current)</ng-container></span>
|
||||||
</h4>
|
</h4>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div *ngFor="let download of session_downloads.value | keyvalue: sort_downloads; let i = index;" class="col-12 my-1">
|
<div *ngFor="let download of session_downloads | keyvalue: sort_downloads; let i = index;" class="col-12 my-1">
|
||||||
<mat-card *ngIf="download.value" class="mat-elevation-z3">
|
<mat-card *ngIf="download.key !== 'session_id' && download.key !== '_id' && download.value" class="mat-elevation-z3">
|
||||||
<app-download-item [download]="download.value" [queueNumber]="i+1" (cancelDownload)="clearDownload(session_downloads.key, download.value.uid)"></app-download-item>
|
<app-download-item [download]="download.value" [queueNumber]="i+1" (cancelDownload)="clearDownload(session_downloads['session_id'], download.value.uid)"></app-download-item>
|
||||||
</mat-card>
|
</mat-card>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<button style="top: 15px;" (click)="clearDownloads(session_downloads['session_id'])" mat-stroked-button color="warn"><ng-container i18n="clear all downloads action button">Clear all downloads</ng-container></button>
|
||||||
|
</div>
|
||||||
</mat-card>
|
</mat-card>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||||
|
|
||||||
import { DownloadsComponent } from './downloads.component';
|
import { DownloadsComponent } from './downloads.component';
|
||||||
|
|
||||||
@@ -6,7 +6,7 @@ describe('DownloadsComponent', () => {
|
|||||||
let component: DownloadsComponent;
|
let component: DownloadsComponent;
|
||||||
let fixture: ComponentFixture<DownloadsComponent>;
|
let fixture: ComponentFixture<DownloadsComponent>;
|
||||||
|
|
||||||
beforeEach(async(() => {
|
beforeEach(waitForAsync(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
declarations: [ DownloadsComponent ]
|
declarations: [ DownloadsComponent ]
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ import { Router } from '@angular/router';
|
|||||||
export class DownloadsComponent implements OnInit, OnDestroy {
|
export class DownloadsComponent implements OnInit, OnDestroy {
|
||||||
|
|
||||||
downloads_check_interval = 1000;
|
downloads_check_interval = 1000;
|
||||||
downloads = {};
|
downloads = [];
|
||||||
interval_id = null;
|
interval_id = null;
|
||||||
|
|
||||||
keys = Object.keys;
|
keys = Object.keys;
|
||||||
@@ -43,7 +43,7 @@ export class DownloadsComponent implements OnInit, OnDestroy {
|
|||||||
valid_sessions_length = 0;
|
valid_sessions_length = 0;
|
||||||
|
|
||||||
sort_downloads = (a, b) => {
|
sort_downloads = (a, b) => {
|
||||||
const result = a.value.timestamp_start < b.value.timestamp_start;
|
const result = b.value.timestamp_start - a.value.timestamp_start;
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,7 +81,7 @@ export class DownloadsComponent implements OnInit, OnDestroy {
|
|||||||
clearDownload(session_id, download_uid) {
|
clearDownload(session_id, download_uid) {
|
||||||
this.postsService.clearDownloads(false, session_id, download_uid).subscribe(res => {
|
this.postsService.clearDownloads(false, session_id, download_uid).subscribe(res => {
|
||||||
if (res['success']) {
|
if (res['success']) {
|
||||||
this.downloads = res['downloads'];
|
// this.downloads = res['downloads'];
|
||||||
} else {
|
} else {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -107,15 +107,37 @@ export class DownloadsComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
assignNewValues(new_downloads_by_session) {
|
assignNewValues(new_downloads_by_session) {
|
||||||
const session_keys = Object.keys(new_downloads_by_session);
|
const session_keys = Object.keys(new_downloads_by_session);
|
||||||
|
|
||||||
|
// remove missing session IDs
|
||||||
|
const current_session_ids = Object.keys(this.downloads);
|
||||||
|
const missing_session_ids = current_session_ids.filter(session => session_keys.indexOf(session) === -1)
|
||||||
|
|
||||||
|
for (const missing_session_id of missing_session_ids) {
|
||||||
|
delete this.downloads[missing_session_id];
|
||||||
|
}
|
||||||
|
|
||||||
|
// loop through sessions
|
||||||
for (let i = 0; i < session_keys.length; i++) {
|
for (let i = 0; i < session_keys.length; i++) {
|
||||||
const session_id = session_keys[i];
|
const session_id = session_keys[i];
|
||||||
const session_downloads_by_id = new_downloads_by_session[session_id];
|
const session_downloads_by_id = new_downloads_by_session[session_id];
|
||||||
const session_download_ids = Object.keys(session_downloads_by_id);
|
const session_download_ids = Object.keys(session_downloads_by_id);
|
||||||
|
|
||||||
|
if (this.downloads[session_id]) {
|
||||||
|
// remove missing download IDs
|
||||||
|
const current_download_ids = Object.keys(this.downloads[session_id]);
|
||||||
|
const missing_download_ids = current_download_ids.filter(download => session_download_ids.indexOf(download) === -1)
|
||||||
|
|
||||||
|
for (const missing_download_id of missing_download_ids) {
|
||||||
|
console.log('removing missing download id');
|
||||||
|
delete this.downloads[session_id][missing_download_id];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!this.downloads[session_id]) {
|
if (!this.downloads[session_id]) {
|
||||||
this.downloads[session_id] = session_downloads_by_id;
|
this.downloads[session_id] = session_downloads_by_id;
|
||||||
} else {
|
} else {
|
||||||
for (let j = 0; j < session_download_ids.length; j++) {
|
for (let j = 0; j < session_download_ids.length; j++) {
|
||||||
|
if (session_download_ids[j] === 'session_id' || session_download_ids[j] === '_id') continue;
|
||||||
const download_id = session_download_ids[j];
|
const download_id = session_download_ids[j];
|
||||||
const download = new_downloads_by_session[session_id][download_id]
|
const download = new_downloads_by_session[session_id][download_id]
|
||||||
if (!this.downloads[session_id][download_id]) {
|
if (!this.downloads[session_id][download_id]) {
|
||||||
@@ -135,11 +157,10 @@ export class DownloadsComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
downloadsValid() {
|
downloadsValid() {
|
||||||
let valid = false;
|
let valid = false;
|
||||||
const keys = this.keys(this.downloads);
|
for (let i = 0; i < this.downloads.length; i++) {
|
||||||
for (let i = 0; i < keys.length; i++) {
|
const session_downloads = this.downloads[i];
|
||||||
const key = keys[i];
|
if (!session_downloads) continue;
|
||||||
const value = this.downloads[key];
|
if (this.keys(session_downloads).length > 2) {
|
||||||
if (this.keys(value).length > 0) {
|
|
||||||
valid = true;
|
valid = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||||
|
|
||||||
import { LoginComponent } from './login.component';
|
import { LoginComponent } from './login.component';
|
||||||
|
|
||||||
@@ -6,7 +6,7 @@ describe('LoginComponent', () => {
|
|||||||
let component: LoginComponent;
|
let component: LoginComponent;
|
||||||
let fixture: ComponentFixture<LoginComponent>;
|
let fixture: ComponentFixture<LoginComponent>;
|
||||||
|
|
||||||
beforeEach(async(() => {
|
beforeEach(waitForAsync(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
declarations: [ LoginComponent ]
|
declarations: [ LoginComponent ]
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ export class LoginComponent implements OnInit {
|
|||||||
constructor(private postsService: PostsService, private snackBar: MatSnackBar, private router: Router) { }
|
constructor(private postsService: PostsService, private snackBar: MatSnackBar, private router: Router) { }
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
if (this.postsService.isLoggedIn) {
|
if (this.postsService.isLoggedIn && localStorage.getItem('jwt_token') !== 'null') {
|
||||||
this.router.navigate(['/home']);
|
this.router.navigate(['/home']);
|
||||||
}
|
}
|
||||||
this.postsService.service_initialized.subscribe(init => {
|
this.postsService.service_initialized.subscribe(init => {
|
||||||
@@ -49,9 +49,19 @@ export class LoginComponent implements OnInit {
|
|||||||
this.loggingIn = false;
|
this.loggingIn = false;
|
||||||
if (res['token']) {
|
if (res['token']) {
|
||||||
this.postsService.afterLogin(res['user'], res['token'], res['permissions'], res['available_permissions']);
|
this.postsService.afterLogin(res['user'], res['token'], res['permissions'], res['available_permissions']);
|
||||||
|
} else {
|
||||||
|
this.openSnackBar('Login failed, unknown error.');
|
||||||
}
|
}
|
||||||
}, err => {
|
}, err => {
|
||||||
this.loggingIn = false;
|
this.loggingIn = false;
|
||||||
|
const error_code = err.status;
|
||||||
|
if (error_code === 401) {
|
||||||
|
this.openSnackBar('User name or password is incorrect!');
|
||||||
|
} else if (error_code === 404) {
|
||||||
|
this.openSnackBar('Login failed, cannot connect to the server.');
|
||||||
|
} else {
|
||||||
|
this.openSnackBar('Login failed, unknown error.');
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,7 +94,7 @@ export class LoginComponent implements OnInit {
|
|||||||
this.loginUsernameInput = res['user']['name'];
|
this.loginUsernameInput = res['user']['name'];
|
||||||
this.selectedTabIndex = 0;
|
this.selectedTabIndex = 0;
|
||||||
} else {
|
} else {
|
||||||
|
this.openSnackBar('Failed to register user, unknown error.');
|
||||||
}
|
}
|
||||||
}, err => {
|
}, err => {
|
||||||
this.registering = false;
|
this.registering = false;
|
||||||
|
|||||||
37
src/app/components/logs-viewer/logs-viewer.component.html
Normal file
37
src/app/components/logs-viewer/logs-viewer.component.html
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
<div style="height: 275px;">
|
||||||
|
<div *ngIf="logs_loading" style="z-index: 999; position: absolute; top: 40%; left: 50%">
|
||||||
|
<mat-spinner [diameter]="32"></mat-spinner>
|
||||||
|
</div>
|
||||||
|
<!-- Virtual mode (fast, select text buggy) -->
|
||||||
|
<!--<cdk-virtual-scroll-viewport style="height: 274px;" itemSize="50" class="example-viewport">
|
||||||
|
<div *cdkVirtualFor="let log of logs; let i = index" class="example-item">
|
||||||
|
<span [ngStyle]="{'color':log.color}">{{log.text}}</span>
|
||||||
|
</div>
|
||||||
|
</cdk-virtual-scroll-viewport>-->
|
||||||
|
|
||||||
|
<!-- Non-virtual mode (slow, bug-free) -->
|
||||||
|
<div style="height: 274px; overflow-y: auto">
|
||||||
|
<div *ngFor="let log of logs; let i = index" class="example-item">
|
||||||
|
<span [ngStyle]="{'color':log.color}">{{log.text}}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<button style="position: absolute; right: 0px; top: 12px;" [cdkCopyToClipboard]="logs_text" (click)="copiedLogsToClipboard()" mat-mini-fab color="primary"><mat-icon style="font-size: 22px !important;">content_copy</mat-icon></button>
|
||||||
|
<div style="display: inline-block;">
|
||||||
|
<ng-container i18n="Label for lines select in logger view">Lines:</ng-container>
|
||||||
|
<mat-form-field style="width: 75px;">
|
||||||
|
<mat-select (selectionChange)="getLogs()" [(ngModel)]="requested_lines">
|
||||||
|
<mat-option [value]="10">10</mat-option>
|
||||||
|
<mat-option [value]="25">25</mat-option>
|
||||||
|
<mat-option [value]="50">50</mat-option>
|
||||||
|
<mat-option [value]="100">100</mat-option>
|
||||||
|
<mat-option [value]="0">All</mat-option>
|
||||||
|
</mat-select>
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
<span class="spacer"></span>
|
||||||
|
<button style="float: right; margin-top: 12px;" (click)="clearLogs()" mat-stroked-button color="warn"><ng-container i18n="Clear logs button">Clear logs</ng-container></button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
.spacer {flex: 1 1 auto;}
|
||||||
25
src/app/components/logs-viewer/logs-viewer.component.spec.ts
Normal file
25
src/app/components/logs-viewer/logs-viewer.component.spec.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { LogsViewerComponent } from './logs-viewer.component';
|
||||||
|
|
||||||
|
describe('LogsViewerComponent', () => {
|
||||||
|
let component: LogsViewerComponent;
|
||||||
|
let fixture: ComponentFixture<LogsViewerComponent>;
|
||||||
|
|
||||||
|
beforeEach(waitForAsync(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [ LogsViewerComponent ]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(LogsViewerComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
86
src/app/components/logs-viewer/logs-viewer.component.ts
Normal file
86
src/app/components/logs-viewer/logs-viewer.component.ts
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
import { PostsService } from '../../posts.services';
|
||||||
|
import { MatDialog } from '@angular/material/dialog';
|
||||||
|
import { ConfirmDialogComponent } from 'app/dialogs/confirm-dialog/confirm-dialog.component';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-logs-viewer',
|
||||||
|
templateUrl: './logs-viewer.component.html',
|
||||||
|
styleUrls: ['./logs-viewer.component.scss']
|
||||||
|
})
|
||||||
|
export class LogsViewerComponent implements OnInit {
|
||||||
|
|
||||||
|
logs: any = null;
|
||||||
|
logs_text: string = null;
|
||||||
|
requested_lines = 50;
|
||||||
|
logs_loading = false;
|
||||||
|
constructor(private postsService: PostsService, private dialog: MatDialog) { }
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.getLogs();
|
||||||
|
}
|
||||||
|
|
||||||
|
getLogs() {
|
||||||
|
if (!this.logs) { this.logs_loading = true; } // only show loading spinner at the first load
|
||||||
|
this.postsService.getLogs(this.requested_lines !== 0 ? this.requested_lines : null).subscribe(res => {
|
||||||
|
this.logs_loading = false;
|
||||||
|
if (res['logs'] !== null || res['logs'] !== undefined) {
|
||||||
|
this.logs_text = res['logs'];
|
||||||
|
this.logs = [];
|
||||||
|
const logs_arr = res['logs'].split('\n');
|
||||||
|
logs_arr.forEach(log_line => {
|
||||||
|
let color = 'inherit'
|
||||||
|
if (log_line.includes('ERROR')) {
|
||||||
|
color = 'red';
|
||||||
|
} else if (log_line.includes('WARN')) {
|
||||||
|
color = 'yellow';
|
||||||
|
} else if (log_line.includes('VERBOSE')) {
|
||||||
|
color = 'gray';
|
||||||
|
}
|
||||||
|
this.logs.push({
|
||||||
|
text: log_line,
|
||||||
|
color: color
|
||||||
|
})
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.postsService.openSnackBar('Failed to retrieve logs!');
|
||||||
|
}
|
||||||
|
}, err => {
|
||||||
|
this.logs_loading = false;
|
||||||
|
console.error(err);
|
||||||
|
this.postsService.openSnackBar('Failed to retrieve logs!');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
copiedLogsToClipboard() {
|
||||||
|
this.postsService.openSnackBar('Logs copied to clipboard!');
|
||||||
|
}
|
||||||
|
|
||||||
|
clearLogs() {
|
||||||
|
const dialogRef = this.dialog.open(ConfirmDialogComponent, {
|
||||||
|
data: {
|
||||||
|
dialogTitle: 'Clear logs',
|
||||||
|
dialogText: 'Would you like to clear your logs? This will delete all your current logs, permanently.',
|
||||||
|
submitText: 'Clear',
|
||||||
|
warnSubmitColor: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
dialogRef.afterClosed().subscribe(confirmed => {
|
||||||
|
if (confirmed) {
|
||||||
|
this.postsService.clearAllLogs().subscribe(res => {
|
||||||
|
if (res['success']) {
|
||||||
|
this.logs = [];
|
||||||
|
this.logs_text = '';
|
||||||
|
this.getLogs();
|
||||||
|
this.postsService.openSnackBar('Logs successfully cleared!');
|
||||||
|
} else {
|
||||||
|
this.postsService.openSnackBar('Failed to clear logs!');
|
||||||
|
}
|
||||||
|
}, err => {
|
||||||
|
this.postsService.openSnackBar('Failed to clear logs!');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
<mat-list-item role="listitem" *ngFor="let permission of available_permissions">
|
<mat-list-item role="listitem" *ngFor="let permission of available_permissions">
|
||||||
<h3 matLine>{{permissionToLabel[permission] ? permissionToLabel[permission] : permission}}</h3>
|
<h3 matLine>{{permissionToLabel[permission] ? permissionToLabel[permission] : permission}}</h3>
|
||||||
<span matLine>
|
<span matLine>
|
||||||
<mat-radio-group [disabled]="permission === 'settings' && role.name === 'admin'" (change)="changeRolePermissions($event, permission, permissions[permission])" [(ngModel)]="permissions[permission]" [attr.aria-label]="'Give role permission for ' + permission">
|
<mat-radio-group [disabled]="permission === 'settings' && role.key === 'admin'" (change)="changeRolePermissions($event, permission, permissions[permission])" [(ngModel)]="permissions[permission]" [attr.aria-label]="'Give role permission for ' + permission">
|
||||||
<mat-radio-button value="yes"><ng-container i18n="Yes">Yes</ng-container></mat-radio-button>
|
<mat-radio-button value="yes"><ng-container i18n="Yes">Yes</ng-container></mat-radio-button>
|
||||||
<mat-radio-button value="no"><ng-container i18n="No">No</ng-container></mat-radio-button>
|
<mat-radio-button value="no"><ng-container i18n="No">No</ng-container></mat-radio-button>
|
||||||
</mat-radio-group>
|
</mat-radio-group>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||||
|
|
||||||
import { ManageRoleComponent } from './manage-role.component';
|
import { ManageRoleComponent } from './manage-role.component';
|
||||||
|
|
||||||
@@ -6,7 +6,7 @@ describe('ManageRoleComponent', () => {
|
|||||||
let component: ManageRoleComponent;
|
let component: ManageRoleComponent;
|
||||||
let fixture: ComponentFixture<ManageRoleComponent>;
|
let fixture: ComponentFixture<ManageRoleComponent>;
|
||||||
|
|
||||||
beforeEach(async(() => {
|
beforeEach(waitForAsync(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
declarations: [ ManageRoleComponent ]
|
declarations: [ ManageRoleComponent ]
|
||||||
})
|
})
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user