2023-07-05 01:14:44 +02:00
|
|
|
const uuidv4 = require("uuid").v4
|
2022-07-31 20:52:34 +02:00
|
|
|
const Path = require('path')
|
2023-03-31 01:04:21 +02:00
|
|
|
const Logger = require('../../Logger')
|
2023-07-05 01:14:44 +02:00
|
|
|
const { cleanStringForSearch, areEquivalent, copyValue } = require('../../utils/index')
|
2022-03-09 02:31:44 +01:00
|
|
|
const AudioFile = require('../files/AudioFile')
|
2022-03-18 01:10:47 +01:00
|
|
|
const AudioTrack = require('../files/AudioTrack')
|
2022-03-09 02:31:44 +01:00
|
|
|
|
|
|
|
class PodcastEpisode {
|
|
|
|
constructor(episode) {
|
2022-04-06 02:40:40 +02:00
|
|
|
this.libraryItemId = null
|
2023-07-05 01:14:44 +02:00
|
|
|
this.podcastId = null
|
2022-03-09 02:31:44 +01:00
|
|
|
this.id = null
|
2023-07-16 22:05:51 +02:00
|
|
|
this.oldEpisodeId = null
|
2022-03-17 01:15:25 +01:00
|
|
|
this.index = null
|
2022-03-09 02:31:44 +01:00
|
|
|
|
2022-05-04 16:14:09 +02:00
|
|
|
this.season = null
|
2022-03-22 01:24:38 +01:00
|
|
|
this.episode = null
|
|
|
|
this.episodeType = null
|
2022-03-19 12:41:54 +01:00
|
|
|
this.title = null
|
2022-03-22 01:24:38 +01:00
|
|
|
this.subtitle = null
|
2022-03-19 12:41:54 +01:00
|
|
|
this.description = null
|
|
|
|
this.enclosure = null
|
|
|
|
this.pubDate = null
|
2023-04-09 21:32:51 +02:00
|
|
|
this.chapters = []
|
2022-03-19 12:41:54 +01:00
|
|
|
|
2022-03-09 02:31:44 +01:00
|
|
|
this.audioFile = null
|
2022-03-22 01:24:38 +01:00
|
|
|
this.publishedAt = null
|
2022-03-09 02:31:44 +01:00
|
|
|
this.addedAt = null
|
|
|
|
this.updatedAt = null
|
|
|
|
|
|
|
|
if (episode) {
|
|
|
|
this.construct(episode)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
construct(episode) {
|
2022-04-06 02:40:40 +02:00
|
|
|
this.libraryItemId = episode.libraryItemId
|
2023-07-05 01:14:44 +02:00
|
|
|
this.podcastId = episode.podcastId
|
2022-03-09 02:31:44 +01:00
|
|
|
this.id = episode.id
|
2023-07-16 22:05:51 +02:00
|
|
|
this.oldEpisodeId = episode.oldEpisodeId
|
2022-03-17 01:15:25 +01:00
|
|
|
this.index = episode.index
|
2022-05-04 16:14:09 +02:00
|
|
|
this.season = episode.season
|
2022-03-22 01:24:38 +01:00
|
|
|
this.episode = episode.episode
|
|
|
|
this.episodeType = episode.episodeType
|
2022-03-19 12:41:54 +01:00
|
|
|
this.title = episode.title
|
2022-03-22 01:24:38 +01:00
|
|
|
this.subtitle = episode.subtitle
|
2022-03-19 12:41:54 +01:00
|
|
|
this.description = episode.description
|
|
|
|
this.enclosure = episode.enclosure ? { ...episode.enclosure } : null
|
|
|
|
this.pubDate = episode.pubDate
|
2023-04-09 21:32:51 +02:00
|
|
|
this.chapters = episode.chapters?.map(ch => ({ ...ch })) || []
|
2022-03-09 02:31:44 +01:00
|
|
|
this.audioFile = new AudioFile(episode.audioFile)
|
2022-03-22 01:24:38 +01:00
|
|
|
this.publishedAt = episode.publishedAt
|
2022-03-09 02:31:44 +01:00
|
|
|
this.addedAt = episode.addedAt
|
|
|
|
this.updatedAt = episode.updatedAt
|
2022-04-16 03:49:13 +02:00
|
|
|
|
|
|
|
this.audioFile.index = 1 // Only 1 audio file per episode
|
2022-03-09 02:31:44 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
toJSON() {
|
|
|
|
return {
|
2022-04-06 02:40:40 +02:00
|
|
|
libraryItemId: this.libraryItemId,
|
2023-07-05 01:14:44 +02:00
|
|
|
podcastId: this.podcastId,
|
2022-03-09 02:31:44 +01:00
|
|
|
id: this.id,
|
2023-07-16 22:05:51 +02:00
|
|
|
oldEpisodeId: this.oldEpisodeId,
|
2022-03-17 01:15:25 +01:00
|
|
|
index: this.index,
|
2022-05-04 16:14:09 +02:00
|
|
|
season: this.season,
|
2022-03-22 01:24:38 +01:00
|
|
|
episode: this.episode,
|
|
|
|
episodeType: this.episodeType,
|
2022-03-19 12:41:54 +01:00
|
|
|
title: this.title,
|
2022-03-22 01:24:38 +01:00
|
|
|
subtitle: this.subtitle,
|
2022-03-19 12:41:54 +01:00
|
|
|
description: this.description,
|
|
|
|
enclosure: this.enclosure ? { ...this.enclosure } : null,
|
|
|
|
pubDate: this.pubDate,
|
2023-04-09 21:32:51 +02:00
|
|
|
chapters: this.chapters.map(ch => ({ ...ch })),
|
2022-03-09 02:31:44 +01:00
|
|
|
audioFile: this.audioFile.toJSON(),
|
2022-03-22 01:24:38 +01:00
|
|
|
publishedAt: this.publishedAt,
|
2022-03-09 02:31:44 +01:00
|
|
|
addedAt: this.addedAt,
|
|
|
|
updatedAt: this.updatedAt
|
|
|
|
}
|
|
|
|
}
|
2022-03-18 01:10:47 +01:00
|
|
|
|
2022-03-26 21:23:25 +01:00
|
|
|
toJSONExpanded() {
|
|
|
|
return {
|
2022-04-06 02:40:40 +02:00
|
|
|
libraryItemId: this.libraryItemId,
|
2023-07-05 01:14:44 +02:00
|
|
|
podcastId: this.podcastId,
|
2022-03-26 21:23:25 +01:00
|
|
|
id: this.id,
|
2023-07-16 22:05:51 +02:00
|
|
|
oldEpisodeId: this.oldEpisodeId,
|
2022-03-26 21:23:25 +01:00
|
|
|
index: this.index,
|
2022-05-04 16:14:09 +02:00
|
|
|
season: this.season,
|
2022-03-26 21:23:25 +01:00
|
|
|
episode: this.episode,
|
|
|
|
episodeType: this.episodeType,
|
|
|
|
title: this.title,
|
|
|
|
subtitle: this.subtitle,
|
2022-05-28 02:41:40 +02:00
|
|
|
description: this.description,
|
2022-03-26 21:23:25 +01:00
|
|
|
enclosure: this.enclosure ? { ...this.enclosure } : null,
|
|
|
|
pubDate: this.pubDate,
|
2023-04-09 21:32:51 +02:00
|
|
|
chapters: this.chapters.map(ch => ({ ...ch })),
|
2022-03-26 21:23:25 +01:00
|
|
|
audioFile: this.audioFile.toJSON(),
|
2022-04-06 02:40:40 +02:00
|
|
|
audioTrack: this.audioTrack.toJSON(),
|
2022-03-26 21:23:25 +01:00
|
|
|
publishedAt: this.publishedAt,
|
|
|
|
addedAt: this.addedAt,
|
|
|
|
updatedAt: this.updatedAt,
|
|
|
|
duration: this.duration,
|
|
|
|
size: this.size
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-04-06 02:40:40 +02:00
|
|
|
get audioTrack() {
|
2022-12-26 23:08:53 +01:00
|
|
|
const audioTrack = new AudioTrack()
|
2022-04-06 02:40:40 +02:00
|
|
|
audioTrack.setData(this.libraryItemId, this.audioFile, 0)
|
|
|
|
return audioTrack
|
|
|
|
}
|
2022-03-18 01:10:47 +01:00
|
|
|
get tracks() {
|
2022-04-06 02:40:40 +02:00
|
|
|
return [this.audioTrack]
|
2022-03-18 01:10:47 +01:00
|
|
|
}
|
2022-03-18 18:37:47 +01:00
|
|
|
get duration() {
|
|
|
|
return this.audioFile.duration
|
|
|
|
}
|
|
|
|
get size() { return this.audioFile.metadata.size }
|
2022-09-16 01:35:56 +02:00
|
|
|
get enclosureUrl() {
|
2023-05-28 18:24:51 +02:00
|
|
|
return this.enclosure?.url || null
|
2022-09-16 01:35:56 +02:00
|
|
|
}
|
2023-03-30 01:05:53 +02:00
|
|
|
get pubYear() {
|
|
|
|
if (!this.publishedAt) return null
|
|
|
|
return new Date(this.publishedAt).getFullYear()
|
|
|
|
}
|
2022-03-18 01:10:47 +01:00
|
|
|
|
2022-03-19 16:13:10 +01:00
|
|
|
setData(data, index = 1) {
|
2023-07-05 01:14:44 +02:00
|
|
|
this.id = uuidv4()
|
2022-03-19 16:13:10 +01:00
|
|
|
this.index = index
|
|
|
|
this.title = data.title
|
2022-03-22 01:24:38 +01:00
|
|
|
this.subtitle = data.subtitle || ''
|
2022-03-19 16:13:10 +01:00
|
|
|
this.pubDate = data.pubDate || ''
|
|
|
|
this.description = data.description || ''
|
|
|
|
this.enclosure = data.enclosure ? { ...data.enclosure } : null
|
2022-05-04 16:14:09 +02:00
|
|
|
this.season = data.season || ''
|
2022-03-22 01:24:38 +01:00
|
|
|
this.episode = data.episode || ''
|
2023-02-22 19:48:36 +01:00
|
|
|
this.episodeType = data.episodeType || 'full'
|
2022-03-22 01:24:38 +01:00
|
|
|
this.publishedAt = data.publishedAt || 0
|
2022-03-19 16:13:10 +01:00
|
|
|
this.addedAt = Date.now()
|
|
|
|
this.updatedAt = Date.now()
|
|
|
|
}
|
|
|
|
|
2022-03-26 20:29:49 +01:00
|
|
|
setDataFromAudioFile(audioFile, index) {
|
2023-07-05 01:14:44 +02:00
|
|
|
this.id = uuidv4()
|
2022-03-26 20:29:49 +01:00
|
|
|
this.audioFile = audioFile
|
2022-07-31 20:52:34 +02:00
|
|
|
this.title = Path.basename(audioFile.metadata.filename, Path.extname(audioFile.metadata.filename))
|
2022-03-26 20:29:49 +01:00
|
|
|
this.index = index
|
2023-03-31 01:04:21 +02:00
|
|
|
|
|
|
|
this.setDataFromAudioMetaTags(audioFile.metaTags, true)
|
|
|
|
|
2023-04-09 21:32:51 +02:00
|
|
|
this.chapters = audioFile.chapters?.map((c) => ({ ...c }))
|
2022-03-26 20:29:49 +01:00
|
|
|
this.addedAt = Date.now()
|
|
|
|
this.updatedAt = Date.now()
|
|
|
|
}
|
|
|
|
|
2022-03-27 22:37:04 +02:00
|
|
|
update(payload) {
|
2022-12-26 23:08:53 +01:00
|
|
|
let hasUpdates = false
|
2022-03-27 22:37:04 +02:00
|
|
|
for (const key in this.toJSON()) {
|
2023-07-06 01:18:37 +02:00
|
|
|
let newValue = payload[key]
|
|
|
|
if (newValue === "") newValue = null
|
|
|
|
let existingValue = this[key]
|
|
|
|
if (existingValue === "") existingValue = null
|
|
|
|
|
|
|
|
if (newValue != undefined && !areEquivalent(newValue, existingValue)) {
|
|
|
|
this[key] = copyValue(newValue)
|
2022-03-27 22:37:04 +02:00
|
|
|
hasUpdates = true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (hasUpdates) {
|
|
|
|
this.updatedAt = Date.now()
|
|
|
|
}
|
|
|
|
return hasUpdates
|
|
|
|
}
|
|
|
|
|
2022-03-18 01:10:47 +01:00
|
|
|
// Only checks container format
|
|
|
|
checkCanDirectPlay(payload) {
|
2022-12-22 23:38:55 +01:00
|
|
|
const supportedMimeTypes = payload.supportedMimeTypes || []
|
2022-03-18 01:10:47 +01:00
|
|
|
return supportedMimeTypes.includes(this.audioFile.mimeType)
|
|
|
|
}
|
|
|
|
|
2022-04-06 02:40:40 +02:00
|
|
|
getDirectPlayTracklist() {
|
|
|
|
return this.tracks
|
2022-03-18 01:10:47 +01:00
|
|
|
}
|
2022-03-27 01:58:59 +01:00
|
|
|
|
|
|
|
checkEqualsEnclosureUrl(url) {
|
|
|
|
if (!this.enclosure || !this.enclosure.url) return false
|
|
|
|
return this.enclosure.url == url
|
|
|
|
}
|
2023-01-05 00:43:15 +01:00
|
|
|
|
|
|
|
searchQuery(query) {
|
|
|
|
return cleanStringForSearch(this.title).includes(query)
|
|
|
|
}
|
2023-03-31 01:04:21 +02:00
|
|
|
|
|
|
|
setDataFromAudioMetaTags(audioFileMetaTags, overrideExistingDetails = false) {
|
|
|
|
if (!audioFileMetaTags) return false
|
|
|
|
|
|
|
|
const MetadataMapArray = [
|
|
|
|
{
|
|
|
|
tag: 'tagComment',
|
|
|
|
altTag: 'tagSubtitle',
|
|
|
|
key: 'description'
|
|
|
|
},
|
|
|
|
{
|
|
|
|
tag: 'tagSubtitle',
|
|
|
|
key: 'subtitle'
|
|
|
|
},
|
|
|
|
{
|
|
|
|
tag: 'tagDate',
|
|
|
|
key: 'pubDate'
|
|
|
|
},
|
|
|
|
{
|
|
|
|
tag: 'tagDisc',
|
|
|
|
key: 'season',
|
|
|
|
},
|
|
|
|
{
|
|
|
|
tag: 'tagTrack',
|
|
|
|
altTag: 'tagSeriesPart',
|
|
|
|
key: 'episode'
|
|
|
|
},
|
|
|
|
{
|
|
|
|
tag: 'tagTitle',
|
|
|
|
key: 'title'
|
|
|
|
},
|
|
|
|
{
|
|
|
|
tag: 'tagEpisodeType',
|
|
|
|
key: 'episodeType'
|
|
|
|
}
|
|
|
|
]
|
|
|
|
|
|
|
|
MetadataMapArray.forEach((mapping) => {
|
|
|
|
let value = audioFileMetaTags[mapping.tag]
|
|
|
|
let tagToUse = mapping.tag
|
|
|
|
if (!value && mapping.altTag) {
|
|
|
|
tagToUse = mapping.altTag
|
|
|
|
value = audioFileMetaTags[mapping.altTag]
|
|
|
|
}
|
|
|
|
|
|
|
|
if (value && typeof value === 'string') {
|
|
|
|
value = value.trim() // Trim whitespace
|
|
|
|
|
|
|
|
if (mapping.key === 'pubDate' && (!this.pubDate || overrideExistingDetails)) {
|
|
|
|
const pubJsDate = new Date(value)
|
|
|
|
if (pubJsDate && !isNaN(pubJsDate)) {
|
|
|
|
this.publishedAt = pubJsDate.valueOf()
|
|
|
|
this.pubDate = value
|
|
|
|
Logger.debug(`[PodcastEpisode] Mapping metadata to key ${tagToUse} => ${mapping.key}: ${this[mapping.key]}`)
|
|
|
|
} else {
|
|
|
|
Logger.warn(`[PodcastEpisode] Mapping pubDate with tag ${tagToUse} has invalid date "${value}"`)
|
|
|
|
}
|
|
|
|
} else if (mapping.key === 'episodeType' && (!this.episodeType || overrideExistingDetails)) {
|
|
|
|
if (['full', 'trailer', 'bonus'].includes(value)) {
|
|
|
|
this.episodeType = value
|
|
|
|
Logger.debug(`[PodcastEpisode] Mapping metadata to key ${tagToUse} => ${mapping.key}: ${this[mapping.key]}`)
|
|
|
|
} else {
|
|
|
|
Logger.warn(`[PodcastEpisode] Mapping episodeType with invalid value "${value}". Must be one of [full, trailer, bonus].`)
|
|
|
|
}
|
|
|
|
} else if (!this[mapping.key] || overrideExistingDetails) {
|
|
|
|
this[mapping.key] = value
|
|
|
|
Logger.debug(`[PodcastEpisode] Mapping metadata to key ${tagToUse} => ${mapping.key}: ${this[mapping.key]}`)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
2022-03-09 02:31:44 +01:00
|
|
|
}
|
2023-02-22 19:48:36 +01:00
|
|
|
module.exports = PodcastEpisode
|