mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2026-11-13 12:23:34 +01:00
Compare commits
120
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7d10b8fc4a | ||
|
|
14bec248f1 | ||
|
|
ff57368f37 | ||
|
|
f589397008 | ||
|
|
fa4f6b58f3 | ||
|
|
1acfd2ae54 | ||
|
|
91717e3b96 | ||
|
|
282d8b8bf6 | ||
|
|
fb4c3ba984 | ||
|
|
72a1c656f9 | ||
|
|
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 | ||
|
|
1b46d68027 | ||
|
|
d511612f21 | ||
|
|
6caa1aa2da | ||
|
|
32fae4a7c6 | ||
|
|
96d4021a3c | ||
|
|
49826f5163 | ||
|
|
76e33a2a4e | ||
|
|
1a440811c5 | ||
|
|
529a16b718 | ||
|
|
032f0b6b51 | ||
|
|
f5bfa1edd6 | ||
|
|
4dfad46722 | ||
|
|
42947e2e64 | ||
|
|
c4e7172c2f | ||
|
|
4502d37d4f | ||
|
|
dfce15a74c |
No files matched your search
@@ -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
|
||||
|
||||
@@ -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"]
|
||||
+4
-3
@@ -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 "--------------------"
|
||||
@@ -48,7 +49,7 @@ Description: $DESCRIPTION"
|
||||
echo "$controlfile" > dist/debian/DEBIAN/control;
|
||||
|
||||
# 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
|
||||
|
||||
|
||||
@@ -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.35.1",
|
||||
"version": "2.37.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "audiobookshelf-client",
|
||||
"version": "2.35.1",
|
||||
"version": "2.37.0",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@nuxtjs/axios": "^5.13.6",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "audiobookshelf-client",
|
||||
"version": "2.35.1",
|
||||
"version": "2.37.0",
|
||||
"buildNumber": 1,
|
||||
"description": "Self-hosted audiobook and podcast client",
|
||||
"main": "index.js",
|
||||
|
||||
@@ -46,6 +46,7 @@
|
||||
"ButtonLatest": "Апошняе",
|
||||
"ButtonLibrary": "Бібліятэка",
|
||||
"ButtonLogout": "Выйсці",
|
||||
"ButtonLogoutAllDevices": "Выйсці з усіх прылад",
|
||||
"ButtonLookup": "Пошук",
|
||||
"ButtonManageTracks": "Кіраванне трэкамі",
|
||||
"ButtonMapChapterTitles": "Супаставіць загалоўкі раздзелаў",
|
||||
@@ -194,6 +195,7 @@
|
||||
"HeaderScheduleEpisodeDownloads": "Расклад аўтаматычных спампоўванняў выпускаў",
|
||||
"HeaderScheduleLibraryScans": "Расклад аўтаматычнага сканавання бібліятэкі",
|
||||
"HeaderSession": "Сеанс",
|
||||
"HeaderSessions": "Сеансы",
|
||||
"HeaderSetBackupSchedule": "Наладзіць расклад рэзервовага капіравання",
|
||||
"HeaderSettings": "Налады",
|
||||
"HeaderSettingsDisplay": "Выгляд",
|
||||
@@ -415,6 +417,7 @@
|
||||
"LabelIntervalEveryHour": "Кожную гадзіну",
|
||||
"LabelIntervalEveryMinute": "Кожную хвіліну",
|
||||
"LabelInvert": "Інвертаваць",
|
||||
"LabelIpAddress": "IP-адрас",
|
||||
"LabelItem": "Элемент",
|
||||
"LabelJumpBackwardAmount": "Час пераходу назад",
|
||||
"LabelJumpForwardAmount": "Час пераходу наперад",
|
||||
@@ -774,6 +777,7 @@
|
||||
"MessageConfirmDeleteSession": "Вы ўпэўнены, што хочаце выдаліць гэты сеанс?",
|
||||
"MessageConfirmEmbedMetadataInAudioFiles": "Вы ўпэўнены, што хочаце ўбудаваць метаданыя ў {0} аўдыяфайлаў?",
|
||||
"MessageConfirmForceReScan": "Вы ўпэўнены, што хочаце прымусова паўторна сканіраваць?",
|
||||
"MessageConfirmLogoutDevice": "Вы ўпэўнены, што хочаце выйсці з \"{0}\"?",
|
||||
"MessageConfirmMarkAllEpisodesFinished": "Вы ўпэўнены, што хочаце пазначыць усе выпускі як завершаныя?",
|
||||
"MessageConfirmMarkAllEpisodesNotFinished": "Вы ўпэўнены, што хочаце пазначыць усе выпускі як незавершаныя?",
|
||||
"MessageConfirmMarkItemFinished": "Вы ўпэўнены, што хочаце пазначыць \"{0}\" як завершаны?",
|
||||
|
||||
+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",
|
||||
|
||||
@@ -62,7 +62,7 @@
|
||||
"ButtonPlay": "Afspil",
|
||||
"ButtonPlayAll": "Afspil alle",
|
||||
"ButtonPlaying": "Afspiller",
|
||||
"ButtonPlaylists": "Afspilningslister",
|
||||
"ButtonPlaylists": "Playlister",
|
||||
"ButtonPrevious": "Forrige",
|
||||
"ButtonPreviousChapter": "Forrige kapitel",
|
||||
"ButtonProbeAudioFile": "Undersøg lydfil",
|
||||
@@ -179,8 +179,8 @@
|
||||
"HeaderPermissions": "Tilladelser",
|
||||
"HeaderPlayerQueue": "Afspilningskø",
|
||||
"HeaderPlayerSettings": "Afspilningsindstillinger",
|
||||
"HeaderPlaylist": "Afspilningsliste",
|
||||
"HeaderPlaylistItems": "Elementer i afspilningslisten",
|
||||
"HeaderPlaylist": "Playliste",
|
||||
"HeaderPlaylistItems": "Elementer i playlisten",
|
||||
"HeaderPodcastsToAdd": "Podcasts som skal tilføjes",
|
||||
"HeaderPresets": "Forudindstillinger",
|
||||
"HeaderPreviewCover": "Forhåndsvis omslag",
|
||||
@@ -231,7 +231,7 @@
|
||||
"LabelActivity": "Aktivitet",
|
||||
"LabelAddToCollection": "Tilføj til samling",
|
||||
"LabelAddToCollectionBatch": "Tilføj {0} bøger til samling",
|
||||
"LabelAddToPlaylist": "Tilføj til afspilningsliste",
|
||||
"LabelAddToPlaylist": "Tilføj til playliste",
|
||||
"LabelAddToPlaylistBatch": "Tilføj {0} elementer til afspilningsliste",
|
||||
"LabelAddedAt": "Tilføjet",
|
||||
"LabelAddedDate": "Tilføjede {0}",
|
||||
|
||||
@@ -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",
|
||||
@@ -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",
|
||||
@@ -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",
|
||||
@@ -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?",
|
||||
|
||||
+16
-10
@@ -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",
|
||||
@@ -247,13 +248,13 @@
|
||||
"LabelDownloadNEpisodes": "Λήψη {0} επεισοδίων",
|
||||
"LabelDuration": "Διάρκεια",
|
||||
"LabelDurationComparisonExactMatch": "(ακριβής ταύτιση)",
|
||||
"LabelEbook": "Ηλεκτρονικό Βιβλίο/ Ebook",
|
||||
"LabelEbooks": "Ηλεκτρονικά Βιβλία / Ebooks",
|
||||
"LabelEbook": "Ηλεκτρονικό βιβλίο",
|
||||
"LabelEbooks": "Ηλεκτρονικά βιβλία",
|
||||
"LabelEdit": "Επεξεργασία",
|
||||
"LabelEmail": "Ηλεκτρονικό Ταχυδρομίο",
|
||||
"LabelEmailSettingsFromAddress": "Από Διεύθυνση",
|
||||
"LabelEmail": "Ηλεκτρονικό ταχυδρομείο",
|
||||
"LabelEmailSettingsFromAddress": "Από διεύθυνση",
|
||||
"LabelEmailSettingsSecure": "Ασφαλές",
|
||||
"LabelEmailSettingsTestAddress": "Δοκιμή Διεύθυνσης",
|
||||
"LabelEmailSettingsTestAddress": "Δοκιμή διεύθυνσης",
|
||||
"LabelEmbeddedCover": "Ενσωματωμένο Εξώφυλλο",
|
||||
"LabelEnable": "Ενεργοποίηση",
|
||||
"LabelEnd": "Τέλος",
|
||||
@@ -272,7 +273,7 @@
|
||||
"LabelFontScale": "Μέγεθος γραμματοσειράς",
|
||||
"LabelGenre": "Λογοτεχνικό Είδος",
|
||||
"LabelGenres": "Λογοτεχνικά Είδη",
|
||||
"LabelHasEbook": "Έχει ebook",
|
||||
"LabelHasEbook": "Έχει βιβλίο",
|
||||
"LabelHasSupplementaryEbook": "Διαθέτει συμπληρωματικό ηλεκτρονικό βιβλίο",
|
||||
"LabelHost": "Διακομιστής",
|
||||
"LabelInProgress": "Σε Εξέλιξη",
|
||||
@@ -299,6 +300,8 @@
|
||||
"LabelNumberOfEpisodes": "# Επεισοδίων",
|
||||
"LabelPassword": "Κωδικός Πρόσβασης",
|
||||
"LabelPath": "Διαδρομή",
|
||||
"LabelPodcast": "Πόντκαστ",
|
||||
"LabelPodcasts": "Πόντκαστ",
|
||||
"LabelProgress": "Πρόοδος",
|
||||
"LabelPubDate": "Ημερομηνία έκδοσης",
|
||||
"LabelPublishYear": "Χρονολογία Έκδοσης",
|
||||
@@ -335,8 +338,11 @@
|
||||
"LabelUsername": "Όνομα Χρήστη",
|
||||
"LabelYourProgress": "Η Πρόοδος Σας",
|
||||
"MessageDownloadingEpisode": "Λήψη επεισοδίου",
|
||||
"MessageEreaderDevices": "Για να διασφαλίσετε την παράδοση των ηλεκτρονικών βιβλίων, ίσως χρειαστεί να προσθέσετε την παραπάνω διεύθυνση ηλεκτρονικού ταχυδρομείου ως έγκυρο αποστολέα για κάθε συσκευή που αναγράφεται παρακάτω.",
|
||||
"MessageLoading": "Φόρτωση...",
|
||||
"MessageMarkAsFinished": "Σήμανση ως Ολοκληρωμένο",
|
||||
"MessageNoItemsFound": "Δεν βρέθηκαν αντικείμενα",
|
||||
"MessageNoUserPlaylists": "Δεν έχετε λίστες αναπαραγωγής"
|
||||
"MessageNoUserPlaylists": "Δεν έχετε λίστες αναπαραγωγής",
|
||||
"ToastEmailSettingsUpdateSuccess": "Ενημερώθηκαν οι ρυθμίσεις ηλεκτρονικού ταχυδρομείου",
|
||||
"ToastNameEmailRequired": "Το όνομα και η διεύθυνση ηλεκτρονικού ταχυδρομείου είναι υποχρεωτικά"
|
||||
}
|
||||
@@ -46,6 +46,7 @@
|
||||
"ButtonLatest": "Más recientes",
|
||||
"ButtonLibrary": "Biblioteca",
|
||||
"ButtonLogout": "Cerrar Sesión",
|
||||
"ButtonLogoutAllDevices": "Cerrar sesión en todos los dispositivos",
|
||||
"ButtonLookup": "Averiguar",
|
||||
"ButtonManageTracks": "Gestionar pistas",
|
||||
"ButtonMapChapterTitles": "Asignar Títulos a Capítulos",
|
||||
@@ -194,6 +195,7 @@
|
||||
"HeaderScheduleEpisodeDownloads": "Planificador de auto‐descargas de episodios",
|
||||
"HeaderScheduleLibraryScans": "Planificar Auto‐Escaneo de Biblioteca",
|
||||
"HeaderSession": "Sesión",
|
||||
"HeaderSessions": "Sesiones",
|
||||
"HeaderSetBackupSchedule": "Establecer Planificación de Respaldo",
|
||||
"HeaderSettings": "Ajustes",
|
||||
"HeaderSettingsDisplay": "Interfaz",
|
||||
@@ -415,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",
|
||||
@@ -774,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?",
|
||||
|
||||
@@ -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",
|
||||
@@ -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é ?",
|
||||
|
||||
@@ -195,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",
|
||||
@@ -416,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",
|
||||
@@ -775,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?",
|
||||
|
||||
@@ -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?",
|
||||
|
||||
@@ -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",
|
||||
@@ -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",
|
||||
@@ -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",
|
||||
@@ -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?",
|
||||
@@ -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",
|
||||
|
||||
@@ -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,7 +232,7 @@
|
||||
"LabelCollections": "Kolekcijos",
|
||||
"LabelComplete": "Baigta",
|
||||
"LabelConfirmPassword": "Patvirtinkite slaptažodį",
|
||||
"LabelContinueListening": "Tęsti klausymąsi",
|
||||
"LabelContinueListening": "Klausytis toliau",
|
||||
"LabelContinueReading": "Tęsti skaitymą",
|
||||
"LabelContinueSeries": "Tęsti seriją",
|
||||
"LabelCover": "Viršelis",
|
||||
@@ -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ė",
|
||||
@@ -392,8 +395,8 @@
|
||||
"LabelRead": "Skaityta",
|
||||
"LabelReadAgain": "Skaityti dar kartą",
|
||||
"LabelReadEbookWithoutProgress": "Skaityti e-knygą be pažangos saugojimo",
|
||||
"LabelRecentSeries": "Naujausios serijos",
|
||||
"LabelRecentlyAdded": "Neseniai pridėta",
|
||||
"LabelRecentSeries": "Naujausiais įkeltos serijos",
|
||||
"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",
|
||||
|
||||
@@ -46,6 +46,7 @@
|
||||
"ButtonLatest": "Meest recent",
|
||||
"ButtonLibrary": "Bibliotheek",
|
||||
"ButtonLogout": "Uitloggen",
|
||||
"ButtonLogoutAllDevices": "Uitloggen op alle apparaten",
|
||||
"ButtonLookup": "Zoeken",
|
||||
"ButtonManageTracks": "Tracks beheren",
|
||||
"ButtonMapChapterTitles": "Hoofdstuktitels mappen",
|
||||
@@ -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",
|
||||
@@ -415,6 +417,7 @@
|
||||
"LabelIntervalEveryHour": "Ieder uur",
|
||||
"LabelIntervalEveryMinute": "Elke minuut",
|
||||
"LabelInvert": "Omdraaien",
|
||||
"LabelIpAddress": "IP-adres",
|
||||
"LabelItem": "Onderdeel",
|
||||
"LabelJumpBackwardAmount": "Terugspoelen hoeveelheid",
|
||||
"LabelJumpForwardAmount": "Vooruitspoelen hoeveelheid",
|
||||
@@ -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?",
|
||||
|
||||
@@ -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?",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
@@ -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?",
|
||||
|
||||
@@ -46,6 +46,7 @@
|
||||
"ButtonLatest": "Последнее",
|
||||
"ButtonLibrary": "Библиотека",
|
||||
"ButtonLogout": "Выход",
|
||||
"ButtonLogoutAllDevices": "Выйти на всех устройствах",
|
||||
"ButtonLookup": "Найти",
|
||||
"ButtonManageTracks": "Управление треками",
|
||||
"ButtonMapChapterTitles": "Найти названия глав",
|
||||
@@ -194,6 +195,7 @@
|
||||
"HeaderScheduleEpisodeDownloads": "Запланировать автоматическое скачивание выпусков",
|
||||
"HeaderScheduleLibraryScans": "Планировщик автоматического сканирования библиотеки",
|
||||
"HeaderSession": "Сеансы",
|
||||
"HeaderSessions": "Сессии",
|
||||
"HeaderSetBackupSchedule": "Установить планировщик бэкапов",
|
||||
"HeaderSettings": "Настройки",
|
||||
"HeaderSettingsDisplay": "Дисплей",
|
||||
@@ -415,6 +417,7 @@
|
||||
"LabelIntervalEveryHour": "Каждый час",
|
||||
"LabelIntervalEveryMinute": "Каждую минуту",
|
||||
"LabelInvert": "Инвертировать",
|
||||
"LabelIpAddress": "IP-адрес",
|
||||
"LabelItem": "Элемент",
|
||||
"LabelJumpBackwardAmount": "Прыжок назад на величину",
|
||||
"LabelJumpForwardAmount": "Прыжок вперед на величину",
|
||||
@@ -774,6 +777,7 @@
|
||||
"MessageConfirmDeleteSession": "Вы уверены, что хотите удалить этот сеанс?",
|
||||
"MessageConfirmEmbedMetadataInAudioFiles": "Вы уверены, что хотите вставить метаданные в {0} аудиофайлов?",
|
||||
"MessageConfirmForceReScan": "Вы уверены, что хотите принудительно выполнить повторное сканирование?",
|
||||
"MessageConfirmLogoutDevice": "Вы уверены, что хотите выйти из «{0}»?",
|
||||
"MessageConfirmMarkAllEpisodesFinished": "Вы уверены, что хотите отметить все выпуски как прослушанные?",
|
||||
"MessageConfirmMarkAllEpisodesNotFinished": "Вы уверены, что хотите отметить все выпуски как непрослушанные?",
|
||||
"MessageConfirmMarkItemFinished": "Вы уверены, что хотите отметить «{0}» как завершенную?",
|
||||
|
||||
@@ -46,6 +46,7 @@
|
||||
"ButtonLatest": "Senaste",
|
||||
"ButtonLibrary": "Bibliotek",
|
||||
"ButtonLogout": "Logga ut",
|
||||
"ButtonLogoutAllDevices": "Logga ut från alla enheter",
|
||||
"ButtonLookup": "Sök",
|
||||
"ButtonManageTracks": "Hantera spår",
|
||||
"ButtonMapChapterTitles": "Mappa kapitelrubriker",
|
||||
@@ -194,6 +195,7 @@
|
||||
"HeaderScheduleEpisodeDownloads": "Schemalägg automatisk hämtning av avsnitt",
|
||||
"HeaderScheduleLibraryScans": "Schemalägg automatiska biblioteksskanningar",
|
||||
"HeaderSession": "Lyssningstillfälle",
|
||||
"HeaderSessions": "Sessioner",
|
||||
"HeaderSetBackupSchedule": "Schemalägg säkerhetskopiering",
|
||||
"HeaderSettings": "Inställningar",
|
||||
"HeaderSettingsDisplay": "Visning",
|
||||
@@ -415,6 +417,7 @@
|
||||
"LabelIntervalEveryHour": "Varje timme",
|
||||
"LabelIntervalEveryMinute": "Varje minut",
|
||||
"LabelInvert": "Invertera",
|
||||
"LabelIpAddress": "IP-adress",
|
||||
"LabelItem": "Objekt",
|
||||
"LabelJumpBackwardAmount": "Inställning för \"hopp bakåt\"",
|
||||
"LabelJumpForwardAmount": "Inställning för \"hopp framåt\"",
|
||||
@@ -449,7 +452,7 @@
|
||||
"LabelLowestPriority": "Lägst prioritet",
|
||||
"LabelMatchConfidence": "Matchningsgrad",
|
||||
"LabelMatchExistingUsersBy": "Matcha befintliga användare med",
|
||||
"LabelMatchExistingUsersByDescription": "Används för att ansluta befintlig användare. När anslutningen sker kommer användaren att matchas med ett unikt ID från SSO-leverantören.",
|
||||
"LabelMatchExistingUsersByDescription": "Används för att ansluta befintliga användare. När anslutningen har gjorts matchas användarna med ett unikt ID från SSO-leverantören.",
|
||||
"LabelMaxEpisodesToDownload": "Maximalt antal avsnitt att hämta (0 = obegränsat).",
|
||||
"LabelMaxEpisodesToDownloadPerCheck": "Maximalt antal nya avsnitt att hämta per tillfälle",
|
||||
"LabelMaxEpisodesToKeep": "Maximalt antal avsnitt att behålla",
|
||||
@@ -590,7 +593,7 @@
|
||||
"LabelSettingsDateFormat": "Datumformat",
|
||||
"LabelSettingsEnableWatcher": "Bevaka biblioteken automatiskt efter ändringar",
|
||||
"LabelSettingsEnableWatcherForLibrary": "Bevaka biblioteket automatiskt efter ändringar",
|
||||
"LabelSettingsEnableWatcherHelp": "Upptäcker automatiskt när objekt<br>läggs till, uppdateras eller tas bort.<br>OBS: Servern måste startas om.",
|
||||
"LabelSettingsEnableWatcherHelp": "Objekt läggs till eller uppdateras automatiskt när filändringar upptäcks.<br>OBS: Servern måste startas om.",
|
||||
"LabelSettingsEpubsAllowScriptedContent": "Tillåt skriptat innehåll i EPUB-filer",
|
||||
"LabelSettingsEpubsAllowScriptedContentHelp": "Tillåt skript i EPUB-filer.<br>Alternativet bör vara inaktiverat<br>om du inte litar på filernas källa.",
|
||||
"LabelSettingsExperimentalFeatures": "Experimentella funktioner",
|
||||
@@ -774,6 +777,7 @@
|
||||
"MessageConfirmDeleteSession": "Är du säker på att du vill ta bort detta lyssningstillfälle?",
|
||||
"MessageConfirmEmbedMetadataInAudioFiles": "Är du säker på att du vill infoga metadata i {0} ljudfiler?",
|
||||
"MessageConfirmForceReScan": "Är du säker på att du vill starta en ny skanning?",
|
||||
"MessageConfirmLogoutDevice": "Är du säker på att du vill logga ut från ”{0}”?",
|
||||
"MessageConfirmMarkAllEpisodesFinished": "Är du säker på att du vill markera alla avsnitt som avslutade?",
|
||||
"MessageConfirmMarkAllEpisodesNotFinished": "Vill du markera alla avsnitt som inte avslutade?",
|
||||
"MessageConfirmMarkItemFinished": "Är du säker på att du vill markera \"{0}\" som avslutad?",
|
||||
@@ -936,7 +940,7 @@
|
||||
"MessageTaskTargetDirectoryNotWritable": "Det går inte att skriva till målmappen",
|
||||
"MessageThinking": "Tänker…",
|
||||
"MessageUploaderItemFailed": "Det gick inte att ladda upp",
|
||||
"MessageUploaderItemSuccess": "har laddats upp",
|
||||
"MessageUploaderItemSuccess": "Uppladdningen lyckades",
|
||||
"MessageUploading": "Laddar upp…",
|
||||
"MessageValidCronExpression": "Giltigt cron-uttryck",
|
||||
"MessageWatcherIsDisabledGlobally": "Bevakningen är inaktiverad i serverinställningarna",
|
||||
|
||||
@@ -195,6 +195,7 @@
|
||||
"HeaderScheduleEpisodeDownloads": "设置自动下载剧集",
|
||||
"HeaderScheduleLibraryScans": "自动扫描媒体库",
|
||||
"HeaderSession": "会话",
|
||||
"HeaderSessions": "会话",
|
||||
"HeaderSetBackupSchedule": "设置备份计划任务",
|
||||
"HeaderSettings": "设置",
|
||||
"HeaderSettingsDisplay": "显示",
|
||||
@@ -416,6 +417,7 @@
|
||||
"LabelIntervalEveryHour": "每小时",
|
||||
"LabelIntervalEveryMinute": "每分钟",
|
||||
"LabelInvert": "倒转",
|
||||
"LabelIpAddress": "IP 地址",
|
||||
"LabelItem": "项目",
|
||||
"LabelJumpBackwardAmount": "向后跳转时长",
|
||||
"LabelJumpForwardAmount": "向前跳转时长",
|
||||
@@ -710,7 +712,7 @@
|
||||
"LabelUploaderDragAndDrop": "拖放文件或文件夹",
|
||||
"LabelUploaderDragAndDropFilesOnly": "拖放文件",
|
||||
"LabelUploaderDropFiles": "拖放文件",
|
||||
"LabelUploaderItemFetchMetadataHelp": "自动获取标题,作者和系列",
|
||||
"LabelUploaderItemFetchMetadataHelp": "自动获取标题、作者和系列",
|
||||
"LabelUseAdvancedOptions": "使用高级选项",
|
||||
"LabelUseChapterTrack": "使用章节音轨",
|
||||
"LabelUseFullTrack": "使用完整音轨",
|
||||
@@ -741,7 +743,7 @@
|
||||
"MessageAuthenticationLegacyTokenWarning": "旧版 API 令牌将来会被移除。请改用 <a href=\"/config/api-keys\">API 密钥</a>。",
|
||||
"MessageAuthenticationOIDCChangesRestart": "保存后重新启动服务器以应用 OIDC 更改。",
|
||||
"MessageAuthenticationSecurityMessage": "身份验证安全性已增强,所有用户都需要重新登录。",
|
||||
"MessageBackupsDescription": "备份包括用户,用户进度,媒体库项目详细信息,服务器设置和图像,存储在 <code>/metadata/items</code> & <code>/metadata/authors</code>。备份不包括存储在你的媒体库文件夹中的任何文件。",
|
||||
"MessageBackupsDescription": "备份包括用户、用户进度、媒体库项目详细信息、服务器设置和图像,存储在 <code>/metadata/items</code> & <code>/metadata/authors</code>。备份不包括存储在你的媒体库文件夹中的任何文件。",
|
||||
"MessageBackupsLocationEditNote": "注意:更新备份位置不会移动或修改现有备份",
|
||||
"MessageBackupsLocationNoEditNote": "注意:备份位置是通过环境变量设置的,不能在此处更改。",
|
||||
"MessageBackupsLocationPathEmpty": "备份位置路径不能为空",
|
||||
@@ -775,6 +777,7 @@
|
||||
"MessageConfirmDeleteSession": "你确定要删除此会话吗?",
|
||||
"MessageConfirmEmbedMetadataInAudioFiles": "你确定要将元数据嵌入到 {0} 个音频文件中吗?",
|
||||
"MessageConfirmForceReScan": "你确定要强制重新扫描吗?",
|
||||
"MessageConfirmLogoutDevice": "确定要注销 \"{0}\" 吗?",
|
||||
"MessageConfirmMarkAllEpisodesFinished": "你确定要将所有剧集都标记为已完成吗?",
|
||||
"MessageConfirmMarkAllEpisodesNotFinished": "你确定要将所有剧集都标记为未完成吗?",
|
||||
"MessageConfirmMarkItemFinished": "你确定要将 “{0}” 标记为已完成吗?",
|
||||
|
||||
@@ -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']) {
|
||||
@@ -32,9 +32,7 @@ if (isDev || options['prod-with-dev-env']) {
|
||||
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.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.35.1",
|
||||
"version": "2.37.0",
|
||||
"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
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,8 @@
|
||||
</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.
|
||||
@@ -73,7 +75,7 @@ See [library docs](https://audiobookshelf.org/docs/category/libraries) for suppo
|
||||
|
||||
# Installation
|
||||
|
||||
See [install docs](https://www.audiobookshelf.org/docs)
|
||||
See [install docs](https://audiobookshelf.org/docs/category/installation)
|
||||
|
||||
<br />
|
||||
|
||||
@@ -93,8 +95,7 @@ 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>
|
||||
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 />
|
||||
|
||||
@@ -164,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`.
|
||||
|
||||
@@ -182,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.
|
||||
|
||||
@@ -191,4 +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.
|
||||
|
||||
+8
-15
@@ -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
|
||||
@@ -514,19 +509,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) {
|
||||
|
||||
+37
-1
@@ -46,6 +46,18 @@ const passport = require('passport')
|
||||
const expressSession = require('express-session')
|
||||
const MemoryStore = require('./libs/memorystore')
|
||||
|
||||
/**
|
||||
* `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
|
||||
@@ -158,6 +170,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()
|
||||
|
||||
@@ -409,7 +427,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/'
|
||||
|
||||
@@ -269,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' })
|
||||
|
||||
@@ -87,16 +87,14 @@ class TokenManager {
|
||||
/**
|
||||
* 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 {
|
||||
const decoded = jwt.verify(token, TokenManager.TokenSecret)
|
||||
if (!TokenManager.isBearerAccessTokenPayload(decoded)) return null
|
||||
return decoded
|
||||
// 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
|
||||
}
|
||||
|
||||
@@ -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`)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -335,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
|
||||
*/
|
||||
@@ -343,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)
|
||||
|
||||
@@ -10,7 +10,9 @@ const Watcher = require('../Watcher')
|
||||
const libraryItemFilters = require('../utils/queries/libraryItemFilters')
|
||||
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')
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -698,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
|
||||
|
||||
|
||||
@@ -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')
|
||||
}
|
||||
|
||||
@@ -146,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)
|
||||
@@ -186,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)
|
||||
@@ -231,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')
|
||||
@@ -295,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)
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@ const { ObjectDefineProperties, ObjectDefineProperty, ObjectSetPrototypeOf } = r
|
||||
|
||||
const stream = require('../../stream')
|
||||
|
||||
const { getDefaultEncoding } = require('../crypto/util')
|
||||
const getDefaultEncoding = () => 'buffer'
|
||||
|
||||
module.exports = LazyTransform
|
||||
|
||||
|
||||
@@ -15,20 +15,23 @@ module.exports = (function () {
|
||||
proc.stdout.setEncoding('utf8')
|
||||
proc.stderr.setEncoding('utf8')
|
||||
|
||||
proc.stdout.on('data', function (data) { probeData.push(data) })
|
||||
proc.stderr.on('data', function (data) { errData.push(data) })
|
||||
proc.stdout.on('data', function (data) {
|
||||
probeData.push(data)
|
||||
})
|
||||
proc.stderr.on('data', function (data) {
|
||||
errData.push(data)
|
||||
})
|
||||
|
||||
proc.on('exit', code => { exitCode = code })
|
||||
proc.on('error', err => reject(err))
|
||||
proc.on('error', (err) => reject(err))
|
||||
proc.on('close', () => {
|
||||
try {
|
||||
resolve(JSON.parse(probeData.join('')))
|
||||
resolve(JSON.parse(probeData.join('')))
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
reject(err)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
return doProbe
|
||||
})()
|
||||
})()
|
||||
@@ -5,8 +5,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.WatcherStats = exports.Stats = void 0;
|
||||
const ripstat_1 = require("ripstat");
|
||||
const ripstat_1 = require("./ripstat");
|
||||
Object.defineProperty(exports, "Stats", { enumerable: true, get: function () { return ripstat_1.Stats; } });
|
||||
const watcher_stats_1 = __importDefault(require("./watcher_stats"));
|
||||
exports.WatcherStats = watcher_stats_1.default;
|
||||
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidHlwZXMuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi9zcmMvdHlwZXMudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IjtBQUNBLFlBQVk7Ozs7OztBQUdaLHFDQUE4QjtBQWlGbUUsc0ZBakZ6RixlQUFLLE9BaUZ5RjtBQTlFdEcsb0VBQTJDO0FBOEUwSyx1QkE5RTlNLHVCQUFZLENBOEU4TSJ9
|
||||
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidHlwZXMuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi9zcmMvdHlwZXMudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IjtBQUNBLFlBQVk7Ozs7OztBQUdaLHFDQUE4QjtBQWlGbUUsc0ZBakZ6RixlQUFLLE9BaUZ5RjtBQTlFdEcsb0VBQTJDO0FBOEUwSyx1QkE5RTlNLHVCQUFZLENBOEU4TSJ9
|
||||
@@ -272,7 +272,7 @@ class AudioMetadataMangaer {
|
||||
this.tasksRunning = this.tasksRunning.filter((t) => t.id !== task.id)
|
||||
|
||||
if (this.tasksRunning.length < this.MAX_CONCURRENT_TASKS && this.tasksQueued.length) {
|
||||
Logger.info(`[AudioMetadataManager] Task finished and dequeueing next task. ${this.tasksQueued} tasks queued.`)
|
||||
Logger.info(`[AudioMetadataManager] Task finished and dequeueing next task. ${this.tasksQueued.length} tasks queued.`)
|
||||
const nextTask = this.tasksQueued.shift()
|
||||
SocketAuthority.emitter('metadata_embed_queue_update', {
|
||||
libraryItemId: nextTask.data.libraryItemId,
|
||||
|
||||
@@ -112,7 +112,7 @@ class BackupManager {
|
||||
.mv(tempPath)
|
||||
.then(() => true)
|
||||
.catch((error) => {
|
||||
Logger.error('[BackupManager] Failed to move backup file', path, error)
|
||||
Logger.error('[BackupManager] Failed to move backup file', tempPath, error)
|
||||
return false
|
||||
})
|
||||
if (!success) {
|
||||
|
||||
@@ -369,8 +369,8 @@ class BinaryManager {
|
||||
* @param {Binary} binary
|
||||
*/
|
||||
async removeBinary(destination, binary) {
|
||||
const binaryPath = path.join(destination, binary.fileName)
|
||||
try {
|
||||
const binaryPath = path.join(destination, binary.fileName)
|
||||
if (await fs.pathExists(binaryPath)) {
|
||||
Logger.debug(`[BinaryManager] Removing binary: ${binaryPath}`)
|
||||
await fs.remove(binaryPath)
|
||||
|
||||
@@ -4,8 +4,13 @@ const stream = require('stream')
|
||||
const Logger = require('../Logger')
|
||||
const { resizeImage } = require('../utils/ffmpegHelpers')
|
||||
const { encodeUriPath } = require('../utils/fileUtils')
|
||||
const { isUUID } = require('../utils/index')
|
||||
const Database = require('../Database')
|
||||
|
||||
function isValidCacheDimension(dimension) {
|
||||
return Number.isSafeInteger(dimension) && dimension > 0
|
||||
}
|
||||
|
||||
class CacheManager {
|
||||
constructor() {
|
||||
this.CachePath = null
|
||||
@@ -40,6 +45,9 @@ class CacheManager {
|
||||
const width = options.width || 400
|
||||
const height = options.height || null
|
||||
|
||||
if (!['webp', 'jpeg', 'png'].includes(format) || !isUUID(libraryItemId) || !isValidCacheDimension(width) || (height !== null && !isValidCacheDimension(height))) {
|
||||
return res.sendStatus(400)
|
||||
}
|
||||
res.type(`image/${format}`)
|
||||
|
||||
const cachePath = Path.join(this.CoverCachePath, `${libraryItemId}_${width}${height ? `x${height}` : ''}`) + '.' + format
|
||||
@@ -149,6 +157,9 @@ class CacheManager {
|
||||
const width = options.width || 400
|
||||
const height = options.height || null
|
||||
|
||||
if (!['webp', 'jpeg', 'png'].includes(format) || !isUUID(authorId) || !isValidCacheDimension(width) || (height !== null && !isValidCacheDimension(height))) {
|
||||
return res.sendStatus(400)
|
||||
}
|
||||
res.type(`image/${format}`)
|
||||
|
||||
var cachePath = Path.join(this.ImageCachePath, `${authorId}_${width}${height ? `x${height}` : ''}`) + '.' + format
|
||||
|
||||
@@ -138,6 +138,16 @@ class CoverManager {
|
||||
}
|
||||
}
|
||||
coverPath = filePathToPOSIX(coverPath)
|
||||
|
||||
// Must already have been scanned in for the library item
|
||||
const isLibraryItemFile = libraryItem.libraryFiles?.some((lf) => filePathToPOSIX(lf.metadata?.path) === coverPath)
|
||||
if (!isLibraryItemFile) {
|
||||
Logger.error(`[CoverManager] validate cover path is not a library item file "${coverPath}"`)
|
||||
return {
|
||||
error: 'Invalid cover path'
|
||||
}
|
||||
}
|
||||
|
||||
// Cover path already set on media
|
||||
if (libraryItem.media.coverPath == coverPath) {
|
||||
Logger.debug(`[CoverManager] validate cover path already set "${coverPath}"`)
|
||||
|
||||
@@ -422,21 +422,6 @@ class Book extends Model {
|
||||
hasUpdates = true
|
||||
}
|
||||
|
||||
// TODO: Remove support for updating audioFiles, chapters and ebookFile here
|
||||
const arrayOfObjectsKeys = ['audioFiles', 'chapters']
|
||||
arrayOfObjectsKeys.forEach((key) => {
|
||||
if (Array.isArray(payload[key]) && !payload[key].some((item) => typeof item !== 'object') && JSON.stringify(this[key]) !== JSON.stringify(payload[key])) {
|
||||
this[key] = payload[key]
|
||||
this.changed(key, true)
|
||||
hasUpdates = true
|
||||
}
|
||||
})
|
||||
if (payload.ebookFile && JSON.stringify(this.ebookFile) !== JSON.stringify(payload.ebookFile)) {
|
||||
this.ebookFile = payload.ebookFile
|
||||
this.changed('ebookFile', true)
|
||||
hasUpdates = true
|
||||
}
|
||||
|
||||
if (hasUpdates) {
|
||||
Logger.debug(`[Book] "${this.title}" changed keys:`, this.changed())
|
||||
await this.save()
|
||||
|
||||
@@ -491,11 +491,7 @@ class LibraryItem extends Model {
|
||||
}
|
||||
Logger.debug(`Loaded ${newestAuthorsPayload.authors.length} of ${newestAuthorsPayload.count} authors for "Newest Authors" in ${newestAuthorsResult.elapsedSeconds}s`)
|
||||
} else if (library.isPodcast) {
|
||||
const [newestEpisodesResult, mostRecentResult, mediaFinishedResult] = await Promise.all([
|
||||
timed(() => libraryFilters.getNewestPodcastEpisodes(library, user, limit)),
|
||||
timed(() => libraryFilters.getLibraryItemsMostRecentlyAdded(library, user, include, limit)),
|
||||
timed(() => libraryFilters.getMediaFinished(library, user, include, limit))
|
||||
])
|
||||
const [newestEpisodesResult, mostRecentResult, mediaFinishedResult] = await Promise.all([timed(() => libraryFilters.getNewestPodcastEpisodes(library, user, limit)), timed(() => libraryFilters.getLibraryItemsMostRecentlyAdded(library, user, include, limit)), timed(() => libraryFilters.getMediaFinished(library, user, include, limit))])
|
||||
|
||||
const newestEpisodesPayload = newestEpisodesResult.payload
|
||||
// "Newest Episodes" shelf
|
||||
@@ -852,8 +848,18 @@ class LibraryItem extends Model {
|
||||
get isPodcast() {
|
||||
return this.mediaType === 'podcast'
|
||||
}
|
||||
/**
|
||||
* Check if book or podcast library item has audio tracks.
|
||||
* Requires expanded library item (media loaded).
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
get hasAudioTracks() {
|
||||
return this.media.hasAudioTracks()
|
||||
if (!this.media) {
|
||||
Logger.error(`[LibraryItem] hasAudioTracks: Library item "${this.id}" does not have media`)
|
||||
return false
|
||||
}
|
||||
return this.media.hasAudioTracks
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -904,24 +910,6 @@ class LibraryItem extends Model {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if book or podcast library item has audio tracks
|
||||
* Requires expanded library item
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
hasAudioTracks() {
|
||||
if (!this.media) {
|
||||
Logger.error(`[LibraryItem] hasAudioTracks: Library item "${this.id}" does not have media`)
|
||||
return false
|
||||
}
|
||||
if (this.isBook) {
|
||||
return this.media.audioFiles?.length > 0
|
||||
} else {
|
||||
return this.media.podcastEpisodes?.length > 0
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} ino
|
||||
|
||||
@@ -5,7 +5,31 @@ const Logger = require('../../Logger')
|
||||
const User = require('../../models/User')
|
||||
const { sanitize } = require('../../utils/htmlSanitizer')
|
||||
|
||||
const PATCHABLE_SETTINGS_KEYS = new Set([
|
||||
'scannerParseSubtitle',
|
||||
'scannerFindCovers',
|
||||
'scannerCoverProvider',
|
||||
'scannerPreferMatchedMetadata',
|
||||
'scannerDisableWatcher',
|
||||
'storeCoverWithItem',
|
||||
'storeMetadataWithItem',
|
||||
'allowIframe',
|
||||
'allowedOrigins',
|
||||
'backupSchedule',
|
||||
'backupsToKeep',
|
||||
'maxBackupSize',
|
||||
'logLevel',
|
||||
'homeBookshelfView',
|
||||
'bookshelfView',
|
||||
'dateFormat',
|
||||
'timeFormat',
|
||||
'language',
|
||||
'chromecastEnabled',
|
||||
'sortingIgnorePrefix'
|
||||
])
|
||||
|
||||
class ServerSettings {
|
||||
static patchableSettingsKeys = PATCHABLE_SETTINGS_KEYS
|
||||
constructor(settings) {
|
||||
this.id = 'server-settings'
|
||||
/** @type {string} JWT secret key ONLY used when JWT_SECRET_KEY is not set in ENV */
|
||||
@@ -341,24 +365,9 @@ class ServerSettings {
|
||||
update(payload) {
|
||||
let hasUpdates = false
|
||||
for (const key in payload) {
|
||||
if (key === 'authLoginCustomMessage') {
|
||||
payload[key] = sanitize(payload[key])
|
||||
}
|
||||
if (key === 'sortingPrefixes') {
|
||||
// Sorting prefixes are updated with the /api/sorting-prefixes endpoint
|
||||
continue
|
||||
} else if (key === 'authActiveAuthMethods') {
|
||||
if (!payload[key]?.length) {
|
||||
Logger.error(`[ServerSettings] Invalid authActiveAuthMethods`, payload[key])
|
||||
continue
|
||||
}
|
||||
this.authActiveAuthMethods.sort()
|
||||
payload[key].sort()
|
||||
if (payload[key].join() !== this.authActiveAuthMethods.join()) {
|
||||
this.authActiveAuthMethods = payload[key]
|
||||
hasUpdates = true
|
||||
}
|
||||
} else if (this[key] !== payload[key]) {
|
||||
if (!PATCHABLE_SETTINGS_KEYS.has(key)) continue
|
||||
|
||||
if (this[key] !== payload[key]) {
|
||||
if (key === 'logLevel') {
|
||||
Logger.setLogLevel(payload[key])
|
||||
}
|
||||
|
||||
@@ -83,7 +83,8 @@ class Audible {
|
||||
duration: runtimeLengthMin && !isNaN(runtimeLengthMin) ? Number(runtimeLengthMin) : 0,
|
||||
region: item.region || null,
|
||||
rating: item.rating || null,
|
||||
abridged: formatType === 'abridged'
|
||||
abridged: formatType === 'abridged',
|
||||
explicit: !!item.isAdult
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -515,10 +515,7 @@ class LibraryScanner {
|
||||
attributes: ['id', 'path'],
|
||||
where: {
|
||||
path: {
|
||||
[sequelize.Op.not]: fullPath
|
||||
},
|
||||
path: {
|
||||
[sequelize.Op.startsWith]: fullPath
|
||||
[sequelize.Op.startsWith]: fullPath + '/'
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -531,10 +528,7 @@ class LibraryScanner {
|
||||
attributes: ['id', 'path'],
|
||||
where: {
|
||||
path: {
|
||||
[sequelize.Op.not]: altFullPath
|
||||
},
|
||||
path: {
|
||||
[sequelize.Op.startsWith]: altFullPath
|
||||
[sequelize.Op.startsWith]: altFullPath + '/'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -58,8 +58,12 @@ class PodcastScanner {
|
||||
/** @type {AudioFile[]} */
|
||||
let newAudioFiles = []
|
||||
|
||||
// Track episode add/remove/update so item_updated includes the current episode list
|
||||
let hasEpisodeChanges = false
|
||||
|
||||
if (libraryItemData.hasAudioFileChanges || libraryItemData.audioLibraryFiles.length !== existingPodcastEpisodes.length) {
|
||||
// Filter out and destroy episodes that were removed
|
||||
// Filter out and destroy episodes that were removed.
|
||||
// filter() returns a new array — reassign to media.podcastEpisodes before emit.
|
||||
const episodesToRemove = []
|
||||
existingPodcastEpisodes = existingPodcastEpisodes.filter((ep) => {
|
||||
if (libraryItemData.checkAudioFileRemoved(ep.audioFile)) {
|
||||
@@ -70,6 +74,7 @@ class PodcastScanner {
|
||||
})
|
||||
|
||||
if (episodesToRemove.length) {
|
||||
hasEpisodeChanges = true
|
||||
// Remove episodes from playlists and media progress
|
||||
const episodeIds = episodesToRemove.map((ep) => ep.id)
|
||||
await Database.playlistModel.removeMediaItemsFromPlaylists(episodeIds)
|
||||
@@ -116,6 +121,7 @@ class PodcastScanner {
|
||||
AudioFileScanner.setPodcastEpisodeMetadataFromAudioMetaTags(podcastEpisode, libraryScan)
|
||||
libraryScan.addLog(LogLevel.INFO, `Podcast episode "${podcastEpisode.title}" keys changed [${podcastEpisode.changed()?.join(', ')}]`)
|
||||
await podcastEpisode.save()
|
||||
hasEpisodeChanges = true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -155,9 +161,13 @@ class PodcastScanner {
|
||||
libraryScan.addLog(LogLevel.INFO, `New Podcast episode "${newPodcastEpisode.title}" added`)
|
||||
await newPodcastEpisode.save()
|
||||
existingPodcastEpisodes.push(newPodcastEpisode)
|
||||
hasEpisodeChanges = true
|
||||
}
|
||||
}
|
||||
|
||||
// Keep association in sync for toOldJSONExpanded() / item_updated socket payload
|
||||
media.podcastEpisodes = existingPodcastEpisodes
|
||||
|
||||
let hasMediaChanges = false
|
||||
if (existingPodcastEpisodes.length !== media.numEpisodes) {
|
||||
media.numEpisodes = existingPodcastEpisodes.length
|
||||
@@ -250,7 +260,7 @@ class PodcastScanner {
|
||||
|
||||
return {
|
||||
libraryItem: existingLibraryItem,
|
||||
wasUpdated: hasMediaChanges || libraryItemUpdated
|
||||
wasUpdated: hasMediaChanges || libraryItemUpdated || hasEpisodeChanges
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,21 @@ const fs = require('../libs/fsExtra')
|
||||
const StreamZip = require('../libs/nodeStreamZip')
|
||||
const Archive = require('../libs/libarchive/archive')
|
||||
const { isWritable } = require('./fileUtils')
|
||||
const { sanitizePath } = require('../libs/archiver/archiverUtils')
|
||||
|
||||
/**
|
||||
* Sanitize a path from an archive
|
||||
*
|
||||
* @param {string} filename
|
||||
* @returns {string}
|
||||
*/
|
||||
function sanitizeArchivePath(filename) {
|
||||
const sanitizedPath = Path.normalize(sanitizePath(filename))
|
||||
if (!sanitizedPath || sanitizedPath === '.' || sanitizedPath === '..' || sanitizedPath.startsWith(`..${Path.sep}`) || Path.isAbsolute(sanitizedPath)) {
|
||||
throw new Error(`[CbrComicBookExtractor] Unsafe archive path "${filename}"`)
|
||||
}
|
||||
return sanitizedPath
|
||||
}
|
||||
|
||||
class AbstractComicBookExtractor {
|
||||
constructor(comicPath) {
|
||||
@@ -57,7 +72,11 @@ class CbrComicBookExtractor extends AbstractComicBookExtractor {
|
||||
this.tmpDir = global.MetadataPath ? Path.join(global.MetadataPath, 'tmp') : os.tmpdir()
|
||||
await fs.ensureDir(this.tmpDir)
|
||||
if (!(await isWritable(this.tmpDir))) throw new Error(`[CbrComicBookExtractor] Temp directory "${this.tmpDir}" is not writable`)
|
||||
this.archive = await unrar.createExtractorFromFile({ filepath: this.comicPath, targetPath: this.tmpDir })
|
||||
this.archive = await unrar.createExtractorFromFile({
|
||||
filepath: this.comicPath,
|
||||
targetPath: this.tmpDir,
|
||||
filenameTransform: sanitizeArchivePath
|
||||
})
|
||||
Logger.debug(`[CbrComicBookExtractor] Opened comic book "${this.comicPath}". Using temp directory "${this.tmpDir}" for extraction.`)
|
||||
}
|
||||
|
||||
@@ -81,14 +100,22 @@ class CbrComicBookExtractor extends AbstractComicBookExtractor {
|
||||
}
|
||||
}
|
||||
|
||||
getExtractedFilePath(file) {
|
||||
const sanitizedFile = sanitizeArchivePath(file)
|
||||
return {
|
||||
filePath: Path.join(this.tmpDir, sanitizedFile),
|
||||
relativePath: sanitizedFile
|
||||
}
|
||||
}
|
||||
|
||||
async extractToBuffer(file) {
|
||||
if (!this.archive) return null
|
||||
const extracted = this.archive.extract({ files: [file] })
|
||||
const files = [...extracted.files]
|
||||
const filePath = Path.join(this.tmpDir, files[0].fileHeader.name)
|
||||
const { filePath, relativePath } = this.getExtractedFilePath(files[0].fileHeader.name)
|
||||
const fileData = await fs.readFile(filePath)
|
||||
await fs.remove(filePath)
|
||||
await this.removeEmptyParentDirs(files[0].fileHeader.name)
|
||||
await this.removeEmptyParentDirs(relativePath)
|
||||
Logger.debug(`[CbrComicBookExtractor] Extracted file "${file}" from comic book "${this.comicPath}" to buffer, size: ${fileData.length}`)
|
||||
return fileData
|
||||
}
|
||||
@@ -97,9 +124,9 @@ class CbrComicBookExtractor extends AbstractComicBookExtractor {
|
||||
if (!this.archive) return false
|
||||
const extracted = this.archive.extract({ files: [file] })
|
||||
const files = [...extracted.files]
|
||||
const extractedFilePath = Path.join(this.tmpDir, files[0].fileHeader.name)
|
||||
const { filePath: extractedFilePath, relativePath } = this.getExtractedFilePath(files[0].fileHeader.name)
|
||||
await fs.move(extractedFilePath, outputFilePath, { overwrite: true })
|
||||
await this.removeEmptyParentDirs(files[0].fileHeader.name)
|
||||
await this.removeEmptyParentDirs(relativePath)
|
||||
Logger.debug(`[CbrComicBookExtractor] Extracted file "${file}" from comic book "${this.comicPath}" to "${outputFilePath}"`)
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
const { createNewSortInstance } = require('../libs/fastSort')
|
||||
const Database = require('../Database')
|
||||
const Logger = require('../Logger')
|
||||
const { getTitlePrefixAtEnd, isNullOrNaN, getTitleIgnorePrefix } = require('../utils/index')
|
||||
const naturalSort = createNewSortInstance({
|
||||
comparer: new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' }).compare
|
||||
})
|
||||
|
||||
const VALID_COLLAPSE_SUBSERIES_SORTS = new Set(['addedAt', 'size', 'birthtimeMs', 'mtimeMs', 'media.duration', 'media.metadata.publishedYear', 'media.metadata.authorName', 'media.metadata.authorNameLF', 'media.metadata.title', 'sequence', 'progress', 'progress.createdAt', 'progress.finishedAt', 'random'])
|
||||
|
||||
module.exports = {
|
||||
/**
|
||||
*
|
||||
@@ -100,6 +103,11 @@ module.exports = {
|
||||
* @returns {Object[]}
|
||||
*/
|
||||
async handleCollapseSubseries(payload, seriesId, user, library) {
|
||||
if (payload.sortBy && !VALID_COLLAPSE_SUBSERIES_SORTS.has(payload.sortBy)) {
|
||||
Logger.warn(`[libraryHelpers] Invalid "sort" query string "${payload.sortBy}"`)
|
||||
payload.sortBy = undefined
|
||||
}
|
||||
|
||||
const seriesWithBooks = await Database.seriesModel.findByPk(seriesId, {
|
||||
include: {
|
||||
model: Database.bookModel,
|
||||
@@ -196,7 +204,9 @@ module.exports = {
|
||||
return li.collapsedSeries?.name || li.media.title
|
||||
}
|
||||
} else {
|
||||
return payload.sortBy.split('.').reduce((a, b) => a[b], li)
|
||||
if (payload.sortBy === 'media.metadata.authorName') return li.authorNamesFirstLast ?? ''
|
||||
if (payload.sortBy === 'media.metadata.authorNameLF') return li.authorNamesLastFirst ?? ''
|
||||
return payload.sortBy.split('.').reduce((a, b) => a?.[b], li) ?? ''
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -69,7 +69,7 @@ function extractStringOrStringify(json) {
|
||||
return json[Object.keys(json)[0]][0]
|
||||
}
|
||||
// Handles case where html was included without being wrapped in CDATA
|
||||
return JSON.stringify(value)
|
||||
return JSON.stringify(json)
|
||||
} catch {
|
||||
return ''
|
||||
}
|
||||
|
||||
+14
-14
@@ -12,22 +12,22 @@ function tryGrabBitRate(stream, all_streams, total_bit_rate) {
|
||||
}
|
||||
|
||||
// Attempt to get bitrate from bps tags
|
||||
var bps = stream.tags.BPS || stream.tags['BPS-eng'] || stream.tags['BPS_eng']
|
||||
const bps = stream.tags.BPS || stream.tags['BPS-eng'] || stream.tags['BPS_eng']
|
||||
if (bps && !isNaN(bps)) {
|
||||
return Number(bps)
|
||||
}
|
||||
|
||||
var tagDuration = stream.tags.DURATION || stream.tags['DURATION-eng'] || stream.tags['DURATION_eng']
|
||||
var tagBytes = stream.tags.NUMBER_OF_BYTES || stream.tags['NUMBER_OF_BYTES-eng'] || stream.tags['NUMBER_OF_BYTES_eng']
|
||||
const tagDuration = stream.tags.DURATION || stream.tags['DURATION-eng'] || stream.tags['DURATION_eng']
|
||||
const tagBytes = stream.tags.NUMBER_OF_BYTES || stream.tags['NUMBER_OF_BYTES-eng'] || stream.tags['NUMBER_OF_BYTES_eng']
|
||||
if (tagDuration && tagBytes && !isNaN(tagDuration) && !isNaN(tagBytes)) {
|
||||
var bps = Math.floor((Number(tagBytes) * 8) / Number(tagDuration))
|
||||
if (bps && !isNaN(bps)) {
|
||||
return bps
|
||||
const calculatedBps = Math.floor((Number(tagBytes) * 8) / Number(tagDuration))
|
||||
if (calculatedBps && !isNaN(calculatedBps)) {
|
||||
return calculatedBps
|
||||
}
|
||||
}
|
||||
|
||||
if (total_bit_rate && stream.codec_type === 'video') {
|
||||
var estimated_bit_rate = total_bit_rate
|
||||
let estimated_bit_rate = total_bit_rate
|
||||
all_streams.forEach((stream) => {
|
||||
if (stream.bit_rate && !isNaN(stream.bit_rate)) {
|
||||
estimated_bit_rate -= Number(stream.bit_rate)
|
||||
@@ -46,9 +46,9 @@ function tryGrabBitRate(stream, all_streams, total_bit_rate) {
|
||||
}
|
||||
|
||||
function tryGrabFrameRate(stream) {
|
||||
var avgFrameRate = stream.avg_frame_rate || stream.r_frame_rate
|
||||
let avgFrameRate = stream.avg_frame_rate || stream.r_frame_rate
|
||||
if (!avgFrameRate) return null
|
||||
var parts = avgFrameRate.split('/')
|
||||
const parts = avgFrameRate.split('/')
|
||||
if (parts.length === 2) {
|
||||
avgFrameRate = Number(parts[0]) / Number(parts[1])
|
||||
} else {
|
||||
@@ -59,13 +59,13 @@ function tryGrabFrameRate(stream) {
|
||||
}
|
||||
|
||||
function tryGrabSampleRate(stream) {
|
||||
var sample_rate = stream.sample_rate
|
||||
const sample_rate = stream.sample_rate
|
||||
if (!isNaN(sample_rate)) return Number(sample_rate)
|
||||
return null
|
||||
}
|
||||
|
||||
function tryGrabChannelLayout(stream) {
|
||||
var layout = stream.channel_layout
|
||||
const layout = stream.channel_layout
|
||||
if (!layout) return null
|
||||
return String(layout).split('(').shift()
|
||||
}
|
||||
@@ -81,7 +81,7 @@ function tryGrabTags(stream, ...tags) {
|
||||
}
|
||||
|
||||
function parseMediaStreamInfo(stream, all_streams, total_bit_rate) {
|
||||
var info = {
|
||||
const info = {
|
||||
index: stream.index,
|
||||
type: stream.codec_type,
|
||||
codec: stream.codec_name || null,
|
||||
@@ -95,7 +95,7 @@ function parseMediaStreamInfo(stream, all_streams, total_bit_rate) {
|
||||
if (stream.tags) info.tags = stream.tags
|
||||
|
||||
if (info.type === 'audio' || info.type === 'subtitle') {
|
||||
var disposition = stream.disposition || {}
|
||||
const disposition = stream.disposition || {}
|
||||
info.is_default = disposition.default === 1 || disposition.default === '1'
|
||||
}
|
||||
|
||||
@@ -230,7 +230,7 @@ function parseTags(format, verbose) {
|
||||
function getDefaultAudioStream(audioStreams) {
|
||||
if (!audioStreams || !audioStreams.length) return null
|
||||
if (audioStreams.length === 1) return audioStreams[0]
|
||||
var defaultStream = audioStreams.find((a) => a.is_default)
|
||||
const defaultStream = audioStreams.find((a) => a.is_default)
|
||||
if (!defaultStream) return audioStreams[0]
|
||||
return defaultStream
|
||||
}
|
||||
|
||||
@@ -11,7 +11,12 @@ const naturalSort = createNewSortInstance({
|
||||
|
||||
module.exports = {
|
||||
decode(text) {
|
||||
return Buffer.from(decodeURIComponent(text), 'base64').toString()
|
||||
try {
|
||||
return Buffer.from(decodeURIComponent(text), 'base64').toString()
|
||||
} catch (error) {
|
||||
Logger.warn(`[libraryFilters] Failed to decode filter value "${text}": ${error.message}`)
|
||||
return null
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
@@ -133,9 +133,10 @@ module.exports = {
|
||||
/**
|
||||
* Get all library items that have narrators
|
||||
* @param {string[]} narrators
|
||||
* @param {string} libraryId
|
||||
* @returns {Promise<import('../../models/LibraryItem')[]>}
|
||||
*/
|
||||
async getAllLibraryItemsWithNarrators(narrators) {
|
||||
async getAllLibraryItemsWithNarrators(narrators, libraryId) {
|
||||
const libraryItems = []
|
||||
const booksWithGenre = await Database.bookModel.findAll({
|
||||
where: Sequelize.where(Sequelize.literal(`(SELECT count(*) FROM json_each(narrators) WHERE json_valid(narrators) AND json_each.value IN (:narrators))`), {
|
||||
@@ -146,7 +147,8 @@ module.exports = {
|
||||
},
|
||||
include: [
|
||||
{
|
||||
model: Database.libraryItemModel
|
||||
model: Database.libraryItemModel,
|
||||
where: { libraryId }
|
||||
},
|
||||
{
|
||||
model: Database.authorModel,
|
||||
|
||||
@@ -309,11 +309,12 @@ module.exports = {
|
||||
* @param {Sequelize.WhereOptions} seriesWhere
|
||||
* @returns {object} { booksToExclude, bookSeriesToInclude }
|
||||
*/
|
||||
async getCollapseSeriesBooksToExclude(bookFindOptions, seriesWhere) {
|
||||
async getCollapseSeriesBooksToExclude(bookFindOptions, seriesWhere, replacements = {}) {
|
||||
const allSeries = await Database.seriesModel.findAll({
|
||||
attributes: ['id', 'name', [Sequelize.literal('(SELECT count(*) FROM bookSeries bs WHERE bs.seriesId = series.id)'), 'numBooks']],
|
||||
distinct: true,
|
||||
subQuery: false,
|
||||
replacements,
|
||||
where: seriesWhere,
|
||||
include: [
|
||||
{
|
||||
@@ -581,7 +582,7 @@ module.exports = {
|
||||
...bookIncludes
|
||||
]
|
||||
}
|
||||
const { booksToExclude, bookSeriesToInclude } = await this.getCollapseSeriesBooksToExclude(bookFindOptions, seriesWhere)
|
||||
const { booksToExclude, bookSeriesToInclude } = await this.getCollapseSeriesBooksToExclude(bookFindOptions, seriesWhere, replacements)
|
||||
if (booksToExclude.length) {
|
||||
bookWhere.push({
|
||||
id: {
|
||||
|
||||
@@ -5,7 +5,12 @@ const libraryItemsBookFilters = require('./libraryItemsBookFilters')
|
||||
|
||||
module.exports = {
|
||||
decode(text) {
|
||||
return Buffer.from(decodeURIComponent(text), 'base64').toString()
|
||||
try {
|
||||
return Buffer.from(decodeURIComponent(text), 'base64').toString()
|
||||
} catch (error) {
|
||||
Logger.warn(`[seriesFilters] Failed to decode filter value "${text}": ${error.message}`)
|
||||
return null
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
+19
-19
@@ -149,16 +149,16 @@ module.exports.buildLibraryFile = buildLibraryFile
|
||||
function getBookDataFromDir(relPath, parseSubtitle = false) {
|
||||
const splitDir = relPath.split('/')
|
||||
|
||||
var folder = splitDir.pop() // Audio files will always be in the directory named for the title
|
||||
series = splitDir.length > 1 ? splitDir.pop() : null // If there are at least 2 more directories, next furthest will be the series
|
||||
author = splitDir.length > 0 ? splitDir.pop() : null // There could be many more directories, but only the top 3 are used for naming /author/series/title/
|
||||
const titleFolder = splitDir.pop() // Audio files will always be in the directory named for the title
|
||||
const series = splitDir.length > 1 ? splitDir.pop() : null // If there are at least 2 more directories, next furthest will be the series
|
||||
const author = splitDir.length > 0 ? splitDir.pop() : null // There could be many more directories, but only the top 3 are used for naming /author/series/title/
|
||||
|
||||
// The may contain various other pieces of metadata, these functions extract it.
|
||||
var [folder, asin] = getASIN(folder)
|
||||
var [folder, narrators] = getNarrator(folder)
|
||||
var [folder, sequence] = series ? getSequence(folder) : [folder, null]
|
||||
var [folder, publishedYear] = getPublishedYear(folder)
|
||||
var [title, subtitle] = parseSubtitle ? getSubtitle(folder) : [folder, null]
|
||||
// Each extractor strips one piece of metadata and returns the remaining folder name.
|
||||
const [afterAsin, asin] = getASIN(titleFolder)
|
||||
const [afterNarrators, narrators] = getNarrator(afterAsin)
|
||||
const [afterSequence, sequence] = series ? getSequence(afterNarrators) : [afterNarrators, null]
|
||||
const [afterYear, publishedYear] = getPublishedYear(afterSequence)
|
||||
const [title, subtitle] = parseSubtitle ? getSubtitle(afterYear) : [afterYear, null]
|
||||
|
||||
return {
|
||||
title,
|
||||
@@ -180,8 +180,8 @@ module.exports.getBookDataFromDir = getBookDataFromDir
|
||||
* @returns {[string, string]} [folder, narrator]
|
||||
*/
|
||||
function getNarrator(folder) {
|
||||
let pattern = /^(?<title>.*) \{(?<narrators>.*)\}$/
|
||||
let match = folder.match(pattern)
|
||||
const pattern = /^(?<title>.*) \{(?<narrators>.*)\}$/
|
||||
const match = folder.match(pattern)
|
||||
return match ? [match.groups.title, match.groups.narrators] : [folder, null]
|
||||
}
|
||||
|
||||
@@ -204,12 +204,12 @@ function getNarrator(folder) {
|
||||
*/
|
||||
function getSequence(folder) {
|
||||
// Matches a valid volume string. Also matches a book whose title starts with a 1 to 3 digit number. Will handle that later.
|
||||
let pattern = /^(?<volumeLabel>vol\.? |volume |book )?(?<sequence>\d{0,3}(?:\.\d{1,2})?)(?<trailingDot>\.?)(?: (?<suffix>.*))?$/i
|
||||
const pattern = /^(?<volumeLabel>vol\.? |volume |book )?(?<sequence>\d{0,3}(?:\.\d{1,2})?)(?<trailingDot>\.?)(?: (?<suffix>.*))?$/i
|
||||
|
||||
let volumeNumber = null
|
||||
let parts = folder.split(' - ')
|
||||
const parts = folder.split(' - ')
|
||||
for (let i = 0; i < parts.length; i++) {
|
||||
let match = parts[i].match(pattern)
|
||||
const match = parts[i].match(pattern)
|
||||
// This excludes '101 Dalmations' but includes '101. Dalmations'
|
||||
if (match && !(match.groups.suffix && !(match.groups.volumeLabel || match.groups.trailingDot))) {
|
||||
volumeNumber = isNaN(match.groups.sequence) ? match.groups.sequence : Number(match.groups.sequence).toString()
|
||||
@@ -232,10 +232,10 @@ function getSequence(folder) {
|
||||
* @returns {[string, string]} [folder, publishedYear]
|
||||
*/
|
||||
function getPublishedYear(folder) {
|
||||
var publishedYear = null
|
||||
let publishedYear = null
|
||||
|
||||
pattern = /^ *\(?([0-9]{4})\)? * - *(.+)/ //Matches #### - title or (####) - title
|
||||
var match = folder.match(pattern)
|
||||
const pattern = /^ *\(?([0-9]{4})\)? * - *(.+)/ //Matches #### - title or (####) - title
|
||||
const match = folder.match(pattern)
|
||||
if (match) {
|
||||
publishedYear = match[1]
|
||||
folder = match[2]
|
||||
@@ -252,7 +252,7 @@ function getPublishedYear(folder) {
|
||||
*/
|
||||
function getSubtitle(folder) {
|
||||
// Subtitle is everything after " - "
|
||||
var splitTitle = folder.split(' - ')
|
||||
const splitTitle = folder.split(' - ')
|
||||
return [splitTitle.shift(), splitTitle.join(' - ')]
|
||||
}
|
||||
|
||||
@@ -265,7 +265,7 @@ function getSubtitle(folder) {
|
||||
function getASIN(folder) {
|
||||
let asin = null
|
||||
|
||||
let pattern = /(?: |^)\[([A-Z0-9]{10})](?= |$)/ // Matches "[B0015T963C]"
|
||||
const pattern = /(?: |^)\[([A-Z0-9]{10})](?= |$)/ // Matches "[B0015T963C]"
|
||||
const match = folder.match(pattern)
|
||||
if (match) {
|
||||
asin = match[1]
|
||||
|
||||
@@ -28,11 +28,6 @@ describe('TokenManager', () => {
|
||||
})
|
||||
|
||||
describe('validateAccessToken', () => {
|
||||
it('rejects refresh tokens', () => {
|
||||
const refreshToken = jwt.sign({ userId, type: 'refresh' }, secret, { expiresIn: 3600 })
|
||||
expect(TokenManager.validateAccessToken(refreshToken)).to.equal(null)
|
||||
})
|
||||
|
||||
it('accepts access tokens', () => {
|
||||
const accessToken = jwt.sign({ userId, type: 'access' }, secret, { expiresIn: 3600 })
|
||||
const decoded = TokenManager.validateAccessToken(accessToken)
|
||||
|
||||
@@ -17,6 +17,7 @@ describe('MigrationManager', () => {
|
||||
let fsMoveStub
|
||||
let fsRemoveStub
|
||||
let fsEnsureDirStub
|
||||
let fsPathExistsStub
|
||||
let processExitStub
|
||||
let configPath = '/path/to/config'
|
||||
|
||||
|
||||
@@ -45,4 +45,26 @@ describe('Audible', () => {
|
||||
expect(result).to.equal('.5')
|
||||
})
|
||||
})
|
||||
|
||||
describe('cleanResult', () => {
|
||||
const baseItem = {
|
||||
title: 'Some Book',
|
||||
formatType: 'unabridged'
|
||||
}
|
||||
|
||||
it('should set explicit to true when isAdult is true', () => {
|
||||
const result = audible.cleanResult({ ...baseItem, isAdult: true })
|
||||
expect(result.explicit).to.equal(true)
|
||||
})
|
||||
|
||||
it('should set explicit to false when isAdult is false', () => {
|
||||
const result = audible.cleanResult({ ...baseItem, isAdult: false })
|
||||
expect(result.explicit).to.equal(false)
|
||||
})
|
||||
|
||||
it('should set explicit to false when isAdult is not present', () => {
|
||||
const result = audible.cleanResult({ ...baseItem })
|
||||
expect(result.explicit).to.equal(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "node",
|
||||
"rootDir": ".",
|
||||
"outDir": "dist-server",
|
||||
"allowJs": true,
|
||||
"resolveJsonModule": true,
|
||||
"esModuleInterop": true,
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"noEmitOnError": true,
|
||||
"incremental": true,
|
||||
"tsBuildInfoFile": "dist-server/.tsbuildinfo",
|
||||
"forceConsistentCasingInFileNames": true
|
||||
},
|
||||
"include": ["index.js", "dev.js", "server/**/*.js", "server/**/*.ts", "test/**/*.js", "test/**/*.ts"],
|
||||
"exclude": ["node_modules", "dist-server", "client", "**/*.d.ts"]
|
||||
}
|
||||
Reference in new issue
Block a user