Merge pull request #657 from Tzahi12345/4.3-prep

4.3 Prep
This commit is contained in:
Tzahi12345
2022-06-26 21:13:51 -04:00
committed by GitHub
74 changed files with 2963 additions and 2142 deletions

View File

@@ -47,11 +47,11 @@ RUN npm config set strict-ssl false && \
# Final image # Final image
FROM base FROM base
RUN npm install -g pm2 && \ RUN apt update && \
apt update && \ apt install -y --no-install-recommends gosu python3-minimal python-is-python3 python3-pip atomicparsley && \
apt install -y --no-install-recommends gosu python3-minimal python-is-python3 atomicparsley && \
apt clean && \ apt clean && \
rm -rf /var/lib/apt/lists/* rm -rf /var/lib/apt/lists/*
RUN pip install tcd
WORKDIR /app WORKDIR /app
# User 1000 already exist from base image # User 1000 already exist from base image
COPY --chown=$UID:$GID --from=ffmpeg [ "/usr/local/bin/ffmpeg", "/usr/local/bin/ffmpeg" ] COPY --chown=$UID:$GID --from=ffmpeg [ "/usr/local/bin/ffmpeg", "/usr/local/bin/ffmpeg" ]
@@ -64,4 +64,4 @@ RUN chmod +x /app/fix-scripts/*.sh
EXPOSE 17442 EXPOSE 17442
ENTRYPOINT [ "/app/entrypoint.sh" ] ENTRYPOINT [ "/app/entrypoint.sh" ]
CMD [ "pm2-runtime","--raw","pm2.config.js" ] CMD [ "npm","start" ]

View File

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

View File

@@ -97,6 +97,11 @@ paths:
summary: Get all files summary: Get all files
description: Gets all files and playlists stored in the db description: Gets all files and playlists stored in the db
operationId: get-getAllFiles operationId: get-getAllFiles
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/GetAllFilesRequest'
responses: responses:
'200': '200':
description: OK description: OK
@@ -1724,6 +1729,41 @@ components:
description: All video playlists description: All video playlists
items: items:
$ref: '#/components/schemas/Playlist' $ref: '#/components/schemas/Playlist'
GetAllFilesRequest:
type: object
properties:
sort:
$ref: '#/components/schemas/Sort'
range:
type: array
items:
type: number
description: Two elements allowed, start index and end index
minItems: 2
maxItems: 2
text_search:
type: string
description: Filter files by title
file_type_filter:
$ref: '#/components/schemas/FileTypeFilter'
sub_id:
type: string
description: Include if you want to filter by subscription
Sort:
type: object
properties:
by:
type: string
description: Property to sort by
order:
type: number
description: 1 for ascending, -1 for descending
FileTypeFilter:
type: string
enum:
- audio_only
- video_only
- both
GetAllFilesResponse: GetAllFilesResponse:
required: required:
- files - files
@@ -1786,7 +1826,6 @@ components:
required: required:
- name - name
- url - url
- streamingOnly
type: object type: object
properties: properties:
name: name:
@@ -1899,7 +1938,6 @@ components:
- uids - uids
- playlistName - playlistName
- thumbnailURL - thumbnailURL
- type
type: object type: object
properties: properties:
playlistName: playlistName:
@@ -1908,8 +1946,6 @@ components:
type: array type: array
items: items:
type: string type: string
type:
$ref: '#/components/schemas/FileType'
thumbnailURL: thumbnailURL:
type: string type: string
CreatePlaylistResponse: CreatePlaylistResponse:
@@ -1939,15 +1975,17 @@ components:
required: required:
- playlist - playlist
- success - success
- type
type: object type: object
properties: properties:
playlist: playlist:
$ref: '#/components/schemas/Playlist' $ref: '#/components/schemas/Playlist'
type:
$ref: '#/components/schemas/FileType'
success: success:
type: boolean type: boolean
file_objs:
type: array
description: File objects for every uid in the playlist's uids property, in the same order
items:
$ref: '#/components/schemas/DatabaseFile'
GetPlaylistsRequest: GetPlaylistsRequest:
type: object type: object
properties: properties:
@@ -1972,13 +2010,10 @@ components:
DeletePlaylistRequest: DeletePlaylistRequest:
required: required:
- playlist_id - playlist_id
- type
type: object type: object
properties: properties:
playlist_id: playlist_id:
type: string type: string
type:
$ref: '#/components/schemas/FileType'
DownloadFileRequest: DownloadFileRequest:
type: object type: object
properties: properties:
@@ -2385,6 +2420,16 @@ components:
type: number type: number
local_view_count: local_view_count:
type: number type: number
sub_id:
type: string
registered:
type: number
height:
type: number
description: In pixels, only for videos
abr:
type: number
description: In Kbps
Playlist: Playlist:
required: required:
- uids - uids
@@ -2466,6 +2511,8 @@ components:
type: string type: string
sub_name: sub_name:
type: string type: string
prefetched_info:
type: object
Task: Task:
required: required:
- key - key
@@ -2480,6 +2527,8 @@ components:
properties: properties:
key: key:
type: string type: string
title:
type: string
last_ran: last_ran:
type: number type: number
last_confirmed: last_confirmed:
@@ -2560,7 +2609,6 @@ components:
- url - url
- type - type
- user_uid - user_uid
- streamingOnly
- isPlaylist - isPlaylist
- videos - videos
type: object type: object
@@ -2576,8 +2624,6 @@ components:
user_uid: user_uid:
type: string type: string
nullable: true nullable: true
streamingOnly:
type: boolean
isPlaylist: isPlaylist:
type: boolean type: boolean
archive: archive:

View File

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

View File

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

View File

@@ -101,7 +101,6 @@ let backendPort = null;
let useDefaultDownloadingAgent = null; let useDefaultDownloadingAgent = null;
let customDownloadingAgent = null; let customDownloadingAgent = null;
let allowSubscriptions = null; let allowSubscriptions = null;
let archivePath = path.join(__dirname, 'appdata', 'archives');
// other needed values // other needed values
let url_domain = null; let url_domain = null;
@@ -500,12 +499,13 @@ async function loadConfig() {
loadConfigValues(); loadConfigValues();
// connect to DB // connect to DB
await db_api.connectToDB(); if (!config_api.getConfigItem('ytdl_use_local_db'))
await db_api.connectToDB();
db_api.database_initialized = true; db_api.database_initialized = true;
db_api.database_initialized_bs.next(true); db_api.database_initialized_bs.next(true);
// creates archive path if missing // creates archive path if missing
await fs.ensureDir(archivePath); await fs.ensureDir(utils.getArchiveFolder());
// check migrations // check migrations
await checkMigrations(); await checkMigrations();
@@ -912,11 +912,11 @@ app.post('/api/getFile', optionalJwt, async function (req, res) {
app.post('/api/getAllFiles', optionalJwt, async function (req, res) { app.post('/api/getAllFiles', optionalJwt, async function (req, res) {
// these are returned // these are returned
let files = null; let files = null;
let playlists = null; const sort = req.body.sort;
let sort = req.body.sort; const range = req.body.range;
let range = req.body.range; const text_search = req.body.text_search;
let text_search = req.body.text_search; const file_type_filter = req.body.file_type_filter;
let file_type_filter = req.body.file_type_filter; const sub_id = req.body.sub_id;
const uuid = req.isAuthenticated() ? req.user.uid : null; const uuid = req.isAuthenticated() ? req.user.uid : null;
const filter_obj = {user_uid: uuid}; const filter_obj = {user_uid: uuid};
@@ -929,6 +929,10 @@ app.post('/api/getAllFiles', optionalJwt, async function (req, res) {
} }
} }
if (sub_id) {
filter_obj['sub_id'] = sub_id;
}
if (file_type_filter === 'audio_only') filter_obj['isAudio'] = true; if (file_type_filter === 'audio_only') filter_obj['isAudio'] = true;
else if (file_type_filter === 'video_only') filter_obj['isAudio'] = false; else if (file_type_filter === 'video_only') filter_obj['isAudio'] = false;
@@ -1268,7 +1272,7 @@ app.post('/api/getSubscription', optionalJwt, async (req, res) => {
subscription = JSON.parse(JSON.stringify(subscription)); subscription = JSON.parse(JSON.stringify(subscription));
// get sub videos // get sub videos
if (subscription.name && !subscription.streamingOnly) { if (subscription.name) {
var parsed_files = await db_api.getRecords('files', {sub_id: subscription.id}); // subscription.videos; var parsed_files = await db_api.getRecords('files', {sub_id: subscription.id}); // subscription.videos;
subscription['videos'] = parsed_files; subscription['videos'] = parsed_files;
// loop through files for extra processing // loop through files for extra processing
@@ -1278,19 +1282,6 @@ app.post('/api/getSubscription', optionalJwt, async (req, res) => {
if (file && file['url'].includes('twitch.tv')) file['chat_exists'] = fs.existsSync(file['path'].substring(0, file['path'].length - 4) + '.twitch_chat.json'); if (file && file['url'].includes('twitch.tv')) file['chat_exists'] = fs.existsSync(file['path'].substring(0, file['path'].length - 4) + '.twitch_chat.json');
} }
res.send({
subscription: subscription,
files: parsed_files
});
} else if (subscription.name && subscription.streamingOnly) {
// return list of videos
let parsed_files = [];
if (subscription.videos) {
for (let i = 0; i < subscription.videos.length; i++) {
const video = subscription.videos[i];
parsed_files.push(new utils.File(video.title, video.title, video.thumbnail, false, video.duration, video.url, video.uploader, video.size, null, null, video.upload_date, video.view_count, video.height, video.abr));
}
}
res.send({ res.send({
subscription: subscription, subscription: subscription,
files: parsed_files files: parsed_files
@@ -1335,9 +1326,8 @@ app.post('/api/getSubscriptions', optionalJwt, async (req, res) => {
app.post('/api/createPlaylist', optionalJwt, async (req, res) => { app.post('/api/createPlaylist', optionalJwt, async (req, res) => {
let playlistName = req.body.playlistName; let playlistName = req.body.playlistName;
let uids = req.body.uids; let uids = req.body.uids;
let type = req.body.type;
const new_playlist = await db_api.createPlaylist(playlistName, uids, type, req.isAuthenticated() ? req.user.uid : null); const new_playlist = await db_api.createPlaylist(playlistName, uids, req.isAuthenticated() ? req.user.uid : null);
res.send({ res.send({
new_playlist: new_playlist, new_playlist: new_playlist,
@@ -1365,7 +1355,6 @@ app.post('/api/getPlaylist', optionalJwt, async (req, res) => {
res.send({ res.send({
playlist: playlist, playlist: playlist,
file_objs: file_objs, file_objs: file_objs,
type: playlist && playlist.type,
success: !!playlist success: !!playlist
}); });
}); });

View File

@@ -31,7 +31,8 @@
"use_youtube_API": false, "use_youtube_API": false,
"youtube_API_key": "", "youtube_API_key": "",
"use_twitch_API": false, "use_twitch_API": false,
"twitch_API_key": "", "twitch_client_ID": "",
"twitch_client_secret": "",
"twitch_auto_download_chat": false, "twitch_auto_download_chat": false,
"use_sponsorblock_API": false, "use_sponsorblock_API": false,
"generate_NFO_files": false "generate_NFO_files": false
@@ -63,7 +64,7 @@
"mongodb_connection_string": "mongodb://127.0.0.1:27017/?compressors=zlib" "mongodb_connection_string": "mongodb://127.0.0.1:27017/?compressors=zlib"
}, },
"Advanced": { "Advanced": {
"default_downloader": "youtube-dl", "default_downloader": "yt-dlp",
"use_default_downloading_agent": true, "use_default_downloading_agent": true,
"custom_downloading_agent": "", "custom_downloading_agent": "",
"multi_user_mode": false, "multi_user_mode": false,

View File

@@ -206,7 +206,8 @@ const DEFAULT_CONFIG = {
"use_youtube_API": false, "use_youtube_API": false,
"youtube_API_key": "", "youtube_API_key": "",
"use_twitch_API": false, "use_twitch_API": false,
"twitch_API_key": "", "twitch_client_ID": "",
"twitch_client_secret": "",
"twitch_auto_download_chat": false, "twitch_auto_download_chat": false,
"use_sponsorblock_API": false, "use_sponsorblock_API": false,
"generate_NFO_files": false "generate_NFO_files": false
@@ -238,7 +239,7 @@ const DEFAULT_CONFIG = {
"mongodb_connection_string": "mongodb://127.0.0.1:27017/?compressors=zlib" "mongodb_connection_string": "mongodb://127.0.0.1:27017/?compressors=zlib"
}, },
"Advanced": { "Advanced": {
"default_downloader": "youtube-dl", "default_downloader": "yt-dlp",
"use_default_downloading_agent": true, "use_default_downloading_agent": true,
"custom_downloading_agent": "", "custom_downloading_agent": "",
"multi_user_mode": false, "multi_user_mode": false,

View File

@@ -102,9 +102,13 @@ exports.CONFIG_ITEMS = {
'key': 'ytdl_use_twitch_api', 'key': 'ytdl_use_twitch_api',
'path': 'YoutubeDLMaterial.API.use_twitch_API' 'path': 'YoutubeDLMaterial.API.use_twitch_API'
}, },
'ytdl_twitch_api_key': { 'ytdl_twitch_client_id': {
'key': 'ytdl_twitch_api_key', 'key': 'ytdl_twitch_client_id',
'path': 'YoutubeDLMaterial.API.twitch_API_key' 'path': 'YoutubeDLMaterial.API.twitch_client_ID'
},
'ytdl_twitch_client_secret': {
'key': 'ytdl_twitch_client_secret',
'path': 'YoutubeDLMaterial.API.twitch_client_secret'
}, },
'ytdl_twitch_auto_download_chat': { 'ytdl_twitch_auto_download_chat': {
'key': 'ytdl_twitch_auto_download_chat', 'key': 'ytdl_twitch_auto_download_chat',
@@ -301,4 +305,4 @@ const YTDL_ARGS_WITH_VALUES = [
// we're using a Set here for performance // we're using a Set here for performance
exports.YTDL_ARGS_WITH_VALUES = new Set(YTDL_ARGS_WITH_VALUES); exports.YTDL_ARGS_WITH_VALUES = new Set(YTDL_ARGS_WITH_VALUES);
exports.CURRENT_VERSION = 'v4.2'; exports.CURRENT_VERSION = 'v4.3';

View File

@@ -357,7 +357,7 @@ exports.addMetadataPropertyToDB = async (property_key) => {
} }
} }
exports.createPlaylist = async (playlist_name, uids, type, user_uid = null) => { exports.createPlaylist = async (playlist_name, uids, user_uid = null) => {
const first_video = await exports.getVideo(uids[0]); const first_video = await exports.getVideo(uids[0]);
const thumbnailToUse = first_video['thumbnailURL']; const thumbnailToUse = first_video['thumbnailURL'];
@@ -366,7 +366,6 @@ exports.createPlaylist = async (playlist_name, uids, type, user_uid = null) => {
uids: uids, uids: uids,
id: uuid(), id: uuid(),
thumbnailURL: thumbnailToUse, thumbnailURL: thumbnailToUse,
type: type,
registered: Date.now(), registered: Date.now(),
randomize_order: false randomize_order: false
}; };
@@ -495,8 +494,7 @@ exports.deleteFile = async (uid, uuid = null, blacklistMode = false) => {
let useYoutubeDLArchive = config_api.getConfigItem('ytdl_use_youtubedl_archive'); let useYoutubeDLArchive = config_api.getConfigItem('ytdl_use_youtubedl_archive');
if (useYoutubeDLArchive) { if (useYoutubeDLArchive) {
const usersFileFolder = config_api.getConfigItem('ytdl_users_base_path'); const archive_path = utils.getArchiveFolder(type, uuid);
const archive_path = uuid ? path.join(usersFileFolder, uuid, 'archives', `archive_${type}.txt`) : path.join('appdata', 'archives', `archive_${type}.txt`);
// get ID from JSON // get ID from JSON
@@ -504,14 +502,8 @@ exports.deleteFile = async (uid, uuid = null, blacklistMode = false) => {
let id = null; let id = null;
if (jsonobj) id = jsonobj.id; if (jsonobj) id = jsonobj.id;
// use subscriptions API to remove video from the archive file, and write it to the blacklist // Remove file ID from the archive file, and write it to the blacklist (if enabled)
if (await fs.pathExists(archive_path)) { await utils.deleteFileFromArchive(uid, type, archive_path, id, blacklistMode);
const line = id ? await utils.removeIDFromArchive(archive_path, id) : null;
if (blacklistMode && line) await writeToBlacklist(type, line);
} else {
logger.info('Could not find archive file for audio files. Creating...');
await fs.close(await fs.open(archive_path, 'w'));
}
} }
if (jsonExists) await fs.unlink(jsonPath); if (jsonExists) await fs.unlink(jsonPath);
@@ -1111,15 +1103,3 @@ exports.applyFilterLocalDB = (db_path, filter_obj, operation) => {
}); });
return return_val; return return_val;
} }
// archive helper functions
async function writeToBlacklist(type, line) {
const archivePath = path.join(__dirname, 'appdata', 'archives');
let blacklistPath = path.join(archivePath, (type === 'audio') ? 'blacklist_audio.txt' : 'blacklist_video.txt');
// adds newline to the beginning of the line
line.replace('\n', '');
line.replace('\r', '');
line = '\n' + line;
await fs.appendFile(blacklistPath, line);
}

View File

@@ -18,8 +18,6 @@ const db_api = require('./db');
const mutex = new Mutex(); const mutex = new Mutex();
let should_check_downloads = true; let should_check_downloads = true;
const archivePath = path.join(__dirname, 'appdata', 'archives');
if (db_api.database_initialized) { if (db_api.database_initialized) {
setupDownloads(); setupDownloads();
} else { } else {
@@ -28,7 +26,7 @@ if (db_api.database_initialized) {
}); });
} }
exports.createDownload = async (url, type, options, user_uid = null, sub_id = null, sub_name = null) => { exports.createDownload = async (url, type, options, user_uid = null, sub_id = null, sub_name = null, prefetched_info = null) => {
return await mutex.runExclusive(async () => { return await mutex.runExclusive(async () => {
const download = { const download = {
url: url, url: url,
@@ -37,6 +35,7 @@ exports.createDownload = async (url, type, options, user_uid = null, sub_id = nu
user_uid: user_uid, user_uid: user_uid,
sub_id: sub_id, sub_id: sub_id,
sub_name: sub_name, sub_name: sub_name,
prefetched_info: prefetched_info,
options: options, options: options,
uid: uuid(), uid: uuid(),
step_index: 0, step_index: 0,
@@ -187,7 +186,7 @@ async function collectInfo(download_uid) {
let args = await exports.generateArgs(url, type, options, download['user_uid']); let args = await exports.generateArgs(url, type, options, download['user_uid']);
// get video info prior to download // get video info prior to download
let info = await exports.getVideoInfoByURL(url, args, download_uid); let info = download['prefetched_info'] ? download['prefetched_info'] : await exports.getVideoInfoByURL(url, args, download_uid);
if (!info) { if (!info) {
// info failed, error presumably already recorded // info failed, error presumably already recorded
@@ -229,7 +228,8 @@ async function collectInfo(download_uid) {
options: options, options: options,
files_to_check_for_progress: files_to_check_for_progress, files_to_check_for_progress: files_to_check_for_progress,
expected_file_size: expected_file_size, expected_file_size: expected_file_size,
title: playlist_title ? playlist_title : info['title'] title: playlist_title ? playlist_title : info['title'],
prefetched_info: null
}); });
} }
@@ -242,6 +242,7 @@ async function downloadQueuedFile(download_uid) {
return new Promise(async resolve => { return new Promise(async resolve => {
const audioFolderPath = config_api.getConfigItem('ytdl_audio_folder_path'); const audioFolderPath = config_api.getConfigItem('ytdl_audio_folder_path');
const videoFolderPath = config_api.getConfigItem('ytdl_video_folder_path'); const videoFolderPath = config_api.getConfigItem('ytdl_video_folder_path');
const usersFolderPath = config_api.getConfigItem('ytdl_users_base_path');
await db_api.updateRecord('download_queue', {uid: download_uid}, {step_index: 2, finished_step: false, running: true}); await db_api.updateRecord('download_queue', {uid: download_uid}, {step_index: 2, finished_step: false, running: true});
const url = download['url']; const url = download['url'];
@@ -249,9 +250,11 @@ async function downloadQueuedFile(download_uid) {
const options = download['options']; const options = download['options'];
const args = download['args']; const args = download['args'];
const category = download['category']; const category = download['category'];
let fileFolderPath = type === 'audio' ? audioFolderPath : videoFolderPath; // TODO: fix let fileFolderPath = type === 'audio' ? audioFolderPath : videoFolderPath;
if (options.customFileFolderPath) { if (options.customFileFolderPath) {
fileFolderPath = options.customFileFolderPath; fileFolderPath = options.customFileFolderPath;
} else if (download['user_uid']) {
fileFolderPath = path.join(usersFolderPath, download['user_uid'], type);
} }
fs.ensureDirSync(fileFolderPath); fs.ensureDirSync(fileFolderPath);
@@ -373,15 +376,23 @@ async function downloadQueuedFile(download_uid) {
// helper functions // helper functions
exports.generateArgs = async (url, type, options, user_uid = null, simulated = false) => { exports.generateArgs = async (url, type, options, user_uid = null, simulated = false) => {
const default_downloader = utils.getCurrentDownloader() || config_api.getConfigItem('ytdl_default_downloader');
const audioFolderPath = config_api.getConfigItem('ytdl_audio_folder_path'); const audioFolderPath = config_api.getConfigItem('ytdl_audio_folder_path');
const videoFolderPath = config_api.getConfigItem('ytdl_video_folder_path'); const videoFolderPath = config_api.getConfigItem('ytdl_video_folder_path');
const usersFolderPath = config_api.getConfigItem('ytdl_users_base_path');
const videopath = config_api.getConfigItem('ytdl_default_file_output') ? config_api.getConfigItem('ytdl_default_file_output') : '%(title)s'; const videopath = config_api.getConfigItem('ytdl_default_file_output') ? config_api.getConfigItem('ytdl_default_file_output') : '%(title)s';
const globalArgs = config_api.getConfigItem('ytdl_custom_args'); const globalArgs = config_api.getConfigItem('ytdl_custom_args');
const useCookies = config_api.getConfigItem('ytdl_use_cookies'); const useCookies = config_api.getConfigItem('ytdl_use_cookies');
const is_audio = type === 'audio'; const is_audio = type === 'audio';
let fileFolderPath = is_audio ? audioFolderPath : videoFolderPath; let fileFolderPath = type === 'audio' ? audioFolderPath : videoFolderPath; // TODO: fix
if (options.customFileFolderPath) {
fileFolderPath = options.customFileFolderPath;
} else if (user_uid) {
fileFolderPath = path.join(usersFolderPath, user_uid, fileFolderPath);
}
if (options.customFileFolderPath) fileFolderPath = options.customFileFolderPath; if (options.customFileFolderPath) fileFolderPath = options.customFileFolderPath;
@@ -404,8 +415,6 @@ exports.generateArgs = async (url, type, options, user_uid = null, simulated = f
if (!is_audio && !is_youtube) { if (!is_audio && !is_youtube) {
// tiktok videos fail when using the default format // tiktok videos fail when using the default format
qualityPath = null; qualityPath = null;
} else if (!is_audio && !is_youtube && (url.includes('reddit') || url.includes('pornhub'))) {
qualityPath = ['-f', 'bestvideo+bestaudio']
} }
if (customArgs) { if (customArgs) {
@@ -414,7 +423,7 @@ exports.generateArgs = async (url, type, options, user_uid = null, simulated = f
if (customQualityConfiguration) { if (customQualityConfiguration) {
qualityPath = ['-f', customQualityConfiguration, '--merge-output-format', 'mp4']; qualityPath = ['-f', customQualityConfiguration, '--merge-output-format', 'mp4'];
} else if (selectedHeight && selectedHeight !== '' && !is_audio) { } else if (selectedHeight && selectedHeight !== '' && !is_audio) {
qualityPath = ['-f', `'(mp4)[height=${selectedHeight}'`]; qualityPath = ['-f', `'(mp4)[height=${selectedHeight}]`];
} else if (is_audio) { } else if (is_audio) {
qualityPath = ['--audio-quality', maxBitrate ? maxBitrate : '0'] qualityPath = ['--audio-quality', maxBitrate ? maxBitrate : '0']
} }
@@ -496,7 +505,6 @@ exports.generateArgs = async (url, type, options, user_uid = null, simulated = f
downloadConfig.push('-r', rate_limit); downloadConfig.push('-r', rate_limit);
} }
const default_downloader = utils.getCurrentDownloader() || config_api.getConfigItem('ytdl_default_downloader');
if (default_downloader === 'yt-dlp') { if (default_downloader === 'yt-dlp') {
downloadConfig.push('--no-clean-infojson'); downloadConfig.push('--no-clean-infojson');
} }
@@ -506,7 +514,7 @@ exports.generateArgs = async (url, type, options, user_uid = null, simulated = f
// filter out incompatible args // filter out incompatible args
downloadConfig = filterArgs(downloadConfig, is_audio); downloadConfig = filterArgs(downloadConfig, is_audio);
if (!simulated) logger.verbose(`youtube-dl args being used: ${downloadConfig.join(',')}`); if (!simulated) logger.verbose(`${default_downloader} args being used: ${downloadConfig.join(',')}`);
return downloadConfig; return downloadConfig;
} }
@@ -565,8 +573,7 @@ exports.getVideoInfoByURL = async (url, args = [], download_uid = null) => {
function filterArgs(args, isAudio) { function filterArgs(args, isAudio) {
const video_only_args = ['--add-metadata', '--embed-subs', '--xattrs']; const video_only_args = ['--add-metadata', '--embed-subs', '--xattrs'];
const audio_only_args = ['-x', '--extract-audio', '--embed-thumbnail']; const audio_only_args = ['-x', '--extract-audio', '--embed-thumbnail'];
const args_to_remove = isAudio ? video_only_args : audio_only_args; return utils.filterArgs(args, isAudio ? video_only_args : audio_only_args);
return args.filter(x => !args_to_remove.includes(x));
} }
async function checkDownloadPercent(download_uid) { async function checkDownloadPercent(download_uid) {
@@ -628,6 +635,6 @@ function getArchiveFolder(fileFolderPath, options, user_uid) {
} else if (user_uid) { } else if (user_uid) {
return path.join(fileFolderPath, 'archives'); return path.join(fileFolderPath, 'archives');
} else { } else {
return path.join(archivePath); return path.join('appdata', 'archives');
} }
} }

View File

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

1825
backend/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -178,7 +178,7 @@ async function deleteSubscriptionFile(sub, file, deleteForever, file_uid = null,
]); ]);
if (jsonExists) { if (jsonExists) {
retrievedID = JSON.parse(await fs.readFile(jsonPath, 'utf8'))['id']; retrievedID = fs.readJSONSync(jsonPath)['id'];
await fs.unlink(jsonPath); await fs.unlink(jsonPath);
} }
@@ -196,12 +196,11 @@ async function deleteSubscriptionFile(sub, file, deleteForever, file_uid = null,
return false; return false;
} else { } else {
// check if the user wants the video to be redownloaded (deleteForever === false) // check if the user wants the video to be redownloaded (deleteForever === false)
if (!deleteForever && useArchive && sub.archive && retrievedID) { if (useArchive && retrievedID) {
const archive_path = path.join(sub.archive, 'archive.txt') const archive_path = utils.getArchiveFolder(sub.type, user_uid, sub);
// if archive exists, remove line with video ID
if (await fs.pathExists(archive_path)) { // Remove file ID from the archive file, and write it to the blacklist (if enabled)
utils.removeIDFromArchive(archive_path, retrievedID); await utils.deleteFileFromArchive(file_uid, sub.type, archive_path, retrievedID, deleteForever);
}
} }
return true; return true;
} }
@@ -242,64 +241,22 @@ async function getVideosForSub(sub, user_uid = null) {
logger.verbose('Subscription: finished check for ' + sub.name); logger.verbose('Subscription: finished check for ' + sub.name);
if (err && !output) { if (err && !output) {
logger.error(err.stderr ? err.stderr : err.message); logger.error(err.stderr ? err.stderr : err.message);
if (err.stderr.includes('This video is unavailable')) { if (err.stderr.includes('This video is unavailable') || err.stderr.includes('Private video')) {
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 {
// TODO: reimplement const outputs = err.stdout.split(/\r\n|\r|\n/);
const files_to_download = await handleOutputJSON(outputs, sub, user_uid);
// const outputs = err.stdout.split(/\r\n|\r|\n/); resolve(files_to_download);
// for (let i = 0; i < outputs.length; i++) {
// const output = JSON.parse(outputs[i]);
// await handleOutputJSON(sub, output, i === 0, multiUserMode)
// if (err.stderr.includes(output['id']) && archive_path) {
// // we found a video that errored! add it to the archive to prevent future errors
// if (sub.archive) {
// archive_dir = sub.archive;
// archive_path = path.join(archive_dir, 'archive.txt')
// fs.appendFileSync(archive_path, output['id']);
// }
// }
// }
} catch(e) { } catch(e) {
logger.error('Backup method failed. See error below:'); logger.error('Backup method failed. See error below:');
logger.error(e); logger.error(e);
} }
} else {
logger.error('Subscription check failed!');
} }
resolve(false); resolve(false);
} else if (output) { } else if (output) {
if (config_api.getConfigItem('ytdl_subscriptions_redownload_fresh_uploads')) { const files_to_download = await handleOutputJSON(output, sub, user_uid);
await setFreshUploads(sub, user_uid);
checkVideosForFreshUploads(sub, user_uid);
}
if (output.length === 0 || (output.length === 1 && output[0] === '')) {
logger.verbose('No additional videos to download for ' + sub.name);
resolve(true);
return;
}
const output_jsons = [];
for (let i = 0; i < output.length; i++) {
let output_json = null;
try {
output_json = JSON.parse(output[i]);
output_jsons.push(output_json);
} catch(e) {
output_json = null;
}
if (!output_json) {
continue;
}
}
const files_to_download = await getFilesToDownload(sub, output_jsons);
const base_download_options = generateOptionsForSubscriptionDownload(sub, user_uid);
for (let j = 0; j < files_to_download.length; j++) {
const file_to_download = files_to_download[j];
await downloader_api.createDownload(file_to_download['webpage_url'], sub.type || 'video', base_download_options, user_uid, sub.id, sub.name);
}
resolve(files_to_download); resolve(files_to_download);
} }
}); });
@@ -309,6 +266,43 @@ async function getVideosForSub(sub, user_uid = null) {
}); });
} }
async function handleOutputJSON(output, sub, user_uid) {
if (config_api.getConfigItem('ytdl_subscriptions_redownload_fresh_uploads')) {
await setFreshUploads(sub, user_uid);
checkVideosForFreshUploads(sub, user_uid);
}
if (output.length === 0 || (output.length === 1 && output[0] === '')) {
logger.verbose('No additional videos to download for ' + sub.name);
return [];
}
const output_jsons = [];
for (let i = 0; i < output.length; i++) {
let output_json = null;
try {
output_json = JSON.parse(output[i]);
output_jsons.push(output_json);
} catch(e) {
output_json = null;
}
if (!output_json) {
continue;
}
}
const files_to_download = await getFilesToDownload(sub, output_jsons);
const base_download_options = generateOptionsForSubscriptionDownload(sub, user_uid);
for (let j = 0; j < files_to_download.length; j++) {
const file_to_download = files_to_download[j];
file_to_download['formats'] = utils.stripPropertiesFromObject(file_to_download['formats'], ['format_id', 'filesize', 'filesize_approx']); // prevent download object from blowing up in size
await downloader_api.createDownload(file_to_download['webpage_url'], sub.type || 'video', base_download_options, user_uid, sub.id, sub.name, file_to_download);
}
return files_to_download;
}
function generateOptionsForSubscriptionDownload(sub, user_uid) { function generateOptionsForSubscriptionDownload(sub, user_uid) {
let basePath = null; let basePath = null;
if (user_uid) if (user_uid)
@@ -322,7 +316,7 @@ function generateOptionsForSubscriptionDownload(sub, user_uid) {
selectedHeight: sub.maxQuality && sub.maxQuality !== 'best' ? sub.maxQuality : null, selectedHeight: sub.maxQuality && sub.maxQuality !== 'best' ? sub.maxQuality : null,
customFileFolderPath: getAppendedBasePath(sub, basePath), customFileFolderPath: getAppendedBasePath(sub, basePath),
customOutput: sub.custom_output ? `${sub.custom_output}` : `${default_output}`, customOutput: sub.custom_output ? `${sub.custom_output}` : `${default_output}`,
customArchivePath: path.join(__dirname, basePath, 'archives', sub.name), customArchivePath: path.join(basePath, 'archives', sub.name),
additionalArgs: sub.custom_args additionalArgs: sub.custom_args
} }
@@ -389,11 +383,6 @@ async function generateArgsForSubscription(sub, user_uid, redownload = false, de
downloadConfig.push('--download-archive', archive_path); downloadConfig.push('--download-archive', archive_path);
} }
// if streaming only mode, just get the list of videos
if (sub.streamingOnly) {
downloadConfig = ['-f', 'best', '--dump-json'];
}
if (sub.timerange && !redownload) { if (sub.timerange && !redownload) {
downloadConfig.push('--dateafter', sub.timerange); downloadConfig.push('--dateafter', sub.timerange);
} }
@@ -421,6 +410,8 @@ async function generateArgsForSubscription(sub, user_uid, redownload = false, de
downloadConfig.push('--no-clean-infojson'); downloadConfig.push('--no-clean-infojson');
} }
downloadConfig = utils.filterArgs(downloadConfig, ['--write-comments']);
return downloadConfig; return downloadConfig;
} }
@@ -479,6 +470,7 @@ async function updateSubscriptionProperty(sub, assignment_obj) {
async function setFreshUploads(sub) { async function setFreshUploads(sub) {
const sub_files = await db_api.getRecords('files', {sub_id: sub.id}); const sub_files = await db_api.getRecords('files', {sub_id: sub.id});
if (!sub_files) return;
const current_date = new Date().toISOString().split('T')[0].replace(/-/g, ''); const current_date = new Date().toISOString().split('T')[0].replace(/-/g, '');
sub_files.forEach(async file => { sub_files.forEach(async file => {
if (current_date === file['upload_date'].replace(/-/g, '')) { if (current_date === file['upload_date'].replace(/-/g, '')) {

View File

@@ -1,6 +1,7 @@
var assert = require('assert'); const assert = require('assert');
const low = require('lowdb') const low = require('lowdb')
var winston = require('winston'); const winston = require('winston');
const path = require('path');
process.chdir('./backend') process.chdir('./backend')
@@ -39,6 +40,7 @@ const utils = require('../utils');
const subscriptions_api = require('../subscriptions'); const subscriptions_api = require('../subscriptions');
const fs = require('fs-extra'); const fs = require('fs-extra');
const { uuid } = require('uuidv4'); const { uuid } = require('uuidv4');
const NodeID3 = require('node-id3');
db_api.initialize(db, users_db); db_api.initialize(db, users_db);
@@ -399,6 +401,19 @@ describe('Downloader', function() {
}); });
it('Tag file', async function() {
const audio_path = './test/sample.mp3';
const sample_json = fs.readJSONSync('./test/sample.info.json');
const tags = {
title: sample_json['title'],
artist: sample_json['artist'] ? sample_json['artist'] : sample_json['uploader'],
TRCK: '27'
}
NodeID3.write(tags, audio_path);
const written_tags = NodeID3.read(audio_path);
assert(written_tags['raw']['TRCK'] === '27');
});
it('Queue file', async function() { it('Queue file', async function() {
this.timeout(300000); this.timeout(300000);
const returned_download = await downloader_api.createDownload(url, 'video', options); const returned_download = await downloader_api.createDownload(url, 'video', options);
@@ -451,6 +466,20 @@ describe('Downloader', function() {
console.log(updated_args2); console.log(updated_args2);
assert(JSON.stringify(updated_args2), JSON.stringify(expected_args2)); assert(JSON.stringify(updated_args2), JSON.stringify(expected_args2));
}); });
describe('Twitch', async function () {
const twitch_api = require('../twitch');
const example_vod = '1493770675';
it('Download VOD', async function() {
const sample_path = path.join('test', 'sample.twitch_chat.json');
if (fs.existsSync(sample_path)) fs.unlinkSync(sample_path);
this.timeout(300000);
await twitch_api.downloadTwitchChatByVODID(example_vod, 'sample', null, null, null, './test');
assert(fs.existsSync(sample_path));
// cleanup
if (fs.existsSync(sample_path)) fs.unlinkSync(sample_path);
});
});
}); });
describe('Tasks', function() { describe('Tasks', function() {
@@ -561,4 +590,40 @@ describe('Tasks', function() {
const dummy_task_obj = await db_api.getRecord('tasks', {key: 'dummy_task'}); const dummy_task_obj = await db_api.getRecord('tasks', {key: 'dummy_task'});
assert(dummy_task_obj['data']); assert(dummy_task_obj['data']);
}); });
});
describe('Archive', async function() {
const archive_path = path.join('test', 'archives');
fs.ensureDirSync(archive_path);
const archive_file_path = path.join(archive_path, 'archive_video.txt');
const blacklist_file_path = path.join(archive_path, 'blacklist_video.txt');
beforeEach(async function() {
if (fs.existsSync(archive_file_path)) fs.unlinkSync(archive_file_path);
fs.writeFileSync(archive_file_path, 'youtube testing1\nyoutube testing2\nyoutube testing3\n');
if (fs.existsSync(blacklist_file_path)) fs.unlinkSync(blacklist_file_path);
fs.writeFileSync(blacklist_file_path, '');
});
it('Delete from archive', async function() {
await utils.deleteFileFromArchive('N/A', 'video', archive_path, 'testing2', false);
const new_archive = fs.readFileSync(archive_file_path);
assert(!new_archive.includes('testing2'));
});
it('Delete from archive - blacklist', async function() {
await utils.deleteFileFromArchive('N/A', 'video', archive_path, 'testing2', true);
const new_archive = fs.readFileSync(archive_file_path);
const new_blacklist = fs.readFileSync(blacklist_file_path);
assert(!new_archive.includes('testing2'));
assert(new_blacklist.includes('testing2'));
});
});
describe('Utils', async function() {
it('Strip properties', async function() {
const test_obj = {test1: 'test1', test2: 'test2', test3: 'test3'};
const stripped_obj = utils.stripPropertiesFromObject(test_obj, ['test1', 'test3']);
assert(!stripped_obj['test1'] && stripped_obj['test2'] && !stripped_obj['test3'])
});
}); });

View File

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

View File

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

View File

@@ -21,4 +21,4 @@ version: 0.1.0
# incremented each time you make changes to the application. Versions are not expected to # incremented each time you make changes to the application. Versions are not expected to
# follow Semantic Versioning. They should reflect the version the application is using. # follow Semantic Versioning. They should reflect the version the application is using.
# It is recommended to use it with quotes. # It is recommended to use it with quotes.
appVersion: "4.2" appVersion: "4.3"

View File

@@ -17,7 +17,7 @@ services:
- ./users:/app/users - ./users:/app/users
ports: ports:
- "8998:17442" - "8998:17442"
image: tzahi12345/youtubedl-material:nightly image: tzahi12345/youtubedl-material:latest
ytdl-mongo-db: ytdl-mongo-db:
image: mongo image: mongo
ports: ports:

76
package-lock.json generated
View File

@@ -1,6 +1,6 @@
{ {
"name": "youtube-dl-material", "name": "youtube-dl-material",
"version": "4.2.0", "version": "4.3.0",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {
@@ -3675,7 +3675,7 @@
"buffer-crc32": { "buffer-crc32": {
"version": "0.2.13", "version": "0.2.13",
"resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
"integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=", "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==",
"dev": true "dev": true
}, },
"buffer-from": { "buffer-from": {
@@ -3785,12 +3785,6 @@
"resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz",
"integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==",
"dev": true "dev": true
},
"normalize-url": {
"version": "4.5.1",
"resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-4.5.1.tgz",
"integrity": "sha512-9UZCFRHQdNrfTpGg8+1INIg93B6zE0aXMVFkw1WFwvO4SlZywU6aLg5Of0Ap/PgcbSw4LNxvMWXMeugwMCX0AA==",
"dev": true
} }
} }
}, },
@@ -3945,7 +3939,7 @@
"clone-response": { "clone-response": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz", "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz",
"integrity": "sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws=", "integrity": "sha512-yjLXh88P599UOyPTFX0POsd7WxnbsVsGohcwzHOLspIhhpalPw1BcqED8NblyZLKcGrL8dTgMlcaZxV2jAD41Q==",
"dev": true, "dev": true,
"requires": { "requires": {
"mimic-response": "^1.0.0" "mimic-response": "^1.0.0"
@@ -4666,7 +4660,7 @@
"decompress-response": { "decompress-response": {
"version": "3.3.0", "version": "3.3.0",
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz", "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz",
"integrity": "sha1-gKTdMjdIOEv6JICDYirt7Jgq3/M=", "integrity": "sha512-BzRPQuY1ip+qDonAOz42gRm/pg9F768C+npV/4JOsxRC2sq+Rlk+Q4ZCAsOhnIaMrgarILY+RMUIvMmmX1qAEA==",
"dev": true, "dev": true,
"requires": { "requires": {
"mimic-response": "^1.0.0" "mimic-response": "^1.0.0"
@@ -4956,7 +4950,7 @@
"duplexer3": { "duplexer3": {
"version": "0.1.4", "version": "0.1.4",
"resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz", "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz",
"integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=", "integrity": "sha512-CEj8FwwNA4cVH2uFCoHUrmojhYh1vmCdOaneKJXwkeY1i9jnlslVo9dx+hQ5Hl9GnH/Bwy/IjxAyOePyPKYnzA==",
"dev": true "dev": true
}, },
"ecc-jsbn": { "ecc-jsbn": {
@@ -4976,20 +4970,20 @@
"dev": true "dev": true
}, },
"electron": { "electron": {
"version": "13.6.6", "version": "19.0.6",
"resolved": "https://registry.npmjs.org/electron/-/electron-13.6.6.tgz", "resolved": "https://registry.npmjs.org/electron/-/electron-19.0.6.tgz",
"integrity": "sha512-TP2Bl1nTxaH1yRmlYiF7imzvKE/NASE0cl6wOYA3AaP/UrBGc4L3NwJfn5Z55o+1t4TH8vCRxENufESyb32HhA==", "integrity": "sha512-S9Yud32nKhB0iWC0lGl2JXz4FQnCiLCnP5Vehm1/CqyeICcQGmgQaZl2HYpCJ2pesKIsYL9nsgmku/10cxm/gg==",
"dev": true, "dev": true,
"requires": { "requires": {
"@electron/get": "^1.0.1", "@electron/get": "^1.14.1",
"@types/node": "^14.6.2", "@types/node": "^16.11.26",
"extract-zip": "^1.0.3" "extract-zip": "^1.0.3"
}, },
"dependencies": { "dependencies": {
"@types/node": { "@types/node": {
"version": "14.18.12", "version": "16.11.41",
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.12.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.41.tgz",
"integrity": "sha512-q4jlIR71hUpWTnGhXWcakgkZeHa3CCjcQcnuzU8M891BAWA2jHiziiWEPEkdS5pFsz7H9HJiy8BrK7tBRNrY7A==", "integrity": "sha512-mqoYK2TnVjdkGk8qXAVGc/x9nSaTpSrFaGFm43BUH3IdoBV0nta6hYaGmdOvIMlbHJbUEVen3gvwpwovAZKNdQ==",
"dev": true "dev": true
} }
} }
@@ -5956,7 +5950,7 @@
"ms": { "ms": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
"dev": true "dev": true
} }
} }
@@ -6024,7 +6018,7 @@
"fd-slicer": { "fd-slicer": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz",
"integrity": "sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4=", "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==",
"dev": true, "dev": true,
"requires": { "requires": {
"pend": "~1.2.0" "pend": "~1.2.0"
@@ -6412,9 +6406,9 @@
}, },
"dependencies": { "dependencies": {
"semver": { "semver": {
"version": "7.3.5", "version": "7.3.7",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz",
"integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==",
"dev": true, "dev": true,
"optional": true, "optional": true,
"requires": { "requires": {
@@ -6442,9 +6436,9 @@
"integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==" "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA=="
}, },
"globalthis": { "globalthis": {
"version": "1.0.2", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.2.tgz", "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.3.tgz",
"integrity": "sha512-ZQnSFO1la8P7auIOQECnm0sSuoMeaSq0EEdXMBFF2QJO4uNcwbyhSgG3MruWNbFTqCLmxVwGOl7LZ9kASvHdeQ==", "integrity": "sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==",
"dev": true, "dev": true,
"optional": true, "optional": true,
"requires": { "requires": {
@@ -7455,7 +7449,7 @@
"json-buffer": { "json-buffer": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.0.tgz", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.0.tgz",
"integrity": "sha1-Wx85evx11ne96Lz8Dkfh+aPZqJg=", "integrity": "sha512-CuUqjv0FUZIdXkHPI8MezCnFCdaTAacej1TZYulLoAg1h/PhwkdXFN4V/gzY4g+fMBCOV2xF+rp7t2XD2ns/NQ==",
"dev": true "dev": true
}, },
"json-parse-better-errors": { "json-parse-better-errors": {
@@ -7511,7 +7505,7 @@
"jsonfile": { "jsonfile": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz",
"integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==",
"dev": true, "dev": true,
"requires": { "requires": {
"graceful-fs": "^4.1.6" "graceful-fs": "^4.1.6"
@@ -8561,6 +8555,12 @@
"integrity": "sha1-LRDAa9/TEuqXd2laTShDlFa3WUI=", "integrity": "sha1-LRDAa9/TEuqXd2laTShDlFa3WUI=",
"dev": true "dev": true
}, },
"normalize-url": {
"version": "4.5.1",
"resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-4.5.1.tgz",
"integrity": "sha512-9UZCFRHQdNrfTpGg8+1INIg93B6zE0aXMVFkw1WFwvO4SlZywU6aLg5Of0Ap/PgcbSw4LNxvMWXMeugwMCX0AA==",
"dev": true
},
"npm-bundled": { "npm-bundled": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.1.2.tgz", "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.1.2.tgz",
@@ -8584,7 +8584,7 @@
"pify": { "pify": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz",
"integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==",
"dev": true, "dev": true,
"optional": true "optional": true
} }
@@ -9370,7 +9370,7 @@
"pend": { "pend": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
"integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA=", "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==",
"dev": true "dev": true
}, },
"performance-now": { "performance-now": {
@@ -9817,7 +9817,7 @@
"prepend-http": { "prepend-http": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz", "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz",
"integrity": "sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=", "integrity": "sha512-ravE6m9Atw9Z/jjttRUZ+clIXogdghyZAuWJ3qEzjT+jI/dL1ifAqhZeC5VHzQp1MSt1+jxKkFNemj/iO7tVUA==",
"dev": true "dev": true
}, },
"pretty-bytes": { "pretty-bytes": {
@@ -9865,7 +9865,7 @@
"proto-list": { "proto-list": {
"version": "1.2.4", "version": "1.2.4",
"resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz",
"integrity": "sha1-IS1b/hMYMGpCD2QCuOJv85ZHqEk=", "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==",
"dev": true, "dev": true,
"optional": true "optional": true
}, },
@@ -10576,7 +10576,7 @@
"responselike": { "responselike": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/responselike/-/responselike-1.0.2.tgz", "resolved": "https://registry.npmjs.org/responselike/-/responselike-1.0.2.tgz",
"integrity": "sha1-kYcg7ztjHFZCvgaPFa3lpG9Loec=", "integrity": "sha512-/Fpe5guzJk1gPqdJLJR5u7eG/gNY4nImjbRDaVWVMRhne55TCmj2i9Q+54PBRfatRC8v/rIiv9BN0pMd9OV5EQ==",
"dev": true, "dev": true,
"requires": { "requires": {
"lowercase-keys": "^1.0.0" "lowercase-keys": "^1.0.0"
@@ -10826,7 +10826,7 @@
"semver-compare": { "semver-compare": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz",
"integrity": "sha1-De4hahyUGrN+nvsXiPavxf9VN/w=", "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==",
"dev": true, "dev": true,
"optional": true "optional": true
}, },
@@ -11937,7 +11937,7 @@
"typedarray": { "typedarray": {
"version": "0.0.6", "version": "0.0.6",
"resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
"integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==",
"dev": true "dev": true
}, },
"typescript": { "typescript": {
@@ -12027,7 +12027,7 @@
"url-parse-lax": { "url-parse-lax": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-3.0.0.tgz", "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-3.0.0.tgz",
"integrity": "sha1-FrXK/Afb42dsGxmZF3gj1lA6yww=", "integrity": "sha512-NjFKA0DidqPa5ciFcSrXnAltTtzz84ogy+NebPvfEgAck0+TNg4UJ4IN+fB7zRZfbgUf0syOo9MDxFkDSMuFaQ==",
"dev": true, "dev": true,
"requires": { "requires": {
"prepend-http": "^2.0.0" "prepend-http": "^2.0.0"
@@ -12576,7 +12576,7 @@
"yauzl": { "yauzl": {
"version": "2.10.0", "version": "2.10.0",
"resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz",
"integrity": "sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk=", "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==",
"dev": true, "dev": true,
"requires": { "requires": {
"buffer-crc32": "~0.2.3", "buffer-crc32": "~0.2.3",

View File

@@ -1,6 +1,6 @@
{ {
"name": "youtube-dl-material", "name": "youtube-dl-material",
"version": "4.2.0", "version": "4.3.0",
"license": "MIT", "license": "MIT",
"scripts": { "scripts": {
"ng": "ng", "ng": "ng",
@@ -66,7 +66,7 @@
"@typescript-eslint/eslint-plugin": "^4.29.0", "@typescript-eslint/eslint-plugin": "^4.29.0",
"@typescript-eslint/parser": "^4.29.0", "@typescript-eslint/parser": "^4.29.0",
"codelyzer": "^6.0.0", "codelyzer": "^6.0.0",
"electron": "^13.6.6", "electron": "^19.0.6",
"eslint": "^7.32.0", "eslint": "^7.32.0",
"jasmine-core": "~3.6.0", "jasmine-core": "~3.6.0",
"jasmine-spec-reporter": "~5.0.0", "jasmine-spec-reporter": "~5.0.0",

View File

@@ -40,11 +40,13 @@ export type { DownloadTwitchChatByVODIDRequest } from './models/DownloadTwitchCh
export type { DownloadTwitchChatByVODIDResponse } from './models/DownloadTwitchChatByVODIDResponse'; export type { DownloadTwitchChatByVODIDResponse } from './models/DownloadTwitchChatByVODIDResponse';
export type { DownloadVideosForSubscriptionRequest } from './models/DownloadVideosForSubscriptionRequest'; export type { DownloadVideosForSubscriptionRequest } from './models/DownloadVideosForSubscriptionRequest';
export { FileType } from './models/FileType'; export { FileType } from './models/FileType';
export { FileTypeFilter } from './models/FileTypeFilter';
export type { GenerateArgsResponse } from './models/GenerateArgsResponse'; export type { GenerateArgsResponse } from './models/GenerateArgsResponse';
export type { GenerateNewApiKeyResponse } from './models/GenerateNewApiKeyResponse'; export type { GenerateNewApiKeyResponse } from './models/GenerateNewApiKeyResponse';
export type { GetAllCategoriesResponse } from './models/GetAllCategoriesResponse'; export type { GetAllCategoriesResponse } from './models/GetAllCategoriesResponse';
export type { GetAllDownloadsRequest } from './models/GetAllDownloadsRequest'; export type { GetAllDownloadsRequest } from './models/GetAllDownloadsRequest';
export type { GetAllDownloadsResponse } from './models/GetAllDownloadsResponse'; export type { GetAllDownloadsResponse } from './models/GetAllDownloadsResponse';
export type { GetAllFilesRequest } from './models/GetAllFilesRequest';
export type { GetAllFilesResponse } from './models/GetAllFilesResponse'; export type { GetAllFilesResponse } from './models/GetAllFilesResponse';
export type { GetAllSubscriptionsResponse } from './models/GetAllSubscriptionsResponse'; export type { GetAllSubscriptionsResponse } from './models/GetAllSubscriptionsResponse';
export type { GetAllTasksResponse } from './models/GetAllTasksResponse'; export type { GetAllTasksResponse } from './models/GetAllTasksResponse';
@@ -82,6 +84,7 @@ export type { RestoreDBBackupRequest } from './models/RestoreDBBackupRequest';
export { Schedule } from './models/Schedule'; export { Schedule } from './models/Schedule';
export type { SetConfigRequest } from './models/SetConfigRequest'; export type { SetConfigRequest } from './models/SetConfigRequest';
export type { SharingToggle } from './models/SharingToggle'; export type { SharingToggle } from './models/SharingToggle';
export type { Sort } from './models/Sort';
export type { SubscribeRequest } from './models/SubscribeRequest'; export type { SubscribeRequest } from './models/SubscribeRequest';
export type { SubscribeResponse } from './models/SubscribeResponse'; export type { SubscribeResponse } from './models/SubscribeResponse';
export type { Subscription } from './models/Subscription'; export type { Subscription } from './models/Subscription';

View File

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

View File

@@ -30,4 +30,14 @@ export type DatabaseFile = {
category?: Category; category?: Category;
view_count?: number; view_count?: number;
local_view_count?: number; local_view_count?: number;
sub_id?: string;
registered?: number;
/**
* In pixels, only for videos
*/
height?: number;
/**
* In Kbps
*/
abr?: number;
}; };

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,14 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type Sort = {
/**
* Property to sort by
*/
by?: string;
/**
* 1 for ascending, -1 for descending
*/
order?: number;
};

View File

@@ -10,7 +10,6 @@ export type Subscription = {
id: string; id: string;
type: FileType; type: FileType;
user_uid: string | null; user_uid: string | null;
streamingOnly: boolean;
isPlaylist: boolean; isPlaylist: boolean;
archive?: string; archive?: string;
timerange?: string; timerange?: string;

View File

@@ -4,6 +4,7 @@
export type Task = { export type Task = {
key: string; key: string;
title?: string;
last_ran: number; last_ran: number;
last_confirmed: number; last_confirmed: number;
running: boolean; running: boolean;

View File

@@ -49,7 +49,6 @@ import { CreatePlaylistComponent } from './create-playlist/create-playlist.compo
import { SubscriptionsComponent } from './subscriptions/subscriptions.component'; import { SubscriptionsComponent } from './subscriptions/subscriptions.component';
import { SubscribeDialogComponent } from './dialogs/subscribe-dialog/subscribe-dialog.component'; import { SubscribeDialogComponent } from './dialogs/subscribe-dialog/subscribe-dialog.component';
import { SubscriptionComponent } from './subscription//subscription/subscription.component'; import { SubscriptionComponent } from './subscription//subscription/subscription.component';
import { SubscriptionFileCardComponent } from './subscription/subscription-file-card/subscription-file-card.component';
import { SubscriptionInfoDialogComponent } from './dialogs/subscription-info-dialog/subscription-info-dialog.component'; import { SubscriptionInfoDialogComponent } from './dialogs/subscription-info-dialog/subscription-info-dialog.component';
import { SettingsComponent } from './settings/settings.component'; import { SettingsComponent } from './settings/settings.component';
import { MatChipsModule } from '@angular/material/chips'; import { MatChipsModule } from '@angular/material/chips';
@@ -74,7 +73,6 @@ import { ManageUserComponent } from './components/manage-user/manage-user.compon
import { ManageRoleComponent } from './components/manage-role/manage-role.component'; import { ManageRoleComponent } from './components/manage-role/manage-role.component';
import { CookiesUploaderDialogComponent } from './dialogs/cookies-uploader-dialog/cookies-uploader-dialog.component'; import { CookiesUploaderDialogComponent } from './dialogs/cookies-uploader-dialog/cookies-uploader-dialog.component';
import { LogsViewerComponent } from './components/logs-viewer/logs-viewer.component'; import { LogsViewerComponent } from './components/logs-viewer/logs-viewer.component';
import { ModifyPlaylistComponent } from './dialogs/modify-playlist/modify-playlist.component';
import { ConfirmDialogComponent } from './dialogs/confirm-dialog/confirm-dialog.component'; import { ConfirmDialogComponent } from './dialogs/confirm-dialog/confirm-dialog.component';
import { UnifiedFileCardComponent } from './components/unified-file-card/unified-file-card.component'; import { UnifiedFileCardComponent } from './components/unified-file-card/unified-file-card.component';
import { RecentVideosComponent } from './components/recent-videos/recent-videos.component'; import { RecentVideosComponent } from './components/recent-videos/recent-videos.component';
@@ -102,7 +100,6 @@ registerLocaleData(es, 'es');
SubscriptionsComponent, SubscriptionsComponent,
SubscribeDialogComponent, SubscribeDialogComponent,
SubscriptionComponent, SubscriptionComponent,
SubscriptionFileCardComponent,
SubscriptionInfoDialogComponent, SubscriptionInfoDialogComponent,
SettingsComponent, SettingsComponent,
AboutDialogComponent, AboutDialogComponent,
@@ -123,7 +120,6 @@ registerLocaleData(es, 'es');
ManageRoleComponent, ManageRoleComponent,
CookiesUploaderDialogComponent, CookiesUploaderDialogComponent,
LogsViewerComponent, LogsViewerComponent,
ModifyPlaylistComponent,
ConfirmDialogComponent, ConfirmDialogComponent,
UnifiedFileCardComponent, UnifiedFileCardComponent,
RecentVideosComponent, RecentVideosComponent,

View File

@@ -3,7 +3,7 @@ import { PostsService } from 'app/posts.services';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
import { CreatePlaylistComponent } from 'app/create-playlist/create-playlist.component'; import { CreatePlaylistComponent } from 'app/create-playlist/create-playlist.component';
import { ModifyPlaylistComponent } from 'app/dialogs/modify-playlist/modify-playlist.component'; import { Playlist } from 'api-types';
@Component({ @Component({
selector: 'app-custom-playlists', selector: 'app-custom-playlists',
@@ -32,7 +32,7 @@ export class CustomPlaylistsComponent implements OnInit {
}); });
} }
getAllPlaylists() { getAllPlaylists(): void {
this.playlists_received = false; this.playlists_received = false;
// must call getAllFiles as we need to get category playlists as well // must call getAllFiles as we need to get category playlists as well
this.postsService.getPlaylists(true).subscribe(res => { this.postsService.getPlaylists(true).subscribe(res => {
@@ -42,22 +42,25 @@ export class CustomPlaylistsComponent implements OnInit {
} }
// creating a playlist // creating a playlist
openCreatePlaylistDialog() { openCreatePlaylistDialog(): void {
const dialogRef = this.dialog.open(CreatePlaylistComponent, { const dialogRef = this.dialog.open(CreatePlaylistComponent, {
data: { data: {
} create_mode: true
},
minWidth: '90vw',
minHeight: '95vh'
}); });
dialogRef.afterClosed().subscribe(result => { dialogRef.afterClosed().subscribe(result => {
if (result) { if (result) {
this.getAllPlaylists(); this.getAllPlaylists();
this.postsService.openSnackBar('Successfully created playlist!', ''); this.postsService.openSnackBar($localize`Successfully created playlist!', '`);
} else if (result === false) { } else if (result === false) {
this.postsService.openSnackBar('ERROR: failed to create playlist!', ''); this.postsService.openSnackBar($localize`ERROR: failed to create playlist!', '`);
} }
}); });
} }
goToPlaylist(info_obj) { goToPlaylist(info_obj: { file: Playlist; }): void {
const playlist = info_obj.file; const playlist = info_obj.file;
const playlistID = playlist.id; const playlistID = playlist.id;
@@ -76,7 +79,7 @@ export class CustomPlaylistsComponent implements OnInit {
} }
} }
downloadPlaylist(playlist_id, playlist_name) { downloadPlaylist(playlist_id: string, playlist_name: string): void {
this.downloading_content[playlist_id] = true; this.downloading_content[playlist_id] = true;
this.postsService.downloadPlaylistFromServer(playlist_id).subscribe(res => { this.postsService.downloadPlaylistFromServer(playlist_id).subscribe(res => {
this.downloading_content[playlist_id] = false; this.downloading_content[playlist_id] = false;
@@ -86,33 +89,34 @@ export class CustomPlaylistsComponent implements OnInit {
} }
deletePlaylist(args) { deletePlaylist(args: { file: Playlist; index: number; }): void {
const playlist = args.file; const playlist = args.file;
const index = args.index; const index = args.index;
const playlistID = playlist.id; const playlistID = playlist.id;
this.postsService.removePlaylist(playlistID, playlist.type).subscribe(res => { this.postsService.removePlaylist(playlistID).subscribe(res => {
if (res['success']) { if (res['success']) {
this.playlists.splice(index, 1); this.playlists.splice(index, 1);
this.postsService.openSnackBar('Playlist successfully removed.', ''); this.postsService.openSnackBar($localize`Playlist successfully removed.', '`);
} }
this.getAllPlaylists(); this.getAllPlaylists();
}); });
} }
editPlaylistDialog(args) { editPlaylistDialog(args: { playlist: Playlist; index: number; }): void {
const playlist = args.playlist; const playlist = args.playlist;
const index = args.index; const index = args.index;
const dialogRef = this.dialog.open(ModifyPlaylistComponent, { const dialogRef = this.dialog.open(CreatePlaylistComponent, {
data: { data: {
playlist_id: playlist.id, playlist_id: playlist.id,
width: '65vw' create_mode: false
} },
minWidth: '85vw'
}); });
dialogRef.afterClosed().subscribe(res => { dialogRef.afterClosed().subscribe(() => {
// updates playlist in file manager if it changed // updates playlist in file manager if it changed
if (dialogRef.componentInstance.playlist_updated) { if (dialogRef.componentInstance.playlist_updated) {
this.playlists[index] = dialogRef.componentInstance.original_playlist; this.playlists[index] = dialogRef.componentInstance.playlist;
} }
}); });
} }

View File

@@ -8,6 +8,7 @@ import { MatDialog } from '@angular/material/dialog';
import { ConfirmDialogComponent } from 'app/dialogs/confirm-dialog/confirm-dialog.component'; import { ConfirmDialogComponent } from 'app/dialogs/confirm-dialog/confirm-dialog.component';
import { MatSort } from '@angular/material/sort'; import { MatSort } from '@angular/material/sort';
import { Clipboard } from '@angular/cdk/clipboard'; import { Clipboard } from '@angular/cdk/clipboard';
import { Download } from 'api-types';
@Component({ @Component({
selector: 'app-downloads', selector: 'app-downloads',
@@ -68,7 +69,7 @@ export class DownloadsComponent implements OnInit, OnDestroy {
@ViewChild(MatPaginator) paginator: MatPaginator; @ViewChild(MatPaginator) paginator: MatPaginator;
@ViewChild(MatSort) sort: MatSort; @ViewChild(MatSort) sort: MatSort;
sort_downloads = (a, b) => { sort_downloads = (a: Download, b: Download): number => {
const result = b.timestamp_start - a.timestamp_start; const result = b.timestamp_start - a.timestamp_start;
return result; return result;
} }
@@ -166,7 +167,7 @@ export class DownloadsComponent implements OnInit, OnDestroy {
pauseDownload(download_uid: string): void { pauseDownload(download_uid: string): void {
this.postsService.pauseDownload(download_uid).subscribe(res => { this.postsService.pauseDownload(download_uid).subscribe(res => {
if (!res['success']) { if (!res['success']) {
this.postsService.openSnackBar('Failed to pause download! See server logs for more info.'); this.postsService.openSnackBar($localize`Failed to pause download! See server logs for more info.`);
} }
}); });
} }
@@ -174,7 +175,7 @@ export class DownloadsComponent implements OnInit, OnDestroy {
pauseAllDownloads(): void { pauseAllDownloads(): void {
this.postsService.pauseAllDownloads().subscribe(res => { this.postsService.pauseAllDownloads().subscribe(res => {
if (!res['success']) { if (!res['success']) {
this.postsService.openSnackBar('Failed to pause all downloads! See server logs for more info.'); this.postsService.openSnackBar($localize`Failed to pause all downloads! See server logs for more info.`);
} }
}); });
} }
@@ -182,7 +183,7 @@ export class DownloadsComponent implements OnInit, OnDestroy {
resumeDownload(download_uid: string): void { resumeDownload(download_uid: string): void {
this.postsService.resumeDownload(download_uid).subscribe(res => { this.postsService.resumeDownload(download_uid).subscribe(res => {
if (!res['success']) { if (!res['success']) {
this.postsService.openSnackBar('Failed to resume download! See server logs for more info.'); this.postsService.openSnackBar($localize`Failed to resume download! See server logs for more info.`);
} }
}); });
} }
@@ -190,7 +191,7 @@ export class DownloadsComponent implements OnInit, OnDestroy {
resumeAllDownloads(): void { resumeAllDownloads(): void {
this.postsService.resumeAllDownloads().subscribe(res => { this.postsService.resumeAllDownloads().subscribe(res => {
if (!res['success']) { if (!res['success']) {
this.postsService.openSnackBar('Failed to resume all downloads! See server logs for more info.'); this.postsService.openSnackBar($localize`Failed to resume all downloads! See server logs for more info.`);
} }
}); });
} }
@@ -198,7 +199,7 @@ export class DownloadsComponent implements OnInit, OnDestroy {
restartDownload(download_uid: string): void { restartDownload(download_uid: string): void {
this.postsService.restartDownload(download_uid).subscribe(res => { this.postsService.restartDownload(download_uid).subscribe(res => {
if (!res['success']) { if (!res['success']) {
this.postsService.openSnackBar('Failed to restart download! See server logs for more info.'); this.postsService.openSnackBar($localize`Failed to restart download! See server logs for more info.`);
} }
}); });
} }
@@ -206,7 +207,7 @@ export class DownloadsComponent implements OnInit, OnDestroy {
cancelDownload(download_uid: string): void { cancelDownload(download_uid: string): void {
this.postsService.cancelDownload(download_uid).subscribe(res => { this.postsService.cancelDownload(download_uid).subscribe(res => {
if (!res['success']) { if (!res['success']) {
this.postsService.openSnackBar('Failed to cancel download! See server logs for more info.'); this.postsService.openSnackBar($localize`Failed to cancel download! See server logs for more info.`);
} }
}); });
} }
@@ -214,12 +215,12 @@ export class DownloadsComponent implements OnInit, OnDestroy {
clearDownload(download_uid: string): void { clearDownload(download_uid: string): void {
this.postsService.clearDownload(download_uid).subscribe(res => { this.postsService.clearDownload(download_uid).subscribe(res => {
if (!res['success']) { if (!res['success']) {
this.postsService.openSnackBar('Failed to pause download! See server logs for more info.'); this.postsService.openSnackBar($localize`Failed to pause download! See server logs for more info.`);
} }
}); });
} }
watchContent(download): void { watchContent(download: Download): void {
const container = download['container']; const container = download['container'];
localStorage.setItem('player_navigator', this.router.url.split(';')[0]); localStorage.setItem('player_navigator', this.router.url.split(';')[0]);
const is_playlist = container['uids']; // hacky, TODO: fix const is_playlist = container['uids']; // hacky, TODO: fix
@@ -230,7 +231,7 @@ export class DownloadsComponent implements OnInit, OnDestroy {
} }
} }
combineDownloads(downloads_old, downloads_new) { combineDownloads(downloads_old: Download[], downloads_new: Download[]): Download[] {
// only keeps downloads that exist in the new set // only keeps downloads that exist in the new set
downloads_old = downloads_old.filter(download_old => downloads_new.some(download_new => download_new.uid === download_old.uid)); downloads_old = downloads_old.filter(download_old => downloads_new.some(download_new => download_new.uid === download_old.uid));
@@ -251,7 +252,7 @@ export class DownloadsComponent implements OnInit, OnDestroy {
return downloads_old; return downloads_old;
} }
showError(download) { showError(download: Download): void {
const copyToClipboardEmitter = new EventEmitter<boolean>(); const copyToClipboardEmitter = new EventEmitter<boolean>();
this.dialog.open(ConfirmDialogComponent, { this.dialog.open(ConfirmDialogComponent, {
data: { data: {
@@ -272,10 +273,3 @@ export class DownloadsComponent implements OnInit, OnDestroy {
}); });
} }
} }
export interface Download {
timestamp_start: number;
title: string;
step_index: number;
progress: string;
}

View File

@@ -43,17 +43,17 @@ export class LogsViewerComponent implements OnInit {
}) })
}); });
} else { } else {
this.postsService.openSnackBar('Failed to retrieve logs!'); this.postsService.openSnackBar($localize`Failed to retrieve logs!`);
} }
}, err => { }, err => {
this.logs_loading = false; this.logs_loading = false;
console.error(err); console.error(err);
this.postsService.openSnackBar('Failed to retrieve logs!'); this.postsService.openSnackBar($localize`Failed to retrieve logs!`);
}); });
} }
copiedLogsToClipboard() { copiedLogsToClipboard() {
this.postsService.openSnackBar('Logs copied to clipboard!'); this.postsService.openSnackBar($localize`Logs copied to clipboard!`);
} }
clearLogs() { clearLogs() {
@@ -72,12 +72,12 @@ export class LogsViewerComponent implements OnInit {
this.logs = []; this.logs = [];
this.logs_text = ''; this.logs_text = '';
this.getLogs(); this.getLogs();
this.postsService.openSnackBar('Logs successfully cleared!'); this.postsService.openSnackBar($localize`Logs successfully cleared!`);
} else { } else {
this.postsService.openSnackBar('Failed to clear logs!'); this.postsService.openSnackBar($localize`Failed to clear logs!`);
} }
}, err => { }, err => {
this.postsService.openSnackBar('Failed to clear logs!'); this.postsService.openSnackBar($localize`Failed to clear logs!`);
}); });
} }
}); });

View File

@@ -17,7 +17,8 @@
</div> </div>
</div> </div>
<div class="col-12 order-1 col-sm-4 order-sm-2 d-flex justify-content-center"> <div class="col-12 order-1 col-sm-4 order-sm-2 d-flex justify-content-center">
<h4 class="my-videos-title" i18n="My videos title">My videos</h4> <h4 *ngIf="!customHeader" class="my-videos-title" i18n="My files title">My files</h4>
<h4 *ngIf="customHeader" class="my-videos-title">{{customHeader}}</h4>
</div> </div>
<div class="col-12 order-3 col-sm-4 order-sm-3 d-flex justify-content-center"> <div class="col-12 order-3 col-sm-4 order-sm-3 d-flex justify-content-center">
<mat-form-field [ngClass]="searchIsFocused ? 'search-bar-focused' : 'search-bar-unfocused'" class="search-bar" color="accent"> <mat-form-field [ngClass]="searchIsFocused ? 'search-bar-focused' : 'search-bar-unfocused'" class="search-bar" color="accent">
@@ -28,14 +29,15 @@
</div> </div>
</div> </div>
<div> <div>
<div class="container" style="margin-bottom: 16px"> <div *ngIf="!selectMode" class="container" style="margin-bottom: 16px">
<div class="row justify-content-center"> <div class="row justify-content-center">
<ng-container *ngIf="normal_files_received && paged_data"> <ng-container *ngIf="normal_files_received && paged_data">
<div *ngFor="let file of paged_data; 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 style="display: flex; align-items: center;" *ngFor="let file of paged_data; 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']" [availablePlaylists]="playlists" (addFileToPlaylist)="addFileToPlaylist($event)" [loading]="false" (deleteFile)="deleteFile($event)" [baseStreamPath]="postsService.path" [jwtString]="postsService.isLoggedIn ? this.postsService.token : ''"></app-unified-file-card> <app-unified-file-card [ngClass]="downloading_content[file.uid] ? 'blurred' : ''" [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']" [availablePlaylists]="playlists" (addFileToPlaylist)="addFileToPlaylist($event)" [loading]="false" (deleteFile)="deleteFile($event)" [baseStreamPath]="postsService.path" [jwtString]="postsService.isLoggedIn ? this.postsService.token : ''"></app-unified-file-card>
<mat-spinner *ngIf="downloading_content[file.uid]" class="downloading-spinner" [diameter]="32"></mat-spinner>
</div> </div>
<div *ngIf="paged_data.length === 0"> <div *ngIf="paged_data.length === 0">
<ng-container i18n="No videos found">No videos found.</ng-container> <ng-container i18n="No files found">No files found.</ng-container>
</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">
@@ -46,7 +48,54 @@
</div> </div>
</div> </div>
<div> <div *ngIf="selectMode">
<mat-tab-group [(selectedIndex)]="selectedIndex">
<mat-tab label="Order" i18n-label="Order">
<div *ngIf="selected_data.length">
<span *ngIf="reverse_order === false" i18n="Normal order">Normal order&nbsp;</span>
<span *ngIf="reverse_order === true" i18n="Reverse order">Reverse order&nbsp;</span>
<button (click)="toggleSelectionOrder()" mat-icon-button><mat-icon>{{!reverse_order ? 'arrow_downward' : 'arrow_upward'}}</mat-icon></button>
</div>
<!-- Selection order -->
<mat-button-toggle-group *ngIf="selected_data.length" class="media-list" cdkDropList (cdkDropListDropped)="drop($event)" style="width: 80%; left: 9%" vertical #group="matButtonToggleGroup">
<!-- The following for loop can be optimized but it requires a pipe https://stackoverflow.com/a/35703364/8088021 -->
<mat-button-toggle class="media-box" cdkDrag *ngFor="let file of (reverse_order ? selected_data_objs.slice().reverse() : selected_data_objs); let i = index" [checked]="false"><div><div class="playlist-item-text">{{file.title}}</div> <button (click)="removeSelectedFile(i)" class="remove-item-button" mat-icon-button><mat-icon>cancel</mat-icon></button></div></mat-button-toggle>
</mat-button-toggle-group>
<div style="margin-top: 20px;" *ngIf="!selected_data.length">
<h4 style="text-align: center;">No files selected!</h4>
</div>
</mat-tab>
<mat-tab label="Select files" i18n-label="Select files">
<mat-selection-list *ngIf="normal_files_received" (selectionChange)="fileSelectionChanged($event)">
<mat-list-option [selected]="selected_data.includes(file.uid)" *ngFor="let file of paged_data" [value]="file">
<div class="container">
<div class="row justify-content-center">
<div class="col-10">
<mat-icon class="audio-video-icon">{{(file.type === 'audio' || file.isAudio) ? 'audiotrack' : 'movie'}}</mat-icon>
{{file.title}}
</div>
<div class="col-2">{{file.registered | date:'shortDate'}}</div>
</div>
</div>
</mat-list-option>
</mat-selection-list>
<ng-container *ngIf="!normal_files_received && loading_files && loading_files.length > 0">
<mat-selection-list *ngIf="!normal_files_received">
<mat-list-option *ngFor="let file of paged_data">
<content-loader class="list-ghosts" [primaryColor]="postsService.theme.ghost_primary" [secondaryColor]="postsService.theme.ghost_secondary" width="250" height="8"><svg:rect x="0" y="0" rx="3" ry="3" width="250" height="8" /></content-loader>
</mat-list-option>
</mat-selection-list>
</ng-container>
</mat-tab>
</mat-tab-group>
</div>
<div style="position: relative;" *ngIf="usePaginator && selectedIndex > 0">
<div style="position: absolute; margin-left: 8px; margin-top: 5px; scale: 0.8"> <div style="position: absolute; margin-left: 8px; margin-top: 5px; scale: 0.8">
<mat-form-field> <mat-form-field>
<mat-label><ng-container i18n="File type">File type</ng-container></mat-label> <mat-label><ng-container i18n="File type">File type</ng-container></mat-label>

View File

@@ -61,4 +61,61 @@
.my-videos-title { .my-videos-title {
top: 0px; top: 0px;
} }
}
.list-ghosts {
position: relative;
top: 4px;
}
.audio-video-icon {
position: relative;
top: 6px;
}
.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-placeholder {
opacity: 0;
}
.cdk-drag-animating {
transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
}
.media-box:last-child {
border: none;
}
.media-list.cdk-drop-list-dragging .media-box:not(.cdk-drag-placeholder) {
transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
}
.remove-item-button {
right: 10px;
position: absolute;
top: 4px;
}
.playlist-item-text {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 70%;
margin: 0 auto;
}
.blurred {
filter: blur(2px);
}
.downloading-spinner {
align-self: center;
position: absolute;
} }

View File

@@ -1,10 +1,11 @@
import { Component, OnInit, ViewChild } from '@angular/core'; import { Component, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core';
import { PostsService } from 'app/posts.services'; import { PostsService } from 'app/posts.services';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { FileType } from '../../../api-types'; import { DatabaseFile, FileType, FileTypeFilter } from '../../../api-types';
import { MatPaginator } from '@angular/material/paginator'; import { MatPaginator } from '@angular/material/paginator';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
import { distinctUntilChanged } from 'rxjs/operators'; import { distinctUntilChanged } from 'rxjs/operators';
import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop';
@Component({ @Component({
selector: 'app-recent-videos', selector: 'app-recent-videos',
@@ -13,6 +14,26 @@ import { distinctUntilChanged } from 'rxjs/operators';
}) })
export class RecentVideosComponent implements OnInit { export class RecentVideosComponent implements OnInit {
@Input() usePaginator = true;
// File selection
@Input() selectMode = false;
@Input() defaultSelected: DatabaseFile[] = [];
@Input() sub_id = null;
@Input() customHeader = null;
@Input() selectedIndex = 1;
@Output() fileSelectionEmitter = new EventEmitter<{new_selection: string[], thumbnailURL: string}>();
pageSize = 10;
paged_data: DatabaseFile[] = null;
selected_data: string[] = [];
selected_data_objs: DatabaseFile[] = [];
reverse_order = false;
// File listing (with cards)
cached_file_count = 0; cached_file_count = 0;
loading_files = null; loading_files = null;
@@ -20,7 +41,7 @@ export class RecentVideosComponent implements OnInit {
subscription_files_received = false; subscription_files_received = false;
file_count = 10; file_count = 10;
searchChangedSubject: Subject<string> = new Subject<string>(); searchChangedSubject: Subject<string> = new Subject<string>();
downloading_content = {'video': {}, 'audio': {}}; downloading_content = {};
search_mode = false; search_mode = false;
search_text = ''; search_text = '';
searchIsFocused = false; searchIsFocused = false;
@@ -57,18 +78,32 @@ export class RecentVideosComponent implements OnInit {
playlists = null; playlists = null;
pageSize = 10;
paged_data = null;
@ViewChild('paginator') paginator: MatPaginator @ViewChild('paginator') paginator: MatPaginator
constructor(public postsService: PostsService, private router: Router) { constructor(public postsService: PostsService, private router: Router) {
// get cached file count // get cached file count
if (localStorage.getItem('cached_file_count')) { if (localStorage.getItem('cached_file_count')) {
this.cached_file_count = +localStorage.getItem('cached_file_count') <= 10 ? +localStorage.getItem('cached_file_count') : 10; this.cached_file_count = +localStorage.getItem('cached_file_count') <= 10 ? +localStorage.getItem('cached_file_count') : 10;
this.loading_files = Array(this.cached_file_count).fill(0); this.loading_files = Array(this.cached_file_count).fill(0);
} }
// set filter property to cached value
const cached_filter_property = localStorage.getItem('filter_property');
if (cached_filter_property && this.filterProperties[cached_filter_property]) {
this.filterProperty = this.filterProperties[cached_filter_property];
}
// set file type filter to cached value
const cached_file_type_filter = localStorage.getItem('file_type_filter');
if (this.usePaginator && cached_file_type_filter) {
this.fileTypeFilter = cached_file_type_filter;
}
const sort_order = localStorage.getItem('recent_videos_sort_order');
if (sort_order) {
this.descendingMode = sort_order === 'descending';
}
} }
ngOnInit(): void { ngOnInit(): void {
@@ -96,23 +131,9 @@ export class RecentVideosComponent implements OnInit {
} }
}); });
// set filter property to cached value
const cached_filter_property = localStorage.getItem('filter_property'); this.selected_data = this.defaultSelected.map(file => file.uid);
if (cached_filter_property && this.filterProperties[cached_filter_property]) { this.selected_data_objs = this.defaultSelected;
this.filterProperty = this.filterProperties[cached_filter_property];
}
// set file type filter to cached value
const cached_file_type_filter = localStorage.getItem('file_type_filter');
if (cached_file_type_filter) {
this.fileTypeFilter = cached_file_type_filter;
}
const sort_order = localStorage.getItem('recent_videos_sort_order');
if (sort_order) {
this.descendingMode = sort_order === 'descending';
}
this.searchChangedSubject this.searchChangedSubject
.debounceTime(500) .debounceTime(500)
@@ -127,7 +148,7 @@ export class RecentVideosComponent implements OnInit {
}); });
} }
getAllPlaylists() { getAllPlaylists(): void {
this.postsService.getPlaylists().subscribe(res => { this.postsService.getPlaylists().subscribe(res => {
this.playlists = res['playlists']; this.playlists = res['playlists'];
}); });
@@ -135,22 +156,22 @@ export class RecentVideosComponent implements OnInit {
// search // search
onSearchInputChanged(newvalue) { onSearchInputChanged(newvalue: string): void {
this.normal_files_received = false; this.normal_files_received = false;
this.searchChangedSubject.next(newvalue); this.searchChangedSubject.next(newvalue);
} }
filterOptionChanged(value) { filterOptionChanged(value: string): void {
localStorage.setItem('filter_property', value['key']); localStorage.setItem('filter_property', value['key']);
this.getAllFiles(); this.getAllFiles();
} }
fileTypeFilterChanged(value) { fileTypeFilterChanged(value: string): void {
localStorage.setItem('file_type_filter', value); localStorage.setItem('file_type_filter', value);
this.getAllFiles(); this.getAllFiles();
} }
toggleModeChange() { toggleModeChange(): void {
this.descendingMode = !this.descendingMode; this.descendingMode = !this.descendingMode;
localStorage.setItem('recent_videos_sort_order', this.descendingMode ? 'descending' : 'ascending'); localStorage.setItem('recent_videos_sort_order', this.descendingMode ? 'descending' : 'ascending');
this.getAllFiles(); this.getAllFiles();
@@ -158,12 +179,12 @@ export class RecentVideosComponent implements OnInit {
// get files // get files
getAllFiles(cache_mode = false) { getAllFiles(cache_mode = false): void {
this.normal_files_received = cache_mode; this.normal_files_received = cache_mode;
const current_file_index = (this.paginator?.pageIndex ? this.paginator.pageIndex : 0)*this.pageSize; const current_file_index = (this.paginator?.pageIndex ? this.paginator.pageIndex : 0)*this.pageSize;
const sort = {by: this.filterProperty['property'], order: this.descendingMode ? -1 : 1}; const sort = {by: this.filterProperty['property'], order: this.descendingMode ? -1 : 1};
const range = [current_file_index, current_file_index + this.pageSize]; const range = [current_file_index, current_file_index + this.pageSize];
this.postsService.getAllFiles(sort, range, this.search_mode ? this.search_text : null, this.fileTypeFilter).subscribe(res => { this.postsService.getAllFiles(sort, range, this.search_mode ? this.search_text : null, this.fileTypeFilter as FileTypeFilter, this.sub_id).subscribe(res => {
this.file_count = res['file_count']; this.file_count = res['file_count'];
this.paged_data = res['files']; this.paged_data = res['files'];
for (let i = 0; i < this.paged_data.length; i++) { for (let i = 0; i < this.paged_data.length; i++) {
@@ -191,21 +212,12 @@ export class RecentVideosComponent implements OnInit {
} }
} }
navigateToFile(file, new_tab) { navigateToFile(file: DatabaseFile, new_tab: boolean): void {
localStorage.setItem('player_navigator', this.router.url); localStorage.setItem('player_navigator', this.router.url);
if (file.sub_id) { if (file.sub_id) {
const sub = this.postsService.getSubscriptionByID(file.sub_id);
if (sub.streamingOnly) {
// streaming only mode subscriptions
// !new_tab ? this.router.navigate(['/player', {name: file.id,
// url: file.requested_formats ? file.requested_formats[0].url : file.url}])
// : window.open(`/#/player;name=${file.id};url=${file.requested_formats ? file.requested_formats[0].url : file.url}`);
} else {
// normal subscriptions
!new_tab ? this.router.navigate(['/player', {uid: file.uid, !new_tab ? this.router.navigate(['/player', {uid: file.uid,
type: file.isAudio ? 'audio' : 'video'}]) type: file.isAudio ? 'audio' : 'video'}])
: window.open(`/#/player;uid=${file.uid};type=${file.isAudio ? 'audio' : 'video'}`); : window.open(`/#/player;uid=${file.uid};type=${file.isAudio ? 'audio' : 'video'}`);
}
} else { } else {
// normal files // normal files
!new_tab ? this.router.navigate(['/player', {type: file.isAudio ? 'audio' : 'video', uid: file.uid}]) !new_tab ? this.router.navigate(['/player', {type: file.isAudio ? 'audio' : 'video', uid: file.uid}])
@@ -213,46 +225,26 @@ export class RecentVideosComponent implements OnInit {
} }
} }
goToSubscription(file) { goToSubscription(file: DatabaseFile): void {
this.router.navigate(['/subscription', {id: file.sub_id}]); this.router.navigate(['/subscription', {id: file.sub_id}]);
} }
// downloading // downloading
downloadFile(file) { downloadFile(file: DatabaseFile): void {
if (file.sub_id) {
this.downloadSubscriptionFile(file);
} else {
this.downloadNormalFile(file);
}
}
downloadSubscriptionFile(file) {
const type = (file.isAudio ? 'audio' : 'video') as FileType;
const ext = type === 'audio' ? '.mp3' : '.mp4'
const sub = this.postsService.getSubscriptionByID(file.sub_id);
this.postsService.downloadFileFromServer(file.uid).subscribe(res => {
const blob: Blob = res;
saveAs(blob, file.id + ext);
}, err => {
console.log(err);
});
}
downloadNormalFile(file) {
const type = (file.isAudio ? 'audio' : 'video') as FileType; const type = (file.isAudio ? 'audio' : 'video') as FileType;
const ext = type === 'audio' ? '.mp3' : '.mp4' const ext = type === 'audio' ? '.mp3' : '.mp4'
const name = file.id; const name = file.id;
this.downloading_content[type][name] = true; this.downloading_content[file.uid] = true;
this.postsService.downloadFileFromServer(file.uid).subscribe(res => { this.postsService.downloadFileFromServer(file.uid).subscribe(res => {
this.downloading_content[type][name] = false; this.downloading_content[file.uid] = false;
const blob: Blob = res; const blob: Blob = res;
saveAs(blob, decodeURIComponent(name) + ext); saveAs(blob, decodeURIComponent(name) + ext);
if (!this.postsService.config.Extra.file_manager_enabled) { if (!this.postsService.config.Extra.file_manager_enabled && !file.sub_id) {
// tell server to delete the file once downloaded // tell server to delete the file once downloaded
this.postsService.deleteFile(file.uid).subscribe(delRes => { this.postsService.deleteFile(file.uid).subscribe(() => {
// reload mp4s // reload files
this.getAllFiles(); this.getAllFiles();
}); });
} }
@@ -263,7 +255,6 @@ export class RecentVideosComponent implements OnInit {
deleteFile(args) { deleteFile(args) {
const file = args.file; const file = args.file;
const index = args.index;
const blacklistMode = args.blacklistMode; const blacklistMode = args.blacklistMode;
if (file.sub_id) { if (file.sub_id) {
@@ -273,20 +264,20 @@ export class RecentVideosComponent implements OnInit {
} }
} }
deleteNormalFile(file, blacklistMode = false) { deleteNormalFile(file: DatabaseFile, blacklistMode = false): void {
this.postsService.deleteFile(file.uid, blacklistMode).subscribe(result => { this.postsService.deleteFile(file.uid, blacklistMode).subscribe(result => {
if (result) { if (result) {
this.postsService.openSnackBar('Delete success!', 'OK.'); this.postsService.openSnackBar($localize`Delete success!`, $localize`OK.`);
this.removeFileCard(file); this.removeFileCard(file);
} else { } else {
this.postsService.openSnackBar('Delete failed!', 'OK.'); this.postsService.openSnackBar($localize`Delete failed!`, $localize`OK.`);
} }
}, err => { }, () => {
this.postsService.openSnackBar('Delete failed!', 'OK.'); this.postsService.openSnackBar($localize`Delete failed!`, $localize`OK.`);
}); });
} }
deleteSubscriptionFile(file, blacklistMode = false) { deleteSubscriptionFile(file: DatabaseFile, blacklistMode = false): void {
if (blacklistMode) { if (blacklistMode) {
this.deleteForever(file); this.deleteForever(file);
} else { } else {
@@ -294,28 +285,29 @@ export class RecentVideosComponent implements OnInit {
} }
} }
deleteAndRedownload(file) { deleteAndRedownload(file: DatabaseFile): void {
const sub = this.postsService.getSubscriptionByID(file.sub_id); const sub = this.postsService.getSubscriptionByID(file.sub_id);
this.postsService.deleteSubscriptionFile(sub, file.id, false, file.uid).subscribe(res => { this.postsService.deleteSubscriptionFile(sub, file.id, false, file.uid).subscribe(() => {
this.postsService.openSnackBar(`Successfully deleted file: '${file.id}'`); this.postsService.openSnackBar($localize`Successfully deleted file: ` + file.id);
this.removeFileCard(file); this.removeFileCard(file);
}); });
} }
deleteForever(file) { deleteForever(file: DatabaseFile): void {
const sub = this.postsService.getSubscriptionByID(file.sub_id); const sub = this.postsService.getSubscriptionByID(file.sub_id);
this.postsService.deleteSubscriptionFile(sub, file.id, true, file.uid).subscribe(res => { this.postsService.deleteSubscriptionFile(sub, file.id, true, file.uid).subscribe(() => {
this.postsService.openSnackBar(`Successfully deleted file: '${file.id}'`); this.postsService.openSnackBar($localize`Successfully deleted file: ` + file.id);
this.removeFileCard(file); this.removeFileCard(file);
}); });
} }
removeFileCard(file_to_remove) { removeFileCard(file_to_remove: DatabaseFile): void {
const index = this.paged_data.map(e => e.uid).indexOf(file_to_remove.uid); const index = this.paged_data.map(e => e.uid).indexOf(file_to_remove.uid);
this.paged_data.splice(index, 1); this.paged_data.splice(index, 1);
this.getAllFiles(true); this.getAllFiles(true);
} }
// TODO: Add translation support for these snackbars
addFileToPlaylist(info_obj) { addFileToPlaylist(info_obj) {
const file = info_obj['file']; const file = info_obj['file'];
const playlist_id = info_obj['playlist_id']; const playlist_id = info_obj['playlist_id'];
@@ -335,13 +327,13 @@ export class RecentVideosComponent implements OnInit {
// sorting and filtering // sorting and filtering
sortFiles(a, b) { sortFiles(a: DatabaseFile, b: DatabaseFile): number {
// uses the 'registered' flag as the timestamp // uses the 'registered' flag as the timestamp
const result = b.registered - a.registered; const result = b.registered - a.registered;
return result; return result;
} }
durationStringToNumber(dur_str) { durationStringToNumber(dur_str: string): number {
let num_sum = 0; let num_sum = 0;
const dur_str_parts = dur_str.split(':'); const dur_str_parts = dur_str.split(':');
for (let i = dur_str_parts.length - 1; i >= 0; i--) { for (let i = dur_str_parts.length - 1; i >= 0; i--) {
@@ -355,4 +347,42 @@ export class RecentVideosComponent implements OnInit {
this.loading_files = Array(this.pageSize).fill(0); this.loading_files = Array(this.pageSize).fill(0);
this.getAllFiles(); this.getAllFiles();
} }
fileSelectionChanged(event: { option: { _selected: boolean; value: DatabaseFile; } }): void {
const adding = event.option._selected;
const value = event.option.value;
if (adding) {
this.selected_data.push(value.uid);
this.selected_data_objs.push(value);
} else {
this.selected_data = this.selected_data.filter(e => e !== value.uid);
this.selected_data_objs = this.selected_data_objs.filter(e => e.uid !== value.uid);
}
this.fileSelectionEmitter.emit({new_selection: this.selected_data, thumbnailURL: this.selected_data_objs[0].thumbnailURL});
}
toggleSelectionOrder(): void {
this.reverse_order = !this.reverse_order;
localStorage.setItem('default_playlist_order_reversed', '' + this.reverse_order);
}
drop(event: CdkDragDrop<string[]>): void {
if (this.reverse_order) {
event.previousIndex = this.selected_data.length - 1 - event.previousIndex;
event.currentIndex = this.selected_data.length - 1 - event.currentIndex;
}
moveItemInArray(this.selected_data, event.previousIndex, event.currentIndex);
moveItemInArray(this.selected_data_objs, event.previousIndex, event.currentIndex);
this.fileSelectionEmitter.emit({new_selection: this.selected_data, thumbnailURL: this.selected_data_objs[0].thumbnailURL});
}
removeSelectedFile(index: number): void {
if (this.reverse_order) {
index = this.selected_data.length - 1 - index;
}
this.selected_data.splice(index, 1);
this.selected_data_objs.splice(index, 1);
this.fileSelectionEmitter.emit({new_selection: this.selected_data, thumbnailURL: this.selected_data_objs[0].thumbnailURL});
}
} }

View File

@@ -7,6 +7,7 @@ import { ConfirmDialogComponent } from 'app/dialogs/confirm-dialog/confirm-dialo
import { RestoreDbDialogComponent } from 'app/dialogs/restore-db-dialog/restore-db-dialog.component'; import { RestoreDbDialogComponent } from 'app/dialogs/restore-db-dialog/restore-db-dialog.component';
import { UpdateTaskScheduleDialogComponent } from 'app/dialogs/update-task-schedule-dialog/update-task-schedule-dialog.component'; import { UpdateTaskScheduleDialogComponent } from 'app/dialogs/update-task-schedule-dialog/update-task-schedule-dialog.component';
import { PostsService } from 'app/posts.services'; import { PostsService } from 'app/posts.services';
import { Task } from 'api-types';
@Component({ @Component({
selector: 'app-tasks', selector: 'app-tasks',
@@ -17,7 +18,7 @@ export class TasksComponent implements OnInit {
interval_id = null; interval_id = null;
tasks_check_interval = 1500; tasks_check_interval = 1500;
tasks = null; tasks: Task[] = null;
tasks_retrieved = false; tasks_retrieved = false;
displayedColumns: string[] = ['title', 'last_ran', 'last_confirmed', 'status', 'actions']; displayedColumns: string[] = ['title', 'last_ran', 'last_confirmed', 'status', 'actions'];
@@ -55,6 +56,11 @@ export class TasksComponent implements OnInit {
getTasks(): void { getTasks(): void {
this.postsService.getTasks().subscribe(res => { this.postsService.getTasks().subscribe(res => {
for (const task of res['tasks']) {
if (task.title.includes('youtube-dl')) {
task.title = task.title.replace('youtube-dl', this.postsService.config.Advanced.default_downloader);
}
}
if (this.tasks) { if (this.tasks) {
if (JSON.stringify(this.tasks) === JSON.stringify(res['tasks'])) return; if (JSON.stringify(this.tasks) === JSON.stringify(res['tasks'])) return;
for (const task of res['tasks']) { for (const task of res['tasks']) {
@@ -94,7 +100,7 @@ export class TasksComponent implements OnInit {
}); });
} }
scheduleTask(task: any): void { scheduleTask(task: Task): void {
// open dialog // open dialog
const dialogRef = this.dialog.open(UpdateTaskScheduleDialogComponent, { const dialogRef = this.dialog.open(UpdateTaskScheduleDialogComponent, {
data: { data: {
@@ -152,13 +158,3 @@ export class TasksComponent implements OnInit {
} }
} }
export interface Task {
key: string;
title: string;
last_ran: number;
last_confirmed: number;
running: boolean;
confirming: boolean;
data: unknown;
}

View File

@@ -96,18 +96,18 @@ export class TwitchChatComponent implements OnInit, OnDestroy {
let vodId = this.db_file.url.split('videos/').length > 1 && this.db_file.url.split('videos/')[1]; let vodId = this.db_file.url.split('videos/').length > 1 && this.db_file.url.split('videos/')[1];
vodId = vodId.split('?')[0]; vodId = vodId.split('?')[0];
if (!vodId) { if (!vodId) {
this.postsService.openSnackBar('VOD url for this video is not supported. VOD ID must be after "twitch.tv/videos/"'); this.postsService.openSnackBar($localize`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, this.sub).subscribe(res => { this.postsService.downloadTwitchChat(this.db_file.id, this.db_file.isAudio ? 'audio' : 'video', vodId, null, this.sub).subscribe(res => {
if (res['chat']) { if (res['chat']) {
this.initializeChatCheck(res['chat']); this.initializeChatCheck(res['chat']);
} else { } else {
this.downloading_chat = false; this.downloading_chat = false;
this.postsService.openSnackBar('Download failed.') this.postsService.openSnackBar($localize`Download failed.`)
} }
}, err => { }, err => {
this.downloading_chat = false; this.downloading_chat = false;
this.postsService.openSnackBar('Chat could not be downloaded.') this.postsService.openSnackBar($localize`Chat could not be downloaded.`)
}); });
} }

View File

@@ -23,7 +23,7 @@
<ng-container *ngIf="!is_playlist && !loading"> <ng-container *ngIf="!is_playlist && !loading">
<button (click)="openFileInfoDialog()" mat-menu-item><mat-icon>info</mat-icon><ng-container i18n="Video info button">Info</ng-container></button> <button (click)="openFileInfoDialog()" mat-menu-item><mat-icon>info</mat-icon><ng-container i18n="Video info button">Info</ng-container></button>
<button (click)="navigateToSubscription()" mat-menu-item *ngIf="file_obj.sub_id"><mat-icon>{{file_obj.isAudio ? 'library_music' : 'video_library'}}</mat-icon>&nbsp;<ng-container i18n="Go to subscription menu item">Go to subscription</ng-container></button> <button (click)="navigateToSubscription()" mat-menu-item *ngIf="file_obj.sub_id"><mat-icon>{{file_obj.isAudio ? 'library_music' : 'video_library'}}</mat-icon>&nbsp;<ng-container i18n="Go to subscription menu item">Go to subscription</ng-container></button>
<button *ngIf="availablePlaylists" [matMenuTriggerFor]="addtoplaylist" mat-menu-item><mat-icon>playlist_add</mat-icon>&nbsp;<ng-container i18n="Add to playlist menu item">Add to playlist</ng-container></button> <button [disabled]="!availablePlaylists || availablePlaylists.length === 0" [matMenuTriggerFor]="addtoplaylist" mat-menu-item><mat-icon>playlist_add</mat-icon>&nbsp;<ng-container i18n="Add to playlist menu item">Add to playlist</ng-container></button>
<mat-menu #addtoplaylist="matMenu"> <mat-menu #addtoplaylist="matMenu">
<ng-container *ngFor="let playlist of availablePlaylists"> <ng-container *ngFor="let playlist of availablePlaylists">
<button *ngIf="(playlist.type === 'audio') === file_obj.isAudio" [disabled]="playlist.uids?.includes(file_obj.uid)" (click)="emitAddFileToPlaylist(playlist.id)" mat-menu-item>{{playlist.name}}</button> <button *ngIf="(playlist.type === 'audio') === file_obj.isAudio" [disabled]="playlist.uids?.includes(file_obj.uid)" (click)="emitAddFileToPlaylist(playlist.id)" mat-menu-item>{{playlist.name}}</button>
@@ -34,10 +34,10 @@
<mat-icon>restore</mat-icon><ng-container i18n="Delete and redownload subscription video button">Delete and redownload</ng-container> <mat-icon>restore</mat-icon><ng-container i18n="Delete and redownload subscription video button">Delete and redownload</ng-container>
</button> </button>
<button *ngIf="file_obj.sub_id && use_youtubedl_archive" (click)="emitDeleteFile(true)" mat-menu-item> <button *ngIf="file_obj.sub_id && use_youtubedl_archive" (click)="emitDeleteFile(true)" mat-menu-item>
<mat-icon>delete_forever</mat-icon><ng-container i18n="Delete forever subscription video button">Delete forever</ng-container> <mat-icon>delete_forever</mat-icon><ng-container i18n="Delete forever subscription video button">Delete and don't download again</ng-container>
</button> </button>
<button *ngIf="!file_obj.sub_id" (click)="emitDeleteFile()" mat-menu-item><mat-icon>delete</mat-icon><ng-container i18n="Delete video button">Delete</ng-container></button> <button *ngIf="!file_obj.sub_id" (click)="emitDeleteFile()" mat-menu-item><mat-icon>delete</mat-icon><ng-container i18n="Delete video button">Delete</ng-container></button>
<button *ngIf="!file_obj.sub_id && use_youtubedl_archive" (click)="emitDeleteFile(true)" mat-menu-item><mat-icon>delete_forever</mat-icon><ng-container i18n="Delete and blacklist video button">Delete and blacklist</ng-container></button> <button *ngIf="!file_obj.sub_id && use_youtubedl_archive" (click)="emitDeleteFile(true)" mat-menu-item><mat-icon>delete_forever</mat-icon><ng-container i18n="Delete and don't download again">Delete and don't download again</ng-container></button>
</ng-container> </ng-container>
<ng-container *ngIf="is_playlist && !loading"> <ng-container *ngIf="is_playlist && !loading">
<button (click)="emitEditPlaylist()" mat-menu-item><mat-icon>edit</mat-icon><ng-container i18n="Playlist edit button">Edit</ng-container></button> <button (click)="emitEditPlaylist()" mat-menu-item><mat-icon>edit</mat-icon><ng-container i18n="Playlist edit button">Edit</ng-container></button>

View File

@@ -9,7 +9,6 @@ import localeES from '@angular/common/locales/es';
import localeDE from '@angular/common/locales/de'; import localeDE from '@angular/common/locales/de';
import localeZH from '@angular/common/locales/zh'; import localeZH from '@angular/common/locales/zh';
import localeNB from '@angular/common/locales/nb'; import localeNB from '@angular/common/locales/nb';
import { DatabaseFile, Playlist } from 'api-types';
registerLocaleData(localeGB); registerLocaleData(localeGB);
registerLocaleData(localeFR); registerLocaleData(localeFR);
@@ -105,12 +104,16 @@ export class UnifiedFileCardComponent implements OnInit {
} }
openFileInfoDialog() { openFileInfoDialog() {
this.dialog.open(VideoInfoDialogComponent, { const dialogRef = this.dialog.open(VideoInfoDialogComponent, {
data: { data: {
file: this.file_obj, file: this.file_obj,
}, },
minWidth: '50vw' minWidth: '50vw'
}) });
dialogRef.afterClosed().subscribe(() => {
this.file_obj = dialogRef.componentInstance.file;
});
} }
emitEditPlaylist() { emitEditPlaylist() {

View File

@@ -1 +1 @@
export const CURRENT_VERSION = 'v4.2'; export const CURRENT_VERSION = 'v4.3';

View File

@@ -1,36 +1,29 @@
<h4 mat-dialog-title i18n="Create a playlist dialog title">Create a playlist</h4> <div class="fixActionRow">
<form> <h4 mat-dialog-title *ngIf="create_mode" ><ng-container i18n="Create a playlist dialog title">Create a playlist</ng-container></h4>
<div *ngIf="filesToSelectFrom || (audiosToSelectFrom && videosToSelectFrom)"> <h4 mat-dialog-title *ngIf="!create_mode"><ng-container i18n="Modify playlist dialog title">Modify playlist</ng-container></h4>
<div>
<mat-form-field color="accent">
<input [(ngModel)]="name" matInput placeholder="Name" i18n-placeholder="Playlist name placeholder" type="text" required aria-required [ngModelOptions]="{standalone: true}">
</mat-form-field>
</div>
<div *ngIf="!filesToSelectFrom">
<mat-form-field color="accent">
<mat-select placeholder="Type" i18n-placeholder="Type select" [(ngModel)]="type" [ngModelOptions]="{standalone: true}">
<mat-option value="audio"><ng-container i18n="Audio">Audio</ng-container></mat-option>
<mat-option value="video"><ng-container i18n="Video">Video</ng-container></mat-option>
</mat-select>
</mat-form-field>
</div>
<div>
<mat-form-field *ngIf="type && ((filesToSelectFrom && filesToSelectFrom.length > 0) || (type === 'audio' && audiosToSelectFrom && audiosToSelectFrom.length > 0) || (type === 'video' && videosToSelectFrom && videosToSelectFrom.length > 0))" color="accent">
<mat-label *ngIf="type === 'audio'"><ng-container i18n="Audio files title">Audio files</ng-container></mat-label>
<mat-label *ngIf="type === 'video'"><ng-container i18n="Videos title">Videos</ng-container></mat-label>
<mat-select [formControl]="filesSelect" multiple required aria-required>
<ng-container *ngIf="filesToSelectFrom"><mat-option *ngFor="let file of filesToSelectFrom" [value]="file.uid">{{file.id}}</mat-option></ng-container>
<ng-container *ngIf="audiosToSelectFrom && type === 'audio'"><mat-option *ngFor="let file of audiosToSelectFrom" [value]="file.uid">{{file.id}}</mat-option></ng-container>
<ng-container *ngIf="videosToSelectFrom && type === 'video'"><mat-option *ngFor="let file of videosToSelectFrom" [value]="file.uid">{{file.id}}</mat-option></ng-container>
</mat-select>
</mat-form-field>
<!-- No videos available -->
<div style="margin-bottom: 15px;" *ngIf="type && ((filesToSelectFrom && filesToSelectFrom.length === 0) || (type === 'audio' && audiosToSelectFrom && audiosToSelectFrom.length === 0) || (type === 'video' && videosToSelectFrom && videosToSelectFrom.length === 0))">
No files available.
</div>
</div>
</div>
</form>
<div *ngIf="create_in_progress" style="float: left"><mat-spinner [diameter]="25"></mat-spinner></div> <mat-dialog-content style="max-height: 85vh;">
<button (click)="createPlaylist()" [disabled]="!name || !filesSelect.value || filesSelect.value.length === 0" color="primary" style="float: right" mat-flat-button>Create</button> <form>
<div *ngIf="create_mode || playlist">
<div>
<mat-form-field color="accent">
<input [(ngModel)]="name" matInput placeholder="Name" i18n-placeholder="Playlist name placeholder" type="text" required aria-required [ngModelOptions]="{standalone: true}">
</mat-form-field>
</div>
<app-recent-videos [selectMode]="true" [defaultSelected]="preselected_files" [customHeader]="'Select files'" (fileSelectionEmitter)="fileSelectionChanged($event)" [selectedIndex]="create_mode ? 1 : 0"></app-recent-videos>
</div>
</form>
</mat-dialog-content>
<div class="spacer"></div>
<mat-dialog-actions>
<button *ngIf="create_mode" (click)="createPlaylist()" [disabled]="!name || !filesSelect.value || filesSelect.value.length === 0" color="primary" style="float: right" mat-flat-button>
<ng-container i18n="Create button">Create</ng-container>
</button>
<button *ngIf="!create_mode" (click)="updatePlaylist()" [disabled]="!name || !playlistChanged()" color="primary" style="float: right" mat-flat-button>
<ng-container i18n="Save button">Save</ng-container>
</button>
<div *ngIf="create_in_progress" style="margin-left: 10px"><mat-spinner [diameter]="25"></mat-spinner></div>
</mat-dialog-actions>
</div>

View File

@@ -0,0 +1,9 @@
.fixActionRow {
height: 89vh;
display: flex;
flex-direction: column;
}
.spacer {
flex-grow: 1;
}

View File

@@ -2,6 +2,7 @@ import { Component, OnInit, Inject } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { FormControl } from '@angular/forms'; import { FormControl } from '@angular/forms';
import { PostsService } from 'app/posts.services'; import { PostsService } from 'app/posts.services';
import { Playlist } from 'api-types';
@Component({ @Component({
selector: 'app-create-playlist', selector: 'app-create-playlist',
@@ -9,7 +10,7 @@ import { PostsService } from 'app/posts.services';
styleUrls: ['./create-playlist.component.scss'] styleUrls: ['./create-playlist.component.scss']
}) })
export class CreatePlaylistComponent implements OnInit { export class CreatePlaylistComponent implements OnInit {
// really "createPlaylistDialogComponent" // really "createAndModifyPlaylistDialogComponent"
filesToSelectFrom = null; filesToSelectFrom = null;
type = null; type = null;
@@ -17,64 +18,86 @@ export class CreatePlaylistComponent implements OnInit {
audiosToSelectFrom = null; audiosToSelectFrom = null;
videosToSelectFrom = null; videosToSelectFrom = null;
name = ''; name = '';
cached_thumbnail_url = null;
create_in_progress = false; create_in_progress = false;
create_mode = false;
// playlist modify mode
playlist: Playlist = null;
playlist_id: string = null;
preselected_files = [];
playlist_updated = false;
constructor(@Inject(MAT_DIALOG_DATA) public data: any, constructor(@Inject(MAT_DIALOG_DATA) public data: any,
private postsService: PostsService, private postsService: PostsService,
public dialogRef: MatDialogRef<CreatePlaylistComponent>) { } public dialogRef: MatDialogRef<CreatePlaylistComponent>) {
if (this.data?.create_mode) this.create_mode = true;
if (this.data?.playlist_id) {
ngOnInit() { this.playlist_id = this.data.playlist_id;
if (this.data) { this.getPlaylist();
this.filesToSelectFrom = this.data.filesToSelectFrom; }
this.type = this.data.type;
}
if (!this.filesToSelectFrom) {
this.getMp3s();
this.getMp4s();
}
} }
getMp3s() {
this.postsService.getMp3s().subscribe(result => {
this.audiosToSelectFrom = result['mp3s'];
});
}
getMp4s() { ngOnInit(): void {}
this.postsService.getMp4s().subscribe(result => {
this.videosToSelectFrom = result['mp4s'];
});
}
createPlaylist() { createPlaylist(): void {
const thumbnailURL = this.getThumbnailURL(); const thumbnailURL = this.getThumbnailURL();
this.create_in_progress = true; this.create_in_progress = true;
this.postsService.createPlaylist(this.name, this.filesSelect.value, this.type, thumbnailURL).subscribe(res => { this.postsService.createPlaylist(this.name, this.filesSelect.value, thumbnailURL).subscribe(res => {
this.create_in_progress = false; this.create_in_progress = false;
if (res['success']) { if (res['success']) {
this.dialogRef.close(true); this.dialogRef.close(true);
} else { } else {
this.dialogRef.close(false); this.dialogRef.close(false);
} }
}, err => {
this.create_in_progress = false;
console.error(err);
}); });
} }
getThumbnailURL() { updatePlaylist(): void {
let properFilesToSelectFrom = this.filesToSelectFrom; this.create_in_progress = true;
if (!this.filesToSelectFrom) { this.playlist['name'] = this.name;
properFilesToSelectFrom = this.type === 'audio' ? this.audiosToSelectFrom : this.videosToSelectFrom; this.playlist['uids'] = this.filesSelect.value;
} this.playlist_updated = true;
for (let i = 0; i < properFilesToSelectFrom.length; i++) { this.postsService.updatePlaylist(this.playlist).subscribe(() => {
const file = properFilesToSelectFrom[i]; this.create_in_progress = false;
if (file.id === this.filesSelect.value[0]) { this.postsService.openSnackBar($localize`Playlist updated successfully.`);
// different services store the thumbnail in different places this.getPlaylist();
if (file.thumbnailURL) { return file.thumbnailURL }; this.postsService.playlists_changed.next(true);
if (file.thumbnail) { return file.thumbnail }; }, err => {
this.create_in_progress = false;
console.error(err)
this.postsService.openSnackBar($localize`Playlist updated successfully.`);
});
}
getThumbnailURL(): string {
return this.cached_thumbnail_url;
}
fileSelectionChanged({new_selection, thumbnailURL}: {new_selection: string[], thumbnailURL: string}): void {
this.filesSelect.setValue(new_selection);
if (new_selection.length) this.cached_thumbnail_url = thumbnailURL;
else this.cached_thumbnail_url = null;
}
playlistChanged(): boolean {
return JSON.stringify(this.playlist.uids) !== JSON.stringify(this.filesSelect.value) || this.name !== this.playlist.name;
}
getPlaylist(): void {
this.postsService.getPlaylist(this.playlist_id, null, true).subscribe(res => {
if (res['playlist']) {
this.filesSelect.setValue(res['file_objs'].map(file => file.uid));
this.preselected_files = res['file_objs'];
this.playlist = res['playlist'];
this.name = this.playlist['name'];
} }
} });
return null;
} }
} }

View File

@@ -39,7 +39,7 @@ export class CookiesUploaderDialogComponent implements OnInit {
this.uploading = false; this.uploading = false;
if (res['success']) { if (res['success']) {
this.uploaded = true; this.uploaded = true;
this.postsService.openSnackBar('Cookies successfully uploaded!'); this.postsService.openSnackBar($localize`Cookies successfully uploaded!`);
} }
}, err => { }, err => {
this.uploading = false; this.uploading = false;

View File

@@ -34,11 +34,6 @@
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
</div> </div>
<div class="col-12 mt-1">
<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>
</div>
</div>
<div class="col-12 mb-3"> <div class="col-12 mb-3">
<mat-form-field color="accent"> <mat-form-field color="accent">
<input [(ngModel)]="new_sub.custom_args" matInput placeholder="Custom args" i18n-placeholder="Subscription custom args placeholder"> <input [(ngModel)]="new_sub.custom_args" matInput placeholder="Custom args" i18n-placeholder="Subscription custom args placeholder">

View File

@@ -1,44 +0,0 @@
<h4 mat-dialog-title><ng-container i18n="Modify playlist dialog title">Modify playlist</ng-container></h4>
<mat-dialog-content>
<div *ngIf="playlist">
<!-- Playlist info -->
<div>
<mat-form-field color="accent">
<input matInput placeholder="Name" i18n-placeholder="Name" [(ngModel)]="playlist.name">
</mat-form-field>
</div>
<div>
<mat-checkbox [(ngModel)]="playlist.randomize_order"><ng-container i18n="Randomize order when playing checkbox label">Randomize order when playing</ng-container></mat-checkbox>
</div>
<div style="margin-bottom: 10px; height: 40px;">
<div style="float: left">
<span *ngIf="reverse_order === false" i18n="Normal order">Normal order&nbsp;</span>
<span *ngIf="reverse_order === true" i18n="Reverse order">Reverse order&nbsp;</span>
<button (click)="togglePlaylistOrder()" mat-icon-button><mat-icon>{{!reverse_order ? 'arrow_downward' : 'arrow_upward'}}</mat-icon></button>
</div>
<div style="float: right">
<button [disabled]="available_files.length === 0" mat-stroked-button [matMenuTriggerFor]="menu"><ng-container i18n="Add content">Add content</ng-container></button>
</div>
</div>
<!-- Playlist order -->
<mat-button-toggle-group class="media-list" cdkDropList (cdkDropListDropped)="drop($event)" style="width: 80%; left: 9%" vertical name="videoSelect" aria-label="Video Select" #group="matButtonToggleGroup">
<!-- The following for loop can be optimized but it requires a pipe https://stackoverflow.com/a/35703364/8088021 -->
<mat-button-toggle class="media-box" cdkDrag *ngFor="let playlist_item of (reverse_order ? playlist_file_objs.slice().reverse() : playlist_file_objs); let i = index" [checked]="false"><div><div class="playlist-item-text">{{playlist_item.title}}</div> <button (click)="removeContent(i)" class="remove-item-button" mat-icon-button><mat-icon>cancel</mat-icon></button></div></mat-button-toggle>
</mat-button-toggle-group>
<mat-menu #menu="matMenu">
<button *ngFor="let file of available_files" (click)="addContent(file)" mat-menu-item>{{file.title}}</button>
</mat-menu>
</div>
</mat-dialog-content>
<mat-dialog-actions>
<!-- Save -->
<button [disabled]="!playlist || !playlistChanged()" (click)="updatePlaylist()" mat-raised-button color="accent"><ng-container i18n="Save">Save</ng-container></button>
</mat-dialog-actions>

View File

@@ -1,45 +0,0 @@
.media-list {
}
.media-box {
}
.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-placeholder {
opacity: 0;
}
.cdk-drag-animating {
transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
}
.media-box:last-child {
border: none;
}
.media-list.cdk-drop-list-dragging .media-box:not(.cdk-drag-placeholder) {
transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
}
.remove-item-button {
right: 10px;
position: absolute;
top: 4px;
}
.playlist-item-text {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 70%;
margin: 0 auto;
}

View File

@@ -1,25 +0,0 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { ModifyPlaylistComponent } from './modify-playlist.component';
describe('ModifyPlaylistComponent', () => {
let component: ModifyPlaylistComponent;
let fixture: ComponentFixture<ModifyPlaylistComponent>;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [ ModifyPlaylistComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ModifyPlaylistComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -1,107 +0,0 @@
import { Component, OnInit, Inject } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop';
import { PostsService } from 'app/posts.services';
@Component({
selector: 'app-modify-playlist',
templateUrl: './modify-playlist.component.html',
styleUrls: ['./modify-playlist.component.scss']
})
export class ModifyPlaylistComponent implements OnInit {
playlist_id = null;
original_playlist = null;
playlist = null;
playlist_file_objs = null;
available_files = [];
all_files = [];
playlist_updated = false;
reverse_order = false;
constructor(@Inject(MAT_DIALOG_DATA) public data: any,
private postsService: PostsService,
public dialogRef: MatDialogRef<ModifyPlaylistComponent>) { }
ngOnInit(): void {
if (this.data) {
this.playlist_id = this.data.playlist_id;
this.getPlaylist();
}
this.reverse_order = localStorage.getItem('default_playlist_order_reversed') === 'true';
}
getFiles() {
if (this.playlist.type === 'audio') {
this.postsService.getMp3s().subscribe(res => {
this.processFiles(res['mp3s']);
});
} else {
this.postsService.getMp4s().subscribe(res => {
this.processFiles(res['mp4s']);
});
}
}
processFiles(new_files = null) {
if (new_files) { this.all_files = new_files; }
this.available_files = this.all_files.filter(e => !this.playlist_file_objs.includes(e))
}
updatePlaylist() {
this.playlist['uids'] = this.playlist_file_objs.map(playlist_file_obj => playlist_file_obj['uid'])
this.postsService.updatePlaylist(this.playlist).subscribe(res => {
this.playlist_updated = true;
this.postsService.openSnackBar('Playlist updated successfully.');
this.getPlaylist();
this.postsService.playlists_changed.next(true);
});
}
playlistChanged() {
return JSON.stringify(this.playlist) !== JSON.stringify(this.original_playlist);
}
getPlaylist() {
this.postsService.getPlaylist(this.playlist_id, null, true).subscribe(res => {
if (res['playlist']) {
this.playlist = res['playlist'];
this.playlist_file_objs = res['file_objs'];
this.original_playlist = JSON.parse(JSON.stringify(this.playlist));
this.getFiles();
}
});
}
addContent(file) {
this.playlist_file_objs.push(file);
this.playlist.uids.push(file.uid);
this.processFiles();
}
removeContent(index) {
if (this.reverse_order) {
index = this.playlist_file_objs.length - 1 - index;
}
this.playlist_file_objs.splice(index, 1);
this.playlist.uids.splice(index, 1);
this.processFiles();
}
togglePlaylistOrder() {
this.reverse_order = !this.reverse_order;
localStorage.setItem('default_playlist_order_reversed', '' + this.reverse_order);
}
drop(event: CdkDragDrop<string[]>) {
if (this.reverse_order) {
event.previousIndex = this.playlist_file_objs.length - 1 - event.previousIndex;
event.currentIndex = this.playlist_file_objs.length - 1 - event.currentIndex;
}
moveItemInArray(this.playlist_file_objs, event.previousIndex, event.currentIndex);
}
}

View File

@@ -36,14 +36,14 @@ export class RestoreDbDialogComponent implements OnInit {
this.postsService.restoreDBBackup(this.selected_backup[0]).subscribe(res => { this.postsService.restoreDBBackup(this.selected_backup[0]).subscribe(res => {
this.restoring = false; this.restoring = false;
if (res['success']) { if (res['success']) {
this.postsService.openSnackBar('Database successfully restored!'); this.postsService.openSnackBar($localize`Database successfully restored!`);
this.dialogRef.close(); this.dialogRef.close();
} else { } else {
this.postsService.openSnackBar('Failed to restore database! See logs for more info.'); this.postsService.openSnackBar($localize`Failed to restore database! See logs for more info.`);
} }
}, err => { }, err => {
this.restoring = false; this.restoring = false;
this.postsService.openSnackBar('Failed to restore database! See browser console for more info.'); this.postsService.openSnackBar($localize`Failed to restore database! See browser console for more info.`);
console.error(err); console.error(err);
}); });
} }

View File

@@ -58,31 +58,31 @@ export class ShareMediaDialogComponent implements OnInit {
} }
copiedToClipboard() { copiedToClipboard() {
this.openSnackBar('Copied to clipboard!'); this.postsService.openSnackBar($localize`Copied to clipboard!`);
} }
sharingChanged(event) { sharingChanged(event) {
if (event.checked) { if (event.checked) {
this.postsService.enableSharing(this.uid, this.is_playlist).subscribe(res => { this.postsService.enableSharing(this.uid, this.is_playlist).subscribe(res => {
if (res['success']) { if (res['success']) {
this.openSnackBar('Sharing enabled.'); this.postsService.openSnackBar($localize`Sharing enabled.`);
this.sharing_enabled = true; this.sharing_enabled = true;
} else { } else {
this.openSnackBar('Failed to enable sharing.'); this.postsService.openSnackBar($localize`Failed to enable sharing.`);
} }
}, err => { }, err => {
this.openSnackBar('Failed to enable sharing - server error.'); this.postsService.openSnackBar($localize`Failed to enable sharing - server error.`);
}); });
} else { } else {
this.postsService.disableSharing(this.uid, this.is_playlist).subscribe(res => { this.postsService.disableSharing(this.uid, this.is_playlist).subscribe(res => {
if (res['success']) { if (res['success']) {
this.openSnackBar('Sharing disabled.'); this.postsService.openSnackBar($localize`Sharing disabled.`);
this.sharing_enabled = false; this.sharing_enabled = false;
} else { } else {
this.openSnackBar('Failed to disable sharing.'); this.postsService.openSnackBar($localize`Failed to disable sharing.`);
} }
}, err => { }, err => {
this.openSnackBar('Failed to disable sharing - server error.'); this.postsService.openSnackBar($localize`Failed to disable sharing - server error.`);
}); });
} }
} }

View File

@@ -47,11 +47,6 @@
<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>
</div> </div>
</div> </div>
<div class="col-12">
<div>
<mat-checkbox [disabled]="audioOnlyMode" [(ngModel)]="streamingOnlyMode"><ng-container i18n="Streaming-only mode">Streaming-only mode</ng-container></mat-checkbox>
</div>
</div>
<div class="col-12 mb-3"> <div class="col-12 mb-3">
<mat-form-field color="accent"> <mat-form-field color="accent">
<input [(ngModel)]="customArgs" matInput placeholder="Custom args" i18n-placeholder="Subscription custom args placeholder"> <input [(ngModel)]="customArgs" matInput placeholder="Custom args" i18n-placeholder="Subscription custom args placeholder">

View File

@@ -1,6 +1,5 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { MatDialogRef, MatDialog } from '@angular/material/dialog'; import { MatDialogRef, MatDialog } from '@angular/material/dialog';
import { MatSnackBar } from '@angular/material/snack-bar';
import { PostsService } from 'app/posts.services'; import { PostsService } from 'app/posts.services';
import { ArgModifierDialogComponent } from '../arg-modifier-dialog/arg-modifier-dialog.component'; import { ArgModifierDialogComponent } from '../arg-modifier-dialog/arg-modifier-dialog.component';
@@ -22,9 +21,6 @@ export class SubscribeDialogComponent implements OnInit {
// state // state
subscribing = false; subscribing = false;
// no videos actually downloaded, just streamed
streamingOnlyMode = false;
// audio only mode // audio only mode
audioOnlyMode = false; audioOnlyMode = false;
@@ -70,7 +66,6 @@ export class SubscribeDialogComponent implements OnInit {
]; ];
constructor(private postsService: PostsService, constructor(private postsService: PostsService,
private snackBar: MatSnackBar,
private dialog: MatDialog, private dialog: MatDialog,
public dialogRef: MatDialogRef<SubscribeDialogComponent>) { } public dialogRef: MatDialogRef<SubscribeDialogComponent>) { }
@@ -81,7 +76,7 @@ export class SubscribeDialogComponent implements OnInit {
if (this.url && this.url !== '') { if (this.url && this.url !== '') {
// timerange must be specified if download_all is false // timerange must be specified if download_all is false
if (!this.download_all && !this.timerange_amount) { if (!this.download_all && !this.timerange_amount) {
this.openSnackBar('You must specify an amount of time'); this.postsService.openSnackBar($localize`You must specify an amount of time`);
return; return;
} }
this.subscribing = true; this.subscribing = true;
@@ -97,7 +92,7 @@ export class SubscribeDialogComponent implements OnInit {
this.dialogRef.close(res['new_sub']); this.dialogRef.close(res['new_sub']);
} else { } else {
if (res['error']) { if (res['error']) {
this.openSnackBar('ERROR: ' + res['error']); this.postsService.openSnackBar($localize`ERROR: ` + res['error']);
} }
this.dialogRef.close(); this.dialogRef.close();
} }
@@ -118,11 +113,4 @@ export class SubscribeDialogComponent implements OnInit {
} }
}); });
} }
public openSnackBar(message: string, action = '') {
this.snackBar.open(message, action, {
duration: 2000,
});
}
} }

View File

@@ -1,6 +1,5 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { PostsService } from 'app/posts.services'; import { PostsService } from 'app/posts.services';
import { MatSnackBar } from '@angular/material/snack-bar';
import { UpdaterStatus } from '../../../api-types'; import { UpdaterStatus } from '../../../api-types';
@Component({ @Component({
@@ -14,7 +13,7 @@ export class UpdateProgressDialogComponent implements OnInit {
updateInterval = 250; updateInterval = 250;
errored = false; errored = false;
constructor(private postsService: PostsService, private snackBar: MatSnackBar) { } constructor(private postsService: PostsService) { }
ngOnInit(): void { ngOnInit(): void {
this.getUpdateProgress(); this.getUpdateProgress();
@@ -28,16 +27,9 @@ export class UpdateProgressDialogComponent implements OnInit {
if (res) { if (res) {
this.updateStatus = res; this.updateStatus = res;
if (this.updateStatus && this.updateStatus['error']) { if (this.updateStatus && this.updateStatus['error']) {
this.openSnackBar('Update failed. Check logs for more details.'); this.postsService.openSnackBar($localize`Update failed. Check logs for more details.`);
} }
} }
}); });
} }
public openSnackBar(message: string, action: string = '') {
this.snackBar.open(message, action, {
duration: 2000,
});
}
} }

View File

@@ -47,6 +47,14 @@
<mat-divider style="margin-bottom: 16px;"></mat-divider> <mat-divider style="margin-bottom: 16px;"></mat-divider>
<div *ngIf="!new_file.isAudio" class="info-item">
<div class="info-item-label"><strong><ng-container i18n="Video resolution property">Resolution:</ng-container>&nbsp;</strong></div>
<div class="info-item-value">{{new_file.height ? new_file.height + 'p' : 'N/A'}}</div>
</div>
<div class="info-item">
<div class="info-item-label"><strong><ng-container i18n="Video audio bitrate property">Audio bitrate:</ng-container>&nbsp;</strong></div>
<div class="info-item-value">{{new_file.abr ? new_file.abr + ' Kbps' : 'N/A'}}</div>
</div>
<div class="info-item"> <div class="info-item">
<div class="info-item-label"><strong><ng-container i18n="Video file size property">File size:</ng-container>&nbsp;</strong></div> <div class="info-item-label"><strong><ng-container i18n="Video file size property">File size:</ng-container>&nbsp;</strong></div>
<div class="info-item-value">{{new_file.size ? filesize(new_file.size) : 'N/A'}}</div> <div class="info-item-value">{{new_file.size ? filesize(new_file.size) : 'N/A'}}</div>

View File

@@ -60,7 +60,7 @@
</div> </div>
</form> </form>
<br/> <br/>
<mat-checkbox [disabled]="current_download" (change)="videoModeChanged($event)" [(ngModel)]="audioOnly" style="float: left; margin-top: -12px"> <mat-checkbox [disabled]="autoplay && current_download" (change)="videoModeChanged($event)" [(ngModel)]="audioOnly" style="float: left; margin-top: -12px">
<ng-container i18n="Only Audio checkbox"> <ng-container i18n="Only Audio checkbox">
Only Audio Only Audio
</ng-container> </ng-container>

View File

@@ -10,12 +10,7 @@ import { Router, ActivatedRoute } from '@angular/router';
import { Platform } from '@angular/cdk/platform'; import { Platform } from '@angular/cdk/platform';
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'; import { RecentVideosComponent } from 'app/components/recent-videos/recent-videos.component';
import { Download, FileType } from 'api-types'; import { DatabaseFile, Download, FileType, Playlist } from 'api-types';
export let audioFilesMouseHovering = false;
export let videoFilesMouseHovering = false;
export let audioFilesOpened = false;
export let videoFilesOpened = false;
@Component({ @Component({
selector: 'app-root', selector: 'app-root',
@@ -55,8 +50,6 @@ export class MainComponent implements OnInit {
allowQualitySelect = false; allowQualitySelect = false;
downloadOnlyMode = false; downloadOnlyMode = false;
allowAutoplay = false; allowAutoplay = false;
audioFolderPath;
videoFolderPath;
use_youtubedl_archive = false; use_youtubedl_archive = false;
globalCustomArgs = null; globalCustomArgs = null;
allowAdvancedDownload = false; allowAdvancedDownload = false;
@@ -74,11 +67,8 @@ export class MainComponent implements OnInit {
results_showing = true; results_showing = true;
results = []; results = [];
mp3s: any[] = [];
mp4s: any[] = [];
playlists = {'audio': [], 'video': []}; playlists = {'audio': [], 'video': []};
playlist_thumbnails = {}; playlist_thumbnails = {};
downloading_content = {'audio': {}, 'video': {}};
downloads: Download[] = []; downloads: Download[] = [];
download_uids: string[] = []; download_uids: string[] = [];
current_download: Download = null; current_download: Download = null;
@@ -206,8 +196,6 @@ export class MainComponent implements OnInit {
&& this.postsService.hasPermission('filemanager'); && this.postsService.hasPermission('filemanager');
this.downloadOnlyMode = this.postsService.config['Extra']['download_only_mode']; this.downloadOnlyMode = this.postsService.config['Extra']['download_only_mode'];
this.allowAutoplay = this.postsService.config['Extra']['allow_autoplay']; this.allowAutoplay = this.postsService.config['Extra']['allow_autoplay'];
this.audioFolderPath = this.postsService.config['Downloader']['path-audio'];
this.videoFolderPath = this.postsService.config['Downloader']['path-video'];
this.use_youtubedl_archive = this.postsService.config['Downloader']['use_youtubedl_archive']; this.use_youtubedl_archive = this.postsService.config['Downloader']['use_youtubedl_archive'];
this.globalCustomArgs = this.postsService.config['Downloader']['custom_args']; this.globalCustomArgs = this.postsService.config['Downloader']['custom_args'];
this.youtubeSearchEnabled = this.postsService.config['API'] && this.postsService.config['API']['use_youtube_API'] && this.youtubeSearchEnabled = this.postsService.config['API'] && this.postsService.config['API']['use_youtube_API'] &&
@@ -314,7 +302,7 @@ export class MainComponent implements OnInit {
} }
// download helpers // download helpers
downloadHelper(container, type: string, is_playlist = false, force_view = false, navigate_mode = false): void { downloadHelper(container: DatabaseFile | Playlist, type: string, is_playlist = false, force_view = false, navigate_mode = false): void {
this.downloadingfile = false; this.downloadingfile = false;
if (!this.autoplay && !this.downloadOnlyMode && !navigate_mode) { if (!this.autoplay && !this.downloadOnlyMode && !navigate_mode) {
// do nothing // do nothing
@@ -325,7 +313,7 @@ export class MainComponent implements OnInit {
if (is_playlist) { if (is_playlist) {
this.downloadPlaylist(container['uid']); this.downloadPlaylist(container['uid']);
} else { } else {
this.downloadFileFromServer(container, type); this.downloadFileFromServer(container as DatabaseFile, type);
} }
this.reloadRecentVideos(); this.reloadRecentVideos();
} else { } else {
@@ -396,7 +384,7 @@ export class MainComponent implements OnInit {
}, () => { // can't access server }, () => { // can't access server
this.downloadingfile = false; this.downloadingfile = false;
this.current_download = null; this.current_download = null;
this.postsService.openSnackBar('Download failed!', 'OK.'); this.postsService.openSnackBar($localize`Download failed!`, 'OK.');
}); });
if (!this.autoplay && urls.length === 1) { if (!this.autoplay && urls.length === 1) {
@@ -444,7 +432,7 @@ export class MainComponent implements OnInit {
return null; return null;
} }
getDownloadByUID(uid: string) { getDownloadByUID(uid: string): Download {
const index = this.downloads.findIndex(download => download.uid === uid); const index = this.downloads.findIndex(download => download.uid === uid);
if (index !== -1) { if (index !== -1) {
return this.downloads[index]; return this.downloads[index];
@@ -453,7 +441,7 @@ export class MainComponent implements OnInit {
} }
} }
removeDownloadFromCurrentDownloads(download_to_remove): boolean { removeDownloadFromCurrentDownloads(download_to_remove: Download): boolean {
if (this.current_download === download_to_remove) { if (this.current_download === download_to_remove) {
this.current_download = null; this.current_download = null;
} }
@@ -466,11 +454,9 @@ export class MainComponent implements OnInit {
} }
} }
downloadFileFromServer(file, type: string): void { downloadFileFromServer(file: DatabaseFile, type: string): void {
const ext = type === 'audio' ? 'mp3' : 'mp4' const ext = type === 'audio' ? 'mp3' : 'mp4'
this.downloading_content[type][file.id] = true;
this.postsService.downloadFileFromServer(file.uid).subscribe(res => { this.postsService.downloadFileFromServer(file.uid).subscribe(res => {
this.downloading_content[type][file.id] = false;
const blob: Blob = res; const blob: Blob = res;
saveAs(blob, decodeURIComponent(file.id) + `.${ext}`); saveAs(blob, decodeURIComponent(file.id) + `.${ext}`);
@@ -481,9 +467,8 @@ export class MainComponent implements OnInit {
}); });
} }
downloadPlaylist(playlist): void { downloadPlaylist(playlist: Playlist): void {
this.postsService.downloadPlaylistFromServer(playlist.id).subscribe(res => { this.postsService.downloadPlaylistFromServer(playlist.id).subscribe(res => {
if (playlist.id) { this.downloading_content[playlist.type][playlist.id] = false };
const blob: Blob = res; const blob: Blob = res;
saveAs(blob, playlist.name + '.zip'); saveAs(blob, playlist.name + '.zip');
}); });
@@ -603,11 +588,11 @@ export class MainComponent implements OnInit {
if (simulated_args) { if (simulated_args) {
// hide password if needed // hide password if needed
const passwordIndex = simulated_args.indexOf('--password'); const passwordIndex = simulated_args.indexOf('--password');
console.log(passwordIndex);
if (passwordIndex !== -1 && passwordIndex !== simulated_args.length - 1) { if (passwordIndex !== -1 && passwordIndex !== simulated_args.length - 1) {
simulated_args[passwordIndex + 1] = simulated_args[passwordIndex + 1].replace(/./g, '*'); simulated_args[passwordIndex + 1] = simulated_args[passwordIndex + 1].replace(/./g, '*');
} }
this.simulatedOutput = `youtube-dl ${this.url} ${simulated_args.join(' ')}`; const downloader = this.postsService.config.Advanced.default_downloader;
this.simulatedOutput = `${downloader} ${this.url} ${simulated_args.join(' ')}`;
} }
}); });
} }
@@ -780,13 +765,14 @@ export class MainComponent implements OnInit {
if (this.current_download['finished'] && !this.current_download['error']) { if (this.current_download['finished'] && !this.current_download['error']) {
const container = this.current_download['container']; const container = this.current_download['container'];
const is_playlist = this.current_download['file_uids'].length > 1; const is_playlist = this.current_download['file_uids'].length > 1;
this.downloadHelper(container, this.current_download['type'], is_playlist, false); const type = this.current_download['type'];
this.current_download = null; this.current_download = null;
this.downloadHelper(container, type, is_playlist, false);
} else if (this.current_download['finished'] && this.current_download['error']) { } else if (this.current_download['finished'] && this.current_download['error']) {
this.downloadingfile = false; this.downloadingfile = false;
this.current_download = null; this.current_download = null;
this.postsService.openSnackBar('Download failed!', 'OK.'); this.postsService.openSnackBar($localize`Download failed!`, 'OK.');
} }
} else { } else {
// console.log('failed to get new download'); // console.log('failed to get new download');

View File

@@ -3,7 +3,6 @@ import { VgApiService } from '@videogular/ngx-videogular/core';
import { PostsService } from 'app/posts.services'; import { PostsService } from 'app/posts.services';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
import { MatSnackBar } from '@angular/material/snack-bar';
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 { FileType } from '../../api-types'; import { FileType } from '../../api-types';
@@ -109,7 +108,7 @@ export class PlayerComponent implements OnInit, AfterViewInit, OnDestroy {
} }
constructor(public postsService: PostsService, private route: ActivatedRoute, private dialog: MatDialog, private router: Router, constructor(public postsService: PostsService, private route: ActivatedRoute, private dialog: MatDialog, private router: Router,
public snackBar: MatSnackBar, private cdr: ChangeDetectorRef) { private cdr: ChangeDetectorRef) {
} }
processConfig(): void { processConfig(): void {
@@ -147,7 +146,7 @@ export class PlayerComponent implements OnInit, AfterViewInit, OnDestroy {
this.postsService.getFile(this.uid, this.uuid).subscribe(res => { this.postsService.getFile(this.uid, this.uuid).subscribe(res => {
this.db_file = res['file']; this.db_file = res['file'];
if (!this.db_file) { if (!this.db_file) {
this.postsService.openSnackBar('Failed to get file information from the server.', 'Dismiss'); this.postsService.openSnackBar($localize`Failed to get file information from the server.`, 'Dismiss');
return; return;
} }
this.postsService.incrementViewCount(this.db_file['uid'], null, this.uuid).subscribe(() => undefined, err => { this.postsService.incrementViewCount(this.db_file['uid'], null, this.uuid).subscribe(() => undefined, err => {
@@ -169,6 +168,7 @@ export class PlayerComponent implements OnInit, AfterViewInit, OnDestroy {
this.uids = this.subscription.videos.map(video => video['uid']); this.uids = this.subscription.videos.map(video => video['uid']);
this.parseFileNames(); this.parseFileNames();
}, () => { }, () => {
// TODO: Make translatable
this.postsService.openSnackBar(`Failed to find subscription ${this.sub_id}`, 'Dismiss'); this.postsService.openSnackBar(`Failed to find subscription ${this.sub_id}`, 'Dismiss');
}); });
} }
@@ -183,10 +183,10 @@ export class PlayerComponent implements OnInit, AfterViewInit, OnDestroy {
this.show_player = true; this.show_player = true;
this.parseFileNames(); this.parseFileNames();
} else { } else {
this.postsService.openSnackBar('Failed to load playlist!', ''); this.postsService.openSnackBar($localize`Failed to load playlist!`);
} }
}, () => { }, () => {
this.postsService.openSnackBar('Failed to load playlist!', ''); this.postsService.openSnackBar($localize`Failed to load playlist!`);
}); });
} }
@@ -351,7 +351,7 @@ export class PlayerComponent implements OnInit, AfterViewInit, OnDestroy {
width: '60vw' width: '60vw'
}); });
dialogRef.afterClosed().subscribe(res => { dialogRef.afterClosed().subscribe(() => {
if (!this.playlist_id) { if (!this.playlist_id) {
this.getFile(); this.getFile();
} else { } else {
@@ -361,12 +361,16 @@ export class PlayerComponent implements OnInit, AfterViewInit, OnDestroy {
} }
openFileInfoDialog(): void { openFileInfoDialog(): void {
this.dialog.open(VideoInfoDialogComponent, { const dialogRef = this.dialog.open(VideoInfoDialogComponent, {
data: { data: {
file: this.db_file, file: this.db_file,
}, },
minWidth: '50vw' minWidth: '50vw'
}) });
dialogRef.afterClosed().subscribe(() => {
this.db_file = dialogRef.componentInstance.file;
});
} }
setPlaybackTimestamp(time: number): void { setPlaybackTimestamp(time: number): void {

View File

@@ -97,7 +97,11 @@ import {
Schedule, Schedule,
ClearDownloadsRequest, ClearDownloadsRequest,
Category, Category,
UpdateFileRequest UpdateFileRequest,
Sort,
FileTypeFilter,
GetAllFilesRequest,
GetAllTasksResponse
} from '../api-types'; } from '../api-types';
import { isoLangs } from './settings/locales_list'; import { isoLangs } from './settings/locales_list';
import { Title } from '@angular/platform-browser'; import { Title } from '@angular/platform-browser';
@@ -355,8 +359,9 @@ export class PostsService implements CanActivate {
return this.http.post<GetFileResponse>(this.path + 'getFile', body, this.httpOptions); return this.http.post<GetFileResponse>(this.path + 'getFile', body, this.httpOptions);
} }
getAllFiles(sort, range, text_search, file_type_filter) { getAllFiles(sort: Sort = null, range: number[] = null, text_search: string = null, file_type_filter: FileTypeFilter = FileTypeFilter.BOTH, sub_id: string = null) {
return this.http.post<GetAllFilesResponse>(this.path + 'getAllFiles', {sort: sort, range: range, text_search: text_search, file_type_filter: file_type_filter}, this.httpOptions); const body: GetAllFilesRequest = {sort: sort, range: range, text_search: text_search, file_type_filter: file_type_filter, sub_id: sub_id};
return this.http.post<GetAllFilesResponse>(this.path + 'getAllFiles', body, this.httpOptions);
} }
updateFile(uid: string, change_obj: Object) { updateFile(uid: string, change_obj: Object) {
@@ -443,10 +448,9 @@ export class PostsService implements CanActivate {
return this.http.post<SuccessObject>(this.path + 'disableSharing', body, this.httpOptions); return this.http.post<SuccessObject>(this.path + 'disableSharing', body, this.httpOptions);
} }
createPlaylist(playlistName: string, uids: string[], type: FileType, thumbnailURL: string) { createPlaylist(playlistName: string, uids: string[], thumbnailURL: string) {
const body: CreatePlaylistRequest = {playlistName: playlistName, const body: CreatePlaylistRequest = {playlistName: playlistName,
uids: uids, uids: uids,
type: type,
thumbnailURL: thumbnailURL}; thumbnailURL: thumbnailURL};
return this.http.post<CreatePlaylistResponse>(this.path + 'createPlaylist', body, this.httpOptions); return this.http.post<CreatePlaylistResponse>(this.path + 'createPlaylist', body, this.httpOptions);
} }
@@ -471,8 +475,8 @@ export class PostsService implements CanActivate {
return this.http.post<SuccessObject>(this.path + 'updatePlaylist', body, this.httpOptions); return this.http.post<SuccessObject>(this.path + 'updatePlaylist', body, this.httpOptions);
} }
removePlaylist(playlist_id: string, type: FileType) { removePlaylist(playlist_id: string) {
const body: DeletePlaylistRequest = {playlist_id: playlist_id, type: type}; const body: DeletePlaylistRequest = {playlist_id: playlist_id};
return this.http.post<SuccessObject>(this.path + 'deletePlaylist', body, this.httpOptions); return this.http.post<SuccessObject>(this.path + 'deletePlaylist', body, this.httpOptions);
} }
@@ -593,7 +597,7 @@ export class PostsService implements CanActivate {
} }
getTasks() { getTasks() {
return this.http.post<SuccessObject>(this.path + 'getTasks', {}, this.httpOptions); return this.http.post<GetAllTasksResponse>(this.path + 'getTasks', {}, this.httpOptions);
} }
resetTasks() { resetTasks() {

View File

@@ -263,11 +263,16 @@
</div> </div>
<div class="col-12"> <div class="col-12">
<mat-form-field class="text-field" color="accent"> <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> <input [disabled]="!new_config['API']['use_twitch_API']" [(ngModel)]="new_config['API']['twitch_client_ID']" matInput placeholder="Twitch Client ID" i18n-placeholder="Twitch Client ID setting placeholder" required>
<mat-hint><ng-container i18n="Twitch API Key setting hint AKA preamble">Also known as a Client ID.</ng-container>&nbsp;<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-hint><a target="_blank" href="https://dev.twitch.tv/docs/api/"><ng-container i18n="Twitch Client ID setting hint">Generating an ID/secret is easy!</ng-container></a></mat-hint>
</mat-form-field> </mat-form-field>
</div> </div>
<div class="col-12 mt-4"> <div class="col-12 mt-2">
<mat-form-field class="text-field" color="accent">
<input [disabled]="!new_config['API']['use_twitch_API']" [(ngModel)]="new_config['API']['twitch_client_secret']" matInput placeholder="Twitch Client Secret" i18n-placeholder="Twitch Client Secret setting placeholder" required>
</mat-form-field>
</div>
<div class="col-12 mt-2">
<mat-checkbox color="accent" [(ngModel)]="new_config['API']['use_sponsorblock_API']" matTooltip="Enables a button to skip ads when viewing supported videos." i18n-matTooltip="SponsorBlock API tooltip"><ng-container i18n="Use SponsorBlock API setting">Use SponsorBlock API</ng-container></mat-checkbox> <mat-checkbox color="accent" [(ngModel)]="new_config['API']['use_sponsorblock_API']" matTooltip="Enables a button to skip ads when viewing supported videos." i18n-matTooltip="SponsorBlock API tooltip"><ng-container i18n="Use SponsorBlock API setting">Use SponsorBlock API</ng-container></mat-checkbox>
</div> </div>
<div class="col-12 mt-2 mb-3"> <div class="col-12 mt-2 mb-3">
@@ -323,6 +328,7 @@
<div class="test-connection-div"> <div class="test-connection-div">
<button (click)="testConnectionString(new_config['Database']['mongodb_connection_string'])" [disabled]="testing_connection_string" mat-flat-button color="accent"><ng-container i18n="Test connection string button">Test connection string</ng-container></button> <button (click)="testConnectionString(new_config['Database']['mongodb_connection_string'])" [disabled]="testing_connection_string" mat-flat-button color="accent"><ng-container i18n="Test connection string button">Test connection string</ng-container></button>
<mat-spinner class="test-connection-spinner" style="margin-left: 10px" *ngIf="testing_connection_string" [diameter]="25"></mat-spinner>
</div> </div>
<div class="transfer-db-div"> <div class="transfer-db-div">

View File

@@ -105,4 +105,11 @@
.action-buttons { .action-buttons {
position: absolute; position: absolute;
bottom: 15px; bottom: 15px;
}
.test-connection-spinner {
display: inline-block;
position: relative;
top: 6px;
margin-left: 10px;
} }

View File

@@ -13,6 +13,7 @@ import { moveItemInArray, CdkDragDrop } from '@angular/cdk/drag-drop';
import { InputDialogComponent } from 'app/input-dialog/input-dialog.component'; import { InputDialogComponent } from 'app/input-dialog/input-dialog.component';
import { EditCategoryDialogComponent } from 'app/dialogs/edit-category-dialog/edit-category-dialog.component'; import { EditCategoryDialogComponent } from 'app/dialogs/edit-category-dialog/edit-category-dialog.component';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { Category } from 'api-types';
@Component({ @Component({
selector: 'app-settings', selector: 'app-settings',
@@ -62,7 +63,7 @@ export class SettingsComponent implements OnInit {
Object.keys(this.INDEX_TO_TAB).forEach(key => { this.TAB_TO_INDEX[this.INDEX_TO_TAB[key]] = key; }); Object.keys(this.INDEX_TO_TAB).forEach(key => { this.TAB_TO_INDEX[this.INDEX_TO_TAB[key]] = key; });
} }
ngOnInit() { ngOnInit(): void {
if (this.postsService.initialized) { if (this.postsService.initialized) {
this.getConfig(); this.getConfig();
this.getDBInfo(); this.getDBInfo();
@@ -90,16 +91,16 @@ export class SettingsComponent implements OnInit {
}); });
} }
getConfig() { getConfig(): void {
this.initial_config = this.postsService.config; this.initial_config = this.postsService.config;
this.new_config = JSON.parse(JSON.stringify(this.initial_config)); this.new_config = JSON.parse(JSON.stringify(this.initial_config));
} }
settingsSame() { settingsSame(): boolean {
return JSON.stringify(this.new_config) === JSON.stringify(this.initial_config); return JSON.stringify(this.new_config) === JSON.stringify(this.initial_config);
} }
saveSettings() { saveSettings(): void {
const settingsToSave = {'YoutubeDLMaterial': this.new_config}; const settingsToSave = {'YoutubeDLMaterial': this.new_config};
this.postsService.setConfig(settingsToSave).subscribe(res => { this.postsService.setConfig(settingsToSave).subscribe(res => {
if (res['success']) { if (res['success']) {
@@ -111,31 +112,31 @@ export class SettingsComponent implements OnInit {
this.initial_config = JSON.parse(JSON.stringify(this.new_config)); this.initial_config = JSON.parse(JSON.stringify(this.new_config));
this.postsService.reload_config.next(true); this.postsService.reload_config.next(true);
} }
}, err => { }, () => {
console.error('Failed to save config!'); console.error('Failed to save config!');
}) })
} }
cancelSettings() { cancelSettings(): void {
this.new_config = JSON.parse(JSON.stringify(this.initial_config)); this.new_config = JSON.parse(JSON.stringify(this.initial_config));
} }
tabChanged(event) { tabChanged(event): void {
const index = event['index']; const index = event['index'];
this.router.navigate(['/settings', {tab: this.INDEX_TO_TAB[index]}]); this.router.navigate(['/settings', {tab: this.INDEX_TO_TAB[index]}]);
} }
dropCategory(event: CdkDragDrop<string[]>) { dropCategory(event: CdkDragDrop<string[]>): void {
moveItemInArray(this.postsService.categories, event.previousIndex, event.currentIndex); moveItemInArray(this.postsService.categories, event.previousIndex, event.currentIndex);
this.postsService.updateCategories(this.postsService.categories).subscribe(res => { this.postsService.updateCategories(this.postsService.categories).subscribe(res => {
}, err => { }, () => {
this.postsService.openSnackBar('Failed to update categories!'); this.postsService.openSnackBar($localize`Failed to update categories!`);
}); });
} }
openAddCategoryDialog() { openAddCategoryDialog(): void {
const done = new EventEmitter<any>(); const done = new EventEmitter<boolean>();
const dialogRef = this.dialog.open(InputDialogComponent, { const dialogRef = this.dialog.open(InputDialogComponent, {
width: '300px', width: '300px',
data: { data: {
@@ -162,7 +163,7 @@ export class SettingsComponent implements OnInit {
}); });
} }
deleteCategory(category) { deleteCategory(category: Category): void {
const dialogRef = this.dialog.open(ConfirmDialogComponent, { const dialogRef = this.dialog.open(ConfirmDialogComponent, {
data: { data: {
dialogTitle: 'Delete category', dialogTitle: 'Delete category',
@@ -175,17 +176,18 @@ export class SettingsComponent implements OnInit {
if (confirmed) { if (confirmed) {
this.postsService.deleteCategory(category['uid']).subscribe(res => { this.postsService.deleteCategory(category['uid']).subscribe(res => {
if (res['success']) { if (res['success']) {
// TODO: Make translatable
this.postsService.openSnackBar(`Successfully deleted ${category['name']}!`); this.postsService.openSnackBar(`Successfully deleted ${category['name']}!`);
this.postsService.reloadCategories(); this.postsService.reloadCategories();
} }
}, err => { }, () => {
this.postsService.openSnackBar(`Failed to delete ${category['name']}!`); this.postsService.openSnackBar(`Failed to delete ${category['name']}!`);
}); });
} }
}); });
} }
openEditCategoryDialog(category) { openEditCategoryDialog(category: Category): void {
this.dialog.open(EditCategoryDialogComponent, { this.dialog.open(EditCategoryDialogComponent, {
data: { data: {
category: category category: category
@@ -193,7 +195,7 @@ export class SettingsComponent implements OnInit {
}); });
} }
generateAPIKey() { generateAPIKey(): void {
this.postsService.generateNewAPIKey().subscribe(res => { this.postsService.generateNewAPIKey().subscribe(res => {
if (res['new_api_key']) { if (res['new_api_key']) {
this.initial_config.API.API_key = res['new_api_key']; this.initial_config.API.API_key = res['new_api_key'];
@@ -202,16 +204,16 @@ export class SettingsComponent implements OnInit {
}); });
} }
localeSelectChanged(new_val) { localeSelectChanged(new_val: string): void {
localStorage.setItem('locale', new_val); localStorage.setItem('locale', new_val);
this.openSnackBar('Language successfully changed! Reload to update the page.') this.postsService.openSnackBar($localize`Language successfully changed! Reload to update the page.`)
} }
generateBookmarklet() { generateBookmarklet(): void {
this.bookmarksite('YTDL-Material', this.generated_bookmarklet_code); this.bookmarksite('YTDL-Material', this.generated_bookmarklet_code);
} }
generateBookmarkletCode() { generateBookmarkletCode(): string {
const currentURL = window.location.href.split('#')[0]; const currentURL = window.location.href.split('#')[0];
const homePageWithArgsURL = currentURL + '#/home;url='; const homePageWithArgsURL = currentURL + '#/home;url=';
const audioOnly = this.bookmarkletAudioOnly; const audioOnly = this.bookmarkletAudioOnly;
@@ -226,13 +228,13 @@ export class SettingsComponent implements OnInit {
} }
// not currently functioning on most platforms. hence not in use // not currently functioning on most platforms. hence not in use
bookmarksite(title, url) { bookmarksite(title: string, url: string): void {
// Internet Explorer // Internet Explorer
if (document.all) { if (document.all) {
window['external']['AddFavorite'](url, title); window['external']['AddFavorite'](url, title);
} else if (window['chrome']) { } else if (window['chrome']) {
// Google Chrome // Google Chrome
this.openSnackBar('Chrome users must drag the \'Alternate URL\' link to your bookmarks.'); this.postsService.openSnackBar($localize`Chrome users must drag the 'Alternate URL' link to your bookmarks.`);
} else if (window['sidebar']) { } else if (window['sidebar']) {
// Firefox // Firefox
window['sidebar'].addPanel(title, url, ''); window['sidebar'].addPanel(title, url, '');
@@ -246,7 +248,7 @@ export class SettingsComponent implements OnInit {
} }
} }
openArgsModifierDialog() { openArgsModifierDialog(): void {
const dialogRef = this.dialog.open(ArgModifierDialogComponent, { const dialogRef = this.dialog.open(ArgModifierDialogComponent, {
data: { data: {
initial_args: this.new_config['Downloader']['custom_args'] initial_args: this.new_config['Downloader']['custom_args']
@@ -259,20 +261,20 @@ export class SettingsComponent implements OnInit {
}); });
} }
getLatestGithubRelease() { getLatestGithubRelease(): void {
this.postsService.getLatestGithubRelease().subscribe(res => { this.postsService.getLatestGithubRelease().subscribe(res => {
this.latestGithubRelease = res; this.latestGithubRelease = res;
}); });
} }
openCookiesUploaderDialog() { openCookiesUploaderDialog(): void {
this.dialog.open(CookiesUploaderDialogComponent, { this.dialog.open(CookiesUploaderDialogComponent, {
width: '65vw' width: '65vw'
}); });
} }
killAllDownloads() { killAllDownloads(): void {
const done = new EventEmitter<any>(); const done = new EventEmitter<boolean>();
const dialogRef = this.dialog.open(ConfirmDialogComponent, { const dialogRef = this.dialog.open(ConfirmDialogComponent, {
data: { data: {
dialogTitle: 'Kill downloads', dialogTitle: 'Kill downloads',
@@ -287,34 +289,34 @@ export class SettingsComponent implements OnInit {
this.postsService.killAllDownloads().subscribe(res => { this.postsService.killAllDownloads().subscribe(res => {
if (res['success']) { if (res['success']) {
dialogRef.close(); dialogRef.close();
this.postsService.openSnackBar('Successfully killed all downloads!'); this.postsService.openSnackBar($localize`Successfully killed all downloads!`);
} else { } else {
dialogRef.close(); dialogRef.close();
this.postsService.openSnackBar('Failed to kill all downloads! Check logs for details.'); this.postsService.openSnackBar($localize`Failed to kill all downloads! Check logs for details.`);
} }
}, err => { }, () => {
dialogRef.close(); dialogRef.close();
this.postsService.openSnackBar('Failed to kill all downloads! Check logs for details.'); this.postsService.openSnackBar($localize`Failed to kill all downloads! Check logs for details.`);
}); });
} }
}); });
} }
restartServer() { restartServer(): void {
this.postsService.restartServer().subscribe(res => { this.postsService.restartServer().subscribe(() => {
this.postsService.openSnackBar('Restarting!'); this.postsService.openSnackBar($localize`Restarting!`);
}, err => { }, () => {
this.postsService.openSnackBar('Failed to restart the server.'); this.postsService.openSnackBar($localize`Failed to restart the server.`);
}); });
} }
getDBInfo() { getDBInfo(): void {
this.postsService.getDBInfo().subscribe(res => { this.postsService.getDBInfo().subscribe(res => {
this.db_info = res['db_info']; this.db_info = res['db_info'];
}); });
} }
transferDB() { transferDB(): void {
const dialogRef = this.dialog.open(ConfirmDialogComponent, { const dialogRef = this.dialog.open(ConfirmDialogComponent, {
data: { data: {
dialogTitle: 'Transfer DB', dialogTitle: 'Transfer DB',
@@ -329,44 +331,36 @@ export class SettingsComponent implements OnInit {
}); });
} }
_transferDB() { _transferDB(): void {
this.db_transferring = true; this.db_transferring = true;
this.postsService.transferDB(this.db_info['using_local_db']).subscribe(res => { this.postsService.transferDB(this.db_info['using_local_db']).subscribe(res => {
this.db_transferring = false; this.db_transferring = false;
const success = res['success']; const success = res['success'];
if (success) { if (success) {
this.openSnackBar('Successfully transfered DB! Reloading info...'); this.postsService.openSnackBar($localize`Successfully transfered DB! Reloading info...`);
this.getDBInfo(); this.getDBInfo();
} else { } else {
this.openSnackBar('Failed to transfer DB -- transfer was aborted. Error: ' + res['error']); this.postsService.openSnackBar($localize`Failed to transfer DB -- transfer was aborted. Error: ` + res['error']);
} }
}, err => { }, err => {
this.db_transferring = false; this.db_transferring = false;
this.openSnackBar('Failed to transfer DB -- API call failed. See browser logs for details.'); this.postsService.openSnackBar($localize`Failed to transfer DB -- API call failed. See browser logs for details.`);
console.error(err); console.error(err);
}); });
} }
testConnectionString(connection_string) { testConnectionString(connection_string: string): void {
this.testing_connection_string = true; this.testing_connection_string = true;
this.postsService.testConnectionString(connection_string).subscribe(res => { this.postsService.testConnectionString(connection_string).subscribe(res => {
this.testing_connection_string = false; this.testing_connection_string = false;
if (res['success']) { if (res['success']) {
this.postsService.openSnackBar('Connection successful!'); this.postsService.openSnackBar($localize`Connection successful!`);
} else { } else {
this.postsService.openSnackBar('Connection failed! Error: ' + res['error']); this.postsService.openSnackBar($localize`Connection failed! Error: ` + res['error']);
} }
}, err => { }, () => {
this.testing_connection_string = false; this.testing_connection_string = false;
this.postsService.openSnackBar('Connection failed! Error: Server error. See logs for more info.'); this.postsService.openSnackBar($localize`Connection failed! Error: Server error. See logs for more info.`);
}); });
} }
// snackbar helper
public openSnackBar(message: string, action: string = '') {
this.snackBar.open(message, action, {
duration: 2000,
});
}
} }

View File

@@ -1,20 +0,0 @@
<div style="position: relative; width: fit-content;">
<div class="duration-time">
<ng-container i18n="Video duration label">Length:</ng-container>&nbsp;{{formattedDuration}}
</div>
<button [matMenuTriggerFor]="action_menu" class="menuButton" mat-icon-button><mat-icon>more_vert</mat-icon></button>
<mat-menu #action_menu="matMenu">
<button (click)="openSubscriptionInfoDialog()" mat-menu-item><mat-icon>info</mat-icon><ng-container i18n="Subscription video info button">Info</ng-container></button>
<button (click)="deleteAndRedownload()" mat-menu-item><mat-icon>restore</mat-icon><ng-container i18n="Delete and redownload subscription video button">Delete and redownload</ng-container></button>
<button (click)="deleteForever()" mat-menu-item *ngIf="sub.archive && use_youtubedl_archive"><mat-icon>delete_forever</mat-icon><ng-container i18n="Delete forever subscription video button">Delete forever</ng-container></button>
</mat-menu>
<mat-card (click)="goToFile()" matRipple class="example-card mat-elevation-z6">
<div style="padding:5px">
<div *ngIf="!image_errored && file.thumbnailURL" class="img-div">
<img class="image" (error)="onImgError($event)" [src]="file.thumbnailURL" alt="Thumbnail">
</div>
<span class="max-two-lines"><strong>{{file.title}}</strong></span>
</div>
</mat-card>
</div>

View File

@@ -1,76 +0,0 @@
.example-card {
width: 200px;
height: 200px;
padding: 0px;
cursor: pointer;
}
.menuButton {
right: 0px;
top: -1px;
position: absolute;
z-index: 999;
}
/* Coerce the <span> icon container away from display:inline */
.mat-icon-button .mat-button-wrapper {
display: flex;
justify-content: center;
}
.image {
width: 200px;
height: 112.5px;
object-fit: cover;
}
.example-full-width-height {
width: 100%;
height: 100%
}
.centered {
margin: 0 auto;
top: 50%;
left: 50%;
}
.img-div {
max-height: 80px;
padding: 0px;
margin: 32px 0px 0px -5px;
width: calc(100% + 5px + 5px);
}
.max-two-lines {
display: -webkit-box;
display: -moz-box;
max-height: 2.4em;
line-height: 1.2em;
overflow: hidden;
text-overflow: ellipsis;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
bottom: 5px;
position: absolute;
}
.duration-time {
position: absolute;
left: 5px;
top: 5px;
z-index: 99999;
}
@media (max-width: 576px){
.example-card {
width: 175px !important;
}
.image {
width: 175px;
}
}

View File

@@ -1,25 +0,0 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { SubscriptionFileCardComponent } from './subscription-file-card.component';
describe('SubscriptionFileCardComponent', () => {
let component: SubscriptionFileCardComponent;
let fixture: ComponentFixture<SubscriptionFileCardComponent>;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [ SubscriptionFileCardComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(SubscriptionFileCardComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -1,98 +0,0 @@
import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core';
import { Observable, Subject } from 'rxjs';
import { MatSnackBar } from '@angular/material/snack-bar';
import { Router } from '@angular/router';
import { PostsService } from 'app/posts.services';
import { MatDialog } from '@angular/material/dialog';
import { VideoInfoDialogComponent } from 'app/dialogs/video-info-dialog/video-info-dialog.component';
@Component({
selector: 'app-subscription-file-card',
templateUrl: './subscription-file-card.component.html',
styleUrls: ['./subscription-file-card.component.scss']
})
export class SubscriptionFileCardComponent implements OnInit {
image_errored = false;
image_loaded = false;
formattedDuration = null;
@Input() file;
@Input() sub;
@Input() use_youtubedl_archive = false;
@Output() goToFileEmit = new EventEmitter<any>();
@Output() reloadSubscription = new EventEmitter<boolean>();
constructor(private snackBar: MatSnackBar, private postsService: PostsService, private dialog: MatDialog) {}
ngOnInit() {
if (this.file.duration) {
this.formattedDuration = fancyTimeFormat(this.file.duration);
}
}
onImgError(event) {
this.image_errored = true;
}
imageLoaded(loaded) {
this.image_loaded = true;
}
goToFile() {
const emit_obj = {
uid: this.file.uid,
url: this.file.requested_formats ? this.file.requested_formats[0].url : this.file.url
}
this.goToFileEmit.emit(emit_obj);
}
openSubscriptionInfoDialog() {
const dialogRef = this.dialog.open(VideoInfoDialogComponent, {
data: {
file: this.file,
},
minWidth: '50vw'
});
}
deleteAndRedownload() {
this.postsService.deleteSubscriptionFile(this.sub, this.file.id, false, this.file.uid).subscribe(res => {
this.reloadSubscription.emit(true);
this.openSnackBar(`Successfully deleted file: '${this.file.id}'`, 'Dismiss.');
});
}
deleteForever() {
this.postsService.deleteSubscriptionFile(this.sub, this.file.id, true, this.file.uid).subscribe(res => {
this.reloadSubscription.emit(true);
this.openSnackBar(`Successfully deleted file: '${this.file.id}'`, 'Dismiss.');
});
}
public openSnackBar(message: string, action: string) {
this.snackBar.open(message, action, {
duration: 2000,
});
}
}
function fancyTimeFormat(time) {
// Hours, minutes and seconds
const hrs = ~~(time / 3600);
const mins = ~~((time % 3600) / 60);
const secs = ~~time % 60;
// Output like "1:01" or "4:03:59" or "123:03:59"
let ret = '';
if (hrs > 0) {
ret += '' + hrs + ':' + (mins < 10 ? '0' : '');
}
ret += '' + mins + ':' + (secs < 10 ? '0' : '');
ret += '' + secs;
return ret;
}

View File

@@ -10,38 +10,7 @@
<br/> <br/>
<div *ngIf="subscription"> <div *ngIf="subscription">
<div class="flex-grid"> <app-recent-videos [sub_id]="subscription.id" [usePaginator]="false"></app-recent-videos>
<div class="filter-select-parent">
<div style="display: inline-block;">
<mat-select style="width: 110px;" [(ngModel)]="this.filterProperty" (selectionChange)="filterOptionChanged($event.value)">
<mat-option *ngFor="let filterOption of filterProperties | keyvalue" [value]="filterOption.value">
{{filterOption['value']['label']}}
</mat-option>
</mat-select>
</div>
<div style="display: inline-block;">
<button (click)="toggleModeChange()" mat-icon-button><mat-icon>{{descendingMode ? 'arrow_downward' : 'arrow_upward'}}</mat-icon></button>
</div>
</div>
<div class="col">
</div>
<div class="col">
<h4 i18n="Subscription videos title" style="text-align: center; margin-bottom: 20px;">Videos</h4>
</div>
<div style="top: -12px;" class="col">
<mat-form-field [ngClass]="searchIsFocused ? 'search-bar-focused' : 'search-bar-unfocused'" class="search-bar" color="accent">
<input (focus)="searchIsFocused = true" (blur)="searchIsFocused = false" class="search-input" type="text" placeholder="Search" i18n-placeholder="Subscription videos search placeholder" [(ngModel)]="search_text" (ngModelChange)="onSearchInputChanged($event)" matInput>
<mat-icon matSuffix>search</mat-icon>
</mat-form-field>
</div>
</div>
<div class="container">
<div class="row justify-content-center">
<div *ngFor="let file of filtered_files" class="col-6 col-lg-4 mb-2 mt-2 sub-file-col">
<app-subscription-file-card (reloadSubscription)="getSubscription()" (goToFileEmit)="goToFile($event)" [file]="file" [sub]="subscription" [use_youtubedl_archive]="use_youtubedl_archive"></app-subscription-file-card>
</div>
</div>
</div>
</div> </div>
<button class="edit-button" color="primary" (click)="editSubscription()" [disabled]="downloading" mat-fab><mat-icon class="save-icon">edit</mat-icon></button> <button class="edit-button" color="primary" (click)="editSubscription()" [disabled]="downloading" mat-fab><mat-icon class="save-icon">edit</mat-icon></button>
<button class="watch-button" color="primary" (click)="watchSubscription()" mat-fab><mat-icon class="save-icon">video_library</mat-icon></button> <button class="watch-button" color="primary" (click)="watchSubscription()" mat-fab><mat-icon class="save-icon">video_library</mat-icon></button>

View File

@@ -100,22 +100,12 @@ export class SubscriptionComponent implements OnInit, OnDestroy {
}); });
} }
getConfig() { getConfig(): void {
this.use_youtubedl_archive = this.postsService.config['Downloader']['use_youtubedl_archive']; this.use_youtubedl_archive = this.postsService.config['Downloader']['use_youtubedl_archive'];
} }
goToFile(emit_obj) {
const uid = emit_obj['uid'];
const url = emit_obj['url'];
localStorage.setItem('player_navigator', this.router.url);
if (this.subscription.streamingOnly) {
this.router.navigate(['/player', {uid: uid, url: url}]);
} else {
this.router.navigate(['/player', {uid: uid}]);
}
}
onSearchInputChanged(newvalue) { onSearchInputChanged(newvalue: string): void {
if (newvalue.length > 0) { if (newvalue.length > 0) {
this.search_mode = true; this.search_mode = true;
this.filterFiles(newvalue); this.filterFiles(newvalue);
@@ -129,7 +119,7 @@ export class SubscriptionComponent implements OnInit, OnDestroy {
this.filtered_files = this.files.filter(option => option.id.toLowerCase().includes(filterValue)); this.filtered_files = this.files.filter(option => option.id.toLowerCase().includes(filterValue));
} }
filterByProperty(prop) { filterByProperty(prop: string): void {
if (this.descendingMode) { if (this.descendingMode) {
this.filtered_files = this.filtered_files.sort((a, b) => (a[prop] > b[prop] ? -1 : 1)); this.filtered_files = this.filtered_files.sort((a, b) => (a[prop] > b[prop] ? -1 : 1));
} else { } else {
@@ -142,17 +132,12 @@ export class SubscriptionComponent implements OnInit, OnDestroy {
localStorage.setItem('filter_property', value['key']); localStorage.setItem('filter_property', value['key']);
} }
toggleModeChange() { toggleModeChange(): void {
this.descendingMode = !this.descendingMode; this.descendingMode = !this.descendingMode;
this.filterByProperty(this.filterProperty['property']); this.filterByProperty(this.filterProperty['property']);
} }
downloadContent() { downloadContent(): void {
const fileNames = [];
for (let i = 0; i < this.files.length; i++) {
fileNames.push(this.files[i].path);
}
this.downloading = true; this.downloading = true;
this.postsService.downloadSubFromServer(this.subscription.id).subscribe(res => { this.postsService.downloadSubFromServer(this.subscription.id).subscribe(res => {
this.downloading = false; this.downloading = false;
@@ -164,7 +149,7 @@ export class SubscriptionComponent implements OnInit, OnDestroy {
}); });
} }
editSubscription() { editSubscription(): void {
this.dialog.open(EditSubscriptionDialogComponent, { this.dialog.open(EditSubscriptionDialogComponent, {
data: { data: {
sub: this.postsService.getSubscriptionByID(this.subscription.id) sub: this.postsService.getSubscriptionByID(this.subscription.id)
@@ -172,7 +157,7 @@ export class SubscriptionComponent implements OnInit, OnDestroy {
}); });
} }
watchSubscription() { watchSubscription(): void {
this.router.navigate(['/player', {sub_id: this.subscription.id}]) this.router.navigate(['/player', {sub_id: this.subscription.id}])
} }

File diff suppressed because it is too large Load Diff