From d9f828c717b4c65b59579baa7e6bdcd6dc2d53f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rasmus=20Kr=C3=A4mer?= Date: Sun, 5 May 2024 13:14:30 +0200 Subject: [PATCH 01/32] Added item sessions endpoint --- server/controllers/MeController.js | 41 +++++++++++++++++++++++++++--- server/routers/ApiRouter.js | 13 +++++++--- 2 files changed, 47 insertions(+), 7 deletions(-) diff --git a/server/controllers/MeController.js b/server/controllers/MeController.js index b5147fb7d..d14c07e35 100644 --- a/server/controllers/MeController.js +++ b/server/controllers/MeController.js @@ -33,6 +33,39 @@ class MeController { res.json(payload) } + // GET: api/me/item/listening-sessions/:libraryItemId/:episodeId + async getListeningSessions(req, res) { + const libraryItem = await Database.libraryItemModel.getOldById(req.params.libraryItemId) + const episode = (req.params.episodeId && libraryItem && libraryItem.isPodcast) ? libraryItem.media.getEpisode(req.params.episodeId) : null + + if (!libraryItem || (libraryItem.isPodcast && !episode)) { + Logger.error(`[PlaybackSessionManager] listening-sessions: Media item not found for library item id "${req.params.id}"`) + return { + success: false, + error: 'Media item not found' + } + } + + const mediaItemId = episode ? episode.id : libraryItem.media.id + let listeningSessions = await this.getUserItemListeningSessionsHelper(req.user.id, mediaItemId) + + const itemsPerPage = toNumber(req.query.itemsPerPage, 10) || 10 + const page = toNumber(req.query.page, 0) + + const start = page * itemsPerPage + const sessions = listeningSessions.slice(start, start + itemsPerPage) + + const payload = { + total: listeningSessions.length, + numPages: Math.ceil(listeningSessions.length / itemsPerPage), + page, + itemsPerPage, + sessions + } + + res.json(payload) + } + // GET: api/me/listening-stats async getListeningStats(req, res) { const listeningStats = await this.getUserListeningStatsHelpers(req.user.id) @@ -337,9 +370,9 @@ class MeController { /** * GET: /api/me/stats/year/:year - * - * @param {import('express').Request} req - * @param {import('express').Response} res + * + * @param {import('express').Request} req + * @param {import('express').Response} res */ async getStatsForYear(req, res) { const year = Number(req.params.year) @@ -351,4 +384,4 @@ class MeController { res.json(data) } } -module.exports = new MeController() \ No newline at end of file +module.exports = new MeController() diff --git a/server/routers/ApiRouter.js b/server/routers/ApiRouter.js index 43e7c907e..fc5d6a294 100644 --- a/server/routers/ApiRouter.js +++ b/server/routers/ApiRouter.js @@ -166,6 +166,8 @@ class ApiRouter { // this.router.get('/me', MeController.getCurrentUser.bind(this)) this.router.get('/me/listening-sessions', MeController.getListeningSessions.bind(this)) + this.router.get('/me/item/listening-sessions/:libraryItemId', MeController.getListeningSessions.bind(this)) + this.router.get('/me/item/listening-sessions/:libraryItemId/:episodeId', MeController.getListeningSessions.bind(this)) this.router.get('/me/listening-stats', MeController.getListeningStats.bind(this)) this.router.get('/me/progress/:id/remove-from-continue-listening', MeController.removeItemFromContinueListening.bind(this)) this.router.get('/me/progress/:id/:episodeId?', MeController.getMediaProgress.bind(this)) @@ -425,9 +427,9 @@ class ApiRouter { /** * Used when a series is removed from a book * Series is removed if it only has 1 book - * + * * @param {string} bookId - * @param {string[]} seriesIds + * @param {string[]} seriesIds */ async checkRemoveEmptySeries(bookId, seriesIds) { if (!seriesIds?.length) return @@ -455,7 +457,7 @@ class ApiRouter { /** * Remove an empty series & close an open RSS feed - * @param {import('../models/Series')} series + * @param {import('../models/Series')} series */ async removeEmptySeries(series) { await this.rssFeedManager.closeFeedForEntityId(series.id) @@ -474,6 +476,11 @@ class ApiRouter { return userSessions.sort((a, b) => b.updatedAt - a.updatedAt) } + async getUserItemListeningSessionsHelper(userId, mediaItemId) { + const userSessions = await Database.getPlaybackSessions({ userId, mediaItemId }) + return userSessions.sort((a, b) => b.updatedAt - a.updatedAt) + } + async getUserListeningStatsHelpers(userId) { const today = date.format(new Date(), 'YYYY-MM-DD') From 09e26a9e564e4540bff44633a31a82d048937c4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rasmus=20Kr=C3=A4mer?= Date: Tue, 14 May 2024 10:51:50 +0200 Subject: [PATCH 02/32] Use new database models, fix function name and use optional path parameter --- server/controllers/MeController.js | 8 ++++---- server/routers/ApiRouter.js | 3 +-- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/server/controllers/MeController.js b/server/controllers/MeController.js index d14c07e35..735728cab 100644 --- a/server/controllers/MeController.js +++ b/server/controllers/MeController.js @@ -34,11 +34,11 @@ class MeController { } // GET: api/me/item/listening-sessions/:libraryItemId/:episodeId - async getListeningSessions(req, res) { - const libraryItem = await Database.libraryItemModel.getOldById(req.params.libraryItemId) - const episode = (req.params.episodeId && libraryItem && libraryItem.isPodcast) ? libraryItem.media.getEpisode(req.params.episodeId) : null + async getItemListeningSessions(req, res) { + const libraryItem = await Database.libraryItemModel.findByPk(req.params.libraryItemId) + const episode = await Database.podcastEpisodeModel.findByPk(req.params.episodeId) - if (!libraryItem || (libraryItem.isPodcast && !episode)) { + if (!libraryItem || (libraryItem.mediaType === "podcast" && !episode)) { Logger.error(`[PlaybackSessionManager] listening-sessions: Media item not found for library item id "${req.params.id}"`) return { success: false, diff --git a/server/routers/ApiRouter.js b/server/routers/ApiRouter.js index fc5d6a294..2b7adacc4 100644 --- a/server/routers/ApiRouter.js +++ b/server/routers/ApiRouter.js @@ -166,8 +166,7 @@ class ApiRouter { // this.router.get('/me', MeController.getCurrentUser.bind(this)) this.router.get('/me/listening-sessions', MeController.getListeningSessions.bind(this)) - this.router.get('/me/item/listening-sessions/:libraryItemId', MeController.getListeningSessions.bind(this)) - this.router.get('/me/item/listening-sessions/:libraryItemId/:episodeId', MeController.getListeningSessions.bind(this)) + this.router.get('/me/item/listening-sessions/:libraryItemId/:episodeId?', MeController.getItemListeningSessions.bind(this)) this.router.get('/me/listening-stats', MeController.getListeningStats.bind(this)) this.router.get('/me/progress/:id/remove-from-continue-listening', MeController.removeItemFromContinueListening.bind(this)) this.router.get('/me/progress/:id/:episodeId?', MeController.getMediaProgress.bind(this)) From 811fd9018a4940436f25ed950c332cf2c7f4d11c Mon Sep 17 00:00:00 2001 From: Machou Date: Sat, 18 May 2024 22:15:46 +0200 Subject: [PATCH 03/32] Update fr.json --- client/strings/fr.json | 80 +++++++++++++++++++++--------------------- 1 file changed, 40 insertions(+), 40 deletions(-) diff --git a/client/strings/fr.json b/client/strings/fr.json index ce910b52f..0ae8363fd 100644 --- a/client/strings/fr.json +++ b/client/strings/fr.json @@ -61,8 +61,8 @@ "ButtonQueueRemoveItem": "Supprimer de la liste de lecture", "ButtonQuickMatch": "Recherche rapide", "ButtonRead": "Lire", - "ButtonReadLess": "Read less", - "ButtonReadMore": "Read more", + "ButtonReadLess": "Lire moins", + "ButtonReadMore": "Lire la suite", "ButtonRefresh": "Rafraîchir", "ButtonRemove": "Supprimer", "ButtonRemoveAll": "Supprimer tout", @@ -186,11 +186,11 @@ "HeaderUpdateDetails": "Mettre à jour les détails", "HeaderUpdateLibrary": "Mettre à jour la bibliothèque", "HeaderUsers": "Utilisateurs", - "HeaderYearReview": "Year {0} in Review", + "HeaderYearReview": "Bilan de l’année {0}", "HeaderYourStats": "Vos statistiques", "LabelAbridged": "Version courte", - "LabelAbridgedChecked": "Abridged (checked)", - "LabelAbridgedUnchecked": "Unabridged (unchecked)", + "LabelAbridgedChecked": "Abrégé (vérifié)", + "LabelAbridgedUnchecked": "Intégral (non vérifié)", "LabelAccountType": "Type de compte", "LabelAccountTypeAdmin": "Admin", "LabelAccountTypeGuest": "Invité", @@ -217,7 +217,7 @@ "LabelAutoFetchMetadata": "Recherche automatique de métadonnées", "LabelAutoFetchMetadataHelp": "Récupère les métadonnées du titre, de l’auteur et de la série pour simplifier le téléchargement. Il se peut que des métadonnées supplémentaires doivent être ajoutées après le téléchargement.", "LabelAutoLaunch": "Lancement automatique", - "LabelAutoLaunchDescription": "Redirection automatique vers le fournisseur d'authentification lors de la navigation vers la page de connexion (chemin de remplacement manuel /login?autoLaunch=0)", + "LabelAutoLaunchDescription": "Redirection automatique vers le fournisseur d’authentification lors de la navigation vers la page de connexion (chemin de remplacement manuel /login?autoLaunch=0)", "LabelAutoRegister": "Enregistrement automatique", "LabelAutoRegisterDescription": "Créer automatiquement de nouveaux utilisateurs après la connexion", "LabelBackToUser": "Retour à l’utilisateur", @@ -231,7 +231,7 @@ "LabelBitrate": "Bitrate", "LabelBooks": "Livres", "LabelButtonText": "Texte du bouton", - "LabelByAuthor": "by {0}", + "LabelByAuthor": "par {0}", "LabelChangePassword": "Modifier le mot de passe", "LabelChannels": "Canaux", "LabelChapters": "Chapitres", @@ -269,9 +269,9 @@ "LabelDownload": "Téléchargement", "LabelDownloadNEpisodes": "Télécharger {0} épisode(s)", "LabelDuration": "Durée", - "LabelDurationComparisonExactMatch": "(exact match)", - "LabelDurationComparisonLonger": "({0} longer)", - "LabelDurationComparisonShorter": "({0} shorter)", + "LabelDurationComparisonExactMatch": "(correspondance exacte)", + "LabelDurationComparisonLonger": "({0} plus long)", + "LabelDurationComparisonShorter": "({0} plus court)", "LabelDurationFound": "Durée trouvée :", "LabelEbook": "Livre numérique", "LabelEbooks": "Livres numériques", @@ -289,8 +289,8 @@ "LabelEpisodeType": "Type de l’épisode", "LabelExample": "Exemple", "LabelExplicit": "Restriction", - "LabelExplicitChecked": "Explicit (checked)", - "LabelExplicitUnchecked": "Not Explicit (unchecked)", + "LabelExplicitChecked": "Explicite (vérifié)", + "LabelExplicitUnchecked": "Non explicite (non vérifié)", "LabelFeedURL": "URL du flux", "LabelFetchingMetadata": "Récupération des métadonnées", "LabelFile": "Fichier", @@ -365,10 +365,10 @@ "LabelMetaTags": "Balises de métadonnée", "LabelMinute": "Minute", "LabelMissing": "Manquant", - "LabelMissingEbook": "Has no ebook", - "LabelMissingSupplementaryEbook": "Has no supplementary ebook", + "LabelMissingEbook": "Ne possède pas de livre numérique", + "LabelMissingSupplementaryEbook": "Ne possède pas de livre numérique supplémentaire", "LabelMobileRedirectURIs": "URI de redirection mobile autorisés", - "LabelMobileRedirectURIsDescription": "Il s'agit d'une liste blanche d’URI de redirection valides pour les applications mobiles. Celui par défaut est audiobookshelf://oauth, que vous pouvez supprimer ou compléter avec des URIs supplémentaires pour l'intégration d'applications tierces. L’utilisation d’un astérisque (*) comme seule entrée autorise n’importe quel URI.", + "LabelMobileRedirectURIsDescription": "Il s’agit d’une liste blanche d’URI de redirection valides pour les applications mobiles. Celui par défaut est audiobookshelf://oauth, que vous pouvez supprimer ou compléter avec des URIs supplémentaires pour l’intégration d’applications tierces. L’utilisation d’un astérisque (*) comme seule entrée autorise n’importe quel URI.", "LabelMore": "Plus", "LabelMoreInfo": "Plus d’informations", "LabelName": "Nom", @@ -395,9 +395,9 @@ "LabelNotStarted": "Pas commencé", "LabelNumberOfBooks": "Nombre de livres", "LabelNumberOfEpisodes": "Nombre d’épisodes", - "LabelOpenIDAdvancedPermsClaimDescription": "Name of the OpenID claim that contains advanced permissions for user actions within the application which will apply to non-admin roles (if configured). If the claim is missing from the response, access to ABS will be denied. If a single option is missing, it will be treated as false. Ensure the identity provider's claim matches the expected structure:", - "LabelOpenIDClaims": "Leave the following options empty to disable advanced group and permissions assignment, automatically assigning 'User' group then.", - "LabelOpenIDGroupClaimDescription": "Name of the OpenID claim that contains a list of the user's groups. Commonly referred to as groups. If configured, the application will automatically assign roles based on the user's group memberships, provided that these groups are named case-insensitively 'admin', 'user', or 'guest' in the claim. The claim should contain a list, and if a user belongs to multiple groups, the application will assign the role corresponding to the highest level of access. If no group matches, access will be denied.", + "LabelOpenIDAdvancedPermsClaimDescription": "Nom de la demande OpenID qui contient des autorisations avancées pour les actions de l’utilisateur dans l’application, qui s’appliqueront à des rôles autres que celui d’administrateur (s’il est configuré). Si la demande est absente de la réponse, l’accès à ABS sera refusé. Si une seule option est manquante, elle sera considérée comme false. Assurez-vous que la demande du fournisseur d’identité correspond à la structure attendue :", + "LabelOpenIDClaims": "Laissez les options suivantes vides pour désactiver l’attribution avancée de groupes et d’autorisations, en attribuant alors automatiquement le groupe « Utilisateur ».", + "LabelOpenIDGroupClaimDescription": "Nom de la demande OpenID qui contient une liste des groupes de l’utilisateur. Communément appelé groups. Si elle est configurée, l’application attribuera automatiquement des rôles en fonction de l’appartenance de l’utilisateur à un groupe, à condition que ces groupes soient nommés -sensible à la casse- tel que « admin », « user » ou « guest » dans la demande. Elle doit contenir une liste, et si un utilisateur appartient à plusieurs groupes, l’application attribuera le rôle correspondant au niveau d’accès le plus élevé. Si aucun groupe ne correspond, l’accès sera refusé.", "LabelOpenRSSFeed": "Ouvrir le flux RSS", "LabelOverwrite": "Écraser", "LabelPassword": "Mot de passe", @@ -447,7 +447,7 @@ "LabelSearchTitle": "Titre de recherche", "LabelSearchTitleOrASIN": "Recherche du titre ou ASIN", "LabelSeason": "Saison", - "LabelSelectAll": "Select all", + "LabelSelectAll": "Tout sélectionner", "LabelSelectAllEpisodes": "Sélectionner tous les épisodes", "LabelSelectEpisodesShowing": "Sélectionner {0} episode(s) en cours", "LabelSelectUsers": "Sélectionner les utilisateurs", @@ -460,7 +460,7 @@ "LabelSetEbookAsPrimary": "Définir comme principale", "LabelSetEbookAsSupplementary": "Définir comme supplémentaire", "LabelSettingsAudiobooksOnly": "Livres audios seulement", - "LabelSettingsAudiobooksOnlyHelp": "L'activation de ce paramètre ignorera les fichiers de type « livre numériques », sauf s'ils se trouvent dans un dossier spécifique , auquel cas ils seront définis comme des livres numériques supplémentaires.", + "LabelSettingsAudiobooksOnlyHelp": "L’activation de ce paramètre ignorera les fichiers de type « livre numériques », sauf s’ils se trouvent dans un dossier spécifique , auquel cas ils seront définis comme des livres numériques supplémentaires.", "LabelSettingsBookshelfViewHelp": "Interface skeuomorphique avec une étagère en bois", "LabelSettingsChromecastSupport": "Support du Chromecast", "LabelSettingsDateFormat": "Format de date", @@ -478,10 +478,10 @@ "LabelSettingsHideSingleBookSeriesHelp": "Les séries qui ne comportent qu’un seul livre seront masquées sur la page de la série et sur les étagères de la page d’accueil.", "LabelSettingsHomePageBookshelfView": "La page d’accueil utilise la vue étagère", "LabelSettingsLibraryBookshelfView": "La bibliothèque utilise la vue étagère", - "LabelSettingsOnlyShowLaterBooksInContinueSeries": "Skip earlier books in Continue Series", - "LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "The Continue Series home page shelf shows the first book not started in series that have at least one book finished and no books in progress. Enabling this setting will continue series from the furthest completed book instead of the first book not started.", + "LabelSettingsOnlyShowLaterBooksInContinueSeries": "Sauter les livres précédents dans « Continuer la série »", + "LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "L’étagère de la page d’accueil « Continuer la série » affiche le premier livre non commencé dans les séries dont au moins un livre est terminé et aucun livre n’est en cours. L’activation de ce paramètre permet de poursuivre la série à partir du dernier livre terminé au lieu du premier livre non commencé.", "LabelSettingsParseSubtitles": "Analyser les sous-titres", - "LabelSettingsParseSubtitlesHelp": "Extrait les sous-titres depuis le dossier du livre audio.
Les sous-titres doivent être séparés par « - »
c’est-à-dire : « Titre du livre - Ceci est un sous-titre » aura le sous-titre « Ceci est un sous-titre »", + "LabelSettingsParseSubtitlesHelp": "Extrait les sous-titres depuis le dossier du livre audio.
Les sous-titres doivent être séparés par des « - »
c’est-à-dire : « Titre du livre - Ceci est un sous-titre » aura le sous-titre « Ceci est un sous-titre »", "LabelSettingsPreferMatchedMetadata": "Préférer les métadonnées par correspondance", "LabelSettingsPreferMatchedMetadataHelp": "Les métadonnées par correspondance écrase les détails de l’article lors d’une recherche par correspondance rapide. Par défaut, la recherche par correspondance rapide ne comblera que les éléments manquant.", "LabelSettingsSkipMatchingBooksWithASIN": "Ignorer la recherche par correspondance sur les livres ayant déjà un ASIN", @@ -496,7 +496,7 @@ "LabelSettingsStoreMetadataWithItemHelp": "Par défaut, les métadonnées sont enregistrées dans /metadata/items", "LabelSettingsTimeFormat": "Format d’heure", "LabelShowAll": "Tout afficher", - "LabelShowSeconds": "Show seconds", + "LabelShowSeconds": "Afficher le seondes", "LabelSize": "Taille", "LabelSleepTimer": "Minuterie", "LabelSlug": "Balise", @@ -517,7 +517,7 @@ "LabelStatsMinutes": "minutes", "LabelStatsMinutesListening": "Minutes d’écoute", "LabelStatsOverallDays": "Nombre total de jours", - "LabelStatsOverallHours": "Nombre total d'heures", + "LabelStatsOverallHours": "Nombre total d’heures", "LabelStatsWeekListening": "Écoute de la semaine", "LabelSubtitle": "Sous-titre", "LabelSupportedFileTypes": "Types de fichiers supportés", @@ -604,12 +604,12 @@ "MessageConfirmDeleteLibraryItems": "Cette opération supprimera {0} éléments de la base de données et de votre système de fichiers. Êtes-vous sûr ?", "MessageConfirmDeleteSession": "Êtes-vous sûr de vouloir supprimer cette session ?", "MessageConfirmForceReScan": "Êtes-vous sûr de vouloir lancer une analyse forcée ?", - "MessageConfirmMarkAllEpisodesFinished": "Êtes-vous sûr de marquer tous les épisodes comme terminés ?", + "MessageConfirmMarkAllEpisodesFinished": "Êtes-vous sûr de marquer tous les épisodes comme terminés ?", "MessageConfirmMarkAllEpisodesNotFinished": "Êtes-vous sûr de vouloir marquer tous les épisodes comme non terminés ?", "MessageConfirmMarkSeriesFinished": "Êtes-vous sûr de vouloir marquer tous les livres de cette série comme terminées ?", "MessageConfirmMarkSeriesNotFinished": "Êtes-vous sûr de vouloir marquer tous les livres de cette série comme comme non terminés ?", - "MessageConfirmPurgeCache": "Purge cache will delete the entire directory at /metadata/cache.

Are you sure you want to remove the cache directory?", - "MessageConfirmQuickEmbed": "Attention ! L’intégration rapide ne sauvegardera pas vos fichiers audio. Assurez-vous d’avoir effectuer une sauvegarde de vos fichiers audio.

Souhaitez-vous continuer ?", + "MessageConfirmPurgeCache": "La purge du cache supprimera l’intégralité du répertoire à /metadata/cache.

Êtes-vous sûr de vouloir supprimer le répertoire de cache ?", + "MessageConfirmQuickEmbed": "Attention ! L’intégration rapide ne sauvegardera pas vos fichiers audio. Assurez-vous d’avoir effectuer une sauvegarde de vos fichiers audio.

Souhaitez-vous continuer ?", "MessageConfirmRemoveAllChapters": "Êtes-vous sûr de vouloir supprimer tous les chapitres ?", "MessageConfirmRemoveAuthor": "Are you sure you want to remove author \"{0}\"?", "MessageConfirmRemoveCollection": "Êtes-vous sûr de vouloir supprimer la collection « {0} » ?", @@ -641,7 +641,7 @@ "MessageListeningSessionsInTheLastYear": "{0} sessions d’écoute l’an dernier", "MessageLoading": "Chargement…", "MessageLoadingFolders": "Chargement des dossiers…", - "MessageLogsDescription": "Logs are stored in /metadata/logs as JSON files. Crash logs are stored in /metadata/logs/crash_logs.txt.", + "MessageLogsDescription": "Les journaux sont stockés dans /metadata/logs sous forme de fichiers JSON. Les journaux d’incidents sont stockés dans /metadata/logs/crash_logs.txt.", "MessageM4BFailed": "M4B échec", "MessageM4BFinished": "M4B terminé", "MessageMapChapterTitles": "Faire correspondre les titres des chapitres aux chapitres existants de votre livre audio sans ajuster l’horodatage.", @@ -745,8 +745,8 @@ "ToastBookmarkRemoveSuccess": "Signet supprimé", "ToastBookmarkUpdateFailed": "Échec de la mise à jour de signet", "ToastBookmarkUpdateSuccess": "Signet mis à jour", - "ToastCachePurgeFailed": "Failed to purge cache", - "ToastCachePurgeSuccess": "Cache purged successfully", + "ToastCachePurgeFailed": "Échec de la purge du cache", + "ToastCachePurgeSuccess": "Cache purgé avec succès", "ToastChaptersHaveErrors": "Les chapitres contiennent des erreurs", "ToastChaptersMustHaveTitles": "Les chapitre doivent avoir un titre", "ToastCollectionItemsRemoveFailed": "Échec de la suppression de(s) article(s) de la collection", @@ -755,9 +755,9 @@ "ToastCollectionRemoveSuccess": "Collection supprimée", "ToastCollectionUpdateFailed": "Échec de la mise à jour de la collection", "ToastCollectionUpdateSuccess": "Collection mise à jour", - "ToastDeleteFileFailed": "Failed to delete file", - "ToastDeleteFileSuccess": "File deleted", - "ToastFailedToLoadData": "Failed to load data", + "ToastDeleteFileFailed": "Échec de la suppression du fichier", + "ToastDeleteFileSuccess": "Fichier supprimé", + "ToastFailedToLoadData": "Échec du chargement des données", "ToastItemCoverUpdateFailed": "Échec de la mise à jour de la couverture de l’article", "ToastItemCoverUpdateSuccess": "Couverture de l’article mise à jour", "ToastItemDetailsUpdateFailed": "Échec de la mise à jour des détails de l’article", @@ -791,16 +791,16 @@ "ToastSendEbookToDeviceSuccess": "Livre numérique envoyé à l’appareil : {0}", "ToastSeriesUpdateFailed": "Échec de la mise à jour de la série", "ToastSeriesUpdateSuccess": "Mise à jour de la série réussie", - "ToastServerSettingsUpdateFailed": "Failed to update server settings", - "ToastServerSettingsUpdateSuccess": "Server settings updated", + "ToastServerSettingsUpdateFailed": "Échec de la mise à jour des paramètres du serveur", + "ToastServerSettingsUpdateSuccess": "Mise à jour des paramètres du serveur", "ToastSessionDeleteFailed": "Échec de la suppression de session", "ToastSessionDeleteSuccess": "Session supprimée", "ToastSocketConnected": "WebSocket connecté", "ToastSocketDisconnected": "WebSocket déconnecté", "ToastSocketFailedToConnect": "Échec de la connexion WebSocket", - "ToastSortingPrefixesEmptyError": "Must have at least 1 sorting prefix", - "ToastSortingPrefixesUpdateFailed": "Failed to update sorting prefixes", - "ToastSortingPrefixesUpdateSuccess": "Sorting prefixes updated ({0} items)", + "ToastSortingPrefixesEmptyError": "Doit avoir au moins 1 préfixe de tri", + "ToastSortingPrefixesUpdateFailed": "Échec de la mise à jour des préfixes de tri", + "ToastSortingPrefixesUpdateSuccess": "Mise à jour des préfixes de tri ({0} élément)", "ToastUserDeleteFailed": "Échec de la suppression de l’utilisateur", "ToastUserDeleteSuccess": "Utilisateur supprimé" -} \ No newline at end of file +} From 69833db819387d00611708244417fd1c9814d77e Mon Sep 17 00:00:00 2001 From: advplyr Date: Sun, 19 May 2024 14:40:46 -0500 Subject: [PATCH 04/32] Add:Env variable setting to allow CORS --- index.js | 1 - server/Logger.js | 11 ++++++----- server/Server.js | 4 +++- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/index.js b/index.js index c4488f1b0..a8686b310 100644 --- a/index.js +++ b/index.js @@ -4,7 +4,6 @@ global.appRoot = __dirname const isDev = process.env.NODE_ENV !== 'production' if (isDev) { const devEnv = require('./dev').config - process.env.NODE_ENV = 'development' if (devEnv.Port) process.env.PORT = devEnv.Port if (devEnv.ConfigPath) process.env.CONFIG_PATH = devEnv.ConfigPath if (devEnv.MetadataPath) process.env.METADATA_PATH = devEnv.MetadataPath diff --git a/server/Logger.js b/server/Logger.js index 8283e1f0a..6b5894005 100644 --- a/server/Logger.js +++ b/server/Logger.js @@ -7,6 +7,7 @@ class Logger { this.logManager = null this.isDev = process.env.NODE_ENV !== 'production' + this.logLevel = !this.isDev ? LogLevel.INFO : LogLevel.TRACE this.socketListeners = [] } @@ -49,7 +50,7 @@ class Logger { } addSocketListener(socket, level) { - var index = this.socketListeners.findIndex(s => s.id === socket.id) + var index = this.socketListeners.findIndex((s) => s.id === socket.id) if (index >= 0) { this.socketListeners.splice(index, 1, { id: socket.id, @@ -66,7 +67,7 @@ class Logger { } removeSocketListener(socketId) { - this.socketListeners = this.socketListeners.filter(s => s.id !== socketId) + this.socketListeners = this.socketListeners.filter((s) => s.id !== socketId) } /** @@ -135,8 +136,8 @@ class Logger { /** * Fatal errors are ones that exit the process * Fatal logs are saved to crash_logs.txt - * - * @param {...any} args + * + * @param {...any} args */ fatal(...args) { console.error(`[${this.timestamp}] FATAL:`, ...args, `(${this.source})`) @@ -148,4 +149,4 @@ class Logger { this.handleLog(LogLevel.NOTE, args, this.source) } } -module.exports = new Logger() \ No newline at end of file +module.exports = new Logger() diff --git a/server/Server.js b/server/Server.js index 9de2f3f63..2d393a8d7 100644 --- a/server/Server.js +++ b/server/Server.js @@ -50,6 +50,7 @@ class Server { global.MetadataPath = fileUtils.filePathToPOSIX(Path.normalize(METADATA_PATH)) global.RouterBasePath = ROUTER_BASE_PATH global.XAccel = process.env.USE_X_ACCEL + global.AllowCors = process.env.ALLOW_CORS === '1' if (!fs.pathExistsSync(global.ConfigPath)) { fs.mkdirSync(global.ConfigPath) @@ -182,11 +183,12 @@ class Server { * @see https://ionicframework.com/docs/troubleshooting/cors * * Running in development allows cors to allow testing the mobile apps in the browser + * or env variable ALLOW_CORS = '1' */ app.use((req, res, next) => { if (Logger.isDev || req.path.match(/\/api\/items\/([a-z0-9-]{36})\/(ebook|cover)(\/[0-9]+)?/)) { const allowedOrigins = ['capacitor://localhost', 'http://localhost'] - if (Logger.isDev || allowedOrigins.some((o) => o === req.get('origin'))) { + if (global.AllowCors || Logger.isDev || allowedOrigins.some((o) => o === req.get('origin'))) { res.header('Access-Control-Allow-Origin', req.get('origin')) res.header('Access-Control-Allow-Methods', 'GET, POST, PATCH, PUT, DELETE, OPTIONS') res.header('Access-Control-Allow-Headers', '*') From 700d7fe68ec10e9e7f163f2b630b1c71cc530178 Mon Sep 17 00:00:00 2001 From: advplyr Date: Wed, 22 May 2024 16:51:11 -0500 Subject: [PATCH 05/32] Fix:Ebook item context menu position on mouseover #2980 --- client/components/widgets/MoreMenu.vue | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/components/widgets/MoreMenu.vue b/client/components/widgets/MoreMenu.vue index f890559ab..3dddf5e52 100644 --- a/client/components/widgets/MoreMenu.vue +++ b/client/components/widgets/MoreMenu.vue @@ -1,5 +1,5 @@