From 9b01d11b27fa0e56debac8610dd02b1cea3aeecc Mon Sep 17 00:00:00 2001 From: Lauri Vuorela Date: Tue, 22 Oct 2024 23:58:09 +0200 Subject: [PATCH 001/303] allow setting createdAt and respect set finishedAt when syncing progress --- server/models/User.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/server/models/User.js b/server/models/User.js index aa63aea8..84f471e9 100644 --- a/server/models/User.js +++ b/server/models/User.js @@ -607,13 +607,14 @@ class User extends Model { ebookLocation: progressPayload.ebookLocation || null, ebookProgress: isNullOrNaN(progressPayload.ebookProgress) ? 0 : Number(progressPayload.ebookProgress), finishedAt: progressPayload.finishedAt || null, + createdAt: progressPayload.createdAt || new Date(), extraData: { libraryItemId: progressPayload.libraryItemId, progress: isNullOrNaN(progressPayload.progress) ? 0 : Number(progressPayload.progress) } } if (newMediaProgressPayload.isFinished) { - newMediaProgressPayload.finishedAt = new Date() + newMediaProgressPayload.finishedAt = newMediaProgressPayload.finishedAt || new Date() newMediaProgressPayload.extraData.progress = 1 } else { newMediaProgressPayload.finishedAt = null From 33e0987d7325191b82bc99b51ef3426b5d300e80 Mon Sep 17 00:00:00 2001 From: Vito0912 <86927734+Vito0912@users.noreply.github.com> Date: Sat, 7 Dec 2024 10:09:14 +0100 Subject: [PATCH 002/303] Added mediaMetadata to playbackSessions --- server/managers/PlaybackSessionManager.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/server/managers/PlaybackSessionManager.js b/server/managers/PlaybackSessionManager.js index ce43fc8c..d0daa3ba 100644 --- a/server/managers/PlaybackSessionManager.js +++ b/server/managers/PlaybackSessionManager.js @@ -175,6 +175,12 @@ class PlaybackSessionManager { // New session from local session = new PlaybackSession(sessionJson) session.deviceInfo = deviceInfo + // This makes sure that the client's metadata is preferred over the library's metadata, if available, to make a non-breaking change + if(session.mediaMetadata == null) { + // Only sync important metadata + const { title, subtitle, narrators, authors, series, genres } = libraryItem.media.metadata || {}; + session.mediaMetadata = { title, subtitle, narrators, authors, series, genres }; + } session.setDuration(libraryItem, sessionJson.episodeId) Logger.debug(`[PlaybackSessionManager] Inserting new session for "${session.displayTitle}" (${session.id})`) await Database.createPlaybackSession(session) From 89167543fa18e43eab06bf194bbc1996bb08b648 Mon Sep 17 00:00:00 2001 From: Vito0912 <86927734+Vito0912@users.noreply.github.com> Date: Sat, 7 Dec 2024 10:25:52 +0100 Subject: [PATCH 003/303] added author for podcasts --- server/managers/PlaybackSessionManager.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/managers/PlaybackSessionManager.js b/server/managers/PlaybackSessionManager.js index d0daa3ba..8dd7bc36 100644 --- a/server/managers/PlaybackSessionManager.js +++ b/server/managers/PlaybackSessionManager.js @@ -178,8 +178,8 @@ class PlaybackSessionManager { // This makes sure that the client's metadata is preferred over the library's metadata, if available, to make a non-breaking change if(session.mediaMetadata == null) { // Only sync important metadata - const { title, subtitle, narrators, authors, series, genres } = libraryItem.media.metadata || {}; - session.mediaMetadata = { title, subtitle, narrators, authors, series, genres }; + const { title, subtitle, narrators, authors, author, series, genres } = libraryItem.media.metadata || {}; + session.mediaMetadata = { title, subtitle, narrators, authors, series, genres, author}; } session.setDuration(libraryItem, sessionJson.episodeId) Logger.debug(`[PlaybackSessionManager] Inserting new session for "${session.displayTitle}" (${session.id})`) From 2fbb31e0ea84fabe0e08564141d427e3ab385cbf Mon Sep 17 00:00:00 2001 From: Vito0912 <86927734+Vito0912@users.noreply.github.com> Date: Sat, 7 Dec 2024 10:37:00 +0100 Subject: [PATCH 004/303] added null saftey and added displayTitle and displayAuthor --- server/managers/PlaybackSessionManager.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/server/managers/PlaybackSessionManager.js b/server/managers/PlaybackSessionManager.js index 8dd7bc36..e8704470 100644 --- a/server/managers/PlaybackSessionManager.js +++ b/server/managers/PlaybackSessionManager.js @@ -178,8 +178,14 @@ class PlaybackSessionManager { // This makes sure that the client's metadata is preferred over the library's metadata, if available, to make a non-breaking change if(session.mediaMetadata == null) { // Only sync important metadata - const { title, subtitle, narrators, authors, author, series, genres } = libraryItem.media.metadata || {}; - session.mediaMetadata = { title, subtitle, narrators, authors, series, genres, author}; + const { title, subtitle, narrators, authors, author, series, genres } = libraryItem?.media?.metadata || {} + session.mediaMetadata = { title, subtitle, narrators, authors, series, genres, author} + } + if(session.displayTitle == null || session.displayTitle === '') { + session.displayTitle = libraryItem?.media?.metadata?.title ?? '' + } + if(session.displayAuthor == null || session.displayAuthor === '') { + session.displayAuthor = libraryItem?.media?.metadata?.authors?.map(a => a.name).join(', ') ?? libraryItem?.media?.metadata?.author ?? '' } session.setDuration(libraryItem, sessionJson.episodeId) Logger.debug(`[PlaybackSessionManager] Inserting new session for "${session.displayTitle}" (${session.id})`) From f9bbd7117405e393c8bef56653cb279575896c32 Mon Sep 17 00:00:00 2001 From: Vito0912 <86927734+Vito0912@users.noreply.github.com> Date: Tue, 17 Dec 2024 15:27:37 +0100 Subject: [PATCH 005/303] added type to be saved. Should support podcasts It should support everything important from the podcast metadata: https://api.audiobookshelf.org/#podcast-metadata And the book metadata: https://api.audiobookshelf.org/#book-metadata --- server/managers/PlaybackSessionManager.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/managers/PlaybackSessionManager.js b/server/managers/PlaybackSessionManager.js index e8704470..1f40f38e 100644 --- a/server/managers/PlaybackSessionManager.js +++ b/server/managers/PlaybackSessionManager.js @@ -178,8 +178,8 @@ class PlaybackSessionManager { // This makes sure that the client's metadata is preferred over the library's metadata, if available, to make a non-breaking change if(session.mediaMetadata == null) { // Only sync important metadata - const { title, subtitle, narrators, authors, author, series, genres } = libraryItem?.media?.metadata || {} - session.mediaMetadata = { title, subtitle, narrators, authors, series, genres, author} + const { title, subtitle, narrators, authors, author, series, genres, type } = libraryItem?.media?.metadata || {} + session.mediaMetadata = { title, subtitle, narrators, authors, author, series, genres, type } } if(session.displayTitle == null || session.displayTitle === '') { session.displayTitle = libraryItem?.media?.metadata?.title ?? '' From 00343a953b20e675e1132213ecb5d24ce517e5a8 Mon Sep 17 00:00:00 2001 From: advplyr Date: Mon, 3 Feb 2025 17:47:10 -0600 Subject: [PATCH 006/303] Update Collection/Playlist and batch quick match modal bg colors to be consistent with other modals --- client/components/modals/BatchQuickMatchModel.vue | 2 +- client/components/modals/collections/AddCreateModal.vue | 2 +- client/components/modals/collections/CollectionItem.vue | 2 +- client/components/modals/playlists/AddCreateModal.vue | 2 +- client/components/modals/playlists/UserPlaylistItem.vue | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/client/components/modals/BatchQuickMatchModel.vue b/client/components/modals/BatchQuickMatchModel.vue index bf596199..558a2fa3 100644 --- a/client/components/modals/BatchQuickMatchModel.vue +++ b/client/components/modals/BatchQuickMatchModel.vue @@ -6,7 +6,7 @@ -
+
diff --git a/client/components/modals/collections/AddCreateModal.vue b/client/components/modals/collections/AddCreateModal.vue index c4878adc..17959526 100644 --- a/client/components/modals/collections/AddCreateModal.vue +++ b/client/components/modals/collections/AddCreateModal.vue @@ -6,7 +6,7 @@
-
+

{{ $strings.LabelAddToCollection }}

diff --git a/client/components/modals/collections/CollectionItem.vue b/client/components/modals/collections/CollectionItem.vue index 70849e98..62ca8620 100644 --- a/client/components/modals/collections/CollectionItem.vue +++ b/client/components/modals/collections/CollectionItem.vue @@ -1,5 +1,5 @@ -
+

{{ $strings.LabelAddToPlaylist }}

diff --git a/client/components/modals/playlists/UserPlaylistItem.vue b/client/components/modals/playlists/UserPlaylistItem.vue index 0cccf359..f7df9218 100644 --- a/client/components/modals/playlists/UserPlaylistItem.vue +++ b/client/components/modals/playlists/UserPlaylistItem.vue @@ -1,5 +1,5 @@ @@ -24,7 +24,8 @@ export default { readonly: Boolean, disabled: Boolean, inputClass: String, - showCopy: Boolean + showCopy: Boolean, + trimWhitespace: Boolean }, data() { return {} diff --git a/client/components/widgets/BookDetailsEdit.vue b/client/components/widgets/BookDetailsEdit.vue index fa26bcf5..a7af02c8 100644 --- a/client/components/widgets/BookDetailsEdit.vue +++ b/client/components/widgets/BookDetailsEdit.vue @@ -3,10 +3,10 @@
- +
- +
@@ -42,19 +42,19 @@
- +
- +
- +
- +
diff --git a/client/components/widgets/PodcastDetailsEdit.vue b/client/components/widgets/PodcastDetailsEdit.vue index 389ca894..e665dc4e 100644 --- a/client/components/widgets/PodcastDetailsEdit.vue +++ b/client/components/widgets/PodcastDetailsEdit.vue @@ -3,14 +3,14 @@
- +
- +
- + @@ -25,13 +25,13 @@
- +
- +
- +
From b5e69630def903b7a00504f5eb54bd4fc01c0eea Mon Sep 17 00:00:00 2001 From: advplyr Date: Thu, 6 Feb 2025 17:29:27 -0600 Subject: [PATCH 013/303] Update batch edit text inputs to trim whitespace --- client/pages/batch/index.vue | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/client/pages/batch/index.vue b/client/pages/batch/index.vue index 263dee58..575f1db1 100644 --- a/client/pages/batch/index.vue +++ b/client/pages/batch/index.vue @@ -22,7 +22,7 @@
- +
@@ -31,7 +31,7 @@
- +
@@ -51,11 +51,11 @@
- +
- +
From a37fe3c3d2262963831afd8c2863018ae30ab571 Mon Sep 17 00:00:00 2001 From: advplyr Date: Fri, 7 Feb 2025 17:09:48 -0600 Subject: [PATCH 014/303] Fix: Users with update permission unable to remove books from collection #3947 --- server/controllers/CollectionController.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/server/controllers/CollectionController.js b/server/controllers/CollectionController.js index 00b82ce9..475adfe0 100644 --- a/server/controllers/CollectionController.js +++ b/server/controllers/CollectionController.js @@ -251,6 +251,7 @@ class CollectionController { /** * DELETE: /api/collections/:id/book/:bookId * Remove a single book from a collection. Re-order books + * Users with update permission can remove books from collections * TODO: bookId is actually libraryItemId. Clients need updating to use bookId * * @param {CollectionControllerRequest} req @@ -427,7 +428,8 @@ class CollectionController { req.collection = collection } - if (req.method == 'DELETE' && !req.user.canDelete) { + // Users with update permission can remove books from collections + if (req.method == 'DELETE' && !req.params.bookId && !req.user.canDelete) { Logger.warn(`[CollectionController] User "${req.user.username}" attempted to delete without permission`) return res.sendStatus(403) } else if ((req.method == 'PATCH' || req.method == 'POST') && !req.user.canUpdate) { From 52bb28669a62da31659b90bfd287f0bf06f629fd Mon Sep 17 00:00:00 2001 From: mikiher Date: Sat, 8 Feb 2025 10:41:56 +0200 Subject: [PATCH 015/303] Add a profile utility function --- server/utils/profiler.js | 41 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 server/utils/profiler.js diff --git a/server/utils/profiler.js b/server/utils/profiler.js new file mode 100644 index 00000000..614ce5b7 --- /dev/null +++ b/server/utils/profiler.js @@ -0,0 +1,41 @@ +const { performance, createHistogram } = require('perf_hooks') +const util = require('util') +const Logger = require('../Logger') + +const histograms = new Map() + +function profile(asyncFunc, isFindQuery = true, funcName = asyncFunc.name) { + if (!histograms.has(funcName)) { + const histogram = createHistogram() + histogram.values = [] + histograms.set(funcName, histogram) + } + const histogram = histograms.get(funcName) + + return async (...args) => { + if (isFindQuery) { + const findOptions = args[0] + Logger.info(`[${funcName}] findOptions:`, util.inspect(findOptions, { depth: null })) + findOptions.logging = (query, time) => Logger.info(`[${funcName}] ${query} Elapsed time: ${time}ms`) + findOptions.benchmark = true + } + const start = performance.now() + try { + const result = await asyncFunc(...args) + return result + } catch (error) { + Logger.error(`[${funcName}] failed`) + throw error + } finally { + const end = performance.now() + const duration = Math.round(end - start) + histogram.record(duration) + histogram.values.push(duration) + Logger.info(`[${funcName}] duration: ${duration}ms`) + Logger.info(`[${funcName}] histogram values:`, histogram.values) + Logger.info(`[${funcName}] histogram:`, histogram) + } + } +} + +module.exports = { profile } From a13143245b5bc1aafd8fac0c27b796be452da39f Mon Sep 17 00:00:00 2001 From: mikiher Date: Sat, 8 Feb 2025 12:29:23 +0200 Subject: [PATCH 016/303] Improve page load queries on title, titleIgnorePrefix, and addedAt sort order --- .../v2.19.1-copy-title-to-library-items.js | 156 ++++++++++++++++++ server/models/Book.js | 9 + server/models/LibraryItem.js | 14 +- server/scanner/BookScanner.js | 2 + .../utils/queries/libraryItemsBookFilters.js | 43 ++++- ...2.19.1-copy-title-to-library-items.test.js | 148 +++++++++++++++++ 6 files changed, 363 insertions(+), 9 deletions(-) create mode 100644 server/migrations/v2.19.1-copy-title-to-library-items.js create mode 100644 test/server/migrations/v2.19.1-copy-title-to-library-items.test.js diff --git a/server/migrations/v2.19.1-copy-title-to-library-items.js b/server/migrations/v2.19.1-copy-title-to-library-items.js new file mode 100644 index 00000000..8f982333 --- /dev/null +++ b/server/migrations/v2.19.1-copy-title-to-library-items.js @@ -0,0 +1,156 @@ +const util = require('util') + +/** + * @typedef MigrationContext + * @property {import('sequelize').QueryInterface} queryInterface - a suquelize QueryInterface object. + * @property {import('../Logger')} logger - a Logger object. + * + * @typedef MigrationOptions + * @property {MigrationContext} context - an object containing the migration context. + */ + +const migrationVersion = '2.19.1' +const migrationName = `${migrationVersion}-copy-title-to-library-items` +const loggerPrefix = `[${migrationVersion} migration]` + +/** + * This upward migration adds a title column to the libraryItems table, copies the title from the book to the libraryItem, + * and creates a new index on the title column. In addition it sets a trigger on the books table to update the title column + * in the libraryItems table when a book is updated. + * + * @param {MigrationOptions} options - an object containing the migration context. + * @returns {Promise} - A promise that resolves when the migration is complete. + */ +async function up({ context: { queryInterface, logger } }) { + // Upwards migration script + logger.info(`${loggerPrefix} UPGRADE BEGIN: ${migrationName}`) + + await addColumn(queryInterface, logger, 'libraryItems', 'title', { type: queryInterface.sequelize.Sequelize.STRING, allowNull: true }) + await copyColumn(queryInterface, logger, 'books', 'title', 'id', 'libraryItems', 'title', 'mediaId') + await addTrigger(queryInterface, logger, 'books', 'title', 'id', 'libraryItems', 'title', 'mediaId') + await addIndex(queryInterface, logger, 'libraryItems', ['libraryId', 'mediaType', { name: 'title', collate: 'NOCASE' }]) + + await addColumn(queryInterface, logger, 'libraryItems', 'titleIgnorePrefix', { type: queryInterface.sequelize.Sequelize.STRING, allowNull: true }) + await copyColumn(queryInterface, logger, 'books', 'titleIgnorePrefix', 'id', 'libraryItems', 'titleIgnorePrefix', 'mediaId') + await addTrigger(queryInterface, logger, 'books', 'titleIgnorePrefix', 'id', 'libraryItems', 'titleIgnorePrefix', 'mediaId') + await addIndex(queryInterface, logger, 'libraryItems', ['libraryId', 'mediaType', { name: 'titleIgnorePrefix', collate: 'NOCASE' }]) + + await addIndex(queryInterface, logger, 'libraryItems', ['libraryId', 'mediaType', 'createdAt']) + + logger.info(`${loggerPrefix} UPGRADE END: ${migrationName}`) +} + +/** + * This downward migration script removes the title column from the libraryItems table, removes the trigger on the books table, + * and removes the index on the title column. + * + * @param {MigrationOptions} options - an object containing the migration context. + * @returns {Promise} - A promise that resolves when the migration is complete. + */ +async function down({ context: { queryInterface, logger } }) { + // Downward migration script + logger.info(`${loggerPrefix} DOWNGRADE BEGIN: ${migrationName}`) + + await removeIndex(queryInterface, logger, 'libraryItems', ['libraryId', 'mediaType', 'title']) + await removeTrigger(queryInterface, logger, 'libraryItems', 'title') + await removeColumn(queryInterface, logger, 'libraryItems', 'title') + + await removeIndex(queryInterface, logger, 'libraryItems', ['libraryId', 'mediaType', 'titleIgnorePrefix']) + await removeTrigger(queryInterface, logger, 'libraryItems', 'titleIgnorePrefix') + await removeColumn(queryInterface, logger, 'libraryItems', 'titleIgnorePrefix') + + await removeIndex(queryInterface, logger, 'libraryItems', ['libraryId', 'mediaType', 'createdAt']) + + logger.info(`${loggerPrefix} DOWNGRADE END: ${migrationName}`) +} + +/** + * Utility function to add an index to a table. If the index already z`exists, it logs a message and continues. + * + * @param {import('sequelize').QueryInterface} queryInterface + * @param {import ('../Logger')} logger + * @param {string} tableName + * @param {string[]} columns + */ +async function addIndex(queryInterface, logger, tableName, columns) { + const columnString = columns.map((column) => util.inspect(column)).join(', ') + const indexName = convertToSnakeCase(`${tableName}_${columns.map((column) => (typeof column === 'string' ? column : column.name)).join('_')}`) + try { + logger.info(`${loggerPrefix} adding index on [${columnString}] to table ${tableName}. index name: ${indexName}"`) + await queryInterface.addIndex(tableName, columns) + logger.info(`${loggerPrefix} added index on [${columnString}] to table ${tableName}. index name: ${indexName}"`) + } catch (error) { + if (error.name === 'SequelizeDatabaseError' && error.message.includes('already exists')) { + logger.info(`${loggerPrefix} index [${columnString}] for table "${tableName}" already exists`) + } else { + throw error + } + } +} + +/** + * Utility function to remove an index from a table. + * Sequelize implemets it using DROP INDEX IF EXISTS, so it won't throw an error if the index doesn't exist. + * + * @param {import('sequelize').QueryInterface} queryInterface + * @param {import ('../Logger')} logger + * @param {string} tableName + * @param {string[]} columns + */ +async function removeIndex(queryInterface, logger, tableName, columns) { + logger.info(`${loggerPrefix} removing index [${columns.join(', ')}] from table "${tableName}"`) + await queryInterface.removeIndex(tableName, columns) + logger.info(`${loggerPrefix} removed index [${columns.join(', ')}] from table "${tableName}"`) +} + +async function addColumn(queryInterface, logger, table, column, options) { + logger.info(`${loggerPrefix} adding column "${column}" to table "${table}"`) + await queryInterface.addColumn(table, column, options) + logger.info(`${loggerPrefix} added column "${column}" to table "${table}"`) +} + +async function removeColumn(queryInterface, logger, table, column) { + logger.info(`${loggerPrefix} removing column "${column}" from table "${table}"`) + await queryInterface.removeColumn(table, column) + logger.info(`${loggerPrefix} removed column "${column}" from table "${table}"`) +} + +async function copyColumn(queryInterface, logger, sourceTable, sourceColumn, sourceIdColumn, targetTable, targetColumn, targetIdColumn) { + logger.info(`${loggerPrefix} copying column "${sourceColumn}" from table "${sourceTable}" to table "${targetTable}"`) + await queryInterface.sequelize.query(` + UPDATE ${targetTable} + SET ${targetColumn} = ${sourceTable}.${sourceColumn} + FROM ${sourceTable} + WHERE ${targetTable}.${targetIdColumn} = ${sourceTable}.${sourceIdColumn} + `) + logger.info(`${loggerPrefix} copied column "${sourceColumn}" from table "${sourceTable}" to table "${targetTable}"`) +} + +async function addTrigger(queryInterface, logger, sourceTable, sourceColumn, sourceIdColumn, targetTable, targetColumn, targetIdColumn) { + logger.info(`${loggerPrefix} adding trigger to update ${targetTable}.${targetColumn} when ${sourceTable}.${sourceColumn} is updated`) + const triggerName = convertToSnakeCase(`update_${targetTable}_${targetColumn}`) + await queryInterface.sequelize.query(` + CREATE TRIGGER ${triggerName} + AFTER UPDATE OF ${sourceColumn} ON ${sourceTable} + FOR EACH ROW + BEGIN + UPDATE ${targetTable} + SET ${targetColumn} = NEW.${sourceColumn} + WHERE ${targetTable}.${targetIdColumn} = NEW.${sourceIdColumn}; + END; + `) + logger.info(`${loggerPrefix} added trigger to update ${targetTable}.${targetColumn} when ${sourceTable}.${sourceColumn} is updated`) +} + +async function removeTrigger(queryInterface, logger, targetTable, targetColumn) { + logger.info(`${loggerPrefix} removing trigger to update ${targetTable}.${targetColumn}`) + const triggerName = convertToSnakeCase(`update_${targetTable}_${targetColumn}`) + await queryInterface.sequelize.query(`DROP TRIGGER IF EXISTS ${triggerName}`) + logger.info(`${loggerPrefix} removed trigger to update ${targetTable}.${targetColumn}`) +} + +function convertToSnakeCase(str) { + return str.replace(/([A-Z])/g, '_$1').toLowerCase() +} + +module.exports = { up, down } diff --git a/server/models/Book.js b/server/models/Book.js index 3684608d..fd24c269 100644 --- a/server/models/Book.js +++ b/server/models/Book.js @@ -3,6 +3,7 @@ const Logger = require('../Logger') const { getTitlePrefixAtEnd, getTitleIgnorePrefix } = require('../utils') const parseNameString = require('../utils/parsers/parseNameString') const htmlSanitizer = require('../utils/htmlSanitizer') +const libraryItemsBookFilters = require('../utils/queries/libraryItemsBookFilters') /** * @typedef EBookFileObject @@ -192,6 +193,14 @@ class Book extends Model { ] } ) + + Book.addHook('afterDestroy', async (instance) => { + libraryItemsBookFilters.clearCountCache('afterDestroy ') + }) + + Book.addHook('afterCreate', async (instance) => { + libraryItemsBookFilters.clearCountCache('afterCreate') + }) } /** diff --git a/server/models/LibraryItem.js b/server/models/LibraryItem.js index 3ed4e31e..bd11e585 100644 --- a/server/models/LibraryItem.js +++ b/server/models/LibraryItem.js @@ -73,6 +73,10 @@ class LibraryItem extends Model { /** @type {Book.BookExpanded|Podcast.PodcastExpanded} - only set when expanded */ this.media + /** @type {string} */ + this.title // Only used for sorting + /** @type {string} */ + this.titleIgnorePrefix // Only used for sorting } /** @@ -677,7 +681,9 @@ class LibraryItem extends Model { lastScan: DataTypes.DATE, lastScanVersion: DataTypes.STRING, libraryFiles: DataTypes.JSON, - extraData: DataTypes.JSON + extraData: DataTypes.JSON, + title: DataTypes.STRING, + titleIgnorePrefix: DataTypes.STRING }, { sequelize, @@ -695,6 +701,12 @@ class LibraryItem extends Model { { fields: ['libraryId', 'mediaType', 'size'] }, + { + fields: ['libraryId', 'mediaType', { name: 'title', collate: 'NOCASE' }] + }, + { + fields: ['libraryId', 'mediaType', { name: 'titleIgnorePrefix', collate: 'NOCASE' }] + }, { fields: ['libraryId', 'mediaId', 'mediaType'] }, diff --git a/server/scanner/BookScanner.js b/server/scanner/BookScanner.js index 74798dd6..210f20f9 100644 --- a/server/scanner/BookScanner.js +++ b/server/scanner/BookScanner.js @@ -521,6 +521,8 @@ class BookScanner { libraryItemObj.isMissing = false libraryItemObj.isInvalid = false libraryItemObj.extraData = {} + libraryItemObj.title = bookMetadata.title + libraryItemObj.titleIgnorePrefix = getTitleIgnorePrefix(bookMetadata.title) // Set isSupplementary flag on ebook library files for (const libraryFile of libraryItemObj.libraryFiles) { diff --git a/server/utils/queries/libraryItemsBookFilters.js b/server/utils/queries/libraryItemsBookFilters.js index 9e74276a..3adb929e 100644 --- a/server/utils/queries/libraryItemsBookFilters.js +++ b/server/utils/queries/libraryItemsBookFilters.js @@ -4,6 +4,9 @@ const Logger = require('../../Logger') const authorFilters = require('./authorFilters') const ShareManager = require('../../managers/ShareManager') +const { profile } = require('../profiler') + +const countCache = new Map() module.exports = { /** @@ -270,9 +273,9 @@ module.exports = { } if (global.ServerSettings.sortingIgnorePrefix) { - return [[Sequelize.literal('titleIgnorePrefix COLLATE NOCASE'), dir]] + return [[Sequelize.literal('`libraryItem`.`titleIgnorePrefix` COLLATE NOCASE'), dir]] } else { - return [[Sequelize.literal('`book`.`title` COLLATE NOCASE'), dir]] + return [[Sequelize.literal('`libraryItem`.`title` COLLATE NOCASE'), dir]] } } else if (sortBy === 'sequence') { const nullDir = sortDesc ? 'DESC NULLS FIRST' : 'ASC NULLS LAST' @@ -336,6 +339,28 @@ module.exports = { return { booksToExclude, bookSeriesToInclude } }, + clearCountCache(hook) { + Logger.debug(`[LibraryItemsBookFilters] book.${hook}: Clearing count cache`) + countCache.clear() + }, + + async findAndCountAll(findOptions, limit, offset) { + const findOptionsKey = JSON.stringify(findOptions) + Logger.debug(`[LibraryItemsBookFilters] findOptionsKey: ${findOptionsKey}`) + + findOptions.limit = limit || null + findOptions.offset = offset + + if (countCache.has(findOptionsKey)) { + const rows = await Database.bookModel.findAll(findOptions) + return { rows, count: countCache.get(findOptionsKey) } + } else { + const result = await Database.bookModel.findAndCountAll(findOptions) + countCache.set(findOptionsKey, result.count) + return result + } + }, + /** * Get library items for book media type using filter and sort * @param {string} libraryId @@ -411,7 +436,8 @@ module.exports = { if (includeRSSFeed) { libraryItemIncludes.push({ model: Database.feedModel, - required: filterGroup === 'feed-open' + required: filterGroup === 'feed-open', + separate: true }) } if (filterGroup === 'feed-open' && !includeRSSFeed) { @@ -560,7 +586,7 @@ module.exports = { } } - const { rows: books, count } = await Database.bookModel.findAndCountAll({ + const findOptions = { where: bookWhere, distinct: true, attributes: bookAttributes, @@ -577,10 +603,11 @@ module.exports = { ...bookIncludes ], order: sortOrder, - subQuery: false, - limit: limit || null, - offset - }) + subQuery: false + } + + const findAndCountAll = process.env.QUERY_PROFILING ? profile(this.findAndCountAll) : this.findAndCountAll + const { rows: books, count } = await findAndCountAll(findOptions, limit, offset) const libraryItems = books.map((bookExpanded) => { const libraryItem = bookExpanded.libraryItem diff --git a/test/server/migrations/v2.19.1-copy-title-to-library-items.test.js b/test/server/migrations/v2.19.1-copy-title-to-library-items.test.js new file mode 100644 index 00000000..5b767856 --- /dev/null +++ b/test/server/migrations/v2.19.1-copy-title-to-library-items.test.js @@ -0,0 +1,148 @@ +const chai = require('chai') +const sinon = require('sinon') +const { expect } = chai + +const { DataTypes, Sequelize } = require('sequelize') +const Logger = require('../../../server/Logger') + +const { up, down } = require('../../../server/migrations/v2.19.1-copy-title-to-library-items') + +describe('Migration v2.19.1-copy-title-to-library-items', () => { + let sequelize + let queryInterface + let loggerInfoStub + + beforeEach(async () => { + sequelize = new Sequelize({ dialect: 'sqlite', storage: ':memory:', logging: false }) + queryInterface = sequelize.getQueryInterface() + loggerInfoStub = sinon.stub(Logger, 'info') + + await queryInterface.createTable('books', { + id: { type: DataTypes.INTEGER, allowNull: false, primaryKey: true, unique: true }, + title: { type: DataTypes.STRING, allowNull: true }, + titleIgnorePrefix: { type: DataTypes.STRING, allowNull: true } + }) + + await queryInterface.createTable('libraryItems', { + id: { type: DataTypes.INTEGER, allowNull: false, primaryKey: true, unique: true }, + libraryId: { type: DataTypes.INTEGER, allowNull: false }, + mediaType: { type: DataTypes.STRING, allowNull: false }, + mediaId: { type: DataTypes.INTEGER, allowNull: false }, + createdAt: { type: DataTypes.DATE, allowNull: false } + }) + + await queryInterface.bulkInsert('books', [ + { id: 1, title: 'The Book 1', titleIgnorePrefix: 'Book 1, The' }, + { id: 2, title: 'Book 2', titleIgnorePrefix: 'Book 2' } + ]) + + await queryInterface.bulkInsert('libraryItems', [ + { id: 1, libraryId: 1, mediaType: 'book', mediaId: 1, createdAt: '2025-01-01 00:00:00.000 +00:00' }, + { id: 2, libraryId: 2, mediaType: 'book', mediaId: 2, createdAt: '2025-01-02 00:00:00.000 +00:00' } + ]) + }) + + afterEach(() => { + sinon.restore() + }) + + describe('up', () => { + it('should copy title and titleIgnorePrefix to libraryItems', async () => { + await up({ context: { queryInterface, logger: Logger } }) + + const [libraryItems] = await queryInterface.sequelize.query('SELECT * FROM libraryItems') + expect(libraryItems).to.deep.equal([ + { id: 1, libraryId: 1, mediaType: 'book', mediaId: 1, title: 'The Book 1', titleIgnorePrefix: 'Book 1, The', createdAt: '2025-01-01 00:00:00.000 +00:00' }, + { id: 2, libraryId: 2, mediaType: 'book', mediaId: 2, title: 'Book 2', titleIgnorePrefix: 'Book 2', createdAt: '2025-01-02 00:00:00.000 +00:00' } + ]) + }) + + it('should add index on title to libraryItems', async () => { + await up({ context: { queryInterface, logger: Logger } }) + + const [[{ count }]] = await queryInterface.sequelize.query(`SELECT COUNT(*) as count FROM sqlite_master WHERE type='index' AND name='library_items_library_id_media_type_title_ignore_prefix'`) + expect(count).to.equal(1) + }) + + it('should add trigger to books.title to update libraryItems.title', async () => { + await up({ context: { queryInterface, logger: Logger } }) + + const [[{ count }]] = await queryInterface.sequelize.query(`SELECT COUNT(*) as count FROM sqlite_master WHERE type='trigger' AND name='update_library_items_title'`) + expect(count).to.equal(1) + }) + + it('should add index on titleIgnorePrefix to libraryItems', async () => { + await up({ context: { queryInterface, logger: Logger } }) + + const [[{ count }]] = await queryInterface.sequelize.query(`SELECT COUNT(*) as count FROM sqlite_master WHERE type='index' AND name='library_items_library_id_media_type_title_ignore_prefix'`) + expect(count).to.equal(1) + }) + + it('should add trigger to books.titleIgnorePrefix to update libraryItems.titleIgnorePrefix', async () => { + await up({ context: { queryInterface, logger: Logger } }) + + const [[{ count }]] = await queryInterface.sequelize.query(`SELECT COUNT(*) as count FROM sqlite_master WHERE type='trigger' AND name='update_library_items_title_ignore_prefix'`) + expect(count).to.equal(1) + }) + + it('should add index on createdAt to libraryItems', async () => { + await up({ context: { queryInterface, logger: Logger } }) + + const [[{ count }]] = await queryInterface.sequelize.query(`SELECT COUNT(*) as count FROM sqlite_master WHERE type='index' AND name='library_items_library_id_media_type_created_at'`) + expect(count).to.equal(1) + }) + }) + + describe('down', () => { + it('should remove title and titleIgnorePrefix from libraryItems', async () => { + await up({ context: { queryInterface, logger: Logger } }) + await down({ context: { queryInterface, logger: Logger } }) + + const [libraryItems] = await queryInterface.sequelize.query('SELECT * FROM libraryItems') + expect(libraryItems).to.deep.equal([ + { id: 1, libraryId: 1, mediaType: 'book', mediaId: 1, createdAt: '2025-01-01 00:00:00.000 +00:00' }, + { id: 2, libraryId: 2, mediaType: 'book', mediaId: 2, createdAt: '2025-01-02 00:00:00.000 +00:00' } + ]) + }) + + it('should remove title trigger from books', async () => { + await up({ context: { queryInterface, logger: Logger } }) + await down({ context: { queryInterface, logger: Logger } }) + + const [[{ count }]] = await queryInterface.sequelize.query(`SELECT COUNT(*) as count FROM sqlite_master WHERE type='trigger' AND name='update_library_items_title'`) + expect(count).to.equal(0) + }) + + it('should remove titleIgnorePrefix trigger from books', async () => { + await up({ context: { queryInterface, logger: Logger } }) + await down({ context: { queryInterface, logger: Logger } }) + + const [[{ count }]] = await queryInterface.sequelize.query(`SELECT COUNT(*) as count FROM sqlite_master WHERE type='trigger' AND name='update_library_items_title_ignore_prefix'`) + expect(count).to.equal(0) + }) + + it('should remove index on titleIgnorePrefix from libraryItems', async () => { + await up({ context: { queryInterface, logger: Logger } }) + await down({ context: { queryInterface, logger: Logger } }) + + const [[{ count }]] = await queryInterface.sequelize.query(`SELECT COUNT(*) as count FROM sqlite_master WHERE type='index' AND name='library_items_library_id_media_type_title_ignore_prefix'`) + expect(count).to.equal(0) + }) + + it('should remove index on title from libraryItems', async () => { + await up({ context: { queryInterface, logger: Logger } }) + await down({ context: { queryInterface, logger: Logger } }) + + const [[{ count }]] = await queryInterface.sequelize.query(`SELECT COUNT(*) as count FROM sqlite_master WHERE type='index' AND name='library_items_library_id_media_type_title'`) + expect(count).to.equal(0) + }) + + it('should remove index on createdAt from libraryItems', async () => { + await up({ context: { queryInterface, logger: Logger } }) + await down({ context: { queryInterface, logger: Logger } }) + + const [[{ count }]] = await queryInterface.sequelize.query(`SELECT COUNT(*) as count FROM sqlite_master WHERE type='index' AND name='library_items_library_id_media_type_created_at'`) + expect(count).to.equal(0) + }) + }) +}) From 3d08a35aa00f9282706e343795a042a2177ef69e Mon Sep 17 00:00:00 2001 From: mikiher Date: Sat, 8 Feb 2025 14:53:01 +0200 Subject: [PATCH 017/303] Add index on (libraryId, mediaType, createdAt) --- server/models/LibraryItem.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/server/models/LibraryItem.js b/server/models/LibraryItem.js index bd11e585..ace6af43 100644 --- a/server/models/LibraryItem.js +++ b/server/models/LibraryItem.js @@ -701,6 +701,9 @@ class LibraryItem extends Model { { fields: ['libraryId', 'mediaType', 'size'] }, + { + fields: ['libraryId', 'mediaType', 'createdAt'] + }, { fields: ['libraryId', 'mediaType', { name: 'title', collate: 'NOCASE' }] }, From 9a261195b74ded2649cb7f394169b1219ff20ed3 Mon Sep 17 00:00:00 2001 From: advplyr Date: Sat, 8 Feb 2025 10:19:13 -0600 Subject: [PATCH 018/303] Update server/models/Book.js --- server/models/Book.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/models/Book.js b/server/models/Book.js index fd24c269..1f4193a2 100644 --- a/server/models/Book.js +++ b/server/models/Book.js @@ -195,7 +195,7 @@ class Book extends Model { ) Book.addHook('afterDestroy', async (instance) => { - libraryItemsBookFilters.clearCountCache('afterDestroy ') + libraryItemsBookFilters.clearCountCache('afterDestroy') }) Book.addHook('afterCreate', async (instance) => { From ef45f844e52c44da36147987f2ddf76c558202f1 Mon Sep 17 00:00:00 2001 From: advplyr Date: Sat, 8 Feb 2025 12:37:34 -0600 Subject: [PATCH 019/303] Update upwards migration to be idempotent --- .../v2.19.1-copy-title-to-library-items.js | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/server/migrations/v2.19.1-copy-title-to-library-items.js b/server/migrations/v2.19.1-copy-title-to-library-items.js index 8f982333..7b75fa85 100644 --- a/server/migrations/v2.19.1-copy-title-to-library-items.js +++ b/server/migrations/v2.19.1-copy-title-to-library-items.js @@ -105,8 +105,13 @@ async function removeIndex(queryInterface, logger, tableName, columns) { async function addColumn(queryInterface, logger, table, column, options) { logger.info(`${loggerPrefix} adding column "${column}" to table "${table}"`) - await queryInterface.addColumn(table, column, options) - logger.info(`${loggerPrefix} added column "${column}" to table "${table}"`) + const tableDescription = await queryInterface.describeTable(table) + if (!tableDescription[column]) { + await queryInterface.addColumn(table, column, options) + logger.info(`${loggerPrefix} added column "${column}" to table "${table}"`) + } else { + logger.info(`${loggerPrefix} column "${column}" already exists in table "${table}"`) + } } async function removeColumn(queryInterface, logger, table, column) { @@ -129,6 +134,9 @@ async function copyColumn(queryInterface, logger, sourceTable, sourceColumn, sou async function addTrigger(queryInterface, logger, sourceTable, sourceColumn, sourceIdColumn, targetTable, targetColumn, targetIdColumn) { logger.info(`${loggerPrefix} adding trigger to update ${targetTable}.${targetColumn} when ${sourceTable}.${sourceColumn} is updated`) const triggerName = convertToSnakeCase(`update_${targetTable}_${targetColumn}`) + + await queryInterface.sequelize.query(`DROP TRIGGER IF EXISTS ${triggerName}`) + await queryInterface.sequelize.query(` CREATE TRIGGER ${triggerName} AFTER UPDATE OF ${sourceColumn} ON ${sourceTable} From 0dd57a8912143c00fc1943f007a3c654466e6487 Mon Sep 17 00:00:00 2001 From: advplyr Date: Sat, 8 Feb 2025 13:02:27 -0600 Subject: [PATCH 020/303] Fix using next/prev in edit modals while rich text input is focused #3951 --- client/components/modals/item/EditModal.vue | 8 +++++++- client/components/modals/podcast/EditEpisode.vue | 8 ++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/client/components/modals/item/EditModal.vue b/client/components/modals/item/EditModal.vue index c41e6b54..ca6094d8 100644 --- a/client/components/modals/item/EditModal.vue +++ b/client/components/modals/item/EditModal.vue @@ -196,6 +196,9 @@ export default { methods: { async goPrevBook() { if (this.currentBookshelfIndex - 1 < 0) return + // Remove focus from active input + document.activeElement?.blur?.() + var prevBookId = this.bookshelfBookIds[this.currentBookshelfIndex - 1] this.processing = true var prevBook = await this.$axios.$get(`/api/items/${prevBookId}?expanded=1`).catch((error) => { @@ -215,6 +218,9 @@ export default { }, async goNextBook() { if (this.currentBookshelfIndex >= this.bookshelfBookIds.length - 1) return + // Remove focus from active input + document.activeElement?.blur?.() + this.processing = true var nextBookId = this.bookshelfBookIds[this.currentBookshelfIndex + 1] var nextBook = await this.$axios.$get(`/api/items/${nextBookId}?expanded=1`).catch((error) => { @@ -300,4 +306,4 @@ export default { .tab.tab-selected { height: 41px; } - \ No newline at end of file + diff --git a/client/components/modals/podcast/EditEpisode.vue b/client/components/modals/podcast/EditEpisode.vue index 9702ce38..9572a36a 100644 --- a/client/components/modals/podcast/EditEpisode.vue +++ b/client/components/modals/podcast/EditEpisode.vue @@ -117,8 +117,12 @@ export default { methods: { async goPrevEpisode() { if (this.currentEpisodeIndex - 1 < 0) return + // Remove focus from active input + document.activeElement?.blur?.() + const prevEpisodeId = this.episodeTableEpisodeIds[this.currentEpisodeIndex - 1] this.processing = true + const prevEpisode = await this.$axios.$get(`/api/podcasts/${this.libraryItem.id}/episode/${prevEpisodeId}`).catch((error) => { const errorMsg = error.response && error.response.data ? error.response.data : 'Failed to fetch episode' this.$toast.error(errorMsg) @@ -134,8 +138,12 @@ export default { }, async goNextEpisode() { if (this.currentEpisodeIndex >= this.episodeTableEpisodeIds.length - 1) return + // Remove focus from active input + document.activeElement?.blur?.() + this.processing = true const nextEpisodeId = this.episodeTableEpisodeIds[this.currentEpisodeIndex + 1] + const nextEpisode = await this.$axios.$get(`/api/podcasts/${this.libraryItem.id}/episode/${nextEpisodeId}`).catch((error) => { const errorMsg = error.response && error.response.data ? error.response.data : 'Failed to fetch book' this.$toast.error(errorMsg) From 36ef675556461d9cd1bdb70e43be7289afded7fa Mon Sep 17 00:00:00 2001 From: advplyr Date: Sat, 8 Feb 2025 13:05:50 -0600 Subject: [PATCH 021/303] Fix edit episode next/prev buttons showing when editing from home page --- client/components/app/BookShelfRow.vue | 1 + client/components/widgets/ItemSlider.vue | 1 + 2 files changed, 2 insertions(+) diff --git a/client/components/app/BookShelfRow.vue b/client/components/app/BookShelfRow.vue index 6e19b9dc..97d64d86 100644 --- a/client/components/app/BookShelfRow.vue +++ b/client/components/app/BookShelfRow.vue @@ -99,6 +99,7 @@ export default { this.$store.commit('showEditModal', libraryItem) }, editEpisode({ libraryItem, episode }) { + this.$store.commit('setEpisodeTableEpisodeIds', [episode.id]) this.$store.commit('setSelectedLibraryItem', libraryItem) this.$store.commit('globals/setSelectedEpisode', episode) this.$store.commit('globals/setShowEditPodcastEpisodeModal', true) diff --git a/client/components/widgets/ItemSlider.vue b/client/components/widgets/ItemSlider.vue index 92639cc6..92e9fcee 100644 --- a/client/components/widgets/ItemSlider.vue +++ b/client/components/widgets/ItemSlider.vue @@ -124,6 +124,7 @@ export default { this.updateSelectionMode(false) }, editEpisode({ libraryItem, episode }) { + this.$store.commit('setEpisodeTableEpisodeIds', [episode.id]) this.$store.commit('setSelectedLibraryItem', libraryItem) this.$store.commit('globals/setSelectedEpisode', episode) this.$store.commit('globals/setShowEditPodcastEpisodeModal', true) From 4cc300d6e9d91b10bd47dd45223b06f6a06f9ffe Mon Sep 17 00:00:00 2001 From: mikiher Date: Sun, 9 Feb 2025 21:39:43 +0200 Subject: [PATCH 022/303] Update changelog with v2.19.1 migration --- server/migrations/changelog.md | 1 + 1 file changed, 1 insertion(+) diff --git a/server/migrations/changelog.md b/server/migrations/changelog.md index 3b5a5626..acccef90 100644 --- a/server/migrations/changelog.md +++ b/server/migrations/changelog.md @@ -13,3 +13,4 @@ Please add a record of every database migration that you create to this file. Th | v2.17.5 | v2.17.5-remove-host-from-feed-urls | removes the host (serverAddress) from URL columns in the feeds and feedEpisodes tables | | v2.17.6 | v2.17.6-share-add-isdownloadable | Adds the isDownloadable column to the mediaItemShares table | | v2.17.7 | v2.17.7-add-indices | Adds indices to the libraryItems and books tables to reduce query times | +| v2.19.1 | v2.19.1-copy-title-to-library-items | Copies title and titleIgnorePrefix to the libraryItems table, creates update triggers and indices | From 0ccb88904a0c97c41f0031c545482f13de5c654e Mon Sep 17 00:00:00 2001 From: advplyr Date: Sun, 9 Feb 2025 17:40:29 -0600 Subject: [PATCH 023/303] fix v2.15.0 migration test --- test/server/migrations/v2.15.0-series-column-unique.test.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/server/migrations/v2.15.0-series-column-unique.test.js b/test/server/migrations/v2.15.0-series-column-unique.test.js index a9ad0fab..34b5e52e 100644 --- a/test/server/migrations/v2.15.0-series-column-unique.test.js +++ b/test/server/migrations/v2.15.0-series-column-unique.test.js @@ -129,9 +129,9 @@ describe('migration-v2.15.0-series-column-unique', () => { { id: series1Id, name: 'Series 1', libraryId: library1Id, createdAt: new Date(), updatedAt: new Date() }, { id: series2Id, name: 'Series 2', libraryId: library2Id, createdAt: new Date(), updatedAt: new Date() }, { id: series3Id, name: 'Series 3', libraryId: library1Id, createdAt: new Date(), updatedAt: new Date() }, - { id: series1Id_dup, name: 'Series 1', libraryId: library1Id, createdAt: new Date(), updatedAt: new Date() }, - { id: series3Id_dup, name: 'Series 3', libraryId: library1Id, createdAt: new Date(), updatedAt: new Date() }, - { id: series1Id_dup2, name: 'Series 1', libraryId: library1Id, createdAt: new Date(), updatedAt: new Date() } + { id: series1Id_dup, name: 'Series 1', libraryId: library1Id, createdAt: new Date(0), updatedAt: new Date(0) }, + { id: series3Id_dup, name: 'Series 3', libraryId: library1Id, createdAt: new Date(0), updatedAt: new Date(0) }, + { id: series1Id_dup2, name: 'Series 1', libraryId: library1Id, createdAt: new Date(0), updatedAt: new Date(0) } ]) // Add some entries to the BookSeries table await queryInterface.bulkInsert('BookSeries', [ From 14e92435ec95eaab2546666db41b59e70e548d53 Mon Sep 17 00:00:00 2001 From: Paul Date: Mon, 10 Feb 2025 11:41:26 +0100 Subject: [PATCH 024/303] Fix `ROUTER_BASE_PATH` override for empty string When the `ROUTER_BASE_PATH` env variable is set to an empty string it's mistakenly overriden to `/audiobookshelf` instead. The `/audiobookshelf` fallback should only be used when the `ROUTER_BASE_PATH` env variable is undefined, not just an empty string. Regression introduced in https://github.com/advplyr/audiobookshelf/pull/3810 See also: https://github.com/advplyr/audiobookshelf/pull/3810#discussion_r1948790937 Partially address https://github.com/advplyr/audiobookshelf/issues/3874 --- client/nuxt.config.js | 2 +- index.js | 4 ++-- prod.js | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/client/nuxt.config.js b/client/nuxt.config.js index bb595477..4df6246f 100644 --- a/client/nuxt.config.js +++ b/client/nuxt.config.js @@ -1,6 +1,6 @@ const pkg = require('./package.json') -const routerBasePath = process.env.ROUTER_BASE_PATH || '/audiobookshelf' +const routerBasePath = process.env.ROUTER_BASE_PATH ?? '/audiobookshelf' const serverHostUrl = process.env.NODE_ENV === 'production' ? '' : 'http://localhost:3333' const serverPaths = ['api/', 'public/', 'hls/', 'auth/', 'feed/', 'status', 'login', 'logout', 'init'] const proxy = Object.fromEntries(serverPaths.map((path) => [`${routerBasePath}/${path}`, { target: process.env.NODE_ENV !== 'production' ? serverHostUrl : '/' }])) diff --git a/index.js b/index.js index 35dcc588..fea9a590 100644 --- a/index.js +++ b/index.js @@ -29,7 +29,7 @@ if (isDev) { if (devEnv.AllowIframe) process.env.ALLOW_IFRAME = '1' if (devEnv.BackupPath) process.env.BACKUP_PATH = devEnv.BackupPath process.env.SOURCE = 'local' - process.env.ROUTER_BASE_PATH = devEnv.RouterBasePath || '' + process.env.ROUTER_BASE_PATH = devEnv.RouterBasePath ?? '' } const inputConfig = options.config ? Path.resolve(options.config) : null @@ -41,7 +41,7 @@ const CONFIG_PATH = inputConfig || process.env.CONFIG_PATH || Path.resolve('conf const METADATA_PATH = inputMetadata || process.env.METADATA_PATH || Path.resolve('metadata') const SOURCE = options.source || process.env.SOURCE || 'debian' -const ROUTER_BASE_PATH = process.env.ROUTER_BASE_PATH || '/audiobookshelf' +const ROUTER_BASE_PATH = process.env.ROUTER_BASE_PATH ?? '/audiobookshelf' console.log(`Running in ${process.env.NODE_ENV} mode.`) console.log(`Options: CONFIG_PATH=${CONFIG_PATH}, METADATA_PATH=${METADATA_PATH}, PORT=${PORT}, HOST=${HOST}, SOURCE=${SOURCE}, ROUTER_BASE_PATH=${ROUTER_BASE_PATH}`) diff --git a/prod.js b/prod.js index 24a0c3f7..b0f53719 100644 --- a/prod.js +++ b/prod.js @@ -25,7 +25,7 @@ const CONFIG_PATH = inputConfig || process.env.CONFIG_PATH || Path.resolve('conf const METADATA_PATH = inputMetadata || process.env.METADATA_PATH || Path.resolve('metadata') const SOURCE = options.source || process.env.SOURCE || 'debian' -const ROUTER_BASE_PATH = process.env.ROUTER_BASE_PATH || '/audiobookshelf' +const ROUTER_BASE_PATH = process.env.ROUTER_BASE_PATH ?? '/audiobookshelf' console.log(process.env.NODE_ENV, 'Config', CONFIG_PATH, METADATA_PATH) From 4e8cd6fba0bc7ae5c41b8a6153fda500fc879985 Mon Sep 17 00:00:00 2001 From: advplyr Date: Mon, 10 Feb 2025 17:58:18 -0600 Subject: [PATCH 025/303] Update index.js dev fallback router base path --- index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.js b/index.js index fea9a590..2839c238 100644 --- a/index.js +++ b/index.js @@ -29,7 +29,7 @@ if (isDev) { if (devEnv.AllowIframe) process.env.ALLOW_IFRAME = '1' if (devEnv.BackupPath) process.env.BACKUP_PATH = devEnv.BackupPath process.env.SOURCE = 'local' - process.env.ROUTER_BASE_PATH = devEnv.RouterBasePath ?? '' + process.env.ROUTER_BASE_PATH = devEnv.RouterBasePath ?? '/audiobookshelf' } const inputConfig = options.config ? Path.resolve(options.config) : null From ec6537656925a43871b07cfee12c9f383844d224 Mon Sep 17 00:00:00 2001 From: mikiher Date: Tue, 11 Feb 2025 22:02:51 +0200 Subject: [PATCH 026/303] Security fix for GHSA-pg8v-5jcv-wrvw --- server/Auth.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/server/Auth.js b/server/Auth.js index 74b767f5..8ece90ae 100644 --- a/server/Auth.js +++ b/server/Auth.js @@ -10,6 +10,7 @@ const ExtractJwt = require('passport-jwt').ExtractJwt const OpenIDClient = require('openid-client') const Database = require('./Database') const Logger = require('./Logger') +const { escapeRegExp } = require('./utils') /** * @class Class for handling all the authentication related functionality. @@ -18,7 +19,11 @@ class Auth { constructor() { // Map of openId sessions indexed by oauth2 state-variable this.openIdAuthSession = new Map() - this.ignorePatterns = [/\/api\/items\/[^/]+\/cover/, /\/api\/authors\/[^/]+\/image/] + const escapedRouterBasePath = escapeRegExp(global.RouterBasePath) + this.ignorePatterns = [ + new RegExp(`^(${escapedRouterBasePath}/api)?/items/[^/]+/cover$`), + new RegExp(`^(${escapedRouterBasePath}/api)?/authors/[^/]+/image$`) + ] } /** @@ -28,7 +33,7 @@ class Auth { * @private */ authNotNeeded(req) { - return req.method === 'GET' && this.ignorePatterns.some((pattern) => pattern.test(req.originalUrl)) + return req.method === 'GET' && this.ignorePatterns.some((pattern) => pattern.test(req.path)) } ifAuthNeeded(middleware) { From ed3af5bdcd7b9ef274d212794358b9042896fcbe Mon Sep 17 00:00:00 2001 From: advplyr Date: Tue, 11 Feb 2025 16:14:49 -0600 Subject: [PATCH 027/303] Fix server crash when feed cover image is requested but doesnt exist --- server/managers/RssFeedManager.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/server/managers/RssFeedManager.js b/server/managers/RssFeedManager.js index de009c3d..c4681bdc 100644 --- a/server/managers/RssFeedManager.js +++ b/server/managers/RssFeedManager.js @@ -246,6 +246,15 @@ class RssFeedManager { const extname = Path.extname(feed.coverPath).toLowerCase().slice(1) res.type(`image/${extname}`) const readStream = fs.createReadStream(feed.coverPath) + + readStream.on('error', (error) => { + Logger.error(`[RssFeedManager] Error streaming cover image: ${error.message}`) + // Only send error if headers haven't been sent yet + if (!res.headersSent) { + res.sendStatus(404) + } + }) + readStream.pipe(res) } From 39567c6c224fb1e5b9d0111461bb0cc78001f888 Mon Sep 17 00:00:00 2001 From: advplyr Date: Tue, 11 Feb 2025 16:47:34 -0600 Subject: [PATCH 028/303] Update view feed modal to sort episodes by pub date ascending --- client/pages/config/rss-feeds.vue | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/client/pages/config/rss-feeds.vue b/client/pages/config/rss-feeds.vue index 9b070f91..c3f60859 100644 --- a/client/pages/config/rss-feeds.vue +++ b/client/pages/config/rss-feeds.vue @@ -137,7 +137,16 @@ export default { this.$toast.error(this.$strings.ToastFailedToLoadData) return } - this.feeds = data.feeds + this.feeds = data.feeds.map((feed) => ({ + ...feed, + episodes: [...feed.episodes].sort((a, b) => { + if (!a.pubDate) return 1 // null dates sort to end + if (!b.pubDate) return -1 + const dateA = new Date(a.pubDate) + const dateB = new Date(b.pubDate) + return dateA - dateB + }) + })) }, init() { this.loadFeeds() From 70621e72e867a52643d59a1448875cb8743bb848 Mon Sep 17 00:00:00 2001 From: biuklija Date: Mon, 3 Feb 2025 20:51:20 +0000 Subject: [PATCH 029/303] Translated using Weblate (Croatian) Currently translated at 100.0% (1089 of 1089 strings) Translation: Audiobookshelf/Abs Web Client Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/hr/ --- client/strings/hr.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/strings/hr.json b/client/strings/hr.json index eda11cfa..9ac33476 100644 --- a/client/strings/hr.json +++ b/client/strings/hr.json @@ -641,10 +641,10 @@ "LabelTimeDurationXMinutes": "{0} minuta", "LabelTimeDurationXSeconds": "{0} sekundi", "LabelTimeInMinutes": "Vrijeme u minutama", - "LabelTimeLeft": "{0} preostalo", + "LabelTimeLeft": "preostalo {0}", "LabelTimeListened": "Vremena odslušano", "LabelTimeListenedToday": "Vremena odslušano danas", - "LabelTimeRemaining": "{0} preostalo", + "LabelTimeRemaining": "preostalo {0}", "LabelTimeToShift": "Vrijeme za pomjeriti u sekundama", "LabelTitle": "Naslov", "LabelToolsEmbedMetadata": "Ugradi meta-podatke", From 1fbd09044135b1ae290c37ee71ac82fa040a996a Mon Sep 17 00:00:00 2001 From: Jan-Eric Myhrgren Date: Mon, 3 Feb 2025 12:06:24 +0000 Subject: [PATCH 030/303] Translated using Weblate (Swedish) Currently translated at 85.8% (935 of 1089 strings) Translation: Audiobookshelf/Abs Web Client Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sv/ --- client/strings/sv.json | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/client/strings/sv.json b/client/strings/sv.json index 6180652f..39ff8a31 100644 --- a/client/strings/sv.json +++ b/client/strings/sv.json @@ -10,6 +10,8 @@ "ButtonApplyChapters": "Tillämpa kapitel", "ButtonAuthors": "Författare", "ButtonBack": "Tillbaka", + "ButtonBatchEditPopulateFromExisting": "Hämta befintlig information", + "ButtonBatchEditPopulateMapDetails": "Addera befintliga information", "ButtonBrowseForFolder": "Bläddra efter mapp", "ButtonCancel": "Avbryt", "ButtonCancelEncode": "Avbryt omkodning", @@ -97,7 +99,7 @@ "ButtonSubmit": "Spara", "ButtonTest": "Testa", "ButtonUpload": "Ladda upp", - "ButtonUploadBackup": "Ladda upp säkerhetskopia", + "ButtonUploadBackup": "Läs in säkerhetskopia", "ButtonUploadCover": "Ladda upp bokomslag", "ButtonUploadOPMLFile": "Ladda upp OPML-fil", "ButtonUserDelete": "Radera användare {0}", @@ -147,7 +149,7 @@ "HeaderLogs": "Loggar", "HeaderManageGenres": "Hantera kategorier", "HeaderManageTags": "Hantera taggar", - "HeaderMapDetails": "Karta detaljer", + "HeaderMapDetails": "Gemensam information för samtliga objekt", "HeaderMatch": "Matcha", "HeaderMetadataOrderOfPrecedence": "Prioriteringsordning vid inläsning av metadata", "HeaderMetadataToEmbed": "Metadata som kommer att adderas", @@ -235,7 +237,7 @@ "LabelBackupLocation": "Plats för säkerhetskopia", "LabelBackupsEnableAutomaticBackups": "Aktivera automatisk säkerhetskopiering", "LabelBackupsEnableAutomaticBackupsHelp": "Säkerhetskopior sparas i \"/metadata/backups\"", - "LabelBackupsMaxBackupSize": "Maximal storlek på säkerhetskopia (i GB) (0 = obegränsad)", + "LabelBackupsMaxBackupSize": "Maximal storlek på säkerhetskopia i GigaByte (0 = obegränsad)", "LabelBackupsMaxBackupSizeHelp": "Som ett skydd mot en felaktig konfiguration kommer säkerhetskopior inte att genomföras om de överskrider den konfigurerade storleken.", "LabelBackupsNumberToKeep": "Antal säkerhetskopior att behålla", "LabelBackupsNumberToKeepHelp": "Endast en gammal säkerhetskopia tas bort åt gången, så om du redan har fler säkerhetskopior än det angivna beloppet bör du ta bort dem manuellt.", @@ -404,7 +406,7 @@ "LabelNewPassword": "Nytt lösenord", "LabelNewestAuthors": "Senaste författarna", "LabelNewestEpisodes": "Senast tillagda avsnitt", - "LabelNextBackupDate": "Nästa datum för säkerhetskopiering", + "LabelNextBackupDate": "Nästa tillfälle för säkerhetskopiering", "LabelNextScheduledRun": "Nästa schemalagda körning", "LabelNoCustomMetadataProviders": "Ingen egen källa för metadata", "LabelNoEpisodesSelected": "Inga avsnitt valda", @@ -464,8 +466,8 @@ "LabelRead": "Läst", "LabelReadAgain": "Läs igen", "LabelReadEbookWithoutProgress": "Läs e-bok utan att behålla framsteg", - "LabelRecentSeries": "Nyaste serierna", - "LabelRecentlyAdded": "Nyligen tillagda", + "LabelRecentSeries": "Senaste serierna", + "LabelRecentlyAdded": "Nyligen adderade", "LabelRecommended": "Rekommenderad", "LabelRegion": "Region", "LabelReleaseDate": "Utgivningsdatum", @@ -641,6 +643,8 @@ "MessageBackupsLocationEditNote": "OBS: När du ändrar plats för säkerhetskopiorna så flyttas INTE gamla säkerhetskopior dit.", "MessageBackupsLocationNoEditNote": "OBS: Platsen där säkerhetskopiorna lagras bestäms av en central inställning och kan inte ändras här.", "MessageBackupsLocationPathEmpty": "Uppgiften om platsen för lagring av säkerhetskopior kan inte lämnas tom", + "MessageBatchEditPopulateMapDetailsAllHelp": "Adderar information från alla objekt nedan i de fält som aktiverats. Om fälten innehåller olika uppgifter kommer informationen att slås samman.", + "MessageBatchEditPopulateMapDetailsItemHelp": "Addera information från detta objekt i aktiva fält ovan", "MessageBatchQuickMatchDescription": "Quick Match kommer försöka lägga till saknade omslag och metadata för de valda föremålen. Aktivera alternativen nedan för att tillåta Quick Match att överskriva befintliga omslag och/eller metadata.", "MessageBookshelfNoCollections": "Du har ännu inte skapat några samlingar", "MessageBookshelfNoRSSFeeds": "Inga RSS-flöden är öppna", @@ -856,6 +860,7 @@ "ToastDeviceTestEmailSuccess": "Ett testmail har skickats", "ToastEmailSettingsUpdateSuccess": "Inställningarna av e-post har uppdaterats", "ToastEncodeCancelSucces": "Omkodningen avbruten", + "ToastEpisodeUpdateSuccess": "{0} avsnitt uppdaterades", "ToastFailedToLoadData": "Misslyckades med att ladda data", "ToastFailedToUpdate": "Misslyckades med att uppdatera", "ToastInvalidImageUrl": "Felaktig URL-adress till omslagsbilden", @@ -869,6 +874,7 @@ "ToastItemMarkedAsFinishedSuccess": "Den har markerat som avslutad", "ToastItemMarkedAsNotFinishedFailed": "Misslyckades med att markera den som ej avslutad", "ToastItemMarkedAsNotFinishedSuccess": "Den har markerats som ej avslutad", + "ToastItemUpdateSuccess": "Objektet har uppdaterats", "ToastLibraryCreateFailed": "Det gick inte att skapa biblioteket", "ToastLibraryCreateSuccess": "Biblioteket \"{0}\" har skapats", "ToastLibraryDeleteFailed": "Det gick inte att ta bort biblioteket", @@ -893,7 +899,7 @@ "ToastPlaylistCreateFailed": "Det gick inte att skapa spellistan", "ToastPlaylistCreateSuccess": "Spellistan skapad", "ToastPlaylistRemoveSuccess": "Spellistan har tagits bort", - "ToastPlaylistUpdateSuccess": "Spellistan uppdaterad", + "ToastPlaylistUpdateSuccess": "Spellistan har uppdaterats", "ToastPodcastCreateFailed": "Misslyckades med att skapa podcasten", "ToastPodcastCreateSuccess": "Podcasten skapad framgångsrikt", "ToastProviderCreatedFailed": "Misslyckades med att addera en källa", From bf795d36621d430fe78988bb4bc089dbb30822b4 Mon Sep 17 00:00:00 2001 From: Jan-Eric Myhrgren Date: Tue, 4 Feb 2025 18:31:20 +0000 Subject: [PATCH 031/303] Translated using Weblate (Swedish) Currently translated at 85.9% (936 of 1089 strings) Translation: Audiobookshelf/Abs Web Client Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sv/ --- client/strings/sv.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/client/strings/sv.json b/client/strings/sv.json index 39ff8a31..e7b32e38 100644 --- a/client/strings/sv.json +++ b/client/strings/sv.json @@ -720,7 +720,7 @@ "MessageMarkAllEpisodesNotFinished": "Markera alla avsnitt som ej avslutade", "MessageMarkAsFinished": "Markera som avslutad", "MessageMarkAsNotFinished": "Markera som ej avslutad", - "MessageMatchBooksDescription": "kommer att försöka matcha böcker i biblioteket med en bok från
den valda källan och fylla i uppgifter som saknas och bokomslag.
Inga befintliga uppgifter kommer att ersättas.", + "MessageMatchBooksDescription": "kommer att försöka matcha böcker i biblioteket med en bok från
den valda källan och fylla i uppgifter som saknas och bokomslag.
Inga befintliga uppgifter kommer att ersättas.", "MessageNoAudioTracks": "Inga ljudspår har hittats", "MessageNoAuthors": "Inga författare", "MessageNoBackups": "Inga säkerhetskopior", @@ -779,6 +779,7 @@ "MessageTaskFailedToCreateCacheDirectory": "Misslyckades med att skapa bibliotek för cachen", "MessageTaskMatchingBooksInLibrary": "Matchar böcker i biblioteket \"{0}\"", "MessageTaskScanItemsAdded": "{0} adderades", + "MessageTaskScanItemsMissing": "{0} saknades", "MessageTaskScanItemsUpdated": "{0} uppdaterades", "MessageTaskScanNoChangesNeeded": "Inget adderades eller uppdaterades", "MessageTaskScanningLibrary": "Biblioteket \"{0}\" har skannats", From 78d8c83e6db3601033d6c47db662831267d5afa4 Mon Sep 17 00:00:00 2001 From: Jan-Eric Myhrgren Date: Wed, 5 Feb 2025 10:53:25 +0000 Subject: [PATCH 032/303] Translated using Weblate (Swedish) Currently translated at 85.9% (936 of 1089 strings) Translation: Audiobookshelf/Abs Web Client Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sv/ --- client/strings/sv.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/client/strings/sv.json b/client/strings/sv.json index e7b32e38..29f26445 100644 --- a/client/strings/sv.json +++ b/client/strings/sv.json @@ -136,7 +136,7 @@ "HeaderFiles": "Filer", "HeaderFindChapters": "Hitta kapitel", "HeaderIgnoredFiles": "Ignorerade filer", - "HeaderItemFiles": "Föremålsfiler", + "HeaderItemFiles": "Filer", "HeaderItemMetadataUtils": "Metadataverktyg för föremål", "HeaderLastListeningSession": "Senaste lyssningstillfället", "HeaderLatestEpisodes": "Senaste avsnitten", @@ -319,8 +319,8 @@ "LabelFeedURL": "Flödes-URL", "LabelFetchingMetadata": "Hämtar metadata", "LabelFile": "Fil", - "LabelFileBirthtime": "Tidpunkt, filen skapades", - "LabelFileModified": "Tidpunkt, filen ändrades", + "LabelFileBirthtime": "Tidpunkt, fil skapad", + "LabelFileModified": "Tidpunkt, fil ändrad", "LabelFileModifiedDate": "Ändrad {0}", "LabelFilename": "Filnamn", "LabelFilterByUser": "Välj användare", @@ -870,7 +870,7 @@ "ToastItemCoverUpdateSuccess": "Objektets bokomslag har uppdaterats", "ToastItemDeletedFailed": "Misslyckades med att radera objektet", "ToastItemDeletedSuccess": "Objektet har raderats", - "ToastItemDetailsUpdateSuccess": "Detaljerna om boken har uppdaterats", + "ToastItemDetailsUpdateSuccess": "Informationen om boken har uppdaterats", "ToastItemMarkedAsFinishedFailed": "Misslyckades med att markera den som avslutad", "ToastItemMarkedAsFinishedSuccess": "Den har markerat som avslutad", "ToastItemMarkedAsNotFinishedFailed": "Misslyckades med att markera den som ej avslutad", From 74b35ea9d1c6febe49f9089771d975fc0dc0c908 Mon Sep 17 00:00:00 2001 From: Jan-Eric Myhrgren Date: Thu, 6 Feb 2025 10:11:19 +0000 Subject: [PATCH 033/303] Translated using Weblate (Swedish) Currently translated at 88.7% (966 of 1089 strings) Translation: Audiobookshelf/Abs Web Client Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sv/ --- client/strings/sv.json | 84 ++++++++++++++++++++++++++++-------------- 1 file changed, 57 insertions(+), 27 deletions(-) diff --git a/client/strings/sv.json b/client/strings/sv.json index 29f26445..d616efe7 100644 --- a/client/strings/sv.json +++ b/client/strings/sv.json @@ -16,7 +16,7 @@ "ButtonCancel": "Avbryt", "ButtonCancelEncode": "Avbryt omkodning", "ButtonChangeRootPassword": "Ändra lösenordet för root", - "ButtonCheckAndDownloadNewEpisodes": "Kontrollera och ladda ner nya avsnitt", + "ButtonCheckAndDownloadNewEpisodes": "Sök & Ladda ner nya avsnitt", "ButtonChooseAFolder": "Välj en mapp", "ButtonChooseFiles": "Välj filer", "ButtonClearFilter": "Rensa filter", @@ -32,7 +32,7 @@ "ButtonEditChapters": "Redigera kapitel", "ButtonEditPodcast": "Redigera podcast", "ButtonEnable": "Aktivera", - "ButtonForceReScan": "Tvinga omstart", + "ButtonForceReScan": "Starta ny skanning", "ButtonFullPath": "Fullständig sökväg", "ButtonHide": "Dölj", "ButtonHome": "Hem", @@ -66,8 +66,8 @@ "ButtonPurgeItemsCache": "Rensa cache för föremål", "ButtonQueueAddItem": "Lägg till i kön", "ButtonQueueRemoveItem": "Ta bort från kön", - "ButtonQuickMatch": "Snabb matchning", - "ButtonReScan": "Omstart", + "ButtonQuickMatch": "Snabbmatchning", + "ButtonReScan": "Ny skanning", "ButtonRead": "Läs", "ButtonReadLess": "Visa mindre", "ButtonReadMore": "Visa mer", @@ -137,7 +137,7 @@ "HeaderFindChapters": "Hitta kapitel", "HeaderIgnoredFiles": "Ignorerade filer", "HeaderItemFiles": "Filer", - "HeaderItemMetadataUtils": "Metadataverktyg för föremål", + "HeaderItemMetadataUtils": "Metadataverktyg", "HeaderLastListeningSession": "Senaste lyssningstillfället", "HeaderLatestEpisodes": "Senaste avsnitten", "HeaderLibraries": "Bibliotek", @@ -170,11 +170,11 @@ "HeaderRSSFeedGeneral": "RSS-information", "HeaderRSSFeedIsOpen": "RSS-flödet är öppet", "HeaderRSSFeeds": "RSS-flöden", - "HeaderRemoveEpisode": "Ta bort avsnitt", - "HeaderRemoveEpisodes": "Ta bort {0} avsnitt", + "HeaderRemoveEpisode": "Radera avsnitt", + "HeaderRemoveEpisodes": "Radera {0} avsnitt", "HeaderSavedMediaProgress": "Sparad historik", "HeaderSchedule": "Schema", - "HeaderScheduleEpisodeDownloads": "Schemalägg automatiska avsnittsnedladdningar", + "HeaderScheduleEpisodeDownloads": "Schemalägg automatiska nedladdning av avsnitt", "HeaderScheduleLibraryScans": "Schema för skanning av biblioteket", "HeaderSession": "Tillfälle", "HeaderSetBackupSchedule": "Ange schemaläggning för säkerhetskopia", @@ -200,7 +200,7 @@ "HeaderUsers": "Användare", "HeaderYearReview": "Sammanställning av {0}", "HeaderYourStats": "Din statistik", - "LabelAbridged": "Förkortad", + "LabelAbridged": "Förkortad version", "LabelAccessibleBy": "Tillgänglig för", "LabelAccountType": "Kontotyp", "LabelAccountTypeAdmin": "Administratör", @@ -240,7 +240,7 @@ "LabelBackupsMaxBackupSize": "Maximal storlek på säkerhetskopia i GigaByte (0 = obegränsad)", "LabelBackupsMaxBackupSizeHelp": "Som ett skydd mot en felaktig konfiguration kommer säkerhetskopior inte att genomföras om de överskrider den konfigurerade storleken.", "LabelBackupsNumberToKeep": "Antal säkerhetskopior att behålla", - "LabelBackupsNumberToKeepHelp": "Endast en gammal säkerhetskopia tas bort åt gången, så om du redan har fler säkerhetskopior än det angivna beloppet bör du ta bort dem manuellt.", + "LabelBackupsNumberToKeepHelp": "Endast en gammal säkerhetskopia tas bort åt gången, så om du redan har fler säkerhetskopior än det angivna värdet bör du ta bort dem manuellt.", "LabelBitrate": "Bitfrekvens", "LabelBonus": "Bonus", "LabelBooks": "Böcker", @@ -312,11 +312,16 @@ "LabelEnd": "Slut", "LabelEndOfChapter": "Slut av kapitel", "LabelEpisode": "Avsnitt", - "LabelEpisodeTitle": "Avsnittsrubrik", - "LabelEpisodeType": "Avsnittstyp", + "LabelEpisodeNumber": "Avsnitt #{0}", + "LabelEpisodeTitle": "Titel på avsnittet", + "LabelEpisodeType": "Typ av avsnitt", + "LabelEpisodes": "Avsnitt", "LabelExample": "Exempel", "LabelExpandSeries": "Expandera serier", - "LabelFeedURL": "Flödes-URL", + "LabelExplicit": "Explicit version", + "LabelExplicitChecked": "Explicit version (markerad)", + "LabelExplicitUnchecked": "Ej Explicit version (ej markerad)", + "LabelFeedURL": "URL-adress för flödet", "LabelFetchingMetadata": "Hämtar metadata", "LabelFile": "Fil", "LabelFileBirthtime": "Tidpunkt, fil skapad", @@ -324,14 +329,14 @@ "LabelFileModifiedDate": "Ändrad {0}", "LabelFilename": "Filnamn", "LabelFilterByUser": "Välj användare", - "LabelFindEpisodes": "Hitta avsnitt", + "LabelFindEpisodes": "Sök avsnitt", "LabelFinished": "Avslutad", "LabelFolder": "Mapp", "LabelFolders": "Mappar", "LabelFontBold": "Fetstil", "LabelFontBoldness": "Fetstil", "LabelFontFamily": "Typsnittsfamilj", - "LabelFontItalic": "Kursiverad", + "LabelFontItalic": "Kursiv", "LabelFontScale": "Skala på typsnitt", "LabelFontStrikethrough": "Genomstruken", "LabelGenre": "Kategori", @@ -386,6 +391,9 @@ "LabelLogLevelWarn": "Varningar", "LabelLookForNewEpisodesAfterDate": "Sök efter nya avsnitt efter detta datum", "LabelLowestPriority": "Lägst prioritet", + "LabelMaxEpisodesToDownload": "Maximalt antal avsnitt att ladda ner (0 = obegränsat).", + "LabelMaxEpisodesToDownloadPerCheck": "Maximalt antal nya avsnitt att ladda ner per tillfälle", + "LabelMaxEpisodesToKeep": "Maximalt antal avsnitt att behålla", "LabelMediaPlayer": "Mediaspelare", "LabelMediaType": "Mediatyp", "LabelMetaTag": "Metadata", @@ -431,7 +439,7 @@ "LabelPath": "Sökväg", "LabelPermissionsAccessAllLibraries": "Kan komma åt alla bibliotek", "LabelPermissionsAccessAllTags": "Kan komma åt alla taggar", - "LabelPermissionsAccessExplicitContent": "Kan komma åt explicit innehåll", + "LabelPermissionsAccessExplicitContent": "Kan komma åt explicit version", "LabelPermissionsCreateEreader": "Kan addera e-läsarenhet", "LabelPermissionsDelete": "Kan radera", "LabelPermissionsDownload": "Kan ladda ner", @@ -444,7 +452,7 @@ "LabelPlaylists": "Spellistor", "LabelPodcast": "Podcast", "LabelPodcastSearchRegion": "Podcast-sökområde", - "LabelPodcastType": "Podcasttyp", + "LabelPodcastType": "Typ av postcast", "LabelPodcasts": "Podcasts", "LabelPort": "Port", "LabelPrefixesToIgnore": "Prefix att ignorera (skiftlägesokänsligt)", @@ -469,6 +477,7 @@ "LabelRecentSeries": "Senaste serierna", "LabelRecentlyAdded": "Nyligen adderade", "LabelRecommended": "Rekommenderad", + "LabelRedo": "Gör om", "LabelRegion": "Region", "LabelReleaseDate": "Utgivningsdatum", "LabelRemoveAllMetadataAbs": "Radera alla 'metadata.abs' filer", @@ -502,16 +511,16 @@ "LabelSettingsDateFormat": "Datumformat", "LabelSettingsDisableWatcher": "Inaktivera Watcher", "LabelSettingsDisableWatcherForLibrary": "Inaktivera bevakning med Watcher för biblioteket", - "LabelSettingsDisableWatcherHelp": "Inaktiverar automatik att addera/uppdatera objekt
när ändringar av filer genomförs.
OBS: Kräver en omstart av servern", + "LabelSettingsDisableWatcherHelp": "Inaktiverar automatik att addera/uppdatera
objekt när ändringar av filer genomförs.
OBS: Kräver en omstart av servern", "LabelSettingsEnableWatcher": "Aktivera Watcher", "LabelSettingsEnableWatcherForLibrary": "Aktivera bevakning med Watcher för biblioteket", - "LabelSettingsEnableWatcherHelp": "Aktiverar automatik att addera/uppdatera objekt
när ändringar av filer genomförs.
OBS: Kräver en omstart av servern", + "LabelSettingsEnableWatcherHelp": "Aktiverar automatik att addera/uppdatera
objekt när ändringar av filer genomförs.
OBS: Kräver en omstart av servern", "LabelSettingsEpubsAllowScriptedContent": "Tillåt e-böcker i epubs-format som innehåller script", "LabelSettingsEpubsAllowScriptedContentHelp": "Tillåt att epub-filer får använda script.
Det rekommenderas att denna inställning är
avstängd när du inte litar på källan för epub-filerna.", "LabelSettingsExperimentalFeatures": "Experimentella funktioner", "LabelSettingsExperimentalFeaturesHelp": "Funktioner under utveckling som behöver din feedback och hjälp med testning. Klicka för att öppna diskussionen på GitHub.", "LabelSettingsFindCovers": "Hitta ett bokomslag", - "LabelSettingsFindCoversHelp": "Om din bok inte har ett bokomslag inbäddat i filen eller en fil med bokomslaget i mappen kommer skannern att försöka hitta ett omslag. OBS: Detta kommer att förlänga inläsningstiden", + "LabelSettingsFindCoversHelp": "Om din bok INTE har ett bokomslag inbäddat i filen eller en fil med bokomslaget i mappen kommer skannern att försöka hitta ett omslag.
OBS: Detta kommer att förlänga inläsningstiden", "LabelSettingsHideSingleBookSeries": "Dölj serier som endast innehåller en bok", "LabelSettingsHideSingleBookSeriesHelp": "Serier som endast har en bok kommer att
döljas från sidan 'Serier' och hyllorna på startsidan.", "LabelSettingsHomePageBookshelfView": "Använd vy liknande en bokhylla på startsidan", @@ -522,9 +531,9 @@ "LabelSettingsOnlyShowLaterBooksInContinueSeries": "Hoppa över tidigare böcker i en serie", "LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "Sektionen 'Fortsätt med serien' på startsidan visar \"nästa bok\" i serien,
där åtminstone en bok avslutats, och ingen bok i serien har påbörjats.
Om detta alternativ aktiveras kommer efterföljande bok till den
avslutade att föreslås - istället för den första ej avslutade boken i serien.", "LabelSettingsParseSubtitles": "Hämta undertitel från bokens mapp", - "LabelSettingsParseSubtitlesHelp": "Hämtar undertiteln från namnet på mappen där boken lagras.
Undertiteln måste vara åtskilda med ett bindestreck ' - '.
En mapp med namnet 'Boktitel - Bokens undertitel'
får undertiteln \"Bokens undertitel\"", + "LabelSettingsParseSubtitlesHelp": "Hämtar undertiteln från namnet
på mappen där boken lagras.
Undertiteln måste vara åtskilda med ett bindestreck ' - '.
En mapp med namnet 'Boktitel - Bokens undertitel'
får undertiteln \"Bokens undertitel\"", "LabelSettingsPreferMatchedMetadata": "Prioritera matchad metadata", - "LabelSettingsPreferMatchedMetadataHelp": "Matchad data kommer att åsidosätta objektdetaljer vid snabbmatchning. Som standard kommer snabbmatchning endast att fylla i saknade detaljer.", + "LabelSettingsPreferMatchedMetadataHelp": "Matchad data kommer att ersätta befintliga uppgifter vid en snabbmatchning. Som standard kommer en snabbmatchning endast att fylla i saknade detaljer.", "LabelSettingsSkipMatchingBooksWithASIN": "Hoppa över matchande böcker som har en ASIN-kod", "LabelSettingsSkipMatchingBooksWithISBN": "Hoppa över matchande böcker som har en ISBN-kod", "LabelSettingsSortingIgnorePrefixes": "Ignorera prefix vid sortering", @@ -571,6 +580,7 @@ "LabelTagsNotAccessibleToUser": "Taggar inte tillgängliga för användaren", "LabelTasks": "Pågående aktivitet", "LabelTextEditorBulletedList": "Punktlista", + "LabelTextEditorLink": "Länk", "LabelTextEditorNumberedList": "Numrerad lista", "LabelTheme": "Utseende", "LabelThemeDark": "Mörkt", @@ -639,7 +649,7 @@ "LabelYourProgress": "Framsteg", "MessageAddToPlayerQueue": "Lägg till i spellistan", "MessageAppriseDescription": "För att använda den här funktionen behöver du ha en instans av Apprise API igång eller en API som hanterar dessa begäranden.
Apprise API-urlen bör vara hela URL-sökvägen för att skicka meddelandet, t.ex., om din API-instans är tillgänglig på http://192.168.1.1:8337, bör du ange http://192.168.1.1:8337/notify.", - "MessageBackupsDescription": "Säkerhetskopior inkluderar användare, användarnas framsteg, biblioteksobjekt, serverinställningar
och bilder lagrade i /metadata/items & /metadata/authors.
De inkluderar INTE några filer lagrade i dina biblioteksmappar.", + "MessageBackupsDescription": "Säkerhetskopior inkluderar användare, användarnas framsteg, biblioteksobjekt,
serverinställningar och bilder lagrade i /metadata/items & /metadata/authors.
De inkluderar INTE några filer lagrade i dina biblioteksmappar.", "MessageBackupsLocationEditNote": "OBS: När du ändrar plats för säkerhetskopiorna så flyttas INTE gamla säkerhetskopior dit.", "MessageBackupsLocationNoEditNote": "OBS: Platsen där säkerhetskopiorna lagras bestäms av en central inställning och kan inte ändras här.", "MessageBackupsLocationPathEmpty": "Uppgiften om platsen för lagring av säkerhetskopior kan inte lämnas tom", @@ -647,6 +657,7 @@ "MessageBatchEditPopulateMapDetailsItemHelp": "Addera information från detta objekt i aktiva fält ovan", "MessageBatchQuickMatchDescription": "Quick Match kommer försöka lägga till saknade omslag och metadata för de valda föremålen. Aktivera alternativen nedan för att tillåta Quick Match att överskriva befintliga omslag och/eller metadata.", "MessageBookshelfNoCollections": "Du har ännu inte skapat några samlingar", + "MessageBookshelfNoCollectionsHelp": "Samlingar är privata. Endast den användare som skapat en samling kan se den.", "MessageBookshelfNoRSSFeeds": "Inga RSS-flöden är öppna", "MessageBookshelfNoResultsForFilter": "Inga resultat för filter \"{0}: {1}\"", "MessageBookshelfNoResultsForQuery": "Sökningen gav inget resultat", @@ -678,7 +689,7 @@ "MessageConfirmPurgeCache": "När du rensar cashen kommer katalogen /metadata/cache att raderas.

Är du säker på att du vill radera katalogen?", "MessageConfirmPurgeItemsCache": "När du rensar cashen för föremål kommer katalogen /metadata/cache/items att raderas.

Är du säker på att du vill radera katalogen?", "MessageConfirmQuickEmbed": "VARNING! Quick embed kommer inte att säkerhetskopiera dina ljudfiler. Se till att du har en säkerhetskopia av dina ljudfiler.

Vill du fortsätta?", - "MessageConfirmReScanLibraryItems": "Är du säker på att du vill göra en ny genomsökning för {0} objekt?", + "MessageConfirmReScanLibraryItems": "Är du säker på att du vill göra en ny skanning för {0} objekt?", "MessageConfirmRemoveAllChapters": "Är du säker på att du vill ta bort alla kapitel?", "MessageConfirmRemoveAuthor": "Är du säker på att du vill ta bort författaren \"{0}\"?", "MessageConfirmRemoveCollection": "Är du säker på att du vill ta bort samlingen \"{0}\"?", @@ -701,7 +712,7 @@ "MessageDragFilesIntoTrackOrder": "Dra filer till rätt spårordning", "MessageEmbedFinished": "Inbäddning genomförd!", "MessageEpisodesQueuedForDownload": "{0} avsnitt i kö för nedladdning", - "MessageEreaderDevices": "För att säkerställa överföring av e-böcker kan du bli tvungen
att addera ovanstående e-postadress som godkänd
avsändare för varje enhet angiven nedan.", + "MessageEreaderDevices": "För att säkerställa överföring av e-böcker kan du bli tvungen
att addera ovanstående e-postadress som godkänd avsändare
för varje enhet angiven nedan.", "MessageFeedURLWillBe": "Flödes-URL kommer att vara {0}", "MessageFetching": "Hämtar...", "MessageForceReScanDescription": "kommer att göra en omgångssökning av alla filer som en färsk sökning. ID3-taggar för ljudfiler, OPF-filer och textfiler kommer att sökas som nya.", @@ -751,6 +762,7 @@ "MessageNoTasksRunning": "Inga pågående uppgifter", "MessageNoUpdatesWereNecessary": "Inga uppdateringar var nödvändiga", "MessageNoUserPlaylists": "Du har inga spellistor", + "MessageNoUserPlaylistsHelp": "Spellistor är privata. Endast den användare som skapat listan kan se den.", "MessageNotYetImplemented": "Ännu inte implementerad", "MessageOr": "eller", "MessagePauseChapter": "Pausa kapiteluppspelning", @@ -774,9 +786,15 @@ "MessageSetChaptersFromTracksDescription": "Ställ in kapitel med varje ljudfil som ett kapitel och kapitelrubrik som ljudfilens namn", "MessageStartPlaybackAtTime": "Starta uppspelning av \"{0}\" vid tidpunkt {1}?", "MessageTaskCanceledByUser": "Uppgiften avslutades av användaren", + "MessageTaskEmbeddingMetadata": "Infogar metadata", + "MessageTaskEmbeddingMetadataDescription": "Infogar metadata i ljudboken \"{0}\"", "MessageTaskEncodingM4bDescription": "Omkodning av ljudbok \"{0}\" till en M4B-fil", "MessageTaskFailed": "Misslyckades", "MessageTaskFailedToCreateCacheDirectory": "Misslyckades med att skapa bibliotek för cachen", + "MessageTaskFailedToEmbedMetadataInFile": "Misslyckades med att infoga metadata i \"{0}\"", + "MessageTaskFailedToMergeAudioFiles": "Misslyckades med att sammanfoga ljudfilerna", + "MessageTaskFailedToMoveM4bFile": "Misslyckades med att flytta M4B-filen", + "MessageTaskFailedToWriteMetadataFile": "Misslyckades med att skapa filen med metadata", "MessageTaskMatchingBooksInLibrary": "Matchar böcker i biblioteket \"{0}\"", "MessageTaskScanItemsAdded": "{0} adderades", "MessageTaskScanItemsMissing": "{0} saknades", @@ -870,7 +888,7 @@ "ToastItemCoverUpdateSuccess": "Objektets bokomslag har uppdaterats", "ToastItemDeletedFailed": "Misslyckades med att radera objektet", "ToastItemDeletedSuccess": "Objektet har raderats", - "ToastItemDetailsUpdateSuccess": "Informationen om boken har uppdaterats", + "ToastItemDetailsUpdateSuccess": "Informationen om objektet har uppdaterats", "ToastItemMarkedAsFinishedFailed": "Misslyckades med att markera den som avslutad", "ToastItemMarkedAsFinishedSuccess": "Den har markerat som avslutad", "ToastItemMarkedAsNotFinishedFailed": "Misslyckades med att markera den som ej avslutad", @@ -883,26 +901,31 @@ "ToastLibraryScanFailedToStart": "Misslyckades med att starta skanningen", "ToastLibraryScanStarted": "Skanning av biblioteket påbörjad", "ToastLibraryUpdateSuccess": "Biblioteket \"{0}\" har uppdaterats", + "ToastMatchAllAuthorsFailed": "Misslyckades med att matcha alla författare", "ToastMetadataFilesRemovedError": "Misslyckades med att radera 'metadata.{0}' filerna", "ToastMetadataFilesRemovedNoneFound": "Inga 'metadata.{0}' filer hittades i biblioteket", "ToastMetadataFilesRemovedNoneRemoved": "Inga 'metadata.{0}' filer raderades", "ToastMetadataFilesRemovedSuccess": "{0} 'metadata.{1}' raderades", "ToastNameEmailRequired": "Ett namn och en e-postadress måste anges", "ToastNameRequired": "Ett namn måste anges", + "ToastNewEpisodesFound": "Hittade {0} nya avsnitt", "ToastNewUserCreatedFailed": "Misslyckades med att skapa kontot \"{0}\"", "ToastNewUserCreatedSuccess": "Ett nytt konto har skapats", "ToastNewUserLibraryError": "Minst ett bibliotek måste anges", "ToastNewUserPasswordError": "Ett lösenord måste anges. Endast användaren 'root' kan vara utan lösenord.", + "ToastNewUserTagError": "Minst en tagg måste läggas till", "ToastNewUserUsernameError": "Ange ett användarnamn", + "ToastNoNewEpisodesFound": "Inga nya avsnitt kunde hittas", "ToastNoUpdatesNecessary": "Inga uppdateringar var nödvändiga", "ToastNotificationCreateFailed": "Misslyckades med att skapa meddelandet", "ToastNotificationDeleteFailed": "Misslyckades med att radera meddelandet", + "ToastNotificationUpdateSuccess": "Meddelandet har uppdaterats", "ToastPlaylistCreateFailed": "Det gick inte att skapa spellistan", "ToastPlaylistCreateSuccess": "Spellistan skapad", "ToastPlaylistRemoveSuccess": "Spellistan har tagits bort", "ToastPlaylistUpdateSuccess": "Spellistan har uppdaterats", "ToastPodcastCreateFailed": "Misslyckades med att skapa podcasten", - "ToastPodcastCreateSuccess": "Podcasten skapad framgångsrikt", + "ToastPodcastCreateSuccess": "Podcasten skapades framgångsrikt", "ToastProviderCreatedFailed": "Misslyckades med att addera en källa", "ToastProviderCreatedSuccess": "En ny källa har adderats", "ToastProviderNameAndUrlRequired": "Ett namn och en URL-adress krävs", @@ -912,7 +935,14 @@ "ToastRemoveFailed": "Misslyckades med att radera", "ToastRemoveItemFromCollectionFailed": "Misslyckades med att ta bort objektet från samlingen", "ToastRemoveItemFromCollectionSuccess": "Objektet borttaget från samlingen", + "ToastRemoveItemsWithIssuesFailed": "Misslyckades med att radera objekt med problem", + "ToastRemoveItemsWithIssuesSuccess": "Raderade objekt med problem", "ToastRenameFailed": "Misslyckades med att ändra namn", + "ToastRescanFailed": "Skanningen misslyckades för {0}", + "ToastRescanRemoved": "Skanningen har genomförts - objektet har raderats", + "ToastRescanUpToDate": "Skanningen har genomförts - objektet behövde inte uppdateras", + "ToastRescanUpdated": "Skanningen har genomförts - objektet har uppdaterats", + "ToastScanFailed": "Misslyckades med att skanna biblioteket", "ToastSelectAtLeastOneUser": "Åtminstone en användare måste väljas", "ToastSendEbookToDeviceFailed": "Misslyckades med att skicka e-boken till enheten", "ToastSendEbookToDeviceSuccess": "E-boken skickad till enheten \"{0}\"", From a14c6a3a8b1bc298d78b302b5b84947224638a22 Mon Sep 17 00:00:00 2001 From: Pepijn Date: Thu, 6 Feb 2025 20:53:14 +0000 Subject: [PATCH 034/303] Translated using Weblate (Dutch) Currently translated at 99.8% (1087 of 1089 strings) Translation: Audiobookshelf/Abs Web Client Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/nl/ --- client/strings/nl.json | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/client/strings/nl.json b/client/strings/nl.json index b2965bb3..9ab6977c 100644 --- a/client/strings/nl.json +++ b/client/strings/nl.json @@ -484,6 +484,7 @@ "LabelPersonalYearReview": "Jouw jaar in review ({0})", "LabelPhotoPathURL": "Foto pad/URL", "LabelPlayMethod": "Afspeelwijze", + "LabelPlaybackRateIncrementDecrement": "Afspeel Snelheid Vermeerderen/Verminderen", "LabelPlayerChapterNumberMarker": "{0} van {1}", "LabelPlaylists": "Afspeellijsten", "LabelPodcast": "Podcast", @@ -704,8 +705,11 @@ "MessageBackupsLocationEditNote": "Let op: het bijwerken van de back-uplocatie zal bestaande back-ups niet verplaatsen of wijzigen", "MessageBackupsLocationNoEditNote": "Let op: De back-uplocatie wordt ingesteld via een omgevingsvariabele en kan hier niet worden gewijzigd.", "MessageBackupsLocationPathEmpty": "Backup locatie pad kan niet leeg zijn", + "MessageBatchEditPopulateMapDetailsAllHelp": "Vul actieve velden in met data van alle items. Velden met meerdere waarden zullen worden samengevoegd", + "MessageBatchEditPopulateMapDetailsItemHelp": "Vul actieve folder detail velden met de data van dit item", "MessageBatchQuickMatchDescription": "Quick Match zal proberen ontbrekende covers en metadata voor de geselecteerde onderdelen te matchten. Schakel de opties hieronder in om Quick Match toe te staan bestaande covers en/of metadata te overschrijven.", "MessageBookshelfNoCollections": "Je hebt nog geen collecties gemaakt", + "MessageBookshelfNoCollectionsHelp": "Collecties zijn publiekelijk. Alle gebruikers met toegang tot de bibliotheek kunnen ze zien.", "MessageBookshelfNoRSSFeeds": "Geen RSS-feeds geopend", "MessageBookshelfNoResultsForFilter": "Geen resultaten voor filter \"{0}: {1}\"", "MessageBookshelfNoResultsForQuery": "Geen resultaten voor query", @@ -816,6 +820,7 @@ "MessageNoTasksRunning": "Geen lopende taken", "MessageNoUpdatesWereNecessary": "Geen bijwerkingen waren noodzakelijk", "MessageNoUserPlaylists": "Je hebt geen afspeellijsten", + "MessageNoUserPlaylistsHelp": "Afspeellijsten zijn privaat. Alleen de gebruikers die ze hebben gemaakt kunnen ze zien.", "MessageNotYetImplemented": "Nog niet geimplementeerd", "MessageOpmlPreviewNote": "Let op: Dit is een preview van het geparseerde OPML-bestand. De werkelijke podcasttitel wordt overgenomen uit de RSS-feed.", "MessageOr": "of", From ac8324e595d28af60f9b75e30acca449075d66f8 Mon Sep 17 00:00:00 2001 From: Jan-Eric Myhrgren Date: Thu, 6 Feb 2025 10:49:59 +0000 Subject: [PATCH 035/303] Translated using Weblate (Swedish) Currently translated at 90.1% (982 of 1089 strings) Translation: Audiobookshelf/Abs Web Client Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sv/ --- client/strings/sv.json | 74 +++++++++++++++++++++++++----------------- 1 file changed, 45 insertions(+), 29 deletions(-) diff --git a/client/strings/sv.json b/client/strings/sv.json index d616efe7..1bc566a5 100644 --- a/client/strings/sv.json +++ b/client/strings/sv.json @@ -75,8 +75,8 @@ "ButtonRemove": "Ta bort", "ButtonRemoveAll": "Ta bort alla", "ButtonRemoveAllLibraryItems": "Ta bort alla objekt i biblioteket", - "ButtonRemoveFromContinueListening": "Radera från 'Fortsätt läsa/lyssna'", - "ButtonRemoveFromContinueReading": "Ta bort från Fortsätt läsa", + "ButtonRemoveFromContinueListening": "Radera från 'Fortsätt lyssna'", + "ButtonRemoveFromContinueReading": "Radera från 'Fortsätt läsa'", "ButtonRemoveSeriesFromContinueSeries": "Radera från 'Fortsätt med serien'", "ButtonReset": "Tillbaka", "ButtonResetToDefault": "Återställ till standard", @@ -100,7 +100,7 @@ "ButtonTest": "Testa", "ButtonUpload": "Ladda upp", "ButtonUploadBackup": "Läs in säkerhetskopia", - "ButtonUploadCover": "Ladda upp bokomslag", + "ButtonUploadCover": "Ladda upp omslag", "ButtonUploadOPMLFile": "Ladda upp OPML-fil", "ButtonUserDelete": "Radera användare {0}", "ButtonUserEdit": "Redigera användare {0}", @@ -122,7 +122,7 @@ "HeaderChooseAFolder": "Välj en mapp", "HeaderCollection": "Samling", "HeaderCollectionItems": "Böcker i samlingen", - "HeaderCover": "Bokomslag", + "HeaderCover": "Omslag", "HeaderCurrentDownloads": "Aktuella nedladdningar", "HeaderCustomMetadataProviders": "Egen källa för metadata", "HeaderDetails": "Detaljer", @@ -166,7 +166,7 @@ "HeaderPlaylist": "Spellista", "HeaderPlaylistItems": "Böcker i spellistan", "HeaderPodcastsToAdd": "Podcaster att lägga till", - "HeaderPreviewCover": "Förhandsgranska bokomslag", + "HeaderPreviewCover": "Förhandsgranska omslag", "HeaderRSSFeedGeneral": "RSS-information", "HeaderRSSFeedIsOpen": "RSS-flödet är öppet", "HeaderRSSFeeds": "RSS-flöden", @@ -264,7 +264,7 @@ "LabelContinueListening": "Fortsätt att lyssna", "LabelContinueReading": "Fortsätt att läsa", "LabelContinueSeries": "Fortsätt med serien", - "LabelCover": "Bokomslag", + "LabelCover": "Omslag", "LabelCoverImageURL": "URL till omslagsbild", "LabelCreatedAt": "Skapad", "LabelCronExpression": "Schemaläggning med hjälp av Cron (Cron Expression)", @@ -299,7 +299,7 @@ "LabelEmailSettingsSecure": "Säker", "LabelEmailSettingsSecureHelp": "Om sant kommer anslutningen att använda TLS vid anslutning till servern. Om falskt används TLS om servern stöder STARTTLS-tillägget. I de flesta fall, om du ansluter till port 465, bör du ställa in detta värde till sant. För port 587 eller 25, låt det vara falskt. (från nodemailer.com/smtp/#authentication)", "LabelEmailSettingsTestAddress": "E-postadress för test", - "LabelEmbeddedCover": "Inbäddat bokomslag", + "LabelEmbeddedCover": "Infogat omslag", "LabelEnable": "Aktivera", "LabelEncodingBackupLocation": "En säkerhetskopia av dina orginalljudfiler kommer att placeras i katalogen:", "LabelEncodingClearItemCache": "Kom ihåg att regelbundet radera cachen för föremål. Du hittar funktionen längst ner på sidan 'Inställningar'.", @@ -316,11 +316,13 @@ "LabelEpisodeTitle": "Titel på avsnittet", "LabelEpisodeType": "Typ av avsnitt", "LabelEpisodes": "Avsnitt", + "LabelEpisodic": "Uppdelad i avsnitt", "LabelExample": "Exempel", "LabelExpandSeries": "Expandera serier", "LabelExplicit": "Explicit version", "LabelExplicitChecked": "Explicit version (markerad)", "LabelExplicitUnchecked": "Ej Explicit version (ej markerad)", + "LabelExportOPML": "Exportera OPML-information", "LabelFeedURL": "URL-adress för flödet", "LabelFetchingMetadata": "Hämtar metadata", "LabelFile": "Fil", @@ -370,7 +372,7 @@ "LabelLanguage": "Språk", "LabelLanguageDefaultServer": "Standardspråk för server", "LabelLanguages": "Språk", - "LabelLastBookAdded": "Bok senast tillagd", + "LabelLastBookAdded": "Bok senast adderad", "LabelLastBookUpdated": "Bok senast uppdaterad", "LabelLastSeen": "Senast inloggad", "LabelLastTime": "Senaste tillfället", @@ -385,7 +387,7 @@ "LabelLibraryName": "Biblioteksnamn", "LabelLimit": "Begränsning", "LabelLineSpacing": "Radavstånd", - "LabelListenAgain": "Läs/Lyssna igen", + "LabelListenAgain": "Lyssna igen", "LabelLogLevelDebug": "Felsökning", "LabelLogLevelInfo": "Information", "LabelLogLevelWarn": "Varningar", @@ -394,6 +396,7 @@ "LabelMaxEpisodesToDownload": "Maximalt antal avsnitt att ladda ner (0 = obegränsat).", "LabelMaxEpisodesToDownloadPerCheck": "Maximalt antal nya avsnitt att ladda ner per tillfälle", "LabelMaxEpisodesToKeep": "Maximalt antal avsnitt att behålla", + "LabelMaxEpisodesToKeepHelp": "'0' innebär obegränsat antal avsnitt. Efter att nya avsnitt laddats ner raderas det äldsta avsnittet om du har mer än maximalt antal avsnitt. Endast ett avsnitt kommer att raderas per tillfälle.", "LabelMediaPlayer": "Mediaspelare", "LabelMediaType": "Mediatyp", "LabelMetaTag": "Metadata", @@ -413,11 +416,11 @@ "LabelNew": "Nytt", "LabelNewPassword": "Nytt lösenord", "LabelNewestAuthors": "Senaste författarna", - "LabelNewestEpisodes": "Senast tillagda avsnitt", + "LabelNewestEpisodes": "Senast adderade avsnitt", "LabelNextBackupDate": "Nästa tillfälle för säkerhetskopiering", "LabelNextScheduledRun": "Nästa schemalagda körning", "LabelNoCustomMetadataProviders": "Ingen egen källa för metadata", - "LabelNoEpisodesSelected": "Inga avsnitt valda", + "LabelNoEpisodesSelected": "Inga avsnitt har valts", "LabelNotFinished": "Ej avslutad", "LabelNotStarted": "Ej påbörjad", "LabelNotes": "Anteckningar", @@ -482,7 +485,7 @@ "LabelReleaseDate": "Utgivningsdatum", "LabelRemoveAllMetadataAbs": "Radera alla 'metadata.abs' filer", "LabelRemoveAllMetadataJson": "Radera alla 'metadata.json' filer", - "LabelRemoveCover": "Ta bort bokomslag", + "LabelRemoveCover": "Ta bort omslag", "LabelRemoveMetadataFile": "Radera metadata-filer i alla mappar i biblioteket", "LabelRemoveMetadataFileHelp": "Radera alla 'metadata.json' och 'metadata.abs' filer i dina {0} mappar.", "LabelRowsPerPage": "Antal rader per sida", @@ -490,6 +493,7 @@ "LabelSearchTitle": "Titel", "LabelSearchTitleOrASIN": "Sök titel eller ASIN-kod", "LabelSeason": "Säsong", + "LabelSeasonNumber": "Säsong #{0}", "LabelSelectAll": "Välj alla", "LabelSelectAllEpisodes": "Välj alla avsnitt", "LabelSelectEpisodesShowing": "Välj {0} avsnitt som visas", @@ -519,8 +523,8 @@ "LabelSettingsEpubsAllowScriptedContentHelp": "Tillåt att epub-filer får använda script.
Det rekommenderas att denna inställning är
avstängd när du inte litar på källan för epub-filerna.", "LabelSettingsExperimentalFeatures": "Experimentella funktioner", "LabelSettingsExperimentalFeaturesHelp": "Funktioner under utveckling som behöver din feedback och hjälp med testning. Klicka för att öppna diskussionen på GitHub.", - "LabelSettingsFindCovers": "Hitta ett bokomslag", - "LabelSettingsFindCoversHelp": "Om din bok INTE har ett bokomslag inbäddat i filen eller en fil med bokomslaget i mappen kommer skannern att försöka hitta ett omslag.
OBS: Detta kommer att förlänga inläsningstiden", + "LabelSettingsFindCovers": "Hitta ett omslag", + "LabelSettingsFindCoversHelp": "Om din bok INTE har ett omslag inbäddat i filen eller en fil med omslaget i mappen kommer skannern att försöka hitta ett omslag.
OBS: Detta kommer att förlänga inläsningstiden", "LabelSettingsHideSingleBookSeries": "Dölj serier som endast innehåller en bok", "LabelSettingsHideSingleBookSeriesHelp": "Serier som endast har en bok kommer att
döljas från sidan 'Serier' och hyllorna på startsidan.", "LabelSettingsHomePageBookshelfView": "Använd vy liknande en bokhylla på startsidan", @@ -538,10 +542,10 @@ "LabelSettingsSkipMatchingBooksWithISBN": "Hoppa över matchande böcker som har en ISBN-kod", "LabelSettingsSortingIgnorePrefixes": "Ignorera prefix vid sortering", "LabelSettingsSortingIgnorePrefixesHelp": "För prefix som t.ex. \"the\" kommer boktiteln \"The Book Title\" att sorteras som \"Book Title, The\"", - "LabelSettingsSquareBookCovers": "Använd kvadratiska bokomslag", - "LabelSettingsSquareBookCoversHelp": "Föredrar att använda kvadratiska bokomslag
före standardformatet 1.6:1", - "LabelSettingsStoreCoversWithItem": "Lagra bokomslag med objektet", - "LabelSettingsStoreCoversWithItemHelp": "Som standard lagras bokomslag i mappen '/metadata/items'.
Genom att aktivera detta alternativ kommer
omslagen att lagra i din biblioteksmapp.
Endast en fil med namnet 'cover' kommer att behållas", + "LabelSettingsSquareBookCovers": "Använd kvadratiska omslag", + "LabelSettingsSquareBookCoversHelp": "Föredrar att använda kvadratiska omslag
före standardformatet 1.6:1", + "LabelSettingsStoreCoversWithItem": "Lagra omslag med objektet", + "LabelSettingsStoreCoversWithItemHelp": "Som standard lagras omslag i mappen '/metadata/items'.
Genom att aktivera detta alternativ kommer
omslagen att lagra i din biblioteksmapp.
Endast en fil med namnet 'cover' kommer att behållas", "LabelSettingsStoreMetadataWithItem": "Lagra metadata med objektet", "LabelSettingsStoreMetadataWithItemHelp": "Som standard lagras metadatafiler i mappen '/metadata/items'. Genom att aktivera detta alternativ kommer metadatafilerna att lagra i din biblioteksmapp", "LabelSettingsTimeFormat": "Tidsformat", @@ -616,8 +620,8 @@ "LabelUndo": "Ångra", "LabelUnknown": "Okänd", "LabelUnknownPublishDate": "Okänt publiceringsdatum", - "LabelUpdateCover": "Uppdatera bokomslag", - "LabelUpdateCoverHelp": "Tillåt att befintliga bokomslag för de valda böckerna ersätts när en matchning hittas", + "LabelUpdateCover": "Uppdatera omslag", + "LabelUpdateCoverHelp": "Tillåt att befintliga omslag för de valda böckerna ersätts när en matchning hittas", "LabelUpdateDetails": "Uppdatera detaljer", "LabelUpdateDetailsHelp": "Tillåt att befintliga detaljer för de valda böckerna ersätts när en matchning hittas", "LabelUpdatedAt": "Uppdaterades", @@ -693,8 +697,8 @@ "MessageConfirmRemoveAllChapters": "Är du säker på att du vill ta bort alla kapitel?", "MessageConfirmRemoveAuthor": "Är du säker på att du vill ta bort författaren \"{0}\"?", "MessageConfirmRemoveCollection": "Är du säker på att du vill ta bort samlingen \"{0}\"?", - "MessageConfirmRemoveEpisode": "Är du säker på att du vill ta bort avsnittet \"{0}\"?", - "MessageConfirmRemoveEpisodes": "Är du säker på att du vill ta bort {0} avsnitt?", + "MessageConfirmRemoveEpisode": "Är du säker på att du vill radera avsnittet \"{0}\"?", + "MessageConfirmRemoveEpisodes": "Är du säker på att du vill radera {0} avsnitt?", "MessageConfirmRemoveListeningSessions": "Är du säker på att du vill radera {0} lyssningstillfällen?", "MessageConfirmRemoveMetadataFiles": "Är du säker på att du vill radera filerna 'metadata.{0}' i alla mappar i ditt bibliotek?", "MessageConfirmRemoveNarrator": "Är du säker på att du vill ta bort uppläsaren \"{0}\"?", @@ -731,19 +735,19 @@ "MessageMarkAllEpisodesNotFinished": "Markera alla avsnitt som ej avslutade", "MessageMarkAsFinished": "Markera som avslutad", "MessageMarkAsNotFinished": "Markera som ej avslutad", - "MessageMatchBooksDescription": "kommer att försöka matcha böcker i biblioteket med en bok från
den valda källan och fylla i uppgifter som saknas och bokomslag.
Inga befintliga uppgifter kommer att ersättas.", + "MessageMatchBooksDescription": "kommer att försöka matcha böcker i biblioteket med en bok från
den valda källan och fylla i uppgifter som saknas och omslag.
Inga befintliga uppgifter kommer att ersättas.", "MessageNoAudioTracks": "Inga ljudspår har hittats", "MessageNoAuthors": "Inga författare", "MessageNoBackups": "Inga säkerhetskopior", "MessageNoBookmarks": "Inga bokmärken", "MessageNoChapters": "Inga kapitel", "MessageNoCollections": "Inga samlingar", - "MessageNoCoversFound": "Inga bokomslag hittades", + "MessageNoCoversFound": "Inga omslag hittades", "MessageNoDescription": "Ingen beskrivning", "MessageNoDevices": "Inga enheter angivna", "MessageNoDownloadsInProgress": "Inga nedladdningar pågår för närvarande", "MessageNoDownloadsQueued": "Inga nedladdningar i kö", - "MessageNoEpisodeMatchesFound": "Inga matchande avsnitt hittades", + "MessageNoEpisodeMatchesFound": "Inga matchande avsnitt kunde hittas", "MessageNoEpisodes": "Inga avsnitt", "MessageNoFoldersAvailable": "Inga mappar tillgängliga", "MessageNoGenres": "Inga kategorier", @@ -770,9 +774,11 @@ "MessagePlaylistCreateFromCollection": "Skapa en spellista från samlingen", "MessagePleaseWait": "Vänta ett ögonblick...", "MessagePodcastHasNoRSSFeedForMatching": "Podcasten har ingen RSS-flödes-URL att använda för matchning", + "MessagePodcastSearchField": "Skriv sökfrågan eller URL-adressen för RSS-flödet", + "MessageQuickMatchAllEpisodes": "Snabbmatchning av alla avsnitt", "MessageQuickMatchDescription": "Adderar uppgifter som saknas samt en omslagsbild från
första träffen i resultatet vid sökningen från '{0}'.
Skriver inte över befintliga uppgifter om inte
inställningen 'Prioritera matchad metadata' är aktiverad.", "MessageRemoveChapter": "Ta bort kapitel", - "MessageRemoveEpisodes": "Ta bort {0} avsnitt", + "MessageRemoveEpisodes": "Radera {0} avsnitt", "MessageRemoveFromPlayerQueue": "Ta bort från spellistan", "MessageRemoveUserWarning": "Är du säker på att du vill radera användaren \"{0}\"?", "MessageReportBugsAndContribute": "Rapportera buggar, begär funktioner och bidra på", @@ -786,6 +792,7 @@ "MessageSetChaptersFromTracksDescription": "Ställ in kapitel med varje ljudfil som ett kapitel och kapitelrubrik som ljudfilens namn", "MessageStartPlaybackAtTime": "Starta uppspelning av \"{0}\" vid tidpunkt {1}?", "MessageTaskCanceledByUser": "Uppgiften avslutades av användaren", + "MessageTaskDownloadingEpisodeDescription": "Laddar ner avsnitt \"{0}\"", "MessageTaskEmbeddingMetadata": "Infogar metadata", "MessageTaskEmbeddingMetadataDescription": "Infogar metadata i ljudboken \"{0}\"", "MessageTaskEncodingM4bDescription": "Omkodning av ljudbok \"{0}\" till en M4B-fil", @@ -796,6 +803,10 @@ "MessageTaskFailedToMoveM4bFile": "Misslyckades med att flytta M4B-filen", "MessageTaskFailedToWriteMetadataFile": "Misslyckades med att skapa filen med metadata", "MessageTaskMatchingBooksInLibrary": "Matchar böcker i biblioteket \"{0}\"", + "MessageTaskOpmlImportFinished": "Adderade {0} podcasts", + "MessageTaskOpmlParseFailed": "Misslyckades att tolka OPML-filen", + "MessageTaskOpmlParseFastFail": "Felaktig OPML-fil. Ingen tag eller tag finns i filen", + "MessageTaskOpmlParseNoneFound": "Inget flöde finns angivet i OPML-filen", "MessageTaskScanItemsAdded": "{0} adderades", "MessageTaskScanItemsMissing": "{0} saknades", "MessageTaskScanItemsUpdated": "{0} uppdaterades", @@ -814,7 +825,7 @@ "NoteChapterEditorTimes": "OBS: Starttiden för första kapitlet måste vara 0:00 och starttiden för det sista kapitlet får inte överstiga ljudbokens totala varaktighet.", "NoteFolderPicker": "OBS: Mappar som redan är kopplade kommer inte att visas", "NoteRSSFeedPodcastAppsHttps": "VARNING: De flesta applikationer för podcasts kräver att URL:en för RSS-flödet använder HTTPS", - "NoteRSSFeedPodcastAppsPubDate": "VARNING: Ett eller flera av dina avsnitt har inte ett publiceringsdatum. Vissa applikationer för podcasts kräver detta.", + "NoteRSSFeedPodcastAppsPubDate": "VARNING: Ett eller flera av dina avsnitt saknar publiceringsdatum. Vissa applikationer för podcasts kräver detta.", "NoteUploaderFoldersWithMediaFiles": "Mappar som innehåller mediefiler hanteras som separata objekt i biblioteket.", "NoteUploaderOnlyAudioFiles": "Om du bara laddar upp ljudfiler kommer varje ljudfil att hanteras som en separat ljudbok.", "NoteUploaderUnsupportedFiles": "Oaccepterade filer ignoreras. När du väljer eller släpper en mapp ignoreras andra filer som inte finns i ett objektmapp.", @@ -868,10 +879,12 @@ "ToastCachePurgeSuccess": "Rensning av cachen har genomförts", "ToastChaptersHaveErrors": "Kapitlen har fel", "ToastChaptersMustHaveTitles": "Kapitel måste ha titlar", + "ToastChaptersRemoved": "Kapitlen har raderats", + "ToastChaptersUpdated": "Kapitlen har uppdaterats", "ToastCollectionItemsAddFailed": "Misslyckades med att addera böcker till samlingen", "ToastCollectionRemoveSuccess": "Samlingen har raderats", "ToastCollectionUpdateSuccess": "Samlingen har uppdaterats", - "ToastCoverUpdateFailed": "Uppdatering av bokomslag misslyckades", + "ToastCoverUpdateFailed": "Uppdatering av omslag misslyckades", "ToastDateTimeInvalidOrIncomplete": "Datum och klockslag är felaktigt eller ej komplett", "ToastDeleteFileFailed": "Misslyckades att radera filen", "ToastDeleteFileSuccess": "Filen har raderats", @@ -879,13 +892,15 @@ "ToastDeviceTestEmailSuccess": "Ett testmail har skickats", "ToastEmailSettingsUpdateSuccess": "Inställningarna av e-post har uppdaterats", "ToastEncodeCancelSucces": "Omkodningen avbruten", + "ToastEpisodeDownloadQueueClearFailed": "Misslyckades med att tömma kön", + "ToastEpisodeDownloadQueueClearSuccess": "Kö för nedladdning av avsnitt har tömts", "ToastEpisodeUpdateSuccess": "{0} avsnitt uppdaterades", "ToastFailedToLoadData": "Misslyckades med att ladda data", "ToastFailedToUpdate": "Misslyckades med att uppdatera", "ToastInvalidImageUrl": "Felaktig URL-adress till omslagsbilden", "ToastInvalidMaxEpisodesToDownload": "Ogiltigt maximalt antal avsnitt att ladda ner", "ToastInvalidUrl": "Felaktig URL-adress", - "ToastItemCoverUpdateSuccess": "Objektets bokomslag har uppdaterats", + "ToastItemCoverUpdateSuccess": "Objektets omslag har uppdaterats", "ToastItemDeletedFailed": "Misslyckades med att radera objektet", "ToastItemDeletedSuccess": "Objektet har raderats", "ToastItemDetailsUpdateSuccess": "Informationen om objektet har uppdaterats", @@ -926,6 +941,7 @@ "ToastPlaylistUpdateSuccess": "Spellistan har uppdaterats", "ToastPodcastCreateFailed": "Misslyckades med att skapa podcasten", "ToastPodcastCreateSuccess": "Podcasten skapades framgångsrikt", + "ToastPodcastNoEpisodesInFeed": "Inga avsnitt finns i RSS-flödet", "ToastProviderCreatedFailed": "Misslyckades med att addera en källa", "ToastProviderCreatedSuccess": "En ny källa har adderats", "ToastProviderNameAndUrlRequired": "Ett namn och en URL-adress krävs", From 08b4d4d7a2b7588ac71d05862d1e17ec19e5386c Mon Sep 17 00:00:00 2001 From: burghy86 Date: Mon, 10 Feb 2025 13:25:01 +0000 Subject: [PATCH 036/303] Translated using Weblate (Italian) Currently translated at 100.0% (1089 of 1089 strings) Translation: Audiobookshelf/Abs Web Client Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/it/ --- client/strings/it.json | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/client/strings/it.json b/client/strings/it.json index 1d04521f..ba106ee3 100644 --- a/client/strings/it.json +++ b/client/strings/it.json @@ -10,6 +10,8 @@ "ButtonApplyChapters": "Applica", "ButtonAuthors": "Autori", "ButtonBack": "Indietro", + "ButtonBatchEditPopulateFromExisting": "Popola da esistente", + "ButtonBatchEditPopulateMapDetails": "Inserisci i dettagli della mappa", "ButtonBrowseForFolder": "Per Cartella", "ButtonCancel": "Cancella", "ButtonCancelEncode": "Ferma la codifica", @@ -88,6 +90,8 @@ "ButtonSaveTracklist": "Salva Tracklist", "ButtonScan": "Scansiona", "ButtonScanLibrary": "Scansiona Libreria", + "ButtonScrollLeft": "Scorri verso sinistra", + "ButtonScrollRight": "Scorri verso destra", "ButtonSearch": "Cerca", "ButtonSelectFolderPath": "Seleziona percorso cartella", "ButtonSeries": "Serie", @@ -190,6 +194,7 @@ "HeaderSettingsExperimental": "Opzioni Sperimentali", "HeaderSettingsGeneral": "Generale", "HeaderSettingsScanner": "Scanner", + "HeaderSettingsWebClient": "Web Client", "HeaderSleepTimer": "Sveglia", "HeaderStatsLargestItems": "File pesanti", "HeaderStatsLongestItems": "libri più lunghi (ore)", @@ -429,7 +434,7 @@ "LabelMetadataProvider": "Metadata Provider", "LabelMinute": "Minuto", "LabelMinutes": "Minuti", - "LabelMissing": "Altro", + "LabelMissing": "Mancante", "LabelMissingEbook": "Non ha libri digitali", "LabelMissingSupplementaryEbook": "Non ha un libro digitale supplementare", "LabelMobileRedirectURIs": "URI di reindirizzamento mobile consentiti", @@ -481,6 +486,7 @@ "LabelPersonalYearReview": "Il tuo anno in rassegna ({0})", "LabelPhotoPathURL": "foto Path/URL", "LabelPlayMethod": "Metodo di riproduzione", + "LabelPlaybackRateIncrementDecrement": "Valore incremento/decremento velocità di riproduzione", "LabelPlayerChapterNumberMarker": "{0} di {1}", "LabelPlaylists": "Playlist", "LabelPodcast": "Podcast", @@ -543,6 +549,7 @@ "LabelServerYearReview": "Anno del server in sintesi({0})", "LabelSetEbookAsPrimary": "Imposta come primario", "LabelSetEbookAsSupplementary": "Imposta come suplementare", + "LabelSettingsAllowIframe": "Consenti l'incorporamento in un iframe", "LabelSettingsAudiobooksOnly": "Solo Audiolibri", "LabelSettingsAudiobooksOnlyHelp": "L'abilitazione di questa impostazione ignorerà i file di libro digitale a meno che non si trovino all'interno di una cartella di audiolibri, nel qual caso verranno impostati come libri digitali supplementari", "LabelSettingsBookshelfViewHelp": "Design con scaffali in legno", @@ -585,6 +592,7 @@ "LabelSettingsStoreMetadataWithItemHelp": "Di default, i metadati sono salvati dentro /metadata/items, abilitando questa opzione si memorizzeranno i metadata nella cartella della libreria", "LabelSettingsTimeFormat": "Formato Ora", "LabelShare": "Condividi", + "LabelShareDownloadableHelp": "Consente agli utenti dotati del link di condivisione di scaricare un file zip dell'elemento della libreria.", "LabelShareOpen": "Apri Condivisioni", "LabelShareURL": "Condividi URL", "LabelShowAll": "Mostra tutto", @@ -593,6 +601,8 @@ "LabelSize": "Dimensione", "LabelSleepTimer": "Temporizzatore", "LabelSlug": "Lento", + "LabelSortAscending": "Crescente", + "LabelSortDescending": "Discendente", "LabelStart": "Inizo", "LabelStartTime": "Tempo di inizio", "LabelStarted": "Iniziato", @@ -664,6 +674,7 @@ "LabelUpdateDetailsHelp": "Consenti la sovrascrittura dei dettagli esistenti per i libri selezionati quando viene individuata una corrispondenza", "LabelUpdatedAt": "Aggiornato alle", "LabelUploaderDragAndDrop": "Drag & drop file o Cartelle", + "LabelUploaderDragAndDropFilesOnly": "Drag & drop files", "LabelUploaderDropFiles": "Elimina file", "LabelUploaderItemFetchMetadataHelp": "Recupera automaticamente titolo, autore e serie", "LabelUseAdvancedOptions": "Usa le opzioni avanzate", @@ -679,6 +690,8 @@ "LabelViewPlayerSettings": "Mostra Impostazioni player", "LabelViewQueue": "Visualizza coda", "LabelVolume": "Volume", + "LabelWebRedirectURLsDescription": "Autorizza questi URL nel tuo provider OAuth per consentire il reindirizzamento all'app Web dopo l'accesso:", + "LabelWebRedirectURLsSubfolder": "Sottocartella per URL di reindirizzamento", "LabelWeekdaysToRun": "Giorni feriali da eseguire", "LabelXBooks": "{0} libri", "LabelXItems": "{0} oggetti", @@ -694,8 +707,11 @@ "MessageBackupsLocationEditNote": "Nota: l'aggiornamento della posizione di backup non sposterà o modificherà i backup esistenti", "MessageBackupsLocationNoEditNote": "Nota: la posizione del backup viene impostata tramite una variabile di ambiente e non può essere modificata qui.", "MessageBackupsLocationPathEmpty": "Il percorso del backup non può essere vuoto", + "MessageBatchEditPopulateMapDetailsAllHelp": "Popola i campi abilitati con i dati di tutti gli elementi. I campi con più valori verranno uniti", + "MessageBatchEditPopulateMapDetailsItemHelp": "Compila i campi dei dettagli della mappa abilitati con i dati di questo elemento", "MessageBatchQuickMatchDescription": "Quick Match tenterà di aggiungere copertine e metadati mancanti per gli elementi selezionati. Attiva l'opzione per consentire a Quick Match di sovrascrivere copertine e/o metadati esistenti.", "MessageBookshelfNoCollections": "Non hai ancora creato nessuna raccolta", + "MessageBookshelfNoCollectionsHelp": "le collezioni sono pubbliche. Tutti gli utenti con accesso alla biblioteca possono vederle.", "MessageBookshelfNoRSSFeeds": "Nessun RSS feeds aperto", "MessageBookshelfNoResultsForFilter": "Nessun risultato per il filtro \"{0}: {1}\"", "MessageBookshelfNoResultsForQuery": "Nessun risultato per la query", @@ -748,6 +764,7 @@ "MessageConfirmResetProgress": "Vuoi davvero azzerare i tuoi progressi?", "MessageConfirmSendEbookToDevice": "Sei sicuro/sicura di voler inviare {0} libro «{1}» al dispositivo «{2}»?", "MessageConfirmUnlinkOpenId": "Vuoi davvero scollegare questo utente da OpenID?", + "MessageDaysListenedInTheLastYear": "{0} giorni ascoltati nell'ultimo anno", "MessageDownloadingEpisode": "Scaricamento dell’episodio in corso", "MessageDragFilesIntoTrackOrder": "Trascina i file nell'ordine di traccia corretto", "MessageEmbedFailed": "Incorporamento non riuscito!", @@ -805,6 +822,7 @@ "MessageNoTasksRunning": "Nessun processo in esecuzione", "MessageNoUpdatesWereNecessary": "Nessun aggiornamento necessario", "MessageNoUserPlaylists": "non hai nessuna Playlist", + "MessageNoUserPlaylistsHelp": "Le playlist sono private. Solo l'utente che le crea può vederle.", "MessageNotYetImplemented": "Non Ancora Implementato", "MessageOpmlPreviewNote": "Nota: questa è un'anteprima del file OPML analizzato. Il titolo effettivo del podcast verrà preso dal feed RSS.", "MessageOr": "o", @@ -826,6 +844,7 @@ "MessageResetChaptersConfirm": "Sei sicuro di voler reimpostare i capitoli e annullare le modifiche ?", "MessageRestoreBackupConfirm": "Sei sicuro di voler ripristinare il backup creato su", "MessageRestoreBackupWarning": "Il ripristino di un backup sovrascriverà l'intero database situato in /config e sovrascrive le immagini in /metadata/items & /metadata/authors.

I backup non modificano alcun file nelle cartelle della libreria. Se hai abilitato le impostazioni del server per archiviare copertine e metadati nelle cartelle della libreria, questi non vengono sottoposti a backup o sovrascritti.

Tutti i client che utilizzano il tuo server verranno aggiornati automaticamente.", + "MessageScheduleLibraryScanNote": "Per la maggior parte degli utenti, si consiglia di lasciare questa funzionalità disabilitata e di mantenere abilitata l'impostazione di folder watcher. Il folder watcher rileverà automaticamente le modifiche nelle cartelle della libreria. Il folder watcher non funziona per ogni file system (come NFS), quindi è possibile utilizzare le scansioni pianificate della libreria.", "MessageSearchResultsFor": "cerca risultati per", "MessageSelected": "{0} selezionati", "MessageServerCouldNotBeReached": "Impossibile raggiungere il server", @@ -952,6 +971,7 @@ "ToastCollectionRemoveSuccess": "Collezione rimossa", "ToastCollectionUpdateSuccess": "Raccolta aggiornata", "ToastCoverUpdateFailed": "Aggiornamento cover fallito", + "ToastDateTimeInvalidOrIncomplete": "Data e ora non sono valide o incomplete", "ToastDeleteFileFailed": "Impossibile eliminare il file", "ToastDeleteFileSuccess": "File eliminato", "ToastDeviceAddFailed": "Aggiunta dispositivo fallita", @@ -1004,6 +1024,7 @@ "ToastNewUserTagError": "Devi selezionare almeno un tag", "ToastNewUserUsernameError": "Inserisci un nome utente", "ToastNoNewEpisodesFound": "Nessun nuovo episodio trovato", + "ToastNoRSSFeed": "Il podcast non ha un feed RSS", "ToastNoUpdatesNecessary": "Nessun aggiornamento necessario", "ToastNotificationCreateFailed": "Impossibile creare la notifica", "ToastNotificationDeleteFailed": "Impossibile eliminare la notifica", From cbe5e3db8a7107342e5dd5364fd568fdfbeaefe0 Mon Sep 17 00:00:00 2001 From: Troja Date: Mon, 10 Feb 2025 15:50:40 +0000 Subject: [PATCH 037/303] Translated using Weblate (Belarusian) Currently translated at 13.0% (142 of 1089 strings) Translation: Audiobookshelf/Abs Web Client Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/be/ --- client/strings/be.json | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/client/strings/be.json b/client/strings/be.json index fcf30d39..09deaa9b 100644 --- a/client/strings/be.json +++ b/client/strings/be.json @@ -100,9 +100,14 @@ "ButtonUserEdit": "Рэдагаваць карыстальніка {0}", "ButtonViewAll": "Прагледзець усе", "ButtonYes": "Так", + "ErrorUploadFetchMetadataAPI": "Памылка пры атрыманні метададзеных", + "ErrorUploadFetchMetadataNoResults": "Не ўдалося атрымаць метададзеныя – паспрабуйце абнавіць назву і/або аўтара", + "ErrorUploadLacksTitle": "Павінна быць назва", "HeaderAccount": "Уліковы запіс", "HeaderAddCustomMetadataProvider": "Дадаць карыстальніцкага пастаўшчыка метаданных", + "HeaderAdvanced": "Дадаткова", "HeaderAppriseNotificationSettings": "Налады апавяшчэнняў Apprise", + "HeaderAudioTracks": "Аўдыёдарожкі", "HeaderAudiobookTools": "Сродкі кіравання файламі аўдыякніг", "HeaderAuthentication": "Аўтэнтыфікацыя", "HeaderBackups": "Рэзервовыя копіі", @@ -113,5 +118,28 @@ "HeaderCollectionItems": "Элементы калекцыі", "HeaderCover": "Вокладка", "HeaderCurrentDownloads": "Бягучыя загрузкі", - "HeaderCustomMessageOnLogin": "Карыстальніцкае паведамленне пры ўваходзе" + "HeaderCustomMessageOnLogin": "Карыстальніцкае паведамленне пры ўваходзе", + "HeaderCustomMetadataProviders": "Карыстальніцкія крыніцы метададзеных", + "HeaderDetails": "Падрабязнасці", + "HeaderDownloadQueue": "Чарга загрузкі", + "HeaderEbookFiles": "Файлы электронных кніг", + "HeaderEmail": "Электронная пошта", + "HeaderEmailSettings": "Налады электроннай пошты", + "HeaderEpisodes": "Эпізоды", + "HeaderEreaderDevices": "Прылады для чытання", + "HeaderEreaderSettings": "Налады прылады для чытання", + "HeaderFiles": "Файлы", + "HeaderFindChapters": "Знайсці раздзелы", + "HeaderIgnoredFiles": "Ігнараваныя файлы", + "HeaderItemFiles": "Файлы элементаў", + "HeaderItemMetadataUtils": "Утыліты для метададзеных элементаў", + "HeaderLastListeningSession": "Апошні сеанс праслухоўвання", + "HeaderLatestEpisodes": "Апошнія эпізоды", + "HeaderLibraries": "Бібліятэкі", + "HeaderLibraryFiles": "Файлы бібліятэкі", + "HeaderLibraryStats": "Статыстыка бібліятэкі", + "HeaderListeningSessions": "Сеансы праслухоўвання", + "HeaderListeningStats": "Статыстыка праслухоўвання", + "HeaderLogin": "Уваход", + "LabelTracks": "Дарожкі" } From 21b27c432c4082d8b065ad2a48aabef7c9f7cd48 Mon Sep 17 00:00:00 2001 From: Troja Date: Mon, 10 Feb 2025 18:43:45 +0000 Subject: [PATCH 038/303] Translated using Weblate (Belarusian) Currently translated at 16.0% (175 of 1089 strings) Translation: Audiobookshelf/Abs Web Client Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/be/ --- client/strings/be.json | 39 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 36 insertions(+), 3 deletions(-) diff --git a/client/strings/be.json b/client/strings/be.json index 09deaa9b..9bdc5c94 100644 --- a/client/strings/be.json +++ b/client/strings/be.json @@ -10,6 +10,7 @@ "ButtonApplyChapters": "Ужыць раздзелы", "ButtonAuthors": "Аўтары", "ButtonBack": "Назад", + "ButtonBatchEditPopulateFromExisting": "Запоўніць з існуючага", "ButtonBrowseForFolder": "Знайсці тэчку", "ButtonCancel": "Адмяніць", "ButtonCancelEncode": "Адмяніць кадзіраванне", @@ -35,14 +36,18 @@ "ButtonForceReScan": "Прымусовае паўторнае сканаванне", "ButtonFullPath": "Поўны шлях", "ButtonHide": "Схаваць", + "ButtonHome": "Галоўная", "ButtonIssues": "Праблемы", "ButtonJumpBackward": "Перайсці назад", "ButtonJumpForward": "Перайсці наперад", + "ButtonLatest": "Апошняе", "ButtonLibrary": "Бібліятэка", "ButtonLogout": "Выйсці", "ButtonLookup": "", + "ButtonManageTracks": "Кіраванне дарожкамі", "ButtonMapChapterTitles": "Супаставіць назвы раздзелаў", "ButtonMatchAllAuthors": "Супадзенне ўсіх аўтараў", + "ButtonMatchBooks": "Падбор кніг", "ButtonNevermind": "Няважна", "ButtonNext": "Далей", "ButtonNextChapter": "Наступны раздзел", @@ -71,6 +76,9 @@ "ButtonRemove": "Выдаліць", "ButtonRemoveAll": "Выдаліць усе", "ButtonRemoveAllLibraryItems": "Выдаліць усе элементы бібліятэкі", + "ButtonRemoveFromContinueListening": "Выдаліць з Працягваць слухаць", + "ButtonRemoveFromContinueReading": "Выдаліць з Працягваць чытанне", + "ButtonRemoveSeriesFromContinueSeries": "Выдаліць серыю з Працягваць серыю", "ButtonReset": "Скінуць", "ButtonResetToDefault": "Скінуць па змаўчанні", "ButtonRestore": "Аднавіць", @@ -117,11 +125,11 @@ "HeaderCollection": "Калекцыя", "HeaderCollectionItems": "Элементы калекцыі", "HeaderCover": "Вокладка", - "HeaderCurrentDownloads": "Бягучыя загрузкі", + "HeaderCurrentDownloads": "Бягучыя спампоўкі", "HeaderCustomMessageOnLogin": "Карыстальніцкае паведамленне пры ўваходзе", "HeaderCustomMetadataProviders": "Карыстальніцкія крыніцы метададзеных", "HeaderDetails": "Падрабязнасці", - "HeaderDownloadQueue": "Чарга загрузкі", + "HeaderDownloadQueue": "Чарга спамповак", "HeaderEbookFiles": "Файлы электронных кніг", "HeaderEmail": "Электронная пошта", "HeaderEmailSettings": "Налады электроннай пошты", @@ -141,5 +149,30 @@ "HeaderListeningSessions": "Сеансы праслухоўвання", "HeaderListeningStats": "Статыстыка праслухоўвання", "HeaderLogin": "Уваход", - "LabelTracks": "Дарожкі" + "HeaderLogs": "Журналы", + "HeaderManageGenres": "Кіраванне жанрамі", + "HeaderManageTags": "Кіраванне тэгамі", + "HeaderMapDetails": "Падрабязнасці адлюстравання", + "HeaderScheduleEpisodeDownloads": "Расклад аўтаматычных спамповак эпізодаў", + "LabelAutoDownloadEpisodes": "Аўтаматычнае спампаванне эпізодаў", + "LabelContinueListening": "Працягваць слухаць", + "LabelDownload": "Спампаваць", + "LabelDownloadNEpisodes": "Спампована {0} эпізодаў", + "LabelDownloadable": "Спампоўваецца", + "LabelMaxEpisodesToDownload": "Максімальная колькасць эпізодаў для спампоўкі. Выкарыстоўвайце 0 для неабмежаванай колькасці.", + "LabelMaxEpisodesToDownloadPerCheck": "Максімальная колькасць новых эпізодаў для спампоўкі за праверку", + "LabelMaxEpisodesToKeepHelp": "Значэнне 0 не ўстанаўлівае максімальнага абмежавання. Пасля аўтаматычнай спампоўкі новага эпізоду будзе выдалены самы стары эпізод, калі ў вас больш за X эпізодаў. Пры кожнай новай спампоўцы будзе выдаляцца толькі 1 эпізод.", + "LabelPermissionsDownload": "Можна спампаваць", + "LabelReAddSeriesToContinueListening": "Дадаць серыю зноў у Працягваць слухаць", + "LabelShareDownloadableHelp": "Дазваляе карыстальнікам, якія маюць спасылку на доступ, спампаваць ZIP-файл элемента бібліятэкі.", + "LabelTracks": "Дарожкі", + "MessageDownloadingEpisode": "Спампоўка эпізоду", + "MessageEpisodesQueuedForDownload": "{0} эпізод(аў) у чарзе для спампоўкі", + "MessageNoDownloadsInProgress": "Зараз няма актыўных спамповак", + "MessageNoDownloadsQueued": "Няма спамповак у чарзе", + "MessageTaskDownloadingEpisodeDescription": "Спампоўка эпізоду \"{0}\"", + "NotificationOnEpisodeDownloadedDescription": "Выклікаецца, калі эпізод падкаста аўтаматычна спампоўваецца", + "ToastEpisodeDownloadQueueClearFailed": "Не ўдалося ачысціць чаргу", + "ToastEpisodeDownloadQueueClearSuccess": "Чарга спампоўкі эпізодаў ачышчана", + "ToastInvalidMaxEpisodesToDownload": "Няправільная максімальная колькасць эпізодаў для спампоўкі" } From 1be1ce6f87dbdc96623510b949f31f50c7365c84 Mon Sep 17 00:00:00 2001 From: Mario Date: Tue, 11 Feb 2025 14:12:45 +0000 Subject: [PATCH 039/303] Translated using Weblate (German) Currently translated at 99.9% (1088 of 1089 strings) Translation: Audiobookshelf/Abs Web Client Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/de/ --- client/strings/de.json | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/client/strings/de.json b/client/strings/de.json index b31fedc9..2b07ee03 100644 --- a/client/strings/de.json +++ b/client/strings/de.json @@ -10,6 +10,8 @@ "ButtonApplyChapters": "Kapitel anwenden", "ButtonAuthors": "Autoren", "ButtonBack": "Zurück", + "ButtonBatchEditPopulateFromExisting": "Auffüllen aus vorhandenem", + "ButtonBatchEditPopulateMapDetails": "Kartendetails auffüllen", "ButtonBrowseForFolder": "Ordnersuche", "ButtonCancel": "Abbrechen", "ButtonCancelEncode": "Codierung abbrechen", @@ -484,6 +486,7 @@ "LabelPersonalYearReview": "Dein Jahr in Übersicht ({0})", "LabelPhotoPathURL": "Foto Pfad/URL", "LabelPlayMethod": "Abspielmethode", + "LabelPlaybackRateIncrementDecrement": "Wiedergaberate der Erhöhung/Verminderung", "LabelPlayerChapterNumberMarker": "{0} von {1}", "LabelPlaylists": "Wiedergabelisten", "LabelPodcast": "Podcast", @@ -704,8 +707,10 @@ "MessageBackupsLocationEditNote": "Hinweis: Durch das Aktualisieren des Backup-Speicherorts werden vorhandene Sicherungen nicht verschoben oder geändert", "MessageBackupsLocationNoEditNote": "Hinweis: Der Sicherungsspeicherort wird über eine Umgebungsvariable festgelegt und kann hier nicht geändert werden.", "MessageBackupsLocationPathEmpty": "Der Backup-Pfad darf nicht leer sein", + "MessageBatchEditPopulateMapDetailsAllHelp": "Fülle die aktivierten Felder mit Daten aus allen Elementen. Felder mit mehreren Werten werden zusammengeführt", "MessageBatchQuickMatchDescription": "Der Schnellabgleich versucht, fehlende Titelbilder und Metadaten für die ausgewählten Artikel hinzuzufügen. Aktiviere die nachstehenden Optionen, damit der Schnellabgleich vorhandene Titelbilder und/oder Metadaten überschreiben kann.", "MessageBookshelfNoCollections": "Es wurden noch keine Sammlungen erstellt", + "MessageBookshelfNoCollectionsHelp": "Sammlungen sind öffentlich. Alle Benutzer mit Zugriff auf die Bibliothek können sie sehen.", "MessageBookshelfNoRSSFeeds": "Keine RSS-Feeds geöffnet", "MessageBookshelfNoResultsForFilter": "Keine Ergebnisse für Filter \"{0}: {1}\"", "MessageBookshelfNoResultsForQuery": "Keine Ergebnisse für die Abfrage", @@ -816,6 +821,7 @@ "MessageNoTasksRunning": "Keine laufenden Aufgaben", "MessageNoUpdatesWereNecessary": "Keine Aktualisierungen waren notwendig", "MessageNoUserPlaylists": "Keine Wiedergabelisten vorhanden", + "MessageNoUserPlaylistsHelp": "Wiedergabelisten sind privat. Nur der Benutzer, der sie erstellt hat, kann sie sehen.", "MessageNotYetImplemented": "Noch nicht implementiert", "MessageOpmlPreviewNote": "Hinweis: Dies ist nur eine Vorschau der geparsten OPML Datei. Der eigentliche Podcast-Titel wird aus dem RSS-Feed übernommen.", "MessageOr": "Oder", From 03a1d7da32b6a7c5e807c068c7e6d28d399f1382 Mon Sep 17 00:00:00 2001 From: Troja Date: Tue, 11 Feb 2025 19:14:44 +0000 Subject: [PATCH 040/303] Translated using Weblate (Belarusian) Currently translated at 19.4% (212 of 1089 strings) Translation: Audiobookshelf/Abs Web Client Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/be/ --- client/strings/be.json | 41 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 39 insertions(+), 2 deletions(-) diff --git a/client/strings/be.json b/client/strings/be.json index 9bdc5c94..5533123b 100644 --- a/client/strings/be.json +++ b/client/strings/be.json @@ -115,7 +115,7 @@ "HeaderAddCustomMetadataProvider": "Дадаць карыстальніцкага пастаўшчыка метаданных", "HeaderAdvanced": "Дадаткова", "HeaderAppriseNotificationSettings": "Налады апавяшчэнняў Apprise", - "HeaderAudioTracks": "Аўдыёдарожкі", + "HeaderAudioTracks": "Аўдыядарожкі", "HeaderAudiobookTools": "Сродкі кіравання файламі аўдыякніг", "HeaderAuthentication": "Аўтэнтыфікацыя", "HeaderBackups": "Рэзервовыя копіі", @@ -153,26 +153,63 @@ "HeaderManageGenres": "Кіраванне жанрамі", "HeaderManageTags": "Кіраванне тэгамі", "HeaderMapDetails": "Падрабязнасці адлюстравання", + "HeaderNewAccount": "Новы ўліковы запіс", + "HeaderNewLibrary": "Новая бібліятэка", + "HeaderNotificationCreate": "Стварыць апавяшчэнне", + "HeaderNotificationUpdate": "Абнавіць апавяшчэнне", + "HeaderNotifications": "Апавяшчэнні", + "HeaderOpenListeningSessions": "Адкрыць сеансы праслухоўвання", "HeaderScheduleEpisodeDownloads": "Расклад аўтаматычных спамповак эпізодаў", + "HeaderSettings": "Налады", + "HeaderSettingsDisplay": "Дысплей", + "HeaderSettingsExperimental": "Эксперыментальныя функцыі", + "HeaderSettingsGeneral": "Агульныя", + "HeaderSettingsScanner": "Сканер", + "HeaderSettingsWebClient": "Вэб-кліент", + "HeaderStatsTop10Authors": "10 лепшых аўтараў", + "HeaderStatsTop5Genres": "5 лепшых жанраў", + "HeaderTableOfContents": "Змест", + "HeaderTools": "Інструменты", + "HeaderUpdateAccount": "Абнавіць уліковы запіс", + "LabelAccountType": "Тып уліковага запіса", + "LabelAccountTypeAdmin": "Адміністратар", + "LabelAccountTypeGuest": "Госць", + "LabelAccountTypeUser": "Карыстальнік", + "LabelAudioBitrate": "Бітрэйт аўдыё (напрыклад, 128к)", + "LabelAudioChannels": "Аўдыёканалы (1 або 2)", + "LabelAudioCodec": "Аўдыёкодэк", "LabelAutoDownloadEpisodes": "Аўтаматычнае спампаванне эпізодаў", + "LabelBackupAudioFiles": "Рэзервовае капіраванне аўдыёфайлаў", "LabelContinueListening": "Працягваць слухаць", "LabelDownload": "Спампаваць", "LabelDownloadNEpisodes": "Спампована {0} эпізодаў", "LabelDownloadable": "Спампоўваецца", + "LabelEncodingBackupLocation": "Рэзервовая копія вашых арыгінальных аўдыёфайлаў будзе захавана ў:", + "LabelEncodingChaptersNotEmbedded": "Раздзелы не ўбудаваны ў шматдарожкавыя аўдыякнігі.", + "LabelEncodingFinishedM4B": "Гатовы файл M4B будзе змешчаны ў вашу тэчку з аўдыякнігамі па адрасе:", + "LabelEncodingInfoEmbedded": "Метаданыя будуць убудаваны ў аўдыядарожкі ўнутры вашай тэчкі з аўдыякнігамі.", "LabelMaxEpisodesToDownload": "Максімальная колькасць эпізодаў для спампоўкі. Выкарыстоўвайце 0 для неабмежаванай колькасці.", "LabelMaxEpisodesToDownloadPerCheck": "Максімальная колькасць новых эпізодаў для спампоўкі за праверку", "LabelMaxEpisodesToKeepHelp": "Значэнне 0 не ўстанаўлівае максімальнага абмежавання. Пасля аўтаматычнай спампоўкі новага эпізоду будзе выдалены самы стары эпізод, калі ў вас больш за X эпізодаў. Пры кожнай новай спампоўцы будзе выдаляцца толькі 1 эпізод.", "LabelPermissionsDownload": "Можна спампаваць", "LabelReAddSeriesToContinueListening": "Дадаць серыю зноў у Працягваць слухаць", "LabelShareDownloadableHelp": "Дазваляе карыстальнікам, якія маюць спасылку на доступ, спампаваць ZIP-файл элемента бібліятэкі.", + "LabelStatsAudioTracks": "Аўдыядарожкі", "LabelTracks": "Дарожкі", + "MessageConfirmRemoveListeningSessions": "Вы ўпэўнены, што жадаеце выдаліць {0} сеансаў праслухоўвання?", "MessageDownloadingEpisode": "Спампоўка эпізоду", "MessageEpisodesQueuedForDownload": "{0} эпізод(аў) у чарзе для спампоўкі", "MessageNoDownloadsInProgress": "Зараз няма актыўных спамповак", "MessageNoDownloadsQueued": "Няма спамповак у чарзе", + "MessageNoListeningSessions": "Няма сеансаў праслухоўвання", "MessageTaskDownloadingEpisodeDescription": "Спампоўка эпізоду \"{0}\"", "NotificationOnEpisodeDownloadedDescription": "Выклікаецца, калі эпізод падкаста аўтаматычна спампоўваецца", + "ToastAccountUpdateSuccess": "Уліковы запіс абноўлены", "ToastEpisodeDownloadQueueClearFailed": "Не ўдалося ачысціць чаргу", "ToastEpisodeDownloadQueueClearSuccess": "Чарга спампоўкі эпізодаў ачышчана", - "ToastInvalidMaxEpisodesToDownload": "Няправільная максімальная колькасць эпізодаў для спампоўкі" + "ToastInvalidMaxEpisodesToDownload": "Няправільная максімальная колькасць эпізодаў для спампоўкі", + "ToastNewUserCreatedFailed": "Не ўдалося стварыць уліковы запіс: \"{0}\"", + "ToastNewUserCreatedSuccess": "Новы ўліковы запіс створаны", + "ToastUserPasswordMustChange": "Новы пароль не можа супадаць са старым", + "ToastUserRootRequireName": "Неабходна ўвесці імя карыстальніка адміністратара" } From 725192fbc0f5b289bc70a4eed451710359464ca6 Mon Sep 17 00:00:00 2001 From: advplyr Date: Tue, 11 Feb 2025 17:17:07 -0600 Subject: [PATCH 041/303] Version bump v2.19.1 --- client/package-lock.json | 4 ++-- client/package.json | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/client/package-lock.json b/client/package-lock.json index 7aa99d67..4d45011a 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -1,12 +1,12 @@ { "name": "audiobookshelf-client", - "version": "2.19.0", + "version": "2.19.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "audiobookshelf-client", - "version": "2.19.0", + "version": "2.19.1", "license": "ISC", "dependencies": { "@nuxtjs/axios": "^5.13.6", diff --git a/client/package.json b/client/package.json index 7b09854a..43d0457f 100644 --- a/client/package.json +++ b/client/package.json @@ -1,6 +1,6 @@ { "name": "audiobookshelf-client", - "version": "2.19.0", + "version": "2.19.1", "buildNumber": 1, "description": "Self-hosted audiobook and podcast client", "main": "index.js", diff --git a/package-lock.json b/package-lock.json index f0bca018..615c8d0f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "audiobookshelf", - "version": "2.19.0", + "version": "2.19.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "audiobookshelf", - "version": "2.19.0", + "version": "2.19.1", "license": "GPL-3.0", "dependencies": { "axios": "^0.27.2", diff --git a/package.json b/package.json index ce36e229..8cb0cdd5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "audiobookshelf", - "version": "2.19.0", + "version": "2.19.1", "buildNumber": 1, "description": "Self-hosted audiobook and podcast server", "main": "index.js", From a34813b3ab81fc96ccea0152a7ec400c4e31e70d Mon Sep 17 00:00:00 2001 From: advplyr Date: Wed, 12 Feb 2025 08:52:20 -0600 Subject: [PATCH 042/303] Fix server crash remove column name ambiguity #3966 --- server/utils/queries/libraryItemsBookFilters.js | 8 ++++---- server/utils/queries/libraryItemsPodcastFilters.js | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/server/utils/queries/libraryItemsBookFilters.js b/server/utils/queries/libraryItemsBookFilters.js index 3adb929e..65ab1fef 100644 --- a/server/utils/queries/libraryItemsBookFilters.js +++ b/server/utils/queries/libraryItemsBookFilters.js @@ -580,9 +580,9 @@ module.exports = { // When collapsing series and sorting by title then use the series name instead of the book title // for this set an attribute "display_title" to use in sorting if (global.ServerSettings.sortingIgnorePrefix) { - bookAttributes.include.push([Sequelize.literal(`IFNULL((SELECT s.nameIgnorePrefix FROM bookSeries AS bs, series AS s WHERE bs.seriesId = s.id AND bs.bookId = book.id AND bs.id IN (${bookSeriesToInclude.map((v) => `"${v.id}"`).join(', ')})), titleIgnorePrefix)`), 'display_title']) + bookAttributes.include.push([Sequelize.literal(`IFNULL((SELECT s.nameIgnorePrefix FROM bookSeries AS bs, series AS s WHERE bs.seriesId = s.id AND bs.bookId = book.id AND bs.id IN (${bookSeriesToInclude.map((v) => `"${v.id}"`).join(', ')})), \`libraryItem\`.\`titleIgnorePrefix\`)`), 'display_title']) } else { - bookAttributes.include.push([Sequelize.literal(`IFNULL((SELECT s.name FROM bookSeries AS bs, series AS s WHERE bs.seriesId = s.id AND bs.bookId = book.id AND bs.id IN (${bookSeriesToInclude.map((v) => `"${v.id}"`).join(', ')})), \`book\`.\`title\`)`), 'display_title']) + bookAttributes.include.push([Sequelize.literal(`IFNULL((SELECT s.name FROM bookSeries AS bs, series AS s WHERE bs.seriesId = s.id AND bs.bookId = book.id AND bs.id IN (${bookSeriesToInclude.map((v) => `"${v.id}"`).join(', ')})), \`libraryItem\`.\`title\`)`), 'display_title']) } } @@ -1035,8 +1035,8 @@ module.exports = { const textSearchQuery = await Database.createTextSearchQuery(query) - const matchTitle = textSearchQuery.matchExpression('title') - const matchSubtitle = textSearchQuery.matchExpression('subtitle') + const matchTitle = textSearchQuery.matchExpression('book.title') + const matchSubtitle = textSearchQuery.matchExpression('book.subtitle') // Search title, subtitle, asin, isbn const books = await Database.bookModel.findAll({ diff --git a/server/utils/queries/libraryItemsPodcastFilters.js b/server/utils/queries/libraryItemsPodcastFilters.js index 36241f33..0cd159ba 100644 --- a/server/utils/queries/libraryItemsPodcastFilters.js +++ b/server/utils/queries/libraryItemsPodcastFilters.js @@ -84,7 +84,7 @@ module.exports = { return [[Sequelize.literal(`\`podcast\`.\`author\` COLLATE NOCASE ${nullDir}`)]] } else if (sortBy === 'media.metadata.title') { if (global.ServerSettings.sortingIgnorePrefix) { - return [[Sequelize.literal('titleIgnorePrefix COLLATE NOCASE'), dir]] + return [[Sequelize.literal('`podcast`.`titleIgnorePrefix` COLLATE NOCASE'), dir]] } else { return [[Sequelize.literal('`podcast`.`title` COLLATE NOCASE'), dir]] } @@ -321,8 +321,8 @@ module.exports = { const textSearchQuery = await Database.createTextSearchQuery(query) - const matchTitle = textSearchQuery.matchExpression('title') - const matchAuthor = textSearchQuery.matchExpression('author') + const matchTitle = textSearchQuery.matchExpression('podcast.title') + const matchAuthor = textSearchQuery.matchExpression('podcast.author') // Search title, author, itunesId, itunesArtistId const podcasts = await Database.podcastModel.findAll({ From ebdf377fc186ee34c1700b8943cfc088b549369e Mon Sep 17 00:00:00 2001 From: advplyr Date: Wed, 12 Feb 2025 10:01:05 -0600 Subject: [PATCH 043/303] Version bump v2.19.2 --- client/package-lock.json | 4 ++-- client/package.json | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/client/package-lock.json b/client/package-lock.json index 4d45011a..7189685c 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -1,12 +1,12 @@ { "name": "audiobookshelf-client", - "version": "2.19.1", + "version": "2.19.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "audiobookshelf-client", - "version": "2.19.1", + "version": "2.19.2", "license": "ISC", "dependencies": { "@nuxtjs/axios": "^5.13.6", diff --git a/client/package.json b/client/package.json index 43d0457f..7b0530a2 100644 --- a/client/package.json +++ b/client/package.json @@ -1,6 +1,6 @@ { "name": "audiobookshelf-client", - "version": "2.19.1", + "version": "2.19.2", "buildNumber": 1, "description": "Self-hosted audiobook and podcast client", "main": "index.js", diff --git a/package-lock.json b/package-lock.json index 615c8d0f..637789aa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "audiobookshelf", - "version": "2.19.1", + "version": "2.19.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "audiobookshelf", - "version": "2.19.1", + "version": "2.19.2", "license": "GPL-3.0", "dependencies": { "axios": "^0.27.2", diff --git a/package.json b/package.json index 8cb0cdd5..a6dd6fb0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "audiobookshelf", - "version": "2.19.1", + "version": "2.19.2", "buildNumber": 1, "description": "Self-hosted audiobook and podcast server", "main": "index.js", From f460297dafa6ddf1ced9bdacaef8609626a9f392 Mon Sep 17 00:00:00 2001 From: Conner McCall Date: Thu, 13 Feb 2025 09:06:53 -0600 Subject: [PATCH 044/303] fix: allow upgrading HTTP to HTTPS for redirects Re: #3142 and #3658 When adding certain podcasts, the server encountered a redirect from an HTTP URL to an HTTPS domain, causing an error that was difficult for end users to diagnose without inspecting logs or HTML. This issue arose due to SSRF security measures that blocked such redirects. Instead of failing in these cases, we now detect when the error is caused by an HTTP-to-HTTPS upgrade. If confirmed, we upgrade the initial URL to HTTPS and resend the request. Since this change does not allow cross-protocol or cross-domain redirections, it remains secure while resolving most of the reported issues. Affected podcasts that are now fixed: - D&D is for Nerds - The New Yorker: The Writer's Voice - New Fiction from The New Yorker - Radiolab --- server/utils/podcastUtils.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/server/utils/podcastUtils.js b/server/utils/podcastUtils.js index 485fccfb..1ecb0a75 100644 --- a/server/utils/podcastUtils.js +++ b/server/utils/podcastUtils.js @@ -343,6 +343,14 @@ module.exports.getPodcastFeed = (feedUrl, excludeEpisodeMetadata = false) => { return payload.podcast }) .catch((error) => { + // Check for failures due to redirecting from http to https. If original url was http, upgrade to https and try again + if (error.code === 'ERR_FR_REDIRECTION_FAILURE' && error.cause.code === 'ERR_INVALID_PROTOCOL') { + if (feedUrl.startsWith('http://') && error.request._options.protocol === 'https:') { + Logger.info('Redirection from http to https detected. Upgrading Request', error.request._options.href) + feedUrl = feedUrl.replace('http://', 'https://') + return this.getPodcastFeed(feedUrl, excludeEpisodeMetadata) + } + } Logger.error('[podcastUtils] getPodcastFeed Error', error) return null }) From 5ca12eee19a8d64d0a2f029465e228da15ca799c Mon Sep 17 00:00:00 2001 From: advplyr Date: Thu, 13 Feb 2025 18:07:59 -0600 Subject: [PATCH 045/303] Fix count cache by stringify Symbols #3979 --- .../utils/queries/libraryItemsBookFilters.js | 5 +-- server/utils/stringifySequelizeQuery.js | 34 +++++++++++++++++++ 2 files changed, 37 insertions(+), 2 deletions(-) create mode 100644 server/utils/stringifySequelizeQuery.js diff --git a/server/utils/queries/libraryItemsBookFilters.js b/server/utils/queries/libraryItemsBookFilters.js index 65ab1fef..d446a5e9 100644 --- a/server/utils/queries/libraryItemsBookFilters.js +++ b/server/utils/queries/libraryItemsBookFilters.js @@ -5,7 +5,7 @@ const authorFilters = require('./authorFilters') const ShareManager = require('../../managers/ShareManager') const { profile } = require('../profiler') - +const stringifySequelizeQuery = require('../stringifySequelizeQuery') const countCache = new Map() module.exports = { @@ -345,7 +345,7 @@ module.exports = { }, async findAndCountAll(findOptions, limit, offset) { - const findOptionsKey = JSON.stringify(findOptions) + const findOptionsKey = stringifySequelizeQuery(findOptions) Logger.debug(`[LibraryItemsBookFilters] findOptionsKey: ${findOptionsKey}`) findOptions.limit = limit || null @@ -353,6 +353,7 @@ module.exports = { if (countCache.has(findOptionsKey)) { const rows = await Database.bookModel.findAll(findOptions) + return { rows, count: countCache.get(findOptionsKey) } } else { const result = await Database.bookModel.findAndCountAll(findOptions) diff --git a/server/utils/stringifySequelizeQuery.js b/server/utils/stringifySequelizeQuery.js new file mode 100644 index 00000000..a41e0650 --- /dev/null +++ b/server/utils/stringifySequelizeQuery.js @@ -0,0 +1,34 @@ +function stringifySequelizeQuery(findOptions) { + // Helper function to handle symbols in nested objects + function handleSymbols(obj) { + if (!obj || typeof obj !== 'object') return obj + + if (Array.isArray(obj)) { + return obj.map(handleSymbols) + } + + const newObj = {} + for (const [key, value] of Object.entries(obj)) { + // Handle Symbol keys from Object.getOwnPropertySymbols + Object.getOwnPropertySymbols(obj).forEach((sym) => { + newObj[`__Op.${sym.toString()}`] = handleSymbols(obj[sym]) + }) + + // Handle regular keys + if (typeof key === 'string') { + if (value && typeof value === 'object' && Object.getPrototypeOf(value) === Symbol.prototype) { + // Handle Symbol values + newObj[key] = `__Op.${value.toString()}` + } else { + // Recursively handle nested objects + newObj[key] = handleSymbols(value) + } + } + } + return newObj + } + + const sanitizedOptions = handleSymbols(findOptions) + return JSON.stringify(sanitizedOptions) +} +module.exports = stringifySequelizeQuery From c4d99a118fc0ebd613bf58dd73324a4776e942f8 Mon Sep 17 00:00:00 2001 From: advplyr Date: Fri, 14 Feb 2025 16:24:39 -0600 Subject: [PATCH 046/303] Fix chapter end sleep timer sometimes not stopping #3969 --- .../components/app/MediaPlayerContainer.vue | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/client/components/app/MediaPlayerContainer.vue b/client/components/app/MediaPlayerContainer.vue index cf22d322..8aa6188b 100644 --- a/client/components/app/MediaPlayerContainer.vue +++ b/client/components/app/MediaPlayerContainer.vue @@ -85,7 +85,8 @@ export default { displayTitle: null, currentPlaybackRate: 1, syncFailedToast: null, - coverAspectRatio: 1 + coverAspectRatio: 1, + lastChapterId: null } }, computed: { @@ -236,12 +237,16 @@ export default { } }, 1000) }, - checkChapterEnd(time) { + checkChapterEnd() { if (!this.currentChapter) return - const chapterEndTime = this.currentChapter.end - const tolerance = 0.75 - if (time >= chapterEndTime - tolerance) { - this.sleepTimerEnd() + + // Track chapter transitions by comparing current chapter with last chapter + if (this.lastChapterId !== this.currentChapter.id) { + // Chapter changed - if we had a previous chapter, this means we crossed a boundary + if (this.lastChapterId) { + this.sleepTimerEnd() + } + this.lastChapterId = this.currentChapter.id } }, sleepTimerEnd() { @@ -301,7 +306,7 @@ export default { } if (this.sleepTimerType === this.$constants.SleepTimerTypes.CHAPTER && this.sleepTimerSet) { - this.checkChapterEnd(time) + this.checkChapterEnd() } }, setDuration(duration) { From d9b206fe1cfdd6f6714917d68e4b1bf726e968b9 Mon Sep 17 00:00:00 2001 From: advplyr Date: Fri, 14 Feb 2025 16:56:37 -0600 Subject: [PATCH 047/303] Fix server crash when quick match all updates existing series sequence #3961 --- server/models/LibraryItem.js | 2 +- server/scanner/Scanner.js | 6 ------ 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/server/models/LibraryItem.js b/server/models/LibraryItem.js index ace6af43..5d23bc8f 100644 --- a/server/models/LibraryItem.js +++ b/server/models/LibraryItem.js @@ -103,7 +103,7 @@ class LibraryItem extends Model { { model: this.sequelize.models.series, through: { - attributes: ['sequence', 'createdAt'] + attributes: ['id', 'sequence', 'createdAt'] } } ] diff --git a/server/scanner/Scanner.js b/server/scanner/Scanner.js index 1a2a7aaf..c5c62532 100644 --- a/server/scanner/Scanner.js +++ b/server/scanner/Scanner.js @@ -48,13 +48,7 @@ class Scanner { let updatePayload = {} let hasUpdated = false - let existingAuthors = [] // Used for checking if authors or series are now empty - let existingSeries = [] - if (libraryItem.isBook) { - existingAuthors = libraryItem.media.authors.map((a) => a.id) - existingSeries = libraryItem.media.series.map((s) => s.id) - const searchISBN = options.isbn || libraryItem.media.isbn const searchASIN = options.asin || libraryItem.media.asin From 8ee5646d790d5bf5a1fef42079b686b93f40a9d1 Mon Sep 17 00:00:00 2001 From: mikiher Date: Sat, 15 Feb 2025 23:57:27 +0200 Subject: [PATCH 048/303] fix stringifySequelizeQuery and add tests --- server/utils/stringifySequelizeQuery.js | 49 +++++++---------- .../utils/stringifySequeslizeQuery.test.js | 52 +++++++++++++++++++ 2 files changed, 72 insertions(+), 29 deletions(-) create mode 100644 test/server/utils/stringifySequeslizeQuery.test.js diff --git a/server/utils/stringifySequelizeQuery.js b/server/utils/stringifySequelizeQuery.js index a41e0650..44d8b12d 100644 --- a/server/utils/stringifySequelizeQuery.js +++ b/server/utils/stringifySequelizeQuery.js @@ -1,34 +1,25 @@ function stringifySequelizeQuery(findOptions) { - // Helper function to handle symbols in nested objects - function handleSymbols(obj) { - if (!obj || typeof obj !== 'object') return obj - - if (Array.isArray(obj)) { - return obj.map(handleSymbols) - } - - const newObj = {} - for (const [key, value] of Object.entries(obj)) { - // Handle Symbol keys from Object.getOwnPropertySymbols - Object.getOwnPropertySymbols(obj).forEach((sym) => { - newObj[`__Op.${sym.toString()}`] = handleSymbols(obj[sym]) - }) - - // Handle regular keys - if (typeof key === 'string') { - if (value && typeof value === 'object' && Object.getPrototypeOf(value) === Symbol.prototype) { - // Handle Symbol values - newObj[key] = `__Op.${value.toString()}` - } else { - // Recursively handle nested objects - newObj[key] = handleSymbols(value) - } - } - } - return newObj + function isClass(func) { + return typeof func === 'function' && /^class\s/.test(func.toString()) } - const sanitizedOptions = handleSymbols(findOptions) - return JSON.stringify(sanitizedOptions) + function replacer(key, value) { + if (typeof value === 'object' && value !== null) { + const symbols = Object.getOwnPropertySymbols(value).reduce((acc, sym) => { + acc[sym.toString()] = value[sym] + return acc + }, {}) + + return { ...value, ...symbols } + } + + if (isClass(value)) { + return `${value.name}` + } + + return value + } + + return JSON.stringify(findOptions, replacer) } module.exports = stringifySequelizeQuery diff --git a/test/server/utils/stringifySequeslizeQuery.test.js b/test/server/utils/stringifySequeslizeQuery.test.js new file mode 100644 index 00000000..764acfd2 --- /dev/null +++ b/test/server/utils/stringifySequeslizeQuery.test.js @@ -0,0 +1,52 @@ +const { expect } = require('chai') +const stringifySequelizeQuery = require('../../../server/utils/stringifySequelizeQuery') +const Sequelize = require('sequelize') + +class DummyClass {} + +describe('stringifySequelizeQuery', () => { + it('should stringify a sequelize query containing an op', () => { + const query = { + where: { + name: 'John', + age: { + [Sequelize.Op.gt]: 20 + } + } + } + + const result = stringifySequelizeQuery(query) + expect(result).to.equal('{"where":{"name":"John","age":{"Symbol(gt)":20}}}') + }) + + it('should stringify a sequelize query containing a literal', () => { + const query = { + order: [[Sequelize.literal('libraryItem.title'), 'ASC']] + } + + const result = stringifySequelizeQuery(query) + expect(result).to.equal('{"order":{"0":{"0":{"val":"libraryItem.title"},"1":"ASC"}}}') + }) + + it('should stringify a sequelize query containing a class', () => { + const query = { + include: [ + { + model: DummyClass + } + ] + } + + const result = stringifySequelizeQuery(query) + expect(result).to.equal('{"include":{"0":{"model":"DummyClass"}}}') + }) + + it('should ignore non-class functions', () => { + const query = { + logging: (query) => console.log(query) + } + + const result = stringifySequelizeQuery(query) + expect(result).to.equal('{}') + }) +}) From 6a7418ad41305c7fbdee12d1513c0c32183b020e Mon Sep 17 00:00:00 2001 From: advplyr Date: Sat, 15 Feb 2025 17:55:56 -0600 Subject: [PATCH 049/303] Fix:Edit book cover tab local images overflowing #3986 --- client/components/modals/item/tabs/Cover.vue | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/components/modals/item/tabs/Cover.vue b/client/components/modals/item/tabs/Cover.vue index 69c11119..5b371fc4 100644 --- a/client/components/modals/item/tabs/Cover.vue +++ b/client/components/modals/item/tabs/Cover.vue @@ -1,7 +1,7 @@