mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2024-12-28 09:38:56 +01:00
Add: track manager sort by filename #128, Add: Support publish year wrapped in parenthesis, Change: Parse out CD Num from filename, Change: Show First Last author everywhere when Last, First is used in folder name, Fix: Re-Scan updates parsed track nums
This commit is contained in:
parent
b32b99418a
commit
92c2c53c09
@ -7,7 +7,7 @@
|
||||
|
||||
<p v-if="matchKey === 'subtitle'" class="truncate text-xs text-gray-300">{{ matchHtml }}</p>
|
||||
|
||||
<p v-if="matchKey !== 'author'" class="text-xs text-gray-200 truncate">by {{ author }}</p>
|
||||
<p v-if="matchKey !== 'authorFL'" class="text-xs text-gray-200 truncate">by {{ authorFL }}</p>
|
||||
<p v-else class="truncate text-xs text-gray-200" v-html="matchHtml" />
|
||||
|
||||
<div v-if="matchKey === 'series' || matchKey === 'tags'" class="m-0 p-0 truncate" v-html="matchHtml" />
|
||||
@ -39,8 +39,8 @@ export default {
|
||||
subtitle() {
|
||||
return this.book ? this.book.subtitle : ''
|
||||
},
|
||||
author() {
|
||||
return this.book ? this.book.author : 'Unknown'
|
||||
authorFL() {
|
||||
return this.book ? this.book.authorFL : 'Unknown'
|
||||
},
|
||||
matchHtml() {
|
||||
if (!this.matchText || !this.search) return ''
|
||||
@ -62,7 +62,7 @@ export default {
|
||||
html += lastPart
|
||||
|
||||
if (this.matchKey === 'tags') return `<p class="truncate">Tags: ${html}</p>`
|
||||
if (this.matchKey === 'author') return `by ${html}`
|
||||
if (this.matchKey === 'authorFL') return `by ${html}`
|
||||
if (this.matchKey === 'series') return `<p class="truncate">Series: ${html}</p>`
|
||||
return `${html}`
|
||||
}
|
||||
|
@ -188,13 +188,13 @@ export default {
|
||||
},
|
||||
init() {
|
||||
this.showLocalCovers = false
|
||||
if (this.coversFound.length && (this.searchTitle !== this.book.title || this.searchAuthor !== this.book.author)) {
|
||||
if (this.coversFound.length && (this.searchTitle !== this.book.title || this.searchAuthor !== this.book.authorFL)) {
|
||||
this.coversFound = []
|
||||
this.hasSearched = false
|
||||
}
|
||||
this.imageUrl = this.book.cover || ''
|
||||
this.searchTitle = this.book.title || ''
|
||||
this.searchAuthor = this.book.author || ''
|
||||
this.searchAuthor = this.book.authorFL || ''
|
||||
},
|
||||
removeCover() {
|
||||
if (!this.book.cover) {
|
||||
|
@ -16,13 +16,18 @@
|
||||
</div>
|
||||
<div class="font-book text-center px-4 w-32 flex items-center cursor-pointer text-white text-opacity-40 hover:text-opacity-100" @click="sortByFilenameTrack" @mousedown.prevent>
|
||||
<span class="text-white">Track From Filename</span>
|
||||
<span class="material-icons ml-1" :class="currentSort === 'filename' ? 'text-white text-opacity-100 text-lg' : 'text-sm'">{{ currentSort === 'filename' ? 'expand_more' : 'unfold_more' }}</span>
|
||||
<span class="material-icons ml-1" :class="currentSort === 'track-filename' ? 'text-white text-opacity-100 text-lg' : 'text-sm'">{{ currentSort === 'track-filename' ? 'expand_more' : 'unfold_more' }}</span>
|
||||
</div>
|
||||
<div class="font-book text-center px-4 w-32 flex items-center cursor-pointer text-white text-opacity-40 hover:text-opacity-100" @click="sortByMetadataTrack" @mousedown.prevent>
|
||||
<span class="text-white">Track From Metadata</span>
|
||||
<span class="material-icons ml-1" :class="currentSort === 'metadata' ? 'text-white text-opacity-100 text-lg' : 'text-sm'">{{ currentSort === 'metadata' ? 'expand_more' : 'unfold_more' }}</span>
|
||||
</div>
|
||||
<div class="font-book truncate px-4 flex-grow">Filename</div>
|
||||
<div class="font-mono w-20 text-center">CD From Filename</div>
|
||||
<div class="font-book text-center px-4 flex-grow flex items-center cursor-pointer text-white text-opacity-40 hover:text-opacity-100" @click="sortByFilename" @mousedown.prevent>
|
||||
<span class="text-white">Filename</span>
|
||||
<span class="material-icons ml-1" :class="currentSort === 'filename' ? 'text-white text-opacity-100 text-lg' : 'text-sm'">{{ currentSort === 'filename' ? 'expand_more' : 'unfold_more' }}</span>
|
||||
</div>
|
||||
<!-- <div class="font-book truncate px-4 flex-grow">Filename</div> -->
|
||||
|
||||
<div class="font-mono w-20 text-center">Size</div>
|
||||
<div class="font-mono w-20 text-center">Duration</div>
|
||||
@ -43,6 +48,9 @@
|
||||
<div class="font-book text-center w-32">
|
||||
{{ audio.trackNumFromMeta }}
|
||||
</div>
|
||||
<div class="font-book truncate px-4 w-20">
|
||||
{{ audio.cdNumFromFilename }}
|
||||
</div>
|
||||
<div class="font-book truncate px-4 flex-grow">
|
||||
{{ audio.filename }}
|
||||
</div>
|
||||
@ -236,6 +244,12 @@ export default {
|
||||
if (a.trackNumFromFilename === null) return 1
|
||||
return a.trackNumFromFilename - b.trackNumFromFilename
|
||||
})
|
||||
this.currentSort = 'track-filename'
|
||||
},
|
||||
sortByFilename() {
|
||||
this.files.sort((a, b) => {
|
||||
return (a.filename || '').toLowerCase().localeCompare((b.filename || '').toLowerCase())
|
||||
})
|
||||
this.currentSort = 'filename'
|
||||
},
|
||||
checkTrackNumbers() {
|
||||
|
@ -19,7 +19,7 @@
|
||||
</div>
|
||||
|
||||
<p class="mb-2 mt-0.5 text-gray-100 text-xl">
|
||||
by <nuxt-link v-if="author" :to="`/library/${libraryId}/bookshelf?filter=authors.${$encode(author)}`" class="hover:underline">{{ author }}</nuxt-link
|
||||
by <nuxt-link v-if="authorFL" :to="`/library/${libraryId}/bookshelf?filter=authors.${$encode(authorFL)}`" class="hover:underline">{{ authorFL }}</nuxt-link
|
||||
><span v-else>Unknown</span>
|
||||
</p>
|
||||
<nuxt-link v-if="series" :to="`/library/${libraryId}/bookshelf/series?series=${$encode(series)}`" class="hover:underline font-sans text-gray-300 text-lg leading-7 mb-4"> {{ seriesText }}</nuxt-link>
|
||||
|
@ -54,7 +54,7 @@ export const getters = {
|
||||
if (filter === 'No Series') filtered = filtered.filter(ab => ab.book && !ab.book.series)
|
||||
else filtered = filtered.filter(ab => ab.book && ab.book.series === filter)
|
||||
}
|
||||
else if (group === 'authors') filtered = filtered.filter(ab => ab.book && ab.book.author === filter)
|
||||
else if (group === 'authors') filtered = filtered.filter(ab => ab.book && ab.book.authorFL === filter)
|
||||
else if (group === 'narrators') filtered = filtered.filter(ab => ab.book && ab.book.narrator === filter)
|
||||
else if (group === 'progress') {
|
||||
filtered = filtered.filter(ab => {
|
||||
@ -122,7 +122,7 @@ export const getters = {
|
||||
return seriesArray
|
||||
},
|
||||
getUniqueAuthors: (state) => {
|
||||
var _authors = state.audiobooks.filter(ab => !!(ab.book && ab.book.author)).map(ab => ab.book.author)
|
||||
var _authors = state.audiobooks.filter(ab => !!(ab.book && ab.book.authorFL)).map(ab => ab.book.authorFL)
|
||||
return [...new Set(_authors)].sort((a, b) => a.toLowerCase() < b.toLowerCase() ? -1 : 1)
|
||||
},
|
||||
getUniqueNarrators: (state) => {
|
||||
|
@ -109,7 +109,7 @@ class Scanner {
|
||||
titleDistance: 2,
|
||||
authorDistance: 2
|
||||
}
|
||||
var results = await this.bookFinder.findCovers('openlibrary', audiobook.title, audiobook.author, options)
|
||||
var results = await this.bookFinder.findCovers('openlibrary', audiobook.title, audiobook.authorFL, options)
|
||||
if (results.length) {
|
||||
Logger.debug(`[Scanner] Found best cover for "${audiobook.title}"`)
|
||||
|
||||
|
@ -13,6 +13,7 @@ class AudioFile {
|
||||
|
||||
this.trackNumFromMeta = null
|
||||
this.trackNumFromFilename = null
|
||||
this.cdNumFromFilename = null
|
||||
|
||||
this.format = null
|
||||
this.duration = null
|
||||
@ -39,6 +40,16 @@ class AudioFile {
|
||||
}
|
||||
}
|
||||
|
||||
// Sort number takes cd num into account
|
||||
// get sortNumber() {
|
||||
// if (this.manuallyVerified) return this.index
|
||||
// var num = this.index
|
||||
// if (this.cdNumFromFilename && !isNaN(this.cdNumFromFilename)) {
|
||||
// num += (Number(this.cdNumFromFilename) * 1000)
|
||||
// }
|
||||
// return num
|
||||
// }
|
||||
|
||||
toJSON() {
|
||||
return {
|
||||
index: this.index,
|
||||
@ -50,6 +61,7 @@ class AudioFile {
|
||||
addedAt: this.addedAt,
|
||||
trackNumFromMeta: this.trackNumFromMeta,
|
||||
trackNumFromFilename: this.trackNumFromFilename,
|
||||
cdNumFromFilename: this.cdNumFromFilename,
|
||||
manuallyVerified: !!this.manuallyVerified,
|
||||
invalid: !!this.invalid,
|
||||
exclude: !!this.exclude,
|
||||
@ -84,6 +96,7 @@ class AudioFile {
|
||||
|
||||
this.trackNumFromMeta = data.trackNumFromMeta || null
|
||||
this.trackNumFromFilename = data.trackNumFromFilename || null
|
||||
this.cdNumFromFilename = data.cdNumFromFilename || null
|
||||
|
||||
this.format = data.format
|
||||
this.duration = data.duration
|
||||
@ -117,6 +130,7 @@ class AudioFile {
|
||||
|
||||
this.trackNumFromMeta = data.trackNumFromMeta || null
|
||||
this.trackNumFromFilename = data.trackNumFromFilename || null
|
||||
this.cdNumFromFilename = data.cdNumFromFilename || null
|
||||
|
||||
this.manuallyVerified = !!data.manuallyVerified
|
||||
this.invalid = !!data.invalid
|
||||
@ -178,7 +192,10 @@ class AudioFile {
|
||||
channels: data.channels,
|
||||
channelLayout: data.channel_layout,
|
||||
chapters: data.chapters || [],
|
||||
embeddedCoverArt: data.embedded_cover_art || null
|
||||
embeddedCoverArt: data.embedded_cover_art || null,
|
||||
trackNumFromMeta: data.trackNumFromMeta,
|
||||
trackNumFromFilename: data.trackNumFromFilename,
|
||||
cdNumFromFilename: data.cdNumFromFilename
|
||||
}
|
||||
|
||||
var hasUpdates = false
|
||||
|
@ -35,11 +35,11 @@ class Book {
|
||||
get _title() { return this.title || '' }
|
||||
get _subtitle() { return this.subtitle || '' }
|
||||
get _narrator() { return this.narrator || '' }
|
||||
get _author() { return this.author || '' }
|
||||
get _author() { return this.authorFL || '' }
|
||||
get _series() { return this.series || '' }
|
||||
|
||||
get shouldSearchForCover() {
|
||||
if (this.author !== this.lastCoverSearchAuthor || this.title !== this.lastCoverSearchTitle || !this.lastCoverSearch) return true
|
||||
if (this.authorFL !== this.lastCoverSearchAuthor || this.title !== this.lastCoverSearchTitle || !this.lastCoverSearch) return true
|
||||
var timeSinceLastSearch = Date.now() - this.lastCoverSearch
|
||||
return timeSinceLastSearch > 1000 * 60 * 60 * 24 * 7 // every 7 days do another lookup
|
||||
}
|
||||
@ -180,7 +180,7 @@ class Book {
|
||||
|
||||
updateLastCoverSearch(coverWasFound) {
|
||||
this.lastCoverSearch = coverWasFound ? null : Date.now()
|
||||
this.lastCoverSearchAuthor = coverWasFound ? null : this.author
|
||||
this.lastCoverSearchAuthor = coverWasFound ? null : this.authorFL
|
||||
this.lastCoverSearchTitle = coverWasFound ? null : this.title
|
||||
}
|
||||
|
||||
@ -225,7 +225,7 @@ class Book {
|
||||
var authorMatch = this._author.toLowerCase().includes(search)
|
||||
var seriesMatch = this._series.toLowerCase().includes(search)
|
||||
|
||||
var bookMatchKey = titleMatch ? 'title' : subtitleMatch ? 'subtitle' : authorMatch ? 'author' : seriesMatch ? 'series' : false
|
||||
var bookMatchKey = titleMatch ? 'title' : subtitleMatch ? 'subtitle' : authorMatch ? 'authorFL' : seriesMatch ? 'series' : false
|
||||
var bookMatchText = bookMatchKey ? this[bookMatchKey] : ''
|
||||
return {
|
||||
book: bookMatchKey,
|
||||
|
@ -89,7 +89,10 @@ function getTrackNumberFromFilename(title, author, series, publishYear, filename
|
||||
if (publishYear) partbasename = partbasename.replace(publishYear)
|
||||
|
||||
// Remove eg. "disc 1" from path
|
||||
partbasename = partbasename.replace(/ disc \d\d? /i, '')
|
||||
partbasename = partbasename.replace(/\bdisc \d\d?\b/i, '')
|
||||
|
||||
// Remove "cd01" or "cd 01" from path
|
||||
partbasename = partbasename.replace(/\bcd ?\d\d?\b/i, '')
|
||||
|
||||
var numbersinpath = partbasename.match(/\d{1,4}/g)
|
||||
if (!numbersinpath) return null
|
||||
@ -98,6 +101,27 @@ function getTrackNumberFromFilename(title, author, series, publishYear, filename
|
||||
return number
|
||||
}
|
||||
|
||||
function getCdNumberFromFilename(title, author, series, publishYear, filename) {
|
||||
var partbasename = Path.basename(filename, Path.extname(filename))
|
||||
|
||||
// Remove title, author, series, and publishYear from filename if there
|
||||
if (title) partbasename = partbasename.replace(title, '')
|
||||
if (author) partbasename = partbasename.replace(author, '')
|
||||
if (series) partbasename = partbasename.replace(series, '')
|
||||
if (publishYear) partbasename = partbasename.replace(publishYear)
|
||||
|
||||
var cdNumber = null
|
||||
|
||||
var cdmatch = partbasename.match(/\b(disc|cd) ?(\d\d?)\b/i)
|
||||
if (cdmatch && cdmatch.length > 2 && cdmatch[2]) {
|
||||
if (!isNaN(cdmatch[2])) {
|
||||
cdNumber = Number(cdmatch[2])
|
||||
}
|
||||
}
|
||||
|
||||
return cdNumber
|
||||
}
|
||||
|
||||
async function scanAudioFiles(audiobook, newAudioFiles) {
|
||||
if (!newAudioFiles || !newAudioFiles.length) {
|
||||
Logger.error('[AudioFileScanner] Scan Audio Files no new files', audiobook.title)
|
||||
@ -123,6 +147,14 @@ async function scanAudioFiles(audiobook, newAudioFiles) {
|
||||
|
||||
var trackNumFromFilename = getTrackNumberFromFilename(book.title, book.author, book.series, book.publishYear, audioFile.filename)
|
||||
|
||||
var cdNumFromFilename = getCdNumberFromFilename(book.title, book.author, book.series, book.publishYear, audioFile.filename)
|
||||
|
||||
// IF CD num was found but no track num - USE cd num as track num
|
||||
if (!trackNumFromFilename && cdNumFromFilename) {
|
||||
trackNumFromFilename = cdNumFromFilename
|
||||
cdNumFromFilename = null
|
||||
}
|
||||
|
||||
var audioFileObj = {
|
||||
ino: audioFile.ino,
|
||||
filename: audioFile.filename,
|
||||
@ -131,7 +163,8 @@ async function scanAudioFiles(audiobook, newAudioFiles) {
|
||||
ext: audioFile.ext,
|
||||
...scanData,
|
||||
trackNumFromMeta,
|
||||
trackNumFromFilename
|
||||
trackNumFromFilename,
|
||||
cdNumFromFilename
|
||||
}
|
||||
var audioFile = audiobook.addAudioFile(audioFileObj)
|
||||
|
||||
@ -172,6 +205,7 @@ async function scanAudioFiles(audiobook, newAudioFiles) {
|
||||
}
|
||||
|
||||
tracks.sort((a, b) => a.index - b.index)
|
||||
|
||||
audiobook.audioFiles.sort((a, b) => {
|
||||
var aNum = isNumber(a.trackNumFromMeta) ? a.trackNumFromMeta : isNumber(a.trackNumFromFilename) ? a.trackNumFromFilename : 0
|
||||
var bNum = isNumber(b.trackNumFromMeta) ? b.trackNumFromMeta : isNumber(b.trackNumFromFilename) ? b.trackNumFromFilename : 0
|
||||
@ -209,7 +243,27 @@ async function rescanAudioFiles(audiobook) {
|
||||
// audiobook.invalidAudioFiles.push(parts[i])
|
||||
continue;
|
||||
}
|
||||
var hasUpdates = audioFile.updateMetadata(scanData)
|
||||
|
||||
var trackNumFromMeta = getTrackNumberFromMeta(scanData)
|
||||
var book = audiobook.book || {}
|
||||
|
||||
var trackNumFromFilename = getTrackNumberFromFilename(book.title, book.author, book.series, book.publishYear, audioFile.filename)
|
||||
|
||||
var cdNumFromFilename = getCdNumberFromFilename(book.title, book.author, book.series, book.publishYear, audioFile.filename)
|
||||
|
||||
// IF CD num was found but no track num - USE cd num as track num
|
||||
if (!trackNumFromFilename && cdNumFromFilename) {
|
||||
trackNumFromFilename = cdNumFromFilename
|
||||
cdNumFromFilename = null
|
||||
}
|
||||
|
||||
var metadataUpdate = {
|
||||
...scanData,
|
||||
trackNumFromMeta,
|
||||
trackNumFromFilename,
|
||||
cdNumFromFilename
|
||||
}
|
||||
var hasUpdates = audioFile.updateMetadata(metadataUpdate)
|
||||
if (hasUpdates) {
|
||||
// Sync audio track with audio file
|
||||
var matchingAudioTrack = audiobook.tracks.find(t => t.ino === audioFile.ino)
|
||||
|
@ -206,9 +206,16 @@ function getAudiobookDataFromDir(folderPath, dir, parseSubtitle = false) {
|
||||
|
||||
|
||||
var publishYear = null
|
||||
// If Title is of format 1999 - Title, then use 1999 as publish year
|
||||
var publishYearMatch = title.match(/^([0-9]{4}) - (.+)/)
|
||||
if (publishYearMatch && publishYearMatch.length > 2) {
|
||||
// OLD regex (not matching parentheses)
|
||||
// var publishYearMatch = title.match(/^([0-9]{4}) - (.+)/)
|
||||
|
||||
// If Title is of format 1999 OR (1999) - Title, then use 1999 as publish year
|
||||
var publishYearMatch = title.match(/^(\(?[0-9]{4}\)?) - (.+)/)
|
||||
if (publishYearMatch && publishYearMatch.length > 2 && publishYearMatch[1]) {
|
||||
// Strip parentheses
|
||||
if (publishYearMatch[1].startsWith('(') && publishYearMatch[1].endsWith(')')) {
|
||||
publishYearMatch[1] = publishYearMatch[1].slice(1, -1)
|
||||
}
|
||||
if (!isNaN(publishYearMatch[1])) {
|
||||
publishYear = publishYearMatch[1]
|
||||
title = publishYearMatch[2]
|
||||
|
Loading…
Reference in New Issue
Block a user