mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2026-06-04 09:50:42 +02:00
Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ae8f3aa918 | |||
| 5d4047c171 | |||
| 6f80591afd | |||
| 788d867ec3 | |||
| 3bc3914fd9 | |||
| 3d821dacb7 | |||
| e0546c6164 | |||
| be7ccfb209 | |||
| 938a8c6f80 | |||
| 5cd343cb01 | |||
| ab0094a53b | |||
| 2d5e4ebcf0 | |||
| 3171ce5aba | |||
| 0e1692d26b | |||
| e8cd18eac2 | |||
| bf928692d5 | |||
| 792490b629 | |||
| 0d1ff35c5e | |||
| 67e02fddbd | |||
| 09beb6a2ae | |||
| 2dba17a7ae | |||
| 4900649908 | |||
| 1350a91fba |
@@ -3,8 +3,15 @@
|
|||||||
name: Build and Push Docker Image
|
name: Build and Push Docker Image
|
||||||
|
|
||||||
on:
|
on:
|
||||||
|
# Allows you to run workflow manually from Actions tab
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
tags:
|
||||||
|
description: 'Docker Tag'
|
||||||
|
required: true
|
||||||
|
default: 'latest'
|
||||||
push:
|
push:
|
||||||
branches: [master]
|
branches: [main,master]
|
||||||
tags:
|
tags:
|
||||||
- 'v*.*.*'
|
- 'v*.*.*'
|
||||||
# Only build when files in these directories have been changed
|
# Only build when files in these directories have been changed
|
||||||
@@ -13,8 +20,6 @@ on:
|
|||||||
- server/**
|
- server/**
|
||||||
- index.js
|
- index.js
|
||||||
- package.json
|
- package.json
|
||||||
# Allows you to run workflow manually from Actions tab
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
@@ -23,24 +28,25 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Check out
|
- name: Check out
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Docker meta
|
- name: Docker meta
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@v3
|
uses: docker/metadata-action@v4
|
||||||
with:
|
with:
|
||||||
images: advplyr/audiobookshelf,ghcr.io/${{ github.repository_owner }}/audiobookshelf
|
images: advplyr/audiobookshelf,ghcr.io/${{ github.repository_owner }}/audiobookshelf
|
||||||
tags: |
|
tags: |
|
||||||
type=edge,branch=master
|
type=edge,branch=master
|
||||||
type=semver,pattern={{version}}
|
type=semver,pattern={{version}}
|
||||||
|
|
||||||
- name: Setup QEMU
|
- name: Setup QEMU
|
||||||
uses: docker/setup-qemu-action@v1
|
uses: docker/setup-qemu-action@v2
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v1
|
uses: docker/setup-buildx-action@v2
|
||||||
|
|
||||||
- name: Cache Docker layers
|
- name: Cache Docker layers
|
||||||
uses: actions/cache@v2
|
uses: actions/cache@v3
|
||||||
with:
|
with:
|
||||||
path: /tmp/.buildx-cache
|
path: /tmp/.buildx-cache
|
||||||
key: ${{ runner.os }}-buildx-${{ github.sha }}
|
key: ${{ runner.os }}-buildx-${{ github.sha }}
|
||||||
@@ -48,22 +54,22 @@ jobs:
|
|||||||
${{ runner.os }}-buildx-
|
${{ runner.os }}-buildx-
|
||||||
|
|
||||||
- name: Login to Dockerhub
|
- name: Login to Dockerhub
|
||||||
uses: docker/login-action@v1
|
uses: docker/login-action@v2
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||||
|
|
||||||
- name: Login to ghcr
|
- name: Login to ghcr
|
||||||
uses: docker/login-action@v1
|
uses: docker/login-action@v2
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.repository_owner }}
|
username: ${{ github.repository_owner }}
|
||||||
password: ${{ secrets.GHCR_PASSWORD }}
|
password: ${{ secrets.GHCR_PASSWORD }}
|
||||||
|
|
||||||
- name: Build image
|
- name: Build image
|
||||||
uses: docker/build-push-action@v2
|
uses: docker/build-push-action@v3
|
||||||
with:
|
with:
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
tags: ${{ github.event.inputs.tags || steps.meta.outputs.tags }}
|
||||||
context: .
|
context: .
|
||||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||||
push: true
|
push: true
|
||||||
|
|||||||
+14
-4
@@ -7,13 +7,23 @@ RUN npm run generate
|
|||||||
|
|
||||||
### STAGE 1: Build server ###
|
### STAGE 1: Build server ###
|
||||||
FROM node:16-alpine
|
FROM node:16-alpine
|
||||||
RUN apk update && apk add --no-cache --update ffmpeg
|
|
||||||
ENV NODE_ENV=production
|
ENV NODE_ENV=production
|
||||||
|
RUN apk update && \
|
||||||
|
apk add --no-cache --update \
|
||||||
|
curl \
|
||||||
|
tzdata \
|
||||||
|
ffmpeg
|
||||||
|
|
||||||
COPY --from=build /client/dist /client/dist
|
COPY --from=build /client/dist /client/dist
|
||||||
COPY index.js index.js
|
COPY index.js package* /
|
||||||
COPY package-lock.json package-lock.json
|
|
||||||
COPY package.json package.json
|
|
||||||
COPY server server
|
COPY server server
|
||||||
|
|
||||||
RUN npm ci --only=production
|
RUN npm ci --only=production
|
||||||
|
|
||||||
EXPOSE 80
|
EXPOSE 80
|
||||||
|
HEALTHCHECK \
|
||||||
|
--interval=30s \
|
||||||
|
--timeout=3s \
|
||||||
|
--start-period=10s \
|
||||||
|
CMD curl -f http://127.0.0.1/ping || exit 1
|
||||||
CMD ["npm", "start"]
|
CMD ["npm", "start"]
|
||||||
|
|||||||
@@ -64,6 +64,11 @@ export default {
|
|||||||
title: 'Users',
|
title: 'Users',
|
||||||
path: '/config/users'
|
path: '/config/users'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'config-sessions',
|
||||||
|
title: 'Sessions',
|
||||||
|
path: '/config/sessions'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'config-backups',
|
id: 'config-backups',
|
||||||
title: 'Backups',
|
title: 'Backups',
|
||||||
|
|||||||
@@ -71,7 +71,8 @@ export default {
|
|||||||
sleepTimerRemaining: 0,
|
sleepTimerRemaining: 0,
|
||||||
sleepTimer: null,
|
sleepTimer: null,
|
||||||
displayTitle: null,
|
displayTitle: null,
|
||||||
initialPlaybackRate: 1
|
initialPlaybackRate: 1,
|
||||||
|
syncFailedToast: null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -380,6 +381,10 @@ export default {
|
|||||||
},
|
},
|
||||||
pauseItem() {
|
pauseItem() {
|
||||||
this.playerHandler.pause()
|
this.playerHandler.pause()
|
||||||
|
},
|
||||||
|
showFailedProgressSyncs() {
|
||||||
|
if (!isNaN(this.syncFailedToast)) this.$toast.dismiss(this.syncFailedToast)
|
||||||
|
this.syncFailedToast = this.$toast('Progress is not being synced. Restart playback', { timeout: false, type: 'error' })
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
|
|||||||
@@ -214,7 +214,7 @@ export default {
|
|||||||
return this.filterData.languages || []
|
return this.filterData.languages || []
|
||||||
},
|
},
|
||||||
progress() {
|
progress() {
|
||||||
return ['Finished', 'In Progress', 'Not Started']
|
return ['Finished', 'In Progress', 'Not Started', 'Not Finished']
|
||||||
},
|
},
|
||||||
missing() {
|
missing() {
|
||||||
return ['ASIN', 'ISBN', 'Subtitle', 'Author', 'Publish Year', 'Series', 'Description', 'Genres', 'Tags', 'Narrator', 'Publisher', 'Language']
|
return ['ASIN', 'ISBN', 'Subtitle', 'Author', 'Publish Year', 'Series', 'Description', 'Genres', 'Tags', 'Narrator', 'Publisher', 'Language']
|
||||||
|
|||||||
@@ -87,7 +87,7 @@
|
|||||||
<p class="mb-1">{{ playMethodName }}</p>
|
<p class="mb-1">{{ playMethodName }}</p>
|
||||||
<p class="mb-1">{{ _session.mediaPlayer }}</p>
|
<p class="mb-1">{{ _session.mediaPlayer }}</p>
|
||||||
|
|
||||||
<p class="font-semibold uppercase text-xs text-gray-400 tracking-wide mt-6 mb-2">Device</p>
|
<p v-if="hasDeviceInfo" class="font-semibold uppercase text-xs text-gray-400 tracking-wide mt-6 mb-2">Device</p>
|
||||||
<p v-if="deviceInfo.ipAddress" class="mb-1">{{ deviceInfo.ipAddress }}</p>
|
<p v-if="deviceInfo.ipAddress" class="mb-1">{{ deviceInfo.ipAddress }}</p>
|
||||||
<p v-if="osDisplayName" class="mb-1">{{ osDisplayName }}</p>
|
<p v-if="osDisplayName" class="mb-1">{{ osDisplayName }}</p>
|
||||||
<p v-if="deviceInfo.browserName" class="mb-1">{{ deviceInfo.browserName }}</p>
|
<p v-if="deviceInfo.browserName" class="mb-1">{{ deviceInfo.browserName }}</p>
|
||||||
@@ -127,6 +127,9 @@ export default {
|
|||||||
deviceInfo() {
|
deviceInfo() {
|
||||||
return this._session.deviceInfo || {}
|
return this._session.deviceInfo || {}
|
||||||
},
|
},
|
||||||
|
hasDeviceInfo() {
|
||||||
|
return Object.keys(this.deviceInfo).length
|
||||||
|
},
|
||||||
osDisplayName() {
|
osDisplayName() {
|
||||||
if (!this.deviceInfo.osName) return null
|
if (!this.deviceInfo.osName) return null
|
||||||
return `${this.deviceInfo.osName} ${this.deviceInfo.osVersion}`
|
return `${this.deviceInfo.osName} ${this.deviceInfo.osVersion}`
|
||||||
|
|||||||
@@ -366,14 +366,18 @@ export default {
|
|||||||
},
|
},
|
||||||
selectMatch(match) {
|
selectMatch(match) {
|
||||||
if (match && match.series) {
|
if (match && match.series) {
|
||||||
match.series = match.series.map((se) => {
|
if (!match.series.length) {
|
||||||
return {
|
delete match.series
|
||||||
id: `new-${Math.floor(Math.random() * 10000)}`,
|
} else {
|
||||||
displayName: se.volumeNumber ? `${se.series} #${se.volumeNumber}` : se.series,
|
match.series = match.series.map((se) => {
|
||||||
name: se.series,
|
return {
|
||||||
sequence: se.volumeNumber || ''
|
id: `new-${Math.floor(Math.random() * 10000)}`,
|
||||||
}
|
displayName: se.volumeNumber ? `${se.series} #${se.volumeNumber}` : se.series,
|
||||||
})
|
name: se.series,
|
||||||
|
sequence: se.volumeNumber || ''
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.selectedMatch = match
|
this.selectedMatch = match
|
||||||
@@ -405,7 +409,9 @@ export default {
|
|||||||
|
|
||||||
updatePayload.metadata.series = seriesPayload
|
updatePayload.metadata.series = seriesPayload
|
||||||
} else if (key === 'author' && !this.isPodcast) {
|
} else if (key === 'author' && !this.isPodcast) {
|
||||||
if (!Array.isArray(this.selectedMatch[key])) this.selectedMatch[key] = [this.selectedMatch[key]]
|
if (!Array.isArray(this.selectedMatch[key])) {
|
||||||
|
this.selectedMatch[key] = this.selectedMatch[key].split(',').map((au) => au.trim())
|
||||||
|
}
|
||||||
var authorPayload = []
|
var authorPayload = []
|
||||||
this.selectedMatch[key].forEach((authorName) =>
|
this.selectedMatch[key].forEach((authorName) =>
|
||||||
authorPayload.push({
|
authorPayload.push({
|
||||||
|
|||||||
@@ -2,18 +2,21 @@
|
|||||||
<div class="flex pt-4 pb-2 md:pt-0 md:pb-2">
|
<div class="flex pt-4 pb-2 md:pt-0 md:pb-2">
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
<template v-if="!loading">
|
<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">
|
<div class="cursor-pointer flex items-center justify-center text-gray-300 mr-4 md:mr-8" @mousedown.prevent @mouseup.prevent @click.stop="prevChapter">
|
||||||
<span class="material-icons text-3xl">first_page</span>
|
<span class="material-icons text-3xl">first_page</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="cursor-pointer flex items-center justify-center text-gray-300" @mousedown.prevent @mouseup.prevent @click.stop="jumpBackward">
|
<div class="cursor-pointer flex items-center justify-center text-gray-300" @mousedown.prevent @mouseup.prevent @click.stop="jumpBackward">
|
||||||
<span class="material-icons text-3xl">replay_10</span>
|
<span class="material-icons text-3xl">replay_10</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="cursor-pointer p-2 shadow-sm bg-accent flex items-center justify-center rounded-full text-primary mx-8" :class="seekLoading ? 'animate-spin' : ''" @mousedown.prevent @mouseup.prevent @click.stop="playPause">
|
<div class="cursor-pointer p-2 shadow-sm bg-accent flex items-center justify-center rounded-full text-primary mx-4 md:mx-8" :class="seekLoading ? 'animate-spin' : ''" @mousedown.prevent @mouseup.prevent @click.stop="playPause">
|
||||||
<span class="material-icons">{{ seekLoading ? 'autorenew' : paused ? 'play_arrow' : 'pause' }}</span>
|
<span class="material-icons">{{ seekLoading ? 'autorenew' : paused ? 'play_arrow' : 'pause' }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="cursor-pointer flex items-center justify-center text-gray-300" @mousedown.prevent @mouseup.prevent @click.stop="jumpForward">
|
<div class="cursor-pointer flex items-center justify-center text-gray-300" @mousedown.prevent @mouseup.prevent @click.stop="jumpForward">
|
||||||
<span class="material-icons text-3xl">forward_10</span>
|
<span class="material-icons text-3xl">forward_10</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex items-center justify-center ml-4 md:ml-8" :class="hasNextChapter ? 'text-gray-300 cursor-pointer' : 'text-gray-500'" @mousedown.prevent @mouseup.prevent @click.stop="nextChapter">
|
||||||
|
<span class="material-icons text-3xl">last_page</span>
|
||||||
|
</div>
|
||||||
<controls-playback-speed-control v-model="playbackRate" @input="playbackRateUpdated" @change="playbackRateChanged" />
|
<controls-playback-speed-control v-model="playbackRate" @input="playbackRateUpdated" @change="playbackRateChanged" />
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
@@ -31,7 +34,8 @@ export default {
|
|||||||
loading: Boolean,
|
loading: Boolean,
|
||||||
seekLoading: Boolean,
|
seekLoading: Boolean,
|
||||||
playbackRate: Number,
|
playbackRate: Number,
|
||||||
paused: Boolean
|
paused: Boolean,
|
||||||
|
hasNextChapter: Boolean
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {}
|
return {}
|
||||||
@@ -41,8 +45,12 @@ export default {
|
|||||||
playPause() {
|
playPause() {
|
||||||
this.$emit('playPause')
|
this.$emit('playPause')
|
||||||
},
|
},
|
||||||
restart() {
|
prevChapter() {
|
||||||
this.$emit('restart')
|
this.$emit('prevChapter')
|
||||||
|
},
|
||||||
|
nextChapter() {
|
||||||
|
if (!this.hasNextChapter) return
|
||||||
|
this.$emit('nextChapter')
|
||||||
},
|
},
|
||||||
jumpBackward() {
|
jumpBackward() {
|
||||||
this.$emit('jumpBackward')
|
this.$emit('jumpBackward')
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<div class="w-full -mt-6">
|
<div class="w-full -mt-6">
|
||||||
<div class="w-full relative mb-1">
|
<div class="w-full relative mb-1">
|
||||||
<div class="absolute -top-10 md:top-0 right-0 md:right-2 flex items-center h-full">
|
<div class="absolute -top-10 md:top-0 right-0 md:right-2 flex items-center h-full">
|
||||||
<span class="material-icons text-2xl cursor-pointer" @click="toggleFullscreen(true)">expand_less</span>
|
<!-- <span class="material-icons text-2xl cursor-pointer" @click="toggleFullscreen(true)">expand_less</span> -->
|
||||||
|
|
||||||
<controls-volume-control ref="volumeControl" v-model="volume" @input="setVolume" class="mx-2 hidden md:block" />
|
<controls-volume-control ref="volumeControl" v-model="volume" @input="setVolume" class="mx-2 hidden md:block" />
|
||||||
|
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<player-playback-controls :loading="loading" :seek-loading="seekLoading" :playback-rate="playbackRate" :paused="paused" @restart="restart" @jumpForward="jumpForward" @jumpBackward="jumpBackward" @setPlaybackRate="setPlaybackRate" @playPause="playPause" />
|
<player-playback-controls :loading="loading" :seek-loading="seekLoading" :playback-rate="playbackRate" :paused="paused" :has-next-chapter="hasNextChapter" @prevChapter="prevChapter" @nextChapter="nextChapter" @jumpForward="jumpForward" @jumpBackward="jumpBackward" @setPlaybackRate="setPlaybackRate" @playPause="playPause" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<player-track-bar ref="trackbar" :loading="loading" :chapters="chapters" :duration="duration" @seek="seek" />
|
<player-track-bar ref="trackbar" :loading="loading" :chapters="chapters" :duration="duration" @seek="seek" />
|
||||||
@@ -106,6 +106,14 @@ export default {
|
|||||||
},
|
},
|
||||||
isFullscreen() {
|
isFullscreen() {
|
||||||
return this.$store.state.playerIsFullscreen
|
return this.$store.state.playerIsFullscreen
|
||||||
|
},
|
||||||
|
currentChapterIndex() {
|
||||||
|
if (!this.currentChapter) return 0
|
||||||
|
return this.chapters.findIndex((ch) => ch.id === this.currentChapter.id)
|
||||||
|
},
|
||||||
|
hasNextChapter() {
|
||||||
|
if (!this.chapters.length) return false
|
||||||
|
return this.currentChapterIndex < this.chapters.length - 1
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@@ -190,6 +198,23 @@ export default {
|
|||||||
restart() {
|
restart() {
|
||||||
this.seek(0)
|
this.seek(0)
|
||||||
},
|
},
|
||||||
|
prevChapter() {
|
||||||
|
if (!this.currentChapter || this.currentChapterIndex === 0) {
|
||||||
|
return this.restart()
|
||||||
|
}
|
||||||
|
var timeInCurrentChapter = this.currentTime - this.currentChapter.start
|
||||||
|
if (timeInCurrentChapter <= 3 && this.chapters[this.currentChapterIndex - 1]) {
|
||||||
|
var prevChapter = this.chapters[this.currentChapterIndex - 1]
|
||||||
|
this.seek(prevChapter.start)
|
||||||
|
} else {
|
||||||
|
this.seek(this.currentChapter.start)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
nextChapter() {
|
||||||
|
if (!this.currentChapter || !this.hasNextChapter) return
|
||||||
|
var nextChapter = this.chapters[this.currentChapterIndex + 1]
|
||||||
|
this.seek(nextChapter.start)
|
||||||
|
},
|
||||||
setStreamReady() {
|
setStreamReady() {
|
||||||
if (this.$refs.trackbar) this.$refs.trackbar.setPercentageReady(1)
|
if (this.$refs.trackbar) this.$refs.trackbar.setPercentageReady(1)
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<button class="icon-btn rounded-md flex items-center justify-center h-9 w-9 relative" @mousedown.prevent :disabled="disabled || loading" :class="className" @click="clickBtn">
|
<button class="icon-btn rounded-md flex items-center justify-center relative" @mousedown.prevent :disabled="disabled || loading" :class="className" @click="clickBtn">
|
||||||
<div v-if="loading" class="text-white absolute top-0 left-0 w-full h-full flex items-center justify-center text-opacity-100">
|
<div v-if="loading" class="text-white absolute top-0 left-0 w-full h-full flex items-center justify-center text-opacity-100">
|
||||||
<svg class="animate-spin" style="width: 24px; height: 24px" viewBox="0 0 24 24">
|
<svg class="animate-spin" style="width: 24px; height: 24px" viewBox="0 0 24 24">
|
||||||
<path fill="currentColor" d="M12,4V2A10,10 0 0,0 2,12H4A8,8 0 0,1 12,4Z" />
|
<path fill="currentColor" d="M12,4V2A10,10 0 0,0 2,12H4A8,8 0 0,1 12,4Z" />
|
||||||
@@ -20,20 +20,29 @@ export default {
|
|||||||
},
|
},
|
||||||
outlined: Boolean,
|
outlined: Boolean,
|
||||||
borderless: Boolean,
|
borderless: Boolean,
|
||||||
loading: Boolean
|
loading: Boolean,
|
||||||
|
iconFontSize: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
type: Number,
|
||||||
|
default: 9
|
||||||
|
}
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {}
|
return {}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
className() {
|
className() {
|
||||||
var classes = []
|
var classes = [`h-${this.size} w-${this.size}`]
|
||||||
if (!this.borderless) {
|
if (!this.borderless) {
|
||||||
classes.push(`bg-${this.bgColor} border border-gray-600`)
|
classes.push(`bg-${this.bgColor} border border-gray-600`)
|
||||||
}
|
}
|
||||||
return classes.join(' ')
|
return classes.join(' ')
|
||||||
},
|
},
|
||||||
fontSize() {
|
fontSize() {
|
||||||
|
if (this.iconFontSize) return this.iconFontSize
|
||||||
if (this.icon === 'edit') return '1.25rem'
|
if (this.icon === 'edit') return '1.25rem'
|
||||||
return '1.4rem'
|
return '1.4rem'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,7 +42,8 @@ export default {
|
|||||||
editable: {
|
editable: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true
|
default: true
|
||||||
}
|
},
|
||||||
|
showAllWhenEmpty: Boolean
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@@ -72,6 +73,7 @@ export default {
|
|||||||
itemsToShow() {
|
itemsToShow() {
|
||||||
if (!this.editable) return this.items
|
if (!this.editable) return this.items
|
||||||
if (!this.textInput || this.textInput === this.input) {
|
if (!this.textInput || this.textInput === this.input) {
|
||||||
|
if (this.showAllWhenEmpty) return this.items
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
return this.items.filter((i) => {
|
return this.items.filter((i) => {
|
||||||
|
|||||||
Generated
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookshelf-client",
|
"name": "audiobookshelf-client",
|
||||||
"version": "2.0.18",
|
"version": "2.0.20",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookshelf-client",
|
"name": "audiobookshelf-client",
|
||||||
"version": "2.0.18",
|
"version": "2.0.20",
|
||||||
"description": "Self-hosted audiobook and podcast client",
|
"description": "Self-hosted audiobook and podcast client",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
<template>
|
<template>
|
||||||
<div id="page-wrapper" class="bg-bg page overflow-y-auto p-8" :class="streamLibraryItem ? 'streaming' : ''">
|
<div id="page-wrapper" class="bg-bg page overflow-y-auto p-4 md:p-8" :class="streamLibraryItem ? 'streaming' : ''">
|
||||||
<div class="max-w-6xl mx-auto">
|
<div class="max-w-6xl mx-auto">
|
||||||
<div class="flex mb-6">
|
<div class="flex flex-wrap sm:flex-nowrap justify-center mb-6">
|
||||||
<div class="w-48 min-w-48">
|
<div class="w-48 min-w-48">
|
||||||
<div class="w-full h-52">
|
<div class="w-full h-52">
|
||||||
<covers-author-image :author="author" rounded="0" />
|
<covers-author-image :author="author" rounded="0" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-grow px-8">
|
<div class="flex-grow py-4 sm:py-0 px-4 md:px-8">
|
||||||
<div class="flex items-center mb-8">
|
<div class="flex items-center mb-8">
|
||||||
<h1 class="text-2xl">{{ author.name }}</h1>
|
<h1 class="text-2xl">{{ author.name }}</h1>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,196 @@
|
|||||||
|
<template>
|
||||||
|
<div class="w-full h-full">
|
||||||
|
<div class="bg-bg rounded-md shadow-lg border border-white border-opacity-5 p-0 sm:p-4 mb-8">
|
||||||
|
<div class="py-2">
|
||||||
|
<div class="flex items-center mb-1">
|
||||||
|
<h1 class="text-lg mb-2 text-white text-opacity-90 px-2 sm:px-0">Listening Sessions</h1>
|
||||||
|
<div class="flex-grow" />
|
||||||
|
|
||||||
|
<ui-dropdown v-model="selectedUser" :items="userItems" label="Filter by User" small class="max-w-48" @input="updateUserFilter" />
|
||||||
|
</div>
|
||||||
|
<div v-if="listeningSessions.length" class="block max-w-full">
|
||||||
|
<table class="userSessionsTable">
|
||||||
|
<tr class="bg-primary bg-opacity-40">
|
||||||
|
<th class="w-48 min-w-48 text-left">Item</th>
|
||||||
|
<th class="w-20 min-w-20 text-left hidden md:table-cell">User</th>
|
||||||
|
<th class="w-32 min-w-32 text-left hidden md:table-cell">Play Method</th>
|
||||||
|
<th class="w-32 min-w-32 text-left hidden sm:table-cell">Device Info</th>
|
||||||
|
<th class="w-32 min-w-32">Listened</th>
|
||||||
|
<th class="w-16 min-w-16">Last Time</th>
|
||||||
|
<th class="flex-grow hidden sm:table-cell">Last Update</th>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr v-for="session in listeningSessions" :key="session.id" class="cursor-pointer" @click="showSession(session)">
|
||||||
|
<td class="py-1 max-w-48">
|
||||||
|
<p class="text-xs text-gray-200 truncate">{{ session.displayTitle }}</p>
|
||||||
|
<p class="text-xs text-gray-400 truncate">{{ session.displayAuthor }}</p>
|
||||||
|
</td>
|
||||||
|
<td class="hidden md:table-cell">
|
||||||
|
<p v-if="filteredUserUsername" class="text-xs">{{ filteredUserUsername }}</p>
|
||||||
|
<p v-else class="text-xs">{{ session.user ? session.user.username : 'N/A' }}</p>
|
||||||
|
</td>
|
||||||
|
<td class="hidden md:table-cell">
|
||||||
|
<p class="text-xs">{{ getPlayMethodName(session.playMethod) }}</p>
|
||||||
|
</td>
|
||||||
|
<td class="hidden sm:table-cell">
|
||||||
|
<p class="text-xs" v-html="getDeviceInfoString(session.deviceInfo)" />
|
||||||
|
</td>
|
||||||
|
<td class="text-center">
|
||||||
|
<p class="text-xs font-mono">{{ $elapsedPretty(session.timeListening) }}</p>
|
||||||
|
</td>
|
||||||
|
<td class="text-center">
|
||||||
|
<p class="text-xs font-mono">{{ $secondsToTimestamp(session.currentTime) }}</p>
|
||||||
|
</td>
|
||||||
|
<td class="text-center hidden sm:table-cell">
|
||||||
|
<ui-tooltip v-if="session.updatedAt" direction="top" :text="$formatDate(session.updatedAt, 'MMMM do, yyyy HH:mm')">
|
||||||
|
<p class="text-xs text-gray-200">{{ $dateDistanceFromNow(session.updatedAt) }}</p>
|
||||||
|
</ui-tooltip>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<div class="flex items-center justify-end py-1">
|
||||||
|
<ui-icon-btn icon="arrow_back_ios_new" :size="7" icon-font-size="1rem" class="mx-1" :disabled="currentPage === 0" @click="prevPage" />
|
||||||
|
<p class="text-sm mx-1">Page {{ currentPage + 1 }} of {{ numPages }}</p>
|
||||||
|
<ui-icon-btn icon="arrow_forward_ios" :size="7" icon-font-size="1rem" class="mx-1" :disabled="currentPage >= numPages - 1" @click="nextPage" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p v-else class="text-white text-opacity-50">No sessions yet...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<modals-listening-session-modal v-model="showSessionModal" :session="selectedSession" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
async asyncData({ params, redirect, app }) {
|
||||||
|
var users = await app.$axios
|
||||||
|
.$get('/api/users')
|
||||||
|
.then((users) => {
|
||||||
|
return users.sort((a, b) => {
|
||||||
|
return a.createdAt - b.createdAt
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Failed', error)
|
||||||
|
return []
|
||||||
|
})
|
||||||
|
return {
|
||||||
|
users
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
showSessionModal: false,
|
||||||
|
selectedSession: null,
|
||||||
|
listeningSessions: [],
|
||||||
|
numPages: 0,
|
||||||
|
total: 0,
|
||||||
|
currentPage: 0,
|
||||||
|
userFilter: null,
|
||||||
|
selectedUser: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
username() {
|
||||||
|
return this.user.username
|
||||||
|
},
|
||||||
|
userOnline() {
|
||||||
|
return this.$store.getters['users/getIsUserOnline'](this.user.id)
|
||||||
|
},
|
||||||
|
userItems() {
|
||||||
|
var userItems = [{ value: '', text: 'All Users' }]
|
||||||
|
return userItems.concat(this.users.map((u) => ({ value: u.id, text: u.username })))
|
||||||
|
},
|
||||||
|
filteredUserUsername() {
|
||||||
|
if (!this.userFilter) return null
|
||||||
|
var user = this.users.find((u) => u.id === this.userFilter)
|
||||||
|
return user ? user.username : null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
updateUserFilter() {
|
||||||
|
this.loadSessions(0)
|
||||||
|
},
|
||||||
|
prevPage() {
|
||||||
|
this.loadSessions(this.currentPage - 1)
|
||||||
|
},
|
||||||
|
nextPage() {
|
||||||
|
this.loadSessions(this.currentPage + 1)
|
||||||
|
},
|
||||||
|
showSession(session) {
|
||||||
|
this.selectedSession = session
|
||||||
|
this.showSessionModal = true
|
||||||
|
},
|
||||||
|
getDeviceInfoString(deviceInfo) {
|
||||||
|
if (!deviceInfo) return ''
|
||||||
|
var lines = []
|
||||||
|
if (deviceInfo.osName) lines.push(`${deviceInfo.osName} ${deviceInfo.osVersion}`)
|
||||||
|
if (deviceInfo.browserName) lines.push(deviceInfo.browserName)
|
||||||
|
|
||||||
|
if (deviceInfo.manufacturer && deviceInfo.model) lines.push(`${deviceInfo.manufacturer} ${deviceInfo.model}`)
|
||||||
|
if (deviceInfo.sdkVersion) lines.push(`SDK Version: ${deviceInfo.sdkVersion}`)
|
||||||
|
return lines.join('<br>')
|
||||||
|
},
|
||||||
|
getPlayMethodName(playMethod) {
|
||||||
|
if (playMethod === this.$constants.PlayMethod.DIRECTPLAY) return 'Direct Play'
|
||||||
|
else if (playMethod === this.$constants.PlayMethod.TRANSCODE) return 'Transcode'
|
||||||
|
else if (playMethod === this.$constants.PlayMethod.DIRECTSTREAM) return 'Direct Stream'
|
||||||
|
else if (playMethod === this.$constants.PlayMethod.LOCAL) return 'Local'
|
||||||
|
return 'Unknown'
|
||||||
|
},
|
||||||
|
async loadSessions(page) {
|
||||||
|
var userFilterQuery = this.selectedUser ? `&user=${this.selectedUser}` : ''
|
||||||
|
const data = await this.$axios.$get(`/api/sessions?page=${page}&itemsPerPage=10${userFilterQuery}`).catch((err) => {
|
||||||
|
console.error('Failed to load listening sesions', err)
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
if (!data) {
|
||||||
|
this.$toast.error('Failed to load listening sessions')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.numPages = data.numPages
|
||||||
|
this.total = data.total
|
||||||
|
this.currentPage = data.page
|
||||||
|
this.listeningSessions = data.sessions
|
||||||
|
this.userFilter = data.userFilter
|
||||||
|
},
|
||||||
|
init() {
|
||||||
|
this.loadSessions(0)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.init()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.userSessionsTable {
|
||||||
|
border-collapse: collapse;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
border: 1px solid #474747;
|
||||||
|
}
|
||||||
|
.userSessionsTable tr:first-child {
|
||||||
|
background-color: #272727;
|
||||||
|
}
|
||||||
|
.userSessionsTable tr:not(:first-child) {
|
||||||
|
background-color: #373838;
|
||||||
|
}
|
||||||
|
.userSessionsTable tr:not(:first-child):nth-child(odd) {
|
||||||
|
background-color: #2f2f2f;
|
||||||
|
}
|
||||||
|
.userSessionsTable tr:hover:not(:first-child) {
|
||||||
|
background-color: #474747;
|
||||||
|
}
|
||||||
|
.userSessionsTable td {
|
||||||
|
padding: 4px 8px;
|
||||||
|
}
|
||||||
|
.userSessionsTable th {
|
||||||
|
padding: 4px 8px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -138,7 +138,9 @@ export default {
|
|||||||
this.$copyToClipboard(str, this)
|
this.$copyToClipboard(str, this)
|
||||||
},
|
},
|
||||||
async init() {
|
async init() {
|
||||||
this.listeningSessions = await this.$axios.$get(`/api/users/${this.user.id}/listening-sessions`).catch((err) => {
|
this.listeningSessions = await this.$axios.$get(`/api/users/${this.user.id}/listening-sessions?page=0&itemsPerPage=10`).then((data) => {
|
||||||
|
return data.sessions || []
|
||||||
|
}).catch((err) => {
|
||||||
console.error('Failed to load listening sesions', err)
|
console.error('Failed to load listening sesions', err)
|
||||||
return []
|
return []
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -17,40 +17,47 @@
|
|||||||
<div class="w-full h-px bg-white bg-opacity-10 my-2" />
|
<div class="w-full h-px bg-white bg-opacity-10 my-2" />
|
||||||
|
|
||||||
<div class="py-2">
|
<div class="py-2">
|
||||||
<h1 class="text-lg mb-2 text-white text-opacity-90 px-2 sm:px-0">Listening Sessions ({{ listeningSessions.length }})</h1>
|
<h1 class="text-lg mb-2 text-white text-opacity-90 px-2 sm:px-0">Listening Sessions</h1>
|
||||||
<table v-if="listeningSessions.length" class="userSessionsTable">
|
<div v-if="listeningSessions.length">
|
||||||
<tr class="bg-primary bg-opacity-40">
|
<table class="userSessionsTable">
|
||||||
<th class="flex-grow text-left">Item</th>
|
<tr class="bg-primary bg-opacity-40">
|
||||||
<th class="w-32 text-left hidden md:table-cell">Play Method</th>
|
<th class="w-48 min-w-48 text-left">Item</th>
|
||||||
<th class="w-40 text-left hidden sm:table-cell">Device Info</th>
|
<th class="w-32 min-w-32 text-left hidden md:table-cell">Play Method</th>
|
||||||
<th class="w-20">Listened</th>
|
<th class="w-32 min-w-32 text-left hidden sm:table-cell">Device Info</th>
|
||||||
<th class="w-20">Last Time</th>
|
<th class="w-32 min-w-32">Listened</th>
|
||||||
<th class="w-40 hidden sm:table-cell">Last Update</th>
|
<th class="w-16 min-w-16">Last Time</th>
|
||||||
</tr>
|
<th class="flex-grow hidden sm:table-cell">Last Update</th>
|
||||||
<tr v-for="session in listeningSessions" :key="session.id" class="cursor-pointer" @click="showSession(session)">
|
</tr>
|
||||||
<td class="py-1">
|
<tr v-for="session in listeningSessions" :key="session.id" class="cursor-pointer" @click="showSession(session)">
|
||||||
<p class="text-sm text-gray-200">{{ session.displayTitle }}</p>
|
<td class="py-1 max-w-48">
|
||||||
<p class="text-xs text-gray-400">{{ session.displayAuthor }}</p>
|
<p class="text-xs text-gray-200 truncate">{{ session.displayTitle }}</p>
|
||||||
</td>
|
<p class="text-xs text-gray-400 truncate">{{ session.displayAuthor }}</p>
|
||||||
<td class="hidden md:table-cell">
|
</td>
|
||||||
<p class="text-xs">{{ getPlayMethodName(session.playMethod) }}</p>
|
<td class="hidden md:table-cell">
|
||||||
</td>
|
<p class="text-xs">{{ getPlayMethodName(session.playMethod) }}</p>
|
||||||
<td class="hidden sm:table-cell">
|
</td>
|
||||||
<p class="text-xs" v-html="getDeviceInfoString(session.deviceInfo)" />
|
<td class="hidden sm:table-cell">
|
||||||
</td>
|
<p class="text-xs" v-html="getDeviceInfoString(session.deviceInfo)" />
|
||||||
<td class="text-center">
|
</td>
|
||||||
<p class="text-xs font-mono">{{ $elapsedPretty(session.timeListening) }}</p>
|
<td class="text-center">
|
||||||
</td>
|
<p class="text-xs font-mono">{{ $elapsedPretty(session.timeListening) }}</p>
|
||||||
<td class="text-center">
|
</td>
|
||||||
<p class="text-xs font-mono">{{ $secondsToTimestamp(session.currentTime) }}</p>
|
<td class="text-center">
|
||||||
</td>
|
<p class="text-xs font-mono">{{ $secondsToTimestamp(session.currentTime) }}</p>
|
||||||
<td class="text-center hidden sm:table-cell">
|
</td>
|
||||||
<ui-tooltip v-if="session.updatedAt" direction="top" :text="$formatDate(session.updatedAt, 'MMMM do, yyyy HH:mm')">
|
<td class="text-center hidden sm:table-cell">
|
||||||
<p class="text-xs">{{ $dateDistanceFromNow(session.updatedAt) }}</p>
|
<ui-tooltip v-if="session.updatedAt" direction="top" :text="$formatDate(session.updatedAt, 'MMMM do, yyyy HH:mm')">
|
||||||
</ui-tooltip>
|
<p class="text-xs text-gray-200">{{ $dateDistanceFromNow(session.updatedAt) }}</p>
|
||||||
</td>
|
</ui-tooltip>
|
||||||
</tr>
|
</td>
|
||||||
</table>
|
</tr>
|
||||||
|
</table>
|
||||||
|
<div class="flex items-center justify-end py-1">
|
||||||
|
<ui-icon-btn icon="arrow_back_ios_new" :size="7" icon-font-size="1rem" class="mx-1" :disabled="currentPage === 0" @click="prevPage" />
|
||||||
|
<p class="text-sm mx-1">Page {{ currentPage + 1 }} of {{ numPages }}</p>
|
||||||
|
<ui-icon-btn icon="arrow_forward_ios" :size="7" icon-font-size="1rem" class="mx-1" :disabled="currentPage >= numPages - 1" @click="nextPage" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<p v-else class="text-white text-opacity-50">No sessions yet...</p>
|
<p v-else class="text-white text-opacity-50">No sessions yet...</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -75,7 +82,10 @@ export default {
|
|||||||
return {
|
return {
|
||||||
showSessionModal: false,
|
showSessionModal: false,
|
||||||
selectedSession: null,
|
selectedSession: null,
|
||||||
listeningSessions: []
|
listeningSessions: [],
|
||||||
|
numPages: 0,
|
||||||
|
total: 0,
|
||||||
|
currentPage: 0
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -87,6 +97,12 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
prevPage() {
|
||||||
|
this.loadSessions(this.currentPage - 1)
|
||||||
|
},
|
||||||
|
nextPage() {
|
||||||
|
this.loadSessions(this.currentPage + 1)
|
||||||
|
},
|
||||||
showSession(session) {
|
showSession(session) {
|
||||||
this.selectedSession = session
|
this.selectedSession = session
|
||||||
this.showSessionModal = true
|
this.showSessionModal = true
|
||||||
@@ -108,13 +124,23 @@ export default {
|
|||||||
else if (playMethod === this.$constants.PlayMethod.LOCAL) return 'Local'
|
else if (playMethod === this.$constants.PlayMethod.LOCAL) return 'Local'
|
||||||
return 'Unknown'
|
return 'Unknown'
|
||||||
},
|
},
|
||||||
async init() {
|
async loadSessions(page) {
|
||||||
console.log(navigator)
|
const data = await this.$axios.$get(`/api/users/${this.user.id}/listening-sessions?page=${page}&itemsPerPage=10`).catch((err) => {
|
||||||
|
|
||||||
this.listeningSessions = await this.$axios.$get(`/api/users/${this.user.id}/listening-sessions`).catch((err) => {
|
|
||||||
console.error('Failed to load listening sesions', err)
|
console.error('Failed to load listening sesions', err)
|
||||||
return []
|
return null
|
||||||
})
|
})
|
||||||
|
if (!data) {
|
||||||
|
this.$toast.error('Failed to load listening sessions')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.numPages = data.numPages
|
||||||
|
this.total = data.total
|
||||||
|
this.currentPage = data.page
|
||||||
|
this.listeningSessions = data.sessions
|
||||||
|
},
|
||||||
|
init() {
|
||||||
|
this.loadSessions(0)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
@@ -123,10 +149,11 @@ export default {
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style scoped>
|
||||||
.userSessionsTable {
|
.userSessionsTable {
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
border: 1px solid #474747;
|
border: 1px solid #474747;
|
||||||
}
|
}
|
||||||
.userSessionsTable tr:first-child {
|
.userSessionsTable tr:first-child {
|
||||||
|
|||||||
@@ -124,6 +124,8 @@ export default class CastPlayer extends EventEmitter {
|
|||||||
|
|
||||||
async seek(time, playWhenReady) {
|
async seek(time, playWhenReady) {
|
||||||
if (!this.player) return
|
if (!this.player) return
|
||||||
|
|
||||||
|
this.playWhenReady = playWhenReady
|
||||||
if (time < this.currentTrack.startOffset || time > this.currentTrack.startOffset + this.currentTrack.duration) {
|
if (time < this.currentTrack.startOffset || time > this.currentTrack.startOffset + this.currentTrack.duration) {
|
||||||
// Change Track
|
// Change Track
|
||||||
var request = buildCastLoadRequest(this.libraryItem, this.coverUrl, this.audioTracks, time, playWhenReady, this.defaultPlaybackRate)
|
var request = buildCastLoadRequest(this.libraryItem, this.coverUrl, this.audioTracks, time, playWhenReady, this.defaultPlaybackRate)
|
||||||
|
|||||||
@@ -71,7 +71,6 @@ export default class LocalAudioPlayer extends EventEmitter {
|
|||||||
console.log(`[LocalPlayer] Track ended - loading next track ${this.currentTrackIndex + 1}`)
|
console.log(`[LocalPlayer] Track ended - loading next track ${this.currentTrackIndex + 1}`)
|
||||||
// Has next track
|
// Has next track
|
||||||
this.currentTrackIndex++
|
this.currentTrackIndex++
|
||||||
this.playWhenReady = true
|
|
||||||
this.startTime = this.currentTrack.startOffset
|
this.startTime = this.currentTrack.startOffset
|
||||||
this.loadCurrentTrack()
|
this.loadCurrentTrack()
|
||||||
} else {
|
} else {
|
||||||
@@ -89,6 +88,7 @@ export default class LocalAudioPlayer extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.emit('stateChange', 'LOADED')
|
this.emit('stateChange', 'LOADED')
|
||||||
|
|
||||||
if (this.playWhenReady) {
|
if (this.playWhenReady) {
|
||||||
this.playWhenReady = false
|
this.playWhenReady = false
|
||||||
this.play()
|
this.play()
|
||||||
@@ -205,10 +205,12 @@ export default class LocalAudioPlayer extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
play() {
|
play() {
|
||||||
|
this.playWhenReady = true
|
||||||
if (this.player) this.player.play()
|
if (this.player) this.player.play()
|
||||||
}
|
}
|
||||||
|
|
||||||
pause() {
|
pause() {
|
||||||
|
this.playWhenReady = false
|
||||||
if (this.player) this.player.pause()
|
if (this.player) this.player.pause()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -229,8 +231,11 @@ export default class LocalAudioPlayer extends EventEmitter {
|
|||||||
this.player.playbackRate = playbackRate
|
this.player.playbackRate = playbackRate
|
||||||
}
|
}
|
||||||
|
|
||||||
seek(time) {
|
seek(time, playWhenReady) {
|
||||||
if (!this.player) return
|
if (!this.player) return
|
||||||
|
|
||||||
|
this.playWhenReady = playWhenReady
|
||||||
|
|
||||||
if (this.isHlsTranscode) {
|
if (this.isHlsTranscode) {
|
||||||
// Seeking HLS stream
|
// Seeking HLS stream
|
||||||
var offsetTime = time - (this.currentTrack.startOffset || 0)
|
var offsetTime = time - (this.currentTrack.startOffset || 0)
|
||||||
@@ -255,7 +260,6 @@ export default class LocalAudioPlayer extends EventEmitter {
|
|||||||
this.player.currentTime = Math.max(0, offsetTime)
|
this.player.currentTime = Math.max(0, offsetTime)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setVolume(volume) {
|
setVolume(volume) {
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ export default class PlayerHandler {
|
|||||||
this.currentSessionId = null
|
this.currentSessionId = null
|
||||||
this.startTime = 0
|
this.startTime = 0
|
||||||
|
|
||||||
|
this.failedProgressSyncs = 0
|
||||||
this.lastSyncTime = 0
|
this.lastSyncTime = 0
|
||||||
this.lastSyncedAt = 0
|
this.lastSyncedAt = 0
|
||||||
this.listeningTimeSinceSync = 0
|
this.listeningTimeSinceSync = 0
|
||||||
@@ -186,6 +187,7 @@ export default class PlayerHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
prepareSession(session) {
|
prepareSession(session) {
|
||||||
|
this.failedProgressSyncs = 0
|
||||||
this.startTime = session.currentTime
|
this.startTime = session.currentTime
|
||||||
this.currentSessionId = session.id
|
this.currentSessionId = session.id
|
||||||
this.displayTitle = session.displayTitle
|
this.displayTitle = session.displayTitle
|
||||||
@@ -286,8 +288,15 @@ export default class PlayerHandler {
|
|||||||
currentTime
|
currentTime
|
||||||
}
|
}
|
||||||
this.listeningTimeSinceSync = 0
|
this.listeningTimeSinceSync = 0
|
||||||
this.ctx.$axios.$post(`/api/session/${this.currentSessionId}/sync`, syncData, { timeout: 1000 }).catch((error) => {
|
this.ctx.$axios.$post(`/api/session/${this.currentSessionId}/sync`, syncData, { timeout: 1000 }).then(() => {
|
||||||
|
this.failedProgressSyncs = 0
|
||||||
|
}).catch((error) => {
|
||||||
console.error('Failed to update session progress', error)
|
console.error('Failed to update session progress', error)
|
||||||
|
this.failedProgressSyncs++
|
||||||
|
if (this.failedProgressSyncs >= 2) {
|
||||||
|
this.ctx.showFailedProgressSyncs()
|
||||||
|
this.failedProgressSyncs = 0
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ module.exports = {
|
|||||||
'6': '1.5rem',
|
'6': '1.5rem',
|
||||||
'12': '3rem',
|
'12': '3rem',
|
||||||
'16': '4rem',
|
'16': '4rem',
|
||||||
|
'20': '5rem',
|
||||||
'24': '6rem',
|
'24': '6rem',
|
||||||
'32': '8rem',
|
'32': '8rem',
|
||||||
'48': '12rem',
|
'48': '12rem',
|
||||||
|
|||||||
Generated
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookshelf",
|
"name": "audiobookshelf",
|
||||||
"version": "2.0.18",
|
"version": "2.0.20",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookshelf",
|
"name": "audiobookshelf",
|
||||||
"version": "2.0.18",
|
"version": "2.0.20",
|
||||||
"description": "Self-hosted audiobook and podcast server",
|
"description": "Self-hosted audiobook and podcast server",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ docker run -d \
|
|||||||
-e AUDIOBOOKSHELF_GID=100 \
|
-e AUDIOBOOKSHELF_GID=100 \
|
||||||
-p 13378:80 \
|
-p 13378:80 \
|
||||||
-v </path/to/audiobooks>:/audiobooks \
|
-v </path/to/audiobooks>:/audiobooks \
|
||||||
-v </path/to/your/podcasts>:/podcasts \
|
-v </path/to/podcasts>:/podcasts \
|
||||||
-v </path/to/config>:/config \
|
-v </path/to/config>:/config \
|
||||||
-v </path/to/metadata>:/metadata \
|
-v </path/to/metadata>:/metadata \
|
||||||
--name audiobookshelf \
|
--name audiobookshelf \
|
||||||
@@ -90,6 +90,7 @@ docker start audiobookshelf
|
|||||||
### docker-compose.yml ###
|
### docker-compose.yml ###
|
||||||
services:
|
services:
|
||||||
audiobookshelf:
|
audiobookshelf:
|
||||||
|
container_name: audiobookshelf
|
||||||
image: ghcr.io/advplyr/audiobookshelf:latest
|
image: ghcr.io/advplyr/audiobookshelf:latest
|
||||||
environment:
|
environment:
|
||||||
- AUDIOBOOKSHELF_UID=99
|
- AUDIOBOOKSHELF_UID=99
|
||||||
@@ -97,8 +98,8 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- 13378:80
|
- 13378:80
|
||||||
volumes:
|
volumes:
|
||||||
- </path/to/your/audiobooks>:/audiobooks
|
- </path/to/audiobooks>:/audiobooks
|
||||||
- </path/to/your/podcasts>:/podcasts
|
- </path/to/podcasts>:/podcasts
|
||||||
- </path/to/config>:/config
|
- </path/to/config>:/config
|
||||||
- </path/to/metadata>:/metadata
|
- </path/to/metadata>:/metadata
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -428,6 +428,15 @@ class Db {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getAllSessions() {
|
||||||
|
return this.sessionsDb.select(() => true).then((results) => {
|
||||||
|
return results.data || []
|
||||||
|
}).catch((error) => {
|
||||||
|
Logger.error('[Db] Failed to select sessions', error)
|
||||||
|
return []
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
selectUserSessions(userId) {
|
selectUserSessions(userId) {
|
||||||
return this.sessionsDb.select((session) => session.userId === userId).then((results) => {
|
return this.sessionsDb.select((session) => session.userId === userId).then((results) => {
|
||||||
return results.data || []
|
return results.data || []
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
const Logger = require('../Logger')
|
const Logger = require('../Logger')
|
||||||
const { isObject } = require('../utils/index')
|
const { isObject, toNumber } = require('../utils/index')
|
||||||
|
|
||||||
class MeController {
|
class MeController {
|
||||||
constructor() { }
|
constructor() { }
|
||||||
@@ -7,7 +7,22 @@ class MeController {
|
|||||||
// GET: api/me/listening-sessions
|
// GET: api/me/listening-sessions
|
||||||
async getListeningSessions(req, res) {
|
async getListeningSessions(req, res) {
|
||||||
var listeningSessions = await this.getUserListeningSessionsHelper(req.user.id)
|
var listeningSessions = await this.getUserListeningSessionsHelper(req.user.id)
|
||||||
res.json(listeningSessions.slice(0, 10))
|
|
||||||
|
const itemsPerPage = toNumber(req.query.itemsPerPage, 10) || 10
|
||||||
|
const page = toNumber(req.query.page, 0)
|
||||||
|
|
||||||
|
const start = page * itemsPerPage
|
||||||
|
const sessions = listeningSessions.slice(start, start + itemsPerPage)
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
total: listeningSessions.length,
|
||||||
|
numPages: Math.ceil(listeningSessions.length / itemsPerPage),
|
||||||
|
page,
|
||||||
|
itemsPerPage,
|
||||||
|
sessions
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json(payload)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GET: api/me/listening-stats
|
// GET: api/me/listening-stats
|
||||||
@@ -16,6 +31,15 @@ class MeController {
|
|||||||
res.json(listeningStats)
|
res.json(listeningStats)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GET: api/me/progress/:id/:episodeId?
|
||||||
|
async getMediaProgress(req, res) {
|
||||||
|
const mediaProgress = req.user.getMediaProgress(req.id, req.episodeId || null)
|
||||||
|
if (!mediaProgress) {
|
||||||
|
return res.sendStatus(404)
|
||||||
|
}
|
||||||
|
res.json(mediaProgress)
|
||||||
|
}
|
||||||
|
|
||||||
// DELETE: api/me/progress/:id
|
// DELETE: api/me/progress/:id
|
||||||
async removeMediaProgress(req, res) {
|
async removeMediaProgress(req, res) {
|
||||||
var wasRemoved = req.user.removeMediaProgress(req.params.id)
|
var wasRemoved = req.user.removeMediaProgress(req.params.id)
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
const Logger = require('../Logger')
|
const Logger = require('../Logger')
|
||||||
|
const { toNumber } = require('../utils/index')
|
||||||
|
|
||||||
class SessionController {
|
class SessionController {
|
||||||
constructor() { }
|
constructor() { }
|
||||||
@@ -7,6 +8,46 @@ class SessionController {
|
|||||||
return res.json(req.session)
|
return res.json(req.session)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getAllWithUserData(req, res) {
|
||||||
|
if (!req.user.isAdminOrUp) {
|
||||||
|
Logger.error(`[SessionController] getAllWithUserData: Non-admin user requested all session data ${req.user.id}/"${req.user.username}"`)
|
||||||
|
return res.sendStatus(404)
|
||||||
|
}
|
||||||
|
|
||||||
|
var listeningSessions = []
|
||||||
|
if (req.query.user) {
|
||||||
|
listeningSessions = await this.getUserListeningSessionsHelper(req.query.user)
|
||||||
|
} else {
|
||||||
|
listeningSessions = await this.getAllSessionsWithUserData()
|
||||||
|
}
|
||||||
|
|
||||||
|
const itemsPerPage = toNumber(req.query.itemsPerPage, 10) || 10
|
||||||
|
const page = toNumber(req.query.page, 0)
|
||||||
|
|
||||||
|
const start = page * itemsPerPage
|
||||||
|
const sessions = listeningSessions.slice(start, start + itemsPerPage)
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
total: listeningSessions.length,
|
||||||
|
numPages: Math.ceil(listeningSessions.length / itemsPerPage),
|
||||||
|
page,
|
||||||
|
itemsPerPage,
|
||||||
|
sessions
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.query.user) {
|
||||||
|
payload.userFilter = req.query.user
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json(payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
getSession(req, res) {
|
||||||
|
var libraryItem = this.db.getLibraryItem(req.session.libraryItemId)
|
||||||
|
var sessionForClient = req.session.toJSONForClient(libraryItem)
|
||||||
|
res.json(sessionForClient)
|
||||||
|
}
|
||||||
|
|
||||||
// POST: api/session/:id/sync
|
// POST: api/session/:id/sync
|
||||||
sync(req, res) {
|
sync(req, res) {
|
||||||
this.playbackSessionManager.syncSessionRequest(req.user, req.session, req.body, res)
|
this.playbackSessionManager.syncSessionRequest(req.user, req.session, req.body, res)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
const Logger = require('../Logger')
|
const Logger = require('../Logger')
|
||||||
const User = require('../objects/user/User')
|
const User = require('../objects/user/User')
|
||||||
|
|
||||||
const { getId } = require('../utils/index')
|
const { getId, toNumber } = require('../utils/index')
|
||||||
|
|
||||||
class UserController {
|
class UserController {
|
||||||
constructor() { }
|
constructor() { }
|
||||||
@@ -142,8 +142,24 @@ class UserController {
|
|||||||
if (!req.user.isAdminOrUp && req.user.id !== req.params.id) {
|
if (!req.user.isAdminOrUp && req.user.id !== req.params.id) {
|
||||||
return res.sendStatus(403)
|
return res.sendStatus(403)
|
||||||
}
|
}
|
||||||
|
|
||||||
var listeningSessions = await this.getUserListeningSessionsHelper(req.params.id)
|
var listeningSessions = await this.getUserListeningSessionsHelper(req.params.id)
|
||||||
res.json(listeningSessions.slice(0, 10))
|
|
||||||
|
const itemsPerPage = toNumber(req.query.itemsPerPage, 10) || 10
|
||||||
|
const page = toNumber(req.query.page, 0)
|
||||||
|
|
||||||
|
const start = page * itemsPerPage
|
||||||
|
const sessions = listeningSessions.slice(start, start + itemsPerPage)
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
total: listeningSessions.length,
|
||||||
|
numPages: Math.ceil(listeningSessions.length / itemsPerPage),
|
||||||
|
page,
|
||||||
|
itemsPerPage,
|
||||||
|
sessions
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json(payload)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GET: api/users/:id/listening-stats
|
// GET: api/users/:id/listening-stats
|
||||||
|
|||||||
@@ -151,7 +151,7 @@ class AbMergeManager {
|
|||||||
input: coverPath,
|
input: coverPath,
|
||||||
options: ['-f image2pipe']
|
options: ['-f image2pipe']
|
||||||
})
|
})
|
||||||
ffmpegOptions.push('-vf [2:v]crop=trunc(iw/2)*2:trunc(ih/2)*2')
|
ffmpegOptions.push('-c:v copy')
|
||||||
ffmpegOptions.push('-map 2:v')
|
ffmpegOptions.push('-map 2:v')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ class AudioMetadataMangaer {
|
|||||||
Ffmpeg premapped id3 tags: https://wiki.multimedia.cx/index.php/FFmpeg_Metadata
|
Ffmpeg premapped id3 tags: https://wiki.multimedia.cx/index.php/FFmpeg_Metadata
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const ffmpegOptions = ['-c copy', '-map_metadata 1', `-metadata track=${audioFile.index}`, '-write_id3v2 1', '-movflags use_metadata_tags']
|
const ffmpegOptions = ['-c copy', '-map_chapters 1', '-map_metadata 1', `-metadata track=${audioFile.index}`, '-write_id3v2 1', '-movflags use_metadata_tags']
|
||||||
var workerData = {
|
var workerData = {
|
||||||
inputs: ffmpegInputs,
|
inputs: ffmpegInputs,
|
||||||
options: ffmpegOptions,
|
options: ffmpegOptions,
|
||||||
|
|||||||
@@ -141,6 +141,9 @@ class BackupManager {
|
|||||||
if (error.message === "Bad archive") {
|
if (error.message === "Bad archive") {
|
||||||
Logger.warn(`[BackupManager] Backup appears to be corrupted: ${fullFilePath}`)
|
Logger.warn(`[BackupManager] Backup appears to be corrupted: ${fullFilePath}`)
|
||||||
continue;
|
continue;
|
||||||
|
} else if (error.message === "unexpected end of file") {
|
||||||
|
Logger.warn(`[BackupManager] Backup appears to be corrupted: ${fullFilePath}`)
|
||||||
|
continue;
|
||||||
} else {
|
} else {
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ const abmetadataGenerator = require('../utils/abmetadataGenerator')
|
|||||||
const LibraryFile = require('./files/LibraryFile')
|
const LibraryFile = require('./files/LibraryFile')
|
||||||
const Book = require('./mediaTypes/Book')
|
const Book = require('./mediaTypes/Book')
|
||||||
const Podcast = require('./mediaTypes/Podcast')
|
const Podcast = require('./mediaTypes/Podcast')
|
||||||
const Video = require('./mediatypes/Video')
|
const Video = require('./mediaTypes/Video')
|
||||||
const { areEquivalent, copyValue, getId } = require('../utils/index')
|
const { areEquivalent, copyValue, getId } = require('../utils/index')
|
||||||
|
|
||||||
class LibraryItem {
|
class LibraryItem {
|
||||||
|
|||||||
@@ -131,6 +131,7 @@ class ApiRouter {
|
|||||||
//
|
//
|
||||||
this.router.get('/me/listening-sessions', MeController.getListeningSessions.bind(this))
|
this.router.get('/me/listening-sessions', MeController.getListeningSessions.bind(this))
|
||||||
this.router.get('/me/listening-stats', MeController.getListeningStats.bind(this))
|
this.router.get('/me/listening-stats', MeController.getListeningStats.bind(this))
|
||||||
|
this.router.get('/me/progress/:id/:episodeId?', MeController.getMediaProgress.bind(this))
|
||||||
this.router.patch('/me/progress/batch/update', MeController.batchUpdateMediaProgress.bind(this))
|
this.router.patch('/me/progress/batch/update', MeController.batchUpdateMediaProgress.bind(this))
|
||||||
this.router.patch('/me/progress/:id', MeController.createUpdateMediaProgress.bind(this))
|
this.router.patch('/me/progress/:id', MeController.createUpdateMediaProgress.bind(this))
|
||||||
this.router.delete('/me/progress/:id', MeController.removeMediaProgress.bind(this))
|
this.router.delete('/me/progress/:id', MeController.removeMediaProgress.bind(this))
|
||||||
@@ -173,6 +174,8 @@ class ApiRouter {
|
|||||||
//
|
//
|
||||||
// Playback Session Routes
|
// Playback Session Routes
|
||||||
//
|
//
|
||||||
|
this.router.get('/sessions', SessionController.getAllWithUserData.bind(this))
|
||||||
|
this.router.get('/session/:id', SessionController.middleware.bind(this), SessionController.getSession.bind(this))
|
||||||
this.router.post('/session/:id/sync', SessionController.middleware.bind(this), SessionController.sync.bind(this))
|
this.router.post('/session/:id/sync', SessionController.middleware.bind(this), SessionController.sync.bind(this))
|
||||||
this.router.post('/session/:id/close', SessionController.middleware.bind(this), SessionController.close.bind(this))
|
this.router.post('/session/:id/close', SessionController.middleware.bind(this), SessionController.close.bind(this))
|
||||||
this.router.post('/session/local', SessionController.syncLocal.bind(this))
|
this.router.post('/session/local', SessionController.syncLocal.bind(this))
|
||||||
@@ -308,6 +311,19 @@ class ApiRouter {
|
|||||||
return userSessions.sort((a, b) => b.updatedAt - a.updatedAt)
|
return userSessions.sort((a, b) => b.updatedAt - a.updatedAt)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getAllSessionsWithUserData() {
|
||||||
|
var sessions = await this.db.getAllSessions()
|
||||||
|
sessions.sort((a, b) => b.updatedAt - a.updatedAt)
|
||||||
|
return sessions.map(se => {
|
||||||
|
var user = this.db.users.find(u => u.id === se.userId)
|
||||||
|
var _se = {
|
||||||
|
...se,
|
||||||
|
user: user ? { id: user.id, username: user.username } : null
|
||||||
|
}
|
||||||
|
return _se
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
async getUserListeningStatsHelpers(userId) {
|
async getUserListeningStatsHelpers(userId) {
|
||||||
const today = date.format(new Date(), 'YYYY-MM-DD')
|
const today = date.format(new Date(), 'YYYY-MM-DD')
|
||||||
|
|
||||||
|
|||||||
@@ -696,7 +696,7 @@ class Scanner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Add or set author if not set
|
// Add or set author if not set
|
||||||
if (matchData.author && !libraryItem.media.metadata.authorName || options.overrideDetails) {
|
if (matchData.author && (!libraryItem.media.metadata.authorName || options.overrideDetails)) {
|
||||||
if (!Array.isArray(matchData.author)) matchData.author = [matchData.author]
|
if (!Array.isArray(matchData.author)) matchData.author = [matchData.author]
|
||||||
const authorPayload = []
|
const authorPayload = []
|
||||||
for (let index = 0; index < matchData.author.length; index++) {
|
for (let index = 0; index < matchData.author.length; index++) {
|
||||||
@@ -714,7 +714,7 @@ class Scanner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Add or set series if not set
|
// Add or set series if not set
|
||||||
if (matchData.series && !libraryItem.media.metadata.seriesName || options.overrideDetails) {
|
if (matchData.series && (!libraryItem.media.metadata.seriesName || options.overrideDetails)) {
|
||||||
if (!Array.isArray(matchData.series)) matchData.series = [{ series: matchData.series, volumeNumber: matchData.volumeNumber }]
|
if (!Array.isArray(matchData.series)) matchData.series = [{ series: matchData.series, volumeNumber: matchData.volumeNumber }]
|
||||||
const seriesPayload = []
|
const seriesPayload = []
|
||||||
for (let index = 0; index < matchData.series.length; index++) {
|
for (let index = 0; index < matchData.series.length; index++) {
|
||||||
|
|||||||
@@ -125,3 +125,8 @@ module.exports.copyValue = (val) => {
|
|||||||
module.exports.encodeUriPath = (path) => {
|
module.exports.encodeUriPath = (path) => {
|
||||||
return path.replace(/\\/g, '/').replace(/%/g, '%25').replace(/#/g, '%23')
|
return path.replace(/\\/g, '/').replace(/%/g, '%25').replace(/#/g, '%23')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
module.exports.toNumber = (val, fallback = 0) => {
|
||||||
|
if (isNaN(val) || val === null) return fallback
|
||||||
|
return Number(val)
|
||||||
|
}
|
||||||
@@ -32,6 +32,7 @@ module.exports = {
|
|||||||
var itemProgress = user.getMediaProgress(li.id)
|
var itemProgress = user.getMediaProgress(li.id)
|
||||||
if (filter === 'Finished' && (itemProgress && itemProgress.isFinished)) return true
|
if (filter === 'Finished' && (itemProgress && itemProgress.isFinished)) return true
|
||||||
if (filter === 'Not Started' && !itemProgress) return true
|
if (filter === 'Not Started' && !itemProgress) return true
|
||||||
|
if (filter === 'Not Finished' && !itemProgress || !itemProgress.isFinished) return true
|
||||||
if (filter === 'In Progress' && (itemProgress && itemProgress.inProgress)) return true
|
if (filter === 'In Progress' && (itemProgress && itemProgress.inProgress)) return true
|
||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user