mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2026-06-01 08:20:40 +02:00
Compare commits
26 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 52a3bc224a | |||
| 54d67e5216 | |||
| b55d8250cc | |||
| 3a1e9abd68 | |||
| c5ba40a178 | |||
| f0c6dccadb | |||
| e701d1ab6a | |||
| e10c8093c9 | |||
| e81b3461b2 | |||
| 9345cb3934 | |||
| eb36a0b3dd | |||
| 7e442ecb3d | |||
| f07c5eb725 | |||
| a486be92cb | |||
| 4d84060036 | |||
| fc503691fe | |||
| c80dd43a3e | |||
| a4a62e0c18 | |||
| 2f98cb9b6d | |||
| 91dc6eebb0 | |||
| d72e0a4418 | |||
| 2c8ebd43cc | |||
| 9f561aa296 | |||
| 930bacd45d | |||
| ef2d736b20 | |||
| f3a453be20 |
@@ -6,10 +6,15 @@
|
||||
</div>
|
||||
</template>
|
||||
<div class="px-8 py-6 w-full rounded-lg bg-bg shadow-lg border border-black-300 relative overflow-y-scroll" style="max-height: 80vh">
|
||||
<p class="text-xl font-bold pb-4">
|
||||
Changelog <a :href="currentTagUrl" target="_blank" class="hover:underline">v{{ currentVersionNumber }}</a> ({{ currentVersionPubDate }})
|
||||
</p>
|
||||
<div class="custom-text" v-html="compiledMarkedown" />
|
||||
<template v-for="release in releasesToShow">
|
||||
<div :key="release.name">
|
||||
<p class="text-xl font-bold pb-4">
|
||||
Changelog <a :href="`https://github.com/advplyr/audiobookshelf/releases/tag/${release.name}`" target="_blank" class="hover:underline">{{ release.name }}</a> ({{ $formatDate(release.pubdate, dateFormat) }})
|
||||
</p>
|
||||
<div class="custom-text" v-html="getChangelog(release)" />
|
||||
</div>
|
||||
<div v-if="release !== releasesToShow[releasesToShow.length - 1]" class="border-b border-black-300 my-8" />
|
||||
</template>
|
||||
</div>
|
||||
</modals-modal>
|
||||
</template>
|
||||
@@ -37,24 +42,15 @@ export default {
|
||||
dateFormat() {
|
||||
return this.$store.state.serverSettings.dateFormat
|
||||
},
|
||||
changelog() {
|
||||
return this.versionData?.currentVersionChangelog || 'No Changelog Available'
|
||||
},
|
||||
compiledMarkedown() {
|
||||
return marked.parse(this.changelog, { gfm: true, breaks: true })
|
||||
},
|
||||
currentVersionPubDate() {
|
||||
if (!this.versionData?.currentVersionPubDate) return 'Unknown release date'
|
||||
return `${this.$formatDate(this.versionData.currentVersionPubDate, this.dateFormat)}`
|
||||
},
|
||||
currentTagUrl() {
|
||||
return this.versionData?.currentTagUrl
|
||||
},
|
||||
currentVersionNumber() {
|
||||
return this.$config.version
|
||||
releasesToShow() {
|
||||
return this.versionData?.releasesToShow || []
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getChangelog(release) {
|
||||
return marked.parse(release.changelog || 'No Changelog Available', { gfm: true, breaks: true })
|
||||
}
|
||||
},
|
||||
methods: {},
|
||||
mounted() {}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -132,6 +132,8 @@ export default {
|
||||
ctx.restore()
|
||||
}
|
||||
|
||||
const twoColumnWidth = 210
|
||||
|
||||
ctx.globalAlpha = 1
|
||||
ctx.textBaseline = 'middle'
|
||||
|
||||
@@ -150,12 +152,12 @@ export default {
|
||||
|
||||
// Top text
|
||||
addText('audiobookshelf', '28px', 'normal', tanColor, '0px', 65, 28)
|
||||
addText(`${this.year} YEAR IN REVIEW`, '18px', 'bold', 'white', '1px', 65, 51)
|
||||
addText(`${this.year} ${this.$strings.StatsYearInReview}`, '18px', 'bold', 'white', '1px', 65, 51,)
|
||||
|
||||
// Top left box
|
||||
createRoundedRect(50, 100, 340, 160)
|
||||
addText(this.yearStats.numBooksFinished, '64px', 'bold', 'white', '0px', 160, 165)
|
||||
addText('books finished', '28px', 'normal', tanColor, '0px', 160, 210)
|
||||
addText(this.$strings.StatsBooksFinished, '28px', 'normal', tanColor, '0px', 160, 210, twoColumnWidth)
|
||||
const readIconPath = new Path2D()
|
||||
readIconPath.addPath(new Path2D('M19 1H5c-1.1 0-1.99.9-1.99 2L3 15.93c0 .69.35 1.3.88 1.66L12 23l8.11-5.41c.53-.36.88-.97.88-1.66L21 3c0-1.1-.9-2-2-2zm-9 15l-5-5 1.41-1.41L10 13.17l7.59-7.59L19 7l-9 9z'), { a: 2, d: 2, e: 100, f: 160 })
|
||||
ctx.fillStyle = '#ffffff'
|
||||
@@ -164,40 +166,40 @@ export default {
|
||||
// Box top right
|
||||
createRoundedRect(410, 100, 340, 160)
|
||||
addText(this.$elapsedPrettyExtended(this.yearStats.totalListeningTime, true, false), '40px', 'bold', 'white', '0px', 500, 165)
|
||||
addText('spent listening', '28px', 'normal', tanColor, '0px', 500, 205)
|
||||
addText(this.$strings.StatsSpentListening, '28px', 'normal', tanColor, '0px', 500, 205, twoColumnWidth)
|
||||
addIcon('watch_later', 'white', '52px', 440, 180)
|
||||
|
||||
// Box bottom left
|
||||
createRoundedRect(50, 280, 340, 160)
|
||||
addText(this.yearStats.totalListeningSessions, '64px', 'bold', 'white', '0px', 160, 345)
|
||||
addText('sessions', '28px', 'normal', tanColor, '1px', 160, 390)
|
||||
addText(this.$strings.StatsSessions, '28px', 'normal', tanColor, '1px', 160, 390, twoColumnWidth)
|
||||
addIcon('headphones', 'white', '52px', 95, 360)
|
||||
|
||||
// Box bottom right
|
||||
createRoundedRect(410, 280, 340, 160)
|
||||
addText(this.yearStats.numBooksListened, '64px', 'bold', 'white', '0px', 500, 345)
|
||||
addText('books listened to', '28px', 'normal', tanColor, '0px', 500, 390)
|
||||
addText(this.$strings.StatsBooksListenedTo, '28px', 'normal', tanColor, '0px', 500, 390, twoColumnWidth)
|
||||
addIcon('local_library', 'white', '52px', 440, 360)
|
||||
|
||||
if (!this.variant) {
|
||||
// Text stats
|
||||
const topNarrator = this.yearStats.mostListenedNarrator
|
||||
if (topNarrator) {
|
||||
addText('TOP NARRATOR', '24px', 'normal', tanColor, '1px', 70, 520)
|
||||
addText(this.$strings.StatsTopNarrator, '24px', 'normal', tanColor, '1px', 70, 520, 330)
|
||||
addText(topNarrator.name, '36px', 'bolder', 'white', '0px', 70, 564, 330)
|
||||
addText(this.$elapsedPrettyExtended(topNarrator.time, true, false), '24px', 'lighter', 'white', '1px', 70, 599)
|
||||
}
|
||||
|
||||
const topGenre = this.yearStats.topGenres[0]
|
||||
if (topGenre) {
|
||||
addText('TOP GENRE', '24px', 'normal', tanColor, '1px', 430, 520)
|
||||
addText(this.$strings.StatsTopGenre, '24px', 'normal', tanColor, '1px', 430, 520, 330)
|
||||
addText(topGenre.genre, '36px', 'bolder', 'white', '0px', 430, 564, 330)
|
||||
addText(this.$elapsedPrettyExtended(topGenre.time, true, false), '24px', 'lighter', 'white', '1px', 430, 599)
|
||||
}
|
||||
|
||||
const topAuthor = this.yearStats.topAuthors[0]
|
||||
if (topAuthor) {
|
||||
addText('TOP AUTHOR', '24px', 'normal', tanColor, '1px', 70, 670)
|
||||
addText(this.$strings.StatsTopAuthor, '24px', 'normal', tanColor, '1px', 70, 670, 330)
|
||||
addText(topAuthor.name, '36px', 'bolder', 'white', '0px', 70, 714, 330)
|
||||
addText(this.$elapsedPrettyExtended(topAuthor.time, true, false), '24px', 'lighter', 'white', '1px', 70, 749)
|
||||
}
|
||||
@@ -205,7 +207,7 @@ export default {
|
||||
if (this.yearStats.mostListenedMonth?.time) {
|
||||
const jsdate = new Date(this.year, this.yearStats.mostListenedMonth.month, 1)
|
||||
const monthName = this.$formatJsDate(jsdate, 'LLLL')
|
||||
addText('TOP MONTH', '24px', 'normal', tanColor, '1px', 430, 670)
|
||||
addText(this.$strings.StatsTopMonth, '24px', 'normal', tanColor, '1px', 430, 670, 330)
|
||||
addText(monthName, '36px', 'bolder', 'white', '0px', 430, 714, 330)
|
||||
addText(this.$elapsedPrettyExtended(this.yearStats.mostListenedMonth.time, true, false), '24px', 'lighter', 'white', '1px', 430, 749)
|
||||
}
|
||||
@@ -214,7 +216,7 @@ export default {
|
||||
finishedBookCoverImgs = Object.values(finishedBookCoverImgs)
|
||||
if (finishedBookCoverImgs.length > 0) {
|
||||
ctx.textAlign = 'center'
|
||||
addText('Some books finished this year...', '28px', 'normal', tanColor, '0px', canvas.width / 2, 530)
|
||||
addText(this.$strings.StatsBooksFinishedThisYear, '28px', 'normal', tanColor, '0px', canvas.width / 2, 530)
|
||||
|
||||
for (let i = 0; i < Math.min(5, finishedBookCoverImgs.length); i++) {
|
||||
let imgToAdd = finishedBookCoverImgs[i]
|
||||
@@ -224,14 +226,14 @@ export default {
|
||||
} else if (this.variant === 2) {
|
||||
// Text stats
|
||||
if (this.yearStats.topAuthors.length) {
|
||||
addText('TOP AUTHORS', '24px', 'normal', tanColor, '1px', 70, 524)
|
||||
addText(this.$strings.StatsTopAuthors, '24px', 'normal', tanColor, '1px', 70, 524)
|
||||
for (let i = 0; i < this.yearStats.topAuthors.length; i++) {
|
||||
addText(this.yearStats.topAuthors[i].name, '36px', 'bolder', 'white', '0px', 70, 584 + i * 60, 330)
|
||||
}
|
||||
}
|
||||
|
||||
if (this.yearStats.topGenres.length) {
|
||||
addText('TOP GENRES', '24px', 'normal', tanColor, '1px', 430, 524)
|
||||
addText(this.$strings.StatsTopGenres, '24px', 'normal', tanColor, '1px', 430, 524)
|
||||
for (let i = 0; i < this.yearStats.topGenres.length; i++) {
|
||||
addText(this.yearStats.topGenres[i].genre, '36px', 'bolder', 'white', '0px', 430, 584 + i * 60, 330)
|
||||
}
|
||||
@@ -263,7 +265,7 @@ export default {
|
||||
}
|
||||
})
|
||||
} else {
|
||||
this.$toast.error('Cannot share natively on this device')
|
||||
this.$toast.error(this.$strings.ToastErrorCannotShare)
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
@@ -123,6 +123,8 @@ export default {
|
||||
ctx.restore()
|
||||
}
|
||||
|
||||
const threeColumnTextWidth = 200
|
||||
|
||||
ctx.globalAlpha = 1
|
||||
ctx.textBaseline = 'middle'
|
||||
|
||||
@@ -141,33 +143,33 @@ export default {
|
||||
|
||||
// Top text
|
||||
addText('audiobookshelf', '28px', 'normal', tanColor, '0px', 65, 28)
|
||||
addText(`${this.year} YEAR IN REVIEW`, '18px', 'bold', 'white', '1px', 65, 51)
|
||||
addText(`${this.year} ${this.$strings.StatsYearInReview}`, '18px', 'bold', 'white', '1px', 65, 51)
|
||||
|
||||
// Top left box
|
||||
createRoundedRect(40, 100, 230, 100)
|
||||
ctx.textAlign = 'center'
|
||||
addText(this.yearStats.numBooksAdded, '48px', 'bold', 'white', '0px', 155, 140)
|
||||
addText('books added', '18px', 'normal', tanColor, '0px', 155, 170)
|
||||
addText(this.$strings.StatsBooksAdded, '18px', 'normal', tanColor, '0px', 155, 170, threeColumnTextWidth)
|
||||
|
||||
// Box top right
|
||||
createRoundedRect(285, 100, 230, 100)
|
||||
addText(this.yearStats.numAuthorsAdded, '48px', 'bold', 'white', '0px', 400, 140)
|
||||
addText('authors added', '18px', 'normal', tanColor, '0px', 400, 170)
|
||||
addText(this.$strings.StatsAuthorsAdded, '18px', 'normal', tanColor, '0px', 400, 170, threeColumnTextWidth)
|
||||
|
||||
// Box bottom left
|
||||
createRoundedRect(530, 100, 230, 100)
|
||||
addText(this.yearStats.numListeningSessions, '48px', 'bold', 'white', '0px', 645, 140)
|
||||
addText('sessions', '18px', 'normal', tanColor, '1px', 645, 170)
|
||||
addText(this.$strings.StatsSessions, '18px', 'normal', tanColor, '1px', 645, 170, threeColumnTextWidth)
|
||||
|
||||
// Text stats
|
||||
if (this.yearStats.totalBooksAddedSize) {
|
||||
addText('Your book collection grew to...', '24px', 'normal', tanColor, '0px', canvas.width / 2, 260)
|
||||
addText(this.$strings.StatsCollectionGrewTo, '24px', 'normal', tanColor, '0px', canvas.width / 2, 260)
|
||||
addText(this.$bytesPretty(this.yearStats.totalBooksSize), '36px', 'bolder', 'white', '0px', canvas.width / 2, 300)
|
||||
addText('+' + this.$bytesPretty(this.yearStats.totalBooksAddedSize), '20px', 'lighter', 'white', '0px', canvas.width / 2, 330)
|
||||
}
|
||||
|
||||
if (this.yearStats.totalBooksAddedDuration) {
|
||||
addText('With a total duration of...', '24px', 'normal', tanColor, '0px', canvas.width / 2, 400)
|
||||
addText(this.$strings.StatsTotalDuration, '24px', 'normal', tanColor, '0px', canvas.width / 2, 400)
|
||||
addText(this.$elapsedPrettyExtended(this.yearStats.totalBooksDuration, true, false), '36px', 'bolder', 'white', '0px', canvas.width / 2, 440)
|
||||
addText('+' + this.$elapsedPrettyExtended(this.yearStats.totalBooksAddedDuration, true, false), '20px', 'lighter', 'white', '0px', canvas.width / 2, 470)
|
||||
}
|
||||
@@ -176,7 +178,7 @@ export default {
|
||||
// Bottom images
|
||||
imgsToAdd = Object.values(imgsToAdd)
|
||||
if (imgsToAdd.length > 0) {
|
||||
addText('Some additions include...', '24px', 'normal', tanColor, '0px', canvas.width / 2, 540)
|
||||
addText(this.$strings.StatsBooksAdditional, '24px', 'normal', tanColor, '0px', canvas.width / 2, 540)
|
||||
|
||||
for (let i = 0; i < Math.min(5, imgsToAdd.length); i++) {
|
||||
let imgToAdd = imgsToAdd[i]
|
||||
@@ -187,14 +189,14 @@ export default {
|
||||
// Text stats
|
||||
ctx.textAlign = 'left'
|
||||
if (this.yearStats.topAuthors.length) {
|
||||
addText('TOP AUTHORS', '24px', 'normal', tanColor, '1px', 70, 549)
|
||||
addText(this.$strings.StatsTopAuthors, '24px', 'normal', tanColor, '1px', 70, 549, 330)
|
||||
for (let i = 0; i < this.yearStats.topAuthors.length; i++) {
|
||||
addText(this.yearStats.topAuthors[i].name, '36px', 'bolder', 'white', '0px', 70, 609 + i * 60, 330)
|
||||
}
|
||||
}
|
||||
|
||||
if (this.yearStats.topNarrators.length) {
|
||||
addText('TOP NARRATORS', '24px', 'normal', tanColor, '1px', 430, 549)
|
||||
addText(this.$strings.StatsTopNarrators, '24px', 'normal', tanColor, '1px', 430, 549)
|
||||
for (let i = 0; i < this.yearStats.topNarrators.length; i++) {
|
||||
addText(this.yearStats.topNarrators[i].name, '36px', 'bolder', 'white', '0px', 430, 609 + i * 60, 330)
|
||||
}
|
||||
@@ -203,14 +205,14 @@ export default {
|
||||
// Text stats
|
||||
ctx.textAlign = 'left'
|
||||
if (this.yearStats.topAuthors.length) {
|
||||
addText('TOP AUTHORS', '24px', 'normal', tanColor, '1px', 70, 549)
|
||||
addText(this.$strings.StatsTopAuthors, '24px', 'normal', tanColor, '1px', 70, 549, 330)
|
||||
for (let i = 0; i < this.yearStats.topAuthors.length; i++) {
|
||||
addText(this.yearStats.topAuthors[i].name, '36px', 'bolder', 'white', '0px', 70, 609 + i * 60, 330)
|
||||
}
|
||||
}
|
||||
|
||||
if (this.yearStats.topGenres.length) {
|
||||
addText('TOP GENRES', '24px', 'normal', tanColor, '1px', 430, 549)
|
||||
addText(this.$strings.StatsTopGenres, '24px', 'normal', tanColor, '1px', 430, 549)
|
||||
for (let i = 0; i < this.yearStats.topGenres.length; i++) {
|
||||
addText(this.yearStats.topGenres[i].genre, '36px', 'bolder', 'white', '0px', 430, 609 + i * 60, 330)
|
||||
}
|
||||
@@ -239,7 +241,7 @@ export default {
|
||||
}
|
||||
})
|
||||
} else {
|
||||
this.$toast.error('Cannot share natively on this device')
|
||||
this.$toast.error(this.$strings.ToastErrorCannotShare)
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
@@ -113,6 +113,8 @@ export default {
|
||||
ctx.restore()
|
||||
}
|
||||
|
||||
const twoColumnWidth = 180
|
||||
|
||||
ctx.globalAlpha = 1
|
||||
ctx.textBaseline = 'middle'
|
||||
|
||||
@@ -131,12 +133,12 @@ export default {
|
||||
|
||||
// Top text
|
||||
addText('audiobookshelf', '28px', 'normal', tanColor, '0px', 65, 28)
|
||||
addText(`${this.year} YEAR IN REVIEW`, '18px', 'bold', 'white', '1px', 65, 51)
|
||||
addText(`${this.year} ${this.$strings.StatsYearInReview}`, '18px', 'bold', 'white', '1px', 65, 51)
|
||||
|
||||
// Top left box
|
||||
createRoundedRect(15, 75, 280, 110)
|
||||
addText(this.yearStats.numBooksFinished, '48px', 'bold', 'white', '0px', 105, 120)
|
||||
addText('books finished', '20px', 'normal', tanColor, '0px', 105, 155)
|
||||
addText(this.$strings.StatsBooksFinished, '20px', 'normal', tanColor, '0px', 105, 155, twoColumnWidth)
|
||||
const readIconPath = new Path2D()
|
||||
readIconPath.addPath(new Path2D('M19 1H5c-1.1 0-1.99.9-1.99 2L3 15.93c0 .69.35 1.3.88 1.66L12 23l8.11-5.41c.53-.36.88-.97.88-1.66L21 3c0-1.1-.9-2-2-2zm-9 15l-5-5 1.41-1.41L10 13.17l7.59-7.59L19 7l-9 9z'), { a: 1.5, d: 1.5, e: 55, f: 115 })
|
||||
ctx.fillStyle = '#ffffff'
|
||||
@@ -144,7 +146,7 @@ export default {
|
||||
|
||||
createRoundedRect(305, 75, 280, 110)
|
||||
addText(this.yearStats.numBooksListened, '48px', 'bold', 'white', '0px', 400, 120)
|
||||
addText('books listened to', '20px', 'normal', tanColor, '0px', 400, 155)
|
||||
addText(this.$strings.StatsBooksListenedTo, '20px', 'normal', tanColor, '0px', 400, 155, twoColumnWidth)
|
||||
addIcon('local_library', 'white', '42px', 345, 130)
|
||||
|
||||
this.canvas = canvas
|
||||
@@ -169,7 +171,7 @@ export default {
|
||||
}
|
||||
})
|
||||
} else {
|
||||
this.$toast.error('Cannot share natively on this device')
|
||||
this.$toast.error(this.$strings.ToastErrorCannotShare)
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "audiobookshelf-client",
|
||||
"version": "2.12.1",
|
||||
"version": "2.12.3",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "audiobookshelf-client",
|
||||
"version": "2.12.1",
|
||||
"version": "2.12.3",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@nuxtjs/axios": "^5.13.6",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "audiobookshelf-client",
|
||||
"version": "2.12.1",
|
||||
"version": "2.12.3",
|
||||
"buildNumber": 1,
|
||||
"description": "Self-hosted audiobook and podcast client",
|
||||
"main": "index.js",
|
||||
|
||||
+46
-33
@@ -11,6 +11,7 @@ function parseSemver(ver) {
|
||||
return null
|
||||
}
|
||||
return {
|
||||
name: ver,
|
||||
total,
|
||||
version: groups[2],
|
||||
major: Number(groups[3]),
|
||||
@@ -24,49 +25,61 @@ function parseSemver(ver) {
|
||||
return null
|
||||
}
|
||||
|
||||
function getReleases() {
|
||||
return axios
|
||||
.get(`https://api.github.com/repos/advplyr/audiobookshelf/releases`)
|
||||
.then((res) => {
|
||||
return res.data
|
||||
.map((release) => {
|
||||
const tagName = release.tag_name
|
||||
const verObj = parseSemver(tagName)
|
||||
if (verObj) {
|
||||
verObj.pubdate = new Date(release.published_at)
|
||||
verObj.changelog = release.body
|
||||
return verObj
|
||||
}
|
||||
return null
|
||||
})
|
||||
.filter((verObj) => verObj)
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to get releases', error)
|
||||
return []
|
||||
})
|
||||
}
|
||||
|
||||
export const currentVersion = packagejson.version
|
||||
|
||||
export async function checkForUpdate() {
|
||||
if (!packagejson.version) {
|
||||
return null
|
||||
}
|
||||
var currVerObj = parseSemver('v' + packagejson.version)
|
||||
if (!currVerObj) {
|
||||
console.error('Invalid version', packagejson.version)
|
||||
return null
|
||||
}
|
||||
var largestVer = null
|
||||
await axios.get(`https://api.github.com/repos/advplyr/audiobookshelf/releases`).then((res) => {
|
||||
var releases = res.data
|
||||
if (releases && releases.length) {
|
||||
releases.forEach((release) => {
|
||||
var tagName = release.tag_name
|
||||
var verObj = parseSemver(tagName)
|
||||
if (verObj) {
|
||||
if (!largestVer || largestVer.total < verObj.total) {
|
||||
largestVer = verObj
|
||||
}
|
||||
}
|
||||
|
||||
if (verObj.version == currVerObj.version) {
|
||||
currVerObj.pubdate = new Date(release.published_at)
|
||||
currVerObj.changelog = release.body
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
if (!largestVer) {
|
||||
console.error('No valid version tags to compare with')
|
||||
const releases = await getReleases()
|
||||
if (!releases.length) {
|
||||
console.error('No releases found')
|
||||
return null
|
||||
}
|
||||
|
||||
const currentVersion = releases.find((release) => release.version == packagejson.version)
|
||||
if (!currentVersion) {
|
||||
console.error('Current version not found in releases')
|
||||
return null
|
||||
}
|
||||
|
||||
const latestVersion = releases[0]
|
||||
const currentVersionMinor = currentVersion.minor
|
||||
const currentVersionMajor = currentVersion.major
|
||||
// Show all releases with the same minor version and lower or equal total version
|
||||
const releasesToShow = releases.filter((release) => {
|
||||
return release.major == currentVersionMajor && release.minor == currentVersionMinor && release.total <= currentVersion.total
|
||||
})
|
||||
|
||||
return {
|
||||
hasUpdate: largestVer.total > currVerObj.total,
|
||||
latestVersion: largestVer.version,
|
||||
githubTagUrl: `https://github.com/advplyr/audiobookshelf/releases/tag/v${largestVer.version}`,
|
||||
currentVersion: currVerObj.version,
|
||||
currentTagUrl: `https://github.com/advplyr/audiobookshelf/releases/tag/v${currVerObj.version}`,
|
||||
currentVersionPubDate: currVerObj.pubdate,
|
||||
currentVersionChangelog: currVerObj.changelog
|
||||
hasUpdate: latestVersion.total > currentVersion.total,
|
||||
latestVersion: latestVersion.version,
|
||||
githubTagUrl: `https://github.com/advplyr/audiobookshelf/releases/tag/v${latestVersion.version}`,
|
||||
currentVersion: currentVersion.version,
|
||||
releasesToShow
|
||||
}
|
||||
}
|
||||
|
||||
+22
-19
@@ -32,33 +32,33 @@ export const state = () => ({
|
||||
})
|
||||
|
||||
export const getters = {
|
||||
getServerSetting: state => key => {
|
||||
getServerSetting: (state) => (key) => {
|
||||
if (!state.serverSettings) return null
|
||||
return state.serverSettings[key]
|
||||
},
|
||||
getLibraryItemIdStreaming: state => {
|
||||
getLibraryItemIdStreaming: (state) => {
|
||||
return state.streamLibraryItem?.id || null
|
||||
},
|
||||
getIsStreamingFromDifferentLibrary: (state, getters, rootState) => {
|
||||
if (!state.streamLibraryItem) return false
|
||||
return state.streamLibraryItem.libraryId !== rootState.libraries.currentLibraryId
|
||||
},
|
||||
getIsMediaStreaming: state => (libraryItemId, episodeId) => {
|
||||
getIsMediaStreaming: (state) => (libraryItemId, episodeId) => {
|
||||
if (!state.streamLibraryItem) return null
|
||||
if (!episodeId) return state.streamLibraryItem.id == libraryItemId
|
||||
return state.streamLibraryItem.id == libraryItemId && state.streamEpisodeId == episodeId
|
||||
},
|
||||
getIsMediaQueued: state => (libraryItemId, episodeId) => {
|
||||
return state.playerQueueItems.some(i => {
|
||||
getIsMediaQueued: (state) => (libraryItemId, episodeId) => {
|
||||
return state.playerQueueItems.some((i) => {
|
||||
if (!episodeId) return i.libraryItemId === libraryItemId
|
||||
return i.libraryItemId === libraryItemId && i.episodeId === episodeId
|
||||
})
|
||||
},
|
||||
getBookshelfView: state => {
|
||||
getBookshelfView: (state) => {
|
||||
if (!state.serverSettings || isNaN(state.serverSettings.bookshelfView)) return Constants.BookshelfView.STANDARD
|
||||
return state.serverSettings.bookshelfView
|
||||
},
|
||||
getHomeBookshelfView: state => {
|
||||
getHomeBookshelfView: (state) => {
|
||||
if (!state.serverSettings || isNaN(state.serverSettings.homeBookshelfView)) return Constants.BookshelfView.STANDARD
|
||||
return state.serverSettings.homeBookshelfView
|
||||
}
|
||||
@@ -69,17 +69,20 @@ export const actions = {
|
||||
const updatePayload = {
|
||||
...payload
|
||||
}
|
||||
return this.$axios.$patch('/api/settings', updatePayload).then((result) => {
|
||||
if (result.success) {
|
||||
commit('setServerSettings', result.serverSettings)
|
||||
return true
|
||||
} else {
|
||||
return this.$axios
|
||||
.$patch('/api/settings', updatePayload)
|
||||
.then((result) => {
|
||||
if (result.success) {
|
||||
commit('setServerSettings', result.serverSettings)
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to update server settings', error)
|
||||
return false
|
||||
}
|
||||
}).catch((error) => {
|
||||
console.error('Failed to update server settings', error)
|
||||
return false
|
||||
})
|
||||
})
|
||||
},
|
||||
checkForUpdate({ commit }) {
|
||||
const VERSION_CHECK_BUFF = 1000 * 60 * 5 // 5 minutes
|
||||
@@ -96,7 +99,7 @@ export const actions = {
|
||||
}
|
||||
|
||||
var shouldCheckForUpdate = Date.now() - Number(lastVerCheck) > VERSION_CHECK_BUFF
|
||||
if (!shouldCheckForUpdate && savedVersionData && savedVersionData.version !== currentVersion) {
|
||||
if (!shouldCheckForUpdate && savedVersionData && (savedVersionData.version !== currentVersion || !savedVersionData.releasesToShow)) {
|
||||
// Version mismatch between saved data so check for update anyway
|
||||
shouldCheckForUpdate = true
|
||||
}
|
||||
@@ -180,7 +183,7 @@ export const mutations = {
|
||||
})
|
||||
},
|
||||
addItemToQueue(state, item) {
|
||||
const exists = state.playerQueueItems.some(i => {
|
||||
const exists = state.playerQueueItems.some((i) => {
|
||||
if (!i.episodeId) return i.libraryItemId === item.libraryItemId
|
||||
return i.libraryItemId === item.libraryItemId && i.episodeId === item.episodeId
|
||||
})
|
||||
|
||||
@@ -771,6 +771,24 @@
|
||||
"PlaceholderNewPlaylist": "New playlist name",
|
||||
"PlaceholderSearch": "Search..",
|
||||
"PlaceholderSearchEpisode": "Search episode..",
|
||||
"StatsAuthorsAdded": "authors added",
|
||||
"StatsBooksAdded": "books added",
|
||||
"StatsBooksAdditional": "Some additions include…",
|
||||
"StatsBooksFinished": "books finished",
|
||||
"StatsBooksFinishedThisYear": "Some books finished this year…",
|
||||
"StatsBooksListenedTo": "books listened to",
|
||||
"StatsCollectionGrewTo": "Your book collection grew to…",
|
||||
"StatsSessions": "sessions",
|
||||
"StatsSpentListening": "spent listening",
|
||||
"StatsTopAuthor": "TOP AUTHOR",
|
||||
"StatsTopAuthors": "TOP AUTHORS",
|
||||
"StatsTopGenre": "TOP GENRE",
|
||||
"StatsTopGenres": "TOP GENRES",
|
||||
"StatsTopMonth": "TOP MONTH",
|
||||
"StatsTopNarrator": "TOP NARRATOR",
|
||||
"StatsTopNarrators": "TOP NARRATORS",
|
||||
"StatsTotalDuration": "With a total duration of…",
|
||||
"StatsYearInReview": "YEAR IN REVIEW",
|
||||
"ToastAccountUpdateFailed": "Failed to update account",
|
||||
"ToastAccountUpdateSuccess": "Account updated",
|
||||
"ToastAuthorImageRemoveFailed": "Failed to remove image",
|
||||
@@ -806,6 +824,7 @@
|
||||
"ToastCollectionUpdateSuccess": "Collection updated",
|
||||
"ToastDeleteFileFailed": "Failed to delete file",
|
||||
"ToastDeleteFileSuccess": "File deleted",
|
||||
"ToastErrorCannotShare": "Cannot share natively on this device",
|
||||
"ToastFailedToLoadData": "Failed to load data",
|
||||
"ToastItemCoverUpdateFailed": "Failed to update item cover",
|
||||
"ToastItemCoverUpdateSuccess": "Item cover updated",
|
||||
|
||||
+94
-1
@@ -88,6 +88,7 @@
|
||||
"ButtonShow": "Näytä",
|
||||
"ButtonStartM4BEncode": "Aloita M4B enkoodaus",
|
||||
"ButtonStartMetadataEmbed": "Aloita metadatan embed",
|
||||
"ButtonStats": "Tilastot",
|
||||
"ButtonSubmit": "Lähetä",
|
||||
"ButtonTest": "Testi",
|
||||
"ButtonUpload": "Lähetä palvelimelle",
|
||||
@@ -120,66 +121,158 @@
|
||||
"HeaderDetails": "Yksityiskohdat",
|
||||
"HeaderDownloadQueue": "Latausjono",
|
||||
"HeaderEbookFiles": "E-kirjatiedostot",
|
||||
"HeaderEmail": "Sähköposti",
|
||||
"HeaderEmailSettings": "Sähköpostiasetukset",
|
||||
"HeaderEpisodes": "Jaksot",
|
||||
"HeaderEreaderDevices": "E-lukijalaitteet",
|
||||
"HeaderEreaderSettings": "E-lukijan asetukset",
|
||||
"HeaderFiles": "Tiedostot",
|
||||
"HeaderIgnoredFiles": "Ohitetut tiedostot",
|
||||
"HeaderLatestEpisodes": "Viimeisimmät jaksot",
|
||||
"HeaderLibraries": "Kirjastot",
|
||||
"HeaderLibraryFiles": "Kirjaston tiedostot",
|
||||
"HeaderLibraryStats": "Kirjaston tilastot",
|
||||
"HeaderListeningStats": "Kuuntelutilastot",
|
||||
"HeaderLogs": "Lokit",
|
||||
"HeaderNewAccount": "Uusi tili",
|
||||
"HeaderNewLibrary": "Uusi kirjasto",
|
||||
"HeaderNotifications": "Ilmoitukset",
|
||||
"HeaderOpenRSSFeed": "Avaa RSS-syöte",
|
||||
"HeaderOtherFiles": "Muut tiedostot",
|
||||
"HeaderPermissions": "Käyttöoikeudet",
|
||||
"HeaderPlaylist": "Soittolista",
|
||||
"HeaderPlaylistItems": "Soittolistan kohteet",
|
||||
"HeaderRSSFeedGeneral": "RSS yksityiskohdat",
|
||||
"HeaderRSSFeedIsOpen": "RSS syöte on avoinna",
|
||||
"HeaderRemoveEpisode": "Poista jakso",
|
||||
"HeaderRemoveEpisodes": "Poista {0} jaksoa",
|
||||
"HeaderSchedule": "Ajoita",
|
||||
"HeaderScheduleLibraryScans": "Ajoita automaattiset kirjastoskannaukset",
|
||||
"HeaderSetBackupSchedule": "Aseta varmuuskopiointiaikataulu",
|
||||
"HeaderSettings": "Asetukset",
|
||||
"HeaderSettingsExperimental": "Kokeelliset ominaisuudet",
|
||||
"HeaderSleepTimer": "Uniajastin",
|
||||
"HeaderStatsMinutesListeningChart": "Kuunteluminuutit (viim. 7 pv)",
|
||||
"HeaderStatsRecentSessions": "Viimeaikaiset istunnot",
|
||||
"HeaderTableOfContents": "Sisällysluettelo",
|
||||
"HeaderTools": "Työkalut",
|
||||
"HeaderUsers": "Käyttäjät",
|
||||
"HeaderYourStats": "Tilastosi",
|
||||
"LabelAccountType": "Tilin tyyppi",
|
||||
"LabelAccountTypeGuest": "Vieras",
|
||||
"LabelAccountTypeUser": "Käyttäjä",
|
||||
"LabelActivity": "Toiminta",
|
||||
"LabelAddToCollection": "Lisää kokoelmaan",
|
||||
"LabelAddToCollectionBatch": "Lisää {0} kirjaa kokoelmaan",
|
||||
"LabelAddToPlaylist": "Lisää soittolistaan",
|
||||
"LabelAddToPlaylistBatch": "Lisää {0} kohdetta soittolistaan",
|
||||
"LabelAdded": "Lisätty",
|
||||
"LabelAddedAt": "Lisätty listalle",
|
||||
"LabelAll": "Kaikki",
|
||||
"LabelAllUsers": "Kaikki käyttäjät",
|
||||
"LabelAllUsersExcludingGuests": "Kaikki käyttäjät vieraita lukuun ottamatta",
|
||||
"LabelAllUsersIncludingGuests": "Kaikki käyttäjät mukaan lukien vieraat",
|
||||
"LabelAuthor": "Tekijä",
|
||||
"LabelAuthorFirstLast": "Tekijä (Etunimi Sukunimi)",
|
||||
"LabelAuthorLastFirst": "Tekijä (Sukunimi, Etunimi)",
|
||||
"LabelAuthors": "Tekijät",
|
||||
"LabelAutoDownloadEpisodes": "Lataa jaksot automaattisesti",
|
||||
"LabelBackupsEnableAutomaticBackups": "Ota automaattinen varmuuskopiointi käyttöön",
|
||||
"LabelBackupsEnableAutomaticBackupsHelp": "Varmuuskopiot tallennettu kansioon /metadata/backups",
|
||||
"LabelBackupsMaxBackupSize": "Varmuuskopion enimmäiskoko (Gt) (0 rajaton)",
|
||||
"LabelBackupsNumberToKeep": "Säilytettävien varmuuskopioiden määrä",
|
||||
"LabelBooks": "Kirjat",
|
||||
"LabelButtonText": "Painikkeen teksti",
|
||||
"LabelChangePassword": "Vaihda salasana",
|
||||
"LabelChapters": "Luvut",
|
||||
"LabelClickForMoreInfo": "Napsauta saadaksesi lisätietoja",
|
||||
"LabelClosePlayer": "Sulje soitin",
|
||||
"LabelCodec": "Koodekki",
|
||||
"LabelCollapseSeries": "Pienennä sarja",
|
||||
"LabelCollection": "Kokoelma",
|
||||
"LabelCollections": "Kokoelmat",
|
||||
"LabelComplete": "Valmis",
|
||||
"LabelConfirmPassword": "Vahvista salasana",
|
||||
"LabelContinueListening": "Jatka kuuntelua",
|
||||
"LabelContinueReading": "Jatka lukemista",
|
||||
"LabelContinueSeries": "Jatka sarjoja",
|
||||
"LabelCover": "Kansikuva",
|
||||
"LabelCoverImageURL": "Kansikuvan URL-osoite",
|
||||
"LabelCurrent": "Nykyinen",
|
||||
"LabelDescription": "Kuvaus",
|
||||
"LabelDevice": "Laite",
|
||||
"LabelDeviceInfo": "Laitteen tiedot",
|
||||
"LabelDownload": "Lataa",
|
||||
"LabelDownloadNEpisodes": "Lataa {0} jaksoa",
|
||||
"LabelDuration": "Kesto",
|
||||
"LabelEbook": "E-kirja",
|
||||
"LabelEbooks": "E-kirjat",
|
||||
"LabelEdit": "Muokkaa",
|
||||
"LabelEmail": "Sähköposti",
|
||||
"LabelEnable": "Ota käyttöön",
|
||||
"LabelEndOfChapter": "Luvun loppu",
|
||||
"LabelEpisode": "Jakso",
|
||||
"LabelFile": "Tiedosto",
|
||||
"LabelFileBirthtime": "Tiedoston syntymäaika",
|
||||
"LabelFileModified": "Muutettu tiedosto",
|
||||
"LabelFilename": "Tiedostonimi",
|
||||
"LabelFolder": "Kansio",
|
||||
"LabelInProgress": "Kesken",
|
||||
"LabelIncomplete": "Keskeneräinen",
|
||||
"LabelLanguage": "Kieli",
|
||||
"LabelListenAgain": "Kuuntele uudelleen",
|
||||
"LabelMediaType": "Mediatyyppi",
|
||||
"LabelMore": "Lisää",
|
||||
"LabelMoreInfo": "Lisätietoja",
|
||||
"LabelName": "Nimi",
|
||||
"LabelNarrator": "Lukija",
|
||||
"LabelNarrators": "Lukijat",
|
||||
"LabelNewestAuthors": "Uusimmat kirjailijat",
|
||||
"LabelNewestEpisodes": "Uusimmat jaksot",
|
||||
"LabelPassword": "Salasana",
|
||||
"LabelPath": "Polku",
|
||||
"LabelPodcast": "Podcast",
|
||||
"LabelPodcasts": "Podcastit",
|
||||
"LabelPublishYear": "Julkaisuvuosi",
|
||||
"LabelRSSFeedPreventIndexing": "Estä indeksointi",
|
||||
"LabelRead": "Lue",
|
||||
"LabelReadAgain": "Lue uudelleen",
|
||||
"LabelRecentSeries": "Viimeisimmät sarjat",
|
||||
"LabelRecentlyAdded": "Viimeeksi lisätyt",
|
||||
"LabelSeason": "Kausi",
|
||||
"LabelSetEbookAsPrimary": "Aseta ensisijaiseksi",
|
||||
"LabelSetEbookAsSupplementary": "Aseta täydentäväksi",
|
||||
"LabelShowAll": "Näytä kaikki",
|
||||
"LabelSize": "Koko",
|
||||
"LabelSleepTimer": "Uniajastin",
|
||||
"LabelStatsDailyAverage": "Päivittäinen keskiarvo",
|
||||
"LabelStatsInARow": "peräjälkeen",
|
||||
"LabelStatsMinutes": "minuuttia",
|
||||
"LabelTheme": "Teema",
|
||||
"LabelThemeDark": "Tumma",
|
||||
"LabelThemeLight": "Kirkas",
|
||||
"LabelTimeRemaining": "{0} jäljellä",
|
||||
"LabelType": "Tyyppi",
|
||||
"LabelUser": "Käyttäjä",
|
||||
"LabelUsername": "Käyttäjätunnus",
|
||||
"MessageDownloadingEpisode": "Ladataan jaksoa"
|
||||
"LabelYourBookmarks": "Kirjanmerkkisi",
|
||||
"LabelYourProgress": "Edistymisesi",
|
||||
"MessageDownloadingEpisode": "Ladataan jaksoa",
|
||||
"MessageEpisodesQueuedForDownload": "{0} jaksoa on latausjonossa",
|
||||
"MessageFetching": "Haetaan...",
|
||||
"MessageLoading": "Ladataan...",
|
||||
"MessageMarkAsFinished": "Merkitse valmiiksi",
|
||||
"MessageNoBookmarks": "Ei kirjanmerkkejä",
|
||||
"MessageNoItems": "Ei kohteita",
|
||||
"MessageNoItemsFound": "Kohteita ei löytynyt",
|
||||
"MessageNoPodcastsFound": "Podcasteja ei löytynyt",
|
||||
"MessageNoUserPlaylists": "Sinulla ei ole soittolistoja",
|
||||
"MessageReportBugsAndContribute": "Ilmoita virheistä, toivo ominaisuuksia ja osallistu",
|
||||
"ToastBookmarkCreateFailed": "Kirjanmerkin luominen epäonnistui",
|
||||
"ToastBookmarkRemoveFailed": "Kirjanmerkin poistaminen epäonnistui",
|
||||
"ToastBookmarkUpdateFailed": "Kirjanmerkin päivittäminen epäonnistui",
|
||||
"ToastItemMarkedAsFinishedFailed": "Valmiiksi merkitseminen epäonnistui",
|
||||
"ToastPlaylistCreateFailed": "Soittolistan luominen epäonnistui",
|
||||
"ToastPodcastCreateFailed": "Podcastin luominen epäonnistui",
|
||||
"ToastPodcastCreateSuccess": "Podcastin luominen onnistui"
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
"ButtonJumpForward": "Avancer",
|
||||
"ButtonLatest": "Dernière version",
|
||||
"ButtonLibrary": "Bibliothèque",
|
||||
"ButtonLogout": "Me déconnecter",
|
||||
"ButtonLogout": "Déconnexion",
|
||||
"ButtonLookup": "Chercher",
|
||||
"ButtonManageTracks": "Gérer les pistes",
|
||||
"ButtonMapChapterTitles": "Correspondance des titres de chapitres",
|
||||
@@ -59,7 +59,7 @@
|
||||
"ButtonPurgeItemsCache": "Purger le cache des éléments",
|
||||
"ButtonQueueAddItem": "Ajouter à la liste de lecture",
|
||||
"ButtonQueueRemoveItem": "Supprimer de la liste de lecture",
|
||||
"ButtonQuickEmbedMetadata": "Ajoutez rapidement des métadonnées",
|
||||
"ButtonQuickEmbedMetadata": "Ajouter rapidement des métadonnées",
|
||||
"ButtonQuickMatch": "Recherche rapide",
|
||||
"ButtonReScan": "Nouvelle analyse",
|
||||
"ButtonRead": "Lire",
|
||||
@@ -285,7 +285,7 @@
|
||||
"LabelEmail": "Courriel",
|
||||
"LabelEmailSettingsFromAddress": "Expéditeur",
|
||||
"LabelEmailSettingsRejectUnauthorized": "Rejeter les certificats non autorisés",
|
||||
"LabelEmailSettingsRejectUnauthorizedHelp": "Désactiver la validation du certificat SSL peut exposer votre connexion à des risques de sécurité, tels que des attaques de type « man-in-the-middle ». Ne désactivez cette option que si vous en comprenez les implications et si vous faites confiance au serveur de messagerie auquel vous vous connectez.",
|
||||
"LabelEmailSettingsRejectUnauthorizedHelp": "Désactiver la validation du certificat SSL peut exposer votre connexion à des risques de sécurité, tels que des attaques de type « Attaque de l’homme du milieu ». Ne désactivez cette option que si vous en comprenez les implications et si vous faites confiance au serveur de messagerie auquel vous vous connectez.",
|
||||
"LabelEmailSettingsSecure": "Sécurisé",
|
||||
"LabelEmailSettingsSecureHelp": "Si vous activez cette option, TLS sera utiliser lors de la connexion au serveur. Sinon, TLS est utilisé uniquement si le serveur supporte l’extension STARTTLS. Dans la plupart des cas, activez l’option, vous vous connecterai sur le port 465. Pour le port 587 ou 25, désactiver l’option. (source : nodemailer.com/smtp/#authentication)",
|
||||
"LabelEmailSettingsTestAddress": "Adresse de test",
|
||||
@@ -454,7 +454,7 @@
|
||||
"LabelRSSFeedSlug": "Balise URL du flux RSS",
|
||||
"LabelRSSFeedURL": "Adresse du flux RSS",
|
||||
"LabelRandomly": "Au hasard",
|
||||
"LabelReAddSeriesToContinueListening": "Ajoutez à nouveau la série pour continuer à l’écouter",
|
||||
"LabelReAddSeriesToContinueListening": "Ajouter à nouveau la série pour continuer à l’écouter",
|
||||
"LabelRead": "Lire",
|
||||
"LabelReadAgain": "Lire à nouveau",
|
||||
"LabelReadEbookWithoutProgress": "Lire le livre numérique sans sauvegarder la progression",
|
||||
@@ -616,7 +616,7 @@
|
||||
"LabelYearReviewShow": "Afficher le bilan de l’année",
|
||||
"LabelYourAudiobookDuration": "Durée de vos livres audios",
|
||||
"LabelYourBookmarks": "Vos favoris",
|
||||
"LabelYourPlaylists": "Vos listes de lecture",
|
||||
"LabelYourPlaylists": "Mes listes de lecture",
|
||||
"LabelYourProgress": "Votre progression",
|
||||
"MessageAddToPlayerQueue": "Ajouter en file d’attente",
|
||||
"MessageAppriseDescription": "Nécessite une instance d’<a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">API Apprise</a> pour utiliser cette fonctionnalité ou une api qui prend en charge les mêmes requêtes.<br />L’URL de l’API Apprise doit comprendre le chemin complet pour envoyer la notification. Par exemple, si votre instance écoute sur <code>http://192.168.1.1:8337</code> alors vous devez mettre <code>http://192.168.1.1:8337/notify</code>.",
|
||||
|
||||
@@ -89,7 +89,7 @@
|
||||
"ButtonShow": "显示",
|
||||
"ButtonStartM4BEncode": "开始 M4B 编码",
|
||||
"ButtonStartMetadataEmbed": "开始嵌入元数据",
|
||||
"ButtonStats": "状态",
|
||||
"ButtonStats": "统计数据",
|
||||
"ButtonSubmit": "提交",
|
||||
"ButtonTest": "测试",
|
||||
"ButtonUpload": "上传",
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "audiobookshelf",
|
||||
"version": "2.12.1",
|
||||
"version": "2.12.3",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "audiobookshelf",
|
||||
"version": "2.12.1",
|
||||
"version": "2.12.3",
|
||||
"license": "GPL-3.0",
|
||||
"dependencies": {
|
||||
"axios": "^0.27.2",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "audiobookshelf",
|
||||
"version": "2.12.1",
|
||||
"version": "2.12.3",
|
||||
"buildNumber": 1,
|
||||
"description": "Self-hosted audiobook and podcast server",
|
||||
"main": "index.js",
|
||||
|
||||
+4
-5
@@ -207,7 +207,6 @@ class Database {
|
||||
|
||||
try {
|
||||
await this.sequelize.authenticate()
|
||||
await this.loadExtensions([process.env.SQLEAN_UNICODE_PATH])
|
||||
Logger.info(`[Database] Db connection was successful`)
|
||||
return true
|
||||
} catch (error) {
|
||||
@@ -217,7 +216,7 @@ class Database {
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* TODO: Temporarily disabled
|
||||
* @param {string[]} extensions paths to extension binaries
|
||||
*/
|
||||
async loadExtensions(extensions) {
|
||||
@@ -827,7 +826,7 @@ class Database {
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* TODO: Temporarily unused
|
||||
* @param {string} value
|
||||
* @returns {string}
|
||||
*/
|
||||
@@ -836,7 +835,7 @@ class Database {
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* TODO: Temporarily unused
|
||||
* @param {string} query
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
@@ -855,7 +854,7 @@ class Database {
|
||||
*/
|
||||
matchExpression(column, normalizedQuery) {
|
||||
const normalizedPattern = this.sequelize.escape(`%${normalizedQuery}%`)
|
||||
const normalizedColumn = this.normalize(column)
|
||||
const normalizedColumn = column
|
||||
return `${normalizedColumn} LIKE ${normalizedPattern}`
|
||||
}
|
||||
}
|
||||
|
||||
+1
-7
@@ -41,7 +41,6 @@ const LibraryScanner = require('./scanner/LibraryScanner')
|
||||
//Import the main Passport and Express-Session library
|
||||
const passport = require('passport')
|
||||
const expressSession = require('express-session')
|
||||
const MemoryStore = require('./libs/memorystore')(expressSession)
|
||||
|
||||
class Server {
|
||||
constructor(SOURCE, PORT, HOST, CONFIG_PATH, METADATA_PATH, ROUTER_BASE_PATH) {
|
||||
@@ -219,12 +218,7 @@ class Server {
|
||||
cookie: {
|
||||
// also send the cookie if were are not on https (not every use has https)
|
||||
secure: false
|
||||
},
|
||||
store: new MemoryStore({
|
||||
checkPeriod: 86400000, // prune expired entries every 24h
|
||||
ttl: 86400000, // 24h
|
||||
max: 1000
|
||||
})
|
||||
}
|
||||
})
|
||||
)
|
||||
// init passport.js
|
||||
|
||||
@@ -113,21 +113,21 @@ class LibraryItemController {
|
||||
Logger.warn('User attempted to download without permission', req.user)
|
||||
return res.sendStatus(403)
|
||||
}
|
||||
const libraryItemPath = req.libraryItem.path
|
||||
const itemTitle = req.libraryItem.media.metadata.title
|
||||
|
||||
// If library item is a single file in root dir then no need to zip
|
||||
if (req.libraryItem.isFile) {
|
||||
// Express does not set the correct mimetype for m4b files so use our defined mimetypes if available
|
||||
const audioMimeType = getAudioMimeTypeFromExtname(Path.extname(req.libraryItem.path))
|
||||
const audioMimeType = getAudioMimeTypeFromExtname(Path.extname(libraryItemPath))
|
||||
if (audioMimeType) {
|
||||
res.setHeader('Content-Type', audioMimeType)
|
||||
}
|
||||
|
||||
res.download(req.libraryItem.path, req.libraryItem.relPath)
|
||||
Logger.info(`[LibraryItemController] User "${req.user.username}" requested download for item "${itemTitle}" at "${libraryItemPath}"`)
|
||||
res.download(libraryItemPath, req.libraryItem.relPath)
|
||||
return
|
||||
}
|
||||
|
||||
const libraryItemPath = req.libraryItem.path
|
||||
const itemTitle = req.libraryItem.media.metadata.title
|
||||
Logger.info(`[LibraryItemController] User "${req.user.username}" requested download for item "${itemTitle}" at "${libraryItemPath}"`)
|
||||
const filename = `${itemTitle}.zip`
|
||||
zipHelpers.zipDirectoryPipe(libraryItemPath, filename, res)
|
||||
@@ -715,7 +715,7 @@ class LibraryItemController {
|
||||
return res.sendStatus(403)
|
||||
}
|
||||
|
||||
Logger.info(`[LibraryItemController] User "${req.user.username}" requested file download at "${libraryFile.metadata.path}"`)
|
||||
Logger.info(`[LibraryItemController] User "${req.user.username}" requested download for item "${req.libraryItem.media.metadata.title}" file at "${libraryFile.metadata.path}"`)
|
||||
|
||||
if (global.XAccel) {
|
||||
const encodedURI = encodeUriPath(global.XAccel + libraryFile.metadata.path)
|
||||
|
||||
@@ -3,8 +3,7 @@ const Logger = require('../Logger')
|
||||
const Database = require('../Database')
|
||||
|
||||
class ApiCacheManager {
|
||||
|
||||
defaultCacheOptions = { max: 1000, maxSize: 10 * 1000 * 1000, sizeCalculation: item => (item.body.length + JSON.stringify(item.headers).length) }
|
||||
defaultCacheOptions = { max: 1000, maxSize: 10 * 1000 * 1000, sizeCalculation: (item) => item.body.length + JSON.stringify(item.headers).length }
|
||||
defaultTtlOptions = { ttl: 30 * 60 * 1000 }
|
||||
|
||||
constructor(cache = new LRUCache(this.defaultCacheOptions), ttlOptions = this.defaultTtlOptions) {
|
||||
@@ -14,7 +13,7 @@ class ApiCacheManager {
|
||||
|
||||
init(database = Database) {
|
||||
let hooks = ['afterCreate', 'afterUpdate', 'afterDestroy', 'afterBulkCreate', 'afterBulkUpdate', 'afterBulkDestroy', 'afterUpsert']
|
||||
hooks.forEach(hook => database.sequelize.addHook(hook, (model) => this.clear(model, hook)))
|
||||
hooks.forEach((hook) => database.sequelize.addHook(hook, (model) => this.clear(model, hook)))
|
||||
}
|
||||
|
||||
clear(model, hook) {
|
||||
@@ -33,7 +32,16 @@ class ApiCacheManager {
|
||||
}
|
||||
|
||||
get middleware() {
|
||||
/**
|
||||
* @param {import('express').Request} req
|
||||
* @param {import('express').Response} res
|
||||
* @param {import('express').NextFunction} next
|
||||
*/
|
||||
return (req, res, next) => {
|
||||
if (req.query.sort === 'random') {
|
||||
Logger.debug(`[ApiCacheManager] Skipping cache for random sort`)
|
||||
return next()
|
||||
}
|
||||
const key = { user: req.user.username, url: req.url }
|
||||
const stringifiedKey = JSON.stringify(key)
|
||||
Logger.debug(`[ApiCacheManager] count: ${this.cache.size} size: ${this.cache.calculatedSize}`)
|
||||
@@ -61,4 +69,4 @@ class ApiCacheManager {
|
||||
}
|
||||
}
|
||||
}
|
||||
module.exports = ApiCacheManager
|
||||
module.exports = ApiCacheManager
|
||||
|
||||
@@ -263,8 +263,9 @@ module.exports.sqlean = sqlean // for testing
|
||||
class BinaryManager {
|
||||
defaultRequiredBinaries = [
|
||||
new Binary('ffmpeg', 'executable', 'FFMPEG_PATH', ['5.1'], ffbinaries), // ffmpeg executable
|
||||
new Binary('ffprobe', 'executable', 'FFPROBE_PATH', ['5.1'], ffbinaries), // ffprobe executable
|
||||
new Binary('unicode', 'library', 'SQLEAN_UNICODE_PATH', ['0.24.2'], sqlean) // sqlean unicode extension
|
||||
new Binary('ffprobe', 'executable', 'FFPROBE_PATH', ['5.1'], ffbinaries) // ffprobe executable
|
||||
// TODO: Temporarily disabled due to db corruption issues
|
||||
// new Binary('unicode', 'library', 'SQLEAN_UNICODE_PATH', ['0.24.2'], sqlean) // sqlean unicode extension
|
||||
]
|
||||
|
||||
constructor(requiredBinaries = this.defaultRequiredBinaries) {
|
||||
|
||||
@@ -104,7 +104,7 @@ module.exports.downloadPodcastEpisode = (podcastEpisodeDownload) => {
|
||||
method: 'GET',
|
||||
responseType: 'stream',
|
||||
headers: {
|
||||
'User-Agent': 'audiobookshelf (+https://audiobookshelf.org; like iTMS)'
|
||||
'User-Agent': 'audiobookshelf (+https://audiobookshelf.org)'
|
||||
},
|
||||
timeout: 30000
|
||||
}).catch((error) => {
|
||||
|
||||
@@ -15,7 +15,7 @@ const { AudioMimeType } = require('./constants')
|
||||
*/
|
||||
const filePathToPOSIX = (path) => {
|
||||
if (!global.isWin || !path) return path
|
||||
return path.replace(/\\/g, '/')
|
||||
return path.startsWith('\\\\') ? '\\\\' + path.slice(2).replace(/\\/g, '/') : path.replace(/\\/g, '/')
|
||||
}
|
||||
module.exports.filePathToPOSIX = filePathToPOSIX
|
||||
|
||||
@@ -169,7 +169,7 @@ async function recurseFiles(path, relPathToReplace = null) {
|
||||
extensions: true,
|
||||
deep: true,
|
||||
realPath: true,
|
||||
normalizePath: true
|
||||
normalizePath: false
|
||||
}
|
||||
let list = await rra.list(path, options)
|
||||
if (list.error) {
|
||||
@@ -186,6 +186,8 @@ async function recurseFiles(path, relPathToReplace = null) {
|
||||
return false
|
||||
}
|
||||
|
||||
item.fullname = filePathToPOSIX(item.fullname)
|
||||
item.path = filePathToPOSIX(item.path)
|
||||
const relpath = item.fullname.replace(relPathToReplace, '')
|
||||
let reldirname = Path.dirname(relpath)
|
||||
if (reldirname === '.') reldirname = ''
|
||||
|
||||
@@ -975,7 +975,7 @@ module.exports = {
|
||||
async search(oldUser, oldLibrary, query, limit, offset) {
|
||||
const userPermissionBookWhere = this.getUserPermissionBookWhereQuery(oldUser)
|
||||
|
||||
const normalizedQuery = await Database.getNormalizedQuery(query)
|
||||
const normalizedQuery = query
|
||||
|
||||
const matchTitle = Database.matchExpression('title', normalizedQuery)
|
||||
const matchSubtitle = Database.matchExpression('subtitle', normalizedQuery)
|
||||
|
||||
@@ -314,7 +314,7 @@ module.exports = {
|
||||
async search(oldUser, oldLibrary, query, limit, offset) {
|
||||
const userPermissionPodcastWhere = this.getUserPermissionPodcastWhereQuery(oldUser)
|
||||
|
||||
const normalizedQuery = await Database.getNormalizedQuery(query)
|
||||
const normalizedQuery = query
|
||||
const matchTitle = Database.matchExpression('title', normalizedQuery)
|
||||
const matchAuthor = Database.matchExpression('author', normalizedQuery)
|
||||
|
||||
|
||||
@@ -11,8 +11,8 @@ describe('ApiCacheManager', () => {
|
||||
let manager
|
||||
|
||||
beforeEach(() => {
|
||||
cache = { get: sinon.stub(), set: sinon.spy() }
|
||||
req = { user: { username: 'testUser' }, url: '/test-url' }
|
||||
cache = { get: sinon.stub(), set: sinon.spy() }
|
||||
req = { user: { username: 'testUser' }, url: '/test-url', query: {} }
|
||||
res = { send: sinon.spy(), getHeaders: sinon.stub(), statusCode: 200, status: sinon.spy(), set: sinon.spy() }
|
||||
next = sinon.spy()
|
||||
})
|
||||
@@ -94,4 +94,4 @@ describe('ApiCacheManager', () => {
|
||||
expect(res.originalSend.calledWith(body)).to.be.true
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user