Compare commits

..

17 Commits

Author SHA1 Message Date
advplyr e0546c6164 Version bump 2.0.19 2022-06-04 14:42:36 -05:00
advplyr be7ccfb209 Merge pull request #678 from jmt-gh/issue_676_chapter_metadata
Support embedding updated chapter metadata (issue #676)
2022-06-04 14:02:44 -05:00
advplyr 938a8c6f80 Fix:Casing typo in LibraryItem 2022-06-04 13:00:51 -05:00
advplyr 5cd343cb01 Add:All listening sessions config page 2022-06-04 12:44:42 -05:00
jmt-gh ab0094a53b Support embedding updated chapter metadata (676)
This commit resolves issue #676. The embed metadata tool was missing the
flag that tells ffmpeg to not only update the "top" metadata, but also
the chapter metadata.
2022-06-04 10:17:42 -07:00
advplyr 2d5e4ebcf0 Add:Audio player next/prev chapter buttons 2022-06-04 12:07:38 -05:00
advplyr 3171ce5aba Update:Paginated listening sessions 2022-06-04 10:52:37 -05:00
advplyr 0e1692d26b Fix:Matching authors with multiple authors split by comma #667 2022-06-03 19:21:31 -05:00
advplyr e8cd18eac2 Add:Alert when progress is not syncing 2022-06-03 19:11:13 -05:00
advplyr bf928692d5 Update:API route for getting playback session and getting media progress 2022-06-03 18:59:42 -05:00
advplyr 792490b629 Merge pull request #664 from bskrtich/docker_updates
feat: Updates to docker file and gh action
2022-06-03 05:02:11 -05:00
advplyr 0d1ff35c5e Add:Not Finished progress filter #650 2022-06-02 18:20:18 -05:00
advplyr 67e02fddbd Comment out expand on player ui 2022-06-02 17:54:07 -05:00
advplyr 09beb6a2ae Merge branch 'master' of https://github.com/advplyr/audiobookshelf 2022-06-02 16:32:42 -05:00
advplyr 2dba17a7ae Merge pull request #651 from selfhost-alt/handle-another-backup-parse-error
Gracefully handle unexpected end of file when listing backup files
2022-06-02 07:26:42 -05:00
Brandon Skrtich 4900649908 feat: Updates to docker file and gh action
* Clean up Dockerfile
* Add health check to Dockerfile
* Update gh action versions
2022-06-02 05:55:01 +00:00
Selfhost Alt 1350a91fba Handle another type of corrupted backup file 2022-05-30 23:53:00 -07:00
33 changed files with 534 additions and 106 deletions
+21 -15
View File
@@ -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,28 +54,28 @@ 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
cache-from: type=local,src=/tmp/.buildx-cache cache-from: type=local,src=/tmp/.buildx-cache
cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max
- name: Move cache - name: Move cache
run: | run: |
rm -rf /tmp/.buildx-cache rm -rf /tmp/.buildx-cache
+14 -4
View File
@@ -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"]
+5
View File
@@ -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',
+6 -1
View File
@@ -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() {
+1 -1
View File
@@ -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']
+15 -9
View File
@@ -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')
+27 -2
View File
@@ -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)
}, },
+12 -3
View File
@@ -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'
} }
+3 -1
View File
@@ -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) => {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "audiobookshelf-client", "name": "audiobookshelf-client",
"version": "2.0.18", "version": "2.0.19",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "audiobookshelf-client", "name": "audiobookshelf-client",
"version": "2.0.18", "version": "2.0.19",
"description": "Self-hosted audiobook and podcast client", "description": "Self-hosted audiobook and podcast client",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
+3 -3
View File
@@ -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>
+194
View File
@@ -0,0 +1,194 @@
<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">
<table class="userSessionsTable">
<tr class="bg-primary bg-opacity-40">
<th class="flex-grow text-left">Item</th>
<th class="w-20 text-left">User</th>
<th class="w-32 text-left hidden md:table-cell">Play Method</th>
<th class="w-40 text-left hidden sm:table-cell">Device Info</th>
<th class="w-20">Listened</th>
<th class="w-20">Last Time</th>
<th class="w-40 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">
<p class="text-sm text-gray-200">{{ session.displayTitle }}</p>
<p class="text-xs text-gray-400">{{ 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">{{ $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>
.userSessionsTable {
border-collapse: collapse;
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>
+3 -1
View File
@@ -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 []
}) })
+66 -40
View File
@@ -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="flex-grow text-left">Item</th>
<th class="w-40 text-left hidden sm:table-cell">Device Info</th> <th class="w-32 text-left hidden md:table-cell">Play Method</th>
<th class="w-20">Listened</th> <th class="w-40 text-left hidden sm:table-cell">Device Info</th>
<th class="w-20">Last Time</th> <th class="w-20">Listened</th>
<th class="w-40 hidden sm:table-cell">Last Update</th> <th class="w-20">Last Time</th>
</tr> <th class="w-40 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">
<p class="text-xs text-gray-400">{{ session.displayAuthor }}</p> <p class="text-sm text-gray-200">{{ session.displayTitle }}</p>
</td> <p class="text-xs text-gray-400">{{ 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">{{ $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() {
+2
View File
@@ -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)
+6 -3
View File
@@ -71,7 +71,7 @@ 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.playWhenReady = !this.player.paused
this.startTime = this.currentTrack.startOffset this.startTime = this.currentTrack.startOffset
this.loadCurrentTrack() this.loadCurrentTrack()
} else { } else {
@@ -89,6 +89,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()
@@ -229,8 +230,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 +259,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) {
+10 -1
View File
@@ -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
}
}) })
} }
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "audiobookshelf", "name": "audiobookshelf",
"version": "2.0.18", "version": "2.0.19",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "audiobookshelf", "name": "audiobookshelf",
"version": "2.0.18", "version": "2.0.19",
"description": "Self-hosted audiobook and podcast server", "description": "Self-hosted audiobook and podcast server",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
+5 -4
View File
@@ -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
``` ```
@@ -195,7 +196,7 @@ server
proxy_redirect http:// https://; proxy_redirect http:// https://;
} }
} }
``` ```
### Apache Reverse Proxy ### Apache Reverse Proxy
+9
View File
@@ -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 || []
+26 -2
View File
@@ -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)
+41
View File
@@ -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)
+18 -2
View File
@@ -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
+2 -2
View File
@@ -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,
@@ -137,4 +137,4 @@ class AudioMetadataMangaer {
}) })
} }
} }
module.exports = AudioMetadataMangaer module.exports = AudioMetadataMangaer
+3
View File
@@ -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
} }
+1 -1
View File
@@ -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 {
+16
View File
@@ -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')
+2 -2
View File
@@ -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++) {
+5
View File
@@ -124,4 +124,9 @@ 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)
} }
+1
View File
@@ -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
}) })