diff options
| author | Maze Winther <mazewinther@gmail.com> | 2026-01-31 22:19:11 +0100 |
|---|---|---|
| committer | Maze Winther <mazewinther@gmail.com> | 2026-01-31 22:19:11 +0100 |
| commit | f820d9a8948b97460ebdfdaa6b42b6d510e2b3cf (patch) | |
| tree | 460b4cd9ac403fe5591bb0b5b4064e1662d08077 /apps/web/src/services/storage/storage-service.ts | |
| parent | 7878c8bb7b83bce568ced990903b8195a9dd4605 (diff) | |
| download | opencut-f820d9a8948b97460ebdfdaa6b42b6d510e2b3cf.tar.gz | |
refactor services structure
Diffstat (limited to 'apps/web/src/services/storage/storage-service.ts')
| -rw-r--r-- | apps/web/src/services/storage/storage-service.ts | 475 |
1 files changed, 0 insertions, 475 deletions
diff --git a/apps/web/src/services/storage/storage-service.ts b/apps/web/src/services/storage/storage-service.ts deleted file mode 100644 index cb0b2ced..00000000 --- a/apps/web/src/services/storage/storage-service.ts +++ /dev/null @@ -1,475 +0,0 @@ -import type { TProject, TProjectMetadata } from "@/types/project"; -import { getProjectDurationFromScenes } from "@/lib/scenes"; -import type { MediaAsset } from "@/types/assets"; -import { IndexedDBAdapter } from "./indexeddb-adapter"; -import { OPFSAdapter } from "./opfs-adapter"; -import type { - MediaAssetData, - StorageConfig, - SerializedProject, - SerializedScene, -} from "./types"; -import type { SavedSoundsData, SavedSound, SoundEffect } from "@/types/sounds"; -import { - migrations, - runStorageMigrations, -} from "@/services/storage/migrations"; -import type { TimelineTrack, TScene } from "@/types/timeline"; - -class StorageService { - private projectsAdapter: IndexedDBAdapter<SerializedProject>; - private savedSoundsAdapter: IndexedDBAdapter<SavedSoundsData>; - private config: StorageConfig; - private migrationsPromise: Promise<void> | null = null; - - constructor() { - this.config = { - projectsDb: "video-editor-projects", - mediaDb: "video-editor-media", - savedSoundsDb: "video-editor-saved-sounds", - version: 1, - }; - - this.projectsAdapter = new IndexedDBAdapter<SerializedProject>( - this.config.projectsDb, - "projects", - this.config.version, - ); - - this.savedSoundsAdapter = new IndexedDBAdapter<SavedSoundsData>( - this.config.savedSoundsDb, - "saved-sounds", - this.config.version, - ); - } - - private async ensureMigrations(): Promise<void> { - if (this.migrationsPromise) { - await this.migrationsPromise; - return; - } - - this.migrationsPromise = runStorageMigrations({ migrations }).then( - () => undefined, - ); - await this.migrationsPromise; - } - - private getProjectMediaAdapters({ projectId }: { projectId: string }) { - const mediaMetadataAdapter = new IndexedDBAdapter<MediaAssetData>( - `${this.config.mediaDb}-${projectId}`, - "media-metadata", - this.config.version, - ); - - const mediaAssetsAdapter = new OPFSAdapter(`media-files-${projectId}`); - - return { mediaMetadataAdapter, mediaAssetsAdapter }; - } - - private stripAudioBuffers({ - tracks, - }: { - tracks: TimelineTrack[]; - }): TimelineTrack[] { - return tracks.map((track) => { - if (track.type !== "audio") return track; - return { - ...track, - elements: track.elements.map((element) => { - const { buffer: _buffer, ...rest } = element; - return rest; - }), - }; - }); - } - - async saveProject({ project }: { project: TProject }): Promise<void> { - const duration = - project.metadata.duration ?? - getProjectDurationFromScenes({ scenes: project.scenes }); - const serializedScenes: SerializedScene[] = project.scenes.map((scene) => ({ - id: scene.id, - name: scene.name, - isMain: scene.isMain, - tracks: this.stripAudioBuffers({ tracks: scene.tracks }), - bookmarks: scene.bookmarks, - createdAt: scene.createdAt.toISOString(), - updatedAt: scene.updatedAt.toISOString(), - })); - - const serializedProject: SerializedProject = { - metadata: { - id: project.metadata.id, - name: project.metadata.name, - thumbnail: project.metadata.thumbnail, - duration, - createdAt: project.metadata.createdAt.toISOString(), - updatedAt: project.metadata.updatedAt.toISOString(), - }, - scenes: serializedScenes, - currentSceneId: project.currentSceneId, - settings: project.settings, - version: project.version, - timelineViewState: project.timelineViewState, - }; - - await this.projectsAdapter.set(project.metadata.id, serializedProject); - } - - async loadProject({ - id, - }: { - id: string; - }): Promise<{ project: TProject } | null> { - await this.ensureMigrations(); - const serializedProject = await this.projectsAdapter.get(id); - - if (!serializedProject) return null; - - const scenes = - serializedProject.scenes?.map((scene) => ({ - id: scene.id, - name: scene.name, - isMain: scene.isMain, - tracks: (scene.tracks ?? []).map((track) => - track.type === "video" - ? { ...track, isMain: track.isMain ?? false } // legacy: isMain was optional - : track, - ), - bookmarks: scene.bookmarks ?? [], - createdAt: new Date(scene.createdAt), - updatedAt: new Date(scene.updatedAt), - })) ?? []; - - const project: TProject = { - metadata: { - id: serializedProject.metadata.id, - name: serializedProject.metadata.name, - thumbnail: serializedProject.metadata.thumbnail, - duration: - serializedProject.metadata.duration ?? - getProjectDurationFromScenes({ scenes }), - createdAt: new Date(serializedProject.metadata.createdAt), - updatedAt: new Date(serializedProject.metadata.updatedAt), - }, - scenes, - currentSceneId: serializedProject.currentSceneId || "", - settings: serializedProject.settings, - version: serializedProject.version, - timelineViewState: serializedProject.timelineViewState, - }; - - return { project }; - } - - async loadAllProjects(): Promise<TProject[]> { - const projectIds = await this.projectsAdapter.list(); - const projects: TProject[] = []; - - for (const id of projectIds) { - const result = await this.loadProject({ id }); - if (result?.project) { - projects.push(result.project); - } - } - - return projects.sort( - (a, b) => b.metadata.updatedAt.getTime() - a.metadata.updatedAt.getTime(), - ); - } - - async loadAllProjectsMetadata(): Promise<TProjectMetadata[]> { - await this.ensureMigrations(); - const serializedProjects = await this.projectsAdapter.getAll(); - - const metadata = serializedProjects.map((serializedProject) => ({ - id: serializedProject.metadata.id, - name: serializedProject.metadata.name, - thumbnail: serializedProject.metadata.thumbnail, - duration: - serializedProject.metadata.duration ?? - getProjectDurationFromScenes({ - scenes: (serializedProject.scenes ?? []) as unknown as TScene[], - }), - createdAt: new Date(serializedProject.metadata.createdAt), - updatedAt: new Date(serializedProject.metadata.updatedAt), - })); - - return metadata.sort( - (a, b) => b.updatedAt.getTime() - a.updatedAt.getTime(), - ); - } - - async deleteProject({ id }: { id: string }): Promise<void> { - await this.projectsAdapter.remove(id); - } - - async saveMediaAsset({ - projectId, - mediaAsset, - }: { - projectId: string; - mediaAsset: MediaAsset; - }): Promise<void> { - const { mediaMetadataAdapter, mediaAssetsAdapter } = - this.getProjectMediaAdapters({ projectId }); - - await mediaAssetsAdapter.set(mediaAsset.id, mediaAsset.file); - - const metadata: MediaAssetData = { - id: mediaAsset.id, - name: mediaAsset.name, - type: mediaAsset.type, - size: mediaAsset.file.size, - lastModified: mediaAsset.file.lastModified, - width: mediaAsset.width, - height: mediaAsset.height, - duration: mediaAsset.duration, - thumbnailUrl: mediaAsset.thumbnailUrl, - ephemeral: mediaAsset.ephemeral, - }; - - await mediaMetadataAdapter.set(mediaAsset.id, metadata); - } - - async loadMediaAsset({ - projectId, - id, - }: { - projectId: string; - id: string; - }): Promise<MediaAsset | null> { - const { mediaMetadataAdapter, mediaAssetsAdapter } = - this.getProjectMediaAdapters({ projectId }); - - const [file, metadata] = await Promise.all([ - mediaAssetsAdapter.get(id), - mediaMetadataAdapter.get(id), - ]); - - if (!file || !metadata) return null; - - let url: string; - if (metadata.type === "image" && (!file.type || file.type === "")) { - try { - const text = await file.text(); - if (text.trim().startsWith("<svg")) { - const svgBlob = new Blob([text], { type: "image/svg+xml" }); - url = URL.createObjectURL(svgBlob); - } else { - url = URL.createObjectURL(file); - } - } catch { - url = URL.createObjectURL(file); - } - } else { - url = URL.createObjectURL(file); - } - - return { - id: metadata.id, - name: metadata.name, - type: metadata.type, - file, - url, - width: metadata.width, - height: metadata.height, - duration: metadata.duration, - thumbnailUrl: metadata.thumbnailUrl, - ephemeral: metadata.ephemeral, - }; - } - - async loadAllMediaAssets({ - projectId, - }: { - projectId: string; - }): Promise<MediaAsset[]> { - const { mediaMetadataAdapter } = this.getProjectMediaAdapters({ - projectId, - }); - - const mediaIds = await mediaMetadataAdapter.list(); - const mediaItems: MediaAsset[] = []; - - for (const id of mediaIds) { - const item = await this.loadMediaAsset({ projectId, id }); - if (item) { - mediaItems.push(item); - } - } - - return mediaItems; - } - - async deleteMediaAsset({ - projectId, - id, - }: { - projectId: string; - id: string; - }): Promise<void> { - const { mediaMetadataAdapter, mediaAssetsAdapter } = - this.getProjectMediaAdapters({ projectId }); - - await Promise.all([ - mediaAssetsAdapter.remove(id), - mediaMetadataAdapter.remove(id), - ]); - } - - async deleteProjectMedia({ - projectId, - }: { - projectId: string; - }): Promise<void> { - const { mediaMetadataAdapter, mediaAssetsAdapter } = - this.getProjectMediaAdapters({ projectId }); - - await Promise.all([ - mediaMetadataAdapter.clear(), - mediaAssetsAdapter.clear(), - ]); - } - - // Utility methods - async clearAllData(): Promise<void> { - // Clear all projects - await this.projectsAdapter.clear(); - - // Note: Project-specific media and timelines will be cleaned up when projects are deleted - } - - async getStorageInfo(): Promise<{ - projects: number; - isOPFSSupported: boolean; - isIndexedDBSupported: boolean; - }> { - const projectIds = await this.projectsAdapter.list(); - - return { - projects: projectIds.length, - isOPFSSupported: this.isOPFSSupported(), - isIndexedDBSupported: this.isIndexedDBSupported(), - }; - } - - async getProjectStorageInfo({ projectId }: { projectId: string }): Promise<{ - mediaItems: number; - }> { - const { mediaMetadataAdapter } = this.getProjectMediaAdapters({ - projectId, - }); - - const mediaIds = await mediaMetadataAdapter.list(); - - return { - mediaItems: mediaIds.length, - }; - } - - async loadSavedSounds(): Promise<SavedSoundsData> { - try { - const savedSoundsData = await this.savedSoundsAdapter.get("user-sounds"); - return ( - savedSoundsData || { - sounds: [], - lastModified: new Date().toISOString(), - } - ); - } catch (error) { - console.error("Failed to load saved sounds:", error); - return { sounds: [], lastModified: new Date().toISOString() }; - } - } - - async saveSoundEffect({ - soundEffect, - }: { - soundEffect: SoundEffect; - }): Promise<void> { - try { - const currentData = await this.loadSavedSounds(); - - // Check if sound is already saved - if (currentData.sounds.some((sound) => sound.id === soundEffect.id)) { - return; // Already saved - } - - const savedSound: SavedSound = { - id: soundEffect.id, - name: soundEffect.name, - username: soundEffect.username, - previewUrl: soundEffect.previewUrl, - downloadUrl: soundEffect.downloadUrl, - duration: soundEffect.duration, - tags: soundEffect.tags, - license: soundEffect.license, - savedAt: new Date().toISOString(), - }; - - const updatedData: SavedSoundsData = { - sounds: [...currentData.sounds, savedSound], - lastModified: new Date().toISOString(), - }; - - await this.savedSoundsAdapter.set("user-sounds", updatedData); - } catch (error) { - console.error("Failed to save sound effect:", error); - throw error; - } - } - - async removeSavedSound({ soundId }: { soundId: number }): Promise<void> { - try { - const currentData = await this.loadSavedSounds(); - - const updatedData: SavedSoundsData = { - sounds: currentData.sounds.filter((sound) => sound.id !== soundId), - lastModified: new Date().toISOString(), - }; - - await this.savedSoundsAdapter.set("user-sounds", updatedData); - } catch (error) { - console.error("Failed to remove saved sound:", error); - throw error; - } - } - - async isSoundSaved({ soundId }: { soundId: number }): Promise<boolean> { - try { - const currentData = await this.loadSavedSounds(); - return currentData.sounds.some((sound) => sound.id === soundId); - } catch (error) { - console.error("Failed to check if sound is saved:", error); - return false; - } - } - - async clearSavedSounds(): Promise<void> { - try { - await this.savedSoundsAdapter.remove("user-sounds"); - } catch (error) { - console.error("Failed to clear saved sounds:", error); - throw error; - } - } - - // Check browser support - isOPFSSupported(): boolean { - return OPFSAdapter.isSupported(); - } - - isIndexedDBSupported(): boolean { - return "indexedDB" in window; - } - - isFullySupported(): boolean { - return this.isIndexedDBSupported() && this.isOPFSSupported(); - } -} - -// Export singleton instance -export const storageService = new StorageService(); -export { StorageService }; |
