mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2025-12-09 13:29:37 +00:00
Laying the groundwork for music media type #964
This commit is contained in:
parent
c3717f6979
commit
b884f8fe11
18 changed files with 491 additions and 134 deletions
|
|
@ -57,7 +57,9 @@ class Library {
|
|||
else if (this.icon === 'comic') this.icon = 'file-picture'
|
||||
else this.icon = 'database'
|
||||
}
|
||||
if (!this.mediaType || (this.mediaType !== 'podcast' && this.mediaType !== 'book' && this.mediaType !== 'video')) {
|
||||
|
||||
const mediaTypes = ['podcast', 'book', 'video', 'music']
|
||||
if (!this.mediaType || !mediaTypes.includes(this.mediaType)) {
|
||||
this.mediaType = 'book'
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ const LibraryFile = require('./files/LibraryFile')
|
|||
const Book = require('./mediaTypes/Book')
|
||||
const Podcast = require('./mediaTypes/Podcast')
|
||||
const Video = require('./mediaTypes/Video')
|
||||
const Music = require('./mediaTypes/Music')
|
||||
const { areEquivalent, copyValue, getId, cleanStringForSearch } = require('../utils/index')
|
||||
|
||||
class LibraryItem {
|
||||
|
|
@ -72,6 +73,8 @@ class LibraryItem {
|
|||
this.media = new Podcast(libraryItem.media)
|
||||
} else if (this.mediaType === 'video') {
|
||||
this.media = new Video(libraryItem.media)
|
||||
} else if (this.mediaType === 'music') {
|
||||
this.media = new Music(libraryItem.media)
|
||||
}
|
||||
this.media.libraryItemId = this.id
|
||||
|
||||
|
|
@ -153,13 +156,14 @@ class LibraryItem {
|
|||
|
||||
get isPodcast() { return this.mediaType === 'podcast' }
|
||||
get isBook() { return this.mediaType === 'book' }
|
||||
get isMusic() { return this.mediaType === 'music' }
|
||||
get size() {
|
||||
var total = 0
|
||||
let total = 0
|
||||
this.libraryFiles.forEach((lf) => total += lf.metadata.size)
|
||||
return total
|
||||
}
|
||||
get audioFileTotalSize() {
|
||||
var total = 0
|
||||
let total = 0
|
||||
this.libraryFiles.filter(lf => lf.fileType == 'audio').forEach((lf) => total += lf.metadata.size)
|
||||
return total
|
||||
}
|
||||
|
|
@ -182,8 +186,10 @@ class LibraryItem {
|
|||
this.media = new Video()
|
||||
} else if (libraryMediaType === 'podcast') {
|
||||
this.media = new Podcast()
|
||||
} else {
|
||||
} else if (libraryMediaType === 'book') {
|
||||
this.media = new Book()
|
||||
} else if (libraryMediaType === 'music') {
|
||||
this.media = new Music()
|
||||
}
|
||||
this.media.libraryItemId = this.id
|
||||
|
||||
|
|
@ -348,11 +354,11 @@ class LibraryItem {
|
|||
}
|
||||
})
|
||||
|
||||
var newLibraryFiles = []
|
||||
var existingLibraryFiles = []
|
||||
const newLibraryFiles = []
|
||||
const existingLibraryFiles = []
|
||||
|
||||
dataFound.libraryFiles.forEach((lf) => {
|
||||
var fileFoundCheck = this.checkFileFound(lf, true)
|
||||
const fileFoundCheck = this.checkFileFound(lf, true)
|
||||
if (fileFoundCheck === null) {
|
||||
newLibraryFiles.push(lf)
|
||||
} else if (fileFoundCheck && lf.metadata.format !== 'abs') { // Ignore abs file updates
|
||||
|
|
@ -397,7 +403,7 @@ class LibraryItem {
|
|||
|
||||
// If cover path is in item folder, make sure libraryFile exists for it
|
||||
if (this.media.coverPath && this.media.coverPath.startsWith(this.path)) {
|
||||
var lf = this.libraryFiles.find(lf => lf.metadata.path === this.media.coverPath)
|
||||
const lf = this.libraryFiles.find(lf => lf.metadata.path === this.media.coverPath)
|
||||
if (!lf) {
|
||||
Logger.warn(`[LibraryItem] Invalid cover path - library file dne "${this.media.coverPath}"`)
|
||||
this.media.updateCover('')
|
||||
|
|
@ -419,7 +425,7 @@ class LibraryItem {
|
|||
|
||||
// Set metadata from files
|
||||
async syncFiles(preferOpfMetadata) {
|
||||
var hasUpdated = false
|
||||
let hasUpdated = false
|
||||
|
||||
if (this.mediaType === 'book') {
|
||||
// Add/update ebook file (ebooks that were removed are removed in checkScanData)
|
||||
|
|
@ -436,7 +442,7 @@ class LibraryItem {
|
|||
}
|
||||
|
||||
// Set cover image if not set
|
||||
var imageFiles = this.libraryFiles.filter(lf => lf.fileType === 'image')
|
||||
const imageFiles = this.libraryFiles.filter(lf => lf.fileType === 'image')
|
||||
if (imageFiles.length && !this.media.coverPath) {
|
||||
this.media.coverPath = imageFiles[0].metadata.path
|
||||
Logger.debug('[LibraryItem] Set media cover path', this.media.coverPath)
|
||||
|
|
@ -444,7 +450,7 @@ class LibraryItem {
|
|||
}
|
||||
|
||||
// Parse metadata files
|
||||
var textMetadataFiles = this.libraryFiles.filter(lf => lf.fileType === 'metadata' || lf.fileType === 'text')
|
||||
const textMetadataFiles = this.libraryFiles.filter(lf => lf.fileType === 'metadata' || lf.fileType === 'text')
|
||||
if (textMetadataFiles.length) {
|
||||
if (await this.media.syncMetadataFiles(textMetadataFiles, preferOpfMetadata)) {
|
||||
hasUpdated = true
|
||||
|
|
@ -468,12 +474,12 @@ class LibraryItem {
|
|||
|
||||
// Saves metadata.abs file
|
||||
async saveMetadata() {
|
||||
if (this.mediaType === 'video') return
|
||||
if (this.mediaType === 'video' || this.mediaType === 'music') return
|
||||
|
||||
if (this.isSavingMetadata) return
|
||||
this.isSavingMetadata = true
|
||||
|
||||
var metadataPath = Path.join(global.MetadataPath, 'items', this.id)
|
||||
let metadataPath = Path.join(global.MetadataPath, 'items', this.id)
|
||||
if (global.ServerSettings.storeMetadataWithItem && !this.isFile) {
|
||||
metadataPath = this.path
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -148,7 +148,7 @@ class PodcastEpisode {
|
|||
|
||||
// Only checks container format
|
||||
checkCanDirectPlay(payload) {
|
||||
var supportedMimeTypes = payload.supportedMimeTypes || []
|
||||
const supportedMimeTypes = payload.supportedMimeTypes || []
|
||||
return supportedMimeTypes.includes(this.audioFile.mimeType)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -118,9 +118,9 @@ class Book {
|
|||
return this.missingParts.length || this.invalidAudioFiles.length
|
||||
}
|
||||
get tracks() {
|
||||
var startOffset = 0
|
||||
let startOffset = 0
|
||||
return this.includedAudioFiles.map((af) => {
|
||||
var audioTrack = new AudioTrack()
|
||||
const audioTrack = new AudioTrack()
|
||||
audioTrack.setData(this.libraryItemId, af, startOffset)
|
||||
startOffset += audioTrack.duration
|
||||
return audioTrack
|
||||
|
|
|
|||
159
server/objects/mediaTypes/Music.js
Normal file
159
server/objects/mediaTypes/Music.js
Normal file
|
|
@ -0,0 +1,159 @@
|
|||
const Logger = require('../../Logger')
|
||||
const AudioFile = require('../files/AudioFile')
|
||||
const AudioTrack = require('../files/AudioTrack')
|
||||
const MusicMetadata = require('../metadata/MusicMetadata')
|
||||
const { areEquivalent, copyValue } = require('../../utils/index')
|
||||
|
||||
class Music {
|
||||
constructor(music) {
|
||||
this.libraryItemId = null
|
||||
this.metadata = null
|
||||
this.coverPath = null
|
||||
this.tags = []
|
||||
this.audioFile = null
|
||||
|
||||
if (music) {
|
||||
this.construct(music)
|
||||
}
|
||||
}
|
||||
|
||||
construct(music) {
|
||||
this.libraryItemId = music.libraryItemId
|
||||
this.metadata = new MusicMetadata(music.metadata)
|
||||
this.coverPath = music.coverPath
|
||||
this.tags = [...music.tags]
|
||||
this.audioFile = new AudioFile(music.audioFile)
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return {
|
||||
libraryItemId: this.libraryItemId,
|
||||
metadata: this.metadata.toJSON(),
|
||||
coverPath: this.coverPath,
|
||||
tags: [...this.tags],
|
||||
audioFile: this.audioFile.toJSON(),
|
||||
}
|
||||
}
|
||||
|
||||
toJSONMinified() {
|
||||
return {
|
||||
metadata: this.metadata.toJSONMinified(),
|
||||
coverPath: this.coverPath,
|
||||
tags: [...this.tags],
|
||||
audioFile: this.audioFile.toJSON(),
|
||||
size: this.size
|
||||
}
|
||||
}
|
||||
|
||||
toJSONExpanded() {
|
||||
return {
|
||||
libraryItemId: this.libraryItemId,
|
||||
metadata: this.metadata.toJSONExpanded(),
|
||||
coverPath: this.coverPath,
|
||||
tags: [...this.tags],
|
||||
audioFile: this.audioFile.toJSON(),
|
||||
size: this.size
|
||||
}
|
||||
}
|
||||
|
||||
get size() {
|
||||
return this.audioFile.metadata.size
|
||||
}
|
||||
get hasMediaEntities() {
|
||||
return !!this.audioFile
|
||||
}
|
||||
get shouldSearchForCover() {
|
||||
return false
|
||||
}
|
||||
get hasEmbeddedCoverArt() {
|
||||
return this.audioFile.embeddedCoverArt
|
||||
}
|
||||
get hasIssues() {
|
||||
return false
|
||||
}
|
||||
get duration() {
|
||||
return this.audioFile.duration || 0
|
||||
}
|
||||
get audioTrack() {
|
||||
const audioTrack = new AudioTrack()
|
||||
audioTrack.setData(this.libraryItemId, this.audioFile, 0)
|
||||
return audioTrack
|
||||
}
|
||||
get numTracks() {
|
||||
return 1
|
||||
}
|
||||
|
||||
update(payload) {
|
||||
const json = this.toJSON()
|
||||
delete json.episodes // do not update media entities here
|
||||
let hasUpdates = false
|
||||
for (const key in json) {
|
||||
if (payload[key] !== undefined) {
|
||||
if (key === 'metadata') {
|
||||
if (this.metadata.update(payload.metadata)) {
|
||||
hasUpdates = true
|
||||
}
|
||||
} else if (!areEquivalent(payload[key], json[key])) {
|
||||
this[key] = copyValue(payload[key])
|
||||
Logger.debug('[Podcast] Key updated', key, this[key])
|
||||
hasUpdates = true
|
||||
}
|
||||
}
|
||||
}
|
||||
return hasUpdates
|
||||
}
|
||||
|
||||
updateCover(coverPath) {
|
||||
coverPath = coverPath.replace(/\\/g, '/')
|
||||
if (this.coverPath === coverPath) return false
|
||||
this.coverPath = coverPath
|
||||
return true
|
||||
}
|
||||
|
||||
removeFileWithInode(inode) {
|
||||
return false
|
||||
}
|
||||
|
||||
findFileWithInode(inode) {
|
||||
return this.audioFile && this.audioFile.ino === inode
|
||||
}
|
||||
|
||||
setData(mediaData) {
|
||||
this.metadata = new MusicMetadata()
|
||||
if (mediaData.metadata) {
|
||||
this.metadata.setData(mediaData.metadata)
|
||||
}
|
||||
|
||||
this.coverPath = mediaData.coverPath || null
|
||||
}
|
||||
|
||||
setAudioFile(audioFile) {
|
||||
this.audioFile = audioFile
|
||||
}
|
||||
|
||||
syncMetadataFiles(textMetadataFiles, opfMetadataOverrideDetails) {
|
||||
return false
|
||||
}
|
||||
|
||||
searchQuery(query) {
|
||||
return {}
|
||||
}
|
||||
|
||||
// Only checks container format
|
||||
checkCanDirectPlay(payload) {
|
||||
return true
|
||||
}
|
||||
|
||||
getDirectPlayTracklist() {
|
||||
return [this.audioTrack]
|
||||
}
|
||||
|
||||
getPlaybackTitle() {
|
||||
return this.metadata.title
|
||||
}
|
||||
|
||||
getPlaybackAuthor() {
|
||||
return this.metadata.artist
|
||||
}
|
||||
}
|
||||
module.exports = Music
|
||||
104
server/objects/metadata/MusicMetadata.js
Normal file
104
server/objects/metadata/MusicMetadata.js
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
const Logger = require('../../Logger')
|
||||
const { areEquivalent, copyValue, cleanStringForSearch, getTitleIgnorePrefix, getTitlePrefixAtEnd } = require('../../utils/index')
|
||||
|
||||
class MusicMetadata {
|
||||
constructor(metadata) {
|
||||
this.title = null
|
||||
this.artist = null
|
||||
this.album = null
|
||||
this.genres = [] // Array of strings
|
||||
this.releaseDate = null
|
||||
this.language = null
|
||||
this.explicit = false
|
||||
|
||||
if (metadata) {
|
||||
this.construct(metadata)
|
||||
}
|
||||
}
|
||||
|
||||
construct(metadata) {
|
||||
this.title = metadata.title
|
||||
this.artist = metadata.artist
|
||||
this.album = metadata.album
|
||||
this.genres = metadata.genres ? [...metadata.genres] : []
|
||||
this.releaseDate = metadata.releaseDate || null
|
||||
this.language = metadata.language
|
||||
this.explicit = !!metadata.explicit
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return {
|
||||
title: this.title,
|
||||
artist: this.artist,
|
||||
album: this.album,
|
||||
genres: [...this.genres],
|
||||
releaseDate: this.releaseDate,
|
||||
language: this.language,
|
||||
explicit: this.explicit
|
||||
}
|
||||
}
|
||||
|
||||
toJSONMinified() {
|
||||
return {
|
||||
title: this.title,
|
||||
titleIgnorePrefix: this.titlePrefixAtEnd,
|
||||
artist: this.artist,
|
||||
album: this.album,
|
||||
genres: [...this.genres],
|
||||
releaseDate: this.releaseDate,
|
||||
language: this.language,
|
||||
explicit: this.explicit
|
||||
}
|
||||
}
|
||||
|
||||
toJSONExpanded() {
|
||||
return this.toJSONMinified()
|
||||
}
|
||||
|
||||
clone() {
|
||||
return new MusicMetadata(this.toJSON())
|
||||
}
|
||||
|
||||
get titleIgnorePrefix() {
|
||||
return getTitleIgnorePrefix(this.title)
|
||||
}
|
||||
|
||||
get titlePrefixAtEnd() {
|
||||
return getTitlePrefixAtEnd(this.title)
|
||||
}
|
||||
|
||||
searchQuery(query) { // Returns key if match is found
|
||||
const keysToCheck = ['title', 'artist', 'album']
|
||||
for (const key of keysToCheck) {
|
||||
if (this[key] && cleanStringForSearch(String(this[key])).includes(query)) {
|
||||
return {
|
||||
matchKey: key,
|
||||
matchText: this[key]
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
setData(mediaMetadata = {}) {
|
||||
this.title = mediaMetadata.title || null
|
||||
this.artist = mediaMetadata.artist || null
|
||||
this.album = mediaMetadata.album || null
|
||||
}
|
||||
|
||||
update(payload) {
|
||||
const json = this.toJSON()
|
||||
let hasUpdates = false
|
||||
for (const key in json) {
|
||||
if (payload[key] !== undefined) {
|
||||
if (!areEquivalent(payload[key], json[key])) {
|
||||
this[key] = copyValue(payload[key])
|
||||
Logger.debug('[MusicMetadata] Key updated', key, this[key])
|
||||
hasUpdates = true
|
||||
}
|
||||
}
|
||||
}
|
||||
return hasUpdates
|
||||
}
|
||||
}
|
||||
module.exports = MusicMetadata
|
||||
Loading…
Add table
Add a link
Reference in a new issue