Merge remote-tracking branch 'origin/master' into auth_passportjs

This commit is contained in:
lukeIam 2023-03-24 18:23:08 +01:00
commit be53b31712
100 changed files with 2799 additions and 1217 deletions

View file

@ -70,17 +70,19 @@ class Feed {
id: this.id,
entityType: this.entityType,
entityId: this.entityId,
feedUrl: this.feedUrl
feedUrl: this.feedUrl,
meta: this.meta.toJSONMinified(),
}
}
getEpisodePath(id) {
var episode = this.episodes.find(ep => ep.id === id)
console.log('getEpisodePath=', id, episode)
if (!episode) return null
return episode.fullPath
}
setFromItem(userId, slug, libraryItem, serverAddress) {
setFromItem(userId, slug, libraryItem, serverAddress, preventIndexing = true, ownerName = null, ownerEmail = null) {
const media = libraryItem.media
const mediaMetadata = media.metadata
const isPodcast = libraryItem.mediaType === 'podcast'
@ -106,6 +108,11 @@ class Feed {
this.meta.feedUrl = feedUrl
this.meta.link = `${serverAddress}/item/${libraryItem.id}`
this.meta.explicit = !!mediaMetadata.explicit
this.meta.type = mediaMetadata.type
this.meta.language = mediaMetadata.language
this.meta.preventIndexing = preventIndexing
this.meta.ownerName = ownerName
this.meta.ownerEmail = ownerEmail
this.episodes = []
if (isPodcast) { // PODCAST EPISODES
@ -142,6 +149,8 @@ class Feed {
this.meta.author = author
this.meta.imageUrl = media.coverPath ? `${this.serverAddress}/feed/${this.slug}/cover` : `${this.serverAddress}/Logo.png`
this.meta.explicit = !!mediaMetadata.explicit
this.meta.type = mediaMetadata.type
this.meta.language = mediaMetadata.language
this.episodes = []
if (isPodcast) { // PODCAST EPISODES
@ -333,4 +342,4 @@ class Feed {
return author
}
}
module.exports = Feed
module.exports = Feed

View file

@ -14,6 +14,9 @@ class FeedEpisode {
this.author = null
this.explicit = null
this.duration = null
this.season = null
this.episode = null
this.episodeType = null
this.libraryItemId = null
this.episodeId = null
@ -35,6 +38,9 @@ class FeedEpisode {
this.author = episode.author
this.explicit = episode.explicit
this.duration = episode.duration
this.season = episode.season
this.episode = episode.episode
this.episodeType = episode.episodeType
this.libraryItemId = episode.libraryItemId
this.episodeId = episode.episodeId || null
this.trackIndex = episode.trackIndex || 0
@ -52,6 +58,9 @@ class FeedEpisode {
author: this.author,
explicit: this.explicit,
duration: this.duration,
season: this.season,
episode: this.episode,
episodeType: this.episodeType,
libraryItemId: this.libraryItemId,
episodeId: this.episodeId,
trackIndex: this.trackIndex,
@ -77,25 +86,31 @@ class FeedEpisode {
this.author = meta.author
this.explicit = mediaMetadata.explicit
this.duration = episode.duration
this.season = episode.season
this.episode = episode.episode
this.episodeType = episode.episodeType
this.libraryItemId = libraryItem.id
this.episodeId = episode.id
this.trackIndex = 0
this.fullPath = episode.audioFile.metadata.path
}
setFromAudiobookTrack(libraryItem, serverAddress, slug, audioTrack, meta, additionalOffset = 0) {
setFromAudiobookTrack(libraryItem, serverAddress, slug, audioTrack, meta, additionalOffset = null) {
// Example: <pubDate>Fri, 04 Feb 2015 00:00:00 GMT</pubDate>
let timeOffset = isNaN(audioTrack.index) ? 0 : (Number(audioTrack.index) * 1000) // Offset pubdate to ensure correct order
let episodeId = String(audioTrack.index)
// Additional offset can be used for collections/series
if (additionalOffset && !isNaN(additionalOffset)) {
if (additionalOffset !== null && !isNaN(additionalOffset)) {
timeOffset += Number(additionalOffset) * 1000
episodeId = String(additionalOffset) + '-' + episodeId
}
// e.g. Track 1 will have a pub date before Track 2
const audiobookPubDate = date.format(new Date(libraryItem.addedAt + timeOffset), 'ddd, DD MMM YYYY HH:mm:ss [GMT]')
const contentUrl = `/feed/${slug}/item/${audioTrack.index}/${audioTrack.metadata.filename}`
const contentUrl = `/feed/${slug}/item/${episodeId}/${audioTrack.metadata.filename}`
const media = libraryItem.media
const mediaMetadata = media.metadata
@ -110,7 +125,7 @@ class FeedEpisode {
}
}
this.id = String(audioTrack.index)
this.id = episodeId
this.title = title
this.description = mediaMetadata.description || ''
this.enclosure = {
@ -144,9 +159,12 @@ class FeedEpisode {
{ 'itunes:summary': this.description || '' },
{
"itunes:explicit": !!this.explicit
}
},
{ "itunes:episodeType": this.episodeType },
{ "itunes:season": this.season },
{ "itunes:episode": this.episode }
]
}
}
}
module.exports = FeedEpisode
module.exports = FeedEpisode

View file

@ -7,6 +7,11 @@ class FeedMeta {
this.feedUrl = null
this.link = null
this.explicit = null
this.type = null
this.language = null
this.preventIndexing = null
this.ownerName = null
this.ownerEmail = null
if (meta) {
this.construct(meta)
@ -21,6 +26,11 @@ class FeedMeta {
this.feedUrl = meta.feedUrl
this.link = meta.link
this.explicit = meta.explicit
this.type = meta.type
this.language = meta.language
this.preventIndexing = meta.preventIndexing
this.ownerName = meta.ownerName
this.ownerEmail = meta.ownerEmail
}
toJSON() {
@ -31,7 +41,22 @@ class FeedMeta {
imageUrl: this.imageUrl,
feedUrl: this.feedUrl,
link: this.link,
explicit: this.explicit
explicit: this.explicit,
type: this.type,
language: this.language,
preventIndexing: this.preventIndexing,
ownerName: this.ownerName,
ownerEmail: this.ownerEmail
}
}
toJSONMinified() {
return {
title: this.title,
description: this.description,
preventIndexing: this.preventIndexing,
ownerName: this.ownerName,
ownerEmail: this.ownerEmail
}
}
@ -43,16 +68,18 @@ class FeedMeta {
feed_url: this.feedUrl,
site_url: this.link,
image_url: this.imageUrl,
language: 'en',
custom_namespaces: {
'itunes': 'http://www.itunes.com/dtds/podcast-1.0.dtd',
'psc': 'http://podlove.org/simple-chapters',
'podcast': 'https://podcastindex.org/namespace/1.0'
'podcast': 'https://podcastindex.org/namespace/1.0',
'googleplay': 'http://www.google.com/schemas/play-podcasts/1.0'
},
custom_elements: [
{ 'language': this.language || 'en' },
{ 'author': this.author || 'advplyr' },
{ 'itunes:author': this.author || 'advplyr' },
{ 'itunes:summary': this.description || '' },
{ 'itunes:type': this.type },
{
'itunes:image': {
_attr: {
@ -62,15 +89,15 @@ class FeedMeta {
},
{
'itunes:owner': [
{ 'itunes:name': this.author || '' },
{ 'itunes:email': '' }
{ 'itunes:name': this.ownerName || this.author || '' },
{ 'itunes:email': this.ownerEmail || '' }
]
},
{
"itunes:explicit": !!this.explicit
}
{ 'itunes:explicit': !!this.explicit },
{ 'itunes:block': this.preventIndexing?"Yes":"No" },
{ 'googleplay:block': this.preventIndexing?"yes":"no" }
]
}
}
}
module.exports = FeedMeta
module.exports = FeedMeta

View file

@ -197,9 +197,15 @@ class LibraryItem {
if (key === 'libraryFiles') {
this.libraryFiles = payload.libraryFiles.map(lf => lf.clone())
// Use first image library file as cover
const firstImageFile = this.libraryFiles.find(lf => lf.fileType === 'image')
if (firstImageFile) this.media.coverPath = firstImageFile.metadata.path
// Set cover image
const imageFiles = this.libraryFiles.filter(lf => lf.fileType === 'image')
const coverMatch = imageFiles.find(iFile => /\/cover\.[^.\/]*$/.test(iFile.metadata.path))
if (coverMatch) {
this.media.coverPath = coverMatch.metadata.path
} else if (imageFiles.length) {
this.media.coverPath = imageFiles[0].metadata.path
}
} else if (this[key] !== undefined && key !== 'media') {
this[key] = payload[key]
}
@ -330,6 +336,7 @@ class LibraryItem {
}
if (dataFound.ino !== this.ino) {
Logger.warn(`[LibraryItem] Check scan item changed inode "${this.ino}" -> "${dataFound.ino}"`)
this.ino = dataFound.ino
hasUpdated = true
}
@ -341,7 +348,7 @@ class LibraryItem {
}
if (dataFound.path !== this.path) {
Logger.warn(`[LibraryItem] Check scan item changed path "${this.path}" -> "${dataFound.path}"`)
Logger.warn(`[LibraryItem] Check scan item changed path "${this.path}" -> "${dataFound.path}" (inode ${this.ino})`)
this.path = dataFound.path
this.relPath = dataFound.relPath
hasUpdated = true
@ -444,8 +451,14 @@ class LibraryItem {
// Set cover image if not set
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)
// attempt to find a file called cover.<ext> otherwise just fall back to the first image found
const coverMatch = imageFiles.find(iFile => /\/cover\.[^.\/]*$/.test(iFile.metadata.path))
if (coverMatch) {
this.media.coverPath = coverMatch.metadata.path
} else {
this.media.coverPath = imageFiles[0].metadata.path
}
Logger.info('[LibraryItem] Set media cover path', this.media.coverPath)
hasUpdated = true
}

View file

@ -1,6 +1,7 @@
const Path = require('path')
const { getId } = require('../utils/index')
const { sanitizeFilename } = require('../utils/fileUtils')
const globals = require('../utils/globals')
class PodcastEpisodeDownload {
constructor() {
@ -8,9 +9,9 @@ class PodcastEpisodeDownload {
this.podcastEpisode = null
this.url = null
this.libraryItem = null
this.libraryId = null
this.isAutoDownload = false
this.isDownloading = false
this.isFinished = false
this.failed = false
@ -22,20 +23,32 @@ class PodcastEpisodeDownload {
toJSONForClient() {
return {
id: this.id,
episodeDisplayTitle: this.podcastEpisode ? this.podcastEpisode.title : null,
episodeDisplayTitle: this.podcastEpisode?.title ?? null,
url: this.url,
libraryItemId: this.libraryItem ? this.libraryItem.id : null,
isDownloading: this.isDownloading,
libraryItemId: this.libraryItem?.id || null,
libraryId: this.libraryId || null,
isFinished: this.isFinished,
failed: this.failed,
startedAt: this.startedAt,
createdAt: this.createdAt,
finishedAt: this.finishedAt
finishedAt: this.finishedAt,
podcastTitle: this.libraryItem?.media.metadata.title ?? null,
podcastExplicit: !!this.libraryItem?.media.metadata.explicit,
season: this.podcastEpisode?.season ?? null,
episode: this.podcastEpisode?.episode ?? null,
episodeType: this.podcastEpisode?.episodeType ?? 'full',
publishedAt: this.podcastEpisode?.publishedAt ?? null
}
}
get fileExtension() {
const extname = Path.extname(this.url).substring(1).toLowerCase()
if (globals.SupportedAudioTypes.includes(extname)) return extname
return 'mp3'
}
get targetFilename() {
return sanitizeFilename(`${this.podcastEpisode.title}.mp3`)
return sanitizeFilename(`${this.podcastEpisode.title}.${this.fileExtension}`)
}
get targetPath() {
return Path.join(this.libraryItem.path, this.targetFilename)
@ -47,13 +60,21 @@ class PodcastEpisodeDownload {
return this.libraryItem ? this.libraryItem.id : null
}
setData(podcastEpisode, libraryItem, isAutoDownload) {
setData(podcastEpisode, libraryItem, isAutoDownload, libraryId) {
this.id = getId('epdl')
this.podcastEpisode = podcastEpisode
this.url = podcastEpisode.enclosure.url
const url = podcastEpisode.enclosure.url
if (decodeURIComponent(url) !== url) { // Already encoded
this.url = url
} else {
this.url = encodeURI(url)
}
this.libraryItem = libraryItem
this.isAutoDownload = isAutoDownload
this.createdAt = Date.now()
this.libraryId = libraryId
}
setFinished(success) {
@ -62,4 +83,4 @@ class PodcastEpisodeDownload {
this.failed = !success
}
}
module.exports = PodcastEpisodeDownload
module.exports = PodcastEpisodeDownload

View file

@ -82,7 +82,8 @@ class Stream extends EventEmitter {
AudioMimeType.WMA,
AudioMimeType.AIFF,
AudioMimeType.WEBM,
AudioMimeType.WEBMA
AudioMimeType.WEBMA,
AudioMimeType.AWB
]
}
get codecsToForceAAC() {

View file

@ -117,7 +117,7 @@ class PodcastEpisode {
this.enclosure = data.enclosure ? { ...data.enclosure } : null
this.season = data.season || ''
this.episode = data.episode || ''
this.episodeType = data.episodeType || ''
this.episodeType = data.episodeType || 'full'
this.publishedAt = data.publishedAt || 0
this.addedAt = Date.now()
this.updatedAt = Date.now()
@ -165,4 +165,4 @@ class PodcastEpisode {
return cleanStringForSearch(this.title).includes(query)
}
}
module.exports = PodcastEpisode
module.exports = PodcastEpisode

View file

@ -296,7 +296,7 @@ class Book {
})
}
} else if (key === 'narrators') {
if (opfMetadata.narrators && opfMetadata.narrators.length && (!this.metadata.narrators.length || opfMetadataOverrideDetails)) {
if (opfMetadata.narrators?.length && (!this.metadata.narrators.length || opfMetadataOverrideDetails)) {
metadataUpdatePayload.narrators = opfMetadata.narrators
}
} else if (key === 'series') {
@ -356,9 +356,9 @@ class Book {
}
updateAudioTracks(orderedFileData) {
var index = 1
let index = 1
this.audioFiles = orderedFileData.map((fileData) => {
var audioFile = this.audioFiles.find(af => af.ino === fileData.ino)
const audioFile = this.audioFiles.find(af => af.ino === fileData.ino)
audioFile.manuallyVerified = true
audioFile.invalid = false
audioFile.error = null
@ -376,11 +376,11 @@ class Book {
this.rebuildTracks()
}
rebuildTracks(preferOverdriveMediaMarker) {
rebuildTracks() {
Logger.debug(`[Book] Tracks being rebuilt...!`)
this.audioFiles.sort((a, b) => a.index - b.index)
this.missingParts = []
this.setChapters(preferOverdriveMediaMarker)
this.setChapters()
this.checkUpdateMissingTracks()
}
@ -412,14 +412,16 @@ class Book {
return wasUpdated
}
setChapters(preferOverdriveMediaMarker = false) {
setChapters() {
const preferOverdriveMediaMarker = !!global.ServerSettings.scannerPreferOverdriveMediaMarker
// If 1 audio file without chapters, then no chapters will be set
var includedAudioFiles = this.audioFiles.filter(af => !af.exclude)
const includedAudioFiles = this.audioFiles.filter(af => !af.exclude)
if (!includedAudioFiles.length) return
// If overdrive media markers are present and preferred, use those instead
if (preferOverdriveMediaMarker) {
var overdriveChapters = parseOverdriveMediaMarkersAsChapters(includedAudioFiles)
const overdriveChapters = parseOverdriveMediaMarkersAsChapters(includedAudioFiles)
if (overdriveChapters) {
Logger.info('[Book] Overdrive Media Markers and preference found! Using these for chapter definitions')
this.chapters = overdriveChapters
@ -460,17 +462,26 @@ class Book {
})
}
} else if (includedAudioFiles.length > 1) {
const preferAudioMetadata = !!global.ServerSettings.scannerPreferAudioMetadata
// Build chapters from audio files
this.chapters = []
var currChapterId = 0
var currStartTime = 0
let currChapterId = 0
let currStartTime = 0
includedAudioFiles.forEach((file) => {
if (file.duration) {
let title = file.metadata.filename ? Path.basename(file.metadata.filename, Path.extname(file.metadata.filename)) : `Chapter ${currChapterId}`
// When prefer audio metadata server setting is set then use ID3 title tag as long as it is not the same as the book title
if (preferAudioMetadata && file.metaTags?.tagTitle && file.metaTags?.tagTitle !== this.metadata.title) {
title = file.metaTags.tagTitle
}
this.chapters.push({
id: currChapterId++,
start: currStartTime,
end: currStartTime + file.duration,
title: file.metadata.filename ? Path.basename(file.metadata.filename, Path.extname(file.metadata.filename)) : `Chapter ${currChapterId}`
title
})
currStartTime += file.duration
}

View file

@ -17,6 +17,7 @@ class BookMetadata {
this.asin = null
this.language = null
this.explicit = false
this.abridged = false
if (metadata) {
this.construct(metadata)
@ -38,6 +39,7 @@ class BookMetadata {
this.asin = metadata.asin
this.language = metadata.language
this.explicit = !!metadata.explicit
this.abridged = !!metadata.abridged
}
toJSON() {
@ -55,7 +57,8 @@ class BookMetadata {
isbn: this.isbn,
asin: this.asin,
language: this.language,
explicit: this.explicit
explicit: this.explicit,
abridged: this.abridged
}
}
@ -76,7 +79,8 @@ class BookMetadata {
isbn: this.isbn,
asin: this.asin,
language: this.language,
explicit: this.explicit
explicit: this.explicit,
abridged: this.abridged
}
}
@ -100,7 +104,8 @@ class BookMetadata {
authorName: this.authorName,
authorNameLF: this.authorNameLF,
narratorName: this.narratorName,
seriesName: this.seriesName
seriesName: this.seriesName,
abridged: this.abridged
}
}

View file

@ -15,6 +15,7 @@ class PodcastMetadata {
this.itunesArtistId = null
this.explicit = false
this.language = null
this.type = null
if (metadata) {
this.construct(metadata)
@ -34,6 +35,7 @@ class PodcastMetadata {
this.itunesArtistId = metadata.itunesArtistId
this.explicit = metadata.explicit
this.language = metadata.language || null
this.type = metadata.type || 'episodic'
}
toJSON() {
@ -49,7 +51,8 @@ class PodcastMetadata {
itunesId: this.itunesId,
itunesArtistId: this.itunesArtistId,
explicit: this.explicit,
language: this.language
language: this.language,
type: this.type
}
}
@ -67,7 +70,8 @@ class PodcastMetadata {
itunesId: this.itunesId,
itunesArtistId: this.itunesArtistId,
explicit: this.explicit,
language: this.language
language: this.language,
type: this.type
}
}
@ -112,6 +116,7 @@ class PodcastMetadata {
this.itunesArtistId = mediaMetadata.itunesArtistId || null
this.explicit = !!mediaMetadata.explicit
this.language = mediaMetadata.language || null
this.type = mediaMetadata.type || null
if (mediaMetadata.genres && mediaMetadata.genres.length) {
this.genres = [...mediaMetadata.genres]
}
@ -132,4 +137,4 @@ class PodcastMetadata {
return hasUpdates
}
}
module.exports = PodcastMetadata
module.exports = PodcastMetadata

View file

@ -51,6 +51,7 @@ class ServerSettings {
this.chromecastEnabled = false
this.enableEReader = false
this.dateFormat = 'MM/dd/yyyy'
this.timeFormat = 'HH:mm'
this.language = 'en-us'
this.logLevel = Logger.logLevel
@ -106,6 +107,7 @@ class ServerSettings {
this.chromecastEnabled = !!settings.chromecastEnabled
this.enableEReader = !!settings.enableEReader
this.dateFormat = settings.dateFormat || 'MM/dd/yyyy'
this.timeFormat = settings.timeFormat || 'HH:mm'
this.language = settings.language || 'en-us'
this.logLevel = settings.logLevel || Logger.logLevel
this.version = settings.version || null
@ -180,6 +182,7 @@ class ServerSettings {
chromecastEnabled: this.chromecastEnabled,
enableEReader: this.enableEReader,
dateFormat: this.dateFormat,
timeFormat: this.timeFormat,
language: this.language,
logLevel: this.logLevel,
version: this.version,
@ -218,4 +221,4 @@ class ServerSettings {
return hasUpdates
}
}
module.exports = ServerSettings
module.exports = ServerSettings