Merge branch 'advplyr:master' into show-subtitles

This commit is contained in:
mikiher 2024-07-05 00:17:01 +03:00 committed by GitHub
commit 0042604e6d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 90 additions and 17 deletions

View File

@ -341,7 +341,7 @@ export default {
if (this.recentEpisode) return this.recentEpisode.title
const ignorePrefix = this.orderBy === 'media.metadata.title' && this.sortingIgnorePrefix
if (this.collapsedSeries) return ignorePrefix ? this.collapsedSeries.nameIgnorePrefix : this.collapsedSeries.name
return ignorePrefix ? this.mediaMetadata.titleIgnorePrefix : this.title || '\u00A0'
return ignorePrefix ? this.mediaMetadata.titleIgnorePrefix || '\u00A0' : this.title || '\u00A0'
},
displaySubtitle() {
if (!this.libraryItem) return '\u00A0'
@ -369,7 +369,10 @@ export default {
if (this.orderBy === 'media.duration') return 'Duration: ' + this.$elapsedPrettyExtended(this.media.duration, false)
if (this.orderBy === 'size') return 'Size: ' + this.$bytesPretty(this._libraryItem.size)
if (this.orderBy === 'media.numTracks') return `${this.numEpisodes} Episodes`
if (this.orderBy === 'media.metadata.publishedYear' && this.mediaMetadata.publishedYear) return 'Published ' + this.mediaMetadata.publishedYear
if (this.orderBy === 'media.metadata.publishedYear') {
if (this.mediaMetadata.publishedYear) return 'Published ' + this.mediaMetadata.publishedYear
return '\u00A0'
}
return null
},
episodeProgress() {

View File

@ -229,6 +229,11 @@ export default {
text: this.$strings.LabelRSSFeedOpen,
value: 'feed-open',
sublist: false
},
{
text: this.$strings.LabelShareOpen,
value: 'share-open',
sublist: false
}
]
},

View File

@ -1,7 +1,7 @@
<template>
<div class="w-full h-screen max-h-screen overflow-hidden" :style="{ backgroundColor: coverRgb }">
<div class="w-full h-dvh max-h-dvh overflow-hidden" :style="{ backgroundColor: coverRgb }">
<div class="w-screen h-screen absolute inset-0 pointer-events-none" style="background: linear-gradient(180deg, rgba(0, 0, 0, 0) 0%, rgba(38, 38, 38, 1) 80%)"></div>
<div class="absolute inset-0 w-screen h-screen flex items-center justify-center z-10">
<div class="absolute inset-0 w-screen h-dvh flex items-center justify-center z-10">
<div class="w-full p-2 sm:p-4 md:p-8">
<div v-if="!isMobileLandscape" :style="{ width: coverWidth + 'px', height: coverHeight + 'px' }" class="mx-auto overflow-hidden rounded-xl my-2">
<img ref="coverImg" :src="coverUrl" class="object-contain w-full h-full" @load="coverImageLoaded" />
@ -113,6 +113,8 @@ export default {
.then((color) => {
this.coverRgb = color.rgba
this.coverBgIsLight = color.isLight
document.body.style.backgroundColor = color.hex
})
.catch((e) => {
console.log(e)

View File

@ -512,6 +512,7 @@
"LabelSettingsStoreMetadataWithItemHelp": "By default metadata files are stored in /metadata/items, enabling this setting will store metadata files in your library item folders",
"LabelSettingsTimeFormat": "Time Format",
"LabelShare": "Share",
"LabelShareOpen": "Share Open",
"LabelShareURL": "Share URL",
"LabelShowAll": "Show All",
"LabelShowSeconds": "Show seconds",

View File

@ -74,7 +74,7 @@ class Server {
this.podcastManager = new PodcastManager(this.watcher, this.notificationManager)
this.audioMetadataManager = new AudioMetadataMangaer()
this.rssFeedManager = new RssFeedManager()
this.cronManager = new CronManager(this.podcastManager)
this.cronManager = new CronManager(this.podcastManager, this.playbackSessionManager)
this.apiCacheManager = new ApiCacheManager()
this.binaryManager = new BinaryManager()

View File

@ -42,19 +42,14 @@ class ShareController {
const playbackSession = ShareManager.findPlaybackSessionBySessionId(req.cookies.share_session_id)
if (playbackSession) {
const playbackSessionMediaItemShare = ShareManager.findByMediaItemId(playbackSession.mediaItemId)
if (!playbackSessionMediaItemShare) {
Logger.error(`[ShareController] Share playback session ${req.cookies.share_session_id} media item share not found with id ${playbackSession.mediaItemId}`)
return res.sendStatus(500)
}
if (playbackSessionMediaItemShare.slug === slug) {
if (mediaItemShare.id === playbackSession.mediaItemShareId) {
Logger.debug(`[ShareController] Found share playback session ${req.cookies.share_session_id}`)
mediaItemShare.playbackSession = playbackSession.toJSONForClient()
return res.json(mediaItemShare)
} else {
// TODO: Close old session and use same session id
Logger.info(`[ShareController] Share playback session found with id ${req.cookies.share_session_id} but media item share slug ${playbackSessionMediaItemShare.slug} does not match requested slug ${slug}`)
res.clearCookie('share_session_id')
// Changed media item share - close other session
Logger.debug(`[ShareController] Other playback session is already open for share session. Closing session "${playbackSession.displayTitle}"`)
ShareManager.closeSharePlaybackSession(playbackSession)
}
} else {
Logger.info(`[ShareController] Share playback session not found with id ${req.cookies.share_session_id}`)
@ -238,6 +233,7 @@ class ShareController {
}
playbackSession.currentTime = Math.min(currentTime, playbackSession.duration)
playbackSession.updatedAt = Date.now()
Logger.debug(`[ShareController] Update share playback session ${req.cookies.share_session_id} currentTime: ${playbackSession.currentTime}`)
res.sendStatus(204)
}

View File

@ -4,9 +4,14 @@ const Logger = require('../Logger')
const Database = require('../Database')
const LibraryScanner = require('../scanner/LibraryScanner')
const ShareManager = require('./ShareManager')
class CronManager {
constructor(podcastManager) {
constructor(podcastManager, playbackSessionManager) {
/** @type {import('./PodcastManager')} */
this.podcastManager = podcastManager
/** @type {import('./PlaybackSessionManager')} */
this.playbackSessionManager = playbackSessionManager
this.libraryScanCrons = []
this.podcastCrons = []
@ -19,10 +24,26 @@ class CronManager {
* @param {import('../objects/Library')[]} libraries
*/
async init(libraries) {
this.initOpenSessionCleanupCron()
this.initLibraryScanCrons(libraries)
await this.initPodcastCrons()
}
/**
* Initialize open session cleanup cron
* Runs every day at 00:30
* Closes open share sessions that have not been updated in 24 hours
* Closes open playback sessions that have not been updated in 36 hours
* TODO: Clients should re-open the session if it is closed so that stale sessions can be closed sooner
*/
initOpenSessionCleanupCron() {
cron.schedule('30 0 * * *', async () => {
Logger.debug('[CronManager] Open session cleanup cron executing')
ShareManager.closeStaleOpenShareSessions()
await this.playbackSessionManager.closeStaleOpenSessions()
})
}
/**
* Initialize library scan crons
* @param {import('../objects/Library')[]} libraries

View File

@ -21,6 +21,8 @@ class PlaybackSessionManager {
this.StreamsPath = Path.join(global.MetadataPath, 'streams')
this.oldPlaybackSessionMap = {} // TODO: Remove after updated mobile versions
/** @type {PlaybackSession[]} */
this.sessions = []
}
@ -346,6 +348,10 @@ class PlaybackSessionManager {
}
}
/**
*
* @param {string} sessionId
*/
async removeSession(sessionId) {
const session = this.sessions.find((s) => s.id === sessionId)
if (!session) return
@ -378,5 +384,18 @@ class PlaybackSessionManager {
Logger.error(`[PlaybackSessionManager] cleanOrphanStreams failed`, error)
}
}
/**
* Close all open sessions that have not been updated in the last 36 hours
*/
async closeStaleOpenSessions() {
const updatedAtTimeCutoff = Date.now() - 1000 * 60 * 60 * 36
const staleSessions = this.sessions.filter((session) => session.updatedAt < updatedAtTimeCutoff)
for (const session of staleSessions) {
const sessionLastUpdate = new Date(session.updatedAt)
Logger.info(`[PlaybackSessionManager] Closing stale session "${session.displayTitle}" (${session.id}) last updated at ${sessionLastUpdate}`)
await this.removeSession(session.id)
}
}
}
module.exports = PlaybackSessionManager

View File

@ -25,10 +25,19 @@ class ShareManager {
* @param {import('../objects/PlaybackSession')} playbackSession
*/
addOpenSharePlaybackSession(playbackSession) {
Logger.info(`[ShareManager] Adding new open share playback session ${playbackSession.shareSessionId}`)
Logger.info(`[ShareManager] Adding new open share playback session "${playbackSession.displayTitle}"`)
this.openSharePlaybackSessions.push(playbackSession)
}
/**
*
* @param {import('../objects/PlaybackSession')} playbackSession
*/
closeSharePlaybackSession(playbackSession) {
Logger.info(`[ShareManager] Closing share playback session "${playbackSession.displayTitle}"`)
this.openSharePlaybackSessions = this.openSharePlaybackSessions.filter((s) => s.id !== playbackSession.id)
}
/**
* Find an open media item share by media item ID
* @param {string} mediaItemId
@ -153,5 +162,18 @@ class ShareManager {
destroyMediaItemShare(mediaItemShareId) {
return Database.models.mediaItemShare.destroy({ where: { id: mediaItemShareId } })
}
/**
* Close open share sessions that have not been updated in the last 24 hours
*/
closeStaleOpenShareSessions() {
const updatedAtTimeCutoff = Date.now() - 1000 * 60 * 60 * 24
const staleSessions = this.openSharePlaybackSessions.filter((session) => session.updatedAt < updatedAtTimeCutoff)
for (const session of staleSessions) {
const sessionLastUpdate = new Date(session.updatedAt)
Logger.info(`[PlaybackSessionManager] Closing stale session "${session.displayTitle}" (${session.id}) last updated at ${sessionLastUpdate}`)
this.closeSharePlaybackSession(session)
}
}
}
module.exports = new ShareManager()

View File

@ -568,7 +568,6 @@ class LibraryItem extends Model {
oldLibraryItem.numEpisodesIncomplete = li.numEpisodesIncomplete
}
if (li.mediaType === 'book' && options.include?.includes?.('share')) {
console.log('Lookup share for media item id', li.mediaId)
oldLibraryItem.mediaItemShare = ShareManager.findByMediaItemId(li.mediaId)
}

View File

@ -412,6 +412,11 @@ module.exports = {
model: Database.feedModel,
required: true
})
} else if (filterGroup === 'share-open') {
bookIncludes.push({
model: Database.mediaItemShareModel,
required: true
})
} else if (filterGroup === 'ebooks' && filterValue === 'supplementary') {
// TODO: Temp workaround for filtering supplementary ebook
libraryItemWhere['libraryFiles'] = {