@opendaw/studio-core 0.0.26 → 0.0.27

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. package/dist/AudioOfflineRenderer.d.ts.map +1 -1
  2. package/dist/AudioOfflineRenderer.js +7 -3
  3. package/dist/Engine.d.ts +4 -4
  4. package/dist/Engine.d.ts.map +1 -1
  5. package/dist/EngineFacade.d.ts +4 -4
  6. package/dist/EngineFacade.d.ts.map +1 -1
  7. package/dist/EngineWorklet.d.ts +4 -4
  8. package/dist/EngineWorklet.d.ts.map +1 -1
  9. package/dist/RecordingWorklet.d.ts +1 -1
  10. package/dist/RecordingWorklet.d.ts.map +1 -1
  11. package/dist/RecordingWorklet.js +1 -1
  12. package/dist/WavFile.d.ts +10 -0
  13. package/dist/WavFile.d.ts.map +1 -0
  14. package/dist/WavFile.js +94 -0
  15. package/dist/WorkerAgents.d.ts +1 -1
  16. package/dist/WorkerAgents.d.ts.map +1 -1
  17. package/dist/WorkerAgents.js +15 -3
  18. package/dist/capture/Capture.d.ts +1 -1
  19. package/dist/capture/Capture.d.ts.map +1 -1
  20. package/dist/capture/CaptureAudio.js +1 -1
  21. package/dist/capture/CaptureDevices.d.ts +1 -1
  22. package/dist/capture/CaptureDevices.d.ts.map +1 -1
  23. package/dist/capture/CaptureMidi.js +1 -1
  24. package/dist/clouds/CloudAuthManager.d.ts +10 -0
  25. package/dist/clouds/CloudAuthManager.d.ts.map +1 -0
  26. package/dist/clouds/CloudAuthManager.js +195 -0
  27. package/dist/clouds/CloudBackup.d.ts +8 -0
  28. package/dist/clouds/CloudBackup.d.ts.map +1 -0
  29. package/dist/clouds/CloudBackup.js +55 -0
  30. package/dist/clouds/CloudBackupProjects.d.ts +10 -0
  31. package/dist/clouds/CloudBackupProjects.d.ts.map +1 -0
  32. package/dist/clouds/CloudBackupProjects.js +167 -0
  33. package/dist/clouds/CloudBackupSamples.d.ts +13 -0
  34. package/dist/clouds/CloudBackupSamples.d.ts.map +1 -0
  35. package/dist/clouds/CloudBackupSamples.js +129 -0
  36. package/dist/clouds/CloudHandler.d.ts +9 -0
  37. package/dist/clouds/CloudHandler.d.ts.map +1 -0
  38. package/dist/clouds/CloudHandler.js +1 -0
  39. package/dist/clouds/CloudService.d.ts +2 -0
  40. package/dist/clouds/CloudService.d.ts.map +1 -0
  41. package/dist/clouds/CloudService.js +1 -0
  42. package/dist/clouds/DropboxHandler.d.ts +12 -0
  43. package/dist/clouds/DropboxHandler.d.ts.map +1 -0
  44. package/dist/clouds/DropboxHandler.js +83 -0
  45. package/dist/clouds/GoogleDriveHandler.d.ts +12 -0
  46. package/dist/clouds/GoogleDriveHandler.d.ts.map +1 -0
  47. package/dist/clouds/GoogleDriveHandler.js +256 -0
  48. package/dist/dawproject/DawProject.d.ts +2 -2
  49. package/dist/dawproject/DawProject.d.ts.map +1 -1
  50. package/dist/dawproject/DawProjectExporter.js +2 -2
  51. package/dist/dawproject/DawProjectImport.d.ts +1 -1
  52. package/dist/dawproject/DawProjectImport.d.ts.map +1 -1
  53. package/dist/dawproject/DeviceIO.d.ts +1 -1
  54. package/dist/dawproject/DeviceIO.d.ts.map +1 -1
  55. package/dist/dawproject/DeviceIO.js +2 -1
  56. package/dist/index.d.ts +6 -1
  57. package/dist/index.d.ts.map +1 -1
  58. package/dist/index.js +6 -1
  59. package/dist/processors.js +3 -3
  60. package/dist/processors.js.map +4 -4
  61. package/dist/project/Project.d.ts +0 -5
  62. package/dist/project/Project.d.ts.map +1 -1
  63. package/dist/project/Project.js +4 -9
  64. package/dist/project/ProjectApi.d.ts +9 -1
  65. package/dist/project/ProjectApi.d.ts.map +1 -1
  66. package/dist/project/ProjectApi.js +67 -3
  67. package/dist/project/ProjectBundle.d.ts +1 -1
  68. package/dist/project/ProjectBundle.d.ts.map +1 -1
  69. package/dist/project/ProjectBundle.js +1 -1
  70. package/dist/project/ProjectPaths.d.ts +4 -4
  71. package/dist/project/ProjectPaths.d.ts.map +1 -1
  72. package/dist/project/ProjectProfile.d.ts +2 -2
  73. package/dist/project/ProjectProfile.d.ts.map +1 -1
  74. package/dist/project/ProjectSignals.d.ts +6 -0
  75. package/dist/project/ProjectSignals.d.ts.map +1 -0
  76. package/dist/project/ProjectSignals.js +4 -0
  77. package/dist/project/ProjectStorage.d.ts +23 -0
  78. package/dist/project/ProjectStorage.d.ts.map +1 -0
  79. package/dist/project/ProjectStorage.js +59 -0
  80. package/dist/samples/MainThreadSampleLoader.d.ts +2 -2
  81. package/dist/samples/MainThreadSampleLoader.d.ts.map +1 -1
  82. package/dist/samples/MainThreadSampleLoader.js +2 -3
  83. package/dist/samples/MainThreadSampleManager.d.ts +4 -4
  84. package/dist/samples/MainThreadSampleManager.d.ts.map +1 -1
  85. package/dist/samples/OpenSampleAPI.d.ts +4 -2
  86. package/dist/samples/OpenSampleAPI.d.ts.map +1 -1
  87. package/dist/samples/OpenSampleAPI.js +19 -2
  88. package/dist/samples/SampleAPI.d.ts +2 -2
  89. package/dist/samples/SampleAPI.d.ts.map +1 -1
  90. package/dist/samples/SampleImporter.d.ts +1 -1
  91. package/dist/samples/SampleImporter.d.ts.map +1 -1
  92. package/dist/samples/SampleProvider.d.ts +1 -1
  93. package/dist/samples/SampleProvider.d.ts.map +1 -1
  94. package/dist/samples/SampleStorage.d.ts +7 -5
  95. package/dist/samples/SampleStorage.d.ts.map +1 -1
  96. package/dist/samples/SampleStorage.js +31 -17
  97. package/dist/workers.js +2 -2
  98. package/dist/workers.js.map +4 -4
  99. package/package.json +14 -14
  100. package/dist/Wav.d.ts +0 -6
  101. package/dist/Wav.d.ts.map +0 -1
  102. package/dist/Wav.js +0 -46
@@ -0,0 +1,55 @@
1
+ import { DefaultObservableValue, Progress, RuntimeNotifier, RuntimeSignal } from "@opendaw/lib-std";
2
+ import { CloudBackupSamples } from "./CloudBackupSamples";
3
+ import { CloudBackupProjects } from "./CloudBackupProjects";
4
+ import { ProjectSignals } from "../project/ProjectSignals";
5
+ export var CloudBackup;
6
+ (function (CloudBackup) {
7
+ CloudBackup.backup = async (cloudAuthManager, service) => {
8
+ const DialogMessage = `openDAW will never store or share your personal account details!
9
+
10
+ Dropbox requires permission to read “basic account info” such as your name and email, but openDAW does not use or retain this information. We only access the files you choose to synchronize.
11
+
12
+ Clicking 'Sync' may open a new tab to authorize your dropbox.`;
13
+ const approved = await RuntimeNotifier.approve({
14
+ headline: "openDAW and your data",
15
+ message: DialogMessage,
16
+ approveText: "Sync",
17
+ cancelText: "Cancel"
18
+ });
19
+ if (!approved) {
20
+ return;
21
+ }
22
+ try {
23
+ const handler = await cloudAuthManager.getHandler(service);
24
+ await CloudBackup.backupWithHandler(handler, service);
25
+ await RuntimeNotifier.info({
26
+ headline: "Cloud Backup",
27
+ message: "Everything is up to date."
28
+ });
29
+ }
30
+ catch (reason) {
31
+ console.warn(reason);
32
+ await RuntimeNotifier.info({
33
+ headline: `Could not sync`,
34
+ message: String(reason)
35
+ });
36
+ }
37
+ finally {
38
+ RuntimeSignal.dispatch(ProjectSignals.StorageUpdated);
39
+ }
40
+ };
41
+ CloudBackup.backupWithHandler = async (cloudHandler, service) => {
42
+ const progressValue = new DefaultObservableValue(0.0);
43
+ const notification = RuntimeNotifier.progress({ headline: `Backup with ${service}`, progress: progressValue });
44
+ const log = (text) => notification.message = text;
45
+ const [progressSamples, progressProjects] = Progress.split(progress => progressValue.setValue(progress), 2);
46
+ try {
47
+ await CloudBackupSamples.start(cloudHandler, progressSamples, log);
48
+ await CloudBackupProjects.start(cloudHandler, progressProjects, log);
49
+ }
50
+ finally {
51
+ progressValue.terminate();
52
+ notification.terminate();
53
+ }
54
+ };
55
+ })(CloudBackup || (CloudBackup = {}));
@@ -0,0 +1,10 @@
1
+ import { Procedure, Progress } from "@opendaw/lib-std";
2
+ import { CloudHandler } from "./CloudHandler";
3
+ export declare class CloudBackupProjects {
4
+ #private;
5
+ static readonly RemotePath = "projects";
6
+ static readonly RemoteCatalogPath: string;
7
+ static start(cloudHandler: CloudHandler, progress: Progress.Handler, log: Procedure<string>): Promise<void>;
8
+ private constructor();
9
+ }
10
+ //# sourceMappingURL=CloudBackupProjects.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CloudBackupProjects.d.ts","sourceRoot":"","sources":["../../src/clouds/CloudBackupProjects.ts"],"names":[],"mappings":"AAAA,OAAO,EAOH,SAAS,EACT,QAAQ,EAKX,MAAM,kBAAkB,CAAA;AAIzB,OAAO,EAAC,YAAY,EAAC,MAAM,gBAAgB,CAAA;AAY3C,qBAAa,mBAAmB;;IAC5B,MAAM,CAAC,QAAQ,CAAC,UAAU,cAAa;IACvC,MAAM,CAAC,QAAQ,CAAC,iBAAiB,SAAkC;WAEtD,KAAK,CAAC,YAAY,EAAE,YAAY,EAC1B,QAAQ,EAAE,QAAQ,CAAC,OAAO,EAC1B,GAAG,EAAE,SAAS,CAAC,MAAM,CAAC;IAmBzC,OAAO;CAyJV"}
@@ -0,0 +1,167 @@
1
+ import { Arrays, Errors, isAbsent, Objects, panic, Progress, RuntimeNotifier, TimeSpan, UUID } from "@opendaw/lib-std";
2
+ import { network, Promises } from "@opendaw/lib-runtime";
3
+ import { ProjectStorage } from "../project/ProjectStorage";
4
+ import { WorkerAgents } from "../WorkerAgents";
5
+ import { ProjectPaths } from "../project/ProjectPaths";
6
+ // these get indexed in the cloud with the uuid in the cloud's catalog
7
+ const catalogFields = ["name", "modified", "created", "tags", "description"];
8
+ export class CloudBackupProjects {
9
+ static RemotePath = "projects";
10
+ static RemoteCatalogPath = `${this.RemotePath}/index.json`;
11
+ static async start(cloudHandler, progress, log) {
12
+ log("Collecting all project domains...");
13
+ const [local, cloud] = await Promise.all([
14
+ ProjectStorage.listProjects()
15
+ .then(list => list.reduce((record, entry) => {
16
+ record[UUID.toString(entry.uuid)] = Objects.include(entry.meta, ...catalogFields);
17
+ return record;
18
+ }, {})),
19
+ cloudHandler.download(CloudBackupProjects.RemoteCatalogPath)
20
+ .then(json => JSON.parse(new TextDecoder().decode(json)))
21
+ .catch(reason => reason instanceof Errors.FileNotFound ? Arrays.empty() : panic(reason))
22
+ ]);
23
+ return new CloudBackupProjects(cloudHandler, { local, cloud }, log).#start(progress);
24
+ }
25
+ #cloudHandler;
26
+ #projectDomains;
27
+ #log;
28
+ constructor(cloudHandler, projectDomains, log) {
29
+ this.#cloudHandler = cloudHandler;
30
+ this.#projectDomains = projectDomains;
31
+ this.#log = log;
32
+ }
33
+ async #start(progress) {
34
+ const trashed = await ProjectStorage.loadTrashedIds();
35
+ const [uploadProgress, trashProgress, downloadProgress] = Progress.splitWithWeights(progress, [0.45, 0.10, 0.45]);
36
+ await this.#upload(uploadProgress);
37
+ await this.#trash(trashed, trashProgress);
38
+ await this.#download(trashed, downloadProgress);
39
+ }
40
+ async #upload(progress) {
41
+ const { local, cloud } = this.#projectDomains;
42
+ const isUnsynced = (localProject, cloudProject) => isAbsent(cloudProject)
43
+ || new Date(cloudProject.modified).getTime() < new Date(localProject.modified).getTime();
44
+ const unsyncedProjects = Object.entries(local)
45
+ .filter(([uuid, localProject]) => isUnsynced(localProject, cloud[uuid]))
46
+ .map(([uuid, localProject]) => ([UUID.asString(uuid), localProject]));
47
+ if (unsyncedProjects.length === 0) {
48
+ this.#log("No unsynced projects found.");
49
+ progress(1.0);
50
+ return;
51
+ }
52
+ const uploaded = await Promises.sequentialAll(unsyncedProjects
53
+ .map(([uuidAsString, meta], index, { length }) => async () => {
54
+ progress((index + 1) / length);
55
+ this.#log(`Uploading project '${meta.name}'`);
56
+ const uuid = UUID.parse(uuidAsString);
57
+ const folder = `${CloudBackupProjects.RemotePath}/${uuidAsString}`;
58
+ const metaFile = await ProjectStorage.loadMeta(uuid);
59
+ const projectFile = await ProjectStorage.loadProject(uuid);
60
+ const optCoverFile = await ProjectStorage.loadCover(uuid);
61
+ const tasks = [];
62
+ const removeProjectPath = `${folder}/project.od`;
63
+ const remoteMetaPath = `${folder}/meta.json`;
64
+ tasks.push(() => this.#cloudHandler.upload(removeProjectPath, projectFile));
65
+ tasks.push(() => this.#cloudHandler.upload(remoteMetaPath, metaFile));
66
+ optCoverFile.ifSome(cover => {
67
+ const removeCoverPath = `${folder}/image.bin`;
68
+ return tasks.push(() => this.#cloudHandler.upload(removeCoverPath, cover));
69
+ });
70
+ await Promises.approvedRetry(() => Promises.timeout(Promises.sequentialAll(tasks), TimeSpan.minutes(10), "Upload timeout (10 min)."), error => ({
71
+ headline: "Upload failed",
72
+ message: `Failed to upload project '${meta.name}'. '${error}'`,
73
+ approveText: "Retry",
74
+ cancelText: "Cancel"
75
+ }));
76
+ return { uuidAsString, meta };
77
+ }));
78
+ const catalog = uploaded
79
+ .reduce((projects, project) => {
80
+ projects[UUID.asString(project.uuidAsString)] = project.meta;
81
+ return projects;
82
+ }, { ...cloud });
83
+ await this.#uploadCatalog(catalog);
84
+ progress(1.0);
85
+ }
86
+ async #trash(trashed, progress) {
87
+ const { cloud } = this.#projectDomains;
88
+ const obsolete = Arrays.intersect(Object.entries(cloud), trashed, ([uuid, _], trashed) => uuid === trashed);
89
+ if (obsolete.length > 0) {
90
+ const approved = await RuntimeNotifier.approve({
91
+ headline: "Delete Projects?",
92
+ message: `Found ${obsolete.length} locally deleted projects. Delete from cloud as well?`,
93
+ approveText: "Yes",
94
+ cancelText: "No"
95
+ });
96
+ if (approved) {
97
+ const deleted = await Promises.sequentialAll(obsolete.map(([uuid, meta], index, { length }) => async () => {
98
+ progress((index + 1) / length);
99
+ const path = `${CloudBackupProjects.RemotePath}/${uuid}`;
100
+ this.#log(`Deleting '${meta.name}'`);
101
+ await this.#cloudHandler.delete(path);
102
+ return UUID.asString(uuid);
103
+ }));
104
+ const catalog = { ...cloud };
105
+ deleted.forEach(uuid => delete catalog[uuid]);
106
+ await this.#uploadCatalog(catalog);
107
+ }
108
+ }
109
+ progress(1.0);
110
+ }
111
+ async #download(trashed, progress) {
112
+ const { cloud, local } = this.#projectDomains;
113
+ const compareFn = ([uuidA], [uuidB]) => uuidA === uuidB;
114
+ const missingLocally = Arrays.subtract(Object.entries(cloud), Object.entries(local), compareFn);
115
+ const download = Arrays.subtract(missingLocally, trashed, ([projectUUID], uuid) => projectUUID === uuid);
116
+ if (download.length === 0) {
117
+ this.#log("No projects to download.");
118
+ progress(1.0);
119
+ return;
120
+ }
121
+ await Promises.sequentialAll(download.map(([uuidAsString, meta], index, { length }) => async () => {
122
+ progress((index + 1) / length);
123
+ const uuid = UUID.parse(uuidAsString);
124
+ const path = `${CloudBackupProjects.RemotePath}/${uuidAsString}`;
125
+ this.#log(`Downloading project '${meta.name}'`);
126
+ const files = await Promises.guardedRetry(() => this.#cloudHandler.list(path), network.DefaultRetry);
127
+ const hasProjectFile = files.includes("project.od");
128
+ const hasMetaFile = files.includes("meta.json");
129
+ if (!hasProjectFile || !hasMetaFile) {
130
+ console.warn(`hasProjectFile: ${hasProjectFile}, hasMetaFile: ${hasMetaFile} for ${uuidAsString}`);
131
+ const approvedDeletion = await RuntimeNotifier.approve({
132
+ headline: "Download failed",
133
+ message: `Project '${meta.name}' is corrupted. Delete it from cloud?.`,
134
+ approveText: "Yes",
135
+ cancelText: "Ignore"
136
+ });
137
+ if (approvedDeletion) {
138
+ await this.#cloudHandler.delete(path);
139
+ }
140
+ else {
141
+ return uuidAsString;
142
+ }
143
+ }
144
+ const projectPath = `${path}/project.od`;
145
+ const metaPath = `${path}/meta.json`;
146
+ const coverPath = `${path}/image.bin`;
147
+ const projectArrayBuffer = await Promises.guardedRetry(() => this.#cloudHandler.download(projectPath), network.DefaultRetry);
148
+ const metaArrayBuffer = await Promises.guardedRetry(() => this.#cloudHandler.download(metaPath), network.DefaultRetry);
149
+ await WorkerAgents.Opfs.write(ProjectPaths.projectFile(uuid), new Uint8Array(projectArrayBuffer));
150
+ await WorkerAgents.Opfs.write(ProjectPaths.projectMeta(uuid), new Uint8Array(metaArrayBuffer));
151
+ const hasCover = files.some(file => file.endsWith("image.bin"));
152
+ if (hasCover) {
153
+ const arrayBuffer = await Promises.guardedRetry(() => this.#cloudHandler.download(coverPath), network.DefaultRetry);
154
+ await WorkerAgents.Opfs.write(ProjectPaths.projectCover(uuid), new Uint8Array(arrayBuffer));
155
+ }
156
+ return uuidAsString;
157
+ }));
158
+ this.#log("Download projects complete.");
159
+ progress(1.0);
160
+ }
161
+ async #uploadCatalog(catalog) {
162
+ this.#log("Uploading project catalog...");
163
+ const jsonString = JSON.stringify(catalog, null, 2);
164
+ const buffer = new TextEncoder().encode(jsonString).buffer;
165
+ return this.#cloudHandler.upload(CloudBackupProjects.RemoteCatalogPath, buffer);
166
+ }
167
+ }
@@ -0,0 +1,13 @@
1
+ import { Procedure, Progress, UUID } from "@opendaw/lib-std";
2
+ import { Sample } from "@opendaw/studio-adapters";
3
+ import { CloudHandler } from "./CloudHandler";
4
+ export declare class CloudBackupSamples {
5
+ #private;
6
+ static readonly RemotePath = "samples";
7
+ static readonly RemoteCatalogPath: string;
8
+ static readonly areSamplesEqual: ({ uuid: a }: Sample, { uuid: b }: Sample) => boolean;
9
+ static pathFor(uuid: UUID.String): string;
10
+ static start(cloudHandler: CloudHandler, progress: Progress.Handler, log: Procedure<string>): Promise<void>;
11
+ private constructor();
12
+ }
13
+ //# sourceMappingURL=CloudBackupSamples.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CloudBackupSamples.d.ts","sourceRoot":"","sources":["../../src/clouds/CloudBackupSamples.ts"],"names":[],"mappings":"AAAA,OAAO,EAAwB,SAAS,EAAE,QAAQ,EAAmB,IAAI,EAAC,MAAM,kBAAkB,CAAA;AAGlG,OAAO,EAAY,MAAM,EAAC,MAAM,0BAA0B,CAAA;AAG1D,OAAO,EAAC,YAAY,EAAC,MAAM,gBAAgB,CAAA;AAM3C,qBAAa,kBAAkB;;IAC3B,MAAM,CAAC,QAAQ,CAAC,UAAU,aAAY;IACtC,MAAM,CAAC,QAAQ,CAAC,iBAAiB,SAAkC;IACnE,MAAM,CAAC,QAAQ,CAAC,eAAe,GAAI,aAAW,MAAM,EAAE,aAAW,MAAM,aAAY;IAEnF,MAAM,CAAC,OAAO,CAAC,IAAI,EAAE,IAAI,CAAC,MAAM,GAAG,MAAM;WAE5B,KAAK,CAAC,YAAY,EAAE,YAAY,EAC1B,QAAQ,EAAE,QAAQ,CAAC,OAAO,EAC1B,GAAG,EAAE,SAAS,CAAC,MAAM,CAAC;IAgBzC,OAAO;CAsHV"}
@@ -0,0 +1,129 @@
1
+ import { Arrays, Errors, panic, Progress, RuntimeNotifier, UUID } from "@opendaw/lib-std";
2
+ import { network, Promises } from "@opendaw/lib-runtime";
3
+ import { SamplePeaks } from "@opendaw/lib-fusion";
4
+ import { OpenSampleAPI } from "../samples/OpenSampleAPI";
5
+ import { SampleStorage } from "../samples/SampleStorage";
6
+ import { WorkerAgents } from "../WorkerAgents";
7
+ import { WavFile } from "../WavFile";
8
+ export class CloudBackupSamples {
9
+ static RemotePath = "samples";
10
+ static RemoteCatalogPath = `${this.RemotePath}/index.json`;
11
+ static areSamplesEqual = ({ uuid: a }, { uuid: b }) => a === b;
12
+ static pathFor(uuid) { return `${this.RemotePath}/${uuid}.wav`; }
13
+ static async start(cloudHandler, progress, log) {
14
+ log("Collecting all sample domains...");
15
+ const [stock, local, cloud] = await Promise.all([
16
+ OpenSampleAPI.get().all(),
17
+ SampleStorage.listSamples(),
18
+ cloudHandler.download(CloudBackupSamples.RemoteCatalogPath)
19
+ .then(json => JSON.parse(new TextDecoder().decode(json)))
20
+ .catch(reason => reason instanceof Errors.FileNotFound ? Arrays.empty() : panic(reason))
21
+ ]);
22
+ return new CloudBackupSamples(cloudHandler, { stock, local, cloud }, log).#start(progress);
23
+ }
24
+ #cloudHandler;
25
+ #sampleDomains;
26
+ #log;
27
+ constructor(cloudHandler, sampleDomains, log) {
28
+ this.#cloudHandler = cloudHandler;
29
+ this.#sampleDomains = sampleDomains;
30
+ this.#log = log;
31
+ }
32
+ async #start(progress) {
33
+ const trashed = await SampleStorage.loadTrashedIds();
34
+ const [uploadProgress, trashProgress, downloadProgress] = Progress.splitWithWeights(progress, [0.45, 0.10, 0.45]);
35
+ await this.#upload(uploadProgress);
36
+ await this.#trash(trashed, trashProgress);
37
+ await this.#download(trashed, downloadProgress);
38
+ }
39
+ async #upload(progress) {
40
+ const { stock, local, cloud } = this.#sampleDomains;
41
+ const maybeUnsyncedSamples = Arrays.subtract(local, stock, CloudBackupSamples.areSamplesEqual);
42
+ const unsyncedSamples = Arrays.subtract(maybeUnsyncedSamples, cloud, CloudBackupSamples.areSamplesEqual);
43
+ if (unsyncedSamples.length === 0) {
44
+ this.#log("No unsynced samples found.");
45
+ progress(1.0);
46
+ return;
47
+ }
48
+ const uploadedSamples = await Promises.sequentialAll(unsyncedSamples.map((sample, index, { length }) => async () => {
49
+ progress((index + 1) / length);
50
+ this.#log(`Uploading sample '${sample.name}'`);
51
+ const arrayBuffer = await SampleStorage.loadSample(UUID.parse(sample.uuid))
52
+ .then(([{ frames: channels, numberOfChannels, numberOfFrames: numFrames, sampleRate }]) => WavFile.encodeFloats({ channels, numberOfChannels, numFrames, sampleRate }));
53
+ const path = CloudBackupSamples.pathFor(sample.uuid);
54
+ await Promises.approvedRetry(() => this.#cloudHandler.upload(path, arrayBuffer), error => ({
55
+ headline: "Upload failed",
56
+ message: `Failed to upload sample '${sample.name}'. '${error}'`,
57
+ approveText: "Retry",
58
+ cancelText: "Cancel"
59
+ }));
60
+ return sample;
61
+ }));
62
+ const catalog = Arrays.merge(cloud, uploadedSamples, CloudBackupSamples.areSamplesEqual);
63
+ await this.#uploadCatalog(catalog);
64
+ progress(1.0);
65
+ }
66
+ async #trash(trashed, progress) {
67
+ const { cloud } = this.#sampleDomains;
68
+ const obsolete = Arrays.intersect(cloud, trashed, (sample, uuid) => sample.uuid === uuid);
69
+ if (obsolete.length === 0) {
70
+ progress(1.0);
71
+ return;
72
+ }
73
+ const approved = await RuntimeNotifier.approve({
74
+ headline: "Delete Samples?",
75
+ message: `Found ${obsolete.length} locally deleted samples. Delete from cloud as well?`,
76
+ approveText: "Yes",
77
+ cancelText: "No"
78
+ });
79
+ if (!approved) {
80
+ progress(1.0);
81
+ return;
82
+ }
83
+ const result = await Promises.sequentialAll(obsolete.map((sample, index, { length }) => async () => {
84
+ progress((index + 1) / length);
85
+ this.#log(`Deleting '${sample.name}'`);
86
+ await this.#cloudHandler.delete(CloudBackupSamples.pathFor(sample.uuid));
87
+ return sample;
88
+ }));
89
+ const catalog = cloud.slice();
90
+ result.forEach((sample) => Arrays.removeIf(catalog, ({ uuid }) => sample.uuid === uuid));
91
+ await this.#uploadCatalog(catalog);
92
+ progress(1.0);
93
+ }
94
+ async #download(trashed, progress) {
95
+ const { cloud, local } = this.#sampleDomains;
96
+ const missingLocally = Arrays.subtract(cloud, local, CloudBackupSamples.areSamplesEqual);
97
+ const download = Arrays.subtract(missingLocally, trashed, (sample, uuid) => sample.uuid === uuid);
98
+ if (download.length === 0) {
99
+ this.#log("No samples to download.");
100
+ progress(1.0);
101
+ return;
102
+ }
103
+ await Promises.sequentialAll(download.map((sample, index, { length }) => async () => {
104
+ progress((index + 1) / length);
105
+ this.#log(`Downloading sample '${sample.name}'`);
106
+ const path = CloudBackupSamples.pathFor(sample.uuid);
107
+ const buffer = await Promises.guardedRetry(() => this.#cloudHandler.download(path), network.DefaultRetry);
108
+ const waveAudio = WavFile.decodeFloats(buffer);
109
+ const audioData = {
110
+ sampleRate: waveAudio.sampleRate,
111
+ numberOfFrames: waveAudio.numFrames,
112
+ numberOfChannels: waveAudio.channels.length,
113
+ frames: waveAudio.channels
114
+ };
115
+ const shifts = SamplePeaks.findBestFit(audioData.numberOfFrames);
116
+ const peaks = await WorkerAgents.Peak.generateAsync(Progress.Empty, shifts, audioData.frames, audioData.numberOfFrames, audioData.numberOfChannels);
117
+ await SampleStorage.saveSample(UUID.parse(sample.uuid), audioData, peaks, sample);
118
+ return sample;
119
+ }));
120
+ this.#log("Download samples complete.");
121
+ progress(1.0);
122
+ }
123
+ async #uploadCatalog(catalog) {
124
+ this.#log("Uploading sample catalog...");
125
+ const jsonString = JSON.stringify(catalog, null, 2);
126
+ const buffer = new TextEncoder().encode(jsonString).buffer;
127
+ return this.#cloudHandler.upload(CloudBackupSamples.RemoteCatalogPath, buffer);
128
+ }
129
+ }
@@ -0,0 +1,9 @@
1
+ export interface CloudHandler {
2
+ upload(path: string, data: ArrayBuffer): Promise<void>;
3
+ exists(path: string): Promise<boolean>;
4
+ download(path: string): Promise<ArrayBuffer>;
5
+ list(path?: string): Promise<string[]>;
6
+ delete(path: string): Promise<void>;
7
+ alive(): Promise<void>;
8
+ }
9
+ //# sourceMappingURL=CloudHandler.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CloudHandler.d.ts","sourceRoot":"","sources":["../../src/clouds/CloudHandler.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,YAAY;IACzB,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACtD,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;IACtC,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CAAA;IAC5C,IAAI,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAAA;IACtC,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACnC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;CACzB"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,2 @@
1
+ export type CloudService = "Dropbox" | "GoogleDrive";
2
+ //# sourceMappingURL=CloudService.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CloudService.d.ts","sourceRoot":"","sources":["../../src/clouds/CloudService.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,YAAY,GAAG,SAAS,GAAG,aAAa,CAAA"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,12 @@
1
+ import { CloudHandler } from "./CloudHandler";
2
+ export declare class DropboxHandler implements CloudHandler {
3
+ #private;
4
+ constructor(accessToken: string);
5
+ alive(): Promise<void>;
6
+ upload(path: string, buffer: ArrayBuffer): Promise<void>;
7
+ download(path: string): Promise<ArrayBuffer>;
8
+ exists(path: string): Promise<boolean>;
9
+ list(path?: string): Promise<Array<string>>;
10
+ delete(path: string): Promise<void>;
11
+ }
12
+ //# sourceMappingURL=DropboxHandler.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"DropboxHandler.d.ts","sourceRoot":"","sources":["../../src/clouds/DropboxHandler.ts"],"names":[],"mappings":"AAGA,OAAO,EAAC,YAAY,EAAC,MAAM,gBAAgB,CAAA;AAI3C,qBAAa,cAAe,YAAW,YAAY;;gBAKnC,WAAW,EAAE,MAAM;IAEzB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAMtB,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC;IAaxD,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC;IAe5C,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAWtC,IAAI,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;IAO3C,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CA4B5C"}
@@ -0,0 +1,83 @@
1
+ import { DropboxResponseError } from "dropbox";
2
+ import { Errors, isDefined, Option, panic } from "@opendaw/lib-std";
3
+ import { Promises } from "@opendaw/lib-runtime";
4
+ // written by ChatGPT
5
+ export class DropboxHandler {
6
+ #accessToken;
7
+ #dropboxClient = Option.None;
8
+ constructor(accessToken) { this.#accessToken = accessToken; }
9
+ async alive() {
10
+ const client = await this.#ensureClient();
11
+ const { status, error } = await Promises.tryCatch(client.usersGetCurrentAccount());
12
+ if (status === "rejected")
13
+ return panic(error);
14
+ }
15
+ async upload(path, buffer) {
16
+ const client = await this.#ensureClient();
17
+ const fullPath = this.#getFullPath(path);
18
+ console.debug("[Dropbox] Uploading to:", fullPath);
19
+ const { status, error, value: result } = await Promises.tryCatch(client
20
+ .filesUpload({ path: fullPath, contents: buffer, mode: { ".tag": "overwrite" } }));
21
+ if (status === "rejected") {
22
+ return panic(error);
23
+ }
24
+ else {
25
+ console.debug("[Dropbox] Upload successful:", result.result.path_display);
26
+ }
27
+ }
28
+ async download(path) {
29
+ const client = await this.#ensureClient();
30
+ const fullPath = this.#getFullPath(path);
31
+ try {
32
+ const response = await client.filesDownload({ path: fullPath });
33
+ const { result: { fileBlob } } = response;
34
+ return fileBlob.arrayBuffer();
35
+ }
36
+ catch (error) {
37
+ if (this.#isNotFoundError(error)) {
38
+ throw new Errors.FileNotFound(path);
39
+ }
40
+ throw error;
41
+ }
42
+ }
43
+ async exists(path) {
44
+ const client = await this.#ensureClient();
45
+ const fullPath = this.#getFullPath(path);
46
+ const { status, error } = await Promises.tryCatch(client.filesGetMetadata({ path: fullPath })).catch(error => error);
47
+ if (status === "resolved")
48
+ return true;
49
+ return this.#isNotFoundError(error) ? false : panic(error);
50
+ }
51
+ async list(path) {
52
+ const client = await this.#ensureClient();
53
+ const fullPath = path ? this.#getFullPath(path) : "";
54
+ const response = await client.filesListFolder({ path: fullPath });
55
+ return response.result.entries.map(entry => entry.name).filter(isDefined);
56
+ }
57
+ async delete(path) {
58
+ const client = await this.#ensureClient();
59
+ const fullPath = this.#getFullPath(path);
60
+ await client.filesDeleteV2({ path: fullPath });
61
+ }
62
+ async #ensureClient() {
63
+ if (this.#dropboxClient.isEmpty()) {
64
+ const DropboxModule = await import("dropbox");
65
+ this.#dropboxClient = Option.wrap(new DropboxModule.Dropbox({ accessToken: this.#accessToken }));
66
+ }
67
+ return this.#dropboxClient.unwrap();
68
+ }
69
+ #getFullPath(path) {
70
+ if (path.includes(":") || path.includes("T")) {
71
+ const filename = path.replace(/:/g, "-");
72
+ return filename.startsWith("/") ? filename : `/${filename}`;
73
+ }
74
+ return path.startsWith("/") ? path : `/${path}`;
75
+ }
76
+ #isNotFoundError(error) {
77
+ if (!(error instanceof DropboxResponseError))
78
+ return false;
79
+ const e = error.error;
80
+ return e?.error?.[".tag"] === "path" &&
81
+ e.error?.path?.[".tag"] === "not_found";
82
+ }
83
+ }
@@ -0,0 +1,12 @@
1
+ import { CloudHandler } from "./CloudHandler";
2
+ export declare class GoogleDriveHandler implements CloudHandler {
3
+ #private;
4
+ constructor(accessToken: string);
5
+ alive(): Promise<void>;
6
+ upload(path: string, data: ArrayBuffer): Promise<void>;
7
+ download(path: string): Promise<ArrayBuffer>;
8
+ exists(path: string): Promise<boolean>;
9
+ list(path?: string): Promise<string[]>;
10
+ delete(path: string): Promise<void>;
11
+ }
12
+ //# sourceMappingURL=GoogleDriveHandler.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"GoogleDriveHandler.d.ts","sourceRoot":"","sources":["../../src/clouds/GoogleDriveHandler.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,YAAY,EAAC,MAAM,gBAAgB,CAAA;AAmB3C,qBAAa,kBAAmB,YAAW,YAAY;;gBAKvC,WAAW,EAAE,MAAM;IAEzB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IActB,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC;IA8BtD,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC;IAe5C,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAKtC,IAAI,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IA4BtC,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAmK5C"}