Merge branch 'advplyr:master' into master

This commit is contained in:
sbyrx 2025-01-04 20:15:59 -05:00 committed by GitHub
commit e64302f1d4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
102 changed files with 4054 additions and 2413 deletions

View file

@ -8,8 +8,8 @@ const { filePathToPOSIX, copyToExisting } = require('./fileUtils')
const LibraryItem = require('../objects/LibraryItem')
function escapeSingleQuotes(path) {
// return path.replace(/'/g, '\'\\\'\'')
return filePathToPOSIX(path).replace(/ /g, '\\ ').replace(/'/g, "\\'")
// A ' within a quoted string is escaped with '\'' in ffmpeg (see https://www.ffmpeg.org/ffmpeg-utils.html#Quoting-and-escaping)
return filePathToPOSIX(path).replace(/'/g, "'\\''")
}
// Returns first track start time
@ -33,7 +33,7 @@ async function writeConcatFile(tracks, outputPath, startTime = 0) {
var tracksToInclude = tracks.filter((t) => t.index >= trackToStartWithIndex)
var trackPaths = tracksToInclude.map((t) => {
var line = 'file ' + escapeSingleQuotes(t.metadata.path) + '\n' + `duration ${t.duration}`
var line = "file '" + escapeSingleQuotes(t.metadata.path) + "'\n" + `duration ${t.duration}`
return line
})
var inputstr = trackPaths.join('\n\n')
@ -97,6 +97,11 @@ async function resizeImage(filePath, outputPath, width, height) {
}
module.exports.resizeImage = resizeImage
/**
*
* @param {import('../objects/PodcastEpisodeDownload')} podcastEpisodeDownload
* @returns
*/
module.exports.downloadPodcastEpisode = (podcastEpisodeDownload) => {
return new Promise(async (resolve) => {
const response = await axios({
@ -118,32 +123,33 @@ module.exports.downloadPodcastEpisode = (podcastEpisodeDownload) => {
ffmpeg.addOption('-loglevel debug') // Debug logs printed on error
ffmpeg.outputOptions('-c:a', 'copy', '-map', '0:a', '-metadata', 'podcast=1')
const podcastMetadata = podcastEpisodeDownload.libraryItem.media.metadata
const podcastEpisode = podcastEpisodeDownload.podcastEpisode
/** @type {import('../models/Podcast')} */
const podcast = podcastEpisodeDownload.libraryItem.media
const podcastEpisode = podcastEpisodeDownload.rssPodcastEpisode
const finalSizeInBytes = Number(podcastEpisode.enclosure?.length || 0)
const taggings = {
album: podcastMetadata.title,
'album-sort': podcastMetadata.title,
artist: podcastMetadata.author,
'artist-sort': podcastMetadata.author,
album: podcast.title,
'album-sort': podcast.title,
artist: podcast.author,
'artist-sort': podcast.author,
comment: podcastEpisode.description,
subtitle: podcastEpisode.subtitle,
disc: podcastEpisode.season,
genre: podcastMetadata.genres.length ? podcastMetadata.genres.join(';') : null,
language: podcastMetadata.language,
MVNM: podcastMetadata.title,
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: podcastEpisode.pubYear,
year: podcastEpisodeDownload.pubYear,
date: podcastEpisode.pubDate,
releasedate: podcastEpisode.pubDate,
'itunes-id': podcastMetadata.itunesId,
'podcast-type': podcastMetadata.type,
'episode-type': podcastMetadata.episodeType
'itunes-id': podcast.itunesId,
'podcast-type': podcast.podcastType,
'episode-type': podcastEpisode.episodeType
}
for (const tag in taggings) {

View file

@ -1200,7 +1200,7 @@ async function migrationPatchNewColumns(queryInterface) {
*/
async function handleOldLibraryItems(ctx) {
const oldLibraryItems = await oldDbFiles.loadOldData('libraryItems')
const libraryItems = await ctx.models.libraryItem.getAllOldLibraryItems()
const libraryItems = (await ctx.models.libraryItem.findAllExpandedWhere()).map((li) => ctx.models.libraryItem.getOldLibraryItem(li))
const bulkUpdateItems = []
const bulkUpdateEpisodes = []

View file

@ -4,6 +4,49 @@ const Logger = require('../Logger')
const { xmlToJSON, levenshteinDistance } = require('./index')
const htmlSanitizer = require('../utils/htmlSanitizer')
/**
* @typedef RssPodcastEpisode
* @property {string} title
* @property {string} subtitle
* @property {string} description
* @property {string} descriptionPlain
* @property {string} pubDate
* @property {string} episodeType
* @property {string} season
* @property {string} episode
* @property {string} author
* @property {string} duration
* @property {string} explicit
* @property {number} publishedAt - Unix timestamp
* @property {{ url: string, type?: string, length?: string }} enclosure
* @property {string} guid
* @property {string} chaptersUrl
* @property {string} chaptersType
*/
/**
* @typedef RssPodcastMetadata
* @property {string} title
* @property {string} language
* @property {string} explicit
* @property {string} author
* @property {string} pubDate
* @property {string} link
* @property {string} image
* @property {string[]} categories
* @property {string} feedUrl
* @property {string} description
* @property {string} descriptionPlain
* @property {string} type
*/
/**
* @typedef RssPodcast
* @property {RssPodcastMetadata} metadata
* @property {RssPodcastEpisode[]} episodes
* @property {number} numEpisodes
*/
function extractFirstArrayItem(json, key) {
if (!json[key]?.length) return null
return json[key][0]
@ -223,7 +266,7 @@ module.exports.parsePodcastRssFeedXml = async (xml, excludeEpisodeMetadata = fal
*
* @param {string} feedUrl
* @param {boolean} [excludeEpisodeMetadata=false]
* @returns {Promise}
* @returns {Promise<RssPodcast|null>}
*/
module.exports.getPodcastFeed = (feedUrl, excludeEpisodeMetadata = false) => {
Logger.debug(`[podcastUtils] getPodcastFeed for "${feedUrl}"`)

View file

@ -18,7 +18,7 @@ module.exports = {
* @param {string} libraryId
* @param {import('../../models/User')} user
* @param {object} options
* @returns {object} { libraryItems:LibraryItem[], count:number }
* @returns {Promise<{ libraryItems:import('../../models/LibraryItem')[], count:number }>}
*/
async getFilteredLibraryItems(libraryId, user, options) {
const { filterBy, sortBy, sortDesc, limit, offset, collapseseries, include, mediaType } = options
@ -52,7 +52,7 @@ module.exports = {
const { libraryItems, count } = await libraryItemsBookFilters.getFilteredLibraryItems(library.id, user, 'progress', 'in-progress', 'progress', true, false, include, limit, 0, true)
return {
items: libraryItems.map((li) => {
const oldLibraryItem = Database.libraryItemModel.getOldLibraryItem(li).toJSONMinified()
const oldLibraryItem = li.toOldJSONMinified()
if (li.rssFeed) {
oldLibraryItem.rssFeed = li.rssFeed.toOldJSONMinified()
}
@ -68,7 +68,7 @@ module.exports = {
return {
count,
items: libraryItems.map((li) => {
const oldLibraryItem = Database.libraryItemModel.getOldLibraryItem(li).toJSONMinified()
const oldLibraryItem = li.toOldJSONMinified()
oldLibraryItem.recentEpisode = li.recentEpisode
return oldLibraryItem
})
@ -89,7 +89,7 @@ module.exports = {
const { libraryItems, count } = await libraryItemsBookFilters.getFilteredLibraryItems(library.id, user, 'recent', null, 'addedAt', true, false, include, limit, 0)
return {
libraryItems: libraryItems.map((li) => {
const oldLibraryItem = Database.libraryItemModel.getOldLibraryItem(li).toJSONMinified()
const oldLibraryItem = li.toOldJSONMinified()
if (li.rssFeed) {
oldLibraryItem.rssFeed = li.rssFeed.toOldJSONMinified()
}
@ -107,7 +107,7 @@ module.exports = {
const { libraryItems, count } = await libraryItemsPodcastFilters.getFilteredLibraryItems(library.id, user, 'recent', null, 'addedAt', true, include, limit, 0)
return {
libraryItems: libraryItems.map((li) => {
const oldLibraryItem = Database.libraryItemModel.getOldLibraryItem(li).toJSONMinified()
const oldLibraryItem = li.toOldJSONMinified()
if (li.rssFeed) {
oldLibraryItem.rssFeed = li.rssFeed.toOldJSONMinified()
}
@ -136,7 +136,7 @@ module.exports = {
const { libraryItems, count } = await libraryItemsBookFilters.getContinueSeriesLibraryItems(library, user, include, limit, 0)
return {
libraryItems: libraryItems.map((li) => {
const oldLibraryItem = Database.libraryItemModel.getOldLibraryItem(li).toJSONMinified()
const oldLibraryItem = li.toOldJSONMinified()
if (li.rssFeed) {
oldLibraryItem.rssFeed = li.rssFeed.toOldJSONMinified()
}
@ -166,7 +166,7 @@ module.exports = {
const { libraryItems, count } = await libraryItemsBookFilters.getFilteredLibraryItems(library.id, user, 'progress', 'finished', 'progress', true, false, include, limit, 0)
return {
items: libraryItems.map((li) => {
const oldLibraryItem = Database.libraryItemModel.getOldLibraryItem(li).toJSONMinified()
const oldLibraryItem = li.toOldJSONMinified()
if (li.rssFeed) {
oldLibraryItem.rssFeed = li.rssFeed.toOldJSONMinified()
}
@ -182,7 +182,7 @@ module.exports = {
return {
count,
items: libraryItems.map((li) => {
const oldLibraryItem = Database.libraryItemModel.getOldLibraryItem(li).toJSONMinified()
const oldLibraryItem = li.toOldJSONMinified()
oldLibraryItem.recentEpisode = li.recentEpisode
return oldLibraryItem
})
@ -293,15 +293,17 @@ module.exports = {
})
oldSeries.books = s.bookSeries
.map((bs) => {
const libraryItem = bs.book.libraryItem?.toJSON()
const libraryItem = bs.book.libraryItem
if (!libraryItem) {
Logger.warn(`Book series book has no libraryItem`, bs, bs.book, 'series=', series)
return null
}
delete bs.book.libraryItem
bs.book.authors = [] // Not needed
bs.book.series = [] // Not needed
libraryItem.media = bs.book
const oldLibraryItem = Database.libraryItemModel.getOldLibraryItem(libraryItem).toJSONMinified()
const oldLibraryItem = libraryItem.toOldJSONMinified()
return oldLibraryItem
})
.filter((b) => b)
@ -373,7 +375,7 @@ module.exports = {
const { libraryItems, count } = await libraryItemsBookFilters.getDiscoverLibraryItems(library.id, user, include, limit)
return {
libraryItems: libraryItems.map((li) => {
const oldLibraryItem = Database.libraryItemModel.getOldLibraryItem(li).toJSONMinified()
const oldLibraryItem = li.toOldJSONMinified()
if (li.rssFeed) {
oldLibraryItem.rssFeed = li.rssFeed.toOldJSONMinified()
}
@ -400,7 +402,7 @@ module.exports = {
return {
count,
libraryItems: libraryItems.map((li) => {
const oldLibraryItem = Database.libraryItemModel.getOldLibraryItem(li).toJSONMinified()
const oldLibraryItem = li.toOldJSONMinified()
oldLibraryItem.recentEpisode = li.recentEpisode
return oldLibraryItem
})

View file

@ -349,7 +349,7 @@ module.exports = {
* @param {number} limit
* @param {number} offset
* @param {boolean} isHomePage for home page shelves
* @returns {object} { libraryItems:LibraryItem[], count:number }
* @returns {{ libraryItems: import('../../models/LibraryItem')[], count: number }}
*/
async getFilteredLibraryItems(libraryId, user, filterGroup, filterValue, sortBy, sortDesc, collapseseries, include, limit, offset, isHomePage = false) {
// TODO: Handle collapse sub-series
@ -583,8 +583,8 @@ module.exports = {
})
const libraryItems = books.map((bookExpanded) => {
const libraryItem = bookExpanded.libraryItem.toJSON()
const book = bookExpanded.toJSON()
const libraryItem = bookExpanded.libraryItem
const book = bookExpanded
if (filterGroup === 'series' && book.series?.length) {
// For showing sequence on book cover when filtering for series
@ -596,27 +596,37 @@ module.exports = {
}
delete book.libraryItem
delete book.authors
delete book.series
book.series =
book.bookSeries?.map((bs) => {
const series = bs.series
delete bs.series
series.bookSeries = bs
return series
}) || []
delete book.bookSeries
book.authors = book.bookAuthors?.map((ba) => ba.author) || []
delete book.bookAuthors
// For showing details of collapsed series
if (collapseseries && book.bookSeries?.length) {
const collapsedSeries = book.bookSeries.find((bs) => collapseSeriesBookSeries.some((cbs) => cbs.id === bs.id))
if (collapseseries && book.series?.length) {
const collapsedSeries = book.series.find((bs) => collapseSeriesBookSeries.some((cbs) => cbs.id === bs.bookSeries.id))
if (collapsedSeries) {
const collapseSeriesObj = collapseSeriesBookSeries.find((csbs) => csbs.id === collapsedSeries.id)
const collapseSeriesObj = collapseSeriesBookSeries.find((csbs) => csbs.id === collapsedSeries.bookSeries.id)
libraryItem.collapsedSeries = {
id: collapsedSeries.series.id,
name: collapsedSeries.series.name,
nameIgnorePrefix: collapsedSeries.series.nameIgnorePrefix,
sequence: collapsedSeries.sequence,
id: collapsedSeries.id,
name: collapsedSeries.name,
nameIgnorePrefix: collapsedSeries.nameIgnorePrefix,
sequence: collapsedSeries.bookSeries.sequence,
numBooks: collapseSeriesObj?.numBooks || 0,
libraryItemIds: collapseSeriesObj?.libraryItemIds || []
}
}
}
if (bookExpanded.libraryItem.feeds?.length) {
libraryItem.rssFeed = bookExpanded.libraryItem.feeds[0]
if (libraryItem.feeds?.length) {
libraryItem.rssFeed = libraryItem.feeds[0]
}
if (includeMediaItemShare) {
@ -646,7 +656,7 @@ module.exports = {
* @param {string[]} include
* @param {number} limit
* @param {number} offset
* @returns {{ libraryItems:import('../../models/LibraryItem')[], count:number }}
* @returns {Promise<{ libraryItems:import('../../models/LibraryItem')[], count:number }>}
*/
async getContinueSeriesLibraryItems(library, user, include, limit, offset) {
const libraryId = library.id
@ -758,16 +768,19 @@ module.exports = {
}
}
const libraryItem = s.bookSeries[bookIndex].book.libraryItem.toJSON()
const book = s.bookSeries[bookIndex].book.toJSON()
const libraryItem = s.bookSeries[bookIndex].book.libraryItem
const book = s.bookSeries[bookIndex].book
delete book.libraryItem
book.series = []
libraryItem.series = {
id: s.id,
name: s.name,
sequence: s.bookSeries[bookIndex].sequence
}
if (s.bookSeries[bookIndex].book.libraryItem.feeds?.length) {
libraryItem.rssFeed = s.bookSeries[bookIndex].book.libraryItem.feeds[0]
if (libraryItem.feeds?.length) {
libraryItem.rssFeed = libraryItem.feeds[0]
}
libraryItem.media = book
return libraryItem
@ -788,7 +801,7 @@ module.exports = {
* @param {import('../../models/User')} user
* @param {string[]} include
* @param {number} limit
* @returns {object} {libraryItems:LibraryItem, count:number}
* @returns {Promise<{ libraryItems: import('../../models/LibraryItem')[], count: number }>}
*/
async getDiscoverLibraryItems(libraryId, user, include, limit) {
const userPermissionBookWhere = this.getUserPermissionBookWhereQuery(user)
@ -895,13 +908,26 @@ module.exports = {
// Step 3: Map books to library items
const libraryItems = books.map((bookExpanded) => {
const libraryItem = bookExpanded.libraryItem.toJSON()
const book = bookExpanded.toJSON()
const libraryItem = bookExpanded.libraryItem
const book = bookExpanded
delete book.libraryItem
book.series =
book.bookSeries?.map((bs) => {
const series = bs.series
delete bs.series
series.bookSeries = bs
return series
}) || []
delete book.bookSeries
book.authors = book.bookAuthors?.map((ba) => ba.author) || []
delete book.bookAuthors
libraryItem.media = book
if (bookExpanded.libraryItem.feeds?.length) {
libraryItem.rssFeed = bookExpanded.libraryItem.feeds[0]
if (libraryItem.feeds?.length) {
libraryItem.rssFeed = libraryItem.feeds[0]
}
return libraryItem
@ -961,11 +987,11 @@ module.exports = {
* Get library items for series
* @param {import('../../models/Series')} series
* @param {import('../../models/User')} [user]
* @returns {Promise<import('../../objects/LibraryItem')[]>}
* @returns {Promise<import('../../models/LibraryItem')[]>}
*/
async getLibraryItemsForSeries(series, user) {
const { libraryItems } = await this.getFilteredLibraryItems(series.libraryId, user, 'series', series.id, null, null, false, [], null, null)
return libraryItems.map((li) => Database.libraryItemModel.getOldLibraryItem(li))
return libraryItems
},
/**
@ -1040,9 +1066,21 @@ module.exports = {
for (const book of books) {
const libraryItem = book.libraryItem
delete book.libraryItem
book.series = book.bookSeries.map((bs) => {
const series = bs.series
delete bs.series
series.bookSeries = bs
return series
})
delete book.bookSeries
book.authors = book.bookAuthors.map((ba) => ba.author)
delete book.bookAuthors
libraryItem.media = book
itemMatches.push({
libraryItem: Database.libraryItemModel.getOldLibraryItem(libraryItem).toJSONExpanded()
libraryItem: libraryItem.toOldJSONExpanded()
})
}
@ -1132,7 +1170,9 @@ module.exports = {
const books = series.bookSeries.map((bs) => {
const libraryItem = bs.book.libraryItem
libraryItem.media = bs.book
return Database.libraryItemModel.getOldLibraryItem(libraryItem).toJSON()
libraryItem.media.authors = []
libraryItem.media.series = []
return libraryItem.toOldJSON()
})
seriesMatches.push({
series: series.toOldJSON(),

View file

@ -107,7 +107,7 @@ module.exports = {
* @param {string[]} include
* @param {number} limit
* @param {number} offset
* @returns {object} { libraryItems:LibraryItem[], count:number }
* @returns {Promise<{ libraryItems: import('../../models/LibraryItem')[], count: number }>}
*/
async getFilteredLibraryItems(libraryId, user, filterGroup, filterValue, sortBy, sortDesc, include, limit, offset) {
const includeRSSFeed = include.includes('rssfeed')
@ -175,16 +175,19 @@ module.exports = {
})
const libraryItems = podcasts.map((podcastExpanded) => {
const libraryItem = podcastExpanded.libraryItem.toJSON()
const podcast = podcastExpanded.toJSON()
const libraryItem = podcastExpanded.libraryItem
const podcast = podcastExpanded
delete podcast.libraryItem
if (podcastExpanded.libraryItem.feeds?.length) {
libraryItem.rssFeed = podcastExpanded.libraryItem.feeds[0]
if (libraryItem.feeds?.length) {
libraryItem.rssFeed = libraryItem.feeds[0]
}
if (podcast.numEpisodesIncomplete) {
libraryItem.numEpisodesIncomplete = podcast.numEpisodesIncomplete
if (podcast.dataValues.numEpisodesIncomplete) {
libraryItem.numEpisodesIncomplete = podcast.dataValues.numEpisodesIncomplete
}
if (podcast.dataValues.numEpisodes) {
podcast.numEpisodes = podcast.dataValues.numEpisodes
}
libraryItem.media = podcast
@ -209,7 +212,7 @@ module.exports = {
* @param {number} limit
* @param {number} offset
* @param {boolean} isHomePage for home page shelves
* @returns {object} {libraryItems:LibraryItem[], count:number}
* @returns {Promise<{ libraryItems: import('../../models/LibraryItem')[], count: number }>}
*/
async getFilteredPodcastEpisodes(libraryId, user, filterGroup, filterValue, sortBy, sortDesc, limit, offset, isHomePage = false) {
if (sortBy === 'progress' && filterGroup !== 'progress') {
@ -289,10 +292,11 @@ module.exports = {
})
const libraryItems = podcastEpisodes.map((ep) => {
const libraryItem = ep.podcast.libraryItem.toJSON()
const podcast = ep.podcast.toJSON()
const libraryItem = ep.podcast.libraryItem
const podcast = ep.podcast
delete podcast.libraryItem
libraryItem.media = podcast
libraryItem.recentEpisode = ep.getOldPodcastEpisode(libraryItem.id).toJSON()
return libraryItem
})
@ -362,8 +366,9 @@ module.exports = {
const libraryItem = podcast.libraryItem
delete podcast.libraryItem
libraryItem.media = podcast
libraryItem.media.podcastEpisodes = []
itemMatches.push({
libraryItem: Database.libraryItemModel.getOldLibraryItem(libraryItem).toJSONExpanded()
libraryItem: libraryItem.toOldJSONExpanded()
})
}

View file

@ -162,6 +162,12 @@ module.exports = {
include: [
{
model: Database.libraryItemModel
},
{
model: Database.authorModel
},
{
model: Database.seriesModel
}
]
},
@ -195,10 +201,10 @@ module.exports = {
})
})
oldSeries.books = s.bookSeries.map((bs) => {
const libraryItem = bs.book.libraryItem.toJSON()
const libraryItem = bs.book.libraryItem
delete bs.book.libraryItem
libraryItem.media = bs.book
const oldLibraryItem = Database.libraryItemModel.getOldLibraryItem(libraryItem).toJSONMinified()
const oldLibraryItem = libraryItem.toOldJSONMinified()
return oldLibraryItem
})
allOldSeries.push(oldSeries)