summaryrefslogtreecommitdiff
path: root/apps/web/src/services/storage/storage-service.ts
diff options
context:
space:
mode:
authorMaze Winther <mazewinther@gmail.com>2026-01-31 22:19:11 +0100
committerMaze Winther <mazewinther@gmail.com>2026-01-31 22:19:11 +0100
commitf820d9a8948b97460ebdfdaa6b42b6d510e2b3cf (patch)
tree460b4cd9ac403fe5591bb0b5b4064e1662d08077 /apps/web/src/services/storage/storage-service.ts
parent7878c8bb7b83bce568ced990903b8195a9dd4605 (diff)
downloadopencut-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.ts475
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 };