mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2026-11-13 20:33:35 +01:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1d89e3a58c | ||
|
|
75d85e971b | ||
|
|
41cbcf551d | ||
|
|
3e0333877b | ||
|
|
2dde0bf1c7 | ||
|
|
1e60d7af64 | ||
|
|
83dce682e8 | ||
|
|
f88fdf1321 | ||
|
|
20fd0cd700 | ||
|
|
9b055f80a6 | ||
|
|
04acd53667 | ||
|
|
92bae242f1 | ||
|
|
49c0ef6c07 | ||
|
|
34b037087b | ||
|
|
f57ca09f6e | ||
|
|
844b04c6ae | ||
|
|
149bae7d73 | ||
|
|
d6d0cf6b8f | ||
|
|
c38fe60360 | ||
|
|
087099faa3 | ||
|
|
8c87ae8bad | ||
|
|
ce41ce489c | ||
|
|
b31af26837 | ||
|
|
4072054e00 | ||
|
|
f1f57d6595 | ||
|
|
7aaa734d3e | ||
|
|
48d9309eb2 | ||
|
|
f85d03ce26 | ||
|
|
616c8ba152 | ||
|
|
edb1f7efc9 | ||
|
|
92517f473b | ||
|
|
9715c7a8f7 | ||
|
|
95cb5bad27 | ||
|
|
5cb461f9b5 | ||
|
|
16aa80ed5c | ||
|
|
743d915220 | ||
|
|
34715558df | ||
|
|
2a0507ffc6 | ||
|
|
cff295fdc9 | ||
|
|
6b60beff6e | ||
|
|
db433ce076 | ||
|
|
5edd20dfa3 | ||
|
|
3563d49424 | ||
|
|
06935b78b5 | ||
|
|
4aee64da07 | ||
|
|
02cf2da4fa | ||
|
|
366320bf94 | ||
|
|
a2cba8dd05 | ||
|
|
86bc18487b | ||
|
|
9e18cb41e9 | ||
|
|
7d10b8fc4a | ||
|
|
14bec248f1 | ||
|
|
ff57368f37 | ||
|
|
f589397008 | ||
|
|
fa4f6b58f3 | ||
|
|
1acfd2ae54 | ||
|
|
91717e3b96 | ||
|
|
282d8b8bf6 | ||
|
|
fb4c3ba984 | ||
|
|
72a1c656f9 | ||
|
|
273693e8ad | ||
|
|
d22c468215 | ||
|
|
c654de4630 | ||
|
|
aef33df8d3 | ||
|
|
63a883e595 | ||
|
|
b8b069eca6 | ||
|
|
0fd0483021 | ||
|
|
8463b8426a | ||
|
|
e874d31e64 | ||
|
|
557f56ff21 | ||
|
|
7f296a5135 | ||
|
|
b5134a3287 | ||
|
|
8fb7a856d5 | ||
|
|
ad8b235058 | ||
|
|
186733f6af | ||
|
|
1e5443f054 | ||
|
|
18e32410ad | ||
|
|
5077f87e0c | ||
|
|
608d3ddde8 | ||
|
|
4c27a7b68e | ||
|
|
a3b150d049 | ||
|
|
088c4f542f | ||
|
|
6acb4b9005 | ||
|
|
8781930ced | ||
|
|
4236eef5c4 | ||
|
|
b14224a6d8 | ||
|
|
a6128a6c98 | ||
|
|
8a2a45751e | ||
|
|
83e9a82f8f | ||
|
|
bf4f74cb93 | ||
|
|
1e88ff01e7 | ||
|
|
5ec4a8a46e | ||
|
|
99e2c45482 | ||
|
|
3c5d191c33 | ||
|
|
b67cbffbf9 | ||
|
|
eeaa408a2d | ||
|
|
4b67c170ce | ||
|
|
d21aec237c | ||
|
|
632ccd5cbf | ||
|
|
62efd5c583 | ||
|
|
9244f9ea5a | ||
|
|
7e76eed8bb | ||
|
|
e4e212cca7 | ||
|
|
fb43db4f75 | ||
|
|
3b4895b1b0 | ||
|
|
a06639886b | ||
|
|
37d7ee20ed | ||
|
|
eb8ed7f5b6 | ||
|
|
3d62a103ae | ||
|
|
42bf9512b0 | ||
|
|
5cb75a85ec | ||
|
|
3c1bbc7f8e | ||
|
|
b204395b8e | ||
|
|
e4c69fe68b | ||
|
|
def8fca33a | ||
|
|
e8ed987034 | ||
|
|
d5be05b11e | ||
|
|
580f9fb649 | ||
|
|
7aecf129ac | ||
|
|
4056f37390 | ||
|
|
6bc2a89827 | ||
|
|
eba1909c9c | ||
|
|
010a73a16a | ||
|
|
ed6663054f | ||
|
|
01263e9379 | ||
|
|
1ab6120596 | ||
|
|
c097655c5c | ||
|
|
9f541063db | ||
|
|
3f2da7febc | ||
|
|
24c68507e7 | ||
|
|
b325d0ee1d | ||
|
|
5f6fa4adbf | ||
|
|
78130a7823 | ||
|
|
e4556b0307 | ||
|
|
6f57182da0 | ||
|
|
1aef21169c | ||
|
|
e02c859437 | ||
|
|
9f475ef3d5 | ||
|
|
5ad5768308 | ||
|
|
d38bf9e868 | ||
|
|
1027b85e68 | ||
|
|
67992540c0 | ||
|
|
733c795989 | ||
|
|
0a797ab8be | ||
|
|
fbfb24c264 | ||
|
|
2440fb37c4 | ||
|
|
65c075283c | ||
|
|
198b2c27b8 | ||
|
|
d6398e54dd | ||
|
|
c0b9110e4e | ||
|
|
13432209ee | ||
|
|
0571747fab | ||
|
|
ec2738cb0f | ||
|
|
a7ebb3e1af | ||
|
|
513a0679f5 | ||
|
|
3f9afe57bc | ||
|
|
ac63b0c7fc | ||
|
|
1b46d68027 | ||
|
|
d511612f21 | ||
|
|
94400acdcd | ||
|
|
6caa1aa2da | ||
|
|
32fae4a7c6 | ||
|
|
96d4021a3c | ||
|
|
49826f5163 | ||
|
|
76e33a2a4e | ||
|
|
1a440811c5 | ||
|
|
529a16b718 | ||
|
|
032f0b6b51 | ||
|
|
f5bfa1edd6 | ||
|
|
4dfad46722 | ||
|
|
c86f336536 | ||
|
|
1876bfbfe7 | ||
|
|
a7b380cd7a | ||
|
|
71c5d27ea5 | ||
|
|
83707e08bf | ||
|
|
e8d1b58bd2 | ||
|
|
aff79b5579 | ||
|
|
321d8bfda1 | ||
|
|
a3ff41f90e | ||
|
|
60f9ac93f7 | ||
|
|
bd61a43458 | ||
|
|
df6ec07f18 | ||
|
|
6d02aa3aa0 | ||
|
|
144fe743c6 | ||
|
|
0851b346d0 | ||
|
|
53e904314e | ||
|
|
84f80f1903 | ||
|
|
98b1f4abad | ||
|
|
ec0d147d6d | ||
|
|
a7825bb461 | ||
|
|
09be7799f6 | ||
|
|
dc6a8a7292 | ||
|
|
0c075674a0 | ||
|
|
5b13820cb4 | ||
|
|
d5b4d57fe3 | ||
|
|
f12b64495a | ||
|
|
43f3150a52 | ||
|
|
65d3a668e3 | ||
|
|
2b5297bee8 | ||
|
|
08caeb5975 | ||
|
|
6c170a5064 | ||
|
|
4b62f90964 | ||
|
|
e9ea511c90 | ||
|
|
c6afeb7dc9 | ||
|
|
35a2c5e87c | ||
|
|
4c71a076e7 | ||
|
|
e56c1099e6 | ||
|
|
dc0b24a03f | ||
|
|
c900676be7 | ||
|
|
a6cde926ee | ||
|
|
e8db14fe33 | ||
|
|
e9b533637f | ||
|
|
8b97da500d | ||
|
|
37b42601ef | ||
|
|
5da4ec51ce | ||
|
|
4c65a87d5c | ||
|
|
0e5618dcd8 | ||
|
|
d063d851e1 | ||
|
|
6458e6d3d2 | ||
|
|
70953d8c82 | ||
|
|
2d9f63963e | ||
|
|
f12abca3fc | ||
|
|
bced308462 | ||
|
|
528d03fef3 | ||
|
|
b93c3133a9 | ||
|
|
8d9db8d103 | ||
|
|
08bbf9cd9d | ||
|
|
95af4812cd | ||
|
|
8ec547e302 | ||
|
|
32e4da8c92 | ||
|
|
0ce37004af | ||
|
|
527745b54e | ||
|
|
3c016314e6 | ||
|
|
59c7d0275c | ||
|
|
4883d0f60c | ||
|
|
9d9ab49664 | ||
|
|
82aec5f60c | ||
|
|
7ee5e1bdf6 | ||
|
|
3bb53d939d | ||
|
|
6f03467f35 | ||
|
|
b2f593f1c4 | ||
|
|
58dcda36a5 | ||
|
|
354e0bf793 | ||
|
|
0784373b61 | ||
|
|
2f219ea3cc | ||
|
|
56e60b8420 | ||
|
|
9b92b5de34 | ||
|
|
3417c0c721 | ||
|
|
cbda0360aa | ||
|
|
036bc081f0 | ||
|
|
42947e2e64 | ||
|
|
c4e7172c2f | ||
|
|
4502d37d4f | ||
|
|
e70e4b9d40 | ||
|
|
aacdcc47ec | ||
|
|
499b52b4dd | ||
|
|
1bad2d9072 | ||
|
|
c009db9f28 | ||
|
|
325469c5a5 | ||
|
|
c97b36e11c | ||
|
|
e944b2a2f5 | ||
|
|
2d0a5462d2 | ||
|
|
72dc75482f | ||
|
|
cac74f3477 | ||
|
|
1ad11b2b9e | ||
|
|
50eeca2e0f | ||
|
|
4f21fc023c | ||
|
|
52a485d135 | ||
|
|
3b025076e8 | ||
|
|
6d5d89429d | ||
|
|
c010f0e1eb | ||
|
|
eee377e081 | ||
|
|
b0aaa24660 | ||
|
|
47ea6b5092 | ||
|
|
4b060febc2 | ||
|
|
40869bcf39 | ||
|
|
3942805129 | ||
|
|
dc446862c1 | ||
|
|
379f6c716a | ||
|
|
47457ee1e7 | ||
|
|
cb6ff9eedf | ||
|
|
5dc01261c1 | ||
|
|
cbc103cf05 | ||
|
|
e79256d0fb | ||
|
|
f8ef56c6bc | ||
|
|
62d7097e23 | ||
|
|
92df92ec99 | ||
|
|
1c229e0627 | ||
|
|
f8a71cc514 | ||
|
|
63de5bb2d5 | ||
|
|
2c3108a1fa | ||
|
|
928051744a | ||
|
|
3ccdcaec1a | ||
|
|
f47bbc7886 | ||
|
|
7c0ca44727 | ||
|
|
d6a2e5596b | ||
|
|
a5362de9cc | ||
|
|
9ab35ef418 | ||
|
|
79cc9765cf | ||
|
|
5b2a788cfc | ||
|
|
80b39abaa2 | ||
|
|
b41db23994 | ||
|
|
125f265f55 | ||
|
|
aa4a191567 | ||
|
|
e431ea0472 | ||
|
|
e3388d4446 | ||
|
|
88879f1409 | ||
|
|
3e0099e8d9 | ||
|
|
f558182d94 | ||
|
|
a30fe15b10 | ||
|
|
0bbf8bde5c | ||
|
|
0e2cdde731 | ||
|
|
bc6bfbe804 | ||
|
|
2755204168 | ||
|
|
2d4df273f0 | ||
|
|
d73b64a19c | ||
|
|
b7e8a0474a | ||
|
|
39adefb632 | ||
|
|
24cab79c66 | ||
|
|
b27f21fd95 | ||
|
|
09fa0b38f5 | ||
|
|
455e605162 | ||
|
|
88667d00a1 | ||
|
|
94c426bd97 | ||
|
|
522b9735e2 | ||
|
|
5a6b3d8e61 | ||
|
|
64cbf59609 | ||
|
|
fda1a6ea9b | ||
|
|
c4c8b8d0f2 | ||
|
|
ab3bd6f4a1 | ||
|
|
093124aac6 | ||
|
|
5de92d08f9 | ||
|
|
8b89b27654 | ||
|
|
3faa6f3e7d | ||
|
|
9821c31f8e | ||
|
|
efe2a22674 | ||
|
|
9634c46bc5 | ||
|
|
5f8db24b96 | ||
|
|
e781ff5eae | ||
|
|
32a17c0044 | ||
|
|
f84831d6f1 | ||
|
|
dc54d42dcf | ||
|
|
15af7407ff | ||
|
|
5d9682410a | ||
|
|
4bdd76d94c | ||
|
|
7c0d9efe91 | ||
|
|
874e9e1856 | ||
|
|
d9355ac3aa | ||
|
|
a9e12657f5 | ||
|
|
cfeb6bd502 | ||
|
|
077b523bd6 | ||
|
|
b8a2d113f0 | ||
|
|
e1ae4f2d31 | ||
|
|
7aa2f84daa | ||
|
|
da0a64daed | ||
|
|
dfce15a74c |
No files matched your search
@@ -1,40 +1,31 @@
|
||||
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
|
||||
// README at: https://github.com/devcontainers/templates/tree/main/src/javascript-node
|
||||
{
|
||||
"name": "Audiobookshelf",
|
||||
"build": {
|
||||
"dockerfile": "Dockerfile",
|
||||
// Update 'VARIANT' to pick a Node version: 18, 16, 14.
|
||||
// Append -bullseye or -buster to pin to an OS version.
|
||||
// Use -bullseye variants on local arm64/Apple Silicon.
|
||||
"args": {
|
||||
"VARIANT": "20"
|
||||
}
|
||||
},
|
||||
"mounts": [
|
||||
"source=abs-server-node_modules,target=${containerWorkspaceFolder}/node_modules,type=volume",
|
||||
"source=abs-client-node_modules,target=${containerWorkspaceFolder}/client/node_modules,type=volume"
|
||||
],
|
||||
// Features to add to the dev container. More info: https://containers.dev/features.
|
||||
// "features": {},
|
||||
// Use 'forwardPorts' to make a list of ports inside the container available locally.
|
||||
"forwardPorts": [
|
||||
3000,
|
||||
3333
|
||||
],
|
||||
// Use 'postCreateCommand' to run commands after the container is created.
|
||||
"postCreateCommand": "sh .devcontainer/post-create.sh",
|
||||
// Configure tool-specific properties.
|
||||
"customizations": {
|
||||
// Configure properties specific to VS Code.
|
||||
"vscode": {
|
||||
// Add the IDs of extensions you want installed when the container is created.
|
||||
"extensions": [
|
||||
"dbaeumer.vscode-eslint",
|
||||
"octref.vetur"
|
||||
]
|
||||
}
|
||||
}
|
||||
// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
|
||||
// "remoteUser": "root"
|
||||
}
|
||||
"name": "Audiobookshelf",
|
||||
"build": {
|
||||
"dockerfile": "Dockerfile",
|
||||
// Update 'VARIANT' to pick a Node version: 18, 16, 14.
|
||||
// Append -bullseye or -buster to pin to an OS version.
|
||||
// Use -bullseye variants on local arm64/Apple Silicon.
|
||||
"args": {
|
||||
"VARIANT": "24"
|
||||
}
|
||||
},
|
||||
"mounts": ["source=abs-server-node_modules,target=${containerWorkspaceFolder}/node_modules,type=volume", "source=abs-client-node_modules,target=${containerWorkspaceFolder}/client/node_modules,type=volume"],
|
||||
// Features to add to the dev container. More info: https://containers.dev/features.
|
||||
// "features": {},
|
||||
// Use 'forwardPorts' to make a list of ports inside the container available locally.
|
||||
"forwardPorts": [3000, 3333],
|
||||
// Use 'postCreateCommand' to run commands after the container is created.
|
||||
"postCreateCommand": "sh .devcontainer/post-create.sh",
|
||||
// Configure tool-specific properties.
|
||||
"customizations": {
|
||||
// Configure properties specific to VS Code.
|
||||
"vscode": {
|
||||
// Add the IDs of extensions you want installed when the container is created.
|
||||
"extensions": ["dbaeumer.vscode-eslint", "octref.vetur"]
|
||||
}
|
||||
}
|
||||
// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
|
||||
// "remoteUser": "root"
|
||||
}
|
||||
@@ -34,7 +34,7 @@ jobs:
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
node-version: 24
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install dependencies
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
# Builds the v3 test image
|
||||
#
|
||||
# Moves the :v3 tag on Docker Hub and GHCR, the same way master
|
||||
# moves :edge. Using the server's v3 branch and react-client master.
|
||||
#
|
||||
# repository_dispatch (build-v3) is for triggering a build on updates to react-client
|
||||
|
||||
name: Build and Push v3 Docker Image
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches: [v3]
|
||||
repository_dispatch:
|
||||
types: [build-v3]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
if: ${{ github.repository == 'advplyr/audiobookshelf' && (github.event_name != 'push' || !contains(github.event.head_commit.message, 'skip ci')) }}
|
||||
runs-on: ubuntu-24.04
|
||||
|
||||
steps:
|
||||
- name: Check out server
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event_name == 'repository_dispatch' && 'v3' || github.sha }}
|
||||
|
||||
- name: Check out React client
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: audiobookshelf/audiobookshelf-client-react
|
||||
ref: master
|
||||
path: client-react
|
||||
|
||||
- name: Server sha
|
||||
id: server-sha
|
||||
run: echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Client sha
|
||||
id: client-sha
|
||||
working-directory: client-react
|
||||
run: echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: advplyr/audiobookshelf,ghcr.io/${{ github.repository_owner }}/audiobookshelf
|
||||
flavor: |
|
||||
latest=false
|
||||
tags: |
|
||||
type=raw,value=v3
|
||||
labels: |
|
||||
audiobookshelf.server.sha=${{ steps.server-sha.outputs.sha }}
|
||||
audiobookshelf.client.sha=${{ steps.client-sha.outputs.sha }}
|
||||
|
||||
- name: Setup QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Cache Docker layers
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: /tmp/.buildx-cache
|
||||
key: ${{ runner.os }}-buildx-v3-${{ github.sha }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-buildx-v3-
|
||||
|
||||
- name: Login to Dockerhub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
|
||||
- name: Login to ghcr
|
||||
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@v6
|
||||
with:
|
||||
file: client-react/Dockerfile
|
||||
context: .
|
||||
build-contexts: |
|
||||
abs-client=client-react
|
||||
abs-server=.
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
cache-from: type=local,src=/tmp/.buildx-cache
|
||||
cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max
|
||||
|
||||
- name: Move cache
|
||||
run: |
|
||||
rm -rf /tmp/.buildx-cache
|
||||
mv /tmp/.buildx-cache-new /tmp/.buildx-cache
|
||||
@@ -19,6 +19,8 @@ on:
|
||||
- server/**
|
||||
- index.js
|
||||
- package.json
|
||||
- tsconfig.server.json
|
||||
- Dockerfile
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
@@ -20,7 +20,7 @@ jobs:
|
||||
- name: Set up node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
node-version: 24
|
||||
cache: 'npm'
|
||||
|
||||
# The only argument is the `directory`, which is where the i18n files are
|
||||
|
||||
@@ -12,6 +12,7 @@ on:
|
||||
- test/**
|
||||
- index.js
|
||||
- package.json
|
||||
- tsconfig.server.json
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@@ -23,10 +24,10 @@ jobs:
|
||||
- name: setup node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
node-version: 24
|
||||
cache: 'npm'
|
||||
|
||||
- name: install pkg (using yao-pkg fork for targeting node20)
|
||||
- name: install pkg (using yao-pkg fork for targeting node24)
|
||||
run: npm install -g @yao-pkg/pkg
|
||||
|
||||
- name: get client dependencies
|
||||
@@ -37,11 +38,11 @@ jobs:
|
||||
working-directory: client
|
||||
run: npm run generate
|
||||
|
||||
- name: get server dependencies
|
||||
run: npm ci --only=production
|
||||
- name: get server dependencies and compile server
|
||||
run: npm ci && npm run build:server && npm prune --omit=dev
|
||||
|
||||
- name: build binary
|
||||
run: pkg -t node20-linux-x64 -o audiobookshelf .
|
||||
run: pkg --config package.json -t node24-linux-x64 -o audiobookshelf dist-server/index.js
|
||||
|
||||
- name: run audiobookshelf
|
||||
run: |
|
||||
|
||||
@@ -23,7 +23,7 @@ jobs:
|
||||
- name: Set up node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
node-version: 24
|
||||
cache: 'npm'
|
||||
|
||||
# Install Redocly CLI
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
name: Prepare translation credits
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
release-notes:
|
||||
if: github.event_name == 'workflow_dispatch' || contains(github.event.head_commit.message, 'weblate')
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- id: translator-credits
|
||||
uses: audiobookshelf/get-translator-credits@v1.1.0
|
||||
|
||||
- name: Show translator credits
|
||||
run: |
|
||||
printf '%s\n' "${{ steps.translator-credits.outputs.credits }}"
|
||||
@@ -28,11 +28,14 @@ jobs:
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
node-version: 24
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Run lint
|
||||
run: npm run lint
|
||||
|
||||
- name: Run tests
|
||||
run: npm test
|
||||
@@ -11,6 +11,7 @@
|
||||
/client/.nuxt/
|
||||
/client/dist/
|
||||
/dist/
|
||||
/dist-server/
|
||||
/deploy/
|
||||
/coverage/
|
||||
/.nyc_output/
|
||||
|
||||
+16
-7
@@ -2,15 +2,23 @@ ARG NUSQLITE3_DIR="/usr/local/lib/nusqlite3"
|
||||
ARG NUSQLITE3_PATH="${NUSQLITE3_DIR}/libnusqlite3.so"
|
||||
|
||||
### STAGE 0: Build client ###
|
||||
FROM node:20-alpine AS build-client
|
||||
FROM node:24-alpine AS build-client
|
||||
|
||||
WORKDIR /client
|
||||
COPY /client /client
|
||||
RUN npm ci && npm cache clean --force
|
||||
RUN npm run generate
|
||||
|
||||
### STAGE 1: Build server ###
|
||||
FROM node:20-alpine AS build-server
|
||||
### STAGE 1: Compile server on the builder CPU (avoid QEMU SIGILL from tsc on arm64) ###
|
||||
FROM --platform=$BUILDPLATFORM node:24-alpine AS compile-server
|
||||
|
||||
WORKDIR /server
|
||||
COPY index.js package* tsconfig.server.json /server
|
||||
COPY /server /server/server
|
||||
RUN npm ci --include=dev --ignore-scripts && npm run build:server
|
||||
|
||||
### STAGE 2: Install native server deps for the target arch ###
|
||||
FROM node:24-alpine AS build-server
|
||||
|
||||
ARG NUSQLITE3_DIR
|
||||
ARG TARGETPLATFORM
|
||||
@@ -27,6 +35,7 @@ RUN apk add --no-cache --update \
|
||||
WORKDIR /server
|
||||
COPY index.js package* /server
|
||||
COPY /server /server/server
|
||||
COPY --from=compile-server /server/dist-server /server/dist-server
|
||||
|
||||
RUN case "$TARGETPLATFORM" in \
|
||||
"linux/amd64") \
|
||||
@@ -38,10 +47,10 @@ RUN case "$TARGETPLATFORM" in \
|
||||
unzip /tmp/library.zip -d $NUSQLITE3_DIR && \
|
||||
rm /tmp/library.zip
|
||||
|
||||
RUN npm ci --only=production
|
||||
RUN npm ci --omit=dev
|
||||
|
||||
### STAGE 2: Create minimal runtime image ###
|
||||
FROM node:20-alpine
|
||||
### STAGE 3: Create minimal runtime image ###
|
||||
FROM node:24-alpine
|
||||
|
||||
ARG NUSQLITE3_DIR
|
||||
ARG NUSQLITE3_PATH
|
||||
@@ -70,4 +79,4 @@ ENV NUSQLITE3_DIR=${NUSQLITE3_DIR}
|
||||
ENV NUSQLITE3_PATH=${NUSQLITE3_PATH}
|
||||
|
||||
ENTRYPOINT ["tini", "--"]
|
||||
CMD ["node", "index.js"]
|
||||
CMD ["node", "dist-server/index.js"]
|
||||
@@ -11,6 +11,8 @@ ExecReload=/bin/kill -HUP $MAINPID
|
||||
Restart=always
|
||||
User=audiobookshelf
|
||||
Group=audiobookshelf
|
||||
Environment=HOME=/var/cache/audiobookshelf
|
||||
CacheDirectory=audiobookshelf
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
+10
-4
@@ -16,7 +16,7 @@ echo "--------------------"
|
||||
|
||||
cd client
|
||||
rm -rf node_modules
|
||||
npm ci --unsafe-perm=true --allow-root
|
||||
npm ci
|
||||
npm run generate
|
||||
cd ..
|
||||
|
||||
@@ -24,7 +24,8 @@ echo ">>> Building Server"
|
||||
echo "--------------------"
|
||||
|
||||
rm -rf node_modules
|
||||
npm ci --unsafe-perm=true --allow-root
|
||||
npm ci
|
||||
npm run build:server
|
||||
|
||||
echo ">>> Packaging"
|
||||
echo "--------------------"
|
||||
@@ -34,7 +35,6 @@ echo "--------------------"
|
||||
mkdir -p dist
|
||||
rm -rf dist/debian
|
||||
cp -R build/debian dist/debian
|
||||
chmod -R 775 dist/debian
|
||||
|
||||
controlfile="Package: audiobookshelf
|
||||
Version: $VERSION
|
||||
@@ -47,8 +47,14 @@ Description: $DESCRIPTION"
|
||||
|
||||
echo "$controlfile" > dist/debian/DEBIAN/control;
|
||||
|
||||
# Package contents: directories 0755, files 0644 (including the systemd unit).
|
||||
# Maintainer scripts must stay executable so dpkg can run them.
|
||||
find dist/debian -type d -exec chmod 755 {} +
|
||||
find dist/debian -type f -exec chmod 644 {} +
|
||||
chmod 755 dist/debian/DEBIAN/preinst dist/debian/DEBIAN/postinst dist/debian/DEBIAN/prerm
|
||||
|
||||
# Package debian
|
||||
pkg -t node20-linux-x64 -o dist/debian/usr/share/audiobookshelf/audiobookshelf .
|
||||
pkg --config package.json -t node24-linux-x64 -o dist/debian/usr/share/audiobookshelf/audiobookshelf dist-server/index.js
|
||||
|
||||
fakeroot dpkg-deb -Zxz --build dist/debian
|
||||
|
||||
|
||||
@@ -196,6 +196,7 @@ export default {
|
||||
requestBatchQuickEmbed() {
|
||||
const payload = {
|
||||
message: this.$strings.MessageConfirmQuickEmbed,
|
||||
allowHtml: true,
|
||||
callback: (confirmed) => {
|
||||
if (confirmed) {
|
||||
this.$axios
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
<div v-if="entityName === 'collections' || entityName === 'playlists'" class="flex justify-center mt-4">
|
||||
{{ emptyMessageHelp }}
|
||||
<ui-tooltip :text="$strings.LabelClickForMoreInfo" class="inline-flex ml-2">
|
||||
<a href="https://www.audiobookshelf.org/guides/collections" target="_blank" class="inline-flex">
|
||||
<a href="https://audiobookshelf.org/docs/documentation/libraries/common-content/playlists" target="_blank" class="inline-flex">
|
||||
<span class="material-symbols text-xl w-5 text-gray-200">help_outline</span>
|
||||
</a>
|
||||
</ui-tooltip>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<div class="px-6 py-8 w-full text-sm rounded-lg bg-bg shadow-lg border border-black-300 overflow-y-auto overflow-x-hidden" style="max-height: 80vh">
|
||||
<div class="absolute top-0 right-0 p-4">
|
||||
<ui-tooltip :text="$strings.LabelClickForMoreInfo" class="inline-flex ml-2">
|
||||
<a href="https://www.audiobookshelf.org/guides/media-item-shares" target="_blank" class="inline-flex">
|
||||
<a href="https://audiobookshelf.org/docs/documentation/libraries/common-content/public-shares#media-item-shares" target="_blank" class="inline-flex">
|
||||
<span class="material-symbols text-xl w-5 text-gray-200">help_outline</span>
|
||||
</a>
|
||||
</ui-tooltip>
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
<div class="text-sm flex items-center justify-center text-gray-200">
|
||||
<p>{{ $strings.MessageBookshelfNoCollectionsHelp }}</p>
|
||||
<ui-tooltip :text="$strings.LabelClickForMoreInfo" class="inline-flex ml-2">
|
||||
<a href="https://www.audiobookshelf.org/guides/collections" target="_blank" class="inline-flex">
|
||||
<a href="https://audiobookshelf.org/docs/documentation/libraries/common-content/playlists" target="_blank" class="inline-flex">
|
||||
<span class="material-symbols text-xl w-5 text-gray-200">help_outline</span>
|
||||
</a>
|
||||
</ui-tooltip>
|
||||
@@ -227,7 +227,7 @@ export default {
|
||||
.catch((error) => {
|
||||
console.error('Failed to create collection', error)
|
||||
var errMsg = error.response ? error.response.data || '' : ''
|
||||
this.$toast.error(this.$strings.ToastCollectionCreateFailed + ': ' + errMsg)
|
||||
this.$toast.error(errMsg)
|
||||
this.processing = false
|
||||
})
|
||||
}
|
||||
|
||||
@@ -158,6 +158,8 @@ export default {
|
||||
this.isProcessing = true
|
||||
var updateResult = await this.$axios.$patch(`/api/items/${this.libraryItemId}/media`, updatePayload).catch((error) => {
|
||||
console.error('Failed to update', error)
|
||||
const errorMessage = typeof error?.response?.data === 'string' ? error?.response?.data : null
|
||||
this.$toast.error(errorMessage || this.$strings.ToastFailedToUpdate)
|
||||
return false
|
||||
})
|
||||
this.isProcessing = false
|
||||
|
||||
@@ -107,6 +107,7 @@ export default {
|
||||
quickEmbed() {
|
||||
const payload = {
|
||||
message: this.$strings.MessageConfirmQuickEmbed,
|
||||
allowHtml: true,
|
||||
callback: (confirmed) => {
|
||||
if (confirmed) {
|
||||
this.$axios
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<div class="flex items-center justify-between md:justify-start mb-4">
|
||||
<p class="text-sm text-gray-300 pr-2">{{ $strings.LabelMetadataOrderOfPrecedenceDescription }}</p>
|
||||
<ui-tooltip :text="$strings.LabelClickForMoreInfo" class="inline-flex">
|
||||
<a href="https://www.audiobookshelf.org/guides/book-scanner" target="_blank" class="inline-flex">
|
||||
<a href="https://audiobookshelf.org/docs/documentation/libraries/book-library/book-metadata" target="_blank" class="inline-flex">
|
||||
<span class="material-symbols text-xl w-5">help_outline</span>
|
||||
</a>
|
||||
</ui-tooltip>
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
<div class="text-sm flex items-center justify-center text-gray-200">
|
||||
<p>{{ $strings.MessageNoUserPlaylistsHelp }}</p>
|
||||
<ui-tooltip :text="$strings.LabelClickForMoreInfo" class="inline-flex ml-2">
|
||||
<a href="https://www.audiobookshelf.org/guides/collections" target="_blank" class="inline-flex">
|
||||
<a href="https://audiobookshelf.org/docs/documentation/libraries/common-content/playlists" target="_blank" class="inline-flex">
|
||||
<span class="material-symbols text-xl w-5 text-gray-200">help_outline</span>
|
||||
</a>
|
||||
</ui-tooltip>
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
<div class="absolute top-0 left-0 right-0 w-full h-36 bg-linear-to-t from-transparent via-black-500 to-black-700 opacity-90 pointer-events-none" />
|
||||
<div ref="content" class="relative text-white" :style="{ height: modalHeight, width: modalWidth }" v-click-outside="clickedOutside">
|
||||
<div class="px-4 w-full text-sm py-6 rounded-lg bg-bg shadow-lg border border-black-300">
|
||||
<p id="confirm-prompt-message" class="text-lg mb-6 mt-2 px-1" v-html="message" />
|
||||
<p v-if="allowHtmlMessage" id="confirm-prompt-message" class="text-lg mb-6 mt-2 px-1" v-html="sanitizedMessage" />
|
||||
<p v-else id="confirm-prompt-message" class="text-lg mb-6 mt-2 px-1">{{ message }}</p>
|
||||
|
||||
<ui-checkbox v-if="checkboxLabel" v-model="checkboxValue" checkbox-bg="bg" :label="checkboxLabel" label-class="pl-2 text-base" class="mb-6 px-1" />
|
||||
|
||||
@@ -52,6 +53,17 @@ export default {
|
||||
message() {
|
||||
return this.confirmPromptOptions.message || ''
|
||||
},
|
||||
allowHtmlMessage() {
|
||||
return !!this.confirmPromptOptions.allowHtml
|
||||
},
|
||||
sanitizedMessage() {
|
||||
if (!this.allowHtmlMessage) return this.message
|
||||
|
||||
return this.escapeHtml(this.message)
|
||||
.replace(/<br\s*\/?>/gi, '<br>')
|
||||
.replace(/<code>/gi, '<code>')
|
||||
.replace(/<\/code>/gi, '</code>')
|
||||
},
|
||||
callback() {
|
||||
return this.confirmPromptOptions.callback
|
||||
},
|
||||
@@ -103,6 +115,14 @@ export default {
|
||||
if (this.callback) this.callback(true, this.checkboxValue)
|
||||
this.show = false
|
||||
},
|
||||
escapeHtml(value) {
|
||||
return String(value)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''')
|
||||
},
|
||||
setShow() {
|
||||
this.checkboxValue = this.checkboxDefaultValue
|
||||
this.$eventBus.$emit('showing-prompt', true)
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
<div v-if="userCanUpdate" class="mx-1" :class="isHovering ? '' : 'ml-6'">
|
||||
<ui-icon-btn icon="edit" borderless @click="clickEdit" />
|
||||
</div>
|
||||
<div v-if="userCanDelete" class="mx-1">
|
||||
<div v-if="userCanUpdate" class="mx-1">
|
||||
<ui-icon-btn icon="close" borderless @click="removeClick" />
|
||||
</div>
|
||||
</div>
|
||||
@@ -79,8 +79,7 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
translateDistance() {
|
||||
if (!this.userCanUpdate && !this.userCanDelete) return 'translate-x-0'
|
||||
else if (!this.userCanUpdate || !this.userCanDelete) return '-translate-x-12'
|
||||
if (!this.userCanUpdate) return 'translate-x-0'
|
||||
return '-translate-x-24'
|
||||
},
|
||||
media() {
|
||||
@@ -144,9 +143,6 @@ export default {
|
||||
},
|
||||
userCanUpdate() {
|
||||
return this.$store.getters['user/getUserCanUpdate']
|
||||
},
|
||||
userCanDelete() {
|
||||
return this.$store.getters['user/getUserCanDelete']
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div ref="wrapper" class="relative">
|
||||
<input :id="inputId" :name="inputName" ref="input" v-model="inputValue" :type="actualType" :step="step" :min="min" :readonly="readonly" :disabled="disabled" :placeholder="placeholder" dir="auto" class="rounded-sm bg-primary text-gray-200 focus:bg-bg focus:outline-hidden border h-full w-full" :class="classList" @keyup="keyup" @change="change" @focus="focused" @blur="blurred" />
|
||||
<input :id="inputId" :name="inputName" ref="input" v-model="inputValue" :type="actualType" :step="step" :min="min" :readonly="readonly" :disabled="disabled" :placeholder="placeholder" :autocomplete="autocomplete" dir="auto" class="rounded-sm bg-primary text-gray-200 focus:bg-bg focus:outline-hidden border h-full w-full" :class="classList" @keyup="keyup" @change="change" @focus="focused" @blur="blurred" />
|
||||
<div v-if="clearable && inputValue" class="absolute top-0 right-0 h-full px-2 flex items-center justify-center">
|
||||
<span class="material-symbols text-gray-300 cursor-pointer" style="font-size: 1.1rem" @click.stop.prevent="clear">close</span>
|
||||
</div>
|
||||
@@ -41,7 +41,8 @@ export default {
|
||||
step: [String, Number],
|
||||
min: [String, Number],
|
||||
customInputClass: String,
|
||||
trimWhitespace: Boolean
|
||||
trimWhitespace: Boolean,
|
||||
autocomplete: String
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<em v-if="note" class="font-normal text-xs pl-2">{{ note }}</em>
|
||||
</label>
|
||||
</slot>
|
||||
<ui-text-input :placeholder="placeholder || label" :inputId="identifier" ref="input" v-model="inputValue" :disabled="disabled" :readonly="readonly" :type="type" :min="min" :show-copy="showCopy" class="w-full" :class="inputClass" :trim-whitespace="trimWhitespace" @blur="inputBlurred" />
|
||||
<ui-text-input :placeholder="placeholder || label" :inputId="identifier" ref="input" v-model="inputValue" :disabled="disabled" :readonly="readonly" :type="type" :min="min" :show-copy="showCopy" :autocomplete="autocomplete" class="w-full" :class="inputClass" :trim-whitespace="trimWhitespace" @blur="inputBlurred" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -26,7 +26,8 @@ export default {
|
||||
disabled: Boolean,
|
||||
inputClass: String,
|
||||
showCopy: Boolean,
|
||||
trimWhitespace: Boolean
|
||||
trimWhitespace: Boolean,
|
||||
autocomplete: String
|
||||
},
|
||||
data() {
|
||||
return {}
|
||||
|
||||
@@ -60,6 +60,7 @@ module.exports = {
|
||||
proxy,
|
||||
|
||||
io: {
|
||||
server: false,
|
||||
sockets: [
|
||||
{
|
||||
name: 'dev',
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "audiobookshelf-client",
|
||||
"version": "2.33.0",
|
||||
"version": "3.0.0-beta",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "audiobookshelf-client",
|
||||
"version": "2.33.0",
|
||||
"version": "3.0.0-beta",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@nuxtjs/axios": "^5.13.6",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "audiobookshelf-client",
|
||||
"version": "2.33.0",
|
||||
"version": "3.0.0-beta",
|
||||
"buildNumber": 1,
|
||||
"description": "Self-hosted audiobook and podcast client",
|
||||
"main": "index.js",
|
||||
|
||||
+142
-5
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div id="page-wrapper" class="page p-6 overflow-y-auto relative" :class="streamLibraryItem ? 'streaming' : ''">
|
||||
<div class="w-full max-w-xl mx-auto">
|
||||
<div class="w-full max-w-2xl mx-auto">
|
||||
<h1 class="text-2xl">{{ $strings.HeaderAccount }}</h1>
|
||||
|
||||
<div class="my-4">
|
||||
@@ -69,8 +69,52 @@
|
||||
</app-settings-content>
|
||||
</div>
|
||||
|
||||
<div class="py-4 mt-8 flex">
|
||||
<ui-btn color="bg-primary flex items-center text-lg" @click="logout"><span class="material-symbols mr-4 icon-text">logout</span>{{ $strings.ButtonLogout }}</ui-btn>
|
||||
<div v-if="!isGuest">
|
||||
<div class="w-full h-px bg-white/10 my-4" />
|
||||
|
||||
<app-settings-content :header-text="$strings.HeaderSessions">
|
||||
<table v-if="authSessions.length" class="tracksTable mt-4 table-fixed w-full">
|
||||
<tr>
|
||||
<th class="text-left">{{ $strings.LabelDeviceInfo }}</th>
|
||||
<th class="text-left hidden sm:table-cell w-36">{{ $strings.LabelIpAddress }}</th>
|
||||
<th class="text-left w-30 sm:w-40">{{ $strings.LabelLastUpdate }}</th>
|
||||
<th class="w-12"></th>
|
||||
</tr>
|
||||
<tr v-for="session in authSessions" :key="session.id">
|
||||
<td class="max-w-0">
|
||||
<div class="flex items-center gap-0.5 min-w-0">
|
||||
<span class="text-sm text-gray-100 truncate min-w-0" :title="session.userAgent">{{ getSessionDeviceLabel(session) }}</span>
|
||||
<ui-tooltip v-if="session.current" direction="top" :text="$strings.LabelCurrent" class="inline-flex shrink-0">
|
||||
<span class="material-symbols text-success text-sm leading-none">check</span>
|
||||
</ui-tooltip>
|
||||
</div>
|
||||
</td>
|
||||
<td class="hidden sm:table-cell w-36">
|
||||
<p class="text-sm text-gray-100 truncate" :title="session.ipAddress">{{ session.ipAddress || '-' }}</p>
|
||||
</td>
|
||||
<td class="w-30 sm:w-40">
|
||||
<ui-tooltip v-if="session.updatedAt" direction="top" :text="$formatDatetime(session.updatedAt, dateFormat, timeFormat)">
|
||||
<p class="text-xs sm:text-sm text-gray-100">{{ $dateDistanceFromNow(session.updatedAt) }}</p>
|
||||
</ui-tooltip>
|
||||
</td>
|
||||
<td class="w-12">
|
||||
<div class="flex justify-end items-center h-10">
|
||||
<ui-icon-btn icon="delete" borderless :size="8" icon-font-size="1.1rem" :disabled="deletingSessionId === session.id || loadingAuthSessions" @click="deleteAuthSessionClick(session)" />
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<div v-if="authSessionsNumPages > 1" class="flex items-center justify-end py-1">
|
||||
<ui-icon-btn icon="arrow_back_ios_new" :size="7" icon-font-size="1rem" class="mx-1" :disabled="loadingAuthSessions || authSessionsPage === 0" @click="prevAuthSessionsPage" />
|
||||
<p class="text-sm mx-1">{{ $getString('LabelPaginationPageXOfY', [authSessionsPage + 1, authSessionsNumPages]) }}</p>
|
||||
<ui-icon-btn icon="arrow_forward_ios" :size="7" icon-font-size="1rem" class="mx-1" :disabled="loadingAuthSessions || authSessionsPage >= authSessionsNumPages - 1" @click="nextAuthSessionsPage" />
|
||||
</div>
|
||||
</app-settings-content>
|
||||
</div>
|
||||
|
||||
<div class="py-4 mt-8 flex flex-wrap gap-2">
|
||||
<ui-btn v-if="!isGuest" color="bg-primary flex items-center text-lg" :disabled="loggingOut" @click="logout(true)"> <span class="material-symbols mr-4 icon-text">devices</span>{{ $strings.ButtonLogoutAllDevices }} </ui-btn>
|
||||
<ui-btn color="bg-primary flex items-center text-lg" :disabled="loggingOut" @click="logout(false)"><span class="material-symbols mr-4 icon-text">logout</span>{{ $strings.ButtonLogout }}</ui-btn>
|
||||
</div>
|
||||
|
||||
<modals-emails-user-e-reader-device-modal v-model="showEReaderDeviceModal" :existing-devices="revisedEreaderDevices" :ereader-device="selectedEReaderDevice" @update="ereaderDevicesUpdated" />
|
||||
@@ -87,6 +131,13 @@ export default {
|
||||
newPassword: null,
|
||||
confirmPassword: null,
|
||||
changingPassword: false,
|
||||
loggingOut: false,
|
||||
authSessions: [],
|
||||
authSessionsPage: 0,
|
||||
authSessionsNumPages: 0,
|
||||
authSessionsItemsPerPage: 10,
|
||||
loadingAuthSessions: false,
|
||||
deletingSessionId: null,
|
||||
selectedLanguage: '',
|
||||
newEReaderDevice: {
|
||||
name: '',
|
||||
@@ -129,13 +180,19 @@ export default {
|
||||
},
|
||||
revisedEreaderDevices() {
|
||||
return this.ereaderDevices.filter((device) => device.users?.length === 1)
|
||||
},
|
||||
dateFormat() {
|
||||
return this.$store.getters['getServerSetting']('dateFormat')
|
||||
},
|
||||
timeFormat() {
|
||||
return this.$store.getters['getServerSetting']('timeFormat')
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
updateLocalLanguage(lang) {
|
||||
this.$setLanguageCode(lang)
|
||||
},
|
||||
logout() {
|
||||
logout(allDevices = false) {
|
||||
// Disconnect from socket
|
||||
if (this.$root.socket) {
|
||||
console.log('Disconnecting from socket', this.$root.socket.id)
|
||||
@@ -149,8 +206,10 @@ export default {
|
||||
this.$store.commit('libraries/setUserPlaylists', [])
|
||||
this.$store.commit('libraries/setCollections', [])
|
||||
|
||||
this.loggingOut = true
|
||||
const url = allDevices ? '/logout?allDevices=1' : '/logout'
|
||||
this.$axios
|
||||
.$post('/logout')
|
||||
.$post(url)
|
||||
.then((logoutPayload) => {
|
||||
const redirect_url = logoutPayload.redirect_url
|
||||
|
||||
@@ -163,6 +222,9 @@ export default {
|
||||
.catch((error) => {
|
||||
console.error(error)
|
||||
})
|
||||
.finally(() => {
|
||||
this.loggingOut = false
|
||||
})
|
||||
},
|
||||
resetForm() {
|
||||
this.password = null
|
||||
@@ -238,11 +300,86 @@ export default {
|
||||
},
|
||||
ereaderDevicesUpdated(ereaderDevices) {
|
||||
this.ereaderDevices = ereaderDevices
|
||||
},
|
||||
loadAuthSessions(page = 0) {
|
||||
if (this.loadingAuthSessions) return
|
||||
if (page < 0) return
|
||||
if (this.authSessionsNumPages > 0 && page > this.authSessionsNumPages - 1) return
|
||||
|
||||
this.loadingAuthSessions = true
|
||||
this.$axios
|
||||
.$get(`/api/me/sessions?page=${page}&itemsPerPage=${this.authSessionsItemsPerPage}`)
|
||||
.then((data) => {
|
||||
this.authSessions = data.sessions || []
|
||||
this.authSessionsPage = data.page ?? page
|
||||
this.authSessionsNumPages = data.numPages ?? 0
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to load sessions', error)
|
||||
})
|
||||
.finally(() => {
|
||||
this.loadingAuthSessions = false
|
||||
})
|
||||
},
|
||||
prevAuthSessionsPage() {
|
||||
if (this.authSessionsPage <= 0) return
|
||||
this.loadAuthSessions(this.authSessionsPage - 1)
|
||||
},
|
||||
nextAuthSessionsPage() {
|
||||
if (this.authSessionsPage >= this.authSessionsNumPages - 1) return
|
||||
this.loadAuthSessions(this.authSessionsPage + 1)
|
||||
},
|
||||
deleteAuthSessionClick(session) {
|
||||
this.$store.commit('globals/setConfirmPrompt', {
|
||||
message: this.$getString('MessageConfirmLogoutDevice', [this.getSessionDeviceLabel(session)]),
|
||||
callback: (confirmed) => {
|
||||
if (confirmed) this.deleteAuthSession(session)
|
||||
},
|
||||
type: 'yesNo'
|
||||
})
|
||||
},
|
||||
deleteAuthSession(session) {
|
||||
// Call logout instead for current session
|
||||
if (session.current) {
|
||||
this.logout(false)
|
||||
return
|
||||
}
|
||||
|
||||
this.deletingSessionId = session.id
|
||||
this.$axios
|
||||
.$delete(`/api/me/sessions/${session.id}`)
|
||||
.then(() => {
|
||||
if (this.authSessions.length === 1 && this.authSessionsPage > 0) {
|
||||
this.loadAuthSessions(this.authSessionsPage - 1)
|
||||
} else {
|
||||
this.loadAuthSessions(this.authSessionsPage)
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to delete session', error)
|
||||
this.$toast.error(this.$strings.ToastFailedToDelete)
|
||||
})
|
||||
.finally(() => {
|
||||
this.deletingSessionId = null
|
||||
})
|
||||
},
|
||||
getSessionDeviceLabel(session) {
|
||||
const deviceInfo = session.deviceInfo
|
||||
if (!deviceInfo) return session.userAgent || '-'
|
||||
|
||||
const parts = []
|
||||
if (deviceInfo.model) parts.push(deviceInfo.model)
|
||||
if (deviceInfo.osName) parts.push(`${deviceInfo.osName}${deviceInfo.osVersion ? ` ${deviceInfo.osVersion}` : ''}`)
|
||||
if (deviceInfo.browserName) parts.push(deviceInfo.browserName)
|
||||
return parts.join(' / ') || session.userAgent || '-'
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.selectedLanguage = this.$languageCodes.current
|
||||
this.ereaderDevices = this.$store.state.libraries.ereaderDevices || []
|
||||
if (!this.isGuest) {
|
||||
this.loadAuthSessions()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -7,7 +7,7 @@
|
||||
</div>
|
||||
|
||||
<ui-tooltip :text="$strings.LabelClickForMoreInfo" class="inline-flex ml-2">
|
||||
<a href="https://www.audiobookshelf.org/guides/api-keys" target="_blank" class="inline-flex">
|
||||
<a href="https://audiobookshelf.org/docs/documentation/server-management/api-keys" target="_blank" class="inline-flex">
|
||||
<span class="material-symbols text-xl w-5 text-gray-200">help_outline</span>
|
||||
</a>
|
||||
</ui-tooltip>
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
<ui-checkbox v-model="enableOpenIDAuth" checkbox-bg="bg" />
|
||||
<p class="text-lg pl-4">{{ $strings.HeaderOpenIDConnectAuthentication }}</p>
|
||||
<ui-tooltip :text="$strings.LabelClickForMoreInfo" class="inline-flex ml-2">
|
||||
<a href="https://www.audiobookshelf.org/guides/oidc_authentication" target="_blank" class="inline-flex">
|
||||
<a href="https://audiobookshelf.org/docs/documentation/server-management/oidc-authentication" target="_blank" class="inline-flex">
|
||||
<span class="material-symbols text-xl w-5 text-gray-200">help_outline</span>
|
||||
</a>
|
||||
</ui-tooltip>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<app-settings-content :header-text="$strings.HeaderEmailSettings" :description="''">
|
||||
<template #header-items>
|
||||
<ui-tooltip :text="$strings.LabelClickForMoreInfo" class="inline-flex ml-2">
|
||||
<a href="https://www.audiobookshelf.org/guides/send_to_ereader" target="_blank" class="inline-flex">
|
||||
<a href="https://audiobookshelf.org/docs/documentation/libraries/book-library/ebooks" target="_blank" class="inline-flex">
|
||||
<span class="material-symbols text-xl w-5 text-gray-200">help_outline</span>
|
||||
</a>
|
||||
</ui-tooltip>
|
||||
|
||||
@@ -390,8 +390,8 @@ export default {
|
||||
},
|
||||
purgeItemsCache() {
|
||||
const payload = {
|
||||
// message: `This will delete the entire folder at <code>/metadata/cache/items</code>.<br />Are you sure you want to purge items cache?`,
|
||||
message: this.$strings.MessageConfirmPurgeItemsCache,
|
||||
allowHtml: true,
|
||||
callback: (confirmed) => {
|
||||
if (confirmed) {
|
||||
this.sendPurgeItemsCache()
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
</template>
|
||||
<template #header-items>
|
||||
<ui-tooltip :text="$strings.LabelClickForMoreInfo" class="inline-flex ml-2">
|
||||
<a href="https://www.audiobookshelf.org/guides/custom-metadata-providers" target="_blank" class="inline-flex">
|
||||
<a href="https://audiobookshelf.org/docs/documentation/community/community-providers" target="_blank" class="inline-flex">
|
||||
<span class="material-symbols text-xl w-5 text-gray-200">help_outline</span>
|
||||
</a>
|
||||
</ui-tooltip>
|
||||
|
||||
@@ -90,9 +90,9 @@ export default {
|
||||
|
||||
let message = this.$getString('MessageConfirmRenameGenre', [this.editingGenre, this.newGenreName])
|
||||
if (genreNameExists) {
|
||||
message += `<br><span class="text-sm">${this.$strings.MessageConfirmRenameGenreMergeNote}</span>`
|
||||
message += ` ${this.$strings.MessageConfirmRenameGenreMergeNote}`
|
||||
} else if (genreNameExistsOfDifferentCase) {
|
||||
message += `<br><span class="text-warning text-sm">${this.$getString('MessageConfirmRenameGenreWarning', [genreNameExistsOfDifferentCase])}</span>`
|
||||
message += ` ${this.$getString('MessageConfirmRenameGenreWarning', [genreNameExistsOfDifferentCase])}`
|
||||
}
|
||||
|
||||
const payload = {
|
||||
|
||||
@@ -86,9 +86,9 @@ export default {
|
||||
|
||||
let message = this.$getString('MessageConfirmRenameTag', [this.editingTag, this.newTagName])
|
||||
if (tagNameExists) {
|
||||
message += `<br><span class="text-sm">${this.$strings.MessageConfirmRenameTagMergeNote}</span>`
|
||||
message += ` ${this.$strings.MessageConfirmRenameTagMergeNote}`
|
||||
} else if (tagNameExistsOfDifferentCase) {
|
||||
message += `<br><span class="text-warning text-sm">${this.$getString('MessageConfirmRenameTagWarning', [tagNameExistsOfDifferentCase])}</span>`
|
||||
message += ` ${this.$getString('MessageConfirmRenameTagWarning', [tagNameExistsOfDifferentCase])}`
|
||||
}
|
||||
|
||||
const payload = {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<app-settings-content :header-text="$strings.HeaderLibraries">
|
||||
<template #header-items>
|
||||
<ui-tooltip :text="$strings.LabelClickForMoreInfo" class="inline-flex ml-2">
|
||||
<a href="https://www.audiobookshelf.org/guides/library_creation" target="_blank" class="inline-flex">
|
||||
<a href="https://audiobookshelf.org/docs/documentation/libraries/common-content/overview" target="_blank" class="inline-flex">
|
||||
<span class="material-symbols text-xl w-5 text-gray-200">help_outline</span>
|
||||
</a>
|
||||
</ui-tooltip>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<app-settings-content :header-text="$strings.HeaderLogs" :description="$strings.MessageLogsDescription">
|
||||
<template #header-items>
|
||||
<ui-tooltip :text="$strings.LabelClickForMoreInfo" class="inline-flex ml-2">
|
||||
<a href="https://www.audiobookshelf.org/guides/server_logs" target="_blank" class="inline-flex">
|
||||
<a href="https://audiobookshelf.org/docs/documentation/server-management/server-logs" target="_blank" class="inline-flex">
|
||||
<span class="material-symbols text-xl w-5 text-gray-200">help_outline</span>
|
||||
</a>
|
||||
</ui-tooltip>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<app-settings-content :header-text="$strings.HeaderRSSFeeds">
|
||||
<template #header-items>
|
||||
<ui-tooltip :text="$strings.LabelClickForMoreInfo" class="inline-flex ml-2">
|
||||
<a href="https://www.audiobookshelf.org/guides/rss_feeds" target="_blank" class="inline-flex">
|
||||
<a href="https://audiobookshelf.org/docs/documentation/libraries/common-content/public-shares#rss-feed" target="_blank" class="inline-flex">
|
||||
<span class="material-symbols text-xl w-5 text-gray-200">help_outline</span>
|
||||
</a>
|
||||
</ui-tooltip>
|
||||
|
||||
@@ -13,11 +13,6 @@
|
||||
<widgets-online-indicator :value="!!userOnline" />
|
||||
<h1 class="text-xl pl-2">{{ username }}</h1>
|
||||
</div>
|
||||
<div v-if="legacyToken" class="text-xs space-y-2 mt-4">
|
||||
<ui-text-input-with-label label="Legacy API Token" :value="legacyToken" readonly show-copy />
|
||||
|
||||
<p class="text-warning" v-html="$strings.MessageAuthenticationLegacyTokenWarning" />
|
||||
</div>
|
||||
<div class="w-full h-px bg-white/10 my-2" />
|
||||
<div class="py-2">
|
||||
<h1 class="text-lg mb-2 text-white/90 px-2 sm:px-0">{{ $strings.HeaderListeningStats }}</h1>
|
||||
@@ -102,12 +97,6 @@ export default {
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
legacyToken() {
|
||||
return this.user.token
|
||||
},
|
||||
userToken() {
|
||||
return this.user.accessToken
|
||||
},
|
||||
bookCoverAspectRatio() {
|
||||
return this.$store.getters['libraries/getBookCoverAspectRatio']
|
||||
},
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
</div>
|
||||
|
||||
<ui-tooltip :text="$strings.LabelClickForMoreInfo" class="inline-flex ml-2">
|
||||
<a href="https://www.audiobookshelf.org/guides/users" target="_blank" class="inline-flex">
|
||||
<a href="https://audiobookshelf.org/docs/documentation/server-management/user-management" target="_blank" class="inline-flex">
|
||||
<span class="material-symbols text-xl w-5 text-gray-200">help_outline</span>
|
||||
</a>
|
||||
</ui-tooltip>
|
||||
|
||||
+6
-29
@@ -17,9 +17,9 @@
|
||||
|
||||
<form @submit.prevent="submitServerSetup">
|
||||
<p class="text-lg font-semibold mb-2 pl-1 text-center">Create Root User</p>
|
||||
<ui-text-input-with-label v-model.trim="newRoot.username" label="Username" :disabled="processing" class="w-full mb-3 text-sm" />
|
||||
<ui-text-input-with-label v-model="newRoot.password" label="Password" type="password" :disabled="processing" class="w-full mb-3 text-sm" />
|
||||
<ui-text-input-with-label v-model="confirmPassword" label="Confirm Password" type="password" :disabled="processing" class="w-full mb-3 text-sm" />
|
||||
<ui-text-input-with-label v-model.trim="newRoot.username" label="Username" autocomplete="username" :disabled="processing" class="w-full mb-3 text-sm" />
|
||||
<ui-text-input-with-label v-model="newRoot.password" label="Password" type="password" autocomplete="new-password" :disabled="processing" class="w-full mb-3 text-sm" />
|
||||
<ui-text-input-with-label v-model="confirmPassword" label="Confirm Password" type="password" autocomplete="new-password" :disabled="processing" class="w-full mb-3 text-sm" />
|
||||
|
||||
<p class="text-lg font-semibold mt-6 mb-2 pl-1 text-center">Directory Paths</p>
|
||||
<ui-text-input-with-label v-model="ConfigPath" label="Config Path" disabled class="w-full mb-3 text-sm" />
|
||||
@@ -40,21 +40,12 @@
|
||||
|
||||
<p v-if="error" class="text-error text-center py-2">{{ error }}</p>
|
||||
|
||||
<div v-if="showNewAuthSystemMessage" class="mb-4">
|
||||
<widgets-alert type="warning">
|
||||
<div>
|
||||
<p>{{ $strings.MessageAuthenticationSecurityMessage }}</p>
|
||||
<a v-if="showNewAuthSystemAdminMessage" href="https://github.com/advplyr/audiobookshelf/discussions/4460" target="_blank" class="underline">{{ $strings.LabelMoreInfo }}</a>
|
||||
</div>
|
||||
</widgets-alert>
|
||||
</div>
|
||||
|
||||
<form v-show="login_local" @submit.prevent="submitForm">
|
||||
<label class="text-xs text-gray-300 uppercase">{{ $strings.LabelUsername }}</label>
|
||||
<ui-text-input v-model.trim="username" :disabled="processing" class="mb-3 w-full" inputName="username" />
|
||||
<ui-text-input v-model.trim="username" autocomplete="username" :disabled="processing" class="mb-3 w-full" inputName="username" />
|
||||
|
||||
<label class="text-xs text-gray-300 uppercase">{{ $strings.LabelPassword }}</label>
|
||||
<ui-text-input v-model.trim="password" type="password" :disabled="processing" class="w-full mb-3" inputName="password" />
|
||||
<ui-text-input v-model.trim="password" type="password" autocomplete="current-password" :disabled="processing" class="w-full mb-3" inputName="password" />
|
||||
<div class="w-full flex justify-end py-3">
|
||||
<ui-btn type="submit" :disabled="processing" color="bg-primary" class="leading-none">{{ processing ? 'Checking...' : $strings.ButtonSubmit }}</ui-btn>
|
||||
</div>
|
||||
@@ -94,10 +85,7 @@ export default {
|
||||
MetadataPath: '',
|
||||
login_local: true,
|
||||
login_openid: false,
|
||||
authFormData: null,
|
||||
// New JWT auth system re-login flags
|
||||
showNewAuthSystemMessage: false,
|
||||
showNewAuthSystemAdminMessage: false
|
||||
authFormData: null
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
@@ -201,8 +189,6 @@ export default {
|
||||
},
|
||||
async submitForm() {
|
||||
this.error = null
|
||||
this.showNewAuthSystemMessage = false
|
||||
this.showNewAuthSystemAdminMessage = false
|
||||
this.processing = true
|
||||
|
||||
const payload = {
|
||||
@@ -238,15 +224,6 @@ export default {
|
||||
}
|
||||
})
|
||||
.then((res) => {
|
||||
// Force re-login if user is using an old token with no expiration
|
||||
if (res.user.isOldToken) {
|
||||
this.username = res.user.username
|
||||
this.showNewAuthSystemMessage = true
|
||||
// Admin user sees link to github discussion
|
||||
this.showNewAuthSystemAdminMessage = res.user.type === 'admin' || res.user.type === 'root'
|
||||
return false
|
||||
}
|
||||
|
||||
this.setUser(res)
|
||||
return true
|
||||
})
|
||||
|
||||
@@ -364,6 +364,7 @@ export default {
|
||||
}
|
||||
|
||||
const startTime = this.playbackSession.currentTime || 0
|
||||
|
||||
this.localAudioPlayer.set(null, this.audioTracks, false, startTime, false)
|
||||
this.localAudioPlayer.on('stateChange', this.playerStateChange.bind(this))
|
||||
this.localAudioPlayer.on('timeupdate', this.playerTimeUpdate.bind(this))
|
||||
|
||||
@@ -46,7 +46,20 @@ export default class LocalAudioPlayer extends EventEmitter {
|
||||
this.player.addEventListener('loadedmetadata', this.evtLoadedMetadata.bind(this))
|
||||
this.player.addEventListener('timeupdate', this.evtTimeupdate.bind(this))
|
||||
|
||||
var mimeTypes = ['audio/flac', 'audio/mpeg', 'audio/mp4', 'audio/ogg', 'audio/aac', 'audio/x-ms-wma', 'audio/x-aiff', 'audio/webm']
|
||||
var mimeTypes = [
|
||||
'audio/flac',
|
||||
'audio/mpeg',
|
||||
'audio/mp4',
|
||||
'audio/ogg',
|
||||
'audio/aac',
|
||||
'audio/x-ms-wma',
|
||||
'audio/x-aiff',
|
||||
'audio/webm',
|
||||
// `audio/matroska` is the correct mimetype, but the server still uses `audio/x-matroska`
|
||||
// ref: https://www.iana.org/assignments/media-types/media-types.xhtml
|
||||
'audio/matroska',
|
||||
'audio/x-matroska'
|
||||
]
|
||||
var mimeTypeCanPlayMap = {}
|
||||
mimeTypes.forEach((mt) => {
|
||||
var canPlay = this.player.canPlayType(mt)
|
||||
|
||||
@@ -21,6 +21,7 @@ const languageCodeMap = {
|
||||
he: { label: 'עברית', dateFnsLocale: 'he' },
|
||||
hr: { label: 'Hrvatski', dateFnsLocale: 'hr' },
|
||||
it: { label: 'Italiano', dateFnsLocale: 'it' },
|
||||
ja: { label: '日本語', dateFnsLocale: 'ja' },
|
||||
lt: { label: 'Lietuvių', dateFnsLocale: 'lt' },
|
||||
hu: { label: 'Magyar', dateFnsLocale: 'hu' },
|
||||
ko: { label: '한국어', dateFnsLocale: 'ko' },
|
||||
@@ -60,6 +61,7 @@ const podcastSearchRegionMap = {
|
||||
hr: { label: 'Hrvatska' },
|
||||
il: { label: 'ישראל / إسرائيل' },
|
||||
it: { label: 'Italia' },
|
||||
jp: { label: '日本' },
|
||||
lu: { label: 'Luxembourg / Luxemburg / Lëtezebuerg' },
|
||||
hu: { label: 'Magyarország' },
|
||||
nl: { label: 'Nederland' },
|
||||
|
||||
@@ -244,6 +244,8 @@
|
||||
"LabelAlreadyInYourLibrary": "موجود بالفعل في مكتبتك",
|
||||
"LabelApiKeyCreated": "تم إنشاء مفتاح API \"{0}\" بنجاح.",
|
||||
"LabelApiKeyCreatedDescription": "تأكد من نسخ مفتاح API الآن، لن تتمكن من رؤيته مرة أخرى.",
|
||||
"LabelApiKeyUser": "التصرف بالنيابة عن مستخدم",
|
||||
"LabelApiKeyUserDescription": "مفتاح API سيمتلك نفس صلاحيات المستخدم الذي ينوب عنه ، سيظهر بالسجلات وكأن المستخدم قام بالطلب.",
|
||||
"LabelApiToken": "رمز API",
|
||||
"LabelAppend": "إلحاق",
|
||||
"LabelAudioBitrate": "معدل بت الصوت (على سبيل المثال 128 كيلو بايت)",
|
||||
@@ -293,6 +295,7 @@
|
||||
"LabelContinueListening": "استمرار الاستماع",
|
||||
"LabelContinueReading": "استمرار القراءة",
|
||||
"LabelContinueSeries": "استمرار المسلسلات",
|
||||
"LabelCorsAllowed": "CORS Origins مسموح",
|
||||
"LabelCover": "الغلاف",
|
||||
"LabelCoverImageURL": "رابط صورة الغلاف",
|
||||
"LabelCoverProvider": "مزود الغلاف",
|
||||
@@ -426,6 +429,9 @@
|
||||
"LabelLibraryFilterSublistEmpty": "لا يوجد {0}",
|
||||
"LabelLibraryItem": "عنصر المكتبة",
|
||||
"LabelLibraryName": "اسم المكتبة",
|
||||
"LabelLibrarySortByProgress": "المرحلة: الأحدث",
|
||||
"LabelLibrarySortByProgressFinished": "المرحلة: تم الانتهاء",
|
||||
"LabelLibrarySortByProgressStarted": "المرحلة: تم البدء",
|
||||
"LabelLimit": "حد",
|
||||
"LabelLineSpacing": "تباعد الأسطر",
|
||||
"LabelListenAgain": "الاستماع مجدداً",
|
||||
|
||||
+49
-45
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"ButtonAdd": "Дадаць",
|
||||
"ButtonAddApiKey": "Дадаць API-ключ",
|
||||
"ButtonAddApiKey": "Дадаць ключ API",
|
||||
"ButtonAddChapters": "Дадаць раздзелы",
|
||||
"ButtonAddDevice": "Дадаць прыладу",
|
||||
"ButtonAddLibrary": "Дадаць бібліятэку",
|
||||
@@ -16,7 +16,7 @@
|
||||
"ButtonBrowseForFolder": "Агляд папак",
|
||||
"ButtonCancel": "Скасаваць",
|
||||
"ButtonCancelEncode": "Скасаваць кадзіраванне",
|
||||
"ButtonChangeRootPassword": "Зменіце Root пароль",
|
||||
"ButtonChangeRootPassword": "Змяніць пароль root",
|
||||
"ButtonCheckAndDownloadNewEpisodes": "Праверыць і спампаваць новыя выпускі",
|
||||
"ButtonChooseAFolder": "Выбраць папку",
|
||||
"ButtonChooseFiles": "Выбраць файлы",
|
||||
@@ -46,11 +46,12 @@
|
||||
"ButtonLatest": "Апошняе",
|
||||
"ButtonLibrary": "Бібліятэка",
|
||||
"ButtonLogout": "Выйсці",
|
||||
"ButtonLogoutAllDevices": "Выйсці з усіх прылад",
|
||||
"ButtonLookup": "Пошук",
|
||||
"ButtonManageTracks": "Кіраванне трэкамі",
|
||||
"ButtonMapChapterTitles": "Супаставіць загалоўкі раздзелаў",
|
||||
"ButtonMatchAllAuthors": "Супадзенне ўсіх аўтараў",
|
||||
"ButtonMatchBooks": "Падбор кніг",
|
||||
"ButtonMatchBooks": "Параўнаць кнігі",
|
||||
"ButtonNevermind": "Няважна",
|
||||
"ButtonNext": "Далей",
|
||||
"ButtonNextChapter": "Наступны раздзел",
|
||||
@@ -81,14 +82,14 @@
|
||||
"ButtonRemove": "Выдаліць",
|
||||
"ButtonRemoveAll": "Выдаліць усе",
|
||||
"ButtonRemoveAllLibraryItems": "Выдаліць усе элементы бібліятэкі",
|
||||
"ButtonRemoveFromContinueListening": "Выдаліць з Працягваць слухаць",
|
||||
"ButtonRemoveFromContinueListening": "Выдаліць з Працяг праслухоўвання",
|
||||
"ButtonRemoveFromContinueReading": "Выдаліць з Працягваць чытанне",
|
||||
"ButtonRemoveSeriesFromContinueSeries": "Выдаліць серыю з Працягваць серыю",
|
||||
"ButtonReset": "Скінуць",
|
||||
"ButtonResetToDefault": "Скінуць да прадвызначаных",
|
||||
"ButtonRestore": "Аднавіць",
|
||||
"ButtonSave": "Захаваць",
|
||||
"ButtonSaveAndClose": "Захаваць і зачыніць",
|
||||
"ButtonSaveAndClose": "Захаваць і закрыць",
|
||||
"ButtonSaveTracklist": "Захаваць спіс трэкаў",
|
||||
"ButtonScan": "Сканаваць",
|
||||
"ButtonScanLibrary": "Сканіраваць бібліятэку",
|
||||
@@ -107,10 +108,10 @@
|
||||
"ButtonSubmit": "Адправіць",
|
||||
"ButtonTest": "Тэст",
|
||||
"ButtonUnlinkOpenId": "Адвязаць OpenID",
|
||||
"ButtonUpload": "Загрузіць",
|
||||
"ButtonUploadBackup": "Загрузіць рэзервовую копію",
|
||||
"ButtonUploadCover": "Загрузіць вокладку",
|
||||
"ButtonUploadOPMLFile": "Загрузіць файл OPML",
|
||||
"ButtonUpload": "Запампаваць",
|
||||
"ButtonUploadBackup": "Запампаваць рэзервовую копію",
|
||||
"ButtonUploadCover": "Запампаваць вокладку",
|
||||
"ButtonUploadOPMLFile": "Запампаваць файл OPML",
|
||||
"ButtonUserDelete": "Выдаліць карыстальніка {0}",
|
||||
"ButtonUserEdit": "Рэдагаваць карыстальніка {0}",
|
||||
"ButtonViewAll": "Прагледзець усе",
|
||||
@@ -121,7 +122,7 @@
|
||||
"HeaderAccount": "Уліковы запіс",
|
||||
"HeaderAddCustomMetadataProvider": "Дадаванне карыстальніцкага пастаўшчыка метаданых",
|
||||
"HeaderAdvanced": "Дадаткова",
|
||||
"HeaderApiKeys": "API-ключы",
|
||||
"HeaderApiKeys": "Ключы API",
|
||||
"HeaderAppriseNotificationSettings": "Налады апавяшчэнняў Apprise",
|
||||
"HeaderAudioTracks": "Аўдыятрэкі",
|
||||
"HeaderAudiobookTools": "Сродкі кіравання файламі аўдыякніг",
|
||||
@@ -149,7 +150,7 @@
|
||||
"HeaderFindChapters": "Знайсці раздзелы",
|
||||
"HeaderIgnoredFiles": "Ігнараваныя файлы",
|
||||
"HeaderItemFiles": "Файлы элементаў",
|
||||
"HeaderItemMetadataUtils": "Утыліты для метаданых элементаў",
|
||||
"HeaderItemMetadataUtils": "Утыліты для метаданых",
|
||||
"HeaderLastListeningSession": "Апошні сеанс праслухоўвання",
|
||||
"HeaderLatestEpisodes": "Апошнія выпускі",
|
||||
"HeaderLibraries": "Бібліятэкі",
|
||||
@@ -166,7 +167,7 @@
|
||||
"HeaderMetadataOrderOfPrecedence": "Парадак прыярытэту метаданых",
|
||||
"HeaderMetadataToEmbed": "Метаданыя для ўбудавання",
|
||||
"HeaderNewAccount": "Новы ўліковы запіс",
|
||||
"HeaderNewApiKey": "Новы API-ключ",
|
||||
"HeaderNewApiKey": "Новы ключ API",
|
||||
"HeaderNewLibrary": "Новая бібліятэка",
|
||||
"HeaderNotificationCreate": "Стварыць апавяшчэнне",
|
||||
"HeaderNotificationUpdate": "Абнавіць апавяшчэнне",
|
||||
@@ -194,9 +195,10 @@
|
||||
"HeaderScheduleEpisodeDownloads": "Расклад аўтаматычных спампоўванняў выпускаў",
|
||||
"HeaderScheduleLibraryScans": "Расклад аўтаматычнага сканавання бібліятэкі",
|
||||
"HeaderSession": "Сеанс",
|
||||
"HeaderSessions": "Сеансы",
|
||||
"HeaderSetBackupSchedule": "Наладзіць расклад рэзервовага капіравання",
|
||||
"HeaderSettings": "Налады",
|
||||
"HeaderSettingsDisplay": "Дысплей",
|
||||
"HeaderSettingsDisplay": "Выгляд",
|
||||
"HeaderSettingsExperimental": "Эксперыментальныя функцыі",
|
||||
"HeaderSettingsGeneral": "Агульныя",
|
||||
"HeaderSettingsScanner": "Сканер",
|
||||
@@ -207,12 +209,12 @@
|
||||
"HeaderStatsLongestItems": "Найдаўжэйшыя элементы (гадзіны)",
|
||||
"HeaderStatsMinutesListeningChart": "Хвілін праслухоўвання (апошнія 7 дзён)",
|
||||
"HeaderStatsRecentSessions": "Апошнія сеансы",
|
||||
"HeaderStatsTop10Authors": "10 лепшых аўтараў",
|
||||
"HeaderStatsTop5Genres": "5 лепшых жанраў",
|
||||
"HeaderStatsTop10Authors": "Топ 10 аўтараў",
|
||||
"HeaderStatsTop5Genres": "Топ 5 жанраў",
|
||||
"HeaderTableOfContents": "Змест",
|
||||
"HeaderTools": "Інструменты",
|
||||
"HeaderUpdateAccount": "Абнавіць уліковы запіс",
|
||||
"HeaderUpdateApiKey": "Абнавіць API-ключ",
|
||||
"HeaderUpdateApiKey": "Абнавіць ключ API",
|
||||
"HeaderUpdateAuthor": "Абнавіць аўтара",
|
||||
"HeaderUpdateDetails": "Абнавіць падрабязнасці",
|
||||
"HeaderUpdateLibrary": "Абнавіць бібліятэку",
|
||||
@@ -239,21 +241,21 @@
|
||||
"LabelAll": "Усе",
|
||||
"LabelAllEpisodesDownloaded": "Усе выпускі спампаваныя",
|
||||
"LabelAllUsers": "Усе карыстальнікі",
|
||||
"LabelAllUsersExcludingGuests": "Усе карыстальнікі, акрамя гасцей",
|
||||
"LabelAllUsersIncludingGuests": "Усе карыстальнікі, уключаючы гасцей",
|
||||
"LabelAllUsersExcludingGuests": "Усіх карыстальнікаў, акрамя гасцей",
|
||||
"LabelAllUsersIncludingGuests": "Усіх карыстальнікаў, уключаючы гасцей",
|
||||
"LabelAlreadyInYourLibrary": "Ужо ў вашай бібліятэцы",
|
||||
"LabelApiKeyCreated": "API-ключ \"{0}\" паспяхова створаны.",
|
||||
"LabelApiKeyCreatedDescription": "Пераканайцеся, што вы скапіявалі API-ключ зараз, бо паўторна яго ўбачыць не атрымаецца.",
|
||||
"LabelApiKeyCreated": "Ключ API \"{0}\" паспяхова створаны.",
|
||||
"LabelApiKeyCreatedDescription": "Абавязкова скапіюйце ключ API зараз, бо паўторна яго ўбачыць не атрымаецца.",
|
||||
"LabelApiKeyUser": "Дзейнічаць ад імя карыстальніка",
|
||||
"LabelApiKeyUserDescription": "Гэты API-ключ будзе мець тыя ж правы, што і карыстальнік, ад імя якога ён дзейнічае. У журналах гэта будзе выглядаць так, быццам запыт робіць сам карыстальнік.",
|
||||
"LabelApiKeyUserDescription": "Гэты ключ API будзе мець тыя ж правы, што і карыстальнік, ад імя якога ён дзейнічае. У журналах гэта будзе выглядаць так, быццам запыт робіць сам карыстальнік.",
|
||||
"LabelApiToken": "Токен API",
|
||||
"LabelAppend": "Дадаць",
|
||||
"LabelAudioBitrate": "Бітрэйт аўдыя (напрыклад, 128к)",
|
||||
"LabelAudioChannels": "Аўдыяканалы (1 або 2)",
|
||||
"LabelAudioCodec": "Аўдыякодэк",
|
||||
"LabelAuthor": "Аўтар",
|
||||
"LabelAuthorFirstLast": "Аўтар (Імя Прозвішча)",
|
||||
"LabelAuthorLastFirst": "Аўтар (Прозвішча, Імя)",
|
||||
"LabelAuthorFirstLast": "Аўтар (імя, прозвішча)",
|
||||
"LabelAuthorLastFirst": "Аўтар (прозвішча, імя)",
|
||||
"LabelAuthors": "Аўтары",
|
||||
"LabelAutoDownloadEpisodes": "Аўтаматычна спампоўваць выпускі",
|
||||
"LabelAutoFetchMetadata": "Аўтаматычнае атрыманне метаданых",
|
||||
@@ -264,7 +266,7 @@
|
||||
"LabelAutoRegisterDescription": "Аўтаматычна ствараць новых карыстальнікаў пасля ўваходу ў сістэму",
|
||||
"LabelBackToUser": "Вярнуцца да карыстальніка",
|
||||
"LabelBackupAudioFiles": "Рэзервовае капіраванне аўдыяфайлаў",
|
||||
"LabelBackupLocation": "Месцазнаходжанне рэзервовых копій",
|
||||
"LabelBackupLocation": "Размяшчэнне рэзервовых копій",
|
||||
"LabelBackupsEnableAutomaticBackups": "Аўтаматычнае рэзервовае капіраванне",
|
||||
"LabelBackupsEnableAutomaticBackupsHelp": "Рэзервовыя копіі захаваныя ў /metadata/backups",
|
||||
"LabelBackupsMaxBackupSize": "Максімальны памер рэзервовай копіі (у ГБ) (0 — неабмежавана)",
|
||||
@@ -284,7 +286,7 @@
|
||||
"LabelChaptersFound": "раздзелаў знойдзена",
|
||||
"LabelClickForMoreInfo": "Націсніце для больш падрабязнай інфармацыі",
|
||||
"LabelClickToUseCurrentValue": "Націсніце, каб выкарыстоўваць бягучае значэнне",
|
||||
"LabelClosePlayer": "Зачыніць прайгравальнік",
|
||||
"LabelClosePlayer": "Закрыць прайгравальнік",
|
||||
"LabelCodec": "Кодэк",
|
||||
"LabelCollapseSeries": "Згарнуць серыі",
|
||||
"LabelCollapseSubSeries": "Згарнуць падсерыі",
|
||||
@@ -292,7 +294,7 @@
|
||||
"LabelCollections": "Калекцыі",
|
||||
"LabelComplete": "Завяршыць",
|
||||
"LabelConfirmPassword": "Пацвердзіце пароль",
|
||||
"LabelContinueListening": "Працягваць слухаць",
|
||||
"LabelContinueListening": "Працяг праслухоўвання",
|
||||
"LabelContinueReading": "Працягнуць чытанне",
|
||||
"LabelContinueSeries": "Працягнуць серыі",
|
||||
"LabelCorsAllowed": "Дазволеныя крыніцы CORS",
|
||||
@@ -316,7 +318,7 @@
|
||||
"LabelDirectory": "Каталог",
|
||||
"LabelDiscFromFilename": "Дыск з файла",
|
||||
"LabelDiscFromMetadata": "Дыск з метаданых",
|
||||
"LabelDiscover": "Знайсці",
|
||||
"LabelDiscover": "Знаходкі",
|
||||
"LabelDownload": "Спампаваць",
|
||||
"LabelDownloadNEpisodes": "Спампавана {0} выпускаў",
|
||||
"LabelDownloadable": "Спампоўваецца",
|
||||
@@ -332,7 +334,7 @@
|
||||
"LabelEmailSettingsFromAddress": "Адрас адпраўніка",
|
||||
"LabelEmailSettingsRejectUnauthorized": "Адхіляць неаўтарызаваныя сертыфікаты",
|
||||
"LabelEmailSettingsRejectUnauthorizedHelp": "Адключэнне праверкі SSL-сертыфіката можа зрабіць ваша злучэнне ўразлівым перад пагрозамі бяспекі, такімі як атакі \"чалавек пасярэдзіне\". Адключайце гэтую опцыю толькі калі цалкам разумееце наступствы і ўпэўнены ў надзейнасці паштовага сервера.",
|
||||
"LabelEmailSettingsSecure": "Бяспечныя",
|
||||
"LabelEmailSettingsSecure": "Бяспечна",
|
||||
"LabelEmailSettingsSecureHelp": "Калі ўключана, злучэнне будзе выкарыстоўваць TLS пры падключэнні да сервера. Калі выключана, TLS будзе выкарыстоўвацца толькі ў выпадку падтрымкі пашырэння STARTTLS на серверы. У большасці выпадкаў усталюйце значэнне true пры падключэнні да порта 465. Для партоў 587 або 25 не ўключайце яго. (інфармацыя з nodemailer.com/smtp/#authentication)",
|
||||
"LabelEmailSettingsTestAddress": "Тэставы адрас",
|
||||
"LabelEmbeddedCover": "Убудаваная вокладка",
|
||||
@@ -415,6 +417,7 @@
|
||||
"LabelIntervalEveryHour": "Кожную гадзіну",
|
||||
"LabelIntervalEveryMinute": "Кожную хвіліну",
|
||||
"LabelInvert": "Інвертаваць",
|
||||
"LabelIpAddress": "IP-адрас",
|
||||
"LabelItem": "Элемент",
|
||||
"LabelJumpBackwardAmount": "Час пераходу назад",
|
||||
"LabelJumpForwardAmount": "Час пераходу наперад",
|
||||
@@ -424,7 +427,7 @@
|
||||
"LabelLastBookAdded": "Апошняя дададзеная кніга",
|
||||
"LabelLastBookUpdated": "Апошняя абноўленая кніга",
|
||||
"LabelLastProgressDate": "Апошні прагрэс: {0}",
|
||||
"LabelLastSeen": "Апошні прагляд",
|
||||
"LabelLastSeen": "Апошняя актыўнасць",
|
||||
"LabelLastTime": "Апошні раз",
|
||||
"LabelLastUpdate": "Апошняе абнаўленне",
|
||||
"LabelLayout": "Знешні выгляд",
|
||||
@@ -545,7 +548,7 @@
|
||||
"LabelRSSFeedSlug": "Ідэнтыфікатар RSS-стужкі",
|
||||
"LabelRSSFeedURL": "URL RSS-стужкі",
|
||||
"LabelRandomly": "Выпадкова",
|
||||
"LabelReAddSeriesToContinueListening": "Дадаць серыю зноў у Працягваць слухаць",
|
||||
"LabelReAddSeriesToContinueListening": "Дадаць серыю зноў у Працяг праслухоўвання",
|
||||
"LabelRead": "Чытаць",
|
||||
"LabelReadAgain": "Чытаць зноў",
|
||||
"LabelReadEbookWithoutProgress": "Чытаць электронную кнігу без захавання прагрэсу",
|
||||
@@ -588,23 +591,23 @@
|
||||
"LabelSettingsBookshelfViewHelp": "Рэалістычны дызайн з драўлянымі паліцамі",
|
||||
"LabelSettingsChromecastSupport": "Падтрымка Chromecast",
|
||||
"LabelSettingsDateFormat": "Фарматы даты",
|
||||
"LabelSettingsEnableWatcher": "Аўтаматычна сачыць за зменамі ў бібліятэцках",
|
||||
"LabelSettingsEnableWatcher": "Аўтаматычна сачыць за зменамі ў бібліятэках",
|
||||
"LabelSettingsEnableWatcherForLibrary": "Аўтаматычна сачыць за зменамі ў бібліятэцы",
|
||||
"LabelSettingsEnableWatcherHelp": "Адключае аўтаматычнае дадаванне/абнаўленне элементаў пры выяўленні змен у файлах. *Патрабуецца перазапуск сервера",
|
||||
"LabelSettingsEpubsAllowScriptedContent": "Дазваляць скрыпты ў файлах EPUB",
|
||||
"LabelSettingsEpubsAllowScriptedContentHelp": "Дазволіць файлам EPUB выконваць скрыпты. Рэкамендуецца пакінуць гэтую наладу выключанай, калі вы не давяраеце крыніцы файлаў EPUB.",
|
||||
"LabelSettingsExperimentalFeatures": "Эксперыментальныя функцыі",
|
||||
"LabelSettingsExperimentalFeaturesHelp": "Функцыі ў распрацоўцы, для якіх вашы водгукі і дапамога ў тэставанні будуць карыснымі. Націсніце, каб адкрыць абмеркаванне на GitHub.",
|
||||
"LabelSettingsFindCovers": "Знайсці вокладкі",
|
||||
"LabelSettingsFindCovers": "Шукаць вокладкі",
|
||||
"LabelSettingsFindCoversHelp": "Калі ў вашай аўдыякнізе няма ўбудаванай вокладкі або відарыса вокладкі ў папцы, сканер паспрабуе знайсці вокладку.<br>Заўвага: гэта павялічыць час сканіравання",
|
||||
"LabelSettingsHideSingleBookSeries": "Схаваць серыі з адной кнігай",
|
||||
"LabelSettingsHideSingleBookSeriesHelp": "Серыі, якія змяшчаюць толькі адну кнігу, будуць схаваны са старонкі серый і паліц на галоўнай старонцы.",
|
||||
"LabelSettingsHomePageBookshelfView": "Галоўная старонка выкарыстоўвае выгляд кніжнай паліцы",
|
||||
"LabelSettingsLibraryBookshelfView": "Бібліятэка выкарыстоўвае выгляд кніжнай паліцы",
|
||||
"LabelSettingsHomePageBookshelfView": "Кніжныя паліцы на галоўнай старонцы",
|
||||
"LabelSettingsLibraryBookshelfView": "Кніжныя паліцы ў бібліятэцы",
|
||||
"LabelSettingsLibraryMarkAsFinishedPercentComplete": "Працэнт завяршэння большы за",
|
||||
"LabelSettingsLibraryMarkAsFinishedTimeRemaining": "Час, што застаўся, менш за (секунд)",
|
||||
"LabelSettingsLibraryMarkAsFinishedTimeRemaining": "Час, што застаўся, меншы за (секунд)",
|
||||
"LabelSettingsLibraryMarkAsFinishedWhen": "Пазначыць элемент медыя як завершаны, калі",
|
||||
"LabelSettingsOnlyShowLaterBooksInContinueSeries": "Пропусціць папярэднія кнігі ў \"Працягнуць серыю\"",
|
||||
"LabelSettingsOnlyShowLaterBooksInContinueSeries": "Прапусціць папярэднія кнігі ў \"Працягнуць серыю\"",
|
||||
"LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "Паліца \"Працягнуць серыю\" на галоўнай старонцы паказвае першую не пачатую кнігу ў серыях, дзе завершана хаця б адна кніга і няма кніг у працэсе чытання. Уключэнне гэтай налады дазволіць працягваць серыю з самай апошняй завершанай кнігі замест першай не пачатай.",
|
||||
"LabelSettingsParseSubtitles": "Аналізаваць падзагалоўкі",
|
||||
"LabelSettingsParseSubtitlesHelp": "Выдзяляць падзагаловак з назваў папак аўдыякніг.<br>Падзагаловак павінен быць аддзелены сімвалам \" - \".<br>Напрыклад, \"Назва кнігі - Падзагаловак тут\" мае падзагаловак \"Падзагаловак тут\"",
|
||||
@@ -618,7 +621,7 @@
|
||||
"LabelSettingsSquareBookCoversHelp": "Аддаваць перавагу квадратным вокладкам замест стандартных вокладак з суадносінамі бакоў 1.6:1",
|
||||
"LabelSettingsStoreCoversWithItem": "Захоўваць вокладкі з элементам",
|
||||
"LabelSettingsStoreCoversWithItemHelp": "Прадвызначана вокладкі захоўваюцца ў /metadata/items, уключэнне гэтай опцыі забяспечыць захоўванне вокладак у папцы элемента бібліятэкі. Захоўвацца будзе толькі адзін файл з назвай \"cover\"",
|
||||
"LabelSettingsStoreMetadataWithItem": "Захоўваць метаданыя разам з элементам",
|
||||
"LabelSettingsStoreMetadataWithItem": "Захоўваць метаданыя з элементам",
|
||||
"LabelSettingsStoreMetadataWithItemHelp": "Прадвызначана метаданыя захоўваюцца ў /metadata/items. Пры ўключэнні гэтай опцыі файлаў метаданых будуць захоўвацца ў папках элементаў бібліятэкі",
|
||||
"LabelSettingsTimeFormat": "Фармат часу",
|
||||
"LabelShare": "Абагуліць",
|
||||
@@ -634,14 +637,14 @@
|
||||
"LabelSortAscending": "Па ўзрастанні",
|
||||
"LabelSortDescending": "Па ўбыванні",
|
||||
"LabelSortPubDate": "Сартаваць па даце публікацыі",
|
||||
"LabelStart": "Пачаць",
|
||||
"LabelStart": "Пачатак",
|
||||
"LabelStartTime": "Час пачатку",
|
||||
"LabelStarted": "Пачата",
|
||||
"LabelStartedAt": "Пачата ў",
|
||||
"LabelStartedDate": "Пачата {0}",
|
||||
"LabelStatsAudioTracks": "Аўдыятрэкі",
|
||||
"LabelStatsAuthors": "Аўтары",
|
||||
"LabelStatsBestDay": "Лепшы дзень",
|
||||
"LabelStatsAuthors": "Аўтараў",
|
||||
"LabelStatsBestDay": "Найлепшы дзень",
|
||||
"LabelStatsDailyAverage": "У сярэднім за дзень",
|
||||
"LabelStatsDays": "Дзён",
|
||||
"LabelStatsDaysListened": "Дзён праслухана",
|
||||
@@ -655,7 +658,7 @@
|
||||
"LabelStatsOverallHours": "Агульная колькасць гадзін",
|
||||
"LabelStatsWeekListening": "Праслухана за тыдзень",
|
||||
"LabelSubtitle": "Падзагаловак",
|
||||
"LabelSupportedFileTypes": "Падтрымліваемыя тыпы файлаў",
|
||||
"LabelSupportedFileTypes": "Падтрымліваюцца тыпы файлаў",
|
||||
"LabelTag": "Метка",
|
||||
"LabelTags": "Меткі",
|
||||
"LabelTagsAccessibleToUser": "Меткі, даступныя карыстальніку",
|
||||
@@ -742,7 +745,7 @@
|
||||
"MessageAuthenticationSecurityMessage": "Дзеля бяспекі была палепшана аўтэнтыфікацыя. Усім карыстальнікам трэба паўторна ўвайсці ў сістэму.",
|
||||
"MessageBackupsDescription": "Рэзервовыя копіі ўключаюць карыстальнікаў, іх прагрэс, падрабязнасці элементаў бібліятэкі, налады сервера і відарысы, якія захоўваюцца ў <code>/metadata/items</code> і <code>/metadata/authors</code>. Рэзервовыя копіі <strong>не</strong> ўключаюць файлы, якія захоўваюцца ў папках бібліятэкі.",
|
||||
"MessageBackupsLocationEditNote": "Заўвага: Абнаўленне месцазнаходжання рэзервовых копій не перамяшчае і не змяняе існуючыя рэзервовыя копіі",
|
||||
"MessageBackupsLocationNoEditNote": "Заўвага: Месцазнаходжанне рэзервовых копій задаецца праз зменную асяроддзя і не можа быць зменена тут.",
|
||||
"MessageBackupsLocationNoEditNote": "Заўвага: Размяшчэнне рэзервовых копій задаецца праз зменную асяроддзя і не можа быць зменена тут.",
|
||||
"MessageBackupsLocationPathEmpty": "Шлях да месцазнаходжання рэзервовых копій не можа быць пустым",
|
||||
"MessageBatchEditPopulateMapDetailsAllHelp": "Запоўніце ўключаныя палі данымі з усіх элементаў. Палі з некалькімі значэннямі будуць аб'яднаны",
|
||||
"MessageBatchEditPopulateMapDetailsItemHelp": "Запоўніце ўключаныя палі падрабязнасцей карты данымі з гэтага элемента",
|
||||
@@ -774,6 +777,7 @@
|
||||
"MessageConfirmDeleteSession": "Вы ўпэўнены, што хочаце выдаліць гэты сеанс?",
|
||||
"MessageConfirmEmbedMetadataInAudioFiles": "Вы ўпэўнены, што хочаце ўбудаваць метаданыя ў {0} аўдыяфайлаў?",
|
||||
"MessageConfirmForceReScan": "Вы ўпэўнены, што хочаце прымусова паўторна сканіраваць?",
|
||||
"MessageConfirmLogoutDevice": "Вы ўпэўнены, што хочаце выйсці з \"{0}\"?",
|
||||
"MessageConfirmMarkAllEpisodesFinished": "Вы ўпэўнены, што хочаце пазначыць усе выпускі як завершаныя?",
|
||||
"MessageConfirmMarkAllEpisodesNotFinished": "Вы ўпэўнены, што хочаце пазначыць усе выпускі як незавершаныя?",
|
||||
"MessageConfirmMarkItemFinished": "Вы ўпэўнены, што хочаце пазначыць \"{0}\" як завершаны?",
|
||||
@@ -884,7 +888,7 @@
|
||||
"MessageRemoveEpisodes": "Выдаліць выпускі ({0})",
|
||||
"MessageRemoveFromPlayerQueue": "Выдаліць з чаргі прагравання",
|
||||
"MessageRemoveUserWarning": "Вы ўпэўнены, што хочаце назаўжды выдаліць карыстальніка \"{0}\"?",
|
||||
"MessageReportBugsAndContribute": "Паведамляйце пра памылкі, прапануйце новыя функцыі і ўдзельнічайце на",
|
||||
"MessageReportBugsAndContribute": "Паведамляйце пра памылкі, прапануйце функцыі і ўносьце свой уклад на",
|
||||
"MessageResetChaptersConfirm": "Вы ўпэўнены, што хочаце скінуць раздзелы і адрабіць зробленыя вамі змены?",
|
||||
"MessageRestoreBackupConfirm": "Вы ўпэўнены, што хочаце аднавіць рэзервовую копію, створаную",
|
||||
"MessageRestoreBackupWarning": "Аднаўленне рэзервовай копіі перазапіша ўсю базу даных, размешчаную ў /config, а таксама відарысы вокладкі ў /metadata/items і /metadata/authors. <br /><br /> Рэзервовыя копіі не змяняюць файлы ў папках бібліятэкі. Калі вы ўключылі налады сервера для захоўвання воклак і метаданых у папках бібліятэкі, гэтыя файлы не будуць захаваныя ў рэзервовых копіях і не зменяцца. <br /><br /> Усе кліенты, якія карыстаюцца вашым серверам, будуць аўтаматычна абноўлены.",
|
||||
@@ -968,7 +972,7 @@
|
||||
"StatsBooksAdditional": "Некаторыя дапаўненні ўключаюць…",
|
||||
"StatsBooksFinished": "завершана кніг",
|
||||
"StatsBooksFinishedThisYear": "Некаторыя кнігі завершаны ў гэтым годзе…",
|
||||
"StatsBooksListenedTo": "кнігі, якія былі праслуханы",
|
||||
"StatsBooksListenedTo": "кніг праслухана",
|
||||
"StatsCollectionGrewTo": "Ваша калекцыя кніг павялічылася да…",
|
||||
"StatsSessions": "сеансаў",
|
||||
"StatsSpentListening": "праслухана",
|
||||
@@ -1147,7 +1151,7 @@
|
||||
"ToastUnlinkOpenIdFailed": "Не ўдалося адвязаць карыстальніка ад OpenID",
|
||||
"ToastUnlinkOpenIdSuccess": "Карыстальнік адвязаны ад OpenID",
|
||||
"ToastUploaderFilepathExistsError": "Файл \"{0}\" ужо існуе на серверы",
|
||||
"ToastUploaderItemExistsInSubdirectoryError": "Элемент \"{0}\" выкарыстоўвае падкаталог шляху загрузкі.",
|
||||
"ToastUploaderItemExistsInSubdirectoryError": "Элемент \"{0}\" выкарыстоўвае падкаталог шляху запампоўкі.",
|
||||
"ToastUserDeleteFailed": "Не ўдалося выдаліць карыстальніка",
|
||||
"ToastUserDeleteSuccess": "Карыстальнік выдалены",
|
||||
"ToastUserPasswordChangeSuccess": "Пароль паспяхова зменены",
|
||||
|
||||
+139
-4
@@ -436,7 +436,7 @@
|
||||
"LabelLibraryFilterSublistEmpty": "Не {0}",
|
||||
"LabelLibraryItem": "Елемент на Библиотека",
|
||||
"LabelLibraryName": "Име на Библиотека",
|
||||
"LabelLibrarySortByProgress": "Прогрес: Последно Обновен",
|
||||
"LabelLibrarySortByProgress": "Прогрес: Последно обновление",
|
||||
"LabelLibrarySortByProgressFinished": "Прогрес: Приключено",
|
||||
"LabelLibrarySortByProgressStarted": "Прогрес: Започнато",
|
||||
"LabelLimit": "Лимит",
|
||||
@@ -752,7 +752,7 @@
|
||||
"MessageBookshelfNoRSSFeeds": "Няма отворени RSS feed-ове",
|
||||
"MessageBookshelfNoResultsForFilter": "Няма резултат за филтер \"{0}: {1}\"",
|
||||
"MessageBookshelfNoResultsForQuery": "Няма резултати от заявката",
|
||||
"MessageBookshelfNoSeries": "Нямаш сеЗЙ",
|
||||
"MessageBookshelfNoSeries": "Нямате поредица",
|
||||
"MessageBulkChapterPattern": "Колко глави искате да добавите, използвайки тази схема за номериране?",
|
||||
"MessageChapterEndIsAfter": "Краят на главата е след края на вашата аудиокнига",
|
||||
"MessageChapterErrorFirstNotZero": "Първата глава трябва да започва от 0",
|
||||
@@ -892,7 +892,7 @@
|
||||
"MessageScheduleRunEveryWeekdayAtTime": "Изпълни всеки {0} в {1}",
|
||||
"MessageSearchResultsFor": "Резултати от търсенето за",
|
||||
"MessageSelected": "{0} избрани",
|
||||
"MessageSeriesSequenceCannotContainSpaces": "Подредбата в серия не може да съдържа шпации.",
|
||||
"MessageSeriesSequenceCannotContainSpaces": "Подредбата в серия не може да съдържа шпации",
|
||||
"MessageServerCouldNotBeReached": "Сървърът не може да бъде достигнат",
|
||||
"MessageSetChaptersFromTracksDescription": "Задайте глави, като използвате всеки аудио файл като глава и заглавие на главата като име на аудио файла",
|
||||
"MessageShareExpirationWillBe": "Изтичането ще бъде на <strong>{0}</strong>",
|
||||
@@ -956,6 +956,8 @@
|
||||
"NotificationOnEpisodeDownloadedDescription": "Изпълнява се при автоматично изтегляне на подкаст епизод",
|
||||
"NotificationOnRSSFeedDisabledDescription": "Изпълнява се, когато автоматичното изтегляне на епизодите е деактивирано, поради твърде много неуспешни опити",
|
||||
"NotificationOnRSSFeedFailedDescription": "Пуска се когато заявката за RSS фийд е неуспешна за автоматично сваляне на епизод",
|
||||
"NotificationOnTestDescription": "Event за тестване на системата за нотификации",
|
||||
"PlaceholderBulkChapterInput": "Въведете име на глава или използвайте номериране (прим. 'Епизод 1', 'Глава 10', '1.')",
|
||||
"PlaceholderNewCollection": "Ново име на колекцията",
|
||||
"PlaceholderNewFolderPath": "Нов път на папката",
|
||||
"PlaceholderNewPlaylist": "Ново име на плейлиста",
|
||||
@@ -963,39 +965,103 @@
|
||||
"PlaceholderSearchEpisode": "Търсене на Епизоди...",
|
||||
"StatsAuthorsAdded": "добаврени автори",
|
||||
"StatsBooksAdded": "добавени книги",
|
||||
"StatsBooksAdditional": "Някой от вкючените добавки…",
|
||||
"StatsBooksFinished": "завършени книги",
|
||||
"StatsBooksFinishedThisYear": "Някой от книгите приключени тази година…",
|
||||
"StatsBooksListenedTo": "слушани книги",
|
||||
"StatsCollectionGrewTo": "Твоята книжна колекция израсна до…",
|
||||
"StatsSessions": "сесии",
|
||||
"StatsSpentListening": "прекарано в слушане",
|
||||
"StatsTopAuthor": "ТОП АВТОР",
|
||||
"StatsTopAuthors": "ТОП АВТОРИ",
|
||||
"StatsTopGenre": "ТОП ЖАНР",
|
||||
"StatsTopGenres": "ТОП ЖАНРА",
|
||||
"StatsTopMonth": "ТОП МЕСЕЦ",
|
||||
"StatsTopNarrator": "ТОП РАЗКАЗВАЧ",
|
||||
"StatsTopNarrators": "ТОП РАЗКАЗВАЧИ",
|
||||
"StatsTotalDuration": "С пълно времетраене…",
|
||||
"StatsYearInReview": "ГОДИНАТА В ПРЕГЛЕД",
|
||||
"ToastAccountUpdateSuccess": "Успешно обновяване на акаунта",
|
||||
"ToastAppriseUrlRequired": "Трябва да въведете Apprise URL",
|
||||
"ToastAsinRequired": "ASIN-а е задължителен",
|
||||
"ToastAuthorImageRemoveSuccess": "Авторската снимка е премахната",
|
||||
"ToastAuthorNotFound": "Автор \"{0}\" не е намерен",
|
||||
"ToastAuthorRemoveSuccess": "Арторът е премахнат",
|
||||
"ToastAuthorSearchNotFound": "Авторът не е намерен",
|
||||
"ToastAuthorUpdateMerged": "Обновяване на автора сливано",
|
||||
"ToastAuthorUpdateSuccess": "Автора обновен",
|
||||
"ToastAuthorUpdateSuccessNoImageFound": "Автор обновен (не е намерена снимка)",
|
||||
"ToastBackupAppliedSuccess": "Архивът е приложен",
|
||||
"ToastBackupCreateFailed": "Неуспешно създаване на архив",
|
||||
"ToastBackupCreateSuccess": "Архивът е създаден",
|
||||
"ToastBackupDeleteFailed": "Неуспешно изтриване на архив",
|
||||
"ToastBackupDeleteSuccess": "Архивът е изтрит",
|
||||
"ToastBackupInvalidMaxKeep": "Невалиден брой за архиви за запазване",
|
||||
"ToastBackupInvalidMaxSize": "Невалиден максимален рамер на архив",
|
||||
"ToastBackupRestoreFailed": "Неуспешно възстановяване на архив",
|
||||
"ToastBackupUploadFailed": "Неуспешно качване на архив",
|
||||
"ToastBackupUploadSuccess": "Архивът е качен",
|
||||
"ToastBatchApplyDetailsToItemsSuccess": "Детайли приложени на предмети",
|
||||
"ToastBatchDeleteFailed": "Груповото изтриване се провали",
|
||||
"ToastBatchDeleteSuccess": "Успешно групово изтриване",
|
||||
"ToastBatchQuickMatchFailed": "Груповото Бързо Съвпадение се провали!",
|
||||
"ToastBatchQuickMatchStarted": "Груповото Бързо Съвпадение на {0} книги започна!",
|
||||
"ToastBatchUpdateFailed": "Неуспешно групово актуализиране",
|
||||
"ToastBatchUpdateSuccess": "Успешно групово актуализиране",
|
||||
"ToastBookmarkCreateFailed": "Неуспешно създаване на отметка",
|
||||
"ToastBookmarkCreateSuccess": "Отметката е създадена",
|
||||
"ToastBookmarkRemoveSuccess": "Отметката е премахната",
|
||||
"ToastBulkChapterInvalidCount": "Въведете число между 1 и 150",
|
||||
"ToastCachePurgeFailed": "Неуспешно изчистване на кеша",
|
||||
"ToastCachePurgeSuccess": "Успешно изчистване на кеша",
|
||||
"ToastChapterLocked": "Главата е заключена.",
|
||||
"ToastChapterStartTimeAdjusted": "Начално време на главате е настоено с {0} секунди",
|
||||
"ToastChaptersAllLocked": "Всички глави са заключени. Оключете някой глави за да преместите техните времена.",
|
||||
"ToastChaptersHaveErrors": "Главите имат грешки",
|
||||
"ToastChaptersInvalidShiftAmountLast": "Невалидно време за преместване. Началният час на последната глава ще превиши общата продължителност на аудиокнигата.",
|
||||
"ToastChaptersInvalidShiftAmountStart": "Невалидно време за преместване. Първата глава ще има нулева или отрицателна дължина и ще бъде презаписана от втората глава. Увеличете началното време на втората глава.",
|
||||
"ToastChaptersMustHaveTitles": "Главите трябва да имат заглавия",
|
||||
"ToastChaptersRemoved": "Главите са премахнати",
|
||||
"ToastChaptersUpdated": "Главите са актуализирани",
|
||||
"ToastCollectionItemsAddFailed": "Неуспешно добавяне на елемент(и) към колекцията",
|
||||
"ToastCollectionRemoveSuccess": "Колекцията е премахната",
|
||||
"ToastCollectionUpdateSuccess": "Колекцията е обновена",
|
||||
"ToastConnectionNotAvailable": "Няма връзка. Моля, опитайте отново по-късно",
|
||||
"ToastCoverSearchFailed": "Търсенето на корица е неуспешно",
|
||||
"ToastCoverUpdateFailed": "Обновяването на корицата е неуспешно",
|
||||
"ToastDateTimeInvalidOrIncomplete": "Датата и часът са невалидни или непълни",
|
||||
"ToastDeleteFileFailed": "Неуспешно изтриване на файла",
|
||||
"ToastDeleteFileSuccess": "Успешно изтриване на файла",
|
||||
"ToastDeviceAddFailed": "Неуспешно добавяне на устройство",
|
||||
"ToastDeviceNameAlreadyExists": "Вече съществува четец с това име",
|
||||
"ToastDeviceTestEmailFailed": "Неуспешно изпращане на тестов имейл",
|
||||
"ToastDeviceTestEmailSuccess": "Тестовият имейл е изпратен",
|
||||
"ToastEmailSettingsUpdateSuccess": "Имейл настройките са актуализирани",
|
||||
"ToastEncodeCancelFailed": "Неуспешно отменяне на кодирането",
|
||||
"ToastEncodeCancelSucces": "Кодирането е отменено",
|
||||
"ToastEpisodeDownloadQueueClearFailed": "Неуспешно изчистване на опашката",
|
||||
"ToastEpisodeDownloadQueueClearSuccess": "Опашката за изтегляне на епизоди е изчистена",
|
||||
"ToastEpisodeUpdateSuccess": "{0} епизода са актуализирани",
|
||||
"ToastErrorCannotShare": "Не може да се споделя директно от това устройство",
|
||||
"ToastFailedToCreate": "Неуспешно създаване",
|
||||
"ToastFailedToDelete": "Неуспешно изтриване",
|
||||
"ToastFailedToLoadData": "Неуспешно зареждане на данни",
|
||||
"ToastFailedToMatch": "Неуспешно съвпадение",
|
||||
"ToastFailedToShare": "Неуспешно споделяне",
|
||||
"ToastFailedToUpdate": "Неуспешно актуализиране",
|
||||
"ToastInvalidImageUrl": "Невалиден URL адрес на изображение",
|
||||
"ToastInvalidMaxEpisodesToDownload": "Невалиден максимален брой епизоди за изтегляне",
|
||||
"ToastInvalidUrl": "Невалиден URL адрес",
|
||||
"ToastInvalidUrls": "Един или повече URL адреси са невалидни",
|
||||
"ToastItemCoverUpdateSuccess": "Корицата на елемента е обновена",
|
||||
"ToastItemDeletedFailed": "Неуспешно изтриване на елемента",
|
||||
"ToastItemDeletedSuccess": "Елементът е изтрит",
|
||||
"ToastItemDetailsUpdateSuccess": "Детайлите на елемента са обновени",
|
||||
"ToastItemMarkedAsFinishedFailed": "Неуспешно маркиране като Завършено",
|
||||
"ToastItemMarkedAsFinishedSuccess": "Елементът е маркиран като завършен",
|
||||
"ToastItemMarkedAsNotFinishedFailed": "Неуспешно маркиране като Незавършено",
|
||||
"ToastItemMarkedAsNotFinishedSuccess": "Елементът е маркиран като незавършен",
|
||||
"ToastItemUpdateSuccess": "Елементът е актуализиран",
|
||||
"ToastLibraryCreateFailed": "Неуспешно създаване на библиотека",
|
||||
"ToastLibraryCreateSuccess": "Библиотеката \"{0}\" е създадена",
|
||||
"ToastLibraryDeleteFailed": "Неуспешно изтриване на библиотека",
|
||||
@@ -1003,28 +1069,97 @@
|
||||
"ToastLibraryScanFailedToStart": "Неуспешно стартиране на сканиране",
|
||||
"ToastLibraryScanStarted": "Сканирането на библиотеката е стартирано",
|
||||
"ToastLibraryUpdateSuccess": "Библиотеката \"{0}\" е обновена",
|
||||
"ToastMatchAllAuthorsFailed": "Неуспешно съвпадение на всички автори",
|
||||
"ToastMetadataFilesRemovedError": "Грешка при премахване на metadata.{0} файлове",
|
||||
"ToastMetadataFilesRemovedNoneFound": "Не са намерени metadata.{0} файлове в библиотеката",
|
||||
"ToastMetadataFilesRemovedNoneRemoved": "Не са премахнати metadata.{0} файлове",
|
||||
"ToastMetadataFilesRemovedSuccess": "Премахнати са {0} файла metadata.{1}",
|
||||
"ToastMustHaveAtLeastOnePath": "Трябва да има поне един път",
|
||||
"ToastNameEmailRequired": "Изискват се име и имейл",
|
||||
"ToastNameRequired": "Изисква се име",
|
||||
"ToastNewApiKeyUserError": "Трябва да изберете потребител",
|
||||
"ToastNewEpisodesFound": "Намерени са {0} нови епизода",
|
||||
"ToastNewUserCreatedFailed": "Неуспешно създаване на акаунт: „{0}“",
|
||||
"ToastNewUserCreatedSuccess": "Създаден е нов акаунт",
|
||||
"ToastNewUserLibraryError": "Трябва да изберете поне една библиотека",
|
||||
"ToastNewUserPasswordError": "Трябва да има парола; само root потребителят може да бъде с празна парола",
|
||||
"ToastNewUserTagError": "Трябва да изберете поне един етикет",
|
||||
"ToastNewUserUsernameError": "Въведете потребителско име",
|
||||
"ToastNoNewEpisodesFound": "Не са намерени нови епизоди",
|
||||
"ToastNoRSSFeed": "Подкастът няма RSS емисия",
|
||||
"ToastNoUpdatesNecessary": "Не са необходими актуализации",
|
||||
"ToastNotificationCreateFailed": "Неуспешно създаване на известие",
|
||||
"ToastNotificationDeleteFailed": "Неуспешно изтриване на известието",
|
||||
"ToastNotificationFailedMaximum": "Максималният брой неуспешни опити трябва да бъде >= 0",
|
||||
"ToastNotificationQueueMaximum": "Максималната опашка за известия трябва да бъде >= 0",
|
||||
"ToastNotificationSettingsUpdateSuccess": "Настройките за известия са актуализирани",
|
||||
"ToastNotificationTestTriggerFailed": "Неуспешно задействане на тестово известие",
|
||||
"ToastNotificationTestTriggerSuccess": "Тестовото известие е задействано",
|
||||
"ToastNotificationUpdateSuccess": "Известието е актуализирано",
|
||||
"ToastPlaylistCreateFailed": "Неуспешно създаване на плейлист",
|
||||
"ToastPlaylistCreateSuccess": "Плейлистът е създаден",
|
||||
"ToastPlaylistRemoveSuccess": "Плейлистът е премахнат",
|
||||
"ToastPlaylistUpdateSuccess": "Плейлистът е обновен",
|
||||
"ToastPodcastCreateFailed": "Неуспешно създаване на подкаст",
|
||||
"ToastPodcastCreateSuccess": "Подкаст успешно създаден",
|
||||
"ToastPodcastEpisodeUpdated": "Епизодът е актуализиран",
|
||||
"ToastPodcastGetFeedFailed": "Неуспешно извличане на емисията на подкаста",
|
||||
"ToastPodcastNoEpisodesInFeed": "Не са намерени епизоди в RSS емисията",
|
||||
"ToastPodcastNoRssFeed": "Подкастът няма RSS емисия",
|
||||
"ToastProgressIsNotBeingSynced": "Напредъкът не се синхронизира, рестартирайте възпроизвеждането",
|
||||
"ToastProviderCreatedFailed": "Неуспешно добавяне на доставчик",
|
||||
"ToastProviderCreatedSuccess": "Добавен е нов доставчик",
|
||||
"ToastProviderNameAndUrlRequired": "Изискват се име и URL адрес",
|
||||
"ToastProviderRemoveSuccess": "Доставчикът е премахнат",
|
||||
"ToastRSSFeedCloseFailed": "Неуспешно затваряне на RSS емисията",
|
||||
"ToastRSSFeedCloseSuccess": "RSS емисията е затворена",
|
||||
"ToastRemoveFailed": "Неуспешно премахване",
|
||||
"ToastRemoveItemFromCollectionFailed": "Неуспешно премахване на елемент от колекция",
|
||||
"ToastRemoveItemFromCollectionSuccess": "Елементът е премахнат от колекция",
|
||||
"ToastRemoveItemsWithIssuesFailed": "Неуспешно премахване на елементите от библиотеката с проблеми",
|
||||
"ToastRemoveItemsWithIssuesSuccess": "Елементите от библиотеката с проблеми са премахнати",
|
||||
"ToastRenameFailed": "Неуспешно преименуване",
|
||||
"ToastRescanFailed": "Повторното сканиране е неуспешно за {0}",
|
||||
"ToastRescanRemoved": "Повторното сканиране завърши: елементът е премахнат",
|
||||
"ToastRescanUpToDate": "Повторното сканиране завърши: елементът вече е актуален",
|
||||
"ToastRescanUpdated": "Повторното сканиране завърши: елементът е актуализиран",
|
||||
"ToastScanFailed": "Неуспешно сканиране на елемент от библиотеката",
|
||||
"ToastSelectAtLeastOneUser": "Изберете поне един потребител",
|
||||
"ToastSendEbookToDeviceFailed": "Неуспешно изпращане на електронна книга до устройство",
|
||||
"ToastSendEbookToDeviceSuccess": "Електронната книга е изпратена до устройство \"{0}\"",
|
||||
"ToastSeriesSubmitFailedSameName": "Не могат да бъдат добавени два сериала с едно и също име",
|
||||
"ToastSeriesUpdateFailed": "Неуспешно обновяване на серия",
|
||||
"ToastSeriesUpdateSuccess": "Серията е обновена",
|
||||
"ToastServerSettingsUpdateSuccess": "Настройките на сървъра са актуализирани",
|
||||
"ToastSessionCloseFailed": "Неуспешно затваряне на сесията",
|
||||
"ToastSessionDeleteFailed": "Неуспешно изтриване на сесия",
|
||||
"ToastSessionDeleteSuccess": "Сесията е изтрита",
|
||||
"ToastSleepTimerDone": "Таймерът за заспиване приключи... zZzzZz",
|
||||
"ToastSlugMustChange": "Краткият URL (slug) съдържа невалидни символи",
|
||||
"ToastSlugRequired": "Изисква се кратък URL (slug)",
|
||||
"ToastSocketConnected": "Свързан сокет",
|
||||
"ToastSocketDisconnected": "Сокетът е прекъснат",
|
||||
"ToastSocketFailedToConnect": "Неуспешно свързване на сокет",
|
||||
"ToastSortingPrefixesEmptyError": "Трябва да има поне 1 префикс за сортиране",
|
||||
"ToastSortingPrefixesUpdateSuccess": "Префиксите за сортиране са актуализирани ({0} елемента)",
|
||||
"ToastTitleRequired": "Изисква се заглавие",
|
||||
"ToastUnknownError": "Неизвестна грешка",
|
||||
"ToastUnlinkOpenIdFailed": "Неуспешно прекъсване на връзката на потребителя с OpenID",
|
||||
"ToastUnlinkOpenIdSuccess": "Връзката на потребителя с OpenID е прекъсната",
|
||||
"ToastUploaderFilepathExistsError": "Файловият път „{0}“ вече съществува на сървъра",
|
||||
"ToastUploaderItemExistsInSubdirectoryError": "Елементът „{0}“ използва поддиректория на пътя за качване.",
|
||||
"ToastUserDeleteFailed": "Неуспешно изтриване на потребител",
|
||||
"ToastUserDeleteSuccess": "Потребителят е изтрит"
|
||||
"ToastUserDeleteSuccess": "Потребителят е изтрит",
|
||||
"ToastUserPasswordChangeSuccess": "Паролата е променена успешно",
|
||||
"ToastUserPasswordMismatch": "Паролите не съвпадат",
|
||||
"ToastUserPasswordMustChange": "Новата парола не може да бъде същата като старата",
|
||||
"ToastUserRootRequireName": "Трябва да въведете root потребителско име",
|
||||
"TooltipAddChapters": "Добавяне на глава(и)",
|
||||
"TooltipAddOneSecond": "Добавяне на 1 секунда",
|
||||
"TooltipAdjustChapterStart": "Кликнете за коригиране на началния час",
|
||||
"TooltipLockAllChapters": "Заключване на всички глави",
|
||||
"TooltipLockChapter": "Заключване на глава (Shift+клик за диапазон)",
|
||||
"TooltipSubtractOneSecond": "Изваждане на 1 секунда",
|
||||
"TooltipUnlockAllChapters": "Отключване на всички глави",
|
||||
"TooltipUnlockChapter": "Отключване на глава (Shift+клик за диапазон)"
|
||||
}
|
||||
+94
-9
@@ -46,6 +46,7 @@
|
||||
"ButtonLatest": "সর্বশেষ",
|
||||
"ButtonLibrary": "লাইব্রেরি",
|
||||
"ButtonLogout": "লগআউট",
|
||||
"ButtonLogoutAllDevices": "সব ডিভাইস থেকে লগআউট করুন",
|
||||
"ButtonLookup": "সন্ধান",
|
||||
"ButtonManageTracks": "ট্র্যাকগুলি পরিচালনা করুন",
|
||||
"ButtonMapChapterTitles": "অধ্যায়ের শিরোনাম ম্যাপ করুন",
|
||||
@@ -76,7 +77,7 @@
|
||||
"ButtonReScan": "পুনরায় স্ক্যান",
|
||||
"ButtonRead": "পড়ুন",
|
||||
"ButtonReadLess": "সংক্ষিপ্ত",
|
||||
"ButtonReadMore": "বিস্তারিত পড়ুন",
|
||||
"ButtonReadMore": "আরও পড়ুন",
|
||||
"ButtonRefresh": "রিফ্রেশ",
|
||||
"ButtonRemove": "মুছে ফেলুন",
|
||||
"ButtonRemoveAll": "সব মুছে ফেলুন",
|
||||
@@ -194,6 +195,7 @@
|
||||
"HeaderScheduleEpisodeDownloads": "স্বয়ংক্রিয় পর্ব ডাউনলোডের সময়সূচী নির্ধারন করুন",
|
||||
"HeaderScheduleLibraryScans": "স্বয়ংক্রিয় লাইব্রেরি স্ক্যানের সময়সূচী",
|
||||
"HeaderSession": "সেশন",
|
||||
"HeaderSessions": "সেশনসমূহ",
|
||||
"HeaderSetBackupSchedule": "ব্যাকআপ সময়সূচী সেট করুন",
|
||||
"HeaderSettings": "সেটিংস",
|
||||
"HeaderSettingsDisplay": "প্রদর্শন",
|
||||
@@ -212,6 +214,7 @@
|
||||
"HeaderTableOfContents": "বিষয়বস্তুর সারণী",
|
||||
"HeaderTools": "টুলস",
|
||||
"HeaderUpdateAccount": "অ্যাকাউন্ট আপডেট করুন",
|
||||
"HeaderUpdateApiKey": "API কী আপডেট করুন",
|
||||
"HeaderUpdateAuthor": "লেখক আপডেট করুন",
|
||||
"HeaderUpdateDetails": "বিশদ আপডেট করুন",
|
||||
"HeaderUpdateLibrary": "লাইব্রেরি আপডেট করুন",
|
||||
@@ -226,6 +229,7 @@
|
||||
"LabelAccountTypeAdmin": "প্রশাসন",
|
||||
"LabelAccountTypeGuest": "অতিথি",
|
||||
"LabelAccountTypeUser": "ব্যবহারকারী",
|
||||
"LabelActivities": "কার্যকলাপ",
|
||||
"LabelActivity": "ক্রিয়াকলাপ",
|
||||
"LabelAddToCollection": "সংগ্রহে যোগ করুন",
|
||||
"LabelAddToCollectionBatch": "সংগ্রহে {0}টি বই যোগ করুন",
|
||||
@@ -235,10 +239,15 @@
|
||||
"LabelAddedDate": "যোগ করা হয়েছে {0}",
|
||||
"LabelAdminUsersOnly": "শুধু অ্যাডমিন ব্যবহারকারী",
|
||||
"LabelAll": "সব",
|
||||
"LabelAllEpisodesDownloaded": "সব পর্ব ডাউনলোড করা হয়েছে",
|
||||
"LabelAllUsers": "সমস্ত ব্যবহারকারী",
|
||||
"LabelAllUsersExcludingGuests": "অতিথি ব্যতীত সকল ব্যবহারকারী",
|
||||
"LabelAllUsersIncludingGuests": "অতিথি সহ সকল ব্যবহারকারী",
|
||||
"LabelAlreadyInYourLibrary": "ইতিমধ্যেই আপনার লাইব্রেরিতে রয়েছে",
|
||||
"LabelApiKeyCreated": "API কী \"{0}\" সফলভাবে তৈরি করা হয়েছে।",
|
||||
"LabelApiKeyCreatedDescription": "API কীটি এখনই কপি করে রাখুন, কারণ এটি পরে আর দেখতে পারবেন না।",
|
||||
"LabelApiKeyUser": "ব্যবহারকারীর পক্ষ থেকে কাজ করুন",
|
||||
"LabelApiKeyUserDescription": "এই API কীটি যে ব্যবহারকারীর পক্ষ থেকে কাজ করবে, সেই ব্যবহারকারীর মতোই একই অনুমতি পাবে। লগে এটি এমনভাবে দেখাবে যেন ব্যবহারকারী নিজেই অনুরোধটি করেছেন।",
|
||||
"LabelApiToken": "API টোকেন",
|
||||
"LabelAppend": "সংযোজন",
|
||||
"LabelAudioBitrate": "অডিও বিটরেট (যেমন- 128k)",
|
||||
@@ -268,7 +277,7 @@
|
||||
"LabelBonus": "উপরিলাভ",
|
||||
"LabelBooks": "বইগুলো",
|
||||
"LabelButtonText": "ঘর পাঠ্য",
|
||||
"LabelByAuthor": "দ্বারা {0}",
|
||||
"LabelByAuthor": "{0} অবশ্যই একদম অপরিবর্তিত রাখবে",
|
||||
"LabelChangePassword": "পাসওয়ার্ড পরিবর্তন করুন",
|
||||
"LabelChannels": "চ্যানেল",
|
||||
"LabelChapterCount": "{0} অধ্যায়",
|
||||
@@ -288,8 +297,10 @@
|
||||
"LabelContinueListening": "শোনা চালিয়ে যান",
|
||||
"LabelContinueReading": "পড়া চালিয়ে যান",
|
||||
"LabelContinueSeries": "সিরিজ চালিয়ে যান",
|
||||
"LabelCorsAllowed": "অনুমোদিত CORS অরিজিনসমূহ",
|
||||
"LabelCover": "কভার",
|
||||
"LabelCoverImageURL": "ছবির কভারের URL",
|
||||
"LabelCoverProvider": "কভার প্রদানকারী",
|
||||
"LabelCreatedAt": "তৈরি করা হয়েছে",
|
||||
"LabelCronExpression": "Cron এক্সপ্রেশন",
|
||||
"LabelCurrent": "বর্তমান",
|
||||
@@ -300,6 +311,7 @@
|
||||
"LabelDeleteFromFileSystemCheckbox": "ফাইল সিস্টেম থেকে মুছে ফেলুন (শুধু ডাটাবেস থেকে সরাতে টিক চিহ্ন মুক্ত করুন)",
|
||||
"LabelDescription": "বিবরণ",
|
||||
"LabelDeselectAll": "সমস্ত অনির্বাচিত করুন",
|
||||
"LabelDetectedPattern": "শনাক্ত করা প্যাটার্ন:",
|
||||
"LabelDevice": "ডিভাইস",
|
||||
"LabelDeviceInfo": "ডিভাইস তথ্য",
|
||||
"LabelDeviceIsAvailableTo": "ডিভাইস এর জন্য উপলব্ধ...",
|
||||
@@ -349,7 +361,11 @@
|
||||
"LabelExample": "উদাহরণ",
|
||||
"LabelExpandSeries": "সিরিজ প্রসারিত করুন",
|
||||
"LabelExpandSubSeries": "সাব সিরিজ প্রসারিত করুন",
|
||||
"LabelExplicit": "বিশদ",
|
||||
"LabelExpired": "মেয়াদ শেষ হয়েছে",
|
||||
"LabelExpiresAt": "মেয়াদ শেষ হওয়ার সময়",
|
||||
"LabelExpiresInSeconds": "মেয়াদ শেষ হবে (সেকেন্ডে)",
|
||||
"LabelExpiresNever": "কখনোই নয়",
|
||||
"LabelExplicit": "অশ্লীল ভাষা",
|
||||
"LabelExplicitChecked": "সুস্পষ্ট (পরীক্ষিত)",
|
||||
"LabelExplicitUnchecked": "অস্পষ্ট (অপরিক্ষীত)",
|
||||
"LabelExportOPML": "OPML এক্সপোর্ট করুন",
|
||||
@@ -364,11 +380,12 @@
|
||||
"LabelFilterByUser": "ব্যবহারকারী দ্বারা ফিল্টারকৃত",
|
||||
"LabelFindEpisodes": "পর্বগুলো খুঁজুন",
|
||||
"LabelFinished": "সমাপ্ত",
|
||||
"LabelFinishedDate": "{0} সম্পন্ন হয়েছে",
|
||||
"LabelFolder": "ফোল্ডার",
|
||||
"LabelFolders": "ফোল্ডারগুলো",
|
||||
"LabelFontBold": "বোল্ড",
|
||||
"LabelFontBoldness": "হরফ বোল্ডনেস",
|
||||
"LabelFontFamily": "হরফ পরিবার",
|
||||
"LabelFontFamily": "ফন্টের ধরন",
|
||||
"LabelFontItalic": "ইটালিক",
|
||||
"LabelFontScale": "ফন্ট স্কেল",
|
||||
"LabelFontStrikethrough": "অবচ্ছেদন রেখা",
|
||||
@@ -398,7 +415,9 @@
|
||||
"LabelIntervalEvery6Hours": "প্রতি ৬ ঘন্টায়",
|
||||
"LabelIntervalEveryDay": "প্রতিদিন",
|
||||
"LabelIntervalEveryHour": "প্রতি ঘন্টা",
|
||||
"LabelIntervalEveryMinute": "প্রতি মিনিটে",
|
||||
"LabelInvert": "উল্টানো",
|
||||
"LabelIpAddress": "IP ঠিকানা",
|
||||
"LabelItem": "আইটেম",
|
||||
"LabelJumpBackwardAmount": "পিছন দিকে ঝাঁপের পরিমাণ",
|
||||
"LabelJumpForwardAmount": "সামনের দিকে ঝাঁপের পরিমাণ",
|
||||
@@ -407,6 +426,7 @@
|
||||
"LabelLanguages": "ভাষাসমূহ",
|
||||
"LabelLastBookAdded": "শেষ বই যোগ করা হয়েছে",
|
||||
"LabelLastBookUpdated": "শেষ বই আপডেট করা হয়েছে",
|
||||
"LabelLastProgressDate": "সর্বশেষ অগ্রগতি: {0}",
|
||||
"LabelLastSeen": "শেষ দেখা",
|
||||
"LabelLastTime": "শেষ বার",
|
||||
"LabelLastUpdate": "শেষ আপডেট",
|
||||
@@ -419,6 +439,9 @@
|
||||
"LabelLibraryFilterSublistEmpty": "না {0}",
|
||||
"LabelLibraryItem": "লাইব্রেরি আইটেম",
|
||||
"LabelLibraryName": "লাইব্রেরির নাম",
|
||||
"LabelLibrarySortByProgress": "অগ্রগতি: সর্বশেষ আপডেট",
|
||||
"LabelLibrarySortByProgressFinished": "অগ্রগতি: সম্পন্ন",
|
||||
"LabelLibrarySortByProgressStarted": "অগ্রগতি: শুরু হয়েছে",
|
||||
"LabelLimit": "সীমা",
|
||||
"LabelLineSpacing": "লাইন স্পেসিং",
|
||||
"LabelListenAgain": "আবার শুনুন",
|
||||
@@ -427,6 +450,7 @@
|
||||
"LabelLogLevelWarn": "সতর্ক",
|
||||
"LabelLookForNewEpisodesAfterDate": "এই তারিখের পরে নতুন পর্বগুলি সন্ধান করুন",
|
||||
"LabelLowestPriority": "সর্বনিম্ন অগ্রাধিকার",
|
||||
"LabelMatchConfidence": "নিশ্চয়তার মাত্রা",
|
||||
"LabelMatchExistingUsersBy": "বিদ্যমান ব্যবহারকারীদের দ্বারা মিলিত করুন",
|
||||
"LabelMatchExistingUsersByDescription": "বিদ্যমান ব্যবহারকারীদের সংযোগ করার জন্য ব্যবহৃত হয়। একবার সংযুক্ত হলে, ব্যবহারকারীদের আপনার SSO প্রদানকারীর থেকে একটি অনন্য আইডি দ্বারা মিলিত হবে",
|
||||
"LabelMaxEpisodesToDownload": "সর্বাধিক # টি পর্ব ডাউনলোড করা হবে। অসীমের জন্য 0 ব্যবহার করুন।",
|
||||
@@ -441,7 +465,7 @@
|
||||
"LabelMetadataProvider": "মেটাডেটা প্রদানকারী",
|
||||
"LabelMinute": "মিনিট",
|
||||
"LabelMinutes": "মিনিটস",
|
||||
"LabelMissing": "নিখোঁজ",
|
||||
"LabelMissing": "অনুপস্থিত",
|
||||
"LabelMissingEbook": "কোনও ই-বই নেই",
|
||||
"LabelMissingSupplementaryEbook": "কোনও সম্পূরক ই-বই নেই",
|
||||
"LabelMobileRedirectURIs": "অনুমোদিত মোবাইল রিডাইরেক্ট URIs",
|
||||
@@ -456,7 +480,9 @@
|
||||
"LabelNewestAuthors": "নতুন লেখক",
|
||||
"LabelNewestEpisodes": "নতুনতম পর্ব",
|
||||
"LabelNextBackupDate": "পরবর্তী ব্যাকআপ তারিখ",
|
||||
"LabelNextChapters": "পরবর্তী অধ্যায়গুলো হবে:",
|
||||
"LabelNextScheduledRun": "পরবর্তী নির্ধারিত দৌড়",
|
||||
"LabelNoApiKeys": "কোনো API কী নেই",
|
||||
"LabelNoCustomMetadataProviders": "কোনো কাস্টম মেটাডেটা প্রদানকারী নেই",
|
||||
"LabelNoEpisodesSelected": "কোন পর্ব নির্বাচন করা হয়নি",
|
||||
"LabelNotFinished": "সমাপ্ত হয়নি",
|
||||
@@ -472,7 +498,8 @@
|
||||
"LabelNotificationsMaxQueueSize": "বিজ্ঞপ্তি ইভেন্টের জন্য সর্বোচ্চ সারির আকার",
|
||||
"LabelNotificationsMaxQueueSizeHelp": "ইভেন্টগুলি প্রতি সেকেন্ডে ১ বার ইন্ধন করার মধ্যে সীমাবদ্ধ। সারি সর্বাধিক আকারে থাকলে ইভেন্টগুলি উপেক্ষা করা হবে। এটি বিজ্ঞপ্তি স্প্যামিং প্রতিরোধ করে।",
|
||||
"LabelNumberOfBooks": "বইয়ের সংখ্যা",
|
||||
"LabelNumberOfEpisodes": "# টি পর্ব",
|
||||
"LabelNumberOfChapters": "অধ্যায়ের সংখ্যা:",
|
||||
"LabelNumberOfEpisodes": "পর্বের সংখ্যা",
|
||||
"LabelOpenIDAdvancedPermsClaimDescription": "ওপেনআইডি দাবির নাম যাতে অ্যাপ্লিকেশনের মধ্যে ব্যবহারকারীর ক্রিয়াকলাপের জন্য উন্নত অনুমতি রয়েছে যা অ-প্রশাসক ভূমিকাগুলিতে প্রযোজ্য হবে (<b>যদি কনফিগার করা হয়</b>)। প্রতিক্রিয়া থেকে দাবিটি অনুপস্থিত থাকলে, অ্যাক্সেস করুন ABS-তে অস্বীকার করা হবে। যদি একটি একক বিকল্প অনুপস্থিত থাকে, তাহলে এটিকে <code>false</code> হিসাবে গণ্য করা হবে। নিশ্চিত করুন যে পরিচয় প্রদানকারীর দাবি প্রত্যাশিত কাঠামোর সাথে মেলে:",
|
||||
"LabelOpenIDClaims": "অ্যাডভান্সড গ্রুপ এবং পারমিশন অ্যাসাইনমেন্ট নিষ্ক্রিয় করতে নিম্নলিখিত বিকল্পগুলিকে খালি ছেড়ে দিন, তারপর স্বয়ংক্রিয়ভাবে 'ব্যবহারকারী' গ্রুপকে বরাদ্দ করা হবে।",
|
||||
"LabelOpenIDGroupClaimDescription": "ওপেনআইডি দাবির নাম যাতে ব্যবহারকারীর গোষ্ঠীর একটি তালিকা থাকে। সাধারণত <code>গ্রুপ</code> হিসাবে উল্লেখ করা হয়। <b>কনফিগার করা থাকলে</b>, অ্যাপ্লিকেশনটি স্বয়ংক্রিয়ভাবে এর উপর ভিত্তি করে ব্যবহারকারীর গোষ্ঠীর সদস্যপদ নির্ধারণ করবে, শর্ত এই যে এই গোষ্ঠীগুলি কেস-অসংবেদনশীলভাবে দাবিতে 'অ্যাডমিন', 'ব্যবহারকারী' বা 'অতিথি' নাম দেওয়া হয়৷ দাবিতে একটি তালিকা থাকা উচিত এবং যদি একজন ব্যবহারকারী একাধিক গোষ্ঠীর অন্তর্গত হয় তবে অ্যাপ্লিকেশনটি বরাদ্দ করবে সর্বোচ্চ স্তরের অ্যাক্সেসের সাথে সঙ্গতিপূর্ণ ভূমিকা৷ যদি কোনও গোষ্ঠীর সাথে মেলে না, তবে অ্যাক্সেস অস্বীকার করা হবে।",
|
||||
@@ -493,6 +520,7 @@
|
||||
"LabelPersonalYearReview": "আপনার বছরের পর্যালোচনা ({0})",
|
||||
"LabelPhotoPathURL": "ছবি পথ/ইউআরএল",
|
||||
"LabelPlayMethod": "প্লে পদ্ধতি",
|
||||
"LabelPlaybackRateIncrementDecrement": "প্লেব্যাকের গতি বাড়ানো/কমানোর পরিমাণ",
|
||||
"LabelPlayerChapterNumberMarker": "{1} এর মধ্যে {0}",
|
||||
"LabelPlaylists": "প্লেলিস্ট",
|
||||
"LabelPodcast": "পডকাস্ট",
|
||||
@@ -515,7 +543,7 @@
|
||||
"LabelPublishers": "প্রকাশকরা",
|
||||
"LabelRSSFeedCustomOwnerEmail": "কাস্টম মালিকের ইমেইল",
|
||||
"LabelRSSFeedCustomOwnerName": "কাস্টম মালিকের নাম",
|
||||
"LabelRSSFeedOpen": "আরএসএস ফিড খুলুন",
|
||||
"LabelRSSFeedOpen": "RSS ফিড খোলা আছে",
|
||||
"LabelRSSFeedPreventIndexing": "সূচীকরণ প্রতিরোধ করুন",
|
||||
"LabelRSSFeedSlug": "আরএসএস ফিড স্লাগ",
|
||||
"LabelRSSFeedURL": "আরএসএস ফিড ইউআরএল",
|
||||
@@ -532,6 +560,7 @@
|
||||
"LabelReleaseDate": "উন্মোচনের তারিখ",
|
||||
"LabelRemoveAllMetadataAbs": "সমস্ত metadata.abs ফাইল সরান",
|
||||
"LabelRemoveAllMetadataJson": "সমস্ত metadata.json ফাইল সরান",
|
||||
"LabelRemoveAudibleBranding": "অধ্যায়গুলো থেকে Audible-এর ইন্ট্রো ও আউট্রো সরান",
|
||||
"LabelRemoveCover": "কভার সরান",
|
||||
"LabelRemoveMetadataFile": "লাইব্রেরি আইটেম ফোল্ডারে মেটাডেটা ফাইল সরান",
|
||||
"LabelRemoveMetadataFileHelp": "আপনার {0} ফোল্ডারের সমস্ত metadata.json এবং metadata.abs ফাইলগুলি সরান।",
|
||||
@@ -544,6 +573,7 @@
|
||||
"LabelSelectAll": "সব নির্বাচন করুন",
|
||||
"LabelSelectAllEpisodes": "সমস্ত পর্ব নির্বাচন করুন",
|
||||
"LabelSelectEpisodesShowing": "দেখানো {0}টি পর্ব নির্বাচন করুন",
|
||||
"LabelSelectUser": "ব্যবহারকারী নির্বাচন করুন",
|
||||
"LabelSelectUsers": "ব্যবহারকারী নির্বাচন করুন",
|
||||
"LabelSendEbookToDevice": "ই-বই পাঠান...",
|
||||
"LabelSequence": "ক্রম",
|
||||
@@ -561,6 +591,8 @@
|
||||
"LabelSettingsBookshelfViewHelp": "কাঠের তাক সহ স্কুমরফিক ডিজাইন",
|
||||
"LabelSettingsChromecastSupport": "ক্রোমকাস্ট সমর্থন",
|
||||
"LabelSettingsDateFormat": "তারিখ বিন্যাস",
|
||||
"LabelSettingsEnableWatcher": "পরিবর্তনের জন্য লাইব্রেরিগুলো স্বয়ংক্রিয়ভাবে পর্যবেক্ষণ করুন",
|
||||
"LabelSettingsEnableWatcherForLibrary": "লাইব্রেরির জন্য ফোল্ডার প্রহরী সক্ষম করুন",
|
||||
"LabelSettingsEnableWatcherHelp": "ফাইলের পরিবর্তন শনাক্ত হলে আইটেমগুলির স্বয়ংক্রিয় যোগ/আপডেট সক্ষম করবে। *সার্ভার পুনরায় চালু করতে হবে",
|
||||
"LabelSettingsEpubsAllowScriptedContent": "ইপাবে স্ক্রিপ্ট করা বিষয়বস্তুর অনুমতি দিন",
|
||||
"LabelSettingsEpubsAllowScriptedContentHelp": "ইপাব ফাইলগুলিকে স্ক্রিপ্ট চালানোর অনুমতি দিন। আপনি ইপাব ফাইলগুলির উৎসকে বিশ্বাস না করলে এই সেটিংটি নিষ্ক্রিয় রাখার সুপারিশ করা হলো।",
|
||||
@@ -578,7 +610,7 @@
|
||||
"LabelSettingsOnlyShowLaterBooksInContinueSeries": "কন্টিনিউ সিরিজে আগের বইগুলো এড়িয়ে যান",
|
||||
"LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "কন্টিনিউ সিরিজের নীড় পেজ শেল্ফ দেখায় যে সিরিজে শুরু হয়নি এমন প্রথম বই যার অন্তত একটি বই শেষ হয়েছে এবং কোনো বই চলছে না। এই সেটিংটি সক্ষম করলে শুরু না হওয়া প্রথম বইটির পরিবর্তে সবচেয়ে দূরের সম্পূর্ণ বই থেকে সিরিজ চলতে থাকবে।",
|
||||
"LabelSettingsParseSubtitles": "সাবটাইটেল পার্স করুন",
|
||||
"LabelSettingsParseSubtitlesHelp": "অডিওবুক ফোল্ডারের নাম থেকে সাবটাইটেল বের করুন৷<br>সাবটাইটেল অবশ্যই \" - \"<br>অর্থাৎ \"বুকের শিরোনাম - এখানে একটি সাবটাইটেল\" এর সাবটাইটেল আছে \"এখানে একটি সাবটাইটেল\"",
|
||||
"LabelSettingsParseSubtitlesHelp": "অডিওবুক ফোল্ডারের নাম থেকে সাবটাইটেল শনাক্ত করুন।<br>সাবটাইটেল অবশ্যই \" - \" দিয়ে আলাদা করা থাকতে হবে।<br>যেমন, \"বইয়ের নাম - একটি সাবটাইটেল\" হলে সাবটাইটেল হবে \"একটি সাবটাইটেল\"",
|
||||
"LabelSettingsPreferMatchedMetadata": "মিলিত মেটাডেটা পছন্দ করুন",
|
||||
"LabelSettingsPreferMatchedMetadataHelp": "দ্রুত ম্যাচ ব্যবহার করার সময় মিলে যাওয়া ডেটা আইটেমের বিবরণকে ওভাররাইড করবে। ডিফল্টরূপে দ্রুত ম্যাচ শুধুমাত্র অনুপস্থিত বিশদগুলি পূরণ করবে।",
|
||||
"LabelSettingsSkipMatchingBooksWithASIN": "এমন বইগুলি এড়িয়ে যান যেগুলির মধ্যে ইতিমধ্যে একটি ASIN আছে",
|
||||
@@ -604,10 +636,12 @@
|
||||
"LabelSlug": "স্লাগ",
|
||||
"LabelSortAscending": "আরোহী",
|
||||
"LabelSortDescending": "অবরোহী",
|
||||
"LabelSortPubDate": "প্রকাশের তারিখ অনুযায়ী সাজান",
|
||||
"LabelStart": "শুরু",
|
||||
"LabelStartTime": "শুরুর সময়",
|
||||
"LabelStarted": "শুরু হয়েছে",
|
||||
"LabelStartedAt": "এতে শুরু হয়েছে",
|
||||
"LabelStartedDate": "{0} শুরু হয়েছে",
|
||||
"LabelStatsAudioTracks": "অডিও ট্র্যাক",
|
||||
"LabelStatsAuthors": "লেখক",
|
||||
"LabelStatsBestDay": "সেরা দিন",
|
||||
@@ -637,6 +671,7 @@
|
||||
"LabelTheme": "থিম",
|
||||
"LabelThemeDark": "অন্ধকার",
|
||||
"LabelThemeLight": "আলো",
|
||||
"LabelThemeSepia": "সেপিয়া",
|
||||
"LabelTimeBase": "সময় বেস",
|
||||
"LabelTimeDurationXHours": "{0} ঘণ্টা",
|
||||
"LabelTimeDurationXMinutes": "{0} মিনিট",
|
||||
@@ -704,23 +739,33 @@
|
||||
"LabelYourProgress": "আপনার অগ্রগতি",
|
||||
"MessageAddToPlayerQueue": "প্লেয়ার সারিতে যোগ করুন",
|
||||
"MessageAppriseDescription": "এই বৈশিষ্ট্যটি ব্যবহার করার জন্য আপনাকে <a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">Apprise API</a> চালানোর একটি উদাহরণ বা একটি এপিআই পরিচালনা করতে হবে যে একই অনুরোধ পরিচালনা করবে। <br />অ্যাপ্রাইজ এপিআই ইউআরএলটি বিজ্ঞপ্তি পাঠানোর জন্য সম্পূর্ণ ইউআরএল পথ হওয়া উচিত, যেমন, যদি আপনার API ইনস্ট্যান্স <code>http://192.168.1.1:8337</code> এ পরিবেশিত হয় তাহলে আপনি <code> রাখবেন >http://192.168.1.1:8337/notify</code>।",
|
||||
"MessageAsinCheck": "নিশ্চিত করুন যে আপনি Amazon-এর নয়, সঠিক Audible অঞ্চলের ASIN ব্যবহার করছেন।",
|
||||
"MessageAuthenticationLegacyTokenWarning": "পুরোনো API টোকেনগুলো ভবিষ্যতে সরিয়ে দেওয়া হবে। এর পরিবর্তে <a href=\"/config/api-keys\">API কী</a> ব্যবহার করুন।",
|
||||
"MessageAuthenticationOIDCChangesRestart": "OIDC-এর পরিবর্তনগুলো কার্যকর করতে সেভ করার পর সার্ভারটি পুনরায় চালু করুন।",
|
||||
"MessageAuthenticationSecurityMessage": "নিরাপত্তার জন্য অথেনটিকেশন ব্যবস্থা উন্নত করা হয়েছে। সকল ব্যবহারকারীকে আবার লগইন করতে হবে।",
|
||||
"MessageBackupsDescription": "ব্যাকআপের মধ্যে রয়েছে ব্যবহারকারী, ব্যবহারকারীর অগ্রগতি, লাইব্রেরি আইটেমের বিবরণ, সার্ভার সেটিংস এবং <code>/metadata/items</code> & <code>/metadata/authors</code>-এ সংরক্ষিত ছবি। ব্যাকআপগুলি <strong> আপনার লাইব্রেরি ফোল্ডারে সঞ্চিত কোনো ফাইল >অন্তর্ভুক্ত করবেন না</strong>।",
|
||||
"MessageBackupsLocationEditNote": "দ্রষ্টব্য: ব্যাকআপ অবস্থান আপডেট করলে বিদ্যমান ব্যাকআপগুলি সরানো বা সংশোধন করা হবে না",
|
||||
"MessageBackupsLocationNoEditNote": "দ্রষ্টব্য: ব্যাকআপ অবস্থান একটি পরিবেশ পরিবর্তনশীল মাধ্যমে স্থির করা হয়েছে এবং এখানে পরিবর্তন করা যাবে না।",
|
||||
"MessageBackupsLocationPathEmpty": "ব্যাকআপ অবস্থানের পথ খালি থাকতে পারবে না",
|
||||
"MessageBatchEditPopulateMapDetailsAllHelp": "সক্রিয় ফিল্ডগুলো সব আইটেমের তথ্য দিয়ে পূরণ করুন। একাধিক মান থাকা ফিল্ডগুলোর মান একত্রিত করা হবে",
|
||||
"MessageBatchEditPopulateMapDetailsItemHelp": "সক্রিয় ম্যাপের বিস্তারিত ফিল্ডগুলো এই আইটেমের তথ্য দিয়ে পূরণ করুন",
|
||||
"MessageBatchQuickMatchDescription": "কুইক ম্যাচ নির্বাচিত আইটেমগুলির জন্য অনুপস্থিত কভার এবং মেটাডেটা যোগ করার চেষ্টা করবে। বিদ্যমান কভার এবং/অথবা মেটাডেটা ওভাররাইট করার জন্য দ্রুত ম্যাচকে অনুমতি দিতে নীচের বিকল্পগুলি সক্ষম করুন।",
|
||||
"MessageBookshelfNoCollections": "আপনি এখনও কোনো সংগ্রহ করেননি",
|
||||
"MessageBookshelfNoCollectionsHelp": "কালেকশনগুলো সবার জন্য উন্মুক্ত। লাইব্রেরিতে অ্যাক্সেস থাকা সকল ব্যবহারকারী এগুলো দেখতে পারবেন।",
|
||||
"MessageBookshelfNoRSSFeeds": "কোনও RSS ফিড খোলা নেই",
|
||||
"MessageBookshelfNoResultsForFilter": "ফিল্টার \"{0}: {1}\" এর জন্য কোন ফলাফল নেই",
|
||||
"MessageBookshelfNoResultsForQuery": "প্রশ্নের জন্য কোন ফলাফল নেই",
|
||||
"MessageBookshelfNoSeries": "আপনার কোনো সিরিজ নেই",
|
||||
"MessageBulkChapterPattern": "এই নম্বর দেওয়ার ধরন অনুযায়ী আপনি কতটি অধ্যায় যোগ করতে চান?",
|
||||
"MessageChapterEndIsAfter": "অধ্যায়ের সমাপ্তি আপনার অডিওবুকের শেষে",
|
||||
"MessageChapterErrorFirstNotZero": "প্রথম অধ্যায় 0 এ শুরু হতে হবে",
|
||||
"MessageChapterErrorStartGteDuration": "অবৈধ শুরুর সময় অবশ্যই অডিওবুকের সময়কালের কম হতে হবে",
|
||||
"MessageChapterErrorStartLtPrev": "অবৈধ শুরুর সময় অবশ্যই আগের অধ্যায় শুরুর সময়ের চেয়ে বেশি বা সমান হতে হবে",
|
||||
"MessageChapterStartIsAfter": "আপনার অডিওবুক শেষ হওয়ার পরে অধ্যায় শুরু হয়",
|
||||
"MessageChaptersNotFound": "কোনো অধ্যায় পাওয়া যায়নি",
|
||||
"MessageCheckingCron": "ক্রন পরীক্ষা করা হচ্ছে...",
|
||||
"MessageConfirmCloseFeed": "আপনি কি নিশ্চিত যে আপনি এই ফিডটি বন্ধ করতে চান?",
|
||||
"MessageConfirmDeleteApiKey": "আপনি কি নিশ্চিত যে API কী \"{0}\" মুছে ফেলতে চান?",
|
||||
"MessageConfirmDeleteBackup": "আপনি কি নিশ্চিত যে আপনি {0} এর ব্যাকআপ মুছে ফেলতে চান?",
|
||||
"MessageConfirmDeleteDevice": "আপনি কি নিশ্চিতভাবে ই-রিডার ডিভাইস \"{0}\" মুছতে চান?",
|
||||
"MessageConfirmDeleteFile": "এটি আপনার ফাইল সিস্টেম থেকে ফাইলটি মুছে দেবে। আপনি কি নিশ্চিত?",
|
||||
@@ -732,6 +777,7 @@
|
||||
"MessageConfirmDeleteSession": "আপনি কি নিশ্চিত আপনি এই অধিবেশন মুছে দিতে চান?",
|
||||
"MessageConfirmEmbedMetadataInAudioFiles": "আপনি কি {0}টি অডিও ফাইলে মেটাডেটা এম্বেড করার বিষয়ে নিশ্চিত?",
|
||||
"MessageConfirmForceReScan": "আপনি কি নিশ্চিত যে আপনি জোর করে পুনরায় স্ক্যান করতে চান?",
|
||||
"MessageConfirmLogoutDevice": "আপনি কি নিশ্চিত যে \"{0}\" থেকে লগআউট করতে চান?",
|
||||
"MessageConfirmMarkAllEpisodesFinished": "আপনি কি নিশ্চিত যে আপনি সমস্ত পর্ব সমাপ্ত হিসাবে চিহ্নিত করতে চান?",
|
||||
"MessageConfirmMarkAllEpisodesNotFinished": "আপনি কি নিশ্চিত যে আপনি সমস্ত পর্বকে শেষ হয়নি বলে চিহ্নিত করতে চান?",
|
||||
"MessageConfirmMarkItemFinished": "আপনি কি \"{0}\" কে সমাপ্ত হিসাবে চিহ্নিত করার বিষয়ে নিশ্চিত?",
|
||||
@@ -748,6 +794,7 @@
|
||||
"MessageConfirmRemoveAuthor": "আপনি কি নিশ্চিত যে আপনি লেখক \"{0}\" অপসারণ করতে চান?",
|
||||
"MessageConfirmRemoveCollection": "আপনি কি নিশ্চিত যে আপনি সংগ্রহ \"{0}\" সরাতে চান?",
|
||||
"MessageConfirmRemoveEpisode": "আপনি কি নিশ্চিত আপনি \"{0}\" পর্বটি সরাতে চান?",
|
||||
"MessageConfirmRemoveEpisodeNote": "দ্রষ্টব্য: \"Hard delete file\" চালু না করলে অডিও ফাইলটি মুছে ফেলা হবে না",
|
||||
"MessageConfirmRemoveEpisodes": "আপনি কি নিশ্চিত যে আপনি {0}টি পর্ব সরাতে চান?",
|
||||
"MessageConfirmRemoveListeningSessions": "আপনি কি নিশ্চিত যে আপনি {0}টি শোনার সেশন সরাতে চান?",
|
||||
"MessageConfirmRemoveMetadataFiles": "আপনি কি আপনার লাইব্রেরি আইটেম ফোল্ডারে থাকা সমস্ত মেটাডেটা {0} ফাইল মুছে ফেলার বিষয়ে নিশ্চিত?",
|
||||
@@ -762,6 +809,7 @@
|
||||
"MessageConfirmResetProgress": "আপনি কি আপনার অগ্রগতি রিসেট করার বিষয়ে নিশ্চিত?",
|
||||
"MessageConfirmSendEbookToDevice": "আপনি কি নিশ্চিত যে আপনি \"{2}\" ডিভাইসে {0} ইবুক \"{1}\" পাঠাতে চান?",
|
||||
"MessageConfirmUnlinkOpenId": "আপনি কি এই ব্যবহারকারীকে ওপেনআইডি থেকে লিঙ্কমুক্ত করার বিষয়ে নিশ্চিত?",
|
||||
"MessageDaysListenedInTheLastYear": "গত এক বছরে {0} দিন শোনা হয়েছে",
|
||||
"MessageDownloadingEpisode": "ডাউনলোডিং পর্ব",
|
||||
"MessageDragFilesIntoTrackOrder": "সঠিক ট্র্যাক অর্ডারে ফাইল টেনে আনুন",
|
||||
"MessageEmbedFailed": "এম্বেড ব্যর্থ হয়েছে!",
|
||||
@@ -772,8 +820,11 @@
|
||||
"MessageFeedURLWillBe": "ফিড URL হবে {0}",
|
||||
"MessageFetching": "আনয় হচ্ছে.।",
|
||||
"MessageForceReScanDescription": "সকল ফাইল আবার নতুন স্ক্যানের মত স্ক্যান করবে। অডিও ফাইল ID3 ট্যাগ, OPF ফাইল, এবং টেক্সট ফাইলগুলি নতুন হিসাবে স্ক্যান করা হবে।",
|
||||
"MessageHeatmapListeningTimeTooltip": "{1}-এ <strong>{0} জন শুনছেন</strong>",
|
||||
"MessageHeatmapNoListeningSessions": "{0}-এ কোনো শোনার সেশন নেই",
|
||||
"MessageImportantNotice": "গুরুত্বপূর্ণ বিজ্ঞপ্তি!",
|
||||
"MessageInsertChapterBelow": "নীচে অধ্যায় ঢোকান",
|
||||
"MessageInvalidAsin": "অবৈধ ASIN",
|
||||
"MessageItemsSelected": "{0}টি আইটেম নির্বাচিত",
|
||||
"MessageItemsUpdated": "{0}টি আইটেম আপডেট করা হয়েছে",
|
||||
"MessageJoinUsOn": "আমাদের সাথে যোগ দিন",
|
||||
@@ -819,6 +870,7 @@
|
||||
"MessageNoTasksRunning": "কোন টাস্ক চলছে না",
|
||||
"MessageNoUpdatesWereNecessary": "কোন আপডেটের প্রয়োজন ছিল না",
|
||||
"MessageNoUserPlaylists": "আপনার কোনো প্লেলিস্ট নেই",
|
||||
"MessageNoUserPlaylistsHelp": "প্লেলিস্টগুলো ব্যক্তিগত। শুধুমাত্র যে ব্যবহারকারী এগুলো তৈরি করেছেন তিনিই সেগুলো দেখতে পারবেন।",
|
||||
"MessageNotYetImplemented": "এখনও বাস্তবায়িত হয়নি",
|
||||
"MessageOpmlPreviewNote": "দ্রষ্টব্য: এটি পার্স করা OPML ফাইলের একটি পূর্বরূপ। প্রকৃত পডকাস্ট শিরোনাম RSS ফিড থেকে নেওয়া হবে।",
|
||||
"MessageOr": "বা",
|
||||
@@ -840,8 +892,11 @@
|
||||
"MessageResetChaptersConfirm": "আপনি কি নিশ্চিত যে আপনি অধ্যায়গুলি পুনরায় সেট করতে চান এবং আপনার করা পরিবর্তনগুলি পূর্বাবস্থায় ফেরাতে চান?",
|
||||
"MessageRestoreBackupConfirm": "আপনি কি নিশ্চিত যে আপনি তৈরি করা ব্যাকআপ পুনরুদ্ধার করতে চান",
|
||||
"MessageRestoreBackupWarning": "একটি ব্যাকআপ পুনরুদ্ধার করা হলে তা /config-এ অবস্থিত সমগ্র ডাটাবেস ওভাররাইট করবে এবং /metadata/items & /metadata/authors-এ থাকা ছবিগুলিকে কভার করবে৷<br /><br />ব্যাকআপগুলি আপনার লাইব্রেরি ফোল্ডারে কোনো ফাইল পরিবর্তন করে না৷ আপনি যদি আপনার লাইব্রেরি ফোল্ডারে কভার আর্ট এবং মেটাডেটা সংরক্ষণ করতে সার্ভার সেটিংস সক্ষম করে থাকেন তবে সেগুলি ব্যাক আপ বা ওভাররাইট করা হয় না৷<br /><br />আপনার সার্ভার ব্যবহারকারী সমস্ত ক্লায়েন্ট স্বয়ংক্রিয়ভাবে রিফ্রেশ হবে৷",
|
||||
"MessageScheduleLibraryScanNote": "বেশিরভাগ ব্যবহারকারীর জন্য এই ফিচারটি বন্ধ রেখে \"Automatically watch library for changes\" সেটিংটি চালু রাখার পরামর্শ দেওয়া হয়—এটি আপনার লাইব্রেরি ফোল্ডারের পরিবর্তনগুলো স্বয়ংক্রিয়ভাবে শনাক্ত করবে। আপনার ফাইল সিস্টেমে (যেমন NFS) \"Automatically watch library for changes\" কাজ না করলে এই ফিচারটি চালু করুন।",
|
||||
"MessageScheduleRunEveryWeekdayAtTime": "প্রতি {0} অন্তর {1}-এ চালান",
|
||||
"MessageSearchResultsFor": "এর জন্য অনুসন্ধান ফলাফল",
|
||||
"MessageSelected": "{0}টি নির্বাচিত",
|
||||
"MessageSeriesSequenceCannotContainSpaces": "সিরিজের ক্রমে স্পেস থাকতে পারবে না",
|
||||
"MessageServerCouldNotBeReached": "সার্ভারে পৌঁছানো যায়নি",
|
||||
"MessageSetChaptersFromTracksDescription": "প্রতিটি অডিও ফাইলকে অধ্যায় হিসেবে ব্যবহার করে অধ্যায় সেট করুন এবং অডিও ফাইলের নাম হিসেবে অধ্যায়ের শিরোনাম করুন",
|
||||
"MessageShareExpirationWillBe": "মেয়াদ শেষ হবে <strong>{0}</strong>",
|
||||
@@ -903,7 +958,10 @@
|
||||
"NotificationOnBackupCompletedDescription": "ব্যাকআপ সম্পূর্ণ হলে ট্রিগার হবে",
|
||||
"NotificationOnBackupFailedDescription": "ব্যাকআপ ব্যর্থ হলে ট্রিগার হবে",
|
||||
"NotificationOnEpisodeDownloadedDescription": "একটি পডকাস্ট পর্ব স্বয়ংক্রিয়ভাবে ডাউনলোড হলে ট্রিগার হবে",
|
||||
"NotificationOnRSSFeedDisabledDescription": "অনেকবার ডাউনলোডের চেষ্টা ব্যর্থ হওয়ার কারণে স্বয়ংক্রিয় পর্ব ডাউনলোড বন্ধ হয়ে গেলে এটি ট্রিগার হয়",
|
||||
"NotificationOnRSSFeedFailedDescription": "স্বয়ংক্রিয় পর্ব ডাউনলোডের জন্য RSS ফিডের অনুরোধ ব্যর্থ হলে এটি ট্রিগার হয়",
|
||||
"NotificationOnTestDescription": "বিজ্ঞপ্তি সিস্টেম পরীক্ষার জন্য ইভেন্ট",
|
||||
"PlaceholderBulkChapterInput": "অধ্যায়ের শিরোনাম লিখুন অথবা নম্বর ব্যবহার করুন (যেমন, 'পর্ব 1', 'অধ্যায় 10', '1.')",
|
||||
"PlaceholderNewCollection": "নতুন সংগ্রহের নাম",
|
||||
"PlaceholderNewFolderPath": "নতুন ফোল্ডার পথ",
|
||||
"PlaceholderNewPlaylist": "নতুন প্লেলিস্টের নাম",
|
||||
@@ -947,6 +1005,7 @@
|
||||
"ToastBackupRestoreFailed": "ব্যাকআপ পুনরুদ্ধার করতে ব্যর্থ",
|
||||
"ToastBackupUploadFailed": "ব্যাকআপ আপলোড করতে ব্যর্থ",
|
||||
"ToastBackupUploadSuccess": "ব্যাকআপ আপলোড হয়েছে",
|
||||
"ToastBatchApplyDetailsToItemsSuccess": "আইটেমগুলোতে বিস্তারিত তথ্য প্রয়োগ করা হয়েছে",
|
||||
"ToastBatchDeleteFailed": "ব্যাচ মুছে ফেলতে ব্যর্থ হয়েছে",
|
||||
"ToastBatchDeleteSuccess": "ব্যাচ মুছে ফেলা সফল হয়েছে",
|
||||
"ToastBatchQuickMatchFailed": "ব্যাচ কুইক ম্যাচ ব্যর্থ!",
|
||||
@@ -956,16 +1015,25 @@
|
||||
"ToastBookmarkCreateFailed": "বুকমার্ক তৈরি করতে ব্যর্থ",
|
||||
"ToastBookmarkCreateSuccess": "বুকমার্ক যোগ করা হয়েছে",
|
||||
"ToastBookmarkRemoveSuccess": "বুকমার্ক সরানো হয়েছে",
|
||||
"ToastBulkChapterInvalidCount": "১ থেকে ১৫০-এর মধ্যে একটি সংখ্যা লিখুন",
|
||||
"ToastCachePurgeFailed": "ক্যাশে পরিষ্কার করতে ব্যর্থ হয়েছে",
|
||||
"ToastCachePurgeSuccess": "ক্যাশে সফলভাবে পরিষ্কার করা হয়েছে",
|
||||
"ToastChapterLocked": "অধ্যায়টি লক করা আছে।",
|
||||
"ToastChapterStartTimeAdjusted": "অধ্যায়ের শুরুর সময় {0} সেকেন্ড সমন্বয় করা হয়েছে",
|
||||
"ToastChaptersAllLocked": "সব অধ্যায় লক করা আছে। সময় পরিবর্তন করতে কিছু অধ্যায় আনলক করুন।",
|
||||
"ToastChaptersHaveErrors": "অধ্যায়ে ত্রুটি আছে",
|
||||
"ToastChaptersInvalidShiftAmountLast": "সময় সরানোর পরিমাণ সঠিক নয়। এতে শেষ অধ্যায়ের শুরুর সময় এই অডিওবুকের মোট সময়সীমা ছাড়িয়ে যাবে।",
|
||||
"ToastChaptersInvalidShiftAmountStart": "সময় সরানোর পরিমাণ সঠিক নয়। প্রথম অধ্যায়ের দৈর্ঘ্য শূন্য বা ঋণাত্মক হয়ে যাবে এবং দ্বিতীয় অধ্যায়টি এটিকে ওভাররাইট করবে। দ্বিতীয় অধ্যায়ের শুরুর সময় বাড়ান।",
|
||||
"ToastChaptersMustHaveTitles": "অধ্যায়ের শিরোনাম থাকতে হবে",
|
||||
"ToastChaptersRemoved": "অধ্যায়গুলো মুছে ফেলা হয়েছে",
|
||||
"ToastChaptersUpdated": "অধ্যায় আপডেট করা হয়েছে",
|
||||
"ToastCollectionItemsAddFailed": "আইটেম(গুলি) সংগ্রহে যোগ করা ব্যর্থ হয়েছে",
|
||||
"ToastCollectionRemoveSuccess": "সংগ্রহ সরানো হয়েছে",
|
||||
"ToastCollectionUpdateSuccess": "সংগ্রহ আপডেট করা হয়েছে",
|
||||
"ToastConnectionNotAvailable": "সংযোগ পাওয়া যাচ্ছে না। অনুগ্রহ করে পরে আবার চেষ্টা করুন",
|
||||
"ToastCoverSearchFailed": "কভার অনুসন্ধান ব্যর্থ হয়েছে",
|
||||
"ToastCoverUpdateFailed": "কভার আপডেট ব্যর্থ হয়েছে",
|
||||
"ToastDateTimeInvalidOrIncomplete": "তারিখ ও সময় সঠিক নয় অথবা অসম্পূর্ণ",
|
||||
"ToastDeleteFileFailed": "ফাইল মুছে ফেলতে ব্যর্থ হয়েছে",
|
||||
"ToastDeleteFileSuccess": "ফাইল মুছে ফেলা হয়েছে",
|
||||
"ToastDeviceAddFailed": "ডিভাইস যোগ করতে ব্যর্থ হয়েছে",
|
||||
@@ -979,6 +1047,8 @@
|
||||
"ToastEpisodeDownloadQueueClearSuccess": "পর্ব ডাউনলোড সারি পরিষ্কার করা হয়েছে",
|
||||
"ToastEpisodeUpdateSuccess": "{0}টি পর্ব আপডেট করা হয়েছে",
|
||||
"ToastErrorCannotShare": "এই ডিভাইসে স্থানীয়ভাবে শেয়ার করা যাবে না",
|
||||
"ToastFailedToCreate": "তৈরি করা যায়নি",
|
||||
"ToastFailedToDelete": "মুছে ফেলা যায়নি",
|
||||
"ToastFailedToLoadData": "ডেটা লোড করা যায়নি",
|
||||
"ToastFailedToMatch": "মেলাতে ব্যর্থ হয়েছে",
|
||||
"ToastFailedToShare": "শেয়ার করতে ব্যর্থ",
|
||||
@@ -986,6 +1056,7 @@
|
||||
"ToastInvalidImageUrl": "অকার্যকর ছবির ইউআরএল",
|
||||
"ToastInvalidMaxEpisodesToDownload": "ডাউনলোড করার জন্য অবৈধ সর্বোচ্চ পর্ব",
|
||||
"ToastInvalidUrl": "অকার্যকর ইউআরএল",
|
||||
"ToastInvalidUrls": "এক বা একাধিক URL সঠিক নয়",
|
||||
"ToastItemCoverUpdateSuccess": "আইটেম কভার আপডেট করা হয়েছে",
|
||||
"ToastItemDeletedFailed": "আইটেম মুছে ফেলতে ব্যর্থ",
|
||||
"ToastItemDeletedSuccess": "মুছে ফেলা আইটেম",
|
||||
@@ -1010,6 +1081,7 @@
|
||||
"ToastMustHaveAtLeastOnePath": "অন্তত একটি পথ থাকতে হবে",
|
||||
"ToastNameEmailRequired": "নাম এবং ইমেইল আবশ্যক",
|
||||
"ToastNameRequired": "নাম আবশ্যক",
|
||||
"ToastNewApiKeyUserError": "একজন ব্যবহারকারী নির্বাচন করতে হবে",
|
||||
"ToastNewEpisodesFound": "{0}টি নতুন পর্ব পাওয়া গেছে",
|
||||
"ToastNewUserCreatedFailed": "অ্যাকাউন্ট তৈরি করতে ব্যর্থ: \"{0}\"",
|
||||
"ToastNewUserCreatedSuccess": "নতুন একাউন্ট তৈরি হয়েছে",
|
||||
@@ -1018,6 +1090,7 @@
|
||||
"ToastNewUserTagError": "অন্তত একটি ট্যাগ নির্বাচন করতে হবে",
|
||||
"ToastNewUserUsernameError": "একটি ব্যবহারকারীর নাম লিখুন",
|
||||
"ToastNoNewEpisodesFound": "কোন নতুন পর্ব পাওয়া যায়নি",
|
||||
"ToastNoRSSFeed": "ডকাস্টটির কোনো RSS ফিড নেই",
|
||||
"ToastNoUpdatesNecessary": "কোন আপডেটের প্রয়োজন নেই",
|
||||
"ToastNotificationCreateFailed": "বিজ্ঞপ্তি তৈরি করতে ব্যর্থ",
|
||||
"ToastNotificationDeleteFailed": "বিজ্ঞপ্তি মুছে ফেলতে ব্যর্থ",
|
||||
@@ -1033,6 +1106,7 @@
|
||||
"ToastPlaylistUpdateSuccess": "প্লেলিস্ট আপডেট করা হয়েছে",
|
||||
"ToastPodcastCreateFailed": "পডকাস্ট তৈরি করতে ব্যর্থ",
|
||||
"ToastPodcastCreateSuccess": "পডকাস্ট সফলভাবে তৈরি করা হয়েছে",
|
||||
"ToastPodcastEpisodeUpdated": "পর্ব আপডেট করা হয়েছে",
|
||||
"ToastPodcastGetFeedFailed": "পডকাস্ট ফিড পেতে ব্যর্থ হয়েছে",
|
||||
"ToastPodcastNoEpisodesInFeed": "আরএসএস ফিডে কোনো পর্ব পাওয়া যায়নি",
|
||||
"ToastPodcastNoRssFeed": "পডকাস্টের কোন আরএসএস ফিড নেই",
|
||||
@@ -1057,6 +1131,7 @@
|
||||
"ToastSelectAtLeastOneUser": "অন্তত একজন ব্যবহারকারী নির্বাচন করুন",
|
||||
"ToastSendEbookToDeviceFailed": "ডিভাইসে ইবুক পাঠাতে ব্যর্থ",
|
||||
"ToastSendEbookToDeviceSuccess": "ইবুক \"{0}\" ডিভাইসে পাঠানো হয়েছে",
|
||||
"ToastSeriesSubmitFailedSameName": "একই নামে দুটি সিরিজ যোগ করা যাবে না",
|
||||
"ToastSeriesUpdateFailed": "সিরিজ আপডেট ব্যর্থ হয়েছে",
|
||||
"ToastSeriesUpdateSuccess": "সিরিজ আপডেট সাফল্য",
|
||||
"ToastServerSettingsUpdateSuccess": "সার্ভার সেটিংস আপডেট করা হয়েছে",
|
||||
@@ -1075,10 +1150,20 @@
|
||||
"ToastUnknownError": "অজানা ত্রুটি",
|
||||
"ToastUnlinkOpenIdFailed": "OpenID থেকে ব্যবহারকারীকে আনলিঙ্ক করতে ব্যর্থ হয়েছে",
|
||||
"ToastUnlinkOpenIdSuccess": "OpenID থেকে ব্যবহারকারীকে লিঙ্কমুক্ত করা হয়েছে",
|
||||
"ToastUploaderFilepathExistsError": "ফাইল পাথ \"{0}\" ইতিমধ্যে সার্ভারে বিদ্যমান",
|
||||
"ToastUploaderItemExistsInSubdirectoryError": "আইটেম \"{0}\" আপলোড পাথের একটি সাবডিরেক্টরি ব্যবহার করছে।",
|
||||
"ToastUserDeleteFailed": "ব্যবহারকারী মুছতে ব্যর্থ",
|
||||
"ToastUserDeleteSuccess": "ব্যবহারকারী মুছে ফেলা হয়েছে",
|
||||
"ToastUserPasswordChangeSuccess": "পাসওয়ার্ড সফলভাবে পরিবর্তন করা হয়েছে",
|
||||
"ToastUserPasswordMismatch": "পাসওয়ার্ড মিলছে না",
|
||||
"ToastUserPasswordMustChange": "নতুন পাসওয়ার্ড পুরানো পাসওয়ার্ডের সাথে মিলতে পারবে না",
|
||||
"ToastUserRootRequireName": "একটি রুট ব্যবহারকারীর নাম লিখতে হবে"
|
||||
"ToastUserRootRequireName": "একটি রুট ব্যবহারকারীর নাম লিখতে হবে",
|
||||
"TooltipAddChapters": "অধ্যায় যোগ করুন",
|
||||
"TooltipAddOneSecond": "১ সেকেন্ড যোগ করুন",
|
||||
"TooltipAdjustChapterStart": "শুরুর সময় সমন্বয় করতে ক্লিক করুন",
|
||||
"TooltipLockAllChapters": "সব অধ্যায় লক করুন",
|
||||
"TooltipLockChapter": "অধ্যায় লক করুন (একাধিক অধ্যায়ের পরিসর নির্বাচন করতে Shift+ক্লিক করুন)",
|
||||
"TooltipSubtractOneSecond": "১ সেকেন্ড কমান",
|
||||
"TooltipUnlockAllChapters": "সব অধ্যায় আনলক করুন",
|
||||
"TooltipUnlockChapter": "অধ্যায় আনলক করুন (একাধিক অধ্যায়ের পরিসর নির্বাচন করতে Shift+ক্লিক করুন)"
|
||||
}
|
||||
@@ -46,6 +46,7 @@
|
||||
"ButtonLatest": "Nejnovější",
|
||||
"ButtonLibrary": "Knihovna",
|
||||
"ButtonLogout": "Odhlásit",
|
||||
"ButtonLogoutAllDevices": "Odhlásit ze všech zařízení",
|
||||
"ButtonLookup": "Vyhledat",
|
||||
"ButtonManageTracks": "Správa stop",
|
||||
"ButtonMapChapterTitles": "Mapovat názvy kapitol",
|
||||
@@ -194,6 +195,7 @@
|
||||
"HeaderScheduleEpisodeDownloads": "Naplánovat automatické stahování epizod",
|
||||
"HeaderScheduleLibraryScans": "Naplánovat automatické prohledávání knihoven",
|
||||
"HeaderSession": "Relace",
|
||||
"HeaderSessions": "Relace",
|
||||
"HeaderSetBackupSchedule": "Nastavit plán zálohování",
|
||||
"HeaderSettings": "Nastavení",
|
||||
"HeaderSettingsDisplay": "Zobrazit",
|
||||
@@ -206,7 +208,7 @@
|
||||
"HeaderStatsLargestItems": "Největší položky",
|
||||
"HeaderStatsLongestItems": "Nejdelší položky (hod.)",
|
||||
"HeaderStatsMinutesListeningChart": "Poslech za posledních 7 dní (min)",
|
||||
"HeaderStatsRecentSessions": "Poslední relace",
|
||||
"HeaderStatsRecentSessions": "Nedávné relace",
|
||||
"HeaderStatsTop10Authors": "Top 10 autorů",
|
||||
"HeaderStatsTop5Genres": "Top 5 žánrů",
|
||||
"HeaderTableOfContents": "Obsah",
|
||||
@@ -415,6 +417,7 @@
|
||||
"LabelIntervalEveryHour": "Každou hodinu",
|
||||
"LabelIntervalEveryMinute": "Každou minutu",
|
||||
"LabelInvert": "Invertovat",
|
||||
"LabelIpAddress": "IP Adresa",
|
||||
"LabelItem": "Položka",
|
||||
"LabelJumpBackwardAmount": "Přeskočit zpět o",
|
||||
"LabelJumpForwardAmount": "Přeskočit dopředu o",
|
||||
@@ -774,6 +777,7 @@
|
||||
"MessageConfirmDeleteSession": "Opravdu chcete smazat tuto relaci?",
|
||||
"MessageConfirmEmbedMetadataInAudioFiles": "Jste si jisti, že chcete vložit metadata do {0} zvukových souborů?",
|
||||
"MessageConfirmForceReScan": "Opravdu chcete vynutit opětovné prohledání?",
|
||||
"MessageConfirmLogoutDevice": "Opravdu se chcete odhlásit z \"{0}\"?",
|
||||
"MessageConfirmMarkAllEpisodesFinished": "Opravdu chcete označit všechny epizody jako dokončené?",
|
||||
"MessageConfirmMarkAllEpisodesNotFinished": "Opravdu chcete označit všechny epizody jako nedokončené?",
|
||||
"MessageConfirmMarkItemFinished": "Opravdu chcete označit \"{0}\" jako dokončené?",
|
||||
@@ -970,7 +974,7 @@
|
||||
"StatsBooksFinishedThisYear": "Některé knihy dokončené tento rok…",
|
||||
"StatsBooksListenedTo": "knih poslechnuto",
|
||||
"StatsCollectionGrewTo": "Vaše kolekce knih se rozrostla na…",
|
||||
"StatsSessions": "sezóna",
|
||||
"StatsSessions": "relace",
|
||||
"StatsSpentListening": "stráveno posloucháním",
|
||||
"StatsTopAuthor": "TOP AUTOR",
|
||||
"StatsTopAuthors": "TOP AUTOŘI",
|
||||
@@ -1131,12 +1135,12 @@
|
||||
"ToastSeriesUpdateFailed": "Aktualizace série se nezdařila",
|
||||
"ToastSeriesUpdateSuccess": "Aktualizace série byla úspěšná",
|
||||
"ToastServerSettingsUpdateSuccess": "Nastavení serveru aktualizováno",
|
||||
"ToastSessionCloseFailed": "Chyba při ukončení",
|
||||
"ToastSessionCloseFailed": "Nepodařilo se ukončit relaci",
|
||||
"ToastSessionDeleteFailed": "Nepodařilo se smazat relaci",
|
||||
"ToastSessionDeleteSuccess": "Relace smazána",
|
||||
"ToastSleepTimerDone": "Uspání knížky ... zZzzZz",
|
||||
"ToastSlugMustChange": "Slug obsahuje chybné znaky",
|
||||
"ToastSlugRequired": "Slug (URL) je vyžadována",
|
||||
"ToastSlugRequired": "Slug (URL) je vyžadován",
|
||||
"ToastSocketConnected": "Socket připojen",
|
||||
"ToastSocketDisconnected": "Socket odpojen",
|
||||
"ToastSocketFailedToConnect": "Socket se nepodařilo připojit",
|
||||
|
||||
+251
-251
@@ -3,7 +3,7 @@
|
||||
"ButtonAddApiKey": "Tilføj API-nøgle",
|
||||
"ButtonAddChapters": "Tilføj kapitler",
|
||||
"ButtonAddDevice": "Tilføj enhed",
|
||||
"ButtonAddLibrary": "Tilføj Bibliotek",
|
||||
"ButtonAddLibrary": "Tilføj bibliotek",
|
||||
"ButtonAddPodcasts": "Tilføj podcasts",
|
||||
"ButtonAddUser": "Tilføj bruger",
|
||||
"ButtonAddYourFirstLibrary": "Tilføj dit første bibliotek",
|
||||
@@ -11,21 +11,21 @@
|
||||
"ButtonApplyChapters": "Anvend kapitler",
|
||||
"ButtonAuthors": "Forfattere",
|
||||
"ButtonBack": "Tilbage",
|
||||
"ButtonBatchEditPopulateFromExisting": "Opret fra eksisterende",
|
||||
"ButtonBatchEditPopulateMapDetails": "Opret fra kortlægnings detaljer",
|
||||
"ButtonBatchEditPopulateFromExisting": "Kopiér fra eksisterende",
|
||||
"ButtonBatchEditPopulateMapDetails": "Kopiér til masseredigering",
|
||||
"ButtonBrowseForFolder": "Gennemse mappe",
|
||||
"ButtonCancel": "Annuller",
|
||||
"ButtonCancelEncode": "Annuller kodning",
|
||||
"ButtonChangeRootPassword": "Ændr rodadgangskode",
|
||||
"ButtonCheckAndDownloadNewEpisodes": "Tjek og download nye episoder",
|
||||
"ButtonCheckAndDownloadNewEpisodes": "Tjek og hent nye afsnit",
|
||||
"ButtonChooseAFolder": "Vælg en mappe",
|
||||
"ButtonChooseFiles": "Vælg filer",
|
||||
"ButtonClearFilter": "Ryd filter",
|
||||
"ButtonClose": "Luk",
|
||||
"ButtonCloseFeed": "Luk feed",
|
||||
"ButtonCloseSession": "Luk Åben Session",
|
||||
"ButtonCloseSession": "Luk åben session",
|
||||
"ButtonCollections": "Samlinger",
|
||||
"ButtonConfigureScanner": "Konfigurer scanner",
|
||||
"ButtonConfigureScanner": "Konfigurer skanner",
|
||||
"ButtonCreate": "Opret",
|
||||
"ButtonCreateBackup": "Opret sikkerhedskopi",
|
||||
"ButtonDelete": "Slet",
|
||||
@@ -33,10 +33,10 @@
|
||||
"ButtonEdit": "Rediger",
|
||||
"ButtonEditChapters": "Rediger kapitler",
|
||||
"ButtonEditPodcast": "Rediger podcast",
|
||||
"ButtonEnable": "Aktiver",
|
||||
"ButtonEnable": "Aktivér",
|
||||
"ButtonFireAndFail": "Affyring Og Fejl",
|
||||
"ButtonFireOnTest": "Affyring vedTest begivenhed",
|
||||
"ButtonForceReScan": "Tving genindlæsning",
|
||||
"ButtonFireOnTest": "Udløs onTest-hændelse",
|
||||
"ButtonForceReScan": "Tving genskanning",
|
||||
"ButtonFullPath": "Fuld sti",
|
||||
"ButtonHide": "Skjul",
|
||||
"ButtonHome": "Hjem",
|
||||
@@ -53,321 +53,321 @@
|
||||
"ButtonMatchBooks": "Match bøger",
|
||||
"ButtonNevermind": "Glem det",
|
||||
"ButtonNext": "Næste",
|
||||
"ButtonNextChapter": "Næste Kapitel",
|
||||
"ButtonNextItemInQueue": "Næste Element i Køen",
|
||||
"ButtonNextChapter": "Næste kapitel",
|
||||
"ButtonNextItemInQueue": "Næste element i køen",
|
||||
"ButtonOk": "Ok",
|
||||
"ButtonOpenFeed": "Åbn feed",
|
||||
"ButtonOpenManager": "Åbn manager",
|
||||
"ButtonPause": "Pause",
|
||||
"ButtonPlay": "Afspil",
|
||||
"ButtonPlayAll": "Afspil Alle",
|
||||
"ButtonPlayAll": "Afspil alle",
|
||||
"ButtonPlaying": "Afspiller",
|
||||
"ButtonPlaylists": "Afspilningslister",
|
||||
"ButtonPrevious": "Sidste",
|
||||
"ButtonPreviousChapter": "Sidste Kapitel",
|
||||
"ButtonProbeAudioFile": "Undersøg Lydfil",
|
||||
"ButtonPurgeAllCache": "Ryd al cache",
|
||||
"ButtonPurgeItemsCache": "Ryd elementcache",
|
||||
"ButtonQueueAddItem": "Tilføj til kø",
|
||||
"ButtonPlaylists": "Playlister",
|
||||
"ButtonPrevious": "Forrige",
|
||||
"ButtonPreviousChapter": "Forrige kapitel",
|
||||
"ButtonProbeAudioFile": "Undersøg lydfil",
|
||||
"ButtonPurgeAllCache": "Ryd alle mellemlagre",
|
||||
"ButtonPurgeItemsCache": "Ryd mediemellemlager",
|
||||
"ButtonQueueAddItem": "Sæt i kø",
|
||||
"ButtonQueueRemoveItem": "Fjern fra kø",
|
||||
"ButtonQuickEmbed": "Hurtig Indlejring",
|
||||
"ButtonQuickEmbedMetadata": "Hurtig Indlejring af Metadata",
|
||||
"ButtonQuickEmbed": "Hurtig indlejring",
|
||||
"ButtonQuickEmbedMetadata": "Hurtig indlejring af metadata",
|
||||
"ButtonQuickMatch": "Hurtig Match",
|
||||
"ButtonReScan": "Gen-scan",
|
||||
"ButtonReScan": "Skan igen",
|
||||
"ButtonRead": "Læs",
|
||||
"ButtonReadLess": "Se mindre",
|
||||
"ButtonReadMore": "Se mere",
|
||||
"ButtonRefresh": "Genindlæs",
|
||||
"ButtonRemove": "Fjern",
|
||||
"ButtonRemoveAll": "Fjern Alle",
|
||||
"ButtonRemoveAllLibraryItems": "Fjern Alle Bibliotekselementer",
|
||||
"ButtonRemoveFromContinueListening": "Fjern fra Fortsæt Lytning",
|
||||
"ButtonRemoveFromContinueReading": "Fjern fra Fortsæt Læsning",
|
||||
"ButtonRemoveSeriesFromContinueSeries": "Fjern Serie fra Fortsæt Serie",
|
||||
"ButtonRemoveAll": "Fjern alle",
|
||||
"ButtonRemoveAllLibraryItems": "Fjern alle bibliotekselementer",
|
||||
"ButtonRemoveFromContinueListening": "Fjern fra »Fortsæt med at lytte«",
|
||||
"ButtonRemoveFromContinueReading": "Fjern fra »Fortsæt med at læse«",
|
||||
"ButtonRemoveSeriesFromContinueSeries": "Fjern Serie fra »Fortsæt serie«",
|
||||
"ButtonReset": "Nulstil",
|
||||
"ButtonResetToDefault": "Nulstil til standard",
|
||||
"ButtonRestore": "Gendan",
|
||||
"ButtonSave": "Gem",
|
||||
"ButtonSaveAndClose": "Gem & Luk",
|
||||
"ButtonSaveTracklist": "Gem Sporliste",
|
||||
"ButtonScan": "Scan",
|
||||
"ButtonScanLibrary": "Scan Bibliotek",
|
||||
"ButtonScrollLeft": "Rul til Venstre",
|
||||
"ButtonScrollRight": "Rul til Højre",
|
||||
"ButtonSaveAndClose": "Gem og luk",
|
||||
"ButtonSaveTracklist": "Gem sporliste",
|
||||
"ButtonScan": "Skan",
|
||||
"ButtonScanLibrary": "Skan bibliotek",
|
||||
"ButtonScrollLeft": "Rul til venstre",
|
||||
"ButtonScrollRight": "Rul til højre",
|
||||
"ButtonSearch": "Søg",
|
||||
"ButtonSelectFolderPath": "Vælg Mappe Sti",
|
||||
"ButtonSelectFolderPath": "Vælg mappesti",
|
||||
"ButtonSeries": "Serier",
|
||||
"ButtonSetChaptersFromTracks": "Sæt kapitler fra spor",
|
||||
"ButtonShare": "Del",
|
||||
"ButtonShiftTimes": "Skift Tider",
|
||||
"ButtonShiftTimes": "Forskyd tider",
|
||||
"ButtonShow": "Vis",
|
||||
"ButtonStartM4BEncode": "Start M4B Kode",
|
||||
"ButtonStartMetadataEmbed": "Start Metadata Indlejring",
|
||||
"ButtonStartM4BEncode": "Start konvertering til M4B",
|
||||
"ButtonStartMetadataEmbed": "Start metadataindlejring",
|
||||
"ButtonStats": "Statistik",
|
||||
"ButtonSubmit": "Send",
|
||||
"ButtonTest": "Test",
|
||||
"ButtonUnlinkOpenId": "Afkobl OpenID",
|
||||
"ButtonUpload": "Upload",
|
||||
"ButtonUploadBackup": "Upload Backup",
|
||||
"ButtonUploadCover": "Upload Omslag",
|
||||
"ButtonUploadOPMLFile": "Upload OPML Fil",
|
||||
"ButtonUpload": "Overfør",
|
||||
"ButtonUploadBackup": "Overfør sikkerhedskopi",
|
||||
"ButtonUploadCover": "Overfør omslag",
|
||||
"ButtonUploadOPMLFile": "Overfør OPML-fil",
|
||||
"ButtonUserDelete": "Slet bruger {0}",
|
||||
"ButtonUserEdit": "Rediger bruger {0}",
|
||||
"ButtonViewAll": "Vis Alle",
|
||||
"ButtonViewAll": "Vis alle",
|
||||
"ButtonYes": "Ja",
|
||||
"ErrorUploadFetchMetadataAPI": "Fejl henter metadata",
|
||||
"ErrorUploadFetchMetadataNoResults": "Kunne ikke hente metadata - prøv at uploade title og/eller forfatter",
|
||||
"ErrorUploadLacksTitle": "Skal have en title",
|
||||
"ErrorUploadFetchMetadataAPI": "Fejl ved hentning af metadata",
|
||||
"ErrorUploadFetchMetadataNoResults": "Kunne ikke hente metadata - prøv at ændre titel og/eller forfatter",
|
||||
"ErrorUploadLacksTitle": "Titel kræves",
|
||||
"HeaderAccount": "Konto",
|
||||
"HeaderAddCustomMetadataProvider": "Tilføj Brugerdefineret Metadataudbyder",
|
||||
"HeaderAddCustomMetadataProvider": "Tilføj brugerdefineret metadataudbyder",
|
||||
"HeaderAdvanced": "Avanceret",
|
||||
"HeaderApiKeys": "API-nøgler",
|
||||
"HeaderAppriseNotificationSettings": "Apprise Notifikationsindstillinger",
|
||||
"HeaderAppriseNotificationSettings": "Apprise-notifikationsindstillinger",
|
||||
"HeaderAudioTracks": "Lydspor",
|
||||
"HeaderAudiobookTools": "Audiobog Filhåndteringsværktøjer",
|
||||
"HeaderAuthentication": "Autentificering",
|
||||
"HeaderAudiobookTools": "Håndtering af lydbogsfiler",
|
||||
"HeaderAuthentication": "Loginindstillinger",
|
||||
"HeaderBackups": "Sikkerhedskopier",
|
||||
"HeaderBulkChapterModal": "Tilføj flere kapitler",
|
||||
"HeaderChangePassword": "Skift Adgangskode",
|
||||
"HeaderChangePassword": "Skift adgangskode",
|
||||
"HeaderChapters": "Kapitler",
|
||||
"HeaderChooseAFolder": "Vælg en Mappe",
|
||||
"HeaderChooseAFolder": "Vælg en mappe",
|
||||
"HeaderCollection": "Samling",
|
||||
"HeaderCollectionItems": "Samlingselementer",
|
||||
"HeaderCollectionItems": "Bøger i samlingen",
|
||||
"HeaderCover": "Omslag",
|
||||
"HeaderCurrentDownloads": "Nuværende Downloads",
|
||||
"HeaderCustomMessageOnLogin": "Brugerdefineret Besked ved Login",
|
||||
"HeaderCustomMetadataProviders": "Brugerdefineret Metadataudbyder",
|
||||
"HeaderCurrentDownloads": "Aktuelle downloads",
|
||||
"HeaderCustomMessageOnLogin": "Brugerdefineret besked ved logind",
|
||||
"HeaderCustomMetadataProviders": "Brugerdefineret metadataudbyder",
|
||||
"HeaderDetails": "Detaljer",
|
||||
"HeaderDownloadQueue": "Download Kø",
|
||||
"HeaderDownloadQueue": "Downloadkø",
|
||||
"HeaderEbookFiles": "E-bogsfiler",
|
||||
"HeaderEmail": "Email",
|
||||
"HeaderEmailSettings": "Email Indstillinger",
|
||||
"HeaderEpisodes": "Episoder",
|
||||
"HeaderEreaderDevices": "E-læser Enheder",
|
||||
"HeaderEreaderSettings": "E-læser Indstillinger",
|
||||
"HeaderEmailSettings": "Emailindstillinger",
|
||||
"HeaderEpisodes": "Afsnit",
|
||||
"HeaderEreaderDevices": "E-bogslæsere",
|
||||
"HeaderEreaderSettings": "Indstillinger for e-bogslæsere",
|
||||
"HeaderFiles": "Filer",
|
||||
"HeaderFindChapters": "Find Kapitler",
|
||||
"HeaderIgnoredFiles": "Ignorerede Filer",
|
||||
"HeaderItemFiles": "Emnefiler",
|
||||
"HeaderItemMetadataUtils": "Emne Metadata Værktøjer",
|
||||
"HeaderLastListeningSession": "Seneste Lyttesession",
|
||||
"HeaderLatestEpisodes": "Seneste episoder",
|
||||
"HeaderFindChapters": "Find kapitler",
|
||||
"HeaderIgnoredFiles": "Ignorerede filer",
|
||||
"HeaderItemFiles": "Filer",
|
||||
"HeaderItemMetadataUtils": "Metadata-værktøjer",
|
||||
"HeaderLastListeningSession": "Seneste lyttesession",
|
||||
"HeaderLatestEpisodes": "Nyeste afsnit",
|
||||
"HeaderLibraries": "Biblioteker",
|
||||
"HeaderLibraryFiles": "Biblioteksfiler",
|
||||
"HeaderLibraryStats": "Biblioteksstatistik",
|
||||
"HeaderListeningSessions": "Lyttesessioner",
|
||||
"HeaderListeningStats": "Lyttestatistik",
|
||||
"HeaderLogin": "Log ind",
|
||||
"HeaderLogs": "Logs",
|
||||
"HeaderManageGenres": "Administrer Genrer",
|
||||
"HeaderManageTags": "Administrer Tags",
|
||||
"HeaderMapDetails": "Kort Detaljer",
|
||||
"HeaderMatch": "Match",
|
||||
"HeaderLogs": "Logfiler",
|
||||
"HeaderManageGenres": "Administrer genrer",
|
||||
"HeaderManageTags": "Administrer emneord",
|
||||
"HeaderMapDetails": "Masseredigering",
|
||||
"HeaderMatch": "Find match",
|
||||
"HeaderMetadataOrderOfPrecedence": "Metadata-prioritet",
|
||||
"HeaderMetadataToEmbed": "Metadata til indlejring",
|
||||
"HeaderNewAccount": "Ny Konto",
|
||||
"HeaderNewAccount": "Ny konto",
|
||||
"HeaderNewApiKey": "Ny API-nøgle",
|
||||
"HeaderNewLibrary": "Nyt Bibliotek",
|
||||
"HeaderNotificationCreate": "Opret Notifikation",
|
||||
"HeaderNewLibrary": "Nyt bibliotek",
|
||||
"HeaderNotificationCreate": "Opret notifikation",
|
||||
"HeaderNotificationUpdate": "Updater Notifikation",
|
||||
"HeaderNotifications": "Meddelelser",
|
||||
"HeaderOpenIDConnectAuthentication": "OpenID Connect-autentificering",
|
||||
"HeaderOpenIDConnectAuthentication": "Log ind med OpenID Connect",
|
||||
"HeaderOpenListeningSessions": "Åbne lyttesessioner",
|
||||
"HeaderOpenRSSFeed": "Åbn RSS Feed",
|
||||
"HeaderOtherFiles": "Andre Filer",
|
||||
"HeaderPasswordAuthentication": "Adgangskodeautentificering",
|
||||
"HeaderOpenRSSFeed": "Åbn RSS-feed",
|
||||
"HeaderOtherFiles": "Andre filer",
|
||||
"HeaderPasswordAuthentication": "Log ind med adgangskode",
|
||||
"HeaderPermissions": "Tilladelser",
|
||||
"HeaderPlayerQueue": "Afspilningskø",
|
||||
"HeaderPlayerSettings": "Afspiller Indstillinger",
|
||||
"HeaderPlaylist": "Afspilningsliste",
|
||||
"HeaderPlaylistItems": "Afspilningsliste Elementer",
|
||||
"HeaderPodcastsToAdd": "Podcasts til Tilføjelse",
|
||||
"HeaderPlayerSettings": "Afspilningsindstillinger",
|
||||
"HeaderPlaylist": "Playliste",
|
||||
"HeaderPlaylistItems": "Elementer i playlisten",
|
||||
"HeaderPodcastsToAdd": "Podcasts som skal tilføjes",
|
||||
"HeaderPresets": "Forudindstillinger",
|
||||
"HeaderPreviewCover": "Forhåndsvis Omslag",
|
||||
"HeaderRSSFeedGeneral": "RSS Detaljer",
|
||||
"HeaderRSSFeedIsOpen": "RSS Feed er Åben",
|
||||
"HeaderRSSFeeds": "RSS-Feeds",
|
||||
"HeaderRemoveEpisode": "Fjern Episode",
|
||||
"HeaderRemoveEpisodes": "Fjern {0} Episoder",
|
||||
"HeaderSavedMediaProgress": "Gemt Medieforløb",
|
||||
"HeaderPreviewCover": "Forhåndsvis omslag",
|
||||
"HeaderRSSFeedGeneral": "RSS-detaljer",
|
||||
"HeaderRSSFeedIsOpen": "RSS-feedet er åbent",
|
||||
"HeaderRSSFeeds": "RSS-feeds",
|
||||
"HeaderRemoveEpisode": "Fjern afsnit",
|
||||
"HeaderRemoveEpisodes": "Fjern {0} afsnit",
|
||||
"HeaderSavedMediaProgress": "Gemt lyttefremgang",
|
||||
"HeaderSchedule": "Planlæg",
|
||||
"HeaderScheduleEpisodeDownloads": "Planlæg Automatisk Episode-Download",
|
||||
"HeaderScheduleLibraryScans": "Planlæg Automatiske Biblioteksscanninger",
|
||||
"HeaderScheduleEpisodeDownloads": "Planlæg automatisk hentning af afsnit",
|
||||
"HeaderScheduleLibraryScans": "Planlæg automatisk biblioteksskanning",
|
||||
"HeaderSession": "Session",
|
||||
"HeaderSetBackupSchedule": "Indstil Sikkerhedskopieringsplan",
|
||||
"HeaderSetBackupSchedule": "Indstil tidsplan for sikkerhedskopiering",
|
||||
"HeaderSettings": "Indstillinger",
|
||||
"HeaderSettingsDisplay": "Skærm",
|
||||
"HeaderSettingsExperimental": "Eksperimentelle Funktioner",
|
||||
"HeaderSettingsDisplay": "Visning",
|
||||
"HeaderSettingsExperimental": "Eksperimentelle funktioner",
|
||||
"HeaderSettingsGeneral": "Generelt",
|
||||
"HeaderSettingsScanner": "Scanner",
|
||||
"HeaderSettingsScanner": "Skanner",
|
||||
"HeaderSettingsSecurity": "Sikkerhed",
|
||||
"HeaderSettingsWebClient": "Webklient",
|
||||
"HeaderSleepTimer": "Søvntimer",
|
||||
"HeaderStatsLargestItems": "Største Elementer",
|
||||
"HeaderStatsLongestItems": "Længste Elementer (timer)",
|
||||
"HeaderStatsMinutesListeningChart": "Minutter Lyttet (sidste 7 dage)",
|
||||
"HeaderStatsRecentSessions": "Seneste Sessions",
|
||||
"HeaderStatsTop10Authors": "Top 10 Forfattere",
|
||||
"HeaderStatsTop5Genres": "Top 5 Genrer",
|
||||
"HeaderSleepTimer": "Sovefunktion",
|
||||
"HeaderStatsLargestItems": "Største bøger",
|
||||
"HeaderStatsLongestItems": "Længste bøger (timer)",
|
||||
"HeaderStatsMinutesListeningChart": "Minutter lyttet (sidste 7 dage)",
|
||||
"HeaderStatsRecentSessions": "Seneste sessioner",
|
||||
"HeaderStatsTop10Authors": "Top 10 forfattere",
|
||||
"HeaderStatsTop5Genres": "Top 5 genrer",
|
||||
"HeaderTableOfContents": "Indholdsfortegnelse",
|
||||
"HeaderTools": "Værktøjer",
|
||||
"HeaderUpdateAccount": "Opdater Konto",
|
||||
"HeaderUpdateAccount": "Opdater konto",
|
||||
"HeaderUpdateApiKey": "Opdater API-nøgle",
|
||||
"HeaderUpdateAuthor": "Opdater Forfatter",
|
||||
"HeaderUpdateDetails": "Opdater Detaljer",
|
||||
"HeaderUpdateLibrary": "Opdater Bibliotek",
|
||||
"HeaderUpdateAuthor": "Opdater forfatter",
|
||||
"HeaderUpdateDetails": "Opdater detaljer",
|
||||
"HeaderUpdateLibrary": "Opdater bibliotek",
|
||||
"HeaderUsers": "Brugere",
|
||||
"HeaderYearReview": "Gennemgang af År {0}",
|
||||
"HeaderYourStats": "Dine Statistikker",
|
||||
"LabelAbridged": "Forkortet",
|
||||
"LabelAbridgedChecked": "Forkortet (kontrolleret)",
|
||||
"LabelAbridgedUnchecked": "Uforkortet (ikke kontrolleret)",
|
||||
"LabelAccessibleBy": "Tilgængelig af",
|
||||
"LabelAccountType": "Brugertype",
|
||||
"HeaderYearReview": "Året {0} opsummeret",
|
||||
"HeaderYourStats": "Din statistik",
|
||||
"LabelAbridged": "Forkortet udgave",
|
||||
"LabelAbridgedChecked": "Forkortet udgave (bekræftet)",
|
||||
"LabelAbridgedUnchecked": "Uforkortet udgave (ikke bekræftet)",
|
||||
"LabelAccessibleBy": "Tilgængelig for",
|
||||
"LabelAccountType": "Kontotype",
|
||||
"LabelAccountTypeAdmin": "Administrator",
|
||||
"LabelAccountTypeGuest": "Gæst",
|
||||
"LabelAccountTypeUser": "Bruger",
|
||||
"LabelActivities": "Aktiviteter",
|
||||
"LabelActivity": "Aktivitet",
|
||||
"LabelAddToCollection": "Tilføj til Samling",
|
||||
"LabelAddToCollectionBatch": "Tilføj {0} Bøger til Samling",
|
||||
"LabelAddToPlaylist": "Tilføj til Afspilningsliste",
|
||||
"LabelAddToPlaylistBatch": "Tilføj {0} Elementer til Afspilningsliste",
|
||||
"LabelAddToCollection": "Tilføj til samling",
|
||||
"LabelAddToCollectionBatch": "Tilføj {0} bøger til samling",
|
||||
"LabelAddToPlaylist": "Tilføj til playliste",
|
||||
"LabelAddToPlaylistBatch": "Tilføj {0} elementer til afspilningsliste",
|
||||
"LabelAddedAt": "Tilføjet",
|
||||
"LabelAddedDate": "Tilføjet {0}",
|
||||
"LabelAdminUsersOnly": "Kun Administratorer",
|
||||
"LabelAddedDate": "Tilføjede {0}",
|
||||
"LabelAdminUsersOnly": "Kun administratorer",
|
||||
"LabelAll": "Alle",
|
||||
"LabelAllEpisodesDownloaded": "Alle episoder hentet",
|
||||
"LabelAllUsers": "Alle Brugere",
|
||||
"LabelAllUsersExcludingGuests": "Alle bruger eksklusiv gæster",
|
||||
"LabelAllUsersIncludingGuests": "Alle bruger inklusiv gæster",
|
||||
"LabelAllEpisodesDownloaded": "Alle afsnit hentet",
|
||||
"LabelAllUsers": "Alle brugere",
|
||||
"LabelAllUsersExcludingGuests": "Alle brugere, bortset fra gæster",
|
||||
"LabelAllUsersIncludingGuests": "Alle brugere, også gæster",
|
||||
"LabelAlreadyInYourLibrary": "Allerede i dit bibliotek",
|
||||
"LabelApiKeyCreated": "API-nøgle\"{0}\" oprettet succesfuldt.",
|
||||
"LabelApiKeyCreated": "API-nøglen \"{0}\" blev oprettet.",
|
||||
"LabelApiKeyCreatedDescription": "Sørg for at kopiere API-nøglen nu, da du ikke vil kunne se den igen.",
|
||||
"LabelApiKeyUser": "Ret på vegne af brugeren",
|
||||
"LabelApiKeyUserDescription": "Denne API-nøgle vil have de samme tilladelser som den bruger, den handler på vegne af. Dette vil fremgå på samme måde i logfiler, som hvis brugeren foretog anmodningen.",
|
||||
"LabelApiToken": "API Token",
|
||||
"LabelAppend": "Tilføj",
|
||||
"LabelApiKeyUser": "Handl på vegne af bruger",
|
||||
"LabelApiKeyUserDescription": "Denne API-nøgle vil have de samme tilladelser som den bruger, den handler på vegne af. Dette vil fremgå på samme måde i logfiler, som hvis brugeren selv foretog anmodningen.",
|
||||
"LabelApiToken": "API-nøgle",
|
||||
"LabelAppend": "Vedhæft",
|
||||
"LabelAudioBitrate": "Lydbitrate (f.eks. 128k)",
|
||||
"LabelAudioChannels": "Lydkanaler (1 eller 2)",
|
||||
"LabelAudioCodec": "Lydkodek",
|
||||
"LabelAudioCodec": "Lyd-codec",
|
||||
"LabelAuthor": "Forfatter",
|
||||
"LabelAuthorFirstLast": "Forfatter (Fornavn Efternavn)",
|
||||
"LabelAuthorLastFirst": "Forfatter (Efternavn, Fornavn)",
|
||||
"LabelAuthors": "Forfattere",
|
||||
"LabelAutoDownloadEpisodes": "Auto Download Episoder",
|
||||
"LabelAutoFetchMetadata": "Automatisk Hent Metadata",
|
||||
"LabelAutoFetchMetadataHelp": "Henter metadata for titler, forfatter og serier for at strømligne uploading. Ekstra metadata har måske brug for at blive matchet efter upload.",
|
||||
"LabelAutoLaunch": "Åben Automatisk",
|
||||
"LabelAutoDownloadEpisodes": "Hent automatisk afsnit",
|
||||
"LabelAutoFetchMetadata": "Hent automatisk metadata",
|
||||
"LabelAutoFetchMetadataHelp": "Henter metadata for titler, forfatter og serie for at strømline uploading. Ekstra metadata har måske brug for at blive matchet efter upload.",
|
||||
"LabelAutoLaunch": "Automatisk start",
|
||||
"LabelAutoLaunchDescription": "Viderestil automatisk til login-udbyderen ved navigation til login-siden (manuel overstyring via <code>/login?autoLaunch=0</code>)",
|
||||
"LabelAutoRegister": "Registrer Automatisk",
|
||||
"LabelAutoRegister": "Registrer automatisk",
|
||||
"LabelAutoRegisterDescription": "Automatisk oprettelse af nye brugere efter login",
|
||||
"LabelBackToUser": "Tilbage til Bruger",
|
||||
"LabelBackupAudioFiles": "Sikkerhedskopier lydfiler",
|
||||
"LabelBackupLocation": "Backup Placering",
|
||||
"LabelBackToUser": "Tilbage til bruger",
|
||||
"LabelBackupAudioFiles": "Sikkerhedskopiér lydfiler",
|
||||
"LabelBackupLocation": "Placering af sikkerhedskopi",
|
||||
"LabelBackupsEnableAutomaticBackups": "Automatisk sikkerhedskopiering",
|
||||
"LabelBackupsEnableAutomaticBackupsHelp": "Sikkerhedskopier gemt i /metadata/backups",
|
||||
"LabelBackupsMaxBackupSize": "Maksimal sikkerhedskopistørrelse (i GB) (0 for ubegrænset)",
|
||||
"LabelBackupsMaxBackupSizeHelp": "Som en beskyttelse mod fejlkonfiguration fejler sikkerhedskopier, hvis de overstiger den konfigurerede størrelse.",
|
||||
"LabelBackupsNumberToKeep": "Antal sikkerhedskopier at beholde",
|
||||
"LabelBackupsNumberToKeepHelp": "Kun 1 sikkerhedskopi fjernes ad gangen, så hvis du allerede har flere sikkerhedskopier end dette, skal du fjerne dem manuelt.",
|
||||
"LabelBackupsNumberToKeep": "Antal sikkerhedskopier som skal beholdes",
|
||||
"LabelBackupsNumberToKeepHelp": "Kun én sikkerhedskopi fjernes ad gangen, så hvis du allerede har flere sikkerhedskopier end dette, skal du fjerne dem manuelt.",
|
||||
"LabelBitrate": "Bitrate",
|
||||
"LabelBonus": "Bonus",
|
||||
"LabelBooks": "Bøger",
|
||||
"LabelButtonText": "Knap tekst",
|
||||
"LabelByAuthor": "Efter Forfatter",
|
||||
"LabelChangePassword": "Ændre Adgangskode",
|
||||
"LabelButtonText": "Tekst på knap",
|
||||
"LabelByAuthor": "af {0}",
|
||||
"LabelChangePassword": "Ændr adgangskode",
|
||||
"LabelChannels": "Kanaler",
|
||||
"LabelChapterCount": "{0} Kapitler",
|
||||
"LabelChapterTitle": "Kapitel Titel",
|
||||
"LabelChapterCount": "{0} kapitler",
|
||||
"LabelChapterTitle": "Kapiteltitel",
|
||||
"LabelChapters": "Kapitler",
|
||||
"LabelChaptersFound": "fundne kapitler",
|
||||
"LabelChaptersFound": "kapitler fundet",
|
||||
"LabelClickForMoreInfo": "Klik for mere info",
|
||||
"LabelClickToUseCurrentValue": "Klik for at bruge nuværende værdi",
|
||||
"LabelClosePlayer": "Luk afspiller",
|
||||
"LabelCodec": "Kodeks",
|
||||
"LabelCollapseSeries": "Fold Serier Sammen",
|
||||
"LabelCodec": "Codec",
|
||||
"LabelCollapseSeries": "Fold serier sammen",
|
||||
"LabelCollapseSubSeries": "Fold underserie sammen",
|
||||
"LabelCollection": "Samling",
|
||||
"LabelCollections": "Samlinger",
|
||||
"LabelComplete": "Fuldfør",
|
||||
"LabelConfirmPassword": "Bekræft Adgangskode",
|
||||
"LabelComplete": "Afsluttet",
|
||||
"LabelConfirmPassword": "Bekræft adgangskode",
|
||||
"LabelContinueListening": "Fortsæt med at lytte",
|
||||
"LabelContinueReading": "Fortsæt med at læse",
|
||||
"LabelContinueSeries": "Fortsæt Serien",
|
||||
"LabelContinueSeries": "Fortsæt serien",
|
||||
"LabelCorsAllowed": "Tilladte CORS-oprindelser",
|
||||
"LabelCover": "Omslag",
|
||||
"LabelCoverImageURL": "Omslagsbillede URL",
|
||||
"LabelCoverProvider": "Cover billede udbyder",
|
||||
"LabelCreatedAt": "Oprettet d.",
|
||||
"LabelCronExpression": "Cron Udtryk",
|
||||
"LabelCoverImageURL": "Omslagsbilledes-URL",
|
||||
"LabelCoverProvider": "Kilde til omslagsbillede",
|
||||
"LabelCreatedAt": "Oprettet",
|
||||
"LabelCronExpression": "Cron-kommando",
|
||||
"LabelCurrent": "Aktuel",
|
||||
"LabelCurrently": "Aktuelt:",
|
||||
"LabelCustomCronExpression": "Brugerdefineret Cron Udtryk:",
|
||||
"LabelDatetime": "Dato og Tid",
|
||||
"LabelCurrently": "Nuværende:",
|
||||
"LabelCustomCronExpression": "Brugerdefineret Cron-kommando:",
|
||||
"LabelDatetime": "Dato og tid",
|
||||
"LabelDays": "Dage",
|
||||
"LabelDeleteFromFileSystemCheckbox": "Slet fra filsystem (afmarker kun for at fjerne fra databasen)",
|
||||
"LabelDeleteFromFileSystemCheckbox": "Slet fra filsystem (afmarkér for udelukkende at fjerne fra databasen)",
|
||||
"LabelDescription": "Beskrivelse",
|
||||
"LabelDeselectAll": "Fravælg Alle",
|
||||
"LabelDeselectAll": "Fravælg alle",
|
||||
"LabelDetectedPattern": "Identificeret mønster:",
|
||||
"LabelDevice": "Enheds",
|
||||
"LabelDevice": "Enhed",
|
||||
"LabelDeviceInfo": "Enhedsinformation",
|
||||
"LabelDeviceIsAvailableTo": "Enhed er tilgængelig for...",
|
||||
"LabelDeviceIsAvailableTo": "Enheden er tilgængelig for...",
|
||||
"LabelDirectory": "Mappe",
|
||||
"LabelDiscFromFilename": "Disk fra Filnavn",
|
||||
"LabelDiscFromMetadata": "Disk fra Metadata",
|
||||
"LabelDiscFromFilename": "Disk fra filnavn",
|
||||
"LabelDiscFromMetadata": "Disk fra metadata",
|
||||
"LabelDiscover": "Opdag",
|
||||
"LabelDownload": "Download",
|
||||
"LabelDownloadNEpisodes": "Download {0} episoder",
|
||||
"LabelDownloadable": "Downloadbar",
|
||||
"LabelDownload": "Hent",
|
||||
"LabelDownloadNEpisodes": "Hent {0} afsnit",
|
||||
"LabelDownloadable": "Kan hentes",
|
||||
"LabelDuration": "Varighed",
|
||||
"LabelDurationComparisonExactMatch": "(præcis match)",
|
||||
"LabelDurationComparisonExactMatch": "(præcist match)",
|
||||
"LabelDurationComparisonLonger": "({0} længere)",
|
||||
"LabelDurationComparisonShorter": "({0} kortere)",
|
||||
"LabelDurationFound": "Fundet varighed:",
|
||||
"LabelEbook": "E-bog",
|
||||
"LabelEbooks": "E-bøger",
|
||||
"LabelEdit": "Rediger",
|
||||
"LabelEmail": "E-mail",
|
||||
"LabelEmailSettingsFromAddress": "Fra Adresse",
|
||||
"LabelEmailSettingsRejectUnauthorized": "Afvis uautoriserede certifikater",
|
||||
"LabelEmailSettingsRejectUnauthorizedHelp": "Deaktivering af SSL certifikat validering kan udsætte din forbindelse for sikkerhedsrisici, eksempelvis man-in-the-middle angreb. Deaktiver kun denne indstilling hvis du forstår de potentielle implikationer og stoler på den mailserver du forbinder til.",
|
||||
"LabelEmailSettingsSecure": "Sikker",
|
||||
"LabelEmailSettingsSecureHelp": "Hvis sandt, vil forbindelsen bruge TLS ved tilslutning til serveren. Hvis falsk, bruges TLS, hvis serveren understøtter STARTTLS-udvidelsen. I de fleste tilfælde skal denne værdi sættes til sandt, hvis du tilslutter til port 465. Til port 587 eller 25 skal du holde det falsk. (fra nodemailer.com/smtp/#authentication)",
|
||||
"LabelEmailSettingsTestAddress": "Test Adresse",
|
||||
"LabelEmbeddedCover": "Indlejret Omslag",
|
||||
"LabelEmail": "Email",
|
||||
"LabelEmailSettingsFromAddress": "Afsenderadresse",
|
||||
"LabelEmailSettingsRejectUnauthorized": "Afvis ukendte sikkerhedscertifikater",
|
||||
"LabelEmailSettingsRejectUnauthorizedHelp": "Hvis du slår SSL-certifikatkontrollen fra, risikerer du at uvedkommende kan opsnappe dine data. Slå kun dette fra, hvis du er helt tryg ved situationen og har fuld tillid til den mailserver, du forbinder til.",
|
||||
"LabelEmailSettingsSecure": "Sikker forbindelse",
|
||||
"LabelEmailSettingsSecureHelp": "Når denne er slået til, bruger forbindelsen altid fuld kryptering (TLS). Hvis den er slået fra, bruges kryptering kun, hvis serveren tillader det (via STARTTLS). Lad dette være slået til, hvis du bruger port 465. Slå det fra, hvis du bruger port 587 eller 25",
|
||||
"LabelEmailSettingsTestAddress": "Modtageradresse til test",
|
||||
"LabelEmbeddedCover": "Indlejret omslag",
|
||||
"LabelEnable": "Aktivér",
|
||||
"LabelEncodingBackupLocation": "En sikkerhedskopi af dine originale lydfiler vil blive gemt under:",
|
||||
"LabelEncodingChaptersNotEmbedded": "Kapitler er ikke indlejret i multi spors lydbøger.",
|
||||
"LabelEncodingClearItemCache": "Sørg for periodisk at rense indholdscachen.",
|
||||
"LabelEncodingFinishedM4B": "Færdiggjort M4B som vil blive placeret i din lydbogsmappe ved:",
|
||||
"LabelEncodingInfoEmbedded": "Metadata vil blive indlejret i lydfiler i lydbogsmappen.",
|
||||
"LabelEncodingStartedNavigation": "Når opgaven er startet kan du navigere væk fra denne side.",
|
||||
"LabelEncodingTimeWarning": "Indkodning kan tage op til 30 minutter.",
|
||||
"LabelEncodingWarningAdvancedSettings": "Advarsel: Opdater ikke disse indstillinger med mindre du kender til ffmpeg indkodningsindstillinger.",
|
||||
"LabelEncodingWatcherDisabled": "Hvis du har watcheren deaktiveret skal du gen-scanne denne lydbog bagefter.",
|
||||
"LabelEncodingBackupLocation": "En sikkerhedskopi af dine originale lydfiler vil blive gemt i mappen:",
|
||||
"LabelEncodingChaptersNotEmbedded": "Kapitler bliver ikke indlejret i lydbøger som består af flere lydfiler.",
|
||||
"LabelEncodingClearItemCache": "Sørg for jævnligt at rense mellemlageret.",
|
||||
"LabelEncodingFinishedM4B": "Den færdige M4B-fil vil blive placeret i lydbogsmappen:",
|
||||
"LabelEncodingInfoEmbedded": "Metadata vil blive indlejret i lydfilerne i lydbogsmappen.",
|
||||
"LabelEncodingStartedNavigation": "Når opgaven er sat i gang kan du navigere væk fra denne side.",
|
||||
"LabelEncodingTimeWarning": "Omkodningen kan tage op til 30 minutter.",
|
||||
"LabelEncodingWarningAdvancedSettings": "Advarsel: Ændr ikke disse indstillinger med mindre du kender til ffmpeg-indkodningsindstillinger.",
|
||||
"LabelEncodingWatcherDisabled": "Hvis du har slået automatisk biblioteksovervågning fra skal du genskanne denne lydbog bagefter.",
|
||||
"LabelEnd": "Slut",
|
||||
"LabelEndOfChapter": "Slutningen af kapitel",
|
||||
"LabelEpisode": "Afsnit",
|
||||
"LabelEpisodeNotLinkedToRssFeed": "Afsnit er ikke koblet til RSS feed",
|
||||
"LabelEpisodeNotLinkedToRssFeed": "Afsnit er ikke koblet til RSS-feed",
|
||||
"LabelEpisodeNumber": "Afsnit #{0}",
|
||||
"LabelEpisodeTitle": "Episodetitel",
|
||||
"LabelEpisodeType": "Episodetype",
|
||||
"LabelEpisodeUrlFromRssFeed": "Afsnit URL fra RSS feed",
|
||||
"LabelEpisodeTitle": "Afsnitstitel",
|
||||
"LabelEpisodeType": "Afsnitstype",
|
||||
"LabelEpisodeUrlFromRssFeed": "Afsnits-URL fra RSS-feed",
|
||||
"LabelEpisodes": "Afsnit",
|
||||
"LabelEpisodic": "Afsnit",
|
||||
"LabelEpisodic": "Episodisk",
|
||||
"LabelExample": "Eksempel",
|
||||
"LabelExpandSeries": "Udfold serie",
|
||||
"LabelExpandSubSeries": "Udfold underserie",
|
||||
"LabelExpandSeries": "Fold serie ud",
|
||||
"LabelExpandSubSeries": "Fold underserie ud",
|
||||
"LabelExpired": "Udløbet",
|
||||
"LabelExpiresAt": "Udløbsdato",
|
||||
"LabelExpiresInSeconds": "Udløber om (seconds)",
|
||||
"LabelExpiresNever": "Aldrig",
|
||||
"LabelExplicit": "Eksplisit",
|
||||
"LabelExplicitChecked": "Eksplicit (markeret)",
|
||||
"LabelExplicitUnchecked": "Ikke eksplicit (ikke markeret)",
|
||||
"LabelExplicit": "Voksenindhold",
|
||||
"LabelExplicitChecked": "Voksenindhold (markeret)",
|
||||
"LabelExplicitUnchecked": "Ikke voksenindhold (ikke markeret)",
|
||||
"LabelExportOPML": "Eksport OPML",
|
||||
"LabelFeedURL": "Feed URL",
|
||||
"LabelFeedURL": "Feed-URL",
|
||||
"LabelFetchingMetadata": "Henter metadata",
|
||||
"LabelFile": "Fil",
|
||||
"LabelFileBirthtime": "Oprettelsestidspunkt for fil",
|
||||
@@ -391,7 +391,7 @@
|
||||
"LabelFull": "Fuld",
|
||||
"LabelGenre": "Genre",
|
||||
"LabelGenres": "Genrer",
|
||||
"LabelHardDeleteFile": "Permanent slet fil",
|
||||
"LabelHardDeleteFile": "Slet fil permanent",
|
||||
"LabelHasEbook": "Har e-bog",
|
||||
"LabelHasSupplementaryEbook": "Har supplerende e-bog",
|
||||
"LabelHideSubtitles": "Skjul undertitler",
|
||||
@@ -400,7 +400,7 @@
|
||||
"LabelHour": "Time",
|
||||
"LabelHours": "Timer",
|
||||
"LabelIcon": "Ikon",
|
||||
"LabelImageURLFromTheWeb": "Billede URL fra nettet",
|
||||
"LabelImageURLFromTheWeb": "Billede-URL fra nettet",
|
||||
"LabelInProgress": "I gang",
|
||||
"LabelIncludeInTracklist": "Inkluder i afspilningsliste",
|
||||
"LabelIncomplete": "Ufuldstændig",
|
||||
@@ -414,22 +414,22 @@
|
||||
"LabelIntervalEveryDay": "Hver dag",
|
||||
"LabelIntervalEveryHour": "Hver time",
|
||||
"LabelIntervalEveryMinute": "Hvert minut",
|
||||
"LabelInvert": "Inverter",
|
||||
"LabelInvert": "Omvendt begrænsning",
|
||||
"LabelItem": "Element",
|
||||
"LabelJumpBackwardAmount": "Spring bagud mængde",
|
||||
"LabelJumpForwardAmount": "Spring fremad mængde",
|
||||
"LabelJumpBackwardAmount": "Tilbagespolingstid",
|
||||
"LabelJumpForwardAmount": "Fremadspolingstid",
|
||||
"LabelLanguage": "Sprog",
|
||||
"LabelLanguageDefaultServer": "Standard server sprog",
|
||||
"LabelLanguageDefaultServer": "Serverens standardsprog",
|
||||
"LabelLanguages": "Sprog",
|
||||
"LabelLastBookAdded": "Senest tilføjede bog",
|
||||
"LabelLastBookUpdated": "Senest opdaterede bog",
|
||||
"LabelLastProgressDate": "Sidste fremgang: {0}",
|
||||
"LabelLastProgressDate": "Seneste fremgang: {0}",
|
||||
"LabelLastSeen": "Sidst set",
|
||||
"LabelLastTime": "Sidste gang",
|
||||
"LabelLastUpdate": "Seneste opdatering",
|
||||
"LabelLayout": "Layout",
|
||||
"LabelLastTime": "Sidst lyttet",
|
||||
"LabelLastUpdate": "Sidst ændret",
|
||||
"LabelLayout": "Udseende",
|
||||
"LabelLayoutSinglePage": "Enkeltside",
|
||||
"LabelLayoutSplitPage": "Opdelt side",
|
||||
"LabelLayoutSplitPage": "Opdelte sider",
|
||||
"LabelLess": "Mindre",
|
||||
"LabelLibrariesAccessibleToUser": "Biblioteker tilgængelige for bruger",
|
||||
"LabelLibrary": "Bibliotek",
|
||||
@@ -445,14 +445,14 @@
|
||||
"LabelLogLevelDebug": "Fejlsøgning",
|
||||
"LabelLogLevelInfo": "Information",
|
||||
"LabelLogLevelWarn": "Advarsel",
|
||||
"LabelLookForNewEpisodesAfterDate": "Søg efter nye episoder efter denne dato",
|
||||
"LabelLookForNewEpisodesAfterDate": "Søg efter nye afsnit efter denne dato",
|
||||
"LabelLowestPriority": "Laveste prioritet",
|
||||
"LabelMatchConfidence": "Confidens",
|
||||
"LabelMatchExistingUsersBy": "Match eksisterende brugere ved",
|
||||
"LabelMatchExistingUsersByDescription": "Anvendt for at forbinde brugere. Når forbundet, brugere vil blive matchet ved unikt id fra din SSO udbyder",
|
||||
"LabelMaxEpisodesToDownload": "Max # afsnit for at downloade. Anvend 0 for ubegrænset.",
|
||||
"LabelMaxEpisodesToDownloadPerCheck": "Max # afsnit til at downloade per check",
|
||||
"LabelMaxEpisodesToKeep": "Max # afsnit at beholde",
|
||||
"LabelMatchConfidence": "Nøjagtighed",
|
||||
"LabelMatchExistingUsersBy": "Forbind eksisterende brugere med",
|
||||
"LabelMatchExistingUsersByDescription": "Anvendes til at tilslutte eksisterende brugere. Ved tilslutningen kobles brugeren til et unikt ID fra din SSO-udbyder",
|
||||
"LabelMaxEpisodesToDownload": "Maksimalt antal afsnit som skal hentes. Anvend 0 for ubegrænset.",
|
||||
"LabelMaxEpisodesToDownloadPerCheck": "Maksimalt antal afsnit som skal hentes per tjek",
|
||||
"LabelMaxEpisodesToKeep": "Maksimalt antal afsnit som skal beholdes",
|
||||
"LabelMaxEpisodesToKeepHelp": "Værdi af 0 sætter intet maks begrænsning. After et nyt afsnit er automatisk downloaded vil det ældste afsnit blive slettet hvis du har mere end X afsnit. Dette vil kun slette 1 afsnit for hvert nye download.",
|
||||
"LabelMediaPlayer": "Medieafspiller",
|
||||
"LabelMediaType": "Medietype",
|
||||
@@ -463,20 +463,20 @@
|
||||
"LabelMinute": "Minut",
|
||||
"LabelMinutes": "Minutter",
|
||||
"LabelMissing": "Mangler",
|
||||
"LabelMissingEbook": "Har ingen ebog",
|
||||
"LabelMissingSupplementaryEbook": "Har ingen tillægsbog",
|
||||
"LabelMissingEbook": "Har ingen e-bog",
|
||||
"LabelMissingSupplementaryEbook": "Har ingen supplerende e-bog",
|
||||
"LabelMobileRedirectURIs": "Godkendte mobil redirect URI'er",
|
||||
"LabelMobileRedirectURIsDescription": "Dete vil whiteliste en gyldig omdirigerings URL for mobile apps. Den standarde er <code>audiobookshelf://oauth</code> som du kan fjerne eller supplere med flere URI'er for tredjeparts app integration. Anvend en stjerne (<code>*</code>) som den eneste indstilling for at tilade en hvilkensomhelst URI.",
|
||||
"LabelMore": "Mere",
|
||||
"LabelMoreInfo": "Mere info",
|
||||
"LabelName": "Navn",
|
||||
"LabelNarrator": "Fortæller",
|
||||
"LabelNarrators": "Fortællere",
|
||||
"LabelNarrator": "Oplæser",
|
||||
"LabelNarrators": "Oplæsere",
|
||||
"LabelNew": "Ny",
|
||||
"LabelNewPassword": "Ny adgangskode",
|
||||
"LabelNewestAuthors": "Nyeste forfattere",
|
||||
"LabelNewestEpisodes": "Nyeste episoder",
|
||||
"LabelNextBackupDate": "Næste sikkerhedskopi dato",
|
||||
"LabelNewestEpisodes": "Nyeste afsnit",
|
||||
"LabelNextBackupDate": "Dato for næste sikkerhedskopi",
|
||||
"LabelNextChapters": "Næste kapitler vil være:",
|
||||
"LabelNextScheduledRun": "Næste planlagte kørsel",
|
||||
"LabelNoApiKeys": "Ingen API-nøgler",
|
||||
@@ -526,28 +526,28 @@
|
||||
"LabelPodcasts": "Podcast",
|
||||
"LabelPort": "Port",
|
||||
"LabelPrefixesToIgnore": "Præfikser der skal ignoreres (skal ikke skelne mellem store og små bogstaver)",
|
||||
"LabelPreventIndexing": "Forhindrer, at dit feed bliver indekseret af iTunes og Google podcastkataloger",
|
||||
"LabelPreventIndexing": "Forhindrer at dit feed bliver indekseret af iTunes- og Google-podcastkataloger",
|
||||
"LabelPrimaryEbook": "Primær e-bog",
|
||||
"LabelProgress": "Fremskridt",
|
||||
"LabelProvider": "Udbyder",
|
||||
"LabelProviderAuthorizationValue": "Authorization Header værdi",
|
||||
"LabelPubDate": "Udgivelsesdato",
|
||||
"LabelPublishYear": "Udgivelsesår",
|
||||
"LabelPublishedDate": "Publiceret {0}",
|
||||
"LabelPublishedDecade": "Publiceret årti",
|
||||
"LabelPublishedDecades": "Publiceret årtier",
|
||||
"LabelPublishedDate": "Udgivet {0}",
|
||||
"LabelPublishedDecade": "Udgivelsesårti",
|
||||
"LabelPublishedDecades": "Udgivelsesårtier",
|
||||
"LabelPublisher": "Forlag",
|
||||
"LabelPublishers": "Forlag",
|
||||
"LabelRSSFeedCustomOwnerEmail": "Brugerdefineret ejerens e-mail",
|
||||
"LabelRSSFeedCustomOwnerName": "Brugerdefineret ejerens navn",
|
||||
"LabelRSSFeedOpen": "RSS-feed åbent",
|
||||
"LabelRSSFeedPreventIndexing": "Forhindrer indeksering",
|
||||
"LabelRSSFeedPreventIndexing": "Forhindr indeksering",
|
||||
"LabelRSSFeedSlug": "RSS-feed-slug",
|
||||
"LabelRSSFeedURL": "RSS-feed-URL",
|
||||
"LabelRandomly": "Tilfældigt",
|
||||
"LabelReAddSeriesToContinueListening": "Gentilføj serier til Fortsæt Lytning",
|
||||
"LabelReAddSeriesToContinueListening": "Gentilføj serier til »Fortsæt med at lytte«",
|
||||
"LabelRead": "Læst",
|
||||
"LabelReadAgain": "Læs Igen",
|
||||
"LabelReadAgain": "Læs igen",
|
||||
"LabelReadEbookWithoutProgress": "Læs e-bog uden at følge fremskridt",
|
||||
"LabelRecentSeries": "Seneste serier",
|
||||
"LabelRecentlyAdded": "Senest tilføjet",
|
||||
@@ -555,9 +555,9 @@
|
||||
"LabelRedo": "Gøre igen",
|
||||
"LabelRegion": "Region",
|
||||
"LabelReleaseDate": "Udgivelsesdato",
|
||||
"LabelRemoveAllMetadataAbs": "Fjern alle metadata.abs filer",
|
||||
"LabelRemoveAllMetadataJson": "Fjern alle metadata.json filer",
|
||||
"LabelRemoveAudibleBranding": "Fjern Audible intro og outro fra kapitler",
|
||||
"LabelRemoveAllMetadataAbs": "Fjern alle \"metadata.abs\"-filer",
|
||||
"LabelRemoveAllMetadataJson": "Fjern alle \"metadata.json\"-filer",
|
||||
"LabelRemoveAudibleBranding": "Fjern Audible-intro og -outro fra kapitler",
|
||||
"LabelRemoveCover": "Fjern omslag",
|
||||
"LabelRemoveMetadataFile": "Fjern alle metadata filer i biblioteksmapper",
|
||||
"LabelRemoveMetadataFileHelp": "Fjern alle metadata.json og metadata.abs filer i dine {0} mapper.",
|
||||
@@ -571,7 +571,7 @@
|
||||
"LabelSelectAllEpisodes": "Vælg alle episoder",
|
||||
"LabelSelectEpisodesShowing": "Vælg {0} episoder vist",
|
||||
"LabelSelectUser": "Vælg bruger",
|
||||
"LabelSelectUsers": "Valgte brugere",
|
||||
"LabelSelectUsers": "Vælg brugere",
|
||||
"LabelSendEbookToDevice": "Send e-bog til...",
|
||||
"LabelSequence": "Sekvens",
|
||||
"LabelSerial": "Seriel",
|
||||
@@ -582,21 +582,21 @@
|
||||
"LabelServerYearReview": "Server år i review ({0})",
|
||||
"LabelSetEbookAsPrimary": "Indstil som primær",
|
||||
"LabelSetEbookAsSupplementary": "Indstil som supplerende",
|
||||
"LabelSettingsAllowIframe": "Tillad embedding i en iframe",
|
||||
"LabelSettingsAllowIframe": "Tillad indlejring i en iframe",
|
||||
"LabelSettingsAudiobooksOnly": "Kun lydbøger",
|
||||
"LabelSettingsAudiobooksOnlyHelp": "Aktivering af denne indstilling vil ignorere e-bogsfiler, medmindre de er inde i en lydbogmappe, hvor de vil blive indstillet som supplerende e-bøger",
|
||||
"LabelSettingsBookshelfViewHelp": "Skeumorfisk design med træhylder",
|
||||
"LabelSettingsChromecastSupport": "Chromecast-understøttelse",
|
||||
"LabelSettingsDateFormat": "Datoformat",
|
||||
"LabelSettingsEnableWatcher": "Automatisk biblioteksovervåger",
|
||||
"LabelSettingsEnableWatcher": "Automatisk biblioteksovervågning",
|
||||
"LabelSettingsEnableWatcherForLibrary": "Automatisk biblioteksovervåger",
|
||||
"LabelSettingsEnableWatcherHelp": "Aktiverer automatisk tilføjelse/opdatering af elementer, når filændringer registreres. *Kræver servergenstart",
|
||||
"LabelSettingsEnableWatcherHelp": "Aktiverer automatisk opdatering af kataloget, når filændringer registreres. *Kræver genstart af serveren",
|
||||
"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.",
|
||||
"LabelSettingsExperimentalFeatures": "Eksperimentelle funktioner",
|
||||
"LabelSettingsExperimentalFeaturesHelp": "Funktioner under udvikling, der kunne bruge din feedback og hjælp til test. Klik for at åbne Github-diskussionen.",
|
||||
"LabelSettingsFindCovers": "Find omslag",
|
||||
"LabelSettingsFindCoversHelp": "Hvis din lydbog ikke har et indlejret omslag eller et omslagsbillede i mappen, vil skanneren forsøge at finde et omslag.<br>Bemærk: Dette vil forlænge scanntiden",
|
||||
"LabelSettingsFindCoversHelp": "Hvis din lydbog ikke har et indlejret omslag eller et omslagsbillede i mappen, vil skanneren forsøge at finde et omslag.<br>Bemærk: Dette vil forlænge skanningens varighed",
|
||||
"LabelSettingsHideSingleBookSeries": "Skjul enkeltbogsserier",
|
||||
"LabelSettingsHideSingleBookSeriesHelp": "Serier med en enkelt bog vil blive skjult fra serie-siden og hjemmesidehylder.",
|
||||
"LabelSettingsHomePageBookshelfView": "Brug bogreolvisning på startside",
|
||||
@@ -607,7 +607,7 @@
|
||||
"LabelSettingsOnlyShowLaterBooksInContinueSeries": "Spring tidligere bøger i Fortsæt serie over",
|
||||
"LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "Fortsæt Serien siden viser den første bog som ikke er startet i serier med mindst en bog som ikke er startet og hvor ingen bøger i gang. Aktivering af denne indstilling vil fortsætte serien fra den sidst gennemførte bog i stedet for fra den første ikke startede bog.",
|
||||
"LabelSettingsParseSubtitles": "Fortolk undertitler",
|
||||
"LabelSettingsParseSubtitlesHelp": "Udtræk undertekster fra lydbogsmappenavne.<br>Undertitler skal adskilles af \" - \"<br>f.eks. \"Bogtitel - En undertitel her\" har undertitlen \"En undertitel her\"",
|
||||
"LabelSettingsParseSubtitlesHelp": "Udtræk undertitler fra lydbogsmappenavne.<br>Undertitler skal adskilles af \" - \"<br>f.eks. \"Bogtitel - En undertitel her\" har undertitlen \"En undertitel her\"",
|
||||
"LabelSettingsPreferMatchedMetadata": "Foretræk matchede metadata",
|
||||
"LabelSettingsPreferMatchedMetadataHelp": "Matchede data vil tilsidesætte elementdetaljer ved brug af Hurtig Match. Som standard udfylder Hurtig Match kun manglende detaljer.",
|
||||
"LabelSettingsSkipMatchingBooksWithASIN": "Ignorer matchende bøger, der allerede har en ASIN",
|
||||
@@ -647,14 +647,14 @@
|
||||
"LabelStatsDaysListened": "Dage hørt",
|
||||
"LabelStatsHours": "Timer",
|
||||
"LabelStatsInARow": "i træk",
|
||||
"LabelStatsItemsFinished": "Elementer færdige",
|
||||
"LabelStatsItemsInLibrary": "Elementer i biblioteket",
|
||||
"LabelStatsItemsFinished": "færdiglæste bøger",
|
||||
"LabelStatsItemsInLibrary": "bøger i biblioteket",
|
||||
"LabelStatsMinutes": "minutter",
|
||||
"LabelStatsMinutesListening": "Minutter hørt",
|
||||
"LabelStatsOverallDays": "Samlede dage",
|
||||
"LabelStatsOverallHours": "Samlede timer",
|
||||
"LabelStatsWeekListening": "Ugens lytning",
|
||||
"LabelSubtitle": "Undertekst",
|
||||
"LabelSubtitle": "Undertitel",
|
||||
"LabelSupportedFileTypes": "Understøttede filtyper",
|
||||
"LabelTag": "Mærke",
|
||||
"LabelTags": "Mærker",
|
||||
@@ -663,7 +663,7 @@
|
||||
"LabelTasks": "Kører opgaver",
|
||||
"LabelTextEditorBulletedList": "Punktopstilling",
|
||||
"LabelTextEditorLink": "Link",
|
||||
"LabelTextEditorNumberedList": "Nummeropstilling",
|
||||
"LabelTextEditorNumberedList": "Nummereret liste",
|
||||
"LabelTextEditorUnlink": "Aflink",
|
||||
"LabelTheme": "Tema",
|
||||
"LabelThemeDark": "Mørk",
|
||||
@@ -735,17 +735,17 @@
|
||||
"LabelYourPlaylists": "Dine spillelister",
|
||||
"LabelYourProgress": "Din fremgang",
|
||||
"MessageAddToPlayerQueue": "Tilføj til afspilningskø",
|
||||
"MessageAppriseDescription": "For at bruge denne funktion skal du have en instans af <a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">Apprise API</a> kørende eller en API, der håndterer de samme anmodninger. <br /> Apprise API-webadressen skal være den fulde URL-sti for at sende underretningen, f.eks. hvis din API-instans er tilgængelig på <code>http://192.168.1.1:8337</code>, så skal du bruge <code>http://192.168.1.1:8337/notify</code>.",
|
||||
"MessageAppriseDescription": "For at bruge denne funktion skal du have en instans af <a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">Apprise API</a> kørende eller en API, der kan håndtere de samme anmodninger. <br />Apprise API-webadressen skal være den fulde URL-sti for at sende underretningen, f.eks. hvis din API-instans er tilgængelig på <code>http://192.168.1.1:8337</code>, så skal du bruge <code>http://192.168.1.1:8337/notify</code>.",
|
||||
"MessageAsinCheck": "Sikr dig at du bruger ASIN fra den korrekte Audible region, ikke Amazon.",
|
||||
"MessageAuthenticationLegacyTokenWarning": "Ældre API tokens vil blive fjernet i fremtiden. Brug <a href=\"/config/api-keys\">API-nøgler</a> i stedet.",
|
||||
"MessageAuthenticationOIDCChangesRestart": "Genstart sin server efter du har gemt for at bekræfte OIDC ændringer.",
|
||||
"MessageAuthenticationSecurityMessage": "Autentificeringen er blevet forbedret af sikkerhedsmæssige årsager. Alle brugere skal logge ind igen.",
|
||||
"MessageAuthenticationSecurityMessage": "Brugerlogin er blevet forbedret af sikkerhedsmæssige årsager. Alle brugere skal logge ind igen.",
|
||||
"MessageBackupsDescription": "Backups inkluderer brugere, brugerfremskridt, biblioteksvareoplysninger, serverindstillinger og billeder gemt i <code>/metadata/items</code> og <code>/metadata/authors</code>. Backups inkluderer <strong>ikke</strong> nogen filer gemt i dine biblioteksmapper.",
|
||||
"MessageBackupsLocationEditNote": "Note: Opdatering af backup sti vil ikke fjerne eller modificere eksisterende backups",
|
||||
"MessageBackupsLocationNoEditNote": "Note: Backup sti er sat igennem miljøvariabel og kan ikke ændres her.",
|
||||
"MessageBackupsLocationPathEmpty": "Backup sti kan ikke være tom",
|
||||
"MessageBatchEditPopulateMapDetailsAllHelp": "Opret felter slået til med data fra alle genstande. Felter med flere værdier vil blive sammenflettet",
|
||||
"MessageBatchEditPopulateMapDetailsItemHelp": "Opret kort med værdier der er slået til fra felter med data fra denne genstand",
|
||||
"MessageBatchEditPopulateMapDetailsAllHelp": "Udfyld valgte felter med data fra alle bøger. Felter med flere værdier vil blive sammenflettet",
|
||||
"MessageBatchEditPopulateMapDetailsItemHelp": "Udfyld de felter, som er tilvalgt i masseredigering med data fra denne bog",
|
||||
"MessageBatchQuickMatchDescription": "Quick Match vil forsøge at tilføje manglende omslag og metadata til de valgte elementer. Aktivér indstillingerne nedenfor for at tillade Quick Match at overskrive eksisterende omslag og/eller metadata.",
|
||||
"MessageBookshelfNoCollections": "Du har ikke oprettet nogen samlinger endnu",
|
||||
"MessageBookshelfNoCollectionsHelp": "Samlinger er offentlige. Alle brugere med adgang til biblioteket kan se dem.",
|
||||
@@ -888,7 +888,7 @@
|
||||
"MessageResetChaptersConfirm": "Er du sikker på, at du vil nulstille kapitler og annullere ændringerne, du har foretaget?",
|
||||
"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, og lade biblioteksovervågeren være aktiveret - den vil automatisk opdage ændringer i dine biblioteksmapper. Aktiver denne funktion, hvis biblioteksovervågeren ikke virker med dit filsystem (f. eks. NFS).",
|
||||
"MessageScheduleLibraryScanNote": "For de fleste brugere er det anbefalet, at lade denne funktion være deaktiveret, og lade biblioteksovervågning være aktiveret - den vil automatisk opdage ændringer i dine biblioteksmapper. Aktiver denne funktion, hvis biblioteksovervågning ikke virker med dit filsystem (f. eks. NFS).",
|
||||
"MessageScheduleRunEveryWeekdayAtTime": "Kør hvert {0} af {1}",
|
||||
"MessageSearchResultsFor": "Søgeresultater for",
|
||||
"MessageSelected": "{0} valgt",
|
||||
@@ -915,7 +915,7 @@
|
||||
"MessageTaskFailedToWriteMetadataFile": "Fejlede i at skrive metadata fil",
|
||||
"MessageTaskMatchingBooksInLibrary": "Matchede bøger i bibliotek \"{0}\"",
|
||||
"MessageTaskNoFilesToScan": "Ingen filer at scanne",
|
||||
"MessageTaskOpmlImport": "OPML import",
|
||||
"MessageTaskOpmlImport": "OPML-import",
|
||||
"MessageTaskOpmlImportDescription": "Oprettelse af podcasts fra {0} RSS feeds",
|
||||
"MessageTaskOpmlImportFeed": "OPML importering fejlede",
|
||||
"MessageTaskOpmlImportFeedDescription": "Importering af RSS feed \"{0}\"",
|
||||
@@ -939,13 +939,13 @@
|
||||
"MessageUploaderItemSuccess": "Uploadet med succes!",
|
||||
"MessageUploading": "Uploader...",
|
||||
"MessageValidCronExpression": "Gyldigt cron-udtryk",
|
||||
"MessageWatcherIsDisabledGlobally": "Watcher er deaktiveret globalt i serverindstillinger",
|
||||
"MessageWatcherIsDisabledGlobally": "Automatisk biblioteksovervågning er deaktiveret globalt i serverens indstillinger",
|
||||
"MessageXLibraryIsEmpty": "{0} bibliotek er tomt!",
|
||||
"MessageYourAudiobookDurationIsLonger": "Din lydbogsvarighed er længere end den fundne varighed",
|
||||
"MessageYourAudiobookDurationIsShorter": "Din lydbogsvarighed er kortere end den fundne varighed",
|
||||
"NoteChangeRootPassword": "Root-brugeren er den eneste bruger, der kan have en tom adgangskode",
|
||||
"NoteChapterEditorTimes": "Bemærk: Første kapitel starttidspunkt skal forblive kl. 0:00, og det sidste kapitel starttidspunkt må ikke overstige denne lydbogs varighed.",
|
||||
"NoteFolderPicker": "Bemærk: Mapper, der allerede er mappet, vises ikke",
|
||||
"NoteFolderPicker": "Bemærk: Allerede kortlagte mapper vises ikke",
|
||||
"NoteRSSFeedPodcastAppsHttps": "Advarsel: De fleste podcast-apps kræver, at RSS-feedets URL bruger HTTPS",
|
||||
"NoteRSSFeedPodcastAppsPubDate": "Advarsel: En eller flere af dine episoder har ikke en Pub Date. Nogle podcast-apps kræver dette.",
|
||||
"NoteUploaderFoldersWithMediaFiles": "Mapper med mediefiler håndteres som separate bibliotekselementer.",
|
||||
@@ -1018,8 +1018,8 @@
|
||||
"ToastChapterStartTimeAdjusted": "Kapitelstarttid justeret med {0} sekunder",
|
||||
"ToastChaptersAllLocked": "Alle kapitler er låst. Lås op for nogle kapitler for at ændre deres tider.",
|
||||
"ToastChaptersHaveErrors": "Kapitler har fejl",
|
||||
"ToastChaptersInvalidShiftAmountLast": "Ugyldig ændring. Det sidste kapitels starttid ville fortsætte længere end varigheden på denne lydbog.",
|
||||
"ToastChaptersInvalidShiftAmountStart": "Ugyldig ændring. Første kapitel ville have en længde på nul eller negativt og ville blive overskrevet af andet kapitel. Udvid startvarigheden på andet kapitel.",
|
||||
"ToastChaptersInvalidShiftAmountLast": "Forskydningen er ikke mulig. Det sidste kapitels starttidspunkt ville strække sig ud over denne lydbogs varighed.",
|
||||
"ToastChaptersInvalidShiftAmountStart": "Forskydningen er ikke mulig. Det første kapitel ville have en længde på nul eller negativ længde og ville blive overskrevet af det andet kapitel. Øg startvarigheden af det andet kapitel.",
|
||||
"ToastChaptersMustHaveTitles": "Kapitler skal have titler",
|
||||
"ToastChaptersRemoved": "Kapitler fjernet",
|
||||
"ToastChaptersUpdated": "Kapitler opdateret",
|
||||
@@ -1044,7 +1044,7 @@
|
||||
"ToastEpisodeUpdateSuccess": "{0} afsnit opdateret",
|
||||
"ToastErrorCannotShare": "Kan ikke dele på denne enhed",
|
||||
"ToastFailedToCreate": "Oprettelsen mislykkedes",
|
||||
"ToastFailedToDelete": "Sletning fejlede",
|
||||
"ToastFailedToDelete": "Kunne ikke slette",
|
||||
"ToastFailedToLoadData": "Indlæsning af data fejlede",
|
||||
"ToastFailedToMatch": "Fejlet match",
|
||||
"ToastFailedToShare": "Deling fejlede",
|
||||
@@ -1109,7 +1109,7 @@
|
||||
"ToastProgressIsNotBeingSynced": "Fremskridt ikke synkroniseret, genstart afspilning",
|
||||
"ToastProviderCreatedFailed": "Fejlede at tilføje udbyder",
|
||||
"ToastProviderCreatedSuccess": "Ny udbyder tilføjet",
|
||||
"ToastProviderNameAndUrlRequired": "Navn og URL påkrævet",
|
||||
"ToastProviderNameAndUrlRequired": "Navn og URL kræves",
|
||||
"ToastProviderRemoveSuccess": "Udbyder fjernet",
|
||||
"ToastRSSFeedCloseFailed": "Mislykkedes lukning af RSS-feed",
|
||||
"ToastRSSFeedCloseSuccess": "RSS-feed lukket",
|
||||
|
||||
+12
-8
@@ -46,6 +46,7 @@
|
||||
"ButtonLatest": "Neueste",
|
||||
"ButtonLibrary": "Bibliothek",
|
||||
"ButtonLogout": "Abmelden",
|
||||
"ButtonLogoutAllDevices": "Von allen Geräte abmelden",
|
||||
"ButtonLookup": "Online-Suche",
|
||||
"ButtonManageTracks": "Tracks verwalten",
|
||||
"ButtonMapChapterTitles": "Kapitelüberschriften zuordnen",
|
||||
@@ -116,7 +117,7 @@
|
||||
"ButtonViewAll": "Alles anzeigen",
|
||||
"ButtonYes": "Ja",
|
||||
"ErrorUploadFetchMetadataAPI": "Fehler beim Abrufen der Metadaten",
|
||||
"ErrorUploadFetchMetadataNoResults": "Metadaten konnten nicht abgerufen werden. Versuche, den Titel und/oder den Autor zu aktualisieren.",
|
||||
"ErrorUploadFetchMetadataNoResults": "Metadaten konnten nicht abgerufen werden - versuche den Titel und/oder den Autor zu aktualisieren",
|
||||
"ErrorUploadLacksTitle": "Es muss ein Titel eingegeben werden",
|
||||
"HeaderAccount": "Konto",
|
||||
"HeaderAddCustomMetadataProvider": "Benutzerdefinierten Metadatenanbieter hinzufügen",
|
||||
@@ -139,12 +140,12 @@
|
||||
"HeaderCustomMetadataProviders": "Benutzerdefinierte Metadatenanbieter",
|
||||
"HeaderDetails": "Details",
|
||||
"HeaderDownloadQueue": "Download-Warteschlange",
|
||||
"HeaderEbookFiles": "E-Book-Dateien",
|
||||
"HeaderEbookFiles": "Buch-Dateien",
|
||||
"HeaderEmail": "E-Mail",
|
||||
"HeaderEmailSettings": "E-Mail-Einstellungen",
|
||||
"HeaderEpisodes": "Episoden",
|
||||
"HeaderEreaderDevices": "E-Reader Geräte",
|
||||
"HeaderEreaderSettings": "E-Reader-Einstellungen",
|
||||
"HeaderEreaderSettings": "E-Reader Einstellungen",
|
||||
"HeaderFiles": "Dateien",
|
||||
"HeaderFindChapters": "Kapitel suchen",
|
||||
"HeaderIgnoredFiles": "Ignorierte Dateien",
|
||||
@@ -194,6 +195,7 @@
|
||||
"HeaderScheduleEpisodeDownloads": "Automatische Episoden-Downloads planen",
|
||||
"HeaderScheduleLibraryScans": "Automatische Bibliotheksscans",
|
||||
"HeaderSession": "Sitzung",
|
||||
"HeaderSessions": "Sitzungen",
|
||||
"HeaderSetBackupSchedule": "Zeitplan für die Datensicherung festlegen",
|
||||
"HeaderSettings": "Einstellungen",
|
||||
"HeaderSettingsDisplay": "Anzeige",
|
||||
@@ -331,7 +333,7 @@
|
||||
"LabelEmail": "E-Mail",
|
||||
"LabelEmailSettingsFromAddress": "Sender",
|
||||
"LabelEmailSettingsRejectUnauthorized": "Nicht autorisierte Zertifikate ablehnen",
|
||||
"LabelEmailSettingsRejectUnauthorizedHelp": "Durch das Deaktivieren der SSL-Zertifikatsüberprüfung kann deine Verbindung Sicherheitsrisiken wie Man-in-the-Middle-Angriffen ausgesetzt sein. Deaktiviere diese Option nur, wenn due die Auswirkungen verstehst und dem E-Mail-Server vertraust, mit dem eine Verbindung hergestellt wird.",
|
||||
"LabelEmailSettingsRejectUnauthorizedHelp": "Durch das Deaktivieren der SSL-Zertifikatsüberprüfung kann deine Verbindung Sicherheitsrisiken wie Man-in-the-Middle-Angriffen ausgesetzt sein. Deaktiviere diese Option nur, wenn du die Auswirkungen verstehst und dem E-Mail-Server vertraust, mit dem eine Verbindung hergestellt wird.",
|
||||
"LabelEmailSettingsSecure": "Sicher",
|
||||
"LabelEmailSettingsSecureHelp": "Wenn an, verwendet die Verbindung TLS, wenn du eine Verbindung zum Server herstellst. Bei „aus“ wird TLS verwendet, wenn der Server die STARTTLS-Erweiterung unterstützt. In den meisten Fällen solltest du diesen Wert auf „an“ schalten, wenn du eine Verbindung zu Port 465 herstellst. Für Port 587 oder 25 behalte den Wert „aus“ bei. (von nodemailer.com/smtp/#authentication)",
|
||||
"LabelEmailSettingsTestAddress": "Test-Adresse",
|
||||
@@ -415,6 +417,7 @@
|
||||
"LabelIntervalEveryHour": "Jede Stunde",
|
||||
"LabelIntervalEveryMinute": "Jede Minute",
|
||||
"LabelInvert": "Umkehren",
|
||||
"LabelIpAddress": "IP-Adresse",
|
||||
"LabelItem": "Medium",
|
||||
"LabelJumpBackwardAmount": "Zurückspringen Zeit",
|
||||
"LabelJumpForwardAmount": "Vorwärtsspringen Zeit",
|
||||
@@ -622,7 +625,7 @@
|
||||
"LabelSettingsStoreMetadataWithItemHelp": "Standardmäßig werden die Metadaten in /metadata/items gespeichert. Wenn diese Option aktiviert ist, werden die Metadaten als OPF-Datei (Textdatei) in dem gleichen Ordner gespeichert in welchem sich auch das Medium befindet",
|
||||
"LabelSettingsTimeFormat": "Zeitformat",
|
||||
"LabelShare": "Freigeben",
|
||||
"LabelShareDownloadableHelp": "Erlaubt es einem Nutzer, mit dem Link, die Dateien des Mediums als ZIP herunterzuladen.",
|
||||
"LabelShareDownloadableHelp": "Erlaubt es einem Nutzer, mit dem Link die Dateien des Mediums als ZIP herunterzuladen.",
|
||||
"LabelShareOpen": "Freigeben",
|
||||
"LabelShareURL": "Freigabe URL",
|
||||
"LabelShowAll": "Alles anzeigen",
|
||||
@@ -737,7 +740,7 @@
|
||||
"MessageAddToPlayerQueue": "Zur Abspielwarteliste hinzufügen",
|
||||
"MessageAppriseDescription": "Um diese Funktion nutzen zu können, musst du eine Instanz von <a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">Apprise API</a> laufen haben oder eine API verwenden welche dieselbe Anfragen bearbeiten kann. <br />Die Apprise API Url muss der vollständige URL-Pfad sein, an den die Benachrichtigung gesendet werden soll, z.B. wenn Ihre API-Instanz unter <code>http://192.168.1.1:8337</code> läuft, würdest du <code>http://192.168.1.1:8337/notify</code> eingeben.",
|
||||
"MessageAsinCheck": "Stelle sicher, dass die ASIN aus der richtigen Audible Region verwendet wird, nicht Amazon.",
|
||||
"MessageAuthenticationLegacyTokenWarning": "Alte API-Token werden in Zukunft entfernt. Benutze stattdessen <a href=\"/config/api-keys\">API Keys</a>.",
|
||||
"MessageAuthenticationLegacyTokenWarning": "Nicht mehr unterstützte API tokens werden in der Zukunft entfernt. Nutze stattdessen <a href=\"/config/api-keys\">API Schlüssel</a>.",
|
||||
"MessageAuthenticationOIDCChangesRestart": "Nach dem Speichern muss der Server neugestartet werden um die OIDC Änderungen zu übernehmen.",
|
||||
"MessageAuthenticationSecurityMessage": "Die Anmeldung wurde abgesichert. Benutzersitzungen werden getrennt, alle Benutzer müssen sich erneut anmelden.",
|
||||
"MessageBackupsDescription": "In einer Sicherung werden Benutzer, Benutzerfortschritte, Details zu den Bibliotheksobjekten, Servereinstellungen und Bilder welche in <code>/metadata/items</code> & <code>/metadata/authors</code> gespeichert sind gespeichert. Sicherungen enthalten keine Dateien welche in den einzelnen Bibliotheksordnern (Medien-Ordnern) gespeichert sind.",
|
||||
@@ -774,6 +777,7 @@
|
||||
"MessageConfirmDeleteSession": "Sitzung wird gelöscht! Bist du dir sicher?",
|
||||
"MessageConfirmEmbedMetadataInAudioFiles": "Bist du dir sicher, dass die Metadaten in {0} Audiodateien eingebettet werden sollen?",
|
||||
"MessageConfirmForceReScan": "Scanvorgang erzwingen! Bist du dir sicher?",
|
||||
"MessageConfirmLogoutDevice": "Bist du sicher, dass du dich von \"{0}\" abmelden willst?",
|
||||
"MessageConfirmMarkAllEpisodesFinished": "Alle Episoden werden als abgeschlossen markiert! Bist du dir sicher?",
|
||||
"MessageConfirmMarkAllEpisodesNotFinished": "Alle Episoden werden als nicht abgeschlossen markiert! Bist du dir sicher?",
|
||||
"MessageConfirmMarkItemFinished": "Möchtest du \"{0}\" wirklich als fertig markieren?",
|
||||
@@ -816,7 +820,7 @@
|
||||
"MessageFeedURLWillBe": "Feed-URL wird {0} sein",
|
||||
"MessageFetching": "Wird abgerufen …",
|
||||
"MessageForceReScanDescription": "Durchsucht alle Dateien erneut, wie bei einem frischen Scan. ID3-Tags von Audiodateien, OPF-Dateien und Textdateien werden neu durchsucht.",
|
||||
"MessageHeatmapListeningTimeTooltip": "<strong>{0} </strong> auf {1} gehört",
|
||||
"MessageHeatmapListeningTimeTooltip": "<strong>{0} gehört</strong> auf {1}",
|
||||
"MessageHeatmapNoListeningSessions": "Keine Hörsitzungen am {0}",
|
||||
"MessageImportantNotice": "Wichtiger Hinweis!",
|
||||
"MessageInsertChapterBelow": "Kapitel unten einfügen",
|
||||
@@ -1103,7 +1107,7 @@
|
||||
"ToastPodcastCreateFailed": "Podcast konnte nicht erstellt werden",
|
||||
"ToastPodcastCreateSuccess": "Podcast erstellt",
|
||||
"ToastPodcastEpisodeUpdated": "Podcast-Folge aktualisiert",
|
||||
"ToastPodcastGetFeedFailed": "Fehler beim abrufen des Podcast-Feeds",
|
||||
"ToastPodcastGetFeedFailed": "Fehler beim Abrufen des Podcast Feeds",
|
||||
"ToastPodcastNoEpisodesInFeed": "Keine Episoden in RSS Feed gefunden",
|
||||
"ToastPodcastNoRssFeed": "Podcast enthält keinen RSS Feed",
|
||||
"ToastProgressIsNotBeingSynced": "Fortschritt wird nicht synchronisiert, Wiedergabe wird neu gestartet",
|
||||
|
||||
+35
-11
@@ -135,9 +135,9 @@
|
||||
"HeaderCurrentDownloads": "Τρέχουσες Λήψεις",
|
||||
"HeaderDetails": "Λεπτομέρειες",
|
||||
"HeaderDownloadQueue": "Ουρά Λήψης",
|
||||
"HeaderEbookFiles": "Αρχεία Ebook",
|
||||
"HeaderEmail": "Ηλεκτρονικό Ταχυδρομίο",
|
||||
"HeaderEmailSettings": "Ρυθμίσεις Ηλεκτρονικού Ταχυδρομίου",
|
||||
"HeaderEbookFiles": "Αρχεία του βιβλίου",
|
||||
"HeaderEmail": "Ηλεκτρονικό ταχυδρομείο",
|
||||
"HeaderEmailSettings": "Ρυθμίσεις ηλεκτρονικού ταχυδρομείου",
|
||||
"HeaderEpisodes": "Επεισόδια",
|
||||
"HeaderEreaderSettings": "Ρυθμίσεις Ereader",
|
||||
"HeaderFiles": "Αρχεία",
|
||||
@@ -150,6 +150,7 @@
|
||||
"HeaderLibraryStats": "Στατιστικά Βιβλιοθήκης",
|
||||
"HeaderListeningSessions": "Συνεδρίες Ακρόασης",
|
||||
"HeaderListeningStats": "Στατιστικά Ακρόασης",
|
||||
"HeaderLogin": "Σύνδεση",
|
||||
"HeaderMatch": "Ταύτιση",
|
||||
"HeaderNewAccount": "Νέος Λογαριασμός",
|
||||
"HeaderNewApiKey": "Νέο Κλειδί API",
|
||||
@@ -242,35 +243,51 @@
|
||||
"LabelDescription": "Περιγραφή",
|
||||
"LabelDevice": "Συσκευή",
|
||||
"LabelDeviceInfo": "Πληροφορίες Συσκευής",
|
||||
"LabelDiscover": "Ανακάλυψη",
|
||||
"LabelDownload": "Λήψη",
|
||||
"LabelDownloadNEpisodes": "Λήψη {0} επεισοδίων",
|
||||
"LabelDuration": "Διάρκεια",
|
||||
"LabelDurationComparisonExactMatch": "(ακριβής ταύτιση)",
|
||||
"LabelEbook": "Ebook",
|
||||
"LabelEbooks": "Ebooks",
|
||||
"LabelEbook": "Ηλεκτρονικό βιβλίο",
|
||||
"LabelEbooks": "Ηλεκτρονικά βιβλία",
|
||||
"LabelEdit": "Επεξεργασία",
|
||||
"LabelEmail": "Ηλεκτρονικό Ταχυδρομίο",
|
||||
"LabelEmailSettingsFromAddress": "Από Διεύθυνση",
|
||||
"LabelEmail": "Ηλεκτρονικό ταχυδρομείο",
|
||||
"LabelEmailSettingsFromAddress": "Από διεύθυνση",
|
||||
"LabelEmailSettingsSecure": "Ασφαλές",
|
||||
"LabelEmailSettingsTestAddress": "Δοκιμή Διεύθυνσης",
|
||||
"LabelEmailSettingsTestAddress": "Δοκιμή διεύθυνσης",
|
||||
"LabelEmbeddedCover": "Ενσωματωμένο Εξώφυλλο",
|
||||
"LabelEnable": "Ενεργοποίηση",
|
||||
"LabelEnd": "Τέλος",
|
||||
"LabelEndOfChapter": "Τέλος Κεφαλαίου",
|
||||
"LabelEpisode": "Επεισόδιο",
|
||||
"LabelExplicit": "Άσεμνο περιεχόμενο",
|
||||
"LabelFeedURL": "URL ροής",
|
||||
"LabelFile": "Αρχείο",
|
||||
"LabelFileBirthtime": "Ώρα δημιουργίας αρχείου",
|
||||
"LabelFileModified": "Τροποποιημένο αρχείο",
|
||||
"LabelFilename": "Όνομα Αρχείου",
|
||||
"LabelFinished": "Ολοκληρώθηκε",
|
||||
"LabelFolder": "Φάκελος",
|
||||
"LabelFontBoldness": "Ένταση γραμματοσειράς",
|
||||
"LabelFontFamily": "Οικογένεια Γραμματοσειράς",
|
||||
"LabelGenre": "Είδος",
|
||||
"LabelGenres": "Είδη",
|
||||
"LabelFontScale": "Μέγεθος γραμματοσειράς",
|
||||
"LabelGenre": "Λογοτεχνικό Είδος",
|
||||
"LabelGenres": "Λογοτεχνικά Είδη",
|
||||
"LabelHasEbook": "Έχει βιβλίο",
|
||||
"LabelHasSupplementaryEbook": "Διαθέτει συμπληρωματικό ηλεκτρονικό βιβλίο",
|
||||
"LabelHost": "Διακομιστής",
|
||||
"LabelInProgress": "Σε Εξέλιξη",
|
||||
"LabelIncomplete": "Ελειπής",
|
||||
"LabelLanguage": "Γλώσσα",
|
||||
"LabelLayout": "Διάταξη",
|
||||
"LabelLayoutSinglePage": "Μονή Σελίδα",
|
||||
"LabelLibrarySortByProgress": "Πρόοδος: Τελευταία ενημέρωση",
|
||||
"LabelLibrarySortByProgressFinished": "Πρόοδος: Ολοκληρώθηκε",
|
||||
"LabelLibrarySortByProgressStarted": "Πρόοδος: Έναρξη",
|
||||
"LabelLineSpacing": "Διάστημα γραμμών",
|
||||
"LabelListenAgain": "Επανάληψη Ακρόασης",
|
||||
"LabelMediaType": "Τύπος Πολυμέσων",
|
||||
"LabelMissing": "Λείπει η ετικέτα",
|
||||
"LabelMore": "Περισσότερα",
|
||||
"LabelMoreInfo": "Περισσότερες Πληροφορίες",
|
||||
"LabelName": "Όνομα",
|
||||
@@ -278,11 +295,15 @@
|
||||
"LabelNarrators": "Αφηγητές",
|
||||
"LabelNewestAuthors": "Πρόσφατοι Συγγραφείς",
|
||||
"LabelNewestEpisodes": "Πρόσφατα Επεισόδια",
|
||||
"LabelNotFinished": "Δεν έχει ολοκληρωθεί",
|
||||
"LabelNotStarted": "Δεν Έχει Ξεκινήσει",
|
||||
"LabelNumberOfEpisodes": "# Επεισοδίων",
|
||||
"LabelPassword": "Κωδικός Πρόσβασης",
|
||||
"LabelPath": "Διαδρομή",
|
||||
"LabelPodcast": "Πόντκαστ",
|
||||
"LabelPodcasts": "Πόντκαστ",
|
||||
"LabelProgress": "Πρόοδος",
|
||||
"LabelPubDate": "Ημερομηνία έκδοσης",
|
||||
"LabelPublishYear": "Χρονολογία Έκδοσης",
|
||||
"LabelPublishedDate": "Εκδόθηκε {0}",
|
||||
"LabelRandomly": "Τυχαία",
|
||||
@@ -317,8 +338,11 @@
|
||||
"LabelUsername": "Όνομα Χρήστη",
|
||||
"LabelYourProgress": "Η Πρόοδος Σας",
|
||||
"MessageDownloadingEpisode": "Λήψη επεισοδίου",
|
||||
"MessageEreaderDevices": "Για να διασφαλίσετε την παράδοση των ηλεκτρονικών βιβλίων, ίσως χρειαστεί να προσθέσετε την παραπάνω διεύθυνση ηλεκτρονικού ταχυδρομείου ως έγκυρο αποστολέα για κάθε συσκευή που αναγράφεται παρακάτω.",
|
||||
"MessageLoading": "Φόρτωση...",
|
||||
"MessageMarkAsFinished": "Σήμανση ως Ολοκληρωμένο",
|
||||
"MessageNoItemsFound": "Δεν βρέθηκαν αντικείμενα",
|
||||
"MessageNoUserPlaylists": "Δεν έχετε λίστες αναπαραγωγής"
|
||||
"MessageNoUserPlaylists": "Δεν έχετε λίστες αναπαραγωγής",
|
||||
"ToastEmailSettingsUpdateSuccess": "Ενημερώθηκαν οι ρυθμίσεις ηλεκτρονικού ταχυδρομείου",
|
||||
"ToastNameEmailRequired": "Το όνομα και η διεύθυνση ηλεκτρονικού ταχυδρομείου είναι υποχρεωτικά"
|
||||
}
|
||||
@@ -46,6 +46,7 @@
|
||||
"ButtonLatest": "Latest",
|
||||
"ButtonLibrary": "Library",
|
||||
"ButtonLogout": "Logout",
|
||||
"ButtonLogoutAllDevices": "Logout All Devices",
|
||||
"ButtonLookup": "Lookup",
|
||||
"ButtonManageTracks": "Manage Tracks",
|
||||
"ButtonMapChapterTitles": "Map Chapter Titles",
|
||||
@@ -194,6 +195,7 @@
|
||||
"HeaderScheduleEpisodeDownloads": "Schedule Automatic Episode Downloads",
|
||||
"HeaderScheduleLibraryScans": "Schedule Automatic Library Scans",
|
||||
"HeaderSession": "Session",
|
||||
"HeaderSessions": "Sessions",
|
||||
"HeaderSetBackupSchedule": "Set Backup Schedule",
|
||||
"HeaderSettings": "Settings",
|
||||
"HeaderSettingsDisplay": "Display",
|
||||
@@ -415,6 +417,7 @@
|
||||
"LabelIntervalEveryHour": "Every hour",
|
||||
"LabelIntervalEveryMinute": "Every minute",
|
||||
"LabelInvert": "Invert",
|
||||
"LabelIpAddress": "IP Address",
|
||||
"LabelItem": "Item",
|
||||
"LabelJumpBackwardAmount": "Jump backward amount",
|
||||
"LabelJumpForwardAmount": "Jump forward amount",
|
||||
@@ -774,6 +777,7 @@
|
||||
"MessageConfirmDeleteSession": "Are you sure you want to delete this session?",
|
||||
"MessageConfirmEmbedMetadataInAudioFiles": "Are you sure you want to embed metadata in {0} audio files?",
|
||||
"MessageConfirmForceReScan": "Are you sure you want to force re-scan?",
|
||||
"MessageConfirmLogoutDevice": "Are you sure you want to log out of \"{0}\"?",
|
||||
"MessageConfirmMarkAllEpisodesFinished": "Are you sure you want to mark all episodes as finished?",
|
||||
"MessageConfirmMarkAllEpisodesNotFinished": "Are you sure you want to mark all episodes as not finished?",
|
||||
"MessageConfirmMarkItemFinished": "Are you sure you want to mark \"{0}\" as finished?",
|
||||
|
||||
+126
-98
@@ -20,10 +20,10 @@
|
||||
"ButtonCheckAndDownloadNewEpisodes": "Comprobar y descargar episodios nuevos",
|
||||
"ButtonChooseAFolder": "Elegir una carpeta",
|
||||
"ButtonChooseFiles": "Elegir archivos",
|
||||
"ButtonClearFilter": "Quitar filtros",
|
||||
"ButtonClearFilter": "Vaciar filtro",
|
||||
"ButtonClose": "Cerrar",
|
||||
"ButtonCloseFeed": "Cerrar suministro",
|
||||
"ButtonCloseSession": "Cerrar la sesión abierta",
|
||||
"ButtonCloseSession": "Cerrar sesión abierta",
|
||||
"ButtonCollections": "Colecciones",
|
||||
"ButtonConfigureScanner": "Configurar Escáner",
|
||||
"ButtonCreate": "Crear",
|
||||
@@ -33,27 +33,28 @@
|
||||
"ButtonEdit": "Editar",
|
||||
"ButtonEditChapters": "Editar capítulos",
|
||||
"ButtonEditPodcast": "Editar pódcast",
|
||||
"ButtonEnable": "Permitir",
|
||||
"ButtonEnable": "Habilitar",
|
||||
"ButtonFireAndFail": "Ejecutado y fallido",
|
||||
"ButtonFireOnTest": "Activar evento de prueba",
|
||||
"ButtonForceReScan": "Forzar Re-Escaneo",
|
||||
"ButtonFullPath": "Ruta completa",
|
||||
"ButtonHide": "Ocultar",
|
||||
"ButtonHome": "Inicio",
|
||||
"ButtonIssues": "Problemas",
|
||||
"ButtonIssues": "Incidencias",
|
||||
"ButtonJumpBackward": "Retroceder",
|
||||
"ButtonJumpForward": "Adelantar",
|
||||
"ButtonLatest": "Más recientes",
|
||||
"ButtonLibrary": "Biblioteca",
|
||||
"ButtonLogout": "Salir",
|
||||
"ButtonLookup": "Buscar",
|
||||
"ButtonLogout": "Cerrar Sesión",
|
||||
"ButtonLogoutAllDevices": "Cerrar sesión en todos los dispositivos",
|
||||
"ButtonLookup": "Averiguar",
|
||||
"ButtonManageTracks": "Gestionar pistas",
|
||||
"ButtonMapChapterTitles": "Asignar Títulos a Capítulos",
|
||||
"ButtonMatchAllAuthors": "Encontrar Todos los Autores",
|
||||
"ButtonMatchBooks": "Encontrar Libros",
|
||||
"ButtonMatchBooks": "Cotejar Libros",
|
||||
"ButtonNevermind": "Olvidar",
|
||||
"ButtonNext": "Siguiente",
|
||||
"ButtonNextChapter": "Siguiente Capítulo",
|
||||
"ButtonNextChapter": "Siguiente capítulo",
|
||||
"ButtonNextItemInQueue": "El siguiente elemento en cola",
|
||||
"ButtonOk": "Aceptar",
|
||||
"ButtonOpenFeed": "Abrir suministro",
|
||||
@@ -64,26 +65,26 @@
|
||||
"ButtonPlaying": "Reproduciendo",
|
||||
"ButtonPlaylists": "Listas de reproducción",
|
||||
"ButtonPrevious": "Anterior",
|
||||
"ButtonPreviousChapter": "Capítulo Anterior",
|
||||
"ButtonProbeAudioFile": "Examinar archivo de audio",
|
||||
"ButtonPurgeAllCache": "Purgar toda la antememoria",
|
||||
"ButtonPurgeItemsCache": "Purgar antememoria de elementos",
|
||||
"ButtonQueueAddItem": "Añadir a la cola",
|
||||
"ButtonQueueRemoveItem": "Quitar de la cola",
|
||||
"ButtonPreviousChapter": "Capítulo anterior",
|
||||
"ButtonProbeAudioFile": "Sonda del archivo de audio",
|
||||
"ButtonPurgeAllCache": "Purgar toda la caché",
|
||||
"ButtonPurgeItemsCache": "Purgar caché de elementos",
|
||||
"ButtonQueueAddItem": "Añadir a cola",
|
||||
"ButtonQueueRemoveItem": "Quitar de cola",
|
||||
"ButtonQuickEmbed": "Inserción rápida",
|
||||
"ButtonQuickEmbedMetadata": "Agregue metadatos rápidamente",
|
||||
"ButtonQuickMatch": "Encontrar Rápido",
|
||||
"ButtonQuickEmbedMetadata": "Empotrar metadatos rápidamente",
|
||||
"ButtonQuickMatch": "Cotejo Rápido",
|
||||
"ButtonReScan": "Re-Escanear",
|
||||
"ButtonRead": "Leer",
|
||||
"ButtonReadLess": "Leer menos",
|
||||
"ButtonReadMore": "Leer más",
|
||||
"ButtonRefresh": "Actualizar",
|
||||
"ButtonRefresh": "Recargar",
|
||||
"ButtonRemove": "Quitar",
|
||||
"ButtonRemoveAll": "Quitar todo",
|
||||
"ButtonRemoveAllLibraryItems": "Quitar todos los elementos de la biblioteca",
|
||||
"ButtonRemoveFromContinueListening": "Quitar de Continuar escuchando",
|
||||
"ButtonRemoveFromContinueReading": "Quitar de Continuar leyendo",
|
||||
"ButtonRemoveSeriesFromContinueSeries": "Quitar serie de Continuar serie",
|
||||
"ButtonRemoveFromContinueListening": "Quitar desde Escucha Continua",
|
||||
"ButtonRemoveFromContinueReading": "Quitar desde Continuar Leyendo",
|
||||
"ButtonRemoveSeriesFromContinueSeries": "Quitar Series desde Series Continuas",
|
||||
"ButtonReset": "Restablecer",
|
||||
"ButtonResetToDefault": "Restaurar valores predeterminados",
|
||||
"ButtonRestore": "Restaurar",
|
||||
@@ -92,47 +93,47 @@
|
||||
"ButtonSaveTracklist": "Guardar lista de pistas",
|
||||
"ButtonScan": "Escanear",
|
||||
"ButtonScanLibrary": "Escanear biblioteca",
|
||||
"ButtonScrollLeft": "Desplazarse hacia la izquierda",
|
||||
"ButtonScrollRight": "Desplazarse hacia la derecha",
|
||||
"ButtonScrollLeft": "Desplazarse a la izquierda",
|
||||
"ButtonScrollRight": "Desplazarse a la derecha",
|
||||
"ButtonSearch": "Buscar",
|
||||
"ButtonSelectFolderPath": "Seleccionar ruta de carpeta",
|
||||
"ButtonSeries": "Series",
|
||||
"ButtonSetChaptersFromTracks": "Seleccionar Capítulos Según las Pistas",
|
||||
"ButtonSetChaptersFromTracks": "Establecer capítulos según las pistas",
|
||||
"ButtonShare": "Compartir",
|
||||
"ButtonShiftTimes": "Desplazar Tiempos",
|
||||
"ButtonShiftTimes": "Veces de Desplazo",
|
||||
"ButtonShow": "Mostrar",
|
||||
"ButtonStartM4BEncode": "Iniciar Codificación M4B",
|
||||
"ButtonStartMetadataEmbed": "Iniciar la Inserción de Metadata",
|
||||
"ButtonStartMetadataEmbed": "Iniciar Inserción de Metadatos",
|
||||
"ButtonStats": "Estadísticas",
|
||||
"ButtonSubmit": "Enviar",
|
||||
"ButtonSubmit": "Entregar",
|
||||
"ButtonTest": "Prueba",
|
||||
"ButtonUnlinkOpenId": "Desvincular OpenID",
|
||||
"ButtonUpload": "Cargar",
|
||||
"ButtonUploadBackup": "Cargar respaldo",
|
||||
"ButtonUploadCover": "Cargar cubierta",
|
||||
"ButtonUploadOPMLFile": "Cargar archivo OPML",
|
||||
"ButtonUnlinkOpenId": "Desenlazar OpenID",
|
||||
"ButtonUpload": "Subir",
|
||||
"ButtonUploadBackup": "Subir Respaldo",
|
||||
"ButtonUploadCover": "Subir Cubierta",
|
||||
"ButtonUploadOPMLFile": "Subir archivo OPML",
|
||||
"ButtonUserDelete": "Eliminar usuario {0}",
|
||||
"ButtonUserEdit": "Editar usuario {0}",
|
||||
"ButtonViewAll": "Ver todo",
|
||||
"ButtonYes": "Sí",
|
||||
"ErrorUploadFetchMetadataAPI": "Error al recuperar los metadatos",
|
||||
"ErrorUploadFetchMetadataNoResults": "No se pudieron recuperar los metadatos; pruebe a actualizar el título o autor",
|
||||
"ErrorUploadLacksTitle": "Debe tener título",
|
||||
"ErrorUploadFetchMetadataNoResults": "No se pudieron recuperar los metadatos; pruebe a actualizar el título y/o autor",
|
||||
"ErrorUploadLacksTitle": "Debe tener un título",
|
||||
"HeaderAccount": "Cuenta",
|
||||
"HeaderAddCustomMetadataProvider": "Añadir proveedor de metadatos personalizado",
|
||||
"HeaderAdvanced": "Avanzado",
|
||||
"HeaderApiKeys": "Claves API",
|
||||
"HeaderAppriseNotificationSettings": "Configuración de notificaciones de Apprise",
|
||||
"HeaderAudioTracks": "Pistas de audio",
|
||||
"HeaderAppriseNotificationSettings": "Ajustes de notificaciones de Apprise",
|
||||
"HeaderAudioTracks": "Pistas de Audio",
|
||||
"HeaderAudiobookTools": "Herramientas de Gestión de Archivos de Audiolibro",
|
||||
"HeaderAuthentication": "Autenticación",
|
||||
"HeaderBackups": "Respaldos",
|
||||
"HeaderBulkChapterModal": "Añadir Múltiples Capítulos",
|
||||
"HeaderChangePassword": "Cambiar contraseña",
|
||||
"HeaderChangePassword": "Cambiar Contraseña",
|
||||
"HeaderChapters": "Capítulos",
|
||||
"HeaderChooseAFolder": "Escoger una Carpeta",
|
||||
"HeaderCollection": "Colección",
|
||||
"HeaderCollectionItems": "Elementos en la colección",
|
||||
"HeaderCollectionItems": "Elementos de colección",
|
||||
"HeaderCover": "Cubierta",
|
||||
"HeaderCurrentDownloads": "Descargas actuales",
|
||||
"HeaderCustomMessageOnLogin": "Mensaje personalizado al acceder",
|
||||
@@ -140,49 +141,49 @@
|
||||
"HeaderDetails": "Detalles",
|
||||
"HeaderDownloadQueue": "Cola de descargas",
|
||||
"HeaderEbookFiles": "Archivos de libros digitales",
|
||||
"HeaderEmail": "Correo electrónico",
|
||||
"HeaderEmailSettings": "Configuración de correo electrónico",
|
||||
"HeaderEmail": "Correo-e",
|
||||
"HeaderEmailSettings": "Ajustes de correo-e",
|
||||
"HeaderEpisodes": "Episodios",
|
||||
"HeaderEreaderDevices": "Dispositivos Ereader",
|
||||
"HeaderEreaderSettings": "Configuración del lector",
|
||||
"HeaderEreaderDevices": "Dispositivos Lector-e",
|
||||
"HeaderEreaderSettings": "Ajustes del Lector-e",
|
||||
"HeaderFiles": "Archivos",
|
||||
"HeaderFindChapters": "Buscar capítulos",
|
||||
"HeaderIgnoredFiles": "Archivos ignorados",
|
||||
"HeaderItemFiles": "Archivos de elementos",
|
||||
"HeaderItemMetadataUtils": "Utilidades de metadatos de elementos",
|
||||
"HeaderItemFiles": "Archivos del elemento",
|
||||
"HeaderItemMetadataUtils": "Utilidades de metadatos del elemento",
|
||||
"HeaderLastListeningSession": "Última sesión de escucha",
|
||||
"HeaderLatestEpisodes": "Episodios más recientes",
|
||||
"HeaderLibraries": "Bibliotecas",
|
||||
"HeaderLibraryFiles": "Archivos de biblioteca",
|
||||
"HeaderLibraryStats": "Estadísticas de biblioteca",
|
||||
"HeaderListeningSessions": "Sesión",
|
||||
"HeaderListeningSessions": "Sesiones Listadas",
|
||||
"HeaderListeningStats": "Estadísticas de Tiempo Escuchado",
|
||||
"HeaderLogin": "Acceder",
|
||||
"HeaderLogs": "Registros",
|
||||
"HeaderLogin": "Inicio de Sesión",
|
||||
"HeaderLogs": "Bitácoras",
|
||||
"HeaderManageGenres": "Gestionar géneros",
|
||||
"HeaderManageTags": "Gestionar etiquetas",
|
||||
"HeaderMapDetails": "Asignar Detalles",
|
||||
"HeaderMatch": "Encontrar",
|
||||
"HeaderMatch": "Coincidir",
|
||||
"HeaderMetadataOrderOfPrecedence": "Orden de precedencia de metadatos",
|
||||
"HeaderMetadataToEmbed": "Metadatos para Insertar",
|
||||
"HeaderNewAccount": "Cuenta nueva",
|
||||
"HeaderMetadataToEmbed": "Metadatos para empotrar",
|
||||
"HeaderNewAccount": "Crear Cuenta",
|
||||
"HeaderNewApiKey": "Nueva clave API",
|
||||
"HeaderNewLibrary": "Biblioteca nueva",
|
||||
"HeaderNotificationCreate": "Crear notificación",
|
||||
"HeaderNotificationUpdate": "Notificación de actualización",
|
||||
"HeaderNotificationCreate": "Crear Notificación",
|
||||
"HeaderNotificationUpdate": "Notificación de Actualización",
|
||||
"HeaderNotifications": "Notificaciones",
|
||||
"HeaderOpenIDConnectAuthentication": "Autenticación OpenID Connect",
|
||||
"HeaderOpenListeningSessions": "Sesiones públicas de escucha",
|
||||
"HeaderOpenListeningSessions": "Abrir escucha de sesiones",
|
||||
"HeaderOpenRSSFeed": "Abrir suministro RSS",
|
||||
"HeaderOtherFiles": "Otros archivos",
|
||||
"HeaderPasswordAuthentication": "Autenticación por contraseña",
|
||||
"HeaderPermissions": "Permisos",
|
||||
"HeaderPlayerQueue": "Cola del reproductor",
|
||||
"HeaderPlayerSettings": "Configuración del reproductor",
|
||||
"HeaderPlayerSettings": "Ajustes del reproductor",
|
||||
"HeaderPlaylist": "Lista de reproducción",
|
||||
"HeaderPlaylistItems": "Elementos de lista de reproducción",
|
||||
"HeaderPodcastsToAdd": "Pódcast para añadir",
|
||||
"HeaderPresets": "Preconfiguraciones",
|
||||
"HeaderPresets": "Preajustes",
|
||||
"HeaderPreviewCover": "Previsualizar cubierta",
|
||||
"HeaderRSSFeedGeneral": "Detalles de RSS",
|
||||
"HeaderRSSFeedIsOpen": "El suministro RSS está abierto",
|
||||
@@ -191,18 +192,19 @@
|
||||
"HeaderRemoveEpisodes": "Quitar {0} episodios",
|
||||
"HeaderSavedMediaProgress": "Guardar Progreso de Multimedia",
|
||||
"HeaderSchedule": "Horario",
|
||||
"HeaderScheduleEpisodeDownloads": "Programar descargas automáticas de episodios",
|
||||
"HeaderScheduleLibraryScans": "Programar Escaneo Automático de Biblioteca",
|
||||
"HeaderScheduleEpisodeDownloads": "Planificador de auto‐descargas de episodios",
|
||||
"HeaderScheduleLibraryScans": "Planificar Auto‐Escaneo de Biblioteca",
|
||||
"HeaderSession": "Sesión",
|
||||
"HeaderSetBackupSchedule": "Programar Respaldo",
|
||||
"HeaderSettings": "Configuración",
|
||||
"HeaderSessions": "Sesiones",
|
||||
"HeaderSetBackupSchedule": "Establecer Planificación de Respaldo",
|
||||
"HeaderSettings": "Ajustes",
|
||||
"HeaderSettingsDisplay": "Interfaz",
|
||||
"HeaderSettingsExperimental": "Funcionalidades experimentales",
|
||||
"HeaderSettingsExperimental": "Características experimentales",
|
||||
"HeaderSettingsGeneral": "Generales",
|
||||
"HeaderSettingsScanner": "Escáner",
|
||||
"HeaderSettingsSecurity": "Seguridad",
|
||||
"HeaderSettingsWebClient": "Cliente web",
|
||||
"HeaderSleepTimer": "Temporizador de apagado",
|
||||
"HeaderSleepTimer": "Cronómetro de dormida",
|
||||
"HeaderStatsLargestItems": "Elementos más grandes",
|
||||
"HeaderStatsLongestItems": "Elementos más extensos (h)",
|
||||
"HeaderStatsMinutesListeningChart": "Minutos escuchando (últimos 7 días)",
|
||||
@@ -233,29 +235,29 @@
|
||||
"LabelAddToCollectionBatch": "Añadir {0} libros a colección",
|
||||
"LabelAddToPlaylist": "Añadir a lista de reproducción",
|
||||
"LabelAddToPlaylistBatch": "Añadir {0} elementos a lista de reproducción",
|
||||
"LabelAddedAt": "Añadido",
|
||||
"LabelAddedDate": "{0} Añadido",
|
||||
"LabelAddedAt": "Añadido en",
|
||||
"LabelAddedDate": "Añadido {0}",
|
||||
"LabelAdminUsersOnly": "Solamente usuarios administradores",
|
||||
"LabelAll": "Todos",
|
||||
"LabelAllEpisodesDownloaded": "Todos los episodios descargados",
|
||||
"LabelAllUsers": "Todos los usuarios",
|
||||
"LabelAllUsersExcludingGuests": "Todos los usuarios excepto invitados",
|
||||
"LabelAllUsersIncludingGuests": "Todos los usuarios e invitados",
|
||||
"LabelAlreadyInYourLibrary": "Ya existe en la Biblioteca",
|
||||
"LabelApiKeyCreated": "La clave de API “{0}” se ha creado con éxito.",
|
||||
"LabelAlreadyInYourLibrary": "Ya dentro de tu biblioteca",
|
||||
"LabelApiKeyCreated": "La clave de API “{0}” se ha creado correctamente.",
|
||||
"LabelApiKeyCreatedDescription": "Asegúrate de copiar la clave de API ahora, no la volverás a ver otra vez.",
|
||||
"LabelApiKeyUser": "Actuar en nombre del usuario",
|
||||
"LabelApiKeyUserDescription": "Esta clave de API tendrá los mismos permisos que el usuario al que representa. En los registros se verá como si la solicitud la hubiera hecho el usuario directamente.",
|
||||
"LabelApiToken": "Token de la API",
|
||||
"LabelApiToken": "Vale del API",
|
||||
"LabelAppend": "Adjuntar",
|
||||
"LabelAudioBitrate": "Tasa de bits del audio (por ejemplo, 128k)",
|
||||
"LabelAudioBitrate": "Tasa de bit del audio (p.ej., 128k)",
|
||||
"LabelAudioChannels": "Canales de audio (1 o 2)",
|
||||
"LabelAudioCodec": "Códec de audio",
|
||||
"LabelAuthor": "Autor",
|
||||
"LabelAuthorFirstLast": "Autor (Nombre Apellido)",
|
||||
"LabelAuthorLastFirst": "Autor (Apellido, Nombre)",
|
||||
"LabelAuthors": "Autores",
|
||||
"LabelAutoDownloadEpisodes": "Descargar episodios automáticamente",
|
||||
"LabelAutoDownloadEpisodes": "Auto‐Descargar episodios",
|
||||
"LabelAutoFetchMetadata": "Recuperar metadatos automáticamente",
|
||||
"LabelAutoFetchMetadataHelp": "Obtiene metadatos de título, autor y serie para agilizar la carga. Es posible que haya que cotejar metadatos adicionales después de la carga.",
|
||||
"LabelAutoLaunch": "Lanzamiento automático",
|
||||
@@ -275,7 +277,7 @@
|
||||
"LabelBonus": "Bonus",
|
||||
"LabelBooks": "Libros",
|
||||
"LabelButtonText": "Texto del botón",
|
||||
"LabelByAuthor": "por",
|
||||
"LabelByAuthor": "por {0}",
|
||||
"LabelChangePassword": "Cambiar contraseña",
|
||||
"LabelChannels": "Canales",
|
||||
"LabelChapterCount": "{0} capítulos",
|
||||
@@ -286,13 +288,13 @@
|
||||
"LabelClickToUseCurrentValue": "Pulse para utilizar el valor actual",
|
||||
"LabelClosePlayer": "Cerrar reproductor",
|
||||
"LabelCodec": "Codec",
|
||||
"LabelCollapseSeries": "Colapsar serie",
|
||||
"LabelCollapseSeries": "Colapsar Series",
|
||||
"LabelCollapseSubSeries": "Contraer la subserie",
|
||||
"LabelCollection": "Colección",
|
||||
"LabelCollections": "Colecciones",
|
||||
"LabelComplete": "Completo",
|
||||
"LabelConfirmPassword": "Confirmar contraseña",
|
||||
"LabelContinueListening": "Seguir escuchando",
|
||||
"LabelContinueListening": "Seguir Escuchando",
|
||||
"LabelContinueReading": "Continuar leyendo",
|
||||
"LabelContinueSeries": "Continuar series",
|
||||
"LabelCorsAllowed": "Orígenes CORS Permitidos",
|
||||
@@ -325,14 +327,14 @@
|
||||
"LabelDurationComparisonLonger": "({0} más largo)",
|
||||
"LabelDurationComparisonShorter": "({0} más corto)",
|
||||
"LabelDurationFound": "Duración Comprobada:",
|
||||
"LabelEbook": "Libro electrónico",
|
||||
"LabelEbooks": "Libros electrónicos",
|
||||
"LabelEbook": "Libro-e",
|
||||
"LabelEbooks": "Libros-e",
|
||||
"LabelEdit": "Editar",
|
||||
"LabelEmail": "Correo electrónico",
|
||||
"LabelEmailSettingsFromAddress": "Remitente",
|
||||
"LabelEmailSettingsRejectUnauthorized": "Rechazar certificados no autorizados",
|
||||
"LabelEmailSettingsRejectUnauthorizedHelp": "Desactivar la validación de certificados SSL puede exponer su conexión a riesgos de seguridad, como los ataques por intermediario. Desactive esta opción solo si conoce las implicaciones y confía en el servidor de correo al que se conecta.",
|
||||
"LabelEmailSettingsSecure": "Seguridad",
|
||||
"LabelEmailSettingsSecure": "Seguro",
|
||||
"LabelEmailSettingsSecureHelp": "Si está activado, se usará TLS para conectarse al servidor. Si está apagado, se usará TLS si su servidor tiene soporte para la extensión STARTTLS. En la mayoría de los casos, puede dejar esta opción activada si se está conectando al puerto 465. Apáguela en el caso de usar los puertos 587 o 25. (de nodemailer.com/smtp/#authentication)",
|
||||
"LabelEmailSettingsTestAddress": "Probar dirección",
|
||||
"LabelEmbeddedCover": "Cubierta incrustada",
|
||||
@@ -377,11 +379,12 @@
|
||||
"LabelFilename": "Nombre del archivo",
|
||||
"LabelFilterByUser": "Filtrar por Usuario",
|
||||
"LabelFindEpisodes": "Buscar Episodio",
|
||||
"LabelFinished": "Terminado",
|
||||
"LabelFinished": "Finalizado",
|
||||
"LabelFinishedDate": "Finalizado {0}",
|
||||
"LabelFolder": "Carpeta",
|
||||
"LabelFolders": "Carpetas",
|
||||
"LabelFontBold": "Negrilla",
|
||||
"LabelFontBoldness": "Peso tipográfico",
|
||||
"LabelFontBoldness": "Tipográfico sin Negrita",
|
||||
"LabelFontFamily": "Familia tipográfica",
|
||||
"LabelFontItalic": "Itálica",
|
||||
"LabelFontScale": "Escala de letra",
|
||||
@@ -391,8 +394,8 @@
|
||||
"LabelGenre": "Género",
|
||||
"LabelGenres": "Géneros",
|
||||
"LabelHardDeleteFile": "Eliminar Definitivamente",
|
||||
"LabelHasEbook": "Tiene un libro",
|
||||
"LabelHasSupplementaryEbook": "Tiene un libro complementario",
|
||||
"LabelHasEbook": "Tiene libro-e",
|
||||
"LabelHasSupplementaryEbook": "Tiene un libro-e suplementario",
|
||||
"LabelHideSubtitles": "Ocultar subtítulos",
|
||||
"LabelHighestPriority": "Mayor prioridad",
|
||||
"LabelHost": "Anfitrión",
|
||||
@@ -414,6 +417,7 @@
|
||||
"LabelIntervalEveryHour": "Cada hora",
|
||||
"LabelIntervalEveryMinute": "Cada minuto",
|
||||
"LabelInvert": "Invertir",
|
||||
"LabelIpAddress": "Dirección IP",
|
||||
"LabelItem": "Elemento",
|
||||
"LabelJumpBackwardAmount": "Cantidad de saltos hacia atrás",
|
||||
"LabelJumpForwardAmount": "Cantidad de saltos hacia adelante",
|
||||
@@ -422,6 +426,7 @@
|
||||
"LabelLanguages": "Idiomas",
|
||||
"LabelLastBookAdded": "Último libro añadido",
|
||||
"LabelLastBookUpdated": "Último libro actualizado",
|
||||
"LabelLastProgressDate": "Último progreso: {0}",
|
||||
"LabelLastSeen": "Última Vez Visto",
|
||||
"LabelLastTime": "Última Vez",
|
||||
"LabelLastUpdate": "Última Actualización",
|
||||
@@ -434,6 +439,9 @@
|
||||
"LabelLibraryFilterSublistEmpty": "Sin {0}",
|
||||
"LabelLibraryItem": "Elemento de Biblioteca",
|
||||
"LabelLibraryName": "Nombre de Biblioteca",
|
||||
"LabelLibrarySortByProgress": "Progreso: Último actualizado",
|
||||
"LabelLibrarySortByProgressFinished": "Progreso: Finalizado",
|
||||
"LabelLibrarySortByProgressStarted": "Progreso: Iniciado",
|
||||
"LabelLimit": "Limites",
|
||||
"LabelLineSpacing": "Interlineado",
|
||||
"LabelListenAgain": "Volver a escuchar",
|
||||
@@ -442,6 +450,7 @@
|
||||
"LabelLogLevelWarn": "Advertencia",
|
||||
"LabelLookForNewEpisodesAfterDate": "Buscar Nuevos Episodios a partir de esta Fecha",
|
||||
"LabelLowestPriority": "Menor prioridad",
|
||||
"LabelMatchConfidence": "Confidencia",
|
||||
"LabelMatchExistingUsersBy": "Emparejar a los usuarios existentes por",
|
||||
"LabelMatchExistingUsersByDescription": "Se utiliza para conectar usuarios existentes. Una vez conectados, los usuarios serán emparejados por un identificador único de su proveedor de SSO",
|
||||
"LabelMaxEpisodesToDownload": "Número máximo # de episodios para descargar. Usa 0 para descargar una cantidad ilimitada.",
|
||||
@@ -460,7 +469,7 @@
|
||||
"LabelMissingEbook": "No tiene libro electrónico",
|
||||
"LabelMissingSupplementaryEbook": "No tiene libro electrónico suplementario",
|
||||
"LabelMobileRedirectURIs": "URIs de redirección a móviles permitidos",
|
||||
"LabelMobileRedirectURIsDescription": "Esta es una lista blanca de URI de redireccionamiento válidos para aplicaciones móviles. El predeterminado es <code> audiobookshelf</code> , que puede eliminar o complementar con URI adicionales para la integración de aplicaciones de terceros. Usando un asterisco (<code> *</code> ) como única entrada que permite cualquier URI.",
|
||||
"LabelMobileRedirectURIsDescription": "Esta es una lista en blanco de las URI de re‐direccionamiento válidos para aplicaciones móviles. El predeterminado es <code>audiobookshelf</code> , que puede retirar o sustituir con las URI adicionales para la integración de aplicaciones de terceros. Usando un asterisco (<code>*</code> ) como única entrada que permite cualquier URI.",
|
||||
"LabelMore": "Más",
|
||||
"LabelMoreInfo": "Más información",
|
||||
"LabelName": "Nombre",
|
||||
@@ -473,9 +482,10 @@
|
||||
"LabelNextBackupDate": "Fecha del siguiente respaldo",
|
||||
"LabelNextChapters": "Los próximos capítulos serán:",
|
||||
"LabelNextScheduledRun": "Próxima ejecución programada",
|
||||
"LabelNoApiKeys": "Sin claves API",
|
||||
"LabelNoCustomMetadataProviders": "Sin proveedores de metadatos personalizados",
|
||||
"LabelNoEpisodesSelected": "Ningún Episodio Seleccionado",
|
||||
"LabelNotFinished": "No terminado",
|
||||
"LabelNotFinished": "No finalizado",
|
||||
"LabelNotStarted": "Sin iniciar",
|
||||
"LabelNotes": "Notas",
|
||||
"LabelNotificationAppriseURL": "URL(s) de Apprise",
|
||||
@@ -489,7 +499,7 @@
|
||||
"LabelNotificationsMaxQueueSizeHelp": "Las notificaciones están limitadas a 1 por segundo. Las notificaciones serán ignoradas si llegan al numero máximo de cola para prevenir spam de eventos.",
|
||||
"LabelNumberOfBooks": "Número de libros",
|
||||
"LabelNumberOfChapters": "Número de capítulos:",
|
||||
"LabelNumberOfEpisodes": "N.º de episodios",
|
||||
"LabelNumberOfEpisodes": "Nº de episodios",
|
||||
"LabelOpenIDAdvancedPermsClaimDescription": "Nombre de la notificación de OpenID que contiene permisos avanzados para acciones de usuario dentro de la aplicación que se aplicarán a roles que no sean de administrador (<b>si están configurados</b>). Si el reclamo no aparece en la respuesta, se denegará el acceso a ABS. Si falta una sola opción, se tratará como <code>falsa</code>. Asegúrese de que la notificación del proveedor de identidades coincida con la estructura esperada:",
|
||||
"LabelOpenIDClaims": "Deje las siguientes opciones vacías para desactivar la asignación avanzada de grupos y permisos, lo que asignaría de manera automática al grupo «Usuario».",
|
||||
"LabelOpenIDGroupClaimDescription": "Nombre de la declaración OpenID que contiene una lista de grupos del usuario. Comúnmente conocidos como <code>grupos</code>. <b>Si se configura</b>, la aplicación asignará automáticamente roles en función de la pertenencia a grupos del usuario, siempre que estos grupos se denominen \"admin\", \"user\" o \"guest\" en la notificación. La solicitud debe contener una lista, y si un usuario pertenece a varios grupos, la aplicación asignará el rol correspondiente al mayor nivel de acceso. Si ningún grupo coincide, se denegará el acceso.",
|
||||
@@ -519,7 +529,7 @@
|
||||
"LabelPodcasts": "Pódcast",
|
||||
"LabelPort": "Puerto",
|
||||
"LabelPrefixesToIgnore": "Prefijos para ignorar (no distingue entre mayúsculas y minúsculas)",
|
||||
"LabelPreventIndexing": "Evite que los directorios de pódcast de iTunes y Google indicen su suministro",
|
||||
"LabelPreventIndexing": "Evite que los directorios de pódcast de iTunes y Google indexen su suministro",
|
||||
"LabelPrimaryEbook": "Libro electrónico principal",
|
||||
"LabelProgress": "Progreso",
|
||||
"LabelProvider": "Proveedor",
|
||||
@@ -531,11 +541,11 @@
|
||||
"LabelPublishedDecades": "Décadas publicadas",
|
||||
"LabelPublisher": "Editor",
|
||||
"LabelPublishers": "Editores",
|
||||
"LabelRSSFeedCustomOwnerEmail": "Correo electrónico de dueño personalizado",
|
||||
"LabelRSSFeedCustomOwnerName": "Nombre de dueño personalizado",
|
||||
"LabelRSSFeedCustomOwnerEmail": "Correo-e de propietario personalizado",
|
||||
"LabelRSSFeedCustomOwnerName": "Nombre de propietario personalizado",
|
||||
"LabelRSSFeedOpen": "Fuente RSS Abierta",
|
||||
"LabelRSSFeedPreventIndexing": "Evitar indización",
|
||||
"LabelRSSFeedSlug": "«Slug» de suministro RSS",
|
||||
"LabelRSSFeedSlug": "Ficha de suministro RSS",
|
||||
"LabelRSSFeedURL": "URL de suministro RSS",
|
||||
"LabelRandomly": "Aleatorio",
|
||||
"LabelReAddSeriesToContinueListening": "Volver a agregar la serie para continuar escuchándola",
|
||||
@@ -563,11 +573,12 @@
|
||||
"LabelSelectAll": "Seleccionar todo",
|
||||
"LabelSelectAllEpisodes": "Seleccionar todos los episodios",
|
||||
"LabelSelectEpisodesShowing": "Seleccionar los {0} episodios visibles",
|
||||
"LabelSelectUser": "Seleccionar usuario",
|
||||
"LabelSelectUsers": "Seleccionar usuarios",
|
||||
"LabelSendEbookToDevice": "Enviar libro electrónico a...",
|
||||
"LabelSequence": "Secuencia",
|
||||
"LabelSerial": "En serie",
|
||||
"LabelSeries": "Serie",
|
||||
"LabelSeries": "Series",
|
||||
"LabelSeriesName": "Nombre de la serie",
|
||||
"LabelSeriesProgress": "Progreso de la serie",
|
||||
"LabelServerLogLevel": "Nivel de registro del servidor",
|
||||
@@ -580,8 +591,8 @@
|
||||
"LabelSettingsBookshelfViewHelp": "Diseño Esqueuomorfo con Estantes de Madera",
|
||||
"LabelSettingsChromecastSupport": "Compatibilidad con Chromecast",
|
||||
"LabelSettingsDateFormat": "Formato de Fecha",
|
||||
"LabelSettingsEnableWatcher": "Buscar cambios automáticamente en las bibliotecas",
|
||||
"LabelSettingsEnableWatcherForLibrary": "Buscar cambios automáticamente en la biblioteca",
|
||||
"LabelSettingsEnableWatcher": "Vigilar automáticamente los cambios en bibliotecas",
|
||||
"LabelSettingsEnableWatcherForLibrary": "Vigilar automáticamente los cambios de 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.",
|
||||
@@ -614,14 +625,14 @@
|
||||
"LabelSettingsStoreMetadataWithItemHelp": "Por defecto, los archivos de metadatos se almacenan en /metadata/items. Si habilita esta opción, los archivos de metadatos se guardarán en la carpeta de elementos de su biblioteca",
|
||||
"LabelSettingsTimeFormat": "Formato de Tiempo",
|
||||
"LabelShare": "Compartir",
|
||||
"LabelShareDownloadableHelp": "Permite a quienes posean el enlace de compartición descargar un archivo ZIP del elemento de la biblioteca.",
|
||||
"LabelShareDownloadableHelp": "Permite a quienes posean el enlace de compartición descargar un archivo zip del elemento de la biblioteca.",
|
||||
"LabelShareOpen": "abrir un recurso compartido",
|
||||
"LabelShareURL": "Compartir la URL",
|
||||
"LabelShowAll": "Mostrar todo",
|
||||
"LabelShowSeconds": "Mostrar segundos",
|
||||
"LabelShowSubtitles": "Mostrar subtítulos",
|
||||
"LabelSize": "Tamaño",
|
||||
"LabelSleepTimer": "Temporizador de apagado",
|
||||
"LabelSleepTimer": "Temporizador de dormida",
|
||||
"LabelSlug": "Slug",
|
||||
"LabelSortAscending": "Ascendente",
|
||||
"LabelSortDescending": "Descendente",
|
||||
@@ -630,6 +641,7 @@
|
||||
"LabelStartTime": "Tiempo de Inicio",
|
||||
"LabelStarted": "Iniciado",
|
||||
"LabelStartedAt": "Iniciado En",
|
||||
"LabelStartedDate": "Iniciado {0}",
|
||||
"LabelStatsAudioTracks": "Pistas de Audio",
|
||||
"LabelStatsAuthors": "Autores",
|
||||
"LabelStatsBestDay": "Mejor día",
|
||||
@@ -659,6 +671,7 @@
|
||||
"LabelTheme": "Tema",
|
||||
"LabelThemeDark": "Oscuro",
|
||||
"LabelThemeLight": "Claro",
|
||||
"LabelThemeSepia": "Sepia",
|
||||
"LabelTimeBase": "Tiempo Base",
|
||||
"LabelTimeDurationXHours": "{0} horas",
|
||||
"LabelTimeDurationXMinutes": "{0} minutos",
|
||||
@@ -727,8 +740,10 @@
|
||||
"MessageAddToPlayerQueue": "Agregar a fila del Reproductor",
|
||||
"MessageAppriseDescription": "Para usar esta función deberás tener <a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">la API de Apprise</a> corriendo o una API que maneje los mismos resultados. <br/>La URL de la API de Apprise debe tener la misma ruta de archivos que donde se envían las notificaciones. Por ejemplo: si su API esta en <code>http://192.168.1.1:8337</code> entonces pondría <code>http://192.168.1.1:8337/notify</code>.",
|
||||
"MessageAsinCheck": "Cerciórese de usar el ASIN de la región correcta de Audible, no de Amazon.",
|
||||
"MessageAuthenticationLegacyTokenWarning": "Los vales de API heredados serán retirados en el futuro. Utilice las <a href=\"/config/api-keys\">claves de API</a> en su lugar.",
|
||||
"MessageAuthenticationOIDCChangesRestart": "Reinicie el servidor tras el guardado para aplicar los cambios de OIDC.",
|
||||
"MessageBackupsDescription": "Los respaldos incluyen: usuarios, el progreso del los usuarios, los detalles de los elementos de la biblioteca, la configuración del servidor y las imágenes en <code>/metadata/items</code> y <code>/metadata/authors</code>. Los Respaldos <strong>NO</strong> incluyen ningún archivo guardado en la carpeta de tu biblioteca.",
|
||||
"MessageAuthenticationSecurityMessage": "La autenticación ha sido mejorada para seguridad. Todos los usuarios requieren reiniciar sesión.",
|
||||
"MessageBackupsDescription": "Los respaldos incluyen: usuarios, el progreso del los usuarios, los detalles de los elementos de la biblioteca, la configuración del servidor y las imágenes en <code>/metadata/items</code> y <code>/metadata/authors</code>. Los Respaldos <strong>no</strong> incluyen ningún archivo guardado en la carpeta de tu biblioteca.",
|
||||
"MessageBackupsLocationEditNote": "Nota: actualizar la ubicación de la copia de respaldo no moverá ni modificará los respaldos existentes",
|
||||
"MessageBackupsLocationNoEditNote": "Nota: la ubicación de la copia de respaldo se establece a través de una variable de entorno y no se puede cambiar aquí.",
|
||||
"MessageBackupsLocationPathEmpty": "La ruta de la copia de seguridad no puede estar vacía",
|
||||
@@ -750,6 +765,7 @@
|
||||
"MessageChaptersNotFound": "Capítulos no encontrados",
|
||||
"MessageCheckingCron": "Revisando cron...",
|
||||
"MessageConfirmCloseFeed": "¿Confirma que quiere cerrar este suministro?",
|
||||
"MessageConfirmDeleteApiKey": "¿Está seguro que desea eliminar la clave API «{0}»?",
|
||||
"MessageConfirmDeleteBackup": "¿Confirma que quiere eliminar el respaldo de {0}?",
|
||||
"MessageConfirmDeleteDevice": "¿Confirma que quiere eliminar el lector electrónico «{0}»?",
|
||||
"MessageConfirmDeleteFile": "Esto eliminará el archivo del sistema de archivos. ¿Quiere continuar?",
|
||||
@@ -761,6 +777,7 @@
|
||||
"MessageConfirmDeleteSession": "¿Confirma que quiere eliminar esta sesión?",
|
||||
"MessageConfirmEmbedMetadataInAudioFiles": "¿Confirma que quiere incrustar metadatos en {0} archivos de audio?",
|
||||
"MessageConfirmForceReScan": "¿Confirma que quiere forzar un reescaneo?",
|
||||
"MessageConfirmLogoutDevice": "¿Seguro que quieres cerrar la sesión de «{0}»?",
|
||||
"MessageConfirmMarkAllEpisodesFinished": "¿Confirma que quiere marcar todos los episodios como terminados?",
|
||||
"MessageConfirmMarkAllEpisodesNotFinished": "¿Confirma que quiere marcar todos los episodios como no terminados?",
|
||||
"MessageConfirmMarkItemFinished": "¿Confirma que quiere marcar «{0}» como terminado?",
|
||||
@@ -768,8 +785,8 @@
|
||||
"MessageConfirmMarkSeriesFinished": "¿Confirma que quiere marcar todos los libros de esta serie como terminados?",
|
||||
"MessageConfirmMarkSeriesNotFinished": "¿Confirma que quiere marcar todos los libros de esta serie como no terminados?",
|
||||
"MessageConfirmNotificationTestTrigger": "¿Activar esta notificación con datos de prueba?",
|
||||
"MessageConfirmPurgeCache": "Purgar la antememoria eliminará el directorio completo ubicado en <code>/metadata/cache</code>. <br /><br />¿Confirma que quiere eliminar el directorio de antememoria?",
|
||||
"MessageConfirmPurgeItemsCache": "Purgar la antememoria de elementos eliminará el directorio completo ubicado en <code>/metadata/cache/items</code>.<br />¿Lo confirma?",
|
||||
"MessageConfirmPurgeCache": "La purga del caché eliminará el directorio completo en <code>/metadata/cache</code>. <br /><br />¿Confirma que desea quitar el directorio de caché?",
|
||||
"MessageConfirmPurgeItemsCache": "Purgar el caché de elementos eliminará el directorio completo ubicado en <code>/metadata/cache/items</code>.<br />¿Lo confirma?",
|
||||
"MessageConfirmQuickEmbed": "Atención: la incrustación rápida no realiza copias de respaldo a ninguno de sus archivos de audio. Cerciórese de haber realizado una copia de los mismos previamente. <br><br>¿Quiere continuar?",
|
||||
"MessageConfirmQuickMatchEpisodes": "El reconocimiento rápido de extensiones sobrescribirá los detalles si se encuentra una coincidencia. Se actualizarán las extensiones no reconocidas. ¿Quiere continuar?",
|
||||
"MessageConfirmReScanLibraryItems": "¿Confirma que quiere volver a analizar {0} elementos?",
|
||||
@@ -795,14 +812,16 @@
|
||||
"MessageDaysListenedInTheLastYear": "{0} días escuchados el año pasado",
|
||||
"MessageDownloadingEpisode": "Descargando episodio",
|
||||
"MessageDragFilesIntoTrackOrder": "Arrastre los archivos al orden correcto de las pistas",
|
||||
"MessageEmbedFailed": "Falló la incrustación.",
|
||||
"MessageEmbedFinished": "Finalizó la incrustación.",
|
||||
"MessageEmbedFailed": "Incorporación incorrecta.",
|
||||
"MessageEmbedFinished": "Incorporación finalizada.",
|
||||
"MessageEmbedQueue": "En cola para incrustar metadatos ({0} en cola)",
|
||||
"MessageEpisodesQueuedForDownload": "{0} episodio(s) en cola para descargar",
|
||||
"MessageEreaderDevices": "Para garantizar la entrega de libros electrónicos, es posible que tenga que agregar la dirección de correo electrónico anterior como remitente válido para cada dispositivo enumerado a continuación.",
|
||||
"MessageFeedURLWillBe": "El URL del suministro será {0}",
|
||||
"MessageFetching": "Recuperando...",
|
||||
"MessageForceReScanDescription": "Escaneará todos los archivos como un nuevo escaneo. Archivos de audio con etiquetas ID3, archivos OPF y archivos de texto serán escaneados como nuevos.",
|
||||
"MessageHeatmapListeningTimeTooltip": "<strong>{0} escuchando</strong> en {1}",
|
||||
"MessageHeatmapNoListeningSessions": "No enumera sesiones en {0}",
|
||||
"MessageImportantNotice": "¡Notificación importante!",
|
||||
"MessageInsertChapterBelow": "Insertar capítulo debajo",
|
||||
"MessageInvalidAsin": "ASIN no válido",
|
||||
@@ -835,7 +854,7 @@
|
||||
"MessageNoEpisodes": "Ningún episodio",
|
||||
"MessageNoFoldersAvailable": "Ninguna carpeta disponible",
|
||||
"MessageNoGenres": "Ningún género",
|
||||
"MessageNoIssues": "Ningún número",
|
||||
"MessageNoIssues": "Sin incidencias",
|
||||
"MessageNoItems": "Ningún elemento",
|
||||
"MessageNoItemsFound": "Ningún elemento encontrado",
|
||||
"MessageNoListeningSessions": "Ninguna sesión de escucha",
|
||||
@@ -873,7 +892,7 @@
|
||||
"MessageResetChaptersConfirm": "¿Confirma que quiere deshacer los cambios y restablecer los capítulos a su estado original?",
|
||||
"MessageRestoreBackupConfirm": "¿Confirma que quiere restaurar el respaldo creado el",
|
||||
"MessageRestoreBackupWarning": "Restaurar sobrescribirá toda la base de datos localizada en /config y las imágenes de portadas en /metadata/items y /metadata/authors.<br /><br />El respaldo no modifica ningún archivo en las carpetas de su biblioteca. Si ha habilitado la opción del servidor para almacenar portadas y metadata en las carpetas de su biblioteca, esos archivos no se respaldan o sobrescriben.<br /><br />Todos los clientes que usen su servidor se actualizarán automáticamente.",
|
||||
"MessageScheduleLibraryScanNote": "Para la mayoría de los usuarios, se recomienda dejar esta función desactivada y mantener activada la configuración del observador de carpetas. El observador de carpetas detectará automáticamente los cambios en las carpetas de la biblioteca. El observador de carpetas no funciona para todos los sistemas de archivos (como NFS), por lo que se pueden utilizar exploraciones programadas de la biblioteca en su lugar.",
|
||||
"MessageScheduleLibraryScanNote": "Para muchos usuarios, es recomendado dejar esta característica inhabilitada y mantener habilitados los ajustes de la «Vigía automática de cambio de biblioteca»: detectará automáticamente los cambios en sus carpetas de bibliotecas. Habilitar esta características si «Vigía automática de cambio de biblioteca» no funciona en su sistema de archivo (como NFS).",
|
||||
"MessageScheduleRunEveryWeekdayAtTime": "Ejecutar cada {0} a las {1}",
|
||||
"MessageSearchResultsFor": "Resultados de la búsqueda de",
|
||||
"MessageSelected": "{0} seleccionado(s)",
|
||||
@@ -999,6 +1018,8 @@
|
||||
"ToastBulkChapterInvalidCount": "Por favor ingrese un número válido entre 1 y 150",
|
||||
"ToastCachePurgeFailed": "No se pudo purgar la antememoria",
|
||||
"ToastCachePurgeSuccess": "Se purgó la antememoria correctamente",
|
||||
"ToastChapterLocked": "El capítulo está bloqueado.",
|
||||
"ToastChapterStartTimeAdjusted": "El capítulo inicia el tiempo ajustado en {0} segundos",
|
||||
"ToastChaptersAllLocked": "Todos los capítulos están bloqueados. Desbloquee algunos capítulos para cambiar sus tiempos.",
|
||||
"ToastChaptersHaveErrors": "Los capítulos tienen errores",
|
||||
"ToastChaptersInvalidShiftAmountLast": "Cantidad de desplazamiento no válida. La hora de inicio del último capítulo se extendería más allá de la duración de este audiolibro.",
|
||||
@@ -1009,6 +1030,8 @@
|
||||
"ToastCollectionItemsAddFailed": "Artículo(s) añadido(s) a la colección fallido(s)",
|
||||
"ToastCollectionRemoveSuccess": "Colección quitada",
|
||||
"ToastCollectionUpdateSuccess": "Colección actualizada",
|
||||
"ToastConnectionNotAvailable": "Conexión no disponible. Intenta de nuevo más tarde",
|
||||
"ToastCoverSearchFailed": "Cobertura de búsqueda incorrecta",
|
||||
"ToastCoverUpdateFailed": "Error al actualizar la cubierta",
|
||||
"ToastDateTimeInvalidOrIncomplete": "Fecha y hora no válidas o incompletas",
|
||||
"ToastDeleteFileFailed": "Falló la eliminación del archivo",
|
||||
@@ -1024,6 +1047,8 @@
|
||||
"ToastEpisodeDownloadQueueClearSuccess": "Se borró la cola de descargas de los episodios",
|
||||
"ToastEpisodeUpdateSuccess": "{0} episodio(s) actualizado(s)",
|
||||
"ToastErrorCannotShare": "No se puede compartir de forma nativa en este dispositivo",
|
||||
"ToastFailedToCreate": "Ha fallado al crear",
|
||||
"ToastFailedToDelete": "Ha fallado al eliminar",
|
||||
"ToastFailedToLoadData": "Error al cargar data",
|
||||
"ToastFailedToMatch": "Error al emparejar",
|
||||
"ToastFailedToShare": "Error al compartir",
|
||||
@@ -1031,6 +1056,7 @@
|
||||
"ToastInvalidImageUrl": "URL de la imagen no válida",
|
||||
"ToastInvalidMaxEpisodesToDownload": "Número máximo de episodios para descargar no válidos",
|
||||
"ToastInvalidUrl": "URL no válida",
|
||||
"ToastInvalidUrls": "Una o más URL son inválidas",
|
||||
"ToastItemCoverUpdateSuccess": "Cubierta del elemento actualizada",
|
||||
"ToastItemDeletedFailed": "Error al eliminar el elemento",
|
||||
"ToastItemDeletedSuccess": "Elemento borrado",
|
||||
@@ -1055,6 +1081,7 @@
|
||||
"ToastMustHaveAtLeastOnePath": "Debe tener al menos una ruta",
|
||||
"ToastNameEmailRequired": "Son obligatorios el nombre y el correo electrónico",
|
||||
"ToastNameRequired": "Nombre obligatorio",
|
||||
"ToastNewApiKeyUserError": "Debe seleccionar un usuario",
|
||||
"ToastNewEpisodesFound": "{0} nuevo(s) episodio(s) encontrado(s)",
|
||||
"ToastNewUserCreatedFailed": "No se pudo crear la cuenta: «{0}»",
|
||||
"ToastNewUserCreatedSuccess": "Nueva cuenta creada",
|
||||
@@ -1093,8 +1120,8 @@
|
||||
"ToastRemoveFailed": "Error al eliminar",
|
||||
"ToastRemoveItemFromCollectionFailed": "Error al eliminar el elemento de la colección",
|
||||
"ToastRemoveItemFromCollectionSuccess": "Elemento eliminado de la colección",
|
||||
"ToastRemoveItemsWithIssuesFailed": "Error en la eliminación de artículos de biblioteca incorrectos",
|
||||
"ToastRemoveItemsWithIssuesSuccess": "Se eliminaron artículos de biblioteca incorrectos",
|
||||
"ToastRemoveItemsWithIssuesFailed": "Error en la eliminación de artículos de biblioteca con incidencias",
|
||||
"ToastRemoveItemsWithIssuesSuccess": "Se eliminaron artículos de biblioteca con incidencias",
|
||||
"ToastRenameFailed": "Error al cambiar el nombre",
|
||||
"ToastRescanFailed": "Error al volver a escanear para {0}",
|
||||
"ToastRescanRemoved": "Se eliminó el elemento reescaneado",
|
||||
@@ -1133,6 +1160,7 @@
|
||||
"ToastUserRootRequireName": "Debe introducir un nombre de usuario administrativo",
|
||||
"TooltipAddChapters": "Añadir capítulo(s)",
|
||||
"TooltipAddOneSecond": "Añadir 1 segundo",
|
||||
"TooltipAdjustChapterStart": "Pulse para ajustar la hora de inicio",
|
||||
"TooltipLockAllChapters": "Bloquear todos los capítulos",
|
||||
"TooltipLockChapter": "Bloquear capítulo (Mayús+clic para rango)",
|
||||
"TooltipSubtractOneSecond": "Restar 1 segundo",
|
||||
|
||||
+12
-9
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"ButtonAdd": "Lisa",
|
||||
"ButtonAddApiKey": "Lisa API võti",
|
||||
"ButtonAddChapters": "Lisa peatükid",
|
||||
"ButtonAddDevice": "Lisa seade",
|
||||
"ButtonAddLibrary": "Lisa raamatukogu",
|
||||
@@ -53,7 +54,7 @@
|
||||
"ButtonOpenFeed": "Ava voog",
|
||||
"ButtonOpenManager": "Ava haldur",
|
||||
"ButtonPause": "Paus",
|
||||
"ButtonPlay": "Play",
|
||||
"ButtonPlay": "Mängi",
|
||||
"ButtonPlayAll": "Mängi kõik",
|
||||
"ButtonPlaying": "Mängib",
|
||||
"ButtonPlaylists": "Playlist",
|
||||
@@ -159,7 +160,7 @@
|
||||
"HeaderPlaylistItems": "Playlisti esemed",
|
||||
"HeaderPodcastsToAdd": "Lisatavad podcastid",
|
||||
"HeaderPreviewCover": "Eelvaate kaas",
|
||||
"HeaderRSSFeedGeneral": "RSS-i üksikasjad",
|
||||
"HeaderRSSFeedGeneral": "RSS detailid",
|
||||
"HeaderRSSFeedIsOpen": "RSS-voog on avatud",
|
||||
"HeaderRSSFeeds": "RSS-vooged",
|
||||
"HeaderRemoveEpisode": "Eemalda episood",
|
||||
@@ -177,7 +178,7 @@
|
||||
"HeaderSleepTimer": "Unetaimer",
|
||||
"HeaderStatsLargestItems": "Suurimad esemed",
|
||||
"HeaderStatsLongestItems": "Kõige pikemad esemed (tunnid)",
|
||||
"HeaderStatsMinutesListeningChart": "Kuulamise minutid (viimased 7 päeva)",
|
||||
"HeaderStatsMinutesListeningChart": "Kuulatud minutid (viimased 7 päeva)",
|
||||
"HeaderStatsRecentSessions": "Hiljutised sessioonid",
|
||||
"HeaderStatsTop10Authors": "Top 10 autorit",
|
||||
"HeaderStatsTop5Genres": "Top 5 žanrit",
|
||||
@@ -245,7 +246,7 @@
|
||||
"LabelConfirmPassword": "Kinnita parool",
|
||||
"LabelContinueListening": "Jätka kuulamist",
|
||||
"LabelContinueReading": "Jätka lugemist",
|
||||
"LabelContinueSeries": "Jätka seeriat",
|
||||
"LabelContinueSeries": "Jäta sarja",
|
||||
"LabelCover": "Ümbris",
|
||||
"LabelCoverImageURL": "Ümbrise pildi URL",
|
||||
"LabelCreatedAt": "Loodud",
|
||||
@@ -325,9 +326,10 @@
|
||||
"LabelIntervalEveryDay": "Iga päev",
|
||||
"LabelIntervalEveryHour": "Iga tunni tagant",
|
||||
"LabelInvert": "Pööra ümber",
|
||||
"LabelIpAddress": "IP-aadress",
|
||||
"LabelItem": "Kirje",
|
||||
"LabelLanguage": "Keel",
|
||||
"LabelLanguageDefaultServer": "Vaikeserveri keel",
|
||||
"LabelLanguageDefaultServer": "Serveri vaikekeel",
|
||||
"LabelLastBookAdded": "Viimati lisatud raamat",
|
||||
"LabelLastBookUpdated": "Viimati uuendatud raamat",
|
||||
"LabelLastSeen": "Viimati nähtud",
|
||||
@@ -368,8 +370,8 @@
|
||||
"LabelNarrators": "Jutustajad",
|
||||
"LabelNew": "Uus",
|
||||
"LabelNewPassword": "Uus parool",
|
||||
"LabelNewestAuthors": "Uuemad autorid",
|
||||
"LabelNewestEpisodes": "Uuemad episoodid",
|
||||
"LabelNewestAuthors": "Uusimad autorid",
|
||||
"LabelNewestEpisodes": "Uusimad episoodid",
|
||||
"LabelNextBackupDate": "Järgmine varukoopia kuupäev",
|
||||
"LabelNextScheduledRun": "Järgmine ajakava järgmine",
|
||||
"LabelNoEpisodesSelected": "Episoodid pole valitud",
|
||||
@@ -458,8 +460,8 @@
|
||||
"LabelSettingsFindCoversHelp": "Kui teie heliraamatul pole sisseehitatud ümbrist ega ümbrise pilti kaustas, proovib skanner leida ümbrist.<br>Märkus: see pikendab skaneerimisaega",
|
||||
"LabelSettingsHideSingleBookSeries": "Peida üksikute raamatute seeriad",
|
||||
"LabelSettingsHideSingleBookSeriesHelp": "Ühe raamatuga seeriaid peidetakse seeria lehelt ja avalehe riiulitelt.",
|
||||
"LabelSettingsHomePageBookshelfView": "Avaleht kasutage raamatukoguvaadet",
|
||||
"LabelSettingsLibraryBookshelfView": "Raamatukogu kasutamiseks kasutage raamatukoguvaadet",
|
||||
"LabelSettingsHomePageBookshelfView": "Avalehel kasuta raamaturiiulivaadet",
|
||||
"LabelSettingsLibraryBookshelfView": "Raamatukogus kasuta raamaturiiulivaadet",
|
||||
"LabelSettingsParseSubtitles": "Lugege subtiitreid",
|
||||
"LabelSettingsParseSubtitlesHelp": "Eraldage subtiitrid heliraamatu kaustade nimedest.<br>Subtiitrid peavad olema eraldatud kasutades \" - \".<br>Näiteks: \"Raamatu pealkiri - Siin on alapealkiri\" alapealkiri on \"Siin on alapealkiri\"",
|
||||
"LabelSettingsPreferMatchedMetadata": "Eelista sobitatud metaandmeid",
|
||||
@@ -565,6 +567,7 @@
|
||||
"MessageBookshelfNoCollectionsHelp": "Kollektsioonid on avalikud. Kõik kasutajad kellel on olemas ligipääs raamatukogule saavad neid näha.",
|
||||
"MessageBookshelfNoRSSFeeds": "Ühtegi RSS-i voogu pole avatud",
|
||||
"MessageBookshelfNoResultsForFilter": "Filtrile \"{0}: {1}\" pole tulemusi",
|
||||
"MessageBookshelfNoResultsForQuery": "Päringul ei ole tulemusi",
|
||||
"MessageBookshelfNoSeries": "Teil pole ühtegi seeriat",
|
||||
"MessageChapterEndIsAfter": "Peatüki lõpp on pärast teie heliraamatu lõppu",
|
||||
"MessageChapterErrorFirstNotZero": "Esimene peatükk peab algama 0-st",
|
||||
|
||||
@@ -46,6 +46,7 @@
|
||||
"ButtonLatest": "Viimeisimmät",
|
||||
"ButtonLibrary": "Kirjasto",
|
||||
"ButtonLogout": "Kirjaudu ulos",
|
||||
"ButtonLogoutAllDevices": "Kirjaudu ulos kaikista laitteista",
|
||||
"ButtonLookup": "Hae",
|
||||
"ButtonManageTracks": "Hallitse raitoja",
|
||||
"ButtonMapChapterTitles": "Kartoita lukujen nimet",
|
||||
@@ -194,6 +195,7 @@
|
||||
"HeaderScheduleEpisodeDownloads": "Ajoita automaattiset jaksolataukset",
|
||||
"HeaderScheduleLibraryScans": "Ajoita automaattiset kirjastoskannaukset",
|
||||
"HeaderSession": "Istunto",
|
||||
"HeaderSessions": "Istunnot",
|
||||
"HeaderSetBackupSchedule": "Aseta varmuuskopiointiaikataulu",
|
||||
"HeaderSettings": "Asetukset",
|
||||
"HeaderSettingsDisplay": "Näyttö",
|
||||
@@ -415,6 +417,7 @@
|
||||
"LabelIntervalEveryHour": "Joka tunti",
|
||||
"LabelIntervalEveryMinute": "Joka minuutti",
|
||||
"LabelInvert": "Saa käänteiseksi",
|
||||
"LabelIpAddress": "IP-osoite",
|
||||
"LabelItem": "Kohde",
|
||||
"LabelJumpBackwardAmount": "Taaksepäin-hyppyjen määrä",
|
||||
"LabelJumpForwardAmount": "Eteenpäin-hyppyjen määrä",
|
||||
@@ -774,6 +777,7 @@
|
||||
"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?",
|
||||
"MessageConfirmLogoutDevice": "Haluatko varmasti kirjautua ulos palvelusta \"{0}\"?",
|
||||
"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?",
|
||||
|
||||
@@ -46,6 +46,7 @@
|
||||
"ButtonLatest": "Dernière version",
|
||||
"ButtonLibrary": "Bibliothèque",
|
||||
"ButtonLogout": "Déconnexion",
|
||||
"ButtonLogoutAllDevices": "Se déconnecter de tous les appareils",
|
||||
"ButtonLookup": "Chercher",
|
||||
"ButtonManageTracks": "Gérer les pistes",
|
||||
"ButtonMapChapterTitles": "Correspondance des titres de chapitres",
|
||||
@@ -139,7 +140,7 @@
|
||||
"HeaderCustomMetadataProviders": "Fournisseurs de métadonnées personnalisés",
|
||||
"HeaderDetails": "Détails",
|
||||
"HeaderDownloadQueue": "File d’attente de téléchargements",
|
||||
"HeaderEbookFiles": "Fichiers des livres numériques",
|
||||
"HeaderEbookFiles": "Fichiers du livre",
|
||||
"HeaderEmail": "Courriel",
|
||||
"HeaderEmailSettings": "Configuration de l’envoie des courriels",
|
||||
"HeaderEpisodes": "Épisodes",
|
||||
@@ -194,6 +195,7 @@
|
||||
"HeaderScheduleEpisodeDownloads": "Programmer des téléchargements automatiques d'épisodes",
|
||||
"HeaderScheduleLibraryScans": "Analyse automatique de la bibliothèque",
|
||||
"HeaderSession": "Session",
|
||||
"HeaderSessions": "Sessions",
|
||||
"HeaderSetBackupSchedule": "Activer la sauvegarde automatique",
|
||||
"HeaderSettings": "Paramètres",
|
||||
"HeaderSettingsDisplay": "Affichage",
|
||||
@@ -415,6 +417,7 @@
|
||||
"LabelIntervalEveryHour": "Toutes les heures",
|
||||
"LabelIntervalEveryMinute": "Toutes les minutes",
|
||||
"LabelInvert": "Inverser",
|
||||
"LabelIpAddress": "Adresse IP",
|
||||
"LabelItem": "Élément",
|
||||
"LabelJumpBackwardAmount": "Dans le lecteur, reculer de",
|
||||
"LabelJumpForwardAmount": "Dans le lecteur, avancer de",
|
||||
@@ -538,7 +541,7 @@
|
||||
"LabelPublishedDecades": "Décennies de publication",
|
||||
"LabelPublisher": "Éditeur",
|
||||
"LabelPublishers": "Éditeurs",
|
||||
"LabelRSSFeedCustomOwnerEmail": "Courriel personnalisée du propriétaire",
|
||||
"LabelRSSFeedCustomOwnerEmail": "Courriel personnalisé du propriétaire",
|
||||
"LabelRSSFeedCustomOwnerName": "Nom propriétaire personnalisé",
|
||||
"LabelRSSFeedOpen": "Flux RSS ouvert",
|
||||
"LabelRSSFeedPreventIndexing": "Empêcher l’indexation",
|
||||
@@ -622,7 +625,7 @@
|
||||
"LabelSettingsStoreMetadataWithItemHelp": "Par défaut, les fichiers de métadonnées sont stockés dans /metadata/items. En activant ce paramètre, les fichiers de métadonnées seront stockés dans les dossiers des éléments de votre bibliothèque",
|
||||
"LabelSettingsTimeFormat": "Format d’heure",
|
||||
"LabelShare": "Partager",
|
||||
"LabelShareDownloadableHelp": "Permet aux utilisateurs de télécharger un fichier ZIP de l'élément de la bibliothèque.",
|
||||
"LabelShareDownloadableHelp": "Permet aux utilisateurs disposant du lien de partage de télécharger un fichier zip contenant l'élément de la bibliothèque.",
|
||||
"LabelShareOpen": "Ouvrir le partage",
|
||||
"LabelShareURL": "Partager l’URL",
|
||||
"LabelShowAll": "Tout afficher",
|
||||
@@ -774,6 +777,7 @@
|
||||
"MessageConfirmDeleteSession": "Êtes-vous sûr·e de vouloir supprimer cette session ?",
|
||||
"MessageConfirmEmbedMetadataInAudioFiles": "Êtes-vous sûr·e de vouloir intégrer des métadonnées dans {0} fichiers audio ?",
|
||||
"MessageConfirmForceReScan": "Êtes-vous sûr·e de vouloir lancer une analyse forcée ?",
|
||||
"MessageConfirmLogoutDevice": "Êtes-vous sûr·e de vouloir vous déconnecter de « {0} » ?",
|
||||
"MessageConfirmMarkAllEpisodesFinished": "Êtes-vous sûr·e de marquer tous les épisodes comme terminés ?",
|
||||
"MessageConfirmMarkAllEpisodesNotFinished": "Êtes-vous sûr·e de vouloir marquer tous les épisodes comme non terminés ?",
|
||||
"MessageConfirmMarkItemFinished": "Êtes-vous sûr·e de vouloir marquer {0} comme terminé ?",
|
||||
|
||||
@@ -46,6 +46,7 @@
|
||||
"ButtonLatest": "Najnovije",
|
||||
"ButtonLibrary": "Knjižnica",
|
||||
"ButtonLogout": "Odjavi se",
|
||||
"ButtonLogoutAllDevices": "Odjava sa svih uređaja",
|
||||
"ButtonLookup": "Potraži",
|
||||
"ButtonManageTracks": "Upravljanje zvučnim zapisima",
|
||||
"ButtonMapChapterTitles": "Mapiraj naslove poglavlja",
|
||||
@@ -154,7 +155,7 @@
|
||||
"HeaderLatestEpisodes": "Najnoviji nastavci",
|
||||
"HeaderLibraries": "Knjižnice",
|
||||
"HeaderLibraryFiles": "Datoteke knjižnice",
|
||||
"HeaderLibraryStats": "Statistika knjižnice",
|
||||
"HeaderLibraryStats": "Knjižnična statistika",
|
||||
"HeaderListeningSessions": "Sesije slušanja",
|
||||
"HeaderListeningStats": "Statistika slušanja",
|
||||
"HeaderLogin": "Prijava",
|
||||
@@ -194,6 +195,7 @@
|
||||
"HeaderScheduleEpisodeDownloads": "Zakazivanje automatskog preuzimanja nastavaka",
|
||||
"HeaderScheduleLibraryScans": "Zakaži automatsko skeniranje knjižnice",
|
||||
"HeaderSession": "Sesija",
|
||||
"HeaderSessions": "Sesije",
|
||||
"HeaderSetBackupSchedule": "Zakazivanje sigurnosne pohrane",
|
||||
"HeaderSettings": "Postavke",
|
||||
"HeaderSettingsDisplay": "Prikaz",
|
||||
@@ -415,6 +417,7 @@
|
||||
"LabelIntervalEveryHour": "Svaki sat",
|
||||
"LabelIntervalEveryMinute": "Svaku minutu",
|
||||
"LabelInvert": "Obrni",
|
||||
"LabelIpAddress": "IP adresa",
|
||||
"LabelItem": "Stavka",
|
||||
"LabelJumpBackwardAmount": "Dužina skoka unatrag",
|
||||
"LabelJumpForwardAmount": "Dužina skoka unaprijed",
|
||||
@@ -774,6 +777,7 @@
|
||||
"MessageConfirmDeleteSession": "Sigurno želite izbrisati ovu sesiju?",
|
||||
"MessageConfirmEmbedMetadataInAudioFiles": "Sigurno želite ugraditi meta-podatke u {0} zvučnih datoteka?",
|
||||
"MessageConfirmForceReScan": "Sigurno želite ponovno pokrenuti skeniranje?",
|
||||
"MessageConfirmLogoutDevice": "Sigurno se želiš odjaviti iz \"{0}\"?",
|
||||
"MessageConfirmMarkAllEpisodesFinished": "Sigurno želite označiti sve nastavke kao završene?",
|
||||
"MessageConfirmMarkAllEpisodesNotFinished": "Sigurno želite označiti sve nastavke kao nezavršene?",
|
||||
"MessageConfirmMarkItemFinished": "Sigurno želite označiti stavku \"{0}\" kao završenu?",
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
"ButtonBrowseForFolder": "Mappa keresése",
|
||||
"ButtonCancel": "Mégse",
|
||||
"ButtonCancelEncode": "Kódolás megszakítása",
|
||||
"ButtonChangeRootPassword": "Gyökérjelszó megváltoztatása",
|
||||
"ButtonChangeRootPassword": "Root jelszó megváltoztatása",
|
||||
"ButtonCheckAndDownloadNewEpisodes": "Új epizódok ellenőrzése és letöltése",
|
||||
"ButtonChooseAFolder": "Válassz egy mappát",
|
||||
"ButtonChooseFiles": "Fájlok kiválasztása",
|
||||
@@ -46,6 +46,7 @@
|
||||
"ButtonLatest": "Legújabb",
|
||||
"ButtonLibrary": "Könyvtár",
|
||||
"ButtonLogout": "Kijelentkezés",
|
||||
"ButtonLogoutAllDevices": "Kijelentkezés az összes eszközről",
|
||||
"ButtonLookup": "Keresés",
|
||||
"ButtonManageTracks": "Sávok kezelése",
|
||||
"ButtonMapChapterTitles": "Fejezetcímek hozzárendelése",
|
||||
@@ -194,6 +195,7 @@
|
||||
"HeaderScheduleEpisodeDownloads": "Automatikus epizódletöltés ütemezése",
|
||||
"HeaderScheduleLibraryScans": "Könyvtárak automatikus szkennelésének ütemezése",
|
||||
"HeaderSession": "Munkamenet",
|
||||
"HeaderSessions": "Munkamenet",
|
||||
"HeaderSetBackupSchedule": "Biztonsági másolatok ütemezésének beállítása",
|
||||
"HeaderSettings": "Beállítások",
|
||||
"HeaderSettingsDisplay": "Kijelző",
|
||||
@@ -415,6 +417,7 @@
|
||||
"LabelIntervalEveryHour": "Minden órában",
|
||||
"LabelIntervalEveryMinute": "Minden percben",
|
||||
"LabelInvert": "Inverz",
|
||||
"LabelIpAddress": "IP-cím",
|
||||
"LabelItem": "Elem",
|
||||
"LabelJumpBackwardAmount": "Visszafelé ugrás mennyisége",
|
||||
"LabelJumpForwardAmount": "Előre ugrás mennyisége",
|
||||
@@ -774,6 +777,7 @@
|
||||
"MessageConfirmDeleteSession": "Biztosan törölni szeretné ezt a munkamenetet?",
|
||||
"MessageConfirmEmbedMetadataInAudioFiles": "Biztos, hogy metaadatokat szeretne beágyazni {0} hangfájlba?",
|
||||
"MessageConfirmForceReScan": "Biztosan kényszeríteni szeretné az újraszkennelést?",
|
||||
"MessageConfirmLogoutDevice": "Biztosan ki szeretne jelentkezni a „{0}”-ból?",
|
||||
"MessageConfirmMarkAllEpisodesFinished": "Biztosan meg szeretné jelölni az összes epizódot befejezettnek?",
|
||||
"MessageConfirmMarkAllEpisodesNotFinished": "Biztosan meg szeretné jelölni az összes epizódot nem befejezettnek?",
|
||||
"MessageConfirmMarkItemFinished": "Biztos, hogy a „{0}”-t befejezettnek akarja jelölni?",
|
||||
|
||||
+32
-1
@@ -1 +1,32 @@
|
||||
{}
|
||||
{
|
||||
"ButtonAdd": "Bæta við",
|
||||
"ButtonAddApiKey": "Bæta við API-lykli",
|
||||
"ButtonAddChapters": "Bæta við köflum",
|
||||
"ButtonAddDevice": "Bæta við tæki",
|
||||
"ButtonAddLibrary": "Bæta við bókasafni",
|
||||
"ButtonAddPodcasts": "Bæta við hlaðvörpum",
|
||||
"ButtonAddUser": "Bæta við notanda",
|
||||
"ButtonAddYourFirstLibrary": "Bættu fyrsta bókasafninu þínu við",
|
||||
"ButtonApply": "Nota",
|
||||
"ButtonApplyChapters": "Skrá kafla",
|
||||
"ButtonAuthors": "Rithöfundar",
|
||||
"ButtonBack": "Bakka",
|
||||
"ButtonBatchEditPopulateFromExisting": "Afrita frá núverandi",
|
||||
"ButtonBatchEditPopulateMapDetails": "Bæta við upplýsingum",
|
||||
"ButtonBrowseForFolder": "Skoða möppu",
|
||||
"ButtonCancel": "Hætta við",
|
||||
"ButtonCancelEncode": "Hætta við Encoding",
|
||||
"ButtonChangeRootPassword": "Breyta Root lykilorði",
|
||||
"ButtonCheckAndDownloadNewEpisodes": "Skoða og sækja nýja þætti",
|
||||
"ButtonChooseAFolder": "Velja möppu",
|
||||
"ButtonChooseFiles": "Velja skrár",
|
||||
"ButtonClearFilter": "Hreinsa síu",
|
||||
"ButtonClose": "Loka",
|
||||
"ButtonCloseSession": "Loka opnu sessioni",
|
||||
"ButtonCollections": "Söfn",
|
||||
"ButtonConfigureScanner": "Stilla skannara",
|
||||
"ButtonCreate": "Stofna",
|
||||
"ButtonCreateBackup": "Búa til backup",
|
||||
"ButtonDelete": "Eyða",
|
||||
"ButtonDownloadQueue": "Röð"
|
||||
}
|
||||
+18
-14
@@ -34,7 +34,7 @@
|
||||
"ButtonEditChapters": "Modifica Capitoli",
|
||||
"ButtonEditPodcast": "Modifica Podcast",
|
||||
"ButtonEnable": "Abilita",
|
||||
"ButtonFireAndFail": "Fire and Fail",
|
||||
"ButtonFireAndFail": "Centro e fallimento",
|
||||
"ButtonFireOnTest": "Fire onTest event",
|
||||
"ButtonForceReScan": "Forza Re-Scan",
|
||||
"ButtonFullPath": "Percorso Completo",
|
||||
@@ -46,6 +46,7 @@
|
||||
"ButtonLatest": "Ultimi",
|
||||
"ButtonLibrary": "Biblioteca",
|
||||
"ButtonLogout": "Disconnetti",
|
||||
"ButtonLogoutAllDevices": "Disconnetti su tutti i dispositivi",
|
||||
"ButtonLookup": "Consulta",
|
||||
"ButtonManageTracks": "Gestisci le Tracce",
|
||||
"ButtonMapChapterTitles": "Titoli dei Capitoli",
|
||||
@@ -139,7 +140,7 @@
|
||||
"HeaderCustomMetadataProviders": "Metadata Providers Personalizzato",
|
||||
"HeaderDetails": "Dettagli",
|
||||
"HeaderDownloadQueue": "Download coda",
|
||||
"HeaderEbookFiles": "File dei libri",
|
||||
"HeaderEbookFiles": "File del libro",
|
||||
"HeaderEmail": "E-mail",
|
||||
"HeaderEmailSettings": "Impostazioni e-mail",
|
||||
"HeaderEpisodes": "Episodi",
|
||||
@@ -182,7 +183,7 @@
|
||||
"HeaderPlaylist": "Playlist",
|
||||
"HeaderPlaylistItems": "Elementi della playlist",
|
||||
"HeaderPodcastsToAdd": "Podcasts da Aggiungere",
|
||||
"HeaderPresets": "Presets",
|
||||
"HeaderPresets": "Preimpostazioni",
|
||||
"HeaderPreviewCover": "Anteprima Cover",
|
||||
"HeaderRSSFeedGeneral": "Dettagli RSS",
|
||||
"HeaderRSSFeedIsOpen": "RSS Feed è aperto",
|
||||
@@ -194,6 +195,7 @@
|
||||
"HeaderScheduleEpisodeDownloads": "Imposta il download automatico degli episodi",
|
||||
"HeaderScheduleLibraryScans": "Schedula la scansione della libreria",
|
||||
"HeaderSession": "Sessione",
|
||||
"HeaderSessions": "Sessioni",
|
||||
"HeaderSetBackupSchedule": "Imposta programmazione Backup",
|
||||
"HeaderSettings": "Impostazioni",
|
||||
"HeaderSettingsDisplay": "Visualizzazione",
|
||||
@@ -306,7 +308,7 @@
|
||||
"LabelCustomCronExpression": "Espressione Cron personalizzata:",
|
||||
"LabelDatetime": "Data & Ora",
|
||||
"LabelDays": "Giorni",
|
||||
"LabelDeleteFromFileSystemCheckbox": "Elimina dal file system (togli la spunta per eliminarla solo dal DB)",
|
||||
"LabelDeleteFromFileSystemCheckbox": "Elimina dal file system (despunta per rimuoverla solo dal database)",
|
||||
"LabelDescription": "Descrizione",
|
||||
"LabelDeselectAll": "Deseleziona Tutto",
|
||||
"LabelDetectedPattern": "Trovato pattern:",
|
||||
@@ -415,6 +417,7 @@
|
||||
"LabelIntervalEveryHour": "Ogni ora",
|
||||
"LabelIntervalEveryMinute": "Ogni minuto",
|
||||
"LabelInvert": "Inverti",
|
||||
"LabelIpAddress": "Indirizzo Ip",
|
||||
"LabelItem": "Oggetti",
|
||||
"LabelJumpBackwardAmount": "secondi di avvolgimento",
|
||||
"LabelJumpForwardAmount": "Secondi di Avvolgimento",
|
||||
@@ -436,9 +439,9 @@
|
||||
"LabelLibraryFilterSublistEmpty": "Nessuno {0}",
|
||||
"LabelLibraryItem": "Elementi della biblioteca",
|
||||
"LabelLibraryName": "Nome della biblioteca",
|
||||
"LabelLibrarySortByProgress": "Progressi: Ultimi aggiornamenti",
|
||||
"LabelLibrarySortByProgressFinished": "Progressi: Completati",
|
||||
"LabelLibrarySortByProgressStarted": "Progressi: Iniziati",
|
||||
"LabelLibrarySortByProgress": "Progresso: ultimo aggiornamento",
|
||||
"LabelLibrarySortByProgressFinished": "Progresso: finito",
|
||||
"LabelLibrarySortByProgressStarted": "Progresso: iniziato",
|
||||
"LabelLimit": "Limiti",
|
||||
"LabelLineSpacing": "Interlinea",
|
||||
"LabelListenAgain": "Ascolta ancora",
|
||||
@@ -497,7 +500,7 @@
|
||||
"LabelNumberOfBooks": "Numero di libri",
|
||||
"LabelNumberOfChapters": "Numero di capitoli:",
|
||||
"LabelNumberOfEpisodes": "Numero di episodi",
|
||||
"LabelOpenIDAdvancedPermsClaimDescription": "Nome dell'attestazione OpenID che contiene autorizzazioni avanzate per le azioni dell'utente all'interno dell'applicazione che verranno applicate ai ruoli non amministratori (<b>se configurato</b>). Se il reclamo manca nella risposta, l'accesso ad ABS verrà negato. Se manca una singola opzione, verrà trattata come<code>falsa</code>. Assicurati che l'attestazione del provider di identità corrisponda alla struttura prevista:",
|
||||
"LabelOpenIDAdvancedPermsClaimDescription": "Nome dell'attestazione OpenID che contiene autorizzazioni avanzate per le azioni dell'utente all'interno dell'applicazione che verranno applicate ai ruoli non amministrativi (<b>se configurato</b>). Se il reclamo manca nella risposta, l'accesso ad ABS verrà negato. Se manca una singola opzione, verrà trattata come <code>falso</code>. Assicurati che l'attestazione del provider di identità corrisponda alla struttura prevista:",
|
||||
"LabelOpenIDClaims": "Lasciare vuote le seguenti opzioni per disabilitare l'assegnazione avanzata di gruppi e autorizzazioni, assegnando quindi automaticamente il gruppo \"Utente\".",
|
||||
"LabelOpenIDGroupClaimDescription": "Nome dell'attestazione OpenID che contiene un elenco dei gruppi dell'utente. Comunemente indicato come <code>gruppo</code>. <b>se configurato</b>, l'applicazione assegnerà automaticamente i ruoli in base alle appartenenze ai gruppi dell'utente, a condizione che tali gruppi siano denominati \"admin\", \"utente\" o \"ospite\" senza distinzione tra maiuscole e minuscole nell'attestazione. L'attestazione deve contenere un elenco e, se un utente appartiene a più gruppi, l'applicazione assegnerà il ruolo corrispondente al livello di accesso più alto. Se nessun gruppo corrisponde, l'accesso verrà negato.",
|
||||
"LabelOpenRSSFeed": "Apri RSS Feed",
|
||||
@@ -530,7 +533,7 @@
|
||||
"LabelPrimaryEbook": "Libro principale",
|
||||
"LabelProgress": "Cominciati",
|
||||
"LabelProvider": "Fornitore",
|
||||
"LabelProviderAuthorizationValue": "Authorization Header Value",
|
||||
"LabelProviderAuthorizationValue": "Valore intestazione di autorizzazione",
|
||||
"LabelPubDate": "Data di pubblicazione",
|
||||
"LabelPublishYear": "Anno di pubblicazione",
|
||||
"LabelPublishedDate": "Pubblicati {0}",
|
||||
@@ -674,7 +677,7 @@
|
||||
"LabelTimeDurationXMinutes": "{0} minuti",
|
||||
"LabelTimeDurationXSeconds": "{0} secondi",
|
||||
"LabelTimeInMinutes": "Tempo in minuti",
|
||||
"LabelTimeLeft": "{0} sinistra",
|
||||
"LabelTimeLeft": "{0} rimasti",
|
||||
"LabelTimeListened": "Tempo di Ascolto",
|
||||
"LabelTimeListenedToday": "Tempo di Ascolto Oggi",
|
||||
"LabelTimeRemaining": "{0} rimanente",
|
||||
@@ -682,7 +685,7 @@
|
||||
"LabelTitle": "Titolo",
|
||||
"LabelToolsEmbedMetadata": "Incorpora Metadata",
|
||||
"LabelToolsEmbedMetadataDescription": "Incorpora i metadati nei file audio, inclusi l'immagine di copertina e i capitoli.",
|
||||
"LabelToolsM4bEncoder": "M4B Encoder",
|
||||
"LabelToolsM4bEncoder": "Codificatore M4B",
|
||||
"LabelToolsMakeM4b": "Crea un file M4B",
|
||||
"LabelToolsMakeM4bDescription": "Genera un file audiolibro M4B con metadati incorporati, immagine di copertina e capitoli.",
|
||||
"LabelToolsSplitM4b": "Converti M4B in MP3",
|
||||
@@ -774,6 +777,7 @@
|
||||
"MessageConfirmDeleteSession": "Sei sicuro di voler eliminare questa sessione?",
|
||||
"MessageConfirmEmbedMetadataInAudioFiles": "Sei sicuro di voler incorporare i metadati nei file audio {0}?",
|
||||
"MessageConfirmForceReScan": "Sei sicuro di voler forzare una nuova scansione?",
|
||||
"MessageConfirmLogoutDevice": "Sei sicuro/a di volerti disconnettere da «{0}»?",
|
||||
"MessageConfirmMarkAllEpisodesFinished": "Sei sicuro di voler contrassegnare tutti gli episodi come finiti?",
|
||||
"MessageConfirmMarkAllEpisodesNotFinished": "Sei sicuro di voler contrassegnare tutti gli episodi come non completati?",
|
||||
"MessageConfirmMarkItemFinished": "Sei sicuro/sicura di voler segnare {0} come finito?",
|
||||
@@ -854,7 +858,7 @@
|
||||
"MessageNoItems": "Nessun oggetto",
|
||||
"MessageNoItemsFound": "Nessun oggetto trovato",
|
||||
"MessageNoListeningSessions": "Nessuna sessione di ascolto",
|
||||
"MessageNoLogs": "Nessun Log",
|
||||
"MessageNoLogs": "Nessun rapporto",
|
||||
"MessageNoMediaProgress": "Nessun progresso multimediale",
|
||||
"MessageNoNotifications": "Nessuna notifica",
|
||||
"MessageNoPodcastFeed": "Podcast non valido: nessun feed",
|
||||
@@ -1075,7 +1079,7 @@
|
||||
"ToastMetadataFilesRemovedNoneRemoved": "Nessun metadato. {0} file rimossi",
|
||||
"ToastMetadataFilesRemovedSuccess": "{0} metadati.{1} file rimossi",
|
||||
"ToastMustHaveAtLeastOnePath": "Deve avere almeno un percorso",
|
||||
"ToastNameEmailRequired": "Nome ed email sono obbligatori",
|
||||
"ToastNameEmailRequired": "Nome ed e-mail sono obbligatori",
|
||||
"ToastNameRequired": "Il nome è obbligatorio",
|
||||
"ToastNewApiKeyUserError": "Deve selezionare un utente",
|
||||
"ToastNewEpisodesFound": "{0} nuovi episodi trovati",
|
||||
@@ -1109,7 +1113,7 @@
|
||||
"ToastProgressIsNotBeingSynced": "L'avanzamento non è sincronizzato, riavviare la riproduzione",
|
||||
"ToastProviderCreatedFailed": "Impossibile aggiungere il provider",
|
||||
"ToastProviderCreatedSuccess": "Aggiunto nuovo provider",
|
||||
"ToastProviderNameAndUrlRequired": "Nome e URL richiesti",
|
||||
"ToastProviderNameAndUrlRequired": "Nome e Url richiesti",
|
||||
"ToastProviderRemoveSuccess": "Provider rimosso",
|
||||
"ToastRSSFeedCloseFailed": "Errore chiusura flusso RSS",
|
||||
"ToastRSSFeedCloseSuccess": "Flusso RSS chiuso",
|
||||
|
||||
+930
-36
File diff suppressed because it is too large.
Load diff
@@ -11,6 +11,7 @@
|
||||
"ButtonApplyChapters": "Taikyti skyrius",
|
||||
"ButtonAuthors": "Autoriai",
|
||||
"ButtonBack": "Atgal",
|
||||
"ButtonBatchEditPopulateFromExisting": "Užpildyti iš esamų",
|
||||
"ButtonBrowseForFolder": "Naršyti aplanko",
|
||||
"ButtonCancel": "Atšaukti",
|
||||
"ButtonCancelEncode": "Atšaukti kodavimą",
|
||||
@@ -42,6 +43,7 @@
|
||||
"ButtonLatest": "Naujausias",
|
||||
"ButtonLibrary": "Biblioteka",
|
||||
"ButtonLogout": "Atsijungti",
|
||||
"ButtonLogoutAllDevices": "Atsijungti visuose įrenginiuose",
|
||||
"ButtonLookup": "Ieškoti",
|
||||
"ButtonManageTracks": "Tvarkyti takelius",
|
||||
"ButtonMapChapterTitles": "Suderinti skyrių pavadinimus",
|
||||
@@ -69,7 +71,7 @@
|
||||
"ButtonReScan": "Iš naujo nuskaityti",
|
||||
"ButtonRead": "Skaityti",
|
||||
"ButtonReadLess": "Mažiau",
|
||||
"ButtonReadMore": "Daugiau",
|
||||
"ButtonReadMore": "Skaityti daugiau",
|
||||
"ButtonRefresh": "Atnaujinti",
|
||||
"ButtonRemove": "Pašalinti",
|
||||
"ButtonRemoveAll": "Pašalinti viską",
|
||||
@@ -230,8 +232,8 @@
|
||||
"LabelCollections": "Kolekcijos",
|
||||
"LabelComplete": "Baigta",
|
||||
"LabelConfirmPassword": "Patvirtinkite slaptažodį",
|
||||
"LabelContinueListening": "Tęsti klausymąsi",
|
||||
"LabelContinueReading": "Tęsti skaitymą",
|
||||
"LabelContinueListening": "Klausytis toliau",
|
||||
"LabelContinueReading": "Skaityti toliau",
|
||||
"LabelContinueSeries": "Tęsti seriją",
|
||||
"LabelCover": "Viršelis",
|
||||
"LabelCoverImageURL": "Viršelio paveikslėlio URL",
|
||||
@@ -248,6 +250,7 @@
|
||||
"LabelDirectory": "Katalogas",
|
||||
"LabelDiscFromFilename": "Diskas pagal failo pavadinimą",
|
||||
"LabelDiscFromMetadata": "Diskas pagal metaduomenis",
|
||||
"LabelDiscover": "Atrask",
|
||||
"LabelDownload": "Atsisiųsti",
|
||||
"LabelDownloadNEpisodes": "Atsisiųsti {0} epizodų",
|
||||
"LabelDuration": "Trukmė",
|
||||
@@ -393,7 +396,7 @@
|
||||
"LabelReadAgain": "Skaityti dar kartą",
|
||||
"LabelReadEbookWithoutProgress": "Skaityti e-knygą be pažangos saugojimo",
|
||||
"LabelRecentSeries": "Naujausios serijos",
|
||||
"LabelRecentlyAdded": "Neseniai pridėta",
|
||||
"LabelRecentlyAdded": "Naujausiai įkelta",
|
||||
"LabelRecommended": "Rekomenduojama",
|
||||
"LabelRegion": "Regionas",
|
||||
"LabelReleaseDate": "Išleidimo data",
|
||||
@@ -511,6 +514,8 @@
|
||||
"LabelViewQueue": "Peržiūrėti grotuvo eilę",
|
||||
"LabelVolume": "Garsumas",
|
||||
"LabelWeekdaysToRun": "Dienos, kuriomis vykdyti",
|
||||
"LabelYearReviewHide": "Slėpti metų apžvalgą",
|
||||
"LabelYearReviewShow": "Pamatyti metų apžvalgą",
|
||||
"LabelYourAudiobookDuration": "Jūsų garso knygos trukmė",
|
||||
"LabelYourBookmarks": "Jūsų skirtukai",
|
||||
"LabelYourPlaylists": "Jūsų grojaraščiai",
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
{}
|
||||
+16
-10
@@ -2,7 +2,7 @@
|
||||
"ButtonAdd": "Toevoegen",
|
||||
"ButtonAddApiKey": "API Key toevoegen",
|
||||
"ButtonAddChapters": "Hoofdstukken toevoegen",
|
||||
"ButtonAddDevice": "Toestel toevoegen",
|
||||
"ButtonAddDevice": "Apparaat toevoegen",
|
||||
"ButtonAddLibrary": "Bibliotheek toevoegen",
|
||||
"ButtonAddPodcasts": "Podcasts toevoegen",
|
||||
"ButtonAddUser": "Gebruiker toevoegen",
|
||||
@@ -46,6 +46,7 @@
|
||||
"ButtonLatest": "Meest recent",
|
||||
"ButtonLibrary": "Bibliotheek",
|
||||
"ButtonLogout": "Uitloggen",
|
||||
"ButtonLogoutAllDevices": "Uitloggen op alle apparaten",
|
||||
"ButtonLookup": "Zoeken",
|
||||
"ButtonManageTracks": "Tracks beheren",
|
||||
"ButtonMapChapterTitles": "Hoofdstuktitels mappen",
|
||||
@@ -139,7 +140,7 @@
|
||||
"HeaderCustomMetadataProviders": "Aangepaste Metadata Providers",
|
||||
"HeaderDetails": "Details",
|
||||
"HeaderDownloadQueue": "Download-wachtrij",
|
||||
"HeaderEbookFiles": "Ebook bestanden",
|
||||
"HeaderEbookFiles": "E-book bestanden",
|
||||
"HeaderEmail": "E-mail",
|
||||
"HeaderEmailSettings": "E-mail instellingen",
|
||||
"HeaderEpisodes": "Afleveringen",
|
||||
@@ -194,6 +195,7 @@
|
||||
"HeaderScheduleEpisodeDownloads": "Automatische afleveringsdownloads plannen",
|
||||
"HeaderScheduleLibraryScans": "Schema automatische bibliotheekscans",
|
||||
"HeaderSession": "Sessie",
|
||||
"HeaderSessions": "Sessies",
|
||||
"HeaderSetBackupSchedule": "Kies schema voor back-up",
|
||||
"HeaderSettings": "Instellingen",
|
||||
"HeaderSettingsDisplay": "Toon",
|
||||
@@ -275,7 +277,7 @@
|
||||
"LabelBonus": "Bonus",
|
||||
"LabelBooks": "Boeken",
|
||||
"LabelButtonText": "Knop Tekst",
|
||||
"LabelByAuthor": "Door {0}",
|
||||
"LabelByAuthor": "door {0}",
|
||||
"LabelChangePassword": "Wachtwoord wijzigen",
|
||||
"LabelChannels": "Kanalen",
|
||||
"LabelChapterCount": "{0} Hoofdstukken",
|
||||
@@ -383,7 +385,7 @@
|
||||
"LabelFolders": "Mappen",
|
||||
"LabelFontBold": "Vetgedrukt",
|
||||
"LabelFontBoldness": "Lettertype Dikte",
|
||||
"LabelFontFamily": "Lettertypefamilie",
|
||||
"LabelFontFamily": "Letterfamilie",
|
||||
"LabelFontItalic": "Cursief",
|
||||
"LabelFontScale": "Lettertype schaal",
|
||||
"LabelFontStrikethrough": "Doorgestreept",
|
||||
@@ -415,6 +417,7 @@
|
||||
"LabelIntervalEveryHour": "Ieder uur",
|
||||
"LabelIntervalEveryMinute": "Elke minuut",
|
||||
"LabelInvert": "Omdraaien",
|
||||
"LabelIpAddress": "IP-adres",
|
||||
"LabelItem": "Onderdeel",
|
||||
"LabelJumpBackwardAmount": "Terugspoelen hoeveelheid",
|
||||
"LabelJumpForwardAmount": "Vooruitspoelen hoeveelheid",
|
||||
@@ -436,9 +439,9 @@
|
||||
"LabelLibraryFilterSublistEmpty": "Nee {0}",
|
||||
"LabelLibraryItem": "Bibliotheekonderdeel",
|
||||
"LabelLibraryName": "Bibliotheeknaam",
|
||||
"LabelLibrarySortByProgress": "Voortuigang geüpdatet",
|
||||
"LabelLibrarySortByProgressFinished": "Datum voltooid",
|
||||
"LabelLibrarySortByProgressStarted": "Datum gestart",
|
||||
"LabelLibrarySortByProgress": "Voortgang: Laatst geüpdatet",
|
||||
"LabelLibrarySortByProgressFinished": "Voortgang: Voltooid",
|
||||
"LabelLibrarySortByProgressStarted": "Voortgang: Gestart",
|
||||
"LabelLimit": "Limiet",
|
||||
"LabelLineSpacing": "Regelruimte",
|
||||
"LabelListenAgain": "Opnieuw Beluisteren",
|
||||
@@ -588,8 +591,8 @@
|
||||
"LabelSettingsBookshelfViewHelp": "Skeumorphisch design met houten planken",
|
||||
"LabelSettingsChromecastSupport": "Chromecast ondersteuning",
|
||||
"LabelSettingsDateFormat": "Datumnotatie",
|
||||
"LabelSettingsEnableWatcher": "Bibliotheken automatisch scannen op wijzigingen",
|
||||
"LabelSettingsEnableWatcherForLibrary": "Bibliotheek automatisch scannen op wijzigingen",
|
||||
"LabelSettingsEnableWatcher": "Bibliotheken automatisch monitoren op wijzigingen",
|
||||
"LabelSettingsEnableWatcherForLibrary": "Bibliotheek automatisch monitoren op wijzigingen",
|
||||
"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.",
|
||||
@@ -774,6 +777,7 @@
|
||||
"MessageConfirmDeleteSession": "Weet je zeker dat je deze sessie wil verwijderen?",
|
||||
"MessageConfirmEmbedMetadataInAudioFiles": "Weet u zeker dat u metagegevens wilt insluiten in {0} audiobestanden?",
|
||||
"MessageConfirmForceReScan": "Weet je zeker dat je geforceerd opnieuw wil scannen?",
|
||||
"MessageConfirmLogoutDevice": "Weet je zeker dat je wil uitloggen op \"{0}\"?",
|
||||
"MessageConfirmMarkAllEpisodesFinished": "Weet je zeker dat je alle afleveringen als voltooid wil markeren?",
|
||||
"MessageConfirmMarkAllEpisodesNotFinished": "Weet je zeker dat je alle afleveringen als niet-voltooid wil markeren?",
|
||||
"MessageConfirmMarkItemFinished": "Weet u zeker dat u \"{0}\" als voltooid wilt markeren?",
|
||||
@@ -888,7 +892,7 @@
|
||||
"MessageResetChaptersConfirm": "Weet je zeker dat je de hoofdstukken wil resetten en de wijzigingen die je gemaakt hebt ongedaan wil maken?",
|
||||
"MessageRestoreBackupConfirm": "Weet je zeker dat je wil herstellen met behulp van de back-up gemaakt op",
|
||||
"MessageRestoreBackupWarning": "Een back-up herstellen zal de volledige database in /config en de omslagen in /metadata/items & /metadata/authors overschrijven.<br /><br />Back-ups wijzigen geen bestanden in je bibliotheekmappen. Als je de serverinstelling gebruikt om omslagen en metadata in je bibliotheekmappen te bewaren dan worden deze niet geback-upt of overschreven.<br /><br />Alle apparaten die je server gebruiken, worden automatisch ververst.",
|
||||
"MessageScheduleLibraryScanNote": "Voor de meeste gebruikers is het raadzaam om deze functie uitgeschakeld te laten en de folder watcher-instelling ingeschakeld te houden. De folder watcher detecteert automatisch wijzigingen in uw bibliotheekmappen. De folder watcher werkt niet voor elk bestandssysteem (zoals NFS), dus geplande bibliotheekscans kunnen in plaats daarvan worden gebruikt.",
|
||||
"MessageScheduleLibraryScanNote": "Voor de meeste gebruikers is het aangeraden om deze functie uitgeschakeld te laten en de \"Bibliotheek automatisch monitoren op wijzigingen\" instelling ingeschakeld te houden - deze detecteert automatisch wijzigingen in uw bibliotheekmappen. Activeer deze instelling als \"Bibliotheek automatisch monitoren op wijzigingen\" niet werkt voor uw bestandssysteem (zoals NFS).",
|
||||
"MessageScheduleRunEveryWeekdayAtTime": "Elke {0} uitvoeren op {1}",
|
||||
"MessageSearchResultsFor": "Zoekresultaten voor",
|
||||
"MessageSelected": "{0} geselecteerd",
|
||||
@@ -1026,6 +1030,8 @@
|
||||
"ToastCollectionItemsAddFailed": "Item(s) toegevoegd aan collectie mislukt",
|
||||
"ToastCollectionRemoveSuccess": "Collectie verwijderd",
|
||||
"ToastCollectionUpdateSuccess": "Collectie bijgewerkt",
|
||||
"ToastConnectionNotAvailable": "Verbinding niet beschikbaar. Gelieve later opnieuw te proberen",
|
||||
"ToastCoverSearchFailed": "Omslag zoeken mislukt",
|
||||
"ToastCoverUpdateFailed": "Omslag bijwerken mislukt",
|
||||
"ToastDateTimeInvalidOrIncomplete": "Datum en tijd ongeldig of onvolledig",
|
||||
"ToastDeleteFileFailed": "Bestand verwijderen mislukt",
|
||||
|
||||
@@ -46,6 +46,7 @@
|
||||
"ButtonLatest": "Siste",
|
||||
"ButtonLibrary": "Bibliotek",
|
||||
"ButtonLogout": "Logg ut",
|
||||
"ButtonLogoutAllDevices": "Logg ut av alle enheter",
|
||||
"ButtonLookup": "Slå opp",
|
||||
"ButtonManageTracks": "Administrer spor",
|
||||
"ButtonMapChapterTitles": "Kartlegg kapittel titler",
|
||||
@@ -194,6 +195,7 @@
|
||||
"HeaderScheduleEpisodeDownloads": "Planlegg automatisk nedlasting av episoder",
|
||||
"HeaderScheduleLibraryScans": "Planlegg automatisk bibliotek skann",
|
||||
"HeaderSession": "Sesjon",
|
||||
"HeaderSessions": "Økter",
|
||||
"HeaderSetBackupSchedule": "Sett timeplan for sikkerhetskopi",
|
||||
"HeaderSettings": "Innstillinger",
|
||||
"HeaderSettingsDisplay": "Vis",
|
||||
@@ -415,6 +417,7 @@
|
||||
"LabelIntervalEveryHour": "Hver time",
|
||||
"LabelIntervalEveryMinute": "Hvert minutt",
|
||||
"LabelInvert": "Inverter",
|
||||
"LabelIpAddress": "IP Adresse",
|
||||
"LabelItem": "Enhet",
|
||||
"LabelJumpBackwardAmount": "Hopp bakover med",
|
||||
"LabelJumpForwardAmount": "Hopp forover med",
|
||||
@@ -774,6 +777,7 @@
|
||||
"MessageConfirmDeleteSession": "Er du sikker på at du vil slette denne sesjonen?",
|
||||
"MessageConfirmEmbedMetadataInAudioFiles": "Er du sikker på at du vil legge til metadata i {0} lyd-filer?",
|
||||
"MessageConfirmForceReScan": "Er du sikker på at du vil tvinge en ny skann?",
|
||||
"MessageConfirmLogoutDevice": "Er du sikker på at du vil logge ut av \"{0}\"?",
|
||||
"MessageConfirmMarkAllEpisodesFinished": "Er du sikker på at du vil markere alle episodene som fullført?",
|
||||
"MessageConfirmMarkAllEpisodesNotFinished": "Er du sikker på at du vil markere alle episodene som ikke fullført?",
|
||||
"MessageConfirmMarkItemFinished": "Er du sikker på at du vil markere {0} som ferdig?",
|
||||
|
||||
+54
-2
@@ -46,6 +46,7 @@
|
||||
"ButtonLatest": "Aktualna wersja",
|
||||
"ButtonLibrary": "Biblioteka",
|
||||
"ButtonLogout": "Wyloguj",
|
||||
"ButtonLogoutAllDevices": "Wyloguj z wszystkich urządzeń",
|
||||
"ButtonLookup": "Importuj",
|
||||
"ButtonManageTracks": "Zarządzaj ścieżkami",
|
||||
"ButtonMapChapterTitles": "Mapuj nazwy rozdziałów",
|
||||
@@ -194,6 +195,7 @@
|
||||
"HeaderScheduleEpisodeDownloads": "Planowanie automatycznego ściągania odcinków",
|
||||
"HeaderScheduleLibraryScans": "Zaplanuj automatyczne skanowanie biblioteki",
|
||||
"HeaderSession": "Sesja",
|
||||
"HeaderSessions": "Sesje",
|
||||
"HeaderSetBackupSchedule": "Ustaw harmonogram tworzenia kopii zapasowej",
|
||||
"HeaderSettings": "Ustawienia",
|
||||
"HeaderSettingsDisplay": "Wyświetlanie",
|
||||
@@ -344,7 +346,7 @@
|
||||
"LabelEncodingInfoEmbedded": "Metadane zostaną osadzone w ścieżkach audio w folderze z audiobookiem.",
|
||||
"LabelEncodingStartedNavigation": "Po uruchomieniu zadania możesz opuścić tę stronę.",
|
||||
"LabelEncodingTimeWarning": "Kodowanie może potrwać do 30 minut.",
|
||||
"LabelEncodingWarningAdvancedSettings": "Ostrzeżenie: Nie aktualizuj tych ustawień, jeśli nie jesteś zaznajomiony ze sposobem działania ffmpeg i opcji konwersji.",
|
||||
"LabelEncodingWarningAdvancedSettings": "Ostrzeżenie: Nie aktualizuj tych ustawień, jeśli nie znasz opcji kodowania ffmpeg.",
|
||||
"LabelEncodingWatcherDisabled": "Jeśli monitorowanie folderów jest wyłączone, należy ponownie przeskanować audiobooka.",
|
||||
"LabelEnd": "Zakończ",
|
||||
"LabelEndOfChapter": "Koniec rozdziału",
|
||||
@@ -415,6 +417,7 @@
|
||||
"LabelIntervalEveryHour": "Każdej godziny",
|
||||
"LabelIntervalEveryMinute": "Co minutę",
|
||||
"LabelInvert": "Inversja",
|
||||
"LabelIpAddress": "Adres IP",
|
||||
"LabelItem": "Pozycja",
|
||||
"LabelJumpBackwardAmount": "Przeskocz do tyłu o:",
|
||||
"LabelJumpForwardAmount": "Przeskocz do przodu o:",
|
||||
@@ -774,6 +777,7 @@
|
||||
"MessageConfirmDeleteSession": "Czy na pewno chcesz usunąć tę sesję?",
|
||||
"MessageConfirmEmbedMetadataInAudioFiles": "Czy na pewno chcesz osadzić metadane w {0} plikach audio?",
|
||||
"MessageConfirmForceReScan": "Czy na pewno chcesz wymusić ponowne skanowanie?",
|
||||
"MessageConfirmLogoutDevice": "Czy na pewno chcesz się wylogować z \"{0}\"?",
|
||||
"MessageConfirmMarkAllEpisodesFinished": "Czy na pewno chcesz oznaczyć wszystkie odcinki jako ukończone?",
|
||||
"MessageConfirmMarkAllEpisodesNotFinished": "Czy na pewno chcesz oznaczyć wszystkie odcinki jako nieukończone?",
|
||||
"MessageConfirmMarkItemFinished": "Czy na pewno chcesz oznaczyć \"{0}\" jako zakończone?",
|
||||
@@ -888,7 +892,7 @@
|
||||
"MessageResetChaptersConfirm": "Czy na pewno chcesz zresetować rozdziały i cofnąć wprowadzone zmiany?",
|
||||
"MessageRestoreBackupConfirm": "Czy na pewno chcesz przywrócić kopię zapasową utworzoną w dniu",
|
||||
"MessageRestoreBackupWarning": "Przywrócenie kopii zapasowej spowoduje nadpisanie bazy danych w folderze /config oraz okładek w folderze /metadata/items & /metadata/authors.<br /><br />Kopie zapasowe nie modyfikują żadnego pliku w folderach z plikami audio. Jeśli włączyłeś ustawienia serwera, aby przechowywać okładki i metadane w folderach biblioteki, to nie są one zapisywane w kopii zapasowej lub nadpisywane<br /><br />Wszyscy klienci korzystający z Twojego serwera będą automatycznie odświeżani.",
|
||||
"MessageScheduleLibraryScanNote": "W przypadku większości użytkowników zaleca się pozostawienie tej funkcji wyłączonej i włączenie opcji monitorowania folderów. Monitor folderów automatycznie wykrywa zmiany w folderach biblioteki. Monitor folderów nie działa w przypadku wszystkich systemów plików (np. NFS), dlatego zamiast niego można używać zaplanowanych skanowań biblioteki.",
|
||||
"MessageScheduleLibraryScanNote": "Dla większości użytkowników zaleca się pozostawienie tej funkcji wyłączonej i włączenie ustawienia \"Automatycznie obserwuj bibliotekę pod kątem zmian\" – będzie ono automatycznie wykrywać zmiany w folderach biblioteki. Włącz tę funkcję, jeśli \"Automatycznie obserwuj bibliotekę pod kątem zmian\" nie działa w Twoim systemie plików (np. NFS).",
|
||||
"MessageScheduleRunEveryWeekdayAtTime": "Uruchom w każdy {0} o {1}",
|
||||
"MessageSearchResultsFor": "Wyniki wyszukiwania dla",
|
||||
"MessageSelected": "{0} wybranych",
|
||||
@@ -951,6 +955,11 @@
|
||||
"NoteUploaderFoldersWithMediaFiles": "Foldery z plikami multimedialnymi będą traktowane jako osobne elementy w bibliotece.",
|
||||
"NoteUploaderOnlyAudioFiles": "Jeśli przesyłasz tylko pliki audio, każdy plik audio będzie traktowany jako osobny audiobook.",
|
||||
"NoteUploaderUnsupportedFiles": "Nieobsługiwane pliki są ignorowane. Podczas dodawania folderu, inne pliki, które nie znajdują się w folderze elementu, są ignorowane.",
|
||||
"NotificationOnBackupCompletedDescription": "Wyzwalane po zakończeniu tworzenia kopii zapasowej",
|
||||
"NotificationOnBackupFailedDescription": "Wyzwalane w przypadku gdy stworzenie kopii zapasowej rzuci błąd",
|
||||
"NotificationOnEpisodeDownloadedDescription": "Wyzwalane, gdy odcinek podcastu zostanie automatycznie pobrany",
|
||||
"NotificationOnRSSFeedDisabledDescription": "Wyzwalane, gdy automatyczne pobieranie odcinków jest wyłączone z powodu zbyt wielu nieudanych prób",
|
||||
"NotificationOnRSSFeedFailedDescription": "Wyzwalane, gdy żądanie kanału RSS dotyczące automatycznego pobrania odcinka nie powiedzie się",
|
||||
"NotificationOnTestDescription": "Zdarzenie używane do testowania systemu powiadomień",
|
||||
"PlaceholderBulkChapterInput": "Wpisz tytuł rozdziału lub użyj numeracji (np. „Odcinek 1”, „Rozdział 10”, „1.”)",
|
||||
"PlaceholderNewCollection": "Nowa nazwa kolekcji",
|
||||
@@ -960,6 +969,7 @@
|
||||
"PlaceholderSearchEpisode": "Szukanie odcinka..",
|
||||
"StatsAuthorsAdded": "dodano autorów",
|
||||
"StatsBooksAdded": "dodano książki",
|
||||
"StatsBooksAdditional": "Niektóre dodatki obejmują…",
|
||||
"StatsBooksFinished": "ukończone książki",
|
||||
"StatsBooksFinishedThisYear": "Wybrane książki ukończone w tym roku…",
|
||||
"StatsBooksListenedTo": "książki wysłuchane",
|
||||
@@ -976,6 +986,7 @@
|
||||
"StatsTotalDuration": "O sumarycznej długości…",
|
||||
"StatsYearInReview": "PRZEGLĄD ROKU",
|
||||
"ToastAccountUpdateSuccess": "Zaktualizowano konto",
|
||||
"ToastAppriseUrlRequired": "Należy wprowadzić adres URL Apprise",
|
||||
"ToastAsinRequired": "ASIN jest wymagany",
|
||||
"ToastAuthorImageRemoveSuccess": "Zdjęcie autora usunięte",
|
||||
"ToastAuthorNotFound": "Autor \"{0}\" nie został znaleziony",
|
||||
@@ -994,8 +1005,11 @@
|
||||
"ToastBackupRestoreFailed": "Nie udało się przywrócić kopii zapasowej",
|
||||
"ToastBackupUploadFailed": "Nie udało się przesłać kopii zapasowej",
|
||||
"ToastBackupUploadSuccess": "Kopia zapasowa została przesłana",
|
||||
"ToastBatchApplyDetailsToItemsSuccess": "Szczegóły zastosowane do elementów",
|
||||
"ToastBatchDeleteFailed": "Usuwanie zbiorcze nie powiodło się",
|
||||
"ToastBatchDeleteSuccess": "Usuwanie zbiorcze powiodło się",
|
||||
"ToastBatchQuickMatchFailed": "Szybkie dopasowanie partii nie powiodło się!",
|
||||
"ToastBatchQuickMatchStarted": "Rozpoczęto partię szybkiego dopasowania {0} książek!",
|
||||
"ToastBatchUpdateFailed": "Aktualizacja zbiorcza nie powiodła się",
|
||||
"ToastBatchUpdateSuccess": "Aktualizacja zbiorcza powiodła się",
|
||||
"ToastBookmarkCreateFailed": "Nie udało się utworzyć zakładki",
|
||||
@@ -1033,7 +1047,14 @@
|
||||
"ToastEpisodeDownloadQueueClearSuccess": "Wyczyszczono kolejkę epizodów do ściągnięcia",
|
||||
"ToastEpisodeUpdateSuccess": "Zaktualizowano {0} odcinków",
|
||||
"ToastErrorCannotShare": "Nie można udostępniać natywnie na tym urządzeniu.",
|
||||
"ToastFailedToCreate": "Nie udało się utworzyć",
|
||||
"ToastFailedToDelete": "Nie udało się usunąć",
|
||||
"ToastFailedToLoadData": "Nie udało się załadować danych",
|
||||
"ToastFailedToMatch": "Nie udało się dopasować",
|
||||
"ToastFailedToShare": "Nie udało się udostępnić",
|
||||
"ToastFailedToUpdate": "Nie udało się zaktualizować",
|
||||
"ToastInvalidImageUrl": "Nieprawidłowy URL obrazu",
|
||||
"ToastInvalidMaxEpisodesToDownload": "Nieprawidłowa maksymalna liczba odcinków do pobrania",
|
||||
"ToastInvalidUrl": "Nieprawidłowy URL",
|
||||
"ToastInvalidUrls": "Jeden lub więcej URL-i są nieprawidłowe",
|
||||
"ToastItemCoverUpdateSuccess": "Zaktualizowano okładkę",
|
||||
@@ -1044,6 +1065,7 @@
|
||||
"ToastItemMarkedAsFinishedSuccess": "Pozycja oznaczona jako ukończona",
|
||||
"ToastItemMarkedAsNotFinishedFailed": "Oznaczenie pozycji jako ukończonej nie powiodło się",
|
||||
"ToastItemMarkedAsNotFinishedSuccess": "Pozycja oznaczona jako nieukończona",
|
||||
"ToastItemUpdateSuccess": "Element zaktualizowany",
|
||||
"ToastLibraryCreateFailed": "Nie udało się utworzyć biblioteki",
|
||||
"ToastLibraryCreateSuccess": "Biblioteka \"{0}\" stworzona",
|
||||
"ToastLibraryDeleteFailed": "Nie udało się usunąć biblioteki",
|
||||
@@ -1052,6 +1074,10 @@
|
||||
"ToastLibraryScanStarted": "Rozpoczęto skanowanie biblioteki",
|
||||
"ToastLibraryUpdateSuccess": "Zaktualizowano \"{0}\" pozycji",
|
||||
"ToastMatchAllAuthorsFailed": "Nie udało się dopasować wszystkich autorów",
|
||||
"ToastMetadataFilesRemovedError": "Błąd podczas usuwania metadata.{0} plików",
|
||||
"ToastMetadataFilesRemovedNoneFound": "Nie znaleziono metadata.{0} plików w bibliotece",
|
||||
"ToastMetadataFilesRemovedNoneRemoved": "Nie usunięto żadnego metadata.{0} pliku",
|
||||
"ToastMetadataFilesRemovedSuccess": "{0} metadata.{0} plików usunięto",
|
||||
"ToastMustHaveAtLeastOnePath": "Musi mieć przynajmniej jedną ścieżkę",
|
||||
"ToastNameEmailRequired": "Nazwa i email są wymagane",
|
||||
"ToastNameRequired": "Imię jest wymagane",
|
||||
@@ -1065,7 +1091,15 @@
|
||||
"ToastNewUserUsernameError": "Wprowadź nazwę użytkownika",
|
||||
"ToastNoNewEpisodesFound": "Nie znaleziono nowych odcinków",
|
||||
"ToastNoRSSFeed": "Podcast nie posiada RSS Feed",
|
||||
"ToastNoUpdatesNecessary": "Brak konieczności aktualizacji",
|
||||
"ToastNotificationCreateFailed": "Nie udało się utworzyć powiadomienia",
|
||||
"ToastNotificationDeleteFailed": "Nie udało się usunąć powiadomienia",
|
||||
"ToastNotificationFailedMaximum": "Maks. ilość nieudanych prób musi być >= 0",
|
||||
"ToastNotificationQueueMaximum": "Maksymalna liczba powiadomień w kolejce musi być >= 0",
|
||||
"ToastNotificationSettingsUpdateSuccess": "Zaktualizowano ustawienia powiadomień",
|
||||
"ToastNotificationTestTriggerFailed": "Nie udało się wywołać powiadomienia testowego",
|
||||
"ToastNotificationTestTriggerSuccess": "Wyzwolono powiadomienie testowe",
|
||||
"ToastNotificationUpdateSuccess": "Powiadomienie zaktualizowane",
|
||||
"ToastPlaylistCreateFailed": "Nie udało się utworzyć playlisty",
|
||||
"ToastPlaylistCreateSuccess": "Playlista utworzona",
|
||||
"ToastPlaylistRemoveSuccess": "Playlista usunięta",
|
||||
@@ -1073,8 +1107,17 @@
|
||||
"ToastPodcastCreateFailed": "Nie udało się utworzyć podcastu",
|
||||
"ToastPodcastCreateSuccess": "Podcast został pomyślnie utworzony",
|
||||
"ToastPodcastEpisodeUpdated": "Zaktualizowano odcinki",
|
||||
"ToastPodcastGetFeedFailed": "Nie udało się pobrać kanału podcastu",
|
||||
"ToastPodcastNoEpisodesInFeed": "Nie znaleziono żadnych odcinków w kanale RSS",
|
||||
"ToastPodcastNoRssFeed": "Podcast nie ma kanału RSS",
|
||||
"ToastProgressIsNotBeingSynced": "Postęp nie jest synchronizowany, uruchom ponownie odtwarzanie",
|
||||
"ToastProviderCreatedFailed": "Nie udało się dodać dostawcy",
|
||||
"ToastProviderCreatedSuccess": "Dodano nowego dostawcę",
|
||||
"ToastProviderNameAndUrlRequired": "Wymagane jest podanie nazwy i adresu URL",
|
||||
"ToastProviderRemoveSuccess": "Dostawca usunięty",
|
||||
"ToastRSSFeedCloseFailed": "Zamknięcie kanału RSS nie powiodło się",
|
||||
"ToastRSSFeedCloseSuccess": "Zamknięcie kanału RSS powiodło się",
|
||||
"ToastRemoveFailed": "Nie udało się usunąć",
|
||||
"ToastRemoveItemFromCollectionFailed": "Nie udało się usunąć elementu z kolekcji",
|
||||
"ToastRemoveItemFromCollectionSuccess": "Pozycja usunięta z kolekcji",
|
||||
"ToastRemoveItemsWithIssuesFailed": "Nie udało się usunąć wadliwych elementów z biblioteki",
|
||||
@@ -1096,16 +1139,25 @@
|
||||
"ToastSessionDeleteFailed": "Nie udało się usunąć sesji",
|
||||
"ToastSessionDeleteSuccess": "Sesja usunięta",
|
||||
"ToastSleepTimerDone": "Słodkich snów... zZzzZz",
|
||||
"ToastSlugMustChange": "Slug zawiera nieprawidłowe znaki",
|
||||
"ToastSlugRequired": "Slug jest wymagany",
|
||||
"ToastSocketConnected": "Nawiązano połączenie z serwerem",
|
||||
"ToastSocketDisconnected": "Połączenie z serwerem zostało zamknięte",
|
||||
"ToastSocketFailedToConnect": "Poączenie z serwerem nie powiodło się",
|
||||
"ToastSortingPrefixesEmptyError": "Musi mieć co najmniej 1 prefiks sortowania",
|
||||
"ToastSortingPrefixesUpdateSuccess": "Zaktualizowano prefiksy sortowania ({0} elementów)",
|
||||
"ToastTitleRequired": "Tytuł jest wymagany",
|
||||
"ToastUnknownError": "Nieznany błąd",
|
||||
"ToastUnlinkOpenIdFailed": "Nie udało się odpiąć użytkownika z OpenID",
|
||||
"ToastUnlinkOpenIdSuccess": "Użytkownik odpięty z OpenID",
|
||||
"ToastUploaderFilepathExistsError": "Ścieżka \"{0}\" już istnieje na serwerze",
|
||||
"ToastUploaderItemExistsInSubdirectoryError": "Element \"{0}\" używa podkatalogu ścieżki przesyłania.",
|
||||
"ToastUserDeleteFailed": "Nie udało się usunąć użytkownika",
|
||||
"ToastUserDeleteSuccess": "Użytkownik usunięty",
|
||||
"ToastUserPasswordChangeSuccess": "Hasło zostało pomyślnie zmienione",
|
||||
"ToastUserPasswordMismatch": "Hasła nie są zgodne",
|
||||
"ToastUserPasswordMustChange": "Nowe hasło nie może być takie samo jak stare hasło",
|
||||
"ToastUserRootRequireName": "Należy wprowadzić nazwę użytkownika root",
|
||||
"TooltipAddChapters": "Dodaj rozdział(y)",
|
||||
"TooltipAddOneSecond": "Dodaj sekundę",
|
||||
"TooltipAdjustChapterStart": "Kliknij, aby skorygować czas początkowy",
|
||||
|
||||
@@ -46,6 +46,7 @@
|
||||
"ButtonLatest": "Mais Recentes",
|
||||
"ButtonLibrary": "Biblioteca",
|
||||
"ButtonLogout": "Logout",
|
||||
"ButtonLogoutAllDevices": "Desconectar de todos os dispositivos",
|
||||
"ButtonLookup": "Procurar",
|
||||
"ButtonManageTracks": "Gerenciar Faixas",
|
||||
"ButtonMapChapterTitles": "Designar Títulos de Capítulos",
|
||||
@@ -194,6 +195,7 @@
|
||||
"HeaderScheduleEpisodeDownloads": "Programar Download Automático de Episódios",
|
||||
"HeaderScheduleLibraryScans": "Programar Verificação Automática da Biblioteca",
|
||||
"HeaderSession": "Sessão",
|
||||
"HeaderSessions": "Sessões",
|
||||
"HeaderSetBackupSchedule": "Definir Programação de Backup",
|
||||
"HeaderSettings": "Configurações",
|
||||
"HeaderSettingsDisplay": "Exibição",
|
||||
@@ -325,8 +327,8 @@
|
||||
"LabelDurationComparisonLonger": "({0} maior)",
|
||||
"LabelDurationComparisonShorter": "({0} menor)",
|
||||
"LabelDurationFound": "Duração comprovada:",
|
||||
"LabelEbook": "Ebook",
|
||||
"LabelEbooks": "Ebooks",
|
||||
"LabelEbook": "E-book",
|
||||
"LabelEbooks": "E-books",
|
||||
"LabelEdit": "Editar",
|
||||
"LabelEmail": "Email",
|
||||
"LabelEmailSettingsFromAddress": "Remetente",
|
||||
@@ -415,6 +417,7 @@
|
||||
"LabelIntervalEveryHour": "Toda hora",
|
||||
"LabelIntervalEveryMinute": "A cada minuto",
|
||||
"LabelInvert": "Inverter",
|
||||
"LabelIpAddress": "Endereço IP",
|
||||
"LabelItem": "Item",
|
||||
"LabelJumpBackwardAmount": "Tempo de retrocesso",
|
||||
"LabelJumpForwardAmount": "Tempo de avanço",
|
||||
@@ -774,6 +777,7 @@
|
||||
"MessageConfirmDeleteSession": "Tem certeza de que deseja apagar essa sessão?",
|
||||
"MessageConfirmEmbedMetadataInAudioFiles": "Tem certeza de que deseja incorporar metadados em arquivos de áudio {0}?",
|
||||
"MessageConfirmForceReScan": "Tem certeza de que deseja forçar a nova verificação?",
|
||||
"MessageConfirmLogoutDevice": "Tem certeza de que deseja sair de \"{0}\"?",
|
||||
"MessageConfirmMarkAllEpisodesFinished": "Tem certeza de que deseja marcar todos os episódios como concluídos?",
|
||||
"MessageConfirmMarkAllEpisodesNotFinished": "Tem certeza de que deseja marcar todos os episódios como não concluídos?",
|
||||
"MessageConfirmMarkItemFinished": "Tem certeza de que deseja marcar \"{0}\" como concluído?",
|
||||
@@ -1109,7 +1113,7 @@
|
||||
"ToastProgressIsNotBeingSynced": "O progresso não está sendo sincronizado, reinicie a reprodução",
|
||||
"ToastProviderCreatedFailed": "Falha ao adicionar o provedor",
|
||||
"ToastProviderCreatedSuccess": "Novo provedor adicionado",
|
||||
"ToastProviderNameAndUrlRequired": "Nome e URL obrigatórios",
|
||||
"ToastProviderNameAndUrlRequired": "Nome e Url obrigatórios",
|
||||
"ToastProviderRemoveSuccess": "Fornecedor removido",
|
||||
"ToastRSSFeedCloseFailed": "Falha ao fechar feed RSS",
|
||||
"ToastRSSFeedCloseSuccess": "Feed RSS fechado",
|
||||
|
||||
@@ -46,6 +46,7 @@
|
||||
"ButtonLatest": "Последнее",
|
||||
"ButtonLibrary": "Библиотека",
|
||||
"ButtonLogout": "Выход",
|
||||
"ButtonLogoutAllDevices": "Выйти на всех устройствах",
|
||||
"ButtonLookup": "Найти",
|
||||
"ButtonManageTracks": "Управление треками",
|
||||
"ButtonMapChapterTitles": "Найти названия глав",
|
||||
@@ -194,6 +195,7 @@
|
||||
"HeaderScheduleEpisodeDownloads": "Запланировать автоматическое скачивание выпусков",
|
||||
"HeaderScheduleLibraryScans": "Планировщик автоматического сканирования библиотеки",
|
||||
"HeaderSession": "Сеансы",
|
||||
"HeaderSessions": "Сессии",
|
||||
"HeaderSetBackupSchedule": "Установить планировщик бэкапов",
|
||||
"HeaderSettings": "Настройки",
|
||||
"HeaderSettingsDisplay": "Дисплей",
|
||||
@@ -392,7 +394,7 @@
|
||||
"LabelGenre": "Жанр",
|
||||
"LabelGenres": "Жанры",
|
||||
"LabelHardDeleteFile": "Жесткое удаление файла",
|
||||
"LabelHasEbook": "Есть e-книга",
|
||||
"LabelHasEbook": "Есть электронная книга",
|
||||
"LabelHasSupplementaryEbook": "Есть дополнительная e-книга",
|
||||
"LabelHideSubtitles": "Скрыть серии",
|
||||
"LabelHighestPriority": "Наивысший приоритет",
|
||||
@@ -415,6 +417,7 @@
|
||||
"LabelIntervalEveryHour": "Каждый час",
|
||||
"LabelIntervalEveryMinute": "Каждую минуту",
|
||||
"LabelInvert": "Инвертировать",
|
||||
"LabelIpAddress": "IP-адрес",
|
||||
"LabelItem": "Элемент",
|
||||
"LabelJumpBackwardAmount": "Прыжок назад на величину",
|
||||
"LabelJumpForwardAmount": "Прыжок вперед на величину",
|
||||
@@ -437,8 +440,8 @@
|
||||
"LabelLibraryItem": "Элемент библиотеки",
|
||||
"LabelLibraryName": "Имя библиотеки",
|
||||
"LabelLibrarySortByProgress": "Прогресс: Последнее обновление",
|
||||
"LabelLibrarySortByProgressFinished": "Прогресс: Завершено",
|
||||
"LabelLibrarySortByProgressStarted": "Прогресс: Начато",
|
||||
"LabelLibrarySortByProgressFinished": "Прогресс: Закончена",
|
||||
"LabelLibrarySortByProgressStarted": "Прогресс: Начата",
|
||||
"LabelLimit": "Лимит",
|
||||
"LabelLineSpacing": "Межстрочный интервал",
|
||||
"LabelListenAgain": "Послушать снова",
|
||||
@@ -774,6 +777,7 @@
|
||||
"MessageConfirmDeleteSession": "Вы уверены, что хотите удалить этот сеанс?",
|
||||
"MessageConfirmEmbedMetadataInAudioFiles": "Вы уверены, что хотите вставить метаданные в {0} аудиофайлов?",
|
||||
"MessageConfirmForceReScan": "Вы уверены, что хотите принудительно выполнить повторное сканирование?",
|
||||
"MessageConfirmLogoutDevice": "Вы уверены, что хотите выйти из «{0}»?",
|
||||
"MessageConfirmMarkAllEpisodesFinished": "Вы уверены, что хотите отметить все выпуски как прослушанные?",
|
||||
"MessageConfirmMarkAllEpisodesNotFinished": "Вы уверены, что хотите отметить все выпуски как непрослушанные?",
|
||||
"MessageConfirmMarkItemFinished": "Вы уверены, что хотите отметить «{0}» как завершенную?",
|
||||
|
||||
@@ -275,7 +275,7 @@
|
||||
"LabelBonus": "Bonus",
|
||||
"LabelBooks": "Knihy",
|
||||
"LabelButtonText": "Text tlačidla",
|
||||
"LabelByAuthor": "od",
|
||||
"LabelByAuthor": "od {0}",
|
||||
"LabelChangePassword": "Zmeniť heslo",
|
||||
"LabelChannels": "Kanály",
|
||||
"LabelChapterCount": "{0} kapitol",
|
||||
|
||||
+538
-534
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1 @@
|
||||
{}
|
||||
+261
-257
File diff suppressed because it is too large.
Load diff
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"ButtonAdd": "添加",
|
||||
"ButtonAddApiKey": "新增API金鑰",
|
||||
"ButtonAddChapters": "新增章節",
|
||||
"ButtonAddDevice": "新增設備",
|
||||
"ButtonAddLibrary": "新增庫",
|
||||
@@ -10,6 +11,7 @@
|
||||
"ButtonApplyChapters": "應用到章節",
|
||||
"ButtonAuthors": "作者",
|
||||
"ButtonBack": "返回",
|
||||
"ButtonBatchEditPopulateFromExisting": "從現有資料帶入",
|
||||
"ButtonBrowseForFolder": "瀏覽資料夾",
|
||||
"ButtonCancel": "取消",
|
||||
"ButtonCancelEncode": "取消編碼",
|
||||
@@ -48,11 +50,12 @@
|
||||
"ButtonNevermind": "沒關係",
|
||||
"ButtonNext": "下個",
|
||||
"ButtonNextChapter": "下個章節",
|
||||
"ButtonOk": "確定",
|
||||
"ButtonOk": "好",
|
||||
"ButtonOpenFeed": "打開源",
|
||||
"ButtonOpenManager": "打開管理器",
|
||||
"ButtonPause": "暫停",
|
||||
"ButtonPlay": "播放",
|
||||
"ButtonPlayAll": "全部播放",
|
||||
"ButtonPlaying": "正在播放",
|
||||
"ButtonPlaylists": "播放列表",
|
||||
"ButtonPrevious": "上一個",
|
||||
@@ -64,6 +67,8 @@
|
||||
"ButtonQuickMatch": "快速匹配",
|
||||
"ButtonReScan": "重新掃描",
|
||||
"ButtonRead": "讀取",
|
||||
"ButtonReadLess": "顯示較少",
|
||||
"ButtonReadMore": "顯示更多",
|
||||
"ButtonRefresh": "重整",
|
||||
"ButtonRemove": "移除",
|
||||
"ButtonRemoveAll": "移除所有",
|
||||
@@ -83,12 +88,15 @@
|
||||
"ButtonSelectFolderPath": "選擇資料夾路徑",
|
||||
"ButtonSeries": "系列",
|
||||
"ButtonSetChaptersFromTracks": "將音軌設定為章節",
|
||||
"ButtonShare": "分享",
|
||||
"ButtonShiftTimes": "快速調整時間",
|
||||
"ButtonShow": "顯示",
|
||||
"ButtonStartM4BEncode": "開始 M4B 編碼",
|
||||
"ButtonStartMetadataEmbed": "開始嵌入元數據",
|
||||
"ButtonStats": "統計資料",
|
||||
"ButtonSubmit": "提交",
|
||||
"ButtonTest": "測試",
|
||||
"ButtonUnlinkOpenId": "解除連結 OpenID",
|
||||
"ButtonUpload": "上傳",
|
||||
"ButtonUploadBackup": "上傳備份",
|
||||
"ButtonUploadCover": "上傳封面",
|
||||
@@ -145,6 +153,7 @@
|
||||
"HeaderMetadataOrderOfPrecedence": "元數據優先級",
|
||||
"HeaderMetadataToEmbed": "嵌入元數據",
|
||||
"HeaderNewAccount": "新建帳號",
|
||||
"HeaderNewApiKey": "新API權杖",
|
||||
"HeaderNewLibrary": "新建媒體庫",
|
||||
"HeaderNotifications": "通知",
|
||||
"HeaderOpenIDConnectAuthentication": "OpenID 連接身份驗證",
|
||||
@@ -164,6 +173,7 @@
|
||||
"HeaderRemoveEpisodes": "移除 {0} 劇集",
|
||||
"HeaderSavedMediaProgress": "保存媒體進度",
|
||||
"HeaderSchedule": "計劃任務",
|
||||
"HeaderScheduleEpisodeDownloads": "設定自動下載集數",
|
||||
"HeaderScheduleLibraryScans": "自動掃描媒體庫",
|
||||
"HeaderSession": "會話",
|
||||
"HeaderSetBackupSchedule": "設定備份計劃任務",
|
||||
@@ -182,6 +192,7 @@
|
||||
"HeaderTableOfContents": "目錄",
|
||||
"HeaderTools": "工具",
|
||||
"HeaderUpdateAccount": "更新帳號",
|
||||
"HeaderUpdateApiKey": "更新API權杖",
|
||||
"HeaderUpdateAuthor": "更新作者",
|
||||
"HeaderUpdateDetails": "更新詳情",
|
||||
"HeaderUpdateLibrary": "更新媒體庫",
|
||||
@@ -202,12 +213,14 @@
|
||||
"LabelAddToPlaylist": "新增到播放列表",
|
||||
"LabelAddToPlaylistBatch": "新增 {0} 個項目到播放列表",
|
||||
"LabelAddedAt": "新增於",
|
||||
"LabelAddedDate": "新增{0}",
|
||||
"LabelAdminUsersOnly": "僅限管理員使用者",
|
||||
"LabelAll": "全部",
|
||||
"LabelAllUsers": "所有使用者",
|
||||
"LabelAllUsersExcludingGuests": "除訪客外的所有使用者",
|
||||
"LabelAllUsersIncludingGuests": "包括訪客的所有使用者",
|
||||
"LabelAlreadyInYourLibrary": "已存在你的庫中",
|
||||
"LabelApiToken": "API 權杖",
|
||||
"LabelAppend": "附加",
|
||||
"LabelAuthor": "作者",
|
||||
"LabelAuthorFirstLast": "作者 (姓 名)",
|
||||
@@ -229,8 +242,10 @@
|
||||
"LabelBackupsNumberToKeep": "要保留的備份個數",
|
||||
"LabelBackupsNumberToKeepHelp": "一次只能刪除一個備份, 因此如果你已經有超過此數量的備份, 則應手動刪除它們.",
|
||||
"LabelBitrate": "位元率",
|
||||
"LabelBonus": "附贈內容",
|
||||
"LabelBooks": "圖書",
|
||||
"LabelButtonText": "按鈕文本",
|
||||
"LabelByAuthor": "由{0}",
|
||||
"LabelChangePassword": "修改密碼",
|
||||
"LabelChannels": "聲道",
|
||||
"LabelChapterTitle": "章節標題",
|
||||
@@ -255,6 +270,7 @@
|
||||
"LabelCurrently": "當前:",
|
||||
"LabelCustomCronExpression": "自定義計劃任務表達式:",
|
||||
"LabelDatetime": "日期時間",
|
||||
"LabelDays": "天",
|
||||
"LabelDeleteFromFileSystemCheckbox": "從檔案系統刪除 (取消選中僅從資料庫中刪除)",
|
||||
"LabelDescription": "描述",
|
||||
"LabelDeselectAll": "全部取消選擇",
|
||||
@@ -289,8 +305,10 @@
|
||||
"LabelEpisode": "劇集",
|
||||
"LabelEpisodeTitle": "劇集標題",
|
||||
"LabelEpisodeType": "劇集類型",
|
||||
"LabelEpisodes": "集數",
|
||||
"LabelEpisodic": "單集式",
|
||||
"LabelExample": "示例",
|
||||
"LabelExplicit": "信息準確",
|
||||
"LabelExplicit": "限制級",
|
||||
"LabelFeedURL": "源鏈接",
|
||||
"LabelFetchingMetadata": "正在獲取元數據",
|
||||
"LabelFile": "文件",
|
||||
@@ -302,12 +320,14 @@
|
||||
"LabelFinished": "已聽完",
|
||||
"LabelFolder": "資料夾",
|
||||
"LabelFolders": "資料夾",
|
||||
"LabelFontBold": "粗體",
|
||||
"LabelFontBoldness": "字體粗細",
|
||||
"LabelFontFamily": "字體系列",
|
||||
"LabelFontFamily": "字體家族",
|
||||
"LabelFontItalic": "斜體",
|
||||
"LabelFontScale": "字體比例",
|
||||
"LabelFontStrikethrough": "刪除線",
|
||||
"LabelFormat": "編碼格式",
|
||||
"LabelFull": "完整",
|
||||
"LabelGenre": "流派",
|
||||
"LabelGenres": "流派",
|
||||
"LabelHardDeleteFile": "完全刪除檔案",
|
||||
@@ -316,6 +336,7 @@
|
||||
"LabelHighestPriority": "最高優先級",
|
||||
"LabelHost": "主機",
|
||||
"LabelHour": "小時",
|
||||
"LabelHours": "小時",
|
||||
"LabelIcon": "圖標",
|
||||
"LabelImageURLFromTheWeb": "來自 Web 圖像的 URL",
|
||||
"LabelInProgress": "正在聽",
|
||||
@@ -334,6 +355,7 @@
|
||||
"LabelItem": "項目",
|
||||
"LabelLanguage": "語言",
|
||||
"LabelLanguageDefaultServer": "預設伺服器語言",
|
||||
"LabelLanguages": "語言",
|
||||
"LabelLastBookAdded": "最後新增的書",
|
||||
"LabelLastBookUpdated": "最後更新的書",
|
||||
"LabelLastSeen": "上次查看時間",
|
||||
@@ -347,6 +369,9 @@
|
||||
"LabelLibrary": "媒體庫",
|
||||
"LabelLibraryItem": "媒體庫項目",
|
||||
"LabelLibraryName": "媒體庫名稱",
|
||||
"LabelLibrarySortByProgress": "進度:最後更新",
|
||||
"LabelLibrarySortByProgressFinished": "進度:已完成",
|
||||
"LabelLibrarySortByProgressStarted": "進度:已開始",
|
||||
"LabelLimit": "限制",
|
||||
"LabelLineSpacing": "行間距",
|
||||
"LabelListenAgain": "再次收聽",
|
||||
@@ -364,7 +389,8 @@
|
||||
"LabelMetadataOrderOfPrecedenceDescription": "較高優先級的元數據源將覆蓋較低優先級的元數據源",
|
||||
"LabelMetadataProvider": "元數據提供者",
|
||||
"LabelMinute": "分鐘",
|
||||
"LabelMissing": "丟失",
|
||||
"LabelMinutes": "分鐘",
|
||||
"LabelMissing": "缺失",
|
||||
"LabelMobileRedirectURIs": "允許移動應用重定向 URI",
|
||||
"LabelMobileRedirectURIsDescription": "這是移動應用程序的有效重定向 URI 白名單. 預設值為 <code>audiobookshelf://oauth</code>,您可以刪除它或加入其他 URI 以進行第三方應用集成. 使用星號 (<code>*</code>) 作為唯一條目允許任何 URI.",
|
||||
"LabelMore": "更多",
|
||||
@@ -392,11 +418,12 @@
|
||||
"LabelNotificationsMaxQueueSize": "通知事件的最大佇列大小",
|
||||
"LabelNotificationsMaxQueueSizeHelp": "通知事件被限制為每秒觸發 1 個. 如果佇列處於最大大小, 則將忽略事件. 這可以防止通知垃圾郵件.",
|
||||
"LabelNumberOfBooks": "圖書數量",
|
||||
"LabelNumberOfEpisodes": "# 集",
|
||||
"LabelNumberOfEpisodes": "集數",
|
||||
"LabelOpenRSSFeed": "打開 RSS 源",
|
||||
"LabelOverwrite": "覆蓋",
|
||||
"LabelPassword": "密碼",
|
||||
"LabelPath": "路徑",
|
||||
"LabelPermanent": "永久",
|
||||
"LabelPermissionsAccessAllLibraries": "可以存取所有媒體庫",
|
||||
"LabelPermissionsAccessAllTags": "可以存取所有標籤",
|
||||
"LabelPermissionsAccessExplicitContent": "可以存取顯式內容",
|
||||
@@ -420,10 +447,12 @@
|
||||
"LabelProvider": "供應商",
|
||||
"LabelPubDate": "出版日期",
|
||||
"LabelPublishYear": "發布年份",
|
||||
"LabelPublishedDate": "已發布{0}",
|
||||
"LabelPublisher": "出版商",
|
||||
"LabelPublishers": "出版商",
|
||||
"LabelRSSFeedCustomOwnerEmail": "自定義所有者電子郵件",
|
||||
"LabelRSSFeedCustomOwnerName": "自定義所有者名稱",
|
||||
"LabelRSSFeedOpen": "打開 RSS 源",
|
||||
"LabelRSSFeedOpen": "開啟RSS訂閱",
|
||||
"LabelRSSFeedPreventIndexing": "防止索引",
|
||||
"LabelRSSFeedSlug": "RSS 源段",
|
||||
"LabelRSSFeedURL": "RSS 源 URL",
|
||||
@@ -447,6 +476,7 @@
|
||||
"LabelSelectEpisodesShowing": "選擇正在播放的 {0} 劇集",
|
||||
"LabelSendEbookToDevice": "發送電子書到...",
|
||||
"LabelSequence": "序列",
|
||||
"LabelSerial": "連續劇式",
|
||||
"LabelSeries": "系列",
|
||||
"LabelSeriesName": "系列名稱",
|
||||
"LabelSeriesProgress": "系列進度",
|
||||
@@ -482,9 +512,12 @@
|
||||
"LabelSettingsStoreMetadataWithItem": "存儲項目元數據",
|
||||
"LabelSettingsStoreMetadataWithItemHelp": "預設情況下元數據檔案存儲在/metadata/items資料夾中, 啟用此設定將存儲元數據在你媒體項目資料夾中",
|
||||
"LabelSettingsTimeFormat": "時間格式",
|
||||
"LabelShare": "分享",
|
||||
"LabelShowAll": "全部顯示",
|
||||
"LabelShowSeconds": "顯示秒數",
|
||||
"LabelSize": "檔案大小",
|
||||
"LabelSleepTimer": "睡眠定時",
|
||||
"LabelSlug": "網址代稱",
|
||||
"LabelStart": "開始",
|
||||
"LabelStartTime": "開始時間",
|
||||
"LabelStarted": "開始於",
|
||||
@@ -512,6 +545,7 @@
|
||||
"LabelTagsNotAccessibleToUser": "使用者無法存取標籤",
|
||||
"LabelTasks": "正在運行的任務",
|
||||
"LabelTextEditorBulletedList": "項目符號列表",
|
||||
"LabelTextEditorLink": "連結",
|
||||
"LabelTextEditorNumberedList": "編號列表",
|
||||
"LabelTextEditorUnlink": "取消連結",
|
||||
"LabelTheme": "主題",
|
||||
@@ -539,6 +573,7 @@
|
||||
"LabelTracksSingleTrack": "單軌",
|
||||
"LabelType": "類型",
|
||||
"LabelUnabridged": "未刪節",
|
||||
"LabelUndo": "復原",
|
||||
"LabelUnknown": "未知",
|
||||
"LabelUpdateCover": "更新封面",
|
||||
"LabelUpdateCoverHelp": "找到匹配項時允許覆蓋所選書籍存在的封面",
|
||||
@@ -546,6 +581,7 @@
|
||||
"LabelUpdateDetailsHelp": "找到匹配項時允許覆蓋所選書籍存在的詳細信息",
|
||||
"LabelUpdatedAt": "更新時間",
|
||||
"LabelUploaderDragAndDrop": "拖放檔案或資料夾",
|
||||
"LabelUploaderDragAndDropFilesOnly": "拖曳檔案至此",
|
||||
"LabelUploaderDropFiles": "刪除檔案",
|
||||
"LabelUploaderItemFetchMetadataHelp": "自動獲取標題, 作者和系列",
|
||||
"LabelUseChapterTrack": "使用章節音軌",
|
||||
@@ -635,7 +671,7 @@
|
||||
"MessageNoBackups": "沒有備份",
|
||||
"MessageNoBookmarks": "沒有書籤",
|
||||
"MessageNoChapters": "沒有章節",
|
||||
"MessageNoCollections": "沒有收藏",
|
||||
"MessageNoCollections": "沒有合集",
|
||||
"MessageNoCoversFound": "沒有找到封面",
|
||||
"MessageNoDescription": "沒有描述",
|
||||
"MessageNoDownloadsInProgress": "當前沒有正在進行的下載",
|
||||
@@ -665,6 +701,7 @@
|
||||
"MessagePlayChapter": "開始章節播放",
|
||||
"MessagePlaylistCreateFromCollection": "從收藏中創建播放列表",
|
||||
"MessagePodcastHasNoRSSFeedForMatching": "播客沒有可用於匹配 RSS 源的 url",
|
||||
"MessagePodcastSearchField": "輸入搜尋關鍵字或 RSS 訂閱源網址",
|
||||
"MessageQuickMatchDescription": "使用來自 '{0}' 的第一個匹配結果填充空白詳細信息和封面. 除非啟用 '首選匹配元數據' 伺服器設定, 否則不會覆蓋詳細信息.",
|
||||
"MessageRemoveChapter": "移除章節",
|
||||
"MessageRemoveEpisodes": "移除 {0} 劇集",
|
||||
@@ -678,7 +715,11 @@
|
||||
"MessageSelected": "{0} 被選取",
|
||||
"MessageServerCouldNotBeReached": "無法連接伺服器",
|
||||
"MessageSetChaptersFromTracksDescription": "把每個音頻檔設定為章節並將章節標題設定為音頻檔名",
|
||||
"MessageShareExpirationWillBe": "有效期限為 <strong>{0}</strong>",
|
||||
"MessageStartPlaybackAtTime": "開始播放 \"{0}\" 在 {1}?",
|
||||
"MessageTaskEmbeddingMetadata": "正在嵌入詮釋資料",
|
||||
"MessageTaskEncodingM4b": "正在編碼 M4B",
|
||||
"MessageTaskFailed": "失敗",
|
||||
"MessageThinking": "正在查找...",
|
||||
"MessageUploaderItemFailed": "上傳失敗",
|
||||
"MessageUploaderItemSuccess": "上傳成功!",
|
||||
@@ -701,8 +742,11 @@
|
||||
"PlaceholderNewPlaylist": "輸入播放列表名稱",
|
||||
"PlaceholderSearch": "查找..",
|
||||
"PlaceholderSearchEpisode": "搜尋劇集..",
|
||||
"StatsSessions": "收聽時段",
|
||||
"ToastAccountUpdateSuccess": "帳號已更新",
|
||||
"ToastAsinRequired": "需要 ASIN",
|
||||
"ToastAuthorImageRemoveSuccess": "作者圖像已刪除",
|
||||
"ToastAuthorSearchNotFound": "找不到作者",
|
||||
"ToastAuthorUpdateMerged": "作者已合併",
|
||||
"ToastAuthorUpdateSuccess": "作者已更新",
|
||||
"ToastAuthorUpdateSuccessNoImageFound": "作者已更新 (未找到圖像)",
|
||||
@@ -713,6 +757,8 @@
|
||||
"ToastBackupRestoreFailed": "備份還原失敗",
|
||||
"ToastBackupUploadFailed": "上傳備份失敗",
|
||||
"ToastBackupUploadSuccess": "備份已上傳",
|
||||
"ToastBatchDeleteFailed": "批次刪除失敗",
|
||||
"ToastBatchDeleteSuccess": "批次刪除成功",
|
||||
"ToastBatchUpdateFailed": "批量更新失敗",
|
||||
"ToastBatchUpdateSuccess": "批量更新成功",
|
||||
"ToastBookmarkCreateFailed": "創建書簽失敗",
|
||||
@@ -720,8 +766,12 @@
|
||||
"ToastBookmarkRemoveSuccess": "書籤已刪除",
|
||||
"ToastChaptersHaveErrors": "章節有錯誤",
|
||||
"ToastChaptersMustHaveTitles": "章節必須有標題",
|
||||
"ToastChaptersUpdated": "章節已更新",
|
||||
"ToastCollectionRemoveSuccess": "收藏夾已刪除",
|
||||
"ToastCollectionUpdateSuccess": "收藏夾已更新",
|
||||
"ToastCoverUpdateFailed": "封面更新失敗",
|
||||
"ToastFailedToMatch": "比對失敗",
|
||||
"ToastFailedToUpdate": "更新失敗",
|
||||
"ToastItemCoverUpdateSuccess": "項目封面已更新",
|
||||
"ToastItemDetailsUpdateSuccess": "項目詳細信息已更新",
|
||||
"ToastItemMarkedAsFinishedFailed": "標記為聽完失敗",
|
||||
@@ -735,6 +785,7 @@
|
||||
"ToastLibraryScanFailedToStart": "無法啟動掃描",
|
||||
"ToastLibraryScanStarted": "媒體庫掃描已啟動",
|
||||
"ToastLibraryUpdateSuccess": "媒體庫 \"{0}\" 已更新",
|
||||
"ToastNewEpisodesFound": "找到{0}個新單集",
|
||||
"ToastPlaylistCreateFailed": "創建播放列表失敗",
|
||||
"ToastPlaylistCreateSuccess": "已成功創建播放列表",
|
||||
"ToastPlaylistRemoveSuccess": "播放列表已刪除",
|
||||
@@ -745,6 +796,7 @@
|
||||
"ToastRSSFeedCloseSuccess": "RSS 源已關閉",
|
||||
"ToastRemoveItemFromCollectionFailed": "從收藏中刪除項目失敗",
|
||||
"ToastRemoveItemFromCollectionSuccess": "項目已從收藏中刪除",
|
||||
"ToastRenameFailed": "重新命名失敗",
|
||||
"ToastSendEbookToDeviceFailed": "發送電子書到設備失敗",
|
||||
"ToastSendEbookToDeviceSuccess": "電子書已經發送到設備 \"{0}\"",
|
||||
"ToastSeriesUpdateFailed": "更新系列失敗",
|
||||
@@ -754,6 +806,8 @@
|
||||
"ToastSocketConnected": "網路已連接",
|
||||
"ToastSocketDisconnected": "網路已斷開",
|
||||
"ToastSocketFailedToConnect": "網路連接失敗",
|
||||
"ToastUnlinkOpenIdSuccess": "已解除使用者與 OpenID 的連結",
|
||||
"ToastUserDeleteFailed": "刪除使用者失敗",
|
||||
"ToastUserDeleteSuccess": "使用者已刪除"
|
||||
"ToastUserDeleteSuccess": "使用者已刪除",
|
||||
"ToastUserPasswordMismatch": "密碼不符"
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
const nodeGlobals = {
|
||||
AbortController: 'readonly',
|
||||
Buffer: 'readonly',
|
||||
TextDecoder: 'readonly',
|
||||
URL: 'readonly',
|
||||
URLSearchParams: 'readonly',
|
||||
__dirname: 'readonly',
|
||||
clearInterval: 'readonly',
|
||||
clearTimeout: 'readonly',
|
||||
console: 'readonly',
|
||||
process: 'readonly',
|
||||
setInterval: 'readonly',
|
||||
setTimeout: 'readonly',
|
||||
structuredClone: 'readonly'
|
||||
}
|
||||
|
||||
const mochaGlobals = {
|
||||
after: 'readonly',
|
||||
afterEach: 'readonly',
|
||||
before: 'readonly',
|
||||
beforeEach: 'readonly',
|
||||
describe: 'readonly',
|
||||
it: 'readonly'
|
||||
}
|
||||
|
||||
const rules = {
|
||||
'no-undef': 'error',
|
||||
'no-redeclare': 'error',
|
||||
'no-dupe-keys': 'error',
|
||||
'no-dupe-class-members': 'error',
|
||||
'no-global-assign': 'error'
|
||||
}
|
||||
|
||||
export default [
|
||||
{
|
||||
ignores: ['**/node_modules/**', 'client/**', 'dist/**', 'dist-server/**', 'coverage/**', 'server/libs/**']
|
||||
},
|
||||
{
|
||||
files: ['index.js', 'prod.js', 'dev.js', 'server/**/*.js'],
|
||||
languageOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
sourceType: 'commonjs',
|
||||
globals: nodeGlobals
|
||||
},
|
||||
rules
|
||||
},
|
||||
{
|
||||
files: ['test/**/*.js'],
|
||||
languageOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
sourceType: 'commonjs',
|
||||
globals: {
|
||||
...nodeGlobals,
|
||||
...mochaGlobals
|
||||
}
|
||||
},
|
||||
rules
|
||||
}
|
||||
]
|
||||
@@ -16,7 +16,7 @@ const Path = require('path')
|
||||
process.env.NODE_ENV = options.dev ? 'development' : process.env.NODE_ENV || 'production'
|
||||
|
||||
const server = require('./server/Server')
|
||||
global.appRoot = __dirname
|
||||
global.appRoot = Path.basename(__dirname) === 'dist-server' ? Path.resolve(__dirname, '..') : __dirname
|
||||
|
||||
const isDev = process.env.NODE_ENV !== 'production'
|
||||
if (isDev || options['prod-with-dev-env']) {
|
||||
@@ -31,6 +31,9 @@ if (isDev || options['prod-with-dev-env']) {
|
||||
if (devEnv.AllowIframe) process.env.ALLOW_IFRAME = '1'
|
||||
if (devEnv.BackupPath) process.env.BACKUP_PATH = devEnv.BackupPath
|
||||
if (devEnv.ReactClientPath) process.env.REACT_CLIENT_PATH = devEnv.ReactClientPath
|
||||
if (devEnv.AllowedDevOrigins) {
|
||||
process.env.ALLOWED_DEV_ORIGINS = Array.isArray(devEnv.AllowedDevOrigins) ? devEnv.AllowedDevOrigins.join(',') : String(devEnv.AllowedDevOrigins)
|
||||
}
|
||||
process.env.SOURCE = 'local'
|
||||
process.env.ROUTER_BASE_PATH = devEnv.RouterBasePath ?? '/audiobookshelf'
|
||||
}
|
||||
|
||||
Generated
+944
-22
File diff suppressed because it is too large.
Load diff
+23
-15
@@ -1,35 +1,37 @@
|
||||
{
|
||||
"name": "audiobookshelf",
|
||||
"version": "2.33.0",
|
||||
"version": "3.0.0-beta",
|
||||
"buildNumber": 1,
|
||||
"description": "Self-hosted audiobook and podcast server",
|
||||
"main": "index.js",
|
||||
"main": "dist-server/index.js",
|
||||
"scripts": {
|
||||
"dev": "nodemon --watch server index.js -- --dev",
|
||||
"start": "node index.js",
|
||||
"start-dev": "node index.js --prod-with-dev-env",
|
||||
"build:server": "tsc -p tsconfig.server.json",
|
||||
"dev": "nodemon --watch server --watch index.js --watch test --watch dev.js --ext js,ts --exec \"npm run build:server && node dist-server/index.js\" -- --dev",
|
||||
"start": "node dist-server/index.js",
|
||||
"start-dev": "node dist-server/index.js --prod-with-dev-env",
|
||||
"client": "cd client && npm ci && npm run generate",
|
||||
"prod": "npm run client && npm ci && node index.js",
|
||||
"build-win": "npm run client && pkg -t node20-win-x64 -o ./dist/win/audiobookshelf -C GZip .",
|
||||
"build-win-no-compress": "npm run client && pkg -t node20-win-x64 -o ./dist/win/audiobookshelf .",
|
||||
"prod": "npm run client && npm ci && npm run build:server && npm run start",
|
||||
"build-win": "npm run client && npm run build:server && pkg --config package.json -t node24-win-x64 -o ./dist/win/audiobookshelf -C GZip dist-server/index.js",
|
||||
"build-win-no-compress": "npm run client && npm run build:server && pkg --config package.json -t node24-win-x64 -o ./dist/win/audiobookshelf dist-server/index.js",
|
||||
"build-linux": "build/linuxpackager",
|
||||
"docker": "docker buildx build --platform linux/amd64,linux/arm64 --push . -t advplyr/audiobookshelf",
|
||||
"docker-amd64-local": "docker buildx build --platform linux/amd64 --load . -t advplyr/audiobookshelf-amd64-local",
|
||||
"docker-arm64-local": "docker buildx build --platform linux/arm64 --load . -t advplyr/audiobookshelf-arm64-local",
|
||||
"deploy-linux": "node deploy/linux",
|
||||
"test": "mocha",
|
||||
"coverage": "nyc mocha"
|
||||
"test": "npm run build:server && mocha dist-server/test",
|
||||
"coverage": "npm run build:server && nyc mocha dist-server/test",
|
||||
"lint": "eslint ."
|
||||
},
|
||||
"bin": "index.js",
|
||||
"bin": "dist-server/index.js",
|
||||
"pkg": {
|
||||
"assets": [
|
||||
"client/dist/**/*",
|
||||
"node_modules/sqlite3/lib/binding/**/*.node",
|
||||
"server/migrations/*.js"
|
||||
"dist-server/server/migrations/*.js"
|
||||
],
|
||||
"scripts": [
|
||||
"index.js",
|
||||
"server/**/*.js"
|
||||
"dist-server/index.js",
|
||||
"dist-server/server/**/*.js"
|
||||
]
|
||||
},
|
||||
"mocha": {
|
||||
@@ -60,10 +62,16 @@
|
||||
"xml2js": "^0.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.13.6",
|
||||
"chai": "^4.3.10",
|
||||
"eslint": "^10.11.0",
|
||||
"mocha": "^10.2.0",
|
||||
"nodemon": "^2.0.20",
|
||||
"nyc": "^15.1.0",
|
||||
"sinon": "^17.0.1"
|
||||
"sinon": "^17.0.1",
|
||||
"typescript": "^5.7.3"
|
||||
},
|
||||
"allowScripts": {
|
||||
"sqlite3@5.1.7": true
|
||||
}
|
||||
}
|
||||
@@ -6,12 +6,14 @@
|
||||
<br />
|
||||
<a href="https://audiobookshelf.org/docs">Documentation</a>
|
||||
·
|
||||
<a href="https://audiobookshelf.org/guides">User Guides</a>
|
||||
·
|
||||
<a href="https://audiobookshelf.org/support">Support</a>
|
||||
·
|
||||
<a href="https://audiobooks.dev/">Demo</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
## ⚠️ Frontend pull requests are not being reviewed or merged for the existing Vue frontend. The frontend is currently being rewritten and migrated to React and should be available soon.
|
||||
|
||||
# About
|
||||
|
||||
Audiobookshelf is a self-hosted audiobook and podcast server.
|
||||
@@ -57,27 +59,23 @@ Try it out on the [Google Play Store](https://play.google.com/store/apps/details
|
||||
|
||||
Using Test Flight: https://testflight.apple.com/join/wiic7QIW **_(beta is full)_**
|
||||
|
||||
### Build your own tools & clients
|
||||
|
||||
Check out the [API documentation](https://api.audiobookshelf.org/)
|
||||
|
||||
<br />
|
||||
|
||||
<img alt="Library Screenshot" src="https://github.com/advplyr/audiobookshelf/raw/master/images/DemoLibrary.png" />
|
||||
|
||||
<br />
|
||||
|
||||
# Organizing your audiobooks
|
||||
# Organizing your media
|
||||
|
||||
#### Directory structure and folder names are important to Audiobookshelf!
|
||||
|
||||
See [documentation](https://audiobookshelf.org/docs#book-directory-structure) for supported directory structure, folder naming conventions, and audio file metadata usage.
|
||||
See [library docs](https://audiobookshelf.org/docs/category/libraries) for supported directory structures, folder naming conventions, and audio file metadata usage.
|
||||
|
||||
<br />
|
||||
|
||||
# Installation
|
||||
|
||||
See [install docs](https://www.audiobookshelf.org/docs)
|
||||
See [install docs](https://audiobookshelf.org/docs/category/installation)
|
||||
|
||||
<br />
|
||||
|
||||
@@ -87,276 +85,24 @@ See [install docs](https://www.audiobookshelf.org/docs)
|
||||
|
||||
#### Note: Using a subfolder is supported with no additional changes but the path must be `/audiobookshelf` (this is not changeable). See [discussion](https://github.com/advplyr/audiobookshelf/discussions/3535)
|
||||
|
||||
### NGINX Proxy Manager
|
||||
See [reverse proxy docs](https://audiobookshelf.org/docs/category/reverse-proxy)
|
||||
|
||||
Toggle websockets support.
|
||||
|
||||
<img alt="NGINX Web socket" src="https://user-images.githubusercontent.com/67830747/153679106-b2a7f5b9-0702-48c6-9740-b26b401986e9.png" />
|
||||
|
||||
### NGINX Reverse Proxy
|
||||
|
||||
Add this to the site config file on your nginx server after you have changed the relevant parts in the <> brackets, and inserted your certificate paths.
|
||||
|
||||
```bash
|
||||
server {
|
||||
listen 443 ssl;
|
||||
server_name <sub>.<domain>.<tld>;
|
||||
|
||||
access_log /var/log/nginx/audiobookshelf.access.log;
|
||||
error_log /var/log/nginx/audiobookshelf.error.log;
|
||||
|
||||
ssl_certificate /path/to/certificate;
|
||||
ssl_certificate_key /path/to/key;
|
||||
|
||||
location / {
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
|
||||
proxy_http_version 1.1;
|
||||
|
||||
proxy_pass http://<URL_to_forward_to>;
|
||||
proxy_redirect http:// https://;
|
||||
|
||||
# Prevent 413 Request Entity Too Large error
|
||||
# by increasing the maximum allowed size of the client request body
|
||||
# For example, set it to 10 GiB
|
||||
client_max_body_size 10240M;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Apache Reverse Proxy
|
||||
|
||||
Add this to the site config file on your Apache server after you have changed the relevant parts in the <> brackets, and inserted your certificate paths.
|
||||
|
||||
For this to work you must enable at least the following mods using `a2enmod`:
|
||||
|
||||
- `ssl`
|
||||
- `proxy`
|
||||
- `proxy_http`
|
||||
- `proxy_balancer`
|
||||
- `proxy_wstunnel`
|
||||
- `rewrite`
|
||||
|
||||
```bash
|
||||
<IfModule mod_ssl.c>
|
||||
<VirtualHost *:443>
|
||||
ServerName <sub>.<domain>.<tld>
|
||||
|
||||
ErrorLog ${APACHE_LOG_DIR}/error.log
|
||||
CustomLog ${APACHE_LOG_DIR}/access.log combined
|
||||
|
||||
ProxyPreserveHost On
|
||||
ProxyPass / http://localhost:<audiobookshelf_port>/
|
||||
RewriteEngine on
|
||||
RewriteCond %{HTTP:Upgrade} websocket [NC]
|
||||
RewriteCond %{HTTP:Connection} upgrade [NC]
|
||||
RewriteRule ^/?(.*) "ws://localhost:<audiobookshelf_port>/$1" [P,L]
|
||||
|
||||
# unless you're doing something special this should be generated by a
|
||||
# tool like certbot by let's encrypt
|
||||
SSLCertificateFile /path/to/cert/file
|
||||
SSLCertificateKeyFile /path/to/key/file
|
||||
</VirtualHost>
|
||||
</IfModule>
|
||||
```
|
||||
|
||||
If using Apache >= 2.4.47 you can use the following, without having to use any of the `RewriteEngine`, `RewriteCond`, or `RewriteRule` directives. For example:
|
||||
|
||||
```xml
|
||||
<Location /audiobookshelf>
|
||||
ProxyPreserveHost on
|
||||
ProxyPass http://localhost:<audiobookshelf_port>/audiobookshelf upgrade=websocket
|
||||
ProxyPassReverse http://localhost:<audiobookshelf_port>/audiobookshelf
|
||||
</Location>
|
||||
```
|
||||
|
||||
Some SSL certificates like those signed by Let's Encrypt require ACME validation. To allow Let's Encrypt to write and confirm the ACME challenge, edit your VirtualHost definition to prevent proxying traffic that queries `/.well-known` and instead serve that directly:
|
||||
|
||||
```bash
|
||||
<VirtualHost *:443>
|
||||
# ...
|
||||
|
||||
# create the directory structure /.well-known/acme-challenges
|
||||
# within DocumentRoot and give the HTTP user recursive write
|
||||
# access to it.
|
||||
DocumentRoot /path/to/local/directory
|
||||
|
||||
ProxyPreserveHost On
|
||||
ProxyPass /.well-known !
|
||||
ProxyPass / http://localhost:<audiobookshelf_port>/
|
||||
|
||||
# ...
|
||||
</VirtualHost>
|
||||
```
|
||||
|
||||
### SWAG Reverse Proxy
|
||||
|
||||
[See LinuxServer.io config sample](https://github.com/linuxserver/reverse-proxy-confs/blob/master/audiobookshelf.subdomain.conf.sample)
|
||||
|
||||
### Synology NAS Reverse Proxy Setup (DSM 7+/Quickconnect)
|
||||
|
||||
1. **Open Control Panel**
|
||||
|
||||
- Navigate to `Login Portal > Advanced`.
|
||||
|
||||
2. **General Tab**
|
||||
|
||||
- Click `Reverse Proxy` > `Create`.
|
||||
|
||||
| Setting | Value |
|
||||
| ------------------ | -------------- |
|
||||
| Reverse Proxy Name | audiobookshelf |
|
||||
|
||||
3. **Source Configuration**
|
||||
|
||||
| Setting | Value |
|
||||
| ---------------------- | ---------------------------------------- |
|
||||
| Protocol | HTTPS |
|
||||
| Hostname | `<sub>.<quickconnectdomain>.synology.me` |
|
||||
| Port | 443 |
|
||||
| Access Control Profile | Leave as is |
|
||||
|
||||
- Example Hostname: `audiobookshelf.mydomain.synology.me`
|
||||
|
||||
4. **Destination Configuration**
|
||||
|
||||
| Setting | Value |
|
||||
| -------- | ----------- |
|
||||
| Protocol | HTTP |
|
||||
| Hostname | Your NAS IP |
|
||||
| Port | 13378 |
|
||||
|
||||
5. **Custom Header Tab**
|
||||
|
||||
- Go to `Create > Websocket`.
|
||||
- Configure Headers (leave as is):
|
||||
|
||||
| Header Name | Value |
|
||||
| ----------- | --------------------- |
|
||||
| Upgrade | `$http_upgrade` |
|
||||
| Connection | `$connection_upgrade` |
|
||||
|
||||
6. **Advanced Settings Tab**
|
||||
- Leave as is.
|
||||
|
||||
### [Traefik Reverse Proxy](https://doc.traefik.io/traefik/)
|
||||
|
||||
Middleware relating to CORS will cause the app to report Unknown Error when logging in. To prevent this don't apply any of the following headers to the router for this site:
|
||||
|
||||
<ul>
|
||||
<li>accessControlAllowMethods</li>
|
||||
<li>accessControlAllowOriginList</li>
|
||||
<li>accessControlMaxAge</li>
|
||||
</ul>
|
||||
|
||||
From [@Dondochaka](https://discord.com/channels/942908292873723984/942914154254176257/945074590374318170) and [@BeastleeUK](https://discord.com/channels/942908292873723984/942914154254176257/970366039294611506) <br />
|
||||
|
||||
### Example Caddyfile - [Caddy Reverse Proxy](https://caddyserver.com/docs/caddyfile/directives/reverse_proxy)
|
||||
|
||||
```
|
||||
subdomain.domain.com {
|
||||
encode gzip zstd
|
||||
reverse_proxy <LOCAL_IP>:<PORT>
|
||||
}
|
||||
```
|
||||
|
||||
### HAProxy
|
||||
|
||||
Below is a generic HAProxy config, using `audiobookshelf.YOUR_DOMAIN.COM`.
|
||||
|
||||
To use `http2`, `ssl` is needed.
|
||||
|
||||
```make
|
||||
global
|
||||
# ... (your global settings go here)
|
||||
|
||||
defaults
|
||||
mode http
|
||||
# ... (your default settings go here)
|
||||
|
||||
frontend my_frontend
|
||||
# Bind to port 443, enable SSL, and specify the certificate list file
|
||||
bind :443 name :443 ssl crt-list /path/to/cert.crt_list alpn h2,http/1.1
|
||||
mode http
|
||||
|
||||
# Define an ACL for subdomains starting with "audiobookshelf"
|
||||
acl is_audiobookshelf hdr_beg(host) -i audiobookshelf
|
||||
|
||||
# Use the ACL to route traffic to audiobookshelf_backend if the condition is met,
|
||||
# otherwise, use the default_backend
|
||||
use_backend audiobookshelf_backend if is_audiobookshelf
|
||||
default_backend default_backend
|
||||
|
||||
backend audiobookshelf_backend
|
||||
mode http
|
||||
# ... (backend settings for audiobookshelf go here)
|
||||
|
||||
# Define the server for the audiobookshelf backend
|
||||
server audiobookshelf_server 127.0.0.99:13378
|
||||
|
||||
backend default_backend
|
||||
mode http
|
||||
# ... (default backend settings go here)
|
||||
|
||||
# Define the server for the default backend
|
||||
server default_server 127.0.0.123:8081
|
||||
|
||||
```
|
||||
|
||||
### pfSense and HAProxy
|
||||
|
||||
For pfSense the inputs are graphical, and `Health checking` is enabled.
|
||||
|
||||
#### Frontend, Default backend, access control lists and actions
|
||||
|
||||
##### Access Control lists
|
||||
|
||||
| Name | Expression | CS | Not | Value |
|
||||
| :------------: | :---------------: | :-: | :-: | :-------------: |
|
||||
| audiobookshelf | Host starts with: | | | audiobookshelf. |
|
||||
|
||||
##### Actions
|
||||
|
||||
The `condition acl names` needs to match the name above `audiobookshelf`.
|
||||
|
||||
| Action | Parameters | Condition acl names |
|
||||
| :-----------: | :------------: | :-----------------: |
|
||||
| `Use Backend` | audiobookshelf | audiobookshelf |
|
||||
|
||||
#### Backend
|
||||
|
||||
The `Name` needs to match the `Parameters` above `audiobookshelf`.
|
||||
|
||||
| Name | audiobookshelf |
|
||||
| ---- | -------------- |
|
||||
|
||||
##### Server list:
|
||||
|
||||
| Name | Expression | CS | Not | Value |
|
||||
| :------------: | :---------------: | :-: | :-: | :-------------: |
|
||||
| audiobookshelf | Host starts with: | | | audiobookshelf. |
|
||||
|
||||
##### Health checking:
|
||||
|
||||
Health checking is enabled by default. `Http check method` of `OPTIONS` is not supported on Audiobookshelf. If Health check fails, data will not be forwared. Need to do one of following:
|
||||
|
||||
- To disable: Change `Health check method` to `none`.
|
||||
- To make Health checking function: Change `Http check method` to `HEAD` or `GET`.
|
||||
|
||||
# Run from source
|
||||
<br />
|
||||
|
||||
# Contributing
|
||||
|
||||
This application is built using [NodeJs](https://nodejs.org/).
|
||||
See [contributing docs](https://audiobookshelf.org/docs/contributing/general/)
|
||||
|
||||
### Localization
|
||||
|
||||
Thank you to [Weblate](https://hosted.weblate.org/engage/audiobookshelf/) for hosting our localization infrastructure pro-bono. If you want to see Audiobookshelf in your language, please help us localize. Additional information on helping with the translations [here](https://www.audiobookshelf.org/faq#how-do-i-help-with-translations). <a href="https://hosted.weblate.org/engage/audiobookshelf/"> <img src="https://hosted.weblate.org/widget/audiobookshelf/abs-web-client/multi-auto.svg" alt="Translation status" /> </a>
|
||||
|
||||
<br />
|
||||
|
||||
# Run from source
|
||||
|
||||
This application is built using [NodeJs](https://nodejs.org/).
|
||||
|
||||
### Dev Container Setup
|
||||
|
||||
The easiest way to begin developing this project is to use a dev container. An introduction to dev containers in VSCode can be found [here](https://code.visualstudio.com/docs/devcontainers/containers).
|
||||
@@ -419,7 +165,7 @@ You are now ready to start development!
|
||||
|
||||
### Manual Environment Setup
|
||||
|
||||
If you don't want to use the dev container, you can still develop this project. First, you will need to install [NodeJs](https://nodejs.org/) (version 20) and [FFmpeg](https://ffmpeg.org/).
|
||||
If you don't want to use the dev container, you can still develop this project. First, you will need to install [NodeJs](https://nodejs.org/) (version 24) and [FFmpeg](https://ffmpeg.org/).
|
||||
|
||||
Next you will need to create a `dev.js` file in the project's root directory. This contains configuration information and paths unique to your development environment. You can find an example of this file in `.devcontainer/dev.js`.
|
||||
|
||||
@@ -437,7 +183,7 @@ cd ..
|
||||
|
||||
After setting up your development environment, either using the dev container or using your own custom environment, the following commands will help you run the server and client.
|
||||
|
||||
To run the server, you can use the command `npm run dev`. This will use the client that was built when you ran `npm run generate` in the client directory or when you started the dev container. If you make changes to the server, you will need to restart the server. If you make changes to the client, you will need to run the command `(cd client; npm run generate)` and then restart the server. By default the client runs at `localhost:3333`, though the port can be configured in `dev.js`.
|
||||
To run the server, you can use the command `npm run dev`. This will compile the server and use the client that was built when you ran `npm run generate` in the client directory or when you started the dev container. Server changes are compiled and restarted automatically. If you make changes to the client, you will need to run the command `(cd client; npm run generate)` and then restart the server. By default the client runs at `localhost:3333`, though the port can be configured in `dev.js`.
|
||||
|
||||
You can also build a version of the client that supports live reloading. To do this, start the server, then run the command `(cd client; npm run dev)`. This will run a separate instance of the client at `localhost:3000` that will be automatically updated as you make changes to the client.
|
||||
|
||||
@@ -446,7 +192,3 @@ If you are using VSCode, this project includes a couple of pre-defined targets t
|
||||
- `Debug server`—Run the server.
|
||||
- `Debug client (nuxt)`—Run the client with live reload.
|
||||
- `Debug server and client (nuxt)`—Runs both the preceding two debug targets.
|
||||
|
||||
# How to Support
|
||||
|
||||
[See the incomplete "How to Support" page](https://www.audiobookshelf.org/support)
|
||||
+24
-25
@@ -62,18 +62,6 @@ class Auth {
|
||||
return passport.authenticate('jwt', { session: false })(req, res, next)
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to generate a jwt token for a given user
|
||||
* TODO: Old method with no expiration
|
||||
* @deprecated
|
||||
*
|
||||
* @param {{ id:string, username:string }} user
|
||||
* @returns {string}
|
||||
*/
|
||||
generateAccessToken(user) {
|
||||
return this.tokenManager.generateAccessToken(user)
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate all JWT sessions for a given user
|
||||
* If user is current user and refresh token is valid, rotate tokens for the current session
|
||||
@@ -81,7 +69,7 @@ class Auth {
|
||||
* @param {import('./models/User')} user
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @returns {Promise<string>} accessToken only if user is current user and refresh token is valid
|
||||
* @returns {Promise<{ accessToken:string, refreshToken:string }|null>} new tokens for the current session if kept alive
|
||||
*/
|
||||
async invalidateJwtSessionsForUser(user, req, res) {
|
||||
return this.tokenManager.invalidateJwtSessionsForUser(user, req, res)
|
||||
@@ -216,14 +204,15 @@ class Auth {
|
||||
* @returns {Object|null} - Returns error object if validation fails, null if successful
|
||||
*/
|
||||
paramsToCookies(req, res, authMethod = 'local') {
|
||||
const TWO_MINUTES = 120000 // 2 minutes in milliseconds
|
||||
// Cookie max age in milliseconds
|
||||
const TEN_MINUTES = 10 * 60 * 1000
|
||||
const callback = req.query.redirect_uri || req.query.callback
|
||||
|
||||
// Additional handling for non-API based authMethod
|
||||
if (!this.isAuthMethodAPIBased(authMethod)) {
|
||||
// Store 'auth_state' if present in the request
|
||||
if (req.query.state) {
|
||||
res.cookie('auth_state', req.query.state, { maxAge: TWO_MINUTES, httpOnly: true })
|
||||
res.cookie('auth_state', req.query.state, { maxAge: TEN_MINUTES, httpOnly: true })
|
||||
}
|
||||
|
||||
// Validate and store the callback URL
|
||||
@@ -239,7 +228,7 @@ class Auth {
|
||||
return { error: 'Invalid callback URL - must be same-origin' }
|
||||
}
|
||||
|
||||
res.cookie('auth_cb', callback, { maxAge: TWO_MINUTES, httpOnly: true })
|
||||
res.cookie('auth_cb', callback, { maxAge: TEN_MINUTES, httpOnly: true })
|
||||
}
|
||||
|
||||
// Store the authentication method for long
|
||||
@@ -268,12 +257,11 @@ class Auth {
|
||||
} else {
|
||||
// UI request -> check if we have a callback url
|
||||
if (req.cookies.auth_cb) {
|
||||
let stateQuery = req.cookies.auth_state ? `&state=${req.cookies.auth_state}` : ''
|
||||
// UI request -> redirect to auth_cb url and send the jwt token as parameter
|
||||
// TODO: Temporarily continue sending the old token as setToken
|
||||
res.redirect(302, `${req.cookies.auth_cb}?setToken=${userResponse.user.token}&accessToken=${userResponse.user.accessToken}${stateQuery}`)
|
||||
const stateQuery = req.cookies.auth_state ? `&state=${req.cookies.auth_state}` : ''
|
||||
res.redirect(302, `${req.cookies.auth_cb}?accessToken=${userResponse.user.accessToken}${stateQuery}`)
|
||||
} else {
|
||||
res.status(400).send('No callback or already expired')
|
||||
Logger.error('[Auth] No callback or already expired')
|
||||
res.redirect(`/login?error=${encodeURIComponent('No callback or already expired')}&autoLaunch=0`)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -385,7 +373,13 @@ class Auth {
|
||||
const sessionKey = this.oidcAuthStrategy.getStrategy()._key
|
||||
|
||||
if (!req.session[sessionKey]) {
|
||||
return res.status(400).send('No session')
|
||||
// Mobile clients send code_verifier and expect a status, not the web login page
|
||||
const isMobile = req.cookies.auth_method === 'openid-mobile' || !!req.query.code_verifier
|
||||
Logger.error('[Auth] /auth/openid/callback route: No session')
|
||||
if (isMobile) {
|
||||
return res.status(400).send('No session')
|
||||
}
|
||||
return res.redirect(`/login?error=${encodeURIComponent('No sign-in session')}&autoLaunch=0`)
|
||||
}
|
||||
|
||||
// If the client sends us a code_verifier, we will tell passport to use this to send this in the token request
|
||||
@@ -471,18 +465,23 @@ class Auth {
|
||||
res.json(openIdIssuerConfig)
|
||||
})
|
||||
|
||||
// Logout route
|
||||
/**
|
||||
* Logout route
|
||||
* Use ?allDevices=1 to destroy every session for this user instead of just the current one
|
||||
*/
|
||||
router.post('/logout', async (req, res) => {
|
||||
// Refresh token be alternatively be sent in the header
|
||||
const refreshToken = req.cookies.refresh_token || req.headers['x-refresh-token']
|
||||
const allDevices = req.query.allDevices === '1'
|
||||
|
||||
// Clear refresh token cookie
|
||||
res.clearCookie('refresh_token', {
|
||||
path: '/'
|
||||
})
|
||||
|
||||
// Invalidate the session in database using refresh token
|
||||
if (refreshToken) {
|
||||
if (allDevices) {
|
||||
await this.tokenManager.invalidateAllSessionsForRefreshToken(refreshToken)
|
||||
} else if (refreshToken) {
|
||||
await this.tokenManager.invalidateRefreshToken(refreshToken)
|
||||
} else {
|
||||
Logger.info(`[Auth] logout: No refresh token on request`)
|
||||
|
||||
+24
-20
@@ -102,11 +102,6 @@ class Database {
|
||||
return this.models.libraryItem
|
||||
}
|
||||
|
||||
/** @type {typeof import('./models/PodcastEpisode')} */
|
||||
get podcastEpisodeModel() {
|
||||
return this.models.podcastEpisode
|
||||
}
|
||||
|
||||
/** @type {typeof import('./models/MediaProgress')} */
|
||||
get mediaProgressModel() {
|
||||
return this.models.mediaProgress
|
||||
@@ -420,14 +415,25 @@ class Database {
|
||||
* Create root user
|
||||
* @param {string} username
|
||||
* @param {string} pash
|
||||
* @param {Auth} auth
|
||||
* @returns {Promise<boolean>} true if created
|
||||
*/
|
||||
async createRootUser(username, pash, auth) {
|
||||
async createRootUser(username, pash) {
|
||||
if (!this.sequelize) return false
|
||||
await this.userModel.createRootUser(username, pash, auth)
|
||||
this.hasRootUser = true
|
||||
return true
|
||||
const transaction = await this.sequelize.transaction()
|
||||
try {
|
||||
const rootCount = await this.userModel.count({ where: { type: 'root' }, transaction })
|
||||
if (rootCount > 0) {
|
||||
await transaction.rollback()
|
||||
return false
|
||||
}
|
||||
await this.userModel.createRootUser(username, pash, transaction)
|
||||
await transaction.commit()
|
||||
this.hasRootUser = true
|
||||
return true
|
||||
} catch (error) {
|
||||
await transaction.rollback()
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
updateServerSettings() {
|
||||
@@ -514,19 +520,17 @@ class Database {
|
||||
})
|
||||
}
|
||||
|
||||
replaceNarratorInFilterData(oldNarrator, newNarrator) {
|
||||
for (const libraryId in this.libraryFilterData) {
|
||||
const indexOf = this.libraryFilterData[libraryId].narrators.findIndex((n) => n === oldNarrator)
|
||||
if (indexOf >= 0) {
|
||||
this.libraryFilterData[libraryId].narrators.splice(indexOf, 1, newNarrator)
|
||||
}
|
||||
replaceNarratorInFilterData(libraryId, oldNarrator, newNarrator) {
|
||||
if (!this.libraryFilterData[libraryId]) return
|
||||
const indexOf = this.libraryFilterData[libraryId].narrators.findIndex((n) => n === oldNarrator)
|
||||
if (indexOf >= 0) {
|
||||
this.libraryFilterData[libraryId].narrators.splice(indexOf, 1, newNarrator)
|
||||
}
|
||||
}
|
||||
|
||||
removeNarratorFromFilterData(narrator) {
|
||||
for (const libraryId in this.libraryFilterData) {
|
||||
this.libraryFilterData[libraryId].narrators = this.libraryFilterData[libraryId].narrators.filter((n) => n !== narrator)
|
||||
}
|
||||
removeNarratorFromFilterData(libraryId, narrator) {
|
||||
if (!this.libraryFilterData[libraryId]) return
|
||||
this.libraryFilterData[libraryId].narrators = this.libraryFilterData[libraryId].narrators.filter((n) => n !== narrator)
|
||||
}
|
||||
|
||||
addNarratorsToFilterData(libraryId, narrators) {
|
||||
|
||||
+64
-15
@@ -4,7 +4,6 @@ const express = require('express')
|
||||
const http = require('http')
|
||||
const util = require('util')
|
||||
const fs = require('./libs/fsExtra')
|
||||
const fileUpload = require('./libs/expressFileupload')
|
||||
const cookieParser = require('cookie-parser')
|
||||
const axios = require('axios')
|
||||
|
||||
@@ -13,7 +12,9 @@ const { version } = require('../package.json')
|
||||
// Utils
|
||||
const is = require('./libs/requestIp/isJs')
|
||||
const fileUtils = require('./utils/fileUtils')
|
||||
const { sweepUploadTempFiles } = require('./utils/fileUploadMiddleware')
|
||||
const { toNumber } = require('./utils/index')
|
||||
const { getRequestOrigin } = require('./utils/requestUtils')
|
||||
const Logger = require('./Logger')
|
||||
|
||||
const Auth = require('./Auth')
|
||||
@@ -45,6 +46,34 @@ const passport = require('passport')
|
||||
const expressSession = require('express-session')
|
||||
const MemoryStore = require('./libs/memorystore')
|
||||
|
||||
/**
|
||||
* Normalize to '' (root) or '/segment' with no trailing slash.
|
||||
* Matches audiobookshelf-client-react `src/lib/basePath.ts`.
|
||||
* @param {string|null|undefined} basePath
|
||||
* @returns {string}
|
||||
*/
|
||||
function normalizeBasePath(basePath) {
|
||||
if (!basePath) return ''
|
||||
|
||||
const trimmed = basePath.trim()
|
||||
if (!trimmed || trimmed === '/') return ''
|
||||
|
||||
const withLeadingSlash = trimmed.startsWith('/') ? trimmed : `/${trimmed}`
|
||||
return withLeadingSlash.endsWith('/') ? withLeadingSlash.slice(0, -1) : withLeadingSlash
|
||||
}
|
||||
|
||||
/**
|
||||
* `next()` for a custom server is a NextCustomServer wrapper. After prepare(),
|
||||
* `.server` is another NextServer wrapper; the Node server with `nextConfig` is
|
||||
* one more `.server` down. Older react clients have no basePath and must keep
|
||||
* Express-stripped URLs.
|
||||
*/
|
||||
function getPreparedNextBasePath(nextApp) {
|
||||
const nodeServer = nextApp.server?.server
|
||||
const basePath = nodeServer?.nextConfig?.basePath
|
||||
return typeof basePath === 'string' ? basePath : ''
|
||||
}
|
||||
|
||||
class Server {
|
||||
constructor(SOURCE, PORT, HOST, CONFIG_PATH, METADATA_PATH, ROUTER_BASE_PATH) {
|
||||
this.Port = PORT
|
||||
@@ -53,7 +82,7 @@ class Server {
|
||||
global.isWin = process.platform === 'win32'
|
||||
global.ConfigPath = fileUtils.filePathToPOSIX(Path.normalize(CONFIG_PATH))
|
||||
global.MetadataPath = fileUtils.filePathToPOSIX(Path.normalize(METADATA_PATH))
|
||||
global.RouterBasePath = ROUTER_BASE_PATH
|
||||
global.RouterBasePath = normalizeBasePath(ROUTER_BASE_PATH)
|
||||
global.XAccel = process.env.USE_X_ACCEL
|
||||
global.AllowCors = process.env.ALLOW_CORS === '1'
|
||||
|
||||
@@ -157,6 +186,12 @@ class Server {
|
||||
}
|
||||
|
||||
await Database.init(false)
|
||||
|
||||
if (typeof global.RouterBasePath !== 'string') {
|
||||
throw new Error('[Server] RouterBasePath must be a string before serving requests')
|
||||
}
|
||||
Logger.info(`[Server] Serving from base path "${global.RouterBasePath || '/'}"`)
|
||||
|
||||
// Create or set JWT secret in token manager
|
||||
await this.auth.tokenManager.initTokenSecret()
|
||||
|
||||
@@ -164,6 +199,7 @@ class Server {
|
||||
|
||||
await this.cleanUserData() // Remove invalid user item progress
|
||||
await CacheManager.ensureCachePaths()
|
||||
await sweepUploadTempFiles()
|
||||
|
||||
await ShareManager.init()
|
||||
await this.backupManager.init()
|
||||
@@ -288,10 +324,9 @@ class Server {
|
||||
// if RouterBasePath is set, modify all requests to include the base path
|
||||
app.use((req, res, next) => {
|
||||
const urlStartsWithRouterBasePath = req.url.startsWith(global.RouterBasePath)
|
||||
const host = req.get('host')
|
||||
const protocol = req.secure || req.get('x-forwarded-proto') === 'https' ? 'https' : 'http'
|
||||
const { origin } = getRequestOrigin(req)
|
||||
const prefix = urlStartsWithRouterBasePath ? global.RouterBasePath : ''
|
||||
req.originalHostPrefix = `${protocol}://${host}${prefix}`
|
||||
req.originalHostPrefix = `${origin}${prefix}`
|
||||
if (!urlStartsWithRouterBasePath) {
|
||||
req.url = `${global.RouterBasePath}${req.url}`
|
||||
}
|
||||
@@ -302,14 +337,6 @@ class Server {
|
||||
|
||||
this.server = http.createServer(app)
|
||||
|
||||
router.use(
|
||||
fileUpload({
|
||||
defCharset: 'utf8',
|
||||
defParamCharset: 'utf8',
|
||||
useTempFiles: true,
|
||||
tempFileDir: Path.join(global.MetadataPath, 'tmp')
|
||||
})
|
||||
)
|
||||
router.use(express.urlencoded({ extended: true, limit: '5mb' }))
|
||||
|
||||
// Skip JSON parsing for internal-api routes
|
||||
@@ -407,7 +434,25 @@ class Server {
|
||||
const nextApp = next({ dev: Logger.isDev, dir: ReactClientPath })
|
||||
const handle = nextApp.getRequestHandler()
|
||||
await nextApp.prepare()
|
||||
router.all('*', (req, res) => handle(req, res))
|
||||
const nextBasePath = getPreparedNextBasePath(nextApp)
|
||||
if (nextBasePath) {
|
||||
Logger.info(`[Server] Next client basePath is "${nextBasePath}"`)
|
||||
} else {
|
||||
Logger.info(`[Server] Next client has no basePath; leaving Express-stripped URLs as-is`)
|
||||
}
|
||||
router.all('*', (req, res) => {
|
||||
// Next is configured with the same base path as this router, but Express strips the mount
|
||||
// prefix from req.url, so put it back. The bare mount path is passed through without a
|
||||
// trailing slash, which Next would otherwise redirect away from on every request.
|
||||
// Skip when Next has no basePath (older client on master).
|
||||
if (nextBasePath && req.baseUrl) {
|
||||
const queryIndex = req.url.indexOf('?')
|
||||
const pathname = queryIndex === -1 ? req.url : req.url.slice(0, queryIndex)
|
||||
const search = queryIndex === -1 ? '' : req.url.slice(queryIndex)
|
||||
req.url = `${req.baseUrl}${pathname === '/' ? '' : pathname}${search}`
|
||||
}
|
||||
handle(req, res)
|
||||
})
|
||||
}
|
||||
|
||||
const unixSocketPrefix = 'unix/'
|
||||
@@ -434,7 +479,11 @@ class Server {
|
||||
const rootUsername = newRoot.username || 'root'
|
||||
const rootPash = newRoot.password ? await this.auth.localAuthStrategy.hashPassword(newRoot.password) : ''
|
||||
if (!rootPash) Logger.warn(`[Server] Creating root user with no password`)
|
||||
await Database.createRootUser(rootUsername, rootPash, this.auth)
|
||||
const created = await Database.createRootUser(rootUsername, rootPash)
|
||||
if (!created) {
|
||||
Logger.error(`[Server] attempt to init server when server already has a root user`)
|
||||
return res.sendStatus(500)
|
||||
}
|
||||
|
||||
res.sendStatus(200)
|
||||
}
|
||||
|
||||
+66
-10
@@ -3,6 +3,7 @@ const Logger = require('./Logger')
|
||||
const Database = require('./Database')
|
||||
const TokenManager = require('./auth/TokenManager')
|
||||
const CoverSearchManager = require('./managers/CoverSearchManager')
|
||||
const { LogLevel } = require('./utils/constants')
|
||||
|
||||
/**
|
||||
* @typedef SocketClient
|
||||
@@ -85,6 +86,14 @@ class SocketAuthority {
|
||||
}
|
||||
}
|
||||
|
||||
requireAdminSocket(socket, eventName) {
|
||||
const client = this.clients[socket.id]
|
||||
if (client?.user?.isAdminOrUp) return true
|
||||
|
||||
Logger.warn(`[SocketAuthority] Unauthorized ${eventName} socket event from socket ${socket.id}`)
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Emits event with library item to all clients that can access the library item
|
||||
* Note: Emits toOldJSONExpanded()
|
||||
@@ -179,14 +188,25 @@ class SocketAuthority {
|
||||
socket.on('auth', (token) => this.authenticateSocket(socket, token))
|
||||
|
||||
// Scanning
|
||||
socket.on('cancel_scan', (libraryId) => this.cancelScan(libraryId))
|
||||
socket.on('cancel_scan', (libraryId) => {
|
||||
if (!this.requireAdminSocket(socket, 'cancel_scan')) return
|
||||
this.cancelScan(libraryId)
|
||||
})
|
||||
|
||||
// Cover search streaming
|
||||
socket.on('search_covers', (payload) => this.handleCoverSearch(socket, payload))
|
||||
socket.on('cancel_cover_search', (requestId) => this.handleCancelCoverSearch(socket, requestId))
|
||||
|
||||
// Logs
|
||||
socket.on('set_log_listener', (level) => Logger.addSocketListener(socket, level))
|
||||
socket.on('set_log_listener', (level) => {
|
||||
if (!this.requireAdminSocket(socket, 'set_log_listener')) return
|
||||
|
||||
if (!Number.isInteger(level) || !Object.values(LogLevel).includes(level)) {
|
||||
Logger.warn(`[SocketAuthority] Invalid set_log_listener level from socket ${socket.id}`)
|
||||
return
|
||||
}
|
||||
Logger.addSocketListener(socket, level)
|
||||
})
|
||||
socket.on('remove_log_listener', () => Logger.removeSocketListener(socket.id))
|
||||
|
||||
// Sent automatically from socket.io clients
|
||||
@@ -249,22 +269,58 @@ class SocketAuthority {
|
||||
async authenticateSocket(socket, token) {
|
||||
// we don't use passport to authenticate the jwt we get over the socket connection.
|
||||
// it's easier to directly verify/decode it.
|
||||
// TODO: Support API keys for web socket connections
|
||||
const token_data = TokenManager.validateAccessToken(token)
|
||||
|
||||
if (!token_data?.userId) {
|
||||
if (!token_data) {
|
||||
// Token invalid
|
||||
Logger.error('Cannot validate socket - invalid token')
|
||||
return socket.emit('auth_failed', { message: 'Invalid token' })
|
||||
}
|
||||
|
||||
// get the user via the id from the decoded jwt.
|
||||
const user = await Database.userModel.getUserByIdOrOldId(token_data.userId)
|
||||
if (!user) {
|
||||
// user not found
|
||||
Logger.error('Cannot validate socket - invalid token')
|
||||
return socket.emit('auth_failed', { message: 'Invalid token' })
|
||||
let user = null
|
||||
|
||||
if (token_data.type === 'api') {
|
||||
// Api key based authentication
|
||||
const apiKey = await Database.apiKeyModel.getById(token_data.keyId)
|
||||
|
||||
if (!apiKey?.isActive) {
|
||||
Logger.error('Cannot validate socket - API key not found or inactive')
|
||||
return socket.emit('auth_failed', { message: 'Invalid API key' })
|
||||
}
|
||||
|
||||
if (token_data.exp && token_data.exp < Date.now() / 1000) {
|
||||
apiKey.isActive = false
|
||||
await apiKey.save()
|
||||
Logger.info(`[SocketAuthority] API key ${apiKey.id} is expired - deactivated`)
|
||||
return socket.emit('auth_failed', { message: 'API key expired' })
|
||||
}
|
||||
|
||||
user = await Database.userModel.getUserById(apiKey.userId)
|
||||
if (!user) {
|
||||
Logger.error('Cannot validate socket - user not found for API key')
|
||||
return socket.emit('auth_failed', { message: 'Invalid API key' })
|
||||
}
|
||||
} else {
|
||||
// JWT based authentication
|
||||
if (!TokenManager.isBearerAccessTokenPayload(token_data)) {
|
||||
Logger.error('Cannot validate socket - invalid token')
|
||||
return socket.emit('auth_failed', { message: 'Invalid token' })
|
||||
}
|
||||
|
||||
if (token_data.exp && token_data.exp < Date.now() / 1000) {
|
||||
Logger.error('Cannot validate socket - token expired')
|
||||
return socket.emit('auth_failed', { message: 'Token expired' })
|
||||
}
|
||||
|
||||
// get the user via the id from the decoded jwt.
|
||||
user = await Database.userModel.getUserByIdOrOldId(token_data.userId)
|
||||
if (!user) {
|
||||
// user not found
|
||||
Logger.error('Cannot validate socket - invalid token')
|
||||
return socket.emit('auth_failed', { message: 'Invalid token' })
|
||||
}
|
||||
}
|
||||
|
||||
if (!user.isActive) {
|
||||
Logger.error('Cannot validate socket - user is not active')
|
||||
return socket.emit('auth_failed', { message: 'Invalid user' })
|
||||
|
||||
@@ -49,6 +49,12 @@ class LocalAuthStrategy {
|
||||
* @param {Function} done - Passport callback
|
||||
*/
|
||||
async verifyCredentials(req, username, password, done) {
|
||||
if (typeof username !== 'string' || typeof password !== 'string') {
|
||||
this.logFailedLoginAttempt(req, typeof username === 'string' ? username : '[non-string]', 'Invalid username or password')
|
||||
done(null, null)
|
||||
return
|
||||
}
|
||||
|
||||
// Load the user given it's username
|
||||
const user = await Database.userModel.getUserByUsername(username.toLowerCase())
|
||||
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
const { LRUCache } = require('lru-cache')
|
||||
const { Request, Response } = require('express')
|
||||
const passport = require('passport')
|
||||
const OpenIDClient = require('openid-client')
|
||||
const axios = require('axios')
|
||||
const Database = require('../Database')
|
||||
const Logger = require('../Logger')
|
||||
const { getRequestOrigin } = require('../utils/requestUtils')
|
||||
|
||||
/**
|
||||
* OpenID Connect authentication strategy
|
||||
@@ -14,7 +16,10 @@ class OidcAuthStrategy {
|
||||
this.strategy = null
|
||||
this.client = null
|
||||
// Map of openId sessions indexed by oauth2 state-variable
|
||||
this.openIdAuthSession = new Map()
|
||||
this.openIdAuthSession = new LRUCache({
|
||||
ttl: 10 * 60 * 1000,
|
||||
ttlAutopurge: true
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -289,8 +294,8 @@ class OidcAuthStrategy {
|
||||
const sessionKey = strategy._key
|
||||
|
||||
try {
|
||||
const protocol = req.secure || req.get('x-forwarded-proto') === 'https' ? 'https' : 'http'
|
||||
const hostUrl = new URL(`${protocol}://${req.get('host')}`)
|
||||
const { origin } = getRequestOrigin(req)
|
||||
const hostUrl = new URL(origin)
|
||||
const isMobileFlow = req.query.response_type === 'code' || req.query.redirect_uri || req.query.code_challenge
|
||||
|
||||
// Only allow code flow (for mobile clients)
|
||||
@@ -388,17 +393,24 @@ class OidcAuthStrategy {
|
||||
* @returns {string|null}
|
||||
*/
|
||||
getEndSessionUrl(req, idToken, authMethod) {
|
||||
const client = this.getClient()
|
||||
// Logout trusts the auth_method cookie. getClient() throws when OpenID is not
|
||||
// configured, and that throw is outside the request promise, so it exits the process.
|
||||
let client
|
||||
try {
|
||||
client = this.getClient()
|
||||
} catch (error) {
|
||||
Logger.error(`[OidcAuth] Cannot build end session URL - ${error.message}`)
|
||||
return null
|
||||
}
|
||||
|
||||
if (client.issuer.end_session_endpoint && client.issuer.end_session_endpoint.length > 0) {
|
||||
let postLogoutRedirectUri = null
|
||||
|
||||
if (authMethod === 'openid') {
|
||||
const protocol = req.secure || req.get('x-forwarded-proto') === 'https' ? 'https' : 'http'
|
||||
const host = req.get('host')
|
||||
const { origin } = getRequestOrigin(req)
|
||||
// TODO: ABS does currently not support subfolders for installation
|
||||
// If we want to support it we need to include a config for the serverurl
|
||||
postLogoutRedirectUri = `${protocol}://${host}${global.RouterBasePath}/login`
|
||||
postLogoutRedirectUri = `${origin}${global.RouterBasePath}/login`
|
||||
}
|
||||
// else for openid-mobile we keep postLogoutRedirectUri on null
|
||||
// nice would be to redirect to the app here, but for example Authentik does not implement
|
||||
@@ -515,42 +527,33 @@ class OidcAuthStrategy {
|
||||
if (!callbackUrl) return false
|
||||
|
||||
try {
|
||||
// Handle relative URLs - these are always safe if they start with router base path
|
||||
if (callbackUrl.startsWith('/')) {
|
||||
// Only allow relative paths that start with the router base path
|
||||
if (callbackUrl.startsWith(global.RouterBasePath + '/')) {
|
||||
return true
|
||||
}
|
||||
// Reject protocol-relative (//host) and backslash-prefixed (/\host) values,
|
||||
// which browsers resolve to a cross-origin absolute URL.
|
||||
if (callbackUrl.startsWith('//') || callbackUrl.startsWith('/\\')) {
|
||||
Logger.warn(`[OidcAuth] Rejected protocol-relative callback URL: ${callbackUrl}`)
|
||||
return false
|
||||
}
|
||||
|
||||
const { origin: serverOrigin } = getRequestOrigin(req)
|
||||
const resolvedUrl = callbackUrl.startsWith('/') ? new URL(callbackUrl, serverOrigin) : new URL(callbackUrl)
|
||||
|
||||
if (resolvedUrl.origin !== serverOrigin) {
|
||||
Logger.warn(`[OidcAuth] Rejected callback URL to different origin: ${callbackUrl} (expected ${serverOrigin})`)
|
||||
return false
|
||||
}
|
||||
|
||||
const pathname = decodeURIComponent(resolvedUrl.pathname)
|
||||
if (pathname.startsWith('//') || pathname.startsWith('/\\')) {
|
||||
Logger.warn(`[OidcAuth] Rejected protocol-relative callback URL path: ${callbackUrl}`)
|
||||
return false
|
||||
}
|
||||
|
||||
if (!resolvedUrl.pathname.startsWith(global.RouterBasePath + '/')) {
|
||||
Logger.warn(`[OidcAuth] Rejected callback URL outside router base path: ${callbackUrl}`)
|
||||
return false
|
||||
}
|
||||
|
||||
// For absolute URLs, ensure they point to the same origin
|
||||
const callbackUrlObj = new URL(callbackUrl)
|
||||
// NPM appends both http and https in x-forwarded-proto sometimes, so we need to check for both
|
||||
const xfp = (req.get('x-forwarded-proto') || '').toLowerCase()
|
||||
const currentProtocol =
|
||||
req.secure ||
|
||||
xfp
|
||||
.split(',')
|
||||
.map((s) => s.trim())
|
||||
.includes('https')
|
||||
? 'https'
|
||||
: 'http'
|
||||
const currentHost = req.get('host')
|
||||
|
||||
// Check if protocol and host match exactly
|
||||
if (callbackUrlObj.protocol === currentProtocol + ':' && callbackUrlObj.host === currentHost) {
|
||||
// Additional check: ensure path starts with router base path
|
||||
if (callbackUrlObj.pathname.startsWith(global.RouterBasePath + '/')) {
|
||||
return true
|
||||
}
|
||||
Logger.warn(`[OidcAuth] Rejected same-origin callback URL outside router base path: ${callbackUrl}`)
|
||||
return false
|
||||
}
|
||||
|
||||
Logger.warn(`[OidcAuth] Rejected callback URL to different origin: ${callbackUrl} (expected ${currentProtocol}://${currentHost})`)
|
||||
return false
|
||||
return true
|
||||
} catch (error) {
|
||||
Logger.error(`[OidcAuth] Invalid callback URL format: ${callbackUrl}`, error)
|
||||
return false
|
||||
|
||||
+161
-62
@@ -1,10 +1,12 @@
|
||||
const { Op } = require('sequelize')
|
||||
const uuid = require('uuid')
|
||||
|
||||
const Database = require('../Database')
|
||||
const Logger = require('../Logger')
|
||||
|
||||
const requestIp = require('../libs/requestIp')
|
||||
const jwt = require('../libs/jsonwebtoken')
|
||||
const { isRequestSecure } = require('../utils/requestUtils')
|
||||
|
||||
class TokenManager {
|
||||
/** @type {string} JWT secret key */
|
||||
@@ -15,6 +17,8 @@ class TokenManager {
|
||||
this.RefreshTokenExpiry = parseInt(process.env.REFRESH_TOKEN_EXPIRY) || 30 * 24 * 60 * 60 // 30 days
|
||||
/** @type {number} Access token expiry in seconds */
|
||||
this.AccessTokenExpiry = parseInt(process.env.ACCESS_TOKEN_EXPIRY) || 1 * 60 * 60 // 1 hour
|
||||
/** @type {number} Grace period in seconds during which a rotated (old) refresh token is still accepted */
|
||||
this.RefreshTokenGracePeriod = parseInt(process.env.REFRESH_TOKEN_GRACE_PERIOD) || 10 * 60 // 10 minutes
|
||||
|
||||
if (parseInt(process.env.REFRESH_TOKEN_EXPIRY) > 0) {
|
||||
Logger.info(`[TokenManager] Refresh token expiry set from ENV variable to ${this.RefreshTokenExpiry} seconds`)
|
||||
@@ -22,6 +26,9 @@ class TokenManager {
|
||||
if (parseInt(process.env.ACCESS_TOKEN_EXPIRY) > 0) {
|
||||
Logger.info(`[TokenManager] Access token expiry set from ENV variable to ${this.AccessTokenExpiry} seconds`)
|
||||
}
|
||||
if (parseInt(process.env.REFRESH_TOKEN_GRACE_PERIOD) > 0) {
|
||||
Logger.info(`[TokenManager] Refresh token grace period set from ENV variable to ${this.RefreshTokenGracePeriod} seconds`)
|
||||
}
|
||||
}
|
||||
|
||||
get TokenSecret() {
|
||||
@@ -58,53 +65,43 @@ class TokenManager {
|
||||
setRefreshTokenCookie(req, res, refreshToken) {
|
||||
res.cookie('refresh_token', refreshToken, {
|
||||
httpOnly: true,
|
||||
secure: req.secure || req.get('x-forwarded-proto') === 'https',
|
||||
secure: isRequestSecure(req),
|
||||
sameSite: 'lax',
|
||||
maxAge: this.RefreshTokenExpiry * 1000,
|
||||
path: '/'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a decoded JWT payload may authenticate API/socket requests (not refresh-only credentials).
|
||||
*
|
||||
* @param {Object} decoded
|
||||
* @returns {boolean}
|
||||
*/
|
||||
static isBearerAccessTokenPayload(decoded) {
|
||||
if (!decoded?.userId) return false
|
||||
// Legacy API tokens were signed with only userId and username, and never expired.
|
||||
if (decoded.type !== 'access') return false
|
||||
if (!decoded.exp) return false
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to validate a jwt token for a given user
|
||||
* Used to authenticate socket connections
|
||||
* TODO: Support API keys for web socket connections
|
||||
*
|
||||
* @param {string} token
|
||||
* @returns {Object} tokens data
|
||||
*/
|
||||
static validateAccessToken(token) {
|
||||
try {
|
||||
return jwt.verify(token, TokenManager.TokenSecret)
|
||||
// Expiration is handled by the caller so expired API keys can be deactivated
|
||||
return jwt.verify(token, TokenManager.TokenSecret, { ignoreExpiration: true })
|
||||
} catch (err) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a JWT token for a given user
|
||||
* TODO: Old method with no expiration
|
||||
* @deprecated
|
||||
*
|
||||
* @param {{ id:string, username:string }} user
|
||||
* @returns {string}
|
||||
*/
|
||||
static generateAccessToken(user) {
|
||||
return jwt.sign({ userId: user.id, username: user.username }, TokenManager.TokenSecret)
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to generate a jwt token for a given user
|
||||
* TODO: Old method with no expiration
|
||||
* @deprecated
|
||||
*
|
||||
* @param {{ id:string, username:string }} user
|
||||
* @returns {string}
|
||||
*/
|
||||
generateAccessToken(user) {
|
||||
return TokenManager.generateAccessToken(user)
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate access token for a given user
|
||||
*
|
||||
@@ -115,6 +112,7 @@ class TokenManager {
|
||||
const payload = {
|
||||
userId: user.id,
|
||||
username: user.username,
|
||||
jti: uuid.v4(),
|
||||
type: 'access'
|
||||
}
|
||||
const options = {
|
||||
@@ -138,6 +136,7 @@ class TokenManager {
|
||||
const payload = {
|
||||
userId: user.id,
|
||||
username: user.username,
|
||||
jti: uuid.v4(),
|
||||
type: 'refresh'
|
||||
}
|
||||
const options = {
|
||||
@@ -183,20 +182,60 @@ class TokenManager {
|
||||
* @param {import('../models/User')} user
|
||||
* @param {import('express').Request} req
|
||||
* @param {import('express').Response} res
|
||||
* @param {boolean} gracePeriod - whether to use the grace period
|
||||
* @returns {Promise<{ accessToken:string, refreshToken:string }>}
|
||||
*/
|
||||
async rotateTokensForSession(session, user, req, res) {
|
||||
// Generate new tokens
|
||||
async rotateTokensForSession(session, user, req, res, gracePeriod = true) {
|
||||
const previousRefreshToken = session.refreshToken
|
||||
const newAccessToken = this.generateTempAccessToken(user)
|
||||
const newRefreshToken = this.generateRefreshToken(user)
|
||||
|
||||
// Calculate new expiration time
|
||||
let newRefreshToken = this.generateRefreshToken(user)
|
||||
const newExpiresAt = new Date(Date.now() + this.RefreshTokenExpiry * 1000)
|
||||
|
||||
// Update the session with the new refresh token and expiration
|
||||
session.refreshToken = newRefreshToken
|
||||
session.expiresAt = newExpiresAt
|
||||
await session.save()
|
||||
let lastRefreshToken = null
|
||||
let lastRefreshTokenExpiresAt = null
|
||||
if (gracePeriod) {
|
||||
// Set grace period of old refresh token in case of race condition in token rotation.
|
||||
// During this window a retry with the old refresh token returns the already-rotated
|
||||
// current token instead of failing, so a client that never received the rotation
|
||||
// response (e.g. dropped/suspended mobile request) can still recover the session.
|
||||
// Configurable via REFRESH_TOKEN_GRACE_PERIOD; may need to be longer if fetching the
|
||||
// user data takes longer due to large progress objects.
|
||||
lastRefreshToken = previousRefreshToken
|
||||
lastRefreshTokenExpiresAt = new Date(Date.now() + this.RefreshTokenGracePeriod * 1000)
|
||||
}
|
||||
|
||||
// Only update if this session row still has the refresh token we read
|
||||
const [numUpdated] = await Database.sessionModel.update(
|
||||
{
|
||||
refreshToken: newRefreshToken,
|
||||
expiresAt: newExpiresAt,
|
||||
lastRefreshToken,
|
||||
lastRefreshTokenExpiresAt
|
||||
},
|
||||
{
|
||||
where: {
|
||||
id: session.id,
|
||||
refreshToken: previousRefreshToken
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
if (numUpdated === 0) {
|
||||
Logger.debug(`[TokenManager] Race condition in rotateTokensForSession for user ${user.id}, getting new token`)
|
||||
|
||||
const updatedSession = await Database.sessionModel.findOne({ where: { id: session.id } })
|
||||
|
||||
newRefreshToken = updatedSession.refreshToken
|
||||
session.refreshToken = updatedSession.refreshToken
|
||||
session.expiresAt = updatedSession.expiresAt
|
||||
session.lastRefreshToken = updatedSession.lastRefreshToken
|
||||
session.lastRefreshTokenExpiresAt = updatedSession.lastRefreshTokenExpiresAt
|
||||
} else {
|
||||
session.refreshToken = newRefreshToken
|
||||
session.expiresAt = newExpiresAt
|
||||
session.lastRefreshToken = lastRefreshToken
|
||||
session.lastRefreshTokenExpiresAt = lastRefreshTokenExpiresAt
|
||||
}
|
||||
|
||||
// Set new refresh token cookie
|
||||
this.setRefreshTokenCookie(req, res, newRefreshToken)
|
||||
@@ -234,9 +273,20 @@ class TokenManager {
|
||||
}
|
||||
|
||||
const user = await Database.userModel.getUserById(apiKey.userId)
|
||||
|
||||
if (!user?.isActive) {
|
||||
// deny login
|
||||
done(null, null)
|
||||
return
|
||||
}
|
||||
|
||||
done(null, user)
|
||||
} else {
|
||||
// JWT based authentication
|
||||
// JWT based authentication — refresh tokens are only valid at POST /auth/refresh
|
||||
if (!TokenManager.isBearerAccessTokenPayload(jwt_payload)) {
|
||||
done(null, null)
|
||||
return
|
||||
}
|
||||
|
||||
// Check if the jwt is expired
|
||||
if (jwt_payload.exp && jwt_payload.exp < Date.now() / 1000) {
|
||||
@@ -253,15 +303,6 @@ class TokenManager {
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: Temporary flag to report old tokens to users
|
||||
// May be a better place for this but here means we dont have to decode the token again
|
||||
if (!jwt_payload.exp && !user.isOldToken) {
|
||||
Logger.debug(`[TokenManager] User ${user.username} is using an access token without an expiration`)
|
||||
user.isOldToken = true
|
||||
} else if (jwt_payload.exp && user.isOldToken !== undefined) {
|
||||
delete user.isOldToken
|
||||
}
|
||||
|
||||
// approve login
|
||||
done(null, user)
|
||||
}
|
||||
@@ -287,23 +328,40 @@ class TokenManager {
|
||||
}
|
||||
}
|
||||
|
||||
const session = await Database.sessionModel.findOne({
|
||||
where: { refreshToken: refreshToken }
|
||||
let session = await Database.sessionModel.findOne({
|
||||
where: {
|
||||
[Op.or]: [{ refreshToken: refreshToken }, { lastRefreshToken: refreshToken }]
|
||||
}
|
||||
})
|
||||
|
||||
if (!session) {
|
||||
Logger.error(`[TokenManager] Failed to refresh token. Session not found for refresh token: ${refreshToken}`)
|
||||
Logger.error(`[TokenManager] Failed to refresh token. Session not found`)
|
||||
return {
|
||||
error: 'Invalid refresh token'
|
||||
}
|
||||
}
|
||||
|
||||
// Check if session is expired in database
|
||||
if (session.expiresAt < new Date()) {
|
||||
Logger.info(`[TokenManager] Session expired in database, cleaning up`)
|
||||
await session.destroy()
|
||||
return {
|
||||
error: 'Refresh token expired'
|
||||
let isGracePeriod = false
|
||||
if (session.refreshToken !== refreshToken) {
|
||||
// Token matched lastRefreshToken
|
||||
if (session.lastRefreshTokenExpiresAt && session.lastRefreshTokenExpiresAt > new Date()) {
|
||||
isGracePeriod = true
|
||||
Logger.debug(`[TokenManager] Grace period hit for user ${session.userId}`)
|
||||
} else {
|
||||
Logger.debug(`[TokenManager] Grace period expired for user ${session.userId}`)
|
||||
return {
|
||||
error: 'Invalid refresh token'
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Token matched current refreshToken
|
||||
// Check if session is expired in database
|
||||
if (session.expiresAt < new Date()) {
|
||||
Logger.info(`[TokenManager] Session expired in database, cleaning up`)
|
||||
await session.destroy()
|
||||
return {
|
||||
error: 'Refresh token expired'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -315,6 +373,20 @@ class TokenManager {
|
||||
}
|
||||
}
|
||||
|
||||
if (isGracePeriod) {
|
||||
// Return the already rotated refresh token store in the database,
|
||||
// and generate a new access token without changing the refresh token
|
||||
// again
|
||||
const accessToken = this.generateTempAccessToken(user)
|
||||
this.setRefreshTokenCookie(req, res, session.refreshToken)
|
||||
|
||||
return {
|
||||
accessToken,
|
||||
refreshToken: session.refreshToken,
|
||||
user
|
||||
}
|
||||
}
|
||||
|
||||
const newTokens = await this.rotateTokensForSession(session, user, req, res)
|
||||
return {
|
||||
accessToken: newTokens.accessToken,
|
||||
@@ -359,16 +431,21 @@ class TokenManager {
|
||||
* @param {import('../models/User')} user
|
||||
* @param {import('express').Request} req
|
||||
* @param {import('express').Response} res
|
||||
* @returns {Promise<string>} accessToken only if user is current user and refresh token is valid
|
||||
* @returns {Promise<{ accessToken:string, refreshToken:string }|null>} new tokens for the current session if kept alive
|
||||
*/
|
||||
async invalidateJwtSessionsForUser(user, req, res) {
|
||||
const currentRefreshToken = req.cookies.refresh_token
|
||||
const currentRefreshToken = req.cookies.refresh_token || req.headers['x-refresh-token']
|
||||
if (req.user.id === user.id && currentRefreshToken) {
|
||||
// Current user is the same as the user to invalidate sessions for
|
||||
// So rotate token for current session
|
||||
const currentSession = await Database.sessionModel.findOne({ where: { refreshToken: currentRefreshToken } })
|
||||
const currentSession = await Database.sessionModel.findOne({
|
||||
where: {
|
||||
userId: user.id,
|
||||
[Op.or]: [{ refreshToken: currentRefreshToken }, { lastRefreshToken: currentRefreshToken }]
|
||||
}
|
||||
})
|
||||
if (currentSession) {
|
||||
const newTokens = await this.rotateTokensForSession(currentSession, user, req, res)
|
||||
const newTokens = await this.rotateTokensForSession(currentSession, user, req, res, false)
|
||||
|
||||
// Invalidate all sessions for the user except the current one
|
||||
await Database.sessionModel.destroy({
|
||||
@@ -380,9 +457,12 @@ class TokenManager {
|
||||
}
|
||||
})
|
||||
|
||||
return newTokens.accessToken
|
||||
return {
|
||||
accessToken: newTokens.accessToken,
|
||||
refreshToken: newTokens.refreshToken
|
||||
}
|
||||
} else {
|
||||
Logger.error(`[TokenManager] No session found to rotate tokens for refresh token ${currentRefreshToken}`)
|
||||
Logger.error(`[TokenManager] No session found to rotate tokens`)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -392,6 +472,25 @@ class TokenManager {
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy all JWT sessions for the user that owns this refresh token
|
||||
*
|
||||
* @param {string} refreshToken
|
||||
*/
|
||||
async invalidateAllSessionsForRefreshToken(refreshToken) {
|
||||
if (!refreshToken) return
|
||||
|
||||
const session = await Database.sessionModel.findOne({
|
||||
where: {
|
||||
[Op.or]: [{ refreshToken: refreshToken }, { lastRefreshToken: refreshToken }]
|
||||
}
|
||||
})
|
||||
if (!session) return
|
||||
|
||||
const numDeleted = await Database.sessionModel.destroy({ where: { userId: session.userId } })
|
||||
Logger.info(`[TokenManager] Invalidated all JWT sessions for user ${session.userId}, ${numDeleted} deleted`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate a refresh token - used for logout
|
||||
*
|
||||
@@ -406,7 +505,7 @@ class TokenManager {
|
||||
|
||||
try {
|
||||
const numDeleted = await Database.sessionModel.destroy({ where: { refreshToken: refreshToken } })
|
||||
Logger.info(`[TokenManager] Refresh token ${refreshToken} invalidated, ${numDeleted} sessions deleted`)
|
||||
Logger.info(`[TokenManager] Refresh token invalidated, ${numDeleted} sessions deleted`)
|
||||
return true
|
||||
} catch (error) {
|
||||
Logger.error(`[TokenManager] Error invalidating refresh token: ${error.message}`)
|
||||
|
||||
@@ -10,7 +10,7 @@ const CacheManager = require('../managers/CacheManager')
|
||||
const CoverManager = require('../managers/CoverManager')
|
||||
const AuthorFinder = require('../finders/AuthorFinder')
|
||||
|
||||
const { reqSupportsWebp, isValidASIN } = require('../utils/index')
|
||||
const { reqSupportsWebp, isValidASIN, clampPositiveInt } = require('../utils/index')
|
||||
|
||||
const naturalSort = createNewSortInstance({
|
||||
comparer: new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' }).compare
|
||||
@@ -149,7 +149,7 @@ class AuthorController {
|
||||
})
|
||||
if (libraryItems.length) {
|
||||
await Database.bookAuthorModel.removeByIds(req.author.id) // Remove all old BookAuthor
|
||||
await Database.bookAuthorModel.bulkCreate(bookAuthorsToCreate) // Create all new BookAuthor
|
||||
await Database.bookAuthorModel.bulkCreate(bookAuthorsToCreate, { ignoreDuplicates: true }) // Create all new unique BookAuthor
|
||||
for (const libraryItem of libraryItems) {
|
||||
await libraryItem.saveMetadataFile()
|
||||
}
|
||||
@@ -412,8 +412,8 @@ class AuthorController {
|
||||
|
||||
const options = {
|
||||
format: format || (reqSupportsWebp(req) ? 'webp' : 'jpeg'),
|
||||
height: height ? parseInt(height) : null,
|
||||
width: width ? parseInt(width) : null
|
||||
height: clampPositiveInt(height ? parseInt(height) : null, 4096),
|
||||
width: clampPositiveInt(width ? parseInt(width) : null, 4096)
|
||||
}
|
||||
return CacheManager.handleAuthorCache(res, authorId, options)
|
||||
}
|
||||
@@ -427,6 +427,7 @@ class AuthorController {
|
||||
async middleware(req, res, next) {
|
||||
const author = await Database.authorModel.findByPk(req.params.id)
|
||||
if (!author) return res.sendStatus(404)
|
||||
if (!req.user.checkCanAccessLibrary(author.libraryId)) return res.sendStatus(404)
|
||||
|
||||
if (req.method == 'DELETE' && !req.user.canDelete) {
|
||||
Logger.warn(`[AuthorController] User "${req.user.username}" attempted to delete without permission`)
|
||||
|
||||
@@ -3,6 +3,7 @@ const Sequelize = require('sequelize')
|
||||
const Logger = require('../Logger')
|
||||
const SocketAuthority = require('../SocketAuthority')
|
||||
const Database = require('../Database')
|
||||
const htmlSanitizer = require('../utils/htmlSanitizer')
|
||||
|
||||
const RssFeedManager = require('../managers/RssFeedManager')
|
||||
|
||||
@@ -31,13 +32,19 @@ class CollectionController {
|
||||
async create(req, res) {
|
||||
const reqBody = req.body || {}
|
||||
|
||||
const nameCleaned = htmlSanitizer.stripAllTags(reqBody.name)
|
||||
|
||||
// Validation
|
||||
if (!reqBody.name || !reqBody.libraryId) {
|
||||
if (!nameCleaned || !reqBody.libraryId) {
|
||||
return res.status(400).send('Invalid collection data')
|
||||
}
|
||||
if (reqBody.description && typeof reqBody.description !== 'string') {
|
||||
return res.status(400).send('Invalid collection description')
|
||||
}
|
||||
if (!req.user.checkCanAccessLibrary(reqBody.libraryId)) {
|
||||
Logger.warn(`[CollectionController] User "${req.user.username}" attempted to create collection in inaccessible library ${reqBody.libraryId}`)
|
||||
return res.sendStatus(403)
|
||||
}
|
||||
const libraryItemIds = (reqBody.books || []).filter((b) => !!b && typeof b == 'string')
|
||||
if (!libraryItemIds.length) {
|
||||
return res.status(400).send('Invalid collection data. No books')
|
||||
@@ -65,7 +72,7 @@ class CollectionController {
|
||||
newCollection = await Database.collectionModel.create(
|
||||
{
|
||||
libraryId: reqBody.libraryId,
|
||||
name: reqBody.name,
|
||||
name: nameCleaned,
|
||||
description: reqBody.description || null
|
||||
},
|
||||
{ transaction }
|
||||
@@ -89,12 +96,8 @@ class CollectionController {
|
||||
return res.status(500).send('Failed to create collection')
|
||||
}
|
||||
|
||||
// Load books expanded
|
||||
newCollection.books = await newCollection.getBooksExpandedWithLibraryItem()
|
||||
|
||||
// Note: The old collection model stores expanded libraryItems in the books property
|
||||
const jsonExpanded = newCollection.toOldJSONExpanded()
|
||||
SocketAuthority.emitter('collection_added', jsonExpanded)
|
||||
const jsonExpanded = await newCollection.getOldJsonExpanded(req.user)
|
||||
if (jsonExpanded) SocketAuthority.clientEmitter(req.user.id, 'collection_added', jsonExpanded)
|
||||
res.json(jsonExpanded)
|
||||
}
|
||||
|
||||
@@ -106,8 +109,9 @@ class CollectionController {
|
||||
*/
|
||||
async findAll(req, res) {
|
||||
const collectionsExpanded = await Database.collectionModel.getOldCollectionsJsonExpanded(req.user)
|
||||
const accessibleCollections = collectionsExpanded.filter((c) => req.user.checkCanAccessLibrary(c.libraryId))
|
||||
res.json({
|
||||
collections: collectionsExpanded
|
||||
collections: accessibleCollections
|
||||
})
|
||||
}
|
||||
|
||||
@@ -145,9 +149,12 @@ class CollectionController {
|
||||
collectionUpdatePayload.description = req.body.description
|
||||
wasUpdated = true
|
||||
}
|
||||
if (req.body.name !== undefined && req.body.name !== req.collection.name) {
|
||||
collectionUpdatePayload.name = req.body.name
|
||||
wasUpdated = true
|
||||
if (req.body.name !== undefined && typeof req.body.name === 'string') {
|
||||
const nameCleaned = htmlSanitizer.stripAllTags(req.body.name)
|
||||
if (nameCleaned !== req.collection.name) {
|
||||
collectionUpdatePayload.name = nameCleaned
|
||||
wasUpdated = true
|
||||
}
|
||||
}
|
||||
|
||||
if (wasUpdated) {
|
||||
@@ -185,9 +192,9 @@ class CollectionController {
|
||||
}
|
||||
}
|
||||
|
||||
const jsonExpanded = await req.collection.getOldJsonExpanded()
|
||||
if (wasUpdated) {
|
||||
SocketAuthority.emitter('collection_updated', jsonExpanded)
|
||||
const jsonExpanded = await req.collection.getOldJsonExpanded(req.user)
|
||||
if (wasUpdated && jsonExpanded) {
|
||||
SocketAuthority.clientEmitter(req.user.id, 'collection_updated', jsonExpanded)
|
||||
}
|
||||
res.json(jsonExpanded)
|
||||
}
|
||||
@@ -201,14 +208,17 @@ class CollectionController {
|
||||
* @param {Response} res
|
||||
*/
|
||||
async delete(req, res) {
|
||||
const jsonExpanded = await req.collection.getOldJsonExpanded()
|
||||
const jsonExpanded = await req.collection.getOldJsonExpanded(req.user)
|
||||
|
||||
// Close rss feed - remove from db and emit socket event
|
||||
await RssFeedManager.closeFeedForEntityId(req.collection.id)
|
||||
|
||||
if (jsonExpanded) {
|
||||
SocketAuthority.clientEmitter(req.user.id, 'collection_removed', jsonExpanded)
|
||||
}
|
||||
|
||||
await req.collection.destroy()
|
||||
|
||||
SocketAuthority.emitter('collection_removed', jsonExpanded)
|
||||
res.sendStatus(200)
|
||||
}
|
||||
|
||||
@@ -243,8 +253,8 @@ class CollectionController {
|
||||
bookId: libraryItem.mediaId,
|
||||
order: collectionBooks.length + 1
|
||||
})
|
||||
const jsonExpanded = await req.collection.getOldJsonExpanded()
|
||||
SocketAuthority.emitter('collection_updated', jsonExpanded)
|
||||
const jsonExpanded = await req.collection.getOldJsonExpanded(req.user)
|
||||
if (jsonExpanded) SocketAuthority.clientEmitter(req.user.id, 'collection_updated', jsonExpanded)
|
||||
res.json(jsonExpanded)
|
||||
}
|
||||
|
||||
@@ -288,10 +298,10 @@ class CollectionController {
|
||||
order++
|
||||
}
|
||||
|
||||
jsonExpanded = await req.collection.getOldJsonExpanded()
|
||||
SocketAuthority.emitter('collection_updated', jsonExpanded)
|
||||
jsonExpanded = await req.collection.getOldJsonExpanded(req.user)
|
||||
if (jsonExpanded) SocketAuthority.clientEmitter(req.user.id, 'collection_updated', jsonExpanded)
|
||||
} else {
|
||||
jsonExpanded = await req.collection.getOldJsonExpanded()
|
||||
jsonExpanded = await req.collection.getOldJsonExpanded(req.user)
|
||||
}
|
||||
res.json(jsonExpanded)
|
||||
}
|
||||
@@ -350,10 +360,10 @@ class CollectionController {
|
||||
if (hasUpdated) {
|
||||
await Database.collectionBookModel.bulkCreate(collectionBooksToAdd)
|
||||
|
||||
jsonExpanded = await req.collection.getOldJsonExpanded()
|
||||
SocketAuthority.emitter('collection_updated', jsonExpanded)
|
||||
jsonExpanded = await req.collection.getOldJsonExpanded(req.user)
|
||||
if (jsonExpanded) SocketAuthority.clientEmitter(req.user.id, 'collection_updated', jsonExpanded)
|
||||
} else {
|
||||
jsonExpanded = await req.collection.getOldJsonExpanded()
|
||||
jsonExpanded = await req.collection.getOldJsonExpanded(req.user)
|
||||
}
|
||||
res.json(jsonExpanded)
|
||||
}
|
||||
@@ -406,9 +416,9 @@ class CollectionController {
|
||||
order++
|
||||
}
|
||||
|
||||
let jsonExpanded = await req.collection.getOldJsonExpanded()
|
||||
if (hasUpdated) {
|
||||
SocketAuthority.emitter('collection_updated', jsonExpanded)
|
||||
let jsonExpanded = await req.collection.getOldJsonExpanded(req.user)
|
||||
if (hasUpdated && jsonExpanded) {
|
||||
SocketAuthority.clientEmitter(req.user.id, 'collection_updated', jsonExpanded)
|
||||
}
|
||||
res.json(jsonExpanded)
|
||||
}
|
||||
@@ -425,6 +435,10 @@ class CollectionController {
|
||||
if (!collection) {
|
||||
return res.status(404).send('Collection not found')
|
||||
}
|
||||
if (!req.user.checkCanAccessLibrary(collection.libraryId)) {
|
||||
Logger.warn(`[CollectionController] User "${req.user.username}" attempted to access collection ${collection.id} in inaccessible library ${collection.libraryId}`)
|
||||
return res.status(404).send('Collection not found')
|
||||
}
|
||||
req.collection = collection
|
||||
}
|
||||
|
||||
@@ -432,7 +446,7 @@ class CollectionController {
|
||||
if (req.method == 'DELETE' && !req.params.bookId && !req.user.canDelete) {
|
||||
Logger.warn(`[CollectionController] User "${req.user.username}" attempted to delete without permission`)
|
||||
return res.sendStatus(403)
|
||||
} else if ((req.method == 'PATCH' || req.method == 'POST') && !req.user.canUpdate) {
|
||||
} else if ((req.method == 'PATCH' || req.method == 'POST' || (req.method == 'DELETE' && req.params.bookId)) && !req.user.canUpdate) {
|
||||
Logger.warn(`[CollectionController] User "${req.user.username}" attempted to update without permission`)
|
||||
return res.sendStatus(403)
|
||||
}
|
||||
|
||||
@@ -117,7 +117,7 @@ class FileSystemController {
|
||||
filepath = fileUtils.filePathToPOSIX(filepath)
|
||||
|
||||
// Ensure filepath is inside library folder (prevents directory traversal)
|
||||
if (!filepath.startsWith(libraryFolder.path)) {
|
||||
if (!fileUtils.isSameOrSubPath(libraryFolder.path, filepath)) {
|
||||
Logger.error(`[FileSystemController] Filepath is not inside library folder: ${filepath}`)
|
||||
return res.sendStatus(400)
|
||||
}
|
||||
|
||||
@@ -462,7 +462,7 @@ class LibraryController {
|
||||
}
|
||||
}
|
||||
Logger.info(`[LibraryController] Removing library item "${libraryItem.id}" from folder "${folder.path}"`)
|
||||
await this.handleDeleteLibraryItem(libraryItem.id, mediaItemIds)
|
||||
await this.handleDeleteLibraryItem(libraryItem.id, mediaItemIds, req.library.id)
|
||||
}
|
||||
|
||||
if (authorIds.length) {
|
||||
@@ -563,7 +563,7 @@ class LibraryController {
|
||||
mediaItemIds.push(libraryItem.mediaId)
|
||||
}
|
||||
Logger.info(`[LibraryController] Removing library item "${libraryItem.id}" from library "${req.library.name}"`)
|
||||
await this.handleDeleteLibraryItem(libraryItem.id, mediaItemIds)
|
||||
await this.handleDeleteLibraryItem(libraryItem.id, mediaItemIds, req.library.id)
|
||||
}
|
||||
|
||||
// Set PlaybackSessions libraryId to null
|
||||
@@ -714,7 +714,7 @@ class LibraryController {
|
||||
}
|
||||
}
|
||||
Logger.info(`[LibraryController] Removing library item "${libraryItem.id}" with issue`)
|
||||
await this.handleDeleteLibraryItem(libraryItem.id, mediaItemIds)
|
||||
await this.handleDeleteLibraryItem(libraryItem.id, mediaItemIds, req.library.id)
|
||||
}
|
||||
|
||||
if (authorIds.length) {
|
||||
@@ -782,7 +782,7 @@ class LibraryController {
|
||||
.filter((v) => !!v)
|
||||
|
||||
const series = await Database.seriesModel.findByPk(req.params.seriesId)
|
||||
if (!series) return res.sendStatus(404)
|
||||
if (!series || series.libraryId !== req.library.id) return res.sendStatus(404)
|
||||
|
||||
const libraryItemsInSeries = await libraryItemsBookFilters.getLibraryItemsForSeries(series, req.user)
|
||||
|
||||
@@ -1167,11 +1167,11 @@ class LibraryController {
|
||||
}
|
||||
|
||||
// Update filter data
|
||||
Database.replaceNarratorInFilterData(narratorName, updatedName)
|
||||
Database.replaceNarratorInFilterData(req.library.id, narratorName, updatedName)
|
||||
|
||||
const itemsUpdated = []
|
||||
|
||||
const itemsWithNarrator = await libraryItemFilters.getAllLibraryItemsWithNarrators([narratorName])
|
||||
const itemsWithNarrator = await libraryItemFilters.getAllLibraryItemsWithNarrators([narratorName], req.library.id)
|
||||
|
||||
for (const libraryItem of itemsWithNarrator) {
|
||||
libraryItem.media.narrators = libraryItem.media.narrators.filter((n) => n !== narratorName)
|
||||
@@ -1211,11 +1211,11 @@ class LibraryController {
|
||||
const narratorName = libraryFilters.decode(req.params.narratorId)
|
||||
|
||||
// Update filter data
|
||||
Database.removeNarratorFromFilterData(narratorName)
|
||||
Database.removeNarratorFromFilterData(req.library.id, narratorName)
|
||||
|
||||
const itemsUpdated = []
|
||||
|
||||
const itemsWithNarrator = await libraryItemFilters.getAllLibraryItemsWithNarrators([narratorName])
|
||||
const itemsWithNarrator = await libraryItemFilters.getAllLibraryItemsWithNarrators([narratorName], req.library.id)
|
||||
|
||||
for (const libraryItem of itemsWithNarrator) {
|
||||
libraryItem.media.narrators = libraryItem.media.narrators.filter((n) => n !== narratorName)
|
||||
@@ -1432,13 +1432,21 @@ class LibraryController {
|
||||
|
||||
const itemIds = req.query.ids.split(',')
|
||||
|
||||
const libraryItems = await Database.libraryItemModel.findAll({
|
||||
attributes: ['id', 'libraryId', 'path', 'isFile'],
|
||||
where: {
|
||||
id: itemIds
|
||||
}
|
||||
const libraryItems = await Database.libraryItemModel.findAllExpandedWhere({
|
||||
id: itemIds,
|
||||
libraryId: req.library.id
|
||||
})
|
||||
|
||||
for (const libraryItem of libraryItems) {
|
||||
if (!req.user.checkCanAccessLibraryItem(libraryItem)) {
|
||||
return res.sendStatus(403)
|
||||
}
|
||||
}
|
||||
|
||||
if (libraryItems.length < itemIds.length) {
|
||||
Logger.warn(`[LibraryController] User "${req.user.username}" requested ${itemIds.length} items but only ${libraryItems.length} are in library "${req.library.id}"`)
|
||||
}
|
||||
|
||||
Logger.info(`[LibraryController] User "${req.user.username}" requested download for items "${itemIds}"`)
|
||||
|
||||
const filename = `LibraryItems-${Date.now()}.zip`
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
const { Request, Response, NextFunction } = require('express')
|
||||
const Path = require('path')
|
||||
const fs = require('../libs/fsExtra')
|
||||
const cron = require('../libs/nodeCron')
|
||||
const uaParserJs = require('../libs/uaParser')
|
||||
const Logger = require('../Logger')
|
||||
const SocketAuthority = require('../SocketAuthority')
|
||||
const Database = require('../Database')
|
||||
|
||||
const zipHelpers = require('../utils/zipHelpers')
|
||||
const { reqSupportsWebp } = require('../utils/index')
|
||||
const { reqSupportsWebp, clampPositiveInt } = require('../utils/index')
|
||||
const { ScanResult, AudioMimeType } = require('../utils/constants')
|
||||
const { getAudioMimeTypeFromExtname, encodeUriPath } = require('../utils/fileUtils')
|
||||
const LibraryItemScanner = require('../scanner/LibraryItemScanner')
|
||||
@@ -36,6 +37,24 @@ const ShareManager = require('../managers/ShareManager')
|
||||
* @typedef {RequestWithUser & RequestEntityObject & RequestLibraryFileObject} LibraryItemControllerRequestWithFile
|
||||
*/
|
||||
|
||||
/**
|
||||
* Enforce per-item access for batch item routes
|
||||
*
|
||||
* @param {RequestWithUser} req
|
||||
* @param {Response} res
|
||||
* @param {import('../models/LibraryItem')[]} libraryItems
|
||||
* @returns {boolean} true if the user may access every item; false if 403 was sent
|
||||
*/
|
||||
function ensureUserCanAccessLibraryItemsForBatch(req, res, libraryItems) {
|
||||
for (const libraryItem of libraryItems) {
|
||||
if (!req.user.checkCanAccessLibraryItem(libraryItem)) {
|
||||
res.sendStatus(403)
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
class LibraryItemController {
|
||||
constructor() {}
|
||||
|
||||
@@ -111,7 +130,7 @@ class LibraryItemController {
|
||||
}
|
||||
}
|
||||
|
||||
await this.handleDeleteLibraryItem(req.libraryItem.id, mediaItemIds)
|
||||
await this.handleDeleteLibraryItem(req.libraryItem.id, mediaItemIds, req.libraryItem.libraryId)
|
||||
if (hardDelete) {
|
||||
Logger.info(`[LibraryItemController] Deleting library item from file system at "${libraryItemPath}"`)
|
||||
await fs.remove(libraryItemPath).catch((error) => {
|
||||
@@ -202,6 +221,11 @@ class LibraryItemController {
|
||||
} else if (mediaPayload.autoDownloadSchedule !== undefined && req.libraryItem.media.autoDownloadSchedule !== mediaPayload.autoDownloadSchedule) {
|
||||
isPodcastAutoDownloadUpdated = true
|
||||
}
|
||||
|
||||
if (mediaPayload.autoDownloadSchedule && !cron.validate(mediaPayload.autoDownloadSchedule)) {
|
||||
Logger.error(`[LibraryItemController] Invalid auto download schedule cron expression "${mediaPayload.autoDownloadSchedule}" for library item "${req.libraryItem.media.title}"`)
|
||||
return res.status(400).send('Invalid auto download schedule cron expression')
|
||||
}
|
||||
}
|
||||
|
||||
let hasUpdates = (await req.libraryItem.media.updateFromRequest(mediaPayload)) || mediaPayload.url
|
||||
@@ -311,6 +335,9 @@ class LibraryItemController {
|
||||
/**
|
||||
* PATCH: /api/items/:id/cover
|
||||
*
|
||||
* This is used when updating the cover to a local image library file for the library item
|
||||
* TODO: In the next API this should be using an inode or similar instead of a file path
|
||||
*
|
||||
* @param {LibraryItemControllerRequest} req
|
||||
* @param {Response} res
|
||||
*/
|
||||
@@ -319,6 +346,7 @@ class LibraryItemController {
|
||||
return res.status(400).send('Invalid request no cover path')
|
||||
}
|
||||
|
||||
// Must be a cover file path inside the libraryFiles array
|
||||
const validationResult = await CoverManager.validateCoverPath(req.body.cover, req.libraryItem)
|
||||
if (validationResult.error) {
|
||||
return res.status(500).send(validationResult.error)
|
||||
@@ -398,8 +426,8 @@ class LibraryItemController {
|
||||
|
||||
const options = {
|
||||
format: format || (reqSupportsWebp(req) ? 'webp' : 'jpeg'),
|
||||
height: height ? parseInt(height) : null,
|
||||
width: width ? parseInt(width) : null
|
||||
height: clampPositiveInt(height ? parseInt(height) : null, 4096),
|
||||
width: clampPositiveInt(width ? parseInt(width) : null, 4096)
|
||||
}
|
||||
return CacheManager.handleCoverCache(res, libraryItemId, options)
|
||||
}
|
||||
@@ -547,7 +575,13 @@ class LibraryItemController {
|
||||
return res.sendStatus(404)
|
||||
}
|
||||
|
||||
// Ensure user has permission to delete these library items
|
||||
if (!ensureUserCanAccessLibraryItemsForBatch(req, res, itemsToDelete)) {
|
||||
return
|
||||
}
|
||||
|
||||
const libraryId = itemsToDelete[0].libraryId
|
||||
|
||||
for (const libraryItem of itemsToDelete) {
|
||||
const libraryItemPath = libraryItem.path
|
||||
Logger.info(`[LibraryItemController] (${hardDelete ? 'Hard' : 'Soft'}) deleting Library Item "${libraryItem.media.title}" with id "${libraryItem.id}"`)
|
||||
@@ -565,7 +599,7 @@ class LibraryItemController {
|
||||
authorIds.push(...libraryItem.media.authors.map((au) => au.id))
|
||||
}
|
||||
}
|
||||
await this.handleDeleteLibraryItem(libraryItem.id, mediaItemIds)
|
||||
await this.handleDeleteLibraryItem(libraryItem.id, mediaItemIds, libraryItem.libraryId)
|
||||
if (hardDelete) {
|
||||
Logger.info(`[LibraryItemController] Deleting library item from file system at "${libraryItemPath}"`)
|
||||
await fs.remove(libraryItemPath).catch((error) => {
|
||||
@@ -581,6 +615,7 @@ class LibraryItemController {
|
||||
}
|
||||
|
||||
await Database.resetLibraryIssuesFilterData(libraryId)
|
||||
|
||||
res.sendStatus(200)
|
||||
}
|
||||
|
||||
@@ -593,6 +628,11 @@ class LibraryItemController {
|
||||
* @param {Response} res
|
||||
*/
|
||||
async batchUpdate(req, res) {
|
||||
if (!req.user.canUpdate) {
|
||||
Logger.warn(`[LibraryItemController] User "${req.user.username}" attempted to batch update without permission`)
|
||||
return res.sendStatus(403)
|
||||
}
|
||||
|
||||
const updatePayloads = req.body
|
||||
if (!Array.isArray(updatePayloads) || !updatePayloads.length) {
|
||||
Logger.error(`[LibraryItemController] Batch update failed. Invalid payload`)
|
||||
@@ -615,6 +655,11 @@ class LibraryItemController {
|
||||
return res.sendStatus(404)
|
||||
}
|
||||
|
||||
// Ensure user has permission to update these library items
|
||||
if (!ensureUserCanAccessLibraryItemsForBatch(req, res, libraryItems)) {
|
||||
return
|
||||
}
|
||||
|
||||
let itemsUpdated = 0
|
||||
|
||||
const seriesIdsRemoved = []
|
||||
@@ -624,6 +669,11 @@ class LibraryItemController {
|
||||
const mediaPayload = updatePayload.mediaPayload
|
||||
const libraryItem = libraryItems.find((li) => li.id === updatePayload.id)
|
||||
|
||||
if (libraryItem.isPodcast && mediaPayload.autoDownloadSchedule && !cron.validate(mediaPayload.autoDownloadSchedule)) {
|
||||
Logger.warn(`[LibraryItemController] Invalid auto download schedule cron expression "${mediaPayload.autoDownloadSchedule}" for library item "${libraryItem.media.title}" - skipping update`)
|
||||
continue
|
||||
}
|
||||
|
||||
let hasUpdates = await libraryItem.media.updateFromRequest(mediaPayload)
|
||||
|
||||
if (libraryItem.isBook && Array.isArray(mediaPayload.metadata?.series)) {
|
||||
@@ -695,6 +745,10 @@ class LibraryItemController {
|
||||
const libraryItems = await Database.libraryItemModel.findAllExpandedWhere({
|
||||
id: libraryItemIds
|
||||
})
|
||||
// Ensure user has permission to access these library items
|
||||
if (!ensureUserCanAccessLibraryItemsForBatch(req, res, libraryItems)) {
|
||||
return
|
||||
}
|
||||
res.json({
|
||||
libraryItems: libraryItems.map((li) => li.toOldJSONExpanded())
|
||||
})
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
const { Request, Response } = require('express')
|
||||
const { Op } = require('sequelize')
|
||||
const Logger = require('../Logger')
|
||||
const SocketAuthority = require('../SocketAuthority')
|
||||
const Database = require('../Database')
|
||||
const { sort } = require('../libs/fastSort')
|
||||
const { toNumber, isNullOrNaN } = require('../utils/index')
|
||||
const { toNumber, isNullOrNaN, isUUID } = require('../utils/index')
|
||||
const userStats = require('../utils/queries/userStats')
|
||||
const parseUserAgent = require('../utils/parsers/parseUserAgent')
|
||||
|
||||
/**
|
||||
* @typedef RequestUserObject
|
||||
@@ -26,6 +28,124 @@ class MeController {
|
||||
res.json(req.user.toOldJSONForBrowser())
|
||||
}
|
||||
|
||||
/**
|
||||
* GET: /api/me/sessions
|
||||
*
|
||||
* @param {RequestWithUser} req
|
||||
* @param {Response} res
|
||||
*/
|
||||
async getSessions(req, res) {
|
||||
const page = Math.max(0, toNumber(req.query.page, 0))
|
||||
const itemsPerPage = Math.max(1, toNumber(req.query.itemsPerPage, 10))
|
||||
|
||||
if (req.user.isGuest) {
|
||||
return res.json({ sessions: [], total: 0, numPages: 0, page, itemsPerPage })
|
||||
}
|
||||
|
||||
const refreshToken = req.cookies.refresh_token || req.headers['x-refresh-token']
|
||||
const { rows, count } = await Database.sessionModel.findAndCountAll({
|
||||
where: {
|
||||
userId: req.user.id,
|
||||
expiresAt: { [Op.gt]: new Date() }
|
||||
},
|
||||
order: [['updatedAt', 'DESC']],
|
||||
limit: itemsPerPage,
|
||||
offset: itemsPerPage * page
|
||||
})
|
||||
|
||||
res.json({
|
||||
total: count,
|
||||
numPages: Math.ceil(count / itemsPerPage),
|
||||
page,
|
||||
itemsPerPage,
|
||||
sessions: rows.map((session) => ({
|
||||
id: session.id,
|
||||
ipAddress: session.ipAddress,
|
||||
userAgent: session.userAgent,
|
||||
// For display convenience
|
||||
deviceInfo: parseUserAgent(session.userAgent),
|
||||
createdAt: session.createdAt?.valueOf() ?? null,
|
||||
updatedAt: session.updatedAt?.valueOf() ?? null,
|
||||
current: !!refreshToken && (session.refreshToken === refreshToken || session.lastRefreshToken === refreshToken)
|
||||
}))
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* DELETE: /api/me/sessions/:id
|
||||
*
|
||||
* @param {RequestWithUser} req
|
||||
* @param {Response} res
|
||||
*/
|
||||
async deleteSession(req, res) {
|
||||
if (req.user.isGuest) {
|
||||
return res.sendStatus(403)
|
||||
}
|
||||
|
||||
if (!isUUID(req.params.id)) {
|
||||
return res.sendStatus(400)
|
||||
}
|
||||
|
||||
const session = await Database.sessionModel.findOne({
|
||||
where: {
|
||||
id: req.params.id,
|
||||
userId: req.user.id
|
||||
}
|
||||
})
|
||||
|
||||
if (!session) {
|
||||
return res.sendStatus(404)
|
||||
}
|
||||
|
||||
await Database.sessionModel.destroy({ where: { id: session.id } })
|
||||
Logger.info(`[MeController] User ${req.user.username} deleted auth session ${session.id}`)
|
||||
|
||||
res.sendStatus(200)
|
||||
}
|
||||
|
||||
/**
|
||||
* GET: /api/me/progress
|
||||
*
|
||||
* @param {RequestWithUser} req
|
||||
* @param {Response} res
|
||||
*/
|
||||
getAllMediaProgress(req, res) {
|
||||
const mediaProgress = req.user.mediaProgresses?.map((mp) => mp.getOldMediaProgress()) || []
|
||||
res.json({ mediaProgress })
|
||||
}
|
||||
|
||||
/**
|
||||
* GET: /api/me/bookmarks
|
||||
*
|
||||
* @param {RequestWithUser} req
|
||||
* @param {Response} res
|
||||
*/
|
||||
getAllBookmarks(req, res) {
|
||||
const bookmarks = req.user.bookmarks?.map((bookmark) => ({ ...bookmark })) || []
|
||||
res.json({ bookmarks })
|
||||
}
|
||||
|
||||
/**
|
||||
* GET: /api/me/bookmarks/:libraryItemId
|
||||
*
|
||||
* @param {RequestWithUser} req
|
||||
* @param {Response} res
|
||||
*/
|
||||
async getBookmarksForLibraryItem(req, res) {
|
||||
const libraryItem = await Database.libraryItemModel.getExpandedById(req.params.libraryItemId)
|
||||
if (!libraryItem) {
|
||||
return res.sendStatus(404)
|
||||
}
|
||||
|
||||
if (!req.user.checkCanAccessLibraryItem(libraryItem)) {
|
||||
Logger.error(`[MeController] User "${req.user.username}" attempted to access bookmarks for library item "${req.params.libraryItemId}" without access`)
|
||||
return res.sendStatus(403)
|
||||
}
|
||||
|
||||
const bookmarks = req.user.bookmarks?.filter((bookmark) => bookmark.libraryItemId === libraryItem.id).map((bookmark) => ({ ...bookmark })) || []
|
||||
res.json({ bookmarks })
|
||||
}
|
||||
|
||||
/**
|
||||
* GET: /api/me/listening-sessions
|
||||
*
|
||||
@@ -308,6 +428,8 @@ class MeController {
|
||||
* User change password. Requires current password.
|
||||
* Guest users cannot change password.
|
||||
*
|
||||
* Invalidates all other JWT sessions for the user. If using x-refresh-token, returns new tokens for the current session.
|
||||
*
|
||||
* @this import('../routers/ApiRouter')
|
||||
*
|
||||
* @param {RequestWithUser} req
|
||||
@@ -330,6 +452,24 @@ class MeController {
|
||||
return res.status(400).send(result.error)
|
||||
}
|
||||
|
||||
const shouldReturnTokens = !!req.headers['x-refresh-token']
|
||||
const newTokens = await this.auth.invalidateJwtSessionsForUser(req.user, req, res)
|
||||
|
||||
if (newTokens?.accessToken) {
|
||||
Logger.info(`[MeController] Invalidated other JWT sessions for user ${req.user.username} after password change`)
|
||||
if (shouldReturnTokens) {
|
||||
return res.json({
|
||||
success: true,
|
||||
user: {
|
||||
accessToken: newTokens.accessToken,
|
||||
refreshToken: newTokens.refreshToken
|
||||
}
|
||||
})
|
||||
}
|
||||
} else {
|
||||
Logger.info(`[MeController] Invalidated all JWT sessions for user ${req.user.username} after password change`)
|
||||
}
|
||||
|
||||
res.sendStatus(200)
|
||||
}
|
||||
|
||||
@@ -355,7 +495,7 @@ class MeController {
|
||||
for (const mediaProgress of mediaProgressesInProgress) {
|
||||
const oldMediaProgress = mediaProgress.getOldMediaProgress()
|
||||
const libraryItem = libraryItems.find((li) => li.id === oldMediaProgress.libraryItemId)
|
||||
if (libraryItem) {
|
||||
if (libraryItem && req.user.checkCanAccessLibraryItem(libraryItem)) {
|
||||
if (oldMediaProgress.episodeId && libraryItem.isPodcast) {
|
||||
const episode = libraryItem.media.podcastEpisodes.find((ep) => ep.id === oldMediaProgress.episodeId)
|
||||
if (episode) {
|
||||
|
||||
@@ -8,9 +8,11 @@ const Database = require('../Database')
|
||||
const Watcher = require('../Watcher')
|
||||
|
||||
const libraryItemFilters = require('../utils/queries/libraryItemFilters')
|
||||
const patternValidation = require('../libs/nodeCron/pattern-validation')
|
||||
const cron = require('../libs/nodeCron')
|
||||
const { isObject, getTitleIgnorePrefix } = require('../utils/index')
|
||||
const ServerSettings = require('../objects/settings/ServerSettings')
|
||||
const { sanitizeFilename } = require('../utils/fileUtils')
|
||||
const { sanitize } = require('../utils/htmlSanitizer')
|
||||
|
||||
const TaskManager = require('../managers/TaskManager')
|
||||
const adminStats = require('../utils/queries/adminStats')
|
||||
@@ -42,7 +44,7 @@ class MiscController {
|
||||
return res.sendStatus(400)
|
||||
}
|
||||
|
||||
const files = Object.values(req.files)
|
||||
const files = Object.values(req.files).flat()
|
||||
let { title, author, series, folder: folderId, library: libraryId } = req.body
|
||||
// Validate request body
|
||||
if (!libraryId || !folderId || typeof libraryId !== 'string' || typeof folderId !== 'string' || !title || typeof title !== 'string') {
|
||||
@@ -78,7 +80,12 @@ class MiscController {
|
||||
const cleanedOutputDirectoryParts = outputDirectoryParts.filter(Boolean).map((part) => sanitizeFilename(part))
|
||||
const outputDirectory = Path.join(...[folder.path, ...cleanedOutputDirectoryParts])
|
||||
|
||||
await fs.ensureDir(outputDirectory)
|
||||
try {
|
||||
await fs.ensureDir(outputDirectory)
|
||||
} catch (error) {
|
||||
Logger.error('[MiscController] Failed to create upload directory', outputDirectory, error)
|
||||
return res.sendStatus(500)
|
||||
}
|
||||
|
||||
Logger.info(`Uploading ${files.length} files to`, outputDirectory)
|
||||
|
||||
@@ -142,16 +149,23 @@ class MiscController {
|
||||
Logger.warn('Cannot disable iframe when ALLOW_IFRAME is enabled in environment')
|
||||
return res.status(400).send('Cannot disable iframe when ALLOW_IFRAME is enabled in environment')
|
||||
}
|
||||
if (settingsUpdate.allowedOrigins && !Array.isArray(settingsUpdate.allowedOrigins)) {
|
||||
const filteredUpdate = {}
|
||||
for (const key in settingsUpdate) {
|
||||
if (ServerSettings.patchableSettingsKeys.has(key)) {
|
||||
filteredUpdate[key] = settingsUpdate[key]
|
||||
}
|
||||
}
|
||||
|
||||
if (filteredUpdate.allowedOrigins && !Array.isArray(filteredUpdate.allowedOrigins)) {
|
||||
return res.status(400).send('allowedOrigins must be an array')
|
||||
}
|
||||
|
||||
const madeUpdates = Database.serverSettings.update(settingsUpdate)
|
||||
const madeUpdates = Database.serverSettings.update(filteredUpdate)
|
||||
if (madeUpdates) {
|
||||
await Database.updateServerSettings()
|
||||
|
||||
// If backup schedule is updated - update backup manager
|
||||
if (settingsUpdate.backupSchedule !== undefined) {
|
||||
if (filteredUpdate.backupSchedule !== undefined) {
|
||||
this.backupManager.updateCronSchedule()
|
||||
}
|
||||
}
|
||||
@@ -605,13 +619,11 @@ class MiscController {
|
||||
return res.sendStatus(400)
|
||||
}
|
||||
|
||||
try {
|
||||
patternValidation(expression)
|
||||
res.sendStatus(200)
|
||||
} catch (error) {
|
||||
Logger.warn(`[MiscController] Invalid cron expression ${expression}`, error.message)
|
||||
res.status(400).send(error.message)
|
||||
if (!cron.validate(expression)) {
|
||||
Logger.warn(`[MiscController] Invalid cron expression ${expression}`)
|
||||
return res.status(400).send('Invalid cron expression')
|
||||
}
|
||||
res.sendStatus(200)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -700,6 +712,7 @@ class MiscController {
|
||||
}
|
||||
let updatedValue = settingsUpdate[key]
|
||||
if (updatedValue === '' && key != 'authOpenIDSubfolderForRedirectURLs') updatedValue = null
|
||||
if (key === 'authLoginCustomMessage' && updatedValue) updatedValue = sanitize(updatedValue) || null
|
||||
let currentValue = currentAuthenticationSettings[key]
|
||||
if (currentValue === '' && key != 'authOpenIDSubfolderForRedirectURLs') currentValue = null
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ const { Request, Response, NextFunction } = require('express')
|
||||
const Logger = require('../Logger')
|
||||
const SocketAuthority = require('../SocketAuthority')
|
||||
const Database = require('../Database')
|
||||
const htmlSanitizer = require('../utils/htmlSanitizer')
|
||||
|
||||
/**
|
||||
* @typedef RequestUserObject
|
||||
@@ -29,12 +30,17 @@ class PlaylistController {
|
||||
const reqBody = req.body || {}
|
||||
|
||||
// Validation
|
||||
if (!reqBody.name || !reqBody.libraryId) {
|
||||
const nameCleaned = htmlSanitizer.stripAllTags(reqBody.name)
|
||||
if (!nameCleaned || !reqBody.libraryId) {
|
||||
return res.status(400).send('Invalid playlist data')
|
||||
}
|
||||
if (reqBody.description && typeof reqBody.description !== 'string') {
|
||||
return res.status(400).send('Invalid playlist description')
|
||||
}
|
||||
if (!req.user.checkCanAccessLibrary(reqBody.libraryId)) {
|
||||
Logger.warn(`[PlaylistController] User "${req.user.username}" attempted to create playlist in inaccessible library ${reqBody.libraryId}`)
|
||||
return res.sendStatus(403)
|
||||
}
|
||||
const items = reqBody.items || []
|
||||
const isPodcast = items.some((i) => i.episodeId)
|
||||
const libraryItemIds = new Set()
|
||||
@@ -84,7 +90,7 @@ class PlaylistController {
|
||||
{
|
||||
libraryId: reqBody.libraryId,
|
||||
userId: req.user.id,
|
||||
name: reqBody.name,
|
||||
name: nameCleaned,
|
||||
description: reqBody.description || null
|
||||
},
|
||||
{ transaction }
|
||||
@@ -131,8 +137,9 @@ class PlaylistController {
|
||||
*/
|
||||
async findAllForUser(req, res) {
|
||||
const playlistsForUser = await Database.playlistModel.getOldPlaylistsForUserAndLibrary(req.user.id)
|
||||
const accessiblePlaylists = playlistsForUser.filter((p) => req.user.checkCanAccessLibrary(p.libraryId))
|
||||
res.json({
|
||||
playlists: playlistsForUser
|
||||
playlists: accessiblePlaylists
|
||||
})
|
||||
}
|
||||
|
||||
@@ -174,7 +181,11 @@ class PlaylistController {
|
||||
}
|
||||
|
||||
const playlistUpdatePayload = {}
|
||||
if (reqBody.name) playlistUpdatePayload.name = reqBody.name
|
||||
|
||||
const nameCleaned = htmlSanitizer.stripAllTags(reqBody.name)
|
||||
if (nameCleaned) {
|
||||
playlistUpdatePayload.name = nameCleaned
|
||||
}
|
||||
if (reqBody.description) playlistUpdatePayload.description = reqBody.description
|
||||
|
||||
// Update name and description
|
||||
@@ -502,6 +513,10 @@ class PlaylistController {
|
||||
if (!collection) {
|
||||
return res.status(404).send('Collection not found')
|
||||
}
|
||||
if (!req.user.checkCanAccessLibrary(collection.libraryId)) {
|
||||
Logger.warn(`[PlaylistController] User "${req.user.username}" attempted to create playlist from collection ${collection.id} in inaccessible library ${collection.libraryId}`)
|
||||
return res.status(404).send('Collection not found')
|
||||
}
|
||||
// Expand collection to get library items
|
||||
const collectionExpanded = await collection.getOldJsonExpanded(req.user)
|
||||
if (!collectionExpanded) {
|
||||
@@ -567,6 +582,10 @@ class PlaylistController {
|
||||
Logger.warn(`[PlaylistController] Playlist ${req.params.id} requested by user ${req.user.id} that is not the owner`)
|
||||
return res.sendStatus(403)
|
||||
}
|
||||
if (!req.user.checkCanAccessLibrary(playlist.libraryId)) {
|
||||
Logger.warn(`[PlaylistController] User "${req.user.username}" attempted to access playlist ${playlist.id} in inaccessible library ${playlist.libraryId}`)
|
||||
return res.status(404).send('Playlist not found')
|
||||
}
|
||||
req.playlist = playlist
|
||||
}
|
||||
|
||||
|
||||
@@ -5,9 +5,10 @@ const SocketAuthority = require('../SocketAuthority')
|
||||
const Database = require('../Database')
|
||||
|
||||
const fs = require('../libs/fsExtra')
|
||||
const cron = require('../libs/nodeCron')
|
||||
|
||||
const { getPodcastFeed, findMatchingEpisodes } = require('../utils/podcastUtils')
|
||||
const { getFileTimestampsWithIno, filePathToPOSIX } = require('../utils/fileUtils')
|
||||
const { getFileTimestampsWithIno, filePathToPOSIX, isSameOrSubPath } = require('../utils/fileUtils')
|
||||
const { validateUrl } = require('../utils/index')
|
||||
const htmlSanitizer = require('../utils/htmlSanitizer')
|
||||
|
||||
@@ -46,6 +47,11 @@ class PodcastController {
|
||||
return res.status(400).send('Invalid request body. "media" and "media.metadata" are required')
|
||||
}
|
||||
|
||||
if (payload.media.autoDownloadSchedule && !cron.validate(payload.media.autoDownloadSchedule)) {
|
||||
Logger.error(`[PodcastController] Invalid auto download schedule cron expression "${payload.media.autoDownloadSchedule}"`)
|
||||
return res.status(400).send('Invalid auto download schedule cron expression')
|
||||
}
|
||||
|
||||
const library = await Database.libraryModel.findByIdWithFolders(payload.libraryId)
|
||||
if (!library) {
|
||||
Logger.error(`[PodcastController] Create: Library not found "${payload.libraryId}"`)
|
||||
@@ -58,8 +64,18 @@ class PodcastController {
|
||||
return res.status(404).send('Folder not found')
|
||||
}
|
||||
|
||||
if (typeof payload.path !== 'string' || !payload.path.trim()) {
|
||||
return res.status(400).send('Invalid request body. "path" must be a non-empty string')
|
||||
}
|
||||
|
||||
const libraryFolderPath = filePathToPOSIX(folder.path)
|
||||
const podcastPath = filePathToPOSIX(payload.path)
|
||||
|
||||
if (!isSameOrSubPath(libraryFolderPath, podcastPath)) {
|
||||
Logger.error(`[PodcastController] Create: Podcast path is outside library folder "${libraryFolderPath}": "${podcastPath}"`)
|
||||
return res.status(400).send('Podcast path must be inside the selected library folder')
|
||||
}
|
||||
|
||||
// Check if a library item with this podcast folder exists already
|
||||
const existingLibraryItem =
|
||||
(await Database.libraryItemModel.count({
|
||||
@@ -83,7 +99,7 @@ class PodcastController {
|
||||
|
||||
const libraryItemFolderStats = await getFileTimestampsWithIno(podcastPath)
|
||||
|
||||
let relPath = payload.path.replace(folder.fullPath, '')
|
||||
let relPath = podcastPath.replace(libraryFolderPath, '')
|
||||
if (relPath.startsWith('/')) relPath = relPath.slice(1)
|
||||
|
||||
let newLibraryItem = null
|
||||
@@ -412,9 +428,26 @@ class PodcastController {
|
||||
Logger.debug(`[PodcastController] Sanitized description from "${req.body[key]}" to "${sanitizedDescription}"`)
|
||||
req.body[key] = sanitizedDescription
|
||||
}
|
||||
} else if (key === 'subtitle' && req.body[key]) {
|
||||
const sanitizedSubtitle = htmlSanitizer.sanitize(req.body[key])
|
||||
if (sanitizedSubtitle !== req.body[key]) {
|
||||
Logger.debug(`[PodcastController] Sanitized subtitle from "${req.body[key]}" to "${sanitizedSubtitle}"`)
|
||||
req.body[key] = sanitizedSubtitle
|
||||
}
|
||||
}
|
||||
|
||||
updatePayload[key] = req.body[key]
|
||||
} else if (key === 'enclosure') {
|
||||
const enclosure = req.body.enclosure
|
||||
if (enclosure === null) {
|
||||
updatePayload.enclosureURL = null
|
||||
updatePayload.enclosureSize = null
|
||||
updatePayload.enclosureType = null
|
||||
} else if (typeof enclosure === 'object' && typeof enclosure.url === 'string') {
|
||||
updatePayload.enclosureURL = enclosure.url
|
||||
updatePayload.enclosureType = typeof enclosure.type === 'string' ? enclosure.type : null
|
||||
updatePayload.enclosureSize = enclosure.length !== undefined && enclosure.length !== null ? enclosure.length : null
|
||||
}
|
||||
} else if (key === 'chapters' && Array.isArray(req.body[key]) && req.body[key].every((ch) => typeof ch === 'object' && ch.title && ch.start)) {
|
||||
updatePayload[key] = req.body[key]
|
||||
} else if (key === 'publishedAt' && typeof req.body[key] === 'number') {
|
||||
|
||||
@@ -57,7 +57,7 @@ class RSSFeedController {
|
||||
}
|
||||
|
||||
// Check item has audio tracks
|
||||
if (!itemExpanded.hasAudioTracks()) {
|
||||
if (!itemExpanded.hasAudioTracks) {
|
||||
Logger.error(`[RSSFeedController] Cannot open RSS feed for item "${itemExpanded.media.title}" because it has no audio tracks`)
|
||||
return res.status(400).send('Item has no audio tracks')
|
||||
}
|
||||
|
||||
@@ -361,6 +361,11 @@ class SessionController {
|
||||
return res.sendStatus(404)
|
||||
}
|
||||
|
||||
if (playbackSession.userId !== req.user.id && !req.user.isAdminOrUp) {
|
||||
Logger.error(`[SessionController] Non-admin user "${req.user.username}" attempted to access session belonging to another user "${req.params.id}"`)
|
||||
return res.sendStatus(403)
|
||||
}
|
||||
|
||||
if (req.method == 'DELETE' && !req.user.canDelete) {
|
||||
Logger.warn(`[SessionController] User "${req.user.username}" attempted to delete without permission`)
|
||||
return res.sendStatus(403)
|
||||
|
||||
@@ -53,6 +53,10 @@ class ShareController {
|
||||
if (playbackSession) {
|
||||
if (mediaItemShare.id === playbackSession.mediaItemShareId) {
|
||||
Logger.debug(`[ShareController] Found share playback session ${req.cookies.share_session_id}`)
|
||||
// If ?t was provided, override the cached currentTime
|
||||
if (startTime > 0 && startTime < playbackSession.duration) {
|
||||
playbackSession.currentTime = startTime
|
||||
}
|
||||
mediaItemShare.playbackSession = playbackSession.toJSONForClient()
|
||||
return res.json(mediaItemShare)
|
||||
} else {
|
||||
@@ -142,7 +146,7 @@ class ShareController {
|
||||
|
||||
const mediaItemShare = ShareManager.findBySlug(slug)
|
||||
if (!mediaItemShare) {
|
||||
return res.status(404)
|
||||
return res.sendStatus(404)
|
||||
}
|
||||
|
||||
const playbackSession = ShareManager.findPlaybackSessionBySessionId(req.cookies.share_session_id)
|
||||
@@ -182,7 +186,7 @@ class ShareController {
|
||||
|
||||
const mediaItemShare = ShareManager.findBySlug(slug)
|
||||
if (!mediaItemShare) {
|
||||
return res.status(404)
|
||||
return res.sendStatus(404)
|
||||
}
|
||||
|
||||
const playbackSession = ShareManager.findPlaybackSessionBySessionId(req.cookies.share_session_id)
|
||||
@@ -227,7 +231,7 @@ class ShareController {
|
||||
const { slug } = req.params
|
||||
const mediaItemShare = ShareManager.findBySlug(slug)
|
||||
if (!mediaItemShare) {
|
||||
return res.status(404)
|
||||
return res.sendStatus(404)
|
||||
}
|
||||
if (!mediaItemShare.isDownloadable) {
|
||||
return res.status(403).send('Download is not allowed for this item')
|
||||
@@ -291,7 +295,7 @@ class ShareController {
|
||||
|
||||
const mediaItemShare = ShareManager.findBySlug(slug)
|
||||
if (!mediaItemShare) {
|
||||
return res.status(404)
|
||||
return res.sendStatus(404)
|
||||
}
|
||||
|
||||
const playbackSession = ShareManager.findPlaybackSessionBySessionId(req.cookies.share_session_id)
|
||||
|
||||
Loaded 100 of 176 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user