feat(upload): spawn worker dynamically, when uploading
This commit is contained in:
parent
5070d06997
commit
acfc4a687b
|
@ -1,6 +1,5 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { reactive, ref, computed } from 'vue'
|
import { reactive, ref, computed } from 'vue'
|
||||||
import { whenever, computedAsync, useWebWorkerFn } from '@vueuse/core'
|
|
||||||
import { UseTimeAgo } from '@vueuse/components'
|
import { UseTimeAgo } from '@vueuse/components'
|
||||||
import { Icon } from '@iconify/vue';
|
import { Icon } from '@iconify/vue';
|
||||||
import { useUploadsStore } from '~/ui/stores/upload'
|
import { useUploadsStore } from '~/ui/stores/upload'
|
||||||
|
@ -52,31 +51,28 @@ const libraryModalAlertOpen = ref(true)
|
||||||
const serverPath = ref('/srv/funkwhale/data/music')
|
const serverPath = ref('/srv/funkwhale/data/music')
|
||||||
|
|
||||||
// Upload
|
// Upload
|
||||||
const files = ref<File[]>([])
|
|
||||||
|
|
||||||
const combinedFileSize = computed(() => bytesToHumanSize(
|
const combinedFileSize = computed(() => bytesToHumanSize(
|
||||||
files.value.reduce((acc, file) => acc + file.size, 0)
|
uploads.queue.reduce((acc, { file }) => acc + file.size, 0)
|
||||||
))
|
))
|
||||||
|
|
||||||
const uploads = useUploadsStore()
|
const uploads = useUploadsStore()
|
||||||
const processFiles = (fileList: FileList) => {
|
const processFiles = (fileList: FileList) => {
|
||||||
console.log('processFiles', fileList)
|
console.log('processFiles', fileList)
|
||||||
// NOTE: Append fileList elements in reverse order so they appear in the UI in the order, user selected them
|
|
||||||
for (const file of Array.from(fileList).reverse()) {
|
|
||||||
files.value.push(file)
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const file of fileList) {
|
for (const file of fileList) {
|
||||||
uploads.queueUpload(file)
|
uploads.queueUpload(file)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const cancel = () => {
|
||||||
|
libraryOpen.value = false
|
||||||
|
uploads.cancelAll()
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<h1>Upload</h1>
|
<h1 class="mr-auto">Upload</h1>
|
||||||
<div class="flex-spacer" />
|
|
||||||
|
|
||||||
<div class="filesystem-stats">
|
<div class="filesystem-stats">
|
||||||
<div class="filesystem-stats--progress" :style="`--progress: ${filesystemProgress}%`" />
|
<div class="filesystem-stats--progress" :style="`--progress: ${filesystemProgress}%`" />
|
||||||
|
@ -95,7 +91,7 @@ const processFiles = (fileList: FileList) => {
|
||||||
|
|
||||||
<p> Select a destination for your audio files: </p>
|
<p> Select a destination for your audio files: </p>
|
||||||
|
|
||||||
<div class="flex space-between">
|
<div class="flex justify-between">
|
||||||
<FwCard
|
<FwCard
|
||||||
v-for="tab in tabs" :key="tab.label"
|
v-for="tab in tabs" :key="tab.label"
|
||||||
:title="tab.label"
|
:title="tab.label"
|
||||||
|
@ -134,10 +130,10 @@ const processFiles = (fileList: FileList) => {
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- Upload path -->
|
<!-- Upload path -->
|
||||||
<div v-if="files && files.length > 0">
|
<div v-if="uploads.queue.length > 0">
|
||||||
<div class="list-header">
|
<div class="list-header">
|
||||||
<div class="file-count">
|
<div class="file-count">
|
||||||
{{ files.length }} files, {{ combinedFileSize }}
|
{{ uploads.queue.length }} files, {{ combinedFileSize }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<FwButton color="secondary">All</FwButton>
|
<FwButton color="secondary">All</FwButton>
|
||||||
|
@ -212,7 +208,7 @@ const processFiles = (fileList: FileList) => {
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #actions>
|
<template #actions>
|
||||||
<FwButton @click="libraryOpen = false" color="secondary">Cancel</FwButton>
|
<FwButton @click="cancel" color="secondary">Cancel</FwButton>
|
||||||
<FwButton @click="libraryOpen = false">
|
<FwButton @click="libraryOpen = false">
|
||||||
{{ uploads.queue.length ? 'Continue in background' : 'Save and close' }}
|
{{ uploads.queue.length ? 'Continue in background' : 'Save and close' }}
|
||||||
</FwButton>
|
</FwButton>
|
||||||
|
@ -228,11 +224,7 @@ h1 {
|
||||||
font-family: Lato, sans-serif;
|
font-family: Lato, sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
.flex {
|
.flex:not(.flex-col) {
|
||||||
display: flex;
|
|
||||||
align-items: flex-start;
|
|
||||||
|
|
||||||
&:not(.flex-col) {
|
|
||||||
.funkwhale.button {
|
.funkwhale.button {
|
||||||
&:first-child {
|
&:first-child {
|
||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
|
@ -242,19 +234,6 @@ h1 {
|
||||||
margin-right: 0;
|
margin-right: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.items-center {
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.space-between {
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
.flex-spacer {
|
|
||||||
flex: 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.filesystem-stats {
|
.filesystem-stats {
|
||||||
|
@ -348,14 +327,6 @@ h1 {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.w-full {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mr-4 {
|
|
||||||
margin-right: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
label {
|
label {
|
||||||
line-height: 1.2;
|
line-height: 1.2;
|
||||||
font-weight: 900;
|
font-weight: 900;
|
||||||
|
@ -467,6 +438,4 @@ label {
|
||||||
border-color: transparent !important;
|
border-color: transparent !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
|
|
||||||
import { defineStore, acceptHMRUpdate } from 'pinia'
|
import { defineStore, acceptHMRUpdate } from 'pinia'
|
||||||
import { computed, reactive, readonly, ref, watchEffect, markRaw, toRaw } from 'vue'
|
import { computed, reactive, readonly, ref, watchEffect, markRaw, toRaw, type Ref } from 'vue'
|
||||||
import { whenever, useWebWorker } from '@vueuse/core'
|
import { whenever, useWebWorker, type UseWebWorkerReturn } from '@vueuse/core'
|
||||||
|
import { not } from '@vueuse/math'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import FileMetadataParserWorker from '~/ui/workers/file-metadata-parser.ts?worker'
|
|
||||||
|
import FileMetadataParserWorker, { type MetadataParsingResult } from '~/ui/workers/file-metadata-parser.ts?worker'
|
||||||
|
|
||||||
import { getCoverUrl, getTags, type Tags } from '~/ui/composables/metadata'
|
import { getCoverUrl, getTags, type Tags } from '~/ui/composables/metadata'
|
||||||
|
|
||||||
|
@ -28,9 +30,25 @@ interface UploadQueueEntry {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useUploadsStore = defineStore('uploads', () => {
|
export const useUploadsStore = defineStore('uploads', () => {
|
||||||
|
const uploadQueue: UploadQueueEntry[] = reactive([])
|
||||||
|
const currentIndex = ref(0)
|
||||||
|
const currentUpload = computed(() => uploadQueue[currentIndex.value])
|
||||||
|
const isUploading = computed(() => !!currentUpload.value)
|
||||||
|
|
||||||
// Tag extraction with a Web Worker
|
// Tag extraction with a Web Worker
|
||||||
const { post: retrieveMetadata, data: workerData, worker } = useWebWorker(FileMetadataParserWorker)
|
const worker = ref<UseWebWorkerReturn<MetadataParsingResult>>()
|
||||||
whenever(workerData, (reactiveData) => {
|
const retrieveMetadata = (entry: Pick<UploadQueueEntry, 'id' | 'file'>) => {
|
||||||
|
if (!worker.value) worker.value = useWebWorker<MetadataParsingResult>(FileMetadataParserWorker)
|
||||||
|
worker.value.post(entry)
|
||||||
|
}
|
||||||
|
|
||||||
|
whenever(not(isUploading), () => {
|
||||||
|
worker.value?.terminate()
|
||||||
|
worker.value = undefined
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
whenever(() => worker.value?.data, (reactiveData) => {
|
||||||
const data = toRaw(reactiveData)
|
const data = toRaw(reactiveData)
|
||||||
if (data.status === 'success') {
|
if (data.status === 'success') {
|
||||||
const id = data.id as number
|
const id = data.id as number
|
||||||
|
@ -72,8 +90,6 @@ export const useUploadsStore = defineStore('uploads', () => {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// TODO: Handle failure with a try/catch block
|
|
||||||
|
|
||||||
console.log(`[${entry.id}] import complete!`)
|
console.log(`[${entry.id}] import complete!`)
|
||||||
entry.importedAt = new Date()
|
entry.importedAt = new Date()
|
||||||
}
|
}
|
||||||
|
@ -91,11 +107,6 @@ export const useUploadsStore = defineStore('uploads', () => {
|
||||||
retrieveMetadata({ id, file })
|
retrieveMetadata({ id, file })
|
||||||
}
|
}
|
||||||
|
|
||||||
const uploadQueue: UploadQueueEntry[] = reactive([])
|
|
||||||
const currentIndex = ref(0)
|
|
||||||
const currentUpload = computed(() => uploadQueue[currentIndex.value])
|
|
||||||
const isUploading = computed(() => !!currentUpload.value)
|
|
||||||
|
|
||||||
// Upload the file whenever it is available
|
// Upload the file whenever it is available
|
||||||
whenever(currentUpload, (entry) => upload(entry).catch((error) => {
|
whenever(currentUpload, (entry) => upload(entry).catch((error) => {
|
||||||
// The tags were missing, so we have cancelled the upload
|
// The tags were missing, so we have cancelled the upload
|
||||||
|
@ -120,11 +131,21 @@ export const useUploadsStore = defineStore('uploads', () => {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const cancelAll = () => {
|
||||||
|
for (const upload of uploadQueue) {
|
||||||
|
upload.abortController.abort()
|
||||||
|
}
|
||||||
|
|
||||||
|
uploadQueue.length = 0
|
||||||
|
currentIndex.value = 0
|
||||||
|
}
|
||||||
|
|
||||||
// Return public API
|
// Return public API
|
||||||
return {
|
return {
|
||||||
isUploading,
|
isUploading,
|
||||||
queueUpload,
|
queueUpload,
|
||||||
currentUpload,
|
currentUpload,
|
||||||
|
cancelAll,
|
||||||
queue: readonly(uploadQueue)
|
queue: readonly(uploadQueue)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,6 +1,21 @@
|
||||||
/// <reference lib="webworker" />
|
/// <reference lib="webworker" />
|
||||||
|
|
||||||
import { getCoverUrl, getTags } from '~/ui/composables/metadata'
|
import { getCoverUrl, getTags, type Tags } from '~/ui/composables/metadata'
|
||||||
|
|
||||||
|
export interface MetadataParsingSuccess {
|
||||||
|
id: number
|
||||||
|
status: 'success'
|
||||||
|
tags: Tags
|
||||||
|
coverUrl: string | undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MetadataParsingFailure {
|
||||||
|
id: number
|
||||||
|
status: 'failure'
|
||||||
|
error: Error
|
||||||
|
}
|
||||||
|
|
||||||
|
export type MetadataParsingResult = MetadataParsingSuccess | MetadataParsingFailure
|
||||||
|
|
||||||
|
|
||||||
const parse = async (id: number, file: File) => {
|
const parse = async (id: number, file: File) => {
|
||||||
|
|
Loading…
Reference in New Issue