mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2026-06-04 09:50:42 +02:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6ce1806359 | |||
| f05a513767 | |||
| d03c338b48 | |||
| 5e5a988f7a | |||
| 6d1f0b27df | |||
| d01a7cb756 | |||
| cae874ef05 | |||
| 733afc3e29 |
@@ -14,6 +14,7 @@
|
|||||||
<ui-text-input-with-label ref="sequenceInput" v-model="selectedSeries.sequence" :label="$strings.LabelSequence" />
|
<ui-text-input-with-label ref="sequenceInput" v-model="selectedSeries.sequence" :label="$strings.LabelSequence" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="error" class="text-error text-sm mt-2 p-1">{{ error }}</div>
|
||||||
<div class="flex justify-end mt-2 p-1">
|
<div class="flex justify-end mt-2 p-1">
|
||||||
<ui-btn type="submit">{{ $strings.ButtonSubmit }}</ui-btn>
|
<ui-btn type="submit">{{ $strings.ButtonSubmit }}</ui-btn>
|
||||||
</div>
|
</div>
|
||||||
@@ -34,12 +35,17 @@ export default {
|
|||||||
existingSeriesNames: {
|
existingSeriesNames: {
|
||||||
type: Array,
|
type: Array,
|
||||||
default: () => []
|
default: () => []
|
||||||
|
},
|
||||||
|
originalSeriesSequence: {
|
||||||
|
type: String,
|
||||||
|
default: null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
el: null,
|
el: null,
|
||||||
content: null
|
content: null,
|
||||||
|
error: null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
@@ -85,10 +91,17 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
submitSeriesForm() {
|
submitSeriesForm() {
|
||||||
|
this.error = null
|
||||||
|
|
||||||
if (this.$refs.newSeriesSelect) {
|
if (this.$refs.newSeriesSelect) {
|
||||||
this.$refs.newSeriesSelect.blur()
|
this.$refs.newSeriesSelect.blur()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.selectedSeries.sequence !== this.originalSeriesSequence && this.selectedSeries.sequence.includes(' ')) {
|
||||||
|
this.error = this.$strings.MessageSeriesSequenceCannotContainSpaces
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
this.$emit('submit')
|
this.$emit('submit')
|
||||||
},
|
},
|
||||||
clickClose() {
|
clickClose() {
|
||||||
@@ -100,6 +113,7 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
setShow() {
|
setShow() {
|
||||||
|
this.error = null
|
||||||
if (!this.el || !this.content) {
|
if (!this.el || !this.content) {
|
||||||
this.init()
|
this.init()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -244,8 +244,8 @@ export default {
|
|||||||
const sizeInMb = payloadSize / 1024 / 1024
|
const sizeInMb = payloadSize / 1024 / 1024
|
||||||
const sizeInMbPretty = sizeInMb.toFixed(2) + 'MB'
|
const sizeInMbPretty = sizeInMb.toFixed(2) + 'MB'
|
||||||
console.log('Request size', sizeInMb)
|
console.log('Request size', sizeInMb)
|
||||||
if (sizeInMb > 4.99) {
|
if (sizeInMb > 9.99) {
|
||||||
return this.$toast.error(`Request is too large (${sizeInMbPretty}) should be < 5Mb`)
|
return this.$toast.error(`Request is too large (${sizeInMbPretty}) should be < 10Mb`)
|
||||||
}
|
}
|
||||||
|
|
||||||
this.processing = true
|
this.processing = true
|
||||||
|
|||||||
@@ -162,7 +162,7 @@ export default {
|
|||||||
} else {
|
} else {
|
||||||
// Find closest bitrate rounding up
|
// Find closest bitrate rounding up
|
||||||
const bitratesToMatch = [32, 64, 128, 192]
|
const bitratesToMatch = [32, 64, 128, 192]
|
||||||
const closestBitrate = bitratesToMatch.find((bitrate) => bitrate >= this.currentBitrate)
|
const closestBitrate = bitratesToMatch.find((bitrate) => bitrate >= this.currentBitrate) || 192
|
||||||
this.selectedBitrate = closestBitrate + 'k'
|
this.selectedBitrate = closestBitrate + 'k'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<div>
|
<div>
|
||||||
<ui-multi-select-query-input v-model="seriesItems" text-key="displayName" :label="$strings.LabelSeries" :disabled="disabled" readonly show-edit @edit="editSeriesItem" @add="addNewSeries" />
|
<ui-multi-select-query-input v-model="seriesItems" text-key="displayName" :label="$strings.LabelSeries" :disabled="disabled" readonly show-edit @edit="editSeriesItem" @add="addNewSeries" />
|
||||||
|
|
||||||
<modals-edit-series-input-inner-modal v-model="showSeriesForm" :selected-series="selectedSeries" :existing-series-names="existingSeriesNames" @submit="submitSeriesForm" />
|
<modals-edit-series-input-inner-modal v-model="showSeriesForm" :selected-series="selectedSeries" :existing-series-names="existingSeriesNames" :original-series-sequence="originalSeriesSequence" @submit="submitSeriesForm" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -18,6 +18,7 @@ export default {
|
|||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
selectedSeries: null,
|
selectedSeries: null,
|
||||||
|
originalSeriesSequence: null,
|
||||||
showSeriesForm: false
|
showSeriesForm: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -59,6 +60,7 @@ export default {
|
|||||||
..._series
|
..._series
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.originalSeriesSequence = _series.sequence
|
||||||
this.showSeriesForm = true
|
this.showSeriesForm = true
|
||||||
},
|
},
|
||||||
addNewSeries() {
|
addNewSeries() {
|
||||||
@@ -68,6 +70,7 @@ export default {
|
|||||||
sequence: ''
|
sequence: ''
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.originalSeriesSequence = null
|
||||||
this.showSeriesForm = true
|
this.showSeriesForm = true
|
||||||
},
|
},
|
||||||
submitSeriesForm() {
|
submitSeriesForm() {
|
||||||
|
|||||||
@@ -359,15 +359,14 @@ export default {
|
|||||||
// Check if path already exists before starting upload
|
// Check if path already exists before starting upload
|
||||||
// uploading fails if path already exists
|
// uploading fails if path already exists
|
||||||
for (const item of items) {
|
for (const item of items) {
|
||||||
const filepath = Path.join(this.selectedFolder.fullPath, item.directory)
|
|
||||||
const exists = await this.$axios
|
const exists = await this.$axios
|
||||||
.$post(`/api/filesystem/pathexists`, { filepath, directory: item.directory, folderPath: this.selectedFolder.fullPath })
|
.$post(`/api/filesystem/pathexists`, { directory: item.directory, folderPath: this.selectedFolder.fullPath })
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
if (data.exists) {
|
if (data.exists) {
|
||||||
if (data.libraryItemTitle) {
|
if (data.libraryItemTitle) {
|
||||||
this.$toast.error(this.$getString('ToastUploaderItemExistsInSubdirectoryError', [data.libraryItemTitle]))
|
this.$toast.error(this.$getString('ToastUploaderItemExistsInSubdirectoryError', [data.libraryItemTitle]))
|
||||||
} else {
|
} else {
|
||||||
this.$toast.error(this.$getString('ToastUploaderFilepathExistsError', [filepath]))
|
this.$toast.error(this.$getString('ToastUploaderFilepathExistsError', [Path.join(this.selectedFolder.fullPath, item.directory)]))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return data.exists
|
return data.exists
|
||||||
|
|||||||
@@ -856,6 +856,7 @@
|
|||||||
"MessageScheduleRunEveryWeekdayAtTime": "Run every {0} at {1}",
|
"MessageScheduleRunEveryWeekdayAtTime": "Run every {0} at {1}",
|
||||||
"MessageSearchResultsFor": "Search results for",
|
"MessageSearchResultsFor": "Search results for",
|
||||||
"MessageSelected": "{0} selected",
|
"MessageSelected": "{0} selected",
|
||||||
|
"MessageSeriesSequenceCannotContainSpaces": "Series sequence cannot contain spaces",
|
||||||
"MessageServerCouldNotBeReached": "Server could not be reached",
|
"MessageServerCouldNotBeReached": "Server could not be reached",
|
||||||
"MessageSetChaptersFromTracksDescription": "Set chapters using each audio file as a chapter and chapter title as the audio file name",
|
"MessageSetChaptersFromTracksDescription": "Set chapters using each audio file as a chapter and chapter title as the audio file name",
|
||||||
"MessageShareExpirationWillBe": "Expiration will be <strong>{0}</strong>",
|
"MessageShareExpirationWillBe": "Expiration will be <strong>{0}</strong>",
|
||||||
|
|||||||
+1
-1
@@ -310,7 +310,7 @@ class Server {
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
router.use(express.urlencoded({ extended: true, limit: '5mb' }))
|
router.use(express.urlencoded({ extended: true, limit: '5mb' }))
|
||||||
router.use(express.json({ limit: '5mb' }))
|
router.use(express.json({ limit: '10mb' }))
|
||||||
|
|
||||||
router.use('/api', this.auth.ifAuthNeeded(this.authMiddleware.bind(this)), this.apiRouter.router)
|
router.use('/api', this.auth.ifAuthNeeded(this.authMiddleware.bind(this)), this.apiRouter.router)
|
||||||
router.use('/hls', this.hlsRouter.router)
|
router.use('/hls', this.hlsRouter.router)
|
||||||
|
|||||||
@@ -84,49 +84,67 @@ class FileSystemController {
|
|||||||
*/
|
*/
|
||||||
async checkPathExists(req, res) {
|
async checkPathExists(req, res) {
|
||||||
if (!req.user.canUpload) {
|
if (!req.user.canUpload) {
|
||||||
Logger.error(`[FileSystemController] Non-admin user "${req.user.username}" attempting to check path exists`)
|
Logger.error(`[FileSystemController] User "${req.user.username}" without upload permissions attempting to check path exists`)
|
||||||
return res.sendStatus(403)
|
return res.sendStatus(403)
|
||||||
}
|
}
|
||||||
|
|
||||||
const { filepath, directory, folderPath } = req.body
|
const { directory, folderPath } = req.body
|
||||||
|
|
||||||
if (!filepath?.length || typeof filepath !== 'string') {
|
if (!directory?.length || typeof directory !== 'string' || !folderPath?.length || typeof folderPath !== 'string') {
|
||||||
|
Logger.error(`[FileSystemController] Invalid request body: ${JSON.stringify(req.body)}`)
|
||||||
|
return res.status(400).json({
|
||||||
|
error: 'Invalid request body'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that library folder exists
|
||||||
|
const libraryFolder = await Database.libraryFolderModel.findOne({
|
||||||
|
where: {
|
||||||
|
path: folderPath
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!libraryFolder) {
|
||||||
|
Logger.error(`[FileSystemController] Library folder not found: ${folderPath}`)
|
||||||
|
return res.sendStatus(404)
|
||||||
|
}
|
||||||
|
|
||||||
|
const filepath = Path.posix.join(libraryFolder.path, directory)
|
||||||
|
// Ensure filepath is inside library folder (prevents directory traversal)
|
||||||
|
if (!filepath.startsWith(libraryFolder.path)) {
|
||||||
|
Logger.error(`[FileSystemController] Filepath is not inside library folder: ${filepath}`)
|
||||||
return res.sendStatus(400)
|
return res.sendStatus(400)
|
||||||
}
|
}
|
||||||
|
|
||||||
const exists = await fs.pathExists(filepath)
|
if (await fs.pathExists(filepath)) {
|
||||||
|
|
||||||
if (exists) {
|
|
||||||
return res.json({
|
return res.json({
|
||||||
exists: true
|
exists: true
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// If directory and folderPath are passed in, check if a library item exists in a subdirectory
|
// Check if a library item exists in a subdirectory
|
||||||
// See: https://github.com/advplyr/audiobookshelf/issues/4146
|
// See: https://github.com/advplyr/audiobookshelf/issues/4146
|
||||||
if (typeof directory === 'string' && typeof folderPath === 'string' && directory.length > 0 && folderPath.length > 0) {
|
const cleanedDirectory = directory.split('/').filter(Boolean).join('/')
|
||||||
const cleanedDirectory = directory.split('/').filter(Boolean).join('/')
|
if (cleanedDirectory.includes('/')) {
|
||||||
if (cleanedDirectory.includes('/')) {
|
// Can only be 2 levels deep
|
||||||
// Can only be 2 levels deep
|
const possiblePaths = []
|
||||||
const possiblePaths = []
|
const subdir = Path.dirname(directory)
|
||||||
const subdir = Path.dirname(directory)
|
possiblePaths.push(fileUtils.filePathToPOSIX(Path.join(folderPath, subdir)))
|
||||||
possiblePaths.push(fileUtils.filePathToPOSIX(Path.join(folderPath, subdir)))
|
if (subdir.includes('/')) {
|
||||||
if (subdir.includes('/')) {
|
possiblePaths.push(fileUtils.filePathToPOSIX(Path.join(folderPath, Path.dirname(subdir))))
|
||||||
possiblePaths.push(fileUtils.filePathToPOSIX(Path.join(folderPath, Path.dirname(subdir))))
|
}
|
||||||
}
|
|
||||||
|
|
||||||
const libraryItem = await Database.libraryItemModel.findOne({
|
const libraryItem = await Database.libraryItemModel.findOne({
|
||||||
where: {
|
where: {
|
||||||
path: possiblePaths
|
path: possiblePaths
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (libraryItem) {
|
||||||
|
return res.json({
|
||||||
|
exists: true,
|
||||||
|
libraryItemTitle: libraryItem.title
|
||||||
})
|
})
|
||||||
|
|
||||||
if (libraryItem) {
|
|
||||||
return res.json({
|
|
||||||
exists: true,
|
|
||||||
libraryItemTitle: libraryItem.title
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -246,9 +246,10 @@ class MediaProgress extends Model {
|
|||||||
// For local sync
|
// For local sync
|
||||||
if (progressPayload.lastUpdate) {
|
if (progressPayload.lastUpdate) {
|
||||||
this.updatedAt = progressPayload.lastUpdate
|
this.updatedAt = progressPayload.lastUpdate
|
||||||
|
this.changed('updatedAt', true)
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.save()
|
return this.save({ silent: !!progressPayload.lastUpdate })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -205,7 +205,7 @@ function extractEpisodeData(item) {
|
|||||||
} else if (typeof guidItem?._ === 'string') {
|
} else if (typeof guidItem?._ === 'string') {
|
||||||
episode.guid = guidItem._
|
episode.guid = guidItem._
|
||||||
} else {
|
} else {
|
||||||
Logger.error(`[podcastUtils] Invalid guid ${item['guid']} for ${episode.enclosure.url}`)
|
Logger.error(`[podcastUtils] Invalid guid for ${episode.enclosure.url}`, item['guid'])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user