mirror of
https://github.com/Tzahi12345/YoutubeDL-Material.git
synced 2026-03-24 21:50:59 +03:00
Compare commits
46 Commits
locale-bas
...
twitch-cha
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
0189d292a8 | ||
|
|
deac54e8d6 | ||
|
|
d4e5082039 | ||
|
|
6f089491a5 | ||
|
|
0a38b01971 | ||
|
|
fe7303a191 | ||
|
|
dff4b141b0 | ||
|
|
fed0a54145 | ||
|
|
8595864118 |
92
.github/workflows/build.yml
vendored
Normal file
92
.github/workflows/build.yml
vendored
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
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@v1
|
||||||
|
- 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: prepare release asset
|
||||||
|
shell: pwsh
|
||||||
|
run: Compress-Archive -Path ${{runner.temp}}/youtubedl-material -DestinationPath youtubedl-material-${{ github.ref }}.zip
|
||||||
|
- name: upload build 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-${{ github.ref }}.zip
|
||||||
|
asset_name: youtubedl-material-${{ github.ref }}.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
|
||||||
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
|
||||||
35
README.md
35
README.md
@@ -1,10 +1,10 @@
|
|||||||
# 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 9](https://angular.io/) for the frontend, and [Node.js](https://nodejs.org/) on the backend.
|
||||||
|
|
||||||
@@ -30,13 +30,25 @@ Dark mode:
|
|||||||
|
|
||||||
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
|
||||||
```
|
```
|
||||||
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
|
||||||
@@ -75,14 +87,16 @@ If you are looking to setup YoutubeDL-Material with Docker, this section is for
|
|||||||
|
|
||||||
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.
|
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. Run `docker-compose pull`. This will download the official YoutubeDL-Material docker image.
|
2. Run `docker-compose pull`. This will download the official YoutubeDL-Material docker image.
|
||||||
3. Run `docker-compose up` to start it up. If successful, it should say "HTTP(S): Started on port 8998" or something similar.
|
3. Run `docker-compose up` to start it up. If successful, it should say "HTTP(S): Started on port 17443" or something similar. This tells you the *container-internal* port of the application. Please check your `docker-compose.yml` file for the *external* port. If you downloaded the file as described above, it defaults to **8998**.
|
||||||
4. Make sure you can connect to the specified URL + port, and if so, you are done!
|
4. Make sure you can connect to the specified URL + *external* port, and if so, you are done!
|
||||||
|
|
||||||
|
NOTE: It is currently recommended that you use the `nightly` tag on Docker. To do so, simply update the docker-compose.yml `image` field so that it points to `tzahi12345/youtubedl-material:nightly`.
|
||||||
|
|
||||||
### Custom UID/GID
|
### Custom UID/GID
|
||||||
|
|
||||||
By default, the Docker container runs as non-root with UID=1000 and GID=1000. To set this to your own UID/GID, simply update the `environment` section in your `docker-compose.yml` like so:
|
By default, the Docker container runs as non-root with UID=1000 and GID=1000. To set this to your own UID/GID, simply update the `environment` section in your `docker-compose.yml` like so:
|
||||||
|
|
||||||
```
|
```yml
|
||||||
environment:
|
environment:
|
||||||
UID: YOUR_UID
|
UID: YOUR_UID
|
||||||
GID: YOUR_GID
|
GID: YOUR_GID
|
||||||
@@ -109,6 +123,7 @@ If you're interested in translating the app into a new language, check out the [
|
|||||||
* **Isaac Grynsztein** (me!) - *Initial work*
|
* **Isaac Grynsztein** (me!) - *Initial work*
|
||||||
|
|
||||||
Official translators:
|
Official translators:
|
||||||
|
|
||||||
* Spanish - tzahi12345
|
* Spanish - tzahi12345
|
||||||
* German - UnlimitedCookies
|
* German - UnlimitedCookies
|
||||||
* Chinese - TyRoyal
|
* Chinese - TyRoyal
|
||||||
|
|||||||
442
backend/app.js
442
backend/app.js
@@ -26,6 +26,8 @@ const shortid = require('shortid')
|
|||||||
const url_api = require('url');
|
const url_api = require('url');
|
||||||
var config_api = require('./config.js');
|
var config_api = require('./config.js');
|
||||||
var subscriptions_api = require('./subscriptions')
|
var subscriptions_api = require('./subscriptions')
|
||||||
|
var categories_api = require('./categories');
|
||||||
|
var twitch_api = require('./twitch');
|
||||||
const CONSTS = require('./consts')
|
const CONSTS = require('./consts')
|
||||||
const { spawn } = require('child_process')
|
const { spawn } = require('child_process')
|
||||||
const read_last_lines = require('read-last-lines');
|
const read_last_lines = require('read-last-lines');
|
||||||
@@ -36,7 +38,8 @@ const is_windows = process.platform === 'win32';
|
|||||||
var app = express();
|
var app = express();
|
||||||
|
|
||||||
// database setup
|
// database setup
|
||||||
const FileSync = require('lowdb/adapters/FileSync')
|
const FileSync = require('lowdb/adapters/FileSync');
|
||||||
|
const config = require('./config.js');
|
||||||
|
|
||||||
const adapter = new FileSync('./appdata/db.json');
|
const adapter = new FileSync('./appdata/db.json');
|
||||||
const db = low(adapter)
|
const db = low(adapter)
|
||||||
@@ -79,8 +82,7 @@ config_api.initialize(logger);
|
|||||||
auth_api.initialize(users_db, logger);
|
auth_api.initialize(users_db, logger);
|
||||||
db_api.initialize(db, users_db, logger);
|
db_api.initialize(db, users_db, logger);
|
||||||
subscriptions_api.initialize(db, users_db, logger, db_api);
|
subscriptions_api.initialize(db, users_db, logger, db_api);
|
||||||
|
categories_api.initialize(db, users_db, logger, db_api);
|
||||||
// var GithubContent = require('github-content');
|
|
||||||
|
|
||||||
// Set some defaults
|
// Set some defaults
|
||||||
db.defaults(
|
db.defaults(
|
||||||
@@ -155,8 +157,8 @@ if (just_restarted) {
|
|||||||
fs.unlinkSync('restart.json');
|
fs.unlinkSync('restart.json');
|
||||||
}
|
}
|
||||||
|
|
||||||
// updates & starts youtubedl
|
// updates & starts youtubedl (commented out b/c of repo takedown)
|
||||||
startYoutubeDL();
|
// startYoutubeDL();
|
||||||
|
|
||||||
var validDownloadingAgents = [
|
var validDownloadingAgents = [
|
||||||
'aria2c',
|
'aria2c',
|
||||||
@@ -173,7 +175,6 @@ const subscription_timeouts = {};
|
|||||||
// don't overwrite config if it already happened.. NOT
|
// don't overwrite config if it already happened.. NOT
|
||||||
// let alreadyWritten = db.get('configWriteFlag').value();
|
// let alreadyWritten = db.get('configWriteFlag').value();
|
||||||
let writeConfigMode = process.env.write_ytdl_config;
|
let writeConfigMode = process.env.write_ytdl_config;
|
||||||
var config = null;
|
|
||||||
|
|
||||||
// checks if config exists, if not, a config is auto generated
|
// checks if config exists, if not, a config is auto generated
|
||||||
config_api.configExistsCheck();
|
config_api.configExistsCheck();
|
||||||
@@ -559,6 +560,9 @@ async function loadConfig() {
|
|||||||
// creates archive path if missing
|
// creates archive path if missing
|
||||||
await fs.ensureDir(archivePath);
|
await fs.ensureDir(archivePath);
|
||||||
|
|
||||||
|
// now this is done here due to youtube-dl's repo takedown
|
||||||
|
await startYoutubeDL();
|
||||||
|
|
||||||
// get subscriptions
|
// get subscriptions
|
||||||
if (allowSubscriptions) {
|
if (allowSubscriptions) {
|
||||||
// runs initially, then runs every ${subscriptionCheckInterval} seconds
|
// runs initially, then runs every ${subscriptionCheckInterval} seconds
|
||||||
@@ -1077,6 +1081,7 @@ async function downloadFileByURL_exec(url, type, options, sessionID = null) {
|
|||||||
var is_audio = type === 'audio';
|
var is_audio = type === 'audio';
|
||||||
var ext = is_audio ? '.mp3' : '.mp4';
|
var ext = is_audio ? '.mp3' : '.mp4';
|
||||||
var fileFolderPath = type === 'audio' ? audioFolderPath : videoFolderPath;
|
var fileFolderPath = type === 'audio' ? audioFolderPath : videoFolderPath;
|
||||||
|
let category = null;
|
||||||
|
|
||||||
// prepend with user if needed
|
// prepend with user if needed
|
||||||
let multiUserMode = null;
|
let multiUserMode = null;
|
||||||
@@ -1093,7 +1098,7 @@ async function downloadFileByURL_exec(url, type, options, sessionID = null) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
options.downloading_method = 'exec';
|
options.downloading_method = 'exec';
|
||||||
const downloadConfig = await generateArgs(url, type, options);
|
let downloadConfig = await generateArgs(url, type, options);
|
||||||
|
|
||||||
// adds download to download helper
|
// adds download to download helper
|
||||||
const download_uid = uuid();
|
const download_uid = uuid();
|
||||||
@@ -1115,11 +1120,22 @@ async function downloadFileByURL_exec(url, type, options, sessionID = null) {
|
|||||||
updateDownloads();
|
updateDownloads();
|
||||||
|
|
||||||
// get video info prior to download
|
// get video info prior to download
|
||||||
const info = await getVideoInfoByURL(url, downloadConfig, download);
|
let info = await getVideoInfoByURL(url, downloadConfig, download);
|
||||||
if (!info) {
|
if (!info) {
|
||||||
resolve(false);
|
resolve(false);
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
|
// check if it fits into a category. If so, then get info again using new downloadConfig
|
||||||
|
category = await categories_api.categorize(info);
|
||||||
|
|
||||||
|
// set custom output if the category has one and re-retrieve info so the download manager has the right file name
|
||||||
|
if (category && category['custom_output']) {
|
||||||
|
options.customOutput = category['custom_output'];
|
||||||
|
options.noRelativePath = true;
|
||||||
|
downloadConfig = await generateArgs(url, type, options);
|
||||||
|
info = await getVideoInfoByURL(url, downloadConfig, download);
|
||||||
|
}
|
||||||
|
|
||||||
// store info in download for future use
|
// store info in download for future use
|
||||||
download['_filename'] = info['_filename'];
|
download['_filename'] = info['_filename'];
|
||||||
download['filesize'] = utils.getExpectedFileSize(info);
|
download['filesize'] = utils.getExpectedFileSize(info);
|
||||||
@@ -1161,7 +1177,7 @@ async function downloadFileByURL_exec(url, type, options, sessionID = null) {
|
|||||||
} catch(e) {
|
} catch(e) {
|
||||||
output_json = null;
|
output_json = null;
|
||||||
}
|
}
|
||||||
var modified_file_name = output_json ? output_json['title'] : null;
|
|
||||||
if (!output_json) {
|
if (!output_json) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -1172,6 +1188,13 @@ async function downloadFileByURL_exec(url, type, options, sessionID = null) {
|
|||||||
var full_file_path = filepath_no_extension + ext;
|
var full_file_path = filepath_no_extension + ext;
|
||||||
var file_name = filepath_no_extension.substring(fileFolderPath.length, filepath_no_extension.length);
|
var file_name = filepath_no_extension.substring(fileFolderPath.length, filepath_no_extension.length);
|
||||||
|
|
||||||
|
if (type === 'video' && url.includes('twitch.tv/videos/') && url.split('twitch.tv/videos/').length > 1
|
||||||
|
&& config.getConfigItem('ytdl_use_twitch_api') && config.getConfigItem('ytdl_twitch_auto_download_chat')) {
|
||||||
|
let vodId = url.split('twitch.tv/videos/')[1];
|
||||||
|
vodId = vodId.split('?')[0];
|
||||||
|
downloadTwitchChatByVODID(vodId, file_name, type, options.user);
|
||||||
|
}
|
||||||
|
|
||||||
// renames file if necessary due to bug
|
// renames file if necessary due to bug
|
||||||
if (!fs.existsSync(output_json['_filename'] && fs.existsSync(output_json['_filename'] + '.webm'))) {
|
if (!fs.existsSync(output_json['_filename'] && fs.existsSync(output_json['_filename'] + '.webm'))) {
|
||||||
try {
|
try {
|
||||||
@@ -1190,8 +1213,11 @@ async function downloadFileByURL_exec(url, type, options, sessionID = null) {
|
|||||||
if (!success) logger.error('Failed to apply ID3 tag to audio file ' + output_json['_filename']);
|
if (!success) logger.error('Failed to apply ID3 tag to audio file ' + output_json['_filename']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const file_path = options.noRelativePath ? path.basename(full_file_path) : full_file_path.substring(fileFolderPath.length, full_file_path.length);
|
||||||
|
const customPath = options.noRelativePath ? path.dirname(full_file_path).split(path.sep).pop() : null;
|
||||||
|
|
||||||
// registers file in DB
|
// registers file in DB
|
||||||
file_uid = db_api.registerFileDB(full_file_path.substring(fileFolderPath.length, full_file_path.length), type, multiUserMode);
|
file_uid = db_api.registerFileDB(file_path, type, multiUserMode, null, customPath);
|
||||||
|
|
||||||
if (file_name) file_names.push(file_name);
|
if (file_name) file_names.push(file_name);
|
||||||
}
|
}
|
||||||
@@ -1362,7 +1388,7 @@ async function downloadFileByURL_normal(url, type, options, sessionID = null) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function generateArgs(url, type, options) {
|
async function generateArgs(url, type, options) {
|
||||||
var videopath = '%(title)s';
|
var videopath = config_api.getConfigItem('ytdl_default_file_output') ? config_api.getConfigItem('ytdl_default_file_output') : '%(title)s';
|
||||||
var globalArgs = config_api.getConfigItem('ytdl_custom_args');
|
var globalArgs = config_api.getConfigItem('ytdl_custom_args');
|
||||||
let useCookies = config_api.getConfigItem('ytdl_use_cookies');
|
let useCookies = config_api.getConfigItem('ytdl_use_cookies');
|
||||||
var is_audio = type === 'audio';
|
var is_audio = type === 'audio';
|
||||||
@@ -1406,7 +1432,8 @@ async function generateArgs(url, type, options) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (customOutput) {
|
if (customOutput) {
|
||||||
downloadConfig = ['-o', path.join(fileFolderPath, customOutput) + ".%(ext)s", '--write-info-json', '--print-json'];
|
customOutput = options.noRelativePath ? customOutput : path.join(fileFolderPath, customOutput);
|
||||||
|
downloadConfig = ['-o', `${customOutput}.%(ext)s`, '--write-info-json', '--print-json'];
|
||||||
} else {
|
} else {
|
||||||
downloadConfig = ['-o', path.join(fileFolderPath, videopath + (is_audio ? '.%(ext)s' : '.mp4')), '--write-info-json', '--print-json'];
|
downloadConfig = ['-o', path.join(fileFolderPath, videopath + (is_audio ? '.%(ext)s' : '.mp4')), '--write-info-json', '--print-json'];
|
||||||
}
|
}
|
||||||
@@ -1577,6 +1604,8 @@ function checkDownloadPercent(download) {
|
|||||||
const filename = path.format(path.parse(download['_filename'].substring(0, download['_filename'].length-4)));
|
const filename = path.format(path.parse(download['_filename'].substring(0, download['_filename'].length-4)));
|
||||||
const resulting_file_size = download['filesize'];
|
const resulting_file_size = download['filesize'];
|
||||||
|
|
||||||
|
if (!resulting_file_size) return;
|
||||||
|
|
||||||
glob(`${filename}*`, (err, files) => {
|
glob(`${filename}*`, (err, files) => {
|
||||||
let sum_size = 0;
|
let sum_size = 0;
|
||||||
files.forEach(file => {
|
files.forEach(file => {
|
||||||
@@ -1598,12 +1627,16 @@ function checkDownloadPercent(download) {
|
|||||||
|
|
||||||
async function startYoutubeDL() {
|
async function startYoutubeDL() {
|
||||||
// auto update youtube-dl
|
// auto update youtube-dl
|
||||||
if (!debugMode) await autoUpdateYoutubeDL();
|
await autoUpdateYoutubeDL();
|
||||||
}
|
}
|
||||||
|
|
||||||
// auto updates the underlying youtube-dl binary, not YoutubeDL-Material
|
// auto updates the underlying youtube-dl binary, not YoutubeDL-Material
|
||||||
async function autoUpdateYoutubeDL() {
|
async function autoUpdateYoutubeDL() {
|
||||||
return new Promise(resolve => {
|
return new Promise(async resolve => {
|
||||||
|
const default_downloader = config_api.getConfigItem('ytdl_default_downloader');
|
||||||
|
const using_youtube_dlc = default_downloader === 'youtube-dlc';
|
||||||
|
const youtube_dl_tags_url = 'https://api.github.com/repos/ytdl-org/youtube-dl/tags'
|
||||||
|
const youtube_dlc_tags_url = 'https://api.github.com/repos/blackjack4494/yt-dlc/tags'
|
||||||
// get current version
|
// get current version
|
||||||
let current_app_details_path = 'node_modules/youtube-dl/bin/details';
|
let current_app_details_path = 'node_modules/youtube-dl/bin/details';
|
||||||
let current_app_details_exists = fs.existsSync(current_app_details_path);
|
let current_app_details_exists = fs.existsSync(current_app_details_path);
|
||||||
@@ -1630,42 +1663,77 @@ async function autoUpdateYoutubeDL() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// got version, now let's check the latest version from the youtube-dl API
|
// got version, now let's check the latest version from the youtube-dl API
|
||||||
let youtubedl_api_path = 'https://api.github.com/repos/ytdl-org/youtube-dl/tags';
|
let youtubedl_api_path = using_youtube_dlc ? youtube_dlc_tags_url : youtube_dl_tags_url;
|
||||||
|
|
||||||
|
if (default_downloader === 'youtube-dl') {
|
||||||
|
await downloadLatestYoutubeDLBinary('unknown', 'unknown');
|
||||||
|
resolve(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
fetch(youtubedl_api_path, {method: 'Get'})
|
fetch(youtubedl_api_path, {method: 'Get'})
|
||||||
.then(async res => res.json())
|
.then(async res => res.json())
|
||||||
.then(async (json) => {
|
.then(async (json) => {
|
||||||
// check if the versions are different
|
// check if the versions are different
|
||||||
if (!json || !json[0]) {
|
if (!json || !json[0]) {
|
||||||
|
logger.error(`Failed to check ${default_downloader} version for an update.`)
|
||||||
resolve(false);
|
resolve(false);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const latest_update_version = json[0]['name'];
|
const latest_update_version = json[0]['name'];
|
||||||
if (current_version !== latest_update_version) {
|
if (current_version !== latest_update_version) {
|
||||||
let binary_path = 'node_modules/youtube-dl/bin';
|
|
||||||
// versions different, download new update
|
// versions different, download new update
|
||||||
logger.info('Found new update for youtube-dl. Updating binary...');
|
logger.info(`Found new update for ${default_downloader}. Updating binary...`);
|
||||||
try {
|
try {
|
||||||
await checkExistsWithTimeout(stored_binary_path, 10000);
|
await checkExistsWithTimeout(stored_binary_path, 10000);
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
logger.error(`Failed to update youtube-dl - ${e}`);
|
logger.error(`Failed to update ${default_downloader} - ${e}`);
|
||||||
}
|
}
|
||||||
downloader(binary_path, function error(err, done) {
|
if (using_youtube_dlc) await downloadLatestYoutubeDLCBinary(latest_update_version);
|
||||||
if (err) {
|
else await downloadLatestYoutubeDLBinary(current_version, latest_update_version);
|
||||||
logger.error(err);
|
|
||||||
resolve(false);
|
resolve(true);
|
||||||
}
|
} else {
|
||||||
logger.info(`Binary successfully updated: ${current_version} -> ${latest_update_version}`);
|
resolve(false);
|
||||||
resolve(true);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
logger.error('Failed to check youtube-dl version for an update.')
|
logger.error(`Failed to check ${default_downloader} version for an update.`)
|
||||||
logger.error(err)
|
logger.error(err)
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function downloadLatestYoutubeDLBinary(current_version, new_version) {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
let binary_path = 'node_modules/youtube-dl/bin';
|
||||||
|
downloader(binary_path, function error(err, done) {
|
||||||
|
if (err) {
|
||||||
|
logger.error(`youtube-dl failed to update. Restart the server to try again.`);
|
||||||
|
logger.error(err);
|
||||||
|
resolve(false);
|
||||||
|
}
|
||||||
|
logger.info(`youtube-dl successfully updated!`);
|
||||||
|
resolve(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function downloadLatestYoutubeDLCBinary(new_version) {
|
||||||
|
const file_ext = is_windows ? '.exe' : '';
|
||||||
|
|
||||||
|
const download_url = `https://github.com/blackjack4494/yt-dlc/releases/latest/download/youtube-dlc${file_ext}`;
|
||||||
|
const output_path = `node_modules/youtube-dl/bin/youtube-dl${file_ext}`;
|
||||||
|
|
||||||
|
await fetchFile(download_url, output_path, `youtube-dlc ${new_version}`);
|
||||||
|
|
||||||
|
const details_path = 'node_modules/youtube-dl/bin/details';
|
||||||
|
const details_json = fs.readJSONSync('node_modules/youtube-dl/bin/details');
|
||||||
|
details_json['version'] = new_version;
|
||||||
|
|
||||||
|
fs.writeJSONSync(details_path, details_json);
|
||||||
|
}
|
||||||
|
|
||||||
async function checkExistsWithTimeout(filePath, timeout) {
|
async function checkExistsWithTimeout(filePath, timeout) {
|
||||||
return new Promise(function (resolve, reject) {
|
return new Promise(function (resolve, reject) {
|
||||||
|
|
||||||
@@ -1700,6 +1768,42 @@ function removeFileExtension(filename) {
|
|||||||
return filename_parts.join('.');
|
return filename_parts.join('.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function getTwitchChatByFileID(id, type, user_uid, uuid) {
|
||||||
|
let file_path = null;
|
||||||
|
|
||||||
|
if (user_uid) {
|
||||||
|
file_path = path.join('users', user_uid, type, 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) {
|
||||||
|
const twitch_api_key = config_api.getConfigItem('ytdl_twitch_api_key');
|
||||||
|
const chat = await twitch_api.getCommentsForVOD(twitch_api_key, vodId);
|
||||||
|
|
||||||
|
// save file if needec params are included
|
||||||
|
if (id && type) {
|
||||||
|
let file_path = null;
|
||||||
|
if (user_uid) {
|
||||||
|
file_path = path.join('users', user_uid, type, id + '.twitch_chat.json');
|
||||||
|
} else {
|
||||||
|
file_path = path.join(type, id + '.twitch_chat.json');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (chat) fs.writeJSONSync(file_path, chat);
|
||||||
|
}
|
||||||
|
|
||||||
|
return chat;
|
||||||
|
}
|
||||||
|
|
||||||
app.use(function(req, res, next) {
|
app.use(function(req, res, next) {
|
||||||
res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, Authorization");
|
res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, Authorization");
|
||||||
res.header("Access-Control-Allow-Origin", getOrigin());
|
res.header("Access-Control-Allow-Origin", getOrigin());
|
||||||
@@ -1715,13 +1819,9 @@ app.use(function(req, res, next) {
|
|||||||
next();
|
next();
|
||||||
} else if (req.query.apiKey === admin_token) {
|
} else if (req.query.apiKey === admin_token) {
|
||||||
next();
|
next();
|
||||||
} else if (req.query.apiKey && config_api.getConfigItem('ytdl_use_api_key')) {
|
} else if (req.query.apiKey && config_api.getConfigItem('ytdl_use_api_key') && req.query.apiKey === config_api.getConfigItem('ytdl_api_key')) {
|
||||||
if (req.query.apiKey === config_api.getConfigItem('ytdl_api_key')) {
|
next();
|
||||||
next();
|
} else if (req.path.includes('/api/stream/') || req.path.includes('/api/thumbnail/')) {
|
||||||
} else {
|
|
||||||
res.status(401).send('Invalid API key');
|
|
||||||
}
|
|
||||||
} else if (req.path.includes('/api/video/') || req.path.includes('/api/audio/')) {
|
|
||||||
next();
|
next();
|
||||||
} else {
|
} else {
|
||||||
logger.verbose(`Rejecting request - invalid API use for endpoint: ${req.path}. API key received: ${req.query.apiKey}`);
|
logger.verbose(`Rejecting request - invalid API use for endpoint: ${req.path}. API key received: ${req.query.apiKey}`);
|
||||||
@@ -1734,8 +1834,7 @@ app.use(compression());
|
|||||||
const optionalJwt = function (req, res, next) {
|
const optionalJwt = function (req, res, next) {
|
||||||
const multiUserMode = config_api.getConfigItem('ytdl_multi_user_mode');
|
const multiUserMode = config_api.getConfigItem('ytdl_multi_user_mode');
|
||||||
if (multiUserMode && ((req.body && req.body.uuid) || (req.query && req.query.uuid)) && (req.path.includes('/api/getFile') ||
|
if (multiUserMode && ((req.body && req.body.uuid) || (req.query && req.query.uuid)) && (req.path.includes('/api/getFile') ||
|
||||||
req.path.includes('/api/audio') ||
|
req.path.includes('/api/stream') ||
|
||||||
req.path.includes('/api/video') ||
|
|
||||||
req.path.includes('/api/downloadFile'))) {
|
req.path.includes('/api/downloadFile'))) {
|
||||||
// check if shared video
|
// check if shared video
|
||||||
const using_body = req.body && req.body.uuid;
|
const using_body = req.body && req.body.uuid;
|
||||||
@@ -1875,8 +1974,11 @@ app.get('/api/getMp3s', optionalJwt, async function(req, res) {
|
|||||||
|
|
||||||
mp3s = JSON.parse(JSON.stringify(mp3s));
|
mp3s = JSON.parse(JSON.stringify(mp3s));
|
||||||
|
|
||||||
// add thumbnails if present
|
if (config_api.getConfigItem('ytdl_include_thumbnail')) {
|
||||||
await addThumbnails(mp3s);
|
// add thumbnails if present
|
||||||
|
// await addThumbnails(mp3s);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
res.send({
|
res.send({
|
||||||
mp3s: mp3s,
|
mp3s: mp3s,
|
||||||
@@ -1899,8 +2001,10 @@ app.get('/api/getMp4s', optionalJwt, async function(req, res) {
|
|||||||
|
|
||||||
mp4s = JSON.parse(JSON.stringify(mp4s));
|
mp4s = JSON.parse(JSON.stringify(mp4s));
|
||||||
|
|
||||||
// add thumbnails if present
|
if (config_api.getConfigItem('ytdl_include_thumbnail')) {
|
||||||
await addThumbnails(mp4s);
|
// add thumbnails if present
|
||||||
|
// await addThumbnails(mp4s);
|
||||||
|
}
|
||||||
|
|
||||||
res.send({
|
res.send({
|
||||||
mp4s: mp4s,
|
mp4s: mp4s,
|
||||||
@@ -1988,8 +2092,10 @@ app.post('/api/getAllFiles', optionalJwt, async function (req, res) {
|
|||||||
|
|
||||||
files = JSON.parse(JSON.stringify(files));
|
files = JSON.parse(JSON.stringify(files));
|
||||||
|
|
||||||
// add thumbnails if present
|
if (config_api.getConfigItem('ytdl_include_thumbnail')) {
|
||||||
await addThumbnails(files);
|
// add thumbnails if present
|
||||||
|
// await addThumbnails(files);
|
||||||
|
}
|
||||||
|
|
||||||
res.send({
|
res.send({
|
||||||
files: files,
|
files: files,
|
||||||
@@ -1997,6 +2103,54 @@ app.post('/api/getAllFiles', optionalJwt, async function (req, res) {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
app.post('/api/getFullTwitchChat', optionalJwt, async (req, res) => {
|
||||||
|
var id = req.body.id;
|
||||||
|
var type = req.body.type;
|
||||||
|
var uuid = req.body.uuid;
|
||||||
|
var user_uid = null;
|
||||||
|
|
||||||
|
if (req.isAuthenticated()) user_uid = req.user.uid;
|
||||||
|
|
||||||
|
const chat_file = await getTwitchChatByFileID(id, type, user_uid, uuid);
|
||||||
|
|
||||||
|
res.send({
|
||||||
|
chat: chat_file
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/api/downloadTwitchChatByVODID', optionalJwt, async (req, res) => {
|
||||||
|
var id = req.body.id;
|
||||||
|
var type = req.body.type;
|
||||||
|
var vodId = req.body.vodId;
|
||||||
|
var uuid = req.body.uuid;
|
||||||
|
var user_uid = null;
|
||||||
|
|
||||||
|
if (req.isAuthenticated()) user_uid = req.user.uid;
|
||||||
|
|
||||||
|
// check if file already exists. if so, send that instead
|
||||||
|
const file_exists_check = await getTwitchChatByFileID(id, type, user_uid, uuid);
|
||||||
|
if (file_exists_check) {
|
||||||
|
res.send({chat: file_exists_check});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const full_chat = await downloadTwitchChatByVODID(vodId);
|
||||||
|
|
||||||
|
let file_path = null;
|
||||||
|
|
||||||
|
if (user_uid) {
|
||||||
|
file_path = path.join('users', req.user.uid, type, id + '.twitch_chat.json');
|
||||||
|
} else {
|
||||||
|
file_path = path.join(type, id + '.twitch_chat.json');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (full_chat) fs.writeJSONSync(file_path, full_chat);
|
||||||
|
|
||||||
|
res.send({
|
||||||
|
chat: full_chat
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// video sharing
|
// video sharing
|
||||||
app.post('/api/enableSharing', optionalJwt, function(req, res) {
|
app.post('/api/enableSharing', optionalJwt, function(req, res) {
|
||||||
var type = req.body.type;
|
var type = req.body.type;
|
||||||
@@ -2084,9 +2238,58 @@ app.post('/api/disableSharing', optionalJwt, function(req, res) {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// categories
|
||||||
|
|
||||||
|
app.post('/api/getAllCategories', optionalJwt, async (req, res) => {
|
||||||
|
const categories = db.get('categories').value();
|
||||||
|
res.send({categories: categories});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/api/createCategory', optionalJwt, async (req, res) => {
|
||||||
|
const name = req.body.name;
|
||||||
|
const new_category = {
|
||||||
|
name: name,
|
||||||
|
uid: uuid(),
|
||||||
|
rules: [],
|
||||||
|
custom_putput: ''
|
||||||
|
};
|
||||||
|
|
||||||
|
db.get('categories').push(new_category).write();
|
||||||
|
|
||||||
|
res.send({
|
||||||
|
new_category: new_category,
|
||||||
|
success: !!new_category
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/api/deleteCategory', optionalJwt, async (req, res) => {
|
||||||
|
const category_uid = req.body.category_uid;
|
||||||
|
|
||||||
|
db.get('categories').remove({uid: category_uid}).write();
|
||||||
|
|
||||||
|
res.send({
|
||||||
|
success: true
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/api/updateCategory', optionalJwt, async (req, res) => {
|
||||||
|
const category = req.body.category;
|
||||||
|
db.get('categories').find({uid: category.uid}).assign(category).write();
|
||||||
|
res.send({success: true});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/api/updateCategories', optionalJwt, async (req, res) => {
|
||||||
|
const categories = req.body.categories;
|
||||||
|
db.get('categories').assign(categories).write();
|
||||||
|
res.send({success: true});
|
||||||
|
});
|
||||||
|
|
||||||
|
// subscriptions
|
||||||
|
|
||||||
app.post('/api/subscribe', optionalJwt, async (req, res) => {
|
app.post('/api/subscribe', optionalJwt, async (req, res) => {
|
||||||
let name = req.body.name;
|
let name = req.body.name;
|
||||||
let url = req.body.url;
|
let url = req.body.url;
|
||||||
|
let maxQuality = req.body.maxQuality;
|
||||||
let timerange = req.body.timerange;
|
let timerange = req.body.timerange;
|
||||||
let streamingOnly = req.body.streamingOnly;
|
let streamingOnly = req.body.streamingOnly;
|
||||||
let audioOnly = req.body.audioOnly;
|
let audioOnly = req.body.audioOnly;
|
||||||
@@ -2096,6 +2299,7 @@ app.post('/api/subscribe', optionalJwt, async (req, res) => {
|
|||||||
const new_sub = {
|
const new_sub = {
|
||||||
name: name,
|
name: name,
|
||||||
url: url,
|
url: url,
|
||||||
|
maxQuality: maxQuality,
|
||||||
id: uuid(),
|
id: uuid(),
|
||||||
streamingOnly: streamingOnly,
|
streamingOnly: streamingOnly,
|
||||||
user_uid: user_uid,
|
user_uid: user_uid,
|
||||||
@@ -2168,10 +2372,17 @@ app.post('/api/deleteSubscriptionFile', optionalJwt, async (req, res) => {
|
|||||||
|
|
||||||
app.post('/api/getSubscription', optionalJwt, async (req, res) => {
|
app.post('/api/getSubscription', optionalJwt, async (req, res) => {
|
||||||
let subID = req.body.id;
|
let subID = req.body.id;
|
||||||
|
let subName = req.body.name; // if included, subID is optional
|
||||||
|
|
||||||
let user_uid = req.isAuthenticated() ? req.user.uid : null;
|
let user_uid = req.isAuthenticated() ? req.user.uid : null;
|
||||||
|
|
||||||
// get sub from db
|
// get sub from db
|
||||||
let subscription = subscriptions_api.getSubscription(subID, user_uid);
|
let subscription = null;
|
||||||
|
if (subID) {
|
||||||
|
subscription = subscriptions_api.getSubscription(subID, user_uid)
|
||||||
|
} else if (subName) {
|
||||||
|
subscription = subscriptions_api.getSubscriptionByName(subName, user_uid)
|
||||||
|
}
|
||||||
|
|
||||||
if (!subscription) {
|
if (!subscription) {
|
||||||
// failed to get subscription from db, send 400 error
|
// failed to get subscription from db, send 400 error
|
||||||
@@ -2401,56 +2612,25 @@ app.post('/api/deletePlaylist', optionalJwt, async (req, res) => {
|
|||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
// deletes mp3 file
|
// deletes non-subscription files
|
||||||
app.post('/api/deleteMp3', optionalJwt, async (req, res) => {
|
app.post('/api/deleteFile', optionalJwt, async (req, res) => {
|
||||||
// var name = req.body.name;
|
|
||||||
var uid = req.body.uid;
|
var uid = req.body.uid;
|
||||||
|
var type = req.body.type;
|
||||||
var blacklistMode = req.body.blacklistMode;
|
var blacklistMode = req.body.blacklistMode;
|
||||||
|
|
||||||
if (req.isAuthenticated()) {
|
if (req.isAuthenticated()) {
|
||||||
let success = await auth_api.deleteUserFile(req.user.uid, uid, 'audio', blacklistMode);
|
let success = auth_api.deleteUserFile(req.user.uid, uid, type, blacklistMode);
|
||||||
res.send(success);
|
res.send(success);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var audio_obj = db.get('files.audio').find({uid: uid}).value();
|
var file_obj = db.get(`files.${type}`).find({uid: uid}).value();
|
||||||
var name = audio_obj.id;
|
var name = file_obj.id;
|
||||||
var fullpath = audioFolderPath + name + ".mp3";
|
var fullpath = file_obj ? file_obj.path : null;
|
||||||
var wasDeleted = false;
|
var wasDeleted = false;
|
||||||
if (await fs.pathExists(fullpath))
|
if (await fs.pathExists(fullpath))
|
||||||
{
|
{
|
||||||
deleteAudioFile(name, null, blacklistMode);
|
wasDeleted = type === 'audio' ? await deleteAudioFile(name, path.basename(fullpath), blacklistMode) : await deleteVideoFile(name, path.basename(fullpath), blacklistMode);
|
||||||
db.get('files.audio').remove({uid: uid}).write();
|
|
||||||
wasDeleted = true;
|
|
||||||
res.send(wasDeleted);
|
|
||||||
} else if (audio_obj) {
|
|
||||||
db.get('files.audio').remove({uid: uid}).write();
|
|
||||||
wasDeleted = true;
|
|
||||||
res.send(wasDeleted);
|
|
||||||
} else {
|
|
||||||
wasDeleted = false;
|
|
||||||
res.send(wasDeleted);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// deletes mp4 file
|
|
||||||
app.post('/api/deleteMp4', optionalJwt, async (req, res) => {
|
|
||||||
var uid = req.body.uid;
|
|
||||||
var blacklistMode = req.body.blacklistMode;
|
|
||||||
|
|
||||||
if (req.isAuthenticated()) {
|
|
||||||
let success = await auth_api.deleteUserFile(req.user.uid, uid, 'video', blacklistMode);
|
|
||||||
res.send(success);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var video_obj = db.get('files.video').find({uid: uid}).value();
|
|
||||||
var name = video_obj.id;
|
|
||||||
var fullpath = videoFolderPath + name + ".mp4";
|
|
||||||
var wasDeleted = false;
|
|
||||||
if (await fs.pathExists(fullpath))
|
|
||||||
{
|
|
||||||
wasDeleted = await deleteVideoFile(name, null, blacklistMode);
|
|
||||||
db.get('files.video').remove({uid: uid}).write();
|
db.get('files.video').remove({uid: uid}).write();
|
||||||
// wasDeleted = true;
|
// wasDeleted = true;
|
||||||
res.send(wasDeleted);
|
res.send(wasDeleted);
|
||||||
@@ -2517,17 +2697,6 @@ app.post('/api/downloadFile', optionalJwt, async (req, res) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
app.post('/api/deleteFile', async (req, res) => {
|
|
||||||
let fileName = req.body.fileName;
|
|
||||||
let type = req.body.type;
|
|
||||||
if (type === 'audio') {
|
|
||||||
deleteAudioFile(fileName);
|
|
||||||
} else if (type === 'video') {
|
|
||||||
deleteVideoFile(fileName);
|
|
||||||
}
|
|
||||||
res.send({});
|
|
||||||
});
|
|
||||||
|
|
||||||
app.post('/api/downloadArchive', async (req, res) => {
|
app.post('/api/downloadArchive', async (req, res) => {
|
||||||
let sub = req.body.sub;
|
let sub = req.body.sub;
|
||||||
let archive_dir = sub.archive;
|
let archive_dir = sub.archive;
|
||||||
@@ -2595,25 +2764,33 @@ app.post('/api/generateNewAPIKey', function (req, res) {
|
|||||||
|
|
||||||
// Streaming API calls
|
// Streaming API calls
|
||||||
|
|
||||||
app.get('/api/video/:id', optionalJwt, function(req , res){
|
app.get('/api/stream/:id', optionalJwt, (req, res) => {
|
||||||
|
const type = req.query.type;
|
||||||
|
const ext = type === 'audio' ? '.mp3' : '.mp4';
|
||||||
|
const mimetype = type === 'audio' ? 'audio/mp3' : 'video/mp4';
|
||||||
var head;
|
var head;
|
||||||
let optionalParams = url_api.parse(req.url,true).query;
|
let optionalParams = url_api.parse(req.url,true).query;
|
||||||
let id = decodeURIComponent(req.params.id);
|
let id = decodeURIComponent(req.params.id);
|
||||||
let file_path = videoFolderPath + id + '.mp4';
|
let file_path = req.query.file_path ? decodeURIComponent(req.query.file_path) : null;
|
||||||
if (req.isAuthenticated() || req.can_watch) {
|
if (!file_path && (req.isAuthenticated() || req.can_watch)) {
|
||||||
let usersFileFolder = config_api.getConfigItem('ytdl_users_base_path');
|
let usersFileFolder = config_api.getConfigItem('ytdl_users_base_path');
|
||||||
if (optionalParams['subName']) {
|
if (optionalParams['subName']) {
|
||||||
const isPlaylist = optionalParams['subPlaylist'];
|
const isPlaylist = optionalParams['subPlaylist'];
|
||||||
file_path = path.join(usersFileFolder, req.user.uid, 'subscriptions', (isPlaylist === 'true' ? 'playlists/' : 'channels/'),optionalParams['subName'], id + '.mp4')
|
file_path = path.join(usersFileFolder, req.user.uid, 'subscriptions', (isPlaylist === 'true' ? 'playlists/' : 'channels/'),optionalParams['subName'], id + ext)
|
||||||
} else {
|
} else {
|
||||||
file_path = path.join(usersFileFolder, req.query.uuid ? req.query.uuid : req.user.uid, 'video', id + '.mp4');
|
file_path = path.join(usersFileFolder, req.query.uuid ? req.query.uuid : req.user.uid, type, id + ext);
|
||||||
}
|
}
|
||||||
} else if (optionalParams['subName']) {
|
} else if (!file_path && optionalParams['subName']) {
|
||||||
let basePath = config_api.getConfigItem('ytdl_subscriptions_base_path');
|
let basePath = config_api.getConfigItem('ytdl_subscriptions_base_path');
|
||||||
const isPlaylist = optionalParams['subPlaylist'];
|
const isPlaylist = optionalParams['subPlaylist'];
|
||||||
basePath += (isPlaylist === 'true' ? 'playlists/' : 'channels/');
|
basePath += (isPlaylist === 'true' ? 'playlists/' : 'channels/');
|
||||||
file_path = basePath + optionalParams['subName'] + '/' + id + '.mp4';
|
file_path = basePath + optionalParams['subName'] + '/' + id + ext;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!file_path) {
|
||||||
|
file_path = path.join(videoFolderPath, id + ext);
|
||||||
|
}
|
||||||
|
|
||||||
const stat = fs.statSync(file_path)
|
const stat = fs.statSync(file_path)
|
||||||
const fileSize = stat.size
|
const fileSize = stat.size
|
||||||
const range = req.headers.range
|
const range = req.headers.range
|
||||||
@@ -2636,76 +2813,25 @@ app.get('/api/video/:id', optionalJwt, function(req , res){
|
|||||||
'Content-Range': `bytes ${start}-${end}/${fileSize}`,
|
'Content-Range': `bytes ${start}-${end}/${fileSize}`,
|
||||||
'Accept-Ranges': 'bytes',
|
'Accept-Ranges': 'bytes',
|
||||||
'Content-Length': chunksize,
|
'Content-Length': chunksize,
|
||||||
'Content-Type': 'video/mp4',
|
'Content-Type': mimetype,
|
||||||
}
|
}
|
||||||
res.writeHead(206, head);
|
res.writeHead(206, head);
|
||||||
file.pipe(res);
|
file.pipe(res);
|
||||||
} else {
|
} else {
|
||||||
head = {
|
head = {
|
||||||
'Content-Length': fileSize,
|
'Content-Length': fileSize,
|
||||||
'Content-Type': 'video/mp4',
|
'Content-Type': mimetype,
|
||||||
}
|
}
|
||||||
res.writeHead(200, head)
|
res.writeHead(200, head)
|
||||||
fs.createReadStream(file_path).pipe(res)
|
fs.createReadStream(file_path).pipe(res)
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
app.get('/api/audio/:id', optionalJwt, function(req , res){
|
app.get('/api/thumbnail/:path', optionalJwt, async (req, res) => {
|
||||||
var head;
|
let file_path = decodeURIComponent(req.params.path);
|
||||||
let id = decodeURIComponent(req.params.id);
|
if (fs.existsSync(file_path)) path.isAbsolute(file_path) ? res.sendFile(file_path) : res.sendFile(path.join(__dirname, file_path));
|
||||||
let file_path = "audio/" + id + '.mp3';
|
else res.sendStatus(404);
|
||||||
let usersFileFolder = config_api.getConfigItem('ytdl_users_base_path');
|
});
|
||||||
let optionalParams = url_api.parse(req.url,true).query;
|
|
||||||
if (req.isAuthenticated()) {
|
|
||||||
if (optionalParams['subName']) {
|
|
||||||
const isPlaylist = optionalParams['subPlaylist'];
|
|
||||||
file_path = path.join(usersFileFolder, req.user.uid, 'subscriptions', (isPlaylist === 'true' ? 'playlists/' : 'channels/'),optionalParams['subName'], id + '.mp3')
|
|
||||||
} else {
|
|
||||||
let usersFileFolder = config_api.getConfigItem('ytdl_users_base_path');
|
|
||||||
file_path = path.join(usersFileFolder, req.user.uid, 'audio', id + '.mp3');
|
|
||||||
}
|
|
||||||
} else if (optionalParams['subName']) {
|
|
||||||
let basePath = config_api.getConfigItem('ytdl_subscriptions_base_path');
|
|
||||||
const isPlaylist = optionalParams['subPlaylist'];
|
|
||||||
basePath += (isPlaylist === 'true' ? 'playlists/' : 'channels/');
|
|
||||||
file_path = basePath + optionalParams['subName'] + '/' + id + '.mp3';
|
|
||||||
}
|
|
||||||
file_path = file_path.replace(/\"/g, '\'');
|
|
||||||
const stat = fs.statSync(file_path)
|
|
||||||
const fileSize = stat.size
|
|
||||||
const range = req.headers.range
|
|
||||||
if (range) {
|
|
||||||
const parts = range.replace(/bytes=/, "").split("-")
|
|
||||||
const start = parseInt(parts[0], 10)
|
|
||||||
const end = parts[1]
|
|
||||||
? parseInt(parts[1], 10)
|
|
||||||
: fileSize-1
|
|
||||||
const chunksize = (end-start)+1
|
|
||||||
const file = fs.createReadStream(file_path, {start, end});
|
|
||||||
if (config_api.descriptors[id]) config_api.descriptors[id].push(file);
|
|
||||||
else config_api.descriptors[id] = [file];
|
|
||||||
file.on('close', function() {
|
|
||||||
let index = config_api.descriptors[id].indexOf(file);
|
|
||||||
config_api.descriptors[id].splice(index, 1);
|
|
||||||
logger.debug('Successfully closed stream and removed file reference.');
|
|
||||||
});
|
|
||||||
head = {
|
|
||||||
'Content-Range': `bytes ${start}-${end}/${fileSize}`,
|
|
||||||
'Accept-Ranges': 'bytes',
|
|
||||||
'Content-Length': chunksize,
|
|
||||||
'Content-Type': 'audio/mp3',
|
|
||||||
}
|
|
||||||
res.writeHead(206, head);
|
|
||||||
file.pipe(res);
|
|
||||||
} else {
|
|
||||||
head = {
|
|
||||||
'Content-Length': fileSize,
|
|
||||||
'Content-Type': 'audio/mp3',
|
|
||||||
}
|
|
||||||
res.writeHead(200, head)
|
|
||||||
fs.createReadStream(file_path).pipe(res)
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Downloads management
|
// Downloads management
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
"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,
|
"safe_download_override": false,
|
||||||
@@ -25,7 +26,10 @@
|
|||||||
"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",
|
||||||
@@ -49,6 +53,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"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,
|
||||||
|
|||||||
123
backend/categories.js
Normal file
123
backend/categories.js
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
const config_api = require('./config');
|
||||||
|
|
||||||
|
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_json) {
|
||||||
|
let selected_category = null;
|
||||||
|
const categories = getCategories();
|
||||||
|
if (!categories) {
|
||||||
|
logger.warn('Categories could not be found. Initializing categories...');
|
||||||
|
db.assign({categories: []}).write();
|
||||||
|
return null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < categories.length; i++) {
|
||||||
|
const category = categories[i];
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCategories() {
|
||||||
|
const categories = db.get('categories').value();
|
||||||
|
return categories ? categories : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
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']].includes(rule['value']);
|
||||||
|
break;
|
||||||
|
case 'not_includes':
|
||||||
|
rule_applies = !(file_json[rule['property']].includes(rule['value']));
|
||||||
|
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,
|
||||||
|
}
|
||||||
@@ -184,6 +184,7 @@ DEFAULT_CONFIG = {
|
|||||||
"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,
|
"safe_download_override": false,
|
||||||
@@ -202,7 +203,10 @@ DEFAULT_CONFIG = {
|
|||||||
"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",
|
||||||
@@ -226,6 +230,7 @@ DEFAULT_CONFIG = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"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,
|
||||||
|
|||||||
@@ -18,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'
|
||||||
@@ -82,6 +86,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': {
|
||||||
@@ -130,6 +146,10 @@ let CONFIG_ITEMS = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
// 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'
|
||||||
|
|||||||
@@ -15,10 +15,10 @@ function initialize(input_db, input_users_db, input_logger) {
|
|||||||
setLogger(input_logger);
|
setLogger(input_logger);
|
||||||
}
|
}
|
||||||
|
|
||||||
function registerFileDB(file_path, type, multiUserMode = null, sub = null) {
|
function registerFileDB(file_path, type, multiUserMode = null, sub = null, customPath = null) {
|
||||||
let db_path = null;
|
let db_path = null;
|
||||||
const file_id = file_path.substring(0, file_path.length-4);
|
const file_id = file_path.substring(0, file_path.length-4);
|
||||||
const file_object = generateFileObject(file_id, type, multiUserMode && multiUserMode.file_path, sub);
|
const file_object = generateFileObject(file_id, type, customPath || multiUserMode && multiUserMode.file_path, sub);
|
||||||
if (!file_object) {
|
if (!file_object) {
|
||||||
logger.error(`Could not find associated JSON file for ${type} file ${file_id}`);
|
logger.error(`Could not find associated JSON file for ${type} file ${file_id}`);
|
||||||
return false;
|
return false;
|
||||||
@@ -27,7 +27,7 @@ function registerFileDB(file_path, type, multiUserMode = null, sub = null) {
|
|||||||
utils.fixVideoMetadataPerms(file_id, type, multiUserMode && multiUserMode.file_path);
|
utils.fixVideoMetadataPerms(file_id, type, multiUserMode && multiUserMode.file_path);
|
||||||
|
|
||||||
// add thumbnail path
|
// add thumbnail path
|
||||||
file_object['thumbnailPath'] = utils.getDownloadedThumbnail(file_id, type, multiUserMode && multiUserMode.file_path);
|
file_object['thumbnailPath'] = utils.getDownloadedThumbnail(file_id, type, customPath || multiUserMode && multiUserMode.file_path);
|
||||||
|
|
||||||
if (!sub) {
|
if (!sub) {
|
||||||
if (multiUserMode) {
|
if (multiUserMode) {
|
||||||
|
|||||||
@@ -114,7 +114,11 @@ 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)
|
if (user_uid)
|
||||||
@@ -296,7 +300,8 @@ async function getVideosForSub(sub, user_uid = null) {
|
|||||||
qualityPath.push('-x');
|
qualityPath.push('-x');
|
||||||
qualityPath.push('--audio-format', 'mp3');
|
qualityPath.push('--audio-format', 'mp3');
|
||||||
} else {
|
} else {
|
||||||
qualityPath = ['-f', 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/mp4']
|
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)
|
downloadConfig.push(...qualityPath)
|
||||||
@@ -351,7 +356,7 @@ async function getVideosForSub(sub, user_uid = null) {
|
|||||||
youtubedl.exec(sub.url, downloadConfig, {}, function(err, output) {
|
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 {
|
||||||
@@ -430,6 +435,13 @@ function getSubscription(subID, user_uid = null) {
|
|||||||
return db.get('subscriptions').find({id: subID}).value();
|
return db.get('subscriptions').find({id: subID}).value();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getSubscriptionByName(subName, user_uid = null) {
|
||||||
|
if (user_uid)
|
||||||
|
return users_db.get('users').find({uid: user_uid}).get('subscriptions').find({name: subName}).value();
|
||||||
|
else
|
||||||
|
return db.get('subscriptions').find({name: subName}).value();
|
||||||
|
}
|
||||||
|
|
||||||
function updateSubscription(sub, user_uid = null) {
|
function updateSubscription(sub, user_uid = null) {
|
||||||
if (user_uid) {
|
if (user_uid) {
|
||||||
users_db.get('users').find({uid: user_uid}).get('subscriptions').find({id: sub.id}).assign(sub).write();
|
users_db.get('users').find({uid: user_uid}).get('subscriptions').find({id: sub.id}).assign(sub).write();
|
||||||
@@ -482,6 +494,7 @@ async function removeIDFromArchive(archive_path, id) {
|
|||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
getSubscription : getSubscription,
|
getSubscription : getSubscription,
|
||||||
|
getSubscriptionByName : getSubscriptionByName,
|
||||||
getAllSubscriptions : getAllSubscriptions,
|
getAllSubscriptions : getAllSubscriptions,
|
||||||
updateSubscription : updateSubscription,
|
updateSubscription : updateSubscription,
|
||||||
subscribe : subscribe,
|
subscribe : subscribe,
|
||||||
|
|||||||
71
backend/twitch.js
Normal file
71
backend/twitch.js
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
var moment = require('moment');
|
||||||
|
var Axios = require('axios');
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
} = 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
|
||||||
|
});
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
getCommentsForVOD: getCommentsForVOD
|
||||||
|
}
|
||||||
@@ -114,6 +114,7 @@ function getExpectedFileSize(info_json) {
|
|||||||
const formats = info_json['format_id'].split('+');
|
const formats = info_json['format_id'].split('+');
|
||||||
let expected_filesize = 0;
|
let expected_filesize = 0;
|
||||||
formats.forEach(format_id => {
|
formats.forEach(format_id => {
|
||||||
|
if (!info_json.formats) return expected_filesize;
|
||||||
info_json.formats.forEach(available_format => {
|
info_json.formats.forEach(available_format => {
|
||||||
if (available_format.format_id === format_id && available_format.filesize) {
|
if (available_format.format_id === format_id && available_format.filesize) {
|
||||||
expected_filesize += available_format.filesize;
|
expected_filesize += available_format.filesize;
|
||||||
|
|||||||
@@ -116,6 +116,8 @@ export class AppComponent implements OnInit, AfterViewInit {
|
|||||||
if (this.allowSubscriptions) {
|
if (this.allowSubscriptions) {
|
||||||
this.postsService.reloadSubscriptions();
|
this.postsService.reloadSubscriptions();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.postsService.reloadCategories();
|
||||||
}
|
}
|
||||||
|
|
||||||
// theme stuff
|
// theme stuff
|
||||||
|
|||||||
@@ -79,6 +79,8 @@ import { UnifiedFileCardComponent } from './components/unified-file-card/unified
|
|||||||
import { RecentVideosComponent } from './components/recent-videos/recent-videos.component';
|
import { RecentVideosComponent } from './components/recent-videos/recent-videos.component';
|
||||||
import { EditSubscriptionDialogComponent } from './dialogs/edit-subscription-dialog/edit-subscription-dialog.component';
|
import { EditSubscriptionDialogComponent } from './dialogs/edit-subscription-dialog/edit-subscription-dialog.component';
|
||||||
import { CustomPlaylistsComponent } from './components/custom-playlists/custom-playlists.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';
|
||||||
|
|
||||||
registerLocaleData(es, 'es');
|
registerLocaleData(es, 'es');
|
||||||
|
|
||||||
@@ -123,7 +125,9 @@ export function isVisible({ event, element, scrollContainer, offset }: IsVisible
|
|||||||
UnifiedFileCardComponent,
|
UnifiedFileCardComponent,
|
||||||
RecentVideosComponent,
|
RecentVideosComponent,
|
||||||
EditSubscriptionDialogComponent,
|
EditSubscriptionDialogComponent,
|
||||||
CustomPlaylistsComponent
|
CustomPlaylistsComponent,
|
||||||
|
EditCategoryDialogComponent,
|
||||||
|
TwitchChatComponent
|
||||||
],
|
],
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
|
|||||||
@@ -61,7 +61,8 @@ export class LogsViewerComponent implements OnInit {
|
|||||||
data: {
|
data: {
|
||||||
dialogTitle: 'Clear logs',
|
dialogTitle: 'Clear logs',
|
||||||
dialogText: 'Would you like to clear your logs? This will delete all your current logs, permanently.',
|
dialogText: 'Would you like to clear your logs? This will delete all your current logs, permanently.',
|
||||||
submitText: 'Clear'
|
submitText: 'Clear',
|
||||||
|
warnSubmitColor: true
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
dialogRef.afterClosed().subscribe(confirmed => {
|
dialogRef.afterClosed().subscribe(confirmed => {
|
||||||
|
|||||||
@@ -32,7 +32,7 @@
|
|||||||
<div class="row justify-content-center">
|
<div class="row justify-content-center">
|
||||||
<ng-container *ngIf="normal_files_received">
|
<ng-container *ngIf="normal_files_received">
|
||||||
<div *ngFor="let file of filtered_files; let i = index" class="mb-2 mt-2 d-flex justify-content-center" [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' : '' ]">
|
<div *ngFor="let file of filtered_files; let i = index" class="mb-2 mt-2 d-flex justify-content-center" [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)="goToFile($event)" (goToSubscription)="goToSubscription($event)" [file_obj]="file" [use_youtubedl_archive]="postsService.config['Downloader']['use_youtubedl_archive']" [loading]="false" (deleteFile)="deleteFile($event)"></app-unified-file-card>
|
<app-unified-file-card [index]="i" [card_size]="postsService.card_size" [locale]="postsService.locale" (goToFile)="goToFile($event)" (goToSubscription)="goToSubscription($event)" [file_obj]="file" [use_youtubedl_archive]="postsService.config['Downloader']['use_youtubedl_archive']" [loading]="false" (deleteFile)="deleteFile($event)" [baseStreamPath]="postsService.path" [jwtString]="postsService.isLoggedIn ? '?jwt=' + this.postsService.token : ''"></app-unified-file-card>
|
||||||
</div>
|
</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-container *ngIf="!normal_files_received && loading_files && loading_files.length > 0">
|
<ng-container *ngIf="!normal_files_received && loading_files && loading_files.length > 0">
|
||||||
|
|||||||
@@ -210,7 +210,7 @@ export class RecentVideosComponent implements OnInit {
|
|||||||
|
|
||||||
if (!this.postsService.config.Extra.file_manager_enabled) {
|
if (!this.postsService.config.Extra.file_manager_enabled) {
|
||||||
// tell server to delete the file once downloaded
|
// tell server to delete the file once downloaded
|
||||||
this.postsService.deleteFile(name, false).subscribe(delRes => {
|
this.postsService.deleteFile(name, type).subscribe(delRes => {
|
||||||
// reload mp4s
|
// reload mp4s
|
||||||
this.getAllFiles();
|
this.getAllFiles();
|
||||||
});
|
});
|
||||||
@@ -233,7 +233,7 @@ export class RecentVideosComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
deleteNormalFile(file, index, blacklistMode = false) {
|
deleteNormalFile(file, index, blacklistMode = false) {
|
||||||
this.postsService.deleteFile(file.uid, file.isAudio, blacklistMode).subscribe(result => {
|
this.postsService.deleteFile(file.uid, file.isAudio ? 'audio' : 'video', blacklistMode).subscribe(result => {
|
||||||
if (result) {
|
if (result) {
|
||||||
this.postsService.openSnackBar('Delete success!', 'OK.');
|
this.postsService.openSnackBar('Delete success!', 'OK.');
|
||||||
this.files.splice(index, 1);
|
this.files.splice(index, 1);
|
||||||
|
|||||||
11
src/app/components/twitch-chat/twitch-chat.component.html
Normal file
11
src/app/components/twitch-chat/twitch-chat.component.html
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<div class="chat-container" #scrollContainer *ngIf="visible_chat">
|
||||||
|
<div style="width: 250px; text-align: center;"><strong>Twitch Chat</strong></div>
|
||||||
|
<div style="max-width: 250px" *ngFor="let chat of visible_chat">
|
||||||
|
{{chat.timestamp_str}} - <strong>{{chat.name}}</strong>: {{chat.message}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ng-container *ngIf="chat_response_received && !full_chat">
|
||||||
|
<button (click)="downloadTwitchChat()" class="download-button" mat-raised-button color="accent"><ng-container i18n="Download Twitch Chat button">Download Twitch Chat</ng-container></button>
|
||||||
|
<mat-spinner *ngIf="downloading_chat" class="downloading-spinner" [diameter]="30"></mat-spinner>
|
||||||
|
</ng-container>
|
||||||
13
src/app/components/twitch-chat/twitch-chat.component.scss
Normal file
13
src/app/components/twitch-chat/twitch-chat.component.scss
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
.chat-container {
|
||||||
|
height: 100%;
|
||||||
|
overflow-y: scroll;
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-button {
|
||||||
|
margin: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.downloading-spinner {
|
||||||
|
top: 50%;
|
||||||
|
left: 80px;
|
||||||
|
}
|
||||||
25
src/app/components/twitch-chat/twitch-chat.component.spec.ts
Normal file
25
src/app/components/twitch-chat/twitch-chat.component.spec.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { TwitchChatComponent } from './twitch-chat.component';
|
||||||
|
|
||||||
|
describe('TwitchChatComponent', () => {
|
||||||
|
let component: TwitchChatComponent;
|
||||||
|
let fixture: ComponentFixture<TwitchChatComponent>;
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [ TwitchChatComponent ]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(TwitchChatComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
135
src/app/components/twitch-chat/twitch-chat.component.ts
Normal file
135
src/app/components/twitch-chat/twitch-chat.component.ts
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
import { AfterViewInit, Component, ElementRef, Input, OnInit, QueryList, ViewChild, ViewChildren } from '@angular/core';
|
||||||
|
import { PostsService } from 'app/posts.services';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-twitch-chat',
|
||||||
|
templateUrl: './twitch-chat.component.html',
|
||||||
|
styleUrls: ['./twitch-chat.component.scss']
|
||||||
|
})
|
||||||
|
export class TwitchChatComponent implements OnInit, AfterViewInit {
|
||||||
|
|
||||||
|
full_chat = null;
|
||||||
|
visible_chat = null;
|
||||||
|
chat_response_received = false;
|
||||||
|
downloading_chat = false;
|
||||||
|
|
||||||
|
current_chat_index = null;
|
||||||
|
|
||||||
|
CHAT_CHECK_INTERVAL_MS = 200;
|
||||||
|
chat_check_interval_obj = null;
|
||||||
|
|
||||||
|
scrollContainer = null;
|
||||||
|
|
||||||
|
@Input() db_file = null;
|
||||||
|
@Input() current_timestamp = null;
|
||||||
|
|
||||||
|
@ViewChild('scrollContainer') scrollRef: ElementRef;
|
||||||
|
|
||||||
|
constructor(private postsService: PostsService) { }
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.getFullChat();
|
||||||
|
}
|
||||||
|
|
||||||
|
ngAfterViewInit() {
|
||||||
|
}
|
||||||
|
|
||||||
|
private isUserNearBottom(): boolean {
|
||||||
|
const threshold = 300;
|
||||||
|
const position = this.scrollContainer.scrollTop + this.scrollContainer.offsetHeight;
|
||||||
|
const height = this.scrollContainer.scrollHeight;
|
||||||
|
return position > height - threshold;
|
||||||
|
}
|
||||||
|
|
||||||
|
scrollToBottom = () => {
|
||||||
|
this.scrollContainer.scrollTop = this.scrollContainer.scrollHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
addNewChatMessages() {
|
||||||
|
if (!this.scrollContainer) {
|
||||||
|
this.scrollContainer = this.scrollRef.nativeElement;
|
||||||
|
}
|
||||||
|
if (this.current_chat_index === null) {
|
||||||
|
this.current_chat_index = this.getIndexOfNextChat();
|
||||||
|
}
|
||||||
|
|
||||||
|
const latest_chat_timestamp = this.visible_chat.length ? this.visible_chat[this.visible_chat.length - 1]['timestamp'] : 0;
|
||||||
|
|
||||||
|
for (let i = this.current_chat_index + 1; i < this.full_chat.length; i++) {
|
||||||
|
if (this.full_chat[i]['timestamp'] >= latest_chat_timestamp && this.full_chat[i]['timestamp'] <= this.current_timestamp) {
|
||||||
|
this.visible_chat.push(this.full_chat[i]);
|
||||||
|
this.current_chat_index = i;
|
||||||
|
if (this.isUserNearBottom()) {
|
||||||
|
this.scrollToBottom();
|
||||||
|
}
|
||||||
|
} else if (this.full_chat[i]['timestamp'] > this.current_timestamp) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getIndexOfNextChat() {
|
||||||
|
const index = binarySearch(this.full_chat, 'timestamp', this.current_timestamp);
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
|
||||||
|
getFullChat() {
|
||||||
|
this.postsService.getFullTwitchChat(this.db_file.id, this.db_file.isAudio ? 'audio' : 'video', null).subscribe(res => {
|
||||||
|
this.chat_response_received = true;
|
||||||
|
if (res['chat']) {
|
||||||
|
this.initializeChatCheck(res['chat']);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
renewChat() {
|
||||||
|
this.visible_chat = [];
|
||||||
|
this.current_chat_index = this.getIndexOfNextChat();
|
||||||
|
}
|
||||||
|
|
||||||
|
downloadTwitchChat() {
|
||||||
|
this.downloading_chat = true;
|
||||||
|
let vodId = this.db_file.url.split('videos/').length > 1 && this.db_file.url.split('videos/')[1];
|
||||||
|
vodId = vodId.split('?')[0];
|
||||||
|
if (!vodId) {
|
||||||
|
this.postsService.openSnackBar('VOD url for this video is not supported. VOD ID must be after "twitch.tv/videos/"');
|
||||||
|
}
|
||||||
|
this.postsService.downloadTwitchChat(this.db_file.id, this.db_file.isAudio ? 'audio' : 'video', vodId, null).subscribe(res => {
|
||||||
|
if (res['chat']) {
|
||||||
|
this.initializeChatCheck(res['chat']);
|
||||||
|
} else {
|
||||||
|
this.downloading_chat = false;
|
||||||
|
this.postsService.openSnackBar('Download failed.')
|
||||||
|
}
|
||||||
|
}, err => {
|
||||||
|
this.downloading_chat = false;
|
||||||
|
this.postsService.openSnackBar('Chat could not be downloaded.')
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
initializeChatCheck(full_chat) {
|
||||||
|
this.full_chat = full_chat;
|
||||||
|
this.visible_chat = [];
|
||||||
|
this.chat_check_interval_obj = setInterval(() => this.addNewChatMessages(), this.CHAT_CHECK_INTERVAL_MS);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function binarySearch(arr, key, n) {
|
||||||
|
let min = 0;
|
||||||
|
let max = arr.length - 1;
|
||||||
|
let mid;
|
||||||
|
while (min <= max) {
|
||||||
|
// tslint:disable-next-line: no-bitwise
|
||||||
|
mid = (min + max) >>> 1;
|
||||||
|
if (arr[mid][key] === n) {
|
||||||
|
return mid;
|
||||||
|
} else if (arr[mid][key] < n) {
|
||||||
|
min = mid + 1;
|
||||||
|
} else {
|
||||||
|
max = mid - 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return min;
|
||||||
|
}
|
||||||
@@ -41,7 +41,7 @@
|
|||||||
<div style="padding:5px">
|
<div style="padding:5px">
|
||||||
<div *ngIf="!loading && file_obj.thumbnailURL" class="img-div">
|
<div *ngIf="!loading && file_obj.thumbnailURL" class="img-div">
|
||||||
<div style="position: relative">
|
<div style="position: relative">
|
||||||
<img [ngClass]="{'image-small': card_size === 'small', 'image': card_size === 'medium', 'image-large': card_size === 'large'}" [src]="file_obj.thumbnailBlob ? thumbnailBlobURL : file_obj.thumbnailURL" alt="Thumbnail">
|
<img [ngClass]="{'image-small': card_size === 'small', 'image': card_size === 'medium', 'image-large': card_size === 'large'}" [src]="file_obj.thumbnailPath ? thumbnailBlobURL : file_obj.thumbnailURL" alt="Thumbnail">
|
||||||
<div class="duration-time">
|
<div class="duration-time">
|
||||||
{{file_length}}
|
{{file_length}}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -44,11 +44,13 @@ export class UnifiedFileCardComponent implements OnInit {
|
|||||||
@Input() is_playlist = false;
|
@Input() is_playlist = false;
|
||||||
@Input() index: number;
|
@Input() index: number;
|
||||||
@Input() locale = null;
|
@Input() locale = null;
|
||||||
|
@Input() baseStreamPath = null;
|
||||||
|
@Input() jwtString = null;
|
||||||
@Output() goToFile = new EventEmitter<any>();
|
@Output() goToFile = new EventEmitter<any>();
|
||||||
@Output() goToSubscription = new EventEmitter<any>();
|
@Output() goToSubscription = new EventEmitter<any>();
|
||||||
@Output() deleteFile = new EventEmitter<any>();
|
@Output() deleteFile = new EventEmitter<any>();
|
||||||
@Output() editPlaylist = new EventEmitter<any>();
|
@Output() editPlaylist = new EventEmitter<any>();
|
||||||
|
|
||||||
|
|
||||||
@ViewChild(MatMenuTrigger) contextMenu: MatMenuTrigger;
|
@ViewChild(MatMenuTrigger) contextMenu: MatMenuTrigger;
|
||||||
contextMenuPosition = { x: '0px', y: '0px' };
|
contextMenuPosition = { x: '0px', y: '0px' };
|
||||||
@@ -67,11 +69,12 @@ export class UnifiedFileCardComponent implements OnInit {
|
|||||||
this.file_length = fancyTimeFormat(this.file_obj.duration);
|
this.file_length = fancyTimeFormat(this.file_obj.duration);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.file_obj && this.file_obj.thumbnailBlob) {
|
if (this.file_obj && this.file_obj.thumbnailPath) {
|
||||||
const mime = getMimeByFilename(this.file_obj.thumbnailPath);
|
this.thumbnailBlobURL = `${this.baseStreamPath}thumbnail/${encodeURIComponent(this.file_obj.thumbnailPath)}${this.jwtString}`;
|
||||||
|
/*const mime = getMimeByFilename(this.file_obj.thumbnailPath);
|
||||||
const blob = new Blob([new Uint8Array(this.file_obj.thumbnailBlob.data)], {type: mime});
|
const blob = new Blob([new Uint8Array(this.file_obj.thumbnailBlob.data)], {type: mime});
|
||||||
const bloburl = URL.createObjectURL(blob);
|
const bloburl = URL.createObjectURL(blob);
|
||||||
this.thumbnailBlobURL = this.sanitizer.bypassSecurityTrustUrl(bloburl);
|
this.thumbnailBlobURL = this.sanitizer.bypassSecurityTrustUrl(bloburl);*/
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
</mat-dialog-content>
|
</mat-dialog-content>
|
||||||
<mat-dialog-actions>
|
<mat-dialog-actions>
|
||||||
<!-- The mat-dialog-close directive optionally accepts a value as a result for the dialog. -->
|
<!-- The mat-dialog-close directive optionally accepts a value as a result for the dialog. -->
|
||||||
<button color="primary" mat-flat-button type="submit" (click)="confirmClicked()">{{submitText}}</button>
|
<button [color]="warnSubmitColor ? 'warn' : 'primary'" mat-flat-button type="submit" (click)="confirmClicked()">{{submitText}}</button>
|
||||||
<div class="mat-spinner" *ngIf="submitClicked">
|
<div class="mat-spinner" *ngIf="submitClicked">
|
||||||
<mat-spinner [diameter]="25"></mat-spinner>
|
<mat-spinner [diameter]="25"></mat-spinner>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -15,12 +15,14 @@ export class ConfirmDialogComponent implements OnInit {
|
|||||||
|
|
||||||
doneEmitter: EventEmitter<any> = null;
|
doneEmitter: EventEmitter<any> = null;
|
||||||
onlyEmitOnDone = false;
|
onlyEmitOnDone = false;
|
||||||
|
|
||||||
|
warnSubmitColor = false;
|
||||||
|
|
||||||
constructor(@Inject(MAT_DIALOG_DATA) public data: any, public dialogRef: MatDialogRef<ConfirmDialogComponent>) {
|
constructor(@Inject(MAT_DIALOG_DATA) public data: any, public dialogRef: MatDialogRef<ConfirmDialogComponent>) {
|
||||||
if (this.data.dialogTitle) { this.dialogTitle = this.data.dialogTitle };
|
if (this.data.dialogTitle) { this.dialogTitle = this.data.dialogTitle };
|
||||||
if (this.data.dialogText) { this.dialogText = this.data.dialogText };
|
if (this.data.dialogText) { this.dialogText = this.data.dialogText };
|
||||||
if (this.data.submitText) { this.submitText = this.data.submitText };
|
if (this.data.submitText) { this.submitText = this.data.submitText };
|
||||||
|
if (this.data.warnSubmitColor) { this.warnSubmitColor = this.data.warnSubmitColor };
|
||||||
|
|
||||||
// checks if emitter exists, if so don't autoclose as it should be handled by caller
|
// checks if emitter exists, if so don't autoclose as it should be handled by caller
|
||||||
if (this.data.doneEmitter) {
|
if (this.data.doneEmitter) {
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
<h4 mat-dialog-title><ng-container i18n="Editing category dialog title">Editing category</ng-container> {{category['name']}}</h4>
|
||||||
|
|
||||||
|
<mat-dialog-content style="max-height: 50vh">
|
||||||
|
<mat-form-field style="width: 250px; margin-bottom: 5px;">
|
||||||
|
<input matInput placeholder="Name" i18n-placeholder="Name" [(ngModel)]="category['name']" required>
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<mat-divider></mat-divider>
|
||||||
|
|
||||||
|
<h6 style="margin-top: 20px;" i18n="Rules">Rules</h6>
|
||||||
|
|
||||||
|
<mat-list>
|
||||||
|
<mat-list-item *ngFor="let rule of category['rules']; let i = index">
|
||||||
|
<mat-form-field [style.visibility]="i === 0 ? 'hidden' : null" class="operator-select">
|
||||||
|
<mat-select [disabled]="i === 0" [(ngModel)]="rule['preceding_operator']">
|
||||||
|
<mat-option value="or">OR</mat-option>
|
||||||
|
<mat-option value="and">AND</mat-option>
|
||||||
|
</mat-select>
|
||||||
|
</mat-form-field>
|
||||||
|
<mat-form-field class="property-select">
|
||||||
|
<mat-select [(ngModel)]="rule['property']">
|
||||||
|
<mat-option *ngFor="let propertyOption of propertyOptions" [value]="propertyOption.value">{{propertyOption.label}}</mat-option>
|
||||||
|
</mat-select>
|
||||||
|
</mat-form-field>
|
||||||
|
<mat-form-field class="comparator-select">
|
||||||
|
<mat-select [(ngModel)]="rule['comparator']">
|
||||||
|
<mat-option *ngFor="let comparatorOption of comparatorOptions" [value]="comparatorOption.value">{{comparatorOption.label}}</mat-option>
|
||||||
|
</mat-select>
|
||||||
|
</mat-form-field>
|
||||||
|
<mat-form-field class="value-input">
|
||||||
|
<input matInput [(ngModel)]="rule['value']">
|
||||||
|
</mat-form-field>
|
||||||
|
<button [disabled]="i === category['rules'].length-1" (click)="swapRules(i, i+1)" mat-icon-button><mat-icon>arrow_downward</mat-icon></button>
|
||||||
|
<button [disabled]="i === 0" (click)="swapRules(i, i-1)" mat-icon-button><mat-icon>arrow_upward</mat-icon></button>
|
||||||
|
<button (click)="removeRule(i)" mat-icon-button><mat-icon>cancel</mat-icon></button>
|
||||||
|
</mat-list-item>
|
||||||
|
</mat-list>
|
||||||
|
|
||||||
|
<button style="margin-bottom: 8px;" mat-icon-button (click)="addNewRule()" matTooltip="Add new rule" i18n-matTooltip="Add new rule tooltip"><mat-icon>add</mat-icon></button>
|
||||||
|
|
||||||
|
<mat-divider></mat-divider>
|
||||||
|
|
||||||
|
<mat-form-field style="width: 250px; margin-top: 10px;">
|
||||||
|
<input matInput [(ngModel)]="category['custom_output']" placeholder="Custom file output" i18n-placeholder="Category custom file output placeholder">
|
||||||
|
<mat-hint>
|
||||||
|
<a target="_blank" href="https://github.com/ytdl-org/youtube-dl/blob/master/README.md#output-template">
|
||||||
|
<ng-container i18n="Custom output template documentation link">Documentation</ng-container></a>.
|
||||||
|
<ng-container i18n="Custom Output input hint">Path is relative to the config download path. Don't include extension.</ng-container>
|
||||||
|
</mat-hint>
|
||||||
|
</mat-form-field>
|
||||||
|
</mat-dialog-content>
|
||||||
|
|
||||||
|
<mat-dialog-actions>
|
||||||
|
<button mat-button mat-dialog-close><ng-container i18n="Cancel">Cancel</ng-container></button>
|
||||||
|
|
||||||
|
<button mat-button [disabled]="categoryChanged()" type="submit" (click)="saveClicked()"><ng-container i18n="Save button">Save</ng-container></button>
|
||||||
|
<div class="mat-spinner" *ngIf="updating">
|
||||||
|
<mat-spinner [diameter]="25"></mat-spinner>
|
||||||
|
</div>
|
||||||
|
</mat-dialog-actions>
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
.operator-select {
|
||||||
|
width: 55px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.property-select {
|
||||||
|
margin-left: 10px;
|
||||||
|
width: 110px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comparator-select {
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value-input {
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { EditCategoryDialogComponent } from './edit-category-dialog.component';
|
||||||
|
|
||||||
|
describe('EditCategoryDialogComponent', () => {
|
||||||
|
let component: EditCategoryDialogComponent;
|
||||||
|
let fixture: ComponentFixture<EditCategoryDialogComponent>;
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [ EditCategoryDialogComponent ]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(EditCategoryDialogComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
import { Component, OnInit, Inject } from '@angular/core';
|
||||||
|
import { MAT_DIALOG_DATA } from '@angular/material/dialog';
|
||||||
|
import { PostsService } from 'app/posts.services';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-edit-category-dialog',
|
||||||
|
templateUrl: './edit-category-dialog.component.html',
|
||||||
|
styleUrls: ['./edit-category-dialog.component.scss']
|
||||||
|
})
|
||||||
|
export class EditCategoryDialogComponent implements OnInit {
|
||||||
|
|
||||||
|
updating = false;
|
||||||
|
original_category = null;
|
||||||
|
category = null;
|
||||||
|
|
||||||
|
propertyOptions = [
|
||||||
|
{
|
||||||
|
value: 'fulltitle',
|
||||||
|
label: 'Title'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'id',
|
||||||
|
label: 'ID'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'webpage_url',
|
||||||
|
label: 'URL'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'view_count',
|
||||||
|
label: 'Views'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'uploader',
|
||||||
|
label: 'Uploader'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: '_filename',
|
||||||
|
label: 'File Name'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'tags',
|
||||||
|
label: 'Tags'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
comparatorOptions = [
|
||||||
|
{
|
||||||
|
value: 'includes',
|
||||||
|
label: 'includes'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'not_includes',
|
||||||
|
label: 'not includes'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'equals',
|
||||||
|
label: 'equals'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'not_equals',
|
||||||
|
label: 'not equals'
|
||||||
|
},
|
||||||
|
|
||||||
|
];
|
||||||
|
|
||||||
|
constructor(@Inject(MAT_DIALOG_DATA) public data: any, private postsService: PostsService) {
|
||||||
|
if (this.data) {
|
||||||
|
this.original_category = this.data.category;
|
||||||
|
this.category = JSON.parse(JSON.stringify(this.original_category));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
}
|
||||||
|
|
||||||
|
addNewRule() {
|
||||||
|
this.category['rules'].push({
|
||||||
|
preceding_operator: 'or',
|
||||||
|
property: 'fulltitle',
|
||||||
|
comparator: 'includes',
|
||||||
|
value: ''
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
saveClicked() {
|
||||||
|
this.updating = true;
|
||||||
|
this.postsService.updateCategory(this.category).subscribe(res => {
|
||||||
|
this.updating = false;
|
||||||
|
this.original_category = JSON.parse(JSON.stringify(this.category));
|
||||||
|
this.postsService.reloadCategories();
|
||||||
|
}, err => {
|
||||||
|
this.updating = false;
|
||||||
|
console.error(err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
categoryChanged() {
|
||||||
|
return JSON.stringify(this.category) === JSON.stringify(this.original_category);
|
||||||
|
}
|
||||||
|
|
||||||
|
swapRules(original_index, new_index) {
|
||||||
|
[this.category.rules[original_index], this.category.rules[new_index]] = [this.category.rules[new_index],
|
||||||
|
this.category.rules[original_index]];
|
||||||
|
}
|
||||||
|
|
||||||
|
removeRule(index) {
|
||||||
|
this.category['rules'].splice(index, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -24,6 +24,13 @@
|
|||||||
<mat-checkbox [disabled]="true" [(ngModel)]="audioOnlyMode"><ng-container i18n="Streaming-only mode">Audio-only mode</ng-container></mat-checkbox>
|
<mat-checkbox [disabled]="true" [(ngModel)]="audioOnlyMode"><ng-container i18n="Streaming-only mode">Audio-only mode</ng-container></mat-checkbox>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="col-12 mt-2">
|
||||||
|
<mat-form-field>
|
||||||
|
<mat-select placeholder="Max quality" i18n-placeholder="Max quality placeholder" [disabled]="new_sub.type === 'audio'" [(ngModel)]="new_sub.maxQuality">
|
||||||
|
<mat-option *ngFor="let available_quality of available_qualities" [value]="available_quality['value']">{{available_quality['label']}}</mat-option>
|
||||||
|
</mat-select>
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<div>
|
<div>
|
||||||
<mat-checkbox [disabled]="new_sub.type === 'audio'" [(ngModel)]="new_sub.streamingOnly"><ng-container i18n="Streaming-only mode">Streaming-only mode</ng-container></mat-checkbox>
|
<mat-checkbox [disabled]="new_sub.type === 'audio'" [(ngModel)]="new_sub.streamingOnly"><ng-container i18n="Streaming-only mode">Streaming-only mode</ng-container></mat-checkbox>
|
||||||
|
|||||||
@@ -22,6 +22,36 @@ export class EditSubscriptionDialogComponent implements OnInit {
|
|||||||
audioOnlyMode = null;
|
audioOnlyMode = null;
|
||||||
download_all = null;
|
download_all = null;
|
||||||
|
|
||||||
|
available_qualities = [
|
||||||
|
{
|
||||||
|
'label': 'Best',
|
||||||
|
'value': 'best'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'label': '4K',
|
||||||
|
'value': '2160'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'label': '1440p',
|
||||||
|
'value': '1440'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'label': '1080p',
|
||||||
|
'value': '1080'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'label': '720p',
|
||||||
|
'value': '720'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'label': '480p',
|
||||||
|
'value': '480'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'label': '360p',
|
||||||
|
'value': '360'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
time_units = [
|
time_units = [
|
||||||
'day',
|
'day',
|
||||||
@@ -39,16 +69,12 @@ export class EditSubscriptionDialogComponent implements OnInit {
|
|||||||
|
|
||||||
if (this.sub.timerange) {
|
if (this.sub.timerange) {
|
||||||
const timerange_str = this.sub.timerange.split('-')[1];
|
const timerange_str = this.sub.timerange.split('-')[1];
|
||||||
console.log(timerange_str);
|
|
||||||
const number = timerange_str.replace(/\D/g,'');
|
const number = timerange_str.replace(/\D/g,'');
|
||||||
let units = timerange_str.replace(/[0-9]/g, '');
|
let units = timerange_str.replace(/[0-9]/g, '');
|
||||||
|
|
||||||
console.log(units);
|
if (+number === 1) {
|
||||||
|
units = units.replace('s', '');
|
||||||
// // remove plural on units
|
}
|
||||||
// if (units[units.length-1] === 's') {
|
|
||||||
// units = units.substring(0, units.length-1);
|
|
||||||
// }
|
|
||||||
|
|
||||||
this.timerange_amount = parseInt(number);
|
this.timerange_amount = parseInt(number);
|
||||||
this.timerange_unit = units;
|
this.timerange_unit = units;
|
||||||
@@ -71,9 +97,10 @@ export class EditSubscriptionDialogComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
saveSubscription() {
|
saveSubscription() {
|
||||||
this.postsService.updateSubscription(this.sub).subscribe(res => {
|
this.postsService.updateSubscription(this.new_sub).subscribe(res => {
|
||||||
this.sub = this.new_sub;
|
this.sub = this.new_sub;
|
||||||
this.new_sub = JSON.parse(JSON.stringify(this.sub));
|
this.new_sub = JSON.parse(JSON.stringify(this.sub));
|
||||||
|
this.postsService.reloadSubscriptions();
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,12 +112,16 @@ export class EditSubscriptionDialogComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
timerangeChanged(value, select_changed) {
|
timerangeChanged(value, select_changed) {
|
||||||
console.log(this.timerange_amount);
|
if (+this.timerange_amount === 1) {
|
||||||
console.log(this.timerange_unit);
|
this.timerange_unit = this.timerange_unit.replace('s', '');
|
||||||
|
} else {
|
||||||
|
if (!this.timerange_unit.includes('s')) {
|
||||||
|
this.timerange_unit += 's';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (this.timerange_amount && this.timerange_unit && !this.download_all) {
|
if (this.timerange_amount && this.timerange_unit && !this.download_all) {
|
||||||
this.new_sub.timerange = 'now-' + this.timerange_amount.toString() + this.timerange_unit;
|
this.new_sub.timerange = 'now-' + this.timerange_amount.toString() + this.timerange_unit;
|
||||||
console.log(this.new_sub.timerange);
|
|
||||||
} else {
|
} else {
|
||||||
this.new_sub.timerange = null;
|
this.new_sub.timerange = null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,6 +35,13 @@
|
|||||||
</mat-select>
|
</mat-select>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="col-12 mt-2">
|
||||||
|
<mat-form-field>
|
||||||
|
<mat-select placeholder="Max quality" i18n-placeholder="Max quality placeholder" [disabled]="audioOnlyMode" [(ngModel)]="maxQuality">
|
||||||
|
<mat-option *ngFor="let available_quality of available_qualities" [value]="available_quality['value']">{{available_quality['label']}}</mat-option>
|
||||||
|
</mat-select>
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<div>
|
<div>
|
||||||
<mat-checkbox [(ngModel)]="audioOnlyMode"><ng-container i18n="Streaming-only mode">Audio-only mode</ng-container></mat-checkbox>
|
<mat-checkbox [(ngModel)]="audioOnlyMode"><ng-container i18n="Streaming-only mode">Audio-only mode</ng-container></mat-checkbox>
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ export class SubscribeDialogComponent implements OnInit {
|
|||||||
url = null;
|
url = null;
|
||||||
name = null;
|
name = null;
|
||||||
|
|
||||||
|
maxQuality = 'best';
|
||||||
|
|
||||||
// state
|
// state
|
||||||
subscribing = false;
|
subscribing = false;
|
||||||
|
|
||||||
@@ -29,12 +31,43 @@ export class SubscribeDialogComponent implements OnInit {
|
|||||||
customFileOutput = '';
|
customFileOutput = '';
|
||||||
customArgs = '';
|
customArgs = '';
|
||||||
|
|
||||||
|
available_qualities = [
|
||||||
|
{
|
||||||
|
'label': 'Best',
|
||||||
|
'value': 'best'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'label': '4K',
|
||||||
|
'value': '2160'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'label': '1440p',
|
||||||
|
'value': '1440'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'label': '1080p',
|
||||||
|
'value': '1080'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'label': '720p',
|
||||||
|
'value': '720'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'label': '480p',
|
||||||
|
'value': '480'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'label': '360p',
|
||||||
|
'value': '360'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
time_units = [
|
time_units = [
|
||||||
'day',
|
'day',
|
||||||
'week',
|
'week',
|
||||||
'month',
|
'month',
|
||||||
'year'
|
'year'
|
||||||
]
|
];
|
||||||
|
|
||||||
constructor(private postsService: PostsService,
|
constructor(private postsService: PostsService,
|
||||||
private snackBar: MatSnackBar,
|
private snackBar: MatSnackBar,
|
||||||
@@ -57,7 +90,7 @@ export class SubscribeDialogComponent implements OnInit {
|
|||||||
if (!this.download_all) {
|
if (!this.download_all) {
|
||||||
timerange = 'now-' + this.timerange_amount.toString() + this.timerange_unit;
|
timerange = 'now-' + this.timerange_amount.toString() + this.timerange_unit;
|
||||||
}
|
}
|
||||||
this.postsService.createSubscription(this.url, this.name, timerange, this.streamingOnlyMode,
|
this.postsService.createSubscription(this.url, this.name, timerange, this.streamingOnlyMode, this.maxQuality,
|
||||||
this.audioOnlyMode, this.customArgs, this.customFileOutput).subscribe(res => {
|
this.audioOnlyMode, this.customArgs, this.customFileOutput).subscribe(res => {
|
||||||
this.subscribing = false;
|
this.subscribing = false;
|
||||||
if (res['new_sub']) {
|
if (res['new_sub']) {
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ export class FileCardComponent implements OnInit {
|
|||||||
|
|
||||||
deleteFile(blacklistMode = false) {
|
deleteFile(blacklistMode = false) {
|
||||||
if (!this.playlist) {
|
if (!this.playlist) {
|
||||||
this.postsService.deleteFile(this.uid, this.isAudio, blacklistMode).subscribe(result => {
|
this.postsService.deleteFile(this.uid, this.isAudio ? 'audio' : 'video', blacklistMode).subscribe(result => {
|
||||||
if (result) {
|
if (result) {
|
||||||
this.openSnackBar('Delete success!', 'OK.');
|
this.openSnackBar('Delete success!', 'OK.');
|
||||||
this.removeFile.emit(this.name);
|
this.removeFile.emit(this.name);
|
||||||
|
|||||||
@@ -182,7 +182,7 @@
|
|||||||
</ng-template>
|
</ng-template>
|
||||||
|
|
||||||
<ng-container *ngIf="cachedFileManagerEnabled || fileManagerEnabled">
|
<ng-container *ngIf="cachedFileManagerEnabled || fileManagerEnabled">
|
||||||
<app-recent-videos></app-recent-videos>
|
<app-recent-videos #recentVideos></app-recent-videos>
|
||||||
<br/>
|
<br/>
|
||||||
<h4 style="text-align: center">Custom playlists</h4>
|
<h4 style="text-align: center">Custom playlists</h4>
|
||||||
<app-custom-playlists></app-custom-playlists>
|
<app-custom-playlists></app-custom-playlists>
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import { CreatePlaylistComponent } from 'app/create-playlist/create-playlist.com
|
|||||||
import { Platform } from '@angular/cdk/platform';
|
import { Platform } from '@angular/cdk/platform';
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
import { ArgModifierDialogComponent } from 'app/dialogs/arg-modifier-dialog/arg-modifier-dialog.component';
|
import { ArgModifierDialogComponent } from 'app/dialogs/arg-modifier-dialog/arg-modifier-dialog.component';
|
||||||
|
import { RecentVideosComponent } from 'app/components/recent-videos/recent-videos.component';
|
||||||
|
|
||||||
export let audioFilesMouseHovering = false;
|
export let audioFilesMouseHovering = false;
|
||||||
export let videoFilesMouseHovering = false;
|
export let videoFilesMouseHovering = false;
|
||||||
@@ -200,6 +201,7 @@ export class MainComponent implements OnInit {
|
|||||||
formats_loading = false;
|
formats_loading = false;
|
||||||
|
|
||||||
@ViewChild('urlinput', { read: ElementRef }) urlInput: ElementRef;
|
@ViewChild('urlinput', { read: ElementRef }) urlInput: ElementRef;
|
||||||
|
@ViewChild('recentVideos') recentVideos: RecentVideosComponent;
|
||||||
@ViewChildren('audiofilecard') audioFileCards: QueryList<FileCardComponent>;
|
@ViewChildren('audiofilecard') audioFileCards: QueryList<FileCardComponent>;
|
||||||
@ViewChildren('videofilecard') videoFileCards: QueryList<FileCardComponent>;
|
@ViewChildren('videofilecard') videoFileCards: QueryList<FileCardComponent>;
|
||||||
last_valid_url = '';
|
last_valid_url = '';
|
||||||
@@ -487,6 +489,7 @@ export class MainComponent implements OnInit {
|
|||||||
this.downloadingfile = false;
|
this.downloadingfile = false;
|
||||||
if (this.multiDownloadMode && !this.downloadOnlyMode && !navigate_mode) {
|
if (this.multiDownloadMode && !this.downloadOnlyMode && !navigate_mode) {
|
||||||
// do nothing
|
// do nothing
|
||||||
|
this.reloadRecentVideos();
|
||||||
} else {
|
} else {
|
||||||
// if download only mode, just download the file. no redirect
|
// if download only mode, just download the file. no redirect
|
||||||
if (forceView === false && this.downloadOnlyMode && !this.iOS) {
|
if (forceView === false && this.downloadOnlyMode && !this.iOS) {
|
||||||
@@ -496,6 +499,7 @@ export class MainComponent implements OnInit {
|
|||||||
} else {
|
} else {
|
||||||
this.downloadAudioFile(decodeURI(name));
|
this.downloadAudioFile(decodeURI(name));
|
||||||
}
|
}
|
||||||
|
this.reloadRecentVideos();
|
||||||
} else {
|
} else {
|
||||||
localStorage.setItem('player_navigator', this.router.url.split(';')[0]);
|
localStorage.setItem('player_navigator', this.router.url.split(';')[0]);
|
||||||
if (is_playlist) {
|
if (is_playlist) {
|
||||||
@@ -524,6 +528,7 @@ export class MainComponent implements OnInit {
|
|||||||
this.downloadingfile = false;
|
this.downloadingfile = false;
|
||||||
if (this.multiDownloadMode && !this.downloadOnlyMode && !navigate_mode) {
|
if (this.multiDownloadMode && !this.downloadOnlyMode && !navigate_mode) {
|
||||||
// do nothing
|
// do nothing
|
||||||
|
this.reloadRecentVideos();
|
||||||
} else {
|
} else {
|
||||||
// if download only mode, just download the file. no redirect
|
// if download only mode, just download the file. no redirect
|
||||||
if (forceView === false && this.downloadOnlyMode) {
|
if (forceView === false && this.downloadOnlyMode) {
|
||||||
@@ -533,6 +538,7 @@ export class MainComponent implements OnInit {
|
|||||||
} else {
|
} else {
|
||||||
this.downloadVideoFile(decodeURI(name));
|
this.downloadVideoFile(decodeURI(name));
|
||||||
}
|
}
|
||||||
|
this.reloadRecentVideos();
|
||||||
} else {
|
} else {
|
||||||
localStorage.setItem('player_navigator', this.router.url.split(';')[0]);
|
localStorage.setItem('player_navigator', this.router.url.split(';')[0]);
|
||||||
if (is_playlist) {
|
if (is_playlist) {
|
||||||
@@ -746,7 +752,7 @@ export class MainComponent implements OnInit {
|
|||||||
|
|
||||||
if (!this.fileManagerEnabled) {
|
if (!this.fileManagerEnabled) {
|
||||||
// tell server to delete the file once downloaded
|
// tell server to delete the file once downloaded
|
||||||
this.postsService.deleteFile(name, true).subscribe(delRes => {
|
this.postsService.deleteFile(name, 'video').subscribe(delRes => {
|
||||||
// reload mp3s
|
// reload mp3s
|
||||||
this.getMp3s();
|
this.getMp3s();
|
||||||
});
|
});
|
||||||
@@ -763,7 +769,7 @@ export class MainComponent implements OnInit {
|
|||||||
|
|
||||||
if (!this.fileManagerEnabled) {
|
if (!this.fileManagerEnabled) {
|
||||||
// tell server to delete the file once downloaded
|
// tell server to delete the file once downloaded
|
||||||
this.postsService.deleteFile(name, false).subscribe(delRes => {
|
this.postsService.deleteFile(name, 'audio').subscribe(delRes => {
|
||||||
// reload mp4s
|
// reload mp4s
|
||||||
this.getMp4s();
|
this.getMp4s();
|
||||||
});
|
});
|
||||||
@@ -1161,4 +1167,10 @@ export class MainComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
reloadRecentVideos() {
|
||||||
|
if (this.recentVideos) {
|
||||||
|
this.recentVideos.getAllFiles();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,35 +1,46 @@
|
|||||||
<div *ngIf="playlist.length > 0 && show_player">
|
<div style="height: 100%" *ngIf="playlist.length > 0 && show_player">
|
||||||
<div [ngClass]="(type === 'audio') ? null : 'container-video'">
|
<div style="height: 100%" [ngClass]="(type === 'audio') ? null : 'container-video'">
|
||||||
<div style="max-width: 100%; margin-left: 0px; height: 70vh">
|
<div style="max-width: 100%; margin-left: 0px; height: 100%">
|
||||||
<div style="height: fit-content" [ngClass]="(type === 'audio') ? 'audio-col' : 'video-col'">
|
<mat-drawer-container style="height: 100%" class="example-container" autosize>
|
||||||
<vg-player style="height: fit-content; max-height: 75vh" (onPlayerReady)="onPlayerReady($event)" [style.background-color]="(type === 'audio') ? 'transparent' : 'black'">
|
<div style="height: fit-content" [ngClass]="(type === 'audio') ? 'audio-col' : 'video-col'">
|
||||||
<video [ngClass]="(type === 'audio') ? 'audio-styles' : 'video-styles'" #media class="video-player" [vgMedia]="media" [src]="currentItem.src" id="singleVideo" preload="auto" controls>
|
<vg-player style="height: fit-content; max-height: 75vh" (onPlayerReady)="onPlayerReady($event)" [style.background-color]="(type === 'audio') ? 'transparent' : 'black'">
|
||||||
</video>
|
<video [ngClass]="(type === 'audio') ? 'audio-styles' : 'video-styles'" #media class="video-player" [vgMedia]="media" [src]="currentItem.src" id="singleVideo" preload="auto" controls>
|
||||||
</vg-player>
|
</video>
|
||||||
</div>
|
</vg-player>
|
||||||
<div style="height: fit-content; width: 100%; margin-top: 10px;">
|
</div>
|
||||||
<mat-button-toggle-group cdkDropList [cdkDropListSortingDisabled]="!id" (cdkDropListDropped)="drop($event)" style="width: 80%; left: 9%" vertical name="videoSelect" aria-label="Video Select" #group="matButtonToggleGroup">
|
<div style="height: fit-content; width: 100%; margin-top: 10px;">
|
||||||
<mat-button-toggle cdkDrag *ngFor="let playlist_item of playlist; let i = index" [checked]="currentItem.title === playlist_item.title" (click)="onClickPlaylistItem(playlist_item, i)" class="toggle-button" [value]="playlist_item.title">{{playlist_item.label}}</mat-button-toggle>
|
<mat-button-toggle-group cdkDropList [cdkDropListSortingDisabled]="!id" (cdkDropListDropped)="drop($event)" style="width: 80%; left: 9%" vertical name="videoSelect" aria-label="Video Select" #group="matButtonToggleGroup">
|
||||||
</mat-button-toggle-group>
|
<mat-button-toggle cdkDrag *ngFor="let playlist_item of playlist; let i = index" [checked]="currentItem.title === playlist_item.title" (click)="onClickPlaylistItem(playlist_item, i)" class="toggle-button" [value]="playlist_item.title">{{playlist_item.label}}</mat-button-toggle>
|
||||||
</div>
|
</mat-button-toggle-group>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<mat-drawer #drawer class="example-sidenav" mode="side" position="end" opened="false">
|
||||||
|
<ng-container *ngIf="api_ready && db_file && db_file.url.includes('twitch.tv/videos/')">
|
||||||
|
<app-twitch-chat #twitchchat [db_file]="db_file" [current_timestamp]="api.currentTime"></app-twitch-chat>
|
||||||
|
</ng-container>
|
||||||
|
</mat-drawer>
|
||||||
|
|
||||||
|
<div *ngIf="db_file && db_file.url.includes('twitch.tv/videos/') && postsService['config']['API']['use_twitch_API']" style="position: absolute; right: 0px">
|
||||||
|
<button style="right: 0px; top: -46px;" (click)="drawer.toggle()" mat-icon-button><mat-icon>chat</mat-icon></button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="update-playlist-button-div" *ngIf="id && playlistChanged()">
|
<div class="update-playlist-button-div" *ngIf="id && playlistChanged()">
|
||||||
<div class="spinner-div">
|
<div class="spinner-div">
|
||||||
<mat-spinner *ngIf="playlist_updating" [diameter]="25"></mat-spinner>
|
<mat-spinner *ngIf="playlist_updating" [diameter]="25"></mat-spinner>
|
||||||
</div>
|
</div>
|
||||||
<button color="primary" [disabled]="playlist_updating" (click)="updatePlaylist()" mat-raised-button><ng-container i18n="Playlist save changes button">Save changes</ng-container> <mat-icon>update</mat-icon></button>
|
<button color="primary" [disabled]="playlist_updating" (click)="updatePlaylist()" mat-raised-button><ng-container i18n="Playlist save changes button">Save changes</ng-container> <mat-icon>update</mat-icon></button>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div *ngIf="playlist.length > 1">
|
<div *ngIf="playlist.length > 1">
|
||||||
<button class="save-button" color="primary" (click)="downloadContent()" [disabled]="downloading" mat-fab><mat-icon class="save-icon">save</mat-icon><mat-spinner *ngIf="downloading" class="spinner" [diameter]="50"></mat-spinner></button>
|
<button class="save-button" color="primary" (click)="downloadContent()" [disabled]="downloading" mat-fab><mat-icon class="save-icon">save</mat-icon><mat-spinner *ngIf="downloading" class="spinner" [diameter]="50"></mat-spinner></button>
|
||||||
<button *ngIf="!id" color="accent" class="favorite-button" color="primary" (click)="namePlaylistDialog()" mat-fab><mat-icon class="save-icon">favorite</mat-icon></button>
|
<button *ngIf="!id" color="accent" class="favorite-button" color="primary" (click)="namePlaylistDialog()" mat-fab><mat-icon class="save-icon">favorite</mat-icon></button>
|
||||||
<button *ngIf="!is_shared && id && (!postsService.isLoggedIn || postsService.permissions.includes('sharing'))" class="share-button" color="primary" (click)="openShareDialog()" mat-fab><mat-icon class="save-icon">share</mat-icon></button>
|
<button *ngIf="!is_shared && id && (!postsService.isLoggedIn || postsService.permissions.includes('sharing'))" class="share-button" color="primary" (click)="openShareDialog()" mat-fab><mat-icon class="save-icon">share</mat-icon></button>
|
||||||
</div>
|
</div>
|
||||||
<div *ngIf="playlist.length === 1">
|
<div *ngIf="playlist.length === 1">
|
||||||
<button class="save-button" color="primary" (click)="downloadFile()" [disabled]="downloading" mat-fab><mat-icon class="save-icon">save</mat-icon><mat-spinner *ngIf="downloading" class="spinner" [diameter]="50"></mat-spinner></button>
|
<button class="save-button" color="primary" (click)="downloadFile()" [disabled]="downloading" mat-fab><mat-icon class="save-icon">save</mat-icon><mat-spinner *ngIf="downloading" class="spinner" [diameter]="50"></mat-spinner></button>
|
||||||
<button *ngIf="!is_shared && uid && uid !== 'false' && type !== 'subscription' && (!postsService.isLoggedIn || postsService.permissions.includes('sharing'))" class="share-button" color="primary" (click)="openShareDialog()" mat-fab><mat-icon class="save-icon">share</mat-icon></button>
|
<button *ngIf="!is_shared && uid && uid !== 'false' && type !== 'subscription' && (!postsService.isLoggedIn || postsService.permissions.includes('sharing'))" class="share-button" color="primary" (click)="openShareDialog()" mat-fab><mat-icon class="save-icon">share</mat-icon></button>
|
||||||
|
</div>
|
||||||
|
</mat-drawer-container>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Component, OnInit, HostListener, EventEmitter, OnDestroy, AfterViewInit } from '@angular/core';
|
import { Component, OnInit, HostListener, EventEmitter, OnDestroy, AfterViewInit, ViewChild } from '@angular/core';
|
||||||
import { VgAPI } from 'ngx-videogular';
|
import { VgAPI } from 'ngx-videogular';
|
||||||
import { PostsService } from 'app/posts.services';
|
import { PostsService } from 'app/posts.services';
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
@@ -7,6 +7,7 @@ import { MatSnackBar } from '@angular/material/snack-bar';
|
|||||||
import { InputDialogComponent } from 'app/input-dialog/input-dialog.component';
|
import { InputDialogComponent } from 'app/input-dialog/input-dialog.component';
|
||||||
import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop';
|
import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop';
|
||||||
import { ShareMediaDialogComponent } from '../dialogs/share-media-dialog/share-media-dialog.component';
|
import { ShareMediaDialogComponent } from '../dialogs/share-media-dialog/share-media-dialog.component';
|
||||||
|
import { TwitchChatComponent } from 'app/components/twitch-chat/twitch-chat.component';
|
||||||
|
|
||||||
export interface IMedia {
|
export interface IMedia {
|
||||||
title: string;
|
title: string;
|
||||||
@@ -31,6 +32,7 @@ export class PlayerComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
currentIndex = 0;
|
currentIndex = 0;
|
||||||
currentItem: IMedia = null;
|
currentItem: IMedia = null;
|
||||||
api: VgAPI;
|
api: VgAPI;
|
||||||
|
api_ready = false;
|
||||||
|
|
||||||
// params
|
// params
|
||||||
fileNames: string[];
|
fileNames: string[];
|
||||||
@@ -65,6 +67,8 @@ export class PlayerComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
save_volume_timer = null;
|
save_volume_timer = null;
|
||||||
original_volume = null;
|
original_volume = null;
|
||||||
|
|
||||||
|
@ViewChild('twitchchat') twitchChat: TwitchChatComponent;
|
||||||
|
|
||||||
@HostListener('window:resize', ['$event'])
|
@HostListener('window:resize', ['$event'])
|
||||||
onResize(event) {
|
onResize(event) {
|
||||||
this.innerWidth = window.innerWidth;
|
this.innerWidth = window.innerWidth;
|
||||||
@@ -124,6 +128,8 @@ export class PlayerComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
this.getFile();
|
this.getFile();
|
||||||
} else if (this.id) {
|
} else if (this.id) {
|
||||||
this.getPlaylistFiles();
|
this.getPlaylistFiles();
|
||||||
|
} else if (this.subscriptionName) {
|
||||||
|
this.getSubscription();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.url) {
|
if (this.url) {
|
||||||
@@ -139,7 +145,7 @@ export class PlayerComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
this.currentItem = this.playlist[0];
|
this.currentItem = this.playlist[0];
|
||||||
this.currentIndex = 0;
|
this.currentIndex = 0;
|
||||||
this.show_player = true;
|
this.show_player = true;
|
||||||
} else if (this.subscriptionName || this.fileNames) {
|
} else if (this.fileNames && !this.subscriptionName) {
|
||||||
this.show_player = true;
|
this.show_player = true;
|
||||||
this.parseFileNames();
|
this.parseFileNames();
|
||||||
}
|
}
|
||||||
@@ -171,6 +177,25 @@ export class PlayerComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getSubscription() {
|
||||||
|
this.postsService.getSubscription(null, this.subscriptionName).subscribe(res => {
|
||||||
|
const subscription = res['subscription'];
|
||||||
|
if (this.fileNames) {
|
||||||
|
subscription.videos.forEach(video => {
|
||||||
|
if (video['id'] === this.fileNames[0]) {
|
||||||
|
this.db_file = video;
|
||||||
|
this.show_player = true;
|
||||||
|
this.parseFileNames();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.log('no file name specified');
|
||||||
|
}
|
||||||
|
}, err => {
|
||||||
|
this.openSnackBar(`Failed to find subscription ${this.subscriptionName}`, 'Dismiss');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
getPlaylistFiles() {
|
getPlaylistFiles() {
|
||||||
this.postsService.getPlaylist(this.id, null, this.uuid).subscribe(res => {
|
this.postsService.getPlaylist(this.id, null, this.uuid).subscribe(res => {
|
||||||
if (res['playlist']) {
|
if (res['playlist']) {
|
||||||
@@ -202,23 +227,26 @@ export class PlayerComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
const fileName = this.fileNames[i];
|
const fileName = this.fileNames[i];
|
||||||
let baseLocation = null;
|
let baseLocation = null;
|
||||||
let fullLocation = null;
|
let fullLocation = null;
|
||||||
if (!this.subscriptionName) {
|
|
||||||
baseLocation = this.type + '/';
|
|
||||||
fullLocation = this.baseStreamPath + baseLocation + encodeURIComponent(fileName);
|
|
||||||
} else {
|
|
||||||
// default to video but include subscription name param
|
|
||||||
baseLocation = this.type === 'audio' ? 'audio/' : 'video/';
|
|
||||||
fullLocation = this.baseStreamPath + baseLocation + encodeURIComponent(fileName) + '?subName=' + this.subscriptionName +
|
|
||||||
'&subPlaylist=' + this.subPlaylist;
|
|
||||||
}
|
|
||||||
|
|
||||||
// adds user token if in multi-user-mode
|
// adds user token if in multi-user-mode
|
||||||
const uuid_str = this.uuid ? `&uuid=${this.uuid}` : '';
|
const uuid_str = this.uuid ? `&uuid=${this.uuid}` : '';
|
||||||
const uid_str = (this.id || !this.db_file) ? '' : `&uid=${this.db_file.uid}`;
|
const uid_str = (this.id || !this.db_file) ? '' : `&uid=${this.db_file.uid}`;
|
||||||
const type_str = (this.id || !this.db_file || !this.db_file.type) ? '' : `&type=${this.db_file.type}`
|
const type_str = (this.type || !this.db_file) ? `&type=${this.type}` : `&type=${this.db_file.type}`
|
||||||
const id_str = this.id ? `&id=${this.id}` : '';
|
const id_str = this.id ? `&id=${this.id}` : '';
|
||||||
|
const file_path_str = (!this.db_file) ? '' : `&file_path=${encodeURIComponent(this.db_file.path)}`;
|
||||||
|
|
||||||
|
if (!this.subscriptionName) {
|
||||||
|
baseLocation = 'stream/';
|
||||||
|
fullLocation = this.baseStreamPath + baseLocation + encodeURIComponent(fileName) + `?test=test${type_str}${file_path_str}`;
|
||||||
|
} else {
|
||||||
|
// default to video but include subscription name param
|
||||||
|
baseLocation = 'stream/';
|
||||||
|
fullLocation = this.baseStreamPath + baseLocation + encodeURIComponent(fileName) + '?subName=' + this.subscriptionName +
|
||||||
|
'&subPlaylist=' + this.subPlaylist + `${file_path_str}${type_str}`;
|
||||||
|
}
|
||||||
|
|
||||||
if (this.postsService.isLoggedIn) {
|
if (this.postsService.isLoggedIn) {
|
||||||
fullLocation += (this.subscriptionName ? '&' : '?') + `jwt=${this.postsService.token}`;
|
fullLocation += (this.subscriptionName ? '&' : '&') + `jwt=${this.postsService.token}`;
|
||||||
if (this.is_shared) { fullLocation += `${uuid_str}${uid_str}${type_str}${id_str}`; }
|
if (this.is_shared) { fullLocation += `${uuid_str}${uid_str}${type_str}${id_str}`; }
|
||||||
} else if (this.is_shared) {
|
} else if (this.is_shared) {
|
||||||
fullLocation += (this.subscriptionName ? '&' : '?') + `test=test${uuid_str}${uid_str}${type_str}${id_str}`;
|
fullLocation += (this.subscriptionName ? '&' : '?') + `test=test${uuid_str}${uid_str}${type_str}${id_str}`;
|
||||||
@@ -246,6 +274,13 @@ export class PlayerComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
|
|
||||||
onPlayerReady(api: VgAPI) {
|
onPlayerReady(api: VgAPI) {
|
||||||
this.api = api;
|
this.api = api;
|
||||||
|
this.api_ready = true;
|
||||||
|
|
||||||
|
this.api.subscriptions.seeked.subscribe(data => {
|
||||||
|
if (this.twitchChat) {
|
||||||
|
this.twitchChat.renewChat();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// checks if volume has been previously set. if so, use that as default
|
// checks if volume has been previously set. if so, use that as default
|
||||||
if (localStorage.getItem('player_volume')) {
|
if (localStorage.getItem('player_volume')) {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { v4 as uuid } from 'uuid';
|
|||||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||||
import * as Fingerprint2 from 'fingerprintjs2';
|
import * as Fingerprint2 from 'fingerprintjs2';
|
||||||
import { isoLangs } from './settings/locales_list';
|
import { isoLangs } from './settings/locales_list';
|
||||||
|
import { Title } from '@angular/platform-browser';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class PostsService implements CanActivate {
|
export class PostsService implements CanActivate {
|
||||||
@@ -53,11 +54,12 @@ export class PostsService implements CanActivate {
|
|||||||
// global vars
|
// global vars
|
||||||
config = null;
|
config = null;
|
||||||
subscriptions = null;
|
subscriptions = null;
|
||||||
|
categories = null;
|
||||||
sidenav = null;
|
sidenav = null;
|
||||||
locale = isoLangs['en'];
|
locale = isoLangs['en'];
|
||||||
|
|
||||||
constructor(private http: HttpClient, private router: Router, @Inject(DOCUMENT) private document: Document,
|
constructor(private http: HttpClient, private router: Router, @Inject(DOCUMENT) private document: Document,
|
||||||
public snackBar: MatSnackBar) {
|
public snackBar: MatSnackBar, private titleService: Title) {
|
||||||
console.log('PostsService Initialized...');
|
console.log('PostsService Initialized...');
|
||||||
this.path = this.document.location.origin + '/api/';
|
this.path = this.document.location.origin + '/api/';
|
||||||
|
|
||||||
@@ -87,6 +89,7 @@ export class PostsService implements CanActivate {
|
|||||||
const result = !this.debugMode ? res['config_file'] : res;
|
const result = !this.debugMode ? res['config_file'] : res;
|
||||||
if (result) {
|
if (result) {
|
||||||
this.config = result['YoutubeDLMaterial'];
|
this.config = result['YoutubeDLMaterial'];
|
||||||
|
this.titleService.setTitle(this.config['Extra']['title_top']);
|
||||||
if (this.config['Advanced']['multi_user_mode']) {
|
if (this.config['Advanced']['multi_user_mode']) {
|
||||||
this.checkAdminCreationStatus();
|
this.checkAdminCreationStatus();
|
||||||
// login stuff
|
// login stuff
|
||||||
@@ -211,12 +214,8 @@ export class PostsService implements CanActivate {
|
|||||||
return this.http.post(this.path + 'setConfig', {new_config_file: config}, this.httpOptions);
|
return this.http.post(this.path + 'setConfig', {new_config_file: config}, this.httpOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteFile(uid: string, isAudio: boolean, blacklistMode = false) {
|
deleteFile(uid: string, type: string, blacklistMode = false) {
|
||||||
if (isAudio) {
|
return this.http.post(this.path + 'deleteFile', {uid: uid, type: type, blacklistMode: blacklistMode}, this.httpOptions);
|
||||||
return this.http.post(this.path + 'deleteMp3', {uid: uid, blacklistMode: blacklistMode}, this.httpOptions);
|
|
||||||
} else {
|
|
||||||
return this.http.post(this.path + 'deleteMp4', {uid: uid, blacklistMode: blacklistMode}, this.httpOptions);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getMp3s() {
|
getMp3s() {
|
||||||
@@ -235,6 +234,14 @@ export class PostsService implements CanActivate {
|
|||||||
return this.http.post(this.path + 'getAllFiles', {}, this.httpOptions);
|
return this.http.post(this.path + 'getAllFiles', {}, this.httpOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getFullTwitchChat(id, type, uuid = null) {
|
||||||
|
return this.http.post(this.path + 'getFullTwitchChat', {id: id, type: type, uuid: uuid}, this.httpOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
downloadTwitchChat(id, type, vodId, uuid = null) {
|
||||||
|
return this.http.post(this.path + 'downloadTwitchChatByVODID', {id: id, type: type, vodId: vodId, uuid: uuid}, this.httpOptions);
|
||||||
|
}
|
||||||
|
|
||||||
downloadFileFromServer(fileName, type, outputName = null, fullPathProvided = null, subscriptionName = null, subPlaylist = null,
|
downloadFileFromServer(fileName, type, outputName = null, fullPathProvided = null, subscriptionName = null, subPlaylist = null,
|
||||||
uid = null, uuid = null, id = null) {
|
uid = null, uuid = null, id = null) {
|
||||||
return this.http.post(this.path + 'downloadFile', {fileNames: fileName,
|
return this.http.post(this.path + 'downloadFile', {fileNames: fileName,
|
||||||
@@ -310,9 +317,39 @@ export class PostsService implements CanActivate {
|
|||||||
return this.http.post(this.path + 'deletePlaylist', {playlistID: playlistID, type: type}, this.httpOptions);
|
return this.http.post(this.path + 'deletePlaylist', {playlistID: playlistID, type: type}, this.httpOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
createSubscription(url, name, timerange = null, streamingOnly = false, audioOnly = false, customArgs = null, customFileOutput = null) {
|
// categories
|
||||||
return this.http.post(this.path + 'subscribe', {url: url, name: name, timerange: timerange, streamingOnly: streamingOnly,
|
|
||||||
audioOnly: audioOnly, customArgs: customArgs, customFileOutput: customFileOutput}, this.httpOptions);
|
getAllCategories() {
|
||||||
|
return this.http.post(this.path + 'getAllCategories', {}, this.httpOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
createCategory(name) {
|
||||||
|
return this.http.post(this.path + 'createCategory', {name: name}, this.httpOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteCategory(category_uid) {
|
||||||
|
return this.http.post(this.path + 'deleteCategory', {category_uid: category_uid}, this.httpOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateCategory(category) {
|
||||||
|
return this.http.post(this.path + 'updateCategory', {category: category}, this.httpOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateCategories(categories) {
|
||||||
|
return this.http.post(this.path + 'updateCategories', {categories: categories}, this.httpOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
reloadCategories() {
|
||||||
|
this.getAllCategories().subscribe(res => {
|
||||||
|
this.categories = res['categories'];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
createSubscription(url, name, timerange = null, streamingOnly = false, maxQuality = 'best', audioOnly = false,
|
||||||
|
customArgs = null, customFileOutput = null) {
|
||||||
|
return this.http.post(this.path + 'subscribe', {url: url, name: name, timerange: timerange, maxQuality: maxQuality,
|
||||||
|
streamingOnly: streamingOnly, audioOnly: audioOnly, customArgs: customArgs,
|
||||||
|
customFileOutput: customFileOutput}, this.httpOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
updateSubscription(subscription) {
|
updateSubscription(subscription) {
|
||||||
@@ -328,8 +365,8 @@ export class PostsService implements CanActivate {
|
|||||||
file_uid: file_uid}, this.httpOptions)
|
file_uid: file_uid}, this.httpOptions)
|
||||||
}
|
}
|
||||||
|
|
||||||
getSubscription(id) {
|
getSubscription(id, name = null) {
|
||||||
return this.http.post(this.path + 'getSubscription', {id: id}, this.httpOptions);
|
return this.http.post(this.path + 'getSubscription', {id: id, name: name}, this.httpOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
getAllSubscriptions() {
|
getAllSubscriptions() {
|
||||||
|
|||||||
@@ -116,14 +116,47 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-12 mt-4">
|
<div class="col-12 mt-4">
|
||||||
|
<mat-form-field class="text-field" color="accent">
|
||||||
|
<input matInput [(ngModel)]="new_config['Downloader']['default_file_output']" matInput placeholder="Default file output" i18n-placeholder="Default file output placeholder">
|
||||||
|
<mat-hint><a target="_blank" href="https://github.com/ytdl-org/youtube-dl/blob/master/README.md#output-template">
|
||||||
|
<ng-container i18n="Youtube-dl output template documentation link">Documentation</ng-container></a>.
|
||||||
|
<ng-container i18n="Custom Output input hint">Path is relative to the above download paths. Don't include extension.</ng-container>
|
||||||
|
</mat-hint>
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12 mt-4 mb-5">
|
||||||
<mat-form-field class="text-field" style="margin-right: 12px;" color="accent">
|
<mat-form-field class="text-field" style="margin-right: 12px;" color="accent">
|
||||||
<textarea matInput [(ngModel)]="new_config['Downloader']['custom_args']" placeholder="Custom args" i18n-placeholder="Custom args input placeholder"></textarea>
|
<textarea matInput [(ngModel)]="new_config['Downloader']['custom_args']" placeholder="Global custom args" i18n-placeholder="Custom args input placeholder"></textarea>
|
||||||
<mat-hint><ng-container i18n="Custom args setting input hint">Global custom args for downloads on the home page. Args are delimited using two commas like so: ,,</ng-container></mat-hint>
|
<mat-hint><ng-container i18n="Custom args setting input hint">Global custom args for downloads on the home page. Args are delimited using two commas like so: ,,</ng-container></mat-hint>
|
||||||
<button class="args-edit-button" (click)="openArgsModifierDialog()" mat-icon-button><mat-icon>edit</mat-icon></button>
|
<button class="args-edit-button" (click)="openArgsModifierDialog()" mat-icon-button><mat-icon>edit</mat-icon></button>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div class="col-12 mt-5">
|
</div>
|
||||||
|
<mat-divider></mat-divider>
|
||||||
|
<div *ngIf="new_config" class="container-fluid">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12 mt-3 mb-2">
|
||||||
|
<h6 i18n="Categories">Categories</h6>
|
||||||
|
<div cdkDropList class="category-list" (cdkDropListDropped)="dropCategory($event)">
|
||||||
|
<div class="category-box" *ngFor="let category of postsService.categories" cdkDrag>
|
||||||
|
<div class="category-custom-placeholder" *cdkDragPlaceholder></div>
|
||||||
|
{{category['name']}}
|
||||||
|
<span style="float: right">
|
||||||
|
<button mat-icon-button (click)="openEditCategoryDialog(category)"><mat-icon>edit</mat-icon></button>
|
||||||
|
<button mat-icon-button (click)="deleteCategory(category)"><mat-icon>cancel</mat-icon></button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button style="margin-top: 10px;" mat-mini-fab (click)="openAddCategoryDialog()"><mat-icon>add</mat-icon></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<mat-divider></mat-divider>
|
||||||
|
<div *ngIf="new_config" class="container-fluid">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12 mt-3">
|
||||||
<mat-checkbox color="accent" [(ngModel)]="new_config['Downloader']['use_youtubedl_archive']"><ng-container i18n="Use youtubedl archive setting">Use youtube-dl archive</ng-container></mat-checkbox>
|
<mat-checkbox color="accent" [(ngModel)]="new_config['Downloader']['use_youtubedl_archive']"><ng-container i18n="Use youtubedl archive setting">Use youtube-dl archive</ng-container></mat-checkbox>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -195,12 +228,24 @@
|
|||||||
<div class="col-12 mt-3">
|
<div class="col-12 mt-3">
|
||||||
<mat-checkbox color="accent" [(ngModel)]="new_config['API']['use_youtube_API']"><ng-container i18n="Use YouTube API setting">Use YouTube API</ng-container></mat-checkbox>
|
<mat-checkbox color="accent" [(ngModel)]="new_config['API']['use_youtube_API']"><ng-container i18n="Use YouTube API setting">Use YouTube API</ng-container></mat-checkbox>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12 mb-3">
|
<div class="col-12 mb-2">
|
||||||
<mat-form-field class="text-field" color="accent">
|
<mat-form-field class="text-field" color="accent">
|
||||||
<input [disabled]="!new_config['API']['use_youtube_API']" [(ngModel)]="new_config['API']['youtube_API_key']" matInput placeholder="Youtube API Key" i18n-placeholder="Youtube API Key setting placeholder" required>
|
<input [disabled]="!new_config['API']['use_youtube_API']" [(ngModel)]="new_config['API']['youtube_API_key']" matInput placeholder="Youtube API Key" i18n-placeholder="Youtube API Key setting placeholder" required>
|
||||||
<mat-hint><a target="_blank" href="https://developers.google.com/youtube/v3/getting-started"><ng-container i18n="Youtube API Key setting hint">Generating a key is easy!</ng-container></a></mat-hint>
|
<mat-hint><a target="_blank" href="https://developers.google.com/youtube/v3/getting-started"><ng-container i18n="Youtube API Key setting hint">Generating a key is easy!</ng-container></a></mat-hint>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="col-12 mt-3">
|
||||||
|
<mat-checkbox color="accent" [(ngModel)]="new_config['API']['use_twitch_API']"><ng-container i18n="Use Twitch API setting">Use Twitch API</ng-container></mat-checkbox>
|
||||||
|
</div>
|
||||||
|
<div *ngIf="new_config['API']['use_twitch_API']" class="col-12 mt-1">
|
||||||
|
<mat-checkbox color="accent" [(ngModel)]="new_config['API']['twitch_auto_download_chat']"><ng-container i18n="Auto download Twitch Chat setting">Auto-download Twitch Chat</ng-container></mat-checkbox>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 mb-5">
|
||||||
|
<mat-form-field class="text-field" color="accent">
|
||||||
|
<input [disabled]="!new_config['API']['use_twitch_API']" [(ngModel)]="new_config['API']['twitch_API_key']" matInput placeholder="Twitch API Key" i18n-placeholder="Twitch API Key setting placeholder" required>
|
||||||
|
<mat-hint><ng-container i18n="Twitch API Key setting hint AKA preamble">Also known as a Client ID.</ng-container> <a target="_blank" href="https://dev.twitch.tv/docs/api/"><ng-container i18n="Twitch API Key setting hint">Generating a key is easy!</ng-container></a></mat-hint>
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<mat-divider></mat-divider>
|
<mat-divider></mat-divider>
|
||||||
@@ -235,11 +280,20 @@
|
|||||||
<div *ngIf="new_config" class="container-fluid">
|
<div *ngIf="new_config" class="container-fluid">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12 mt-3">
|
<div class="col-12 mt-3">
|
||||||
|
<mat-form-field>
|
||||||
|
<mat-label><ng-container i18n="Default downloader select label">Select a downloader</ng-container></mat-label>
|
||||||
|
<mat-select color="accent" [(ngModel)]="new_config['Advanced']['default_downloader']">
|
||||||
|
<mat-option value="youtube-dlc">youtube-dlc</mat-option>
|
||||||
|
<mat-option value="youtube-dl">youtube-dl</mat-option>
|
||||||
|
</mat-select>
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 mt-1">
|
||||||
<mat-checkbox color="accent" [(ngModel)]="new_config['Advanced']['use_default_downloading_agent']"><ng-container i18n="Use default downloading agent setting">Use default downloading agent</ng-container></mat-checkbox>
|
<mat-checkbox color="accent" [(ngModel)]="new_config['Advanced']['use_default_downloading_agent']"><ng-container i18n="Use default downloading agent setting">Use default downloading agent</ng-container></mat-checkbox>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<mat-form-field>
|
<mat-form-field>
|
||||||
<mat-label><ng-container i18n="Custom downloader select label">Select a downloader</ng-container></mat-label>
|
<mat-label><ng-container i18n="Custom downloader select label">Select a download agent</ng-container></mat-label>
|
||||||
<mat-select [disabled]="new_config['Advanced']['use_default_downloading_agent']" color="accent" [(ngModel)]="new_config['Advanced']['custom_downloading_agent']">
|
<mat-select [disabled]="new_config['Advanced']['use_default_downloading_agent']" color="accent" [(ngModel)]="new_config['Advanced']['custom_downloading_agent']">
|
||||||
<mat-option value="aria2c">aria2c</mat-option>
|
<mat-option value="aria2c">aria2c</mat-option>
|
||||||
<mat-option value="avconv">avconv</mat-option>
|
<mat-option value="avconv">avconv</mat-option>
|
||||||
@@ -251,7 +305,7 @@
|
|||||||
</mat-select>
|
</mat-select>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12 mt-2 mb-1">
|
<div class="col-12 mt-2">
|
||||||
<mat-form-field>
|
<mat-form-field>
|
||||||
<mat-label><ng-container i18n="Log Level label">Log Level</ng-container></mat-label>
|
<mat-label><ng-container i18n="Log Level label">Log Level</ng-container></mat-label>
|
||||||
<mat-select color="accent" [(ngModel)]="new_config['Advanced']['logger_level']">
|
<mat-select color="accent" [(ngModel)]="new_config['Advanced']['logger_level']">
|
||||||
@@ -263,7 +317,7 @@
|
|||||||
</mat-select>
|
</mat-select>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12 mt-2 mb-1">
|
<div class="col-12 mb-1">
|
||||||
<mat-form-field>
|
<mat-form-field>
|
||||||
<mat-label><ng-container i18n="Login expiration select label">Login expiration</ng-container></mat-label>
|
<mat-label><ng-container i18n="Login expiration select label">Login expiration</ng-container></mat-label>
|
||||||
<mat-select color="accent" [(ngModel)]="new_config['Advanced']['jwt_expiration']">
|
<mat-select color="accent" [(ngModel)]="new_config['Advanced']['jwt_expiration']">
|
||||||
|
|||||||
@@ -30,4 +30,55 @@
|
|||||||
margin-left: 15px;
|
margin-left: 15px;
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
bottom: 4px;
|
bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-list {
|
||||||
|
width: 500px;
|
||||||
|
max-width: 100%;
|
||||||
|
border: solid 1px #ccc;
|
||||||
|
min-height: 60px;
|
||||||
|
display: block;
|
||||||
|
// background: white;
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-box {
|
||||||
|
padding: 20px 10px;
|
||||||
|
border-bottom: solid 1px #ccc;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
box-sizing: border-box;
|
||||||
|
cursor: move;
|
||||||
|
// background: white;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cdk-drag-preview {
|
||||||
|
box-sizing: border-box;
|
||||||
|
border-radius: 4px;
|
||||||
|
box-shadow: 0 5px 5px -3px rgba(0, 0, 0, 0.2),
|
||||||
|
0 8px 10px 1px rgba(0, 0, 0, 0.14),
|
||||||
|
0 3px 14px 2px rgba(0, 0, 0, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cdk-drag-animating {
|
||||||
|
transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-box:last-child {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-list.cdk-drop-list-dragging .category-box:not(.cdk-drag-placeholder) {
|
||||||
|
transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-custom-placeholder {
|
||||||
|
background: #ccc;
|
||||||
|
border: dotted 3px #999;
|
||||||
|
min-height: 60px;
|
||||||
|
transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
|
||||||
}
|
}
|
||||||
@@ -9,6 +9,9 @@ import { CURRENT_VERSION } from 'app/consts';
|
|||||||
import { MatCheckboxChange } from '@angular/material/checkbox';
|
import { MatCheckboxChange } from '@angular/material/checkbox';
|
||||||
import { CookiesUploaderDialogComponent } from 'app/dialogs/cookies-uploader-dialog/cookies-uploader-dialog.component';
|
import { CookiesUploaderDialogComponent } from 'app/dialogs/cookies-uploader-dialog/cookies-uploader-dialog.component';
|
||||||
import { ConfirmDialogComponent } from 'app/dialogs/confirm-dialog/confirm-dialog.component';
|
import { ConfirmDialogComponent } from 'app/dialogs/confirm-dialog/confirm-dialog.component';
|
||||||
|
import { moveItemInArray, CdkDragDrop } from '@angular/cdk/drag-drop';
|
||||||
|
import { InputDialogComponent } from 'app/input-dialog/input-dialog.component';
|
||||||
|
import { EditCategoryDialogComponent } from 'app/dialogs/edit-category-dialog/edit-category-dialog.component';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-settings',
|
selector: 'app-settings',
|
||||||
@@ -77,6 +80,74 @@ export class SettingsComponent implements OnInit {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dropCategory(event: CdkDragDrop<string[]>) {
|
||||||
|
moveItemInArray(this.postsService.categories, event.previousIndex, event.currentIndex);
|
||||||
|
this.postsService.updateCategories(this.postsService.categories).subscribe(res => {
|
||||||
|
|
||||||
|
}, err => {
|
||||||
|
this.postsService.openSnackBar('Failed to update categories!');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
openAddCategoryDialog() {
|
||||||
|
const done = new EventEmitter<any>();
|
||||||
|
const dialogRef = this.dialog.open(InputDialogComponent, {
|
||||||
|
width: '300px',
|
||||||
|
data: {
|
||||||
|
inputTitle: 'Name the category',
|
||||||
|
inputPlaceholder: 'Name',
|
||||||
|
submitText: 'Add',
|
||||||
|
doneEmitter: done
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
done.subscribe(name => {
|
||||||
|
|
||||||
|
// Eventually do additional checks on name
|
||||||
|
if (name) {
|
||||||
|
this.postsService.createCategory(name).subscribe(res => {
|
||||||
|
if (res['success']) {
|
||||||
|
this.postsService.reloadCategories();
|
||||||
|
dialogRef.close();
|
||||||
|
const new_category = res['new_category'];
|
||||||
|
this.openEditCategoryDialog(new_category);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteCategory(category) {
|
||||||
|
const dialogRef = this.dialog.open(ConfirmDialogComponent, {
|
||||||
|
data: {
|
||||||
|
dialogTitle: 'Delete category',
|
||||||
|
dialogText: `Would you like to delete ${category['name']}?`,
|
||||||
|
submitText: 'Delete',
|
||||||
|
warnSubmitColor: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
dialogRef.afterClosed().subscribe(confirmed => {
|
||||||
|
if (confirmed) {
|
||||||
|
this.postsService.deleteCategory(category['uid']).subscribe(res => {
|
||||||
|
if (res['success']) {
|
||||||
|
this.postsService.openSnackBar(`Successfully deleted ${category['name']}!`);
|
||||||
|
this.postsService.reloadCategories();
|
||||||
|
}
|
||||||
|
}, err => {
|
||||||
|
this.postsService.openSnackBar(`Failed to delete ${category['name']}!`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
openEditCategoryDialog(category) {
|
||||||
|
this.dialog.open(EditCategoryDialogComponent, {
|
||||||
|
data: {
|
||||||
|
category: category
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
generateAPIKey() {
|
generateAPIKey() {
|
||||||
this.postsService.generateNewAPIKey().subscribe(res => {
|
this.postsService.generateNewAPIKey().subscribe(res => {
|
||||||
if (res['new_api_key']) {
|
if (res['new_api_key']) {
|
||||||
@@ -162,7 +233,8 @@ export class SettingsComponent implements OnInit {
|
|||||||
dialogTitle: 'Kill downloads',
|
dialogTitle: 'Kill downloads',
|
||||||
dialogText: 'Are you sure you want to kill all downloads? Any subscription and non-subscription downloads will end immediately, though this operation may take a minute or so to complete.',
|
dialogText: 'Are you sure you want to kill all downloads? Any subscription and non-subscription downloads will end immediately, though this operation may take a minute or so to complete.',
|
||||||
submitText: 'Kill all downloads',
|
submitText: 'Kill all downloads',
|
||||||
doneEmitter: done
|
doneEmitter: done,
|
||||||
|
warnSubmitColor: true
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
done.subscribe(confirmed => {
|
done.subscribe(confirmed => {
|
||||||
|
|||||||
@@ -164,7 +164,7 @@ export class SubscriptionComponent implements OnInit {
|
|||||||
editSubscription() {
|
editSubscription() {
|
||||||
this.dialog.open(EditSubscriptionDialogComponent, {
|
this.dialog.open(EditSubscriptionDialogComponent, {
|
||||||
data: {
|
data: {
|
||||||
sub: this.subscription
|
sub: this.postsService.getSubscriptionByID(this.subscription.id)
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,9 +7,11 @@
|
|||||||
"Downloader": {
|
"Downloader": {
|
||||||
"path-audio": "audio/",
|
"path-audio": "audio/",
|
||||||
"path-video": "video/",
|
"path-video": "video/",
|
||||||
"use_youtubedl_archive": true,
|
"use_youtubedl_archive": false,
|
||||||
"custom_args": "",
|
"custom_args": "",
|
||||||
"safe_download_override": false
|
"safe_download_override": false,
|
||||||
|
"include_thumbnail": false,
|
||||||
|
"include_metadata": true
|
||||||
},
|
},
|
||||||
"Extra": {
|
"Extra": {
|
||||||
"title_top": "YoutubeDL-Material",
|
"title_top": "YoutubeDL-Material",
|
||||||
@@ -33,12 +35,20 @@
|
|||||||
"Subscriptions": {
|
"Subscriptions": {
|
||||||
"allow_subscriptions": true,
|
"allow_subscriptions": true,
|
||||||
"subscriptions_base_path": "subscriptions/",
|
"subscriptions_base_path": "subscriptions/",
|
||||||
"subscriptions_check_interval": "30",
|
"subscriptions_check_interval": "300",
|
||||||
"subscriptions_use_youtubedl_archive": true
|
"subscriptions_use_youtubedl_archive": true
|
||||||
},
|
},
|
||||||
"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}})"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"Advanced": {
|
"Advanced": {
|
||||||
"use_default_downloading_agent": true,
|
"use_default_downloading_agent": true,
|
||||||
@@ -47,7 +57,7 @@
|
|||||||
"allow_advanced_download": true,
|
"allow_advanced_download": true,
|
||||||
"jwt_expiration": 86400,
|
"jwt_expiration": 86400,
|
||||||
"logger_level": "debug",
|
"logger_level": "debug",
|
||||||
"use_cookies": true
|
"use_cookies": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user