From 188ebb6fd9c51f49bd4f877ddc3e9f0bfc71c171 Mon Sep 17 00:00:00 2001 From: 4gray Date: Tue, 10 Mar 2026 08:52:38 +0100 Subject: [PATCH] refactor: enhance error handling for playlist updates and add missing i18n strings --- apps/web/src/app/services/electron.service.ts | 100 ++++++++++++++---- apps/web/src/app/services/pwa.service.ts | 87 ++++++++++----- apps/web/src/assets/i18n/en.json | 2 + 3 files changed, 144 insertions(+), 45 deletions(-) diff --git a/apps/web/src/app/services/electron.service.ts b/apps/web/src/app/services/electron.service.ts index 4b63961ea..cadc61797 100644 --- a/apps/web/src/app/services/electron.service.ts +++ b/apps/web/src/app/services/electron.service.ts @@ -313,27 +313,26 @@ export class ElectronService extends DataService { filePath?: string; title: string; }) { - let methodToCall = null; - if (data.url && !data.filePath) { - // fetch from url - methodToCall = window.electron.fetchPlaylistByUrl( - data.url, - data.title - ); - } else if (data.filePath && !data.url) { - // update from file path - methodToCall = window.electron.updatePlaylistFromFilePath( - data.filePath, - data.title - ); - } else { - console.error( - 'Either url or filePath must be provided, but not both.' - ); - return; - } + try { + let playlistObject: Playlist; + if (data.url && !data.filePath) { + playlistObject = await window.electron.fetchPlaylistByUrl( + data.url, + data.title + ); + } else if (data.filePath && !data.url) { + playlistObject = + await window.electron.updatePlaylistFromFilePath( + data.filePath, + data.title + ); + } else { + console.error( + 'Either url or filePath must be provided, but not both.' + ); + return; + } - methodToCall.then((playlistObject) => { this.store.dispatch( PlaylistActions.updatePlaylist({ playlist: { @@ -351,7 +350,66 @@ export class ElectronService extends DataService { null, { duration: 2000 } ); - }); + } catch (error: unknown) { + console.error('Playlist refresh error:', error); + this.snackBar.open( + this.getPlaylistRefreshErrorMessage(error, data), + this.translateService.instant('CLOSE'), + { duration: 5000 } + ); + } + } + + private getPlaylistRefreshErrorMessage( + error: unknown, + data: { url?: string; filePath?: string } + ): string { + if (data.filePath) { + const errorMessage = String( + this.getErrorDetails(error)?.message ?? error ?? '' + ); + + if ( + /(ENOENT|no such file or directory|not found)/i.test( + errorMessage + ) + ) { + return this.translateWithFallback( + 'HOME.PLAYLISTS.PLAYLIST_UPDATE_FILE_NOT_FOUND', + 'Playlist refresh failed. The local file is no longer available. Check the file path or re-import the playlist.' + ); + } + + if (/(EACCES|EPERM|permission denied)/i.test(errorMessage)) { + return this.translateWithFallback( + 'HOME.PLAYLISTS.PLAYLIST_UPDATE_FILE_ACCESS_ERROR', + 'Playlist refresh failed. The app can no longer access the local file.' + ); + } + + return this.translateService.instant( + 'HOME.PLAYLISTS.PLAYLIST_UPDATE_ERROR' + ); + } + + const statusCode = this.extractHttpStatusCode(error); + if (statusCode === 404) { + return this.translateService.instant('HOME.URL_UPLOAD.ERROR_404'); + } + if (statusCode === 403) { + return this.translateService.instant('HOME.URL_UPLOAD.ERROR_403'); + } + if (statusCode === 401) { + return this.translateService.instant('HOME.URL_UPLOAD.ERROR_401'); + } + return this.translateService.instant( + 'HOME.URL_UPLOAD.ERROR_FETCH_FAILED' + ); + } + + private translateWithFallback(key: string, fallback: string): string { + const translated = this.translateService.instant(key); + return translated === key ? fallback : translated; } /* private getErrorMessageByStatusCode(status: number) { diff --git a/apps/web/src/app/services/pwa.service.ts b/apps/web/src/app/services/pwa.service.ts index a7cc74ceb..3d090343e 100644 --- a/apps/web/src/app/services/pwa.service.ts +++ b/apps/web/src/app/services/pwa.service.ts @@ -131,10 +131,8 @@ export class PwaService extends DataService { .pipe( catchError((error) => { this.snackBar.open( - `Error: ${error.message ?? 'Unknown error'}, status: ${ - error.status ?? 500 - }`, - 'Close', + this.getPlaylistRefreshErrorMessage(error), + this.translateService.instant('CLOSE'), { duration: 5000, } @@ -160,6 +158,25 @@ export class PwaService extends DataService { }); } + private getPlaylistRefreshErrorMessage(error: unknown): string { + const statusCode = + this.getErrorDetails(error)?.status ?? + this.extractHttpStatusCode(error); + + if (statusCode === 404) { + return this.translateService.instant('HOME.URL_UPLOAD.ERROR_404'); + } + if (statusCode === 403) { + return this.translateService.instant('HOME.URL_UPLOAD.ERROR_403'); + } + if (statusCode === 401) { + return this.translateService.instant('HOME.URL_UPLOAD.ERROR_401'); + } + return this.translateService.instant( + 'HOME.URL_UPLOAD.ERROR_FETCH_FAILED' + ); + } + /** * Fetches playlist from the specified url * @param payload playlist payload @@ -189,19 +206,38 @@ export class PwaService extends DataService { } getErrorMessageByStatusCode(status: number) { - let message = 'Something went wrong'; + let messageKey = 'HOME.URL_UPLOAD.ERROR_FETCH_FAILED'; switch (status) { - case 0: - message = 'The backend is not reachable'; - break; case 413: - message = - 'This file is too big. Use standalone or self-hosted version of the app.'; + return 'This file is too big. Use standalone or self-hosted version of the app.'; + case 403: + messageKey = 'HOME.URL_UPLOAD.ERROR_403'; + break; + case 404: + messageKey = 'HOME.URL_UPLOAD.ERROR_404'; + break; + case 401: + messageKey = 'HOME.URL_UPLOAD.ERROR_401'; break; default: break; } - return message; + return this.translateService.instant(messageKey); + } + + private extractHttpStatusCode(error: unknown): number | null { + if ( + error && + typeof error === 'object' && + 'status' in error && + typeof error.status === 'number' + ) { + return error.status; + } + + const msg = String((error as { message?: string })?.message ?? error); + const match = msg.match(/status code (\d{3})/); + return match ? parseInt(match[1], 10) : null; } async forwardXtreamRequest(payload: { @@ -242,21 +278,23 @@ export class PwaService extends DataService { try { let result: PwaErrorResult | PwaXtreamResult; const response = (await firstValueFrom( - this.http.get(`${this.corsProxyUrl}/xtream`, { - params: { - url: payload.url, - ...payload.params, - }, - ...headers, - }) + this.http.get( + `${this.corsProxyUrl}/xtream`, + { + params: { + url: payload.url, + ...payload.params, + }, + ...headers, + } + ) )) as PwaXtreamResponse; if (!response.payload) { const action = payload.params.action; const isSilentAction = this.silentXtreamActions.has(action); - const normalizedMessage = this.getReadableXtreamErrorMessage( - response - ); + const normalizedMessage = + this.getReadableXtreamErrorMessage(response); logPortalDebugEvent( createPortalDebugErrorEvent(context, response) ); @@ -347,9 +385,10 @@ export class PwaService extends DataService { if ( maybeError.error && typeof maybeError.error === 'object' && - 'message' in (maybeError.error as Record) && - typeof (maybeError.error as Record).message === - 'string' + 'message' in + (maybeError.error as Record) && + typeof (maybeError.error as Record) + .message === 'string' ) { return (maybeError.error as Record).message; } diff --git a/apps/web/src/assets/i18n/en.json b/apps/web/src/assets/i18n/en.json index b4ef5566c..b2c558f8a 100644 --- a/apps/web/src/assets/i18n/en.json +++ b/apps/web/src/assets/i18n/en.json @@ -77,6 +77,8 @@ "MY_PLAYLISTS_SUBTITLE": "all available playlists", "PLAYLIST_UPDATE_SUCCESS": "Success! The playlist was successfully updated.", "PLAYLIST_UPDATE_ERROR": "Error updating playlist details.", + "PLAYLIST_UPDATE_FILE_NOT_FOUND": "Playlist refresh failed. The local file is no longer available. Check the file path or re-import the playlist.", + "PLAYLIST_UPDATE_FILE_ACCESS_ERROR": "Playlist refresh failed. The app can no longer access the local file.", "AUTO_REFRESH_UPDATE_SUCCESS": "Success! The playlists were successfully updated", "AUTO_REFRESH_ENABLED": "Auto-refresh enabled" },