mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2026-07-10 23:02:50 +02:00
Compare commits
37 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4b52f31d58 | |||
| 70f466d03c | |||
| ef82e8b0d0 | |||
| c643d4cec8 | |||
| 718d8b5999 | |||
| 2ba0f9157d | |||
| 53fdb5273c | |||
| fabdfd5517 | |||
| 950993f652 | |||
| 5a968b002a | |||
| 3acd29fab3 | |||
| 315b21db00 | |||
| f9aaeb3a34 | |||
| d19bb909b3 | |||
| f850db23fe | |||
| 5f81010f6a | |||
| daf2493f50 | |||
| 57222f3611 | |||
| 62b185979e | |||
| ebcc85acc4 | |||
| 33a7ba4acd | |||
| 1d4e6993fc | |||
| 784b761629 | |||
| 268fb2ce9a | |||
| fc5f35b388 | |||
| ff026a06bb | |||
| b148a57c98 | |||
| ee6e2d2983 | |||
| ea3a6fd75e | |||
| 22f85d3af9 | |||
| 75f4c2ee99 | |||
| dd3467efa2 | |||
| 4adb15c11b | |||
| a5e38d1473 | |||
| 778256ca16 | |||
| d5fbc1d455 | |||
| 0d54b57151 |
@@ -0,0 +1,33 @@
|
|||||||
|
<!--
|
||||||
|
For Work In Progress Pull Requests, please use the Draft PR feature,
|
||||||
|
see https://github.blog/2019-02-14-introducing-draft-pull-requests/ for further details.
|
||||||
|
|
||||||
|
If you do not follow this template, the PR may be closed without review.
|
||||||
|
|
||||||
|
Please ensure all checks pass.
|
||||||
|
If you are a new contributor, the workflows will need to be manually approved before they run.
|
||||||
|
-->
|
||||||
|
|
||||||
|
## Brief summary
|
||||||
|
|
||||||
|
<!-- Please provide a brief summary of what your PR attempts to achieve. -->
|
||||||
|
|
||||||
|
## Which issue is fixed?
|
||||||
|
|
||||||
|
<!-- Which issue number does this PR fix? Ex: "Fixes #1234" -->
|
||||||
|
|
||||||
|
## In-depth Description
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Describe your solution in more depth.
|
||||||
|
How does it work? Why is this the best solution?
|
||||||
|
Does it solve a problem that affects multiple users or is this an edge case for your setup?
|
||||||
|
-->
|
||||||
|
|
||||||
|
## How have you tested this?
|
||||||
|
|
||||||
|
<!-- Please describe in detail with reproducible steps how you tested your changes. -->
|
||||||
|
|
||||||
|
## Screenshots
|
||||||
|
|
||||||
|
<!-- If your PR includes any changes to the web client, please include screenshots or a short video from before and after your changes. -->
|
||||||
@@ -53,7 +53,6 @@
|
|||||||
@showBookmarks="showBookmarks"
|
@showBookmarks="showBookmarks"
|
||||||
@showSleepTimer="showSleepTimerModal = true"
|
@showSleepTimer="showSleepTimerModal = true"
|
||||||
@showPlayerQueueItems="showPlayerQueueItemsModal = true"
|
@showPlayerQueueItems="showPlayerQueueItemsModal = true"
|
||||||
@showPlayerSettings="showPlayerSettingsModal = true"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<modals-bookmarks-modal v-model="showBookmarksModal" :bookmarks="bookmarks" :current-time="bookmarkCurrentTime" :library-item-id="libraryItemId" @select="selectBookmark" />
|
<modals-bookmarks-modal v-model="showBookmarksModal" :bookmarks="bookmarks" :current-time="bookmarkCurrentTime" :library-item-id="libraryItemId" @select="selectBookmark" />
|
||||||
@@ -61,8 +60,6 @@
|
|||||||
<modals-sleep-timer-modal v-model="showSleepTimerModal" :timer-set="sleepTimerSet" :timer-type="sleepTimerType" :remaining="sleepTimerRemaining" :has-chapters="!!chapters.length" @set="setSleepTimer" @cancel="cancelSleepTimer" @increment="incrementSleepTimer" @decrement="decrementSleepTimer" />
|
<modals-sleep-timer-modal v-model="showSleepTimerModal" :timer-set="sleepTimerSet" :timer-type="sleepTimerType" :remaining="sleepTimerRemaining" :has-chapters="!!chapters.length" @set="setSleepTimer" @cancel="cancelSleepTimer" @increment="incrementSleepTimer" @decrement="decrementSleepTimer" />
|
||||||
|
|
||||||
<modals-player-queue-items-modal v-model="showPlayerQueueItemsModal" />
|
<modals-player-queue-items-modal v-model="showPlayerQueueItemsModal" />
|
||||||
|
|
||||||
<modals-player-settings-modal v-model="showPlayerSettingsModal" />
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -81,7 +78,6 @@ export default {
|
|||||||
currentTime: 0,
|
currentTime: 0,
|
||||||
showSleepTimerModal: false,
|
showSleepTimerModal: false,
|
||||||
showPlayerQueueItemsModal: false,
|
showPlayerQueueItemsModal: false,
|
||||||
showPlayerSettingsModal: false,
|
|
||||||
sleepTimerSet: false,
|
sleepTimerSet: false,
|
||||||
sleepTimerRemaining: 0,
|
sleepTimerRemaining: 0,
|
||||||
sleepTimerType: null,
|
sleepTimerType: null,
|
||||||
|
|||||||
@@ -59,12 +59,19 @@ export default {
|
|||||||
setJumpBackwardAmount(val) {
|
setJumpBackwardAmount(val) {
|
||||||
this.jumpBackwardAmount = val
|
this.jumpBackwardAmount = val
|
||||||
this.$store.dispatch('user/updateUserSettings', { jumpBackwardAmount: val })
|
this.$store.dispatch('user/updateUserSettings', { jumpBackwardAmount: val })
|
||||||
|
},
|
||||||
|
settingsUpdated() {
|
||||||
|
this.useChapterTrack = this.$store.getters['user/getUserSetting']('useChapterTrack')
|
||||||
|
this.jumpForwardAmount = this.$store.getters['user/getUserSetting']('jumpForwardAmount')
|
||||||
|
this.jumpBackwardAmount = this.$store.getters['user/getUserSetting']('jumpBackwardAmount')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.useChapterTrack = this.$store.getters['user/getUserSetting']('useChapterTrack')
|
this.settingsUpdated()
|
||||||
this.jumpForwardAmount = this.$store.getters['user/getUserSetting']('jumpForwardAmount')
|
this.$eventBus.$on('user-settings', this.settingsUpdated)
|
||||||
this.jumpBackwardAmount = this.$store.getters['user/getUserSetting']('jumpBackwardAmount')
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
this.$eventBus.$off('user-settings', this.settingsUpdated)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -18,6 +18,23 @@
|
|||||||
<p dir="auto" class="text-lg font-semibold mb-6">{{ title }}</p>
|
<p dir="auto" class="text-lg font-semibold mb-6">{{ title }}</p>
|
||||||
<div v-if="description" dir="auto" class="default-style" v-html="description" />
|
<div v-if="description" dir="auto" class="default-style" v-html="description" />
|
||||||
<p v-else class="mb-2">{{ $strings.MessageNoDescription }}</p>
|
<p v-else class="mb-2">{{ $strings.MessageNoDescription }}</p>
|
||||||
|
|
||||||
|
<div class="w-full h-px bg-white/5 my-4" />
|
||||||
|
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div class="flex-grow">
|
||||||
|
<p class="font-semibold text-xs mb-1">{{ $strings.LabelFilename }}</p>
|
||||||
|
<p class="mb-2 text-xs">
|
||||||
|
{{ audioFileFilename }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex-grow">
|
||||||
|
<p class="font-semibold text-xs mb-1">{{ $strings.LabelSize }}</p>
|
||||||
|
<p class="mb-2 text-xs">
|
||||||
|
{{ audioFileSize }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</modals-modal>
|
</modals-modal>
|
||||||
</template>
|
</template>
|
||||||
@@ -54,7 +71,7 @@ export default {
|
|||||||
return this.episode.description || ''
|
return this.episode.description || ''
|
||||||
},
|
},
|
||||||
media() {
|
media() {
|
||||||
return this.libraryItem ? this.libraryItem.media || {} : {}
|
return this.libraryItem?.media || {}
|
||||||
},
|
},
|
||||||
mediaMetadata() {
|
mediaMetadata() {
|
||||||
return this.media.metadata || {}
|
return this.media.metadata || {}
|
||||||
@@ -65,6 +82,14 @@ export default {
|
|||||||
podcastAuthor() {
|
podcastAuthor() {
|
||||||
return this.mediaMetadata.author
|
return this.mediaMetadata.author
|
||||||
},
|
},
|
||||||
|
audioFileFilename() {
|
||||||
|
return this.episode.audioFile?.metadata?.filename || ''
|
||||||
|
},
|
||||||
|
audioFileSize() {
|
||||||
|
const size = this.episode.audioFile?.metadata?.size || 0
|
||||||
|
|
||||||
|
return this.$bytesPretty(size)
|
||||||
|
},
|
||||||
bookCoverAspectRatio() {
|
bookCoverAspectRatio() {
|
||||||
return this.$store.getters['libraries/getBookCoverAspectRatio']
|
return this.$store.getters['libraries/getBookCoverAspectRatio']
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,7 +37,7 @@
|
|||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
|
|
||||||
<ui-tooltip direction="top" :text="$strings.LabelViewPlayerSettings">
|
<ui-tooltip direction="top" :text="$strings.LabelViewPlayerSettings">
|
||||||
<button :aria-label="$strings.LabelViewPlayerSettings" class="outline-none text-gray-300 mx-1 lg:mx-2 hover:text-white" @mousedown.prevent @mouseup.prevent @click.stop="$emit('showPlayerSettings')">
|
<button :aria-label="$strings.LabelViewPlayerSettings" class="outline-none text-gray-300 mx-1 lg:mx-2 hover:text-white" @mousedown.prevent @mouseup.prevent @click.stop="showPlayerSettings">
|
||||||
<span class="material-symbols text-2xl sm:text-2.5xl">settings_slow_motion</span>
|
<span class="material-symbols text-2xl sm:text-2.5xl">settings_slow_motion</span>
|
||||||
</button>
|
</button>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
@@ -64,6 +64,8 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<modals-chapters-modal v-model="showChaptersModal" :current-chapter="currentChapter" :playback-rate="playbackRate" :chapters="chapters" @select="selectChapter" />
|
<modals-chapters-modal v-model="showChaptersModal" :current-chapter="currentChapter" :playback-rate="playbackRate" :chapters="chapters" @select="selectChapter" />
|
||||||
|
|
||||||
|
<modals-player-settings-modal v-model="showPlayerSettingsModal" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -96,6 +98,7 @@ export default {
|
|||||||
audioEl: null,
|
audioEl: null,
|
||||||
seekLoading: false,
|
seekLoading: false,
|
||||||
showChaptersModal: false,
|
showChaptersModal: false,
|
||||||
|
showPlayerSettingsModal: false,
|
||||||
currentTime: 0,
|
currentTime: 0,
|
||||||
duration: 0
|
duration: 0
|
||||||
}
|
}
|
||||||
@@ -315,6 +318,9 @@ export default {
|
|||||||
if (!this.chapters.length) return
|
if (!this.chapters.length) return
|
||||||
this.showChaptersModal = !this.showChaptersModal
|
this.showChaptersModal = !this.showChaptersModal
|
||||||
},
|
},
|
||||||
|
showPlayerSettings() {
|
||||||
|
this.showPlayerSettingsModal = !this.showPlayerSettingsModal
|
||||||
|
},
|
||||||
init() {
|
init() {
|
||||||
this.playbackRate = this.$store.getters['user/getUserSetting']('playbackRate') || 1
|
this.playbackRate = this.$store.getters['user/getUserSetting']('playbackRate') || 1
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,6 @@
|
|||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- <p v-if="!episodes.length" class="py-4 text-center text-lg">{{ $strings.MessageNoEpisodes }}</p> -->
|
|
||||||
<div v-if="episodes.length" class="w-full py-3 mx-auto flex">
|
<div v-if="episodes.length" class="w-full py-3 mx-auto flex">
|
||||||
<form @submit.prevent="submit" class="flex flex-grow">
|
<form @submit.prevent="submit" class="flex flex-grow">
|
||||||
<ui-text-input v-model="search" @input="inputUpdate" type="search" :placeholder="$strings.PlaceholderSearchEpisode" class="flex-grow mr-2 text-sm md:text-base" />
|
<ui-text-input v-model="search" @input="inputUpdate" type="search" :placeholder="$strings.PlaceholderSearchEpisode" class="flex-grow mr-2 text-sm md:text-base" />
|
||||||
@@ -515,6 +514,10 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
filterSortChanged() {
|
filterSortChanged() {
|
||||||
|
// Save filterKey and sortKey to local storage
|
||||||
|
localStorage.setItem('podcastEpisodesFilter', this.filterKey)
|
||||||
|
localStorage.setItem('podcastEpisodesSortBy', this.sortKey + (this.sortDesc ? '-desc' : ''))
|
||||||
|
|
||||||
this.init()
|
this.init()
|
||||||
},
|
},
|
||||||
refresh() {
|
refresh() {
|
||||||
@@ -537,6 +540,11 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
|
this.filterKey = localStorage.getItem('podcastEpisodesFilter') || 'incomplete'
|
||||||
|
const sortBy = localStorage.getItem('podcastEpisodesSortBy') || 'publishedAt-desc'
|
||||||
|
this.sortKey = sortBy.split('-')[0]
|
||||||
|
this.sortDesc = sortBy.split('-')[1] === 'desc'
|
||||||
|
|
||||||
this.episodesCopy = this.episodes.map((ep) => ({ ...ep }))
|
this.episodesCopy = this.episodes.map((ep) => ({ ...ep }))
|
||||||
this.initListeners()
|
this.initListeners()
|
||||||
this.init()
|
this.init()
|
||||||
|
|||||||
@@ -71,8 +71,6 @@ export default {
|
|||||||
this.showSeriesForm = true
|
this.showSeriesForm = true
|
||||||
},
|
},
|
||||||
submitSeriesForm() {
|
submitSeriesForm() {
|
||||||
console.log('submit series form', this.value, this.selectedSeries)
|
|
||||||
|
|
||||||
if (!this.selectedSeries.name) {
|
if (!this.selectedSeries.name) {
|
||||||
this.$toast.error('Must enter a series')
|
this.$toast.error('Must enter a series')
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -28,10 +28,8 @@ export default {
|
|||||||
var validOtherFiles = []
|
var validOtherFiles = []
|
||||||
var ignoredFiles = []
|
var ignoredFiles = []
|
||||||
files.forEach((file) => {
|
files.forEach((file) => {
|
||||||
// var filetype = this.checkFileType(file.name)
|
|
||||||
if (!file.filetype) ignoredFiles.push(file)
|
if (!file.filetype) ignoredFiles.push(file)
|
||||||
else {
|
else {
|
||||||
// file.filetype = filetype
|
|
||||||
if (file.filetype === 'audio' || (file.filetype === 'ebook' && mediaType === 'book')) validItemFiles.push(file)
|
if (file.filetype === 'audio' || (file.filetype === 'ebook' && mediaType === 'book')) validItemFiles.push(file)
|
||||||
else validOtherFiles.push(file)
|
else validOtherFiles.push(file)
|
||||||
}
|
}
|
||||||
@@ -165,7 +163,7 @@ export default {
|
|||||||
|
|
||||||
var firstBookPath = Path.dirname(firstBookFile.filepath)
|
var firstBookPath = Path.dirname(firstBookFile.filepath)
|
||||||
|
|
||||||
var dirs = firstBookPath.split('/').filter(d => !!d && d !== '.')
|
var dirs = firstBookPath.split('/').filter((d) => !!d && d !== '.')
|
||||||
if (dirs.length) {
|
if (dirs.length) {
|
||||||
audiobook.title = dirs.pop()
|
audiobook.title = dirs.pop()
|
||||||
if (dirs.length > 1) {
|
if (dirs.length > 1) {
|
||||||
@@ -189,7 +187,7 @@ export default {
|
|||||||
var firstAudioFile = podcast.itemFiles[0]
|
var firstAudioFile = podcast.itemFiles[0]
|
||||||
if (!firstAudioFile.filepath) return podcast // No path
|
if (!firstAudioFile.filepath) return podcast // No path
|
||||||
var firstPath = Path.dirname(firstAudioFile.filepath)
|
var firstPath = Path.dirname(firstAudioFile.filepath)
|
||||||
var dirs = firstPath.split('/').filter(d => !!d && d !== '.')
|
var dirs = firstPath.split('/').filter((d) => !!d && d !== '.')
|
||||||
if (dirs.length) {
|
if (dirs.length) {
|
||||||
podcast.title = dirs.length > 1 ? dirs[1] : dirs[0]
|
podcast.title = dirs.length > 1 ? dirs[1] : dirs[0]
|
||||||
} else {
|
} else {
|
||||||
@@ -212,13 +210,15 @@ export default {
|
|||||||
}
|
}
|
||||||
var ignoredFiles = itemData.ignoredFiles
|
var ignoredFiles = itemData.ignoredFiles
|
||||||
var index = 1
|
var index = 1
|
||||||
var items = itemData.items.filter((ab) => {
|
var items = itemData.items
|
||||||
if (!ab.itemFiles.length) {
|
.filter((ab) => {
|
||||||
if (ab.otherFiles.length) ignoredFiles = ignoredFiles.concat(ab.otherFiles)
|
if (!ab.itemFiles.length) {
|
||||||
if (ab.ignoredFiles.length) ignoredFiles = ignoredFiles.concat(ab.ignoredFiles)
|
if (ab.otherFiles.length) ignoredFiles = ignoredFiles.concat(ab.otherFiles)
|
||||||
}
|
if (ab.ignoredFiles.length) ignoredFiles = ignoredFiles.concat(ab.ignoredFiles)
|
||||||
return ab.itemFiles.length
|
}
|
||||||
}).map(ab => this.cleanItem(ab, mediaType, index++))
|
return ab.itemFiles.length
|
||||||
|
})
|
||||||
|
.map((ab) => this.cleanItem(ab, mediaType, index++))
|
||||||
return {
|
return {
|
||||||
items,
|
items,
|
||||||
ignoredFiles
|
ignoredFiles
|
||||||
@@ -259,7 +259,7 @@ export default {
|
|||||||
|
|
||||||
otherFiles.forEach((file) => {
|
otherFiles.forEach((file) => {
|
||||||
var dir = Path.dirname(file.filepath)
|
var dir = Path.dirname(file.filepath)
|
||||||
var findItem = Object.values(itemMap).find(b => dir.startsWith(b.path))
|
var findItem = Object.values(itemMap).find((b) => dir.startsWith(b.path))
|
||||||
if (findItem) {
|
if (findItem) {
|
||||||
findItem.otherFiles.push(file)
|
findItem.otherFiles.push(file)
|
||||||
} else {
|
} else {
|
||||||
@@ -270,18 +270,18 @@ export default {
|
|||||||
var items = []
|
var items = []
|
||||||
var index = 1
|
var index = 1
|
||||||
// If book media type and all files are audio files then treat each one as an audiobook
|
// If book media type and all files are audio files then treat each one as an audiobook
|
||||||
if (itemMap[''] && !otherFiles.length && mediaType === 'book' && !itemMap[''].itemFiles.some(f => f.filetype !== 'audio')) {
|
if (itemMap[''] && !otherFiles.length && mediaType === 'book' && !itemMap[''].itemFiles.some((f) => f.filetype !== 'audio')) {
|
||||||
items = itemMap[''].itemFiles.map((audioFile) => {
|
items = itemMap[''].itemFiles.map((audioFile) => {
|
||||||
return this.cleanItem({ itemFiles: [audioFile], otherFiles: [], ignoredFiles: [] }, mediaType, index++)
|
return this.cleanItem({ itemFiles: [audioFile], otherFiles: [], ignoredFiles: [] }, mediaType, index++)
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
items = Object.values(itemMap).map(i => this.cleanItem(i, mediaType, index++))
|
items = Object.values(itemMap).map((i) => this.cleanItem(i, mediaType, index++))
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
items,
|
items,
|
||||||
ignoredFiles: ignoredFiles
|
ignoredFiles: ignoredFiles
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookshelf-client",
|
"name": "audiobookshelf-client",
|
||||||
"version": "2.17.0",
|
"version": "2.17.2",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "audiobookshelf-client",
|
"name": "audiobookshelf-client",
|
||||||
"version": "2.17.0",
|
"version": "2.17.2",
|
||||||
"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.17.0",
|
"version": "2.17.2",
|
||||||
"buildNumber": 1,
|
"buildNumber": 1,
|
||||||
"description": "Self-hosted audiobook and podcast client",
|
"description": "Self-hosted audiobook and podcast client",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
|
|||||||
@@ -126,12 +126,14 @@ export default {
|
|||||||
if (!this.localAudioPlayer || !this.hasLoaded) return
|
if (!this.localAudioPlayer || !this.hasLoaded) return
|
||||||
const currentTime = this.localAudioPlayer.getCurrentTime()
|
const currentTime = this.localAudioPlayer.getCurrentTime()
|
||||||
const duration = this.localAudioPlayer.getDuration()
|
const duration = this.localAudioPlayer.getDuration()
|
||||||
this.seek(Math.min(currentTime + 10, duration))
|
const jumpForwardAmount = this.$store.getters['user/getUserSetting']('jumpForwardAmount') || 10
|
||||||
|
this.seek(Math.min(currentTime + jumpForwardAmount, duration))
|
||||||
},
|
},
|
||||||
jumpBackward() {
|
jumpBackward() {
|
||||||
if (!this.localAudioPlayer || !this.hasLoaded) return
|
if (!this.localAudioPlayer || !this.hasLoaded) return
|
||||||
const currentTime = this.localAudioPlayer.getCurrentTime()
|
const currentTime = this.localAudioPlayer.getCurrentTime()
|
||||||
this.seek(Math.max(currentTime - 10, 0))
|
const jumpBackwardAmount = this.$store.getters['user/getUserSetting']('jumpBackwardAmount') || 10
|
||||||
|
this.seek(Math.max(currentTime - jumpBackwardAmount, 0))
|
||||||
},
|
},
|
||||||
setVolume(volume) {
|
setVolume(volume) {
|
||||||
if (!this.localAudioPlayer || !this.hasLoaded) return
|
if (!this.localAudioPlayer || !this.hasLoaded) return
|
||||||
@@ -248,6 +250,8 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
|
this.$store.dispatch('user/loadUserSettings')
|
||||||
|
|
||||||
this.resize()
|
this.resize()
|
||||||
window.addEventListener('resize', this.resize)
|
window.addEventListener('resize', this.resize)
|
||||||
window.addEventListener('keydown', this.keyDown)
|
window.addEventListener('keydown', this.keyDown)
|
||||||
|
|||||||
@@ -1,20 +1,20 @@
|
|||||||
<template>
|
<template>
|
||||||
<div id="page-wrapper" class="page p-0 sm:p-6 overflow-y-auto" :class="streamLibraryItem ? 'streaming' : ''">
|
<div id="page-wrapper" class="page p-1 sm:p-6 overflow-y-auto" :class="streamLibraryItem ? 'streaming' : ''">
|
||||||
<div class="w-full max-w-6xl mx-auto">
|
<div class="w-full max-w-6xl mx-auto">
|
||||||
<!-- Library & folder picker -->
|
<!-- Library & folder picker -->
|
||||||
<div class="flex my-6 -mx-2">
|
<div class="flex flex-wrap my-6 md:-mx-2">
|
||||||
<div class="w-1/5 px-2">
|
<div class="w-full md:w-1/5 px-2">
|
||||||
<ui-dropdown v-model="selectedLibraryId" :items="libraryItems" :label="$strings.LabelLibrary" :disabled="!!items.length" @input="libraryChanged" />
|
<ui-dropdown v-model="selectedLibraryId" :items="libraryItems" :label="$strings.LabelLibrary" :disabled="!!items.length" @input="libraryChanged" />
|
||||||
</div>
|
</div>
|
||||||
<div class="w-3/5 px-2">
|
<div class="w-full md:w-3/5 px-2">
|
||||||
<ui-dropdown v-model="selectedFolderId" :items="folderItems" :disabled="!selectedLibraryId || !!items.length" :label="$strings.LabelFolder" />
|
<ui-dropdown v-model="selectedFolderId" :items="folderItems" :disabled="!selectedLibraryId || !!items.length" :label="$strings.LabelFolder" />
|
||||||
</div>
|
</div>
|
||||||
<div class="w-1/5 px-2">
|
<div class="w-full md:w-1/5 px-2">
|
||||||
<ui-text-input-with-label :value="selectedLibraryMediaType" readonly :label="$strings.LabelMediaType" />
|
<ui-text-input-with-label :value="selectedLibraryMediaType" readonly :label="$strings.LabelMediaType" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="!selectedLibraryIsPodcast" class="flex items-center mb-6">
|
<div v-if="!selectedLibraryIsPodcast" class="flex items-center mb-6 px-2 md:px-0">
|
||||||
<label class="flex cursor-pointer pt-4">
|
<label class="flex cursor-pointer pt-4">
|
||||||
<ui-toggle-switch v-model="fetchMetadata.enabled" class="inline-flex" />
|
<ui-toggle-switch v-model="fetchMetadata.enabled" class="inline-flex" />
|
||||||
<span class="pl-2 text-base">{{ $strings.LabelAutoFetchMetadata }}</span>
|
<span class="pl-2 text-base">{{ $strings.LabelAutoFetchMetadata }}</span>
|
||||||
@@ -33,13 +33,13 @@
|
|||||||
</widgets-alert>
|
</widgets-alert>
|
||||||
|
|
||||||
<!-- Picker display -->
|
<!-- Picker display -->
|
||||||
<div v-if="!items.length && !ignoredFiles.length" class="w-full mx-auto border border-white border-opacity-20 px-12 pt-12 pb-4 my-12 relative" :class="isDragging ? 'bg-primary bg-opacity-40' : 'border-dashed'">
|
<div v-if="!items.length && !ignoredFiles.length" class="w-full mx-auto border border-white border-opacity-20 px-4 md:px-12 pt-12 pb-4 my-12 relative" :class="isDragging ? 'bg-primary bg-opacity-40' : 'border-dashed'">
|
||||||
<p class="text-2xl text-center">{{ isDragging ? $strings.LabelUploaderDropFiles : $strings.LabelUploaderDragAndDrop }}</p>
|
<p class="text-2xl text-center">{{ isDragging ? $strings.LabelUploaderDropFiles : isIOS ? $strings.LabelUploaderDragAndDropFilesOnly : $strings.LabelUploaderDragAndDrop }}</p>
|
||||||
<p class="text-center text-sm my-5">{{ $strings.MessageOr }}</p>
|
<p class="text-center text-sm my-5">{{ $strings.MessageOr }}</p>
|
||||||
<div class="w-full max-w-xl mx-auto">
|
<div class="w-full max-w-xl mx-auto">
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
<ui-btn class="w-full mx-1" @click="openFilePicker">{{ $strings.ButtonChooseFiles }}</ui-btn>
|
<ui-btn class="w-full mx-1" @click="openFilePicker">{{ $strings.ButtonChooseFiles }}</ui-btn>
|
||||||
<ui-btn class="w-full mx-1" @click="openFolderPicker">{{ $strings.ButtonChooseAFolder }}</ui-btn>
|
<ui-btn v-if="!isIOS" class="w-full mx-1" @click="openFolderPicker">{{ $strings.ButtonChooseAFolder }} </ui-btn>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="pt-8 text-center">
|
<div class="pt-8 text-center">
|
||||||
@@ -48,7 +48,7 @@
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p class="text-sm text-white text-opacity-70">
|
<p class="text-sm text-white text-opacity-70">
|
||||||
{{ $strings.NoteUploaderFoldersWithMediaFiles }} <span v-if="selectedLibraryMediaType === 'book'">{{ $strings.NoteUploaderOnlyAudioFiles }}</span>
|
<span v-if="!isIOS">{{ $strings.NoteUploaderFoldersWithMediaFiles }}</span> <span v-if="selectedLibraryMediaType === 'book'">{{ $strings.NoteUploaderOnlyAudioFiles }}</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -84,8 +84,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<input ref="fileInput" type="file" multiple :accept="inputAccept" class="hidden" @change="inputChanged" />
|
<input ref="fileInput" type="file" multiple :accept="isIOS ? '' : inputAccept" class="hidden" @change="inputChanged" />
|
||||||
<input ref="fileFolderInput" type="file" webkitdirectory multiple :accept="inputAccept" class="hidden" @change="inputChanged" />
|
<input ref="fileFolderInput" type="file" webkitdirectory multiple :accept="inputAccept" class="hidden" @change="inputChanged" v-if="!isIOS" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -127,6 +127,10 @@ export default {
|
|||||||
})
|
})
|
||||||
return extensions
|
return extensions
|
||||||
},
|
},
|
||||||
|
isIOS() {
|
||||||
|
const ua = window.navigator.userAgent
|
||||||
|
return /iPad|iPhone|iPod/.test(ua) && !window.MSStream
|
||||||
|
},
|
||||||
streamLibraryItem() {
|
streamLibraryItem() {
|
||||||
return this.$store.state.streamLibraryItem
|
return this.$store.state.streamLibraryItem
|
||||||
},
|
},
|
||||||
|
|||||||
+26
-1
@@ -127,5 +127,30 @@
|
|||||||
"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": "إدارة العلامات"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,8 +71,8 @@
|
|||||||
"ButtonQuickMatch": "Schnellabgleich",
|
"ButtonQuickMatch": "Schnellabgleich",
|
||||||
"ButtonReScan": "Neu scannen",
|
"ButtonReScan": "Neu scannen",
|
||||||
"ButtonRead": "Lesen",
|
"ButtonRead": "Lesen",
|
||||||
"ButtonReadLess": "Weniger anzeigen",
|
"ButtonReadLess": "weniger Anzeigen",
|
||||||
"ButtonReadMore": "Mehr anzeigen",
|
"ButtonReadMore": "Mehr Anzeigen",
|
||||||
"ButtonRefresh": "Neu Laden",
|
"ButtonRefresh": "Neu Laden",
|
||||||
"ButtonRemove": "Entfernen",
|
"ButtonRemove": "Entfernen",
|
||||||
"ButtonRemoveAll": "Alles entfernen",
|
"ButtonRemoveAll": "Alles entfernen",
|
||||||
@@ -220,7 +220,7 @@
|
|||||||
"LabelAddToPlaylist": "Zur Wiedergabeliste hinzufügen",
|
"LabelAddToPlaylist": "Zur Wiedergabeliste hinzufügen",
|
||||||
"LabelAddToPlaylistBatch": "Füge {0} Hörbüch(er)/Podcast(s) der Wiedergabeliste hinzu",
|
"LabelAddToPlaylistBatch": "Füge {0} Hörbüch(er)/Podcast(s) der Wiedergabeliste hinzu",
|
||||||
"LabelAddedAt": "Hinzugefügt am",
|
"LabelAddedAt": "Hinzugefügt am",
|
||||||
"LabelAddedDate": "Hinzugefügt {0}",
|
"LabelAddedDate": "{0} Hinzugefügt",
|
||||||
"LabelAdminUsersOnly": "Nur Admin Benutzer",
|
"LabelAdminUsersOnly": "Nur Admin Benutzer",
|
||||||
"LabelAll": "Alle",
|
"LabelAll": "Alle",
|
||||||
"LabelAllUsers": "Alle Benutzer",
|
"LabelAllUsers": "Alle Benutzer",
|
||||||
@@ -534,6 +534,7 @@
|
|||||||
"LabelSelectUsers": "Benutzer auswählen",
|
"LabelSelectUsers": "Benutzer auswählen",
|
||||||
"LabelSendEbookToDevice": "E-Buch senden an …",
|
"LabelSendEbookToDevice": "E-Buch senden an …",
|
||||||
"LabelSequence": "Reihenfolge",
|
"LabelSequence": "Reihenfolge",
|
||||||
|
"LabelSerial": "fortlaufend",
|
||||||
"LabelSeries": "Serien",
|
"LabelSeries": "Serien",
|
||||||
"LabelSeriesName": "Serienname",
|
"LabelSeriesName": "Serienname",
|
||||||
"LabelSeriesProgress": "Serienfortschritt",
|
"LabelSeriesProgress": "Serienfortschritt",
|
||||||
@@ -680,8 +681,8 @@
|
|||||||
"LabelWeekdaysToRun": "Wochentage für die Ausführung",
|
"LabelWeekdaysToRun": "Wochentage für die Ausführung",
|
||||||
"LabelXBooks": "{0} Bücher",
|
"LabelXBooks": "{0} Bücher",
|
||||||
"LabelXItems": "{0} Medien",
|
"LabelXItems": "{0} Medien",
|
||||||
"LabelYearReviewHide": "Verstecke Jahr in Übersicht",
|
"LabelYearReviewHide": "Jahresrückblick verbergen",
|
||||||
"LabelYearReviewShow": "Zeige Jahr in Übersicht",
|
"LabelYearReviewShow": "Jahresrückblick anzeigen",
|
||||||
"LabelYourAudiobookDuration": "Laufzeit deines Mediums",
|
"LabelYourAudiobookDuration": "Laufzeit deines Mediums",
|
||||||
"LabelYourBookmarks": "Lesezeichen",
|
"LabelYourBookmarks": "Lesezeichen",
|
||||||
"LabelYourPlaylists": "Eigene Wiedergabelisten",
|
"LabelYourPlaylists": "Eigene Wiedergabelisten",
|
||||||
|
|||||||
@@ -663,6 +663,7 @@
|
|||||||
"LabelUpdateDetailsHelp": "Allow overwriting of existing details for the selected books when a match is located",
|
"LabelUpdateDetailsHelp": "Allow overwriting of existing details for the selected books when a match is located",
|
||||||
"LabelUpdatedAt": "Updated At",
|
"LabelUpdatedAt": "Updated At",
|
||||||
"LabelUploaderDragAndDrop": "Drag & drop files or folders",
|
"LabelUploaderDragAndDrop": "Drag & drop files or folders",
|
||||||
|
"LabelUploaderDragAndDropFilesOnly": "Drag & drop files",
|
||||||
"LabelUploaderDropFiles": "Drop files",
|
"LabelUploaderDropFiles": "Drop files",
|
||||||
"LabelUploaderItemFetchMetadataHelp": "Automatically fetch title, author, and series",
|
"LabelUploaderItemFetchMetadataHelp": "Automatically fetch title, author, and series",
|
||||||
"LabelUseAdvancedOptions": "Use Advanced Options",
|
"LabelUseAdvancedOptions": "Use Advanced Options",
|
||||||
|
|||||||
+24
-1
@@ -18,7 +18,8 @@
|
|||||||
"ButtonChooseAFolder": "בחר תיקייה",
|
"ButtonChooseAFolder": "בחר תיקייה",
|
||||||
"ButtonChooseFiles": "בחר קבצים",
|
"ButtonChooseFiles": "בחר קבצים",
|
||||||
"ButtonClearFilter": "נקה סינון",
|
"ButtonClearFilter": "נקה סינון",
|
||||||
"ButtonCloseFeed": "סגור פיד",
|
"ButtonCloseFeed": "סגור ערוץ",
|
||||||
|
"ButtonCloseSession": "סגור סשן פתוח",
|
||||||
"ButtonCollections": "אוספים",
|
"ButtonCollections": "אוספים",
|
||||||
"ButtonConfigureScanner": "הגדר סורק",
|
"ButtonConfigureScanner": "הגדר סורק",
|
||||||
"ButtonCreate": "צור",
|
"ButtonCreate": "צור",
|
||||||
@@ -28,6 +29,7 @@
|
|||||||
"ButtonEdit": "ערוך",
|
"ButtonEdit": "ערוך",
|
||||||
"ButtonEditChapters": "ערוך פרקים",
|
"ButtonEditChapters": "ערוך פרקים",
|
||||||
"ButtonEditPodcast": "ערוך פודקאסט",
|
"ButtonEditPodcast": "ערוך פודקאסט",
|
||||||
|
"ButtonEnable": "הפעל",
|
||||||
"ButtonForceReScan": "סרוק מחדש בכוח",
|
"ButtonForceReScan": "סרוק מחדש בכוח",
|
||||||
"ButtonFullPath": "נתיב מלא",
|
"ButtonFullPath": "נתיב מלא",
|
||||||
"ButtonHide": "הסתר",
|
"ButtonHide": "הסתר",
|
||||||
@@ -46,19 +48,24 @@
|
|||||||
"ButtonNevermind": "לא משנה",
|
"ButtonNevermind": "לא משנה",
|
||||||
"ButtonNext": "הבא",
|
"ButtonNext": "הבא",
|
||||||
"ButtonNextChapter": "פרק הבא",
|
"ButtonNextChapter": "פרק הבא",
|
||||||
|
"ButtonNextItemInQueue": "פריט הבא בתור",
|
||||||
"ButtonOk": "אישור",
|
"ButtonOk": "אישור",
|
||||||
"ButtonOpenFeed": "פתח פיד",
|
"ButtonOpenFeed": "פתח פיד",
|
||||||
"ButtonOpenManager": "פתח מנהל",
|
"ButtonOpenManager": "פתח מנהל",
|
||||||
"ButtonPause": "השהה",
|
"ButtonPause": "השהה",
|
||||||
"ButtonPlay": "נגן",
|
"ButtonPlay": "נגן",
|
||||||
|
"ButtonPlayAll": "נגן הכל",
|
||||||
"ButtonPlaying": "מנגן",
|
"ButtonPlaying": "מנגן",
|
||||||
"ButtonPlaylists": "רשימות השמעה",
|
"ButtonPlaylists": "רשימות השמעה",
|
||||||
"ButtonPrevious": "קודם",
|
"ButtonPrevious": "קודם",
|
||||||
"ButtonPreviousChapter": "פרק קודם",
|
"ButtonPreviousChapter": "פרק קודם",
|
||||||
|
"ButtonProbeAudioFile": "בדוק קובץ אודיו",
|
||||||
"ButtonPurgeAllCache": "נקה את כל המטמון",
|
"ButtonPurgeAllCache": "נקה את כל המטמון",
|
||||||
"ButtonPurgeItemsCache": "נקה את מטמון הפריטים",
|
"ButtonPurgeItemsCache": "נקה את מטמון הפריטים",
|
||||||
"ButtonQueueAddItem": "הוסף לתור",
|
"ButtonQueueAddItem": "הוסף לתור",
|
||||||
"ButtonQueueRemoveItem": "הסר מהתור",
|
"ButtonQueueRemoveItem": "הסר מהתור",
|
||||||
|
"ButtonQuickEmbed": "הטמעה מהירה",
|
||||||
|
"ButtonQuickEmbedMetadata": "הטמעת מטא נתונים מהירה",
|
||||||
"ButtonQuickMatch": "התאמה מהירה",
|
"ButtonQuickMatch": "התאמה מהירה",
|
||||||
"ButtonReScan": "סרוק מחדש",
|
"ButtonReScan": "סרוק מחדש",
|
||||||
"ButtonRead": "קרא",
|
"ButtonRead": "קרא",
|
||||||
@@ -88,8 +95,10 @@
|
|||||||
"ButtonShow": "הצג",
|
"ButtonShow": "הצג",
|
||||||
"ButtonStartM4BEncode": "התחל קידוד M4B",
|
"ButtonStartM4BEncode": "התחל קידוד M4B",
|
||||||
"ButtonStartMetadataEmbed": "התחל הטמעת מטא-נתונים",
|
"ButtonStartMetadataEmbed": "התחל הטמעת מטא-נתונים",
|
||||||
|
"ButtonStats": "סטטיסטיקות",
|
||||||
"ButtonSubmit": "שלח",
|
"ButtonSubmit": "שלח",
|
||||||
"ButtonTest": "בדיקה",
|
"ButtonTest": "בדיקה",
|
||||||
|
"ButtonUnlinkOpenId": "נתק OpenID",
|
||||||
"ButtonUpload": "העלה",
|
"ButtonUpload": "העלה",
|
||||||
"ButtonUploadBackup": "העלה גיבוי",
|
"ButtonUploadBackup": "העלה גיבוי",
|
||||||
"ButtonUploadCover": "העלה כריכה",
|
"ButtonUploadCover": "העלה כריכה",
|
||||||
@@ -102,6 +111,7 @@
|
|||||||
"ErrorUploadFetchMetadataNoResults": "לא ניתן לשלוף מטא-נתונים - נסה לעדכן כותרת ו/או יוצר",
|
"ErrorUploadFetchMetadataNoResults": "לא ניתן לשלוף מטא-נתונים - נסה לעדכן כותרת ו/או יוצר",
|
||||||
"ErrorUploadLacksTitle": "חובה לתת כותרת",
|
"ErrorUploadLacksTitle": "חובה לתת כותרת",
|
||||||
"HeaderAccount": "חשבון",
|
"HeaderAccount": "חשבון",
|
||||||
|
"HeaderAddCustomMetadataProvider": "הוסף ספק מטא-נתונים מותאם אישית",
|
||||||
"HeaderAdvanced": "מתקדם",
|
"HeaderAdvanced": "מתקדם",
|
||||||
"HeaderAppriseNotificationSettings": "הגדרות התראות של Apprise",
|
"HeaderAppriseNotificationSettings": "הגדרות התראות של Apprise",
|
||||||
"HeaderAudioTracks": "רצועות קול",
|
"HeaderAudioTracks": "רצועות קול",
|
||||||
@@ -147,13 +157,17 @@
|
|||||||
"HeaderMetadataToEmbed": "מטא-נתונים להטמעה",
|
"HeaderMetadataToEmbed": "מטא-נתונים להטמעה",
|
||||||
"HeaderNewAccount": "חשבון חדש",
|
"HeaderNewAccount": "חשבון חדש",
|
||||||
"HeaderNewLibrary": "ספרייה חדשה",
|
"HeaderNewLibrary": "ספרייה חדשה",
|
||||||
|
"HeaderNotificationCreate": "צור התראה",
|
||||||
|
"HeaderNotificationUpdate": "עדכון התראה",
|
||||||
"HeaderNotifications": "התראות",
|
"HeaderNotifications": "התראות",
|
||||||
"HeaderOpenIDConnectAuthentication": "אימות OpenID Connect",
|
"HeaderOpenIDConnectAuthentication": "אימות OpenID Connect",
|
||||||
|
"HeaderOpenListeningSessions": "פתח הפעלות האזנה",
|
||||||
"HeaderOpenRSSFeed": "פתח ערוץ RSS",
|
"HeaderOpenRSSFeed": "פתח ערוץ RSS",
|
||||||
"HeaderOtherFiles": "קבצים אחרים",
|
"HeaderOtherFiles": "קבצים אחרים",
|
||||||
"HeaderPasswordAuthentication": "אימות סיסמה",
|
"HeaderPasswordAuthentication": "אימות סיסמה",
|
||||||
"HeaderPermissions": "הרשאות",
|
"HeaderPermissions": "הרשאות",
|
||||||
"HeaderPlayerQueue": "תור ניגון",
|
"HeaderPlayerQueue": "תור ניגון",
|
||||||
|
"HeaderPlayerSettings": "הגדרות נגן",
|
||||||
"HeaderPlaylist": "רשימת השמעה",
|
"HeaderPlaylist": "רשימת השמעה",
|
||||||
"HeaderPlaylistItems": "פריטי רשימת השמעה",
|
"HeaderPlaylistItems": "פריטי רשימת השמעה",
|
||||||
"HeaderPodcastsToAdd": "פודקאסטים להוספה",
|
"HeaderPodcastsToAdd": "פודקאסטים להוספה",
|
||||||
@@ -165,6 +179,7 @@
|
|||||||
"HeaderRemoveEpisodes": "הסר {0} פרקים",
|
"HeaderRemoveEpisodes": "הסר {0} פרקים",
|
||||||
"HeaderSavedMediaProgress": "התקדמות מדיה שמורה",
|
"HeaderSavedMediaProgress": "התקדמות מדיה שמורה",
|
||||||
"HeaderSchedule": "תיזמון",
|
"HeaderSchedule": "תיזמון",
|
||||||
|
"HeaderScheduleEpisodeDownloads": "תזמן הורדת פרקים אוטומטית",
|
||||||
"HeaderScheduleLibraryScans": "קבע סריקות ספרייה אוטומטיות",
|
"HeaderScheduleLibraryScans": "קבע סריקות ספרייה אוטומטיות",
|
||||||
"HeaderSession": "הפעלה",
|
"HeaderSession": "הפעלה",
|
||||||
"HeaderSetBackupSchedule": "קבע לוח זמנים לגיבוי",
|
"HeaderSetBackupSchedule": "קבע לוח זמנים לגיבוי",
|
||||||
@@ -190,6 +205,9 @@
|
|||||||
"HeaderYearReview": "שנת {0} בסקירה",
|
"HeaderYearReview": "שנת {0} בסקירה",
|
||||||
"HeaderYourStats": "הסטטיסטיקות שלך",
|
"HeaderYourStats": "הסטטיסטיקות שלך",
|
||||||
"LabelAbridged": "מקוצר",
|
"LabelAbridged": "מקוצר",
|
||||||
|
"LabelAbridgedChecked": "מקוצר (מסומן)",
|
||||||
|
"LabelAbridgedUnchecked": "בלתי מקוצר (לא מסומן)",
|
||||||
|
"LabelAccessibleBy": "נגיש על ידי",
|
||||||
"LabelAccountType": "סוג חשבון",
|
"LabelAccountType": "סוג חשבון",
|
||||||
"LabelAccountTypeAdmin": "מנהל",
|
"LabelAccountTypeAdmin": "מנהל",
|
||||||
"LabelAccountTypeGuest": "אורח",
|
"LabelAccountTypeGuest": "אורח",
|
||||||
@@ -200,13 +218,18 @@
|
|||||||
"LabelAddToPlaylist": "הוסף לרשימת השמעה",
|
"LabelAddToPlaylist": "הוסף לרשימת השמעה",
|
||||||
"LabelAddToPlaylistBatch": "הוסף {0} פריטים לרשימת השמעה",
|
"LabelAddToPlaylistBatch": "הוסף {0} פריטים לרשימת השמעה",
|
||||||
"LabelAddedAt": "נוסף בתאריך",
|
"LabelAddedAt": "נוסף בתאריך",
|
||||||
|
"LabelAddedDate": "נוסף ב-{0}",
|
||||||
"LabelAdminUsersOnly": "רק מנהלים",
|
"LabelAdminUsersOnly": "רק מנהלים",
|
||||||
"LabelAll": "הכל",
|
"LabelAll": "הכל",
|
||||||
"LabelAllUsers": "כל המשתמשים",
|
"LabelAllUsers": "כל המשתמשים",
|
||||||
"LabelAllUsersExcludingGuests": "כל המשתמשים, ללא אורחים",
|
"LabelAllUsersExcludingGuests": "כל המשתמשים, ללא אורחים",
|
||||||
"LabelAllUsersIncludingGuests": "כל המשתמשים כולל אורחים",
|
"LabelAllUsersIncludingGuests": "כל המשתמשים כולל אורחים",
|
||||||
"LabelAlreadyInYourLibrary": "כבר קיים בספרייה שלך",
|
"LabelAlreadyInYourLibrary": "כבר קיים בספרייה שלך",
|
||||||
|
"LabelApiToken": "טוקן API",
|
||||||
"LabelAppend": "הוסף לסוף",
|
"LabelAppend": "הוסף לסוף",
|
||||||
|
"LabelAudioBitrate": "קצב סיביות (לדוגמא 128k)",
|
||||||
|
"LabelAudioChannels": "ערוצי קול (1 או 2)",
|
||||||
|
"LabelAudioCodec": "קידוד קול",
|
||||||
"LabelAuthor": "יוצר",
|
"LabelAuthor": "יוצר",
|
||||||
"LabelAuthorFirstLast": "יוצר (שם פרטי שם משפחה)",
|
"LabelAuthorFirstLast": "יוצר (שם פרטי שם משפחה)",
|
||||||
"LabelAuthorLastFirst": "יוצר (שם משפחה, שם פרטי)",
|
"LabelAuthorLastFirst": "יוצר (שם משפחה, שם פרטי)",
|
||||||
|
|||||||
@@ -66,13 +66,13 @@
|
|||||||
"ButtonPurgeItemsCache": "Elimina la Cache selezionata",
|
"ButtonPurgeItemsCache": "Elimina la Cache selezionata",
|
||||||
"ButtonQueueAddItem": "Aggiungi alla Coda",
|
"ButtonQueueAddItem": "Aggiungi alla Coda",
|
||||||
"ButtonQueueRemoveItem": "Rimuovi dalla Coda",
|
"ButtonQueueRemoveItem": "Rimuovi dalla Coda",
|
||||||
"ButtonQuickEmbed": "Quick Embed",
|
"ButtonQuickEmbed": "Incorporazione Rapida",
|
||||||
"ButtonQuickEmbedMetadata": "Incorporamento rapido Metadati",
|
"ButtonQuickEmbedMetadata": "Incorporamento rapido Metadati",
|
||||||
"ButtonQuickMatch": "Controlla Metadata Auto",
|
"ButtonQuickMatch": "Controlla Metadata Auto",
|
||||||
"ButtonReScan": "Ri-scansiona",
|
"ButtonReScan": "Ri-scansiona",
|
||||||
"ButtonRead": "Leggi",
|
"ButtonRead": "Leggi",
|
||||||
"ButtonReadLess": "Leggi di Meno",
|
"ButtonReadLess": "Riduci",
|
||||||
"ButtonReadMore": "Leggi di Più",
|
"ButtonReadMore": "Espandi",
|
||||||
"ButtonRefresh": "Aggiorna",
|
"ButtonRefresh": "Aggiorna",
|
||||||
"ButtonRemove": "Rimuovi",
|
"ButtonRemove": "Rimuovi",
|
||||||
"ButtonRemoveAll": "Rimuovi Tutto",
|
"ButtonRemoveAll": "Rimuovi Tutto",
|
||||||
@@ -220,7 +220,7 @@
|
|||||||
"LabelAddToPlaylist": "Aggiungi alla playlist",
|
"LabelAddToPlaylist": "Aggiungi alla playlist",
|
||||||
"LabelAddToPlaylistBatch": "Aggiungi {0} file alla Playlist",
|
"LabelAddToPlaylistBatch": "Aggiungi {0} file alla Playlist",
|
||||||
"LabelAddedAt": "Aggiunto il",
|
"LabelAddedAt": "Aggiunto il",
|
||||||
"LabelAddedDate": "{0} aggiunti",
|
"LabelAddedDate": "Aggiunti {0}",
|
||||||
"LabelAdminUsersOnly": "Solo utenti Amministratori",
|
"LabelAdminUsersOnly": "Solo utenti Amministratori",
|
||||||
"LabelAll": "Tutti",
|
"LabelAll": "Tutti",
|
||||||
"LabelAllUsers": "Tutti gli Utenti",
|
"LabelAllUsers": "Tutti gli Utenti",
|
||||||
@@ -495,7 +495,7 @@
|
|||||||
"LabelProviderAuthorizationValue": "Authorization Header Value",
|
"LabelProviderAuthorizationValue": "Authorization Header Value",
|
||||||
"LabelPubDate": "Data di pubblicazione",
|
"LabelPubDate": "Data di pubblicazione",
|
||||||
"LabelPublishYear": "Anno di pubblicazione",
|
"LabelPublishYear": "Anno di pubblicazione",
|
||||||
"LabelPublishedDate": "{0} pubblicati",
|
"LabelPublishedDate": "Pubblicati {0}",
|
||||||
"LabelPublishedDecade": "Decennio di pubblicazione",
|
"LabelPublishedDecade": "Decennio di pubblicazione",
|
||||||
"LabelPublishedDecades": "Decenni di pubblicazione",
|
"LabelPublishedDecades": "Decenni di pubblicazione",
|
||||||
"LabelPublisher": "Editore",
|
"LabelPublisher": "Editore",
|
||||||
@@ -682,7 +682,7 @@
|
|||||||
"LabelXBooks": "{0} libri",
|
"LabelXBooks": "{0} libri",
|
||||||
"LabelXItems": "{0} oggetti",
|
"LabelXItems": "{0} oggetti",
|
||||||
"LabelYearReviewHide": "Nascondi Anno in rassegna",
|
"LabelYearReviewHide": "Nascondi Anno in rassegna",
|
||||||
"LabelYearReviewShow": "Vedi Anno in rassegna",
|
"LabelYearReviewShow": "Mostra Anno in rassegna",
|
||||||
"LabelYourAudiobookDuration": "La durata dell'audiolibro",
|
"LabelYourAudiobookDuration": "La durata dell'audiolibro",
|
||||||
"LabelYourBookmarks": "I tuoi preferiti",
|
"LabelYourBookmarks": "I tuoi preferiti",
|
||||||
"LabelYourPlaylists": "le tue Playlist",
|
"LabelYourPlaylists": "le tue Playlist",
|
||||||
@@ -779,7 +779,7 @@
|
|||||||
"MessageNoBackups": "Nessun Backup",
|
"MessageNoBackups": "Nessun Backup",
|
||||||
"MessageNoBookmarks": "Nessun preferito",
|
"MessageNoBookmarks": "Nessun preferito",
|
||||||
"MessageNoChapters": "Nessun capitolo",
|
"MessageNoChapters": "Nessun capitolo",
|
||||||
"MessageNoCollections": "Nessuna Raccolta",
|
"MessageNoCollections": "Nessuna Collezione",
|
||||||
"MessageNoCoversFound": "Nessuna Cover Trovata",
|
"MessageNoCoversFound": "Nessuna Cover Trovata",
|
||||||
"MessageNoDescription": "Nessuna descrizione",
|
"MessageNoDescription": "Nessuna descrizione",
|
||||||
"MessageNoDevices": "nessun dispositivo",
|
"MessageNoDevices": "nessun dispositivo",
|
||||||
|
|||||||
@@ -495,7 +495,7 @@
|
|||||||
"LabelProviderAuthorizationValue": "Vrednost glave avtorizacije",
|
"LabelProviderAuthorizationValue": "Vrednost glave avtorizacije",
|
||||||
"LabelPubDate": "Datum objave",
|
"LabelPubDate": "Datum objave",
|
||||||
"LabelPublishYear": "Leto izdaje",
|
"LabelPublishYear": "Leto izdaje",
|
||||||
"LabelPublishedDate": "Izdano {0}",
|
"LabelPublishedDate": "Objavljeno {0}",
|
||||||
"LabelPublishedDecade": "Desetletje izdaje",
|
"LabelPublishedDecade": "Desetletje izdaje",
|
||||||
"LabelPublishedDecades": "Desetletja izdaje",
|
"LabelPublishedDecades": "Desetletja izdaje",
|
||||||
"LabelPublisher": "Izdajatelj",
|
"LabelPublisher": "Izdajatelj",
|
||||||
@@ -682,7 +682,7 @@
|
|||||||
"LabelXBooks": "{0} knjig",
|
"LabelXBooks": "{0} knjig",
|
||||||
"LabelXItems": "{0} elementov",
|
"LabelXItems": "{0} elementov",
|
||||||
"LabelYearReviewHide": "Skrij pregled leta",
|
"LabelYearReviewHide": "Skrij pregled leta",
|
||||||
"LabelYearReviewShow": "Poglej pregled leta",
|
"LabelYearReviewShow": "Poglej si pregled leta",
|
||||||
"LabelYourAudiobookDuration": "Trajanje tvojih zvočnih knjig",
|
"LabelYourAudiobookDuration": "Trajanje tvojih zvočnih knjig",
|
||||||
"LabelYourBookmarks": "Tvoji zaznamki",
|
"LabelYourBookmarks": "Tvoji zaznamki",
|
||||||
"LabelYourPlaylists": "Tvoje seznami predvajanj",
|
"LabelYourPlaylists": "Tvoje seznami predvajanj",
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookshelf",
|
"name": "audiobookshelf",
|
||||||
"version": "2.17.0",
|
"version": "2.17.2",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "audiobookshelf",
|
"name": "audiobookshelf",
|
||||||
"version": "2.17.0",
|
"version": "2.17.2",
|
||||||
"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.17.0",
|
"version": "2.17.2",
|
||||||
"buildNumber": 1,
|
"buildNumber": 1,
|
||||||
"description": "Self-hosted audiobook and podcast server",
|
"description": "Self-hosted audiobook and podcast server",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
|
|||||||
@@ -41,6 +41,13 @@ Is there a feature you are looking for? [Suggest it](https://github.com/advplyr/
|
|||||||
|
|
||||||
Join us on [Discord](https://discord.gg/HQgCbd6E75)
|
Join us on [Discord](https://discord.gg/HQgCbd6E75)
|
||||||
|
|
||||||
|
### Demo
|
||||||
|
|
||||||
|
Check out the web client demo: https://audiobooks.dev/ (thanks for hosting [@Vito0912](https://github.com/Vito0912)!)
|
||||||
|
|
||||||
|
Username/password: `demo`/`demo` (user account)
|
||||||
|
|
||||||
|
|
||||||
### Android App (beta)
|
### Android App (beta)
|
||||||
|
|
||||||
Try it out on the [Google Play Store](https://play.google.com/store/apps/details?id=com.audiobookshelf.app)
|
Try it out on the [Google Play Store](https://play.google.com/store/apps/details?id=com.audiobookshelf.app)
|
||||||
|
|||||||
@@ -406,11 +406,6 @@ class Database {
|
|||||||
return Promise.all(oldBooks.map((oldBook) => this.models.book.saveFromOld(oldBook)))
|
return Promise.all(oldBooks.map((oldBook) => this.models.book.saveFromOld(oldBook)))
|
||||||
}
|
}
|
||||||
|
|
||||||
removeLibrary(libraryId) {
|
|
||||||
if (!this.sequelize) return false
|
|
||||||
return this.models.library.removeById(libraryId)
|
|
||||||
}
|
|
||||||
|
|
||||||
createBulkCollectionBooks(collectionBooks) {
|
createBulkCollectionBooks(collectionBooks) {
|
||||||
if (!this.sequelize) return false
|
if (!this.sequelize) return false
|
||||||
return this.models.collectionBook.bulkCreate(collectionBooks)
|
return this.models.collectionBook.bulkCreate(collectionBooks)
|
||||||
|
|||||||
+14
-11
@@ -194,18 +194,21 @@ class Server {
|
|||||||
|
|
||||||
const app = express()
|
const app = express()
|
||||||
|
|
||||||
/**
|
|
||||||
* @temporary
|
|
||||||
* This is necessary for the ebook & cover API endpoint in the mobile apps
|
|
||||||
* The mobile app ereader is using fetch api in Capacitor that is currently difficult to switch to native requests
|
|
||||||
* so we have to allow cors for specific origins to the /api/items/:id/ebook endpoint
|
|
||||||
* The cover image is fetched with XMLHttpRequest in the mobile apps to load into a canvas and extract colors
|
|
||||||
* @see https://ionicframework.com/docs/troubleshooting/cors
|
|
||||||
*
|
|
||||||
* Running in development allows cors to allow testing the mobile apps in the browser
|
|
||||||
* or env variable ALLOW_CORS = '1'
|
|
||||||
*/
|
|
||||||
app.use((req, res, next) => {
|
app.use((req, res, next) => {
|
||||||
|
// Prevent clickjacking by disallowing iframes
|
||||||
|
res.setHeader('Content-Security-Policy', "frame-ancestors 'self'")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @temporary
|
||||||
|
* This is necessary for the ebook & cover API endpoint in the mobile apps
|
||||||
|
* The mobile app ereader is using fetch api in Capacitor that is currently difficult to switch to native requests
|
||||||
|
* so we have to allow cors for specific origins to the /api/items/:id/ebook endpoint
|
||||||
|
* The cover image is fetched with XMLHttpRequest in the mobile apps to load into a canvas and extract colors
|
||||||
|
* @see https://ionicframework.com/docs/troubleshooting/cors
|
||||||
|
*
|
||||||
|
* Running in development allows cors to allow testing the mobile apps in the browser
|
||||||
|
* or env variable ALLOW_CORS = '1'
|
||||||
|
*/
|
||||||
if (Logger.isDev || req.path.match(/\/api\/items\/([a-z0-9-]{36})\/(ebook|cover)(\/[0-9]+)?/)) {
|
if (Logger.isDev || req.path.match(/\/api\/items\/([a-z0-9-]{36})\/(ebook|cover)(\/[0-9]+)?/)) {
|
||||||
const allowedOrigins = ['capacitor://localhost', 'http://localhost']
|
const allowedOrigins = ['capacitor://localhost', 'http://localhost']
|
||||||
if (global.AllowCors || Logger.isDev || allowedOrigins.some((o) => o === req.get('origin'))) {
|
if (global.AllowCors || Logger.isDev || allowedOrigins.some((o) => o === req.get('origin'))) {
|
||||||
|
|||||||
@@ -504,8 +504,21 @@ class LibraryController {
|
|||||||
await this.handleDeleteLibraryItem(libraryItem.mediaType, libraryItem.id, mediaItemIds)
|
await this.handleDeleteLibraryItem(libraryItem.mediaType, libraryItem.id, mediaItemIds)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set PlaybackSessions libraryId to null
|
||||||
|
const [sessionsUpdated] = await Database.playbackSessionModel.update(
|
||||||
|
{
|
||||||
|
libraryId: null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
where: {
|
||||||
|
libraryId: req.library.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
Logger.info(`[LibraryController] Updated ${sessionsUpdated} playback sessions to remove library id`)
|
||||||
|
|
||||||
const libraryJson = req.library.toOldJSON()
|
const libraryJson = req.library.toOldJSON()
|
||||||
await Database.removeLibrary(req.library.id)
|
await req.library.destroy()
|
||||||
|
|
||||||
// Re-order libraries
|
// Re-order libraries
|
||||||
await Database.libraryModel.resetDisplayOrder()
|
await Database.libraryModel.resetDisplayOrder()
|
||||||
|
|||||||
@@ -368,6 +368,19 @@ class UserController {
|
|||||||
await playlist.destroy()
|
await playlist.destroy()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set PlaybackSessions userId to null
|
||||||
|
const [sessionsUpdated] = await Database.playbackSessionModel.update(
|
||||||
|
{
|
||||||
|
userId: null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
where: {
|
||||||
|
userId: user.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
Logger.info(`[UserController] Updated ${sessionsUpdated} playback sessions to remove user id`)
|
||||||
|
|
||||||
const userJson = user.toOldJSONForBrowser()
|
const userJson = user.toOldJSONForBrowser()
|
||||||
await user.destroy()
|
await user.destroy()
|
||||||
SocketAuthority.adminEmitter('user_removed', userJson)
|
SocketAuthority.adminEmitter('user_removed', userJson)
|
||||||
|
|||||||
@@ -2,9 +2,10 @@
|
|||||||
|
|
||||||
Please add a record of every database migration that you create to this file. This will help us keep track of changes to the database schema over time.
|
Please add a record of every database migration that you create to this file. This will help us keep track of changes to the database schema over time.
|
||||||
|
|
||||||
| Server Version | Migration Script Name | Description |
|
| Server Version | Migration Script Name | Description |
|
||||||
| -------------- | ---------------------------- | ------------------------------------------------------------------------------------ |
|
| -------------- | ---------------------------- | ------------------------------------------------------------------------------------------------------------- |
|
||||||
| v2.15.0 | v2.15.0-series-column-unique | Series must have unique names in the same library |
|
| v2.15.0 | v2.15.0-series-column-unique | Series must have unique names in the same library |
|
||||||
| v2.15.1 | v2.15.1-reindex-nocase | Fix potential db corruption issues due to bad sqlite extension introduced in v2.12.0 |
|
| v2.15.1 | v2.15.1-reindex-nocase | Fix potential db corruption issues due to bad sqlite extension introduced in v2.12.0 |
|
||||||
| v2.15.2 | v2.15.2-index-creation | Creates author, series, and podcast episode indexes |
|
| v2.15.2 | v2.15.2-index-creation | Creates author, series, and podcast episode indexes |
|
||||||
| v2.17.0 | v2.17.0-uuid-replacement | Changes the data type of columns with UUIDv4 to UUID matching the associated model |
|
| v2.17.0 | v2.17.0-uuid-replacement | Changes the data type of columns with UUIDv4 to UUID matching the associated model |
|
||||||
|
| v2.17.3 | v2.17.3-fk-constraints | Changes the foreign key constraints for tables due to sequelize bug dropping constraints in v2.17.0 migration |
|
||||||
|
|||||||
@@ -27,10 +27,14 @@ async function up({ context: { queryInterface, logger } }) {
|
|||||||
type: 'UUID'
|
type: 'UUID'
|
||||||
})
|
})
|
||||||
|
|
||||||
logger.info('[2.17.0 migration] Changing mediaItemShares.mediaItemId column to UUID')
|
if (await queryInterface.tableExists('mediaItemShares')) {
|
||||||
await queryInterface.changeColumn('mediaItemShares', 'mediaItemId', {
|
logger.info('[2.17.0 migration] Changing mediaItemShares.mediaItemId column to UUID')
|
||||||
type: 'UUID'
|
await queryInterface.changeColumn('mediaItemShares', 'mediaItemId', {
|
||||||
})
|
type: 'UUID'
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
logger.info('[2.17.0 migration] mediaItemShares table does not exist, skipping column change')
|
||||||
|
}
|
||||||
|
|
||||||
logger.info('[2.17.0 migration] Changing playbackSessions.mediaItemId column to UUID')
|
logger.info('[2.17.0 migration] Changing playbackSessions.mediaItemId column to UUID')
|
||||||
await queryInterface.changeColumn('playbackSessions', 'mediaItemId', {
|
await queryInterface.changeColumn('playbackSessions', 'mediaItemId', {
|
||||||
|
|||||||
@@ -0,0 +1,259 @@
|
|||||||
|
/**
|
||||||
|
* @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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This upward migration script changes foreign key constraints for the
|
||||||
|
* libraryItems, feeds, mediaItemShares, playbackSessions, playlistMediaItems, and mediaProgresses tables.
|
||||||
|
*
|
||||||
|
* @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('[2.17.3 migration] UPGRADE BEGIN: 2.17.3-fk-constraints')
|
||||||
|
|
||||||
|
const execQuery = queryInterface.sequelize.query.bind(queryInterface.sequelize)
|
||||||
|
|
||||||
|
// Disable foreign key constraints for the next sequence of operations
|
||||||
|
await execQuery(`PRAGMA foreign_keys = OFF;`)
|
||||||
|
|
||||||
|
try {
|
||||||
|
await execQuery(`BEGIN TRANSACTION;`)
|
||||||
|
|
||||||
|
logger.info('[2.17.3 migration] Updating libraryItems constraints')
|
||||||
|
const libraryItemsConstraints = [
|
||||||
|
{ field: 'libraryId', onDelete: 'SET NULL', onUpdate: 'CASCADE' },
|
||||||
|
{ field: 'libraryFolderId', onDelete: 'SET NULL', onUpdate: 'CASCADE' }
|
||||||
|
]
|
||||||
|
if (await changeConstraints(queryInterface, 'libraryItems', libraryItemsConstraints)) {
|
||||||
|
logger.info('[2.17.3 migration] Finished updating libraryItems constraints')
|
||||||
|
} else {
|
||||||
|
logger.info('[2.17.3 migration] No changes needed for libraryItems constraints')
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info('[2.17.3 migration] Updating feeds constraints')
|
||||||
|
const feedsConstraints = [{ field: 'userId', onDelete: 'SET NULL', onUpdate: 'CASCADE' }]
|
||||||
|
if (await changeConstraints(queryInterface, 'feeds', feedsConstraints)) {
|
||||||
|
logger.info('[2.17.3 migration] Finished updating feeds constraints')
|
||||||
|
} else {
|
||||||
|
logger.info('[2.17.3 migration] No changes needed for feeds constraints')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (await queryInterface.tableExists('mediaItemShares')) {
|
||||||
|
logger.info('[2.17.3 migration] Updating mediaItemShares constraints')
|
||||||
|
const mediaItemSharesConstraints = [{ field: 'userId', onDelete: 'SET NULL', onUpdate: 'CASCADE' }]
|
||||||
|
if (await changeConstraints(queryInterface, 'mediaItemShares', mediaItemSharesConstraints)) {
|
||||||
|
logger.info('[2.17.3 migration] Finished updating mediaItemShares constraints')
|
||||||
|
} else {
|
||||||
|
logger.info('[2.17.3 migration] No changes needed for mediaItemShares constraints')
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logger.info('[2.17.3 migration] mediaItemShares table does not exist, skipping column change')
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info('[2.17.3 migration] Updating playbackSessions constraints')
|
||||||
|
const playbackSessionsConstraints = [
|
||||||
|
{ field: 'deviceId', onDelete: 'SET NULL', onUpdate: 'CASCADE' },
|
||||||
|
{ field: 'libraryId', onDelete: 'SET NULL', onUpdate: 'CASCADE' },
|
||||||
|
{ field: 'userId', onDelete: 'SET NULL', onUpdate: 'CASCADE' }
|
||||||
|
]
|
||||||
|
if (await changeConstraints(queryInterface, 'playbackSessions', playbackSessionsConstraints)) {
|
||||||
|
logger.info('[2.17.3 migration] Finished updating playbackSessions constraints')
|
||||||
|
} else {
|
||||||
|
logger.info('[2.17.3 migration] No changes needed for playbackSessions constraints')
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info('[2.17.3 migration] Updating playlistMediaItems constraints')
|
||||||
|
const playlistMediaItemsConstraints = [{ field: 'playlistId', onDelete: 'CASCADE', onUpdate: 'CASCADE' }]
|
||||||
|
if (await changeConstraints(queryInterface, 'playlistMediaItems', playlistMediaItemsConstraints)) {
|
||||||
|
logger.info('[2.17.3 migration] Finished updating playlistMediaItems constraints')
|
||||||
|
} else {
|
||||||
|
logger.info('[2.17.3 migration] No changes needed for playlistMediaItems constraints')
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info('[2.17.3 migration] Updating mediaProgresses constraints')
|
||||||
|
const mediaProgressesConstraints = [{ field: 'userId', onDelete: 'CASCADE', onUpdate: 'CASCADE' }]
|
||||||
|
if (await changeConstraints(queryInterface, 'mediaProgresses', mediaProgressesConstraints)) {
|
||||||
|
logger.info('[2.17.3 migration] Finished updating mediaProgresses constraints')
|
||||||
|
} else {
|
||||||
|
logger.info('[2.17.3 migration] No changes needed for mediaProgresses constraints')
|
||||||
|
}
|
||||||
|
|
||||||
|
await execQuery(`COMMIT;`)
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`[2.17.3 migration] Migration failed - rolling back. Error:`, error)
|
||||||
|
await execQuery(`ROLLBACK;`)
|
||||||
|
}
|
||||||
|
|
||||||
|
await execQuery(`PRAGMA foreign_keys = ON;`)
|
||||||
|
|
||||||
|
// Completed migration
|
||||||
|
logger.info('[2.17.3 migration] UPGRADE END: 2.17.3-fk-constraints')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This downward migration script is a no-op.
|
||||||
|
*
|
||||||
|
* @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('[2.17.3 migration] DOWNGRADE BEGIN: 2.17.3-fk-constraints')
|
||||||
|
|
||||||
|
// This migration is a no-op
|
||||||
|
logger.info('[2.17.3 migration] No action required for downgrade')
|
||||||
|
|
||||||
|
// Completed migration
|
||||||
|
logger.info('[2.17.3 migration] DOWNGRADE END: 2.17.3-fk-constraints')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef ConstraintUpdateObj
|
||||||
|
* @property {string} field - The field to update
|
||||||
|
* @property {string} onDelete - The onDelete constraint
|
||||||
|
* @property {string} onUpdate - The onUpdate constraint
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef SequelizeFKObj
|
||||||
|
* @property {{ model: string, key: string }} references
|
||||||
|
* @property {string} onDelete
|
||||||
|
* @property {string} onUpdate
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Object} fk - The foreign key object from PRAGMA foreign_key_list
|
||||||
|
* @returns {SequelizeFKObj} - The foreign key object formatted for Sequelize
|
||||||
|
*/
|
||||||
|
const formatFKsPragmaToSequelizeFK = (fk) => {
|
||||||
|
return {
|
||||||
|
references: {
|
||||||
|
model: fk.table,
|
||||||
|
key: fk.to
|
||||||
|
},
|
||||||
|
onDelete: fk['on_delete'],
|
||||||
|
onUpdate: fk['on_update']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {import('sequelize').QueryInterface} queryInterface
|
||||||
|
* @param {string} tableName
|
||||||
|
* @param {ConstraintUpdateObj[]} constraints
|
||||||
|
* @returns {Promise<Record<string, SequelizeFKObj>|null>}
|
||||||
|
*/
|
||||||
|
async function getUpdatedForeignKeys(queryInterface, tableName, constraints) {
|
||||||
|
const execQuery = queryInterface.sequelize.query.bind(queryInterface.sequelize)
|
||||||
|
const quotedTableName = queryInterface.quoteIdentifier(tableName)
|
||||||
|
|
||||||
|
const foreignKeys = await execQuery(`PRAGMA foreign_key_list(${quotedTableName});`)
|
||||||
|
|
||||||
|
let hasUpdates = false
|
||||||
|
const foreignKeysByColName = foreignKeys.reduce((prev, curr) => {
|
||||||
|
const fk = formatFKsPragmaToSequelizeFK(curr)
|
||||||
|
|
||||||
|
const constraint = constraints.find((c) => c.field === curr.from)
|
||||||
|
if (constraint && (constraint.onDelete !== fk.onDelete || constraint.onUpdate !== fk.onUpdate)) {
|
||||||
|
fk.onDelete = constraint.onDelete
|
||||||
|
fk.onUpdate = constraint.onUpdate
|
||||||
|
hasUpdates = true
|
||||||
|
}
|
||||||
|
|
||||||
|
return { ...prev, [curr.from]: fk }
|
||||||
|
}, {})
|
||||||
|
|
||||||
|
return hasUpdates ? foreignKeysByColName : null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extends the Sequelize describeTable function to include the updated foreign key constraints
|
||||||
|
*
|
||||||
|
* @param {import('sequelize').QueryInterface} queryInterface
|
||||||
|
* @param {String} tableName
|
||||||
|
* @param {Record<string, SequelizeFKObj>} updatedForeignKeys
|
||||||
|
*/
|
||||||
|
async function describeTableWithFKs(queryInterface, tableName, updatedForeignKeys) {
|
||||||
|
const tableDescription = await queryInterface.describeTable(tableName)
|
||||||
|
|
||||||
|
const tableDescriptionWithFks = Object.entries(tableDescription).reduce((prev, [col, attributes]) => {
|
||||||
|
let extendedAttributes = attributes
|
||||||
|
|
||||||
|
if (updatedForeignKeys[col]) {
|
||||||
|
extendedAttributes = {
|
||||||
|
...extendedAttributes,
|
||||||
|
...updatedForeignKeys[col]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { ...prev, [col]: extendedAttributes }
|
||||||
|
}, {})
|
||||||
|
|
||||||
|
return tableDescriptionWithFks
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see https://www.sqlite.org/lang_altertable.html#otheralter
|
||||||
|
* @see https://sequelize.org/docs/v6/other-topics/query-interface/#changing-and-removing-columns-in-sqlite
|
||||||
|
*
|
||||||
|
* @param {import('sequelize').QueryInterface} queryInterface
|
||||||
|
* @param {string} tableName
|
||||||
|
* @param {ConstraintUpdateObj[]} constraints
|
||||||
|
* @returns {Promise<boolean>} - Return false if no changes are needed, true otherwise
|
||||||
|
*/
|
||||||
|
async function changeConstraints(queryInterface, tableName, constraints) {
|
||||||
|
const updatedForeignKeys = await getUpdatedForeignKeys(queryInterface, tableName, constraints)
|
||||||
|
if (!updatedForeignKeys) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const execQuery = queryInterface.sequelize.query.bind(queryInterface.sequelize)
|
||||||
|
const quotedTableName = queryInterface.quoteIdentifier(tableName)
|
||||||
|
|
||||||
|
const backupTableName = `${tableName}_${Math.round(Math.random() * 100)}_backup`
|
||||||
|
const quotedBackupTableName = queryInterface.quoteIdentifier(backupTableName)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const tableDescriptionWithFks = await describeTableWithFKs(queryInterface, tableName, updatedForeignKeys)
|
||||||
|
|
||||||
|
const attributes = queryInterface.queryGenerator.attributesToSQL(tableDescriptionWithFks)
|
||||||
|
|
||||||
|
// Create the backup table
|
||||||
|
await queryInterface.createTable(backupTableName, attributes)
|
||||||
|
|
||||||
|
const attributeNames = Object.keys(attributes)
|
||||||
|
.map((attr) => queryInterface.quoteIdentifier(attr))
|
||||||
|
.join(', ')
|
||||||
|
|
||||||
|
// Copy all data from the target table to the backup table
|
||||||
|
await execQuery(`INSERT INTO ${quotedBackupTableName} SELECT ${attributeNames} FROM ${quotedTableName};`)
|
||||||
|
|
||||||
|
// Drop the old (original) table
|
||||||
|
await queryInterface.dropTable(tableName)
|
||||||
|
|
||||||
|
// Rename the backup table to the original table's name
|
||||||
|
await queryInterface.renameTable(backupTableName, tableName)
|
||||||
|
|
||||||
|
// Validate that all foreign key constraints are correct
|
||||||
|
const result = await execQuery(`PRAGMA foreign_key_check(${quotedTableName});`, {
|
||||||
|
type: queryInterface.sequelize.Sequelize.QueryTypes.SELECT
|
||||||
|
})
|
||||||
|
|
||||||
|
// There are foreign key violations, exit
|
||||||
|
if (result.length) {
|
||||||
|
return Promise.reject(`Foreign key violations detected: ${JSON.stringify(result, null, 2)}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
} catch (error) {
|
||||||
|
return Promise.reject(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { up, down }
|
||||||
@@ -107,19 +107,6 @@ class Library extends Model {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Destroy library by id
|
|
||||||
* @param {string} libraryId
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
static removeById(libraryId) {
|
|
||||||
return this.destroy({
|
|
||||||
where: {
|
|
||||||
id: libraryId
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all library ids
|
* Get all library ids
|
||||||
* @returns {Promise<string[]>} array of library ids
|
* @returns {Promise<string[]>} array of library ids
|
||||||
|
|||||||
@@ -479,7 +479,7 @@ class LibraryItem extends Model {
|
|||||||
{
|
{
|
||||||
model: this.sequelize.models.series,
|
model: this.sequelize.models.series,
|
||||||
through: {
|
through: {
|
||||||
attributes: ['sequence']
|
attributes: ['id', 'sequence']
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -611,7 +611,7 @@ class User extends Model {
|
|||||||
*/
|
*/
|
||||||
getOldMediaProgress(libraryItemId, episodeId = null) {
|
getOldMediaProgress(libraryItemId, episodeId = null) {
|
||||||
const mediaProgress = this.mediaProgresses?.find((mp) => {
|
const mediaProgress = this.mediaProgresses?.find((mp) => {
|
||||||
if (episodeId && mp.mediaItemId === episodeId) return true
|
if (episodeId && mp.mediaItemId !== episodeId) return false
|
||||||
return mp.extraData?.libraryItemId === libraryItemId
|
return mp.extraData?.libraryItemId === libraryItemId
|
||||||
})
|
})
|
||||||
return mediaProgress?.getOldMediaProgress() || null
|
return mediaProgress?.getOldMediaProgress() || null
|
||||||
|
|||||||
@@ -29,7 +29,12 @@ class BookMetadata {
|
|||||||
this.subtitle = metadata.subtitle
|
this.subtitle = metadata.subtitle
|
||||||
this.authors = metadata.authors?.map ? metadata.authors.map((a) => ({ ...a })) : []
|
this.authors = metadata.authors?.map ? metadata.authors.map((a) => ({ ...a })) : []
|
||||||
this.narrators = metadata.narrators ? [...metadata.narrators].filter((n) => n) : []
|
this.narrators = metadata.narrators ? [...metadata.narrators].filter((n) => n) : []
|
||||||
this.series = metadata.series?.map ? metadata.series.map((s) => ({ ...s })) : []
|
this.series = metadata.series?.map
|
||||||
|
? metadata.series.map((s) => ({
|
||||||
|
...s,
|
||||||
|
name: s.name || 'No Title'
|
||||||
|
}))
|
||||||
|
: []
|
||||||
this.genres = metadata.genres ? [...metadata.genres] : []
|
this.genres = metadata.genres ? [...metadata.genres] : []
|
||||||
this.publishedYear = metadata.publishedYear || null
|
this.publishedYear = metadata.publishedYear || null
|
||||||
this.publishedDate = metadata.publishedDate || null
|
this.publishedDate = metadata.publishedDate || null
|
||||||
|
|||||||
@@ -510,7 +510,7 @@ module.exports = {
|
|||||||
// If nothing has changed, check if the number of podcasts in
|
// If nothing has changed, check if the number of podcasts in
|
||||||
// library is still the same as prior check before updating cache creation time
|
// library is still the same as prior check before updating cache creation time
|
||||||
|
|
||||||
if (podcastCountFromDatabase === Database.libraryFilterData[libraryId].podcastCount) {
|
if (podcastCountFromDatabase === Database.libraryFilterData[libraryId]?.podcastCount) {
|
||||||
Logger.debug(`Filter data for ${libraryId} has not changed, returning cached data and updating cache time after ${((Date.now() - start) / 1000).toFixed(2)}s`)
|
Logger.debug(`Filter data for ${libraryId} has not changed, returning cached data and updating cache time after ${((Date.now() - start) / 1000).toFixed(2)}s`)
|
||||||
Database.libraryFilterData[libraryId].loadedAt = Date.now()
|
Database.libraryFilterData[libraryId].loadedAt = Date.now()
|
||||||
return cachedFilterData
|
return cachedFilterData
|
||||||
@@ -613,7 +613,7 @@ module.exports = {
|
|||||||
if (changedBooks + changedSeries + changedAuthors === 0) {
|
if (changedBooks + changedSeries + changedAuthors === 0) {
|
||||||
// If nothing has changed, check if the number of authors, series, and books
|
// If nothing has changed, check if the number of authors, series, and books
|
||||||
// matches the prior check before updating cache creation time
|
// matches the prior check before updating cache creation time
|
||||||
if (bookCountFromDatabase === Database.libraryFilterData[libraryId].bookCount && seriesCountFromDatabase === Database.libraryFilterData[libraryId].seriesCount && authorCountFromDatabase === Database.libraryFilterData[libraryId].authorCount) {
|
if (bookCountFromDatabase === Database.libraryFilterData[libraryId]?.bookCount && seriesCountFromDatabase === Database.libraryFilterData[libraryId]?.seriesCount && authorCountFromDatabase === Database.libraryFilterData[libraryId].authorCount) {
|
||||||
Logger.debug(`Filter data for ${libraryId} has not changed, returning cached data and updating cache time after ${((Date.now() - start) / 1000).toFixed(2)}s`)
|
Logger.debug(`Filter data for ${libraryId} has not changed, returning cached data and updating cache time after ${((Date.now() - start) / 1000).toFixed(2)}s`)
|
||||||
Database.libraryFilterData[libraryId].loadedAt = Date.now()
|
Database.libraryFilterData[libraryId].loadedAt = Date.now()
|
||||||
return cachedFilterData
|
return cachedFilterData
|
||||||
@@ -662,7 +662,7 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
attributes: ['id', 'name']
|
attributes: ['id', 'name']
|
||||||
})
|
})
|
||||||
series.forEach((s) => data.series.push({ id: s.id, name: s.name }))
|
series.forEach((s) => data.series.push({ id: s.id, name: s.name || 'No Title' }))
|
||||||
|
|
||||||
const authors = await Database.authorModel.findAll({
|
const authors = await Database.authorModel.findAll({
|
||||||
where: {
|
where: {
|
||||||
|
|||||||
@@ -0,0 +1,230 @@
|
|||||||
|
const { expect } = require('chai')
|
||||||
|
const sinon = require('sinon')
|
||||||
|
const { up } = require('../../../server/migrations/v2.17.3-fk-constraints')
|
||||||
|
const { Sequelize, QueryInterface } = require('sequelize')
|
||||||
|
const Logger = require('../../../server/Logger')
|
||||||
|
|
||||||
|
describe('migration-v2.17.3-fk-constraints', () => {
|
||||||
|
let sequelize
|
||||||
|
/** @type {QueryInterface} */
|
||||||
|
let queryInterface
|
||||||
|
let loggerInfoStub
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
sequelize = new Sequelize({ dialect: 'sqlite', storage: ':memory:', logging: false })
|
||||||
|
queryInterface = sequelize.getQueryInterface()
|
||||||
|
loggerInfoStub = sinon.stub(Logger, 'info')
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
sinon.restore()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('up', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
// Create associated tables: Users, libraries, libraryFolders, playlists, devices
|
||||||
|
await queryInterface.sequelize.query('CREATE TABLE `users` (`id` UUID PRIMARY KEY);')
|
||||||
|
await queryInterface.sequelize.query('CREATE TABLE `libraries` (`id` UUID PRIMARY KEY);')
|
||||||
|
await queryInterface.sequelize.query('CREATE TABLE `libraryFolders` (`id` UUID PRIMARY KEY);')
|
||||||
|
await queryInterface.sequelize.query('CREATE TABLE `playlists` (`id` UUID PRIMARY KEY);')
|
||||||
|
await queryInterface.sequelize.query('CREATE TABLE `devices` (`id` UUID PRIMARY KEY);')
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await queryInterface.dropAllTables()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should fix table foreign key constraints', async () => {
|
||||||
|
// Create tables with missing foreign key constraints: libraryItems, feeds, mediaItemShares, playbackSessions, playlistMediaItems, mediaProgresses
|
||||||
|
await queryInterface.sequelize.query('CREATE TABLE `libraryItems` (`id` UUID UNIQUE PRIMARY KEY, `libraryId` UUID REFERENCES `libraries` (`id`), `libraryFolderId` UUID REFERENCES `libraryFolders` (`id`));')
|
||||||
|
await queryInterface.sequelize.query('CREATE TABLE `feeds` (`id` UUID UNIQUE PRIMARY KEY, `userId` UUID REFERENCES `users` (`id`));')
|
||||||
|
await queryInterface.sequelize.query('CREATE TABLE `mediaItemShares` (`id` UUID UNIQUE PRIMARY KEY, `userId` UUID REFERENCES `users` (`id`));')
|
||||||
|
await queryInterface.sequelize.query('CREATE TABLE `playbackSessions` (`id` UUID UNIQUE PRIMARY KEY, `userId` UUID REFERENCES `users` (`id`), `deviceId` UUID REFERENCES `devices` (`id`), `libraryId` UUID REFERENCES `libraries` (`id`));')
|
||||||
|
await queryInterface.sequelize.query('CREATE TABLE `playlistMediaItems` (`id` UUID UNIQUE PRIMARY KEY, `playlistId` UUID REFERENCES `playlists` (`id`));')
|
||||||
|
await queryInterface.sequelize.query('CREATE TABLE `mediaProgresses` (`id` UUID UNIQUE PRIMARY KEY, `userId` UUID REFERENCES `users` (`id`));')
|
||||||
|
|
||||||
|
//
|
||||||
|
// Validate that foreign key constraints are missing
|
||||||
|
//
|
||||||
|
let libraryItemsForeignKeys = await queryInterface.sequelize.query(`PRAGMA foreign_key_list(libraryItems);`)
|
||||||
|
expect(libraryItemsForeignKeys).to.have.deep.members([
|
||||||
|
{ id: 0, seq: 0, table: 'libraryFolders', from: 'libraryFolderId', to: 'id', on_update: 'NO ACTION', on_delete: 'NO ACTION', match: 'NONE' },
|
||||||
|
{ id: 1, seq: 0, table: 'libraries', from: 'libraryId', to: 'id', on_update: 'NO ACTION', on_delete: 'NO ACTION', match: 'NONE' }
|
||||||
|
])
|
||||||
|
|
||||||
|
let feedsForeignKeys = await queryInterface.sequelize.query(`PRAGMA foreign_key_list(feeds);`)
|
||||||
|
expect(feedsForeignKeys).to.deep.equal([{ id: 0, seq: 0, table: 'users', from: 'userId', to: 'id', on_update: 'NO ACTION', on_delete: 'NO ACTION', match: 'NONE' }])
|
||||||
|
|
||||||
|
let mediaItemSharesForeignKeys = await queryInterface.sequelize.query(`PRAGMA foreign_key_list(mediaItemShares);`)
|
||||||
|
expect(mediaItemSharesForeignKeys).to.deep.equal([{ id: 0, seq: 0, table: 'users', from: 'userId', to: 'id', on_update: 'NO ACTION', on_delete: 'NO ACTION', match: 'NONE' }])
|
||||||
|
|
||||||
|
let playbackSessionForeignKeys = await queryInterface.sequelize.query(`PRAGMA foreign_key_list(playbackSessions);`)
|
||||||
|
expect(playbackSessionForeignKeys).to.deep.equal([
|
||||||
|
{ id: 0, seq: 0, table: 'libraries', from: 'libraryId', to: 'id', on_update: 'NO ACTION', on_delete: 'NO ACTION', match: 'NONE' },
|
||||||
|
{ id: 1, seq: 0, table: 'devices', from: 'deviceId', to: 'id', on_update: 'NO ACTION', on_delete: 'NO ACTION', match: 'NONE' },
|
||||||
|
{ id: 2, seq: 0, table: 'users', from: 'userId', to: 'id', on_update: 'NO ACTION', on_delete: 'NO ACTION', match: 'NONE' }
|
||||||
|
])
|
||||||
|
|
||||||
|
let playlistMediaItemsForeignKeys = await queryInterface.sequelize.query(`PRAGMA foreign_key_list(playlistMediaItems);`)
|
||||||
|
expect(playlistMediaItemsForeignKeys).to.deep.equal([{ id: 0, seq: 0, table: 'playlists', from: 'playlistId', to: 'id', on_update: 'NO ACTION', on_delete: 'NO ACTION', match: 'NONE' }])
|
||||||
|
|
||||||
|
let mediaProgressesForeignKeys = await queryInterface.sequelize.query(`PRAGMA foreign_key_list(mediaProgresses);`)
|
||||||
|
expect(mediaProgressesForeignKeys).to.deep.equal([{ id: 0, seq: 0, table: 'users', from: 'userId', to: 'id', on_update: 'NO ACTION', on_delete: 'NO ACTION', match: 'NONE' }])
|
||||||
|
|
||||||
|
//
|
||||||
|
// Insert test data into tables
|
||||||
|
//
|
||||||
|
await queryInterface.bulkInsert('users', [{ id: 'e1a96857-48a8-43b6-8966-abc909c55b0f' }])
|
||||||
|
await queryInterface.bulkInsert('libraries', [{ id: 'a41a40e3-f516-40f5-810d-757ab668ebba' }])
|
||||||
|
await queryInterface.bulkInsert('libraryFolders', [{ id: 'b41a40e3-f516-40f5-810d-757ab668ebba' }])
|
||||||
|
await queryInterface.bulkInsert('playlists', [{ id: 'f41a40e3-f516-40f5-810d-757ab668ebba' }])
|
||||||
|
await queryInterface.bulkInsert('devices', [{ id: 'g41a40e3-f516-40f5-810d-757ab668ebba' }])
|
||||||
|
|
||||||
|
await queryInterface.bulkInsert('libraryItems', [{ id: 'c1a96857-48a8-43b6-8966-abc909c55b0f', libraryId: 'a41a40e3-f516-40f5-810d-757ab668ebba', libraryFolderId: 'b41a40e3-f516-40f5-810d-757ab668ebba' }])
|
||||||
|
await queryInterface.bulkInsert('feeds', [{ id: 'd1a96857-48a8-43b6-8966-abc909c55b0f', userId: 'e1a96857-48a8-43b6-8966-abc909c55b0f' }])
|
||||||
|
await queryInterface.bulkInsert('mediaItemShares', [{ id: 'h1a96857-48a8-43b6-8966-abc909c55b0f', userId: 'e1a96857-48a8-43b6-8966-abc909c55b0f' }])
|
||||||
|
await queryInterface.bulkInsert('playbackSessions', [{ id: 'f1a96857-48a8-43b6-8966-abc909c55b0x', userId: 'e1a96857-48a8-43b6-8966-abc909c55b0f', deviceId: 'g41a40e3-f516-40f5-810d-757ab668ebba', libraryId: 'a41a40e3-f516-40f5-810d-757ab668ebba' }])
|
||||||
|
await queryInterface.bulkInsert('playlistMediaItems', [{ id: 'i1a96857-48a8-43b6-8966-abc909c55b0f', playlistId: 'f41a40e3-f516-40f5-810d-757ab668ebba' }])
|
||||||
|
await queryInterface.bulkInsert('mediaProgresses', [{ id: 'j1a96857-48a8-43b6-8966-abc909c55b0f', userId: 'e1a96857-48a8-43b6-8966-abc909c55b0f' }])
|
||||||
|
|
||||||
|
//
|
||||||
|
// Query data before migration
|
||||||
|
//
|
||||||
|
const libraryItems = await queryInterface.sequelize.query('SELECT * FROM libraryItems;')
|
||||||
|
const feeds = await queryInterface.sequelize.query('SELECT * FROM feeds;')
|
||||||
|
const mediaItemShares = await queryInterface.sequelize.query('SELECT * FROM mediaItemShares;')
|
||||||
|
const playbackSessions = await queryInterface.sequelize.query('SELECT * FROM playbackSessions;')
|
||||||
|
const playlistMediaItems = await queryInterface.sequelize.query('SELECT * FROM playlistMediaItems;')
|
||||||
|
const mediaProgresses = await queryInterface.sequelize.query('SELECT * FROM mediaProgresses;')
|
||||||
|
|
||||||
|
//
|
||||||
|
// Run migration
|
||||||
|
//
|
||||||
|
await up({ context: { queryInterface, logger: Logger } })
|
||||||
|
|
||||||
|
//
|
||||||
|
// Validate that foreign key constraints are updated
|
||||||
|
//
|
||||||
|
libraryItemsForeignKeys = await queryInterface.sequelize.query(`PRAGMA foreign_key_list(libraryItems);`)
|
||||||
|
expect(libraryItemsForeignKeys).to.have.deep.members([
|
||||||
|
{ id: 0, seq: 0, table: 'libraryFolders', from: 'libraryFolderId', to: 'id', on_update: 'CASCADE', on_delete: 'SET NULL', match: 'NONE' },
|
||||||
|
{ id: 1, seq: 0, table: 'libraries', from: 'libraryId', to: 'id', on_update: 'CASCADE', on_delete: 'SET NULL', match: 'NONE' }
|
||||||
|
])
|
||||||
|
|
||||||
|
feedsForeignKeys = await queryInterface.sequelize.query(`PRAGMA foreign_key_list(feeds);`)
|
||||||
|
expect(feedsForeignKeys).to.deep.equal([{ id: 0, seq: 0, table: 'users', from: 'userId', to: 'id', on_update: 'CASCADE', on_delete: 'SET NULL', match: 'NONE' }])
|
||||||
|
|
||||||
|
mediaItemSharesForeignKeys = await queryInterface.sequelize.query(`PRAGMA foreign_key_list(mediaItemShares);`)
|
||||||
|
expect(mediaItemSharesForeignKeys).to.deep.equal([{ id: 0, seq: 0, table: 'users', from: 'userId', to: 'id', on_update: 'CASCADE', on_delete: 'SET NULL', match: 'NONE' }])
|
||||||
|
|
||||||
|
playbackSessionForeignKeys = await queryInterface.sequelize.query(`PRAGMA foreign_key_list(playbackSessions);`)
|
||||||
|
expect(playbackSessionForeignKeys).to.deep.equal([
|
||||||
|
{ id: 0, seq: 0, table: 'libraries', from: 'libraryId', to: 'id', on_update: 'CASCADE', on_delete: 'SET NULL', match: 'NONE' },
|
||||||
|
{ id: 1, seq: 0, table: 'devices', from: 'deviceId', to: 'id', on_update: 'CASCADE', on_delete: 'SET NULL', match: 'NONE' },
|
||||||
|
{ id: 2, seq: 0, table: 'users', from: 'userId', to: 'id', on_update: 'CASCADE', on_delete: 'SET NULL', match: 'NONE' }
|
||||||
|
])
|
||||||
|
|
||||||
|
playlistMediaItemsForeignKeys = await queryInterface.sequelize.query(`PRAGMA foreign_key_list(playlistMediaItems);`)
|
||||||
|
expect(playlistMediaItemsForeignKeys).to.deep.equal([{ id: 0, seq: 0, table: 'playlists', from: 'playlistId', to: 'id', on_update: 'CASCADE', on_delete: 'CASCADE', match: 'NONE' }])
|
||||||
|
|
||||||
|
mediaProgressesForeignKeys = await queryInterface.sequelize.query(`PRAGMA foreign_key_list(mediaProgresses);`)
|
||||||
|
expect(mediaProgressesForeignKeys).to.deep.equal([{ id: 0, seq: 0, table: 'users', from: 'userId', to: 'id', on_update: 'CASCADE', on_delete: 'CASCADE', match: 'NONE' }])
|
||||||
|
|
||||||
|
//
|
||||||
|
// Validate that data is not changed
|
||||||
|
//
|
||||||
|
const libraryItemsAfter = await queryInterface.sequelize.query('SELECT * FROM libraryItems;')
|
||||||
|
expect(libraryItemsAfter).to.deep.equal(libraryItems)
|
||||||
|
|
||||||
|
const feedsAfter = await queryInterface.sequelize.query('SELECT * FROM feeds;')
|
||||||
|
expect(feedsAfter).to.deep.equal(feeds)
|
||||||
|
|
||||||
|
const mediaItemSharesAfter = await queryInterface.sequelize.query('SELECT * FROM mediaItemShares;')
|
||||||
|
expect(mediaItemSharesAfter).to.deep.equal(mediaItemShares)
|
||||||
|
|
||||||
|
const playbackSessionsAfter = await queryInterface.sequelize.query('SELECT * FROM playbackSessions;')
|
||||||
|
expect(playbackSessionsAfter).to.deep.equal(playbackSessions)
|
||||||
|
|
||||||
|
const playlistMediaItemsAfter = await queryInterface.sequelize.query('SELECT * FROM playlistMediaItems;')
|
||||||
|
expect(playlistMediaItemsAfter).to.deep.equal(playlistMediaItems)
|
||||||
|
|
||||||
|
const mediaProgressesAfter = await queryInterface.sequelize.query('SELECT * FROM mediaProgresses;')
|
||||||
|
expect(mediaProgressesAfter).to.deep.equal(mediaProgresses)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should keep correct table foreign key constraints', async () => {
|
||||||
|
// Create tables with correct foreign key constraints: libraryItems, feeds, mediaItemShares, playbackSessions, playlistMediaItems, mediaProgresses
|
||||||
|
await queryInterface.sequelize.query('CREATE TABLE `libraryItems` (`id` UUID PRIMARY KEY, `libraryId` UUID REFERENCES `libraries` (`id`) ON DELETE SET NULL ON UPDATE CASCADE, `libraryFolderId` UUID REFERENCES `libraryFolders` (`id`) ON DELETE SET NULL ON UPDATE CASCADE);')
|
||||||
|
await queryInterface.sequelize.query('CREATE TABLE `feeds` (`id` UUID PRIMARY KEY, `userId` UUID REFERENCES `users` (`id`) ON DELETE SET NULL ON UPDATE CASCADE);')
|
||||||
|
await queryInterface.sequelize.query('CREATE TABLE `mediaItemShares` (`id` UUID PRIMARY KEY, `userId` UUID REFERENCES `users` (`id`) ON DELETE SET NULL ON UPDATE CASCADE);')
|
||||||
|
await queryInterface.sequelize.query('CREATE TABLE `playbackSessions` (`id` UUID PRIMARY KEY, `userId` UUID REFERENCES `users` (`id`) ON DELETE SET NULL ON UPDATE CASCADE, `deviceId` UUID REFERENCES `devices` (`id`) ON DELETE SET NULL ON UPDATE CASCADE, `libraryId` UUID REFERENCES `libraries` (`id`) ON DELETE SET NULL ON UPDATE CASCADE);')
|
||||||
|
await queryInterface.sequelize.query('CREATE TABLE `playlistMediaItems` (`id` UUID PRIMARY KEY, `playlistId` UUID REFERENCES `playlists` (`id`) ON DELETE CASCADE ON UPDATE CASCADE);')
|
||||||
|
await queryInterface.sequelize.query('CREATE TABLE `mediaProgresses` (`id` UUID PRIMARY KEY, `userId` UUID REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE CASCADE);')
|
||||||
|
|
||||||
|
//
|
||||||
|
// Insert test data into tables
|
||||||
|
//
|
||||||
|
await queryInterface.bulkInsert('users', [{ id: 'e1a96857-48a8-43b6-8966-abc909c55b0f' }])
|
||||||
|
await queryInterface.bulkInsert('libraries', [{ id: 'a41a40e3-f516-40f5-810d-757ab668ebba' }])
|
||||||
|
await queryInterface.bulkInsert('libraryFolders', [{ id: 'b41a40e3-f516-40f5-810d-757ab668ebba' }])
|
||||||
|
await queryInterface.bulkInsert('playlists', [{ id: 'f41a40e3-f516-40f5-810d-757ab668ebba' }])
|
||||||
|
await queryInterface.bulkInsert('devices', [{ id: 'g41a40e3-f516-40f5-810d-757ab668ebba' }])
|
||||||
|
|
||||||
|
await queryInterface.bulkInsert('libraryItems', [{ id: 'c1a96857-48a8-43b6-8966-abc909c55b0f', libraryId: 'a41a40e3-f516-40f5-810d-757ab668ebba', libraryFolderId: 'b41a40e3-f516-40f5-810d-757ab668ebba' }])
|
||||||
|
await queryInterface.bulkInsert('feeds', [{ id: 'd1a96857-48a8-43b6-8966-abc909c55b0f', userId: 'e1a96857-48a8-43b6-8966-abc909c55b0f' }])
|
||||||
|
await queryInterface.bulkInsert('mediaItemShares', [{ id: 'h1a96857-48a8-43b6-8966-abc909c55b0f', userId: 'e1a96857-48a8-43b6-8966-abc909c55b0f' }])
|
||||||
|
await queryInterface.bulkInsert('playbackSessions', [{ id: 'f1a96857-48a8-43b6-8966-abc909c55b0x', userId: 'e1a96857-48a8-43b6-8966-abc909c55b0f', deviceId: 'g41a40e3-f516-40f5-810d-757ab668ebba', libraryId: 'a41a40e3-f516-40f5-810d-757ab668ebba' }])
|
||||||
|
await queryInterface.bulkInsert('playlistMediaItems', [{ id: 'i1a96857-48a8-43b6-8966-abc909c55b0f', playlistId: 'f41a40e3-f516-40f5-810d-757ab668ebba' }])
|
||||||
|
await queryInterface.bulkInsert('mediaProgresses', [{ id: 'j1a96857-48a8-43b6-8966-abc909c55b0f', userId: 'e1a96857-48a8-43b6-8966-abc909c55b0f' }])
|
||||||
|
|
||||||
|
//
|
||||||
|
// Query data before migration
|
||||||
|
//
|
||||||
|
const libraryItems = await queryInterface.sequelize.query('SELECT * FROM libraryItems;')
|
||||||
|
const feeds = await queryInterface.sequelize.query('SELECT * FROM feeds;')
|
||||||
|
const mediaItemShares = await queryInterface.sequelize.query('SELECT * FROM mediaItemShares;')
|
||||||
|
const playbackSessions = await queryInterface.sequelize.query('SELECT * FROM playbackSessions;')
|
||||||
|
const playlistMediaItems = await queryInterface.sequelize.query('SELECT * FROM playlistMediaItems;')
|
||||||
|
const mediaProgresses = await queryInterface.sequelize.query('SELECT * FROM mediaProgresses;')
|
||||||
|
|
||||||
|
await up({ context: { queryInterface, logger: Logger } })
|
||||||
|
|
||||||
|
expect(loggerInfoStub.callCount).to.equal(14)
|
||||||
|
expect(loggerInfoStub.getCall(0).calledWith(sinon.match('[2.17.3 migration] UPGRADE BEGIN: 2.17.3-fk-constraints'))).to.be.true
|
||||||
|
expect(loggerInfoStub.getCall(1).calledWith(sinon.match('[2.17.3 migration] Updating libraryItems constraints'))).to.be.true
|
||||||
|
expect(loggerInfoStub.getCall(2).calledWith(sinon.match('[2.17.3 migration] No changes needed for libraryItems constraints'))).to.be.true
|
||||||
|
expect(loggerInfoStub.getCall(3).calledWith(sinon.match('[2.17.3 migration] Updating feeds constraints'))).to.be.true
|
||||||
|
expect(loggerInfoStub.getCall(4).calledWith(sinon.match('[2.17.3 migration] No changes needed for feeds constraints'))).to.be.true
|
||||||
|
expect(loggerInfoStub.getCall(5).calledWith(sinon.match('[2.17.3 migration] Updating mediaItemShares constraints'))).to.be.true
|
||||||
|
expect(loggerInfoStub.getCall(6).calledWith(sinon.match('[2.17.3 migration] No changes needed for mediaItemShares constraints'))).to.be.true
|
||||||
|
expect(loggerInfoStub.getCall(7).calledWith(sinon.match('[2.17.3 migration] Updating playbackSessions constraints'))).to.be.true
|
||||||
|
expect(loggerInfoStub.getCall(8).calledWith(sinon.match('[2.17.3 migration] No changes needed for playbackSessions constraints'))).to.be.true
|
||||||
|
expect(loggerInfoStub.getCall(9).calledWith(sinon.match('[2.17.3 migration] Updating playlistMediaItems constraints'))).to.be.true
|
||||||
|
expect(loggerInfoStub.getCall(10).calledWith(sinon.match('[2.17.3 migration] No changes needed for playlistMediaItems constraints'))).to.be.true
|
||||||
|
expect(loggerInfoStub.getCall(11).calledWith(sinon.match('[2.17.3 migration] Updating mediaProgresses constraints'))).to.be.true
|
||||||
|
expect(loggerInfoStub.getCall(12).calledWith(sinon.match('[2.17.3 migration] No changes needed for mediaProgresses constraints'))).to.be.true
|
||||||
|
expect(loggerInfoStub.getCall(13).calledWith(sinon.match('[2.17.3 migration] UPGRADE END: 2.17.3-fk-constraints'))).to.be.true
|
||||||
|
|
||||||
|
//
|
||||||
|
// Validate that data is not changed
|
||||||
|
//
|
||||||
|
const libraryItemsAfter = await queryInterface.sequelize.query('SELECT * FROM libraryItems;')
|
||||||
|
expect(libraryItemsAfter).to.deep.equal(libraryItems)
|
||||||
|
|
||||||
|
const feedsAfter = await queryInterface.sequelize.query('SELECT * FROM feeds;')
|
||||||
|
expect(feedsAfter).to.deep.equal(feeds)
|
||||||
|
|
||||||
|
const mediaItemSharesAfter = await queryInterface.sequelize.query('SELECT * FROM mediaItemShares;')
|
||||||
|
expect(mediaItemSharesAfter).to.deep.equal(mediaItemShares)
|
||||||
|
|
||||||
|
const playbackSessionsAfter = await queryInterface.sequelize.query('SELECT * FROM playbackSessions;')
|
||||||
|
expect(playbackSessionsAfter).to.deep.equal(playbackSessions)
|
||||||
|
|
||||||
|
const playlistMediaItemsAfter = await queryInterface.sequelize.query('SELECT * FROM playlistMediaItems;')
|
||||||
|
expect(playlistMediaItemsAfter).to.deep.equal(playlistMediaItems)
|
||||||
|
|
||||||
|
const mediaProgressesAfter = await queryInterface.sequelize.query('SELECT * FROM mediaProgresses;')
|
||||||
|
expect(mediaProgressesAfter).to.deep.equal(mediaProgresses)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user