From 92c2c53c0991c7c9c81903e51d2a5f251238e21b Mon Sep 17 00:00:00 2001 From: advplyr Date: Sat, 23 Oct 2021 20:31:48 -0500 Subject: [PATCH] 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 --- .../components/cards/AudiobookSearchCard.vue | 8 +-- client/components/modals/edit-tabs/Cover.vue | 4 +- client/pages/audiobook/_id/edit.vue | 18 +++++- client/pages/audiobook/_id/index.vue | 2 +- client/store/audiobooks.js | 4 +- server/Scanner.js | 2 +- server/objects/AudioFile.js | 19 +++++- server/objects/Book.js | 8 +-- server/utils/audioFileScanner.js | 60 ++++++++++++++++++- server/utils/scandir.js | 13 +++- 10 files changed, 115 insertions(+), 23 deletions(-) diff --git a/client/components/cards/AudiobookSearchCard.vue b/client/components/cards/AudiobookSearchCard.vue index 9edbb9cd..a7de5c21 100644 --- a/client/components/cards/AudiobookSearchCard.vue +++ b/client/components/cards/AudiobookSearchCard.vue @@ -7,7 +7,7 @@

{{ matchHtml }}

-

by {{ author }}

+

by {{ authorFL }}

@@ -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 `

Tags: ${html}

` - if (this.matchKey === 'author') return `by ${html}` + if (this.matchKey === 'authorFL') return `by ${html}` if (this.matchKey === 'series') return `

Series: ${html}

` return `${html}` } diff --git a/client/components/modals/edit-tabs/Cover.vue b/client/components/modals/edit-tabs/Cover.vue index 4241ea19..22dc340f 100644 --- a/client/components/modals/edit-tabs/Cover.vue +++ b/client/components/modals/edit-tabs/Cover.vue @@ -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) { diff --git a/client/pages/audiobook/_id/edit.vue b/client/pages/audiobook/_id/edit.vue index d6a77f92..d2bf15db 100644 --- a/client/pages/audiobook/_id/edit.vue +++ b/client/pages/audiobook/_id/edit.vue @@ -16,13 +16,18 @@
Track From Filename - {{ currentSort === 'filename' ? 'expand_more' : 'unfold_more' }} + {{ currentSort === 'track-filename' ? 'expand_more' : 'unfold_more' }}
Track From Metadata {{ currentSort === 'metadata' ? 'expand_more' : 'unfold_more' }}
-
Filename
+
CD From Filename
+
+ Filename + {{ currentSort === 'filename' ? 'expand_more' : 'unfold_more' }} +
+
Size
Duration
@@ -43,6 +48,9 @@
{{ audio.trackNumFromMeta }}
+
+ {{ audio.cdNumFromFilename }} +
{{ audio.filename }}
@@ -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() { diff --git a/client/pages/audiobook/_id/index.vue b/client/pages/audiobook/_id/index.vue index 051dc90e..6ac5f69b 100644 --- a/client/pages/audiobook/_id/index.vue +++ b/client/pages/audiobook/_id/index.vue @@ -19,7 +19,7 @@

- by {{ author }}{{ authorFL }}Unknown

{{ seriesText }} diff --git a/client/store/audiobooks.js b/client/store/audiobooks.js index 94b9f43b..b27fb24b 100644 --- a/client/store/audiobooks.js +++ b/client/store/audiobooks.js @@ -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) => { diff --git a/server/Scanner.js b/server/Scanner.js index 7d15d4a2..2322e252 100644 --- a/server/Scanner.js +++ b/server/Scanner.js @@ -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}"`) diff --git a/server/objects/AudioFile.js b/server/objects/AudioFile.js index c9248e46..cf873e42 100644 --- a/server/objects/AudioFile.js +++ b/server/objects/AudioFile.js @@ -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 diff --git a/server/objects/Book.js b/server/objects/Book.js index 295682b4..30539b10 100644 --- a/server/objects/Book.js +++ b/server/objects/Book.js @@ -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, diff --git a/server/utils/audioFileScanner.js b/server/utils/audioFileScanner.js index 19e358d5..b7322de3 100644 --- a/server/utils/audioFileScanner.js +++ b/server/utils/audioFileScanner.js @@ -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) diff --git a/server/utils/scandir.js b/server/utils/scandir.js index a4d421dd..c7f28e60 100644 --- a/server/utils/scandir.js +++ b/server/utils/scandir.js @@ -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]