@opendaw/studio-core 0.0.25 → 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 (118) hide show
  1. package/dist/AudioDevices.js +4 -4
  2. package/dist/AudioOfflineRenderer.d.ts.map +1 -1
  3. package/dist/AudioOfflineRenderer.js +9 -5
  4. package/dist/Engine.d.ts +4 -4
  5. package/dist/Engine.d.ts.map +1 -1
  6. package/dist/EngineFacade.d.ts +4 -4
  7. package/dist/EngineFacade.d.ts.map +1 -1
  8. package/dist/EngineWorklet.d.ts +4 -4
  9. package/dist/EngineWorklet.d.ts.map +1 -1
  10. package/dist/RecordingWorklet.d.ts +1 -1
  11. package/dist/RecordingWorklet.d.ts.map +1 -1
  12. package/dist/RecordingWorklet.js +1 -1
  13. package/dist/WavFile.d.ts +10 -0
  14. package/dist/WavFile.d.ts.map +1 -0
  15. package/dist/WavFile.js +94 -0
  16. package/dist/WorkerAgents.d.ts +1 -1
  17. package/dist/WorkerAgents.d.ts.map +1 -1
  18. package/dist/WorkerAgents.js +15 -3
  19. package/dist/capture/Capture.d.ts +1 -1
  20. package/dist/capture/Capture.d.ts.map +1 -1
  21. package/dist/capture/CaptureAudio.d.ts.map +1 -1
  22. package/dist/capture/CaptureAudio.js +4 -4
  23. package/dist/capture/CaptureDevices.d.ts +1 -1
  24. package/dist/capture/CaptureDevices.d.ts.map +1 -1
  25. package/dist/capture/CaptureMidi.d.ts.map +1 -1
  26. package/dist/capture/CaptureMidi.js +6 -5
  27. package/dist/capture/Recording.d.ts.map +1 -1
  28. package/dist/capture/Recording.js +3 -3
  29. package/dist/clouds/CloudAuthManager.d.ts +10 -0
  30. package/dist/clouds/CloudAuthManager.d.ts.map +1 -0
  31. package/dist/clouds/CloudAuthManager.js +195 -0
  32. package/dist/clouds/CloudBackup.d.ts +8 -0
  33. package/dist/clouds/CloudBackup.d.ts.map +1 -0
  34. package/dist/clouds/CloudBackup.js +55 -0
  35. package/dist/clouds/CloudBackupProjects.d.ts +10 -0
  36. package/dist/clouds/CloudBackupProjects.d.ts.map +1 -0
  37. package/dist/clouds/CloudBackupProjects.js +167 -0
  38. package/dist/clouds/CloudBackupSamples.d.ts +13 -0
  39. package/dist/clouds/CloudBackupSamples.d.ts.map +1 -0
  40. package/dist/clouds/CloudBackupSamples.js +129 -0
  41. package/dist/clouds/CloudHandler.d.ts +9 -0
  42. package/dist/clouds/CloudHandler.d.ts.map +1 -0
  43. package/dist/clouds/CloudHandler.js +1 -0
  44. package/dist/clouds/CloudService.d.ts +2 -0
  45. package/dist/clouds/CloudService.d.ts.map +1 -0
  46. package/dist/clouds/CloudService.js +1 -0
  47. package/dist/clouds/DropboxHandler.d.ts +12 -0
  48. package/dist/clouds/DropboxHandler.d.ts.map +1 -0
  49. package/dist/clouds/DropboxHandler.js +83 -0
  50. package/dist/clouds/GoogleDriveHandler.d.ts +12 -0
  51. package/dist/clouds/GoogleDriveHandler.d.ts.map +1 -0
  52. package/dist/clouds/GoogleDriveHandler.js +256 -0
  53. package/dist/dawproject/DawProject.d.ts +2 -2
  54. package/dist/dawproject/DawProject.d.ts.map +1 -1
  55. package/dist/dawproject/DawProjectExporter.js +2 -2
  56. package/dist/dawproject/DawProjectImport.d.ts +1 -1
  57. package/dist/dawproject/DawProjectImport.d.ts.map +1 -1
  58. package/dist/dawproject/DeviceIO.d.ts +1 -1
  59. package/dist/dawproject/DeviceIO.d.ts.map +1 -1
  60. package/dist/dawproject/DeviceIO.js +2 -1
  61. package/dist/index.d.ts +18 -12
  62. package/dist/index.d.ts.map +1 -1
  63. package/dist/index.js +18 -12
  64. package/dist/midi/MIDILearning.d.ts +26 -0
  65. package/dist/midi/MIDILearning.d.ts.map +1 -0
  66. package/dist/midi/MIDILearning.js +88 -0
  67. package/dist/midi/MIDIMessageSubscriber.d.ts +5 -0
  68. package/dist/midi/MIDIMessageSubscriber.d.ts.map +1 -0
  69. package/dist/midi/MIDIMessageSubscriber.js +40 -0
  70. package/dist/midi/MidiDevices.d.ts.map +1 -0
  71. package/dist/{MidiDevices.js → midi/MidiDevices.js} +6 -5
  72. package/dist/processors.js +3 -3
  73. package/dist/processors.js.map +4 -4
  74. package/dist/project/Project.d.ts +2 -0
  75. package/dist/project/Project.d.ts.map +1 -1
  76. package/dist/project/Project.js +7 -1
  77. package/dist/project/ProjectApi.d.ts +9 -1
  78. package/dist/project/ProjectApi.d.ts.map +1 -1
  79. package/dist/project/ProjectApi.js +67 -3
  80. package/dist/project/ProjectBundle.d.ts +1 -1
  81. package/dist/project/ProjectBundle.d.ts.map +1 -1
  82. package/dist/project/ProjectBundle.js +10 -12
  83. package/dist/project/ProjectPaths.d.ts +4 -4
  84. package/dist/project/ProjectPaths.d.ts.map +1 -1
  85. package/dist/project/ProjectProfile.d.ts +2 -2
  86. package/dist/project/ProjectProfile.d.ts.map +1 -1
  87. package/dist/project/ProjectProfile.js +3 -3
  88. package/dist/project/ProjectSignals.d.ts +6 -0
  89. package/dist/project/ProjectSignals.d.ts.map +1 -0
  90. package/dist/project/ProjectSignals.js +4 -0
  91. package/dist/project/ProjectStorage.d.ts +23 -0
  92. package/dist/project/ProjectStorage.d.ts.map +1 -0
  93. package/dist/project/ProjectStorage.js +59 -0
  94. package/dist/samples/MainThreadSampleLoader.d.ts +2 -2
  95. package/dist/samples/MainThreadSampleLoader.d.ts.map +1 -1
  96. package/dist/samples/MainThreadSampleLoader.js +2 -3
  97. package/dist/samples/MainThreadSampleManager.d.ts +4 -4
  98. package/dist/samples/MainThreadSampleManager.d.ts.map +1 -1
  99. package/dist/samples/OpenSampleAPI.d.ts +4 -2
  100. package/dist/samples/OpenSampleAPI.d.ts.map +1 -1
  101. package/dist/samples/OpenSampleAPI.js +19 -2
  102. package/dist/samples/SampleAPI.d.ts +2 -2
  103. package/dist/samples/SampleAPI.d.ts.map +1 -1
  104. package/dist/samples/SampleImporter.d.ts +1 -1
  105. package/dist/samples/SampleImporter.d.ts.map +1 -1
  106. package/dist/samples/SampleProvider.d.ts +1 -1
  107. package/dist/samples/SampleProvider.d.ts.map +1 -1
  108. package/dist/samples/SampleStorage.d.ts +7 -5
  109. package/dist/samples/SampleStorage.d.ts.map +1 -1
  110. package/dist/samples/SampleStorage.js +31 -17
  111. package/dist/workers.js +2 -2
  112. package/dist/workers.js.map +4 -4
  113. package/package.json +14 -14
  114. package/dist/MidiDevices.d.ts.map +0 -1
  115. package/dist/Wav.d.ts +0 -6
  116. package/dist/Wav.d.ts.map +0 -1
  117. package/dist/Wav.js +0 -46
  118. /package/dist/{MidiDevices.d.ts → midi/MidiDevices.d.ts} +0 -0
@@ -0,0 +1,195 @@
1
+ import { asDefined, Errors, isDefined, isNull, Maps, panic, RuntimeNotifier, TimeSpan } from "@opendaw/lib-std";
2
+ import { Promises } from "@opendaw/lib-runtime";
3
+ export class CloudAuthManager {
4
+ static create() { return new CloudAuthManager(); }
5
+ static async #createCodes() {
6
+ const array = new Uint8Array(32);
7
+ crypto.getRandomValues(array);
8
+ const codeVerifier = btoa(String.fromCharCode(...array))
9
+ .replace(/\+/g, "-")
10
+ .replace(/\//g, "_")
11
+ .replace(/=/g, "");
12
+ const encoder = new TextEncoder();
13
+ const data = encoder.encode(codeVerifier);
14
+ const digest = await crypto.subtle.digest("SHA-256", data);
15
+ const codeChallenge = btoa(String.fromCharCode(...new Uint8Array(digest)))
16
+ .replace(/\+/g, "-")
17
+ .replace(/\//g, "_")
18
+ .replace(/=/g, "");
19
+ return { codeVerifier, codeChallenge };
20
+ }
21
+ static #ID = 0;
22
+ id = CloudAuthManager.#ID++;
23
+ #memoizedHandlers = new Map();
24
+ constructor() { }
25
+ async getHandler(service) {
26
+ const memo = Maps.createIfAbsent(this.#memoizedHandlers, service, service => {
27
+ switch (service) {
28
+ case "Dropbox": {
29
+ return Promises.memoizeAsync(this.#oauthDropbox.bind(this), TimeSpan.hours(1));
30
+ }
31
+ case "GoogleDrive": {
32
+ return Promises.memoizeAsync(this.#oauthGoogle.bind(this), TimeSpan.hours(1));
33
+ }
34
+ default:
35
+ return panic(`Unsupported service: ${service}`);
36
+ }
37
+ });
38
+ const handler = await memo();
39
+ const { status } = await Promises.tryCatch(handler.alive());
40
+ if (status === "rejected") {
41
+ this.#memoizedHandlers.delete(service);
42
+ return this.getHandler(service);
43
+ }
44
+ console.debug(`Handler for '${service}' is alive`);
45
+ return handler;
46
+ }
47
+ async #oauthPkceFlow(config) {
48
+ const redirectUri = `${location.origin}/auth-callback.html`;
49
+ const { codeVerifier, codeChallenge } = await CloudAuthManager.#createCodes();
50
+ const params = new URLSearchParams({
51
+ client_id: config.clientId,
52
+ response_type: "code",
53
+ redirect_uri: redirectUri,
54
+ scope: config.scope,
55
+ code_challenge: codeChallenge,
56
+ code_challenge_method: "S256",
57
+ ...(config.extraAuthParams ?? {})
58
+ });
59
+ const authUrl = `${config.authUrlBase}?${params.toString()}`;
60
+ console.debug("[CloudAuth] Opening auth window:", authUrl);
61
+ const authWindow = window.open(authUrl, "cloudAuth");
62
+ if (isNull(authWindow)) {
63
+ return Errors.warn("Failed to open authentication window. Please check popup blockers.");
64
+ }
65
+ const { resolve, reject, promise } = Promise.withResolvers();
66
+ const channel = new BroadcastChannel("auth-callback");
67
+ const dialog = RuntimeNotifier.progress({
68
+ headline: "Cloud Service",
69
+ message: "Please wait for authentication...",
70
+ cancel: () => reject("cancelled")
71
+ });
72
+ channel.onmessage = async (event) => {
73
+ const data = asDefined(event.data, "No data");
74
+ console.debug("[CloudAuth] Received via BroadcastChannel:", this.id, data);
75
+ if (data.type === "auth-callback" && isDefined(data.code)) {
76
+ console.debug("[CloudAuth] Processing code from BroadcastChannel...", data.type, data.code);
77
+ try {
78
+ const tokenParams = new URLSearchParams({
79
+ code: data.code,
80
+ grant_type: "authorization_code",
81
+ client_id: config.clientId,
82
+ redirect_uri: redirectUri,
83
+ code_verifier: codeVerifier,
84
+ ...(config.extraTokenParams ?? {})
85
+ });
86
+ const response = await fetch(config.tokenUrl, {
87
+ method: "POST",
88
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
89
+ body: tokenParams.toString()
90
+ });
91
+ if (!response.ok) {
92
+ const errorText = await response.text();
93
+ console.error("[CloudAuth] Token exchange error:", errorText);
94
+ return panic(`Token exchange failed: ${response.statusText}`);
95
+ }
96
+ const dataJson = await response.json();
97
+ const accessToken = dataJson.access_token;
98
+ if (!accessToken) {
99
+ return panic("No access_token in token response");
100
+ }
101
+ resolve(await this.#createHandler(config.service, accessToken));
102
+ }
103
+ catch (err) {
104
+ console.debug("[CloudAuth] Token exchange failed:", err);
105
+ reject(err);
106
+ }
107
+ }
108
+ else if (data.type === "closed") {
109
+ console.debug("[CloudAuth] Callback window closed");
110
+ reject(null);
111
+ }
112
+ };
113
+ return promise.finally(() => {
114
+ console.debug("[CloudAuth] Closing auth window");
115
+ authWindow.close();
116
+ dialog.terminate();
117
+ channel.close();
118
+ });
119
+ }
120
+ async #oauthDropbox() {
121
+ return this.#oauthPkceFlow({
122
+ service: "dropbox",
123
+ clientId: "jtehjzxaxf3bf1l",
124
+ authUrlBase: "https://www.dropbox.com/oauth2/authorize",
125
+ tokenUrl: "https://api.dropboxapi.com/oauth2/token",
126
+ scope: "", // Dropbox scope is optional
127
+ extraAuthParams: {
128
+ token_access_type: "offline"
129
+ }
130
+ });
131
+ }
132
+ async #oauthGoogle() {
133
+ const clientId = "628747153367-gt1oqcn3trr9l9a7jhigja6l1t3f1oik.apps.googleusercontent.com";
134
+ const scope = "https://www.googleapis.com/auth/drive.appdata";
135
+ const redirectUri = `${location.origin}/auth-callback.html`;
136
+ const params = new URLSearchParams({
137
+ client_id: clientId,
138
+ response_type: "token",
139
+ redirect_uri: redirectUri,
140
+ scope,
141
+ include_granted_scopes: "true",
142
+ prompt: "consent"
143
+ });
144
+ const authUrl = `https://accounts.google.com/o/oauth2/v2/auth?${params.toString()}`;
145
+ console.debug("[CloudAuth] Opening auth window:", authUrl);
146
+ const authWindow = window.open(authUrl, "cloudAuth");
147
+ if (isNull(authWindow)) {
148
+ return Errors.warn("Failed to open authentication window. Please check popup blockers.");
149
+ }
150
+ const { resolve, reject, promise } = Promise.withResolvers();
151
+ const channel = new BroadcastChannel("auth-callback");
152
+ const dialog = RuntimeNotifier.progress({
153
+ headline: "Google Drive",
154
+ message: "Please authorize access to app data...",
155
+ cancel: () => reject("cancelled")
156
+ });
157
+ channel.onmessage = async (event) => {
158
+ const data = asDefined(event.data, "No data");
159
+ console.debug("[CloudAuth] Received via BroadcastChannel:", this.id, data);
160
+ if (data.type === "auth-callback" && isDefined(data.access_token)) {
161
+ try {
162
+ const accessToken = data.access_token;
163
+ resolve(await this.#createHandler("google", accessToken));
164
+ }
165
+ catch (err) {
166
+ reject(err);
167
+ }
168
+ }
169
+ else if (data.type === "closed") {
170
+ console.debug("[CloudAuth] Callback window closed");
171
+ reject(null);
172
+ }
173
+ };
174
+ return promise.finally(() => {
175
+ console.debug("[CloudAuth] Closing auth window");
176
+ authWindow.close();
177
+ dialog.terminate();
178
+ channel.close();
179
+ });
180
+ }
181
+ async #createHandler(service, token) {
182
+ switch (service) {
183
+ case "dropbox": {
184
+ const { DropboxHandler } = await import("./DropboxHandler");
185
+ return new DropboxHandler(token);
186
+ }
187
+ case "google": {
188
+ const { GoogleDriveHandler } = await import("./GoogleDriveHandler");
189
+ return new GoogleDriveHandler(token);
190
+ }
191
+ default:
192
+ return panic(`Handler not implemented for service: ${service}`);
193
+ }
194
+ }
195
+ }
@@ -0,0 +1,8 @@
1
+ import { CloudHandler } from "./CloudHandler";
2
+ import { CloudAuthManager } from "./CloudAuthManager";
3
+ import { CloudService } from "./CloudService";
4
+ export declare namespace CloudBackup {
5
+ const backup: (cloudAuthManager: CloudAuthManager, service: CloudService) => Promise<void>;
6
+ const backupWithHandler: (cloudHandler: CloudHandler, service: CloudService) => Promise<void>;
7
+ }
8
+ //# sourceMappingURL=CloudBackup.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CloudBackup.d.ts","sourceRoot":"","sources":["../../src/clouds/CloudBackup.ts"],"names":[],"mappings":"AACA,OAAO,EAAC,YAAY,EAAC,MAAM,gBAAgB,CAAA;AAG3C,OAAO,EAAC,gBAAgB,EAAC,MAAM,oBAAoB,CAAA;AACnD,OAAO,EAAC,YAAY,EAAC,MAAM,gBAAgB,CAAA;AAG3C,yBAAiB,WAAW,CAAC;IAClB,MAAM,MAAM,GAAU,kBAAkB,gBAAgB,EAAE,SAAS,YAAY,kBA8BrF,CAAA;IAEM,MAAM,iBAAiB,GAAU,cAAc,YAAY,EAAE,SAAS,YAAY,kBAYxF,CAAA;CACJ"}
@@ -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"}