mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2026-06-02 00:40:39 +02:00
Compare commits
84 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 565eb423ee | |||
| 42b0e31b4a | |||
| 97a8959bf8 | |||
| b5b99cbaca | |||
| f04ef320aa | |||
| 4e33059ac8 | |||
| 699644322b | |||
| 49ba364b2a | |||
| adb3967f89 | |||
| cfdcac9475 | |||
| b1d57bc0b3 | |||
| f7cea8ca12 | |||
| 293440006b | |||
| 45f7f54b6c | |||
| bb5e16157c | |||
| 2e8cb46c57 | |||
| f9c0e52f18 | |||
| 6290cfaeb1 | |||
| fd3d4f5fcf | |||
| 9f9bee2ddc | |||
| 568bf0254d | |||
| 79f4db5ff3 | |||
| 7038f5730f | |||
| 0a8186cbda | |||
| 659164003f | |||
| de5d8650e8 | |||
| bacefb5f6f | |||
| 0169bf5518 | |||
| 8f192b1b17 | |||
| 21343b5aa0 | |||
| a5508cdc4c | |||
| bd4f48ec39 | |||
| cb9fc3e0d1 | |||
| 707533df8f | |||
| 2e48ec0dde | |||
| f1e46a351b | |||
| da8fd2d9d5 | |||
| f1de307bf9 | |||
| 7282afcfde | |||
| e2f1aeed75 | |||
| 23a750214f | |||
| 6a7418ad41 | |||
| 8b00c16062 | |||
| 8ee5646d79 | |||
| 373551fb74 | |||
| d9b206fe1c | |||
| fe4e0145c9 | |||
| c4d99a118f | |||
| b96226966b | |||
| 5ca12eee19 | |||
| f460297daf | |||
| ebdf377fc1 | |||
| 808d23561c | |||
| a34813b3ab | |||
| 725192fbc0 | |||
| 2915c072b5 | |||
| 03a1d7da32 | |||
| 1be1ce6f87 | |||
| 21b27c432c | |||
| cbe5e3db8a | |||
| 08b4d4d7a2 | |||
| ac8324e595 | |||
| a14c6a3a8b | |||
| 74b35ea9d1 | |||
| 78d8c83e6d | |||
| bf795d3662 | |||
| 1fbd090441 | |||
| 70621e72e8 | |||
| d30a09f503 | |||
| 39567c6c22 | |||
| ed3af5bdcd | |||
| 9e54b4f7ca | |||
| ec65376569 | |||
| 4e8cd6fba0 | |||
| 1a3d70d041 | |||
| 14e92435ec | |||
| 0ccb88904a | |||
| 4cc300d6e9 | |||
| 068ba84a8c | |||
| ef45f844e5 | |||
| 9a261195b7 | |||
| 3d08a35aa0 | |||
| a13143245b | |||
| 52bb28669a |
@@ -419,7 +419,7 @@ export default {
|
|||||||
|
|
||||||
this.postScrollTimeout = setTimeout(this.postScroll, 500)
|
this.postScrollTimeout = setTimeout(this.postScroll, 500)
|
||||||
},
|
},
|
||||||
async resetEntities() {
|
async resetEntities(scrollPositionToRestore) {
|
||||||
if (this.isFetchingEntities) {
|
if (this.isFetchingEntities) {
|
||||||
this.pendingReset = true
|
this.pendingReset = true
|
||||||
return
|
return
|
||||||
@@ -437,6 +437,12 @@ export default {
|
|||||||
await this.loadPage(0)
|
await this.loadPage(0)
|
||||||
var lastBookIndex = Math.min(this.totalEntities, this.shelvesPerPage * this.entitiesPerShelf)
|
var lastBookIndex = Math.min(this.totalEntities, this.shelvesPerPage * this.entitiesPerShelf)
|
||||||
this.mountEntities(0, lastBookIndex)
|
this.mountEntities(0, lastBookIndex)
|
||||||
|
|
||||||
|
if (scrollPositionToRestore) {
|
||||||
|
if (window.bookshelf) {
|
||||||
|
window.bookshelf.scrollTop = scrollPositionToRestore
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
async rebuild() {
|
async rebuild() {
|
||||||
this.initSizeData()
|
this.initSizeData()
|
||||||
@@ -444,9 +450,8 @@ export default {
|
|||||||
var lastBookIndex = Math.min(this.totalEntities, this.booksPerFetch)
|
var lastBookIndex = Math.min(this.totalEntities, this.booksPerFetch)
|
||||||
this.destroyEntityComponents()
|
this.destroyEntityComponents()
|
||||||
await this.loadPage(0)
|
await this.loadPage(0)
|
||||||
var bookshelfEl = document.getElementById('bookshelf')
|
if (window.bookshelf) {
|
||||||
if (bookshelfEl) {
|
window.bookshelf.scrollTop = 0
|
||||||
bookshelfEl.scrollTop = 0
|
|
||||||
}
|
}
|
||||||
this.mountEntities(0, lastBookIndex)
|
this.mountEntities(0, lastBookIndex)
|
||||||
},
|
},
|
||||||
@@ -547,6 +552,15 @@ export default {
|
|||||||
if (this.entityName === 'items' || this.entityName === 'series-books') {
|
if (this.entityName === 'items' || this.entityName === 'series-books') {
|
||||||
var indexOf = this.entities.findIndex((ent) => ent && ent.id === libraryItem.id)
|
var indexOf = this.entities.findIndex((ent) => ent && ent.id === libraryItem.id)
|
||||||
if (indexOf >= 0) {
|
if (indexOf >= 0) {
|
||||||
|
if (this.entityName === 'items' && this.orderBy === 'media.metadata.title') {
|
||||||
|
const curTitle = this.entities[indexOf].media.metadata?.title
|
||||||
|
const newTitle = libraryItem.media.metadata?.title
|
||||||
|
if (curTitle != newTitle) {
|
||||||
|
console.log('Title changed. Re-sorting...')
|
||||||
|
this.resetEntities(this.currScrollTop)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
this.entities[indexOf] = libraryItem
|
this.entities[indexOf] = libraryItem
|
||||||
if (this.entityComponentRefs[indexOf]) {
|
if (this.entityComponentRefs[indexOf]) {
|
||||||
this.entityComponentRefs[indexOf].setEntity(libraryItem)
|
this.entityComponentRefs[indexOf].setEntity(libraryItem)
|
||||||
|
|||||||
@@ -85,7 +85,8 @@ export default {
|
|||||||
displayTitle: null,
|
displayTitle: null,
|
||||||
currentPlaybackRate: 1,
|
currentPlaybackRate: 1,
|
||||||
syncFailedToast: null,
|
syncFailedToast: null,
|
||||||
coverAspectRatio: 1
|
coverAspectRatio: 1,
|
||||||
|
lastChapterId: null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -236,12 +237,16 @@ export default {
|
|||||||
}
|
}
|
||||||
}, 1000)
|
}, 1000)
|
||||||
},
|
},
|
||||||
checkChapterEnd(time) {
|
checkChapterEnd() {
|
||||||
if (!this.currentChapter) return
|
if (!this.currentChapter) return
|
||||||
const chapterEndTime = this.currentChapter.end
|
|
||||||
const tolerance = 0.75
|
// Track chapter transitions by comparing current chapter with last chapter
|
||||||
if (time >= chapterEndTime - tolerance) {
|
if (this.lastChapterId !== this.currentChapter.id) {
|
||||||
this.sleepTimerEnd()
|
// Chapter changed - if we had a previous chapter, this means we crossed a boundary
|
||||||
|
if (this.lastChapterId) {
|
||||||
|
this.sleepTimerEnd()
|
||||||
|
}
|
||||||
|
this.lastChapterId = this.currentChapter.id
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
sleepTimerEnd() {
|
sleepTimerEnd() {
|
||||||
@@ -301,7 +306,7 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (this.sleepTimerType === this.$constants.SleepTimerTypes.CHAPTER && this.sleepTimerSet) {
|
if (this.sleepTimerType === this.$constants.SleepTimerTypes.CHAPTER && this.sleepTimerSet) {
|
||||||
this.checkChapterEnd(time)
|
this.checkChapterEnd()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
setDuration(duration) {
|
setDuration(duration) {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="w-full h-full overflow-hidden overflow-y-auto px-2 sm:px-4 py-6 relative">
|
<div class="w-full h-full overflow-hidden overflow-y-auto px-2 sm:px-4 py-6 relative">
|
||||||
<div class="flex flex-col sm:flex-row mb-4">
|
<div class="flex flex-col sm:flex-row mb-4">
|
||||||
<div class="relative self-center">
|
<div class="relative self-center md:self-start">
|
||||||
<covers-preview-cover :src="$store.getters['globals/getLibraryItemCoverSrcById'](libraryItemId, libraryItemUpdatedAt, true)" :width="120" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
<covers-preview-cover :src="$store.getters['globals/getLibraryItemCoverSrcById'](libraryItemId, libraryItemUpdatedAt, true)" :width="120" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
||||||
|
|
||||||
<!-- book cover overlay -->
|
<!-- book cover overlay -->
|
||||||
@@ -36,7 +36,7 @@
|
|||||||
<ui-btn small @click="showLocalCovers = !showLocalCovers">{{ showLocalCovers ? $strings.ButtonHide : $strings.ButtonShow }}</ui-btn>
|
<ui-btn small @click="showLocalCovers = !showLocalCovers">{{ showLocalCovers ? $strings.ButtonHide : $strings.ButtonShow }}</ui-btn>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="showLocalCovers" class="flex items-center justify-center pb-2">
|
<div v-if="showLocalCovers" class="flex items-center justify-center flex-wrap pb-2">
|
||||||
<template v-for="localCoverFile in localCovers">
|
<template v-for="localCoverFile in localCovers">
|
||||||
<div :key="localCoverFile.ino" class="m-0.5 mb-5 border-2 border-transparent hover:border-yellow-300 cursor-pointer" :class="localCoverFile.metadata.path === coverPath ? 'border-yellow-300' : ''" @click="setCover(localCoverFile)">
|
<div :key="localCoverFile.ino" class="m-0.5 mb-5 border-2 border-transparent hover:border-yellow-300 cursor-pointer" :class="localCoverFile.metadata.path === coverPath ? 'border-yellow-300' : ''" @click="setCover(localCoverFile)">
|
||||||
<div class="h-24 bg-primary" :style="{ width: 96 / bookCoverAspectRatio + 'px' }">
|
<div class="h-24 bg-primary" :style="{ width: 96 / bookCoverAspectRatio + 'px' }">
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
const pkg = require('./package.json')
|
const pkg = require('./package.json')
|
||||||
|
|
||||||
const routerBasePath = process.env.ROUTER_BASE_PATH || '/audiobookshelf'
|
const routerBasePath = process.env.ROUTER_BASE_PATH ?? '/audiobookshelf'
|
||||||
const serverHostUrl = process.env.NODE_ENV === 'production' ? '' : 'http://localhost:3333'
|
const serverHostUrl = process.env.NODE_ENV === 'production' ? '' : 'http://localhost:3333'
|
||||||
const serverPaths = ['api/', 'public/', 'hls/', 'auth/', 'feed/', 'status', 'login', 'logout', 'init']
|
const serverPaths = ['api/', 'public/', 'hls/', 'auth/', 'feed/', 'status', 'login', 'logout', 'init']
|
||||||
const proxy = Object.fromEntries(serverPaths.map((path) => [`${routerBasePath}/${path}`, { target: process.env.NODE_ENV !== 'production' ? serverHostUrl : '/' }]))
|
const proxy = Object.fromEntries(serverPaths.map((path) => [`${routerBasePath}/${path}`, { target: process.env.NODE_ENV !== 'production' ? serverHostUrl : '/' }]))
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookshelf-client",
|
"name": "audiobookshelf-client",
|
||||||
"version": "2.19.0",
|
"version": "2.19.4",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "audiobookshelf-client",
|
"name": "audiobookshelf-client",
|
||||||
"version": "2.19.0",
|
"version": "2.19.4",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nuxtjs/axios": "^5.13.6",
|
"@nuxtjs/axios": "^5.13.6",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookshelf-client",
|
"name": "audiobookshelf-client",
|
||||||
"version": "2.19.0",
|
"version": "2.19.4",
|
||||||
"buildNumber": 1,
|
"buildNumber": 1,
|
||||||
"description": "Self-hosted audiobook and podcast client",
|
"description": "Self-hosted audiobook and podcast client",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
|
|||||||
@@ -137,7 +137,16 @@ export default {
|
|||||||
this.$toast.error(this.$strings.ToastFailedToLoadData)
|
this.$toast.error(this.$strings.ToastFailedToLoadData)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.feeds = data.feeds
|
this.feeds = data.feeds.map((feed) => ({
|
||||||
|
...feed,
|
||||||
|
episodes: [...feed.episodes].sort((a, b) => {
|
||||||
|
if (!a.pubDate) return 1 // null dates sort to end
|
||||||
|
if (!b.pubDate) return -1
|
||||||
|
const dateA = new Date(a.pubDate)
|
||||||
|
const dateB = new Date(b.pubDate)
|
||||||
|
return dateA - dateB
|
||||||
|
})
|
||||||
|
}))
|
||||||
},
|
},
|
||||||
init() {
|
init() {
|
||||||
this.loadFeeds()
|
this.loadFeeds()
|
||||||
|
|||||||
+100
-2
@@ -10,6 +10,7 @@
|
|||||||
"ButtonApplyChapters": "Ужыць раздзелы",
|
"ButtonApplyChapters": "Ужыць раздзелы",
|
||||||
"ButtonAuthors": "Аўтары",
|
"ButtonAuthors": "Аўтары",
|
||||||
"ButtonBack": "Назад",
|
"ButtonBack": "Назад",
|
||||||
|
"ButtonBatchEditPopulateFromExisting": "Запоўніць з існуючага",
|
||||||
"ButtonBrowseForFolder": "Знайсці тэчку",
|
"ButtonBrowseForFolder": "Знайсці тэчку",
|
||||||
"ButtonCancel": "Адмяніць",
|
"ButtonCancel": "Адмяніць",
|
||||||
"ButtonCancelEncode": "Адмяніць кадзіраванне",
|
"ButtonCancelEncode": "Адмяніць кадзіраванне",
|
||||||
@@ -35,14 +36,18 @@
|
|||||||
"ButtonForceReScan": "Прымусовае паўторнае сканаванне",
|
"ButtonForceReScan": "Прымусовае паўторнае сканаванне",
|
||||||
"ButtonFullPath": "Поўны шлях",
|
"ButtonFullPath": "Поўны шлях",
|
||||||
"ButtonHide": "Схаваць",
|
"ButtonHide": "Схаваць",
|
||||||
|
"ButtonHome": "Галоўная",
|
||||||
"ButtonIssues": "Праблемы",
|
"ButtonIssues": "Праблемы",
|
||||||
"ButtonJumpBackward": "Перайсці назад",
|
"ButtonJumpBackward": "Перайсці назад",
|
||||||
"ButtonJumpForward": "Перайсці наперад",
|
"ButtonJumpForward": "Перайсці наперад",
|
||||||
|
"ButtonLatest": "Апошняе",
|
||||||
"ButtonLibrary": "Бібліятэка",
|
"ButtonLibrary": "Бібліятэка",
|
||||||
"ButtonLogout": "Выйсці",
|
"ButtonLogout": "Выйсці",
|
||||||
"ButtonLookup": "",
|
"ButtonLookup": "",
|
||||||
|
"ButtonManageTracks": "Кіраванне дарожкамі",
|
||||||
"ButtonMapChapterTitles": "Супаставіць назвы раздзелаў",
|
"ButtonMapChapterTitles": "Супаставіць назвы раздзелаў",
|
||||||
"ButtonMatchAllAuthors": "Супадзенне ўсіх аўтараў",
|
"ButtonMatchAllAuthors": "Супадзенне ўсіх аўтараў",
|
||||||
|
"ButtonMatchBooks": "Падбор кніг",
|
||||||
"ButtonNevermind": "Няважна",
|
"ButtonNevermind": "Няважна",
|
||||||
"ButtonNext": "Далей",
|
"ButtonNext": "Далей",
|
||||||
"ButtonNextChapter": "Наступны раздзел",
|
"ButtonNextChapter": "Наступны раздзел",
|
||||||
@@ -71,6 +76,9 @@
|
|||||||
"ButtonRemove": "Выдаліць",
|
"ButtonRemove": "Выдаліць",
|
||||||
"ButtonRemoveAll": "Выдаліць усе",
|
"ButtonRemoveAll": "Выдаліць усе",
|
||||||
"ButtonRemoveAllLibraryItems": "Выдаліць усе элементы бібліятэкі",
|
"ButtonRemoveAllLibraryItems": "Выдаліць усе элементы бібліятэкі",
|
||||||
|
"ButtonRemoveFromContinueListening": "Выдаліць з Працягваць слухаць",
|
||||||
|
"ButtonRemoveFromContinueReading": "Выдаліць з Працягваць чытанне",
|
||||||
|
"ButtonRemoveSeriesFromContinueSeries": "Выдаліць серыю з Працягваць серыю",
|
||||||
"ButtonReset": "Скінуць",
|
"ButtonReset": "Скінуць",
|
||||||
"ButtonResetToDefault": "Скінуць па змаўчанні",
|
"ButtonResetToDefault": "Скінуць па змаўчанні",
|
||||||
"ButtonRestore": "Аднавіць",
|
"ButtonRestore": "Аднавіць",
|
||||||
@@ -100,9 +108,14 @@
|
|||||||
"ButtonUserEdit": "Рэдагаваць карыстальніка {0}",
|
"ButtonUserEdit": "Рэдагаваць карыстальніка {0}",
|
||||||
"ButtonViewAll": "Прагледзець усе",
|
"ButtonViewAll": "Прагледзець усе",
|
||||||
"ButtonYes": "Так",
|
"ButtonYes": "Так",
|
||||||
|
"ErrorUploadFetchMetadataAPI": "Памылка пры атрыманні метададзеных",
|
||||||
|
"ErrorUploadFetchMetadataNoResults": "Не ўдалося атрымаць метададзеныя – паспрабуйце абнавіць назву і/або аўтара",
|
||||||
|
"ErrorUploadLacksTitle": "Павінна быць назва",
|
||||||
"HeaderAccount": "Уліковы запіс",
|
"HeaderAccount": "Уліковы запіс",
|
||||||
"HeaderAddCustomMetadataProvider": "Дадаць карыстальніцкага пастаўшчыка метаданных",
|
"HeaderAddCustomMetadataProvider": "Дадаць карыстальніцкага пастаўшчыка метаданных",
|
||||||
|
"HeaderAdvanced": "Дадаткова",
|
||||||
"HeaderAppriseNotificationSettings": "Налады апавяшчэнняў Apprise",
|
"HeaderAppriseNotificationSettings": "Налады апавяшчэнняў Apprise",
|
||||||
|
"HeaderAudioTracks": "Аўдыядарожкі",
|
||||||
"HeaderAudiobookTools": "Сродкі кіравання файламі аўдыякніг",
|
"HeaderAudiobookTools": "Сродкі кіравання файламі аўдыякніг",
|
||||||
"HeaderAuthentication": "Аўтэнтыфікацыя",
|
"HeaderAuthentication": "Аўтэнтыфікацыя",
|
||||||
"HeaderBackups": "Рэзервовыя копіі",
|
"HeaderBackups": "Рэзервовыя копіі",
|
||||||
@@ -112,6 +125,91 @@
|
|||||||
"HeaderCollection": "Калекцыя",
|
"HeaderCollection": "Калекцыя",
|
||||||
"HeaderCollectionItems": "Элементы калекцыі",
|
"HeaderCollectionItems": "Элементы калекцыі",
|
||||||
"HeaderCover": "Вокладка",
|
"HeaderCover": "Вокладка",
|
||||||
"HeaderCurrentDownloads": "Бягучыя загрузкі",
|
"HeaderCurrentDownloads": "Бягучыя спампоўкі",
|
||||||
"HeaderCustomMessageOnLogin": "Карыстальніцкае паведамленне пры ўваходзе"
|
"HeaderCustomMessageOnLogin": "Карыстальніцкае паведамленне пры ўваходзе",
|
||||||
|
"HeaderCustomMetadataProviders": "Карыстальніцкія крыніцы метададзеных",
|
||||||
|
"HeaderDetails": "Падрабязнасці",
|
||||||
|
"HeaderDownloadQueue": "Чарга спамповак",
|
||||||
|
"HeaderEbookFiles": "Файлы электронных кніг",
|
||||||
|
"HeaderEmail": "Электронная пошта",
|
||||||
|
"HeaderEmailSettings": "Налады электроннай пошты",
|
||||||
|
"HeaderEpisodes": "Эпізоды",
|
||||||
|
"HeaderEreaderDevices": "Прылады для чытання",
|
||||||
|
"HeaderEreaderSettings": "Налады прылады для чытання",
|
||||||
|
"HeaderFiles": "Файлы",
|
||||||
|
"HeaderFindChapters": "Знайсці раздзелы",
|
||||||
|
"HeaderIgnoredFiles": "Ігнараваныя файлы",
|
||||||
|
"HeaderItemFiles": "Файлы элементаў",
|
||||||
|
"HeaderItemMetadataUtils": "Утыліты для метададзеных элементаў",
|
||||||
|
"HeaderLastListeningSession": "Апошні сеанс праслухоўвання",
|
||||||
|
"HeaderLatestEpisodes": "Апошнія эпізоды",
|
||||||
|
"HeaderLibraries": "Бібліятэкі",
|
||||||
|
"HeaderLibraryFiles": "Файлы бібліятэкі",
|
||||||
|
"HeaderLibraryStats": "Статыстыка бібліятэкі",
|
||||||
|
"HeaderListeningSessions": "Сеансы праслухоўвання",
|
||||||
|
"HeaderListeningStats": "Статыстыка праслухоўвання",
|
||||||
|
"HeaderLogin": "Уваход",
|
||||||
|
"HeaderLogs": "Журналы",
|
||||||
|
"HeaderManageGenres": "Кіраванне жанрамі",
|
||||||
|
"HeaderManageTags": "Кіраванне тэгамі",
|
||||||
|
"HeaderMapDetails": "Падрабязнасці адлюстравання",
|
||||||
|
"HeaderNewAccount": "Новы ўліковы запіс",
|
||||||
|
"HeaderNewLibrary": "Новая бібліятэка",
|
||||||
|
"HeaderNotificationCreate": "Стварыць апавяшчэнне",
|
||||||
|
"HeaderNotificationUpdate": "Абнавіць апавяшчэнне",
|
||||||
|
"HeaderNotifications": "Апавяшчэнні",
|
||||||
|
"HeaderOpenListeningSessions": "Адкрыць сеансы праслухоўвання",
|
||||||
|
"HeaderScheduleEpisodeDownloads": "Расклад аўтаматычных спамповак эпізодаў",
|
||||||
|
"HeaderSettings": "Налады",
|
||||||
|
"HeaderSettingsDisplay": "Дысплей",
|
||||||
|
"HeaderSettingsExperimental": "Эксперыментальныя функцыі",
|
||||||
|
"HeaderSettingsGeneral": "Агульныя",
|
||||||
|
"HeaderSettingsScanner": "Сканер",
|
||||||
|
"HeaderSettingsWebClient": "Вэб-кліент",
|
||||||
|
"HeaderStatsTop10Authors": "10 лепшых аўтараў",
|
||||||
|
"HeaderStatsTop5Genres": "5 лепшых жанраў",
|
||||||
|
"HeaderTableOfContents": "Змест",
|
||||||
|
"HeaderTools": "Інструменты",
|
||||||
|
"HeaderUpdateAccount": "Абнавіць уліковы запіс",
|
||||||
|
"LabelAccountType": "Тып уліковага запіса",
|
||||||
|
"LabelAccountTypeAdmin": "Адміністратар",
|
||||||
|
"LabelAccountTypeGuest": "Госць",
|
||||||
|
"LabelAccountTypeUser": "Карыстальнік",
|
||||||
|
"LabelAudioBitrate": "Бітрэйт аўдыё (напрыклад, 128к)",
|
||||||
|
"LabelAudioChannels": "Аўдыёканалы (1 або 2)",
|
||||||
|
"LabelAudioCodec": "Аўдыёкодэк",
|
||||||
|
"LabelAutoDownloadEpisodes": "Аўтаматычнае спампаванне эпізодаў",
|
||||||
|
"LabelBackupAudioFiles": "Рэзервовае капіраванне аўдыёфайлаў",
|
||||||
|
"LabelContinueListening": "Працягваць слухаць",
|
||||||
|
"LabelDownload": "Спампаваць",
|
||||||
|
"LabelDownloadNEpisodes": "Спампована {0} эпізодаў",
|
||||||
|
"LabelDownloadable": "Спампоўваецца",
|
||||||
|
"LabelEncodingBackupLocation": "Рэзервовая копія вашых арыгінальных аўдыёфайлаў будзе захавана ў:",
|
||||||
|
"LabelEncodingChaptersNotEmbedded": "Раздзелы не ўбудаваны ў шматдарожкавыя аўдыякнігі.",
|
||||||
|
"LabelEncodingFinishedM4B": "Гатовы файл M4B будзе змешчаны ў вашу тэчку з аўдыякнігамі па адрасе:",
|
||||||
|
"LabelEncodingInfoEmbedded": "Метаданыя будуць убудаваны ў аўдыядарожкі ўнутры вашай тэчкі з аўдыякнігамі.",
|
||||||
|
"LabelMaxEpisodesToDownload": "Максімальная колькасць эпізодаў для спампоўкі. Выкарыстоўвайце 0 для неабмежаванай колькасці.",
|
||||||
|
"LabelMaxEpisodesToDownloadPerCheck": "Максімальная колькасць новых эпізодаў для спампоўкі за праверку",
|
||||||
|
"LabelMaxEpisodesToKeepHelp": "Значэнне 0 не ўстанаўлівае максімальнага абмежавання. Пасля аўтаматычнай спампоўкі новага эпізоду будзе выдалены самы стары эпізод, калі ў вас больш за X эпізодаў. Пры кожнай новай спампоўцы будзе выдаляцца толькі 1 эпізод.",
|
||||||
|
"LabelPermissionsDownload": "Можна спампаваць",
|
||||||
|
"LabelReAddSeriesToContinueListening": "Дадаць серыю зноў у Працягваць слухаць",
|
||||||
|
"LabelShareDownloadableHelp": "Дазваляе карыстальнікам, якія маюць спасылку на доступ, спампаваць ZIP-файл элемента бібліятэкі.",
|
||||||
|
"LabelStatsAudioTracks": "Аўдыядарожкі",
|
||||||
|
"LabelTracks": "Дарожкі",
|
||||||
|
"MessageConfirmRemoveListeningSessions": "Вы ўпэўнены, што жадаеце выдаліць {0} сеансаў праслухоўвання?",
|
||||||
|
"MessageDownloadingEpisode": "Спампоўка эпізоду",
|
||||||
|
"MessageEpisodesQueuedForDownload": "{0} эпізод(аў) у чарзе для спампоўкі",
|
||||||
|
"MessageNoDownloadsInProgress": "Зараз няма актыўных спамповак",
|
||||||
|
"MessageNoDownloadsQueued": "Няма спамповак у чарзе",
|
||||||
|
"MessageNoListeningSessions": "Няма сеансаў праслухоўвання",
|
||||||
|
"MessageTaskDownloadingEpisodeDescription": "Спампоўка эпізоду \"{0}\"",
|
||||||
|
"NotificationOnEpisodeDownloadedDescription": "Выклікаецца, калі эпізод падкаста аўтаматычна спампоўваецца",
|
||||||
|
"ToastAccountUpdateSuccess": "Уліковы запіс абноўлены",
|
||||||
|
"ToastEpisodeDownloadQueueClearFailed": "Не ўдалося ачысціць чаргу",
|
||||||
|
"ToastEpisodeDownloadQueueClearSuccess": "Чарга спампоўкі эпізодаў ачышчана",
|
||||||
|
"ToastInvalidMaxEpisodesToDownload": "Няправільная максімальная колькасць эпізодаў для спампоўкі",
|
||||||
|
"ToastNewUserCreatedFailed": "Не ўдалося стварыць уліковы запіс: \"{0}\"",
|
||||||
|
"ToastNewUserCreatedSuccess": "Новы ўліковы запіс створаны",
|
||||||
|
"ToastUserPasswordMustChange": "Новы пароль не можа супадаць са старым",
|
||||||
|
"ToastUserRootRequireName": "Неабходна ўвесці імя карыстальніка адміністратара"
|
||||||
}
|
}
|
||||||
|
|||||||
+186
-108
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"ButtonAdd": "Добави",
|
"ButtonAdd": "Създай",
|
||||||
"ButtonAddChapters": "Добави Глави",
|
"ButtonAddChapters": "Добави Глави",
|
||||||
"ButtonAddDevice": "Добави Устройство",
|
"ButtonAddDevice": "Добави Устройство",
|
||||||
"ButtonAddLibrary": "Добави Библиотека",
|
"ButtonAddLibrary": "Добави Библиотека",
|
||||||
@@ -10,15 +10,18 @@
|
|||||||
"ButtonApplyChapters": "Приложи Глави",
|
"ButtonApplyChapters": "Приложи Глави",
|
||||||
"ButtonAuthors": "Автори",
|
"ButtonAuthors": "Автори",
|
||||||
"ButtonBack": "Назад",
|
"ButtonBack": "Назад",
|
||||||
|
"ButtonBatchEditPopulateFromExisting": "Попълни от съществуващи",
|
||||||
|
"ButtonBatchEditPopulateMapDetails": "Попълни подробности за картата",
|
||||||
"ButtonBrowseForFolder": "Прегледай за папка",
|
"ButtonBrowseForFolder": "Прегледай за папка",
|
||||||
"ButtonCancel": "Откажи",
|
"ButtonCancel": "Отказ",
|
||||||
"ButtonCancelEncode": "Откажи закодирането",
|
"ButtonCancelEncode": "Откажи закодирането",
|
||||||
"ButtonChangeRootPassword": "Промени паролата за Root",
|
"ButtonChangeRootPassword": "Промени паролата за Root",
|
||||||
"ButtonCheckAndDownloadNewEpisodes": "Провери и Свали Нови Епизоди",
|
"ButtonCheckAndDownloadNewEpisodes": "Провери и Свали Нови Епизоди",
|
||||||
"ButtonChooseAFolder": "Избери Папка",
|
"ButtonChooseAFolder": "Избери Папка",
|
||||||
"ButtonChooseFiles": "Избери Файлове",
|
"ButtonChooseFiles": "Избери Файлове",
|
||||||
"ButtonClearFilter": "Изчисти Филтър",
|
"ButtonClearFilter": "Изчисти филтър",
|
||||||
"ButtonCloseFeed": "Затвори Feed",
|
"ButtonCloseFeed": "Затвори стената",
|
||||||
|
"ButtonCloseSession": "Затвори отворената сесия",
|
||||||
"ButtonCollections": "Колекции",
|
"ButtonCollections": "Колекции",
|
||||||
"ButtonConfigureScanner": "Конфигурирай Скенера",
|
"ButtonConfigureScanner": "Конфигурирай Скенера",
|
||||||
"ButtonCreate": "Създай",
|
"ButtonCreate": "Създай",
|
||||||
@@ -28,6 +31,9 @@
|
|||||||
"ButtonEdit": "Редактирай",
|
"ButtonEdit": "Редактирай",
|
||||||
"ButtonEditChapters": "Редактирай Глави",
|
"ButtonEditChapters": "Редактирай Глави",
|
||||||
"ButtonEditPodcast": "Редактирай Подкаст",
|
"ButtonEditPodcast": "Редактирай Подкаст",
|
||||||
|
"ButtonEnable": "Активирай",
|
||||||
|
"ButtonFireAndFail": "Задействай и неуспей",
|
||||||
|
"ButtonFireOnTest": "Задействай събитие onTest",
|
||||||
"ButtonForceReScan": "Принудително Пресканиране",
|
"ButtonForceReScan": "Принудително Пресканиране",
|
||||||
"ButtonFullPath": "Пълен Път",
|
"ButtonFullPath": "Пълен Път",
|
||||||
"ButtonHide": "Скрий",
|
"ButtonHide": "Скрий",
|
||||||
@@ -44,24 +50,31 @@
|
|||||||
"ButtonMatchAllAuthors": "Съвпадение на Всички Автори",
|
"ButtonMatchAllAuthors": "Съвпадение на Всички Автори",
|
||||||
"ButtonMatchBooks": "Съвпадение на Книги",
|
"ButtonMatchBooks": "Съвпадение на Книги",
|
||||||
"ButtonNevermind": "Няма значение",
|
"ButtonNevermind": "Няма значение",
|
||||||
|
"ButtonNext": "Следващо",
|
||||||
"ButtonNextChapter": "Следваща Глава",
|
"ButtonNextChapter": "Следваща Глава",
|
||||||
"ButtonOk": "Добре",
|
"ButtonNextItemInQueue": "Следващият елемент в опашката",
|
||||||
"ButtonOpenFeed": "Отвори Feed",
|
"ButtonOk": "Приемам",
|
||||||
|
"ButtonOpenFeed": "Отвори стената",
|
||||||
"ButtonOpenManager": "Отвори Мениджър",
|
"ButtonOpenManager": "Отвори Мениджър",
|
||||||
"ButtonPause": "Пауза",
|
"ButtonPause": "Паузирай",
|
||||||
"ButtonPlay": "Пусни",
|
"ButtonPlay": "Пусни",
|
||||||
|
"ButtonPlayAll": "Пусни всички",
|
||||||
"ButtonPlaying": "Пуска се",
|
"ButtonPlaying": "Пуска се",
|
||||||
"ButtonPlaylists": "Плейлисти",
|
"ButtonPlaylists": "Плейлисти",
|
||||||
|
"ButtonPrevious": "Предишен",
|
||||||
"ButtonPreviousChapter": "Предишна Глава",
|
"ButtonPreviousChapter": "Предишна Глава",
|
||||||
|
"ButtonProbeAudioFile": "Провери аудио файла",
|
||||||
"ButtonPurgeAllCache": "Изчисти Всички Кешове",
|
"ButtonPurgeAllCache": "Изчисти Всички Кешове",
|
||||||
"ButtonPurgeItemsCache": "Изчисти Кеша на Елементи",
|
"ButtonPurgeItemsCache": "Изчисти Кеша на Елементи",
|
||||||
"ButtonQueueAddItem": "Добави към опашката",
|
"ButtonQueueAddItem": "Добави към опашката",
|
||||||
"ButtonQueueRemoveItem": "Премахни от опашката",
|
"ButtonQueueRemoveItem": "Премахни от опашката",
|
||||||
|
"ButtonQuickEmbed": "Бързо вграждане",
|
||||||
|
"ButtonQuickEmbedMetadata": "Бързо вграждане метадата",
|
||||||
"ButtonQuickMatch": "Бързо Съпоставяне",
|
"ButtonQuickMatch": "Бързо Съпоставяне",
|
||||||
"ButtonReScan": "Пресканирай",
|
"ButtonReScan": "Пресканирай",
|
||||||
"ButtonRead": "Прочети",
|
"ButtonRead": "Прочети",
|
||||||
"ButtonReadLess": "Покажи по-малко",
|
"ButtonReadLess": "Изчети по-малко",
|
||||||
"ButtonReadMore": "Покажи повече",
|
"ButtonReadMore": "Прочети дълго",
|
||||||
"ButtonRefresh": "Обнови",
|
"ButtonRefresh": "Обнови",
|
||||||
"ButtonRemove": "Премахни",
|
"ButtonRemove": "Премахни",
|
||||||
"ButtonRemoveAll": "Премахни Всички",
|
"ButtonRemoveAll": "Премахни Всички",
|
||||||
@@ -77,7 +90,9 @@
|
|||||||
"ButtonSaveTracklist": "Запази Списък с Канали",
|
"ButtonSaveTracklist": "Запази Списък с Канали",
|
||||||
"ButtonScan": "Сканирай",
|
"ButtonScan": "Сканирай",
|
||||||
"ButtonScanLibrary": "Сканирай Библиотека",
|
"ButtonScanLibrary": "Сканирай Библиотека",
|
||||||
"ButtonSearch": "Търси",
|
"ButtonScrollLeft": "Скролни наляво",
|
||||||
|
"ButtonScrollRight": "Скролни надясно",
|
||||||
|
"ButtonSearch": "Търси в",
|
||||||
"ButtonSelectFolderPath": "Избери Път на Папка",
|
"ButtonSelectFolderPath": "Избери Път на Папка",
|
||||||
"ButtonSeries": "Серии",
|
"ButtonSeries": "Серии",
|
||||||
"ButtonSetChaptersFromTracks": "Задай Глави от Песни",
|
"ButtonSetChaptersFromTracks": "Задай Глави от Песни",
|
||||||
@@ -86,8 +101,10 @@
|
|||||||
"ButtonShow": "Покажи",
|
"ButtonShow": "Покажи",
|
||||||
"ButtonStartM4BEncode": "Започни M4B Кодиране",
|
"ButtonStartM4BEncode": "Започни M4B Кодиране",
|
||||||
"ButtonStartMetadataEmbed": "Започни Вграждане на Метаданни",
|
"ButtonStartMetadataEmbed": "Започни Вграждане на Метаданни",
|
||||||
|
"ButtonStats": "Статистики",
|
||||||
"ButtonSubmit": "Изпрати",
|
"ButtonSubmit": "Изпрати",
|
||||||
"ButtonTest": "Тест",
|
"ButtonTest": "Тест",
|
||||||
|
"ButtonUnlinkOpenId": "Премахни връзката с OpenID",
|
||||||
"ButtonUpload": "Качи",
|
"ButtonUpload": "Качи",
|
||||||
"ButtonUploadBackup": "Качи Backup",
|
"ButtonUploadBackup": "Качи Backup",
|
||||||
"ButtonUploadCover": "Качи Корица",
|
"ButtonUploadCover": "Качи Корица",
|
||||||
@@ -100,9 +117,10 @@
|
|||||||
"ErrorUploadFetchMetadataNoResults": "Метаданните не могат да бъдат взети - опитайте да обновите заглавието и/или автора",
|
"ErrorUploadFetchMetadataNoResults": "Метаданните не могат да бъдат взети - опитайте да обновите заглавието и/или автора",
|
||||||
"ErrorUploadLacksTitle": "Трябва да има Заглавие",
|
"ErrorUploadLacksTitle": "Трябва да има Заглавие",
|
||||||
"HeaderAccount": "Профил",
|
"HeaderAccount": "Профил",
|
||||||
"HeaderAdvanced": "Разширени",
|
"HeaderAddCustomMetadataProvider": "Добави персонализиран доставчик на метаданни",
|
||||||
|
"HeaderAdvanced": "Разширени настройки",
|
||||||
"HeaderAppriseNotificationSettings": "Apprise Notification Опции",
|
"HeaderAppriseNotificationSettings": "Apprise Notification Опции",
|
||||||
"HeaderAudioTracks": "Звуков Канал",
|
"HeaderAudioTracks": "Песни",
|
||||||
"HeaderAudiobookTools": "Инструмент за Менижиране на Аудиокниги",
|
"HeaderAudiobookTools": "Инструмент за Менижиране на Аудиокниги",
|
||||||
"HeaderAuthentication": "Аутентикация",
|
"HeaderAuthentication": "Аутентикация",
|
||||||
"HeaderBackups": "Архив",
|
"HeaderBackups": "Архив",
|
||||||
@@ -110,26 +128,26 @@
|
|||||||
"HeaderChapters": "Глави",
|
"HeaderChapters": "Глави",
|
||||||
"HeaderChooseAFolder": "Избети Папка",
|
"HeaderChooseAFolder": "Избети Папка",
|
||||||
"HeaderCollection": "Колекция",
|
"HeaderCollection": "Колекция",
|
||||||
"HeaderCollectionItems": "Елементи на Колекция",
|
"HeaderCollectionItems": "Елемент в колекция",
|
||||||
"HeaderCover": "Корица",
|
"HeaderCover": "Корица",
|
||||||
"HeaderCurrentDownloads": "Текущи Сваляния",
|
"HeaderCurrentDownloads": "Текущи Сваляния",
|
||||||
"HeaderCustomMessageOnLogin": "Потребителско съобщение при влизане",
|
"HeaderCustomMessageOnLogin": "Потребителско съобщение при влизане",
|
||||||
"HeaderCustomMetadataProviders": "Потребителски Доставчици на Метаданни",
|
"HeaderCustomMetadataProviders": "Потребителски Доставчици на Метаданни",
|
||||||
"HeaderDetails": "Детайли",
|
"HeaderDetails": "Детайли",
|
||||||
"HeaderDownloadQueue": "Опашка за Сваляне",
|
"HeaderDownloadQueue": "Опашка за Сваляне",
|
||||||
"HeaderEbookFiles": "Файлове на Електронни книги",
|
"HeaderEbookFiles": "Е-книги файлове",
|
||||||
"HeaderEmail": "Емейл",
|
"HeaderEmail": "Емейл",
|
||||||
"HeaderEmailSettings": "Настройки Емайл",
|
"HeaderEmailSettings": "Настройки Емайл",
|
||||||
"HeaderEpisodes": "Епизоди",
|
"HeaderEpisodes": "Епизоди",
|
||||||
"HeaderEreaderDevices": "Елктронни Четци",
|
"HeaderEreaderDevices": "Елктронни Четци",
|
||||||
"HeaderEreaderSettings": "Настройки на Електронни Четци",
|
"HeaderEreaderSettings": "Настройки на Е-четецът",
|
||||||
"HeaderFiles": "Файлове",
|
"HeaderFiles": "Файлове",
|
||||||
"HeaderFindChapters": "Намери Глави",
|
"HeaderFindChapters": "Намери Глави",
|
||||||
"HeaderIgnoredFiles": "Игнорирани Файлове",
|
"HeaderIgnoredFiles": "Игнорирани Файлове",
|
||||||
"HeaderItemFiles": "Файлове на Елемент",
|
"HeaderItemFiles": "Файлове на Елемент",
|
||||||
"HeaderItemMetadataUtils": "Инструменти за Метаданни на Елемент",
|
"HeaderItemMetadataUtils": "Инструменти за Метаданни на Елемент",
|
||||||
"HeaderLastListeningSession": "Последна Сесия на Слушане",
|
"HeaderLastListeningSession": "Последна Сесия на Слушане",
|
||||||
"HeaderLatestEpisodes": "Последни Епизоди",
|
"HeaderLatestEpisodes": "Последни епизоди",
|
||||||
"HeaderLibraries": "Библиотеки",
|
"HeaderLibraries": "Библиотеки",
|
||||||
"HeaderLibraryFiles": "Файлове на Библиотека",
|
"HeaderLibraryFiles": "Файлове на Библиотека",
|
||||||
"HeaderLibraryStats": "Статистика на Библиотека",
|
"HeaderLibraryStats": "Статистика на Библиотека",
|
||||||
@@ -145,24 +163,29 @@
|
|||||||
"HeaderMetadataToEmbed": "Метаданни за Вграждане",
|
"HeaderMetadataToEmbed": "Метаданни за Вграждане",
|
||||||
"HeaderNewAccount": "Нов Профил",
|
"HeaderNewAccount": "Нов Профил",
|
||||||
"HeaderNewLibrary": "Нова Библиотека",
|
"HeaderNewLibrary": "Нова Библиотека",
|
||||||
|
"HeaderNotificationCreate": "Създай нотификация",
|
||||||
|
"HeaderNotificationUpdate": "Обнови нотификация",
|
||||||
"HeaderNotifications": "Известия",
|
"HeaderNotifications": "Известия",
|
||||||
"HeaderOpenIDConnectAuthentication": "OpenID Connect Аутентикация",
|
"HeaderOpenIDConnectAuthentication": "OpenID Connect Аутентикация",
|
||||||
"HeaderOpenRSSFeed": "Отвори RSS Feed",
|
"HeaderOpenListeningSessions": "Отвори сесия",
|
||||||
|
"HeaderOpenRSSFeed": "Отвори RSS емисията",
|
||||||
"HeaderOtherFiles": "Други Файлове",
|
"HeaderOtherFiles": "Други Файлове",
|
||||||
"HeaderPasswordAuthentication": "Паролна Аутентикация",
|
"HeaderPasswordAuthentication": "Паролна Аутентикация",
|
||||||
"HeaderPermissions": "Права",
|
"HeaderPermissions": "Права",
|
||||||
"HeaderPlayerQueue": "Опашка на Плейъра",
|
"HeaderPlayerQueue": "Опашка на Плейъра",
|
||||||
|
"HeaderPlayerSettings": "Настройки на плейъра",
|
||||||
"HeaderPlaylist": "Плейлист",
|
"HeaderPlaylist": "Плейлист",
|
||||||
"HeaderPlaylistItems": "Елементи на Плейлист",
|
"HeaderPlaylistItems": "Елементи от плейлист",
|
||||||
"HeaderPodcastsToAdd": "Подкасти за Добавяне",
|
"HeaderPodcastsToAdd": "Подкасти за Добавяне",
|
||||||
"HeaderPreviewCover": "Преглед на Корица",
|
"HeaderPreviewCover": "Преглед на Корица",
|
||||||
"HeaderRSSFeedGeneral": "RSS Детайли",
|
"HeaderRSSFeedGeneral": "RSS подробности",
|
||||||
"HeaderRSSFeedIsOpen": "RSS Feed е Отворен",
|
"HeaderRSSFeedIsOpen": "RSS емисията е отворена",
|
||||||
"HeaderRSSFeeds": "RSS Feed-ове",
|
"HeaderRSSFeeds": "RSS Feed-ове",
|
||||||
"HeaderRemoveEpisode": "Премахни Епизод",
|
"HeaderRemoveEpisode": "Премахни Епизод",
|
||||||
"HeaderRemoveEpisodes": "Премахни {0} Епизоди",
|
"HeaderRemoveEpisodes": "Премахни {0} Епизоди",
|
||||||
"HeaderSavedMediaProgress": "Запазен Прогрес на Медията",
|
"HeaderSavedMediaProgress": "Запазен Прогрес на Медията",
|
||||||
"HeaderSchedule": "График",
|
"HeaderSchedule": "График",
|
||||||
|
"HeaderScheduleEpisodeDownloads": "Планирай автоматично изтегляне на епизоди",
|
||||||
"HeaderScheduleLibraryScans": "График за Автоматично Сканиране на Библиотека",
|
"HeaderScheduleLibraryScans": "График за Автоматично Сканиране на Библиотека",
|
||||||
"HeaderSession": "Сесия",
|
"HeaderSession": "Сесия",
|
||||||
"HeaderSetBackupSchedule": "Задай График за Backup",
|
"HeaderSetBackupSchedule": "Задай График за Backup",
|
||||||
@@ -171,11 +194,12 @@
|
|||||||
"HeaderSettingsExperimental": "Експериментални Функции",
|
"HeaderSettingsExperimental": "Експериментални Функции",
|
||||||
"HeaderSettingsGeneral": "Общи",
|
"HeaderSettingsGeneral": "Общи",
|
||||||
"HeaderSettingsScanner": "Скенер",
|
"HeaderSettingsScanner": "Скенер",
|
||||||
"HeaderSleepTimer": "Таймер за Сън",
|
"HeaderSettingsWebClient": "Уеб клиент",
|
||||||
|
"HeaderSleepTimer": "Таймер за заспиване",
|
||||||
"HeaderStatsLargestItems": "Най-Големите Елементи",
|
"HeaderStatsLargestItems": "Най-Големите Елементи",
|
||||||
"HeaderStatsLongestItems": "Най-Дългите Елементи (часове)",
|
"HeaderStatsLongestItems": "Най-Дългите Елементи (часове)",
|
||||||
"HeaderStatsMinutesListeningChart": "Минути на Слушане (последни 7 дни)",
|
"HeaderStatsMinutesListeningChart": "Изслушани минути (последните 7 дни)",
|
||||||
"HeaderStatsRecentSessions": "Скорошни Сесии",
|
"HeaderStatsRecentSessions": "Последни сесии",
|
||||||
"HeaderStatsTop10Authors": "Топ 10 Автори",
|
"HeaderStatsTop10Authors": "Топ 10 Автори",
|
||||||
"HeaderStatsTop5Genres": "Топ 5 Жанрове",
|
"HeaderStatsTop5Genres": "Топ 5 Жанрове",
|
||||||
"HeaderTableOfContents": "Съдържание",
|
"HeaderTableOfContents": "Съдържание",
|
||||||
@@ -186,7 +210,7 @@
|
|||||||
"HeaderUpdateLibrary": "Обнови Библиотека",
|
"HeaderUpdateLibrary": "Обнови Библиотека",
|
||||||
"HeaderUsers": "Потребители",
|
"HeaderUsers": "Потребители",
|
||||||
"HeaderYearReview": "Преглед на {0} Година",
|
"HeaderYearReview": "Преглед на {0} Година",
|
||||||
"HeaderYourStats": "Твоята Статистика",
|
"HeaderYourStats": "Вашата статистика",
|
||||||
"LabelAbridged": "Съкратен",
|
"LabelAbridged": "Съкратен",
|
||||||
"LabelAbridgedChecked": "Съкратена (отбелязано)",
|
"LabelAbridgedChecked": "Съкратена (отбелязано)",
|
||||||
"LabelAbridgedUnchecked": "Несъкратена (не отбелязано)",
|
"LabelAbridgedUnchecked": "Несъкратена (не отбелязано)",
|
||||||
@@ -198,21 +222,26 @@
|
|||||||
"LabelActivity": "Дейност",
|
"LabelActivity": "Дейност",
|
||||||
"LabelAddToCollection": "Добави в Колекция",
|
"LabelAddToCollection": "Добави в Колекция",
|
||||||
"LabelAddToCollectionBatch": "Добави {0} Книги в Колекция",
|
"LabelAddToCollectionBatch": "Добави {0} Книги в Колекция",
|
||||||
"LabelAddToPlaylist": "Добави в Плейлист",
|
"LabelAddToPlaylist": "Добави в плейлист",
|
||||||
"LabelAddToPlaylistBatch": "Добави {0} Елемент в Плейлист",
|
"LabelAddToPlaylistBatch": "Добави {0} Елемент в Плейлист",
|
||||||
"LabelAddedAt": "Добавени На",
|
"LabelAddedAt": "Добавено в",
|
||||||
|
"LabelAddedDate": "Добавено",
|
||||||
"LabelAdminUsersOnly": "Само за Администратори",
|
"LabelAdminUsersOnly": "Само за Администратори",
|
||||||
"LabelAll": "Всички",
|
"LabelAll": "Всичко",
|
||||||
"LabelAllUsers": "Всички Потребители",
|
"LabelAllUsers": "Всички Потребители",
|
||||||
"LabelAllUsersExcludingGuests": "Всички потребители без гости",
|
"LabelAllUsersExcludingGuests": "Всички потребители без гости",
|
||||||
"LabelAllUsersIncludingGuests": "Всички потребители включително гости",
|
"LabelAllUsersIncludingGuests": "Всички потребители включително гости",
|
||||||
"LabelAlreadyInYourLibrary": "Вече е в твоята библиотека",
|
"LabelAlreadyInYourLibrary": "Вече е в твоята библиотека",
|
||||||
|
"LabelApiToken": "АПИ Токен",
|
||||||
"LabelAppend": "Добави",
|
"LabelAppend": "Добави",
|
||||||
|
"LabelAudioBitrate": "Аудио битрейт (напр. 128k)",
|
||||||
|
"LabelAudioChannels": "Аудио канали (1 или 2)",
|
||||||
|
"LabelAudioCodec": "Аудио кодек",
|
||||||
"LabelAuthor": "Автор",
|
"LabelAuthor": "Автор",
|
||||||
"LabelAuthorFirstLast": "Автор (Първо Име, Фамилия)",
|
"LabelAuthorFirstLast": "Автор (Първи, Последен)",
|
||||||
"LabelAuthorLastFirst": "Автор (Фамилия, Първо Име)",
|
"LabelAuthorLastFirst": "Автор (Последен, Първи)",
|
||||||
"LabelAuthors": "Автори",
|
"LabelAuthors": "Автори",
|
||||||
"LabelAutoDownloadEpisodes": "Автоматично Сваляне на Епизоди",
|
"LabelAutoDownloadEpisodes": "Автоматично изтегляне на епизоди",
|
||||||
"LabelAutoFetchMetadata": "Автоматично Взимане на Метаданни",
|
"LabelAutoFetchMetadata": "Автоматично Взимане на Метаданни",
|
||||||
"LabelAutoFetchMetadataHelp": "Взима метаданни за заглвие, автор и серии за да опрости качването. Допълнителни метаданни може да трябва да бъде взера след качване.",
|
"LabelAutoFetchMetadataHelp": "Взима метаданни за заглвие, автор и серии за да опрости качването. Допълнителни метаданни може да трябва да бъде взера след качване.",
|
||||||
"LabelAutoLaunch": "Автоматично Стартиране",
|
"LabelAutoLaunch": "Автоматично Стартиране",
|
||||||
@@ -220,6 +249,7 @@
|
|||||||
"LabelAutoRegister": "Автоматична Регистрация",
|
"LabelAutoRegister": "Автоматична Регистрация",
|
||||||
"LabelAutoRegisterDescription": "Автоматично създаване на нови потребители след вход",
|
"LabelAutoRegisterDescription": "Автоматично създаване на нови потребители след вход",
|
||||||
"LabelBackToUser": "Обратно към Потребител",
|
"LabelBackToUser": "Обратно към Потребител",
|
||||||
|
"LabelBackupAudioFiles": "Създай резервно копие на аудио файлове",
|
||||||
"LabelBackupLocation": "Местоположение на Архив",
|
"LabelBackupLocation": "Местоположение на Архив",
|
||||||
"LabelBackupsEnableAutomaticBackups": "Включи автоматично архивиране",
|
"LabelBackupsEnableAutomaticBackups": "Включи автоматично архивиране",
|
||||||
"LabelBackupsEnableAutomaticBackupsHelp": "Архиви запазени в /metadata/backups",
|
"LabelBackupsEnableAutomaticBackupsHelp": "Архиви запазени в /metadata/backups",
|
||||||
@@ -228,31 +258,38 @@
|
|||||||
"LabelBackupsNumberToKeep": "Брой архиви за запазване",
|
"LabelBackupsNumberToKeep": "Брой архиви за запазване",
|
||||||
"LabelBackupsNumberToKeepHelp": "Само 1 архив ще бъде премахнат на веднъж, така че ако вече имате повече архиви от това трябва да ги премахнете ръчно.",
|
"LabelBackupsNumberToKeepHelp": "Само 1 архив ще бъде премахнат на веднъж, така че ако вече имате повече архиви от това трябва да ги премахнете ръчно.",
|
||||||
"LabelBitrate": "Битрейт",
|
"LabelBitrate": "Битрейт",
|
||||||
|
"LabelBonus": "Бонус",
|
||||||
"LabelBooks": "Книги",
|
"LabelBooks": "Книги",
|
||||||
"LabelButtonText": "Текст на Бутон",
|
"LabelButtonText": "Текст на Бутон",
|
||||||
|
"LabelByAuthor": "от {0}",
|
||||||
"LabelChangePassword": "Промени Парола",
|
"LabelChangePassword": "Промени Парола",
|
||||||
"LabelChannels": "Канали",
|
"LabelChannels": "Канали",
|
||||||
|
"LabelChapterCount": "{0} Глави",
|
||||||
"LabelChapterTitle": "Заглавие на Глава",
|
"LabelChapterTitle": "Заглавие на Глава",
|
||||||
"LabelChapters": "Глави",
|
"LabelChapters": "Глави",
|
||||||
"LabelChaptersFound": "намерени глави",
|
"LabelChaptersFound": "намерени глави",
|
||||||
"LabelClickForMoreInfo": "Кликни за повече информация",
|
"LabelClickForMoreInfo": "Кликни за повече информация",
|
||||||
"LabelClosePlayer": "Затвори Плейъра",
|
"LabelClickToUseCurrentValue": "Натисни да ползваш сегашната стойност",
|
||||||
|
"LabelClosePlayer": "Затвори",
|
||||||
"LabelCodec": "Кодек",
|
"LabelCodec": "Кодек",
|
||||||
"LabelCollapseSeries": "Свий Серия",
|
"LabelCollapseSeries": "Скрий сериите",
|
||||||
|
"LabelCollapseSubSeries": "Свий подсерии",
|
||||||
"LabelCollection": "Колекция",
|
"LabelCollection": "Колекция",
|
||||||
"LabelCollections": "Колекции",
|
"LabelCollections": "Колекции",
|
||||||
"LabelComplete": "Завършено",
|
"LabelComplete": "Приключено",
|
||||||
"LabelConfirmPassword": "Потвърди Парола",
|
"LabelConfirmPassword": "Потвърди Парола",
|
||||||
"LabelContinueListening": "Продължи Слушане",
|
"LabelContinueListening": "Продължи слушане",
|
||||||
"LabelContinueReading": "Продължи Четене",
|
"LabelContinueReading": "Продължи четене",
|
||||||
"LabelContinueSeries": "Продължи Серия",
|
"LabelContinueSeries": "Продължи серии",
|
||||||
"LabelCover": "Корица",
|
"LabelCover": "Корица",
|
||||||
"LabelCoverImageURL": "URL на Корица",
|
"LabelCoverImageURL": "URL на Корица",
|
||||||
"LabelCreatedAt": "Създадено на",
|
"LabelCreatedAt": "Създадено на",
|
||||||
|
"LabelCronExpression": "Cron израз",
|
||||||
"LabelCurrent": "Текущо",
|
"LabelCurrent": "Текущо",
|
||||||
"LabelCurrently": "Текущо:",
|
"LabelCurrently": "Текущо:",
|
||||||
"LabelCustomCronExpression": "Потребителски Cron Expression:",
|
"LabelCustomCronExpression": "Потребителски Cron Expression:",
|
||||||
"LabelDatetime": "Дата и Време",
|
"LabelDatetime": "Дата и Време",
|
||||||
|
"LabelDays": "Дни",
|
||||||
"LabelDeleteFromFileSystemCheckbox": "Изтрий от файловата система (отмени за да бъдат премахни само от базата данни)",
|
"LabelDeleteFromFileSystemCheckbox": "Изтрий от файловата система (отмени за да бъдат премахни само от базата данни)",
|
||||||
"LabelDescription": "Описание",
|
"LabelDescription": "Описание",
|
||||||
"LabelDeselectAll": "Премахни всички",
|
"LabelDeselectAll": "Премахни всички",
|
||||||
@@ -263,16 +300,18 @@
|
|||||||
"LabelDiscFromFilename": "Диск от Име на Файл",
|
"LabelDiscFromFilename": "Диск от Име на Файл",
|
||||||
"LabelDiscFromMetadata": "Диск от Метаданни",
|
"LabelDiscFromMetadata": "Диск от Метаданни",
|
||||||
"LabelDiscover": "Открий",
|
"LabelDiscover": "Открий",
|
||||||
"LabelDownload": "Сваляне",
|
"LabelDownload": "Свали",
|
||||||
"LabelDownloadNEpisodes": "Свали {0} епизоди",
|
"LabelDownloadNEpisodes": "Свали {0} епизоди",
|
||||||
|
"LabelDownloadable": "Може да се изтегли",
|
||||||
"LabelDuration": "Продължителност",
|
"LabelDuration": "Продължителност",
|
||||||
"LabelDurationComparisonExactMatch": "(точно съвпадение)",
|
"LabelDurationComparisonExactMatch": "(точно съвпадение)",
|
||||||
"LabelDurationComparisonLonger": "({0} по-дълго)",
|
"LabelDurationComparisonLonger": "({0} по-дълго)",
|
||||||
"LabelDurationComparisonShorter": "({0} по-късо)",
|
"LabelDurationComparisonShorter": "({0} по-късо)",
|
||||||
"LabelDurationFound": "Намерена продължителност:",
|
"LabelDurationFound": "Намерена продължителност:",
|
||||||
"LabelEbook": "Електронна книга",
|
"LabelEbook": "Е-Книга",
|
||||||
"LabelEbooks": "Електронни книги",
|
"LabelEbooks": "Е-книги",
|
||||||
"LabelEdit": "Редакция",
|
"LabelEdit": "Редакция",
|
||||||
|
"LabelEmail": "Имейл",
|
||||||
"LabelEmailSettingsFromAddress": "От Адрес",
|
"LabelEmailSettingsFromAddress": "От Адрес",
|
||||||
"LabelEmailSettingsRejectUnauthorized": "Отхвърли неавторизирани сертификати",
|
"LabelEmailSettingsRejectUnauthorized": "Отхвърли неавторизирани сертификати",
|
||||||
"LabelEmailSettingsRejectUnauthorizedHelp": "Спирането на валидацията на SSL сертификате може да изложи връзката ви на рискове, като man-in-the-middle атака. Спираите тази опция само ако знете имоликацийте от това и се доверявате на mail сървъра към който се свързвате.",
|
"LabelEmailSettingsRejectUnauthorizedHelp": "Спирането на валидацията на SSL сертификате може да изложи връзката ви на рискове, като man-in-the-middle атака. Спираите тази опция само ако знете имоликацийте от това и се доверявате на mail сървъра към който се свързвате.",
|
||||||
@@ -280,41 +319,53 @@
|
|||||||
"LabelEmailSettingsSecureHelp": "Ако е вярно възката ще изполва TLS когате се свързва със сървъра. Ако не е то TLS ще се използва ако сървъра поддържа разширението STARTTLS. В повечето случаи задайте тази стойност на истина ако се свързвате към порт 465. За порт 587 или 25 оставете я на лъжа. (от nodemailer.com/smtp/#authentication)",
|
"LabelEmailSettingsSecureHelp": "Ако е вярно възката ще изполва TLS когате се свързва със сървъра. Ако не е то TLS ще се използва ако сървъра поддържа разширението STARTTLS. В повечето случаи задайте тази стойност на истина ако се свързвате към порт 465. За порт 587 или 25 оставете я на лъжа. (от nodemailer.com/smtp/#authentication)",
|
||||||
"LabelEmailSettingsTestAddress": "Тестов Адрес",
|
"LabelEmailSettingsTestAddress": "Тестов Адрес",
|
||||||
"LabelEmbeddedCover": "Вградена Корица",
|
"LabelEmbeddedCover": "Вградена Корица",
|
||||||
"LabelEnable": "Включи",
|
"LabelEnable": "Активирай",
|
||||||
|
"LabelEncodingBackupLocation": "Резервно копие на вашите оригинални аудио файлове ще бъде съхранено в:",
|
||||||
|
"LabelEncodingChaptersNotEmbedded": "Главите не са вградени в аудиокнигите с множество тракове.",
|
||||||
|
"LabelEncodingClearItemCache": "Уверете се, че периодично изчиствате кеша на елементите.",
|
||||||
|
"LabelEncodingFinishedM4B": "Завършеният M4B файл ще бъде поставен в папката на вашите аудиокниги на:",
|
||||||
|
"LabelEncodingInfoEmbedded": "Метаданните ще бъдат вградени в аудио траковете в папката на вашите аудиокниги.",
|
||||||
"LabelEnd": "Край",
|
"LabelEnd": "Край",
|
||||||
|
"LabelEndOfChapter": "Край на глава",
|
||||||
"LabelEpisode": "Епизод",
|
"LabelEpisode": "Епизод",
|
||||||
"LabelEpisodeTitle": "Заглавие на Епизод",
|
"LabelEpisodeTitle": "Заглавие на Епизод",
|
||||||
"LabelEpisodeType": "Тип на Епизод",
|
"LabelEpisodeType": "Тип на Епизод",
|
||||||
"LabelExample": "Пример",
|
"LabelExample": "Пример",
|
||||||
"LabelExplicit": "Експлицитно",
|
"LabelExpandSeries": "Покажи сериите",
|
||||||
|
"LabelExpandSubSeries": "Покажи съб сериите",
|
||||||
|
"LabelExplicit": "С нецензурно съдържание",
|
||||||
|
"LabelExplicitChecked": "С нецензурно съдържание (проверено)",
|
||||||
|
"LabelExplicitUnchecked": "Без нецензурно съдържание (непроверено)",
|
||||||
|
"LabelExportOPML": "Експортирай OPML",
|
||||||
|
"LabelFeedURL": "URL на емисия",
|
||||||
"LabelFetchingMetadata": "Взимане на Метаданни",
|
"LabelFetchingMetadata": "Взимане на Метаданни",
|
||||||
"LabelFile": "Файл",
|
"LabelFile": "Файл",
|
||||||
"LabelFileBirthtime": "Дата на създаване на файла",
|
"LabelFileBirthtime": "Дата на създаване на файла",
|
||||||
"LabelFileModified": "Файлът променен",
|
"LabelFileModified": "Дата на модификация на файла",
|
||||||
"LabelFilename": "Име на Файл",
|
"LabelFilename": "Име на файла",
|
||||||
"LabelFilterByUser": "Филтриране по Потребител",
|
"LabelFilterByUser": "Филтриране по Потребител",
|
||||||
"LabelFindEpisodes": "Намери Епизоди",
|
"LabelFindEpisodes": "Намери Епизоди",
|
||||||
"LabelFinished": "Завършено",
|
"LabelFinished": "Дата на приключване",
|
||||||
"LabelFolder": "Папка",
|
"LabelFolder": "Папка",
|
||||||
"LabelFolders": "Папки",
|
"LabelFolders": "Папки",
|
||||||
"LabelFontBold": "Получерно",
|
"LabelFontBold": "Получерно",
|
||||||
"LabelFontBoldness": "Плътност на шрифта",
|
"LabelFontBoldness": "Дебелина на шрифта",
|
||||||
"LabelFontFamily": "Шрифт",
|
"LabelFontFamily": "Шрифт",
|
||||||
"LabelFontItalic": "Курсив",
|
"LabelFontItalic": "Курсив",
|
||||||
"LabelFontScale": "Мащаб на Шрифта",
|
"LabelFontScale": "Мащаб на шрифта",
|
||||||
"LabelFontStrikethrough": "Зачертан",
|
"LabelFontStrikethrough": "Зачертан",
|
||||||
"LabelFormat": "Формат",
|
"LabelFormat": "Формат",
|
||||||
"LabelGenre": "Жанр",
|
"LabelGenre": "Жанр",
|
||||||
"LabelGenres": "Жанрове",
|
"LabelGenres": "Жанрове",
|
||||||
"LabelHardDeleteFile": "Пълно Изтриване на Файл",
|
"LabelHardDeleteFile": "Пълно Изтриване на Файл",
|
||||||
"LabelHasEbook": "Има електронна книга",
|
"LabelHasEbook": "Има е-книга",
|
||||||
"LabelHasSupplementaryEbook": "Има допълнителна електронна книга",
|
"LabelHasSupplementaryEbook": "Има допълнителна е-книга",
|
||||||
"LabelHighestPriority": "Най-висок Приоритет",
|
"LabelHighestPriority": "Най-висок Приоритет",
|
||||||
"LabelHost": "Хост",
|
"LabelHost": "Хост",
|
||||||
"LabelHour": "Час",
|
"LabelHour": "Час",
|
||||||
"LabelIcon": "Икона",
|
"LabelIcon": "Икона",
|
||||||
"LabelImageURLFromTheWeb": "URL на Изображение от Интернет",
|
"LabelImageURLFromTheWeb": "URL на Изображение от Интернет",
|
||||||
"LabelInProgress": "В Прогрес",
|
"LabelInProgress": "В процес на изпълнение",
|
||||||
"LabelIncludeInTracklist": "Включи в Списъка с Канали",
|
"LabelIncludeInTracklist": "Включи в Списъка с Канали",
|
||||||
"LabelIncomplete": "Незавършено",
|
"LabelIncomplete": "Незавършено",
|
||||||
"LabelInterval": "Интервал",
|
"LabelInterval": "Интервал",
|
||||||
@@ -337,7 +388,7 @@
|
|||||||
"LabelLastTime": "Последно Време",
|
"LabelLastTime": "Последно Време",
|
||||||
"LabelLastUpdate": "Последно Обновяване",
|
"LabelLastUpdate": "Последно Обновяване",
|
||||||
"LabelLayout": "Оформление",
|
"LabelLayout": "Оформление",
|
||||||
"LabelLayoutSinglePage": "Една Страница",
|
"LabelLayoutSinglePage": "Единична страница",
|
||||||
"LabelLayoutSplitPage": "Разделена Страница",
|
"LabelLayoutSplitPage": "Разделена Страница",
|
||||||
"LabelLess": "По-малко",
|
"LabelLess": "По-малко",
|
||||||
"LabelLibrariesAccessibleToUser": "Библиотеки Достъпни за Потребителя",
|
"LabelLibrariesAccessibleToUser": "Библиотеки Достъпни за Потребителя",
|
||||||
@@ -345,8 +396,8 @@
|
|||||||
"LabelLibraryItem": "Елемент на Библиотека",
|
"LabelLibraryItem": "Елемент на Библиотека",
|
||||||
"LabelLibraryName": "Име на Библиотека",
|
"LabelLibraryName": "Име на Библиотека",
|
||||||
"LabelLimit": "Лимит",
|
"LabelLimit": "Лимит",
|
||||||
"LabelLineSpacing": "Линейно Разстояние",
|
"LabelLineSpacing": "Междуредие",
|
||||||
"LabelListenAgain": "Слушай Отново",
|
"LabelListenAgain": "Слушай отново",
|
||||||
"LabelLogLevelDebug": "Дебъг",
|
"LabelLogLevelDebug": "Дебъг",
|
||||||
"LabelLogLevelInfo": "Информация",
|
"LabelLogLevelInfo": "Информация",
|
||||||
"LabelLogLevelWarn": "Предупреждение",
|
"LabelLogLevelWarn": "Предупреждение",
|
||||||
@@ -355,7 +406,7 @@
|
|||||||
"LabelMatchExistingUsersBy": "Съпостави съществуващи потребители по",
|
"LabelMatchExistingUsersBy": "Съпостави съществуващи потребители по",
|
||||||
"LabelMatchExistingUsersByDescription": "Използва се за свързване на съществуващи потребители. След свързване потребителите ще бъдат съпоставени по уникален идентификатор от вашия доставчик на SSO",
|
"LabelMatchExistingUsersByDescription": "Използва се за свързване на съществуващи потребители. След свързване потребителите ще бъдат съпоставени по уникален идентификатор от вашия доставчик на SSO",
|
||||||
"LabelMediaPlayer": "Медия Плейър",
|
"LabelMediaPlayer": "Медия Плейър",
|
||||||
"LabelMediaType": "Тип на Медията",
|
"LabelMediaType": "Тип медия",
|
||||||
"LabelMetaTag": "Мета Таг",
|
"LabelMetaTag": "Мета Таг",
|
||||||
"LabelMetaTags": "Мета Тагове",
|
"LabelMetaTags": "Мета Тагове",
|
||||||
"LabelMetadataOrderOfPrecedenceDescription": "По-високите източници на метаданни ще заменят по-ниските",
|
"LabelMetadataOrderOfPrecedenceDescription": "По-високите източници на метаданни ще заменят по-ниските",
|
||||||
@@ -367,19 +418,19 @@
|
|||||||
"LabelMobileRedirectURIs": "Позволени URI за Мобилно Пренасочване",
|
"LabelMobileRedirectURIs": "Позволени URI за Мобилно Пренасочване",
|
||||||
"LabelMobileRedirectURIsDescription": "Това е whitelist на валидни URI за пренасочване за мобилни приложения. По подразбиране е <code>audiobookshelf://oauth</code>, който може да премахнете или допълните с допълнителни URI за интеграция на приложения от трети страни. Използването на звезда (<code>*</code>) като единствен запис позволява всеки URI.",
|
"LabelMobileRedirectURIsDescription": "Това е whitelist на валидни URI за пренасочване за мобилни приложения. По подразбиране е <code>audiobookshelf://oauth</code>, който може да премахнете или допълните с допълнителни URI за интеграция на приложения от трети страни. Използването на звезда (<code>*</code>) като единствен запис позволява всеки URI.",
|
||||||
"LabelMore": "Повече",
|
"LabelMore": "Повече",
|
||||||
"LabelMoreInfo": "Повече Информация",
|
"LabelMoreInfo": "Повече информация",
|
||||||
"LabelName": "Име",
|
"LabelName": "Име",
|
||||||
"LabelNarrator": "Разказвач",
|
"LabelNarrator": "Разказвач",
|
||||||
"LabelNarrators": "Разказвачи",
|
"LabelNarrators": "Разказвачи",
|
||||||
"LabelNew": "Нови",
|
"LabelNew": "Нови",
|
||||||
"LabelNewPassword": "Нова Парола",
|
"LabelNewPassword": "Нова Парола",
|
||||||
"LabelNewestAuthors": "Най-нови Автори",
|
"LabelNewestAuthors": "Най-новите автори",
|
||||||
"LabelNewestEpisodes": "Най-нови Епизоди",
|
"LabelNewestEpisodes": "Най-новите епизоди",
|
||||||
"LabelNextBackupDate": "Следваща Дата на Архивиране",
|
"LabelNextBackupDate": "Следваща Дата на Архивиране",
|
||||||
"LabelNextScheduledRun": "Следващо Планирано Изпълнение",
|
"LabelNextScheduledRun": "Следващо Планирано Изпълнение",
|
||||||
"LabelNoCustomMetadataProviders": "Няма потребителски доставчици на метаданни",
|
"LabelNoCustomMetadataProviders": "Няма потребителски доставчици на метаданни",
|
||||||
"LabelNoEpisodesSelected": "Няма избрани епизоди",
|
"LabelNoEpisodesSelected": "Няма избрани епизоди",
|
||||||
"LabelNotFinished": "Не е завършено",
|
"LabelNotFinished": "Не е приключено",
|
||||||
"LabelNotStarted": "Не е започнато",
|
"LabelNotStarted": "Не е започнато",
|
||||||
"LabelNotes": "Бележки",
|
"LabelNotes": "Бележки",
|
||||||
"LabelNotificationAppriseURL": "Apprise URL-и",
|
"LabelNotificationAppriseURL": "Apprise URL-и",
|
||||||
@@ -392,7 +443,10 @@
|
|||||||
"LabelNotificationsMaxQueueSize": "Максимален размер на опашката за известия",
|
"LabelNotificationsMaxQueueSize": "Максимален размер на опашката за известия",
|
||||||
"LabelNotificationsMaxQueueSizeHelp": "Събитията са ограничени до изстрелване на 1 на секунда. Събитията ще бъдат игнорирани ако опашката е на максимален размер. Това предотвратява спамирането на известия.",
|
"LabelNotificationsMaxQueueSizeHelp": "Събитията са ограничени до изстрелване на 1 на секунда. Събитията ще бъдат игнорирани ако опашката е на максимален размер. Това предотвратява спамирането на известия.",
|
||||||
"LabelNumberOfBooks": "Брой на Книги",
|
"LabelNumberOfBooks": "Брой на Книги",
|
||||||
"LabelNumberOfEpisodes": "# Епизоди",
|
"LabelNumberOfEpisodes": "Брой епизоди",
|
||||||
|
"LabelOpenIDAdvancedPermsClaimDescription": "Име на OpenID твърдението, което съдържа разширени права за достъп до потребителски действия в приложението, които ще се прилагат за роли, различни от администраторските (<b>ако е конфигурирано</b>). Ако твърдението липсва в отговора, достъпът до ABS ще бъде отказан. Ако липсва една опция, тя ще се третира като <code>false</code>. Уверете се, че твърдението на доставчика на идентичност съответства на очакваната структура:",
|
||||||
|
"LabelOpenIDClaims": "Оставете следните опции празни, за да деактивирате разширеното присвояване на групи, като автоматично ще бъде присвоена групата 'Потребител'.",
|
||||||
|
"LabelOpenIDGroupClaimDescription": "Име на OpenID твърдението, което съдържа списък с групите на потребителя. Обикновено се нарича <code>groups</code>. <b>Ако е конфигурирано</b>, приложението автоматично ще присвоява роли въз основа на членството на потребителя в групи, при условие че тези групи са наименувани без чувствителност към регистъра като 'admin', 'user' или 'guest' в твърдението. Твърдението трябва да съдържа списък и ако потребителят принадлежи към множество групи, приложението ще присвои ролята, съответстваща на най-високото ниво на достъп. Ако няма съвпадение с група, достъпът ще бъде отказан.",
|
||||||
"LabelOpenRSSFeed": "Отвори RSS Feed",
|
"LabelOpenRSSFeed": "Отвори RSS Feed",
|
||||||
"LabelOverwrite": "Презапиши",
|
"LabelOverwrite": "Презапиши",
|
||||||
"LabelPassword": "Парола",
|
"LabelPassword": "Парола",
|
||||||
@@ -414,24 +468,27 @@
|
|||||||
"LabelPodcasts": "Подкасти",
|
"LabelPodcasts": "Подкасти",
|
||||||
"LabelPort": "Порт",
|
"LabelPort": "Порт",
|
||||||
"LabelPrefixesToIgnore": "Префикси за Игнориране (без значение за главни/малки букви)",
|
"LabelPrefixesToIgnore": "Префикси за Игнориране (без значение за главни/малки букви)",
|
||||||
"LabelPreventIndexing": "Предотврати индексирането на вашия feed от iTunes и Google podcast директории",
|
"LabelPreventIndexing": "Предотвратете индексирането на вашата емисия от директориите на iTunes и Google за подкасти",
|
||||||
"LabelPrimaryEbook": "Основна Електронна Книга",
|
"LabelPrimaryEbook": "Основна Електронна Книга",
|
||||||
"LabelProgress": "Прогрес",
|
"LabelProgress": "Прогрес",
|
||||||
"LabelProvider": "Доставчик",
|
"LabelProvider": "Доставчик",
|
||||||
"LabelPubDate": "Дата на Издаване",
|
"LabelPubDate": "Дата на публикуване",
|
||||||
"LabelPublishYear": "Година на Издаване",
|
"LabelPublishYear": "Година на публикуване",
|
||||||
|
"LabelPublishedDate": "Публикувани {0}",
|
||||||
"LabelPublisher": "Издател",
|
"LabelPublisher": "Издател",
|
||||||
"LabelPublishers": "Издателство",
|
"LabelPublishers": "Издателство",
|
||||||
"LabelRSSFeedCustomOwnerEmail": "Потребителски собственик Email",
|
"LabelRSSFeedCustomOwnerEmail": "Персонализиран имейл на собственика",
|
||||||
"LabelRSSFeedCustomOwnerName": "Потребителски собственик Име",
|
"LabelRSSFeedCustomOwnerName": "Персонализирано име на собственика",
|
||||||
"LabelRSSFeedOpen": "RSS Feed Оптворен",
|
"LabelRSSFeedOpen": "RSS Feed Оптворен",
|
||||||
"LabelRSSFeedPreventIndexing": "Предотврати индексиране",
|
"LabelRSSFeedPreventIndexing": "Предотвратете индексиране",
|
||||||
"LabelRSSFeedSlug": "RSS Feed слъг",
|
"LabelRSSFeedSlug": "идентификатор на RSS емисия",
|
||||||
|
"LabelRSSFeedURL": "URL на RSS емисия",
|
||||||
|
"LabelRandomly": "Случайно",
|
||||||
"LabelRead": "Прочети",
|
"LabelRead": "Прочети",
|
||||||
"LabelReadAgain": "Прочети Отново",
|
"LabelReadAgain": "Прочети отново",
|
||||||
"LabelReadEbookWithoutProgress": "Прочети електронната книга без записване прогрес",
|
"LabelReadEbookWithoutProgress": "Прочети електронната книга без записване прогрес",
|
||||||
"LabelRecentSeries": "Скорошни Серии",
|
"LabelRecentSeries": "Скорошни серии",
|
||||||
"LabelRecentlyAdded": "Наскоро Добавени",
|
"LabelRecentlyAdded": "Скорошно добавени",
|
||||||
"LabelRecommended": "Препоръчано",
|
"LabelRecommended": "Препоръчано",
|
||||||
"LabelRedo": "Повтори",
|
"LabelRedo": "Повтори",
|
||||||
"LabelRegion": "Регион",
|
"LabelRegion": "Регион",
|
||||||
@@ -448,12 +505,12 @@
|
|||||||
"LabelSelectUsers": "Избери Потребители",
|
"LabelSelectUsers": "Избери Потребители",
|
||||||
"LabelSendEbookToDevice": "Изпрати електронна книга до ...",
|
"LabelSendEbookToDevice": "Изпрати електронна книга до ...",
|
||||||
"LabelSequence": "Последователност",
|
"LabelSequence": "Последователност",
|
||||||
"LabelSeries": "Серия",
|
"LabelSeries": "От сериите",
|
||||||
"LabelSeriesName": "Име на Серия",
|
"LabelSeriesName": "Име на Серия",
|
||||||
"LabelSeriesProgress": "Прогрес на Серия",
|
"LabelSeriesProgress": "Прогрес на Серия",
|
||||||
"LabelServerYearReview": "Преглед на годината на сървъра ({0})",
|
"LabelServerYearReview": "Преглед на годината на сървъра ({0})",
|
||||||
"LabelSetEbookAsPrimary": "Задай като основна",
|
"LabelSetEbookAsPrimary": "Направи главен",
|
||||||
"LabelSetEbookAsSupplementary": "Задай като допълнителна",
|
"LabelSetEbookAsSupplementary": "Направи второстепенен",
|
||||||
"LabelSettingsAudiobooksOnly": "Само аудиокниги",
|
"LabelSettingsAudiobooksOnly": "Само аудиокниги",
|
||||||
"LabelSettingsAudiobooksOnlyHelp": "Активирането на тази настройка ще игнорира файловете на електронни книги, освен ако не са в папка с аудиокниги, в което случай ще бъдат зададени като допълнителни електронни книги",
|
"LabelSettingsAudiobooksOnlyHelp": "Активирането на тази настройка ще игнорира файловете на електронни книги, освен ако не са в папка с аудиокниги, в което случай ще бъдат зададени като допълнителни електронни книги",
|
||||||
"LabelSettingsBookshelfViewHelp": "Скеуморфен дизайн с дървени рафтове",
|
"LabelSettingsBookshelfViewHelp": "Скеуморфен дизайн с дървени рафтове",
|
||||||
@@ -476,6 +533,7 @@
|
|||||||
"LabelSettingsHomePageBookshelfView": "Начална страница изглед на рафт",
|
"LabelSettingsHomePageBookshelfView": "Начална страница изглед на рафт",
|
||||||
"LabelSettingsLibraryBookshelfView": "Библиотека изглед на рафт",
|
"LabelSettingsLibraryBookshelfView": "Библиотека изглед на рафт",
|
||||||
"LabelSettingsOnlyShowLaterBooksInContinueSeries": "Пропусни предишни книги в Продължи Поредица",
|
"LabelSettingsOnlyShowLaterBooksInContinueSeries": "Пропусни предишни книги в Продължи Поредица",
|
||||||
|
"LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "Рафтът на началната страница 'Продължи поредицата' показва първата книга, която не е започната в поредици, в които има поне една завършена книга и няма книги в процес на четене. Активирането на тази настройка ще продължи поредицата от най-далечната завършена книга вместо от първата незапочната книга.",
|
||||||
"LabelSettingsParseSubtitles": "Извлечи подзаглавия",
|
"LabelSettingsParseSubtitles": "Извлечи подзаглавия",
|
||||||
"LabelSettingsParseSubtitlesHelp": "Извлича подзаглавия от имената на папките на аудиокнигите.<br>Подзаглавията трябва да бъдат разделени с \" - \"<br>например \"Заглавие на Книга - Тук е Подзаглавито\" има подзаглавие \"Тук е Подзаглавито\"",
|
"LabelSettingsParseSubtitlesHelp": "Извлича подзаглавия от имената на папките на аудиокнигите.<br>Подзаглавията трябва да бъдат разделени с \" - \"<br>например \"Заглавие на Книга - Тук е Подзаглавито\" има подзаглавие \"Тук е Подзаглавито\"",
|
||||||
"LabelSettingsPreferMatchedMetadata": "Предпочети съвпадащи метаданни",
|
"LabelSettingsPreferMatchedMetadata": "Предпочети съвпадащи метаданни",
|
||||||
@@ -491,9 +549,10 @@
|
|||||||
"LabelSettingsStoreMetadataWithItem": "Запази метаданните с елемента",
|
"LabelSettingsStoreMetadataWithItem": "Запази метаданните с елемента",
|
||||||
"LabelSettingsStoreMetadataWithItemHelp": "По подразбиране метаданните се съхраняват в /metadata/items, като активирате тази настройка метаданните ще се съхраняват в папката на елемента на вашата библиотека",
|
"LabelSettingsStoreMetadataWithItemHelp": "По подразбиране метаданните се съхраняват в /metadata/items, като активирате тази настройка метаданните ще се съхраняват в папката на елемента на вашата библиотека",
|
||||||
"LabelSettingsTimeFormat": "Формат на Време",
|
"LabelSettingsTimeFormat": "Формат на Време",
|
||||||
"LabelShowAll": "Покажи Всички",
|
"LabelShowAll": "Покажи всички",
|
||||||
|
"LabelShowSeconds": "Покажи секунди",
|
||||||
"LabelSize": "Размер",
|
"LabelSize": "Размер",
|
||||||
"LabelSleepTimer": "Таймер за Сън",
|
"LabelSleepTimer": "Таймер за изключване",
|
||||||
"LabelSlug": "Слъг",
|
"LabelSlug": "Слъг",
|
||||||
"LabelStart": "Старт",
|
"LabelStart": "Старт",
|
||||||
"LabelStartTime": "Начално Време",
|
"LabelStartTime": "Начално Време",
|
||||||
@@ -501,19 +560,19 @@
|
|||||||
"LabelStartedAt": "Стартирано на",
|
"LabelStartedAt": "Стартирано на",
|
||||||
"LabelStatsAudioTracks": "Аудио Канали",
|
"LabelStatsAudioTracks": "Аудио Канали",
|
||||||
"LabelStatsAuthors": "Автори",
|
"LabelStatsAuthors": "Автори",
|
||||||
"LabelStatsBestDay": "Най-добър Ден",
|
"LabelStatsBestDay": "Най-добър ден",
|
||||||
"LabelStatsDailyAverage": "Дневна Средна Стойност",
|
"LabelStatsDailyAverage": "Средно дневно",
|
||||||
"LabelStatsDays": "Дни",
|
"LabelStatsDays": "Общо дни",
|
||||||
"LabelStatsDaysListened": "Дни Слушани",
|
"LabelStatsDaysListened": "Общо слушани дни",
|
||||||
"LabelStatsHours": "Часове",
|
"LabelStatsHours": "Часове",
|
||||||
"LabelStatsInARow": "подред",
|
"LabelStatsInARow": "последователно",
|
||||||
"LabelStatsItemsFinished": "Завършени Елементи",
|
"LabelStatsItemsFinished": "Приключени елементи",
|
||||||
"LabelStatsItemsInLibrary": "Елементи в Библиотеката",
|
"LabelStatsItemsInLibrary": "Елементи в Библиотеката",
|
||||||
"LabelStatsMinutes": "минути",
|
"LabelStatsMinutes": "минути",
|
||||||
"LabelStatsMinutesListening": "Минути Слушани",
|
"LabelStatsMinutesListening": "Общо слушани минути",
|
||||||
"LabelStatsOverallDays": "Общо Дни",
|
"LabelStatsOverallDays": "Общо Дни",
|
||||||
"LabelStatsOverallHours": "Общо Часове",
|
"LabelStatsOverallHours": "Общо Часове",
|
||||||
"LabelStatsWeekListening": "Седмица Слушане",
|
"LabelStatsWeekListening": "Общо слушани седмици",
|
||||||
"LabelSubtitle": "Подзаглавие",
|
"LabelSubtitle": "Подзаглавие",
|
||||||
"LabelSupportedFileTypes": "Поддържани Типове Файлове",
|
"LabelSupportedFileTypes": "Поддържани Типове Файлове",
|
||||||
"LabelTag": "Таг",
|
"LabelTag": "Таг",
|
||||||
@@ -531,7 +590,7 @@
|
|||||||
"LabelTimeBase": "Времева Основа",
|
"LabelTimeBase": "Времева Основа",
|
||||||
"LabelTimeListened": "Време Слушано",
|
"LabelTimeListened": "Време Слушано",
|
||||||
"LabelTimeListenedToday": "Време Слушано Днес",
|
"LabelTimeListenedToday": "Време Слушано Днес",
|
||||||
"LabelTimeRemaining": "{0} оставащо време",
|
"LabelTimeRemaining": "{0} оставащи",
|
||||||
"LabelTimeToShift": "Време за изместване в секунди",
|
"LabelTimeToShift": "Време за изместване в секунди",
|
||||||
"LabelTitle": "Заглавие",
|
"LabelTitle": "Заглавие",
|
||||||
"LabelToolsEmbedMetadata": "Вграждане на Метаданни",
|
"LabelToolsEmbedMetadata": "Вграждане на Метаданни",
|
||||||
@@ -544,14 +603,14 @@
|
|||||||
"LabelTotalTimeListened": "Общо Време Слушано",
|
"LabelTotalTimeListened": "Общо Време Слушано",
|
||||||
"LabelTrackFromFilename": "Канал от Име на Файл",
|
"LabelTrackFromFilename": "Канал от Име на Файл",
|
||||||
"LabelTrackFromMetadata": "Канал от Метаданни",
|
"LabelTrackFromMetadata": "Канал от Метаданни",
|
||||||
"LabelTracks": "Канали",
|
"LabelTracks": "Тракове",
|
||||||
"LabelTracksMultiTrack": "Многоканален",
|
"LabelTracksMultiTrack": "Многоканален",
|
||||||
"LabelTracksNone": "Няма канали",
|
"LabelTracksNone": "Няма канали",
|
||||||
"LabelTracksSingleTrack": "Единичен канал",
|
"LabelTracksSingleTrack": "Единичен канал",
|
||||||
"LabelType": "Тип",
|
"LabelType": "Тип",
|
||||||
"LabelUnabridged": "Несъкратен",
|
"LabelUnabridged": "Несъкратен",
|
||||||
"LabelUndo": "Отмени",
|
"LabelUndo": "Отмени",
|
||||||
"LabelUnknown": "Неизвестно",
|
"LabelUnknown": "Неизвестен",
|
||||||
"LabelUpdateCover": "Обнови Корица",
|
"LabelUpdateCover": "Обнови Корица",
|
||||||
"LabelUpdateCoverHelp": "Позволи презаписване на съществуващите корици за избраните книги, когато се намери съвпадение",
|
"LabelUpdateCoverHelp": "Позволи презаписване на съществуващите корици за избраните книги, когато се намери съвпадение",
|
||||||
"LabelUpdateDetails": "Обнови Детайли",
|
"LabelUpdateDetails": "Обнови Детайли",
|
||||||
@@ -563,7 +622,7 @@
|
|||||||
"LabelUseChapterTrack": "Използвай канал за глава",
|
"LabelUseChapterTrack": "Използвай канал за глава",
|
||||||
"LabelUseFullTrack": "Използвай пълен канал",
|
"LabelUseFullTrack": "Използвай пълен канал",
|
||||||
"LabelUser": "Потребител",
|
"LabelUser": "Потребител",
|
||||||
"LabelUsername": "Потребителско Име",
|
"LabelUsername": "Потребителско име",
|
||||||
"LabelValue": "Стойност",
|
"LabelValue": "Стойност",
|
||||||
"LabelVersion": "Версия",
|
"LabelVersion": "Версия",
|
||||||
"LabelViewBookmarks": "Виж Отметки",
|
"LabelViewBookmarks": "Виж Отметки",
|
||||||
@@ -571,16 +630,20 @@
|
|||||||
"LabelViewQueue": "Виж Опашка",
|
"LabelViewQueue": "Виж Опашка",
|
||||||
"LabelVolume": "Сила на Звука",
|
"LabelVolume": "Сила на Звука",
|
||||||
"LabelWeekdaysToRun": "Делници за изпълнение",
|
"LabelWeekdaysToRun": "Делници за изпълнение",
|
||||||
|
"LabelYearReviewHide": "Скрий ревю на годината ти",
|
||||||
|
"LabelYearReviewShow": "Виж ревю на годината ти",
|
||||||
"LabelYourAudiobookDuration": "Продължителност на вашата аудиокнига",
|
"LabelYourAudiobookDuration": "Продължителност на вашата аудиокнига",
|
||||||
"LabelYourBookmarks": "Вашите Отметки",
|
"LabelYourBookmarks": "Твойте отметки",
|
||||||
"LabelYourPlaylists": "Вашите Плейлисти",
|
"LabelYourPlaylists": "Вашите Плейлисти",
|
||||||
"LabelYourProgress": "Вашият Прогрес",
|
"LabelYourProgress": "Твоят прогрес",
|
||||||
"MessageAddToPlayerQueue": "Добави към опашката на плейъра",
|
"MessageAddToPlayerQueue": "Добави към опашката на плейъра",
|
||||||
"MessageAppriseDescription": "За да ползвате тази функция трябва да имате активна инстанция на <a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">Apprise API</a> или на друго АПИ което да обработва тези заявки. <br />The Apprise API Url-а трябва дае пълния URL път за изпращане на известията, например, ако вашето АПИ ве подава от <code>http://192.168.1.1:8337</code> трябва да сложитев <code>http://192.168.1.1:8337/notify</code>.",
|
"MessageAppriseDescription": "За да ползвате тази функция трябва да имате активна инстанция на <a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">Apprise API</a> или на друго АПИ което да обработва тези заявки. <br />The Apprise API Url-а трябва дае пълния URL път за изпращане на известията, например, ако вашето АПИ ве подава от <code>http://192.168.1.1:8337</code> трябва да сложитев <code>http://192.168.1.1:8337/notify</code>.",
|
||||||
|
"MessageBackupsDescription": "Резервните копия включват потребители, напредък на потребителите, подробности за елементите в библиотеката, настройки на сървъра и изображения, съхранени в <code>/metadata/items</code> и <code>/metadata/authors</code>. Резервните копия <strong>не</strong> включват никакви файлове, съхранени в папките на вашата библиотека.",
|
||||||
"MessageBatchQuickMatchDescription": "Бързото Съпоставяне ще опита да добави липсващи корици и метаданни за избраните елементи. Активирайте опциите по-долу, за да позволите на Бързото съпоставяне да презапише съществуващите корици и/или метаданни.",
|
"MessageBatchQuickMatchDescription": "Бързото Съпоставяне ще опита да добави липсващи корици и метаданни за избраните елементи. Активирайте опциите по-долу, за да позволите на Бързото съпоставяне да презапише съществуващите корици и/или метаданни.",
|
||||||
"MessageBookshelfNoCollections": "Все още нямате създадени колекции",
|
"MessageBookshelfNoCollections": "Все още нямате създадени колекции",
|
||||||
"MessageBookshelfNoRSSFeeds": "Няма отворени RSS feed-ове",
|
"MessageBookshelfNoRSSFeeds": "Няма отворени RSS feed-ове",
|
||||||
"MessageBookshelfNoResultsForFilter": "Няма резултат за филтер \"{0}: {1}\"",
|
"MessageBookshelfNoResultsForFilter": "Няма резултат за филтер \"{0}: {1}\"",
|
||||||
|
"MessageBookshelfNoResultsForQuery": "Няма резултати от заявката",
|
||||||
"MessageBookshelfNoSeries": "Нямаш сеЗЙ",
|
"MessageBookshelfNoSeries": "Нямаш сеЗЙ",
|
||||||
"MessageChapterEndIsAfter": "Краят на главата е след края на вашата аудиокнига",
|
"MessageChapterEndIsAfter": "Краят на главата е след края на вашата аудиокнига",
|
||||||
"MessageChapterErrorFirstNotZero": "Първата глава трябва да започва от 0",
|
"MessageChapterErrorFirstNotZero": "Първата глава трябва да започва от 0",
|
||||||
@@ -600,6 +663,8 @@
|
|||||||
"MessageConfirmMarkAllEpisodesNotFinished": "Сигурни ли сте, че искате да маркирате всички епизоди като незавършени?",
|
"MessageConfirmMarkAllEpisodesNotFinished": "Сигурни ли сте, че искате да маркирате всички епизоди като незавършени?",
|
||||||
"MessageConfirmMarkSeriesFinished": "Сигурни ли сте, че искате да маркирате всички книги в тази серия като завършени?",
|
"MessageConfirmMarkSeriesFinished": "Сигурни ли сте, че искате да маркирате всички книги в тази серия като завършени?",
|
||||||
"MessageConfirmMarkSeriesNotFinished": "Сигурни ли сте, че искате да маркирате всички книги в тази серия като незавършени?",
|
"MessageConfirmMarkSeriesNotFinished": "Сигурни ли сте, че искате да маркирате всички книги в тази серия като незавършени?",
|
||||||
|
"MessageConfirmPurgeCache": "Изчистването на кеша ще изтрие цялата директория в <code>/metadata/cache</code>. <br /><br />Сигурни ли сте, че искате да премахнете директорията на кеша?",
|
||||||
|
"MessageConfirmPurgeItemsCache": "Изчистването на кеша на елементите ще изтрие цялата директория в <code>/metadata/cache/items</code>. <br />Сигурни ли сте?",
|
||||||
"MessageConfirmQuickEmbed": "Внимание! Бързото вграждане няма да архивира вашите аудио файлове. Уверете се, че имате резервно копие на вашите аудио файлове. <br><br>Искате ли да продължите?",
|
"MessageConfirmQuickEmbed": "Внимание! Бързото вграждане няма да архивира вашите аудио файлове. Уверете се, че имате резервно копие на вашите аудио файлове. <br><br>Искате ли да продължите?",
|
||||||
"MessageConfirmReScanLibraryItems": "Сигурни ли сте, че искате да сканирате отново {0} елемента?",
|
"MessageConfirmReScanLibraryItems": "Сигурни ли сте, че искате да сканирате отново {0} елемента?",
|
||||||
"MessageConfirmRemoveAllChapters": "Сигурни ли сте, че искате да премахнете всички глави?",
|
"MessageConfirmRemoveAllChapters": "Сигурни ли сте, че искате да премахнете всички глави?",
|
||||||
@@ -617,34 +682,36 @@
|
|||||||
"MessageConfirmRenameTagMergeNote": "Забележка: Този таг вече съществува и ще бъде слято.",
|
"MessageConfirmRenameTagMergeNote": "Забележка: Този таг вече съществува и ще бъде слято.",
|
||||||
"MessageConfirmRenameTagWarning": "Внимание! Вече съществува подобен таг с различно писане \"{0}\".",
|
"MessageConfirmRenameTagWarning": "Внимание! Вече съществува подобен таг с различно писане \"{0}\".",
|
||||||
"MessageConfirmSendEbookToDevice": "Сигурни ли сте, че искате да изпратите {0} електронна книга \"{1}\" до устройство \"{2}\"?",
|
"MessageConfirmSendEbookToDevice": "Сигурни ли сте, че искате да изпратите {0} електронна книга \"{1}\" до устройство \"{2}\"?",
|
||||||
"MessageDownloadingEpisode": "Изтегляне на епизод",
|
"MessageDownloadingEpisode": "Сваля епизод",
|
||||||
"MessageDragFilesIntoTrackOrder": "Плъзнете файлове в правилния ред на каналите",
|
"MessageDragFilesIntoTrackOrder": "Плъзнете файлове в правилния ред на каналите",
|
||||||
"MessageEmbedFinished": "Вграждането завърши!",
|
"MessageEmbedFinished": "Вграждането завърши!",
|
||||||
"MessageEpisodesQueuedForDownload": "{0} епизод(и) в опашка за изтегляне",
|
"MessageEpisodesQueuedForDownload": "{0} Епизод(и) са сложени за сваляне",
|
||||||
"MessageFeedURLWillBe": "Feed URL-a ще бъде {0}",
|
"MessageEreaderDevices": "За да осигурите доставката на е-книги, може да се наложи да добавите горепосочения имейл адрес като валиден подател за всяко устройство, изброено по-долу.",
|
||||||
"MessageFetching": "Взимане...",
|
"MessageFeedURLWillBe": "Адресът на емисията ще бъде {0}",
|
||||||
|
"MessageFetching": "Извличане...",
|
||||||
"MessageForceReScanDescription": "ще сканира всички файлове отново като прясно сканиране. Аудио файлове ID3 тагове, OPF файлове и текстови файлове ще бъдат сканирани като нови.",
|
"MessageForceReScanDescription": "ще сканира всички файлове отново като прясно сканиране. Аудио файлове ID3 тагове, OPF файлове и текстови файлове ще бъдат сканирани като нови.",
|
||||||
"MessageImportantNotice": "Важно Съобщение!",
|
"MessageImportantNotice": "Важно Съобщение!",
|
||||||
"MessageInsertChapterBelow": "Вмъкни глава под",
|
"MessageInsertChapterBelow": "Вмъкни глава под",
|
||||||
"MessageItemsSelected": "{0} избрани",
|
"MessageItemsSelected": "{0} избрани",
|
||||||
"MessageItemsUpdated": "{0} елемента обновени",
|
"MessageItemsUpdated": "{0} елемента обновени",
|
||||||
"MessageJoinUsOn": "Присъединете се към нас",
|
"MessageJoinUsOn": "Присъединете се към нас",
|
||||||
"MessageLoading": "Зареждане...",
|
"MessageLoading": "Зарежда...",
|
||||||
"MessageLoadingFolders": "Зареждане на Папки...",
|
"MessageLoadingFolders": "Зареждане на Папки...",
|
||||||
|
"MessageLogsDescription": "Логовете се съхраняват в <code>/metadata/logs</code> като JSON файлове. Дневниците за сривове се съхраняват в <code>/metadata/logs/crash_logs.txt</code>.",
|
||||||
"MessageM4BFailed": "M4B Провалено!",
|
"MessageM4BFailed": "M4B Провалено!",
|
||||||
"MessageM4BFinished": "M4B Завършено!",
|
"MessageM4BFinished": "M4B Завършено!",
|
||||||
"MessageMapChapterTitles": "Съпостави заглавията на главите със съществуващите глави на аудиокнигата без да променяш времената",
|
"MessageMapChapterTitles": "Съпостави заглавията на главите със съществуващите глави на аудиокнигата без да променяш времената",
|
||||||
"MessageMarkAllEpisodesFinished": "Маркирай всички епизоди като завършени",
|
"MessageMarkAllEpisodesFinished": "Маркирай всички епизоди като завършени",
|
||||||
"MessageMarkAllEpisodesNotFinished": "Маркирай всички епизоди като незавършени",
|
"MessageMarkAllEpisodesNotFinished": "Маркирай всички епизоди като незавършени",
|
||||||
"MessageMarkAsFinished": "Маркирай като Завършено",
|
"MessageMarkAsFinished": "Маркирай като завършено",
|
||||||
"MessageMarkAsNotFinished": "Маркирай като Незавършено",
|
"MessageMarkAsNotFinished": "Маркирай като Незавършено",
|
||||||
"MessageMatchBooksDescription": "ще се опита да съпостави книги в библиотеката с книга от избрания доставчик за търсене и ще попълни празни детайли и корици. Не презаписва детайлите.",
|
"MessageMatchBooksDescription": "ще се опита да съпостави книги в библиотеката с книга от избрания доставчик за търсене и ще попълни празни детайли и корици. Не презаписва детайлите.",
|
||||||
"MessageNoAudioTracks": "Няма аудио канали",
|
"MessageNoAudioTracks": "Няма аудио канали",
|
||||||
"MessageNoAuthors": "Няма Автори",
|
"MessageNoAuthors": "Няма Автори",
|
||||||
"MessageNoBackups": "Няма архиви",
|
"MessageNoBackups": "Няма архиви",
|
||||||
"MessageNoBookmarks": "Няма Отметки",
|
"MessageNoBookmarks": "Няма отметки",
|
||||||
"MessageNoChapters": "Няма Глави",
|
"MessageNoChapters": "Няма глави",
|
||||||
"MessageNoCollections": "Няма Колекции",
|
"MessageNoCollections": "Няма колекции",
|
||||||
"MessageNoCoversFound": "Не са намерени корици",
|
"MessageNoCoversFound": "Не са намерени корици",
|
||||||
"MessageNoDescription": "Няма описание",
|
"MessageNoDescription": "Няма описание",
|
||||||
"MessageNoDownloadsInProgress": "Няма изтегляния в прогрес",
|
"MessageNoDownloadsInProgress": "Няма изтегляния в прогрес",
|
||||||
@@ -654,9 +721,9 @@
|
|||||||
"MessageNoFoldersAvailable": "Няма налични папки",
|
"MessageNoFoldersAvailable": "Няма налични папки",
|
||||||
"MessageNoGenres": "Няма Жанрове",
|
"MessageNoGenres": "Няма Жанрове",
|
||||||
"MessageNoIssues": "Няма проблеми",
|
"MessageNoIssues": "Няма проблеми",
|
||||||
"MessageNoItems": "Няма Елементи",
|
"MessageNoItems": "Няма елементи",
|
||||||
"MessageNoItemsFound": "Няма намерени елементи",
|
"MessageNoItemsFound": "Няма намерени елементи",
|
||||||
"MessageNoListeningSessions": "Няма слушателски сесии",
|
"MessageNoListeningSessions": "Няма сесии за слушане",
|
||||||
"MessageNoLogs": "Няма логове",
|
"MessageNoLogs": "Няма логове",
|
||||||
"MessageNoMediaProgress": "Няма прогрес на медията",
|
"MessageNoMediaProgress": "Няма прогрес на медията",
|
||||||
"MessageNoNotifications": "Няма известия",
|
"MessageNoNotifications": "Няма известия",
|
||||||
@@ -666,20 +733,21 @@
|
|||||||
"MessageNoSeries": "Няма Серии",
|
"MessageNoSeries": "Няма Серии",
|
||||||
"MessageNoTags": "Няма Тагове",
|
"MessageNoTags": "Няма Тагове",
|
||||||
"MessageNoTasksRunning": "Няма вършещи се задачи",
|
"MessageNoTasksRunning": "Няма вършещи се задачи",
|
||||||
"MessageNoUpdatesWereNecessary": "Не бяха необходими обновления",
|
"MessageNoUpdatesWereNecessary": "Няма нужда от обновяване",
|
||||||
"MessageNoUserPlaylists": "Няма плейлисти на потребителя",
|
"MessageNoUserPlaylists": "Нямате създадени плейлисти",
|
||||||
"MessageNotYetImplemented": "Още не е изпълнено",
|
"MessageNotYetImplemented": "Още не е изпълнено",
|
||||||
"MessageOr": "или",
|
"MessageOr": "или",
|
||||||
"MessagePauseChapter": "Пауза на глава",
|
"MessagePauseChapter": "Пауза на глава",
|
||||||
"MessagePlayChapter": "Пусни налчалото на глава",
|
"MessagePlayChapter": "Пусни налчалото на глава",
|
||||||
"MessagePlaylistCreateFromCollection": "Създай плейлист от колекция",
|
"MessagePlaylistCreateFromCollection": "Създай плейлист от колекция",
|
||||||
"MessagePodcastHasNoRSSFeedForMatching": "Подкастът няма URL адрес на RSS feed за използване за съпоставяне",
|
"MessagePodcastHasNoRSSFeedForMatching": "Подкастът няма URL адрес на RSS feed за използване за съпоставяне",
|
||||||
|
"MessagePodcastSearchField": "Въведи какво да търся или RSS емисия адрес",
|
||||||
"MessageQuickMatchDescription": "Попълни празните детайли и корици с първия резултат от '{0}'. Не презаписва детайлите, освен ако не е активирана настройката 'Предпочети съвпадащи метаданни' на сървъра.",
|
"MessageQuickMatchDescription": "Попълни празните детайли и корици с първия резултат от '{0}'. Не презаписва детайлите, освен ако не е активирана настройката 'Предпочети съвпадащи метаданни' на сървъра.",
|
||||||
"MessageRemoveChapter": "Премахни глава",
|
"MessageRemoveChapter": "Премахни глава",
|
||||||
"MessageRemoveEpisodes": "Премахни {0} епизод(и)",
|
"MessageRemoveEpisodes": "Премахни {0} епизод(и)",
|
||||||
"MessageRemoveFromPlayerQueue": "Премахни от опашката на плейъра",
|
"MessageRemoveFromPlayerQueue": "Премахни от опашката на плейъра",
|
||||||
"MessageRemoveUserWarning": "Сигурни ли сте, че искате да изтриете потребител \"{0}\" завинаги?",
|
"MessageRemoveUserWarning": "Сигурни ли сте, че искате да изтриете потребител \"{0}\" завинаги?",
|
||||||
"MessageReportBugsAndContribute": "Съобщавайте за грешки, заявявайте функции и допринасяйте на",
|
"MessageReportBugsAndContribute": "Докладвайте грешки, поискайте нови функции и допринасяйте на",
|
||||||
"MessageResetChaptersConfirm": "Сигурни ли сте, че искате да нулирате главите и да отмените промените, които сте направили?",
|
"MessageResetChaptersConfirm": "Сигурни ли сте, че искате да нулирате главите и да отмените промените, които сте направили?",
|
||||||
"MessageRestoreBackupConfirm": "Сигурни ли сте, че искате да възстановите архива създаден на",
|
"MessageRestoreBackupConfirm": "Сигурни ли сте, че искате да възстановите архива създаден на",
|
||||||
"MessageRestoreBackupWarning": "Възстановяването на архив ще презапише цялата база данни, намираща се в /config и кориците в /metadata/items & /metadata/authors.<br /><br />Архивите не променят файловете в папките на вашата библиотека. Ако сте активирали настройките на сървъра за съхранение на корици и метаданни в папките на вашата библиотека, те няма да бъдат архивирани или презаписани.<br /><br />Всички клиенти, използващи вашия сървър, ще бъдат автоматично обновени.",
|
"MessageRestoreBackupWarning": "Възстановяването на архив ще презапише цялата база данни, намираща се в /config и кориците в /metadata/items & /metadata/authors.<br /><br />Архивите не променят файловете в папките на вашата библиотека. Ако сте активирали настройките на сървъра за съхранение на корици и метаданни в папките на вашата библиотека, те няма да бъдат архивирани или презаписани.<br /><br />Всички клиенти, използващи вашия сървър, ще бъдат автоматично обновени.",
|
||||||
@@ -700,8 +768,8 @@
|
|||||||
"NoteChangeRootPassword": "Root потребителят е единственият потребител, който може да има празна парола",
|
"NoteChangeRootPassword": "Root потребителят е единственият потребител, който може да има празна парола",
|
||||||
"NoteChapterEditorTimes": "Забележка: Първото време на начало на главата трябва да остане на 0:00, а последното време на начало на главата не може да надвишава продължителността на тази аудиокнига.",
|
"NoteChapterEditorTimes": "Забележка: Първото време на начало на главата трябва да остане на 0:00, а последното време на начало на главата не може да надвишава продължителността на тази аудиокнига.",
|
||||||
"NoteFolderPicker": "Забележка: папките, които вече са картографирани, няма да бъдат показани",
|
"NoteFolderPicker": "Забележка: папките, които вече са картографирани, няма да бъдат показани",
|
||||||
"NoteRSSFeedPodcastAppsHttps": "Внимание: Повечето приложения за подкасти изискват URL адреса на RSS feed да използва HTTPS",
|
"NoteRSSFeedPodcastAppsHttps": "Предупреждение: Повечето приложения за подкасти изискват URL адресът на RSS емисията да използва HTTPS",
|
||||||
"NoteRSSFeedPodcastAppsPubDate": "Внимание: 1 или повече от вашите епизоди нямат дата на публикуване. Някои приложения за подкасти изискват това",
|
"NoteRSSFeedPodcastAppsPubDate": "Предупреждение: Един или повече от вашите епизоди нямат дата на публикуване. Някои приложения за подкасти изискват това.",
|
||||||
"NoteUploaderFoldersWithMediaFiles": "Папките с медийни файлове ще бъдат обработени като отделни елементи на библиотеката.",
|
"NoteUploaderFoldersWithMediaFiles": "Папките с медийни файлове ще бъдат обработени като отделни елементи на библиотеката.",
|
||||||
"NoteUploaderOnlyAudioFiles": "Ако качвате само аудио файлове, то всеки аудио файл ще бъде обработен като отделна аудиокнига.",
|
"NoteUploaderOnlyAudioFiles": "Ако качвате само аудио файлове, то всеки аудио файл ще бъде обработен като отделна аудиокнига.",
|
||||||
"NoteUploaderUnsupportedFiles": "Неподдържаните файлове се игнорират. При избор или пускане на папка, други файлове, които не са в папка на елемент, се игнорират.",
|
"NoteUploaderUnsupportedFiles": "Неподдържаните файлове се игнорират. При избор или пускане на папка, други файлове, които не са в папка на елемент, се игнорират.",
|
||||||
@@ -722,18 +790,25 @@
|
|||||||
"ToastBackupRestoreFailed": "Неуспешно възстановяване на архив",
|
"ToastBackupRestoreFailed": "Неуспешно възстановяване на архив",
|
||||||
"ToastBackupUploadFailed": "Неуспешно качване на архив",
|
"ToastBackupUploadFailed": "Неуспешно качване на архив",
|
||||||
"ToastBackupUploadSuccess": "Архивът е качен",
|
"ToastBackupUploadSuccess": "Архивът е качен",
|
||||||
|
"ToastBatchUpdateFailed": "Неуспешно групово актуализиране",
|
||||||
|
"ToastBatchUpdateSuccess": "Успешно групово актуализиране",
|
||||||
"ToastBookmarkCreateFailed": "Неуспешно създаване на отметка",
|
"ToastBookmarkCreateFailed": "Неуспешно създаване на отметка",
|
||||||
"ToastBookmarkCreateSuccess": "Отметката е създадена",
|
"ToastBookmarkCreateSuccess": "Отметката е създадена",
|
||||||
"ToastBookmarkRemoveSuccess": "Отметката е премахната",
|
"ToastBookmarkRemoveSuccess": "Отметката е премахната",
|
||||||
|
"ToastCachePurgeFailed": "Неуспешно изчистване на кеша",
|
||||||
|
"ToastCachePurgeSuccess": "Успешно изчистване на кеша",
|
||||||
"ToastChaptersHaveErrors": "Главите имат грешки",
|
"ToastChaptersHaveErrors": "Главите имат грешки",
|
||||||
"ToastChaptersMustHaveTitles": "Главите трябва да имат заглавия",
|
"ToastChaptersMustHaveTitles": "Главите трябва да имат заглавия",
|
||||||
"ToastCollectionRemoveSuccess": "Колекцията е премахната",
|
"ToastCollectionRemoveSuccess": "Колекцията е премахната",
|
||||||
"ToastCollectionUpdateSuccess": "Колекцията е обновена",
|
"ToastCollectionUpdateSuccess": "Колекцията е обновена",
|
||||||
|
"ToastDeleteFileFailed": "Неуспешно изтриване на файла",
|
||||||
|
"ToastDeleteFileSuccess": "Успешно изтриване на файла",
|
||||||
|
"ToastFailedToLoadData": "Неуспешно зареждане на данни",
|
||||||
"ToastItemCoverUpdateSuccess": "Корицата на елемента е обновена",
|
"ToastItemCoverUpdateSuccess": "Корицата на елемента е обновена",
|
||||||
"ToastItemDetailsUpdateSuccess": "Детайлите на елемента са обновени",
|
"ToastItemDetailsUpdateSuccess": "Детайлите на елемента са обновени",
|
||||||
"ToastItemMarkedAsFinishedFailed": "Неуспешно маркиране като завършено",
|
"ToastItemMarkedAsFinishedFailed": "Неуспешно маркиране като Завършено",
|
||||||
"ToastItemMarkedAsFinishedSuccess": "Елементът е маркиран като завършен",
|
"ToastItemMarkedAsFinishedSuccess": "Елементът е маркиран като завършен",
|
||||||
"ToastItemMarkedAsNotFinishedFailed": "Неуспешно маркиране като незавършено",
|
"ToastItemMarkedAsNotFinishedFailed": "Неуспешно маркиране като Незавършено",
|
||||||
"ToastItemMarkedAsNotFinishedSuccess": "Елементът е маркиран като незавършен",
|
"ToastItemMarkedAsNotFinishedSuccess": "Елементът е маркиран като незавършен",
|
||||||
"ToastLibraryCreateFailed": "Неуспешно създаване на библиотека",
|
"ToastLibraryCreateFailed": "Неуспешно създаване на библиотека",
|
||||||
"ToastLibraryCreateSuccess": "Библиотеката \"{0}\" е създадена",
|
"ToastLibraryCreateSuccess": "Библиотеката \"{0}\" е създадена",
|
||||||
@@ -747,20 +822,23 @@
|
|||||||
"ToastPlaylistRemoveSuccess": "Плейлистът е премахнат",
|
"ToastPlaylistRemoveSuccess": "Плейлистът е премахнат",
|
||||||
"ToastPlaylistUpdateSuccess": "Плейлистът е обновен",
|
"ToastPlaylistUpdateSuccess": "Плейлистът е обновен",
|
||||||
"ToastPodcastCreateFailed": "Неуспешно създаване на подкаст",
|
"ToastPodcastCreateFailed": "Неуспешно създаване на подкаст",
|
||||||
"ToastPodcastCreateSuccess": "Подкастът е създаден",
|
"ToastPodcastCreateSuccess": "Подкаст успешно създаден",
|
||||||
"ToastRSSFeedCloseFailed": "Неуспешно затваряне на RSS feed",
|
"ToastRSSFeedCloseFailed": "Неуспешно затваряне на RSS емисията",
|
||||||
"ToastRSSFeedCloseSuccess": "RSS feed затворен",
|
"ToastRSSFeedCloseSuccess": "RSS емисията е затворена",
|
||||||
"ToastRemoveItemFromCollectionFailed": "Неуспешно премахване на елемент от колекция",
|
"ToastRemoveItemFromCollectionFailed": "Неуспешно премахване на елемент от колекция",
|
||||||
"ToastRemoveItemFromCollectionSuccess": "Елементът е премахнат от колекция",
|
"ToastRemoveItemFromCollectionSuccess": "Елементът е премахнат от колекция",
|
||||||
"ToastSendEbookToDeviceFailed": "Неуспешно изпращане на електронна книга до устройство",
|
"ToastSendEbookToDeviceFailed": "Неуспешно изпращане на електронна книга до устройство",
|
||||||
"ToastSendEbookToDeviceSuccess": "Електронната книга е изпратена до устройство \"{0}\"",
|
"ToastSendEbookToDeviceSuccess": "Електронната книга е изпратена до устройство \"{0}\"",
|
||||||
"ToastSeriesUpdateFailed": "Неуспешно обновяване на серия",
|
"ToastSeriesUpdateFailed": "Неуспешно обновяване на серия",
|
||||||
"ToastSeriesUpdateSuccess": "Серията е обновена",
|
"ToastSeriesUpdateSuccess": "Серията е обновена",
|
||||||
|
"ToastServerSettingsUpdateSuccess": "Настройките на сървъра са актуализирани",
|
||||||
"ToastSessionDeleteFailed": "Неуспешно изтриване на сесия",
|
"ToastSessionDeleteFailed": "Неуспешно изтриване на сесия",
|
||||||
"ToastSessionDeleteSuccess": "Сесията е изтрита",
|
"ToastSessionDeleteSuccess": "Сесията е изтрита",
|
||||||
"ToastSocketConnected": "Свързан сокет",
|
"ToastSocketConnected": "Свързан сокет",
|
||||||
"ToastSocketDisconnected": "Сокетът е прекъснат",
|
"ToastSocketDisconnected": "Сокетът е прекъснат",
|
||||||
"ToastSocketFailedToConnect": "Неуспешно свързване на сокет",
|
"ToastSocketFailedToConnect": "Неуспешно свързване на сокет",
|
||||||
|
"ToastSortingPrefixesEmptyError": "Трябва да има поне 1 префикс за сортиране",
|
||||||
|
"ToastSortingPrefixesUpdateSuccess": "Префиксите за сортиране са актуализирани ({0} елемента)",
|
||||||
"ToastUserDeleteFailed": "Неуспешно изтриване на потребител",
|
"ToastUserDeleteFailed": "Неуспешно изтриване на потребител",
|
||||||
"ToastUserDeleteSuccess": "Потребителят е изтрит"
|
"ToastUserDeleteSuccess": "Потребителят е изтрит"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,8 @@
|
|||||||
"ButtonApplyChapters": "Kapitel anwenden",
|
"ButtonApplyChapters": "Kapitel anwenden",
|
||||||
"ButtonAuthors": "Autoren",
|
"ButtonAuthors": "Autoren",
|
||||||
"ButtonBack": "Zurück",
|
"ButtonBack": "Zurück",
|
||||||
|
"ButtonBatchEditPopulateFromExisting": "Auffüllen aus vorhandenem",
|
||||||
|
"ButtonBatchEditPopulateMapDetails": "Kartendetails auffüllen",
|
||||||
"ButtonBrowseForFolder": "Ordnersuche",
|
"ButtonBrowseForFolder": "Ordnersuche",
|
||||||
"ButtonCancel": "Abbrechen",
|
"ButtonCancel": "Abbrechen",
|
||||||
"ButtonCancelEncode": "Codierung abbrechen",
|
"ButtonCancelEncode": "Codierung abbrechen",
|
||||||
@@ -484,6 +486,7 @@
|
|||||||
"LabelPersonalYearReview": "Dein Jahr in Übersicht ({0})",
|
"LabelPersonalYearReview": "Dein Jahr in Übersicht ({0})",
|
||||||
"LabelPhotoPathURL": "Foto Pfad/URL",
|
"LabelPhotoPathURL": "Foto Pfad/URL",
|
||||||
"LabelPlayMethod": "Abspielmethode",
|
"LabelPlayMethod": "Abspielmethode",
|
||||||
|
"LabelPlaybackRateIncrementDecrement": "Wiedergaberate der Erhöhung/Verminderung",
|
||||||
"LabelPlayerChapterNumberMarker": "{0} von {1}",
|
"LabelPlayerChapterNumberMarker": "{0} von {1}",
|
||||||
"LabelPlaylists": "Wiedergabelisten",
|
"LabelPlaylists": "Wiedergabelisten",
|
||||||
"LabelPodcast": "Podcast",
|
"LabelPodcast": "Podcast",
|
||||||
@@ -704,8 +707,10 @@
|
|||||||
"MessageBackupsLocationEditNote": "Hinweis: Durch das Aktualisieren des Backup-Speicherorts werden vorhandene Sicherungen nicht verschoben oder geändert",
|
"MessageBackupsLocationEditNote": "Hinweis: Durch das Aktualisieren des Backup-Speicherorts werden vorhandene Sicherungen nicht verschoben oder geändert",
|
||||||
"MessageBackupsLocationNoEditNote": "Hinweis: Der Sicherungsspeicherort wird über eine Umgebungsvariable festgelegt und kann hier nicht geändert werden.",
|
"MessageBackupsLocationNoEditNote": "Hinweis: Der Sicherungsspeicherort wird über eine Umgebungsvariable festgelegt und kann hier nicht geändert werden.",
|
||||||
"MessageBackupsLocationPathEmpty": "Der Backup-Pfad darf nicht leer sein",
|
"MessageBackupsLocationPathEmpty": "Der Backup-Pfad darf nicht leer sein",
|
||||||
|
"MessageBatchEditPopulateMapDetailsAllHelp": "Fülle die aktivierten Felder mit Daten aus allen Elementen. Felder mit mehreren Werten werden zusammengeführt",
|
||||||
"MessageBatchQuickMatchDescription": "Der Schnellabgleich versucht, fehlende Titelbilder und Metadaten für die ausgewählten Artikel hinzuzufügen. Aktiviere die nachstehenden Optionen, damit der Schnellabgleich vorhandene Titelbilder und/oder Metadaten überschreiben kann.",
|
"MessageBatchQuickMatchDescription": "Der Schnellabgleich versucht, fehlende Titelbilder und Metadaten für die ausgewählten Artikel hinzuzufügen. Aktiviere die nachstehenden Optionen, damit der Schnellabgleich vorhandene Titelbilder und/oder Metadaten überschreiben kann.",
|
||||||
"MessageBookshelfNoCollections": "Es wurden noch keine Sammlungen erstellt",
|
"MessageBookshelfNoCollections": "Es wurden noch keine Sammlungen erstellt",
|
||||||
|
"MessageBookshelfNoCollectionsHelp": "Sammlungen sind öffentlich. Alle Benutzer mit Zugriff auf die Bibliothek können sie sehen.",
|
||||||
"MessageBookshelfNoRSSFeeds": "Keine RSS-Feeds geöffnet",
|
"MessageBookshelfNoRSSFeeds": "Keine RSS-Feeds geöffnet",
|
||||||
"MessageBookshelfNoResultsForFilter": "Keine Ergebnisse für Filter \"{0}: {1}\"",
|
"MessageBookshelfNoResultsForFilter": "Keine Ergebnisse für Filter \"{0}: {1}\"",
|
||||||
"MessageBookshelfNoResultsForQuery": "Keine Ergebnisse für die Abfrage",
|
"MessageBookshelfNoResultsForQuery": "Keine Ergebnisse für die Abfrage",
|
||||||
@@ -816,6 +821,7 @@
|
|||||||
"MessageNoTasksRunning": "Keine laufenden Aufgaben",
|
"MessageNoTasksRunning": "Keine laufenden Aufgaben",
|
||||||
"MessageNoUpdatesWereNecessary": "Keine Aktualisierungen waren notwendig",
|
"MessageNoUpdatesWereNecessary": "Keine Aktualisierungen waren notwendig",
|
||||||
"MessageNoUserPlaylists": "Keine Wiedergabelisten vorhanden",
|
"MessageNoUserPlaylists": "Keine Wiedergabelisten vorhanden",
|
||||||
|
"MessageNoUserPlaylistsHelp": "Wiedergabelisten sind privat. Nur der Benutzer, der sie erstellt hat, kann sie sehen.",
|
||||||
"MessageNotYetImplemented": "Noch nicht implementiert",
|
"MessageNotYetImplemented": "Noch nicht implementiert",
|
||||||
"MessageOpmlPreviewNote": "Hinweis: Dies ist nur eine Vorschau der geparsten OPML Datei. Der eigentliche Podcast-Titel wird aus dem RSS-Feed übernommen.",
|
"MessageOpmlPreviewNote": "Hinweis: Dies ist nur eine Vorschau der geparsten OPML Datei. Der eigentliche Podcast-Titel wird aus dem RSS-Feed übernommen.",
|
||||||
"MessageOr": "Oder",
|
"MessageOr": "Oder",
|
||||||
|
|||||||
@@ -641,10 +641,10 @@
|
|||||||
"LabelTimeDurationXMinutes": "{0} minuta",
|
"LabelTimeDurationXMinutes": "{0} minuta",
|
||||||
"LabelTimeDurationXSeconds": "{0} sekundi",
|
"LabelTimeDurationXSeconds": "{0} sekundi",
|
||||||
"LabelTimeInMinutes": "Vrijeme u minutama",
|
"LabelTimeInMinutes": "Vrijeme u minutama",
|
||||||
"LabelTimeLeft": "{0} preostalo",
|
"LabelTimeLeft": "preostalo {0}",
|
||||||
"LabelTimeListened": "Vremena odslušano",
|
"LabelTimeListened": "Vremena odslušano",
|
||||||
"LabelTimeListenedToday": "Vremena odslušano danas",
|
"LabelTimeListenedToday": "Vremena odslušano danas",
|
||||||
"LabelTimeRemaining": "{0} preostalo",
|
"LabelTimeRemaining": "preostalo {0}",
|
||||||
"LabelTimeToShift": "Vrijeme za pomjeriti u sekundama",
|
"LabelTimeToShift": "Vrijeme za pomjeriti u sekundama",
|
||||||
"LabelTitle": "Naslov",
|
"LabelTitle": "Naslov",
|
||||||
"LabelToolsEmbedMetadata": "Ugradi meta-podatke",
|
"LabelToolsEmbedMetadata": "Ugradi meta-podatke",
|
||||||
@@ -678,7 +678,7 @@
|
|||||||
"LabelUploaderDropFiles": "Ispusti datoteke",
|
"LabelUploaderDropFiles": "Ispusti datoteke",
|
||||||
"LabelUploaderItemFetchMetadataHelp": "Automatski dohvati naslov, autora i serijal",
|
"LabelUploaderItemFetchMetadataHelp": "Automatski dohvati naslov, autora i serijal",
|
||||||
"LabelUseAdvancedOptions": "Koristi se naprednim opcijama",
|
"LabelUseAdvancedOptions": "Koristi se naprednim opcijama",
|
||||||
"LabelUseChapterTrack": "Koristi zvučni zapis poglavlja",
|
"LabelUseChapterTrack": "Upravljaj trakom poglavlja",
|
||||||
"LabelUseFullTrack": "Koristi cijeli zvučni zapis",
|
"LabelUseFullTrack": "Koristi cijeli zvučni zapis",
|
||||||
"LabelUseZeroForUnlimited": "0 za neograničeno",
|
"LabelUseZeroForUnlimited": "0 za neograničeno",
|
||||||
"LabelUser": "Korisnik",
|
"LabelUser": "Korisnik",
|
||||||
|
|||||||
+22
-1
@@ -10,6 +10,8 @@
|
|||||||
"ButtonApplyChapters": "Applica",
|
"ButtonApplyChapters": "Applica",
|
||||||
"ButtonAuthors": "Autori",
|
"ButtonAuthors": "Autori",
|
||||||
"ButtonBack": "Indietro",
|
"ButtonBack": "Indietro",
|
||||||
|
"ButtonBatchEditPopulateFromExisting": "Popola da esistente",
|
||||||
|
"ButtonBatchEditPopulateMapDetails": "Inserisci i dettagli della mappa",
|
||||||
"ButtonBrowseForFolder": "Per Cartella",
|
"ButtonBrowseForFolder": "Per Cartella",
|
||||||
"ButtonCancel": "Cancella",
|
"ButtonCancel": "Cancella",
|
||||||
"ButtonCancelEncode": "Ferma la codifica",
|
"ButtonCancelEncode": "Ferma la codifica",
|
||||||
@@ -88,6 +90,8 @@
|
|||||||
"ButtonSaveTracklist": "Salva Tracklist",
|
"ButtonSaveTracklist": "Salva Tracklist",
|
||||||
"ButtonScan": "Scansiona",
|
"ButtonScan": "Scansiona",
|
||||||
"ButtonScanLibrary": "Scansiona Libreria",
|
"ButtonScanLibrary": "Scansiona Libreria",
|
||||||
|
"ButtonScrollLeft": "Scorri verso sinistra",
|
||||||
|
"ButtonScrollRight": "Scorri verso destra",
|
||||||
"ButtonSearch": "Cerca",
|
"ButtonSearch": "Cerca",
|
||||||
"ButtonSelectFolderPath": "Seleziona percorso cartella",
|
"ButtonSelectFolderPath": "Seleziona percorso cartella",
|
||||||
"ButtonSeries": "Serie",
|
"ButtonSeries": "Serie",
|
||||||
@@ -190,6 +194,7 @@
|
|||||||
"HeaderSettingsExperimental": "Opzioni Sperimentali",
|
"HeaderSettingsExperimental": "Opzioni Sperimentali",
|
||||||
"HeaderSettingsGeneral": "Generale",
|
"HeaderSettingsGeneral": "Generale",
|
||||||
"HeaderSettingsScanner": "Scanner",
|
"HeaderSettingsScanner": "Scanner",
|
||||||
|
"HeaderSettingsWebClient": "Web Client",
|
||||||
"HeaderSleepTimer": "Sveglia",
|
"HeaderSleepTimer": "Sveglia",
|
||||||
"HeaderStatsLargestItems": "File pesanti",
|
"HeaderStatsLargestItems": "File pesanti",
|
||||||
"HeaderStatsLongestItems": "libri più lunghi (ore)",
|
"HeaderStatsLongestItems": "libri più lunghi (ore)",
|
||||||
@@ -429,7 +434,7 @@
|
|||||||
"LabelMetadataProvider": "Metadata Provider",
|
"LabelMetadataProvider": "Metadata Provider",
|
||||||
"LabelMinute": "Minuto",
|
"LabelMinute": "Minuto",
|
||||||
"LabelMinutes": "Minuti",
|
"LabelMinutes": "Minuti",
|
||||||
"LabelMissing": "Altro",
|
"LabelMissing": "Mancante",
|
||||||
"LabelMissingEbook": "Non ha libri digitali",
|
"LabelMissingEbook": "Non ha libri digitali",
|
||||||
"LabelMissingSupplementaryEbook": "Non ha un libro digitale supplementare",
|
"LabelMissingSupplementaryEbook": "Non ha un libro digitale supplementare",
|
||||||
"LabelMobileRedirectURIs": "URI di reindirizzamento mobile consentiti",
|
"LabelMobileRedirectURIs": "URI di reindirizzamento mobile consentiti",
|
||||||
@@ -481,6 +486,7 @@
|
|||||||
"LabelPersonalYearReview": "Il tuo anno in rassegna ({0})",
|
"LabelPersonalYearReview": "Il tuo anno in rassegna ({0})",
|
||||||
"LabelPhotoPathURL": "foto Path/URL",
|
"LabelPhotoPathURL": "foto Path/URL",
|
||||||
"LabelPlayMethod": "Metodo di riproduzione",
|
"LabelPlayMethod": "Metodo di riproduzione",
|
||||||
|
"LabelPlaybackRateIncrementDecrement": "Valore incremento/decremento velocità di riproduzione",
|
||||||
"LabelPlayerChapterNumberMarker": "{0} di {1}",
|
"LabelPlayerChapterNumberMarker": "{0} di {1}",
|
||||||
"LabelPlaylists": "Playlist",
|
"LabelPlaylists": "Playlist",
|
||||||
"LabelPodcast": "Podcast",
|
"LabelPodcast": "Podcast",
|
||||||
@@ -543,6 +549,7 @@
|
|||||||
"LabelServerYearReview": "Anno del server in sintesi({0})",
|
"LabelServerYearReview": "Anno del server in sintesi({0})",
|
||||||
"LabelSetEbookAsPrimary": "Imposta come primario",
|
"LabelSetEbookAsPrimary": "Imposta come primario",
|
||||||
"LabelSetEbookAsSupplementary": "Imposta come suplementare",
|
"LabelSetEbookAsSupplementary": "Imposta come suplementare",
|
||||||
|
"LabelSettingsAllowIframe": "Consenti l'incorporamento in un iframe",
|
||||||
"LabelSettingsAudiobooksOnly": "Solo Audiolibri",
|
"LabelSettingsAudiobooksOnly": "Solo Audiolibri",
|
||||||
"LabelSettingsAudiobooksOnlyHelp": "L'abilitazione di questa impostazione ignorerà i file di libro digitale a meno che non si trovino all'interno di una cartella di audiolibri, nel qual caso verranno impostati come libri digitali supplementari",
|
"LabelSettingsAudiobooksOnlyHelp": "L'abilitazione di questa impostazione ignorerà i file di libro digitale a meno che non si trovino all'interno di una cartella di audiolibri, nel qual caso verranno impostati come libri digitali supplementari",
|
||||||
"LabelSettingsBookshelfViewHelp": "Design con scaffali in legno",
|
"LabelSettingsBookshelfViewHelp": "Design con scaffali in legno",
|
||||||
@@ -585,6 +592,7 @@
|
|||||||
"LabelSettingsStoreMetadataWithItemHelp": "Di default, i metadati sono salvati dentro /metadata/items, abilitando questa opzione si memorizzeranno i metadata nella cartella della libreria",
|
"LabelSettingsStoreMetadataWithItemHelp": "Di default, i metadati sono salvati dentro /metadata/items, abilitando questa opzione si memorizzeranno i metadata nella cartella della libreria",
|
||||||
"LabelSettingsTimeFormat": "Formato Ora",
|
"LabelSettingsTimeFormat": "Formato Ora",
|
||||||
"LabelShare": "Condividi",
|
"LabelShare": "Condividi",
|
||||||
|
"LabelShareDownloadableHelp": "Consente agli utenti dotati del link di condivisione di scaricare un file zip dell'elemento della libreria.",
|
||||||
"LabelShareOpen": "Apri Condivisioni",
|
"LabelShareOpen": "Apri Condivisioni",
|
||||||
"LabelShareURL": "Condividi URL",
|
"LabelShareURL": "Condividi URL",
|
||||||
"LabelShowAll": "Mostra tutto",
|
"LabelShowAll": "Mostra tutto",
|
||||||
@@ -593,6 +601,8 @@
|
|||||||
"LabelSize": "Dimensione",
|
"LabelSize": "Dimensione",
|
||||||
"LabelSleepTimer": "Temporizzatore",
|
"LabelSleepTimer": "Temporizzatore",
|
||||||
"LabelSlug": "Lento",
|
"LabelSlug": "Lento",
|
||||||
|
"LabelSortAscending": "Crescente",
|
||||||
|
"LabelSortDescending": "Discendente",
|
||||||
"LabelStart": "Inizo",
|
"LabelStart": "Inizo",
|
||||||
"LabelStartTime": "Tempo di inizio",
|
"LabelStartTime": "Tempo di inizio",
|
||||||
"LabelStarted": "Iniziato",
|
"LabelStarted": "Iniziato",
|
||||||
@@ -664,6 +674,7 @@
|
|||||||
"LabelUpdateDetailsHelp": "Consenti la sovrascrittura dei dettagli esistenti per i libri selezionati quando viene individuata una corrispondenza",
|
"LabelUpdateDetailsHelp": "Consenti la sovrascrittura dei dettagli esistenti per i libri selezionati quando viene individuata una corrispondenza",
|
||||||
"LabelUpdatedAt": "Aggiornato alle",
|
"LabelUpdatedAt": "Aggiornato alle",
|
||||||
"LabelUploaderDragAndDrop": "Drag & drop file o Cartelle",
|
"LabelUploaderDragAndDrop": "Drag & drop file o Cartelle",
|
||||||
|
"LabelUploaderDragAndDropFilesOnly": "Drag & drop files",
|
||||||
"LabelUploaderDropFiles": "Elimina file",
|
"LabelUploaderDropFiles": "Elimina file",
|
||||||
"LabelUploaderItemFetchMetadataHelp": "Recupera automaticamente titolo, autore e serie",
|
"LabelUploaderItemFetchMetadataHelp": "Recupera automaticamente titolo, autore e serie",
|
||||||
"LabelUseAdvancedOptions": "Usa le opzioni avanzate",
|
"LabelUseAdvancedOptions": "Usa le opzioni avanzate",
|
||||||
@@ -679,6 +690,8 @@
|
|||||||
"LabelViewPlayerSettings": "Mostra Impostazioni player",
|
"LabelViewPlayerSettings": "Mostra Impostazioni player",
|
||||||
"LabelViewQueue": "Visualizza coda",
|
"LabelViewQueue": "Visualizza coda",
|
||||||
"LabelVolume": "Volume",
|
"LabelVolume": "Volume",
|
||||||
|
"LabelWebRedirectURLsDescription": "Autorizza questi URL nel tuo provider OAuth per consentire il reindirizzamento all'app Web dopo l'accesso:",
|
||||||
|
"LabelWebRedirectURLsSubfolder": "Sottocartella per URL di reindirizzamento",
|
||||||
"LabelWeekdaysToRun": "Giorni feriali da eseguire",
|
"LabelWeekdaysToRun": "Giorni feriali da eseguire",
|
||||||
"LabelXBooks": "{0} libri",
|
"LabelXBooks": "{0} libri",
|
||||||
"LabelXItems": "{0} oggetti",
|
"LabelXItems": "{0} oggetti",
|
||||||
@@ -694,8 +707,11 @@
|
|||||||
"MessageBackupsLocationEditNote": "Nota: l'aggiornamento della posizione di backup non sposterà o modificherà i backup esistenti",
|
"MessageBackupsLocationEditNote": "Nota: l'aggiornamento della posizione di backup non sposterà o modificherà i backup esistenti",
|
||||||
"MessageBackupsLocationNoEditNote": "Nota: la posizione del backup viene impostata tramite una variabile di ambiente e non può essere modificata qui.",
|
"MessageBackupsLocationNoEditNote": "Nota: la posizione del backup viene impostata tramite una variabile di ambiente e non può essere modificata qui.",
|
||||||
"MessageBackupsLocationPathEmpty": "Il percorso del backup non può essere vuoto",
|
"MessageBackupsLocationPathEmpty": "Il percorso del backup non può essere vuoto",
|
||||||
|
"MessageBatchEditPopulateMapDetailsAllHelp": "Popola i campi abilitati con i dati di tutti gli elementi. I campi con più valori verranno uniti",
|
||||||
|
"MessageBatchEditPopulateMapDetailsItemHelp": "Compila i campi dei dettagli della mappa abilitati con i dati di questo elemento",
|
||||||
"MessageBatchQuickMatchDescription": "Quick Match tenterà di aggiungere copertine e metadati mancanti per gli elementi selezionati. Attiva l'opzione per consentire a Quick Match di sovrascrivere copertine e/o metadati esistenti.",
|
"MessageBatchQuickMatchDescription": "Quick Match tenterà di aggiungere copertine e metadati mancanti per gli elementi selezionati. Attiva l'opzione per consentire a Quick Match di sovrascrivere copertine e/o metadati esistenti.",
|
||||||
"MessageBookshelfNoCollections": "Non hai ancora creato nessuna raccolta",
|
"MessageBookshelfNoCollections": "Non hai ancora creato nessuna raccolta",
|
||||||
|
"MessageBookshelfNoCollectionsHelp": "le collezioni sono pubbliche. Tutti gli utenti con accesso alla biblioteca possono vederle.",
|
||||||
"MessageBookshelfNoRSSFeeds": "Nessun RSS feeds aperto",
|
"MessageBookshelfNoRSSFeeds": "Nessun RSS feeds aperto",
|
||||||
"MessageBookshelfNoResultsForFilter": "Nessun risultato per il filtro \"{0}: {1}\"",
|
"MessageBookshelfNoResultsForFilter": "Nessun risultato per il filtro \"{0}: {1}\"",
|
||||||
"MessageBookshelfNoResultsForQuery": "Nessun risultato per la query",
|
"MessageBookshelfNoResultsForQuery": "Nessun risultato per la query",
|
||||||
@@ -748,6 +764,7 @@
|
|||||||
"MessageConfirmResetProgress": "Vuoi davvero azzerare i tuoi progressi?",
|
"MessageConfirmResetProgress": "Vuoi davvero azzerare i tuoi progressi?",
|
||||||
"MessageConfirmSendEbookToDevice": "Sei sicuro/sicura di voler inviare {0} libro «{1}» al dispositivo «{2}»?",
|
"MessageConfirmSendEbookToDevice": "Sei sicuro/sicura di voler inviare {0} libro «{1}» al dispositivo «{2}»?",
|
||||||
"MessageConfirmUnlinkOpenId": "Vuoi davvero scollegare questo utente da OpenID?",
|
"MessageConfirmUnlinkOpenId": "Vuoi davvero scollegare questo utente da OpenID?",
|
||||||
|
"MessageDaysListenedInTheLastYear": "{0} giorni ascoltati nell'ultimo anno",
|
||||||
"MessageDownloadingEpisode": "Scaricamento dell’episodio in corso",
|
"MessageDownloadingEpisode": "Scaricamento dell’episodio in corso",
|
||||||
"MessageDragFilesIntoTrackOrder": "Trascina i file nell'ordine di traccia corretto",
|
"MessageDragFilesIntoTrackOrder": "Trascina i file nell'ordine di traccia corretto",
|
||||||
"MessageEmbedFailed": "Incorporamento non riuscito!",
|
"MessageEmbedFailed": "Incorporamento non riuscito!",
|
||||||
@@ -805,6 +822,7 @@
|
|||||||
"MessageNoTasksRunning": "Nessun processo in esecuzione",
|
"MessageNoTasksRunning": "Nessun processo in esecuzione",
|
||||||
"MessageNoUpdatesWereNecessary": "Nessun aggiornamento necessario",
|
"MessageNoUpdatesWereNecessary": "Nessun aggiornamento necessario",
|
||||||
"MessageNoUserPlaylists": "non hai nessuna Playlist",
|
"MessageNoUserPlaylists": "non hai nessuna Playlist",
|
||||||
|
"MessageNoUserPlaylistsHelp": "Le playlist sono private. Solo l'utente che le crea può vederle.",
|
||||||
"MessageNotYetImplemented": "Non Ancora Implementato",
|
"MessageNotYetImplemented": "Non Ancora Implementato",
|
||||||
"MessageOpmlPreviewNote": "Nota: questa è un'anteprima del file OPML analizzato. Il titolo effettivo del podcast verrà preso dal feed RSS.",
|
"MessageOpmlPreviewNote": "Nota: questa è un'anteprima del file OPML analizzato. Il titolo effettivo del podcast verrà preso dal feed RSS.",
|
||||||
"MessageOr": "o",
|
"MessageOr": "o",
|
||||||
@@ -826,6 +844,7 @@
|
|||||||
"MessageResetChaptersConfirm": "Sei sicuro di voler reimpostare i capitoli e annullare le modifiche ?",
|
"MessageResetChaptersConfirm": "Sei sicuro di voler reimpostare i capitoli e annullare le modifiche ?",
|
||||||
"MessageRestoreBackupConfirm": "Sei sicuro di voler ripristinare il backup creato su",
|
"MessageRestoreBackupConfirm": "Sei sicuro di voler ripristinare il backup creato su",
|
||||||
"MessageRestoreBackupWarning": "Il ripristino di un backup sovrascriverà l'intero database situato in /config e sovrascrive le immagini in /metadata/items & /metadata/authors.<br /><br />I backup non modificano alcun file nelle cartelle della libreria. Se hai abilitato le impostazioni del server per archiviare copertine e metadati nelle cartelle della libreria, questi non vengono sottoposti a backup o sovrascritti.<br /><br />Tutti i client che utilizzano il tuo server verranno aggiornati automaticamente.",
|
"MessageRestoreBackupWarning": "Il ripristino di un backup sovrascriverà l'intero database situato in /config e sovrascrive le immagini in /metadata/items & /metadata/authors.<br /><br />I backup non modificano alcun file nelle cartelle della libreria. Se hai abilitato le impostazioni del server per archiviare copertine e metadati nelle cartelle della libreria, questi non vengono sottoposti a backup o sovrascritti.<br /><br />Tutti i client che utilizzano il tuo server verranno aggiornati automaticamente.",
|
||||||
|
"MessageScheduleLibraryScanNote": "Per la maggior parte degli utenti, si consiglia di lasciare questa funzionalità disabilitata e di mantenere abilitata l'impostazione di folder watcher. Il folder watcher rileverà automaticamente le modifiche nelle cartelle della libreria. Il folder watcher non funziona per ogni file system (come NFS), quindi è possibile utilizzare le scansioni pianificate della libreria.",
|
||||||
"MessageSearchResultsFor": "cerca risultati per",
|
"MessageSearchResultsFor": "cerca risultati per",
|
||||||
"MessageSelected": "{0} selezionati",
|
"MessageSelected": "{0} selezionati",
|
||||||
"MessageServerCouldNotBeReached": "Impossibile raggiungere il server",
|
"MessageServerCouldNotBeReached": "Impossibile raggiungere il server",
|
||||||
@@ -952,6 +971,7 @@
|
|||||||
"ToastCollectionRemoveSuccess": "Collezione rimossa",
|
"ToastCollectionRemoveSuccess": "Collezione rimossa",
|
||||||
"ToastCollectionUpdateSuccess": "Raccolta aggiornata",
|
"ToastCollectionUpdateSuccess": "Raccolta aggiornata",
|
||||||
"ToastCoverUpdateFailed": "Aggiornamento cover fallito",
|
"ToastCoverUpdateFailed": "Aggiornamento cover fallito",
|
||||||
|
"ToastDateTimeInvalidOrIncomplete": "Data e ora non sono valide o incomplete",
|
||||||
"ToastDeleteFileFailed": "Impossibile eliminare il file",
|
"ToastDeleteFileFailed": "Impossibile eliminare il file",
|
||||||
"ToastDeleteFileSuccess": "File eliminato",
|
"ToastDeleteFileSuccess": "File eliminato",
|
||||||
"ToastDeviceAddFailed": "Aggiunta dispositivo fallita",
|
"ToastDeviceAddFailed": "Aggiunta dispositivo fallita",
|
||||||
@@ -1004,6 +1024,7 @@
|
|||||||
"ToastNewUserTagError": "Devi selezionare almeno un tag",
|
"ToastNewUserTagError": "Devi selezionare almeno un tag",
|
||||||
"ToastNewUserUsernameError": "Inserisci un nome utente",
|
"ToastNewUserUsernameError": "Inserisci un nome utente",
|
||||||
"ToastNoNewEpisodesFound": "Nessun nuovo episodio trovato",
|
"ToastNoNewEpisodesFound": "Nessun nuovo episodio trovato",
|
||||||
|
"ToastNoRSSFeed": "Il podcast non ha un feed RSS",
|
||||||
"ToastNoUpdatesNecessary": "Nessun aggiornamento necessario",
|
"ToastNoUpdatesNecessary": "Nessun aggiornamento necessario",
|
||||||
"ToastNotificationCreateFailed": "Impossibile creare la notifica",
|
"ToastNotificationCreateFailed": "Impossibile creare la notifica",
|
||||||
"ToastNotificationDeleteFailed": "Impossibile eliminare la notifica",
|
"ToastNotificationDeleteFailed": "Impossibile eliminare la notifica",
|
||||||
|
|||||||
@@ -484,6 +484,7 @@
|
|||||||
"LabelPersonalYearReview": "Jouw jaar in review ({0})",
|
"LabelPersonalYearReview": "Jouw jaar in review ({0})",
|
||||||
"LabelPhotoPathURL": "Foto pad/URL",
|
"LabelPhotoPathURL": "Foto pad/URL",
|
||||||
"LabelPlayMethod": "Afspeelwijze",
|
"LabelPlayMethod": "Afspeelwijze",
|
||||||
|
"LabelPlaybackRateIncrementDecrement": "Afspeel Snelheid Vermeerderen/Verminderen",
|
||||||
"LabelPlayerChapterNumberMarker": "{0} van {1}",
|
"LabelPlayerChapterNumberMarker": "{0} van {1}",
|
||||||
"LabelPlaylists": "Afspeellijsten",
|
"LabelPlaylists": "Afspeellijsten",
|
||||||
"LabelPodcast": "Podcast",
|
"LabelPodcast": "Podcast",
|
||||||
@@ -704,8 +705,11 @@
|
|||||||
"MessageBackupsLocationEditNote": "Let op: het bijwerken van de back-uplocatie zal bestaande back-ups niet verplaatsen of wijzigen",
|
"MessageBackupsLocationEditNote": "Let op: het bijwerken van de back-uplocatie zal bestaande back-ups niet verplaatsen of wijzigen",
|
||||||
"MessageBackupsLocationNoEditNote": "Let op: De back-uplocatie wordt ingesteld via een omgevingsvariabele en kan hier niet worden gewijzigd.",
|
"MessageBackupsLocationNoEditNote": "Let op: De back-uplocatie wordt ingesteld via een omgevingsvariabele en kan hier niet worden gewijzigd.",
|
||||||
"MessageBackupsLocationPathEmpty": "Backup locatie pad kan niet leeg zijn",
|
"MessageBackupsLocationPathEmpty": "Backup locatie pad kan niet leeg zijn",
|
||||||
|
"MessageBatchEditPopulateMapDetailsAllHelp": "Vul actieve velden in met data van alle items. Velden met meerdere waarden zullen worden samengevoegd",
|
||||||
|
"MessageBatchEditPopulateMapDetailsItemHelp": "Vul actieve folder detail velden met de data van dit item",
|
||||||
"MessageBatchQuickMatchDescription": "Quick Match zal proberen ontbrekende covers en metadata voor de geselecteerde onderdelen te matchten. Schakel de opties hieronder in om Quick Match toe te staan bestaande covers en/of metadata te overschrijven.",
|
"MessageBatchQuickMatchDescription": "Quick Match zal proberen ontbrekende covers en metadata voor de geselecteerde onderdelen te matchten. Schakel de opties hieronder in om Quick Match toe te staan bestaande covers en/of metadata te overschrijven.",
|
||||||
"MessageBookshelfNoCollections": "Je hebt nog geen collecties gemaakt",
|
"MessageBookshelfNoCollections": "Je hebt nog geen collecties gemaakt",
|
||||||
|
"MessageBookshelfNoCollectionsHelp": "Collecties zijn publiekelijk. Alle gebruikers met toegang tot de bibliotheek kunnen ze zien.",
|
||||||
"MessageBookshelfNoRSSFeeds": "Geen RSS-feeds geopend",
|
"MessageBookshelfNoRSSFeeds": "Geen RSS-feeds geopend",
|
||||||
"MessageBookshelfNoResultsForFilter": "Geen resultaten voor filter \"{0}: {1}\"",
|
"MessageBookshelfNoResultsForFilter": "Geen resultaten voor filter \"{0}: {1}\"",
|
||||||
"MessageBookshelfNoResultsForQuery": "Geen resultaten voor query",
|
"MessageBookshelfNoResultsForQuery": "Geen resultaten voor query",
|
||||||
@@ -816,6 +820,7 @@
|
|||||||
"MessageNoTasksRunning": "Geen lopende taken",
|
"MessageNoTasksRunning": "Geen lopende taken",
|
||||||
"MessageNoUpdatesWereNecessary": "Geen bijwerkingen waren noodzakelijk",
|
"MessageNoUpdatesWereNecessary": "Geen bijwerkingen waren noodzakelijk",
|
||||||
"MessageNoUserPlaylists": "Je hebt geen afspeellijsten",
|
"MessageNoUserPlaylists": "Je hebt geen afspeellijsten",
|
||||||
|
"MessageNoUserPlaylistsHelp": "Afspeellijsten zijn privaat. Alleen de gebruikers die ze hebben gemaakt kunnen ze zien.",
|
||||||
"MessageNotYetImplemented": "Nog niet geimplementeerd",
|
"MessageNotYetImplemented": "Nog niet geimplementeerd",
|
||||||
"MessageOpmlPreviewNote": "Let op: Dit is een preview van het geparseerde OPML-bestand. De werkelijke podcasttitel wordt overgenomen uit de RSS-feed.",
|
"MessageOpmlPreviewNote": "Let op: Dit is een preview van het geparseerde OPML-bestand. De werkelijke podcasttitel wordt overgenomen uit de RSS-feed.",
|
||||||
"MessageOr": "of",
|
"MessageOr": "of",
|
||||||
|
|||||||
+142
-70
@@ -10,11 +10,13 @@
|
|||||||
"ButtonApplyChapters": "Tillämpa kapitel",
|
"ButtonApplyChapters": "Tillämpa kapitel",
|
||||||
"ButtonAuthors": "Författare",
|
"ButtonAuthors": "Författare",
|
||||||
"ButtonBack": "Tillbaka",
|
"ButtonBack": "Tillbaka",
|
||||||
|
"ButtonBatchEditPopulateFromExisting": "Hämta befintlig information",
|
||||||
|
"ButtonBatchEditPopulateMapDetails": "Addera befintliga information",
|
||||||
"ButtonBrowseForFolder": "Bläddra efter mapp",
|
"ButtonBrowseForFolder": "Bläddra efter mapp",
|
||||||
"ButtonCancel": "Avbryt",
|
"ButtonCancel": "Avbryt",
|
||||||
"ButtonCancelEncode": "Avbryt omkodning",
|
"ButtonCancelEncode": "Avbryt omkodning",
|
||||||
"ButtonChangeRootPassword": "Ändra lösenordet för root",
|
"ButtonChangeRootPassword": "Ändra lösenordet för root",
|
||||||
"ButtonCheckAndDownloadNewEpisodes": "Kontrollera och ladda ner nya avsnitt",
|
"ButtonCheckAndDownloadNewEpisodes": "Sök & Hämta nya avsnitt",
|
||||||
"ButtonChooseAFolder": "Välj en mapp",
|
"ButtonChooseAFolder": "Välj en mapp",
|
||||||
"ButtonChooseFiles": "Välj filer",
|
"ButtonChooseFiles": "Välj filer",
|
||||||
"ButtonClearFilter": "Rensa filter",
|
"ButtonClearFilter": "Rensa filter",
|
||||||
@@ -30,7 +32,7 @@
|
|||||||
"ButtonEditChapters": "Redigera kapitel",
|
"ButtonEditChapters": "Redigera kapitel",
|
||||||
"ButtonEditPodcast": "Redigera podcast",
|
"ButtonEditPodcast": "Redigera podcast",
|
||||||
"ButtonEnable": "Aktivera",
|
"ButtonEnable": "Aktivera",
|
||||||
"ButtonForceReScan": "Tvinga omstart",
|
"ButtonForceReScan": "Starta ny skanning",
|
||||||
"ButtonFullPath": "Fullständig sökväg",
|
"ButtonFullPath": "Fullständig sökväg",
|
||||||
"ButtonHide": "Dölj",
|
"ButtonHide": "Dölj",
|
||||||
"ButtonHome": "Hem",
|
"ButtonHome": "Hem",
|
||||||
@@ -64,8 +66,8 @@
|
|||||||
"ButtonPurgeItemsCache": "Rensa cache för föremål",
|
"ButtonPurgeItemsCache": "Rensa cache för föremål",
|
||||||
"ButtonQueueAddItem": "Lägg till i kön",
|
"ButtonQueueAddItem": "Lägg till i kön",
|
||||||
"ButtonQueueRemoveItem": "Ta bort från kön",
|
"ButtonQueueRemoveItem": "Ta bort från kön",
|
||||||
"ButtonQuickMatch": "Snabb matchning",
|
"ButtonQuickMatch": "Snabbmatchning",
|
||||||
"ButtonReScan": "Omstart",
|
"ButtonReScan": "Ny skanning",
|
||||||
"ButtonRead": "Läs",
|
"ButtonRead": "Läs",
|
||||||
"ButtonReadLess": "Visa mindre",
|
"ButtonReadLess": "Visa mindre",
|
||||||
"ButtonReadMore": "Visa mer",
|
"ButtonReadMore": "Visa mer",
|
||||||
@@ -73,8 +75,8 @@
|
|||||||
"ButtonRemove": "Ta bort",
|
"ButtonRemove": "Ta bort",
|
||||||
"ButtonRemoveAll": "Ta bort alla",
|
"ButtonRemoveAll": "Ta bort alla",
|
||||||
"ButtonRemoveAllLibraryItems": "Ta bort alla objekt i biblioteket",
|
"ButtonRemoveAllLibraryItems": "Ta bort alla objekt i biblioteket",
|
||||||
"ButtonRemoveFromContinueListening": "Radera från 'Fortsätt läsa/lyssna'",
|
"ButtonRemoveFromContinueListening": "Radera från 'Fortsätt att lyssna'",
|
||||||
"ButtonRemoveFromContinueReading": "Ta bort från Fortsätt läsa",
|
"ButtonRemoveFromContinueReading": "Radera från 'Fortsätt att läsa'",
|
||||||
"ButtonRemoveSeriesFromContinueSeries": "Radera från 'Fortsätt med serien'",
|
"ButtonRemoveSeriesFromContinueSeries": "Radera från 'Fortsätt med serien'",
|
||||||
"ButtonReset": "Tillbaka",
|
"ButtonReset": "Tillbaka",
|
||||||
"ButtonResetToDefault": "Återställ till standard",
|
"ButtonResetToDefault": "Återställ till standard",
|
||||||
@@ -97,8 +99,8 @@
|
|||||||
"ButtonSubmit": "Spara",
|
"ButtonSubmit": "Spara",
|
||||||
"ButtonTest": "Testa",
|
"ButtonTest": "Testa",
|
||||||
"ButtonUpload": "Ladda upp",
|
"ButtonUpload": "Ladda upp",
|
||||||
"ButtonUploadBackup": "Ladda upp säkerhetskopia",
|
"ButtonUploadBackup": "Läs in säkerhetskopia",
|
||||||
"ButtonUploadCover": "Ladda upp bokomslag",
|
"ButtonUploadCover": "Ladda upp omslag",
|
||||||
"ButtonUploadOPMLFile": "Ladda upp OPML-fil",
|
"ButtonUploadOPMLFile": "Ladda upp OPML-fil",
|
||||||
"ButtonUserDelete": "Radera användare {0}",
|
"ButtonUserDelete": "Radera användare {0}",
|
||||||
"ButtonUserEdit": "Redigera användare {0}",
|
"ButtonUserEdit": "Redigera användare {0}",
|
||||||
@@ -120,7 +122,7 @@
|
|||||||
"HeaderChooseAFolder": "Välj en mapp",
|
"HeaderChooseAFolder": "Välj en mapp",
|
||||||
"HeaderCollection": "Samling",
|
"HeaderCollection": "Samling",
|
||||||
"HeaderCollectionItems": "Böcker i samlingen",
|
"HeaderCollectionItems": "Böcker i samlingen",
|
||||||
"HeaderCover": "Bokomslag",
|
"HeaderCover": "Omslag",
|
||||||
"HeaderCurrentDownloads": "Aktuella nedladdningar",
|
"HeaderCurrentDownloads": "Aktuella nedladdningar",
|
||||||
"HeaderCustomMetadataProviders": "Egen källa för metadata",
|
"HeaderCustomMetadataProviders": "Egen källa för metadata",
|
||||||
"HeaderDetails": "Detaljer",
|
"HeaderDetails": "Detaljer",
|
||||||
@@ -134,8 +136,8 @@
|
|||||||
"HeaderFiles": "Filer",
|
"HeaderFiles": "Filer",
|
||||||
"HeaderFindChapters": "Hitta kapitel",
|
"HeaderFindChapters": "Hitta kapitel",
|
||||||
"HeaderIgnoredFiles": "Ignorerade filer",
|
"HeaderIgnoredFiles": "Ignorerade filer",
|
||||||
"HeaderItemFiles": "Föremålsfiler",
|
"HeaderItemFiles": "Filer",
|
||||||
"HeaderItemMetadataUtils": "Metadataverktyg för föremål",
|
"HeaderItemMetadataUtils": "Metadataverktyg",
|
||||||
"HeaderLastListeningSession": "Senaste lyssningstillfället",
|
"HeaderLastListeningSession": "Senaste lyssningstillfället",
|
||||||
"HeaderLatestEpisodes": "Senaste avsnitten",
|
"HeaderLatestEpisodes": "Senaste avsnitten",
|
||||||
"HeaderLibraries": "Bibliotek",
|
"HeaderLibraries": "Bibliotek",
|
||||||
@@ -147,7 +149,7 @@
|
|||||||
"HeaderLogs": "Loggar",
|
"HeaderLogs": "Loggar",
|
||||||
"HeaderManageGenres": "Hantera kategorier",
|
"HeaderManageGenres": "Hantera kategorier",
|
||||||
"HeaderManageTags": "Hantera taggar",
|
"HeaderManageTags": "Hantera taggar",
|
||||||
"HeaderMapDetails": "Karta detaljer",
|
"HeaderMapDetails": "Gemensam information för samtliga objekt",
|
||||||
"HeaderMatch": "Matcha",
|
"HeaderMatch": "Matcha",
|
||||||
"HeaderMetadataOrderOfPrecedence": "Prioriteringsordning vid inläsning av metadata",
|
"HeaderMetadataOrderOfPrecedence": "Prioriteringsordning vid inläsning av metadata",
|
||||||
"HeaderMetadataToEmbed": "Metadata som kommer att adderas",
|
"HeaderMetadataToEmbed": "Metadata som kommer att adderas",
|
||||||
@@ -164,15 +166,15 @@
|
|||||||
"HeaderPlaylist": "Spellista",
|
"HeaderPlaylist": "Spellista",
|
||||||
"HeaderPlaylistItems": "Böcker i spellistan",
|
"HeaderPlaylistItems": "Böcker i spellistan",
|
||||||
"HeaderPodcastsToAdd": "Podcaster att lägga till",
|
"HeaderPodcastsToAdd": "Podcaster att lägga till",
|
||||||
"HeaderPreviewCover": "Förhandsgranska bokomslag",
|
"HeaderPreviewCover": "Förhandsgranska omslag",
|
||||||
"HeaderRSSFeedGeneral": "RSS-information",
|
"HeaderRSSFeedGeneral": "RSS-information",
|
||||||
"HeaderRSSFeedIsOpen": "RSS-flödet är öppet",
|
"HeaderRSSFeedIsOpen": "RSS-flödet är öppet",
|
||||||
"HeaderRSSFeeds": "RSS-flöden",
|
"HeaderRSSFeeds": "RSS-flöden",
|
||||||
"HeaderRemoveEpisode": "Ta bort avsnitt",
|
"HeaderRemoveEpisode": "Radera avsnitt",
|
||||||
"HeaderRemoveEpisodes": "Ta bort {0} avsnitt",
|
"HeaderRemoveEpisodes": "Radera {0} avsnitt",
|
||||||
"HeaderSavedMediaProgress": "Sparad historik",
|
"HeaderSavedMediaProgress": "Sparad historik",
|
||||||
"HeaderSchedule": "Schema",
|
"HeaderSchedule": "Schema",
|
||||||
"HeaderScheduleEpisodeDownloads": "Schemalägg automatiska avsnittsnedladdningar",
|
"HeaderScheduleEpisodeDownloads": "Schemalägg automatiska nedladdning av avsnitt",
|
||||||
"HeaderScheduleLibraryScans": "Schema för skanning av biblioteket",
|
"HeaderScheduleLibraryScans": "Schema för skanning av biblioteket",
|
||||||
"HeaderSession": "Tillfälle",
|
"HeaderSession": "Tillfälle",
|
||||||
"HeaderSetBackupSchedule": "Ange schemaläggning för säkerhetskopia",
|
"HeaderSetBackupSchedule": "Ange schemaläggning för säkerhetskopia",
|
||||||
@@ -198,7 +200,7 @@
|
|||||||
"HeaderUsers": "Användare",
|
"HeaderUsers": "Användare",
|
||||||
"HeaderYearReview": "Sammanställning av {0}",
|
"HeaderYearReview": "Sammanställning av {0}",
|
||||||
"HeaderYourStats": "Din statistik",
|
"HeaderYourStats": "Din statistik",
|
||||||
"LabelAbridged": "Förkortad",
|
"LabelAbridged": "Förkortad version",
|
||||||
"LabelAccessibleBy": "Tillgänglig för",
|
"LabelAccessibleBy": "Tillgänglig för",
|
||||||
"LabelAccountType": "Kontotyp",
|
"LabelAccountType": "Kontotyp",
|
||||||
"LabelAccountTypeAdmin": "Administratör",
|
"LabelAccountTypeAdmin": "Administratör",
|
||||||
@@ -229,18 +231,19 @@
|
|||||||
"LabelAutoDownloadEpisodes": "Automatisk nedladdning av avsnitt",
|
"LabelAutoDownloadEpisodes": "Automatisk nedladdning av avsnitt",
|
||||||
"LabelAutoFetchMetadata": "Automatisk nedladdning av metadata",
|
"LabelAutoFetchMetadata": "Automatisk nedladdning av metadata",
|
||||||
"LabelAutoFetchMetadataHelp": "Hämtar metadata för titel, författare och serier. Kompletterande metadata kan manuellt adderas efter uppladdningen.",
|
"LabelAutoFetchMetadataHelp": "Hämtar metadata för titel, författare och serier. Kompletterande metadata kan manuellt adderas efter uppladdningen.",
|
||||||
|
"LabelAutoLaunch": "Automatisk start",
|
||||||
"LabelAutoRegisterDescription": "Skapa automatiskt nya användare efter inloggning",
|
"LabelAutoRegisterDescription": "Skapa automatiskt nya användare efter inloggning",
|
||||||
"LabelBackToUser": "Tillbaka till användaren",
|
"LabelBackToUser": "Tillbaka till användaren",
|
||||||
"LabelBackupAudioFiles": "Säkerhetskopiera ljudfiler",
|
"LabelBackupAudioFiles": "Säkerhetskopiera ljudfiler",
|
||||||
"LabelBackupLocation": "Plats för säkerhetskopia",
|
"LabelBackupLocation": "Plats för säkerhetskopia",
|
||||||
"LabelBackupsEnableAutomaticBackups": "Aktivera automatisk säkerhetskopiering",
|
"LabelBackupsEnableAutomaticBackups": "Aktivera automatisk säkerhetskopiering",
|
||||||
"LabelBackupsEnableAutomaticBackupsHelp": "Säkerhetskopior sparas i \"/metadata/backups\"",
|
"LabelBackupsEnableAutomaticBackupsHelp": "Säkerhetskopior sparas i \"/metadata/backups\"",
|
||||||
"LabelBackupsMaxBackupSize": "Maximal storlek på säkerhetskopia (i GB) (0 = obegränsad)",
|
"LabelBackupsMaxBackupSize": "Maximal storlek på säkerhetskopia i GigaByte (0 = obegränsad)",
|
||||||
"LabelBackupsMaxBackupSizeHelp": "Som ett skydd mot en felaktig konfiguration kommer säkerhetskopior inte att genomföras om de överskrider den konfigurerade storleken.",
|
"LabelBackupsMaxBackupSizeHelp": "Som ett skydd mot en felaktig konfiguration kommer säkerhetskopior inte att genomföras om de överskrider den konfigurerade storleken.",
|
||||||
"LabelBackupsNumberToKeep": "Antal säkerhetskopior att behålla",
|
"LabelBackupsNumberToKeep": "Antal säkerhetskopior att behålla",
|
||||||
"LabelBackupsNumberToKeepHelp": "Endast en gammal säkerhetskopia tas bort åt gången, så om du redan har fler säkerhetskopior än det angivna beloppet bör du ta bort dem manuellt.",
|
"LabelBackupsNumberToKeepHelp": "Endast en gammal säkerhetskopia tas bort åt gången, så om du redan har fler säkerhetskopior än det angivna värdet bör du ta bort dem manuellt.",
|
||||||
"LabelBitrate": "Bitfrekvens",
|
"LabelBitrate": "Bitfrekvens",
|
||||||
"LabelBonus": "Bonus",
|
"LabelBonus": "Bonusavsnitt",
|
||||||
"LabelBooks": "Böcker",
|
"LabelBooks": "Böcker",
|
||||||
"LabelButtonText": "Knapptext",
|
"LabelButtonText": "Knapptext",
|
||||||
"LabelByAuthor": "av {0}",
|
"LabelByAuthor": "av {0}",
|
||||||
@@ -262,7 +265,7 @@
|
|||||||
"LabelContinueListening": "Fortsätt att lyssna",
|
"LabelContinueListening": "Fortsätt att lyssna",
|
||||||
"LabelContinueReading": "Fortsätt att läsa",
|
"LabelContinueReading": "Fortsätt att läsa",
|
||||||
"LabelContinueSeries": "Fortsätt med serien",
|
"LabelContinueSeries": "Fortsätt med serien",
|
||||||
"LabelCover": "Bokomslag",
|
"LabelCover": "Omslag",
|
||||||
"LabelCoverImageURL": "URL till omslagsbild",
|
"LabelCoverImageURL": "URL till omslagsbild",
|
||||||
"LabelCreatedAt": "Skapad",
|
"LabelCreatedAt": "Skapad",
|
||||||
"LabelCronExpression": "Schemaläggning med hjälp av Cron (Cron Expression)",
|
"LabelCronExpression": "Schemaläggning med hjälp av Cron (Cron Expression)",
|
||||||
@@ -297,7 +300,7 @@
|
|||||||
"LabelEmailSettingsSecure": "Säker",
|
"LabelEmailSettingsSecure": "Säker",
|
||||||
"LabelEmailSettingsSecureHelp": "Om sant kommer anslutningen att använda TLS vid anslutning till servern. Om falskt används TLS om servern stöder STARTTLS-tillägget. I de flesta fall, om du ansluter till port 465, bör du ställa in detta värde till sant. För port 587 eller 25, låt det vara falskt. (från nodemailer.com/smtp/#authentication)",
|
"LabelEmailSettingsSecureHelp": "Om sant kommer anslutningen att använda TLS vid anslutning till servern. Om falskt används TLS om servern stöder STARTTLS-tillägget. I de flesta fall, om du ansluter till port 465, bör du ställa in detta värde till sant. För port 587 eller 25, låt det vara falskt. (från nodemailer.com/smtp/#authentication)",
|
||||||
"LabelEmailSettingsTestAddress": "E-postadress för test",
|
"LabelEmailSettingsTestAddress": "E-postadress för test",
|
||||||
"LabelEmbeddedCover": "Inbäddat bokomslag",
|
"LabelEmbeddedCover": "Infogat omslag",
|
||||||
"LabelEnable": "Aktivera",
|
"LabelEnable": "Aktivera",
|
||||||
"LabelEncodingBackupLocation": "En säkerhetskopia av dina orginalljudfiler kommer att placeras i katalogen:",
|
"LabelEncodingBackupLocation": "En säkerhetskopia av dina orginalljudfiler kommer att placeras i katalogen:",
|
||||||
"LabelEncodingClearItemCache": "Kom ihåg att regelbundet radera cachen för föremål. Du hittar funktionen längst ner på sidan 'Inställningar'.",
|
"LabelEncodingClearItemCache": "Kom ihåg att regelbundet radera cachen för föremål. Du hittar funktionen längst ner på sidan 'Inställningar'.",
|
||||||
@@ -310,28 +313,39 @@
|
|||||||
"LabelEnd": "Slut",
|
"LabelEnd": "Slut",
|
||||||
"LabelEndOfChapter": "Slut av kapitel",
|
"LabelEndOfChapter": "Slut av kapitel",
|
||||||
"LabelEpisode": "Avsnitt",
|
"LabelEpisode": "Avsnitt",
|
||||||
"LabelEpisodeTitle": "Avsnittsrubrik",
|
"LabelEpisodeNotLinkedToRssFeed": "Avsnittet är inte knutet till ett RSS-flöde",
|
||||||
"LabelEpisodeType": "Avsnittstyp",
|
"LabelEpisodeNumber": "Avsnitt #{0}",
|
||||||
|
"LabelEpisodeTitle": "Titel på avsnittet",
|
||||||
|
"LabelEpisodeType": "Typ av avsnitt",
|
||||||
|
"LabelEpisodeUrlFromRssFeed": "URL-adress till avsnittet i RSS-flödet",
|
||||||
|
"LabelEpisodes": "Avsnitt",
|
||||||
|
"LabelEpisodic": "Uppdelad i avsnitt",
|
||||||
"LabelExample": "Exempel",
|
"LabelExample": "Exempel",
|
||||||
"LabelExpandSeries": "Expandera serier",
|
"LabelExpandSeries": "Expandera serier",
|
||||||
"LabelFeedURL": "Flödes-URL",
|
"LabelExplicit": "Explicit version",
|
||||||
|
"LabelExplicitChecked": "Explicit version (markerad)",
|
||||||
|
"LabelExplicitUnchecked": "Ej Explicit version (ej markerad)",
|
||||||
|
"LabelExportOPML": "Exportera OPML-information",
|
||||||
|
"LabelFeedURL": "URL-adress för flödet",
|
||||||
"LabelFetchingMetadata": "Hämtar metadata",
|
"LabelFetchingMetadata": "Hämtar metadata",
|
||||||
"LabelFile": "Fil",
|
"LabelFile": "Fil",
|
||||||
"LabelFileBirthtime": "Tidpunkt, filen skapades",
|
"LabelFileBirthtime": "Tidpunkt, fil skapad",
|
||||||
"LabelFileModified": "Tidpunkt, filen ändrades",
|
"LabelFileBornDate": "Skapad {0}",
|
||||||
|
"LabelFileModified": "Tidpunkt, fil ändrad",
|
||||||
"LabelFileModifiedDate": "Ändrad {0}",
|
"LabelFileModifiedDate": "Ändrad {0}",
|
||||||
"LabelFilename": "Filnamn",
|
"LabelFilename": "Filnamn",
|
||||||
"LabelFilterByUser": "Välj användare",
|
"LabelFilterByUser": "Välj användare",
|
||||||
"LabelFindEpisodes": "Hitta avsnitt",
|
"LabelFindEpisodes": "Sök avsnitt",
|
||||||
"LabelFinished": "Avslutad",
|
"LabelFinished": "Avslutad",
|
||||||
"LabelFolder": "Mapp",
|
"LabelFolder": "Mapp",
|
||||||
"LabelFolders": "Mappar",
|
"LabelFolders": "Mappar",
|
||||||
"LabelFontBold": "Fetstil",
|
"LabelFontBold": "Fetstil",
|
||||||
"LabelFontBoldness": "Fetstil",
|
"LabelFontBoldness": "Fetstil",
|
||||||
"LabelFontFamily": "Typsnittsfamilj",
|
"LabelFontFamily": "Typsnittsfamilj",
|
||||||
"LabelFontItalic": "Kursiverad",
|
"LabelFontItalic": "Kursiv",
|
||||||
"LabelFontScale": "Skala på typsnitt",
|
"LabelFontScale": "Skala på typsnitt",
|
||||||
"LabelFontStrikethrough": "Genomstruken",
|
"LabelFontStrikethrough": "Genomstruken",
|
||||||
|
"LabelFull": "Komplett",
|
||||||
"LabelGenre": "Kategori",
|
"LabelGenre": "Kategori",
|
||||||
"LabelGenres": "Kategorier",
|
"LabelGenres": "Kategorier",
|
||||||
"LabelHardDeleteFile": "Hård radering av fil",
|
"LabelHardDeleteFile": "Hård radering av fil",
|
||||||
@@ -346,7 +360,7 @@
|
|||||||
"LabelImageURLFromTheWeb": "Skriv URL-adressen till bilden på webben",
|
"LabelImageURLFromTheWeb": "Skriv URL-adressen till bilden på webben",
|
||||||
"LabelInProgress": "Pågående",
|
"LabelInProgress": "Pågående",
|
||||||
"LabelIncludeInTracklist": "Inkludera i spårlista",
|
"LabelIncludeInTracklist": "Inkludera i spårlista",
|
||||||
"LabelIncomplete": "Ofullständig",
|
"LabelIncomplete": "Ofullständigt",
|
||||||
"LabelInterval": "Intervall",
|
"LabelInterval": "Intervall",
|
||||||
"LabelIntervalCustomDailyWeekly": "Anpassad daglig/veckovis",
|
"LabelIntervalCustomDailyWeekly": "Anpassad daglig/veckovis",
|
||||||
"LabelIntervalEvery12Hours": "Var 12:e timme",
|
"LabelIntervalEvery12Hours": "Var 12:e timme",
|
||||||
@@ -363,7 +377,7 @@
|
|||||||
"LabelLanguage": "Språk",
|
"LabelLanguage": "Språk",
|
||||||
"LabelLanguageDefaultServer": "Standardspråk för server",
|
"LabelLanguageDefaultServer": "Standardspråk för server",
|
||||||
"LabelLanguages": "Språk",
|
"LabelLanguages": "Språk",
|
||||||
"LabelLastBookAdded": "Bok senast tillagd",
|
"LabelLastBookAdded": "Bok senast adderad",
|
||||||
"LabelLastBookUpdated": "Bok senast uppdaterad",
|
"LabelLastBookUpdated": "Bok senast uppdaterad",
|
||||||
"LabelLastSeen": "Senast inloggad",
|
"LabelLastSeen": "Senast inloggad",
|
||||||
"LabelLastTime": "Senaste tillfället",
|
"LabelLastTime": "Senaste tillfället",
|
||||||
@@ -378,12 +392,16 @@
|
|||||||
"LabelLibraryName": "Biblioteksnamn",
|
"LabelLibraryName": "Biblioteksnamn",
|
||||||
"LabelLimit": "Begränsning",
|
"LabelLimit": "Begränsning",
|
||||||
"LabelLineSpacing": "Radavstånd",
|
"LabelLineSpacing": "Radavstånd",
|
||||||
"LabelListenAgain": "Läs/Lyssna igen",
|
"LabelListenAgain": "Lyssna igen",
|
||||||
"LabelLogLevelDebug": "Felsökning",
|
"LabelLogLevelDebug": "Felsökning",
|
||||||
"LabelLogLevelInfo": "Information",
|
"LabelLogLevelInfo": "Information",
|
||||||
"LabelLogLevelWarn": "Varningar",
|
"LabelLogLevelWarn": "Varningar",
|
||||||
"LabelLookForNewEpisodesAfterDate": "Sök efter nya avsnitt efter detta datum",
|
"LabelLookForNewEpisodesAfterDate": "Sök efter nya avsnitt efter detta datum",
|
||||||
"LabelLowestPriority": "Lägst prioritet",
|
"LabelLowestPriority": "Lägst prioritet",
|
||||||
|
"LabelMaxEpisodesToDownload": "Maximalt antal avsnitt att ladda ner (0 = obegränsat).",
|
||||||
|
"LabelMaxEpisodesToDownloadPerCheck": "Maximalt antal nya avsnitt att ladda ner per tillfälle",
|
||||||
|
"LabelMaxEpisodesToKeep": "Maximalt antal avsnitt att behålla",
|
||||||
|
"LabelMaxEpisodesToKeepHelp": "'0' innebär obegränsat antal avsnitt. Efter att nya avsnitt laddats ner raderas det äldsta avsnittet om du har mer än maximalt antal avsnitt. Endast ett avsnitt kommer att raderas per tillfälle.",
|
||||||
"LabelMediaPlayer": "Mediaspelare",
|
"LabelMediaPlayer": "Mediaspelare",
|
||||||
"LabelMediaType": "Mediatyp",
|
"LabelMediaType": "Mediatyp",
|
||||||
"LabelMetaTag": "Metadata",
|
"LabelMetaTag": "Metadata",
|
||||||
@@ -403,11 +421,11 @@
|
|||||||
"LabelNew": "Nytt",
|
"LabelNew": "Nytt",
|
||||||
"LabelNewPassword": "Nytt lösenord",
|
"LabelNewPassword": "Nytt lösenord",
|
||||||
"LabelNewestAuthors": "Senaste författarna",
|
"LabelNewestAuthors": "Senaste författarna",
|
||||||
"LabelNewestEpisodes": "Senast tillagda avsnitt",
|
"LabelNewestEpisodes": "Senaste avsnitten",
|
||||||
"LabelNextBackupDate": "Nästa datum för säkerhetskopiering",
|
"LabelNextBackupDate": "Nästa tillfälle för säkerhetskopiering",
|
||||||
"LabelNextScheduledRun": "Nästa schemalagda körning",
|
"LabelNextScheduledRun": "Nästa schemalagda körning",
|
||||||
"LabelNoCustomMetadataProviders": "Ingen egen källa för metadata",
|
"LabelNoCustomMetadataProviders": "Ingen egen källa för metadata",
|
||||||
"LabelNoEpisodesSelected": "Inga avsnitt valda",
|
"LabelNoEpisodesSelected": "Inga avsnitt har valts",
|
||||||
"LabelNotFinished": "Ej avslutad",
|
"LabelNotFinished": "Ej avslutad",
|
||||||
"LabelNotStarted": "Ej påbörjad",
|
"LabelNotStarted": "Ej påbörjad",
|
||||||
"LabelNotes": "Anteckningar",
|
"LabelNotes": "Anteckningar",
|
||||||
@@ -429,7 +447,7 @@
|
|||||||
"LabelPath": "Sökväg",
|
"LabelPath": "Sökväg",
|
||||||
"LabelPermissionsAccessAllLibraries": "Kan komma åt alla bibliotek",
|
"LabelPermissionsAccessAllLibraries": "Kan komma åt alla bibliotek",
|
||||||
"LabelPermissionsAccessAllTags": "Kan komma åt alla taggar",
|
"LabelPermissionsAccessAllTags": "Kan komma åt alla taggar",
|
||||||
"LabelPermissionsAccessExplicitContent": "Kan komma åt explicit innehåll",
|
"LabelPermissionsAccessExplicitContent": "Kan komma åt explicit version",
|
||||||
"LabelPermissionsCreateEreader": "Kan addera e-läsarenhet",
|
"LabelPermissionsCreateEreader": "Kan addera e-läsarenhet",
|
||||||
"LabelPermissionsDelete": "Kan radera",
|
"LabelPermissionsDelete": "Kan radera",
|
||||||
"LabelPermissionsDownload": "Kan ladda ner",
|
"LabelPermissionsDownload": "Kan ladda ner",
|
||||||
@@ -442,7 +460,7 @@
|
|||||||
"LabelPlaylists": "Spellistor",
|
"LabelPlaylists": "Spellistor",
|
||||||
"LabelPodcast": "Podcast",
|
"LabelPodcast": "Podcast",
|
||||||
"LabelPodcastSearchRegion": "Podcast-sökområde",
|
"LabelPodcastSearchRegion": "Podcast-sökområde",
|
||||||
"LabelPodcastType": "Podcasttyp",
|
"LabelPodcastType": "Typ av postcast",
|
||||||
"LabelPodcasts": "Podcasts",
|
"LabelPodcasts": "Podcasts",
|
||||||
"LabelPort": "Port",
|
"LabelPort": "Port",
|
||||||
"LabelPrefixesToIgnore": "Prefix att ignorera (skiftlägesokänsligt)",
|
"LabelPrefixesToIgnore": "Prefix att ignorera (skiftlägesokänsligt)",
|
||||||
@@ -454,24 +472,26 @@
|
|||||||
"LabelPublishYear": "Publiceringsår",
|
"LabelPublishYear": "Publiceringsår",
|
||||||
"LabelPublishedDecade": "Årtionde för publicering",
|
"LabelPublishedDecade": "Årtionde för publicering",
|
||||||
"LabelPublisher": "Utgivare",
|
"LabelPublisher": "Utgivare",
|
||||||
|
"LabelPublishers": "Utgivare",
|
||||||
"LabelRSSFeedCustomOwnerEmail": "Anpassad ägarens e-post",
|
"LabelRSSFeedCustomOwnerEmail": "Anpassad ägarens e-post",
|
||||||
"LabelRSSFeedCustomOwnerName": "Anpassat ägarnamn",
|
"LabelRSSFeedCustomOwnerName": "Anpassat ägarnamn",
|
||||||
"LabelRSSFeedOpen": "Öppna RSS-flöde",
|
"LabelRSSFeedOpen": "Öppna RSS-flöde",
|
||||||
"LabelRSSFeedPreventIndexing": "Förhindra indexering",
|
"LabelRSSFeedPreventIndexing": "Förhindra indexering",
|
||||||
"LabelRSSFeedSlug": "RSS-flödesslag",
|
"LabelRSSFeedSlug": "RSS-flödesslag",
|
||||||
"LabelRSSFeedURL": "RSS-flöde URL",
|
"LabelRSSFeedURL": "URL-adress för RSS-flödet",
|
||||||
"LabelRandomly": "Slumpartat",
|
"LabelRandomly": "Slumpartat",
|
||||||
"LabelRead": "Läst",
|
"LabelRead": "Läst",
|
||||||
"LabelReadAgain": "Läs igen",
|
"LabelReadAgain": "Läs igen",
|
||||||
"LabelReadEbookWithoutProgress": "Läs e-bok utan att behålla framsteg",
|
"LabelReadEbookWithoutProgress": "Läs e-bok utan att behålla framsteg",
|
||||||
"LabelRecentSeries": "Nyaste serierna",
|
"LabelRecentSeries": "Senaste serierna",
|
||||||
"LabelRecentlyAdded": "Nyligen tillagda",
|
"LabelRecentlyAdded": "Nyligen adderade",
|
||||||
"LabelRecommended": "Rekommenderad",
|
"LabelRecommended": "Rekommenderad",
|
||||||
|
"LabelRedo": "Gör om",
|
||||||
"LabelRegion": "Region",
|
"LabelRegion": "Region",
|
||||||
"LabelReleaseDate": "Utgivningsdatum",
|
"LabelReleaseDate": "Utgivningsdatum",
|
||||||
"LabelRemoveAllMetadataAbs": "Radera alla 'metadata.abs' filer",
|
"LabelRemoveAllMetadataAbs": "Radera alla 'metadata.abs' filer",
|
||||||
"LabelRemoveAllMetadataJson": "Radera alla 'metadata.json' filer",
|
"LabelRemoveAllMetadataJson": "Radera alla 'metadata.json' filer",
|
||||||
"LabelRemoveCover": "Ta bort bokomslag",
|
"LabelRemoveCover": "Ta bort omslag",
|
||||||
"LabelRemoveMetadataFile": "Radera metadata-filer i alla mappar i biblioteket",
|
"LabelRemoveMetadataFile": "Radera metadata-filer i alla mappar i biblioteket",
|
||||||
"LabelRemoveMetadataFileHelp": "Radera alla 'metadata.json' och 'metadata.abs' filer i dina {0} mappar.",
|
"LabelRemoveMetadataFileHelp": "Radera alla 'metadata.json' och 'metadata.abs' filer i dina {0} mappar.",
|
||||||
"LabelRowsPerPage": "Antal rader per sida",
|
"LabelRowsPerPage": "Antal rader per sida",
|
||||||
@@ -479,6 +499,7 @@
|
|||||||
"LabelSearchTitle": "Titel",
|
"LabelSearchTitle": "Titel",
|
||||||
"LabelSearchTitleOrASIN": "Sök titel eller ASIN-kod",
|
"LabelSearchTitleOrASIN": "Sök titel eller ASIN-kod",
|
||||||
"LabelSeason": "Säsong",
|
"LabelSeason": "Säsong",
|
||||||
|
"LabelSeasonNumber": "Säsong #{0}",
|
||||||
"LabelSelectAll": "Välj alla",
|
"LabelSelectAll": "Välj alla",
|
||||||
"LabelSelectAllEpisodes": "Välj alla avsnitt",
|
"LabelSelectAllEpisodes": "Välj alla avsnitt",
|
||||||
"LabelSelectEpisodesShowing": "Välj {0} avsnitt som visas",
|
"LabelSelectEpisodesShowing": "Välj {0} avsnitt som visas",
|
||||||
@@ -500,16 +521,16 @@
|
|||||||
"LabelSettingsDateFormat": "Datumformat",
|
"LabelSettingsDateFormat": "Datumformat",
|
||||||
"LabelSettingsDisableWatcher": "Inaktivera Watcher",
|
"LabelSettingsDisableWatcher": "Inaktivera Watcher",
|
||||||
"LabelSettingsDisableWatcherForLibrary": "Inaktivera bevakning med Watcher för biblioteket",
|
"LabelSettingsDisableWatcherForLibrary": "Inaktivera bevakning med Watcher för biblioteket",
|
||||||
"LabelSettingsDisableWatcherHelp": "Inaktiverar automatik att addera/uppdatera objekt<br>när ändringar av filer genomförs.<br>OBS: Kräver en omstart av servern",
|
"LabelSettingsDisableWatcherHelp": "Inaktiverar automatik att addera/uppdatera<br> objekt när ändringar av filer genomförs.<br>OBS: Kräver en omstart av servern",
|
||||||
"LabelSettingsEnableWatcher": "Aktivera Watcher",
|
"LabelSettingsEnableWatcher": "Aktivera Watcher",
|
||||||
"LabelSettingsEnableWatcherForLibrary": "Aktivera bevakning med Watcher för biblioteket",
|
"LabelSettingsEnableWatcherForLibrary": "Aktivera bevakning med Watcher för biblioteket",
|
||||||
"LabelSettingsEnableWatcherHelp": "Aktiverar automatik att addera/uppdatera objekt<br>när ändringar av filer genomförs.<br>OBS: Kräver en omstart av servern",
|
"LabelSettingsEnableWatcherHelp": "Aktiverar automatik att addera/uppdatera<br> objekt när ändringar av filer genomförs.<br>OBS: Kräver en omstart av servern",
|
||||||
"LabelSettingsEpubsAllowScriptedContent": "Tillåt e-böcker i epubs-format som innehåller script",
|
"LabelSettingsEpubsAllowScriptedContent": "Tillåt e-böcker i epubs-format som innehåller script",
|
||||||
"LabelSettingsEpubsAllowScriptedContentHelp": "Tillåt att epub-filer får använda script.<br>Det rekommenderas att denna inställning är<br>avstängd när du inte litar på källan för epub-filerna.",
|
"LabelSettingsEpubsAllowScriptedContentHelp": "Tillåt att epub-filer får använda script.<br>Det rekommenderas att denna inställning är<br>avstängd när du inte litar på källan för epub-filerna.",
|
||||||
"LabelSettingsExperimentalFeatures": "Experimentella funktioner",
|
"LabelSettingsExperimentalFeatures": "Experimentella funktioner",
|
||||||
"LabelSettingsExperimentalFeaturesHelp": "Funktioner under utveckling som behöver din feedback och hjälp med testning. Klicka för att öppna diskussionen på GitHub.",
|
"LabelSettingsExperimentalFeaturesHelp": "Funktioner under utveckling som behöver din feedback och hjälp med testning. Klicka för att öppna diskussionen på GitHub.",
|
||||||
"LabelSettingsFindCovers": "Hitta ett bokomslag",
|
"LabelSettingsFindCovers": "Hitta ett omslag",
|
||||||
"LabelSettingsFindCoversHelp": "Om din bok inte har ett bokomslag inbäddat i filen eller en fil med bokomslaget i mappen kommer skannern att försöka hitta ett omslag. OBS: Detta kommer att förlänga inläsningstiden",
|
"LabelSettingsFindCoversHelp": "Om din bok INTE har ett omslag inbäddat i filen eller en fil med omslaget i mappen kommer skannern att försöka hitta ett omslag.<br>OBS: Detta kommer att förlänga inläsningstiden",
|
||||||
"LabelSettingsHideSingleBookSeries": "Dölj serier som endast innehåller en bok",
|
"LabelSettingsHideSingleBookSeries": "Dölj serier som endast innehåller en bok",
|
||||||
"LabelSettingsHideSingleBookSeriesHelp": "Serier som endast har en bok kommer att<br>döljas från sidan 'Serier' och hyllorna på startsidan.",
|
"LabelSettingsHideSingleBookSeriesHelp": "Serier som endast har en bok kommer att<br>döljas från sidan 'Serier' och hyllorna på startsidan.",
|
||||||
"LabelSettingsHomePageBookshelfView": "Använd vy liknande en bokhylla på startsidan",
|
"LabelSettingsHomePageBookshelfView": "Använd vy liknande en bokhylla på startsidan",
|
||||||
@@ -520,21 +541,22 @@
|
|||||||
"LabelSettingsOnlyShowLaterBooksInContinueSeries": "Hoppa över tidigare böcker i en serie",
|
"LabelSettingsOnlyShowLaterBooksInContinueSeries": "Hoppa över tidigare böcker i en serie",
|
||||||
"LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "Sektionen 'Fortsätt med serien' på startsidan visar \"nästa bok\" i serien,<br>där åtminstone en bok avslutats, och ingen bok i serien har påbörjats.<br>Om detta alternativ aktiveras kommer efterföljande bok till den<br>avslutade att föreslås - istället för den första ej avslutade boken i serien.",
|
"LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "Sektionen 'Fortsätt med serien' på startsidan visar \"nästa bok\" i serien,<br>där åtminstone en bok avslutats, och ingen bok i serien har påbörjats.<br>Om detta alternativ aktiveras kommer efterföljande bok till den<br>avslutade att föreslås - istället för den första ej avslutade boken i serien.",
|
||||||
"LabelSettingsParseSubtitles": "Hämta undertitel från bokens mapp",
|
"LabelSettingsParseSubtitles": "Hämta undertitel från bokens mapp",
|
||||||
"LabelSettingsParseSubtitlesHelp": "Hämtar undertiteln från namnet på mappen där boken lagras.<br>Undertiteln måste vara åtskilda med ett bindestreck ' - '.<br>En mapp med namnet 'Boktitel - Bokens undertitel'<br> får undertiteln \"Bokens undertitel\"",
|
"LabelSettingsParseSubtitlesHelp": "Hämtar undertiteln från namnet<br> på mappen där boken lagras.<br>Undertiteln måste vara åtskilda med ett bindestreck ' - '.<br>En mapp med namnet 'Boktitel - Bokens undertitel'<br> får undertiteln \"Bokens undertitel\"",
|
||||||
"LabelSettingsPreferMatchedMetadata": "Prioritera matchad metadata",
|
"LabelSettingsPreferMatchedMetadata": "Prioritera matchad metadata",
|
||||||
"LabelSettingsPreferMatchedMetadataHelp": "Matchad data kommer att åsidosätta objektdetaljer vid snabbmatchning. Som standard kommer snabbmatchning endast att fylla i saknade detaljer.",
|
"LabelSettingsPreferMatchedMetadataHelp": "Matchad data kommer att ersätta befintliga uppgifter vid en snabbmatchning. Som standard kommer en snabbmatchning endast att fylla i saknade detaljer.",
|
||||||
"LabelSettingsSkipMatchingBooksWithASIN": "Hoppa över matchande böcker som har en ASIN-kod",
|
"LabelSettingsSkipMatchingBooksWithASIN": "Hoppa över matchande böcker som har en ASIN-kod",
|
||||||
"LabelSettingsSkipMatchingBooksWithISBN": "Hoppa över matchande böcker som har en ISBN-kod",
|
"LabelSettingsSkipMatchingBooksWithISBN": "Hoppa över matchande böcker som har en ISBN-kod",
|
||||||
"LabelSettingsSortingIgnorePrefixes": "Ignorera prefix vid sortering",
|
"LabelSettingsSortingIgnorePrefixes": "Ignorera prefix vid sortering",
|
||||||
"LabelSettingsSortingIgnorePrefixesHelp": "För prefix som t.ex. \"the\" kommer boktiteln \"The Book Title\" att sorteras som \"Book Title, The\"",
|
"LabelSettingsSortingIgnorePrefixesHelp": "För prefix som t.ex. \"the\" kommer boktiteln \"The Book Title\" att sorteras som \"Book Title, The\"",
|
||||||
"LabelSettingsSquareBookCovers": "Använd kvadratiska bokomslag",
|
"LabelSettingsSquareBookCovers": "Använd kvadratiska omslag",
|
||||||
"LabelSettingsSquareBookCoversHelp": "Föredrar att använda kvadratiska bokomslag<br>före standardformatet 1.6:1",
|
"LabelSettingsSquareBookCoversHelp": "Föredrar att använda kvadratiska omslag<br>före standardformatet 1.6:1",
|
||||||
"LabelSettingsStoreCoversWithItem": "Lagra bokomslag med objektet",
|
"LabelSettingsStoreCoversWithItem": "Lagra omslag med objektet",
|
||||||
"LabelSettingsStoreCoversWithItemHelp": "Som standard lagras bokomslag i mappen '/metadata/items'.<br>Genom att aktivera detta alternativ kommer<br>omslagen att lagra i din biblioteksmapp.<br>Endast en fil med namnet 'cover' kommer att behållas",
|
"LabelSettingsStoreCoversWithItemHelp": "Som standard lagras omslag i mappen '/metadata/items'.<br>Genom att aktivera detta alternativ kommer<br>omslagen att lagra i din biblioteksmapp.<br>Endast en fil med namnet 'cover' kommer att behållas",
|
||||||
"LabelSettingsStoreMetadataWithItem": "Lagra metadata med objektet",
|
"LabelSettingsStoreMetadataWithItem": "Lagra metadata med objektet",
|
||||||
"LabelSettingsStoreMetadataWithItemHelp": "Som standard lagras metadatafiler i mappen '/metadata/items'. Genom att aktivera detta alternativ kommer metadatafilerna att lagra i din biblioteksmapp",
|
"LabelSettingsStoreMetadataWithItemHelp": "Som standard lagras metadatafiler i mappen '/metadata/items'. Genom att aktivera detta alternativ kommer metadatafilerna att lagra i din biblioteksmapp",
|
||||||
"LabelSettingsTimeFormat": "Tidsformat",
|
"LabelSettingsTimeFormat": "Tidsformat",
|
||||||
"LabelShare": "Dela",
|
"LabelShare": "Dela",
|
||||||
|
"LabelShareURL": "Dela URL-länk",
|
||||||
"LabelShowAll": "Visa alla",
|
"LabelShowAll": "Visa alla",
|
||||||
"LabelShowSeconds": "Visa sekunder",
|
"LabelShowSeconds": "Visa sekunder",
|
||||||
"LabelShowSubtitles": "Visa underrubriker",
|
"LabelShowSubtitles": "Visa underrubriker",
|
||||||
@@ -569,6 +591,7 @@
|
|||||||
"LabelTagsNotAccessibleToUser": "Taggar inte tillgängliga för användaren",
|
"LabelTagsNotAccessibleToUser": "Taggar inte tillgängliga för användaren",
|
||||||
"LabelTasks": "Pågående aktivitet",
|
"LabelTasks": "Pågående aktivitet",
|
||||||
"LabelTextEditorBulletedList": "Punktlista",
|
"LabelTextEditorBulletedList": "Punktlista",
|
||||||
|
"LabelTextEditorLink": "Länk",
|
||||||
"LabelTextEditorNumberedList": "Numrerad lista",
|
"LabelTextEditorNumberedList": "Numrerad lista",
|
||||||
"LabelTheme": "Utseende",
|
"LabelTheme": "Utseende",
|
||||||
"LabelThemeDark": "Mörkt",
|
"LabelThemeDark": "Mörkt",
|
||||||
@@ -604,8 +627,8 @@
|
|||||||
"LabelUndo": "Ångra",
|
"LabelUndo": "Ångra",
|
||||||
"LabelUnknown": "Okänd",
|
"LabelUnknown": "Okänd",
|
||||||
"LabelUnknownPublishDate": "Okänt publiceringsdatum",
|
"LabelUnknownPublishDate": "Okänt publiceringsdatum",
|
||||||
"LabelUpdateCover": "Uppdatera bokomslag",
|
"LabelUpdateCover": "Uppdatera omslag",
|
||||||
"LabelUpdateCoverHelp": "Tillåt att befintliga bokomslag för de valda böckerna ersätts när en matchning hittas",
|
"LabelUpdateCoverHelp": "Tillåt att befintliga omslag för de valda böckerna ersätts när en matchning hittas",
|
||||||
"LabelUpdateDetails": "Uppdatera detaljer",
|
"LabelUpdateDetails": "Uppdatera detaljer",
|
||||||
"LabelUpdateDetailsHelp": "Tillåt att befintliga detaljer för de valda böckerna ersätts när en matchning hittas",
|
"LabelUpdateDetailsHelp": "Tillåt att befintliga detaljer för de valda böckerna ersätts när en matchning hittas",
|
||||||
"LabelUpdatedAt": "Uppdaterades",
|
"LabelUpdatedAt": "Uppdaterades",
|
||||||
@@ -637,12 +660,15 @@
|
|||||||
"LabelYourProgress": "Framsteg",
|
"LabelYourProgress": "Framsteg",
|
||||||
"MessageAddToPlayerQueue": "Lägg till i spellistan",
|
"MessageAddToPlayerQueue": "Lägg till i spellistan",
|
||||||
"MessageAppriseDescription": "För att använda den här funktionen behöver du ha en instans av <a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">Apprise API</a> igång eller en API som hanterar dessa begäranden. <br />Apprise API-urlen bör vara hela URL-sökvägen för att skicka meddelandet, t.ex., om din API-instans är tillgänglig på <code>http://192.168.1.1:8337</code>, bör du ange <code>http://192.168.1.1:8337/notify</code>.",
|
"MessageAppriseDescription": "För att använda den här funktionen behöver du ha en instans av <a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">Apprise API</a> igång eller en API som hanterar dessa begäranden. <br />Apprise API-urlen bör vara hela URL-sökvägen för att skicka meddelandet, t.ex., om din API-instans är tillgänglig på <code>http://192.168.1.1:8337</code>, bör du ange <code>http://192.168.1.1:8337/notify</code>.",
|
||||||
"MessageBackupsDescription": "Säkerhetskopior inkluderar användare, användarnas framsteg, biblioteksobjekt, serverinställningar<br>och bilder lagrade i <code>/metadata/items</code> & <code>/metadata/authors</code>.<br>De inkluderar <strong>INTE</strong> några filer lagrade i dina biblioteksmappar.",
|
"MessageBackupsDescription": "Säkerhetskopior inkluderar användare, användarnas framsteg, biblioteksobjekt,<br>serverinställningar och bilder lagrade i <code>/metadata/items</code> & <code>/metadata/authors</code>.<br>De inkluderar <strong>INTE</strong> några filer lagrade i dina biblioteksmappar.",
|
||||||
"MessageBackupsLocationEditNote": "OBS: När du ändrar plats för säkerhetskopiorna så flyttas INTE gamla säkerhetskopior dit.",
|
"MessageBackupsLocationEditNote": "OBS: När du ändrar plats för säkerhetskopiorna så flyttas INTE gamla säkerhetskopior dit.",
|
||||||
"MessageBackupsLocationNoEditNote": "OBS: Platsen där säkerhetskopiorna lagras bestäms av en central inställning och kan inte ändras här.",
|
"MessageBackupsLocationNoEditNote": "OBS: Platsen där säkerhetskopiorna lagras bestäms av en central inställning och kan inte ändras här.",
|
||||||
"MessageBackupsLocationPathEmpty": "Uppgiften om platsen för lagring av säkerhetskopior kan inte lämnas tom",
|
"MessageBackupsLocationPathEmpty": "Uppgiften om platsen för lagring av säkerhetskopior kan inte lämnas tom",
|
||||||
|
"MessageBatchEditPopulateMapDetailsAllHelp": "Adderar information från alla objekt nedan i de fält som aktiverats. Om fälten innehåller olika uppgifter kommer informationen att slås samman.",
|
||||||
|
"MessageBatchEditPopulateMapDetailsItemHelp": "Addera information från detta objekt i aktiva fält ovan",
|
||||||
"MessageBatchQuickMatchDescription": "Quick Match kommer försöka lägga till saknade omslag och metadata för de valda föremålen. Aktivera alternativen nedan för att tillåta Quick Match att överskriva befintliga omslag och/eller metadata.",
|
"MessageBatchQuickMatchDescription": "Quick Match kommer försöka lägga till saknade omslag och metadata för de valda föremålen. Aktivera alternativen nedan för att tillåta Quick Match att överskriva befintliga omslag och/eller metadata.",
|
||||||
"MessageBookshelfNoCollections": "Du har ännu inte skapat några samlingar",
|
"MessageBookshelfNoCollections": "Du har ännu inte skapat några samlingar",
|
||||||
|
"MessageBookshelfNoCollectionsHelp": "Samlingar är privata. Endast den användare som skapat en samling kan se den.",
|
||||||
"MessageBookshelfNoRSSFeeds": "Inga RSS-flöden är öppna",
|
"MessageBookshelfNoRSSFeeds": "Inga RSS-flöden är öppna",
|
||||||
"MessageBookshelfNoResultsForFilter": "Inga resultat för filter \"{0}: {1}\"",
|
"MessageBookshelfNoResultsForFilter": "Inga resultat för filter \"{0}: {1}\"",
|
||||||
"MessageBookshelfNoResultsForQuery": "Sökningen gav inget resultat",
|
"MessageBookshelfNoResultsForQuery": "Sökningen gav inget resultat",
|
||||||
@@ -674,19 +700,20 @@
|
|||||||
"MessageConfirmPurgeCache": "När du rensar cashen kommer katalogen <code>/metadata/cache</code> att raderas. <br /><br />Är du säker på att du vill radera katalogen?",
|
"MessageConfirmPurgeCache": "När du rensar cashen kommer katalogen <code>/metadata/cache</code> att raderas. <br /><br />Är du säker på att du vill radera katalogen?",
|
||||||
"MessageConfirmPurgeItemsCache": "När du rensar cashen för föremål kommer katalogen <code>/metadata/cache/items</code> att raderas. <br /><br />Är du säker på att du vill radera katalogen?",
|
"MessageConfirmPurgeItemsCache": "När du rensar cashen för föremål kommer katalogen <code>/metadata/cache/items</code> att raderas. <br /><br />Är du säker på att du vill radera katalogen?",
|
||||||
"MessageConfirmQuickEmbed": "VARNING! Quick embed kommer inte att säkerhetskopiera dina ljudfiler. Se till att du har en säkerhetskopia av dina ljudfiler. <br><br>Vill du fortsätta?",
|
"MessageConfirmQuickEmbed": "VARNING! Quick embed kommer inte att säkerhetskopiera dina ljudfiler. Se till att du har en säkerhetskopia av dina ljudfiler. <br><br>Vill du fortsätta?",
|
||||||
"MessageConfirmReScanLibraryItems": "Är du säker på att du vill göra en ny genomsökning för {0} objekt?",
|
"MessageConfirmQuickMatchEpisodes": "Snabbmatchning av avsnitt kommer att ersätta befintlig information vid en träff. Endast omatchade avsnitt kommer att uppdateras. Vill du fortsätta?",
|
||||||
|
"MessageConfirmReScanLibraryItems": "Är du säker på att du vill göra en ny skanning för {0} objekt?",
|
||||||
"MessageConfirmRemoveAllChapters": "Är du säker på att du vill ta bort alla kapitel?",
|
"MessageConfirmRemoveAllChapters": "Är du säker på att du vill ta bort alla kapitel?",
|
||||||
"MessageConfirmRemoveAuthor": "Är du säker på att du vill ta bort författaren \"{0}\"?",
|
"MessageConfirmRemoveAuthor": "Är du säker på att du vill ta bort författaren \"{0}\"?",
|
||||||
"MessageConfirmRemoveCollection": "Är du säker på att du vill ta bort samlingen \"{0}\"?",
|
"MessageConfirmRemoveCollection": "Är du säker på att du vill ta bort samlingen \"{0}\"?",
|
||||||
"MessageConfirmRemoveEpisode": "Är du säker på att du vill ta bort avsnittet \"{0}\"?",
|
"MessageConfirmRemoveEpisode": "Är du säker på att du vill radera avsnittet \"{0}\"?",
|
||||||
"MessageConfirmRemoveEpisodes": "Är du säker på att du vill ta bort {0} avsnitt?",
|
"MessageConfirmRemoveEpisodes": "Är du säker på att du vill radera {0} avsnitt?",
|
||||||
"MessageConfirmRemoveListeningSessions": "Är du säker på att du vill radera {0} lyssningstillfällen?",
|
"MessageConfirmRemoveListeningSessions": "Är du säker på att du vill radera {0} lyssningstillfällen?",
|
||||||
"MessageConfirmRemoveMetadataFiles": "Är du säker på att du vill radera filerna 'metadata.{0}' i alla mappar i ditt bibliotek?",
|
"MessageConfirmRemoveMetadataFiles": "Är du säker på att du vill radera filerna 'metadata.{0}' i alla mappar i ditt bibliotek?",
|
||||||
"MessageConfirmRemoveNarrator": "Är du säker på att du vill ta bort uppläsaren \"{0}\"?",
|
"MessageConfirmRemoveNarrator": "Är du säker på att du vill ta bort uppläsaren \"{0}\"?",
|
||||||
"MessageConfirmRemovePlaylist": "Är du säker på att du vill ta bort din spellista \"{0}\"?",
|
"MessageConfirmRemovePlaylist": "Är du säker på att du vill ta bort din spellista \"{0}\"?",
|
||||||
"MessageConfirmRenameGenre": "Är du säker på att du vill byta namn på kategorin \"{0}\" till \"{1}\" för alla objekt?",
|
"MessageConfirmRenameGenre": "Är du säker på att du vill byta namn på kategorin \"{0}\" till \"{1}\" för alla objekt?",
|
||||||
"MessageConfirmRenameGenreMergeNote": "OBS: Den här kategorin finns redan, så de kommer att slås samman.",
|
"MessageConfirmRenameGenreMergeNote": "OBS: Den här kategorin finns redan, så de kommer att slås samman.",
|
||||||
"MessageConfirmRenameGenreWarning": "Varning! En liknande kategori med annat skrivsätt finns redan \"{0}\".",
|
"MessageConfirmRenameGenreWarning": "VARNING! En liknande kategori med annat skrivsätt finns redan \"{0}\".",
|
||||||
"MessageConfirmRenameTag": "Är du säker på att du vill byta namn på taggen \"{0}\" till \"{1}\" för alla objekt?",
|
"MessageConfirmRenameTag": "Är du säker på att du vill byta namn på taggen \"{0}\" till \"{1}\" för alla objekt?",
|
||||||
"MessageConfirmRenameTagMergeNote": "OBS: Den här taggen finns redan, så de kommer att slås samman.",
|
"MessageConfirmRenameTagMergeNote": "OBS: Den här taggen finns redan, så de kommer att slås samman.",
|
||||||
"MessageConfirmRenameTagWarning": "VARNING! En liknande tagg med annat skrivsätt finns redan \"{0}\".",
|
"MessageConfirmRenameTagWarning": "VARNING! En liknande tagg med annat skrivsätt finns redan \"{0}\".",
|
||||||
@@ -697,7 +724,7 @@
|
|||||||
"MessageDragFilesIntoTrackOrder": "Dra filer till rätt spårordning",
|
"MessageDragFilesIntoTrackOrder": "Dra filer till rätt spårordning",
|
||||||
"MessageEmbedFinished": "Inbäddning genomförd!",
|
"MessageEmbedFinished": "Inbäddning genomförd!",
|
||||||
"MessageEpisodesQueuedForDownload": "{0} avsnitt i kö för nedladdning",
|
"MessageEpisodesQueuedForDownload": "{0} avsnitt i kö för nedladdning",
|
||||||
"MessageEreaderDevices": "För att säkerställa överföring av e-böcker kan du bli tvungen<br>att addera ovanstående e-postadress som godkänd<br>avsändare för varje enhet angiven nedan.",
|
"MessageEreaderDevices": "För att säkerställa överföring av e-böcker kan du bli tvungen<br>att addera ovanstående e-postadress som godkänd avsändare<br>för varje enhet angiven nedan.",
|
||||||
"MessageFeedURLWillBe": "Flödes-URL kommer att vara {0}",
|
"MessageFeedURLWillBe": "Flödes-URL kommer att vara {0}",
|
||||||
"MessageFetching": "Hämtar...",
|
"MessageFetching": "Hämtar...",
|
||||||
"MessageForceReScanDescription": "kommer att göra en omgångssökning av alla filer som en färsk sökning. ID3-taggar för ljudfiler, OPF-filer och textfiler kommer att sökas som nya.",
|
"MessageForceReScanDescription": "kommer att göra en omgångssökning av alla filer som en färsk sökning. ID3-taggar för ljudfiler, OPF-filer och textfiler kommer att sökas som nya.",
|
||||||
@@ -716,19 +743,19 @@
|
|||||||
"MessageMarkAllEpisodesNotFinished": "Markera alla avsnitt som ej avslutade",
|
"MessageMarkAllEpisodesNotFinished": "Markera alla avsnitt som ej avslutade",
|
||||||
"MessageMarkAsFinished": "Markera som avslutad",
|
"MessageMarkAsFinished": "Markera som avslutad",
|
||||||
"MessageMarkAsNotFinished": "Markera som ej avslutad",
|
"MessageMarkAsNotFinished": "Markera som ej avslutad",
|
||||||
"MessageMatchBooksDescription": "kommer att försöka matcha böcker i biblioteket med en bok från<br>den valda källan och fylla i uppgifter som saknas och bokomslag.<br>Inga befintliga uppgifter kommer att ersättas.",
|
"MessageMatchBooksDescription": "kommer att försöka matcha böcker i biblioteket med en bok från den valda källan och fylla i uppgifter som saknas och omslag. Inga befintliga uppgifter kommer att ersättas.",
|
||||||
"MessageNoAudioTracks": "Inga ljudspår har hittats",
|
"MessageNoAudioTracks": "Inga ljudspår har hittats",
|
||||||
"MessageNoAuthors": "Inga författare",
|
"MessageNoAuthors": "Inga författare",
|
||||||
"MessageNoBackups": "Inga säkerhetskopior",
|
"MessageNoBackups": "Inga säkerhetskopior",
|
||||||
"MessageNoBookmarks": "Inga bokmärken",
|
"MessageNoBookmarks": "Inga bokmärken",
|
||||||
"MessageNoChapters": "Inga kapitel",
|
"MessageNoChapters": "Inga kapitel",
|
||||||
"MessageNoCollections": "Inga samlingar",
|
"MessageNoCollections": "Inga samlingar",
|
||||||
"MessageNoCoversFound": "Inga bokomslag hittades",
|
"MessageNoCoversFound": "Inga omslag hittades",
|
||||||
"MessageNoDescription": "Ingen beskrivning",
|
"MessageNoDescription": "Ingen beskrivning",
|
||||||
"MessageNoDevices": "Inga enheter angivna",
|
"MessageNoDevices": "Inga enheter angivna",
|
||||||
"MessageNoDownloadsInProgress": "Inga nedladdningar pågår för närvarande",
|
"MessageNoDownloadsInProgress": "Inga nedladdningar pågår för närvarande",
|
||||||
"MessageNoDownloadsQueued": "Inga nedladdningar i kö",
|
"MessageNoDownloadsQueued": "Inga nedladdningar i kö",
|
||||||
"MessageNoEpisodeMatchesFound": "Inga matchande avsnitt hittades",
|
"MessageNoEpisodeMatchesFound": "Inga matchande avsnitt kunde hittas",
|
||||||
"MessageNoEpisodes": "Inga avsnitt",
|
"MessageNoEpisodes": "Inga avsnitt",
|
||||||
"MessageNoFoldersAvailable": "Inga mappar tillgängliga",
|
"MessageNoFoldersAvailable": "Inga mappar tillgängliga",
|
||||||
"MessageNoGenres": "Inga kategorier",
|
"MessageNoGenres": "Inga kategorier",
|
||||||
@@ -747,6 +774,7 @@
|
|||||||
"MessageNoTasksRunning": "Inga pågående uppgifter",
|
"MessageNoTasksRunning": "Inga pågående uppgifter",
|
||||||
"MessageNoUpdatesWereNecessary": "Inga uppdateringar var nödvändiga",
|
"MessageNoUpdatesWereNecessary": "Inga uppdateringar var nödvändiga",
|
||||||
"MessageNoUserPlaylists": "Du har inga spellistor",
|
"MessageNoUserPlaylists": "Du har inga spellistor",
|
||||||
|
"MessageNoUserPlaylistsHelp": "Spellistor är privata. Endast den användare som skapat listan kan se den.",
|
||||||
"MessageNotYetImplemented": "Ännu inte implementerad",
|
"MessageNotYetImplemented": "Ännu inte implementerad",
|
||||||
"MessageOr": "eller",
|
"MessageOr": "eller",
|
||||||
"MessagePauseChapter": "Pausa kapiteluppspelning",
|
"MessagePauseChapter": "Pausa kapiteluppspelning",
|
||||||
@@ -754,9 +782,11 @@
|
|||||||
"MessagePlaylistCreateFromCollection": "Skapa en spellista från samlingen",
|
"MessagePlaylistCreateFromCollection": "Skapa en spellista från samlingen",
|
||||||
"MessagePleaseWait": "Vänta ett ögonblick...",
|
"MessagePleaseWait": "Vänta ett ögonblick...",
|
||||||
"MessagePodcastHasNoRSSFeedForMatching": "Podcasten har ingen RSS-flödes-URL att använda för matchning",
|
"MessagePodcastHasNoRSSFeedForMatching": "Podcasten har ingen RSS-flödes-URL att använda för matchning",
|
||||||
|
"MessagePodcastSearchField": "Skriv sökfrågan eller URL-adressen för RSS-flödet",
|
||||||
|
"MessageQuickMatchAllEpisodes": "Snabbmatchning av alla avsnitt",
|
||||||
"MessageQuickMatchDescription": "Adderar uppgifter som saknas samt en omslagsbild från<br>första träffen i resultatet vid sökningen från '{0}'.<br>Skriver inte över befintliga uppgifter om inte<br>inställningen 'Prioritera matchad metadata' är aktiverad.",
|
"MessageQuickMatchDescription": "Adderar uppgifter som saknas samt en omslagsbild från<br>första träffen i resultatet vid sökningen från '{0}'.<br>Skriver inte över befintliga uppgifter om inte<br>inställningen 'Prioritera matchad metadata' är aktiverad.",
|
||||||
"MessageRemoveChapter": "Ta bort kapitel",
|
"MessageRemoveChapter": "Ta bort kapitel",
|
||||||
"MessageRemoveEpisodes": "Ta bort {0} avsnitt",
|
"MessageRemoveEpisodes": "Radera {0} avsnitt",
|
||||||
"MessageRemoveFromPlayerQueue": "Ta bort från spellistan",
|
"MessageRemoveFromPlayerQueue": "Ta bort från spellistan",
|
||||||
"MessageRemoveUserWarning": "Är du säker på att du vill radera användaren \"{0}\"?",
|
"MessageRemoveUserWarning": "Är du säker på att du vill radera användaren \"{0}\"?",
|
||||||
"MessageReportBugsAndContribute": "Rapportera buggar, begär funktioner och bidra på",
|
"MessageReportBugsAndContribute": "Rapportera buggar, begär funktioner och bidra på",
|
||||||
@@ -770,17 +800,32 @@
|
|||||||
"MessageSetChaptersFromTracksDescription": "Ställ in kapitel med varje ljudfil som ett kapitel och kapitelrubrik som ljudfilens namn",
|
"MessageSetChaptersFromTracksDescription": "Ställ in kapitel med varje ljudfil som ett kapitel och kapitelrubrik som ljudfilens namn",
|
||||||
"MessageStartPlaybackAtTime": "Starta uppspelning av \"{0}\" vid tidpunkt {1}?",
|
"MessageStartPlaybackAtTime": "Starta uppspelning av \"{0}\" vid tidpunkt {1}?",
|
||||||
"MessageTaskCanceledByUser": "Uppgiften avslutades av användaren",
|
"MessageTaskCanceledByUser": "Uppgiften avslutades av användaren",
|
||||||
|
"MessageTaskDownloadingEpisodeDescription": "Laddar ner avsnitt \"{0}\"",
|
||||||
|
"MessageTaskEmbeddingMetadata": "Infogar metadata",
|
||||||
|
"MessageTaskEmbeddingMetadataDescription": "Infogar metadata i ljudboken \"{0}\"",
|
||||||
"MessageTaskEncodingM4bDescription": "Omkodning av ljudbok \"{0}\" till en M4B-fil",
|
"MessageTaskEncodingM4bDescription": "Omkodning av ljudbok \"{0}\" till en M4B-fil",
|
||||||
"MessageTaskFailed": "Misslyckades",
|
"MessageTaskFailed": "Misslyckades",
|
||||||
|
"MessageTaskFailedToBackupAudioFile": "Misslyckades med att göra backup på ljudfil \"{0}\"",
|
||||||
"MessageTaskFailedToCreateCacheDirectory": "Misslyckades med att skapa bibliotek för cachen",
|
"MessageTaskFailedToCreateCacheDirectory": "Misslyckades med att skapa bibliotek för cachen",
|
||||||
|
"MessageTaskFailedToEmbedMetadataInFile": "Misslyckades med att infoga metadata i \"{0}\"",
|
||||||
|
"MessageTaskFailedToMergeAudioFiles": "Misslyckades med att sammanfoga ljudfilerna",
|
||||||
|
"MessageTaskFailedToMoveM4bFile": "Misslyckades med att flytta M4B-filen",
|
||||||
|
"MessageTaskFailedToWriteMetadataFile": "Misslyckades med att skapa filen med metadata",
|
||||||
"MessageTaskMatchingBooksInLibrary": "Matchar böcker i biblioteket \"{0}\"",
|
"MessageTaskMatchingBooksInLibrary": "Matchar böcker i biblioteket \"{0}\"",
|
||||||
|
"MessageTaskOpmlImportFeedPodcastDescription": "Skapar podcast \"{0}\"",
|
||||||
|
"MessageTaskOpmlImportFeedPodcastFailed": "Misslyckades med att skapa podcast",
|
||||||
|
"MessageTaskOpmlImportFinished": "Adderade {0} podcasts",
|
||||||
|
"MessageTaskOpmlParseFailed": "Misslyckades att tolka OPML-filen",
|
||||||
|
"MessageTaskOpmlParseFastFail": "Felaktig OPML-fil. Ingen <opml> tag eller <outline> tag finns i filen",
|
||||||
|
"MessageTaskOpmlParseNoneFound": "Inget flöde finns angivet i OPML-filen",
|
||||||
"MessageTaskScanItemsAdded": "{0} adderades",
|
"MessageTaskScanItemsAdded": "{0} adderades",
|
||||||
|
"MessageTaskScanItemsMissing": "{0} saknades",
|
||||||
"MessageTaskScanItemsUpdated": "{0} uppdaterades",
|
"MessageTaskScanItemsUpdated": "{0} uppdaterades",
|
||||||
"MessageTaskScanNoChangesNeeded": "Inget adderades eller uppdaterades",
|
"MessageTaskScanNoChangesNeeded": "Inget adderades eller uppdaterades",
|
||||||
"MessageTaskScanningLibrary": "Biblioteket \"{0}\" har skannats",
|
"MessageTaskScanningLibrary": "Biblioteket \"{0}\" har skannats",
|
||||||
"MessageThinking": "Tänker...",
|
"MessageThinking": "Tänker...",
|
||||||
"MessageUploaderItemFailed": "Misslyckades med att ladda upp",
|
"MessageUploaderItemFailed": "Misslyckades med att ladda upp",
|
||||||
"MessageUploaderItemSuccess": "Uppladdning lyckades!",
|
"MessageUploaderItemSuccess": "har blivit uppladdad!",
|
||||||
"MessageUploading": "Laddar upp...",
|
"MessageUploading": "Laddar upp...",
|
||||||
"MessageValidCronExpression": "Giltigt cron-uttryck",
|
"MessageValidCronExpression": "Giltigt cron-uttryck",
|
||||||
"MessageWatcherIsDisabledGlobally": "Watcher är inaktiverad centralt under rubriken 'Inställningar'",
|
"MessageWatcherIsDisabledGlobally": "Watcher är inaktiverad centralt under rubriken 'Inställningar'",
|
||||||
@@ -791,10 +836,13 @@
|
|||||||
"NoteChapterEditorTimes": "OBS: Starttiden för första kapitlet måste vara 0:00 och starttiden för det sista kapitlet får inte överstiga ljudbokens totala varaktighet.",
|
"NoteChapterEditorTimes": "OBS: Starttiden för första kapitlet måste vara 0:00 och starttiden för det sista kapitlet får inte överstiga ljudbokens totala varaktighet.",
|
||||||
"NoteFolderPicker": "OBS: Mappar som redan är kopplade kommer inte att visas",
|
"NoteFolderPicker": "OBS: Mappar som redan är kopplade kommer inte att visas",
|
||||||
"NoteRSSFeedPodcastAppsHttps": "VARNING: De flesta applikationer för podcasts kräver att URL:en för RSS-flödet använder HTTPS",
|
"NoteRSSFeedPodcastAppsHttps": "VARNING: De flesta applikationer för podcasts kräver att URL:en för RSS-flödet använder HTTPS",
|
||||||
"NoteRSSFeedPodcastAppsPubDate": "VARNING: Ett eller flera av dina avsnitt har inte ett publiceringsdatum. Vissa applikationer för podcasts kräver detta.",
|
"NoteRSSFeedPodcastAppsPubDate": "VARNING: Ett eller flera av dina avsnitt saknar publiceringsdatum. Vissa applikationer för podcasts kräver detta.",
|
||||||
"NoteUploaderFoldersWithMediaFiles": "Mappar som innehåller mediefiler hanteras som separata objekt i biblioteket.",
|
"NoteUploaderFoldersWithMediaFiles": "Mappar som innehåller mediefiler hanteras som separata objekt i biblioteket.",
|
||||||
"NoteUploaderOnlyAudioFiles": "Om du bara laddar upp ljudfiler kommer varje ljudfil att hanteras som en separat ljudbok.",
|
"NoteUploaderOnlyAudioFiles": "Om du bara laddar upp ljudfiler kommer varje ljudfil att hanteras som en separat ljudbok.",
|
||||||
"NoteUploaderUnsupportedFiles": "Oaccepterade filer ignoreras. När du väljer eller släpper en mapp ignoreras andra filer som inte finns i ett objektmapp.",
|
"NoteUploaderUnsupportedFiles": "Oaccepterade filer ignoreras. När du väljer eller släpper en mapp ignoreras andra filer som inte finns i ett objektmapp.",
|
||||||
|
"NotificationOnBackupCompletedDescription": "Aktiveras när en backup är genomförd",
|
||||||
|
"NotificationOnBackupFailedDescription": "Aktiveras när en backup misslyckas",
|
||||||
|
"NotificationOnEpisodeDownloadedDescription": "Aktiveras när avsnitt i en podcast automatiskt har hämtats",
|
||||||
"PlaceholderNewCollection": "Nytt namn på samlingen",
|
"PlaceholderNewCollection": "Nytt namn på samlingen",
|
||||||
"PlaceholderNewFolderPath": "Nytt sökväg till mappen",
|
"PlaceholderNewFolderPath": "Nytt sökväg till mappen",
|
||||||
"PlaceholderNewPlaylist": "Nytt namn på spellistan",
|
"PlaceholderNewPlaylist": "Nytt namn på spellistan",
|
||||||
@@ -845,30 +893,39 @@
|
|||||||
"ToastCachePurgeSuccess": "Rensning av cachen har genomförts",
|
"ToastCachePurgeSuccess": "Rensning av cachen har genomförts",
|
||||||
"ToastChaptersHaveErrors": "Kapitlen har fel",
|
"ToastChaptersHaveErrors": "Kapitlen har fel",
|
||||||
"ToastChaptersMustHaveTitles": "Kapitel måste ha titlar",
|
"ToastChaptersMustHaveTitles": "Kapitel måste ha titlar",
|
||||||
|
"ToastChaptersRemoved": "Kapitlen har raderats",
|
||||||
|
"ToastChaptersUpdated": "Kapitlen har uppdaterats",
|
||||||
"ToastCollectionItemsAddFailed": "Misslyckades med att addera böcker till samlingen",
|
"ToastCollectionItemsAddFailed": "Misslyckades med att addera böcker till samlingen",
|
||||||
"ToastCollectionRemoveSuccess": "Samlingen har raderats",
|
"ToastCollectionRemoveSuccess": "Samlingen har raderats",
|
||||||
"ToastCollectionUpdateSuccess": "Samlingen har uppdaterats",
|
"ToastCollectionUpdateSuccess": "Samlingen har uppdaterats",
|
||||||
"ToastCoverUpdateFailed": "Uppdatering av bokomslag misslyckades",
|
"ToastCoverUpdateFailed": "Uppdatering av omslag misslyckades",
|
||||||
"ToastDateTimeInvalidOrIncomplete": "Datum och klockslag är felaktigt eller ej komplett",
|
"ToastDateTimeInvalidOrIncomplete": "Datum och klockslag är felaktigt eller ej komplett",
|
||||||
"ToastDeleteFileFailed": "Misslyckades att radera filen",
|
"ToastDeleteFileFailed": "Misslyckades att radera filen",
|
||||||
"ToastDeleteFileSuccess": "Filen har raderats",
|
"ToastDeleteFileSuccess": "Filen har raderats",
|
||||||
|
"ToastDeviceAddFailed": "Misslyckades med att addera enheten",
|
||||||
|
"ToastDeviceNameAlreadyExists": "En enhet för att läsa e-böcker med det namnet finns redan",
|
||||||
"ToastDeviceTestEmailFailed": "Misslyckades med att skicka ett testmail",
|
"ToastDeviceTestEmailFailed": "Misslyckades med att skicka ett testmail",
|
||||||
"ToastDeviceTestEmailSuccess": "Ett testmail har skickats",
|
"ToastDeviceTestEmailSuccess": "Ett testmail har skickats",
|
||||||
"ToastEmailSettingsUpdateSuccess": "Inställningarna av e-post har uppdaterats",
|
"ToastEmailSettingsUpdateSuccess": "Inställningarna av e-post har uppdaterats",
|
||||||
|
"ToastEncodeCancelFailed": "Misslyckades med att avbryta omkodningen",
|
||||||
"ToastEncodeCancelSucces": "Omkodningen avbruten",
|
"ToastEncodeCancelSucces": "Omkodningen avbruten",
|
||||||
|
"ToastEpisodeDownloadQueueClearFailed": "Misslyckades med att tömma kön",
|
||||||
|
"ToastEpisodeDownloadQueueClearSuccess": "Kö för nedladdning av avsnitt har tömts",
|
||||||
|
"ToastEpisodeUpdateSuccess": "{0} avsnitt uppdaterades",
|
||||||
"ToastFailedToLoadData": "Misslyckades med att ladda data",
|
"ToastFailedToLoadData": "Misslyckades med att ladda data",
|
||||||
"ToastFailedToUpdate": "Misslyckades med att uppdatera",
|
"ToastFailedToUpdate": "Misslyckades med att uppdatera",
|
||||||
"ToastInvalidImageUrl": "Felaktig URL-adress till omslagsbilden",
|
"ToastInvalidImageUrl": "Felaktig URL-adress till omslagsbilden",
|
||||||
"ToastInvalidMaxEpisodesToDownload": "Ogiltigt maximalt antal avsnitt att ladda ner",
|
"ToastInvalidMaxEpisodesToDownload": "Ogiltigt maximalt antal avsnitt att ladda ner",
|
||||||
"ToastInvalidUrl": "Felaktig URL-adress",
|
"ToastInvalidUrl": "Felaktig URL-adress",
|
||||||
"ToastItemCoverUpdateSuccess": "Objektets bokomslag har uppdaterats",
|
"ToastItemCoverUpdateSuccess": "Objektets omslag har uppdaterats",
|
||||||
"ToastItemDeletedFailed": "Misslyckades med att radera objektet",
|
"ToastItemDeletedFailed": "Misslyckades med att radera objektet",
|
||||||
"ToastItemDeletedSuccess": "Objektet har raderats",
|
"ToastItemDeletedSuccess": "Objektet har raderats",
|
||||||
"ToastItemDetailsUpdateSuccess": "Detaljerna om boken har uppdaterats",
|
"ToastItemDetailsUpdateSuccess": "Informationen om objektet har uppdaterats",
|
||||||
"ToastItemMarkedAsFinishedFailed": "Misslyckades med att markera den som avslutad",
|
"ToastItemMarkedAsFinishedFailed": "Misslyckades med att markera den som avslutad",
|
||||||
"ToastItemMarkedAsFinishedSuccess": "Den har markerat som avslutad",
|
"ToastItemMarkedAsFinishedSuccess": "Den har markerat som avslutad",
|
||||||
"ToastItemMarkedAsNotFinishedFailed": "Misslyckades med att markera den som ej avslutad",
|
"ToastItemMarkedAsNotFinishedFailed": "Misslyckades med att markera den som ej avslutad",
|
||||||
"ToastItemMarkedAsNotFinishedSuccess": "Den har markerats som ej avslutad",
|
"ToastItemMarkedAsNotFinishedSuccess": "Den har markerats som ej avslutad",
|
||||||
|
"ToastItemUpdateSuccess": "Objektet har uppdaterats",
|
||||||
"ToastLibraryCreateFailed": "Det gick inte att skapa biblioteket",
|
"ToastLibraryCreateFailed": "Det gick inte att skapa biblioteket",
|
||||||
"ToastLibraryCreateSuccess": "Biblioteket \"{0}\" har skapats",
|
"ToastLibraryCreateSuccess": "Biblioteket \"{0}\" har skapats",
|
||||||
"ToastLibraryDeleteFailed": "Det gick inte att ta bort biblioteket",
|
"ToastLibraryDeleteFailed": "Det gick inte att ta bort biblioteket",
|
||||||
@@ -876,26 +933,34 @@
|
|||||||
"ToastLibraryScanFailedToStart": "Misslyckades med att starta skanningen",
|
"ToastLibraryScanFailedToStart": "Misslyckades med att starta skanningen",
|
||||||
"ToastLibraryScanStarted": "Skanning av biblioteket påbörjad",
|
"ToastLibraryScanStarted": "Skanning av biblioteket påbörjad",
|
||||||
"ToastLibraryUpdateSuccess": "Biblioteket \"{0}\" har uppdaterats",
|
"ToastLibraryUpdateSuccess": "Biblioteket \"{0}\" har uppdaterats",
|
||||||
|
"ToastMatchAllAuthorsFailed": "Misslyckades med att matcha alla författare",
|
||||||
"ToastMetadataFilesRemovedError": "Misslyckades med att radera 'metadata.{0}' filerna",
|
"ToastMetadataFilesRemovedError": "Misslyckades med att radera 'metadata.{0}' filerna",
|
||||||
"ToastMetadataFilesRemovedNoneFound": "Inga 'metadata.{0}' filer hittades i biblioteket",
|
"ToastMetadataFilesRemovedNoneFound": "Inga 'metadata.{0}' filer hittades i biblioteket",
|
||||||
"ToastMetadataFilesRemovedNoneRemoved": "Inga 'metadata.{0}' filer raderades",
|
"ToastMetadataFilesRemovedNoneRemoved": "Inga 'metadata.{0}' filer raderades",
|
||||||
"ToastMetadataFilesRemovedSuccess": "{0} 'metadata.{1}' raderades",
|
"ToastMetadataFilesRemovedSuccess": "{0} 'metadata.{1}' raderades",
|
||||||
"ToastNameEmailRequired": "Ett namn och en e-postadress måste anges",
|
"ToastNameEmailRequired": "Ett namn och en e-postadress måste anges",
|
||||||
"ToastNameRequired": "Ett namn måste anges",
|
"ToastNameRequired": "Ett namn måste anges",
|
||||||
|
"ToastNewEpisodesFound": "Hittade {0} nya avsnitt",
|
||||||
"ToastNewUserCreatedFailed": "Misslyckades med att skapa kontot \"{0}\"",
|
"ToastNewUserCreatedFailed": "Misslyckades med att skapa kontot \"{0}\"",
|
||||||
"ToastNewUserCreatedSuccess": "Ett nytt konto har skapats",
|
"ToastNewUserCreatedSuccess": "Ett nytt konto har skapats",
|
||||||
"ToastNewUserLibraryError": "Minst ett bibliotek måste anges",
|
"ToastNewUserLibraryError": "Minst ett bibliotek måste anges",
|
||||||
"ToastNewUserPasswordError": "Ett lösenord måste anges. Endast användaren 'root' kan vara utan lösenord.",
|
"ToastNewUserPasswordError": "Ett lösenord måste anges. Endast användaren 'root' kan vara utan lösenord.",
|
||||||
|
"ToastNewUserTagError": "Minst en tagg måste läggas till",
|
||||||
"ToastNewUserUsernameError": "Ange ett användarnamn",
|
"ToastNewUserUsernameError": "Ange ett användarnamn",
|
||||||
|
"ToastNoNewEpisodesFound": "Inga nya avsnitt kunde hittas",
|
||||||
|
"ToastNoRSSFeed": "Denna podcast har ingen RSS-flöde",
|
||||||
"ToastNoUpdatesNecessary": "Inga uppdateringar var nödvändiga",
|
"ToastNoUpdatesNecessary": "Inga uppdateringar var nödvändiga",
|
||||||
"ToastNotificationCreateFailed": "Misslyckades med att skapa meddelandet",
|
"ToastNotificationCreateFailed": "Misslyckades med att skapa meddelandet",
|
||||||
"ToastNotificationDeleteFailed": "Misslyckades med att radera meddelandet",
|
"ToastNotificationDeleteFailed": "Misslyckades med att radera meddelandet",
|
||||||
|
"ToastNotificationUpdateSuccess": "Meddelandet har uppdaterats",
|
||||||
"ToastPlaylistCreateFailed": "Det gick inte att skapa spellistan",
|
"ToastPlaylistCreateFailed": "Det gick inte att skapa spellistan",
|
||||||
"ToastPlaylistCreateSuccess": "Spellistan skapad",
|
"ToastPlaylistCreateSuccess": "Spellistan skapad",
|
||||||
"ToastPlaylistRemoveSuccess": "Spellistan har tagits bort",
|
"ToastPlaylistRemoveSuccess": "Spellistan har tagits bort",
|
||||||
"ToastPlaylistUpdateSuccess": "Spellistan uppdaterad",
|
"ToastPlaylistUpdateSuccess": "Spellistan har uppdaterats",
|
||||||
"ToastPodcastCreateFailed": "Misslyckades med att skapa podcasten",
|
"ToastPodcastCreateFailed": "Misslyckades med att skapa podcasten",
|
||||||
"ToastPodcastCreateSuccess": "Podcasten skapad framgångsrikt",
|
"ToastPodcastCreateSuccess": "Podcasten skapades framgångsrikt",
|
||||||
|
"ToastPodcastNoEpisodesInFeed": "Inga avsnitt finns i RSS-flödet",
|
||||||
|
"ToastPodcastNoRssFeed": "Denna podcast har ingen RSS-flöde",
|
||||||
"ToastProviderCreatedFailed": "Misslyckades med att addera en källa",
|
"ToastProviderCreatedFailed": "Misslyckades med att addera en källa",
|
||||||
"ToastProviderCreatedSuccess": "En ny källa har adderats",
|
"ToastProviderCreatedSuccess": "En ny källa har adderats",
|
||||||
"ToastProviderNameAndUrlRequired": "Ett namn och en URL-adress krävs",
|
"ToastProviderNameAndUrlRequired": "Ett namn och en URL-adress krävs",
|
||||||
@@ -905,7 +970,14 @@
|
|||||||
"ToastRemoveFailed": "Misslyckades med att radera",
|
"ToastRemoveFailed": "Misslyckades med att radera",
|
||||||
"ToastRemoveItemFromCollectionFailed": "Misslyckades med att ta bort objektet från samlingen",
|
"ToastRemoveItemFromCollectionFailed": "Misslyckades med att ta bort objektet från samlingen",
|
||||||
"ToastRemoveItemFromCollectionSuccess": "Objektet borttaget från samlingen",
|
"ToastRemoveItemFromCollectionSuccess": "Objektet borttaget från samlingen",
|
||||||
|
"ToastRemoveItemsWithIssuesFailed": "Misslyckades med att radera objekt med problem",
|
||||||
|
"ToastRemoveItemsWithIssuesSuccess": "Raderade objekt med problem",
|
||||||
"ToastRenameFailed": "Misslyckades med att ändra namn",
|
"ToastRenameFailed": "Misslyckades med att ändra namn",
|
||||||
|
"ToastRescanFailed": "Skanningen misslyckades för {0}",
|
||||||
|
"ToastRescanRemoved": "Skanningen har genomförts - objektet har raderats",
|
||||||
|
"ToastRescanUpToDate": "Skanningen har genomförts - objektet behövde inte uppdateras",
|
||||||
|
"ToastRescanUpdated": "Skanningen har genomförts - objektet har uppdaterats",
|
||||||
|
"ToastScanFailed": "Misslyckades med att skanna biblioteket",
|
||||||
"ToastSelectAtLeastOneUser": "Åtminstone en användare måste väljas",
|
"ToastSelectAtLeastOneUser": "Åtminstone en användare måste väljas",
|
||||||
"ToastSendEbookToDeviceFailed": "Misslyckades med att skicka e-boken till enheten",
|
"ToastSendEbookToDeviceFailed": "Misslyckades med att skicka e-boken till enheten",
|
||||||
"ToastSendEbookToDeviceSuccess": "E-boken skickad till enheten \"{0}\"",
|
"ToastSendEbookToDeviceSuccess": "E-boken skickad till enheten \"{0}\"",
|
||||||
|
|||||||
+209
-1
@@ -1 +1,209 @@
|
|||||||
{}
|
{
|
||||||
|
"ButtonAdd": "Ekle",
|
||||||
|
"ButtonAddChapters": "Bölüm Ekle",
|
||||||
|
"ButtonAddDevice": "Cihaz Ekle",
|
||||||
|
"ButtonAddLibrary": "Kütüphane Ekle",
|
||||||
|
"ButtonAddPodcasts": "Podcast Ekle",
|
||||||
|
"ButtonAddUser": "Kullanıcı Ekle",
|
||||||
|
"ButtonAddYourFirstLibrary": "İlk kütüphaneni ekle",
|
||||||
|
"ButtonApply": "Uygula",
|
||||||
|
"ButtonApplyChapters": "Bölümleri Uygula",
|
||||||
|
"ButtonAuthors": "Yazarlar",
|
||||||
|
"ButtonBack": "Geri",
|
||||||
|
"ButtonBatchEditPopulateFromExisting": "Mevcut olandan çoğalt",
|
||||||
|
"ButtonBatchEditPopulateMapDetails": "Harita detaylarını çoğalt",
|
||||||
|
"ButtonBrowseForFolder": "Klasör için göz at",
|
||||||
|
"ButtonCancel": "İptal",
|
||||||
|
"ButtonCancelEncode": "Kodlamayı Durdur",
|
||||||
|
"ButtonChangeRootPassword": "Root Şifresini Değiştir",
|
||||||
|
"ButtonCheckAndDownloadNewEpisodes": "Yeni Bölümleri Kontrol Et & İndir",
|
||||||
|
"ButtonChooseAFolder": "Klasör seç",
|
||||||
|
"ButtonChooseFiles": "Dosya seç",
|
||||||
|
"ButtonClearFilter": "Filtreyi Temizle",
|
||||||
|
"ButtonCloseFeed": "Akışı Kapat",
|
||||||
|
"ButtonCloseSession": "Acık Oturumu Kapat",
|
||||||
|
"ButtonCollections": "Koleksiyonlar",
|
||||||
|
"ButtonConfigureScanner": "Tarayıcı Ayarları",
|
||||||
|
"ButtonCreate": "Oluştur",
|
||||||
|
"ButtonCreateBackup": "Yedek Oluştur",
|
||||||
|
"ButtonDelete": "Sil",
|
||||||
|
"ButtonDownloadQueue": "Sıra",
|
||||||
|
"ButtonEdit": "Düzenle",
|
||||||
|
"ButtonEditChapters": "Bölümleri Düzenle",
|
||||||
|
"ButtonEditPodcast": "Podcast Düzenle",
|
||||||
|
"ButtonEnable": "Etkinleştir",
|
||||||
|
"ButtonFireAndFail": "Gönder ve hata al",
|
||||||
|
"ButtonFireOnTest": "onTest Gönder",
|
||||||
|
"ButtonForceReScan": "Zorla Yeniden Tara",
|
||||||
|
"ButtonFullPath": "Tam Dosya Yolu",
|
||||||
|
"ButtonHide": "Gizle",
|
||||||
|
"ButtonHome": "Ana sayfa",
|
||||||
|
"ButtonIssues": "Sorunlar",
|
||||||
|
"ButtonJumpBackward": "Geri Sar",
|
||||||
|
"ButtonJumpForward": "İleri Sar",
|
||||||
|
"ButtonLatest": "En yeni",
|
||||||
|
"ButtonLibrary": "Kütüphane",
|
||||||
|
"ButtonLogout": "Çıkış Yap",
|
||||||
|
"ButtonLookup": "Sorgula",
|
||||||
|
"ButtonManageTracks": "Parçaları Yönet",
|
||||||
|
"ButtonMapChapterTitles": "Bölüm Başlıklarını Haritalandır",
|
||||||
|
"ButtonNevermind": "Vazgeç",
|
||||||
|
"ButtonNext": "Sonraki",
|
||||||
|
"ButtonNextChapter": "Sonraki Bölüm",
|
||||||
|
"ButtonNextItemInQueue": "Sıradaki Sonraki Öğe",
|
||||||
|
"ButtonOk": "Tamam",
|
||||||
|
"ButtonOpenFeed": "Akışı Aç",
|
||||||
|
"ButtonOpenManager": "Yöneticiyi Aç",
|
||||||
|
"ButtonPause": "Durdur",
|
||||||
|
"ButtonPlay": "Oynat",
|
||||||
|
"ButtonPlayAll": "Hepsini Oynat",
|
||||||
|
"ButtonPlaying": "Oynatılıyor",
|
||||||
|
"ButtonPlaylists": "Oynatma listeleri",
|
||||||
|
"ButtonPrevious": "Önceki",
|
||||||
|
"ButtonPreviousChapter": "Önceki Bölüm",
|
||||||
|
"ButtonProbeAudioFile": "Ses Dosyasını Yokla",
|
||||||
|
"ButtonPurgeAllCache": "Bütün Önbelleği Temizle",
|
||||||
|
"ButtonPurgeItemsCache": "Öğenin Önbelleğini Temizle",
|
||||||
|
"ButtonQueueAddItem": "Sıraya ekle",
|
||||||
|
"ButtonQueueRemoveItem": "Sıradan çıkar",
|
||||||
|
"ButtonReScan": "Yeniden Tara",
|
||||||
|
"ButtonRead": "Oku",
|
||||||
|
"ButtonReadLess": "Daha az göster",
|
||||||
|
"ButtonReadMore": "Daha fazla göster",
|
||||||
|
"ButtonRefresh": "Yenile",
|
||||||
|
"ButtonRemove": "Kaldır",
|
||||||
|
"ButtonRemoveAll": "Hepsini Sil",
|
||||||
|
"ButtonRemoveAllLibraryItems": "Bütün Kütüphane Öğelerini Sil",
|
||||||
|
"ButtonSave": "Kaydet",
|
||||||
|
"ButtonSearch": "Ara",
|
||||||
|
"ButtonSeries": "Dizi",
|
||||||
|
"ButtonSubmit": "Gönder",
|
||||||
|
"ButtonYes": "Evet",
|
||||||
|
"HeaderAccount": "Hesap",
|
||||||
|
"HeaderAdvanced": "Gelişmiş",
|
||||||
|
"HeaderAudioTracks": "Ses Kanalları",
|
||||||
|
"HeaderChapters": "Bölümler",
|
||||||
|
"HeaderCollection": "Koleksiyon",
|
||||||
|
"HeaderCollectionItems": "Koleksiyon Öğeleri",
|
||||||
|
"HeaderDetails": "Detaylar",
|
||||||
|
"HeaderEbookFiles": "Ebook Dosyaları",
|
||||||
|
"HeaderEpisodes": "Bölümler",
|
||||||
|
"HeaderEreaderSettings": "Ereader Ayarları",
|
||||||
|
"HeaderLatestEpisodes": "En son bölümler",
|
||||||
|
"HeaderLibraries": "Kütüphaneler",
|
||||||
|
"HeaderOpenRSSFeed": "RSS Akışını Aç",
|
||||||
|
"HeaderPlaylist": "Oynatma listesi",
|
||||||
|
"HeaderPlaylistItems": "Oynatma Listesi Öğeleri",
|
||||||
|
"HeaderRSSFeedGeneral": "RSS Detayları",
|
||||||
|
"HeaderRSSFeedIsOpen": "RSS Akışı Açık",
|
||||||
|
"HeaderSettings": "Ayarlar",
|
||||||
|
"HeaderSleepTimer": "Uyku Zamanlayıcısı",
|
||||||
|
"HeaderStatsMinutesListeningChart": "Dinlenilen Dakika (son 7 gün)",
|
||||||
|
"HeaderStatsRecentSessions": "Geçmiş Oturumlar",
|
||||||
|
"HeaderTableOfContents": "İçindekiler",
|
||||||
|
"HeaderYourStats": "İstatistiklerin",
|
||||||
|
"LabelAddToPlaylist": "Oynatma Listesine Ekle",
|
||||||
|
"LabelAddedAt": "Eklenme Zamanı",
|
||||||
|
"LabelAddedDate": "Eklendi {0}",
|
||||||
|
"LabelAll": "Hepsi",
|
||||||
|
"LabelAuthor": "Yazar",
|
||||||
|
"LabelAuthorFirstLast": "Yazar (İlk Son)",
|
||||||
|
"LabelAuthorLastFirst": "Yazar (Son, İlk)",
|
||||||
|
"LabelAuthors": "Yazarlar",
|
||||||
|
"LabelAutoDownloadEpisodes": "Bölümleri Otomatik Olarak İndir",
|
||||||
|
"LabelBooks": "Kitaplar",
|
||||||
|
"LabelChapters": "Bölümler",
|
||||||
|
"LabelClosePlayer": "Oynatıcıyı kapat",
|
||||||
|
"LabelCollapseSeries": "Seriyi Daralt",
|
||||||
|
"LabelComplete": "Tamamlandı",
|
||||||
|
"LabelContinueListening": "Dinlemeye Devam Et",
|
||||||
|
"LabelContinueReading": "Okumaya Devam Et",
|
||||||
|
"LabelContinueSeries": "Seriye Devam Et",
|
||||||
|
"LabelDescription": "Açıklama",
|
||||||
|
"LabelDiscover": "Keşfet",
|
||||||
|
"LabelDownload": "İndir",
|
||||||
|
"LabelDuration": "Süre",
|
||||||
|
"LabelEbook": "Ekitap",
|
||||||
|
"LabelEbooks": "Ekitaplar",
|
||||||
|
"LabelEnable": "Etkinleştir",
|
||||||
|
"LabelEnd": "Son",
|
||||||
|
"LabelEndOfChapter": "Bölüm Sonu",
|
||||||
|
"LabelEpisode": "Bölüm",
|
||||||
|
"LabelFeedURL": "Akış URLsi",
|
||||||
|
"LabelFile": "Dosya",
|
||||||
|
"LabelFileBirthtime": "Dosya Oluşum Zamanı",
|
||||||
|
"LabelFileModified": "Dosya Düzenlendi",
|
||||||
|
"LabelFilename": "Dosya İsmi",
|
||||||
|
"LabelFinished": "Tamamlandı",
|
||||||
|
"LabelFolder": "Klasör",
|
||||||
|
"LabelFontBoldness": "Font Kalınlığı",
|
||||||
|
"LabelFontScale": "Font büyüklüğü",
|
||||||
|
"LabelGenre": "Tür",
|
||||||
|
"LabelGenres": "Türler",
|
||||||
|
"LabelHasEbook": "Ekitabı var",
|
||||||
|
"LabelHasSupplementaryEbook": "İlave ekitabı var",
|
||||||
|
"LabelHost": "Sunucu",
|
||||||
|
"LabelInProgress": "İlerleme Halinde",
|
||||||
|
"LabelIncomplete": "Tamamlanmamış",
|
||||||
|
"LabelLanguage": "Dil",
|
||||||
|
"LabelLayout": "Düzen",
|
||||||
|
"LabelLayoutSinglePage": "Tek sayfa",
|
||||||
|
"LabelLineSpacing": "Satır aralığı",
|
||||||
|
"LabelListenAgain": "Tekrar Dinle",
|
||||||
|
"LabelMediaType": "Medya Türü",
|
||||||
|
"LabelMissing": "Kayıp",
|
||||||
|
"LabelMore": "Daha fazla",
|
||||||
|
"LabelMoreInfo": "Daha fazla bilgi",
|
||||||
|
"LabelName": "İsim",
|
||||||
|
"LabelNarrator": "Anlatıcı",
|
||||||
|
"LabelNarrators": "Anlatıcılar",
|
||||||
|
"LabelNewestAuthors": "En Yeni Yazarlar",
|
||||||
|
"LabelNewestEpisodes": "En Yeni Bölümler",
|
||||||
|
"LabelNotFinished": "Tamamlanmadı",
|
||||||
|
"LabelNotStarted": "Başlanmadı",
|
||||||
|
"LabelNumberOfEpisodes": "Bölüm Sayısı",
|
||||||
|
"LabelPassword": "Şifre",
|
||||||
|
"LabelPath": "Yol",
|
||||||
|
"LabelPodcast": "Podcast",
|
||||||
|
"LabelPodcasts": "Podcastler",
|
||||||
|
"LabelPreventIndexing": "Akışınızın iTunes ve Google podcast dizinleri tarafından dizinlenmesini önleyin",
|
||||||
|
"LabelProgress": "İlerleme",
|
||||||
|
"LabelPubDate": "Yay. Tarihi",
|
||||||
|
"LabelPublishYear": "Yayım Yılı",
|
||||||
|
"LabelPublishedDate": "Yayımlandı {0}",
|
||||||
|
"LabelRSSFeedCustomOwnerEmail": "Özelleştirilmiş sahip Emaili",
|
||||||
|
"LabelRSSFeedCustomOwnerName": "Özelleştirilmis sahip İsmi",
|
||||||
|
"LabelRSSFeedPreventIndexing": "Dizinlemeyi Önle",
|
||||||
|
"LabelRandomly": "Rastgele",
|
||||||
|
"LabelRead": "Oku",
|
||||||
|
"LabelReadAgain": "Tekrar Oku",
|
||||||
|
"LabelRecentlyAdded": "Yakınlarda Eklenmiş",
|
||||||
|
"LabelSeason": "Sezon",
|
||||||
|
"LabelSetEbookAsPrimary": "Birincil olarak ayarla",
|
||||||
|
"LabelSetEbookAsSupplementary": "Yedek olarak ayarla",
|
||||||
|
"LabelShowAll": "Hepsini Göster",
|
||||||
|
"LabelSize": "Boyut",
|
||||||
|
"LabelSleepTimer": "Uyku Zamanlayıcısı",
|
||||||
|
"LabelStart": "Başla",
|
||||||
|
"LabelStatsBestDay": "En İyi Gün",
|
||||||
|
"LabelStatsDailyAverage": "Günlük Ortalama",
|
||||||
|
"LabelStatsDays": "Günler",
|
||||||
|
"LabelStatsDaysListened": "Dinlenen Günler",
|
||||||
|
"LabelStatsInARow": "art arda",
|
||||||
|
"LabelStatsItemsFinished": "Bitirilen Öğeler",
|
||||||
|
"LabelStatsMinutes": "dakika",
|
||||||
|
"LabelStatsMinutesListening": "Dinlenen Dakika",
|
||||||
|
"LabelTag": "Etiket",
|
||||||
|
"LabelTags": "Etiketler",
|
||||||
|
"LabelTheme": "Tema",
|
||||||
|
"LabelThemeDark": "Koyu",
|
||||||
|
"LabelThemeLight": "Açık",
|
||||||
|
"LabelTimeRemaining": "{0} kalan",
|
||||||
|
"LabelTitle": "Başlık",
|
||||||
|
"LabelTracks": "Parçalar",
|
||||||
|
"LabelType": "Tür",
|
||||||
|
"LabelUnknown": "Bilinmeyen",
|
||||||
|
"LabelUser": "Kullanıcı",
|
||||||
|
"LabelUsername": "Kullanıcı Adı",
|
||||||
|
"LabelYourBookmarks": "Yer İşaretleriniz"
|
||||||
|
}
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ if (isDev) {
|
|||||||
if (devEnv.AllowIframe) process.env.ALLOW_IFRAME = '1'
|
if (devEnv.AllowIframe) process.env.ALLOW_IFRAME = '1'
|
||||||
if (devEnv.BackupPath) process.env.BACKUP_PATH = devEnv.BackupPath
|
if (devEnv.BackupPath) process.env.BACKUP_PATH = devEnv.BackupPath
|
||||||
process.env.SOURCE = 'local'
|
process.env.SOURCE = 'local'
|
||||||
process.env.ROUTER_BASE_PATH = devEnv.RouterBasePath || ''
|
process.env.ROUTER_BASE_PATH = devEnv.RouterBasePath ?? '/audiobookshelf'
|
||||||
}
|
}
|
||||||
|
|
||||||
const inputConfig = options.config ? Path.resolve(options.config) : null
|
const inputConfig = options.config ? Path.resolve(options.config) : null
|
||||||
@@ -41,7 +41,7 @@ const CONFIG_PATH = inputConfig || process.env.CONFIG_PATH || Path.resolve('conf
|
|||||||
const METADATA_PATH = inputMetadata || process.env.METADATA_PATH || Path.resolve('metadata')
|
const METADATA_PATH = inputMetadata || process.env.METADATA_PATH || Path.resolve('metadata')
|
||||||
const SOURCE = options.source || process.env.SOURCE || 'debian'
|
const SOURCE = options.source || process.env.SOURCE || 'debian'
|
||||||
|
|
||||||
const ROUTER_BASE_PATH = process.env.ROUTER_BASE_PATH || '/audiobookshelf'
|
const ROUTER_BASE_PATH = process.env.ROUTER_BASE_PATH ?? '/audiobookshelf'
|
||||||
|
|
||||||
console.log(`Running in ${process.env.NODE_ENV} mode.`)
|
console.log(`Running in ${process.env.NODE_ENV} mode.`)
|
||||||
console.log(`Options: CONFIG_PATH=${CONFIG_PATH}, METADATA_PATH=${METADATA_PATH}, PORT=${PORT}, HOST=${HOST}, SOURCE=${SOURCE}, ROUTER_BASE_PATH=${ROUTER_BASE_PATH}`)
|
console.log(`Options: CONFIG_PATH=${CONFIG_PATH}, METADATA_PATH=${METADATA_PATH}, PORT=${PORT}, HOST=${HOST}, SOURCE=${SOURCE}, ROUTER_BASE_PATH=${ROUTER_BASE_PATH}`)
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookshelf",
|
"name": "audiobookshelf",
|
||||||
"version": "2.19.0",
|
"version": "2.19.4",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "audiobookshelf",
|
"name": "audiobookshelf",
|
||||||
"version": "2.19.0",
|
"version": "2.19.4",
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^0.27.2",
|
"axios": "^0.27.2",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookshelf",
|
"name": "audiobookshelf",
|
||||||
"version": "2.19.0",
|
"version": "2.19.4",
|
||||||
"buildNumber": 1,
|
"buildNumber": 1,
|
||||||
"description": "Self-hosted audiobook and podcast server",
|
"description": "Self-hosted audiobook and podcast server",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ const CONFIG_PATH = inputConfig || process.env.CONFIG_PATH || Path.resolve('conf
|
|||||||
const METADATA_PATH = inputMetadata || process.env.METADATA_PATH || Path.resolve('metadata')
|
const METADATA_PATH = inputMetadata || process.env.METADATA_PATH || Path.resolve('metadata')
|
||||||
const SOURCE = options.source || process.env.SOURCE || 'debian'
|
const SOURCE = options.source || process.env.SOURCE || 'debian'
|
||||||
|
|
||||||
const ROUTER_BASE_PATH = process.env.ROUTER_BASE_PATH || '/audiobookshelf'
|
const ROUTER_BASE_PATH = process.env.ROUTER_BASE_PATH ?? '/audiobookshelf'
|
||||||
|
|
||||||
console.log(process.env.NODE_ENV, 'Config', CONFIG_PATH, METADATA_PATH)
|
console.log(process.env.NODE_ENV, 'Config', CONFIG_PATH, METADATA_PATH)
|
||||||
|
|
||||||
|
|||||||
+7
-2
@@ -10,6 +10,7 @@ const ExtractJwt = require('passport-jwt').ExtractJwt
|
|||||||
const OpenIDClient = require('openid-client')
|
const OpenIDClient = require('openid-client')
|
||||||
const Database = require('./Database')
|
const Database = require('./Database')
|
||||||
const Logger = require('./Logger')
|
const Logger = require('./Logger')
|
||||||
|
const { escapeRegExp } = require('./utils')
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @class Class for handling all the authentication related functionality.
|
* @class Class for handling all the authentication related functionality.
|
||||||
@@ -18,7 +19,11 @@ class Auth {
|
|||||||
constructor() {
|
constructor() {
|
||||||
// Map of openId sessions indexed by oauth2 state-variable
|
// Map of openId sessions indexed by oauth2 state-variable
|
||||||
this.openIdAuthSession = new Map()
|
this.openIdAuthSession = new Map()
|
||||||
this.ignorePatterns = [/\/api\/items\/[^/]+\/cover/, /\/api\/authors\/[^/]+\/image/]
|
const escapedRouterBasePath = escapeRegExp(global.RouterBasePath)
|
||||||
|
this.ignorePatterns = [
|
||||||
|
new RegExp(`^(${escapedRouterBasePath}/api)?/items/[^/]+/cover$`),
|
||||||
|
new RegExp(`^(${escapedRouterBasePath}/api)?/authors/[^/]+/image$`)
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -28,7 +33,7 @@ class Auth {
|
|||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
authNotNeeded(req) {
|
authNotNeeded(req) {
|
||||||
return req.method === 'GET' && this.ignorePatterns.some((pattern) => pattern.test(req.originalUrl))
|
return req.method === 'GET' && this.ignorePatterns.some((pattern) => pattern.test(req.path))
|
||||||
}
|
}
|
||||||
|
|
||||||
ifAuthNeeded(middleware) {
|
ifAuthNeeded(middleware) {
|
||||||
|
|||||||
@@ -190,7 +190,13 @@ class Database {
|
|||||||
await this.buildModels(force)
|
await this.buildModels(force)
|
||||||
Logger.info(`[Database] Db initialized with models:`, Object.keys(this.sequelize.models).join(', '))
|
Logger.info(`[Database] Db initialized with models:`, Object.keys(this.sequelize.models).join(', '))
|
||||||
|
|
||||||
|
await this.addTriggers()
|
||||||
|
|
||||||
await this.loadData()
|
await this.loadData()
|
||||||
|
|
||||||
|
Logger.info(`[Database] running ANALYZE`)
|
||||||
|
await this.sequelize.query('ANALYZE')
|
||||||
|
Logger.info(`[Database] ANALYZE completed`)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -767,6 +773,43 @@ class Database {
|
|||||||
return textQuery
|
return textQuery
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is used to create necessary triggers for new databases.
|
||||||
|
* It adds triggers to update libraryItems.title[IgnorePrefix] when (books|podcasts).title[IgnorePrefix] is updated
|
||||||
|
*/
|
||||||
|
async addTriggers() {
|
||||||
|
await this.addTriggerIfNotExists('books', 'title', 'id', 'libraryItems', 'title', 'mediaId')
|
||||||
|
await this.addTriggerIfNotExists('books', 'titleIgnorePrefix', 'id', 'libraryItems', 'titleIgnorePrefix', 'mediaId')
|
||||||
|
await this.addTriggerIfNotExists('podcasts', 'title', 'id', 'libraryItems', 'title', 'mediaId')
|
||||||
|
await this.addTriggerIfNotExists('podcasts', 'titleIgnorePrefix', 'id', 'libraryItems', 'titleIgnorePrefix', 'mediaId')
|
||||||
|
}
|
||||||
|
|
||||||
|
async addTriggerIfNotExists(sourceTable, sourceColumn, sourceIdColumn, targetTable, targetColumn, targetIdColumn) {
|
||||||
|
const action = `update_${targetTable}_${targetColumn}`
|
||||||
|
const fromSource = sourceTable === 'books' ? '' : `_from_${sourceTable}_${sourceColumn}`
|
||||||
|
const triggerName = this.convertToSnakeCase(`${action}${fromSource}`)
|
||||||
|
|
||||||
|
const [[{ count }]] = await this.sequelize.query(`SELECT COUNT(*) as count FROM sqlite_master WHERE type='trigger' AND name='${triggerName}'`)
|
||||||
|
if (count > 0) return // Trigger already exists
|
||||||
|
|
||||||
|
Logger.info(`[Database] Adding trigger ${triggerName}`)
|
||||||
|
|
||||||
|
await this.sequelize.query(`
|
||||||
|
CREATE TRIGGER ${triggerName}
|
||||||
|
AFTER UPDATE OF ${sourceColumn} ON ${sourceTable}
|
||||||
|
FOR EACH ROW
|
||||||
|
BEGIN
|
||||||
|
UPDATE ${targetTable}
|
||||||
|
SET ${targetColumn} = NEW.${sourceColumn}
|
||||||
|
WHERE ${targetTable}.${targetIdColumn} = NEW.${sourceIdColumn};
|
||||||
|
END;
|
||||||
|
`)
|
||||||
|
}
|
||||||
|
|
||||||
|
convertToSnakeCase(str) {
|
||||||
|
return str.replace(/([A-Z])/g, '_$1').toLowerCase()
|
||||||
|
}
|
||||||
|
|
||||||
TextSearchQuery = class {
|
TextSearchQuery = class {
|
||||||
constructor(sequelize, supportsUnaccent, query) {
|
constructor(sequelize, supportsUnaccent, query) {
|
||||||
this.sequelize = sequelize
|
this.sequelize = sequelize
|
||||||
|
|||||||
@@ -107,7 +107,9 @@ class PodcastController {
|
|||||||
libraryFiles: [],
|
libraryFiles: [],
|
||||||
extraData: {},
|
extraData: {},
|
||||||
libraryId: library.id,
|
libraryId: library.id,
|
||||||
libraryFolderId: folder.id
|
libraryFolderId: folder.id,
|
||||||
|
title: podcast.title,
|
||||||
|
titleIgnorePrefix: podcast.titleIgnorePrefix
|
||||||
},
|
},
|
||||||
{ transaction }
|
{ transaction }
|
||||||
)
|
)
|
||||||
@@ -498,6 +500,10 @@ class PodcastController {
|
|||||||
req.libraryItem.changed('libraryFiles', true)
|
req.libraryItem.changed('libraryFiles', true)
|
||||||
await req.libraryItem.save()
|
await req.libraryItem.save()
|
||||||
|
|
||||||
|
// update number of episodes
|
||||||
|
req.libraryItem.media.numEpisodes = req.libraryItem.media.podcastEpisodes.length
|
||||||
|
await req.libraryItem.media.save()
|
||||||
|
|
||||||
SocketAuthority.emitter('item_updated', req.libraryItem.toOldJSONExpanded())
|
SocketAuthority.emitter('item_updated', req.libraryItem.toOldJSONExpanded())
|
||||||
res.json(req.libraryItem.toOldJSON())
|
res.json(req.libraryItem.toOldJSON())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -232,6 +232,11 @@ class PodcastManager {
|
|||||||
|
|
||||||
await libraryItem.save()
|
await libraryItem.save()
|
||||||
|
|
||||||
|
if (libraryItem.media.numEpisodes !== libraryItem.media.podcastEpisodes.length) {
|
||||||
|
libraryItem.media.numEpisodes = libraryItem.media.podcastEpisodes.length
|
||||||
|
await libraryItem.media.save()
|
||||||
|
}
|
||||||
|
|
||||||
SocketAuthority.emitter('item_updated', libraryItem.toOldJSONExpanded())
|
SocketAuthority.emitter('item_updated', libraryItem.toOldJSONExpanded())
|
||||||
const podcastEpisodeExpanded = podcastEpisode.toOldJSONExpanded(libraryItem.id)
|
const podcastEpisodeExpanded = podcastEpisode.toOldJSONExpanded(libraryItem.id)
|
||||||
podcastEpisodeExpanded.libraryItem = libraryItem.toOldJSONExpanded()
|
podcastEpisodeExpanded.libraryItem = libraryItem.toOldJSONExpanded()
|
||||||
@@ -622,7 +627,9 @@ class PodcastManager {
|
|||||||
libraryFiles: [],
|
libraryFiles: [],
|
||||||
extraData: {},
|
extraData: {},
|
||||||
libraryId: folder.libraryId,
|
libraryId: folder.libraryId,
|
||||||
libraryFolderId: folder.id
|
libraryFolderId: folder.id,
|
||||||
|
title: podcast.title,
|
||||||
|
titleIgnorePrefix: podcast.titleIgnorePrefix
|
||||||
},
|
},
|
||||||
{ transaction }
|
{ transaction }
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -246,6 +246,15 @@ class RssFeedManager {
|
|||||||
const extname = Path.extname(feed.coverPath).toLowerCase().slice(1)
|
const extname = Path.extname(feed.coverPath).toLowerCase().slice(1)
|
||||||
res.type(`image/${extname}`)
|
res.type(`image/${extname}`)
|
||||||
const readStream = fs.createReadStream(feed.coverPath)
|
const readStream = fs.createReadStream(feed.coverPath)
|
||||||
|
|
||||||
|
readStream.on('error', (error) => {
|
||||||
|
Logger.error(`[RssFeedManager] Error streaming cover image: ${error.message}`)
|
||||||
|
// Only send error if headers haven't been sent yet
|
||||||
|
if (!res.headersSent) {
|
||||||
|
res.sendStatus(404)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
readStream.pipe(res)
|
readStream.pipe(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,3 +13,5 @@ Please add a record of every database migration that you create to this file. Th
|
|||||||
| v2.17.5 | v2.17.5-remove-host-from-feed-urls | removes the host (serverAddress) from URL columns in the feeds and feedEpisodes tables |
|
| v2.17.5 | v2.17.5-remove-host-from-feed-urls | removes the host (serverAddress) from URL columns in the feeds and feedEpisodes tables |
|
||||||
| v2.17.6 | v2.17.6-share-add-isdownloadable | Adds the isDownloadable column to the mediaItemShares table |
|
| v2.17.6 | v2.17.6-share-add-isdownloadable | Adds the isDownloadable column to the mediaItemShares table |
|
||||||
| v2.17.7 | v2.17.7-add-indices | Adds indices to the libraryItems and books tables to reduce query times |
|
| v2.17.7 | v2.17.7-add-indices | Adds indices to the libraryItems and books tables to reduce query times |
|
||||||
|
| v2.19.1 | v2.19.1-copy-title-to-library-items | Copies title and titleIgnorePrefix to the libraryItems table, creates update triggers and indices |
|
||||||
|
| v2.19.4 | v2.19.4-improve-podcast-queries | Adds numEpisodes to podcasts, adds podcastId to mediaProgresses, copies podcast title to libraryItems |
|
||||||
|
|||||||
@@ -0,0 +1,164 @@
|
|||||||
|
const util = require('util')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef MigrationContext
|
||||||
|
* @property {import('sequelize').QueryInterface} queryInterface - a suquelize QueryInterface object.
|
||||||
|
* @property {import('../Logger')} logger - a Logger object.
|
||||||
|
*
|
||||||
|
* @typedef MigrationOptions
|
||||||
|
* @property {MigrationContext} context - an object containing the migration context.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const migrationVersion = '2.19.1'
|
||||||
|
const migrationName = `${migrationVersion}-copy-title-to-library-items`
|
||||||
|
const loggerPrefix = `[${migrationVersion} migration]`
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This upward migration adds a title column to the libraryItems table, copies the title from the book to the libraryItem,
|
||||||
|
* and creates a new index on the title column. In addition it sets a trigger on the books table to update the title column
|
||||||
|
* in the libraryItems table when a book is updated.
|
||||||
|
*
|
||||||
|
* @param {MigrationOptions} options - an object containing the migration context.
|
||||||
|
* @returns {Promise<void>} - A promise that resolves when the migration is complete.
|
||||||
|
*/
|
||||||
|
async function up({ context: { queryInterface, logger } }) {
|
||||||
|
// Upwards migration script
|
||||||
|
logger.info(`${loggerPrefix} UPGRADE BEGIN: ${migrationName}`)
|
||||||
|
|
||||||
|
await addColumn(queryInterface, logger, 'libraryItems', 'title', { type: queryInterface.sequelize.Sequelize.STRING, allowNull: true })
|
||||||
|
await copyColumn(queryInterface, logger, 'books', 'title', 'id', 'libraryItems', 'title', 'mediaId')
|
||||||
|
await addTrigger(queryInterface, logger, 'books', 'title', 'id', 'libraryItems', 'title', 'mediaId')
|
||||||
|
await addIndex(queryInterface, logger, 'libraryItems', ['libraryId', 'mediaType', { name: 'title', collate: 'NOCASE' }])
|
||||||
|
|
||||||
|
await addColumn(queryInterface, logger, 'libraryItems', 'titleIgnorePrefix', { type: queryInterface.sequelize.Sequelize.STRING, allowNull: true })
|
||||||
|
await copyColumn(queryInterface, logger, 'books', 'titleIgnorePrefix', 'id', 'libraryItems', 'titleIgnorePrefix', 'mediaId')
|
||||||
|
await addTrigger(queryInterface, logger, 'books', 'titleIgnorePrefix', 'id', 'libraryItems', 'titleIgnorePrefix', 'mediaId')
|
||||||
|
await addIndex(queryInterface, logger, 'libraryItems', ['libraryId', 'mediaType', { name: 'titleIgnorePrefix', collate: 'NOCASE' }])
|
||||||
|
|
||||||
|
await addIndex(queryInterface, logger, 'libraryItems', ['libraryId', 'mediaType', 'createdAt'])
|
||||||
|
|
||||||
|
logger.info(`${loggerPrefix} UPGRADE END: ${migrationName}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This downward migration script removes the title column from the libraryItems table, removes the trigger on the books table,
|
||||||
|
* and removes the index on the title column.
|
||||||
|
*
|
||||||
|
* @param {MigrationOptions} options - an object containing the migration context.
|
||||||
|
* @returns {Promise<void>} - A promise that resolves when the migration is complete.
|
||||||
|
*/
|
||||||
|
async function down({ context: { queryInterface, logger } }) {
|
||||||
|
// Downward migration script
|
||||||
|
logger.info(`${loggerPrefix} DOWNGRADE BEGIN: ${migrationName}`)
|
||||||
|
|
||||||
|
await removeIndex(queryInterface, logger, 'libraryItems', ['libraryId', 'mediaType', 'title'])
|
||||||
|
await removeTrigger(queryInterface, logger, 'libraryItems', 'title')
|
||||||
|
await removeColumn(queryInterface, logger, 'libraryItems', 'title')
|
||||||
|
|
||||||
|
await removeIndex(queryInterface, logger, 'libraryItems', ['libraryId', 'mediaType', 'titleIgnorePrefix'])
|
||||||
|
await removeTrigger(queryInterface, logger, 'libraryItems', 'titleIgnorePrefix')
|
||||||
|
await removeColumn(queryInterface, logger, 'libraryItems', 'titleIgnorePrefix')
|
||||||
|
|
||||||
|
await removeIndex(queryInterface, logger, 'libraryItems', ['libraryId', 'mediaType', 'createdAt'])
|
||||||
|
|
||||||
|
logger.info(`${loggerPrefix} DOWNGRADE END: ${migrationName}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility function to add an index to a table. If the index already z`exists, it logs a message and continues.
|
||||||
|
*
|
||||||
|
* @param {import('sequelize').QueryInterface} queryInterface
|
||||||
|
* @param {import ('../Logger')} logger
|
||||||
|
* @param {string} tableName
|
||||||
|
* @param {string[]} columns
|
||||||
|
*/
|
||||||
|
async function addIndex(queryInterface, logger, tableName, columns) {
|
||||||
|
const columnString = columns.map((column) => util.inspect(column)).join(', ')
|
||||||
|
const indexName = convertToSnakeCase(`${tableName}_${columns.map((column) => (typeof column === 'string' ? column : column.name)).join('_')}`)
|
||||||
|
try {
|
||||||
|
logger.info(`${loggerPrefix} adding index on [${columnString}] to table ${tableName}. index name: ${indexName}"`)
|
||||||
|
await queryInterface.addIndex(tableName, columns)
|
||||||
|
logger.info(`${loggerPrefix} added index on [${columnString}] to table ${tableName}. index name: ${indexName}"`)
|
||||||
|
} catch (error) {
|
||||||
|
if (error.name === 'SequelizeDatabaseError' && error.message.includes('already exists')) {
|
||||||
|
logger.info(`${loggerPrefix} index [${columnString}] for table "${tableName}" already exists`)
|
||||||
|
} else {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility function to remove an index from a table.
|
||||||
|
* Sequelize implemets it using DROP INDEX IF EXISTS, so it won't throw an error if the index doesn't exist.
|
||||||
|
*
|
||||||
|
* @param {import('sequelize').QueryInterface} queryInterface
|
||||||
|
* @param {import ('../Logger')} logger
|
||||||
|
* @param {string} tableName
|
||||||
|
* @param {string[]} columns
|
||||||
|
*/
|
||||||
|
async function removeIndex(queryInterface, logger, tableName, columns) {
|
||||||
|
logger.info(`${loggerPrefix} removing index [${columns.join(', ')}] from table "${tableName}"`)
|
||||||
|
await queryInterface.removeIndex(tableName, columns)
|
||||||
|
logger.info(`${loggerPrefix} removed index [${columns.join(', ')}] from table "${tableName}"`)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function addColumn(queryInterface, logger, table, column, options) {
|
||||||
|
logger.info(`${loggerPrefix} adding column "${column}" to table "${table}"`)
|
||||||
|
const tableDescription = await queryInterface.describeTable(table)
|
||||||
|
if (!tableDescription[column]) {
|
||||||
|
await queryInterface.addColumn(table, column, options)
|
||||||
|
logger.info(`${loggerPrefix} added column "${column}" to table "${table}"`)
|
||||||
|
} else {
|
||||||
|
logger.info(`${loggerPrefix} column "${column}" already exists in table "${table}"`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function removeColumn(queryInterface, logger, table, column) {
|
||||||
|
logger.info(`${loggerPrefix} removing column "${column}" from table "${table}"`)
|
||||||
|
await queryInterface.removeColumn(table, column)
|
||||||
|
logger.info(`${loggerPrefix} removed column "${column}" from table "${table}"`)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function copyColumn(queryInterface, logger, sourceTable, sourceColumn, sourceIdColumn, targetTable, targetColumn, targetIdColumn) {
|
||||||
|
logger.info(`${loggerPrefix} copying column "${sourceColumn}" from table "${sourceTable}" to table "${targetTable}"`)
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
UPDATE ${targetTable}
|
||||||
|
SET ${targetColumn} = ${sourceTable}.${sourceColumn}
|
||||||
|
FROM ${sourceTable}
|
||||||
|
WHERE ${targetTable}.${targetIdColumn} = ${sourceTable}.${sourceIdColumn}
|
||||||
|
`)
|
||||||
|
logger.info(`${loggerPrefix} copied column "${sourceColumn}" from table "${sourceTable}" to table "${targetTable}"`)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function addTrigger(queryInterface, logger, sourceTable, sourceColumn, sourceIdColumn, targetTable, targetColumn, targetIdColumn) {
|
||||||
|
logger.info(`${loggerPrefix} adding trigger to update ${targetTable}.${targetColumn} when ${sourceTable}.${sourceColumn} is updated`)
|
||||||
|
const triggerName = convertToSnakeCase(`update_${targetTable}_${targetColumn}`)
|
||||||
|
|
||||||
|
await queryInterface.sequelize.query(`DROP TRIGGER IF EXISTS ${triggerName}`)
|
||||||
|
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
CREATE TRIGGER ${triggerName}
|
||||||
|
AFTER UPDATE OF ${sourceColumn} ON ${sourceTable}
|
||||||
|
FOR EACH ROW
|
||||||
|
BEGIN
|
||||||
|
UPDATE ${targetTable}
|
||||||
|
SET ${targetColumn} = NEW.${sourceColumn}
|
||||||
|
WHERE ${targetTable}.${targetIdColumn} = NEW.${sourceIdColumn};
|
||||||
|
END;
|
||||||
|
`)
|
||||||
|
logger.info(`${loggerPrefix} added trigger to update ${targetTable}.${targetColumn} when ${sourceTable}.${sourceColumn} is updated`)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function removeTrigger(queryInterface, logger, targetTable, targetColumn) {
|
||||||
|
logger.info(`${loggerPrefix} removing trigger to update ${targetTable}.${targetColumn}`)
|
||||||
|
const triggerName = convertToSnakeCase(`update_${targetTable}_${targetColumn}`)
|
||||||
|
await queryInterface.sequelize.query(`DROP TRIGGER IF EXISTS ${triggerName}`)
|
||||||
|
logger.info(`${loggerPrefix} removed trigger to update ${targetTable}.${targetColumn}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
function convertToSnakeCase(str) {
|
||||||
|
return str.replace(/([A-Z])/g, '_$1').toLowerCase()
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { up, down }
|
||||||
@@ -0,0 +1,219 @@
|
|||||||
|
const util = require('util')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef MigrationContext
|
||||||
|
* @property {import('sequelize').QueryInterface} queryInterface - a suquelize QueryInterface object.
|
||||||
|
* @property {import('../Logger')} logger - a Logger object.
|
||||||
|
*
|
||||||
|
* @typedef MigrationOptions
|
||||||
|
* @property {MigrationContext} context - an object containing the migration context.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const migrationVersion = '2.19.4'
|
||||||
|
const migrationName = `${migrationVersion}-improve-podcast-queries`
|
||||||
|
const loggerPrefix = `[${migrationVersion} migration]`
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This upward migration adds a numEpisodes column to the podcasts table and populates it.
|
||||||
|
* It also adds a podcastId column to the mediaProgresses table and populates it.
|
||||||
|
* It also copies the title and titleIgnorePrefix columns from the podcasts table to the libraryItems table,
|
||||||
|
* and adds triggers to update them when the corresponding columns in the podcasts table are updated.
|
||||||
|
*
|
||||||
|
* @param {MigrationOptions} options - an object containing the migration context.
|
||||||
|
* @returns {Promise<void>} - A promise that resolves when the migration is complete.
|
||||||
|
*/
|
||||||
|
async function up({ context: { queryInterface, logger } }) {
|
||||||
|
// Upwards migration script
|
||||||
|
logger.info(`${loggerPrefix} UPGRADE BEGIN: ${migrationName}`)
|
||||||
|
|
||||||
|
// Add numEpisodes column to podcasts table
|
||||||
|
await addColumn(queryInterface, logger, 'podcasts', 'numEpisodes', { type: queryInterface.sequelize.Sequelize.INTEGER, allowNull: false, defaultValue: 0 })
|
||||||
|
|
||||||
|
// Populate numEpisodes column with the number of episodes for each podcast
|
||||||
|
await populateNumEpisodes(queryInterface, logger)
|
||||||
|
|
||||||
|
// Add podcastId column to mediaProgresses table
|
||||||
|
await addColumn(queryInterface, logger, 'mediaProgresses', 'podcastId', { type: queryInterface.sequelize.Sequelize.UUID, allowNull: true })
|
||||||
|
|
||||||
|
// Populate podcastId column with the podcastId for each mediaProgress
|
||||||
|
await populatePodcastId(queryInterface, logger)
|
||||||
|
|
||||||
|
// Copy title and titleIgnorePrefix columns from podcasts to libraryItems
|
||||||
|
await copyColumn(queryInterface, logger, 'podcasts', 'title', 'id', 'libraryItems', 'title', 'mediaId')
|
||||||
|
await copyColumn(queryInterface, logger, 'podcasts', 'titleIgnorePrefix', 'id', 'libraryItems', 'titleIgnorePrefix', 'mediaId')
|
||||||
|
|
||||||
|
// Add triggers to update title and titleIgnorePrefix in libraryItems
|
||||||
|
await addTrigger(queryInterface, logger, 'podcasts', 'title', 'id', 'libraryItems', 'title', 'mediaId')
|
||||||
|
await addTrigger(queryInterface, logger, 'podcasts', 'titleIgnorePrefix', 'id', 'libraryItems', 'titleIgnorePrefix', 'mediaId')
|
||||||
|
|
||||||
|
logger.info(`${loggerPrefix} UPGRADE END: ${migrationName}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This downward migration removes the triggers on the podcasts table,
|
||||||
|
* the numEpisodes column from the podcasts table, and the podcastId column from the mediaProgresses table.
|
||||||
|
*
|
||||||
|
* @param {MigrationOptions} options - an object containing the migration context.
|
||||||
|
* @returns {Promise<void>} - A promise that resolves when the migration is complete.
|
||||||
|
*/
|
||||||
|
async function down({ context: { queryInterface, logger } }) {
|
||||||
|
// Downward migration script
|
||||||
|
logger.info(`${loggerPrefix} DOWNGRADE BEGIN: ${migrationName}`)
|
||||||
|
|
||||||
|
// Remove triggers from libraryItems
|
||||||
|
await removeTrigger(queryInterface, logger, 'podcasts', 'title', 'libraryItems', 'title')
|
||||||
|
await removeTrigger(queryInterface, logger, 'podcasts', 'titleIgnorePrefix', 'libraryItems', 'titleIgnorePrefix')
|
||||||
|
|
||||||
|
// Remove numEpisodes column from podcasts table
|
||||||
|
await removeColumn(queryInterface, logger, 'podcasts', 'numEpisodes')
|
||||||
|
|
||||||
|
// Remove podcastId column from mediaProgresses table
|
||||||
|
await removeColumn(queryInterface, logger, 'mediaProgresses', 'podcastId')
|
||||||
|
|
||||||
|
logger.info(`${loggerPrefix} DOWNGRADE END: ${migrationName}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function populateNumEpisodes(queryInterface, logger) {
|
||||||
|
logger.info(`${loggerPrefix} populating numEpisodes column in podcasts table`)
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
UPDATE podcasts
|
||||||
|
SET numEpisodes = (SELECT COUNT(*) FROM podcastEpisodes WHERE podcastEpisodes.podcastId = podcasts.id)
|
||||||
|
`)
|
||||||
|
logger.info(`${loggerPrefix} populated numEpisodes column in podcasts table`)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function populatePodcastId(queryInterface, logger) {
|
||||||
|
logger.info(`${loggerPrefix} populating podcastId column in mediaProgresses table`)
|
||||||
|
// bulk update podcastId to the podcastId of the podcastEpisode if the mediaItemType is podcastEpisode
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
UPDATE mediaProgresses
|
||||||
|
SET podcastId = (SELECT podcastId FROM podcastEpisodes WHERE podcastEpisodes.id = mediaProgresses.mediaItemId)
|
||||||
|
WHERE mediaItemType = 'podcastEpisode'
|
||||||
|
`)
|
||||||
|
logger.info(`${loggerPrefix} populated podcastId column in mediaProgresses table`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility function to add a column to a table. If the column already exists, it logs a message and continues.
|
||||||
|
*
|
||||||
|
* @param {import('sequelize').QueryInterface} queryInterface - a suquelize QueryInterface object.
|
||||||
|
* @param {import('../Logger')} logger - a Logger object.
|
||||||
|
* @param {string} table - the name of the table to add the column to.
|
||||||
|
* @param {string} column - the name of the column to add.
|
||||||
|
* @param {Object} options - the options for the column.
|
||||||
|
*/
|
||||||
|
async function addColumn(queryInterface, logger, table, column, options) {
|
||||||
|
logger.info(`${loggerPrefix} adding column "${column}" to table "${table}"`)
|
||||||
|
const tableDescription = await queryInterface.describeTable(table)
|
||||||
|
if (!tableDescription[column]) {
|
||||||
|
await queryInterface.addColumn(table, column, options)
|
||||||
|
logger.info(`${loggerPrefix} added column "${column}" to table "${table}"`)
|
||||||
|
} else {
|
||||||
|
logger.info(`${loggerPrefix} column "${column}" already exists in table "${table}"`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility function to remove a column from a table. If the column does not exist, it logs a message and continues.
|
||||||
|
*
|
||||||
|
* @param {import('sequelize').QueryInterface} queryInterface - a suquelize QueryInterface object.
|
||||||
|
* @param {import('../Logger')} logger - a Logger object.
|
||||||
|
* @param {string} table - the name of the table to remove the column from.
|
||||||
|
* @param {string} column - the name of the column to remove.
|
||||||
|
*/
|
||||||
|
async function removeColumn(queryInterface, logger, table, column) {
|
||||||
|
logger.info(`${loggerPrefix} removing column "${column}" from table "${table}"`)
|
||||||
|
const tableDescription = await queryInterface.describeTable(table)
|
||||||
|
if (tableDescription[column]) {
|
||||||
|
await queryInterface.sequelize.query(`ALTER TABLE ${table} DROP COLUMN ${column}`)
|
||||||
|
logger.info(`${loggerPrefix} removed column "${column}" from table "${table}"`)
|
||||||
|
} else {
|
||||||
|
logger.info(`${loggerPrefix} column "${column}" does not exist in table "${table}"`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility function to add a trigger to update a column in a target table when a column in a source table is updated.
|
||||||
|
* If the trigger already exists, it drops it and creates a new one.
|
||||||
|
* sourceIdColumn and targetIdColumn are used to match the source and target rows.
|
||||||
|
*
|
||||||
|
* @param {import('sequelize').QueryInterface} queryInterface - a suquelize QueryInterface object.
|
||||||
|
* @param {import('../Logger')} logger - a Logger object.
|
||||||
|
* @param {string} sourceTable - the name of the source table.
|
||||||
|
* @param {string} sourceColumn - the name of the column to update.
|
||||||
|
* @param {string} sourceIdColumn - the name of the id column of the source table.
|
||||||
|
* @param {string} targetTable - the name of the target table.
|
||||||
|
* @param {string} targetColumn - the name of the column to update.
|
||||||
|
* @param {string} targetIdColumn - the name of the id column of the target table.
|
||||||
|
*/
|
||||||
|
async function addTrigger(queryInterface, logger, sourceTable, sourceColumn, sourceIdColumn, targetTable, targetColumn, targetIdColumn) {
|
||||||
|
logger.info(`${loggerPrefix} adding trigger to update ${targetTable}.${targetColumn} when ${sourceTable}.${sourceColumn} is updated`)
|
||||||
|
const triggerName = convertToSnakeCase(`update_${targetTable}_${targetColumn}_from_${sourceTable}_${sourceColumn}`)
|
||||||
|
|
||||||
|
await queryInterface.sequelize.query(`DROP TRIGGER IF EXISTS ${triggerName}`)
|
||||||
|
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
CREATE TRIGGER ${triggerName}
|
||||||
|
AFTER UPDATE OF ${sourceColumn} ON ${sourceTable}
|
||||||
|
FOR EACH ROW
|
||||||
|
BEGIN
|
||||||
|
UPDATE ${targetTable}
|
||||||
|
SET ${targetColumn} = NEW.${sourceColumn}
|
||||||
|
WHERE ${targetTable}.${targetIdColumn} = NEW.${sourceIdColumn};
|
||||||
|
END;
|
||||||
|
`)
|
||||||
|
logger.info(`${loggerPrefix} added trigger to update ${targetTable}.${targetColumn} when ${sourceTable}.${sourceColumn} is updated`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility function to remove an update trigger from a table.
|
||||||
|
*
|
||||||
|
* @param {import('sequelize').QueryInterface} queryInterface - a suquelize QueryInterface object.
|
||||||
|
* @param {import('../Logger')} logger - a Logger object.
|
||||||
|
* @param {string} sourceTable - the name of the source table.
|
||||||
|
* @param {string} sourceColumn - the name of the column to update.
|
||||||
|
* @param {string} targetTable - the name of the target table.
|
||||||
|
* @param {string} targetColumn - the name of the column to update.
|
||||||
|
*/
|
||||||
|
async function removeTrigger(queryInterface, logger, sourceTable, sourceColumn, targetTable, targetColumn) {
|
||||||
|
logger.info(`${loggerPrefix} removing trigger to update ${targetTable}.${targetColumn}`)
|
||||||
|
const triggerName = convertToSnakeCase(`update_${targetTable}_${targetColumn}_from_${sourceTable}_${sourceColumn}`)
|
||||||
|
await queryInterface.sequelize.query(`DROP TRIGGER IF EXISTS ${triggerName}`)
|
||||||
|
logger.info(`${loggerPrefix} removed trigger to update ${targetTable}.${targetColumn}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility function to copy a column from a source table to a target table.
|
||||||
|
* sourceIdColumn and targetIdColumn are used to match the source and target rows.
|
||||||
|
*
|
||||||
|
* @param {import('sequelize').QueryInterface} queryInterface - a suquelize QueryInterface object.
|
||||||
|
* @param {import('../Logger')} logger - a Logger object.
|
||||||
|
* @param {string} sourceTable - the name of the source table.
|
||||||
|
* @param {string} sourceColumn - the name of the column to copy.
|
||||||
|
* @param {string} sourceIdColumn - the name of the id column of the source table.
|
||||||
|
* @param {string} targetTable - the name of the target table.
|
||||||
|
* @param {string} targetColumn - the name of the column to copy to.
|
||||||
|
* @param {string} targetIdColumn - the name of the id column of the target table.
|
||||||
|
*/
|
||||||
|
async function copyColumn(queryInterface, logger, sourceTable, sourceColumn, sourceIdColumn, targetTable, targetColumn, targetIdColumn) {
|
||||||
|
logger.info(`${loggerPrefix} copying column "${sourceColumn}" from table "${sourceTable}" to table "${targetTable}"`)
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
UPDATE ${targetTable}
|
||||||
|
SET ${targetColumn} = ${sourceTable}.${sourceColumn}
|
||||||
|
FROM ${sourceTable}
|
||||||
|
WHERE ${targetTable}.${targetIdColumn} = ${sourceTable}.${sourceIdColumn}
|
||||||
|
`)
|
||||||
|
logger.info(`${loggerPrefix} copied column "${sourceColumn}" from table "${sourceTable}" to table "${targetTable}"`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility function to convert a string to snake case, e.g. "titleIgnorePrefix" -> "title_ignore_prefix"
|
||||||
|
*
|
||||||
|
* @param {string} str - the string to convert to snake case.
|
||||||
|
* @returns {string} - the string in snake case.
|
||||||
|
*/
|
||||||
|
function convertToSnakeCase(str) {
|
||||||
|
return str.replace(/([A-Z])/g, '_$1').toLowerCase()
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { up, down }
|
||||||
@@ -3,6 +3,7 @@ const Logger = require('../Logger')
|
|||||||
const { getTitlePrefixAtEnd, getTitleIgnorePrefix } = require('../utils')
|
const { getTitlePrefixAtEnd, getTitleIgnorePrefix } = require('../utils')
|
||||||
const parseNameString = require('../utils/parsers/parseNameString')
|
const parseNameString = require('../utils/parsers/parseNameString')
|
||||||
const htmlSanitizer = require('../utils/htmlSanitizer')
|
const htmlSanitizer = require('../utils/htmlSanitizer')
|
||||||
|
const libraryItemsBookFilters = require('../utils/queries/libraryItemsBookFilters')
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef EBookFileObject
|
* @typedef EBookFileObject
|
||||||
@@ -192,6 +193,14 @@ class Book extends Model {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
Book.addHook('afterDestroy', async (instance) => {
|
||||||
|
libraryItemsBookFilters.clearCountCache('afterDestroy')
|
||||||
|
})
|
||||||
|
|
||||||
|
Book.addHook('afterCreate', async (instance) => {
|
||||||
|
libraryItemsBookFilters.clearCountCache('afterCreate')
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -73,6 +73,10 @@ class LibraryItem extends Model {
|
|||||||
|
|
||||||
/** @type {Book.BookExpanded|Podcast.PodcastExpanded} - only set when expanded */
|
/** @type {Book.BookExpanded|Podcast.PodcastExpanded} - only set when expanded */
|
||||||
this.media
|
this.media
|
||||||
|
/** @type {string} */
|
||||||
|
this.title // Only used for sorting
|
||||||
|
/** @type {string} */
|
||||||
|
this.titleIgnorePrefix // Only used for sorting
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -99,7 +103,7 @@ class LibraryItem extends Model {
|
|||||||
{
|
{
|
||||||
model: this.sequelize.models.series,
|
model: this.sequelize.models.series,
|
||||||
through: {
|
through: {
|
||||||
attributes: ['sequence', 'createdAt']
|
attributes: ['id', 'sequence', 'createdAt']
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -677,7 +681,9 @@ class LibraryItem extends Model {
|
|||||||
lastScan: DataTypes.DATE,
|
lastScan: DataTypes.DATE,
|
||||||
lastScanVersion: DataTypes.STRING,
|
lastScanVersion: DataTypes.STRING,
|
||||||
libraryFiles: DataTypes.JSON,
|
libraryFiles: DataTypes.JSON,
|
||||||
extraData: DataTypes.JSON
|
extraData: DataTypes.JSON,
|
||||||
|
title: DataTypes.STRING,
|
||||||
|
titleIgnorePrefix: DataTypes.STRING
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
sequelize,
|
sequelize,
|
||||||
@@ -695,6 +701,15 @@ class LibraryItem extends Model {
|
|||||||
{
|
{
|
||||||
fields: ['libraryId', 'mediaType', 'size']
|
fields: ['libraryId', 'mediaType', 'size']
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
fields: ['libraryId', 'mediaType', 'createdAt']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fields: ['libraryId', 'mediaType', { name: 'title', collate: 'NOCASE' }]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fields: ['libraryId', 'mediaType', { name: 'titleIgnorePrefix', collate: 'NOCASE' }]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
fields: ['libraryId', 'mediaId', 'mediaType']
|
fields: ['libraryId', 'mediaId', 'mediaType']
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -34,6 +34,8 @@ class MediaProgress extends Model {
|
|||||||
this.updatedAt
|
this.updatedAt
|
||||||
/** @type {Date} */
|
/** @type {Date} */
|
||||||
this.createdAt
|
this.createdAt
|
||||||
|
/** @type {UUIDV4} */
|
||||||
|
this.podcastId
|
||||||
}
|
}
|
||||||
|
|
||||||
static removeById(mediaProgressId) {
|
static removeById(mediaProgressId) {
|
||||||
@@ -69,7 +71,8 @@ class MediaProgress extends Model {
|
|||||||
ebookLocation: DataTypes.STRING,
|
ebookLocation: DataTypes.STRING,
|
||||||
ebookProgress: DataTypes.FLOAT,
|
ebookProgress: DataTypes.FLOAT,
|
||||||
finishedAt: DataTypes.DATE,
|
finishedAt: DataTypes.DATE,
|
||||||
extraData: DataTypes.JSON
|
extraData: DataTypes.JSON,
|
||||||
|
podcastId: DataTypes.UUID
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
sequelize,
|
sequelize,
|
||||||
@@ -123,6 +126,16 @@ class MediaProgress extends Model {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// make sure to call the afterDestroy hook for each instance
|
||||||
|
MediaProgress.addHook('beforeBulkDestroy', (options) => {
|
||||||
|
options.individualHooks = true
|
||||||
|
})
|
||||||
|
|
||||||
|
// update the potentially cached user after destroying the media progress
|
||||||
|
MediaProgress.addHook('afterDestroy', (instance) => {
|
||||||
|
user.mediaProgressRemoved(instance)
|
||||||
|
})
|
||||||
|
|
||||||
user.hasMany(MediaProgress, {
|
user.hasMany(MediaProgress, {
|
||||||
onDelete: 'CASCADE'
|
onDelete: 'CASCADE'
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
const { DataTypes, Model } = require('sequelize')
|
const { DataTypes, Model } = require('sequelize')
|
||||||
const { getTitlePrefixAtEnd, getTitleIgnorePrefix } = require('../utils')
|
const { getTitlePrefixAtEnd, getTitleIgnorePrefix } = require('../utils')
|
||||||
const Logger = require('../Logger')
|
const Logger = require('../Logger')
|
||||||
|
const libraryItemsPodcastFilters = require('../utils/queries/libraryItemsPodcastFilters')
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef PodcastExpandedProperties
|
* @typedef PodcastExpandedProperties
|
||||||
@@ -61,6 +62,8 @@ class Podcast extends Model {
|
|||||||
this.createdAt
|
this.createdAt
|
||||||
/** @type {Date} */
|
/** @type {Date} */
|
||||||
this.updatedAt
|
this.updatedAt
|
||||||
|
/** @type {number} */
|
||||||
|
this.numEpisodes
|
||||||
|
|
||||||
/** @type {import('./PodcastEpisode')[]} */
|
/** @type {import('./PodcastEpisode')[]} */
|
||||||
this.podcastEpisodes
|
this.podcastEpisodes
|
||||||
@@ -138,13 +141,22 @@ class Podcast extends Model {
|
|||||||
maxNewEpisodesToDownload: DataTypes.INTEGER,
|
maxNewEpisodesToDownload: DataTypes.INTEGER,
|
||||||
coverPath: DataTypes.STRING,
|
coverPath: DataTypes.STRING,
|
||||||
tags: DataTypes.JSON,
|
tags: DataTypes.JSON,
|
||||||
genres: DataTypes.JSON
|
genres: DataTypes.JSON,
|
||||||
|
numEpisodes: DataTypes.INTEGER
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
sequelize,
|
sequelize,
|
||||||
modelName: 'podcast'
|
modelName: 'podcast'
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
Podcast.addHook('afterDestroy', async (instance) => {
|
||||||
|
libraryItemsPodcastFilters.clearCountCache('podcast', 'afterDestroy')
|
||||||
|
})
|
||||||
|
|
||||||
|
Podcast.addHook('afterCreate', async (instance) => {
|
||||||
|
libraryItemsPodcastFilters.clearCountCache('podcast', 'afterCreate')
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
get hasMediaFiles() {
|
get hasMediaFiles() {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
const { DataTypes, Model } = require('sequelize')
|
const { DataTypes, Model } = require('sequelize')
|
||||||
|
const libraryItemsPodcastFilters = require('../utils/queries/libraryItemsPodcastFilters')
|
||||||
/**
|
/**
|
||||||
* @typedef ChapterObject
|
* @typedef ChapterObject
|
||||||
* @property {number} id
|
* @property {number} id
|
||||||
@@ -132,6 +132,14 @@ class PodcastEpisode extends Model {
|
|||||||
onDelete: 'CASCADE'
|
onDelete: 'CASCADE'
|
||||||
})
|
})
|
||||||
PodcastEpisode.belongsTo(podcast)
|
PodcastEpisode.belongsTo(podcast)
|
||||||
|
|
||||||
|
PodcastEpisode.addHook('afterDestroy', async (instance) => {
|
||||||
|
libraryItemsPodcastFilters.clearCountCache('podcastEpisode', 'afterDestroy')
|
||||||
|
})
|
||||||
|
|
||||||
|
PodcastEpisode.addHook('afterCreate', async (instance) => {
|
||||||
|
libraryItemsPodcastFilters.clearCountCache('podcastEpisode', 'afterCreate')
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
get size() {
|
get size() {
|
||||||
|
|||||||
@@ -404,6 +404,14 @@ class User extends Model {
|
|||||||
return count > 0
|
return count > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static mediaProgressRemoved(mediaProgress) {
|
||||||
|
const cachedUser = userCache.getById(mediaProgress.userId)
|
||||||
|
if (cachedUser) {
|
||||||
|
Logger.debug(`[User] mediaProgressRemoved: ${mediaProgress.id} from user ${cachedUser.id}`)
|
||||||
|
cachedUser.mediaProgresses = cachedUser.mediaProgresses.filter((mp) => mp.id !== mediaProgress.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize model
|
* Initialize model
|
||||||
* @param {import('../Database').sequelize} sequelize
|
* @param {import('../Database').sequelize} sequelize
|
||||||
@@ -626,6 +634,7 @@ class User extends Model {
|
|||||||
/** @type {import('./MediaProgress')|null} */
|
/** @type {import('./MediaProgress')|null} */
|
||||||
let mediaProgress = null
|
let mediaProgress = null
|
||||||
let mediaItemId = null
|
let mediaItemId = null
|
||||||
|
let podcastId = null
|
||||||
if (progressPayload.episodeId) {
|
if (progressPayload.episodeId) {
|
||||||
const podcastEpisode = await this.sequelize.models.podcastEpisode.findByPk(progressPayload.episodeId, {
|
const podcastEpisode = await this.sequelize.models.podcastEpisode.findByPk(progressPayload.episodeId, {
|
||||||
attributes: ['id', 'podcastId'],
|
attributes: ['id', 'podcastId'],
|
||||||
@@ -654,6 +663,7 @@ class User extends Model {
|
|||||||
}
|
}
|
||||||
mediaItemId = podcastEpisode.id
|
mediaItemId = podcastEpisode.id
|
||||||
mediaProgress = podcastEpisode.mediaProgresses?.[0]
|
mediaProgress = podcastEpisode.mediaProgresses?.[0]
|
||||||
|
podcastId = podcastEpisode.podcastId
|
||||||
} else {
|
} else {
|
||||||
const libraryItem = await this.sequelize.models.libraryItem.findByPk(progressPayload.libraryItemId, {
|
const libraryItem = await this.sequelize.models.libraryItem.findByPk(progressPayload.libraryItemId, {
|
||||||
attributes: ['id', 'mediaId', 'mediaType'],
|
attributes: ['id', 'mediaId', 'mediaType'],
|
||||||
@@ -686,6 +696,7 @@ class User extends Model {
|
|||||||
const newMediaProgressPayload = {
|
const newMediaProgressPayload = {
|
||||||
userId: this.id,
|
userId: this.id,
|
||||||
mediaItemId,
|
mediaItemId,
|
||||||
|
podcastId,
|
||||||
mediaItemType: progressPayload.episodeId ? 'podcastEpisode' : 'book',
|
mediaItemType: progressPayload.episodeId ? 'podcastEpisode' : 'book',
|
||||||
duration: isNullOrNaN(progressPayload.duration) ? 0 : Number(progressPayload.duration),
|
duration: isNullOrNaN(progressPayload.duration) ? 0 : Number(progressPayload.duration),
|
||||||
currentTime: isNullOrNaN(progressPayload.currentTime) ? 0 : Number(progressPayload.currentTime),
|
currentTime: isNullOrNaN(progressPayload.currentTime) ? 0 : Number(progressPayload.currentTime),
|
||||||
|
|||||||
@@ -521,6 +521,8 @@ class BookScanner {
|
|||||||
libraryItemObj.isMissing = false
|
libraryItemObj.isMissing = false
|
||||||
libraryItemObj.isInvalid = false
|
libraryItemObj.isInvalid = false
|
||||||
libraryItemObj.extraData = {}
|
libraryItemObj.extraData = {}
|
||||||
|
libraryItemObj.title = bookMetadata.title
|
||||||
|
libraryItemObj.titleIgnorePrefix = getTitleIgnorePrefix(bookMetadata.title)
|
||||||
|
|
||||||
// Set isSupplementary flag on ebook library files
|
// Set isSupplementary flag on ebook library files
|
||||||
for (const libraryFile of libraryItemObj.libraryFiles) {
|
for (const libraryFile of libraryItemObj.libraryFiles) {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
const uuidv4 = require("uuid").v4
|
const uuidv4 = require('uuid').v4
|
||||||
const Path = require('path')
|
const Path = require('path')
|
||||||
const { LogLevel } = require('../utils/constants')
|
const { LogLevel } = require('../utils/constants')
|
||||||
const { getTitleIgnorePrefix } = require('../utils/index')
|
const { getTitleIgnorePrefix } = require('../utils/index')
|
||||||
@@ -8,9 +8,9 @@ const { filePathToPOSIX, getFileTimestampsWithIno } = require('../utils/fileUtil
|
|||||||
const AudioFile = require('../objects/files/AudioFile')
|
const AudioFile = require('../objects/files/AudioFile')
|
||||||
const CoverManager = require('../managers/CoverManager')
|
const CoverManager = require('../managers/CoverManager')
|
||||||
const LibraryFile = require('../objects/files/LibraryFile')
|
const LibraryFile = require('../objects/files/LibraryFile')
|
||||||
const fsExtra = require("../libs/fsExtra")
|
const fsExtra = require('../libs/fsExtra')
|
||||||
const PodcastEpisode = require("../models/PodcastEpisode")
|
const PodcastEpisode = require('../models/PodcastEpisode')
|
||||||
const AbsMetadataFileScanner = require("./AbsMetadataFileScanner")
|
const AbsMetadataFileScanner = require('./AbsMetadataFileScanner')
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Metadata for podcasts pulled from files
|
* Metadata for podcasts pulled from files
|
||||||
@@ -32,13 +32,13 @@ const AbsMetadataFileScanner = require("./AbsMetadataFileScanner")
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
class PodcastScanner {
|
class PodcastScanner {
|
||||||
constructor() { }
|
constructor() {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {import('../models/LibraryItem')} existingLibraryItem
|
* @param {import('../models/LibraryItem')} existingLibraryItem
|
||||||
* @param {import('./LibraryItemScanData')} libraryItemData
|
* @param {import('./LibraryItemScanData')} libraryItemData
|
||||||
* @param {import('../models/Library').LibrarySettingsObject} librarySettings
|
* @param {import('../models/Library').LibrarySettingsObject} librarySettings
|
||||||
* @param {import('./LibraryScan')} libraryScan
|
* @param {import('./LibraryScan')} libraryScan
|
||||||
* @returns {Promise<{libraryItem:import('../models/LibraryItem'), wasUpdated:boolean}>}
|
* @returns {Promise<{libraryItem:import('../models/LibraryItem'), wasUpdated:boolean}>}
|
||||||
*/
|
*/
|
||||||
async rescanExistingPodcastLibraryItem(existingLibraryItem, libraryItemData, librarySettings, libraryScan) {
|
async rescanExistingPodcastLibraryItem(existingLibraryItem, libraryItemData, librarySettings, libraryScan) {
|
||||||
@@ -59,28 +59,34 @@ class PodcastScanner {
|
|||||||
|
|
||||||
if (libraryItemData.hasAudioFileChanges || libraryItemData.audioLibraryFiles.length !== existingPodcastEpisodes.length) {
|
if (libraryItemData.hasAudioFileChanges || libraryItemData.audioLibraryFiles.length !== existingPodcastEpisodes.length) {
|
||||||
// Filter out and destroy episodes that were removed
|
// Filter out and destroy episodes that were removed
|
||||||
existingPodcastEpisodes = await Promise.all(existingPodcastEpisodes.filter(async ep => {
|
existingPodcastEpisodes = await Promise.all(
|
||||||
if (libraryItemData.checkAudioFileRemoved(ep.audioFile)) {
|
existingPodcastEpisodes.filter(async (ep) => {
|
||||||
libraryScan.addLog(LogLevel.INFO, `Podcast episode "${ep.title}" audio file was removed`)
|
if (libraryItemData.checkAudioFileRemoved(ep.audioFile)) {
|
||||||
// TODO: Should clean up other data linked to this episode
|
libraryScan.addLog(LogLevel.INFO, `Podcast episode "${ep.title}" audio file was removed`)
|
||||||
await ep.destroy()
|
// TODO: Should clean up other data linked to this episode
|
||||||
return false
|
await ep.destroy()
|
||||||
}
|
return false
|
||||||
return true
|
}
|
||||||
}))
|
return true
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
// Update audio files that were modified
|
// Update audio files that were modified
|
||||||
if (libraryItemData.audioLibraryFilesModified.length) {
|
if (libraryItemData.audioLibraryFilesModified.length) {
|
||||||
let scannedAudioFiles = await AudioFileScanner.executeMediaFileScans(existingLibraryItem.mediaType, libraryItemData, libraryItemData.audioLibraryFilesModified.map(lf => lf.new))
|
let scannedAudioFiles = await AudioFileScanner.executeMediaFileScans(
|
||||||
|
existingLibraryItem.mediaType,
|
||||||
|
libraryItemData,
|
||||||
|
libraryItemData.audioLibraryFilesModified.map((lf) => lf.new)
|
||||||
|
)
|
||||||
|
|
||||||
for (const podcastEpisode of existingPodcastEpisodes) {
|
for (const podcastEpisode of existingPodcastEpisodes) {
|
||||||
let matchedScannedAudioFile = scannedAudioFiles.find(saf => saf.metadata.path === podcastEpisode.audioFile.metadata.path)
|
let matchedScannedAudioFile = scannedAudioFiles.find((saf) => saf.metadata.path === podcastEpisode.audioFile.metadata.path)
|
||||||
if (!matchedScannedAudioFile) {
|
if (!matchedScannedAudioFile) {
|
||||||
matchedScannedAudioFile = scannedAudioFiles.find(saf => saf.ino === podcastEpisode.audioFile.ino)
|
matchedScannedAudioFile = scannedAudioFiles.find((saf) => saf.ino === podcastEpisode.audioFile.ino)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (matchedScannedAudioFile) {
|
if (matchedScannedAudioFile) {
|
||||||
scannedAudioFiles = scannedAudioFiles.filter(saf => saf !== matchedScannedAudioFile)
|
scannedAudioFiles = scannedAudioFiles.filter((saf) => saf !== matchedScannedAudioFile)
|
||||||
const audioFile = new AudioFile(podcastEpisode.audioFile)
|
const audioFile = new AudioFile(podcastEpisode.audioFile)
|
||||||
audioFile.updateFromScan(matchedScannedAudioFile)
|
audioFile.updateFromScan(matchedScannedAudioFile)
|
||||||
podcastEpisode.audioFile = audioFile.toJSON()
|
podcastEpisode.audioFile = audioFile.toJSON()
|
||||||
@@ -131,15 +137,20 @@ class PodcastScanner {
|
|||||||
|
|
||||||
let hasMediaChanges = false
|
let hasMediaChanges = false
|
||||||
|
|
||||||
|
if (existingPodcastEpisodes.length !== media.numEpisodes) {
|
||||||
|
media.numEpisodes = existingPodcastEpisodes.length
|
||||||
|
hasMediaChanges = true
|
||||||
|
}
|
||||||
|
|
||||||
// Check if cover was removed
|
// Check if cover was removed
|
||||||
if (media.coverPath && libraryItemData.imageLibraryFilesRemoved.some(lf => lf.metadata.path === media.coverPath)) {
|
if (media.coverPath && libraryItemData.imageLibraryFilesRemoved.some((lf) => lf.metadata.path === media.coverPath)) {
|
||||||
media.coverPath = null
|
media.coverPath = null
|
||||||
hasMediaChanges = true
|
hasMediaChanges = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update cover if it was modified
|
// Update cover if it was modified
|
||||||
if (media.coverPath && libraryItemData.imageLibraryFilesModified.length) {
|
if (media.coverPath && libraryItemData.imageLibraryFilesModified.length) {
|
||||||
let coverMatch = libraryItemData.imageLibraryFilesModified.find(iFile => iFile.old.metadata.path === media.coverPath)
|
let coverMatch = libraryItemData.imageLibraryFilesModified.find((iFile) => iFile.old.metadata.path === media.coverPath)
|
||||||
if (coverMatch) {
|
if (coverMatch) {
|
||||||
const coverPath = coverMatch.new.metadata.path
|
const coverPath = coverMatch.new.metadata.path
|
||||||
if (coverPath !== media.coverPath) {
|
if (coverPath !== media.coverPath) {
|
||||||
@@ -154,7 +165,7 @@ class PodcastScanner {
|
|||||||
// Check if cover is not set and image files were found
|
// Check if cover is not set and image files were found
|
||||||
if (!media.coverPath && libraryItemData.imageLibraryFiles.length) {
|
if (!media.coverPath && libraryItemData.imageLibraryFiles.length) {
|
||||||
// Prefer using a cover image with the name "cover" otherwise use the first image
|
// Prefer using a cover image with the name "cover" otherwise use the first image
|
||||||
const coverMatch = libraryItemData.imageLibraryFiles.find(iFile => /\/cover\.[^.\/]*$/.test(iFile.metadata.path))
|
const coverMatch = libraryItemData.imageLibraryFiles.find((iFile) => /\/cover\.[^.\/]*$/.test(iFile.metadata.path))
|
||||||
media.coverPath = coverMatch?.metadata.path || libraryItemData.imageLibraryFiles[0].metadata.path
|
media.coverPath = coverMatch?.metadata.path || libraryItemData.imageLibraryFiles[0].metadata.path
|
||||||
hasMediaChanges = true
|
hasMediaChanges = true
|
||||||
}
|
}
|
||||||
@@ -167,7 +178,7 @@ class PodcastScanner {
|
|||||||
|
|
||||||
if (key === 'genres') {
|
if (key === 'genres') {
|
||||||
const existingGenres = media.genres || []
|
const existingGenres = media.genres || []
|
||||||
if (podcastMetadata.genres.some(g => !existingGenres.includes(g)) || existingGenres.some(g => !podcastMetadata.genres.includes(g))) {
|
if (podcastMetadata.genres.some((g) => !existingGenres.includes(g)) || existingGenres.some((g) => !podcastMetadata.genres.includes(g))) {
|
||||||
libraryScan.addLog(LogLevel.DEBUG, `Updating podcast genres "${existingGenres.join(',')}" => "${podcastMetadata.genres.join(',')}" for podcast "${podcastMetadata.title}"`)
|
libraryScan.addLog(LogLevel.DEBUG, `Updating podcast genres "${existingGenres.join(',')}" => "${podcastMetadata.genres.join(',')}" for podcast "${podcastMetadata.title}"`)
|
||||||
media.genres = podcastMetadata.genres
|
media.genres = podcastMetadata.genres
|
||||||
media.changed('genres', true)
|
media.changed('genres', true)
|
||||||
@@ -175,7 +186,7 @@ class PodcastScanner {
|
|||||||
}
|
}
|
||||||
} else if (key === 'tags') {
|
} else if (key === 'tags') {
|
||||||
const existingTags = media.tags || []
|
const existingTags = media.tags || []
|
||||||
if (podcastMetadata.tags.some(t => !existingTags.includes(t)) || existingTags.some(t => !podcastMetadata.tags.includes(t))) {
|
if (podcastMetadata.tags.some((t) => !existingTags.includes(t)) || existingTags.some((t) => !podcastMetadata.tags.includes(t))) {
|
||||||
libraryScan.addLog(LogLevel.DEBUG, `Updating podcast tags "${existingTags.join(',')}" => "${podcastMetadata.tags.join(',')}" for podcast "${podcastMetadata.title}"`)
|
libraryScan.addLog(LogLevel.DEBUG, `Updating podcast tags "${existingTags.join(',')}" => "${podcastMetadata.tags.join(',')}" for podcast "${podcastMetadata.title}"`)
|
||||||
media.tags = podcastMetadata.tags
|
media.tags = podcastMetadata.tags
|
||||||
media.changed('tags', true)
|
media.changed('tags', true)
|
||||||
@@ -190,7 +201,7 @@ class PodcastScanner {
|
|||||||
|
|
||||||
// If no cover then extract cover from audio file if available
|
// If no cover then extract cover from audio file if available
|
||||||
if (!media.coverPath && existingPodcastEpisodes.length) {
|
if (!media.coverPath && existingPodcastEpisodes.length) {
|
||||||
const audioFiles = existingPodcastEpisodes.map(ep => ep.audioFile)
|
const audioFiles = existingPodcastEpisodes.map((ep) => ep.audioFile)
|
||||||
const extractedCoverPath = await CoverManager.saveEmbeddedCoverArt(audioFiles, existingLibraryItem.id, existingLibraryItem.path)
|
const extractedCoverPath = await CoverManager.saveEmbeddedCoverArt(audioFiles, existingLibraryItem.id, existingLibraryItem.path)
|
||||||
if (extractedCoverPath) {
|
if (extractedCoverPath) {
|
||||||
libraryScan.addLog(LogLevel.DEBUG, `Updating podcast "${podcastMetadata.title}" extracted embedded cover art from audio file to path "${extractedCoverPath}"`)
|
libraryScan.addLog(LogLevel.DEBUG, `Updating podcast "${podcastMetadata.title}" extracted embedded cover art from audio file to path "${extractedCoverPath}"`)
|
||||||
@@ -222,10 +233,10 @@ class PodcastScanner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {import('./LibraryItemScanData')} libraryItemData
|
* @param {import('./LibraryItemScanData')} libraryItemData
|
||||||
* @param {import('../models/Library').LibrarySettingsObject} librarySettings
|
* @param {import('../models/Library').LibrarySettingsObject} librarySettings
|
||||||
* @param {import('./LibraryScan')} libraryScan
|
* @param {import('./LibraryScan')} libraryScan
|
||||||
* @returns {Promise<import('../models/LibraryItem')>}
|
* @returns {Promise<import('../models/LibraryItem')>}
|
||||||
*/
|
*/
|
||||||
async scanNewPodcastLibraryItem(libraryItemData, librarySettings, libraryScan) {
|
async scanNewPodcastLibraryItem(libraryItemData, librarySettings, libraryScan) {
|
||||||
@@ -267,7 +278,7 @@ class PodcastScanner {
|
|||||||
// Set cover image from library file
|
// Set cover image from library file
|
||||||
if (libraryItemData.imageLibraryFiles.length) {
|
if (libraryItemData.imageLibraryFiles.length) {
|
||||||
// Prefer using a cover image with the name "cover" otherwise use the first image
|
// Prefer using a cover image with the name "cover" otherwise use the first image
|
||||||
const coverMatch = libraryItemData.imageLibraryFiles.find(iFile => /\/cover\.[^.\/]*$/.test(iFile.metadata.path))
|
const coverMatch = libraryItemData.imageLibraryFiles.find((iFile) => /\/cover\.[^.\/]*$/.test(iFile.metadata.path))
|
||||||
podcastMetadata.coverPath = coverMatch?.metadata.path || libraryItemData.imageLibraryFiles[0].metadata.path
|
podcastMetadata.coverPath = coverMatch?.metadata.path || libraryItemData.imageLibraryFiles[0].metadata.path
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -283,7 +294,8 @@ class PodcastScanner {
|
|||||||
lastEpisodeCheck: 0,
|
lastEpisodeCheck: 0,
|
||||||
maxEpisodesToKeep: 0,
|
maxEpisodesToKeep: 0,
|
||||||
maxNewEpisodesToDownload: 3,
|
maxNewEpisodesToDownload: 3,
|
||||||
podcastEpisodes: newPodcastEpisodes
|
podcastEpisodes: newPodcastEpisodes,
|
||||||
|
numEpisodes: newPodcastEpisodes.length
|
||||||
}
|
}
|
||||||
|
|
||||||
const libraryItemObj = libraryItemData.libraryItemObject
|
const libraryItemObj = libraryItemData.libraryItemObject
|
||||||
@@ -291,6 +303,8 @@ class PodcastScanner {
|
|||||||
libraryItemObj.isMissing = false
|
libraryItemObj.isMissing = false
|
||||||
libraryItemObj.isInvalid = false
|
libraryItemObj.isInvalid = false
|
||||||
libraryItemObj.extraData = {}
|
libraryItemObj.extraData = {}
|
||||||
|
libraryItemObj.title = podcastObject.title
|
||||||
|
libraryItemObj.titleIgnorePrefix = getTitleIgnorePrefix(podcastObject.title)
|
||||||
|
|
||||||
// If cover was not found in folder then check embedded covers in audio files
|
// If cover was not found in folder then check embedded covers in audio files
|
||||||
if (!podcastObject.coverPath && scannedAudioFiles.length) {
|
if (!podcastObject.coverPath && scannedAudioFiles.length) {
|
||||||
@@ -324,10 +338,10 @@ class PodcastScanner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {PodcastEpisode[]} podcastEpisodes Not the models for new podcasts
|
* @param {PodcastEpisode[]} podcastEpisodes Not the models for new podcasts
|
||||||
* @param {import('./LibraryItemScanData')} libraryItemData
|
* @param {import('./LibraryItemScanData')} libraryItemData
|
||||||
* @param {import('./LibraryScan')} libraryScan
|
* @param {import('./LibraryScan')} libraryScan
|
||||||
* @param {string} [existingLibraryItemId]
|
* @param {string} [existingLibraryItemId]
|
||||||
* @returns {Promise<PodcastMetadataObject>}
|
* @returns {Promise<PodcastMetadataObject>}
|
||||||
*/
|
*/
|
||||||
@@ -364,8 +378,8 @@ class PodcastScanner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {import('../models/LibraryItem')} libraryItem
|
* @param {import('../models/LibraryItem')} libraryItem
|
||||||
* @param {import('./LibraryScan')} libraryScan
|
* @param {import('./LibraryScan')} libraryScan
|
||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
*/
|
*/
|
||||||
@@ -399,41 +413,44 @@ class PodcastScanner {
|
|||||||
explicit: !!libraryItem.media.explicit,
|
explicit: !!libraryItem.media.explicit,
|
||||||
podcastType: libraryItem.media.podcastType
|
podcastType: libraryItem.media.podcastType
|
||||||
}
|
}
|
||||||
return fsExtra.writeFile(metadataFilePath, JSON.stringify(jsonObject, null, 2)).then(async () => {
|
return fsExtra
|
||||||
// Add metadata.json to libraryFiles array if it is new
|
.writeFile(metadataFilePath, JSON.stringify(jsonObject, null, 2))
|
||||||
let metadataLibraryFile = libraryItem.libraryFiles.find(lf => lf.metadata.path === filePathToPOSIX(metadataFilePath))
|
.then(async () => {
|
||||||
if (storeMetadataWithItem) {
|
// Add metadata.json to libraryFiles array if it is new
|
||||||
if (!metadataLibraryFile) {
|
let metadataLibraryFile = libraryItem.libraryFiles.find((lf) => lf.metadata.path === filePathToPOSIX(metadataFilePath))
|
||||||
const newLibraryFile = new LibraryFile()
|
if (storeMetadataWithItem) {
|
||||||
await newLibraryFile.setDataFromPath(metadataFilePath, `metadata.json`)
|
if (!metadataLibraryFile) {
|
||||||
metadataLibraryFile = newLibraryFile.toJSON()
|
const newLibraryFile = new LibraryFile()
|
||||||
libraryItem.libraryFiles.push(metadataLibraryFile)
|
await newLibraryFile.setDataFromPath(metadataFilePath, `metadata.json`)
|
||||||
} else {
|
metadataLibraryFile = newLibraryFile.toJSON()
|
||||||
const fileTimestamps = await getFileTimestampsWithIno(metadataFilePath)
|
libraryItem.libraryFiles.push(metadataLibraryFile)
|
||||||
if (fileTimestamps) {
|
} else {
|
||||||
metadataLibraryFile.metadata.mtimeMs = fileTimestamps.mtimeMs
|
const fileTimestamps = await getFileTimestampsWithIno(metadataFilePath)
|
||||||
metadataLibraryFile.metadata.ctimeMs = fileTimestamps.ctimeMs
|
if (fileTimestamps) {
|
||||||
metadataLibraryFile.metadata.size = fileTimestamps.size
|
metadataLibraryFile.metadata.mtimeMs = fileTimestamps.mtimeMs
|
||||||
metadataLibraryFile.ino = fileTimestamps.ino
|
metadataLibraryFile.metadata.ctimeMs = fileTimestamps.ctimeMs
|
||||||
|
metadataLibraryFile.metadata.size = fileTimestamps.size
|
||||||
|
metadataLibraryFile.ino = fileTimestamps.ino
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const libraryItemDirTimestamps = await getFileTimestampsWithIno(libraryItem.path)
|
||||||
|
if (libraryItemDirTimestamps) {
|
||||||
|
libraryItem.mtime = libraryItemDirTimestamps.mtimeMs
|
||||||
|
libraryItem.ctime = libraryItemDirTimestamps.ctimeMs
|
||||||
|
let size = 0
|
||||||
|
libraryItem.libraryFiles.forEach((lf) => (size += !isNaN(lf.metadata.size) ? Number(lf.metadata.size) : 0))
|
||||||
|
libraryItem.size = size
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const libraryItemDirTimestamps = await getFileTimestampsWithIno(libraryItem.path)
|
|
||||||
if (libraryItemDirTimestamps) {
|
|
||||||
libraryItem.mtime = libraryItemDirTimestamps.mtimeMs
|
|
||||||
libraryItem.ctime = libraryItemDirTimestamps.ctimeMs
|
|
||||||
let size = 0
|
|
||||||
libraryItem.libraryFiles.forEach((lf) => size += (!isNaN(lf.metadata.size) ? Number(lf.metadata.size) : 0))
|
|
||||||
libraryItem.size = size
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
libraryScan.addLog(LogLevel.DEBUG, `Success saving abmetadata to "${metadataFilePath}"`)
|
libraryScan.addLog(LogLevel.DEBUG, `Success saving abmetadata to "${metadataFilePath}"`)
|
||||||
|
|
||||||
return metadataLibraryFile
|
return metadataLibraryFile
|
||||||
}).catch((error) => {
|
})
|
||||||
libraryScan.addLog(LogLevel.ERROR, `Failed to save json file at "${metadataFilePath}"`, error)
|
.catch((error) => {
|
||||||
return null
|
libraryScan.addLog(LogLevel.ERROR, `Failed to save json file at "${metadataFilePath}"`, error)
|
||||||
})
|
return null
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
module.exports = new PodcastScanner()
|
module.exports = new PodcastScanner()
|
||||||
|
|||||||
@@ -48,13 +48,7 @@ class Scanner {
|
|||||||
let updatePayload = {}
|
let updatePayload = {}
|
||||||
let hasUpdated = false
|
let hasUpdated = false
|
||||||
|
|
||||||
let existingAuthors = [] // Used for checking if authors or series are now empty
|
|
||||||
let existingSeries = []
|
|
||||||
|
|
||||||
if (libraryItem.isBook) {
|
if (libraryItem.isBook) {
|
||||||
existingAuthors = libraryItem.media.authors.map((a) => a.id)
|
|
||||||
existingSeries = libraryItem.media.series.map((s) => s.id)
|
|
||||||
|
|
||||||
const searchISBN = options.isbn || libraryItem.media.isbn
|
const searchISBN = options.isbn || libraryItem.media.isbn
|
||||||
const searchASIN = options.asin || libraryItem.media.asin
|
const searchASIN = options.asin || libraryItem.media.asin
|
||||||
|
|
||||||
|
|||||||
@@ -145,15 +145,15 @@ function extractEpisodeData(item) {
|
|||||||
|
|
||||||
if (item.enclosure?.[0]?.['$']?.url) {
|
if (item.enclosure?.[0]?.['$']?.url) {
|
||||||
enclosure = item.enclosure[0]['$']
|
enclosure = item.enclosure[0]['$']
|
||||||
} else if(item['media:content']?.find(c => c?.['$']?.url && (c?.['$']?.type ?? "").startsWith("audio"))) {
|
} else if (item['media:content']?.find((c) => c?.['$']?.url && (c?.['$']?.type ?? '').startsWith('audio'))) {
|
||||||
enclosure = item['media:content'].find(c => (c['$']?.type ?? "").startsWith("audio"))['$']
|
enclosure = item['media:content'].find((c) => (c['$']?.type ?? '').startsWith('audio'))['$']
|
||||||
} else {
|
} else {
|
||||||
Logger.error(`[podcastUtils] Invalid podcast episode data`)
|
Logger.error(`[podcastUtils] Invalid podcast episode data`)
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
const episode = {
|
const episode = {
|
||||||
enclosure: enclosure,
|
enclosure: enclosure
|
||||||
}
|
}
|
||||||
|
|
||||||
episode.enclosure.url = episode.enclosure.url.trim()
|
episode.enclosure.url = episode.enclosure.url.trim()
|
||||||
@@ -343,6 +343,14 @@ module.exports.getPodcastFeed = (feedUrl, excludeEpisodeMetadata = false) => {
|
|||||||
return payload.podcast
|
return payload.podcast
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
|
// Check for failures due to redirecting from http to https. If original url was http, upgrade to https and try again
|
||||||
|
if (error.code === 'ERR_FR_REDIRECTION_FAILURE' && error.cause.code === 'ERR_INVALID_PROTOCOL') {
|
||||||
|
if (feedUrl.startsWith('http://') && error.request._options.protocol === 'https:') {
|
||||||
|
Logger.info('Redirection from http to https detected. Upgrading Request', error.request._options.href)
|
||||||
|
feedUrl = feedUrl.replace('http://', 'https://')
|
||||||
|
return this.getPodcastFeed(feedUrl, excludeEpisodeMetadata)
|
||||||
|
}
|
||||||
|
}
|
||||||
Logger.error('[podcastUtils] getPodcastFeed Error', error)
|
Logger.error('[podcastUtils] getPodcastFeed Error', error)
|
||||||
return null
|
return null
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
const { performance, createHistogram } = require('perf_hooks')
|
||||||
|
const util = require('util')
|
||||||
|
const Logger = require('../Logger')
|
||||||
|
|
||||||
|
const histograms = new Map()
|
||||||
|
|
||||||
|
function profile(asyncFunc, isFindQuery = true, funcName = asyncFunc.name) {
|
||||||
|
if (!histograms.has(funcName)) {
|
||||||
|
const histogram = createHistogram()
|
||||||
|
histogram.values = []
|
||||||
|
histograms.set(funcName, histogram)
|
||||||
|
}
|
||||||
|
const histogram = histograms.get(funcName)
|
||||||
|
|
||||||
|
return async (...args) => {
|
||||||
|
if (isFindQuery) {
|
||||||
|
const findOptions = args[0]
|
||||||
|
Logger.info(`[${funcName}] findOptions:`, util.inspect(findOptions, { depth: null }))
|
||||||
|
findOptions.logging = (query, time) => Logger.info(`[${funcName}] ${query} Elapsed time: ${time}ms`)
|
||||||
|
findOptions.benchmark = true
|
||||||
|
}
|
||||||
|
const start = performance.now()
|
||||||
|
try {
|
||||||
|
const result = await asyncFunc(...args)
|
||||||
|
return result
|
||||||
|
} catch (error) {
|
||||||
|
Logger.error(`[${funcName}] failed`)
|
||||||
|
throw error
|
||||||
|
} finally {
|
||||||
|
const end = performance.now()
|
||||||
|
const duration = Math.round(end - start)
|
||||||
|
histogram.record(duration)
|
||||||
|
histogram.values.push(duration)
|
||||||
|
Logger.info(`[${funcName}] duration: ${duration}ms`)
|
||||||
|
Logger.info(`[${funcName}] histogram values:`, histogram.values)
|
||||||
|
Logger.info(`[${funcName}] histogram:`, histogram)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { profile }
|
||||||
@@ -4,6 +4,7 @@ const Database = require('../../Database')
|
|||||||
const libraryItemsBookFilters = require('./libraryItemsBookFilters')
|
const libraryItemsBookFilters = require('./libraryItemsBookFilters')
|
||||||
const libraryItemsPodcastFilters = require('./libraryItemsPodcastFilters')
|
const libraryItemsPodcastFilters = require('./libraryItemsPodcastFilters')
|
||||||
const { createNewSortInstance } = require('../../libs/fastSort')
|
const { createNewSortInstance } = require('../../libs/fastSort')
|
||||||
|
const { profile } = require('../../utils/profiler')
|
||||||
const naturalSort = createNewSortInstance({
|
const naturalSort = createNewSortInstance({
|
||||||
comparer: new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' }).compare
|
comparer: new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' }).compare
|
||||||
})
|
})
|
||||||
@@ -474,7 +475,8 @@ module.exports = {
|
|||||||
// Check how many podcasts are in library to determine if we need to load all of the data
|
// Check how many podcasts are in library to determine if we need to load all of the data
|
||||||
// This is done to handle the edge case of podcasts having been deleted and not having
|
// This is done to handle the edge case of podcasts having been deleted and not having
|
||||||
// an updatedAt timestamp to trigger a reload of the filter data
|
// an updatedAt timestamp to trigger a reload of the filter data
|
||||||
const podcastCountFromDatabase = await Database.podcastModel.count({
|
const podcastModelCount = process.env.QUERY_PROFILING ? profile(Database.podcastModel.count.bind(Database.podcastModel)) : Database.podcastModel.count.bind(Database.podcastModel)
|
||||||
|
const podcastCountFromDatabase = await podcastModelCount({
|
||||||
include: {
|
include: {
|
||||||
model: Database.libraryItemModel,
|
model: Database.libraryItemModel,
|
||||||
attributes: [],
|
attributes: [],
|
||||||
@@ -489,7 +491,7 @@ module.exports = {
|
|||||||
// data was loaded. If so, we can skip loading all of the data.
|
// data was loaded. If so, we can skip loading all of the data.
|
||||||
// Because many items could change, just check the count of items instead
|
// Because many items could change, just check the count of items instead
|
||||||
// of actually loading the data twice
|
// of actually loading the data twice
|
||||||
const changedPodcasts = await Database.podcastModel.count({
|
const changedPodcasts = await podcastModelCount({
|
||||||
include: {
|
include: {
|
||||||
model: Database.libraryItemModel,
|
model: Database.libraryItemModel,
|
||||||
attributes: [],
|
attributes: [],
|
||||||
@@ -520,7 +522,8 @@ module.exports = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Something has changed in the podcasts table, so reload all of the filter data for library
|
// Something has changed in the podcasts table, so reload all of the filter data for library
|
||||||
const podcasts = await Database.podcastModel.findAll({
|
const findAll = process.env.QUERY_PROFILING ? profile(Database.podcastModel.findAll.bind(Database.podcastModel)) : Database.podcastModel.findAll.bind(Database.podcastModel)
|
||||||
|
const podcasts = await findAll({
|
||||||
include: {
|
include: {
|
||||||
model: Database.libraryItemModel,
|
model: Database.libraryItemModel,
|
||||||
attributes: [],
|
attributes: [],
|
||||||
|
|||||||
@@ -4,6 +4,9 @@ const Logger = require('../../Logger')
|
|||||||
const authorFilters = require('./authorFilters')
|
const authorFilters = require('./authorFilters')
|
||||||
|
|
||||||
const ShareManager = require('../../managers/ShareManager')
|
const ShareManager = require('../../managers/ShareManager')
|
||||||
|
const { profile } = require('../profiler')
|
||||||
|
const stringifySequelizeQuery = require('../stringifySequelizeQuery')
|
||||||
|
const countCache = new Map()
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
/**
|
/**
|
||||||
@@ -270,9 +273,9 @@ module.exports = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (global.ServerSettings.sortingIgnorePrefix) {
|
if (global.ServerSettings.sortingIgnorePrefix) {
|
||||||
return [[Sequelize.literal('titleIgnorePrefix COLLATE NOCASE'), dir]]
|
return [[Sequelize.literal('`libraryItem`.`titleIgnorePrefix` COLLATE NOCASE'), dir]]
|
||||||
} else {
|
} else {
|
||||||
return [[Sequelize.literal('`book`.`title` COLLATE NOCASE'), dir]]
|
return [[Sequelize.literal('`libraryItem`.`title` COLLATE NOCASE'), dir]]
|
||||||
}
|
}
|
||||||
} else if (sortBy === 'sequence') {
|
} else if (sortBy === 'sequence') {
|
||||||
const nullDir = sortDesc ? 'DESC NULLS FIRST' : 'ASC NULLS LAST'
|
const nullDir = sortDesc ? 'DESC NULLS FIRST' : 'ASC NULLS LAST'
|
||||||
@@ -336,6 +339,29 @@ module.exports = {
|
|||||||
return { booksToExclude, bookSeriesToInclude }
|
return { booksToExclude, bookSeriesToInclude }
|
||||||
},
|
},
|
||||||
|
|
||||||
|
clearCountCache(hook) {
|
||||||
|
Logger.debug(`[LibraryItemsBookFilters] book.${hook}: Clearing count cache`)
|
||||||
|
countCache.clear()
|
||||||
|
},
|
||||||
|
|
||||||
|
async findAndCountAll(findOptions, limit, offset) {
|
||||||
|
const findOptionsKey = stringifySequelizeQuery(findOptions)
|
||||||
|
Logger.debug(`[LibraryItemsBookFilters] findOptionsKey: ${findOptionsKey}`)
|
||||||
|
|
||||||
|
findOptions.limit = limit || null
|
||||||
|
findOptions.offset = offset
|
||||||
|
|
||||||
|
if (countCache.has(findOptionsKey)) {
|
||||||
|
const rows = await Database.bookModel.findAll(findOptions)
|
||||||
|
|
||||||
|
return { rows, count: countCache.get(findOptionsKey) }
|
||||||
|
} else {
|
||||||
|
const result = await Database.bookModel.findAndCountAll(findOptions)
|
||||||
|
countCache.set(findOptionsKey, result.count)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get library items for book media type using filter and sort
|
* Get library items for book media type using filter and sort
|
||||||
* @param {string} libraryId
|
* @param {string} libraryId
|
||||||
@@ -411,7 +437,8 @@ module.exports = {
|
|||||||
if (includeRSSFeed) {
|
if (includeRSSFeed) {
|
||||||
libraryItemIncludes.push({
|
libraryItemIncludes.push({
|
||||||
model: Database.feedModel,
|
model: Database.feedModel,
|
||||||
required: filterGroup === 'feed-open'
|
required: filterGroup === 'feed-open',
|
||||||
|
separate: true
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if (filterGroup === 'feed-open' && !includeRSSFeed) {
|
if (filterGroup === 'feed-open' && !includeRSSFeed) {
|
||||||
@@ -554,13 +581,13 @@ module.exports = {
|
|||||||
// When collapsing series and sorting by title then use the series name instead of the book title
|
// When collapsing series and sorting by title then use the series name instead of the book title
|
||||||
// for this set an attribute "display_title" to use in sorting
|
// for this set an attribute "display_title" to use in sorting
|
||||||
if (global.ServerSettings.sortingIgnorePrefix) {
|
if (global.ServerSettings.sortingIgnorePrefix) {
|
||||||
bookAttributes.include.push([Sequelize.literal(`IFNULL((SELECT s.nameIgnorePrefix FROM bookSeries AS bs, series AS s WHERE bs.seriesId = s.id AND bs.bookId = book.id AND bs.id IN (${bookSeriesToInclude.map((v) => `"${v.id}"`).join(', ')})), titleIgnorePrefix)`), 'display_title'])
|
bookAttributes.include.push([Sequelize.literal(`IFNULL((SELECT s.nameIgnorePrefix FROM bookSeries AS bs, series AS s WHERE bs.seriesId = s.id AND bs.bookId = book.id AND bs.id IN (${bookSeriesToInclude.map((v) => `"${v.id}"`).join(', ')})), \`libraryItem\`.\`titleIgnorePrefix\`)`), 'display_title'])
|
||||||
} else {
|
} else {
|
||||||
bookAttributes.include.push([Sequelize.literal(`IFNULL((SELECT s.name FROM bookSeries AS bs, series AS s WHERE bs.seriesId = s.id AND bs.bookId = book.id AND bs.id IN (${bookSeriesToInclude.map((v) => `"${v.id}"`).join(', ')})), \`book\`.\`title\`)`), 'display_title'])
|
bookAttributes.include.push([Sequelize.literal(`IFNULL((SELECT s.name FROM bookSeries AS bs, series AS s WHERE bs.seriesId = s.id AND bs.bookId = book.id AND bs.id IN (${bookSeriesToInclude.map((v) => `"${v.id}"`).join(', ')})), \`libraryItem\`.\`title\`)`), 'display_title'])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const { rows: books, count } = await Database.bookModel.findAndCountAll({
|
const findOptions = {
|
||||||
where: bookWhere,
|
where: bookWhere,
|
||||||
distinct: true,
|
distinct: true,
|
||||||
attributes: bookAttributes,
|
attributes: bookAttributes,
|
||||||
@@ -577,10 +604,11 @@ module.exports = {
|
|||||||
...bookIncludes
|
...bookIncludes
|
||||||
],
|
],
|
||||||
order: sortOrder,
|
order: sortOrder,
|
||||||
subQuery: false,
|
subQuery: false
|
||||||
limit: limit || null,
|
}
|
||||||
offset
|
|
||||||
})
|
const findAndCountAll = process.env.QUERY_PROFILING ? profile(this.findAndCountAll) : this.findAndCountAll
|
||||||
|
const { rows: books, count } = await findAndCountAll(findOptions, limit, offset)
|
||||||
|
|
||||||
const libraryItems = books.map((bookExpanded) => {
|
const libraryItems = books.map((bookExpanded) => {
|
||||||
const libraryItem = bookExpanded.libraryItem
|
const libraryItem = bookExpanded.libraryItem
|
||||||
@@ -1008,8 +1036,8 @@ module.exports = {
|
|||||||
|
|
||||||
const textSearchQuery = await Database.createTextSearchQuery(query)
|
const textSearchQuery = await Database.createTextSearchQuery(query)
|
||||||
|
|
||||||
const matchTitle = textSearchQuery.matchExpression('title')
|
const matchTitle = textSearchQuery.matchExpression('book.title')
|
||||||
const matchSubtitle = textSearchQuery.matchExpression('subtitle')
|
const matchSubtitle = textSearchQuery.matchExpression('book.subtitle')
|
||||||
|
|
||||||
// Search title, subtitle, asin, isbn
|
// Search title, subtitle, asin, isbn
|
||||||
const books = await Database.bookModel.findAll({
|
const books = await Database.bookModel.findAll({
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
const Sequelize = require('sequelize')
|
const Sequelize = require('sequelize')
|
||||||
const Database = require('../../Database')
|
const Database = require('../../Database')
|
||||||
const Logger = require('../../Logger')
|
const Logger = require('../../Logger')
|
||||||
|
const { profile } = require('../../utils/profiler')
|
||||||
|
const stringifySequelizeQuery = require('../stringifySequelizeQuery')
|
||||||
|
|
||||||
|
const countCache = new Map()
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
/**
|
/**
|
||||||
@@ -84,9 +88,9 @@ module.exports = {
|
|||||||
return [[Sequelize.literal(`\`podcast\`.\`author\` COLLATE NOCASE ${nullDir}`)]]
|
return [[Sequelize.literal(`\`podcast\`.\`author\` COLLATE NOCASE ${nullDir}`)]]
|
||||||
} else if (sortBy === 'media.metadata.title') {
|
} else if (sortBy === 'media.metadata.title') {
|
||||||
if (global.ServerSettings.sortingIgnorePrefix) {
|
if (global.ServerSettings.sortingIgnorePrefix) {
|
||||||
return [[Sequelize.literal('titleIgnorePrefix COLLATE NOCASE'), dir]]
|
return [[Sequelize.literal('`libraryItem`.`titleIgnorePrefix` COLLATE NOCASE'), dir]]
|
||||||
} else {
|
} else {
|
||||||
return [[Sequelize.literal('`podcast`.`title` COLLATE NOCASE'), dir]]
|
return [[Sequelize.literal('`libraryItem`.`title` COLLATE NOCASE'), dir]]
|
||||||
}
|
}
|
||||||
} else if (sortBy === 'media.numTracks') {
|
} else if (sortBy === 'media.numTracks') {
|
||||||
return [['numEpisodes', dir]]
|
return [['numEpisodes', dir]]
|
||||||
@@ -96,6 +100,29 @@ module.exports = {
|
|||||||
return []
|
return []
|
||||||
},
|
},
|
||||||
|
|
||||||
|
clearCountCache(model, hook) {
|
||||||
|
Logger.debug(`[LibraryItemsPodcastFilters] ${model}.${hook}: Clearing count cache`)
|
||||||
|
countCache.clear()
|
||||||
|
},
|
||||||
|
|
||||||
|
async findAndCountAll(findOptions, model, limit, offset) {
|
||||||
|
const cacheKey = stringifySequelizeQuery(findOptions)
|
||||||
|
if (!countCache.has(cacheKey)) {
|
||||||
|
const count = await model.count(findOptions)
|
||||||
|
countCache.set(cacheKey, count)
|
||||||
|
}
|
||||||
|
|
||||||
|
findOptions.limit = limit
|
||||||
|
findOptions.offset = offset
|
||||||
|
|
||||||
|
const rows = await model.findAll(findOptions)
|
||||||
|
|
||||||
|
return {
|
||||||
|
rows,
|
||||||
|
count: countCache.get(cacheKey)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get library items for podcast media type using filter and sort
|
* Get library items for podcast media type using filter and sort
|
||||||
* @param {string} libraryId
|
* @param {string} libraryId
|
||||||
@@ -120,7 +147,8 @@ module.exports = {
|
|||||||
if (includeRSSFeed) {
|
if (includeRSSFeed) {
|
||||||
libraryItemIncludes.push({
|
libraryItemIncludes.push({
|
||||||
model: Database.feedModel,
|
model: Database.feedModel,
|
||||||
required: filterGroup === 'feed-open'
|
required: filterGroup === 'feed-open',
|
||||||
|
separate: true
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if (filterGroup === 'issues') {
|
if (filterGroup === 'issues') {
|
||||||
@@ -139,9 +167,6 @@ module.exports = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const podcastIncludes = []
|
const podcastIncludes = []
|
||||||
if (includeNumEpisodesIncomplete) {
|
|
||||||
podcastIncludes.push([Sequelize.literal(`(SELECT count(*) FROM podcastEpisodes pe LEFT OUTER JOIN mediaProgresses mp ON mp.mediaItemId = pe.id AND mp.userId = :userId WHERE pe.podcastId = podcast.id AND (mp.isFinished = 0 OR mp.isFinished IS NULL))`), 'numEpisodesIncomplete'])
|
|
||||||
}
|
|
||||||
|
|
||||||
let { mediaWhere, replacements } = this.getMediaGroupQuery(filterGroup, filterValue)
|
let { mediaWhere, replacements } = this.getMediaGroupQuery(filterGroup, filterValue)
|
||||||
replacements.userId = user.id
|
replacements.userId = user.id
|
||||||
@@ -153,12 +178,12 @@ module.exports = {
|
|||||||
replacements = { ...replacements, ...userPermissionPodcastWhere.replacements }
|
replacements = { ...replacements, ...userPermissionPodcastWhere.replacements }
|
||||||
podcastWhere.push(...userPermissionPodcastWhere.podcastWhere)
|
podcastWhere.push(...userPermissionPodcastWhere.podcastWhere)
|
||||||
|
|
||||||
const { rows: podcasts, count } = await Database.podcastModel.findAndCountAll({
|
const findOptions = {
|
||||||
where: podcastWhere,
|
where: podcastWhere,
|
||||||
replacements,
|
replacements,
|
||||||
distinct: true,
|
distinct: true,
|
||||||
attributes: {
|
attributes: {
|
||||||
include: [[Sequelize.literal(`(SELECT count(*) FROM podcastEpisodes pe WHERE pe.podcastId = podcast.id)`), 'numEpisodes'], ...podcastIncludes]
|
include: [...podcastIncludes]
|
||||||
},
|
},
|
||||||
include: [
|
include: [
|
||||||
{
|
{
|
||||||
@@ -169,10 +194,12 @@ module.exports = {
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
order: this.getOrder(sortBy, sortDesc),
|
order: this.getOrder(sortBy, sortDesc),
|
||||||
subQuery: false,
|
subQuery: false
|
||||||
limit: limit || null,
|
}
|
||||||
offset
|
|
||||||
})
|
const findAndCountAll = process.env.QUERY_PROFILING ? profile(this.findAndCountAll) : this.findAndCountAll
|
||||||
|
|
||||||
|
const { rows: podcasts, count } = await findAndCountAll(findOptions, Database.podcastModel, limit, offset)
|
||||||
|
|
||||||
const libraryItems = podcasts.map((podcastExpanded) => {
|
const libraryItems = podcasts.map((podcastExpanded) => {
|
||||||
const libraryItem = podcastExpanded.libraryItem
|
const libraryItem = podcastExpanded.libraryItem
|
||||||
@@ -183,11 +210,15 @@ module.exports = {
|
|||||||
if (libraryItem.feeds?.length) {
|
if (libraryItem.feeds?.length) {
|
||||||
libraryItem.rssFeed = libraryItem.feeds[0]
|
libraryItem.rssFeed = libraryItem.feeds[0]
|
||||||
}
|
}
|
||||||
if (podcast.dataValues.numEpisodesIncomplete) {
|
|
||||||
libraryItem.numEpisodesIncomplete = podcast.dataValues.numEpisodesIncomplete
|
if (includeNumEpisodesIncomplete) {
|
||||||
}
|
const numEpisodesComplete = user.mediaProgresses.reduce((acc, mp) => {
|
||||||
if (podcast.dataValues.numEpisodes) {
|
if (mp.podcastId === podcast.id && mp.isFinished) {
|
||||||
podcast.numEpisodes = podcast.dataValues.numEpisodes
|
acc += 1
|
||||||
|
}
|
||||||
|
return acc
|
||||||
|
}, 0)
|
||||||
|
libraryItem.numEpisodesIncomplete = podcast.numEpisodes - numEpisodesComplete
|
||||||
}
|
}
|
||||||
|
|
||||||
libraryItem.media = podcast
|
libraryItem.media = podcast
|
||||||
@@ -268,28 +299,31 @@ module.exports = {
|
|||||||
|
|
||||||
const userPermissionPodcastWhere = this.getUserPermissionPodcastWhereQuery(user)
|
const userPermissionPodcastWhere = this.getUserPermissionPodcastWhereQuery(user)
|
||||||
|
|
||||||
const { rows: podcastEpisodes, count } = await Database.podcastEpisodeModel.findAndCountAll({
|
const findOptions = {
|
||||||
where: podcastEpisodeWhere,
|
where: podcastEpisodeWhere,
|
||||||
replacements: userPermissionPodcastWhere.replacements,
|
replacements: userPermissionPodcastWhere.replacements,
|
||||||
include: [
|
include: [
|
||||||
{
|
{
|
||||||
model: Database.podcastModel,
|
model: Database.podcastModel,
|
||||||
|
required: true,
|
||||||
where: userPermissionPodcastWhere.podcastWhere,
|
where: userPermissionPodcastWhere.podcastWhere,
|
||||||
include: [
|
include: [
|
||||||
{
|
{
|
||||||
model: Database.libraryItemModel,
|
model: Database.libraryItemModel,
|
||||||
|
required: true,
|
||||||
where: libraryItemWhere
|
where: libraryItemWhere
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
...podcastEpisodeIncludes
|
...podcastEpisodeIncludes
|
||||||
],
|
],
|
||||||
distinct: true,
|
|
||||||
subQuery: false,
|
subQuery: false,
|
||||||
order: podcastEpisodeOrder,
|
order: podcastEpisodeOrder
|
||||||
limit,
|
}
|
||||||
offset
|
|
||||||
})
|
const findAndCountAll = process.env.QUERY_PROFILING ? profile(this.findAndCountAll) : this.findAndCountAll
|
||||||
|
|
||||||
|
const { rows: podcastEpisodes, count } = await findAndCountAll(findOptions, Database.podcastEpisodeModel, limit, offset)
|
||||||
|
|
||||||
const libraryItems = podcastEpisodes.map((ep) => {
|
const libraryItems = podcastEpisodes.map((ep) => {
|
||||||
const libraryItem = ep.podcast.libraryItem
|
const libraryItem = ep.podcast.libraryItem
|
||||||
@@ -321,8 +355,8 @@ module.exports = {
|
|||||||
|
|
||||||
const textSearchQuery = await Database.createTextSearchQuery(query)
|
const textSearchQuery = await Database.createTextSearchQuery(query)
|
||||||
|
|
||||||
const matchTitle = textSearchQuery.matchExpression('title')
|
const matchTitle = textSearchQuery.matchExpression('podcast.title')
|
||||||
const matchAuthor = textSearchQuery.matchExpression('author')
|
const matchAuthor = textSearchQuery.matchExpression('podcast.author')
|
||||||
|
|
||||||
// Search title, author, itunesId, itunesArtistId
|
// Search title, author, itunesId, itunesArtistId
|
||||||
const podcasts = await Database.podcastModel.findAll({
|
const podcasts = await Database.podcastModel.findAll({
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
function stringifySequelizeQuery(findOptions) {
|
||||||
|
function isClass(func) {
|
||||||
|
return typeof func === 'function' && /^class\s/.test(func.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
function replacer(key, value) {
|
||||||
|
if (typeof value === 'object' && value !== null) {
|
||||||
|
const symbols = Object.getOwnPropertySymbols(value).reduce((acc, sym) => {
|
||||||
|
acc[sym.toString()] = value[sym]
|
||||||
|
return acc
|
||||||
|
}, {})
|
||||||
|
|
||||||
|
return { ...value, ...symbols }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isClass(value)) {
|
||||||
|
return `${value.name}`
|
||||||
|
}
|
||||||
|
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
return JSON.stringify(findOptions, replacer)
|
||||||
|
}
|
||||||
|
module.exports = stringifySequelizeQuery
|
||||||
@@ -129,9 +129,9 @@ describe('migration-v2.15.0-series-column-unique', () => {
|
|||||||
{ id: series1Id, name: 'Series 1', libraryId: library1Id, createdAt: new Date(), updatedAt: new Date() },
|
{ id: series1Id, name: 'Series 1', libraryId: library1Id, createdAt: new Date(), updatedAt: new Date() },
|
||||||
{ id: series2Id, name: 'Series 2', libraryId: library2Id, createdAt: new Date(), updatedAt: new Date() },
|
{ id: series2Id, name: 'Series 2', libraryId: library2Id, createdAt: new Date(), updatedAt: new Date() },
|
||||||
{ id: series3Id, name: 'Series 3', libraryId: library1Id, createdAt: new Date(), updatedAt: new Date() },
|
{ id: series3Id, name: 'Series 3', libraryId: library1Id, createdAt: new Date(), updatedAt: new Date() },
|
||||||
{ id: series1Id_dup, name: 'Series 1', libraryId: library1Id, createdAt: new Date(), updatedAt: new Date() },
|
{ id: series1Id_dup, name: 'Series 1', libraryId: library1Id, createdAt: new Date(0), updatedAt: new Date(0) },
|
||||||
{ id: series3Id_dup, name: 'Series 3', libraryId: library1Id, createdAt: new Date(), updatedAt: new Date() },
|
{ id: series3Id_dup, name: 'Series 3', libraryId: library1Id, createdAt: new Date(0), updatedAt: new Date(0) },
|
||||||
{ id: series1Id_dup2, name: 'Series 1', libraryId: library1Id, createdAt: new Date(), updatedAt: new Date() }
|
{ id: series1Id_dup2, name: 'Series 1', libraryId: library1Id, createdAt: new Date(0), updatedAt: new Date(0) }
|
||||||
])
|
])
|
||||||
// Add some entries to the BookSeries table
|
// Add some entries to the BookSeries table
|
||||||
await queryInterface.bulkInsert('BookSeries', [
|
await queryInterface.bulkInsert('BookSeries', [
|
||||||
|
|||||||
@@ -0,0 +1,148 @@
|
|||||||
|
const chai = require('chai')
|
||||||
|
const sinon = require('sinon')
|
||||||
|
const { expect } = chai
|
||||||
|
|
||||||
|
const { DataTypes, Sequelize } = require('sequelize')
|
||||||
|
const Logger = require('../../../server/Logger')
|
||||||
|
|
||||||
|
const { up, down } = require('../../../server/migrations/v2.19.1-copy-title-to-library-items')
|
||||||
|
|
||||||
|
describe('Migration v2.19.1-copy-title-to-library-items', () => {
|
||||||
|
let sequelize
|
||||||
|
let queryInterface
|
||||||
|
let loggerInfoStub
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
sequelize = new Sequelize({ dialect: 'sqlite', storage: ':memory:', logging: false })
|
||||||
|
queryInterface = sequelize.getQueryInterface()
|
||||||
|
loggerInfoStub = sinon.stub(Logger, 'info')
|
||||||
|
|
||||||
|
await queryInterface.createTable('books', {
|
||||||
|
id: { type: DataTypes.INTEGER, allowNull: false, primaryKey: true, unique: true },
|
||||||
|
title: { type: DataTypes.STRING, allowNull: true },
|
||||||
|
titleIgnorePrefix: { type: DataTypes.STRING, allowNull: true }
|
||||||
|
})
|
||||||
|
|
||||||
|
await queryInterface.createTable('libraryItems', {
|
||||||
|
id: { type: DataTypes.INTEGER, allowNull: false, primaryKey: true, unique: true },
|
||||||
|
libraryId: { type: DataTypes.INTEGER, allowNull: false },
|
||||||
|
mediaType: { type: DataTypes.STRING, allowNull: false },
|
||||||
|
mediaId: { type: DataTypes.INTEGER, allowNull: false },
|
||||||
|
createdAt: { type: DataTypes.DATE, allowNull: false }
|
||||||
|
})
|
||||||
|
|
||||||
|
await queryInterface.bulkInsert('books', [
|
||||||
|
{ id: 1, title: 'The Book 1', titleIgnorePrefix: 'Book 1, The' },
|
||||||
|
{ id: 2, title: 'Book 2', titleIgnorePrefix: 'Book 2' }
|
||||||
|
])
|
||||||
|
|
||||||
|
await queryInterface.bulkInsert('libraryItems', [
|
||||||
|
{ id: 1, libraryId: 1, mediaType: 'book', mediaId: 1, createdAt: '2025-01-01 00:00:00.000 +00:00' },
|
||||||
|
{ id: 2, libraryId: 2, mediaType: 'book', mediaId: 2, createdAt: '2025-01-02 00:00:00.000 +00:00' }
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
sinon.restore()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('up', () => {
|
||||||
|
it('should copy title and titleIgnorePrefix to libraryItems', async () => {
|
||||||
|
await up({ context: { queryInterface, logger: Logger } })
|
||||||
|
|
||||||
|
const [libraryItems] = await queryInterface.sequelize.query('SELECT * FROM libraryItems')
|
||||||
|
expect(libraryItems).to.deep.equal([
|
||||||
|
{ id: 1, libraryId: 1, mediaType: 'book', mediaId: 1, title: 'The Book 1', titleIgnorePrefix: 'Book 1, The', createdAt: '2025-01-01 00:00:00.000 +00:00' },
|
||||||
|
{ id: 2, libraryId: 2, mediaType: 'book', mediaId: 2, title: 'Book 2', titleIgnorePrefix: 'Book 2', createdAt: '2025-01-02 00:00:00.000 +00:00' }
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should add index on title to libraryItems', async () => {
|
||||||
|
await up({ context: { queryInterface, logger: Logger } })
|
||||||
|
|
||||||
|
const [[{ count }]] = await queryInterface.sequelize.query(`SELECT COUNT(*) as count FROM sqlite_master WHERE type='index' AND name='library_items_library_id_media_type_title_ignore_prefix'`)
|
||||||
|
expect(count).to.equal(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should add trigger to books.title to update libraryItems.title', async () => {
|
||||||
|
await up({ context: { queryInterface, logger: Logger } })
|
||||||
|
|
||||||
|
const [[{ count }]] = await queryInterface.sequelize.query(`SELECT COUNT(*) as count FROM sqlite_master WHERE type='trigger' AND name='update_library_items_title'`)
|
||||||
|
expect(count).to.equal(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should add index on titleIgnorePrefix to libraryItems', async () => {
|
||||||
|
await up({ context: { queryInterface, logger: Logger } })
|
||||||
|
|
||||||
|
const [[{ count }]] = await queryInterface.sequelize.query(`SELECT COUNT(*) as count FROM sqlite_master WHERE type='index' AND name='library_items_library_id_media_type_title_ignore_prefix'`)
|
||||||
|
expect(count).to.equal(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should add trigger to books.titleIgnorePrefix to update libraryItems.titleIgnorePrefix', async () => {
|
||||||
|
await up({ context: { queryInterface, logger: Logger } })
|
||||||
|
|
||||||
|
const [[{ count }]] = await queryInterface.sequelize.query(`SELECT COUNT(*) as count FROM sqlite_master WHERE type='trigger' AND name='update_library_items_title_ignore_prefix'`)
|
||||||
|
expect(count).to.equal(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should add index on createdAt to libraryItems', async () => {
|
||||||
|
await up({ context: { queryInterface, logger: Logger } })
|
||||||
|
|
||||||
|
const [[{ count }]] = await queryInterface.sequelize.query(`SELECT COUNT(*) as count FROM sqlite_master WHERE type='index' AND name='library_items_library_id_media_type_created_at'`)
|
||||||
|
expect(count).to.equal(1)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('down', () => {
|
||||||
|
it('should remove title and titleIgnorePrefix from libraryItems', async () => {
|
||||||
|
await up({ context: { queryInterface, logger: Logger } })
|
||||||
|
await down({ context: { queryInterface, logger: Logger } })
|
||||||
|
|
||||||
|
const [libraryItems] = await queryInterface.sequelize.query('SELECT * FROM libraryItems')
|
||||||
|
expect(libraryItems).to.deep.equal([
|
||||||
|
{ id: 1, libraryId: 1, mediaType: 'book', mediaId: 1, createdAt: '2025-01-01 00:00:00.000 +00:00' },
|
||||||
|
{ id: 2, libraryId: 2, mediaType: 'book', mediaId: 2, createdAt: '2025-01-02 00:00:00.000 +00:00' }
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should remove title trigger from books', async () => {
|
||||||
|
await up({ context: { queryInterface, logger: Logger } })
|
||||||
|
await down({ context: { queryInterface, logger: Logger } })
|
||||||
|
|
||||||
|
const [[{ count }]] = await queryInterface.sequelize.query(`SELECT COUNT(*) as count FROM sqlite_master WHERE type='trigger' AND name='update_library_items_title'`)
|
||||||
|
expect(count).to.equal(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should remove titleIgnorePrefix trigger from books', async () => {
|
||||||
|
await up({ context: { queryInterface, logger: Logger } })
|
||||||
|
await down({ context: { queryInterface, logger: Logger } })
|
||||||
|
|
||||||
|
const [[{ count }]] = await queryInterface.sequelize.query(`SELECT COUNT(*) as count FROM sqlite_master WHERE type='trigger' AND name='update_library_items_title_ignore_prefix'`)
|
||||||
|
expect(count).to.equal(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should remove index on titleIgnorePrefix from libraryItems', async () => {
|
||||||
|
await up({ context: { queryInterface, logger: Logger } })
|
||||||
|
await down({ context: { queryInterface, logger: Logger } })
|
||||||
|
|
||||||
|
const [[{ count }]] = await queryInterface.sequelize.query(`SELECT COUNT(*) as count FROM sqlite_master WHERE type='index' AND name='library_items_library_id_media_type_title_ignore_prefix'`)
|
||||||
|
expect(count).to.equal(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should remove index on title from libraryItems', async () => {
|
||||||
|
await up({ context: { queryInterface, logger: Logger } })
|
||||||
|
await down({ context: { queryInterface, logger: Logger } })
|
||||||
|
|
||||||
|
const [[{ count }]] = await queryInterface.sequelize.query(`SELECT COUNT(*) as count FROM sqlite_master WHERE type='index' AND name='library_items_library_id_media_type_title'`)
|
||||||
|
expect(count).to.equal(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should remove index on createdAt from libraryItems', async () => {
|
||||||
|
await up({ context: { queryInterface, logger: Logger } })
|
||||||
|
await down({ context: { queryInterface, logger: Logger } })
|
||||||
|
|
||||||
|
const [[{ count }]] = await queryInterface.sequelize.query(`SELECT COUNT(*) as count FROM sqlite_master WHERE type='index' AND name='library_items_library_id_media_type_created_at'`)
|
||||||
|
expect(count).to.equal(0)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,265 @@
|
|||||||
|
const chai = require('chai')
|
||||||
|
const sinon = require('sinon')
|
||||||
|
const { expect } = chai
|
||||||
|
|
||||||
|
const { DataTypes, Sequelize } = require('sequelize')
|
||||||
|
const Logger = require('../../../server/Logger')
|
||||||
|
|
||||||
|
const { up, down } = require('../../../server/migrations/v2.19.4-improve-podcast-queries')
|
||||||
|
|
||||||
|
describe('Migration v2.19.4-improve-podcast-queries', () => {
|
||||||
|
let sequelize
|
||||||
|
let queryInterface
|
||||||
|
let loggerInfoStub
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
sequelize = new Sequelize({ dialect: 'sqlite', storage: ':memory:', logging: false })
|
||||||
|
queryInterface = sequelize.getQueryInterface()
|
||||||
|
loggerInfoStub = sinon.stub(Logger, 'info')
|
||||||
|
|
||||||
|
await queryInterface.createTable('libraryItems', {
|
||||||
|
id: { type: DataTypes.INTEGER, allowNull: false, primaryKey: true, unique: true },
|
||||||
|
mediaId: { type: DataTypes.INTEGER, allowNull: false },
|
||||||
|
title: { type: DataTypes.STRING, allowNull: true },
|
||||||
|
titleIgnorePrefix: { type: DataTypes.STRING, allowNull: true }
|
||||||
|
})
|
||||||
|
await queryInterface.createTable('podcasts', {
|
||||||
|
id: { type: DataTypes.INTEGER, allowNull: false, primaryKey: true, unique: true },
|
||||||
|
title: { type: DataTypes.STRING, allowNull: false },
|
||||||
|
titleIgnorePrefix: { type: DataTypes.STRING, allowNull: false }
|
||||||
|
})
|
||||||
|
|
||||||
|
await queryInterface.createTable('podcastEpisodes', {
|
||||||
|
id: { type: DataTypes.INTEGER, allowNull: false, primaryKey: true, unique: true },
|
||||||
|
podcastId: { type: DataTypes.INTEGER, allowNull: false, references: { model: 'podcasts', key: 'id', onDelete: 'CASCADE' } }
|
||||||
|
})
|
||||||
|
|
||||||
|
await queryInterface.createTable('mediaProgresses', {
|
||||||
|
id: { type: DataTypes.INTEGER, allowNull: false, primaryKey: true, unique: true },
|
||||||
|
userId: { type: DataTypes.INTEGER, allowNull: false },
|
||||||
|
mediaItemId: { type: DataTypes.INTEGER, allowNull: false },
|
||||||
|
mediaItemType: { type: DataTypes.STRING, allowNull: false },
|
||||||
|
isFinished: { type: DataTypes.BOOLEAN, allowNull: false, defaultValue: false }
|
||||||
|
})
|
||||||
|
|
||||||
|
await queryInterface.bulkInsert('libraryItems', [
|
||||||
|
{ id: 1, mediaId: 1, title: null, titleIgnorePrefix: null },
|
||||||
|
{ id: 2, mediaId: 2, title: null, titleIgnorePrefix: null }
|
||||||
|
])
|
||||||
|
|
||||||
|
await queryInterface.bulkInsert('podcasts', [
|
||||||
|
{ id: 1, title: 'The Podcast 1', titleIgnorePrefix: 'Podcast 1, The' },
|
||||||
|
{ id: 2, title: 'The Podcast 2', titleIgnorePrefix: 'Podcast 2, The' }
|
||||||
|
])
|
||||||
|
|
||||||
|
await queryInterface.bulkInsert('podcastEpisodes', [
|
||||||
|
{ id: 1, podcastId: 1 },
|
||||||
|
{ id: 2, podcastId: 1 },
|
||||||
|
{ id: 3, podcastId: 2 }
|
||||||
|
])
|
||||||
|
|
||||||
|
await queryInterface.bulkInsert('mediaProgresses', [
|
||||||
|
{ id: 1, userId: 1, mediaItemId: 1, mediaItemType: 'podcastEpisode', isFinished: 1 },
|
||||||
|
{ id: 2, userId: 1, mediaItemId: 2, mediaItemType: 'podcastEpisode', isFinished: 0 },
|
||||||
|
{ id: 3, userId: 1, mediaItemId: 3, mediaItemType: 'podcastEpisode', isFinished: 1 },
|
||||||
|
{ id: 4, userId: 2, mediaItemId: 1, mediaItemType: 'podcastEpisode', isFinished: 0 },
|
||||||
|
{ id: 5, userId: 2, mediaItemId: 2, mediaItemType: 'podcastEpisode', isFinished: 1 },
|
||||||
|
{ id: 6, userId: 2, mediaItemId: 3, mediaItemType: 'podcastEpisode', isFinished: 0 },
|
||||||
|
{ id: 7, userId: 1, mediaItemId: 1, mediaItemType: 'book', isFinished: 1 },
|
||||||
|
{ id: 8, userId: 1, mediaItemId: 2, mediaItemType: 'book', isFinished: 0 }
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
sinon.restore()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('up', () => {
|
||||||
|
it('should add numEpisodes column to podcasts', async () => {
|
||||||
|
await up({ context: { queryInterface, logger: Logger } })
|
||||||
|
|
||||||
|
const [podcasts] = await queryInterface.sequelize.query('SELECT * FROM podcasts')
|
||||||
|
expect(podcasts).to.deep.equal([
|
||||||
|
{ id: 1, numEpisodes: 2, title: 'The Podcast 1', titleIgnorePrefix: 'Podcast 1, The' },
|
||||||
|
{ id: 2, numEpisodes: 1, title: 'The Podcast 2', titleIgnorePrefix: 'Podcast 2, The' }
|
||||||
|
])
|
||||||
|
|
||||||
|
// Make sure podcastEpisodes are not affected due to ON DELETE CASCADE
|
||||||
|
const [podcastEpisodes] = await queryInterface.sequelize.query('SELECT * FROM podcastEpisodes')
|
||||||
|
expect(podcastEpisodes).to.deep.equal([
|
||||||
|
{ id: 1, podcastId: 1 },
|
||||||
|
{ id: 2, podcastId: 1 },
|
||||||
|
{ id: 3, podcastId: 2 }
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should add podcastId column to mediaProgresses', async () => {
|
||||||
|
await up({ context: { queryInterface, logger: Logger } })
|
||||||
|
|
||||||
|
const [mediaProgresses] = await queryInterface.sequelize.query('SELECT * FROM mediaProgresses')
|
||||||
|
expect(mediaProgresses).to.deep.equal([
|
||||||
|
{ id: 1, userId: 1, mediaItemId: 1, mediaItemType: 'podcastEpisode', podcastId: 1, isFinished: 1 },
|
||||||
|
{ id: 2, userId: 1, mediaItemId: 2, mediaItemType: 'podcastEpisode', podcastId: 1, isFinished: 0 },
|
||||||
|
{ id: 3, userId: 1, mediaItemId: 3, mediaItemType: 'podcastEpisode', podcastId: 2, isFinished: 1 },
|
||||||
|
{ id: 4, userId: 2, mediaItemId: 1, mediaItemType: 'podcastEpisode', podcastId: 1, isFinished: 0 },
|
||||||
|
{ id: 5, userId: 2, mediaItemId: 2, mediaItemType: 'podcastEpisode', podcastId: 1, isFinished: 1 },
|
||||||
|
{ id: 6, userId: 2, mediaItemId: 3, mediaItemType: 'podcastEpisode', podcastId: 2, isFinished: 0 },
|
||||||
|
{ id: 7, userId: 1, mediaItemId: 1, mediaItemType: 'book', podcastId: null, isFinished: 1 },
|
||||||
|
{ id: 8, userId: 1, mediaItemId: 2, mediaItemType: 'book', podcastId: null, isFinished: 0 }
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should copy title and titleIgnorePrefix from podcasts to libraryItems', async () => {
|
||||||
|
await up({ context: { queryInterface, logger: Logger } })
|
||||||
|
|
||||||
|
const [libraryItems] = await queryInterface.sequelize.query('SELECT * FROM libraryItems')
|
||||||
|
expect(libraryItems).to.deep.equal([
|
||||||
|
{ id: 1, mediaId: 1, title: 'The Podcast 1', titleIgnorePrefix: 'Podcast 1, The' },
|
||||||
|
{ id: 2, mediaId: 2, title: 'The Podcast 2', titleIgnorePrefix: 'Podcast 2, The' }
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should add trigger to update title in libraryItems', async () => {
|
||||||
|
await up({ context: { queryInterface, logger: Logger } })
|
||||||
|
|
||||||
|
const [[{ count }]] = await queryInterface.sequelize.query(`SELECT COUNT(*) as count FROM sqlite_master WHERE type='trigger' AND name='update_library_items_title_from_podcasts_title'`)
|
||||||
|
expect(count).to.equal(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should add trigger to update titleIgnorePrefix in libraryItems', async () => {
|
||||||
|
await up({ context: { queryInterface, logger: Logger } })
|
||||||
|
|
||||||
|
const [[{ count }]] = await queryInterface.sequelize.query(`SELECT COUNT(*) as count FROM sqlite_master WHERE type='trigger' AND name='update_library_items_title_ignore_prefix_from_podcasts_title_ignore_prefix'`)
|
||||||
|
expect(count).to.equal(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should be idempotent', async () => {
|
||||||
|
await up({ context: { queryInterface, logger: Logger } })
|
||||||
|
await up({ context: { queryInterface, logger: Logger } })
|
||||||
|
|
||||||
|
const [podcasts] = await queryInterface.sequelize.query('SELECT * FROM podcasts')
|
||||||
|
expect(podcasts).to.deep.equal([
|
||||||
|
{ id: 1, numEpisodes: 2, title: 'The Podcast 1', titleIgnorePrefix: 'Podcast 1, The' },
|
||||||
|
{ id: 2, numEpisodes: 1, title: 'The Podcast 2', titleIgnorePrefix: 'Podcast 2, The' }
|
||||||
|
])
|
||||||
|
|
||||||
|
const [mediaProgresses] = await queryInterface.sequelize.query('SELECT * FROM mediaProgresses')
|
||||||
|
expect(mediaProgresses).to.deep.equal([
|
||||||
|
{ id: 1, userId: 1, mediaItemId: 1, mediaItemType: 'podcastEpisode', podcastId: 1, isFinished: 1 },
|
||||||
|
{ id: 2, userId: 1, mediaItemId: 2, mediaItemType: 'podcastEpisode', podcastId: 1, isFinished: 0 },
|
||||||
|
{ id: 3, userId: 1, mediaItemId: 3, mediaItemType: 'podcastEpisode', podcastId: 2, isFinished: 1 },
|
||||||
|
{ id: 4, userId: 2, mediaItemId: 1, mediaItemType: 'podcastEpisode', podcastId: 1, isFinished: 0 },
|
||||||
|
{ id: 5, userId: 2, mediaItemId: 2, mediaItemType: 'podcastEpisode', podcastId: 1, isFinished: 1 },
|
||||||
|
{ id: 6, userId: 2, mediaItemId: 3, mediaItemType: 'podcastEpisode', podcastId: 2, isFinished: 0 },
|
||||||
|
{ id: 7, userId: 1, mediaItemId: 1, mediaItemType: 'book', podcastId: null, isFinished: 1 },
|
||||||
|
{ id: 8, userId: 1, mediaItemId: 2, mediaItemType: 'book', podcastId: null, isFinished: 0 }
|
||||||
|
])
|
||||||
|
|
||||||
|
const [libraryItems] = await queryInterface.sequelize.query('SELECT * FROM libraryItems')
|
||||||
|
expect(libraryItems).to.deep.equal([
|
||||||
|
{ id: 1, mediaId: 1, title: 'The Podcast 1', titleIgnorePrefix: 'Podcast 1, The' },
|
||||||
|
{ id: 2, mediaId: 2, title: 'The Podcast 2', titleIgnorePrefix: 'Podcast 2, The' }
|
||||||
|
])
|
||||||
|
|
||||||
|
const [[{ count: count1 }]] = await queryInterface.sequelize.query(`SELECT COUNT(*) as count FROM sqlite_master WHERE type='trigger' AND name='update_library_items_title_from_podcasts_title'`)
|
||||||
|
expect(count1).to.equal(1)
|
||||||
|
|
||||||
|
const [[{ count: count2 }]] = await queryInterface.sequelize.query(`SELECT COUNT(*) as count FROM sqlite_master WHERE type='trigger' AND name='update_library_items_title_ignore_prefix_from_podcasts_title_ignore_prefix'`)
|
||||||
|
expect(count2).to.equal(1)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('down', () => {
|
||||||
|
it('should remove numEpisodes column from podcasts', async () => {
|
||||||
|
await up({ context: { queryInterface, logger: Logger } })
|
||||||
|
try {
|
||||||
|
await down({ context: { queryInterface, logger: Logger } })
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error)
|
||||||
|
}
|
||||||
|
|
||||||
|
const [podcasts] = await queryInterface.sequelize.query('SELECT * FROM podcasts')
|
||||||
|
expect(podcasts).to.deep.equal([
|
||||||
|
{ id: 1, title: 'The Podcast 1', titleIgnorePrefix: 'Podcast 1, The' },
|
||||||
|
{ id: 2, title: 'The Podcast 2', titleIgnorePrefix: 'Podcast 2, The' }
|
||||||
|
])
|
||||||
|
|
||||||
|
// Make sure podcastEpisodes are not affected due to ON DELETE CASCADE
|
||||||
|
const [podcastEpisodes] = await queryInterface.sequelize.query('SELECT * FROM podcastEpisodes')
|
||||||
|
expect(podcastEpisodes).to.deep.equal([
|
||||||
|
{ id: 1, podcastId: 1 },
|
||||||
|
{ id: 2, podcastId: 1 },
|
||||||
|
{ id: 3, podcastId: 2 }
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should remove podcastId column from mediaProgresses', async () => {
|
||||||
|
await up({ context: { queryInterface, logger: Logger } })
|
||||||
|
await down({ context: { queryInterface, logger: Logger } })
|
||||||
|
|
||||||
|
const [mediaProgresses] = await queryInterface.sequelize.query('SELECT * FROM mediaProgresses')
|
||||||
|
expect(mediaProgresses).to.deep.equal([
|
||||||
|
{ id: 1, userId: 1, mediaItemId: 1, mediaItemType: 'podcastEpisode', isFinished: 1 },
|
||||||
|
{ id: 2, userId: 1, mediaItemId: 2, mediaItemType: 'podcastEpisode', isFinished: 0 },
|
||||||
|
{ id: 3, userId: 1, mediaItemId: 3, mediaItemType: 'podcastEpisode', isFinished: 1 },
|
||||||
|
{ id: 4, userId: 2, mediaItemId: 1, mediaItemType: 'podcastEpisode', isFinished: 0 },
|
||||||
|
{ id: 5, userId: 2, mediaItemId: 2, mediaItemType: 'podcastEpisode', isFinished: 1 },
|
||||||
|
{ id: 6, userId: 2, mediaItemId: 3, mediaItemType: 'podcastEpisode', isFinished: 0 },
|
||||||
|
{ id: 7, userId: 1, mediaItemId: 1, mediaItemType: 'book', isFinished: 1 },
|
||||||
|
{ id: 8, userId: 1, mediaItemId: 2, mediaItemType: 'book', isFinished: 0 }
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should remove trigger to update title in libraryItems', async () => {
|
||||||
|
await up({ context: { queryInterface, logger: Logger } })
|
||||||
|
await down({ context: { queryInterface, logger: Logger } })
|
||||||
|
|
||||||
|
const [[{ count }]] = await queryInterface.sequelize.query(`SELECT COUNT(*) as count FROM sqlite_master WHERE type='trigger' AND name='update_library_items_title_from_podcasts_title'`)
|
||||||
|
expect(count).to.equal(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should remove trigger to update titleIgnorePrefix in libraryItems', async () => {
|
||||||
|
await up({ context: { queryInterface, logger: Logger } })
|
||||||
|
await down({ context: { queryInterface, logger: Logger } })
|
||||||
|
|
||||||
|
const [[{ count }]] = await queryInterface.sequelize.query(`SELECT COUNT(*) as count FROM sqlite_master WHERE type='trigger' AND name='update_library_items_title_ignore_prefix_from_podcasts_title_ignore_prefix'`)
|
||||||
|
expect(count).to.equal(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should be idempotent', async () => {
|
||||||
|
await up({ context: { queryInterface, logger: Logger } })
|
||||||
|
await down({ context: { queryInterface, logger: Logger } })
|
||||||
|
await down({ context: { queryInterface, logger: Logger } })
|
||||||
|
|
||||||
|
const [podcasts] = await queryInterface.sequelize.query('SELECT * FROM podcasts')
|
||||||
|
expect(podcasts).to.deep.equal([
|
||||||
|
{ id: 1, title: 'The Podcast 1', titleIgnorePrefix: 'Podcast 1, The' },
|
||||||
|
{ id: 2, title: 'The Podcast 2', titleIgnorePrefix: 'Podcast 2, The' }
|
||||||
|
])
|
||||||
|
|
||||||
|
const [mediaProgresses] = await queryInterface.sequelize.query('SELECT * FROM mediaProgresses')
|
||||||
|
expect(mediaProgresses).to.deep.equal([
|
||||||
|
{ id: 1, userId: 1, mediaItemId: 1, mediaItemType: 'podcastEpisode', isFinished: 1 },
|
||||||
|
{ id: 2, userId: 1, mediaItemId: 2, mediaItemType: 'podcastEpisode', isFinished: 0 },
|
||||||
|
{ id: 3, userId: 1, mediaItemId: 3, mediaItemType: 'podcastEpisode', isFinished: 1 },
|
||||||
|
{ id: 4, userId: 2, mediaItemId: 1, mediaItemType: 'podcastEpisode', isFinished: 0 },
|
||||||
|
{ id: 5, userId: 2, mediaItemId: 2, mediaItemType: 'podcastEpisode', isFinished: 1 },
|
||||||
|
{ id: 6, userId: 2, mediaItemId: 3, mediaItemType: 'podcastEpisode', isFinished: 0 },
|
||||||
|
{ id: 7, userId: 1, mediaItemId: 1, mediaItemType: 'book', isFinished: 1 },
|
||||||
|
{ id: 8, userId: 1, mediaItemId: 2, mediaItemType: 'book', isFinished: 0 }
|
||||||
|
])
|
||||||
|
|
||||||
|
const [libraryItems] = await queryInterface.sequelize.query('SELECT * FROM libraryItems')
|
||||||
|
expect(libraryItems).to.deep.equal([
|
||||||
|
{ id: 1, mediaId: 1, title: 'The Podcast 1', titleIgnorePrefix: 'Podcast 1, The' },
|
||||||
|
{ id: 2, mediaId: 2, title: 'The Podcast 2', titleIgnorePrefix: 'Podcast 2, The' }
|
||||||
|
])
|
||||||
|
|
||||||
|
const [[{ count: count1 }]] = await queryInterface.sequelize.query(`SELECT COUNT(*) as count FROM sqlite_master WHERE type='trigger' AND name='update_library_items_title_from_podcasts_title'`)
|
||||||
|
expect(count1).to.equal(0)
|
||||||
|
|
||||||
|
const [[{ count: count2 }]] = await queryInterface.sequelize.query(`SELECT COUNT(*) as count FROM sqlite_master WHERE type='trigger' AND name='update_library_items_title_ignore_prefix_from_podcasts_title_ignore_prefix'`)
|
||||||
|
expect(count2).to.equal(0)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
const { expect } = require('chai')
|
||||||
|
const stringifySequelizeQuery = require('../../../server/utils/stringifySequelizeQuery')
|
||||||
|
const Sequelize = require('sequelize')
|
||||||
|
|
||||||
|
class DummyClass {}
|
||||||
|
|
||||||
|
describe('stringifySequelizeQuery', () => {
|
||||||
|
it('should stringify a sequelize query containing an op', () => {
|
||||||
|
const query = {
|
||||||
|
where: {
|
||||||
|
name: 'John',
|
||||||
|
age: {
|
||||||
|
[Sequelize.Op.gt]: 20
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = stringifySequelizeQuery(query)
|
||||||
|
expect(result).to.equal('{"where":{"name":"John","age":{"Symbol(gt)":20}}}')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should stringify a sequelize query containing a literal', () => {
|
||||||
|
const query = {
|
||||||
|
order: [[Sequelize.literal('libraryItem.title'), 'ASC']]
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = stringifySequelizeQuery(query)
|
||||||
|
expect(result).to.equal('{"order":{"0":{"0":{"val":"libraryItem.title"},"1":"ASC"}}}')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should stringify a sequelize query containing a class', () => {
|
||||||
|
const query = {
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: DummyClass
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = stringifySequelizeQuery(query)
|
||||||
|
expect(result).to.equal('{"include":{"0":{"model":"DummyClass"}}}')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should ignore non-class functions', () => {
|
||||||
|
const query = {
|
||||||
|
logging: (query) => console.log(query)
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = stringifySequelizeQuery(query)
|
||||||
|
expect(result).to.equal('{}')
|
||||||
|
})
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user