From 2711b989e153669aaf869521ebbc0e9f006834ef Mon Sep 17 00:00:00 2001 From: Nicholas Wallace Date: Fri, 13 Sep 2024 16:55:48 -0700 Subject: [PATCH 01/44] Add: series migration to be unique --- .../v2.13.5-series-column-unique.js | 126 ++++++++++ server/models/Series.js | 6 + .../v2.13.5-series-column-unique.test.js | 226 ++++++++++++++++++ 3 files changed, 358 insertions(+) create mode 100644 server/migrations/v2.13.5-series-column-unique.js create mode 100644 test/server/migrations/v2.13.5-series-column-unique.test.js diff --git a/server/migrations/v2.13.5-series-column-unique.js b/server/migrations/v2.13.5-series-column-unique.js new file mode 100644 index 00000000..e7201bae --- /dev/null +++ b/server/migrations/v2.13.5-series-column-unique.js @@ -0,0 +1,126 @@ +const Logger = require('../Logger') + +/** + * @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. + */ + +/** + * This upward migration script cleans any duplicate series in the `Series` table and + * adds a unique index on the `name` and `libraryId` columns. + * + * @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('UPGRADE BEGIN: 2.13.5-series-column-unique ') + + // Use the queryInterface to get the series table and find duplicates in the `name` column + const [duplicates] = await queryInterface.sequelize.query(` + SELECT name, libraryId, MAX(updatedAt) AS latestUpdatedAt, COUNT(name) AS count + FROM Series + GROUP BY name, libraryId + HAVING COUNT(name) > 1 + `) + + // Print out how many duplicates were found + logger.info(`[2.13.5 migration] Found ${duplicates.length} duplicate series`) + + // Iterate over each duplicate series + for (const duplicate of duplicates) { + // Report the series name that is being deleted + logger.info(`[2.13.5 migration] Deduplicating series "${duplicate.name}" in library ${duplicate.libraryId}`) + + // Get all the most recent series which matches the `name` and `libraryId` + const [mostRecentSeries] = await queryInterface.sequelize.query( + ` + SELECT id + FROM Series + WHERE name = :name AND libraryId = :libraryId + ORDER BY updatedAt DESC + LIMIT 1 + `, + { + replacements: { + name: duplicate.name, + libraryId: duplicate.libraryId + }, + type: queryInterface.sequelize.QueryTypes.SELECT + } + ) + + if (mostRecentSeries) { + // Update all BookSeries records for this series to point to the most recent series + const [seriesUpdated] = await queryInterface.sequelize.query( + ` + UPDATE BookSeries + SET seriesId = :mostRecentSeriesId + WHERE seriesId IN ( + SELECT id + FROM Series + WHERE name = :name AND libraryId = :libraryId + AND id != :mostRecentSeriesId + ) + `, + { + replacements: { + name: duplicate.name, + libraryId: duplicate.libraryId, + mostRecentSeriesId: mostRecentSeries.id + } + } + ) + + // Delete the older series + const seriesDeleted = await queryInterface.sequelize.query( + ` + DELETE FROM Series + WHERE name = :name AND libraryId = :libraryId + AND id != :mostRecentSeriesId + `, + { + replacements: { + name: duplicate.name, + libraryId: duplicate.libraryId, + mostRecentSeriesId: mostRecentSeries.id + } + } + ) + } + } + + logger.info(`[2.13.5 migration] Deduplication complete`) + + // Create a unique index based on the name and library ID for the `Series` table + await queryInterface.addIndex('Series', ['name', 'libraryId'], { + unique: true, + name: 'unique_series_name_per_library' + }) + logger.info('Added unique index on Series.name and Series.libraryId') + + logger.info('UPGRADE END: 2.13.5-series-column-unique ') +} + +/** + * This removes the unique index on the `Series` table. + * + * @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('DOWNGRADE BEGIN: 2.13.5-series-column-unique ') + + // Remove the unique index + await queryInterface.removeIndex('Series', 'unique_series_name_per_library') + logger.info('Removed unique index on Series.name and Series.libraryId') + + logger.info('DOWNGRADE END: 2.13.5-series-column-unique ') +} + +module.exports = { up, down } diff --git a/server/models/Series.js b/server/models/Series.js index c57a1a11..731908e9 100644 --- a/server/models/Series.js +++ b/server/models/Series.js @@ -83,6 +83,12 @@ class Series extends Model { // collate: 'NOCASE' // }] // }, + { + // unique constraint on name and libraryId + fields: ['name', 'libraryId'], + unique: true, + name: 'unique_series_name_per_library' + }, { fields: ['libraryId'] } diff --git a/test/server/migrations/v2.13.5-series-column-unique.test.js b/test/server/migrations/v2.13.5-series-column-unique.test.js new file mode 100644 index 00000000..6d55b629 --- /dev/null +++ b/test/server/migrations/v2.13.5-series-column-unique.test.js @@ -0,0 +1,226 @@ +const { expect } = require('chai') +const sinon = require('sinon') +const { up, down } = require('../../../server/migrations/v2.13.5-series-column-unique') +const { Sequelize } = require('sequelize') +const Logger = require('../../../server/Logger') +const { query } = require('express') +const { logger } = require('sequelize/lib/utils/logger') +const e = require('express') + +describe('migration_example', () => { + let sequelize + let queryInterface + let loggerInfoStub + let series1Id + let series2Id + let series3Id + let series1Id_dup + let series3Id_dup + let book1Id + let book2Id + let book3Id + let book4Id + let book5Id + let library1Id + let library2Id + let bookSeries1Id + let bookSeries2Id + let bookSeries3Id + let bookSeries1Id_dup + let bookSeries3Id_dup + + beforeEach(() => { + sequelize = new Sequelize({ dialect: 'sqlite', storage: ':memory:', logging: false }) + queryInterface = sequelize.getQueryInterface() + loggerInfoStub = sinon.stub(Logger, 'info') + }) + + afterEach(() => { + sinon.restore() + }) + + describe('up', () => { + beforeEach(async () => { + await queryInterface.createTable('Series', { + id: { type: Sequelize.UUID, primaryKey: true }, + name: { type: Sequelize.STRING, allowNull: false }, + libraryId: { type: Sequelize.UUID, allowNull: false }, + createdAt: { type: Sequelize.DATE, allowNull: false }, + updatedAt: { type: Sequelize.DATE, allowNull: false } + }) + await queryInterface.createTable('BookSeries', { + id: { type: Sequelize.UUID, primaryKey: true }, + bookId: { type: Sequelize.UUID, allowNull: false }, + seriesId: { type: Sequelize.UUID, allowNull: false } + }) + // Set UUIDs for the tests + series1Id = 'fc086255-3fd2-4a95-8a28-840d9206501b' + series2Id = '70f46ac2-ee48-4b3c-9822-933cc15c29bd' + series3Id = '01cac008-142b-4e15-b0ff-cf7cc2c5b64e' + series1Id_dup = 'ad0b3b3b-4b3b-4b3b-4b3b-4b3b4b3b4b3b' + series3Id_dup = '4b3b4b3b-4b3b-4b3b-4b3b-4b3b4b3b4b3b' + series1Id_dup2 = '0123456a-4b3b-4b3b-4b3b-4b3b4b3b4b3b' + book1Id = '4a38b6e5-0ae4-4de4-b119-4e33891bd63f' + book2Id = '8bc2e61d-47f6-42ef-a3f4-93cf2f1de82f' + book3Id = 'ec9bbaaf-1e55-457f-b59c-bd2bd955a404' + book4Id = '876f3b3b-4b3b-4b3b-4b3b-4b3b4b3b4b3b' + book5Id = '4e5b4b3b-4b3b-4b3b-4b3b-4b3b4b3b4b3b' + book6Id = 'abcda123-4b3b-4b3b-4b3b-4b3b4b3b4b3b' + library1Id = '3a5a1c7c-a914-472e-88b0-b871ceae63e7' + library2Id = 'fd6c324a-4f3a-4bb0-99d6-7a330e765e7e' + bookSeries1Id = 'eca24687-2241-4ffa-a9b3-02a0ba03c763' + bookSeries2Id = '56f56105-813b-4395-9689-fd04198e7d5d' + bookSeries3Id = '404a1761-c710-4d86-9d78-68d9a9c0fb6b' + bookSeries1Id_dup = '8bea3b3b-4b3b-4b3b-4b3b-4b3b4b3b4b3b' + bookSeries3Id_dup = '89656a3b-4b3b-4b3b-4b3b-4b3b4b3b4b3b' + bookSeries1Id_dup2 = '9bea3b3b-4b3b-4b3b-4b3b-4b3b4b3b4b3b' + }) + afterEach(async () => { + await queryInterface.dropTable('Series') + await queryInterface.dropTable('BookSeries') + }) + it('upgrade with no duplicate series', async () => { + // Add some entries to the Series table using the UUID for the ids + await queryInterface.bulkInsert('Series', [ + { 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() } + ]) + // Add some entries to the BookSeries table + await queryInterface.bulkInsert('BookSeries', [ + { id: bookSeries1Id, bookId: book1Id, seriesId: series1Id }, + { id: bookSeries2Id, bookId: book2Id, seriesId: series2Id }, + { id: bookSeries3Id, bookId: book3Id, seriesId: series3Id } + ]) + + await up({ context: { queryInterface, logger: Logger } }) + + expect(loggerInfoStub.callCount).to.equal(5) + expect(loggerInfoStub.getCall(0).calledWith(sinon.match('UPGRADE BEGIN: 2.13.5-series-column-unique '))).to.be.true + expect(loggerInfoStub.getCall(1).calledWith(sinon.match('[2.13.5 migration] Found 0 duplicate series'))).to.be.true + expect(loggerInfoStub.getCall(2).calledWith(sinon.match('[2.13.5 migration] Deduplication complete'))).to.be.true + expect(loggerInfoStub.getCall(3).calledWith(sinon.match('Added unique index on Series.name and Series.libraryId'))).to.be.true + expect(loggerInfoStub.getCall(4).calledWith(sinon.match('UPGRADE END: 2.13.5-series-column-unique '))).to.be.true + // Validate rows in tables + const series = await queryInterface.sequelize.query('SELECT "id", "name", "libraryId" FROM Series', { type: queryInterface.sequelize.QueryTypes.SELECT }) + expect(series).to.have.length(3) + expect(series).to.deep.include({ id: series1Id, name: 'Series 1', libraryId: library1Id }) + expect(series).to.deep.include({ id: series2Id, name: 'Series 2', libraryId: library2Id }) + expect(series).to.deep.include({ id: series3Id, name: 'Series 3', libraryId: library1Id }) + const bookSeries = await queryInterface.sequelize.query('SELECT "id", "bookId", "seriesId" FROM BookSeries', { type: queryInterface.sequelize.QueryTypes.SELECT }) + expect(bookSeries).to.have.length(3) + expect(bookSeries).to.deep.include({ id: bookSeries1Id, bookId: book1Id, seriesId: series1Id }) + expect(bookSeries).to.deep.include({ id: bookSeries2Id, bookId: book2Id, seriesId: series2Id }) + expect(bookSeries).to.deep.include({ id: bookSeries3Id, bookId: book3Id, seriesId: series3Id }) + }) + it('upgrade with duplicate series', async () => { + // Add some entries to the Series table using the UUID for the ids + await queryInterface.bulkInsert('Series', [ + { 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() } + ]) + // Add some entries to the BookSeries table + await queryInterface.bulkInsert('BookSeries', [ + { id: bookSeries1Id, bookId: book1Id, seriesId: series1Id }, + { id: bookSeries2Id, bookId: book2Id, seriesId: series2Id }, + { id: bookSeries3Id, bookId: book3Id, seriesId: series3Id }, + { id: bookSeries1Id_dup, bookId: book4Id, seriesId: series1Id_dup }, + { id: bookSeries3Id_dup, bookId: book5Id, seriesId: series3Id_dup }, + { id: bookSeries1Id_dup2, bookId: book6Id, seriesId: series1Id_dup2 } + ]) + + await up({ context: { queryInterface, logger: Logger } }) + + expect(loggerInfoStub.callCount).to.equal(7) + expect(loggerInfoStub.getCall(0).calledWith(sinon.match('UPGRADE BEGIN: 2.13.5-series-column-unique '))).to.be.true + expect(loggerInfoStub.getCall(1).calledWith(sinon.match('[2.13.5 migration] Found 2 duplicate series'))).to.be.true + expect(loggerInfoStub.getCall(2).calledWith(sinon.match('[2.13.5 migration] Deduplicating series "Series 1" in library 3a5a1c7c-a914-472e-88b0-b871ceae63e7'))).to.be.true + expect(loggerInfoStub.getCall(3).calledWith(sinon.match('[2.13.5 migration] Deduplicating series "Series 3" in library 3a5a1c7c-a914-472e-88b0-b871ceae63e7'))).to.be.true + expect(loggerInfoStub.getCall(4).calledWith(sinon.match('[2.13.5 migration] Deduplication complete'))).to.be.true + expect(loggerInfoStub.getCall(5).calledWith(sinon.match('Added unique index on Series.name and Series.libraryId'))).to.be.true + expect(loggerInfoStub.getCall(6).calledWith(sinon.match('UPGRADE END: 2.13.5-series-column-unique '))).to.be.true + // Validate rows + const series = await queryInterface.sequelize.query('SELECT "id", "name", "libraryId" FROM Series', { type: queryInterface.sequelize.QueryTypes.SELECT }) + expect(series).to.have.length(3) + expect(series).to.deep.include({ id: series1Id, name: 'Series 1', libraryId: library1Id }) + expect(series).to.deep.include({ id: series2Id, name: 'Series 2', libraryId: library2Id }) + expect(series).to.deep.include({ id: series3Id, name: 'Series 3', libraryId: library1Id }) + const bookSeries = await queryInterface.sequelize.query('SELECT "id", "bookId", "seriesId" FROM BookSeries', { type: queryInterface.sequelize.QueryTypes.SELECT }) + expect(bookSeries).to.have.length(6) + expect(bookSeries).to.deep.include({ id: bookSeries1Id, bookId: book1Id, seriesId: series1Id }) + expect(bookSeries).to.deep.include({ id: bookSeries2Id, bookId: book2Id, seriesId: series2Id }) + expect(bookSeries).to.deep.include({ id: bookSeries3Id, bookId: book3Id, seriesId: series3Id }) + expect(bookSeries).to.deep.include({ id: bookSeries1Id_dup, bookId: book4Id, seriesId: series1Id }) + expect(bookSeries).to.deep.include({ id: bookSeries3Id_dup, bookId: book5Id, seriesId: series3Id }) + expect(bookSeries).to.deep.include({ id: bookSeries1Id_dup2, bookId: book6Id, seriesId: series1Id }) + }) + it('update with same series name in different libraries', async () => { + // Add some entries to the Series table using the UUID for the ids + await queryInterface.bulkInsert('Series', [ + { id: series1Id, name: 'Series 1', libraryId: library1Id, createdAt: new Date(), updatedAt: new Date() }, + { id: series2Id, name: 'Series 1', libraryId: library2Id, createdAt: new Date(), updatedAt: new Date() } + ]) + // Add some entries to the BookSeries table + await queryInterface.bulkInsert('BookSeries', [ + { id: bookSeries1Id, bookId: book1Id, seriesId: series1Id }, + { id: bookSeries2Id, bookId: book2Id, seriesId: series2Id } + ]) + + await up({ context: { queryInterface, logger: Logger } }) + + expect(loggerInfoStub.callCount).to.equal(5) + expect(loggerInfoStub.getCall(0).calledWith(sinon.match('UPGRADE BEGIN: 2.13.5-series-column-unique '))).to.be.true + expect(loggerInfoStub.getCall(1).calledWith(sinon.match('[2.13.5 migration] Found 0 duplicate series'))).to.be.true + expect(loggerInfoStub.getCall(2).calledWith(sinon.match('[2.13.5 migration] Deduplication complete'))).to.be.true + expect(loggerInfoStub.getCall(3).calledWith(sinon.match('Added unique index on Series.name and Series.libraryId'))).to.be.true + expect(loggerInfoStub.getCall(4).calledWith(sinon.match('UPGRADE END: 2.13.5-series-column-unique '))).to.be.true + // Validate rows + const series = await queryInterface.sequelize.query('SELECT "id", "name", "libraryId" FROM Series', { type: queryInterface.sequelize.QueryTypes.SELECT }) + expect(series).to.have.length(2) + expect(series).to.deep.include({ id: series1Id, name: 'Series 1', libraryId: library1Id }) + expect(series).to.deep.include({ id: series2Id, name: 'Series 1', libraryId: library2Id }) + const bookSeries = await queryInterface.sequelize.query('SELECT "id", "bookId", "seriesId" FROM BookSeries', { type: queryInterface.sequelize.QueryTypes.SELECT }) + expect(bookSeries).to.have.length(2) + expect(bookSeries).to.deep.include({ id: bookSeries1Id, bookId: book1Id, seriesId: series1Id }) + expect(bookSeries).to.deep.include({ id: bookSeries2Id, bookId: book2Id, seriesId: series2Id }) + }) + }) + + describe('down', () => { + beforeEach(async () => { + await queryInterface.createTable('Series', { + id: { type: Sequelize.UUID, primaryKey: true }, + name: { type: Sequelize.STRING, allowNull: false }, + libraryId: { type: Sequelize.UUID, allowNull: false }, + createdAt: { type: Sequelize.DATE, allowNull: false }, + updatedAt: { type: Sequelize.DATE, allowNull: false } + }) + await queryInterface.createTable('BookSeries', { + id: { type: Sequelize.UUID, primaryKey: true }, + bookId: { type: Sequelize.UUID, allowNull: false }, + seriesId: { type: Sequelize.UUID, allowNull: false } + }) + }) + it('should not have unique constraint on series name and libraryId', async () => { + await up({ context: { queryInterface, logger: Logger } }) + await down({ context: { queryInterface, logger: Logger } }) + + expect(loggerInfoStub.callCount).to.equal(8) + expect(loggerInfoStub.getCall(0).calledWith(sinon.match('UPGRADE BEGIN: 2.13.5-series-column-unique '))).to.be.true + expect(loggerInfoStub.getCall(1).calledWith(sinon.match('[2.13.5 migration] Found 0 duplicate series'))).to.be.true + expect(loggerInfoStub.getCall(2).calledWith(sinon.match('[2.13.5 migration] Deduplication complete'))).to.be.true + expect(loggerInfoStub.getCall(3).calledWith(sinon.match('Added unique index on Series.name and Series.libraryId'))).to.be.true + expect(loggerInfoStub.getCall(4).calledWith(sinon.match('UPGRADE END: 2.13.5-series-column-unique '))).to.be.true + expect(loggerInfoStub.getCall(5).calledWith(sinon.match('DOWNGRADE BEGIN: 2.13.5-series-column-unique '))).to.be.true + expect(loggerInfoStub.getCall(6).calledWith(sinon.match('Removed unique index on Series.name and Series.libraryId'))).to.be.true + expect(loggerInfoStub.getCall(7).calledWith(sinon.match('DOWNGRADE END: 2.13.5-series-column-unique '))).to.be.true + // Ensure index does not exist + const indexes = await queryInterface.showIndex('Series') + expect(indexes).to.not.deep.include({ tableName: 'Series', unique: true, fields: ['name', 'libraryId'], name: 'unique_series_name_per_library' }) + }) + }) +}) From c163f84aec65da8de7d31390e100d83dff86312a Mon Sep 17 00:00:00 2001 From: Nicholas Wallace Date: Fri, 13 Sep 2024 17:01:48 -0700 Subject: [PATCH 02/44] Update migration changelog for series name unique --- server/migrations/changelog.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/server/migrations/changelog.md b/server/migrations/changelog.md index 2e3c295a..bac3ec25 100644 --- a/server/migrations/changelog.md +++ b/server/migrations/changelog.md @@ -2,6 +2,6 @@ Please add a record of every database migration that you create to this file. This will help us keep track of changes to the database schema over time. -| Server Version | Migration Script Name | Description | -| -------------- | --------------------- | ----------- | -| | | | +| Server Version | Migration Script Name | Description | +| -------------- | ---------------------------- | ------------------------------------------------- | +| v2.13.5 | v2.13.5-series-column-unique | Series must have unique names in the same library | From 8ae62da1389cd4fe87ba4327e9d76a66237b087a Mon Sep 17 00:00:00 2001 From: advplyr Date: Sat, 14 Sep 2024 10:40:01 -0500 Subject: [PATCH 03/44] Update migration unit test name --- test/server/migrations/v2.13.5-series-column-unique.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/server/migrations/v2.13.5-series-column-unique.test.js b/test/server/migrations/v2.13.5-series-column-unique.test.js index 6d55b629..ed950a01 100644 --- a/test/server/migrations/v2.13.5-series-column-unique.test.js +++ b/test/server/migrations/v2.13.5-series-column-unique.test.js @@ -7,7 +7,7 @@ const { query } = require('express') const { logger } = require('sequelize/lib/utils/logger') const e = require('express') -describe('migration_example', () => { +describe('migration-v2.13.5-series-column-unique', () => { let sequelize let queryInterface let loggerInfoStub From 868659a2f1260d350694ffeda0d59c58d2aa2769 Mon Sep 17 00:00:00 2001 From: Nicholas Wallace Date: Sat, 14 Sep 2024 11:44:19 -0700 Subject: [PATCH 04/44] Add: unique constraint on bookseries table --- .../v2.13.5-series-column-unique.test.js | 30 ++++++++++++------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/test/server/migrations/v2.13.5-series-column-unique.test.js b/test/server/migrations/v2.13.5-series-column-unique.test.js index ed950a01..d98fb4f7 100644 --- a/test/server/migrations/v2.13.5-series-column-unique.test.js +++ b/test/server/migrations/v2.13.5-series-column-unique.test.js @@ -48,11 +48,16 @@ describe('migration-v2.13.5-series-column-unique', () => { createdAt: { type: Sequelize.DATE, allowNull: false }, updatedAt: { type: Sequelize.DATE, allowNull: false } }) - await queryInterface.createTable('BookSeries', { - id: { type: Sequelize.UUID, primaryKey: true }, - bookId: { type: Sequelize.UUID, allowNull: false }, - seriesId: { type: Sequelize.UUID, allowNull: false } - }) + // Create a table for BookSeries, with a unique constraint of bookId and seriesId + await queryInterface.createTable( + 'BookSeries', + { + id: { type: Sequelize.UUID, primaryKey: true }, + bookId: { type: Sequelize.UUID, allowNull: false }, + seriesId: { type: Sequelize.UUID, allowNull: false } + }, + { uniqueKeys: { book_series_unique: { fields: ['bookId', 'seriesId'] } } } + ) // Set UUIDs for the tests series1Id = 'fc086255-3fd2-4a95-8a28-840d9206501b' series2Id = '70f46ac2-ee48-4b3c-9822-933cc15c29bd' @@ -199,11 +204,16 @@ describe('migration-v2.13.5-series-column-unique', () => { createdAt: { type: Sequelize.DATE, allowNull: false }, updatedAt: { type: Sequelize.DATE, allowNull: false } }) - await queryInterface.createTable('BookSeries', { - id: { type: Sequelize.UUID, primaryKey: true }, - bookId: { type: Sequelize.UUID, allowNull: false }, - seriesId: { type: Sequelize.UUID, allowNull: false } - }) + // Create a table for BookSeries, with a unique constraint of bookId and seriesId + await queryInterface.createTable( + 'BookSeries', + { + id: { type: Sequelize.UUID, primaryKey: true }, + bookId: { type: Sequelize.UUID, allowNull: false }, + seriesId: { type: Sequelize.UUID, allowNull: false } + }, + { uniqueKeys: { book_series_unique: { fields: ['bookId', 'seriesId'] } } } + ) }) it('should not have unique constraint on series name and libraryId', async () => { await up({ context: { queryInterface, logger: Logger } }) From fa451f362b0a83390bbe9dbc3b30abea0af21d8d Mon Sep 17 00:00:00 2001 From: Nicholas Wallace Date: Sat, 14 Sep 2024 12:11:31 -0700 Subject: [PATCH 05/44] Add: tests for one book in duplicate series --- .../v2.13.5-series-column-unique.test.js | 102 ++++++++++++++++-- 1 file changed, 95 insertions(+), 7 deletions(-) diff --git a/test/server/migrations/v2.13.5-series-column-unique.test.js b/test/server/migrations/v2.13.5-series-column-unique.test.js index d98fb4f7..19dc4a8e 100644 --- a/test/server/migrations/v2.13.5-series-column-unique.test.js +++ b/test/server/migrations/v2.13.5-series-column-unique.test.js @@ -53,6 +53,7 @@ describe('migration-v2.13.5-series-column-unique', () => { 'BookSeries', { id: { type: Sequelize.UUID, primaryKey: true }, + sequence: { type: Sequelize.STRING, allowNull: true }, bookId: { type: Sequelize.UUID, allowNull: false }, seriesId: { type: Sequelize.UUID, allowNull: false } }, @@ -93,9 +94,9 @@ describe('migration-v2.13.5-series-column-unique', () => { ]) // Add some entries to the BookSeries table await queryInterface.bulkInsert('BookSeries', [ - { id: bookSeries1Id, bookId: book1Id, seriesId: series1Id }, + { id: bookSeries1Id, sequence: '1', bookId: book1Id, seriesId: series1Id }, { id: bookSeries2Id, bookId: book2Id, seriesId: series2Id }, - { id: bookSeries3Id, bookId: book3Id, seriesId: series3Id } + { id: bookSeries3Id, sequence: '1', bookId: book3Id, seriesId: series3Id } ]) await up({ context: { queryInterface, logger: Logger } }) @@ -112,13 +113,13 @@ describe('migration-v2.13.5-series-column-unique', () => { expect(series).to.deep.include({ id: series1Id, name: 'Series 1', libraryId: library1Id }) expect(series).to.deep.include({ id: series2Id, name: 'Series 2', libraryId: library2Id }) expect(series).to.deep.include({ id: series3Id, name: 'Series 3', libraryId: library1Id }) - const bookSeries = await queryInterface.sequelize.query('SELECT "id", "bookId", "seriesId" FROM BookSeries', { type: queryInterface.sequelize.QueryTypes.SELECT }) + const bookSeries = await queryInterface.sequelize.query('SELECT "id", "sequence", "bookId", "seriesId" FROM BookSeries', { type: queryInterface.sequelize.QueryTypes.SELECT }) expect(bookSeries).to.have.length(3) - expect(bookSeries).to.deep.include({ id: bookSeries1Id, bookId: book1Id, seriesId: series1Id }) - expect(bookSeries).to.deep.include({ id: bookSeries2Id, bookId: book2Id, seriesId: series2Id }) - expect(bookSeries).to.deep.include({ id: bookSeries3Id, bookId: book3Id, seriesId: series3Id }) + expect(bookSeries).to.deep.include({ id: bookSeries1Id, sequence: '1', bookId: book1Id, seriesId: series1Id }) + expect(bookSeries).to.deep.include({ id: bookSeries2Id, sequence: null, bookId: book2Id, seriesId: series2Id }) + expect(bookSeries).to.deep.include({ id: bookSeries3Id, sequence: '1', bookId: book3Id, seriesId: series3Id }) }) - it('upgrade with duplicate series', async () => { + it('upgrade with duplicate series and no sequence', async () => { // Add some entries to the Series table using the UUID for the ids await queryInterface.bulkInsert('Series', [ { id: series1Id, name: 'Series 1', libraryId: library1Id, createdAt: new Date(), updatedAt: new Date() }, @@ -193,6 +194,93 @@ describe('migration-v2.13.5-series-column-unique', () => { expect(bookSeries).to.deep.include({ id: bookSeries1Id, bookId: book1Id, seriesId: series1Id }) expect(bookSeries).to.deep.include({ id: bookSeries2Id, bookId: book2Id, seriesId: series2Id }) }) + it('upgrade with one book in two of the same series, both sequence are null', async () => { + // Create two different series with the same name in the same library + await queryInterface.bulkInsert('Series', [ + { id: series1Id, name: 'Series 1', libraryId: library1Id, createdAt: new Date(), updatedAt: new Date() }, + { id: series2Id, name: 'Series 1', libraryId: library1Id, createdAt: new Date(), updatedAt: new Date() } + ]) + // Create a book that is in both series + await queryInterface.bulkInsert('BookSeries', [ + { id: bookSeries1Id, bookId: book1Id, seriesId: series1Id }, + { id: bookSeries2Id, bookId: book1Id, seriesId: series2Id } + ]) + + await up({ context: { queryInterface, logger: Logger } }) + + expect(loggerInfoStub.callCount).to.equal(6) + expect(loggerInfoStub.getCall(0).calledWith(sinon.match('UPGRADE BEGIN: 2.13.5-series-column-unique '))).to.be.true + expect(loggerInfoStub.getCall(1).calledWith(sinon.match('[2.13.5 migration] Found 1 duplicate series'))).to.be.true + expect(loggerInfoStub.getCall(2).calledWith(sinon.match('[2.13.5 migration] Deduplicating series "Series 1" in library 3a5a1c7c-a914-472e-88b0-b871ceae63e7'))).to.be.true + expect(loggerInfoStub.getCall(3).calledWith(sinon.match('[2.13.5 migration] Deduplication complete'))).to.be.true + expect(loggerInfoStub.getCall(4).calledWith(sinon.match('Added unique index on Series.name and Series.libraryId')).to.be.true) + expect(loggerInfoStub.getCall(5).calledWith(sinon.match('UPGRADE END: 2.13.5-series-column-unique '))).to.be.true + // validate rows + const series = await queryInterface.sequelize.query('SELECT "id", "name", "libraryId" FROM Series', { type: queryInterface.sequelize.QueryTypes.SELECT }) + expect(series).to.have.length(1) + expect(series).to.deep.include({ id: series1Id, name: 'Series 1', libraryId: library1Id }) + const bookSeries = await queryInterface.sequelize.query('SELECT "id", "sequence", "bookId", "seriesId" FROM BookSeries', { type: queryInterface.sequelize.QueryTypes.SELECT }) + expect(bookSeries).to.have.length(1) + expect(bookSeries).to.deep.include({ id: bookSeries1Id, sequence: null, bookId: book1Id, seriesId: series1Id }) + }) + it('upgrade with one book in two of the same series, one sequence is null', async () => { + // Create two different series with the same name in the same library + await queryInterface.bulkInsert('Series', [ + { id: series1Id, name: 'Series 1', libraryId: library1Id, createdAt: new Date(), updatedAt: new Date() }, + { id: series2Id, name: 'Series 1', libraryId: library1Id, createdAt: new Date(), updatedAt: new Date() } + ]) + // Create a book that is in both series + await queryInterface.bulkInsert('BookSeries', [ + { id: bookSeries1Id, sequence: '1', bookId: book1Id, seriesId: series1Id }, + { id: bookSeries2Id, bookId: book1Id, seriesId: series2Id } + ]) + + await up({ context: { queryInterface, logger: Logger } }) + + expect(loggerInfoStub.callCount).to.equal(6) + expect(loggerInfoStub.getCall(0).calledWith(sinon.match('UPGRADE BEGIN: 2.13.5-series-column-unique '))).to.be.true + expect(loggerInfoStub.getCall(1).calledWith(sinon.match('[2.13.5 migration] Found 1 duplicate series'))).to.be.true + expect(loggerInfoStub.getCall(2).calledWith(sinon.match('[2.13.5 migration] Deduplicating series "Series 1" in library 3a5a1c7c-a914-472e-88b0-b871ceae63e7'))).to.be.true + expect(loggerInfoStub.getCall(3).calledWith(sinon.match('[2.13.5 migration] Deduplication complete'))).to.be.true + expect(loggerInfoStub.getCall(4).calledWith(sinon.match('Added unique index on Series.name and Series.libraryId')).to.be.true) + expect(loggerInfoStub.getCall(5).calledWith(sinon.match('UPGRADE END: 2.13.5-series-column-unique '))).to.be.true + // validate rows + const series = await queryInterface.sequelize.query('SELECT "id", "name", "libraryId" FROM Series', { type: queryInterface.sequelize.QueryTypes.SELECT }) + expect(series).to.have.length(1) + expect(series).to.deep.include({ id: series1Id, name: 'Series 1', libraryId: library1Id }) + const bookSeries = await queryInterface.sequelize.query('SELECT "id", "sequence", "bookId", "seriesId" FROM BookSeries', { type: queryInterface.sequelize.QueryTypes.SELECT }) + expect(bookSeries).to.have.length(1) + expect(bookSeries).to.deep.include({ id: bookSeries1Id, sequence: '1', bookId: book1Id, seriesId: series1Id }) + }) + it('upgrade with one book in two of the same series, both sequence are not null', async () => { + // Create two different series with the same name in the same library + await queryInterface.bulkInsert('Series', [ + { id: series1Id, name: 'Series 1', libraryId: library1Id, createdAt: new Date(), updatedAt: new Date() }, + { id: series2Id, name: 'Series 1', libraryId: library1Id, createdAt: new Date(), updatedAt: new Date() } + ]) + // Create a book that is in both series + await queryInterface.bulkInsert('BookSeries', [ + { id: bookSeries1Id, sequence: '1', bookId: book1Id, seriesId: series1Id }, + { id: bookSeries2Id, sequence: '2', bookId: book1Id, seriesId: series2Id } + ]) + + await up({ context: { queryInterface, logger: Logger } }) + + expect(loggerInfoStub.callCount).to.equal(6) + expect(loggerInfoStub.getCall(0).calledWith(sinon.match('UPGRADE BEGIN: 2.13.5-series-column-unique '))).to.be.true + expect(loggerInfoStub.getCall(1).calledWith(sinon.match('[2.13.5 migration] Found 1 duplicate series'))).to.be.true + expect(loggerInfoStub.getCall(2).calledWith(sinon.match('[2.13.5 migration] Deduplicating series "Series 1" in library 3a5a1c7c-a914-472e-88b0-b871ceae63e7'))).to.be.true + expect(loggerInfoStub.getCall(3).calledWith(sinon.match('[2.13.5 migration] Deduplication complete'))).to.be.true + expect(loggerInfoStub.getCall(4).calledWith(sinon.match('Added unique index on Series.name and Series.libraryId')).to.be.true) + expect(loggerInfoStub.getCall(5).calledWith(sinon.match('UPGRADE END: 2.13.5-series-column-unique '))).to.be.true + // validate rows + const series = await queryInterface.sequelize.query('SELECT "id", "name", "libraryId" FROM Series', { type: queryInterface.sequelize.QueryTypes.SELECT }) + expect(series).to.have.length(1) + expect(series).to.deep.include({ id: series1Id, name: 'Series 1', libraryId: library1Id }) + const bookSeries = await queryInterface.sequelize.query('SELECT "id", "sequence", "bookId", "seriesId" FROM BookSeries', { type: queryInterface.sequelize.QueryTypes.SELECT }) + expect(bookSeries).to.have.length(1) + expect(bookSeries).to.deep.include({ id: bookSeries1Id, sequence: '1, 2', bookId: book1Id, seriesId: series1Id }) + }) }) describe('down', () => { From 999ada03d16fd5b6faf387c757c454e74dcb50a6 Mon Sep 17 00:00:00 2001 From: Nicholas Wallace Date: Sat, 14 Sep 2024 14:36:47 -0700 Subject: [PATCH 06/44] Fix: missing variables --- test/server/migrations/v2.13.5-series-column-unique.test.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/test/server/migrations/v2.13.5-series-column-unique.test.js b/test/server/migrations/v2.13.5-series-column-unique.test.js index 19dc4a8e..5ce5a465 100644 --- a/test/server/migrations/v2.13.5-series-column-unique.test.js +++ b/test/server/migrations/v2.13.5-series-column-unique.test.js @@ -16,11 +16,13 @@ describe('migration-v2.13.5-series-column-unique', () => { let series3Id let series1Id_dup let series3Id_dup + let series1Id_dup2 let book1Id let book2Id let book3Id let book4Id let book5Id + let book6Id let library1Id let library2Id let bookSeries1Id @@ -28,6 +30,7 @@ describe('migration-v2.13.5-series-column-unique', () => { let bookSeries3Id let bookSeries1Id_dup let bookSeries3Id_dup + let bookSeries1Id_dup2 beforeEach(() => { sequelize = new Sequelize({ dialect: 'sqlite', storage: ':memory:', logging: false }) @@ -164,7 +167,7 @@ describe('migration-v2.13.5-series-column-unique', () => { expect(bookSeries).to.deep.include({ id: bookSeries3Id_dup, bookId: book5Id, seriesId: series3Id }) expect(bookSeries).to.deep.include({ id: bookSeries1Id_dup2, bookId: book6Id, seriesId: series1Id }) }) - it('update with same series name in different libraries', async () => { + it('upgrade with same series name in different libraries', async () => { // Add some entries to the Series table using the UUID for the ids await queryInterface.bulkInsert('Series', [ { id: series1Id, name: 'Series 1', libraryId: library1Id, createdAt: new Date(), updatedAt: new Date() }, From 836d772cd4502f109a06d8c78f824a03bad0d249 Mon Sep 17 00:00:00 2001 From: Nicholas Wallace Date: Sat, 14 Sep 2024 15:23:29 -0700 Subject: [PATCH 07/44] Update: remove the same book if occurs multiple times in duplicate series --- .../v2.13.5-series-column-unique.js | 74 ++++++++++++++++++- 1 file changed, 73 insertions(+), 1 deletion(-) diff --git a/server/migrations/v2.13.5-series-column-unique.js b/server/migrations/v2.13.5-series-column-unique.js index e7201bae..21e4d4d5 100644 --- a/server/migrations/v2.13.5-series-column-unique.js +++ b/server/migrations/v2.13.5-series-column-unique.js @@ -20,7 +20,15 @@ async function up({ context: { queryInterface, logger } }) { // Upwards migration script logger.info('UPGRADE BEGIN: 2.13.5-series-column-unique ') - // Use the queryInterface to get the series table and find duplicates in the `name` column + // The steps taken to deduplicate the series are as follows: + // 1. Find all duplicate series in the `Series` table. + // 2. Iterate over the duplicate series and find all book IDs that are associated with the duplicate series in `bookSeries` table. + // 2.a For each book ID, check if the ID occurs multiple times for the duplicate series. + // 2.b If so, keep only one of the rows that has this bookId and seriesId. + // 3. Update `bookSeries` table to point to the most recent series. + // 4. Delete the older series. + + // Use the queryInterface to get the series table and find duplicates in the `name` and `libraryId` column const [duplicates] = await queryInterface.sequelize.query(` SELECT name, libraryId, MAX(updatedAt) AS latestUpdatedAt, COUNT(name) AS count FROM Series @@ -36,6 +44,70 @@ async function up({ context: { queryInterface, logger } }) { // Report the series name that is being deleted logger.info(`[2.13.5 migration] Deduplicating series "${duplicate.name}" in library ${duplicate.libraryId}`) + // Determine any duplicate book IDs in the `bookSeries` table for the same series + const [duplicateBookIds] = await queryInterface.sequelize.query( + ` + SELECT bookId, COUNT(bookId) AS count + FROM BookSeries + WHERE seriesId IN ( + SELECT id + FROM Series + WHERE name = :name AND libraryId = :libraryId + ) + GROUP BY bookId + HAVING COUNT(bookId) > 1 + `, + { + replacements: { + name: duplicate.name, + libraryId: duplicate.libraryId + } + } + ) + + // Iterate over the duplicate book IDs if there is at least one and only keep the first row that has this bookId and seriesId + for (const { bookId } of duplicateBookIds) { + // Get all rows of `BookSeries` table that have the same `bookId` and `seriesId`. Sort by `sequence` with nulls sorted last + const [duplicateBookSeries] = await queryInterface.sequelize.query( + ` + SELECT id + FROM BookSeries + WHERE bookId = :bookId + AND seriesId IN ( + SELECT id + FROM Series + WHERE name = :name AND libraryId = :libraryId + ) + ORDER BY sequence NULLS LAST + `, + { + replacements: { + bookId, + name: duplicate.name, + libraryId: duplicate.libraryId + } + } + ) + + // remove the first element from the array + duplicateBookSeries.shift() + + // Delete the remaining duplicate rows + if (duplicateBookSeries.length > 0) { + const [deletedBookSeries] = await queryInterface.sequelize.query( + ` + DELETE FROM BookSeries + WHERE id IN (:ids) + `, + { + replacements: { + ids: duplicateBookSeries.map((row) => row.id) + } + } + ) + } + } + // Get all the most recent series which matches the `name` and `libraryId` const [mostRecentSeries] = await queryInterface.sequelize.query( ` From 691ed88096082a42f30e077c0f3c3170c84ab5af Mon Sep 17 00:00:00 2001 From: Nicholas Wallace Date: Sat, 14 Sep 2024 15:34:38 -0700 Subject: [PATCH 08/44] Add more logging, clean up typo --- .../v2.13.5-series-column-unique.js | 2 ++ .../v2.13.5-series-column-unique.test.js | 34 +++++++++++-------- 2 files changed, 22 insertions(+), 14 deletions(-) diff --git a/server/migrations/v2.13.5-series-column-unique.js b/server/migrations/v2.13.5-series-column-unique.js index 21e4d4d5..8c977588 100644 --- a/server/migrations/v2.13.5-series-column-unique.js +++ b/server/migrations/v2.13.5-series-column-unique.js @@ -67,6 +67,7 @@ async function up({ context: { queryInterface, logger } }) { // Iterate over the duplicate book IDs if there is at least one and only keep the first row that has this bookId and seriesId for (const { bookId } of duplicateBookIds) { + logger.info(`[2.13.5 migration] Deduplicating bookId ${bookId} in series "${duplicate.name}" of library ${duplicate.libraryId}`) // Get all rows of `BookSeries` table that have the same `bookId` and `seriesId`. Sort by `sequence` with nulls sorted last const [duplicateBookSeries] = await queryInterface.sequelize.query( ` @@ -106,6 +107,7 @@ async function up({ context: { queryInterface, logger } }) { } ) } + logger.info(`[2.13.5 migration] Finished cleanup of bookId ${bookId} in series "${duplicate.name}" of library ${duplicate.libraryId}`) } // Get all the most recent series which matches the `name` and `libraryId` diff --git a/test/server/migrations/v2.13.5-series-column-unique.test.js b/test/server/migrations/v2.13.5-series-column-unique.test.js index 5ce5a465..2fd59eeb 100644 --- a/test/server/migrations/v2.13.5-series-column-unique.test.js +++ b/test/server/migrations/v2.13.5-series-column-unique.test.js @@ -211,13 +211,15 @@ describe('migration-v2.13.5-series-column-unique', () => { await up({ context: { queryInterface, logger: Logger } }) - expect(loggerInfoStub.callCount).to.equal(6) + expect(loggerInfoStub.callCount).to.equal(8) expect(loggerInfoStub.getCall(0).calledWith(sinon.match('UPGRADE BEGIN: 2.13.5-series-column-unique '))).to.be.true expect(loggerInfoStub.getCall(1).calledWith(sinon.match('[2.13.5 migration] Found 1 duplicate series'))).to.be.true expect(loggerInfoStub.getCall(2).calledWith(sinon.match('[2.13.5 migration] Deduplicating series "Series 1" in library 3a5a1c7c-a914-472e-88b0-b871ceae63e7'))).to.be.true - expect(loggerInfoStub.getCall(3).calledWith(sinon.match('[2.13.5 migration] Deduplication complete'))).to.be.true - expect(loggerInfoStub.getCall(4).calledWith(sinon.match('Added unique index on Series.name and Series.libraryId')).to.be.true) - expect(loggerInfoStub.getCall(5).calledWith(sinon.match('UPGRADE END: 2.13.5-series-column-unique '))).to.be.true + expect(loggerInfoStub.getCall(3).calledWith(sinon.match('[2.13.5 migration] Deduplicating bookId 4a38b6e5-0ae4-4de4-b119-4e33891bd63f in series "Series 1" of library 3a5a1c7c-a914-472e-88b0-b871ceae63e7'))).to.be.true + expect(loggerInfoStub.getCall(4).calledWith(sinon.match('[2.13.5 migration] Finished cleanup of bookId 4a38b6e5-0ae4-4de4-b119-4e33891bd63f in series "Series 1" of library 3a5a1c7c-a914-472e-88b0-b871ceae63e7'))).to.be.true + expect(loggerInfoStub.getCall(5).calledWith(sinon.match('[2.13.5 migration] Deduplication complete'))).to.be.true + expect(loggerInfoStub.getCall(6).calledWith(sinon.match('Added unique index on Series.name and Series.libraryId'))).to.be.true + expect(loggerInfoStub.getCall(7).calledWith(sinon.match('UPGRADE END: 2.13.5-series-column-unique '))).to.be.true // validate rows const series = await queryInterface.sequelize.query('SELECT "id", "name", "libraryId" FROM Series', { type: queryInterface.sequelize.QueryTypes.SELECT }) expect(series).to.have.length(1) @@ -240,13 +242,15 @@ describe('migration-v2.13.5-series-column-unique', () => { await up({ context: { queryInterface, logger: Logger } }) - expect(loggerInfoStub.callCount).to.equal(6) + expect(loggerInfoStub.callCount).to.equal(8) expect(loggerInfoStub.getCall(0).calledWith(sinon.match('UPGRADE BEGIN: 2.13.5-series-column-unique '))).to.be.true expect(loggerInfoStub.getCall(1).calledWith(sinon.match('[2.13.5 migration] Found 1 duplicate series'))).to.be.true expect(loggerInfoStub.getCall(2).calledWith(sinon.match('[2.13.5 migration] Deduplicating series "Series 1" in library 3a5a1c7c-a914-472e-88b0-b871ceae63e7'))).to.be.true - expect(loggerInfoStub.getCall(3).calledWith(sinon.match('[2.13.5 migration] Deduplication complete'))).to.be.true - expect(loggerInfoStub.getCall(4).calledWith(sinon.match('Added unique index on Series.name and Series.libraryId')).to.be.true) - expect(loggerInfoStub.getCall(5).calledWith(sinon.match('UPGRADE END: 2.13.5-series-column-unique '))).to.be.true + expect(loggerInfoStub.getCall(3).calledWith(sinon.match('[2.13.5 migration] Deduplicating bookId 4a38b6e5-0ae4-4de4-b119-4e33891bd63f in series "Series 1" of library 3a5a1c7c-a914-472e-88b0-b871ceae63e7'))).to.be.true + expect(loggerInfoStub.getCall(4).calledWith(sinon.match('[2.13.5 migration] Finished cleanup of bookId 4a38b6e5-0ae4-4de4-b119-4e33891bd63f in series "Series 1" of library 3a5a1c7c-a914-472e-88b0-b871ceae63e7'))).to.be.true + expect(loggerInfoStub.getCall(5).calledWith(sinon.match('[2.13.5 migration] Deduplication complete'))).to.be.true + expect(loggerInfoStub.getCall(6).calledWith(sinon.match('Added unique index on Series.name and Series.libraryId'))).to.be.true + expect(loggerInfoStub.getCall(7).calledWith(sinon.match('UPGRADE END: 2.13.5-series-column-unique '))).to.be.true // validate rows const series = await queryInterface.sequelize.query('SELECT "id", "name", "libraryId" FROM Series', { type: queryInterface.sequelize.QueryTypes.SELECT }) expect(series).to.have.length(1) @@ -263,26 +267,28 @@ describe('migration-v2.13.5-series-column-unique', () => { ]) // Create a book that is in both series await queryInterface.bulkInsert('BookSeries', [ - { id: bookSeries1Id, sequence: '1', bookId: book1Id, seriesId: series1Id }, + { id: bookSeries1Id, sequence: '3', bookId: book1Id, seriesId: series1Id }, { id: bookSeries2Id, sequence: '2', bookId: book1Id, seriesId: series2Id } ]) await up({ context: { queryInterface, logger: Logger } }) - expect(loggerInfoStub.callCount).to.equal(6) + expect(loggerInfoStub.callCount).to.equal(8) expect(loggerInfoStub.getCall(0).calledWith(sinon.match('UPGRADE BEGIN: 2.13.5-series-column-unique '))).to.be.true expect(loggerInfoStub.getCall(1).calledWith(sinon.match('[2.13.5 migration] Found 1 duplicate series'))).to.be.true expect(loggerInfoStub.getCall(2).calledWith(sinon.match('[2.13.5 migration] Deduplicating series "Series 1" in library 3a5a1c7c-a914-472e-88b0-b871ceae63e7'))).to.be.true - expect(loggerInfoStub.getCall(3).calledWith(sinon.match('[2.13.5 migration] Deduplication complete'))).to.be.true - expect(loggerInfoStub.getCall(4).calledWith(sinon.match('Added unique index on Series.name and Series.libraryId')).to.be.true) - expect(loggerInfoStub.getCall(5).calledWith(sinon.match('UPGRADE END: 2.13.5-series-column-unique '))).to.be.true + expect(loggerInfoStub.getCall(3).calledWith(sinon.match('[2.13.5 migration] Deduplicating bookId 8bc2e61d-47f6-42ef-a3f4-93cf2f1de82f in series "Series 1" of library 3a5a1c7c-a914-472e-88b0-b871ceae63e7'))).to.be.true + expect(loggerInfoStub.getCall(4).calledWith(sinon.match('[2.13.5 migration] Finished cleanup of bookId 8bc2e61d-47f6-42ef-a3f4-93cf2f1de82f in series "Series 1" of library 3a5a1c7c-a914-472e-88b0-b871ceae63e7'))).to.be.true + expect(loggerInfoStub.getCall(5).calledWith(sinon.match('[2.13.5 migration] Deduplication complete'))).to.be.true + expect(loggerInfoStub.getCall(6).calledWith(sinon.match('Added unique index on Series.name and Series.libraryId'))).to.be.true + expect(loggerInfoStub.getCall(7).calledWith(sinon.match('UPGRADE END: 2.13.5-series-column-unique '))).to.be.true // validate rows const series = await queryInterface.sequelize.query('SELECT "id", "name", "libraryId" FROM Series', { type: queryInterface.sequelize.QueryTypes.SELECT }) expect(series).to.have.length(1) expect(series).to.deep.include({ id: series1Id, name: 'Series 1', libraryId: library1Id }) const bookSeries = await queryInterface.sequelize.query('SELECT "id", "sequence", "bookId", "seriesId" FROM BookSeries', { type: queryInterface.sequelize.QueryTypes.SELECT }) expect(bookSeries).to.have.length(1) - expect(bookSeries).to.deep.include({ id: bookSeries1Id, sequence: '1, 2', bookId: book1Id, seriesId: series1Id }) + expect(bookSeries).to.deep.include({ id: bookSeries1Id, sequence: '2', bookId: book1Id, seriesId: series1Id }) }) }) From 8b95dd65d917fe2c2b200918362b308f2514d6ee Mon Sep 17 00:00:00 2001 From: Nicholas Wallace Date: Sat, 14 Sep 2024 15:43:10 -0700 Subject: [PATCH 09/44] Fix: test cases checking the wrong bookSeriesId --- .../migrations/v2.13.5-series-column-unique.test.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/test/server/migrations/v2.13.5-series-column-unique.test.js b/test/server/migrations/v2.13.5-series-column-unique.test.js index 2fd59eeb..3c5b1b04 100644 --- a/test/server/migrations/v2.13.5-series-column-unique.test.js +++ b/test/server/migrations/v2.13.5-series-column-unique.test.js @@ -226,7 +226,8 @@ describe('migration-v2.13.5-series-column-unique', () => { expect(series).to.deep.include({ id: series1Id, name: 'Series 1', libraryId: library1Id }) const bookSeries = await queryInterface.sequelize.query('SELECT "id", "sequence", "bookId", "seriesId" FROM BookSeries', { type: queryInterface.sequelize.QueryTypes.SELECT }) expect(bookSeries).to.have.length(1) - expect(bookSeries).to.deep.include({ id: bookSeries1Id, sequence: null, bookId: book1Id, seriesId: series1Id }) + // Keep BookSeries 2 because it was edited last from cleaning up duplicate books + expect(bookSeries).to.deep.include({ id: bookSeries2Id, sequence: null, bookId: book1Id, seriesId: series1Id }) }) it('upgrade with one book in two of the same series, one sequence is null', async () => { // Create two different series with the same name in the same library @@ -277,8 +278,8 @@ describe('migration-v2.13.5-series-column-unique', () => { expect(loggerInfoStub.getCall(0).calledWith(sinon.match('UPGRADE BEGIN: 2.13.5-series-column-unique '))).to.be.true expect(loggerInfoStub.getCall(1).calledWith(sinon.match('[2.13.5 migration] Found 1 duplicate series'))).to.be.true expect(loggerInfoStub.getCall(2).calledWith(sinon.match('[2.13.5 migration] Deduplicating series "Series 1" in library 3a5a1c7c-a914-472e-88b0-b871ceae63e7'))).to.be.true - expect(loggerInfoStub.getCall(3).calledWith(sinon.match('[2.13.5 migration] Deduplicating bookId 8bc2e61d-47f6-42ef-a3f4-93cf2f1de82f in series "Series 1" of library 3a5a1c7c-a914-472e-88b0-b871ceae63e7'))).to.be.true - expect(loggerInfoStub.getCall(4).calledWith(sinon.match('[2.13.5 migration] Finished cleanup of bookId 8bc2e61d-47f6-42ef-a3f4-93cf2f1de82f in series "Series 1" of library 3a5a1c7c-a914-472e-88b0-b871ceae63e7'))).to.be.true + expect(loggerInfoStub.getCall(3).calledWith(sinon.match('[2.13.5 migration] Deduplicating bookId 4a38b6e5-0ae4-4de4-b119-4e33891bd63f in series "Series 1" of library 3a5a1c7c-a914-472e-88b0-b871ceae63e7'))).to.be.true + expect(loggerInfoStub.getCall(4).calledWith(sinon.match('[2.13.5 migration] Finished cleanup of bookId 4a38b6e5-0ae4-4de4-b119-4e33891bd63f in series "Series 1" of library 3a5a1c7c-a914-472e-88b0-b871ceae63e7'))).to.be.true expect(loggerInfoStub.getCall(5).calledWith(sinon.match('[2.13.5 migration] Deduplication complete'))).to.be.true expect(loggerInfoStub.getCall(6).calledWith(sinon.match('Added unique index on Series.name and Series.libraryId'))).to.be.true expect(loggerInfoStub.getCall(7).calledWith(sinon.match('UPGRADE END: 2.13.5-series-column-unique '))).to.be.true @@ -288,7 +289,8 @@ describe('migration-v2.13.5-series-column-unique', () => { expect(series).to.deep.include({ id: series1Id, name: 'Series 1', libraryId: library1Id }) const bookSeries = await queryInterface.sequelize.query('SELECT "id", "sequence", "bookId", "seriesId" FROM BookSeries', { type: queryInterface.sequelize.QueryTypes.SELECT }) expect(bookSeries).to.have.length(1) - expect(bookSeries).to.deep.include({ id: bookSeries1Id, sequence: '2', bookId: book1Id, seriesId: series1Id }) + // Keep BookSeries 2 because it is the lower sequence number + expect(bookSeries).to.deep.include({ id: bookSeries2Id, sequence: '2', bookId: book1Id, seriesId: series1Id }) }) }) From 66b290577c3663a62edd2f5aaf8291b18cb72b98 Mon Sep 17 00:00:00 2001 From: Nicholas Wallace Date: Tue, 17 Sep 2024 20:00:06 -0700 Subject: [PATCH 10/44] Clean up unused parts of statement --- server/migrations/v2.13.5-series-column-unique.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/server/migrations/v2.13.5-series-column-unique.js b/server/migrations/v2.13.5-series-column-unique.js index 8c977588..1860772d 100644 --- a/server/migrations/v2.13.5-series-column-unique.js +++ b/server/migrations/v2.13.5-series-column-unique.js @@ -1,5 +1,3 @@ -const Logger = require('../Logger') - /** * @typedef MigrationContext * @property {import('sequelize').QueryInterface} queryInterface - a suquelize QueryInterface object. @@ -30,7 +28,7 @@ async function up({ context: { queryInterface, logger } }) { // Use the queryInterface to get the series table and find duplicates in the `name` and `libraryId` column const [duplicates] = await queryInterface.sequelize.query(` - SELECT name, libraryId, MAX(updatedAt) AS latestUpdatedAt, COUNT(name) AS count + SELECT name, libraryId FROM Series GROUP BY name, libraryId HAVING COUNT(name) > 1 @@ -47,7 +45,7 @@ async function up({ context: { queryInterface, logger } }) { // Determine any duplicate book IDs in the `bookSeries` table for the same series const [duplicateBookIds] = await queryInterface.sequelize.query( ` - SELECT bookId, COUNT(bookId) AS count + SELECT bookId FROM BookSeries WHERE seriesId IN ( SELECT id From 8a7b5cc87d0e8351569fa4e8772da781327d7cd4 Mon Sep 17 00:00:00 2001 From: advplyr Date: Tue, 24 Sep 2024 16:47:09 -0500 Subject: [PATCH 11/44] Ensure series-column-unique migration is idempotent --- server/managers/MigrationManager.js | 3 +-- .../v2.13.5-series-column-unique.js | 20 +++++++++++++------ 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/server/managers/MigrationManager.js b/server/managers/MigrationManager.js index 706e359c..beaf8a4d 100644 --- a/server/managers/MigrationManager.js +++ b/server/managers/MigrationManager.js @@ -38,6 +38,7 @@ class MigrationManager { if (!(await fs.pathExists(this.configPath))) throw new Error(`Config path does not exist: ${this.configPath}`) this.migrationsDir = path.join(this.configPath, 'migrations') + await fs.ensureDir(this.migrationsDir) this.serverVersion = this.extractVersionFromTag(serverVersion) if (!this.serverVersion) throw new Error(`Invalid server version: ${serverVersion}. Expected a version tag like v1.2.3.`) @@ -222,8 +223,6 @@ class MigrationManager { } async copyMigrationsToConfigDir() { - await fs.ensureDir(this.migrationsDir) // Ensure the target directory exists - if (!(await fs.pathExists(this.migrationsSourceDir))) return const files = await fs.readdir(this.migrationsSourceDir) diff --git a/server/migrations/v2.13.5-series-column-unique.js b/server/migrations/v2.13.5-series-column-unique.js index 1860772d..2724221a 100644 --- a/server/migrations/v2.13.5-series-column-unique.js +++ b/server/migrations/v2.13.5-series-column-unique.js @@ -16,7 +16,15 @@ */ async function up({ context: { queryInterface, logger } }) { // Upwards migration script - logger.info('UPGRADE BEGIN: 2.13.5-series-column-unique ') + logger.info('[2.13.5 migration] UPGRADE BEGIN: 2.13.5-series-column-unique ') + + // Check if the unique index already exists + const seriesIndexes = await queryInterface.showIndex('Series') + if (seriesIndexes.some((index) => index.name === 'unique_series_name_per_library')) { + logger.info('[2.13.5 migration] Unique index on Series.name and Series.libraryId already exists') + logger.info('[2.13.5 migration] UPGRADE END: 2.13.5-series-column-unique ') + return + } // The steps taken to deduplicate the series are as follows: // 1. Find all duplicate series in the `Series` table. @@ -173,9 +181,9 @@ async function up({ context: { queryInterface, logger } }) { unique: true, name: 'unique_series_name_per_library' }) - logger.info('Added unique index on Series.name and Series.libraryId') + logger.info('[2.13.5 migration] Added unique index on Series.name and Series.libraryId') - logger.info('UPGRADE END: 2.13.5-series-column-unique ') + logger.info('[2.13.5 migration] UPGRADE END: 2.13.5-series-column-unique ') } /** @@ -186,13 +194,13 @@ async function up({ context: { queryInterface, logger } }) { */ async function down({ context: { queryInterface, logger } }) { // Downward migration script - logger.info('DOWNGRADE BEGIN: 2.13.5-series-column-unique ') + logger.info('[2.13.5 migration] DOWNGRADE BEGIN: 2.13.5-series-column-unique ') // Remove the unique index await queryInterface.removeIndex('Series', 'unique_series_name_per_library') - logger.info('Removed unique index on Series.name and Series.libraryId') + logger.info('[2.13.5 migration] Removed unique index on Series.name and Series.libraryId') - logger.info('DOWNGRADE END: 2.13.5-series-column-unique ') + logger.info('[2.13.5 migration] DOWNGRADE END: 2.13.5-series-column-unique ') } module.exports = { up, down } From c67b5e950edbd86e74708e92d3c58c700bea5f1b Mon Sep 17 00:00:00 2001 From: advplyr Date: Tue, 24 Sep 2024 16:54:13 -0500 Subject: [PATCH 12/44] Update MigrationManager.test.js - moved migrations ensureDir to init() --- test/server/managers/MigrationManager.test.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/test/server/managers/MigrationManager.test.js b/test/server/managers/MigrationManager.test.js index ae94cd75..af2e9da8 100644 --- a/test/server/managers/MigrationManager.test.js +++ b/test/server/managers/MigrationManager.test.js @@ -63,6 +63,8 @@ describe('MigrationManager', () => { await migrationManager.init(serverVersion) // Assert + expect(fsEnsureDirStub.calledOnce).to.be.true + expect(fsEnsureDirStub.calledWith(migrationManager.migrationsDir)).to.be.true expect(migrationManager.serverVersion).to.equal(serverVersion) expect(migrationManager.sequelize).to.equal(sequelizeStub) expect(migrationManager.migrationsDir).to.equal(path.join(__dirname, 'migrations')) @@ -353,8 +355,6 @@ describe('MigrationManager', () => { await migrationManager.copyMigrationsToConfigDir() // Assert - expect(fsEnsureDirStub.calledOnce).to.be.true - expect(fsEnsureDirStub.calledWith(targetDir)).to.be.true expect(readdirStub.calledOnce).to.be.true expect(readdirStub.calledWith(migrationsSourceDir)).to.be.true expect(fsCopyStub.calledTwice).to.be.true @@ -382,8 +382,6 @@ describe('MigrationManager', () => { } catch (error) {} // Assert - expect(fsEnsureDirStub.calledOnce).to.be.true - expect(fsEnsureDirStub.calledWith(targetDir)).to.be.true expect(readdirStub.calledOnce).to.be.true expect(readdirStub.calledWith(migrationsSourceDir)).to.be.true expect(fsCopyStub.calledTwice).to.be.true From 5154e31c1cba8bad9a088f5586632fabb3abd656 Mon Sep 17 00:00:00 2001 From: advplyr Date: Tue, 24 Sep 2024 17:06:00 -0500 Subject: [PATCH 13/44] Update migration to v2.14.0 --- server/migrations/changelog.md | 2 +- ...que.js => v2.14.0-series-column-unique.js} | 26 ++--- ...s => v2.14.0-series-column-unique.test.js} | 102 +++++++++--------- 3 files changed, 65 insertions(+), 65 deletions(-) rename server/migrations/{v2.13.5-series-column-unique.js => v2.14.0-series-column-unique.js} (88%) rename test/server/migrations/{v2.13.5-series-column-unique.test.js => v2.14.0-series-column-unique.test.js} (78%) diff --git a/server/migrations/changelog.md b/server/migrations/changelog.md index bac3ec25..3ab52ac3 100644 --- a/server/migrations/changelog.md +++ b/server/migrations/changelog.md @@ -4,4 +4,4 @@ Please add a record of every database migration that you create to this file. Th | Server Version | Migration Script Name | Description | | -------------- | ---------------------------- | ------------------------------------------------- | -| v2.13.5 | v2.13.5-series-column-unique | Series must have unique names in the same library | +| v2.14.0 | v2.14.0-series-column-unique | Series must have unique names in the same library | diff --git a/server/migrations/v2.13.5-series-column-unique.js b/server/migrations/v2.14.0-series-column-unique.js similarity index 88% rename from server/migrations/v2.13.5-series-column-unique.js rename to server/migrations/v2.14.0-series-column-unique.js index 2724221a..489b670b 100644 --- a/server/migrations/v2.13.5-series-column-unique.js +++ b/server/migrations/v2.14.0-series-column-unique.js @@ -16,13 +16,13 @@ */ async function up({ context: { queryInterface, logger } }) { // Upwards migration script - logger.info('[2.13.5 migration] UPGRADE BEGIN: 2.13.5-series-column-unique ') + logger.info('[2.14.0 migration] UPGRADE BEGIN: 2.14.0-series-column-unique ') // Check if the unique index already exists const seriesIndexes = await queryInterface.showIndex('Series') if (seriesIndexes.some((index) => index.name === 'unique_series_name_per_library')) { - logger.info('[2.13.5 migration] Unique index on Series.name and Series.libraryId already exists') - logger.info('[2.13.5 migration] UPGRADE END: 2.13.5-series-column-unique ') + logger.info('[2.14.0 migration] Unique index on Series.name and Series.libraryId already exists') + logger.info('[2.14.0 migration] UPGRADE END: 2.14.0-series-column-unique ') return } @@ -43,12 +43,12 @@ async function up({ context: { queryInterface, logger } }) { `) // Print out how many duplicates were found - logger.info(`[2.13.5 migration] Found ${duplicates.length} duplicate series`) + logger.info(`[2.14.0 migration] Found ${duplicates.length} duplicate series`) // Iterate over each duplicate series for (const duplicate of duplicates) { // Report the series name that is being deleted - logger.info(`[2.13.5 migration] Deduplicating series "${duplicate.name}" in library ${duplicate.libraryId}`) + logger.info(`[2.14.0 migration] Deduplicating series "${duplicate.name}" in library ${duplicate.libraryId}`) // Determine any duplicate book IDs in the `bookSeries` table for the same series const [duplicateBookIds] = await queryInterface.sequelize.query( @@ -73,7 +73,7 @@ async function up({ context: { queryInterface, logger } }) { // Iterate over the duplicate book IDs if there is at least one and only keep the first row that has this bookId and seriesId for (const { bookId } of duplicateBookIds) { - logger.info(`[2.13.5 migration] Deduplicating bookId ${bookId} in series "${duplicate.name}" of library ${duplicate.libraryId}`) + logger.info(`[2.14.0 migration] Deduplicating bookId ${bookId} in series "${duplicate.name}" of library ${duplicate.libraryId}`) // Get all rows of `BookSeries` table that have the same `bookId` and `seriesId`. Sort by `sequence` with nulls sorted last const [duplicateBookSeries] = await queryInterface.sequelize.query( ` @@ -113,7 +113,7 @@ async function up({ context: { queryInterface, logger } }) { } ) } - logger.info(`[2.13.5 migration] Finished cleanup of bookId ${bookId} in series "${duplicate.name}" of library ${duplicate.libraryId}`) + logger.info(`[2.14.0 migration] Finished cleanup of bookId ${bookId} in series "${duplicate.name}" of library ${duplicate.libraryId}`) } // Get all the most recent series which matches the `name` and `libraryId` @@ -174,16 +174,16 @@ async function up({ context: { queryInterface, logger } }) { } } - logger.info(`[2.13.5 migration] Deduplication complete`) + logger.info(`[2.14.0 migration] Deduplication complete`) // Create a unique index based on the name and library ID for the `Series` table await queryInterface.addIndex('Series', ['name', 'libraryId'], { unique: true, name: 'unique_series_name_per_library' }) - logger.info('[2.13.5 migration] Added unique index on Series.name and Series.libraryId') + logger.info('[2.14.0 migration] Added unique index on Series.name and Series.libraryId') - logger.info('[2.13.5 migration] UPGRADE END: 2.13.5-series-column-unique ') + logger.info('[2.14.0 migration] UPGRADE END: 2.14.0-series-column-unique ') } /** @@ -194,13 +194,13 @@ async function up({ context: { queryInterface, logger } }) { */ async function down({ context: { queryInterface, logger } }) { // Downward migration script - logger.info('[2.13.5 migration] DOWNGRADE BEGIN: 2.13.5-series-column-unique ') + logger.info('[2.14.0 migration] DOWNGRADE BEGIN: 2.14.0-series-column-unique ') // Remove the unique index await queryInterface.removeIndex('Series', 'unique_series_name_per_library') - logger.info('[2.13.5 migration] Removed unique index on Series.name and Series.libraryId') + logger.info('[2.14.0 migration] Removed unique index on Series.name and Series.libraryId') - logger.info('[2.13.5 migration] DOWNGRADE END: 2.13.5-series-column-unique ') + logger.info('[2.14.0 migration] DOWNGRADE END: 2.14.0-series-column-unique ') } module.exports = { up, down } diff --git a/test/server/migrations/v2.13.5-series-column-unique.test.js b/test/server/migrations/v2.14.0-series-column-unique.test.js similarity index 78% rename from test/server/migrations/v2.13.5-series-column-unique.test.js rename to test/server/migrations/v2.14.0-series-column-unique.test.js index 3c5b1b04..43acc927 100644 --- a/test/server/migrations/v2.13.5-series-column-unique.test.js +++ b/test/server/migrations/v2.14.0-series-column-unique.test.js @@ -1,13 +1,13 @@ const { expect } = require('chai') const sinon = require('sinon') -const { up, down } = require('../../../server/migrations/v2.13.5-series-column-unique') +const { up, down } = require('../../../server/migrations/v2.14.0-series-column-unique') const { Sequelize } = require('sequelize') const Logger = require('../../../server/Logger') const { query } = require('express') const { logger } = require('sequelize/lib/utils/logger') const e = require('express') -describe('migration-v2.13.5-series-column-unique', () => { +describe('migration-v2.14.0-series-column-unique', () => { let sequelize let queryInterface let loggerInfoStub @@ -105,11 +105,11 @@ describe('migration-v2.13.5-series-column-unique', () => { await up({ context: { queryInterface, logger: Logger } }) expect(loggerInfoStub.callCount).to.equal(5) - expect(loggerInfoStub.getCall(0).calledWith(sinon.match('UPGRADE BEGIN: 2.13.5-series-column-unique '))).to.be.true - expect(loggerInfoStub.getCall(1).calledWith(sinon.match('[2.13.5 migration] Found 0 duplicate series'))).to.be.true - expect(loggerInfoStub.getCall(2).calledWith(sinon.match('[2.13.5 migration] Deduplication complete'))).to.be.true - expect(loggerInfoStub.getCall(3).calledWith(sinon.match('Added unique index on Series.name and Series.libraryId'))).to.be.true - expect(loggerInfoStub.getCall(4).calledWith(sinon.match('UPGRADE END: 2.13.5-series-column-unique '))).to.be.true + expect(loggerInfoStub.getCall(0).calledWith(sinon.match('[2.14.0 migration] UPGRADE BEGIN: 2.14.0-series-column-unique '))).to.be.true + expect(loggerInfoStub.getCall(1).calledWith(sinon.match('[2.14.0 migration] Found 0 duplicate series'))).to.be.true + expect(loggerInfoStub.getCall(2).calledWith(sinon.match('[2.14.0 migration] Deduplication complete'))).to.be.true + expect(loggerInfoStub.getCall(3).calledWith(sinon.match('[2.14.0 migration] Added unique index on Series.name and Series.libraryId'))).to.be.true + expect(loggerInfoStub.getCall(4).calledWith(sinon.match('[2.14.0 migration] UPGRADE END: 2.14.0-series-column-unique '))).to.be.true // Validate rows in tables const series = await queryInterface.sequelize.query('SELECT "id", "name", "libraryId" FROM Series', { type: queryInterface.sequelize.QueryTypes.SELECT }) expect(series).to.have.length(3) @@ -145,13 +145,13 @@ describe('migration-v2.13.5-series-column-unique', () => { await up({ context: { queryInterface, logger: Logger } }) expect(loggerInfoStub.callCount).to.equal(7) - expect(loggerInfoStub.getCall(0).calledWith(sinon.match('UPGRADE BEGIN: 2.13.5-series-column-unique '))).to.be.true - expect(loggerInfoStub.getCall(1).calledWith(sinon.match('[2.13.5 migration] Found 2 duplicate series'))).to.be.true - expect(loggerInfoStub.getCall(2).calledWith(sinon.match('[2.13.5 migration] Deduplicating series "Series 1" in library 3a5a1c7c-a914-472e-88b0-b871ceae63e7'))).to.be.true - expect(loggerInfoStub.getCall(3).calledWith(sinon.match('[2.13.5 migration] Deduplicating series "Series 3" in library 3a5a1c7c-a914-472e-88b0-b871ceae63e7'))).to.be.true - expect(loggerInfoStub.getCall(4).calledWith(sinon.match('[2.13.5 migration] Deduplication complete'))).to.be.true - expect(loggerInfoStub.getCall(5).calledWith(sinon.match('Added unique index on Series.name and Series.libraryId'))).to.be.true - expect(loggerInfoStub.getCall(6).calledWith(sinon.match('UPGRADE END: 2.13.5-series-column-unique '))).to.be.true + expect(loggerInfoStub.getCall(0).calledWith(sinon.match('[2.14.0 migration] UPGRADE BEGIN: 2.14.0-series-column-unique '))).to.be.true + expect(loggerInfoStub.getCall(1).calledWith(sinon.match('[2.14.0 migration] Found 2 duplicate series'))).to.be.true + expect(loggerInfoStub.getCall(2).calledWith(sinon.match('[2.14.0 migration] Deduplicating series "Series 1" in library 3a5a1c7c-a914-472e-88b0-b871ceae63e7'))).to.be.true + expect(loggerInfoStub.getCall(3).calledWith(sinon.match('[2.14.0 migration] Deduplicating series "Series 3" in library 3a5a1c7c-a914-472e-88b0-b871ceae63e7'))).to.be.true + expect(loggerInfoStub.getCall(4).calledWith(sinon.match('[2.14.0 migration] Deduplication complete'))).to.be.true + expect(loggerInfoStub.getCall(5).calledWith(sinon.match('[2.14.0 migration] Added unique index on Series.name and Series.libraryId'))).to.be.true + expect(loggerInfoStub.getCall(6).calledWith(sinon.match('[2.14.0 migration] UPGRADE END: 2.14.0-series-column-unique '))).to.be.true // Validate rows const series = await queryInterface.sequelize.query('SELECT "id", "name", "libraryId" FROM Series', { type: queryInterface.sequelize.QueryTypes.SELECT }) expect(series).to.have.length(3) @@ -182,11 +182,11 @@ describe('migration-v2.13.5-series-column-unique', () => { await up({ context: { queryInterface, logger: Logger } }) expect(loggerInfoStub.callCount).to.equal(5) - expect(loggerInfoStub.getCall(0).calledWith(sinon.match('UPGRADE BEGIN: 2.13.5-series-column-unique '))).to.be.true - expect(loggerInfoStub.getCall(1).calledWith(sinon.match('[2.13.5 migration] Found 0 duplicate series'))).to.be.true - expect(loggerInfoStub.getCall(2).calledWith(sinon.match('[2.13.5 migration] Deduplication complete'))).to.be.true - expect(loggerInfoStub.getCall(3).calledWith(sinon.match('Added unique index on Series.name and Series.libraryId'))).to.be.true - expect(loggerInfoStub.getCall(4).calledWith(sinon.match('UPGRADE END: 2.13.5-series-column-unique '))).to.be.true + expect(loggerInfoStub.getCall(0).calledWith(sinon.match('[2.14.0 migration] UPGRADE BEGIN: 2.14.0-series-column-unique '))).to.be.true + expect(loggerInfoStub.getCall(1).calledWith(sinon.match('[2.14.0 migration] Found 0 duplicate series'))).to.be.true + expect(loggerInfoStub.getCall(2).calledWith(sinon.match('[2.14.0 migration] Deduplication complete'))).to.be.true + expect(loggerInfoStub.getCall(3).calledWith(sinon.match('[2.14.0 migration] Added unique index on Series.name and Series.libraryId'))).to.be.true + expect(loggerInfoStub.getCall(4).calledWith(sinon.match('[2.14.0 migration] UPGRADE END: 2.14.0-series-column-unique '))).to.be.true // Validate rows const series = await queryInterface.sequelize.query('SELECT "id", "name", "libraryId" FROM Series', { type: queryInterface.sequelize.QueryTypes.SELECT }) expect(series).to.have.length(2) @@ -212,14 +212,14 @@ describe('migration-v2.13.5-series-column-unique', () => { await up({ context: { queryInterface, logger: Logger } }) expect(loggerInfoStub.callCount).to.equal(8) - expect(loggerInfoStub.getCall(0).calledWith(sinon.match('UPGRADE BEGIN: 2.13.5-series-column-unique '))).to.be.true - expect(loggerInfoStub.getCall(1).calledWith(sinon.match('[2.13.5 migration] Found 1 duplicate series'))).to.be.true - expect(loggerInfoStub.getCall(2).calledWith(sinon.match('[2.13.5 migration] Deduplicating series "Series 1" in library 3a5a1c7c-a914-472e-88b0-b871ceae63e7'))).to.be.true - expect(loggerInfoStub.getCall(3).calledWith(sinon.match('[2.13.5 migration] Deduplicating bookId 4a38b6e5-0ae4-4de4-b119-4e33891bd63f in series "Series 1" of library 3a5a1c7c-a914-472e-88b0-b871ceae63e7'))).to.be.true - expect(loggerInfoStub.getCall(4).calledWith(sinon.match('[2.13.5 migration] Finished cleanup of bookId 4a38b6e5-0ae4-4de4-b119-4e33891bd63f in series "Series 1" of library 3a5a1c7c-a914-472e-88b0-b871ceae63e7'))).to.be.true - expect(loggerInfoStub.getCall(5).calledWith(sinon.match('[2.13.5 migration] Deduplication complete'))).to.be.true - expect(loggerInfoStub.getCall(6).calledWith(sinon.match('Added unique index on Series.name and Series.libraryId'))).to.be.true - expect(loggerInfoStub.getCall(7).calledWith(sinon.match('UPGRADE END: 2.13.5-series-column-unique '))).to.be.true + expect(loggerInfoStub.getCall(0).calledWith(sinon.match('[2.14.0 migration] UPGRADE BEGIN: 2.14.0-series-column-unique '))).to.be.true + expect(loggerInfoStub.getCall(1).calledWith(sinon.match('[2.14.0 migration] Found 1 duplicate series'))).to.be.true + expect(loggerInfoStub.getCall(2).calledWith(sinon.match('[2.14.0 migration] Deduplicating series "Series 1" in library 3a5a1c7c-a914-472e-88b0-b871ceae63e7'))).to.be.true + expect(loggerInfoStub.getCall(3).calledWith(sinon.match('[2.14.0 migration] Deduplicating bookId 4a38b6e5-0ae4-4de4-b119-4e33891bd63f in series "Series 1" of library 3a5a1c7c-a914-472e-88b0-b871ceae63e7'))).to.be.true + expect(loggerInfoStub.getCall(4).calledWith(sinon.match('[2.14.0 migration] Finished cleanup of bookId 4a38b6e5-0ae4-4de4-b119-4e33891bd63f in series "Series 1" of library 3a5a1c7c-a914-472e-88b0-b871ceae63e7'))).to.be.true + expect(loggerInfoStub.getCall(5).calledWith(sinon.match('[2.14.0 migration] Deduplication complete'))).to.be.true + expect(loggerInfoStub.getCall(6).calledWith(sinon.match('[2.14.0 migration] Added unique index on Series.name and Series.libraryId'))).to.be.true + expect(loggerInfoStub.getCall(7).calledWith(sinon.match('[2.14.0 migration] UPGRADE END: 2.14.0-series-column-unique '))).to.be.true // validate rows const series = await queryInterface.sequelize.query('SELECT "id", "name", "libraryId" FROM Series', { type: queryInterface.sequelize.QueryTypes.SELECT }) expect(series).to.have.length(1) @@ -244,14 +244,14 @@ describe('migration-v2.13.5-series-column-unique', () => { await up({ context: { queryInterface, logger: Logger } }) expect(loggerInfoStub.callCount).to.equal(8) - expect(loggerInfoStub.getCall(0).calledWith(sinon.match('UPGRADE BEGIN: 2.13.5-series-column-unique '))).to.be.true - expect(loggerInfoStub.getCall(1).calledWith(sinon.match('[2.13.5 migration] Found 1 duplicate series'))).to.be.true - expect(loggerInfoStub.getCall(2).calledWith(sinon.match('[2.13.5 migration] Deduplicating series "Series 1" in library 3a5a1c7c-a914-472e-88b0-b871ceae63e7'))).to.be.true - expect(loggerInfoStub.getCall(3).calledWith(sinon.match('[2.13.5 migration] Deduplicating bookId 4a38b6e5-0ae4-4de4-b119-4e33891bd63f in series "Series 1" of library 3a5a1c7c-a914-472e-88b0-b871ceae63e7'))).to.be.true - expect(loggerInfoStub.getCall(4).calledWith(sinon.match('[2.13.5 migration] Finished cleanup of bookId 4a38b6e5-0ae4-4de4-b119-4e33891bd63f in series "Series 1" of library 3a5a1c7c-a914-472e-88b0-b871ceae63e7'))).to.be.true - expect(loggerInfoStub.getCall(5).calledWith(sinon.match('[2.13.5 migration] Deduplication complete'))).to.be.true - expect(loggerInfoStub.getCall(6).calledWith(sinon.match('Added unique index on Series.name and Series.libraryId'))).to.be.true - expect(loggerInfoStub.getCall(7).calledWith(sinon.match('UPGRADE END: 2.13.5-series-column-unique '))).to.be.true + expect(loggerInfoStub.getCall(0).calledWith(sinon.match('[2.14.0 migration] UPGRADE BEGIN: 2.14.0-series-column-unique '))).to.be.true + expect(loggerInfoStub.getCall(1).calledWith(sinon.match('[2.14.0 migration] Found 1 duplicate series'))).to.be.true + expect(loggerInfoStub.getCall(2).calledWith(sinon.match('[2.14.0 migration] Deduplicating series "Series 1" in library 3a5a1c7c-a914-472e-88b0-b871ceae63e7'))).to.be.true + expect(loggerInfoStub.getCall(3).calledWith(sinon.match('[2.14.0 migration] Deduplicating bookId 4a38b6e5-0ae4-4de4-b119-4e33891bd63f in series "Series 1" of library 3a5a1c7c-a914-472e-88b0-b871ceae63e7'))).to.be.true + expect(loggerInfoStub.getCall(4).calledWith(sinon.match('[2.14.0 migration] Finished cleanup of bookId 4a38b6e5-0ae4-4de4-b119-4e33891bd63f in series "Series 1" of library 3a5a1c7c-a914-472e-88b0-b871ceae63e7'))).to.be.true + expect(loggerInfoStub.getCall(5).calledWith(sinon.match('[2.14.0 migration] Deduplication complete'))).to.be.true + expect(loggerInfoStub.getCall(6).calledWith(sinon.match('[2.14.0 migration] Added unique index on Series.name and Series.libraryId'))).to.be.true + expect(loggerInfoStub.getCall(7).calledWith(sinon.match('[2.14.0 migration] UPGRADE END: 2.14.0-series-column-unique '))).to.be.true // validate rows const series = await queryInterface.sequelize.query('SELECT "id", "name", "libraryId" FROM Series', { type: queryInterface.sequelize.QueryTypes.SELECT }) expect(series).to.have.length(1) @@ -275,14 +275,14 @@ describe('migration-v2.13.5-series-column-unique', () => { await up({ context: { queryInterface, logger: Logger } }) expect(loggerInfoStub.callCount).to.equal(8) - expect(loggerInfoStub.getCall(0).calledWith(sinon.match('UPGRADE BEGIN: 2.13.5-series-column-unique '))).to.be.true - expect(loggerInfoStub.getCall(1).calledWith(sinon.match('[2.13.5 migration] Found 1 duplicate series'))).to.be.true - expect(loggerInfoStub.getCall(2).calledWith(sinon.match('[2.13.5 migration] Deduplicating series "Series 1" in library 3a5a1c7c-a914-472e-88b0-b871ceae63e7'))).to.be.true - expect(loggerInfoStub.getCall(3).calledWith(sinon.match('[2.13.5 migration] Deduplicating bookId 4a38b6e5-0ae4-4de4-b119-4e33891bd63f in series "Series 1" of library 3a5a1c7c-a914-472e-88b0-b871ceae63e7'))).to.be.true - expect(loggerInfoStub.getCall(4).calledWith(sinon.match('[2.13.5 migration] Finished cleanup of bookId 4a38b6e5-0ae4-4de4-b119-4e33891bd63f in series "Series 1" of library 3a5a1c7c-a914-472e-88b0-b871ceae63e7'))).to.be.true - expect(loggerInfoStub.getCall(5).calledWith(sinon.match('[2.13.5 migration] Deduplication complete'))).to.be.true - expect(loggerInfoStub.getCall(6).calledWith(sinon.match('Added unique index on Series.name and Series.libraryId'))).to.be.true - expect(loggerInfoStub.getCall(7).calledWith(sinon.match('UPGRADE END: 2.13.5-series-column-unique '))).to.be.true + expect(loggerInfoStub.getCall(0).calledWith(sinon.match('[2.14.0 migration] UPGRADE BEGIN: 2.14.0-series-column-unique '))).to.be.true + expect(loggerInfoStub.getCall(1).calledWith(sinon.match('[2.14.0 migration] Found 1 duplicate series'))).to.be.true + expect(loggerInfoStub.getCall(2).calledWith(sinon.match('[2.14.0 migration] Deduplicating series "Series 1" in library 3a5a1c7c-a914-472e-88b0-b871ceae63e7'))).to.be.true + expect(loggerInfoStub.getCall(3).calledWith(sinon.match('[2.14.0 migration] Deduplicating bookId 4a38b6e5-0ae4-4de4-b119-4e33891bd63f in series "Series 1" of library 3a5a1c7c-a914-472e-88b0-b871ceae63e7'))).to.be.true + expect(loggerInfoStub.getCall(4).calledWith(sinon.match('[2.14.0 migration] Finished cleanup of bookId 4a38b6e5-0ae4-4de4-b119-4e33891bd63f in series "Series 1" of library 3a5a1c7c-a914-472e-88b0-b871ceae63e7'))).to.be.true + expect(loggerInfoStub.getCall(5).calledWith(sinon.match('[2.14.0 migration] Deduplication complete'))).to.be.true + expect(loggerInfoStub.getCall(6).calledWith(sinon.match('[2.14.0 migration] Added unique index on Series.name and Series.libraryId'))).to.be.true + expect(loggerInfoStub.getCall(7).calledWith(sinon.match('[2.14.0 migration] UPGRADE END: 2.14.0-series-column-unique '))).to.be.true // validate rows const series = await queryInterface.sequelize.query('SELECT "id", "name", "libraryId" FROM Series', { type: queryInterface.sequelize.QueryTypes.SELECT }) expect(series).to.have.length(1) @@ -319,14 +319,14 @@ describe('migration-v2.13.5-series-column-unique', () => { await down({ context: { queryInterface, logger: Logger } }) expect(loggerInfoStub.callCount).to.equal(8) - expect(loggerInfoStub.getCall(0).calledWith(sinon.match('UPGRADE BEGIN: 2.13.5-series-column-unique '))).to.be.true - expect(loggerInfoStub.getCall(1).calledWith(sinon.match('[2.13.5 migration] Found 0 duplicate series'))).to.be.true - expect(loggerInfoStub.getCall(2).calledWith(sinon.match('[2.13.5 migration] Deduplication complete'))).to.be.true - expect(loggerInfoStub.getCall(3).calledWith(sinon.match('Added unique index on Series.name and Series.libraryId'))).to.be.true - expect(loggerInfoStub.getCall(4).calledWith(sinon.match('UPGRADE END: 2.13.5-series-column-unique '))).to.be.true - expect(loggerInfoStub.getCall(5).calledWith(sinon.match('DOWNGRADE BEGIN: 2.13.5-series-column-unique '))).to.be.true - expect(loggerInfoStub.getCall(6).calledWith(sinon.match('Removed unique index on Series.name and Series.libraryId'))).to.be.true - expect(loggerInfoStub.getCall(7).calledWith(sinon.match('DOWNGRADE END: 2.13.5-series-column-unique '))).to.be.true + expect(loggerInfoStub.getCall(0).calledWith(sinon.match('[2.14.0 migration] UPGRADE BEGIN: 2.14.0-series-column-unique '))).to.be.true + expect(loggerInfoStub.getCall(1).calledWith(sinon.match('[2.14.0 migration] Found 0 duplicate series'))).to.be.true + expect(loggerInfoStub.getCall(2).calledWith(sinon.match('[2.14.0 migration] Deduplication complete'))).to.be.true + expect(loggerInfoStub.getCall(3).calledWith(sinon.match('[2.14.0 migration] Added unique index on Series.name and Series.libraryId'))).to.be.true + expect(loggerInfoStub.getCall(4).calledWith(sinon.match('[2.14.0 migration] UPGRADE END: 2.14.0-series-column-unique '))).to.be.true + expect(loggerInfoStub.getCall(5).calledWith(sinon.match('[2.14.0 migration] DOWNGRADE BEGIN: 2.14.0-series-column-unique '))).to.be.true + expect(loggerInfoStub.getCall(6).calledWith(sinon.match('[2.14.0 migration] Removed unique index on Series.name and Series.libraryId'))).to.be.true + expect(loggerInfoStub.getCall(7).calledWith(sinon.match('[2.14.0 migration] DOWNGRADE END: 2.14.0-series-column-unique '))).to.be.true // Ensure index does not exist const indexes = await queryInterface.showIndex('Series') expect(indexes).to.not.deep.include({ tableName: 'Series', unique: true, fields: ['name', 'libraryId'], name: 'unique_series_name_per_library' }) From e6e494a92cbf5d72e40fd9268c5a65c07a6630ee Mon Sep 17 00:00:00 2001 From: Nicholas Wallace Date: Mon, 7 Oct 2024 18:52:14 -0700 Subject: [PATCH 14/44] Rename for next minor release --- server/migrations/changelog.md | 2 +- ...que.js => v2.15.0-series-column-unique.js} | 26 ++--- ...s => v2.15.0-series-column-unique.test.js} | 102 +++++++++--------- 3 files changed, 65 insertions(+), 65 deletions(-) rename server/migrations/{v2.14.0-series-column-unique.js => v2.15.0-series-column-unique.js} (88%) rename test/server/migrations/{v2.14.0-series-column-unique.test.js => v2.15.0-series-column-unique.test.js} (86%) diff --git a/server/migrations/changelog.md b/server/migrations/changelog.md index 3ab52ac3..b5dde749 100644 --- a/server/migrations/changelog.md +++ b/server/migrations/changelog.md @@ -4,4 +4,4 @@ Please add a record of every database migration that you create to this file. Th | Server Version | Migration Script Name | Description | | -------------- | ---------------------------- | ------------------------------------------------- | -| v2.14.0 | v2.14.0-series-column-unique | Series must have unique names in the same library | +| v2.15.0 | v2.15.0-series-column-unique | Series must have unique names in the same library | diff --git a/server/migrations/v2.14.0-series-column-unique.js b/server/migrations/v2.15.0-series-column-unique.js similarity index 88% rename from server/migrations/v2.14.0-series-column-unique.js rename to server/migrations/v2.15.0-series-column-unique.js index 489b670b..96b0ea60 100644 --- a/server/migrations/v2.14.0-series-column-unique.js +++ b/server/migrations/v2.15.0-series-column-unique.js @@ -16,13 +16,13 @@ */ async function up({ context: { queryInterface, logger } }) { // Upwards migration script - logger.info('[2.14.0 migration] UPGRADE BEGIN: 2.14.0-series-column-unique ') + logger.info('[2.15.0 migration] UPGRADE BEGIN: 2.15.0-series-column-unique ') // Check if the unique index already exists const seriesIndexes = await queryInterface.showIndex('Series') if (seriesIndexes.some((index) => index.name === 'unique_series_name_per_library')) { - logger.info('[2.14.0 migration] Unique index on Series.name and Series.libraryId already exists') - logger.info('[2.14.0 migration] UPGRADE END: 2.14.0-series-column-unique ') + logger.info('[2.15.0 migration] Unique index on Series.name and Series.libraryId already exists') + logger.info('[2.15.0 migration] UPGRADE END: 2.15.0-series-column-unique ') return } @@ -43,12 +43,12 @@ async function up({ context: { queryInterface, logger } }) { `) // Print out how many duplicates were found - logger.info(`[2.14.0 migration] Found ${duplicates.length} duplicate series`) + logger.info(`[2.15.0 migration] Found ${duplicates.length} duplicate series`) // Iterate over each duplicate series for (const duplicate of duplicates) { // Report the series name that is being deleted - logger.info(`[2.14.0 migration] Deduplicating series "${duplicate.name}" in library ${duplicate.libraryId}`) + logger.info(`[2.15.0 migration] Deduplicating series "${duplicate.name}" in library ${duplicate.libraryId}`) // Determine any duplicate book IDs in the `bookSeries` table for the same series const [duplicateBookIds] = await queryInterface.sequelize.query( @@ -73,7 +73,7 @@ async function up({ context: { queryInterface, logger } }) { // Iterate over the duplicate book IDs if there is at least one and only keep the first row that has this bookId and seriesId for (const { bookId } of duplicateBookIds) { - logger.info(`[2.14.0 migration] Deduplicating bookId ${bookId} in series "${duplicate.name}" of library ${duplicate.libraryId}`) + logger.info(`[2.15.0 migration] Deduplicating bookId ${bookId} in series "${duplicate.name}" of library ${duplicate.libraryId}`) // Get all rows of `BookSeries` table that have the same `bookId` and `seriesId`. Sort by `sequence` with nulls sorted last const [duplicateBookSeries] = await queryInterface.sequelize.query( ` @@ -113,7 +113,7 @@ async function up({ context: { queryInterface, logger } }) { } ) } - logger.info(`[2.14.0 migration] Finished cleanup of bookId ${bookId} in series "${duplicate.name}" of library ${duplicate.libraryId}`) + logger.info(`[2.15.0 migration] Finished cleanup of bookId ${bookId} in series "${duplicate.name}" of library ${duplicate.libraryId}`) } // Get all the most recent series which matches the `name` and `libraryId` @@ -174,16 +174,16 @@ async function up({ context: { queryInterface, logger } }) { } } - logger.info(`[2.14.0 migration] Deduplication complete`) + logger.info(`[2.15.0 migration] Deduplication complete`) // Create a unique index based on the name and library ID for the `Series` table await queryInterface.addIndex('Series', ['name', 'libraryId'], { unique: true, name: 'unique_series_name_per_library' }) - logger.info('[2.14.0 migration] Added unique index on Series.name and Series.libraryId') + logger.info('[2.15.0 migration] Added unique index on Series.name and Series.libraryId') - logger.info('[2.14.0 migration] UPGRADE END: 2.14.0-series-column-unique ') + logger.info('[2.15.0 migration] UPGRADE END: 2.15.0-series-column-unique ') } /** @@ -194,13 +194,13 @@ async function up({ context: { queryInterface, logger } }) { */ async function down({ context: { queryInterface, logger } }) { // Downward migration script - logger.info('[2.14.0 migration] DOWNGRADE BEGIN: 2.14.0-series-column-unique ') + logger.info('[2.15.0 migration] DOWNGRADE BEGIN: 2.15.0-series-column-unique ') // Remove the unique index await queryInterface.removeIndex('Series', 'unique_series_name_per_library') - logger.info('[2.14.0 migration] Removed unique index on Series.name and Series.libraryId') + logger.info('[2.15.0 migration] Removed unique index on Series.name and Series.libraryId') - logger.info('[2.14.0 migration] DOWNGRADE END: 2.14.0-series-column-unique ') + logger.info('[2.15.0 migration] DOWNGRADE END: 2.15.0-series-column-unique ') } module.exports = { up, down } diff --git a/test/server/migrations/v2.14.0-series-column-unique.test.js b/test/server/migrations/v2.15.0-series-column-unique.test.js similarity index 86% rename from test/server/migrations/v2.14.0-series-column-unique.test.js rename to test/server/migrations/v2.15.0-series-column-unique.test.js index 43acc927..4ae07e63 100644 --- a/test/server/migrations/v2.14.0-series-column-unique.test.js +++ b/test/server/migrations/v2.15.0-series-column-unique.test.js @@ -1,13 +1,13 @@ const { expect } = require('chai') const sinon = require('sinon') -const { up, down } = require('../../../server/migrations/v2.14.0-series-column-unique') +const { up, down } = require('../../../server/migrations/v2.15.0-series-column-unique') const { Sequelize } = require('sequelize') const Logger = require('../../../server/Logger') const { query } = require('express') const { logger } = require('sequelize/lib/utils/logger') const e = require('express') -describe('migration-v2.14.0-series-column-unique', () => { +describe('migration-v2.15.0-series-column-unique', () => { let sequelize let queryInterface let loggerInfoStub @@ -105,11 +105,11 @@ describe('migration-v2.14.0-series-column-unique', () => { await up({ context: { queryInterface, logger: Logger } }) expect(loggerInfoStub.callCount).to.equal(5) - expect(loggerInfoStub.getCall(0).calledWith(sinon.match('[2.14.0 migration] UPGRADE BEGIN: 2.14.0-series-column-unique '))).to.be.true - expect(loggerInfoStub.getCall(1).calledWith(sinon.match('[2.14.0 migration] Found 0 duplicate series'))).to.be.true - expect(loggerInfoStub.getCall(2).calledWith(sinon.match('[2.14.0 migration] Deduplication complete'))).to.be.true - expect(loggerInfoStub.getCall(3).calledWith(sinon.match('[2.14.0 migration] Added unique index on Series.name and Series.libraryId'))).to.be.true - expect(loggerInfoStub.getCall(4).calledWith(sinon.match('[2.14.0 migration] UPGRADE END: 2.14.0-series-column-unique '))).to.be.true + expect(loggerInfoStub.getCall(0).calledWith(sinon.match('[2.15.0 migration] UPGRADE BEGIN: 2.15.0-series-column-unique '))).to.be.true + expect(loggerInfoStub.getCall(1).calledWith(sinon.match('[2.15.0 migration] Found 0 duplicate series'))).to.be.true + expect(loggerInfoStub.getCall(2).calledWith(sinon.match('[2.15.0 migration] Deduplication complete'))).to.be.true + expect(loggerInfoStub.getCall(3).calledWith(sinon.match('[2.15.0 migration] Added unique index on Series.name and Series.libraryId'))).to.be.true + expect(loggerInfoStub.getCall(4).calledWith(sinon.match('[2.15.0 migration] UPGRADE END: 2.15.0-series-column-unique '))).to.be.true // Validate rows in tables const series = await queryInterface.sequelize.query('SELECT "id", "name", "libraryId" FROM Series', { type: queryInterface.sequelize.QueryTypes.SELECT }) expect(series).to.have.length(3) @@ -145,13 +145,13 @@ describe('migration-v2.14.0-series-column-unique', () => { await up({ context: { queryInterface, logger: Logger } }) expect(loggerInfoStub.callCount).to.equal(7) - expect(loggerInfoStub.getCall(0).calledWith(sinon.match('[2.14.0 migration] UPGRADE BEGIN: 2.14.0-series-column-unique '))).to.be.true - expect(loggerInfoStub.getCall(1).calledWith(sinon.match('[2.14.0 migration] Found 2 duplicate series'))).to.be.true - expect(loggerInfoStub.getCall(2).calledWith(sinon.match('[2.14.0 migration] Deduplicating series "Series 1" in library 3a5a1c7c-a914-472e-88b0-b871ceae63e7'))).to.be.true - expect(loggerInfoStub.getCall(3).calledWith(sinon.match('[2.14.0 migration] Deduplicating series "Series 3" in library 3a5a1c7c-a914-472e-88b0-b871ceae63e7'))).to.be.true - expect(loggerInfoStub.getCall(4).calledWith(sinon.match('[2.14.0 migration] Deduplication complete'))).to.be.true - expect(loggerInfoStub.getCall(5).calledWith(sinon.match('[2.14.0 migration] Added unique index on Series.name and Series.libraryId'))).to.be.true - expect(loggerInfoStub.getCall(6).calledWith(sinon.match('[2.14.0 migration] UPGRADE END: 2.14.0-series-column-unique '))).to.be.true + expect(loggerInfoStub.getCall(0).calledWith(sinon.match('[2.15.0 migration] UPGRADE BEGIN: 2.15.0-series-column-unique '))).to.be.true + expect(loggerInfoStub.getCall(1).calledWith(sinon.match('[2.15.0 migration] Found 2 duplicate series'))).to.be.true + expect(loggerInfoStub.getCall(2).calledWith(sinon.match('[2.15.0 migration] Deduplicating series "Series 1" in library 3a5a1c7c-a914-472e-88b0-b871ceae63e7'))).to.be.true + expect(loggerInfoStub.getCall(3).calledWith(sinon.match('[2.15.0 migration] Deduplicating series "Series 3" in library 3a5a1c7c-a914-472e-88b0-b871ceae63e7'))).to.be.true + expect(loggerInfoStub.getCall(4).calledWith(sinon.match('[2.15.0 migration] Deduplication complete'))).to.be.true + expect(loggerInfoStub.getCall(5).calledWith(sinon.match('[2.15.0 migration] Added unique index on Series.name and Series.libraryId'))).to.be.true + expect(loggerInfoStub.getCall(6).calledWith(sinon.match('[2.15.0 migration] UPGRADE END: 2.15.0-series-column-unique '))).to.be.true // Validate rows const series = await queryInterface.sequelize.query('SELECT "id", "name", "libraryId" FROM Series', { type: queryInterface.sequelize.QueryTypes.SELECT }) expect(series).to.have.length(3) @@ -182,11 +182,11 @@ describe('migration-v2.14.0-series-column-unique', () => { await up({ context: { queryInterface, logger: Logger } }) expect(loggerInfoStub.callCount).to.equal(5) - expect(loggerInfoStub.getCall(0).calledWith(sinon.match('[2.14.0 migration] UPGRADE BEGIN: 2.14.0-series-column-unique '))).to.be.true - expect(loggerInfoStub.getCall(1).calledWith(sinon.match('[2.14.0 migration] Found 0 duplicate series'))).to.be.true - expect(loggerInfoStub.getCall(2).calledWith(sinon.match('[2.14.0 migration] Deduplication complete'))).to.be.true - expect(loggerInfoStub.getCall(3).calledWith(sinon.match('[2.14.0 migration] Added unique index on Series.name and Series.libraryId'))).to.be.true - expect(loggerInfoStub.getCall(4).calledWith(sinon.match('[2.14.0 migration] UPGRADE END: 2.14.0-series-column-unique '))).to.be.true + expect(loggerInfoStub.getCall(0).calledWith(sinon.match('[2.15.0 migration] UPGRADE BEGIN: 2.15.0-series-column-unique '))).to.be.true + expect(loggerInfoStub.getCall(1).calledWith(sinon.match('[2.15.0 migration] Found 0 duplicate series'))).to.be.true + expect(loggerInfoStub.getCall(2).calledWith(sinon.match('[2.15.0 migration] Deduplication complete'))).to.be.true + expect(loggerInfoStub.getCall(3).calledWith(sinon.match('[2.15.0 migration] Added unique index on Series.name and Series.libraryId'))).to.be.true + expect(loggerInfoStub.getCall(4).calledWith(sinon.match('[2.15.0 migration] UPGRADE END: 2.15.0-series-column-unique '))).to.be.true // Validate rows const series = await queryInterface.sequelize.query('SELECT "id", "name", "libraryId" FROM Series', { type: queryInterface.sequelize.QueryTypes.SELECT }) expect(series).to.have.length(2) @@ -212,14 +212,14 @@ describe('migration-v2.14.0-series-column-unique', () => { await up({ context: { queryInterface, logger: Logger } }) expect(loggerInfoStub.callCount).to.equal(8) - expect(loggerInfoStub.getCall(0).calledWith(sinon.match('[2.14.0 migration] UPGRADE BEGIN: 2.14.0-series-column-unique '))).to.be.true - expect(loggerInfoStub.getCall(1).calledWith(sinon.match('[2.14.0 migration] Found 1 duplicate series'))).to.be.true - expect(loggerInfoStub.getCall(2).calledWith(sinon.match('[2.14.0 migration] Deduplicating series "Series 1" in library 3a5a1c7c-a914-472e-88b0-b871ceae63e7'))).to.be.true - expect(loggerInfoStub.getCall(3).calledWith(sinon.match('[2.14.0 migration] Deduplicating bookId 4a38b6e5-0ae4-4de4-b119-4e33891bd63f in series "Series 1" of library 3a5a1c7c-a914-472e-88b0-b871ceae63e7'))).to.be.true - expect(loggerInfoStub.getCall(4).calledWith(sinon.match('[2.14.0 migration] Finished cleanup of bookId 4a38b6e5-0ae4-4de4-b119-4e33891bd63f in series "Series 1" of library 3a5a1c7c-a914-472e-88b0-b871ceae63e7'))).to.be.true - expect(loggerInfoStub.getCall(5).calledWith(sinon.match('[2.14.0 migration] Deduplication complete'))).to.be.true - expect(loggerInfoStub.getCall(6).calledWith(sinon.match('[2.14.0 migration] Added unique index on Series.name and Series.libraryId'))).to.be.true - expect(loggerInfoStub.getCall(7).calledWith(sinon.match('[2.14.0 migration] UPGRADE END: 2.14.0-series-column-unique '))).to.be.true + expect(loggerInfoStub.getCall(0).calledWith(sinon.match('[2.15.0 migration] UPGRADE BEGIN: 2.15.0-series-column-unique '))).to.be.true + expect(loggerInfoStub.getCall(1).calledWith(sinon.match('[2.15.0 migration] Found 1 duplicate series'))).to.be.true + expect(loggerInfoStub.getCall(2).calledWith(sinon.match('[2.15.0 migration] Deduplicating series "Series 1" in library 3a5a1c7c-a914-472e-88b0-b871ceae63e7'))).to.be.true + expect(loggerInfoStub.getCall(3).calledWith(sinon.match('[2.15.0 migration] Deduplicating bookId 4a38b6e5-0ae4-4de4-b119-4e33891bd63f in series "Series 1" of library 3a5a1c7c-a914-472e-88b0-b871ceae63e7'))).to.be.true + expect(loggerInfoStub.getCall(4).calledWith(sinon.match('[2.15.0 migration] Finished cleanup of bookId 4a38b6e5-0ae4-4de4-b119-4e33891bd63f in series "Series 1" of library 3a5a1c7c-a914-472e-88b0-b871ceae63e7'))).to.be.true + expect(loggerInfoStub.getCall(5).calledWith(sinon.match('[2.15.0 migration] Deduplication complete'))).to.be.true + expect(loggerInfoStub.getCall(6).calledWith(sinon.match('[2.15.0 migration] Added unique index on Series.name and Series.libraryId'))).to.be.true + expect(loggerInfoStub.getCall(7).calledWith(sinon.match('[2.15.0 migration] UPGRADE END: 2.15.0-series-column-unique '))).to.be.true // validate rows const series = await queryInterface.sequelize.query('SELECT "id", "name", "libraryId" FROM Series', { type: queryInterface.sequelize.QueryTypes.SELECT }) expect(series).to.have.length(1) @@ -244,14 +244,14 @@ describe('migration-v2.14.0-series-column-unique', () => { await up({ context: { queryInterface, logger: Logger } }) expect(loggerInfoStub.callCount).to.equal(8) - expect(loggerInfoStub.getCall(0).calledWith(sinon.match('[2.14.0 migration] UPGRADE BEGIN: 2.14.0-series-column-unique '))).to.be.true - expect(loggerInfoStub.getCall(1).calledWith(sinon.match('[2.14.0 migration] Found 1 duplicate series'))).to.be.true - expect(loggerInfoStub.getCall(2).calledWith(sinon.match('[2.14.0 migration] Deduplicating series "Series 1" in library 3a5a1c7c-a914-472e-88b0-b871ceae63e7'))).to.be.true - expect(loggerInfoStub.getCall(3).calledWith(sinon.match('[2.14.0 migration] Deduplicating bookId 4a38b6e5-0ae4-4de4-b119-4e33891bd63f in series "Series 1" of library 3a5a1c7c-a914-472e-88b0-b871ceae63e7'))).to.be.true - expect(loggerInfoStub.getCall(4).calledWith(sinon.match('[2.14.0 migration] Finished cleanup of bookId 4a38b6e5-0ae4-4de4-b119-4e33891bd63f in series "Series 1" of library 3a5a1c7c-a914-472e-88b0-b871ceae63e7'))).to.be.true - expect(loggerInfoStub.getCall(5).calledWith(sinon.match('[2.14.0 migration] Deduplication complete'))).to.be.true - expect(loggerInfoStub.getCall(6).calledWith(sinon.match('[2.14.0 migration] Added unique index on Series.name and Series.libraryId'))).to.be.true - expect(loggerInfoStub.getCall(7).calledWith(sinon.match('[2.14.0 migration] UPGRADE END: 2.14.0-series-column-unique '))).to.be.true + expect(loggerInfoStub.getCall(0).calledWith(sinon.match('[2.15.0 migration] UPGRADE BEGIN: 2.15.0-series-column-unique '))).to.be.true + expect(loggerInfoStub.getCall(1).calledWith(sinon.match('[2.15.0 migration] Found 1 duplicate series'))).to.be.true + expect(loggerInfoStub.getCall(2).calledWith(sinon.match('[2.15.0 migration] Deduplicating series "Series 1" in library 3a5a1c7c-a914-472e-88b0-b871ceae63e7'))).to.be.true + expect(loggerInfoStub.getCall(3).calledWith(sinon.match('[2.15.0 migration] Deduplicating bookId 4a38b6e5-0ae4-4de4-b119-4e33891bd63f in series "Series 1" of library 3a5a1c7c-a914-472e-88b0-b871ceae63e7'))).to.be.true + expect(loggerInfoStub.getCall(4).calledWith(sinon.match('[2.15.0 migration] Finished cleanup of bookId 4a38b6e5-0ae4-4de4-b119-4e33891bd63f in series "Series 1" of library 3a5a1c7c-a914-472e-88b0-b871ceae63e7'))).to.be.true + expect(loggerInfoStub.getCall(5).calledWith(sinon.match('[2.15.0 migration] Deduplication complete'))).to.be.true + expect(loggerInfoStub.getCall(6).calledWith(sinon.match('[2.15.0 migration] Added unique index on Series.name and Series.libraryId'))).to.be.true + expect(loggerInfoStub.getCall(7).calledWith(sinon.match('[2.15.0 migration] UPGRADE END: 2.15.0-series-column-unique '))).to.be.true // validate rows const series = await queryInterface.sequelize.query('SELECT "id", "name", "libraryId" FROM Series', { type: queryInterface.sequelize.QueryTypes.SELECT }) expect(series).to.have.length(1) @@ -275,14 +275,14 @@ describe('migration-v2.14.0-series-column-unique', () => { await up({ context: { queryInterface, logger: Logger } }) expect(loggerInfoStub.callCount).to.equal(8) - expect(loggerInfoStub.getCall(0).calledWith(sinon.match('[2.14.0 migration] UPGRADE BEGIN: 2.14.0-series-column-unique '))).to.be.true - expect(loggerInfoStub.getCall(1).calledWith(sinon.match('[2.14.0 migration] Found 1 duplicate series'))).to.be.true - expect(loggerInfoStub.getCall(2).calledWith(sinon.match('[2.14.0 migration] Deduplicating series "Series 1" in library 3a5a1c7c-a914-472e-88b0-b871ceae63e7'))).to.be.true - expect(loggerInfoStub.getCall(3).calledWith(sinon.match('[2.14.0 migration] Deduplicating bookId 4a38b6e5-0ae4-4de4-b119-4e33891bd63f in series "Series 1" of library 3a5a1c7c-a914-472e-88b0-b871ceae63e7'))).to.be.true - expect(loggerInfoStub.getCall(4).calledWith(sinon.match('[2.14.0 migration] Finished cleanup of bookId 4a38b6e5-0ae4-4de4-b119-4e33891bd63f in series "Series 1" of library 3a5a1c7c-a914-472e-88b0-b871ceae63e7'))).to.be.true - expect(loggerInfoStub.getCall(5).calledWith(sinon.match('[2.14.0 migration] Deduplication complete'))).to.be.true - expect(loggerInfoStub.getCall(6).calledWith(sinon.match('[2.14.0 migration] Added unique index on Series.name and Series.libraryId'))).to.be.true - expect(loggerInfoStub.getCall(7).calledWith(sinon.match('[2.14.0 migration] UPGRADE END: 2.14.0-series-column-unique '))).to.be.true + expect(loggerInfoStub.getCall(0).calledWith(sinon.match('[2.15.0 migration] UPGRADE BEGIN: 2.15.0-series-column-unique '))).to.be.true + expect(loggerInfoStub.getCall(1).calledWith(sinon.match('[2.15.0 migration] Found 1 duplicate series'))).to.be.true + expect(loggerInfoStub.getCall(2).calledWith(sinon.match('[2.15.0 migration] Deduplicating series "Series 1" in library 3a5a1c7c-a914-472e-88b0-b871ceae63e7'))).to.be.true + expect(loggerInfoStub.getCall(3).calledWith(sinon.match('[2.15.0 migration] Deduplicating bookId 4a38b6e5-0ae4-4de4-b119-4e33891bd63f in series "Series 1" of library 3a5a1c7c-a914-472e-88b0-b871ceae63e7'))).to.be.true + expect(loggerInfoStub.getCall(4).calledWith(sinon.match('[2.15.0 migration] Finished cleanup of bookId 4a38b6e5-0ae4-4de4-b119-4e33891bd63f in series "Series 1" of library 3a5a1c7c-a914-472e-88b0-b871ceae63e7'))).to.be.true + expect(loggerInfoStub.getCall(5).calledWith(sinon.match('[2.15.0 migration] Deduplication complete'))).to.be.true + expect(loggerInfoStub.getCall(6).calledWith(sinon.match('[2.15.0 migration] Added unique index on Series.name and Series.libraryId'))).to.be.true + expect(loggerInfoStub.getCall(7).calledWith(sinon.match('[2.15.0 migration] UPGRADE END: 2.15.0-series-column-unique '))).to.be.true // validate rows const series = await queryInterface.sequelize.query('SELECT "id", "name", "libraryId" FROM Series', { type: queryInterface.sequelize.QueryTypes.SELECT }) expect(series).to.have.length(1) @@ -319,14 +319,14 @@ describe('migration-v2.14.0-series-column-unique', () => { await down({ context: { queryInterface, logger: Logger } }) expect(loggerInfoStub.callCount).to.equal(8) - expect(loggerInfoStub.getCall(0).calledWith(sinon.match('[2.14.0 migration] UPGRADE BEGIN: 2.14.0-series-column-unique '))).to.be.true - expect(loggerInfoStub.getCall(1).calledWith(sinon.match('[2.14.0 migration] Found 0 duplicate series'))).to.be.true - expect(loggerInfoStub.getCall(2).calledWith(sinon.match('[2.14.0 migration] Deduplication complete'))).to.be.true - expect(loggerInfoStub.getCall(3).calledWith(sinon.match('[2.14.0 migration] Added unique index on Series.name and Series.libraryId'))).to.be.true - expect(loggerInfoStub.getCall(4).calledWith(sinon.match('[2.14.0 migration] UPGRADE END: 2.14.0-series-column-unique '))).to.be.true - expect(loggerInfoStub.getCall(5).calledWith(sinon.match('[2.14.0 migration] DOWNGRADE BEGIN: 2.14.0-series-column-unique '))).to.be.true - expect(loggerInfoStub.getCall(6).calledWith(sinon.match('[2.14.0 migration] Removed unique index on Series.name and Series.libraryId'))).to.be.true - expect(loggerInfoStub.getCall(7).calledWith(sinon.match('[2.14.0 migration] DOWNGRADE END: 2.14.0-series-column-unique '))).to.be.true + expect(loggerInfoStub.getCall(0).calledWith(sinon.match('[2.15.0 migration] UPGRADE BEGIN: 2.15.0-series-column-unique '))).to.be.true + expect(loggerInfoStub.getCall(1).calledWith(sinon.match('[2.15.0 migration] Found 0 duplicate series'))).to.be.true + expect(loggerInfoStub.getCall(2).calledWith(sinon.match('[2.15.0 migration] Deduplication complete'))).to.be.true + expect(loggerInfoStub.getCall(3).calledWith(sinon.match('[2.15.0 migration] Added unique index on Series.name and Series.libraryId'))).to.be.true + expect(loggerInfoStub.getCall(4).calledWith(sinon.match('[2.15.0 migration] UPGRADE END: 2.15.0-series-column-unique '))).to.be.true + expect(loggerInfoStub.getCall(5).calledWith(sinon.match('[2.15.0 migration] DOWNGRADE BEGIN: 2.15.0-series-column-unique '))).to.be.true + expect(loggerInfoStub.getCall(6).calledWith(sinon.match('[2.15.0 migration] Removed unique index on Series.name and Series.libraryId'))).to.be.true + expect(loggerInfoStub.getCall(7).calledWith(sinon.match('[2.15.0 migration] DOWNGRADE END: 2.15.0-series-column-unique '))).to.be.true // Ensure index does not exist const indexes = await queryInterface.showIndex('Series') expect(indexes).to.not.deep.include({ tableName: 'Series', unique: true, fields: ['name', 'libraryId'], name: 'unique_series_name_per_library' }) From f98f78a5bd2673f1cc62e798f18b09fc16f35491 Mon Sep 17 00:00:00 2001 From: Nicholas Wallace Date: Thu, 10 Oct 2024 21:14:51 -0700 Subject: [PATCH 15/44] Podcast search strings --- client/pages/library/_library/podcast/search.vue | 10 +++++----- client/strings/en-us.json | 5 +++++ 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/client/pages/library/_library/podcast/search.vue b/client/pages/library/_library/podcast/search.vue index b80ca2f8..983b9025 100644 --- a/client/pages/library/_library/podcast/search.vue +++ b/client/pages/library/_library/podcast/search.vue @@ -5,7 +5,7 @@
- + {{ $strings.ButtonSubmit }} @@ -108,7 +108,7 @@ export default { if (!txt || !txt.includes(' tag not found OR an tag was not found') + this.$toast.error(this.$strings.MessageTaskOpmlParseFastFail) this.processing = false return } @@ -117,7 +117,7 @@ export default { .$post(`/api/podcasts/opml/parse`, { opmlText: txt }) .then((data) => { if (!data.feeds?.length) { - this.$toast.error('No feeds found in OPML file') + this.$toast.error(this.$strings.MessageTaskOpmlParseNoneFound) } else { this.opmlFeeds = data.feeds || [] this.showOPMLFeedsModal = true @@ -125,7 +125,7 @@ export default { }) .catch((error) => { console.error('Failed', error) - this.$toast.error('Failed to parse OPML file') + this.$toast.error(this.$strings.MessageTaskOpmlParseFailed) }) .finally(() => { this.processing = false @@ -191,7 +191,7 @@ export default { return } if (!podcast.feedUrl) { - this.$toast.error('Invalid podcast - no feed') + this.$toast.error(this.$strings.MessageNoPodcastFeed) return } this.processing = true diff --git a/client/strings/en-us.json b/client/strings/en-us.json index 34b014dc..bbc29146 100644 --- a/client/strings/en-us.json +++ b/client/strings/en-us.json @@ -746,6 +746,7 @@ "MessageNoLogs": "No Logs", "MessageNoMediaProgress": "No Media Progress", "MessageNoNotifications": "No Notifications", + "MessageNoPodcastFeed": "Invalid podcast: No Feed", "MessageNoPodcastsFound": "No podcasts found", "MessageNoResults": "No Results", "MessageNoSearchResultsFor": "No search results for \"{0}\"", @@ -762,6 +763,7 @@ "MessagePlaylistCreateFromCollection": "Create playlist from collection", "MessagePleaseWait": "Please wait...", "MessagePodcastHasNoRSSFeedForMatching": "Podcast has no RSS feed url to use for matching", + "MessagePodcastSearchField": "Enter search term or RSS feed URL", "MessageQuickMatchDescription": "Populate empty item details & cover with first match result from '{0}'. Does not overwrite details unless 'Prefer matched metadata' server setting is enabled.", "MessageRemoveChapter": "Remove chapter", "MessageRemoveEpisodes": "Remove {0} episode(s)", @@ -804,6 +806,9 @@ "MessageTaskOpmlImportFeedPodcastExists": "Podcast already exists at path", "MessageTaskOpmlImportFeedPodcastFailed": "Failed to create podcast", "MessageTaskOpmlImportFinished": "Added {0} podcasts", + "MessageTaskOpmlParseFailed": "Failed to parse OPML file", + "MessageTaskOpmlParseFastFail": "Invalid OPML file tag not found OR an tag was not found", + "MessageTaskOpmlParseNoneFound": "No feeds found in OPML file", "MessageTaskScanItemsAdded": "{0} added", "MessageTaskScanItemsMissing": "{0} missing", "MessageTaskScanItemsUpdated": "{0} updated", From 29db5f199095693522cf2e715ebd9c5eab944ae4 Mon Sep 17 00:00:00 2001 From: Nicholas Wallace Date: Thu, 10 Oct 2024 21:21:15 -0700 Subject: [PATCH 16/44] Update: tools strings --- client/components/modals/item/tabs/Tools.vue | 8 ++++---- client/strings/en-us.json | 3 +++ 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/client/components/modals/item/tabs/Tools.vue b/client/components/modals/item/tabs/Tools.vue index ad9435c4..5f148c3d 100644 --- a/client/components/modals/item/tabs/Tools.vue +++ b/client/components/modals/item/tabs/Tools.vue @@ -33,18 +33,18 @@ launch - Quick Embed + {{ $strings.ButtonQuickEmbed }}
-

Queued for metadata embed ({{ queuedEmbedLIds.length }} in queue)

+

{{ $getString('MessageQuickEmbedQueue', [queuedEmbedLIds.length]) }}

-

Currently embedding metadata

+

{{ $strings.MessageQuickEmbedInProgress }}

@@ -113,7 +113,7 @@ export default { methods: { quickEmbed() { const payload = { - message: 'Warning! Quick embed will not backup your audio files. Make sure that you have a backup of your audio files.

Would you like to continue?', + message: this.$strings.MessageConfirmQuickEmbed, callback: (confirmed) => { if (confirmed) { this.$axios diff --git a/client/strings/en-us.json b/client/strings/en-us.json index bbc29146..cd8f7684 100644 --- a/client/strings/en-us.json +++ b/client/strings/en-us.json @@ -66,6 +66,7 @@ "ButtonPurgeItemsCache": "Purge Items Cache", "ButtonQueueAddItem": "Add to queue", "ButtonQueueRemoveItem": "Remove from queue", + "ButtonQuickEmbed": "Quick Embed", "ButtonQuickEmbedMetadata": "Quick Embed Metadata", "ButtonQuickMatch": "Quick Match", "ButtonReScan": "Re-Scan", @@ -764,6 +765,8 @@ "MessagePleaseWait": "Please wait...", "MessagePodcastHasNoRSSFeedForMatching": "Podcast has no RSS feed url to use for matching", "MessagePodcastSearchField": "Enter search term or RSS feed URL", + "MessageQuickEmbedInProgress": "Quick embed in progress", + "MessageQuickEmbedQueue": "Queued for quick embed ({0} in queue)", "MessageQuickMatchDescription": "Populate empty item details & cover with first match result from '{0}'. Does not overwrite details unless 'Prefer matched metadata' server setting is enabled.", "MessageRemoveChapter": "Remove chapter", "MessageRemoveEpisodes": "Remove {0} episode(s)", From c33b470fcaf660eb7a59be57dc55587230b00e57 Mon Sep 17 00:00:00 2001 From: Nicholas Wallace Date: Thu, 10 Oct 2024 21:58:17 -0700 Subject: [PATCH 17/44] Tools Manager strings --- client/pages/audiobook/_id/manage.vue | 34 +++++++++++++-------------- client/strings/en-us.json | 16 +++++++++++++ 2 files changed, 33 insertions(+), 17 deletions(-) diff --git a/client/pages/audiobook/_id/manage.vue b/client/pages/audiobook/_id/manage.vue index 7de82b51..fb5ccc29 100644 --- a/client/pages/audiobook/_id/manage.vue +++ b/client/pages/audiobook/_id/manage.vue @@ -63,11 +63,11 @@
-

Audiobook is queued for metadata embed ({{ queuedEmbedLIds.length }} in queue)

+

{{ $getString('MessageEmbedQueue', [queuedEmbedLIds.length]) }}

- +
@@ -78,7 +78,7 @@
@@ -94,11 +94,11 @@
- - - + + +
-

Warning: Do not update these settings unless you are familiar with ffmpeg encoding options.

+

{{ $strings.LabelEncodingWarningAdvancedSettings }}

@@ -106,36 +106,36 @@
star -

Metadata will be embedded in the audio tracks inside your audiobook folder.

+

{{ $strings.LabelEncodingInfoEmbedded }}

star

- Finished M4B will be put into your audiobook folder at .../{{ libraryItemRelPath }}/. + {{ $strings.LabelEncodingFinishedM4B }} .../{{ libraryItemRelPath }}/.

star

- A backup of your original audio files will be stored in /metadata/cache/items/{{ libraryItemId }}/. Make sure to periodically purge items cache. + {{ $strings.LabelEncodingBackupLocation }} /metadata/cache/items/{{ libraryItemId }}/. {{ $strings.LabelEncodingClearItemCache }}

star -

Chapters are not embedded in multi-track audiobooks.

+

{{ $strings.LabelEncodingChaptersNotEmbedded }}

star -

Encoding can take up to 30 minutes.

+

{{ $strings.LabelEncodingTimeWarning }}

star -

If you have the watcher disabled you will need to re-scan this audiobook afterwards.

+

{{ $strings.LabelEncodingWatcherDisabled }}

star -

Once the task is started you can navigate away from this page.

+

{{ $strings.LabelEncodingStartedNavigation }}

@@ -269,11 +269,11 @@ export default { }, availableTools() { if (this.isSingleM4b) { - return [{ value: 'embed', text: 'Embed Metadata' }] + return [{ value: 'embed', text: this.$strings.LabelToolsEmbedMetadata }] } else { return [ - { value: 'embed', text: 'Embed Metadata' }, - { value: 'm4b', text: 'M4B Encoder' } + { value: 'embed', text: this.$strings.LabelToolsEmbedMetadata }, + { value: 'm4b', text: this.$strings.LabelToolsM4bEncoder } ] } }, diff --git a/client/strings/en-us.json b/client/strings/en-us.json index cd8f7684..4370851d 100644 --- a/client/strings/en-us.json +++ b/client/strings/en-us.json @@ -226,6 +226,9 @@ "LabelAllUsersIncludingGuests": "All users including guests", "LabelAlreadyInYourLibrary": "Already in your library", "LabelAppend": "Append", + "LabelAudioBitrate": "Audio Bitrate (e.g. 128k)", + "LabelAudioChannels": "Audio Channels (1 or 2)", + "LabelAudioCodec": "Audio Codec", "LabelAuthor": "Author", "LabelAuthorFirstLast": "Author (First Last)", "LabelAuthorLastFirst": "Author (Last, First)", @@ -238,6 +241,7 @@ "LabelAutoRegister": "Auto Register", "LabelAutoRegisterDescription": "Automatically create new users after logging in", "LabelBackToUser": "Back to User", + "LabelBackupAudioFiles": "Backup Audio Files", "LabelBackupLocation": "Backup Location", "LabelBackupsEnableAutomaticBackups": "Enable automatic backups", "LabelBackupsEnableAutomaticBackupsHelp": "Backups saved in /metadata/backups", @@ -304,6 +308,15 @@ "LabelEmailSettingsTestAddress": "Test Address", "LabelEmbeddedCover": "Embedded Cover", "LabelEnable": "Enable", + "LabelEncodingBackupLocation": "A backup of your original audio files will be stored in:", + "LabelEncodingChaptersNotEmbedded": "Chapters are not embedded in multi-track audiobooks.", + "LabelEncodingClearItemCache": "Make sure to periodically purge items cache.", + "LabelEncodingFinishedM4B": "Finished M4B will be put into your audiobook folder at:", + "LabelEncodingInfoEmbedded": "Metadata will be embedded in the audio tracks inside your audiobook folder.", + "LabelEncodingStartedNavigation": "Once the task is started you can navigate away from this page.", + "LabelEncodingTimeWarning": "Encoding can take up to 30 minutes.", + "LabelEncodingWarningAdvancedSettings": "Warning: Do not update these settings unless you are familiar with ffmpeg encoding options.", + "LabelEncodingWatcherDisabled": "If you have the watcher disabled you will need to re-scan this audiobook afterwards.", "LabelEnd": "End", "LabelEndOfChapter": "End of Chapter", "LabelEpisode": "Episode", @@ -597,6 +610,7 @@ "LabelTitle": "Title", "LabelToolsEmbedMetadata": "Embed Metadata", "LabelToolsEmbedMetadataDescription": "Embed metadata into audio files including cover image and chapters.", + "LabelToolsM4bEncoder": "M4B Encoder", "LabelToolsMakeM4b": "Make M4B Audiobook File", "LabelToolsMakeM4bDescription": "Generate a .M4B audiobook file with embedded metadata, cover image, and chapters.", "LabelToolsSplitM4b": "Split M4B to MP3's", @@ -622,6 +636,7 @@ "LabelUploaderDragAndDrop": "Drag & drop files or folders", "LabelUploaderDropFiles": "Drop files", "LabelUploaderItemFetchMetadataHelp": "Automatically fetch title, author, and series", + "LabelUseAdvancedOptions": "Use Advanced Options", "LabelUseChapterTrack": "Use chapter track", "LabelUseFullTrack": "Use full track", "LabelUser": "User", @@ -703,6 +718,7 @@ "MessageDragFilesIntoTrackOrder": "Drag files into correct track order", "MessageEmbedFailed": "Embed Failed!", "MessageEmbedFinished": "Embed Finished!", + "MessageEmbedQueue": "Queued for metadata embed ({0} in queue)", "MessageEpisodesQueuedForDownload": "{0} Episode(s) queued for download", "MessageEreaderDevices": "To ensure delivery of ebooks, you may need to add the above email address as a valid sender for each device listed below.", "MessageFeedURLWillBe": "Feed URL will be {0}", From 8a20510cde0286eca1790210c471d5bdb59ec2c5 Mon Sep 17 00:00:00 2001 From: Nicholas Wallace Date: Thu, 10 Oct 2024 22:12:31 -0700 Subject: [PATCH 18/44] Localize: subtitle `books` --- client/components/cards/LazyBookCard.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/components/cards/LazyBookCard.vue b/client/components/cards/LazyBookCard.vue index ae2cdd5b..09424b3c 100644 --- a/client/components/cards/LazyBookCard.vue +++ b/client/components/cards/LazyBookCard.vue @@ -325,7 +325,7 @@ export default { }, displaySubtitle() { if (!this.libraryItem) return '\u00A0' - if (this.collapsedSeries) return this.collapsedSeries.numBooks === 1 ? '1 book' : `${this.collapsedSeries.numBooks} books` + if (this.collapsedSeries) return `${this.collapsedSeries.numBooks} ${this.$strings.LabelBooks}` if (this.mediaMetadata.subtitle) return this.mediaMetadata.subtitle if (this.mediaMetadata.seriesName) return this.mediaMetadata.seriesName return '' From df6afc957f51e2b46955d6dab7e1fd8649ce6fdd Mon Sep 17 00:00:00 2001 From: advplyr Date: Sat, 12 Oct 2024 15:22:21 -0500 Subject: [PATCH 19/44] Add localization for notification descriptions --- .../modals/notification/NotificationEditModal.vue | 8 +++++++- client/strings/en-us.json | 4 ++++ server/utils/notifications.js | 4 ++++ 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/client/components/modals/notification/NotificationEditModal.vue b/client/components/modals/notification/NotificationEditModal.vue index 32fa20ea..83b5a9d0 100644 --- a/client/components/modals/notification/NotificationEditModal.vue +++ b/client/components/modals/notification/NotificationEditModal.vue @@ -77,7 +77,13 @@ export default { return this.notificationData.events || [] }, eventOptions() { - return this.notificationEvents.map((e) => ({ value: e.name, text: e.name, subtext: e.description })) + return this.notificationEvents.map((e) => { + return { + value: e.name, + text: e.name, + subtext: this.$strings[e.descriptionKey] || e.description + } + }) }, selectedEventData() { return this.notificationEvents.find((e) => e.name === this.newNotification.eventName) diff --git a/client/strings/en-us.json b/client/strings/en-us.json index 4370851d..edad1672 100644 --- a/client/strings/en-us.json +++ b/client/strings/en-us.json @@ -852,6 +852,10 @@ "NoteUploaderFoldersWithMediaFiles": "Folders with media files will be handled as separate library items.", "NoteUploaderOnlyAudioFiles": "If uploading only audio files then each audio file will be handled as a separate audiobook.", "NoteUploaderUnsupportedFiles": "Unsupported files are ignored. When choosing or dropping a folder, other files that are not in an item folder are ignored.", + "NotificationOnBackupCompletedDescription": "Triggered when a backup is completed", + "NotificationOnBackupFailedDescription": "Triggered when a backup fails", + "NotificationOnEpisodeDownloadedDescription": "Triggered when a podcast episode is auto-downloaded", + "NotificationOnTestDescription": "Event for testing the notification system", "PlaceholderNewCollection": "New collection name", "PlaceholderNewFolderPath": "New folder path", "PlaceholderNewPlaylist": "New playlist name", diff --git a/server/utils/notifications.js b/server/utils/notifications.js index 96e8ddf8..7a3e1198 100644 --- a/server/utils/notifications.js +++ b/server/utils/notifications.js @@ -7,6 +7,7 @@ module.exports.notificationData = { requiresLibrary: true, libraryMediaType: 'podcast', description: 'Triggered when a podcast episode is auto-downloaded', + descriptionKey: 'NotificationOnEpisodeDownloadedDescription', variables: ['libraryItemId', 'libraryId', 'podcastTitle', 'podcastAuthor', 'podcastDescription', 'podcastGenres', 'episodeTitle', 'episodeSubtitle', 'episodeDescription', 'libraryName', 'episodeId', 'mediaTags'], defaults: { title: 'New {{podcastTitle}} Episode!', @@ -31,6 +32,7 @@ module.exports.notificationData = { name: 'onBackupCompleted', requiresLibrary: false, description: 'Triggered when a backup is completed', + descriptionKey: 'NotificationOnBackupCompletedDescription', variables: ['completionTime', 'backupPath', 'backupSize', 'backupCount', 'removedOldest'], defaults: { title: 'Backup Completed', @@ -48,6 +50,7 @@ module.exports.notificationData = { name: 'onBackupFailed', requiresLibrary: false, description: 'Triggered when a backup fails', + descriptionKey: 'NotificationOnBackupFailedDescription', variables: ['errorMsg'], defaults: { title: 'Backup Failed', @@ -61,6 +64,7 @@ module.exports.notificationData = { name: 'onTest', requiresLibrary: false, description: 'Event for testing the notification system', + descriptionKey: 'NotificationOnTestDescription', variables: ['version'], defaults: { title: 'Test Notification on Abs {{version}}', From 1cac42aec5a8002980e9e1f2acf6521b98051f57 Mon Sep 17 00:00:00 2001 From: advplyr Date: Sat, 12 Oct 2024 15:32:51 -0500 Subject: [PATCH 20/44] Add localization on logs page and confirm embed #3495 --- client/pages/audiobook/_id/manage.vue | 2 +- client/pages/config/log.vue | 4 ++-- client/strings/en-us.json | 2 ++ 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/client/pages/audiobook/_id/manage.vue b/client/pages/audiobook/_id/manage.vue index fb5ccc29..56d78d1c 100644 --- a/client/pages/audiobook/_id/manage.vue +++ b/client/pages/audiobook/_id/manage.vue @@ -370,7 +370,7 @@ export default { }, embedClick() { const payload = { - message: `Are you sure you want to embed metadata in ${this.audioFiles.length} audio files?`, + message: this.$getString('MessageConfirmEmbedMetadataInAudioFiles', [this.audioFiles.length]), callback: (confirmed) => { if (confirmed) { this.updateAudioFileMetadata() diff --git a/client/pages/config/log.vue b/client/pages/config/log.vue index 41df00e2..4015a9f6 100644 --- a/client/pages/config/log.vue +++ b/client/pages/config/log.vue @@ -10,9 +10,9 @@
- + - +
diff --git a/client/strings/en-us.json b/client/strings/en-us.json index edad1672..adfe1001 100644 --- a/client/strings/en-us.json +++ b/client/strings/en-us.json @@ -515,6 +515,7 @@ "LabelSeries": "Series", "LabelSeriesName": "Series Name", "LabelSeriesProgress": "Series Progress", + "LabelServerLogLevel": "Server Log Level", "LabelServerYearReview": "Server Year in Review ({0})", "LabelSetEbookAsPrimary": "Set as primary", "LabelSetEbookAsSupplementary": "Set as supplementary", @@ -685,6 +686,7 @@ "MessageConfirmDeleteMetadataProvider": "Are you sure you want to delete custom metadata provider \"{0}\"?", "MessageConfirmDeleteNotification": "Are you sure you want to delete this notification?", "MessageConfirmDeleteSession": "Are you sure you want to delete this session?", + "MessageConfirmEmbedMetadataInAudioFiles": "Are you sure you want to embed metadata in {0} audio files?", "MessageConfirmForceReScan": "Are you sure you want to force re-scan?", "MessageConfirmMarkAllEpisodesFinished": "Are you sure you want to mark all episodes as finished?", "MessageConfirmMarkAllEpisodesNotFinished": "Are you sure you want to mark all episodes as not finished?", From ddd2c0ae4e596159dc7f391ef792749865074197 Mon Sep 17 00:00:00 2001 From: advplyr Date: Sat, 12 Oct 2024 15:56:49 -0500 Subject: [PATCH 21/44] Add:Filter for missing chapters & alphabetize missing subitems #3497 --- .../controls/LibraryFilterSelect.vue | 32 +++++++++++-------- .../utils/queries/libraryItemsBookFilters.js | 2 +- 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/client/components/controls/LibraryFilterSelect.vue b/client/components/controls/LibraryFilterSelect.vue index ba96f103..2d9ced5a 100644 --- a/client/components/controls/LibraryFilterSelect.vue +++ b/client/components/controls/LibraryFilterSelect.vue @@ -413,21 +413,17 @@ export default { id: 'isbn', name: 'ISBN' }, - { - id: 'subtitle', - name: this.$strings.LabelSubtitle - }, { id: 'authors', name: this.$strings.LabelAuthor }, { - id: 'publishedYear', - name: this.$strings.LabelPublishYear + id: 'chapters', + name: this.$strings.LabelChapters }, { - id: 'series', - name: this.$strings.LabelSeries + id: 'cover', + name: this.$strings.LabelCover }, { id: 'description', @@ -438,24 +434,32 @@ export default { name: this.$strings.LabelGenres }, { - id: 'tags', - name: this.$strings.LabelTags + id: 'language', + name: this.$strings.LabelLanguage }, { id: 'narrators', name: this.$strings.LabelNarrator }, + { + id: 'publishedYear', + name: this.$strings.LabelPublishYear + }, { id: 'publisher', name: this.$strings.LabelPublisher }, { - id: 'language', - name: this.$strings.LabelLanguage + id: 'series', + name: this.$strings.LabelSeries }, { - id: 'cover', - name: this.$strings.LabelCover + id: 'subtitle', + name: this.$strings.LabelSubtitle + }, + { + id: 'tags', + name: this.$strings.LabelTags } ] }, diff --git a/server/utils/queries/libraryItemsBookFilters.js b/server/utils/queries/libraryItemsBookFilters.js index 43ffafef..e64e7b78 100644 --- a/server/utils/queries/libraryItemsBookFilters.js +++ b/server/utils/queries/libraryItemsBookFilters.js @@ -219,7 +219,7 @@ module.exports = { mediaWhere[key] = { [Sequelize.Op.or]: [null, ''] } - } else if (['genres', 'tags', 'narrators'].includes(value)) { + } else if (['genres', 'tags', 'narrators', 'chapters'].includes(value)) { mediaWhere[value] = { [Sequelize.Op.or]: [null, Sequelize.where(Sequelize.fn('json_array_length', Sequelize.col(value)), 0)] } From 576eb9106f49704261ee56f20a74b67f48c3ba36 Mon Sep 17 00:00:00 2001 From: "K. J" Date: Sat, 5 Oct 2024 08:33:50 +0000 Subject: [PATCH 22/44] Translated using Weblate (German) Currently translated at 100.0% (990 of 990 strings) Translation: Audiobookshelf/Abs Web Client Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/de/ --- client/strings/de.json | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/client/strings/de.json b/client/strings/de.json index 25c34c7e..d6d03b18 100644 --- a/client/strings/de.json +++ b/client/strings/de.json @@ -791,17 +791,24 @@ "MessageTaskFailedToMergeAudioFiles": "Fehler beim zusammenführen der Audiodateien", "MessageTaskFailedToMoveM4bFile": "Fehler beim verschieben der m4b Datei", "MessageTaskFailedToWriteMetadataFile": "Fehler beim schreiben der Metadaten-Datei", + "MessageTaskMatchingBooksInLibrary": "Vergleiche Bücher in Bibliothek \"{0}\"", "MessageTaskNoFilesToScan": "Keine Dateien zum scannen", + "MessageTaskOpmlImport": "OPML-Import", "MessageTaskOpmlImportDescription": "Podcasts von {0} RSS-Feeds werden ersrtellt", + "MessageTaskOpmlImportFeed": "OPML-Feed importieren", "MessageTaskOpmlImportFeedDescription": "RSS-Feed \"{0}\" wird importiert", "MessageTaskOpmlImportFeedFailed": "Podcast Feed konnte nicht geladen werden", "MessageTaskOpmlImportFeedPodcastDescription": "Podcast \"{0}\" wird erstellt", + "MessageTaskOpmlImportFeedPodcastExists": "Der Podcast ist bereits im Pfad vorhanden", "MessageTaskOpmlImportFeedPodcastFailed": "Erstellen des Podcasts fehlgeschlagen", "MessageTaskOpmlImportFinished": "{0} Podcasts hinzugefügt", "MessageTaskScanItemsAdded": "{0} hinzugefügt", "MessageTaskScanItemsMissing": "{0} fehlend", "MessageTaskScanItemsUpdated": "{0} aktualisiert", "MessageTaskScanNoChangesNeeded": "Keine Änderungen nötig", + "MessageTaskScanningFileChanges": "Überprüfe \"{0}\" nach geänderten Dateien", + "MessageTaskScanningLibrary": "Bibliothek \"{0}\" wird durchsucht", + "MessageTaskTargetDirectoryNotWritable": "Das Zielverzeichnis ist schreibgeschützt", "MessageThinking": "Nachdenken...", "MessageUploaderItemFailed": "Hochladen fehlgeschlagen", "MessageUploaderItemSuccess": "Erfolgreich hochgeladen!", @@ -894,6 +901,7 @@ "ToastErrorCannotShare": "Das kann nicht nativ auf diesem Gerät freigegeben werden", "ToastFailedToLoadData": "Daten laden fehlgeschlagen", "ToastFailedToShare": "Fehler beim Teilen", + "ToastFailedToUpdate": "Aktualisierung ist fehlgeschlagen", "ToastInvalidImageUrl": "Ungültiger Bild URL", "ToastInvalidUrl": "Ungültiger URL", "ToastItemCoverUpdateSuccess": "Titelbild aktualisiert", From 08acfdcd24f92500a5a225d33cf3055e2a996c8d Mon Sep 17 00:00:00 2001 From: Charlie Date: Sat, 5 Oct 2024 20:39:07 +0000 Subject: [PATCH 23/44] Translated using Weblate (French) Currently translated at 100.0% (990 of 990 strings) Translation: Audiobookshelf/Abs Web Client Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/fr/ --- client/strings/fr.json | 1 + 1 file changed, 1 insertion(+) diff --git a/client/strings/fr.json b/client/strings/fr.json index 3c5e4002..4dc4ea05 100644 --- a/client/strings/fr.json +++ b/client/strings/fr.json @@ -901,6 +901,7 @@ "ToastErrorCannotShare": "Impossible de partager nativement sur cet appareil", "ToastFailedToLoadData": "Échec du chargement des données", "ToastFailedToShare": "Échec du partage", + "ToastFailedToUpdate": "Échec de la mise à jour", "ToastInvalidImageUrl": "URL de l'image invalide", "ToastInvalidUrl": "URL invalide", "ToastItemCoverUpdateSuccess": "Couverture mise à jour", From a9fb6eb8bcb7eebe8ded5f1b17b662f24712677e Mon Sep 17 00:00:00 2001 From: gallegonovato Date: Sun, 6 Oct 2024 23:39:00 +0000 Subject: [PATCH 24/44] Translated using Weblate (Spanish) Currently translated at 100.0% (991 of 991 strings) Translation: Audiobookshelf/Abs Web Client Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/es/ --- client/strings/es.json | 1 + 1 file changed, 1 insertion(+) diff --git a/client/strings/es.json b/client/strings/es.json index 2510d809..ff4aa186 100644 --- a/client/strings/es.json +++ b/client/strings/es.json @@ -920,6 +920,7 @@ "ToastLibraryScanFailedToStart": "Error al iniciar el escaneo", "ToastLibraryScanStarted": "Se inició el escaneo de la biblioteca", "ToastLibraryUpdateSuccess": "Biblioteca \"{0}\" actualizada", + "ToastMatchAllAuthorsFailed": "No se pudo hacer coincidir todos los autores", "ToastNameEmailRequired": "Nombre y correo electrónico obligatorios", "ToastNameRequired": "Nombre obligatorio", "ToastNewUserCreatedFailed": "Error al crear la cuenta: \"{0}\"", From 88e087d50f2047dacf5d68309ed1ad80709c3ec5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20K=C3=BCnzel?= Date: Mon, 7 Oct 2024 13:56:02 +0000 Subject: [PATCH 25/44] Translated using Weblate (German) Currently translated at 99.8% (990 of 991 strings) Translation: Audiobookshelf/Abs Web Client Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/de/ --- client/strings/de.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/strings/de.json b/client/strings/de.json index d6d03b18..202e1b56 100644 --- a/client/strings/de.json +++ b/client/strings/de.json @@ -567,7 +567,7 @@ "LabelStatsMinutesListening": "Gehörte Minuten", "LabelStatsOverallDays": "Gesamte Tage", "LabelStatsOverallHours": "Gesamte Stunden", - "LabelStatsWeekListening": "Wochenhördauer", + "LabelStatsWeekListening": "7-Tage-Durchschnitt", "LabelSubtitle": "Untertitel", "LabelSupportedFileTypes": "Unterstützte Dateitypen", "LabelTag": "Schlagwort", From 0c43f3d15ad9aa141db6f62dccf8aa0c836aa3bd Mon Sep 17 00:00:00 2001 From: DiamondtipDR Date: Mon, 7 Oct 2024 15:47:23 +0000 Subject: [PATCH 26/44] Translated using Weblate (Spanish) Currently translated at 100.0% (991 of 991 strings) Translation: Audiobookshelf/Abs Web Client Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/es/ --- client/strings/es.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/strings/es.json b/client/strings/es.json index ff4aa186..9fc82b05 100644 --- a/client/strings/es.json +++ b/client/strings/es.json @@ -921,8 +921,8 @@ "ToastLibraryScanStarted": "Se inició el escaneo de la biblioteca", "ToastLibraryUpdateSuccess": "Biblioteca \"{0}\" actualizada", "ToastMatchAllAuthorsFailed": "No se pudo hacer coincidir todos los autores", - "ToastNameEmailRequired": "Nombre y correo electrónico obligatorios", - "ToastNameRequired": "Nombre obligatorio", + "ToastNameEmailRequired": "Nombre y correo electrónico son obligatorios", + "ToastNameRequired": "Nombre es requerido", "ToastNewUserCreatedFailed": "Error al crear la cuenta: \"{0}\"", "ToastNewUserCreatedSuccess": "Nueva cuenta creada", "ToastNewUserLibraryError": "Debes seleccionar al menos una biblioteca", From 83fcb0efdc2e2f70543f43a71a1a46b4f9eab1e5 Mon Sep 17 00:00:00 2001 From: thehijacker Date: Mon, 7 Oct 2024 14:39:23 +0000 Subject: [PATCH 27/44] Translated using Weblate (Slovenian) Currently translated at 100.0% (991 of 991 strings) Translation: Audiobookshelf/Abs Web Client Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sl/ --- client/strings/sl.json | 1 + 1 file changed, 1 insertion(+) diff --git a/client/strings/sl.json b/client/strings/sl.json index 28655c52..0e653f76 100644 --- a/client/strings/sl.json +++ b/client/strings/sl.json @@ -920,6 +920,7 @@ "ToastLibraryScanFailedToStart": "Pregleda ni bilo mogoče začeti", "ToastLibraryScanStarted": "Pregled knjižnice se je začel", "ToastLibraryUpdateSuccess": "Knjižnica \"{0}\" je bila posodobljena", + "ToastMatchAllAuthorsFailed": "Ujemanje vseh avtorjev ni bilo uspešno", "ToastNameEmailRequired": "Ime in e-pošta sta obvezna", "ToastNameRequired": "Ime je obvezno", "ToastNewUserCreatedFailed": "Računa ni bilo mogoče ustvariti: \"{0}\"", From fdf871af1745345960b09af2596071dd4d106856 Mon Sep 17 00:00:00 2001 From: Soaibuzzaman Date: Mon, 7 Oct 2024 18:45:54 +0000 Subject: [PATCH 28/44] Translated using Weblate (Bengali) Currently translated at 99.8% (990 of 991 strings) Translation: Audiobookshelf/Abs Web Client Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/bn/ --- client/strings/bn.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/client/strings/bn.json b/client/strings/bn.json index a76b4046..b705a802 100644 --- a/client/strings/bn.json +++ b/client/strings/bn.json @@ -550,7 +550,7 @@ "LabelSleepTimer": "স্লিপ টাইমার", "LabelSlug": "স্লাগ", "LabelStart": "শুরু", - "LabelStartTime": "শুরু করার সময়", + "LabelStartTime": "শুরুর সময়", "LabelStarted": "শুরু হয়েছে", "LabelStartedAt": "এতে শুরু হয়েছে", "LabelStatsAudioTracks": "অডিও ট্র্যাক", @@ -901,6 +901,7 @@ "ToastErrorCannotShare": "এই ডিভাইসে স্থানীয়ভাবে শেয়ার করা যাবে না", "ToastFailedToLoadData": "ডেটা লোড করা যায়নি", "ToastFailedToShare": "শেয়ার করতে ব্যর্থ", + "ToastFailedToUpdate": "আপডেট করতে ব্যর্থ হয়েছে", "ToastInvalidImageUrl": "অকার্যকর ছবির ইউআরএল", "ToastInvalidUrl": "অকার্যকর ইউআরএল", "ToastItemCoverUpdateSuccess": "আইটেম কভার আপডেট করা হয়েছে", From abc378954ce34f433824aff98e2c4166bcda0290 Mon Sep 17 00:00:00 2001 From: Daniel Schosser Date: Mon, 7 Oct 2024 17:58:55 +0000 Subject: [PATCH 29/44] Translated using Weblate (German) Currently translated at 100.0% (991 of 991 strings) Translation: Audiobookshelf/Abs Web Client Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/de/ --- client/strings/de.json | 1 + 1 file changed, 1 insertion(+) diff --git a/client/strings/de.json b/client/strings/de.json index 202e1b56..5a4729b3 100644 --- a/client/strings/de.json +++ b/client/strings/de.json @@ -920,6 +920,7 @@ "ToastLibraryScanFailedToStart": "Scan konnte nicht gestartet werden", "ToastLibraryScanStarted": "Bibliotheksscan gestartet", "ToastLibraryUpdateSuccess": "Bibliothek \"{0}\" aktualisiert", + "ToastMatchAllAuthorsFailed": "Nicht alle Autoren konnten zugeordnet werden", "ToastNameEmailRequired": "Name und E-Mail sind erforderlich", "ToastNameRequired": "Name ist erforderlich", "ToastNewUserCreatedFailed": "Fehler beim erstellen des Accounts: \"{ 0}\"", From 47b1d2a2c2ccdaf1187d97de666b5d8a058c1a8b Mon Sep 17 00:00:00 2001 From: gallegonovato Date: Mon, 7 Oct 2024 16:23:11 +0000 Subject: [PATCH 30/44] Translated using Weblate (Spanish) Currently translated at 100.0% (991 of 991 strings) Translation: Audiobookshelf/Abs Web Client Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/es/ --- client/strings/es.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/client/strings/es.json b/client/strings/es.json index 9fc82b05..112f16d5 100644 --- a/client/strings/es.json +++ b/client/strings/es.json @@ -920,9 +920,9 @@ "ToastLibraryScanFailedToStart": "Error al iniciar el escaneo", "ToastLibraryScanStarted": "Se inició el escaneo de la biblioteca", "ToastLibraryUpdateSuccess": "Biblioteca \"{0}\" actualizada", - "ToastMatchAllAuthorsFailed": "No se pudo hacer coincidir todos los autores", - "ToastNameEmailRequired": "Nombre y correo electrónico son obligatorios", - "ToastNameRequired": "Nombre es requerido", + "ToastMatchAllAuthorsFailed": "No coincide con todos los autores", + "ToastNameEmailRequired": "Son obligatorios el nombre y el correo electrónico", + "ToastNameRequired": "Nombre obligatorio", "ToastNewUserCreatedFailed": "Error al crear la cuenta: \"{0}\"", "ToastNewUserCreatedSuccess": "Nueva cuenta creada", "ToastNewUserLibraryError": "Debes seleccionar al menos una biblioteca", From cba547083d897813aa8484685e4d1aa25ea59939 Mon Sep 17 00:00:00 2001 From: biuklija Date: Mon, 7 Oct 2024 17:37:09 +0000 Subject: [PATCH 31/44] Translated using Weblate (Croatian) Currently translated at 100.0% (991 of 991 strings) Translation: Audiobookshelf/Abs Web Client Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/hr/ --- client/strings/hr.json | 1 + 1 file changed, 1 insertion(+) diff --git a/client/strings/hr.json b/client/strings/hr.json index 9d0ed0d5..622b82ea 100644 --- a/client/strings/hr.json +++ b/client/strings/hr.json @@ -920,6 +920,7 @@ "ToastLibraryScanFailedToStart": "Skeniranje nije uspjelo", "ToastLibraryScanStarted": "Skeniranje knjižnice započelo", "ToastLibraryUpdateSuccess": "Knjižnica \"{0}\" ažurirana", + "ToastMatchAllAuthorsFailed": "Nisu prepoznati svi autori", "ToastNameEmailRequired": "Ime i adresa e-pošte su obavezni", "ToastNameRequired": "Ime je obavezno", "ToastNewUserCreatedFailed": "Račun \"{0}\" nije uspješno izrađen", From d7a543e1436f06927fb95c82c401e50c285f8b06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petras=20=C5=A0ukys?= Date: Tue, 8 Oct 2024 07:03:05 +0000 Subject: [PATCH 32/44] Translated using Weblate (Lithuanian) Currently translated at 71.1% (705 of 991 strings) Translation: Audiobookshelf/Abs Web Client Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/lt/ --- client/strings/lt.json | 35 ++++++++++++++++++++++++++++++++--- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/client/strings/lt.json b/client/strings/lt.json index 6cc7966c..9fe65e3a 100644 --- a/client/strings/lt.json +++ b/client/strings/lt.json @@ -19,6 +19,7 @@ "ButtonChooseFiles": "Pasirinkite failus", "ButtonClearFilter": "Valyti filtrą", "ButtonCloseFeed": "Uždaryti srautą", + "ButtonCloseSession": "Uždaryti Atidarytą sesiją", "ButtonCollections": "Kolekcijos", "ButtonConfigureScanner": "Konfigūruoti skenerį", "ButtonCreate": "Kurti", @@ -28,11 +29,14 @@ "ButtonEdit": "Redaguoti", "ButtonEditChapters": "Redaguoti skyrius", "ButtonEditPodcast": "Redaguoti tinklalaidę", + "ButtonEnable": "Įjungti", "ButtonForceReScan": "Priverstinai nuskaityti iš naujo", "ButtonFullPath": "Visas kelias", "ButtonHide": "Slėpti", "ButtonHome": "Pradžia", "ButtonIssues": "Problemos", + "ButtonJumpBackward": "Peršokti atgal", + "ButtonJumpForward": "Peršokti į priekį", "ButtonLatest": "Naujausias", "ButtonLibrary": "Biblioteka", "ButtonLogout": "Atsijungti", @@ -42,12 +46,19 @@ "ButtonMatchAllAuthors": "Pritaikyti visus autorius", "ButtonMatchBooks": "Pritaikyti knygas", "ButtonNevermind": "Nesvarbu", + "ButtonNext": "Kitas", "ButtonNextChapter": "Kitas Skyrius", + "ButtonNextItemInQueue": "Kitas eilėje", + "ButtonOk": "Ok", "ButtonOpenFeed": "Atidaryti srautą", "ButtonOpenManager": "Atidaryti tvarkyklę", + "ButtonPause": "Pauzė", "ButtonPlay": "Groti", + "ButtonPlayAll": "Groti Visus", "ButtonPlaying": "Grojama", "ButtonPlaylists": "Grojaraščiai", + "ButtonPrevious": "Praeitas", + "ButtonPreviousChapter": "Praeitas Skyrius", "ButtonPurgeAllCache": "Valyti visą saugyklą", "ButtonPurgeItemsCache": "Valyti elementų saugyklą", "ButtonQueueAddItem": "Pridėti į eilę", @@ -55,6 +66,9 @@ "ButtonQuickMatch": "Greitas pritaikymas", "ButtonReScan": "Iš naujo nuskaityti", "ButtonRead": "Skaityti", + "ButtonReadLess": "Mažiau", + "ButtonReadMore": "Daugiau", + "ButtonRefresh": "Atnaujinti", "ButtonRemove": "Pašalinti", "ButtonRemoveAll": "Pašalinti viską", "ButtonRemoveAllLibraryItems": "Pašalinti visus bibliotekos elementus", @@ -72,12 +86,15 @@ "ButtonSelectFolderPath": "Pasirinkti aplanko kelią", "ButtonSeries": "Serijos", "ButtonSetChaptersFromTracks": "Nustatyti skyrius iš takelių", + "ButtonShare": "Dalintis", "ButtonShiftTimes": "Perstumti laikus", "ButtonShow": "Rodyti", "ButtonStartM4BEncode": "Pradėti M4B kodavimą", "ButtonStartMetadataEmbed": "Pradėti metaduomenų įterpimą", + "ButtonStats": "Statistika", "ButtonSubmit": "Pateikti", "ButtonTest": "Testuoti", + "ButtonUnlinkOpenId": "Atsieti OpenID", "ButtonUpload": "Įkelti", "ButtonUploadBackup": "Įkelti atsarginę kopiją", "ButtonUploadCover": "Įkelti viršelį", @@ -86,11 +103,15 @@ "ButtonUserEdit": "Redaguoti naudotoją {0}", "ButtonViewAll": "Peržiūrėti visus", "ButtonYes": "Taip", + "ErrorUploadFetchMetadataAPI": "Klaida gaunant metaduomenis", + "ErrorUploadFetchMetadataNoResults": "Nepavyko gauti metaduomenų - pabandykite atnaujinti pavadinimą ir/ar autorių.", + "ErrorUploadLacksTitle": "Pavadinimas yra privalomas", "HeaderAccount": "Paskyra", "HeaderAdvanced": "Papildomi", "HeaderAppriseNotificationSettings": "Apprise pranešimo nustatymai", "HeaderAudioTracks": "Garso takeliai", "HeaderAudiobookTools": "Audioknygų failų valdymo įrankiai", + "HeaderAuthentication": "Autentifikacija", "HeaderBackups": "Atsarginės kopijos", "HeaderChangePassword": "Pakeisti slaptažodį", "HeaderChapters": "Skyriai", @@ -99,6 +120,7 @@ "HeaderCollectionItems": "Kolekcijos elementai", "HeaderCover": "Viršelis", "HeaderCurrentDownloads": "Dabartiniai parsisiuntimai", + "HeaderCustomMessageOnLogin": "Pritaikyta prisijungimo žinutė", "HeaderDetails": "Detalės", "HeaderDownloadQueue": "Parsisiuntimo eilė", "HeaderEbookFiles": "Eknygos failai", @@ -189,7 +211,7 @@ "LabelBackToUser": "Grįžti į naudotoją", "LabelBackupsEnableAutomaticBackups": "Įjungti automatinį atsarginių kopijų kūrimą", "LabelBackupsEnableAutomaticBackupsHelp": "Atsarginės kopijos bus išsaugotos /metadata/backups aplanke", - "LabelBackupsMaxBackupSize": "Maksimalus atsarginių kopijų dydis (GB)", + "LabelBackupsMaxBackupSize": "Maksimalus atsarginių kopijų dydis (GB) (0 - neribotai)", "LabelBackupsMaxBackupSizeHelp": "Jei konfigūruotas dydis viršijamas, atsarginės kopijos nebus sukurtos, kad būtų išvengta klaidingų konfigūracijų.", "LabelBackupsNumberToKeep": "Laikytinų atsarginių kopijų skaičius", "LabelBackupsNumberToKeepHelp": "Tik viena atsarginė kopija bus pašalinta vienu metu, todėl jei jau turite daugiau atsarginių kopijų nei nurodyta, turite jas pašalinti rankiniu būdu.", @@ -397,7 +419,7 @@ "LabelSettingsExperimentalFeatures": "Eksperimentiniai funkcionalumai", "LabelSettingsExperimentalFeaturesHelp": "Funkcijos, kurios yra kuriamos ir laukiami jūsų komentarai. Spustelėkite, kad atidarytumėte „GitHub“ diskusiją.", "LabelSettingsFindCovers": "Rasti viršelius", - "LabelSettingsFindCoversHelp": "Jei jūsų audioknyga neturi įterpto viršelio arba viršelio paveikslėlio aplanko, skeneris bandys rasti viršelį.
Pastaba: Tai padidins skenavimo trukmę.", + "LabelSettingsFindCoversHelp": "Jei jūsų audioknyga neturi įterpto viršelio arba viršelio paveikslėlio aplanke, bandyti rasti viršelį.
Pastaba: Tai padidins skenavimo trukmę.", "LabelSettingsHideSingleBookSeries": "Slėpti serijas, turinčias tik vieną knygą", "LabelSettingsHideSingleBookSeriesHelp": "Serijos, turinčios tik vieną knygą, bus paslėptos nuo serijų puslapio ir pagrindinio puslapio lentynų.", "LabelSettingsHomePageBookshelfView": "Naudoti pagrindinio puslapio knygų lentynų vaizdą", @@ -413,7 +435,7 @@ "LabelSettingsSquareBookCovers": "Naudoti kvadratinius knygos viršelius", "LabelSettingsSquareBookCoversHelp": "Naudoti kvadratinius viršelius vietoj standartinių 1.6:1 knygų viršelių", "LabelSettingsStoreCoversWithItem": "Saugoti viršelius su elementu", - "LabelSettingsStoreCoversWithItemHelp": "Pagal nutylėjimą viršeliai saugomi /metadata/items aplanke, įjungus šią parinktį viršeliai bus saugomi jūsų bibliotekos elemento aplanke. Bus išsaugotas tik vienas „cover“ pavadinimo failas.", + "LabelSettingsStoreCoversWithItemHelp": "Pagal nutylėjimą viršeliai saugomi /metadata/items aplanke, įjungus šią parinktį viršeliai bus saugomi jūsų bibliotekos elemento aplanke. Bus išsaugotas tik vienas failas su \"cover\" pavadinimu.", "LabelSettingsStoreMetadataWithItem": "Saugoti metaduomenis su elementu", "LabelSettingsStoreMetadataWithItemHelp": "Pagal nutylėjimą metaduomenų failai saugomi /metadata/items aplanke, įjungus šią parinktį metaduomenų failai bus saugomi jūsų bibliotekos elemento aplanke", "LabelSettingsTimeFormat": "Laiko formatas", @@ -642,10 +664,17 @@ "ToastBookmarkUpdateSuccess": "Žyma atnaujinta", "ToastChaptersHaveErrors": "Skyriai turi klaidų", "ToastChaptersMustHaveTitles": "Skyriai turi turėti pavadinimus", + "ToastChaptersRemoved": "Skyriai pašalinti", + "ToastCollectionItemsAddFailed": "Nepavyko pridėti į kolekciją", + "ToastCollectionItemsAddSuccess": "Pridėta į kolekciją", "ToastCollectionItemsRemoveSuccess": "Elementai pašalinti iš kolekcijos", "ToastCollectionRemoveSuccess": "Kolekcija pašalinta", "ToastCollectionUpdateSuccess": "Kolekcija atnaujinta", + "ToastCoverUpdateFailed": "Viršelio atnaujinimas nepavyko", + "ToastDeviceTestEmailSuccess": "Bandomasis el. laiškas išsiųstas", "ToastItemCoverUpdateSuccess": "Elemento viršelis atnaujintas", + "ToastItemDeletedFailed": "Nepavyko ištrinti", + "ToastItemDeletedSuccess": "Ištrinta", "ToastItemDetailsUpdateSuccess": "Elemento detalės atnaujintos", "ToastItemMarkedAsFinishedFailed": "Pažymėti kaip Baigta nepavyko", "ToastItemMarkedAsFinishedSuccess": "Elementas pažymėtas kaip Baigta", From ff81d70cb1b5b3badb760e31de6e98af4f42798d Mon Sep 17 00:00:00 2001 From: thehijacker Date: Tue, 8 Oct 2024 06:52:31 +0000 Subject: [PATCH 33/44] Translated using Weblate (Slovenian) Currently translated at 100.0% (991 of 991 strings) Translation: Audiobookshelf/Abs Web Client Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sl/ --- client/strings/sl.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/strings/sl.json b/client/strings/sl.json index 0e653f76..58a38102 100644 --- a/client/strings/sl.json +++ b/client/strings/sl.json @@ -640,7 +640,7 @@ "LabelYourPlaylists": "Tvoje seznami predvajanj", "LabelYourProgress": "Tvoj napredek", "MessageAddToPlayerQueue": "Dodaj v čakalno vrsto predvajalnika", - "MessageAppriseDescription": "Če želite uporabljati to funkcijo, morate imeti zagnan primerek API Apprise ali API, ki bo obravnaval te iste zahteve.
Url API-ja Apprise mora biti celotna pot URL-ja za pošiljanje obvestila, npr. če je vaš primerek API-ja postrežen na http://192.168.1.1:8337, bi morali vnesti http://192.168.1.1:8337/notify.", + "MessageAppriseDescription": "Če želite uporabljati to funkcijo, morate imeti zagnano namestitev API Apprise ali API, ki bo obravnavala te iste zahteve.
Url API-ja Apprise mora biti celotna pot URL-ja za pošiljanje obvestila, npr. če je vaša namestitev API-ja postrežena na http://192.168.1.1:8337, bi morali vnesti http://192.168.1.1:8337/notify.", "MessageBackupsDescription": "Varnostne kopije vključujejo uporabnike, napredek uporabnikov, podrobnosti elementov knjižnice, nastavitve strežnika in slike, shranjene v /metadata/items & /metadata/authors. Varnostne kopije ne vključujejo datotek, shranjenih v mapah vaše knjižnice.", "MessageBackupsLocationEditNote": "Opomba: Posodabljanje lokacije varnostne kopije ne bo premaknilo ali spremenilo obstoječih varnostnih kopij", "MessageBackupsLocationNoEditNote": "Opomba: Lokacija varnostne kopije je nastavljena s spremenljivko okolja in je tu ni mogoče spremeniti.", From c4963d0de8f1ddf61f1972b812579102c8c534b3 Mon Sep 17 00:00:00 2001 From: thehijacker Date: Wed, 9 Oct 2024 06:50:21 +0000 Subject: [PATCH 34/44] Translated using Weblate (Slovenian) Currently translated at 100.0% (993 of 993 strings) Translation: Audiobookshelf/Abs Web Client Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sl/ --- client/strings/sl.json | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/client/strings/sl.json b/client/strings/sl.json index 58a38102..39bc7c55 100644 --- a/client/strings/sl.json +++ b/client/strings/sl.json @@ -463,10 +463,12 @@ "LabelProvider": "Ponudnik", "LabelProviderAuthorizationValue": "Vrednost glave avtorizacije", "LabelPubDate": "Datum objave", - "LabelPublishYear": "Leto objave", - "LabelPublishedDate": "Objavljeno {0}", - "LabelPublisher": "Založnik", - "LabelPublishers": "Založniki", + "LabelPublishYear": "Leto izdaje", + "LabelPublishedDate": "Izdano {0}", + "LabelPublishedDecade": "Desetletje izdaje", + "LabelPublishedDecades": "Desetletja izdaje", + "LabelPublisher": "Izdajatelj", + "LabelPublishers": "Izdajatelji", "LabelRSSFeedCustomOwnerEmail": "E-pošta lastnika po meri", "LabelRSSFeedCustomOwnerName": "Ime lastnika po meri", "LabelRSSFeedOpen": "Odprt vir RSS", @@ -610,7 +612,7 @@ "LabelUnabridged": "Neskrajšano", "LabelUndo": "Razveljavi", "LabelUnknown": "Neznano", - "LabelUnknownPublishDate": "Neznan datum objave", + "LabelUnknownPublishDate": "Neznan datum izdaje", "LabelUpdateCover": "Posodobi naslovnico", "LabelUpdateCoverHelp": "Dovoli prepisovanje obstoječih naslovnic za izbrane knjige, ko se najde ujemanje", "LabelUpdateDetails": "Posodobi podrobnosti", From 44e82fc45436b453c0a0b8b044f190af3d2a9d52 Mon Sep 17 00:00:00 2001 From: thehijacker Date: Thu, 10 Oct 2024 05:26:02 +0000 Subject: [PATCH 35/44] Translated using Weblate (Slovenian) Currently translated at 100.0% (993 of 993 strings) Translation: Audiobookshelf/Abs Web Client Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sl/ --- client/strings/sl.json | 66 +++++++++++++++++++++--------------------- 1 file changed, 33 insertions(+), 33 deletions(-) diff --git a/client/strings/sl.json b/client/strings/sl.json index 39bc7c55..68676313 100644 --- a/client/strings/sl.json +++ b/client/strings/sl.json @@ -134,7 +134,7 @@ "HeaderEmail": "E-pošta", "HeaderEmailSettings": "Nastavitve e-pošte", "HeaderEpisodes": "Epizode", - "HeaderEreaderDevices": "Ebralne naprave", + "HeaderEreaderDevices": "E-bralniki", "HeaderEreaderSettings": "Nastavitve ebralnika", "HeaderFiles": "Datoteke", "HeaderFindChapters": "Najdi poglavja", @@ -146,7 +146,7 @@ "HeaderLibraries": "Knjižnice", "HeaderLibraryFiles": "Datoteke knjižnice", "HeaderLibraryStats": "Statistika knjižnice", - "HeaderListeningSessions": "Seje poslušanja", + "HeaderListeningSessions": "Sej poslušanja", "HeaderListeningStats": "Statistika poslušanja", "HeaderLogin": "Prijava", "HeaderLogs": "Dnevniki", @@ -161,10 +161,10 @@ "HeaderNotificationCreate": "Ustvari obvestilo", "HeaderNotificationUpdate": "Posodobi obvestilo", "HeaderNotifications": "Obvestila", - "HeaderOpenIDConnectAuthentication": "Preverjanje pristnosti OpenID Connect", + "HeaderOpenIDConnectAuthentication": "Prijava z OpenID Connect", "HeaderOpenRSSFeed": "Odpri vir RSS", "HeaderOtherFiles": "Ostale datoteke", - "HeaderPasswordAuthentication": "Preverjanje pristnosti gesla", + "HeaderPasswordAuthentication": "Preverjanje pristnosti z geslom", "HeaderPermissions": "Dovoljenja", "HeaderPlayerQueue": "Čakalna vrsta predvajalnika", "HeaderPlayerSettings": "Nastavitve predvajalnika", @@ -186,7 +186,7 @@ "HeaderSettingsDisplay": "Zaslon", "HeaderSettingsExperimental": "Eksperimentalne funkcije", "HeaderSettingsGeneral": "Splošno", - "HeaderSettingsScanner": "Skener", + "HeaderSettingsScanner": "Pregledovalnik", "HeaderSleepTimer": "Časovnik za izklop", "HeaderStatsLargestItems": "Največji elementi", "HeaderStatsLongestItems": "Najdaljši elementi (ure)", @@ -219,7 +219,7 @@ "LabelAddedAt": "Dodano ob", "LabelAddedDate": "Dodano {0}", "LabelAdminUsersOnly": "Samo administratorji", - "LabelAll": "Vsi", + "LabelAll": "Vse", "LabelAllUsers": "Vsi uporabniki", "LabelAllUsersExcludingGuests": "Vsi uporabniki razen gosti", "LabelAllUsersIncludingGuests": "Vsi uporabniki vključno z gosti", @@ -245,7 +245,7 @@ "LabelBackupsNumberToKeep": "Število varnostnih kopij, ki jih je treba hraniti", "LabelBackupsNumberToKeepHelp": "Naenkrat bo odstranjena samo ena varnostna kopija, če že imate več varnostnih kopij, jih odstranite ročno.", "LabelBitrate": "Bitna hitrost", - "LabelBooks": "Knjige", + "LabelBooks": "knjig", "LabelButtonText": "Besedilo gumba", "LabelByAuthor": "od {0}", "LabelChangePassword": "Spremeni geslo", @@ -400,8 +400,8 @@ "LabelMinute": "Minuta", "LabelMinutes": "Minute", "LabelMissing": "Manjkajoče", - "LabelMissingEbook": "Nima nobene eknjige", - "LabelMissingSupplementaryEbook": "Nima nobene dodatne eknjige", + "LabelMissingEbook": "Nima nobene e-knjige", + "LabelMissingSupplementaryEbook": "Nima nobene dodatne e-knjige", "LabelMobileRedirectURIs": "Dovoljeni mobilni preusmeritveni URI-ji", "LabelMobileRedirectURIsDescription": "To je seznam dovoljenih veljavnih preusmeritvenih URI-jev za mobilne aplikacije. Privzeti je audiobookshelf://oauth, ki ga lahko odstranite ali dopolnite z dodatnimi URI-ji za integracijo aplikacij tretjih oseb. Uporaba zvezdice (*) kot edinega vnosa dovoljuje kateri koli URI.", "LabelMore": "Več", @@ -509,11 +509,11 @@ "LabelSettingsBookshelfViewHelp": "Skeuomorfna oblika z lesenimi policami", "LabelSettingsChromecastSupport": "Podpora za Chromecast", "LabelSettingsDateFormat": "Oblika datuma", - "LabelSettingsDisableWatcher": "Onemogoči pregledovalca", - "LabelSettingsDisableWatcherForLibrary": "Onemogoči pregledovalca map za knjižnico", + "LabelSettingsDisableWatcher": "Onemogoči spremljanje datotečnega sistema", + "LabelSettingsDisableWatcherForLibrary": "Onemogoči spremljanje map za knjižnico", "LabelSettingsDisableWatcherHelp": "Onemogoči samodejno dodajanje/posodabljanje elementov, ko so zaznane spremembe datoteke. *Potreben je ponovni zagon strežnika", - "LabelSettingsEnableWatcher": "Omogoči pregledovalca", - "LabelSettingsEnableWatcherForLibrary": "Omogoči pregledovalca map za knjižnico", + "LabelSettingsEnableWatcher": "Omogoči spremljanje sprememb", + "LabelSettingsEnableWatcherForLibrary": "Omogoči spremljanje sprememb v mapi knjižnice", "LabelSettingsEnableWatcherHelp": "Omogoča samodejno dodajanje/posodabljanje elementov, ko so zaznane spremembe datoteke. *Potreben je ponovni zagon strežnika", "LabelSettingsEpubsAllowScriptedContent": "Dovoli skriptirano vsebino v epubih", "LabelSettingsEpubsAllowScriptedContentHelp": "Dovoli datotekam epub izvajanje skript. Priporočljivo je, da to nastavitev pustite onemogočeno, razen če zaupate viru datotek epub.", @@ -528,12 +528,12 @@ "LabelSettingsOnlyShowLaterBooksInContinueSeries": "Preskoči prejšnje knjige v nadaljevanju serije", "LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "Polica z domačo stranjo Nadaljuj serijo prikazuje prvo nezačeto knjigo v seriji, ki ima vsaj eno dokončano knjigo in ni nobene knjige v teku. Če omogočite to nastavitev, se bo serija nadaljevala od najbolj dokončane knjige namesto od prve nezačete knjige.", "LabelSettingsParseSubtitles": "Uporabi podnapise", - "LabelSettingsParseSubtitlesHelp": "Izvleci podnapise iz imen map zvočnih knjig.
Podnaslov mora biti ločen z \" - \"
npr. »Naslov knjige – Tu podnapis« ima podnaslov »Tu podnapis«", + "LabelSettingsParseSubtitlesHelp": "Izvleci podnapise iz imen map zvočnih knjig.
Podnapis mora biti ločen z \" - \"
npr. \"Naslov knjige – tu podnapis\" ima podnapis \"tu podnapis\"", "LabelSettingsPreferMatchedMetadata": "Prednost imajo ujemajoči se metapodatki", "LabelSettingsPreferMatchedMetadataHelp": "Pri uporabi hitrega ujemanja bodo ujemajoči se podatki preglasili podrobnosti artikla. Hitro ujemanje bo privzeto izpolnil samo manjkajoče podrobnosti.", "LabelSettingsSkipMatchingBooksWithASIN": "Preskoči ujemajoče se knjige, ki že imajo ASIN", "LabelSettingsSkipMatchingBooksWithISBN": "Preskoči ujemajoče se knjige, ki že imajo oznako ISBN", - "LabelSettingsSortingIgnorePrefixes": "Pri razvrščanju ne upoštevajte predpon", + "LabelSettingsSortingIgnorePrefixes": "Pri razvrščanju ne upoštevaj predpon", "LabelSettingsSortingIgnorePrefixesHelp": "npr. za naslov knjige s predpono \"the\" bi se \"The Book Title\" razvrstil kot \"Book Title, The\"", "LabelSettingsSquareBookCovers": "Uporabi kvadratne platnice knjig", "LabelSettingsSquareBookCoversHelp": "Raje uporabi kvadratne platnice kot standardne knjižne platnice 1.6:1", @@ -560,15 +560,15 @@ "LabelStatsBestDay": "Najboljši dan", "LabelStatsDailyAverage": "Dnevno povprečje", "LabelStatsDays": "Dnevi", - "LabelStatsDaysListened": "Poslušani dnevi", + "LabelStatsDaysListened": "Dnevi poslušanja", "LabelStatsHours": "Ure", "LabelStatsInARow": "v vrsti", "LabelStatsItemsFinished": "Končani elementi", "LabelStatsItemsInLibrary": "Elementi v knjižnici", "LabelStatsMinutes": "minute", - "LabelStatsMinutesListening": "Poslušane minute", + "LabelStatsMinutesListening": "Minut poslušanja", "LabelStatsOverallDays": "Skupaj dnevi", - "LabelStatsOverallHours": "Skupaj ure", + "LabelStatsOverallHours": "Skupaj ur", "LabelStatsWeekListening": "Tednov poslušanja", "LabelSubtitle": "Podnapis", "LabelSupportedFileTypes": "Podprte vrste datotek", @@ -596,8 +596,8 @@ "LabelTitle": "Naslov", "LabelToolsEmbedMetadata": "Vdelaj metapodatke", "LabelToolsEmbedMetadataDescription": "Vdelajte metapodatke v zvočne datoteke, vključno s sliko naslovnice in poglavji.", - "LabelToolsMakeM4b": "Ustvari datoteko zvočne knjige M4B", - "LabelToolsMakeM4bDescription": "Ustvarite datoteko zvočne knjige .M4B z vdelanimi metapodatki, sliko naslovnice in poglavji.", + "LabelToolsMakeM4b": "Ustvari M4B datoteko zvočne knjige", + "LabelToolsMakeM4bDescription": "Ustvari zvočno knjigo v .M4B obliki z vdelanimi metapodatki, sliko naslovnice in poglavji.", "LabelToolsSplitM4b": "Razdeli M4B v MP3 datoteke", "LabelToolsSplitM4bDescription": "Ustvarite MP3 datoteke iz datoteke M4B, razdeljene po poglavjih z vdelanimi metapodatki, naslovno sliko in poglavji.", "LabelTotalDuration": "Skupno trajanje", @@ -653,9 +653,9 @@ "MessageBookshelfNoResultsForFilter": "Ni rezultatov za filter \"{0}: {1}\"", "MessageBookshelfNoResultsForQuery": "Ni rezultatov za poizvedbo", "MessageBookshelfNoSeries": "Nimate serij", - "MessageChapterEndIsAfter": "Konec poglavja je za koncem vaše zvočne knjige", + "MessageChapterEndIsAfter": "Konec poglavja je po koncu zvočne knjige", "MessageChapterErrorFirstNotZero": "Prvo poglavje se mora začeti pri 0", - "MessageChapterErrorStartGteDuration": "Neveljaven začetni čas mora biti krajši od trajanja zvočne knjige", + "MessageChapterErrorStartGteDuration": "Neveljaven začetni čas, mora biti krajši od trajanja zvočne knjige", "MessageChapterErrorStartLtPrev": "Neveljaven začetni čas mora biti večji od ali enak začetnemu času prejšnjega poglavja", "MessageChapterStartIsAfter": "Začetek poglavja je po koncu vaše zvočne knjige", "MessageCheckingCron": "Preverjam cron...", @@ -669,7 +669,7 @@ "MessageConfirmDeleteMetadataProvider": "Ali ste prepričani, da želite izbrisati ponudnika metapodatkov po meri \"{0}\"?", "MessageConfirmDeleteNotification": "Ali ste prepričani, da želite izbrisati to obvestilo?", "MessageConfirmDeleteSession": "Ali ste prepričani, da želite izbrisati to sejo?", - "MessageConfirmForceReScan": "Ali ste prepričani, da želite vsiliti ponovno iskanje?", + "MessageConfirmForceReScan": "Ali ste prepričani, da želite vsiliti ponovno pregledovanje?", "MessageConfirmMarkAllEpisodesFinished": "Ali ste prepričani, da želite označiti vse epizode kot dokončane?", "MessageConfirmMarkAllEpisodesNotFinished": "Ali ste prepričani, da želite vse epizode označiti kot nedokončane?", "MessageConfirmMarkItemFinished": "Ali ste prepričani, da želite \"{0}\" označiti kot dokončanega?", @@ -680,7 +680,7 @@ "MessageConfirmPurgeCache": "Čiščenje predpomnilnika bo izbrisalo celoten imenik v /metadata/cache.

Ali ste prepričani, da želite odstraniti imenik predpomnilnika?", "MessageConfirmPurgeItemsCache": "Čiščenje predpomnilnika elementov bo izbrisalo celoten imenik na /metadata/cache/items.
Ste prepričani?", "MessageConfirmQuickEmbed": "Opozorilo! Hitra vdelava ne bo varnostno kopirala vaših zvočnih datotek. Prepričajte se, da imate varnostno kopijo zvočnih datotek.

Ali želite nadaljevati?", - "MessageConfirmReScanLibraryItems": "Ali ste prepričani, da želite ponovno poiskati {0} elementov?", + "MessageConfirmReScanLibraryItems": "Ali ste prepričani, da želite ponovno pregledati {0} elementov?", "MessageConfirmRemoveAllChapters": "Ali ste prepričani, da želite odstraniti vsa poglavja?", "MessageConfirmRemoveAuthor": "Ali ste prepričani, da želite odstraniti avtorja \"{0}\"?", "MessageConfirmRemoveCollection": "Ali ste prepričani, da želite odstraniti zbirko \"{0}\"?", @@ -706,7 +706,7 @@ "MessageEreaderDevices": "Da zagotovite dostavo e-knjig, boste morda morali dodati zgornji e-poštni naslov kot veljavnega pošiljatelja za vsako spodaj navedeno napravo.", "MessageFeedURLWillBe": "URL vira bo {0}", "MessageFetching": "Pridobivam...", - "MessageForceReScanDescription": "bo znova pregledal vse datoteke kot nov pregled. Oznake ID3 zvočnih datotek, datoteke OPF in besedilne datoteke bodo pregledane kot nove.", + "MessageForceReScanDescription": "bo znova pregledal vse datoteke kot pregled od začetka. Oznake ID3 zvočnih datotek, datoteke OPF in besedilne datoteke bodo pregledane kot nove.", "MessageImportantNotice": "Pomembno obvestilo!", "MessageInsertChapterBelow": "Spodaj vstavite poglavje", "MessageItemsSelected": "{0} izbranih elementov", @@ -718,12 +718,12 @@ "MessageLogsDescription": "Dnevniki so shranjeni v /metadata/logs kot datoteke JSON. Dnevniki zrušitev so shranjeni v /metadata/logs/crash_logs.txt.", "MessageM4BFailed": "M4B ni uspel!", "MessageM4BFinished": "M4B končan!", - "MessageMapChapterTitles": "Preslikajte naslove poglavij v obstoječa poglavja zvočne knjige brez prilagajanja časovnih žigov", + "MessageMapChapterTitles": "Preslikaj naslove poglavij v obstoječa poglavja zvočne knjige brez prilagajanja časovnih indentifikatorjev", "MessageMarkAllEpisodesFinished": "Označi vse epizode kot končane", "MessageMarkAllEpisodesNotFinished": "Označi vse epizode kot nedokončane", "MessageMarkAsFinished": "Označi kot dokončano", "MessageMarkAsNotFinished": "Označi kot nedokončano", - "MessageMatchBooksDescription": "bo poskušal povezati knjige v knjižnici s knjigo izbranega ponudnika iskanja in izpolniti prazne podatke in naslovnico. Ne prepisujejo se pa podrobnosti.", + "MessageMatchBooksDescription": "bo poskušal povezati knjige v knjižnici s knjigo izbranega ponudnika iskanja in izpolniti prazne podatke in naslovnico. Ne prepisuje čez obstoječe podatke.", "MessageNoAudioTracks": "Ni zvočnih posnetkov", "MessageNoAuthors": "Brez avtorjev", "MessageNoBackups": "Brez varnostnih kopij", @@ -793,7 +793,7 @@ "MessageTaskFailedToMergeAudioFiles": "Zvočnih datotek ni bilo mogoče združiti", "MessageTaskFailedToMoveM4bFile": "Datoteke m4b ni bilo mogoče premakniti", "MessageTaskFailedToWriteMetadataFile": "Metapodatke ni bilo mogoče zapisati v datoteke", - "MessageTaskMatchingBooksInLibrary": "Ujemam knjige v knjižnici \"{0}\"", + "MessageTaskMatchingBooksInLibrary": "Prepoznavam knjige v knjižnici \"{0}\"", "MessageTaskNoFilesToScan": "Ni datotek za pregledovanje", "MessageTaskOpmlImport": "Uvoz OPML", "MessageTaskOpmlImportDescription": "Ustvarjanje podcastov iz {0} virov RSS", @@ -809,14 +809,14 @@ "MessageTaskScanItemsUpdated": "{0} posodobljeno", "MessageTaskScanNoChangesNeeded": "Spremembe niso potrebne", "MessageTaskScanningFileChanges": "Pregledovanje sprememb v datoteki \"{0}\"", - "MessageTaskScanningLibrary": "Pregled knjižnice \"{0}\"", + "MessageTaskScanningLibrary": "Pregledujem knjižnico \"{0}\"", "MessageTaskTargetDirectoryNotWritable": "Ciljni imenik ni zapisljiv", "MessageThinking": "Razmišljam...", "MessageUploaderItemFailed": "Nalaganje ni uspelo", "MessageUploaderItemSuccess": "Uspešno naloženo!", "MessageUploading": "Nalaganje...", "MessageValidCronExpression": "Veljaven cron izraz", - "MessageWatcherIsDisabledGlobally": "Pregledovalec je globalno onemogočen v nastavitvah strežnika", + "MessageWatcherIsDisabledGlobally": "Spremljanje sprememb datotek je globalno onemogočeno v nastavitvah strežnika", "MessageXLibraryIsEmpty": "{0} Knjižnica je prazna!", "MessageYourAudiobookDurationIsLonger": "Trajanje vaše zvočne knjige je daljše od ugotovljenega trajanja", "MessageYourAudiobookDurationIsShorter": "Trajanje vaše zvočne knjige je krajše od ugotovljenega trajanja", @@ -836,11 +836,11 @@ "StatsAuthorsAdded": "dodanih avtorjev", "StatsBooksAdded": "dodanih knjig", "StatsBooksAdditional": "Nekateri dodatki vključujejo…", - "StatsBooksFinished": "končane knjige", + "StatsBooksFinished": "končanih knjig", "StatsBooksFinishedThisYear": "Nekaj knjig, ki so bile dokončane letos…", - "StatsBooksListenedTo": "poslušane knjige", + "StatsBooksListenedTo": "poslušanih knjig", "StatsCollectionGrewTo": "Vaša zbirka knjig se je povečala na …", - "StatsSessions": "seje", + "StatsSessions": "sej", "StatsSpentListening": "porabil za poslušanje", "StatsTopAuthor": "TOP AVTOR", "StatsTopAuthors": "TOP AVTORJI", From 465775bd552646ed95ed03ac73d3092e966b8a59 Mon Sep 17 00:00:00 2001 From: biuklija Date: Thu, 10 Oct 2024 19:50:45 +0000 Subject: [PATCH 36/44] Translated using Weblate (Croatian) Currently translated at 100.0% (993 of 993 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, 3 insertions(+), 1 deletion(-) diff --git a/client/strings/hr.json b/client/strings/hr.json index 622b82ea..1ae07981 100644 --- a/client/strings/hr.json +++ b/client/strings/hr.json @@ -463,8 +463,10 @@ "LabelProvider": "Dobavljač", "LabelProviderAuthorizationValue": "Vrijednost autorizacijskog zaglavlja", "LabelPubDate": "Datum izdavanja", - "LabelPublishYear": "Godina izdavanja", + "LabelPublishYear": "Godina objavljivanja", "LabelPublishedDate": "Objavljeno {0}", + "LabelPublishedDecade": "Desetljeće objavljivanja", + "LabelPublishedDecades": "Desetljeća objavljivanja", "LabelPublisher": "Izdavač", "LabelPublishers": "Izdavači", "LabelRSSFeedCustomOwnerEmail": "Prilagođena adresa e-pošte vlasnika", From 2b6fb46cdb7121ba86b26fab25305f4bef9ba147 Mon Sep 17 00:00:00 2001 From: gallegonovato Date: Fri, 11 Oct 2024 12:45:49 +0000 Subject: [PATCH 37/44] Translated using Weblate (Spanish) Currently translated at 100.0% (993 of 993 strings) Translation: Audiobookshelf/Abs Web Client Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/es/ --- client/strings/es.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/client/strings/es.json b/client/strings/es.json index 112f16d5..cd9621bf 100644 --- a/client/strings/es.json +++ b/client/strings/es.json @@ -465,6 +465,8 @@ "LabelPubDate": "Fecha de publicación", "LabelPublishYear": "Año de publicación", "LabelPublishedDate": "Publicado {0}", + "LabelPublishedDecade": "Una década de publicaciones", + "LabelPublishedDecades": "Décadas publicadas", "LabelPublisher": "Editor", "LabelPublishers": "Editores", "LabelRSSFeedCustomOwnerEmail": "Correo electrónico de dueño personalizado", From 347e3ff674c2e7c6b77ec237d80547a3c5e1064a Mon Sep 17 00:00:00 2001 From: Mathias Franco Date: Fri, 11 Oct 2024 15:20:58 +0000 Subject: [PATCH 38/44] Translated using Weblate (Dutch) Currently translated at 67.6% (672 of 993 strings) Translation: Audiobookshelf/Abs Web Client Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/nl/ --- client/strings/nl.json | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/client/strings/nl.json b/client/strings/nl.json index cd2c872c..06a1ffa8 100644 --- a/client/strings/nl.json +++ b/client/strings/nl.json @@ -31,6 +31,7 @@ "ButtonForceReScan": "Forceer nieuwe scan", "ButtonFullPath": "Volledig pad", "ButtonHide": "Verberg", + "ButtonHome": "Thuis", "ButtonIssues": "Problemen", "ButtonJumpBackward": "Spring achteruit", "ButtonJumpForward": "Spring vooruit", @@ -76,6 +77,7 @@ "ButtonScanLibrary": "Scan bibliotheek", "ButtonSearch": "Zoeken", "ButtonSelectFolderPath": "Maplocatie selecteren", + "ButtonSeries": "Series", "ButtonSetChaptersFromTracks": "Maak hoofdstukken op basis van tracks", "ButtonShare": "Deel", "ButtonShiftTimes": "Tijden verschuiven", @@ -93,6 +95,7 @@ "ErrorUploadFetchMetadataAPI": "Error metadata ophalen", "ErrorUploadFetchMetadataNoResults": "Kan metadata niet ophalen - probeer de titel en/of auteur te updaten", "ErrorUploadLacksTitle": "Moet een titel hebben", + "HeaderAccount": "Account", "HeaderAdvanced": "Geavanceerd", "HeaderAppriseNotificationSettings": "Apprise-notificatie instellingen", "HeaderAudioTracks": "Audiotracks", @@ -105,6 +108,7 @@ "HeaderCollectionItems": "Collectie-objecten", "HeaderCover": "Omslag", "HeaderCurrentDownloads": "Huidige downloads", + "HeaderDetails": "Details", "HeaderDownloadQueue": "Download-wachtrij", "HeaderEbookFiles": "Ebook bestanden", "HeaderEmail": "E-mail", @@ -207,8 +211,8 @@ "LabelCollections": "Collecties", "LabelComplete": "Compleet", "LabelConfirmPassword": "Bevestig wachtwoord", - "LabelContinueListening": "Verder luisteren", - "LabelContinueReading": "Verder luisteren", + "LabelContinueListening": "Verder Luisteren", + "LabelContinueReading": "Verder lezen", "LabelContinueSeries": "Ga verder met serie", "LabelCoverImageURL": "Coverafbeelding URL", "LabelCreatedAt": "Gecreëerd op", From ba9595a1bea6008a0299f4de70004d6d169ab080 Mon Sep 17 00:00:00 2001 From: "J. Lavoie" Date: Fri, 11 Oct 2024 22:31:13 +0000 Subject: [PATCH 39/44] Translated using Weblate (German) Currently translated at 100.0% (993 of 993 strings) Translation: Audiobookshelf/Abs Web Client Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/de/ --- client/strings/de.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/client/strings/de.json b/client/strings/de.json index 5a4729b3..e8ca3f59 100644 --- a/client/strings/de.json +++ b/client/strings/de.json @@ -465,6 +465,8 @@ "LabelPubDate": "Veröffentlichungsdatum", "LabelPublishYear": "Jahr", "LabelPublishedDate": "Veröffentlicht {0}", + "LabelPublishedDecade": "Jahrzehnt", + "LabelPublishedDecades": "Jahrzehnte", "LabelPublisher": "Herausgeber", "LabelPublishers": "Herausgeber", "LabelRSSFeedCustomOwnerEmail": "Benutzerdefinierte Eigentümer-E-Mail", From 158cdeed5720b371bd9553e1b953487fb5d21e54 Mon Sep 17 00:00:00 2001 From: "J. Lavoie" Date: Fri, 11 Oct 2024 22:29:02 +0000 Subject: [PATCH 40/44] Translated using Weblate (French) Currently translated at 100.0% (993 of 993 strings) Translation: Audiobookshelf/Abs Web Client Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/fr/ --- client/strings/fr.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/client/strings/fr.json b/client/strings/fr.json index 4dc4ea05..064597b3 100644 --- a/client/strings/fr.json +++ b/client/strings/fr.json @@ -465,6 +465,8 @@ "LabelPubDate": "Date de publication", "LabelPublishYear": "Année de publication", "LabelPublishedDate": "Publié en {0}", + "LabelPublishedDecade": "Décennie de publication", + "LabelPublishedDecades": "Décennies de publication", "LabelPublisher": "Éditeur", "LabelPublishers": "Éditeurs", "LabelRSSFeedCustomOwnerEmail": "Courriel personnalisée du propriétaire", @@ -920,6 +922,7 @@ "ToastLibraryScanFailedToStart": "Échec du démarrage de l’analyse", "ToastLibraryScanStarted": "Analyse de la bibliothèque démarrée", "ToastLibraryUpdateSuccess": "Bibliothèque « {0} » mise à jour", + "ToastMatchAllAuthorsFailed": "Tous les auteurs et autrices n’ont pas pu être classés", "ToastNameEmailRequired": "Le nom et le courriel sont requis", "ToastNameRequired": "Le nom est requis", "ToastNewUserCreatedFailed": "La création du compte à échouée : « {0} »", From 70273ba2ba8a332644b18715c6763b80965c6d8b Mon Sep 17 00:00:00 2001 From: "J. Lavoie" Date: Fri, 11 Oct 2024 22:33:28 +0000 Subject: [PATCH 41/44] Translated using Weblate (Italian) Currently translated at 99.7% (991 of 993 strings) Translation: Audiobookshelf/Abs Web Client Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/it/ --- client/strings/it.json | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/client/strings/it.json b/client/strings/it.json index 3cffc1eb..3078706a 100644 --- a/client/strings/it.json +++ b/client/strings/it.json @@ -465,6 +465,8 @@ "LabelPubDate": "Data di pubblicazione", "LabelPublishYear": "Anno di pubblicazione", "LabelPublishedDate": "{0} pubblicati", + "LabelPublishedDecade": "Decennio di pubblicazione", + "LabelPublishedDecades": "Decenni di pubblicazione", "LabelPublisher": "Editore", "LabelPublishers": "Editori", "LabelRSSFeedCustomOwnerEmail": "E-mail del proprietario personalizzato", @@ -777,6 +779,38 @@ "MessageShareExpiresIn": "Scade in {0}", "MessageShareURLWillBe": "L'indirizzo sarà: {0}", "MessageStartPlaybackAtTime": "Avvia la riproduzione per \"{0}\" a {1}?", + "MessageTaskAudioFileNotWritable": "Il file audio «{0}» non è scrivibile", + "MessageTaskCanceledByUser": "Attività annullata dall'utente", + "MessageTaskDownloadingEpisodeDescription": "Scaricamento dell'episodio «{0}»", + "MessageTaskEmbeddingMetadata": "Metadati integrati", + "MessageTaskEmbeddingMetadataDescription": "Integrazione dei metadati nell'audiolibro «{0}»", + "MessageTaskEncodingM4b": "Codifica M4B", + "MessageTaskEncodingM4bDescription": "Codifica dell'audiolibro «{0}» in un singolo file m4b", + "MessageTaskFailed": "Fallimento", + "MessageTaskFailedToBackupAudioFile": "Non riuscita a eseguire il backup del file audio «{0}»", + "MessageTaskFailedToCreateCacheDirectory": "Non riuscita a creare la cartella della cache", + "MessageTaskFailedToEmbedMetadataInFile": "Non ha inserito i metadati nel file «{0}»", + "MessageTaskFailedToMergeAudioFiles": "Non è riuscito a fondere i file audio", + "MessageTaskFailedToMoveM4bFile": "Non è riuscito a spostare il file m4b", + "MessageTaskFailedToWriteMetadataFile": "Non è riuscito a scrivere file di metadati", + "MessageTaskMatchingBooksInLibrary": "Libri di corrispondenza in biblioteca «{0}»", + "MessageTaskNoFilesToScan": "Nessun file per la scansione", + "MessageTaskOpmlImport": "Importazione OPML", + "MessageTaskOpmlImportDescription": "Creazione di podcast da {0} flusso RSS", + "MessageTaskOpmlImportFeed": "Flusso di importazione OPML", + "MessageTaskOpmlImportFeedDescription": "Importazione del flusso RSS «{0}»", + "MessageTaskOpmlImportFeedFailed": "Impossibile ottenere il flusso del podcast", + "MessageTaskOpmlImportFeedPodcastDescription": "Creazione di podcast «{0}»", + "MessageTaskOpmlImportFeedPodcastExists": "Il podcast esiste già nel percorso", + "MessageTaskOpmlImportFeedPodcastFailed": "Errore durante la creazione del podcast", + "MessageTaskOpmlImportFinished": "{0} podcast aggiunti", + "MessageTaskScanItemsAdded": "{0} aggiunti", + "MessageTaskScanItemsMissing": "{0} mancanti", + "MessageTaskScanItemsUpdated": "{0} aggiornati", + "MessageTaskScanNoChangesNeeded": "Nessuna modifica necessaria", + "MessageTaskScanningFileChanges": "Cambiamenti di file di scansione in «{0}»", + "MessageTaskScanningLibrary": "Scansione della biblioteca «{0}»", + "MessageTaskTargetDirectoryNotWritable": "La cartella di destinazione non è scrivibile", "MessageThinking": "Elaborazione...", "MessageUploaderItemFailed": "Caricamento Fallito", "MessageUploaderItemSuccess": "Caricato con successo!", @@ -869,6 +903,7 @@ "ToastErrorCannotShare": "Impossibile condividere in modo nativo su questo dispositivo", "ToastFailedToLoadData": "Impossibile caricare i dati", "ToastFailedToShare": "Impossibile condividere", + "ToastFailedToUpdate": "Non aggiornato", "ToastInvalidImageUrl": "URL dell'immagine non valido", "ToastInvalidUrl": "URL non valido", "ToastItemCoverUpdateSuccess": "Cover aggiornata", @@ -887,6 +922,7 @@ "ToastLibraryScanFailedToStart": "Errore inizio scansione", "ToastLibraryScanStarted": "Scansione Libreria iniziata", "ToastLibraryUpdateSuccess": "Libreria \"{0}\" aggiornata", + "ToastMatchAllAuthorsFailed": "Tutti gli autori non hanno potuto essere classificati", "ToastNameEmailRequired": "Nome ed email sono obbligatori", "ToastNameRequired": "Il nome è obbligatorio", "ToastNewUserCreatedFailed": "Impossibile creare l'account: \"{0}\"", From 926a85fff079dd7223efe0ee550257228aec2c61 Mon Sep 17 00:00:00 2001 From: Languages add-on Date: Sat, 12 Oct 2024 06:46:10 +0200 Subject: [PATCH 42/44] Added translation using Weblate (English (United States)) --- client/strings/en_US.json | 1 + 1 file changed, 1 insertion(+) create mode 100644 client/strings/en_US.json diff --git a/client/strings/en_US.json b/client/strings/en_US.json new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/client/strings/en_US.json @@ -0,0 +1 @@ +{} From 80e0cac4747e61d1fbb5374ec4ac41d3499042e2 Mon Sep 17 00:00:00 2001 From: advplyr Date: Sat, 12 Oct 2024 16:18:45 -0500 Subject: [PATCH 43/44] Version bump v2.15.0 --- 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 4c6df795..073ca445 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -1,12 +1,12 @@ { "name": "audiobookshelf-client", - "version": "2.14.0", + "version": "2.15.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "audiobookshelf-client", - "version": "2.14.0", + "version": "2.15.0", "license": "ISC", "dependencies": { "@nuxtjs/axios": "^5.13.6", diff --git a/client/package.json b/client/package.json index ab70a30b..626895e0 100644 --- a/client/package.json +++ b/client/package.json @@ -1,6 +1,6 @@ { "name": "audiobookshelf-client", - "version": "2.14.0", + "version": "2.15.0", "buildNumber": 1, "description": "Self-hosted audiobook and podcast client", "main": "index.js", diff --git a/package-lock.json b/package-lock.json index 7c1798ed..a428e6f7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "audiobookshelf", - "version": "2.14.0", + "version": "2.15.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "audiobookshelf", - "version": "2.14.0", + "version": "2.15.0", "license": "GPL-3.0", "dependencies": { "axios": "^0.27.2", diff --git a/package.json b/package.json index 69bc41dd..7e8a4a9a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "audiobookshelf", - "version": "2.14.0", + "version": "2.15.0", "buildNumber": 1, "description": "Self-hosted audiobook and podcast server", "main": "index.js", From f2ac24e62329753ad0e64ff3fa9ddf6538fc0898 Mon Sep 17 00:00:00 2001 From: mikiher Date: Sun, 13 Oct 2024 10:56:38 +0300 Subject: [PATCH 44/44] Fix next/previous chapter behavior on public share player --- client/pages/share/_slug.vue | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/client/pages/share/_slug.vue b/client/pages/share/_slug.vue index 384a9513..f4f93b1d 100644 --- a/client/pages/share/_slug.vue +++ b/client/pages/share/_slug.vue @@ -10,7 +10,7 @@

{{ mediaItemShare.playbackSession.displayAuthor }}

- +
@@ -51,7 +51,8 @@ export default { windowHeight: 0, listeningTimeSinceSync: 0, coverRgb: null, - coverBgIsLight: false + coverBgIsLight: false, + currentTime: 0 } }, computed: { @@ -83,6 +84,9 @@ export default { chapters() { return this.playbackSession.chapters || [] }, + currentChapter() { + return this.chapters.find((chapter) => chapter.start <= this.currentTime && this.currentTime < chapter.end) + }, coverAspectRatio() { const coverAspectRatio = this.playbackSession.coverAspectRatio return coverAspectRatio === this.$constants.BookCoverAspectRatio.STANDARD ? 1.6 : 1 @@ -154,6 +158,7 @@ export default { // Update UI this.$refs.audioPlayer.setCurrentTime(time) + this.currentTime = time }, setDuration() { if (!this.localAudioPlayer) return