mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2026-06-15 23:14:25 +02:00
Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a992400d6a | |||
| 108b2a60f5 | |||
| af684e6a69 | |||
| 5336d0525e | |||
| bb4eec9355 | |||
| 28404f37b8 | |||
| 7b92c15a46 | |||
| c150ed4e98 | |||
| cb7632b216 | |||
| b8849677de | |||
| 9bf8d7de11 | |||
| 6634ce8fd4 |
@@ -94,6 +94,9 @@ export default {
|
||||
userIsAdminOrUp() {
|
||||
return this.$store.getters['user/getIsAdminOrUp']
|
||||
},
|
||||
userCanAccessExplicitContent() {
|
||||
return this.$store.getters['user/getUserCanAccessExplicitContent']
|
||||
},
|
||||
libraryMediaType() {
|
||||
return this.$store.getters['libraries/getCurrentLibraryMediaType']
|
||||
},
|
||||
@@ -239,6 +242,15 @@ export default {
|
||||
sublist: false
|
||||
}
|
||||
]
|
||||
|
||||
if (this.userCanAccessExplicitContent) {
|
||||
items.push({
|
||||
text: this.$strings.LabelExplicit,
|
||||
value: 'explicit',
|
||||
sublist: false
|
||||
})
|
||||
}
|
||||
|
||||
if (this.userIsAdminOrUp) {
|
||||
items.push({
|
||||
text: this.$strings.LabelShareOpen,
|
||||
@@ -249,7 +261,7 @@ export default {
|
||||
return items
|
||||
},
|
||||
podcastItems() {
|
||||
return [
|
||||
const items = [
|
||||
{
|
||||
text: this.$strings.LabelAll,
|
||||
value: 'all'
|
||||
@@ -283,6 +295,16 @@ export default {
|
||||
sublist: false
|
||||
}
|
||||
]
|
||||
|
||||
if (this.userCanAccessExplicitContent) {
|
||||
items.push({
|
||||
text: this.$strings.LabelExplicit,
|
||||
value: 'explicit',
|
||||
sublist: false
|
||||
})
|
||||
}
|
||||
|
||||
return items
|
||||
},
|
||||
selectItems() {
|
||||
if (this.isSeries) return this.seriesItems
|
||||
|
||||
@@ -35,7 +35,14 @@
|
||||
<widgets-podcast-type-indicator :type="episode.episodeType" />
|
||||
</div>
|
||||
<p v-if="episode.subtitle" class="mb-1 text-sm text-gray-300 line-clamp-2">{{ episode.subtitle }}</p>
|
||||
<p class="text-xs text-gray-300">Published {{ episode.publishedAt ? $dateDistanceFromNow(episode.publishedAt) : 'Unknown' }}</p>
|
||||
<div class="flex items-center space-x-2">
|
||||
<!-- published -->
|
||||
<p class="text-xs text-gray-300 w-40">Published {{ episode.publishedAt ? $dateDistanceFromNow(episode.publishedAt) : 'Unknown' }}</p>
|
||||
<!-- duration -->
|
||||
<p v-if="episode.durationSeconds && !isNaN(episode.durationSeconds)" class="text-xs text-gray-300 min-w-28">{{ $strings.LabelDuration }}: {{ $elapsedPretty(episode.durationSeconds) }}</p>
|
||||
<!-- size -->
|
||||
<p v-if="episode.enclosure?.length && !isNaN(episode.enclosure.length) && Number(episode.enclosure.length) > 0" class="text-xs text-gray-300">{{ $strings.LabelSize }}: {{ $bytesPretty(Number(episode.enclosure.length)) }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<p dir="auto" class="text-lg font-semibold mb-6">{{ title }}</p>
|
||||
<div v-if="description" dir="auto" class="default-style less-spacing" v-html="description" />
|
||||
<div v-if="description" dir="auto" class="default-style less-spacing" @click="handleDescriptionClick" v-html="description" />
|
||||
<p v-else class="mb-2">{{ $strings.MessageNoDescription }}</p>
|
||||
|
||||
<div class="w-full h-px bg-white/5 my-4" />
|
||||
@@ -34,6 +34,12 @@
|
||||
{{ audioFileSize }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="grow">
|
||||
<p class="font-semibold text-xs mb-1">{{ $strings.LabelDuration }}</p>
|
||||
<p class="mb-2 text-xs">
|
||||
{{ audioFileDuration }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</modals-modal>
|
||||
@@ -68,7 +74,7 @@ export default {
|
||||
return this.episode.title || 'No Episode Title'
|
||||
},
|
||||
description() {
|
||||
return this.episode.description || ''
|
||||
return this.parseDescription(this.episode.description || '')
|
||||
},
|
||||
media() {
|
||||
return this.libraryItem?.media || {}
|
||||
@@ -90,11 +96,49 @@ export default {
|
||||
|
||||
return this.$bytesPretty(size)
|
||||
},
|
||||
audioFileDuration() {
|
||||
const duration = this.episode.duration || 0
|
||||
return this.$elapsedPretty(duration)
|
||||
},
|
||||
bookCoverAspectRatio() {
|
||||
return this.$store.getters['libraries/getBookCoverAspectRatio']
|
||||
}
|
||||
},
|
||||
methods: {},
|
||||
methods: {
|
||||
handleDescriptionClick(e) {
|
||||
if (e.target.matches('span.time-marker')) {
|
||||
const time = parseInt(e.target.dataset.time)
|
||||
if (!isNaN(time)) {
|
||||
this.$eventBus.$emit('play-item', {
|
||||
episodeId: this.episodeId,
|
||||
libraryItemId: this.libraryItem.id,
|
||||
startTime: time
|
||||
})
|
||||
}
|
||||
e.preventDefault()
|
||||
}
|
||||
},
|
||||
parseDescription(description) {
|
||||
const timeMarkerLinkRegex = /<a href="#([^"]*?\b\d{1,2}:\d{1,2}(?::\d{1,2})?)">(.*?)<\/a>/g
|
||||
const timeMarkerRegex = /\b\d{1,2}:\d{1,2}(?::\d{1,2})?\b/g
|
||||
|
||||
function convertToSeconds(time) {
|
||||
const timeParts = time.split(':').map(Number)
|
||||
return timeParts.reduce((acc, part, index) => acc * 60 + part, 0)
|
||||
}
|
||||
|
||||
return description
|
||||
.replace(timeMarkerLinkRegex, (match, href, displayTime) => {
|
||||
const time = displayTime.match(timeMarkerRegex)[0]
|
||||
const seekTimeInSeconds = convertToSeconds(time)
|
||||
return `<span class="time-marker cursor-pointer text-blue-400 hover:text-blue-300" data-time="${seekTimeInSeconds}">${displayTime}</span>`
|
||||
})
|
||||
.replace(timeMarkerRegex, (match) => {
|
||||
const seekTimeInSeconds = convertToSeconds(match)
|
||||
return `<span class="time-marker cursor-pointer text-blue-400 hover:text-blue-300" data-time="${seekTimeInSeconds}">${match}</span>`
|
||||
})
|
||||
}
|
||||
},
|
||||
mounted() {}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -58,6 +58,9 @@ export const getters = {
|
||||
getUserCanAccessAllLibraries: (state) => {
|
||||
return !!state.user?.permissions?.accessAllLibraries
|
||||
},
|
||||
getUserCanAccessExplicitContent: (state) => {
|
||||
return !!state.user?.permissions?.accessExplicitContent
|
||||
},
|
||||
getLibrariesAccessible: (state, getters) => {
|
||||
if (!state.user) return []
|
||||
if (getters.getUserCanAccessAllLibraries) return []
|
||||
|
||||
@@ -28,6 +28,7 @@ if (isDev) {
|
||||
if (devEnv.SkipBinariesCheck) process.env.SKIP_BINARIES_CHECK = '1'
|
||||
if (devEnv.AllowIframe) process.env.ALLOW_IFRAME = '1'
|
||||
if (devEnv.BackupPath) process.env.BACKUP_PATH = devEnv.BackupPath
|
||||
if (devEnv.ReactClientPath) process.env.REACT_CLIENT_PATH = devEnv.ReactClientPath
|
||||
process.env.SOURCE = 'local'
|
||||
process.env.ROUTER_BASE_PATH = devEnv.RouterBasePath ?? '/audiobookshelf'
|
||||
}
|
||||
|
||||
+12
-1
@@ -442,7 +442,17 @@ class Auth {
|
||||
// Local strategy login route (takes username and password)
|
||||
router.post('/login', passport.authenticate('local'), async (req, res) => {
|
||||
// return the user login response json if the login was successfull
|
||||
res.json(await this.getUserLoginResponsePayload(req.user))
|
||||
const userResponse = await this.getUserLoginResponsePayload(req.user)
|
||||
|
||||
// Experimental Next.js client uses bearer token in cookies
|
||||
res.cookie('auth_token', userResponse.user.token, {
|
||||
httpOnly: true,
|
||||
secure: req.secure || req.get('x-forwarded-proto') === 'https',
|
||||
sameSite: 'strict',
|
||||
maxAge: 1000 * 60 * 60 * 24 * 7 // 7 days
|
||||
})
|
||||
|
||||
res.json(userResponse)
|
||||
})
|
||||
|
||||
// openid strategy login route (this redirects to the configured openid login provider)
|
||||
@@ -718,6 +728,7 @@ class Auth {
|
||||
const authMethod = req.cookies.auth_method
|
||||
|
||||
res.clearCookie('auth_method')
|
||||
res.clearCookie('auth_token')
|
||||
|
||||
let logoutUrl = null
|
||||
|
||||
|
||||
+44
-30
@@ -220,6 +220,7 @@ class Server {
|
||||
|
||||
async start() {
|
||||
Logger.info('=== Starting Server ===')
|
||||
|
||||
this.initProcessEventListeners()
|
||||
await this.init()
|
||||
|
||||
@@ -281,6 +282,7 @@ class Server {
|
||||
await this.auth.initPassportJs()
|
||||
|
||||
const router = express.Router()
|
||||
|
||||
// if RouterBasePath is set, modify all requests to include the base path
|
||||
app.use((req, res, next) => {
|
||||
const urlStartsWithRouterBasePath = req.url.startsWith(global.RouterBasePath)
|
||||
@@ -313,10 +315,6 @@ class Server {
|
||||
router.use('/hls', this.hlsRouter.router)
|
||||
router.use('/public', this.publicRouter.router)
|
||||
|
||||
// Static path to generated nuxt
|
||||
const distPath = Path.join(global.appRoot, '/client/dist')
|
||||
router.use(express.static(distPath))
|
||||
|
||||
// Static folder
|
||||
router.use(express.static(Path.join(global.appRoot, 'static')))
|
||||
|
||||
@@ -336,32 +334,6 @@ class Server {
|
||||
// Auth routes
|
||||
await this.auth.initAuthRoutes(router)
|
||||
|
||||
// Client dynamic routes
|
||||
const dynamicRoutes = [
|
||||
'/item/:id',
|
||||
'/author/:id',
|
||||
'/audiobook/:id/chapters',
|
||||
'/audiobook/:id/edit',
|
||||
'/audiobook/:id/manage',
|
||||
'/library/:library',
|
||||
'/library/:library/search',
|
||||
'/library/:library/bookshelf/:id?',
|
||||
'/library/:library/authors',
|
||||
'/library/:library/narrators',
|
||||
'/library/:library/stats',
|
||||
'/library/:library/series/:id?',
|
||||
'/library/:library/podcast/search',
|
||||
'/library/:library/podcast/latest',
|
||||
'/library/:library/podcast/download-queue',
|
||||
'/config/users/:id',
|
||||
'/config/users/:id/sessions',
|
||||
'/config/item-metadata-utils/:id',
|
||||
'/collection/:id',
|
||||
'/playlist/:id',
|
||||
'/share/:slug'
|
||||
]
|
||||
dynamicRoutes.forEach((route) => router.get(route, (req, res) => res.sendFile(Path.join(distPath, 'index.html'))))
|
||||
|
||||
router.post('/init', (req, res) => {
|
||||
if (Database.hasRootUser) {
|
||||
Logger.error(`[Server] attempt to init server when server already has a root user`)
|
||||
@@ -392,6 +364,48 @@ class Server {
|
||||
})
|
||||
router.get('/healthcheck', (req, res) => res.sendStatus(200))
|
||||
|
||||
const ReactClientPath = process.env.REACT_CLIENT_PATH
|
||||
if (!ReactClientPath) {
|
||||
// Static path to generated nuxt
|
||||
const distPath = Path.join(global.appRoot, '/client/dist')
|
||||
router.use(express.static(distPath))
|
||||
|
||||
// Client dynamic routes
|
||||
const dynamicRoutes = [
|
||||
'/item/:id',
|
||||
'/author/:id',
|
||||
'/audiobook/:id/chapters',
|
||||
'/audiobook/:id/edit',
|
||||
'/audiobook/:id/manage',
|
||||
'/library/:library',
|
||||
'/library/:library/search',
|
||||
'/library/:library/bookshelf/:id?',
|
||||
'/library/:library/authors',
|
||||
'/library/:library/narrators',
|
||||
'/library/:library/stats',
|
||||
'/library/:library/series/:id?',
|
||||
'/library/:library/podcast/search',
|
||||
'/library/:library/podcast/latest',
|
||||
'/library/:library/podcast/download-queue',
|
||||
'/config/users/:id',
|
||||
'/config/users/:id/sessions',
|
||||
'/config/item-metadata-utils/:id',
|
||||
'/collection/:id',
|
||||
'/playlist/:id',
|
||||
'/share/:slug'
|
||||
]
|
||||
dynamicRoutes.forEach((route) => router.get(route, (req, res) => res.sendFile(Path.join(distPath, 'index.html'))))
|
||||
} else {
|
||||
// This is for using the experimental Next.js client
|
||||
Logger.info(`Using React client at ${ReactClientPath}`)
|
||||
const nextPath = Path.join(ReactClientPath, 'node_modules/next')
|
||||
const next = require(nextPath)
|
||||
const nextApp = next({ dev: Logger.isDev, dir: ReactClientPath })
|
||||
const handle = nextApp.getRequestHandler()
|
||||
await nextApp.prepare()
|
||||
router.get('*', (req, res) => handle(req, res))
|
||||
}
|
||||
|
||||
const unixSocketPrefix = 'unix/'
|
||||
if (this.Host?.startsWith(unixSocketPrefix)) {
|
||||
const sockPath = this.Host.slice(unixSocketPrefix.length)
|
||||
|
||||
@@ -52,9 +52,7 @@ class FantLab {
|
||||
return []
|
||||
})
|
||||
|
||||
return Promise.all(items.map(async (item) => await this.getWork(item, timeout))).then((resArray) => {
|
||||
return resArray.filter((res) => res)
|
||||
})
|
||||
return Promise.all(items.map(async (item) => await this.getWork(item, timeout))).then((resArray) => resArray.filter(Boolean))
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -83,6 +81,10 @@ class FantLab {
|
||||
return null
|
||||
})
|
||||
|
||||
if (!bookData) {
|
||||
return null
|
||||
}
|
||||
|
||||
return this.cleanBookData(bookData, timeout)
|
||||
}
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ const Fuse = require('../libs/fusejs')
|
||||
* @property {string} episode
|
||||
* @property {string} author
|
||||
* @property {string} duration
|
||||
* @property {number|null} durationSeconds - Parsed from duration string if duration is valid
|
||||
* @property {string} explicit
|
||||
* @property {number} publishedAt - Unix timestamp
|
||||
* @property {{ url: string, type?: string, length?: string }} enclosure
|
||||
@@ -217,8 +218,9 @@ function extractEpisodeData(item) {
|
||||
})
|
||||
|
||||
// Extract psc:chapters if duration is set
|
||||
let episodeDuration = !isNaN(episode.duration) ? timestampToSeconds(episode.duration) : null
|
||||
if (item['psc:chapters']?.[0]?.['psc:chapter']?.length && episodeDuration) {
|
||||
episode.durationSeconds = episode.duration ? timestampToSeconds(episode.duration) : null
|
||||
|
||||
if (item['psc:chapters']?.[0]?.['psc:chapter']?.length && episode.durationSeconds) {
|
||||
// Example chapter:
|
||||
// {"id":0,"start":0,"end":43.004286,"title":"chapter 1"}
|
||||
|
||||
@@ -244,7 +246,7 @@ function extractEpisodeData(item) {
|
||||
} else {
|
||||
episode.chapters = cleanedChapters.map((chapter, index) => {
|
||||
const nextChapter = cleanedChapters[index + 1]
|
||||
const end = nextChapter ? nextChapter.start : episodeDuration
|
||||
const end = nextChapter ? nextChapter.start : episode.durationSeconds
|
||||
return {
|
||||
id: chapter.id,
|
||||
title: chapter.title,
|
||||
@@ -273,6 +275,7 @@ function cleanEpisodeData(data) {
|
||||
episode: data.episode || '',
|
||||
author: data.author || '',
|
||||
duration: data.duration || '',
|
||||
durationSeconds: data.durationSeconds || null,
|
||||
explicit: data.explicit || '',
|
||||
publishedAt,
|
||||
enclosure: data.enclosure,
|
||||
|
||||
@@ -186,6 +186,8 @@ module.exports = {
|
||||
mediaWhere['$series.id$'] = null
|
||||
} else if (group === 'abridged') {
|
||||
mediaWhere['abridged'] = true
|
||||
} else if (group === 'explicit') {
|
||||
mediaWhere['explicit'] = true
|
||||
} else if (['genres', 'tags', 'narrators'].includes(group)) {
|
||||
mediaWhere[group] = Sequelize.where(Sequelize.literal(`(SELECT count(*) FROM json_each(${group}) WHERE json_valid(${group}) AND json_each.value = :filterValue)`), {
|
||||
[Sequelize.Op.gte]: 1
|
||||
|
||||
@@ -59,6 +59,8 @@ module.exports = {
|
||||
replacements.filterValue = value
|
||||
} else if (group === 'languages') {
|
||||
mediaWhere['language'] = value
|
||||
} else if (group === 'explicit') {
|
||||
mediaWhere['explicit'] = true
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user