mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2025-09-10 17:58:02 +02:00
Merge 44f13ea4f6
into 6ea70608a1
This commit is contained in:
commit
4c6c2a2998
@ -168,7 +168,7 @@ export default {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
if (!this.isPodcastLibrary && this.selectedMediaItemsArePlayable) {
|
if (this.selectedMediaItemsArePlayable) {
|
||||||
options.push({
|
options.push({
|
||||||
text: this.$strings.ButtonQuickEmbedMetadata,
|
text: this.$strings.ButtonQuickEmbedMetadata,
|
||||||
action: 'quick-embed'
|
action: 'quick-embed'
|
||||||
|
@ -115,7 +115,6 @@ export default {
|
|||||||
id: 'tools',
|
id: 'tools',
|
||||||
title: this.$strings.HeaderTools,
|
title: this.$strings.HeaderTools,
|
||||||
component: 'modals-item-tabs-tools',
|
component: 'modals-item-tabs-tools',
|
||||||
mediaType: 'book',
|
|
||||||
admin: true
|
admin: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -20,7 +20,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Embed Metadata -->
|
<!-- Embed Metadata -->
|
||||||
<div v-if="mediaTracks.length" class="w-full border border-black-200 p-4 my-8">
|
<div v-if="hasMediaToEmbed" class="w-full border border-black-200 p-4 my-8">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<div>
|
<div>
|
||||||
<p class="text-lg">{{ $strings.LabelToolsEmbedMetadata }}</p>
|
<p class="text-lg">{{ $strings.LabelToolsEmbedMetadata }}</p>
|
||||||
@ -28,7 +28,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="grow" />
|
<div class="grow" />
|
||||||
<div>
|
<div>
|
||||||
<ui-btn :to="`/audiobook/${libraryItemId}/manage?tool=embed`" class="flex items-center"
|
<ui-btn v-if="!isPodcast" :to="`/audiobook/${libraryItemId}/manage?tool=embed`" class="flex items-center"
|
||||||
>{{ $strings.ButtonOpenManager }}
|
>{{ $strings.ButtonOpenManager }}
|
||||||
<span class="material-symbols text-lg ml-2">launch</span>
|
<span class="material-symbols text-lg ml-2">launch</span>
|
||||||
</ui-btn>
|
</ui-btn>
|
||||||
@ -48,7 +48,7 @@
|
|||||||
</widgets-alert>
|
</widgets-alert>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p v-if="!mediaTracks.length" class="text-lg text-center my-8">{{ $strings.MessageNoAudioTracks }}</p>
|
<p v-if="!hasMediaToEmbed" class="text-lg text-center my-8">{{ $strings.MessageNoAudioTracks }}</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -74,6 +74,18 @@ export default {
|
|||||||
mediaTracks() {
|
mediaTracks() {
|
||||||
return this.media.tracks || []
|
return this.media.tracks || []
|
||||||
},
|
},
|
||||||
|
isPodcast() {
|
||||||
|
return (this.libraryItem?.mediaType || '') === 'podcast'
|
||||||
|
},
|
||||||
|
podcastEpisodes() {
|
||||||
|
return this.media.episodes || []
|
||||||
|
},
|
||||||
|
hasMediaToEmbed() {
|
||||||
|
if (this.isPodcast) {
|
||||||
|
return this.podcastEpisodes.some((ep) => ep && ep.audioFile)
|
||||||
|
}
|
||||||
|
return this.mediaTracks.length > 0
|
||||||
|
},
|
||||||
chapters() {
|
chapters() {
|
||||||
return this.media.chapters || []
|
return this.media.chapters || []
|
||||||
},
|
},
|
||||||
|
@ -82,7 +82,7 @@ class ToolsController {
|
|||||||
* @param {Response} res
|
* @param {Response} res
|
||||||
*/
|
*/
|
||||||
async embedAudioFileMetadata(req, res) {
|
async embedAudioFileMetadata(req, res) {
|
||||||
if (req.libraryItem.isMissing || !req.libraryItem.hasAudioTracks || !req.libraryItem.isBook) {
|
if (req.libraryItem.isMissing || !req.libraryItem.hasAudioTracks) {
|
||||||
Logger.error(`[ToolsController] Invalid library item`)
|
Logger.error(`[ToolsController] Invalid library item`)
|
||||||
return res.sendStatus(400)
|
return res.sendStatus(400)
|
||||||
}
|
}
|
||||||
@ -129,7 +129,7 @@ class ToolsController {
|
|||||||
return res.sendStatus(403)
|
return res.sendStatus(403)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (libraryItem.isMissing || !libraryItem.hasAudioTracks || !libraryItem.isBook) {
|
if (libraryItem.isMissing || !libraryItem.hasAudioTracks) {
|
||||||
Logger.error(`[ToolsController] Batch embed invalid library item (${libraryItemId})`)
|
Logger.error(`[ToolsController] Batch embed invalid library item (${libraryItemId})`)
|
||||||
return res.sendStatus(400)
|
return res.sendStatus(400)
|
||||||
}
|
}
|
||||||
|
@ -40,6 +40,17 @@ class AudioMetadataMangaer {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
getMetadataObjectForApi(libraryItem) {
|
getMetadataObjectForApi(libraryItem) {
|
||||||
|
if (libraryItem.isPodcast) {
|
||||||
|
return {
|
||||||
|
title: libraryItem.media.title,
|
||||||
|
artist: libraryItem.media.author,
|
||||||
|
album_artist: libraryItem.media.author,
|
||||||
|
album: libraryItem.media.title,
|
||||||
|
genre: libraryItem.media.genres?.join('; '),
|
||||||
|
description: libraryItem.media.description,
|
||||||
|
language: libraryItem.media.language
|
||||||
|
}
|
||||||
|
}
|
||||||
return ffmpegHelpers.getFFMetadataObject(libraryItem, libraryItem.media.includedAudioFiles.length)
|
return ffmpegHelpers.getFFMetadataObject(libraryItem, libraryItem.media.includedAudioFiles.length)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -65,17 +76,33 @@ class AudioMetadataMangaer {
|
|||||||
const forceEmbedChapters = !!options.forceEmbedChapters
|
const forceEmbedChapters = !!options.forceEmbedChapters
|
||||||
const backupFiles = !!options.backup
|
const backupFiles = !!options.backup
|
||||||
|
|
||||||
const audioFiles = libraryItem.media.includedAudioFiles
|
const audioFiles = libraryItem.isPodcast
|
||||||
|
? libraryItem.media.podcastEpisodes
|
||||||
|
.filter((ep) => !!ep.audioFile && !!ep.audioFile.metadata?.path)
|
||||||
|
.map((ep) => ({
|
||||||
|
episode: ep,
|
||||||
|
index: 1,
|
||||||
|
ino: ep.audioFile?.ino,
|
||||||
|
filename: ep.audioFile?.metadata?.filename,
|
||||||
|
path: ep.audioFile?.metadata?.path,
|
||||||
|
duration: ep.duration,
|
||||||
|
mimeType: ep.audioFile?.mimeType
|
||||||
|
}))
|
||||||
|
: libraryItem.media.includedAudioFiles
|
||||||
|
|
||||||
|
if (!audioFiles.length) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const task = new Task()
|
const task = new Task()
|
||||||
|
|
||||||
const itemCachePath = Path.join(this.itemsCacheDir, libraryItem.id)
|
const itemCachePath = Path.join(this.itemsCacheDir, libraryItem.id)
|
||||||
|
|
||||||
// Only writing chapters for single file audiobooks
|
// Only writing chapters for single file audiobooks
|
||||||
const chapters = audioFiles.length == 1 || forceEmbedChapters ? libraryItem.media.chapters.map((c) => ({ ...c })) : null
|
const chapters = libraryItem.isPodcast ? null : audioFiles.length == 1 || forceEmbedChapters ? libraryItem.media.chapters.map((c) => ({ ...c })) : null
|
||||||
|
|
||||||
let mimeType = audioFiles[0].mimeType
|
let mimeType = libraryItem.isPodcast ? audioFiles[0]?.mimeType || null : audioFiles[0].mimeType
|
||||||
if (audioFiles.some((a) => a.mimeType !== mimeType)) mimeType = null
|
if (audioFiles.some((a) => (libraryItem.isPodcast ? a.mimeType : a.mimeType) !== mimeType)) mimeType = null
|
||||||
|
|
||||||
// Create task
|
// Create task
|
||||||
const libraryItemDir = libraryItem.isFile ? Path.dirname(libraryItem.path) : libraryItem.path
|
const libraryItemDir = libraryItem.isFile ? Path.dirname(libraryItem.path) : libraryItem.path
|
||||||
@ -83,16 +110,51 @@ class AudioMetadataMangaer {
|
|||||||
libraryItemId: libraryItem.id,
|
libraryItemId: libraryItem.id,
|
||||||
libraryItemDir,
|
libraryItemDir,
|
||||||
userId,
|
userId,
|
||||||
audioFiles: audioFiles.map((af) => ({
|
audioFiles: libraryItem.isPodcast
|
||||||
index: af.index,
|
? audioFiles.map((af) => ({
|
||||||
ino: af.ino,
|
index: 1,
|
||||||
filename: af.metadata.filename,
|
ino: af.ino,
|
||||||
path: af.metadata.path,
|
filename: af.filename,
|
||||||
cachePath: Path.join(itemCachePath, af.metadata.filename),
|
path: af.path,
|
||||||
duration: af.duration
|
cachePath: Path.join(itemCachePath, af.filename || 'episode'),
|
||||||
})),
|
duration: af.duration,
|
||||||
|
episodeId: af.episode?.id,
|
||||||
|
mimeType: af.mimeType
|
||||||
|
}))
|
||||||
|
: audioFiles.map((af) => ({
|
||||||
|
index: af.index,
|
||||||
|
ino: af.ino,
|
||||||
|
filename: af.metadata.filename,
|
||||||
|
path: af.metadata.path,
|
||||||
|
cachePath: Path.join(itemCachePath, af.metadata.filename),
|
||||||
|
duration: af.duration
|
||||||
|
})),
|
||||||
coverPath: libraryItem.media.coverPath,
|
coverPath: libraryItem.media.coverPath,
|
||||||
metadataObject: ffmpegHelpers.getFFMetadataObject(libraryItem, audioFiles.length),
|
metadataObject: libraryItem.isPodcast ? null : ffmpegHelpers.getFFMetadataObject(libraryItem, audioFiles.length),
|
||||||
|
podcast: libraryItem.isPodcast
|
||||||
|
? {
|
||||||
|
title: libraryItem.media.title,
|
||||||
|
author: libraryItem.media.author,
|
||||||
|
genres: Array.isArray(libraryItem.media.genres) ? [...libraryItem.media.genres] : [],
|
||||||
|
language: libraryItem.media.language,
|
||||||
|
itunesId: libraryItem.media.itunesId,
|
||||||
|
podcastType: libraryItem.media.podcastType,
|
||||||
|
releaseDate: libraryItem.media.releaseDate
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
podcastEpisodes: libraryItem.isPodcast
|
||||||
|
? libraryItem.media.podcastEpisodes.map((ep) => ({
|
||||||
|
id: ep.id,
|
||||||
|
title: ep.title,
|
||||||
|
description: ep.description,
|
||||||
|
subtitle: ep.subtitle,
|
||||||
|
season: ep.season,
|
||||||
|
episode: ep.episode,
|
||||||
|
episodeType: ep.episodeType,
|
||||||
|
pubDate: ep.pubDate,
|
||||||
|
chapters: Array.isArray(ep.chapters) ? ep.chapters.map((c) => ({ ...c })) : []
|
||||||
|
}))
|
||||||
|
: null,
|
||||||
itemCachePath,
|
itemCachePath,
|
||||||
chapters,
|
chapters,
|
||||||
mimeType,
|
mimeType,
|
||||||
@ -100,18 +162,24 @@ class AudioMetadataMangaer {
|
|||||||
forceEmbedChapters,
|
forceEmbedChapters,
|
||||||
backupFiles
|
backupFiles
|
||||||
},
|
},
|
||||||
duration: libraryItem.media.duration
|
duration: libraryItem.isPodcast ? audioFiles.reduce((acc, af) => acc + (af.duration || 0), 0) || 0 : libraryItem.media.duration
|
||||||
}
|
}
|
||||||
|
|
||||||
const taskTitleString = {
|
const taskTitleString = {
|
||||||
text: 'Embedding Metadata',
|
text: 'Embedding Metadata',
|
||||||
key: 'MessageTaskEmbeddingMetadata'
|
key: 'MessageTaskEmbeddingMetadata'
|
||||||
}
|
}
|
||||||
const taskDescriptionString = {
|
const taskDescriptionString = libraryItem.isPodcast
|
||||||
text: `Embedding metadata in audiobook "${libraryItem.media.title}".`,
|
? {
|
||||||
key: 'MessageTaskEmbeddingMetadataDescription',
|
text: `Embedding metadata in podcast "${libraryItem.media.title}" episodes.`,
|
||||||
subs: [libraryItem.media.title]
|
key: 'MessageTaskEmbeddingMetadataDescription',
|
||||||
}
|
subs: [libraryItem.media.title]
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
text: `Embedding metadata in audiobook "${libraryItem.media.title}".`,
|
||||||
|
key: 'MessageTaskEmbeddingMetadataDescription',
|
||||||
|
subs: [libraryItem.media.title]
|
||||||
|
}
|
||||||
task.setData('embed-metadata', taskTitleString, taskDescriptionString, false, taskData)
|
task.setData('embed-metadata', taskTitleString, taskDescriptionString, false, taskData)
|
||||||
|
|
||||||
if (this.tasksRunning.length >= this.MAX_CONCURRENT_TASKS) {
|
if (this.tasksRunning.length >= this.MAX_CONCURRENT_TASKS) {
|
||||||
@ -185,20 +253,24 @@ class AudioMetadataMangaer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create ffmetadata file
|
let ffmetadataPath = Path.join(task.data.itemCachePath, 'ffmetadata.txt')
|
||||||
const ffmetadataPath = Path.join(task.data.itemCachePath, 'ffmetadata.txt')
|
// Pre-write single ffmetadata file for non-podcast items
|
||||||
const success = await ffmpegHelpers.writeFFMetadataFile(task.data.metadataObject, task.data.chapters, ffmetadataPath)
|
if (task.data.metadataObject) {
|
||||||
if (!success) {
|
const success = await ffmpegHelpers.writeFFMetadataFile(task.data.metadataObject, task.data.chapters, ffmetadataPath)
|
||||||
Logger.error(`[AudioMetadataManager] Failed to write ffmetadata file for audiobook "${task.data.libraryItemId}"`)
|
if (!success) {
|
||||||
const taskFailedString = {
|
Logger.error(`[AudioMetadataManager] Failed to write ffmetadata file for audiobook "${task.data.libraryItemId}"`)
|
||||||
text: 'Failed to write metadata file',
|
const taskFailedString = {
|
||||||
key: 'MessageTaskFailedToWriteMetadataFile'
|
text: 'Failed to write metadata file',
|
||||||
|
key: 'MessageTaskFailedToWriteMetadataFile'
|
||||||
|
}
|
||||||
|
task.setFailed(taskFailedString)
|
||||||
|
this.handleTaskFinished(task)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
task.setFailed(taskFailedString)
|
|
||||||
this.handleTaskFinished(task)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const createdFFMetadataFiles = []
|
||||||
|
|
||||||
// Tag audio files
|
// Tag audio files
|
||||||
let cummulativeProgress = 0
|
let cummulativeProgress = 0
|
||||||
for (const af of task.data.audioFiles) {
|
for (const af of task.data.audioFiles) {
|
||||||
@ -228,7 +300,23 @@ class AudioMetadataMangaer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await ffmpegHelpers.addCoverAndMetadataToFile(af.path, task.data.coverPath, ffmetadataPath, af.index, task.data.mimeType, (progress) => {
|
// For podcasts, write per-episode ffmetadata file and chapters
|
||||||
|
let perFileMetaPath = ffmetadataPath
|
||||||
|
if (!task.data.metadataObject) {
|
||||||
|
// Podcast flow: metadataObject is null; generate per-episode metadata
|
||||||
|
const episode = (task.data.podcastEpisodes || []).find((ep) => ep.id === af.episodeId)
|
||||||
|
const liStub = { media: task.data.podcast }
|
||||||
|
const perEpisodeMeta = ffmpegHelpers.getPodcastEpisodeFFMetadataObject(liStub, episode)
|
||||||
|
perFileMetaPath = Path.join(task.data.itemCachePath, `${af.filename || 'episode'}.ffmetadata.txt`)
|
||||||
|
const episodeChapters = Array.isArray(episode?.chapters) && episode.chapters.length ? episode.chapters.map((c) => ({ ...c })) : null
|
||||||
|
const wrote = await ffmpegHelpers.writeFFMetadataFile(perEpisodeMeta, episodeChapters, perFileMetaPath)
|
||||||
|
if (!wrote) {
|
||||||
|
throw new Error('Failed to write episode ffmetadata file')
|
||||||
|
}
|
||||||
|
createdFFMetadataFiles.push(perFileMetaPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
await ffmpegHelpers.addCoverAndMetadataToFile(af.path, task.data.coverPath, perFileMetaPath, af.index, af.mimeType || task.data.mimeType, (progress) => {
|
||||||
SocketAuthority.adminEmitter('task_progress', { libraryItemId: task.data.libraryItemId, progress: cummulativeProgress + progress * audioFileRelativeDuration })
|
SocketAuthority.adminEmitter('task_progress', { libraryItemId: task.data.libraryItemId, progress: cummulativeProgress + progress * audioFileRelativeDuration })
|
||||||
SocketAuthority.adminEmitter('track_progress', { libraryItemId: task.data.libraryItemId, ino: af.ino, progress })
|
SocketAuthority.adminEmitter('track_progress', { libraryItemId: task.data.libraryItemId, ino: af.ino, progress })
|
||||||
})
|
})
|
||||||
@ -259,7 +347,13 @@ class AudioMetadataMangaer {
|
|||||||
if (cacheDirCreated) {
|
if (cacheDirCreated) {
|
||||||
await fs.remove(task.data.itemCachePath)
|
await fs.remove(task.data.itemCachePath)
|
||||||
} else {
|
} else {
|
||||||
await fs.remove(ffmetadataPath)
|
if (createdFFMetadataFiles.length) {
|
||||||
|
for (const metaPath of createdFFMetadataFiles) {
|
||||||
|
await fs.remove(metaPath)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
await fs.remove(ffmetadataPath)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -153,29 +153,7 @@ module.exports.downloadPodcastEpisode = (podcastEpisodeDownload) => {
|
|||||||
const podcastEpisode = podcastEpisodeDownload.rssPodcastEpisode
|
const podcastEpisode = podcastEpisodeDownload.rssPodcastEpisode
|
||||||
const finalSizeInBytes = Number(podcastEpisode.enclosure?.length || 0)
|
const finalSizeInBytes = Number(podcastEpisode.enclosure?.length || 0)
|
||||||
|
|
||||||
const taggings = {
|
const taggings = getPodcastEpisodeFFMetadataObject(podcast, podcastEpisode, podcastEpisodeDownload)
|
||||||
album: podcast.title,
|
|
||||||
'album-sort': podcast.title,
|
|
||||||
artist: podcast.author,
|
|
||||||
'artist-sort': podcast.author,
|
|
||||||
comment: podcastEpisode.description,
|
|
||||||
subtitle: podcastEpisode.subtitle,
|
|
||||||
disc: podcastEpisode.season,
|
|
||||||
genre: podcast.genres.length ? podcast.genres.join(';') : null,
|
|
||||||
language: podcast.language,
|
|
||||||
MVNM: podcast.title,
|
|
||||||
MVIN: podcastEpisode.episode,
|
|
||||||
track: podcastEpisode.episode,
|
|
||||||
'series-part': podcastEpisode.episode,
|
|
||||||
title: podcastEpisode.title,
|
|
||||||
'title-sort': podcastEpisode.title,
|
|
||||||
year: podcastEpisodeDownload.pubYear,
|
|
||||||
date: podcastEpisode.pubDate,
|
|
||||||
releasedate: podcastEpisode.pubDate,
|
|
||||||
'itunes-id': podcast.itunesId,
|
|
||||||
'podcast-type': podcast.podcastType,
|
|
||||||
'episode-type': podcastEpisode.episodeType
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const tag in taggings) {
|
for (const tag in taggings) {
|
||||||
if (taggings[tag]) {
|
if (taggings[tag]) {
|
||||||
@ -427,6 +405,73 @@ function getFFMetadataObject(libraryItem, audioFilesLength) {
|
|||||||
|
|
||||||
module.exports.getFFMetadataObject = getFFMetadataObject
|
module.exports.getFFMetadataObject = getFFMetadataObject
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build ffmetadata key-value pairs for a single podcast episode based on the library item and episode.
|
||||||
|
*
|
||||||
|
* @param {import('../models/LibraryItem')} libraryItem
|
||||||
|
* @param {import('../models/PodcastEpisode')} podcastEpisode
|
||||||
|
* @returns {Object}
|
||||||
|
*/
|
||||||
|
function getPodcastEpisodeFFMetadataObject(podcast, podcastEpisode, podcastEpisodeDownload = {}) {
|
||||||
|
function formatToISO8601(date) {
|
||||||
|
if (!date) return null
|
||||||
|
|
||||||
|
let d
|
||||||
|
if (date instanceof Date) {
|
||||||
|
d = date
|
||||||
|
} else {
|
||||||
|
d = new Date(date)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isNaN(d.getTime())) return null
|
||||||
|
return d.toISOString()
|
||||||
|
}
|
||||||
|
|
||||||
|
const pubDateISO = formatToISO8601(podcastEpisode.pubDate)
|
||||||
|
let pubYearISO = null
|
||||||
|
if (podcastEpisodeDownload?.pubYear) {
|
||||||
|
pubYearISO = podcastEpisodeDownload.pubYear
|
||||||
|
} else if (podcastEpisode.pubDate) {
|
||||||
|
const pubDateObj = new Date(podcastEpisode.pubDate)
|
||||||
|
if (pubDateObj && !isNaN(pubDateObj.getTime())) {
|
||||||
|
pubYearISO = pubDateObj.getUTCFullYear().toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const taggings = {
|
||||||
|
album: podcast.title,
|
||||||
|
'album-sort': podcast.title,
|
||||||
|
artist: podcast.author,
|
||||||
|
'artist-sort': podcast.author,
|
||||||
|
comment: podcastEpisode.description,
|
||||||
|
subtitle: podcastEpisode.subtitle,
|
||||||
|
disc: podcastEpisode.season,
|
||||||
|
genre: Array.isArray(podcast.genres) && podcast.genres.length ? podcast.genres.join(';') : null,
|
||||||
|
language: podcast.language,
|
||||||
|
MVNM: podcast.title,
|
||||||
|
MVIN: podcastEpisode.episode,
|
||||||
|
track: podcastEpisode.episode,
|
||||||
|
'series-part': podcastEpisode.episode,
|
||||||
|
title: podcastEpisode.title,
|
||||||
|
'title-sort': podcastEpisode.title,
|
||||||
|
year: pubYearISO,
|
||||||
|
date: pubDateISO,
|
||||||
|
releasedate: pubDateISO,
|
||||||
|
'itunes-id': podcast.itunesId,
|
||||||
|
'podcast-type': podcast.podcastType,
|
||||||
|
'episode-type': podcastEpisode.episodeType
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.keys(taggings).forEach((key) => {
|
||||||
|
if (taggings[key] === undefined || taggings[key] === null || taggings[key] === '') {
|
||||||
|
delete taggings[key]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return taggings
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports.getPodcastEpisodeFFMetadataObject = getPodcastEpisodeFFMetadataObject
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Merges audio files into a single output file using FFmpeg.
|
* Merges audio files into a single output file using FFmpeg.
|
||||||
*
|
*
|
||||||
|
Loading…
Reference in New Issue
Block a user