mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2026-06-07 19:22:43 +02:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a992400d6a | |||
| 108b2a60f5 | |||
| af684e6a69 | |||
| 5336d0525e | |||
| bb4eec9355 | |||
| 28404f37b8 | |||
| 7b92c15a46 | |||
| c150ed4e98 | |||
| cb7632b216 |
@@ -94,6 +94,9 @@ export default {
|
|||||||
userIsAdminOrUp() {
|
userIsAdminOrUp() {
|
||||||
return this.$store.getters['user/getIsAdminOrUp']
|
return this.$store.getters['user/getIsAdminOrUp']
|
||||||
},
|
},
|
||||||
|
userCanAccessExplicitContent() {
|
||||||
|
return this.$store.getters['user/getUserCanAccessExplicitContent']
|
||||||
|
},
|
||||||
libraryMediaType() {
|
libraryMediaType() {
|
||||||
return this.$store.getters['libraries/getCurrentLibraryMediaType']
|
return this.$store.getters['libraries/getCurrentLibraryMediaType']
|
||||||
},
|
},
|
||||||
@@ -239,6 +242,15 @@ export default {
|
|||||||
sublist: false
|
sublist: false
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
if (this.userCanAccessExplicitContent) {
|
||||||
|
items.push({
|
||||||
|
text: this.$strings.LabelExplicit,
|
||||||
|
value: 'explicit',
|
||||||
|
sublist: false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
if (this.userIsAdminOrUp) {
|
if (this.userIsAdminOrUp) {
|
||||||
items.push({
|
items.push({
|
||||||
text: this.$strings.LabelShareOpen,
|
text: this.$strings.LabelShareOpen,
|
||||||
@@ -249,7 +261,7 @@ export default {
|
|||||||
return items
|
return items
|
||||||
},
|
},
|
||||||
podcastItems() {
|
podcastItems() {
|
||||||
return [
|
const items = [
|
||||||
{
|
{
|
||||||
text: this.$strings.LabelAll,
|
text: this.$strings.LabelAll,
|
||||||
value: 'all'
|
value: 'all'
|
||||||
@@ -283,6 +295,16 @@ export default {
|
|||||||
sublist: false
|
sublist: false
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
if (this.userCanAccessExplicitContent) {
|
||||||
|
items.push({
|
||||||
|
text: this.$strings.LabelExplicit,
|
||||||
|
value: 'explicit',
|
||||||
|
sublist: false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return items
|
||||||
},
|
},
|
||||||
selectItems() {
|
selectItems() {
|
||||||
if (this.isSeries) return this.seriesItems
|
if (this.isSeries) return this.seriesItems
|
||||||
|
|||||||
@@ -35,7 +35,14 @@
|
|||||||
<widgets-podcast-type-indicator :type="episode.episodeType" />
|
<widgets-podcast-type-indicator :type="episode.episodeType" />
|
||||||
</div>
|
</div>
|
||||||
<p v-if="episode.subtitle" class="mb-1 text-sm text-gray-300 line-clamp-2">{{ episode.subtitle }}</p>
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -34,6 +34,12 @@
|
|||||||
{{ audioFileSize }}
|
{{ audioFileSize }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
</modals-modal>
|
</modals-modal>
|
||||||
@@ -90,6 +96,10 @@ export default {
|
|||||||
|
|
||||||
return this.$bytesPretty(size)
|
return this.$bytesPretty(size)
|
||||||
},
|
},
|
||||||
|
audioFileDuration() {
|
||||||
|
const duration = this.episode.duration || 0
|
||||||
|
return this.$elapsedPretty(duration)
|
||||||
|
},
|
||||||
bookCoverAspectRatio() {
|
bookCoverAspectRatio() {
|
||||||
return this.$store.getters['libraries/getBookCoverAspectRatio']
|
return this.$store.getters['libraries/getBookCoverAspectRatio']
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,6 +58,9 @@ export const getters = {
|
|||||||
getUserCanAccessAllLibraries: (state) => {
|
getUserCanAccessAllLibraries: (state) => {
|
||||||
return !!state.user?.permissions?.accessAllLibraries
|
return !!state.user?.permissions?.accessAllLibraries
|
||||||
},
|
},
|
||||||
|
getUserCanAccessExplicitContent: (state) => {
|
||||||
|
return !!state.user?.permissions?.accessExplicitContent
|
||||||
|
},
|
||||||
getLibrariesAccessible: (state, getters) => {
|
getLibrariesAccessible: (state, getters) => {
|
||||||
if (!state.user) return []
|
if (!state.user) return []
|
||||||
if (getters.getUserCanAccessAllLibraries) return []
|
if (getters.getUserCanAccessAllLibraries) return []
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ if (isDev) {
|
|||||||
if (devEnv.SkipBinariesCheck) process.env.SKIP_BINARIES_CHECK = '1'
|
if (devEnv.SkipBinariesCheck) process.env.SKIP_BINARIES_CHECK = '1'
|
||||||
if (devEnv.AllowIframe) process.env.ALLOW_IFRAME = '1'
|
if (devEnv.AllowIframe) process.env.ALLOW_IFRAME = '1'
|
||||||
if (devEnv.BackupPath) process.env.BACKUP_PATH = devEnv.BackupPath
|
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.SOURCE = 'local'
|
||||||
process.env.ROUTER_BASE_PATH = devEnv.RouterBasePath ?? '/audiobookshelf'
|
process.env.ROUTER_BASE_PATH = devEnv.RouterBasePath ?? '/audiobookshelf'
|
||||||
}
|
}
|
||||||
|
|||||||
+12
-1
@@ -442,7 +442,17 @@ class Auth {
|
|||||||
// Local strategy login route (takes username and password)
|
// Local strategy login route (takes username and password)
|
||||||
router.post('/login', passport.authenticate('local'), async (req, res) => {
|
router.post('/login', passport.authenticate('local'), async (req, res) => {
|
||||||
// return the user login response json if the login was successfull
|
// 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)
|
// openid strategy login route (this redirects to the configured openid login provider)
|
||||||
@@ -718,6 +728,7 @@ class Auth {
|
|||||||
const authMethod = req.cookies.auth_method
|
const authMethod = req.cookies.auth_method
|
||||||
|
|
||||||
res.clearCookie('auth_method')
|
res.clearCookie('auth_method')
|
||||||
|
res.clearCookie('auth_token')
|
||||||
|
|
||||||
let logoutUrl = null
|
let logoutUrl = null
|
||||||
|
|
||||||
|
|||||||
+44
-30
@@ -220,6 +220,7 @@ class Server {
|
|||||||
|
|
||||||
async start() {
|
async start() {
|
||||||
Logger.info('=== Starting Server ===')
|
Logger.info('=== Starting Server ===')
|
||||||
|
|
||||||
this.initProcessEventListeners()
|
this.initProcessEventListeners()
|
||||||
await this.init()
|
await this.init()
|
||||||
|
|
||||||
@@ -281,6 +282,7 @@ class Server {
|
|||||||
await this.auth.initPassportJs()
|
await this.auth.initPassportJs()
|
||||||
|
|
||||||
const router = express.Router()
|
const router = express.Router()
|
||||||
|
|
||||||
// if RouterBasePath is set, modify all requests to include the base path
|
// if RouterBasePath is set, modify all requests to include the base path
|
||||||
app.use((req, res, next) => {
|
app.use((req, res, next) => {
|
||||||
const urlStartsWithRouterBasePath = req.url.startsWith(global.RouterBasePath)
|
const urlStartsWithRouterBasePath = req.url.startsWith(global.RouterBasePath)
|
||||||
@@ -313,10 +315,6 @@ class Server {
|
|||||||
router.use('/hls', this.hlsRouter.router)
|
router.use('/hls', this.hlsRouter.router)
|
||||||
router.use('/public', this.publicRouter.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
|
// Static folder
|
||||||
router.use(express.static(Path.join(global.appRoot, 'static')))
|
router.use(express.static(Path.join(global.appRoot, 'static')))
|
||||||
|
|
||||||
@@ -336,32 +334,6 @@ class Server {
|
|||||||
// Auth routes
|
// Auth routes
|
||||||
await this.auth.initAuthRoutes(router)
|
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) => {
|
router.post('/init', (req, res) => {
|
||||||
if (Database.hasRootUser) {
|
if (Database.hasRootUser) {
|
||||||
Logger.error(`[Server] attempt to init server when server already has a root user`)
|
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))
|
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/'
|
const unixSocketPrefix = 'unix/'
|
||||||
if (this.Host?.startsWith(unixSocketPrefix)) {
|
if (this.Host?.startsWith(unixSocketPrefix)) {
|
||||||
const sockPath = this.Host.slice(unixSocketPrefix.length)
|
const sockPath = this.Host.slice(unixSocketPrefix.length)
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ const Fuse = require('../libs/fusejs')
|
|||||||
* @property {string} episode
|
* @property {string} episode
|
||||||
* @property {string} author
|
* @property {string} author
|
||||||
* @property {string} duration
|
* @property {string} duration
|
||||||
|
* @property {number|null} durationSeconds - Parsed from duration string if duration is valid
|
||||||
* @property {string} explicit
|
* @property {string} explicit
|
||||||
* @property {number} publishedAt - Unix timestamp
|
* @property {number} publishedAt - Unix timestamp
|
||||||
* @property {{ url: string, type?: string, length?: string }} enclosure
|
* @property {{ url: string, type?: string, length?: string }} enclosure
|
||||||
@@ -217,8 +218,9 @@ function extractEpisodeData(item) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Extract psc:chapters if duration is set
|
// Extract psc:chapters if duration is set
|
||||||
let episodeDuration = !isNaN(episode.duration) ? timestampToSeconds(episode.duration) : null
|
episode.durationSeconds = episode.duration ? timestampToSeconds(episode.duration) : null
|
||||||
if (item['psc:chapters']?.[0]?.['psc:chapter']?.length && episodeDuration) {
|
|
||||||
|
if (item['psc:chapters']?.[0]?.['psc:chapter']?.length && episode.durationSeconds) {
|
||||||
// Example chapter:
|
// Example chapter:
|
||||||
// {"id":0,"start":0,"end":43.004286,"title":"chapter 1"}
|
// {"id":0,"start":0,"end":43.004286,"title":"chapter 1"}
|
||||||
|
|
||||||
@@ -244,7 +246,7 @@ function extractEpisodeData(item) {
|
|||||||
} else {
|
} else {
|
||||||
episode.chapters = cleanedChapters.map((chapter, index) => {
|
episode.chapters = cleanedChapters.map((chapter, index) => {
|
||||||
const nextChapter = cleanedChapters[index + 1]
|
const nextChapter = cleanedChapters[index + 1]
|
||||||
const end = nextChapter ? nextChapter.start : episodeDuration
|
const end = nextChapter ? nextChapter.start : episode.durationSeconds
|
||||||
return {
|
return {
|
||||||
id: chapter.id,
|
id: chapter.id,
|
||||||
title: chapter.title,
|
title: chapter.title,
|
||||||
@@ -273,6 +275,7 @@ function cleanEpisodeData(data) {
|
|||||||
episode: data.episode || '',
|
episode: data.episode || '',
|
||||||
author: data.author || '',
|
author: data.author || '',
|
||||||
duration: data.duration || '',
|
duration: data.duration || '',
|
||||||
|
durationSeconds: data.durationSeconds || null,
|
||||||
explicit: data.explicit || '',
|
explicit: data.explicit || '',
|
||||||
publishedAt,
|
publishedAt,
|
||||||
enclosure: data.enclosure,
|
enclosure: data.enclosure,
|
||||||
|
|||||||
@@ -186,6 +186,8 @@ module.exports = {
|
|||||||
mediaWhere['$series.id$'] = null
|
mediaWhere['$series.id$'] = null
|
||||||
} else if (group === 'abridged') {
|
} else if (group === 'abridged') {
|
||||||
mediaWhere['abridged'] = true
|
mediaWhere['abridged'] = true
|
||||||
|
} else if (group === 'explicit') {
|
||||||
|
mediaWhere['explicit'] = true
|
||||||
} else if (['genres', 'tags', 'narrators'].includes(group)) {
|
} 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)`), {
|
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
|
[Sequelize.Op.gte]: 1
|
||||||
|
|||||||
@@ -59,6 +59,8 @@ module.exports = {
|
|||||||
replacements.filterValue = value
|
replacements.filterValue = value
|
||||||
} else if (group === 'languages') {
|
} else if (group === 'languages') {
|
||||||
mediaWhere['language'] = value
|
mediaWhere['language'] = value
|
||||||
|
} else if (group === 'explicit') {
|
||||||
|
mediaWhere['explicit'] = true
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
Reference in New Issue
Block a user