mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2025-01-02 12:09:11 +01:00
Merge pull request #716 from jmt-gh/abs_overdrive
Add support for leveraging chapter data directly from Overdrive mp3s during scanning
This commit is contained in:
commit
8894f52439
3
.gitignore
vendored
3
.gitignore
vendored
@ -11,6 +11,7 @@ test/
|
|||||||
/client/.nuxt/
|
/client/.nuxt/
|
||||||
/client/dist/
|
/client/dist/
|
||||||
/dist/
|
/dist/
|
||||||
|
library/
|
||||||
|
|
||||||
sw.*
|
sw.*
|
||||||
.DS_STORE
|
.DS_STORE
|
||||||
|
@ -103,6 +103,16 @@
|
|||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center py-2">
|
||||||
|
<ui-toggle-switch v-model="newServerSettings.scannerPreferOverdriveMediaMarker" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('scannerPreferOverdriveMediaMarker', val)" />
|
||||||
|
<ui-tooltip :text="tooltips.scannerPreferOverdriveMediaMarker">
|
||||||
|
<p class="pl-4 text-lg">
|
||||||
|
Scanner prefer Overdrive Media Markers for chapters
|
||||||
|
<span class="material-icons icon-text">info_outlined</span>
|
||||||
|
</p>
|
||||||
|
</ui-tooltip>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center py-2">
|
<div class="flex items-center py-2">
|
||||||
<ui-toggle-switch v-model="newServerSettings.scannerPreferOpfMetadata" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('scannerPreferOpfMetadata', val)" />
|
<ui-toggle-switch v-model="newServerSettings.scannerPreferOpfMetadata" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('scannerPreferOpfMetadata', val)" />
|
||||||
<ui-tooltip :text="tooltips.scannerPreferOpfMetadata">
|
<ui-tooltip :text="tooltips.scannerPreferOpfMetadata">
|
||||||
@ -245,7 +255,8 @@ export default {
|
|||||||
storeCoverWithItem: 'By default covers are stored in /metadata/items, enabling this setting will store covers in your library item folder. Only one file named "cover" will be kept',
|
storeCoverWithItem: 'By default covers are stored in /metadata/items, enabling this setting will store covers in your library item folder. Only one file named "cover" will be kept',
|
||||||
storeMetadataWithItem: 'By default metadata files are stored in /metadata/items, enabling this setting will store metadata files in your library item folders. Uses .abs file extension',
|
storeMetadataWithItem: 'By default metadata files are stored in /metadata/items, enabling this setting will store metadata files in your library item folders. Uses .abs file extension',
|
||||||
coverAspectRatio: 'Prefer to use square covers over standard 1.6:1 book covers',
|
coverAspectRatio: 'Prefer to use square covers over standard 1.6:1 book covers',
|
||||||
enableEReader: 'E-reader is still a work in progress, but use this setting to open it up to all your users (or use the "Experimental Features" toggle below just for you)'
|
enableEReader: 'E-reader is still a work in progress, but use this setting to open it up to all your users (or use the "Experimental Features" toggle below just for you)',
|
||||||
|
scannerPreferOverdriveMediaMarker: 'MP3 files from Overdrive come with chapter timings embedded as custom metadata. Enabling this will use these tags for chapter timings automatically'
|
||||||
},
|
},
|
||||||
showConfirmPurgeCache: false
|
showConfirmPurgeCache: false
|
||||||
}
|
}
|
||||||
|
@ -103,7 +103,7 @@ class PodcastController {
|
|||||||
Logger.error('Invalid podcast feed request response')
|
Logger.error('Invalid podcast feed request response')
|
||||||
return res.status(500).send('Bad response from feed request')
|
return res.status(500).send('Bad response from feed request')
|
||||||
}
|
}
|
||||||
Logger.debug(`[PdocastController] Podcast feed size ${(data.data.length / 1024 / 1024).toFixed(2)}MB`)
|
Logger.debug(`[PodcastController] Podcast feed size ${(data.data.length / 1024 / 1024).toFixed(2)}MB`)
|
||||||
var payload = await parsePodcastRssFeedXml(data.data, false, includeRaw)
|
var payload = await parsePodcastRssFeedXml(data.data, false, includeRaw)
|
||||||
if (!payload) {
|
if (!payload) {
|
||||||
return res.status(500).send('Invalid podcast RSS feed')
|
return res.status(500).send('Invalid podcast RSS feed')
|
||||||
|
@ -3,6 +3,7 @@ const Logger = require('../../Logger')
|
|||||||
const BookMetadata = require('../metadata/BookMetadata')
|
const BookMetadata = require('../metadata/BookMetadata')
|
||||||
const { areEquivalent, copyValue } = require('../../utils/index')
|
const { areEquivalent, copyValue } = require('../../utils/index')
|
||||||
const { parseOpfMetadataXML } = require('../../utils/parsers/parseOpfMetadata')
|
const { parseOpfMetadataXML } = require('../../utils/parsers/parseOpfMetadata')
|
||||||
|
const { overdriveMediaMarkersExist, parseOverdriveMediaMarkersAsChapters } = require('../../utils/parsers/parseOverdriveMediaMarkers')
|
||||||
const abmetadataGenerator = require('../../utils/abmetadataGenerator')
|
const abmetadataGenerator = require('../../utils/abmetadataGenerator')
|
||||||
const { readTextFile } = require('../../utils/fileUtils')
|
const { readTextFile } = require('../../utils/fileUtils')
|
||||||
const AudioFile = require('../files/AudioFile')
|
const AudioFile = require('../files/AudioFile')
|
||||||
@ -360,10 +361,11 @@ class Book {
|
|||||||
this.rebuildTracks()
|
this.rebuildTracks()
|
||||||
}
|
}
|
||||||
|
|
||||||
rebuildTracks() {
|
rebuildTracks(preferOverdriveMediaMarker) {
|
||||||
|
Logger.debug(`[Book] Tracks being rebuilt...!`)
|
||||||
this.audioFiles.sort((a, b) => a.index - b.index)
|
this.audioFiles.sort((a, b) => a.index - b.index)
|
||||||
this.missingParts = []
|
this.missingParts = []
|
||||||
this.setChapters()
|
this.setChapters(preferOverdriveMediaMarker)
|
||||||
this.checkUpdateMissingTracks()
|
this.checkUpdateMissingTracks()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -395,9 +397,16 @@ class Book {
|
|||||||
return wasUpdated
|
return wasUpdated
|
||||||
}
|
}
|
||||||
|
|
||||||
setChapters() {
|
setChapters(preferOverdriveMediaMarker = false) {
|
||||||
// If 1 audio file without chapters, then no chapters will be set
|
// If 1 audio file without chapters, then no chapters will be set
|
||||||
var includedAudioFiles = this.audioFiles.filter(af => !af.exclude)
|
var includedAudioFiles = this.audioFiles.filter(af => !af.exclude)
|
||||||
|
|
||||||
|
// If overdrive media markers are present and preferred, use those instead
|
||||||
|
if (preferOverdriveMediaMarker && overdriveMediaMarkersExist(includedAudioFiles)) {
|
||||||
|
Logger.info('[Book] Overdrive Media Markers and preference found! Using these for chapter definitions')
|
||||||
|
return this.chapters = parseOverdriveMediaMarkersAsChapters(includedAudioFiles)
|
||||||
|
}
|
||||||
|
|
||||||
if (includedAudioFiles.length === 1) {
|
if (includedAudioFiles.length === 1) {
|
||||||
// 1 audio file with chapters
|
// 1 audio file with chapters
|
||||||
if (includedAudioFiles[0].chapters) {
|
if (includedAudioFiles[0].chapters) {
|
||||||
|
@ -20,6 +20,7 @@ class AudioMetaTags {
|
|||||||
this.tagIsbn = null
|
this.tagIsbn = null
|
||||||
this.tagLanguage = null
|
this.tagLanguage = null
|
||||||
this.tagASIN = null
|
this.tagASIN = null
|
||||||
|
this.tagOverdriveMediaMarker = null
|
||||||
|
|
||||||
if (metadata) {
|
if (metadata) {
|
||||||
this.construct(metadata)
|
this.construct(metadata)
|
||||||
@ -58,6 +59,7 @@ class AudioMetaTags {
|
|||||||
this.tagIsbn = metadata.tagIsbn || null
|
this.tagIsbn = metadata.tagIsbn || null
|
||||||
this.tagLanguage = metadata.tagLanguage || null
|
this.tagLanguage = metadata.tagLanguage || null
|
||||||
this.tagASIN = metadata.tagASIN || null
|
this.tagASIN = metadata.tagASIN || null
|
||||||
|
this.tagOverdriveMediaMarker = metadata.tagOverdriveMediaMarker || null
|
||||||
}
|
}
|
||||||
|
|
||||||
// Data parsed in prober.js
|
// Data parsed in prober.js
|
||||||
@ -82,6 +84,7 @@ class AudioMetaTags {
|
|||||||
this.tagIsbn = payload.file_tag_isbn || null
|
this.tagIsbn = payload.file_tag_isbn || null
|
||||||
this.tagLanguage = payload.file_tag_language || null
|
this.tagLanguage = payload.file_tag_language || null
|
||||||
this.tagASIN = payload.file_tag_asin || null
|
this.tagASIN = payload.file_tag_asin || null
|
||||||
|
this.tagOverdriveMediaMarker = payload.file_tag_overdrive_media_marker || null
|
||||||
}
|
}
|
||||||
|
|
||||||
updateData(payload) {
|
updateData(payload) {
|
||||||
@ -105,7 +108,8 @@ class AudioMetaTags {
|
|||||||
tagEncodedBy: payload.file_tag_encodedby || null,
|
tagEncodedBy: payload.file_tag_encodedby || null,
|
||||||
tagIsbn: payload.file_tag_isbn || null,
|
tagIsbn: payload.file_tag_isbn || null,
|
||||||
tagLanguage: payload.file_tag_language || null,
|
tagLanguage: payload.file_tag_language || null,
|
||||||
tagASIN: payload.file_tag_asin || null
|
tagASIN: payload.file_tag_asin || null,
|
||||||
|
tagOverdriveMediaMarker: payload.file_tag_overdrive_media_marker || null,
|
||||||
}
|
}
|
||||||
|
|
||||||
var hasUpdates = false
|
var hasUpdates = false
|
||||||
|
@ -262,6 +262,10 @@ class BookMetadata {
|
|||||||
{
|
{
|
||||||
tag: 'tagASIN',
|
tag: 'tagASIN',
|
||||||
key: 'asin'
|
key: 'asin'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tag: 'tagOverdriveMediaMarker',
|
||||||
|
key: 'overdriveMediaMarker'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -13,6 +13,7 @@ class ServerSettings {
|
|||||||
this.scannerPreferOpfMetadata = false
|
this.scannerPreferOpfMetadata = false
|
||||||
this.scannerPreferMatchedMetadata = false
|
this.scannerPreferMatchedMetadata = false
|
||||||
this.scannerDisableWatcher = false
|
this.scannerDisableWatcher = false
|
||||||
|
this.scannerPreferOverdriveMediaMarker = false
|
||||||
|
|
||||||
// Metadata - choose to store inside users library item folder
|
// Metadata - choose to store inside users library item folder
|
||||||
this.storeCoverWithItem = false
|
this.storeCoverWithItem = false
|
||||||
@ -65,6 +66,7 @@ class ServerSettings {
|
|||||||
this.scannerPreferOpfMetadata = !!settings.scannerPreferOpfMetadata
|
this.scannerPreferOpfMetadata = !!settings.scannerPreferOpfMetadata
|
||||||
this.scannerPreferMatchedMetadata = !!settings.scannerPreferMatchedMetadata
|
this.scannerPreferMatchedMetadata = !!settings.scannerPreferMatchedMetadata
|
||||||
this.scannerDisableWatcher = !!settings.scannerDisableWatcher
|
this.scannerDisableWatcher = !!settings.scannerDisableWatcher
|
||||||
|
this.scannerPreferOverdriveMediaMarker = !!settings.scannerPreferOverdriveMediaMarker
|
||||||
|
|
||||||
this.storeCoverWithItem = !!settings.storeCoverWithItem
|
this.storeCoverWithItem = !!settings.storeCoverWithItem
|
||||||
if (settings.storeCoverWithBook != undefined) { // storeCoverWithBook was old name of setting < v2
|
if (settings.storeCoverWithBook != undefined) { // storeCoverWithBook was old name of setting < v2
|
||||||
@ -111,6 +113,7 @@ class ServerSettings {
|
|||||||
scannerPreferOpfMetadata: this.scannerPreferOpfMetadata,
|
scannerPreferOpfMetadata: this.scannerPreferOpfMetadata,
|
||||||
scannerPreferMatchedMetadata: this.scannerPreferMatchedMetadata,
|
scannerPreferMatchedMetadata: this.scannerPreferMatchedMetadata,
|
||||||
scannerDisableWatcher: this.scannerDisableWatcher,
|
scannerDisableWatcher: this.scannerDisableWatcher,
|
||||||
|
scannerPreferOverdriveMediaMarker: this.scannerPreferOverdriveMediaMarker,
|
||||||
storeCoverWithItem: this.storeCoverWithItem,
|
storeCoverWithItem: this.storeCoverWithItem,
|
||||||
storeMetadataWithItem: this.storeMetadataWithItem,
|
storeMetadataWithItem: this.storeMetadataWithItem,
|
||||||
rateLimitLoginRequests: this.rateLimitLoginRequests,
|
rateLimitLoginRequests: this.rateLimitLoginRequests,
|
||||||
|
@ -34,6 +34,7 @@ class LibraryScan {
|
|||||||
get forceRescan() { return !!this._scanOptions.forceRescan }
|
get forceRescan() { return !!this._scanOptions.forceRescan }
|
||||||
get preferAudioMetadata() { return !!this._scanOptions.preferAudioMetadata }
|
get preferAudioMetadata() { return !!this._scanOptions.preferAudioMetadata }
|
||||||
get preferOpfMetadata() { return !!this._scanOptions.preferOpfMetadata }
|
get preferOpfMetadata() { return !!this._scanOptions.preferOpfMetadata }
|
||||||
|
get preferOverdriveMediaMarker() { return !!this._scanOptions.preferOverdriveMediaMarker }
|
||||||
get findCovers() { return !!this._scanOptions.findCovers }
|
get findCovers() { return !!this._scanOptions.findCovers }
|
||||||
get timestamp() {
|
get timestamp() {
|
||||||
return (new Date()).toISOString()
|
return (new Date()).toISOString()
|
||||||
|
@ -195,7 +195,7 @@ class MediaFileScanner {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async scanMediaFiles(mediaLibraryFiles, scanData, libraryItem, preferAudioMetadata, libraryScan = null) {
|
async scanMediaFiles(mediaLibraryFiles, scanData, libraryItem, preferAudioMetadata, preferOverdriveMediaMarker, libraryScan = null) {
|
||||||
var hasUpdated = false
|
var hasUpdated = false
|
||||||
|
|
||||||
var mediaScanResult = await this.executeMediaFileScans(libraryItem.mediaType, mediaLibraryFiles, scanData)
|
var mediaScanResult = await this.executeMediaFileScans(libraryItem.mediaType, mediaLibraryFiles, scanData)
|
||||||
@ -208,6 +208,7 @@ class MediaFileScanner {
|
|||||||
} else if (mediaScanResult.audioFiles.length) {
|
} else if (mediaScanResult.audioFiles.length) {
|
||||||
if (libraryScan) {
|
if (libraryScan) {
|
||||||
libraryScan.addLog(LogLevel.DEBUG, `Library Item "${scanData.path}" Audio file scan took ${mediaScanResult.elapsed}ms for ${mediaScanResult.audioFiles.length} with average time of ${mediaScanResult.averageScanDuration}ms`)
|
libraryScan.addLog(LogLevel.DEBUG, `Library Item "${scanData.path}" Audio file scan took ${mediaScanResult.elapsed}ms for ${mediaScanResult.audioFiles.length} with average time of ${mediaScanResult.averageScanDuration}ms`)
|
||||||
|
Logger.debug(`Library Item "${scanData.path}" Audio file scan took ${mediaScanResult.elapsed}ms for ${mediaScanResult.audioFiles.length} with average time of ${mediaScanResult.averageScanDuration}ms`)
|
||||||
}
|
}
|
||||||
|
|
||||||
var totalAudioFilesToInclude = mediaScanResult.audioFiles.length
|
var totalAudioFilesToInclude = mediaScanResult.audioFiles.length
|
||||||
@ -247,7 +248,7 @@ class MediaFileScanner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (hasUpdated) {
|
if (hasUpdated) {
|
||||||
libraryItem.media.rebuildTracks()
|
libraryItem.media.rebuildTracks(preferOverdriveMediaMarker)
|
||||||
}
|
}
|
||||||
} else { // Podcast Media Type
|
} else { // Podcast Media Type
|
||||||
var existingAudioFiles = mediaScanResult.audioFiles.filter(af => libraryItem.media.findFileWithInode(af.ino))
|
var existingAudioFiles = mediaScanResult.audioFiles.filter(af => libraryItem.media.findFileWithInode(af.ino))
|
||||||
|
@ -9,6 +9,7 @@ class ScanOptions {
|
|||||||
this.preferAudioMetadata = false
|
this.preferAudioMetadata = false
|
||||||
this.preferOpfMetadata = false
|
this.preferOpfMetadata = false
|
||||||
this.preferMatchedMetadata = false
|
this.preferMatchedMetadata = false
|
||||||
|
this.preferOverdriveMediaMarker = false
|
||||||
|
|
||||||
if (options) {
|
if (options) {
|
||||||
this.construct(options)
|
this.construct(options)
|
||||||
@ -34,7 +35,8 @@ class ScanOptions {
|
|||||||
storeCoverWithItem: this.storeCoverWithItem,
|
storeCoverWithItem: this.storeCoverWithItem,
|
||||||
preferAudioMetadata: this.preferAudioMetadata,
|
preferAudioMetadata: this.preferAudioMetadata,
|
||||||
preferOpfMetadata: this.preferOpfMetadata,
|
preferOpfMetadata: this.preferOpfMetadata,
|
||||||
preferMatchedMetadata: this.preferMatchedMetadata
|
preferMatchedMetadata: this.preferMatchedMetadata,
|
||||||
|
preferOverdriveMediaMarker: this.preferOverdriveMediaMarker
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -47,6 +49,7 @@ class ScanOptions {
|
|||||||
this.preferAudioMetadata = serverSettings.scannerPreferAudioMetadata
|
this.preferAudioMetadata = serverSettings.scannerPreferAudioMetadata
|
||||||
this.preferOpfMetadata = serverSettings.scannerPreferOpfMetadata
|
this.preferOpfMetadata = serverSettings.scannerPreferOpfMetadata
|
||||||
this.scannerPreferMatchedMetadata = serverSettings.scannerPreferMatchedMetadata
|
this.scannerPreferMatchedMetadata = serverSettings.scannerPreferMatchedMetadata
|
||||||
|
this.preferOverdriveMediaMarker = serverSettings.scannerPreferOverdriveMediaMarker
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
module.exports = ScanOptions
|
module.exports = ScanOptions
|
@ -80,7 +80,7 @@ class Scanner {
|
|||||||
// Scan all audio files
|
// Scan all audio files
|
||||||
if (libraryItem.hasAudioFiles) {
|
if (libraryItem.hasAudioFiles) {
|
||||||
var libraryAudioFiles = libraryItem.libraryFiles.filter(lf => lf.fileType === 'audio')
|
var libraryAudioFiles = libraryItem.libraryFiles.filter(lf => lf.fileType === 'audio')
|
||||||
if (await MediaFileScanner.scanMediaFiles(libraryAudioFiles, libraryItemData, libraryItem, this.db.serverSettings.scannerPreferAudioMetadata)) {
|
if (await MediaFileScanner.scanMediaFiles(libraryAudioFiles, libraryItemData, libraryItem, this.db.serverSettings.scannerPreferAudioMetadata, this.db.serverSettings.scannerPreferOverdriveMediaMarker)) {
|
||||||
hasUpdated = true
|
hasUpdated = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -310,7 +310,7 @@ class Scanner {
|
|||||||
|
|
||||||
async scanNewLibraryItemDataChunk(newLibraryItemsData, libraryScan) {
|
async scanNewLibraryItemDataChunk(newLibraryItemsData, libraryScan) {
|
||||||
var newLibraryItems = await Promise.all(newLibraryItemsData.map((lid) => {
|
var newLibraryItems = await Promise.all(newLibraryItemsData.map((lid) => {
|
||||||
return this.scanNewLibraryItem(lid, libraryScan.libraryMediaType, libraryScan.preferAudioMetadata, libraryScan.preferOpfMetadata, libraryScan.findCovers, libraryScan)
|
return this.scanNewLibraryItem(lid, libraryScan.libraryMediaType, libraryScan.preferAudioMetadata, libraryScan.preferOpfMetadata, libraryScan.findCovers, libraryScan.preferOverdriveMediaMarker, libraryScan)
|
||||||
}))
|
}))
|
||||||
newLibraryItems = newLibraryItems.filter(li => li) // Filter out nulls
|
newLibraryItems = newLibraryItems.filter(li => li) // Filter out nulls
|
||||||
|
|
||||||
@ -337,7 +337,7 @@ class Scanner {
|
|||||||
// forceRescan all existing audio files - will probe and update ID3 tag metadata
|
// forceRescan all existing audio files - will probe and update ID3 tag metadata
|
||||||
var existingAudioFiles = existingLibraryFiles.filter(lf => lf.fileType === 'audio')
|
var existingAudioFiles = existingLibraryFiles.filter(lf => lf.fileType === 'audio')
|
||||||
if (libraryScan.scanOptions.forceRescan && existingAudioFiles.length) {
|
if (libraryScan.scanOptions.forceRescan && existingAudioFiles.length) {
|
||||||
if (await MediaFileScanner.scanMediaFiles(existingAudioFiles, scanData, libraryItem, libraryScan.preferAudioMetadata, libraryScan)) {
|
if (await MediaFileScanner.scanMediaFiles(existingAudioFiles, scanData, libraryItem, libraryScan.preferAudioMetadata, libraryScan.preferOverdriveMediaMarker, libraryScan)) {
|
||||||
hasUpdated = true
|
hasUpdated = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -345,7 +345,7 @@ class Scanner {
|
|||||||
var newAudioFiles = newLibraryFiles.filter(lf => lf.fileType === 'audio')
|
var newAudioFiles = newLibraryFiles.filter(lf => lf.fileType === 'audio')
|
||||||
var removedAudioFiles = filesRemoved.filter(lf => lf.fileType === 'audio')
|
var removedAudioFiles = filesRemoved.filter(lf => lf.fileType === 'audio')
|
||||||
if (newAudioFiles.length || removedAudioFiles.length) {
|
if (newAudioFiles.length || removedAudioFiles.length) {
|
||||||
if (await MediaFileScanner.scanMediaFiles(newAudioFiles, scanData, libraryItem, libraryScan.preferAudioMetadata, libraryScan)) {
|
if (await MediaFileScanner.scanMediaFiles(newAudioFiles, scanData, libraryItem, libraryScan.preferAudioMetadata, libraryScan.preferOverdriveMediaMarker, libraryScan)) {
|
||||||
hasUpdated = true
|
hasUpdated = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -379,7 +379,7 @@ class Scanner {
|
|||||||
return hasUpdated ? libraryItem : null
|
return hasUpdated ? libraryItem : null
|
||||||
}
|
}
|
||||||
|
|
||||||
async scanNewLibraryItem(libraryItemData, libraryMediaType, preferAudioMetadata, preferOpfMetadata, findCovers, libraryScan = null) {
|
async scanNewLibraryItem(libraryItemData, libraryMediaType, preferAudioMetadata, preferOpfMetadata, findCovers, preferOverdriveMediaMarker, libraryScan = null) {
|
||||||
if (libraryScan) libraryScan.addLog(LogLevel.DEBUG, `Scanning new library item "${libraryItemData.path}"`)
|
if (libraryScan) libraryScan.addLog(LogLevel.DEBUG, `Scanning new library item "${libraryItemData.path}"`)
|
||||||
else Logger.debug(`[Scanner] Scanning new item "${libraryItemData.path}"`)
|
else Logger.debug(`[Scanner] Scanning new item "${libraryItemData.path}"`)
|
||||||
|
|
||||||
@ -388,7 +388,7 @@ class Scanner {
|
|||||||
|
|
||||||
var mediaFiles = libraryItemData.libraryFiles.filter(lf => lf.fileType === 'audio' || lf.fileType === 'video')
|
var mediaFiles = libraryItemData.libraryFiles.filter(lf => lf.fileType === 'audio' || lf.fileType === 'video')
|
||||||
if (mediaFiles.length) {
|
if (mediaFiles.length) {
|
||||||
await MediaFileScanner.scanMediaFiles(mediaFiles, libraryItemData, libraryItem, preferAudioMetadata, libraryScan)
|
await MediaFileScanner.scanMediaFiles(mediaFiles, libraryItemData, libraryItem, preferAudioMetadata, preferOverdriveMediaMarker, libraryScan)
|
||||||
}
|
}
|
||||||
|
|
||||||
await libraryItem.syncFiles(preferOpfMetadata)
|
await libraryItem.syncFiles(preferOpfMetadata)
|
||||||
@ -608,7 +608,7 @@ class Scanner {
|
|||||||
var libraryItemData = await getLibraryItemFileData(libraryMediaType, folder, fullPath, isSingleMediaItem, this.db.serverSettings)
|
var libraryItemData = await getLibraryItemFileData(libraryMediaType, folder, fullPath, isSingleMediaItem, this.db.serverSettings)
|
||||||
if (!libraryItemData) return null
|
if (!libraryItemData) return null
|
||||||
var serverSettings = this.db.serverSettings
|
var serverSettings = this.db.serverSettings
|
||||||
return this.scanNewLibraryItem(libraryItemData, libraryMediaType, serverSettings.scannerPreferAudioMetadata, serverSettings.scannerPreferOpfMetadata, serverSettings.scannerFindCovers)
|
return this.scanNewLibraryItem(libraryItemData, libraryMediaType, serverSettings.scannerPreferAudioMetadata, serverSettings.scannerPreferOpfMetadata, serverSettings.scannerFindCovers, serverSettings.scannerPreferOverdriveMediaMarker)
|
||||||
}
|
}
|
||||||
|
|
||||||
async searchForCover(libraryItem, libraryScan = null) {
|
async searchForCover(libraryItem, libraryScan = null) {
|
||||||
|
148
server/utils/parsers/parseOverdriveMediaMarkers.js
Normal file
148
server/utils/parsers/parseOverdriveMediaMarkers.js
Normal file
@ -0,0 +1,148 @@
|
|||||||
|
const Logger = require('../../Logger')
|
||||||
|
|
||||||
|
// given a list of audio files, extract all of the Overdrive Media Markers metaTags, and return an array of them as XML
|
||||||
|
function extractOverdriveMediaMarkers(includedAudioFiles) {
|
||||||
|
Logger.debug('[parseOverdriveMediaMarkers] Extracting overdrive media markers')
|
||||||
|
var markers = includedAudioFiles.map((af) => af.metaTags.tagOverdriveMediaMarker).filter(notUndefined => notUndefined !== undefined).filter(elem => { return elem !== null }) || []
|
||||||
|
|
||||||
|
return markers
|
||||||
|
}
|
||||||
|
|
||||||
|
// given the array of Overdrive Media Markers from generateOverdriveMediaMarkers()
|
||||||
|
// parse and clean them in to something a bit more usable
|
||||||
|
function cleanOverdriveMediaMarkers(overdriveMediaMarkers) {
|
||||||
|
Logger.debug('[parseOverdriveMediaMarkers] Cleaning up overdrive media markers')
|
||||||
|
/*
|
||||||
|
returns an array of arrays of objects. Each inner array corresponds to an audio track, with it's objects being a chapter:
|
||||||
|
[
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"Name": "Chapter 1",
|
||||||
|
"Time": "0:00.000"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "Chapter 2",
|
||||||
|
"Time": "15:51.000"
|
||||||
|
},
|
||||||
|
{ etc }
|
||||||
|
]
|
||||||
|
]
|
||||||
|
*/
|
||||||
|
|
||||||
|
var parseString = require('xml2js').parseString; // function to convert xml to JSON
|
||||||
|
var parsedOverdriveMediaMarkers = []
|
||||||
|
|
||||||
|
overdriveMediaMarkers.forEach(function (item, index) {
|
||||||
|
var parsed_result
|
||||||
|
parseString(item, function (err, result) {
|
||||||
|
/*
|
||||||
|
result.Markers.Marker is the result of parsing the XML for the MediaMarker tags for the MP3 file (Part##.mp3)
|
||||||
|
it is shaped like this, and needs further cleaning below:
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"Name": [
|
||||||
|
"Chapter 1: "
|
||||||
|
],
|
||||||
|
"Time": [
|
||||||
|
"0:00.000"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ANOTHER CHAPTER
|
||||||
|
},
|
||||||
|
]
|
||||||
|
*/
|
||||||
|
|
||||||
|
// The values for Name and Time in results.Markers.Marker are returned as Arrays from parseString and should be strings
|
||||||
|
parsed_result = objectValuesArrayToString(result.Markers.Marker)
|
||||||
|
})
|
||||||
|
|
||||||
|
parsedOverdriveMediaMarkers.push(parsed_result)
|
||||||
|
})
|
||||||
|
|
||||||
|
return removeExtraChapters(parsedOverdriveMediaMarkers)
|
||||||
|
}
|
||||||
|
|
||||||
|
// given an array of objects, convert any values that are arrays to strings
|
||||||
|
function objectValuesArrayToString(arrayOfObjects) {
|
||||||
|
Logger.debug('[parseOverdriveMediaMarkers] Converting Marker object values from arrays to strings')
|
||||||
|
arrayOfObjects.forEach((item) => {
|
||||||
|
Object.keys(item).forEach(key => {
|
||||||
|
item[key] = item[key].toString()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
return arrayOfObjects
|
||||||
|
}
|
||||||
|
|
||||||
|
// Overdrive sometimes has weird chapters and subchapters defined
|
||||||
|
// These aren't necessary, so lets remove them
|
||||||
|
function removeExtraChapters(parsedOverdriveMediaMarkers) {
|
||||||
|
Logger.debug('[parseOverdriveMediaMarkers] Removing any unnecessary chapters')
|
||||||
|
const weirdChapterFilterRegex = /([(]\d|[cC]ontinued)/
|
||||||
|
var cleaned = []
|
||||||
|
parsedOverdriveMediaMarkers.forEach(function (item) {
|
||||||
|
cleaned.push(item.filter(chapter => !weirdChapterFilterRegex.test(chapter.Name)))
|
||||||
|
})
|
||||||
|
|
||||||
|
return cleaned
|
||||||
|
}
|
||||||
|
|
||||||
|
// Given a set of chapters from generateParsedChapters, add the end time to each one
|
||||||
|
function addChapterEndTimes(chapters, totalAudioDuration) {
|
||||||
|
Logger.debug('[parseOverdriveMediaMarkers] Adding chapter end times')
|
||||||
|
chapters.forEach((chapter, chapter_index) => {
|
||||||
|
if (chapter_index < chapters.length - 1) {
|
||||||
|
chapter.end = chapters[chapter_index + 1].start
|
||||||
|
} else {
|
||||||
|
chapter.end = totalAudioDuration
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return chapters
|
||||||
|
}
|
||||||
|
|
||||||
|
// The function that actually generates the Chapters object that we update ABS with
|
||||||
|
function generateParsedChapters(includedAudioFiles, cleanedOverdriveMediaMarkers) {
|
||||||
|
Logger.debug('[parseOverdriveMediaMarkers] Generating new chapters for ABS')
|
||||||
|
// logic ported over from benonymity's OverdriveChapterizer:
|
||||||
|
// https://github.com/benonymity/OverdriveChapterizer/blob/main/chapters.py
|
||||||
|
var parsedChapters = []
|
||||||
|
var length = 0.0
|
||||||
|
var index = 0
|
||||||
|
var time = 0.0
|
||||||
|
|
||||||
|
// cleanedOverdriveMediaMarkers is an array of array of objects, where the inner array matches to the included audio files tracks
|
||||||
|
// this allows us to leverage the individual track durations when calculating the start times of chapters in tracks after the first (using length)
|
||||||
|
includedAudioFiles.forEach((track, track_index) => {
|
||||||
|
cleanedOverdriveMediaMarkers[track_index].forEach((chapter) => {
|
||||||
|
time = chapter.Time.split(":")
|
||||||
|
time = length + parseFloat(time[0]) * 60 + parseFloat(time[1])
|
||||||
|
var newChapterData = {
|
||||||
|
id: index++,
|
||||||
|
start: time,
|
||||||
|
title: chapter.Name
|
||||||
|
}
|
||||||
|
parsedChapters.push(newChapterData)
|
||||||
|
})
|
||||||
|
length += track.duration
|
||||||
|
})
|
||||||
|
|
||||||
|
parsedChapters = addChapterEndTimes(parsedChapters, length) // we need all the start times sorted out before we can add the end times
|
||||||
|
|
||||||
|
return parsedChapters
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports.overdriveMediaMarkersExist = (includedAudioFiles) => {
|
||||||
|
return extractOverdriveMediaMarkers(includedAudioFiles).length > 1
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports.parseOverdriveMediaMarkersAsChapters = (includedAudioFiles) => {
|
||||||
|
Logger.info('[parseOverdriveMediaMarkers] Parsing of Overdrive Media Markers started')
|
||||||
|
|
||||||
|
var overdriveMediaMarkers = extractOverdriveMediaMarkers(includedAudioFiles)
|
||||||
|
var cleanedOverdriveMediaMarkers = cleanOverdriveMediaMarkers(overdriveMediaMarkers)
|
||||||
|
var parsedChapters = generateParsedChapters(includedAudioFiles, cleanedOverdriveMediaMarkers)
|
||||||
|
|
||||||
|
return parsedChapters
|
||||||
|
}
|
@ -192,6 +192,7 @@ function parseTags(format, verbose) {
|
|||||||
file_tag_movement: tryGrabTags(format, 'movement', 'mvin'),
|
file_tag_movement: tryGrabTags(format, 'movement', 'mvin'),
|
||||||
file_tag_genre1: tryGrabTags(format, 'tmp_genre1', 'genre1'),
|
file_tag_genre1: tryGrabTags(format, 'tmp_genre1', 'genre1'),
|
||||||
file_tag_genre2: tryGrabTags(format, 'tmp_genre2', 'genre2'),
|
file_tag_genre2: tryGrabTags(format, 'tmp_genre2', 'genre2'),
|
||||||
|
file_tag_overdrive_media_marker: tryGrabTags(format, 'OverDrive MediaMarkers'),
|
||||||
}
|
}
|
||||||
for (const key in tags) {
|
for (const key in tags) {
|
||||||
if (!tags[key]) {
|
if (!tags[key]) {
|
||||||
|
Loading…
Reference in New Issue
Block a user