mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2025-01-03 00:06:46 +01:00
Adding upload permission to users, directory structure readme update
This commit is contained in:
parent
587adb3773
commit
8f1152762a
@ -16,11 +16,11 @@
|
|||||||
<span class="pl-2">Update is available! Check release notes for v{{ latestVersion }}</span>
|
<span class="pl-2">Update is available! Check release notes for v{{ latestVersion }}</span>
|
||||||
</a> -->
|
</a> -->
|
||||||
|
|
||||||
<nuxt-link v-if="isRootUser" to="/upload" class="outline-none hover:text-gray-200 cursor-pointer w-8 h-8 flex items-center justify-center mr-4">
|
<nuxt-link v-if="userCanUpload" to="/upload" class="outline-none hover:text-gray-200 cursor-pointer w-8 h-8 flex items-center justify-center">
|
||||||
<span class="material-icons">upload</span>
|
<span class="material-icons">upload</span>
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
|
|
||||||
<nuxt-link v-if="isRootUser" to="/config" class="outline-none hover:text-gray-200 cursor-pointer w-8 h-8 flex items-center justify-center">
|
<nuxt-link v-if="isRootUser" to="/config" class="outline-none hover:text-gray-200 cursor-pointer w-8 h-8 flex items-center justify-center ml-4">
|
||||||
<span class="material-icons">settings</span>
|
<span class="material-icons">settings</span>
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
|
|
||||||
@ -96,6 +96,9 @@ export default {
|
|||||||
userCanDelete() {
|
userCanDelete() {
|
||||||
return this.$store.getters['user/getUserCanDelete']
|
return this.$store.getters['user/getUserCanDelete']
|
||||||
},
|
},
|
||||||
|
userCanUpload() {
|
||||||
|
return this.$store.getters['user/getUserCanUpload']
|
||||||
|
},
|
||||||
selectedIsRead() {
|
selectedIsRead() {
|
||||||
// Find an audiobook that is not read, if none then all audiobooks read
|
// Find an audiobook that is not read, if none then all audiobooks read
|
||||||
return !this.selectedAudiobooks.find((ab) => {
|
return !this.selectedAudiobooks.find((ab) => {
|
||||||
|
@ -18,7 +18,7 @@
|
|||||||
</li>
|
</li>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<template v-for="item in items">
|
<template v-for="item in items">
|
||||||
<li :key="item.id" class="text-gray-50 select-none relative cursor-pointer hover:bg-black-400" role="option" @click="clickedOption(item)">
|
<li :key="item.id" class="text-gray-50 select-none relative cursor-pointer hover:bg-black-400 py-1" role="option" @click="clickedOption(item)">
|
||||||
<template v-if="item.type === 'audiobook'">
|
<template v-if="item.type === 'audiobook'">
|
||||||
<cards-audiobook-search-card :audiobook="item.data" />
|
<cards-audiobook-search-card :audiobook="item.data" />
|
||||||
</template>
|
</template>
|
||||||
|
@ -27,9 +27,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="!isEditingRoot && newUser.permissions" class="w-full border-t border-b border-black-200 py-2 mt-4">
|
<div v-if="!isEditingRoot && newUser.permissions" class="w-full border-t border-b border-black-200 py-2 px-3 mt-4">
|
||||||
<p class="text-lg mb-2">Permissions</p>
|
<p class="text-lg mb-2 font-semibold">Permissions</p>
|
||||||
<div class="flex items-center my-2 max-w-lg">
|
<div class="flex items-center my-2 max-w-md">
|
||||||
<div class="w-1/2">
|
<div class="w-1/2">
|
||||||
<p>Can Download</p>
|
<p>Can Download</p>
|
||||||
</div>
|
</div>
|
||||||
@ -38,7 +38,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center my-2 max-w-lg">
|
<div class="flex items-center my-2 max-w-md">
|
||||||
<div class="w-1/2">
|
<div class="w-1/2">
|
||||||
<p>Can Update</p>
|
<p>Can Update</p>
|
||||||
</div>
|
</div>
|
||||||
@ -47,7 +47,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center my-2 max-w-lg">
|
<div class="flex items-center my-2 max-w-md">
|
||||||
<div class="w-1/2">
|
<div class="w-1/2">
|
||||||
<p>Can Delete</p>
|
<p>Can Delete</p>
|
||||||
</div>
|
</div>
|
||||||
@ -55,6 +55,15 @@
|
|||||||
<ui-toggle-switch v-model="newUser.permissions.delete" />
|
<ui-toggle-switch v-model="newUser.permissions.delete" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center my-2 max-w-md">
|
||||||
|
<div class="w-1/2">
|
||||||
|
<p>Can Upload</p>
|
||||||
|
</div>
|
||||||
|
<div class="w-1/2">
|
||||||
|
<ui-toggle-switch v-model="newUser.permissions.upload" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex pt-4">
|
<div class="flex pt-4">
|
||||||
@ -179,7 +188,8 @@ export default {
|
|||||||
this.newUser.permissions = {
|
this.newUser.permissions = {
|
||||||
download: type !== 'guest',
|
download: type !== 'guest',
|
||||||
update: type === 'admin',
|
update: type === 'admin',
|
||||||
delete: type === 'admin'
|
delete: type === 'admin',
|
||||||
|
upload: type === 'admin'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
init() {
|
init() {
|
||||||
@ -201,7 +211,8 @@ export default {
|
|||||||
permissions: {
|
permissions: {
|
||||||
download: true,
|
download: true,
|
||||||
update: false,
|
update: false,
|
||||||
delete: false
|
delete: false,
|
||||||
|
upload: false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookshelf-client",
|
"name": "audiobookshelf-client",
|
||||||
"version": "1.1.12",
|
"version": "1.1.13",
|
||||||
"description": "Audiobook manager and player",
|
"description": "Audiobook manager and player",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
@ -13,10 +13,12 @@
|
|||||||
<div class="mb-2">
|
<div class="mb-2">
|
||||||
<h1 class="text-2xl font-book leading-7">{{ title }}</h1>
|
<h1 class="text-2xl font-book leading-7">{{ title }}</h1>
|
||||||
<h3 v-if="series" class="font-book text-gray-300 text-lg leading-7">{{ seriesText }}</h3>
|
<h3 v-if="series" class="font-book text-gray-300 text-lg leading-7">{{ seriesText }}</h3>
|
||||||
|
<div class="w-min">
|
||||||
<ui-tooltip :text="authorTooltipText" direction="bottom">
|
<ui-tooltip :text="authorTooltipText" direction="bottom">
|
||||||
<p class="text-sm text-gray-100 leading-7">by {{ author }}</p>
|
<span class="text-sm text-gray-100 leading-7 whitespace-nowrap">by {{ author }}</span>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
</div>
|
</div>
|
||||||
<p class="text-gray-300 text-sm my-1">
|
<p class="text-gray-300 text-sm my-1">
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<div id="page-wrapper" class="page p-6" :class="streamAudiobook ? 'streaming' : ''">
|
<div id="page-wrapper" class="page p-6" :class="streamAudiobook ? 'streaming' : ''">
|
||||||
<main class="container mx-auto h-full max-w-screen-lg p-6">
|
<main class="container mx-auto h-full max-w-screen-lg p-6">
|
||||||
<article class="max-h-full overflow-y-auto relative flex flex-col bg-primary shadow-xl rounded-md" @drop="drop" @dragover="dragover" @dragleave="dragleave" @dragenter="dragenter">
|
<article class="max-h-full overflow-y-auto relative flex flex-col rounded-md" @drop="drop" @dragover="dragover" @dragleave="dragleave" @dragenter="dragenter">
|
||||||
<h1 class="text-xl font-book px-4 pt-4 pb-2"><span class="text-error pr-4">(Experimental)</span>Audiobook Uploader</h1>
|
<h1 class="text-xl font-book px-8 pt-4 pb-2">Audiobook Uploader</h1>
|
||||||
|
|
||||||
<div class="flex my-2 px-6">
|
<div class="flex my-2 px-6">
|
||||||
<div class="w-1/2 px-2">
|
<div class="w-1/2 px-2">
|
||||||
@ -170,19 +170,16 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
drop(evt) {
|
drop(evt) {
|
||||||
console.log('Dropped event', evt)
|
|
||||||
this.isDragOver = false
|
this.isDragOver = false
|
||||||
this.preventDefaults(evt)
|
this.preventDefaults(evt)
|
||||||
const files = [...evt.dataTransfer.files]
|
const files = [...evt.dataTransfer.files]
|
||||||
this.filesChanged(files)
|
this.filesChanged(files)
|
||||||
},
|
},
|
||||||
dragover(evt) {
|
dragover(evt) {
|
||||||
console.log('Dragged over', evt)
|
|
||||||
this.isDragOver = true
|
this.isDragOver = true
|
||||||
this.preventDefaults(evt)
|
this.preventDefaults(evt)
|
||||||
},
|
},
|
||||||
dragleave(evt) {
|
dragleave(evt) {
|
||||||
console.log('Dragged leave', evt)
|
|
||||||
this.isDragOver = false
|
this.isDragOver = false
|
||||||
this.preventDefaults(evt)
|
this.preventDefaults(evt)
|
||||||
},
|
},
|
||||||
@ -195,7 +192,6 @@ export default {
|
|||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
},
|
},
|
||||||
filesChanged(files) {
|
filesChanged(files) {
|
||||||
console.log('FilesChanged', files)
|
|
||||||
this.showUploader = false
|
this.showUploader = false
|
||||||
|
|
||||||
for (let i = 0; i < files.length; i++) {
|
for (let i = 0; i < files.length; i++) {
|
||||||
|
@ -33,6 +33,9 @@ export const getters = {
|
|||||||
},
|
},
|
||||||
getUserCanDownload: (state) => {
|
getUserCanDownload: (state) => {
|
||||||
return state.user && state.user.permissions ? !!state.user.permissions.download : false
|
return state.user && state.user.permissions ? !!state.user.permissions.download : false
|
||||||
|
},
|
||||||
|
getUserCanUpload: (state) => {
|
||||||
|
return state.user && state.user.permissions ? !!state.user.permissions.upload : false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookshelf",
|
"name": "audiobookshelf",
|
||||||
"version": "1.1.12",
|
"version": "1.1.13",
|
||||||
"description": "Self-hosted audiobook server for managing and playing audiobooks.",
|
"description": "Self-hosted audiobook server for managing and playing audiobooks.",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
59
readme.md
59
readme.md
@ -9,21 +9,56 @@ Android app is in beta, try it out on the [Google Play Store](https://play.googl
|
|||||||
<img alt="Screenshot1" src="https://github.com/advplyr/audiobookshelf/raw/master/images/ss_streaming.png" />
|
<img alt="Screenshot1" src="https://github.com/advplyr/audiobookshelf/raw/master/images/ss_streaming.png" />
|
||||||
|
|
||||||
|
|
||||||
#### Folder Structures Supported:
|
### Directory Structure
|
||||||
|
|
||||||
```bash
|
Author, Series, Volume Number, Title and Publish Year can all be parsed from your folder structure.
|
||||||
/Title/...
|
|
||||||
/Author/Title/...
|
|
||||||
/Author/Series/Title/...
|
|
||||||
|
|
||||||
Title can start with the publish year like so:
|
**Note**: Files in the root directory `/audiobooks` will be ignored, all audiobooks should be in a directory
|
||||||
/1989 - Book Title/...
|
|
||||||
|
|
||||||
(Optional Setting) Subtitle can be seperated to its own field:
|
**1 Folder:** `/Title/...`\
|
||||||
/Book Title - With a Subtitle/...
|
**2 Folders:** `/Author/Title/...`\
|
||||||
/1989 - Book Title - With a Subtitle/...
|
**3 Folders:** `/Author/Series/Title/...`
|
||||||
will store "With a Subtitle" as the subtitle
|
|
||||||
```
|
\
|
||||||
|
**Parsing publish year**
|
||||||
|
|
||||||
|
`/1984 - Hackers/...`\
|
||||||
|
Will save the publish year as `1984` and the title as `Hackers`
|
||||||
|
|
||||||
|
\
|
||||||
|
**Parsing volume number** (only if there is a series folder)
|
||||||
|
|
||||||
|
`/Book 3 - Hackers/...`\
|
||||||
|
Will save the volume number as `3` and the title as `Hackers`
|
||||||
|
|
||||||
|
`Book` `Volume` `Vol` `Vol.` are all supported case insensitive
|
||||||
|
|
||||||
|
These combinations will also work:\
|
||||||
|
`/Hackers - Vol. 3/...`\
|
||||||
|
`/1984 - Volume 3 - Hackers/...`\
|
||||||
|
`/1984 - Hackers Book 3/...`
|
||||||
|
|
||||||
|
\
|
||||||
|
**Parsing book subtitles** (optional in settings)
|
||||||
|
|
||||||
|
Title Folder: `/Hackers - Heroes of the Computer Revolution/...`
|
||||||
|
|
||||||
|
Will save the title as `Hackers` and the subtitle as `Heroes of the Computer Revolution`
|
||||||
|
|
||||||
|
\
|
||||||
|
**Full example**
|
||||||
|
|
||||||
|
`/Steven Levy/The Hacker Series/1984 - Hackers - Heroes of the Computer Revolution - Vol. 1/...`
|
||||||
|
|
||||||
|
Becomes:\
|
||||||
|
|
||||||
|
| Author | Steven Levy |
|
||||||
|
|---------------|-----------------------------------|
|
||||||
|
| Series | The Hacker Series |
|
||||||
|
| Publish Year | 1984 |
|
||||||
|
| Title | Hackers |
|
||||||
|
| Subtitle | Heroes of the Computer Revolution |
|
||||||
|
| Volume Number | 1 |
|
||||||
|
|
||||||
|
|
||||||
#### Features coming soon:
|
#### Features coming soon:
|
||||||
|
@ -123,6 +123,51 @@ class Server {
|
|||||||
this.auth.authMiddleware(req, res, next)
|
this.auth.authMiddleware(req, res, next)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async handleUpload(req, res) {
|
||||||
|
if (!req.user.canUpload) {
|
||||||
|
Logger.warn('User attempted to upload without permission', req.user)
|
||||||
|
return res.sendStatus(403)
|
||||||
|
}
|
||||||
|
var files = Object.values(req.files)
|
||||||
|
var title = req.body.title
|
||||||
|
var author = req.body.author
|
||||||
|
var series = req.body.series
|
||||||
|
|
||||||
|
if (!files.length || !title || !author) {
|
||||||
|
return res.json({
|
||||||
|
error: 'Invalid post data received'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var outputDirectory = ''
|
||||||
|
if (series && series.length && series !== 'null') {
|
||||||
|
outputDirectory = Path.join(this.AudiobookPath, author, series, title)
|
||||||
|
} else {
|
||||||
|
outputDirectory = Path.join(this.AudiobookPath, author, title)
|
||||||
|
}
|
||||||
|
|
||||||
|
var exists = await fs.pathExists(outputDirectory)
|
||||||
|
if (exists) {
|
||||||
|
Logger.error(`[Server] Upload directory "${outputDirectory}" already exists`)
|
||||||
|
return res.json({
|
||||||
|
error: `Directory "${outputDirectory}" already exists`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
await fs.ensureDir(outputDirectory)
|
||||||
|
Logger.info(`Uploading ${files.length} files to`, outputDirectory)
|
||||||
|
|
||||||
|
for (let i = 0; i < files.length; i++) {
|
||||||
|
var file = files[i]
|
||||||
|
|
||||||
|
var path = Path.join(outputDirectory, file.name)
|
||||||
|
await file.mv(path).catch((error) => {
|
||||||
|
Logger.error('Failed to move file', path, error)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
res.sendStatus(200)
|
||||||
|
}
|
||||||
|
|
||||||
async start() {
|
async start() {
|
||||||
Logger.info('=== Starting Server ===')
|
Logger.info('=== Starting Server ===')
|
||||||
|
|
||||||
@ -157,38 +202,7 @@ class Server {
|
|||||||
// app.use('/hls', this.hlsController.router)
|
// app.use('/hls', this.hlsController.router)
|
||||||
app.use('/feeds', this.rssFeeds.router)
|
app.use('/feeds', this.rssFeeds.router)
|
||||||
|
|
||||||
app.post('/upload', this.authMiddleware.bind(this), async (req, res) => {
|
app.post('/upload', this.authMiddleware.bind(this), this.handleUpload.bind(this))
|
||||||
var files = Object.values(req.files)
|
|
||||||
var title = req.body.title
|
|
||||||
var author = req.body.author
|
|
||||||
var series = req.body.series
|
|
||||||
|
|
||||||
if (!files.length || !title || !author) {
|
|
||||||
return res.json({
|
|
||||||
error: 'Invalid post data received'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
var outputDirectory = ''
|
|
||||||
if (series && series.length && series !== 'null') {
|
|
||||||
outputDirectory = Path.join(this.AudiobookPath, author, series, title)
|
|
||||||
} else {
|
|
||||||
outputDirectory = Path.join(this.AudiobookPath, author, title)
|
|
||||||
}
|
|
||||||
|
|
||||||
await fs.ensureDir(outputDirectory)
|
|
||||||
Logger.info(`Uploading ${files.length} files to`, outputDirectory)
|
|
||||||
|
|
||||||
for (let i = 0; i < files.length; i++) {
|
|
||||||
var file = files[i]
|
|
||||||
|
|
||||||
var path = Path.join(outputDirectory, file.name)
|
|
||||||
await file.mv(path).catch((error) => {
|
|
||||||
Logger.error('Failed to move file', path, error)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
res.sendStatus(200)
|
|
||||||
})
|
|
||||||
|
|
||||||
app.post('/login', (req, res) => this.auth.login(req, res))
|
app.post('/login', (req, res) => this.auth.login(req, res))
|
||||||
app.post('/logout', this.logout.bind(this))
|
app.post('/logout', this.logout.bind(this))
|
||||||
|
@ -32,6 +32,9 @@ class User {
|
|||||||
get canDownload() {
|
get canDownload() {
|
||||||
return !!this.permissions.download && this.isActive
|
return !!this.permissions.download && this.isActive
|
||||||
}
|
}
|
||||||
|
get canUpload() {
|
||||||
|
return !!this.permissions.upload && this.isActive
|
||||||
|
}
|
||||||
|
|
||||||
getDefaultUserSettings() {
|
getDefaultUserSettings() {
|
||||||
return {
|
return {
|
||||||
@ -47,7 +50,8 @@ class User {
|
|||||||
return {
|
return {
|
||||||
download: true,
|
download: true,
|
||||||
update: true,
|
update: true,
|
||||||
delete: this.id === 'root'
|
delete: this.type === 'root',
|
||||||
|
upload: this.type === 'root' || this.type === 'admin'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -112,6 +116,8 @@ class User {
|
|||||||
this.createdAt = user.createdAt || Date.now()
|
this.createdAt = user.createdAt || Date.now()
|
||||||
this.settings = user.settings || this.getDefaultUserSettings()
|
this.settings = user.settings || this.getDefaultUserSettings()
|
||||||
this.permissions = user.permissions || this.getDefaultUserPermissions()
|
this.permissions = user.permissions || this.getDefaultUserPermissions()
|
||||||
|
// Upload permission added v1.1.13, make sure root user has upload permissions
|
||||||
|
if (this.type === 'root' && !this.permissions.upload) this.permissions.upload = true
|
||||||
}
|
}
|
||||||
|
|
||||||
update(payload) {
|
update(payload) {
|
||||||
|
@ -157,15 +157,6 @@ function getAudiobookDataFromDir(abRootPath, dir, parseSubtitle = false) {
|
|||||||
if (splitDir.length > 0) author = splitDir.pop()
|
if (splitDir.length > 0) author = splitDir.pop()
|
||||||
// There could be many more directories, but only the top 3 are used for naming /author/series/title/
|
// There could be many more directories, but only the top 3 are used for naming /author/series/title/
|
||||||
|
|
||||||
var publishYear = null
|
|
||||||
// If Title is of format 1999 - Title, then use 1999 as publish year
|
|
||||||
var publishYearMatch = title.match(/^([0-9]{4}) - (.+)/)
|
|
||||||
if (publishYearMatch && publishYearMatch.length > 2) {
|
|
||||||
if (!isNaN(publishYearMatch[1])) {
|
|
||||||
publishYear = publishYearMatch[1]
|
|
||||||
title = publishYearMatch[2]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If in a series directory check for volume number match
|
// If in a series directory check for volume number match
|
||||||
/* ACCEPTS:
|
/* ACCEPTS:
|
||||||
@ -196,6 +187,18 @@ function getAudiobookDataFromDir(abRootPath, dir, parseSubtitle = false) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var publishYear = null
|
||||||
|
// If Title is of format 1999 - Title, then use 1999 as publish year
|
||||||
|
var publishYearMatch = title.match(/^([0-9]{4}) - (.+)/)
|
||||||
|
if (publishYearMatch && publishYearMatch.length > 2) {
|
||||||
|
if (!isNaN(publishYearMatch[1])) {
|
||||||
|
publishYear = publishYearMatch[1]
|
||||||
|
title = publishYearMatch[2]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// Subtitle can be parsed from the title if user enabled
|
// Subtitle can be parsed from the title if user enabled
|
||||||
// Subtitle is everything after " - "
|
// Subtitle is everything after " - "
|
||||||
var subtitle = null
|
var subtitle = null
|
||||||
|
Loading…
Reference in New Issue
Block a user