mirror of
				https://github.com/advplyr/audiobookshelf.git
				synced 2025-10-27 11:18:14 +01:00 
			
		
		
		
	Update get User API endpoint to load media progress from db
This commit is contained in:
		
							parent
							
								
									1ebe8a6f4c
								
							
						
					
					
						commit
						361732a463
					
				@ -13,8 +13,8 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    <div v-if="imageFailed" class="absolute top-0 left-0 right-0 bottom-0 w-full h-full bg-red-100" :style="{ padding: placeholderCoverPadding + 'rem' }">
 | 
					    <div v-if="imageFailed" class="absolute top-0 left-0 right-0 bottom-0 w-full h-full bg-red-100" :style="{ padding: placeholderCoverPadding + 'rem' }">
 | 
				
			||||||
      <div class="w-full h-full border-2 border-error flex flex-col items-center justify-center">
 | 
					      <div class="w-full h-full border-2 border-error flex flex-col items-center justify-center">
 | 
				
			||||||
        <img src="/Logo.png" class="mb-2" :style="{ height: 64 * sizeMultiplier + 'px' }" />
 | 
					        <img v-if="width > 100" src="/Logo.png" class="mb-2" :style="{ height: 40 * sizeMultiplier + 'px' }" />
 | 
				
			||||||
        <p class="text-center text-error" :style="{ fontSize: sizeMultiplier + 'rem' }">Invalid Cover</p>
 | 
					        <p class="text-center text-error" :style="{ fontSize: invalidCoverFontSize + 'rem' }">Invalid Cover</p>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -58,6 +58,9 @@ export default {
 | 
				
			|||||||
    sizeMultiplier() {
 | 
					    sizeMultiplier() {
 | 
				
			||||||
      return this.width / 120
 | 
					      return this.width / 120
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    invalidCoverFontSize() {
 | 
				
			||||||
 | 
					      return Math.max(this.sizeMultiplier * 0.8, 0.5)
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    placeholderCoverPadding() {
 | 
					    placeholderCoverPadding() {
 | 
				
			||||||
      return 0.8 * this.sizeMultiplier
 | 
					      return 0.8 * this.sizeMultiplier
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
				
			|||||||
@ -47,7 +47,7 @@
 | 
				
			|||||||
      <div class="py-2">
 | 
					      <div class="py-2">
 | 
				
			||||||
        <h1 class="text-lg mb-2 text-white text-opacity-90 px-2 sm:px-0">{{ $strings.HeaderSavedMediaProgress }}</h1>
 | 
					        <h1 class="text-lg mb-2 text-white text-opacity-90 px-2 sm:px-0">{{ $strings.HeaderSavedMediaProgress }}</h1>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <table v-if="mediaProgressWithMedia.length" class="userAudiobooksTable">
 | 
					        <table v-if="mediaProgress.length" class="userAudiobooksTable">
 | 
				
			||||||
          <tr class="bg-primary bg-opacity-40">
 | 
					          <tr class="bg-primary bg-opacity-40">
 | 
				
			||||||
            <th class="w-16 text-left">{{ $strings.LabelItem }}</th>
 | 
					            <th class="w-16 text-left">{{ $strings.LabelItem }}</th>
 | 
				
			||||||
            <th class="text-left"></th>
 | 
					            <th class="text-left"></th>
 | 
				
			||||||
@ -55,19 +55,14 @@
 | 
				
			|||||||
            <th class="w-40 hidden sm:table-cell">{{ $strings.LabelStartedAt }}</th>
 | 
					            <th class="w-40 hidden sm:table-cell">{{ $strings.LabelStartedAt }}</th>
 | 
				
			||||||
            <th class="w-40 hidden sm:table-cell">{{ $strings.LabelLastUpdate }}</th>
 | 
					            <th class="w-40 hidden sm:table-cell">{{ $strings.LabelLastUpdate }}</th>
 | 
				
			||||||
          </tr>
 | 
					          </tr>
 | 
				
			||||||
          <tr v-for="item in mediaProgressWithMedia" :key="item.id" :class="!item.isFinished ? '' : 'isFinished'">
 | 
					          <tr v-for="item in mediaProgress" :key="item.id" :class="!item.isFinished ? '' : 'isFinished'">
 | 
				
			||||||
            <td>
 | 
					            <td>
 | 
				
			||||||
              <covers-book-cover :width="50" :library-item="item" :book-cover-aspect-ratio="bookCoverAspectRatio" />
 | 
					              <covers-preview-cover v-if="item.coverPath" :width="50" :src="$store.getters['globals/getLibraryItemCoverSrcById'](item.libraryItemId, item.mediaUpdatedAt)" :book-cover-aspect-ratio="bookCoverAspectRatio" :show-resolution="false" />
 | 
				
			||||||
 | 
					              <div v-else class="bg-primary flex items-center justify-center text-center text-xs text-gray-400 p-1" :style="{ width: '50px', height: 50 * bookCoverAspectRatio + 'px' }">No Cover</div>
 | 
				
			||||||
            </td>
 | 
					            </td>
 | 
				
			||||||
            <td>
 | 
					            <td>
 | 
				
			||||||
              <template v-if="item.media && item.media.metadata && item.episode">
 | 
					              <p>{{ item.displayTitle || 'Unknown' }}</p>
 | 
				
			||||||
                <p>{{ item.episode.title || 'Unknown' }}</p>
 | 
					              <p v-if="item.displaySubtitle" class="text-white text-opacity-50 text-sm font-sans">{{ item.displaySubtitle }}</p>
 | 
				
			||||||
                <p class="text-white text-opacity-50 text-sm font-sans">{{ item.media.metadata.title }}</p>
 | 
					 | 
				
			||||||
              </template>
 | 
					 | 
				
			||||||
              <template v-else-if="item.media && item.media.metadata">
 | 
					 | 
				
			||||||
                <p>{{ item.media.metadata.title || 'Unknown' }}</p>
 | 
					 | 
				
			||||||
                <p v-if="item.media.metadata.authorName" class="text-white text-opacity-50 text-sm font-sans">by {{ item.media.metadata.authorName }}</p>
 | 
					 | 
				
			||||||
              </template>
 | 
					 | 
				
			||||||
            </td>
 | 
					            </td>
 | 
				
			||||||
            <td class="text-center">
 | 
					            <td class="text-center">
 | 
				
			||||||
              <p class="text-sm">{{ Math.floor(item.progress * 100) }}%</p>
 | 
					              <p class="text-sm">{{ Math.floor(item.progress * 100) }}%</p>
 | 
				
			||||||
@ -124,9 +119,6 @@ export default {
 | 
				
			|||||||
    mediaProgress() {
 | 
					    mediaProgress() {
 | 
				
			||||||
      return this.user.mediaProgress.sort((a, b) => b.lastUpdate - a.lastUpdate)
 | 
					      return this.user.mediaProgress.sort((a, b) => b.lastUpdate - a.lastUpdate)
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    mediaProgressWithMedia() {
 | 
					 | 
				
			||||||
      return this.mediaProgress.filter((mp) => mp.media)
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    totalListeningTime() {
 | 
					    totalListeningTime() {
 | 
				
			||||||
      return this.listeningStats.totalTime || 0
 | 
					      return this.listeningStats.totalTime || 0
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
				
			|||||||
@ -32,13 +32,60 @@ class UserController {
 | 
				
			|||||||
    })
 | 
					    })
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * GET: /api/users/:id
 | 
				
			||||||
 | 
					   * Get a single user toJSONForBrowser
 | 
				
			||||||
 | 
					   * Media progress items include: `displayTitle`, `displaySubtitle` (for podcasts), `coverPath` and `mediaUpdatedAt`
 | 
				
			||||||
 | 
					   * 
 | 
				
			||||||
 | 
					   * @param {import("express").Request} req 
 | 
				
			||||||
 | 
					   * @param {import("express").Response} res 
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
  async findOne(req, res) {
 | 
					  async findOne(req, res) {
 | 
				
			||||||
    if (!req.user.isAdminOrUp) {
 | 
					    if (!req.user.isAdminOrUp) {
 | 
				
			||||||
      Logger.error('User other than admin attempting to get user', req.user)
 | 
					      Logger.error('User other than admin attempting to get user', req.user)
 | 
				
			||||||
      return res.sendStatus(403)
 | 
					      return res.sendStatus(403)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    res.json(this.userJsonWithItemProgressDetails(req.reqUser, !req.user.isRoot))
 | 
					    // Get user media progress with associated mediaItem
 | 
				
			||||||
 | 
					    const mediaProgresses = await Database.models.mediaProgress.findAll({
 | 
				
			||||||
 | 
					      where: {
 | 
				
			||||||
 | 
					        userId: req.reqUser.id
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      include: [
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          model: Database.models.book,
 | 
				
			||||||
 | 
					          attributes: ['id', 'title', 'coverPath', 'updatedAt']
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          model: Database.models.podcastEpisode,
 | 
				
			||||||
 | 
					          attributes: ['id', 'title'],
 | 
				
			||||||
 | 
					          include: {
 | 
				
			||||||
 | 
					            model: Database.models.podcast,
 | 
				
			||||||
 | 
					            attributes: ['id', 'title', 'coverPath', 'updatedAt']
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      ]
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const oldMediaProgresses = mediaProgresses.map(mp => {
 | 
				
			||||||
 | 
					      const oldMediaProgress = mp.getOldMediaProgress()
 | 
				
			||||||
 | 
					      oldMediaProgress.displayTitle = mp.mediaItem?.title
 | 
				
			||||||
 | 
					      if (mp.mediaItem?.podcast) {
 | 
				
			||||||
 | 
					        oldMediaProgress.displaySubtitle = mp.mediaItem.podcast?.title
 | 
				
			||||||
 | 
					        oldMediaProgress.coverPath = mp.mediaItem.podcast?.coverPath
 | 
				
			||||||
 | 
					        oldMediaProgress.mediaUpdatedAt = mp.mediaItem.podcast?.updatedAt
 | 
				
			||||||
 | 
					      } else if (mp.mediaItem) {
 | 
				
			||||||
 | 
					        oldMediaProgress.coverPath = mp.mediaItem.coverPath
 | 
				
			||||||
 | 
					        oldMediaProgress.mediaUpdatedAt = mp.mediaItem.updatedAt
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      return oldMediaProgress
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const userJson = req.reqUser.toJSONForBrowser(!req.user.isRoot)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    userJson.mediaProgress = oldMediaProgresses
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    res.json(userJson)
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async create(req, res) {
 | 
					  async create(req, res) {
 | 
				
			||||||
 | 
				
			|||||||
@ -352,34 +352,6 @@ class ApiRouter {
 | 
				
			|||||||
  //
 | 
					  //
 | 
				
			||||||
  // Helper Methods
 | 
					  // Helper Methods
 | 
				
			||||||
  //
 | 
					  //
 | 
				
			||||||
  userJsonWithItemProgressDetails(user, hideRootToken = false) {
 | 
					 | 
				
			||||||
    const json = user.toJSONForBrowser(hideRootToken)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    json.mediaProgress = json.mediaProgress.map(lip => {
 | 
					 | 
				
			||||||
      const libraryItem = Database.libraryItems.find(li => li.id === lip.libraryItemId)
 | 
					 | 
				
			||||||
      if (!libraryItem) {
 | 
					 | 
				
			||||||
        Logger.warn('[ApiRouter] Library item not found for users progress ' + lip.libraryItemId)
 | 
					 | 
				
			||||||
        lip.media = null
 | 
					 | 
				
			||||||
      } else {
 | 
					 | 
				
			||||||
        if (lip.episodeId) {
 | 
					 | 
				
			||||||
          const episode = libraryItem.mediaType === 'podcast' ? libraryItem.media.getEpisode(lip.episodeId) : null
 | 
					 | 
				
			||||||
          if (!episode) {
 | 
					 | 
				
			||||||
            Logger.warn(`[ApiRouter] Episode ${lip.episodeId} not found for user media progress, podcast: ${libraryItem.media.metadata.title}`)
 | 
					 | 
				
			||||||
            lip.media = null
 | 
					 | 
				
			||||||
          } else {
 | 
					 | 
				
			||||||
            lip.media = libraryItem.media.toJSONExpanded()
 | 
					 | 
				
			||||||
            lip.episode = episode.toJSON()
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
        } else {
 | 
					 | 
				
			||||||
          lip.media = libraryItem.media.toJSONExpanded()
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      return lip
 | 
					 | 
				
			||||||
    }).filter(lip => !!lip)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return json
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  /**
 | 
					  /**
 | 
				
			||||||
   * Remove library item and associated entities
 | 
					   * Remove library item and associated entities
 | 
				
			||||||
   * @param {string} mediaType 
 | 
					   * @param {string} mediaType 
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
		Reference in New Issue
	
	Block a user