Improve track order detection, allow for excluding audio files from tracklist

This commit is contained in:
advplyr 2021-09-04 18:02:42 -05:00
parent 45ee42bddd
commit af0365c81f
10 changed files with 114 additions and 37 deletions

View File

@ -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
} }
} }

View File

@ -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": {

View File

@ -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>

View File

@ -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
} }
}, },

View File

@ -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": {

View File

@ -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())

View File

@ -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) => {

View File

@ -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']

View File

@ -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()
} }

View File

@ -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,