Implement "import history" in import page

This commit is contained in:
2026-01-08 03:57:00 +05:00
parent c29c12feec
commit 9984bb804a
26 changed files with 591 additions and 127 deletions

View File

@ -15,10 +15,6 @@
<p class="text-sm text-muted-foreground">
{{ trackCount }} track(s)
</p>
<Dot />
<p class="text-sm text-muted-foreground">
{{ type }}
</p>
</div>
<div class="flex flex-row items-center gap-2" v-if="hasProgress">
<p class="text-sm text-muted-foreground">
@ -56,7 +52,7 @@
</DropdownMenu>
</div>
<Dialog :open="isDialogOpen">
<Dialog :open="isDialogOpen" @update:open="toggleDialog">
<DialogContent class="max-w-3xl">
<DialogHeader>
<DialogTitle>Playlist Upload Details</DialogTitle>
@ -84,25 +80,19 @@
<Label>Track Count</Label>
<p class="text-sm">{{ trackCount }} tracks</p>
</div>
<div class="space-y-2">
<Label>Type</Label>
<p class="text-sm">{{ type }}</p>
</div>
</div>
</div>
<div v-if="playlistProgressData?.ytdlnStdout" class="space-y-4">
<div v-if="ytdlnStdout" class="space-y-4">
<div class="flex justify-between items-center">
<h3 class="text-lg font-semibold">yt-dlp Output</h3>
<Button variant="outline" size="sm"
@click="copyToClipboard(playlistProgressData.ytdlnStdout)">
<Button variant="outline" size="sm" @click="copyToClipboard(ytdlnStdout)">
<Copy class="mr-2 h-4 w-4" />
Copy
</Button>
</div>
<div class="bg-muted rounded-md p-4">
<pre class="text-xs whitespace-pre-wrap overflow-x-auto max-h-60">{{
playlistProgressData.ytdlnStdout }}</pre>
<pre class="text-xs whitespace-pre-wrap overflow-x-auto max-h-60">{{ ytdlnStdout }}</pre>
</div>
</div>
@ -130,7 +120,6 @@
import {
CassetteTape,
Copy,
Dot,
EllipsisVertical,
Eye,
FileQuestionMark,
@ -139,22 +128,15 @@ import {
Trash2
} from 'lucide-vue-next';
import { ref } from 'vue';
import type { PlaylistProgressAllOfStatus } from '~/composeables/api/models';
interface Props {
title: string
trackCount?: number
type?: string
ytdlnStdout: string
status: PlaylistProgressAllOfStatus
progress?: number
error?: string
playlistProgressData?: {
playlistId: number
trackSourceId: number
userId: number
timestamp: number
ytdlnStdout: string
overallProgress: number
status: 'LOADING' | 'FINISHED'
}
}
const props = withDefaults(defineProps<Props>(), {
@ -167,36 +149,34 @@ const emit = defineEmits<{
const isDialogOpen = ref(false);
const hasLoaded = props.trackCount && props.type;
const hasLoaded = props.trackCount;
const hasProgress = props.progress !== undefined && props.progress > 0;
const hasError = props.error;
const openDialog = () => {
isDialogOpen.value = true;
console.log(isDialogOpen.value)
};
const toggleDialog = (value: boolean) => {
isDialogOpen.value = value;
}
const getStatusColor = () => {
if (hasError) return 'bg-destructive';
if (props.playlistProgressData?.status === 'FINISHED') return 'bg-green-500';
if (props.playlistProgressData?.status === 'LOADING') return 'bg-blue-500';
if (props.status === 'FINISHED') return 'bg-green-500';
if (props.status === 'LOADING') return 'bg-blue-500';
if (hasProgress) return 'bg-amber-500';
return 'bg-gray-500';
};
const getStatusText = () => {
if (hasError) return 'Error';
if (props.playlistProgressData?.status === 'FINISHED') return 'Completed';
if (props.playlistProgressData?.status === 'LOADING') return 'Loading';
if (props.status === 'FINISHED') return 'Completed';
if (props.status === 'LOADING') return 'Loading';
if (hasProgress) return 'In Progress';
return 'Pending';
};
const formatTimestamp = (timestamp?: number) => {
if (!timestamp) return 'N/A';
return new Date(timestamp).toLocaleString();
};
const copyToClipboard = async (text: string) => {
try {
await navigator.clipboard.writeText(text);

View File

@ -19,10 +19,6 @@
<p class="text-sm text-muted-foreground">
{{ format }}
</p>
<Dot />
<p class="text-sm text-muted-foreground">
{{ type }}
</p>
</div>
<div class="flex flex-row items-center gap-2" v-if="hasProgress">
<p class="text-sm text-muted-foreground">
@ -85,10 +81,6 @@
<Label>Size</Label>
<p class="text-sm">{{ size }}</p>
</div>
<div class="space-y-2">
<Label>Type</Label>
<p class="text-sm">{{ type }}</p>
</div>
</div>
</div>
@ -194,7 +186,6 @@ interface Props {
title: string
size?: string
format?: string
type?: string
progress?: number
error?: string
trackProgressData?: {
@ -219,7 +210,7 @@ const emit = defineEmits<{
const isDialogOpen = ref(false);
const hasLoaded = props.size && props.format && props.type;
const hasLoaded = props.size && props.format;
const hasProgress = props.progress !== undefined && props.progress > 0;
const hasError = props.error;

View File

@ -0,0 +1,27 @@
<template>
<SingleUploadEntry v-if="entry.type === 'TRACK'" :title="(entry as SingleTrackProgressAllOf).title || ''" :size="''"
:format="(entry as SingleTrackProgressAllOf).format || ''" />
<PlaylistUploadEntry v-if="entry.type === 'PLAYLIST'" title=""
:trackCount="(entry as PlaylistProgressAllOf).trackCount"
:ytdlnStdout="(entry as PlaylistProgressAllOf).ytdlnStdout || ''"
:status="(entry as PlaylistProgressAllOf).status || 'LOADING'" />
</template>
<script setup lang="ts">
import type { PlaylistProgressAllOf, PlaylistProgressAllOfStatus, SingleTrackProgressAllOf, StreamProgress200Item } from '~/composeables/api/models';
import SingleUploadEntry from './SingleUploadEntry.vue';
import PlaylistUploadEntry from './PlaylistUploadEntry.vue';
interface Props {
entry: StreamProgress200Item
}
const props = withDefaults(defineProps<Props>(), {
});
const emit = defineEmits<{
retry: []
download: []
play: []
}>();
</script>

View File

@ -1,6 +1,12 @@
<script setup lang="ts">
import { FileMusicIcon } from 'lucide-vue-next';
import PlaylistCreateDialog from './PlaylistCreateDialog.vue';
import Empty from '@/components/ui/empty/Empty.vue';
import EmptyHeader from '@/components/ui/empty/EmptyHeader.vue';
import EmptyMedia from '@/components/ui/empty/EmptyMedia.vue';
import EmptyTitle from '@/components/ui/empty/EmptyTitle.vue';
import EmptyDescription from '@/components/ui/empty/EmptyDescription.vue';
import EmptyContent from '@/components/ui/empty/EmptyContent.vue';
</script>

View File

@ -1,7 +1,7 @@
import Axios, { type AxiosRequestConfig, type AxiosResponse } from 'axios';
export const AXIOS_INSTANCE = Axios.create();
export const axiosInstance = <T>(
const axiosInstance = <T>(
config: AxiosRequestConfig,
options?: AxiosRequestConfig,
): Promise<AxiosResponse<T, any>> => {
@ -13,7 +13,7 @@ export const axiosInstance = <T>(
...options,
baseURL: baseURL,
auth: {
username: 'user',
username: 'user1',
password: 'password',
}
},
@ -23,3 +23,4 @@ export const axiosInstance = <T>(
return promise;
};
export default axiosInstance;

View File

@ -0,0 +1,94 @@
import type { AxiosRequestConfig } from "axios";
import { ErrorEvent, EventSource } from 'eventsource'
type Unarray<T> = T extends Array<infer U> ? U : T;
export interface EventSourceListener<T> {
handle: (callback: (data: T) => void) => EventSourceListener<T>;
close: () => void;
onError: (callback: (error: ErrorEvent) => void) => EventSourceListener<T>;
onOpen: (callback: (event: Event) => void) => EventSourceListener<T>;
}
const eventSource = <T>(
config: AxiosRequestConfig,
options?: AxiosRequestConfig,
): Promise<EventSourceListener<Unarray<T>>> => {
return new Promise((resolve, reject) => {
try {
const baseURL = useRuntimeConfig().public.apiBaseUrl;
const endpoint = config.url;
const eventSource = new EventSource(baseURL + endpoint, {
fetch: (input, init) => fetch(input, {
...init,
headers: {
...init.headers,
Authorization: 'Basic ' + btoa('user1' + ":" + 'password'),
},
})
})
let messageCallback: ((data: Unarray<T>) => void) | null = null;
let errorCallback: ((error: ErrorEvent) => void) | null = null;
let openCallback: ((event: Event) => void) | null = null;
eventSource.addEventListener("message", (event: MessageEvent<any>) => {
if (messageCallback) {
try {
let data = JSON.parse(event.data) as Unarray<T>;
messageCallback(data);
} catch (error) {
console.error('Error parsing EventSource data:', error);
}
}
});
eventSource.addEventListener('error', (event) => {
if (errorCallback) {
errorCallback(event);
}
});
eventSource.addEventListener('open', (event) => {
if (openCallback) {
openCallback(event);
}
});
const listener: EventSourceListener<Unarray<T>> = {
handle: (callback: (data: Unarray<T>) => void) => {
messageCallback = callback;
return listener;
},
onError: (callback: (error: ErrorEvent) => void) => {
errorCallback = callback;
return listener;
},
onOpen: (callback: (event: Event) => void) => {
openCallback = callback;
return listener;
},
close: () => {
eventSource.close();
messageCallback = null;
errorCallback = null;
openCallback = null;
}
};
resolve(listener);
setTimeout(() => {
if (eventSource.readyState === EventSource.CLOSED && !errorCallback) {
reject(new Error('EventSource connection failed'));
}
}, 5000);
} catch (error) {
reject(error);
}
});
}
export default eventSource;

View File

@ -0,0 +1,16 @@
/**
* Generated by orval v7.16.0 🍺
* Do not edit manually.
* OpenAPI definition
* OpenAPI spec version: v0
*/
import type { BaseTrackProgressType } from './baseTrackProgressType';
export interface BaseTrackProgress {
id?: string;
playlistId?: number;
trackSourceId?: number;
userId?: number;
timestamp?: number;
type?: BaseTrackProgressType;
}

View File

@ -0,0 +1,16 @@
/**
* Generated by orval v7.16.0 🍺
* Do not edit manually.
* OpenAPI definition
* OpenAPI spec version: v0
*/
export type BaseTrackProgressType = typeof BaseTrackProgressType[keyof typeof BaseTrackProgressType];
// eslint-disable-next-line @typescript-eslint/no-redeclare
export const BaseTrackProgressType = {
PLAYLIST: 'PLAYLIST',
TRACK: 'TRACK',
EXTERNAL_TRACK: 'EXTERNAL_TRACK',
} as const;

View File

@ -6,14 +6,19 @@
*/
export * from './addLocalTrackRequest';
export * from './playlistCreateDTO';
export * from './baseTrackProgress';
export * from './baseTrackProgressType';
export * from './playlistCreateRequest';
export * from './playlistReadDTO';
export * from './playlistProgress';
export * from './playlistProgressAllOf';
export * from './playlistProgressAllOfStatus';
export * from './playlistReadResponse';
export * from './playlistTrackResponse';
export * from './readParams';
export * from './singleTrackProgress';
export * from './singleTrackProgressAllOf';
export * from './streamProgress200Item';
export * from './trackBulkReorderRequest';
export * from './trackReoderAfterRequest';
export * from './trackReorderAfterRequest';
export * from './trackResponse';
export * from './uploadBody';
export * from './youtubeTrackRequest';

View File

@ -0,0 +1,10 @@
/**
* Generated by orval v7.16.0 🍺
* Do not edit manually.
* OpenAPI definition
* OpenAPI spec version: v0
*/
import type { BaseTrackProgress } from './baseTrackProgress';
import type { PlaylistProgressAllOf } from './playlistProgressAllOf';
export type PlaylistProgress = BaseTrackProgress & PlaylistProgressAllOf;

View File

@ -0,0 +1,14 @@
/**
* Generated by orval v7.16.0 🍺
* Do not edit manually.
* OpenAPI definition
* OpenAPI spec version: v0
*/
import type { PlaylistProgressAllOfStatus } from './playlistProgressAllOfStatus';
export type PlaylistProgressAllOf = {
ytdlnStdout?: string;
overallProgress?: number;
trackCount?: number;
status?: PlaylistProgressAllOfStatus;
};

View File

@ -0,0 +1,15 @@
/**
* Generated by orval v7.16.0 🍺
* Do not edit manually.
* OpenAPI definition
* OpenAPI spec version: v0
*/
export type PlaylistProgressAllOfStatus = typeof PlaylistProgressAllOfStatus[keyof typeof PlaylistProgressAllOfStatus];
// eslint-disable-next-line @typescript-eslint/no-redeclare
export const PlaylistProgressAllOfStatus = {
LOADING: 'LOADING',
FINISHED: 'FINISHED',
} as const;

View File

@ -1,14 +0,0 @@
/**
* Generated by orval v7.16.0 🍺
* Do not edit manually.
* OpenAPI definition
* OpenAPI spec version: v0
*/
export interface PlaylistReadDTO {
id?: number;
ownerId?: number;
title?: string;
createdAt?: string;
updatedAt?: string;
}

View File

@ -0,0 +1,10 @@
/**
* Generated by orval v7.16.0 🍺
* Do not edit manually.
* OpenAPI definition
* OpenAPI spec version: v0
*/
import type { BaseTrackProgress } from './baseTrackProgress';
import type { SingleTrackProgressAllOf } from './singleTrackProgressAllOf';
export type SingleTrackProgress = BaseTrackProgress & SingleTrackProgressAllOf;

View File

@ -5,6 +5,7 @@
* OpenAPI spec version: v0
*/
export interface PlaylistCreateDTO {
export type SingleTrackProgressAllOf = {
title?: string;
}
format?: string;
};

View File

@ -0,0 +1,10 @@
/**
* Generated by orval v7.16.0 🍺
* Do not edit manually.
* OpenAPI definition
* OpenAPI spec version: v0
*/
import type { PlaylistProgress } from './playlistProgress';
import type { SingleTrackProgress } from './singleTrackProgress';
export type StreamProgress200Item = PlaylistProgress | SingleTrackProgress;

View File

@ -10,6 +10,7 @@ export interface TrackResponse {
title?: string;
artist?: string;
audioPath?: string;
fileFormat?: string;
durationSeconds?: number;
fileName?: string;
}

View File

@ -5,7 +5,6 @@
* OpenAPI spec version: v0
*/
export interface TrackReorderAfterRequest {
moveTrackId?: number;
targetTrackId?: number;
export interface YoutubeTrackRequest {
youtubeUrl?: string;
}

View File

@ -32,7 +32,8 @@ import type {
PlaylistReadResponse
} from '.././models';
import { axiosInstance } from '.././axios-instance';
import createPlaylistMutator from '.././axios-instance';
import playlistsMutator from '.././axios-instance';
type SecondParameter<T extends (...args: never) => unknown> = Parameters<T>[1];
@ -41,11 +42,11 @@ type SecondParameter<T extends (...args: never) => unknown> = Parameters<T>[1];
export const createPlaylist = (
playlistCreateRequest: MaybeRef<PlaylistCreateRequest>,
options?: SecondParameter<typeof axiosInstance>,signal?: AbortSignal
options?: SecondParameter<typeof createPlaylistMutator>,signal?: AbortSignal
) => {
playlistCreateRequest = unref(playlistCreateRequest);
return axiosInstance<PlaylistReadResponse>(
return createPlaylistMutator<PlaylistReadResponse>(
{url: `/playlist`, method: 'POST',
headers: {'Content-Type': 'application/json', },
data: playlistCreateRequest, signal
@ -56,7 +57,7 @@ export const createPlaylist = (
export const getCreatePlaylistMutationOptions = <TError = unknown,
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof createPlaylist>>, TError,{data: PlaylistCreateRequest}, TContext>, request?: SecondParameter<typeof axiosInstance>}
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof createPlaylist>>, TError,{data: PlaylistCreateRequest}, TContext>, request?: SecondParameter<typeof createPlaylistMutator>}
): UseMutationOptions<Awaited<ReturnType<typeof createPlaylist>>, TError,{data: PlaylistCreateRequest}, TContext> => {
const mutationKey = ['createPlaylist'];
@ -85,7 +86,7 @@ const {mutation: mutationOptions, request: requestOptions} = options ?
export type CreatePlaylistMutationError = unknown
export const useCreatePlaylist = <TError = unknown,
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof createPlaylist>>, TError,{data: PlaylistCreateRequest}, TContext>, request?: SecondParameter<typeof axiosInstance>}
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof createPlaylist>>, TError,{data: PlaylistCreateRequest}, TContext>, request?: SecondParameter<typeof createPlaylistMutator>}
, queryClient?: QueryClient): UseMutationReturnType<
Awaited<ReturnType<typeof createPlaylist>>,
TError,
@ -99,11 +100,11 @@ const {mutation: mutationOptions, request: requestOptions} = options ?
}
export const playlists = (
options?: SecondParameter<typeof axiosInstance>,signal?: AbortSignal
options?: SecondParameter<typeof playlistsMutator>,signal?: AbortSignal
) => {
return axiosInstance<PlaylistReadResponse[]>(
return playlistsMutator<PlaylistReadResponse[]>(
{url: `/playlists`, method: 'GET', signal
},
options);
@ -119,7 +120,7 @@ export const getPlaylistsQueryKey = () => {
}
export const getPlaylistsQueryOptions = <TData = Awaited<ReturnType<typeof playlists>>, TError = unknown>( options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof playlists>>, TError, TData>>, request?: SecondParameter<typeof axiosInstance>}
export const getPlaylistsQueryOptions = <TData = Awaited<ReturnType<typeof playlists>>, TError = unknown>( options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof playlists>>, TError, TData>>, request?: SecondParameter<typeof playlistsMutator>}
) => {
const {query: queryOptions, request: requestOptions} = options ?? {};
@ -143,7 +144,7 @@ export type PlaylistsQueryError = unknown
export function usePlaylists<TData = Awaited<ReturnType<typeof playlists>>, TError = unknown>(
options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof playlists>>, TError, TData>>, request?: SecondParameter<typeof axiosInstance>}
options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof playlists>>, TError, TData>>, request?: SecondParameter<typeof playlistsMutator>}
, queryClient?: QueryClient
): UseQueryReturnType<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> } {

View File

@ -0,0 +1,99 @@
/**
* Generated by orval v7.16.0 🍺
* Do not edit manually.
* OpenAPI definition
* OpenAPI spec version: v0
*/
import {
useQuery
} from '@tanstack/vue-query';
import type {
DataTag,
QueryClient,
QueryFunction,
QueryKey,
UseQueryOptions,
UseQueryReturnType
} from '@tanstack/vue-query';
import {
computed,
unref
} from 'vue';
import type {
MaybeRef
} from 'vue';
import type {
StreamProgress200Item
} from '.././models';
import streamProgressMutator from '.././event-source';
type SecondParameter<T extends (...args: never) => unknown> = Parameters<T>[1];
export const streamProgress = (
playlistId: MaybeRef<number>,
options?: SecondParameter<typeof streamProgressMutator>,signal?: AbortSignal
) => {
playlistId = unref(playlistId);
return streamProgressMutator<StreamProgress200Item[]>(
{url: `/importing/stream/${playlistId}`, method: 'GET', signal
},
options);
}
export const getStreamProgressQueryKey = (playlistId?: MaybeRef<number>,) => {
return [
'importing','stream',playlistId
] as const;
}
export const getStreamProgressQueryOptions = <TData = Awaited<ReturnType<typeof streamProgress>>, TError = unknown>(playlistId: MaybeRef<number>, options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof streamProgress>>, TError, TData>>, request?: SecondParameter<typeof streamProgressMutator>}
) => {
const {query: queryOptions, request: requestOptions} = options ?? {};
const queryKey = getStreamProgressQueryKey(playlistId);
const queryFn: QueryFunction<Awaited<ReturnType<typeof streamProgress>>> = ({ signal }) => streamProgress(playlistId, requestOptions, signal);
return { queryKey, queryFn, enabled: computed(() => !!(unref(playlistId))), ...queryOptions} as UseQueryOptions<Awaited<ReturnType<typeof streamProgress>>, TError, TData>
}
export type StreamProgressQueryResult = NonNullable<Awaited<ReturnType<typeof streamProgress>>>
export type StreamProgressQueryError = unknown
export function useStreamProgress<TData = Awaited<ReturnType<typeof streamProgress>>, TError = unknown>(
playlistId: MaybeRef<number>, options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof streamProgress>>, TError, TData>>, request?: SecondParameter<typeof streamProgressMutator>}
, queryClient?: QueryClient
): UseQueryReturnType<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> } {
const queryOptions = getStreamProgressQueryOptions(playlistId,options)
const query = useQuery(queryOptions, queryClient) as UseQueryReturnType<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> };
query.queryKey = unref(queryOptions).queryKey as DataTag<QueryKey, TData, TError>;
return query;
}

View File

@ -32,7 +32,8 @@ import type {
UploadBody
} from '.././models';
import { axiosInstance } from '.././axios-instance';
import uploadMutator from '.././axios-instance';
import readMutator from '.././axios-instance';
type SecondParameter<T extends (...args: never) => unknown> = Parameters<T>[1];
@ -41,13 +42,13 @@ type SecondParameter<T extends (...args: never) => unknown> = Parameters<T>[1];
export const upload = (
uploadBody: MaybeRef<UploadBody>,
options?: SecondParameter<typeof axiosInstance>,signal?: AbortSignal
options?: SecondParameter<typeof uploadMutator>,signal?: AbortSignal
) => {
uploadBody = unref(uploadBody);
const formData = new FormData();
formData.append(`document`, uploadBody.document)
return axiosInstance<string>(
return uploadMutator<string>(
{url: `/upload`, method: 'POST',
headers: {'Content-Type': 'multipart/form-data', },
data: formData, signal
@ -58,7 +59,7 @@ formData.append(`document`, uploadBody.document)
export const getUploadMutationOptions = <TError = unknown,
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof upload>>, TError,{data: UploadBody}, TContext>, request?: SecondParameter<typeof axiosInstance>}
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof upload>>, TError,{data: UploadBody}, TContext>, request?: SecondParameter<typeof uploadMutator>}
): UseMutationOptions<Awaited<ReturnType<typeof upload>>, TError,{data: UploadBody}, TContext> => {
const mutationKey = ['upload'];
@ -87,7 +88,7 @@ const {mutation: mutationOptions, request: requestOptions} = options ?
export type UploadMutationError = unknown
export const useUpload = <TError = unknown,
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof upload>>, TError,{data: UploadBody}, TContext>, request?: SecondParameter<typeof axiosInstance>}
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof upload>>, TError,{data: UploadBody}, TContext>, request?: SecondParameter<typeof uploadMutator>}
, queryClient?: QueryClient): UseMutationReturnType<
Awaited<ReturnType<typeof upload>>,
TError,
@ -101,11 +102,11 @@ const {mutation: mutationOptions, request: requestOptions} = options ?
}
export const read = (
params: MaybeRef<ReadParams>,
options?: SecondParameter<typeof axiosInstance>,signal?: AbortSignal
options?: SecondParameter<typeof readMutator>,signal?: AbortSignal
) => {
params = unref(params);
return axiosInstance<string>(
return readMutator<string>(
{url: `/read`, method: 'GET',
params: unref(params), signal
},
@ -122,7 +123,7 @@ export const getReadQueryKey = (params?: MaybeRef<ReadParams>,) => {
}
export const getReadQueryOptions = <TData = Awaited<ReturnType<typeof read>>, TError = unknown>(params: MaybeRef<ReadParams>, options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof read>>, TError, TData>>, request?: SecondParameter<typeof axiosInstance>}
export const getReadQueryOptions = <TData = Awaited<ReturnType<typeof read>>, TError = unknown>(params: MaybeRef<ReadParams>, options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof read>>, TError, TData>>, request?: SecondParameter<typeof readMutator>}
) => {
const {query: queryOptions, request: requestOptions} = options ?? {};
@ -146,7 +147,7 @@ export type ReadQueryError = unknown
export function useRead<TData = Awaited<ReturnType<typeof read>>, TError = unknown>(
params: MaybeRef<ReadParams>, options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof read>>, TError, TData>>, request?: SecondParameter<typeof axiosInstance>}
params: MaybeRef<ReadParams>, options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof read>>, TError, TData>>, request?: SecondParameter<typeof readMutator>}
, queryClient?: QueryClient
): UseQueryReturnType<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> } {

View File

@ -32,27 +32,150 @@ import type {
AddLocalTrackRequest,
PlaylistTrackResponse,
TrackBulkReorderRequest,
TrackResponse
TrackResponse,
YoutubeTrackRequest
} from '.././models';
import { axiosInstance } from '.././axios-instance';
import addYoutubeTrackMutator from '.././axios-instance';
import addYoutubeTrack1Mutator from '.././axios-instance';
import addLocalTrackMutator from '.././axios-instance';
import bulkReorderMutator from '.././axios-instance';
import getPlaylistTracksMutator from '.././axios-instance';
type SecondParameter<T extends (...args: never) => unknown> = Parameters<T>[1];
export const addLocalTrack = (
export const addYoutubeTrack = (
playlistId: MaybeRef<number>,
youtubeTrackRequest: MaybeRef<YoutubeTrackRequest>,
options?: SecondParameter<typeof addYoutubeTrackMutator>,signal?: AbortSignal
) => {
playlistId = unref(playlistId);
youtubeTrackRequest = unref(youtubeTrackRequest);
return addYoutubeTrackMutator<TrackResponse[]>(
{url: `/playlist/${playlistId}/track/youtube`, method: 'POST',
headers: {'Content-Type': 'application/json', },
data: youtubeTrackRequest, signal
},
options);
}
export const getAddYoutubeTrackMutationOptions = <TError = unknown,
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof addYoutubeTrack>>, TError,{playlistId: number;data: YoutubeTrackRequest}, TContext>, request?: SecondParameter<typeof addYoutubeTrackMutator>}
): UseMutationOptions<Awaited<ReturnType<typeof addYoutubeTrack>>, TError,{playlistId: number;data: YoutubeTrackRequest}, TContext> => {
const mutationKey = ['addYoutubeTrack'];
const {mutation: mutationOptions, request: requestOptions} = options ?
options.mutation && 'mutationKey' in options.mutation && options.mutation.mutationKey ?
options
: {...options, mutation: {...options.mutation, mutationKey}}
: {mutation: { mutationKey, }, request: undefined};
const mutationFn: MutationFunction<Awaited<ReturnType<typeof addYoutubeTrack>>, {playlistId: number;data: YoutubeTrackRequest}> = (props) => {
const {playlistId,data} = props ?? {};
return addYoutubeTrack(playlistId,data,requestOptions)
}
return { mutationFn, ...mutationOptions }}
export type AddYoutubeTrackMutationResult = NonNullable<Awaited<ReturnType<typeof addYoutubeTrack>>>
export type AddYoutubeTrackMutationBody = YoutubeTrackRequest
export type AddYoutubeTrackMutationError = unknown
export const useAddYoutubeTrack = <TError = unknown,
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof addYoutubeTrack>>, TError,{playlistId: number;data: YoutubeTrackRequest}, TContext>, request?: SecondParameter<typeof addYoutubeTrackMutator>}
, queryClient?: QueryClient): UseMutationReturnType<
Awaited<ReturnType<typeof addYoutubeTrack>>,
TError,
{playlistId: number;data: YoutubeTrackRequest},
TContext
> => {
const mutationOptions = getAddYoutubeTrackMutationOptions(options);
return useMutation(mutationOptions, queryClient);
}
export const addYoutubeTrack1 = (
playlistId: MaybeRef<number>,
sourceId: MaybeRef<number>,
options?: SecondParameter<typeof addYoutubeTrack1Mutator>,signal?: AbortSignal
) => {
playlistId = unref(playlistId);
sourceId = unref(sourceId);
return addYoutubeTrack1Mutator<TrackResponse[]>(
{url: `/playlist/${playlistId}/track/youtube/refresh/${sourceId}`, method: 'POST', signal
},
options);
}
export const getAddYoutubeTrack1MutationOptions = <TError = unknown,
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof addYoutubeTrack1>>, TError,{playlistId: number;sourceId: number}, TContext>, request?: SecondParameter<typeof addYoutubeTrack1Mutator>}
): UseMutationOptions<Awaited<ReturnType<typeof addYoutubeTrack1>>, TError,{playlistId: number;sourceId: number}, TContext> => {
const mutationKey = ['addYoutubeTrack1'];
const {mutation: mutationOptions, request: requestOptions} = options ?
options.mutation && 'mutationKey' in options.mutation && options.mutation.mutationKey ?
options
: {...options, mutation: {...options.mutation, mutationKey}}
: {mutation: { mutationKey, }, request: undefined};
const mutationFn: MutationFunction<Awaited<ReturnType<typeof addYoutubeTrack1>>, {playlistId: number;sourceId: number}> = (props) => {
const {playlistId,sourceId} = props ?? {};
return addYoutubeTrack1(playlistId,sourceId,requestOptions)
}
return { mutationFn, ...mutationOptions }}
export type AddYoutubeTrack1MutationResult = NonNullable<Awaited<ReturnType<typeof addYoutubeTrack1>>>
export type AddYoutubeTrack1MutationError = unknown
export const useAddYoutubeTrack1 = <TError = unknown,
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof addYoutubeTrack1>>, TError,{playlistId: number;sourceId: number}, TContext>, request?: SecondParameter<typeof addYoutubeTrack1Mutator>}
, queryClient?: QueryClient): UseMutationReturnType<
Awaited<ReturnType<typeof addYoutubeTrack1>>,
TError,
{playlistId: number;sourceId: number},
TContext
> => {
const mutationOptions = getAddYoutubeTrack1MutationOptions(options);
return useMutation(mutationOptions, queryClient);
}
export const addLocalTrack = (
playlistId: MaybeRef<number>,
addLocalTrackRequest: MaybeRef<AddLocalTrackRequest>,
options?: SecondParameter<typeof axiosInstance>,signal?: AbortSignal
options?: SecondParameter<typeof addLocalTrackMutator>,signal?: AbortSignal
) => {
playlistId = unref(playlistId);
addLocalTrackRequest = unref(addLocalTrackRequest);
const formData = new FormData();
formData.append(`source`, addLocalTrackRequest.source)
return axiosInstance<TrackResponse>(
return addLocalTrackMutator<TrackResponse>(
{url: `/playlist/${playlistId}/track/local`, method: 'POST',
headers: {'Content-Type': 'multipart/form-data', },
data: formData, signal
@ -63,7 +186,7 @@ formData.append(`source`, addLocalTrackRequest.source)
export const getAddLocalTrackMutationOptions = <TError = unknown,
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof addLocalTrack>>, TError,{playlistId: number;data: AddLocalTrackRequest}, TContext>, request?: SecondParameter<typeof axiosInstance>}
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof addLocalTrack>>, TError,{playlistId: number;data: AddLocalTrackRequest}, TContext>, request?: SecondParameter<typeof addLocalTrackMutator>}
): UseMutationOptions<Awaited<ReturnType<typeof addLocalTrack>>, TError,{playlistId: number;data: AddLocalTrackRequest}, TContext> => {
const mutationKey = ['addLocalTrack'];
@ -92,7 +215,7 @@ const {mutation: mutationOptions, request: requestOptions} = options ?
export type AddLocalTrackMutationError = unknown
export const useAddLocalTrack = <TError = unknown,
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof addLocalTrack>>, TError,{playlistId: number;data: AddLocalTrackRequest}, TContext>, request?: SecondParameter<typeof axiosInstance>}
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof addLocalTrack>>, TError,{playlistId: number;data: AddLocalTrackRequest}, TContext>, request?: SecondParameter<typeof addLocalTrackMutator>}
, queryClient?: QueryClient): UseMutationReturnType<
Awaited<ReturnType<typeof addLocalTrack>>,
TError,
@ -107,12 +230,12 @@ const {mutation: mutationOptions, request: requestOptions} = options ?
export const bulkReorder = (
playlistId: MaybeRef<number>,
trackBulkReorderRequest: MaybeRef<TrackBulkReorderRequest>,
options?: SecondParameter<typeof axiosInstance>,signal?: AbortSignal
options?: SecondParameter<typeof bulkReorderMutator>,signal?: AbortSignal
) => {
playlistId = unref(playlistId);
trackBulkReorderRequest = unref(trackBulkReorderRequest);
return axiosInstance<void>(
return bulkReorderMutator<void>(
{url: `/playlist/${playlistId}/bulk-reorder`, method: 'POST',
headers: {'Content-Type': 'application/json', },
data: trackBulkReorderRequest, signal
@ -123,7 +246,7 @@ trackBulkReorderRequest = unref(trackBulkReorderRequest);
export const getBulkReorderMutationOptions = <TError = unknown,
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof bulkReorder>>, TError,{playlistId: number;data: TrackBulkReorderRequest}, TContext>, request?: SecondParameter<typeof axiosInstance>}
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof bulkReorder>>, TError,{playlistId: number;data: TrackBulkReorderRequest}, TContext>, request?: SecondParameter<typeof bulkReorderMutator>}
): UseMutationOptions<Awaited<ReturnType<typeof bulkReorder>>, TError,{playlistId: number;data: TrackBulkReorderRequest}, TContext> => {
const mutationKey = ['bulkReorder'];
@ -152,7 +275,7 @@ const {mutation: mutationOptions, request: requestOptions} = options ?
export type BulkReorderMutationError = unknown
export const useBulkReorder = <TError = unknown,
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof bulkReorder>>, TError,{playlistId: number;data: TrackBulkReorderRequest}, TContext>, request?: SecondParameter<typeof axiosInstance>}
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof bulkReorder>>, TError,{playlistId: number;data: TrackBulkReorderRequest}, TContext>, request?: SecondParameter<typeof bulkReorderMutator>}
, queryClient?: QueryClient): UseMutationReturnType<
Awaited<ReturnType<typeof bulkReorder>>,
TError,
@ -166,11 +289,11 @@ const {mutation: mutationOptions, request: requestOptions} = options ?
}
export const getPlaylistTracks = (
playlistId: MaybeRef<number>,
options?: SecondParameter<typeof axiosInstance>,signal?: AbortSignal
options?: SecondParameter<typeof getPlaylistTracksMutator>,signal?: AbortSignal
) => {
playlistId = unref(playlistId);
return axiosInstance<PlaylistTrackResponse[]>(
return getPlaylistTracksMutator<PlaylistTrackResponse[]>(
{url: `/playlist/${playlistId}/tracks`, method: 'GET', signal
},
options);
@ -186,7 +309,7 @@ export const getGetPlaylistTracksQueryKey = (playlistId?: MaybeRef<number>,) =>
}
export const getGetPlaylistTracksQueryOptions = <TData = Awaited<ReturnType<typeof getPlaylistTracks>>, TError = unknown>(playlistId: MaybeRef<number>, options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof getPlaylistTracks>>, TError, TData>>, request?: SecondParameter<typeof axiosInstance>}
export const getGetPlaylistTracksQueryOptions = <TData = Awaited<ReturnType<typeof getPlaylistTracks>>, TError = unknown>(playlistId: MaybeRef<number>, options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof getPlaylistTracks>>, TError, TData>>, request?: SecondParameter<typeof getPlaylistTracksMutator>}
) => {
const {query: queryOptions, request: requestOptions} = options ?? {};
@ -210,7 +333,7 @@ export type GetPlaylistTracksQueryError = unknown
export function useGetPlaylistTracks<TData = Awaited<ReturnType<typeof getPlaylistTracks>>, TError = unknown>(
playlistId: MaybeRef<number>, options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof getPlaylistTracks>>, TError, TData>>, request?: SecondParameter<typeof axiosInstance>}
playlistId: MaybeRef<number>, options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof getPlaylistTracks>>, TError, TData>>, request?: SecondParameter<typeof getPlaylistTracksMutator>}
, queryClient?: QueryClient
): UseQueryReturnType<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> } {

View File

@ -1,9 +1,62 @@
<script setup lang="ts">
import { Download, Play } from 'lucide-vue-next';
import PlaylistUploadEntry from '@/components/internal/import/uploadentry/PlaylistUploadEntry.vue';
import SingleUploadEntry from '@/components/internal/import/uploadentry/SingleUploadEntry.vue';
import Outline from '@/components/ui/outline/Outline.vue';
import SidebarTrigger from '@/components/ui/sidebar/SidebarTrigger.vue';
import Empty from '@/components/ui/empty/Empty.vue';
import UploadEntry from '@/components/internal/import/uploadentry/UploadEntry.vue';
import EmptyHeader from '@/components/ui/empty/EmptyHeader.vue';
import EmptyMedia from '@/components/ui/empty/EmptyMedia.vue';
import EmptyTitle from '@/components/ui/empty/EmptyTitle.vue';
import EmptyDescription from '@/components/ui/empty/EmptyDescription.vue';
import { Download, MegaphoneOff, Play } from 'lucide-vue-next';
import type { EventSourceListener } from '~/composeables/api/event-source';
import type { StreamProgress200Item } from '~/composeables/api/models';
import { streamProgress } from '~/composeables/api/progress-sse-controller/progress-sse-controller';
import { useCurrentPlaylistStore } from '~/stores/use-current-playlist-store';
const currentPlaylistStore = useCurrentPlaylistStore();
const progressEntries = ref<Map<string, StreamProgress200Item>>(new Map());
let listener: EventSourceListener<StreamProgress200Item> | null = null;
const unwatch = watch(() => currentPlaylistStore.id, (newId, oldId) => {
if (newId !== -1 && newId !== oldId) {
listenImports();
} else if (newId === -1) {
stopImports();
}
}, { immediate: true });
const updateEntryFromProgress = (data: StreamProgress200Item) => {
if (!data.trackSourceId)
return;
progressEntries.value = new Map(progressEntries.value);
progressEntries.value.set(data.id!.toString(), data);
}
async function listenImports() {
if (listener) {
listener.close();
}
streamProgress(currentPlaylistStore.id).then(listener => {
listener.handle(data => {
updateEntryFromProgress(data);
})
}).catch(err => {
console.error(err);
})
}
function stopImports() {
if (listener) {
listener.close();
listener = null;
}
}
onUnmounted(() => {
stopImports();
unwatch();
});
</script>
<template>
@ -38,24 +91,19 @@ import SidebarTrigger from '@/components/ui/sidebar/SidebarTrigger.vue';
Uploaded files
</h3>
<div class="space-y-2">
<SingleUploadEntry title="Test" size="3.8 MB" format="mp4" type="file" />
<SingleUploadEntry title="Test" :progress="78" />
<SingleUploadEntry title="Test" error="Uploading failed, please check your internet" />
<PlaylistUploadEntry title="My Playlist" :trackCount="10" type="YouTube" :progress="75"
:playlistProgressData="{
playlistId: 1,
trackSourceId: 2,
userId: 3,
timestamp: 123456,
ytdlnStdout: `[youtube] Extracting URL: https://www.youtube.com/playlist?list=PL1234567890
[youtube:playlist] PL1234567890: Downloading 10 videos
[download] Destination: My Playlist [PL1234567890].mp3
[ExtractAudio] Destination: My Playlist [PL1234567890].mp3
[info] Download completed: My Playlist [PL1234567890].mp3
[info] 10 files downloaded successfully`,
overallProgress: 34,
status: 'LOADING'
}" />
<UploadEntry v-for="entry in Array.from(progressEntries.values())" :key="entry.id"
:entry="entry" />
<Empty class="border border-dashed" v-if="progressEntries.size === 0">
<EmptyHeader>
<EmptyMedia variant="icon">
<MegaphoneOff />
</EmptyMedia>
<EmptyTitle>No imports found</EmptyTitle>
<EmptyDescription>
Upload any track to see their progress.
</EmptyDescription>
</EmptyHeader>
</Empty>
</div>
</div>
</div>

View File

@ -17,6 +17,7 @@
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"dotenv": "^17.2.3",
"eventsource": "^4.1.0",
"lucide-vue-next": "^0.548.0",
"nuxt": "^4.2.0",
"oidc-client-ts": "^3.3.0",
@ -1000,6 +1001,10 @@
"events-universal": ["events-universal@1.0.1", "", { "dependencies": { "bare-events": "^2.7.0" } }, "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw=="],
"eventsource": ["eventsource@4.1.0", "", { "dependencies": { "eventsource-parser": "^3.0.1" } }, "sha512-2GuF51iuHX6A9xdTccMTsNb7VO0lHZihApxhvQzJB5A03DvHDd2FQepodbMaztPBmBcE/ox7o2gqaxGhYB9LhQ=="],
"eventsource-parser": ["eventsource-parser@3.0.6", "", {}, "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg=="],
"execa": ["execa@5.1.1", "", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^6.0.0", "human-signals": "^2.1.0", "is-stream": "^2.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^4.0.1", "onetime": "^5.1.2", "signal-exit": "^3.0.3", "strip-final-newline": "^2.0.0" } }, "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg=="],
"expand-template": ["expand-template@2.0.3", "", {}, "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg=="],

View File

@ -26,8 +26,12 @@ export default defineConfig({
override: {
mutator: {
path: './app/composeables/api/axios-instance.ts',
name: 'axiosInstance',
},
operations: {
streamProgress: {
mutator: './app/composeables/api/event-source.ts',
}
}
},
}
},

View File

@ -22,6 +22,7 @@
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"dotenv": "^17.2.3",
"eventsource": "^4.1.0",
"lucide-vue-next": "^0.548.0",
"nuxt": "^4.2.0",
"oidc-client-ts": "^3.3.0",