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

This commit is contained in:
lukeIam 2023-04-14 20:27:43 +02:00
commit 812395b21b
90 changed files with 3469 additions and 1148 deletions

View file

@ -1,5 +1,6 @@
class DeviceInfo {
constructor(deviceInfo = null) {
this.deviceId = null
this.ipAddress = null
// From User Agent (see: https://www.npmjs.com/package/ua-parser-js)
@ -32,6 +33,7 @@ class DeviceInfo {
toJSON() {
const obj = {
deviceId: this.deviceId,
ipAddress: this.ipAddress,
browserName: this.browserName,
browserVersion: this.browserVersion,
@ -60,23 +62,42 @@ class DeviceInfo {
return `${this.osName} ${this.osVersion} / ${this.browserName}`
}
// When client doesn't send a device id
getTempDeviceId() {
const keys = [
this.browserName,
this.browserVersion,
this.osName,
this.osVersion,
this.clientVersion,
this.manufacturer,
this.model,
this.sdkVersion,
this.ipAddress
].map(k => k || '')
return 'temp-' + Buffer.from(keys.join('-'), 'utf-8').toString('base64')
}
setData(ip, ua, clientDeviceInfo, serverVersion) {
this.deviceId = clientDeviceInfo?.deviceId || null
this.ipAddress = ip || null
const uaObj = ua || {}
this.browserName = uaObj.browser.name || null
this.browserVersion = uaObj.browser.version || null
this.osName = uaObj.os.name || null
this.osVersion = uaObj.os.version || null
this.deviceType = uaObj.device.type || null
this.browserName = ua?.browser.name || null
this.browserVersion = ua?.browser.version || null
this.osName = ua?.os.name || null
this.osVersion = ua?.os.version || null
this.deviceType = ua?.device.type || null
const cdi = clientDeviceInfo || {}
this.clientVersion = cdi.clientVersion || null
this.manufacturer = cdi.manufacturer || null
this.model = cdi.model || null
this.sdkVersion = cdi.sdkVersion || null
this.clientVersion = clientDeviceInfo?.clientVersion || null
this.manufacturer = clientDeviceInfo?.manufacturer || null
this.model = clientDeviceInfo?.model || null
this.sdkVersion = clientDeviceInfo?.sdkVersion || null
this.serverVersion = serverVersion || null
if (!this.deviceId) {
this.deviceId = this.getTempDeviceId()
}
}
}
module.exports = DeviceInfo

View file

@ -55,7 +55,7 @@ class PlaybackSession {
libraryItemId: this.libraryItemId,
episodeId: this.episodeId,
mediaType: this.mediaType,
mediaMetadata: this.mediaMetadata ? this.mediaMetadata.toJSON() : null,
mediaMetadata: this.mediaMetadata?.toJSON() || null,
chapters: (this.chapters || []).map(c => ({ ...c })),
displayTitle: this.displayTitle,
displayAuthor: this.displayAuthor,
@ -63,7 +63,7 @@ class PlaybackSession {
duration: this.duration,
playMethod: this.playMethod,
mediaPlayer: this.mediaPlayer,
deviceInfo: this.deviceInfo ? this.deviceInfo.toJSON() : null,
deviceInfo: this.deviceInfo?.toJSON() || null,
date: this.date,
dayOfWeek: this.dayOfWeek,
timeListening: this.timeListening,
@ -82,7 +82,7 @@ class PlaybackSession {
libraryItemId: this.libraryItemId,
episodeId: this.episodeId,
mediaType: this.mediaType,
mediaMetadata: this.mediaMetadata ? this.mediaMetadata.toJSON() : null,
mediaMetadata: this.mediaMetadata?.toJSON() || null,
chapters: (this.chapters || []).map(c => ({ ...c })),
displayTitle: this.displayTitle,
displayAuthor: this.displayAuthor,
@ -90,7 +90,7 @@ class PlaybackSession {
duration: this.duration,
playMethod: this.playMethod,
mediaPlayer: this.mediaPlayer,
deviceInfo: this.deviceInfo ? this.deviceInfo.toJSON() : null,
deviceInfo: this.deviceInfo?.toJSON() || null,
date: this.date,
dayOfWeek: this.dayOfWeek,
timeListening: this.timeListening,
@ -151,6 +151,10 @@ class PlaybackSession {
return Math.max(0, Math.min(this.currentTime / this.duration, 1))
}
get deviceId() {
return this.deviceInfo?.deviceId
}
get deviceDescription() {
if (!this.deviceInfo) return 'No Device Info'
return this.deviceInfo.deviceDescription

View file

@ -41,8 +41,12 @@ class PodcastEpisodeDownload {
}
}
get urlFileExtension() {
const cleanUrl = this.url.split('?')[0] // Remove query string
return Path.extname(cleanUrl).substring(1).toLowerCase()
}
get fileExtension() {
const extname = Path.extname(this.url).substring(1).toLowerCase()
const extname = this.urlFileExtension
if (globals.SupportedAudioTypes.includes(extname)) return extname
return 'mp3'
}

View file

@ -1,5 +1,6 @@
const Path = require('path')
const { getId, cleanStringForSearch } = require('../../utils/index')
const Logger = require('../../Logger')
const { getId, cleanStringForSearch, areEquivalent, copyValue } = require('../../utils/index')
const AudioFile = require('../files/AudioFile')
const AudioTrack = require('../files/AudioTrack')
@ -17,6 +18,7 @@ class PodcastEpisode {
this.description = null
this.enclosure = null
this.pubDate = null
this.chapters = []
this.audioFile = null
this.publishedAt = null
@ -40,6 +42,7 @@ class PodcastEpisode {
this.description = episode.description
this.enclosure = episode.enclosure ? { ...episode.enclosure } : null
this.pubDate = episode.pubDate
this.chapters = episode.chapters?.map(ch => ({ ...ch })) || []
this.audioFile = new AudioFile(episode.audioFile)
this.publishedAt = episode.publishedAt
this.addedAt = episode.addedAt
@ -61,6 +64,7 @@ class PodcastEpisode {
description: this.description,
enclosure: this.enclosure ? { ...this.enclosure } : null,
pubDate: this.pubDate,
chapters: this.chapters.map(ch => ({ ...ch })),
audioFile: this.audioFile.toJSON(),
publishedAt: this.publishedAt,
addedAt: this.addedAt,
@ -81,6 +85,7 @@ class PodcastEpisode {
description: this.description,
enclosure: this.enclosure ? { ...this.enclosure } : null,
pubDate: this.pubDate,
chapters: this.chapters.map(ch => ({ ...ch })),
audioFile: this.audioFile.toJSON(),
audioTrack: this.audioTrack.toJSON(),
publishedAt: this.publishedAt,
@ -106,6 +111,10 @@ class PodcastEpisode {
get enclosureUrl() {
return this.enclosure ? this.enclosure.url : null
}
get pubYear() {
if (!this.publishedAt) return null
return new Date(this.publishedAt).getFullYear()
}
setData(data, index = 1) {
this.id = getId('ep')
@ -128,6 +137,10 @@ class PodcastEpisode {
this.audioFile = audioFile
this.title = Path.basename(audioFile.metadata.filename, Path.extname(audioFile.metadata.filename))
this.index = index
this.setDataFromAudioMetaTags(audioFile.metaTags, true)
this.chapters = audioFile.chapters?.map((c) => ({ ...c }))
this.addedAt = Date.now()
this.updatedAt = Date.now()
}
@ -135,8 +148,8 @@ class PodcastEpisode {
update(payload) {
let hasUpdates = false
for (const key in this.toJSON()) {
if (payload[key] != undefined && payload[key] != this[key]) {
this[key] = payload[key]
if (payload[key] != undefined && !areEquivalent(payload[key], this[key])) {
this[key] = copyValue(payload[key])
hasUpdates = true
}
}
@ -164,5 +177,76 @@ class PodcastEpisode {
searchQuery(query) {
return cleanStringForSearch(this.title).includes(query)
}
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]}`)
}
}
})
}
}
module.exports = PodcastEpisode

View file

@ -166,7 +166,11 @@ class Podcast {
}
removeFileWithInode(inode) {
this.episodes = this.episodes.filter(ep => ep.ino !== inode)
const hasEpisode = this.episodes.some(ep => ep.audioFile.ino === inode)
if (hasEpisode) {
this.episodes = this.episodes.filter(ep => ep.audioFile.ino !== inode)
}
return hasEpisode
}
findFileWithInode(inode) {
@ -175,6 +179,10 @@ class Podcast {
return null
}
findEpisodeWithInode(inode) {
return this.episodes.find(ep => ep.audioFile.ino === inode)
}
setData(mediaData) {
this.metadata = new PodcastMetadata()
if (mediaData.metadata) {
@ -315,5 +323,13 @@ class Podcast {
getEpisode(episodeId) {
return this.episodes.find(ep => ep.id == episodeId)
}
// Audio file metadata tags map to podcast details
setMetadataFromAudioFile(overrideExistingDetails = false) {
if (!this.episodes.length) return false
const audioFile = this.episodes[0].audioFile
if (!audioFile?.metaTags) return false
return this.metadata.setDataFromAudioMetaTags(audioFile.metaTags, overrideExistingDetails)
}
}
module.exports = Podcast

View file

@ -1,9 +1,12 @@
class AudioMetaTags {
constructor(metadata) {
this.tagAlbum = null
this.tagAlbumSort = null
this.tagArtist = null
this.tagArtistSort = null
this.tagGenre = null
this.tagTitle = null
this.tagTitleSort = null
this.tagSeries = null
this.tagSeriesPart = null
this.tagTrack = null
@ -20,6 +23,9 @@ class AudioMetaTags {
this.tagIsbn = null
this.tagLanguage = null
this.tagASIN = null
this.tagItunesId = null
this.tagPodcastType = null
this.tagEpisodeType = null
this.tagOverdriveMediaMarker = null
this.tagOriginalYear = null
this.tagReleaseCountry = null
@ -94,9 +100,12 @@ class AudioMetaTags {
construct(metadata) {
this.tagAlbum = metadata.tagAlbum || null
this.tagAlbumSort = metadata.tagAlbumSort || null
this.tagArtist = metadata.tagArtist || null
this.tagArtistSort = metadata.tagArtistSort || null
this.tagGenre = metadata.tagGenre || null
this.tagTitle = metadata.tagTitle || null
this.tagTitleSort = metadata.tagTitleSort || null
this.tagSeries = metadata.tagSeries || null
this.tagSeriesPart = metadata.tagSeriesPart || null
this.tagTrack = metadata.tagTrack || null
@ -113,6 +122,9 @@ class AudioMetaTags {
this.tagIsbn = metadata.tagIsbn || null
this.tagLanguage = metadata.tagLanguage || null
this.tagASIN = metadata.tagASIN || null
this.tagItunesId = metadata.tagItunesId || null
this.tagPodcastType = metadata.tagPodcastType || null
this.tagEpisodeType = metadata.tagEpisodeType || null
this.tagOverdriveMediaMarker = metadata.tagOverdriveMediaMarker || null
this.tagOriginalYear = metadata.tagOriginalYear || null
this.tagReleaseCountry = metadata.tagReleaseCountry || null
@ -128,9 +140,12 @@ class AudioMetaTags {
// Data parsed in prober.js
setData(payload) {
this.tagAlbum = payload.file_tag_album || null
this.tagAlbumSort = payload.file_tag_albumsort || null
this.tagArtist = payload.file_tag_artist || null
this.tagArtistSort = payload.file_tag_artistsort || null
this.tagGenre = payload.file_tag_genre || null
this.tagTitle = payload.file_tag_title || null
this.tagTitleSort = payload.file_tag_titlesort || null
this.tagSeries = payload.file_tag_series || null
this.tagSeriesPart = payload.file_tag_seriespart || null
this.tagTrack = payload.file_tag_track || null
@ -147,6 +162,9 @@ class AudioMetaTags {
this.tagIsbn = payload.file_tag_isbn || null
this.tagLanguage = payload.file_tag_language || null
this.tagASIN = payload.file_tag_asin || null
this.tagItunesId = payload.file_tag_itunesid || null
this.tagPodcastType = payload.file_tag_podcasttype || null
this.tagEpisodeType = payload.file_tag_episodetype || null
this.tagOverdriveMediaMarker = payload.file_tag_overdrive_media_marker || null
this.tagOriginalYear = payload.file_tag_originalyear || null
this.tagReleaseCountry = payload.file_tag_releasecountry || null
@ -166,9 +184,12 @@ class AudioMetaTags {
updateData(payload) {
const dataMap = {
tagAlbum: payload.file_tag_album || null,
tagAlbumSort: payload.file_tag_albumsort || null,
tagArtist: payload.file_tag_artist || null,
tagArtistSort: payload.file_tag_artistsort || null,
tagGenre: payload.file_tag_genre || null,
tagTitle: payload.file_tag_title || null,
tagTitleSort: payload.file_tag_titlesort || null,
tagSeries: payload.file_tag_series || null,
tagSeriesPart: payload.file_tag_seriespart || null,
tagTrack: payload.file_tag_track || null,
@ -185,6 +206,9 @@ class AudioMetaTags {
tagIsbn: payload.file_tag_isbn || null,
tagLanguage: payload.file_tag_language || null,
tagASIN: payload.file_tag_asin || null,
tagItunesId: payload.file_tag_itunesid || null,
tagPodcastType: payload.file_tag_podcasttype || null,
tagEpisodeType: payload.file_tag_episodetype || null,
tagOverdriveMediaMarker: payload.file_tag_overdrive_media_marker || null,
tagOriginalYear: payload.file_tag_originalyear || null,
tagReleaseCountry: payload.file_tag_releasecountry || null,

View file

@ -136,5 +136,74 @@ class PodcastMetadata {
}
return hasUpdates
}
setDataFromAudioMetaTags(audioFileMetaTags, overrideExistingDetails = false) {
const MetadataMapArray = [
{
tag: 'tagAlbum',
altTag: 'tagSeries',
key: 'title'
},
{
tag: 'tagArtist',
key: 'author'
},
{
tag: 'tagGenre',
key: 'genres'
},
{
tag: 'tagLanguage',
key: 'language'
},
{
tag: 'tagItunesId',
key: 'itunesId'
},
{
tag: 'tagPodcastType',
key: 'type',
}
]
const updatePayload = {}
MetadataMapArray.forEach((mapping) => {
let value = audioFileMetaTags[mapping.tag]
let tagToUse = mapping.tag
if (!value && mapping.altTag) {
value = audioFileMetaTags[mapping.altTag]
tagToUse = mapping.altTag
}
if (value && typeof value === 'string') {
value = value.trim() // Trim whitespace
if (mapping.key === 'genres' && (!this.genres.length || overrideExistingDetails)) {
updatePayload.genres = this.parseGenresTag(value)
Logger.debug(`[Podcast] Mapping metadata to key ${tagToUse} => ${mapping.key}: ${updatePayload.genres.join(', ')}`)
} else if (!this[mapping.key] || overrideExistingDetails) {
updatePayload[mapping.key] = value
Logger.debug(`[Podcast] Mapping metadata to key ${tagToUse} => ${mapping.key}: ${updatePayload[mapping.key]}`)
}
}
})
if (Object.keys(updatePayload).length) {
return this.update(updatePayload)
}
return false
}
parseGenresTag(genreTag) {
if (!genreTag || !genreTag.length) return []
const separators = ['/', '//', ';']
for (let i = 0; i < separators.length; i++) {
if (genreTag.includes(separators[i])) {
return genreTag.split(separators[i]).map(genre => genre.trim()).filter(g => !!g)
}
}
return [genreTag]
}
}
module.exports = PodcastMetadata

View file

@ -10,6 +10,9 @@ class MediaProgress {
this.isFinished = false
this.hideFromContinueListening = false
this.ebookLocation = null // current cfi tag
this.ebookProgress = null // 0 to 1
this.lastUpdate = null
this.startedAt = null
this.finishedAt = null
@ -29,6 +32,8 @@ class MediaProgress {
currentTime: this.currentTime,
isFinished: this.isFinished,
hideFromContinueListening: this.hideFromContinueListening,
ebookLocation: this.ebookLocation,
ebookProgress: this.ebookProgress,
lastUpdate: this.lastUpdate,
startedAt: this.startedAt,
finishedAt: this.finishedAt
@ -44,13 +49,15 @@ class MediaProgress {
this.currentTime = progress.currentTime
this.isFinished = !!progress.isFinished
this.hideFromContinueListening = !!progress.hideFromContinueListening
this.ebookLocation = progress.ebookLocation || null
this.ebookProgress = progress.ebookProgress
this.lastUpdate = progress.lastUpdate
this.startedAt = progress.startedAt
this.finishedAt = progress.finishedAt || null
}
get inProgress() {
return !this.isFinished && this.progress > 0
return !this.isFinished && (this.progress > 0 || this.ebookLocation != null)
}
setData(libraryItemId, progress, episodeId = null) {
@ -62,6 +69,8 @@ class MediaProgress {
this.currentTime = progress.currentTime || 0
this.isFinished = !!progress.isFinished || this.progress == 1
this.hideFromContinueListening = !!progress.hideFromContinueListening
this.ebookLocation = progress.ebookLocation
this.ebookProgress = Math.min(1, (progress.ebookProgress || 0))
this.lastUpdate = Date.now()
this.finishedAt = null
if (this.isFinished) {

View file

@ -18,7 +18,6 @@ class User {
this.seriesHideFromContinueListening = [] // Series IDs that should not show on home page continue listening
this.bookmarks = []
this.settings = {} // TODO: Remove after mobile release v0.9.61-beta
this.permissions = {}
this.librariesAccessible = [] // Library IDs (Empty if ALL libraries)
this.itemTagsAccessible = [] // Empty if ALL item tags accessible
@ -59,15 +58,6 @@ class User {
return !!this.pash && !!this.pash.length
}
// TODO: Remove after mobile release v0.9.61-beta
getDefaultUserSettings() {
return {
mobileOrderBy: 'recent',
mobileOrderDesc: true,
mobileFilterBy: 'all'
}
}
getDefaultUserPermissions() {
return {
download: true,
@ -94,19 +84,18 @@ class User {
isLocked: this.isLocked,
lastSeen: this.lastSeen,
createdAt: this.createdAt,
settings: this.settings, // TODO: Remove after mobile release v0.9.61-beta
permissions: this.permissions,
librariesAccessible: [...this.librariesAccessible],
itemTagsAccessible: [...this.itemTagsAccessible]
}
}
toJSONForBrowser() {
return {
toJSONForBrowser(hideRootToken = false, minimal = false) {
const json = {
id: this.id,
username: this.username,
type: this.type,
token: this.token,
token: (this.type === 'root' && hideRootToken) ? '' : this.token,
mediaProgress: this.mediaProgress ? this.mediaProgress.map(li => li.toJSON()) : [],
seriesHideFromContinueListening: [...this.seriesHideFromContinueListening],
bookmarks: this.bookmarks ? this.bookmarks.map(b => b.toJSON()) : [],
@ -114,11 +103,15 @@ class User {
isLocked: this.isLocked,
lastSeen: this.lastSeen,
createdAt: this.createdAt,
settings: this.settings, // TODO: Remove after mobile release v0.9.61-beta
permissions: this.permissions,
librariesAccessible: [...this.librariesAccessible],
itemTagsAccessible: [...this.itemTagsAccessible]
}
if (minimal) {
delete json.mediaProgress
delete json.bookmarks
}
return json
}
// Data broadcasted
@ -166,7 +159,6 @@ class User {
this.isLocked = user.type === 'root' ? false : !!user.isLocked
this.lastSeen = user.lastSeen || null
this.createdAt = user.createdAt || Date.now()
this.settings = user.settings || this.getDefaultUserSettings() // TODO: Remove after mobile release v0.9.61-beta
this.permissions = user.permissions || this.getDefaultUserPermissions()
// Upload permission added v1.1.13, make sure root user has upload permissions
if (this.type === 'root' && !this.permissions.upload) this.permissions.upload = true
@ -343,33 +335,6 @@ class User {
return true
}
// TODO: Remove after mobile release v0.9.61-beta
// Returns Boolean If update was made
updateSettings(settings) {
if (!this.settings) {
this.settings = { ...settings }
return true
}
var madeUpdates = false
for (const key in this.settings) {
if (settings[key] !== undefined && this.settings[key] !== settings[key]) {
this.settings[key] = settings[key]
madeUpdates = true
}
}
// Check if new settings update has keys not currently in user settings
for (const key in settings) {
if (settings[key] !== undefined && this.settings[key] === undefined) {
this.settings[key] = settings[key]
madeUpdates = true
}
}
return madeUpdates
}
checkCanAccessLibrary(libraryId) {
if (this.permissions.accessAllLibraries) return true
if (!this.librariesAccessible) return false