Compare commits

...

79 Commits

Author SHA1 Message Date
advplyr
38f05a857f Version bump v2.20.0 2025-03-17 17:11:01 -05:00
mikiher
40504da4d7 Improve book library page query performance for author sort order (#4080)
* Add migration to create authorNames* columns, in libraryItems including update triggers and indices

* Add authorNames columns and indices to LibraryItem model

* Add database triggers for updating author names in libraryItems (for new databases)

* Populate authorNames during book scanning

* Update book sorting to use new authorNames columns

* Add an index on podcastEpisodes.publishedAt

* Fix group_concat order by and update to sqlite 3.44.2

---------

Co-authored-by: advplyr <advplyr@protonmail.com>
2025-03-17 17:09:49 -05:00
advplyr
bba09626a7 Merge pull request #4115 from weblate/weblate-audiobookshelf-abs-web-client
Translations update from Hosted Weblate
2025-03-17 17:07:30 -05:00
thehijacker
6c968bfca4 Translated using Weblate (Slovenian)
Currently translated at 100.0% (1090 of 1090 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sl/
2025-03-17 14:24:50 +01:00
J. Lavoie
8fa733e144 Translated using Weblate (French)
Currently translated at 99.3% (1083 of 1090 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/fr/
2025-03-17 09:04:44 +01:00
peter cerny
e76fbda9e0 Translated using Weblate (Slovak)
Currently translated at 8.3% (91 of 1090 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sk/
2025-03-17 00:02:33 +01:00
peter cerny
9fedab738f Translated using Weblate (Slovak)
Currently translated at 7.7% (84 of 1090 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sk/
2025-03-17 00:02:31 +01:00
peter cerny
5d8a88dc08 Translated using Weblate (Slovak)
Currently translated at 7.5% (82 of 1090 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sk/
2025-03-17 00:02:31 +01:00
SunSpring
23d20f4a5c Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (1090 of 1090 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/zh_Hans/
2025-03-17 00:02:30 +01:00
biuklija
3dc2022239 Translated using Weblate (Croatian)
Currently translated at 100.0% (1090 of 1090 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/hr/
2025-03-17 00:02:29 +01:00
advplyr
b2001eca23 Added translation using Weblate (Slovak) 2025-03-17 00:02:28 +01:00
Jan-Eric Myhrgren
0f7867a12a Translated using Weblate (Swedish)
Currently translated at 94.5% (1031 of 1090 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sv/
2025-03-17 00:02:27 +01:00
Максим Горпиніч
43706aac6d Translated using Weblate (Ukrainian)
Currently translated at 100.0% (1090 of 1090 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/uk/
2025-03-17 00:02:26 +01:00
Jan-Eric Myhrgren
5c7865f945 Translated using Weblate (Swedish)
Currently translated at 94.5% (1031 of 1090 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sv/
2025-03-17 00:02:24 +01:00
advplyr
7f8de7915c Update remove playlist translations and use our custom confirm modal 2025-03-16 18:02:16 -05:00
Gabriel Gavrilov
394bf8cb70 Allow number types for payload metadata when updating books. (#4118)
* Allow number types for payload metadata

* cast numbers to string
2025-03-16 08:42:18 -05:00
advplyr
3f6609ab1b Merge pull request #4119 from jfrazx/master
ci: update actions
2025-03-15 17:43:54 -05:00
advplyr
e29d3a3672 Cast OpenLibrary publishYear to string #4114 2025-03-15 17:41:07 -05:00
jfrazx
ecd782c8a9 fix: docker action 2025-03-15 00:49:27 -07:00
jfrazx
cb102deaed Merge pull request #1 from jfrazx/ci/update-actions
ci: update actions
2025-03-14 20:18:59 -07:00
jfrazx
9f883a5019 ci: update actions 2025-03-14 19:43:09 -07:00
advplyr
607f143861 Merge pull request #4113 from advplyr/parsing-opf-v3
Update opf parser to support refines meta elements
2025-03-14 17:39:20 -05:00
advplyr
804dafdfcb Add test for parseOpfMetadata OPF v3 author 2025-03-14 17:32:32 -05:00
advplyr
de22177dbf Update opf parser to support refines meta elements 2025-03-13 17:49:05 -05:00
advplyr
2bd46eb67b Fix conflicts 2025-03-13 17:15:30 -05:00
advplyr
9960986e6e Remove old unused i18n strings 2025-03-12 17:22:12 -05:00
ejlaner
759efc0d7d Translated using Weblate (Japanese)
Currently translated at 1.7% (19 of 1093 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/ja/
2025-03-12 23:20:27 +01:00
Xeratone
72f2712a5f Translated using Weblate (Japanese)
Currently translated at 0.2% (3 of 1093 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/ja/
2025-03-12 23:20:26 +01:00
Jan-Eric Myhrgren
1f609e023d Translated using Weblate (Swedish)
Currently translated at 94.5% (1033 of 1093 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sv/
2025-03-12 23:20:25 +01:00
Jan-Eric Myhrgren
d2f506eefe Translated using Weblate (Swedish)
Currently translated at 94.5% (1033 of 1093 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sv/
2025-03-12 23:20:24 +01:00
Ricky Tigg
78031b1a89 Translated using Weblate (Finnish)
Currently translated at 78.0% (853 of 1093 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/fi/
2025-03-12 23:20:23 +01:00
Ricky Tigg
c3ce084aac Translated using Weblate (Finnish)
Currently translated at 76.3% (835 of 1093 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/fi/
2025-03-12 23:20:23 +01:00
Jan-Eric Myhrgren
3d6e50a099 Translated using Weblate (Swedish)
Currently translated at 94.5% (1033 of 1093 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sv/
2025-03-12 23:20:22 +01:00
Максим Горпиніч
8820fac6a6 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (1093 of 1093 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/uk/
2025-03-12 23:20:21 +01:00
Jan Schoenfeld
d6950eab21 Translated using Weblate (German)
Currently translated at 100.0% (1093 of 1093 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/de/
2025-03-12 23:20:20 +01:00
Miró Allard
03f5038882 Translated using Weblate (Swedish)
Currently translated at 94.6% (1034 of 1093 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sv/
2025-03-12 23:20:19 +01:00
advplyr
2685d12ca3 Replace enable watcher setting strings, update enable automatic backups #4095 2025-03-12 17:20:11 -05:00
advplyr
e504bb09eb Merge pull request #4106 from Roukanken42/fix/loading-epub-covers
Fix: Load epub covers via cover-image property
2025-03-12 17:06:00 -05:00
advplyr
90d1aab1de Merge pull request #4097 from Vito0912/master
fix updating progress not updating finishedAt
2025-03-12 17:01:10 -05:00
advplyr
a3cd9e4440 Update confirm mark as finished to use translation #4017 2025-03-11 17:52:42 -05:00
Roukanken
b86797a245 Fix: Load epub covers via cover-image property 2025-03-11 21:05:21 +01:00
Vito0912
953f21ed53 fix updating progress not updating finishedAt 2025-03-10 13:58:52 +01:00
advplyr
ef77a88fce Merge pull request #4093 from gitting/master
Fix spelling
2025-03-09 17:09:54 -05:00
IUser
e7ca6a4ea9 Fix spelling 2025-03-09 14:01:53 -07:00
advplyr
e74b6982f9 Merge pull request #4046 from weblate/weblate-audiobookshelf-abs-web-client
Translations update from Hosted Weblate
2025-03-08 18:04:15 -06:00
Troj@
438364dafb Translated using Weblate (Belarusian)
Currently translated at 56.2% (615 of 1093 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/be/
2025-03-09 00:04:04 +00:00
Troj@
c8f79dca6c Translated using Weblate (Belarusian)
Currently translated at 53.0% (580 of 1093 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/be/
2025-03-09 00:04:04 +00:00
Phantomwise
9a2eb24d4b Translated using Weblate (French)
Currently translated at 99.7% (1090 of 1093 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/fr/
2025-03-09 00:04:03 +00:00
Andreas Morell-Reng
e5f7f0812e Translated using Weblate (Danish)
Currently translated at 100.0% (1093 of 1093 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/da/
2025-03-09 00:04:02 +00:00
Simple16
4705e73714 Translated using Weblate (Russian)
Currently translated at 100.0% (1093 of 1093 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/ru/
2025-03-09 00:04:01 +00:00
Troj@
738d936243 Translated using Weblate (Belarusian)
Currently translated at 51.6% (564 of 1093 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/be/
2025-03-09 00:03:59 +00:00
Troj@
507338d906 Translated using Weblate (Belarusian)
Currently translated at 50.9% (557 of 1093 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/be/
2025-03-09 00:03:59 +00:00
Troj@
776819ad52 Translated using Weblate (Belarusian)
Currently translated at 48.3% (529 of 1093 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/be/
2025-03-09 00:03:58 +00:00
Jan-Eric Myhrgren
f8af265440 Translated using Weblate (Swedish)
Currently translated at 94.6% (1034 of 1093 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sv/
2025-03-09 00:03:57 +00:00
Marcus skoding
d426ed101e Translated using Weblate (Swedish)
Currently translated at 94.2% (1030 of 1093 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sv/
2025-03-09 00:03:56 +00:00
Fredrik Lindqvist
e4eead75b1 Translated using Weblate (Swedish)
Currently translated at 94.2% (1030 of 1093 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sv/
2025-03-09 00:03:56 +00:00
Jan-Eric Myhrgren
4dd2f7cf18 Translated using Weblate (Swedish)
Currently translated at 94.2% (1030 of 1093 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sv/
2025-03-09 00:03:55 +00:00
Troj@
9c9c60a5bd Translated using Weblate (Belarusian)
Currently translated at 47.5% (520 of 1093 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/be/
2025-03-09 00:03:54 +00:00
Prashant Mhatre
4d88deabd2 Translated using Weblate (Hindi)
Currently translated at 8.3% (91 of 1093 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/hi/
2025-03-09 00:03:54 +00:00
Troj@
3a539a4dd3 Translated using Weblate (Belarusian)
Currently translated at 43.3% (474 of 1093 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/be/
2025-03-09 00:03:53 +00:00
phewi
a57addccae Translated using Weblate (Finnish)
Currently translated at 55.3% (605 of 1093 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/fi/
2025-03-09 00:03:52 +00:00
Troj@
a73372c51d Translated using Weblate (Belarusian)
Currently translated at 40.4% (442 of 1093 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/be/
2025-03-09 00:03:51 +00:00
thehijacker
dea457adcd Translated using Weblate (Slovenian)
Currently translated at 100.0% (1093 of 1093 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sl/
2025-03-09 00:03:50 +00:00
phewi
20e007ecd4 Translated using Weblate (Finnish)
Currently translated at 51.6% (565 of 1093 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/fi/
2025-03-09 00:03:49 +00:00
Ricky Tigg
4ee6b6d49b Translated using Weblate (Finnish)
Currently translated at 51.6% (565 of 1093 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/fi/
2025-03-09 00:03:48 +00:00
Jan-Eric Myhrgren
e5cb43bd75 Translated using Weblate (Swedish)
Currently translated at 92.6% (1013 of 1093 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sv/
2025-03-09 00:03:47 +00:00
Troj@
fb877779d1 Translated using Weblate (Belarusian)
Currently translated at 35.4% (388 of 1093 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/be/
2025-03-09 00:03:46 +00:00
Krissse10
abcc2eb22b Translated using Weblate (Swedish)
Currently translated at 92.6% (1013 of 1093 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sv/
2025-03-09 00:03:46 +00:00
Ranforingus
a21b6e1dec Translated using Weblate (Dutch)
Currently translated at 99.5% (1088 of 1093 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/nl/
2025-03-09 00:03:44 +00:00
advplyr
ddd8f15f2b Merge pull request #4088 from nichwall/checkRemoveAuthors_fix
Fix empty series delete check
2025-03-08 18:03:36 -06:00
advplyr
8f308c6180 Close RSS feeds after removing empty series 2025-03-08 17:47:47 -06:00
advplyr
e93b18745e Merge pull request #4089 from nichwall/logger_cleanup
Simplify log level determination
2025-03-08 17:25:42 -06:00
Nicholas Wallace
10acf28fa6 Simplify log level determination 2025-03-08 12:46:36 -07:00
Nicholas Wallace
84e20e041c Fix: empty series delete flakiness 2025-03-08 11:16:34 -07:00
Nicholas Wallace
167617cce0 Add: transaction to empty author remove 2025-03-08 10:43:27 -07:00
advplyr
d3fd19da65 Fixes for screen readers on podcast page and episodes table 2025-03-07 17:23:18 -06:00
advplyr
31be775c32 Merge pull request #4082 from mikiher/fix-lazy-episode-row-rtl
Fix RTL issue in LazyEpisodeRow
2025-03-07 16:56:18 -06:00
mikiher
81cd6f6c7d Fix RTL issue in LazyEpisodeRow 2025-03-07 21:14:50 +02:00
advplyr
4fdb37c9dc Merge pull request #4078 from advplyr/validate_migration_files
Update migration manager to validate migration files #4042
2025-03-06 17:35:12 -06:00
71 changed files with 2239 additions and 577 deletions

View File

@@ -14,11 +14,11 @@ jobs:
steps:
- name: Check issue headings
uses: actions/github-script@v6
uses: actions/github-script@v7
with:
script: |
const issueBody = context.payload.issue.body || "";
// Match Markdown headings (e.g., # Heading, ## Heading)
const headingRegex = /^(#{1,6})\s.+/gm;
const headings = [...issueBody.matchAll(headingRegex)];
@@ -39,4 +39,4 @@ jobs:
issue_number: context.payload.issue.number,
state: "closed"
});
}
}

View File

@@ -43,7 +43,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v3
uses: actions/checkout@v4
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL

View File

@@ -1,5 +1,4 @@
---
name: Build and Push Docker Image
on:
@@ -11,7 +10,7 @@ on:
required: true
default: 'latest'
push:
branches: [main,master]
branches: [main, master]
tags:
- 'v*.*.*'
# Only build when files in these directories have been changed
@@ -23,16 +22,16 @@ on:
jobs:
build:
if: "!contains(github.event.head_commit.message, 'skip ci')"
if: ${{ !contains(github.event.head_commit.message, 'skip ci') && github.repository == 'advplyr/audiobookshelf' }}
runs-on: ubuntu-20.04
steps:
- name: Check out
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Docker meta
id: meta
uses: docker/metadata-action@v4
uses: docker/metadata-action@v5
with:
images: advplyr/audiobookshelf,ghcr.io/${{ github.repository_owner }}/audiobookshelf
tags: |
@@ -40,13 +39,13 @@ jobs:
type=semver,pattern={{version}}
- name: Setup QEMU
uses: docker/setup-qemu-action@v2
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
uses: docker/setup-buildx-action@v3
- name: Cache Docker layers
uses: actions/cache@v3
uses: actions/cache@v4
with:
path: /tmp/.buildx-cache
key: ${{ runner.os }}-buildx-${{ github.sha }}
@@ -54,20 +53,20 @@ jobs:
${{ runner.os }}-buildx-
- name: Login to Dockerhub
uses: docker/login-action@v2
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_PASSWORD }}
- name: Login to ghcr
uses: docker/login-action@v2
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GHCR_PASSWORD }}
- name: Build image
uses: docker/build-push-action@v3
uses: docker/build-push-action@v6
with:
tags: ${{ github.event.inputs.tags || steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}

View File

@@ -20,7 +20,8 @@ jobs:
- name: Set up node
uses: actions/setup-node@v4
with:
node-version: '20'
node-version: 20
cache: 'npm'
# The only argument is the `directory`, which is where the i18n files are
# stored.

View File

@@ -18,14 +18,15 @@ jobs:
name: build and test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: setup nade
uses: actions/setup-node@v3
- name: setup node
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- name: install pkg (using yao-pkg fork for targetting node20)
- name: install pkg (using yao-pkg fork for targeting node20)
run: npm install -g @yao-pkg/pkg
- name: get client dependencies

View File

@@ -18,15 +18,22 @@ jobs:
# Check out the repository
- name: Checkout
uses: actions/checkout@v4
# Set up node to run the javascript
- name: Set up node
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
# Install Redocly CLI
- name: Install Redocly CLI
run: npm install -g @redocly/cli@latest
# Perform linting for exploded spec
- name: Run linting for exploded spec
run: redocly lint docs/root.yaml --format=github-actions
# Perform linting for bundled spec
- name: Run linting for bundled spec
run: redocly lint docs/openapi.json --format=github-actions

View File

@@ -29,6 +29,7 @@ jobs:
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- name: Install dependencies
run: npm ci

View File

@@ -11,7 +11,7 @@
</template>
</div>
</div>
<div v-if="publishedYear" class="flex py-0.5">
<div v-if="publishedYear" role="paragraph" class="flex py-0.5">
<div class="w-24 min-w-24 sm:w-32 sm:min-w-32">
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelPublishYear }}</span>
</div>
@@ -19,7 +19,7 @@
{{ publishedYear }}
</div>
</div>
<div v-if="publisher" class="flex py-0.5">
<div v-if="publisher" role="paragraph" class="flex py-0.5">
<div class="w-24 min-w-24 sm:w-32 sm:min-w-32">
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelPublisher }}</span>
</div>
@@ -27,7 +27,7 @@
<nuxt-link :to="`/library/${libraryId}/bookshelf?filter=publishers.${$encode(publisher)}`" class="hover:underline">{{ publisher }}</nuxt-link>
</div>
</div>
<div v-if="podcastType" class="flex py-0.5">
<div v-if="podcastType" role="paragraph" class="flex py-0.5">
<div class="w-24 min-w-24 sm:w-32 sm:min-w-32">
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelPodcastType }}</span>
</div>
@@ -65,7 +65,7 @@
<nuxt-link :to="`/library/${libraryId}/bookshelf?filter=languages.${$encode(language)}`" class="hover:underline">{{ language }}</nuxt-link>
</div>
</div>
<div v-if="tracks.length || (isPodcast && totalPodcastDuration)" class="flex py-0.5">
<div v-if="tracks.length || (isPodcast && totalPodcastDuration)" role="paragraph" class="flex py-0.5">
<div class="w-24 min-w-24 sm:w-32 sm:min-w-32">
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelDuration }}</span>
</div>
@@ -73,7 +73,7 @@
{{ durationPretty }}
</div>
</div>
<div class="flex py-0.5">
<div role="paragraph" class="flex py-0.5">
<div class="w-24 min-w-24 sm:w-32 sm:min-w-32">
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelSize }}</span>
</div>

View File

@@ -1,23 +1,25 @@
<template>
<div ref="wrapper" class="relative" v-click-outside="clickOutside">
<button type="button" class="relative w-full h-full border border-gray-500 hover:border-gray-400 rounded shadow-sm pl-3 pr-3 py-0 text-left focus:outline-none cursor-pointer" aria-haspopup="listbox" aria-expanded="true" aria-labelledby="listbox-label" @click.prevent="showMenu = !showMenu">
<span class="flex items-center justify-between">
<span class="block truncate text-xs">{{ selectedText }}</span>
</span>
<div class="relative h-9">
<button type="button" class="relative w-full h-full border border-gray-500 hover:border-gray-400 rounded shadow-sm pl-3 pr-3 py-0 text-left focus:outline-none cursor-pointer" aria-haspopup="menu" :aria-expanded="showMenu" @click.prevent="showMenu = !showMenu">
<span class="flex items-center justify-between">
<span class="block truncate text-xs">{{ selectedText }}</span>
</span>
</button>
<span v-if="selected === 'all'" class="ml-3 absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
<svg class="h-5 w-5 text-gray-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path fill-rule="evenodd" d="M10 3a1 1 0 01.707.293l3 3a1 1 0 01-1.414 1.414L10 5.414 7.707 7.707a1 1 0 01-1.414-1.414l3-3A1 1 0 0110 3zm-3.707 9.293a1 1 0 011.414 0L10 14.586l2.293-2.293a1 1 0 011.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z" clip-rule="evenodd" />
</svg>
</span>
<div v-else class="ml-3 absolute inset-y-0 right-0 flex items-center pr-2 cursor-pointer text-gray-400 hover:text-gray-300" @mousedown.stop @mouseup.stop @click.stop.prevent="clearSelected">
<button v-else type="button" :aria-label="$strings.ButtonClearFilter" class="ml-3 absolute inset-y-0 right-0 flex items-center pr-2 cursor-pointer text-gray-400 hover:text-gray-300" @mousedown.stop @mouseup.stop @click.stop.prevent="clearSelected">
<span class="material-symbols" style="font-size: 1.1rem">close</span>
</div>
</button>
</button>
</div>
<div v-show="showMenu" class="absolute z-10 mt-1 w-full bg-bg border border-black-200 shadow-lg max-h-96 rounded-md py-1 text-sm ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none">
<ul class="h-full w-full" role="listbox" aria-labelledby="listbox-label">
<ul class="h-full w-full" role="menu">
<template v-for="item in items">
<li :key="item.value" class="select-none relative py-2 pr-9 cursor-pointer hover:bg-white/5" :class="item.value === selected ? 'bg-white/5 text-yellow-400' : 'text-gray-200 hover:text-white'" role="option" @click="clickedOption(item)">
<li :key="item.value" class="select-none relative py-2 pr-9 cursor-pointer hover:bg-white/5" :class="item.value === selected ? 'bg-white/5 text-yellow-400' : 'text-gray-200 hover:text-white'" role="menuitem" @click="clickedOption(item)">
<div class="flex items-center justify-between">
<span class="font-normal ml-3 block truncate">{{ item.text }}</span>
</div>
@@ -86,4 +88,4 @@ export default {
}
}
}
</script>
</script>

View File

@@ -74,21 +74,32 @@ export default {
this.newPlaylistDescription = this.playlist.description || ''
},
removeClick() {
if (confirm(this.$getString('MessageConfirmRemovePlaylist', [this.playlistName]))) {
this.processing = true
this.$axios
.$delete(`/api/playlists/${this.playlist.id}`)
.then(() => {
this.processing = false
this.show = false
this.$toast.success(this.$strings.ToastPlaylistRemoveSuccess)
})
.catch((error) => {
console.error('Failed to remove playlist', error)
this.processing = false
this.$toast.error(this.$strings.ToastRemoveFailed)
})
const payload = {
message: this.$getString('MessageConfirmRemovePlaylist', [this.playlistName]),
callback: (confirmed) => {
if (confirmed) {
this.removePlaylist()
}
},
type: 'yesNo'
}
this.$store.commit('globals/setConfirmPrompt', payload)
},
removePlaylist() {
this.processing = true
this.$axios
.$delete(`/api/playlists/${this.playlist.id}`)
.then(() => {
this.show = false
this.$toast.success(this.$strings.ToastPlaylistRemoveSuccess)
})
.catch((error) => {
console.error('Failed to remove playlist', error)
this.$toast.error(this.$strings.ToastRemoveFailed)
})
.finally(() => {
this.processing = false
})
},
submitForm() {
if (this.newPlaylistName === this.playlistName && this.newPlaylistDescription === this.playlist.description) {

View File

@@ -8,7 +8,7 @@
</div>
<div class="h-10 flex items-center mt-1.5 mb-0.5 overflow-hidden">
<p class="text-sm text-gray-200 line-clamp-2" v-html="episodeSubtitle"></p>
<div dir="auto" class="text-sm text-gray-200 line-clamp-2" v-html="episodeSubtitle"></div>
</div>
<div class="h-8 flex items-center">
@@ -26,12 +26,13 @@
<div class="flex items-center pt-2">
<button class="h-8 px-4 border border-white border-opacity-20 hover:bg-white hover:bg-opacity-10 rounded-full flex items-center justify-center cursor-pointer focus:outline-none" :class="userIsFinished ? 'text-white text-opacity-40' : ''" @click.stop="playClick">
<span class="material-symbols fill text-2xl" :class="streamIsPlaying ? '' : 'text-success'">{{ streamIsPlaying ? 'pause' : 'play_arrow' }}</span>
<p class="pl-2 pr-1 text-sm font-semibold">{{ timeRemaining }}</p>
<span class="material-symbols fill text-2xl" aria-hidden="true" :class="streamIsPlaying ? '' : 'text-success'">{{ streamIsPlaying ? 'pause' : 'play_arrow' }}</span>
<span class="sr-only">{{ streamIsPlaying ? $strings.ButtonPause : $strings.ButtonPlay }}</span>
<p class="pl-2 pr-1 text-sm font-semibold" aria-hidden="true">{{ timeRemaining }}</p>
</button>
<ui-tooltip v-if="libraryItemIdStreaming && !isStreamingFromDifferentLibrary" :text="isQueued ? $strings.MessageRemoveFromPlayerQueue : $strings.MessageAddToPlayerQueue" :class="isQueued ? 'text-success' : ''" direction="top">
<ui-icon-btn :icon="isQueued ? 'playlist_add_check' : 'playlist_play'" borderless @click="queueBtnClick" />
<ui-icon-btn :icon="isQueued ? 'playlist_add_check' : 'playlist_play'" :aria-label="isQueued ? $strings.LabelRemoveFromPlayerQueue : $strings.LabelAddToPlayerQueue" borderless @click="queueBtnClick" />
</ui-tooltip>
<ui-tooltip :text="userIsFinished ? $strings.MessageMarkAsNotFinished : $strings.MessageMarkAsFinished" direction="top">
@@ -39,11 +40,11 @@
</ui-tooltip>
<ui-tooltip :text="$strings.LabelYourPlaylists" direction="top">
<ui-icon-btn icon="playlist_add" borderless @click="clickAddToPlaylist" />
<ui-icon-btn icon="playlist_add" :aria-label="$strings.LabelYourPlaylists" borderless @click="clickAddToPlaylist" />
</ui-tooltip>
<ui-icon-btn v-if="userCanUpdate" icon="edit" borderless @click="clickEdit" />
<ui-icon-btn v-if="userCanDelete" icon="close" borderless @click="removeClick" />
<ui-icon-btn v-if="userCanDelete" icon="close" :aria-label="$strings.HeaderRemoveEpisode" borderless @click="removeClick" />
</div>
</div>
<div v-if="isHovering || isSelected || isSelectionMode" class="hidden md:block w-12 min-w-12" />
@@ -53,7 +54,7 @@
<div class="hidden md:block md:w-12 md:min-w-12 md:-right-0 md:absolute md:top-0 h-full transform transition-transform z-20" :class="!isHovering && !isSelected && !isSelectionMode ? 'translate-x-24' : 'translate-x-0'">
<div class="flex h-full items-center">
<div class="mx-1">
<ui-checkbox v-model="isSelected" @input="selectedUpdated" checkbox-bg="bg" />
<ui-checkbox v-model="isSelected" @input="selectedUpdated" checkbox-bg="bg" aria-label="Select episode" />
</div>
</div>
</div>

View File

@@ -1,7 +1,7 @@
<template>
<label class="flex justify-start items-center" :class="!disabled ? 'cursor-pointer' : ''">
<div class="border-2 rounded flex flex-shrink-0 justify-center items-center" :class="wrapperClass">
<input v-model="selected" :disabled="disabled" type="checkbox" class="opacity-0 absolute" :class="!disabled ? 'cursor-pointer' : ''" />
<input v-model="selected" :disabled="disabled" type="checkbox" :aria-label="ariaLabel" class="opacity-0 absolute" :class="!disabled ? 'cursor-pointer' : ''" />
<span v-if="partial" class="material-symbols text-base leading-none text-gray-400">remove</span>
<svg v-else-if="selected" class="fill-current pointer-events-none" :class="svgClass" viewBox="0 0 20 20"><path d="M0 11l2-2 5 5L18 3l2 2L7 18z" /></svg>
</div>
@@ -33,7 +33,11 @@ export default {
default: ''
},
disabled: Boolean,
partial: Boolean
partial: Boolean,
ariaLabel: {
type: String,
default: ''
}
},
data() {
return {}
@@ -75,4 +79,4 @@ export default {
methods: {},
mounted() {}
}
</script>
</script>

View File

@@ -1,12 +1,12 @@
{
"name": "audiobookshelf-client",
"version": "2.19.5",
"version": "2.20.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "audiobookshelf-client",
"version": "2.19.5",
"version": "2.20.0",
"license": "ISC",
"dependencies": {
"@nuxtjs/axios": "^5.13.6",

View File

@@ -1,6 +1,6 @@
{
"name": "audiobookshelf-client",
"version": "2.19.5",
"version": "2.20.0",
"buildNumber": 1,
"description": "Self-hosted audiobook and podcast client",
"main": "index.js",

View File

@@ -123,7 +123,7 @@
</div>
<div class="my-4 w-full">
<div ref="description" id="item-description" dir="auto" class="default-style less-spacing text-base text-gray-100 whitespace-pre-line mb-1" :class="{ 'show-full': showFullDescription }" v-html="description" />
<div ref="description" id="item-description" dir="auto" role="paragraph" class="default-style less-spacing text-base text-gray-100 whitespace-pre-line mb-1" :class="{ 'show-full': showFullDescription }" v-html="description" />
<button v-if="isDescriptionClamped" class="py-0.5 flex items-center text-slate-300 hover:text-white" @click="showFullDescription = !showFullDescription">{{ showFullDescription ? $strings.ButtonReadLess : $strings.ButtonReadMore }} <span class="material-symbols text-xl pl-1" v-html="showFullDescription ? 'expand_less' : '&#xe313;'" /></button>
</div>
@@ -503,7 +503,7 @@ export default {
toggleFinished(confirmed = false) {
if (!this.userIsFinished && this.progressPercent > 0 && !confirmed) {
const payload = {
message: `Are you sure you want to mark "${this.title}" as finished?`,
message: this.$getString('MessageConfirmMarkItemFinished', [this.title]),
callback: (confirmed) => {
if (confirmed) {
this.toggleFinished(true)

View File

@@ -155,7 +155,7 @@ export default {
const itemProgressPercent = episode.progress?.progress || 0
if (!isFinished && itemProgressPercent > 0 && !confirmed) {
const payload = {
message: `Are you sure you want to mark "${episode.title}" as finished?`,
message: this.$getString('MessageConfirmMarkItemFinished', [episode.title]),
callback: (confirmed) => {
if (confirmed) {
this.toggleEpisodeFinished(episode, true)

View File

@@ -109,21 +109,31 @@ export default {
this.$store.commit('globals/setEditPlaylist', this.playlist)
},
removeClick() {
if (confirm(`Are you sure you want to remove playlist "${this.playlistName}"?`)) {
this.processingRemove = true
var playlistName = this.playlistName
this.$axios
.$delete(`/api/playlists/${this.playlist.id}`)
.then(() => {
this.processingRemove = false
this.$toast.success(`Playlist "${playlistName}" Removed`)
})
.catch((error) => {
console.error('Failed to remove playlist', error)
this.processingRemove = false
this.$toast.error(`Failed to remove playlist`)
})
const payload = {
message: this.$getString('MessageConfirmRemovePlaylist', [this.playlistName]),
callback: (confirmed) => {
if (confirmed) {
this.removePlaylist()
}
},
type: 'yesNo'
}
this.$store.commit('globals/setConfirmPrompt', payload)
},
removePlaylist() {
this.processingRemove = true
this.$axios
.$delete(`/api/playlists/${this.playlist.id}`)
.then(() => {
this.$toast.success(this.$strings.ToastPlaylistRemoveSuccess)
})
.catch((error) => {
console.error('Failed to remove playlist', error)
this.$toast.error(this.$strings.ToastRemoveFailed)
})
.finally(() => {
this.processingRemove = false
})
},
clickPlay() {
const queueItems = []

View File

@@ -11,9 +11,10 @@
"ButtonAuthors": "Аўтары",
"ButtonBack": "Назад",
"ButtonBatchEditPopulateFromExisting": "Запоўніць з існуючага",
"ButtonBatchEditPopulateMapDetails": "Запоўніць падрабязнасці карты",
"ButtonBrowseForFolder": "Знайсці тэчку",
"ButtonCancel": "Адмяніць",
"ButtonCancelEncode": "Адмяніць кадзіраванне",
"ButtonCancel": "Скасаваць",
"ButtonCancelEncode": "Скасаваць кадзіраванне",
"ButtonChangeRootPassword": "Зменіце Root пароль",
"ButtonCheckAndDownloadNewEpisodes": "Праверыць і спампаваць новыя эпізоды",
"ButtonChooseAFolder": "Выбраць тэчку",
@@ -59,7 +60,7 @@
"ButtonPlay": "Прайграць",
"ButtonPlayAll": "Прайграць усё",
"ButtonPlaying": "Прайграваецца",
"ButtonPlaylists": "Плэйлісты",
"ButtonPlaylists": "Спісы прайгравання",
"ButtonPrevious": "Папярэдні",
"ButtonPreviousChapter": "Папярэдні раздзел",
"ButtonProbeAudioFile": "Праверыць аўдыяфайл",
@@ -72,6 +73,8 @@
"ButtonQuickMatch": "Хуткі пошук",
"ButtonReScan": "Паўторнае сканаванне",
"ButtonRead": "Чытаць",
"ButtonReadLess": "Чытаць менш",
"ButtonReadMore": "Чытаць больш",
"ButtonRefresh": "Абнавіць",
"ButtonRemove": "Выдаліць",
"ButtonRemoveAll": "Выдаліць усе",
@@ -94,6 +97,7 @@
"ButtonSeries": "Серыі",
"ButtonSetChaptersFromTracks": "Усталяваць раздзелы з трэкаў",
"ButtonShare": "Падзяліцца",
"ButtonShiftTimes": "Карэкцыя часу",
"ButtonStartM4BEncode": "Пачаць кадзіраванне ў M4B",
"ButtonStartMetadataEmbed": "Пачаць убудаванне метаданых",
"ButtonStats": "Статыстыка",
@@ -153,22 +157,36 @@
"HeaderManageGenres": "Кіраванне жанрамі",
"HeaderManageTags": "Кіраванне тэгамі",
"HeaderMapDetails": "Падрабязнасці адлюстравання",
"HeaderMetadataOrderOfPrecedence": "Парадак прыярытэтнасці метададзеных",
"HeaderMetadataToEmbed": "Метададзеныя для ўбудавання",
"HeaderNewAccount": "Новы ўліковы запіс",
"HeaderNewLibrary": "Новая бібліятэка",
"HeaderNotificationCreate": "Стварыць апавяшчэнне",
"HeaderNotificationUpdate": "Абнавіць апавяшчэнне",
"HeaderNotifications": "Апавяшчэнні",
"HeaderOpenIDConnectAuthentication": "Аўтэнтыфікацыя праз OpenID Connect",
"HeaderOpenListeningSessions": "Адкрыць сеансы праслухоўвання",
"HeaderOpenRSSFeed": "Адкрыць RSS-стужку",
"HeaderPlaylist": "Плэйліст",
"HeaderPlaylistItems": "Элементы плэйліста",
"HeaderOtherFiles": "Іншыя файлы",
"HeaderPasswordAuthentication": "Аўтэнтыфікацыя паролем",
"HeaderPermissions": "Дазволы",
"HeaderPlayerQueue": "Чарга прайгравання",
"HeaderPlayerSettings": "Налады прайгравальніка",
"HeaderPlaylist": "Спіс прайгравання",
"HeaderPlaylistItems": "Элементы спіса прайгравання",
"HeaderPodcastsToAdd": "Падкасты для дадання",
"HeaderPreviewCover": "Прадпрагляд вокладкі",
"HeaderRSSFeedGeneral": "Падрабязнасці RSS",
"HeaderRSSFeedIsOpen": "RSS-стужка адкрыта",
"HeaderRSSFeeds": "RSS-стужкі",
"HeaderRemoveEpisode": "Выдаліць эпізод",
"HeaderRemoveEpisodes": "Выдаліць {0} эпізодаў",
"HeaderSavedMediaProgress": "Захаваны прагрэс медыя",
"HeaderSchedule": "Расклад",
"HeaderScheduleEpisodeDownloads": "Расклад аўтаматычных спамповак эпізодаў",
"HeaderScheduleLibraryScans": "Расклад аўтаматычнага сканавання бібліятэкі",
"HeaderSession": "Сеанс",
"HeaderSetBackupSchedule": "Наладзіць расклад рэзервовага капіравання",
"HeaderSettings": "Налады",
"HeaderSettingsDisplay": "Дысплей",
"HeaderSettingsExperimental": "Эксперыментальныя функцыі",
@@ -176,20 +194,45 @@
"HeaderSettingsScanner": "Сканер",
"HeaderSettingsWebClient": "Вэб-кліент",
"HeaderSleepTimer": "Таймер сну",
"HeaderStatsMinutesListeningChart": "Хвіліны праслухоўвання (апошнія 7 дзён)",
"HeaderStatsLargestItems": "Найбуйнейшыя элементы",
"HeaderStatsLongestItems": "Найдаўжэйшыя элементы (гадзіны)",
"HeaderStatsMinutesListeningChart": "Хвілін праслухоўвання (апошнія 7 дзён)",
"HeaderStatsRecentSessions": "Апошнія сеансы",
"HeaderStatsTop10Authors": "10 лепшых аўтараў",
"HeaderStatsTop5Genres": "5 лепшых жанраў",
"HeaderTableOfContents": "Змест",
"HeaderTools": "Інструменты",
"HeaderUpdateAccount": "Абнавіць уліковы запіс",
"HeaderUpdateAuthor": "Абнавіць аўтара",
"HeaderUpdateDetails": "Абнавіць падрабязнасці",
"HeaderUpdateLibrary": "Абнавіць бібліятэку",
"HeaderUsers": "Карыстальнікі",
"HeaderYearReview": "Вынікі {0} года",
"HeaderYourStats": "Ваша статыстыка",
"LabelAbridged": "Скарочаная версія",
"LabelAbridgedChecked": "Скарочаная версія (праверана)",
"LabelAbridgedUnchecked": "Поўная версія (неправерана)",
"LabelAccessibleBy": "Даступна для",
"LabelAccountType": "Тып уліковага запіса",
"LabelAccountTypeAdmin": "Адміністратар",
"LabelAccountTypeGuest": "Госць",
"LabelAccountTypeUser": "Карыстальнік",
"LabelAddToPlaylist": "Дадаць у плэйліст",
"LabelActivities": "Дзеянні",
"LabelActivity": "Дзеянне",
"LabelAddToCollection": "Дадаць у калекцыю",
"LabelAddToCollectionBatch": "Дадаць {0} кніг у калекцыю",
"LabelAddToPlaylist": "Дадаць у спіс прайгравання",
"LabelAddToPlaylistBatch": "Дадаць {0} элементаў у спіс прайгравання",
"LabelAddedAt": "Дата дабаўлення",
"LabelAddedDate": "Дададзена {0}",
"LabelAdminUsersOnly": "Толькі для адміністратараў",
"LabelAll": "Усе",
"LabelAllUsers": "Усе карыстальнікі",
"LabelAllUsersExcludingGuests": "Усе карыстальнікі, акрамя гасцей",
"LabelAllUsersIncludingGuests": "Усе карыстальнікі, уключаючы гасцей",
"LabelAlreadyInYourLibrary": "Ужо ў вашай бібліятэцы",
"LabelApiToken": "Токен API",
"LabelAppend": "Дадаць",
"LabelAudioBitrate": "Бітрэйт аўдыё (напрыклад, 128к)",
"LabelAudioChannels": "Аўдыёканалы (1 або 2)",
"LabelAudioCodec": "Аўдыёкодэк",
@@ -198,7 +241,10 @@
"LabelAuthorLastFirst": "Аўтар (Прозвішча, Імя)",
"LabelAuthors": "Аўтары",
"LabelAutoDownloadEpisodes": "Аўтаматычнае спампаванне эпізодаў",
"LabelAutoFetchMetadata": "Аўтаматычнае атрыманне метададзеных",
"LabelBackupAudioFiles": "Рэзервовае капіраванне аўдыёфайлаў",
"LabelBackupLocation": "Месцазнаходжанне рэзервовых копій",
"LabelBackupsNumberToKeepHelp": "Адначасова будзе выдаляцца толькі 1 рэзервовая копія, таму, калі ў вас іх больш, вам варта выдаліць іх уручную.",
"LabelBooks": "Кнігі",
"LabelChapters": "Раздзелы",
"LabelClosePlayer": "Зачыніць прайгравальнік",
@@ -207,7 +253,9 @@
"LabelContinueListening": "Працягваць слухаць",
"LabelContinueReading": "Працягнуць чытанне",
"LabelContinueSeries": "Працягнуць серыі",
"LabelDatetime": "Дата і час",
"LabelDescription": "Апісанне",
"LabelDiscFromFilename": "Дыск з імя файла",
"LabelDiscover": "Знайсці",
"LabelDownload": "Спампаваць",
"LabelDownloadNEpisodes": "Спампована {0} эпізодаў",
@@ -220,6 +268,7 @@
"LabelEncodingChaptersNotEmbedded": "Раздзелы не ўбудаваны ў шматдарожкавыя аўдыякнігі.",
"LabelEncodingFinishedM4B": "Гатовы файл M4B будзе змешчаны ў вашу тэчку з аўдыякнігамі па адрасе:",
"LabelEncodingInfoEmbedded": "Метаданыя будуць убудаваны ў аўдыядарожкі ўнутры вашай тэчкі з аўдыякнігамі.",
"LabelEncodingTimeWarning": "Кадаванне можа заняць да 30 хвілін.",
"LabelEnd": "Канец",
"LabelEndOfChapter": "Канец раздзела",
"LabelEpisode": "Эпізод",
@@ -238,17 +287,45 @@
"LabelGenres": "Жанры",
"LabelHasEbook": "Мае электронную кнігу",
"LabelHasSupplementaryEbook": "Мае дадатковую электронную кнігу",
"LabelHideSubtitles": "Схаваць падзагалоўкі",
"LabelHost": "Хост",
"LabelInProgress": "У працэсе",
"LabelIncomplete": "Незавершана",
"LabelIntervalCustomDailyWeekly": "Карыстальніцкі штодзённы/штотыднёвы",
"LabelIntervalEvery12Hours": "Кожныя 12 гадзін",
"LabelIntervalEvery15Minutes": "Кожныя 15 хвілін",
"LabelIntervalEvery2Hours": "Кожныя 2 гадзіны",
"LabelIntervalEvery30Minutes": "Кожныя 30 хвілін",
"LabelIntervalEvery6Hours": "Кожныя 6 гадзін",
"LabelIntervalEveryDay": "Кожны дзень",
"LabelIntervalEveryHour": "Кожную гадзіну",
"LabelIntervalEveryMinute": "Кожную хвіліну",
"LabelInvert": "Інвертаваць",
"LabelItem": "Элемент",
"LabelLanguage": "Мова",
"LabelLanguageDefaultServer": "Мова сервера па змаўчанні",
"LabelLanguages": "Мовы",
"LabelLastBookAdded": "Апошняя дададзеная кніга",
"LabelLastBookUpdated": "Апошняя абноўленая кніга",
"LabelLastSeen": "Апошні прагляд",
"LabelLastTime": "Апошні раз",
"LabelLastUpdate": "Апошняе абнаўленне",
"LabelLayout": "Знешні выгляд",
"LabelLayoutSinglePage": "Аднабаковы",
"LabelLayoutSplitPage": "Падзяліць старонку",
"LabelLess": "Менш",
"LabelLibrariesAccessibleToUser": "Бібліятэкі, даступныя карыстальніку",
"LabelLibrary": "Бібліятэка",
"LabelLibraryFilterSublistEmpty": "Не {0}",
"LabelLibraryItem": "Элемент бібліятэкі",
"LabelLibraryName": "Імя бібліятэкі",
"LabelLimit": "Абмежаванне",
"LabelLineSpacing": "Міжрадковы інтэрвал",
"LabelListenAgain": "Паслухаць зноў",
"LabelMaxEpisodesToDownload": "Максімальная колькасць эпізодаў для спампоўкі. Выкарыстоўвайце 0 для неабмежаванай колькасці.",
"LabelMaxEpisodesToDownloadPerCheck": "Максімальная колькасць новых эпізодаў для спампоўкі за праверку",
"LabelMaxEpisodesToKeepHelp": "Значэнне 0 не ўстанаўлівае максімальнага абмежавання. Пасля аўтаматычнай спампоўкі новага эпізоду будзе выдалены самы стары эпізод, калі ў вас больш за X эпізодаў. Пры кожнай новай спампоўцы будзе выдаляцца толькі 1 эпізод.",
"LabelMediaPlayer": "Медыяплэер",
"LabelMediaPlayer": "Медыяпрайгравальнік",
"LabelMediaType": "Тып медыя",
"LabelMissing": "Адсутнічае",
"LabelMore": "Больш",
@@ -256,41 +333,220 @@
"LabelName": "Імя",
"LabelNarrator": "Чытальнік",
"LabelNarrators": "Чытальнікі",
"LabelNewestAuthors": "Новыя аўтары",
"LabelNewestEpisodes": "Новыя эпізоды",
"LabelNotFinished": "Не скончана",
"LabelNotStarted": "Не пачата",
"LabelNotificationsMaxFailedAttemptsHelp": "Апавяшчэнні адключаюцца пасля таго, як не ўдаецца іх адправіць гэтулькі разоў",
"LabelNumberOfEpisodes": "# з эпізодаў",
"LabelOpenRSSFeed": "Адкрыць RSS-стужку",
"LabelPassword": "Пароль",
"LabelPath": "Шлях",
"LabelPermissionsDownload": "Можна спампаваць",
"LabelPlaylists": "Cпісs прайгравання",
"LabelPodcast": "Падкаст",
"LabelPodcasts": "Падкасты",
"LabelPreventIndexing": "Прадухіліць індэксацыю вашай стужкі каталогамі падкастаў iTunes і Google",
"LabelProgress": "Прагрэс",
"LabelPubDate": "Дата публікацыі",
"LabelPublishYear": "Год публікацыі",
"LabelPublishedDate": "Апублікавана {0}",
"LabelRSSFeedCustomOwnerEmail": "Карыстальніцкая электронная пошта ўладальніка",
"LabelRSSFeedCustomOwnerName": "Карыстальніцкае імя ўладальніка",
"LabelRSSFeedOpen": "RSS-стужка адкрытая",
"LabelRSSFeedPreventIndexing": "Прадухіліць індэксацыю",
"LabelRSSFeedSlug": "Ідэнтыфікатар RSS-стужкі",
"LabelRSSFeedURL": "URL RSS-стужкі",
"LabelRandomly": "Выпадкова",
"LabelReAddSeriesToContinueListening": "Дадаць серыю зноў у Працягваць слухаць",
"LabelRead": "Чытаць",
"LabelReadAgain": "Чытаць зноў",
"LabelRecentSeries": "Апошнія серыі",
"LabelRecentlyAdded": "Нядаўна дададзеныя",
"LabelRemoveAllMetadataAbs": "Выдаліць усе файлы metadata.abs",
"LabelRemoveAllMetadataJson": "Выдаліць усе файлы metadata.json",
"LabelRemoveCover": "Выдаліць вокладку",
"LabelRemoveMetadataFile": "Выдаліць файлы метададзеных у тэчках элементаў бібліятэкі",
"LabelRemoveMetadataFileHelp": "Выдаліць усе файлы metadata.json і metadata.abs у вашых {0} тэчках.",
"LabelRowsPerPage": "Радкоў на старонку",
"LabelSearchTerm": "Пошукавы запыт",
"LabelSearchTitle": "Пошук па загалоўку",
"LabelSearchTitleOrASIN": "Пошук па загалоўку або ASIN",
"LabelSeason": "Сезон",
"LabelSeasonNumber": "Сезон #{0}",
"LabelSelectAll": "Выбраць усё",
"LabelSelectAllEpisodes": "Выбраць усе эпізоды",
"LabelSelectEpisodesShowing": "Выбраць {0} эпізодаў для паказу",
"LabelSelectUsers": "Выбраць карыстальнікаў",
"LabelSendEbookToDevice": "Адправіць электронную кнігу на...",
"LabelSequence": "Паслядоўнасць",
"LabelSerial": "Серыйны",
"LabelSeries": "Серыі",
"LabelSeriesName": "Назва серыі",
"LabelSeriesProgress": "Прагрэс серыі",
"LabelServerLogLevel": "Узровень журнала сервера",
"LabelServerYearReview": "Вынікі года сервера ({0})",
"LabelSetEbookAsPrimary": "Зрабіць асноўным",
"LabelSetEbookAsSupplementary": "Зрабіць дадатковым",
"LabelSettingsAllowIframe": "Дазволіць убудоўванне ў iframe",
"LabelSettingsAudiobooksOnly": "Толькі аўдыякнігі",
"LabelSettingsAudiobooksOnlyHelp": "Уключэнне гэтай налады будзе ігнараваць файлы электронных кніг, калі толькі яны не знаходзяцца ў тэчцы з аўдыякнігамі. У такім выпадку яны будуць пазначаны як дадатковыя электронныя кнігі.",
"LabelSettingsBookshelfViewHelp": "Рэалістычны дызайн з драўлянымі паліцамі",
"LabelSettingsEnableWatcherHelp": "Адключае аўтаматычнае дадаванне/абнаўленне элементаў пры выяўленні змен у файлах. *Патрабуецца перазапуск сервера",
"LabelSettingsEpubsAllowScriptedContent": "Дазволіць скрыптавы кантэнт у EPUB",
"LabelSettingsEpubsAllowScriptedContentHelp": "Дазволіць EPUB-файлам выконваць скрыпты. Рэкамендуецца пакінуць гэтую наладу выключанай, калі вы не давяраеце крыніцы EPUB-файлаў.",
"LabelSettingsExperimentalFeatures": "Эксперыментальныя функцыі",
"LabelSettingsExperimentalFeaturesHelp": "Функцыі ў распрацоўцы, для якіх вашы водгукі і дапамога ў тэставанні будуць карыснымі. Націсніце, каб адкрыць абмеркаванне на GitHub.",
"LabelSettingsFindCovers": "Знайсці вокладкі",
"LabelSettingsFindCoversHelp": "Калі ў вашай аўдыякнізе няма ўбудаванай вокладкі або выявы вокладкі ў тэчцы, сканер паспрабуе знайсці вокладку.<br>Заўвага: гэта павялічыць час сканавання",
"LabelSettingsHideSingleBookSeries": "Схаваць серыі з адной кнігай",
"LabelSettingsHideSingleBookSeriesHelp": "Серыі, якія змяшчаюць толькі адну кнігу, будуць схаваны са старонкі серый і паліц на галоўнай старонцы.",
"LabelSettingsHomePageBookshelfView": "Галоўная старонка выкарыстоўвае выгляд кніжнай паліцы",
"LabelSettingsLibraryBookshelfView": "Бібліятэка выкарыстоўвае выгляд кніжнай паліцы",
"LabelSettingsLibraryMarkAsFinishedTimeRemaining": "Час, што застаўся, менш за (секунды)",
"LabelSettingsLibraryMarkAsFinishedWhen": "Пазначыць элемент медыя як скончаны, калі",
"LabelSettingsOnlyShowLaterBooksInContinueSeries": "Пропусціць папярэднія кнігі ў \"Працягнуць серыю\"",
"LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "Палка \"Працягнуць серыю\" на галоўнай старонцы паказвае першую не пачатую кнігу ў серыях, дзе завершана хаця б адна кніга і няма кніг у працэсе чытання. Уключэнне гэтай налады дазволіць працягваць серыю з самай апошняй завершанай кнігі замест першай не пачатай.",
"LabelSettingsParseSubtitles": "Разабраць падзагалоўкі",
"LabelSettingsParseSubtitlesHelp": "Выдзяляць падзагаловак з назваў тэчак аўдыякніг.<br>Падзагаловак павінен быць аддзелены знакам \" - \".<br>Напрыклад, \"Назва кнігі - Падзагаловак тут\" мае падзагаловак \"Падзагаловак тут\"",
"LabelSettingsTimeFormat": "Фармат часу",
"LabelShareDownloadableHelp": "Дазваляе карыстальнікам, якія маюць спасылку на доступ, спампаваць ZIP-файл элемента бібліятэкі.",
"LabelShowAll": "Паказаць усё",
"LabelShowSubtitles": "Паказаць падзагалоўкі",
"LabelSize": "Памер",
"LabelSleepTimer": "Таймер сну",
"LabelStart": "Пачаць",
"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": "Часавая база",
"LabelTimeDurationXHours": "{0} гадзін",
"LabelTimeDurationXMinutes": "{0} хвілін",
"LabelTimeDurationXSeconds": "{0} секунд",
"LabelTimeInMinutes": "Час у хвілінах",
"LabelTimeLeft": "{0} засталося",
"LabelTimeListened": "Час праслухоўвання",
"LabelTimeListenedToday": "Час праслухоўвання сёння",
"LabelTimeRemaining": "Засталося {0}",
"LabelTimeToShift": "Час зрушэння ў секундах",
"LabelTitle": "Назва",
"LabelToolsSplitM4bDescription": "Стварэнне MP3 з M4B, падзеленага па раздзелах, з убудаванымі метаданымі, вокладкай і раздзеламі.",
"LabelTotalDuration": "Агульная працягласць",
"LabelTotalTimeListened": "Агульны час праслухоўвання",
"LabelTrackFromFilename": "Дарожка з імя файла",
"LabelTrackFromMetadata": "Дарожка з метаданых",
"LabelTracks": "Дарожкі",
"LabelTracksMultiTrack": "Шматдарожкавы",
"LabelTracksNone": "Няма дарожак",
"LabelTracksSingleTrack": "Аднадарожкавы",
"LabelType": "Тып",
"LabelUndo": "Адмяніць",
"LabelUnknown": "Невядома",
"LabelUnknownPublishDate": "Невядомая дата публікацыі",
"LabelUpdateCover": "Абнавіць вокладку",
"LabelUpdateCoverHelp": "Дазволіць замену існуючых вокладак для выбраных кніг пры выяўленні адпаведнасці",
"LabelUpdateDetails": "Абнавіць падрабязнасці",
"LabelUpdateDetailsHelp": "Дазволіць замену існуючых падрабязнасцей для выбраных кніг пры выяўленні адпаведнасці",
"LabelUpdatedAt": "Абноўлена ў",
"LabelUploaderDragAndDrop": "Перацягвайце і скідайце файлы або тэчкі",
"LabelUploaderDragAndDropFilesOnly": "Перацягвайце і скідайце файлы",
"LabelUploaderDropFiles": "Скідайце файлы",
"LabelUploaderItemFetchMetadataHelp": "Аўтаматычна атрымліваць назву, аўтара і серыю",
"LabelUseAdvancedOptions": "Выкарыстоўваць пашыраныя параметры",
"LabelUseChapterTrack": "Выкарыстоўваць дарожку раздзелаў",
"LabelUseFullTrack": "Выкарыстоўваць поўную дарожку",
"LabelUser": "Карыстальнік",
"LabelUsername": "Імя карыстальніка",
"LabelValue": "Значэнне",
"LabelVersion": "Версія",
"LabelViewBookmarks": "Праглядзець закладкі",
"LabelViewChapters": "Праглядзець раздзелы",
"LabelViewPlayerSettings": "Праглядзець налады прайгравальніка",
"LabelViewQueue": "Праглядзець чаргу прайгравальніка",
"LabelVolume": "Гучнасць",
"LabelWebRedirectURLsDescription": "Аўтарызуйце гэтыя URL у вашым OAuth-правайдары для перанакіравання ў вэб-дадатак пасля ўваходу:",
"LabelWebRedirectURLsSubfolder": "Падтэчка для URL-перанакіраванняў",
"LabelWeekdaysToRun": "Дні тыдня для запуску",
"LabelXBooks": "{0} кніг",
"LabelXItems": "{0} элементаў",
"LabelYearReviewHide": "Схаваць вынікі года",
"LabelYearReviewShow": "Азнаёміцца з вынікамі года",
"LabelYourAudiobookDuration": "Працягласць вашай аўдыякнігі",
"LabelYourBookmarks": "Вашы закладкі",
"LabelYourPlaylists": "Вашы спісы прайгравання",
"LabelYourProgress": "Ваш прагрэс",
"MessageAddToPlayerQueue": "Дадаць у чаргу прайгравальніка",
"MessageAppriseDescription": "Каб выкарыстоўваць гэтую функцыю, вам спатрэбіцца запусціць асобнік <a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">Apprise API</a> або API, які будзе апрацоўваць тыя ж запыты.<br />URL Apprise API павінен быць поўным шляхам для адпраўкі апавяшчэння, напрыклад, калі ваш API працуе па адрасе <code>http://192.168.1.1:8337</code>, то вы павінны ўвесці <code>http://192.168.1.1:8337/notify</code>.",
"MessageBackupsDescription": "Рэзервовыя копіі ўключаюць карыстальнікаў, іх прагрэс, падрабязнасці элементаў бібліятэкі, налады сервера і выявы, якія захоўваюцца ў <code>/metadata/items</code> і <code>/metadata/authors</code>. Рэзервовыя копіі <strong>не</strong> ўключаюць файлы, якія захоўваюцца ў вашых тэчках бібліятэкі.",
"MessageBackupsLocationEditNote": "Заўвага: Абнаўленне месцазнаходжання рэзервовых копій не перамяшчае і не змяняе існуючыя рэзервовыя копіі",
"MessageBackupsLocationNoEditNote": "Заўвага: Месцазнаходжанне рэзервовых копій задаецца праз зменную асяроддзя і не можа быць зменена тут.",
"MessageBackupsLocationPathEmpty": "Шлях да месцазнаходжання рэзервовых копій не можа быць пустым",
"MessageBatchEditPopulateMapDetailsAllHelp": "Запоўніце ўключаныя палі дадзенымі з усіх элементаў. Палі з некалькімі значэннямі будуць аб'яднаны",
"MessageBatchEditPopulateMapDetailsItemHelp": "Запоўніце ўключаныя палі падрабязнасцей карты дадзенымі з гэтага элемента",
"MessageBookshelfNoRSSFeeds": "Няма адкрытых RSS-стужак",
"MessageChapterErrorStartGteDuration": "Няправільны час пачатку: ён павінен быць меншым за працягласць аўдыякнігі",
"MessageChapterErrorStartLtPrev": "Няправільны час пачатку: ён павінен быць большым або роўным часу пачатку папярэдняга раздзела",
"MessageConfirmCloseFeed": "Вы ўпэўнены, што жадаеце закрыць гэтую стужку?",
"MessageConfirmRemoveListeningSessions": "Вы ўпэўнены, што жадаеце выдаліць {0} сеансаў праслухоўвання?",
"MessageConfirmRemovePlaylist": "Вы ўпэўненыя, што жадаеце выдаліць свой спіс прайгравання \"{0}\"?",
"MessageConfirmSendEbookToDevice": "Вы ўпэўнены, што хочаце адправіць {0} электронную кнігу \"{1}\" на прыладу \"{2}\"?",
"MessageDownloadingEpisode": "Спампоўка эпізоду",
"MessageEpisodesQueuedForDownload": "{0} эпізод(аў) у чарзе для спампоўкі",
"MessageEreaderDevices": "Каб забяспечыць дастаўку электронных кніг, вам можа спатрэбіцца дадаць вышэйзгаданы адрас электроннай пошты як дазволенага адпраўніка для кожнай прылады, пералічанай ніжэй.",
"MessageFeedURLWillBe": "URL стужкі будзе {0}",
"MessageFetching": "Атрыманне...",
"MessageLoading": "Загрузка...",
"MessageMapChapterTitles": "Супаставіць назвы раздзелаў з вашымі існуючымі раздзеламі аўдыякнігі без змянення часовых метак",
"MessageMarkAsFinished": "Пазначыць як скончана",
"MessageNoBookmarks": "Няма закладак",
"MessageNoChapters": "Няма раздзелаў",
"MessageNoCollections": "Няма калекцый",
"MessageNoDownloadsInProgress": "Зараз няма актыўных спамповак",
"MessageNoDownloadsQueued": "Няма спамповак у чарзе",
"MessageNoItems": "Няма элементаў",
"MessageNoItemsFound": "Элементы не знойдзены",
"MessageNoListeningSessions": "Няма сеансаў праслухоўвання",
"MessageNoMediaProgress": "Няма прагрэсу медыя",
"MessageNoPodcastFeed": "Няправільны падкаст: Няма стужкі",
"MessageNoPodcastsFound": "Падкасты не знойдзены",
"MessageNoUpdatesWereNecessary": "Абнаўленні не патрабаваліся",
"MessageNoUserPlaylists": "У вас няма спісаў прайгравання",
"MessageNoUserPlaylistsHelp": "Спісы прайгравання прыватныя. Толькі карыстальнік, які іх стварыў, можа іх бачыць.",
"MessageOpmlPreviewNote": "Заўвага: гэта папярэдні прагляд разабранага OPML-файла. Фактычная назва падкаста будзе ўзятая з RSS-стужкі.",
"MessagePlaylistCreateFromCollection": "Стварыць спіс прайгравання з калекцыі",
"MessagePodcastHasNoRSSFeedForMatching": "У падкаста няма URL RSS-стужкі для супадзення",
"MessagePodcastSearchField": "Увядзіце пошукавы запыт або URL RSS-стужкі",
"MessageReportBugsAndContribute": "Паведамляйце пра памылкі, прапануйце новыя функцыі і ўдзельнічайце на",
"MessageScheduleRunEveryWeekdayAtTime": "Выконваць кожныя {0} у {1}",
"MessageStartPlaybackAtTime": "Пачаць прайграванне для \"{0}\" з {1}?",
"MessageTaskCanceledByUser": "Задача скасавана карыстальнікам",
"MessageTaskDownloadingEpisodeDescription": "Спампоўка эпізоду \"{0}\"",
"MessageTaskOpmlImportDescription": "Стварэнне падкастаў з {0} RSS-стужак",
"MessageTaskOpmlImportFeed": "Імпарт стужкі з OPML",
@@ -300,11 +556,22 @@
"MessageTaskOpmlImportFeedPodcastExists": "Падкаст ужо існуе па гэтым шляху",
"MessageTaskOpmlImportFeedPodcastFailed": "Не ўдалося стварыць падкаст",
"MessageTaskOpmlParseNoneFound": "У OPML-файле не знойдзена стужак",
"NoteChapterEditorTimes": "Заўвага: Час пачатку першага раздзела павінен заставацца 0:00, а час пачатку апошняга раздзела не можа перавышаць працягласць гэтай аўдыякнігі.",
"NoteRSSFeedPodcastAppsHttps": "Папярэджанне: большасць праграм для падкастаў патрабуюць, каб URL RSS-стужкі выкарыстоўваў HTTPS",
"NoteRSSFeedPodcastAppsPubDate": "Папярэджанне: адзін ці больш вашых эпізодаў не маюць даты публікацыі. Некаторыя праграмы для падкастаў патрабуюць гэтага.",
"NoteUploaderFoldersWithMediaFiles": "Тэчкі з медыяфайламі будуць апрацоўвацца як асобныя элементы бібліятэкі.",
"NotificationOnEpisodeDownloadedDescription": "Выклікаецца, калі эпізод падкаста аўтаматычна спампоўваецца",
"PlaceholderNewPlaylist": "Імя новага спіса прайгравання",
"StatsBooksFinished": "кнігі скончаны",
"StatsBooksFinishedThisYear": "Некаторыя кнігі скончаны ў гэтым годзе…",
"StatsBooksListenedTo": "кнігі, якія былі праслуханы",
"StatsCollectionGrewTo": "Ваша калекцыя кніг павялічылася да…",
"ToastAccountUpdateSuccess": "Уліковы запіс абноўлены",
"ToastBookmarkCreateFailed": "Не ўдалося стварыць закладку",
"ToastDateTimeInvalidOrIncomplete": "Дата і час указаны некарэктна або не цалкам",
"ToastDeviceTestEmailFailed": "Не ўдалося адправіць тэставае электроннае пісьмо",
"ToastEncodeCancelFailed": "Не ўдалося скасаваць кадаванне",
"ToastEncodeCancelSucces": "Кадаванне скасавана",
"ToastEpisodeDownloadQueueClearFailed": "Не ўдалося ачысціць чаргу",
"ToastEpisodeDownloadQueueClearSuccess": "Чарга спампоўкі эпізодаў ачышчана",
"ToastInvalidMaxEpisodesToDownload": "Няправільная максімальная колькасць эпізодаў для спампоўкі",
@@ -331,11 +598,18 @@
"ToastNewUserCreatedFailed": "Не ўдалося стварыць уліковы запіс: \"{0}\"",
"ToastNewUserCreatedSuccess": "Новы ўліковы запіс створаны",
"ToastNoRSSFeed": "У падкаста няма RSS-стужкі",
"ToastPlaylistCreateFailed": "Не ўдалося стварыць спіс прайгравання",
"ToastPlaylistCreateSuccess": "Спіс прайгравання створаны",
"ToastPlaylistRemoveSuccess": "Спіс прайгравання выдалены",
"ToastPlaylistUpdateSuccess": "Спіс прайгравання абноўлены",
"ToastPodcastGetFeedFailed": "Не ўдалося атрымаць стужку падкаста",
"ToastPodcastNoEpisodesInFeed": "У RSS-стужцы не знойдзена эпізодаў",
"ToastPodcastNoRssFeed": "У падкаста няма RSS-стужкі",
"ToastRSSFeedCloseFailed": "Не ўдалося закрыць RSS-стужку",
"ToastRSSFeedCloseSuccess": "RSS-стужка закрыта",
"ToastSendEbookToDeviceFailed": "Не ўдалося адправіць электронную кнігу на прыладу",
"ToastSendEbookToDeviceSuccess": "Электронная кніга адпраўлена на прыладу \"{0}\"",
"ToastSleepTimerDone": "Таймер сну скончыўся... Хр-р-р",
"ToastUserPasswordMustChange": "Новы пароль не можа супадаць са старым",
"ToastUserRootRequireName": "Неабходна ўвесці імя карыстальніка адміністратара"
}

View File

@@ -516,11 +516,6 @@
"LabelSettingsBookshelfViewHelp": "Скеуморфен дизайн с дървени рафтове",
"LabelSettingsChromecastSupport": "Chromecast поддръжка",
"LabelSettingsDateFormat": "Формат на Дата",
"LabelSettingsDisableWatcher": "Изключи наблюдателя",
"LabelSettingsDisableWatcherForLibrary": "Изключи наблюдателя за библиотека",
"LabelSettingsDisableWatcherHelp": "Изключва автоматичното добавяне/обновяване на елементи, когато се открият промени във файловете. *Изисква рестарт на сървъра",
"LabelSettingsEnableWatcher": "Включи наблюдателя",
"LabelSettingsEnableWatcherForLibrary": "Включи наблюдателя за библиотека",
"LabelSettingsEnableWatcherHelp": "Включва автоматичното добавяне/обновяване на елементи, когато се открият промени във файловете. *Изисква рестарт на сървъра",
"LabelSettingsEpubsAllowScriptedContent": "Позволи скриптово съдържание в epub-и",
"LabelSettingsEpubsAllowScriptedContentHelp": "Позволи epub файловете да изпълняват скриптове. Препоръчително е да бъде изключено освен ако не се доверявате на източника на epub файловете.",

View File

@@ -552,11 +552,6 @@
"LabelSettingsBookshelfViewHelp": "কাঠের তাক সহ স্কুমরফিক ডিজাইন",
"LabelSettingsChromecastSupport": "ক্রোমকাস্ট সমর্থন",
"LabelSettingsDateFormat": "তারিখ বিন্যাস",
"LabelSettingsDisableWatcher": "প্রহরী নিষ্ক্রিয় করুন",
"LabelSettingsDisableWatcherForLibrary": "লাইব্রেরির জন্য ফোল্ডার প্রহরী নিষ্ক্রিয় করুন",
"LabelSettingsDisableWatcherHelp": "ফাইলের পরিবর্তন শনাক্ত হলে স্বয়ংক্রিয়ভাবে আইটেম যোগ/আপডেট করা অক্ষম করবে। *সার্ভার পুনরায় চালু করতে হবে",
"LabelSettingsEnableWatcher": "প্রহরী সক্ষম করুন",
"LabelSettingsEnableWatcherForLibrary": "লাইব্রেরির জন্য ফোল্ডার প্রহরী সক্ষম করুন",
"LabelSettingsEnableWatcherHelp": "ফাইলের পরিবর্তন শনাক্ত হলে আইটেমগুলির স্বয়ংক্রিয় যোগ/আপডেট সক্ষম করবে। *সার্ভার পুনরায় চালু করতে হবে",
"LabelSettingsEpubsAllowScriptedContent": "ইপাবে স্ক্রিপ্ট করা বিষয়বস্তুর অনুমতি দিন",
"LabelSettingsEpubsAllowScriptedContentHelp": "ইপাব ফাইলগুলিকে স্ক্রিপ্ট চালানোর অনুমতি দিন। আপনি ইপাব ফাইলগুলির উৎসকে বিশ্বাস না করলে এই সেটিংটি নিষ্ক্রিয় রাখার সুপারিশ করা হলো।",

View File

@@ -548,11 +548,6 @@
"LabelSettingsBookshelfViewHelp": "Disseny esqueomorf amb prestatgeries de fusta",
"LabelSettingsChromecastSupport": "Compatibilitat amb Chromecast",
"LabelSettingsDateFormat": "Format de Data",
"LabelSettingsDisableWatcher": "Desactivar Watcher",
"LabelSettingsDisableWatcherForLibrary": "Desactivar Watcher de Carpetes per a aquesta biblioteca",
"LabelSettingsDisableWatcherHelp": "Desactiva la funció d'afegir/actualitzar elements automàticament quan es detectin canvis en els fitxers. *Requereix reiniciar el servidor",
"LabelSettingsEnableWatcher": "Habilitar Watcher",
"LabelSettingsEnableWatcherForLibrary": "Habilitar Watcher per a la carpeta d'aquesta biblioteca",
"LabelSettingsEnableWatcherHelp": "Permet afegir/actualitzar elements automàticament quan es detectin canvis en els fitxers. *Requereix reiniciar el servidor",
"LabelSettingsEpubsAllowScriptedContent": "Permetre scripts en epubs",
"LabelSettingsEpubsAllowScriptedContentHelp": "Permetre que els fitxers epub executin scripts. Es recomana mantenir aquesta opció desactivada tret que confiïs en l'origen dels fitxers epub.",

View File

@@ -555,11 +555,6 @@
"LabelSettingsBookshelfViewHelp": "Skeumorfní design s dřevěnými policemi",
"LabelSettingsChromecastSupport": "Podpora Chromecastu",
"LabelSettingsDateFormat": "Formát data",
"LabelSettingsDisableWatcher": "Zakázat sledování",
"LabelSettingsDisableWatcherForLibrary": "Zakázat sledování složky pro knihovnu",
"LabelSettingsDisableWatcherHelp": "Zakáže automatické přidávání/aktualizaci položek při zjištění změn v souboru. *Vyžaduje restart serveru",
"LabelSettingsEnableWatcher": "Povolit sledování",
"LabelSettingsEnableWatcherForLibrary": "Povolit sledování složky pro knihovnu",
"LabelSettingsEnableWatcherHelp": "Povoluje automatické přidávání/aktualizaci položek, když jsou zjištěny změny souborů. *Vyžaduje restart serveru",
"LabelSettingsEpubsAllowScriptedContent": "Povolení skriptovaného obsahu v epubu",
"LabelSettingsEpubsAllowScriptedContentHelp": "Povolení spouštění skriptů v souborech epub. Doporučujeme toto nastavení vypnout, pokud nedůvěřujete zdroji souborů epub.",

View File

@@ -219,6 +219,7 @@
"LabelAccountTypeAdmin": "Administrator",
"LabelAccountTypeGuest": "Gæst",
"LabelAccountTypeUser": "Bruger",
"LabelActivities": "Aktiviteter",
"LabelActivity": "Aktivitet",
"LabelAddToCollection": "Tilføj til Samling",
"LabelAddToCollectionBatch": "Tilføj {0} Bøger til Samling",
@@ -283,6 +284,7 @@
"LabelContinueSeries": "Fortsæt Serien",
"LabelCover": "Omslag",
"LabelCoverImageURL": "Omslagsbillede URL",
"LabelCoverProvider": "Cover billede udbyder",
"LabelCreatedAt": "Oprettet Kl.",
"LabelCronExpression": "Cron Udtryk",
"LabelCurrent": "Aktuel",
@@ -391,6 +393,7 @@
"LabelIntervalEvery6Hours": "Hver 6. time",
"LabelIntervalEveryDay": "Hver dag",
"LabelIntervalEveryHour": "Hver time",
"LabelIntervalEveryMinute": "Hvert minut",
"LabelInvert": "Inverter",
"LabelItem": "Element",
"LabelJumpBackwardAmount": "Spring bagud mængde",
@@ -555,11 +558,6 @@
"LabelSettingsBookshelfViewHelp": "Skeumorfisk design med træhylder",
"LabelSettingsChromecastSupport": "Chromecast-understøttelse",
"LabelSettingsDateFormat": "Datoformat",
"LabelSettingsDisableWatcher": "Deaktiver overvågning",
"LabelSettingsDisableWatcherForLibrary": "Deaktiver mappeovervågning for bibliotek",
"LabelSettingsDisableWatcherHelp": "Deaktiverer automatisk tilføjelse/opdatering af elementer, når der registreres filændringer. *Kræver servergenstart",
"LabelSettingsEnableWatcher": "Aktiver overvågning",
"LabelSettingsEnableWatcherForLibrary": "Aktiver mappeovervågning for bibliotek",
"LabelSettingsEnableWatcherHelp": "Aktiverer automatisk tilføjelse/opdatering af elementer, når filændringer registreres. *Kræver servergenstart",
"LabelSettingsEpubsAllowScriptedContent": "Tillad scriptet indhold i epub",
"LabelSettingsEpubsAllowScriptedContentHelp": "Tillad epub filer at køre scripts. Det anbefales at holde denne indstilling deaktiveret med mindre du stoler på kilderne af epub filerne.",
@@ -845,6 +843,7 @@
"MessageRestoreBackupConfirm": "Er du sikker på, at du vil gendanne sikkerhedskopien oprettet den",
"MessageRestoreBackupWarning": "Gendannelse af en sikkerhedskopi vil overskrive hele databasen, som er placeret på /config, og omslagsbilleder i /metadata/items & /metadata/authors.<br /><br />Sikkerhedskopier ændrer ikke nogen filer i dine biblioteksmapper. Hvis du har aktiveret serverindstillinger for at gemme omslagskunst og metadata i dine biblioteksmapper, sikkerhedskopieres eller overskrives disse ikke.<br /><br />Alle klienter, der bruger din server, opdateres automatisk.",
"MessageScheduleLibraryScanNote": "For de fleste brugere, er det anbefalet at efterlade denne funktion deaktiveret for at holde mappe lurer indstilling aktiveret. Mappe lureren vil automatisk opdage ændringer i biblioteksmapper. Mappe lureren virker ikke for alle filsystemer (så som NFS) så schedulerede biblioteksscans vil blive anvendt.",
"MessageScheduleRunEveryWeekdayAtTime": "Kør hvert {0} af {1}",
"MessageSearchResultsFor": "Søgeresultater for",
"MessageSelected": "{0} valgt",
"MessageServerCouldNotBeReached": "Serveren kunne ikke nås",

View File

@@ -558,11 +558,6 @@
"LabelSettingsBookshelfViewHelp": "Skeumorphes Design mit Holzeinlegeböden",
"LabelSettingsChromecastSupport": "Chromecastunterstützung",
"LabelSettingsDateFormat": "Datumsformat",
"LabelSettingsDisableWatcher": "Überwachung deaktivieren",
"LabelSettingsDisableWatcherForLibrary": "Ordnerüberwachung für die Bibliothek deaktivieren",
"LabelSettingsDisableWatcherHelp": "Deaktiviert das automatische Hinzufügen/Aktualisieren von Elementen, wenn Dateiänderungen erkannt werden. *Erfordert einen Server-Neustart",
"LabelSettingsEnableWatcher": "Überwachung aktivieren",
"LabelSettingsEnableWatcherForLibrary": "Ordnerüberwachung für die Bibliothek aktivieren",
"LabelSettingsEnableWatcherHelp": "Aktiviert das automatische Hinzufügen/Aktualisieren von Elementen, wenn Dateiänderungen erkannt werden. *Erfordert einen Server-Neustart",
"LabelSettingsEpubsAllowScriptedContent": "Skriptinhalte in Epubs zulassen",
"LabelSettingsEpubsAllowScriptedContentHelp": "Erlaube Epub-Dateien, Skripte auszuführen. Es wird empfohlen, diese Einstellung deaktiviert zu lassen, es sei denn, du vertraust der Quelle der Epub-Dateien.",
@@ -711,6 +706,7 @@
"MessageBackupsLocationNoEditNote": "Hinweis: Der Sicherungsspeicherort wird über eine Umgebungsvariable festgelegt und kann hier nicht geändert werden.",
"MessageBackupsLocationPathEmpty": "Der Backup-Pfad darf nicht leer sein",
"MessageBatchEditPopulateMapDetailsAllHelp": "Fülle die aktivierten Felder mit Daten aus allen Elementen. Felder mit mehreren Werten werden zusammengeführt",
"MessageBatchEditPopulateMapDetailsItemHelp": "Aktivierte Felder für Kartendetails mit Daten aus diesem Element füllen",
"MessageBatchQuickMatchDescription": "Der Schnellabgleich versucht, fehlende Titelbilder und Metadaten für die ausgewählten Artikel hinzuzufügen. Aktiviere die nachstehenden Optionen, damit der Schnellabgleich vorhandene Titelbilder und/oder Metadaten überschreiben kann.",
"MessageBookshelfNoCollections": "Es wurden noch keine Sammlungen erstellt",
"MessageBookshelfNoCollectionsHelp": "Sammlungen sind öffentlich. Alle Benutzer mit Zugriff auf die Bibliothek können sie sehen.",

View File

@@ -252,7 +252,7 @@
"LabelBackToUser": "Back to User",
"LabelBackupAudioFiles": "Backup Audio Files",
"LabelBackupLocation": "Backup Location",
"LabelBackupsEnableAutomaticBackups": "Enable automatic backups",
"LabelBackupsEnableAutomaticBackups": "Automatic backups",
"LabelBackupsEnableAutomaticBackupsHelp": "Backups saved in /metadata/backups",
"LabelBackupsMaxBackupSize": "Maximum backup size (in GB) (0 for unlimited)",
"LabelBackupsMaxBackupSizeHelp": "As a safeguard against misconfiguration, backups will fail if they exceed the configured size.",
@@ -558,11 +558,8 @@
"LabelSettingsBookshelfViewHelp": "Skeumorphic design with wooden shelves",
"LabelSettingsChromecastSupport": "Chromecast support",
"LabelSettingsDateFormat": "Date Format",
"LabelSettingsDisableWatcher": "Disable Watcher",
"LabelSettingsDisableWatcherForLibrary": "Disable folder watcher for library",
"LabelSettingsDisableWatcherHelp": "Disables the automatic adding/updating of items when file changes are detected. *Requires server restart",
"LabelSettingsEnableWatcher": "Enable Watcher",
"LabelSettingsEnableWatcherForLibrary": "Enable folder watcher for library",
"LabelSettingsEnableWatcher": "Automatically scan libraries for changes",
"LabelSettingsEnableWatcherForLibrary": "Automatically scan library for changes",
"LabelSettingsEnableWatcherHelp": "Enables the automatic adding/updating of items when file changes are detected. *Requires server restart",
"LabelSettingsEpubsAllowScriptedContent": "Allow scripted content in epubs",
"LabelSettingsEpubsAllowScriptedContentHelp": "Allow epub files to execute scripts. It is recommended to keep this setting disabled unless you trust the source of the epub files.",
@@ -580,7 +577,7 @@
"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": "Parse subtitles",
"LabelSettingsParseSubtitlesHelp": "Extract subtitles from audiobook folder names.<br>Subtitle must be seperated by \" - \"<br>i.e. \"Book Title - A Subtitle Here\" has the subtitle \"A Subtitle Here\"",
"LabelSettingsParseSubtitlesHelp": "Extract subtitles from audiobook folder names.<br>Subtitle must be separated by \" - \"<br>i.e. \"Book Title - A Subtitle Here\" has the subtitle \"A Subtitle Here\"",
"LabelSettingsPreferMatchedMetadata": "Prefer matched metadata",
"LabelSettingsPreferMatchedMetadataHelp": "Matched data will override item details when using Quick Match. By default Quick Match will only fill in missing details.",
"LabelSettingsSkipMatchingBooksWithASIN": "Skip matching books that already have an ASIN",

View File

@@ -552,11 +552,6 @@
"LabelSettingsBookshelfViewHelp": "Diseño Esqueuomorfo con Estantes de Madera",
"LabelSettingsChromecastSupport": "Soporte para Chromecast",
"LabelSettingsDateFormat": "Formato de Fecha",
"LabelSettingsDisableWatcher": "Deshabilitar Watcher",
"LabelSettingsDisableWatcherForLibrary": "Deshabilitar Watcher de Carpetas para esta biblioteca",
"LabelSettingsDisableWatcherHelp": "Deshabilitar la función de agregar/actualizar elementos automáticamente cuando se detectan cambios en los archivos. *Require Reiniciar el Servidor",
"LabelSettingsEnableWatcher": "Habilitar Watcher",
"LabelSettingsEnableWatcherForLibrary": "Habilitar Watcher para la carpeta de esta biblioteca",
"LabelSettingsEnableWatcherHelp": "Permite agregar/actualizar elementos automáticamente cuando se detectan cambios en los archivos. *Requiere reiniciar el servidor",
"LabelSettingsEpubsAllowScriptedContent": "Permitir scripts en epubs",
"LabelSettingsEpubsAllowScriptedContentHelp": "Permitir que los archivos epub ejecuten scripts. Se recomienda mantener esta opción desactivada a menos que confíe en el origen de los archivos epub.",

View File

@@ -445,11 +445,6 @@
"LabelSettingsBookshelfViewHelp": "Skeumorfne kujundus puidust riiulitega",
"LabelSettingsChromecastSupport": "Chromecasti tugi",
"LabelSettingsDateFormat": "Kuupäeva vorming",
"LabelSettingsDisableWatcher": "Keela vaatamine",
"LabelSettingsDisableWatcherForLibrary": "Keela kaustavaatamine raamatukogu jaoks",
"LabelSettingsDisableWatcherHelp": "Keelab automaatse lisamise/uuendamise, kui failimuudatusi tuvastatakse. *Nõuab serveri taaskäivitamist",
"LabelSettingsEnableWatcher": "Luba vaatamine",
"LabelSettingsEnableWatcherForLibrary": "Luba kaustavaatamine raamatukogu jaoks",
"LabelSettingsEnableWatcherHelp": "Lubab automaatset lisamist/uuendamist, kui tuvastatakse failimuudatused. *Nõuab serveri taaskäivitamist",
"LabelSettingsExperimentalFeatures": "Eksperimentaalsed funktsioonid",
"LabelSettingsExperimentalFeaturesHelp": "Arengus olevad funktsioonid, mis vajavad teie tagasisidet ja abi testimisel. Klõpsake GitHubi arutelu avamiseks.",

View File

@@ -10,6 +10,8 @@
"ButtonApplyChapters": "Käytä lukuihin",
"ButtonAuthors": "Tekijät",
"ButtonBack": "Takaisin",
"ButtonBatchEditPopulateFromExisting": "Täydennä olemassa olevista",
"ButtonBatchEditPopulateMapDetails": "Täydennä karttatiedot",
"ButtonBrowseForFolder": "Selaa (kansio)",
"ButtonCancel": "Peruuta",
"ButtonCancelEncode": "Lopeta enkoodaus",
@@ -51,7 +53,7 @@
"ButtonNext": "Seuraava",
"ButtonNextChapter": "Seuraava luku",
"ButtonNextItemInQueue": "Seuraava jonossa",
"ButtonOk": "Ok",
"ButtonOk": "Hyvä on",
"ButtonOpenFeed": "Avaa syöte",
"ButtonOpenManager": "Avaa hallinta",
"ButtonPause": "Pysäytä",
@@ -61,13 +63,14 @@
"ButtonPlaylists": "Soittolistat",
"ButtonPrevious": "Edellinen",
"ButtonPreviousChapter": "Edellinen luku",
"ButtonProbeAudioFile": "Luotaa äänitiedosto",
"ButtonPurgeAllCache": "Tyhjennä kaikki välimuistit",
"ButtonPurgeItemsCache": "Tyhjennä kohteiden välimuisti",
"ButtonQueueAddItem": "Lisää jonoon",
"ButtonQueueRemoveItem": "Poista jonosta",
"ButtonQuickEmbed": "Pikaupota",
"ButtonQuickEmbedMetadata": "Upota kuvailutiedot nopeasti",
"ButtonQuickMatch": "Pikatäsmää",
"ButtonQuickMatch": "Pikatäsmäys",
"ButtonReScan": "Uudelleenskannaa",
"ButtonRead": "Lue",
"ButtonReadLess": "Lue vähemmän",
@@ -132,7 +135,7 @@
"HeaderCustomMetadataProviders": "Mukautetut metadatan tarjoajat",
"HeaderDetails": "Yksityiskohdat",
"HeaderDownloadQueue": "Latausjono",
"HeaderEbookFiles": "E-kirjatiedostot",
"HeaderEbookFiles": "S-kirjatiedostot",
"HeaderEmail": "Sähköposti",
"HeaderEmailSettings": "Sähköpostiasetukset",
"HeaderEpisodes": "Jaksot",
@@ -141,6 +144,8 @@
"HeaderFiles": "Tiedostot",
"HeaderFindChapters": "Etsi kappaleet",
"HeaderIgnoredFiles": "Ohitetut tiedostot",
"HeaderItemFiles": "Kohteen tiedostot",
"HeaderItemMetadataUtils": "Metadatan hallinta",
"HeaderLastListeningSession": "Edellinen kuuntelukerta",
"HeaderLatestEpisodes": "Viimeisimmät jaksot",
"HeaderLibraries": "Kirjastot",
@@ -152,6 +157,8 @@
"HeaderLogs": "Lokit",
"HeaderManageGenres": "Hallitse lajityyppejä",
"HeaderManageTags": "Hallitse tageja",
"HeaderMapDetails": "Karttatiedot",
"HeaderMatch": "Täsmää",
"HeaderMetadataOrderOfPrecedence": "Metadatan tärkeysjärjestys",
"HeaderMetadataToEmbed": "Sisällytettävä metadata",
"HeaderNewAccount": "Uusi tili",
@@ -159,6 +166,8 @@
"HeaderNotificationCreate": "Luo ilmoitus",
"HeaderNotificationUpdate": "Päivitä ilmoitus",
"HeaderNotifications": "Ilmoitukset",
"HeaderOpenIDConnectAuthentication": "OpenID Connect -todennus",
"HeaderOpenListeningSessions": "Avoimet kuuntelusessiot",
"HeaderOpenRSSFeed": "Avaa RSS-syöte",
"HeaderOtherFiles": "Muut tiedostot",
"HeaderPasswordAuthentication": "Salasanan todentaminen",
@@ -174,6 +183,7 @@
"HeaderRSSFeeds": "RSS syötteet",
"HeaderRemoveEpisode": "Poista jakso",
"HeaderRemoveEpisodes": "Poista {0} jaksoa",
"HeaderSavedMediaProgress": "Tallennettu median edistyminen",
"HeaderSchedule": "Ajoita",
"HeaderScheduleEpisodeDownloads": "Ajoita automaattiset jaksolataukset",
"HeaderScheduleLibraryScans": "Ajoita automaattiset kirjastoskannaukset",
@@ -184,17 +194,18 @@
"HeaderSettingsExperimental": "Kokeelliset ominaisuudet",
"HeaderSettingsGeneral": "Yleiset",
"HeaderSettingsScanner": "Skannaaja",
"HeaderSettingsWebClient": "Webasiakasohjelma",
"HeaderSleepTimer": "Uniajastin",
"HeaderStatsLargestItems": "Suurimmat kohteet",
"HeaderStatsLongestItems": "Pisimmät kohteet (h)",
"HeaderStatsMinutesListeningChart": "Kuunteluminuutit (viim. 7 pv)",
"HeaderStatsRecentSessions": "Viimeaikaiset istunnot",
"HeaderStatsTop10Authors": "Suosituimmat 10 kirjailijaa",
"HeaderStatsTop10Authors": "Suosituimmat 10 tekijää",
"HeaderStatsTop5Genres": "Suosituimmat 5 lajityyppiä",
"HeaderTableOfContents": "Sisällysluettelo",
"HeaderTools": "Työkalut",
"HeaderUpdateAccount": "Päivitä tili",
"HeaderUpdateAuthor": "Päivitä kirjailija",
"HeaderUpdateAuthor": "Päivitä tekijä",
"HeaderUpdateDetails": "Päivitä yksityiskohdat",
"HeaderUpdateLibrary": "Päivitä kirjasto",
"HeaderUsers": "Käyttäjät",
@@ -203,10 +214,12 @@
"LabelAbridged": "Lyhennetty",
"LabelAbridgedChecked": "Lyhennetty (tarkistettu)",
"LabelAbridgedUnchecked": "Lyhentämätön (tarkistamaton)",
"LabelAccessibleBy": "Saavutettavissa:",
"LabelAccountType": "Tilin tyyppi",
"LabelAccountTypeAdmin": "Järjestelmänvalvoja",
"LabelAccountTypeGuest": "Vieras",
"LabelAccountTypeUser": "Käyttäjä",
"LabelActivities": "Toiminnot",
"LabelActivity": "Toiminta",
"LabelAddToCollection": "Lisää kokoelmaan",
"LabelAddToCollectionBatch": "Lisää {0} kirjaa kokoelmaan",
@@ -221,6 +234,7 @@
"LabelAllUsersIncludingGuests": "Kaikki käyttäjät mukaan lukien vieraat",
"LabelAlreadyInYourLibrary": "Jo kirjastossasi",
"LabelApiToken": "Sovellusliittymätunnus",
"LabelAppend": "Lisää loppuun",
"LabelAudioBitrate": "Äänen bittinopeus (esim. 128k)",
"LabelAudioChannels": "Äänikanavat (1 tai 2)",
"LabelAudioCodec": "Äänikoodekki",
@@ -230,7 +244,9 @@
"LabelAuthors": "Tekijät",
"LabelAutoDownloadEpisodes": "Lataa jaksot automaattisesti",
"LabelAutoFetchMetadata": "Etsi metadata automaattisesti",
"LabelAutoFetchMetadataHelp": "Hakee metatiedot kohteille, kirjailijoille ja sarjoille lähetyksen nopeuttamiseksi. Joitain metatietoja voidaan joutua täsmäämään lähetyksen jälkeen.",
"LabelAutoLaunch": "Automaattinen käynnistys",
"LabelAutoLaunchDescription": "Uudelleenohjaa automaattisesti kirjautumisen tarjoajaan kirjautumissivulle saavuttaessa. (ohitettavissa käyttämällä polkua <code>/login?autoLaunch=0</code>)",
"LabelAutoRegister": "Automaattinen rekisteröinti",
"LabelAutoRegisterDescription": "Luo automaattisesti uusia käyttäjiä kirjautumisen jälkeen",
"LabelBackToUser": "Takaisin käyttäjään",
@@ -246,17 +262,19 @@
"LabelBonus": "Bonus",
"LabelBooks": "Kirjat",
"LabelButtonText": "Painikkeen teksti",
"LabelByAuthor": "tekijältä {0}",
"LabelChangePassword": "Vaihda salasana",
"LabelChannels": "Kanavat",
"LabelChapterCount": "{0} lukua",
"LabelChapterTitle": "Luvun nimi",
"LabelChapters": "Luvut",
"LabelChaptersFound": "lukua löydetty",
"LabelChaptersFound": "lukuja löydetty",
"LabelClickForMoreInfo": "Napsauta saadaksesi lisätietoja",
"LabelClickToUseCurrentValue": "Käytä nykyistä arvoa napsauttamalla",
"LabelClosePlayer": "Sulje soitin",
"LabelCodec": "Koodekki",
"LabelCollapseSeries": "Pienennä sarja",
"LabelCollapseSubSeries": "Tiivistä alisarjat",
"LabelCollection": "Kokoelma",
"LabelCollections": "Kokoelmat",
"LabelComplete": "Valmis",
@@ -266,9 +284,13 @@
"LabelContinueSeries": "Jatka sarjoja",
"LabelCover": "Kansikuva",
"LabelCoverImageURL": "Kansikuvan URL-osoite",
"LabelCoverProvider": "Kansikuvan tarjoaja",
"LabelCreatedAt": "Luotu",
"LabelCronExpression": "Cron ajastin",
"LabelCurrent": "Nykyinen",
"LabelCurrently": "Nyt:",
"LabelCustomCronExpression": "Mukautettu Cron-ajastin:",
"LabelDatetime": "Päivämäärä/Aika",
"LabelDays": "Päivää",
"LabelDeleteFromFileSystemCheckbox": "Poista tiedostojärjestelmästä (poista merkintä, jos haluat poistaa vain tietokannasta)",
"LabelDescription": "Kuvaus",
@@ -277,6 +299,8 @@
"LabelDeviceInfo": "Laitteen tiedot",
"LabelDeviceIsAvailableTo": "Laite on saatavilla...",
"LabelDirectory": "Kansio",
"LabelDiscFromFilename": "Levyn numero tiedostonimestä",
"LabelDiscFromMetadata": "Levyn numero metatiedoista",
"LabelDiscover": "Löydä",
"LabelDownload": "Lataa",
"LabelDownloadNEpisodes": "Lataa {0} jaksoa",
@@ -286,23 +310,27 @@
"LabelDurationComparisonLonger": "({0} pidempi)",
"LabelDurationComparisonShorter": "({0} lyhyempi)",
"LabelDurationFound": "Kesto löydetty:",
"LabelEbook": "E-kirja",
"LabelEbooks": "E-kirjat",
"LabelEbook": "S-kirja",
"LabelEbooks": "S-kirjat",
"LabelEdit": "Muokkaa",
"LabelEmail": "Sähköposti",
"LabelEmailSettingsFromAddress": "Osoitteesta",
"LabelEmailSettingsRejectUnauthorized": "Hylkää luvattomat sertifikaatit",
"LabelEmailSettingsRejectUnauthorizedHelp": "SSL-sertifikaatin varmentamisen käytöstä poistaminen saattaa vaarantaa yhteytesti turvallisuusriskeihin, kuten man-in-the-middle hyökkäyksiin. Poista käytöstä vain jos ymmärrät vaaran ja luotat yhdistämääsi sähköpostipalvelimeen.",
"LabelEmailSettingsSecure": "Turvallinen",
"LabelEmailSettingsSecureHelp": "Jos tosi, niin yhteys käyttää TLS:ää yhdistäessään palvelimeen. Jos epätosi, niin TSL käytetään jos palvelin tukee STARTTLS-lisäosaa. Yleensä tämä arvo on tosi jos yhdistät porttiin 465. Porteille 587 tai 25 käytä arvoa epätosi (Lähde: nodemailer.com/smtp/#authentication)",
"LabelEmailSettingsTestAddress": "Testiosoite",
"LabelEmbeddedCover": "Upotettu kansikuva",
"LabelEnable": "Ota käyttöön",
"LabelEncodingBackupLocation": "Alkuperäisistä audiotiedostoistasi tallennetaan varmuuskopio osoitteessa:",
"LabelEncodingChaptersNotEmbedded": "Lukuja ei upoteta moniraitaisiin äänikirjoihin.",
"LabelEncodingChaptersNotEmbedded": "Lukuja ei ole upotettu moniraitaisiin äänikirjoihin.",
"LabelEncodingClearItemCache": "Varmista, että kohteiden välimuisti tyhjennetään säännöllisesti.",
"LabelEncodingFinishedM4B": "Valmistunut M4B tullaan viemään äänikirjakansioosi:",
"LabelEncodingInfoEmbedded": "Kuvailutiedot upotetaan äänikirjakansion ääniraitoihin.",
"LabelEncodingStartedNavigation": "Voit poistua sivulta kun tehtävä on aloitettu.",
"LabelEncodingTimeWarning": "Koodaus saattaa kestää 30 minuuttiin asti.",
"LabelEncodingWarningAdvancedSettings": "Varoitus: Älä päivitä näitä asetuksia ellet ymmärrä ffmpeg-koodausasetuksia.",
"LabelEncodingWatcherDisabled": "Jos kansiotarkkailu on poistettu käytöstä, tämä äänikirja pitää skannata uudestaan myöhemmin.",
"LabelEnd": "Loppu",
"LabelEndOfChapter": "Luvun loppu",
"LabelEpisode": "Jakso",
@@ -312,6 +340,7 @@
"LabelEpisodeType": "Jakson tyyppi",
"LabelEpisodeUrlFromRssFeed": "Jakson URL RSS-syötteestä",
"LabelEpisodes": "Jaksot",
"LabelEpisodic": "Jaksollinen",
"LabelExample": "Esimerkki",
"LabelExpandSeries": "Laajenna sarja",
"LabelExpandSubSeries": "Laajenna alisarja",
@@ -335,15 +364,22 @@
"LabelFontItalic": "Kursiivi",
"LabelFontScale": "Kirjasintyyppien skaalautuminen",
"LabelFontStrikethrough": "Yliviivattu",
"LabelFormat": "Muoto",
"LabelFull": "Täynnä",
"LabelGenre": "Lajityyppi",
"LabelGenres": "Lajityypit",
"LabelHardDeleteFile": "Kova tiedostojen poisto",
"LabelHasEbook": "Sillä on s-kirja",
"LabelHasSupplementaryEbook": "Sillä on täydentävän s-kirjan",
"LabelHideSubtitles": "Piilota tekstitykset",
"LabelHighestPriority": "Tärkein",
"LabelHost": "Isäntä",
"LabelHour": "Tunti",
"LabelHours": "Tunnit",
"LabelIcon": "Kuvake",
"LabelImageURLFromTheWeb": "Kuvan verkko-osoite",
"LabelInProgress": "Kesken",
"LabelIncludeInTracklist": "Sisällytä kappalelistaan",
"LabelIncomplete": "Keskeneräinen",
"LabelInterval": "Väli",
"LabelIntervalCustomDailyWeekly": "Mukautettu päivittäinen/viikoittainen",
@@ -354,6 +390,8 @@
"LabelIntervalEvery6Hours": "6 tunnin välein",
"LabelIntervalEveryDay": "Joka päivä",
"LabelIntervalEveryHour": "Joka tunti",
"LabelIntervalEveryMinute": "Joka minuutti",
"LabelInvert": "Saa käänteiseksi",
"LabelItem": "Kohde",
"LabelLanguage": "Kieli",
"LabelLanguageDefaultServer": "Palvelimen oletuskieli",
@@ -361,6 +399,7 @@
"LabelLastBookAdded": "Viimeisin lisätty kirja",
"LabelLastBookUpdated": "Viimeisin päivitetty kirja",
"LabelLastSeen": "Nähty viimeksi",
"LabelLastTime": "Viimeinen kerta",
"LabelLastUpdate": "Viimeisin päivitys",
"LabelLayout": "Asettelu",
"LabelLayoutSinglePage": "Yksi sivu",
@@ -368,15 +407,21 @@
"LabelLess": "Vähemmän",
"LabelLibrariesAccessibleToUser": "Käyttäjälle saatavilla olevat kirjastot",
"LabelLibrary": "Kirjasto",
"LabelLibraryFilterSublistEmpty": "Ei {0}",
"LabelLibraryItem": "Kirjaston kohde",
"LabelLibraryName": "Kirjaston nimi",
"LabelLimit": "Raja",
"LabelLineSpacing": "Riviväli",
"LabelListenAgain": "Kuuntele uudelleen",
"LabelLogLevelDebug": "Viankorjaus",
"LabelLogLevelInfo": "Tiedot",
"LabelLogLevelWarn": "Varoita",
"LabelLogLevelWarn": "Varoitus",
"LabelLookForNewEpisodesAfterDate": "Etsi uusia jaksoja tämän päivämäärän jälkeen",
"LabelLowestPriority": "Vähiten tärkeä",
"LabelMatchExistingUsersBy": "Vastaa olemassa olevia käyttäjiä mukaan",
"LabelMatchExistingUsersByDescription": "Käytetään olemassa olevien käyttäjien yhdistämiseen. Kun yhteys on muodostettu, käyttäjät saavat yksilöllisen tunnuksen SSO-palveluntarjoajaltasi",
"LabelMaxEpisodesToDownload": "Jaksojen maksimilatausmäärä. 0 poistaa rajoituksen.",
"LabelMaxEpisodesToDownloadPerCheck": "Enintään # ladattavia uusia jaksoja tarkistusta kohden",
"LabelMaxEpisodesToKeep": "Säilytettävien jaksojen enimmäismäärä",
"LabelMaxEpisodesToKeepHelp": "Jos arvona on 0, enimmäisrajaa ei ole. Kun uusi jakso ladataan automaattisesti, vanhin jakso poistetaan, jos jaksoja on yli X. Tämä poistaa vain yhden jakson uutta latauskertaa kohden.",
"LabelMediaPlayer": "Mediasoitin",
@@ -387,8 +432,11 @@
"LabelMetadataProvider": "Kuvailutietojen toimittaja",
"LabelMinute": "Minuutti",
"LabelMinutes": "Minuutit",
"LabelMissing": "Puuttuu",
"LabelMissingEbook": "Ei e-kirjaa",
"LabelMissing": "Puuttuva",
"LabelMissingEbook": "Sillä ei ole s-kirjaa",
"LabelMissingSupplementaryEbook": "Ei täydentävää s-kirjaa",
"LabelMobileRedirectURIs": "Sallitut mobiiliuudelleenohjaus-URI:t",
"LabelMobileRedirectURIsDescription": "Tämä on valkoluettelo kelvollisista uudelleenohjaus-URI:ista mobiilisovelluksille. Oletusarvo on <code>äänikirjahylly://oauth</code>, jonka voit poistaa tai täydentää ylimääräisillä URI:lla kolmannen osapuolen sovellusten integrointia varten. Asteriskin (<code>*</code>) käyttäminen ainoana merkintänä sallii minkä tahansa URI:n.",
"LabelMore": "Lisää",
"LabelMoreInfo": "Lisätietoja",
"LabelName": "Nimi",
@@ -396,7 +444,7 @@
"LabelNarrators": "Lukijat",
"LabelNew": "Uusi",
"LabelNewPassword": "Uusi salasana",
"LabelNewestAuthors": "Uusimmat kirjailijat",
"LabelNewestAuthors": "Uusimmat tekijät",
"LabelNewestEpisodes": "Uusimmat jaksot",
"LabelNextBackupDate": "Seuraava varmuuskopiointipäivämäärä",
"LabelNextScheduledRun": "Seuraava ajastettu suorittaminen",
@@ -405,25 +453,36 @@
"LabelNotFinished": "Ei valmis",
"LabelNotStarted": "Ei aloitettu",
"LabelNotes": "Muistiinpanoja",
"LabelNotificationAppriseURL": "Apprise osoitteet (URL)",
"LabelNotificationAvailableVariables": "Käytettävissä olevat muuttujat",
"LabelNotificationBodyTemplate": "Runkomalli",
"LabelNotificationEvent": "Ilmoitustapahtuma",
"LabelNotificationTitleTemplate": "Otsikkomalli",
"LabelNotificationsMaxFailedAttempts": "Epäonnistuneiden yritysten enimmäismäärä",
"LabelNotificationsMaxFailedAttemptsHelp": "Ilmoitukset poistetaan käytöstä, jos niiden lähettäminen epäonnistuu näin monta kertaa",
"LabelNotificationsMaxQueueSize": "Ilmoitustapahtumajonon enimmäispituus",
"LabelNotificationsMaxQueueSizeHelp": "Tapahtumat on rajoitettu ampumaan yksi sekunnissa. Tapahtumat ohitetaan, jos jono on enimmäiskoko. Tämä estää ilmoitusten roskapostin.",
"LabelNumberOfBooks": "Kirjojen määrä",
"LabelNumberOfEpisodes": "Jaksojen määrä",
"LabelNumberOfEpisodes": "# jaksoja",
"LabelOpenIDAdvancedPermsClaimDescription": "OpenID-vaatimuksen nimi, joka sisältää lisäoikeudet sovelluksen käyttäjän toimiin, joita sovelletaan muihin kuin järjestelmänvalvojan rooleihin (<b>jos määritetty</b>). Jos vaatimus puuttuu vastauksesta, pääsy ABS:iin evätään. Jos yksittäinen vaihtoehto puuttuu, sitä käsitellään <code>false</code>-arvona. Varmista, että identiteetin tarjoajan vaatimus vastaa odotettua rakennetta:",
"LabelOpenIDClaims": "Jätä seuraavat vaihtoehdot tyhjiksi, jos haluat poistaa edistyneen ryhmän ja lupien määrityksen käytöstä ja määrittää sitten automaattisesti käyttäjäryhmän.",
"LabelOpenIDGroupClaimDescription": "Sen OpenID-vaatimuksen nimi, joka sisältää luettelon käyttäjäryhmistä. Kutsutaan yleisesti <code>ryhmiksi</code>. <b>Jos se on määritetty</b>, sovellus jakaa automaattisesti roolit käyttäjän ryhmäjäsenyyksien perusteella, jos näiden ryhmien nimet eivät erota kirjainkoosta \"admin\", \"user\" tai \"guest\" vaatimuksessa. Vaatimuksen tulee sisältää luettelo, ja jos käyttäjä kuuluu useisiin ryhmiin, sovellus määrittää korkeinta pääsytasoa vastaavan roolin. Jos mikään ryhmä ei täsmää, pääsy evätään.",
"LabelOpenRSSFeed": "Avaa RSS-syöte",
"LabelOverwrite": "Korvaa",
"LabelPaginationPageXOfY": "Sivu {0}/{1}",
"LabelPassword": "Salasana",
"LabelPath": "Polku",
"LabelPermanent": "Pysyvä",
"LabelPermissionsAccessAllLibraries": "Käyttöoikeudet kaikkiin kirjastoihin",
"LabelPermissionsAccessAllTags": "Saa käyttää kaikkia tunnisteita",
"LabelPermissionsAccessAllTags": "On pääsy kaikkiin tunnisteihin",
"LabelPermissionsAccessExplicitContent": "Saa käyttää aikuisille tarkoitettua sisältöä",
"LabelPermissionsCreateEreader": "Voi luoda e-lukijan",
"LabelPermissionsDelete": "Voi poistaa",
"LabelPermissionsDownload": "Voi ladata",
"LabelPermissionsUpdate": "Voi päivittää",
"LabelPermissionsUpload": "Voi lähettää",
"LabelPersonalYearReview": "Vuotesi katsauksessa ({0})",
"LabelPhotoPathURL": "Valokuvan polku/URL-osoite",
"LabelPlayMethod": "Toistotapa",
"LabelPlayerChapterNumberMarker": "{0}/{1}",
"LabelPlaylists": "Soittolistat",
@@ -432,82 +491,296 @@
"LabelPodcastType": "Podcastien tyyppi",
"LabelPodcasts": "Podcastit",
"LabelPort": "Portti",
"LabelPrimaryEbook": "Ensisijainen e-kirja",
"LabelPrefixesToIgnore": "Ohitettavat etuliitteet (kirjainkoolla ei väliä)",
"LabelPreventIndexing": "Estä syötteesi olemasta iTunesin ja Googlen podcast-hakemistojen indeksoinnin kohteena",
"LabelPrimaryEbook": "Ensisijainen s-kirja",
"LabelProgress": "Edistyminen",
"LabelProvider": "Toimittaja",
"LabelProviderAuthorizationValue": "Valtuutusotsikon arvo",
"LabelPubDate": "Julkaisupäivä",
"LabelPublishYear": "Julkaisuvuosi",
"LabelPublishedDate": "Julkaistu {0}",
"LabelPublishedDecade": "Julkaistu vuosikymmen",
"LabelPublishedDecades": "Julkaistu vuosikymmenet",
"LabelPublisher": "Julkaisija",
"LabelPublishers": "Julkaisijat",
"LabelRSSFeedCustomOwnerEmail": "Mukautettu omistajan sähköposti",
"LabelRSSFeedCustomOwnerName": "Mukautettu omistajan nimi",
"LabelRSSFeedOpen": "RSS-syöte avoin",
"LabelRSSFeedPreventIndexing": "Estä indeksointi",
"LabelRSSFeedSlug": "RSS-syöte Slug",
"LabelRSSFeedURL": "RSS-syötteen URL-osoite",
"LabelRandomly": "Satunnaisesti",
"LabelReAddSeriesToContinueListening": "Lisää sarja uudelleen jatkaaksesi kuuntelua",
"LabelRead": "Lue",
"LabelReadAgain": "Lue uudelleen",
"LabelReadEbookWithoutProgress": "Lue e-kirja tallentamatta edistymistietoja",
"LabelReadEbookWithoutProgress": "Lue s-kirja tallentamatta edistymistietoja",
"LabelRecentSeries": "Viimeisimmät sarjat",
"LabelRecentlyAdded": "Viimeeksi lisätyt",
"LabelRecommended": "Suositeltu",
"LabelRedo": "Tee uudelleen",
"LabelRegion": "Alue",
"LabelReleaseDate": "Julkaisupäivä",
"LabelRemoveAllMetadataAbs": "Poista kaikki metadata.abs-tiedostot",
"LabelRemoveAllMetadataJson": "Poista kaikki metadata.json-tiedostot",
"LabelRemoveCover": "Poista kansikuva",
"LabelRemoveMetadataFile": "Poista metatietotiedostot kirjaston kohdekansioista",
"LabelRemoveMetadataFileHelp": "Poista kaikki metadata.json- ja metadata.abs-tiedostot {0} kansiostasi.",
"LabelRowsPerPage": "Rivejä sivulla",
"LabelSearchTerm": "Hakusana",
"LabelSearchTitle": "Etsi otsikko",
"LabelSearchTitleOrASIN": "Etsi otsikko tai ASIN",
"LabelSeason": "Kausi",
"LabelSeasonNumber": "Kausi #{0}",
"LabelSelectAll": "Valitse kaikki",
"LabelSelectAllEpisodes": "Valitse kaikki jaksot",
"LabelSelectEpisodesShowing": "Valitse {0} näytettävää jaksoa",
"LabelSelectUsers": "Valitse käyttäjät",
"LabelSendEbookToDevice": "Lähetä s-kirja kohteeseen...",
"LabelSequence": "Sekvenssi",
"LabelSerial": "Sarja",
"LabelSeries": "Sarja",
"LabelSeriesName": "Sarjan nimi",
"LabelSeriesProgress": "Sarjan edistyminen",
"LabelServerLogLevel": "Palvelimen lokitaso",
"LabelServerYearReview": "Palvelimen vuosi katsauksessa ({0})",
"LabelSetEbookAsPrimary": "Aseta ensisijaiseksi",
"LabelSetEbookAsSupplementary": "Aseta täydentäväksi",
"LabelSettingsAllowIframe": "Salli upottaminen iframe-kehykseen",
"LabelSettingsAudiobooksOnly": "Vain äänikirjat",
"LabelSettingsAudiobooksOnlyHelp": "Tämän asetuksen käyttöönotto ohittaa s-kirjatiedostot, elleivät ne ole äänikirjakansiossa, jolloin ne asetetaan täydentäviksi s-kirjoiksi",
"LabelSettingsBookshelfViewHelp": "Skeuomorfinen muotoilu puisilla hyllyillä",
"LabelSettingsChromecastSupport": "Chromecast-tuki",
"LabelSettingsDateFormat": "Päivämäärän muoto",
"LabelSettingsEnableWatcherHelp": "Ottaa käyttöön kohteiden automaattisen lisäämisen ja päivityksen kun tiedostomuutoksia havaitaan. *Tarvitsee palvelimen uudelleenkäynnistyksen",
"LabelSettingsEpubsAllowScriptedContent": "Salli komentosarjamuotoinen sisältö epubissa",
"LabelSettingsEpubsAllowScriptedContentHelp": "Salli epub-tiedostojen suorittaa komentosarjoja. On suositeltavaa pitää tämä asetus pois käytöstä, ellet luota epub-tiedostojen lähteeseen.",
"LabelSettingsExperimentalFeatures": "Kokeelliset ominaisuudet",
"LabelSettingsExperimentalFeaturesHelp": "Kehitettävissä olevat ominaisuudet, jotka voivat hyödyntää palautettasi ja auttaa testaamisessa. Napsauta avataksesi github-keskustelun.",
"LabelSettingsFindCovers": "Etsi kansikuvia",
"LabelSettingsFindCoversHelp": "Jos äänikirjassasi ei ole kansion sisällä upotettua kantta tai kansikuvaa, skanneri yrittää löytää kannen.<br>Huomaa: Tämä pidentää skannausaikaa",
"LabelSettingsHideSingleBookSeries": "Piilota yksittäinen kirjasarja",
"LabelSettingsHideSingleBookSeriesHelp": "Sarjat, joissa on yksi kirja, piilotetaan sarjasivulta ja kotisivujen hyllyiltä.",
"LabelSettingsHomePageBookshelfView": "Kotisivu käyttää kirjahyllynäkymää",
"LabelSettingsLibraryBookshelfView": "Kirjasto käyttää kirjahyllynäkymää",
"LabelSettingsLibraryMarkAsFinishedPercentComplete": "Valmistumisprosentti on suurempi kuin",
"LabelSettingsLibraryMarkAsFinishedTimeRemaining": "Jäljellä oleva aika on alle (sekuntia)",
"LabelSettingsLibraryMarkAsFinishedWhen": "Merkitse mediakohde valmiiksi, kun",
"LabelSettingsOnlyShowLaterBooksInContinueSeries": "Ohita aiemmat kirjat Jatka sarjassa",
"LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "Jatka sarja -kotisivun hyllyssä näkyy ensimmäinen kirja, jota ei ole aloitettu sarjoissa, joissa on vähintään yksi kirja valmiina eikä yhtään kirjaa kesken. Tämän asetuksen ottaminen käyttöön jatkaa sarjaa kauimpana valmistuneesta kirjasta ensimmäisen aloittamattoman kirjan sijaan.",
"LabelSettingsParseSubtitles": "Jäsennä tekstitykset",
"LabelSettingsParseSubtitlesHelp": "Pura tekstitykset äänikirjojen kansioiden nimistä.<br>Tekstitys on erotettava toisistaan merkillä \"-\"<br>ts. \"Kirjan otsikko - Tekstitys täällä\" on alaotsikko \"Tekstitys täällä\"",
"LabelSettingsPreferMatchedMetadata": "Pidä mieluummin täsmäävät metatiedot",
"LabelSettingsPreferMatchedMetadataHelp": "Täsmäävät tiedot ohittavat kohteen tiedot käytettäessä Pikatäsmäystä. Oletuksena Pikatäsmäys täyttää vain puuttuvat tiedot.",
"LabelSettingsSkipMatchingBooksWithASIN": "Ohita täsmäävät kirjat, joilla on jo ASIN",
"LabelSettingsSkipMatchingBooksWithISBN": "Ohita täsmäävät kirjat, joilla on jo ISBN",
"LabelSettingsSortingIgnorePrefixes": "Jätä etuliitteet huomioimatta lajittelussa",
"LabelSettingsSortingIgnorePrefixesHelp": "eli etuliitteelle \"tämän\" kirjan nimi \"Tämän kirjan nimi\" lajitellaan muodossa \"Kirjan nimi, Tämän\"",
"LabelSettingsSquareBookCovers": "Käytä neliömäisiä kirjankansia",
"LabelSettingsSquareBookCoversHelp": "Käytä mieluummin neliömäisiä kansia kuin tavallisia 1,6:1 kirjankansia",
"LabelSettingsStoreCoversWithItem": "Säilytyskannet esineen kanssa",
"LabelSettingsStoreCoversWithItemHelp": "Oletusarvoisesti kannet tallennetaan kansioon /metadata/items, ja tämän asetuksen ottaminen käyttöön tallentaa kannet kirjaston kohdekansioon. Vain yksi tiedosto nimeltä \"cover\" säilytetään",
"LabelSettingsStoreMetadataWithItem": "Tallenna metatiedot kohteen kanssa",
"LabelSettingsStoreMetadataWithItemHelp": "Oletuksena metatietotiedostot tallennetaan kansioon /metadata/items, ja tämän asetuksen ottaminen käyttöön tallentaa metatietotiedostot kirjastosi kohdekansioihin",
"LabelSettingsTimeFormat": "Aikamuoto",
"LabelShare": "Jaa",
"LabelShareDownloadableHelp": "Antaa käyttäjien, joilla on jakolinkki, ladata kirjastokohteen zip-tiedoston.",
"LabelShareOpen": "Jaa Avoin",
"LabelShareURL": "Jaa URL-osoite",
"LabelShowAll": "Näytä kaikki",
"LabelShowSeconds": "Näytä sekunnit",
"LabelShowSubtitles": "Näytä tekstitykset",
"LabelSize": "Koko",
"LabelSleepTimer": "Uniajastin",
"LabelSlug": "Slug",
"LabelSortAscending": "Nouseva",
"LabelSortDescending": "Laskeva",
"LabelStart": "Aloita",
"LabelStartTime": "Aloitusaika",
"LabelStarted": "Aloitettu",
"LabelStartedAt": "Aloitettu",
"LabelStatsAudioTracks": "Ääniraidat",
"LabelStatsAuthors": "Tekijät",
"LabelStatsBestDay": "Paras päivä",
"LabelStatsDailyAverage": "Päivittäinen keskiarvo",
"LabelStatsDays": "Päivää",
"LabelStatsDaysListened": "Päivää kuunneltu",
"LabelStatsHours": "Tunnit",
"LabelStatsInARow": "peräjälkeen",
"LabelStatsItemsFinished": "Valmiit tuotteet",
"LabelStatsItemsInLibrary": "Kohteet kirjastossa",
"LabelStatsMinutes": "minuuttia",
"LabelStatsMinutesListening": "Minuuttia kuunneltu",
"LabelStatsOverallDays": "Päivät kokonaisuudessaan",
"LabelStatsOverallHours": "Tunnit kokonaisuudessaan",
"LabelStatsWeekListening": "Viikon aikana kuunneltu",
"LabelSubtitle": "Tekstitys",
"LabelSupportedFileTypes": "Tuetut tiedostotyypit",
"LabelTag": "Tägi",
"LabelTags": "Tägit",
"LabelTagsAccessibleToUser": "Tunnisteet käyttäjän käytettävissä",
"LabelTagsNotAccessibleToUser": "Tunnisteet ei käyttäjien käytettävissä",
"LabelTasks": "Tehtävät käynnissä",
"LabelTextEditorBulletedList": "Luettelomerkitty luettelo",
"LabelTextEditorLink": "Linkki",
"LabelTextEditorNumberedList": "Numeroitu luettelo",
"LabelTextEditorUnlink": "Poista linkitys",
"LabelTheme": "Teema",
"LabelThemeDark": "Tumma",
"LabelThemeLight": "Kirkas",
"LabelTimeBase": "Aika-alusta",
"LabelTimeDurationXHours": "{0} tuntia",
"LabelTimeDurationXMinutes": "{0} minuuttia",
"LabelTimeDurationXSeconds": "{0} sekuntia",
"LabelTimeInMinutes": "Aika minuutteina",
"LabelTimeLeft": "{0} jäljellä",
"LabelTimeListened": "Aika kuunneltu",
"LabelTimeListenedToday": "Kuunneltu aika tänään",
"LabelTimeRemaining": "{0} jäljellä",
"LabelTimeToShift": "Vaihtoaika sekunteina",
"LabelTitle": "Nimi",
"LabelToolsEmbedMetadata": "Upota metatiedot",
"LabelToolsEmbedMetadataDescription": "Upota metatiedot äänitiedostoihin, mukaan lukien kansikuva ja luvut.",
"LabelToolsM4bEncoder": "M4B Enkooderi",
"LabelToolsMakeM4b": "Tee M4B-äänikirjatiedosto",
"LabelToolsMakeM4bDescription": "Luo .M4B-äänikirjatiedosto, joka sisältää upotetut metatiedot, kansikuvan ja luvut.",
"LabelToolsSplitM4b": "Jaa M4B MP3:ksi",
"LabelToolsSplitM4bDescription": "Luo MP3-tiedostoja M4B:stä, jaettuna lukujen mukaan, upotetulla metatiedolla, kansikuvalla ja luvuilla.",
"LabelTotalDuration": "Kokonaiskesto",
"LabelTotalTimeListened": "Yhteensä kuunneltu aika",
"LabelTrackFromFilename": "Raita tiedostonimestä",
"LabelTrackFromMetadata": "Raita metatiedoista",
"LabelTracks": "Raidat",
"LabelTracksMultiTrack": "Moniraitainen",
"LabelTracksNone": "Ei raitoja",
"LabelTracksSingleTrack": "Yksiraitainen",
"LabelTrailer": "Traileri",
"LabelType": "Tyyppi",
"LabelUnabridged": "Lyhentämätön",
"LabelUndo": "Kumoa",
"LabelUnknown": "Tuntematon",
"LabelUnknownPublishDate": "Tuntematon julkaisupäivämäärä",
"LabelUpdateCover": "Päivitä kansikuva",
"LabelUpdateCoverHelp": "Salli valittujen kirjojen olemassa olevien kansien päällekirjoittaminen, kun osuma löytyy",
"LabelUpdateDetails": "Päivitä yksityiskohdat",
"LabelUpdateDetailsHelp": "Salli valittujen kirjojen olemassa olevien tietojen korvaaminen, kun osuma löytyy",
"LabelUpdatedAt": "Päivitetty",
"LabelUploaderDragAndDrop": "Vedä ja pudota tiedostoja tai kansioita",
"LabelUploaderDragAndDropFilesOnly": "Vedä ja pudota tiedostoja",
"LabelUploaderDropFiles": "Pudota tiedostot",
"LabelUploaderItemFetchMetadataHelp": "Nouda automaattisesti otsikko, tekijä ja sarja",
"LabelUseAdvancedOptions": "Käytä edistyneitä vaihtoehtoja",
"LabelUseChapterTrack": "Käytä luvunraitaa",
"LabelUseFullTrack": "Käytä täyttä raitaa",
"LabelUseZeroForUnlimited": "Käytä 0 rajatonta varten",
"LabelUser": "Käyttäjä",
"LabelUsername": "Käyttäjätunnus",
"LabelValue": "Arvo",
"LabelVersion": "Versio",
"LabelViewBookmarks": "Katso kirjanmerkit",
"LabelViewChapters": "Katso luvut",
"LabelViewPlayerSettings": "Katso soittimen asetukset",
"LabelViewQueue": "Katso soittimen jono",
"LabelVolume": "Äänenvoimakkuus",
"LabelWebRedirectURLsDescription": "Valtuuta nämä URL-osoitteet OAuth-palveluntarjoajassasi sallimaan uudelleenohjauksen takaisin verkkosovellukseen sisäänkirjautumisen jälkeen:",
"LabelWebRedirectURLsSubfolder": "Alikansio URL-osoitteiden uudelleenohjaukselle",
"LabelWeekdaysToRun": "Ajettavat arkipäivät",
"LabelXBooks": "{0} kirjaa",
"LabelXItems": "{0} kohdetta",
"LabelYearReviewHide": "Piilota vuosi arvostelussa",
"LabelYearReviewShow": "Näytä vuosi arvostelussa",
"LabelYourAudiobookDuration": "Äänikirjan kesto",
"LabelYourBookmarks": "Kirjanmerkkisi",
"LabelYourPlaylists": "Soittolistasi",
"LabelYourProgress": "Edistymisesi",
"MessageAddToPlayerQueue": "Lisää soittimen jonoon",
"MessageAppriseDescription": "Käyttääksesi tätä toimintoa tarvitset <a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">Apprise API</a> -instanssin tai rajapinnan joka käsittelee samoja pyyntöjä. <br />Apprise rajapinnan osoite tulee olla täysi URL polku ilmoituksen lähetykseen, esim. jos rajapinta on osoitteessa <code>http://192.168.1.1:8337</code>,niin arvoksi tulee antaa <code>http://192.168.1.1:8337/notify</code>.",
"MessageBackupsDescription": "Varmuuskopiot sisältävät käyttäjät, käyttäjien edistymisen, kirjastokohteiden tiedot, palvelinasetukset ja <code>/metadata/items</code>- ja <code>/metadata/authors</code> -kansioihin tallennetut kuvat. Varmuuskopiot <strong>eivät sisällä</strong> kirjastosi kansioihin tallennettuja tiedostoja.",
"MessageBackupsLocationEditNote": "Huomautus: Varmuuskopion sijainnin päivittäminen ei siirrä tai muokkaa olemassa olevia varmuuskopioita",
"MessageBackupsLocationNoEditNote": "Huomautus: Varmuuskopion sijainti asetetaan ympäristömuuttujan kautta, eikä sitä voi muuttaa tässä.",
"MessageBackupsLocationPathEmpty": "Varmuuskopiointisijainnin polku ei voi olla tyhjä",
"MessageBatchEditPopulateMapDetailsAllHelp": "Täytä käytössä olevat kentät tiedoilla kaikista kohteista. Kentät, joilla on useita arvoja, yhdistetään",
"MessageBatchEditPopulateMapDetailsItemHelp": "Täytä käytössä olevat karttayksityiskohtakentät tämän kohteen tiedoilla",
"MessageBatchQuickMatchDescription": "Pikatäsmäys yrittää lisätä puuttuvat kannet ja metatiedot valituille kohteille. Ota käyttöön alla olevat vaihtoehdot, jotta Pikatäsmäys korvaa olemassa olevat kannet ja/tai metatiedot.",
"MessageBookshelfNoCollections": "Et ole vielä tehnyt kokoelmia",
"MessageBookshelfNoCollectionsHelp": "Kokoelmat ovat julkisia. Kaikki käyttäjät, joilla on pääsy kirjastoon, voivat nähdä ne.",
"MessageBookshelfNoRSSFeeds": "RSS-syötteitä ei ole auki",
"MessageBookshelfNoResultsForFilter": "Ei tuloksia suodattimelle \"{0}: {1}\"",
"MessageBookshelfNoResultsForQuery": "Ei tuloksia kyselylle",
"MessageBookshelfNoSeries": "Sinulla ei ole sarjoja",
"MessageChapterEndIsAfter": "Luvun loppu sijaitsee äänikirjan lopun jälkeen",
"MessageChapterErrorFirstNotZero": "Ensimmäisen luvun tulee alkaa nollasta",
"MessageChapterErrorStartGteDuration": "Epäkelvollinen aloitusaika; on oltava lyhyempi kuin äänikirjan kesto",
"MessageChapterErrorStartLtPrev": "Epäkelvollinen aloitusaika; on oltava suurempi tai yhtä suuri kuin edellisen luvun aloitusaika",
"MessageChapterStartIsAfter": "Luku alkaa äänikirjan lopun jälkeen",
"MessageCheckingCron": "Tarkistetaan cronia...",
"MessageConfirmCloseFeed": "Oletko varma, että haluat sulkea tämän syötteen?",
"MessageConfirmDeleteBackup": "Oletko varma, että haluat poistaa varmuuskopion {0}:lle?",
"MessageConfirmDeleteDevice": "Oletko varma, että haluat poistaa s-lukulaitteen \"{0}\"?",
"MessageConfirmDeleteFile": "Tämä poistaa tiedoston tiedostojärjestelmästäsi. Oletko varma?",
"MessageConfirmDeleteLibrary": "Oletko varma, että haluat poistaa kirjaston \"{0}\" pysyvästi?",
"MessageConfirmDeleteLibraryItem": "Tämä poistaa kirjastokohteen tietokannasta ja tiedostojärjestelmästäsi. Oletko varma?",
"MessageConfirmDeleteLibraryItems": "Tämä poistaa {0} kirjastokohdetta tietokannasta ja tiedostojärjestelmästäsi. Oletko varma?",
"MessageConfirmDeleteMetadataProvider": "Oletko varma, että haluat poistaa mukautetun metatietojen tarjoajan \"{0}\"?",
"MessageConfirmDeleteNotification": "Oletko varma, että haluat poistaa tämän ilmoituksen?",
"MessageConfirmDeleteSession": "Oletko varma, että haluat poistaa tämän istunnon?",
"MessageConfirmEmbedMetadataInAudioFiles": "Oletko varma, että haluat upottaa metatiedot {0} äänitiedostoihin?",
"MessageConfirmForceReScan": "Oletko varma, että haluat pakottaa uudelleenskannauksen?",
"MessageConfirmMarkAllEpisodesFinished": "Oletko varma, että haluat merkitä kaikki jaksot päättyneiksi?",
"MessageConfirmMarkAllEpisodesNotFinished": "Oletko varma, että haluat merkitä kaikki jaksot ei-valmiiksi?",
"MessageConfirmMarkItemFinished": "Oletko varma, että haluat merkitä \"{0}\":n valmiiksi?",
"MessageConfirmMarkItemNotFinished": "Oletko varma, että haluat merkitä \"{0}\":n ei-valmiiksi?",
"MessageConfirmMarkSeriesFinished": "Oletko varma, että haluat merkitä kaikki tämän sarjan kirjat valmiiksi?",
"MessageConfirmMarkSeriesNotFinished": "Oletko varma, että haluat merkitä kaikki tämän sarjan kirjat ei-valmiiksi?",
"MessageConfirmNotificationTestTrigger": "Käynnistetäänkö tämä ilmoitus testitiedoilla?",
"MessageConfirmPurgeCache": "'Tyhjennä välimuisti' poistaa koko hakemiston sijainnilla <code>/metadata/cache</code>. <br /><br />Oletko varma, että haluat poistaa välimuistihakemiston?",
"MessageConfirmPurgeItemsCache": "'Tyhjennä kohteiden välimuisti' poistaa koko hakemiston sijainnilla <code>/metadata/cache/items</code>.<br />Oletko varma?",
"MessageConfirmQuickEmbed": "Varoitus! Pikaupottaminen ei varmuuskopioi äänitiedostojasi. Varmista, että sinulla on varmuuskopio äänitiedostoistasi. <br><br>Haluatko jatkaa?",
"MessageConfirmQuickMatchEpisodes": "Jaksojen pikatäsmääminen korvaa tiedot, jos vastaavuus löytyy. Vain täsmäämättömät jaksot päivitetään. Oletko varma?",
"MessageConfirmReScanLibraryItems": "Oletko varma, että haluat skannata uudelleen {0} kohdetta?",
"MessageConfirmRemoveAllChapters": "Oletko varma, että haluat poistaa kaikki jaksot?",
"MessageConfirmRemoveAuthor": "Oletko varma, että haluat poistaa tekijän \"{0}\"?",
"MessageConfirmRemoveCollection": "Oletko varma, että haluat poistaa kokoelman \"{0}\"?",
"MessageConfirmRemoveEpisode": "Oletko varma, että haluat poistaa jakson \"{0}\"?",
"MessageConfirmRemoveEpisodes": "Oletko varma, että haluat poistaa {0} jaksoa?",
"MessageConfirmRemoveListeningSessions": "Oletko varma, että haluat poistaa {0} kuuntelukertaa?",
"MessageConfirmRemoveMetadataFiles": "Oletko varma, että haluat poistaa kaikki metadata.{0}-tiedostot kirjaston kohdekansioista?",
"MessageConfirmRemoveNarrator": "Oletko varma, että haluat poistaa kertojan \"{0}\"?",
"MessageConfirmRemovePlaylist": "Oletko varma, että haluat poistaa soittolistan \"{0}\"?",
"MessageConfirmRenameGenre": "Oletko varma, että haluat nimetä lajityypin \"{0}\" uudelleen \"{1}\":ksi kaikille kohteille?",
"MessageConfirmRenameGenreMergeNote": "Huomautus: Tämä lajityyppi on jo olemassa, joten ne yhdistetään.",
"MessageConfirmRenameGenreWarning": "Varoitus! Samanlainen lajityyppi eri kotelolla on jo olemassa \"{0}\".",
"MessageConfirmRenameTag": "Oletko varma, että haluat nimetä tunnisteen \"{0}\" uudelleen \"{1}\":ksi kaikille kohteille?",
"MessageConfirmRenameTagMergeNote": "Huomautus: Tämä tunniste on jo olemassa, joten ne yhdistetään.",
"MessageConfirmRenameTagWarning": "Varoitus! Samanlainen tunniste eri kotelolla on jo olemassa \"{0}\".",
"MessageConfirmResetProgress": "Oletko varma, että haluat nollata edistymisesi?",
"MessageConfirmSendEbookToDevice": "Oletko varma, että haluat lähettää {0} s-kirjan \"{1}\" laitteeseen \"{2}\"?",
"MessageConfirmUnlinkOpenId": "Oletko varma, että haluat poistaa tämän käyttäjän linkityksen OpenID:stä?",
"MessageDaysListenedInTheLastYear": "{0} kuunneltua päivää viime vuonna",
"MessageDownloadingEpisode": "Ladataan jaksoa",
"MessageDragFilesIntoTrackOrder": "Vedä tiedostot oikeaan raitojen järjestykseen",
"MessageEmbedFailed": "Upotus epäonnistui!",
"MessageEmbedFinished": "Upotus valmis!",
"MessageEmbedQueue": "Jonossa metatietojen upottamista varten ({0} jonossa)",
"MessageEpisodesQueuedForDownload": "{0} jaksoa on latausjonossa",
"MessageEreaderDevices": "S-kirjojen toimituksen varmistamiseksi sinun on ehkä lisättävä yllä oleva sähköpostiosoite kelvolliseksi lähettäjäksi jokaiselle alla luetellulle laitteelle.",
"MessageFeedURLWillBe": "Syötteen URL tulee olemaan {0}",
"MessageFetching": "Haetaan...",
"MessageForceReScanDescription": "skannaa kaikki tiedostot uudelleen kuten uusi tarkistus. Äänitiedoston ID3-tunnisteet, OPF-tiedostot ja tekstitiedostot skannataan uusina.",
"MessageImportantNotice": "Tärkeä huomautus!",
"MessageInsertChapterBelow": "Syötä luku alle",
"MessageItemsSelected": "{0} kohdetta valittu",
"MessageItemsUpdated": "{0} kohdetta päivitetty",
"MessageJoinUsOn": "Liity meihin",
"MessageLoading": "Ladataan...",
"MessageLoadingFolders": "Ladataan kansioita...",
"MessageLogsDescription": "Lokitiedot tallennetaan kansioon <code>/metadata/logs</code> JSON-tiedostoina. Kaatumislokit tallennetaan kansioon <code>/metadata/logs/crash_logs.txt</code>.",
"MessageM4BFailed": "M4B epäonnistui!",
"MessageM4BFinished": "M4B valmis!",
"MessageMarkAsFinished": "Merkitse valmiiksi",
"MessageNoBookmarks": "Ei kirjanmerkkejä",
"MessageNoChapters": "Ei kappaleita",
"MessageNoCollections": "Ei kokoelmia",
"MessageNoCoversFound": "Kansikuvia ei löydetty",
"MessageNoGenres": "Ei lajityyppejä",
"MessageNoItems": "Ei kohteita",
@@ -517,10 +790,22 @@
"MessageNoUpdatesWereNecessary": "Päivityksiä ei tarvittu",
"MessageNoUserPlaylists": "Sinulla ei ole soittolistoja",
"MessageOr": "tai",
"MessagePodcastSearchField": "Syötä hakutermi tai RSS-syötteen URL-osoite",
"MessageQuickMatchAllEpisodes": "Pikatäsmää kaikki jaksot",
"MessageRemoveUserWarning": "Oletko varma, että haluat poistaa käyttäjän \"{0}\" pysyvästi?",
"MessageReportBugsAndContribute": "Ilmoita virheistä, toivo ominaisuuksia ja osallistu",
"MessageResetChaptersConfirm": "Oletko varma, että haluat nollata luvut ja kumota tekemäsi muutokset?",
"MessageRestoreBackupConfirm": "Oletko varma, että haluat palauttaa varmuuskopion, joka on luotu",
"MessageScheduleLibraryScanNote": "Suurimmalle osaa käyttäjistä on suositeltavaa jättää tämä ominaisuus pois päältä ja säilyttää kansiotarkkailu päällä. Kansiotarkkailu havaitsee automaattisesti tiedostomuutokset kirjaston kansioissa. Kansiotarkkailu ei toimi kaikille tiedostojärjestelmille (kuten NFS), jolloin voidaan käyttää ajastettuja kirjastoskannauksia.",
"MessageTaskFailed": "Epäonnistunut",
"MessageWatcherIsDisabledGlobally": "Kansiotarkkailu on poistettu käytöstä kaikkialla palvelimen asetuksissa",
"NoteRSSFeedPodcastAppsHttps": "Varoitus: Useimmat podcast-sovellukset edellyttävät, että RSS-syötteen URL-osoite käyttää HTTPS:a",
"NoteRSSFeedPodcastAppsPubDate": "Varoitus: yhdellä tai useammalla jaksollasi ei ole julkaisupäivämäärää. Jotkut podcast-sovellukset vaativat tämän.",
"StatsSessions": "istunnot",
"ToastAccountUpdateSuccess": "Tili päivitetty",
"ToastAppriseUrlRequired": "Arvon tulee olla Apprise URL",
"ToastBatchQuickMatchFailed": "Erän pikatäsmäys epäonnistui!",
"ToastBatchQuickMatchStarted": "{0} kirjan erän pikatäsmäys aloitettu!",
"ToastBookmarkCreateFailed": "Kirjanmerkin luominen epäonnistui",
"ToastCoverUpdateFailed": "Kansikuvan päivitys epäonnistui",
"ToastItemCoverUpdateSuccess": "Kohteen kansikuva päivitetty",

View File

@@ -76,7 +76,7 @@
"ButtonReadLess": "Lire moins",
"ButtonReadMore": "Lire la suite",
"ButtonRefresh": "Rafraîchir",
"ButtonRemove": "Supprimer",
"ButtonRemove": "Retirer",
"ButtonRemoveAll": "Supprimer tout",
"ButtonRemoveAllLibraryItems": "Supprimer tous les éléments de la bibliothèque",
"ButtonRemoveFromContinueListening": "Ne plus continuer à écouter",
@@ -219,6 +219,7 @@
"LabelAccountTypeAdmin": "Admin",
"LabelAccountTypeGuest": "Invité",
"LabelAccountTypeUser": "Utilisateur",
"LabelActivities": "Activités",
"LabelActivity": "Activité",
"LabelAddToCollection": "Ajouter à la collection",
"LabelAddToCollectionBatch": "Ajout de {0} livres à la collection",
@@ -283,6 +284,7 @@
"LabelContinueSeries": "Continuer les séries",
"LabelCover": "Couverture",
"LabelCoverImageURL": "URL vers limage de couverture",
"LabelCoverProvider": "Source des couvertures",
"LabelCreatedAt": "Créé le",
"LabelCronExpression": "Expression cron",
"LabelCurrent": "Actuel",
@@ -391,6 +393,7 @@
"LabelIntervalEvery6Hours": "Toutes les 6 heures",
"LabelIntervalEveryDay": "Tous les jours",
"LabelIntervalEveryHour": "Toutes les heures",
"LabelIntervalEveryMinute": "Toutes les minutes",
"LabelInvert": "Inverser",
"LabelItem": "Élément",
"LabelJumpBackwardAmount": "Dans le lecteur, reculer de",
@@ -555,11 +558,6 @@
"LabelSettingsBookshelfViewHelp": "Interface skeumorphique avec étagères en bois",
"LabelSettingsChromecastSupport": "Support du Chromecast",
"LabelSettingsDateFormat": "Format de date",
"LabelSettingsDisableWatcher": "Désactiver la surveillance",
"LabelSettingsDisableWatcherForLibrary": "Désactiver la surveillance des dossiers pour la bibliothèque",
"LabelSettingsDisableWatcherHelp": "Désactive la mise à jour automatique lorsque des modifications de fichiers sont détectées. * Nécessite le redémarrage du serveur",
"LabelSettingsEnableWatcher": "Activer la veille",
"LabelSettingsEnableWatcherForLibrary": "Activer la surveillance des dossiers pour la bibliothèque",
"LabelSettingsEnableWatcherHelp": "Active la mise à jour automatique d'éléments lorsque des modifications de fichiers sont détectées. * Nécessite le redémarrage du serveur",
"LabelSettingsEpubsAllowScriptedContent": "Autoriser le contenu scénarisé pour les fichiers EPUB",
"LabelSettingsEpubsAllowScriptedContentHelp": "Autoriser les fichiers EPUB à exécuter des scripts. Il est recommandé de laisser ce paramètre désactivé, sauf si vous faites confiance à la source des fichiers EPUB.",
@@ -710,6 +708,7 @@
"MessageBatchEditPopulateMapDetailsAllHelp": "Remplir les champs disponibles avec les données de tous les éléments. Les champs avec des valeurs multiples seront fusionnés.",
"MessageBatchQuickMatchDescription": "La recherche par correspondance rapide tentera dajouter les couvertures et métadonnées manquantes pour les éléments sélectionnés. Activez les options ci-dessous pour permettre la Recherche par correspondance décraser les couvertures et/ou métadonnées existantes.",
"MessageBookshelfNoCollections": "Vous navez pas encore de collections",
"MessageBookshelfNoCollectionsHelp": "Les collections sont publiques. Tous les utilisateurs ayant accès à la bibliothèque pourront les voir.",
"MessageBookshelfNoRSSFeeds": "Aucun flux RSS nest ouvert",
"MessageBookshelfNoResultsForFilter": "Aucun résultat pour le filtre « {0} : {1} »",
"MessageBookshelfNoResultsForQuery": "Aucun résultat pour la requête",
@@ -820,6 +819,7 @@
"MessageNoTasksRunning": "Aucune tâche en cours",
"MessageNoUpdatesWereNecessary": "Aucune mise à jour nétait nécessaire",
"MessageNoUserPlaylists": "Vous navez aucune liste de lecture",
"MessageNoUserPlaylistsHelp": "Les playlists sont privées. Seul l'utilisateur qui les a créées peut les voir.",
"MessageNotYetImplemented": "Non implémenté",
"MessageOpmlPreviewNote": "Remarque: Il sagit dun aperçu du fichier OPML analysé. Le titre réel du podcast provient du flux RSS.",
"MessageOr": "ou",
@@ -842,6 +842,7 @@
"MessageRestoreBackupConfirm": "Êtes-vous sûr·e de vouloir restaurer la sauvegarde créée le",
"MessageRestoreBackupWarning": "Restaurer la sauvegarde écrasera la base de donnée située dans le dossier /config ainsi que les images sur /metadata/items et /metadata/authors.<br><br>Les sauvegardes ne touchent pas aux fichiers de la bibliothèque. Si vous avez activé le paramètre pour sauvegarder les métadonnées et les images de couverture dans le même dossier que les fichiers, ceux-ci ne ni sauvegardés, ni écrasés lors de la restauration.<br><br>Tous les clients utilisant votre serveur seront automatiquement mis à jour.",
"MessageScheduleLibraryScanNote": "Pour la plupart des utilisateurs, il est recommandé de laisser cette fonctionnalité désactivée et de maintenir le réglage du moniteur de dossier activé. Le moniteur de dossier détectera automatiquement les changements dans vos dossiers de bibliothèque. Le moniteur de dossier ne fonctionne pas pour chaque système de fichiers (comme NFS) afin que les scans de bibliothèques programmés puissent être utilisés à la place.",
"MessageScheduleRunEveryWeekdayAtTime": "Exécuté tous les {0} à {1}",
"MessageSearchResultsFor": "Résultats de recherche pour",
"MessageSelected": "{0} sélectionnés",
"MessageServerCouldNotBeReached": "Serveur inaccessible",

View File

@@ -471,11 +471,6 @@
"LabelSettingsBookshelfViewHelp": "עיצוב סקאומורפי עם מדפי עץ",
"LabelSettingsChromecastSupport": "תמיכה ב-Chromecast",
"LabelSettingsDateFormat": "פורמט תאריך",
"LabelSettingsDisableWatcher": "השבת עוקב",
"LabelSettingsDisableWatcherForLibrary": "השבת עוקב תיקייה עבור ספרייה",
"LabelSettingsDisableWatcherHelp": "מבטל את הוספת/עדכון אוטומטי של פריטים כאשר שינויי קבצים זוהים. *דורש איתחול שרת",
"LabelSettingsEnableWatcher": "הפעל עוקב",
"LabelSettingsEnableWatcherForLibrary": "הפעל עוקב תיקייה עבור ספרייה",
"LabelSettingsEnableWatcherHelp": "מאפשר הוספת/עדכון אוטומטי של פריטים כאשר שינויי קבצים זוהים. *דורש איתחול שרת",
"LabelSettingsExperimentalFeatures": "תכונות ניסיוניות",
"LabelSettingsExperimentalFeaturesHelp": "תכונות בפיתוח שדורשות משובך ובדיקה. לחץ לפתיחת דיון ב-GitHub.",

View File

@@ -6,6 +6,7 @@
"ButtonApply": "लागू करें",
"ButtonApplyChapters": "अध्यायों में परिवर्तन लागू करें",
"ButtonAuthors": "लेखक",
"ButtonBack": "पीछे",
"ButtonBrowseForFolder": "फ़ोल्डर खोजें",
"ButtonCancel": "रद्द करें",
"ButtonCancelEncode": "एनकोड रद्द करें",

View File

@@ -252,7 +252,7 @@
"LabelBackToUser": "Povratak na korisnika",
"LabelBackupAudioFiles": "Sigurnosno kopiranje zvučnih datoteka",
"LabelBackupLocation": "Lokacija sigurnosnih kopija",
"LabelBackupsEnableAutomaticBackups": "Omogući automatsku izradu sigurnosnih kopija",
"LabelBackupsEnableAutomaticBackups": "Automatske sigurnosne kopije",
"LabelBackupsEnableAutomaticBackupsHelp": "Sigurnosne kopije spremaju se u /metadata/backups",
"LabelBackupsMaxBackupSize": "Maksimalna veličina sigurnosne kopije (u GB) (0 za neograničeno)",
"LabelBackupsMaxBackupSizeHelp": "U svrhu sprečavanja izrade krive konfiguracije, sigurnosne kopije neće se izraditi ako su veće od zadane veličine.",
@@ -403,8 +403,8 @@
"LabelLanguages": "Jezici",
"LabelLastBookAdded": "Zadnja dodana knjiga",
"LabelLastBookUpdated": "Zadnja ažurirana knjiga",
"LabelLastSeen": "Zadnje gledano",
"LabelLastTime": "Vrijeme zadnjeg slušanja",
"LabelLastSeen": "Zadnji puta viđen",
"LabelLastTime": "Zadnje doslušano vrijeme",
"LabelLastUpdate": "Zadnje ažuriranje",
"LabelLayout": "Prikaz",
"LabelLayoutSinglePage": "Jedna stranica",
@@ -558,11 +558,8 @@
"LabelSettingsBookshelfViewHelp": "Skeumorfni dizajn sa drvenim policama",
"LabelSettingsChromecastSupport": "Podrška za Chromecast",
"LabelSettingsDateFormat": "Format datuma",
"LabelSettingsDisableWatcher": "Isključi praćenje datotečnog sustava",
"LabelSettingsDisableWatcherForLibrary": "Onemogući praćenje datotečnog sustava za ovu knjižnicu",
"LabelSettingsDisableWatcherHelp": "Onemogućuje automatsko dodavanje ili ažuriranje stavki kod uočenih promjena datoteka. *Potrebno je ponovno pokrenuti poslužitelj",
"LabelSettingsEnableWatcher": "Omogući praćenje promjena",
"LabelSettingsEnableWatcherForLibrary": "Omogući praćenje promjena u mapi knjižnice",
"LabelSettingsEnableWatcher": "Automatski pretražuj ima li promjena u knjižnicama",
"LabelSettingsEnableWatcherForLibrary": "Automatski traži promjene u knjižnicama",
"LabelSettingsEnableWatcherHelp": "Omogućuje automatsko dodavanje/ažuriranje stavki kada se uoče izmjene datoteka. *Potrebno je ponovno pokretanje poslužitelja",
"LabelSettingsEpubsAllowScriptedContent": "Omogući skripte u epub datotekama",
"LabelSettingsEpubsAllowScriptedContentHelp": "Omogućuje epub datotekama izvođenje skripti. Preporučamo isključiti ovu mogućnost ukoliko nemate povjerenja u izvore epub datoteka.",

View File

@@ -552,11 +552,6 @@
"LabelSettingsBookshelfViewHelp": "Skeuomorfikus dizájn fa polcokkal",
"LabelSettingsChromecastSupport": "Chromecast támogatás",
"LabelSettingsDateFormat": "Dátumformátum",
"LabelSettingsDisableWatcher": "Figyelő letiltása",
"LabelSettingsDisableWatcherForLibrary": "Mappafigyelő letiltása a könyvtárban",
"LabelSettingsDisableWatcherHelp": "Letiltja az automatikus elem hozzáadás/frissítés funkciót, amikor fájlváltozásokat észlel. *Szerver újraindítása szükséges",
"LabelSettingsEnableWatcher": "Figyelő engedélyezése",
"LabelSettingsEnableWatcherForLibrary": "Mappafigyelő engedélyezése a könyvtárban",
"LabelSettingsEnableWatcherHelp": "Engedélyezi az automatikus elem hozzáadás/frissítés funkciót, amikor fájlváltozásokat észlel. *Szerver újraindítása szükséges",
"LabelSettingsEpubsAllowScriptedContent": "Szkriptelt tartalmak engedélyezése epub-okban",
"LabelSettingsEpubsAllowScriptedContentHelp": "Megengedi, hogy az epub fájlok szkripteket hajtsanak végre. Ezt a beállítást kikapcsolva ajánlott tartani, kivéve, ha megbízik az epub fájlok forrásában.",

View File

@@ -558,11 +558,6 @@
"LabelSettingsBookshelfViewHelp": "Design con scaffali in legno",
"LabelSettingsChromecastSupport": "Supporto a Chromecast",
"LabelSettingsDateFormat": "Formato Data",
"LabelSettingsDisableWatcher": "Disattiva Watcher",
"LabelSettingsDisableWatcherForLibrary": "Disattiva Watcher per le librerie",
"LabelSettingsDisableWatcherHelp": "Disattiva il controllo automatico libri nelle cartelle delle librerie. *Richiede il Riavvio del Server",
"LabelSettingsEnableWatcher": "Abilita Watcher",
"LabelSettingsEnableWatcherForLibrary": "Abilita il controllo cartelle per la libreria",
"LabelSettingsEnableWatcherHelp": "Abilita l'aggiunta/aggiornamento automatico degli elementi quando vengono rilevate modifiche ai file. *Richiede il riavvio del Server",
"LabelSettingsEpubsAllowScriptedContent": "Consenti contenuti con script negli epub",
"LabelSettingsEpubsAllowScriptedContentHelp": "Consenti ai file epub di eseguire script. Si consiglia di mantenere questa impostazione disabilitata a meno che non si ritenga attendibile l'origine dei file epub.",

View File

@@ -1,3 +1,21 @@
{
"ButtonAdd": "追加"
"ButtonAdd": "追加",
"ButtonAddChapters": "チャプターの追加",
"ButtonCancel": "キャンセル",
"ButtonOk": "はい",
"ButtonPlay": "プレイ",
"ButtonPlaying": "プレイ中",
"ButtonPrevious": "先",
"ButtonRead": "野村",
"ButtonYes": "はい",
"HeaderPlayerSettings": "プレーヤーの設定",
"LabelBooks": "ほん",
"LabelLanguage": "言語",
"LabelLanguages": "言語",
"LabelName": "名",
"LabelNew": "新しい",
"LabelNewPassword": "新しいのパスワード",
"LabelPassword": "パスワード",
"LabelPlaylists": "プレイリスト",
"LabelPodcast": "ポッドキャスト"
}

View File

@@ -413,9 +413,6 @@
"LabelSettingsBookshelfViewHelp": "Knygų lentynos dizainas su medinėmis lentynomis",
"LabelSettingsChromecastSupport": "„Chromecast“ palaikymas",
"LabelSettingsDateFormat": "Datos formatas",
"LabelSettingsDisableWatcher": "Išjungti stebėtoją",
"LabelSettingsDisableWatcherForLibrary": "Išjungti aplankų stebėtoją bibliotekai",
"LabelSettingsDisableWatcherHelp": "Išjungia automatinį elementų pridėjimą/atnaujinimą, jei pastebėti failų pokyčiai. *Reikalingas serverio paleidimas iš naujo",
"LabelSettingsExperimentalFeatures": "Eksperimentiniai funkcionalumai",
"LabelSettingsExperimentalFeaturesHelp": "Funkcijos, kurios yra kuriamos ir laukiami jūsų komentarai. Spustelėkite, kad atidarytumėte „GitHub“ diskusiją.",
"LabelSettingsFindCovers": "Rasti viršelius",

View File

@@ -10,6 +10,7 @@
"ButtonApplyChapters": "Hoofdstukken toepassen",
"ButtonAuthors": "Auteurs",
"ButtonBack": "Terug",
"ButtonBatchEditPopulateMapDetails": "Kaartgegevens invullen",
"ButtonBrowseForFolder": "Bladeren naar map",
"ButtonCancel": "Annuleren",
"ButtonCancelEncode": "Encoding annuleren",
@@ -553,11 +554,6 @@
"LabelSettingsBookshelfViewHelp": "Skeumorphisch design met houten planken",
"LabelSettingsChromecastSupport": "Chromecast ondersteuning",
"LabelSettingsDateFormat": "Datum format",
"LabelSettingsDisableWatcher": "Watcher uitschakelen",
"LabelSettingsDisableWatcherForLibrary": "Map-watcher voor bibliotheek uitschakelen",
"LabelSettingsDisableWatcherHelp": "Schakelt het automatisch toevoegen/bijwerken van onderdelen wanneer bestandswijzigingen gedetecteerd zijn uit. *Vereist herstart server",
"LabelSettingsEnableWatcher": "Watcher inschakelen",
"LabelSettingsEnableWatcherForLibrary": "Map-watcher voor bibliotheek inschakelen",
"LabelSettingsEnableWatcherHelp": "Zorgt voor het automatisch toevoegen/bijwerken van onderdelen als bestandswijzigingen worden gedetecteerd. *Vereist herstarten van server",
"LabelSettingsEpubsAllowScriptedContent": "Sta scripted content toe in epubs",
"LabelSettingsEpubsAllowScriptedContentHelp": "Sta toe dat epub-bestanden scripts uitvoeren. Het wordt aanbevolen om deze instelling uitgeschakeld te houden, tenzij u de bron van de epub-bestanden vertrouwt.",

View File

@@ -550,11 +550,6 @@
"LabelSettingsBookshelfViewHelp": "Skeuomorf design med hyller av ved",
"LabelSettingsChromecastSupport": "Chromecast støtte",
"LabelSettingsDateFormat": "Dato Format",
"LabelSettingsDisableWatcher": "Deaktiver overvåker",
"LabelSettingsDisableWatcherForLibrary": "Deaktiver mappe overvåker for bibliotek",
"LabelSettingsDisableWatcherHelp": "Deaktiverer automatisk opprettelse/oppdatering av enheter når filendringer er oppdaget. *Krever restart av server*",
"LabelSettingsEnableWatcher": "Aktiver overvåker",
"LabelSettingsEnableWatcherForLibrary": "Aktiver mappe overvåker for bibliotek",
"LabelSettingsEnableWatcherHelp": "Aktiverer automatisk opprettelse/oppdatering av enheter når filendringer er oppdaget. *Krever restart av server*",
"LabelSettingsEpubsAllowScriptedContent": "Tillat scripting i innholdet i ebub-bøker",
"LabelSettingsEpubsAllowScriptedContentHelp": "Tillat epub-filer å kjøre script. Det er anbefalt å slå av denne innstillingen med mindre du stoler på kilden til epub-filene.",

View File

@@ -508,11 +508,6 @@
"LabelSettingsBookshelfViewHelp": "Widok półki z książkami",
"LabelSettingsChromecastSupport": "Wsparcie Chromecast",
"LabelSettingsDateFormat": "Format daty",
"LabelSettingsDisableWatcher": "Wyłącz monitorowanie",
"LabelSettingsDisableWatcherForLibrary": "Wyłącz monitorowanie folderów dla biblioteki",
"LabelSettingsDisableWatcherHelp": "Wyłącz automatyczne dodawanie/aktualizowanie elementów po wykryciu zmian w plikach. *Wymaga restartu serwera",
"LabelSettingsEnableWatcher": "Włącz monitorowanie",
"LabelSettingsEnableWatcherForLibrary": "Włącz monitorowanie folderów dla biblioteki",
"LabelSettingsEnableWatcherHelp": "Włącza automatyczne dodawanie/aktualizację pozycji gdy wykryte zostaną zmiany w plikach. Wymaga restartu serwera",
"LabelSettingsEpubsAllowScriptedContent": "Zezwalanie na skrypty w plikach epub",
"LabelSettingsEpubsAllowScriptedContentHelp": "Zezwala plikom epub na wykonywanie skryptów. Zaleca się mieć to ustawienie wyłączone, chyba że ma się zaufanie do źródła plików epub.",

View File

@@ -454,11 +454,6 @@
"LabelSettingsBookshelfViewHelp": "Aparência esqueomorfa com prateleiras de madeira",
"LabelSettingsChromecastSupport": "Suporte ao Chromecast",
"LabelSettingsDateFormat": "Formato de data",
"LabelSettingsDisableWatcher": "Desativar Monitoramento",
"LabelSettingsDisableWatcherForLibrary": "Desativa o monitoramento de pastas para a biblioteca",
"LabelSettingsDisableWatcherHelp": "Desativa o acréscimo/atualização de itens quando forem detectadas mudanças no arquivo. *Requer reiniciar o servidor",
"LabelSettingsEnableWatcher": "Ativar Monitoramento",
"LabelSettingsEnableWatcherForLibrary": "Ativa o monitoramento de pastas para a biblioteca",
"LabelSettingsEnableWatcherHelp": "Ativa o acréscimo/atualização de itens quando forem detectadas mudanças no arquivo. *Requer reiniciar o servidor",
"LabelSettingsEpubsAllowScriptedContent": "Permitir scripts em epubs",
"LabelSettingsEpubsAllowScriptedContentHelp": "Permitir que arquivos epub executem scripts. É recomendado manter essa configuração desativada, a não ser que confie na fonte dos arquivos epub.",

View File

@@ -219,6 +219,7 @@
"LabelAccountTypeAdmin": "Администратор",
"LabelAccountTypeGuest": "Гость",
"LabelAccountTypeUser": "Пользователь",
"LabelActivities": "Мероприятия",
"LabelActivity": "Активность",
"LabelAddToCollection": "Добавить в коллекцию",
"LabelAddToCollectionBatch": "Добавить {0} книг в коллекцию",
@@ -283,7 +284,8 @@
"LabelContinueSeries": "Продолжить серию",
"LabelCover": "Обложка",
"LabelCoverImageURL": "URL изображения обложки",
"LabelCreatedAt": "Создано",
"LabelCoverProvider": "Провайдер обложек",
"LabelCreatedAt": "Создан",
"LabelCronExpression": "Выражение Cron",
"LabelCurrent": "Текущий",
"LabelCurrently": "Текущее:",
@@ -391,6 +393,7 @@
"LabelIntervalEvery6Hours": "Каждые 6 часов",
"LabelIntervalEveryDay": "Каждый день",
"LabelIntervalEveryHour": "Каждый час",
"LabelIntervalEveryMinute": "Каждую минуту",
"LabelInvert": "Инвертировать",
"LabelItem": "Элемент",
"LabelJumpBackwardAmount": "Прыжок назад на величину",
@@ -555,11 +558,6 @@
"LabelSettingsBookshelfViewHelp": "Конструкция с деревянными полками",
"LabelSettingsChromecastSupport": "Поддержка Chromecast",
"LabelSettingsDateFormat": "Формат даты",
"LabelSettingsDisableWatcher": "Отключить отслеживание",
"LabelSettingsDisableWatcherForLibrary": "Отключить отслеживание для библиотеки",
"LabelSettingsDisableWatcherHelp": "Отключает автоматическое добавление/обновление элементов, когда обнаружено изменение файлов. *Требуется перезапуск сервера",
"LabelSettingsEnableWatcher": "Включить отслеживание",
"LabelSettingsEnableWatcherForLibrary": "Включить отслеживание за папками библиотеки",
"LabelSettingsEnableWatcherHelp": "Включает автоматическое добавление/обновление элементов при обнаружении изменений файлов. *Требуется перезапуск сервера",
"LabelSettingsEpubsAllowScriptedContent": "Разрешение содержимого epub с скриптами",
"LabelSettingsEpubsAllowScriptedContentHelp": "Разрешить файлам epub выполнять скрипты. Рекомендуется отключать этот параметр, если вы не доверяете источнику файлов epub.",
@@ -845,6 +843,7 @@
"MessageRestoreBackupConfirm": "Вы уверены, что хотите восстановить резервную копию, созданную",
"MessageRestoreBackupWarning": "Восстановление резервной копии перезапишет всю базу данных, расположенную в /config, и обложки изображений в /metadata/items и /metadata/authors.<br/><br/>Бэкапы не изменяют файлы в папках библиотеки. Если вы включили параметры сервера для хранения обложек и метаданных в папках библиотеки, то они не резервируются и не перезаписываются.<br/><br/>Все клиенты, использующие ваш сервер, будут автоматически обновлены.",
"MessageScheduleLibraryScanNote": "Большинству пользователей рекомендуется отключить эту функцию и включить функцию просмотра папок. Программа просмотра папок автоматически обнаружит изменения в папках вашей библиотеки. Программа просмотра папок работает не для каждой файловой системы (например, NFS), поэтому вместо этого можно использовать запланированные проверки библиотеки.",
"MessageScheduleRunEveryWeekdayAtTime": "Запуск каждые {0} по {1}",
"MessageSearchResultsFor": "Результаты поиска для",
"MessageSelected": "{0} выбрано",
"MessageServerCouldNotBeReached": "Не удалось связаться с сервером",

93
client/strings/sk.json Normal file
View File

@@ -0,0 +1,93 @@
{
"ButtonAdd": "Pridať",
"ButtonAddChapters": "Pridať kapitoly",
"ButtonAddDevice": "Pridať zariadenie",
"ButtonAddLibrary": "Pridať knižnicu",
"ButtonAddPodcasts": "Pridať podcasty",
"ButtonAddUser": "Pridať užívateľa",
"ButtonAddYourFirstLibrary": "Pridajte vašu prvú knižnicu",
"ButtonApply": "Použiť",
"ButtonApplyChapters": "Použiť kapitoly",
"ButtonAuthors": "Autori",
"ButtonBack": "Späť",
"ButtonBatchEditPopulateFromExisting": "Vytvoriť z existujúcej",
"ButtonBatchEditPopulateMapDetails": "Vyplniť detaily na mape",
"ButtonBrowseForFolder": "Prehľadávať adresáre",
"ButtonCancel": "Zrušiť",
"ButtonCancelEncode": "Zrušiť kódovanie",
"ButtonChangeRootPassword": "Zmeniť Root heslo",
"ButtonCheckAndDownloadNewEpisodes": "Skontrolovať a stiahnuť nové epizódy",
"ButtonChooseAFolder": "Vyberte adresár",
"ButtonChooseFiles": "Vyberte súbory",
"ButtonClearFilter": "Zrušiť filter",
"ButtonCloseFeed": "Zatvoriť zdroj",
"ButtonCloseSession": "Ukončiť otvorené pripojenie",
"ButtonCollections": "Zbierky",
"ButtonConfigureScanner": "Nastaviť skener",
"ButtonCreate": "Vytvoriť",
"ButtonCreateBackup": "Vytvoriť zálohu",
"ButtonDelete": "Zmazať",
"ButtonDownloadQueue": "Poradie",
"ButtonEdit": "Upraviť",
"ButtonEditChapters": "Upraviť kapitoly",
"ButtonEditPodcast": "Upraviť podcast",
"ButtonEnable": "Povoliť",
"ButtonForceReScan": "Vynútiť preskenovanie",
"ButtonFullPath": "Zobraziť cestu",
"ButtonHide": "Skryť",
"ButtonHome": "Domov",
"ButtonIssues": "Problémy",
"ButtonJumpBackward": "Posun späť",
"ButtonJumpForward": "Posun vpred",
"ButtonLatest": "Najnovšie",
"ButtonLibrary": "Knižnica",
"ButtonLogout": "Odhlásenie",
"ButtonLookup": "Vyhľadať",
"ButtonManageTracks": "Spravovať stopy",
"ButtonMapChapterTitles": "Mapovať názvy kapitol",
"ButtonMatchAllAuthors": "Vyhľadať všetkých autorov",
"ButtonMatchBooks": "Vyhľadať knihy",
"ButtonNevermind": "Nevadí",
"ButtonNext": "Ďalšie",
"ButtonNextChapter": "Ďalšia kapitola",
"ButtonNextItemInQueue": "Ďalšia položka v poradí",
"ButtonOk": "OK",
"ButtonOpenFeed": "Otvoriť zdroj",
"ButtonOpenManager": "Otvoriť správcu",
"ButtonPause": "Zastaviť",
"ButtonPlay": "Prehrať",
"ButtonPlayAll": "Prehrať všetko",
"ButtonPlaying": "Prehráva sa",
"ButtonPlaylists": "Playlisty",
"ButtonPrevious": "Predchádzajúci",
"ButtonPreviousChapter": "Predchádzajúca kapitola",
"ButtonProbeAudioFile": "Preskúmaj zvukový súbor",
"ButtonPurgeAllCache": "Vymaž celú medzipamäť",
"ButtonPurgeItemsCache": "Vymaž medzipamäť položiek",
"ButtonQueueAddItem": "Pridať do poradia",
"ButtonQueueRemoveItem": "Vymazať z poradia",
"ButtonQuickEmbed": "Rýchle vloženie",
"ButtonQuickEmbedMetadata": "Rýchle vloženie metadát",
"ButtonQuickMatch": "Rýchle vyhľadanie",
"ButtonReScan": "Preskenovať",
"ButtonRead": "Načítať",
"ButtonReadLess": "Načítať menej",
"ButtonReadMore": "Načítať viac",
"ButtonRefresh": "Obnoviť",
"ButtonRemove": "Odstrániť",
"ButtonRemoveAll": "Odstrániť všetko",
"ButtonRemoveAllLibraryItems": "Odstrániť všetky položky knižnice",
"ButtonRemoveFromContinueListening": "Odstrániť z nedokončených podcastov",
"ButtonRemoveFromContinueReading": "Odtrániť z nedokončených audiokníh",
"ButtonRemoveSeriesFromContinueSeries": "Odstrániť z nedokončených sérií",
"ButtonReset": "Resetovať",
"ButtonResetToDefault": "Resetovať do predvolené",
"ButtonRestore": "Obnoviť zo zálohy",
"ButtonSave": "Uložiť",
"ButtonSaveAndClose": "Uložiť a zavrieť",
"ButtonSaveTracklist": "Uložiť zoznam",
"ButtonScan": "Skenovať",
"ButtonScanLibrary": "Skenovať knižnicu",
"HeaderMatch": "Spárovať",
"LabelBackupsNumberToKeepHelp": "Týmto spôsobom odstránite vždy iba jednu zálohu. V prípade, ak chcete odtrániť viacero záloh, mali by ste ich odstrániť manuálne."
}

View File

@@ -219,6 +219,7 @@
"LabelAccountTypeAdmin": "Administrator",
"LabelAccountTypeGuest": "Gost",
"LabelAccountTypeUser": "Uporabnik",
"LabelActivities": "Aktivnosti",
"LabelActivity": "Aktivnost",
"LabelAddToCollection": "Dodaj v zbirko",
"LabelAddToCollectionBatch": "Dodaj {0} knjig v zbirko",
@@ -283,6 +284,7 @@
"LabelContinueSeries": "Nadaljuj s serijo",
"LabelCover": "Naslovnica",
"LabelCoverImageURL": "URL naslovne slike",
"LabelCoverProvider": "Ponudnik naslovnic",
"LabelCreatedAt": "Ustvarjeno ob",
"LabelCronExpression": "Cron izraz",
"LabelCurrent": "Trenutno",
@@ -391,6 +393,7 @@
"LabelIntervalEvery6Hours": "Vsakih 6 ur",
"LabelIntervalEveryDay": "Vsak dan",
"LabelIntervalEveryHour": "Vsako uro",
"LabelIntervalEveryMinute": "Vsako minuto",
"LabelInvert": "Obrni izbor",
"LabelItem": "Element",
"LabelJumpBackwardAmount": "Količina skoka nazaj",
@@ -555,11 +558,8 @@
"LabelSettingsBookshelfViewHelp": "Skeuomorfna oblika z lesenimi policami",
"LabelSettingsChromecastSupport": "Podpora za Chromecast",
"LabelSettingsDateFormat": "Oblika datuma",
"LabelSettingsDisableWatcher": "Onemogoči spremljanje datotečnega sistema",
"LabelSettingsDisableWatcherForLibrary": "Onemogoči spremljanje map za knjižnico",
"LabelSettingsDisableWatcherHelp": "Onemogoči samodejno dodajanje/posodabljanje elementov, ko so zaznane spremembe datoteke. *Potreben je ponovni zagon strežnika",
"LabelSettingsEnableWatcher": "Omogoči spremljanje sprememb",
"LabelSettingsEnableWatcherForLibrary": "Omogoči spremljanje sprememb v mapi knjižnice",
"LabelSettingsEnableWatcher": "Samodejno preglej knjižnice za spremembe",
"LabelSettingsEnableWatcherForLibrary": "Samodejno preglej knjižnico za spremembe",
"LabelSettingsEnableWatcherHelp": "Omogoča samodejno dodajanje/posodabljanje elementov, ko so zaznane spremembe datoteke. *Potreben je ponovni zagon strežnika",
"LabelSettingsEpubsAllowScriptedContent": "Dovoli skriptirano vsebino v epubih",
"LabelSettingsEpubsAllowScriptedContentHelp": "Dovoli datotekam epub izvajanje skript. Priporočljivo je, da to nastavitev pustite onemogočeno, razen če zaupate viru datotek epub.",
@@ -845,6 +845,7 @@
"MessageRestoreBackupConfirm": "Ali ste prepričani, da želite obnoviti varnostno kopijo, ustvarjeno ob",
"MessageRestoreBackupWarning": "Obnovitev varnostne kopije bo prepisala celotno zbirko podatkov, ki se nahaja v /config, in zajema slike v /metadata/items in /metadata/authors.<br /><br />Varnostne kopije ne spreminjajo nobenih datotek v mapah vaše knjižnice. Če ste omogočili nastavitve strežnika za shranjevanje naslovnic in metapodatkov v mapah vaše knjižnice, potem ti niso varnostno kopirani ali prepisani.<br /><br />Vsi odjemalci, ki uporabljajo vaš strežnik, bodo samodejno osveženi.",
"MessageScheduleLibraryScanNote": "Za večino uporabnikov je priporočljivo, da to funkcijo pustite onemogočeno in ohranite nastavitev pregledovalnika map omogočeno. Pregledovalnik map bo samodejno zaznal spremembe v mapah vaše knjižnice. Pregledovalnik map ne deluje za vse datotečne sisteme (na primer NFS), zato lahko namesto tega uporabite načrtovane preglede knjižnic.",
"MessageScheduleRunEveryWeekdayAtTime": "Zaženi vsakih {0} ob {1}",
"MessageSearchResultsFor": "Rezultati iskanja za",
"MessageSelected": "{0} izbrano",
"MessageServerCouldNotBeReached": "Strežnika ni bilo mogoče doseči",

View File

@@ -36,7 +36,7 @@
"ButtonFullPath": "Fullständig sökväg",
"ButtonHide": "Dölj",
"ButtonHome": "Hem",
"ButtonIssues": "Problem",
"ButtonIssues": "Objekt med problem",
"ButtonJumpBackward": "Hoppa bakåt",
"ButtonJumpForward": "Hoppa framåt",
"ButtonLatest": "Senaste",
@@ -66,11 +66,13 @@
"ButtonPurgeItemsCache": "Rensa cache för föremål",
"ButtonQueueAddItem": "Lägg till i kön",
"ButtonQueueRemoveItem": "Ta bort från kön",
"ButtonQuickEmbed": "Infoga metadata",
"ButtonQuickEmbedMetadata": "Infoga metadata",
"ButtonQuickMatch": "Snabbmatchning",
"ButtonReScan": "Ny skanning",
"ButtonRead": "Läs",
"ButtonReadLess": "Visa mindre",
"ButtonReadMore": "Visa mer",
"ButtonReadLess": "Läs mindre",
"ButtonReadMore": "Läs mer",
"ButtonRefresh": "Uppdatera",
"ButtonRemove": "Ta bort",
"ButtonRemoveAll": "Ta bort alla",
@@ -86,6 +88,8 @@
"ButtonSaveTracklist": "Spara spårlista",
"ButtonScan": "Skanna",
"ButtonScanLibrary": "Skanna bibliotek",
"ButtonScrollLeft": "Scroll vänster",
"ButtonScrollRight": "Scrolla höger",
"ButtonSearch": "Sök",
"ButtonSelectFolderPath": "Välj mappens sökväg",
"ButtonSeries": "Serier",
@@ -94,7 +98,7 @@
"ButtonShiftTimes": "Förskjut tider",
"ButtonShow": "Visa",
"ButtonStartM4BEncode": "Starta M4B-omkodning",
"ButtonStartMetadataEmbed": "Starta inbäddning av metadata",
"ButtonStartMetadataEmbed": "Infoga metadata",
"ButtonStats": "Statistik",
"ButtonSubmit": "Spara",
"ButtonTest": "Testa",
@@ -114,7 +118,7 @@
"HeaderAdvanced": "Avancerad",
"HeaderAppriseNotificationSettings": "Inställningar av meddelanden med Apprise",
"HeaderAudioTracks": "Ljudspår",
"HeaderAudiobookTools": "Hantering av ljudboksfil",
"HeaderAudiobookTools": "Hantering av ljudboksfiler",
"HeaderAuthentication": "Autentisering",
"HeaderBackups": "Säkerhetskopior",
"HeaderChangePassword": "Ändra lösenord",
@@ -124,11 +128,12 @@
"HeaderCollectionItems": "Böcker i samlingen",
"HeaderCover": "Omslag",
"HeaderCurrentDownloads": "Aktuella nedladdningar",
"HeaderCustomMessageOnLogin": "Meddelande att visa på sidan för inloggning",
"HeaderCustomMetadataProviders": "Egen källa för metadata",
"HeaderDetails": "Detaljer",
"HeaderDownloadQueue": "Nedladdningskö",
"HeaderEbookFiles": "E-boksfiler",
"HeaderEmail": "E-postadress",
"HeaderEmail": "E-post",
"HeaderEmailSettings": "Inställningar för e-post",
"HeaderEpisodes": "Avsnitt",
"HeaderEreaderDevices": "Enheter för att läsa e-böcker",
@@ -146,7 +151,7 @@
"HeaderListeningSessions": "Lyssningstillfällen",
"HeaderListeningStats": "Lyssningsstatistik",
"HeaderLogin": "Logga in",
"HeaderLogs": "Loggar",
"HeaderLogs": "Loggning",
"HeaderManageGenres": "Hantera kategorier",
"HeaderManageTags": "Hantera taggar",
"HeaderMapDetails": "Gemensam information för samtliga objekt",
@@ -156,7 +161,9 @@
"HeaderNewAccount": "Nytt konto",
"HeaderNewLibrary": "Nytt bibliotek",
"HeaderNotificationCreate": "Addera ett meddelande",
"HeaderNotificationUpdate": "Uppdateringsnotis",
"HeaderNotifications": "Meddelanden",
"HeaderOpenIDConnectAuthentication": "OpenID Connect Autentisering",
"HeaderOpenRSSFeed": "Öppna RSS-flöde",
"HeaderOtherFiles": "Andra filer",
"HeaderPasswordAuthentication": "Lösenordsautentisering",
@@ -201,6 +208,7 @@
"HeaderYearReview": "Sammanställning av {0}",
"HeaderYourStats": "Din statistik",
"LabelAbridged": "Förkortad version",
"LabelAbridgedUnchecked": "Oavkortad (okontrollerad)",
"LabelAccessibleBy": "Tillgänglig för",
"LabelAccountType": "Kontotyp",
"LabelAccountTypeAdmin": "Administratör",
@@ -222,7 +230,7 @@
"LabelAlreadyInYourLibrary": "Finns redan i samlingen",
"LabelApiToken": "API-token",
"LabelAppend": "Lägg till",
"LabelAudioBitrate": "Bitrate för ljud (t.ex. 128k)",
"LabelAudioBitrate": "Bitrate (t.ex. 128k)",
"LabelAudioChannels": "Ljudkanaler (1 eller 2)",
"LabelAudioCodec": "Codec för ljud",
"LabelAuthor": "Författare",
@@ -233,6 +241,8 @@
"LabelAutoFetchMetadata": "Automatisk nedladdning av metadata",
"LabelAutoFetchMetadataHelp": "Hämtar metadata för titel, författare och serier. Kompletterande metadata kan manuellt adderas efter uppladdningen.",
"LabelAutoLaunch": "Automatisk start",
"LabelAutoLaunchDescription": "Omdirigera till auth-leverantören automatiskt när du navigerar till inloggningssidan (manual override path <code>/login?autoLaunch=0</code>)",
"LabelAutoRegister": "Auto Register",
"LabelAutoRegisterDescription": "Skapa automatiskt nya användare efter inloggning",
"LabelBackToUser": "Tillbaka till användaren",
"LabelBackupAudioFiles": "Säkerhetskopiera ljudfiler",
@@ -291,11 +301,13 @@
"LabelDownloadable": "Nedladdningsbar",
"LabelDuration": "Varaktighet",
"LabelDurationComparisonExactMatch": "(exakt matchning)",
"LabelDurationComparisonLonger": "({0} längre)",
"LabelDurationComparisonShorter": "({0} kortare)",
"LabelDurationFound": "Varaktighet hittad:",
"LabelEbook": "E-bok",
"LabelEbooks": "E-böcker",
"LabelEdit": "Redigera",
"LabelEmail": "E-postadress",
"LabelEmail": "E-post",
"LabelEmailSettingsFromAddress": "Från e-postadress",
"LabelEmailSettingsRejectUnauthorized": "Avvisa icke-autentiserade certifikat",
"LabelEmailSettingsRejectUnauthorizedHelp": "Inaktivering av SSL-certifikatsvalidering kan exponera din anslutning för säkerhetsrisker, såsom man-in-the-middle-attacker. Inaktivera bara denna inställning om du förstår implikationerna och litar på den epostserver du ansluter till.",
@@ -304,12 +316,13 @@
"LabelEmailSettingsTestAddress": "E-postadress för test",
"LabelEmbeddedCover": "Infogat omslag",
"LabelEnable": "Aktivera",
"LabelEncodingBackupLocation": "En säkerhetskopia av dina orginalljudfiler kommer att placeras i katalogen:",
"LabelEncodingClearItemCache": "Kom ihåg att regelbundet radera cachen för föremål. Du hittar funktionen längst ner på sidan 'Inställningar'.",
"LabelEncodingBackupLocation": "En säkerhetskopia av ljudfilerna kommer att placeras i katalogen:",
"LabelEncodingChaptersNotEmbedded": "Information om kapitel kommer inte att inkluderas i ljudböcker med flera ljudspår.",
"LabelEncodingClearItemCache": "Kom ihåg att regelbundet rensa cachen för föremål. Du hittar funktionen längst ner på sidan 'Inställningar'.",
"LabelEncodingFinishedM4B": "Den färdiga M4B-filen kommer att placeras i katalogen:",
"LabelEncodingInfoEmbedded": "Metadata kommer att adderas i ljudfilerna i mappen med ljudboken.",
"LabelEncodingStartedNavigation": "När du startad omkodningen kan du lämna denna sida. Omkodningen fortsätter i bakgrunden.",
"LabelEncodingTimeWarning": "Avkodningen kan ta upp till 30 minuter eller ännu längre för riktigt stora filer.",
"LabelEncodingStartedNavigation": "När du startat uppgiften kan du lämna denna sida. Arbetet fortsätter i bakgrunden.",
"LabelEncodingTimeWarning": "Omkodningen kan ta upp till 30 minuter eller ännu längre för riktigt stora filer.",
"LabelEncodingWarningAdvancedSettings": "VARNING: Ändra inte inställningarna om du inte är bekant med inställningarna för omkodning med 'ffmpeg'.",
"LabelEncodingWatcherDisabled": "Om funktionen 'Watcher' är avstängd behöver du göra en ny skanning av ljudboken efteråt.",
"LabelEnd": "Slut",
@@ -391,6 +404,7 @@
"LabelLess": "Mindre",
"LabelLibrariesAccessibleToUser": "Bibliotek användaren har tillgång till",
"LabelLibrary": "Bibliotek",
"LabelLibraryFilterSublistEmpty": "Ingen {0}",
"LabelLibraryItem": "Objekt",
"LabelLibraryName": "Biblioteksnamn",
"LabelLimit": "Begränsning",
@@ -401,6 +415,8 @@
"LabelLogLevelWarn": "Varningar",
"LabelLookForNewEpisodesAfterDate": "Sök efter nya avsnitt efter detta datum",
"LabelLowestPriority": "Lägst prioritet",
"LabelMatchExistingUsersBy": "Matcha befintliga användare med",
"LabelMatchExistingUsersByDescription": "Används för att koppla existerande användare. När kopplingen sker kommer användaren att matchas med ett unikt ID från SSO-leverantören.",
"LabelMaxEpisodesToDownload": "Maximalt antal avsnitt att ladda ner (0 = obegränsat).",
"LabelMaxEpisodesToDownloadPerCheck": "Maximalt antal nya avsnitt att ladda ner per tillfälle",
"LabelMaxEpisodesToKeep": "Maximalt antal avsnitt att behålla",
@@ -413,7 +429,7 @@
"LabelMetadataProvider": "Källa för metadata",
"LabelMinute": "Minut",
"LabelMinutes": "Minuter",
"LabelMissing": "Saknar",
"LabelMissing": "Saknad",
"LabelMissingEbook": "Saknar e-bok",
"LabelMissingSupplementaryEbook": "Saknar kompletterande e-bok",
"LabelMore": "Mer",
@@ -442,7 +458,7 @@
"LabelNotificationsMaxQueueSize": "Max köstorlek för aviseringsevenemang",
"LabelNotificationsMaxQueueSizeHelp": "Evenemang är begränsade till att utlösa ett per sekund. Evenemang kommer att ignoreras om kön är full. Detta förhindrar aviseringsspam.",
"LabelNumberOfBooks": "Antal böcker",
"LabelNumberOfEpisodes": "Antal avsnitt",
"LabelNumberOfEpisodes": "# av Avsnitt",
"LabelOpenRSSFeed": "Öppna RSS-flöde",
"LabelOverwrite": "Skriv över",
"LabelPaginationPageXOfY": "Sida {0} av {1}",
@@ -475,6 +491,7 @@
"LabelPublishYear": "Publiceringsår",
"LabelPublishedDate": "Publicerad {0}",
"LabelPublishedDecade": "Årtionde för publicering",
"LabelPublishedDecades": "Årtionde för publicering",
"LabelPublisher": "Utgivare",
"LabelPublishers": "Utgivare",
"LabelRSSFeedCustomOwnerEmail": "Anpassad ägarens e-post",
@@ -484,7 +501,8 @@
"LabelRSSFeedSlug": "RSS-flödesslag",
"LabelRSSFeedURL": "URL-adress för RSS-flödet",
"LabelRandomly": "Slumpartat",
"LabelRead": "Läst",
"LabelReAddSeriesToContinueListening": "Addera serien på nytt till 'Fortsätt att lyssna'",
"LabelRead": "Läs",
"LabelReadAgain": "Läs igen",
"LabelReadEbookWithoutProgress": "Läs e-bok utan att behålla framsteg",
"LabelRecentSeries": "Senaste serierna",
@@ -509,7 +527,8 @@
"LabelSelectEpisodesShowing": "Välj {0} avsnitt som visas",
"LabelSelectUsers": "Välj användare",
"LabelSendEbookToDevice": "Skicka e-bok till...",
"LabelSequence": "Sekvensnummer",
"LabelSequence": "Ordningsnummer",
"LabelSerial": "Seriell",
"LabelSeries": "Serier",
"LabelSeriesName": "Serienamn",
"LabelSeriesProgress": "Status för serier",
@@ -523,18 +542,13 @@
"LabelSettingsBookshelfViewHelp": "Bakgrund med ett utseende liknande en bokhylla i trä",
"LabelSettingsChromecastSupport": "Stöd för Chromecast",
"LabelSettingsDateFormat": "Datumformat",
"LabelSettingsDisableWatcher": "Inaktivera Watcher",
"LabelSettingsDisableWatcherForLibrary": "Inaktivera bevakning med Watcher för biblioteket",
"LabelSettingsDisableWatcherHelp": "Inaktiverar automatik att addera/uppdatera<br> objekt när ändringar av filer genomförs.<br>OBS: Kräver en omstart av servern",
"LabelSettingsEnableWatcher": "Aktivera Watcher",
"LabelSettingsEnableWatcherForLibrary": "Aktivera bevakning med Watcher för biblioteket",
"LabelSettingsEnableWatcherHelp": "Aktiverar automatik att addera/uppdatera<br> objekt när ändringar av filer genomförs.<br>OBS: Kräver en omstart av servern",
"LabelSettingsEpubsAllowScriptedContent": "Tillåt e-böcker i epubs-format som innehåller script",
"LabelSettingsEpubsAllowScriptedContentHelp": "Tillåt att epub-filer får använda script.<br>Det rekommenderas att denna inställning är<br>avstängd när du inte litar på källan för epub-filerna.",
"LabelSettingsExperimentalFeatures": "Experimentella funktioner",
"LabelSettingsExperimentalFeaturesHelp": "Funktioner under utveckling som behöver din feedback och hjälp med testning. Klicka för att öppna diskussionen på GitHub.",
"LabelSettingsFindCovers": "Hitta ett omslag",
"LabelSettingsFindCoversHelp": "Om din bok INTE har ett omslag inbäddat i filen eller en fil med omslaget i mappen kommer skannern att försöka hitta ett omslag.<br>OBS: Detta kommer att förlänga inläsningstiden",
"LabelSettingsFindCoversHelp": "Om din bok INTE har ett omslag inkluderat i filen eller en fil med omslaget i mappen kommer skannern att försöka hitta ett omslag.<br>OBS: Detta kommer att förlänga inläsningstiden",
"LabelSettingsHideSingleBookSeries": "Dölj serier som endast innehåller en bok",
"LabelSettingsHideSingleBookSeriesHelp": "Serier som endast har en bok kommer att<br>döljas från sidan 'Serier' och hyllorna på startsidan.",
"LabelSettingsHomePageBookshelfView": "Använd vy liknande en bokhylla på startsidan",
@@ -560,6 +574,7 @@
"LabelSettingsStoreMetadataWithItemHelp": "Som standard lagras metadatafiler i mappen '/metadata/items'. Genom att aktivera detta alternativ kommer metadatafilerna att lagra i din biblioteksmapp",
"LabelSettingsTimeFormat": "Tidsformat",
"LabelShare": "Dela",
"LabelShareDownloadableHelp": "Tillåt att användare som fått en delad länk att ladda ner ett komprimerat objekt från biblioteket.",
"LabelShareURL": "Dela URL-länk",
"LabelShowAll": "Visa alla",
"LabelShowSeconds": "Visa sekunder",
@@ -597,6 +612,7 @@
"LabelTextEditorBulletedList": "Punktlista",
"LabelTextEditorLink": "Länk",
"LabelTextEditorNumberedList": "Numrerad lista",
"LabelTextEditorUnlink": "Radera länk",
"LabelTheme": "Utseende",
"LabelThemeDark": "Mörkt",
"LabelThemeLight": "Ljust",
@@ -612,11 +628,12 @@
"LabelTimeToShift": "Tid att skifta i sekunder",
"LabelTitle": "Titel",
"LabelToolsEmbedMetadata": "Infoga metadata",
"LabelToolsEmbedMetadataDescription": "Bädda in metadata i ljudfiler, inklusive omslagsbild och kapitel.",
"LabelToolsEmbedMetadataDescription": "Infoga metadata i ljudfiler, inklusive omslagsbild och kapitel.",
"LabelToolsM4bEncoder": "Omkodning av M4B-fil",
"LabelToolsMakeM4b": "Skapa ljudboksfil i M4B-format",
"LabelToolsMakeM4bDescription": "Skapa en ljudboksfil i M4B-format med inbäddad metadata, omslagsbild och kapitel.",
"LabelToolsMakeM4bDescription": "Skapa en ljudboksfil i M4B-format som inkluderar metadata, omslagsbild och kapitel.",
"LabelToolsSplitM4b": "Dela upp M4B-fil i MP3-filer",
"LabelToolsSplitM4bDescription": "Skapa MP3-filer från en M4B-fil uppdelad i kapitel med inbäddad metadata, omslagsbild och kapitel.",
"LabelToolsSplitM4bDescription": "Skapa MP3-filer från en M4B-fil uppdelad i kapitel som inkluderar metadata, omslagsbild och kapitel.",
"LabelTotalDuration": "Total varaktighet",
"LabelTotalTimeListened": "Total tid lyssnad",
"LabelTrackFromFilename": "Spår från filnamn",
@@ -665,7 +682,7 @@
"MessageAddToPlayerQueue": "Lägg till i spellistan",
"MessageAppriseDescription": "För att använda den här funktionen behöver du ha en instans av <a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">Apprise API</a> igång eller en API som hanterar dessa begäranden. <br />Apprise API-urlen bör vara hela URL-sökvägen för att skicka meddelandet, t.ex., om din API-instans är tillgänglig på <code>http://192.168.1.1:8337</code>, bör du ange <code>http://192.168.1.1:8337/notify</code>.",
"MessageBackupsDescription": "Säkerhetskopior inkluderar användare, användarnas framsteg, biblioteksobjekt,<br>serverinställningar och bilder lagrade i <code>/metadata/items</code> & <code>/metadata/authors</code>.<br>De inkluderar <strong>INTE</strong> några filer lagrade i dina biblioteksmappar.",
"MessageBackupsLocationEditNote": "OBS: När du ändrar plats för säkerhetskopiorna så flyttas INTE gamla säkerhetskopior dit.",
"MessageBackupsLocationEditNote": "OBS: När du ändrar plats för säkerhetskopiorna så flyttas INTE gamla säkerhetskopior dit",
"MessageBackupsLocationNoEditNote": "OBS: Platsen där säkerhetskopiorna lagras bestäms av en central inställning och kan inte ändras här.",
"MessageBackupsLocationPathEmpty": "Uppgiften om platsen för lagring av säkerhetskopior kan inte lämnas tom",
"MessageBatchEditPopulateMapDetailsAllHelp": "Adderar information från alla objekt nedan i de fält som aktiverats. Om fälten innehåller olika uppgifter kommer informationen att slås samman.",
@@ -703,7 +720,7 @@
"MessageConfirmMarkSeriesNotFinished": "Är du säker på att du vill markera alla böcker i denna serie som ej avslutade?",
"MessageConfirmPurgeCache": "När du rensar cashen kommer katalogen <code>/metadata/cache</code> att raderas. <br /><br />Är du säker på att du vill radera katalogen?",
"MessageConfirmPurgeItemsCache": "När du rensar cashen för föremål kommer katalogen <code>/metadata/cache/items</code> att raderas. <br /><br />Är du säker på att du vill radera katalogen?",
"MessageConfirmQuickEmbed": "VARNING! Quick embed kommer inte att säkerhetskopiera dina ljudfiler. Se till att du har en säkerhetskopia av dina ljudfiler. <br><br>Vill du fortsätta?",
"MessageConfirmQuickEmbed": "VARNING! När du infogar metadata i dina ljudfiler kommer INGEN SÄKERHETSKOPIA av filerna att göras. Se därför till att först säkerhetskopiera ljudfilerna. <br><br>Vill du fortsätta?",
"MessageConfirmQuickMatchEpisodes": "Snabbmatchning av avsnitt kommer att ersätta befintlig information vid en träff. Endast omatchade avsnitt kommer att uppdateras. Vill du fortsätta?",
"MessageConfirmReScanLibraryItems": "Är du säker på att du vill göra en ny skanning för {0} objekt?",
"MessageConfirmRemoveAllChapters": "Är du säker på att du vill ta bort alla kapitel?",
@@ -739,7 +756,7 @@
"MessageJoinUsOn": "Anslut dig till oss på",
"MessageLoading": "Laddar...",
"MessageLoadingFolders": "Laddar mappar...",
"MessageLogsDescription": "Filer med loggar sparas i mappen <code>/metadata/logs</code> som JSON-filer.<br>Filer med information om krascher sparas i <code>/metadata/logs/crash_logs.txt</code>.",
"MessageLogsDescription": "Filer med loggningsinformation sparas i mappen <code>/metadata/logs</code> som JSON-filer.<br>Filer med information om krascher sparas i <code>/metadata/logs/crash_logs.txt</code>.",
"MessageM4BFailed": "M4B misslyckades!",
"MessageM4BFinished": "M4B klar!",
"MessageMapChapterTitles": "Kartlägg kapitelrubriker till dina befintliga ljudbokskapitel utan att justera tidstämplar",
@@ -753,7 +770,7 @@
"MessageNoBackups": "Inga säkerhetskopior",
"MessageNoBookmarks": "Inga bokmärken",
"MessageNoChapters": "Inga kapitel",
"MessageNoCollections": "Inga samlingar",
"MessageNoCollections": "Inga Samlingar",
"MessageNoCoversFound": "Inga omslag hittades",
"MessageNoDescription": "Ingen beskrivning",
"MessageNoDevices": "Inga enheter angivna",
@@ -763,11 +780,11 @@
"MessageNoEpisodes": "Inga avsnitt",
"MessageNoFoldersAvailable": "Inga mappar tillgängliga",
"MessageNoGenres": "Inga kategorier",
"MessageNoIssues": "Inga problem",
"MessageNoIssues": "Inga objekt med problem hittades",
"MessageNoItems": "Inga objekt",
"MessageNoItemsFound": "Inga objekt hittades",
"MessageNoListeningSessions": "Inga lyssningstillfällen",
"MessageNoLogs": "Inga loggar",
"MessageNoLogs": "Inga loggningsinformation finns",
"MessageNoMediaProgress": "Ingen medieförlopp",
"MessageNoNotifications": "Inga aviseringar",
"MessageNoPodcastsFound": "Inga podcasts hittade",
@@ -787,6 +804,8 @@
"MessagePleaseWait": "Vänta ett ögonblick...",
"MessagePodcastHasNoRSSFeedForMatching": "Podcasten har ingen RSS-flödes-URL att använda för matchning",
"MessagePodcastSearchField": "Skriv sökfrågan eller URL-adressen för RSS-flödet",
"MessageQuickEmbedInProgress": "Infogande av metadata pågår",
"MessageQuickEmbedQueue": "Kö för infogaden av metadata ({0} objekt i kön)",
"MessageQuickMatchAllEpisodes": "Snabbmatchning av alla avsnitt",
"MessageQuickMatchDescription": "Adderar uppgifter som saknas samt en omslagsbild från<br>första träffen i resultatet vid sökningen från '{0}'.<br>Skriver inte över befintliga uppgifter om inte<br>inställningen 'Prioritera matchad metadata' är aktiverad.",
"MessageRemoveChapter": "Ta bort kapitel",
@@ -832,6 +851,7 @@
"MessageTaskScanItemsMissing": "{0} saknades",
"MessageTaskScanItemsUpdated": "{0} uppdaterades",
"MessageTaskScanNoChangesNeeded": "Inget adderades eller uppdaterades",
"MessageTaskScanningFileChanges": "Söker efter ändrade filer i \"{0}\"",
"MessageTaskScanningLibrary": "Biblioteket \"{0}\" har skannats",
"MessageTaskTargetDirectoryNotWritable": "Det är inte tillåtet att skriva i den angivna katalogen",
"MessageThinking": "Tänker...",

View File

@@ -558,11 +558,8 @@
"LabelSettingsBookshelfViewHelp": "Імітує вигляд дерев'яних полиць",
"LabelSettingsChromecastSupport": "Підтримка Chromecast",
"LabelSettingsDateFormat": "Формат дати",
"LabelSettingsDisableWatcher": "Вимкнути спостерігача",
"LabelSettingsDisableWatcherForLibrary": "Вимкнути спостерігання тек бібліотеки",
"LabelSettingsDisableWatcherHelp": "Вимикає автоматичне додавання/оновлення елементів, коли спостерігаються зміни файлів. *Потребує перезавантаження сервера",
"LabelSettingsEnableWatcher": "Увімкнути спостерігача",
"LabelSettingsEnableWatcherForLibrary": "Увімкнути спостерігання тек бібліотеки",
"LabelSettingsEnableWatcher": "Автоматично сканувати бібліотеки на наявність змін",
"LabelSettingsEnableWatcherForLibrary": "Автоматично сканувати бібліотеку на наявність змін",
"LabelSettingsEnableWatcherHelp": "Вмикає автоматичне додавання/оновлення елементів, коли спостерігаються зміни файлів. *Потребує перезавантаження сервера",
"LabelSettingsEpubsAllowScriptedContent": "Дозволити JavaScript-вміст у epub",
"LabelSettingsEpubsAllowScriptedContentHelp": "Дозволяти epub-файлам виконувати код. Вмикайте цей параметр лише якщо ви довіряєте джерелу epub-файлів.",
@@ -580,7 +577,7 @@
"LabelSettingsOnlyShowLaterBooksInContinueSeries": "Пропускати попередні книги у Продовжити серії",
"LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "Полиця Продовжити серії на головній сторінці показує найпершу непочату книгу з тих серій, у яких ви завершили хоча б одну книгу та не маєте книг у процесі. Якщо увімкнути це налаштування, то серії продовжуватимуться з останньої завершеної книги, а не з першої непочатої.",
"LabelSettingsParseSubtitles": "Дістати підзаголовки",
"LabelSettingsParseSubtitlesHelp": "Дістати підзаголовки з назв тек аудіокниг.<br>Підзаголовок мусить йти після \" - \"<br>Наприклад, \"Назва книги - Це підзаголовок\" має підзаголовок \"Це підзаголовок\"",
"LabelSettingsParseSubtitlesHelp": "Витягти субтитри з імен папок аудіокниг.<br>Підзаголовки мають бути розділені символом \" - \"<br>тобто. «Назва книги тут підзаголовок» має підзаголовок «Тут підзаголовок»",
"LabelSettingsPreferMatchedMetadata": "Надавати перевагу віднайденим метаданим",
"LabelSettingsPreferMatchedMetadataHelp": "Подробиці буде перезаписано віднайденими даними Швидкого пошуку. Без цього Швидкий пошук заповнить лише подробиці, яких бракує.",
"LabelSettingsSkipMatchingBooksWithASIN": "Не шукати книги, що мають ASIN",

View File

@@ -413,11 +413,6 @@
"LabelSettingsBookshelfViewHelp": "Thiết kế giả lập với kệ gỗ",
"LabelSettingsChromecastSupport": "Hỗ trợ Chromecast",
"LabelSettingsDateFormat": "Định dạng Ngày",
"LabelSettingsDisableWatcher": "Tắt Watcher",
"LabelSettingsDisableWatcherForLibrary": "Tắt watcher thư mục cho thư viện",
"LabelSettingsDisableWatcherHelp": "Tắt chức năng tự động thêm/cập nhật các mục khi phát hiện thay đổi tập tin. *Yêu cầu khởi động lại máy chủ",
"LabelSettingsEnableWatcher": "Bật Watcher",
"LabelSettingsEnableWatcherForLibrary": "Bật watcher thư mục cho thư viện",
"LabelSettingsEnableWatcherHelp": "Bật chức năng tự động thêm/cập nhật các mục khi phát hiện thay đổi tập tin. *Yêu cầu khởi động lại máy chủ",
"LabelSettingsExperimentalFeatures": "Tính năng thử nghiệm",
"LabelSettingsExperimentalFeaturesHelp": "Các tính năng đang phát triển có thể cần phản hồi của bạn và sự giúp đỡ trong thử nghiệm. Nhấp để mở thảo luận trên github.",

View File

@@ -219,6 +219,7 @@
"LabelAccountTypeAdmin": "管理员",
"LabelAccountTypeGuest": "来宾",
"LabelAccountTypeUser": "用户",
"LabelActivities": "活动",
"LabelActivity": "活动",
"LabelAddToCollection": "添加到收藏",
"LabelAddToCollectionBatch": "批量添加 {0} 个媒体到收藏",
@@ -251,7 +252,7 @@
"LabelBackToUser": "返回到用户",
"LabelBackupAudioFiles": "备份音频文件",
"LabelBackupLocation": "备份位置",
"LabelBackupsEnableAutomaticBackups": "启用自动备份",
"LabelBackupsEnableAutomaticBackups": "自动备份",
"LabelBackupsEnableAutomaticBackupsHelp": "备份保存到 /metadata/backups",
"LabelBackupsMaxBackupSize": "最大备份大小 (GB) (0 为无限制)",
"LabelBackupsMaxBackupSizeHelp": "为了防止错误配置, 如果备份超过配置的大小, 备份将失败.",
@@ -283,6 +284,7 @@
"LabelContinueSeries": "继续收听系列",
"LabelCover": "封面",
"LabelCoverImageURL": "封面图像 URL",
"LabelCoverProvider": "封面提供者",
"LabelCreatedAt": "创建时间",
"LabelCronExpression": "计划任务表达式",
"LabelCurrent": "当前",
@@ -391,6 +393,7 @@
"LabelIntervalEvery6Hours": "每 6 小时",
"LabelIntervalEveryDay": "每天",
"LabelIntervalEveryHour": "每小时",
"LabelIntervalEveryMinute": "每分钟",
"LabelInvert": "倒转",
"LabelItem": "项目",
"LabelJumpBackwardAmount": "向后跳转时间",
@@ -555,11 +558,8 @@
"LabelSettingsBookshelfViewHelp": "带有木架子的拟物化设计",
"LabelSettingsChromecastSupport": "Chromecast 支持",
"LabelSettingsDateFormat": "日期格式",
"LabelSettingsDisableWatcher": "禁用监视程序",
"LabelSettingsDisableWatcherForLibrary": "禁用媒体库的文件夹监视程序",
"LabelSettingsDisableWatcherHelp": "检测到文件更改时禁用自动添加和更新项目. *需要重启服务器",
"LabelSettingsEnableWatcher": "启用监视程序",
"LabelSettingsEnableWatcherForLibrary": "为库启用文件夹监视程序",
"LabelSettingsEnableWatcher": "自动扫描库以查找更改",
"LabelSettingsEnableWatcherForLibrary": "自动扫描库以查找更改",
"LabelSettingsEnableWatcherHelp": "当检测到文件更改时, 启用项目的自动添加/更新. *需要重新启动服务器",
"LabelSettingsEpubsAllowScriptedContent": "允许 epubs 中包含脚本内容",
"LabelSettingsEpubsAllowScriptedContentHelp": "允许 epub 文件执行脚本. 建议将此设置保持禁用, 除非你信任 epub 文件的来源.",
@@ -845,6 +845,7 @@
"MessageRestoreBackupConfirm": "你确定要恢复创建的这个备份",
"MessageRestoreBackupWarning": "恢复备份将覆盖位于 /config 的整个数据库并覆盖 /metadata/items & /metadata/authors 中的图像.<br /><br />备份不会修改媒体库文件夹中的任何文件. 如果你已启用服务器设置将封面和元数据存储在库文件夹中,则不会备份或覆盖这些内容.<br /><br />将自动刷新使用服务器的所有客户端.",
"MessageScheduleLibraryScanNote": "对于大多数用户, 建议禁用此功能并保持文件夹监视程序设置启用. 文件夹监视程序将自动检测库文件夹中的更改. 文件夹监视程序不适用于每个文件系统 (如 NFS), 因此可以使用计划库扫描.",
"MessageScheduleRunEveryWeekdayAtTime": "每隔 {0} 在 {1} 运行一次",
"MessageSearchResultsFor": "搜索结果",
"MessageSelected": "{0} 已选择",
"MessageServerCouldNotBeReached": "无法访问服务器",

View File

@@ -458,11 +458,6 @@
"LabelSettingsBookshelfViewHelp": "帶有木架子的擬物化設計",
"LabelSettingsChromecastSupport": "Chromecast 支援",
"LabelSettingsDateFormat": "日期格式",
"LabelSettingsDisableWatcher": "禁用監視程序",
"LabelSettingsDisableWatcherForLibrary": "禁用媒體庫的資料夾監視程序",
"LabelSettingsDisableWatcherHelp": "檢測到檔案更改時禁用自動新增和更新項目. *需要重啟伺服器",
"LabelSettingsEnableWatcher": "啟用監視程序",
"LabelSettingsEnableWatcherForLibrary": "為庫啟用資料夾監視程序",
"LabelSettingsEnableWatcherHelp": "當檢測到檔案更改時, 啟用項目的自動新增/更新. *需要重新啟動伺服器",
"LabelSettingsExperimentalFeatures": "實驗功能",
"LabelSettingsExperimentalFeaturesHelp": "開發中的功能需要你的反饋並幫助測試. 點擊打開 github 討論.",

540
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "audiobookshelf",
"version": "2.19.5",
"version": "2.20.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "audiobookshelf",
"version": "2.19.5",
"version": "2.20.0",
"license": "GPL-3.0",
"dependencies": {
"axios": "^0.27.2",
@@ -25,7 +25,7 @@
"semver": "^7.6.3",
"sequelize": "^6.35.2",
"socket.io": "^4.5.4",
"sqlite3": "^5.1.6",
"sqlite3": "^5.1.7",
"ssrf-req-filter": "^1.1.0",
"xml2js": "^0.5.0"
},
@@ -587,39 +587,6 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
"node_modules/@mapbox/node-pre-gyp": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.10.tgz",
"integrity": "sha512-4ySo4CjzStuprMwk35H5pPbkymjv1SF3jGLj6rAHp/xT/RF7TL7bd9CTm1xDY49K2qF7jmR/g7k+SkLETP6opA==",
"dependencies": {
"detect-libc": "^2.0.0",
"https-proxy-agent": "^5.0.0",
"make-dir": "^3.1.0",
"node-fetch": "^2.6.7",
"nopt": "^5.0.0",
"npmlog": "^5.0.1",
"rimraf": "^3.0.2",
"semver": "^7.3.5",
"tar": "^6.1.11"
},
"bin": {
"node-pre-gyp": "bin/node-pre-gyp"
}
},
"node_modules/@mapbox/node-pre-gyp/node_modules/nopt": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz",
"integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==",
"dependencies": {
"abbrev": "1"
},
"bin": {
"nopt": "bin/nopt.js"
},
"engines": {
"node": ">=6"
}
},
"node_modules/@npmcli/fs": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-1.1.1.tgz",
@@ -741,7 +708,8 @@
"node_modules/abbrev": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
"integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q=="
"integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==",
"devOptional": true
},
"node_modules/accepts": {
"version": "1.3.8",
@@ -759,6 +727,7 @@
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
"integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
"optional": true,
"dependencies": {
"debug": "4"
},
@@ -770,6 +739,7 @@
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
"optional": true,
"dependencies": {
"ms": "2.1.2"
},
@@ -785,7 +755,8 @@
"node_modules/agent-base/node_modules/ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
"optional": true
},
"node_modules/agentkeepalive": {
"version": "4.3.0",
@@ -850,6 +821,7 @@
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"devOptional": true,
"engines": {
"node": ">=8"
}
@@ -897,7 +869,8 @@
"node_modules/aproba": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz",
"integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ=="
"integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==",
"optional": true
},
"node_modules/archy": {
"version": "1.0.0",
@@ -905,18 +878,6 @@
"integrity": "sha512-Xg+9RwCg/0p32teKdGMPTPnVXKD0w3DfHnFTficozsAgsvq2XenPJq/MYpzzQ/v8zrOyJn6Ds39VA4JIDwFfqw==",
"dev": true
},
"node_modules/are-we-there-yet": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz",
"integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==",
"dependencies": {
"delegates": "^1.0.0",
"readable-stream": "^3.6.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/argparse": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
@@ -957,7 +918,28 @@
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"devOptional": true
},
"node_modules/base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT"
},
"node_modules/base64id": {
"version": "2.0.0",
@@ -976,6 +958,26 @@
"node": ">=8"
}
},
"node_modules/bindings": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
"integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
"license": "MIT",
"dependencies": {
"file-uri-to-path": "1.0.0"
}
},
"node_modules/bl": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
"integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
"license": "MIT",
"dependencies": {
"buffer": "^5.5.0",
"inherits": "^2.0.4",
"readable-stream": "^3.4.0"
}
},
"node_modules/body-parser": {
"version": "1.20.1",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz",
@@ -1003,6 +1005,7 @@
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"devOptional": true,
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
@@ -1058,6 +1061,30 @@
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
}
},
"node_modules/buffer": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
"integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT",
"dependencies": {
"base64-js": "^1.3.1",
"ieee754": "^1.1.13"
}
},
"node_modules/buffer-equal-constant-time": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
@@ -1326,6 +1353,7 @@
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz",
"integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==",
"optional": true,
"bin": {
"color-support": "bin.js"
}
@@ -1350,12 +1378,14 @@
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
"devOptional": true
},
"node_modules/console-control-strings": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz",
"integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ=="
"integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==",
"optional": true
},
"node_modules/content-disposition": {
"version": "0.5.4",
@@ -1461,6 +1491,21 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/decompress-response": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
"integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
"license": "MIT",
"dependencies": {
"mimic-response": "^3.1.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/deep-eql": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.3.tgz",
@@ -1473,6 +1518,15 @@
"node": ">=6"
}
},
"node_modules/deep-extend": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
"integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==",
"license": "MIT",
"engines": {
"node": ">=4.0.0"
}
},
"node_modules/default-require-extensions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-3.0.1.tgz",
@@ -1499,7 +1553,8 @@
"node_modules/delegates": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
"integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ=="
"integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==",
"optional": true
},
"node_modules/depd": {
"version": "2.0.0",
@@ -1519,9 +1574,10 @@
}
},
"node_modules/detect-libc": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.1.tgz",
"integrity": "sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w==",
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz",
"integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==",
"license": "Apache-2.0",
"engines": {
"node": ">=8"
}
@@ -1613,7 +1669,8 @@
"node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"devOptional": true
},
"node_modules/encodeurl": {
"version": "1.0.2",
@@ -1644,6 +1701,15 @@
"node": ">=0.10.0"
}
},
"node_modules/end-of-stream": {
"version": "1.4.4",
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
"integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==",
"license": "MIT",
"dependencies": {
"once": "^1.4.0"
}
},
"node_modules/engine.io": {
"version": "6.5.4",
"resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.5.4.tgz",
@@ -1777,6 +1843,15 @@
"node": ">= 0.6"
}
},
"node_modules/expand-template": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz",
"integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==",
"license": "(MIT OR WTFPL)",
"engines": {
"node": ">=6"
}
},
"node_modules/express": {
"version": "4.18.2",
"resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz",
@@ -1844,6 +1919,12 @@
"node": ">= 0.6"
}
},
"node_modules/file-uri-to-path": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
"integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==",
"license": "MIT"
},
"node_modules/fill-range": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
@@ -1993,6 +2074,12 @@
}
]
},
"node_modules/fs-constants": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
"integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
"license": "MIT"
},
"node_modules/fs-minipass": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz",
@@ -2007,7 +2094,8 @@
"node_modules/fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
"devOptional": true
},
"node_modules/function-bind": {
"version": "1.1.2",
@@ -2017,25 +2105,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/gauge": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz",
"integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==",
"dependencies": {
"aproba": "^1.0.3 || ^2.0.0",
"color-support": "^1.1.2",
"console-control-strings": "^1.0.0",
"has-unicode": "^2.0.1",
"object-assign": "^4.1.1",
"signal-exit": "^3.0.0",
"string-width": "^4.2.3",
"strip-ansi": "^6.0.1",
"wide-align": "^1.1.2"
},
"engines": {
"node": ">=10"
}
},
"node_modules/gensync": {
"version": "1.0.0-beta.2",
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
@@ -2085,10 +2154,17 @@
"node": ">=8.0.0"
}
},
"node_modules/github-from-package": {
"version": "0.0.0",
"resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz",
"integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==",
"license": "MIT"
},
"node_modules/glob": {
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
"devOptional": true,
"dependencies": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
@@ -2164,7 +2240,8 @@
"node_modules/has-unicode": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz",
"integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ=="
"integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==",
"optional": true
},
"node_modules/hasha": {
"version": "5.2.2",
@@ -2277,6 +2354,7 @@
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
"integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
"optional": true,
"dependencies": {
"agent-base": "6",
"debug": "4"
@@ -2289,6 +2367,7 @@
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
"optional": true,
"dependencies": {
"ms": "2.1.2"
},
@@ -2304,7 +2383,8 @@
"node_modules/https-proxy-agent/node_modules/ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
"optional": true
},
"node_modules/humanize-ms": {
"version": "1.2.1",
@@ -2326,6 +2406,26 @@
"node": ">=0.10.0"
}
},
"node_modules/ieee754": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "BSD-3-Clause"
},
"node_modules/ignore-by-default": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz",
@@ -2368,6 +2468,7 @@
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
"integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
"devOptional": true,
"dependencies": {
"once": "^1.3.0",
"wrappy": "1"
@@ -2378,6 +2479,12 @@
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
},
"node_modules/ini": {
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
"integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
"license": "ISC"
},
"node_modules/ip": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ip/-/ip-2.0.0.tgz",
@@ -2417,6 +2524,7 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
"devOptional": true,
"engines": {
"node": ">=8"
}
@@ -2885,6 +2993,7 @@
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
"integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==",
"dev": true,
"dependencies": {
"semver": "^6.0.0"
},
@@ -2899,6 +3008,7 @@
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
"dev": true,
"bin": {
"semver": "bin/semver.js"
}
@@ -2993,10 +3103,23 @@
"node": ">= 0.6"
}
},
"node_modules/mimic-response": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
"integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==",
"license": "MIT",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"devOptional": true,
"dependencies": {
"brace-expansion": "^1.1.7"
},
@@ -3004,6 +3127,15 @@
"node": "*"
}
},
"node_modules/minimist": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/minipass": {
"version": "3.3.6",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
@@ -3103,6 +3235,12 @@
"node": ">=10"
}
},
"node_modules/mkdirp-classic": {
"version": "0.5.3",
"resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
"integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==",
"license": "MIT"
},
"node_modules/mocha": {
"version": "10.2.0",
"resolved": "https://registry.npmjs.org/mocha/-/mocha-10.2.0.tgz",
@@ -3399,6 +3537,12 @@
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
"node_modules/napi-build-utils": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz",
"integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==",
"license": "MIT"
},
"node_modules/negotiator": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
@@ -3438,30 +3582,24 @@
"isarray": "0.0.1"
}
},
"node_modules/node-addon-api": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-4.3.0.tgz",
"integrity": "sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ=="
},
"node_modules/node-fetch": {
"version": "2.6.12",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.12.tgz",
"integrity": "sha512-C/fGU2E8ToujUivIO0H+tpQ6HWo4eEmchoPIoXtxCrVghxdKq+QOHqEZW7tuP3KlV3bC8FRMO5nMCC7Zm1VP6g==",
"node_modules/node-abi": {
"version": "3.74.0",
"resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.74.0.tgz",
"integrity": "sha512-c5XK0MjkGBrQPGYG24GBADZud0NCbznxNx0ZkS+ebUTrmV1qTDxPxSL8zEAPURXSbLRWVexxmP4986BziahL5w==",
"license": "MIT",
"dependencies": {
"whatwg-url": "^5.0.0"
"semver": "^7.3.5"
},
"engines": {
"node": "4.x || >=6.0.0"
},
"peerDependencies": {
"encoding": "^0.1.0"
},
"peerDependenciesMeta": {
"encoding": {
"optional": true
}
"node": ">=10"
}
},
"node_modules/node-addon-api": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz",
"integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==",
"license": "MIT"
},
"node_modules/node-gyp": {
"version": "8.4.1",
"resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-8.4.1.tgz",
@@ -3658,17 +3796,6 @@
"node": ">=0.10.0"
}
},
"node_modules/npmlog": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz",
"integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==",
"dependencies": {
"are-we-there-yet": "^2.0.0",
"console-control-strings": "^1.1.0",
"gauge": "^3.0.0",
"set-blocking": "^2.0.0"
}
},
"node_modules/nyc": {
"version": "15.1.0",
"resolved": "https://registry.npmjs.org/nyc/-/nyc-15.1.0.tgz",
@@ -3962,6 +4089,7 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
"integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
"devOptional": true,
"engines": {
"node": ">=0.10.0"
}
@@ -4029,6 +4157,32 @@
"node": ">=8"
}
},
"node_modules/prebuild-install": {
"version": "7.1.3",
"resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz",
"integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==",
"license": "MIT",
"dependencies": {
"detect-libc": "^2.0.0",
"expand-template": "^2.0.3",
"github-from-package": "0.0.0",
"minimist": "^1.2.3",
"mkdirp-classic": "^0.5.3",
"napi-build-utils": "^2.0.0",
"node-abi": "^3.3.0",
"pump": "^3.0.0",
"rc": "^1.2.7",
"simple-get": "^4.0.0",
"tar-fs": "^2.0.0",
"tunnel-agent": "^0.6.0"
},
"bin": {
"prebuild-install": "bin.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/process-on-spawn": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/process-on-spawn/-/process-on-spawn-1.0.0.tgz",
@@ -4078,6 +4232,16 @@
"integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==",
"dev": true
},
"node_modules/pump": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz",
"integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==",
"license": "MIT",
"dependencies": {
"end-of-stream": "^1.1.0",
"once": "^1.3.1"
}
},
"node_modules/qs": {
"version": "6.11.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz",
@@ -4131,6 +4295,30 @@
"node": ">= 0.8"
}
},
"node_modules/rc": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
"integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
"license": "(BSD-2-Clause OR MIT OR Apache-2.0)",
"dependencies": {
"deep-extend": "^0.6.0",
"ini": "~1.3.0",
"minimist": "^1.2.0",
"strip-json-comments": "~2.0.1"
},
"bin": {
"rc": "cli.js"
}
},
"node_modules/rc/node_modules/strip-json-comments": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
"integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/readable-stream": {
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
@@ -4210,6 +4398,7 @@
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
"integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
"devOptional": true,
"dependencies": {
"glob": "^7.1.3"
},
@@ -4404,7 +4593,8 @@
"node_modules/set-blocking": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
"integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw=="
"integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==",
"devOptional": true
},
"node_modules/setprototypeof": {
"version": "1.2.0",
@@ -4448,7 +4638,53 @@
"node_modules/signal-exit": {
"version": "3.0.7",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
"integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="
"integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
"devOptional": true
},
"node_modules/simple-concat": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz",
"integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT"
},
"node_modules/simple-get": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz",
"integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT",
"dependencies": {
"decompress-response": "^6.0.0",
"once": "^1.3.1",
"simple-concat": "^1.0.0"
}
},
"node_modules/simple-update-notifier": {
"version": "1.1.0",
@@ -4701,13 +4937,15 @@
"dev": true
},
"node_modules/sqlite3": {
"version": "5.1.6",
"resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-5.1.6.tgz",
"integrity": "sha512-olYkWoKFVNSSSQNvxVUfjiVbz3YtBwTJj+mfV5zpHmqW3sELx2Cf4QCdirMelhM5Zh+KDVaKgQHqCxrqiWHybw==",
"version": "5.1.7",
"resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-5.1.7.tgz",
"integrity": "sha512-GGIyOiFaG+TUra3JIfkI/zGP8yZYLPQ0pl1bH+ODjiX57sPhrLU5sQJn1y9bDKZUFYkX1crlrPfSYt0BKKdkog==",
"hasInstallScript": true,
"license": "BSD-3-Clause",
"dependencies": {
"@mapbox/node-pre-gyp": "^1.0.0",
"node-addon-api": "^4.2.0",
"bindings": "^1.5.0",
"node-addon-api": "^7.0.0",
"prebuild-install": "^7.1.1",
"tar": "^6.1.11"
},
"optionalDependencies": {
@@ -4770,6 +5008,7 @@
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"devOptional": true,
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
@@ -4783,6 +5022,7 @@
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"devOptional": true,
"dependencies": {
"ansi-regex": "^5.0.1"
},
@@ -4839,6 +5079,40 @@
"node": ">=10"
}
},
"node_modules/tar-fs": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.2.tgz",
"integrity": "sha512-EsaAXwxmx8UB7FRKqeozqEPop69DXcmYwTQwXvyAPF352HJsPdkVhvTaDPYqfNgruveJIJy3TA2l+2zj8LJIJA==",
"license": "MIT",
"dependencies": {
"chownr": "^1.1.1",
"mkdirp-classic": "^0.5.2",
"pump": "^3.0.0",
"tar-stream": "^2.1.4"
}
},
"node_modules/tar-fs/node_modules/chownr": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
"integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
"license": "ISC"
},
"node_modules/tar-stream": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
"integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
"license": "MIT",
"dependencies": {
"bl": "^4.0.3",
"end-of-stream": "^1.4.1",
"fs-constants": "^1.0.0",
"inherits": "^2.0.3",
"readable-stream": "^3.1.1"
},
"engines": {
"node": ">=6"
}
},
"node_modules/tar/node_modules/minipass": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz",
@@ -4907,10 +5181,17 @@
"nodetouch": "bin/nodetouch.js"
}
},
"node_modules/tr46": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
"node_modules/tunnel-agent": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
"integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==",
"license": "Apache-2.0",
"dependencies": {
"safe-buffer": "^5.0.1"
},
"engines": {
"node": "*"
}
},
"node_modules/type-detect": {
"version": "4.0.8",
@@ -5061,20 +5342,6 @@
"node": ">= 0.8"
}
},
"node_modules/webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
},
"node_modules/whatwg-url": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
"dependencies": {
"tr46": "~0.0.3",
"webidl-conversions": "^3.0.0"
}
},
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
@@ -5100,6 +5367,7 @@
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz",
"integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==",
"optional": true,
"dependencies": {
"string-width": "^1.0.2 || 2 || 3 || 4"
}

View File

@@ -1,6 +1,6 @@
{
"name": "audiobookshelf",
"version": "2.19.5",
"version": "2.20.0",
"buildNumber": 1,
"description": "Self-hosted audiobook and podcast server",
"main": "index.js",
@@ -52,7 +52,7 @@
"semver": "^7.6.3",
"sequelize": "^6.35.2",
"socket.io": "^4.5.4",
"sqlite3": "^5.1.6",
"sqlite3": "^5.1.7",
"ssrf-req-filter": "^1.1.0",
"xml2js": "^0.5.0"
},

View File

@@ -782,6 +782,7 @@ class Database {
await this.addTriggerIfNotExists('books', 'titleIgnorePrefix', 'id', 'libraryItems', 'titleIgnorePrefix', 'mediaId')
await this.addTriggerIfNotExists('podcasts', 'title', 'id', 'libraryItems', 'title', 'mediaId')
await this.addTriggerIfNotExists('podcasts', 'titleIgnorePrefix', 'id', 'libraryItems', 'titleIgnorePrefix', 'mediaId')
await this.addAuthorNamesTriggersIfNotExist()
}
async addTriggerIfNotExists(sourceTable, sourceColumn, sourceIdColumn, targetTable, targetColumn, targetIdColumn) {
@@ -806,6 +807,74 @@ class Database {
`)
}
async addAuthorNamesTriggersIfNotExist() {
const libraryItems = 'libraryItems'
const bookAuthors = 'bookAuthors'
const authors = 'authors'
const columns = [
{ name: 'authorNamesFirstLast', source: `${authors}.name`, spec: { type: Sequelize.STRING, allowNull: true } },
{ name: 'authorNamesLastFirst', source: `${authors}.lastFirst`, spec: { type: Sequelize.STRING, allowNull: true } }
]
const authorsSort = `${bookAuthors}.createdAt ASC`
const columnNames = columns.map((column) => column.name).join(', ')
const columnSourcesExpression = columns.map((column) => `GROUP_CONCAT(${column.source}, ', ' ORDER BY ${authorsSort})`).join(', ')
const authorsJoin = `${authors} JOIN ${bookAuthors} ON ${authors}.id = ${bookAuthors}.authorId`
const addBookAuthorsTriggerIfNotExists = async (action) => {
const modifiedRecord = action === 'delete' ? 'OLD' : 'NEW'
const triggerName = this.convertToSnakeCase(`update_${libraryItems}_authorNames_on_${bookAuthors}_${action}`)
const authorNamesSubQuery = `
SELECT ${columnSourcesExpression}
FROM ${authorsJoin}
WHERE ${bookAuthors}.bookId = ${modifiedRecord}.bookId
`
const [[{ count }]] = await this.sequelize.query(`SELECT COUNT(*) as count FROM sqlite_master WHERE type='trigger' AND name='${triggerName}'`)
if (count > 0) return // Trigger already exists
Logger.info(`[Database] Adding trigger ${triggerName}`)
await this.sequelize.query(`
CREATE TRIGGER ${triggerName}
AFTER ${action} ON ${bookAuthors}
FOR EACH ROW
BEGIN
UPDATE ${libraryItems}
SET (${columnNames}) = (${authorNamesSubQuery})
WHERE mediaId = ${modifiedRecord}.bookId;
END;
`)
}
const addAuthorsUpdateTriggerIfNotExists = async () => {
const triggerName = this.convertToSnakeCase(`update_${libraryItems}_authorNames_on_authors_update`)
const authorNamesSubQuery = `
SELECT ${columnSourcesExpression}
FROM ${authorsJoin}
WHERE ${bookAuthors}.bookId = ${libraryItems}.mediaId
`
const [[{ count }]] = await this.sequelize.query(`SELECT COUNT(*) as count FROM sqlite_master WHERE type='trigger' AND name='${triggerName}'`)
if (count > 0) return // Trigger already exists
Logger.info(`[Database] Adding trigger ${triggerName}`)
await this.sequelize.query(`
CREATE TRIGGER ${triggerName}
AFTER UPDATE OF name ON ${authors}
FOR EACH ROW
BEGIN
UPDATE ${libraryItems}
SET (${columnNames}) = (${authorNamesSubQuery})
WHERE mediaId IN (SELECT bookId FROM ${bookAuthors} WHERE authorId = NEW.id);
END;
`)
}
await addBookAuthorsTriggerIfNotExists('insert')
await addBookAuthorsTriggerIfNotExists('delete')
await addAuthorsUpdateTriggerIfNotExists()
}
convertToSnakeCase(str) {
return str.replace(/([A-Z])/g, '_$1').toLowerCase()
}

View File

@@ -21,12 +21,7 @@ class Logger {
}
get levelString() {
for (const key in LogLevel) {
if (LogLevel[key] === this.logLevel) {
return key
}
}
return 'UNKNOWN'
return this.getLogLevelString(this.logLevel)
}
/**

View File

@@ -15,3 +15,4 @@ Please add a record of every database migration that you create to this file. Th
| v2.17.7 | v2.17.7-add-indices | Adds indices to the libraryItems and books tables to reduce query times |
| v2.19.1 | v2.19.1-copy-title-to-library-items | Copies title and titleIgnorePrefix to the libraryItems table, creates update triggers and indices |
| v2.19.4 | v2.19.4-improve-podcast-queries | Adds numEpisodes to podcasts, adds podcastId to mediaProgresses, copies podcast title to libraryItems |
| v2.20.0 | v2.20.0-improve-author-sort-queries | Adds AuthorNames(FirstLast\|LastFirst) to libraryItems to improve author sort queries |

View File

@@ -0,0 +1,272 @@
const util = require('util')
const { Sequelize } = require('sequelize')
/**
* @typedef MigrationContext
* @property {import('sequelize').QueryInterface} queryInterface - a suquelize QueryInterface object.
* @property {import('../Logger')} logger - a Logger object.
*
* @typedef MigrationOptions
* @property {MigrationContext} context - an object containing the migration context.
*/
const migrationVersion = '2.20.0'
const migrationName = `${migrationVersion}-improve-author-sort-queries`
const loggerPrefix = `[${migrationVersion} migration]`
// Migration constants
const libraryItems = 'libraryItems'
const bookAuthors = 'bookAuthors'
const authors = 'authors'
const podcastEpisodes = 'podcastEpisodes'
const columns = [
{ name: 'authorNamesFirstLast', source: `${authors}.name`, spec: { type: Sequelize.STRING, allowNull: true } },
{ name: 'authorNamesLastFirst', source: `${authors}.lastFirst`, spec: { type: Sequelize.STRING, allowNull: true } }
]
const authorsSort = `${bookAuthors}.createdAt ASC`
const columnNames = columns.map((column) => column.name).join(', ')
const columnSourcesExpression = columns.map((column) => `GROUP_CONCAT(${column.source}, ', ' ORDER BY ${authorsSort})`).join(', ')
const authorsJoin = `${authors} JOIN ${bookAuthors} ON ${authors}.id = ${bookAuthors}.authorId`
/**
* This upward migration adds an authorNames column to the libraryItems table and populates it.
* It also creates triggers to update the authorNames column when the corresponding bookAuthors and authors records are updated.
* It also creates an index on the authorNames column.
*
* It also adds an index on publishedAt to the podcastEpisodes table.
*
* @param {MigrationOptions} options - an object containing the migration context.
* @returns {Promise<void>} - A promise that resolves when the migration is complete.
*/
async function up({ context: { queryInterface, logger } }) {
const helper = new MigrationHelper(queryInterface, logger)
// Upwards migration script
logger.info(`${loggerPrefix} UPGRADE BEGIN: ${migrationName}`)
// Add authorNames columns to libraryItems table
await helper.addColumns()
// Populate authorNames columns with the author names for each libraryItem
await helper.populateColumnsFromSource()
// Create triggers to update the authorNames column when the corresponding bookAuthors and authors records are updated
await helper.addTriggers()
// Create indexes on the authorNames columns
await helper.addIndexes()
// Add index on publishedAt to the podcastEpisodes table
await helper.addIndex(podcastEpisodes, ['publishedAt'])
logger.info(`${loggerPrefix} UPGRADE END: ${migrationName}`)
}
/**
* This downward migration removes the authorNames column from the libraryItems table,
* the triggers on the bookAuthors and authors tables, and the index on the authorNames column.
*
* It also removes the index on publishedAt from the podcastEpisodes table.
*
* @param {MigrationOptions} options - an object containing the migration context.
* @returns {Promise<void>} - A promise that resolves when the migration is complete.
*/
async function down({ context: { queryInterface, logger } }) {
// Downward migration script
logger.info(`${loggerPrefix} DOWNGRADE BEGIN: ${migrationName}`)
const helper = new MigrationHelper(queryInterface, logger)
// Remove triggers to update authorNames columns
await helper.removeTriggers()
// Remove index on publishedAt from the podcastEpisodes table
await helper.removeIndex(podcastEpisodes, ['publishedAt'])
// Remove indexes on the authorNames columns
await helper.removeIndexes()
// Remove authorNames columns from libraryItems table
await helper.removeColumns()
logger.info(`${loggerPrefix} DOWNGRADE END: ${migrationName}`)
}
class MigrationHelper {
constructor(queryInterface, logger) {
this.queryInterface = queryInterface
this.logger = logger
}
async addColumn(table, column, options) {
this.logger.info(`${loggerPrefix} adding column "${column}" to table "${table}"`)
const tableDescription = await this.queryInterface.describeTable(table)
if (!tableDescription[column]) {
await this.queryInterface.addColumn(table, column, options)
this.logger.info(`${loggerPrefix} added column "${column}" to table "${table}"`)
} else {
this.logger.info(`${loggerPrefix} column "${column}" already exists in table "${table}"`)
}
}
async addColumns() {
this.logger.info(`${loggerPrefix} adding ${columnNames} columns to ${libraryItems} table`)
for (const column of columns) {
await this.addColumn(libraryItems, column.name, column.spec)
}
this.logger.info(`${loggerPrefix} added ${columnNames} columns to ${libraryItems} table`)
}
async removeColumn(table, column) {
this.logger.info(`${loggerPrefix} removing column "${column}" from table "${table}"`)
const tableDescription = await this.queryInterface.describeTable(table)
if (tableDescription[column]) {
await this.queryInterface.sequelize.query(`ALTER TABLE ${table} DROP COLUMN ${column}`)
this.logger.info(`${loggerPrefix} removed column "${column}" from table "${table}"`)
} else {
this.logger.info(`${loggerPrefix} column "${column}" does not exist in table "${table}"`)
}
}
async removeColumns() {
this.logger.info(`${loggerPrefix} removing ${columnNames} columns from ${libraryItems} table`)
for (const column of columns) {
await this.removeColumn(libraryItems, column.name)
}
this.logger.info(`${loggerPrefix} removed ${columnNames} columns from ${libraryItems} table`)
}
async populateColumnsFromSource() {
this.logger.info(`${loggerPrefix} populating ${columnNames} columns in ${libraryItems} table`)
const authorNamesSubQuery = `
SELECT ${columnSourcesExpression}
FROM ${authorsJoin}
WHERE ${bookAuthors}.bookId = ${libraryItems}.mediaId
`
await this.queryInterface.sequelize.query(`
UPDATE ${libraryItems}
SET (${columnNames}) = (${authorNamesSubQuery})
WHERE mediaType = 'book';
`)
this.logger.info(`${loggerPrefix} populated ${columnNames} columns in ${libraryItems} table`)
}
async addBookAuthorsTrigger(action) {
this.logger.info(`${loggerPrefix} adding trigger to update ${libraryItems} ${columnNames} on ${bookAuthors} ${action}`)
const modifiedRecord = action === 'delete' ? 'OLD' : 'NEW'
const triggerName = convertToSnakeCase(`update_${libraryItems}_authorNames_on_${bookAuthors}_${action}`)
const authorNamesSubQuery = `
SELECT ${columnSourcesExpression}
FROM ${authorsJoin}
WHERE ${bookAuthors}.bookId = ${modifiedRecord}.bookId
`
await this.queryInterface.sequelize.query(`DROP TRIGGER IF EXISTS ${triggerName}`)
await this.queryInterface.sequelize.query(`
CREATE TRIGGER ${triggerName}
AFTER ${action} ON ${bookAuthors}
FOR EACH ROW
BEGIN
UPDATE ${libraryItems}
SET (${columnNames}) = (${authorNamesSubQuery})
WHERE mediaId = ${modifiedRecord}.bookId;
END;
`)
this.logger.info(`${loggerPrefix} added trigger to update ${libraryItems} ${columnNames} on ${bookAuthors} ${action}`)
}
async addAuthorsUpdateTrigger() {
this.logger.info(`${loggerPrefix} adding trigger to update ${libraryItems} ${columnNames} on ${authors} update`)
const triggerName = convertToSnakeCase(`update_${libraryItems}_authorNames_on_authors_update`)
const authorNamesSubQuery = `
SELECT ${columnSourcesExpression}
FROM ${authorsJoin}
WHERE ${bookAuthors}.bookId = ${libraryItems}.mediaId
`
await this.queryInterface.sequelize.query(`DROP TRIGGER IF EXISTS ${triggerName}`)
await this.queryInterface.sequelize.query(`
CREATE TRIGGER ${triggerName}
AFTER UPDATE OF name ON ${authors}
FOR EACH ROW
BEGIN
UPDATE ${libraryItems}
SET (${columnNames}) = (${authorNamesSubQuery})
WHERE mediaId IN (SELECT bookId FROM ${bookAuthors} WHERE authorId = NEW.id);
END;
`)
this.logger.info(`${loggerPrefix} added trigger to update ${libraryItems} ${columnNames} on ${authors} update`)
}
async addTriggers() {
await this.addBookAuthorsTrigger('insert')
await this.addBookAuthorsTrigger('delete')
await this.addAuthorsUpdateTrigger()
}
async removeBookAuthorsTrigger(action) {
this.logger.info(`${loggerPrefix} removing trigger to update ${libraryItems} ${columnNames} on ${bookAuthors} ${action}`)
const triggerName = convertToSnakeCase(`update_${libraryItems}_authorNames_on_${bookAuthors}_${action}`)
await this.queryInterface.sequelize.query(`DROP TRIGGER IF EXISTS ${triggerName}`)
this.logger.info(`${loggerPrefix} removed trigger to update ${libraryItems} ${columnNames} on ${bookAuthors} ${action}`)
}
async removeAuthorsUpdateTrigger() {
this.logger.info(`${loggerPrefix} removing trigger to update ${libraryItems} ${columnNames} on ${authors} update`)
const triggerName = convertToSnakeCase(`update_${libraryItems}_authorNames_on_authors_update`)
await this.queryInterface.sequelize.query(`DROP TRIGGER IF EXISTS ${triggerName}`)
this.logger.info(`${loggerPrefix} removed trigger to update ${libraryItems} ${columnNames} on ${authors} update`)
}
async removeTriggers() {
await this.removeBookAuthorsTrigger('insert')
await this.removeBookAuthorsTrigger('delete')
await this.removeAuthorsUpdateTrigger()
}
async addIndex(tableName, columns) {
const columnString = columns.map((column) => util.inspect(column)).join(', ')
const indexName = convertToSnakeCase(`${tableName}_${columns.map((column) => (typeof column === 'string' ? column : column.name)).join('_')}`)
try {
this.logger.info(`${loggerPrefix} adding index on [${columnString}] to table ${tableName}. index name: ${indexName}"`)
await this.queryInterface.addIndex(tableName, columns)
this.logger.info(`${loggerPrefix} added index on [${columnString}] to table ${tableName}. index name: ${indexName}"`)
} catch (error) {
if (error.name === 'SequelizeDatabaseError' && error.message.includes('already exists')) {
this.logger.info(`${loggerPrefix} index [${columnString}] for table "${tableName}" already exists`)
} else {
throw error
}
}
}
async addIndexes() {
for (const column of columns) {
await this.addIndex(libraryItems, ['libraryId', 'mediaType', { name: column.name, collate: 'NOCASE' }])
}
}
async removeIndex(tableName, columns) {
this.logger.info(`${loggerPrefix} removing index [${columns.join(', ')}] from table "${tableName}"`)
await this.queryInterface.removeIndex(tableName, columns)
this.logger.info(`${loggerPrefix} removed index [${columns.join(', ')}] from table "${tableName}"`)
}
async removeIndexes() {
for (const column of columns) {
await this.removeIndex(libraryItems, ['libraryId', 'mediaType', column.name])
}
}
}
/**
* Utility function to convert a string to snake case, e.g. "titleIgnorePrefix" -> "title_ignore_prefix"
*
* @param {string} str - the string to convert to snake case.
* @returns {string} - the string in snake case.
*/
function convertToSnakeCase(str) {
return str.replace(/([A-Z])/g, '_$1').toLowerCase()
}
module.exports = { up, down }

View File

@@ -374,6 +374,10 @@ class Book extends Model {
if (payload.metadata) {
const metadataStringKeys = ['title', 'subtitle', 'publishedYear', 'publishedDate', 'publisher', 'description', 'isbn', 'asin', 'language']
metadataStringKeys.forEach((key) => {
if (typeof payload.metadata[key] == 'number') {
payload.metadata[key] = String(payload.metadata[key])
}
if ((typeof payload.metadata[key] === 'string' || payload.metadata[key] === null) && this[key] !== payload.metadata[key]) {
this[key] = payload.metadata[key] || null

View File

@@ -77,6 +77,10 @@ class LibraryItem extends Model {
this.title // Only used for sorting
/** @type {string} */
this.titleIgnorePrefix // Only used for sorting
/** @type {string} */
this.authorNamesFirstLast // Only used for sorting
/** @type {string} */
this.authorNamesLastFirst // Only used for sorting
}
/**
@@ -683,7 +687,9 @@ class LibraryItem extends Model {
libraryFiles: DataTypes.JSON,
extraData: DataTypes.JSON,
title: DataTypes.STRING,
titleIgnorePrefix: DataTypes.STRING
titleIgnorePrefix: DataTypes.STRING,
authorNamesFirstLast: DataTypes.STRING,
authorNamesLastFirst: DataTypes.STRING
},
{
sequelize,
@@ -710,6 +716,12 @@ class LibraryItem extends Model {
{
fields: ['libraryId', 'mediaType', { name: 'titleIgnorePrefix', collate: 'NOCASE' }]
},
{
fields: ['libraryId', 'mediaType', { name: 'authorNamesFirstLast', collate: 'NOCASE' }]
},
{
fields: ['libraryId', 'mediaType', { name: 'authorNamesLastFirst', collate: 'NOCASE' }]
},
{
fields: ['libraryId', 'mediaId', 'mediaType']
},

View File

@@ -187,7 +187,7 @@ class MediaProgress extends Model {
if (!this.extraData) this.extraData = {}
if (progressPayload.isFinished !== undefined) {
if (progressPayload.isFinished && !this.isFinished) {
this.finishedAt = Date.now()
this.finishedAt = progressPayload.finishedAt || Date.now()
this.extraData.progress = 1
this.changed('extraData', true)
delete progressPayload.finishedAt

View File

@@ -122,6 +122,10 @@ class PodcastEpisode extends Model {
{
name: 'podcastEpisode_createdAt_podcastId',
fields: ['createdAt', 'podcastId']
},
{
name: 'podcast_episodes_published_at',
fields: ['publishedAt']
}
]
}

View File

@@ -66,10 +66,10 @@ class OpenLibrary {
}
parsePublishYear(doc, worksData) {
if (doc.first_publish_year && !isNaN(doc.first_publish_year)) return doc.first_publish_year
if (doc.first_publish_year && !isNaN(doc.first_publish_year)) return String(doc.first_publish_year)
if (worksData.first_publish_date) {
var year = worksData.first_publish_date.split('-')[0]
if (!isNaN(year)) return year
if (!isNaN(year)) return String(year)
}
return null
}

View File

@@ -392,21 +392,51 @@ class ApiRouter {
async checkRemoveEmptySeries(seriesIds) {
if (!seriesIds?.length) return
const series = await Database.seriesModel.findAll({
where: {
id: seriesIds
},
attributes: ['id', 'name', 'libraryId'],
include: {
model: Database.bookModel,
attributes: ['id']
}
})
const transaction = await Database.sequelize.transaction()
try {
const seriesToRemove = (
await Database.seriesModel.findAll({
where: [
{
id: seriesIds
},
sequelize.where(sequelize.literal('(SELECT count(*) FROM bookSeries bs WHERE bs.seriesId = series.id)'), 0)
],
attributes: ['id', 'name', 'libraryId'],
include: {
model: Database.bookModel,
attributes: ['id'],
required: false // Ensure it includes series even if no books exist
},
transaction
})
).map((s) => ({ id: s.id, name: s.name, libraryId: s.libraryId }))
for (const s of series) {
if (!s.books.length) {
await this.removeEmptySeries(s)
if (seriesToRemove.length) {
await Database.seriesModel.destroy({
where: {
id: seriesToRemove.map((s) => s.id)
},
transaction
})
}
await transaction.commit()
seriesToRemove.forEach(({ id, name, libraryId }) => {
Logger.info(`[ApiRouter] Series "${name}" is now empty. Removing series`)
// Remove series from library filter data
Database.removeSeriesFromFilterData(libraryId, id)
SocketAuthority.emitter('series_removed', { id: id, libraryId: libraryId })
})
// Close rss feeds - remove from db and emit socket event
if (seriesToRemove.length) {
await RssFeedManager.closeFeedsForEntityIds(seriesToRemove.map((s) => s.id))
}
} catch (error) {
await transaction.rollback()
Logger.error(`[ApiRouter] Error removing empty series: ${error.message}`)
}
}
@@ -420,61 +450,56 @@ class ApiRouter {
async checkRemoveAuthorsWithNoBooks(authorIds) {
if (!authorIds?.length) return
const bookAuthorsToRemove = (
await Database.authorModel.findAll({
where: [
{
id: authorIds,
asin: {
[sequelize.Op.or]: [null, '']
const transaction = await Database.sequelize.transaction()
try {
// Select authors with locking to prevent concurrent updates
const bookAuthorsToRemove = (
await Database.authorModel.findAll({
where: [
{
id: authorIds,
asin: {
[sequelize.Op.or]: [null, '']
},
description: {
[sequelize.Op.or]: [null, '']
},
imagePath: {
[sequelize.Op.or]: [null, '']
}
},
description: {
[sequelize.Op.or]: [null, '']
},
imagePath: {
[sequelize.Op.or]: [null, '']
}
},
sequelize.where(sequelize.literal('(SELECT count(*) FROM bookAuthors ba WHERE ba.authorId = author.id)'), 0)
],
attributes: ['id', 'name', 'libraryId'],
raw: true
})
).map((au) => ({ id: au.id, name: au.name, libraryId: au.libraryId }))
sequelize.where(sequelize.literal('(SELECT count(*) FROM bookAuthors ba WHERE ba.authorId = author.id)'), 0)
],
attributes: ['id', 'name', 'libraryId'],
raw: true,
transaction
})
).map((au) => ({ id: au.id, name: au.name, libraryId: au.libraryId }))
if (bookAuthorsToRemove.length) {
await Database.authorModel.destroy({
where: {
id: bookAuthorsToRemove.map((au) => au.id)
}
})
if (bookAuthorsToRemove.length) {
await Database.authorModel.destroy({
where: {
id: bookAuthorsToRemove.map((au) => au.id)
},
transaction
})
}
await transaction.commit()
// Remove all book authors after completing remove from database
bookAuthorsToRemove.forEach(({ id, name, libraryId }) => {
Database.removeAuthorFromFilterData(libraryId, id)
// TODO: Clients were expecting full author in payload but its unnecessary
SocketAuthority.emitter('author_removed', { id, libraryId })
Logger.info(`[ApiRouter] Removed author "${name}" with no books`)
})
} catch (error) {
await transaction.rollback()
Logger.error(`[ApiRouter] Error removing authors: ${error.message}`)
}
}
/**
* Remove an empty series & close an open RSS feed
* @param {import('../models/Series')} series
*/
async removeEmptySeries(series) {
await RssFeedManager.closeFeedForEntityId(series.id)
Logger.info(`[ApiRouter] Series "${series.name}" is now empty. Removing series`)
// Remove series from library filter data
Database.removeSeriesFromFilterData(series.libraryId, series.id)
SocketAuthority.emitter('series_removed', {
id: series.id,
libraryId: series.libraryId
})
await series.destroy()
}
async getUserListeningSessionsHelper(userId) {
const userSessions = await Database.getPlaybackSessions({ userId })
return userSessions.sort((a, b) => b.updatedAt - a.updatedAt)

View File

@@ -523,6 +523,8 @@ class BookScanner {
libraryItemObj.extraData = {}
libraryItemObj.title = bookMetadata.title
libraryItemObj.titleIgnorePrefix = getTitleIgnorePrefix(bookMetadata.title)
libraryItemObj.authorNamesFirstLast = bookMetadata.authors.join(', ')
libraryItemObj.authorNamesLastFirst = bookMetadata.authors.map((author) => Database.authorModel.getLastFirst(author)).join(', ')
// Set isSupplementary flag on ebook library files
for (const libraryFile of libraryItemObj.libraryFiles) {

View File

@@ -4,7 +4,6 @@ const fs = require('../libs/fsExtra')
const date = require('../libs/dateAndTime')
const Logger = require('../Logger')
const { LogLevel } = require('../utils/constants')
const { secondsToTimestamp, elapsedPretty } = require('../utils/index')
class LibraryScan {
@@ -109,20 +108,11 @@ class LibraryScan {
this.elapsed = this.finishedAt - this.startedAt
}
getLogLevelString(level) {
for (const key in LogLevel) {
if (LogLevel[key] === level) {
return key
}
}
return 'UNKNOWN'
}
addLog(level, ...args) {
const logObj = {
timestamp: this.timestamp,
message: args.join(' '),
levelName: this.getLogLevelString(level),
levelName: Logger.getLogLevelString(level),
level
}

View File

@@ -2,24 +2,26 @@ const { parseOpfMetadataXML } = require('../utils/parsers/parseOpfMetadata')
const { readTextFile } = require('../utils/fileUtils')
class OpfFileScanner {
constructor() { }
constructor() {}
/**
* Parse metadata from .opf file found in library scan and update bookMetadata
*
* @param {import('../models/LibraryItem').LibraryFileObject} opfLibraryFileObj
* @param {Object} bookMetadata
*
* @param {import('../models/LibraryItem').LibraryFileObject} opfLibraryFileObj
* @param {Object} bookMetadata
*/
async scanBookOpfFile(opfLibraryFileObj, bookMetadata) {
const xmlText = await readTextFile(opfLibraryFileObj.metadata.path)
const opfMetadata = xmlText ? await parseOpfMetadataXML(xmlText) : null
if (opfMetadata) {
for (const key in opfMetadata) {
if (key === 'tags') { // Add tags only if tags are empty
if (key === 'tags') {
// Add tags only if tags are empty
if (opfMetadata.tags.length) {
bookMetadata.tags = opfMetadata.tags
}
} else if (key === 'genres') { // Add genres only if genres are empty
} else if (key === 'genres') {
// Add genres only if genres are empty
if (opfMetadata.genres.length) {
bookMetadata.genres = opfMetadata.genres
}
@@ -42,4 +44,4 @@ class OpfFileScanner {
}
}
}
module.exports = new OpfFileScanner()
module.exports = new OpfFileScanner()

View File

@@ -1,6 +1,5 @@
const uuidv4 = require("uuid").v4
const uuidv4 = require('uuid').v4
const Logger = require('../Logger')
const { LogLevel } = require('../utils/constants')
class ScanLogger {
constructor() {
@@ -44,20 +43,11 @@ class ScanLogger {
this.elapsed = this.finishedAt - this.startedAt
}
getLogLevelString(level) {
for (const key in LogLevel) {
if (LogLevel[key] === level) {
return key
}
}
return 'UNKNOWN'
}
addLog(level, ...args) {
const logObj = {
timestamp: (new Date()).toISOString(),
timestamp: new Date().toISOString(),
message: args.join(' '),
levelName: this.getLogLevelString(level),
levelName: Logger.getLogLevelString(level),
level
}
@@ -67,4 +57,4 @@ class ScanLogger {
this.logs.push(logObj)
}
}
module.exports = ScanLogger
module.exports = ScanLogger

View File

@@ -107,7 +107,8 @@ async function parse(ebookFile) {
// Attempt to find filepath to cover image:
// Metadata may include <meta name="cover" content="id"/> where content is the id of the cover image in the manifest
// Otherwise the first image in the manifest is used as the cover image
// Otherwise find image in the manifest with cover-image property set
// As a fallback the first image in the manifest is used as the cover image
let packageMetadata = packageJson.package?.metadata
if (Array.isArray(packageMetadata)) {
packageMetadata = packageMetadata[0]
@@ -118,6 +119,9 @@ async function parse(ebookFile) {
if (metaCoverId) {
manifestFirstImage = packageJson.package?.manifest?.[0]?.item?.find((item) => item.$?.id === metaCoverId)
}
if (!manifestFirstImage) {
manifestFirstImage = packageJson.package?.manifest?.[0]?.item?.find((item) => item.$?.['properties']?.split(' ')?.includes('cover-image'))
}
if (!manifestFirstImage) {
manifestFirstImage = packageJson.package?.manifest?.[0]?.item?.find((item) => item.$?.['media-type']?.startsWith('image/'))
}

View File

@@ -22,11 +22,22 @@ function parseCreators(metadata) {
Object.keys(c['$'])
.find((key) => key.startsWith('xmlns:'))
?.split(':')[1] || 'opf'
return {
const creator = {
value: c['_'],
role: c['$'][`${namespace}:role`] || null,
fileAs: c['$'][`${namespace}:file-as`] || null
}
const id = c['$']['id']
if (id && metadata.meta.refines?.some((r) => r.refines === `#${id}`)) {
const creatorMeta = metadata.meta.refines.filter((r) => r.refines === `#${id}`)
if (creatorMeta) {
creator.role = creatorMeta.find((r) => r.property === 'role')?.value || creator.role || null
creator.fileAs = creatorMeta.find((r) => r.property === 'file-as')?.value || creator.fileAs || null
}
}
return creator
})
}
@@ -187,7 +198,6 @@ module.exports.parseOpfMetadataJson = (json) => {
const prefix = packageKey.split(':').shift()
let metadata = prefix ? json[packageKey][`${prefix}:metadata`] || json[packageKey].metadata : json[packageKey].metadata
if (!metadata) return null
if (Array.isArray(metadata)) {
if (!metadata.length) return null
metadata = metadata[0]
@@ -198,12 +208,22 @@ module.exports.parseOpfMetadataJson = (json) => {
metadata.meta = {}
if (metadataMeta?.length) {
metadataMeta.forEach((meta) => {
if (meta && meta['$'] && meta['$'].name) {
if (meta?.['$']?.name) {
metadata.meta[meta['$'].name] = [meta['$'].content || '']
} else if (meta?.['$']?.refines) {
// https://www.w3.org/TR/epub-33/#sec-meta-elem
if (!metadata.meta.refines) {
metadata.meta.refines = []
}
metadata.meta.refines.push({
value: meta._,
refines: meta['$'].refines,
property: meta['$'].property
})
}
})
}
const creators = parseCreators(metadata)
const authors = (fetchCreators(creators, 'aut') || []).map((au) => au?.trim()).filter((au) => au)
const narrators = (fetchNarrators(creators, metadata) || []).map((nrt) => nrt?.trim()).filter((nrt) => nrt)
@@ -227,5 +247,6 @@ module.exports.parseOpfMetadataJson = (json) => {
module.exports.parseOpfMetadataXML = async (xml) => {
const json = await xmlToJSON(xml)
if (!json) return null
return this.parseOpfMetadataJson(json)
}

View File

@@ -264,9 +264,9 @@ module.exports = {
} else if (sortBy === 'media.metadata.publishedYear') {
return [[Sequelize.literal(`CAST(\`book\`.\`publishedYear\` AS INTEGER)`), dir]]
} else if (sortBy === 'media.metadata.authorNameLF') {
return [[Sequelize.literal('author_name COLLATE NOCASE'), dir]]
return [[Sequelize.literal('`libraryItem`.`authorNamesLastFirst` COLLATE NOCASE'), dir]]
} else if (sortBy === 'media.metadata.authorName') {
return [[Sequelize.literal('author_name COLLATE NOCASE'), dir]]
return [[Sequelize.literal('`libraryItem`.`authorNamesFirstLast` COLLATE NOCASE'), dir]]
} else if (sortBy === 'media.metadata.title') {
if (collapseseries) {
return [[Sequelize.literal('display_title COLLATE NOCASE'), dir]]
@@ -397,18 +397,7 @@ module.exports = {
const includeRSSFeed = include.includes('rssfeed')
const includeMediaItemShare = !!user?.isAdminOrUp && include.includes('share')
// For sorting by author name an additional attribute must be added
// with author names concatenated
let bookAttributes = null
if (sortBy === 'media.metadata.authorNameLF') {
bookAttributes = {
include: [[Sequelize.literal(`(SELECT group_concat(lastFirst, ", ") FROM (SELECT a.lastFirst FROM authors AS a, bookAuthors as ba WHERE ba.authorId = a.id AND ba.bookId = book.id ORDER BY ba.createdAt ASC))`), 'author_name']]
}
} else if (sortBy === 'media.metadata.authorName') {
bookAttributes = {
include: [[Sequelize.literal(`(SELECT group_concat(name, ", ") FROM (SELECT a.name FROM authors AS a, bookAuthors as ba WHERE ba.authorId = a.id AND ba.bookId = book.id ORDER BY ba.createdAt ASC))`), 'author_name']]
}
}
const libraryItemWhere = {
libraryId

View File

@@ -465,7 +465,7 @@ module.exports = {
async getRecentEpisodes(user, library, limit, offset) {
const userPermissionPodcastWhere = this.getUserPermissionPodcastWhereQuery(user)
const episodes = await Database.podcastEpisodeModel.findAll({
const findOptions = {
where: {
'$mediaProgresses.isFinished$': {
[Sequelize.Op.or]: [null, false]
@@ -496,7 +496,11 @@ module.exports = {
subQuery: false,
limit,
offset
})
}
const findtAll = process.env.QUERY_PROFILING ? profile(Database.podcastEpisodeModel.findAll.bind(Database.podcastEpisodeModel)) : Database.podcastEpisodeModel.findAll.bind(Database.podcastEpisodeModel)
const episodes = await findtAll(findOptions)
const episodeResults = episodes.map((ep) => {
ep.podcast.podcastEpisodes = [] // Not needed

View File

@@ -44,7 +44,7 @@ function groupFileItemsIntoLibraryItemDirs(mediaType, fileItems, audiobooksOnly
return i.deep > 0 || (mediaType === 'book' && isMediaFile(mediaType, i.extension, audiobooksOnly))
})
// Step 2: Seperate media files and other files
// Step 2: Separate media files and other files
// - Directories without a media file will not be included
/** @type {import('./fileUtils').FilePathItem[]} */
const mediaFileItems = []

View File

@@ -0,0 +1,361 @@
const chai = require('chai')
const sinon = require('sinon')
const { expect } = chai
const { DataTypes, Sequelize } = require('sequelize')
const Logger = require('../../../server/Logger')
const { up, down } = require('../../../server/migrations/v2.20.0-improve-author-sort-queries')
const normalizeWhitespaceAndBackticks = (str) => str.replace(/\s+/g, ' ').trim().replace(/`/g, '')
describe('Migration v2.20.0-improve-author-sort-queries', () => {
let sequelize
let queryInterface
let loggerInfoStub
beforeEach(async () => {
sequelize = new Sequelize({ dialect: 'sqlite', storage: ':memory:', logging: false })
queryInterface = sequelize.getQueryInterface()
loggerInfoStub = sinon.stub(Logger, 'info')
await queryInterface.createTable('libraryItems', {
id: { type: DataTypes.INTEGER, allowNull: false, primaryKey: true, unique: true },
mediaId: { type: DataTypes.INTEGER, allowNull: false },
mediaType: { type: DataTypes.STRING, allowNull: false },
libraryId: { type: DataTypes.INTEGER, allowNull: false }
})
await queryInterface.createTable('authors', {
id: { type: DataTypes.INTEGER, allowNull: false, primaryKey: true, unique: true },
name: { type: DataTypes.STRING, allowNull: false },
lastFirst: { type: DataTypes.STRING, allowNull: false }
})
await queryInterface.createTable('bookAuthors', {
id: { type: DataTypes.INTEGER, allowNull: false, primaryKey: true, unique: true },
bookId: { type: DataTypes.INTEGER, allowNull: false, references: { model: 'libraryItems', key: 'id', onDelete: 'CASCADE' } },
authorId: { type: DataTypes.INTEGER, allowNull: false, references: { model: 'authors', key: 'id', onDelete: 'CASCADE' } },
createdAt: { type: DataTypes.DATE, allowNull: false }
})
await queryInterface.createTable('podcastEpisodes', {
id: { type: DataTypes.INTEGER, allowNull: false, primaryKey: true, unique: true },
publishedAt: { type: DataTypes.DATE, allowNull: true }
})
await queryInterface.bulkInsert('libraryItems', [
{ id: 1, mediaId: 1, mediaType: 'book', libraryId: 1 },
{ id: 2, mediaId: 2, mediaType: 'book', libraryId: 1 }
])
await queryInterface.bulkInsert('authors', [
{ id: 1, name: 'John Doe', lastFirst: 'Doe, John' },
{ id: 2, name: 'Jane Smith', lastFirst: 'Smith, Jane' },
{ id: 3, name: 'John Smith', lastFirst: 'Smith, John' }
])
await queryInterface.bulkInsert('bookAuthors', [
{ id: 1, bookId: 1, authorId: 1, createdAt: '2025-01-01 00:00:00.000 +00:00' },
{ id: 2, bookId: 2, authorId: 2, createdAt: '2025-01-02 00:00:00.000 +00:00' },
{ id: 3, bookId: 1, authorId: 3, createdAt: '2024-12-31 00:00:00.000 +00:00' }
])
await queryInterface.bulkInsert('podcastEpisodes', [
{ id: 1, publishedAt: '2025-01-01 00:00:00.000 +00:00' },
{ id: 2, publishedAt: '2025-01-02 00:00:00.000 +00:00' },
{ id: 3, publishedAt: '2025-01-03 00:00:00.000 +00:00' }
])
})
afterEach(() => {
sinon.restore()
})
describe('up', () => {
it('should add the authorNamesFirstLast and authorNamesLastFirst columns to the libraryItems table', async () => {
await up({ context: { queryInterface, logger: Logger } })
const libraryItems = await queryInterface.describeTable('libraryItems')
expect(libraryItems.authorNamesFirstLast).to.exist
expect(libraryItems.authorNamesLastFirst).to.exist
})
it('should populate the authorNamesFirstLast and authorNamesLastFirst columns with the author names for each libraryItem', async () => {
await up({ context: { queryInterface, logger: Logger } })
const [libraryItems] = await queryInterface.sequelize.query('SELECT * FROM libraryItems')
expect(libraryItems).to.deep.equal([
{ id: 1, mediaId: 1, mediaType: 'book', libraryId: 1, authorNamesFirstLast: 'John Smith, John Doe', authorNamesLastFirst: 'Smith, John, Doe, John' },
{ id: 2, mediaId: 2, mediaType: 'book', libraryId: 1, authorNamesFirstLast: 'Jane Smith', authorNamesLastFirst: 'Smith, Jane' }
])
})
it('should create triggers to update the authorNamesFirstLast and authorNamesLastFirst columns when the corresponding bookAuthors and authors records are updated', async () => {
await up({ context: { queryInterface, logger: Logger } })
const [[{ count }]] = await queryInterface.sequelize.query(`SELECT COUNT(*) as count FROM sqlite_master WHERE type='trigger' AND name='update_library_items_author_names_on_book_authors_insert'`)
expect(count).to.equal(1)
const [[{ sql }]] = await queryInterface.sequelize.query(`SELECT sql FROM sqlite_master WHERE type='trigger' AND name='update_library_items_author_names_on_book_authors_insert'`)
expect(normalizeWhitespaceAndBackticks(sql)).to.equal(
normalizeWhitespaceAndBackticks(`
CREATE TRIGGER update_library_items_author_names_on_book_authors_insert
AFTER insert ON bookAuthors
FOR EACH ROW
BEGIN
UPDATE libraryItems
SET (authorNamesFirstLast, authorNamesLastFirst) = (
SELECT GROUP_CONCAT(authors.name, ', ' ORDER BY bookAuthors.createdAt ASC), GROUP_CONCAT(authors.lastFirst, ', ' ORDER BY bookAuthors.createdAt ASC)
FROM authors JOIN bookAuthors ON authors.id = bookAuthors.authorId
WHERE bookAuthors.bookId = NEW.bookId
)
WHERE mediaId = NEW.bookId;
END
`)
)
const [[{ count: count2 }]] = await queryInterface.sequelize.query(`SELECT COUNT(*) as count FROM sqlite_master WHERE type='trigger' AND name='update_library_items_author_names_on_book_authors_delete'`)
expect(count2).to.equal(1)
const [[{ sql: sql2 }]] = await queryInterface.sequelize.query(`SELECT sql FROM sqlite_master WHERE type='trigger' AND name='update_library_items_author_names_on_book_authors_delete'`)
expect(normalizeWhitespaceAndBackticks(sql2)).to.equal(
normalizeWhitespaceAndBackticks(`
CREATE TRIGGER update_library_items_author_names_on_book_authors_delete
AFTER delete ON bookAuthors
FOR EACH ROW
BEGIN
UPDATE libraryItems
SET (authorNamesFirstLast, authorNamesLastFirst) = (
SELECT GROUP_CONCAT(authors.name, ', ' ORDER BY bookAuthors.createdAt ASC), GROUP_CONCAT(authors.lastFirst, ', ' ORDER BY bookAuthors.createdAt ASC)
FROM authors JOIN bookAuthors ON authors.id = bookAuthors.authorId
WHERE bookAuthors.bookId = OLD.bookId
)
WHERE mediaId = OLD.bookId;
END
`)
)
const [[{ count: count3 }]] = await queryInterface.sequelize.query(`SELECT COUNT(*) as count FROM sqlite_master WHERE type='trigger' AND name='update_library_items_author_names_on_authors_update'`)
expect(count3).to.equal(1)
const [[{ sql: sql3 }]] = await queryInterface.sequelize.query(`SELECT sql FROM sqlite_master WHERE type='trigger' AND name='update_library_items_author_names_on_authors_update'`)
expect(normalizeWhitespaceAndBackticks(sql3)).to.equal(
normalizeWhitespaceAndBackticks(`
CREATE TRIGGER update_library_items_author_names_on_authors_update
AFTER UPDATE OF name ON authors
FOR EACH ROW
BEGIN
UPDATE libraryItems
SET (authorNamesFirstLast, authorNamesLastFirst) = (
SELECT GROUP_CONCAT(authors.name, ', ' ORDER BY bookAuthors.createdAt ASC), GROUP_CONCAT(authors.lastFirst, ', ' ORDER BY bookAuthors.createdAt ASC)
FROM authors JOIN bookAuthors ON authors.id = bookAuthors.authorId
WHERE bookAuthors.bookId = libraryItems.mediaId
)
WHERE mediaId IN (SELECT bookId FROM bookAuthors WHERE authorId = NEW.id);
END
`)
)
})
it('should create indexes on the authorNamesFirstLast and authorNamesLastFirst columns', async () => {
await up({ context: { queryInterface, logger: Logger } })
const [[{ count }]] = await queryInterface.sequelize.query(`SELECT COUNT(*) as count FROM sqlite_master WHERE type='index' AND name='library_items_library_id_media_type_author_names_first_last'`)
expect(count).to.equal(1)
const [[{ sql }]] = await queryInterface.sequelize.query(`SELECT sql FROM sqlite_master WHERE type='index' AND name='library_items_library_id_media_type_author_names_first_last'`)
expect(normalizeWhitespaceAndBackticks(sql)).to.equal(
normalizeWhitespaceAndBackticks(`
CREATE INDEX library_items_library_id_media_type_author_names_first_last ON libraryItems (libraryId, mediaType, authorNamesFirstLast COLLATE NOCASE)
`)
)
const [[{ count: count2 }]] = await queryInterface.sequelize.query(`SELECT COUNT(*) as count FROM sqlite_master WHERE type='index' AND name='library_items_library_id_media_type_author_names_last_first'`)
expect(count2).to.equal(1)
const [[{ sql: sql2 }]] = await queryInterface.sequelize.query(`SELECT sql FROM sqlite_master WHERE type='index' AND name='library_items_library_id_media_type_author_names_last_first'`)
expect(normalizeWhitespaceAndBackticks(sql2)).to.equal(
normalizeWhitespaceAndBackticks(`
CREATE INDEX library_items_library_id_media_type_author_names_last_first ON libraryItems (libraryId, mediaType, authorNamesLastFirst COLLATE NOCASE)
`)
)
})
it('should trigger after update on authors', async () => {
await up({ context: { queryInterface, logger: Logger } })
// update author name
await queryInterface.sequelize.query(`UPDATE authors SET (name, lastFirst) = ('John Wayne', 'Wayne, John') WHERE id = 1`)
// check that the libraryItems table was updated
const [libraryItems] = await queryInterface.sequelize.query(`SELECT * FROM libraryItems`)
expect(libraryItems).to.deep.equal([
{ id: 1, mediaId: 1, mediaType: 'book', libraryId: 1, authorNamesFirstLast: 'John Smith, John Wayne', authorNamesLastFirst: 'Smith, John, Wayne, John' },
{ id: 2, mediaId: 2, mediaType: 'book', libraryId: 1, authorNamesFirstLast: 'Jane Smith', authorNamesLastFirst: 'Smith, Jane' }
])
})
it('should trigger after insert on bookAuthors', async () => {
await up({ context: { queryInterface, logger: Logger } })
// insert a new author
await queryInterface.sequelize.query(`INSERT INTO authors (id, name, lastFirst) VALUES (4, 'John Wayne', 'Wayne, John')`)
// insert a new bookAuthor
await queryInterface.sequelize.query(`INSERT INTO bookAuthors (id, bookId, authorId, createdAt) VALUES (4, 1, 4, '2025-01-04 00:00:00.000 +00:00')`)
// check that the libraryItems table was updated
const [libraryItems] = await queryInterface.sequelize.query(`SELECT * FROM libraryItems`)
expect(libraryItems).to.deep.equal([
{ id: 1, mediaId: 1, mediaType: 'book', libraryId: 1, authorNamesFirstLast: 'John Smith, John Doe, John Wayne', authorNamesLastFirst: 'Smith, John, Doe, John, Wayne, John' },
{ id: 2, mediaId: 2, mediaType: 'book', libraryId: 1, authorNamesFirstLast: 'Jane Smith', authorNamesLastFirst: 'Smith, Jane' }
])
})
it('should trigger after delete on bookAuthors', async () => {
await up({ context: { queryInterface, logger: Logger } })
// delete a bookAuthor
await queryInterface.sequelize.query(`DELETE FROM bookAuthors WHERE id = 1`)
// check that the libraryItems table was updated
const [libraryItems] = await queryInterface.sequelize.query(`SELECT * FROM libraryItems`)
expect(libraryItems).to.deep.equal([
{ id: 1, mediaId: 1, mediaType: 'book', libraryId: 1, authorNamesFirstLast: 'John Smith', authorNamesLastFirst: 'Smith, John' },
{ id: 2, mediaId: 2, mediaType: 'book', libraryId: 1, authorNamesFirstLast: 'Jane Smith', authorNamesLastFirst: 'Smith, Jane' }
])
})
it('should add an index on publishedAt to the podcastEpisodes table', async () => {
await up({ context: { queryInterface, logger: Logger } })
const [[{ count }]] = await queryInterface.sequelize.query(`SELECT COUNT(*) as count FROM sqlite_master WHERE type='index' AND name='podcast_episodes_published_at'`)
expect(count).to.equal(1)
const [[{ sql }]] = await queryInterface.sequelize.query(`SELECT sql FROM sqlite_master WHERE type='index' AND name='podcast_episodes_published_at'`)
expect(normalizeWhitespaceAndBackticks(sql)).to.equal(
normalizeWhitespaceAndBackticks(`
CREATE INDEX podcast_episodes_published_at ON podcastEpisodes (publishedAt)
`)
)
})
it('should be idempotent', async () => {
await up({ context: { queryInterface, logger: Logger } })
await up({ context: { queryInterface, logger: Logger } })
const libraryItemsTable = await queryInterface.describeTable('libraryItems')
expect(libraryItemsTable.authorNamesFirstLast).to.exist
expect(libraryItemsTable.authorNamesLastFirst).to.exist
const [[{ count }]] = await queryInterface.sequelize.query(`SELECT COUNT(*) as count FROM sqlite_master WHERE type='trigger' AND name='update_library_items_author_names_on_book_authors_insert'`)
expect(count).to.equal(1)
const [[{ count: count2 }]] = await queryInterface.sequelize.query(`SELECT COUNT(*) as count FROM sqlite_master WHERE type='trigger' AND name='update_library_items_author_names_on_book_authors_delete'`)
expect(count2).to.equal(1)
const [[{ count: count3 }]] = await queryInterface.sequelize.query(`SELECT COUNT(*) as count FROM sqlite_master WHERE type='trigger' AND name='update_library_items_author_names_on_authors_update'`)
expect(count3).to.equal(1)
const [[{ count: count4 }]] = await queryInterface.sequelize.query(`SELECT COUNT(*) as count FROM sqlite_master WHERE type='index' AND name='library_items_library_id_media_type_author_names_first_last'`)
expect(count4).to.equal(1)
const [[{ count: count5 }]] = await queryInterface.sequelize.query(`SELECT COUNT(*) as count FROM sqlite_master WHERE type='index' AND name='library_items_library_id_media_type_author_names_last_first'`)
expect(count5).to.equal(1)
const [[{ count: count6 }]] = await queryInterface.sequelize.query(`SELECT COUNT(*) as count FROM sqlite_master WHERE type='index' AND name='podcast_episodes_published_at'`)
expect(count6).to.equal(1)
const [libraryItems] = await queryInterface.sequelize.query(`SELECT * FROM libraryItems`)
expect(libraryItems).to.deep.equal([
{ id: 1, mediaId: 1, mediaType: 'book', libraryId: 1, authorNamesFirstLast: 'John Smith, John Doe', authorNamesLastFirst: 'Smith, John, Doe, John' },
{ id: 2, mediaId: 2, mediaType: 'book', libraryId: 1, authorNamesFirstLast: 'Jane Smith', authorNamesLastFirst: 'Smith, Jane' }
])
})
})
describe('down', () => {
it('should remove the authorNamesFirstLast and authorNamesLastFirst columns from the libraryItems table', async () => {
await up({ context: { queryInterface, logger: Logger } })
await down({ context: { queryInterface, logger: Logger } })
const libraryItemsTable = await queryInterface.describeTable('libraryItems')
expect(libraryItemsTable.authorNamesFirstLast).to.not.exist
expect(libraryItemsTable.authorNamesLastFirst).to.not.exist
const [libraryItems] = await queryInterface.sequelize.query(`SELECT * FROM libraryItems`)
expect(libraryItems).to.deep.equal([
{ id: 1, mediaId: 1, mediaType: 'book', libraryId: 1 },
{ id: 2, mediaId: 2, mediaType: 'book', libraryId: 1 }
])
})
it('should remove the triggers from the libraryItems table', async () => {
await up({ context: { queryInterface, logger: Logger } })
await down({ context: { queryInterface, logger: Logger } })
const [[{ count }]] = await queryInterface.sequelize.query(`SELECT COUNT(*) as count FROM sqlite_master WHERE type='trigger' AND name='update_library_items_author_names_on_book_authors_insert'`)
expect(count).to.equal(0)
const [[{ count: count2 }]] = await queryInterface.sequelize.query(`SELECT COUNT(*) as count FROM sqlite_master WHERE type='trigger' AND name='update_library_items_author_names_on_book_authors_delete'`)
expect(count2).to.equal(0)
const [[{ count: count3 }]] = await queryInterface.sequelize.query(`SELECT COUNT(*) as count FROM sqlite_master WHERE type='trigger' AND name='update_library_items_author_names_on_authors_update'`)
expect(count3).to.equal(0)
})
it('should remove the indexes from the libraryItems table', async () => {
await up({ context: { queryInterface, logger: Logger } })
await down({ context: { queryInterface, logger: Logger } })
const [[{ count }]] = await queryInterface.sequelize.query(`SELECT COUNT(*) as count FROM sqlite_master WHERE type='index' AND name='library_items_library_id_media_type_author_names_first_last'`)
expect(count).to.equal(0)
const [[{ count: count2 }]] = await queryInterface.sequelize.query(`SELECT COUNT(*) as count FROM sqlite_master WHERE type='index' AND name='library_items_library_id_media_type_author_names_last_first'`)
expect(count2).to.equal(0)
})
it('should remove the index on publishedAt from the podcastEpisodes table', async () => {
await up({ context: { queryInterface, logger: Logger } })
await down({ context: { queryInterface, logger: Logger } })
const [[{ count }]] = await queryInterface.sequelize.query(`SELECT COUNT(*) as count FROM sqlite_master WHERE type='index' AND name='podcast_episodes_published_at'`)
expect(count).to.equal(0)
})
it('should be idempotent', async () => {
await up({ context: { queryInterface, logger: Logger } })
await down({ context: { queryInterface, logger: Logger } })
await down({ context: { queryInterface, logger: Logger } })
const libraryItemsTable = await queryInterface.describeTable('libraryItems')
expect(libraryItemsTable.authorNamesFirstLast).to.not.exist
expect(libraryItemsTable.authorNamesLastFirst).to.not.exist
const [libraryItems] = await queryInterface.sequelize.query(`SELECT * FROM libraryItems`)
expect(libraryItems).to.deep.equal([
{ id: 1, mediaId: 1, mediaType: 'book', libraryId: 1 },
{ id: 2, mediaId: 2, mediaType: 'book', libraryId: 1 }
])
const [[{ count }]] = await queryInterface.sequelize.query(`SELECT COUNT(*) as count FROM sqlite_master WHERE type='trigger' AND name='update_library_items_author_names_on_book_authors_insert'`)
expect(count).to.equal(0)
const [[{ count: count2 }]] = await queryInterface.sequelize.query(`SELECT COUNT(*) as count FROM sqlite_master WHERE type='trigger' AND name='update_library_items_author_names_on_book_authors_delete'`)
expect(count2).to.equal(0)
const [[{ count: count3 }]] = await queryInterface.sequelize.query(`SELECT COUNT(*) as count FROM sqlite_master WHERE type='trigger' AND name='update_library_items_author_names_on_authors_update'`)
expect(count3).to.equal(0)
const [[{ count: count4 }]] = await queryInterface.sequelize.query(`SELECT COUNT(*) as count FROM sqlite_master WHERE type='index' AND name='library_items_library_id_media_type_author_names_first_last'`)
expect(count4).to.equal(0)
const [[{ count: count5 }]] = await queryInterface.sequelize.query(`SELECT COUNT(*) as count FROM sqlite_master WHERE type='index' AND name='library_items_library_id_media_type_author_names_last_first'`)
expect(count5).to.equal(0)
const [[{ count: count6 }]] = await queryInterface.sequelize.query(`SELECT COUNT(*) as count FROM sqlite_master WHERE type='index' AND name='podcast_episodes_published_at'`)
expect(count6).to.equal(0)
})
})
})

View File

@@ -3,8 +3,8 @@ const expect = chai.expect
const { parseOpfMetadataXML } = require('../../../../server/utils/parsers/parseOpfMetadata')
describe('parseOpfMetadata - test series', async () => {
it('test one series', async () => {
const opf = `
it('test one series', async () => {
const opf = `
<?xml version='1.0' encoding='UTF-8'?>
<package xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:opf="http://www.idpf.org/2007/opf" xml:lang="en" version="3.0" unique-identifier="bookid">
<metadata>
@@ -13,12 +13,12 @@ describe('parseOpfMetadata - test series', async () => {
</metadata>
</package>
`
const parsedOpf = await parseOpfMetadataXML(opf)
expect(parsedOpf.series).to.deep.equal([{ "name": "Serie", "sequence": "1" }])
})
const parsedOpf = await parseOpfMetadataXML(opf)
expect(parsedOpf.series).to.deep.equal([{ name: 'Serie', sequence: '1' }])
})
it('test more then 1 series - in correct order', async () => {
const opf = `
it('test more then 1 series - in correct order', async () => {
const opf = `
<?xml version='1.0' encoding='UTF-8'?>
<package xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:opf="http://www.idpf.org/2007/opf" xml:lang="en" version="3.0" unique-identifier="bookid">
<metadata>
@@ -31,16 +31,16 @@ describe('parseOpfMetadata - test series', async () => {
</metadata>
</package>
`
const parsedOpf = await parseOpfMetadataXML(opf)
expect(parsedOpf.series).to.deep.equal([
{ "name": "Serie 1", "sequence": "1" },
{ "name": "Serie 2", "sequence": "2" },
{ "name": "Serie 3", "sequence": "3" },
])
})
const parsedOpf = await parseOpfMetadataXML(opf)
expect(parsedOpf.series).to.deep.equal([
{ name: 'Serie 1', sequence: '1' },
{ name: 'Serie 2', sequence: '2' },
{ name: 'Serie 3', sequence: '3' }
])
})
it('test messed order of series content and index', async () => {
const opf = `
it('test messed order of series content and index', async () => {
const opf = `
<?xml version='1.0' encoding='UTF-8'?>
<package xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:opf="http://www.idpf.org/2007/opf" xml:lang="en" version="3.0" unique-identifier="bookid">
<metadata>
@@ -52,15 +52,15 @@ describe('parseOpfMetadata - test series', async () => {
</metadata>
</package>
`
const parsedOpf = await parseOpfMetadataXML(opf)
expect(parsedOpf.series).to.deep.equal([
{ "name": "Serie 1", "sequence": "1" },
{ "name": "Serie 3", "sequence": null },
])
})
const parsedOpf = await parseOpfMetadataXML(opf)
expect(parsedOpf.series).to.deep.equal([
{ name: 'Serie 1', sequence: '1' },
{ name: 'Serie 3', sequence: null }
])
})
it('test different values of series content and index', async () => {
const opf = `
it('test different values of series content and index', async () => {
const opf = `
<?xml version='1.0' encoding='UTF-8'?>
<package xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:opf="http://www.idpf.org/2007/opf" xml:lang="en" version="3.0" unique-identifier="bookid">
<metadata>
@@ -73,16 +73,16 @@ describe('parseOpfMetadata - test series', async () => {
</metadata>
</package>
`
const parsedOpf = await parseOpfMetadataXML(opf)
expect(parsedOpf.series).to.deep.equal([
{ "name": "Serie 1", "sequence": null },
{ "name": "Serie 2", "sequence": "abc" },
{ "name": "Serie 3", "sequence": null },
])
})
const parsedOpf = await parseOpfMetadataXML(opf)
expect(parsedOpf.series).to.deep.equal([
{ name: 'Serie 1', sequence: null },
{ name: 'Serie 2', sequence: 'abc' },
{ name: 'Serie 3', sequence: null }
])
})
it('test empty series content', async () => {
const opf = `
it('test empty series content', async () => {
const opf = `
<?xml version='1.0' encoding='UTF-8'?>
<package xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:opf="http://www.idpf.org/2007/opf" xml:lang="en" version="3.0" unique-identifier="bookid">
<metadata>
@@ -91,12 +91,12 @@ describe('parseOpfMetadata - test series', async () => {
</metadata>
</package>
`
const parsedOpf = await parseOpfMetadataXML(opf)
expect(parsedOpf.series).to.deep.equal([])
})
const parsedOpf = await parseOpfMetadataXML(opf)
expect(parsedOpf.series).to.deep.equal([])
})
it('test series and index using an xml namespace', async () => {
const opf = `
it('test series and index using an xml namespace', async () => {
const opf = `
<?xml version='1.0' encoding='UTF-8'?>
<ns0:package xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:opf="http://www.idpf.org/2007/opf" xml:lang="en" version="3.0" unique-identifier="bookid">
<ns0:metadata>
@@ -105,14 +105,12 @@ describe('parseOpfMetadata - test series', async () => {
</ns0:metadata>
</ns0:package>
`
const parsedOpf = await parseOpfMetadataXML(opf)
expect(parsedOpf.series).to.deep.equal([
{ "name": "Serie 1", "sequence": null }
])
})
const parsedOpf = await parseOpfMetadataXML(opf)
expect(parsedOpf.series).to.deep.equal([{ name: 'Serie 1', sequence: null }])
})
it('test series and series index not directly underneath', async () => {
const opf = `
it('test series and series index not directly underneath', async () => {
const opf = `
<?xml version='1.0' encoding='UTF-8'?>
<package xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:opf="http://www.idpf.org/2007/opf" xml:lang="en" version="3.0" unique-identifier="bookid">
<metadata>
@@ -122,9 +120,21 @@ describe('parseOpfMetadata - test series', async () => {
</metadata>
</package>
`
const parsedOpf = await parseOpfMetadataXML(opf)
expect(parsedOpf.series).to.deep.equal([
{ "name": "Serie 1", "sequence": "1" }
])
})
const parsedOpf = await parseOpfMetadataXML(opf)
expect(parsedOpf.series).to.deep.equal([{ name: 'Serie 1', sequence: '1' }])
})
it('test author is parsed from refines meta', async () => {
const opf = `
<package version="3.0" unique-identifier="uuid_id" prefix="rendition: http://www.idpf.org/vocab/rendition/#" xmlns="http://www.idpf.org/2007/opf">
<metadata>
<dc:creator id="create1">Nevil Shute</dc:creator>
<meta refines="#create1" property="role" scheme="marc:relators">aut</meta>
<meta refines="#create1" property="file-as">Shute, Nevil</meta>
</metadata>
</package>
`
const parsedOpf = await parseOpfMetadataXML(opf)
expect(parsedOpf.authors).to.deep.equal(['Nevil Shute'])
})
})