mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2026-06-05 18:22:44 +02:00
Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 190a1000d9 | |||
| 455b96d1ab | |||
| 8aaf62f243 | |||
| e6d754113e | |||
| 5f72e30e63 | |||
| 57906540fe | |||
| 726adbb3bf | |||
| f7b7b85673 | |||
| 5646466aa3 | |||
| b38ce41731 | |||
| a8ab8badd5 | |||
| 5eca43082e | |||
| 6fa11934be | |||
| ff7edc32a1 | |||
| 9b8e059efe | |||
| 7486d6345d | |||
| 835490a9fc | |||
| 3b4a5b8785 | |||
| 9a1c773b7a |
@@ -10,9 +10,9 @@
|
|||||||
<p class="text-lg font-semibold mb-4">{{ $strings.HeaderRSSFeedIsOpen }}</p>
|
<p class="text-lg font-semibold mb-4">{{ $strings.HeaderRSSFeedIsOpen }}</p>
|
||||||
|
|
||||||
<div class="w-full relative">
|
<div class="w-full relative">
|
||||||
<ui-text-input v-model="currentFeed.feedUrl" readonly />
|
<ui-text-input :value="feedUrl" readonly />
|
||||||
|
|
||||||
<span class="material-symbols absolute right-2 bottom-2 p-0.5 text-base transition-transform duration-100 text-gray-300 hover:text-white transform hover:scale-125 cursor-pointer" @click="copyToClipboard(currentFeed.feedUrl)">content_copy</span>
|
<span class="material-symbols absolute right-2 bottom-2 p-0.5 text-base transition-transform duration-100 text-gray-300 hover:text-white transform hover:scale-125 cursor-pointer" @click="copyToClipboard(feedUrl)">content_copy</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="currentFeed.meta" class="mt-5">
|
<div v-if="currentFeed.meta" class="mt-5">
|
||||||
@@ -111,8 +111,11 @@ export default {
|
|||||||
userIsAdminOrUp() {
|
userIsAdminOrUp() {
|
||||||
return this.$store.getters['user/getIsAdminOrUp']
|
return this.$store.getters['user/getIsAdminOrUp']
|
||||||
},
|
},
|
||||||
|
feedUrl() {
|
||||||
|
return this.currentFeed ? `${window.origin}${this.$config.routerBasePath}${this.currentFeed.feedUrl}` : ''
|
||||||
|
},
|
||||||
demoFeedUrl() {
|
demoFeedUrl() {
|
||||||
return `${window.origin}/feed/${this.newFeedSlug}`
|
return `${window.origin}${this.$config.routerBasePath}/feed/${this.newFeedSlug}`
|
||||||
},
|
},
|
||||||
isHttp() {
|
isHttp() {
|
||||||
return window.origin.startsWith('http://')
|
return window.origin.startsWith('http://')
|
||||||
|
|||||||
@@ -5,8 +5,8 @@
|
|||||||
<p class="text-lg font-semibold mb-4">{{ $strings.HeaderRSSFeedGeneral }}</p>
|
<p class="text-lg font-semibold mb-4">{{ $strings.HeaderRSSFeedGeneral }}</p>
|
||||||
|
|
||||||
<div class="w-full relative">
|
<div class="w-full relative">
|
||||||
<ui-text-input v-model="feed.feedUrl" readonly />
|
<ui-text-input :value="feedUrl" readonly />
|
||||||
<span class="material-symbols absolute right-2 bottom-2 p-0.5 text-base transition-transform duration-100 text-gray-300 hover:text-white transform hover:scale-125 cursor-pointer" @click="copyToClipboard(feed.feedUrl)">content_copy</span>
|
<span class="material-symbols absolute right-2 bottom-2 p-0.5 text-base transition-transform duration-100 text-gray-300 hover:text-white transform hover:scale-125 cursor-pointer" @click="copyToClipboard(feedUrl)">content_copy</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="feed.meta" class="mt-5">
|
<div v-if="feed.meta" class="mt-5">
|
||||||
@@ -70,6 +70,9 @@ export default {
|
|||||||
},
|
},
|
||||||
_feed() {
|
_feed() {
|
||||||
return this.feed || {}
|
return this.feed || {}
|
||||||
|
},
|
||||||
|
feedUrl() {
|
||||||
|
return this.feed ? `${window.origin}${this.$config.routerBasePath}${this.feed.feedUrl}` : ''
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookshelf-client",
|
"name": "audiobookshelf-client",
|
||||||
"version": "2.17.4",
|
"version": "2.17.5",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "audiobookshelf-client",
|
"name": "audiobookshelf-client",
|
||||||
"version": "2.17.4",
|
"version": "2.17.5",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nuxtjs/axios": "^5.13.6",
|
"@nuxtjs/axios": "^5.13.6",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookshelf-client",
|
"name": "audiobookshelf-client",
|
||||||
"version": "2.17.4",
|
"version": "2.17.5",
|
||||||
"buildNumber": 1,
|
"buildNumber": 1,
|
||||||
"description": "Self-hosted audiobook and podcast client",
|
"description": "Self-hosted audiobook and podcast client",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
|
|||||||
@@ -42,11 +42,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center py-2 mb-2">
|
|
||||||
<ui-toggle-switch labeledBy="settings-chromecast-support" v-model="newServerSettings.chromecastEnabled" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('chromecastEnabled', val)" />
|
|
||||||
<p class="pl-4" id="settings-chromecast-support">{{ $strings.LabelSettingsChromecastSupport }}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="pt-4">
|
<div class="pt-4">
|
||||||
<h2 class="font-semibold">{{ $strings.HeaderSettingsScanner }}</h2>
|
<h2 class="font-semibold">{{ $strings.HeaderSettingsScanner }}</h2>
|
||||||
</div>
|
</div>
|
||||||
@@ -94,6 +89,20 @@
|
|||||||
</p>
|
</p>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="pt-4">
|
||||||
|
<h2 class="font-semibold">{{ $strings.HeaderSettingsWebClient }}</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center py-2">
|
||||||
|
<ui-toggle-switch labeledBy="settings-chromecast-support" v-model="newServerSettings.chromecastEnabled" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('chromecastEnabled', val)" />
|
||||||
|
<p class="pl-4" id="settings-chromecast-support">{{ $strings.LabelSettingsChromecastSupport }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center py-2 mb-2">
|
||||||
|
<ui-toggle-switch labeledBy="settings-allow-iframe" v-model="newServerSettings.allowIframe" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('allowIframe', val)" />
|
||||||
|
<p class="pl-4" id="settings-allow-iframe">{{ $strings.LabelSettingsAllowIframe }}</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
@@ -324,21 +333,21 @@ export default {
|
|||||||
},
|
},
|
||||||
updateServerSettings(payload) {
|
updateServerSettings(payload) {
|
||||||
this.updatingServerSettings = true
|
this.updatingServerSettings = true
|
||||||
this.$store
|
this.$store.dispatch('updateServerSettings', payload).then((response) => {
|
||||||
.dispatch('updateServerSettings', payload)
|
this.updatingServerSettings = false
|
||||||
.then(() => {
|
|
||||||
this.updatingServerSettings = false
|
|
||||||
|
|
||||||
if (payload.language) {
|
if (response.error) {
|
||||||
// Updating language after save allows for re-rendering
|
console.error('Failed to update server settins', response.error)
|
||||||
this.$setLanguageCode(payload.language)
|
this.$toast.error(response.error)
|
||||||
}
|
this.initServerSettings()
|
||||||
})
|
return
|
||||||
.catch((error) => {
|
}
|
||||||
console.error('Failed to update server settings', error)
|
|
||||||
this.updatingServerSettings = false
|
if (payload.language) {
|
||||||
this.$toast.error(this.$strings.ToastFailedToUpdate)
|
// Updating language after save allows for re-rendering
|
||||||
})
|
this.$setLanguageCode(payload.language)
|
||||||
|
}
|
||||||
|
})
|
||||||
},
|
},
|
||||||
initServerSettings() {
|
initServerSettings() {
|
||||||
this.newServerSettings = this.serverSettings ? { ...this.serverSettings } : {}
|
this.newServerSettings = this.serverSettings ? { ...this.serverSettings } : {}
|
||||||
|
|||||||
@@ -126,7 +126,7 @@ export default {
|
|||||||
},
|
},
|
||||||
coverUrl(feed) {
|
coverUrl(feed) {
|
||||||
if (!feed.coverPath) return `${this.$config.routerBasePath}/Logo.png`
|
if (!feed.coverPath) return `${this.$config.routerBasePath}/Logo.png`
|
||||||
return `${feed.feedUrl}/cover`
|
return `${this.$config.routerBasePath}${feed.feedUrl}/cover`
|
||||||
},
|
},
|
||||||
async loadFeeds() {
|
async loadFeeds() {
|
||||||
const data = await this.$axios.$get(`/api/feeds`).catch((err) => {
|
const data = await this.$axios.$get(`/api/feeds`).catch((err) => {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ const defaultCode = 'en-us'
|
|||||||
const languageCodeMap = {
|
const languageCodeMap = {
|
||||||
bg: { label: 'Български', dateFnsLocale: 'bg' },
|
bg: { label: 'Български', dateFnsLocale: 'bg' },
|
||||||
bn: { label: 'বাংলা', dateFnsLocale: 'bn' },
|
bn: { label: 'বাংলা', dateFnsLocale: 'bn' },
|
||||||
|
ca: { label: 'Català', dateFnsLocale: 'ca' },
|
||||||
cs: { label: 'Čeština', dateFnsLocale: 'cs' },
|
cs: { label: 'Čeština', dateFnsLocale: 'cs' },
|
||||||
da: { label: 'Dansk', dateFnsLocale: 'da' },
|
da: { label: 'Dansk', dateFnsLocale: 'da' },
|
||||||
de: { label: 'Deutsch', dateFnsLocale: 'de' },
|
de: { label: 'Deutsch', dateFnsLocale: 'de' },
|
||||||
|
|||||||
@@ -72,16 +72,17 @@ export const actions = {
|
|||||||
return this.$axios
|
return this.$axios
|
||||||
.$patch('/api/settings', updatePayload)
|
.$patch('/api/settings', updatePayload)
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
if (result.success) {
|
if (result.serverSettings) {
|
||||||
commit('setServerSettings', result.serverSettings)
|
commit('setServerSettings', result.serverSettings)
|
||||||
return true
|
|
||||||
} else {
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
return result
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed to update server settings', error)
|
console.error('Failed to update server settings', error)
|
||||||
return false
|
const errorMsg = error.response?.data || 'Unknown error'
|
||||||
|
return {
|
||||||
|
error: errorMsg
|
||||||
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
checkForUpdate({ commit }) {
|
checkForUpdate({ commit }) {
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -679,6 +679,8 @@
|
|||||||
"LabelViewPlayerSettings": "Zeige player Einstellungen",
|
"LabelViewPlayerSettings": "Zeige player Einstellungen",
|
||||||
"LabelViewQueue": "Player-Warteschlange anzeigen",
|
"LabelViewQueue": "Player-Warteschlange anzeigen",
|
||||||
"LabelVolume": "Lautstärke",
|
"LabelVolume": "Lautstärke",
|
||||||
|
"LabelWebRedirectURLsDescription": "Autorisieren Sie diese URLs bei ihrem OAuth-Anbieter, um die Weiterleitung zurück zur Webanwendung nach dem Login zu ermöglichen:",
|
||||||
|
"LabelWebRedirectURLsSubfolder": "Unterordner für Weiterleitung-URLs",
|
||||||
"LabelWeekdaysToRun": "Wochentage für die Ausführung",
|
"LabelWeekdaysToRun": "Wochentage für die Ausführung",
|
||||||
"LabelXBooks": "{0} Bücher",
|
"LabelXBooks": "{0} Bücher",
|
||||||
"LabelXItems": "{0} Medien",
|
"LabelXItems": "{0} Medien",
|
||||||
|
|||||||
@@ -190,6 +190,7 @@
|
|||||||
"HeaderSettingsExperimental": "Experimental Features",
|
"HeaderSettingsExperimental": "Experimental Features",
|
||||||
"HeaderSettingsGeneral": "General",
|
"HeaderSettingsGeneral": "General",
|
||||||
"HeaderSettingsScanner": "Scanner",
|
"HeaderSettingsScanner": "Scanner",
|
||||||
|
"HeaderSettingsWebClient": "Web Client",
|
||||||
"HeaderSleepTimer": "Sleep Timer",
|
"HeaderSleepTimer": "Sleep Timer",
|
||||||
"HeaderStatsLargestItems": "Largest Items",
|
"HeaderStatsLargestItems": "Largest Items",
|
||||||
"HeaderStatsLongestItems": "Longest Items (hrs)",
|
"HeaderStatsLongestItems": "Longest Items (hrs)",
|
||||||
@@ -542,6 +543,7 @@
|
|||||||
"LabelServerYearReview": "Server Year in Review ({0})",
|
"LabelServerYearReview": "Server Year in Review ({0})",
|
||||||
"LabelSetEbookAsPrimary": "Set as primary",
|
"LabelSetEbookAsPrimary": "Set as primary",
|
||||||
"LabelSetEbookAsSupplementary": "Set as supplementary",
|
"LabelSetEbookAsSupplementary": "Set as supplementary",
|
||||||
|
"LabelSettingsAllowIframe": "Allow embedding in an iframe",
|
||||||
"LabelSettingsAudiobooksOnly": "Audiobooks only",
|
"LabelSettingsAudiobooksOnly": "Audiobooks only",
|
||||||
"LabelSettingsAudiobooksOnlyHelp": "Enabling this setting will ignore ebook files unless they are inside an audiobook folder in which case they will be set as supplementary ebooks",
|
"LabelSettingsAudiobooksOnlyHelp": "Enabling this setting will ignore ebook files unless they are inside an audiobook folder in which case they will be set as supplementary ebooks",
|
||||||
"LabelSettingsBookshelfViewHelp": "Skeumorphic design with wooden shelves",
|
"LabelSettingsBookshelfViewHelp": "Skeumorphic design with wooden shelves",
|
||||||
|
|||||||
@@ -679,6 +679,8 @@
|
|||||||
"LabelViewPlayerSettings": "Ogled nastavitev predvajalnika",
|
"LabelViewPlayerSettings": "Ogled nastavitev predvajalnika",
|
||||||
"LabelViewQueue": "Ogled čakalno vrsto predvajalnika",
|
"LabelViewQueue": "Ogled čakalno vrsto predvajalnika",
|
||||||
"LabelVolume": "Glasnost",
|
"LabelVolume": "Glasnost",
|
||||||
|
"LabelWebRedirectURLsDescription": "Avtorizirajte URL-je pri svojem ponudniku OAuth ter s tem omogočite preusmeritev nazaj v spletno aplikacijo po prijavi:",
|
||||||
|
"LabelWebRedirectURLsSubfolder": "Podmapa za URL-je preusmeritve",
|
||||||
"LabelWeekdaysToRun": "Delovni dnevi predvajanja",
|
"LabelWeekdaysToRun": "Delovni dnevi predvajanja",
|
||||||
"LabelXBooks": "{0} knjig",
|
"LabelXBooks": "{0} knjig",
|
||||||
"LabelXItems": "{0} elementov",
|
"LabelXItems": "{0} elementov",
|
||||||
|
|||||||
@@ -679,6 +679,8 @@
|
|||||||
"LabelViewPlayerSettings": "Переглянути налаштування програвача",
|
"LabelViewPlayerSettings": "Переглянути налаштування програвача",
|
||||||
"LabelViewQueue": "Переглянути чергу відтворення",
|
"LabelViewQueue": "Переглянути чергу відтворення",
|
||||||
"LabelVolume": "Гучність",
|
"LabelVolume": "Гучність",
|
||||||
|
"LabelWebRedirectURLsDescription": "Авторизуйте ці URL у вашому OAuth постачальнику, щоб дозволити редирекцію назад до веб-додатку після входу:",
|
||||||
|
"LabelWebRedirectURLsSubfolder": "Підпапка для Redirect URL",
|
||||||
"LabelWeekdaysToRun": "Виконувати у дні",
|
"LabelWeekdaysToRun": "Виконувати у дні",
|
||||||
"LabelXBooks": "{0} книг",
|
"LabelXBooks": "{0} книг",
|
||||||
"LabelXItems": "{0} елементів",
|
"LabelXItems": "{0} елементів",
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ if (isDev) {
|
|||||||
if (devEnv.FFProbePath) process.env.FFPROBE_PATH = devEnv.FFProbePath
|
if (devEnv.FFProbePath) process.env.FFPROBE_PATH = devEnv.FFProbePath
|
||||||
if (devEnv.NunicodePath) process.env.NUSQLITE3_PATH = devEnv.NunicodePath
|
if (devEnv.NunicodePath) process.env.NUSQLITE3_PATH = devEnv.NunicodePath
|
||||||
if (devEnv.SkipBinariesCheck) process.env.SKIP_BINARIES_CHECK = '1'
|
if (devEnv.SkipBinariesCheck) process.env.SKIP_BINARIES_CHECK = '1'
|
||||||
|
if (devEnv.AllowIframe) process.env.ALLOW_IFRAME = '1'
|
||||||
if (devEnv.BackupPath) process.env.BACKUP_PATH = devEnv.BackupPath
|
if (devEnv.BackupPath) process.env.BACKUP_PATH = devEnv.BackupPath
|
||||||
process.env.SOURCE = 'local'
|
process.env.SOURCE = 'local'
|
||||||
process.env.ROUTER_BASE_PATH = devEnv.RouterBasePath || ''
|
process.env.ROUTER_BASE_PATH = devEnv.RouterBasePath || ''
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookshelf",
|
"name": "audiobookshelf",
|
||||||
"version": "2.17.4",
|
"version": "2.17.5",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "audiobookshelf",
|
"name": "audiobookshelf",
|
||||||
"version": "2.17.4",
|
"version": "2.17.5",
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^0.27.2",
|
"axios": "^0.27.2",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookshelf",
|
"name": "audiobookshelf",
|
||||||
"version": "2.17.4",
|
"version": "2.17.5",
|
||||||
"buildNumber": 1,
|
"buildNumber": 1,
|
||||||
"description": "Self-hosted audiobook and podcast server",
|
"description": "Self-hosted audiobook and podcast server",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
|
|||||||
+15
-10
@@ -194,8 +194,10 @@ class Server {
|
|||||||
const app = express()
|
const app = express()
|
||||||
|
|
||||||
app.use((req, res, next) => {
|
app.use((req, res, next) => {
|
||||||
// Prevent clickjacking by disallowing iframes
|
if (!global.ServerSettings.allowIframe) {
|
||||||
res.setHeader('Content-Security-Policy', "frame-ancestors 'self'")
|
// Prevent clickjacking by disallowing iframes
|
||||||
|
res.setHeader('Content-Security-Policy', "frame-ancestors 'self'")
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @temporary
|
* @temporary
|
||||||
@@ -248,14 +250,17 @@ class Server {
|
|||||||
|
|
||||||
const router = express.Router()
|
const router = express.Router()
|
||||||
// if RouterBasePath is set, modify all requests to include the base path
|
// if RouterBasePath is set, modify all requests to include the base path
|
||||||
if (global.RouterBasePath) {
|
app.use((req, res, next) => {
|
||||||
app.use((req, res, next) => {
|
const urlStartsWithRouterBasePath = req.url.startsWith(global.RouterBasePath)
|
||||||
if (!req.url.startsWith(global.RouterBasePath)) {
|
const host = req.get('host')
|
||||||
req.url = `${global.RouterBasePath}${req.url}`
|
const protocol = req.secure || req.get('x-forwarded-proto') === 'https' ? 'https' : 'http'
|
||||||
}
|
const prefix = urlStartsWithRouterBasePath ? global.RouterBasePath : ''
|
||||||
next()
|
req.originalHostPrefix = `${protocol}://${host}${prefix}`
|
||||||
})
|
if (!urlStartsWithRouterBasePath) {
|
||||||
}
|
req.url = `${global.RouterBasePath}${req.url}`
|
||||||
|
}
|
||||||
|
next()
|
||||||
|
})
|
||||||
app.use(global.RouterBasePath, router)
|
app.use(global.RouterBasePath, router)
|
||||||
app.disable('x-powered-by')
|
app.disable('x-powered-by')
|
||||||
|
|
||||||
|
|||||||
@@ -126,6 +126,10 @@ class MiscController {
|
|||||||
if (!isObject(settingsUpdate)) {
|
if (!isObject(settingsUpdate)) {
|
||||||
return res.status(400).send('Invalid settings update object')
|
return res.status(400).send('Invalid settings update object')
|
||||||
}
|
}
|
||||||
|
if (settingsUpdate.allowIframe == false && process.env.ALLOW_IFRAME === '1') {
|
||||||
|
Logger.warn('Cannot disable iframe when ALLOW_IFRAME is enabled in environment')
|
||||||
|
return res.status(400).send('Cannot disable iframe when ALLOW_IFRAME is enabled in environment')
|
||||||
|
}
|
||||||
|
|
||||||
const madeUpdates = Database.serverSettings.update(settingsUpdate)
|
const madeUpdates = Database.serverSettings.update(settingsUpdate)
|
||||||
if (madeUpdates) {
|
if (madeUpdates) {
|
||||||
@@ -137,7 +141,6 @@ class MiscController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return res.json({
|
return res.json({
|
||||||
success: true,
|
|
||||||
serverSettings: Database.serverSettings.toJSONForBrowser()
|
serverSettings: Database.serverSettings.toJSONForBrowser()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ const parseEbookMetadata = require('../utils/parsers/parseEbookMetadata')
|
|||||||
const CacheManager = require('../managers/CacheManager')
|
const CacheManager = require('../managers/CacheManager')
|
||||||
|
|
||||||
class CoverManager {
|
class CoverManager {
|
||||||
constructor() { }
|
constructor() {}
|
||||||
|
|
||||||
getCoverDirectory(libraryItem) {
|
getCoverDirectory(libraryItem) {
|
||||||
if (global.ServerSettings.storeCoverWithItem && !libraryItem.isFile) {
|
if (global.ServerSettings.storeCoverWithItem && !libraryItem.isFile) {
|
||||||
@@ -93,10 +93,13 @@ class CoverManager {
|
|||||||
const coverFullPath = Path.posix.join(coverDirPath, `cover${extname}`)
|
const coverFullPath = Path.posix.join(coverDirPath, `cover${extname}`)
|
||||||
|
|
||||||
// Move cover from temp upload dir to destination
|
// Move cover from temp upload dir to destination
|
||||||
const success = await coverFile.mv(coverFullPath).then(() => true).catch((error) => {
|
const success = await coverFile
|
||||||
Logger.error('[CoverManager] Failed to move cover file', path, error)
|
.mv(coverFullPath)
|
||||||
return false
|
.then(() => true)
|
||||||
})
|
.catch((error) => {
|
||||||
|
Logger.error('[CoverManager] Failed to move cover file', coverFullPath, error)
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
|
||||||
if (!success) {
|
if (!success) {
|
||||||
return {
|
return {
|
||||||
@@ -124,11 +127,13 @@ class CoverManager {
|
|||||||
var temppath = Path.posix.join(coverDirPath, 'cover')
|
var temppath = Path.posix.join(coverDirPath, 'cover')
|
||||||
|
|
||||||
let errorMsg = ''
|
let errorMsg = ''
|
||||||
let success = await downloadImageFile(url, temppath).then(() => true).catch((err) => {
|
let success = await downloadImageFile(url, temppath)
|
||||||
errorMsg = err.message || 'Unknown error'
|
.then(() => true)
|
||||||
Logger.error(`[CoverManager] Download image file failed for "${url}"`, errorMsg)
|
.catch((err) => {
|
||||||
return false
|
errorMsg = err.message || 'Unknown error'
|
||||||
})
|
Logger.error(`[CoverManager] Download image file failed for "${url}"`, errorMsg)
|
||||||
|
return false
|
||||||
|
})
|
||||||
if (!success) {
|
if (!success) {
|
||||||
return {
|
return {
|
||||||
error: 'Failed to download image from url: ' + errorMsg
|
error: 'Failed to download image from url: ' + errorMsg
|
||||||
@@ -180,7 +185,7 @@ class CoverManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Cover path does not exist
|
// Cover path does not exist
|
||||||
if (!await fs.pathExists(coverPath)) {
|
if (!(await fs.pathExists(coverPath))) {
|
||||||
Logger.error(`[CoverManager] validate cover path does not exist "${coverPath}"`)
|
Logger.error(`[CoverManager] validate cover path does not exist "${coverPath}"`)
|
||||||
return {
|
return {
|
||||||
error: 'Cover path does not exist'
|
error: 'Cover path does not exist'
|
||||||
@@ -188,7 +193,7 @@ class CoverManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Cover path is not a file
|
// Cover path is not a file
|
||||||
if (!await checkPathIsFile(coverPath)) {
|
if (!(await checkPathIsFile(coverPath))) {
|
||||||
Logger.error(`[CoverManager] validate cover path is not a file "${coverPath}"`)
|
Logger.error(`[CoverManager] validate cover path is not a file "${coverPath}"`)
|
||||||
return {
|
return {
|
||||||
error: 'Cover path is not a file'
|
error: 'Cover path is not a file'
|
||||||
@@ -211,10 +216,13 @@ class CoverManager {
|
|||||||
var newCoverPath = Path.posix.join(coverDirPath, coverFilename)
|
var newCoverPath = Path.posix.join(coverDirPath, coverFilename)
|
||||||
Logger.debug(`[CoverManager] validate cover path copy cover from "${coverPath}" to "${newCoverPath}"`)
|
Logger.debug(`[CoverManager] validate cover path copy cover from "${coverPath}" to "${newCoverPath}"`)
|
||||||
|
|
||||||
var copySuccess = await fs.copy(coverPath, newCoverPath, { overwrite: true }).then(() => true).catch((error) => {
|
var copySuccess = await fs
|
||||||
Logger.error(`[CoverManager] validate cover path failed to copy cover`, error)
|
.copy(coverPath, newCoverPath, { overwrite: true })
|
||||||
return false
|
.then(() => true)
|
||||||
})
|
.catch((error) => {
|
||||||
|
Logger.error(`[CoverManager] validate cover path failed to copy cover`, error)
|
||||||
|
return false
|
||||||
|
})
|
||||||
if (!copySuccess) {
|
if (!copySuccess) {
|
||||||
return {
|
return {
|
||||||
error: 'Failed to copy cover to dir'
|
error: 'Failed to copy cover to dir'
|
||||||
@@ -236,14 +244,14 @@ class CoverManager {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Extract cover art from audio file and save for library item
|
* Extract cover art from audio file and save for library item
|
||||||
*
|
*
|
||||||
* @param {import('../models/Book').AudioFileObject[]} audioFiles
|
* @param {import('../models/Book').AudioFileObject[]} audioFiles
|
||||||
* @param {string} libraryItemId
|
* @param {string} libraryItemId
|
||||||
* @param {string} [libraryItemPath] null for isFile library items
|
* @param {string} [libraryItemPath] null for isFile library items
|
||||||
* @returns {Promise<string>} returns cover path
|
* @returns {Promise<string>} returns cover path
|
||||||
*/
|
*/
|
||||||
async saveEmbeddedCoverArt(audioFiles, libraryItemId, libraryItemPath) {
|
async saveEmbeddedCoverArt(audioFiles, libraryItemId, libraryItemPath) {
|
||||||
let audioFileWithCover = audioFiles.find(af => af.embeddedCoverArt)
|
let audioFileWithCover = audioFiles.find((af) => af.embeddedCoverArt)
|
||||||
if (!audioFileWithCover) return null
|
if (!audioFileWithCover) return null
|
||||||
|
|
||||||
let coverDirPath = null
|
let coverDirPath = null
|
||||||
@@ -273,10 +281,10 @@ class CoverManager {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Extract cover art from ebook and save for library item
|
* Extract cover art from ebook and save for library item
|
||||||
*
|
*
|
||||||
* @param {import('../utils/parsers/parseEbookMetadata').EBookFileScanData} ebookFileScanData
|
* @param {import('../utils/parsers/parseEbookMetadata').EBookFileScanData} ebookFileScanData
|
||||||
* @param {string} libraryItemId
|
* @param {string} libraryItemId
|
||||||
* @param {string} [libraryItemPath] null for isFile library items
|
* @param {string} [libraryItemPath] null for isFile library items
|
||||||
* @returns {Promise<string>} returns cover path
|
* @returns {Promise<string>} returns cover path
|
||||||
*/
|
*/
|
||||||
async saveEbookCoverArt(ebookFileScanData, libraryItemId, libraryItemPath) {
|
async saveEbookCoverArt(ebookFileScanData, libraryItemId, libraryItemPath) {
|
||||||
@@ -310,9 +318,9 @@ class CoverManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {string} url
|
* @param {string} url
|
||||||
* @param {string} libraryItemId
|
* @param {string} libraryItemId
|
||||||
* @param {string} [libraryItemPath] null if library item isFile or is from adding new podcast
|
* @param {string} [libraryItemPath] null if library item isFile or is from adding new podcast
|
||||||
* @returns {Promise<{error:string}|{cover:string}>}
|
* @returns {Promise<{error:string}|{cover:string}>}
|
||||||
*/
|
*/
|
||||||
@@ -328,10 +336,12 @@ class CoverManager {
|
|||||||
await fs.ensureDir(coverDirPath)
|
await fs.ensureDir(coverDirPath)
|
||||||
|
|
||||||
const temppath = Path.posix.join(coverDirPath, 'cover')
|
const temppath = Path.posix.join(coverDirPath, 'cover')
|
||||||
const success = await downloadImageFile(url, temppath).then(() => true).catch((err) => {
|
const success = await downloadImageFile(url, temppath)
|
||||||
Logger.error(`[CoverManager] Download image file failed for "${url}"`, err)
|
.then(() => true)
|
||||||
return false
|
.catch((err) => {
|
||||||
})
|
Logger.error(`[CoverManager] Download image file failed for "${url}"`, err)
|
||||||
|
return false
|
||||||
|
})
|
||||||
if (!success) {
|
if (!success) {
|
||||||
return {
|
return {
|
||||||
error: 'Failed to download image from url'
|
error: 'Failed to download image from url'
|
||||||
@@ -361,4 +371,4 @@ class CoverManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
module.exports = new CoverManager()
|
module.exports = new CoverManager()
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
const { Request, Response } = require('express')
|
||||||
const Path = require('path')
|
const Path = require('path')
|
||||||
|
|
||||||
const Logger = require('../Logger')
|
const Logger = require('../Logger')
|
||||||
@@ -77,6 +78,12 @@ class RssFeedManager {
|
|||||||
return Database.feedModel.findByPkOld(id)
|
return Database.feedModel.findByPkOld(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET: /feed/:slug
|
||||||
|
*
|
||||||
|
* @param {Request} req
|
||||||
|
* @param {Response} res
|
||||||
|
*/
|
||||||
async getFeed(req, res) {
|
async getFeed(req, res) {
|
||||||
const feed = await this.findFeedBySlug(req.params.slug)
|
const feed = await this.findFeedBySlug(req.params.slug)
|
||||||
if (!feed) {
|
if (!feed) {
|
||||||
@@ -162,11 +169,17 @@ class RssFeedManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const xml = feed.buildXml()
|
const xml = feed.buildXml(req.originalHostPrefix)
|
||||||
res.set('Content-Type', 'text/xml')
|
res.set('Content-Type', 'text/xml')
|
||||||
res.send(xml)
|
res.send(xml)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET: /feed/:slug/item/:episodeId/*
|
||||||
|
*
|
||||||
|
* @param {Request} req
|
||||||
|
* @param {Response} res
|
||||||
|
*/
|
||||||
async getFeedItem(req, res) {
|
async getFeedItem(req, res) {
|
||||||
const feed = await this.findFeedBySlug(req.params.slug)
|
const feed = await this.findFeedBySlug(req.params.slug)
|
||||||
if (!feed) {
|
if (!feed) {
|
||||||
@@ -183,6 +196,12 @@ class RssFeedManager {
|
|||||||
res.sendFile(episodePath)
|
res.sendFile(episodePath)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET: /feed/:slug/cover*
|
||||||
|
*
|
||||||
|
* @param {Request} req
|
||||||
|
* @param {Response} res
|
||||||
|
*/
|
||||||
async getFeedCover(req, res) {
|
async getFeedCover(req, res) {
|
||||||
const feed = await this.findFeedBySlug(req.params.slug)
|
const feed = await this.findFeedBySlug(req.params.slug)
|
||||||
if (!feed) {
|
if (!feed) {
|
||||||
|
|||||||
@@ -10,3 +10,4 @@ Please add a record of every database migration that you create to this file. Th
|
|||||||
| v2.17.0 | v2.17.0-uuid-replacement | Changes the data type of columns with UUIDv4 to UUID matching the associated model |
|
| v2.17.0 | v2.17.0-uuid-replacement | Changes the data type of columns with UUIDv4 to UUID matching the associated model |
|
||||||
| v2.17.3 | v2.17.3-fk-constraints | Changes the foreign key constraints for tables due to sequelize bug dropping constraints in v2.17.0 migration |
|
| v2.17.3 | v2.17.3-fk-constraints | Changes the foreign key constraints for tables due to sequelize bug dropping constraints in v2.17.0 migration |
|
||||||
| v2.17.4 | v2.17.4-use-subfolder-for-oidc-redirect-uris | Save subfolder to OIDC redirect URIs to support existing installations |
|
| v2.17.4 | v2.17.4-use-subfolder-for-oidc-redirect-uris | Save subfolder to OIDC redirect URIs to support existing installations |
|
||||||
|
| v2.17.5 | v2.17.5-remove-host-from-feed-urls | removes the host (serverAddress) from URL columns in the feeds and feedEpisodes tables |
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
/**
|
||||||
|
* @typedef MigrationContext
|
||||||
|
* @property {import('sequelize').QueryInterface} queryInterface - a suquelize QueryInterface object.
|
||||||
|
* @property {import('../Logger')} logger - a Logger object.
|
||||||
|
*
|
||||||
|
* @typedef MigrationOptions
|
||||||
|
* @property {MigrationContext} context - an object containing the migration context.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const migrationVersion = '2.17.5'
|
||||||
|
const migrationName = `${migrationVersion}-remove-host-from-feed-urls`
|
||||||
|
const loggerPrefix = `[${migrationVersion} migration]`
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This upward migration removes the host (serverAddress) from URL columns in the feeds and feedEpisodes tables.
|
||||||
|
*
|
||||||
|
* @param {MigrationOptions} options - an object containing the migration context.
|
||||||
|
* @returns {Promise<void>} - A promise that resolves when the migration is complete.
|
||||||
|
*/
|
||||||
|
async function up({ context: { queryInterface, logger } }) {
|
||||||
|
// Upwards migration script
|
||||||
|
logger.info(`${loggerPrefix} UPGRADE BEGIN: ${migrationName}`)
|
||||||
|
|
||||||
|
logger.info(`${loggerPrefix} Removing serverAddress from Feeds table URLs`)
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
UPDATE Feeds
|
||||||
|
SET feedUrl = REPLACE(feedUrl, COALESCE(serverAddress, ''), ''),
|
||||||
|
imageUrl = REPLACE(imageUrl, COALESCE(serverAddress, ''), ''),
|
||||||
|
siteUrl = REPLACE(siteUrl, COALESCE(serverAddress, ''), '');
|
||||||
|
`)
|
||||||
|
logger.info(`${loggerPrefix} Removed serverAddress from Feeds table URLs`)
|
||||||
|
|
||||||
|
logger.info(`${loggerPrefix} Removing serverAddress from FeedEpisodes table URLs`)
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
UPDATE FeedEpisodes
|
||||||
|
SET siteUrl = REPLACE(siteUrl, (SELECT COALESCE(serverAddress, '') FROM Feeds WHERE Feeds.id = FeedEpisodes.feedId), ''),
|
||||||
|
enclosureUrl = REPLACE(enclosureUrl, (SELECT COALESCE(serverAddress, '') FROM Feeds WHERE Feeds.id = FeedEpisodes.feedId), '');
|
||||||
|
`)
|
||||||
|
logger.info(`${loggerPrefix} Removed serverAddress from FeedEpisodes table URLs`)
|
||||||
|
|
||||||
|
logger.info(`${loggerPrefix} UPGRADE END: ${migrationName}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This downward migration script adds the host (serverAddress) back to URL columns in the feeds and feedEpisodes tables.
|
||||||
|
*
|
||||||
|
* @param {MigrationOptions} options - an object containing the migration context.
|
||||||
|
* @returns {Promise<void>} - A promise that resolves when the migration is complete.
|
||||||
|
*/
|
||||||
|
async function down({ context: { queryInterface, logger } }) {
|
||||||
|
// Downward migration script
|
||||||
|
logger.info(`${loggerPrefix} DOWNGRADE BEGIN: ${migrationName}`)
|
||||||
|
|
||||||
|
logger.info(`${loggerPrefix} Adding serverAddress back to Feeds table URLs`)
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
UPDATE Feeds
|
||||||
|
SET feedUrl = COALESCE(serverAddress, '') || feedUrl,
|
||||||
|
imageUrl = COALESCE(serverAddress, '') || imageUrl,
|
||||||
|
siteUrl = COALESCE(serverAddress, '') || siteUrl;
|
||||||
|
`)
|
||||||
|
logger.info(`${loggerPrefix} Added serverAddress back to Feeds table URLs`)
|
||||||
|
|
||||||
|
logger.info(`${loggerPrefix} Adding serverAddress back to FeedEpisodes table URLs`)
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
UPDATE FeedEpisodes
|
||||||
|
SET siteUrl = (SELECT COALESCE(serverAddress, '') || FeedEpisodes.siteUrl FROM Feeds WHERE Feeds.id = FeedEpisodes.feedId),
|
||||||
|
enclosureUrl = (SELECT COALESCE(serverAddress, '') || FeedEpisodes.enclosureUrl FROM Feeds WHERE Feeds.id = FeedEpisodes.feedId);
|
||||||
|
`)
|
||||||
|
logger.info(`${loggerPrefix} Added serverAddress back to FeedEpisodes table URLs`)
|
||||||
|
|
||||||
|
logger.info(`${loggerPrefix} DOWNGRADE END: ${migrationName}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { up, down }
|
||||||
+16
-25
@@ -29,9 +29,6 @@ class Feed {
|
|||||||
this.createdAt = null
|
this.createdAt = null
|
||||||
this.updatedAt = null
|
this.updatedAt = null
|
||||||
|
|
||||||
// Cached xml
|
|
||||||
this.xml = null
|
|
||||||
|
|
||||||
if (feed) {
|
if (feed) {
|
||||||
this.construct(feed)
|
this.construct(feed)
|
||||||
}
|
}
|
||||||
@@ -109,7 +106,7 @@ class Feed {
|
|||||||
const mediaMetadata = media.metadata
|
const mediaMetadata = media.metadata
|
||||||
const isPodcast = libraryItem.mediaType === 'podcast'
|
const isPodcast = libraryItem.mediaType === 'podcast'
|
||||||
|
|
||||||
const feedUrl = `${serverAddress}/feed/${slug}`
|
const feedUrl = `/feed/${slug}`
|
||||||
const author = isPodcast ? mediaMetadata.author : mediaMetadata.authorName
|
const author = isPodcast ? mediaMetadata.author : mediaMetadata.authorName
|
||||||
|
|
||||||
this.id = uuidv4()
|
this.id = uuidv4()
|
||||||
@@ -128,9 +125,9 @@ class Feed {
|
|||||||
this.meta.title = mediaMetadata.title
|
this.meta.title = mediaMetadata.title
|
||||||
this.meta.description = mediaMetadata.description
|
this.meta.description = mediaMetadata.description
|
||||||
this.meta.author = author
|
this.meta.author = author
|
||||||
this.meta.imageUrl = media.coverPath ? `${serverAddress}/feed/${slug}/cover${coverFileExtension}` : `${serverAddress}/Logo.png`
|
this.meta.imageUrl = media.coverPath ? `/feed/${slug}/cover${coverFileExtension}` : `/Logo.png`
|
||||||
this.meta.feedUrl = feedUrl
|
this.meta.feedUrl = feedUrl
|
||||||
this.meta.link = `${serverAddress}/item/${libraryItem.id}`
|
this.meta.link = `/item/${libraryItem.id}`
|
||||||
this.meta.explicit = !!mediaMetadata.explicit
|
this.meta.explicit = !!mediaMetadata.explicit
|
||||||
this.meta.type = mediaMetadata.type
|
this.meta.type = mediaMetadata.type
|
||||||
this.meta.language = mediaMetadata.language
|
this.meta.language = mediaMetadata.language
|
||||||
@@ -176,7 +173,7 @@ class Feed {
|
|||||||
this.meta.title = mediaMetadata.title
|
this.meta.title = mediaMetadata.title
|
||||||
this.meta.description = mediaMetadata.description
|
this.meta.description = mediaMetadata.description
|
||||||
this.meta.author = author
|
this.meta.author = author
|
||||||
this.meta.imageUrl = media.coverPath ? `${this.serverAddress}/feed/${this.slug}/cover${coverFileExtension}` : `${this.serverAddress}/Logo.png`
|
this.meta.imageUrl = media.coverPath ? `/feed/${this.slug}/cover${coverFileExtension}` : `/Logo.png`
|
||||||
this.meta.explicit = !!mediaMetadata.explicit
|
this.meta.explicit = !!mediaMetadata.explicit
|
||||||
this.meta.type = mediaMetadata.type
|
this.meta.type = mediaMetadata.type
|
||||||
this.meta.language = mediaMetadata.language
|
this.meta.language = mediaMetadata.language
|
||||||
@@ -202,11 +199,10 @@ class Feed {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.updatedAt = Date.now()
|
this.updatedAt = Date.now()
|
||||||
this.xml = null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setFromCollection(userId, slug, collectionExpanded, serverAddress, preventIndexing = true, ownerName = null, ownerEmail = null) {
|
setFromCollection(userId, slug, collectionExpanded, serverAddress, preventIndexing = true, ownerName = null, ownerEmail = null) {
|
||||||
const feedUrl = `${serverAddress}/feed/${slug}`
|
const feedUrl = `/feed/${slug}`
|
||||||
|
|
||||||
const itemsWithTracks = collectionExpanded.books.filter((libraryItem) => libraryItem.media.tracks.length)
|
const itemsWithTracks = collectionExpanded.books.filter((libraryItem) => libraryItem.media.tracks.length)
|
||||||
const firstItemWithCover = itemsWithTracks.find((item) => item.media.coverPath)
|
const firstItemWithCover = itemsWithTracks.find((item) => item.media.coverPath)
|
||||||
@@ -227,9 +223,9 @@ class Feed {
|
|||||||
this.meta.title = collectionExpanded.name
|
this.meta.title = collectionExpanded.name
|
||||||
this.meta.description = collectionExpanded.description || ''
|
this.meta.description = collectionExpanded.description || ''
|
||||||
this.meta.author = this.getAuthorsStringFromLibraryItems(itemsWithTracks)
|
this.meta.author = this.getAuthorsStringFromLibraryItems(itemsWithTracks)
|
||||||
this.meta.imageUrl = this.coverPath ? `${serverAddress}/feed/${slug}/cover${coverFileExtension}` : `${serverAddress}/Logo.png`
|
this.meta.imageUrl = this.coverPath ? `/feed/${slug}/cover${coverFileExtension}` : `/Logo.png`
|
||||||
this.meta.feedUrl = feedUrl
|
this.meta.feedUrl = feedUrl
|
||||||
this.meta.link = `${serverAddress}/collection/${collectionExpanded.id}`
|
this.meta.link = `/collection/${collectionExpanded.id}`
|
||||||
this.meta.explicit = !!itemsWithTracks.some((li) => li.media.metadata.explicit) // explicit if any item is explicit
|
this.meta.explicit = !!itemsWithTracks.some((li) => li.media.metadata.explicit) // explicit if any item is explicit
|
||||||
this.meta.preventIndexing = preventIndexing
|
this.meta.preventIndexing = preventIndexing
|
||||||
this.meta.ownerName = ownerName
|
this.meta.ownerName = ownerName
|
||||||
@@ -272,7 +268,7 @@ class Feed {
|
|||||||
this.meta.title = collectionExpanded.name
|
this.meta.title = collectionExpanded.name
|
||||||
this.meta.description = collectionExpanded.description || ''
|
this.meta.description = collectionExpanded.description || ''
|
||||||
this.meta.author = this.getAuthorsStringFromLibraryItems(itemsWithTracks)
|
this.meta.author = this.getAuthorsStringFromLibraryItems(itemsWithTracks)
|
||||||
this.meta.imageUrl = this.coverPath ? `${this.serverAddress}/feed/${this.slug}/cover${coverFileExtension}` : `${this.serverAddress}/Logo.png`
|
this.meta.imageUrl = this.coverPath ? `/feed/${this.slug}/cover${coverFileExtension}` : `/Logo.png`
|
||||||
this.meta.explicit = !!itemsWithTracks.some((li) => li.media.metadata.explicit) // explicit if any item is explicit
|
this.meta.explicit = !!itemsWithTracks.some((li) => li.media.metadata.explicit) // explicit if any item is explicit
|
||||||
|
|
||||||
this.episodes = []
|
this.episodes = []
|
||||||
@@ -297,11 +293,10 @@ class Feed {
|
|||||||
})
|
})
|
||||||
|
|
||||||
this.updatedAt = Date.now()
|
this.updatedAt = Date.now()
|
||||||
this.xml = null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setFromSeries(userId, slug, seriesExpanded, serverAddress, preventIndexing = true, ownerName = null, ownerEmail = null) {
|
setFromSeries(userId, slug, seriesExpanded, serverAddress, preventIndexing = true, ownerName = null, ownerEmail = null) {
|
||||||
const feedUrl = `${serverAddress}/feed/${slug}`
|
const feedUrl = `/feed/${slug}`
|
||||||
|
|
||||||
let itemsWithTracks = seriesExpanded.books.filter((libraryItem) => libraryItem.media.tracks.length)
|
let itemsWithTracks = seriesExpanded.books.filter((libraryItem) => libraryItem.media.tracks.length)
|
||||||
// Sort series items by series sequence
|
// Sort series items by series sequence
|
||||||
@@ -326,9 +321,9 @@ class Feed {
|
|||||||
this.meta.title = seriesExpanded.name
|
this.meta.title = seriesExpanded.name
|
||||||
this.meta.description = seriesExpanded.description || ''
|
this.meta.description = seriesExpanded.description || ''
|
||||||
this.meta.author = this.getAuthorsStringFromLibraryItems(itemsWithTracks)
|
this.meta.author = this.getAuthorsStringFromLibraryItems(itemsWithTracks)
|
||||||
this.meta.imageUrl = this.coverPath ? `${serverAddress}/feed/${slug}/cover${coverFileExtension}` : `${serverAddress}/Logo.png`
|
this.meta.imageUrl = this.coverPath ? `/feed/${slug}/cover${coverFileExtension}` : `/Logo.png`
|
||||||
this.meta.feedUrl = feedUrl
|
this.meta.feedUrl = feedUrl
|
||||||
this.meta.link = `${serverAddress}/library/${libraryId}/series/${seriesExpanded.id}`
|
this.meta.link = `/library/${libraryId}/series/${seriesExpanded.id}`
|
||||||
this.meta.explicit = !!itemsWithTracks.some((li) => li.media.metadata.explicit) // explicit if any item is explicit
|
this.meta.explicit = !!itemsWithTracks.some((li) => li.media.metadata.explicit) // explicit if any item is explicit
|
||||||
this.meta.preventIndexing = preventIndexing
|
this.meta.preventIndexing = preventIndexing
|
||||||
this.meta.ownerName = ownerName
|
this.meta.ownerName = ownerName
|
||||||
@@ -374,7 +369,7 @@ class Feed {
|
|||||||
this.meta.title = seriesExpanded.name
|
this.meta.title = seriesExpanded.name
|
||||||
this.meta.description = seriesExpanded.description || ''
|
this.meta.description = seriesExpanded.description || ''
|
||||||
this.meta.author = this.getAuthorsStringFromLibraryItems(itemsWithTracks)
|
this.meta.author = this.getAuthorsStringFromLibraryItems(itemsWithTracks)
|
||||||
this.meta.imageUrl = this.coverPath ? `${this.serverAddress}/feed/${this.slug}/cover${coverFileExtension}` : `${this.serverAddress}/Logo.png`
|
this.meta.imageUrl = this.coverPath ? `/feed/${this.slug}/cover${coverFileExtension}` : `/Logo.png`
|
||||||
this.meta.explicit = !!itemsWithTracks.some((li) => li.media.metadata.explicit) // explicit if any item is explicit
|
this.meta.explicit = !!itemsWithTracks.some((li) => li.media.metadata.explicit) // explicit if any item is explicit
|
||||||
|
|
||||||
this.episodes = []
|
this.episodes = []
|
||||||
@@ -399,18 +394,14 @@ class Feed {
|
|||||||
})
|
})
|
||||||
|
|
||||||
this.updatedAt = Date.now()
|
this.updatedAt = Date.now()
|
||||||
this.xml = null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
buildXml() {
|
buildXml(originalHostPrefix) {
|
||||||
if (this.xml) return this.xml
|
var rssfeed = new RSS(this.meta.getRSSData(originalHostPrefix))
|
||||||
|
|
||||||
var rssfeed = new RSS(this.meta.getRSSData())
|
|
||||||
this.episodes.forEach((ep) => {
|
this.episodes.forEach((ep) => {
|
||||||
rssfeed.item(ep.getRSSData())
|
rssfeed.item(ep.getRSSData(originalHostPrefix))
|
||||||
})
|
})
|
||||||
this.xml = rssfeed.xml()
|
return rssfeed.xml()
|
||||||
return this.xml
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getAuthorsStringFromLibraryItems(libraryItems) {
|
getAuthorsStringFromLibraryItems(libraryItems) {
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ class FeedEpisode {
|
|||||||
this.title = episode.title
|
this.title = episode.title
|
||||||
this.description = episode.description || ''
|
this.description = episode.description || ''
|
||||||
this.enclosure = {
|
this.enclosure = {
|
||||||
url: `${serverAddress}${contentUrl}`,
|
url: `${contentUrl}`,
|
||||||
type: episode.audioTrack.mimeType,
|
type: episode.audioTrack.mimeType,
|
||||||
size: episode.size
|
size: episode.size
|
||||||
}
|
}
|
||||||
@@ -136,7 +136,7 @@ class FeedEpisode {
|
|||||||
this.title = title
|
this.title = title
|
||||||
this.description = mediaMetadata.description || ''
|
this.description = mediaMetadata.description || ''
|
||||||
this.enclosure = {
|
this.enclosure = {
|
||||||
url: `${serverAddress}${contentUrl}`,
|
url: `${contentUrl}`,
|
||||||
type: audioTrack.mimeType,
|
type: audioTrack.mimeType,
|
||||||
size: audioTrack.metadata.size
|
size: audioTrack.metadata.size
|
||||||
}
|
}
|
||||||
@@ -151,15 +151,19 @@ class FeedEpisode {
|
|||||||
this.fullPath = audioTrack.metadata.path
|
this.fullPath = audioTrack.metadata.path
|
||||||
}
|
}
|
||||||
|
|
||||||
getRSSData() {
|
getRSSData(hostPrefix) {
|
||||||
return {
|
return {
|
||||||
title: this.title,
|
title: this.title,
|
||||||
description: this.description || '',
|
description: this.description || '',
|
||||||
url: this.link,
|
url: `${hostPrefix}${this.link}`,
|
||||||
guid: this.enclosure.url,
|
guid: `${hostPrefix}${this.enclosure.url}`,
|
||||||
author: this.author,
|
author: this.author,
|
||||||
date: this.pubDate,
|
date: this.pubDate,
|
||||||
enclosure: this.enclosure,
|
enclosure: {
|
||||||
|
url: `${hostPrefix}${this.enclosure.url}`,
|
||||||
|
type: this.enclosure.type,
|
||||||
|
size: this.enclosure.size
|
||||||
|
},
|
||||||
custom_elements: [
|
custom_elements: [
|
||||||
{ 'itunes:author': this.author },
|
{ 'itunes:author': this.author },
|
||||||
{ 'itunes:duration': secondsToTimestamp(this.duration) },
|
{ 'itunes:duration': secondsToTimestamp(this.duration) },
|
||||||
|
|||||||
+13
-19
@@ -60,42 +60,36 @@ class FeedMeta {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getRSSData() {
|
getRSSData(hostPrefix) {
|
||||||
const blockTags = [
|
const blockTags = [{ 'itunes:block': 'yes' }, { 'googleplay:block': 'yes' }]
|
||||||
{ 'itunes:block': 'yes' },
|
|
||||||
{ 'googleplay:block': 'yes' }
|
|
||||||
]
|
|
||||||
return {
|
return {
|
||||||
title: this.title,
|
title: this.title,
|
||||||
description: this.description || '',
|
description: this.description || '',
|
||||||
generator: 'Audiobookshelf',
|
generator: 'Audiobookshelf',
|
||||||
feed_url: this.feedUrl,
|
feed_url: `${hostPrefix}${this.feedUrl}`,
|
||||||
site_url: this.link,
|
site_url: `${hostPrefix}${this.link}`,
|
||||||
image_url: this.imageUrl,
|
image_url: `${hostPrefix}${this.imageUrl}`,
|
||||||
custom_namespaces: {
|
custom_namespaces: {
|
||||||
'itunes': 'http://www.itunes.com/dtds/podcast-1.0.dtd',
|
itunes: 'http://www.itunes.com/dtds/podcast-1.0.dtd',
|
||||||
'psc': 'http://podlove.org/simple-chapters',
|
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'
|
googleplay: 'http://www.google.com/schemas/play-podcasts/1.0'
|
||||||
},
|
},
|
||||||
custom_elements: [
|
custom_elements: [
|
||||||
{ 'language': this.language || 'en' },
|
{ language: this.language || 'en' },
|
||||||
{ 'author': this.author || 'advplyr' },
|
{ author: this.author || 'advplyr' },
|
||||||
{ 'itunes:author': this.author || 'advplyr' },
|
{ 'itunes:author': this.author || 'advplyr' },
|
||||||
{ 'itunes:summary': this.description || '' },
|
{ 'itunes:summary': this.description || '' },
|
||||||
{ 'itunes:type': this.type },
|
{ 'itunes:type': this.type },
|
||||||
{
|
{
|
||||||
'itunes:image': {
|
'itunes:image': {
|
||||||
_attr: {
|
_attr: {
|
||||||
href: this.imageUrl
|
href: `${hostPrefix}${this.imageUrl}`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'itunes:owner': [
|
'itunes:owner': [{ 'itunes:name': this.ownerName || this.author || '' }, { 'itunes:email': this.ownerEmail || '' }]
|
||||||
{ 'itunes:name': this.ownerName || this.author || '' },
|
|
||||||
{ 'itunes:email': this.ownerEmail || '' }
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
{ 'itunes:explicit': !!this.explicit },
|
{ 'itunes:explicit': !!this.explicit },
|
||||||
...(this.preventIndexing ? blockTags : [])
|
...(this.preventIndexing ? blockTags : [])
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ class ServerSettings {
|
|||||||
// Security/Rate limits
|
// Security/Rate limits
|
||||||
this.rateLimitLoginRequests = 10
|
this.rateLimitLoginRequests = 10
|
||||||
this.rateLimitLoginWindow = 10 * 60 * 1000 // 10 Minutes
|
this.rateLimitLoginWindow = 10 * 60 * 1000 // 10 Minutes
|
||||||
|
this.allowIframe = false
|
||||||
|
|
||||||
// Backups
|
// Backups
|
||||||
this.backupPath = Path.join(global.MetadataPath, 'backups')
|
this.backupPath = Path.join(global.MetadataPath, 'backups')
|
||||||
@@ -99,6 +100,7 @@ class ServerSettings {
|
|||||||
|
|
||||||
this.rateLimitLoginRequests = !isNaN(settings.rateLimitLoginRequests) ? Number(settings.rateLimitLoginRequests) : 10
|
this.rateLimitLoginRequests = !isNaN(settings.rateLimitLoginRequests) ? Number(settings.rateLimitLoginRequests) : 10
|
||||||
this.rateLimitLoginWindow = !isNaN(settings.rateLimitLoginWindow) ? Number(settings.rateLimitLoginWindow) : 10 * 60 * 1000 // 10 Minutes
|
this.rateLimitLoginWindow = !isNaN(settings.rateLimitLoginWindow) ? Number(settings.rateLimitLoginWindow) : 10 * 60 * 1000 // 10 Minutes
|
||||||
|
this.allowIframe = !!settings.allowIframe
|
||||||
|
|
||||||
this.backupPath = settings.backupPath || Path.join(global.MetadataPath, 'backups')
|
this.backupPath = settings.backupPath || Path.join(global.MetadataPath, 'backups')
|
||||||
this.backupSchedule = settings.backupSchedule || false
|
this.backupSchedule = settings.backupSchedule || false
|
||||||
@@ -190,6 +192,11 @@ class ServerSettings {
|
|||||||
Logger.info(`[ServerSettings] Using backup path from environment variable ${process.env.BACKUP_PATH}`)
|
Logger.info(`[ServerSettings] Using backup path from environment variable ${process.env.BACKUP_PATH}`)
|
||||||
this.backupPath = process.env.BACKUP_PATH
|
this.backupPath = process.env.BACKUP_PATH
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (process.env.ALLOW_IFRAME === '1' && !this.allowIframe) {
|
||||||
|
Logger.info(`[ServerSettings] Using allowIframe from environment variable`)
|
||||||
|
this.allowIframe = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
toJSON() {
|
toJSON() {
|
||||||
@@ -207,6 +214,7 @@ class ServerSettings {
|
|||||||
metadataFileFormat: this.metadataFileFormat,
|
metadataFileFormat: this.metadataFileFormat,
|
||||||
rateLimitLoginRequests: this.rateLimitLoginRequests,
|
rateLimitLoginRequests: this.rateLimitLoginRequests,
|
||||||
rateLimitLoginWindow: this.rateLimitLoginWindow,
|
rateLimitLoginWindow: this.rateLimitLoginWindow,
|
||||||
|
allowIframe: this.allowIframe,
|
||||||
backupPath: this.backupPath,
|
backupPath: this.backupPath,
|
||||||
backupSchedule: this.backupSchedule,
|
backupSchedule: this.backupSchedule,
|
||||||
backupsToKeep: this.backupsToKeep,
|
backupsToKeep: this.backupsToKeep,
|
||||||
|
|||||||
@@ -127,20 +127,20 @@ module.exports = {
|
|||||||
bookListeningMap[ls.displayTitle] += listeningSessionListeningTime
|
bookListeningMap[ls.displayTitle] += listeningSessionListeningTime
|
||||||
}
|
}
|
||||||
|
|
||||||
const authors = ls.mediaMetadata.authors || []
|
const authors = ls.mediaMetadata?.authors || []
|
||||||
authors.forEach((au) => {
|
authors.forEach((au) => {
|
||||||
if (!authorListeningMap[au.name]) authorListeningMap[au.name] = 0
|
if (!authorListeningMap[au.name]) authorListeningMap[au.name] = 0
|
||||||
authorListeningMap[au.name] += listeningSessionListeningTime
|
authorListeningMap[au.name] += listeningSessionListeningTime
|
||||||
})
|
})
|
||||||
|
|
||||||
const narrators = ls.mediaMetadata.narrators || []
|
const narrators = ls.mediaMetadata?.narrators || []
|
||||||
narrators.forEach((narrator) => {
|
narrators.forEach((narrator) => {
|
||||||
if (!narratorListeningMap[narrator]) narratorListeningMap[narrator] = 0
|
if (!narratorListeningMap[narrator]) narratorListeningMap[narrator] = 0
|
||||||
narratorListeningMap[narrator] += listeningSessionListeningTime
|
narratorListeningMap[narrator] += listeningSessionListeningTime
|
||||||
})
|
})
|
||||||
|
|
||||||
// Filter out bad genres like "audiobook" and "audio book"
|
// Filter out bad genres like "audiobook" and "audio book"
|
||||||
const genres = (ls.mediaMetadata.genres || []).filter((g) => g && !g.toLowerCase().includes('audiobook') && !g.toLowerCase().includes('audio book'))
|
const genres = (ls.mediaMetadata?.genres || []).filter((g) => g && !g.toLowerCase().includes('audiobook') && !g.toLowerCase().includes('audio book'))
|
||||||
genres.forEach((genre) => {
|
genres.forEach((genre) => {
|
||||||
if (!genreListeningMap[genre]) genreListeningMap[genre] = 0
|
if (!genreListeningMap[genre]) genreListeningMap[genre] = 0
|
||||||
genreListeningMap[genre] += listeningSessionListeningTime
|
genreListeningMap[genre] += listeningSessionListeningTime
|
||||||
|
|||||||
@@ -0,0 +1,202 @@
|
|||||||
|
const { expect } = require('chai')
|
||||||
|
const sinon = require('sinon')
|
||||||
|
const { up, down } = require('../../../server/migrations/v2.17.5-remove-host-from-feed-urls')
|
||||||
|
const { Sequelize, DataTypes } = require('sequelize')
|
||||||
|
const Logger = require('../../../server/Logger')
|
||||||
|
|
||||||
|
const defineModels = (sequelize) => {
|
||||||
|
const Feeds = sequelize.define('Feeds', {
|
||||||
|
id: { type: DataTypes.UUID, primaryKey: true, defaultValue: DataTypes.UUIDV4 },
|
||||||
|
feedUrl: { type: DataTypes.STRING },
|
||||||
|
imageUrl: { type: DataTypes.STRING },
|
||||||
|
siteUrl: { type: DataTypes.STRING },
|
||||||
|
serverAddress: { type: DataTypes.STRING }
|
||||||
|
})
|
||||||
|
|
||||||
|
const FeedEpisodes = sequelize.define('FeedEpisodes', {
|
||||||
|
id: { type: DataTypes.UUID, primaryKey: true, defaultValue: DataTypes.UUIDV4 },
|
||||||
|
feedId: { type: DataTypes.UUID },
|
||||||
|
siteUrl: { type: DataTypes.STRING },
|
||||||
|
enclosureUrl: { type: DataTypes.STRING }
|
||||||
|
})
|
||||||
|
|
||||||
|
return { Feeds, FeedEpisodes }
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('Migration v2.17.4-use-subfolder-for-oidc-redirect-uris', () => {
|
||||||
|
let queryInterface, logger, context
|
||||||
|
let sequelize
|
||||||
|
let Feeds, FeedEpisodes
|
||||||
|
const feed1Id = '00000000-0000-4000-a000-000000000001'
|
||||||
|
const feed2Id = '00000000-0000-4000-a000-000000000002'
|
||||||
|
const feedEpisode1Id = '00000000-4000-a000-0000-000000000011'
|
||||||
|
const feedEpisode2Id = '00000000-4000-a000-0000-000000000012'
|
||||||
|
const feedEpisode3Id = '00000000-4000-a000-0000-000000000021'
|
||||||
|
|
||||||
|
before(async () => {
|
||||||
|
sequelize = new Sequelize({ dialect: 'sqlite', storage: ':memory:', logging: false })
|
||||||
|
queryInterface = sequelize.getQueryInterface()
|
||||||
|
;({ Feeds, FeedEpisodes } = defineModels(sequelize))
|
||||||
|
await sequelize.sync()
|
||||||
|
})
|
||||||
|
|
||||||
|
after(async () => {
|
||||||
|
await sequelize.close()
|
||||||
|
})
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
// Reset tables before each test
|
||||||
|
await Feeds.destroy({ where: {}, truncate: true })
|
||||||
|
await FeedEpisodes.destroy({ where: {}, truncate: true })
|
||||||
|
|
||||||
|
logger = {
|
||||||
|
info: sinon.stub(),
|
||||||
|
error: sinon.stub()
|
||||||
|
}
|
||||||
|
context = { queryInterface, logger }
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('up', () => {
|
||||||
|
it('should remove serverAddress from URLs in Feeds and FeedEpisodes tables', async () => {
|
||||||
|
await Feeds.bulkCreate([
|
||||||
|
{ id: feed1Id, feedUrl: 'http://server1.com/feed1', imageUrl: 'http://server1.com/img1', siteUrl: 'http://server1.com/site1', serverAddress: 'http://server1.com' },
|
||||||
|
{ id: feed2Id, feedUrl: 'http://server2.com/feed2', imageUrl: 'http://server2.com/img2', siteUrl: 'http://server2.com/site2', serverAddress: 'http://server2.com' }
|
||||||
|
])
|
||||||
|
|
||||||
|
await FeedEpisodes.bulkCreate([
|
||||||
|
{ id: feedEpisode1Id, feedId: feed1Id, siteUrl: 'http://server1.com/episode11', enclosureUrl: 'http://server1.com/enclosure11' },
|
||||||
|
{ id: feedEpisode2Id, feedId: feed1Id, siteUrl: 'http://server1.com/episode12', enclosureUrl: 'http://server1.com/enclosure12' },
|
||||||
|
{ id: feedEpisode3Id, feedId: feed2Id, siteUrl: 'http://server2.com/episode21', enclosureUrl: 'http://server2.com/enclosure21' }
|
||||||
|
])
|
||||||
|
|
||||||
|
await up({ context })
|
||||||
|
const feeds = await Feeds.findAll({ raw: true })
|
||||||
|
const feedEpisodes = await FeedEpisodes.findAll({ raw: true })
|
||||||
|
|
||||||
|
expect(logger.info.calledWith('[2.17.5 migration] UPGRADE BEGIN: 2.17.5-remove-host-from-feed-urls')).to.be.true
|
||||||
|
expect(logger.info.calledWith('[2.17.5 migration] Removing serverAddress from Feeds table URLs')).to.be.true
|
||||||
|
|
||||||
|
expect(feeds[0].feedUrl).to.equal('/feed1')
|
||||||
|
expect(feeds[0].imageUrl).to.equal('/img1')
|
||||||
|
expect(feeds[0].siteUrl).to.equal('/site1')
|
||||||
|
expect(feeds[1].feedUrl).to.equal('/feed2')
|
||||||
|
expect(feeds[1].imageUrl).to.equal('/img2')
|
||||||
|
expect(feeds[1].siteUrl).to.equal('/site2')
|
||||||
|
|
||||||
|
expect(logger.info.calledWith('[2.17.5 migration] Removed serverAddress from Feeds table URLs')).to.be.true
|
||||||
|
expect(logger.info.calledWith('[2.17.5 migration] Removing serverAddress from FeedEpisodes table URLs')).to.be.true
|
||||||
|
|
||||||
|
expect(feedEpisodes[0].siteUrl).to.equal('/episode11')
|
||||||
|
expect(feedEpisodes[0].enclosureUrl).to.equal('/enclosure11')
|
||||||
|
expect(feedEpisodes[1].siteUrl).to.equal('/episode12')
|
||||||
|
expect(feedEpisodes[1].enclosureUrl).to.equal('/enclosure12')
|
||||||
|
expect(feedEpisodes[2].siteUrl).to.equal('/episode21')
|
||||||
|
expect(feedEpisodes[2].enclosureUrl).to.equal('/enclosure21')
|
||||||
|
|
||||||
|
expect(logger.info.calledWith('[2.17.5 migration] Removed serverAddress from FeedEpisodes table URLs')).to.be.true
|
||||||
|
expect(logger.info.calledWith('[2.17.5 migration] UPGRADE END: 2.17.5-remove-host-from-feed-urls')).to.be.true
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle null URLs in Feeds and FeedEpisodes tables', async () => {
|
||||||
|
await Feeds.bulkCreate([{ id: feed1Id, feedUrl: 'http://server1.com/feed1', imageUrl: null, siteUrl: 'http://server1.com/site1', serverAddress: 'http://server1.com' }])
|
||||||
|
|
||||||
|
await FeedEpisodes.bulkCreate([{ id: feedEpisode1Id, feedId: feed1Id, siteUrl: null, enclosureUrl: 'http://server1.com/enclosure11' }])
|
||||||
|
|
||||||
|
await up({ context })
|
||||||
|
const feeds = await Feeds.findAll({ raw: true })
|
||||||
|
const feedEpisodes = await FeedEpisodes.findAll({ raw: true })
|
||||||
|
|
||||||
|
expect(feeds[0].feedUrl).to.equal('/feed1')
|
||||||
|
expect(feeds[0].imageUrl).to.be.null
|
||||||
|
expect(feeds[0].siteUrl).to.equal('/site1')
|
||||||
|
expect(feedEpisodes[0].siteUrl).to.be.null
|
||||||
|
expect(feedEpisodes[0].enclosureUrl).to.equal('/enclosure11')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle null serverAddress in Feeds table', async () => {
|
||||||
|
await Feeds.bulkCreate([{ id: feed1Id, feedUrl: 'http://server1.com/feed1', imageUrl: 'http://server1.com/img1', siteUrl: 'http://server1.com/site1', serverAddress: null }])
|
||||||
|
await FeedEpisodes.bulkCreate([{ id: feedEpisode1Id, feedId: feed1Id, siteUrl: 'http://server1.com/episode11', enclosureUrl: 'http://server1.com/enclosure11' }])
|
||||||
|
|
||||||
|
await up({ context })
|
||||||
|
const feeds = await Feeds.findAll({ raw: true })
|
||||||
|
const feedEpisodes = await FeedEpisodes.findAll({ raw: true })
|
||||||
|
|
||||||
|
expect(feeds[0].feedUrl).to.equal('http://server1.com/feed1')
|
||||||
|
expect(feeds[0].imageUrl).to.equal('http://server1.com/img1')
|
||||||
|
expect(feeds[0].siteUrl).to.equal('http://server1.com/site1')
|
||||||
|
expect(feedEpisodes[0].siteUrl).to.equal('http://server1.com/episode11')
|
||||||
|
expect(feedEpisodes[0].enclosureUrl).to.equal('http://server1.com/enclosure11')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('down', () => {
|
||||||
|
it('should add serverAddress back to URLs in Feeds and FeedEpisodes tables', async () => {
|
||||||
|
await Feeds.bulkCreate([
|
||||||
|
{ id: feed1Id, feedUrl: '/feed1', imageUrl: '/img1', siteUrl: '/site1', serverAddress: 'http://server1.com' },
|
||||||
|
{ id: feed2Id, feedUrl: '/feed2', imageUrl: '/img2', siteUrl: '/site2', serverAddress: 'http://server2.com' }
|
||||||
|
])
|
||||||
|
|
||||||
|
await FeedEpisodes.bulkCreate([
|
||||||
|
{ id: feedEpisode1Id, feedId: feed1Id, siteUrl: '/episode11', enclosureUrl: '/enclosure11' },
|
||||||
|
{ id: feedEpisode2Id, feedId: feed1Id, siteUrl: '/episode12', enclosureUrl: '/enclosure12' },
|
||||||
|
{ id: feedEpisode3Id, feedId: feed2Id, siteUrl: '/episode21', enclosureUrl: '/enclosure21' }
|
||||||
|
])
|
||||||
|
|
||||||
|
await down({ context })
|
||||||
|
const feeds = await Feeds.findAll({ raw: true })
|
||||||
|
const feedEpisodes = await FeedEpisodes.findAll({ raw: true })
|
||||||
|
|
||||||
|
expect(logger.info.calledWith('[2.17.5 migration] DOWNGRADE BEGIN: 2.17.5-remove-host-from-feed-urls')).to.be.true
|
||||||
|
expect(logger.info.calledWith('[2.17.5 migration] Adding serverAddress back to Feeds table URLs')).to.be.true
|
||||||
|
|
||||||
|
expect(feeds[0].feedUrl).to.equal('http://server1.com/feed1')
|
||||||
|
expect(feeds[0].imageUrl).to.equal('http://server1.com/img1')
|
||||||
|
expect(feeds[0].siteUrl).to.equal('http://server1.com/site1')
|
||||||
|
expect(feeds[1].feedUrl).to.equal('http://server2.com/feed2')
|
||||||
|
expect(feeds[1].imageUrl).to.equal('http://server2.com/img2')
|
||||||
|
expect(feeds[1].siteUrl).to.equal('http://server2.com/site2')
|
||||||
|
|
||||||
|
expect(logger.info.calledWith('[2.17.5 migration] Added serverAddress back to Feeds table URLs')).to.be.true
|
||||||
|
expect(logger.info.calledWith('[2.17.5 migration] Adding serverAddress back to FeedEpisodes table URLs')).to.be.true
|
||||||
|
|
||||||
|
expect(feedEpisodes[0].siteUrl).to.equal('http://server1.com/episode11')
|
||||||
|
expect(feedEpisodes[0].enclosureUrl).to.equal('http://server1.com/enclosure11')
|
||||||
|
expect(feedEpisodes[1].siteUrl).to.equal('http://server1.com/episode12')
|
||||||
|
expect(feedEpisodes[1].enclosureUrl).to.equal('http://server1.com/enclosure12')
|
||||||
|
expect(feedEpisodes[2].siteUrl).to.equal('http://server2.com/episode21')
|
||||||
|
expect(feedEpisodes[2].enclosureUrl).to.equal('http://server2.com/enclosure21')
|
||||||
|
|
||||||
|
expect(logger.info.calledWith('[2.17.5 migration] DOWNGRADE END: 2.17.5-remove-host-from-feed-urls')).to.be.true
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle null URLs in Feeds and FeedEpisodes tables', async () => {
|
||||||
|
await Feeds.bulkCreate([{ id: feed1Id, feedUrl: '/feed1', imageUrl: null, siteUrl: '/site1', serverAddress: 'http://server1.com' }])
|
||||||
|
await FeedEpisodes.bulkCreate([{ id: feedEpisode1Id, feedId: feed1Id, siteUrl: null, enclosureUrl: '/enclosure11' }])
|
||||||
|
|
||||||
|
await down({ context })
|
||||||
|
const feeds = await Feeds.findAll({ raw: true })
|
||||||
|
const feedEpisodes = await FeedEpisodes.findAll({ raw: true })
|
||||||
|
|
||||||
|
expect(feeds[0].feedUrl).to.equal('http://server1.com/feed1')
|
||||||
|
expect(feeds[0].imageUrl).to.be.null
|
||||||
|
expect(feeds[0].siteUrl).to.equal('http://server1.com/site1')
|
||||||
|
expect(feedEpisodes[0].siteUrl).to.be.null
|
||||||
|
expect(feedEpisodes[0].enclosureUrl).to.equal('http://server1.com/enclosure11')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle null serverAddress in Feeds table', async () => {
|
||||||
|
await Feeds.bulkCreate([{ id: feed1Id, feedUrl: '/feed1', imageUrl: '/img1', siteUrl: '/site1', serverAddress: null }])
|
||||||
|
await FeedEpisodes.bulkCreate([{ id: feedEpisode1Id, feedId: feed1Id, siteUrl: '/episode11', enclosureUrl: '/enclosure11' }])
|
||||||
|
|
||||||
|
await down({ context })
|
||||||
|
const feeds = await Feeds.findAll({ raw: true })
|
||||||
|
const feedEpisodes = await FeedEpisodes.findAll({ raw: true })
|
||||||
|
|
||||||
|
expect(feeds[0].feedUrl).to.equal('/feed1')
|
||||||
|
expect(feeds[0].imageUrl).to.equal('/img1')
|
||||||
|
expect(feeds[0].siteUrl).to.equal('/site1')
|
||||||
|
expect(feedEpisodes[0].siteUrl).to.equal('/episode11')
|
||||||
|
expect(feedEpisodes[0].enclosureUrl).to.equal('/enclosure11')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user