mirror of
				https://github.com/advplyr/audiobookshelf.git
				synced 2025-10-27 11:18:14 +01:00 
			
		
		
		
	Merge branch 'advplyr:master' into audible-confidence-score
This commit is contained in:
		
						commit
						9c44fc0d01
					
				| @ -57,7 +57,7 @@ WORKDIR /app | ||||
| # Copy compiled frontend and server from build stages | ||||
| COPY --from=build-client /client/dist /app/client/dist | ||||
| COPY --from=build-server /server /app | ||||
| COPY --from=build-server /usr/local/lib/nusqlite3 /usr/local/lib/nusqlite3 | ||||
| COPY --from=build-server ${NUSQLITE3_PATH} ${NUSQLITE3_PATH} | ||||
| 
 | ||||
| EXPOSE 80 | ||||
| 
 | ||||
|  | ||||
| @ -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 [] | ||||
|  | ||||
							
								
								
									
										1
									
								
								index.js
									
									
									
									
									
								
							
							
						
						
									
										1
									
								
								index.js
									
									
									
									
									
								
							| @ -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' | ||||
| } | ||||
|  | ||||
| @ -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 | ||||
| 
 | ||||
|  | ||||
| @ -766,14 +766,25 @@ class Database { | ||||
|       Logger.warn(`Removed ${badSessionsRemoved} sessions that were 3 seconds or less`) | ||||
|     } | ||||
| 
 | ||||
|     // Remove mediaProgresses with duplicate mediaItemId (remove the oldest updatedAt)
 | ||||
|     // const [duplicateMediaProgresses] = await this.sequelize.query(`SELECT id, mediaItemId FROM mediaProgresses WHERE (mediaItemId, userId, updatedAt) IN (SELECT mediaItemId, userId, MIN(updatedAt) FROM mediaProgresses GROUP BY mediaItemId, userId HAVING COUNT(*) > 1)`)
 | ||||
|     // for (const duplicateMediaProgress of duplicateMediaProgresses) {
 | ||||
|     //   Logger.warn(`Found duplicate mediaProgress for mediaItem "${duplicateMediaProgress.mediaItemId}" - removing it`)
 | ||||
|     //   await this.mediaProgressModel.destroy({
 | ||||
|     //     where: { id: duplicateMediaProgress.id }
 | ||||
|     //   })
 | ||||
|     // }
 | ||||
|     // Remove mediaProgresses with duplicate mediaItemId (remove the oldest updatedAt or if updatedAt is the same, remove arbitrary one)
 | ||||
|     const [duplicateMediaProgresses] = await this.sequelize.query(`SELECT mp1.id, mp1.mediaItemId
 | ||||
| FROM mediaProgresses mp1 | ||||
| WHERE EXISTS ( | ||||
|     SELECT 1 | ||||
|     FROM mediaProgresses mp2 | ||||
|     WHERE mp2.mediaItemId = mp1.mediaItemId | ||||
|     AND mp2.userId = mp1.userId | ||||
|     AND ( | ||||
|         mp2.updatedAt > mp1.updatedAt | ||||
|         OR (mp2.updatedAt = mp1.updatedAt AND mp2.id < mp1.id) | ||||
|     ) | ||||
| )`)
 | ||||
|     for (const duplicateMediaProgress of duplicateMediaProgresses) { | ||||
|       Logger.warn(`Found duplicate mediaProgress for mediaItem "${duplicateMediaProgress.mediaItemId}" - removing it`) | ||||
|       await this.mediaProgressModel.destroy({ | ||||
|         where: { id: duplicateMediaProgress.id } | ||||
|       }) | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   async createTextSearchQuery(query) { | ||||
|  | ||||
| @ -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 | ||||
| @ -251,6 +253,15 @@ module.exports = { | ||||
|    */ | ||||
|   getOrder(sortBy, sortDesc, collapseseries) { | ||||
|     const dir = sortDesc ? 'DESC' : 'ASC' | ||||
| 
 | ||||
|     const getTitleOrder = () => { | ||||
|       if (global.ServerSettings.sortingIgnorePrefix) { | ||||
|         return [Sequelize.literal('`libraryItem`.`titleIgnorePrefix` COLLATE NOCASE'), dir] | ||||
|       } else { | ||||
|         return [Sequelize.literal('`libraryItem`.`title` COLLATE NOCASE'), dir] | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     if (sortBy === 'addedAt') { | ||||
|       return [[Sequelize.literal('libraryItem.createdAt'), dir]] | ||||
|     } else if (sortBy === 'size') { | ||||
| @ -264,25 +275,16 @@ module.exports = { | ||||
|     } else if (sortBy === 'media.metadata.publishedYear') { | ||||
|       return [[Sequelize.literal(`CAST(\`book\`.\`publishedYear\` AS INTEGER)`), dir]] | ||||
|     } else if (sortBy === 'media.metadata.authorNameLF') { | ||||
|       return [ | ||||
|         [Sequelize.literal('`libraryItem`.`authorNamesLastFirst` COLLATE NOCASE'), dir], | ||||
|         [Sequelize.literal('`libraryItem`.`title` COLLATE NOCASE'), dir] | ||||
|       ] | ||||
|       // Sort by author name last first, secondary sort by title
 | ||||
|       return [[Sequelize.literal('`libraryItem`.`authorNamesLastFirst` COLLATE NOCASE'), dir], getTitleOrder()] | ||||
|     } else if (sortBy === 'media.metadata.authorName') { | ||||
|       return [ | ||||
|         [Sequelize.literal('`libraryItem`.`authorNamesFirstLast` COLLATE NOCASE'), dir], | ||||
|         [Sequelize.literal('`libraryItem`.`title` COLLATE NOCASE'), dir] | ||||
|       ] | ||||
|       // Sort by author name first last, secondary sort by title
 | ||||
|       return [[Sequelize.literal('`libraryItem`.`authorNamesFirstLast` COLLATE NOCASE'), dir], getTitleOrder()] | ||||
|     } else if (sortBy === 'media.metadata.title') { | ||||
|       if (collapseseries) { | ||||
|         return [[Sequelize.literal('display_title COLLATE NOCASE'), dir]] | ||||
|       } | ||||
| 
 | ||||
|       if (global.ServerSettings.sortingIgnorePrefix) { | ||||
|         return [[Sequelize.literal('`libraryItem`.`titleIgnorePrefix` COLLATE NOCASE'), dir]] | ||||
|       } else { | ||||
|         return [[Sequelize.literal('`libraryItem`.`title` COLLATE NOCASE'), dir]] | ||||
|       } | ||||
|       return [getTitleOrder()] | ||||
|     } else if (sortBy === 'sequence') { | ||||
|       const nullDir = sortDesc ? 'DESC NULLS FIRST' : 'ASC NULLS LAST' | ||||
|       return [[Sequelize.literal(`CAST(\`series.bookSeries.sequence\` AS FLOAT) ${nullDir}`)]] | ||||
|  | ||||
| @ -59,6 +59,8 @@ module.exports = { | ||||
|       replacements.filterValue = value | ||||
|     } else if (group === 'languages') { | ||||
|       mediaWhere['language'] = value | ||||
|     } else if (group === 'explicit') { | ||||
|       mediaWhere['explicit'] = true | ||||
|     } | ||||
| 
 | ||||
|     return { | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user