New api routes, updating web client pages, audiobooks to libraryItem migration

This commit is contained in:
advplyr 2022-03-10 18:45:02 -06:00
parent b97ed953f7
commit 2a30cc428f
51 changed files with 1225 additions and 654 deletions

View file

@ -1,129 +0,0 @@
class AudioFileMetadata {
constructor(metadata) {
this.tagAlbum = null
this.tagArtist = null
this.tagGenre = null
this.tagTitle = null
this.tagSeries = null
this.tagSeriesPart = null
this.tagTrack = null
this.tagDisc = null
this.tagSubtitle = null
this.tagAlbumArtist = null
this.tagDate = null
this.tagComposer = null
this.tagPublisher = null
this.tagComment = null
this.tagDescription = null
this.tagEncoder = null
this.tagEncodedBy = null
this.tagIsbn = null
this.tagLanguage = null
this.tagASIN = null
if (metadata) {
this.construct(metadata)
}
}
toJSON() {
// Only return the tags that are actually set
var json = {}
for (const key in this) {
if (key.startsWith('tag') && this[key]) {
json[key] = this[key]
}
}
return json
}
construct(metadata) {
this.tagAlbum = metadata.tagAlbum || null
this.tagArtist = metadata.tagArtist || null
this.tagGenre = metadata.tagGenre || null
this.tagTitle = metadata.tagTitle || null
this.tagSeries = metadata.tagSeries || null
this.tagSeriesPart = metadata.tagSeriesPart || null
this.tagTrack = metadata.tagTrack || null
this.tagDisc = metadata.tagDisc || null
this.tagSubtitle = metadata.tagSubtitle || null
this.tagAlbumArtist = metadata.tagAlbumArtist || null
this.tagDate = metadata.tagDate || null
this.tagComposer = metadata.tagComposer || null
this.tagPublisher = metadata.tagPublisher || null
this.tagComment = metadata.tagComment || null
this.tagDescription = metadata.tagDescription || null
this.tagEncoder = metadata.tagEncoder || null
this.tagEncodedBy = metadata.tagEncodedBy || null
this.tagIsbn = metadata.tagIsbn || null
this.tagLanguage = metadata.tagLanguage || null
this.tagASIN = metadata.tagASIN || null
}
// Data parsed in prober.js
setData(payload) {
this.tagAlbum = payload.file_tag_album || null
this.tagArtist = payload.file_tag_artist || null
this.tagGenre = payload.file_tag_genre || null
this.tagTitle = payload.file_tag_title || null
this.tagSeries = payload.file_tag_series || null
this.tagSeriesPart = payload.file_tag_seriespart || null
this.tagTrack = payload.file_tag_track || null
this.tagDisc = payload.file_tag_disc || null
this.tagSubtitle = payload.file_tag_subtitle || null
this.tagAlbumArtist = payload.file_tag_albumartist || null
this.tagDate = payload.file_tag_date || null
this.tagComposer = payload.file_tag_composer || null
this.tagPublisher = payload.file_tag_publisher || null
this.tagComment = payload.file_tag_comment || null
this.tagDescription = payload.file_tag_description || null
this.tagEncoder = payload.file_tag_encoder || null
this.tagEncodedBy = payload.file_tag_encodedby || null
this.tagIsbn = payload.file_tag_isbn || null
this.tagLanguage = payload.file_tag_language || null
this.tagASIN = payload.file_tag_asin || null
}
updateData(payload) {
const dataMap = {
tagAlbum: payload.file_tag_album || null,
tagArtist: payload.file_tag_artist || null,
tagGenre: payload.file_tag_genre || null,
tagTitle: payload.file_tag_title || null,
tagSeries: payload.file_tag_series || null,
tagSeriesPart: payload.file_tag_seriespart || null,
tagTrack: payload.file_tag_track || null,
tagDisc: payload.file_tag_disc || null,
tagSubtitle: payload.file_tag_subtitle || null,
tagAlbumArtist: payload.file_tag_albumartist || null,
tagDate: payload.file_tag_date || null,
tagComposer: payload.file_tag_composer || null,
tagPublisher: payload.file_tag_publisher || null,
tagComment: payload.file_tag_comment || null,
tagDescription: payload.file_tag_description || null,
tagEncoder: payload.file_tag_encoder || null,
tagEncodedBy: payload.file_tag_encodedby || null,
tagIsbn: payload.file_tag_isbn || null,
tagLanguage: payload.file_tag_language || null,
tagASIN: payload.file_tag_asin || null
}
var hasUpdates = false
for (const key in dataMap) {
if (dataMap[key] !== this[key]) {
this[key] = dataMap[key]
hasUpdates = true
}
}
return hasUpdates
}
isEqual(audioFileMetadata) {
if (!audioFileMetadata || !audioFileMetadata.toJSON) return false
for (const key in audioFileMetadata.toJSON()) {
if (audioFileMetadata[key] !== this[key]) return false
}
return true
}
}
module.exports = AudioFileMetadata

View file

@ -17,15 +17,15 @@ class LibraryItem {
this.ctimeMs = null
this.birthtimeMs = null
this.addedAt = null
this.lastUpdate = null
this.updatedAt = null
this.lastScan = null
this.scanVersion = null
// Entity was scanned and not found
this.isMissing = false
this.entityType = null
this.entity = null
this.mediaType = null
this.media = null
this.libraryFiles = []
@ -45,17 +45,17 @@ class LibraryItem {
this.ctimeMs = libraryItem.ctimeMs || 0
this.birthtimeMs = libraryItem.birthtimeMs || 0
this.addedAt = libraryItem.addedAt
this.lastUpdate = libraryItem.lastUpdate || this.addedAt
this.updatedAt = libraryItem.updatedAt || this.addedAt
this.lastScan = libraryItem.lastScan || null
this.scanVersion = libraryItem.scanVersion || null
this.isMissing = !!libraryItem.isMissing
this.entityType = libraryItem.entityType
if (this.entityType === 'book') {
this.entity = new Book(libraryItem.entity)
} else if (this.entityType === 'podcast') {
this.entity = new Podcast(libraryItem.entity)
this.mediaType = libraryItem.mediaType
if (this.mediaType === 'book') {
this.media = new Book(libraryItem.media)
} else if (this.mediaType === 'podcast') {
this.media = new Podcast(libraryItem.media)
}
this.libraryFiles = libraryItem.libraryFiles.map(f => new LibraryFile(f))
@ -73,14 +73,64 @@ class LibraryItem {
ctimeMs: this.ctimeMs,
birthtimeMs: this.birthtimeMs,
addedAt: this.addedAt,
lastUpdate: this.lastUpdate,
updatedAt: this.updatedAt,
lastScan: this.lastScan,
scanVersion: this.scanVersion,
isMissing: !!this.isMissing,
entityType: this.entityType,
entity: this.entity.toJSON(),
mediaType: this.mediaType,
media: this.media.toJSON(),
libraryFiles: this.libraryFiles.map(f => f.toJSON())
}
}
toJSONMinified() {
return {
id: this.id,
ino: this.ino,
libraryId: this.libraryId,
folderId: this.folderId,
path: this.path,
relPath: this.relPath,
mtimeMs: this.mtimeMs,
ctimeMs: this.ctimeMs,
birthtimeMs: this.birthtimeMs,
addedAt: this.addedAt,
updatedAt: this.updatedAt,
isMissing: !!this.isMissing,
mediaType: this.mediaType,
media: this.media.toJSONMinified(),
numFiles: this.libraryFiles.length
}
}
// Adds additional helpful fields like media duration, tracks, etc.
toJSONExpanded() {
return {
id: this.id,
ino: this.ino,
libraryId: this.libraryId,
folderId: this.folderId,
path: this.path,
relPath: this.relPath,
mtimeMs: this.mtimeMs,
ctimeMs: this.ctimeMs,
birthtimeMs: this.birthtimeMs,
addedAt: this.addedAt,
updatedAt: this.updatedAt,
lastScan: this.lastScan,
scanVersion: this.scanVersion,
isMissing: !!this.isMissing,
mediaType: this.mediaType,
media: this.media.toJSONExpanded(),
libraryFiles: this.libraryFiles.map(f => f.toJSON()),
size: this.size
}
}
get size() {
var total = 0
this.libraryFiles.forEach((lf) => total += lf.metadata.size)
return total
}
}
module.exports = LibraryItem

View file

@ -7,7 +7,6 @@ class Book {
this.metadata = null
this.coverPath = null
this.relCoverPath = null
this.tags = []
this.audioFiles = []
this.ebookFiles = []
@ -21,7 +20,6 @@ class Book {
construct(book) {
this.metadata = new BookMetadata(book.metadata)
this.coverPath = book.coverPath
this.relCoverPath = book.relCoverPath
this.tags = [...book.tags]
this.audioFiles = book.audioFiles.map(f => new AudioFile(f))
this.ebookFiles = book.ebookFiles.map(f => new EBookFile(f))
@ -32,12 +30,53 @@ class Book {
return {
metadata: this.metadata.toJSON(),
coverPath: this.coverPath,
relCoverPath: this.relCoverPath,
tags: [...this.tags],
audioFiles: this.audioFiles.map(f => f.toJSON()),
ebookFiles: this.ebookFiles.map(f => f.toJSON()),
chapters: this.chapters.map(c => ({ ...c }))
}
}
toJSONMinified() {
return {
metadata: this.metadata.toJSON(),
coverPath: this.coverPath,
tags: [...this.tags],
numTracks: this.tracks.length,
numAudioFiles: this.audioFiles.length,
numEbooks: this.ebookFiles.length,
numChapters: this.chapters.length,
duration: this.duration,
size: this.size
}
}
toJSONExpanded() {
return {
metadata: this.metadata.toJSONExpanded(),
coverPath: this.coverPath,
tags: [...this.tags],
audioFiles: this.audioFiles.map(f => f.toJSON()),
ebookFiles: this.ebookFiles.map(f => f.toJSON()),
chapters: this.chapters.map(c => ({ ...c })),
duration: this.duration,
size: this.size,
tracks: this.tracks.map(t => t.toJSON())
}
}
get tracks() {
return this.audioFiles.filter(af => !af.exclude && !af.invalid)
}
get duration() {
var total = 0
this.tracks.forEach((track) => total += track.duration)
return total
}
get size() {
var total = 0
this.audioFiles.forEach((af) => total += af.metadata.size)
return total
}
}
module.exports = Book

View file

@ -6,8 +6,8 @@ class Podcast {
this.id = null
this.metadata = null
this.cover = null
this.coverFullPath = null
this.coverPath = null
this.tags = []
this.episodes = []
this.createdAt = null
@ -21,8 +21,8 @@ class Podcast {
construct(podcast) {
this.id = podcast.id
this.metadata = new PodcastMetadata(podcast.metadata)
this.cover = podcast.cover
this.coverFullPath = podcast.coverFullPath
this.coverPath = podcast.coverPath
this.tags = [...podcast.tags]
this.episodes = podcast.episodes.map((e) => new PodcastEpisode(e))
this.createdAt = podcast.createdAt
this.lastUpdate = podcast.lastUpdate
@ -32,8 +32,32 @@ class Podcast {
return {
id: this.id,
metadata: this.metadata.toJSON(),
cover: this.cover,
coverFullPath: this.coverFullPath,
coverPath: this.coverPath,
tags: [...this.tags],
episodes: this.episodes.map(e => e.toJSON()),
createdAt: this.createdAt,
lastUpdate: this.lastUpdate
}
}
toJSONMinified() {
return {
id: this.id,
metadata: this.metadata.toJSON(),
coverPath: this.coverPath,
tags: [...this.tags],
episodes: this.episodes.map(e => e.toJSON()),
createdAt: this.createdAt,
lastUpdate: this.lastUpdate
}
}
toJSONExpanded() {
return {
id: this.id,
metadata: this.metadata.toJSONExpanded(),
coverPath: this.coverPath,
tags: [...this.tags],
episodes: this.episodes.map(e => e.toJSON()),
createdAt: this.createdAt,
lastUpdate: this.lastUpdate

View file

@ -1,3 +1,4 @@
const globals = require('../../utils/globals')
const FileMetadata = require('../metadata/FileMetadata')
class LibraryFile {
@ -24,8 +25,18 @@ class LibraryFile {
ino: this.ino,
metadata: this.metadata.toJSON(),
addedAt: this.addedAt,
updatedAt: this.updatedAt
updatedAt: this.updatedAt,
fileType: this.fileType
}
}
get fileType() {
if (globals.SupportedImageTypes.includes(this.metadata.format)) return 'image'
if (globals.SupportedAudioTypes.includes(this.metadata.format)) return 'audio'
if (globals.SupportedEbookTypes.includes(this.metadata.format)) return 'ebook'
if (globals.TextFileTypes.includes(this.metadata.format)) return 'text'
if (globals.MetadataFileTypes.includes(this.metadata.format)) return 'metadata'
return 'unknown'
}
}
module.exports = LibraryFile

View file

@ -1,5 +1,5 @@
const { isNullOrNaN } = require('../utils/index')
const AudioFileMetadata = require('./metadata/AudioMetaTags')
const { isNullOrNaN } = require('../../utils/index')
const AudioFileMetadata = require('../metadata/AudioMetaTags')
class AudioFile {
constructor(data) {

View file

@ -1,4 +1,4 @@
var { bytesPretty } = require('../utils/fileUtils')
var { bytesPretty } = require('../../utils/fileUtils')
class AudioTrack {
constructor(audioTrack = null) {

View file

@ -1,12 +1,12 @@
const Path = require('path')
const fs = require('fs-extra')
const { bytesPretty, readTextFile, getIno } = require('../utils/fileUtils')
const { comparePaths, getId, elapsedPretty } = require('../utils/index')
const { parseOpfMetadataXML } = require('../utils/parseOpfMetadata')
const { extractCoverArt } = require('../utils/ffmpegHelpers')
const nfoGenerator = require('../utils/nfoGenerator')
const abmetadataGenerator = require('../utils/abmetadataGenerator')
const Logger = require('../Logger')
const { bytesPretty, readTextFile, getIno } = require('../../utils/fileUtils')
const { comparePaths, getId, elapsedPretty } = require('../../utils/index')
const { parseOpfMetadataXML } = require('../../utils/parseOpfMetadata')
const { extractCoverArt } = require('../../utils/ffmpegHelpers')
const nfoGenerator = require('../../utils/nfoGenerator')
const abmetadataGenerator = require('../../utils/abmetadataGenerator')
const Logger = require('../../Logger')
const Book = require('./Book')
const AudioTrack = require('./AudioTrack')
const AudioFile = require('./AudioFile')

View file

@ -1,5 +1,5 @@
const { getId } = require('../utils/index')
const Logger = require('../Logger')
const { getId } = require('../../utils/index')
const Logger = require('../../Logger')
class Author {
constructor(author = null) {

View file

@ -1,6 +1,6 @@
const Path = require('path')
const Logger = require('../Logger')
const parseAuthors = require('../utils/parseAuthors')
const Logger = require('../../Logger')
const parseAuthors = require('../../utils/parseAuthors')
class Book {
constructor(book = null) {

View file

@ -13,6 +13,7 @@ class BookMetadata {
this.isbn = null
this.asin = null
this.language = null
this.explicit = false
if (metadata) {
this.construct(metadata)
@ -33,6 +34,7 @@ class BookMetadata {
this.isbn = metadata.isbn
this.asin = metadata.asin
this.language = metadata.language
this.explicit = metadata.explicit
}
toJSON() {
@ -49,8 +51,54 @@ class BookMetadata {
description: this.description,
isbn: this.isbn,
asin: this.asin,
language: this.language
language: this.language,
explicit: this.explicit
}
}
toJSONExpanded() {
return {
title: this.title,
subtitle: this.subtitle,
authors: this.authors.map(a => ({ ...a })), // Author JSONMinimal with name and id
narrators: [...this.narrators],
series: this.series.map(s => ({ ...s })),
genres: [...this.genres],
publishedYear: this.publishedYear,
publishedDate: this.publishedDate,
publisher: this.publisher,
description: this.description,
isbn: this.isbn,
asin: this.asin,
language: this.language,
explicit: this.explicit,
authorName: this.authorName,
narratorName: this.narratorName
}
}
get titleIgnorePrefix() {
if (!this.title) return ''
if (this.title.toLowerCase().startsWith('the ')) {
return this.title.substr(4) + ', The'
}
return this.title
}
get authorName() {
return this.authors.map(au => au.name).join(', ')
}
get narratorName() {
return this.narrators.join(', ')
}
hasAuthor(authorName) {
return !!this.authors.find(au => au.name == authorName)
}
hasSeries(seriesName) {
return !!this.series.find(se => se.name == seriesName)
}
hasNarrator(narratorName) {
return this.narrators.includes(narratorName)
}
}
module.exports = BookMetadata

View file

@ -41,5 +41,10 @@ class FileMetadata {
clone() {
return new FileMetadata(this.toJSON())
}
get format() {
if (!this.ext) return ''
return this.ext.slice(1)
}
}
module.exports = FileMetadata

View file

@ -9,6 +9,7 @@ class PodcastMetadata {
this.itunesPageUrl = null
this.itunesId = null
this.itunesArtistId = null
this.explicit = false
if (metadata) {
this.construct(metadata)
@ -25,6 +26,7 @@ class PodcastMetadata {
this.itunesPageUrl = metadata.itunesPageUrl
this.itunesId = metadata.itunesId
this.itunesArtistId = metadata.itunesArtistId
this.explicit = metadata.explicit
}
toJSON() {
@ -38,7 +40,12 @@ class PodcastMetadata {
itunesPageUrl: this.itunesPageUrl,
itunesId: this.itunesId,
itunesArtistId: this.itunesArtistId,
explicit: this.explicit
}
}
toJSONExpanded() {
return this.toJSON()
}
}
module.exports = PodcastMetadata