mirror of
				https://github.com/advplyr/audiobookshelf.git
				synced 2025-10-27 11:18:14 +01:00 
			
		
		
		
	Update:Paginated listening sessions
This commit is contained in:
		
							parent
							
								
									0e1692d26b
								
							
						
					
					
						commit
						3171ce5aba
					
				@ -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'
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
				
			|||||||
@ -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 []
 | 
				
			||||||
      })
 | 
					      })
 | 
				
			||||||
 | 
				
			|||||||
@ -18,39 +18,46 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
      <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 ({{ listeningSessions.length }})</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() {
 | 
				
			||||||
 | 
				
			|||||||
@ -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
 | 
				
			||||||
 | 
				
			|||||||
@ -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
 | 
				
			||||||
 | 
				
			|||||||
@ -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++) {
 | 
				
			||||||
 | 
				
			|||||||
@ -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)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
		Loading…
	
		Reference in New Issue
	
	Block a user