From 679bdf36b117a59b4db0cd8022818acc5d17f5b7 Mon Sep 17 00:00:00 2001 From: Jorge <46056498+jorgectf@users.noreply.github.com> Date: Mon, 3 Jul 2023 09:15:04 +0200 Subject: [PATCH 0001/1695] Add CodeQL workflow --- .github/workflows/codeql.yml | 65 ++++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 .github/workflows/codeql.yml diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 00000000..a77ab3e0 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,65 @@ +name: "CodeQL" + +on: + push: + branches: [ 'master' ] + pull_request: + # The branches below must be a subset of the branches above + branches: [ 'master' ] + schedule: + - cron: '16 5 * * 4' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ 'javascript' ] + # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] + # Use only 'java' to analyze code written in Java, Kotlin or both + # Use only 'javascript' to analyze code written in JavaScript, TypeScript or both + # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v2 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + + # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + # queries: security-extended,security-and-quality + + + # Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v2 + + # ℹ️ Command-line programs to run using the OS shell. + # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun + + # If the Autobuild fails above, remove it and uncomment the following three lines. + # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. + + # - run: | + # echo "Run, Build Application using script" + # ./location_of_script_within_repo/buildscript.sh + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v2 + with: + category: "/language:${{matrix.language}}" From 4e6b75d6506d94c19fad81ae62cbac0c3370fd1e Mon Sep 17 00:00:00 2001 From: jfrazx Date: Thu, 5 Oct 2023 13:48:55 -0700 Subject: [PATCH 0002/1695] fix; HTTP/429 when requesting authors information, resolves #1570 --- package-lock.json | 29 ++++++++- package.json | 3 +- server/providers/Audnexus.js | 117 ++++++++++++++++++++++++----------- 3 files changed, 111 insertions(+), 38 deletions(-) diff --git a/package-lock.json b/package-lock.json index 77948004..a1e7ccd6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "express": "^4.17.1", "graceful-fs": "^4.2.10", "htmlparser2": "^8.0.1", + "limiter": "^2.1.0", "node-tone": "^1.0.1", "nodemailer": "^6.9.2", "sequelize": "^6.32.1", @@ -1308,6 +1309,19 @@ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "optional": true }, + "node_modules/just-performance": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/just-performance/-/just-performance-4.3.0.tgz", + "integrity": "sha512-L7RjvtJsL0QO8xFs5wEoDDzzJwoiowRw6Rn/GnvldlchS2JQr9wFYPiwZcDfrbbujEKqKN0tvENdbjXdYhDp5Q==" + }, + "node_modules/limiter": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/limiter/-/limiter-2.1.0.tgz", + "integrity": "sha512-361TYz6iay6n+9KvUUImqdLuFigK+K79qrUtBsXhJTLdH4rIt/r1y8r1iozwh8KbZNpujbFTSh74mJ7bwbAMOw==", + "dependencies": { + "just-performance": "4.3.0" + } + }, "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", @@ -3673,6 +3687,19 @@ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "optional": true }, + "just-performance": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/just-performance/-/just-performance-4.3.0.tgz", + "integrity": "sha512-L7RjvtJsL0QO8xFs5wEoDDzzJwoiowRw6Rn/GnvldlchS2JQr9wFYPiwZcDfrbbujEKqKN0tvENdbjXdYhDp5Q==" + }, + "limiter": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/limiter/-/limiter-2.1.0.tgz", + "integrity": "sha512-361TYz6iay6n+9KvUUImqdLuFigK+K79qrUtBsXhJTLdH4rIt/r1y8r1iozwh8KbZNpujbFTSh74mJ7bwbAMOw==", + "requires": { + "just-performance": "4.3.0" + } + }, "lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", @@ -4672,4 +4699,4 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" } } -} \ No newline at end of file +} diff --git a/package.json b/package.json index 4a00fa59..082006e8 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "express": "^4.17.1", "graceful-fs": "^4.2.10", "htmlparser2": "^8.0.1", + "limiter": "^2.1.0", "node-tone": "^1.0.1", "nodemailer": "^6.9.2", "sequelize": "^6.32.1", @@ -44,4 +45,4 @@ "devDependencies": { "nodemon": "^2.0.20" } -} \ No newline at end of file +} diff --git a/server/providers/Audnexus.js b/server/providers/Audnexus.js index b74d1d13..06433f5d 100644 --- a/server/providers/Audnexus.js +++ b/server/providers/Audnexus.js @@ -1,78 +1,123 @@ const axios = require('axios') const { levenshteinDistance } = require('../utils/index') const Logger = require('../Logger') +const { RateLimiter } = require('limiter'); class Audnexus { + static _instance = null; + constructor() { + // ensures Audnexus class is singleton + if (Audnexus._instance) { + return Audnexus._instance + } + this.baseUrl = 'https://api.audnex.us' + + // @see https://github.com/laxamentumtech/audnexus#-deployment- + this.limiter = new RateLimiter({ + tokensPerInterval: 100, + fireImmediately: true, + interval: 'minute', + }) + + Audnexus._instance = this } authorASINsRequest(name, region) { name = encodeURIComponent(name) const regionQuery = region ? `®ion=${region}` : '' const authorRequestUrl = `${this.baseUrl}/authors?name=${name}${regionQuery}` + Logger.info(`[Audnexus] Searching for author "${authorRequestUrl}"`) - return axios.get(authorRequestUrl).then((res) => { - return res.data || [] - }).catch((error) => { - Logger.error(`[Audnexus] Author ASIN request failed for ${name}`, error) - return [] - }) + + return this._processRequest(() => axios.get(authorRequestUrl)) + .then((res) => res.data || []) + .catch((error) => { + Logger.error(`[Audnexus] Author ASIN request failed for ${name}`, error) + return [] + }) } authorRequest(asin, region) { asin = encodeURIComponent(asin) const regionQuery = region ? `?region=${region}` : '' const authorRequestUrl = `${this.baseUrl}/authors/${asin}${regionQuery}` + Logger.info(`[Audnexus] Searching for author "${authorRequestUrl}"`) - return axios.get(authorRequestUrl).then((res) => { - return res.data - }).catch((error) => { - Logger.error(`[Audnexus] Author request failed for ${asin}`, error) - return null - }) + + return this._processRequest(() => axios.get(authorRequestUrl)) + .then((res) => res.data) + .catch((error) => { + Logger.error(`[Audnexus] Author request failed for ${asin}`, error) + return null + }) + } + + /** + * @description Process a request with a rate limiter + * + * @param {*} request + * @returns + */ + async _processRequest(request) { + const remainingTokens = await this.limiter.removeTokens(1) + Logger.info(`[Audnexus] Attempting request with ${remainingTokens} remaining tokens and ${this.limiter.tokensThisInterval} this interval`) + + if (remainingTokens >= 1) { + return request() + } + + // 100 tokens(requests) per minute give a refresh of ~1.67 per second, + // so a 10 second wait will yield ~16.7 additional tokens + Logger.info('[Audnexus] Sleeping for 10 seconds') + await new Promise(resolve => setTimeout(resolve, 10000)) + + return this._processRequest(request) } async findAuthorByASIN(asin, region) { const author = await this.authorRequest(asin, region) - if (!author) { - return null - } - return { - asin: author.asin, - description: author.description, - image: author.image || null, - name: author.name - } + + return author ? + { + asin: author.asin, + description: author.description, + image: author.image || null, + name: author.name + } : null } async findAuthorByName(name, region, maxLevenshtein = 3) { Logger.debug(`[Audnexus] Looking up author by name ${name}`) + const asins = await this.authorASINsRequest(name, region) const matchingAsin = asins.find(obj => levenshteinDistance(obj.name, name) <= maxLevenshtein) + if (!matchingAsin) { return null } + const author = await this.authorRequest(matchingAsin.asin) - if (!author) { - return null - } - return { - asin: author.asin, - description: author.description, - image: author.image || null, - name: author.name - } + return author ? + { + description: author.description, + image: author.image || null, + asin: author.asin, + name: author.name + } : null } getChaptersByASIN(asin, region) { Logger.debug(`[Audnexus] Get chapters for ASIN ${asin}/${region}`) - return axios.get(`${this.baseUrl}/books/${asin}/chapters?region=${region}`).then((res) => { - return res.data - }).catch((error) => { - Logger.error(`[Audnexus] Chapter ASIN request failed for ${asin}/${region}`, error) - return null - }) + + return this._processRequest(() => axios.get(`${this.baseUrl}/books/${asin}/chapters?region=${region}`)) + .then((res) => res.data) + .catch((error) => { + Logger.error(`[Audnexus] Chapter ASIN request failed for ${asin}/${region}`, error) + return null + }) } } + module.exports = Audnexus \ No newline at end of file From 73c21242b4a70f0314bbfc62dd2841a38afc4bcb Mon Sep 17 00:00:00 2001 From: jfrazx Date: Mon, 22 Jan 2024 20:36:20 -0800 Subject: [PATCH 0003/1695] feat: utilize p-throttle instad of limiter --- package-lock.json | 26 +++++++++---------- package.json | 2 +- server/providers/Audnexus.js | 48 +++++++++++------------------------- 3 files changed, 28 insertions(+), 48 deletions(-) diff --git a/package-lock.json b/package-lock.json index 65316f65..30b40e28 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,11 +15,11 @@ "express-session": "^1.17.3", "graceful-fs": "^4.2.10", "htmlparser2": "^8.0.1", - "limiter": "^2.1.0", "lru-cache": "^10.0.3", "node-tone": "^1.0.1", "nodemailer": "^6.9.2", "openid-client": "^5.6.1", + "p-throttle": "^4.1.1", "passport": "^0.6.0", "passport-jwt": "^4.0.1", "sequelize": "^6.35.2", @@ -2841,11 +2841,6 @@ "integrity": "sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg==", "dev": true }, - "node_modules/just-performance": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/just-performance/-/just-performance-4.3.0.tgz", - "integrity": "sha512-L7RjvtJsL0QO8xFs5wEoDDzzJwoiowRw6Rn/GnvldlchS2JQr9wFYPiwZcDfrbbujEKqKN0tvENdbjXdYhDp5Q==" - }, "node_modules/jwa": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", @@ -2865,14 +2860,6 @@ "safe-buffer": "^5.0.1" } }, - "node_modules/limiter": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/limiter/-/limiter-2.1.0.tgz", - "integrity": "sha512-361TYz6iay6n+9KvUUImqdLuFigK+K79qrUtBsXhJTLdH4rIt/r1y8r1iozwh8KbZNpujbFTSh74mJ7bwbAMOw==", - "dependencies": { - "just-performance": "4.3.0" - } - }, "node_modules/locate-path": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", @@ -3977,6 +3964,17 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/p-throttle": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/p-throttle/-/p-throttle-4.1.1.tgz", + "integrity": "sha512-TuU8Ato+pRTPJoDzYD4s7ocJYcNSEZRvlxoq3hcPI2kZDZ49IQ1Wkj7/gDJc3X7XiEAAvRGtDzdXJI0tC3IL1g==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/p-try": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", diff --git a/package.json b/package.json index 65dbdd1e..46624752 100644 --- a/package.json +++ b/package.json @@ -41,11 +41,11 @@ "express-session": "^1.17.3", "graceful-fs": "^4.2.10", "htmlparser2": "^8.0.1", - "limiter": "^2.1.0", "lru-cache": "^10.0.3", "node-tone": "^1.0.1", "nodemailer": "^6.9.2", "openid-client": "^5.6.1", + "p-throttle": "^4.1.1", "passport": "^0.6.0", "passport-jwt": "^4.0.1", "sequelize": "^6.35.2", diff --git a/server/providers/Audnexus.js b/server/providers/Audnexus.js index d9223374..14eab4a1 100644 --- a/server/providers/Audnexus.js +++ b/server/providers/Audnexus.js @@ -1,7 +1,7 @@ const axios = require('axios') const { levenshteinDistance } = require('../utils/index') const Logger = require('../Logger') -const { RateLimiter } = require('limiter') +const pThrottle = require('p-throttle') class Audnexus { static _instance = null @@ -14,11 +14,15 @@ class Audnexus { this.baseUrl = 'https://api.audnex.us' + // Rate limit is 100 requests per minute. // @see https://github.com/laxamentumtech/audnexus#-deployment- - this.limiter = new RateLimiter({ - tokensPerInterval: 100, - fireImmediately: true, - interval: 'minute', + this.limiter = pThrottle({ + // Setting the limit to 1 allows for a short pause between requests that is almost imperceptible to + // the end user. A larger limit will grab blocks faster and then wait for the alloted time(interval) before + // fetching another batch. + limit: 1, + strict: true, + interval: 300 }) Audnexus._instance = this @@ -31,8 +35,8 @@ class Audnexus { Logger.info(`[Audnexus] Searching for author "${authorRequestUrl}"`) - return this._processRequest(() => axios.get(authorRequestUrl)) - .then((res) => res.data || []) + const throttle = this.limiter(() => axios.get(authorRequestUrl)) + return throttle().then((res) => res.data || []) .catch((error) => { Logger.error(`[Audnexus] Author ASIN request failed for ${name}`, error) return [] @@ -46,36 +50,14 @@ class Audnexus { Logger.info(`[Audnexus] Searching for author "${authorRequestUrl}"`) - return this._processRequest(() => axios.get(authorRequestUrl)) - .then((res) => res.data) + const throttle = this.limiter(() => axios.get(authorRequestUrl)) + return throttle().then((res) => res.data) .catch((error) => { Logger.error(`[Audnexus] Author request failed for ${asin}`, error) return null }) } - /** - * @description Process a request with a rate limiter - * - * @param {*} request - * @returns - */ - async _processRequest(request) { - const remainingTokens = await this.limiter.removeTokens(1) - Logger.info(`[Audnexus] Attempting request with ${remainingTokens} remaining tokens and ${this.limiter.tokensThisInterval} this interval`) - - if (remainingTokens >= 1) { - return request() - } - - // 100 tokens(requests) per minute give a refresh of ~1.67 per second, - // so a 10 second wait will yield ~16.7 additional tokens - Logger.info('[Audnexus] Sleeping for 10 seconds') - await new Promise(resolve => setTimeout(resolve, 10000)) - - return this._processRequest(request) - } - async findAuthorByASIN(asin, region) { const author = await this.authorRequest(asin, region) @@ -111,8 +93,8 @@ class Audnexus { getChaptersByASIN(asin, region) { Logger.debug(`[Audnexus] Get chapters for ASIN ${asin}/${region}`) - return this._processRequest(() => axios.get(`${this.baseUrl}/books/${asin}/chapters?region=${region}`)) - .then((res) => res.data) + const throttle = this.limiter(() => axios.get(`${this.baseUrl}/books/${asin}/chapters?region=${region}`)) + return throttle().then((res) => res.data) .catch((error) => { Logger.error(`[Audnexus] Chapter ASIN request failed for ${asin}/${region}`, error) return null From 70827727aa8edfff13e8acfd8fe5c486b7f254bd Mon Sep 17 00:00:00 2001 From: jfrazx Date: Mon, 22 Jan 2024 22:19:05 -0800 Subject: [PATCH 0004/1695] feat(429): retry 429 request errors --- server/providers/Audnexus.js | 48 ++++++++++++++++++++++++++---------- 1 file changed, 35 insertions(+), 13 deletions(-) diff --git a/server/providers/Audnexus.js b/server/providers/Audnexus.js index 14eab4a1..9e4dc6b7 100644 --- a/server/providers/Audnexus.js +++ b/server/providers/Audnexus.js @@ -1,7 +1,7 @@ const axios = require('axios') const { levenshteinDistance } = require('../utils/index') const Logger = require('../Logger') -const pThrottle = require('p-throttle') +const Throttle = require('p-throttle') class Audnexus { static _instance = null @@ -16,13 +16,13 @@ class Audnexus { // Rate limit is 100 requests per minute. // @see https://github.com/laxamentumtech/audnexus#-deployment- - this.limiter = pThrottle({ - // Setting the limit to 1 allows for a short pause between requests that is almost imperceptible to - // the end user. A larger limit will grab blocks faster and then wait for the alloted time(interval) before - // fetching another batch. + this.limiter = Throttle({ + // Setting the limit to 1 allows for a short pause between requests that is imperceptible to the end user. + // A larger limit will grab blocks faster and then wait for the alloted time(interval) before + // fetching another batch, but with a discernable pause from the user perspective. limit: 1, strict: true, - interval: 300 + interval: 150 }) Audnexus._instance = this @@ -35,8 +35,8 @@ class Audnexus { Logger.info(`[Audnexus] Searching for author "${authorRequestUrl}"`) - const throttle = this.limiter(() => axios.get(authorRequestUrl)) - return throttle().then((res) => res.data || []) + return this._processRequest(this.limiter(() => axios.get(authorRequestUrl))) + .then((res) => res.data || []) .catch((error) => { Logger.error(`[Audnexus] Author ASIN request failed for ${name}`, error) return [] @@ -50,8 +50,8 @@ class Audnexus { Logger.info(`[Audnexus] Searching for author "${authorRequestUrl}"`) - const throttle = this.limiter(() => axios.get(authorRequestUrl)) - return throttle().then((res) => res.data) + return this._processRequest(this.limiter(() => axios.get(authorRequestUrl))) + .then((res) => res.data) .catch((error) => { Logger.error(`[Audnexus] Author request failed for ${asin}`, error) return null @@ -93,13 +93,35 @@ class Audnexus { getChaptersByASIN(asin, region) { Logger.debug(`[Audnexus] Get chapters for ASIN ${asin}/${region}`) - const throttle = this.limiter(() => axios.get(`${this.baseUrl}/books/${asin}/chapters?region=${region}`)) - return throttle().then((res) => res.data) + return this._processRequest(this.limiter(() => axios.get(`${this.baseUrl}/books/${asin}/chapters?region=${region}`))) + .then((res) => res.data) .catch((error) => { Logger.error(`[Audnexus] Chapter ASIN request failed for ${asin}/${region}`, error) return null }) } + + /** + * Internal method to process requests and retry if rate limit is exceeded. + */ + async _processRequest(request) { + try { + const response = await request() + return response + } catch (error) { + if (error.response?.status === 429) { + const retryAfter = parseInt(error.response.headers?.['retry-after'], 10) || 5 + + Logger.warn(`[Audnexus] Rate limit exceeded. Retrying in ${retryAfter} seconds.`) + await new Promise(resolve => setTimeout(resolve, retryAfter * 1000)) + + return this._processRequest(request) + } + + throw error + } + } } -module.exports = Audnexus \ No newline at end of file +module.exports = Audnexus + From e368ffe29f837ee00787b41012e9a01e9862b970 Mon Sep 17 00:00:00 2001 From: Teekeks Date: Thu, 22 Feb 2024 19:20:49 +0100 Subject: [PATCH 0005/1695] feat(i18n): added missing translatable string in player ui --- client/components/player/PlayerUi.vue | 2 +- client/strings/en-us.json | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/client/components/player/PlayerUi.vue b/client/components/player/PlayerUi.vue index e1b0f96d..bca3e7ee 100644 --- a/client/components/player/PlayerUi.vue +++ b/client/components/player/PlayerUi.vue @@ -53,7 +53,7 @@

- {{ currentChapterName }}  ({{ currentChapterIndex + 1 }} of {{ chapters.length }}) + {{ currentChapterName }}  {{ $setString('LabelPlayerChaperMarker', [currentChapterIndex + 1, chapters.length]) }}

{{ timeRemainingPretty }}

diff --git a/client/strings/en-us.json b/client/strings/en-us.json index e3349d1f..0df642a7 100644 --- a/client/strings/en-us.json +++ b/client/strings/en-us.json @@ -386,6 +386,7 @@ "LabelPermissionsUpdate": "Can Update", "LabelPermissionsUpload": "Can Upload", "LabelPhotoPathURL": "Photo Path/URL", + "LabelPlayerChaperMarker": "({0} of {1})", "LabelPlaylists": "Playlists", "LabelPlayMethod": "Play Method", "LabelPodcast": "Podcast", From 9e3b3f3e129fcabea023d34a048d840d9c229024 Mon Sep 17 00:00:00 2001 From: dor Date: Sat, 16 Mar 2024 19:26:22 +0200 Subject: [PATCH 0006/1695] add Hebrew translation json and Hebrew to i18n.js --- client/plugins/i18n.js | 2 + client/strings/he.json | 781 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 783 insertions(+) create mode 100644 client/strings/he.json diff --git a/client/plugins/i18n.js b/client/plugins/i18n.js index 246ffebd..e35b7e7c 100644 --- a/client/plugins/i18n.js +++ b/client/plugins/i18n.js @@ -12,6 +12,7 @@ const languageCodeMap = { 'es': { label: 'Español', dateFnsLocale: 'es' }, 'et': { label: 'Eesti', dateFnsLocale: 'et' }, 'fr': { label: 'Français', dateFnsLocale: 'fr' }, + 'he': { label: 'עברית', dateFnsLocale: 'he' }, 'hr': { label: 'Hrvatski', dateFnsLocale: 'hr' }, 'it': { label: 'Italiano', dateFnsLocale: 'it' }, 'lt': { label: 'Lietuvių', dateFnsLocale: 'lt' }, @@ -25,6 +26,7 @@ const languageCodeMap = { 'vi-vn': { label: 'Tiếng Việt', dateFnsLocale: 'vi' }, 'zh-cn': { label: '简体中文 (Simplified Chinese)', dateFnsLocale: 'zhCN' }, 'zh-tw': { label: '正體中文 (Traditional Chinese)', dateFnsLocale: 'zhTW' }, + 'he': { label: 'עברית', dateFnsLocale: 'he' } } Vue.prototype.$languageCodeOptions = Object.keys(languageCodeMap).map(code => { return { diff --git a/client/strings/he.json b/client/strings/he.json new file mode 100644 index 00000000..582663c6 --- /dev/null +++ b/client/strings/he.json @@ -0,0 +1,781 @@ +{ + "ButtonAdd": "הוסף", + "ButtonAddChapters": "הוסף פרקים", + "ButtonAddDevice": "הוסף התקן", + "ButtonAddLibrary": "הוסף ספרייה", + "ButtonAddPodcasts": "הוסף פודקאסטים", + "ButtonAddUser": "הוסף משתמש", + "ButtonAddYourFirstLibrary": "הוסף את הספרייה הראשונה שלך", + "ButtonApply": "החל", + "ButtonApplyChapters": "החל פרקים", + "ButtonAuthors": "יוצרים", + "ButtonBrowseForFolder": "עיין בתיקייה", + "ButtonCancel": "בטל", + "ButtonCancelEncode": "בטל הצפנה", + "ButtonChangeRootPassword": "שנה סיסמת שורש", + "ButtonCheckAndDownloadNewEpisodes": "בדוק והורד פרקים חדשים", + "ButtonChooseAFolder": "בחר תיקייה", + "ButtonChooseFiles": "בחר קבצים", + "ButtonClearFilter": "נקה פילטר", + "ButtonCloseFeed": "סגור פיד", + "ButtonCollections": "אוספים", + "ButtonConfigureScanner": "הגדר סורק", + "ButtonCreate": "צור", + "ButtonCreateBackup": "צור גיבוי", + "ButtonDelete": "מחק", + "ButtonDownloadQueue": "תור הורדה", + "ButtonEdit": "ערוך", + "ButtonEditChapters": "ערוך פרקים", + "ButtonEditPodcast": "ערוך פודקאסט", + "ButtonForceReScan": "סרוק מחדש בכוח", + "ButtonFullPath": "נתיב מלא", + "ButtonHide": "הסתר", + "ButtonHome": "בית", + "ButtonIssues": "בעיות", + "ButtonJumpBackward": "דלג אחורה", + "ButtonJumpForward": "דלג קדימה", + "ButtonLatest": "אחרון", + "ButtonLibrary": "ספרייה", + "ButtonLogout": "התנתק", + "ButtonLookup": "חפש", + "ButtonManageTracks": "נהל רצועות", + "ButtonMapChapterTitles": "מפה של כותרות פרק", + "ButtonMatchAllAuthors": "התאם את כל היוצרים", + "ButtonMatchBooks": "התאם ספרים", + "ButtonNevermind": "אל תדאג", + "ButtonNext": "הבא", + "ButtonNextChapter": "פרק הבא", + "ButtonOk": "אישור", + "ButtonOpenFeed": "פתח פיד", + "ButtonOpenManager": "פתח מנהל", + "ButtonPause": "השהה", + "ButtonPlay": "נגן", + "ButtonPlaying": "מנגן", + "ButtonPlaylists": "רשימות השמעה", + "ButtonPrevious": "קודם", + "ButtonPreviousChapter": "פרק קודם", + "ButtonPurgeAllCache": "נקה את כל המטמון", + "ButtonPurgeItemsCache": "נקה את מטמון הפריטים", + "ButtonPurgeMediaProgress": "נקה את ההתקדמות במדיה", + "ButtonQueueAddItem": "הוסף לתור", + "ButtonQueueRemoveItem": "הסר מהתור", + "ButtonQuickMatch": "התאם מהר", + "ButtonRead": "קרא", + "ButtonRefresh": "רענן", + "ButtonRemove": "הסר", + "ButtonRemoveAll": "הסר הכל", + "ButtonRemoveAllLibraryItems": "הסר את כל פריטי הספרייה", + "ButtonRemoveFromContinueListening": "הסר מההמשך להאזנה", + "ButtonRemoveFromContinueReading": "הסר מההמשך לקריאה", + "ButtonRemoveSeriesFromContinueSeries": "הסר סדרה מהמשך לסדרות", + "ButtonReScan": "סרוק מחדש", + "ButtonReset": "איפוס", + "ButtonResetToDefault": "איפוס לברירת המחדל", + "ButtonRestore": "שחזר", + "ButtonSave": "שמור", + "ButtonSaveAndClose": "שמור וסגור", + "ButtonSaveTracklist": "שמור רשימת רצועות", + "ButtonScan": "סרוק", + "ButtonScanLibrary": "סרוק ספרייה", + "ButtonSearch": "חפש", + "ButtonSelectFolderPath": "בחר נתיב לתיקייה", + "ButtonSeries": "סדרה", + "ButtonSetChaptersFromTracks": "קבע פרקים מרצועות", + "ButtonShare": "שתף", + "ButtonShiftTimes": "הזז זמנים", + "ButtonShow": "הצג", + "ButtonStartM4BEncode": "התחל הצפנה M4B", + "ButtonStartMetadataEmbed": "התחל הטמעת מטאדאטה", + "ButtonSubmit": "שלח", + "ButtonTest": "בדיקה", + "ButtonUpload": "העלה", + "ButtonUploadBackup": "העלה גיבוי", + "ButtonUploadCover": "העלה כריכה", + "ButtonUploadOPMLFile": "העלה קובץ OPML", + "ButtonUserDelete": "מחק משתמש {0}", + "ButtonUserEdit": "ערוך משתמש {0}", + "ButtonViewAll": "הצג הכול", + "ButtonYes": "כן", + "ErrorUploadFetchMetadataAPI": "שגיאה בשליפת מטאדאטה", + "ErrorUploadFetchMetadataNoResults": "לא ניתן לשלוף מטאדאטה - נסה לעדכן כותרת ו/או יוצר", + "ErrorUploadLacksTitle": "חייב להיות כותרת", + "HeaderAccount": "חשבון", + "HeaderAdvanced": "מתקדם", + "HeaderAppriseNotificationSettings": "הגדרות התראה ב-Apprise", + "HeaderAudiobookTools": "כלים לניהול קבצי אודיו", + "HeaderAudioTracks": "רצועות אודיו", + "HeaderAuthentication": "אימות", + "HeaderBackups": "גיבויים", + "HeaderChangePassword": "שנה סיסמה", + "HeaderChapters": "פרקים", + "HeaderChooseAFolder": "בחר תיקייה", + "HeaderCollection": "אוסף", + "HeaderCollectionItems": "פריטי אוסף", + "HeaderCover": "כריכה", + "HeaderCurrentDownloads": "הורדות נוכחיות", + "HeaderCustomMetadataProviders": "ספקי מטאדאטה מותאמים", + "HeaderDetails": "פרטים", + "HeaderDownloadQueue": "תור הורדה", + "HeaderEbookFiles": "קבצי ספר אלקטרוני", + "HeaderEmail": "אימייל", + "HeaderEmailSettings": "הגדרות אימייל", + "HeaderEpisodes": "פרקים", + "HeaderEreaderDevices": "התקני קריאה", + "HeaderEreaderSettings": "הגדרות קריאה", + "HeaderFiles": "קבצים", + "HeaderFindChapters": "מצא פרקים", + "HeaderIgnoredFiles": "קבצים שנתעלמו", + "HeaderItemFiles": "קבצי פריט", + "HeaderItemMetadataUtils": "כלי מטאדאטה פריט", + "HeaderLastListeningSession": "הפעלת האזנה אחרונה", + "HeaderLatestEpisodes": "הפרקים האחרונים", + "HeaderLibraries": "ספריות", + "HeaderLibraryFiles": "קבצי ספרייה", + "HeaderLibraryStats": "סטטיסטיקות ספרייה", + "HeaderListeningSessions": "הפעלות האזנה", + "HeaderListeningStats": "סטטיסטיקות האזנה", + "HeaderLogin": "כניסה", + "HeaderLogs": "לוגים", + "HeaderManageGenres": "נהל ז'אנרים", + "HeaderManageTags": "נהל תגיות", + "HeaderMapDetails": "מפה פרטים", + "HeaderMatch": "התאם", + "HeaderMetadataOrderOfPrecedence": "סדר העדפת מטאדאטה", + "HeaderMetadataToEmbed": "מטאדאטה להטמעה", + "HeaderNewAccount": "חשבון חדש", + "HeaderNewLibrary": "ספרייה חדשה", + "HeaderNotifications": "התראות", + "HeaderOpenIDConnectAuthentication": "אימות OpenID Connect", + "HeaderOpenRSSFeed": "פתח פיד RSS", + "HeaderOtherFiles": "קבצים אחרים", + "HeaderPasswordAuthentication": "אימות סיסמה", + "HeaderPermissions": "הרשאות", + "HeaderPlayerQueue": "תור ניגון", + "HeaderPlaylist": "רשימת השמעה", + "HeaderPlaylistItems": "פריטי רשימת השמעה", + "HeaderPodcastsToAdd": "פודקאסטים להוסיף", + "HeaderPreviewCover": "תצוגה מקדימה של כריכה", + "HeaderRemoveEpisode": "הסר פרק", + "HeaderRemoveEpisodes": "הסר {0} פרקים", + "HeaderRSSFeedGeneral": "פיד RSS כללי", + "HeaderRSSFeedIsOpen": "הפיד RSS פתוח", + "HeaderRSSFeeds": "פידי RSS", + "HeaderSavedMediaProgress": "התקדמות מדיה שמורה", + "HeaderSchedule": "מתזמן", + "HeaderScheduleLibraryScans": "קבע סריקות ספרייה אוטומטיות", + "HeaderSession": "הפעלה", + "HeaderSetBackupSchedule": "קבע מתז גיבוי", + "HeaderSettings": "הגדרות", + "HeaderSettingsDisplay": "הצגה", + "HeaderSettingsExperimental": "תכונות ניסיוניות", + "HeaderSettingsGeneral": "כללי", + "HeaderSettingsScanner": "סורק", + "HeaderSleepTimer": "טיימר שינה", + "HeaderStatsLargestItems": "הפריטים הגדולים ביותר", + "HeaderStatsLongestItems": "הפריטים הארוכים ביותר (בשעות)", + "HeaderStatsMinutesListeningChart": "דקות האזנה (בימים האחרונים)", + "HeaderStatsRecentSessions": "הפעלות אחרונות", + "HeaderStatsTop10Authors": "היוצרים המובילים 10", + "HeaderStatsTop5Genres": "הז'אנרים המובילים 5", + "HeaderTableOfContents": "תוכן העניינים", + "HeaderTools": "כלים", + "HeaderUpdateAccount": "עדכן חשבון", + "HeaderUpdateAuthor": "עדכן יוצר", + "HeaderUpdateDetails": "עדכן פרטים", + "HeaderUpdateLibrary": "עדכן ספרייה", + "HeaderUsers": "משתמשים", + "HeaderYearReview": "שנת {0} בסקירה", + "HeaderYourStats": "הסטטיסטיקות שלך", + "LabelAbridged": "מקוצר", + "LabelAccountType": "סוג חשבון", + "LabelAccountTypeAdmin": "מנהל", + "LabelAccountTypeGuest": "אורח", + "LabelAccountTypeUser": "משתמש", + "LabelActivity": "פעילות", + "LabelAdded": "נוסף", + "LabelAddedAt": "נוסף בתאריך", + "LabelAddToCollection": "הוסף לאוסף", + "LabelAddToCollectionBatch": "הוסף {0} ספרים לאוסף", + "LabelAddToPlaylist": "הוסף לרשימת השמעה", + "LabelAddToPlaylistBatch": "הוסף {0} פריטים לרשימת השמעה", + "LabelAdminUsersOnly": "רק מנהלים", + "LabelAll": "הכל", + "LabelAllUsers": "כל המשתמשים", + "LabelAllUsersExcludingGuests": "כל המשתמשים ללא אורחים", + "LabelAllUsersIncludingGuests": "כל המשתמשים כולל אורחים", + "LabelAlreadyInYourLibrary": "כבר בספרייה שלך", + "LabelAppend": "הוסף לסוף", + "LabelAuthor": "יוצר", + "LabelAuthorFirstLast": "יוצר (שם פרטי שם משפחה)", + "LabelAuthorLastFirst": "יוצר (שם משפחה, שם פרטי)", + "LabelAuthors": "יוצרים", + "LabelAutoDownloadEpisodes": "הורד פרקים באופן אוטומטי", + "LabelAutoFetchMetadata": "שלף מטאדאטה באופן אוטומטי", + "LabelAutoFetchMetadataHelp": "משיג מטאדאטה עבור כותרת, יוצר וסדרה כדי לשפר את תהליך ההעלאה. ייתכן שיהיה על יכולתך להתאים מטאדאטה נוספת לאחר ההעלאה.", + "LabelAutoLaunch": "הפעלה אוטומטית", + "LabelAutoLaunchDescription": "הפניה אוטומטית לספק האימות כאשר מגיעים לדף ההתחברות (ניתן להפעיל ידנית במסלול /login?autoLaunch=0)", + "LabelAutoRegister": "הרשמה אוטומטית", + "LabelAutoRegisterDescription": "יצירת משתמשים חדשים אוטומטית לאחר התחברות", + "LabelBackToUser": "חזרה למשתמש", + "LabelBackupLocation": "מיקום גיבוי", + "LabelBackupsEnableAutomaticBackups": "הפעל גיבויים אוטומטיים", + "LabelBackupsEnableAutomaticBackupsHelp": "גיבויים שמורים ב /metadata/backups", + "LabelBackupsMaxBackupSize": "גודל הגיבוי המרבי (בג'יגה-בייט)", + "LabelBackupsMaxBackupSizeHelp": "כהגנה על עצמך מפני תצורה שגויה, הגיבויים ייכשלו אם הם יעברו את הגודל שהוגדר.", + "LabelBackupsNumberToKeep": "מספר הגיבויים לשמירה", + "LabelBackupsNumberToKeepHelp": "יש להסיר רק גיבוי אחד בכל פעם, לכן אם יש לך כבר יותר מגיבוי אחד יש להסיר אותם באופן ידני.", + "LabelBitrate": "ביטרייט", + "LabelBooks": "ספרים", + "LabelButtonText": "טקסט לחצן", + "LabelChangePassword": "שינוי סיסמה", + "LabelChannels": "ערוצים", + "LabelChapters": "פרקים", + "LabelChaptersFound": "פרקים נמצאו", + "LabelChapterTitle": "כותרת הפרק", + "LabelClickForMoreInfo": "לחץ למידע נוסף", + "LabelClosePlayer": "סגור נגן", + "LabelCodec": "קודק", + "LabelCollapseSeries": "צמצום סדרה", + "LabelCollection": "אוסף", + "LabelCollections": "אוספים", + "LabelComplete": "מלא", + "LabelConfirmPassword": "אישור סיסמה", + "LabelContinueListening": "המשך האזנה", + "LabelContinueReading": "המשך קריאה", + "LabelContinueSeries": "המשך סדרה", + "LabelCover": "כיסוי", + "LabelCoverImageURL": "כתובת התמונה המצויה ברשת", + "LabelCreatedAt": "נוצר בתאריך", + "LabelCronExpression": "ביטוי קרון", + "LabelCurrent": "נוכחי", + "LabelCurrently": "כעת:", + "LabelCustomCronExpression": "ביטוי קרון מותאם אישית:", + "LabelDatetime": "תאריך ושעה", + "LabelDeleteFromFileSystemCheckbox": "מחיקה מהמערכת הקבצים (הסר סימון למחיקה רק מהמסד נתונים)", + "LabelDescription": "תיאור", + "LabelDeselectAll": "הסר בחירת כל הפריטים", + "LabelDevice": "התקן", + "LabelDeviceInfo": "מידע על התקן", + "LabelDeviceIsAvailableTo": "התקן זמין ל...", + "LabelDirectory": "תיקייה", + "LabelDiscFromFilename": "דיסק משם הקובץ", + "LabelDiscFromMetadata": "דיסק מהמטא-נתונים", + "LabelDiscover": "גלה", + "LabelDownload": "הורד", + "LabelDownloadNEpisodes": "הורד {0} פרקים", + "LabelDuration": "משך", + "LabelDurationFound": "משך נמצא:", + "LabelEbook": "ספר אלקטרוני", + "LabelEbooks": "ספרים אלקטרוניים", + "LabelEdit": "עריכה", + "LabelEmail": "דואר אלקטרוני", + "LabelEmailSettingsFromAddress": "כתובת מאיתה", + "LabelEmailSettingsSecure": "מאובטח", + "LabelEmailSettingsSecureHelp": "אם נכון, החיבור ישתמש ב-TLS בעת התחברות לשרת. אם לא, תישתמש חיבור זה ב-TLS אם השרת תומך בהרחבת STARTTLS. ברוב המקרים עדיף להגדיר ערך זה כנכון אם אתה מחבר לפורט 465. לפורט 587 או 25 שמור על ערך זה כשקר.", + "LabelEmailSettingsTestAddress": "כתובת לבדיקת מבנה", + "LabelEmbeddedCover": "כיסוי משובץ", + "LabelEnable": "הפעל", + "LabelEnd": "סיום", + "LabelEpisode": "פרק", + "LabelEpisodeTitle": "כותרת הפרק", + "LabelEpisodeType": "סוג הפרק", + "LabelExample": "דוגמה", + "LabelExplicit": "ברור", + "LabelFeedURL": "כתובת ערוץ", + "LabelFetchingMetadata": "מושכים מטא-נתונים", + "LabelFile": "קובץ", + "LabelFileBirthtime": "זמן הולדת הקובץ", + "LabelFileModified": "הקובץ הוחלף", + "LabelFilename": "שם הקובץ", + "LabelFilterByUser": "סינון לפי משתמש", + "LabelFindEpisodes": "מצא פרקים", + "LabelFinished": "סיים", + "LabelFolder": "תיקייה", + "LabelFolders": "תיקיות", + "LabelFontBold": "מודגש", + "LabelFontFamily": "משפחת הפונטים", + "LabelFontItalic": "נטוי", + "LabelFontScale": "קנה מידה של הפונט", + "LabelFontStrikethrough": "קו חוצה", + "LabelFormat": "תבנית", + "LabelGenre": "ז'אנר", + "LabelGenres": "ז'אנרים", + "LabelHardDeleteFile": "מחיקה קשה של הקובץ", + "LabelHasEbook": "יש ספר אלקטרוני", + "LabelHasSupplementaryEbook": "יש ספר אלקטרוני תוספתי", + "LabelHighestPriority": "עדיפות הגבוהה ביותר", + "LabelHost": "מארח", + "LabelHour": "שעה", + "LabelIcon": "סמל", + "LabelImageURLFromTheWeb": "כתובת התמונה מהרשת", + "LabelIncludeInTracklist": "כלול ברשימת השמעה", + "LabelIncomplete": "לא הושלם", + "LabelInProgress": "בתהליך", + "LabelInterval": "מרווח", + "LabelIntervalCustomDailyWeekly": "מותאם אישית יומי/שבועי", + "LabelIntervalEvery12Hours": "כל 12 שעות", + "LabelIntervalEvery15Minutes": "כל 15 דקות", + "LabelIntervalEvery2Hours": "כל 2 שעות", + "LabelIntervalEvery30Minutes": "כל 30 דקות", + "LabelIntervalEvery6Hours": "כל 6 שעות", + "LabelIntervalEveryDay": "כל יום", + "LabelIntervalEveryHour": "כל שעה", + "LabelInvalidParts": "חלקים לא תקינים", + "LabelInvert": "הפוך", + "LabelItem": "פריט", + "LabelLanguage": "שפה", + "LabelLanguageDefaultServer": "שפת השרת ברירת המחדל", + "LabelLastBookAdded": "הספר האחרון שהוסף", + "LabelLastBookUpdated": "הספר האחרון שעודכן", + "LabelLastSeen": "נראה לאחרונה", + "LabelLastTime": "הפעם האחרונה", + "LabelLastUpdate": "עדכון אחרון", + "LabelLayout": "פריסה", + "LabelLayoutSinglePage": "דף בודד", + "LabelLayoutSplitPage": "פיצול הדף", + "LabelLess": "פחות", + "LabelLibrariesAccessibleToUser": "ספריות נגישות למשתמש", + "LabelLibrary": "ספרייה", + "LabelLibraryItem": "פריט ספרייה", + "LabelLibraryName": "שם הספרייה", + "LabelLimit": "מגבלה", + "LabelLineSpacing": "רווח שורות", + "LabelListenAgain": "האזן שוב", + "LabelLogLevelDebug": "דיבוג", + "LabelLogLevelInfo": "מידע", + "LabelLogLevelWarn": "אזהרה", + "LabelLookForNewEpisodesAfterDate": "חפש פרקים חדשים לאחר תאריך זה", + "LabelLowestPriority": "עדיפות הנמוכה ביותר", + "LabelMatchExistingUsersBy": "התאם משתמשים קיימים לפי", + "LabelMatchExistingUsersByDescription": "משמש לחיבור משתמשים קיימים. לאחר החיבור, המשתמשים יתאמו לפי זיהוי ייחודי מספק ה-SSO שלך", + "LabelMediaPlayer": "נגן מדיה", + "LabelMediaType": "סוג מדיה", + "LabelMetadataOrderOfPrecedenceDescription": "מקורות המטא-נתונים עם עדיפות גבוהה יחליפו מקורות עם עדיפות נמוכה יותר", + "LabelMetadataProvider": "ספק מטא-נתונים", + "LabelMetaTag": "תג מטא", + "LabelMetaTags": "תגי מטא", + "LabelMinute": "דקה", + "LabelMissing": "חסר", + "LabelMissingEbook": "אין ספר אלקטרוני", + "LabelMissingParts": "חלקים חסרים", + "LabelMissingSupplementaryEbook": "אין ספר אלקטרוני נלווה", + "LabelMobileRedirectURIs": "כתובות משדר ניידות מורשות", + "LabelMobileRedirectURIsDescription": "זהו רשימה לבניה של כתובות ה-URI הנתמכות להפניות עבור אפליקציות ניידות. הברירת מחדל היא audiobookshelf://oauth, שניתן להסיר או להוסיף לה כתובות נוספות לאינטגרציה עם אפליקציות צד שלישי. שימוש בכוכבית (*) כקלט בודד מאפשר כל URI.", + "LabelMore": "עוד", + "LabelMoreInfo": "מידע נוסף", + "LabelName": "שם", + "LabelNarrator": "נרטור", + "LabelNarrators": "נרטורים", + "LabelNew": "חדש", + "LabelNewestAuthors": "סופרים החדשים ביותר", + "LabelNewestEpisodes": "הפרקים החדשים ביותר", + "LabelNewPassword": "סיסמה חדשה", + "LabelNextBackupDate": "תאריך גיבוי הבא", + "LabelNextScheduledRun": "הרצה מתוזמנת הבאה", + "LabelNoEpisodesSelected": "לא נבחרו פרקים", + "LabelNotes": "הערות", + "LabelNotFinished": "לא הושלם", + "LabelNotificationAppriseURL": "כתובת URL של התראה", + "LabelNotificationAvailableVariables": "משתנים זמינים", + "LabelNotificationBodyTemplate": "תבנית גוף", + "LabelNotificationEvent": "אירוע התראה", + "LabelNotificationsMaxFailedAttempts": "מספר הניסיונות הנכשלים המרבי", + "LabelNotificationsMaxFailedAttemptsHelp": "ההתראות מושבתות לאחר שהן נכשלות לשלוח מספר זה פעמים", + "LabelNotificationsMaxQueueSize": "גודל התור המרבי לאירועי התראה", + "LabelNotificationsMaxQueueSizeHelp": "האירועים מוגבלים לשליחה אחת לשנייה. האירועים יתעלמו אם התור מלא בגודלו המרבי. זה מונע ספאם בהתראות.", + "LabelNotificationTitleTemplate": "תבנית כותרת", + "LabelNotStarted": "לא התחיל", + "LabelNumberOfBooks": "מספר הספרים", + "LabelNumberOfEpisodes": "מספר הפרקים", + "LabelOpenRSSFeed": "פתח ערוץ RSS", + "LabelOverwrite": "לשכפל", + "LabelPassword": "סיסמה", + "LabelPath": "נתיב", + "LabelPermissionsAccessAllLibraries": "ניתן לגשת לכל הספריות", + "LabelPermissionsAccessAllTags": "ניתן לגשת לכל התגיות", + "LabelPermissionsAccessExplicitContent": "ניתן לגשת לתוכן מפורט", + "LabelPermissionsDelete": "ניתן למחוק", + "LabelPermissionsDownload": "ניתן להוריד", + "LabelPermissionsUpdate": "ניתן לעדכן", + "LabelPermissionsUpload": "ניתן להעלות", + "LabelPersonalYearReview": "השנה שלך בסקירה ({0})", + "LabelPhotoPathURL": "נתיב/URL לתמונה", + "LabelPlaylists": "רשימות השמעה", + "LabelPlayMethod": "שיטת הפעלה", + "LabelPodcast": "פודקאסט", + "LabelPodcasts": "פודקאסטים", + "LabelPodcastSearchRegion": "אזור חיפוש פודקאסט", + "LabelPodcastType": "סוג פודקאסט", + "LabelPort": "יציאה", + "LabelPrefixesToIgnore": "קידומות להתעלמות (קסה אינסנסיטיבית)", + "LabelPreventIndexing": "מנע את האינדוקסציה של הפיד שלך על ידי ספריות אייטונס וגוגל פודקאסט", + "LabelPrimaryEbook": "ספר אלקטרוני ראשי", + "LabelProgress": "התקדמות", + "LabelProvider": "ספק", + "LabelPubDate": "תאריך פרסום", + "LabelPublisher": "מוציא לאור", + "LabelPublishYear": "שנת הפרסום", + "LabelRead": "קריאה", + "LabelReadAgain": "קרא שוב", + "LabelReadEbookWithoutProgress": "קרוא ספר אלקטרוני בלי לשמור התקדמות", + "LabelRecentlyAdded": "נוסף לאחרונה", + "LabelRecentSeries": "סדרות אחרונות", + "LabelRecommended": "מומלץ", + "LabelRedo": "עשה שוב", + "LabelRegion": "אזור", + "LabelReleaseDate": "תאריך שחרור", + "LabelRemoveCover": "הסר כיסוי", + "LabelRowsPerPage": "שורות לעמוד", + "LabelRSSFeedCustomOwnerEmail": "אימייל בעלים מותאם אישית", + "LabelRSSFeedCustomOwnerName": "שם בעלים מותאם אישית", + "LabelRSSFeedOpen": "פתח ערוץ RSS", + "LabelRSSFeedPreventIndexing": "מנע אינדוקסציה", + "LabelRSSFeedSlug": "שם תמצאות RSS", + "LabelRSSFeedURL": "URL של פיד RSS", + "LabelSearchTerm": "מונח חיפוש", + "LabelSearchTitle": "כותרת חיפוש", + "LabelSearchTitleOrASIN": "כותרת חיפוש או ASIN", + "LabelSeason": "עונה", + "LabelSelectAllEpisodes": "בחר את כל הפרקים", + "LabelSelectEpisodesShowing": "בחר {0} פרקים המוצגים", + "LabelSelectUsers": "בחר משתמשים", + "LabelSendEbookToDevice": "שלח ספר אלקטרוני ל...", + "LabelSequence": "רצף", + "LabelSeries": "סדרה", + "LabelSeriesName": "שם הסדרה", + "LabelSeriesProgress": "התקדמות בסדרה", + "LabelServerYearReview": "השנה בסקירה של השרת ({0})", + "LabelSetEbookAsPrimary": "קבע כראשי", + "LabelSetEbookAsSupplementary": "קבע כספר אלקטרוני נלווה", + "LabelSettingsAudiobooksOnly": "רק ספרי קול", + "LabelSettingsAudiobooksOnlyHelp": "הפעלת ההגדרה הזו תתעלם מקבצי ספרים אלקטרוניים אלא אם כן הם נמצאים בתיקיית ספרי קול, שבמקרה זה יקבעו כספרים אלקטרוניים נלווים", + "LabelSettingsBookshelfViewHelp": "עיצוב סקיומורפי עם מדפים עץ", + "LabelSettingsChromecastSupport": "תמיכת Chromecast", + "LabelSettingsDateFormat": "פורמט תאריך", + "LabelSettingsDisableWatcher": "השבת צופה", + "LabelSettingsDisableWatcherForLibrary": "השבת צופה תיקייה עבור ספרייה", + "LabelSettingsDisableWatcherHelp": "מבטל את הוספת/עדכון אוטומטי של פריטים כאשר שינויי קבצים זוהים. *דורש איתחול שרת", + "LabelSettingsEnableWatcher": "הפעל צופה", + "LabelSettingsEnableWatcherForLibrary": "הפעל צופה תיקייה עבור ספרייה", + "LabelSettingsEnableWatcherHelp": "מאפשר הוספת/עדכון אוטומטי של פריטים כאשר שינויי קבצים זוהים. *דורש איתחול שרת", + "LabelSettingsExperimentalFeatures": "תכונות ניסיוניות", + "LabelSettingsExperimentalFeaturesHelp": "תכונות בפיתוח שדורשות משובך ובדיקה. לחץ לפתיחת דיון ב-GitHub.", + "LabelSettingsFindCovers": "מצא כיסויים", + "LabelSettingsFindCoversHelp": "אם לספר השמע שלך אין כיסוי מוטמע או תמונת כיסוי בתיקייה, הסורק ינסה למצוא כיסוי.
שים לב: זה יאריך את זמן הסריקה", + "LabelSettingsHideSingleBookSeries": "הסתר סדרות ספר אחד", + "LabelSettingsHideSingleBookSeriesHelp": "סדרות הכוללות ספר אחד יוסתרו מדף הסדרות ומדף הבית.", + "LabelSettingsHomePageBookshelfView": "השתמש בתצוגת מדף על דף הבית", + "LabelSettingsLibraryBookshelfView": "השתמש בתצוגת מדף בספרייה", + "LabelSettingsOnlyShowLaterBooksInContinueSeries": "דלג על ספרים קודמים בהמשך סדרות", + "LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "מדף המשך סדרות מציג את הספר הראשון שלא התחיל בסדרה שיש לפחות ספר אחד שהושלם ואין ספרים בתהליך. הפעלת ההגדרה הזו תמשיך סדרות מהספר שהושלם הכי הרחק במקום מהספר הראשון שלא התחיל.", + "LabelSettingsParseSubtitles": "פענח כתוביות", + "LabelSettingsParseSubtitlesHelp": "העתק כותרת משנה משם תיקיית הספר.
כותרת המשנה חייבת להיות מופרדת עם התו ״-״
לדוגמא, כותרת המשנה לספר ״שם הספר - כותרת משנה״, היא ״כותרת משנה״", + "LabelSettingsPreferMatchedMetadata": "עדיף מטה-נתונים מתואמים", + "LabelSettingsPreferMatchedMetadataHelp": "נתונים מתואמים ידריך פרטי פריט כאשר משתמשים בהתאמה מהירה. כברירת מחדל, התאמה מהירה תמלא פרטים חסרים בלבד.", + "LabelSettingsSkipMatchingBooksWithASIN": "דלג על ספרים שכבר יש להם ASIN", + "LabelSettingsSkipMatchingBooksWithISBN": "דלג על ספרים שכבר יש להם ISBN", + "LabelSettingsSortingIgnorePrefixes": "התעלם מקידומות במיון", + "LabelSettingsSortingIgnorePrefixesHelp": "לדוגמא, לקידומת ״ה״ שם הספר, שם הספר ימוין בתור ״שם הספר״, ״ה״", + "LabelSettingsSquareBookCovers": "השתמש בכיסויים מרובעים לספרים", + "LabelSettingsSquareBookCoversHelp": "מעדיף להשתמש בכיסויים מרובעים מעל כיסויים סטנדרטיים ביחס 1.6:1", + "LabelSettingsStoreCoversWithItem": "אחסן כיסויים עם פריט", + "LabelSettingsStoreCoversWithItemHelp": "כברירת מחדל, צילומי כריכות נשמרים בתיקיית /metadata/items, לאחר הפעלת הגדרה זו צילומי כריכות יישמרו בתיקיית הספר, רק קובץ אחד בשם ״cover״ יישמר", + "LabelSettingsStoreMetadataWithItem": "אחסן מטה-נתונים עם פריט", + "LabelSettingsStoreMetadataWithItemHelp": "כברירת מחדל, קבצי מטה-נתונים מאוחסנים ב- /metadata/items, הפעלת ההגדרה תאחסן קבצי מטה-נתונים בתיקיית פריט שלך בספרייה", + "LabelSettingsTimeFormat": "פורמט זמן", + "LabelShowAll": "הצג הכל", + "LabelSize": "גודל", + "LabelSleepTimer": "טיימר שינה", + "LabelSlug": "Slug", + "LabelStart": "התחלה", + "LabelStarted": "התחיל", + "LabelStartedAt": "התחיל ב", + "LabelStartTime": "זמן התחלה", + "LabelStatsAudioTracks": "רצועות שמע", + "LabelStatsAuthors": "מחברים", + "LabelStatsBestDay": "היום הטוב ביותר", + "LabelStatsDailyAverage": "ממוצע יומי", + "LabelStatsDays": "ימים", + "LabelStatsDaysListened": "ימים שהוקשבו", + "LabelStatsHours": "שעות", + "LabelStatsInARow": "ברצף", + "LabelStatsItemsFinished": "פריטים שסיימו", + "LabelStatsItemsInLibrary": "פריטים בספרייה", + "LabelStatsMinutes": "דקות", + "LabelStatsMinutesListening": "דקות האזנה", + "LabelStatsOverallDays": "ימים כולל", + "LabelStatsOverallHours": "שעות כולל", + "LabelStatsWeekListening": "האזנה שבועית", + "LabelSubtitle": "תת כותרת", + "LabelSupportedFileTypes": "סוגי קבצים נתמכים", + "LabelTag": "תג", + "LabelTags": "תגיות", + "LabelTagsAccessibleToUser": "תגיות נגישות למשתמש", + "LabelTagsNotAccessibleToUser": "תגיות לא נגישות למשתמש", + "LabelTasks": "משימות פעילות", + "LabelTextEditorBulletedList": "רשימה עם נקודות", + "LabelTextEditorLink": "קישור", + "LabelTextEditorNumberedList": "רשימה ממוספרת", + "LabelTextEditorUnlink": "ביטול קישור", + "LabelTheme": "ערכת נושא", + "LabelThemeDark": "כהה", + "LabelThemeLight": "בהיר", + "LabelTimeBase": "בסיס זמן", + "LabelTimeListened": "זמן האזנה", + "LabelTimeListenedToday": "זמן האזנה היום", + "LabelTimeRemaining": "{0} נותרו", + "LabelTimeToShift": "זמן להיסט בשניות", + "LabelTitle": "כותרת", + "LabelToolsEmbedMetadata": "הטמעת מטה-נתונים", + "LabelToolsEmbedMetadataDescription": "הטמעת מטה-נתונים לקבצי שמע כולל תמונות שער ופרקים.", + "LabelToolsMakeM4b": "יצירת קובץ אודיו M4B", + "LabelToolsMakeM4bDescription": "יצירת קובץ אודיו .M4B עם מטה-נתונים מוטמעים, תמונת שער ופרקים.", + "LabelToolsSplitM4b": "פיצול M4B ל-MP3", + "LabelToolsSplitM4bDescription": "יצירת קבצי MP3 מ-M4B מפוצל לפי פרקים עם מטה-נתונים מוטמעים, תמונת שער ופרקים.", + "LabelTotalDuration": "משך כולל", + "LabelTotalTimeListened": "סך הזמן שהוקשב", + "LabelTrackFromFilename": "מסלול משמות קבצים", + "LabelTrackFromMetadata": "מסלול ממטה-נתונים", + "LabelTracks": "מסלולים", + "LabelTracksMultiTrack": "מסלול רב-ערוצים", + "LabelTracksNone": "אין מסלולים", + "LabelTracksSingleTrack": "מסלול יחיד", + "LabelType": "סוג", + "LabelUnabridged": "לא מקוצר", + "LabelUndo": "בטל", + "LabelUnknown": "לא ידוע", + "LabelUpdateCover": "עדכן כריכה", + "LabelUpdateCoverHelp": "אפשר כיסוי מעלה של כריכות קיימות עבור הספרים הנבחרים כאשר נמצאה התאמה", + "LabelUpdatedAt": "עודכן ב", + "LabelUpdateDetails": "עדכון פרטים", + "LabelUpdateDetailsHelp": "אפשר עדכון מעלה של פרטים קיימים עבור הספרים הנבחרים כאשר נמצאה התאמה", + "LabelUploaderDragAndDrop": "גרור ושחרר קבצים או תיקיות", + "LabelUploaderDropFiles": "שחרר קבצים", + "LabelUploaderItemFetchMetadataHelp": "משיכת כותרת, סופר וסדרה באופן אוטומטי", + "LabelUseChapterTrack": "השתמש במסלול פרקים", + "LabelUseFullTrack": "השתמש במסלול מלא", + "LabelUser": "משתמש", + "LabelUsername": "שם משתמש", + "LabelValue": "ערך", + "LabelVersion": "גרסה", + "LabelViewBookmarks": "הצג סימניות", + "LabelViewChapters": "הצג פרקים", + "LabelViewQueue": "הצג תור הנגן", + "LabelVolume": "עוצמת קול", + "LabelWeekdaysToRun": "ימי השבוע להרצה", + "LabelYearReviewHide": "הסתר שנת סקירה", + "LabelYearReviewShow": "ראה שנת סקירה", + "LabelYourAudiobookDuration": "משך האודיובוק שלך", + "LabelYourBookmarks": "הסימניות שלך", + "LabelYourPlaylists": "הפלייליסטים שלך", + "LabelYourProgress": "ההתקדמות שלך", + "MessageAddToPlayerQueue": "הוסף לתור הנגן", + "MessageAppriseDescription": "כדי להשתמש בתכונה זו יש לך להריץ מופע של ממשק התכנית האפליקציה או API שיטפל בבקשות אלו.
כתובת URL של ממשק ה-Apprise API צריכה להיות הנתיב המלא לשליחת ההתראה, לדוגמה, אם המופע של ה-API שלך מוצע ב-http://192.168.1.1:8337 אז עליך לשים http://192.168.1.1:8337/notify.", + "MessageBackupsDescription": "גיבויים כוללים משתמשים, התקדמות משתמש, פרטי פריטי ספרייה, הגדרות שרת ותמונות השמורות ב-/metadata/items & /metadata/authors. גיבויים לא כוללים קבצים שמורים בתיקיות הספרייה שלך.", + "MessageBatchQuickMatchDescription": "התאמה מהירה תנסה להוסיף כריכות ומטה-נתונים חסרים עבור הפריטים הנבחרים. הפעל את האפשרויות למטה כדי לאפשר להתאמה מהירה לדרוס כריכות קיימות ו/או מטה-נתונים.", + "MessageBookshelfNoCollections": "עדיין לא יצרת שום אוספים", + "MessageBookshelfNoResultsForFilter": "אין תוצאות עבור מסנן \"{0}: {1}\"", + "MessageBookshelfNoRSSFeeds": "אין RSS feeds פתוחים", + "MessageBookshelfNoSeries": "אין לך סדרות", + "MessageChapterEndIsAfter": "סיום הפרק מאוחר מסיום האודיובוק שלך", + "MessageChapterErrorFirstNotZero": "הפרק הראשון חייב להתחיל ב-0", + "MessageChapterErrorStartGteDuration": "זמן התחלה לא תקין חייב להיות פחות ממשך האודיובוק", + "MessageChapterErrorStartLtPrev": "זמן התחלה לא תקין חייב להיות גדול או שווה לזמן התחלה של הפרק הקודם", + "MessageChapterStartIsAfter": "התחלת הפרק מאוחרת מסיום האודיובוק שלך", + "MessageCheckingCron": "בודק את תזמון העבודה...", + "MessageConfirmCloseFeed": "האם אתה בטוח שאתה רוצה לסגור את הפיד הזה?", + "MessageConfirmDeleteBackup": "האם אתה בטוח שברצונך למחוק גיבוי עבור {0}?", + "MessageConfirmDeleteFile": "זה ימחק את הקובץ מהמערכת שלך. האם אתה בטוח?", + "MessageConfirmDeleteLibrary": "האם אתה בטוח שברצונך למחוק לצמיתות את הספרייה \"{0}\"?", + "MessageConfirmDeleteLibraryItem": "זה ימחק את פריט הספרייה ממסד הנתונים ומהמערכת שלך. האם אתה בטוח?", + "MessageConfirmDeleteLibraryItems": "זה ימחק {0} פריטי ספרייה ממסד הנתונים ומהמערכת שלך. האם אתה בטוח?", + "MessageConfirmDeleteSession": "האם אתה בטוח שאתה רוצה למחוק את ההפעלה הזו?", + "MessageConfirmForceReScan": "האם אתה בטוח שאתה רוצה לכוון איתור מחדש?", + "MessageConfirmMarkAllEpisodesFinished": "האם אתה בטוח שברצונך לסמן את כל הפרקים כסיום?", + "MessageConfirmMarkAllEpisodesNotFinished": "האם אתה בטוח שברצונך לסמן את כל הפרקים כלא סיום?", + "MessageConfirmMarkSeriesFinished": "האם אתה בטוח שברצונך לסמן את כל הספרים בסדרה זו כסיום?", + "MessageConfirmMarkSeriesNotFinished": "האם אתה בטוח שברצונך לסמן את כל הספרים בסדרה זו כלא סיום?", + "MessageConfirmQuickEmbed": "אזהרה! הטמעה מהירה לא תגבה גיבוי של קבצי האודיו שלך. וודא שיש לך גיבוי של קבצי האודיו שלך.

האם ברצונך להמשיך?", + "MessageConfirmRemoveAllChapters": "האם אתה בטוח שברצונך להסיר את כל הפרקים?", + "MessageConfirmRemoveAuthor": "האם אתה בטוח שברצונך להסיר את המחבר \"{0}\"?", + "MessageConfirmRemoveCollection": "האם אתה בטוח שברצונך להסיר אוסף \"{0}\"?", + "MessageConfirmRemoveEpisode": "האם אתה בטוח שברצונך להסיר פרק \"{0}\"?", + "MessageConfirmRemoveEpisodes": "האם אתה בטוח שברצונך להסיר {0} פרקים?", + "MessageConfirmRemoveListeningSessions": "האם אתה בטוח שברצונך להסיר {0} מפגשי האזנה?", + "MessageConfirmRemoveNarrator": "האם אתה בטוח שברצונך להסיר נרטור \"{0}\"?", + "MessageConfirmRemovePlaylist": "האם אתה בטוח שברצונך להסיר את רשימת ההשמעה שלך \"{0}\"?", + "MessageConfirmRenameGenre": "האם אתה בטוח שברצונך לשנות את שם הג'אנר \"{0}\" ל \"{1}\" עבור כל הפריטים?", + "MessageConfirmRenameGenreMergeNote": "הערה: ג'אנר זה כבר קיים ולכן הם יתמזגו.", + "MessageConfirmRenameGenreWarning": "אזהרה! יש ג'אנר דומה עם רישום שונה הכבר קיים \"{0}\".", + "MessageConfirmRenameTag": "האם אתה בטוח שברצונך לשנות את שם התג \"{0}\" ל \"{1}\" עבור כל הפריטים?", + "MessageConfirmRenameTagMergeNote": "הערה: התג זה כבר קיים ולכן הם יתמזגו.", + "MessageConfirmRenameTagWarning": "אזהרה! יש תג דומה עם רישום שונה הכבר קיים \"{0}\".", + "MessageConfirmReScanLibraryItems": "האם אתה בטוח שברצונך לסרוק מחדש {0} פריטים?", + "MessageConfirmSendEbookToDevice": "האם אתה בטוח שברצונך לשלוח {0} איבוק \"{1}\" למכשיר \"{2}\"?", + "MessageDownloadingEpisode": "מוריד פרק", + "MessageDragFilesIntoTrackOrder": "גרור קבצים לסדר השמעה נכון", + "MessageEmbedFinished": "ההטמעה הושלמה!", + "MessageEpisodesQueuedForDownload": "{0} פרקים בתור להורדה", + "MessageFeedURLWillBe": "כתובת URL של העדכון תהיה {0}", + "MessageFetching": "מבצע גישה...", + "MessageForceReScanDescription": "יסרוק את כל הקבצים מחדש כמו סריקה חדשה. תגיות ID3 של קבצי שמע, קבצי OPF וקבצי טקסט יסרקו כחדשים.", + "MessageImportantNotice": "הודעה חשובה!", + "MessageInsertChapterBelow": "הוסף פרק למטה", + "MessageItemsSelected": "{0} פריטים נבחרו", + "MessageItemsUpdated": "{0} פריטים עודכנו", + "MessageJoinUsOn": "הצטרף אלינו ב-", + "MessageListeningSessionsInTheLastYear": "{0} מפגשי האזנה בשנה האחרונה", + "MessageLoading": "טוען...", + "MessageLoadingFolders": "טוען תיקיות...", + "MessageM4BFailed": "M4B נכשל!", + "MessageM4BFinished": "M4B הושלם!", + "MessageMapChapterTitles": "מפה שמות פרקים לפרקי הספר השמורים שלך ללא שינוי תגים זמניים", + "MessageMarkAllEpisodesFinished": "סמן את כל הפרקים כהסתיימו", + "MessageMarkAllEpisodesNotFinished": "סמן את כל הפרקים כלא הסתיימו", + "MessageMarkAsFinished": "סמן כהסתיים", + "MessageMarkAsNotFinished": "סמן כלא הסתיים", + "MessageMatchBooksDescription": "ינסה להתאים ספרים בספריית הספרים שלך עם ספר מספק החיפוש הנבחר וימלא פרטים ריקים ותמונות כריכה. לא ימחק פרטים.", + "MessageNoAudioTracks": "אין רצועות שמע", + "MessageNoAuthors": "אין סופרים", + "MessageNoBackups": "אין גיבויים", + "MessageNoBookmarks": "אין סימניות", + "MessageNoChapters": "אין פרקים", + "MessageNoCollections": "אין אוספים", + "MessageNoCoversFound": "לא נמצאו כריכות", + "MessageNoDescription": "אין תיאור", + "MessageNoDownloadsInProgress": "אין הורדות פעילות כרגע", + "MessageNoDownloadsQueued": "אין הורדות בתור", + "MessageNoEpisodeMatchesFound": "לא נמצאו התאמות לפרק", + "MessageNoEpisodes": "אין פרקים", + "MessageNoFoldersAvailable": "אין תיקיות זמינות", + "MessageNoGenres": "אין ז'אנרים", + "MessageNoIssues": "אין בעיות", + "MessageNoItems": "אין פריטים", + "MessageNoItemsFound": "לא נמצאו פריטים", + "MessageNoListeningSessions": "אין מפגשי האזנה", + "MessageNoLogs": "אין לוגים", + "MessageNoMediaProgress": "אין התקדמות מדיה", + "MessageNoNotifications": "אין התראות", + "MessageNoPodcastsFound": "לא נמצאו פודקאסטים", + "MessageNoResults": "אין תוצאות", + "MessageNoSearchResultsFor": "אין תוצאות חיפוש עבור \"{0}\"", + "MessageNoSeries": "אין סדרות", + "MessageNoTags": "אין תגיות", + "MessageNoTasksRunning": "אין משימות פעילות", + "MessageNotYetImplemented": "עדיין לא מיושם", + "MessageNoUpdateNecessary": "לא נדרש עדכון", + "MessageNoUpdatesWereNecessary": "לא הייתה צורך בעדכונים", + "MessageNoUserPlaylists": "אין לך רשימות השמעה", + "MessageOr": "או", + "MessagePauseChapter": "השהה השמעת הפרק", + "MessagePlayChapter": "השמע לתחילת הפרק", + "MessagePlaylistCreateFromCollection": "צור רשימת השמעה מאוסף", + "MessagePodcastHasNoRSSFeedForMatching": "הפודקאסט אין לו כתובת URL של הזנת RSS לשימוש בהתאמה", + "MessageQuickMatchDescription": "ממלא פרטים ריקים וכריכות עם התוצאה הראשונה מ '{0}'. לא ימחק פרטים אלא אם הגדרות השרת 'מעדיפים מטה-נתונים התואמים' מופעלות.", + "MessageRemoveChapter": "הסר פרק", + "MessageRemoveEpisodes": "הסר {0} פרקים", + "MessageRemoveFromPlayerQueue": "הסר מתור ההשמעה של הנגן", + "MessageRemoveUserWarning": "האם אתה בטוח שברצונך למחוק לצמיתות את המשתמש \"{0}\"?", + "MessageReportBugsAndContribute": "דווח על באגים, בקש תכונות חדשות, ותרום ב-", + "MessageResetChaptersConfirm": "האם אתה בטוח שברצונך לאפס את הפרקים ולבטל את השינויים שביצעת?", + "MessageRestoreBackupConfirm": "האם אתה בטוח שברצונך לשחזר את הגיבוי שנוצר ב", + "MessageRestoreBackupWarning": "שחזור גיבוי ימחק את כל מסד הנתונים השוכן ב /config ואת תמונות הכריכה ב- /metadata/items & /metadata/authors.

גיבויים אינם משנים קבצים בתיקיות הספרייה שלך. אם הגדרות השרת מופעלות כדי לאחסן תמונות כריכה ומטאדאטה בתיקיות הספרייה שלך אז אלה לא יגובו או ימחקו.

כל הלקוחות המשתמשים בשרת שלך יתעדכנו באופן אוטומטי.", + "MessageSearchResultsFor": "תוצאות חיפוש עבור", + "MessageSelected": "{0} נבחרו", + "MessageServerCouldNotBeReached": "לא ניתן להגיע אל השרת", + "MessageSetChaptersFromTracksDescription": "קבע פרקים באמצעות כל קובץ שמע כפרק וכותרת פרק כשם הקובץ שמע", + "MessageStartPlaybackAtTime": "האם אתה בטוח שברצונך להתחיל השמעה עבור \"{0}\" ב-{1}?", + "MessageThinking": "חושב...", + "MessageUploaderItemFailed": "העלאת הפריט נכשלה", + "MessageUploaderItemSuccess": "העלאה הצליחה!", + "MessageUploading": "מעלה...", + "MessageValidCronExpression": "ביטוי Cron חוקי", + "MessageWatcherIsDisabledGlobally": "שומר מנוטרל באופן גלובלי בהגדרות השרת", + "MessageXLibraryIsEmpty": "ספריית {0} ריקה!", + "MessageYourAudiobookDurationIsLonger": "הזמן של הספר השמע שלך ארוך יותר מהזמן שנמצא", + "MessageYourAudiobookDurationIsShorter": "הזמן של הספר השמע שלך קצר יותר מהזמן שנמצא", + "NoteChangeRootPassword": "המשתמש השורש הוא המשתמש היחיד שיכול להיות לו סיסמה ריקה", + "NoteChapterEditorTimes": "הערה: זמן ההתחלה של הפרק הראשון חייב להישאר 0:00 וזמן ההתחלה של הפרק האחרון לא יכול לחרוג מהזמן של ספר השמע הזה.", + "NoteFolderPicker": "הערה: תיקיות שכבר מוגדרות לא יוצגו", + "NoteRSSFeedPodcastAppsHttps": "אזהרה: רוב יישומי הפודקאסט דורשים שהכתובת URL של העדכון תשתמש ב HTTPS", + "NoteRSSFeedPodcastAppsPubDate": "אזהרה: פרק(ים) אחד או יותר לא מכילים תאריך פרסום. חלק מיישומי הפודקאסט דורשים זאת.", + "NoteUploaderFoldersWithMediaFiles": "תיקיות עם קבצי מדיה יעובדו כפריטי ספריה נפרדים.", + "NoteUploaderOnlyAudioFiles": "אם מעלים רק קבצי שמע, כל קובץ שמע יעובד כספר שמע נפרד.", + "NoteUploaderUnsupportedFiles": "קבצים לא נתמכים מתעלמים. בעת בחירת תיקייה או זריקתה, קבצים אחרים שאינם בתיקיית פריט מתעלמים.", + "PlaceholderNewCollection": "שם אוסף חדש", + "PlaceholderNewFolderPath": "נתיב תיקייה חדשה", + "PlaceholderNewPlaylist": "שם רשימת השמעה חדשה", + "PlaceholderSearch": "חיפוש..", + "PlaceholderSearchEpisode": "חיפוש פרק..", + "ToastAccountUpdateFailed": "עדכון חשבון נכשל", + "ToastAccountUpdateSuccess": "חשבון עודכן בהצלחה", + "ToastAuthorImageRemoveFailed": "הסרת התמונה של המחבר נכשלה", + "ToastAuthorImageRemoveSuccess": "תמונת המחבר הוסרה בהצלחה", + "ToastAuthorUpdateFailed": "עדכון המחבר נכשל", + "ToastAuthorUpdateMerged": "המחבר ממוזג", + "ToastAuthorUpdateSuccess": "המחבר עודכן בהצלחה", + "ToastAuthorUpdateSuccessNoImageFound": "המחבר עודכן (תמונה לא נמצאה)", + "ToastBackupCreateFailed": "יצירת גיבוי נכשלה", + "ToastBackupCreateSuccess": "גיבוי נוצר בהצלחה", + "ToastBackupDeleteFailed": "מחיקת הגיבוי נכשלה", + "ToastBackupDeleteSuccess": "הגיבוי נמחק בהצלחה", + "ToastBackupRestoreFailed": "שחזור הגיבוי נכשל", + "ToastBackupUploadFailed": "העלאת הגיבוי נכשלה", + "ToastBackupUploadSuccess": "הגיבוי הועלה בהצלחה", + "ToastBatchUpdateFailed": "עדכון הצטברות נכשל", + "ToastBatchUpdateSuccess": "הצטברות עודכנה בהצלחה", + "ToastBookmarkCreateFailed": "יצירת סימניה נכשלה", + "ToastBookmarkCreateSuccess": "הסימניה נוספה בהצלחה", + "ToastBookmarkRemoveFailed": "הסרת הסימניה נכשלה", + "ToastBookmarkRemoveSuccess": "הסימניה הוסרה בהצלחה", + "ToastBookmarkUpdateFailed": "עדכון הסימניה נכשל", + "ToastBookmarkUpdateSuccess": "הסימניה עודכנה בהצלחה", + "ToastChaptersHaveErrors": "הפרקים מכילים שגיאות", + "ToastChaptersMustHaveTitles": "הפרקים חייבים לכלול כותרות", + "ToastCollectionItemsRemoveFailed": "הסרת הפריט(ים) מהאוסף נכשלה", + "ToastCollectionItemsRemoveSuccess": "הפריט(ים) הוסרו מהאוסף בהצלחה", + "ToastCollectionRemoveFailed": "מחיקת האוסף נכשלה", + "ToastCollectionRemoveSuccess": "האוסף הוסר בהצלחה", + "ToastCollectionUpdateFailed": "עדכון האוסף נכשל", + "ToastCollectionUpdateSuccess": "האוסף עודכן בהצלחה", + "ToastItemCoverUpdateFailed": "עדכון כיסוי הפריט נכשל", + "ToastItemCoverUpdateSuccess": "כיסוי הפריט עודכן בהצלחה", + "ToastItemDetailsUpdateFailed": "עדכון פרטי הפריט נכשל", + "ToastItemDetailsUpdateSuccess": "פרטי הפריט עודכנו בהצלחה", + "ToastItemDetailsUpdateUnneeded": "לא נדרשים עדכונים לפרטי הפריט", + "ToastItemMarkedAsFinishedFailed": "סימון כפריט הושלם נכשל", + "ToastItemMarkedAsFinishedSuccess": "הפריט סומן כהושלם בהצלחה", + "ToastItemMarkedAsNotFinishedFailed": "סימון כפריט שלא הושלם נכשל", + "ToastItemMarkedAsNotFinishedSuccess": "הפריט סומן כלא הושלם בהצלחה", + "ToastLibraryCreateFailed": "יצירת הספרייה נכשלה", + "ToastLibraryCreateSuccess": "הספרייה \"{0}\" נוצרה בהצלחה", + "ToastLibraryDeleteFailed": "מחיקת הספרייה נכשלה", + "ToastLibraryDeleteSuccess": "הספרייה נמחקה בהצלחה", + "ToastLibraryScanFailedToStart": "הפעלת הסריקה נכשלה", + "ToastLibraryScanStarted": "הסריקה של הספרייה החלה", + "ToastLibraryUpdateFailed": "עדכון הספרייה נכשל", + "ToastLibraryUpdateSuccess": "הספרייה \"{0}\" עודכנה בהצלחה", + "ToastPlaylistCreateFailed": "יצירת רשימת השמעה נכשלה", + "ToastPlaylistCreateSuccess": "רשימת השמעה נוצרה בהצלחה", + "ToastPlaylistRemoveFailed": "הסרת רשימת השמעה נכשלה", + "ToastPlaylistRemoveSuccess": "רשימת השמעה הוסרה בהצלחה", + "ToastPlaylistUpdateFailed": "עדכון רשימת השמעה נכשל", + "ToastPlaylistUpdateSuccess": "רשימת השמעה עודכנה בהצלחה", + "ToastPodcastCreateFailed": "יצירת הפודקאסט נכשלה", + "ToastPodcastCreateSuccess": "הפודקאסט נוצר בהצלחה", + "ToastRemoveItemFromCollectionFailed": "הסרת הפריט מהאוסף נכשלה", + "ToastRemoveItemFromCollectionSuccess": "הפריט הוסר מהאוסף בהצלחה", + "ToastRSSFeedCloseFailed": "סגירת הערוץ RSS נכשלה", + "ToastRSSFeedCloseSuccess": "הערוץ RSS נסגר בהצלחה", + "ToastSendEbookToDeviceFailed": "שליחת הספר אל המכשיר נכשלה", + "ToastSendEbookToDeviceSuccess": "הספר נשלח אל המכשיר \"{0}\"", + "ToastSeriesUpdateFailed": "עדכון הסדרה נכשל", + "ToastSeriesUpdateSuccess": "הסדרה עודכנה בהצלחה", + "ToastSessionDeleteFailed": "מחיקת הפעולה נכשלה", + "ToastSessionDeleteSuccess": "הפעולה נמחקה בהצלחה", + "ToastSocketConnected": "חיבור קצה התקשורת בוצע בהצלחה", + "ToastSocketDisconnected": "התנתקות קצה התקשורת בוצעה", + "ToastSocketFailedToConnect": "התחברות קצה התקשורת נכשלה", + "ToastUserDeleteFailed": "מחיקת המשתמש נכשלה", + "ToastUserDeleteSuccess": "המשתמש נמחק בהצלחה" +} \ No newline at end of file From 752268effbbf44b7fd4735993462c7afa484265b Mon Sep 17 00:00:00 2001 From: dor Date: Sat, 16 Mar 2024 21:00:44 +0200 Subject: [PATCH 0007/1695] mid point proofing --- client/strings/he.json | 198 ++++++++++++++++++++--------------------- 1 file changed, 99 insertions(+), 99 deletions(-) diff --git a/client/strings/he.json b/client/strings/he.json index 582663c6..63295c46 100644 --- a/client/strings/he.json +++ b/client/strings/he.json @@ -11,12 +11,12 @@ "ButtonAuthors": "יוצרים", "ButtonBrowseForFolder": "עיין בתיקייה", "ButtonCancel": "בטל", - "ButtonCancelEncode": "בטל הצפנה", - "ButtonChangeRootPassword": "שנה סיסמת שורש", + "ButtonCancelEncode": "בטל קידוד", + "ButtonChangeRootPassword": "שנה סיסמת root", "ButtonCheckAndDownloadNewEpisodes": "בדוק והורד פרקים חדשים", "ButtonChooseAFolder": "בחר תיקייה", "ButtonChooseFiles": "בחר קבצים", - "ButtonClearFilter": "נקה פילטר", + "ButtonClearFilter": "נקה סינון", "ButtonCloseFeed": "סגור פיד", "ButtonCollections": "אוספים", "ButtonConfigureScanner": "הגדר סורק", @@ -31,18 +31,18 @@ "ButtonFullPath": "נתיב מלא", "ButtonHide": "הסתר", "ButtonHome": "בית", - "ButtonIssues": "בעיות", + "ButtonIssues": "תקלות", "ButtonJumpBackward": "דלג אחורה", "ButtonJumpForward": "דלג קדימה", - "ButtonLatest": "אחרון", + "ButtonLatest": "חדש ביותר", "ButtonLibrary": "ספרייה", "ButtonLogout": "התנתק", "ButtonLookup": "חפש", "ButtonManageTracks": "נהל רצועות", - "ButtonMapChapterTitles": "מפה של כותרות פרק", + "ButtonMapChapterTitles": "מפה כותרות פרקים", "ButtonMatchAllAuthors": "התאם את כל היוצרים", "ButtonMatchBooks": "התאם ספרים", - "ButtonNevermind": "אל תדאג", + "ButtonNevermind": "לא משנה", "ButtonNext": "הבא", "ButtonNextChapter": "פרק הבא", "ButtonOk": "אישור", @@ -59,15 +59,15 @@ "ButtonPurgeMediaProgress": "נקה את ההתקדמות במדיה", "ButtonQueueAddItem": "הוסף לתור", "ButtonQueueRemoveItem": "הסר מהתור", - "ButtonQuickMatch": "התאם מהר", + "ButtonQuickMatch": "התאמה מהירה", "ButtonRead": "קרא", "ButtonRefresh": "רענן", "ButtonRemove": "הסר", "ButtonRemoveAll": "הסר הכל", "ButtonRemoveAllLibraryItems": "הסר את כל פריטי הספרייה", - "ButtonRemoveFromContinueListening": "הסר מההמשך להאזנה", - "ButtonRemoveFromContinueReading": "הסר מההמשך לקריאה", - "ButtonRemoveSeriesFromContinueSeries": "הסר סדרה מהמשך לסדרות", + "ButtonRemoveFromContinueListening": "הסר מ- המשך האזנה", + "ButtonRemoveFromContinueReading": "הסר מ- המשך קריאה", + "ButtonRemoveSeriesFromContinueSeries": "הסר סדרה מ- המשך סדרה", "ButtonReScan": "סרוק מחדש", "ButtonReset": "איפוס", "ButtonResetToDefault": "איפוס לברירת המחדל", @@ -80,12 +80,12 @@ "ButtonSearch": "חפש", "ButtonSelectFolderPath": "בחר נתיב לתיקייה", "ButtonSeries": "סדרה", - "ButtonSetChaptersFromTracks": "קבע פרקים מרצועות", - "ButtonShare": "שתף", + "ButtonSetChaptersFromTracks": "קבע פרקים לפי הרצועות", + "ButtonShare": "שיתוף", "ButtonShiftTimes": "הזז זמנים", "ButtonShow": "הצג", - "ButtonStartM4BEncode": "התחל הצפנה M4B", - "ButtonStartMetadataEmbed": "התחל הטמעת מטאדאטה", + "ButtonStartM4BEncode": "התחל קידוד M4B", + "ButtonStartMetadataEmbed": "התחל הטמעת מטא-נתונים", "ButtonSubmit": "שלח", "ButtonTest": "בדיקה", "ButtonUpload": "העלה", @@ -96,14 +96,14 @@ "ButtonUserEdit": "ערוך משתמש {0}", "ButtonViewAll": "הצג הכול", "ButtonYes": "כן", - "ErrorUploadFetchMetadataAPI": "שגיאה בשליפת מטאדאטה", - "ErrorUploadFetchMetadataNoResults": "לא ניתן לשלוף מטאדאטה - נסה לעדכן כותרת ו/או יוצר", - "ErrorUploadLacksTitle": "חייב להיות כותרת", + "ErrorUploadFetchMetadataAPI": "שגיאה בשליפת מטא-נתונים", + "ErrorUploadFetchMetadataNoResults": "לא ניתן לשלוף מטא-נתונים - נסה לעדכן כותרת ו/או יוצר", + "ErrorUploadLacksTitle": "חובה לתת כותרת", "HeaderAccount": "חשבון", "HeaderAdvanced": "מתקדם", - "HeaderAppriseNotificationSettings": "הגדרות התראה ב-Apprise", - "HeaderAudiobookTools": "כלים לניהול קבצי אודיו", - "HeaderAudioTracks": "רצועות אודיו", + "HeaderAppriseNotificationSettings": "הגדרות התראות של Apprise", + "HeaderAudiobookTools": "כלים לניהול קבצי ספרים קוליים", + "HeaderAudioTracks": "רצועות קול", "HeaderAuthentication": "אימות", "HeaderBackups": "גיבויים", "HeaderChangePassword": "שנה סיסמה", @@ -113,60 +113,60 @@ "HeaderCollectionItems": "פריטי אוסף", "HeaderCover": "כריכה", "HeaderCurrentDownloads": "הורדות נוכחיות", - "HeaderCustomMetadataProviders": "ספקי מטאדאטה מותאמים", + "HeaderCustomMetadataProviders": "ספקי מטא-נתונים מותאמים אישית", "HeaderDetails": "פרטים", "HeaderDownloadQueue": "תור הורדה", "HeaderEbookFiles": "קבצי ספר אלקטרוני", "HeaderEmail": "אימייל", "HeaderEmailSettings": "הגדרות אימייל", "HeaderEpisodes": "פרקים", - "HeaderEreaderDevices": "התקני קריאה", - "HeaderEreaderSettings": "הגדרות קריאה", + "HeaderEreaderDevices": "התקני קריאה דיגיטליים", + "HeaderEreaderSettings": "הגדרות התקני קריאה דיגיטליים", "HeaderFiles": "קבצים", "HeaderFindChapters": "מצא פרקים", "HeaderIgnoredFiles": "קבצים שנתעלמו", "HeaderItemFiles": "קבצי פריט", - "HeaderItemMetadataUtils": "כלי מטאדאטה פריט", + "HeaderItemMetadataUtils": "כלי מטא-נתונים", "HeaderLastListeningSession": "הפעלת האזנה אחרונה", - "HeaderLatestEpisodes": "הפרקים האחרונים", + "HeaderLatestEpisodes": "הפרקים העדכניים ביותר", "HeaderLibraries": "ספריות", "HeaderLibraryFiles": "קבצי ספרייה", "HeaderLibraryStats": "סטטיסטיקות ספרייה", "HeaderListeningSessions": "הפעלות האזנה", "HeaderListeningStats": "סטטיסטיקות האזנה", - "HeaderLogin": "כניסה", + "HeaderLogin": "התחברות", "HeaderLogs": "לוגים", "HeaderManageGenres": "נהל ז'אנרים", "HeaderManageTags": "נהל תגיות", "HeaderMapDetails": "מפה פרטים", "HeaderMatch": "התאם", - "HeaderMetadataOrderOfPrecedence": "סדר העדפת מטאדאטה", - "HeaderMetadataToEmbed": "מטאדאטה להטמעה", + "HeaderMetadataOrderOfPrecedence": "סדר העדפת מטא-נתונים", + "HeaderMetadataToEmbed": "מטא-נתונים להטמעה", "HeaderNewAccount": "חשבון חדש", "HeaderNewLibrary": "ספרייה חדשה", "HeaderNotifications": "התראות", "HeaderOpenIDConnectAuthentication": "אימות OpenID Connect", - "HeaderOpenRSSFeed": "פתח פיד RSS", + "HeaderOpenRSSFeed": "פתח ערוץ RSS", "HeaderOtherFiles": "קבצים אחרים", "HeaderPasswordAuthentication": "אימות סיסמה", "HeaderPermissions": "הרשאות", "HeaderPlayerQueue": "תור ניגון", "HeaderPlaylist": "רשימת השמעה", "HeaderPlaylistItems": "פריטי רשימת השמעה", - "HeaderPodcastsToAdd": "פודקאסטים להוסיף", + "HeaderPodcastsToAdd": "פודקאסטים להוספה", "HeaderPreviewCover": "תצוגה מקדימה של כריכה", "HeaderRemoveEpisode": "הסר פרק", "HeaderRemoveEpisodes": "הסר {0} פרקים", - "HeaderRSSFeedGeneral": "פיד RSS כללי", - "HeaderRSSFeedIsOpen": "הפיד RSS פתוח", - "HeaderRSSFeeds": "פידי RSS", + "HeaderRSSFeedGeneral": "פרטי ערוץ RSS", + "HeaderRSSFeedIsOpen": "ערוץ RSS פתוח", + "HeaderRSSFeeds": "ערוצי RSS", "HeaderSavedMediaProgress": "התקדמות מדיה שמורה", - "HeaderSchedule": "מתזמן", + "HeaderSchedule": "תיזמון", "HeaderScheduleLibraryScans": "קבע סריקות ספרייה אוטומטיות", "HeaderSession": "הפעלה", - "HeaderSetBackupSchedule": "קבע מתז גיבוי", + "HeaderSetBackupSchedule": "קבע לוח זמנים לגיבוי", "HeaderSettings": "הגדרות", - "HeaderSettingsDisplay": "הצגה", + "HeaderSettingsDisplay": "תצוגה", "HeaderSettingsExperimental": "תכונות ניסיוניות", "HeaderSettingsGeneral": "כללי", "HeaderSettingsScanner": "סורק", @@ -175,7 +175,7 @@ "HeaderStatsLongestItems": "הפריטים הארוכים ביותר (בשעות)", "HeaderStatsMinutesListeningChart": "דקות האזנה (בימים האחרונים)", "HeaderStatsRecentSessions": "הפעלות אחרונות", - "HeaderStatsTop10Authors": "היוצרים המובילים 10", + "HeaderStatsTop10Authors": "10 היוצרים המובילים", "HeaderStatsTop5Genres": "הז'אנרים המובילים 5", "HeaderTableOfContents": "תוכן העניינים", "HeaderTools": "כלים", @@ -201,19 +201,19 @@ "LabelAdminUsersOnly": "רק מנהלים", "LabelAll": "הכל", "LabelAllUsers": "כל המשתמשים", - "LabelAllUsersExcludingGuests": "כל המשתמשים ללא אורחים", + "LabelAllUsersExcludingGuests": "כל המשתמשים, ללא אורחים", "LabelAllUsersIncludingGuests": "כל המשתמשים כולל אורחים", - "LabelAlreadyInYourLibrary": "כבר בספרייה שלך", + "LabelAlreadyInYourLibrary": "כבר קיים בספרייה שלך", "LabelAppend": "הוסף לסוף", "LabelAuthor": "יוצר", "LabelAuthorFirstLast": "יוצר (שם פרטי שם משפחה)", "LabelAuthorLastFirst": "יוצר (שם משפחה, שם פרטי)", "LabelAuthors": "יוצרים", "LabelAutoDownloadEpisodes": "הורד פרקים באופן אוטומטי", - "LabelAutoFetchMetadata": "שלף מטאדאטה באופן אוטומטי", - "LabelAutoFetchMetadataHelp": "משיג מטאדאטה עבור כותרת, יוצר וסדרה כדי לשפר את תהליך ההעלאה. ייתכן שיהיה על יכולתך להתאים מטאדאטה נוספת לאחר ההעלאה.", + "LabelAutoFetchMetadata": "חפש והורד מטא-נתונים באופן אוטומטי", + "LabelAutoFetchMetadataHelp": "מחפש ומוריד מטא-נתונים לשדות כותרת, יוצר וסדרה כדי לשפר את תהליך ההעלאה. ייתכן שיהיה צורך להתאים מטא-נתונים נוסף לאחר ההעלאה.", "LabelAutoLaunch": "הפעלה אוטומטית", - "LabelAutoLaunchDescription": "הפניה אוטומטית לספק האימות כאשר מגיעים לדף ההתחברות (ניתן להפעיל ידנית במסלול /login?autoLaunch=0)", + "LabelAutoLaunchDescription": "הפניה אוטומטית לספק האימות כאשר מגיעים לדף ההתחברות (ניתן להפעיל ידנית בכתובת /login?autoLaunch=0)", "LabelAutoRegister": "הרשמה אוטומטית", "LabelAutoRegisterDescription": "יצירת משתמשים חדשים אוטומטית לאחר התחברות", "LabelBackToUser": "חזרה למשתמש", @@ -223,18 +223,18 @@ "LabelBackupsMaxBackupSize": "גודל הגיבוי המרבי (בג'יגה-בייט)", "LabelBackupsMaxBackupSizeHelp": "כהגנה על עצמך מפני תצורה שגויה, הגיבויים ייכשלו אם הם יעברו את הגודל שהוגדר.", "LabelBackupsNumberToKeep": "מספר הגיבויים לשמירה", - "LabelBackupsNumberToKeepHelp": "יש להסיר רק גיבוי אחד בכל פעם, לכן אם יש לך כבר יותר מגיבוי אחד יש להסיר אותם באופן ידני.", - "LabelBitrate": "ביטרייט", + "LabelBackupsNumberToKeepHelp": "רק גיבוי אחד יוסר בכל פעם, לכן אם יש לך כבר יותר מגיבוי אחד יש להסיר אותם באופן ידני.", + "LabelBitrate": "קצב סיביות", "LabelBooks": "ספרים", "LabelButtonText": "טקסט לחצן", "LabelChangePassword": "שינוי סיסמה", "LabelChannels": "ערוצים", "LabelChapters": "פרקים", - "LabelChaptersFound": "פרקים נמצאו", + "LabelChaptersFound": "פרקים שנמצאו", "LabelChapterTitle": "כותרת הפרק", "LabelClickForMoreInfo": "לחץ למידע נוסף", "LabelClosePlayer": "סגור נגן", - "LabelCodec": "קודק", + "LabelCodec": "Codec", "LabelCollapseSeries": "צמצום סדרה", "LabelCollection": "אוסף", "LabelCollections": "אוספים", @@ -243,15 +243,15 @@ "LabelContinueListening": "המשך האזנה", "LabelContinueReading": "המשך קריאה", "LabelContinueSeries": "המשך סדרה", - "LabelCover": "כיסוי", - "LabelCoverImageURL": "כתובת התמונה המצויה ברשת", + "LabelCover": "כריכה", + "LabelCoverImageURL": "כתובת התמונה ברשת", "LabelCreatedAt": "נוצר בתאריך", - "LabelCronExpression": "ביטוי קרון", + "LabelCronExpression": "Cron Expression", "LabelCurrent": "נוכחי", "LabelCurrently": "כעת:", - "LabelCustomCronExpression": "ביטוי קרון מותאם אישית:", - "LabelDatetime": "תאריך ושעה", - "LabelDeleteFromFileSystemCheckbox": "מחיקה מהמערכת הקבצים (הסר סימון למחיקה רק מהמסד נתונים)", + "LabelCustomCronExpression": "Custom Cron Expression:", + "LabelDatetime": "Datetime", + "LabelDeleteFromFileSystemCheckbox": "מחיקה מהמערכת הקבצים (הסר סימון למחיקה רק ממסד הנתונים)", "LabelDescription": "תיאור", "LabelDeselectAll": "הסר בחירת כל הפריטים", "LabelDevice": "התקן", @@ -269,27 +269,27 @@ "LabelEbooks": "ספרים אלקטרוניים", "LabelEdit": "עריכה", "LabelEmail": "דואר אלקטרוני", - "LabelEmailSettingsFromAddress": "כתובת מאיתה", + "LabelEmailSettingsFromAddress": "מאת", "LabelEmailSettingsSecure": "מאובטח", - "LabelEmailSettingsSecureHelp": "אם נכון, החיבור ישתמש ב-TLS בעת התחברות לשרת. אם לא, תישתמש חיבור זה ב-TLS אם השרת תומך בהרחבת STARTTLS. ברוב המקרים עדיף להגדיר ערך זה כנכון אם אתה מחבר לפורט 465. לפורט 587 או 25 שמור על ערך זה כשקר.", - "LabelEmailSettingsTestAddress": "כתובת לבדיקת מבנה", - "LabelEmbeddedCover": "כיסוי משובץ", + "LabelEmailSettingsSecureHelp": "אם מופעל, החיבור ישתמש ב-TLS בעת ההתחברות לשרת. אם לא, אז TLS יהיה בשימוש אם השרת תומך בהרחבת STARTTLS. ברוב המקרים מומלץ להפעיל את הגדרה זו אם אתה מתחבר לפורט 465. לפורט 587 או 25, השאר כבוי. (from nodemailer.com/smtp/#authentication)", + "LabelEmailSettingsTestAddress": "כתובת לבדיקה", + "LabelEmbeddedCover": "כריכה מוטמעת", "LabelEnable": "הפעל", "LabelEnd": "סיום", "LabelEpisode": "פרק", "LabelEpisodeTitle": "כותרת הפרק", "LabelEpisodeType": "סוג הפרק", "LabelExample": "דוגמה", - "LabelExplicit": "ברור", + "LabelExplicit": "בוטה", "LabelFeedURL": "כתובת ערוץ", - "LabelFetchingMetadata": "מושכים מטא-נתונים", + "LabelFetchingMetadata": "מושך מטא-נתונים", "LabelFile": "קובץ", - "LabelFileBirthtime": "זמן הולדת הקובץ", - "LabelFileModified": "הקובץ הוחלף", + "LabelFileBirthtime": "זמן יצירת הקובץ", + "LabelFileModified": "הקובץ שונה", "LabelFilename": "שם הקובץ", "LabelFilterByUser": "סינון לפי משתמש", "LabelFindEpisodes": "מצא פרקים", - "LabelFinished": "סיים", + "LabelFinished": "הושלם", "LabelFolder": "תיקייה", "LabelFolders": "תיקיות", "LabelFontBold": "מודגש", @@ -300,10 +300,10 @@ "LabelFormat": "תבנית", "LabelGenre": "ז'אנר", "LabelGenres": "ז'אנרים", - "LabelHardDeleteFile": "מחיקה קשה של הקובץ", - "LabelHasEbook": "יש ספר אלקטרוני", - "LabelHasSupplementaryEbook": "יש ספר אלקטרוני תוספתי", - "LabelHighestPriority": "עדיפות הגבוהה ביותר", + "LabelHardDeleteFile": "מחיקה חזקה של הקובץ", + "LabelHasEbook": "ספר אלקטרוני קיים", + "LabelHasSupplementaryEbook": "קיים ספר אלקטרוני נלווה", + "LabelHighestPriority": "העדיפות הגבוהה ביותר", "LabelHost": "מארח", "LabelHour": "שעה", "LabelIcon": "סמל", @@ -315,7 +315,7 @@ "LabelIntervalCustomDailyWeekly": "מותאם אישית יומי/שבועי", "LabelIntervalEvery12Hours": "כל 12 שעות", "LabelIntervalEvery15Minutes": "כל 15 דקות", - "LabelIntervalEvery2Hours": "כל 2 שעות", + "LabelIntervalEvery2Hours": "כל שעתיים", "LabelIntervalEvery30Minutes": "כל 30 דקות", "LabelIntervalEvery6Hours": "כל 6 שעות", "LabelIntervalEveryDay": "כל יום", @@ -324,11 +324,11 @@ "LabelInvert": "הפוך", "LabelItem": "פריט", "LabelLanguage": "שפה", - "LabelLanguageDefaultServer": "שפת השרת ברירת המחדל", - "LabelLastBookAdded": "הספר האחרון שהוסף", + "LabelLanguageDefaultServer": "שפת ברירת המחדל של השרת", + "LabelLastBookAdded": "הספר האחרון שנוסף", "LabelLastBookUpdated": "הספר האחרון שעודכן", "LabelLastSeen": "נראה לאחרונה", - "LabelLastTime": "הפעם האחרונה", + "LabelLastTime": "הזמן האחרון", "LabelLastUpdate": "עדכון אחרון", "LabelLayout": "פריסה", "LabelLayoutSinglePage": "דף בודד", @@ -339,15 +339,15 @@ "LabelLibraryItem": "פריט ספרייה", "LabelLibraryName": "שם הספרייה", "LabelLimit": "מגבלה", - "LabelLineSpacing": "רווח שורות", + "LabelLineSpacing": "ריווח שורות", "LabelListenAgain": "האזן שוב", "LabelLogLevelDebug": "דיבוג", "LabelLogLevelInfo": "מידע", "LabelLogLevelWarn": "אזהרה", "LabelLookForNewEpisodesAfterDate": "חפש פרקים חדשים לאחר תאריך זה", - "LabelLowestPriority": "עדיפות הנמוכה ביותר", + "LabelLowestPriority": "העדיפות הנמוכה ביותר", "LabelMatchExistingUsersBy": "התאם משתמשים קיימים לפי", - "LabelMatchExistingUsersByDescription": "משמש לחיבור משתמשים קיימים. לאחר החיבור, המשתמשים יתאמו לפי זיהוי ייחודי מספק ה-SSO שלך", + "LabelMatchExistingUsersByDescription": "משמש לחיבור משתמשים קיימים. לאחר החיבור, המשתמשים יותאמו לפי זיהוי ייחודי מספק ה-SSO שלך", "LabelMediaPlayer": "נגן מדיה", "LabelMediaType": "סוג מדיה", "LabelMetadataOrderOfPrecedenceDescription": "מקורות המטא-נתונים עם עדיפות גבוהה יחליפו מקורות עם עדיפות נמוכה יותר", @@ -364,25 +364,25 @@ "LabelMore": "עוד", "LabelMoreInfo": "מידע נוסף", "LabelName": "שם", - "LabelNarrator": "נרטור", - "LabelNarrators": "נרטורים", + "LabelNarrator": "מספר", + "LabelNarrators": "מספרים", "LabelNew": "חדש", - "LabelNewestAuthors": "סופרים החדשים ביותר", + "LabelNewestAuthors": "הסופרים החדשים ביותר", "LabelNewestEpisodes": "הפרקים החדשים ביותר", "LabelNewPassword": "סיסמה חדשה", - "LabelNextBackupDate": "תאריך גיבוי הבא", + "LabelNextBackupDate": "תאריך הגיבוי הבא", "LabelNextScheduledRun": "הרצה מתוזמנת הבאה", "LabelNoEpisodesSelected": "לא נבחרו פרקים", "LabelNotes": "הערות", "LabelNotFinished": "לא הושלם", - "LabelNotificationAppriseURL": "כתובת URL של התראה", + "LabelNotificationAppriseURL": "כתובות Apprise", "LabelNotificationAvailableVariables": "משתנים זמינים", "LabelNotificationBodyTemplate": "תבנית גוף", "LabelNotificationEvent": "אירוע התראה", "LabelNotificationsMaxFailedAttempts": "מספר הניסיונות הנכשלים המרבי", - "LabelNotificationsMaxFailedAttemptsHelp": "ההתראות מושבתות לאחר שהן נכשלות לשלוח מספר זה פעמים", + "LabelNotificationsMaxFailedAttemptsHelp": "ההתראות מושבתות לאחר שהן נכשלות לשלוח מספר פעמים זה", "LabelNotificationsMaxQueueSize": "גודל התור המרבי לאירועי התראה", - "LabelNotificationsMaxQueueSizeHelp": "האירועים מוגבלים לשליחה אחת לשנייה. האירועים יתעלמו אם התור מלא בגודלו המרבי. זה מונע ספאם בהתראות.", + "LabelNotificationsMaxQueueSizeHelp": "האירועים מוגבלים לשליחה אחת לשנייה. האירועים יתעלמו אם התור מלא. הגדרה זו נועדה למנוע ספאם התראות.", "LabelNotificationTitleTemplate": "תבנית כותרת", "LabelNotStarted": "לא התחיל", "LabelNumberOfBooks": "מספר הספרים", @@ -393,11 +393,11 @@ "LabelPath": "נתיב", "LabelPermissionsAccessAllLibraries": "ניתן לגשת לכל הספריות", "LabelPermissionsAccessAllTags": "ניתן לגשת לכל התגיות", - "LabelPermissionsAccessExplicitContent": "ניתן לגשת לתוכן מפורט", - "LabelPermissionsDelete": "ניתן למחוק", - "LabelPermissionsDownload": "ניתן להוריד", - "LabelPermissionsUpdate": "ניתן לעדכן", - "LabelPermissionsUpload": "ניתן להעלות", + "LabelPermissionsAccessExplicitContent": "ניתן לגשת לתוכן בוטה", + "LabelPermissionsDelete": "מותר למחוק", + "LabelPermissionsDownload": "מותר להוריד", + "LabelPermissionsUpdate": "מותר לעדכן", + "LabelPermissionsUpload": "מותר להעלות", "LabelPersonalYearReview": "השנה שלך בסקירה ({0})", "LabelPhotoPathURL": "נתיב/URL לתמונה", "LabelPlaylists": "רשימות השמעה", @@ -406,9 +406,9 @@ "LabelPodcasts": "פודקאסטים", "LabelPodcastSearchRegion": "אזור חיפוש פודקאסט", "LabelPodcastType": "סוג פודקאסט", - "LabelPort": "יציאה", - "LabelPrefixesToIgnore": "קידומות להתעלמות (קסה אינסנסיטיבית)", - "LabelPreventIndexing": "מנע את האינדוקסציה של הפיד שלך על ידי ספריות אייטונס וגוגל פודקאסט", + "LabelPort": "פורט", + "LabelPrefixesToIgnore": "קידומות להתעלמות (מתעלם מאותיות גדולות/קטנות)", + "LabelPreventIndexing": "מנע רישום של הערוץ שלך על ידי ספריות אייטונס וגוגל פודקאסט", "LabelPrimaryEbook": "ספר אלקטרוני ראשי", "LabelProgress": "התקדמות", "LabelProvider": "ספק", @@ -417,21 +417,21 @@ "LabelPublishYear": "שנת הפרסום", "LabelRead": "קריאה", "LabelReadAgain": "קרא שוב", - "LabelReadEbookWithoutProgress": "קרוא ספר אלקטרוני בלי לשמור התקדמות", + "LabelReadEbookWithoutProgress": "קרא/י ספר אלקטרוני ללא שמירת התקדמות", "LabelRecentlyAdded": "נוסף לאחרונה", "LabelRecentSeries": "סדרות אחרונות", "LabelRecommended": "מומלץ", "LabelRedo": "עשה שוב", "LabelRegion": "אזור", - "LabelReleaseDate": "תאריך שחרור", - "LabelRemoveCover": "הסר כיסוי", + "LabelReleaseDate": "תאריך הוצאה לאור", + "LabelRemoveCover": "הסר כריכה", "LabelRowsPerPage": "שורות לעמוד", "LabelRSSFeedCustomOwnerEmail": "אימייל בעלים מותאם אישית", "LabelRSSFeedCustomOwnerName": "שם בעלים מותאם אישית", "LabelRSSFeedOpen": "פתח ערוץ RSS", - "LabelRSSFeedPreventIndexing": "מנע אינדוקסציה", - "LabelRSSFeedSlug": "שם תמצאות RSS", - "LabelRSSFeedURL": "URL של פיד RSS", + "LabelRSSFeedPreventIndexing": "מנע רישום", + "LabelRSSFeedSlug": "Slug של ערוץ ה-RSS", + "LabelRSSFeedURL": "כתובת ערוץ ה-RSS", "LabelSearchTerm": "מונח חיפוש", "LabelSearchTitle": "כותרת חיפוש", "LabelSearchTitleOrASIN": "כותרת חיפוש או ASIN", @@ -449,11 +449,11 @@ "LabelSetEbookAsSupplementary": "קבע כספר אלקטרוני נלווה", "LabelSettingsAudiobooksOnly": "רק ספרי קול", "LabelSettingsAudiobooksOnlyHelp": "הפעלת ההגדרה הזו תתעלם מקבצי ספרים אלקטרוניים אלא אם כן הם נמצאים בתיקיית ספרי קול, שבמקרה זה יקבעו כספרים אלקטרוניים נלווים", - "LabelSettingsBookshelfViewHelp": "עיצוב סקיומורפי עם מדפים עץ", - "LabelSettingsChromecastSupport": "תמיכת Chromecast", + "LabelSettingsBookshelfViewHelp": "עיצוב סקאומורפי עם מדפי עץ", + "LabelSettingsChromecastSupport": "תמיכה ב-Chromecast", "LabelSettingsDateFormat": "פורמט תאריך", - "LabelSettingsDisableWatcher": "השבת צופה", - "LabelSettingsDisableWatcherForLibrary": "השבת צופה תיקייה עבור ספרייה", + "LabelSettingsDisableWatcher": "השבת עוקב", + "LabelSettingsDisableWatcherForLibrary": "השבת עוקב תיקייה עבור ספרייה", "LabelSettingsDisableWatcherHelp": "מבטל את הוספת/עדכון אוטומטי של פריטים כאשר שינויי קבצים זוהים. *דורש איתחול שרת", "LabelSettingsEnableWatcher": "הפעל צופה", "LabelSettingsEnableWatcherForLibrary": "הפעל צופה תיקייה עבור ספרייה", @@ -679,7 +679,7 @@ "MessageReportBugsAndContribute": "דווח על באגים, בקש תכונות חדשות, ותרום ב-", "MessageResetChaptersConfirm": "האם אתה בטוח שברצונך לאפס את הפרקים ולבטל את השינויים שביצעת?", "MessageRestoreBackupConfirm": "האם אתה בטוח שברצונך לשחזר את הגיבוי שנוצר ב", - "MessageRestoreBackupWarning": "שחזור גיבוי ימחק את כל מסד הנתונים השוכן ב /config ואת תמונות הכריכה ב- /metadata/items & /metadata/authors.

גיבויים אינם משנים קבצים בתיקיות הספרייה שלך. אם הגדרות השרת מופעלות כדי לאחסן תמונות כריכה ומטאדאטה בתיקיות הספרייה שלך אז אלה לא יגובו או ימחקו.

כל הלקוחות המשתמשים בשרת שלך יתעדכנו באופן אוטומטי.", + "MessageRestoreBackupWarning": "שחזור גיבוי ימחק את כל מסד הנתונים השוכן ב /config ואת תמונות הכריכה ב- /metadata/items & /metadata/authors.

גיבויים אינם משנים קבצים בתיקיות הספרייה שלך. אם הגדרות השרת מופעלות כדי לאחסן תמונות כריכה ומטא-נתונים בתיקיות הספרייה שלך אז אלה לא יגובו או ימחקו.

כל הלקוחות המשתמשים בשרת שלך יתעדכנו באופן אוטומטי.", "MessageSearchResultsFor": "תוצאות חיפוש עבור", "MessageSelected": "{0} נבחרו", "MessageServerCouldNotBeReached": "לא ניתן להגיע אל השרת", From d562f6a69f4dc7c0538065b34530f757e7f3acc9 Mon Sep 17 00:00:00 2001 From: mikiher Date: Sun, 17 Mar 2024 07:36:13 +0200 Subject: [PATCH 0008/1695] Change unit-tests.yml workflow to include conditional checkout step --- .github/workflows/unit-tests.yml | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index 24f6398c..80b9855d 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -11,13 +11,19 @@ on: jobs: run-unit-tests: + name: Run Unit Tests runs-on: ubuntu-latest steps: - - name: Checkout code + - name: Checkout (push/pull request) + uses: actions/checkout@v4 + if: github.event_name != 'workflow_dispatch' + + - name: Checkout (workflow_dispatch) uses: actions/checkout@v4 with: - ref: ${{ github.event_name != 'workflow_dispatch' && github.ref_name || inputs.ref}} + ref: inputs.ref + if: github.event_name == 'workflow_dispatch' - name: Set up Node.js uses: actions/setup-node@v4 From f938fca2c701ee26e0843a3a8a9eeb5158109828 Mon Sep 17 00:00:00 2001 From: mikiher Date: Sun, 17 Mar 2024 07:57:28 +0200 Subject: [PATCH 0009/1695] Fix bug in workflow_dispatch checkout step --- .github/workflows/unit-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index 80b9855d..695696c6 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -22,7 +22,7 @@ jobs: - name: Checkout (workflow_dispatch) uses: actions/checkout@v4 with: - ref: inputs.ref + ref: ${{ inputs.ref }} if: github.event_name == 'workflow_dispatch' - name: Set up Node.js From be4eb28b210f430bbe9f12221288ba927d987c86 Mon Sep 17 00:00:00 2001 From: dor Date: Sun, 17 Mar 2024 22:25:49 +0200 Subject: [PATCH 0010/1695] finished proofing Hebrew translation --- client/strings/he.json | 190 ++++++++++++++++++++--------------------- 1 file changed, 95 insertions(+), 95 deletions(-) diff --git a/client/strings/he.json b/client/strings/he.json index 63295c46..435f60ad 100644 --- a/client/strings/he.json +++ b/client/strings/he.json @@ -455,32 +455,32 @@ "LabelSettingsDisableWatcher": "השבת עוקב", "LabelSettingsDisableWatcherForLibrary": "השבת עוקב תיקייה עבור ספרייה", "LabelSettingsDisableWatcherHelp": "מבטל את הוספת/עדכון אוטומטי של פריטים כאשר שינויי קבצים זוהים. *דורש איתחול שרת", - "LabelSettingsEnableWatcher": "הפעל צופה", - "LabelSettingsEnableWatcherForLibrary": "הפעל צופה תיקייה עבור ספרייה", + "LabelSettingsEnableWatcher": "הפעל עוקב", + "LabelSettingsEnableWatcherForLibrary": "הפעל עוקב תיקייה עבור ספרייה", "LabelSettingsEnableWatcherHelp": "מאפשר הוספת/עדכון אוטומטי של פריטים כאשר שינויי קבצים זוהים. *דורש איתחול שרת", "LabelSettingsExperimentalFeatures": "תכונות ניסיוניות", "LabelSettingsExperimentalFeaturesHelp": "תכונות בפיתוח שדורשות משובך ובדיקה. לחץ לפתיחת דיון ב-GitHub.", - "LabelSettingsFindCovers": "מצא כיסויים", - "LabelSettingsFindCoversHelp": "אם לספר השמע שלך אין כיסוי מוטמע או תמונת כיסוי בתיקייה, הסורק ינסה למצוא כיסוי.
שים לב: זה יאריך את זמן הסריקה", - "LabelSettingsHideSingleBookSeries": "הסתר סדרות ספר אחד", + "LabelSettingsFindCovers": "מצא כריכות", + "LabelSettingsFindCoversHelp": "אם לספר הקולי שלך אין כריכה מוטמעת או תמונת כריכה בתיקייה, הסורק ינסה למצוא תמונת כריכה.
שים לב: זה יאריך את זמן הסריקה", + "LabelSettingsHideSingleBookSeries": "הסתר סדרות עם ספר אחד", "LabelSettingsHideSingleBookSeriesHelp": "סדרות הכוללות ספר אחד יוסתרו מדף הסדרות ומדף הבית.", - "LabelSettingsHomePageBookshelfView": "השתמש בתצוגת מדף על דף הבית", + "LabelSettingsHomePageBookshelfView": "השתמש בתצוגת מדף בדף הבית", "LabelSettingsLibraryBookshelfView": "השתמש בתצוגת מדף בספרייה", - "LabelSettingsOnlyShowLaterBooksInContinueSeries": "דלג על ספרים קודמים בהמשך סדרות", - "LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "מדף המשך סדרות מציג את הספר הראשון שלא התחיל בסדרה שיש לפחות ספר אחד שהושלם ואין ספרים בתהליך. הפעלת ההגדרה הזו תמשיך סדרות מהספר שהושלם הכי הרחק במקום מהספר הראשון שלא התחיל.", + "LabelSettingsOnlyShowLaterBooksInContinueSeries": "דלג על ספרים קודמים ב-המשך סדרה", + "LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "מדף המשך סדרות מציג את הספר הראשון שלא הושמע בסדרה שיש בה לפחות ספר אחד שהושלם ואין ספרים שכבר באמצע שמיעה. הפעלת הגדרה זו תמשיך סדרות מהספר שהושלם הכי מתקדם בסדרה במקום מהספר הראשון שלא הושמע.", "LabelSettingsParseSubtitles": "פענח כתוביות", "LabelSettingsParseSubtitlesHelp": "העתק כותרת משנה משם תיקיית הספר.
כותרת המשנה חייבת להיות מופרדת עם התו ״-״
לדוגמא, כותרת המשנה לספר ״שם הספר - כותרת משנה״, היא ״כותרת משנה״", - "LabelSettingsPreferMatchedMetadata": "עדיף מטה-נתונים מתואמים", - "LabelSettingsPreferMatchedMetadataHelp": "נתונים מתואמים ידריך פרטי פריט כאשר משתמשים בהתאמה מהירה. כברירת מחדל, התאמה מהירה תמלא פרטים חסרים בלבד.", + "LabelSettingsPreferMatchedMetadata": "העדף מטה-נתונים מותאמים", + "LabelSettingsPreferMatchedMetadataHelp": "נתונים מותאמים יועדפו על פני פרטים שכבר מוטמעים בפריט כאשר התאמה מהירה בשימוש. כברירת מחדל, התאמה מהירה תמלא פרטים חסרים בלבד.", "LabelSettingsSkipMatchingBooksWithASIN": "דלג על ספרים שכבר יש להם ASIN", "LabelSettingsSkipMatchingBooksWithISBN": "דלג על ספרים שכבר יש להם ISBN", "LabelSettingsSortingIgnorePrefixes": "התעלם מקידומות במיון", "LabelSettingsSortingIgnorePrefixesHelp": "לדוגמא, לקידומת ״ה״ שם הספר, שם הספר ימוין בתור ״שם הספר״, ״ה״", - "LabelSettingsSquareBookCovers": "השתמש בכיסויים מרובעים לספרים", - "LabelSettingsSquareBookCoversHelp": "מעדיף להשתמש בכיסויים מרובעים מעל כיסויים סטנדרטיים ביחס 1.6:1", - "LabelSettingsStoreCoversWithItem": "אחסן כיסויים עם פריט", + "LabelSettingsSquareBookCovers": "השתמש בכריכות מרובעות לספרים", + "LabelSettingsSquareBookCoversHelp": "השתמש בכריכות מרובעות על פני בכריכות סטנדרטיות ביחס 1.6:1", + "LabelSettingsStoreCoversWithItem": "אחסן תמונת כריכה עם הפריט", "LabelSettingsStoreCoversWithItemHelp": "כברירת מחדל, צילומי כריכות נשמרים בתיקיית /metadata/items, לאחר הפעלת הגדרה זו צילומי כריכות יישמרו בתיקיית הספר, רק קובץ אחד בשם ״cover״ יישמר", - "LabelSettingsStoreMetadataWithItem": "אחסן מטה-נתונים עם פריט", + "LabelSettingsStoreMetadataWithItem": "אחסן מטה-נתונים עם הפריט", "LabelSettingsStoreMetadataWithItemHelp": "כברירת מחדל, קבצי מטה-נתונים מאוחסנים ב- /metadata/items, הפעלת ההגדרה תאחסן קבצי מטה-נתונים בתיקיית פריט שלך בספרייה", "LabelSettingsTimeFormat": "פורמט זמן", "LabelShowAll": "הצג הכל", @@ -496,24 +496,24 @@ "LabelStatsBestDay": "היום הטוב ביותר", "LabelStatsDailyAverage": "ממוצע יומי", "LabelStatsDays": "ימים", - "LabelStatsDaysListened": "ימים שהוקשבו", + "LabelStatsDaysListened": "מספר ימים בהם נשמע ספר", "LabelStatsHours": "שעות", "LabelStatsInARow": "ברצף", - "LabelStatsItemsFinished": "פריטים שסיימו", + "LabelStatsItemsFinished": "פריטים שסיימת", "LabelStatsItemsInLibrary": "פריטים בספרייה", "LabelStatsMinutes": "דקות", "LabelStatsMinutesListening": "דקות האזנה", "LabelStatsOverallDays": "ימים כולל", "LabelStatsOverallHours": "שעות כולל", "LabelStatsWeekListening": "האזנה שבועית", - "LabelSubtitle": "תת כותרת", + "LabelSubtitle": "כותרת משנה", "LabelSupportedFileTypes": "סוגי קבצים נתמכים", "LabelTag": "תג", "LabelTags": "תגיות", "LabelTagsAccessibleToUser": "תגיות נגישות למשתמש", "LabelTagsNotAccessibleToUser": "תגיות לא נגישות למשתמש", "LabelTasks": "משימות פעילות", - "LabelTextEditorBulletedList": "רשימה עם נקודות", + "LabelTextEditorBulletedList": "רשימת נקודות", "LabelTextEditorLink": "קישור", "LabelTextEditorNumberedList": "רשימה ממוספרת", "LabelTextEditorUnlink": "ביטול קישור", @@ -527,100 +527,100 @@ "LabelTimeToShift": "זמן להיסט בשניות", "LabelTitle": "כותרת", "LabelToolsEmbedMetadata": "הטמעת מטה-נתונים", - "LabelToolsEmbedMetadataDescription": "הטמעת מטה-נתונים לקבצי שמע כולל תמונות שער ופרקים.", + "LabelToolsEmbedMetadataDescription": "הטמעת מטה-נתונים לקבצי שמע כולל תמונות כריכה ופרקים.", "LabelToolsMakeM4b": "יצירת קובץ אודיו M4B", "LabelToolsMakeM4bDescription": "יצירת קובץ אודיו .M4B עם מטה-נתונים מוטמעים, תמונת שער ופרקים.", "LabelToolsSplitM4b": "פיצול M4B ל-MP3", "LabelToolsSplitM4bDescription": "יצירת קבצי MP3 מ-M4B מפוצל לפי פרקים עם מטה-נתונים מוטמעים, תמונת שער ופרקים.", "LabelTotalDuration": "משך כולל", - "LabelTotalTimeListened": "סך הזמן שהוקשב", - "LabelTrackFromFilename": "מסלול משמות קבצים", - "LabelTrackFromMetadata": "מסלול ממטה-נתונים", - "LabelTracks": "מסלולים", - "LabelTracksMultiTrack": "מסלול רב-ערוצים", - "LabelTracksNone": "אין מסלולים", - "LabelTracksSingleTrack": "מסלול יחיד", + "LabelTotalTimeListened": "סך הזמן שהקשבת", + "LabelTrackFromFilename": "רצועות משמות קבצים", + "LabelTrackFromMetadata": "רצועות ממטה-נתונים", + "LabelTracks": "רצועות", + "LabelTracksMultiTrack": "רב-ערוצי", + "LabelTracksNone": "אין ערוצים", + "LabelTracksSingleTrack": "רצועה יחידה", "LabelType": "סוג", "LabelUnabridged": "לא מקוצר", "LabelUndo": "בטל", "LabelUnknown": "לא ידוע", "LabelUpdateCover": "עדכן כריכה", - "LabelUpdateCoverHelp": "אפשר כיסוי מעלה של כריכות קיימות עבור הספרים הנבחרים כאשר נמצאה התאמה", - "LabelUpdatedAt": "עודכן ב", + "LabelUpdateCoverHelp": "אפשר החלפה של כריכות קיימות עבור הספרים הנבחרים כאשר נמצאה התאמה", + "LabelUpdatedAt": "עודכן ב-", "LabelUpdateDetails": "עדכון פרטים", - "LabelUpdateDetailsHelp": "אפשר עדכון מעלה של פרטים קיימים עבור הספרים הנבחרים כאשר נמצאה התאמה", + "LabelUpdateDetailsHelp": "אפשר החלפה של פרטים קיימים עבור הספרים הנבחרים כאשר נמצאה התאמה", "LabelUploaderDragAndDrop": "גרור ושחרר קבצים או תיקיות", "LabelUploaderDropFiles": "שחרר קבצים", "LabelUploaderItemFetchMetadataHelp": "משיכת כותרת, סופר וסדרה באופן אוטומטי", - "LabelUseChapterTrack": "השתמש במסלול פרקים", - "LabelUseFullTrack": "השתמש במסלול מלא", + "LabelUseChapterTrack": "השתמש ברצועות הפרקים", + "LabelUseFullTrack": "השתמש ברצועה המלאה", "LabelUser": "משתמש", "LabelUsername": "שם משתמש", "LabelValue": "ערך", "LabelVersion": "גרסה", "LabelViewBookmarks": "הצג סימניות", "LabelViewChapters": "הצג פרקים", - "LabelViewQueue": "הצג תור הנגן", + "LabelViewQueue": "הצג תור נגן", "LabelVolume": "עוצמת קול", "LabelWeekdaysToRun": "ימי השבוע להרצה", "LabelYearReviewHide": "הסתר שנת סקירה", - "LabelYearReviewShow": "ראה שנת סקירה", - "LabelYourAudiobookDuration": "משך האודיובוק שלך", + "LabelYearReviewShow": "הצג שנת סקירה", + "LabelYourAudiobookDuration": "משך הספר הקולי שלך", "LabelYourBookmarks": "הסימניות שלך", "LabelYourPlaylists": "הפלייליסטים שלך", "LabelYourProgress": "ההתקדמות שלך", "MessageAddToPlayerQueue": "הוסף לתור הנגן", "MessageAppriseDescription": "כדי להשתמש בתכונה זו יש לך להריץ מופע של ממשק התכנית האפליקציה או API שיטפל בבקשות אלו.
כתובת URL של ממשק ה-Apprise API צריכה להיות הנתיב המלא לשליחת ההתראה, לדוגמה, אם המופע של ה-API שלך מוצע ב-http://192.168.1.1:8337 אז עליך לשים http://192.168.1.1:8337/notify.", "MessageBackupsDescription": "גיבויים כוללים משתמשים, התקדמות משתמש, פרטי פריטי ספרייה, הגדרות שרת ותמונות השמורות ב-/metadata/items & /metadata/authors. גיבויים לא כוללים קבצים שמורים בתיקיות הספרייה שלך.", - "MessageBatchQuickMatchDescription": "התאמה מהירה תנסה להוסיף כריכות ומטה-נתונים חסרים עבור הפריטים הנבחרים. הפעל את האפשרויות למטה כדי לאפשר להתאמה מהירה לדרוס כריכות קיימות ו/או מטה-נתונים.", - "MessageBookshelfNoCollections": "עדיין לא יצרת שום אוספים", - "MessageBookshelfNoResultsForFilter": "אין תוצאות עבור מסנן \"{0}: {1}\"", - "MessageBookshelfNoRSSFeeds": "אין RSS feeds פתוחים", + "MessageBatchQuickMatchDescription": "התאמה מהירה תנסה להוסיף כריכות ומטה-נתונים חסרים עבור הפריטים הנבחרים. הפעל את האפשרויות למטה כדי לאפשר להתאמה מהירה להחליף כריכות קיימות ו/או מטה-נתונים.", + "MessageBookshelfNoCollections": "עדיין לא יצרת אוספים", + "MessageBookshelfNoResultsForFilter": "אין תוצאות עבור סינון \"{0}: {1}\"", + "MessageBookshelfNoRSSFeeds": "אין ערוצי RSS פתוחים", "MessageBookshelfNoSeries": "אין לך סדרות", - "MessageChapterEndIsAfter": "סיום הפרק מאוחר מסיום האודיובוק שלך", + "MessageChapterEndIsAfter": "זמן סיום הפרק אחרי סיום הספר הקולי שלך", "MessageChapterErrorFirstNotZero": "הפרק הראשון חייב להתחיל ב-0", - "MessageChapterErrorStartGteDuration": "זמן התחלה לא תקין חייב להיות פחות ממשך האודיובוק", - "MessageChapterErrorStartLtPrev": "זמן התחלה לא תקין חייב להיות גדול או שווה לזמן התחלה של הפרק הקודם", - "MessageChapterStartIsAfter": "התחלת הפרק מאוחרת מסיום האודיובוק שלך", + "MessageChapterErrorStartGteDuration": "זמן התחלה לא תקין, חייב להיות פחות ממשך הספר הקולי", + "MessageChapterErrorStartLtPrev": "זמן התחלה לא תקין, חייב להיות גדול או שווה לזמן ההתחלה של הפרק הקודם", + "MessageChapterStartIsAfter": "התחלת הפרק אחרי סיום הספר הקולי שלך", "MessageCheckingCron": "בודק את תזמון העבודה...", - "MessageConfirmCloseFeed": "האם אתה בטוח שאתה רוצה לסגור את הפיד הזה?", + "MessageConfirmCloseFeed": "האם אתה בטוח שאתה רוצה לסגור את הערוץ הזה?", "MessageConfirmDeleteBackup": "האם אתה בטוח שברצונך למחוק גיבוי עבור {0}?", - "MessageConfirmDeleteFile": "זה ימחק את הקובץ מהמערכת שלך. האם אתה בטוח?", + "MessageConfirmDeleteFile": "הקובץ ימחק לצמיתות מהמערכת שלך. האם אתה בטוח?", "MessageConfirmDeleteLibrary": "האם אתה בטוח שברצונך למחוק לצמיתות את הספרייה \"{0}\"?", - "MessageConfirmDeleteLibraryItem": "זה ימחק את פריט הספרייה ממסד הנתונים ומהמערכת שלך. האם אתה בטוח?", - "MessageConfirmDeleteLibraryItems": "זה ימחק {0} פריטי ספרייה ממסד הנתונים ומהמערכת שלך. האם אתה בטוח?", + "MessageConfirmDeleteLibraryItem": "פריט הספרייה יימחק לצמיתות ממסד הנתונים ומהמערכת שלך. האם אתה בטוח?", + "MessageConfirmDeleteLibraryItems": "פריטי הספרייה {0} יימחקו ממסד הנתונים ומהמערכת שלך. האם אתה בטוח?", "MessageConfirmDeleteSession": "האם אתה בטוח שאתה רוצה למחוק את ההפעלה הזו?", - "MessageConfirmForceReScan": "האם אתה בטוח שאתה רוצה לכוון איתור מחדש?", - "MessageConfirmMarkAllEpisodesFinished": "האם אתה בטוח שברצונך לסמן את כל הפרקים כסיום?", - "MessageConfirmMarkAllEpisodesNotFinished": "האם אתה בטוח שברצונך לסמן את כל הפרקים כלא סיום?", - "MessageConfirmMarkSeriesFinished": "האם אתה בטוח שברצונך לסמן את כל הספרים בסדרה זו כסיום?", - "MessageConfirmMarkSeriesNotFinished": "האם אתה בטוח שברצונך לסמן את כל הספרים בסדרה זו כלא סיום?", + "MessageConfirmForceReScan": "האם אתה בטוח שאתה רוצה להכריח סריקה מחדש?", + "MessageConfirmMarkAllEpisodesFinished": "האם אתה בטוח שברצונך לסמן את כל הפרקים כהסתיימו?", + "MessageConfirmMarkAllEpisodesNotFinished": "האם אתה בטוח שברצונך לסמן את כל הפרקים כלא הסתיימו?", + "MessageConfirmMarkSeriesFinished": "האם אתה בטוח שברצונך לסמן את כל הספרים בסדרה זו כהסתיימו?", + "MessageConfirmMarkSeriesNotFinished": "האם אתה בטוח שברצונך לסמן את כל הספרים בסדרה זו כלא הסתיימו?", "MessageConfirmQuickEmbed": "אזהרה! הטמעה מהירה לא תגבה גיבוי של קבצי האודיו שלך. וודא שיש לך גיבוי של קבצי האודיו שלך.

האם ברצונך להמשיך?", "MessageConfirmRemoveAllChapters": "האם אתה בטוח שברצונך להסיר את כל הפרקים?", "MessageConfirmRemoveAuthor": "האם אתה בטוח שברצונך להסיר את המחבר \"{0}\"?", "MessageConfirmRemoveCollection": "האם אתה בטוח שברצונך להסיר אוסף \"{0}\"?", "MessageConfirmRemoveEpisode": "האם אתה בטוח שברצונך להסיר פרק \"{0}\"?", "MessageConfirmRemoveEpisodes": "האם אתה בטוח שברצונך להסיר {0} פרקים?", - "MessageConfirmRemoveListeningSessions": "האם אתה בטוח שברצונך להסיר {0} מפגשי האזנה?", - "MessageConfirmRemoveNarrator": "האם אתה בטוח שברצונך להסיר נרטור \"{0}\"?", + "MessageConfirmRemoveListeningSessions": "האם אתה בטוח שברצונך להסיר {0} הפעלות האזנה?", + "MessageConfirmRemoveNarrator": "האם אתה בטוח שברצונך להסיר מקריא \"{0}\"?", "MessageConfirmRemovePlaylist": "האם אתה בטוח שברצונך להסיר את רשימת ההשמעה שלך \"{0}\"?", - "MessageConfirmRenameGenre": "האם אתה בטוח שברצונך לשנות את שם הג'אנר \"{0}\" ל \"{1}\" עבור כל הפריטים?", - "MessageConfirmRenameGenreMergeNote": "הערה: ג'אנר זה כבר קיים ולכן הם יתמזגו.", - "MessageConfirmRenameGenreWarning": "אזהרה! יש ג'אנר דומה עם רישום שונה הכבר קיים \"{0}\".", + "MessageConfirmRenameGenre": "האם אתה בטוח שברצונך לשנות את שם הז'אנר \"{0}\" ל \"{1}\" עבור כל הפריטים?", + "MessageConfirmRenameGenreMergeNote": "הערה: ז'אנר זה כבר קיים ולכן הם יתמזגו.", + "MessageConfirmRenameGenreWarning": "אזהרה! יש ז'אנר דומה עם רישום שונה שכבר קיים \"{0}\".", "MessageConfirmRenameTag": "האם אתה בטוח שברצונך לשנות את שם התג \"{0}\" ל \"{1}\" עבור כל הפריטים?", "MessageConfirmRenameTagMergeNote": "הערה: התג זה כבר קיים ולכן הם יתמזגו.", - "MessageConfirmRenameTagWarning": "אזהרה! יש תג דומה עם רישום שונה הכבר קיים \"{0}\".", + "MessageConfirmRenameTagWarning": "אזהרה! יש תג דומה עם רישום שונה שכבר קיים \"{0}\".", "MessageConfirmReScanLibraryItems": "האם אתה בטוח שברצונך לסרוק מחדש {0} פריטים?", - "MessageConfirmSendEbookToDevice": "האם אתה בטוח שברצונך לשלוח {0} איבוק \"{1}\" למכשיר \"{2}\"?", + "MessageConfirmSendEbookToDevice": "האם אתה בטוח שברצונך לשלוח {0} את הספר האלקטרוני \"{1}\" למכשיר \"{2}\"?", "MessageDownloadingEpisode": "מוריד פרק", - "MessageDragFilesIntoTrackOrder": "גרור קבצים לסדר השמעה נכון", + "MessageDragFilesIntoTrackOrder": "גרור קבצים לסדר ההשמעה נכון", "MessageEmbedFinished": "ההטמעה הושלמה!", "MessageEpisodesQueuedForDownload": "{0} פרקים בתור להורדה", "MessageFeedURLWillBe": "כתובת URL של העדכון תהיה {0}", - "MessageFetching": "מבצע גישה...", - "MessageForceReScanDescription": "יסרוק את כל הקבצים מחדש כמו סריקה חדשה. תגיות ID3 של קבצי שמע, קבצי OPF וקבצי טקסט יסרקו כחדשים.", + "MessageFetching": "מושך...", + "MessageForceReScanDescription": "תבוצע סריקה מחדש כמו סריקה חדש מאפס, תגי ID3 של קבצי קול, קבצי OPF, וקבצי טקסט ייסרקו כחדשים.", "MessageImportantNotice": "הודעה חשובה!", - "MessageInsertChapterBelow": "הוסף פרק למטה", + "MessageInsertChapterBelow": "הוסף פרק מתחת", "MessageItemsSelected": "{0} פריטים נבחרו", "MessageItemsUpdated": "{0} פריטים עודכנו", "MessageJoinUsOn": "הצטרף אלינו ב-", @@ -629,12 +629,12 @@ "MessageLoadingFolders": "טוען תיקיות...", "MessageM4BFailed": "M4B נכשל!", "MessageM4BFinished": "M4B הושלם!", - "MessageMapChapterTitles": "מפה שמות פרקים לפרקי הספר השמורים שלך ללא שינוי תגים זמניים", + "MessageMapChapterTitles": "מפה שמות פרקים לפרקי הספר השמורים שלך ללא שינוי תגי זמן", "MessageMarkAllEpisodesFinished": "סמן את כל הפרקים כהסתיימו", "MessageMarkAllEpisodesNotFinished": "סמן את כל הפרקים כלא הסתיימו", "MessageMarkAsFinished": "סמן כהסתיים", "MessageMarkAsNotFinished": "סמן כלא הסתיים", - "MessageMatchBooksDescription": "ינסה להתאים ספרים בספריית הספרים שלך עם ספר מספק החיפוש הנבחר וימלא פרטים ריקים ותמונות כריכה. לא ימחק פרטים.", + "MessageMatchBooksDescription": "ינסה להתאים ספרים בספריית הספרים שלך עם ספר מספק החיפוש הנבחר וימלא פרטים ריקים ותמונות כריכה. לא יחליף פרטים קיימים.", "MessageNoAudioTracks": "אין רצועות שמע", "MessageNoAuthors": "אין סופרים", "MessageNoBackups": "אין גיבויים", @@ -649,12 +649,12 @@ "MessageNoEpisodes": "אין פרקים", "MessageNoFoldersAvailable": "אין תיקיות זמינות", "MessageNoGenres": "אין ז'אנרים", - "MessageNoIssues": "אין בעיות", + "MessageNoIssues": "אין תקלות", "MessageNoItems": "אין פריטים", "MessageNoItemsFound": "לא נמצאו פריטים", - "MessageNoListeningSessions": "אין מפגשי האזנה", + "MessageNoListeningSessions": "אין הפעלות האזנה", "MessageNoLogs": "אין לוגים", - "MessageNoMediaProgress": "אין התקדמות מדיה", + "MessageNoMediaProgress": "אין התקדמות במדיה", "MessageNoNotifications": "אין התראות", "MessageNoPodcastsFound": "לא נמצאו פודקאסטים", "MessageNoResults": "אין תוצאות", @@ -664,14 +664,14 @@ "MessageNoTasksRunning": "אין משימות פעילות", "MessageNotYetImplemented": "עדיין לא מיושם", "MessageNoUpdateNecessary": "לא נדרש עדכון", - "MessageNoUpdatesWereNecessary": "לא הייתה צורך בעדכונים", + "MessageNoUpdatesWereNecessary": "לא היה צורך בעדכונים", "MessageNoUserPlaylists": "אין לך רשימות השמעה", "MessageOr": "או", "MessagePauseChapter": "השהה השמעת הפרק", - "MessagePlayChapter": "השמע לתחילת הפרק", + "MessagePlayChapter": "הקשב לתחילת הפרק", "MessagePlaylistCreateFromCollection": "צור רשימת השמעה מאוסף", - "MessagePodcastHasNoRSSFeedForMatching": "הפודקאסט אין לו כתובת URL של הזנת RSS לשימוש בהתאמה", - "MessageQuickMatchDescription": "ממלא פרטים ריקים וכריכות עם התוצאה הראשונה מ '{0}'. לא ימחק פרטים אלא אם הגדרות השרת 'מעדיפים מטה-נתונים התואמים' מופעלות.", + "MessagePodcastHasNoRSSFeedForMatching": "לפודקאסט אין כתובת URL של ערוץ RSS להתאמה", + "MessageQuickMatchDescription": "ממלא פרטים ריקים וכריכות עם התוצאה הראשונה מ '{0}'. לא ימחק פרטים אלא אם הגדרת השרת 'העדף מטה-נתונים מותאמים' מופעלת.", "MessageRemoveChapter": "הסר פרק", "MessageRemoveEpisodes": "הסר {0} פרקים", "MessageRemoveFromPlayerQueue": "הסר מתור ההשמעה של הנגן", @@ -679,29 +679,29 @@ "MessageReportBugsAndContribute": "דווח על באגים, בקש תכונות חדשות, ותרום ב-", "MessageResetChaptersConfirm": "האם אתה בטוח שברצונך לאפס את הפרקים ולבטל את השינויים שביצעת?", "MessageRestoreBackupConfirm": "האם אתה בטוח שברצונך לשחזר את הגיבוי שנוצר ב", - "MessageRestoreBackupWarning": "שחזור גיבוי ימחק את כל מסד הנתונים השוכן ב /config ואת תמונות הכריכה ב- /metadata/items & /metadata/authors.

גיבויים אינם משנים קבצים בתיקיות הספרייה שלך. אם הגדרות השרת מופעלות כדי לאחסן תמונות כריכה ומטא-נתונים בתיקיות הספרייה שלך אז אלה לא יגובו או ימחקו.

כל הלקוחות המשתמשים בשרת שלך יתעדכנו באופן אוטומטי.", + "MessageRestoreBackupWarning": "שחזור גיבוי ימחק את כל מסד הנתונים הנוכחי השוכן ב /config ואת תמונות הכריכה ב- /metadata/items & /metadata/authors.

גיבויים אינם משנים קבצים בתיקיות הספרייה שלך. אם הגדרות השרת לאחסן תמונות כריכה ומטא-נתונים בתיקיות הספרייה שלך מופעלות אז אלה לא יגובו או ימחקו.

כל האפליקציות המשתמשות בשרת שלך יתעדכנו באופן אוטומטי.", "MessageSearchResultsFor": "תוצאות חיפוש עבור", "MessageSelected": "{0} נבחרו", "MessageServerCouldNotBeReached": "לא ניתן להגיע אל השרת", "MessageSetChaptersFromTracksDescription": "קבע פרקים באמצעות כל קובץ שמע כפרק וכותרת פרק כשם הקובץ שמע", - "MessageStartPlaybackAtTime": "האם אתה בטוח שברצונך להתחיל השמעה עבור \"{0}\" ב-{1}?", + "MessageStartPlaybackAtTime": "להתחיל השמעה עבור \"{0}\" ב-{1}?", "MessageThinking": "חושב...", "MessageUploaderItemFailed": "העלאת הפריט נכשלה", "MessageUploaderItemSuccess": "העלאה הצליחה!", "MessageUploading": "מעלה...", "MessageValidCronExpression": "ביטוי Cron חוקי", - "MessageWatcherIsDisabledGlobally": "שומר מנוטרל באופן גלובלי בהגדרות השרת", + "MessageWatcherIsDisabledGlobally": "עוקב מנוטרל באופן גלובלי בהגדרות השרת", "MessageXLibraryIsEmpty": "ספריית {0} ריקה!", - "MessageYourAudiobookDurationIsLonger": "הזמן של הספר השמע שלך ארוך יותר מהזמן שנמצא", - "MessageYourAudiobookDurationIsShorter": "הזמן של הספר השמע שלך קצר יותר מהזמן שנמצא", - "NoteChangeRootPassword": "המשתמש השורש הוא המשתמש היחיד שיכול להיות לו סיסמה ריקה", - "NoteChapterEditorTimes": "הערה: זמן ההתחלה של הפרק הראשון חייב להישאר 0:00 וזמן ההתחלה של הפרק האחרון לא יכול לחרוג מהזמן של ספר השמע הזה.", - "NoteFolderPicker": "הערה: תיקיות שכבר מוגדרות לא יוצגו", - "NoteRSSFeedPodcastAppsHttps": "אזהרה: רוב יישומי הפודקאסט דורשים שהכתובת URL של העדכון תשתמש ב HTTPS", - "NoteRSSFeedPodcastAppsPubDate": "אזהרה: פרק(ים) אחד או יותר לא מכילים תאריך פרסום. חלק מיישומי הפודקאסט דורשים זאת.", + "MessageYourAudiobookDurationIsLonger": "הזמן של הספר הקולי שלך ארוך יותר מהזמן שנמצא", + "MessageYourAudiobookDurationIsShorter": "הזמן של הספר הקולי שלך קצר יותר מהזמן שנמצא", + "NoteChangeRootPassword": "המשתמש root הוא המשתמש היחיד שיכולה להיות לו סיסמה ריקה", + "NoteChapterEditorTimes": "הערה: זמן ההתחלה של הפרק הראשון חייב להישאר 0:00 וזמן ההתחלה של הפרק האחרון לא יכול לחרוג מהזמן של ספר השמע.", + "NoteFolderPicker": "הערה: תיקיות שכבר מופו לא יוצגו", + "NoteRSSFeedPodcastAppsHttps": "אזהרה: רוב יישומי הפודקאסט דורשים שכתובת ה-URL ערוץ ה-RSS תשתמש ב-HTTPS", + "NoteRSSFeedPodcastAppsPubDate": "אזהרה: פרק אחד או יותר לא מכילים תאריך פרסום. חלק מיישומי הפודקאסט דורשים זאת.", "NoteUploaderFoldersWithMediaFiles": "תיקיות עם קבצי מדיה יעובדו כפריטי ספריה נפרדים.", - "NoteUploaderOnlyAudioFiles": "אם מעלים רק קבצי שמע, כל קובץ שמע יעובד כספר שמע נפרד.", - "NoteUploaderUnsupportedFiles": "קבצים לא נתמכים מתעלמים. בעת בחירת תיקייה או זריקתה, קבצים אחרים שאינם בתיקיית פריט מתעלמים.", + "NoteUploaderOnlyAudioFiles": "אם מועלים רק קבצי שמע, כל קובץ שמע יעובד כספר שמע נפרד.", + "NoteUploaderUnsupportedFiles": "מתעלם מקבצים לא נתמכים. בעת בחירת תיקייה או גרירה לדף, מתעלם מקבצים אחרים שאינם בתיקיית פריט.", "PlaceholderNewCollection": "שם אוסף חדש", "PlaceholderNewFolderPath": "נתיב תיקייה חדשה", "PlaceholderNewPlaylist": "שם רשימת השמעה חדשה", @@ -712,7 +712,7 @@ "ToastAuthorImageRemoveFailed": "הסרת התמונה של המחבר נכשלה", "ToastAuthorImageRemoveSuccess": "תמונת המחבר הוסרה בהצלחה", "ToastAuthorUpdateFailed": "עדכון המחבר נכשל", - "ToastAuthorUpdateMerged": "המחבר ממוזג", + "ToastAuthorUpdateMerged": "המחבר מוזג", "ToastAuthorUpdateSuccess": "המחבר עודכן בהצלחה", "ToastAuthorUpdateSuccessNoImageFound": "המחבר עודכן (תמונה לא נמצאה)", "ToastBackupCreateFailed": "יצירת גיבוי נכשלה", @@ -722,28 +722,28 @@ "ToastBackupRestoreFailed": "שחזור הגיבוי נכשל", "ToastBackupUploadFailed": "העלאת הגיבוי נכשלה", "ToastBackupUploadSuccess": "הגיבוי הועלה בהצלחה", - "ToastBatchUpdateFailed": "עדכון הצטברות נכשל", - "ToastBatchUpdateSuccess": "הצטברות עודכנה בהצלחה", + "ToastBatchUpdateFailed": "עדכון קבוצתי נכשל", + "ToastBatchUpdateSuccess": "עדכון קבוצתי הצליח", "ToastBookmarkCreateFailed": "יצירת סימניה נכשלה", "ToastBookmarkCreateSuccess": "הסימניה נוספה בהצלחה", "ToastBookmarkRemoveFailed": "הסרת הסימניה נכשלה", "ToastBookmarkRemoveSuccess": "הסימניה הוסרה בהצלחה", "ToastBookmarkUpdateFailed": "עדכון הסימניה נכשל", "ToastBookmarkUpdateSuccess": "הסימניה עודכנה בהצלחה", - "ToastChaptersHaveErrors": "הפרקים מכילים שגיאות", - "ToastChaptersMustHaveTitles": "הפרקים חייבים לכלול כותרות", + "ToastChaptersHaveErrors": "פרקים מכילים שגיאות", + "ToastChaptersMustHaveTitles": "פרקים חייבים לכלול כותרות", "ToastCollectionItemsRemoveFailed": "הסרת הפריט(ים) מהאוסף נכשלה", "ToastCollectionItemsRemoveSuccess": "הפריט(ים) הוסרו מהאוסף בהצלחה", "ToastCollectionRemoveFailed": "מחיקת האוסף נכשלה", "ToastCollectionRemoveSuccess": "האוסף הוסר בהצלחה", "ToastCollectionUpdateFailed": "עדכון האוסף נכשל", "ToastCollectionUpdateSuccess": "האוסף עודכן בהצלחה", - "ToastItemCoverUpdateFailed": "עדכון כיסוי הפריט נכשל", - "ToastItemCoverUpdateSuccess": "כיסוי הפריט עודכן בהצלחה", + "ToastItemCoverUpdateFailed": "עדכון כריכת הפריט נכשל", + "ToastItemCoverUpdateSuccess": "כריכת הפריט עודכנה בהצלחה", "ToastItemDetailsUpdateFailed": "עדכון פרטי הפריט נכשל", "ToastItemDetailsUpdateSuccess": "פרטי הפריט עודכנו בהצלחה", "ToastItemDetailsUpdateUnneeded": "לא נדרשים עדכונים לפרטי הפריט", - "ToastItemMarkedAsFinishedFailed": "סימון כפריט הושלם נכשל", + "ToastItemMarkedAsFinishedFailed": "סימון כפריט כהושלם נכשל", "ToastItemMarkedAsFinishedSuccess": "הפריט סומן כהושלם בהצלחה", "ToastItemMarkedAsNotFinishedFailed": "סימון כפריט שלא הושלם נכשל", "ToastItemMarkedAsNotFinishedSuccess": "הפריט סומן כלא הושלם בהצלחה", @@ -765,16 +765,16 @@ "ToastPodcastCreateSuccess": "הפודקאסט נוצר בהצלחה", "ToastRemoveItemFromCollectionFailed": "הסרת הפריט מהאוסף נכשלה", "ToastRemoveItemFromCollectionSuccess": "הפריט הוסר מהאוסף בהצלחה", - "ToastRSSFeedCloseFailed": "סגירת הערוץ RSS נכשלה", - "ToastRSSFeedCloseSuccess": "הערוץ RSS נסגר בהצלחה", + "ToastRSSFeedCloseFailed": "סגירת ערוץ ה-RSS נכשלה", + "ToastRSSFeedCloseSuccess": "ערוץ ה-RSS נסגר בהצלחה", "ToastSendEbookToDeviceFailed": "שליחת הספר אל המכשיר נכשלה", "ToastSendEbookToDeviceSuccess": "הספר נשלח אל המכשיר \"{0}\"", "ToastSeriesUpdateFailed": "עדכון הסדרה נכשל", "ToastSeriesUpdateSuccess": "הסדרה עודכנה בהצלחה", "ToastSessionDeleteFailed": "מחיקת הפעולה נכשלה", "ToastSessionDeleteSuccess": "הפעולה נמחקה בהצלחה", - "ToastSocketConnected": "חיבור קצה התקשורת בוצע בהצלחה", - "ToastSocketDisconnected": "התנתקות קצה התקשורת בוצעה", + "ToastSocketConnected": "קצה תקשורת חובר", + "ToastSocketDisconnected": "קצה תקשורת נותק", "ToastSocketFailedToConnect": "התחברות קצה התקשורת נכשלה", "ToastUserDeleteFailed": "מחיקת המשתמש נכשלה", "ToastUserDeleteSuccess": "המשתמש נמחק בהצלחה" From 8f80948211c203b2c966ca360b76f693368bee00 Mon Sep 17 00:00:00 2001 From: pmangro <160148596+pmangro@users.noreply.github.com> Date: Sun, 17 Mar 2024 19:09:16 -0300 Subject: [PATCH 0011/1695] [PT-BR] Continue Series --- client/strings/pt-br.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/client/strings/pt-br.json b/client/strings/pt-br.json index 7a5a1eec..c4d00eb7 100644 --- a/client/strings/pt-br.json +++ b/client/strings/pt-br.json @@ -466,8 +466,8 @@ "LabelSettingsHideSingleBookSeriesHelp": "Séries com um só livro serão ocultadas na página de séries e na prateleira de séries na página principal.", "LabelSettingsHomePageBookshelfView": "Usar visão estante na página principal", "LabelSettingsLibraryBookshelfView": "Usar visão estante na página da biblioteca", - "LabelSettingsOnlyShowLaterBooksInContinueSeries": "Skip earlier books in Continue Series", - "LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "The Continue Series home page shelf shows the first book not started in series that have at least one book finished and no books in progress. Enabling this setting will continue series from the furthest completed book instead of the first book not started.", + "LabelSettingsOnlyShowLaterBooksInContinueSeries": "Pular livros anteriores em Continuar Série", + "LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "A prateleira Continuar Série na página principal de exibe o primeiro livro não iniciado em uma série que tem pelo menos um livro concluído e nenhum livro em andamento. Ativar essa configuração irá continuar a série a partir do livro mais recentemente concluído ao invés do primeiro livro não iniciado.", "LabelSettingsParseSubtitles": "Analisar subtítulos", "LabelSettingsParseSubtitlesHelp": "Extrair subtítulos do nome da pasta do audiobook.
Subtítulo deve estar separado por \" - \"
ex: \"Título do Livro - Um Subtítulo Aqui\" tem o subtítulo \"Um Subtítulo Aqui\"", "LabelSettingsPreferMatchedMetadata": "Preferir metadados consultados", @@ -778,4 +778,4 @@ "ToastSocketFailedToConnect": "Falha na conexão do socket", "ToastUserDeleteFailed": "Falha ao apagar usuário", "ToastUserDeleteSuccess": "Usuário apagado" -} \ No newline at end of file +} From 5b836dfa288928af31476f418f0ca06bd9b60c9b Mon Sep 17 00:00:00 2001 From: advplyr Date: Sun, 17 Mar 2024 19:19:52 -0500 Subject: [PATCH 0012/1695] Remove duplicate he language code --- client/plugins/i18n.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/client/plugins/i18n.js b/client/plugins/i18n.js index e35b7e7c..0ee0e5b1 100644 --- a/client/plugins/i18n.js +++ b/client/plugins/i18n.js @@ -25,8 +25,7 @@ const languageCodeMap = { 'sv': { label: 'Svenska', dateFnsLocale: 'sv' }, 'vi-vn': { label: 'Tiếng Việt', dateFnsLocale: 'vi' }, 'zh-cn': { label: '简体中文 (Simplified Chinese)', dateFnsLocale: 'zhCN' }, - 'zh-tw': { label: '正體中文 (Traditional Chinese)', dateFnsLocale: 'zhTW' }, - 'he': { label: 'עברית', dateFnsLocale: 'he' } + 'zh-tw': { label: '正體中文 (Traditional Chinese)', dateFnsLocale: 'zhTW' } } Vue.prototype.$languageCodeOptions = Object.keys(languageCodeMap).map(code => { return { From 56f1bfef507228ddeb58795666d6264bfe5cd966 Mon Sep 17 00:00:00 2001 From: Denis Arnst Date: Tue, 19 Mar 2024 17:57:24 +0100 Subject: [PATCH 0013/1695] Auth/OpenID: Implement Permissions via OpenID * Ability to set group * Ability to set more advanced permissions * Modified TextInputWithLabel to provide an ability to specify a different placeholder then the name --- client/components/ui/TextInputWithLabel.vue | 3 +- client/pages/config/authentication.vue | 45 +++++++++++- server/Auth.js | 81 ++++++++++++++++++++- server/objects/settings/ServerSettings.js | 17 ++++- server/objects/user/User.js | 72 ++++++++++++++++++ 5 files changed, 210 insertions(+), 8 deletions(-) diff --git a/client/components/ui/TextInputWithLabel.vue b/client/components/ui/TextInputWithLabel.vue index 032e24ca..f653a18b 100644 --- a/client/components/ui/TextInputWithLabel.vue +++ b/client/components/ui/TextInputWithLabel.vue @@ -5,7 +5,7 @@ >{{ label }}{{ note }} - +
@@ -14,6 +14,7 @@ export default { props: { value: [String, Number], label: String, + placeholder: String, note: String, type: { type: String, diff --git a/client/pages/config/authentication.vue b/client/pages/config/authentication.vue index 3373e287..91c6cfe2 100644 --- a/client/pages/config/authentication.vue +++ b/client/pages/config/authentication.vue @@ -70,17 +70,42 @@

{{ $strings.LabelMatchExistingUsersByDescription }}

-
+

{{ $strings.LabelAutoLaunch }}

-
+

{{ $strings.LabelAutoRegister }}

{{ $strings.LabelAutoRegisterDescription }}

+ +
Leave the following options empty to disable advanced group and permissions assignment, automatically assigning 'User' group then.
+
+
+ +
+

+ Name of the OpenID claim that contains a list of the user's groups. Commonly referred to as groups. If configured, the application will automatically assign roles based on the user's group memberships, provided that these groups are named case-insensitively 'admin', 'user', or 'guest' in the claim. The claim should contain a list, and if a user belongs to + multiple groups, the application will assign the role corresponding to the highest level of access. If no group matches, access will be denied. +

+
+ +
+
+ +
+
+

+ Name of the OpenID claim that contains advanced permissions for user actions within the application which will apply to non-admin roles (if configured). If the claim is missing from the response, access to ABS will be denied. If a single option is missing, it will be treated as false. Ensure the identity provider's claim matches the expected structure: +

+
{{ newAuthSettings.authOpenIDSamplePermissions }}
+                
+
+
@@ -222,6 +247,22 @@ export default { } }) } + + function isValidClaim(claim) { + if (claim === '') return true + + const pattern = new RegExp('^[a-zA-Z][a-zA-Z0-9_-]*$', 'i') + return pattern.test(claim) + } + if (!isValidClaim(this.newAuthSettings.authOpenIDGroupClaim)) { + this.$toast.error('Group Claim: Invalid claim name') + isValid = false + } + if (!isValidClaim(this.newAuthSettings.authOpenIDAdvancedPermsClaim)) { + this.$toast.error('Advanced Permission Claim: Invalid claim name') + isValid = false + } + return isValid }, async saveSettings() { diff --git a/server/Auth.js b/server/Auth.js index 352faf66..a4cdd1fc 100644 --- a/server/Auth.js +++ b/server/Auth.js @@ -98,7 +98,7 @@ class Auth { scope: 'openid profile email' } }, async (tokenset, userinfo, done) => { - Logger.debug(`[Auth] openid callback userinfo=`, userinfo) + Logger.debug(`[Auth] openid callback userinfo=`, JSON.stringify(userinfo, null, 2)) let failureMessage = 'Unauthorized' if (!userinfo.sub) { @@ -106,6 +106,35 @@ class Auth { return done(null, null, failureMessage) } + // Check if the claims itself are returned correctly + const groupClaimName = Database.serverSettings.authOpenIDGroupClaim; + if (groupClaimName) { + if (!userinfo[groupClaimName]) { + Logger.error(`[Auth] openid callback invalid: Group claim ${groupClaimName} configured, but not found or empty in userinfo`) + return done(null, null, failureMessage) + } + + const groupsList = userinfo[groupClaimName] + const targetRoles = ['admin', 'user', 'guest'] + + // Convert the list to lowercase for case-insensitive comparison + const groupsListLowercase = groupsList.map(group => group.toLowerCase()) + + // Check if any of the target roles exist in the groups list + const containsTargetRole = targetRoles.some(role => groupsListLowercase.includes(role.toLowerCase())) + + if (!containsTargetRole) { + Logger.info(`[Auth] openid callback: Denying access because neither admin nor user or guest is included is inside the group claim. Groups found: `, groupsList) + return done(null, null, failureMessage) + } + } + + const advancedPermsClaimName = Database.serverSettings.authOpenIDAdvancedPermsClaim + if (advancedPermsClaimName && !userinfo[advancedPermsClaimName]) { + Logger.error(`[Auth] openid callback invalid: Advanced perms claim ${advancedPermsClaimName} configured, but not found or empty in userinfo`) + return done(null, null, failureMessage) + } + // First check for matching user by sub let user = await Database.userModel.getUserByOpenIDSub(userinfo.sub) if (!user) { @@ -157,6 +186,43 @@ class Auth { return } + // Set user group if name of groups claim is configured + if (groupClaimName) { + const groupsList = userinfo[groupClaimName] ? userinfo[groupClaimName].map(group => group.toLowerCase()) : [] + const rolesInOrderOfPriority = ['admin', 'user', 'guest'] + + let userType = null + + for (let role of rolesInOrderOfPriority) { + if (groupsList.includes(role)) { + userType = role // This will override with the highest priority role found + break // Stop searching once the highest priority role is found + } + } + + // Actually already checked above, but just to be sure + if (!userType) { + Logger.error(`[Auth] openid callback: Denying access because neither admin nor user or guest is included is inside the group claim. Groups found: `, groupsList) + return done(null, null, failureMessage) + } + + Logger.debug(`[Auth] openid callback: Setting user ${user.username} type to ${userType}`) + user.type = userType + await Database.userModel.updateFromOld(user) + } + + if (advancedPermsClaimName) { + try { + Logger.debug(`[Auth] openid callback: Updating advanced perms for user ${user.username} to ${JSON.stringify(userinfo[advancedPermsClaimName])}`) + + user.updatePermissionsFromExternalJSON(userinfo[advancedPermsClaimName]) + await Database.userModel.updateFromOld(user) + } catch (error) { + Logger.error(`[Auth] openid callback: Error updating advanced perms for user, error: `, error) + return done(null, null, failureMessage) + } + } + // We also have to save the id_token for later (used for logout) because we cannot set cookies here user.openid_id_token = tokenset.id_token @@ -334,10 +400,19 @@ class Auth { sso_redirect_uri: oidcStrategy._params.redirect_uri // Save the redirect_uri (for the SSO Provider) for the callback } + var scope = 'openid profile email' + if (global.ServerSettings.authOpenIDGroupClaim) { + scope += ' ' + global.ServerSettings.authOpenIDGroupClaim + } + if (global.ServerSettings.authOpenIDAdvancedPermsClaim) { + scope += ' ' + global.ServerSettings.authOpenIDAdvancedPermsClaim + } + const authorizationUrl = client.authorizationUrl({ ...oidcStrategy._params, state: state, response_type: 'code', + scope: scope, code_challenge, code_challenge_method }) @@ -424,12 +499,12 @@ class Auth { } function handleAuthError(isMobile, errorCode, errorMessage, logMessage, response) { - Logger.error(logMessage) + Logger.error(JSON.stringify(logMessage, null, 2)) if (response) { // Depending on the error, it can also have a body // We also log the request header the passport plugin sents for the URL const header = response.req?._header.replace(/Authorization: [^\r\n]*/i, 'Authorization: REDACTED') - Logger.debug(header + '\n' + response.body?.toString() + '\n' + JSON.stringify(response.body, null, 2)) + Logger.debug(header + '\n' + JSON.stringify(response.body, null, 2)) } if (isMobile) { diff --git a/server/objects/settings/ServerSettings.js b/server/objects/settings/ServerSettings.js index 5cc68a5c..5c2da381 100644 --- a/server/objects/settings/ServerSettings.js +++ b/server/objects/settings/ServerSettings.js @@ -1,6 +1,7 @@ const packageJson = require('../../../package.json') const { BookshelfView } = require('../../utils/constants') const Logger = require('../../Logger') +const User = require('../user/User') class ServerSettings { constructor(settings) { @@ -72,6 +73,8 @@ class ServerSettings { this.authOpenIDAutoRegister = false this.authOpenIDMatchExistingBy = null this.authOpenIDMobileRedirectURIs = ['audiobookshelf://oauth'] + this.authOpenIDGroupClaim = '' + this.authOpenIDAdvancedPermsClaim = '' if (settings) { this.construct(settings) @@ -129,6 +132,8 @@ class ServerSettings { this.authOpenIDAutoRegister = !!settings.authOpenIDAutoRegister this.authOpenIDMatchExistingBy = settings.authOpenIDMatchExistingBy || null this.authOpenIDMobileRedirectURIs = settings.authOpenIDMobileRedirectURIs || ['audiobookshelf://oauth'] + this.authOpenIDGroupClaim = settings.authOpenIDGroupClaim || '' + this.authOpenIDAdvancedPermsClaim = settings.authOpenIDAdvancedPermsClaim || '' if (!Array.isArray(this.authActiveAuthMethods)) { this.authActiveAuthMethods = ['local'] @@ -216,7 +221,9 @@ class ServerSettings { authOpenIDAutoLaunch: this.authOpenIDAutoLaunch, authOpenIDAutoRegister: this.authOpenIDAutoRegister, authOpenIDMatchExistingBy: this.authOpenIDMatchExistingBy, - authOpenIDMobileRedirectURIs: this.authOpenIDMobileRedirectURIs // Do not return to client + authOpenIDMobileRedirectURIs: this.authOpenIDMobileRedirectURIs, // Do not return to client + authOpenIDGroupClaim: this.authOpenIDGroupClaim, // Do not return to client + authOpenIDAdvancedPermsClaim: this.authOpenIDAdvancedPermsClaim // Do not return to client } } @@ -226,6 +233,8 @@ class ServerSettings { delete json.authOpenIDClientID delete json.authOpenIDClientSecret delete json.authOpenIDMobileRedirectURIs + delete json.authOpenIDGroupClaim + delete json.authOpenIDAdvancedPermsClaim return json } @@ -262,7 +271,11 @@ class ServerSettings { authOpenIDAutoLaunch: this.authOpenIDAutoLaunch, authOpenIDAutoRegister: this.authOpenIDAutoRegister, authOpenIDMatchExistingBy: this.authOpenIDMatchExistingBy, - authOpenIDMobileRedirectURIs: this.authOpenIDMobileRedirectURIs // Do not return to client + authOpenIDMobileRedirectURIs: this.authOpenIDMobileRedirectURIs, // Do not return to client + authOpenIDGroupClaim: this.authOpenIDGroupClaim, // Do not return to client + authOpenIDAdvancedPermsClaim: this.authOpenIDAdvancedPermsClaim, // Do not return to client + + authOpenIDSamplePermissions: User.getSampleAbsPermissions() } } diff --git a/server/objects/user/User.js b/server/objects/user/User.js index d926e8be..d09e921d 100644 --- a/server/objects/user/User.js +++ b/server/objects/user/User.js @@ -268,6 +268,78 @@ class User { return hasUpdates } + // List of expected permission properties from the client + static permissionMapping = { + canDownload: 'download', + canUpload: 'upload', + canDelete: 'delete', + canUpdate: 'update', + canAccessExplicitContent: 'accessExplicitContent', + canAccessAllLibraries: 'accessAllLibraries', + canAccessAllTags: 'accessAllTags', + tagsAreBlacklist: 'selectedTagsNotAccessible', + // Direct mapping for array-based permissions + allowedLibraries: 'librariesAccessible', + allowedTags: 'itemTagsSelected', + } + + /** + * Update user from external JSON + * + * @param {object} absPermissions JSON containg user permissions + */ + updatePermissionsFromExternalJSON(absPermissions) { + // Initialize all permissions to false first + Object.keys(User.permissionMapping).forEach(mappingKey => { + const userPermKey = User.permissionMapping[mappingKey]; + if (typeof this.permissions[userPermKey] === 'boolean') { + this.permissions[userPermKey] = false; // Default to false for boolean permissions + } else { + this[userPermKey] = []; // Default to empty array for other properties + } + }); + + Object.keys(absPermissions).forEach(absKey => { + const userPermKey = User.permissionMapping[absKey] + if (!userPermKey) { + throw new Error(`Unexpected permission property: ${absKey}`) + } + + // Update the user's permissions based on absPermissions + this.permissions[userPermKey] = absPermissions[absKey] + }); + + // Handle allowedLibraries and allowedTags separately if needed + if (absPermissions.allowedLibraries) { + this.librariesAccessible = absPermissions.allowedLibraries + } + if (absPermissions.allowedTags) { + this.itemTagsSelected = absPermissions.allowedTags + } + } + + /** + * Get a sample to show how a JSON for updatePermissionsFromExternalJSON should look like + * + * @returns JSON string + */ + static getSampleAbsPermissions() { + // Start with a template object where all permissions are false for simplicity + const samplePermissions = Object.keys(User.permissionMapping).reduce((acc, key) => { + // For array-based permissions, provide a sample array + if (key === 'allowedLibraries') { + acc[key] = [`ExampleLibrary`, `AnotherLibrary`]; + } else if (key === 'allowedTags') { + acc[key] = [`ExampleTag`, `AnotherTag`, `ThirdTag`]; + } else { + acc[key] = false; + } + return acc; + }, {}); + + return JSON.stringify(samplePermissions, null, 2); // Pretty print the JSON + } + /** * Get first available library id for user * From 9511122bae10ae67dd2299961e35ea2e82c8f284 Mon Sep 17 00:00:00 2001 From: mikiher Date: Tue, 19 Mar 2024 19:28:26 +0200 Subject: [PATCH 0014/1695] Fix LibraryItem and Media file update logic for library scans --- server/scanner/BookScanner.js | 32 +++++++++++++++-- server/scanner/LibraryItemScanData.js | 49 +++++++++++++++++++++++---- server/scanner/PodcastScanner.js | 18 ++++++++-- 3 files changed, 89 insertions(+), 10 deletions(-) diff --git a/server/scanner/BookScanner.js b/server/scanner/BookScanner.js index c738c52e..c12441b2 100644 --- a/server/scanner/BookScanner.js +++ b/server/scanner/BookScanner.js @@ -20,6 +20,7 @@ const LibraryScan = require("./LibraryScan") const OpfFileScanner = require('./OpfFileScanner') const NfoFileScanner = require('./NfoFileScanner') const AbsMetadataFileScanner = require('./AbsMetadataFileScanner') +const EBookFile = require("../objects/files/EBookFile") /** * Metadata for books pulled from files @@ -84,7 +85,7 @@ class BookScanner { // Update audio files that were modified if (libraryItemData.audioLibraryFilesModified.length) { - let scannedAudioFiles = await AudioFileScanner.executeMediaFileScans(existingLibraryItem.mediaType, libraryItemData, libraryItemData.audioLibraryFilesModified) + let scannedAudioFiles = await AudioFileScanner.executeMediaFileScans(existingLibraryItem.mediaType, libraryItemData, libraryItemData.audioLibraryFilesModified.map(lf => lf.new)) media.audioFiles = media.audioFiles.map((audioFileObj) => { let matchedScannedAudioFile = scannedAudioFiles.find(saf => saf.metadata.path === audioFileObj.metadata.path) if (!matchedScannedAudioFile) { @@ -138,11 +139,25 @@ class BookScanner { } // Check if cover was removed - if (media.coverPath && !libraryItemData.imageLibraryFiles.some(lf => lf.metadata.path === media.coverPath) && !(await fsExtra.pathExists(media.coverPath))) { + if (media.coverPath && libraryItemData.imageLibraryFilesRemoved.some(lf => lf.metadata.path === media.coverPath) && !(await fsExtra.pathExists(media.coverPath))) { media.coverPath = null hasMediaChanges = true } + // Update cover if it was modified + if (media.coverPath && libraryItemData.imageLibraryFilesModified.length) { + let coverMatch = libraryItemData.imageLibraryFilesModified.find(iFile => iFile.old.metadata.path === media.coverPath) + if (coverMatch) { + const coverPath = coverMatch.new.metadata.path + if (coverPath !== media.coverPath) { + libraryScan.addLog(LogLevel.DEBUG, `Updating book cover "${media.coverPath}" => "${coverPath}" for book "${media.title}"`) + media.coverPath = coverPath + media.changed('coverPath', true) + hasMediaChanges = true + } + } + } + // Check if cover is not set and image files were found if (!media.coverPath && libraryItemData.imageLibraryFiles.length) { // Prefer using a cover image with the name "cover" otherwise use the first image @@ -157,6 +172,19 @@ class BookScanner { hasMediaChanges = true } + // Update ebook if it was modified + if (media.ebookFile && libraryItemData.ebookLibraryFilesModified.length) { + let ebookMatch = libraryItemData.ebookLibraryFilesModified.find(eFile => eFile.old.metadata.path === media.ebookFile.metadata.path) + if (ebookMatch) { + const ebookFile = new EBookFile(ebookMatch.new) + ebookFile.ebookFormat = ebookFile.metadata.ext.slice(1).toLowerCase() + libraryScan.addLog(LogLevel.DEBUG, `Updating book ebook file "${media.ebookFile.metadata.path}" => "${ebookFile.metadata.path}" for book "${media.title}"`) + media.ebookFile = ebookFile.toJSON() + media.changed('ebookFile', true) + hasMediaChanges = true + } + } + // Check if ebook is not set and ebooks were found if (!media.ebookFile && !librarySettings.audiobooksOnly && libraryItemData.ebookLibraryFiles.length) { // Prefer to use an epub ebook then fallback to the first ebook found diff --git a/server/scanner/LibraryItemScanData.js b/server/scanner/LibraryItemScanData.js index b604e4d7..d5a4a7a2 100644 --- a/server/scanner/LibraryItemScanData.js +++ b/server/scanner/LibraryItemScanData.js @@ -4,6 +4,12 @@ const LibraryItem = require('../models/LibraryItem') const globals = require('../utils/globals') class LibraryItemScanData { + /** + * @typedef LibraryFileModifiedObject + * @property {LibraryItem.LibraryFileObject} old + * @property {LibraryItem.LibraryFileObject} new + */ + constructor(data) { /** @type {string} */ this.libraryFolderId = data.libraryFolderId @@ -39,7 +45,7 @@ class LibraryItemScanData { this.libraryFilesRemoved = [] /** @type {LibraryItem.LibraryFileObject[]} */ this.libraryFilesAdded = [] - /** @type {LibraryItem.LibraryFileObject[]} */ + /** @type {LibraryFileModifiedObject[]} */ this.libraryFilesModified = [] } @@ -77,9 +83,9 @@ class LibraryItemScanData { return (this.audioLibraryFilesRemoved.length + this.audioLibraryFilesAdded.length + this.audioLibraryFilesModified.length) > 0 } - /** @type {LibraryItem.LibraryFileObject[]} */ + /** @type {LibraryFileModifiedObject[]} */ get audioLibraryFilesModified() { - return this.libraryFilesModified.filter(lf => globals.SupportedAudioTypes.includes(lf.metadata.ext?.slice(1).toLowerCase() || '')) + return this.libraryFilesModified.filter(lf => globals.SupportedAudioTypes.includes(lf.old.metadata.ext?.slice(1).toLowerCase() || '')) } /** @type {LibraryItem.LibraryFileObject[]} */ @@ -97,12 +103,42 @@ class LibraryItemScanData { return this.libraryFiles.filter(lf => globals.SupportedAudioTypes.includes(lf.metadata.ext?.slice(1).toLowerCase() || '')) } + /** @type {LibraryFileModifiedObject[]} */ + get imageLibraryFilesModified() { + return this.libraryFilesModified.filter(lf => globals.SupportedImageTypes.includes(lf.old.metadata.ext?.slice(1).toLowerCase() || '')) + } + + /** @type {LibraryItem.LibraryFileObject[]} */ + get imageLibraryFilesRemoved() { + return this.libraryFilesRemoved.filter(lf => globals.SupportedImageTypes.includes(lf.metadata.ext?.slice(1).toLowerCase() || '')) + } + + /** @type {LibraryItem.LibraryFileObject[]} */ + get imageLibraryFilesAdded() { + return this.libraryFilesAdded.filter(lf => globals.SupportedImageTypes.includes(lf.metadata.ext?.slice(1).toLowerCase() || '')) + } + /** @type {LibraryItem.LibraryFileObject[]} */ get imageLibraryFiles() { return this.libraryFiles.filter(lf => globals.SupportedImageTypes.includes(lf.metadata.ext?.slice(1).toLowerCase() || '')) } - /** @type {import('../objects/files/LibraryFile')[]} */ + /** @type {LibraryFileModifiedObject[]} */ + get ebookLibraryFilesModified() { + return this.libraryFilesModified.filter(lf => globals.SupportedEbookTypes.includes(lf.old.metadata.ext?.slice(1).toLowerCase() || '')) + } + + /** @type {LibraryItem.LibraryFileObject[]} */ + get ebookLibraryFilesRemoved() { + return this.libraryFilesRemoved.filter(lf => globals.SupportedEbookTypes.includes(lf.metadata.ext?.slice(1).toLowerCase() || '')) + } + + /** @type {LibraryItem.LibraryFileObject[]} */ + get ebookLibraryFilesAdded() { + return this.libraryFilesAdded.filter(lf => globals.SupportedEbookTypes.includes(lf.metadata.ext?.slice(1).toLowerCase() || '')) + } + + /** @type {LibraryItem.LibraryFileObject[]} */ get ebookLibraryFiles() { return this.libraryFiles.filter(lf => globals.SupportedEbookTypes.includes(lf.metadata.ext?.slice(1).toLowerCase() || '')) } @@ -153,7 +189,7 @@ class LibraryItemScanData { existingLibraryItem[key] = this[key] this.hasChanges = true - if (key === 'relPath') { + if (key === 'relPath' || key === 'path') { this.hasPathChange = true } } @@ -202,8 +238,9 @@ class LibraryItemScanData { this.hasChanges = true } else { libraryFilesAdded = libraryFilesAdded.filter(lf => lf !== matchingLibraryFile) + let existingLibraryFileBefore = structuredClone(existingLibraryFile) if (this.compareUpdateLibraryFile(existingLibraryItem.path, existingLibraryFile, matchingLibraryFile, libraryScan)) { - this.libraryFilesModified.push(existingLibraryFile) + this.libraryFilesModified.push({old: existingLibraryFileBefore, new: existingLibraryFile}) this.hasChanges = true } } diff --git a/server/scanner/PodcastScanner.js b/server/scanner/PodcastScanner.js index 07dcbb11..4958d5f7 100644 --- a/server/scanner/PodcastScanner.js +++ b/server/scanner/PodcastScanner.js @@ -71,7 +71,7 @@ class PodcastScanner { // Update audio files that were modified if (libraryItemData.audioLibraryFilesModified.length) { - let scannedAudioFiles = await AudioFileScanner.executeMediaFileScans(existingLibraryItem.mediaType, libraryItemData, libraryItemData.audioLibraryFilesModified) + let scannedAudioFiles = await AudioFileScanner.executeMediaFileScans(existingLibraryItem.mediaType, libraryItemData, libraryItemData.audioLibraryFilesModified.map(lf => lf.new)) for (const podcastEpisode of existingPodcastEpisodes) { let matchedScannedAudioFile = scannedAudioFiles.find(saf => saf.metadata.path === podcastEpisode.audioFile.metadata.path) @@ -132,11 +132,25 @@ class PodcastScanner { let hasMediaChanges = false // Check if cover was removed - if (media.coverPath && !libraryItemData.imageLibraryFiles.some(lf => lf.metadata.path === media.coverPath)) { + if (media.coverPath && libraryItemData.imageLibraryFilesRemoved.some(lf => lf.metadata.path === media.coverPath)) { media.coverPath = null hasMediaChanges = true } + // Update cover if it was modified + if (media.coverPath && libraryItemData.imageLibraryFilesModified.length) { + let coverMatch = libraryItemData.imageLibraryFilesModified.find(iFile => iFile.old.metadata.path === media.coverPath) + if (coverMatch) { + const coverPath = coverMatch.new.metadata.path + if (coverPath !== media.coverPath) { + libraryScan.addLog(LogLevel.DEBUG, `Updating podcast cover "${media.coverPath}" => "${coverPath}" for podcast "${media.title}"`) + media.coverPath = coverPath + media.changed('coverPath', true) + hasMediaChanges = true + } + } + } + // Check if cover is not set and image files were found if (!media.coverPath && libraryItemData.imageLibraryFiles.length) { // Prefer using a cover image with the name "cover" otherwise use the first image From f661e0835ce3653640dabcc19559348c0c70dff2 Mon Sep 17 00:00:00 2001 From: Denis Arnst Date: Tue, 19 Mar 2024 19:18:38 +0100 Subject: [PATCH 0015/1695] Auth: Simplify Code --- server/Auth.js | 277 ++++++++++++++++++++++++++----------------------- 1 file changed, 147 insertions(+), 130 deletions(-) diff --git a/server/Auth.js b/server/Auth.js index a4cdd1fc..368f9a4d 100644 --- a/server/Auth.js +++ b/server/Auth.js @@ -98,139 +98,156 @@ class Auth { scope: 'openid profile email' } }, async (tokenset, userinfo, done) => { - Logger.debug(`[Auth] openid callback userinfo=`, JSON.stringify(userinfo, null, 2)) + try { + Logger.debug(`[Auth] openid callback userinfo=`, JSON.stringify(userinfo, null, 2)) + + if (!userinfo.sub) { + throw new Error('Invalid userinfo, no sub') + } + + if (!this.validateGroupClaim(userinfo)) { + throw new Error(`Group claim ${Database.serverSettings.authOpenIDGroupClaim} not found or empty in userinfo`) + } + + let user = await this.findOrCreateUser(userinfo) + + if (!user || !user.isActive) { + throw new Error('User not active or not found') + } + + await this.setUserGroup(user, userinfo) + await this.updateUserPermissions(user, userinfo) + + // We also have to save the id_token for later (used for logout) because we cannot set cookies here + user.openid_id_token = tokenset.id_token - let failureMessage = 'Unauthorized' - if (!userinfo.sub) { - Logger.error(`[Auth] openid callback invalid userinfo, no sub`) - return done(null, null, failureMessage) + return done(null, user) + } catch (error) { + Logger.error(`[Auth] openid callback error: ${error?.message}\n${error?.stack}`) + + return done(null, null, 'Unauthorized') } - - // Check if the claims itself are returned correctly - const groupClaimName = Database.serverSettings.authOpenIDGroupClaim; - if (groupClaimName) { - if (!userinfo[groupClaimName]) { - Logger.error(`[Auth] openid callback invalid: Group claim ${groupClaimName} configured, but not found or empty in userinfo`) - return done(null, null, failureMessage) - } - - const groupsList = userinfo[groupClaimName] - const targetRoles = ['admin', 'user', 'guest'] - - // Convert the list to lowercase for case-insensitive comparison - const groupsListLowercase = groupsList.map(group => group.toLowerCase()) - - // Check if any of the target roles exist in the groups list - const containsTargetRole = targetRoles.some(role => groupsListLowercase.includes(role.toLowerCase())) - - if (!containsTargetRole) { - Logger.info(`[Auth] openid callback: Denying access because neither admin nor user or guest is included is inside the group claim. Groups found: `, groupsList) - return done(null, null, failureMessage) - } - } - - const advancedPermsClaimName = Database.serverSettings.authOpenIDAdvancedPermsClaim - if (advancedPermsClaimName && !userinfo[advancedPermsClaimName]) { - Logger.error(`[Auth] openid callback invalid: Advanced perms claim ${advancedPermsClaimName} configured, but not found or empty in userinfo`) - return done(null, null, failureMessage) - } - - // First check for matching user by sub - let user = await Database.userModel.getUserByOpenIDSub(userinfo.sub) - if (!user) { - // Optionally match existing by email or username based on server setting "authOpenIDMatchExistingBy" - if (Database.serverSettings.authOpenIDMatchExistingBy === 'email' && userinfo.email && userinfo.email_verified) { - Logger.info(`[Auth] openid: User not found, checking existing with email "${userinfo.email}"`) - user = await Database.userModel.getUserByEmail(userinfo.email) - // Check that user is not already matched - if (user?.authOpenIDSub) { - Logger.warn(`[Auth] openid: User found with email "${userinfo.email}" but is already matched with sub "${user.authOpenIDSub}"`) - // TODO: Message isn't actually returned to the user yet. Need to override the passport authenticated callback - failureMessage = 'A matching user was found but is already matched with another user from your auth provider' - user = null - } - } else if (Database.serverSettings.authOpenIDMatchExistingBy === 'username' && userinfo.preferred_username) { - Logger.info(`[Auth] openid: User not found, checking existing with username "${userinfo.preferred_username}"`) - user = await Database.userModel.getUserByUsername(userinfo.preferred_username) - // Check that user is not already matched - if (user?.authOpenIDSub) { - Logger.warn(`[Auth] openid: User found with username "${userinfo.preferred_username}" but is already matched with sub "${user.authOpenIDSub}"`) - // TODO: Message isn't actually returned to the user yet. Need to override the passport authenticated callback - failureMessage = 'A matching user was found but is already matched with another user from your auth provider' - user = null - } - } - - // If existing user was matched and isActive then save sub to user - if (user?.isActive) { - Logger.info(`[Auth] openid: New user found matching existing user "${user.username}"`) - user.authOpenIDSub = userinfo.sub - await Database.userModel.updateFromOld(user) - } else if (user && !user.isActive) { - Logger.warn(`[Auth] openid: New user found matching existing user "${user.username}" but that user is deactivated`) - } - - // Optionally auto register the user - if (!user && Database.serverSettings.authOpenIDAutoRegister) { - Logger.info(`[Auth] openid: Auto-registering user with sub "${userinfo.sub}"`, userinfo) - user = await Database.userModel.createUserFromOpenIdUserInfo(userinfo, this) - } - } - - if (!user?.isActive) { - if (user && !user.isActive) { - failureMessage = 'Unauthorized' - } - // deny login - done(null, null, failureMessage) - return - } - - // Set user group if name of groups claim is configured - if (groupClaimName) { - const groupsList = userinfo[groupClaimName] ? userinfo[groupClaimName].map(group => group.toLowerCase()) : [] - const rolesInOrderOfPriority = ['admin', 'user', 'guest'] - - let userType = null - - for (let role of rolesInOrderOfPriority) { - if (groupsList.includes(role)) { - userType = role // This will override with the highest priority role found - break // Stop searching once the highest priority role is found - } - } - - // Actually already checked above, but just to be sure - if (!userType) { - Logger.error(`[Auth] openid callback: Denying access because neither admin nor user or guest is included is inside the group claim. Groups found: `, groupsList) - return done(null, null, failureMessage) - } - - Logger.debug(`[Auth] openid callback: Setting user ${user.username} type to ${userType}`) - user.type = userType - await Database.userModel.updateFromOld(user) - } - - if (advancedPermsClaimName) { - try { - Logger.debug(`[Auth] openid callback: Updating advanced perms for user ${user.username} to ${JSON.stringify(userinfo[advancedPermsClaimName])}`) - - user.updatePermissionsFromExternalJSON(userinfo[advancedPermsClaimName]) - await Database.userModel.updateFromOld(user) - } catch (error) { - Logger.error(`[Auth] openid callback: Error updating advanced perms for user, error: `, error) - return done(null, null, failureMessage) - } - } - - // We also have to save the id_token for later (used for logout) because we cannot set cookies here - user.openid_id_token = tokenset.id_token - - // permit login - return done(null, user) })) } + /** + * Finds an existing user by OpenID subject identifier, or by email/username based on server settings, + * or creates a new user if configured to do so. + */ + async findOrCreateUser(userinfo) { + let user = await Database.userModel.getUserByOpenIDSub(userinfo.sub) + + // Matched by sub + if (user) { + Logger.debug(`[Auth] openid: User found by sub`) + return user + } + + // Match existing user by email + if (Database.serverSettings.authOpenIDMatchExistingBy === 'email' && userinfo.email && userinfo.email_verified) { + Logger.info(`[Auth] openid: User not found, checking existing with email "${userinfo.email}"`) + user = await Database.userModel.getUserByEmail(userinfo.email) + + if (user?.authOpenIDSub) { + Logger.warn(`[Auth] openid: User found with email "${userinfo.email}" but is already matched with sub "${user.authOpenIDSub}"`) + return null // User is linked to a different OpenID subject; do not proceed. + } + } + // Match existing user by username + else if (Database.serverSettings.authOpenIDMatchExistingBy === 'username' && userinfo.preferred_username) { + Logger.info(`[Auth] openid: User not found, checking existing with username "${userinfo.preferred_username}"`) + user = await Database.userModel.getUserByUsername(userinfo.preferred_username) + + if (user?.authOpenIDSub) { + Logger.warn(`[Auth] openid: User found with username "${userinfo.preferred_username}" but is already matched with sub "${user.authOpenIDSub}"`) + return null // User is linked to a different OpenID subject; do not proceed. + } + } + + // Found existing user via email or username + if (user) { + if (!user.isActive) { + Logger.warn(`[Auth] openid: User found but is not active`) + return null + } + + user.authOpenIDSub = userinfo.sub + await Database.userModel.updateFromOld(user) + + Logger.debug(`[Auth] openid: User found by email/username`) + return user + } + + // If no existing user was matched, auto-register if configured + if (Database.serverSettings.authOpenIDAutoRegister) { + Logger.info(`[Auth] openid: Auto-registering user with sub "${userinfo.sub}"`, userinfo) + user = await Database.userModel.createUserFromOpenIdUserInfo(userinfo, this) + return user + } + + Logger.warn(`[Auth] openid: User not found and auto-register is disabled`) + return null + } + + /** + * Validates the presence and content of the group claim in userinfo. + */ + validateGroupClaim(userinfo) { + const groupClaimName = Database.serverSettings.authOpenIDGroupClaim; + if (!groupClaimName) // Allow no group claim when configured like this + return true + + // If configured it must exist in userinfo + if (!userinfo[groupClaimName]) { + return false + } + return true + } + +/** + * Sets the user group based on group claim in userinfo. + */ +async setUserGroup(user, userinfo) { + const groupClaimName = Database.serverSettings.authOpenIDGroupClaim; + if (!groupClaimName) // No group claim configured, don't set anything + return + + if (!userinfo[groupClaimName]) + throw new Error(`Group claim ${groupClaimName} not found in userinfo`) + + const groupsList = userinfo[groupClaimName].map(group => group.toLowerCase()) + const rolesInOrderOfPriority = ['admin', 'user', 'guest'] + + let userType = rolesInOrderOfPriority.find(role => groupsList.includes(role)) + if (userType) { + Logger.debug(`[Auth] openid callback: Setting user ${user.username} type to ${userType}`) + + if (user.type !== userType) { + user.type = userType; + await Database.userModel.updateFromOld(user) + } + } else { + throw new Error(`No valid group found in userinfo: ${JSON.stringify(userinfo[groupClaimName], null, 2)}`) + } +} + +/** + * Updates user permissions based on the advanced permissions claim. + */ +async updateUserPermissions(user, userinfo) { + const absPermissionsClaim = Database.serverSettings.authOpenIDAdvancedPermsClaim + if (!absPermissionsClaim) // No advanced permissions claim configured, don't set anything + return + + const absPermissions = userinfo[absPermissionsClaim] + if (!absPermissions) + throw new Error(`Advanced permissions claim ${absPermissionsClaim} not found in userinfo`) + + Logger.debug(`[Auth] openid callback: Updating advanced perms for user ${user.username} to ${JSON.stringify(absPermissions)}`) + user.updatePermissionsFromExternalJSON(absPermissions) + await Database.userModel.updateFromOld(user) +} + /** * Unuse strategy * @@ -421,7 +438,7 @@ class Auth { res.redirect(authorizationUrl) } catch (error) { - Logger.error(`[Auth] Error in /auth/openid route: ${error}`) + Logger.error(`[Auth] Error in /auth/openid route: ${error}\n${error?.stack}`) res.status(500).send('Internal Server Error') } @@ -477,7 +494,7 @@ class Auth { // Redirect to the overwrite URI saved in the map res.redirect(redirectUri) } catch (error) { - Logger.error(`[Auth] Error in /auth/openid/mobile-redirect route: ${error}`) + Logger.error(`[Auth] Error in /auth/openid/mobile-redirect route: ${error}\n${error?.stack}`) res.status(500).send('Internal Server Error') } }) From 50330b0a606901f320cdb8eda802575db2aa3ae6 Mon Sep 17 00:00:00 2001 From: Denis Arnst Date: Tue, 19 Mar 2024 19:18:47 +0100 Subject: [PATCH 0016/1695] Auth: Add translations --- client/pages/config/authentication.vue | 11 +++-------- client/strings/de.json | 3 +++ client/strings/en-us.json | 3 +++ 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/client/pages/config/authentication.vue b/client/pages/config/authentication.vue index 91c6cfe2..cecccee4 100644 --- a/client/pages/config/authentication.vue +++ b/client/pages/config/authentication.vue @@ -82,15 +82,12 @@

{{ $strings.LabelAutoRegisterDescription }}

-
Leave the following options empty to disable advanced group and permissions assignment, automatically assigning 'User' group then.
+
{{ $strings.LabelOpenIDClaims }}
-

- Name of the OpenID claim that contains a list of the user's groups. Commonly referred to as groups. If configured, the application will automatically assign roles based on the user's group memberships, provided that these groups are named case-insensitively 'admin', 'user', or 'guest' in the claim. The claim should contain a list, and if a user belongs to - multiple groups, the application will assign the role corresponding to the highest level of access. If no group matches, access will be denied. -

+

@@ -98,9 +95,7 @@
-

- Name of the OpenID claim that contains advanced permissions for user actions within the application which will apply to non-admin roles (if configured). If the claim is missing from the response, access to ABS will be denied. If a single option is missing, it will be treated as false. Ensure the identity provider's claim matches the expected structure: -

+

{{ newAuthSettings.authOpenIDSamplePermissions }}
                 
diff --git a/client/strings/de.json b/client/strings/de.json index ed99f095..611432f1 100644 --- a/client/strings/de.json +++ b/client/strings/de.json @@ -387,6 +387,9 @@ "LabelNotStarted": "Nicht begonnen", "LabelNumberOfBooks": "Anzahl der Hörbücher", "LabelNumberOfEpisodes": "Anzahl der Episoden", + "LabelOpenIDClaims": "Lass die folgenden Optionen leer, um die erweiterte Zuweisung von Gruppen und Berechtigungen zu deaktivieren und automatisch die 'User'-Gruppe zuzuweisen.", + "LabelOpenIDGroupClaimDescription": "Name des OpenID-Claims, der eine Liste der Benutzergruppen enthält. Wird häufig als groups bezeichnet. Wenn konfiguriert, wird die Anwendung automatisch Rollen basierend auf den Gruppenmitgliedschaften des Benutzers zuweisen, vorausgesetzt, dass diese Gruppen im Claim als 'admin', 'user' oder 'guest' benannt sind (Groß/Kleinschreibung ist irrelevant). Der Claim eine Liste sein, und wenn ein Benutzer mehreren Gruppen angehört, wird die Anwendung die Rolle zuordnen, die dem höchsten Zugriffslevel entspricht. Wenn keine Gruppe übereinstimmt, wird der Zugang verweigert.", + "LabelOpenIDAdvancedPermsClaimDescription": "Name des OpenID-Claims, der erweiterte Berechtigungen für Benutzeraktionen innerhalb der Anwendung enthält, die auf Nicht-Admin-Rollen angewendet werden (wenn konfiguriert). Wenn der Claim in der Antwort fehlt, wird der Zugang zu ABS verweigert. Fehlt eine einzelne Option, wird sie als false behandelt. Stelle sicher, dass der Claim des Identitätsanbieters der erwarteten Struktur entspricht:", "LabelOpenRSSFeed": "Öffne RSS-Feed", "LabelOverwrite": "Überschreiben", "LabelPassword": "Passwort", diff --git a/client/strings/en-us.json b/client/strings/en-us.json index 43a1ef44..b6fe3505 100644 --- a/client/strings/en-us.json +++ b/client/strings/en-us.json @@ -387,6 +387,9 @@ "LabelNotStarted": "Not Started", "LabelNumberOfBooks": "Number of Books", "LabelNumberOfEpisodes": "# of Episodes", + "LabelOpenIDClaims": "Leave the following options empty to disable advanced group and permissions assignment, automatically assigning 'User' group then.", + "LabelOpenIDGroupClaimDescription": "Name of the OpenID claim that contains a list of the user's groups. Commonly referred to as groups. If configured, the application will automatically assign roles based on the user's group memberships, provided that these groups are named case-insensitively 'admin', 'user', or 'guest' in the claim. The claim should contain a list, and if a user belongs to multiple groups, the application will assign the role corresponding to the highest level of access. If no group matches, access will be denied.", + "LabelOpenIDAdvancedPermsClaimDescription": "Name of the OpenID claim that contains advanced permissions for user actions within the application which will apply to non-admin roles (if configured). If the claim is missing from the response, access to ABS will be denied. If a single option is missing, it will be treated as false. Ensure the identity provider's claim matches the expected structure:", "LabelOpenRSSFeed": "Open RSS Feed", "LabelOverwrite": "Overwrite", "LabelPassword": "Password", From 1646f0ebc21505a1ed00866cb7a033c5028ba5c4 Mon Sep 17 00:00:00 2001 From: Denis Arnst Date: Tue, 19 Mar 2024 19:35:34 +0100 Subject: [PATCH 0017/1695] OpenID: Ignore admin for advanced permissions Also removed some semicolons --- server/Auth.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/server/Auth.js b/server/Auth.js index 368f9a4d..e14348c7 100644 --- a/server/Auth.js +++ b/server/Auth.js @@ -193,7 +193,7 @@ class Auth { * Validates the presence and content of the group claim in userinfo. */ validateGroupClaim(userinfo) { - const groupClaimName = Database.serverSettings.authOpenIDGroupClaim; + const groupClaimName = Database.serverSettings.authOpenIDGroupClaim if (!groupClaimName) // Allow no group claim when configured like this return true @@ -208,7 +208,7 @@ class Auth { * Sets the user group based on group claim in userinfo. */ async setUserGroup(user, userinfo) { - const groupClaimName = Database.serverSettings.authOpenIDGroupClaim; + const groupClaimName = Database.serverSettings.authOpenIDGroupClaim if (!groupClaimName) // No group claim configured, don't set anything return @@ -223,7 +223,7 @@ async setUserGroup(user, userinfo) { Logger.debug(`[Auth] openid callback: Setting user ${user.username} type to ${userType}`) if (user.type !== userType) { - user.type = userType; + user.type = userType await Database.userModel.updateFromOld(user) } } else { @@ -239,6 +239,9 @@ async updateUserPermissions(user, userinfo) { if (!absPermissionsClaim) // No advanced permissions claim configured, don't set anything return + if (user.type === 'admin') + return + const absPermissions = userinfo[absPermissionsClaim] if (!absPermissions) throw new Error(`Advanced permissions claim ${absPermissionsClaim} not found in userinfo`) From 1d7434cbbbbe3d3f1349d266bc717d9c11e07367 Mon Sep 17 00:00:00 2001 From: Illia Pyshniak Date: Tue, 19 Mar 2024 23:12:29 +0200 Subject: [PATCH 0018/1695] Create uk.json --- client/strings/uk.json | 781 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 781 insertions(+) create mode 100644 client/strings/uk.json diff --git a/client/strings/uk.json b/client/strings/uk.json new file mode 100644 index 00000000..9953bda5 --- /dev/null +++ b/client/strings/uk.json @@ -0,0 +1,781 @@ +{ + "ButtonAdd": "Додати", + "ButtonAddChapters": "Додати глави", + "ButtonAddDevice": "Додати пристрій", + "ButtonAddLibrary": "Додати бібліотеку", + "ButtonAddPodcasts": "Додати подкаст", + "ButtonAddUser": "Додати користувача", + "ButtonAddYourFirstLibrary": "Додайте вашу першу бібліотеку", + "ButtonApply": "Застосувати", + "ButtonApplyChapters": "Зберегти глави", + "ButtonAuthors": "Автори", + "ButtonBrowseForFolder": "Огляд тек", + "ButtonCancel": "Скасувати", + "ButtonCancelEncode": "Скасувати кодування", + "ButtonChangeRootPassword": "Змінити кореневий пароль", + "ButtonCheckAndDownloadNewEpisodes": "Перевірити та завантажити нові епізоди", + "ButtonChooseAFolder": "Обрати теку", + "ButtonChooseFiles": "Обрати файли", + "ButtonClearFilter": "Очистити фільтр", + "ButtonCloseFeed": "Закрити стрічку", + "ButtonCollections": "Колекції", + "ButtonConfigureScanner": "Налаштувати сканер", + "ButtonCreate": "Створити", + "ButtonCreateBackup": "Створити резервну копію", + "ButtonDelete": "Видалити", + "ButtonDownloadQueue": "Черга", + "ButtonEdit": "Редагувати", + "ButtonEditChapters": "Редагувати глави", + "ButtonEditPodcast": "Редагувати подкаст", + "ButtonForceReScan": "Примусово сканувати", + "ButtonFullPath": "Повний шлях", + "ButtonHide": "Приховати", + "ButtonHome": "Головна", + "ButtonIssues": "Проблеми", + "ButtonJumpBackward": "Перейти назад", + "ButtonJumpForward": "Перейти вперед", + "ButtonLatest": "Останній", + "ButtonLibrary": "Бібліотека", + "ButtonLogout": "Вийти", + "ButtonLookup": "Пошук", + "ButtonManageTracks": "Керувати доріжками", + "ButtonMapChapterTitles": "Призначити назви глав", + "ButtonMatchAllAuthors": "Віднайти усіх авторів", + "ButtonMatchBooks": "Віднайти книги", + "ButtonNevermind": "Скасувати", + "ButtonNext": "Наступний", + "ButtonNextChapter": "Наступна глава", + "ButtonOk": "Гаразд", + "ButtonOpenFeed": "Відкрити стрічку", + "ButtonOpenManager": "Відкрити менеджер", + "ButtonPause": "Пауза", + "ButtonPlay": "Грати", + "ButtonPlaying": "Відтворюється", + "ButtonPlaylists": "Списки відтворення", + "ButtonPrevious": "Попередній", + "ButtonPreviousChapter": "Попередня глава", + "ButtonPurgeAllCache": "Очистити весь кеш", + "ButtonPurgeItemsCache": "Очистити кеш елементів", + "ButtonPurgeMediaProgress": "Очистити прогрес", + "ButtonQueueAddItem": "Додати до черги", + "ButtonQueueRemoveItem": "Вилучити з черги", + "ButtonQuickMatch": "Швидкий пошук", + "ButtonRead": "Читати", + "ButtonRefresh": "Оновити", + "ButtonRemove": "Видалити", + "ButtonRemoveAll": "Видалити все", + "ButtonRemoveAllLibraryItems": "Видалити всі елементи бібліотеки", + "ButtonRemoveFromContinueListening": "Видалити з Продовжити слухати", + "ButtonRemoveFromContinueReading": "Видалити з Продовжити читання", + "ButtonRemoveSeriesFromContinueSeries": "Видалити серію з Продовжити серії", + "ButtonReScan": "Пересканувати", + "ButtonReset": "Скинути", + "ButtonResetToDefault": "Скинути до стандартних", + "ButtonRestore": "Відновити", + "ButtonSave": "Зберегти", + "ButtonSaveAndClose": "Зберегти та закрити", + "ButtonSaveTracklist": "Зберегти порядок", + "ButtonScan": "Сканувати", + "ButtonScanLibrary": "Сканувати бібліотеку", + "ButtonSearch": "Пошук", + "ButtonSelectFolderPath": "Обрати шлях до теки", + "ButtonSeries": "Серії", + "ButtonSetChaptersFromTracks": "Встановити глави за доріжками", + "ButtonShare": "Поширити", + "ButtonShiftTimes": "Зсунути час", + "ButtonShow": "Показати", + "ButtonStartM4BEncode": "Почати кодування у M4B", + "ButtonStartMetadataEmbed": "Почати вбудування метаданих", + "ButtonSubmit": "Надіслати", + "ButtonTest": "Перевірити", + "ButtonUpload": "Завантажити", + "ButtonUploadBackup": "Завантажити резервну копію", + "ButtonUploadCover": "Завантажити обкладинку", + "ButtonUploadOPMLFile": "Завантажити OPML-файл", + "ButtonUserDelete": "Видалити користувача {0}", + "ButtonUserEdit": "Редагувати користувача {0}", + "ButtonViewAll": "Переглянути все", + "ButtonYes": "Так", + "ErrorUploadFetchMetadataAPI": "Помилка при отриманні метаданих", + "ErrorUploadFetchMetadataNoResults": "Не вдалося отримати метадані — спробуйте оновити заголовок та/або автора", + "ErrorUploadLacksTitle": "Назва обов'язкова", + "HeaderAccount": "Профіль", + "HeaderAdvanced": "Розширені", + "HeaderAppriseNotificationSettings": "Налаштування сповіщень Apprise", + "HeaderAudiobookTools": "Інструменти керування файлами книг", + "HeaderAudioTracks": "Аудіодоріжки", + "HeaderAuthentication": "Автентифікація", + "HeaderBackups": "Резервні копії", + "HeaderChangePassword": "Змінити пароль", + "HeaderChapters": "Глави", + "HeaderChooseAFolder": "Обрати теку", + "HeaderCollection": "Колекція", + "HeaderCollectionItems": "Елементи колекції", + "HeaderCover": "Обкладинка", + "HeaderCurrentDownloads": "Поточні завантаження", + "HeaderCustomMetadataProviders": "Постачальники метаданих", + "HeaderDetails": "Подробиці", + "HeaderDownloadQueue": "Черга завантажень", + "HeaderEbookFiles": "Файли електронних книг", + "HeaderEmail": "Електронна пошта", + "HeaderEmailSettings": "Налаштування електронної пошти", + "HeaderEpisodes": "Епізоди", + "HeaderEreaderDevices": "Пристрої для читання", + "HeaderEreaderSettings": "Налаштування пристрою для читання", + "HeaderFiles": "Файли", + "HeaderFindChapters": "Пошук глав", + "HeaderIgnoredFiles": "Ігноровані файли", + "HeaderItemFiles": "Файли елементів", + "HeaderItemMetadataUtils": "Інструменти для метаданих", + "HeaderLastListeningSession": "Останній сеанс прослуховування", + "HeaderLatestEpisodes": "Останні епізоди", + "HeaderLibraries": "Бібліотеки", + "HeaderLibraryFiles": "Файли бібліотеки", + "HeaderLibraryStats": "Статистика бібліотеки", + "HeaderListeningSessions": "Сеанси прослуховування", + "HeaderListeningStats": "Статистика відтворення", + "HeaderLogin": "Вхід", + "HeaderLogs": "Журнал", + "HeaderManageGenres": "Керувати жанрами", + "HeaderManageTags": "Керувати мітками", + "HeaderMapDetails": "Призначити подробиці", + "HeaderMatch": "Пошук", + "HeaderMetadataOrderOfPrecedence": "Порядок метаданих", + "HeaderMetadataToEmbed": "Вбудувати метадані", + "HeaderNewAccount": "Новий профіль", + "HeaderNewLibrary": "Нова бібліотека", + "HeaderNotifications": "Сповіщення", + "HeaderOpenIDConnectAuthentication": "Автентифікація OpenID Connect", + "HeaderOpenRSSFeed": "Відкрити RSS-канал", + "HeaderOtherFiles": "Інші файли", + "HeaderPasswordAuthentication": "Автентифікація за паролем", + "HeaderPermissions": "Дозволи", + "HeaderPlayerQueue": "Черга відтворення", + "HeaderPlaylist": "Список відтворення", + "HeaderPlaylistItems": "Елементи списку відтворення", + "HeaderPodcastsToAdd": "Додати подкасти", + "HeaderPreviewCover": "Попередній перегляд", + "HeaderRemoveEpisode": "Видалити епізод", + "HeaderRemoveEpisodes": "Видалити епізодів: {0}", + "HeaderRSSFeedGeneral": "Подробиці RSS", + "HeaderRSSFeedIsOpen": "RSS-канал відкрито", + "HeaderRSSFeeds": "RSS-канали", + "HeaderSavedMediaProgress": "Збережений прогрес медіа", + "HeaderSchedule": "Розклад", + "HeaderScheduleLibraryScans": "Розклад автосканування бібліотеки", + "HeaderSession": "Сеанс", + "HeaderSetBackupSchedule": "Встановити розклад резервного копіювання", + "HeaderSettings": "Налаштування", + "HeaderSettingsDisplay": "Відображення", + "HeaderSettingsExperimental": "Експериментальні функції", + "HeaderSettingsGeneral": "Основне", + "HeaderSettingsScanner": "Сканер", + "HeaderSleepTimer": "Таймер вимкнення", + "HeaderStatsLargestItems": "Найбільші елементи", + "HeaderStatsLongestItems": "Найдовші елементи (год)", + "HeaderStatsMinutesListeningChart": "Хвилин прослухано (останні 7 днів)", + "HeaderStatsRecentSessions": "Останні сеанси", + "HeaderStatsTop10Authors": "10 улюблених авторів", + "HeaderStatsTop5Genres": "5 улюблених жанрів", + "HeaderTableOfContents": "Зміст", + "HeaderTools": "Інструменти", + "HeaderUpdateAccount": "Оновити профіль", + "HeaderUpdateAuthor": "Оновити автора", + "HeaderUpdateDetails": "Оновити подробиці", + "HeaderUpdateLibrary": "Оновити бібліотеку", + "HeaderUsers": "Користувачі", + "HeaderYearReview": "Підсумки {0} року", + "HeaderYourStats": "Ваша статистика", + "LabelAbridged": "Скорочена", + "LabelAccountType": "Тип профілю", + "LabelAccountTypeAdmin": "Адміністратор", + "LabelAccountTypeGuest": "Гість", + "LabelAccountTypeUser": "Користувач", + "LabelActivity": "Активність", + "LabelAdded": "Додано", + "LabelAddedAt": "Дата додавання", + "LabelAddToCollection": "Додати у добірку", + "LabelAddToCollectionBatch": "Додати книги до добірки: {0}", + "LabelAddToPlaylist": "Додати до списку відтворення", + "LabelAddToPlaylistBatch": "Додано елементів у список відтворення: {0}", + "LabelAdminUsersOnly": "Тільки для адміністраторів", + "LabelAll": "Усе", + "LabelAllUsers": "Усі користувачі", + "LabelAllUsersExcludingGuests": "Усі, крім гостей", + "LabelAllUsersIncludingGuests": "Усі, включно з гостями", + "LabelAlreadyInYourLibrary": "Вже у вашій бібліотеці", + "LabelAppend": "Додати", + "LabelAuthor": "Автор", + "LabelAuthorFirstLast": "Автор (за ім'ям)", + "LabelAuthorLastFirst": "Автор (за прізвищем)", + "LabelAuthors": "Автори", + "LabelAutoDownloadEpisodes": "Автозавантаження епізодів", + "LabelAutoFetchMetadata": "Автозавантаження метаданих", + "LabelAutoFetchMetadataHelp": "Отримує метадані про назву, автора та серію під час послідового завантаження. Після завантаження може знадобитися пошук додаткових метаданих.", + "LabelAutoLaunch": "Автозапуск", + "LabelAutoLaunchDescription": "Автоматично перенаправляти зі сторінки входу до сервісу автентифікації (ручний перезапис шляху /login?autoLaunch=0)", + "LabelAutoRegister": "Автореєстрація", + "LabelAutoRegisterDescription": "Автоматично створювати нових користувачів після входу", + "LabelBackToUser": "Повернутися до користувача", + "LabelBackupLocation": "Розташування резервних копій", + "LabelBackupsEnableAutomaticBackups": "Автоматичне резервне копіювання", + "LabelBackupsEnableAutomaticBackupsHelp": "Резервні копії збережено у /metadata/backups", + "LabelBackupsMaxBackupSize": "Максимальний розмір резервної копії (у ГБ)", + "LabelBackupsMaxBackupSizeHelp": "У якості захисту від неправильного налаштування, резервну копію не буде збережено, якщо її розмір перевищуватиме вказаний.", + "LabelBackupsNumberToKeep": "Кількість резервних копій", + "LabelBackupsNumberToKeepHelp": "Лиш 1 резервну копію буде видалено за раз, тож якщо їх багато, то вам варто видалити їх вручну.", + "LabelBitrate": "Бітрейт", + "LabelBooks": "Книги", + "LabelButtonText": "Текст кнопки", + "LabelChangePassword": "Змінити пароль", + "LabelChannels": "Канали", + "LabelChapters": "Глави", + "LabelChaptersFound": "глав знайдено", + "LabelChapterTitle": "Назва глави", + "LabelClickForMoreInfo": "Натисніть, щоб дізнатися більше", + "LabelClosePlayer": "Закрити програвач", + "LabelCodec": "Кодек", + "LabelCollapseSeries": "Згорнути серії", + "LabelCollection": "Добірка", + "LabelCollections": "Добірки", + "LabelComplete": "Завершити", + "LabelConfirmPassword": "Підтвердити пароль", + "LabelContinueListening": "Слухати далі", + "LabelContinueReading": "Читати далі", + "LabelContinueSeries": "Продовжити серії", + "LabelCover": "Обкладинка", + "LabelCoverImageURL": "URL-адреса обкладинки", + "LabelCreatedAt": "Дата створення", + "LabelCronExpression": "Команда cron", + "LabelCurrent": "Поточне", + "LabelCurrently": "Поточний:", + "LabelCustomCronExpression": "Спецільна команда cron:", + "LabelDatetime": "Дата й час", + "LabelDeleteFromFileSystemCheckbox": "Видалити з файлової системи (зніміть прапорець, щоб видалити лише з бази даних)", + "LabelDescription": "Опис", + "LabelDeselectAll": "Скасувати вибір", + "LabelDevice": "Пристрій", + "LabelDeviceInfo": "Про пристрій", + "LabelDeviceIsAvailableTo": "Пристрій доступний для...", + "LabelDirectory": "Каталог", + "LabelDiscFromFilename": "Диск за назвою файлу", + "LabelDiscFromMetadata": "Диск за метаданими", + "LabelDiscover": "Огляд", + "LabelDownload": "Завантажити", + "LabelDownloadNEpisodes": "Завантажити епізодів: {0}", + "LabelDuration": "Тривалість", + "LabelDurationFound": "Виявлена тривалість:", + "LabelEbook": "Електронна книга", + "LabelEbooks": "Електронні книги", + "LabelEdit": "Редагувати", + "LabelEmail": "Електронна пошта", + "LabelEmailSettingsFromAddress": "Адреса відправника", + "LabelEmailSettingsSecure": "Безпечне", + "LabelEmailSettingsSecureHelp": "Увімкніть, аби використовувати TLS при підключенні до сервера. Якщо вимкнути, то TLS буде використано, якщо сервер підтримує STARTTLS. Увімкніть, якщо ви підключаєтеся до порту 465. Вимкніть для портів 587 або 25. (з nodemailer.com/smtp/#authentication)", + "LabelEmailSettingsTestAddress": "Тестова адреса", + "LabelEmbeddedCover": "Вбудована обкладинка", + "LabelEnable": "Увімкнути", + "LabelEnd": "Кінець", + "LabelEpisode": "Епізод", + "LabelEpisodeTitle": "Назва епізоду", + "LabelEpisodeType": "Тип епізоду", + "LabelExample": "Приклад", + "LabelExplicit": "Відверта", + "LabelFeedURL": "Адреса стрічки", + "LabelFetchingMetadata": "Отримання метаданих", + "LabelFile": "Файл", + "LabelFileBirthtime": "Дата створення", + "LabelFileModified": "Дата змінення", + "LabelFilename": "Ім'я файлу", + "LabelFilterByUser": "Фільтрувати за користувачем", + "LabelFindEpisodes": "Знайти епізоди", + "LabelFinished": "Завершено", + "LabelFolder": "Тека", + "LabelFolders": "Теки", + "LabelFontBold": "Жирний", + "LabelFontFamily": "Гарнітура", + "LabelFontItalic": "Курсив", + "LabelFontScale": "Розмір шрифту", + "LabelFontStrikethrough": "Закреслений", + "LabelFormat": "Формат", + "LabelGenre": "Жанр", + "LabelGenres": "Жанри", + "LabelHardDeleteFile": "Остаточно видалити файл", + "LabelHasEbook": "Має електронну книгу", + "LabelHasSupplementaryEbook": "Має додаткову електронну книгу", + "LabelHighestPriority": "Найвищий пріоритет", + "LabelHost": "Гост", + "LabelHour": "Година", + "LabelIcon": "Іконка", + "LabelImageURLFromTheWeb": "URL зображення з мережі", + "LabelIncludeInTracklist": "Включити у список", + "LabelIncomplete": "Не завершено", + "LabelInProgress": "У процесі", + "LabelInterval": "Частота", + "LabelIntervalCustomDailyWeekly": "Налаштувати щодня/щотижня", + "LabelIntervalEvery12Hours": "Кожні 12 годин", + "LabelIntervalEvery15Minutes": "Кожні 15 хвилин", + "LabelIntervalEvery2Hours": "Кожні 2 години", + "LabelIntervalEvery30Minutes": "Кожні 30 хвилин", + "LabelIntervalEvery6Hours": "Кожні 6 годин", + "LabelIntervalEveryDay": "Щодня", + "LabelIntervalEveryHour": "Щогодини", + "LabelInvalidParts": "Недопустимі частини", + "LabelInvert": "Інвертувати", + "LabelItem": "Елемент", + "LabelLanguage": "Мова", + "LabelLanguageDefaultServer": "Типова мова сервера", + "LabelLastBookAdded": "Останню книгу додано", + "LabelLastBookUpdated": "Останню книгу оновлено", + "LabelLastSeen": "Активність", + "LabelLastTime": "Останній час", + "LabelLastUpdate": "Останнє оновлення", + "LabelLayout": "Вигляд", + "LabelLayoutSinglePage": "Одна сторінка", + "LabelLayoutSplitPage": "Розділити сторінку", + "LabelLess": "Менше", + "LabelLibrariesAccessibleToUser": "Бібліотеки, доступні користувачу", + "LabelLibrary": "Бібліотека", + "LabelLibraryItem": "Елемент бібліотеки", + "LabelLibraryName": "Назва бібліотеки", + "LabelLimit": "Обмеження", + "LabelLineSpacing": "Відстань між рядками", + "LabelListenAgain": "Слухати знову", + "LabelLogLevelDebug": "Зневадження", + "LabelLogLevelInfo": "Відомості", + "LabelLogLevelWarn": "Увага", + "LabelLookForNewEpisodesAfterDate": "Шукати нові епізоди після вказаної дати", + "LabelLowestPriority": "Найнижчий пріоритет", + "LabelMatchExistingUsersBy": "Шукати наявних користувачів за", + "LabelMatchExistingUsersByDescription": "Використовується для підключення наявних користувачів. Після підключення користувач отримає унікальний id від вашого сервісу SSO", + "LabelMediaPlayer": "Програвач медіа", + "LabelMediaType": "Тип медіа", + "LabelMetadataOrderOfPrecedenceDescription": "Пріоритетніші джерела метаданих перезапишуть менш пріоритетні метадані", + "LabelMetadataProvider": "Джерело метаданих", + "LabelMetaTag": "Метатег", + "LabelMetaTags": "Метатеги", + "LabelMinute": "Хвилина", + "LabelMissing": "Бракує", + "LabelMissingEbook": "Без електронної книги", + "LabelMissingParts": "Відсутні частини", + "LabelMissingSupplementaryEbook": "Без додаткової електронної книги", + "LabelMobileRedirectURIs": "Дозволені адреси перенаправлення", + "LabelMobileRedirectURIsDescription": "Це білий список наявних URI, що перенаправляють у мобільний додаток. За замовчуванням це audiobookshelf://oauth, який ви можете видалити або ж додати інші адреси для сторонніх інтеграцій. Використайте зірочку (*), аби дозволити будь-яке URI.", + "LabelMore": "Більше", + "LabelMoreInfo": "Докладніше", + "LabelName": "Назва", + "LabelNarrator": "Читець", + "LabelNarrators": "Читці", + "LabelNew": "Нове", + "LabelNewestAuthors": "Нові автори", + "LabelNewestEpisodes": "Нові епізоди", + "LabelNewPassword": "Новий пароль", + "LabelNextBackupDate": "Дата наступного резервного копіювання", + "LabelNextScheduledRun": "Наступний запланований запуск", + "LabelNoEpisodesSelected": "Не вибрано жодного епізоду", + "LabelNotes": "Примітки", + "LabelNotFinished": "Незавершені", + "LabelNotificationAppriseURL": "URL Apprise", + "LabelNotificationAvailableVariables": "Доступні змінні", + "LabelNotificationBodyTemplate": "Шаблон сповіщення", + "LabelNotificationEvent": "Сповіщення про події", + "LabelNotificationsMaxFailedAttempts": "Ліміт невдалих спроб", + "LabelNotificationsMaxFailedAttemptsHelp": "Сповіщення буде вимкнено після багатьох невдалих надсилань", + "LabelNotificationsMaxQueueSize": "Ліміт розміру черги сповіщень", + "LabelNotificationsMaxQueueSizeHelp": "Події обмежені до 1 на секунду. Події буде проігноровано, якщо ліміт черги досягнуто. Це запобігає спаму сповіщеннями.", + "LabelNotificationTitleTemplate": "Шаблон заголовку", + "LabelNotStarted": "Не розпочато", + "LabelNumberOfBooks": "Кількість книг", + "LabelNumberOfEpisodes": "Кількість епізодів", + "LabelOpenRSSFeed": "Відкрити RSS-канал", + "LabelOverwrite": "Перезаписати", + "LabelPassword": "Пароль", + "LabelPath": "Шлях", + "LabelPermissionsAccessAllLibraries": "Доступ до усіх бібліотек", + "LabelPermissionsAccessAllTags": "Доступ до усіх міток", + "LabelPermissionsAccessExplicitContent": "Доступ до відвертого вмісту", + "LabelPermissionsDelete": "Може видаляти", + "LabelPermissionsDownload": "Може завантажувати", + "LabelPermissionsUpdate": "Може оновлювати", + "LabelPermissionsUpload": "Може завантажувати", + "LabelPersonalYearReview": "Ваші підсумки року ({0})", + "LabelPhotoPathURL": "Шлях/URL фото", + "LabelPlaylists": "Списки відтворення", + "LabelPlayMethod": "Метод відтворення", + "LabelPodcast": "Подкаст", + "LabelPodcasts": "Подкасти", + "LabelPodcastSearchRegion": "Регіон пошуку подкасту", + "LabelPodcastType": "Тип подкасту", + "LabelPort": "Порт", + "LabelPrefixesToIgnore": "Ігнорувати префікси (з урахуванням регістру)", + "LabelPreventIndexing": "Заборонити індексування вашого каналу каталогами подкастів iTunes та Google", + "LabelPrimaryEbook": "Основна електронна книга", + "LabelProgress": "Прогрес", + "LabelProvider": "Джерело", + "LabelPubDate": "Дата публікації", + "LabelPublisher": "Видавець", + "LabelPublishYear": "Рік публікації", + "LabelRead": "Читати", + "LabelReadAgain": "Читати знову", + "LabelReadEbookWithoutProgress": "Читати книгу без збереження прогресу", + "LabelRecentlyAdded": "Нещодавно додані", + "LabelRecentSeries": "Останні серії", + "LabelRecommended": "Рекомендовані", + "LabelRedo": "Повторити", + "LabelRegion": "Регіон", + "LabelReleaseDate": "Дата публікації", + "LabelRemoveCover": "Видалити обкладинку", + "LabelRowsPerPage": "Рядків на сторінку", + "LabelRSSFeedCustomOwnerEmail": "Користувацька електронна адреса власника", + "LabelRSSFeedCustomOwnerName": "Користувацьке ім'я власника", + "LabelRSSFeedOpen": "RSS-канал відкрито", + "LabelRSSFeedPreventIndexing": "Запобігати індексації", + "LabelRSSFeedSlug": "Назва RSS-каналу", + "LabelRSSFeedURL": "Адреса RSS-каналу", + "LabelSearchTerm": "Пошуковий запит", + "LabelSearchTitle": "Пошук за назвою", + "LabelSearchTitleOrASIN": "Пошук назви або ASIN", + "LabelSeason": "Сезон", + "LabelSelectAllEpisodes": "Вибрати всі серії", + "LabelSelectEpisodesShowing": "Обрати показані епізоди: {0}", + "LabelSelectUsers": "Вибрати користувачів", + "LabelSendEbookToDevice": "Надіслати електронну книгу на...", + "LabelSequence": "Послідовність", + "LabelSeries": "Серії", + "LabelSeriesName": "Назва серії", + "LabelSeriesProgress": "Прогрес серії", + "LabelServerYearReview": "Підсумки року сервера ({0})", + "LabelSetEbookAsPrimary": "Зробити основною", + "LabelSetEbookAsSupplementary": "Зробити додатковою", + "LabelSettingsAudiobooksOnly": "Лише аудіокниги", + "LabelSettingsAudiobooksOnlyHelp": "Увімкніть цей параметр, щоб ігнорувати файли електронних книг, якщо вони не знаходяться у теці аудіокниги, тоді вони будуть встановлені як додаткові електронні книги", + "LabelSettingsBookshelfViewHelp": "Імітує вигляд дерев'яних полиць", + "LabelSettingsChromecastSupport": "Підтримка Chromecast", + "LabelSettingsDateFormat": "Формат дати", + "LabelSettingsDisableWatcher": "Вимкнути спостерігача", + "LabelSettingsDisableWatcherForLibrary": "Вимкнути спостерігання тек бібліотеки", + "LabelSettingsDisableWatcherHelp": "Вимикає автоматичне додавання/оновлення елементів, коли спостерігаються зміни файлів. *Потребує перезавантаження сервера", + "LabelSettingsEnableWatcher": "Увімкнути спостерігача", + "LabelSettingsEnableWatcherForLibrary": "Увімкнути спостерігання тек бібліотеки", + "LabelSettingsEnableWatcherHelp": "Вмикає автоматичне додавання/оновлення елементів, коли спостерігаються зміни файлів. *Потребує перезавантаження сервера", + "LabelSettingsExperimentalFeatures": "Експериментальні функції", + "LabelSettingsExperimentalFeaturesHelp": "Функції в розробці, які потребують вашого відгуку та допомоги в тестуванні. Натисніть, щоб відкрити обговорення на Github.", + "LabelSettingsFindCovers": "Пошук обкладинок", + "LabelSettingsFindCoversHelp": "Якщо ваша аудіокнига не містить вбудованої обкладинки або зображення у теці, сканувальник спробує знайти обкладинку.
Примітка: Це збільшить час сканування", + "LabelSettingsHideSingleBookSeries": "Сховати серії з однією книгою", + "LabelSettingsHideSingleBookSeriesHelp": "Серії, що містять одну книгу, будуть приховані зі сторінки серій та полиць головної сторінки.", + "LabelSettingsHomePageBookshelfView": "Полиці на головній сторінці", + "LabelSettingsLibraryBookshelfView": "Показувати полиці у бібліотеці", + "LabelSettingsOnlyShowLaterBooksInContinueSeries": "Пропускати попередні книги у Продовжити серії", + "LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "Полиця Продовжити серії на головній сторінці показує найпершу непочату книгу з тих серій, у яких ви завершили хоча б одну книгу та не маєте книг у процесі. Якщо увімкнути це налаштування, то серії продовжуватимуться з останньої завершеної книги, а не з першої непочатої.", + "LabelSettingsParseSubtitles": "Дістати підзаголовки", + "LabelSettingsParseSubtitlesHelp": "Дістати підзаголовки з назв тек аудіокниг.
Підзаголовок мусить йти після \" - \"
Наприклад, \"Назва книги - Це підзаголовок\" має підзаголовок \"Це підзаголовок\"", + "LabelSettingsPreferMatchedMetadata": "Надавати перевагу віднайденим метаданим", + "LabelSettingsPreferMatchedMetadataHelp": "Подробиці буде перезаписано віднайденими даними Швидкого пошуку. Без цього Швидкий пошук заповнить лише подробиці, яких бракує.", + "LabelSettingsSkipMatchingBooksWithASIN": "Не шукати книги, що мають ASIN", + "LabelSettingsSkipMatchingBooksWithISBN": "Не шукати книги, що мають ISBN", + "LabelSettingsSortingIgnorePrefixes": "Ігнорувати префікси при сортуванні", + "LabelSettingsSortingIgnorePrefixesHelp": "Наприклад, для префіксу \"1.\" назва книги \"1. Назва книги\" буде визначена як \"Назва книги, 1.\"", + "LabelSettingsSquareBookCovers": "Квадратні обкладинки", + "LabelSettingsSquareBookCoversHelp": "Надавати перевагу квадратним обкладинкам замість формату 1,6:1", + "LabelSettingsStoreCoversWithItem": "Зберігати обкладинки з елементом", + "LabelSettingsStoreCoversWithItemHelp": "За замовчуванням обкладинки зберігаються у /metadata/items. Цей параметр увімкне збереження обкладинок у теці елемента бібліотеки. Буде збережено лише один файл \"cover\"", + "LabelSettingsStoreMetadataWithItem": "Зберігати метадані з елементом", + "LabelSettingsStoreMetadataWithItemHelp": "За замовчуванням файли метаданих зберігаються у /metadata/items. Цей параметр увімкне збереження метаданих у теці елемента бібліотеки", + "LabelSettingsTimeFormat": "Формат часу", + "LabelShowAll": "Показати все", + "LabelSize": "Розмір", + "LabelSleepTimer": "Таймер вимкнення", + "LabelSlug": "Назва", + "LabelStart": "Початок", + "LabelStarted": "Почато", + "LabelStartedAt": "Почато", + "LabelStartTime": "Час початку", + "LabelStatsAudioTracks": "Аудіодоріжки", + "LabelStatsAuthors": "Автори", + "LabelStatsBestDay": "Найкращий день", + "LabelStatsDailyAverage": "В середньому за добу", + "LabelStatsDays": "Днів", + "LabelStatsDaysListened": "Днів прослухано", + "LabelStatsHours": "Годин", + "LabelStatsInARow": "поспіль", + "LabelStatsItemsFinished": "Елементів завершено", + "LabelStatsItemsInLibrary": "Елементів у бібліотеці", + "LabelStatsMinutes": "хвилин", + "LabelStatsMinutesListening": "Хвилин прослухано", + "LabelStatsOverallDays": "Днів загалом", + "LabelStatsOverallHours": "Годин загалом", + "LabelStatsWeekListening": "Прослухано за тиждень", + "LabelSubtitle": "Підзаголовок", + "LabelSupportedFileTypes": "Підтримувані типи файлів", + "LabelTag": "Мітка", + "LabelTags": "Мітки", + "LabelTagsAccessibleToUser": "Мітки, доступні користувачу", + "LabelTagsNotAccessibleToUser": "Мітки, недоступні користувачу", + "LabelTasks": "Запущені завдання", + "LabelTextEditorBulletedList": "Маркований список", + "LabelTextEditorLink": "Посилання", + "LabelTextEditorNumberedList": "Нумерований список", + "LabelTextEditorUnlink": "Прибрати посилання", + "LabelTheme": "Тема", + "LabelThemeDark": "Темна", + "LabelThemeLight": "Світла", + "LabelTimeBase": "Шкала часу", + "LabelTimeListened": "Часу прослухано", + "LabelTimeListenedToday": "Сьогодні прослухано", + "LabelTimeRemaining": "Залишилося: {0}", + "LabelTimeToShift": "На скільки секунд зсунути", + "LabelTitle": "Назва", + "LabelToolsEmbedMetadata": "Вбудувати метадані", + "LabelToolsEmbedMetadataDescription": "Вбудувати метадані в аудіофайли, включно з обкладинками та главами", + "LabelToolsMakeM4b": "Створити M4B-файл аудіокниги", + "LabelToolsMakeM4bDescription": "Створити .M4B-аудіокнигу з вбудованими метаданими, обкладинкою та главами.", + "LabelToolsSplitM4b": "Розділити M4B на MP3", + "LabelToolsSplitM4bDescription": "Створення MP3 з розділеного за главами M4B з вбудованими метаданими, обкладинкою та главами.", + "LabelTotalDuration": "Загальна тривалість", + "LabelTotalTimeListened": "Усього прослухано", + "LabelTrackFromFilename": "Доріжка за назвою файлу", + "LabelTrackFromMetadata": "Доріжка за метаданими", + "LabelTracks": "Доріжки", + "LabelTracksMultiTrack": "Декілька доріжок", + "LabelTracksNone": "Доріжки відсутні", + "LabelTracksSingleTrack": "Одна доріжка", + "LabelType": "Тип", + "LabelUnabridged": "Повна", + "LabelUndo": "Скасувати", + "LabelUnknown": "Невідомо", + "LabelUpdateCover": "Оновити обкладинку", + "LabelUpdateCoverHelp": "Дозволити перезапис наявних обкладинок обраних книг після віднайдення", + "LabelUpdatedAt": "Оновлення", + "LabelUpdateDetails": "Оновити подробиці", + "LabelUpdateDetailsHelp": "Дозволити перезапис наявних подробиць обраних книг після віднайдення", + "LabelUploaderDragAndDrop": "Перетягніть файли або теки", + "LabelUploaderDropFiles": "Перетягніть файли", + "LabelUploaderItemFetchMetadataHelp": "Автоматично шукати назву, автора та серію", + "LabelUseChapterTrack": "Прогрес глави", + "LabelUseFullTrack": "Використовувати доріжку повністю", + "LabelUser": "Користувач", + "LabelUsername": "Ім’я користувача", + "LabelValue": "Значення", + "LabelVersion": "Версія", + "LabelViewBookmarks": "Переглянути закладки", + "LabelViewChapters": "Переглянути глави", + "LabelViewQueue": "Переглянути чергу відтворення", + "LabelVolume": "Гучність", + "LabelWeekdaysToRun": "Виконувати у дні", + "LabelYearReviewHide": "Сховати підсумки року", + "LabelYearReviewShow": "Переглянути підсумки року", + "LabelYourAudiobookDuration": "Тривалість вашої аудіокниги", + "LabelYourBookmarks": "Ваші закладки", + "LabelYourPlaylists": "Ваші списки відтворення", + "LabelYourProgress": "Ваш прогрес", + "MessageAddToPlayerQueue": "Додати до черги відтворення", + "MessageAppriseDescription": "Щоб скористатися цією функцією, вам потрібно мати запущену Apprise API або API, що оброблятиме ті ж запити.
Аби надсилати сповіщення, URL-адреса API Apprise мусить бути повною, наприклад, якщо ваш API розміщено за адресою http://192.168.1.1:8337, то необхідно вказати адресу http://192.168.1.1:8337/notify.", + "MessageBackupsDescription": "Резервні копії містять користувачів, прогрес, подробиці елементів бібліотеки, налаштування сервера та зображення з /metadata/items та /metadata/authors. Резервні копії не містять жодних файлів з тек бібліотеки.", + "MessageBatchQuickMatchDescription": "Швидкий пошук спробує знайти відсутні обкладинки та метадані обраних елементів. Увімкніть налаштування нижче, аби дозволити заміну наявних обкладинок та/або метаданих під час швидкого пошуку.", + "MessageBookshelfNoCollections": "Ви не створили жодної добірки", + "MessageBookshelfNoResultsForFilter": "Немає результатів з фільтром \"{0}: {1}\"", + "MessageBookshelfNoRSSFeeds": "Немає відкритих RSS-каналів", + "MessageBookshelfNoSeries": "Серії відсутні", + "MessageChapterEndIsAfter": "Кінець глави знаходиться після закінчення книги", + "MessageChapterErrorFirstNotZero": "Перша глава мусить починатися з 0", + "MessageChapterErrorStartGteDuration": "Час початку мусить бути меншим за тривалість аудіокниги", + "MessageChapterErrorStartLtPrev": "Неприпустимий час початку, має бути більшим за час початку попередньої глави", + "MessageChapterStartIsAfter": "Початок глави знаходиться після закінчення книги", + "MessageCheckingCron": "Перевірка планувальника...", + "MessageConfirmCloseFeed": "Ви дійсно бажаєте закрити цей канал?", + "MessageConfirmDeleteBackup": "Ви дійсно бажаєте видалити резервну копію за {0}?", + "MessageConfirmDeleteFile": "Файл буде видалено з вашої файлової системи. Ви впевнені?", + "MessageConfirmDeleteLibrary": "Ви дійсно бажаєте назавжди видалити бібліотеку \"{0}\"?", + "MessageConfirmDeleteLibraryItem": "Елемент бібліотеки буде видалено з бази даних та вашої файлової системи. Ви впевнені?", + "MessageConfirmDeleteLibraryItems": "З бази даних та вашої файлової системи будуть видалені елементи бібліотеки: {0}. Ви впевнені?", + "MessageConfirmDeleteSession": "Ви дійсно бажаєте видалити цей сеанс?", + "MessageConfirmForceReScan": "Ви дійсно бажаєте примусово пересканувати?", + "MessageConfirmMarkAllEpisodesFinished": "Ви дійсно бажаєте позначити усі епізоди завершеними?", + "MessageConfirmMarkAllEpisodesNotFinished": "Ви дійсно бажаєте позначити усі епізоди незавершеними?", + "MessageConfirmMarkSeriesFinished": "Ви дійсно бажаєте позначити усі книги серії завершеними?", + "MessageConfirmMarkSeriesNotFinished": "Ви дійсно бажаєте позначити всі книги серії незавершеними?", + "MessageConfirmQuickEmbed": "Увага! Швидке вбудування не створює резервних копій ваших аудіо. Переконайтеся, що маєте копію ваших файлів.

Продовжити?", + "MessageConfirmRemoveAllChapters": "Ви дійсно бажаєте видалити усі глави?", + "MessageConfirmRemoveAuthor": "Ви дійсно бажаєте видалити автора \"{0}\"?", + "MessageConfirmRemoveCollection": "Ви дійсно бажаєте видалити добірку \"{0}\"?", + "MessageConfirmRemoveEpisode": "Ви дійсно бажаєте видалити епізод \"{0}\"?", + "MessageConfirmRemoveEpisodes": "Ви дійсно бажаєте видалити епізодів: {0}?", + "MessageConfirmRemoveListeningSessions": "Ви дійсно бажаєте видалити сеанси прослуховування: {0}?", + "MessageConfirmRemoveNarrator": "Ви дійсно бажаєте видалити читця \"{0}\"?", + "MessageConfirmRemovePlaylist": "Ви дійсно бажаєте видалити список відтворення \"{0}\"?", + "MessageConfirmRenameGenre": "Ви дійсно бажаєте замінити жанр \"{0}\" на \"{1}\" для усіх елементів?", + "MessageConfirmRenameGenreMergeNote": "Примітка: такий жанр вже існує, тож їх буде об'єднано.", + "MessageConfirmRenameGenreWarning": "Увага! Вже існує схожий жанр у іншому регістрі \"{0}\".", + "MessageConfirmRenameTag": "Ви дійсно бажаєте замінити мітку \"{0}\" на \"{1}\" для усіх елементів?", + "MessageConfirmRenameTagMergeNote": "Примітка: така мітка вже існує, тож їх буде об'єднано.", + "MessageConfirmRenameTagWarning": "Увага! Вже існує схожа мітка у іншому регістрі \"{0}\".", + "MessageConfirmReScanLibraryItems": "Ви дійсно бажаєте пересканувати елементи: {0}?", + "MessageConfirmSendEbookToDevice": "Ви дійсно хочете відправити на пристрій \"{2}\" електроні книги: {0}, \"{1}\"?", + "MessageDownloadingEpisode": "Завантаження епізоду", + "MessageDragFilesIntoTrackOrder": "Перетягніть файли до правильного порядку", + "MessageEmbedFinished": "Вбудовано!", + "MessageEpisodesQueuedForDownload": "Епізодів у черзі завантаження: {0}", + "MessageFeedURLWillBe": "URL-адреса каналу буде {0}", + "MessageFetching": "Отримання...", + "MessageForceReScanDescription": "Просканує усі файли заново, неначе вперше. ID3-мітки, файли OPF та текстові файли будуть проскановані як нові.", + "MessageImportantNotice": "Важливе повідомлення!", + "MessageInsertChapterBelow": "Введіть главу нижче", + "MessageItemsSelected": "Обрано елементів: {0}", + "MessageItemsUpdated": "Оновлено елементів: {0}", + "MessageJoinUsOn": "Приєднуйтесь до", + "MessageListeningSessionsInTheLastYear": "Сесій прослуховування минулого року: {0}", + "MessageLoading": "Завантаження...", + "MessageLoadingFolders": "Завантаження тек...", + "MessageM4BFailed": "Помилка M4B!", + "MessageM4BFinished": "M4B створено!", + "MessageMapChapterTitles": "Встановіть назви глав вашої аудіокниги без визначення налаштувань тривалості", + "MessageMarkAllEpisodesFinished": "Позначити всі епізоди завершеними", + "MessageMarkAllEpisodesNotFinished": "Позначити всі епізоди незавершеними", + "MessageMarkAsFinished": "Позначити завершеним", + "MessageMarkAsNotFinished": "Позначити незавершеним", + "MessageMatchBooksDescription": "Спробує віднайти книгу у вказаному джерелі пошуку та встановити подробиці та обкладинку, яких бракує. Не перезаписує подробиці.", + "MessageNoAudioTracks": "Аудіодоріжки відсутні", + "MessageNoAuthors": "Автори відсутні", + "MessageNoBackups": "Резервні копії відсутні", + "MessageNoBookmarks": "Немає закладок", + "MessageNoChapters": "Глави відсутні", + "MessageNoCollections": "Добірки відсутні", + "MessageNoCoversFound": "Обкладинок не знайдено", + "MessageNoDescription": "Без опису", + "MessageNoDownloadsInProgress": "Немає активних завантажень", + "MessageNoDownloadsQueued": "Немає завантажень у черзі", + "MessageNoEpisodeMatchesFound": "Відповідних епізодів не знайдено", + "MessageNoEpisodes": "Епізоди відсутні", + "MessageNoFoldersAvailable": "Немає доступних тек", + "MessageNoGenres": "Без жанру", + "MessageNoIssues": "Немає проблем", + "MessageNoItems": "Елементи відсутні", + "MessageNoItemsFound": "Елементів не знайдено", + "MessageNoListeningSessions": "Сеанси прослуховування відсутні", + "MessageNoLogs": "Журнал порожній", + "MessageNoMediaProgress": "Прогрес відсутній", + "MessageNoNotifications": "Сповіщення відсутні", + "MessageNoPodcastsFound": "Подкастів не знайдено", + "MessageNoResults": "Немає результатів", + "MessageNoSearchResultsFor": "Немає результатів пошуку для \"{0}\"", + "MessageNoSeries": "Без серії", + "MessageNoTags": "Без міток", + "MessageNoTasksRunning": "Немає активних завдань", + "MessageNotYetImplemented": "Ще не реалізовано", + "MessageNoUpdateNecessary": "Оновлення не потрібно", + "MessageNoUpdatesWereNecessary": "Оновлень не потрібно", + "MessageNoUserPlaylists": "У вас немає списків відтворення", + "MessageOr": "або", + "MessagePauseChapter": "Призупинити відтворення глави", + "MessagePlayChapter": "Слухати початок глави", + "MessagePlaylistCreateFromCollection": "Створити список відтворення з добірки", + "MessagePodcastHasNoRSSFeedForMatching": "Подкаст не має RSS-каналу для пошуку", + "MessageQuickMatchDescription": "Заповнити відсутні подробиці та обкладинку першим результатом пошуку '{0}'. Не перезаписує подробиці, якщо не увімкнено параметр \"Надавати перевагу віднайденим метаданим\".", + "MessageRemoveChapter": "Видалити главу", + "MessageRemoveEpisodes": "Видалити епізодів: {0}", + "MessageRemoveFromPlayerQueue": "Вилучити з черги відтворення", + "MessageRemoveUserWarning": "Ви дійсно бажаєте назавжди видалити користувача \"{0}\"?", + "MessageReportBugsAndContribute": "Повідомляйте про помилки, пропонуйте функції та долучайтеся на", + "MessageResetChaptersConfirm": "Ви дійсно бажаєте скинути глави та скасувати внесені зміни?", + "MessageRestoreBackupConfirm": "Ви дійсно бажаєте відновити резервну копію від", + "MessageRestoreBackupWarning": "Відновлення резервної копії перезапише всю базу даних, розташовану в /config, і зображення обкладинок в /metadata/items та /metadata/authors.

Резервні копії не змінюють жодних файлів у теках бібліотеки. Якщо у налаштуваннях сервера увімкнено збереження обкладинок і метаданих у теках бібліотеки, вони не створюються під час резервного копіювання і не перезаписуються..

Всі клієнти, що користуються вашим сервером, будуть автоматично оновлені.", + "MessageSearchResultsFor": "Результати пошуку для", + "MessageSelected": "Вибрано: {0}", + "MessageServerCouldNotBeReached": "Не вдалося підключитися до сервера", + "MessageSetChaptersFromTracksDescription": "Створити глави з аудіодоріжок, встановивши назви файлів за заголовки", + "MessageStartPlaybackAtTime": "Почати відтворення \"{0}\" з {1}?", + "MessageThinking": "Думаю…", + "MessageUploaderItemFailed": "Не вдалося завантажити", + "MessageUploaderItemSuccess": "Успішно завантажено!", + "MessageUploading": "Завантаження...", + "MessageValidCronExpression": "Допустима команда cron", + "MessageWatcherIsDisabledGlobally": "Спостерігача вимкнено в налаштуваннях сервера", + "MessageXLibraryIsEmpty": "Бібліотека {0} порожня!", + "MessageYourAudiobookDurationIsLonger": "Тривалість вашої аудіокниги довша за віднайдену", + "MessageYourAudiobookDurationIsShorter": "Тривалість вашої аудіокниги коротша за віднайдену", + "NoteChangeRootPassword": "Кореневий користувач — єдиний, хто може мати порожній пароль", + "NoteChapterEditorTimes": "Примітка: Перша глава мусить починатися з 0:00, а час початку останньої глави не може бути більшим за зазначену тривалість аудіокниги.", + "NoteFolderPicker": "Примітка: вже обрані теки не буде показано", + "NoteRSSFeedPodcastAppsHttps": "Попередження: Більшість додатків подкастів вимагатимуть використання протоколу HTTPS від RSS-каналу", + "NoteRSSFeedPodcastAppsPubDate": "Попередження: 1 або більше ваших епізодів не мають дати публікації. Деякі додатки подкастів вимагають це.", + "NoteUploaderFoldersWithMediaFiles": "Теки з медіафайлами буде оброблено як окремі елементи бібліотеки.", + "NoteUploaderOnlyAudioFiles": "Якщо завантажувати лише аудіофайли, то кожен файл буде оброблено як окрему книгу.", + "NoteUploaderUnsupportedFiles": "Непідтримувані файли пропущено. Під час вибору або перетягування теки, файли, що знаходяться поза текою, пропускаються.", + "PlaceholderNewCollection": "Нова назва добірки", + "PlaceholderNewFolderPath": "Новий шлях до теки", + "PlaceholderNewPlaylist": "Нова назва списку", + "PlaceholderSearch": "Пошук...", + "PlaceholderSearchEpisode": "Шукати епізод...", + "ToastAccountUpdateFailed": "Не вдалося оновити профіль", + "ToastAccountUpdateSuccess": "Профіль оновлено", + "ToastAuthorImageRemoveFailed": "Не вдалося видалити зображення", + "ToastAuthorImageRemoveSuccess": "Фото автора видалено", + "ToastAuthorUpdateFailed": "Не вдалося оновити автора", + "ToastAuthorUpdateMerged": "Автора об'єднано", + "ToastAuthorUpdateSuccess": "Автора оновлено", + "ToastAuthorUpdateSuccessNoImageFound": "Автора оновлено (фото не знайдено)", + "ToastBackupCreateFailed": "Не вдалося створити резервну копію", + "ToastBackupCreateSuccess": "Резервну копію створено", + "ToastBackupDeleteFailed": "Не вдалося видалити резервну копію", + "ToastBackupDeleteSuccess": "Резервну копію видалено", + "ToastBackupRestoreFailed": "Не вдалося відновити резервну копію", + "ToastBackupUploadFailed": "Не вдалося завантажити резервну копію", + "ToastBackupUploadSuccess": "Резервну копію завантажено", + "ToastBatchUpdateFailed": "Не вдалося оновити обрані", + "ToastBatchUpdateSuccess": "Обрані успішно оновлено", + "ToastBookmarkCreateFailed": "Не вдалося створити закладку", + "ToastBookmarkCreateSuccess": "Закладку додано", + "ToastBookmarkRemoveFailed": "Не вдалося видалити закладку", + "ToastBookmarkRemoveSuccess": "Закладку видалено", + "ToastBookmarkUpdateFailed": "Не вдалося оновити закладку", + "ToastBookmarkUpdateSuccess": "Закладку оновлено", + "ToastChaptersHaveErrors": "Глави містять помилки", + "ToastChaptersMustHaveTitles": "Глави повинні мати назви", + "ToastCollectionItemsRemoveFailed": "Не вдалося видалити елемент(и) з добірки", + "ToastCollectionItemsRemoveSuccess": "Елемент(и) видалено з добірки", + "ToastCollectionRemoveFailed": "Не вдалося видалити добірку", + "ToastCollectionRemoveSuccess": "Добірку видалено", + "ToastCollectionUpdateFailed": "Не вдалося оновити добірку", + "ToastCollectionUpdateSuccess": "Добірку оновлено", + "ToastItemCoverUpdateFailed": "Не вдалося оновити обкладинку", + "ToastItemCoverUpdateSuccess": "Обкладинку елемента оновлено", + "ToastItemDetailsUpdateFailed": "Не вдалося оновити подробиці елемента", + "ToastItemDetailsUpdateSuccess": "Подробиці про елемент оновлено", + "ToastItemDetailsUpdateUnneeded": "Оновлення подробиць непотрібне", + "ToastItemMarkedAsFinishedFailed": "Не вдалося позначити як завершене", + "ToastItemMarkedAsFinishedSuccess": "Елемент позначено як завершений", + "ToastItemMarkedAsNotFinishedFailed": "Не вдалося позначити незавершеним", + "ToastItemMarkedAsNotFinishedSuccess": "Елемент позначено незавершеним", + "ToastLibraryCreateFailed": "Не вдалося створити бібліотеку", + "ToastLibraryCreateSuccess": "Бібліотеку \"{0}\" створено", + "ToastLibraryDeleteFailed": "Не вдалося видалити бібліотеку", + "ToastLibraryDeleteSuccess": "Бібліотеку видалено", + "ToastLibraryScanFailedToStart": "Не вдалося розпочати сканування", + "ToastLibraryScanStarted": "Почалося сканування бібліотеки", + "ToastLibraryUpdateFailed": "Не вдалося оновити бібліотеку", + "ToastLibraryUpdateSuccess": "Бібліотеку \"{0}\" оновлено", + "ToastPlaylistCreateFailed": "Не вдалося створити список", + "ToastPlaylistCreateSuccess": "Список відтворення створено", + "ToastPlaylistRemoveFailed": "Не вдалося видалити список", + "ToastPlaylistRemoveSuccess": "Список відтворення видалено", + "ToastPlaylistUpdateFailed": "Не вдалося оновити список", + "ToastPlaylistUpdateSuccess": "Список відтворення оновлено", + "ToastPodcastCreateFailed": "Не вдалося створити подкаст", + "ToastPodcastCreateSuccess": "Подкаст успішно створено", + "ToastRemoveItemFromCollectionFailed": "Не вдалося видалити елемент із добірки", + "ToastRemoveItemFromCollectionSuccess": "Елемент видалено з добірки", + "ToastRSSFeedCloseFailed": "Не вдалося закрити RSS-канал", + "ToastRSSFeedCloseSuccess": "RSS-канал закрито", + "ToastSendEbookToDeviceFailed": "Не вдалося надіслати електронну книгу на пристрій", + "ToastSendEbookToDeviceSuccess": "Електронну книгу надіслано на пристрій \"{0}\"", + "ToastSeriesUpdateFailed": "Не вдалося оновити серію", + "ToastSeriesUpdateSuccess": "Серію успішно оновлено", + "ToastSessionDeleteFailed": "Не вдалося видалити сесію", + "ToastSessionDeleteSuccess": "Сесію видалено", + "ToastSocketConnected": "Сокет під'єднано", + "ToastSocketDisconnected": "Сокет від'єднано", + "ToastSocketFailedToConnect": "Не вдалося під'єднатися до сокета", + "ToastUserDeleteFailed": "Не вдалося видалити користувача", + "ToastUserDeleteSuccess": "Користувача видалено" +} From b0a9bed15a0dbae6eab9d616bb617f37eb14a5dd Mon Sep 17 00:00:00 2001 From: Illia Pyshniak Date: Tue, 19 Mar 2024 23:18:34 +0200 Subject: [PATCH 0019/1695] Update i18n.js Add Ukrainian language and podcast region --- client/plugins/i18n.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/client/plugins/i18n.js b/client/plugins/i18n.js index 0ee0e5b1..ff7af170 100644 --- a/client/plugins/i18n.js +++ b/client/plugins/i18n.js @@ -23,6 +23,7 @@ const languageCodeMap = { 'pt-br': { label: 'Português (Brasil)', dateFnsLocale: 'ptBR' }, 'ru': { label: 'Русский', dateFnsLocale: 'ru' }, 'sv': { label: 'Svenska', dateFnsLocale: 'sv' }, + 'uk': { label: 'Українська', dateFnsLocale: 'uk' }, 'vi-vn': { label: 'Tiếng Việt', dateFnsLocale: 'vi' }, 'zh-cn': { label: '简体中文 (Simplified Chinese)', dateFnsLocale: 'zhCN' }, 'zh-tw': { label: '正體中文 (Traditional Chinese)', dateFnsLocale: 'zhTW' } @@ -36,6 +37,7 @@ Vue.prototype.$languageCodeOptions = Object.keys(languageCodeMap).map(code => { // iTunes search API uses ISO 3166 country codes: https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2 const podcastSearchRegionMap = { + 'ua': { label: 'Україна' }, 'us': { label: 'United States' }, 'cn': { label: '中国' } } From 1bee0827200d95d2cf7839bff99b46cf21dc5ac6 Mon Sep 17 00:00:00 2001 From: mikiher Date: Wed, 20 Mar 2024 11:40:50 +0200 Subject: [PATCH 0020/1695] Update libraryFolderID correctly in scanFolderUpdates --- server/scanner/LibraryItemScanner.js | 11 ++++++----- server/scanner/LibraryScanner.js | 9 +++++---- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/server/scanner/LibraryItemScanner.js b/server/scanner/LibraryItemScanner.js index 588b7744..872000d8 100644 --- a/server/scanner/LibraryItemScanner.js +++ b/server/scanner/LibraryItemScanner.js @@ -21,10 +21,10 @@ class LibraryItemScanner { * Scan single library item * * @param {string} libraryItemId - * @param {{relPath:string, path:string}} [renamedPaths] used by watcher when item folder was renamed + * @param {{relPath:string, path:string}} [updateLibraryItemDetails] used by watcher when item folder was renamed * @returns {number} ScanResult */ - async scanLibraryItem(libraryItemId, renamedPaths = null) { + async scanLibraryItem(libraryItemId, updateLibraryItemDetails = null) { // TODO: Add task manager const libraryItem = await Database.libraryItemModel.findByPk(libraryItemId) if (!libraryItem) { @@ -32,11 +32,12 @@ class LibraryItemScanner { return ScanResult.NOTHING } + const libraryFolderId = updateLibraryItemDetails?.libraryFolderId || libraryItem.libraryFolderId const library = await Database.libraryModel.findByPk(libraryItem.libraryId, { include: { model: Database.libraryFolderModel, where: { - id: libraryItem.libraryFolderId + id: libraryFolderId } } }) @@ -51,9 +52,9 @@ class LibraryItemScanner { const scanLogger = new ScanLogger() scanLogger.verbose = true - scanLogger.setData('libraryItem', renamedPaths?.relPath || libraryItem.relPath) + scanLogger.setData('libraryItem', updateLibraryItemDetails?.relPath || libraryItem.relPath) - const libraryItemPath = renamedPaths?.path || fileUtils.filePathToPOSIX(libraryItem.path) + const libraryItemPath = updateLibraryItemDetails?.path || fileUtils.filePathToPOSIX(libraryItem.path) const folder = library.libraryFolders[0] const libraryItemScanData = await this.getLibraryItemScanData(libraryItemPath, library, folder, false) diff --git a/server/scanner/LibraryScanner.js b/server/scanner/LibraryScanner.js index 27c507bd..ac422c79 100644 --- a/server/scanner/LibraryScanner.js +++ b/server/scanner/LibraryScanner.js @@ -525,7 +525,7 @@ class LibraryScanner { path: potentialChildDirs }) - let renamedPaths = {} + let updatedLibraryItemDetails = {} if (!existingLibraryItem) { const dirIno = await fileUtils.getIno(fullPath) existingLibraryItem = await Database.libraryItemModel.findOneOld({ @@ -536,8 +536,9 @@ class LibraryScanner { // Update library item paths for scan existingLibraryItem.path = fullPath existingLibraryItem.relPath = itemDir - renamedPaths.path = fullPath - renamedPaths.relPath = itemDir + updatedLibraryItemDetails.path = fullPath + updatedLibraryItemDetails.relPath = itemDir + updatedLibraryItemDetails.libraryFolderId = folder.id } } if (existingLibraryItem) { @@ -557,7 +558,7 @@ class LibraryScanner { // Scan library item for updates Logger.debug(`[LibraryScanner] Folder update for relative path "${itemDir}" is in library item "${existingLibraryItem.media.metadata.title}" - scan for updates`) - itemGroupingResults[itemDir] = await LibraryItemScanner.scanLibraryItem(existingLibraryItem.id, renamedPaths) + itemGroupingResults[itemDir] = await LibraryItemScanner.scanLibraryItem(existingLibraryItem.id, updatedLibraryItemDetails) continue } else if (library.settings.audiobooksOnly && !hasAudioFiles(fileUpdateGroup, itemDir)) { Logger.debug(`[LibraryScanner] Folder update for relative path "${itemDir}" has no audio files`) From 01c8d42291b97fdd770290e2ae70e2d433874f45 Mon Sep 17 00:00:00 2001 From: burghy86 Date: Thu, 21 Mar 2024 12:52:49 +0100 Subject: [PATCH 0021/1695] Update it.json month fix string --- client/strings/it.json | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/client/strings/it.json b/client/strings/it.json index 8924a28b..bd6e5fa9 100644 --- a/client/strings/it.json +++ b/client/strings/it.json @@ -43,7 +43,7 @@ "ButtonMatchAllAuthors": "Aggiungi metadata agli Autori", "ButtonMatchBooks": "Aggiungi metadata della Libreria", "ButtonNevermind": "Nevermind", - "ButtonNext": "Next", + "ButtonNext": "Prossimo", "ButtonNextChapter": "Prossimo Capitolo", "ButtonOk": "Ok", "ButtonOpenFeed": "Apri Feed", @@ -52,7 +52,7 @@ "ButtonPlay": "Play", "ButtonPlaying": "In Riproduzione", "ButtonPlaylists": "Playlists", - "ButtonPrevious": "Previous", + "ButtonPrevious": "Precendente", "ButtonPreviousChapter": "Capitolo Precendente", "ButtonPurgeAllCache": "Elimina tutta la Cache", "ButtonPurgeItemsCache": "Elimina la Cache selezionata", @@ -113,7 +113,7 @@ "HeaderCollectionItems": "Elementi della Raccolta", "HeaderCover": "Cover", "HeaderCurrentDownloads": "Download Correnti", - "HeaderCustomMetadataProviders": "Custom Metadata Providers", + "HeaderCustomMetadataProviders": " Metadata Providers Personalizzato", "HeaderDetails": "Dettagli", "HeaderDownloadQueue": "Download coda", "HeaderEbookFiles": "Ebook File", @@ -184,7 +184,7 @@ "HeaderUpdateDetails": "Aggiorna Dettagli", "HeaderUpdateLibrary": "Aggiorna Libreria", "HeaderUsers": "Utenti", - "HeaderYearReview": "Year {0} in Review", + "HeaderYearReview": "Anno {0} in Sintesi", "HeaderYourStats": "Statistiche Personali", "LabelAbridged": "Abbreviato", "LabelAccountType": "Tipo di Account", @@ -356,9 +356,9 @@ "LabelMetaTags": "Meta Tags", "LabelMinute": "Minuto", "LabelMissing": "Altro", - "LabelMissingEbook": "Has no ebook", + "LabelMissingEbook": "Non ha ebook", "LabelMissingParts": "Parti rimanenti", - "LabelMissingSupplementaryEbook": "Has no supplementary ebook", + "LabelMissingSupplementaryEbook": "Non ha ebook supplementare", "LabelMobileRedirectURIs": "URI di reindirizzamento mobile consentiti", "LabelMobileRedirectURIsDescription": "Questa è una lista bianca di URI di reindirizzamento validi per le app mobili. Quello predefinito è audiobookshelf://oauth, che puoi rimuovere o integrare con URI aggiuntivi per l'integrazione di app di terze parti. Utilizzando un asterisco (*) poiché l'unica voce consente qualsiasi URI.", "LabelMore": "Molto", @@ -444,7 +444,7 @@ "LabelSeries": "Serie", "LabelSeriesName": "Nome Serie", "LabelSeriesProgress": "Cominciato", - "LabelServerYearReview": "Server Year in Review ({0})", + "LabelServerYearReview": "Anno del server in sintesi({0})", "LabelSetEbookAsPrimary": "Immposta come Primario", "LabelSetEbookAsSupplementary": "Imposta come Suplementare", "LabelSettingsAudiobooksOnly": "Solo Audiolibri", @@ -778,4 +778,4 @@ "ToastSocketFailedToConnect": "Socket non riesce a connettersi", "ToastUserDeleteFailed": "Errore eliminazione utente", "ToastUserDeleteSuccess": "Utente eliminato" -} \ No newline at end of file +} From 8d7530254c5dca603e754aede713ea015cefa77d Mon Sep 17 00:00:00 2001 From: advplyr Date: Thu, 21 Mar 2024 14:23:49 -0500 Subject: [PATCH 0022/1695] Update:Re-order chapters table infront of audio tracks table on book item page #2778 --- client/pages/item/_id/index.vue | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/pages/item/_id/index.vue b/client/pages/item/_id/index.vue index d568d534..8c78d97d 100644 --- a/client/pages/item/_id/index.vue +++ b/client/pages/item/_id/index.vue @@ -137,12 +137,12 @@

- {{ audioFile.metadata.filename }} ({{ audioFile.error }})

+ + - - From ff5226fa9369b599b2b10cc2c1fe7579a1ceb902 Mon Sep 17 00:00:00 2001 From: advplyr Date: Thu, 21 Mar 2024 14:38:52 -0500 Subject: [PATCH 0023/1695] Update:Remove unused missing/invalid audiobook parts logic and keys --- client/components/cards/LazyBookCard.vue | 20 +---- client/components/widgets/AudiobookData.vue | 84 --------------------- client/pages/audiobook/_id/chapters.vue | 2 +- client/pages/audiobook/_id/edit.vue | 3 - client/pages/audiobook/_id/manage.vue | 2 +- client/pages/item/_id/index.vue | 18 ++--- client/strings/cs.json | 2 - client/strings/da.json | 2 - client/strings/de.json | 2 - client/strings/en-us.json | 2 - client/strings/es.json | 2 - client/strings/et.json | 2 - client/strings/fr.json | 2 - client/strings/gu.json | 2 - client/strings/he.json | 2 - client/strings/hi.json | 2 - client/strings/hr.json | 2 - client/strings/hu.json | 2 - client/strings/it.json | 4 +- client/strings/lt.json | 2 - client/strings/nl.json | 2 - client/strings/no.json | 2 - client/strings/pl.json | 2 - client/strings/pt-br.json | 4 +- client/strings/ru.json | 2 - client/strings/sv.json | 2 - client/strings/uk.json | 4 +- client/strings/vi-vn.json | 2 - client/strings/zh-cn.json | 2 - client/strings/zh-tw.json | 6 +- server/models/Book.js | 2 +- server/objects/files/AudioFile.js | 7 -- server/objects/mediaTypes/Book.js | 13 +--- 33 files changed, 19 insertions(+), 190 deletions(-) delete mode 100644 client/components/widgets/AudiobookData.vue diff --git a/client/components/cards/LazyBookCard.vue b/client/components/cards/LazyBookCard.vue index 42b020e3..efeb0165 100644 --- a/client/components/cards/LazyBookCard.vue +++ b/client/components/cards/LazyBookCard.vue @@ -358,7 +358,7 @@ export default { }, showError() { if (this.recentEpisode) return false // Dont show podcast error on episode card - return this.numInvalidAudioFiles || this.numMissingParts || this.isMissing || this.isInvalid + return this.isMissing || this.isInvalid }, libraryItemIdStreaming() { return this.store.getters['getLibraryItemIdStreaming'] @@ -388,29 +388,13 @@ export default { isInvalid() { return this._libraryItem.isInvalid }, - numMissingParts() { - if (this.isPodcast) return 0 - return this.media.numMissingParts - }, - numInvalidAudioFiles() { - if (this.isPodcast) return 0 - return this.media.numInvalidAudioFiles - }, errorText() { if (this.isMissing) return 'Item directory is missing!' else if (this.isInvalid) { if (this.isPodcast) return 'Podcast has no episodes' return 'Item has no audio tracks & ebook' } - let txt = '' - if (this.numMissingParts) { - txt += `${this.numMissingParts} missing parts.` - } - if (this.numInvalidAudioFiles) { - if (txt) txt += ' ' - txt += `${this.numInvalidAudioFiles} invalid audio files.` - } - return txt || 'Unknown Error' + return 'Unknown Error' }, overlayWrapperClasslist() { const classes = [] diff --git a/client/components/widgets/AudiobookData.vue b/client/components/widgets/AudiobookData.vue deleted file mode 100644 index 8309faca..00000000 --- a/client/components/widgets/AudiobookData.vue +++ /dev/null @@ -1,84 +0,0 @@ - - - \ No newline at end of file diff --git a/client/pages/audiobook/_id/chapters.vue b/client/pages/audiobook/_id/chapters.vue index 0f5db77a..ef4bbee4 100644 --- a/client/pages/audiobook/_id/chapters.vue +++ b/client/pages/audiobook/_id/chapters.vue @@ -281,7 +281,7 @@ export default { return this.media.audioFiles || [] }, audioTracks() { - return this.audioFiles.filter((af) => !af.exclude && !af.invalid) + return this.audioFiles.filter((af) => !af.exclude) }, selectedChapterId() { return this.selectedChapter ? this.selectedChapter.id : null diff --git a/client/pages/audiobook/_id/edit.vue b/client/pages/audiobook/_id/edit.vue index f6d4879d..69e96bf8 100644 --- a/client/pages/audiobook/_id/edit.vue +++ b/client/pages/audiobook/_id/edit.vue @@ -137,9 +137,6 @@ export default { }) return count }, - missingParts() { - return this.media.missingParts || [] - }, libraryItemId() { return this.libraryItem.id }, diff --git a/client/pages/audiobook/_id/manage.vue b/client/pages/audiobook/_id/manage.vue index 965f8c25..03c214b4 100644 --- a/client/pages/audiobook/_id/manage.vue +++ b/client/pages/audiobook/_id/manage.vue @@ -249,7 +249,7 @@ export default { return this.media.metadata || {} }, audioFiles() { - return (this.media.audioFiles || []).filter((af) => !af.exclude && !af.invalid) + return (this.media.audioFiles || []).filter((af) => !af.exclude) }, isSingleM4b() { return this.audioFiles.length === 1 && this.audioFiles[0].metadata.ext.toLowerCase() === '.m4b' diff --git a/client/pages/item/_id/index.vue b/client/pages/item/_id/index.vue index 8c78d97d..b4b58bda 100644 --- a/client/pages/item/_id/index.vue +++ b/client/pages/item/_id/index.vue @@ -131,15 +131,9 @@ -
-

Invalid audio files

- -

- {{ audioFile.metadata.filename }} ({{ audioFile.error }})

-
- - + @@ -239,10 +233,6 @@ export default { isAbridged() { return !!this.mediaMetadata.abridged }, - invalidAudioFiles() { - if (!this.isBook) return [] - return this.libraryItem.media.audioFiles.filter((af) => af.invalid) - }, showPlayButton() { if (this.isMissing || this.isInvalid) return false if (this.isMusic) return !!this.audioFile @@ -275,6 +265,12 @@ export default { tracks() { return this.media.tracks || [] }, + tracksWithAudioFile() { + return this.tracks.map((track) => { + track.audioFile = this.media.audioFiles?.find((af) => af.metadata.path === track.metadata.path) + return track + }) + }, podcastEpisodes() { return this.media.episodes || [] }, diff --git a/client/strings/cs.json b/client/strings/cs.json index 4ce358a2..6e935617 100644 --- a/client/strings/cs.json +++ b/client/strings/cs.json @@ -320,7 +320,6 @@ "LabelIntervalEvery6Hours": "Každých 6 hodin", "LabelIntervalEveryDay": "Každý den", "LabelIntervalEveryHour": "Každou hodinu", - "LabelInvalidParts": "Neplatné části", "LabelInvert": "Invertovat", "LabelItem": "Položka", "LabelLanguage": "Jazyk", @@ -357,7 +356,6 @@ "LabelMinute": "Minuta", "LabelMissing": "Chybějící", "LabelMissingEbook": "Has no ebook", - "LabelMissingParts": "Chybějící díly", "LabelMissingSupplementaryEbook": "Has no supplementary ebook", "LabelMobileRedirectURIs": "Allowed Mobile Redirect URIs", "LabelMobileRedirectURIsDescription": "This is a whitelist of valid redirect URIs for mobile apps. The default one is audiobookshelf://oauth, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (*) as the sole entry permits any URI.", diff --git a/client/strings/da.json b/client/strings/da.json index de00a1fc..ea69844c 100644 --- a/client/strings/da.json +++ b/client/strings/da.json @@ -320,7 +320,6 @@ "LabelIntervalEvery6Hours": "Hver 6. time", "LabelIntervalEveryDay": "Hver dag", "LabelIntervalEveryHour": "Hver time", - "LabelInvalidParts": "Ugyldige dele", "LabelInvert": "Inverter", "LabelItem": "Element", "LabelLanguage": "Sprog", @@ -357,7 +356,6 @@ "LabelMinute": "Minut", "LabelMissing": "Mangler", "LabelMissingEbook": "Has no ebook", - "LabelMissingParts": "Manglende dele", "LabelMissingSupplementaryEbook": "Has no supplementary ebook", "LabelMobileRedirectURIs": "Allowed Mobile Redirect URIs", "LabelMobileRedirectURIsDescription": "This is a whitelist of valid redirect URIs for mobile apps. The default one is audiobookshelf://oauth, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (*) as the sole entry permits any URI.", diff --git a/client/strings/de.json b/client/strings/de.json index ed99f095..207bc399 100644 --- a/client/strings/de.json +++ b/client/strings/de.json @@ -320,7 +320,6 @@ "LabelIntervalEvery6Hours": "Alle 6 Stunden", "LabelIntervalEveryDay": "Jeden Tag", "LabelIntervalEveryHour": "Jede Stunde", - "LabelInvalidParts": "Ungültige Teile", "LabelInvert": "Umkehren", "LabelItem": "Medium", "LabelLanguage": "Sprache", @@ -357,7 +356,6 @@ "LabelMinute": "Minute", "LabelMissing": "Fehlend", "LabelMissingEbook": "E-Book fehlt", - "LabelMissingParts": "Fehlende Teile", "LabelMissingSupplementaryEbook": "Ergänzendes E-Book fehlt", "LabelMobileRedirectURIs": "Erlaubte Weiterleitungs-URIs für die mobile App", "LabelMobileRedirectURIsDescription": "Dies ist eine Whitelist gültiger Umleitungs-URIs für mobile Apps. Der Standardwert ist audiobookshelf://oauth, den du entfernen oder durch zusätzliche URIs für die Integration von Drittanbieter-Apps ergänzen kannst. Die Verwendung eines Sternchens (*) als alleiniger Eintrag erlaubt jede URI.", diff --git a/client/strings/en-us.json b/client/strings/en-us.json index 43a1ef44..bba4669b 100644 --- a/client/strings/en-us.json +++ b/client/strings/en-us.json @@ -320,7 +320,6 @@ "LabelIntervalEvery6Hours": "Every 6 hours", "LabelIntervalEveryDay": "Every day", "LabelIntervalEveryHour": "Every hour", - "LabelInvalidParts": "Invalid Parts", "LabelInvert": "Invert", "LabelItem": "Item", "LabelLanguage": "Language", @@ -357,7 +356,6 @@ "LabelMinute": "Minute", "LabelMissing": "Missing", "LabelMissingEbook": "Has no ebook", - "LabelMissingParts": "Missing Parts", "LabelMissingSupplementaryEbook": "Has no supplementary ebook", "LabelMobileRedirectURIs": "Allowed Mobile Redirect URIs", "LabelMobileRedirectURIsDescription": "This is a whitelist of valid redirect URIs for mobile apps. The default one is audiobookshelf://oauth, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (*) as the sole entry permits any URI.", diff --git a/client/strings/es.json b/client/strings/es.json index 068c5bc4..b791a472 100644 --- a/client/strings/es.json +++ b/client/strings/es.json @@ -320,7 +320,6 @@ "LabelIntervalEvery6Hours": "Cada 6 Horas", "LabelIntervalEveryDay": "Cada Día", "LabelIntervalEveryHour": "Cada Hora", - "LabelInvalidParts": "Partes Inválidas", "LabelInvert": "Invertir", "LabelItem": "Elemento", "LabelLanguage": "Lenguaje", @@ -357,7 +356,6 @@ "LabelMinute": "Minuto", "LabelMissing": "Ausente", "LabelMissingEbook": "Has no ebook", - "LabelMissingParts": "Partes Ausentes", "LabelMissingSupplementaryEbook": "Has no supplementary ebook", "LabelMobileRedirectURIs": "URIs de redirección a móviles permitidos", "LabelMobileRedirectURIsDescription": "Esta es una lista de URIs válidos para redireccionamiento de apps móviles. La URI por defecto es audiobookshelf://oauth, la cual puedes remover or corroborar con URIs adicionales para la integración con apps de terceros. Utilizando un asterisco (*) como el único punto de entrada permite cualquier URI.", diff --git a/client/strings/et.json b/client/strings/et.json index da145027..9aa0d0db 100644 --- a/client/strings/et.json +++ b/client/strings/et.json @@ -320,7 +320,6 @@ "LabelIntervalEvery6Hours": "Iga 6 tunni tagant", "LabelIntervalEveryDay": "Iga päev", "LabelIntervalEveryHour": "Iga tunni tagant", - "LabelInvalidParts": "Vigased osad", "LabelInvert": "Pööra ümber", "LabelItem": "Kirje", "LabelLanguage": "Keel", @@ -357,7 +356,6 @@ "LabelMinute": "Minut", "LabelMissing": "Puudub", "LabelMissingEbook": "Has no ebook", - "LabelMissingParts": "Puuduvad osad", "LabelMissingSupplementaryEbook": "Has no supplementary ebook", "LabelMobileRedirectURIs": "Lubatud mobiilile suunamise URI-d", "LabelMobileRedirectURIsDescription": "See on mobiilirakenduste jaoks kehtivate suunamise URI-de lubatud nimekiri. Vaikimisi on selleks audiobookshelf://oauth, mida saate eemaldada või täiendada täiendavate URI-dega kolmanda osapoole rakenduste integreerimiseks. Tärni (*) ainukese kirjena kasutamine võimaldab mis tahes URI-d.", diff --git a/client/strings/fr.json b/client/strings/fr.json index b7b28472..516a2f32 100644 --- a/client/strings/fr.json +++ b/client/strings/fr.json @@ -320,7 +320,6 @@ "LabelIntervalEvery6Hours": "Toutes les 6 heures", "LabelIntervalEveryDay": "Tous les jours", "LabelIntervalEveryHour": "Toutes les heures", - "LabelInvalidParts": "Parties invalides", "LabelInvert": "Inverser", "LabelItem": "Article", "LabelLanguage": "Langue", @@ -357,7 +356,6 @@ "LabelMinute": "Minute", "LabelMissing": "Manquant", "LabelMissingEbook": "Has no ebook", - "LabelMissingParts": "Parties manquantes", "LabelMissingSupplementaryEbook": "Has no supplementary ebook", "LabelMobileRedirectURIs": "URI de redirection mobile autorisés", "LabelMobileRedirectURIsDescription": "Il s'agit d'une liste blanche d’URI de redirection valides pour les applications mobiles. Celui par défaut est audiobookshelf://oauth, que vous pouvez supprimer ou compléter avec des URIs supplémentaires pour l'intégration d'applications tierces. L’utilisation d’un astérisque (*) comme seule entrée autorise n’importe quel URI.", diff --git a/client/strings/gu.json b/client/strings/gu.json index 0a2dc3b6..0cf6d86d 100644 --- a/client/strings/gu.json +++ b/client/strings/gu.json @@ -320,7 +320,6 @@ "LabelIntervalEvery6Hours": "Every 6 hours", "LabelIntervalEveryDay": "Every day", "LabelIntervalEveryHour": "Every hour", - "LabelInvalidParts": "Invalid Parts", "LabelInvert": "Invert", "LabelItem": "Item", "LabelLanguage": "Language", @@ -357,7 +356,6 @@ "LabelMinute": "Minute", "LabelMissing": "Missing", "LabelMissingEbook": "Has no ebook", - "LabelMissingParts": "Missing Parts", "LabelMissingSupplementaryEbook": "Has no supplementary ebook", "LabelMobileRedirectURIs": "Allowed Mobile Redirect URIs", "LabelMobileRedirectURIsDescription": "This is a whitelist of valid redirect URIs for mobile apps. The default one is audiobookshelf://oauth, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (*) as the sole entry permits any URI.", diff --git a/client/strings/he.json b/client/strings/he.json index 435f60ad..63bd3339 100644 --- a/client/strings/he.json +++ b/client/strings/he.json @@ -320,7 +320,6 @@ "LabelIntervalEvery6Hours": "כל 6 שעות", "LabelIntervalEveryDay": "כל יום", "LabelIntervalEveryHour": "כל שעה", - "LabelInvalidParts": "חלקים לא תקינים", "LabelInvert": "הפוך", "LabelItem": "פריט", "LabelLanguage": "שפה", @@ -357,7 +356,6 @@ "LabelMinute": "דקה", "LabelMissing": "חסר", "LabelMissingEbook": "אין ספר אלקטרוני", - "LabelMissingParts": "חלקים חסרים", "LabelMissingSupplementaryEbook": "אין ספר אלקטרוני נלווה", "LabelMobileRedirectURIs": "כתובות משדר ניידות מורשות", "LabelMobileRedirectURIsDescription": "זהו רשימה לבניה של כתובות ה-URI הנתמכות להפניות עבור אפליקציות ניידות. הברירת מחדל היא audiobookshelf://oauth, שניתן להסיר או להוסיף לה כתובות נוספות לאינטגרציה עם אפליקציות צד שלישי. שימוש בכוכבית (*) כקלט בודד מאפשר כל URI.", diff --git a/client/strings/hi.json b/client/strings/hi.json index 4e8aae7b..3859f3d7 100644 --- a/client/strings/hi.json +++ b/client/strings/hi.json @@ -320,7 +320,6 @@ "LabelIntervalEvery6Hours": "Every 6 hours", "LabelIntervalEveryDay": "Every day", "LabelIntervalEveryHour": "Every hour", - "LabelInvalidParts": "Invalid Parts", "LabelInvert": "Invert", "LabelItem": "Item", "LabelLanguage": "Language", @@ -357,7 +356,6 @@ "LabelMinute": "Minute", "LabelMissing": "Missing", "LabelMissingEbook": "Has no ebook", - "LabelMissingParts": "Missing Parts", "LabelMissingSupplementaryEbook": "Has no supplementary ebook", "LabelMobileRedirectURIs": "Allowed Mobile Redirect URIs", "LabelMobileRedirectURIsDescription": "This is a whitelist of valid redirect URIs for mobile apps. The default one is audiobookshelf://oauth, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (*) as the sole entry permits any URI.", diff --git a/client/strings/hr.json b/client/strings/hr.json index fa5bcd35..3c81c88d 100644 --- a/client/strings/hr.json +++ b/client/strings/hr.json @@ -320,7 +320,6 @@ "LabelIntervalEvery6Hours": "Every 6 hours", "LabelIntervalEveryDay": "Every day", "LabelIntervalEveryHour": "Every hour", - "LabelInvalidParts": "Nevaljajuči dijelovi", "LabelInvert": "Invert", "LabelItem": "Stavka", "LabelLanguage": "Jezik", @@ -357,7 +356,6 @@ "LabelMinute": "Minuta", "LabelMissing": "Nedostaje", "LabelMissingEbook": "Has no ebook", - "LabelMissingParts": "Nedostajali dijelovi", "LabelMissingSupplementaryEbook": "Has no supplementary ebook", "LabelMobileRedirectURIs": "Allowed Mobile Redirect URIs", "LabelMobileRedirectURIsDescription": "This is a whitelist of valid redirect URIs for mobile apps. The default one is audiobookshelf://oauth, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (*) as the sole entry permits any URI.", diff --git a/client/strings/hu.json b/client/strings/hu.json index f7e2abd8..c5eec3c7 100644 --- a/client/strings/hu.json +++ b/client/strings/hu.json @@ -320,7 +320,6 @@ "LabelIntervalEvery6Hours": "Minden 6 órában", "LabelIntervalEveryDay": "Minden nap", "LabelIntervalEveryHour": "Minden órában", - "LabelInvalidParts": "Érvénytelen részek", "LabelInvert": "Megfordítás", "LabelItem": "Elem", "LabelLanguage": "Nyelv", @@ -357,7 +356,6 @@ "LabelMinute": "Perc", "LabelMissing": "Hiányzó", "LabelMissingEbook": "Has no ebook", - "LabelMissingParts": "Hiányzó részek", "LabelMissingSupplementaryEbook": "Has no supplementary ebook", "LabelMobileRedirectURIs": "Engedélyezett mobil átirányítási URI-k", "LabelMobileRedirectURIsDescription": "Ez egy fehérlista az érvényes mobilalkalmazás-átirányítási URI-k számára. Az alapértelmezett audiobookshelf://oauth, amely eltávolítható vagy kiegészíthető további URI-kkal harmadik féltől származó alkalmazásintegráció érdekében. Ha az egyetlen bejegyzés egy csillag (*), akkor bármely URI engedélyezett.", diff --git a/client/strings/it.json b/client/strings/it.json index bd6e5fa9..22c0503a 100644 --- a/client/strings/it.json +++ b/client/strings/it.json @@ -320,7 +320,6 @@ "LabelIntervalEvery6Hours": "Ogni 6 ore", "LabelIntervalEveryDay": "Ogni Giorno", "LabelIntervalEveryHour": "Ogni ora", - "LabelInvalidParts": "Parti Invalide", "LabelInvert": "Inverti", "LabelItem": "Oggetti", "LabelLanguage": "Lingua", @@ -357,7 +356,6 @@ "LabelMinute": "Minuto", "LabelMissing": "Altro", "LabelMissingEbook": "Non ha ebook", - "LabelMissingParts": "Parti rimanenti", "LabelMissingSupplementaryEbook": "Non ha ebook supplementare", "LabelMobileRedirectURIs": "URI di reindirizzamento mobile consentiti", "LabelMobileRedirectURIsDescription": "Questa è una lista bianca di URI di reindirizzamento validi per le app mobili. Quello predefinito è audiobookshelf://oauth, che puoi rimuovere o integrare con URI aggiuntivi per l'integrazione di app di terze parti. Utilizzando un asterisco (*) poiché l'unica voce consente qualsiasi URI.", @@ -778,4 +776,4 @@ "ToastSocketFailedToConnect": "Socket non riesce a connettersi", "ToastUserDeleteFailed": "Errore eliminazione utente", "ToastUserDeleteSuccess": "Utente eliminato" -} +} \ No newline at end of file diff --git a/client/strings/lt.json b/client/strings/lt.json index d36861a4..3eaba010 100644 --- a/client/strings/lt.json +++ b/client/strings/lt.json @@ -320,7 +320,6 @@ "LabelIntervalEvery6Hours": "Kas 6 valandas", "LabelIntervalEveryDay": "Kasdien", "LabelIntervalEveryHour": "Kiekvieną valandą", - "LabelInvalidParts": "Netinkamos dalys", "LabelInvert": "Apversti", "LabelItem": "Elementas", "LabelLanguage": "Kalba", @@ -357,7 +356,6 @@ "LabelMinute": "Minutė", "LabelMissing": "Trūksta", "LabelMissingEbook": "Has no ebook", - "LabelMissingParts": "Trūkstamos dalys", "LabelMissingSupplementaryEbook": "Has no supplementary ebook", "LabelMobileRedirectURIs": "Allowed Mobile Redirect URIs", "LabelMobileRedirectURIsDescription": "This is a whitelist of valid redirect URIs for mobile apps. The default one is audiobookshelf://oauth, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (*) as the sole entry permits any URI.", diff --git a/client/strings/nl.json b/client/strings/nl.json index b8d3f669..fb746672 100644 --- a/client/strings/nl.json +++ b/client/strings/nl.json @@ -320,7 +320,6 @@ "LabelIntervalEvery6Hours": "Iedere 6 uur", "LabelIntervalEveryDay": "Iedere dag", "LabelIntervalEveryHour": "Ieder uur", - "LabelInvalidParts": "Ongeldige delen", "LabelInvert": "Omdraaien", "LabelItem": "Onderdeel", "LabelLanguage": "Taal", @@ -357,7 +356,6 @@ "LabelMinute": "Minuut", "LabelMissing": "Ontbrekend", "LabelMissingEbook": "Has no ebook", - "LabelMissingParts": "Ontbrekende delen", "LabelMissingSupplementaryEbook": "Has no supplementary ebook", "LabelMobileRedirectURIs": "Allowed Mobile Redirect URIs", "LabelMobileRedirectURIsDescription": "This is a whitelist of valid redirect URIs for mobile apps. The default one is audiobookshelf://oauth, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (*) as the sole entry permits any URI.", diff --git a/client/strings/no.json b/client/strings/no.json index 818ee0fa..b66ab6f3 100644 --- a/client/strings/no.json +++ b/client/strings/no.json @@ -320,7 +320,6 @@ "LabelIntervalEvery6Hours": "Hver 6. timer", "LabelIntervalEveryDay": "Hver dag", "LabelIntervalEveryHour": "Hver time", - "LabelInvalidParts": "Ugyldige deler", "LabelInvert": "Inverter", "LabelItem": "Enhet", "LabelLanguage": "Språk", @@ -357,7 +356,6 @@ "LabelMinute": "Minutt", "LabelMissing": "Mangler", "LabelMissingEbook": "Has no ebook", - "LabelMissingParts": "Manglende deler", "LabelMissingSupplementaryEbook": "Has no supplementary ebook", "LabelMobileRedirectURIs": "Allowed Mobile Redirect URIs", "LabelMobileRedirectURIsDescription": "This is a whitelist of valid redirect URIs for mobile apps. The default one is audiobookshelf://oauth, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (*) as the sole entry permits any URI.", diff --git a/client/strings/pl.json b/client/strings/pl.json index cf4274cc..e1d7f289 100644 --- a/client/strings/pl.json +++ b/client/strings/pl.json @@ -320,7 +320,6 @@ "LabelIntervalEvery6Hours": "Co 6 godzin", "LabelIntervalEveryDay": "Każdego dnia", "LabelIntervalEveryHour": "Każdej godziny", - "LabelInvalidParts": "Nieprawidłowe części", "LabelInvert": "Invert", "LabelItem": "Pozycja", "LabelLanguage": "Język", @@ -357,7 +356,6 @@ "LabelMinute": "Minuta", "LabelMissing": "Brakujący", "LabelMissingEbook": "Has no ebook", - "LabelMissingParts": "Brakujące cześci", "LabelMissingSupplementaryEbook": "Has no supplementary ebook", "LabelMobileRedirectURIs": "Allowed Mobile Redirect URIs", "LabelMobileRedirectURIsDescription": "This is a whitelist of valid redirect URIs for mobile apps. The default one is audiobookshelf://oauth, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (*) as the sole entry permits any URI.", diff --git a/client/strings/pt-br.json b/client/strings/pt-br.json index c4d00eb7..47edcfac 100644 --- a/client/strings/pt-br.json +++ b/client/strings/pt-br.json @@ -320,7 +320,6 @@ "LabelIntervalEvery6Hours": "A cada 6 horas", "LabelIntervalEveryDay": "Todo dia", "LabelIntervalEveryHour": "Toda hora", - "LabelInvalidParts": "Partes Inválidas", "LabelInvert": "Inverter", "LabelItem": "Item", "LabelLanguage": "Idioma", @@ -357,7 +356,6 @@ "LabelMinute": "Minuto", "LabelMissing": "Ausente", "LabelMissingEbook": "Ebook não existe", - "LabelMissingParts": "Partes Ausentes", "LabelMissingSupplementaryEbook": "Ebook complementar não existe", "LabelMobileRedirectURIs": "URIs de redirecionamento móveis permitidas", "LabelMobileRedirectURIsDescription": "Essa é uma lista de permissionamento para URIs válidas para o redirecionamento de aplicativos móveis. A padrão é audiobookshelf://oauth, que pode ser removida ou acrescentada com novas URIs para integração com apps de terceiros. Usando um asterisco (*) como um item único dará permissão para qualquer URI.", @@ -778,4 +776,4 @@ "ToastSocketFailedToConnect": "Falha na conexão do socket", "ToastUserDeleteFailed": "Falha ao apagar usuário", "ToastUserDeleteSuccess": "Usuário apagado" -} +} \ No newline at end of file diff --git a/client/strings/ru.json b/client/strings/ru.json index a55e4668..9388739d 100644 --- a/client/strings/ru.json +++ b/client/strings/ru.json @@ -320,7 +320,6 @@ "LabelIntervalEvery6Hours": "Каждые 6 часов", "LabelIntervalEveryDay": "Каждый день", "LabelIntervalEveryHour": "Каждый час", - "LabelInvalidParts": "Неверные части", "LabelInvert": "Инвертировать", "LabelItem": "Элемент", "LabelLanguage": "Язык", @@ -357,7 +356,6 @@ "LabelMinute": "Минуты", "LabelMissing": "Потеряно", "LabelMissingEbook": "Has no ebook", - "LabelMissingParts": "Потерянные части", "LabelMissingSupplementaryEbook": "Has no supplementary ebook", "LabelMobileRedirectURIs": "Разрешенные URI перенаправления с мобильных устройств", "LabelMobileRedirectURIsDescription": "Это белый список допустимых URI перенаправления для мобильных приложений. По умолчанию используется audiobookshelf://oauth, который можно удалить или дополнить дополнительными URI для интеграции со сторонними приложениями. Использование звездочки (*) в качестве единственной записи разрешает любой URI.", diff --git a/client/strings/sv.json b/client/strings/sv.json index 820fe18a..b16f6aec 100644 --- a/client/strings/sv.json +++ b/client/strings/sv.json @@ -320,7 +320,6 @@ "LabelIntervalEvery6Hours": "Var 6:e timme", "LabelIntervalEveryDay": "Varje dag", "LabelIntervalEveryHour": "Varje timme", - "LabelInvalidParts": "Ogiltiga delar", "LabelInvert": "Invertera", "LabelItem": "Objekt", "LabelLanguage": "Språk", @@ -357,7 +356,6 @@ "LabelMinute": "Minut", "LabelMissing": "Saknad", "LabelMissingEbook": "Has no ebook", - "LabelMissingParts": "Saknade delar", "LabelMissingSupplementaryEbook": "Has no supplementary ebook", "LabelMobileRedirectURIs": "Allowed Mobile Redirect URIs", "LabelMobileRedirectURIsDescription": "This is a whitelist of valid redirect URIs for mobile apps. The default one is audiobookshelf://oauth, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (*) as the sole entry permits any URI.", diff --git a/client/strings/uk.json b/client/strings/uk.json index 9953bda5..7c0d4476 100644 --- a/client/strings/uk.json +++ b/client/strings/uk.json @@ -320,7 +320,6 @@ "LabelIntervalEvery6Hours": "Кожні 6 годин", "LabelIntervalEveryDay": "Щодня", "LabelIntervalEveryHour": "Щогодини", - "LabelInvalidParts": "Недопустимі частини", "LabelInvert": "Інвертувати", "LabelItem": "Елемент", "LabelLanguage": "Мова", @@ -357,7 +356,6 @@ "LabelMinute": "Хвилина", "LabelMissing": "Бракує", "LabelMissingEbook": "Без електронної книги", - "LabelMissingParts": "Відсутні частини", "LabelMissingSupplementaryEbook": "Без додаткової електронної книги", "LabelMobileRedirectURIs": "Дозволені адреси перенаправлення", "LabelMobileRedirectURIsDescription": "Це білий список наявних URI, що перенаправляють у мобільний додаток. За замовчуванням це audiobookshelf://oauth, який ви можете видалити або ж додати інші адреси для сторонніх інтеграцій. Використайте зірочку (*), аби дозволити будь-яке URI.", @@ -778,4 +776,4 @@ "ToastSocketFailedToConnect": "Не вдалося під'єднатися до сокета", "ToastUserDeleteFailed": "Не вдалося видалити користувача", "ToastUserDeleteSuccess": "Користувача видалено" -} +} \ No newline at end of file diff --git a/client/strings/vi-vn.json b/client/strings/vi-vn.json index bddfd647..1f356676 100644 --- a/client/strings/vi-vn.json +++ b/client/strings/vi-vn.json @@ -320,7 +320,6 @@ "LabelIntervalEvery6Hours": "Mỗi 6 giờ", "LabelIntervalEveryDay": "Mỗi ngày", "LabelIntervalEveryHour": "Mỗi giờ", - "LabelInvalidParts": "Phần không hợp lệ", "LabelInvert": "Nghịch đảo", "LabelItem": "Mục", "LabelLanguage": "Ngôn ngữ", @@ -357,7 +356,6 @@ "LabelMinute": "Phút", "LabelMissing": "Thiếu", "LabelMissingEbook": "Không có ebook", - "LabelMissingParts": "Các phần thiếu", "LabelMissingSupplementaryEbook": "Không có ebook bổ sung", "LabelMobileRedirectURIs": "URI chuyển hướng di động được cho phép", "LabelMobileRedirectURIsDescription": "Đây là danh sách trắng các URI chuyển hướng hợp lệ cho ứng dụng di động. Mặc định là audiobookshelf://oauth, bạn có thể loại bỏ hoặc bổ sung thêm các URI cho tích hợp ứng dụng bên thứ ba. Sử dụng dấu hoa thị (*) như một mục duy nhất cho phép bất kỳ URI nào.", diff --git a/client/strings/zh-cn.json b/client/strings/zh-cn.json index bf816f89..2dc1ab16 100644 --- a/client/strings/zh-cn.json +++ b/client/strings/zh-cn.json @@ -320,7 +320,6 @@ "LabelIntervalEvery6Hours": "每 6 小时", "LabelIntervalEveryDay": "每天", "LabelIntervalEveryHour": "每小时", - "LabelInvalidParts": "无效部件", "LabelInvert": "倒转", "LabelItem": "项目", "LabelLanguage": "语言", @@ -357,7 +356,6 @@ "LabelMinute": "分钟", "LabelMissing": "丢失", "LabelMissingEbook": "Has no ebook", - "LabelMissingParts": "丢失的部分", "LabelMissingSupplementaryEbook": "Has no supplementary ebook", "LabelMobileRedirectURIs": "允许移动应用重定向 URI", "LabelMobileRedirectURIsDescription": "这是移动应用程序的有效重定向 URI 白名单. 默认值为 audiobookshelf://oauth,您可以删除它或添加其他 URI 以进行第三方应用集成. 使用星号 (*) 作为唯一条目允许任何 URI.", diff --git a/client/strings/zh-tw.json b/client/strings/zh-tw.json index 5a9afc98..23321911 100644 --- a/client/strings/zh-tw.json +++ b/client/strings/zh-tw.json @@ -320,7 +320,6 @@ "LabelIntervalEvery6Hours": "每 6 小時", "LabelIntervalEveryDay": "每天", "LabelIntervalEveryHour": "每小時", - "LabelInvalidParts": "無效部件", "LabelInvert": "倒轉", "LabelItem": "項目", "LabelLanguage": "語言", @@ -357,7 +356,6 @@ "LabelMinute": "分鐘", "LabelMissing": "丟失", "LabelMissingEbook": "Has no ebook", - "LabelMissingParts": "丟失的部分", "LabelMissingSupplementaryEbook": "Has no supplementary ebook", "LabelMobileRedirectURIs": "允許移動應用重定向 URI", "LabelMobileRedirectURIsDescription": "這是移動應用程序的有效重定向 URI 白名單. 預設值為 audiobookshelf://oauth,您可以刪除它或加入其他 URI 以進行第三方應用集成. 使用星號 (*) 作為唯一條目允許任何 URI.", @@ -466,6 +464,8 @@ "LabelSettingsHideSingleBookSeriesHelp": "只有一本書的系列將從系列頁面和主頁書架中隱藏.", "LabelSettingsHomePageBookshelfView": "首頁使用書架視圖", "LabelSettingsLibraryBookshelfView": "媒體庫使用書架視圖", + "LabelSettingsOnlyShowLaterBooksInContinueSeries": "Skip earlier books in Continue Series", + "LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "The Continue Series home page shelf shows the first book not started in series that have at least one book finished and no books in progress. Enabling this setting will continue series from the furthest completed book instead of the first book not started.", "LabelSettingsParseSubtitles": "解析副標題", "LabelSettingsParseSubtitlesHelp": "從有聲書資料夾中提取副標題.
副標題必須用 \" - \" 分隔.
例: \"書名 - 這裡是副標題\" 則顯示副標題 \"這裡是副標題\"", "LabelSettingsPreferMatchedMetadata": "首選匹配的元數據", @@ -776,4 +776,4 @@ "ToastSocketFailedToConnect": "網路連接失敗", "ToastUserDeleteFailed": "刪除使用者失敗", "ToastUserDeleteSuccess": "使用者已刪除" -} +} \ No newline at end of file diff --git a/server/models/Book.js b/server/models/Book.js index 6b179c36..e2b56fbe 100644 --- a/server/models/Book.js +++ b/server/models/Book.js @@ -7,7 +7,7 @@ const Logger = require('../Logger') * @property {string} ebookFormat * @property {number} addedAt * @property {number} updatedAt - * @property {{filename:string, ext:string, path:string, relPath:string, size:number, mtimeMs:number, ctimeMs:number, birthtimeMs:number}} metadata + * @property {{filename:string, ext:string, path:string, relPath:strFing, size:number, mtimeMs:number, ctimeMs:number, birthtimeMs:number}} metadata */ /** diff --git a/server/objects/files/AudioFile.js b/server/objects/files/AudioFile.js index 8a3c2a74..c0c425ba 100644 --- a/server/objects/files/AudioFile.js +++ b/server/objects/files/AudioFile.js @@ -32,7 +32,6 @@ class AudioFile { this.metaTags = null this.manuallyVerified = false - this.invalid = false this.exclude = false this.error = null @@ -53,7 +52,6 @@ class AudioFile { trackNumFromFilename: this.trackNumFromFilename, discNumFromFilename: this.discNumFromFilename, manuallyVerified: !!this.manuallyVerified, - invalid: !!this.invalid, exclude: !!this.exclude, error: this.error || null, format: this.format, @@ -78,7 +76,6 @@ class AudioFile { this.addedAt = data.addedAt this.updatedAt = data.updatedAt this.manuallyVerified = !!data.manuallyVerified - this.invalid = !!data.invalid this.exclude = !!data.exclude this.error = data.error || null @@ -112,10 +109,6 @@ class AudioFile { } } - get isValidTrack() { - return !this.invalid && !this.exclude - } - // New scanner creates AudioFile from AudioFileScanner setDataFromProbe(libraryFile, probeData) { this.ino = libraryFile.ino || null diff --git a/server/objects/mediaTypes/Book.js b/server/objects/mediaTypes/Book.js index d53a53a7..8fdff988 100644 --- a/server/objects/mediaTypes/Book.js +++ b/server/objects/mediaTypes/Book.js @@ -17,7 +17,6 @@ class Book { this.audioFiles = [] this.chapters = [] - this.missingParts = [] this.ebookFile = null this.lastCoverSearch = null @@ -36,7 +35,6 @@ class Book { this.tags = [...book.tags] this.audioFiles = book.audioFiles.map(f => new AudioFile(f)) this.chapters = book.chapters.map(c => ({ ...c })) - this.missingParts = book.missingParts ? [...book.missingParts] : [] this.ebookFile = book.ebookFile ? new EBookFile(book.ebookFile) : null this.lastCoverSearch = book.lastCoverSearch || null this.lastCoverSearchQuery = book.lastCoverSearchQuery || null @@ -51,7 +49,6 @@ class Book { tags: [...this.tags], audioFiles: this.audioFiles.map(f => f.toJSON()), chapters: this.chapters.map(c => ({ ...c })), - missingParts: [...this.missingParts], ebookFile: this.ebookFile ? this.ebookFile.toJSON() : null } } @@ -65,8 +62,6 @@ class Book { numTracks: this.tracks.length, numAudioFiles: this.audioFiles.length, numChapters: this.chapters.length, - numMissingParts: this.missingParts.length, - numInvalidAudioFiles: this.invalidAudioFiles.length, duration: this.duration, size: this.size, ebookFormat: this.ebookFile?.ebookFormat @@ -85,7 +80,6 @@ class Book { duration: this.duration, size: this.size, tracks: this.tracks.map(t => t.toJSON()), - missingParts: [...this.missingParts], ebookFile: this.ebookFile?.toJSON() || null } } @@ -109,11 +103,8 @@ class Book { get hasMediaEntities() { return !!this.tracks.length || this.ebookFile } - get invalidAudioFiles() { - return this.audioFiles.filter(af => af.invalid) - } get includedAudioFiles() { - return this.audioFiles.filter(af => !af.exclude && !af.invalid) + return this.audioFiles.filter(af => !af.exclude) } get tracks() { let startOffset = 0 @@ -238,7 +229,6 @@ class Book { this.audioFiles = orderedFileData.map((fileData) => { const audioFile = this.audioFiles.find(af => af.ino === fileData.ino) audioFile.manuallyVerified = true - audioFile.invalid = false audioFile.error = null if (fileData.exclude !== undefined) { audioFile.exclude = !!fileData.exclude @@ -257,7 +247,6 @@ class Book { rebuildTracks() { Logger.debug(`[Book] Tracks being rebuilt...!`) this.audioFiles.sort((a, b) => a.index - b.index) - this.missingParts = [] } // Only checks container format From 7d0eb215d6d0d5e39fb4480daf3e765104142316 Mon Sep 17 00:00:00 2001 From: Nicholas Wallace Date: Fri, 22 Mar 2024 01:28:50 +0000 Subject: [PATCH 0024/1695] Add integration workflow --- .github/workflows/i18n-integration.yml | 27 ++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 .github/workflows/i18n-integration.yml diff --git a/.github/workflows/i18n-integration.yml b/.github/workflows/i18n-integration.yml new file mode 100644 index 00000000..cee07b20 --- /dev/null +++ b/.github/workflows/i18n-integration.yml @@ -0,0 +1,27 @@ +name: Verify all i18n files are alphabetized + +on: + push: + paths: + - client/strings/** # Should only check if any strings changed + +jobs: + update_translations: + runs-on: ubuntu-latest + steps: + # Check out the repository + - name: Checkout repository + uses: actions/checkout@v4 + + # Set up node to run the javascript + - name: Set up node + uses: actions/setup-node@v4 + with: + node-version: "20" + + # The only argument is the `directory`, which is where the i18n files are + # stored. + - name: Run Update JSON Files action + uses: audiobookshelf/audiobookshelf-i18n-updater@v1.1.1 + with: + directory: "client/strings/" # Adjust the directory path as needed From 961533765f8006492c3c1ee8c877ab29c255e32c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rasmus=20Kr=C3=A4mer?= Date: Sat, 23 Mar 2024 14:54:34 +0100 Subject: [PATCH 0025/1695] Fix custom metadata provider crash --- server/finders/BookFinder.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/finders/BookFinder.js b/server/finders/BookFinder.js index 2c6fc9ee..9aa0a182 100644 --- a/server/finders/BookFinder.js +++ b/server/finders/BookFinder.js @@ -158,7 +158,7 @@ class BookFinder { * @returns {Promise} */ async getCustomProviderResults(title, author, isbn, providerSlug) { - const books = await this.customProviderAdapter.search(title, author, providerSlug, 'book') + const books = await this.customProviderAdapter.search(title, author, isbn, providerSlug, 'book') if (this.verbose) Logger.debug(`Custom provider '${providerSlug}' Search Results: ${books.length || 0}`) return books From 68276fe30ba4397e1d293e855e8740f4efb93453 Mon Sep 17 00:00:00 2001 From: mikiher Date: Sat, 23 Mar 2024 18:31:52 +0200 Subject: [PATCH 0026/1695] Fix handling of file moves from root folder to sub folder and back --- server/scanner/LibraryItemScanner.js | 2 +- server/scanner/LibraryScanner.js | 75 +++++++++++++++++++++++++--- 2 files changed, 69 insertions(+), 8 deletions(-) diff --git a/server/scanner/LibraryItemScanner.js b/server/scanner/LibraryItemScanner.js index 872000d8..1c3123df 100644 --- a/server/scanner/LibraryItemScanner.js +++ b/server/scanner/LibraryItemScanner.js @@ -56,7 +56,7 @@ class LibraryItemScanner { const libraryItemPath = updateLibraryItemDetails?.path || fileUtils.filePathToPOSIX(libraryItem.path) const folder = library.libraryFolders[0] - const libraryItemScanData = await this.getLibraryItemScanData(libraryItemPath, library, folder, false) + const libraryItemScanData = await this.getLibraryItemScanData(libraryItemPath, library, folder, updateLibraryItemDetails?.isFile || false) let libraryItemDataUpdated = await libraryItemScanData.checkLibraryItemData(libraryItem, scanLogger) diff --git a/server/scanner/LibraryScanner.js b/server/scanner/LibraryScanner.js index ac422c79..d394128c 100644 --- a/server/scanner/LibraryScanner.js +++ b/server/scanner/LibraryScanner.js @@ -154,7 +154,11 @@ class LibraryScanner { let libraryItemData = libraryItemDataFound.find(lid => lid.path === existingLibraryItem.path) if (!libraryItemData) { // Fallback to finding matching library item with matching inode value - libraryItemData = libraryItemDataFound.find(lid => lid.ino === existingLibraryItem.ino) + libraryItemData = libraryItemDataFound.find(lid => + ItemToItemInoMatch(lid, existingLibraryItem) || + ItemToFileInoMatch(lid, existingLibraryItem) || + ItemToFileInoMatch(existingLibraryItem, lid) + ) if (libraryItemData) { libraryScan.addLog(LogLevel.INFO, `Library item with path "${existingLibraryItem.path}" was not found, but library item inode "${existingLibraryItem.ino}" was found at path "${libraryItemData.path}"`) } @@ -522,23 +526,25 @@ class LibraryScanner { // Check if book dir group is already an item let existingLibraryItem = await Database.libraryItemModel.findOneOld({ + libraryId: library.id, path: potentialChildDirs }) let updatedLibraryItemDetails = {} if (!existingLibraryItem) { - const dirIno = await fileUtils.getIno(fullPath) - existingLibraryItem = await Database.libraryItemModel.findOneOld({ - ino: dirIno - }) + const isSingleMedia = isSingleMediaFile(fileUpdateGroup, itemDir) + existingLibraryItem = + await findLibraryItemByItemToItemInoMatch(library.id, fullPath) || + await findLibraryItemByItemToFileInoMatch(library.id, fullPath, isSingleMedia) || + await findLibraryItemByFileToItemInoMatch(library.id, fullPath, isSingleMedia, fileUpdateGroup[itemDir]) if (existingLibraryItem) { - Logger.debug(`[LibraryScanner] scanFolderUpdates: Library item found by inode value=${dirIno}. "${existingLibraryItem.relPath} => ${itemDir}"`) // Update library item paths for scan existingLibraryItem.path = fullPath existingLibraryItem.relPath = itemDir updatedLibraryItemDetails.path = fullPath updatedLibraryItemDetails.relPath = itemDir updatedLibraryItemDetails.libraryFolderId = folder.id + updatedLibraryItemDetails.isFile = isSingleMedia } } if (existingLibraryItem) { @@ -555,7 +561,6 @@ class LibraryScanner { continue } } - // Scan library item for updates Logger.debug(`[LibraryScanner] Folder update for relative path "${itemDir}" is in library item "${existingLibraryItem.media.metadata.title}" - scan for updates`) itemGroupingResults[itemDir] = await LibraryItemScanner.scanLibraryItem(existingLibraryItem.id, updatedLibraryItemDetails) @@ -595,6 +600,14 @@ class LibraryScanner { } module.exports = new LibraryScanner() +function ItemToFileInoMatch(libraryItem1, libraryItem2) { + return libraryItem1.isFile && libraryItem2.libraryFiles.some(lf => lf.ino === libraryItem1.ino) +} + +function ItemToItemInoMatch(libraryItem1, libraryItem2) { + return libraryItem1.ino === libraryItem2.ino +} + function hasAudioFiles(fileUpdateGroup, itemDir) { return isSingleMediaFile(fileUpdateGroup, itemDir) ? scanUtils.checkFilepathIsAudioFile(fileUpdateGroup[itemDir]) : @@ -604,3 +617,51 @@ function hasAudioFiles(fileUpdateGroup, itemDir) { function isSingleMediaFile(fileUpdateGroup, itemDir) { return itemDir === fileUpdateGroup[itemDir] } + +async function findLibraryItemByItemToItemInoMatch(libraryId, fullPath) { + const ino = await fileUtils.getIno(fullPath) + if (!ino) return null + const existingLibraryItem = await Database.libraryItemModel.findOneOld({ + libraryId: libraryId, + ino: ino + }) + if (existingLibraryItem) + Logger.debug(`[LibraryScanner] Found library item with matching inode "${ino}" at path "${existingLibraryItem.path}"`) + return existingLibraryItem +} + +async function findLibraryItemByItemToFileInoMatch(libraryId, fullPath, isSingleMedia) { + if (!isSingleMedia) return null + // check if it was moved from another folder by comparing the ino to the library files + const ino = await fileUtils.getIno(fullPath) + if (!ino) return null + const existingLibraryItem = await Database.libraryItemModel.findOneOld({ + libraryId: libraryId, + libraryFiles: { + [ sequelize.Op.substring ]: ino + } + }) + if (existingLibraryItem) + Logger.debug(`[LibraryScanner] Found library item with a library file matching inode "${ino}" at path "${existingLibraryItem.path}"`) + return existingLibraryItem +} + +async function findLibraryItemByFileToItemInoMatch(libraryId, fullPath, isSingleMedia, itemFiles) { + if (isSingleMedia) return null + // check if it was moved from the root folder by comparing the ino to the ino of the scanned files + let itemFileInos = [] + for (const itemFile of itemFiles) { + const ino = await fileUtils.getIno(Path.posix.join(fullPath, itemFile)) + if (ino) itemFileInos.push(ino) + } + if (!itemFileInos.length) return null + const existingLibraryItem = await Database.libraryItemModel.findOneOld({ + libraryId: libraryId, + ino: { + [ sequelize.Op.in ] : itemFileInos + } + }) + if (existingLibraryItem) + Logger.debug(`[LibraryScanner] Found library item with inode matching one of "${itemFileInos.join(',')}" at path "${existingLibraryItem.path}"`) + return existingLibraryItem +} \ No newline at end of file From f827aa97f8d74aead118387f2e8cd608cad26393 Mon Sep 17 00:00:00 2001 From: advplyr Date: Sat, 23 Mar 2024 14:56:32 -0500 Subject: [PATCH 0027/1695] Update library scanner findLibraryItemByItemToFileInoMatch query to iterate through json objects comparing inodes --- server/Logger.js | 2 +- server/models/LibraryItem.js | 10 ++++++---- server/scanner/LibraryScanner.js | 28 ++++++++++++++++------------ 3 files changed, 23 insertions(+), 17 deletions(-) diff --git a/server/Logger.js b/server/Logger.js index 7cc7aa4c..8283e1f0 100644 --- a/server/Logger.js +++ b/server/Logger.js @@ -93,7 +93,7 @@ class Logger { // Save log to file if (level >= this.logLevel) { - await this.logManager.logToFile(logObj) + await this.logManager?.logToFile(logObj) } } diff --git a/server/models/LibraryItem.js b/server/models/LibraryItem.js index 44758750..704e2f10 100644 --- a/server/models/LibraryItem.js +++ b/server/models/LibraryItem.js @@ -1,4 +1,4 @@ -const { DataTypes, Model, WhereOptions } = require('sequelize') +const { DataTypes, Model } = require('sequelize') const Logger = require('../Logger') const oldLibraryItem = require('../objects/LibraryItem') const libraryFilters = require('../utils/queries/libraryFilters') @@ -116,7 +116,7 @@ class LibraryItem extends Model { /** * Currently unused because this is too slow and uses too much mem - * @param {[WhereOptions]} where + * @param {import('sequelize').WhereOptions} [where] * @returns {Array} old library items */ static async getAllOldLibraryItems(where = null) { @@ -773,12 +773,14 @@ class LibraryItem extends Model { /** * - * @param {WhereOptions} where + * @param {import('sequelize').WhereOptions} where + * @param {import('sequelize').BindOrReplacements} replacements * @returns {Object} oldLibraryItem */ - static async findOneOld(where) { + static async findOneOld(where, replacements = {}) { const libraryItem = await this.findOne({ where, + replacements, include: [ { model: this.sequelize.models.book, diff --git a/server/scanner/LibraryScanner.js b/server/scanner/LibraryScanner.js index d394128c..8131300d 100644 --- a/server/scanner/LibraryScanner.js +++ b/server/scanner/LibraryScanner.js @@ -155,10 +155,10 @@ class LibraryScanner { if (!libraryItemData) { // Fallback to finding matching library item with matching inode value libraryItemData = libraryItemDataFound.find(lid => - ItemToItemInoMatch(lid, existingLibraryItem) || - ItemToFileInoMatch(lid, existingLibraryItem) || + ItemToItemInoMatch(lid, existingLibraryItem) || + ItemToFileInoMatch(lid, existingLibraryItem) || ItemToFileInoMatch(existingLibraryItem, lid) - ) + ) if (libraryItemData) { libraryScan.addLog(LogLevel.INFO, `Library item with path "${existingLibraryItem.path}" was not found, but library item inode "${existingLibraryItem.ino}" was found at path "${libraryItemData.path}"`) } @@ -533,8 +533,8 @@ class LibraryScanner { let updatedLibraryItemDetails = {} if (!existingLibraryItem) { const isSingleMedia = isSingleMediaFile(fileUpdateGroup, itemDir) - existingLibraryItem = - await findLibraryItemByItemToItemInoMatch(library.id, fullPath) || + existingLibraryItem = + await findLibraryItemByItemToItemInoMatch(library.id, fullPath) || await findLibraryItemByItemToFileInoMatch(library.id, fullPath, isSingleMedia) || await findLibraryItemByFileToItemInoMatch(library.id, fullPath, isSingleMedia, fileUpdateGroup[itemDir]) if (existingLibraryItem) { @@ -630,16 +630,20 @@ async function findLibraryItemByItemToItemInoMatch(libraryId, fullPath) { return existingLibraryItem } -async function findLibraryItemByItemToFileInoMatch(libraryId, fullPath, isSingleMedia) { +async function findLibraryItemByItemToFileInoMatch(libraryId, fullPath, isSingleMedia) { if (!isSingleMedia) return null // check if it was moved from another folder by comparing the ino to the library files const ino = await fileUtils.getIno(fullPath) if (!ino) return null - const existingLibraryItem = await Database.libraryItemModel.findOneOld({ - libraryId: libraryId, - libraryFiles: { - [ sequelize.Op.substring ]: ino - } + const existingLibraryItem = await Database.libraryItemModel.findOneOld([ + { + libraryId: libraryId + }, + sequelize.where(sequelize.literal('(SELECT count(*) FROM json_each(libraryFiles) WHERE json_valid(json_each.value) AND json_each.value->>"$.ino" = :inode)'), { + [sequelize.Op.gt]: 0 + }) + ], { + inode: ino }) if (existingLibraryItem) Logger.debug(`[LibraryScanner] Found library item with a library file matching inode "${ino}" at path "${existingLibraryItem.path}"`) @@ -658,7 +662,7 @@ async function findLibraryItemByFileToItemInoMatch(libraryId, fullPath, isSingle const existingLibraryItem = await Database.libraryItemModel.findOneOld({ libraryId: libraryId, ino: { - [ sequelize.Op.in ] : itemFileInos + [sequelize.Op.in]: itemFileInos } }) if (existingLibraryItem) From 125346bb5c9b2b879d686a3a0d9e80a9b46afb8b Mon Sep 17 00:00:00 2001 From: Nicholas Wallace Date: Sun, 24 Mar 2024 03:11:00 +0000 Subject: [PATCH 0028/1695] Translation guide link added to readme --- readme.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/readme.md b/readme.md index 1f3f1d05..1fc2e327 100644 --- a/readme.md +++ b/readme.md @@ -336,6 +336,8 @@ Need to do one of following: This application is built using [NodeJs](https://nodejs.org/). +Information on helping with translations of the web client [here](https://www.audiobookshelf.org/faq#how-do-i-help-with-translations). + ### Dev Container Setup The easiest way to begin developing this project is to use a dev container. An introduction to dev containers in VSCode can be found [here](https://code.visualstudio.com/docs/devcontainers/containers). From 2d68fa2c278d5c3b5642486c8d410935a2baf3af Mon Sep 17 00:00:00 2001 From: Lauri Vuorela Date: Mon, 25 Mar 2024 16:32:29 +0100 Subject: [PATCH 0029/1695] fix book limit for the contiue series shelf --- server/utils/queries/libraryItemsBookFilters.js | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/server/utils/queries/libraryItemsBookFilters.js b/server/utils/queries/libraryItemsBookFilters.js index 07a8458d..12bc9701 100644 --- a/server/utils/queries/libraryItemsBookFilters.js +++ b/server/utils/queries/libraryItemsBookFilters.js @@ -658,8 +658,13 @@ module.exports = { let includeAttributes = [ [Sequelize.literal('(SELECT max(mp.updatedAt) FROM bookSeries bs, mediaProgresses mp WHERE mp.mediaItemId = bs.bookId AND mp.userId = :userId AND bs.seriesId = series.id)'), 'recent_progress'], ] + let booksNotFinishedQuery = `SELECT count(*) FROM bookSeries bs LEFT OUTER JOIN mediaProgresses mp ON mp.mediaItemId = bs.bookId AND mp.userId = :userId WHERE bs.seriesId = series.id AND (mp.isFinished = 0 OR mp.isFinished IS NULL)` + if (library.settings.onlyShowLaterBooksInContinueSeries) { - includeAttributes.push([Sequelize.literal('(SELECT CAST(max(bs.sequence) as FLOAT) FROM bookSeries bs, mediaProgresses mp WHERE mp.mediaItemId = bs.bookId AND mp.isFinished = 1 AND mp.userId = :userId AND bs.seriesId = series.id)'), 'maxSequence']) + const maxSequenceQuery = `(SELECT CAST(max(bs.sequence) as FLOAT) FROM bookSeries bs, mediaProgresses mp WHERE mp.mediaItemId = bs.bookId AND mp.isFinished = 1 AND mp.userId = :userId AND bs.seriesId = series.id)` + includeAttributes.push([Sequelize.literal(`${maxSequenceQuery}`), 'maxSequence']) + + booksNotFinishedQuery = booksNotFinishedQuery + ` AND CAST(bs.sequence as FLOAT) > ${maxSequenceQuery}` } const { rows: series, count } = await Database.seriesModel.findAndCountAll({ @@ -675,8 +680,8 @@ module.exports = { Sequelize.where(Sequelize.literal(`(SELECT count(*) FROM mediaProgresses mp, bookSeries bs WHERE bs.seriesId = series.id AND mp.mediaItemId = bs.bookId AND mp.userId = :userId AND mp.isFinished = 1)`), { [Sequelize.Op.gte]: 1 }), - // Has at least 1 book not finished - Sequelize.where(Sequelize.literal(`(SELECT count(*) FROM bookSeries bs LEFT OUTER JOIN mediaProgresses mp ON mp.mediaItemId = bs.bookId AND mp.userId = :userId WHERE bs.seriesId = series.id AND (mp.isFinished = 0 OR mp.isFinished IS NULL))`), { + // Has at least 1 book not finished (that has a sequence number higher than the highest already read, if library config is toggled) + Sequelize.where(Sequelize.literal(`(${booksNotFinishedQuery})`), { [Sequelize.Op.gte]: 1 }), // Has no books in progress From 8ce5a5cdbdbcda02c65fb0dbd1e6a25c2d3b6fe8 Mon Sep 17 00:00:00 2001 From: mikiher Date: Wed, 27 Mar 2024 13:18:02 +0200 Subject: [PATCH 0030/1695] Add workflow to dispatch an abs-windows event --- .github/workflows/notify-abs-windows.yml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 .github/workflows/notify-abs-windows.yml diff --git a/.github/workflows/notify-abs-windows.yml b/.github/workflows/notify-abs-windows.yml new file mode 100644 index 00000000..9ede33b8 --- /dev/null +++ b/.github/workflows/notify-abs-windows.yml @@ -0,0 +1,17 @@ +name: Dispatch an abs-windows event + +on: + release: + types: [published] + workflow_dispatch: + +jobs: + abs-windows-dispatch: + runs-on: ubuntu-latest + steps: + - name: Send a remote repository dispatch event + uses: peter-evans/repository-dispatch@v3 + with: + token: ${{ secrets.ABS_WINDOWS_PAT }} + repository: mikiher/audiobookshelf-windows + event-type: build-windows From 1cf0bd0f01d442899fc5943dfbdfde6704a76c74 Mon Sep 17 00:00:00 2001 From: mikiher Date: Wed, 27 Mar 2024 13:30:00 +0200 Subject: [PATCH 0031/1695] add dummy pull_request event for the workflow to appear in the list --- .github/workflows/notify-abs-windows.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/notify-abs-windows.yml b/.github/workflows/notify-abs-windows.yml index 9ede33b8..2f7414c8 100644 --- a/.github/workflows/notify-abs-windows.yml +++ b/.github/workflows/notify-abs-windows.yml @@ -4,6 +4,8 @@ on: release: types: [published] workflow_dispatch: + pull_request: + types: [opened] jobs: abs-windows-dispatch: From 33e4b51aee873b9a00ae54bb95c36efdcae7c842 Mon Sep 17 00:00:00 2001 From: mikiher Date: Wed, 27 Mar 2024 13:38:17 +0200 Subject: [PATCH 0032/1695] Revert "add dummy pull_request event for the workflow to appear in the list" This reverts commit 1cf0bd0f01d442899fc5943dfbdfde6704a76c74. --- .github/workflows/notify-abs-windows.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/notify-abs-windows.yml b/.github/workflows/notify-abs-windows.yml index 2f7414c8..9ede33b8 100644 --- a/.github/workflows/notify-abs-windows.yml +++ b/.github/workflows/notify-abs-windows.yml @@ -4,8 +4,6 @@ on: release: types: [published] workflow_dispatch: - pull_request: - types: [opened] jobs: abs-windows-dispatch: From 740640884fa8ec2f85acc20d48d7aeb65d66e967 Mon Sep 17 00:00:00 2001 From: advplyr Date: Wed, 27 Mar 2024 16:11:47 -0500 Subject: [PATCH 0033/1695] Update:Support for comic files with webp images #2792 --- client/components/readers/ComicReader.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/components/readers/ComicReader.vue b/client/components/readers/ComicReader.vue index d55fc0d6..49e9b093 100644 --- a/client/components/readers/ComicReader.vue +++ b/client/components/readers/ComicReader.vue @@ -334,7 +334,7 @@ export default { } }, parseFilenames(filenames) { - const acceptableImages = ['.jpeg', '.jpg', '.png'] + const acceptableImages = ['.jpeg', '.jpg', '.png', '.webp'] var imageFiles = filenames.filter((f) => { return acceptableImages.includes((Path.extname(f) || '').toLowerCase()) }) From 617b8f4487d506da962658a8fd371584e8ba7734 Mon Sep 17 00:00:00 2001 From: Denis Arnst Date: Thu, 28 Mar 2024 16:16:26 +0100 Subject: [PATCH 0034/1695] OpenID: Rename tags switch --- server/objects/user/User.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/objects/user/User.js b/server/objects/user/User.js index d09e921d..b473637b 100644 --- a/server/objects/user/User.js +++ b/server/objects/user/User.js @@ -277,7 +277,7 @@ class User { canAccessExplicitContent: 'accessExplicitContent', canAccessAllLibraries: 'accessAllLibraries', canAccessAllTags: 'accessAllTags', - tagsAreBlacklist: 'selectedTagsNotAccessible', + tagsAreDenylist: 'selectedTagsNotAccessible', // Direct mapping for array-based permissions allowedLibraries: 'librariesAccessible', allowedTags: 'itemTagsSelected', From 33254654d5fbf589eee90861309a5ef2a51d572b Mon Sep 17 00:00:00 2001 From: mikiher Date: Thu, 28 Mar 2024 23:56:59 +0200 Subject: [PATCH 0035/1695] Add dir="auto" attribute where it makes sense --- client/components/cards/LazyBookCard.vue | 2 +- client/components/modals/item/tabs/Episodes.vue | 2 +- client/components/modals/podcast/ViewEpisode.vue | 4 ++-- client/components/tables/ChaptersTable.vue | 2 +- client/components/tables/podcast/DownloadQueueTable.vue | 2 +- client/components/tables/podcast/LazyEpisodeRow.vue | 2 +- client/components/ui/TextInput.vue | 2 +- client/components/ui/TextareaInput.vue | 2 +- client/pages/item/_id/index.vue | 2 +- client/pages/library/_library/podcast/latest.vue | 4 ++-- 10 files changed, 12 insertions(+), 12 deletions(-) diff --git a/client/components/cards/LazyBookCard.vue b/client/components/cards/LazyBookCard.vue index efeb0165..faa93997 100644 --- a/client/components/cards/LazyBookCard.vue +++ b/client/components/cards/LazyBookCard.vue @@ -6,7 +6,7 @@ -
+

{{ displayTitle }}

diff --git a/client/components/modals/item/tabs/Episodes.vue b/client/components/modals/item/tabs/Episodes.vue index 661f41e0..ecf58330 100644 --- a/client/components/modals/item/tabs/Episodes.vue +++ b/client/components/modals/item/tabs/Episodes.vue @@ -29,7 +29,7 @@

{{ episode.episode }}

- + {{ episode.title }} diff --git a/client/components/modals/podcast/ViewEpisode.vue b/client/components/modals/podcast/ViewEpisode.vue index 79f22a03..411e9efd 100644 --- a/client/components/modals/podcast/ViewEpisode.vue +++ b/client/components/modals/podcast/ViewEpisode.vue @@ -15,8 +15,8 @@

{{ podcastAuthor }}

-

{{ title }}

-
+

{{ title }}

+

{{ $strings.MessageNoDescription }}

diff --git a/client/components/tables/ChaptersTable.vue b/client/components/tables/ChaptersTable.vue index 0dd9f2ab..2abe1607 100644 --- a/client/components/tables/ChaptersTable.vue +++ b/client/components/tables/ChaptersTable.vue @@ -21,7 +21,7 @@

{{ chapter.id }}

- + {{ chapter.title }} diff --git a/client/components/tables/podcast/DownloadQueueTable.vue b/client/components/tables/podcast/DownloadQueueTable.vue index 4b911229..04e631e2 100644 --- a/client/components/tables/podcast/DownloadQueueTable.vue +++ b/client/components/tables/podcast/DownloadQueueTable.vue @@ -30,7 +30,7 @@
- + {{ downloadQueued.episodeDisplayTitle }} diff --git a/client/components/tables/podcast/LazyEpisodeRow.vue b/client/components/tables/podcast/LazyEpisodeRow.vue index 18576340..0b32609b 100644 --- a/client/components/tables/podcast/LazyEpisodeRow.vue +++ b/client/components/tables/podcast/LazyEpisodeRow.vue @@ -2,7 +2,7 @@
-
+
{{ episodeTitle }}
diff --git a/client/components/ui/TextInput.vue b/client/components/ui/TextInput.vue index e06740ea..462118f0 100644 --- a/client/components/ui/TextInput.vue +++ b/client/components/ui/TextInput.vue @@ -1,6 +1,6 @@