Compare commits

...

8 Commits

Author SHA1 Message Date
advplyr 9f29b245d7 Version bump 1.7.2 2022-03-05 15:46:06 -06:00
advplyr ecf62c5443 Add:Experimental direct play support 2022-03-05 15:37:30 -06:00
advplyr eb82d9c300 Add:Sleep timer #165 2022-03-05 12:30:46 -06:00
advplyr 45582343b8 Fix:Backups table UI 2022-03-05 10:23:30 -06:00
advplyr 6a4d3a55b1 Fix:User last activity 2022-03-05 10:10:42 -06:00
advplyr 95bacce5e5 Add:Start/complete date on audiobook page #383, Fix local hls stream url 2022-03-04 18:44:20 -06:00
advplyr 408775a25a Add:Library media type selection and library icons 2022-03-03 19:03:34 -06:00
advplyr d3a8ecc8d1 Remove unnecessary env 2022-03-01 17:59:47 -06:00
25 changed files with 653 additions and 92 deletions
+42 -26
View File
@@ -1,21 +1,27 @@
<template>
<div class="w-full -mt-6">
<div class="w-full relative mb-1">
<div v-if="chapters.length" class="hidden md:flex absolute right-20 top-0 bottom-0 h-full items-end">
<div class="cursor-pointer text-gray-300" @mousedown.prevent @mouseup.prevent @click.stop="showChapters">
<div class="absolute -top-10 md:top-0 right-0 md:right-2 flex items-center h-full">
<controls-volume-control ref="volumeControl" v-model="volume" @input="setVolume" class="mx-2 hidden md:block" />
<div class="cursor-pointer text-gray-300 mx-1 md:mx-2" @mousedown.prevent @mouseup.prevent @click.stop="$emit('showSleepTimer')">
<span v-if="!sleepTimerSet" class="material-icons" style="font-size: 1.7rem">snooze</span>
<div v-else class="flex items-center">
<span class="material-icons text-lg text-warning">snooze</span>
<p class="text-xl text-warning font-mono font-semibold text-center px-0.5 pb-0.5" style="min-width: 30px">{{ sleepTimerRemainingString }}</p>
</div>
</div>
<div class="cursor-pointer text-gray-300 mx-1 md:mx-2" @mousedown.prevent @mouseup.prevent @click.stop="$emit('showBookmarks')">
<span class="material-icons" style="font-size: 1.7rem">{{ bookmarks.length ? 'bookmarks' : 'bookmark_border' }}</span>
</div>
<div v-if="chapters.length" class="cursor-pointer text-gray-300 mx-1 md:mx-2" @mousedown.prevent @mouseup.prevent @click.stop="showChapters">
<span class="material-icons text-3xl">format_list_bulleted</span>
</div>
</div>
<div class="absolute top-0 bottom-0 h-full hidden md:flex items-end" :class="chapters.length ? ' right-32' : 'right-20'">
<div class="cursor-pointer text-gray-300" @mousedown.prevent @mouseup.prevent @click.stop="showBookmarks">
<span class="material-icons" style="font-size: 1.7rem">{{ bookmarks.length ? 'bookmarks' : 'bookmark_border' }}</span>
</div>
</div>
<div class="absolute top-0 bottom-0 h-full hidden md:flex items-end" :class="chapters.length ? ' right-44' : 'right-32'">
<controls-volume-control ref="volumeControl" v-model="volume" @input="setVolume" />
</div>
<div class="flex pb-4 md:pb-2">
<div class="flex pt-4 pb-2 md:pt-0 md:pb-2">
<div class="flex-grow" />
<template v-if="!loading">
<div class="cursor-pointer flex items-center justify-center text-gray-300 mr-8" @mousedown.prevent @mouseup.prevent @click.stop="restart">
@@ -91,7 +97,9 @@ export default {
bookmarks: {
type: Array,
default: () => []
}
},
sleepTimerSet: Boolean,
sleepTimerRemaining: Number
},
data() {
return {
@@ -106,10 +114,23 @@ export default {
showChaptersModal: false,
currentTime: 0,
trackOffsetLeft: 16, // Track is 16px from edge
duration: 0
duration: 0,
chapterTicks: []
}
},
computed: {
sleepTimerRemainingString() {
var rounded = Math.round(this.sleepTimerRemaining)
if (rounded < 90) {
return `${rounded}s`
}
var minutesRounded = Math.round(rounded / 60)
if (minutesRounded < 90) {
return `${minutesRounded}m`
}
var hoursRounded = Math.round(minutesRounded / 60)
return `${hoursRounded}h`
},
token() {
return this.$store.getters['user/getToken']
},
@@ -118,7 +139,6 @@ export default {
},
timeRemainingPretty() {
if (this.timeRemaining < 0) {
console.warn('Time remaining < 0', this.duration, this.currentTime, this.timeRemaining)
return this.$secondsToTimestamp(this.timeRemaining * -1)
}
return '-' + this.$secondsToTimestamp(this.timeRemaining)
@@ -127,15 +147,6 @@ export default {
if (!this.duration) return 0
return Math.round((100 * this.currentTime) / this.duration)
},
chapterTicks() {
return this.chapters.map((chap) => {
var perc = chap.start / this.duration
return {
title: chap.title,
left: perc * this.trackWidth
}
})
},
currentChapter() {
return this.chapters.find((chapter) => chapter.start <= this.currentTime && this.currentTime < chapter.end)
},
@@ -149,6 +160,14 @@ export default {
methods: {
setDuration(duration) {
this.duration = duration
this.chapterTicks = this.chapters.map((chap) => {
var perc = chap.start / this.duration
return {
title: chap.title,
left: perc * this.trackWidth
}
})
},
setCurrentTime(time) {
this.currentTime = time
@@ -330,9 +349,6 @@ export default {
if (!this.chapters.length) return
this.showChaptersModal = !this.showChaptersModal
},
showBookmarks() {
this.$emit('showBookmarks', this.currentTime)
},
init() {
this.playbackRate = this.$store.getters['user/getUserSetting']('playbackRate') || 1
this.$emit('setPlaybackRate', this.playbackRate)
+76 -6
View File
@@ -1,5 +1,5 @@
<template>
<div v-if="streamAudiobook" id="streamContainer" class="w-full fixed bottom-0 left-0 right-0 h-48 sm:h-44 md:h-40 z-40 bg-primary px-4 pb-4 pt-2">
<div v-if="streamAudiobook" id="streamContainer" class="w-full fixed bottom-0 left-0 right-0 h-48 sm:h-44 md:h-40 z-40 bg-primary px-4 pb-1 md:pb-4 pt-2">
<nuxt-link :to="`/audiobook/${streamAudiobook.id}`" class="absolute left-4 cursor-pointer" :style="{ top: bookCoverPosTop + 'px' }">
<covers-book-cover :audiobook="streamAudiobook" :width="bookCoverWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" />
</nuxt-link>
@@ -22,12 +22,31 @@
</div>
</div>
<div class="flex-grow" />
<span class="material-icons p-4 cursor-pointer" @click="closePlayer">close</span>
<span class="material-icons px-2 py-1 md:p-4 cursor-pointer" @click="closePlayer">close</span>
</div>
<audio-player ref="audioPlayer" :chapters="chapters" :paused="!isPlaying" :loading="playerLoading" :bookmarks="bookmarks" @playPause="playPause" @jumpForward="jumpForward" @jumpBackward="jumpBackward" @setVolume="setVolume" @setPlaybackRate="setPlaybackRate" @seek="seek" @close="closePlayer" @showBookmarks="showBookmarks" />
<audio-player
ref="audioPlayer"
:chapters="chapters"
:paused="!isPlaying"
:loading="playerLoading"
:bookmarks="bookmarks"
:sleep-timer-set="sleepTimerSet"
:sleep-timer-remaining="sleepTimerRemaining"
@playPause="playPause"
@jumpForward="jumpForward"
@jumpBackward="jumpBackward"
@setVolume="setVolume"
@setPlaybackRate="setPlaybackRate"
@seek="seek"
@close="closePlayer"
@showBookmarks="showBookmarks"
@showSleepTimer="showSleepTimerModal = true"
/>
<modals-bookmarks-modal v-model="showBookmarksModal" :bookmarks="bookmarks" :audiobook-id="bookmarkAudiobookId" :current-time="bookmarkCurrentTime" @select="selectBookmark" />
<modals-sleep-timer-modal v-model="showSleepTimerModal" :timer-set="sleepTimerSet" :timer-time="sleepTimerTime" :remaining="sleepTimerRemaining" @set="setSleepTimer" @cancel="cancelSleepTimer" @increment="incrementSleepTimer" @decrement="decrementSleepTimer" />
</div>
</template>
@@ -44,10 +63,18 @@ export default {
bookmarkAudiobookId: null,
playerLoading: false,
isPlaying: false,
currentTime: 0
currentTime: 0,
showSleepTimerModal: false,
sleepTimerSet: false,
sleepTimerTime: 0,
sleepTimerRemaining: 0,
sleepTimer: null
}
},
computed: {
showExperimentalFeatures() {
return this.$store.state.showExperimentalFeatures
},
coverAspectRatio() {
return this.$store.getters['getServerSetting']('coverAspectRatio')
},
@@ -111,6 +138,49 @@ export default {
}
},
methods: {
setSleepTimer(seconds) {
this.sleepTimerSet = true
this.sleepTimerTime = seconds
this.sleepTimerRemaining = seconds
this.runSleepTimer()
this.showSleepTimerModal = false
},
runSleepTimer() {
var lastTick = Date.now()
clearInterval(this.sleepTimer)
this.sleepTimer = setInterval(() => {
var elapsed = Date.now() - lastTick
lastTick = Date.now()
this.sleepTimerRemaining -= elapsed / 1000
if (this.sleepTimerRemaining <= 0) {
this.clearSleepTimer()
this.playerHandler.pause()
this.$toast.info('Sleep Timer Done.. zZzzZz')
}
}, 1000)
},
cancelSleepTimer() {
this.showSleepTimerModal = false
this.clearSleepTimer()
},
clearSleepTimer() {
clearInterval(this.sleepTimer)
this.sleepTimerRemaining = 0
this.sleepTimer = null
this.sleepTimerSet = false
},
incrementSleepTimer(amount) {
if (!this.sleepTimerSet) return
this.sleepTimerRemaining += amount
},
decrementSleepTimer(amount) {
if (this.sleepTimerRemaining < amount) {
this.sleepTimerRemaining = 3
return
}
this.sleepTimerRemaining = Math.max(0, this.sleepTimerRemaining - amount)
},
playPause() {
this.playerHandler.playPause()
},
@@ -146,9 +216,9 @@ export default {
this.$refs.audioPlayer.setBufferTime(buffertime)
}
},
showBookmarks(currentTime) {
showBookmarks() {
this.bookmarkAudiobookId = this.audiobookId
this.bookmarkCurrentTime = currentTime
this.bookmarkCurrentTime = this.currentTime
this.showBookmarksModal = true
},
selectBookmark(bookmark) {
@@ -0,0 +1,23 @@
<template>
<svg fill="currentColor" class="h-full w-full p-px" viewBox="0 0 1978.03 2349.44">
<path
d="M2519.5,1438.39c-12.13-10.1-31-25-56.57-42.62V1197.31c0-505.94-410.15-916.09-916.1-916.09h0c-505.94,0-916.09,410.15-916.09,916.09v198.46c-25.57,17.66-44.44,32.52-56.57,42.62a45.45,45.45,0,0,0-16.35,34.95v237.74a45.45,45.45,0,0,0,16.35,35c28.28,23.54,93.18,72.92,194.22,123.55v23.11c0,62.32,40.21,112.85,89.8,112.85h0c49.59,0,89.8-50.53,89.8-112.85V1322.51c0-62.33-40.21-112.86-89.8-112.86h0c-47.51,0-86.4,46.38-89.58,105.07l-.22.11V1197.31c0-429.92,348.52-778.43,778.44-778.43h0c429.92,0,778.44,348.51,778.44,778.43v117.52l-.22-.11c-3.18-58.69-42.06-105.07-89.58-105.07h0c-49.59,0-89.79,50.53-89.79,112.86v570.18c0,62.32,40.2,112.85,89.79,112.85h0c49.6,0,89.8-50.53,89.8-112.85v-23.11c101.05-50.63,165.95-100,194.23-123.55a45.48,45.48,0,0,0,16.35-35V1473.34A45.48,45.48,0,0,0,2519.5,1438.39Z"
transform="translate(-557.82 -281.22)"
/>
<path d="M1227.4,2429.63a108.47,108.47,0,0,0,108.47-108.47V1106.56A108.47,108.47,0,0,0,1227.4,998.08H1115.33a108.48,108.48,0,0,0-108.48,108.48v1214.6a108.47,108.47,0,0,0,108.48,108.47ZM1047.75,1289.38H1295v25.83H1047.75Z" transform="translate(-557.82 -281.22)" />
<path d="M1602.87,2429.63a108.47,108.47,0,0,0,108.47-108.47V1106.56a108.47,108.47,0,0,0-108.47-108.48H1490.8a108.48,108.48,0,0,0-108.48,108.48v1214.6a108.47,108.47,0,0,0,108.48,108.47ZM1423.22,1289.38h247.22v25.83H1423.22Z" transform="translate(-557.82 -281.22)" />
<path d="M1978.34,2429.63a108.47,108.47,0,0,0,108.47-108.47V1106.56a108.47,108.47,0,0,0-108.47-108.48H1866.27a108.48,108.48,0,0,0-108.48,108.48v1214.6a108.47,108.47,0,0,0,108.48,108.47ZM1798.69,1289.38h247.22v25.83H1798.69Z" transform="translate(-557.82 -281.22)" />
<rect x="180.05" y="2185.95" width="1617.93" height="163.49" rx="81.74" />
</svg>
</template>
<script>
export default {
data() {
return {}
},
computed: {},
methods: {},
mounted() {}
}
</script>
+16
View File
@@ -0,0 +1,16 @@
<template>
<svg fill="currentColor" viewBox="0 0 24 24">
<path fill="currentColor" d="M9 3V18H12V3H9M12 5L16 18L19 17L15 4L12 5M5 5V18H8V5H5M3 19V21H21V19H3Z" />
</svg>
</template>
<script>
export default {
data() {
return {}
},
computed: {},
methods: {},
mounted() {}
}
</script>
+16
View File
@@ -0,0 +1,16 @@
<template>
<svg viewBox="0 0 24 24">
<path fill="currentColor" d="M6,19L9,15.14L11.14,17.72L14.14,13.86L18,19H6M6,4H11V12L8.5,10.5L6,12M18,2H6A2,2 0 0,0 4,4V20A2,2 0 0,0 6,22H18A2,2 0 0,0 20,20V4A2,2 0 0,0 18,2Z" />
</svg>
</template>
<script>
export default {
data() {
return {}
},
computed: {},
methods: {},
mounted() {}
}
</script>
+16
View File
@@ -0,0 +1,16 @@
<template>
<svg class="p-px" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4m0 5c0 2.21-3.582 4-8 4s-8-1.79-8-4" />
</svg>
</template>
<script>
export default {
data() {
return {}
},
computed: {},
methods: {},
mounted() {}
}
</script>
+19
View File
@@ -0,0 +1,19 @@
<template>
<svg class="p-px" viewBox="0 0 122.877 120.596">
<path
fill="currentColor"
d="M68.925,69.906v50.689H53.953V69.906c-4.918-2.662-8.259-7.867-8.259-13.854 c0-8.694,7.05-15.744,15.745-15.744c8.694,0,15.745,7.05,15.745,15.744C77.184,62.039,73.843,67.244,68.925,69.906L68.925,69.906z M39.32,11.165c2.916-1.438,4.111-4.969,2.673-7.882c-1.438-2.914-4.966-4.111-7.88-2.674C22.213,6.479,12.958,16.19,7.11,27.625 c-4.32,8.445-6.783,17.842-7.08,27.325c-0.299,9.563,1.587,19.223,5.973,28.114c5.401,10.953,14.558,20.695,28.039,27.592 c2.889,1.477,6.429,0.33,7.905-2.559c1.477-2.889,0.331-6.428-2.558-7.904c-11.037-5.645-18.486-13.525-22.833-22.334 c-3.506-7.111-5.014-14.857-4.774-22.539c0.243-7.757,2.256-15.442,5.79-22.348C22.304,23.721,29.76,15.879,39.32,11.165 L39.32,11.165z M88.765,0.608c-2.914-1.438-6.443-0.24-7.881,2.674c-1.438,2.914-0.242,6.445,2.674,7.882 c9.561,4.715,17.017,12.556,21.747,21.808c3.533,6.905,5.547,14.59,5.789,22.348c0.24,7.682-1.268,15.428-4.773,22.539 c-4.347,8.809-11.796,16.689-22.833,22.334c-2.889,1.477-4.034,5.016-2.558,7.904c1.476,2.889,5.016,4.035,7.905,2.559 c13.48-6.896,22.638-16.639,28.039-27.592c4.386-8.891,6.272-18.551,5.973-28.114c-0.297-9.483-2.76-18.88-7.079-27.325 C109.919,16.19,100.665,6.479,88.765,0.608L88.765,0.608z M82.791,26.505c-2.195-1.581-5.256-1.082-6.837,1.113 c-1.58,2.195-1.082,5.256,1.113,6.837c0.885,0.637,1.753,1.352,2.604,2.134c4.971,4.583,7.919,10.694,8.538,17.16 c0.626,6.524-1.111,13.437-5.518,19.552c-0.748,1.039-1.61,2.092-2.585,3.15c-1.835,1.992-1.708,5.098,0.287,6.932 c1.994,1.834,5.099,1.705,6.933-0.287c1.18-1.279,2.286-2.641,3.315-4.072c5.862-8.139,8.166-17.4,7.322-26.197 c-0.848-8.853-4.871-17.208-11.648-23.457C85.249,28.387,84.074,27.431,82.791,26.505L82.791,26.505z M45.81,34.458 c2.195-1.581,2.694-4.642,1.113-6.837c-1.581-2.195-4.642-2.694-6.837-1.114c-1.284,0.926-2.458,1.882-3.524,2.864 c-6.778,6.25-10.801,14.604-11.649,23.457c-0.844,8.796,1.46,18.06,7.323,26.199c1.031,1.43,2.136,2.791,3.315,4.07 c1.834,1.992,4.939,2.121,6.932,0.287c1.996-1.834,2.123-4.939,0.288-6.932c-0.975-1.059-1.837-2.111-2.585-3.15 c-4.406-6.115-6.144-13.027-5.518-19.551c0.619-6.465,3.567-12.577,8.538-17.16C44.058,35.81,44.926,35.095,45.81,34.458 L45.81,34.458z"
/>
</svg>
</template>
<script>
export default {
data() {
return {}
},
computed: {},
methods: {},
mounted() {}
}
</script>
@@ -0,0 +1,115 @@
<template>
<modals-modal v-model="show" name="sleep-timer" :width="350" :height="'unset'">
<template #outer>
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden pointer-events-none">
<p class="font-book text-3xl text-white truncate pointer-events-none">Sleep Timer</p>
</div>
</template>
<div ref="container" class="w-full rounded-lg bg-primary box-shadow-md overflow-y-auto overflow-x-hidden" style="max-height: 80vh">
<div v-if="!timerSet" class="w-full">
<template v-for="time in sleepTimes">
<div :key="time.text" class="flex items-center px-6 py-3 justify-center cursor-pointer hover:bg-bg relative" @click="setTime(time)">
<p class="text-xl text-center">{{ time.text }}</p>
</div>
</template>
</div>
<div v-else class="w-full p-4">
<div class="mb-4 flex items-center justify-center">
<ui-btn :padding-x="2" small :disabled="remaining < 30 * 60" class="flex items-center mr-4" @click="decrement(30 * 60)">
<span class="material-icons text-lg">remove</span>
<span class="pl-1 text-base font-mono">30m</span>
</ui-btn>
<ui-icon-btn icon="remove" @click="decrement(60 * 5)" />
<p class="mx-6 text-2xl font-mono">{{ $secondsToTimestamp(remaining) }}</p>
<ui-icon-btn icon="add" @click="increment(60 * 5)" />
<ui-btn :padding-x="2" small class="flex items-center ml-4" @click="increment(30 * 60)">
<span class="material-icons text-lg">add</span>
<span class="pl-1 text-base font-mono">30m</span>
</ui-btn>
</div>
<ui-btn class="w-full" @click="$emit('cancel')">Cancel</ui-btn>
</div>
</div>
</modals-modal>
</template>
<script>
export default {
props: {
value: Boolean,
timerSet: Boolean,
timerTime: Number,
remaining: Number
},
data() {
return {
sleepTimes: [
{
seconds: 10,
text: '10 seconds'
},
{
seconds: 60 * 5,
text: '5 minutes'
},
{
seconds: 60 * 30,
text: '30 minutes'
},
{
seconds: 60 * 60,
text: '60 minutes'
},
{
seconds: 60 * 90,
text: '90 minutes'
},
{
seconds: 60 * 120,
text: '2 hours'
},
{
seconds: 60 * 180,
text: '3 hours'
}
]
}
},
watch: {
show(newVal) {
if (newVal) {
}
}
},
computed: {
show: {
get() {
return this.value
},
set(val) {
this.$emit('input', val)
}
}
},
methods: {
setTime(time) {
this.$emit('set', time.seconds)
},
increment(amount) {
this.$emit('increment', amount)
},
decrement(amount) {
if (amount > this.remaining) {
if (this.remaining > 60) amount = 60
else amount = 5
}
this.$emit('decrement', amount)
}
}
}
</script>
@@ -6,11 +6,14 @@
</div>
<div v-if="!showDirectoryPicker" class="w-full h-full py-4">
<div class="flex flex-wrap -mx-1">
<div class="w-full md:w-2/3 px-1">
<div class="flex flex-wrap md:flex-nowrap -mx-1">
<div class="w-full md:flex-grow px-1 py-1 md:py-0">
<ui-text-input-with-label v-model="name" label="Library Name" />
</div>
<div class="w-full md:w-1/3 px-1">
<div class="w-1/2 md:w-72 px-1 py-1 md:py-0">
<ui-media-type-picker v-model="mediaType" />
</div>
<div class="w-1/2 md:w-72 px-1 py-1 md:py-0">
<ui-dropdown v-model="provider" :items="providers" label="Metadata Provider" small />
</div>
</div>
@@ -18,7 +21,6 @@
<div class="w-full py-4">
<p class="px-1 text-sm font-semibold">Folders</p>
<div v-for="(folder, index) in folders" :key="index" class="w-full flex items-center py-1 px-2">
<!-- <ui-text-input :value="folder.fullPath" type="text" class="w-full" /> -->
<span class="material-icons bg-opacity-50 mr-2 text-yellow-200" style="font-size: 1.2rem">folder</span>
<ui-editable-text v-model="folder.fullPath" type="text" class="w-full" />
<span v-show="folders.length > 1" class="material-icons ml-2 cursor-pointer hover:text-error" @click="removeFolder(folder)">close</span>
@@ -60,6 +62,7 @@ export default {
return {
name: '',
provider: '',
mediaType: '',
folders: [],
showDirectoryPicker: false,
disableWatcher: false
@@ -80,7 +83,7 @@ export default {
var newfolderpaths = this.folderPaths.join(',')
var origfolderpaths = this.library.folders.map((f) => f.fullPath).join(',')
return newfolderpaths === origfolderpaths && this.name === this.library.name && this.provider === this.library.provider && this.disableWatcher === this.library.disableWatcher
return newfolderpaths === origfolderpaths && this.name === this.library.name && this.provider === this.library.provider && this.disableWatcher === this.library.disableWatcher && this.mediaType === this.library.mediaType
},
providers() {
return this.$store.state.scanners.providers
@@ -103,6 +106,7 @@ export default {
this.provider = this.library ? this.library.provider : ''
this.folders = this.library ? this.library.folders.map((p) => ({ ...p })) : []
this.disableWatcher = this.library ? !!this.library.disableWatcher : false
this.mediaType = this.library ? this.library.mediaType : 'default'
this.showDirectoryPicker = false
},
selectFolder(fullPath) {
@@ -129,6 +133,8 @@ export default {
name: this.name,
provider: this.provider,
folders: this.folders,
mediaType: this.mediaType,
icon: this.mediaType,
disableWatcher: this.disableWatcher
}
@@ -163,6 +169,8 @@ export default {
name: this.name,
provider: this.provider,
folders: this.folders,
mediaType: this.mediaType,
icon: this.mediaType,
disableWatcher: this.disableWatcher
}
@@ -1,9 +1,7 @@
<template>
<div class="w-full px-4 h-12 border border-white border-opacity-10 flex items-center relative -mt-px" :class="selected ? 'bg-primary bg-opacity-50' : 'hover:bg-primary hover:bg-opacity-25'" @mouseover="mouseover = true" @mouseleave="mouseover = false">
<div v-show="selected" class="absolute top-0 left-0 h-full w-0.5 bg-warning z-10" />
<svg v-if="!libraryScan" xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-white" :class="isHovering ? 'text-opacity-90' : 'text-opacity-50'" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4m0 5c0 2.21-3.582 4-8 4s-8-1.79-8-4" />
</svg>
<widgets-library-icon v-if="!libraryScan" :icon="library.icon" :size="6" class="text-white" :class="isHovering ? 'text-opacity-90' : 'text-opacity-50'" />
<svg v-else viewBox="0 0 24 24" class="h-6 w-6 text-white text-opacity-50 animate-spin">
<path fill="currentColor" d="M12,4V2A10,10 0 0,0 2,12H4A8,8 0 0,1 12,4Z" />
</svg>
+5 -5
View File
@@ -9,21 +9,21 @@
<table id="backups">
<tr>
<th>File</th>
<th class="hidden sm:block w-32 md:w-56">Datetime</th>
<th class="hidden sm:block w-20 md:w-28">Size</th>
<th class="hidden sm:table-cell w-32 md:w-56">Datetime</th>
<th class="hidden sm:table-cell w-20 md:w-28">Size</th>
<th class="w-36"></th>
</tr>
<tr v-for="backup in backups" :key="backup.id">
<td>
<p class="truncate text-xs sm:text-sm md:text-base">/{{ backup.path.replace(/\\/g, '/') }}</p>
</td>
<td class="hidden sm:block font-sans text-base">{{ backup.datePretty }}</td>
<td class="hidden sm:block font-mono md:text-base text-xs">{{ $bytesPretty(backup.fileSize) }}</td>
<td class="hidden sm:table-cell font-sans text-base">{{ backup.datePretty }}</td>
<td class="hidden sm:table-cell font-mono md:text-base text-xs">{{ $bytesPretty(backup.fileSize) }}</td>
<td>
<div class="w-full flex flex-row items-center justify-center">
<ui-btn small color="primary" @click="applyBackup(backup)">Apply</ui-btn>
<a :href="`/metadata/${backup.path.replace(/%/g, '%25').replace(/#/g, '%23')}?token=${userToken}`" class="mx-1 pt-0.5 hover:text-opacity-100 text-opacity-70 text-white" download><span class="material-icons text-xl">download</span></a>
<a :href="`/metadata/${backup.path.replace(/%/g, '%25').replace(/#/g, '%23')}?token=${userToken}`" class="mx-1 pt-1 hover:text-opacity-100 text-opacity-70 text-white" download><span class="material-icons text-xl">download</span></a>
<span class="material-icons text-xl hover:text-error hover:text-opacity-100 text-opacity-70 text-white cursor-pointer mx-1" @click="deleteBackupClick(backup)">delete</span>
</div>
+4 -2
View File
@@ -87,9 +87,11 @@ export default {
},
methods: {
getLastRead(audiobooks) {
var abs = Object.values(audiobooks)
var abs = Object.values(audiobooks).filter((ab) => {
return ab.progress > 0
})
if (abs.length) {
abs = abs.sort((a, b) => a.lastUpdate - b.lastUpdate)
abs = abs.sort((a, b) => b.lastUpdate - a.lastUpdate)
// Book object is attached on request
if (abs[0].book) return abs[0].book.title
return abs[0].audiobookTitle ? abs[0].audiobookTitle : null
+1 -1
View File
@@ -12,7 +12,7 @@
<template v-for="library in librariesFiltered">
<li :key="library.id" class="text-gray-100 select-none relative py-2 cursor-pointer hover:bg-black-400" id="listbox-option-0" role="option" @click="selectLibrary(library)">
<div class="flex items-center px-3">
<widgets-library-icon :icon="currentLibraryIcon" class="mr-2" />
<widgets-library-icon :icon="library.icon" class="mr-2" />
<span class="font-normal block truncate font-sans text-sm">{{ library.name }}</span>
</div>
</li>
+104
View File
@@ -0,0 +1,104 @@
<template>
<div class="relative w-full h-9" v-click-outside="clickOutsideObj">
<p class="text-sm font-semibold">{{ label }}</p>
<button type="button" :disabled="disabled" class="relative h-full w-full border border-gray-600 rounded shadow-sm pl-3 pr-3 text-left focus:outline-none cursor-pointer bg-primary text-gray-100 hover:text-gray-200" aria-haspopup="listbox" aria-expanded="true" @click.stop.prevent="clickShowMenu">
<span class="flex items-center">
<widgets-library-icon :icon="selected" class="mr-2" />
<span class="block truncate text-sm">{{ selectedName }}</span>
</span>
<span class="ml-3 absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
<span class="material-icons text-gray-100">expand_more</span>
</span>
</button>
<transition name="menu">
<ul v-show="showMenu" class="absolute z-10 -mt-px w-full bg-primary border border-black-200 shadow-lg max-h-56 rounded-b-md py-1 ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm" tabindex="-1" role="listbox">
<template v-for="type in types">
<li :key="type.id" class="text-gray-100 select-none relative py-2 cursor-pointer hover:bg-black-400" id="listbox-option-0" role="option" @click="select(type)">
<div class="flex items-center px-3">
<widgets-library-icon :icon="type.id" class="mr-2" />
<span class="font-normal block truncate font-sans text-sm">{{ type.name }}</span>
</div>
</li>
</template>
</ul>
</transition>
</div>
</template>
<script>
export default {
props: {
value: String,
disabled: Boolean,
label: {
type: String,
default: 'Media Type'
}
},
data() {
return {
clickOutsideObj: {
handler: this.clickedOutside,
events: ['mousedown'],
isActive: true
},
showMenu: false,
types: [
{
id: 'default',
name: 'Default'
},
{
id: 'audiobooks',
name: 'Audiobooks'
},
{
id: 'books',
name: 'Books'
},
{
id: 'podcasts',
name: 'Podcasts'
},
{
id: 'comics',
name: 'Comics'
}
]
}
},
computed: {
selected: {
get() {
return this.value || 'default'
},
set(val) {
this.$emit('input', val)
}
},
selectedItem() {
return this.types.find((t) => t.id === this.selected)
},
selectedName() {
return this.selectedItem ? this.selectedItem.name : 'Default'
}
},
methods: {
clickShowMenu() {
if (this.disabled) return
this.showMenu = !this.showMenu
},
clickedOutside() {
this.showMenu = false
},
select(type) {
if (this.disabled) return
this.selected = type.id
this.showMenu = false
}
},
mounted() {}
}
</script>
+13 -6
View File
@@ -1,20 +1,27 @@
<template>
<div class="h-4 w-4">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4m0 5c0 2.21-3.582 4-8 4s-8-1.79-8-4" />
</svg>
<div :class="`h-${size} w-${size}`">
<component :is="iconComponentName" />
</div>
</template>
<script>
export default {
props: {
icon: String
icon: String,
size: {
type: Number,
default: 5
}
},
data() {
return {}
},
computed: {},
computed: {
iconComponentName() {
if (this.icon === 'default') return `icons-database-svg`
return `icons-${this.icon}-svg`
}
},
methods: {},
mounted() {}
}
+1 -2
View File
@@ -7,8 +7,7 @@ module.exports = {
dev: process.env.NODE_ENV !== 'production',
env: {
serverUrl: process.env.NODE_ENV === 'production' ? '' : 'http://localhost:3333',
chromecastReceiver: 'FD1F76C5',
baseUrl: process.env.BASE_URL || 'http://0.0.0.0'
chromecastReceiver: 'FD1F76C5'
},
// rootDir: process.env.NODE_ENV !== 'production' ? 'client/' : '',
telemetry: false,
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "audiobookshelf-client",
"version": "1.7.1",
"version": "1.7.2",
"description": "Audiobook manager and player",
"main": "index.js",
"scripts": {
+14 -5
View File
@@ -47,7 +47,6 @@
<nuxt-link :key="narrator" :to="`/library/${libraryId}/bookshelf?filter=narrators.${$encode(narrator)}`" class="hover:underline">{{ narrator }}</nuxt-link
><span :key="index" v-if="index < narrators.length - 1">,&nbsp;</span>
</template>
<!-- <nuxt-link :to="`/library/${libraryId}/bookshelf?filter=narrators.${$encode(narrator)}`" class="hover:underline">{{ narrator }}</nuxt-link> -->
</div>
</div>
<div v-if="publishYear" class="flex py-0.5">
@@ -95,9 +94,13 @@
<p class="ml-4">Book has no audio tracks but has valid ebook files. The e-reader is experimental and can be turned on in config.</p>
</div>
<div v-if="progressPercent > 0 && progressPercent < 1" class="px-4 py-2 mt-4 bg-primary text-sm font-semibold rounded-md text-gray-200 relative max-w-max mx-auto md:mx-0" :class="resettingProgress ? 'opacity-25' : ''">
<p class="leading-6">Your Progress: {{ Math.round(progressPercent * 100) }}%</p>
<p class="text-gray-400 text-xs">{{ $elapsedPretty(userTimeRemaining) }} remaining</p>
<!-- Progress -->
<div v-if="progressPercent > 0" class="px-4 py-2 mt-4 bg-primary text-sm font-semibold rounded-md text-gray-100 relative max-w-max mx-auto md:mx-0" :class="resettingProgress ? 'opacity-25' : ''">
<p v-if="progressPercent < 1" class="leading-6">Your Progress: {{ Math.round(progressPercent * 100) }}%</p>
<p v-else class="text-xs">Finished {{ $formatDate(userProgressFinishedAt, 'MM/dd/yyyy') }}</p>
<p v-if="progressPercent < 1" class="text-gray-200 text-xs">{{ $elapsedPretty(userTimeRemaining) }} remaining</p>
<p class="text-gray-400 text-xs pt-1">Started {{ $formatDate(userProgressStartedAt, 'MM/dd/yyyy') }}</p>
<div v-if="!resettingProgress" class="absolute -top-1.5 -right-1.5 p-1 w-5 h-5 rounded-full bg-bg hover:bg-error border border-primary flex items-center justify-center cursor-pointer" @click.stop="clearProgressClick">
<span class="material-icons text-sm">close</span>
</div>
@@ -372,7 +375,13 @@ export default {
return this.duration - this.userCurrentTime
},
progressPercent() {
return this.userAudiobook ? this.userAudiobook.progress : 0
return this.userAudiobook ? Math.max(Math.min(1, this.userAudiobook.progress), 0) : 0
},
userProgressStartedAt() {
return this.userAudiobook ? this.userAudiobook.startedAt : 0
},
userProgressFinishedAt() {
return this.userAudiobook ? this.userAudiobook.finishedAt : 0
},
streamAudiobook() {
return this.$store.state.streamAudiobook
+3 -3
View File
@@ -15,18 +15,18 @@
</div>
<div v-if="showExperimentalFeatures" class="w-full h-px bg-white bg-opacity-10 my-2" />
<div v-if="showExperimentalFeatures" class="py-2">
<h1 class="text-lg mb-2 text-white text-opacity-90 px-2 sm:px-0">Listening Stats <span class="pl-2 text-xs text-error">(web app only)</span></h1>
<h1 class="text-lg mb-2 text-white text-opacity-90 px-2 sm:px-0">Listening Stats <span class="pl-2 text-xs text-error">(experimental)</span></h1>
<p class="text-sm text-gray-300">
Total Time Listened:&nbsp;
<span class="font-mono text-base">{{ listeningTimePretty }}</span>
</p>
<p class="text-sm text-gray-300">
<p v-if="timeListenedToday" class="text-sm text-gray-300">
Time Listened Today:&nbsp;
<span class="font-mono text-base">{{ $elapsedPrettyExtended(timeListenedToday) }}</span>
</p>
<div v-if="latestSession" class="mt-4">
<h1 class="text-lg mb-2 text-white text-opacity-90 px-2 sm:px-0">Last Listening Session <span class="pl-2 text-xs text-error">(web app only)</span></h1>
<h1 class="text-lg mb-2 text-white text-opacity-90 px-2 sm:px-0">Last Listening Session</h1>
<p class="text-sm text-gray-300">{{ latestSession.audiobookTitle }} {{ $dateDistanceFromNow(latestSession.lastUpdate) }} for {{ $elapsedPrettyExtended(this.latestSession.timeListening) }}</p>
</div>
</div>
+10
View File
@@ -16,4 +16,14 @@ export default class AudioTrack {
}
return `${window.location.origin}${this.contentUrl}`
}
get relativeContentUrl() {
if (!this.contentUrl || this.contentUrl.startsWith('http')) return this.contentUrl
if (process.env.NODE_ENV === 'development') {
return `${process.env.serverUrl}${this.contentUrl}`
}
return this.contentUrl
}
}
+2
View File
@@ -17,6 +17,8 @@ export default class CastPlayer extends EventEmitter {
this.playWhenReady = false
this.defaultPlaybackRate = 1
this.playableMimetypes = {}
this.coverUrl = ''
this.castPlayerState = 'IDLE'
+85 -9
View File
@@ -14,10 +14,13 @@ export default class LocalPlayer extends EventEmitter {
this.hlsStreamId = null
this.hlsInstance = null
this.usingNativeplayer = false
this.currentTime = 0
this.startTime = 0
this.trackStartTime = 0
this.playWhenReady = false
this.defaultPlaybackRate = 1
this.playableMimetypes = {}
this.initialize()
}
@@ -38,9 +41,16 @@ export default class LocalPlayer extends EventEmitter {
this.player.addEventListener('play', this.evtPlay.bind(this))
this.player.addEventListener('pause', this.evtPause.bind(this))
this.player.addEventListener('progress', this.evtProgress.bind(this))
this.player.addEventListener('ended', this.evtEnded.bind(this))
this.player.addEventListener('error', this.evtError.bind(this))
this.player.addEventListener('loadedmetadata', this.evtLoadedMetadata.bind(this))
this.player.addEventListener('timeupdate', this.evtTimeupdate.bind(this))
var mimeTypes = ['audio/flac', 'audio/mpeg', 'audio/mp4', 'audio/ogg', 'audio/aac']
mimeTypes.forEach((mt) => {
this.playableMimetypes[mt] = this.player.canPlayType(mt)
})
console.log(`[LocalPlayer] Supported mime types`, this.playableMimetypes)
}
evtPlay() {
@@ -53,11 +63,27 @@ export default class LocalPlayer extends EventEmitter {
var lastBufferTime = this.getLastBufferedTime()
this.emit('buffertimeUpdate', lastBufferTime)
}
evtEnded() {
if (this.currentTrackIndex < this.audioTracks.length - 1) {
console.log(`[LocalPlayer] Track ended - loading next track ${this.currentTrackIndex + 1}`)
// Has next track
this.currentTrackIndex++
this.playWhenReady = true
this.startTime = this.currentTrack.startOffset
this.loadCurrentTrack()
} else {
console.log(`[LocalPlayer] Ended`)
}
}
evtError(error) {
console.error('Player error', error)
this.emit('error', error)
}
evtLoadedMetadata(data) {
console.log('Audio Loaded Metadata', data)
if (!this.hlsStreamId) {
this.player.currentTime = this.trackStartTime
}
this.emit('stateChange', 'LOADED')
if (this.playWhenReady) {
this.playWhenReady = false
@@ -89,23 +115,33 @@ export default class LocalPlayer extends EventEmitter {
this.audioTracks = tracks
this.hlsStreamId = hlsStreamId
this.playWhenReady = playWhenReady
this.startTime = startTime
if (this.hlsInstance) {
this.destroyHlsInstance()
}
this.currentTime = startTime
if (this.hlsStreamId) {
this.setHlsStream()
} else {
this.setDirectPlay()
}
}
setHlsStream() {
this.trackStartTime = 0
// iOS does not support Media Elements but allows for HLS in the native audio player
if (!Hls.isSupported()) {
console.warn('HLS is not supported - fallback to using audio element')
this.usingNativeplayer = true
this.player.src = this.currentTrack.contentUrl
this.player.currentTime = this.currentTime
this.player.src = this.currentTrack.relativeContentUrl
this.player.currentTime = this.startTime
return
}
var hlsOptions = {
startPosition: this.currentTime || -1
startPosition: this.startTime || -1
// No longer needed because token is put in a query string
// xhrSetup: (xhr) => {
// xhr.setRequestHeader('Authorization', `Bearer ${this.token}`)
@@ -115,7 +151,7 @@ export default class LocalPlayer extends EventEmitter {
this.hlsInstance.attachMedia(this.player)
this.hlsInstance.on(Hls.Events.MEDIA_ATTACHED, () => {
this.hlsInstance.loadSource(this.currentTrack.contentUrl)
this.hlsInstance.loadSource(this.currentTrack.relativeContentUrl)
this.hlsInstance.on(Hls.Events.MANIFEST_PARSED, () => {
console.log('[HLS] Manifest Parsed')
@@ -133,6 +169,23 @@ export default class LocalPlayer extends EventEmitter {
})
}
setDirectPlay() {
// Set initial track and track time offset
var trackIndex = this.audioTracks.findIndex(t => this.startTime >= t.startOffset && this.startTime < (t.startOffset + t.duration))
this.currentTrackIndex = trackIndex >= 0 ? trackIndex : 0
this.loadCurrentTrack()
}
loadCurrentTrack() {
if (!this.currentTrack) return
// When direct play track is loaded current time needs to be set
this.trackStartTime = Math.max(0, this.startTime - (this.currentTrack.startOffset || 0))
this.player.src = this.currentTrack.relativeContentUrl
console.log(`[LocalPlayer] Loading track src ${this.currentTrack.relativeContentUrl}`)
this.player.load()
}
destroyHlsInstance() {
if (!this.hlsInstance) return
if (this.hlsInstance.destroy) {
@@ -181,8 +234,31 @@ export default class LocalPlayer extends EventEmitter {
seek(time) {
if (!this.player) return
var offsetTime = time - (this.currentTrack.startOffset || 0)
this.player.currentTime = Math.max(0, offsetTime)
if (this.hlsStreamId) {
// Seeking HLS stream
var offsetTime = time - (this.currentTrack.startOffset || 0)
this.player.currentTime = Math.max(0, offsetTime)
} else {
// Seeking Direct play
if (time < this.currentTrack.startOffset || time > this.currentTrack.startOffset + this.currentTrack.duration) {
// Change Track
var trackIndex = this.audioTracks.findIndex(t => time >= t.startOffset && time < (t.startOffset + t.duration))
if (trackIndex >= 0) {
this.startTime = time
this.currentTrackIndex = trackIndex
if (!this.player.paused) {
// audio player playing so play when track loads
this.playWhenReady = true
}
this.loadCurrentTrack()
}
} else {
var offsetTime = time - (this.currentTrack.startOffset || 0)
this.player.currentTime = Math.max(0, offsetTime)
}
}
}
setVolume(volume) {
+58 -16
View File
@@ -38,7 +38,6 @@ export default class PlayerHandler {
load(audiobook, playWhenReady, startTime = 0) {
if (!this.player) this.switchPlayer()
console.log('Load audiobook', audiobook)
this.audiobook = audiobook
this.startTime = startTime
this.playWhenReady = playWhenReady
@@ -88,6 +87,15 @@ export default class PlayerHandler {
this.player.on('stateChange', this.playerStateChange.bind(this))
this.player.on('timeupdate', this.playerTimeupdate.bind(this))
this.player.on('buffertimeUpdate', this.playerBufferTimeUpdate.bind(this))
this.player.on('error', this.playerError.bind(this))
}
playerError() {
// Switch to HLS stream on error
if (!this.isCasting && !this.currentStreamId && (this.player instanceof LocalPlayer)) {
console.log(`[PlayerHandler] Audio player error switching to HLS stream`)
this.prepare(true)
}
}
playerStateChange(state) {
@@ -117,8 +125,36 @@ export default class PlayerHandler {
this.ctx.setBufferTime(buffertime)
}
async prepare() {
var useHls = !this.isCasting
async prepare(forceHls = false) {
var useHls = false
var runningTotal = 0
var audioTracks = (this.audiobook.tracks || []).map((track) => {
var audioTrack = new AudioTrack(track)
audioTrack.startOffset = runningTotal
audioTrack.contentUrl = `/lib/${this.audiobook.libraryId}/${this.audiobook.folderId}/${track.path}?token=${this.userToken}`
audioTrack.mimeType = this.getMimeTypeForTrack(track)
audioTrack.canDirectPlay = !!this.player.playableMimetypes[audioTrack.mimeType]
runningTotal += audioTrack.duration
return audioTrack
})
// All html5 audio player plays use HLS unless experimental features is on
if (!this.isCasting) {
if (forceHls || !this.ctx.showExperimentalFeatures) {
useHls = true
} else {
// Use HLS if any audio track cannot be direct played
useHls = !!audioTracks.find(at => !at.canDirectPlay)
if (useHls) {
console.warn(`[PlayerHandler] An audio track cannot be direct played`, audioTracks.find(at => !at.canDirectPlay))
}
}
}
if (useHls) {
var stream = await this.ctx.$axios.$get(`/api/books/${this.audiobook.id}/stream`).catch((error) => {
console.error('Failed to start stream', error)
@@ -126,23 +162,30 @@ export default class PlayerHandler {
if (stream) {
console.log(`[PlayerHandler] prepare hls stream`, stream)
this.setHlsStream(stream)
} else {
console.error(`[PlayerHandler] Failed to start HLS stream`)
}
} else {
// Setup tracks
var runningTotal = 0
var audioTracks = (this.audiobook.tracks || []).map((track) => {
var audioTrack = new AudioTrack(track)
audioTrack.startOffset = runningTotal
audioTrack.contentUrl = `/lib/${this.audiobook.libraryId}/${this.audiobook.folderId}/${track.path}?token=${this.userToken}`
audioTrack.mimeType = (track.codec === 'm4b' || track.codec === 'm4a') ? 'audio/mp4' : `audio/${track.codec}`
runningTotal += audioTrack.duration
return audioTrack
})
this.setDirectPlay(audioTracks)
}
}
getMimeTypeForTrack(track) {
var ext = track.ext
if (ext === '.mp3' || ext === '.m4b' || ext === '.m4a') {
return 'audio/mpeg'
} else if (ext === '.mp4') {
return 'audio/mp4'
} else if (ext === '.ogg') {
return 'audio/ogg'
} else if (ext === '.aac' || ext === '.m4p') {
return 'audio/aac'
} else if (ext === '.flac') {
return 'audio/flac'
}
return 'audio/mpeg'
}
closePlayer() {
console.log('[PlayerHandler] CLose Player')
if (this.player) {
@@ -255,8 +298,7 @@ export default class PlayerHandler {
}
play() {
if (!this.player) return
this.player.play()
if (this.player) this.player.play()
}
pause() {
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "audiobookshelf",
"version": "1.7.1",
"version": "1.7.2",
"description": "Self-hosted audiobook server for managing and playing audiobooks",
"main": "index.js",
"scripts": {
@@ -55,4 +55,4 @@
"xml2js": "^0.4.23"
},
"devDependencies": {}
}
}
+13
View File
@@ -8,6 +8,7 @@ class Library {
this.folders = []
this.displayOrder = 1
this.icon = 'database'
this.mediaType = 'default'
this.provider = 'google'
this.disableWatcher = false
@@ -31,6 +32,7 @@ class Library {
this.folders = (library.folders || []).map(f => new Folder(f))
this.displayOrder = library.displayOrder || 1
this.icon = library.icon || 'database'
this.mediaType = library.mediaType || 'default'
this.provider = library.provider || 'google'
this.disableWatcher = !!library.disableWatcher
@@ -45,6 +47,7 @@ class Library {
folders: (this.folders || []).map(f => f.toJSON()),
displayOrder: this.displayOrder,
icon: this.icon,
mediaType: this.mediaType,
provider: this.provider,
disableWatcher: this.disableWatcher,
createdAt: this.createdAt,
@@ -71,6 +74,7 @@ class Library {
}
this.displayOrder = data.displayOrder || 1
this.icon = data.icon || 'database'
this.mediaType = data.mediaType || 'default'
this.disableWatcher = !!data.disableWatcher
this.createdAt = Date.now()
this.lastUpdate = Date.now()
@@ -86,6 +90,15 @@ class Library {
this.provider = payload.provider
hasUpdates = true
}
if (payload.mediaType && payload.mediaType !== this.mediaType) {
this.mediaType = payload.mediaType
hasUpdates = true
}
if (payload.icon && payload.icon !== this.icon) {
this.icon = payload.icon
hasUpdates = true
}
if (payload.disableWatcher !== this.disableWatcher) {
this.disableWatcher = !!payload.disableWatcher
hasUpdates = true