@@ -301,19 +79,11 @@ useEventListener(window, 'beforeunload', (event) => {
>
{{ $t('components.library.FileUpload.empty.noFiles') }}
-
- {{ uploadedFilesCount + erroredFilesCount }}
-
- {{ files.length }}
-
- {{ uploadedFilesCount + erroredFilesCount }}
+ {{ files.filter(file => file.status !== 'queued' && file.status !== 'uploading' ).length }}
{{ files.length }}
@@ -324,27 +94,8 @@ useEventListener(window, 'beforeunload', (event) => {
@click.prevent="currentTab = 'processing'"
>
{{ $t('components.library.FileUpload.link.processing') }}
-
- {{ $t('components.library.FileUpload.empty.noFiles') }}
-
-
- {{ processedFilesCount }}
-
- {{ processableFiles }}
-
-
- {{ processedFilesCount }}
-
- {{ processableFiles }}
+
+ {{ importStatus.pending }}
@@ -382,18 +133,9 @@ useEventListener(window, 'beforeunload', (event) => {
-
{{ $t('components.library.FileUpload.label.uploadWidget') }}
@@ -402,7 +144,7 @@ useEventListener(window, 'beforeunload', (event) => {
{{ $t('components.library.FileUpload.label.extensions', {extensions: supportedExtensions.join(', ')}) }}
-
+
{
- |
- {{ truncate(file.name ?? '', 60) }}
+ |
+ {{ truncate(upload.file.name, 60) }}
|
- {{ humanSize(file.size ?? 0) }} |
+ {{ humanSize(upload.file.size) }} |
- {{ file.error }}
+ {{ upload.error }}
-
+
{{ $t('components.library.FileUpload.table.upload.status.uploaded') }}
+
+ {{ $t('components.library.FileUpload.table.upload.status.imported') }}
+
{{ $t('components.library.FileUpload.table.upload.status.uploading') }}
- {{ $t('components.library.FileUpload.table.upload.progress', {percent: parseFloat(file.progress ?? '0.00')}) }}
+ {{ $t('components.library.FileUpload.table.upload.progress', { percent: upload.progress?.toFixed(2) }) }}
{
|
-
+
|
@@ -562,7 +306,7 @@ useEventListener(window, 'beforeunload', (event) => {
:needs-refresh="needsRefresh"
ordering-config-name="library.detail.upload"
:filters="{import_reference: importReference}"
- :custom-objects="Object.values(uploads.objects)"
+ :custom-objects="Object.values({})"
@fetch-start="needsRefresh = false"
/>
diff --git a/front/src/composables/files/imports.ts b/front/src/composables/files/imports.ts
new file mode 100644
index 000000000..8f798e784
--- /dev/null
+++ b/front/src/composables/files/imports.ts
@@ -0,0 +1,65 @@
+import { reactive } from 'vue'
+import axios from 'axios'
+import useErrorHandler from '../useErrorHandler'
+import useWebSocketHandler from '../useWebSocketHandler'
+
+type ImportStatus = Record<'pending' | 'finished' | 'skipped' | 'errored', number>
+
+const fetchImportStatus = async (importReference: string) => {
+ const importStatus: ImportStatus = {
+ pending: 0,
+ finished: 0,
+ skipped: 0,
+ errored: 0
+ }
+
+ for (const status of Object.keys(importStatus)) {
+ try {
+ const response = await axios.get('uploads/', {
+ params: {
+ import_reference: importReference,
+ import_status: status,
+ page_size: 1
+ }
+ })
+
+ importStatus[status as keyof typeof importStatus] = response.data.count
+ } catch (error) {
+ useErrorHandler(error as Error)
+ }
+ }
+
+ return importStatus
+}
+
+export const useImportStatus = (importReference: string) => {
+ const importStatus: ImportStatus = reactive({
+ pending: 0,
+ finished: 0,
+ skipped: 0,
+ errored: 0
+ })
+
+ fetchImportStatus(importReference).then((status) => {
+ for (const key of Object.keys(status)) {
+ importStatus[key as keyof ImportStatus] = status[key as keyof ImportStatus]
+ }
+ })
+
+ useWebSocketHandler('import.status_updated', async (event) => {
+ if (event.upload.import_reference !== importReference) {
+ return
+ }
+
+ importStatus[event.old_status] -= 1
+ importStatus[event.new_status] += 1
+ })
+
+ return importStatus
+}
+
+export const useImports = () => {
+ return {
+ fetchImportStatus
+ }
+}
diff --git a/front/src/composables/files/upload.ts b/front/src/composables/files/upload.ts
new file mode 100644
index 000000000..21543e24d
--- /dev/null
+++ b/front/src/composables/files/upload.ts
@@ -0,0 +1,93 @@
+import { resolveUnref, useFileDialog, useSessionStorage, whenever, type MaybeComputedRef } from '@vueuse/core'
+import { markRaw, reactive, readonly, ref, watchEffect } from 'vue'
+
+import axios from 'axios'
+import type { BackendError } from '~/types'
+
+export const importReference = useSessionStorage('uploads:import-reference', new Date().toISOString())
+
+export const useTrackUpload = (libraryUUID: MaybeComputedRef) => {
+ const { open, files } = useFileDialog({
+ multiple: true,
+ accept: 'audio/*'
+ })
+
+ interface FileUpload {
+ id: string
+ file: File
+ progress: number
+ status: 'queued' | 'uploading' | 'uploaded' | 'imported'
+ error?: 'denied' | 'error'
+ }
+
+ const filesToUpload: FileUpload[] = reactive([])
+ whenever(files, (files) => {
+ for (const file of files) {
+ filesToUpload.push({
+ id: Math.random().toString().slice(2),
+ file: markRaw(file),
+ status: 'queued',
+ progress: 0
+ })
+ }
+ })
+
+ const uploadingIndex = ref(0)
+ watchEffect(async () => {
+ if (uploadingIndex.value >= filesToUpload.length) {
+ return
+ }
+
+ const upload = filesToUpload[uploadingIndex.value]
+ switch (upload.status) {
+ case 'uploading':
+ return
+
+ case 'uploaded':
+ case 'imported':
+ uploadingIndex.value += 1
+ return
+ }
+
+ const formData = new FormData()
+
+ const { file } = upload
+ const name = file.webkitRelativePath || file.name || 'unknown'
+ formData.append('audio_file', file, name)
+ formData.append('source', `upload://${name}`)
+ formData.append('library', resolveUnref(libraryUUID))
+ formData.append('import_reference', importReference.value)
+ // formData.append('import_metadata', JSON.stringify({
+ // title: name.replace(/\.[^/.]+$/, '')
+ // }))
+
+ try {
+ upload.status = 'uploading'
+
+ const { data } = await axios.post('/uploads', formData, {
+ onUploadProgress: (progressEvent) => {
+ upload.progress = progressEvent.loaded / progressEvent.total * 100
+ }
+ })
+
+ upload.id = data.uuid
+ upload.status = 'uploaded'
+ } catch (error) {
+ upload.error = (error as BackendError).backendErrors[0] === 'Entity Too Large'
+ ? 'denied'
+ : 'error'
+ }
+
+ uploadingIndex.value += 1
+ })
+
+ const uploadFiles = () => {
+ open()
+ }
+
+ return {
+ importReference,
+ uploadFiles,
+ files: readonly(filesToUpload)
+ }
+}
diff --git a/front/src/init/axios.ts b/front/src/init/axios.ts
index 88a90621d..482262217 100644
--- a/front/src/init/axios.ts
+++ b/front/src/init/axios.ts
@@ -61,6 +61,10 @@ export const install: InitModule = ({ store, router }) => {
error.backendErrors.push('Permission denied')
break
+ case 413:
+ error.backendErrors.push('Entity Too Large')
+ break
+
case 429: {
let message
const rateLimitStatus: RateLimitStatus = {
diff --git a/front/src/router/routes/library.ts b/front/src/router/routes/library.ts
index 34989e5de..562a98589 100644
--- a/front/src/router/routes/library.ts
+++ b/front/src/router/routes/library.ts
@@ -229,10 +229,7 @@ export default [
{
path: 'upload',
name: 'library.detail.upload',
- component: () => import('~/views/library/Upload.vue'),
- props: route => ({
- defaultImportReference: route.query.import
- })
+ component: () => import('~/views/library/Upload.vue')
}
]
}
diff --git a/front/src/store/ui.ts b/front/src/store/ui.ts
index 56ff605b8..bf95a69bf 100644
--- a/front/src/store/ui.ts
+++ b/front/src/store/ui.ts
@@ -60,7 +60,7 @@ const store: Module = {
lastDate: new Date(),
maxMessages: 100,
messageDisplayDuration: 5 * 1000,
- supportedExtensions: ['flac', 'ogg', 'mp3', 'opus', 'aac', 'm4a', 'aiff', 'aif'],
+ supportedExtensions: ['flac', 'mp3', 'aac', 'm4a', 'aiff', 'aif'],
messages: [],
window: {
height: 0,
diff --git a/front/src/views/content/libraries/Quota.vue b/front/src/views/content/libraries/Quota.vue
index 1d0224c46..da8a2a50c 100644
--- a/front/src/views/content/libraries/Quota.vue
+++ b/front/src/views/content/libraries/Quota.vue
@@ -72,7 +72,7 @@ const purgeErroredFiles = () => purge('errored')
:style="{width: `${progress}%`}"
>
- {{ $t('views.content.libraries.Quota.label.percentUsed', {progress: progress}) }}
+ {{ $t('views.content.libraries.Quota.label.percentUsed', { progress: progress.toFixed(2) }) }}
()
-withDefaults(defineProps
(), {
- defaultImportReference: ''
-})
+defineProps()
const fileupload = ref()
onBeforeRouteLeave((to, from, next) => {
@@ -39,7 +36,6 @@ onBeforeRouteLeave((to, from, next) => {