diff --git a/Public API v1.yaml b/Public API v1.yaml index 0cf95078..9682671b 100644 --- a/Public API v1.yaml +++ b/Public API v1.yaml @@ -293,6 +293,48 @@ paths: $ref: '#/components/schemas/UnsubscribeResponse' security: - Auth query parameter: [] + /api/checkSubscription: + post: + tags: + - subscriptions + summary: Run a check for videos for a subscription + description: Runs a subscription check + operationId: post-api-checksubscription + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/CheckSubscriptionRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/SuccessObject' + security: + - Auth query parameter: [] + /api/cancelCheckSubscription: + post: + tags: + - subscriptions + summary: Cancels check for videos for a subscription + description: Cancels subscription check + operationId: post-api-checksubscription + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/CheckSubscriptionRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/SuccessObject' + security: + - Auth query parameter: [] /api/deleteSubscriptionFile: post: tags: @@ -1981,11 +2023,11 @@ components: type: string UnsubscribeRequest: required: - - sub + - sub_id type: object properties: - sub: - $ref: '#/components/schemas/SubscriptionRequestData' + sub_id: + type: string deleteMode: type: boolean description: Defaults to false @@ -1998,6 +2040,13 @@ components: type: boolean error: type: string + CheckSubscriptionRequest: + required: + - sub_id + type: object + properties: + sub_id: + type: string DeleteAllFilesResponse: type: object properties: @@ -2843,6 +2892,8 @@ components: nullable: true isPlaylist: type: boolean + child_process: + type: object archive: type: string timerange: @@ -2851,6 +2902,10 @@ components: type: string custom_output: type: string + downloading: + type: boolean + paused: + type: boolean videos: type: array items: diff --git a/backend/app.js b/backend/app.js index eb8c886e..967688f7 100644 --- a/backend/app.js +++ b/backend/app.js @@ -534,7 +534,7 @@ async function loadConfig() { // set downloading to false let subscriptions = await subscriptions_api.getAllSubscriptions(); subscriptions.forEach(async sub => subscriptions_api.writeSubscriptionMetadata(sub)); - subscriptions_api.updateSubscriptionPropertyMultiple(subscriptions, {downloading: false}); + subscriptions_api.updateSubscriptionPropertyMultiple(subscriptions, {downloading: false, child_process: null}); // runs initially, then runs every ${subscriptionCheckInterval} seconds const watchSubscriptionsInterval = function() { watchSubscriptions(); @@ -1196,10 +1196,10 @@ app.post('/api/subscribe', optionalJwt, async (req, res) => { app.post('/api/unsubscribe', optionalJwt, async (req, res) => { let deleteMode = req.body.deleteMode - let sub = req.body.sub; + let sub_id = req.body.sub_id; let user_uid = req.isAuthenticated() ? req.user.uid : null; - let result_obj = subscriptions_api.unsubscribe(sub, deleteMode, user_uid); + let result_obj = subscriptions_api.unsubscribe(sub_id, deleteMode, user_uid); if (result_obj.success) { res.send({ success: result_obj.success @@ -1289,6 +1289,36 @@ app.post('/api/updateSubscription', optionalJwt, async (req, res) => { }); }); +app.post('/api/checkSubscription', optionalJwt, async (req, res) => { + let sub_id = req.body.sub_id; + let user_uid = req.isAuthenticated() ? req.user.uid : null; + + const success = subscriptions_api.getVideosForSub(sub_id, user_uid); + res.send({ + success: success + }); +}); + +app.post('/api/cancelCheckSubscription', optionalJwt, async (req, res) => { + let sub_id = req.body.sub_id; + let user_uid = req.isAuthenticated() ? req.user.uid : null; + + const success = subscriptions_api.cancelCheckSubscription(sub_id, user_uid); + res.send({ + success: success + }); +}); + +app.post('/api/cancelSubscriptionCheck', optionalJwt, async (req, res) => { + let sub_id = req.body.sub_id; + let user_uid = req.isAuthenticated() ? req.user.uid : null; + + const success = subscriptions_api.getVideosForSub(sub_id, user_uid); + res.send({ + success: success + }); +}); + app.post('/api/getSubscriptions', optionalJwt, async (req, res) => { let user_uid = req.isAuthenticated() ? req.user.uid : null; diff --git a/backend/downloader.js b/backend/downloader.js index 4ef9987d..cebc87d8 100644 --- a/backend/downloader.js +++ b/backend/downloader.js @@ -139,7 +139,7 @@ exports.clearDownload = async (download_uid) => { async function handleDownloadError(download_uid, error_message, error_type = null) { if (!download_uid) return; const download = await db_api.getRecord('download_queue', {uid: download_uid}); - if (download['error']) return; + if (!download || download['error']) return; notifications_api.sendDownloadErrorNotification(download, download['user_uid'], error_message, error_type); await db_api.updateRecord('download_queue', {uid: download['uid']}, {error: error_message, finished: true, running: false, error_type: error_type}); } @@ -332,7 +332,7 @@ exports.downloadQueuedFile = async(download_uid, customDownloadHandler = null) = logger.debug(`${type === 'audio' ? 'Audio' : 'Video'} download delay: ${difference} seconds.`); if (!parsed_output) { const errored_download = await db_api.getRecord('download_queue', {uid: download_uid}); - if (errored_download['paused']) return; + if (errored_download && errored_download['paused']) return; logger.error(err.toString()); await handleDownloadError(download_uid, err.toString(), 'unknown_error'); resolve(false); @@ -599,6 +599,7 @@ async function checkDownloadPercent(download_uid) { */ const download = await db_api.getRecord('download_queue', {uid: download_uid}); + if (!download) return; const files_to_check_for_progress = download['files_to_check_for_progress']; const resulting_file_size = download['expected_file_size']; diff --git a/backend/subscriptions.js b/backend/subscriptions.js index fd7de693..cca0c6ca 100644 --- a/backend/subscriptions.js +++ b/backend/subscriptions.js @@ -39,7 +39,7 @@ exports.subscribe = async (sub, user_uid = null, skip_get_info = false) => { exports.writeSubscriptionMetadata(sub); if (success) { - if (!sub.paused) exports.getVideosForSub(sub, user_uid); + if (!sub.paused) exports.getVideosForSub(sub.id); } else { logger.error('Subscribe: Failed to get subscription info. Subscribe failed.') } @@ -96,7 +96,8 @@ async function getSubscriptionInfo(sub) { return false; } -exports.unsubscribe = async (sub, deleteMode, user_uid = null) => { +exports.unsubscribe = async (sub_id, deleteMode, user_uid = null) => { + const sub = await exports.getSubscription(sub_id); let basePath = null; if (user_uid) basePath = path.join(config_api.getConfigItem('ytdl_users_base_path'), user_uid, 'subscriptions'); @@ -119,6 +120,7 @@ exports.unsubscribe = async (sub, deleteMode, user_uid = null) => { } } + await killSubDownloads(sub_id, true); await db_api.removeRecord('subscriptions', {id: id}); await db_api.removeAllRecords('files', {sub_id: id}); @@ -203,12 +205,18 @@ exports.deleteSubscriptionFile = async (sub, file, deleteForever, file_uid = nul } } -exports.getVideosForSub = async (sub, user_uid = null) => { - const latest_sub_obj = await exports.getSubscription(sub.id); - if (!latest_sub_obj || latest_sub_obj['downloading']) { +exports.getVideosForSub = async (sub_id) => { + const sub = await exports.getSubscription(sub_id); + if (!sub || sub['downloading']) { return false; } + _getVideosForSub(sub); + return true; +} + +async function _getVideosForSub(sub) { + const user_uid = sub['user_uid']; updateSubscriptionProperty(sub, {downloading: true}, user_uid); // get basePath @@ -226,7 +234,8 @@ exports.getVideosForSub = async (sub, user_uid = null) => { // get videos logger.verbose(`Subscription: getting list of videos to download for ${sub.name} with args: ${downloadConfig.join(',')}`); - let {callback} = await youtubedl_api.runYoutubeDL(sub.url, downloadConfig); + let {child_process, callback} = await youtubedl_api.runYoutubeDL(sub.url, downloadConfig); + updateSubscriptionProperty(sub, {child_process: child_process}, user_uid); const {parsed_output, err} = await callback; updateSubscriptionProperty(sub, {downloading: false, child_process: null}, user_uid); if (!parsed_output) { @@ -396,8 +405,37 @@ async function getFilesToDownload(sub, output_jsons) { return files_to_download; } +exports.cancelCheckSubscription = async (sub_id) => { + const sub = await exports.getSubscription(sub_id); + if (!sub['downloading'] && !sub['child_process']) { + logger.error('Failed to cancel subscription check, verify that it is still running!'); + return false; + } + + // if check is ongoing + if (sub['child_process']) { + const child_process = sub['child_process']; + youtubedl_api.killYoutubeDLProcess(child_process); + } + + // cancel activate video downloads + await killSubDownloads(sub_id); + + return true; +} + +async function killSubDownloads(sub_id, remove_downloads = false) { + const sub_downloads = await db_api.getRecords('download_queue', {sub_id: sub_id}); + for (const sub_download of sub_downloads) { + if (sub_download['running']) + await downloader_api.cancelDownload(sub_download['uid']); + if (remove_downloads) + await db_api.removeRecord('download_queue', {uid: sub_download['uid']}); + } +} exports.getSubscriptions = async (user_uid = null) => { + // TODO: fix issue where the downloading property may not match getSubscription() return await db_api.getRecords('subscriptions', {user_uid: user_uid}); } diff --git a/src/api-types/index.ts b/src/api-types/index.ts index c749b0b2..04aa3951 100644 --- a/src/api-types/index.ts +++ b/src/api-types/index.ts @@ -14,6 +14,7 @@ export type { ChangeRolePermissionsRequest } from './models/ChangeRolePermission export type { ChangeUserPermissionsRequest } from './models/ChangeUserPermissionsRequest'; export type { CheckConcurrentStreamRequest } from './models/CheckConcurrentStreamRequest'; export type { CheckConcurrentStreamResponse } from './models/CheckConcurrentStreamResponse'; +export type { CheckSubscriptionRequest } from './models/CheckSubscriptionRequest'; export type { ClearDownloadsRequest } from './models/ClearDownloadsRequest'; export type { ConcurrentStream } from './models/ConcurrentStream'; export type { Config } from './models/Config'; diff --git a/src/api-types/models/CheckSubscriptionRequest.ts b/src/api-types/models/CheckSubscriptionRequest.ts new file mode 100644 index 00000000..2d1aaebf --- /dev/null +++ b/src/api-types/models/CheckSubscriptionRequest.ts @@ -0,0 +1,7 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +export type CheckSubscriptionRequest = { + sub_id: string; +}; diff --git a/src/api-types/models/Subscription.ts b/src/api-types/models/Subscription.ts index 9d84e2c3..e54947ff 100644 --- a/src/api-types/models/Subscription.ts +++ b/src/api-types/models/Subscription.ts @@ -11,9 +11,12 @@ export type Subscription = { type: FileType; user_uid: string | null; isPlaylist: boolean; + child_process?: any; archive?: string; timerange?: string; custom_args?: string; custom_output?: string; + downloading?: boolean; + paused?: boolean; videos: Array; }; diff --git a/src/api-types/models/UnsubscribeRequest.ts b/src/api-types/models/UnsubscribeRequest.ts index 9eed3c52..09692c1e 100644 --- a/src/api-types/models/UnsubscribeRequest.ts +++ b/src/api-types/models/UnsubscribeRequest.ts @@ -2,10 +2,8 @@ /* tslint:disable */ /* eslint-disable */ -import type { SubscriptionRequestData } from './SubscriptionRequestData'; - export type UnsubscribeRequest = { - sub: SubscriptionRequestData; + sub_id: string; /** * Defaults to false */ diff --git a/src/app/dialogs/subscription-info-dialog/subscription-info-dialog.component.ts b/src/app/dialogs/subscription-info-dialog/subscription-info-dialog.component.ts index 5e14fc38..0a7783ef 100644 --- a/src/app/dialogs/subscription-info-dialog/subscription-info-dialog.component.ts +++ b/src/app/dialogs/subscription-info-dialog/subscription-info-dialog.component.ts @@ -2,6 +2,7 @@ import { Component, OnInit, Inject } from '@angular/core'; import { MatDialog, MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; import { PostsService } from 'app/posts.services'; import { ConfirmDialogComponent } from '../confirm-dialog/confirm-dialog.component'; +import { Subscription } from 'api-types'; @Component({ selector: 'app-subscription-info-dialog', @@ -10,7 +11,7 @@ import { ConfirmDialogComponent } from '../confirm-dialog/confirm-dialog.compone }) export class SubscriptionInfoDialogComponent implements OnInit { - sub = null; + sub: Subscription = null; unsubbedEmitter = null; constructor(public dialogRef: MatDialogRef, @@ -41,7 +42,7 @@ export class SubscriptionInfoDialogComponent implements OnInit { } unsubscribe() { - this.postsService.unsubscribe(this.sub, true).subscribe(res => { + this.postsService.unsubscribe(this.sub.id, true).subscribe(res => { this.unsubbedEmitter.emit(true); this.dialogRef.close(); }); diff --git a/src/app/posts.services.ts b/src/app/posts.services.ts index 67f86e37..e1a4af14 100644 --- a/src/app/posts.services.ts +++ b/src/app/posts.services.ts @@ -113,7 +113,8 @@ import { Archive, Subscription, RestartDownloadResponse, - TaskType + TaskType, + CheckSubscriptionRequest } from '../api-types'; import { isoLangs } from './dialogs/user-profile-dialog/locales_list'; import { Title } from '@angular/platform-browser'; @@ -566,8 +567,18 @@ export class PostsService implements CanActivate { return this.http.post(this.path + 'updateSubscription', {subscription: subscription}, this.httpOptions); } - unsubscribe(sub: SubscriptionRequestData, deleteMode = false) { - const body: UnsubscribeRequest = {sub: sub, deleteMode: deleteMode}; + checkSubscription(sub_id: string) { + const body: CheckSubscriptionRequest = {sub_id: sub_id}; + return this.http.post(this.path + 'checkSubscription', body, this.httpOptions); + } + + cancelCheckSubscription(sub_id: string) { + const body: CheckSubscriptionRequest = {sub_id: sub_id}; + return this.http.post(this.path + 'cancelCheckSubscription', body, this.httpOptions); + } + + unsubscribe(sub_id: string, deleteMode = false) { + const body: UnsubscribeRequest = {sub_id: sub_id, deleteMode: deleteMode}; return this.http.post(this.path + 'unsubscribe', body, this.httpOptions) } diff --git a/src/app/subscription/subscription/subscription.component.html b/src/app/subscription/subscription/subscription.component.html index 661279ab..93845455 100644 --- a/src/app/subscription/subscription/subscription.component.html +++ b/src/app/subscription/subscription/subscription.component.html @@ -3,6 +3,7 @@

{{subscription.name}} (Paused) +

@@ -13,7 +14,14 @@
- +
+ + + + + + +
\ No newline at end of file diff --git a/src/app/subscription/subscription/subscription.component.scss b/src/app/subscription/subscription/subscription.component.scss index d82a7066..7dd757e5 100644 --- a/src/app/subscription/subscription/subscription.component.scss +++ b/src/app/subscription/subscription/subscription.component.scss @@ -58,13 +58,19 @@ bottom: 25px; } -.edit-button { +.check-button { left: 25px; position: fixed; bottom: 25px; z-index: 99999; } +.edit-button { + right: 35px; + position: fixed; + z-index: 99999; +} + .save-icon { bottom: 1px; position: relative; diff --git a/src/app/subscription/subscription/subscription.component.ts b/src/app/subscription/subscription/subscription.component.ts index 4c73efcf..8b98cea1 100644 --- a/src/app/subscription/subscription/subscription.component.ts +++ b/src/app/subscription/subscription/subscription.component.ts @@ -3,6 +3,7 @@ import { PostsService } from 'app/posts.services'; import { ActivatedRoute, Router, ParamMap } from '@angular/router'; import { MatDialog } from '@angular/material/dialog'; import { EditSubscriptionDialogComponent } from 'app/dialogs/edit-subscription-dialog/edit-subscription-dialog.component'; +import { Subscription } from 'api-types'; @Component({ selector: 'app-subscription', @@ -12,11 +13,13 @@ import { EditSubscriptionDialogComponent } from 'app/dialogs/edit-subscription-d export class SubscriptionComponent implements OnInit, OnDestroy { id = null; - subscription = null; + subscription: Subscription = null; use_youtubedl_archive = false; descendingMode = true; downloading = false; sub_interval = null; + check_clicked = false; + cancel_clicked = false; constructor(private postsService: PostsService, private route: ActivatedRoute, private router: Router, private dialog: MatDialog) { } @@ -90,4 +93,34 @@ export class SubscriptionComponent implements OnInit, OnDestroy { this.router.navigate(['/player', {sub_id: this.subscription.id}]) } + checkSubscription(): void { + this.check_clicked = true; + this.postsService.checkSubscription(this.subscription.id).subscribe(res => { + this.check_clicked = false; + if (!res['success']) { + this.postsService.openSnackBar('Failed to check subscription!'); + return; + } + }, err => { + console.error(err); + this.check_clicked = false; + this.postsService.openSnackBar('Failed to check subscription!'); + }); + } + + cancelCheckSubscription(): void { + this.cancel_clicked = true; + this.postsService.cancelCheckSubscription(this.subscription.id).subscribe(res => { + this.cancel_clicked = false; + if (!res['success']) { + this.postsService.openSnackBar('Failed to cancel check subscription!'); + return; + } + }, err => { + console.error(err); + this.cancel_clicked = false; + this.postsService.openSnackBar('Failed to cancel check subscription!'); + }); + } + }