mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2025-01-03 00:06:46 +01:00
Improve track order detection, allow for excluding audio files from tracklist
This commit is contained in:
parent
45ee42bddd
commit
af0365c81f
@ -9,7 +9,15 @@
|
|||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
value: Boolean
|
value: Boolean,
|
||||||
|
onColor: {
|
||||||
|
type: String,
|
||||||
|
default: 'success'
|
||||||
|
},
|
||||||
|
offColor: {
|
||||||
|
type: String,
|
||||||
|
default: 'primary'
|
||||||
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
toggleValue: {
|
toggleValue: {
|
||||||
@ -21,12 +29,11 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
toggleColor() {
|
toggleColor() {
|
||||||
return this.toggleValue ? 'bg-success' : 'bg-primary'
|
return this.toggleValue ? `bg-${this.onColor}` : `bg-${this.offColor}`
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
clickToggle() {
|
clickToggle() {
|
||||||
console.log('click toggle', this.toggleValue)
|
|
||||||
this.toggleValue = !this.toggleValue
|
this.toggleValue = !this.toggleValue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookshelf-client",
|
"name": "audiobookshelf-client",
|
||||||
"version": "1.0.0",
|
"version": "1.0.1",
|
||||||
"description": "Audiobook manager and player",
|
"description": "Audiobook manager and player",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
@ -19,16 +19,15 @@
|
|||||||
<div class="font-mono w-20 text-center">Duration</div>
|
<div class="font-mono w-20 text-center">Duration</div>
|
||||||
<div class="font-mono text-center w-20">Status</div>
|
<div class="font-mono text-center w-20">Status</div>
|
||||||
<div class="font-mono w-56">Notes</div>
|
<div class="font-mono w-56">Notes</div>
|
||||||
|
<div class="font-book w-40">Include in Tracklist</div>
|
||||||
</div>
|
</div>
|
||||||
<draggable v-model="files" v-bind="dragOptions" class="list-group border border-gray-600" draggable=".item" tag="ul" @start="drag = true" @end="drag = false">
|
<draggable v-model="files" v-bind="dragOptions" class="list-group border border-gray-600" draggable=".item" tag="ul" @start="drag = true" @end="drag = false">
|
||||||
<transition-group type="transition" :name="!drag ? 'flip-list' : null">
|
<transition-group type="transition" :name="!drag ? 'flip-list' : null">
|
||||||
<li v-for="(audio, index) in files" :key="audio.path" class="w-full list-group-item item flex items-center">
|
<li v-for="(audio, index) in files" :key="audio.path" :class="audio.include ? 'item' : 'exclude'" class="w-full list-group-item flex items-center">
|
||||||
<div class="font-book text-center px-4 py-1 w-12">
|
<div class="font-book text-center px-4 py-1 w-12">
|
||||||
{{ index + 1 }}
|
{{ audio.include ? index - numExcluded + 1 : -1 }}
|
||||||
</div>
|
|
||||||
<div class="font-book text-center px-4 w-12">
|
|
||||||
{{ audio.index }}
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="font-book text-center px-4 w-12">{{ audio.index }}</div>
|
||||||
<div class="font-book text-center px-2 w-32">
|
<div class="font-book text-center px-2 w-32">
|
||||||
{{ audio.trackNumFromFilename }}
|
{{ audio.trackNumFromFilename }}
|
||||||
</div>
|
</div>
|
||||||
@ -51,6 +50,9 @@
|
|||||||
<div class="font-sans text-xs font-normal w-56">
|
<div class="font-sans text-xs font-normal w-56">
|
||||||
{{ audio.error }}
|
{{ audio.error }}
|
||||||
</div>
|
</div>
|
||||||
|
<div class="font-sans text-xs font-normal w-40 flex justify-center">
|
||||||
|
<ui-toggle-switch v-model="audio.include" :off-color="'error'" @input="includeToggled(audio)" />
|
||||||
|
</div>
|
||||||
</li>
|
</li>
|
||||||
</transition-group>
|
</transition-group>
|
||||||
</draggable>
|
</draggable>
|
||||||
@ -77,10 +79,10 @@ export default {
|
|||||||
console.error('No audiobook...', params.id)
|
console.error('No audiobook...', params.id)
|
||||||
return redirect('/')
|
return redirect('/')
|
||||||
}
|
}
|
||||||
let index = 0
|
|
||||||
return {
|
return {
|
||||||
audiobook,
|
audiobook,
|
||||||
files: audiobook.audioFiles ? audiobook.audioFiles.map((af) => ({ ...af, index: ++index })) : []
|
files: audiobook.audioFiles ? audiobook.audioFiles.map((af) => ({ ...af, include: !af.exclude })) : []
|
||||||
|
// files: audiobook.audioFiles ? audiobook.audioFiles.map((af) => ({ ...af, index: ++index })) : []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
@ -98,6 +100,13 @@ export default {
|
|||||||
audioFiles() {
|
audioFiles() {
|
||||||
return this.audiobook.audioFiles || []
|
return this.audiobook.audioFiles || []
|
||||||
},
|
},
|
||||||
|
numExcluded() {
|
||||||
|
var count = 0
|
||||||
|
this.files.forEach((file) => {
|
||||||
|
if (!file.include) count++
|
||||||
|
})
|
||||||
|
return count
|
||||||
|
},
|
||||||
missingPartChunks() {
|
missingPartChunks() {
|
||||||
if (this.missingParts === 1) return this.missingParts[0]
|
if (this.missingParts === 1) return this.missingParts[0]
|
||||||
var chunks = []
|
var chunks = []
|
||||||
@ -164,15 +173,36 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
includeToggled(audio) {
|
||||||
|
var new_index = 0
|
||||||
|
if (audio.include) {
|
||||||
|
new_index = this.numExcluded
|
||||||
|
}
|
||||||
|
var old_index = this.files.findIndex((f) => f.ino === audio.ino)
|
||||||
|
if (new_index >= this.files.length) {
|
||||||
|
var k = new_index - this.files.length + 1
|
||||||
|
while (k--) {
|
||||||
|
this.files.push(undefined)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.files.splice(new_index, 0, this.files.splice(old_index, 1)[0])
|
||||||
|
},
|
||||||
saveTracklist() {
|
saveTracklist() {
|
||||||
console.log('Tracklist', this.files)
|
var orderedFileData = this.files.map((file) => {
|
||||||
|
return {
|
||||||
|
index: file.index,
|
||||||
|
filename: file.filename,
|
||||||
|
ino: file.ino,
|
||||||
|
exclude: !file.include
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
this.saving = true
|
this.saving = true
|
||||||
this.$axios
|
this.$axios
|
||||||
.$patch(`/api/audiobook/${this.audiobook.id}/tracks`, { files: this.files })
|
.$patch(`/api/audiobook/${this.audiobook.id}/tracks`, { orderedFileData })
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
console.log('Finished patching files', data)
|
console.log('Finished patching files', data)
|
||||||
this.saving = false
|
this.saving = false
|
||||||
// this.$router.go()
|
|
||||||
this.$toast.success('Tracks Updated')
|
this.$toast.success('Tracks Updated')
|
||||||
this.$router.push(`/audiobook/${this.audiobookId}`)
|
this.$router.push(`/audiobook/${this.audiobookId}`)
|
||||||
})
|
})
|
||||||
@ -207,16 +237,26 @@ export default {
|
|||||||
.list-group {
|
.list-group {
|
||||||
min-height: 30px;
|
min-height: 30px;
|
||||||
}
|
}
|
||||||
.list-group-item {
|
.list-group-item:not(.exclude) {
|
||||||
cursor: n-resize;
|
cursor: n-resize;
|
||||||
}
|
}
|
||||||
.list-group-item:not(.ghost):hover {
|
.list-group-item.exclude {
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
.list-group-item:not(.ghost):not(.exclude):hover {
|
||||||
background-color: rgba(0, 0, 0, 0.1);
|
background-color: rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
.list-group-item:nth-child(even):not(.ghost) {
|
.list-group-item:nth-child(even):not(.ghost):not(.exclude) {
|
||||||
background-color: rgba(0, 0, 0, 0.25);
|
background-color: rgba(0, 0, 0, 0.25);
|
||||||
}
|
}
|
||||||
.list-group-item:nth-child(even):not(.ghost):hover {
|
.list-group-item:nth-child(even):not(.ghost):not(.exclude):hover {
|
||||||
background-color: rgba(0, 0, 0, 0.1);
|
background-color: rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.list-group-item.exclude:not(.ghost) {
|
||||||
|
background-color: rgba(255, 0, 0, 0.25);
|
||||||
|
}
|
||||||
|
.list-group-item.exclude:not(.ghost):hover {
|
||||||
|
background-color: rgba(223, 0, 0, 0.25);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
@ -209,7 +209,7 @@ export default {
|
|||||||
streamAudiobook() {
|
streamAudiobook() {
|
||||||
return this.$store.state.streamAudiobook
|
return this.$store.state.streamAudiobook
|
||||||
},
|
},
|
||||||
isStreaming() {
|
streaming() {
|
||||||
return this.streamAudiobook && this.streamAudiobook.id === this.audiobookId
|
return this.streamAudiobook && this.streamAudiobook.id === this.audiobookId
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookshelf",
|
"name": "audiobookshelf",
|
||||||
"version": "1.0.0",
|
"version": "1.0.1",
|
||||||
"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": {
|
||||||
|
@ -182,9 +182,9 @@ class ApiController {
|
|||||||
async updateAudiobookTracks(req, res) {
|
async updateAudiobookTracks(req, res) {
|
||||||
var audiobook = this.db.audiobooks.find(a => a.id === req.params.id)
|
var audiobook = this.db.audiobooks.find(a => a.id === req.params.id)
|
||||||
if (!audiobook) return res.sendStatus(404)
|
if (!audiobook) return res.sendStatus(404)
|
||||||
var files = req.body.files
|
var orderedFileData = req.body.orderedFileData
|
||||||
Logger.info(`Updating audiobook tracks called ${audiobook.id}`)
|
Logger.info(`Updating audiobook tracks called ${audiobook.id}`)
|
||||||
audiobook.updateAudioTracks(files)
|
audiobook.updateAudioTracks(orderedFileData)
|
||||||
await this.db.updateAudiobook(audiobook)
|
await this.db.updateAudiobook(audiobook)
|
||||||
this.emitter('audiobook_updated', audiobook.toJSONMinified())
|
this.emitter('audiobook_updated', audiobook.toJSONMinified())
|
||||||
res.json(audiobook.toJSON())
|
res.json(audiobook.toJSON())
|
||||||
|
@ -25,7 +25,8 @@ class FolderWatcher extends EventEmitter {
|
|||||||
.on('add', (path) => {
|
.on('add', (path) => {
|
||||||
this.onNewFile(path)
|
this.onNewFile(path)
|
||||||
}).on('change', (path) => {
|
}).on('change', (path) => {
|
||||||
this.onFileUpdated(path)
|
// This is triggered from metadata changes, not what we want
|
||||||
|
// this.onFileUpdated(path)
|
||||||
}).on('unlink', path => {
|
}).on('unlink', path => {
|
||||||
this.onFileRemoved(path)
|
this.onFileRemoved(path)
|
||||||
}).on('rename', (path, pathNext) => {
|
}).on('rename', (path, pathNext) => {
|
||||||
|
@ -29,6 +29,7 @@ class AudioFile {
|
|||||||
|
|
||||||
this.manuallyVerified = false
|
this.manuallyVerified = false
|
||||||
this.invalid = false
|
this.invalid = false
|
||||||
|
this.exclude = false
|
||||||
this.error = null
|
this.error = null
|
||||||
|
|
||||||
if (data) {
|
if (data) {
|
||||||
@ -49,6 +50,7 @@ class AudioFile {
|
|||||||
trackNumFromFilename: this.trackNumFromFilename,
|
trackNumFromFilename: this.trackNumFromFilename,
|
||||||
manuallyVerified: !!this.manuallyVerified,
|
manuallyVerified: !!this.manuallyVerified,
|
||||||
invalid: !!this.invalid,
|
invalid: !!this.invalid,
|
||||||
|
exclude: !!this.exclude,
|
||||||
error: this.error || null,
|
error: this.error || null,
|
||||||
format: this.format,
|
format: this.format,
|
||||||
duration: this.duration,
|
duration: this.duration,
|
||||||
@ -76,6 +78,7 @@ class AudioFile {
|
|||||||
this.addedAt = data.addedAt
|
this.addedAt = data.addedAt
|
||||||
this.manuallyVerified = !!data.manuallyVerified
|
this.manuallyVerified = !!data.manuallyVerified
|
||||||
this.invalid = !!data.invalid
|
this.invalid = !!data.invalid
|
||||||
|
this.exclude = !!data.exclude
|
||||||
this.error = data.error || null
|
this.error = data.error || null
|
||||||
|
|
||||||
this.trackNumFromMeta = data.trackNumFromMeta || null
|
this.trackNumFromMeta = data.trackNumFromMeta || null
|
||||||
@ -112,6 +115,7 @@ class AudioFile {
|
|||||||
|
|
||||||
this.manuallyVerified = !!data.manuallyVerified
|
this.manuallyVerified = !!data.manuallyVerified
|
||||||
this.invalid = !!data.invalid
|
this.invalid = !!data.invalid
|
||||||
|
this.exclude = !!data.exclude
|
||||||
this.error = data.error || null
|
this.error = data.error || null
|
||||||
|
|
||||||
this.format = data.format
|
this.format = data.format
|
||||||
@ -131,6 +135,10 @@ class AudioFile {
|
|||||||
this.tagTrack = data.file_tag_track || null
|
this.tagTrack = data.file_tag_track || null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
clone() {
|
||||||
|
return new AudioFile(this.toJSON())
|
||||||
|
}
|
||||||
|
|
||||||
syncFile(newFile) {
|
syncFile(newFile) {
|
||||||
var hasUpdates = false
|
var hasUpdates = false
|
||||||
var keysToSync = ['path', 'fullPath', 'ext', 'filename']
|
var keysToSync = ['path', 'fullPath', 'ext', 'filename']
|
||||||
|
@ -57,14 +57,14 @@ class Audiobook {
|
|||||||
return this.book ? this.book.title : 'No Title'
|
return this.book ? this.book.title : 'No Title'
|
||||||
}
|
}
|
||||||
|
|
||||||
get cover() {
|
|
||||||
return this.book ? this.book.cover : ''
|
|
||||||
}
|
|
||||||
|
|
||||||
get author() {
|
get author() {
|
||||||
return this.book ? this.book.author : 'Unknown'
|
return this.book ? this.book.author : 'Unknown'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get cover() {
|
||||||
|
return this.book ? this.book.cover : ''
|
||||||
|
}
|
||||||
|
|
||||||
get authorLF() {
|
get authorLF() {
|
||||||
return this.book ? this.book.authorLF : null
|
return this.book ? this.book.authorLF : null
|
||||||
}
|
}
|
||||||
@ -273,19 +273,32 @@ class Audiobook {
|
|||||||
return hasUpdates
|
return hasUpdates
|
||||||
}
|
}
|
||||||
|
|
||||||
updateAudioTracks(files) {
|
updateAudioTracks(orderedFileData) {
|
||||||
var index = 1
|
var index = 1
|
||||||
this.audioFiles = files.map((file) => {
|
this.audioFiles = orderedFileData.map((fileData) => {
|
||||||
file.manuallyVerified = true
|
var audioFile = this.audioFiles.find(af => af.ino === fileData.ino)
|
||||||
file.invalid = false
|
audioFile.manuallyVerified = true
|
||||||
file.error = null
|
audioFile.invalid = false
|
||||||
file.index = index++
|
audioFile.error = null
|
||||||
return new AudioFile(file)
|
if (fileData.exclude !== undefined) {
|
||||||
|
audioFile.exclude = !!fileData.exclude
|
||||||
|
}
|
||||||
|
if (audioFile.exclude) {
|
||||||
|
audioFile.index = -1
|
||||||
|
} else {
|
||||||
|
audioFile.index = index++
|
||||||
|
}
|
||||||
|
return audioFile
|
||||||
})
|
})
|
||||||
|
|
||||||
|
this.audioFiles.sort((a, b) => a.index - b.index)
|
||||||
|
|
||||||
this.tracks = []
|
this.tracks = []
|
||||||
this.missingParts = []
|
this.missingParts = []
|
||||||
this.audioFiles.forEach((file) => {
|
this.audioFiles.forEach((file) => {
|
||||||
this.addTrack(file)
|
if (!file.exclude) {
|
||||||
|
this.addTrack(file)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
this.lastUpdate = Date.now()
|
this.lastUpdate = Date.now()
|
||||||
}
|
}
|
||||||
|
@ -66,8 +66,15 @@ function getTrackNumberFromMeta(scanData) {
|
|||||||
return !isNaN(scanData.trackNumber) && scanData.trackNumber !== null ? Number(scanData.trackNumber) : null
|
return !isNaN(scanData.trackNumber) && scanData.trackNumber !== null ? Number(scanData.trackNumber) : null
|
||||||
}
|
}
|
||||||
|
|
||||||
function getTrackNumberFromFilename(filename) {
|
function getTrackNumberFromFilename(title, author, series, publishYear, filename) {
|
||||||
var partbasename = Path.basename(filename, Path.extname(filename))
|
var partbasename = Path.basename(filename, Path.extname(filename))
|
||||||
|
|
||||||
|
// Remove title, author, series, and publishYear from filename if there
|
||||||
|
if (title) partbasename = partbasename.replace(title, '')
|
||||||
|
if (author) partbasename = partbasename.replace(author, '')
|
||||||
|
if (series) partbasename = partbasename.replace(series, '')
|
||||||
|
if (publishYear) partbasename = partbasename.replace(publishYear)
|
||||||
|
|
||||||
var numbersinpath = partbasename.match(/\d+/g)
|
var numbersinpath = partbasename.match(/\d+/g)
|
||||||
if (!numbersinpath) return null
|
if (!numbersinpath) return null
|
||||||
|
|
||||||
@ -92,7 +99,8 @@ async function scanAudioFiles(audiobook, newAudioFiles) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var trackNumFromMeta = getTrackNumberFromMeta(scanData)
|
var trackNumFromMeta = getTrackNumberFromMeta(scanData)
|
||||||
var trackNumFromFilename = getTrackNumberFromFilename(audioFile.filename)
|
var book = audiobook.book || {}
|
||||||
|
var trackNumFromFilename = getTrackNumberFromFilename(book.title, book.author, book.series, book.publishYear, audioFile.filename)
|
||||||
|
|
||||||
var audioFileObj = {
|
var audioFileObj = {
|
||||||
ino: audioFile.ino,
|
ino: audioFile.ino,
|
||||||
|
Loading…
Reference in New Issue
Block a user